@phi-code-admin/phi-code 0.96.0 → 0.97.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/dist/cli/args.d.ts +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +43 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/api-key-store.d.ts +20 -4
  7. package/dist/core/api-key-store.d.ts.map +1 -1
  8. package/dist/core/api-key-store.js +37 -7
  9. package/dist/core/api-key-store.js.map +1 -1
  10. package/dist/core/compaction/compaction.d.ts +16 -0
  11. package/dist/core/compaction/compaction.d.ts.map +1 -1
  12. package/dist/core/compaction/compaction.js +43 -19
  13. package/dist/core/compaction/compaction.js.map +1 -1
  14. package/dist/core/json-utils.d.ts +11 -0
  15. package/dist/core/json-utils.d.ts.map +1 -0
  16. package/dist/core/json-utils.js +15 -0
  17. package/dist/core/json-utils.js.map +1 -0
  18. package/dist/core/model-registry.d.ts +6 -1
  19. package/dist/core/model-registry.d.ts.map +1 -1
  20. package/dist/core/model-registry.js +10 -9
  21. package/dist/core/model-registry.js.map +1 -1
  22. package/dist/core/slash-commands.d.ts +13 -0
  23. package/dist/core/slash-commands.d.ts.map +1 -1
  24. package/dist/core/slash-commands.js +38 -0
  25. package/dist/core/slash-commands.js.map +1 -1
  26. package/dist/migrations.d.ts.map +1 -1
  27. package/dist/migrations.js +2 -2
  28. package/dist/migrations.js.map +1 -1
  29. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  30. package/dist/modes/interactive/interactive-mode.js +24 -4
  31. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  32. package/extensions/phi/benchmark.ts +43 -83
  33. package/extensions/phi/init.ts +86 -673
  34. package/extensions/phi/memory.ts +1 -1
  35. package/extensions/phi/models.ts +4 -3
  36. package/extensions/phi/providers/catalog.ts +133 -0
  37. package/extensions/phi/setup.ts +180 -280
  38. package/extensions/phi/smart-router.ts +0 -17
  39. package/package.json +5 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,95 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.97.0] - 2026-07-12
4
+
5
+ ### Changed — the remaining debt from the line-by-line review
6
+
7
+ 1. **One provider catalog** (`extensions/phi/providers/catalog.ts`): /setup,
8
+ /phi-init and /benchmark each hardcoded their own provider list and they
9
+ had already drifted (benchmark used a phantom `anthropic-openai` id;
10
+ init's env vars diverged). All three now project from a single catalog;
11
+ bench targets are `benchModels` entries validated as a subset of the
12
+ provider's known models.
13
+ 2. **One wizard**: /phi-init's 600-line wizard was a drifting near-copy of
14
+ /setup — with its own models.json reader whose `catch → {}` silently
15
+ WIPED a commented config on the next write. /phi-init now scaffolds
16
+ ~/.phi (dirs, bundled agents, AGENTS.md template) and delegates to the
17
+ exact same wizard as /setup (`runSetupWizard` export). /plan-models stays,
18
+ rebuilt on the shared picker/routing helpers — it now writes the modern
19
+ routing.json shape ($schema, version, derived debug route).
20
+ 3. **sigma-agents dependency un-pinned from the past**: the declared range
21
+ `^0.1.7` cannot match the published 0.2.1 (caret on 0.x), so global
22
+ installs silently ran sigma-agents 0.1.10 while the extensions were
23
+ written against 0.2.1. Range is now `^0.2.1` (sigma-skills aligned to
24
+ `^0.1.5`).
25
+ 4. **/debug no longer shadowed in the TUI**: the interactive debug overlay
26
+ claimed the bare `/debug` before extension dispatch, so the debug
27
+ orchestrator's own `/debug` could never be reached bare. The overlay now
28
+ yields when an extension registers /debug (overlay stays available with
29
+ extensions disabled).
30
+ 5. **`/new please` no longer leaks to the model as prose**: a bare builtin
31
+ invoked with arguments (exact-match ladder miss) silently became a chat
32
+ message. The TUI now surfaces a usage warning instead — unless an
33
+ extension owns a command with that name. Pure helper
34
+ `matchBareBuiltinWithArgs` + `BARE_BUILTIN_COMMAND_NAMES` in
35
+ core/slash-commands.ts.
36
+ 6. **Dead code removed**: smart-router's never-called `_resolveModel`.
37
+ 7. **repository.url fixed** on tui/mom/pods package.json (still pointed at
38
+ upstream pi-mono forks).
39
+
40
+ 13 new tests (catalog invariants incl. bench-subset, bare-builtin matcher
41
+ matrix); full suite green (1536).
42
+
43
+ ## [0.96.1] - 2026-07-12
44
+
45
+ ### Fixed — ten correctness bugs from the exhaustive line-by-line review
46
+
47
+ 1. **ApiKeyStore parses models.json like everyone else**: the store did a raw
48
+ `JSON.parse` while model-registry tolerates `//` comments and trailing
49
+ commas — a commented models.json crashed `/keys` hot-reload while the agent
50
+ kept running on the registry's copy. Both readers now share one
51
+ `stripJsonComments` (new `core/json-utils.ts`).
52
+ 2. **ApiKeyStore.getKey resolves keys the way requests do**: stored values now
53
+ follow the documented convention (env-var NAME → its value, `!cmd` → shell,
54
+ else literal; legacy `$NAME` resolves NAME and is never returned
55
+ literally). Before, an env-var-name key worked at request time but came
56
+ back raw (or was skipped) from the store — breaking `/benchmark`,
57
+ web-search and live-model pings.
58
+ 3. **hasConfiguredAuth: empty apiKey is not auth**: a `""` apiKey in
59
+ models.json made models look available in pickers and then fail at send
60
+ time. Empty no longer counts (mirrors getProviderAuthStatus).
61
+ 4. **Compaction anti-drift union is capped (60k chars)**: the union becomes
62
+ the next compaction's previousSummary and is always bigger than a fresh
63
+ summary, so the <60% check re-triggered every time and the summary grew
64
+ monotonically without bound. Past the cap the newest summary wins. The cap
65
+ is checked FIRST, which also keeps the quadratic path-extraction regex away
66
+ from unbounded input.
67
+ 5. **getModel is honest about unknown models** (phi-code-ai 0.74.4): runtime
68
+ strings now type as `Model | undefined` instead of casting `undefined` to
69
+ `Model` and crashing somewhere downstream; statically-valid literals keep
70
+ the non-optional type (zero churn).
71
+ 6. **Ontology graph appends take a file lock** (sigma-memory 0.2.9):
72
+ concurrent phi processes could interleave partial JSONL lines in
73
+ graph.jsonl. Appends now hold a proper-lockfile lock; stale locks from
74
+ crashed processes are taken over after 5s.
75
+ 7. **`--flag=value` works for known flags**: `--model=x` was silently
76
+ mis-classified as an unknown extension flag (only unknown flags handled
77
+ `=`). Known value flags now accept both spellings.
78
+ 8. **/benchmark sees keys stored via /setup or /keys**: it only read
79
+ `process.env`; it now falls back to the ApiKeyStore (which also honors
80
+ PHI_AGENT_DIR and comment-tolerant models.json).
81
+ 9. **Memory extension AGENTS.md autoload**: read the private
82
+ `sigmaMemory.config` field (worked only because jiti strips types) — now
83
+ uses the public `getConfig()`.
84
+ 10. **Migration messages point at the fork**: the extensions-migration
85
+ guide/doc URLs printed by startup migrations still pointed at upstream
86
+ `earendil-works/pi-mono` (with a dead anchor); they now point at
87
+ `uglyswap/phi-code`.
88
+
89
+ 33 new tests (JSONC parsing, key resolution incl. `$NAME`/env/empty, empty-key
90
+ auth, union cap + repeated unions, `--flag=value` matrix, unknown-model
91
+ lookups, lock release + stale-lock takeover); full suite green.
92
+
3
93
  ## [0.96.0] - 2026-07-12
4
94
 
5
95
  ### Added — the six efficiency upgrades, each tied to a measured failure
@@ -48,6 +48,6 @@ export interface Args {
48
48
  }>;
49
49
  }
50
50
  export declare function isValidThinkingLevel(level: string): level is ThinkingLevel;
51
- export declare function parseArgs(args: string[]): Args;
51
+ export declare function parseArgs(rawArgs: string[]): Args;
52
52
  export declare function printHelp(extensionFlags?: ExtensionFlag[]): void;
