diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c32a6830..2b5b5e6f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -263,7 +263,7 @@ { "name": "pi", "displayName": "Pi", - "version": "0.1.3", + "version": "0.2.0", "description": "Bridges to pi (dev/pi), a minimal terminal coding harness. Delegates coding tasks to the pi CLI for execution with full file and git context.", "author": { "name": "Frad LEE", diff --git a/pi/.claude-plugin/plugin.json b/pi/.claude-plugin/plugin.json index b55928b1..e748e81f 100644 --- a/pi/.claude-plugin/plugin.json +++ b/pi/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pi", - "version": "0.1.3", + "version": "0.2.0", "description": "Bridges to pi (dev/pi), a minimal terminal coding harness. Delegates tasks to pi CLI for execution.", "author": { "name": "Frad LEE", @@ -13,6 +13,9 @@ "ai-agent", "bridge" ], + "agents": [ + "./agents/pi-agent.md" + ], "commands": [ "./skills/delegate/", "./skills/review/", diff --git a/pi/README.md b/pi/README.md index c33ad4a5..1ac0e6a4 100644 --- a/pi/README.md +++ b/pi/README.md @@ -16,15 +16,15 @@ Bridges [pi](https://github.com/earendil-works/pi) (dev/pi), a minimal terminal ### `/pi:delegate` — Delegate a task to pi ``` -/pi:delegate [--provider PROVIDER] [--model MODEL] [--api-key KEY] [--thinking LEVEL] [--tools TOOL_LIST] [--exclude-tools TOOL_LIST] [--no-files] [--no-git] +/pi:delegate [--endpoint ENDPOINT] [--provider PROVIDER] [--model MODEL] [--api-key KEY] [--thinking LEVEL] [--tools TOOL_LIST] [--exclude-tools TOOL_LIST] [--no-git] ``` **Examples:** ``` /pi:delegate refactor this component --model claude-sonnet-4-20250514 -/pi:delegate write unit tests --provider openai --model gemini-3.6-flash-high -/pi:delegate explain how React reconciliation works --no-files +/pi:delegate write unit tests --endpoint local-proxy --model gemini-3.6-flash-high +/pi:delegate explain how React reconciliation works --no-git ``` ### `/pi:delegate --edit-config` — Edit persistent settings @@ -41,7 +41,7 @@ Project personal overrides project shared, which overrides global personal. CLI ### `/pi:review` — Review code with pi -Read-only code review via pi CLI. Runs pi with `--tools read,grep,find,ls` to prevent edits. +Read-only code review via pi CLI. By default reviews uncommitted working-tree changes (`git diff HEAD`) with pi restricted to `--tools read` (it cannot explore the repo); explicit targets (`--branch`, `--diff`, `@file`, PR) or `--explore` widen it to `read,grep,find,ls`. ``` /pi:review [@target] [--branch BRANCH] [--diff RANGE] [--endpoint ENDPOINT] [--model MODEL] [--thinking LEVEL] | --edit-config [--local|--shared|--global] | --list-models @@ -50,7 +50,7 @@ Read-only code review via pi CLI. Runs pi with `--tools read,grep,find,ls` to pr **Examples:** ``` -/pi:review Review the whole working directory (pi uses its own tools to explore) +/pi:review Review uncommitted working-tree changes (git diff HEAD, read-only) /pi:review --branch feat/new Review diff against main /pi:review --diff HEAD~5..HEAD Review recent commits /pi:review @src/index.ts Review a specific file @@ -83,32 +83,34 @@ After setup, both `/pi:delegate` and `/pi:review` will use these settings by def ## How It Works +Both `/pi:delegate` and `/pi:review` delegate execution to the dedicated `pi:pi-agent` — the plugin's single execution layer. It builds the pi command, runs it in the background (no timeout), and verifies the outcome. + ### `/pi:delegate` 1. The skill checks if `pi` is installed globally. -2. It reads persistent settings from `.claude/pi.local.json` (project) and `~/.claude/pi.local.json` (global), then merges with CLI flags. Custom base URLs are configured via `~/.pi/agent/models.json` — written automatically by the skill when `baseUrl` is set in the config file. -3. It collects context from the current working directory (relevant files, git status, directory structure). -4. It calls `pi -p` (print mode) via `run_in_background` with the collected context and your task description. No timeout — pi tasks run to completion naturally. -5. pi executes the task — its real output is **file edits in the working directory**, not stdout text. Always check `git diff --stat` after completion. +2. It reads persistent settings from `.claude/pi.local.json` (project), `.claude/pi.json` (shared), and `~/.claude/pi.local.json` (global), then merges with CLI flags — using the same **named-endpoint format** as `/pi:review`. Legacy flat fields still work as a fallback. +3. It resolves the endpoint, then launches `pi:pi-agent` with the resolved provider/model and the task description. +4. `pi:pi-agent` collects context (CLAUDE.md, git status), writes custom base URLs to the agent-dir `models.json`, and calls `pi -p` (print mode) via `run_in_background`. +5. pi executes the task — its real output is **file edits in the working directory**, not stdout text. The agent verifies via `git diff --stat`. ### `/pi:review` -1. Same checks and settings chain as `/pi:delegate`, but uses a **multi-provider** settings format. -2. Supports `--list-models` to display all configured providers and their models. -3. Runs `pi -p` with `--tools read,grep,find,ls` (read-only) — pi cannot edit files. -4. pi's stdout **is** the review output — present it directly to the user. +1. Same checks, settings chain, and endpoint format as `/pi:delegate`. +2. Supports `--list-models` to display all configured endpoints and their models. +3. Captures the review target (diff/branch/PR/file), then delegates to `pi:pi-agent`, which runs `pi -p` with `--tools read` (or `read,grep,find,ls` for explicit targets) — pi cannot edit files. +4. pi's stdout **is** the review output — the agent returns it and you present it directly to the user. ## Flags | Flag | Description | Source Priority | |------|-------------|-----------------| -| `--provider` | LLM provider (anthropic, openai, google, etc.) | CLI > settings > pi's default | +| `--endpoint` | Endpoint key name (must match a key in settings `endpoints`) | CLI > settings > `defaultEndpoint` | +| `--provider` | LLM provider (anthropic, openai, google, etc.) | CLI > settings > endpoint/provider > pi's default | | `--model` | Model pattern or ID | CLI > settings > pi's default | | `--api-key` | API key for the provider | CLI > settings > env var or config file | -| `--thinking` | Thinking level (off/minimal/low/medium/high/xhigh/max) | CLI > settings > `low` | +| `--thinking` | Thinking level (off/minimal/low/medium/high/xhigh/max) | CLI > settings > `max` | | `--tools` | Comma-separated allowed tools list | CLI > settings > `read,bash,write,edit,grep,find,ls` | | `--exclude-tools` | Comma-separated blocked tools list | CLI > settings > (none) | -| `--no-files` | Skip collecting file context | CLI > settings > `false` | | `--no-git` | Skip collecting git context | CLI > settings > `false` | ## Persistent Settings @@ -123,39 +125,34 @@ Three-tier JSON preference files (priority high to low): CLI flags override all three. Run `/pi:delegate --edit-config` or `/pi:review --edit-config` to quickly create or edit your project settings. -Both skills share the same settings files, but use different formats: - -- **`/pi:delegate`** uses flat format (single provider + model): - ```json - { - "provider": "openai", - "model": "gemini-3.6-flash-high", - "baseUrl": "http://10.10.0.195:8317/v1" - } - ``` +Both skills share the same settings files **and the same named-endpoint format**: + +```json +{ + "endpoints": { + "local-proxy": { + "provider": "openai", + "baseUrl": "http://10.10.0.195:8317/v1", + "models": ["gemini-3.6-flash-high", "gemini-3.6-pro"] + } + }, + "defaultEndpoint": "local-proxy", + "defaultModel": "gemini-3.6-flash-high", + "thinking": "max" +} +``` -- **`/pi:review`** uses multi-endpoint format (named endpoints, each with `provider` + optional `baseUrl` + `models`): - ```json - { - "endpoints": { - "local-proxy": { - "provider": "openai", - "baseUrl": "http://10.10.0.195:8317/v1", - "models": ["gemini-3.6-flash-high", "gemini-3.6-pro"] - } - }, - "defaultEndpoint": "local-proxy", - "defaultModel": "gemini-3.6-flash-high" - } - ``` +Each endpoint has `provider` (required), optional `baseUrl`/`apiKey`, and a `models` array. Both `/pi:delegate` and `/pi:review` resolve the active endpoint via `defaultEndpoint` (or a `--endpoint`/`--model` CLI flag), falling back to the first `models` entry when `defaultModel` is unset. Values can reference environment variables using `$VAR` or `${VAR}` syntax — they are resolved at read time. This is useful for API keys: `"apiKey": "$MY_API_KEY"` reads from the environment variable at runtime. -You can include both formats in the same file — the `jq` merge will combine them. The review skill reads `endpoints` and `defaultEndpoint`, while the delegate skill reads `provider` and `model`. +Legacy flat fields (`provider`/`model`/`baseUrl`/`apiKey`) are still honored by `/pi:delegate` as a fallback when no `defaultEndpoint` is set, so old configs keep working — but new setups should use the endpoint format above. ## Design - **Two entry points**: `/pi:delegate` for coding tasks (file edits), `/pi:review` for read-only code review. -- **Context-aware**: The skill automatically collects file and git context from the current project. +- **Dedicated execution layer**: Both commands delegate to `pi:pi-agent`, which builds the pi command, runs it in the background, and verifies the outcome — the main context stays clean and the user never interacts with pi directly. +- **Unified config**: Both commands share the same named-endpoint settings format and the same settings chain. +- **Context-aware**: The skill collects file and git context from the current project. - **Non-interactive**: All pi tasks run in `-p` (print) mode for clean, text-based output. - **Isolated**: Uses `--no-session --no-context-files --approve` to avoid conflicts with the current project's state. \ No newline at end of file diff --git a/pi/agents/pi-agent.md b/pi/agents/pi-agent.md new file mode 100644 index 00000000..87c9c119 --- /dev/null +++ b/pi/agents/pi-agent.md @@ -0,0 +1,158 @@ +--- +name: pi-agent +description: | + Executes a task through the pi CLI (@earendil-works/pi-coding-agent). This is the pi plugin's dedicated execution layer — every /pi:delegate and /pi:review call runs pi through this agent. Trigger when the pi delegate/review skill has resolved the provider/model and needs to actually run the pi command in the background. + + + Context: /pi:delegate resolved an openai proxy config and a task description + user: "refactor the launcher widget" + assistant: "I'll run this through pi-agent with the resolved openai provider and local proxy endpoint." + + Standard delegate path — run pi in the background, verify via git diff --stat. + + + + + Context: /pi:review resolved a read-only review task with a captured diff + user: "review the working tree changes" + assistant: "I'll run the read-only review through pi-agent; its stdout is the review output." + + Review path — read-only tools, stdout is the deliverable. + + +tools: ["Bash(pi:*)", "Bash(jq:*)", "Bash(command:*)", "Bash(git:*)", "Bash(mkdir:*)", "Bash(mv:*)", "Bash(mktemp:*)", "Bash(rm:*)", "Bash(cat:*)", "Bash(echo:*)", "Read", "Grep", "Glob"] +--- + +You are the execution layer for the pi plugin. You run the `pi` CLI (`@earendil-works/pi-coding-agent`) so the calling skill does not have to. You receive the resolved configuration and task description, run pi in the background, and report what actually happened. + +## Responsibilities + +1. Run the pi CLI exactly as the caller's mode requires — `delegate` (pi edits files) or `review` (pi reads only, stdout is the result). +2. Apply the resolved endpoint config (provider, model, baseUrl, apiKey) — never hardcode a provider. +3. Verify the real outcome: for delegate that is file edits (`git diff --stat`), for review it is pi's stdout. +4. Report the execution result back to the caller with exit code, changed files, and any output. + +## Process + +1. **Extract inputs** from the caller's prompt: `MODE` (`delegate`|`review`|`doctor`), `TASK` (task description), `PROVIDER`/`MODEL` (may be empty — leave pi to its own defaults), `API_KEY` (optional — a CLI `--api-key` the user typed explicitly, which overrides the config key; empty otherwise), `ENDPOINT` (optional — the resolved endpoint key, so credentials are read from the right endpoint), `THINKING` (default `max`), `TOOLS` (default `read,bash,write,edit,grep,find,ls`), `EXCLUDE_TOOLS` (optional denylist), `AGENT_DIR` (optional — overrides pi's agent directory), `NO_GIT` (`true` to skip git context), `APPEND_PATHS` (array of file paths to pass via `--append-system-prompt` — CLAUDE.md files, git-context file, diff file), review-only `FILE_REFS` (`@file` args), and `CLEANUP_FILES` (temp files to remove when pi exits). **A config-file `API_KEY`/`BASE_URL` is NEVER passed in the caller prompt** — those are read from the settings files in step 3 so config secrets never enter the model's context; only a CLI-typed `--api-key` may be passed (it is already in the user's prompt). +2. **Check installation** — if `pi` is missing, stop and report the install command. +3. **Read credentials/endpoint from config** — resolve `API_KEY`/`BASE_URL` from the same settings chain the skills use (project `.claude/pi.local.json` > `.claude/pi.json` > `~/.claude/pi.local.json`), resolving `$VAR`/`${VAR}` references to environment variables. This keeps the actual key/URL out of the calling model's context — the caller only passes provider/model/endpoint keys, never the secret itself. Use a helper that reads `apiKey`/`baseUrl` from the merged config and resolves env references: + ```bash + resolve_env() { + local val="$1" + while [[ "$val" =~ \$\{?([a-zA-Z_][a-zA-Z0-9_]*)\}? ]]; do + local var_name="${BASH_REMATCH[1]}" + local var_value="${!var_name:-}" + val="${val//${BASH_REMATCH[0]}/$var_value}" + done + echo "$val" + } + CONFIG='{}' + for f in "$HOME/.claude/pi.local.json" ".claude/pi.json" ".claude/pi.local.json"; do + [ -f "$f" ] && CONFIG=$(jq -s '.[0] * .[1]' /dev/stdin "$f" 2>/dev/null <<<"$CONFIG" || echo "$CONFIG") + done + # Endpoint-first with flat fallback (matches the skills). A CLI-passed API_KEY + # (step 1) wins — only read the config key when the caller did not pass one. + EP="${ENDPOINT:-$(echo "$CONFIG" | jq -r '.defaultEndpoint // ""')}" + [ -z "$API_KEY" ] && API_KEY=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$EP" 'if $e != "" then .endpoints[$e].apiKey // "" else (.apiKey // "") end')") + BASE_URL=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$EP" 'if $e != "" then .endpoints[$e].baseUrl // "" else (.baseUrl // "") end')") + ``` + If `API_KEY` is empty after resolution, fall back to the caller-provided `PROVIDER`-specific env var (e.g. `OPENAI_API_KEY`) which pi reads itself. +3. **Resolve the agent directory** — pi reads endpoints/credentials/sessions from an "agent dir" (default `~/.pi/agent`). It is overridable via the `PI_CODING_AGENT_DIR` env var, which `getAgentDir()` honors. Use `AGENT_DIR` from the caller, else `$PI_CODING_AGENT_DIR`, else `~/.pi/agent`: + ```bash + AGENT_DIR="${AGENT_DIR:-${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}}" + ``` + Running pi with a custom `AGENT_DIR` keeps all pi state inside the current project — required in git-worktree/sandboxed sessions where writing `$HOME/.pi` would be rejected as out-of-worktree. When set, export it to pi (`PI_CODING_AGENT_DIR="$AGENT_DIR"`). +5. **Ensure the endpoint is in models.json (idempotent merge)** — pi has no `--base-url` flag; it reads custom endpoints from `$AGENT_DIR/models.json`. When `BASE_URL` is non-empty, merge `baseUrl` and register the current `MODEL` (so pi stops warning "Model not found" and the model shows in `--list-models`). Compare the **compact** result against the existing content and write only when it differs — a matching value skips the write, avoiding an out-of-worktree write on every run once configured. **Use a separate `PROVIDER_KEY` for the models.json write so the caller's `PROVIDER` (possibly empty, meaning "let pi choose its default") is never mutated or leaked into the pi command.** `openai` is only a fallback for the models.json key, since custom endpoints are OpenAI-compatible: + ```bash + if [ -n "$BASE_URL" ]; then + PROVIDER_KEY="${PROVIDER:-openai}" + mkdir -p "$AGENT_DIR" + EXISTING=$(cat "$AGENT_DIR/models.json" 2>/dev/null || echo '{}') + NEW=$(echo "$EXISTING" | jq -c --arg provider "$PROVIDER_KEY" --arg baseUrl "$BASE_URL" --arg model "$MODEL" \ + '.providers[$provider] = (.providers[$provider] // {}) | + .providers[$provider].baseUrl = $baseUrl | + if $model != "" then + (.providers[$provider].models //= []) | + .providers[$provider].models |= ( + if any(.id == $model) then . else . + [{id: $model}] end + ) + else . end') + if [ "$NEW" != "$EXISTING" ]; then + echo "$NEW" > "$AGENT_DIR/models.json.tmp" && mv "$AGENT_DIR/models.json.tmp" "$AGENT_DIR/models.json" + fi + fi + ``` +6. **Collect git + CLAUDE.md context (delegate only)** — review already sends these via `APPEND_PATHS` (CLAUDE.md, GIT_FILE, DIFF_FILE), so gate this collection on `MODE=delegate` to avoid duplicate `--append-system-prompt` entries. For delegate, ensure `APPEND_PATHS` carries the user and project CLAUDE.md files (the caller omits them), then capture `git status --short`, `git diff --stat`, `git log --oneline -10`, and the current branch into a temp file. Record the git file as `GIT_FILE` and add it to `APPEND_PATHS` (plus `CLEANUP_FILES` so it is removed when pi exits). This gives pi the same situational awareness the caller has: + ```bash + if [ "$MODE" = "delegate" ]; then + # Delegate: caller omits APPEND_PATHS — add CLAUDE.md here + [ -f "$HOME/.claude/CLAUDE.md" ] && APPEND_PATHS+=("$HOME/.claude/CLAUDE.md") + [ -f "CLAUDE.md" ] && APPEND_PATHS+=("CLAUDE.md") + # Delegate: collect git context unless --no-git + if [ "$NO_GIT" != "true" ]; then + GIT_FILE=$(mktemp /tmp/pi-gitctx.XXXXXX) + { git status --short; git diff --stat; git log --oneline -10; git branch --show-current; } > "$GIT_FILE" + APPEND_PATHS+=("$GIT_FILE") + CLEANUP_FILES+=("$GIT_FILE") + fi + fi + ``` +7. **Doctor mode** (`MODE=doctor`) — run the configuration check from the delegate skill's `references/doctor.md`: check `pi` is installed (`pi --version`), list config files, print the effective resolved settings, and run a connectivity probe (`pi -p --provider ... "Reply with exactly: OK"`). **Compose the variables resolved in step 3 into the doctor run** — export `PROVIDER`, `MODEL`, `API_KEY`, `BASE_URL`, `AGENT_DIR` (and `THINKING`) as environment variables in the same shell that executes the doctor script, since `references/doctor.md` consumes them purely as env vars and Claude Code's Bash tool is stateless across invocations. Without this, the doctor probe would run with empty credentials and misdiagnose the configured endpoint. Also `` pair, collected in an array. **Do NOT accumulate into a space-joined string and expand unquoted** — under zsh that expansion is a single argument, so pi receives the literal string `--append-system-prompt /path/CLAUDE.md` as one token and appends it as text instead of reading the file: + ```bash + APPENDS=() + for p in "${APPEND_PATHS[@]}"; do + [ -n "$p" ] && APPENDS+=(--append-system-prompt "$p") + done + ``` +9. **Assemble the command in an array**, then run it. Arrays keep every flag a distinct argument regardless of shell (zsh does not word-split unquoted variables): + ```bash + CMD=(pi -p) + # Provider: use the caller's resolved PROVIDER; else if a custom BASE_URL is set, + # use the models.json key (PROVIDER_KEY, default openai) so pi reaches the endpoint. + # Explicit if/elif — NOT `A && B || C && D` (left-associative parsing appends an + # empty --provider when PROVIDER is set but BASE_URL is empty). + if [ -n "$PROVIDER" ]; then + CMD+=(--provider "$PROVIDER") + elif [ -n "$BASE_URL" ]; then + CMD+=(--provider "$PROVIDER_KEY") + fi + [ -n "$MODEL" ] && CMD+=(--model "$MODEL") + [ -n "$API_KEY" ] && CMD+=(--api-key "$API_KEY") + CMD+=(--thinking "${THINKING:-max}") + [ -n "$TOOLS" ] && CMD+=(--tools "$TOOLS") + [ -n "$EXCLUDE_TOOLS" ] && CMD+=(--exclude-tools "$EXCLUDE_TOOLS") + CMD+=(--no-session --no-context-files --approve) + CMD+=("${APPENDS[@]}") + CMD+=("${FILE_REFS[@]}") # review-only: @file references as separate args + CMD+=("$TASK") + ``` +10. **Run in the background** via Bash with `run_in_background` — `PI_CODING_AGENT_DIR="$AGENT_DIR" "${CMD[@]}" completed +- exit: +- changed files: +- stdout: +- errors: +``` diff --git a/pi/skills/delegate/SKILL.md b/pi/skills/delegate/SKILL.md index 794f75d3..901ea09c 100644 --- a/pi/skills/delegate/SKILL.md +++ b/pi/skills/delegate/SKILL.md @@ -1,14 +1,14 @@ --- name: delegate -description: Delegates a coding task to pi (dev/pi), a minimal terminal coding harness. This skill should be used when the user asks to "use pi", "run pi", "delegate to pi", "let pi handle this", "ask pi to", "have pi do", or invokes /pi:delegate. It bridges the current Claude Code context to pi CLI for execution, passing relevant files, git state, and the task description. +description: Delegates a coding task to pi (dev/pi), a minimal terminal coding harness. This skill should be used when the user asks to "use pi", "run pi", "delegate to pi", "let pi handle this", "ask pi to", "have pi do", or invokes /pi:delegate. It bridges the current Claude Code context to the pi CLI, passing relevant files, git state, and the task description for execution by the pi-agent. user-invocable: true -argument-hint: " [--provider PROVIDER] [--model MODEL] [--api-key KEY] [--thinking LEVEL] [--tools TOOL_LIST] [--exclude-tools TOOL_LIST] [--no-files] [--no-git] | --edit-config [--local|--shared|--global] | --doctor" -allowed-tools: ["Bash(git:*)", "Bash(jq:*)", "Bash(ls:*)", "Bash(find:*)", "Bash(cat:*)", "Bash(mkdir:*)", "Bash(echo:*)", "Bash(command:*)", "Bash(pi:*)", "Read", "Grep", "Glob"] +argument-hint: " [--endpoint ENDPOINT] [--provider PROVIDER] [--model MODEL] [--api-key KEY] [--thinking LEVEL] [--tools TOOL_LIST] [--exclude-tools TOOL_LIST] [--no-git] | --edit-config [--local|--shared|--global] | --list-models | --doctor" +allowed-tools: ["Task", "Bash(git:*)", "Bash(jq:*)", "Bash(ls:*)", "Bash(find:*)", "Bash(cat:*)", "Bash(mkdir:*)", "Bash(mv:*)", "Bash(echo:*)", "Bash(command:*)", "Bash(grep:*)", "Bash(head:*)", "Bash(vi:*)", "Read", "Grep", "Glob"] --- # CRITICAL: pi CLI Integration -This skill delegates a task to the `pi` CLI tool (`@earendil-works/pi-coding-agent`). It is the ONLY entry point for the pi plugin. +This skill delegates a task to the `pi` CLI tool (`@earendil-works/pi-coding-agent`) via the dedicated `pi:pi-agent` execution layer. Config is read endpoint-first (same format as `/pi:review`); legacy flat fields remain as a fallback. ## Before Execution: Check Installation @@ -35,80 +35,40 @@ Then stop — do not proceed without pi installed. User preferences persist across invocations via JSON files. The resolution chain (highest priority first): 1. **CLI flag** (from `$ARGUMENTS`) -2. **Process env** — `PI_PROVIDER`, `PI_MODEL`, `PI_API_KEY`, `PI_BASE_URL`, `PI_THINKING`, `PI_TOOLS` -3. **`.claude/pi.local.json`** — project-specific overrides, gitignored +2. **`.claude/pi.local.json`** — project-specific overrides, gitignored +3. **`.claude/pi.json`** — project shared defaults, committed 4. **`~/.claude/pi.local.json`** — global user-wide defaults 5. **pi's own defaults** (pi decides its own default provider and model) +Provider/model/baseUrl/apiKey come from these files (or CLI flags). API keys reach pi via `--api-key` — nothing is written to `models.json` except `baseUrl`. pi itself reads the usual provider env vars (`OPENAI_API_KEY`, etc.) as its own last-resort defaults. + ### Settings file format -Settings files only override what the user wants to change. All fields are optional — omit any field to let pi's own default apply. +The settings file uses the **same named-endpoint format as `/pi:review`** — endpoint-first. Legacy flat fields (`provider`, `model`, `baseUrl`, `apiKey`) are still honored as a fallback when no `defaultEndpoint` is set, so existing configs keep working. Values can reference environment variables using `$VAR` or `${VAR}` syntax — they are resolved at read time. ```json { - "provider": "", - "model": "", - "baseUrl": "", - "apiKey": "", - "thinking": "", - "tools": "", - "excludeTools": "", - "noFiles": false, - "noGit": false + "endpoints": { + "local-proxy": { + "provider": "openai", + "baseUrl": "http://10.10.0.195:8317/v1", + "models": ["gemini-3.6-flash-high", "gemini-3.6-pro"] + } + }, + "defaultEndpoint": "local-proxy", + "defaultModel": "gemini-3.6-flash-high" } ``` -Only include fields the user wants to override. Partial files are fine — the chain merges per-field. - -### Reading settings - -Before parsing `$ARGUMENTS`, read the settings files in priority order (lowest first, so each overrides the previous): - -```bash -# Start with empty config -CONFIG='{}' - -# 1. Global personal (lowest priority) -if [ -f "$HOME/.claude/pi.local.json" ]; then - CONFIG=$(jq -s '.[0] * .[1]' /dev/stdin "$HOME/.claude/pi.local.json" 2>/dev/null <<<"$CONFIG" || echo "$CONFIG") -fi - -# 2. Project shared -if [ -f ".claude/pi.json" ]; then - CONFIG=$(jq -s '.[0] * .[1]' /dev/stdin ".claude/pi.json" 2>/dev/null <<<"$CONFIG" || echo "$CONFIG") -fi - -# 3. Project personal (highest file priority) -if [ -f ".claude/pi.local.json" ]; then - CONFIG=$(jq -s '.[0] * .[1]' /dev/stdin ".claude/pi.local.json" 2>/dev/null <<<"$CONFIG" || echo "$CONFIG") -fi -``` +Each endpoint key has `provider` (required), optional `baseUrl`, optional `apiKey`, and `models` (array). Only include fields you want to override — partial files merge per-field across the chain. -Then extract values, resolving environment variable references. After file-based config, process env vars (`PI_*`) override any JSON value: +**Note on precedence:** endpoint-first resolution means that once a `defaultEndpoint` exists anywhere in the merged chain, the active endpoint wins and a project's legacy flat fields (`provider`/`model`/`baseUrl`) are ignored for that key. If you need a project to override the global endpoint, set that project's `.claude/pi.local.json` `defaultEndpoint` to its own endpoint — the flat fields are only consulted when no `defaultEndpoint` is set at all. -```bash -# Resolve env vars in a JSON value: "$VAR" or "${VAR}" → actual value -resolve_env() { - local val="$1" - while [[ "$val" =~ \$\{?([a-zA-Z_][a-zA-Z0-9_]*)\}? ]]; do - local var_name="${BASH_REMATCH[1]}" - local var_value="${!var_name:-}" - val="${val//${BASH_REMATCH[0]}/$var_value}" - done - echo "$val" -} +### Reading settings -# Read from merged config (files), then let process env override -PROVIDER=$(resolve_env "${PI_PROVIDER:-$(echo "$CONFIG" | jq -r '.provider // ""')}") -MODEL=$(resolve_env "${PI_MODEL:-$(echo "$CONFIG" | jq -r '.model // ""')}") -BASE_URL=$(resolve_env "${PI_BASE_URL:-$(echo "$CONFIG" | jq -r '.baseUrl // ""')}") -API_KEY=$(resolve_env "${PI_API_KEY:-$(echo "$CONFIG" | jq -r '.apiKey // ""')}") -THINKING=$(resolve_env "${PI_THINKING:-$(echo "$CONFIG" | jq -r '.thinking // "low"')}") -TOOLS=$(resolve_env "${PI_TOOLS:-$(echo "$CONFIG" | jq -r '.tools // ""')}") -EXCLUDE_TOOLS=$(resolve_env "${PI_EXCLUDE_TOOLS:-$(echo "$CONFIG" | jq -r '.excludeTools // ""')}") -``` +Read the settings files in priority order (lowest first, so each overrides the previous), resolving endpoint-first with flat-field fallback, CLI `--endpoint`/`--model`/`--api-key` overrides, and the model-ownership check. The full snippet lives in `references/settings.md` — run it before building the pi-agent prompt so `$PROVIDER`/`$MODEL`/`$ENDPOINT`/`$API_KEY`/`$BASE_URL` are resolved. It yields `PROVIDER`, `MODEL`, `ENDPOINT`, `API_KEY`, `BASE_URL`, `THINKING`, `TOOLS`, `EXCLUDE_TOOLS`. ### `--edit-config` flag @@ -120,6 +80,8 @@ When `$ARGUMENTS` is exactly `--edit-config` (with optional scope flag), open th | Project shared | `--edit-config --shared` | `.claude/pi.json` | Team defaults, committed | tracked | | Global personal | `--edit-config --global` or `--edit-config -g` | `~/.claude/pi.local.json` | User-wide across all projects | user home | +**Shared scope (`--shared`) is committed to git** — never put a literal `apiKey` in it. The `apiKey` field there must be a `$ENV_VAR` reference (e.g. `"$MY_API_KEY"`) so no secret is committed. Literal keys belong in the personal scopes (`.claude/pi.local.json`, `~/.claude/pi.local.json`). + ```bash # Detect scope if [[ "$ARGUMENTS" == *"--global"* || "$ARGUMENTS" == *"-g"* ]]; then @@ -131,20 +93,15 @@ else CONFIG_PATH=".claude/pi.local.json" fi -# Create if not exists -mkdir -p "$(dirname "$CONFIG_PATH")" +# Create if not exists (seed matches the review skill's template) +mkdir -p "${CONFIG_PATH%/*}" if [ ! -f "$CONFIG_PATH" ]; then cat > "$CONFIG_PATH" << 'EOF' { - "provider": "", - "model": "", - "baseUrl": "", - "apiKey": "", - "thinking": "", - "tools": "", - "excludeTools": "", - "noFiles": false, - "noGit": false + "endpoints": {}, + "defaultEndpoint": "", + "defaultModel": "", + "thinking": "" } EOF fi @@ -155,9 +112,34 @@ ${EDITOR:-vi} "$CONFIG_PATH" Report: "Settings file created/opened at ``. Changes take effect on the next `/pi:delegate` invocation." +### `--list-models` flag + +When `$ARGUMENTS` is exactly `--list-models`, read the merged config and display all configured endpoints and their models: + +```bash +# Endpoints present → list them; otherwise fall back to flat fields +if echo "$CONFIG" | jq -e '.endpoints | length > 0' >/dev/null 2>&1; then + echo "$CONFIG" | jq -r ' + .defaultEndpoint as $def | + .defaultModel as $defm | + ((.endpoints // {}) | to_entries[] | + "\(.key)" + if .key == $def then " (default)" else "" end + + " → " + .value.provider + + ":" + + ((.value.models // []) | join(", ")) + + if .key == $def and $defm != "" then " ← active: " + $defm else "" end + ) + ' +else + echo "$CONFIG" | jq -r '"flat: " + (.provider // "") + " / " + (.model // "") + " @ " + (.baseUrl // "(default)")' +fi +``` + +Then stop — do not proceed to delegate. + ### `--doctor` flag -When `$ARGUMENTS` is exactly `--doctor`, run a comprehensive configuration check. See `references/doctor.md` for the script. +When `$ARGUMENTS` is exactly `--doctor`, run a comprehensive configuration check. **Delegate it to `pi:pi-agent` with `MODE: doctor`** — the agent holds `Bash(pi:*)` (which delegate's allowed-tools deliberately does not, to enforce the delegation contract), so it can run the pi installation/connectivity probes in `references/doctor.md`. Pass the resolved `$PROVIDER`/`$MODEL`/`$THINKING` (and `$ENDPOINT` if a CLI `--endpoint` selected one) as inputs; **do NOT pass `$API_KEY`/`$BASE_URL`** — the pi-agent re-reads them from the settings files itself, so the key never enters the model's context and doctor inherits the agent's endpoint resolution. The agent runs the doctor script and returns the check results. ## Argument Parsing @@ -165,13 +147,13 @@ Parse `$ARGUMENTS` to extract the task description and optional flags. The task | Flag | Description | Source Priority | |------|-------------|-----------------| -| `--provider` | LLM provider (anthropic, openai, google, etc.) | CLI > settings > pi's default | -| `--model` | Model pattern or ID (e.g. `claude-sonnet-4-20250514`, `openai/gpt-4o`) | CLI > settings > pi's default | +| `--endpoint` | Endpoint key name (must match a key in settings `endpoints`) | CLI > settings > `defaultEndpoint` | +| `--provider` | LLM provider (anthropic, openai, google, etc.) | CLI > settings > endpoint/provider > pi's default | +| `--model` | Model pattern or ID (e.g. `claude-sonnet-4-20250514`, `openai/gpt-4o`) | CLI > settings > endpoint models > pi's default | | `--api-key` | API key for the provider | CLI > settings > env var or config file | -| `--thinking` | Thinking level (off/minimal/low/medium/high/xhigh/max) | CLI > settings > `low` | +| `--thinking` | Thinking level (off/minimal/low/medium/high/xhigh/max) | CLI > settings > `max` | | `--tools` | Comma-separated allowed tools list | CLI > settings > `read,bash,write,edit,grep,find,ls` | | `--exclude-tools` | Comma-separated blocked tools list | CLI > settings > (none) | -| `--no-files` | Skip collecting file context | CLI > settings > `false` | | `--no-git` | Skip collecting git context | CLI > settings > `false` | ### Resolution order per flag @@ -180,162 +162,50 @@ For each flag, resolve the value by checking CLI flag first, then settings file, 1. Parse `$ARGUMENTS` for that flag. If present, use it. 2. Otherwise, read from `$CONFIG` (the merged settings). If non-null/non-empty, use it. -3. Otherwise, use pi's built-in default (see below). - -### Defaults behavior - -- **provider**: No default — let pi's own default decide. The settings chain (CLI > config file > pi's internal default) resolves the value. -- **model**: No default — let pi's own default decide. Only pass `--model` if the user explicitly specified it or the settings file has a value. -- **thinking**: Default to `low` to keep responses fast and cheap. -- **tools**: Default to full toolset `read,bash,write,edit,grep,find,ls`. -- **base-url**: When resolved, write to `~/.pi/agent/models.json` for the provider (defaults to `openai`). The user can override the provider explicitly via CLI flag. - -## Context Collection Strategy - -Collect context from the current working directory before calling pi. The goal is to give pi the same situational awareness that Claude Code has. - -### 1. Pass CLAUDE.md as System Prompt Context - -**pi has `read`, `grep`, `find`, and `ls` tools** — it can explore the codebase on its own. Do not pass `@.` file references (pi errors on directory paths). Just pass the task description and let pi use its tools to read what it needs. - -However, **always pass the CLAUDE.md files** as system prompt context so pi understands the project and user conventions. `--append-system-prompt` accepts file paths directly — pi reads them automatically. - -```bash -# Build CLAUDE.md context — pass file paths, pi reads them -CLAUDE_CONTEXT="" -if [ -f "$HOME/.claude/CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt $HOME/.claude/CLAUDE.md" -fi -if [ -f "CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt CLAUDE.md" -fi -``` - -### 2. Collect Git Context (unless `--no-git`) - -```bash -git status --short -git diff --stat -git log --oneline -10 -``` - -These go into `--append-system-prompt` as structured context. - -### 3. Collect Directory Structure - -```bash -ls -la -``` - -Or for deeper context: -```bash -find . -maxdepth 2 -type f 2>/dev/null | head -50 -``` - -## Executing pi - -### Command Construction +3. Otherwise, use pi's built-in default. -Build the pi command with these components in order: +### Endpoint resolution -1. **Base**: `pi -p` (print mode, non-interactive) -2. **Provider/Model flags** (use resolved values from settings chain, only pass if non-default): - - `--provider ` (from settings or pi's default) - - `--model ` (only if specified in settings or CLI) - - `--api-key ` (only if resolved from settings or CLI flag) - - `--thinking low` (default, only pass if needed) -3. **Custom base URL**: If `baseUrl` is resolved from settings or CLI flag, write it to `~/.pi/agent/models.json` for the provider before calling pi (see Base URL section below). Never pass `--base-url` to pi — pi does not support it as a CLI flag. -4. **Tool restrictions** (only if user specified): - - `--tools ` (only if user wants to restrict) - - `--exclude-tools ` (only if user specified) -5. **Session control**: `--no-session --no-context-files --approve` -6. **CLAUDE.md context**: `$CLAUDE_CONTEXT` (built above — passes `~/.claude/CLAUDE.md` and `./CLAUDE.md` as `--append-system-prompt` file paths, pi reads them automatically) -7. **Appended context**: `--append-system-prompt "context block"` (for git status, directory listing, etc.) -8. **Task description**: The quoted task description as the final argument +1. If `--endpoint` is specified, use it as the key into `endpoints` config. +2. If `--model` is specified without `--endpoint`, scan all endpoints for a model matching the ID — use the first match's endpoint. +3. Otherwise, use `defaultEndpoint` from settings. +4. Resolve the pi provider from the endpoint's `provider` field; the model from `defaultModel` then the endpoint's first `models` entry. +5. If no endpoint is configured, fall back to legacy flat fields (`provider`/`model`/`baseUrl`). -### Default provider note +## Delegating to pi-agent -Do not hardcode a provider. Resolve from the settings chain (CLI flag > config file > pi's built-in default). pi's own default is `google` (Gemini). If the user has set `"provider": "google"` in `~/.claude/pi.local.json`, that takes effect without needing CLI flags. +Do NOT run `pi` directly. After parsing arguments and resolving settings, launch the dedicated `pi:pi-agent` execution layer with the Task tool. It builds the command, runs pi in the background, and verifies the outcome. -### Pattern for appended context - -First build the CLAUDE.md context, then format the git/project context as a single block: - -```bash -# Build CLAUDE.md context — pass file paths, pi reads them -CLAUDE_CONTEXT="" -if [ -f "$HOME/.claude/CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt $HOME/.claude/CLAUDE.md" -fi -if [ -f "CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt CLAUDE.md" -fi -``` - -Then format the git context: +**Pass these fields in the agent prompt:** ``` ---append-system-prompt "Project context at $(pwd): -Working directory: $(basename $(pwd)) -Git status: -$(git status --short 2>/dev/null || echo '(not a git repo)') - -Recent commits: -$(git log --oneline -10 2>/dev/null || echo '') - -Current branch: $(git branch --show-current 2>/dev/null || echo '')" -``` - -### Execute - -Always use `Bash` with `run_in_background` — pi -p is a single-shot command, not a continuous stream. Monitor is designed for event streams (like `tail -f`) and will timeout when pi's output is delayed or batched. - -**Do not add a shell `timeout`** — pi tasks can be heavy and may run for a long time. Let pi run to completion naturally. - -```bash -# Build CLAUDE.md context — pass file paths, pi reads them -CLAUDE_CONTEXT="" -if [ -f "$HOME/.claude/CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt $HOME/.claude/CLAUDE.md" -fi -if [ -f "CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt CLAUDE.md" -fi - -# Build the pi command — no file references, pi uses its tools to explore -PI_CMD="pi -p --thinking low${API_KEY:+ --api-key $API_KEY} --no-session --no-context-files --approve $CLAUDE_CONTEXT \"task description\"" - -# Run in background — no timeout, let pi finish naturally -bash -c "$PI_CMD 2>&1" & +MODE: delegate +TASK: +PROVIDER: +MODEL: +API_KEY: +ENDPOINT: +AGENT_DIR: +THINKING: +TOOLS: +EXCLUDE_TOOLS: +NO_GIT: +APPEND_PATHS: <(omit — the pi-agent adds CLAUDE.md itself, and collects git context per NO_GIT)> ``` -### Important: Pi's Default Provider +**Do NOT pass a config-file `API_KEY` or `BASE_URL`** — the pi-agent reads them from the settings files itself (so config secrets never enter the model's context). The only key that may be passed is a CLI `--api-key` the user typed explicitly, which overrides the config key. -Do not hardcode a provider. Resolve from the settings chain (CLI flag > config file > pi's built-in default). pi's own default is `google` (Gemini). If the user has set `"provider": "google"` in `~/.claude/pi.local.json`, that takes effect without needing CLI flags. +The pi-agent handles: agent-dir resolution, CLAUDE.md context, git context (unless `--no-git`), command construction, background execution (no timeout), and verification via `git diff --stat`. -### Base URL via models.json +### Worktree / sandboxed sessions -pi does not support `--base-url` CLI flag or `OPENAI_BASE_URL` environment variable. Custom API endpoints are configured through `~/.pi/agent/models.json`. When `baseUrl` is resolved from settings or CLI flag, the skill writes it to pi's models.json before calling pi — then calls pi without `--base-url`. +In a git-worktree or sandboxed session, `$HOME/.pi` is outside the worktree and writes to it get rejected. Two mitigations, applied by the pi-agent: +- `AGENT_DIR` — pass a worktree-local directory (e.g. `.pi-agent`); pi keeps all its state there via `PI_CODING_AGENT_DIR`. Best for isolated sessions. +- Conditional write — once a `BASE_URL` already matches the agent-dir `models.json`, the pi-agent skips the write entirely, so an already-configured endpoint causes no out-of-worktree write. -```bash -# When base-url is resolved, write/merge into pi's models.json -if [ -n "$BASE_URL" ]; then - PROVIDER="${PROVIDER:-openai}" - mkdir -p "$HOME/.pi/agent" - EXISTING=$(cat "$HOME/.pi/agent/models.json" 2>/dev/null || echo '{}') - # Merge: set baseUrl and apiKey for the provider - echo "$EXISTING" | jq --arg provider "$PROVIDER" \ - --arg baseUrl "$BASE_URL" \ - --arg apiKey "${API_KEY:-}" \ - '.providers[$provider] = (.providers[$provider] // {}) | - .providers[$provider].baseUrl = $baseUrl | - if $apiKey != "" then .providers[$provider].apiKey = $apiKey else . end' \ - > "$HOME/.pi/agent/models.json.tmp" && \ - mv "$HOME/.pi/agent/models.json.tmp" "$HOME/.pi/agent/models.json" -fi -``` +### `--base-url` note -The `--provider` defaults to `openai` when `--base-url` is used (since custom endpoints are typically OpenAI-compatible). The user can override with `--provider` explicitly. +pi does not support a `--base-url` CLI flag. Custom endpoints are configured through the agent-dir `models.json`, which the pi-agent writes when `BASE_URL` is non-empty — never pass `--base-url` to pi. ## Handling Output @@ -352,20 +222,11 @@ The `--provider` defaults to `openai` when `--base-url` is used (since custom en ### On Success (exit code 0) -**Always check for modified files first** — this is where pi's real output lives: - -```bash -git diff --stat -``` - -- If git shows changes, present those changes to the user. Describe what pi modified (additions, deletions, file count). -- If git shows no changes and exit was 0, the task was understood but resulted in no modifications (read-only analysis, conceptual questions, or the task was already satisfied). - -Include pi's stdout in the report if it's non-empty, but the file changes are the primary deliverable. +The pi-agent reports the changed files. Present those changes to the user and describe what pi modified (additions, deletions, file count). If git shows no changes and exit was 0, the task was understood but resulted in no modifications (read-only analysis, conceptual questions, or the task was already satisfied). ### On Error (exit code 1+) -Show the error message from stderr. Common error causes: +Show the error message from the pi-agent's report. Common error causes: - pi not configured (no API key) - Provider/model not available - Task interrupted or killed @@ -375,97 +236,33 @@ Show the error message from stderr. Common error causes: ### Basic task with file context User: `/pi:delegate review the TypeScript types in src/` -Claude: Builds CLAUDE.md context, collects git context, then runs: -```bash -CLAUDE_CONTEXT="" -if [ -f "$HOME/.claude/CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt $HOME/.claude/CLAUDE.md" -fi -if [ -f "CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt CLAUDE.md" -fi - -PI_CMD="pi -p --thinking low --no-session --no-context-files --approve $CLAUDE_CONTEXT \"review the TypeScript types in src/\"" -bash -c "$PI_CMD 2>&1" & -``` +Claude: Reads settings, resolves the endpoint, then launches `pi:pi-agent` with `MODE: delegate` and the task description. pi-agent builds the pi command, runs it in the background, and reports `git diff --stat`. ### Specific model User: `/pi:delegate refactor this component --model claude-sonnet-4-20250514` -Claude: Builds CLAUDE.md context, passes --model flag: -```bash -CLAUDE_CONTEXT="" -if [ -f "$HOME/.claude/CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt $HOME/.claude/CLAUDE.md" -fi -if [ -f "CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt CLAUDE.md" -fi - -PI_CMD="pi -p --model claude-sonnet-4-20250514 --thinking low --no-session --no-context-files --approve $CLAUDE_CONTEXT --append-system-prompt \"Git status: ...\" \"refactor this component\"" -bash -c "$PI_CMD 2>&1" & -``` - -### Custom base URL (OpenAI-compatible proxy via models.json) -User: `/pi:delegate write unit tests for this module --provider openai --model gemini-3.6-flash-high` +Claude: Resolves `--model` from the flag (overriding settings), then passes `MODEL: claude-sonnet-4-20250514` to pi-agent. -Claude: Checks `~/.claude/pi.local.json` for `baseUrl`, writes it to `~/.pi/agent/models.json` for the `openai` provider, then runs: -```bash -CLAUDE_CONTEXT="" -if [ -f "$HOME/.claude/CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt $HOME/.claude/CLAUDE.md" -fi -if [ -f "CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt CLAUDE.md" -fi +### Custom base URL (OpenAI-compatible proxy) +User: `/pi:delegate write unit tests for this module --endpoint local-proxy` -PI_CMD="pi -p --provider openai --model gemini-3.6-flash-high --thinking low --no-session --no-context-files --approve $CLAUDE_CONTEXT \"write unit tests for this module\"" -bash -c "$PI_CMD 2>&1" & -``` +Claude: Resolves the `local-proxy` endpoint from settings, extracts its `baseUrl`, and passes it to pi-agent, which writes it to the agent-dir `models.json` (default `~/.pi/agent/models.json`) before running pi. ### Read-only analysis User: `/pi:delegate audit the security of this codebase --tools read,grep,find,ls` -Claude: Builds CLAUDE.md context, passes --tools to restrict pi to read-only tools: -```bash -CLAUDE_CONTEXT="" -if [ -f "$HOME/.claude/CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt $HOME/.claude/CLAUDE.md" -fi -if [ -f "CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt CLAUDE.md" -fi +Claude: Passes `TOOLS: read,grep,find,ls` to pi-agent so pi is restricted to read-only tools. -PI_CMD="pi -p --thinking low --tools read,grep,find,ls --no-session --no-context-files --approve $CLAUDE_CONTEXT \"audit the security of this codebase\"" -bash -c "$PI_CMD 2>&1" & -``` +### No git context, just conceptual +User: `/pi:delegate explain how React reconciliation works --no-git` -### No file context, just conceptual -User: `/pi:delegate explain how React reconciliation works --no-files` - -Claude: Builds CLAUDE.md context, sends the prompt: -```bash -CLAUDE_CONTEXT="" -if [ -f "$HOME/.claude/CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt $HOME/.claude/CLAUDE.md" -fi -if [ -f "CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt CLAUDE.md" -fi - -PI_CMD="pi -p --thinking low --no-session --no-context-files --approve $CLAUDE_CONTEXT \"explain how React reconciliation works\"" -bash -c "$PI_CMD 2>&1" & -``` +Claude: Passes `NO_GIT: true` along with the task description to pi-agent, so it skips collecting git status/log context. ## Important Notes - pi MUST be installed globally. The skill checks and blocks if not found. -- The skill uses `pi -p` (print mode) for all tasks — this is non-interactive and produces text output. -- Settings in `.claude/pi.local.json` and `.claude/pi.json` are on the reading path — shared settings (`.claude/pi.json`) is tracked, personal settings (`.claude/pi.local.json`) is gitignored by `**/.claude/*.local.*`. -- `--base-url` writes to `~/.pi/agent/models.json` (pi's global provider config) — this is a one-time setup per endpoint, not per-session. -- `--no-session` prevents pi from creating session files. -- `--no-context-files` prevents pi from reading its own AGENTS.md/CLAUDE.md (which could conflict with the current project's context). -- `--approve` skips any project trust prompts (non-interactive mode). -- **CLAUDE.md context is always passed** via `--append-system-prompt` as file paths — `~/.claude/CLAUDE.md` (user global) and `./CLAUDE.md` (project). pi reads them automatically. +- **Never run pi directly — always delegate to `pi:pi-agent`.** The agent is the plugin's single execution path and owns backgrounding, verification, and error handling. +- Settings are shared with `/pi:review` via the same file chain (`.claude/pi.local.json`, `.claude/pi.json`, `~/.claude/pi.local.json`) and the same endpoint-first format. +- `--no-session` prevents pi from creating session files; `--no-context-files` prevents pi from reading its own AGENTS.md/CLAUDE.md (which could conflict with the current project's context); `--approve` skips any project trust prompts. The pi-agent adds these. +- **CLAUDE.md context is always passed** by pi-agent via `--append-system-prompt` as file paths — `~/.claude/CLAUDE.md` (user global) and `./CLAUDE.md` (project). - To configure pi (provider, model, base URL), run `/pi:setup` instead of passing flags manually. -- Git context is passed via `--append-system-prompt` as structured text. \ No newline at end of file diff --git a/pi/skills/delegate/references/doctor.md b/pi/skills/delegate/references/doctor.md index 40977405..ed3dcbc1 100644 --- a/pi/skills/delegate/references/doctor.md +++ b/pi/skills/delegate/references/doctor.md @@ -9,9 +9,9 @@ echo "" # 1. Check installation echo "1. pi installation:" if command -v pi >/dev/null 2>&1; then - echo " ✅ pi $(pi --version 2>&1 | head -1) at $(command -v pi)" + echo " [OK] pi $(pi --version 2>&1 | head -1) at $(command -v pi)" else - echo " ❌ pi not installed" + echo " [FAIL] pi not installed" fi # 2. Check config files @@ -19,9 +19,9 @@ echo "" echo "2. Config files:" for f in "$HOME/.claude/pi.local.json" ".claude/pi.json" ".claude/pi.local.json"; do if [ -f "$f" ]; then - echo " ✅ $f" + echo " [OK] $f" else - echo " ⬜ $f (not present)" + echo " [MISSING] $f (not present)" fi done @@ -32,28 +32,30 @@ echo " Provider: ${PROVIDER:-pi default}" echo " Model: ${MODEL:-pi default}" echo " Base URL: ${BASE_URL:-not set}" echo " API Key: ${API_KEY:+set (${#API_KEY} chars)}" -echo " Thinking: ${THINKING:-low}" +echo " Thinking: ${THINKING:-max}" # 4. Test connectivity echo "" echo -n "4. Connectivity test: " +# Resolve the agent dir the same way pi-agent does (matches AGENT_DIR / PI_CODING_AGENT_DIR). +AGENT_DIR="${AGENT_DIR:-${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}}" if [ -n "$BASE_URL" ]; then - mkdir -p "$HOME/.pi/agent" - EXISTING=$(cat "$HOME/.pi/agent/models.json" 2>/dev/null || echo '{}') + mkdir -p "$AGENT_DIR" + EXISTING=$(cat "$AGENT_DIR/models.json" 2>/dev/null || echo '{}') echo "$EXISTING" | jq --arg provider "${PROVIDER:-openai}" \ --arg baseUrl "$BASE_URL" \ '.providers[$provider] = (.providers[$provider] // {}) | .providers[$provider].baseUrl = $baseUrl' \ - > "$HOME/.pi/agent/models.json.tmp" && \ - mv "$HOME/.pi/agent/models.json.tmp" "$HOME/.pi/agent/models.json" + > "$AGENT_DIR/models.json.tmp" && \ + mv "$AGENT_DIR/models.json.tmp" "$AGENT_DIR/models.json" fi -TEST_OUTPUT=$(pi -p --provider "${PROVIDER:-openai}" --model "${MODEL:-gemini-3.6-flash-high}"${API_KEY:+ --api-key "$API_KEY"} --thinking low --no-session --no-context-files --approve "Reply with exactly: OK" 2>&1) +TEST_OUTPUT=$(PI_CODING_AGENT_DIR="$AGENT_DIR" pi -p --provider "${PROVIDER:-openai}" --model "${MODEL:-gemini-3.6-flash-high}"${API_KEY:+ --api-key "$API_KEY"} --thinking low --no-session --no-context-files --approve "Reply with exactly: OK" &1) TEST_EXIT=$? if [ "$TEST_EXIT" -eq 0 ] && echo "$TEST_OUTPUT" | grep -q "OK"; then - echo "✅ pi responded successfully" + echo "[OK] pi responded successfully" else - echo "❌ pi failed (exit $TEST_EXIT):" + echo "[FAIL] pi failed (exit $TEST_EXIT):" echo " $TEST_OUTPUT" | head -3 fi ``` \ No newline at end of file diff --git a/pi/skills/delegate/references/settings.md b/pi/skills/delegate/references/settings.md new file mode 100644 index 00000000..1836ef43 --- /dev/null +++ b/pi/skills/delegate/references/settings.md @@ -0,0 +1,114 @@ +# Delegate Settings Reading + +This reference holds the full config-reading snippet for `/pi:delegate`. Load it when the skill needs to resolve provider/model/baseUrl/apiKey from the settings files. + +## Reading settings + +Read the settings files in priority order (lowest first, so each overrides the previous): + +```bash +# Start with empty config +CONFIG='{}' + +# 1. Global personal (lowest file priority) +if [ -f "$HOME/.claude/pi.local.json" ]; then + CONFIG=$(jq -s '.[0] * .[1]' /dev/stdin "$HOME/.claude/pi.local.json" 2>/dev/null <<<"$CONFIG" || echo "$CONFIG") +fi + +# 2. Project shared +if [ -f ".claude/pi.json" ]; then + CONFIG=$(jq -s '.[0] * .[1]' /dev/stdin ".claude/pi.json" 2>/dev/null <<<"$CONFIG" || echo "$CONFIG") +fi + +# 3. Project personal (highest file priority) +if [ -f ".claude/pi.local.json" ]; then + CONFIG=$(jq -s '.[0] * .[1]' /dev/stdin ".claude/pi.local.json" 2>/dev/null <<<"$CONFIG" || echo "$CONFIG") +fi +``` + +Then extract values, resolving environment variable references. Endpoint-first with flat-field fallback: + +```bash +# Resolve env vars in a JSON value: "$VAR" or "${VAR}" → actual value. +# NOTE: requires bash — the `BASH_REMATCH` capture is bash-only; under zsh this +# returns the literal `$VAR`. Run the config-reading snippet under bash. +resolve_env() { + local val="$1" + while [[ "$val" =~ \$\{?([a-zA-Z_][a-zA-Z0-9_]*)\}? ]]; do + local var_name="${BASH_REMATCH[1]}" + local var_value="${!var_name:-}" + val="${val//${BASH_REMATCH[0]}/$var_value}" + done + echo "$val" +} + +# Endpoint-first; legacy flat fields fill in when no defaultEndpoint is set +ENDPOINT=$(resolve_env "$(echo "$CONFIG" | jq -r '.defaultEndpoint // ""')") +# Provider stays empty when unset — pi-agent lets pi's own default apply. +PROVIDER=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" 'if $e != "" then (.endpoints[$e].provider // "") else (.provider // "") end')") +MODEL=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" 'if $e != "" then (.defaultModel // "") else (.model // "") end')") +BASE_URL=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" 'if $e != "" then (.endpoints[$e].baseUrl // "") else (.baseUrl // "") end')") +API_KEY=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" 'if $e != "" then (.endpoints[$e].apiKey // "") else (.apiKey // "") end')") +THINKING=$(resolve_env "$(echo "$CONFIG" | jq -r '.thinking // "max"')") +TOOLS=$(resolve_env "$(echo "$CONFIG" | jq -r '.tools // ""')") +EXCLUDE_TOOLS=$(resolve_env "$(echo "$CONFIG" | jq -r '.excludeTools // ""')") +# Base model: resolve defaultModel (or endpoint's first model), used by the ownership +# check below after any CLI override is applied. +# If model still empty, use the first model from the endpoint. +if [ -z "$MODEL" ] || [ "$MODEL" = "null" ]; then + MODEL=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" '.endpoints[$e].models[0] // ""')") +fi + +# CLI override: --endpoint / --model re-resolve the active endpoint (highest priority). +# Match only the flag region (after the task description's first -- flag), so a task +# that merely mentions "--endpoint" does not hijack resolution. Accept an endpoint only +# if it exists in .endpoints. +FLAGS_REGION=$(echo "$ARGUMENTS" | awk '{ for(i=1;i<=NF;i++) if($i ~ /^--/) { for(j=i;j<=NF;j++) printf "%s%s", $j, (j settings), so do not override it. +if [ -z "$CLI_MODEL" ] && [ -n "$ENDPOINT" ] && [ -n "$MODEL" ] && [ "$MODEL" != "null" ]; then + IN_ENDPOINT=$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" --arg m "$MODEL" '(.endpoints[$e].models // []) | index($m) // -1') + if [ "$IN_ENDPOINT" = "-1" ]; then + MODEL=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" '.endpoints[$e].models[0] // ""')") + fi +fi +``` + +The `$VAR` references inside JSON values are resolved at read time by `resolve_env`. (pi reads its own provider env vars — `OPENAI_API_KEY`, etc. — as its last-resort default, so you do not need to set them when a config file or flag provides the key.) diff --git a/pi/skills/review/SKILL.md b/pi/skills/review/SKILL.md index d22cf8be..15777c62 100644 --- a/pi/skills/review/SKILL.md +++ b/pi/skills/review/SKILL.md @@ -3,7 +3,7 @@ name: review description: Reviews code using pi CLI with read-only tools. Delegates the review to pi (dev/pi) with a structured review rubric, running in read-only mode to prevent accidental edits. By default reviews uncommitted working tree changes (git diff HEAD) with pi restricted to the read tool; explicit targets (--branch/--diff/@file/PR) or --explore widen pi to read,grep,find,ls. Use when the user asks to "review code with pi", "pi review", "have pi review", "let pi review", or invokes /pi:review. user-invocable: true argument-hint: "[@target] [--branch BRANCH] [--diff RANGE] [--endpoint ENDPOINT] [--model MODEL] [--thinking LEVEL] [--explore] | --edit-config [--local|--shared|--global] | --list-models" -allowed-tools: ["Bash(git:*)", "Bash(jq:*)", "Bash(ls:*)", "Bash(find:*)", "Bash(cat:*)", "Bash(mkdir:*)", "Bash(echo:*)", "Bash(command:*)", "Bash(mktemp:*)", "Bash(rm:*)", "Bash(mv:*)", "Bash(sed:*)", "Bash(grep:*)", "Bash(head:*)", "Bash(gh:*)", "Bash(bash:*)", "Bash(pi:*)", "Read", "Grep", "Glob"] +allowed-tools: ["Task", "Bash(git:*)", "Bash(jq:*)", "Bash(ls:*)", "Bash(find:*)", "Bash(cat:*)", "Bash(mkdir:*)", "Bash(echo:*)", "Bash(command:*)", "Bash(mktemp:*)", "Bash(rm:*)", "Bash(mv:*)", "Bash(sed:*)", "Bash(grep:*)", "Bash(head:*)", "Bash(awk:*)", "Bash(tr:*)", "Bash(gh:*)", "Bash(bash:*)", "Bash(vi:*)", "Read", "Grep", "Glob"] --- # CRITICAL: pi Code Review @@ -59,7 +59,7 @@ Parse `$ARGUMENTS` to extract the review target and optional flags. The target i |------|-------------|-----------------| | `--endpoint` | Endpoint key name (must match a key in settings `endpoints`) | CLI > settings > `defaultEndpoint` | | `--model` | Model ID to use for this review | CLI > settings > (endpoint's first model) | -| `--thinking` | Thinking level (off/minimal/low/medium/high/xhigh/max) | CLI > settings > `low` | +| `--thinking` | Thinking level (off/minimal/low/medium/high/xhigh/max) | CLI > settings > `max` | | `--explore` | Force full read-only exploration tools (`read,grep,find,ls`) even for the default working-tree review. Without this, the default review gets `read` only. | CLI flag | ### Resolution order per flag @@ -80,20 +80,7 @@ For each flag, resolve the value by checking CLI flag first, then settings file, ### Base URL resolution -If the resolved endpoint has a `baseUrl`, write it to `~/.pi/agent/models.json` for the resolved provider before running pi: - -```bash -if [ -n "$BASE_URL" ]; then - mkdir -p "$HOME/.pi/agent" - EXISTING=$(cat "$HOME/.pi/agent/models.json" 2>/dev/null || echo '{}') - echo "$EXISTING" | jq --arg provider "$PROVIDER" \ - --arg baseUrl "$BASE_URL" \ - '.providers[$provider] = (.providers[$provider] // {}) | - .providers[$provider].baseUrl = $baseUrl' \ - > "$HOME/.pi/agent/models.json.tmp" && \ - mv "$HOME/.pi/agent/models.json.tmp" "$HOME/.pi/agent/models.json" -fi -``` +The pi-agent handles `baseUrl` — it conditionally writes it into the agent-dir `models.json` (pi has no `--base-url` flag) only when it differs from the existing value. The skill just passes the resolved `BASE_URL` to the agent; it does not write models.json itself. ## Review Target @@ -125,33 +112,7 @@ When the user passes `@filepath`, pass those file paths directly to pi as `@file ## Review Rubric (Embedded in Prompt) -The review prompt given to pi must cover these dimensions. Embed them as part of the task description, not as `--append-system-prompt`: - -### 1. Correctness & Logic -- Does the code do what it intends? Any off-by-one, race conditions, null pointer, or type errors? -- Are error paths handled (not just the happy path)? -- Are async operations properly awaited or chained? - -### 2. Code Quality & Maintainability -- Naming: do names reveal intent? (Mysterious Name) -- Duplication: is the same logic repeated? (Duplicated Code) -- Coupling: does a module reach into another's internals? (Feature Envy) -- Abstraction: is there speculative generality or missing domain types? (Speculative Generality, Primitive Obsession) -- Size: are functions/classes too large? Do they do one thing? - -### 3. Security -- Are user inputs validated/sanitized? -- Any hardcoded secrets, tokens, or credentials? -- Any injection vectors (SQL, command, path traversal)? - -### 4. Architecture & Design -- Does the change follow the project's established patterns? -- Does it introduce unnecessary dependencies? -- Is the change scoped appropriately (not shotgun surgery)? - -### 5. Testing -- Are there tests for the changed code? -- Do tests cover edge cases and error paths? +The review prompt given to pi must cover these dimensions. Embed them as part of the task description, not as `--append-system-prompt`. See `references/rubric.md` for the full five-dimension rubric — the TASK prompt in the agent launch embeds a condensed version of it. ## Context Collection @@ -159,15 +120,13 @@ The review prompt given to pi must cover these dimensions. Embed them as part of Always pass the CLAUDE.md files as system prompt context so pi understands the project and user conventions. `--append-system-prompt` accepts file paths directly — pi reads them automatically. +**CRITICAL: do not accumulate the flags into a space-joined variable and expand it unquoted.** Under zsh (the default shell on macOS) that expansion is a single argument, so pi receives the literal string `--append-system-prompt /path/CLAUDE.md` as one token and appends it as text instead of reading the file. The pi-agent assembles the command as an array, so the skill only needs to hand the agent the resolved context files: + ```bash -# Build CLAUDE.md context — pass file paths, pi reads them -CLAUDE_CONTEXT="" -if [ -f "$HOME/.claude/CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt $HOME/.claude/CLAUDE.md" -fi -if [ -f "CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt CLAUDE.md" -fi +# Collect CLAUDE.md paths to pass to the agent as separate --append-system-prompt args +APPEND_PATHS=() +[ -f "$HOME/.claude/CLAUDE.md" ] && APPEND_PATHS+=("$HOME/.claude/CLAUDE.md") +[ -f "CLAUDE.md" ] && APPEND_PATHS+=("CLAUDE.md") ``` ### 2. Git Context @@ -187,7 +146,7 @@ Capture the diff for the resolved target into a temp file, then pass its path vi DIFF_FILE=$(mktemp /tmp/pi-review-diff.XXXXXX) HAS_EXPLICIT_TARGET="" TASK_TEXT="" # free-text task description, if any -FILE_REFS="" # @file references, if any +FILE_REFS=() # @file references, one per array element # No target (default): uncommitted working tree changes (staged + unstaged vs HEAD) git diff HEAD > "$DIFF_FILE" @@ -217,7 +176,11 @@ fi if [[ "$ARGUMENTS" == *"@"* ]]; then HAS_EXPLICIT_TARGET="1" : > "$DIFF_FILE" # clear: @file reviews pass files, not a diff - FILE_REFS=$(echo "$ARGUMENTS" | grep -oE '@[^ ]+' | tr '\n' ' ') + # Read one @ref per line into the array (avoids zsh word-splitting pitfalls) + FILE_REFS=() + while IFS= read -r ref; do + [ -n "$ref" ] && FILE_REFS+=("$ref") + done < <(echo "$ARGUMENTS" | grep -oE '@[^ ]+' || true) fi # Free-text task description: only when there is no explicit target and no @file refs. @@ -228,20 +191,19 @@ if [ -z "$HAS_EXPLICIT_TARGET" ] && [[ "$ARGUMENTS" != *"@"* ]]; then fi ``` -Then build the diff context — this MUST be run as actual commands in the same shell, not left as prose: +Then build the diff context — this MUST be run as actual commands in the same shell, not left as prose. Collect context file paths into `APPEND_PATHS` (CLAUDE.md files from step 1, plus the diff when present) so the pi-agent emits each as its own `--append-system-prompt` argument: ```bash -# Diff context: only add the --append-system-prompt for the diff when a diff exists -DIFF_CONTEXT="" +# Context files: CLAUDE.md paths (from "Context Collection") plus the diff when non-empty if [ -s "$DIFF_FILE" ]; then - DIFF_CONTEXT="--append-system-prompt $DIFF_FILE" + APPEND_PATHS+=("$DIFF_FILE") fi ``` **Empty-diff guard (default target only):** if `git diff HEAD` produces no output and no explicit target was given (`HAS_EXPLICIT_TARGET` empty), the working tree is clean — report "No uncommitted changes to review — the working tree is clean. Use `/pi:review --branch `, `/pi:review --diff `, or `/pi:review ` to review committed code." and stop before invoking pi. Run the guard as a command: ```bash -if [ ! -s "$DIFF_FILE" ] && [ -z "$HAS_EXPLICIT_TARGET" ]; then +if [ ! -s "$DIFF_FILE" ] && [ -z "$HAS_EXPLICIT_TARGET" ] && [ -z "$TASK_TEXT" ]; then echo "No uncommitted changes to review — the working tree is clean. Use --branch, --diff, or a PR number." exit 0 fi @@ -249,22 +211,11 @@ fi ## Execution -Always use `Bash` with `run_in_background` — pi -p is a single-shot command, not a continuous stream. **Do not add a shell `timeout`** — reviews can be heavy. **Do not use Monitor.** +Do NOT run pi directly. After resolving settings and capturing the review target, launch the dedicated `pi:pi-agent` execution layer with the Task tool. It builds the pi command, runs it in the background, and returns pi's stdout. -```bash -# PREREQUISITE: run the "Reading settings" snippet from references/settings.md first. -# It defines $PROVIDER, $MODEL, $API_KEY, $THINKING used below. Without it, pi has no -# provider/model and fails. Run those commands in the SAME shell as this block. - -# Build CLAUDE.md context — pass file paths, pi reads them -CLAUDE_CONTEXT="" -if [ -f "$HOME/.claude/CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt $HOME/.claude/CLAUDE.md" -fi -if [ -f "CLAUDE.md" ]; then - CLAUDE_CONTEXT="$CLAUDE_CONTEXT --append-system-prompt CLAUDE.md" -fi +**PREREQUISITE:** run the "Reading settings" snippet from `references/settings.md` first — it defines `$PROVIDER`, `$MODEL`, `$API_KEY`, `$THINKING`. Then capture the diff (see "Context Collection") into `$DIFF_FILE`/`$GIT_FILE`, and resolve `$TOOLS`: +```bash # Tool selection — see "Review Target" resolution logic # Default (no target): read only, so pi cannot scan the codebase or run git. # Explicit target or --explore: read,grep,find,ls. Never include bash. @@ -277,23 +228,31 @@ fi # Collect git context (status/stat/log/branch) into a temp file, passed like the diff GIT_FILE=$(mktemp /tmp/pi-review-git.XXXXXX) { git status --short; git diff --stat; git log --oneline -20; git branch --show-current; } > "$GIT_FILE" +APPEND_PATHS+=("$GIT_FILE") # git context is a second append (besides CLAUDE.md + diff) +``` -# Build the pi review command with resolved variables -# $DIFF_CONTEXT / $GIT_CONTEXT carry the captured files via --append-system-prompt; empty when not applicable -# $FILE_REFS carries @file references; empty unless the user named files -# $TASK_TEXT carries the free-text task description; empty otherwise -PI_CMD="pi -p --provider $PROVIDER --model $MODEL${API_KEY:+ --api-key $API_KEY} --thinking ${THINKING:-low} --tools $TOOLS --no-session --no-context-files --approve $CLAUDE_CONTEXT $DIFF_CONTEXT --append-system-prompt $GIT_FILE $FILE_REFS ${TASK_TEXT:+$TASK_TEXT }\"Review the code in the provided diff${TASK_TEXT:+ (task: $TASK_TEXT)}. Use your tools only to read the files mentioned in the diff for context — do NOT search the rest of the codebase, do NOT run git. Focus on correctness, code quality, security, architecture, and testing. For each issue found, report: file:line: severity (HIGH/MEDIUM/LOW) + description + suggested fix. Group findings by severity. If no issues found, explicitly state that the code looks clean.\"" +Then launch `pi:pi-agent` with the Task tool, passing: -# Run via run_in_background (no trailing & — the tool backgrounds it; pi stdout must be captured) -# Clean up the temp files when pi exits -bash -c "$PI_CMD 2>&1; rm -f '$DIFF_FILE' '$GIT_FILE'" ``` +MODE: review +TASK: Review the code in the provided diff${TASK_TEXT:+ (task: $TASK_TEXT)}. Use your tools only to read the files mentioned in the diff for context — do NOT search the rest of the codebase, do NOT run git. Focus on correctness, code quality, security, architecture, and testing. For each issue found, report: file:line: severity (HIGH/MEDIUM/LOW) + description + suggested fix. Group findings by severity. If no issues found, explicitly state that the code looks clean. +PROVIDER: +MODEL: +ENDPOINT: +THINKING: +TOOLS: <$TOOLS — read, or read,grep,find,ls> +APPEND_PATHS: <$APPEND_PATHS — CLAUDE.md paths + GIT_FILE + DIFF_FILE; the agent emits each as its own --append-system-prompt> +FILE_REFS: <$FILE_REFS — @file args, one per line; omit if none> +CLEANUP_FILES: <$DIFF_FILE $GIT_FILE — the agent removes them when pi exits> +``` + +**Do NOT pass `API_KEY` or `BASE_URL`** — the pi-agent reads them from the settings files itself (so the actual key/URL never enters the model's context). The pi-agent emits `--append-system-prompt` for every `APPEND_PATHS` entry, writes `baseUrl` to the agent-dir `models.json` when it resolves one, runs pi in the background with `--no-session --no-context-files --approve`, and reports pi's stdout — which is the review text. ## Handling Output ### CRITICAL: pi's stdout is the review text -Unlike `/pi:delegate` where pi edits files, review mode uses read-only tools (`read`, or `read,grep,find,ls` with an explicit target) — pi cannot write files or run bash. **Its stdout IS the review output.** Capture it and present it to the user. +Unlike `/pi:delegate` where pi edits files, review mode uses read-only tools (`read`, or `read,grep,find,ls` with an explicit target) — pi cannot write files or run bash. **Its stdout IS the review output.** The pi-agent returns this stdout; present it to the user. ### On Success (exit code 0) @@ -305,7 +264,7 @@ Present pi's output as the review findings. Format it clearly: ### On Error (exit code 1+) -Show the error message from stderr. Common causes: +Show the error message from the pi-agent's report. Common causes: - pi not configured (no API key) - Provider/model not available - Task interrupted or killed @@ -381,8 +340,9 @@ Overrides the default `read`-only restriction to `read,grep,find,ls` for the wor - pi runs with **read-only tools** — it cannot edit files or run bash. - **Default review is restricted to `--tools read`** — pi can read files mentioned in the diff but cannot `grep`/`find`/`ls` the codebase or run `git`, so it cannot silently review the whole repo. Explicit targets (`--branch`, `--diff`, `@filepath`, PR number) or `--explore` expand it to `read,grep,find,ls`. - **Default behavior reviews uncommitted working tree changes** (`git diff HEAD`, staged + unstaged). If the working tree is clean, the skill reports it and stops — use `--branch `, `--diff `, or a PR number to review committed code. -- **CLAUDE.md context is always passed** via `--append-system-prompt` as file paths — `~/.claude/CLAUDE.md` (user global) and `./CLAUDE.md` (project). pi reads them automatically. -- **pi only knows built-in provider names** (`openai`, `anthropic`, `google`, etc.). The settings `endpoints` map is just for user convenience. The skill writes `baseUrl` to `~/.pi/agent/models.json` under the endpoint's `provider` field, then passes `--provider ` to pi. +- **Never run pi directly — always delegate to `pi:pi-agent`.** The agent is the plugin's single execution path and owns backgrounding, verification, and error handling. +- **CLAUDE.md context is always passed** by pi-agent via `--append-system-prompt` as file paths — `~/.claude/CLAUDE.md` (user global) and `./CLAUDE.md` (project). pi reads them automatically. +- **pi only knows built-in provider names** (`openai`, `anthropic`, `google`, etc.). The settings `endpoints` map is just for user convenience. The pi-agent writes `baseUrl` to the agent-dir `models.json` (default `~/.pi/agent/models.json`, redirectable via `PI_CODING_AGENT_DIR`/`AGENT_DIR`) under the endpoint's `provider` field, then passes `--provider ` to pi. - No shell `timeout` — reviews can be heavy and should run to completion. - The review rubric is embedded in the task description, not `--append-system-prompt`, to keep it in pi's context window. - Git context and diffs go into `--append-system-prompt` as structured context. diff --git a/pi/skills/review/references/rubric.md b/pi/skills/review/references/rubric.md new file mode 100644 index 00000000..559289d6 --- /dev/null +++ b/pi/skills/review/references/rubric.md @@ -0,0 +1,29 @@ +# Review Rubric (Embedded in Prompt) + +The review prompt given to pi must cover these dimensions. Embed them as part of the task description, not as `--append-system-prompt`: + +### 1. Correctness & Logic +- Does the code do what it intends? Any off-by-one, race conditions, null pointer, or type errors? +- Are error paths handled (not just the happy path)? +- Are async operations properly awaited or chained? + +### 2. Code Quality & Maintainability +- Naming: do names reveal intent? (Mysterious Name) +- Duplication: is the same logic repeated? (Duplicated Code) +- Coupling: does a module reach into another's internals? (Feature Envy) +- Abstraction: is there speculative generality or missing domain types? (Speculative Generality, Primitive Obsession) +- Size: are functions/classes too large? Do they do one thing? + +### 3. Security +- Are user inputs validated/sanitized? +- Any hardcoded secrets, tokens, or credentials? +- Any injection vectors (SQL, command, path traversal)? + +### 4. Architecture & Design +- Does the change follow the project's established patterns? +- Does it introduce unnecessary dependencies? +- Is the change scoped appropriately (not shotgun surgery)? + +### 5. Testing +- Are there tests for the changed code? +- Do tests cover edge cases and error paths? diff --git a/pi/skills/review/references/settings.md b/pi/skills/review/references/settings.md index a973b17e..5c841365 100644 --- a/pi/skills/review/references/settings.md +++ b/pi/skills/review/references/settings.md @@ -34,7 +34,8 @@ All fields are optional. The example below shows the format — fill in your own Each endpoint entry has: - `provider` (required) — pi's known provider name (`openai`, `anthropic`, `google`, etc.). This is what pi's `--provider` flag receives. -- `baseUrl` (optional) — custom API endpoint. When present, the skill writes it to `~/.pi/agent/models.json` for the specified `provider` before running. +- `baseUrl` (optional) — custom API endpoint. When present, the pi-agent writes it to the agent-dir `models.json` (default `~/.pi/agent/models.json`, redirectable via `PI_CODING_AGENT_DIR`/`AGENT_DIR`) for the specified `provider` before running. +- `apiKey` (optional) — API key or `$ENV_VAR` reference, passed to pi via `--api-key`. - `models` — array of model IDs available via this endpoint. Only include fields the user wants to override. Partial files are fine — the chain merges per-field. @@ -66,7 +67,9 @@ fi Then extract values, resolving environment variable references: ```bash -# Resolve env vars in a JSON value: "$VAR" or "${VAR}" → actual value +# Resolve env vars in a JSON value: "$VAR" or "${VAR}" → actual value. +# NOTE: requires bash — the `BASH_REMATCH` capture is bash-only; under zsh this +# returns the literal `$VAR`. Run the config-reading snippet under bash. resolve_env() { local val="$1" while [[ "$val" =~ \$\{?([a-zA-Z_][a-zA-Z0-9_]*)\}? ]]; do @@ -78,14 +81,61 @@ resolve_env() { } ENDPOINT=$(resolve_env "$(echo "$CONFIG" | jq -r '.defaultEndpoint // ""')") -MODEL=$(resolve_env "$(echo "$CONFIG" | jq -r '.defaultModel // ""')") -THINKING=$(resolve_env "$(echo "$CONFIG" | jq -r '.thinking // "low"')") -PROVIDER=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" '.endpoints[$e].provider // "openai"')") -BASE_URL=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" '.endpoints[$e].baseUrl // ""')") -API_KEY=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" '.endpoints[$e].apiKey // ""')") -# If model not set, use first model from the endpoint +# Endpoint-first with legacy flat-field fallback (matches /pi:delegate). When +# defaultEndpoint is empty, fall back to the flat .provider/.model/.baseUrl/.apiKey. +# Provider stays empty when unset — pi-agent lets pi's own default apply. +PROVIDER=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" 'if $e != "" then (.endpoints[$e].provider // "") else (.provider // "") end')") +MODEL=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" 'if $e != "" then (.defaultModel // "") else (.model // "") end')") +BASE_URL=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" 'if $e != "" then (.endpoints[$e].baseUrl // "") else (.baseUrl // "") end')") +API_KEY=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" 'if $e != "" then (.endpoints[$e].apiKey // "") else (.apiKey // "") end')") +THINKING=$(resolve_env "$(echo "$CONFIG" | jq -r '.thinking // "max"')") +# If model still empty, use the first model from the endpoint. if [ -z "$MODEL" ] || [ "$MODEL" = "null" ]; then - MODEL=$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" '.endpoints[$e].models[0] // ""') + MODEL=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" '.endpoints[$e].models[0] // ""')") +fi + +# CLI override: --endpoint / --model re-resolve the active endpoint (highest priority). +# Match only the flag region (after the task description's first -- flag), so a task +# that merely mentions "--endpoint" does not hijack resolution. Accept an endpoint only +# if it exists in .endpoints. +FLAGS_REGION=$(echo "$ARGUMENTS" | awk '{ for(i=1;i<=NF;i++) if($i ~ /^--/) { for(j=i;j<=NF;j++) printf "%s%s", $j, (j settings), so do not override it. +if [ -z "$CLI_MODEL" ] && [ -n "$ENDPOINT" ] && [ -n "$MODEL" ] && [ "$MODEL" != "null" ]; then + IN_ENDPOINT=$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" --arg m "$MODEL" '(.endpoints[$e].models // []) | index($m) // -1') + if [ "$IN_ENDPOINT" = "-1" ]; then + MODEL=$(resolve_env "$(echo "$CONFIG" | jq -r --arg e "$ENDPOINT" '.endpoints[$e].models[0] // ""')") + fi fi ``` @@ -99,6 +149,8 @@ When `$ARGUMENTS` is exactly `--edit-config` (with optional scope flag), open th | Project shared | `--edit-config --shared` | `.claude/pi.json` | Team defaults, committed | tracked | | Global personal | `--edit-config --global` or `--edit-config -g` | `~/.claude/pi.local.json` | User-wide across all projects | user home | +**Shared scope (`--shared`) is committed to git** — never put a literal `apiKey` in it. The `apiKey` field there must be a `$ENV_VAR` reference (e.g. `"$MY_API_KEY"`) so no secret is committed. Literal keys belong in the personal scopes (`.claude/pi.local.json`, `~/.claude/pi.local.json`). + ```bash # Detect scope if [[ "$ARGUMENTS" == *"--global"* || "$ARGUMENTS" == *"-g"* ]]; then @@ -110,7 +162,7 @@ else fi # Create if not exists -mkdir -p "$(dirname "$CONFIG_PATH")" +mkdir -p "${CONFIG_PATH%/*}" if [ ! -f "$CONFIG_PATH" ]; then cat > "$CONFIG_PATH" << 'EOF' { @@ -133,17 +185,22 @@ Report: "Settings file created/opened at ``. Changes take effect on the ne When `$ARGUMENTS` is exactly `--list-models`, read the merged config and display all configured endpoints and their models: ```bash -echo "$CONFIG" | jq -r ' - .defaultEndpoint as $def | - .defaultModel as $defm | - (.endpoints | to_entries[] | - "\(.key)" + if .key == $def then " (default)" else "" end + - " → " + .value.provider + - ":" + - (.value.models | join(", ")) + - if .key == $def and $defm != "" then " ← active: " + $defm else "" end - ) -' +# Endpoints present → list them; otherwise fall back to flat fields +if echo "$CONFIG" | jq -e '.endpoints | length > 0' >/dev/null 2>&1; then + echo "$CONFIG" | jq -r ' + .defaultEndpoint as $def | + .defaultModel as $defm | + ((.endpoints // {}) | to_entries[] | + "\(.key)" + if .key == $def then " (default)" else "" end + + " → " + .value.provider + + ":" + + ((.value.models // []) | join(", ")) + + if .key == $def and $defm != "" then " ← active: " + $defm else "" end + ) + ' +else + echo "$CONFIG" | jq -r '"flat: " + (.provider // "") + " / " + (.model // "") + " @ " + (.baseUrl // "(default)")' +fi ``` Then stop — do not proceed to review. diff --git a/pi/skills/setup/SKILL.md b/pi/skills/setup/SKILL.md index 3b6f5ca3..254c537d 100644 --- a/pi/skills/setup/SKILL.md +++ b/pi/skills/setup/SKILL.md @@ -2,8 +2,8 @@ name: setup description: Guides the user through configuring pi — provider, model, base URL, and API key. Use when the user asks to "setup pi", "configure pi", "pi setup", "set up pi provider", "pi config", "change pi model", or invokes /pi:setup. Only run this skill when the user explicitly requests pi setup — never auto-invoke. user-invocable: true -argument-hint: "[--provider PROVIDER] [--model MODEL] [--api-key KEY] | --edit-config | --list-models | --test | --doctor" -allowed-tools: ["Bash(pi:*)", "Bash(jq:*)", "Bash(cat:*)", "Bash(mkdir:*)", "Bash(echo:*)", "Bash(command:*)", "Bash(ls:*)", "Read", "Write"] +argument-hint: "[--endpoint NAME] [--provider PROVIDER] [--model MODEL] [--base-url URL] [--api-key KEY] | --edit-config | --list-models | --test | --doctor" +allowed-tools: ["Bash(pi:*)", "Bash(jq:*)", "Bash(cat:*)", "Bash(mkdir:*)", "Bash(mv:*)", "Bash(echo:*)", "Bash(command:*)", "Bash(ls:*)", "Bash(vi:*)", "Read", "Write"] --- # CRITICAL: User setup only — do not auto-invoke @@ -36,31 +36,41 @@ Both `/pi:delegate` and `/pi:review` read from the same settings chain: 1. **CLI flag** (from `$ARGUMENTS`) 2. **`.claude/pi.local.json`** — project-specific overrides, gitignored -3. **`~/.claude/pi.local.json`** — global user-wide defaults -4. **pi's own defaults** (pi decides its own default provider and model) +3. **`.claude/pi.json`** — project shared defaults, committed +4. **`~/.claude/pi.local.json`** — global user-wide defaults +5. **pi's own defaults** (pi decides its own default provider and model) This skill writes to `~/.claude/pi.local.json` (global, takes effect for all projects). ### Settings file format -Settings files only override what the user wants to change. All fields are optional: +The settings file uses the **named-endpoint format** shared by `/pi:delegate` and `/pi:review`. All fields are optional — only override what you want to change. Values can reference environment variables using `$VAR` or `${VAR}` syntax — they are resolved at read time by `/pi:delegate` and `/pi:review`. This is useful for API keys: `"apiKey": "$MY_API_KEY"` reads from the environment variable at runtime. ```json { - "provider": "", - "model": "", - "baseUrl": "", - "apiKey": "", - "thinking": "", - "tools": "", - "excludeTools": "", - "noFiles": false, - "noGit": false + "endpoints": { + "local-proxy": { + "provider": "openai", + "baseUrl": "http://10.10.0.195:8317/v1", + "models": ["gemini-3.6-flash-high", "gemini-3.6-pro"] + } + }, + "defaultEndpoint": "local-proxy", + "defaultModel": "gemini-3.6-flash-high", + "thinking": "max" } ``` +Each endpoint key has: +- `provider` (required) — pi's known provider name (`openai`, `anthropic`, `google`, etc.) +- `baseUrl` (optional) — custom API endpoint; when present it is written to `~/.pi/agent/models.json` at runtime +- `apiKey` (optional) — API key or `$ENV_VAR` reference +- `models` — array of model IDs available via this endpoint + +Legacy flat fields (`provider`/`model`/`baseUrl`/`apiKey`) are still honored by the delegate skill as a fallback when no `defaultEndpoint` is set, but new setups should use the endpoint format. + ### `--list-models` flag When `$ARGUMENTS` is exactly `--list-models`, read the current settings and show the effective configuration: @@ -74,7 +84,7 @@ else echo "(not configured — pi uses its defaults)" fi echo "" -echo "To configure, run: /pi:setup --provider --model [--base-url ]" +echo "To configure, run: /pi:setup --endpoint --provider --model [--base-url ]" echo "Or use interactive mode: /pi:setup --edit-config" ``` @@ -82,10 +92,20 @@ Then stop — do not proceed to setup. ### `--test` flag -When `$ARGUMENTS` includes `--test`, run a quick connectivity test: +When `$ARGUMENTS` includes `--test`, run a quick connectivity test. Build the command as an array so each flag is a distinct argument under both bash and zsh (zsh does not word-split an unquoted `${PROVIDER:+--provider ...}` expansion): ```bash -pi -p --provider "$PROVIDER" --model "$MODEL" --thinking low --no-session --no-context-files --approve "Reply with exactly: OK. Model: " +CMD=(pi -p) +# Provider: use the resolved PROVIDER; else if a custom BASE_URL is set, use the +# models.json key (PROVIDER_KEY, default openai) so the probe reaches the custom endpoint. +if [ -n "$PROVIDER" ]; then + CMD+=(--provider "$PROVIDER") +elif [ -n "$BASE_URL" ]; then + CMD+=(--provider "${PROVIDER_KEY:-openai}") +fi +CMD+=(--model "$MODEL" --thinking low --no-session --no-context-files --approve "Reply with exactly: OK. Model: ") +# " on exit 0, or the error on failure. @@ -111,15 +131,18 @@ If `$ARGUMENTS` contains flags, parse them directly: | Flag | Description | |------|-------------| +| `--endpoint` | Endpoint key name (default `local-proxy`) | | `--provider` | LLM provider name (`openai`, `anthropic`, `google`, etc.) | | `--model` | Model ID (e.g. `gemini-3.6-flash-high`, `claude-sonnet-4-20250514`) | +| `--base-url` | Custom API endpoint URL (OpenAI-compatible) | | `--api-key` | API key for the provider (stored in settings file, or reference `$ENV_VAR`) | If no flags are provided, use the AskUserQuestion tool to ask the user: 1. **Provider**: What provider do you want to use? (Options: `openai`, `anthropic`, `google`, or "Other" for custom) 2. **Model**: What model ID? (e.g. `gemini-3.6-flash-high`, `claude-sonnet-4-20250514`) -3. **API Key** (optional): API key or `$ENV_VAR` reference? (leave empty to use environment variables) +3. **Base URL** (optional): Custom endpoint URL, or empty for the provider's default +4. **API Key** (optional): API key or `$ENV_VAR` reference? (leave empty to use environment variables) ### Step 3: Write configuration @@ -132,25 +155,78 @@ if [ -f "$HOME/.claude/pi.local.json" ]; then EXISTING=$(cat "$HOME/.claude/pi.local.json") fi -# Merge with new values (only override non-empty fields) +# Endpoint target: an explicit --endpoint names the key to write; otherwise reconfigure +# the existing defaultEndpoint (so a plain /pi:setup reaches the endpoint the skills read). +ENDPOINT_IS_EXPLICIT="0" +if [[ "$ARGUMENTS" == *"--endpoint"* ]]; then + ENDPOINT_IS_EXPLICIT="1" + ENDPOINT="${ENDPOINT:-local-proxy}" +else + ENDPOINT="${ENDPOINT:-$(echo "$EXISTING" | jq -r '.defaultEndpoint // "local-proxy"')}" +fi + +# Merge into the endpoints map — only override non-empty fields. +# When an explicit --endpoint is given, make it the default so provider and model +# resolve consistently (see defaultModel note below). echo "$EXISTING" | jq \ + --arg e "$ENDPOINT" \ + --arg explicit "${ENDPOINT_IS_EXPLICIT:-}" \ --arg provider "${PROVIDER:-}" \ --arg model "${MODEL:-}" \ --arg baseUrl "${BASE_URL:-}" \ --arg apiKey "${API_KEY:-}" \ - '.provider = (if $provider != "" then $provider else .provider end) | - .model = (if $model != "" then $model else .model end) | - .baseUrl = (if $baseUrl != "" then $baseUrl else .baseUrl end) | - .apiKey = (if $apiKey != "" then $apiKey else .apiKey end)' \ - > "$HOME/.claude/pi.local.json" + '.endpoints[$e] = (.endpoints[$e] // {provider: $provider}) | + .endpoints[$e].provider = (if $provider != "" then $provider else .endpoints[$e].provider end) | + .endpoints[$e].baseUrl = (if $baseUrl != "" then $baseUrl else .endpoints[$e].baseUrl // "" end) | + .endpoints[$e].apiKey = (if $apiKey != "" then $apiKey else .endpoints[$e].apiKey // "" end) | + .endpoints[$e].models = ((.endpoints[$e].models // []) + [($model | select(. != ""))] | unique) | + .defaultEndpoint = (if $explicit == "1" then $e else (.defaultEndpoint // $e) end) | + .defaultModel = (if $model != "" then $model else .defaultModel // "" end)' \ + > "$HOME/.claude/pi.local.json.tmp" && \ + mv "$HOME/.claude/pi.local.json.tmp" "$HOME/.claude/pi.local.json" ``` ### Step 4: Verify with `--test` -Run the test automatically after writing config: +Run the test automatically after writing config. pi has no `--base-url` flag — a custom endpoint goes through `~/.pi/agent/models.json`. Write it there first (idempotent: register baseUrl + model, skip when unchanged), then test: ```bash -pi -p --provider "$PROVIDER" --model "$MODEL" ${BASE_URL:+--base-url "$BASE_URL"} --thinking low --no-session --no-context-files --approve "Reply with exactly: OK. Model: " +# Resolve the agent dir the same way pi-agent does, so setup writes to the +# endpoint pi actually reads (matches AGENT_DIR / PI_CODING_AGENT_DIR overrides). +AGENT_DIR="${AGENT_DIR:-${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}}" + +# Ensure the endpoint is in the agent-dir models.json (pi reads endpoints from there) +# Use PROVIDER_KEY for the write so $PROVIDER (possibly empty = pi's default) is untouched. +if [ -n "$BASE_URL" ]; then + PROVIDER_KEY="${PROVIDER:-openai}" + mkdir -p "$AGENT_DIR" + EXISTING=$(cat "$AGENT_DIR/models.json" 2>/dev/null || echo '{}') + NEW=$(echo "$EXISTING" | jq -c --arg provider "$PROVIDER_KEY" --arg baseUrl "$BASE_URL" --arg model "$MODEL" \ + '.providers[$provider] = (.providers[$provider] // {}) | + .providers[$provider].baseUrl = $baseUrl | + if $model != "" then + (.providers[$provider].models //= []) | + .providers[$provider].models |= ( + if any(.id == $model) then . else . + [{id: $model}] end + ) + else . end') + if [ "$NEW" != "$EXISTING" ]; then + echo "$NEW" > "$AGENT_DIR/models.json.tmp" && mv "$AGENT_DIR/models.json.tmp" "$AGENT_DIR/models.json" + fi +fi + +CMD=(pi -p) +# Provider: use the resolved PROVIDER; else if a custom BASE_URL is set, use the +# models.json key (PROVIDER_KEY, default openai) so the probe reaches the custom endpoint. +if [ -n "$PROVIDER" ]; then + CMD+=(--provider "$PROVIDER") +elif [ -n "$BASE_URL" ]; then + CMD+=(--provider "${PROVIDER_KEY:-openai}") +fi +CMD+=(--model "$MODEL" --thinking low --no-session --no-context-files --approve "Reply with exactly: OK. Model: ") +# Pin the probe to the agent dir we just wrote (matches pi-agent's run pattern). +# --provider --model - /pi:review --model + /pi:delegate --endpoint --model + /pi:review --endpoint --model To edit manually: /pi:setup --edit-config @@ -177,13 +253,19 @@ To view current config: ### OpenAI-compatible endpoint (with custom base URL) -Configure `baseUrl` in `~/.claude/pi.local.json`: +Configure an endpoint in `~/.claude/pi.local.json`: ```json { - "provider": "openai", - "model": "gemini-3.6-flash-high", - "baseUrl": "http://10.10.0.195:8317/v1" + "endpoints": { + "local-proxy": { + "provider": "openai", + "baseUrl": "http://10.10.0.195:8317/v1", + "models": ["gemini-3.6-flash-high", "gemini-3.6-pro"] + } + }, + "defaultEndpoint": "local-proxy", + "defaultModel": "gemini-3.6-flash-high" } ``` @@ -192,17 +274,17 @@ Or via `/pi:setup --edit-config --global`. ### Anthropic direct ```bash -/pi:setup --provider anthropic --model claude-sonnet-4-20250514 +/pi:setup --endpoint anthropic-direct --provider anthropic --model claude-sonnet-4-20250514 ``` ### Google Gemini direct ```bash -/pi:setup --provider google --model gemini-3.6-flash-high +/pi:setup --endpoint google --provider google --model gemini-3.6-flash-high ``` ## References - `/pi:delegate` — delegates coding tasks to pi - `/pi:review` — reviews code via pi with read-only tools -- `~/.claude/pi.local.json` — global user settings (read by both delegate and review) \ No newline at end of file +- `~/.claude/pi.local.json` — global user settings (read by both delegate and review)