[object Object]

← back to Cli Printing Press

feat(cli): named-profile system for repeatable agent contexts

7140c3e72a63bcaf6057e07149e6817c3c30e267 · 2026-04-17 23:11:45 -0400 · Matt Van Horn

Every printed CLI ships a profile subcommand (save/use/list/show/delete)
backed by a JSON store at ~/.<cli>-pp-cli/profiles.json. --profile <name>
is a persistent root flag that overlays the profile's values onto flags
the caller has not set explicitly.

Precedence: explicit flag > env var > profile > default.

agent-context surfaces available_profiles so introspecting agents
discover saved personas at runtime.

Mapped from HeyGen's "Beacon" pattern (2026-04-13): same configuration
invoked day after day, different input each run.

Files touched

Diff

commit 7140c3e72a63bcaf6057e07149e6817c3c30e267
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date:   Fri Apr 17 23:11:45 2026 -0400

    feat(cli): named-profile system for repeatable agent contexts
    
    Every printed CLI ships a profile subcommand (save/use/list/show/delete)
    backed by a JSON store at ~/.<cli>-pp-cli/profiles.json. --profile <name>
    is a persistent root flag that overlays the profile's values onto flags
    the caller has not set explicitly.
    
    Precedence: explicit flag > env var > profile > default.
    
    agent-context surfaces available_profiles so introspecting agents
    discover saved personas at runtime.
    
    Mapped from HeyGen's "Beacon" pattern (2026-04-13): same configuration
    invoked day after day, different input each run.
---
 ...7-002-fix-private-library-registry-auth-plan.md |  72 +++++
 internal/generator/generator.go                    |   1 +
 internal/generator/generator_test.go               |   7 +-
 internal/generator/templates/agent_context.go.tmpl |  16 +-
 internal/generator/templates/profile.go.tmpl       | 328 +++++++++++++++++++++
 internal/generator/templates/root.go.tmpl          |  19 ++
 internal/generator/templates/skill.md.tmpl         |  14 +
 7 files changed, 449 insertions(+), 8 deletions(-)