53
53
  //# sourceMappingURL=args.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAE3C,MAAM,WAAW,IAAI;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,8EAA8E;IAC9E,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;IAC5C,WAAW,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnE;AAID,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,aAAa,CAE1E;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAkI9C;AAED,wBAAgB,SAAS,CAAC,cAAc,CAAC,EAAE,aAAa,EAAE,GAAG,IAAI,CAmKhE","sourcesContent":["/**\n * CLI argument parsing and help display\n */\n\nimport chalk from \"chalk\";\nimport type { ThinkingLevel } from \"phi-code-agent\";\nimport { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from \"../config.js\";\nimport type { ExtensionFlag } from \"../core/extensions/types.js\";\n\nexport type Mode = \"text\" | \"json\" | \"rpc\";\n\nexport interface Args {\n\tprovider?: string;\n\tmodel?: string;\n\tapiKey?: string;\n\tsystemPrompt?: string;\n\tappendSystemPrompt?: string[];\n\tthinking?: ThinkingLevel;\n\tcontinue?: boolean;\n\tresume?: boolean;\n\thelp?: boolean;\n\tversion?: boolean;\n\tmode?: Mode;\n\tnoSession?: boolean;\n\tsession?: string;\n\tfork?: string;\n\tsessionDir?: string;\n\tmodels?: string[];\n\ttools?: string[];\n\tnoTools?: boolean;\n\tnoBuiltinTools?: boolean;\n\textensions?: string[];\n\tnoExtensions?: boolean;\n\tprint?: boolean;\n\texport?: string;\n\tnoSkills?: boolean;\n\tskills?: string[];\n\tpromptTemplates?: string[];\n\tnoPromptTemplates?: boolean;\n\tthemes?: string[];\n\tnoThemes?: boolean;\n\tnoContextFiles?: boolean;\n\tlistModels?: string | true;\n\toffline?: boolean;\n\tverbose?: boolean;\n\tmessages: string[];\n\tfileArgs: string[];\n\t/** Unknown flags (potentially extension flags) - map of flag name to value */\n\tunknownFlags: Map<string, boolean | string>;\n\tdiagnostics: Array<{ type: \"warning\" | \"error\"; message: string }>;\n}\n\nconst VALID_THINKING_LEVELS = [\"off\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"] as const;\n\nexport function isValidThinkingLevel(level: string): level is ThinkingLevel {\n\treturn VALID_THINKING_LEVELS.includes(level as ThinkingLevel);\n}\n\nexport function parseArgs(args: string[]): Args {\n\tconst result: Args = {\n\t\tmessages: [],\n\t\tfileArgs: [],\n\t\tunknownFlags: new Map(),\n\t\tdiagnostics: [],\n\t};\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\n\t\tif (arg === \"--help\" || arg === \"-h\") {\n\t\t\tresult.help = true;\n\t\t} else if (arg === \"--version\" || arg === \"-v\") {\n\t\t\tresult.version = true;\n\t\t} else if (arg === \"--mode\" && i + 1 < args.length) {\n\t\t\tconst mode = args[++i];\n\t\t\tif (mode === \"text\" || mode === \"json\" || mode === \"rpc\") {\n\t\t\t\tresult.mode = mode;\n\t\t\t}\n\t\t} else if (arg === \"--continue\" || arg === \"-c\") {\n\t\t\tresult.continue = true;\n\t\t} else if (arg === \"--resume\" || arg === \"-r\") {\n\t\t\tresult.resume = true;\n\t\t} else if (arg === \"--provider\" && i + 1 < args.length) {\n\t\t\tresult.provider = args[++i];\n\t\t} else if (arg === \"--model\" && i + 1 < args.length) {\n\t\t\tresult.model = args[++i];\n\t\t} else if (arg === \"--api-key\" && i + 1 < args.length) {\n\t\t\tresult.apiKey = args[++i];\n\t\t} else if (arg === \"--system-prompt\" && i + 1 < args.length) {\n\t\t\tresult.systemPrompt = args[++i];\n\t\t} else if (arg === \"--append-system-prompt\" && i + 1 < args.length) {\n\t\t\tresult.appendSystemPrompt = result.appendSystemPrompt ?? [];\n\t\t\tresult.appendSystemPrompt.push(args[++i]);\n\t\t} else if (arg === \"--no-session\") {\n\t\t\tresult.noSession = true;\n\t\t} else if (arg === \"--session\" && i + 1 < args.length) {\n\t\t\tresult.session = args[++i];\n\t\t} else if (arg === \"--fork\" && i + 1 < args.length) {\n\t\t\tresult.fork = args[++i];\n\t\t} else if (arg === \"--session-dir\" && i + 1 < args.length) {\n\t\t\tresult.sessionDir = args[++i];\n\t\t} else if (arg === \"--models\" && i + 1 < args.length) {\n\t\t\tresult.models = args[++i].split(\",\").map((s) => s.trim());\n\t\t} else if (arg === \"--no-tools\" || arg === \"-nt\") {\n\t\t\tresult.noTools = true;\n\t\t} else if (arg === \"--no-builtin-tools\" || arg === \"-nbt\") {\n\t\t\tresult.noBuiltinTools = true;\n\t\t} else if ((arg === \"--tools\" || arg === \"-t\") && i + 1 < args.length) {\n\t\t\tresult.tools = args[++i]\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter((name) => name.length > 0);\n\t\t} else if (arg === \"--thinking\" && i + 1 < args.length) {\n\t\t\tconst level = args[++i];\n\t\t\tif (isValidThinkingLevel(level)) {\n\t\t\t\tresult.thinking = level;\n\t\t\t} else {\n\t\t\t\tresult.diagnostics.push({\n\t\t\t\t\ttype: \"warning\",\n\t\t\t\t\tmessage: `Invalid thinking level \"${level}\". Valid values: ${VALID_THINKING_LEVELS.join(\", \")}`,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (arg === \"--print\" || arg === \"-p\") {\n\t\t\tresult.print = true;\n\t\t\tconst next = args[i + 1];\n\t\t\tif (next !== undefined && !next.startsWith(\"@\") && (!next.startsWith(\"-\") || next.startsWith(\"---\"))) {\n\t\t\t\tresult.messages.push(next);\n\t\t\t\ti++;\n\t\t\t}\n\t\t} else if (arg === \"--export\" && i + 1 < args.length) {\n\t\t\tresult.export = args[++i];\n\t\t} else if ((arg === \"--extension\" || arg === \"-e\") && i + 1 < args.length) {\n\t\t\tresult.extensions = result.extensions ?? [];\n\t\t\tresult.extensions.push(args[++i]);\n\t\t} else if (arg === \"--no-extensions\" || arg === \"-ne\") {\n\t\t\tresult.noExtensions = true;\n\t\t} else if (arg === \"--skill\" && i + 1 < args.length) {\n\t\t\tresult.skills = result.skills ?? [];\n\t\t\tresult.skills.push(args[++i]);\n\t\t} else if (arg === \"--prompt-template\" && i + 1 < args.length) {\n\t\t\tresult.promptTemplates = result.promptTemplates ?? [];\n\t\t\tresult.promptTemplates.push(args[++i]);\n\t\t} else if (arg === \"--theme\" && i + 1 < args.length) {\n\t\t\tresult.themes = result.themes ?? [];\n\t\t\tresult.themes.push(args[++i]);\n\t\t} else if (arg === \"--no-skills\" || arg === \"-ns\") {\n\t\t\tresult.noSkills = true;\n\t\t} else if (arg === \"--no-prompt-templates\" || arg === \"-np\") {\n\t\t\tresult.noPromptTemplates = true;\n\t\t} else if (arg === \"--no-themes\") {\n\t\t\tresult.noThemes = true;\n\t\t} else if (arg === \"--no-context-files\" || arg === \"-nc\") {\n\t\t\tresult.noContextFiles = true;\n\t\t} else if (arg === \"--list-models\") {\n\t\t\t// Check if next arg is a search pattern (not a flag or file arg)\n\t\t\tif (i + 1 < args.length && !args[i + 1].startsWith(\"-\") && !args[i + 1].startsWith(\"@\")) {\n\t\t\t\tresult.listModels = args[++i];\n\t\t\t} else {\n\t\t\t\tresult.listModels = true;\n\t\t\t}\n\t\t} else if (arg === \"--verbose\") {\n\t\t\tresult.verbose = true;\n\t\t} else if (arg === \"--offline\") {\n\t\t\tresult.offline = true;\n\t\t} else if (arg.startsWith(\"@\")) {\n\t\t\tresult.fileArgs.push(arg.slice(1)); // Remove @ prefix\n\t\t} else if (arg.startsWith(\"--\")) {\n\t\t\tconst eqIndex = arg.indexOf(\"=\");\n\t\t\tif (eqIndex !== -1) {\n\t\t\t\tresult.unknownFlags.set(arg.slice(2, eqIndex), arg.slice(eqIndex + 1));\n\t\t\t} else {\n\t\t\t\tconst flagName = arg.slice(2);\n\t\t\t\tconst next = args[i + 1];\n\t\t\t\tif (next !== undefined && !next.startsWith(\"-\") && !next.startsWith(\"@\")) {\n\t\t\t\t\tresult.unknownFlags.set(flagName, next);\n\t\t\t\t\ti++;\n\t\t\t\t} else {\n\t\t\t\t\tresult.unknownFlags.set(flagName, true);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (arg.startsWith(\"-\") && !arg.startsWith(\"--\")) {\n\t\t\tresult.diagnostics.push({ type: \"error\", message: `Unknown option: ${arg}` });\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tresult.messages.push(arg);\n\t\t}\n\t}\n\n\treturn result;\n}\n\nexport function printHelp(extensionFlags?: ExtensionFlag[]): void {\n\tconst extensionFlagsText =\n\t\textensionFlags && extensionFlags.length > 0\n\t\t\t? `\\n${chalk.bold(\"Extension CLI Flags:\")}\\n${extensionFlags\n\t\t\t\t\t.map((flag) => {\n\t\t\t\t\t\tconst value = flag.type === \"string\" ? \" <value>\" : \"\";\n\t\t\t\t\t\tconst description = flag.description ?? `Registered by ${flag.extensionPath}`;\n\t\t\t\t\t\treturn ` --${flag.name}${value}`.padEnd(30) + description;\n\t\t\t\t\t})\n\t\t\t\t\t.join(\"\\n\")}\\n`\n\t\t\t: \"\";\n\tconsole.log(`${chalk.bold(APP_NAME)} - AI coding assistant with read, bash, edit, write tools\n\n${chalk.bold(\"Usage:\")}\n ${APP_NAME} [options] [@files...] [messages...]\n\n${chalk.bold(\"Commands:\")}\n ${APP_NAME} install <source> [-l] Install extension source and add to settings\n ${APP_NAME} remove <source> [-l] Remove extension source from settings\n ${APP_NAME} uninstall <source> [-l] Alias for remove\n ${APP_NAME} update [source|self] Update ${APP_NAME} and installed extensions\n ${APP_NAME} list List installed extensions from settings\n ${APP_NAME} config Open TUI to enable/disable package resources\n ${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list\n\n${chalk.bold(\"Options:\")}\n --provider <name> Provider name (default: google)\n --model <pattern> Model pattern or ID (supports \"provider/id\" and optional \":<thinking>\")\n --api-key <key> API key (defaults to env vars)\n --system-prompt <text> System prompt (default: coding assistant prompt)\n --append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)\n --mode <mode> Output mode: text (default), json, or rpc\n --print, -p Non-interactive mode: process prompt and exit\n --continue, -c Continue previous session\n --resume, -r Select a session to resume\n --session <path|id> Use specific session file or partial UUID\n --fork <path|id> Fork specific session file or partial UUID into a new session\n --session-dir <dir> Directory for session storage and lookup\n --no-session Don't save session (ephemeral)\n --models <patterns> Comma-separated model patterns for Ctrl+P cycling\n Supports globs (anthropic/*, *sonnet*) and fuzzy matching\n --no-tools, -nt Disable all tools by default (built-in and extension)\n --no-builtin-tools, -nbt Disable built-in tools by default but keep extension/custom tools enabled\n --tools, -t <tools> Comma-separated allowlist of tool names to enable\n Applies to built-in, extension, and custom tools\n --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh\n --extension, -e <path> Load an extension file (can be used multiple times)\n --no-extensions, -ne Disable extension discovery (explicit -e paths still work)\n --skill <path> Load a skill file or directory (can be used multiple times)\n --no-skills, -ns Disable skills discovery and loading\n --prompt-template <path> Load a prompt template file or directory (can be used multiple times)\n --no-prompt-templates, -np Disable prompt template discovery and loading\n --theme <path> Load a theme file or directory (can be used multiple times)\n --no-themes Disable theme discovery and loading\n --no-context-files, -nc Disable AGENTS.md and CLAUDE.md discovery and loading\n --export <file> Export session file to HTML and exit\n --list-models [search] List available models (with optional fuzzy search)\n --verbose Force verbose startup (overrides quietStartup setting)\n --offline Disable startup network operations (same as PI_OFFLINE=1)\n --help, -h Show this help\n --version, -v Show version number\n\nExtensions can register additional flags (e.g., --plan from plan-mode extension).${extensionFlagsText}\n\n${chalk.bold(\"Examples:\")}\n # Interactive mode\n ${APP_NAME}\n\n # Interactive mode with initial prompt\n ${APP_NAME} \"List all .ts files in src/\"\n\n # Include files in initial message\n ${APP_NAME} @prompt.md @image.png \"What color is the sky?\"\n\n # Non-interactive mode (process and exit)\n ${APP_NAME} -p \"List all .ts files in src/\"\n\n # Multiple messages (interactive)\n ${APP_NAME} \"Read package.json\" \"What dependencies do we have?\"\n\n # Continue previous session\n ${APP_NAME} --continue \"What did we discuss?\"\n\n # Use different model\n ${APP_NAME} --provider openai --model gpt-4o-mini \"Help me refactor this code\"\n\n # Use model with provider prefix (no --provider needed)\n ${APP_NAME} --model openai/gpt-4o \"Help me refactor this code\"\n\n # Use model with thinking level shorthand\n ${APP_NAME} --model sonnet:high \"Solve this complex problem\"\n\n # Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o\n\n # Limit to a specific provider with glob pattern\n ${APP_NAME} --models \"github-copilot/*\"\n\n # Cycle models with fixed thinking levels\n ${APP_NAME} --models sonnet:high,haiku:low\n\n # Start with a specific thinking level\n ${APP_NAME} --thinking high \"Solve this complex problem\"\n\n # Read-only mode (no file modifications possible)\n ${APP_NAME} --tools read,grep,find,ls -p \"Review the code in src/\"\n\n # Export a session file to HTML\n ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl\n ${APP_NAME} --export session.jsonl output.html\n\n${chalk.bold(\"Environment Variables:\")}\n ANTHROPIC_API_KEY - Anthropic Claude API key\n ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)\n OPENAI_API_KEY - OpenAI GPT API key\n AZURE_OPENAI_API_KEY - Azure OpenAI API key\n AZURE_OPENAI_BASE_URL - Azure OpenAI/Cognitive Services base URL (e.g. https://{resource}.openai.azure.com)\n AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL)\n AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)\n AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)\n DEEPSEEK_API_KEY - DeepSeek API key\n GEMINI_API_KEY - Google Gemini API key\n GROQ_API_KEY - Groq API key\n CEREBRAS_API_KEY - Cerebras API key\n XAI_API_KEY - xAI Grok API key\n FIREWORKS_API_KEY - Fireworks API key\n TOGETHER_API_KEY - Together AI API key\n OPENROUTER_API_KEY - OpenRouter API key\n AI_GATEWAY_API_KEY - Vercel AI Gateway API key\n ZAI_API_KEY - ZAI API key\n MISTRAL_API_KEY - Mistral API key\n MINIMAX_API_KEY - MiniMax API key\n MOONSHOT_API_KEY - Moonshot AI API key\n OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key\n KIMI_API_KEY - Kimi For Coding API key\n CLOUDFLARE_API_KEY - Cloudflare API token (Workers AI and AI Gateway)\n CLOUDFLARE_ACCOUNT_ID - Cloudflare account id (required for both)\n CLOUDFLARE_GATEWAY_ID - Cloudflare AI Gateway slug (required for AI Gateway)\n XIAOMI_API_KEY - Xiaomi MiMo API key (api.xiaomimimo.com billing)\n XIAOMI_TOKEN_PLAN_CN_API_KEY - Xiaomi MiMo Token Plan API key (China region)\n XIAOMI_TOKEN_PLAN_AMS_API_KEY - Xiaomi MiMo Token Plan API key (Amsterdam region)\n XIAOMI_TOKEN_PLAN_SGP_API_KEY - Xiaomi MiMo Token Plan API key (Singapore region)\n AWS_PROFILE - AWS profile for Amazon Bedrock\n AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock\n AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock\n AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token)\n AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1)\n ${ENV_AGENT_DIR.padEnd(32)} - Config directory (default: ~/${CONFIG_DIR_NAME}/agent)\n ${ENV_SESSION_DIR.padEnd(32)} - Session storage directory (overridden by --session-dir)\n PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)\n PI_OFFLINE - Disable startup network operations when set to 1/true/yes\n PI_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no\n PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)\n\n${chalk.bold(\"Built-in Tool Names:\")}\n read - Read file contents\n bash - Execute bash commands\n edit - Edit files with find/replace\n write - Write files (creates/overwrites)\n grep - Search file contents (read-only, off by default)\n find - Find files by glob pattern (read-only, off by default)\n ls - List directory contents (read-only, off by default)\n`);\n}\n"]}
