From 9b8bbc65d7c6ced01d2d10578743f5eb02c56658 Mon Sep 17 00:00:00 2001 From: Renan Modenese Date: Sun, 30 Aug 2026 12:14:56 -0300 Subject: [PATCH 1/7] docs: update and tighten README for the current backend/tool surface Adds the OpenAI-compatible backend, MCP config/command, and the search_files/glob_files/edit_file tools, all missing from the README; condenses the prompt-caching writeup. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017b6CCzxUKtSnLVpAEgfPkR --- README.md | 97 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 55 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 5ebfd21..283eb4b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # pico code -A small CLI AI agent written in Go. One agent loop, two LLM backends — -[Anthropic](https://docs.anthropic.com/) (cloud) and [Ollama](https://ollama.com/) -(local) — and a handful of sandboxed tools (read a file, list a directory, run -an allowlisted command, write a file behind a flag). +A small CLI AI agent written in Go. One agent loop, three LLM backends — +[Anthropic](https://docs.anthropic.com/) (cloud), [Ollama](https://ollama.com/) +(local), and any OpenAI-compatible `/chat/completions` endpoint — plus +sandboxed filesystem tools (read, list, search, glob, edit, write-behind-a-flag), +an allowlisted `run_command`, and optional MCP servers for anything beyond that. ## Install @@ -45,6 +46,9 @@ pico --provider=ollama --model=qwen3:8b # Cloud — needs ANTHROPIC_API_KEY set first. pico --provider=anthropic --model=claude-sonnet-4-5 + +# Any OpenAI-compatible endpoint (OpenAI itself, vLLM, LM Studio, ...). +pico --provider=openai --model=gpt-4o ``` Either command drops you into a `> ` prompt. Ctrl+D (EOF) exits cleanly; @@ -52,7 +56,7 @@ Ctrl+C interrupts the process the way it would any CLI tool — one to stop, a second if the first doesn't. `--tui` behaves differently on purpose: see [While chatting](#while-chatting). -## The two backends +## The three backends ### Anthropic @@ -68,34 +72,34 @@ or an unreachable network surfaces as a clear error rather than a hang. Every request marks two `cache_control` breakpoints: one on the system prompt, one on the last block of the last message. Because history only -ever grows by appending, each new turn's request shares an identical -prefix with the one before it up to that second breakpoint — the provider -serves everything up to there from cache and only charges full price for -the newly appended tail. A `/compact` (or automatic compaction, 8.2) -rewrites the oldest turns into a fresh synthetic summary, which changes -the cached prefix's bytes; the next request simply writes a new cache -entry there instead of ever risking a stale one. - -The savings are real but numbers vary a lot by conversation shape, so -here's the arithmetic rather than a single "X% faster" claim, using -`claude-sonnet-4-5`'s published per-token rates ($3.00 input / $3.75 cache -write / $0.30 cache read, per million tokens — see `internal/config/pricing.go` -for the same "these are estimates" caveat `/usage` carries). For a 2,000 -token stable prefix (a typical system prompt plus tool definitions) reused -across 10 requests within the cache's TTL: - -| | cost | -|---|---| -| without caching (10 × full-price reads of the 2,000 tokens) | 10 × 2000 × $3.00 / 1e6 = **$0.0600** | -| with caching (1 write + 9 reads of the 2,000 tokens) | (2000 × $3.75 / 1e6) + 9 × (2000 × $0.30 / 1e6) = $0.0075 + $0.0054 = **$0.0129** | - -That's a ~78% reduction on the *stable-prefix portion* of input cost over -10 turns, and it keeps improving the longer the conversation runs — the -one-time write premium amortizes away while every later turn keeps paying -the ~10×-cheaper read rate. It only covers the part of each request that's -actually stable; the growing tail (each turn's own new messages) is never -cached and always costs full price, so a whole conversation's total -savings will be smaller than this in practice. +ever grows by appending, each new turn shares an identical prefix with the +last one up to that second breakpoint, so the provider serves it from +cache and only charges full price for the newly appended tail. A +`/compact` (or automatic compaction, 8.2) rewrites the oldest turns into a +fresh summary, which simply starts a new cache entry rather than risking a +stale one. + +Using `claude-sonnet-4-5`'s published rates ($3.00 input / $3.75 cache +write / $0.30 cache read per million tokens — see `internal/config/pricing.go`), +a 2,000-token stable prefix reused across 10 requests costs $0.0600 +without caching vs. $0.0129 with it: a ~78% reduction on the +*stable-prefix portion* of input cost, improving further the longer the +conversation runs. It doesn't touch the growing tail of new messages each +turn adds, which is never cached and always full price. + +### OpenAI-compatible + +```bash +export OPENAI_API_KEY=sk-... # optional — some endpoints need no auth +export OPENAI_BASE_URL=http://localhost:8000/v1 # optional, default api.openai.com/v1 +./bin/pico --provider=openai --model= +``` + +One adapter for anything that speaks the same flat `/chat/completions` + +`tool_calls` shape as OpenAI: OpenAI itself, vLLM, LM Studio, or a local +Ollama's own `/v1` endpoint. `OPENAI_BASE_URL` is what points it at any of +those; an unset `OPENAI_API_KEY` is not an error, since many +OpenAI-compatible servers run with no auth at all. ### Ollama @@ -117,7 +121,8 @@ Not every local model supports native tool calling, and some that advertise support still narrate a tool call in prose instead of using it. So `--provider=ollama` defaults `--tools` to `prompted` unless you pass `--tools` explicitly; pass `--tools=native` yourself if your model handles -it reliably. (`--provider=anthropic` keeps defaulting to `native`.) +it reliably. (`--provider=anthropic` and `--provider=openai` keep +defaulting to `native`.) `--tools=prompted` injects tool schemas into the system prompt and parses a fenced JSON block back out of the reply instead of relying on the model's native tool-calling — it can't stream, so it also forces `--stream=false` @@ -129,7 +134,7 @@ you'll need to add `--tools=native` explicitly. | Flag | Default | Meaning | | ----------------- | ------------ | -------------------------------------------------------------- | -| `--provider` | `anthropic` | `anthropic` or `ollama` | +| `--provider` | `anthropic` | `anthropic`, `ollama`, or `openai` | | `--model` | (none) | model ID for the selected provider | | `--max-turns` | `25` | agent loop turns before it stops on its own | | `--token-budget` | `100000` | cumulative tokens before it stops on its own | @@ -139,17 +144,19 @@ you'll need to add `--tools=native` explicitly. | `--stream` | `true` | stream a reply as it arrives, plain mode only (the TUI always streams) | | `--tui` | `false` | bubbletea TUI instead of the plain read-eval-print loop | | `--log-level` | `info` | `debug`, `info`, `warn`, or `error` | -| `--num-ctx` | `4096` | Ollama's context window (`num_ctx`); ignored by Anthropic | -| `--allow-write` | `false` | register the `write_file` tool | +| `--num-ctx` | `4096` | Ollama's context window (`num_ctx`); ignored by other providers | +| `--allow-write` | `false` | register the `write_file` and `edit_file` tools | | `--session` | (none) | name a session to resume or start; saved after every turn | | `--allow-commands`| (none) | comma-separated binary allowlist; registers `run_command` only if non-empty | | `--context-window`| `200000` | context window compaction measures usage against; ignored by Ollama, which uses `--num-ctx` | | `--think` | `false` | ask the model for a reasoning trace ahead of its reply; currently Ollama only | +| `--mcp-config` | (none) | path to a JSON file listing MCP servers (`{"mcpServers": {name: {command, args, env}}}`) | `PICO_CODE_PROVIDER` is an environment fallback for `--provider`, checked -only when the flag isn't explicitly set. `ANTHROPIC_API_KEY` and -`OLLAMA_HOST` are read from the environment only — there's no flag for -either, so a credential never ends up in shell history. +only when the flag isn't explicitly set. `ANTHROPIC_API_KEY`, `OLLAMA_HOST`, +`OPENAI_API_KEY`, and `OPENAI_BASE_URL` are read from the environment +only — there's no flag for any of them, so a credential never ends up in +shell history. ## While chatting @@ -161,6 +168,8 @@ Both the plain REPL and the TUI understand the same commands: - `/new` — start a fresh, unsaved conversation - `/save [name]` — save the current conversation as a session - `/load ` — replace the current conversation with a saved session +- `/mcp [reconnect ]` — list configured MCP servers and their status, + or reconnect one (only meaningful with `--mcp-config` set) `--session ` does the same save/resume automatically, after every turn, in both — kill the process mid-conversation and `--session ` @@ -204,11 +213,15 @@ design. binary) executes only that allowlisted binary, directly (`exec.Command`, never a shell — no `|`, `&&`, `$()`, or similar can do anything), with a timeout and truncated output. -- `write_file` and `run_command` both need approval unless `--yes` is - passed; the plain REPL prompts on the terminal, the TUI shows a modal. +- `write_file`, `edit_file`, `run_command`, and any tool from an MCP server + all need approval unless `--yes` is passed; the plain REPL prompts on the + terminal, the TUI shows a modal. - Tool output is truncated (head + tail, with an elision marker) before it ever reaches the model's context, so one large file can't blow the context window. +- A provider or model-resolution failure (an unreachable Ollama daemon, a + typo'd `--model`) is rewritten into a one-line message naming the actual + problem instead of the raw transport error (`cmd/pico/errors.go`). ## How the agent loop works From 00377de3f3e2b4fe0da2b33e9c1b6fe0c794a109 Mon Sep 17 00:00:00 2001 From: Renan Modenese Date: Sun, 30 Aug 2026 12:17:29 -0300 Subject: [PATCH 2/7] =?UTF-8?q?docs:=20fix=20README=20intro=20=E2=80=94=20?= =?UTF-8?q?edit=5Ffile=20is=20also=20gated=20behind=20--allow-write?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not just write_file: the intro paragraph implied edit was unconditional. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017b6CCzxUKtSnLVpAEgfPkR --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 283eb4b..f0f070f 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,9 @@ A small CLI AI agent written in Go. One agent loop, three LLM backends — [Anthropic](https://docs.anthropic.com/) (cloud), [Ollama](https://ollama.com/) (local), and any OpenAI-compatible `/chat/completions` endpoint — plus -sandboxed filesystem tools (read, list, search, glob, edit, write-behind-a-flag), -an allowlisted `run_command`, and optional MCP servers for anything beyond that. +sandboxed filesystem tools (read, list, search, glob always on; write and +edit behind a flag), an allowlisted `run_command`, and optional MCP servers +for anything beyond that. ## Install From d0e426bed73d7ec42a7416aa23696156d32f964e Mon Sep 17 00:00:00 2001 From: Renan Modenese Date: Sun, 30 Aug 2026 12:25:14 -0300 Subject: [PATCH 3/7] chore: delete old docs --- .gitignore | 1 + docs/tui-check.md | 29 ----------------------------- 2 files changed, 1 insertion(+), 29 deletions(-) delete mode 100644 docs/tui-check.md diff --git a/.gitignore b/.gitignore index 45bdc01..525fe55 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.dll *.so *.dylib +bin/ # Test binary, built with `go test -c` *.test diff --git a/docs/tui-check.md b/docs/tui-check.md deleted file mode 100644 index 97c5ae0..0000000 --- a/docs/tui-check.md +++ /dev/null @@ -1,29 +0,0 @@ -# TUI manual check - -Run `./bin/pico --tui --provider=ollama --model=` (or -`--provider=anthropic`) against a live backend and walk this list. Tick each -box only after actually observing the behavior — this is 7.2's AC, and it is -explicitly manual: nothing here is exercised by `make test`. - -- [ ] Scrollback viewport shows prior turns; PgUp/PgDn scroll it. -- [ ] Textarea accepts multi-line input; Enter (not shift+Enter) submits. -- [ ] Spinner appears while waiting on the provider and disappears once the - reply starts. -- [ ] Assistant text appears incrementally as it streams, not all at once. -- [ ] A tool call shows a running indicator, then flips to ✓ or ✗ once it - finishes. -- [ ] Markdown in a finished reply (headings, code fences, lists) renders - formatted, not as raw `#`/`` ``` `` characters. -- [ ] A tool that needs approval (`write_file` with `--allow-write`, or - `run_command`) shows a modal; `y` approves, `n`/Esc denies, and the - turn continues either way. -- [ ] Two tools needing approval in the same round show their modals one at - a time, not stacked. -- [ ] Ctrl+C while a turn is in flight cancels that turn and returns to the - prompt; the TUI keeps running. -- [ ] Ctrl+D exits the program cleanly, including while a turn is in flight. -- [ ] Resizing the terminal reflows the viewport and textarea without - corrupting prior output. -- [ ] `/usage`, `/new`, `/save `, and `/load ` each show their - output in the scrollback, prefixed with the command line typed; `/new` - clears history, `/load` restores it. From 3ca7f1ebe44a8432b030d51a5403212277120f22 Mon Sep 17 00:00:00 2001 From: Renan Modenese Date: Sun, 30 Aug 2026 12:33:21 -0300 Subject: [PATCH 4/7] fix: warn on install if pico already exists on PATH Debian/Ubuntu ship pico as nano's Pine-compatibility alias, so a fresh install can silently shadow an existing editor of the same name. --- install.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install.sh b/install.sh index 0b76363..caf293c 100755 --- a/install.sh +++ b/install.sh @@ -63,6 +63,8 @@ curl -fsSL "$base_url/checksums.txt" -o "$tmp_dir/checksums.txt" tar -xzf "$tmp_dir/$archive" -C "$tmp_dir" pico +existing_pico=$(command -v pico 2>/dev/null || true) + install_dir=${INSTALL_DIR:-/usr/local/bin} if [ ! -w "$install_dir" ] 2>/dev/null; then install_dir="$HOME/.local/bin" @@ -72,6 +74,10 @@ fi install -m 755 "$tmp_dir/pico" "$install_dir/pico" echo "Installed $("$install_dir/pico" --version) to $install_dir/pico" +if [ -n "$existing_pico" ] && [ "$existing_pico" != "$install_dir/pico" ]; then + echo "note: another 'pico' was already on your PATH at $existing_pico (often nano's Pine-compatibility alias) — $install_dir/pico takes priority only if $install_dir comes first in PATH" +fi + case ":$PATH:" in *":$install_dir:"*) ;; *) echo "note: $install_dir is not on your PATH — add it to use 'pico' directly" ;; From 6eb4b3cf7f2581698696ebf9ecd77d717498be70 Mon Sep 17 00:00:00 2001 From: Renan Modenese Date: Sun, 30 Aug 2026 13:09:25 -0300 Subject: [PATCH 5/7] build: run built binary from make run, add run-plain target make run now depends on build and executes ./bin/pico instead of go run, and shares its flags with a new run-plain target that omits --tui so the plain writer path can be smoke-tested. Also gofmt cmd/pico/chat.go. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XnMFo3E2ExaqaabKBPTxuz --- .gitignore | 4 ++- Makefile | 11 +++++--- cmd/pico/chat.go | 66 ++++++++++++++++++++++++------------------------ 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 525fe55..313017c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ go.work.sum # agents .claude/ -docs/ \ No newline at end of file +docs/ +# build output +bin/ diff --git a/Makefile b/Makefile index b329544..b14fc91 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ -.PHONY: build test lint run +.PHONY: build test lint run run-plain + +RUN_FLAGS = --provider=ollama --model=qwen3.5:4b --tools=native --think --num-ctx 8192 build: go build -o bin/pico ./cmd/pico @@ -11,5 +13,8 @@ lint: go vet ./... golangci-lint run -run: - go run ./cmd/pico --provider=ollama --model=qwen3.5:4b --tui --tools=native --think --num-ctx 8192 +run: build + ./bin/pico $(RUN_FLAGS) --tui + +run-plain: build + ./bin/pico $(RUN_FLAGS) diff --git a/cmd/pico/chat.go b/cmd/pico/chat.go index 500082e..fb95a13 100644 --- a/cmd/pico/chat.go +++ b/cmd/pico/chat.go @@ -69,44 +69,44 @@ func configureChat(cmd *cobra.Command, getenv func(string) string) { } cmd.RunE = func(cmd *cobra.Command, _ []string) error { - provider := flags.provider - if !cmd.Flags().Changed("provider") { - if v := getenv("PICO_CODE_PROVIDER"); v != "" { - provider = v - } - } - - toolsMode := flags.tools - if !cmd.Flags().Changed("tools") && provider == string(config.ProviderOllama) { - toolsMode = string(config.ToolsPrompted) + provider := flags.provider + if !cmd.Flags().Changed("provider") { + if v := getenv("PICO_CODE_PROVIDER"); v != "" { + provider = v } + } - cfg, err := config.Load(config.Flags{ - Provider: provider, - Model: flags.model, - MaxTurns: flags.maxTurns, - TokenBudget: flags.tokenBudget, - Workspace: flags.workspace, - Yes: flags.yes, - Tools: toolsMode, - Stream: flags.stream, - TUI: flags.tui, - LogLevel: flags.logLevel, - NumCtx: flags.numCtx, - AllowWrite: flags.allowWrite, - Session: flags.session, - AllowCommands: flags.allowCommands, - ContextWindow: flags.contextWindow, - MCPConfig: flags.mcpConfig, - Think: flags.think, - }, getenv) - if err != nil { - return fmt.Errorf("resolving config: %w", err) - } + toolsMode := flags.tools + if !cmd.Flags().Changed("tools") && provider == string(config.ProviderOllama) { + toolsMode = string(config.ToolsPrompted) + } - return runChat(cmd, cfg) + cfg, err := config.Load(config.Flags{ + Provider: provider, + Model: flags.model, + MaxTurns: flags.maxTurns, + TokenBudget: flags.tokenBudget, + Workspace: flags.workspace, + Yes: flags.yes, + Tools: toolsMode, + Stream: flags.stream, + TUI: flags.tui, + LogLevel: flags.logLevel, + NumCtx: flags.numCtx, + AllowWrite: flags.allowWrite, + Session: flags.session, + AllowCommands: flags.allowCommands, + ContextWindow: flags.contextWindow, + MCPConfig: flags.mcpConfig, + Think: flags.think, + }, getenv) + if err != nil { + return fmt.Errorf("resolving config: %w", err) } + return runChat(cmd, cfg) + } + f := cmd.Flags() f.StringVar(&flags.provider, "provider", "anthropic", "LLM backend to use (anthropic|ollama|openai)") f.StringVar(&flags.model, "model", "", "model identifier for the selected provider") From ff8c80bdfeb9cba0d22ee20858ec482500c865df Mon Sep 17 00:00:00 2001 From: Renan Modenese Date: Fri, 28 Aug 2026 18:53:21 -0300 Subject: [PATCH 6/7] fix: return a friendly error when --model is missing config.Load now validates Model is non-empty, failing fast with a message that names --model instead of letting an empty model reach a provider adapter and surface as an opaque transport error (404 from Anthropic, a failed local connection to Ollama). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014QmC7oUExcrEMLRtQK75RV --- cmd/pico/root_test.go | 26 +++++++++++++++++++++----- internal/config/config.go | 10 ++++++++++ internal/config/config_test.go | 10 ++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/cmd/pico/root_test.go b/cmd/pico/root_test.go index a6249d7..6b47587 100644 --- a/cmd/pico/root_test.go +++ b/cmd/pico/root_test.go @@ -60,7 +60,7 @@ func TestChatProviderEnvFallback(t *testing.T) { root := newRootCmd(getenv) buf := &bytes.Buffer{} root.SetOut(buf) - root.SetArgs(nil) + root.SetArgs([]string{"--model=test-model"}) if err := root.Execute(); err != nil { t.Fatalf("chat returned error: %v", err) @@ -78,7 +78,7 @@ func TestChatFlagOverridesEnv(t *testing.T) { root := newRootCmd(getenv) buf := &bytes.Buffer{} root.SetOut(buf) - root.SetArgs([]string{"--provider=anthropic"}) + root.SetArgs([]string{"--provider=anthropic", "--model=test-model"}) if err := root.Execute(); err != nil { t.Fatalf("chat returned error: %v", err) @@ -93,7 +93,7 @@ func TestChatToolsDefaultsToPromptedForOllama(t *testing.T) { root := newRootCmd(func(string) string { return "" }) buf := &bytes.Buffer{} root.SetOut(buf) - root.SetArgs([]string{"--provider=ollama"}) + root.SetArgs([]string{"--provider=ollama", "--model=test-model"}) if err := root.Execute(); err != nil { t.Fatalf("chat returned error: %v", err) @@ -108,7 +108,7 @@ func TestChatToolsDefaultsToNativeForAnthropic(t *testing.T) { root := newRootCmd(func(string) string { return "" }) buf := &bytes.Buffer{} root.SetOut(buf) - root.SetArgs([]string{"--provider=anthropic"}) + root.SetArgs([]string{"--provider=anthropic", "--model=test-model"}) if err := root.Execute(); err != nil { t.Fatalf("chat returned error: %v", err) @@ -123,7 +123,7 @@ func TestChatExplicitToolsOverridesOllamaDefault(t *testing.T) { root := newRootCmd(func(string) string { return "" }) buf := &bytes.Buffer{} root.SetOut(buf) - root.SetArgs([]string{"--provider=ollama", "--tools=native"}) + root.SetArgs([]string{"--provider=ollama", "--tools=native", "--model=test-model"}) if err := root.Execute(); err != nil { t.Fatalf("chat returned error: %v", err) @@ -148,3 +148,19 @@ func TestChatUnknownProviderIsClearError(t *testing.T) { t.Errorf("expected error to name the offending value, got: %v", err) } } + +func TestChatMissingModelIsClearError(t *testing.T) { + root := newRootCmd(func(string) string { return "" }) + buf := &bytes.Buffer{} + root.SetOut(buf) + root.SetErr(buf) + root.SetArgs(nil) + + err := root.Execute() + if err == nil { + t.Fatal("expected an error when --model is not passed") + } + if !strings.Contains(err.Error(), "--model") { + t.Errorf("expected error to mention --model, got: %v", err) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index f365ed4..edda3e8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,6 +49,12 @@ var ErrUnknownToolsMode = errors.New("unknown tools mode") // debug, info, warn, or error. var ErrUnknownLogLevel = errors.New("unknown log level") +// ErrModelRequired is returned when --model is empty. Nothing downstream +// defaults it, so an empty model would otherwise reach a provider adapter +// and fail as an opaque transport error (a 404 from Anthropic, a connection +// attempt to Ollama) instead of a clear message at startup. +var ErrModelRequired = errors.New("model is required") + // Flags carries the values collected from the chat subcommand's flags, // already resolved against any environment fallback (e.g. PICO_CODE_PROVIDER) // by the caller. Load only validates and fills in credentials. @@ -146,6 +152,10 @@ func Load(f Flags, getenv func(string) string) (*Config, error) { return nil, fmt.Errorf("%w: %q (want %q, %q, or %q)", ErrUnknownProvider, f.Provider, ProviderAnthropic, ProviderOllama, ProviderOpenAI) } + if f.Model == "" { + return nil, fmt.Errorf("%w: pass --model with an identifier for %q (e.g. a Claude model name for anthropic, or a locally pulled model tag for ollama)", ErrModelRequired, provider) + } + tools := ToolsMode(f.Tools) switch tools { case ToolsNative, ToolsPrompted: diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 2db5057..3bf7b43 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -126,6 +126,16 @@ func TestLoad(t *testing.T) { getenv: noEnv, wantErr: ErrUnknownProvider, }, + { + name: "empty model", + flags: func() Flags { + f := baseFlags() + f.Model = "" + return f + }(), + getenv: noEnv, + wantErr: ErrModelRequired, + }, { name: "unknown tools mode", flags: func() Flags { From d569f2d8d82582cc0feeea841db1aee4f57f3c3e Mon Sep 17 00:00:00 2001 From: Renan Modenese Date: Sun, 30 Aug 2026 13:30:17 -0300 Subject: [PATCH 7/7] feat: add BIN_NAME override to install.sh Most systems already ship a pico (nano's Pine-compatibility alias at /usr/bin/pico), and the default install dir precedes /usr/bin on a typical PATH, so installing would silently shadow it. BIN_NAME picks the installed command name so both can coexist; the existing-pico warning now names the chosen command and points at the override. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XnMFo3E2ExaqaabKBPTxuz --- README.md | 9 +++++++++ install.sh | 18 ++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f0f070f..7bfb1d7 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,15 @@ into `/usr/local/bin` (or `~/.local/bin` if that isn't writable) — no Go required. Windows isn't supported yet: command timeouts rely on POSIX process groups. +Many systems already have a `pico` — usually nano's Pine-compatibility alias +at `/usr/bin/pico`. Set `BIN_NAME` to install under a different command name +and keep both, and `INSTALL_DIR` to choose where it lands: + +```bash +curl -fsSL https://raw.githubusercontent.com/reno/pico-code/main/install.sh \ + | BIN_NAME=pico-code INSTALL_DIR=~/.local/bin sh +``` + Or via Homebrew (macOS/Linux): ```bash diff --git a/install.sh b/install.sh index caf293c..82589f6 100755 --- a/install.sh +++ b/install.sh @@ -7,6 +7,10 @@ # VERSION release tag to install, e.g. "v0.3.0" (default: latest) # INSTALL_DIR where to place the binary (default: /usr/local/bin, falling # back to ~/.local/bin if that isn't writable) +# BIN_NAME command name to install as (default: pico). Use this when a +# different pico — commonly nano's Pine-compatibility alias at +# /usr/bin/pico — is already on your PATH and you want to keep +# both, e.g. BIN_NAME=pico-code. set -eu repo="reno/pico-code" @@ -63,7 +67,9 @@ curl -fsSL "$base_url/checksums.txt" -o "$tmp_dir/checksums.txt" tar -xzf "$tmp_dir/$archive" -C "$tmp_dir" pico -existing_pico=$(command -v pico 2>/dev/null || true) +bin_name=${BIN_NAME:-pico} + +existing_pico=$(command -v "$bin_name" 2>/dev/null || true) install_dir=${INSTALL_DIR:-/usr/local/bin} if [ ! -w "$install_dir" ] 2>/dev/null; then @@ -71,14 +77,14 @@ if [ ! -w "$install_dir" ] 2>/dev/null; then mkdir -p "$install_dir" fi -install -m 755 "$tmp_dir/pico" "$install_dir/pico" -echo "Installed $("$install_dir/pico" --version) to $install_dir/pico" +install -m 755 "$tmp_dir/pico" "$install_dir/$bin_name" +echo "Installed $("$install_dir/$bin_name" --version) to $install_dir/$bin_name" -if [ -n "$existing_pico" ] && [ "$existing_pico" != "$install_dir/pico" ]; then - echo "note: another 'pico' was already on your PATH at $existing_pico (often nano's Pine-compatibility alias) — $install_dir/pico takes priority only if $install_dir comes first in PATH" +if [ -n "$existing_pico" ] && [ "$existing_pico" != "$install_dir/$bin_name" ]; then + echo "note: another '$bin_name' was already on your PATH at $existing_pico (often nano's Pine-compatibility alias) — $install_dir/$bin_name takes priority only if $install_dir comes first in PATH; re-run with BIN_NAME=pico-code to install under a different name and keep both" fi case ":$PATH:" in *":$install_dir:"*) ;; - *) echo "note: $install_dir is not on your PATH — add it to use 'pico' directly" ;; + *) echo "note: $install_dir is not on your PATH — add it to use '$bin_name' directly" ;; esac