diff --git a/docs/plans/2026-04-17-002-fix-private-library-registry-auth-plan.md b/docs/plans/2026-04-17-002-fix-private-library-registry-auth-plan.md
new file mode 100644
index 00000000..01033548
--- /dev/null
+++ b/docs/plans/2026-04-17-002-fix-private-library-registry-auth-plan.md
@@ -0,0 +1,72 @@
+---
+title: "fix: Private library registry auth for mega MCP"
+type: fix
+status: active
+date: 2026-04-17
+---
+
+# fix: Private library registry auth for mega MCP
+
+## Overview
+
+The `printing-press-mcp` binary fetches `registry.json` and per-API `tools-manifest.json` files from `raw.githubusercontent.com/mvanhorn/printing-press-library/main/...` via unauthenticated HTTP. The library repo is private, so every fetch returns 404 and the mega MCP ships with an empty tool catalog. Fix: attach `Authorization: token $GITHUB_TOKEN` when the env var is set. Same URL, same response shape, just with auth.
+
+## Problem Frame
+
+Two `http.Get` calls need the header:
+
+- `FetchRegistry` in `internal/megamcp/registry.go:18`
+- `fetchManifestData` in `internal/megamcp/manifest.go:283`
+
+Single user (the maintainer), single private repo, token read from `GITHUB_TOKEN` the same way `internal/crowdsniff/github.go` already does it.
+
+## Scope Boundaries
+
+- **In scope:** Add `Authorization` header to the two fetch calls. One test that the header gets set when `GITHUB_TOKEN` is present.
+- **Out of scope:** Shared auth helper, upgraded error diagnostics, doc reconciliation, tests that the token never appears in error strings, a `library_status` meta-tool, flipping repo visibility.
+
+## Key Technical Decisions
+
+- **Env var is `GITHUB_TOKEN`.** Matches `internal/crowdsniff/github.go:19,43` convention. No new env var.
+- **Inline at each call site.** Two fetches, ~5 lines each. A shared helper adds more surface than it saves.
+- **Keep existing error shapes.** `HTTP 404` on 404 is fine. Maintainer reading their own repo knows what that means.
+
+## Implementation Units
+
+- [ ] **Unit 1: Attach GITHUB_TOKEN auth header to registry and manifest fetches**
+
+**Goal:** Both outbound GETs carry `Authorization: token <value>` when `GITHUB_TOKEN` is set. Nothing else changes.
+
+**Files:**
+- Modify: `internal/megamcp/registry.go`
+- Modify: `internal/megamcp/manifest.go`
+- Modify: `internal/megamcp/registry_test.go` (add one auth-header assertion)
+
+**Approach:**
+- In both files, replace `http.Get(url)` with a `http.NewRequest("GET", url, nil)` + `http.DefaultClient.Do(req)` pair, and set the `Authorization` header when `os.Getenv("GITHUB_TOKEN")` is non-empty.
+- Use `token <value>` format (matches the crowdsniff package, not `Bearer`).
+- Don't wrap in a helper - two call sites isn't enough to justify the indirection.
+
+**Patterns to follow:**
+- `internal/crowdsniff/github.go` for the token-read + header-set pattern.
+
+**Test scenarios:**
+- Happy path: with `GITHUB_TOKEN` set via `t.Setenv`, the `httptest.NewServer` handler observes `Authorization: token <value>` on the inbound request. (Extend an existing `registry_test.go` case - don't add a new file.)
+
+**Verification:**
+- `go test ./internal/megamcp/...` passes.
+- Rebuild (`go install ./cmd/printing-press-mcp`), restart Claude Code, `library_info` returns a non-empty API list.
+
+## Risks & Dependencies
+
+| Risk | Mitigation |
+|------|------------|
+| Token missing or scope-insufficient still returns 404, with no hint why | Accepted. Maintainer context - you know what 404 means on a private repo read. |
+| Future contributor adds a third fetch and forgets the header | Accepted for now. Revisit if a third fetch path lands. |
+
+## Sources & References
+
+- `internal/megamcp/registry.go:15-39`
+- `internal/megamcp/manifest.go:282-299`
+- `internal/crowdsniff/github.go:17-44`
+- Repo visibility: `gh repo view mvanhorn/printing-press-library --json visibility` -> `PRIVATE` as of 2026-04-17.
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 37d2b3ec..fcb1c1c1 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -659,6 +659,7 @@ func (g *Generator) Generate() error {
 		"helpers.go.tmpl":        filepath.Join("internal", "cli", "helpers.go"),
 		"doctor.go.tmpl":         filepath.Join("internal", "cli", "doctor.go"),
 		"agent_context.go.tmpl":  filepath.Join("internal", "cli", "agent_context.go"),
+		"profile.go.tmpl":        filepath.Join("internal", "cli", "profile.go"),
 		"config.go.tmpl":         filepath.Join("internal", "config", "config.go"),
 		"cache.go.tmpl":          filepath.Join("internal", "cache", "cache.go"),
 		"client.go.tmpl":         filepath.Join("internal", "client", "client.go"),
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 4e98b587..bdbaed21 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -26,9 +26,10 @@ func TestGenerateProjectsCompile(t *testing.T) {
 	}{
 		// +3 for cliutil package: fanout.go, text.go, cliutil_test.go
 		// +1 for internal/cli/agent_context.go (Cloudflare-style runtime introspection)
-		{name: "stytch", specPath: filepath.Join("..", "..", "testdata", "stytch.yaml"), expectedFiles: 36},
-		{name: "clerk", specPath: filepath.Join("..", "..", "testdata", "clerk.yaml"), expectedFiles: 40},
-		{name: "loops", specPath: filepath.Join("..", "..", "testdata", "loops.yaml"), expectedFiles: 40},
+		// +1 for internal/cli/profile.go (HeyGen-style named-profile system)
+		{name: "stytch", specPath: filepath.Join("..", "..", "testdata", "stytch.yaml"), expectedFiles: 37},
+		{name: "clerk", specPath: filepath.Join("..", "..", "testdata", "clerk.yaml"), expectedFiles: 41},
+		{name: "loops", specPath: filepath.Join("..", "..", "testdata", "loops.yaml"), expectedFiles: 41},
 	}
 
 	for _, tt := range tests {
diff --git a/internal/generator/templates/agent_context.go.tmpl b/internal/generator/templates/agent_context.go.tmpl
index 4fb2b14b..97f691b3 100644
--- a/internal/generator/templates/agent_context.go.tmpl
+++ b/internal/generator/templates/agent_context.go.tmpl
@@ -22,10 +22,11 @@ const agentContextSchemaVersion = "1"
 // (2026-04-13 Wrangler post): agents can introspect the live CLI without
 // parsing --help or reading source.
 type agentContext struct {
-	SchemaVersion string                `json:"schema_version"`
-	CLI           agentContextCLI       `json:"cli"`
-	Auth          agentContextAuth      `json:"auth"`
-	Commands      []agentContextCommand `json:"commands"`
+	SchemaVersion     string                `json:"schema_version"`
+	CLI               agentContextCLI       `json:"cli"`
+	Auth              agentContextAuth      `json:"auth"`
+	Commands          []agentContextCommand `json:"commands"`
+	AvailableProfiles []string              `json:"available_profiles"`
 }
 
 type agentContextCLI struct {
@@ -85,6 +86,10 @@ func buildAgentContext(rootCmd *cobra.Command) agentContext {
 	if authMode == "" {
 		authMode = "none"
 	}
+	profiles := ListProfileNames()
+	if profiles == nil {
+		profiles = []string{}
+	}
 	return agentContext{
 		SchemaVersion: agentContextSchemaVersion,
 		CLI: agentContextCLI{
@@ -96,7 +101,8 @@ func buildAgentContext(rootCmd *cobra.Command) agentContext {
 			Mode:    authMode,
 			EnvVars: envVars,
 		},
-		Commands: collectAgentCommands(rootCmd),
+		Commands:          collectAgentCommands(rootCmd),
+		AvailableProfiles: profiles,
 	}
 }
 
diff --git a/internal/generator/templates/profile.go.tmpl b/internal/generator/templates/profile.go.tmpl
new file mode 100644
index 00000000..7a785e46
--- /dev/null
+++ b/internal/generator/templates/profile.go.tmpl
@@ -0,0 +1,328 @@
+// Copyright {{currentYear}} {{.Owner}}. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+)
+
+// Profile is a named set of flag values saved for reuse across invocations.
+// HeyGen's "Beacon" pattern: one named context that a scheduled agent reuses
+// day after day with the same voice/format but different input each run.
+type Profile struct {
+	Name        string            `json:"name"`
+	Description string            `json:"description,omitempty"`
+	Values      map[string]string `json:"values"`
+}
+
+type profileStore struct {
+	Profiles map[string]Profile `json:"profiles"`
+}
+
+func profileStorePath() (string, error) {
+	home, err := os.UserHomeDir()
+	if err != nil {
+		return "", fmt.Errorf("resolving home dir: %w", err)
+	}
+	dir := filepath.Join(home, ".{{.Name}}-pp-cli")
+	if err := os.MkdirAll(dir, 0o700); err != nil {
+		return "", fmt.Errorf("creating state dir: %w", err)
+	}
+	return filepath.Join(dir, "profiles.json"), nil
+}
+
+func loadProfileStore() (*profileStore, error) {
+	p, err := profileStorePath()
+	if err != nil {
+		return nil, err
+	}
+	data, err := os.ReadFile(p)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &profileStore{Profiles: map[string]Profile{}}, nil
+		}
+		return nil, fmt.Errorf("reading profiles: %w", err)
+	}
+	var s profileStore
+	if err := json.Unmarshal(data, &s); err != nil {
+		return nil, fmt.Errorf("parsing profiles: %w", err)
+	}
+	if s.Profiles == nil {
+		s.Profiles = map[string]Profile{}
+	}
+	return &s, nil
+}
+
+func saveProfileStore(s *profileStore) error {
+	p, err := profileStorePath()
+	if err != nil {
+		return err
+	}
+	data, err := json.MarshalIndent(s, "", "  ")
+	if err != nil {
+		return fmt.Errorf("marshaling profiles: %w", err)
+	}
+	tmp := p + ".tmp"
+	if err := os.WriteFile(tmp, data, 0o600); err != nil {
+		return fmt.Errorf("writing profiles: %w", err)
+	}
+	return os.Rename(tmp, p)
+}
+
+// GetProfile returns a profile by name, or (nil, nil) if not found.
+func GetProfile(name string) (*Profile, error) {
+	s, err := loadProfileStore()
+	if err != nil {
+		return nil, err
+	}
+	if p, ok := s.Profiles[name]; ok {
+		return &p, nil
+	}
+	return nil, nil
+}
+
+// ApplyProfileToFlags overlays profile values onto flags that the user has
+// not set explicitly on the command line. Used from root.go's
+// PersistentPreRunE so profile values feed the whole command tree.
+func ApplyProfileToFlags(cmd *cobra.Command, profile *Profile) error {
+	if profile == nil || len(profile.Values) == 0 {
+		return nil
+	}
+	// Reserved flags that never come from a profile - they control profile
+	// resolution itself or are dangerous to overlay.
+	reserved := map[string]bool{
+		"profile": true, "config": true, "help": true,
+	}
+	for name, value := range profile.Values {
+		if reserved[name] {
+			continue
+		}
+		flag := cmd.Flags().Lookup(name)
+		if flag == nil {
+			flag = cmd.InheritedFlags().Lookup(name)
+		}
+		if flag == nil {
+			continue
+		}
+		if flag.Changed {
+			continue
+		}
+		if err := flag.Value.Set(value); err != nil {
+			return fmt.Errorf("applying profile value %s=%q: %w", name, value, err)
+		}
+	}
+	return nil
+}
+
+// ListProfileNames returns profile names sorted alphabetically. Used by the
+// agent-context subcommand to expose available_profiles at runtime.
+func ListProfileNames() []string {
+	s, err := loadProfileStore()
+	if err != nil {
+		return nil
+	}
+	names := make([]string, 0, len(s.Profiles))
+	for name := range s.Profiles {
+		names = append(names, name)
+	}
+	sort.Strings(names)
+	return names
+}
+
+func newProfileCmd(flags *rootFlags) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "profile",
+		Short: "Named sets of flags saved for reuse",
+		Long: `Profiles capture a set of flag values under a name so a scheduled
+agent can invoke the same command with the same configuration each run.
+
+  profile save <name>         captures the current invocation's set flags
+  profile use <name>          prints the values (for inspection)
+  profile list                lists all saved profiles
+  profile show <name>         shows the values of one profile
+  profile delete <name>       removes a profile
+
+Use --profile <name> on any command to apply that profile's values.
+Explicit flags override profile values.`,
+	}
+	cmd.AddCommand(newProfileSaveCmd(flags))
+	cmd.AddCommand(newProfileUseCmd(flags))
+	cmd.AddCommand(newProfileListCmd(flags))
+	cmd.AddCommand(newProfileShowCmd(flags))
+	cmd.AddCommand(newProfileDeleteCmd(flags))
+	return cmd
+}
+
+func newProfileSaveCmd(flags *rootFlags) *cobra.Command {
+	var description string
+	cmd := &cobra.Command{
+		Use:   "save <name> [--<flag> <value> ...]",
+		Short: "Save the current invocation's non-default flags as a named profile",
+		Long: `Captures every flag explicitly set on the invocation and stores
+them under <name>. To update an existing profile, run save again; the
+entry is replaced.
+
+To avoid creating empty profiles, at least one non-default flag must be
+present (other than --profile and --config).`,
+		Args: cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			name := args[0]
+			if strings.ContainsAny(name, `/\: `) {
+				return fmt.Errorf("profile name %q contains reserved characters", name)
+			}
+			values := map[string]string{}
+			// Walk inherited + local flags, capture only those the user set.
+			skip := map[string]bool{"profile": true, "config": true, "help": true, "description": true}
+			visit := func(fl *pflag.Flag) {
+				if fl.Changed && !skip[fl.Name] {
+					values[fl.Name] = fl.Value.String()
+				}
+			}
+			cmd.InheritedFlags().VisitAll(visit)
+			cmd.Flags().VisitAll(visit)
+			if len(values) == 0 {
+				return fmt.Errorf("no non-default flags set - pass at least one flag to save into %q", name)
+			}
+			s, err := loadProfileStore()
+			if err != nil {
+				return err
+			}
+			s.Profiles[name] = Profile{Name: name, Description: description, Values: values}
+			if err := saveProfileStore(s); err != nil {
+				return err
+			}
+			if flags.asJSON {
+				return flags.printJSON(cmd, s.Profiles[name])
+			}
+			fmt.Fprintf(cmd.OutOrStdout(), "saved profile %q with %d values\n", name, len(values))
+			return nil
+		},
+	}
+	cmd.Flags().StringVar(&description, "description", "", "Short description shown in 'profile list'")
+	return cmd
+}
+
+func newProfileUseCmd(flags *rootFlags) *cobra.Command {
+	return &cobra.Command{
+		Use:   "use <name>",
+		Short: "Print the flag values a profile will apply (does not execute anything)",
+		Args:  cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			p, err := GetProfile(args[0])
+			if err != nil {
+				return err
+			}
+			if p == nil {
+				return fmt.Errorf("profile %q not found", args[0])
+			}
+			if flags.asJSON {
+				return flags.printJSON(cmd, p)
+			}
+			fmt.Fprintf(cmd.OutOrStdout(), "profile %q:\n", p.Name)
+			if p.Description != "" {
+				fmt.Fprintf(cmd.OutOrStdout(), "  description: %s\n", p.Description)
+			}
+			keys := make([]string, 0, len(p.Values))
+			for k := range p.Values {
+				keys = append(keys, k)
+			}
+			sort.Strings(keys)
+			for _, k := range keys {
+				fmt.Fprintf(cmd.OutOrStdout(), "  --%s %s\n", k, p.Values[k])
+			}
+			return nil
+		},
+	}
+}
+
+func newProfileListCmd(flags *rootFlags) *cobra.Command {
+	return &cobra.Command{
+		Use:   "list",
+		Short: "List saved profiles",
+		RunE: func(cmd *cobra.Command, _ []string) error {
+			s, err := loadProfileStore()
+			if err != nil {
+				return err
+			}
+			names := make([]string, 0, len(s.Profiles))
+			for n := range s.Profiles {
+				names = append(names, n)
+			}
+			sort.Strings(names)
+			if flags.asJSON {
+				out := make([]map[string]any, 0, len(names))
+				for _, n := range names {
+					p := s.Profiles[n]
+					out = append(out, map[string]any{
+						"name":        p.Name,
+						"description": p.Description,
+						"field_count": len(p.Values),
+					})
+				}
+				return flags.printJSON(cmd, out)
+			}
+			headers := []string{"NAME", "FIELDS", "DESCRIPTION"}
+			rows := make([][]string, 0, len(names))
+			for _, n := range names {
+				p := s.Profiles[n]
+				rows = append(rows, []string{p.Name, fmt.Sprintf("%d", len(p.Values)), p.Description})
+			}
+			return flags.printTable(cmd, headers, rows)
+		},
+	}
+}
+
+func newProfileShowCmd(flags *rootFlags) *cobra.Command {
+	return &cobra.Command{
+		Use:   "show <name>",
+		Short: "Show a profile's values as JSON",
+		Args:  cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			p, err := GetProfile(args[0])
+			if err != nil {
+				return err
+			}
+			if p == nil {
+				return fmt.Errorf("profile %q not found", args[0])
+			}
+			return flags.printJSON(cmd, p)
+		},
+	}
+}
+
+func newProfileDeleteCmd(flags *rootFlags) *cobra.Command {
+	return &cobra.Command{
+		Use:   "delete <name>",
+		Short: "Remove a profile",
+		Args:  cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			name := args[0]
+			s, err := loadProfileStore()
+			if err != nil {
+				return err
+			}
+			if _, ok := s.Profiles[name]; !ok {
+				return fmt.Errorf("profile %q not found", name)
+			}
+			if !flags.yes {
+				fmt.Fprintf(cmd.ErrOrStderr(), "refusing to delete %q without --yes\n", name)
+				return fmt.Errorf("confirmation required: pass --yes")
+			}
+			delete(s.Profiles, name)
+			if err := saveProfileStore(s); err != nil {
+				return err
+			}
+			fmt.Fprintf(cmd.OutOrStdout(), "deleted profile %q\n", name)
+			return nil
+		},
+	}
+}
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index 2353582c..6dbe0fb6 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -30,6 +30,7 @@ type rootFlags struct {
 	agent        bool
 	selectFields string
 	configPath   string
+	profileName  string
 	timeout      time.Duration
 	rateLimit    float64
 	dataSource   string
@@ -100,6 +101,7 @@ Run '{{.Name}}-pp-cli doctor' to verify auth and connectivity.{{backtick}},
 	rootCmd.PersistentFlags().BoolVar(&humanFriendly, "human-friendly", false, "Enable colored output and rich formatting")
 	rootCmd.PersistentFlags().BoolVar(&flags.agent, "agent", false, "Set all agent-friendly defaults (--json --compact --no-input --no-color --yes)")
 	rootCmd.PersistentFlags().StringVar(&flags.dataSource, "data-source", "auto", "Data source for read commands: auto (live with local fallback), live (API only), local (synced data only)")
+	rootCmd.PersistentFlags().StringVar(&flags.profileName, "profile", "", "Apply values from a saved profile (see '{{.Name}}-pp-cli profile list')")
 {{- if eq .SpecSource "sniffed"}}
 	rootCmd.PersistentFlags().Float64Var(&flags.rateLimit, "rate-limit", 2, "Max requests per second (0 to disable, default 2 for sniffed APIs)")
 {{- else}}
@@ -107,6 +109,22 @@ Run '{{.Name}}-pp-cli doctor' to verify auth and connectivity.{{backtick}},
 {{- end}}
 
 	rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
+		if flags.profileName != "" {
+			profile, err := GetProfile(flags.profileName)
+			if err != nil {
+				return err
+			}
+			if profile == nil {
+				available := ListProfileNames()
+				if len(available) == 0 {
+					return fmt.Errorf("profile %q not found (no profiles saved yet; run '%s profile save <name> --<flag> <value>')", flags.profileName, cmd.Root().Name())
+				}
+				return fmt.Errorf("profile %q not found; available: %s", flags.profileName, strings.Join(available, ", "))
+			}
+			if err := ApplyProfileToFlags(cmd, profile); err != nil {
+				return err
+			}
+		}
 		if flags.agent {
 			if !cmd.Flags().Changed("json") {
 				flags.asJSON = true
@@ -141,6 +159,7 @@ Run '{{.Name}}-pp-cli doctor' to verify auth and connectivity.{{backtick}},
 	rootCmd.AddCommand(newDoctorCmd(&flags))
 	rootCmd.AddCommand(newAuthCmd(&flags))
 	rootCmd.AddCommand(newAgentContextCmd(rootCmd))
+	rootCmd.AddCommand(newProfileCmd(&flags))
 {{- if .HasAsyncJobs}}
 	rootCmd.AddCommand(newJobsCmd(&flags))
 {{- end}}
diff --git a/internal/generator/templates/skill.md.tmpl b/internal/generator/templates/skill.md.tmpl
index 0969497c..ccf56bc6 100644
--- a/internal/generator/templates/skill.md.tmpl
+++ b/internal/generator/templates/skill.md.tmpl
@@ -146,6 +146,20 @@ Add `--agent` to any command. Expands to: `--json --compact --no-input --no-colo
 - **Cacheable** — GET responses cached for 5 minutes, bypass with `--no-cache`
 - **Non-interactive** — never prompts, every input is a flag
 
+## Named Profiles
+
+A profile is a saved set of flag values, reused across invocations. Use it when a scheduled agent calls the same command every run with the same configuration - HeyGen's "Beacon" pattern.
+
+```
+<cli>-pp-cli profile save briefing --<flag> <value> ...
+<cli>-pp-cli --profile briefing <command> ...
+<cli>-pp-cli profile list [--json]
+<cli>-pp-cli profile show briefing
+<cli>-pp-cli profile delete briefing --yes
+```
+
+Explicit flags always win over profile values; profile values win over defaults. `agent-context` lists all available profiles under `available_profiles` so introspecting agents discover them at runtime.
+
 ## Async Jobs
 
 For endpoints that submit long-running work, the generator detects the submit-then-poll pattern (a `job_id`/`task_id`/`operation_id` field in the response plus a sibling status endpoint) and wires up three extra flags on the submitting command:

← 1c172850 feat(cli): async-job detection, --wait flag, jobs command  ·  back to Cli Printing Press  ·  feat(cli): --deliver routes command output to file or webhoo f6e74931 →