1
+ {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAE3C,MAAM,WAAW,IAAI;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,8EAA8E;IAC9E,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;IAC5C,WAAW,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnE;AAID,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,aAAa,CAE1E;AA6CD,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAmIjD;AAED,wBAAgB,SAAS,CAAC,cAAc,CAAC,EAAE,aAAa,EAAE,GAAG,IAAI,CAmKhE","sourcesContent":["/**\n * CLI argument parsing and help display\n */\n\nimport chalk from \"chalk\";\nimport type { ThinkingLevel } from \"phi-code-agent\";\nimport { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from \"../config.js\";\nimport type { ExtensionFlag } from \"../core/extensions/types.js\";\n\nexport type Mode = \"text\" | \"json\" | \"rpc\";\n\nexport interface Args {\n\tprovider?: string;\n\tmodel?: string;\n\tapiKey?: string;\n\tsystemPrompt?: string;\n\tappendSystemPrompt?: string[];\n\tthinking?: ThinkingLevel;\n\tcontinue?: boolean;\n\tresume?: boolean;\n\thelp?: boolean;\n\tversion?: boolean;\n\tmode?: Mode;\n\tnoSession?: boolean;\n\tsession?: string;\n\tfork?: string;\n\tsessionDir?: string;\n\tmodels?: string[];\n\ttools?: string[];\n\tnoTools?: boolean;\n\tnoBuiltinTools?: boolean;\n\textensions?: string[];\n\tnoExtensions?: boolean;\n\tprint?: boolean;\n\texport?: string;\n\tnoSkills?: boolean;\n\tskills?: string[];\n\tpromptTemplates?: string[];\n\tnoPromptTemplates?: boolean;\n\tthemes?: string[];\n\tnoThemes?: boolean;\n\tnoContextFiles?: boolean;\n\tlistModels?: string | true;\n\toffline?: boolean;\n\tverbose?: boolean;\n\tmessages: string[];\n\tfileArgs: string[];\n\t/** Unknown flags (potentially extension flags) - map of flag name to value */\n\tunknownFlags: Map<string, boolean | string>;\n\tdiagnostics: Array<{ type: \"warning\" | \"error\"; message: string }>;\n}\n\nconst VALID_THINKING_LEVELS = [\"off\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"] as const;\n\nexport function isValidThinkingLevel(level: string): level is ThinkingLevel {\n\treturn VALID_THINKING_LEVELS.includes(level as ThinkingLevel);\n}\n\n/** Long flags that consume a value; `--flag=value` is accepted for these. */\nconst KNOWN_VALUE_FLAGS = new Set([\n\t\"mode\",\n\t\"provider\",\n\t\"model\",\n\t\"api-key\",\n\t\"system-prompt\",\n\t\"append-system-prompt\",\n\t\"session\",\n\t\"fork\",\n\t\"session-dir\",\n\t\"models\",\n\t\"tools\",\n\t\"thinking\",\n\t\"export\",\n\t\"extension\",\n\t\"skill\",\n\t\"prompt-template\",\n\t\"theme\",\n\t\"list-models\",\n\t\"print\",\n]);\n\n/**\n * Expand `--flag=value` into two tokens for flags the parser knows take a\n * value, so both spellings work. Unknown flags keep the raw `--x=y` handling\n * below (extensions rely on it, including values that start with `-`).\n */\nfunction normalizeEqualsSyntax(args: string[]): string[] {\n\tconst normalized: string[] = [];\n\tfor (const arg of args) {\n\t\tif (arg.startsWith(\"--\")) {\n\t\t\tconst eqIndex = arg.indexOf(\"=\");\n\t\t\tif (eqIndex > 2 && KNOWN_VALUE_FLAGS.has(arg.slice(2, eqIndex))) {\n\t\t\t\tnormalized.push(arg.slice(0, eqIndex), arg.slice(eqIndex + 1));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tnormalized.push(arg);\n\t}\n\treturn normalized;\n}\n\nexport function parseArgs(rawArgs: string[]): Args {\n\tconst args = normalizeEqualsSyntax(rawArgs);\n\tconst result: Args = {\n\t\tmessages: [],\n\t\tfileArgs: [],\n\t\tunknownFlags: new Map(),\n\t\tdiagnostics: [],\n\t};\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\n\t\tif (arg === \"--help\" || arg === \"-h\") {\n\t\t\tresult.help = true;\n\t\t} else if (arg === \"--version\" || arg === \"-v\") {\n\t\t\tresult.version = true;\n\t\t} else if (arg === \"--mode\" && i + 1 < args.length) {\n\t\t\tconst mode = args[++i];\n\t\t\tif (mode === \"text\" || mode === \"json\" || mode === \"rpc\") {\n\t\t\t\tresult.mode = mode;\n\t\t\t}\n\t\t} else if (arg === \"--continue\" || arg === \"-c\") {\n\t\t\tresult.continue = true;\n\t\t} else if (arg === \"--resume\" || arg === \"-r\") {\n\t\t\tresult.resume = true;\n\t\t} else if (arg === \"--provider\" && i + 1 < args.length) {\n\t\t\tresult.provider = args[++i];\n\t\t} else if (arg === \"--model\" && i + 1 < args.length) {\n\t\t\tresult.model = args[++i];\n\t\t} else if (arg === \"--api-key\" && i + 1 < args.length) {\n\t\t\tresult.apiKey = args[++i];\n\t\t} else if (arg === \"--system-prompt\" && i + 1 < args.length) {\n\t\t\tresult.systemPrompt = args[++i];\n\t\t} else if (arg === \"--append-system-prompt\" && i + 1 < args.length) {\n\t\t\tresult.appendSystemPrompt = result.appendSystemPrompt ?? [];\n\t\t\tresult.appendSystemPrompt.push(args[++i]);\n\t\t} else if (arg === \"--no-session\") {\n\t\t\tresult.noSession = true;\n\t\t} else if (arg === \"--session\" && i + 1 < args.length) {\n\t\t\tresult.session = args[++i];\n\t\t} else if (arg === \"--fork\" && i + 1 < args.length) {\n\t\t\tresult.fork = args[++i];\n\t\t} else if (arg === \"--session-dir\" && i + 1 < args.length) {\n\t\t\tresult.sessionDir = args[++i];\n\t\t} else if (arg === \"--models\" && i + 1 < args.length) {\n\t\t\tresult.models = args[++i].split(\",\").map((s) => s.trim());\n\t\t} else if (arg === \"--no-tools\" || arg === \"-nt\") {\n\t\t\tresult.noTools = true;\n\t\t} else if (arg === \"--no-builtin-tools\" || arg === \"-nbt\") {\n\t\t\tresult.noBuiltinTools = true;\n\t\t} else if ((arg === \"--tools\" || arg === \"-t\") && i + 1 < args.length) {\n\t\t\tresult.tools = args[++i]\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter((name) => name.length > 0);\n\t\t} else if (arg === \"--thinking\" && i + 1 < args.length) {\n\t\t\tconst level = args[++i];\n\t\t\tif (isValidThinkingLevel(level)) {\n\t\t\t\tresult.thinking = level;\n\t\t\t} else {\n\t\t\t\tresult.diagnostics.push({\n\t\t\t\t\ttype: \"warning\",\n\t\t\t\t\tmessage: `Invalid thinking level \"${level}\". Valid values: ${VALID_THINKING_LEVELS.join(\", \")}`,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (arg === \"--print\" || arg === \"-p\") {\n\t\t\tresult.print = true;\n\t\t\tconst next = args[i + 1];\n\t\t\tif (next !== undefined && !next.startsWith(\"@\") && (!next.startsWith(\"-\") || next.startsWith(\"---\"))) {\n\t\t\t\tresult.messages.push(next);\n\t\t\t\ti++;\n\t\t\t}\n\t\t} else if (arg === \"--export\" && i + 1 < args.length) {\n\t\t\tresult.export = args[++i];\n\t\t} else if ((arg === \"--extension\" || arg === \"-e\") && i + 1 < args.length) {\n\t\t\tresult.extensions = result.extensions ?? [];\n\t\t\tresult.extensions.push(args[++i]);\n\t\t} else if (arg === \"--no-extensions\" || arg === \"-ne\") {\n\t\t\tresult.noExtensions = true;\n\t\t} else if (arg === \"--skill\" && i + 1 < args.length) {\n\t\t\tresult.skills = result.skills ?? [];\n\t\t\tresult.skills.push(args[++i]);\n\t\t} else if (arg === \"--prompt-template\" && i + 1 < args.length) {\n\t\t\tresult.promptTemplates = result.promptTemplates ?? [];\n\t\t\tresult.promptTemplates.push(args[++i]);\n\t\t} else if (arg === \"--theme\" && i + 1 < args.length) {\n\t\t\tresult.themes = result.themes ?? [];\n\t\t\tresult.themes.push(args[++i]);\n\t\t} else if (arg === \"--no-skills\" || arg === \"-ns\") {\n\t\t\tresult.noSkills = true;\n\t\t} else if (arg === \"--no-prompt-templates\" || arg === \"-np\") {\n\t\t\tresult.noPromptTemplates = true;\n\t\t} else if (arg === \"--no-themes\") {\n\t\t\tresult.noThemes = true;\n\t\t} else if (arg === \"--no-context-files\" || arg === \"-nc\") {\n\t\t\tresult.noContextFiles = true;\n\t\t} else if (arg === \"--list-models\") {\n\t\t\t// Check if next arg is a search pattern (not a flag or file arg)\n\t\t\tif (i + 1 < args.length && !args[i + 1].startsWith(\"-\") && !args[i + 1].startsWith(\"@\")) {\n\t\t\t\tresult.listModels = args[++i];\n\t\t\t} else {\n\t\t\t\tresult.listModels = true;\n\t\t\t}\n\t\t} else if (arg === \"--verbose\") {\n\t\t\tresult.verbose = true;\n\t\t} else if (arg === \"--offline\") {\n\t\t\tresult.offline = true;\n\t\t} else if (arg.startsWith(\"@\")) {\n\t\t\tresult.fileArgs.push(arg.slice(1)); // Remove @ prefix\n\t\t} else if (arg.startsWith(\"--\")) {\n\t\t\tconst eqIndex = arg.indexOf(\"=\");\n\t\t\tif (eqIndex !== -1) {\n\t\t\t\tresult.unknownFlags.set(arg.slice(2, eqIndex), arg.slice(eqIndex + 1));\n\t\t\t} else {\n\t\t\t\tconst flagName = arg.slice(2);\n\t\t\t\tconst next = args[i + 1];\n\t\t\t\tif (next !== undefined && !next.startsWith(\"-\") && !next.startsWith(\"@\")) {\n\t\t\t\t\tresult.unknownFlags.set(flagName, next);\n\t\t\t\t\ti++;\n\t\t\t\t} else {\n\t\t\t\t\tresult.unknownFlags.set(flagName, true);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (arg.startsWith(\"-\") && !arg.startsWith(\"--\")) {\n\t\t\tresult.diagnostics.push({ type: \"error\", message: `Unknown option: ${arg}` });\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tresult.messages.push(arg);\n\t\t}\n\t}\n\n\treturn result;\n}\n\nexport function printHelp(extensionFlags?: ExtensionFlag[]): void {\n\tconst extensionFlagsText =\n\t\textensionFlags && extensionFlags.length > 0\n\t\t\t? `\\n${chalk.bold(\"Extension CLI Flags:\")}\\n${extensionFlags\n\t\t\t\t\t.map((flag) => {\n\t\t\t\t\t\tconst value = flag.type === \"string\" ? \" <value>\" : \"\";\n\t\t\t\t\t\tconst description = flag.description ?? `Registered by ${flag.extensionPath}`;\n\t\t\t\t\t\treturn ` --${flag.name}${value}`.padEnd(30) + description;\n\t\t\t\t\t})\n\t\t\t\t\t.join(\"\\n\")}\\n`\n\t\t\t: \"\";\n\tconsole.log(`${chalk.bold(APP_NAME)} - AI coding assistant with read, bash, edit, write tools\n\n${chalk.bold(\"Usage:\")}\n ${APP_NAME} [options] [@files...] [messages...]\n\n${chalk.bold(\"Commands:\")}\n ${APP_NAME} install <source> [-l] Install extension source and add to settings\n ${APP_NAME} remove <source> [-l] Remove extension source from settings\n ${APP_NAME} uninstall <source> [-l] Alias for remove\n ${APP_NAME} update [source|self] Update ${APP_NAME} and installed extensions\n ${APP_NAME} list List installed extensions from settings\n ${APP_NAME} config Open TUI to enable/disable package resources\n ${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list\n\n${chalk.bold(\"Options:\")}\n --provider <name> Provider name (default: google)\n --model <pattern> Model pattern or ID (supports \"provider/id\" and optional \":<thinking>\")\n --api-key <key> API key (defaults to env vars)\n --system-prompt <text> System prompt (default: coding assistant prompt)\n --append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)\n --mode <mode> Output mode: text (default), json, or rpc\n --print, -p Non-interactive mode: process prompt and exit\n --continue, -c Continue previous session\n --resume, -r Select a session to resume\n --session <path|id> Use specific session file or partial UUID\n --fork <path|id> Fork specific session file or partial UUID into a new session\n --session-dir <dir> Directory for session storage and lookup\n --no-session Don't save session (ephemeral)\n --models <patterns> Comma-separated model patterns for Ctrl+P cycling\n Supports globs (anthropic/*, *sonnet*) and fuzzy matching\n --no-tools, -nt Disable all tools by default (built-in and extension)\n --no-builtin-tools, -nbt Disable built-in tools by default but keep extension/custom tools enabled\n --tools, -t <tools> Comma-separated allowlist of tool names to enable\n Applies to built-in, extension, and custom tools\n --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh\n --extension, -e <path> Load an extension file (can be used multiple times)\n --no-extensions, -ne Disable extension discovery (explicit -e paths still work)\n --skill <path> Load a skill file or directory (can be used multiple times)\n --no-skills, -ns Disable skills discovery and loading\n --prompt-template <path> Load a prompt template file or directory (can be used multiple times)\n --no-prompt-templates, -np Disable prompt template discovery and loading\n --theme <path> Load a theme file or directory (can be used multiple times)\n --no-themes Disable theme discovery and loading\n --no-context-files, -nc Disable AGENTS.md and CLAUDE.md discovery and loading\n --export <file> Export session file to HTML and exit\n --list-models [search] List available models (with optional fuzzy search)\n --verbose Force verbose startup (overrides quietStartup setting)\n --offline Disable startup network operations (same as PI_OFFLINE=1)\n --help, -h Show this help\n --version, -v Show version number\n\nExtensions can register additional flags (e.g., --plan from plan-mode extension).${extensionFlagsText}\n\n${chalk.bold(\"Examples:\")}\n # Interactive mode\n ${APP_NAME}\n\n # Interactive mode with initial prompt\n ${APP_NAME} \"List all .ts files in src/\"\n\n # Include files in initial message\n ${APP_NAME} @prompt.md @image.png \"What color is the sky?\"\n\n # Non-interactive mode (process and exit)\n ${APP_NAME} -p \"List all .ts files in src/\"\n\n # Multiple messages (interactive)\n ${APP_NAME} \"Read package.json\" \"What dependencies do we have?\"\n\n # Continue previous session\n ${APP_NAME} --continue \"What did we discuss?\"\n\n # Use different model\n ${APP_NAME} --provider openai --model gpt-4o-mini \"Help me refactor this code\"\n\n # Use model with provider prefix (no --provider needed)\n ${APP_NAME} --model openai/gpt-4o \"Help me refactor this code\"\n\n # Use model with thinking level shorthand\n ${APP_NAME} --model sonnet:high \"Solve this complex problem\"\n\n # Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o\n\n # Limit to a specific provider with glob pattern\n ${APP_NAME} --models \"github-copilot/*\"\n\n # Cycle models with fixed thinking levels\n ${APP_NAME} --models sonnet:high,haiku:low\n\n # Start with a specific thinking level\n ${APP_NAME} --thinking high \"Solve this complex problem\"\n\n # Read-only mode (no file modifications possible)\n ${APP_NAME} --tools read,grep,find,ls -p \"Review the code in src/\"\n\n # Export a session file to HTML\n ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl\n ${APP_NAME} --export session.jsonl output.html\n\n${chalk.bold(\"Environment Variables:\")}\n ANTHROPIC_API_KEY - Anthropic Claude API key\n ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)\n OPENAI_API_KEY - OpenAI GPT API key\n AZURE_OPENAI_API_KEY - Azure OpenAI API key\n AZURE_OPENAI_BASE_URL - Azure OpenAI/Cognitive Services base URL (e.g. https://{resource}.openai.azure.com)\n AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL)\n AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)\n AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)\n DEEPSEEK_API_KEY - DeepSeek API key\n GEMINI_API_KEY - Google Gemini API key\n GROQ_API_KEY - Groq API key\n CEREBRAS_API_KEY - Cerebras API key\n XAI_API_KEY - xAI Grok API key\n FIREWORKS_API_KEY - Fireworks API key\n TOGETHER_API_KEY - Together AI API key\n OPENROUTER_API_KEY - OpenRouter API key\n AI_GATEWAY_API_KEY - Vercel AI Gateway API key\n ZAI_API_KEY - ZAI API key\n MISTRAL_API_KEY - Mistral API key\n MINIMAX_API_KEY - MiniMax API key\n MOONSHOT_API_KEY - Moonshot AI API key\n OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key\n KIMI_API_KEY - Kimi For Coding API key\n CLOUDFLARE_API_KEY - Cloudflare API token (Workers AI and AI Gateway)\n CLOUDFLARE_ACCOUNT_ID - Cloudflare account id (required for both)\n CLOUDFLARE_GATEWAY_ID - Cloudflare AI Gateway slug (required for AI Gateway)\n XIAOMI_API_KEY - Xiaomi MiMo API key (api.xiaomimimo.com billing)\n XIAOMI_TOKEN_PLAN_CN_API_KEY - Xiaomi MiMo Token Plan API key (China region)\n XIAOMI_TOKEN_PLAN_AMS_API_KEY - Xiaomi MiMo Token Plan API key (Amsterdam region)\n XIAOMI_TOKEN_PLAN_SGP_API_KEY - Xiaomi MiMo Token Plan API key (Singapore region)\n AWS_PROFILE - AWS profile for Amazon Bedrock\n AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock\n AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock\n AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token)\n AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1)\n ${ENV_AGENT_DIR.padEnd(32)} - Config directory (default: ~/${CONFIG_DIR_NAME}/agent)\n ${ENV_SESSION_DIR.padEnd(32)} - Session storage directory (overridden by --session-dir)\n PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)\n PI_OFFLINE - Disable startup network operations when set to 1/true/yes\n PI_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no\n PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)\n\n${chalk.bold(\"Built-in Tool Names:\")}\n read - Read file contents\n bash - Execute bash commands\n edit - Edit files with find/replace\n write - Write files (creates/overwrites)\n grep - Search file contents (read-only, off by default)\n find - Find files by glob pattern (read-only, off by default)\n ls - List directory contents (read-only, off by default)\n`);\n}\n"]}
package/dist/cli/args.js CHANGED
@@ -7,7 +7,49 @@ const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh
7
7
  export function isValidThinkingLevel(level) {
8
8
  return VALID_THINKING_LEVELS.includes(level);
9
9
  }
