Add context-free CLI processing
- commit
- 086a9fe2504703281cca08352012f4d03b3dd9ba
- author
- patriceckhart · · 2026-08-06 15:02 UTC
- parent
- 7859d9c
- signature
- unsigned
- refs
- tag: v0.3.36
8 files · +98 -17
M README.md +3 -0
| @@ -201,6 +201,7 @@ The first time you launch a newer zot binary, the TUI shows the GitHub release n | |||
| 201 | 201 | zot # interactive tui | |
| 202 | 202 | zot "fix the failing test" # tui, pre-filled prompt | |
| 203 | 203 | zot -p "list all go files" # print final text, exit | |
| 204 | + | cat README.md | zot -p "summarize this text" # combine stdin with the prompt | |
| 204 | 205 | zot -p --stats stats.json "task" # print final text and write generation stats | |
| 205 | 206 | zot --json "refactor main.go" # newline-delimited json events, exit | |
| 206 | 207 | zot --continue # resume the most recent session for this cwd | |
| @@ -235,6 +236,7 @@ Print-mode stats contain `provider`, `model`, `prompt_tokens`, `reasoning_tokens | |||
| 235 | 236 | | `-e`, `--ext <path>` | Load an extension from `<path>` for this run (repeatable; wins against installed extensions of the same name). | | |
| 236 | 237 | | `--no-ext` | Skip extension discovery for this run. `--ext` still works on top, so `--no-ext --ext ./x` runs only `x`. | | |
| 237 | 238 | | `--no-skill` | Disable all skills, including built-ins. No `skill` tool is registered and the system prompt has no skill manifest. | | |
| 239 | + | | `--no-context-files`, `-nc` | Disable discovery and loading of global and project `AGENTS.md` files. | | |
| 238 | 240 | | `--no-yolo` | Confirm every tool call before it runs (interactive TUI only). A dialog shows the tool name and a one-line preview of its args, while edit calls show the proposed diff in the tool panel, with four choices: yes, yes-always-this-tool-this-session, yes-always-this-session, no. Press `/` while the dialog is focused to run a slash command first; closing its input or child dialog returns to the pending confirmation. Ignored with a stderr warning in print / json / rpc modes, where tools still run freely so scripts and automation keep working. | | |
| 239 | 241 | ||
| 240 | 242 | ## Tools | |
| @@ -251,6 +253,7 @@ When the sandbox is on (see `/jail`), all four tools refuse paths outside the se | |||
| 251 | 253 | - **Interactive** (default): chat TUI with streaming output, spinner, cost meter, slash commands. | |
| 252 | 254 | - **Print**: `zot -p "prompt"` runs the agent to completion and writes only the final assistant text to stdout. | |
| 253 | 255 | - **Stream**: `zot --stream "prompt"` runs without the TUI and writes assistant text to stdout as it arrives. Tool activity goes to stderr. | |
| 256 | + | - **Piped input**: print, stream, and JSON modes prepend piped stdin to the positional prompt, separated by a newline. For example, `cat README.md | zot -p "summarize this text"`. | |
| 254 | 257 | - **JSON**: `zot --json "prompt"` emits one JSON object per agent event to stdout, newline-delimited. The schema is documented in [docs/rpc.md](docs/rpc.md). | |
| 255 | 258 | - **RPC**: `zot rpc` runs as a long-lived child process; commands in on stdin, events and responses out on stdout, both as NDJSON. Designed for embedding zot in third-party apps written in any language. See [docs/rpc.md](docs/rpc.md) for the wire schema and `examples/rpc/{python,node,shell,go}` for working clients. | |
| 256 | 259 | ||
| @@ -201,6 +201,7 @@ The first time you launch a newer zot binary, the TUI shows the GitHub release n | |||||
| 201 | zot # interactive tui | 201 | zot # interactive tui | ||
| 202 | zot "fix the failing test" # tui, pre-filled prompt | 202 | zot "fix the failing test" # tui, pre-filled prompt | ||
| 203 | zot -p "list all go files" # print final text, exit | 203 | zot -p "list all go files" # print final text, exit | ||
| 204 | + | cat README.md | zot -p "summarize this text" # combine stdin with the prompt | |||
| 204 | zot -p --stats stats.json "task" # print final text and write generation stats | 205 | zot -p --stats stats.json "task" # print final text and write generation stats | ||
| 205 | zot --json "refactor main.go" # newline-delimited json events, exit | 206 | zot --json "refactor main.go" # newline-delimited json events, exit | ||
| 206 | zot --continue # resume the most recent session for this cwd | 207 | zot --continue # resume the most recent session for this cwd | ||
| @@ -235,6 +236,7 @@ Print-mode stats contain `provider`, `model`, `prompt_tokens`, `reasoning_tokens | |||||
| 235 | | `-e`, `--ext <path>` | Load an extension from `<path>` for this run (repeatable; wins against installed extensions of the same name). | | 236 | | `-e`, `--ext <path>` | Load an extension from `<path>` for this run (repeatable; wins against installed extensions of the same name). | | ||
| 236 | | `--no-ext` | Skip extension discovery for this run. `--ext` still works on top, so `--no-ext --ext ./x` runs only `x`. | | 237 | | `--no-ext` | Skip extension discovery for this run. `--ext` still works on top, so `--no-ext --ext ./x` runs only `x`. | | ||
| 237 | | `--no-skill` | Disable all skills, including built-ins. No `skill` tool is registered and the system prompt has no skill manifest. | | 238 | | `--no-skill` | Disable all skills, including built-ins. No `skill` tool is registered and the system prompt has no skill manifest. | | ||
| 239 | + | | `--no-context-files`, `-nc` | Disable discovery and loading of global and project `AGENTS.md` files. | | |||
| 238 | | `--no-yolo` | Confirm every tool call before it runs (interactive TUI only). A dialog shows the tool name and a one-line preview of its args, while edit calls show the proposed diff in the tool panel, with four choices: yes, yes-always-this-tool-this-session, yes-always-this-session, no. Press `/` while the dialog is focused to run a slash command first; closing its input or child dialog returns to the pending confirmation. Ignored with a stderr warning in print / json / rpc modes, where tools still run freely so scripts and automation keep working. | | 240 | | `--no-yolo` | Confirm every tool call before it runs (interactive TUI only). A dialog shows the tool name and a one-line preview of its args, while edit calls show the proposed diff in the tool panel, with four choices: yes, yes-always-this-tool-this-session, yes-always-this-session, no. Press `/` while the dialog is focused to run a slash command first; closing its input or child dialog returns to the pending confirmation. Ignored with a stderr warning in print / json / rpc modes, where tools still run freely so scripts and automation keep working. | | ||
| 239 | 241 | ||||
| 240 | ## Tools | 242 | ## Tools | ||
| @@ -251,6 +253,7 @@ When the sandbox is on (see `/jail`), all four tools refuse paths outside the se | |||||
| 251 | - **Interactive** (default): chat TUI with streaming output, spinner, cost meter, slash commands. | 253 | - **Interactive** (default): chat TUI with streaming output, spinner, cost meter, slash commands. | ||
| 252 | - **Print**: `zot -p "prompt"` runs the agent to completion and writes only the final assistant text to stdout. | 254 | - **Print**: `zot -p "prompt"` runs the agent to completion and writes only the final assistant text to stdout. | ||
| 253 | - **Stream**: `zot --stream "prompt"` runs without the TUI and writes assistant text to stdout as it arrives. Tool activity goes to stderr. | 255 | - **Stream**: `zot --stream "prompt"` runs without the TUI and writes assistant text to stdout as it arrives. Tool activity goes to stderr. | ||
| 256 | + | - **Piped input**: print, stream, and JSON modes prepend piped stdin to the positional prompt, separated by a newline. For example, `cat README.md | zot -p "summarize this text"`. | |||
| 254 | - **JSON**: `zot --json "prompt"` emits one JSON object per agent event to stdout, newline-delimited. The schema is documented in [docs/rpc.md](docs/rpc.md). | 257 | - **JSON**: `zot --json "prompt"` emits one JSON object per agent event to stdout, newline-delimited. The schema is documented in [docs/rpc.md](docs/rpc.md). | ||
| 255 | - **RPC**: `zot rpc` runs as a long-lived child process; commands in on stdin, events and responses out on stdout, both as NDJSON. Designed for embedding zot in third-party apps written in any language. See [docs/rpc.md](docs/rpc.md) for the wire schema and `examples/rpc/{python,node,shell,go}` for working clients. | 258 | - **RPC**: `zot rpc` runs as a long-lived child process; commands in on stdin, events and responses out on stdout, both as NDJSON. Designed for embedding zot in third-party apps written in any language. See [docs/rpc.md](docs/rpc.md) for the wire schema and `examples/rpc/{python,node,shell,go}` for working clients. | ||
| 256 | 259 | ||||
M docs/rpc.md +1 -1
| @@ -23,7 +23,7 @@ You'll see one JSON object per line on stdout: a response acknowledging the prom | |||
| 23 | 23 | ||
| 24 | 24 | ## Flags | |
| 25 | 25 | ||
| 26 | - | `zot rpc` accepts the same flags as the other modes: `--provider`, `--model`, `--cwd`, `--api-key`, `--base-url`, `--system-prompt`, `--append-system-prompt`, `--reasoning`, `--max-steps`, `--no-tools`, `--tools`. Sessions are disabled by default in RPC mode — the embedding application owns persistence. | |
| 26 | + | `zot rpc` accepts the same flags as the other modes: `--provider`, `--model`, `--cwd`, `--api-key`, `--base-url`, `--system-prompt`, `--append-system-prompt`, `--reasoning`, `--max-steps`, `--no-tools`, `--tools`, `--no-context-files`. Sessions are disabled by default in RPC mode; the embedding application owns persistence. | |
| 27 | 27 | ||
| 28 | 28 | ## Auth | |
| 29 | 29 | ||
| @@ -23,7 +23,7 @@ You'll see one JSON object per line on stdout: a response acknowledging the prom | |||||
| 23 | 23 | ||||
| 24 | ## Flags | 24 | ## Flags | ||
| 25 | 25 | ||||
| 26 | - | `zot rpc` accepts the same flags as the other modes: `--provider`, `--model`, `--cwd`, `--api-key`, `--base-url`, `--system-prompt`, `--append-system-prompt`, `--reasoning`, `--max-steps`, `--no-tools`, `--tools`. Sessions are disabled by default in RPC mode — the embedding application owns persistence. | 26 | + | `zot rpc` accepts the same flags as the other modes: `--provider`, `--model`, `--cwd`, `--api-key`, `--base-url`, `--system-prompt`, `--append-system-prompt`, `--reasoning`, `--max-steps`, `--no-tools`, `--tools`, `--no-context-files`. Sessions are disabled by default in RPC mode; the embedding application owns persistence. |
| 27 | 27 | ||||
| 28 | ## Auth | 28 | ## Auth | ||
| 29 | 29 | ||||
M packages/agent/args.go +7 -0
| @@ -82,6 +82,10 @@ type Args struct { | |||
| 82 | 82 | // skill discovery, including built-ins. | |
| 83 | 83 | WithSkills bool | |
| 84 | 84 | ||
| 85 | + | // NoContextFiles disables discovery and loading of AGENTS.md files | |
| 86 | + | // from $ZOT_HOME and the cwd's ancestor directories. | |
| 87 | + | NoContextFiles bool | |
| 88 | + | ||
| 85 | 89 | // InsecureTLS skips TLS verification for custom inference endpoints. | |
| 86 | 90 | InsecureTLS bool | |
| 87 | 91 | ||
| @@ -217,6 +221,8 @@ func ParseArgs(in []string) (Args, error) { | |||
| 217 | 221 | a.NoExt = true | |
| 218 | 222 | case "--no-skill", "--no-skills": | |
| 219 | 223 | a.NoSkill = true | |
| 224 | + | case "--no-context-files", "-nc": | |
| 225 | + | a.NoContextFiles = true | |
| 220 | 226 | case "--with-skills", "--with-skill": | |
| 221 | 227 | // Deprecated no-op: user skills are loaded by default. | |
| 222 | 228 | a.WithSkills = true | |
| @@ -438,6 +444,7 @@ func PrintHelp(version string) { | |||
| 438 | 444 | row{"-y, --yes", "accept zot run consent without prompting"}, | |
| 439 | 445 | row{"--no-ext", "skip extension discovery for this run"}, | |
| 440 | 446 | row{"--no-skill", "skip all skill discovery for this run"}, | |
| 447 | + | row{"--no-context-files, -nc", "skip AGENTS.md discovery for this run"}, | |
| 441 | 448 | ) | |
| 442 | 449 | section("misc", | |
| 443 | 450 | row{"--max-steps N", "agent loop iteration cap (default: unlimited)"}, | |
| @@ -82,6 +82,10 @@ type Args struct { | |||||
| 82 | // skill discovery, including built-ins. | 82 | // skill discovery, including built-ins. | ||
| 83 | WithSkills bool | 83 | WithSkills bool | ||
| 84 | 84 | ||||
| 85 | + | // NoContextFiles disables discovery and loading of AGENTS.md files | |||
| 86 | + | // from $ZOT_HOME and the cwd's ancestor directories. | |||
| 87 | + | NoContextFiles bool | |||
| 88 | + | ||||
| 85 | // InsecureTLS skips TLS verification for custom inference endpoints. | 89 | // InsecureTLS skips TLS verification for custom inference endpoints. | ||
| 86 | InsecureTLS bool | 90 | InsecureTLS bool | ||
| 87 | 91 | ||||
| @@ -217,6 +221,8 @@ func ParseArgs(in []string) (Args, error) { | |||||
| 217 | a.NoExt = true | 221 | a.NoExt = true | ||
| 218 | case "--no-skill", "--no-skills": | 222 | case "--no-skill", "--no-skills": | ||
| 219 | a.NoSkill = true | 223 | a.NoSkill = true | ||
| 224 | + | case "--no-context-files", "-nc": | |||
| 225 | + | a.NoContextFiles = true | |||
| 220 | case "--with-skills", "--with-skill": | 226 | case "--with-skills", "--with-skill": | ||
| 221 | // Deprecated no-op: user skills are loaded by default. | 227 | // Deprecated no-op: user skills are loaded by default. | ||
| 222 | a.WithSkills = true | 228 | a.WithSkills = true | ||
| @@ -438,6 +444,7 @@ func PrintHelp(version string) { | |||||
| 438 | row{"-y, --yes", "accept zot run consent without prompting"}, | 444 | row{"-y, --yes", "accept zot run consent without prompting"}, | ||
| 439 | row{"--no-ext", "skip extension discovery for this run"}, | 445 | row{"--no-ext", "skip extension discovery for this run"}, | ||
| 440 | row{"--no-skill", "skip all skill discovery for this run"}, | 446 | row{"--no-skill", "skip all skill discovery for this run"}, | ||
| 447 | + | row{"--no-context-files, -nc", "skip AGENTS.md discovery for this run"}, | |||
| 441 | ) | 448 | ) | ||
| 442 | section("misc", | 449 | section("misc", | ||
| 443 | row{"--max-steps N", "agent loop iteration cap (default: unlimited)"}, | 450 | row{"--max-steps N", "agent loop iteration cap (default: unlimited)"}, | ||
M packages/agent/args_test.go +12 -0
| @@ -47,6 +47,18 @@ func TestParseArgsStatsRequiresPrintMode(t *testing.T) { | |||
| 47 | 47 | } | |
| 48 | 48 | } | |
| 49 | 49 | ||
| 50 | + | func TestParseArgsNoContextFiles(t *testing.T) { | |
| 51 | + | for _, flag := range []string{"--no-context-files", "-nc"} { | |
| 52 | + | args, err := ParseArgs([]string{flag}) | |
| 53 | + | if err != nil { | |
| 54 | + | t.Fatalf("ParseArgs(%q): %v", flag, err) | |
| 55 | + | } | |
| 56 | + | if !args.NoContextFiles { | |
| 57 | + | t.Fatalf("ParseArgs(%q): NoContextFiles = false", flag) | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | ||
| 50 | 62 | func TestParseArgsStream(t *testing.T) { | |
| 51 | 63 | args, err := ParseArgs([]string{"--stream", "hi"}) | |
| 52 | 64 | if err != nil { | |
| @@ -47,6 +47,18 @@ func TestParseArgsStatsRequiresPrintMode(t *testing.T) { | |||||
| 47 | } | 47 | } | ||
| 48 | } | 48 | } | ||
| 49 | 49 | ||||
| 50 | + | func TestParseArgsNoContextFiles(t *testing.T) { | |||
| 51 | + | for _, flag := range []string{"--no-context-files", "-nc"} { | |||
| 52 | + | args, err := ParseArgs([]string{flag}) | |||
| 53 | + | if err != nil { | |||
| 54 | + | t.Fatalf("ParseArgs(%q): %v", flag, err) | |||
| 55 | + | } | |||
| 56 | + | if !args.NoContextFiles { | |||
| 57 | + | t.Fatalf("ParseArgs(%q): NoContextFiles = false", flag) | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | ||||
| 50 | func TestParseArgsStream(t *testing.T) { | 62 | func TestParseArgsStream(t *testing.T) { | ||
| 51 | args, err := ParseArgs([]string{"--stream", "hi"}) | 63 | args, err := ParseArgs([]string{"--stream", "hi"}) | ||
| 52 | if err != nil { | 64 | if err != nil { | ||
M packages/agent/build.go +4 -1
| @@ -599,7 +599,10 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 599 | 599 | ||
| 600 | 600 | summaries := toolSummaries(reg, args) | |
| 601 | 601 | ||
| 602 | - | contextFiles := loadAgentsContext(args.CWD, ZotHome()) | |
| 602 | + | var contextFiles []ContextFile | |
| 603 | + | if !args.NoContextFiles { | |
| 604 | + | contextFiles = loadAgentsContext(args.CWD, ZotHome()) | |
| 605 | + | } | |
| 603 | 606 | append_ := append([]string(nil), args.AppendSystemPrompt...) | |
| 604 | 607 | if agentsAddendum := formatAgentsContext(contextFiles); agentsAddendum != "" { | |
| 605 | 608 | append_ = append(append_, agentsAddendum) | |
| @@ -599,7 +599,10 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 599 | 599 | ||||
| 600 | summaries := toolSummaries(reg, args) | 600 | summaries := toolSummaries(reg, args) | ||
| 601 | 601 | ||||
| 602 | - | contextFiles := loadAgentsContext(args.CWD, ZotHome()) | 602 | + | var contextFiles []ContextFile |
| 603 | + | if !args.NoContextFiles { | |||
| 604 | + | contextFiles = loadAgentsContext(args.CWD, ZotHome()) | |||
| 605 | + | } | |||
| 603 | append_ := append([]string(nil), args.AppendSystemPrompt...) | 606 | append_ := append([]string(nil), args.AppendSystemPrompt...) | ||
| 604 | if agentsAddendum := formatAgentsContext(contextFiles); agentsAddendum != "" { | 607 | if agentsAddendum := formatAgentsContext(contextFiles); agentsAddendum != "" { | ||
| 605 | append_ = append(append_, agentsAddendum) | 608 | append_ = append(append_, agentsAddendum) | ||
M packages/agent/build_test.go +29 -0
| @@ -69,6 +69,35 @@ func TestReadAgentsContextMissingFilesIsEmpty(t *testing.T) { | |||
| 69 | 69 | } | |
| 70 | 70 | } | |
| 71 | 71 | ||
| 72 | + | func TestResolveNoContextFilesSkipsAgentsInstructions(t *testing.T) { | |
| 73 | + | zotHome := t.TempDir() | |
| 74 | + | cwd := t.TempDir() | |
| 75 | + | t.Setenv("ZOT_HOME", zotHome) | |
| 76 | + | t.Setenv("OPENAI_API_KEY", "test-key") | |
| 77 | + | if err := os.WriteFile(filepath.Join(zotHome, "AGENTS.md"), []byte("global rule"), 0o644); err != nil { | |
| 78 | + | t.Fatal(err) | |
| 79 | + | } | |
| 80 | + | if err := os.WriteFile(filepath.Join(cwd, "AGENTS.md"), []byte("project rule"), 0o644); err != nil { | |
| 81 | + | t.Fatal(err) | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | r, err := Resolve(Args{ | |
| 85 | + | Provider: "openai", | |
| 86 | + | Model: "gpt-5", | |
| 87 | + | CWD: cwd, | |
| 88 | + | NoContextFiles: true, | |
| 89 | + | }, false) | |
| 90 | + | if err != nil { | |
| 91 | + | t.Fatal(err) | |
| 92 | + | } | |
| 93 | + | if len(r.ContextFiles) != 0 { | |
| 94 | + | t.Fatalf("ContextFiles = %#v, want empty", r.ContextFiles) | |
| 95 | + | } | |
| 96 | + | if strings.Contains(r.SystemPrompt, "global rule") || strings.Contains(r.SystemPrompt, "project rule") { | |
| 97 | + | t.Fatalf("system prompt contains disabled context files:\n%s", r.SystemPrompt) | |
| 98 | + | } | |
| 99 | + | } | |
| 100 | + | ||
| 72 | 101 | // TestResolveFallsBackWhenConfiguredModelIsGone reproduces the | |
| 73 | 102 | // startup failure caught by the user's screenshot: the persisted | |
| 74 | 103 | // config.json points at a model id that's no longer in the active | |
| @@ -69,6 +69,35 @@ func TestReadAgentsContextMissingFilesIsEmpty(t *testing.T) { | |||||
| 69 | } | 69 | } | ||
| 70 | } | 70 | } | ||
| 71 | 71 | ||||
| 72 | + | func TestResolveNoContextFilesSkipsAgentsInstructions(t *testing.T) { | |||
| 73 | + | zotHome := t.TempDir() | |||
| 74 | + | cwd := t.TempDir() | |||
| 75 | + | t.Setenv("ZOT_HOME", zotHome) | |||
| 76 | + | t.Setenv("OPENAI_API_KEY", "test-key") | |||
| 77 | + | if err := os.WriteFile(filepath.Join(zotHome, "AGENTS.md"), []byte("global rule"), 0o644); err != nil { | |||
| 78 | + | t.Fatal(err) | |||
| 79 | + | } | |||
| 80 | + | if err := os.WriteFile(filepath.Join(cwd, "AGENTS.md"), []byte("project rule"), 0o644); err != nil { | |||
| 81 | + | t.Fatal(err) | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | r, err := Resolve(Args{ | |||
| 85 | + | Provider: "openai", | |||
| 86 | + | Model: "gpt-5", | |||
| 87 | + | CWD: cwd, | |||
| 88 | + | NoContextFiles: true, | |||
| 89 | + | }, false) | |||
| 90 | + | if err != nil { | |||
| 91 | + | t.Fatal(err) | |||
| 92 | + | } | |||
| 93 | + | if len(r.ContextFiles) != 0 { | |||
| 94 | + | t.Fatalf("ContextFiles = %#v, want empty", r.ContextFiles) | |||
| 95 | + | } | |||
| 96 | + | if strings.Contains(r.SystemPrompt, "global rule") || strings.Contains(r.SystemPrompt, "project rule") { | |||
| 97 | + | t.Fatalf("system prompt contains disabled context files:\n%s", r.SystemPrompt) | |||
| 98 | + | } | |||
| 99 | + | } | |||
| 100 | + | ||||
| 72 | // TestResolveFallsBackWhenConfiguredModelIsGone reproduces the | 101 | // TestResolveFallsBackWhenConfiguredModelIsGone reproduces the | ||
| 73 | // startup failure caught by the user's screenshot: the persisted | 102 | // startup failure caught by the user's screenshot: the persisted | ||
| 74 | // config.json points at a model id that's no longer in the active | 103 | // config.json points at a model id that's no longer in the active | ||
M packages/agent/cli.go +17 -15
| @@ -389,11 +389,8 @@ func runPrintMode(ctx context.Context, args Args, version string) error { | |||
| 389 | 389 | sess, _ := openOrCreateSession(args, r, ag, version) | |
| 390 | 390 | defer sess.Close() | |
| 391 | 391 | ||
| 392 | - | prompt := args.Prompt | |
| 393 | - | if prompt == "" { | |
| 394 | - | piped, _ := readAllStdin() | |
| 395 | - | prompt = strings.TrimSpace(piped) | |
| 396 | - | } | |
| 392 | + | piped, _ := readAllStdin() | |
| 393 | + | prompt := combinePromptInput(piped, args.Prompt) | |
| 397 | 394 | if prompt == "" { | |
| 398 | 395 | return fmt.Errorf("print mode requires a prompt (arg or stdin)") | |
| 399 | 396 | } | |
| @@ -432,11 +429,8 @@ func runStreamMode(ctx context.Context, args Args, version string) error { | |||
| 432 | 429 | sess, _ := openOrCreateSession(args, r, ag, version) | |
| 433 | 430 | defer sess.Close() | |
| 434 | 431 | ||
| 435 | - | prompt := args.Prompt | |
| 436 | - | if prompt == "" { | |
| 437 | - | piped, _ := readAllStdin() | |
| 438 | - | prompt = strings.TrimSpace(piped) | |
| 439 | - | } | |
| 432 | + | piped, _ := readAllStdin() | |
| 433 | + | prompt := combinePromptInput(piped, args.Prompt) | |
| 440 | 434 | if prompt == "" { | |
| 441 | 435 | return fmt.Errorf("stream mode requires a prompt (arg or stdin)") | |
| 442 | 436 | } | |
| @@ -514,11 +508,8 @@ func runJSONMode(ctx context.Context, args Args, version string) error { | |||
| 514 | 508 | sess, _ := openOrCreateSession(args, r, ag, version) | |
| 515 | 509 | defer sess.Close() | |
| 516 | 510 | ||
| 517 | - | prompt := args.Prompt | |
| 518 | - | if prompt == "" { | |
| 519 | - | piped, _ := readAllStdin() | |
| 520 | - | prompt = strings.TrimSpace(piped) | |
| 521 | - | } | |
| 511 | + | piped, _ := readAllStdin() | |
| 512 | + | prompt := combinePromptInput(piped, args.Prompt) | |
| 522 | 513 | if prompt == "" { | |
| 523 | 514 | return fmt.Errorf("json mode requires a prompt (arg or stdin)") | |
| 524 | 515 | } | |
| @@ -1580,6 +1571,17 @@ func writeNewTranscriptLocked(ag *core.Agent, sess *core.Session, from int) { | |||
| 1580 | 1571 | _ = sess.AppendUsage(cum, cum) | |
| 1581 | 1572 | } | |
| 1582 | 1573 | ||
| 1574 | + | func combinePromptInput(stdin, positional string) string { | |
| 1575 | + | parts := make([]string, 0, 2) | |
| 1576 | + | if stdin = strings.TrimSpace(stdin); stdin != "" { | |
| 1577 | + | parts = append(parts, stdin) | |
| 1578 | + | } | |
| 1579 | + | if positional = strings.TrimSpace(positional); positional != "" { | |
| 1580 | + | parts = append(parts, positional) | |
| 1581 | + | } | |
| 1582 | + | return strings.Join(parts, "\n") | |
| 1583 | + | } | |
| 1584 | + | ||
| 1583 | 1585 | func readAllStdin() (string, error) { | |
| 1584 | 1586 | fi, err := os.Stdin.Stat() | |
| 1585 | 1587 | if err != nil { | |
| @@ -389,11 +389,8 @@ func runPrintMode(ctx context.Context, args Args, version string) error { | |||||
| 389 | sess, _ := openOrCreateSession(args, r, ag, version) | 389 | sess, _ := openOrCreateSession(args, r, ag, version) | ||
| 390 | defer sess.Close() | 390 | defer sess.Close() | ||
| 391 | 391 | ||||
| 392 | - | prompt := args.Prompt | 392 | + | piped, _ := readAllStdin() |
| 393 | - | if prompt == "" { | 393 | + | prompt := combinePromptInput(piped, args.Prompt) |
| 394 | - | piped, _ := readAllStdin() | |||
| 395 | - | prompt = strings.TrimSpace(piped) | |||
| 396 | - | } | |||
| 397 | if prompt == "" { | 394 | if prompt == "" { | ||
| 398 | return fmt.Errorf("print mode requires a prompt (arg or stdin)") | 395 | return fmt.Errorf("print mode requires a prompt (arg or stdin)") | ||
| 399 | } | 396 | } | ||
| @@ -432,11 +429,8 @@ func runStreamMode(ctx context.Context, args Args, version string) error { | |||||
| 432 | sess, _ := openOrCreateSession(args, r, ag, version) | 429 | sess, _ := openOrCreateSession(args, r, ag, version) | ||
| 433 | defer sess.Close() | 430 | defer sess.Close() | ||
| 434 | 431 | ||||
| 435 | - | prompt := args.Prompt | 432 | + | piped, _ := readAllStdin() |
| 436 | - | if prompt == "" { | 433 | + | prompt := combinePromptInput(piped, args.Prompt) |
| 437 | - | piped, _ := readAllStdin() | |||
| 438 | - | prompt = strings.TrimSpace(piped) | |||
| 439 | - | } | |||
| 440 | if prompt == "" { | 434 | if prompt == "" { | ||
| 441 | return fmt.Errorf("stream mode requires a prompt (arg or stdin)") | 435 | return fmt.Errorf("stream mode requires a prompt (arg or stdin)") | ||
| 442 | } | 436 | } | ||
| @@ -514,11 +508,8 @@ func runJSONMode(ctx context.Context, args Args, version string) error { | |||||
| 514 | sess, _ := openOrCreateSession(args, r, ag, version) | 508 | sess, _ := openOrCreateSession(args, r, ag, version) | ||
| 515 | defer sess.Close() | 509 | defer sess.Close() | ||
| 516 | 510 | ||||
| 517 | - | prompt := args.Prompt | 511 | + | piped, _ := readAllStdin() |
| 518 | - | if prompt == "" { | 512 | + | prompt := combinePromptInput(piped, args.Prompt) |
| 519 | - | piped, _ := readAllStdin() | |||
| 520 | - | prompt = strings.TrimSpace(piped) | |||
| 521 | - | } | |||
| 522 | if prompt == "" { | 513 | if prompt == "" { | ||
| 523 | return fmt.Errorf("json mode requires a prompt (arg or stdin)") | 514 | return fmt.Errorf("json mode requires a prompt (arg or stdin)") | ||
| 524 | } | 515 | } | ||
| @@ -1580,6 +1571,17 @@ func writeNewTranscriptLocked(ag *core.Agent, sess *core.Session, from int) { | |||||
| 1580 | _ = sess.AppendUsage(cum, cum) | 1571 | _ = sess.AppendUsage(cum, cum) | ||
| 1581 | } | 1572 | } | ||
| 1582 | 1573 | ||||
| 1574 | + | func combinePromptInput(stdin, positional string) string { | |||
| 1575 | + | parts := make([]string, 0, 2) | |||
| 1576 | + | if stdin = strings.TrimSpace(stdin); stdin != "" { | |||
| 1577 | + | parts = append(parts, stdin) | |||
| 1578 | + | } | |||
| 1579 | + | if positional = strings.TrimSpace(positional); positional != "" { | |||
| 1580 | + | parts = append(parts, positional) | |||
| 1581 | + | } | |||
| 1582 | + | return strings.Join(parts, "\n") | |||
| 1583 | + | } | |||
| 1584 | + | ||||
| 1583 | func readAllStdin() (string, error) { | 1585 | func readAllStdin() (string, error) { | ||
| 1584 | fi, err := os.Stdin.Stat() | 1586 | fi, err := os.Stdin.Stat() | ||
| 1585 | if err != nil { | 1587 | if err != nil { | ||
A packages/agent/prompt_input_test.go +25 -0
| @@ -0,0 +1,25 @@ | |||
| 1 | + | package agent | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | func TestCombinePromptInput(t *testing.T) { | |
| 6 | + | tests := []struct { | |
| 7 | + | name string | |
| 8 | + | stdin string | |
| 9 | + | positional string | |
| 10 | + | want string | |
| 11 | + | }{ | |
| 12 | + | {name: "stdin and instruction", stdin: "input text\n", positional: "summarize it", want: "input text\nsummarize it"}, | |
| 13 | + | {name: "stdin only", stdin: " input text\n", want: "input text"}, | |
| 14 | + | {name: "positional only", positional: " summarize it ", want: "summarize it"}, | |
| 15 | + | {name: "empty", stdin: " \n", positional: " ", want: ""}, | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | for _, tc := range tests { | |
| 19 | + | t.Run(tc.name, func(t *testing.T) { | |
| 20 | + | if got := combinePromptInput(tc.stdin, tc.positional); got != tc.want { | |
| 21 | + | t.Fatalf("combinePromptInput(%q, %q) = %q, want %q", tc.stdin, tc.positional, got, tc.want) | |
| 22 | + | } | |
| 23 | + | }) | |
| 24 | + | } | |
| 25 | + | } | |
| @@ -0,0 +1,25 @@ | |||||
| 1 | + | package agent | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | func TestCombinePromptInput(t *testing.T) { | |||
| 6 | + | tests := []struct { | |||
| 7 | + | name string | |||
| 8 | + | stdin string | |||
| 9 | + | positional string | |||
| 10 | + | want string | |||
| 11 | + | }{ | |||
| 12 | + | {name: "stdin and instruction", stdin: "input text\n", positional: "summarize it", want: "input text\nsummarize it"}, | |||
| 13 | + | {name: "stdin only", stdin: " input text\n", want: "input text"}, | |||
| 14 | + | {name: "positional only", positional: " summarize it ", want: "summarize it"}, | |||
| 15 | + | {name: "empty", stdin: " \n", positional: " ", want: ""}, | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | for _, tc := range tests { | |||
| 19 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 20 | + | if got := combinePromptInput(tc.stdin, tc.positional); got != tc.want { | |||
| 21 | + | t.Fatalf("combinePromptInput(%q, %q) = %q, want %q", tc.stdin, tc.positional, got, tc.want) | |||
| 22 | + | } | |||
| 23 | + | }) | |||
| 24 | + | } | |||
| 25 | + | } | |||