10
- export function parseArgs(args) {
10
+ /** Long flags that consume a value; `--flag=value` is accepted for these. */
11
+ const KNOWN_VALUE_FLAGS = new Set([
12
+ "mode",
13
+ "provider",
14
+ "model",
15
+ "api-key",
16
+ "system-prompt",
17
+ "append-system-prompt",
18
+ "session",
19
+ "fork",
20
+ "session-dir",
21
+ "models",
22
+ "tools",
23
+ "thinking",
24
+ "export",
25
+ "extension",
26
+ "skill",
27
+ "prompt-template",
28
+ "theme",
29
+ "list-models",
30
+ "print",
31
+ ]);
32
+ /**
33
+ * Expand `--flag=value` into two tokens for flags the parser knows take a
34
+ * value, so both spellings work. Unknown flags keep the raw `--x=y` handling
35
+ * below (extensions rely on it, including values that start with `-`).
36
+ */
37
+ function normalizeEqualsSyntax(args) {
38
+ const normalized = [];
39
+ for (const arg of args) {
40
+ if (arg.startsWith("--")) {
41
+ const eqIndex = arg.indexOf("=");
42
+ if (eqIndex > 2 && KNOWN_VALUE_FLAGS.has(arg.slice(2, eqIndex))) {
43
+ normalized.push(arg.slice(0, eqIndex), arg.slice(eqIndex + 1));
44
+ continue;
45
+ }
46
+ }
47
+ normalized.push(arg);
48
+ }
49
+ return normalized;
50
+ }
51
+ export function parseArgs(rawArgs) {
52
+ const args = normalizeEqualsSyntax(rawArgs);
11
53
  const result = {
12
54
  messages: [],
13
55
  fileArgs: [],
@@ -1 +1 @@
1
- {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * CLI argument parsing and help display\n */\n\nimport chalk from \"chalk\";\nimport type { ThinkingLevel } from \"phi-code-agent\";\nimport { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from \"../config.js\";\nimport type { ExtensionFlag } from \"../core/extensions/types.js\";\n\nexport type Mode = \"text\" | \"json\" | \"rpc\";\n\nexport interface Args {\n\tprovider?: string;\n\tmodel?: string;\n\tapiKey?: string;\n\tsystemPrompt?: string;\n\tappendSystemPrompt?: string[];\n\tthinking?: ThinkingLevel;\n\tcontinue?: boolean;\n\tresume?: boolean;\n\thelp?: boolean;\n\tversion?: boolean;\n\tmode?: Mode;\n\tnoSession?: boolean;\n\tsession?: string;\n\tfork?: string;\n\tsessionDir?: string;\n\tmodels?: string[];\n\ttools?: string[];\n\tnoTools?: boolean;\n\tnoBuiltinTools?: boolean;\n\textensions?: string[];\n\tnoExtensions?: boolean;\n\tprint?: boolean;\n\texport?: string;\n\tnoSkills?: boolean;\n\tskills?: string[];\n\tpromptTemplates?: string[];\n\tnoPromptTemplates?: boolean;\n\tthemes?: string[];\n\tnoThemes?: boolean;\n\tnoContextFiles?: boolean;\n\tlistModels?: string | true;\n\toffline?: boolean;\n\tverbose?: boolean;\n\tmessages: string[];\n\tfileArgs: string[];\n\t/** Unknown flags (potentially extension flags) - map of flag name to value */\n\tunknownFlags: Map<string, boolean | string>;\n\tdiagnostics: Array<{ type: \"warning\" | \"error\"; message: string }>;\n}\n\nconst VALID_THINKING_LEVELS = [\"off\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"] as const;\n\nexport function isValidThinkingLevel(level: string): level is ThinkingLevel {\n\treturn VALID_THINKING_LEVELS.includes(level as ThinkingLevel);\n}\n\nexport function parseArgs(args: string[]): Args {\n\tconst result: Args = {\n\t\tmessages: [],\n\t\tfileArgs: [],\n\t\tunknownFlags: new Map(),\n\t\tdiagnostics: [],\n\t};\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\n\t\tif (arg === \"--help\" || arg === \"-h\") {\n\t\t\tresult.help = true;\n\t\t} else if (arg === \"--version\" || arg === \"-v\") {\n\t\t\tresult.version = true;\n\t\t} else if (arg === \"--mode\" && i + 1 < args.length) {\n\t\t\tconst mode = args[++i];\n\t\t\tif (mode === \"text\" || mode === \"json\" || mode === \"rpc\") {\n\t\t\t\tresult.mode = mode;\n\t\t\t}\n\t\t} else if (arg === \"--continue\" || arg === \"-c\") {\n\t\t\tresult.continue = true;\n\t\t} else if (arg === \"--resume\" || arg === \"-r\") {\n\t\t\tresult.resume = true;\n\t\t} else if (arg === \"--provider\" && i + 1 < args.length) {\n\t\t\tresult.provider = args[++i];\n\t\t} else if (arg === \"--model\" && i + 1 < args.length) {\n\t\t\tresult.model = args[++i];\n\t\t} else if (arg === \"--api-key\" && i + 1 < args.length) {\n\t\t\tresult.apiKey = args[++i];\n\t\t} else if (arg === \"--system-prompt\" && i + 1 < args.length) {\n\t\t\tresult.systemPrompt = args[++i];\n\t\t} else if (arg === \"--append-system-prompt\" && i + 1 < args.length) {\n\t\t\tresult.appendSystemPrompt = result.appendSystemPrompt ?? [];\n\t\t\tresult.appendSystemPrompt.push(args[++i]);\n\t\t} else if (arg === \"--no-session\") {\n\t\t\tresult.noSession = true;\n\t\t} else if (arg === \"--session\" && i + 1 < args.length) {\n\t\t\tresult.session = args[++i];\n\t\t} else if (arg === \"--fork\" && i + 1 < args.length) {\n\t\t\tresult.fork = args[++i];\n\t\t} else if (arg === \"--session-dir\" && i + 1 < args.length) {\n\t\t\tresult.sessionDir = args[++i];\n\t\t} else if (arg === \"--models\" && i + 1 < args.length) {\n\t\t\tresult.models = args[++i].split(\",\").map((s) => s.trim());\n\t\t} else if (arg === \"--no-tools\" || arg === \"-nt\") {\n\t\t\tresult.noTools = true;\n\t\t} else if (arg === \"--no-builtin-tools\" || arg === \"-nbt\") {\n\t\t\tresult.noBuiltinTools = true;\n\t\t} else if ((arg === \"--tools\" || arg === \"-t\") && i + 1 < args.length) {\n\t\t\tresult.tools = args[++i]\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter((name) => name.length > 0);\n\t\t} else if (arg === \"--thinking\" && i + 1 < args.length) {\n\t\t\tconst level = args[++i];\n\t\t\tif (isValidThinkingLevel(level)) {\n\t\t\t\tresult.thinking = level;\n\t\t\t} else {\n\t\t\t\tresult.diagnostics.push({\n\t\t\t\t\ttype: \"warning\",\n\t\t\t\t\tmessage: `Invalid thinking level \"${level}\". Valid values: ${VALID_THINKING_LEVELS.join(\", \")}`,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (arg === \"--print\" || arg === \"-p\") {\n\t\t\tresult.print = true;\n\t\t\tconst next = args[i + 1];\n\t\t\tif (next !== undefined && !next.startsWith(\"@\") && (!next.startsWith(\"-\") || next.startsWith(\"---\"))) {\n\t\t\t\tresult.messages.push(next);\n\t\t\t\ti++;\n\t\t\t}\n\t\t} else if (arg === \"--export\" && i + 1 < args.length) {\n\t\t\tresult.export = args[++i];\n\t\t} else if ((arg === \"--extension\" || arg === \"-e\") && i + 1 < args.length) {\n\t\t\tresult.extensions = result.extensions ?? [];\n\t\t\tresult.extensions.push(args[++i]);\n\t\t} else if (arg === \"--no-extensions\" || arg === \"-ne\") {\n\t\t\tresult.noExtensions = true;\n\t\t} else if (arg === \"--skill\" && i + 1 < args.length) {\n\t\t\tresult.skills = result.skills ?? [];\n\t\t\tresult.skills.push(args[++i]);\n\t\t} else if (arg === \"--prompt-template\" && i + 1 < args.length) {\n\t\t\tresult.promptTemplates = result.promptTemplates ?? [];\n\t\t\tresult.promptTemplates.push(args[++i]);\n\t\t} else if (arg === \"--theme\" && i + 1 < args.length) {\n\t\t\tresult.themes = result.themes ?? [];\n\t\t\tresult.themes.push(args[++i]);\n\t\t} else if (arg === \"--no-skills\" || arg === \"-ns\") {\n\t\t\tresult.noSkills = true;\n\t\t} else if (arg === \"--no-prompt-templates\" || arg === \"-np\") {\n\t\t\tresult.noPromptTemplates = true;\n\t\t} else if (arg === \"--no-themes\") {\n\t\t\tresult.noThemes = true;\n\t\t} else if (arg === \"--no-context-files\" || arg === \"-nc\") {\n\t\t\tresult.noContextFiles = true;\n\t\t} else if (arg === \"--list-models\") {\n\t\t\t// Check if next arg is a search pattern (not a flag or file arg)\n\t\t\tif (i + 1 < args.length && !args[i + 1].startsWith(\"-\") && !args[i + 1].startsWith(\"@\")) {\n\t\t\t\tresult.listModels = args[++i];\n\t\t\t} else {\n\t\t\t\tresult.listModels = true;\n\t\t\t}\n\t\t} else if (arg === \"--verbose\") {\n\t\t\tresult.verbose = true;\n\t\t} else if (arg === \"--offline\") {\n\t\t\tresult.offline = true;\n\t\t} else if (arg.startsWith(\"@\")) {\n\t\t\tresult.fileArgs.push(arg.slice(1)); // Remove @ prefix\n\t\t} else if (arg.startsWith(\"--\")) {\n\t\t\tconst eqIndex = arg.indexOf(\"=\");\n\t\t\tif (eqIndex !== -1) {\n\t\t\t\tresult.unknownFlags.set(arg.slice(2, eqIndex), arg.slice(eqIndex + 1));\n\t\t\t} else {\n\t\t\t\tconst flagName = arg.slice(2);\n\t\t\t\tconst next = args[i + 1];\n\t\t\t\tif (next !== undefined && !next.startsWith(\"-\") && !next.startsWith(\"@\")) {\n\t\t\t\t\tresult.unknownFlags.set(flagName, next);\n\t\t\t\t\ti++;\n\t\t\t\t} else {\n\t\t\t\t\tresult.unknownFlags.set(flagName, true);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (arg.startsWith(\"-\") && !arg.startsWith(\"--\")) {\n\t\t\tresult.diagnostics.push({ type: \"error\", message: `Unknown option: ${arg}` });\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tresult.messages.push(arg);\n\t\t}\n\t}\n\n\treturn result;\n}\n\nexport function printHelp(extensionFlags?: ExtensionFlag[]): void {\n\tconst extensionFlagsText =\n\t\textensionFlags && extensionFlags.length > 0\n\t\t\t? `\\n${chalk.bold(\"Extension CLI Flags:\")}\\n${extensionFlags\n\t\t\t\t\t.map((flag) => {\n\t\t\t\t\t\tconst value = flag.type === \"string\" ? \" <value>\" : \"\";\n\t\t\t\t\t\tconst description = flag.description ?? `Registered by ${flag.extensionPath}`;\n\t\t\t\t\t\treturn ` --${flag.name}${value}`.padEnd(30) + description;\n\t\t\t\t\t})\n\t\t\t\t\t.join(\"\\n\")}\\n`\n\t\t\t: \"\";\n\tconsole.log(`${chalk.bold(APP_NAME)} - AI coding assistant with read, bash, edit, write tools\n\n${chalk.bold(\"Usage:\")}\n ${APP_NAME} [options] [@files...] [messages...]\n\n${chalk.bold(\"Commands:\")}\n ${APP_NAME} install <source> [-l] Install extension source and add to settings\n ${APP_NAME} remove <source> [-l] Remove extension source from settings\n ${APP_NAME} uninstall <source> [-l] Alias for remove\n ${APP_NAME} update [source|self] Update ${APP_NAME} and installed extensions\n ${APP_NAME} list List installed extensions from settings\n ${APP_NAME} config Open TUI to enable/disable package resources\n ${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list\n\n${chalk.bold(\"Options:\")}\n --provider <name> Provider name (default: google)\n --model <pattern> Model pattern or ID (supports \"provider/id\" and optional \":<thinking>\")\n --api-key <key> API key (defaults to env vars)\n --system-prompt <text> System prompt (default: coding assistant prompt)\n --append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)\n --mode <mode> Output mode: text (default), json, or rpc\n --print, -p Non-interactive mode: process prompt and exit\n --continue, -c Continue previous session\n --resume, -r Select a session to resume\n --session <path|id> Use specific session file or partial UUID\n --fork <path|id> Fork specific session file or partial UUID into a new session\n --session-dir <dir> Directory for session storage and lookup\n --no-session Don't save session (ephemeral)\n --models <patterns> Comma-separated model patterns for Ctrl+P cycling\n Supports globs (anthropic/*, *sonnet*) and fuzzy matching\n --no-tools, -nt Disable all tools by default (built-in and extension)\n --no-builtin-tools, -nbt Disable built-in tools by default but keep extension/custom tools enabled\n --tools, -t <tools> Comma-separated allowlist of tool names to enable\n Applies to built-in, extension, and custom tools\n --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh\n --extension, -e <path> Load an extension file (can be used multiple times)\n --no-extensions, -ne Disable extension discovery (explicit -e paths still work)\n --skill <path> Load a skill file or directory (can be used multiple times)\n --no-skills, -ns Disable skills discovery and loading\n --prompt-template <path> Load a prompt template file or directory (can be used multiple times)\n --no-prompt-templates, -np Disable prompt template discovery and loading\n --theme <path> Load a theme file or directory (can be used multiple times)\n --no-themes Disable theme discovery and loading\n --no-context-files, -nc Disable AGENTS.md and CLAUDE.md discovery and loading\n --export <file> Export session file to HTML and exit\n --list-models [search] List available models (with optional fuzzy search)\n --verbose Force verbose startup (overrides quietStartup setting)\n --offline Disable startup network operations (same as PI_OFFLINE=1)\n --help, -h Show this help\n --version, -v Show version number\n\nExtensions can register additional flags (e.g., --plan from plan-mode extension).${extensionFlagsText}\n\n${chalk.bold(\"Examples:\")}\n # Interactive mode\n ${APP_NAME}\n\n # Interactive mode with initial prompt\n ${APP_NAME} \"List all .ts files in src/\"\n\n # Include files in initial message\n ${APP_NAME} @prompt.md @image.png \"What color is the sky?\"\n\n # Non-interactive mode (process and exit)\n ${APP_NAME} -p \"List all .ts files in src/\"\n\n # Multiple messages (interactive)\n ${APP_NAME} \"Read package.json\" \"What dependencies do we have?\"\n\n # Continue previous session\n ${APP_NAME} --continue \"What did we discuss?\"\n\n # Use different model\n ${APP_NAME} --provider openai --model gpt-4o-mini \"Help me refactor this code\"\n\n # Use model with provider prefix (no --provider needed)\n ${APP_NAME} --model openai/gpt-4o \"Help me refactor this code\"\n\n # Use model with thinking level shorthand\n ${APP_NAME} --model sonnet:high \"Solve this complex problem\"\n\n # Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o\n\n # Limit to a specific provider with glob pattern\n ${APP_NAME} --models \"github-copilot/*\"\n\n # Cycle models with fixed thinking levels\n ${APP_NAME} --models sonnet:high,haiku:low\n\n # Start with a specific thinking level\n ${APP_NAME} --thinking high \"Solve this complex problem\"\n\n # Read-only mode (no file modifications possible)\n ${APP_NAME} --tools read,grep,find,ls -p \"Review the code in src/\"\n\n # Export a session file to HTML\n ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl\n ${APP_NAME} --export session.jsonl output.html\n\n${chalk.bold(\"Environment Variables:\")}\n ANTHROPIC_API_KEY - Anthropic Claude API key\n ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)\n OPENAI_API_KEY - OpenAI GPT API key\n AZURE_OPENAI_API_KEY - Azure OpenAI API key\n AZURE_OPENAI_BASE_URL - Azure OpenAI/Cognitive Services base URL (e.g. https://{resource}.openai.azure.com)\n AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL)\n AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)\n AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)\n DEEPSEEK_API_KEY - DeepSeek API key\n GEMINI_API_KEY - Google Gemini API key\n GROQ_API_KEY - Groq API key\n CEREBRAS_API_KEY - Cerebras API key\n XAI_API_KEY - xAI Grok API key\n FIREWORKS_API_KEY - Fireworks API key\n TOGETHER_API_KEY - Together AI API key\n OPENROUTER_API_KEY - OpenRouter API key\n AI_GATEWAY_API_KEY - Vercel AI Gateway API key\n ZAI_API_KEY - ZAI API key\n MISTRAL_API_KEY - Mistral API key\n MINIMAX_API_KEY - MiniMax API key\n MOONSHOT_API_KEY - Moonshot AI API key\n OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key\n KIMI_API_KEY - Kimi For Coding API key\n CLOUDFLARE_API_KEY - Cloudflare API token (Workers AI and AI Gateway)\n CLOUDFLARE_ACCOUNT_ID - Cloudflare account id (required for both)\n CLOUDFLARE_GATEWAY_ID - Cloudflare AI Gateway slug (required for AI Gateway)\n XIAOMI_API_KEY - Xiaomi MiMo API key (api.xiaomimimo.com billing)\n XIAOMI_TOKEN_PLAN_CN_API_KEY - Xiaomi MiMo Token Plan API key (China region)\n XIAOMI_TOKEN_PLAN_AMS_API_KEY - Xiaomi MiMo Token Plan API key (Amsterdam region)\n XIAOMI_TOKEN_PLAN_SGP_API_KEY - Xiaomi MiMo Token Plan API key (Singapore region)\n AWS_PROFILE - AWS profile for Amazon Bedrock\n AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock\n AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock\n AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token)\n AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1)\n ${ENV_AGENT_DIR.padEnd(32)} - Config directory (default: ~/${CONFIG_DIR_NAME}/agent)\n ${ENV_SESSION_DIR.padEnd(32)} - Session storage directory (overridden by --session-dir)\n PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)\n PI_OFFLINE - Disable startup network operations when set to 1/true/yes\n PI_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no\n PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)\n\n${chalk.bold(\"Built-in Tool Names:\")}\n read - Read file contents\n bash - Execute bash commands\n edit - Edit files with find/replace\n write - Write files (creates/overwrites)\n grep - Search file contents (read-only, off by default)\n find - Find files by glob pattern (read-only, off by default)\n ls - List directory contents (read-only, off by default)\n`);\n}\n"]}
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * CLI argument parsing and help display\n */\n\nimport chalk from \"chalk\";\nimport type { ThinkingLevel } from \"phi-code-agent\";\nimport { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from \"../config.js\";\nimport type { ExtensionFlag } from \"../core/extensions/types.js\";\n\nexport type Mode = \"text\" | \"json\" | \"rpc\";\n\nexport interface Args {\n\tprovider?: string;\n\tmodel?: string;\n\tapiKey?: string;\n\tsystemPrompt?: string;\n\tappendSystemPrompt?: string[];\n\tthinking?: ThinkingLevel;\n\tcontinue?: boolean;\n\tresume?: boolean;\n\thelp?: boolean;\n\tversion?: boolean;\n\tmode?: Mode;\n\tnoSession?: boolean;\n\tsession?: string;\n\tfork?: string;\n\tsessionDir?: string;\n\tmodels?: string[];\n\ttools?: string[];\n\tnoTools?: boolean;\n\tnoBuiltinTools?: boolean;\n\textensions?: string[];\n\tnoExtensions?: boolean;\n\tprint?: boolean;\n\texport?: string;\n\tnoSkills?: boolean;\n\tskills?: string[];\n\tpromptTemplates?: string[];\n\tnoPromptTemplates?: boolean;\n\tthemes?: string[];\n\tnoThemes?: boolean;\n\tnoContextFiles?: boolean;\n\tlistModels?: string | true;\n\toffline?: boolean;\n\tverbose?: boolean;\n\tmessages: string[];\n\tfileArgs: string[];\n\t/** Unknown flags (potentially extension flags) - map of flag name to value */\n\tunknownFlags: Map<string, boolean | string>;\n\tdiagnostics: Array<{ type: \"warning\" | \"error\"; message: string }>;\n}\n\nconst VALID_THINKING_LEVELS = [\"off\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"] as const;\n\nexport function isValidThinkingLevel(level: string): level is ThinkingLevel {\n\treturn VALID_THINKING_LEVELS.includes(level as ThinkingLevel);\n}\n\n/** Long flags that consume a value; `--flag=value` is accepted for these. */\nconst KNOWN_VALUE_FLAGS = new Set([\n\t\"mode\",\n\t\"provider\",\n\t\"model\",\n\t\"api-key\",\n\t\"system-prompt\",\n\t\"append-system-prompt\",\n\t\"session\",\n\t\"fork\",\n\t\"session-dir\",\n\t\"models\",\n\t\"tools\",\n\t\"thinking\",\n\t\"export\",\n\t\"extension\",\n\t\"skill\",\n\t\"prompt-template\",\n\t\"theme\",\n\t\"list-models\",\n\t\"print\",\n]);\n\n/**\n * Expand `--flag=value` into two tokens for flags the parser knows take a\n * value, so both spellings work. Unknown flags keep the raw `--x=y` handling\n * below (extensions rely on it, including values that start with `-`).\n */\nfunction normalizeEqualsSyntax(args: string[]): string[] {\n\tconst normalized: string[] = [];\n\tfor (const arg of args) {\n\t\tif (arg.startsWith(\"--\")) {\n\t\t\tconst eqIndex = arg.indexOf(\"=\");\n\t\t\tif (eqIndex > 2 && KNOWN_VALUE_FLAGS.has(arg.slice(2, eqIndex))) {\n\t\t\t\tnormalized.push(arg.slice(0, eqIndex), arg.slice(eqIndex + 1));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tnormalized.push(arg);\n\t}\n\treturn normalized;\n}\n\nexport function parseArgs(rawArgs: string[]): Args {\n\tconst args = normalizeEqualsSyntax(rawArgs);\n\tconst result: Args = {\n\t\tmessages: [],\n\t\tfileArgs: [],\n\t\tunknownFlags: new Map(),\n\t\tdiagnostics: [],\n\t};\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\n\t\tif (arg === \"--help\" || arg === \"-h\") {\n\t\t\tresult.help = true;\n\t\t} else if (arg === \"--version\" || arg === \"-v\") {\n\t\t\tresult.version = true;\n\t\t} else if (arg === \"--mode\" && i + 1 < args.length) {\n\t\t\tconst mode = args[++i];\n\t\t\tif (mode === \"text\" || mode === \"json\" || mode === \"rpc\") {\n\t\t\t\tresult.mode = mode;\n\t\t\t}\n\t\t} else if (arg === \"--continue\" || arg === \"-c\") {\n\t\t\tresult.continue = true;\n\t\t} else if (arg === \"--resume\" || arg === \"-r\") {\n\t\t\tresult.resume = true;\n\t\t} else if (arg === \"--provider\" && i + 1 < args.length) {\n\t\t\tresult.provider = args[++i];\n\t\t} else if (arg === \"--model\" && i + 1 < args.length) {\n\t\t\tresult.model = args[++i];\n\t\t} else if (arg === \"--api-key\" && i + 1 < args.length) {\n\t\t\tresult.apiKey = args[++i];\n\t\t} else if (arg === \"--system-prompt\" && i + 1 < args.length) {\n\t\t\tresult.systemPrompt = args[++i];\n\t\t} else if (arg === \"--append-system-prompt\" && i + 1 < args.length) {\n\t\t\tresult.appendSystemPrompt = result.appendSystemPrompt ?? [];\n\t\t\tresult.appendSystemPrompt.push(args[++i]);\n\t\t} else if (arg === \"--no-session\") {\n\t\t\tresult.noSession = true;\n\t\t} else if (arg === \"--session\" && i + 1 < args.length) {\n\t\t\tresult.session = args[++i];\n\t\t} else if (arg === \"--fork\" && i + 1 < args.length) {\n\t\t\tresult.fork = args[++i];\n\t\t} else if (arg === \"--session-dir\" && i + 1 < args.length) {\n\t\t\tresult.sessionDir = args[++i];\n\t\t} else if (arg === \"--models\" && i + 1 < args.length) {\n\t\t\tresult.models = args[++i].split(\",\").map((s) => s.trim());\n\t\t} else if (arg === \"--no-tools\" || arg === \"-nt\") {\n\t\t\tresult.noTools = true;\n\t\t} else if (arg === \"--no-builtin-tools\" || arg === \"-nbt\") {\n\t\t\tresult.noBuiltinTools = true;\n\t\t} else if ((arg === \"--tools\" || arg === \"-t\") && i + 1 < args.length) {\n\t\t\tresult.tools = args[++i]\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter((name) => name.length > 0);\n\t\t} else if (arg === \"--thinking\" && i + 1 < args.length) {\n\t\t\tconst level = args[++i];\n\t\t\tif (isValidThinkingLevel(level)) {\n\t\t\t\tresult.thinking = level;\n\t\t\t} else {\n\t\t\t\tresult.diagnostics.push({\n\t\t\t\t\ttype: \"warning\",\n\t\t\t\t\tmessage: `Invalid thinking level \"${level}\". Valid values: ${VALID_THINKING_LEVELS.join(\", \")}`,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (arg === \"--print\" || arg === \"-p\") {\n\t\t\tresult.print = true;\n\t\t\tconst next = args[i + 1];\n\t\t\tif (next !== undefined && !next.startsWith(\"@\") && (!next.startsWith(\"-\") || next.startsWith(\"---\"))) {\n\t\t\t\tresult.messages.push(next);\n\t\t\t\ti++;\n\t\t\t}\n\t\t} else if (arg === \"--export\" && i + 1 < args.length) {\n\t\t\tresult.export = args[++i];\n\t\t} else if ((arg === \"--extension\" || arg === \"-e\") && i + 1 < args.length) {\n\t\t\tresult.extensions = result.extensions ?? [];\n\t\t\tresult.extensions.push(args[++i]);\n\t\t} else if (arg === \"--no-extensions\" || arg === \"-ne\") {\n\t\t\tresult.noExtensions = true;\n\t\t} else if (arg === \"--skill\" && i + 1 < args.length) {\n\t\t\tresult.skills = result.skills ?? [];\n\t\t\tresult.skills.push(args[++i]);\n\t\t} else if (arg === \"--prompt-template\" && i + 1 < args.length) {\n\t\t\tresult.promptTemplates = result.promptTemplates ?? [];\n\t\t\tresult.promptTemplates.push(args[++i]);\n\t\t} else if (arg === \"--theme\" && i + 1 < args.length) {\n\t\t\tresult.themes = result.themes ?? [];\n\t\t\tresult.themes.push(args[++i]);\n\t\t} else if (arg === \"--no-skills\" || arg === \"-ns\") {\n\t\t\tresult.noSkills = true;\n\t\t} else if (arg === \"--no-prompt-templates\" || arg === \"-np\") {\n\t\t\tresult.noPromptTemplates = true;\n\t\t} else if (arg === \"--no-themes\") {\n\t\t\tresult.noThemes = true;\n\t\t} else if (arg === \"--no-context-files\" || arg === \"-nc\") {\n\t\t\tresult.noContextFiles = true;\n\t\t} else if (arg === \"--list-models\") {\n\t\t\t// Check if next arg is a search pattern (not a flag or file arg)\n\t\t\tif (i + 1 < args.length && !args[i + 1].startsWith(\"-\") && !args[i + 1].startsWith(\"@\")) {\n\t\t\t\tresult.listModels = args[++i];\n\t\t\t} else {\n\t\t\t\tresult.listModels = true;\n\t\t\t}\n\t\t} else if (arg === \"--verbose\") {\n\t\t\tresult.verbose = true;\n\t\t} else if (arg === \"--offline\") {\n\t\t\tresult.offline = true;\n\t\t} else if (arg.startsWith(\"@\")) {\n\t\t\tresult.fileArgs.push(arg.slice(1)); // Remove @ prefix\n\t\t} else if (arg.startsWith(\"--\")) {\n\t\t\tconst eqIndex = arg.indexOf(\"=\");\n\t\t\tif (eqIndex !== -1) {\n\t\t\t\tresult.unknownFlags.set(arg.slice(2, eqIndex), arg.slice(eqIndex + 1));\n\t\t\t} else {\n\t\t\t\tconst flagName = arg.slice(2);\n\t\t\t\tconst next = args[i + 1];\n\t\t\t\tif (next !== undefined && !next.startsWith(\"-\") && !next.startsWith(\"@\")) {\n\t\t\t\t\tresult.unknownFlags.set(flagName, next);\n\t\t\t\t\ti++;\n\t\t\t\t} else {\n\t\t\t\t\tresult.unknownFlags.set(flagName, true);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (arg.startsWith(\"-\") && !arg.startsWith(\"--\")) {\n\t\t\tresult.diagnostics.push({ type: \"error\", message: `Unknown option: ${arg}` });\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tresult.messages.push(arg);\n\t\t}\n\t}\n\n\treturn result;\n}\n\nexport function printHelp(extensionFlags?: ExtensionFlag[]): void {\n\tconst extensionFlagsText =\n\t\textensionFlags && extensionFlags.length > 0\n\t\t\t? `\\n${chalk.bold(\"Extension CLI Flags:\")}\\n${extensionFlags\n\t\t\t\t\t.map((flag) => {\n\t\t\t\t\t\tconst value = flag.type === \"string\" ? \" <value>\" : \"\";\n\t\t\t\t\t\tconst description = flag.description ?? `Registered by ${flag.extensionPath}`;\n\t\t\t\t\t\treturn ` --${flag.name}${value}`.padEnd(30) + description;\n\t\t\t\t\t})\n\t\t\t\t\t.join(\"\\n\")}\\n`\n\t\t\t: \"\";\n\tconsole.log(`${chalk.bold(APP_NAME)} - AI coding assistant with read, bash, edit, write tools\n\n${chalk.bold(\"Usage:\")}\n ${APP_NAME} [options] [@files...] [messages...]\n\n${chalk.bold(\"Commands:\")}\n ${APP_NAME} install <source> [-l] Install extension source and add to settings\n ${APP_NAME} remove <source> [-l] Remove extension source from settings\n ${APP_NAME} uninstall <source> [-l] Alias for remove\n ${APP_NAME} update [source|self] Update ${APP_NAME} and installed extensions\n ${APP_NAME} list List installed extensions from settings\n ${APP_NAME} config Open TUI to enable/disable package resources\n ${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list\n\n${chalk.bold(\"Options:\")}\n --provider <name> Provider name (default: google)\n --model <pattern> Model pattern or ID (supports \"provider/id\" and optional \":<thinking>\")\n --api-key <key> API key (defaults to env vars)\n --system-prompt <text> System prompt (default: coding assistant prompt)\n --append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)\n --mode <mode> Output mode: text (default), json, or rpc\n --print, -p Non-interactive mode: process prompt and exit\n --continue, -c Continue previous session\n --resume, -r Select a session to resume\n --session <path|id> Use specific session file or partial UUID\n --fork <path|id> Fork specific session file or partial UUID into a new session\n --session-dir <dir> Directory for session storage and lookup\n --no-session Don't save session (ephemeral)\n --models <patterns> Comma-separated model patterns for Ctrl+P cycling\n Supports globs (anthropic/*, *sonnet*) and fuzzy matching\n --no-tools, -nt Disable all tools by default (built-in and extension)\n --no-builtin-tools, -nbt Disable built-in tools by default but keep extension/custom tools enabled\n --tools, -t <tools> Comma-separated allowlist of tool names to enable\n Applies to built-in, extension, and custom tools\n --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh\n --extension, -e <path> Load an extension file (can be used multiple times)\n --no-extensions, -ne Disable extension discovery (explicit -e paths still work)\n --skill <path> Load a skill file or directory (can be used multiple times)\n --no-skills, -ns Disable skills discovery and loading\n --prompt-template <path> Load a prompt template file or directory (can be used multiple times)\n --no-prompt-templates, -np Disable prompt template discovery and loading\n --theme <path> Load a theme file or directory (can be used multiple times)\n --no-themes Disable theme discovery and loading\n --no-context-files, -nc Disable AGENTS.md and CLAUDE.md discovery and loading\n --export <file> Export session file to HTML and exit\n --list-models [search] List available models (with optional fuzzy search)\n --verbose Force verbose startup (overrides quietStartup setting)\n --offline Disable startup network operations (same as PI_OFFLINE=1)\n --help, -h Show this help\n --version, -v Show version number\n\nExtensions can register additional flags (e.g., --plan from plan-mode extension).${extensionFlagsText}\n\n${chalk.bold(\"Examples:\")}\n # Interactive mode\n ${APP_NAME}\n\n # Interactive mode with initial prompt\n ${APP_NAME} \"List all .ts files in src/\"\n\n # Include files in initial message\n ${APP_NAME} @prompt.md @image.png \"What color is the sky?\"\n\n # Non-interactive mode (process and exit)\n ${APP_NAME} -p \"List all .ts files in src/\"\n\n # Multiple messages (interactive)\n ${APP_NAME} \"Read package.json\" \"What dependencies do we have?\"\n\n # Continue previous session\n ${APP_NAME} --continue \"What did we discuss?\"\n\n # Use different model\n ${APP_NAME} --provider openai --model gpt-4o-mini \"Help me refactor this code\"\n\n # Use model with provider prefix (no --provider needed)\n ${APP_NAME} --model openai/gpt-4o \"Help me refactor this code\"\n\n # Use model with thinking level shorthand\n ${APP_NAME} --model sonnet:high \"Solve this complex problem\"\n\n # Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o\n\n # Limit to a specific provider with glob pattern\n ${APP_NAME} --models \"github-copilot/*\"\n\n # Cycle models with fixed thinking levels\n ${APP_NAME} --models sonnet:high,haiku:low\n\n # Start with a specific thinking level\n ${APP_NAME} --thinking high \"Solve this complex problem\"\n\n # Read-only mode (no file modifications possible)\n ${APP_NAME} --tools read,grep,find,ls -p \"Review the code in src/\"\n\n # Export a session file to HTML\n ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl\n ${APP_NAME} --export session.jsonl output.html\n\n${chalk.bold(\"Environment Variables:\")}\n ANTHROPIC_API_KEY - Anthropic Claude API key\n ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)\n OPENAI_API_KEY - OpenAI GPT API key\n AZURE_OPENAI_API_KEY - Azure OpenAI API key\n AZURE_OPENAI_BASE_URL - Azure OpenAI/Cognitive Services base URL (e.g. https://{resource}.openai.azure.com)\n AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL)\n AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)\n AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)\n DEEPSEEK_API_KEY - DeepSeek API key\n GEMINI_API_KEY - Google Gemini API key\n GROQ_API_KEY - Groq API key\n CEREBRAS_API_KEY - Cerebras API key\n XAI_API_KEY - xAI Grok API key\n FIREWORKS_API_KEY - Fireworks API key\n TOGETHER_API_KEY - Together AI API key\n OPENROUTER_API_KEY - OpenRouter API key\n AI_GATEWAY_API_KEY - Vercel AI Gateway API key\n ZAI_API_KEY - ZAI API key\n MISTRAL_API_KEY - Mistral API key\n MINIMAX_API_KEY - MiniMax API key\n MOONSHOT_API_KEY - Moonshot AI API key\n OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key\n KIMI_API_KEY - Kimi For Coding API key\n CLOUDFLARE_API_KEY - Cloudflare API token (Workers AI and AI Gateway)\n CLOUDFLARE_ACCOUNT_ID - Cloudflare account id (required for both)\n CLOUDFLARE_GATEWAY_ID - Cloudflare AI Gateway slug (required for AI Gateway)\n XIAOMI_API_KEY - Xiaomi MiMo API key (api.xiaomimimo.com billing)\n XIAOMI_TOKEN_PLAN_CN_API_KEY - Xiaomi MiMo Token Plan API key (China region)\n XIAOMI_TOKEN_PLAN_AMS_API_KEY - Xiaomi MiMo Token Plan API key (Amsterdam region)\n XIAOMI_TOKEN_PLAN_SGP_API_KEY - Xiaomi MiMo Token Plan API key (Singapore region)\n AWS_PROFILE - AWS profile for Amazon Bedrock\n AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock\n AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock\n AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token)\n AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1)\n ${ENV_AGENT_DIR.padEnd(32)} - Config directory (default: ~/${CONFIG_DIR_NAME}/agent)\n ${ENV_SESSION_DIR.padEnd(32)} - Session storage directory (overridden by --session-dir)\n PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)\n PI_OFFLINE - Disable startup network operations when set to 1/true/yes\n PI_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no\n PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)\n\n${chalk.bold(\"Built-in Tool Names:\")}\n read - Read file contents\n bash - Execute bash commands\n edit - Edit files with find/replace\n write - Write files (creates/overwrites)\n grep - Search file contents (read-only, off by default)\n find - Find files by glob pattern (read-only, off by default)\n ls - List directory contents (read-only, off by default)\n`);\n}\n"]}
@@ -5,11 +5,19 @@
5
5
  * Per Q6 strategy A: keys stored in plain text in ~/.phi/agent/models.json
6
6
  * with chmod 0600 on Unix and clear warnings to users.
7
7
  *
8
- * Storage format: models.json providers.<id>.apiKey contains the key value
9
- * directly (not an env var reference). Resolution priority:
10
- * 1. Value in models.json (if non-empty)
8
+ * Storage format: models.json providers.<id>.apiKey. Resolution follows the
9
+ * same convention as model-registry/resolve-config-value ("env var name or
10
+ * literal value", plus "!cmd" shell commands):
11
+ * 1. Value in models.json — "!cmd" runs the command (cached per process),
12
+ * an env-var NAME resolves to its value, "$NAME" (legacy notation)
13
+ * resolves NAME and is never returned literally, anything else is the
14
+ * literal key
11
15
  * 2. process.env[envVar] (legacy fallback)
12
16
  *
17
+ * The file may contain // comments and trailing commas (same tolerance as
18
+ * model-registry). Note that setKey()/removeKey() rewrite the file as plain
19
+ * JSON, so comments do not survive a programmatic write.
20
+ *
13
21
  * Events emitted via the EventEmitter:
14
22
  * - "key_changed" { provider } : when setKey() or external edit detected
15
23
  * - "key_removed" { provider } : when removeKey() called
@@ -47,7 +55,15 @@ export declare class ApiKeyStore extends EventEmitter {
47
55
  reloadFromDisk(): void;
48
56
  /**
49
57
  * Get the API key for a provider, with env-var fallback.
50
- * Returns undefined if neither source has a value.
58
+ *
59
+ * The stored value follows the documented convention ("env var name or
60
+ * literal value", plus "!cmd"): it is resolved through the same logic as
61
+ * model-registry instead of being returned raw, so a config that works at
62
+ * request time also works here. Legacy "$NAME" values resolve NAME and
63
+ * fall through to the envVar fallback when unset (they are never returned
64
+ * literally — no real API key starts with "$").
65
+ *
66
+ * Returns undefined if no source yields a value.
51
67
  */
52
68
  getKey(providerId: string, envVar?: string): string | undefined;
53
69
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"api-key-store.d.ts","sourceRoot":"","sources":["../../src/core/api-key-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAK3C,MAAM,WAAW,uBAAuB;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,kBAAkB;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,WAAY,SAAQ,YAAY;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,MAAM,CAA4C;IAC1D,OAAO,CAAC,MAAM,CAAS;IAEvB,YAAY,OAAO,GAAE,kBAAuB,EAG3C;IAED;;OAEG;IACH,IAAI,IAAI,qBAAqB,CAgB5B;IAED;;OAEG;IACH,cAAc,IAAI,IAAI,CAGrB;IAED;;;OAGG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAM9D;IAED;;OAEG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CAGnE;IAED;;OAEG;IACH,aAAa,IAAI,MAAM,EAAE,CAGxB;IAED;;;;OAIG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,GAAG,IAAI,CAa/F;IAED;;;OAGG;IACH,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAQlC;IAED;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAM9C;IAED;;;;;;OAMG;IACH,OAAO,CAAC,OAAO;CAmBf;AAID,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,WAAW,CAKxE;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAExC","sourcesContent":["/**\n * API Key Store - Centralized storage and hot-reload for provider API keys.\n *\n * Per Q5 strategy C: explicit /keys command + file watcher both supported.\n * Per Q6 strategy A: keys stored in plain text in ~/.phi/agent/models.json\n * with chmod 0600 on Unix and clear warnings to users.\n *\n * Storage format: models.json providers.<id>.apiKey contains the key value\n * directly (not an env var reference). Resolution priority:\n * 1. Value in models.json (if non-empty)\n * 2. process.env[envVar] (legacy fallback)\n *\n * Events emitted via the EventEmitter:\n * - \"key_changed\" { provider } : when setKey() or external edit detected\n * - \"key_removed\" { provider } : when removeKey() called\n * - \"store_reloaded\" : when reloadFromDisk() succeeds\n */\n\nimport { EventEmitter } from \"node:events\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { getModelsPath } from \"../config.js\";\n\nexport interface ProviderConfigPersisted {\n\tbaseUrl?: string;\n\tapi?: string;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tauthHeader?: boolean;\n\tmodels?: unknown[];\n\tmodelOverrides?: Record<string, unknown>;\n}\n\nexport interface ModelsConfigPersisted {\n\t$comment?: string;\n\tproviders: Record<string, ProviderConfigPersisted>;\n}\n\nexport interface ApiKeyStoreOptions {\n\tconfigPath?: string;\n}\n\nexport class ApiKeyStore extends EventEmitter {\n\treadonly configPath: string;\n\tprivate config: ModelsConfigPersisted = { providers: {} };\n\tprivate loaded = false;\n\n\tconstructor(options: ApiKeyStoreOptions = {}) {\n\t\tsuper();\n\t\tthis.configPath = options.configPath ?? getModelsPath();\n\t}\n\n\t/**\n\t * Load config from disk. Creates an empty file if missing.\n\t */\n\tload(): ModelsConfigPersisted {\n\t\ttry {\n\t\t\tif (!existsSync(this.configPath)) {\n\t\t\t\treturn this.config;\n\t\t\t}\n\t\t\tconst raw = readFileSync(this.configPath, \"utf-8\");\n\t\t\tconst parsed = JSON.parse(raw) as ModelsConfigPersisted;\n\t\t\tif (!parsed.providers || typeof parsed.providers !== \"object\") {\n\t\t\t\tparsed.providers = {};\n\t\t\t}\n\t\t\tthis.config = parsed;\n\t\t\tthis.loaded = true;\n\t\t\treturn this.config;\n\t\t} catch (err) {\n\t\t\tthrow new Error(`Failed to load ${this.configPath}: ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Reload from disk and emit \"store_reloaded\".\n\t */\n\treloadFromDisk(): void {\n\t\tthis.load();\n\t\tthis.emit(\"store_reloaded\");\n\t}\n\n\t/**\n\t * Get the API key for a provider, with env-var fallback.\n\t * Returns undefined if neither source has a value.\n\t */\n\tgetKey(providerId: string, envVar?: string): string | undefined {\n\t\tif (!this.loaded) this.load();\n\t\tconst stored = this.config.providers[providerId]?.apiKey?.trim();\n\t\tif (stored && stored.length > 0 && !stored.startsWith(\"$\")) return stored;\n\t\tif (envVar) return process.env[envVar]?.trim() || undefined;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get the full provider config block for a provider.\n\t */\n\tgetProvider(providerId: string): ProviderConfigPersisted | undefined {\n\t\tif (!this.loaded) this.load();\n\t\treturn this.config.providers[providerId];\n\t}\n\n\t/**\n\t * List all configured provider IDs.\n\t */\n\tlistProviders(): string[] {\n\t\tif (!this.loaded) this.load();\n\t\treturn Object.keys(this.config.providers);\n\t}\n\n\t/**\n\t * Set the API key for a provider. Creates the provider entry if missing.\n\t * Performs atomic write (tmp + rename) and chmod 0600 on Unix.\n\t * Emits \"key_changed\" event.\n\t */\n\tsetKey(providerId: string, key: string, providerConfig?: Partial<ProviderConfigPersisted>): void {\n\t\tif (!this.loaded) this.load();\n\t\tconst existing = this.config.providers[providerId] ?? {};\n\t\tthis.config.providers[providerId] = {\n\t\t\t...existing,\n\t\t\t...providerConfig,\n\t\t\tapiKey: key,\n\t\t};\n\t\tthis.persist();\n\t\t// Do not include the raw key in the event payload to avoid leaking the\n\t\t// secret to listeners/logs. Listeners that need the value read it back\n\t\t// via getKey(providerId).\n\t\tthis.emit(\"key_changed\", { provider: providerId });\n\t}\n\n\t/**\n\t * Remove the API key for a provider (but keep the rest of the provider config).\n\t * Emits \"key_removed\" event.\n\t */\n\tremoveKey(providerId: string): void {\n\t\tif (!this.loaded) this.load();\n\t\tconst existing = this.config.providers[providerId];\n\t\tif (!existing) return;\n\t\tconst { apiKey: _removed, ...rest } = existing;\n\t\tthis.config.providers[providerId] = rest;\n\t\tthis.persist();\n\t\tthis.emit(\"key_removed\", { provider: providerId });\n\t}\n\n\t/**\n\t * Mask an API key for display (preserves length info without exposing the secret).\n\t */\n\tstatic maskKey(key: string | undefined): string {\n\t\tif (!key) return \"(not set)\";\n\t\tconst trimmed = key.trim();\n\t\tif (trimmed.length === 0) return \"(empty)\";\n\t\tif (trimmed.length <= 8) return \"********\";\n\t\treturn `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;\n\t}\n\n\t/**\n\t * Atomic write of the config to disk:\n\t * 1. Ensure parent dir exists\n\t * 2. Write to <path>.tmp\n\t * 3. Rename to <path> (atomic on POSIX)\n\t * 4. Apply chmod 0600 on Unix (silently skip on Windows)\n\t */\n\tprivate persist(): void {\n\t\tconst isPosix = process.platform !== \"win32\";\n\t\tconst parent = dirname(this.configPath);\n\t\tmkdirSync(parent, isPosix ? { recursive: true, mode: 0o700 } : { recursive: true });\n\t\tconst tmpPath = `${this.configPath}.tmp`;\n\t\tconst payload = `${JSON.stringify(this.config, null, 2)}\\n`;\n\t\t// Write the tmp file with restrictive perms from the start so the plaintext\n\t\t// key is never world-readable on disk (mode is still subject to umask, hence\n\t\t// the belt-and-suspenders chmod below after the rename).\n\t\twriteFileSync(tmpPath, payload, isPosix ? { encoding: \"utf-8\", mode: 0o600 } : \"utf-8\");\n\t\trenameSync(tmpPath, this.configPath);\n\t\tif (isPosix) {\n\t\t\ttry {\n\t\t\t\tchmodSync(this.configPath, 0o600);\n\t\t\t} catch {\n\t\t\t\t// chmod may fail on some filesystems (eg WSL); non-critical\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Module-level singleton for convenience. */\nlet _singleton: ApiKeyStore | null = null;\nexport function getApiKeyStore(options?: ApiKeyStoreOptions): ApiKeyStore {\n\tif (!_singleton || (options?.configPath && _singleton.configPath !== options.configPath)) {\n\t\t_singleton = new ApiKeyStore(options);\n\t}\n\treturn _singleton;\n}\n\nexport function _resetApiKeyStore(): void {\n\t_singleton = null;\n}\n"]}
1
+ {"version":3,"file":"api-key-store.d.ts","sourceRoot":"","sources":["../../src/core/api-key-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAO3C,MAAM,WAAW,uBAAuB;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,kBAAkB;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,WAAY,SAAQ,YAAY;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,MAAM,CAA4C;IAC1D,OAAO,CAAC,MAAM,CAAS;IAEvB,YAAY,OAAO,GAAE,kBAAuB,EAG3C;IAED;;OAEG;IACH,IAAI,IAAI,qBAAqB,CAkB5B;IAED;;OAEG;IACH,cAAc,IAAI,IAAI,CAGrB;IAED;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAc9D;IAED;;OAEG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CAGnE;IAED;;OAEG;IACH,aAAa,IAAI,MAAM,EAAE,CAGxB;IAED;;;;OAIG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,GAAG,IAAI,CAa/F;IAED;;;OAGG;IACH,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAQlC;IAED;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAM9C;IAED;;;;;;OAMG;IACH,OAAO,CAAC,OAAO;CAmBf;AAID,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,WAAW,CAKxE;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAExC","sourcesContent":["/**\n * API Key Store - Centralized storage and hot-reload for provider API keys.\n *\n * Per Q5 strategy C: explicit /keys command + file watcher both supported.\n * Per Q6 strategy A: keys stored in plain text in ~/.phi/agent/models.json\n * with chmod 0600 on Unix and clear warnings to users.\n *\n * Storage format: models.json providers.<id>.apiKey. Resolution follows the\n * same convention as model-registry/resolve-config-value (\"env var name or\n * literal value\", plus \"!cmd\" shell commands):\n * 1. Value in models.json — \"!cmd\" runs the command (cached per process),\n * an env-var NAME resolves to its value, \"$NAME\" (legacy notation)\n * resolves NAME and is never returned literally, anything else is the\n * literal key\n * 2. process.env[envVar] (legacy fallback)\n *\n * The file may contain // comments and trailing commas (same tolerance as\n * model-registry). Note that setKey()/removeKey() rewrite the file as plain\n * JSON, so comments do not survive a programmatic write.\n *\n * Events emitted via the EventEmitter:\n * - \"key_changed\" { provider } : when setKey() or external edit detected\n * - \"key_removed\" { provider } : when removeKey() called\n * - \"store_reloaded\" : when reloadFromDisk() succeeds\n */\n\nimport { EventEmitter } from \"node:events\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { getModelsPath } from \"../config.js\";\nimport { stripJsonComments } from \"./json-utils.js\";\nimport { resolveConfigValue } from \"./resolve-config-value.js\";\n\nexport interface ProviderConfigPersisted {\n\tbaseUrl?: string;\n\tapi?: string;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tauthHeader?: boolean;\n\tmodels?: unknown[];\n\tmodelOverrides?: Record<string, unknown>;\n}\n\nexport interface ModelsConfigPersisted {\n\t$comment?: string;\n\tproviders: Record<string, ProviderConfigPersisted>;\n}\n\nexport interface ApiKeyStoreOptions {\n\tconfigPath?: string;\n}\n\nexport class ApiKeyStore extends EventEmitter {\n\treadonly configPath: string;\n\tprivate config: ModelsConfigPersisted = { providers: {} };\n\tprivate loaded = false;\n\n\tconstructor(options: ApiKeyStoreOptions = {}) {\n\t\tsuper();\n\t\tthis.configPath = options.configPath ?? getModelsPath();\n\t}\n\n\t/**\n\t * Load config from disk. Creates an empty file if missing.\n\t */\n\tload(): ModelsConfigPersisted {\n\t\ttry {\n\t\t\tif (!existsSync(this.configPath)) {\n\t\t\t\treturn this.config;\n\t\t\t}\n\t\t\tconst raw = readFileSync(this.configPath, \"utf-8\");\n\t\t\t// Same comment/trailing-comma tolerance as model-registry: a commented\n\t\t\t// models.json must not break /keys hot-reload while the agent still runs.\n\t\t\tconst parsed = JSON.parse(stripJsonComments(raw)) as ModelsConfigPersisted;\n\t\t\tif (!parsed.providers || typeof parsed.providers !== \"object\") {\n\t\t\t\tparsed.providers = {};\n\t\t\t}\n\t\t\tthis.config = parsed;\n\t\t\tthis.loaded = true;\n\t\t\treturn this.config;\n\t\t} catch (err) {\n\t\t\tthrow new Error(`Failed to load ${this.configPath}: ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Reload from disk and emit \"store_reloaded\".\n\t */\n\treloadFromDisk(): void {\n\t\tthis.load();\n\t\tthis.emit(\"store_reloaded\");\n\t}\n\n\t/**\n\t * Get the API key for a provider, with env-var fallback.\n\t *\n\t * The stored value follows the documented convention (\"env var name or\n\t * literal value\", plus \"!cmd\"): it is resolved through the same logic as\n\t * model-registry instead of being returned raw, so a config that works at\n\t * request time also works here. Legacy \"$NAME\" values resolve NAME and\n\t * fall through to the envVar fallback when unset (they are never returned\n\t * literally — no real API key starts with \"$\").\n\t *\n\t * Returns undefined if no source yields a value.\n\t */\n\tgetKey(providerId: string, envVar?: string): string | undefined {\n\t\tif (!this.loaded) this.load();\n\t\tconst stored = this.config.providers[providerId]?.apiKey?.trim();\n\t\tif (stored && stored.length > 0) {\n\t\t\tif (stored.startsWith(\"$\")) {\n\t\t\t\tconst fromEnv = process.env[stored.slice(1)]?.trim();\n\t\t\t\tif (fromEnv) return fromEnv;\n\t\t\t} else {\n\t\t\t\tconst resolved = resolveConfigValue(stored)?.trim();\n\t\t\t\tif (resolved) return resolved;\n\t\t\t}\n\t\t}\n\t\tif (envVar) return process.env[envVar]?.trim() || undefined;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get the full provider config block for a provider.\n\t */\n\tgetProvider(providerId: string): ProviderConfigPersisted | undefined {\n\t\tif (!this.loaded) this.load();\n\t\treturn this.config.providers[providerId];\n\t}\n\n\t/**\n\t * List all configured provider IDs.\n\t */\n\tlistProviders(): string[] {\n\t\tif (!this.loaded) this.load();\n\t\treturn Object.keys(this.config.providers);\n\t}\n\n\t/**\n\t * Set the API key for a provider. Creates the provider entry if missing.\n\t * Performs atomic write (tmp + rename) and chmod 0600 on Unix.\n\t * Emits \"key_changed\" event.\n\t */\n\tsetKey(providerId: string, key: string, providerConfig?: Partial<ProviderConfigPersisted>): void {\n\t\tif (!this.loaded) this.load();\n\t\tconst existing = this.config.providers[providerId] ?? {};\n\t\tthis.config.providers[providerId] = {\n\t\t\t...existing,\n\t\t\t...providerConfig,\n\t\t\tapiKey: key,\n\t\t};\n\t\tthis.persist();\n\t\t// Do not include the raw key in the event payload to avoid leaking the\n\t\t// secret to listeners/logs. Listeners that need the value read it back\n\t\t// via getKey(providerId).\n\t\tthis.emit(\"key_changed\", { provider: providerId });\n\t}\n\n\t/**\n\t * Remove the API key for a provider (but keep the rest of the provider config).\n\t * Emits \"key_removed\" event.\n\t */\n\tremoveKey(providerId: string): void {\n\t\tif (!this.loaded) this.load();\n\t\tconst existing = this.config.providers[providerId];\n\t\tif (!existing) return;\n\t\tconst { apiKey: _removed, ...rest } = existing;\n\t\tthis.config.providers[providerId] = rest;\n\t\tthis.persist();\n\t\tthis.emit(\"key_removed\", { provider: providerId });\n\t}\n\n\t/**\n\t * Mask an API key for display (preserves length info without exposing the secret).\n\t */\n\tstatic maskKey(key: string | undefined): string {\n\t\tif (!key) return \"(not set)\";\n\t\tconst trimmed = key.trim();\n\t\tif (trimmed.length === 0) return \"(empty)\";\n\t\tif (trimmed.length <= 8) return \"********\";\n\t\treturn `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;\n\t}\n\n\t/**\n\t * Atomic write of the config to disk:\n\t * 1. Ensure parent dir exists\n\t * 2. Write to <path>.tmp\n\t * 3. Rename to <path> (atomic on POSIX)\n\t * 4. Apply chmod 0600 on Unix (silently skip on Windows)\n\t */\n\tprivate persist(): void {\n\t\tconst isPosix = process.platform !== \"win32\";\n\t\tconst parent = dirname(this.configPath);\n\t\tmkdirSync(parent, isPosix ? { recursive: true, mode: 0o700 } : { recursive: true });\n\t\tconst tmpPath = `${this.configPath}.tmp`;\n\t\tconst payload = `${JSON.stringify(this.config, null, 2)}\\n`;\n\t\t// Write the tmp file with restrictive perms from the start so the plaintext\n\t\t// key is never world-readable on disk (mode is still subject to umask, hence\n\t\t// the belt-and-suspenders chmod below after the rename).\n\t\twriteFileSync(tmpPath, payload, isPosix ? { encoding: \"utf-8\", mode: 0o600 } : \"utf-8\");\n\t\trenameSync(tmpPath, this.configPath);\n\t\tif (isPosix) {\n\t\t\ttry {\n\t\t\t\tchmodSync(this.configPath, 0o600);\n\t\t\t} catch {\n\t\t\t\t// chmod may fail on some filesystems (eg WSL); non-critical\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Module-level singleton for convenience. */\nlet _singleton: ApiKeyStore | null = null;\nexport function getApiKeyStore(options?: ApiKeyStoreOptions): ApiKeyStore {\n\tif (!_singleton || (options?.configPath && _singleton.configPath !== options.configPath)) {\n\t\t_singleton = new ApiKeyStore(options);\n\t}\n\treturn _singleton;\n}\n\nexport function _resetApiKeyStore(): void {\n\t_singleton = null;\n}\n"]}
@@ -5,11 +5,19 @@
5
5
  * Per Q6 strategy A: keys stored in plain text in ~/.phi/agent/models.json
6
6
  * with chmod 0600 on Unix and clear warnings to users.
7
7
  *
8
- * Storage format: models.json providers.<id>.apiKey contains the key value
9
- * directly (not an env var reference). Resolution priority:
10
- * 1. Value in models.json (if non-empty)
8
+ * Storage format: models.json providers.<id>.apiKey. Resolution follows the
9
+ * same convention as model-registry/resolve-config-value ("env var name or
10
+ * literal value", plus "!cmd" shell commands):
11
+ * 1. Value in models.json — "!cmd" runs the command (cached per process),
12
+ * an env-var NAME resolves to its value, "$NAME" (legacy notation)
13
+ * resolves NAME and is never returned literally, anything else is the
14
+ * literal key
11
15
  * 2. process.env[envVar] (legacy fallback)
12
16
  *
17
+ * The file may contain // comments and trailing commas (same tolerance as
18
+ * model-registry). Note that setKey()/removeKey() rewrite the file as plain
19
+ * JSON, so comments do not survive a programmatic write.
20
+ *
13
21
  * Events emitted via the EventEmitter:
14
22
  * - "key_changed" { provider } : when setKey() or external edit detected
15
23
  * - "key_removed" { provider } : when removeKey() called
@@ -19,6 +27,8 @@ import { EventEmitter } from "node:events";
19
27
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
20
28
  import { dirname } from "node:path";
21
29
  import { getModelsPath } from "../config.js";
30
+ import { stripJsonComments } from "./json-utils.js";
31
+ import { resolveConfigValue } from "./resolve-config-value.js";
22
32
  export class ApiKeyStore extends EventEmitter {
23
33
  configPath;
24
34
  config = { providers: {} };
@@ -36,7 +46,9 @@ export class ApiKeyStore extends EventEmitter {
36
46
  return this.config;
37
47
  }
38
48
  const raw = readFileSync(this.configPath, "utf-8");
39
- const parsed = JSON.parse(raw);
49
+ // Same comment/trailing-comma tolerance as model-registry: a commented
50
+ // models.json must not break /keys hot-reload while the agent still runs.
51
+ const parsed = JSON.parse(stripJsonComments(raw));
40
52
  if (!parsed.providers || typeof parsed.providers !== "object") {
41
53
  parsed.providers = {};
42
54
  }
@@ -57,14 +69,32 @@ export class ApiKeyStore extends EventEmitter {
57
69
  }
58
70
  /**
59
71
  * Get the API key for a provider, with env-var fallback.
60
- * Returns undefined if neither source has a value.
72
+ *
73
+ * The stored value follows the documented convention ("env var name or
74
+ * literal value", plus "!cmd"): it is resolved through the same logic as
75
+ * model-registry instead of being returned raw, so a config that works at
76
+ * request time also works here. Legacy "$NAME" values resolve NAME and
77
+ * fall through to the envVar fallback when unset (they are never returned
78
+ * literally — no real API key starts with "$").
79
+ *
80
+ * Returns undefined if no source yields a value.
61
81
  */
62
82
  getKey(providerId, envVar) {
63
83
  if (!this.loaded)
64
84
  this.load();
65
85
  const stored = this.config.providers[providerId]?.apiKey?.trim();
66
- if (stored && stored.length > 0 && !stored.startsWith("$"))
67
- return stored;
86
+ if (stored && stored.length > 0) {
87
+ if (stored.startsWith("$")) {
88
+ const fromEnv = process.env[stored.slice(1)]?.trim();
89
+ if (fromEnv)
90
+ return fromEnv;
91
+ }
92
+ else {
93
+ const resolved = resolveConfigValue(stored)?.trim();
94
+ if (resolved)
95
+ return resolved;
96
+ }
97
+ }
68
98
  if (envVar)
69
99
  return process.env[envVar]?.trim() || undefined;
70
100
  return undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"api-key-store.js","sourceRoot":"","sources":["../../src/core/api-key-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Key Store - Centralized storage and hot-reload for provider API keys.\n *\n * Per Q5 strategy C: explicit /keys command + file watcher both supported.\n * Per Q6 strategy A: keys stored in plain text in ~/.phi/agent/models.json\n * with chmod 0600 on Unix and clear warnings to users.\n *\n * Storage format: models.json providers.<id>.apiKey contains the key value\n * directly (not an env var reference). Resolution priority:\n * 1. Value in models.json (if non-empty)\n * 2. process.env[envVar] (legacy fallback)\n *\n * Events emitted via the EventEmitter:\n * - \"key_changed\" { provider } : when setKey() or external edit detected\n * - \"key_removed\" { provider } : when removeKey() called\n * - \"store_reloaded\" : when reloadFromDisk() succeeds\n */\n\nimport { EventEmitter } from \"node:events\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { getModelsPath } from \"../config.js\";\n\nexport interface ProviderConfigPersisted {\n\tbaseUrl?: string;\n\tapi?: string;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tauthHeader?: boolean;\n\tmodels?: unknown[];\n\tmodelOverrides?: Record<string, unknown>;\n}\n\nexport interface ModelsConfigPersisted {\n\t$comment?: string;\n\tproviders: Record<string, ProviderConfigPersisted>;\n}\n\nexport interface ApiKeyStoreOptions {\n\tconfigPath?: string;\n}\n\nexport class ApiKeyStore extends EventEmitter {\n\treadonly configPath: string;\n\tprivate config: ModelsConfigPersisted = { providers: {} };\n\tprivate loaded = false;\n\n\tconstructor(options: ApiKeyStoreOptions = {}) {\n\t\tsuper();\n\t\tthis.configPath = options.configPath ?? getModelsPath();\n\t}\n\n\t/**\n\t * Load config from disk. Creates an empty file if missing.\n\t */\n\tload(): ModelsConfigPersisted {\n\t\ttry {\n\t\t\tif (!existsSync(this.configPath)) {\n\t\t\t\treturn this.config;\n\t\t\t}\n\t\t\tconst raw = readFileSync(this.configPath, \"utf-8\");\n\t\t\tconst parsed = JSON.parse(raw) as ModelsConfigPersisted;\n\t\t\tif (!parsed.providers || typeof parsed.providers !== \"object\") {\n\t\t\t\tparsed.providers = {};\n\t\t\t}\n\t\t\tthis.config = parsed;\n\t\t\tthis.loaded = true;\n\t\t\treturn this.config;\n\t\t} catch (err) {\n\t\t\tthrow new Error(`Failed to load ${this.configPath}: ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Reload from disk and emit \"store_reloaded\".\n\t */\n\treloadFromDisk(): void {\n\t\tthis.load();\n\t\tthis.emit(\"store_reloaded\");\n\t}\n\n\t/**\n\t * Get the API key for a provider, with env-var fallback.\n\t * Returns undefined if neither source has a value.\n\t */\n\tgetKey(providerId: string, envVar?: string): string | undefined {\n\t\tif (!this.loaded) this.load();\n\t\tconst stored = this.config.providers[providerId]?.apiKey?.trim();\n\t\tif (stored && stored.length > 0 && !stored.startsWith(\"$\")) return stored;\n\t\tif (envVar) return process.env[envVar]?.trim() || undefined;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get the full provider config block for a provider.\n\t */\n\tgetProvider(providerId: string): ProviderConfigPersisted | undefined {\n\t\tif (!this.loaded) this.load();\n\t\treturn this.config.providers[providerId];\n\t}\n\n\t/**\n\t * List all configured provider IDs.\n\t */\n\tlistProviders(): string[] {\n\t\tif (!this.loaded) this.load();\n\t\treturn Object.keys(this.config.providers);\n\t}\n\n\t/**\n\t * Set the API key for a provider. Creates the provider entry if missing.\n\t * Performs atomic write (tmp + rename) and chmod 0600 on Unix.\n\t * Emits \"key_changed\" event.\n\t */\n\tsetKey(providerId: string, key: string, providerConfig?: Partial<ProviderConfigPersisted>): void {\n\t\tif (!this.loaded) this.load();\n\t\tconst existing = this.config.providers[providerId] ?? {};\n\t\tthis.config.providers[providerId] = {\n\t\t\t...existing,\n\t\t\t...providerConfig,\n\t\t\tapiKey: key,\n\t\t};\n\t\tthis.persist();\n\t\t// Do not include the raw key in the event payload to avoid leaking the\n\t\t// secret to listeners/logs. Listeners that need the value read it back\n\t\t// via getKey(providerId).\n\t\tthis.emit(\"key_changed\", { provider: providerId });\n\t}\n\n\t/**\n\t * Remove the API key for a provider (but keep the rest of the provider config).\n\t * Emits \"key_removed\" event.\n\t */\n\tremoveKey(providerId: string): void {\n\t\tif (!this.loaded) this.load();\n\t\tconst existing = this.config.providers[providerId];\n\t\tif (!existing) return;\n\t\tconst { apiKey: _removed, ...rest } = existing;\n\t\tthis.config.providers[providerId] = rest;\n\t\tthis.persist();\n\t\tthis.emit(\"key_removed\", { provider: providerId });\n\t}\n\n\t/**\n\t * Mask an API key for display (preserves length info without exposing the secret).\n\t */\n\tstatic maskKey(key: string | undefined): string {\n\t\tif (!key) return \"(not set)\";\n\t\tconst trimmed = key.trim();\n\t\tif (trimmed.length === 0) return \"(empty)\";\n\t\tif (trimmed.length <= 8) return \"********\";\n\t\treturn `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;\n\t}\n\n\t/**\n\t * Atomic write of the config to disk:\n\t * 1. Ensure parent dir exists\n\t * 2. Write to <path>.tmp\n\t * 3. Rename to <path> (atomic on POSIX)\n\t * 4. Apply chmod 0600 on Unix (silently skip on Windows)\n\t */\n\tprivate persist(): void {\n\t\tconst isPosix = process.platform !== \"win32\";\n\t\tconst parent = dirname(this.configPath);\n\t\tmkdirSync(parent, isPosix ? { recursive: true, mode: 0o700 } : { recursive: true });\n\t\tconst tmpPath = `${this.configPath}.tmp`;\n\t\tconst payload = `${JSON.stringify(this.config, null, 2)}\\n`;\n\t\t// Write the tmp file with restrictive perms from the start so the plaintext\n\t\t// key is never world-readable on disk (mode is still subject to umask, hence\n\t\t// the belt-and-suspenders chmod below after the rename).\n\t\twriteFileSync(tmpPath, payload, isPosix ? { encoding: \"utf-8\", mode: 0o600 } : \"utf-8\");\n\t\trenameSync(tmpPath, this.configPath);\n\t\tif (isPosix) {\n\t\t\ttry {\n\t\t\t\tchmodSync(this.configPath, 0o600);\n\t\t\t} catch {\n\t\t\t\t// chmod may fail on some filesystems (eg WSL); non-critical\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Module-level singleton for convenience. */\nlet _singleton: ApiKeyStore | null = null;\nexport function getApiKeyStore(options?: ApiKeyStoreOptions): ApiKeyStore {\n\tif (!_singleton || (options?.configPath && _singleton.configPath !== options.configPath)) {\n\t\t_singleton = new ApiKeyStore(options);\n\t}\n\treturn _singleton;\n}\n\nexport function _resetApiKeyStore(): void {\n\t_singleton = null;\n}\n"]}
1
+ {"version":3,"file":"api-key-store.js","sourceRoot":"","sources":["../../src/core/api-key-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Key Store - Centralized storage and hot-reload for provider API keys.\n *\n * Per Q5 strategy C: explicit /keys command + file watcher both supported.\n * Per Q6 strategy A: keys stored in plain text in ~/.phi/agent/models.json\n * with chmod 0600 on Unix and clear warnings to users.\n *\n * Storage format: models.json providers.<id>.apiKey. Resolution follows the\n * same convention as model-registry/resolve-config-value (\"env var name or\n * literal value\", plus \"!cmd\" shell commands):\n * 1. Value in models.json — \"!cmd\" runs the command (cached per process),\n * an env-var NAME resolves to its value, \"$NAME\" (legacy notation)\n * resolves NAME and is never returned literally, anything else is the\n * literal key\n * 2. process.env[envVar] (legacy fallback)\n *\n * The file may contain // comments and trailing commas (same tolerance as\n * model-registry). Note that setKey()/removeKey() rewrite the file as plain\n * JSON, so comments do not survive a programmatic write.\n *\n * Events emitted via the EventEmitter:\n * - \"key_changed\" { provider } : when setKey() or external edit detected\n * - \"key_removed\" { provider } : when removeKey() called\n * - \"store_reloaded\" : when reloadFromDisk() succeeds\n */\n\nimport { EventEmitter } from \"node:events\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { getModelsPath } from \"../config.js\";\nimport { stripJsonComments } from \"./json-utils.js\";\nimport { resolveConfigValue } from \"./resolve-config-value.js\";\n\nexport interface ProviderConfigPersisted {\n\tbaseUrl?: string;\n\tapi?: string;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tauthHeader?: boolean;\n\tmodels?: unknown[];\n\tmodelOverrides?: Record<string, unknown>;\n}\n\nexport interface ModelsConfigPersisted {\n\t$comment?: string;\n\tproviders: Record<string, ProviderConfigPersisted>;\n}\n\nexport interface ApiKeyStoreOptions {\n\tconfigPath?: string;\n}\n\nexport class ApiKeyStore extends EventEmitter {\n\treadonly configPath: string;\n\tprivate config: ModelsConfigPersisted = { providers: {} };\n\tprivate loaded = false;\n\n\tconstructor(options: ApiKeyStoreOptions = {}) {\n\t\tsuper();\n\t\tthis.configPath = options.configPath ?? getModelsPath();\n\t}\n\n\t/**\n\t * Load config from disk. Creates an empty file if missing.\n\t */\n\tload(): ModelsConfigPersisted {\n\t\ttry {\n\t\t\tif (!existsSync(this.configPath)) {\n\t\t\t\treturn this.config;\n\t\t\t}\n\t\t\tconst raw = readFileSync(this.configPath, \"utf-8\");\n\t\t\t// Same comment/trailing-comma tolerance as model-registry: a commented\n\t\t\t// models.json must not break /keys hot-reload while the agent still runs.\n\t\t\tconst parsed = JSON.parse(stripJsonComments(raw)) as ModelsConfigPersisted;\n\t\t\tif (!parsed.providers || typeof parsed.providers !== \"object\") {\n\t\t\t\tparsed.providers = {};\n\t\t\t}\n\t\t\tthis.config = parsed;\n\t\t\tthis.loaded = true;\n\t\t\treturn this.config;\n\t\t} catch (err) {\n\t\t\tthrow new Error(`Failed to load ${this.configPath}: ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Reload from disk and emit \"store_reloaded\".\n\t */\n\treloadFromDisk(): void {\n\t\tthis.load();\n\t\tthis.emit(\"store_reloaded\");\n\t}\n\n\t/**\n\t * Get the API key for a provider, with env-var fallback.\n\t *\n\t * The stored value follows the documented convention (\"env var name or\n\t * literal value\", plus \"!cmd\"): it is resolved through the same logic as\n\t * model-registry instead of being returned raw, so a config that works at\n\t * request time also works here. Legacy \"$NAME\" values resolve NAME and\n\t * fall through to the envVar fallback when unset (they are never returned\n\t * literally — no real API key starts with \"$\").\n\t *\n\t * Returns undefined if no source yields a value.\n\t */\n\tgetKey(providerId: string, envVar?: string): string | undefined {\n\t\tif (!this.loaded) this.load();\n\t\tconst stored = this.config.providers[providerId]?.apiKey?.trim();\n\t\tif (stored && stored.length > 0) {\n\t\t\tif (stored.startsWith(\"$\")) {\n\t\t\t\tconst fromEnv = process.env[stored.slice(1)]?.trim();\n\t\t\t\tif (fromEnv) return fromEnv;\n\t\t\t} else {\n\t\t\t\tconst resolved = resolveConfigValue(stored)?.trim();\n\t\t\t\tif (resolved) return resolved;\n\t\t\t}\n\t\t}\n\t\tif (envVar) return process.env[envVar]?.trim() || undefined;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get the full provider config block for a provider.\n\t */\n\tgetProvider(providerId: string): ProviderConfigPersisted | undefined {\n\t\tif (!this.loaded) this.load();\n\t\treturn this.config.providers[providerId];\n\t}\n\n\t/**\n\t * List all configured provider IDs.\n\t */\n\tlistProviders(): string[] {\n\t\tif (!this.loaded) this.load();\n\t\treturn Object.keys(this.config.providers);\n\t}\n\n\t/**\n\t * Set the API key for a provider. Creates the provider entry if missing.\n\t * Performs atomic write (tmp + rename) and chmod 0600 on Unix.\n\t * Emits \"key_changed\" event.\n\t */\n\tsetKey(providerId: string, key: string, providerConfig?: Partial<ProviderConfigPersisted>): void {\n\t\tif (!this.loaded) this.load();\n\t\tconst existing = this.config.providers[providerId] ?? {};\n\t\tthis.config.providers[providerId] = {\n\t\t\t...existing,\n\t\t\t...providerConfig,\n\t\t\tapiKey: key,\n\t\t};\n\t\tthis.persist();\n\t\t// Do not include the raw key in the event payload to avoid leaking the\n\t\t// secret to listeners/logs. Listeners that need the value read it back\n\t\t// via getKey(providerId).\n\t\tthis.emit(\"key_changed\", { provider: providerId });\n\t}\n\n\t/**\n\t * Remove the API key for a provider (but keep the rest of the provider config).\n\t * Emits \"key_removed\" event.\n\t */\n\tremoveKey(providerId: string): void {\n\t\tif (!this.loaded) this.load();\n\t\tconst existing = this.config.providers[providerId];\n\t\tif (!existing) return;\n\t\tconst { apiKey: _removed, ...rest } = existing;\n\t\tthis.config.providers[providerId] = rest;\n\t\tthis.persist();\n\t\tthis.emit(\"key_removed\", { provider: providerId });\n\t}\n\n\t/**\n\t * Mask an API key for display (preserves length info without exposing the secret).\n\t */\n\tstatic maskKey(key: string | undefined): string {\n\t\tif (!key) return \"(not set)\";\n\t\tconst trimmed = key.trim();\n\t\tif (trimmed.length === 0) return \"(empty)\";\n\t\tif (trimmed.length <= 8) return \"********\";\n\t\treturn `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;\n\t}\n\n\t/**\n\t * Atomic write of the config to disk:\n\t * 1. Ensure parent dir exists\n\t * 2. Write to <path>.tmp\n\t * 3. Rename to <path> (atomic on POSIX)\n\t * 4. Apply chmod 0600 on Unix (silently skip on Windows)\n\t */\n\tprivate persist(): void {\n\t\tconst isPosix = process.platform !== \"win32\";\n\t\tconst parent = dirname(this.configPath);\n\t\tmkdirSync(parent, isPosix ? { recursive: true, mode: 0o700 } : { recursive: true });\n\t\tconst tmpPath = `${this.configPath}.tmp`;\n\t\tconst payload = `${JSON.stringify(this.config, null, 2)}\\n`;\n\t\t// Write the tmp file with restrictive perms from the start so the plaintext\n\t\t// key is never world-readable on disk (mode is still subject to umask, hence\n\t\t// the belt-and-suspenders chmod below after the rename).\n\t\twriteFileSync(tmpPath, payload, isPosix ? { encoding: \"utf-8\", mode: 0o600 } : \"utf-8\");\n\t\trenameSync(tmpPath, this.configPath);\n\t\tif (isPosix) {\n\t\t\ttry {\n\t\t\t\tchmodSync(this.configPath, 0o600);\n\t\t\t} catch {\n\t\t\t\t// chmod may fail on some filesystems (eg WSL); non-critical\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Module-level singleton for convenience. */\nlet _singleton: ApiKeyStore | null = null;\nexport function getApiKeyStore(options?: ApiKeyStoreOptions): ApiKeyStore {\n\tif (!_singleton || (options?.configPath && _singleton.configPath !== options.configPath)) {\n\t\t_singleton = new ApiKeyStore(options);\n\t}\n\treturn _singleton;\n}\n\nexport function _resetApiKeyStore(): void {\n\t_singleton = null;\n}\n"]}