@oh-my-pi/pi-coding-agent 15.13.0 → 15.13.1

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 (83) hide show
  1. package/CHANGELOG.md +1656 -613
  2. package/dist/cli.js +12765 -12731
  3. package/dist/types/autolearn/managed-skills.d.ts +1 -1
  4. package/dist/types/capability/mcp.d.ts +2 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/flag-tables.d.ts +126 -0
  7. package/dist/types/cli/profile-alias.d.ts +29 -0
  8. package/dist/types/cli/profile-bootstrap.d.ts +55 -0
  9. package/dist/types/commands/launch.d.ts +6 -0
  10. package/dist/types/config/model-roles.d.ts +3 -2
  11. package/dist/types/config/settings-schema.d.ts +2 -0
  12. package/dist/types/edit/file-snapshot-store.d.ts +14 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +11 -0
  14. package/dist/types/mcp/manager.d.ts +5 -1
  15. package/dist/types/mcp/oauth-credentials.d.ts +17 -0
  16. package/dist/types/mcp/oauth-flow.d.ts +41 -0
  17. package/dist/types/mcp/types.d.ts +2 -0
  18. package/dist/types/modes/components/background-tan-message.d.ts +9 -0
  19. package/dist/types/modes/components/mcp-add-wizard.d.ts +9 -5
  20. package/dist/types/modes/interactive-mode.d.ts +4 -0
  21. package/dist/types/modes/types.d.ts +3 -0
  22. package/dist/types/sdk.d.ts +1 -1
  23. package/dist/types/session/messages.d.ts +8 -0
  24. package/dist/types/session/session-manager.d.ts +6 -0
  25. package/dist/types/tools/builtin-names.d.ts +2 -0
  26. package/dist/types/tools/index.d.ts +3 -2
  27. package/dist/types/utils/external-editor.d.ts +11 -1
  28. package/package.json +12 -12
  29. package/src/autolearn/managed-skills.ts +3 -5
  30. package/src/capability/mcp.ts +2 -1
  31. package/src/cli/args.ts +61 -103
  32. package/src/cli/completion-gen.ts +2 -2
  33. package/src/cli/flag-tables.ts +270 -0
  34. package/src/cli/profile-alias.ts +338 -0
  35. package/src/cli/profile-bootstrap.ts +243 -0
  36. package/src/cli.ts +83 -16
  37. package/src/commands/launch.ts +7 -0
  38. package/src/config/mcp-schema.json +4 -0
  39. package/src/config/model-roles.ts +17 -4
  40. package/src/config/settings-schema.ts +2 -0
  41. package/src/discovery/builtin.ts +15 -9
  42. package/src/discovery/helpers.ts +25 -0
  43. package/src/discovery/mcp-json.ts +1 -0
  44. package/src/discovery/omp-extension-roots.ts +2 -2
  45. package/src/edit/file-snapshot-store.ts +43 -0
  46. package/src/eval/__tests__/agent-bridge.test.ts +3 -2
  47. package/src/eval/__tests__/helpers-local-roots.test.ts +1 -1
  48. package/src/eval/js/shared/runtime.ts +54 -0
  49. package/src/extensibility/extensions/runner.ts +25 -2
  50. package/src/goals/runtime.ts +4 -1
  51. package/src/internal-urls/docs-index.generated.ts +6 -6
  52. package/src/mcp/manager.ts +108 -71
  53. package/src/mcp/oauth-credentials.ts +104 -0
  54. package/src/mcp/oauth-flow.ts +67 -0
  55. package/src/mcp/types.ts +2 -0
  56. package/src/modes/components/agent-hub.ts +6 -0
  57. package/src/modes/components/background-tan-message.ts +36 -0
  58. package/src/modes/components/mcp-add-wizard.ts +17 -10
  59. package/src/modes/components/model-selector.ts +50 -6
  60. package/src/modes/components/tool-execution.ts +12 -0
  61. package/src/modes/controllers/input-controller.ts +21 -10
  62. package/src/modes/controllers/mcp-command-controller.ts +184 -112
  63. package/src/modes/controllers/tan-command-controller.ts +27 -11
  64. package/src/modes/interactive-mode.ts +6 -0
  65. package/src/modes/types.ts +3 -0
  66. package/src/modes/utils/ui-helpers.ts +6 -0
  67. package/src/prompts/bench.md +9 -4
  68. package/src/sdk.ts +6 -5
  69. package/src/session/agent-session.ts +30 -1
  70. package/src/session/messages.ts +9 -0
  71. package/src/session/session-manager.ts +7 -2
  72. package/src/tiny/text.ts +5 -1
  73. package/src/tools/ast-grep.ts +5 -1
  74. package/src/tools/builtin-names.ts +35 -0
  75. package/src/tools/index.ts +3 -2
  76. package/src/tools/read.ts +9 -0
  77. package/src/tools/search.ts +5 -1
  78. package/src/tts/tts-worker.ts +13 -5
  79. package/src/utils/external-editor.ts +15 -2
  80. package/src/utils/title-generator.ts +1 -1
  81. package/src/workspace-tree.ts +46 -6
  82. package/dist/types/utils/tools-manager.test.d.ts +0 -1
  83. package/src/utils/tools-manager.test.ts +0 -25
@@ -9,11 +9,9 @@
9
9
  */
10
10
  import { constants as fsConstants, type Stats } from "node:fs";
11
11
  import * as fs from "node:fs/promises";
12
- import * as os from "node:os";
13
12
  import * as path from "node:path";
14
- import { isEnoent } from "@oh-my-pi/pi-utils";
13
+ import { getAgentDir, isEnoent } from "@oh-my-pi/pi-utils";
15
14
  import { YAML } from "bun";
16
- import { SOURCE_PATHS } from "../discovery/helpers";
17
15
 
18
16
  /** Provider id stamped on discovered managed skills (distinguishes them from authored). */
19
17
  export const MANAGED_SKILLS_PROVIDER_ID = "omp-managed";
@@ -24,8 +22,8 @@ export const MAX_MANAGED_SKILL_BYTES = 64_000;
24
22
  const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
25
23
 
26
24
  /** Resolve the isolated managed-skills directory (`~/.omp/agent/managed-skills`). */
27
- export function getManagedSkillsDir(home: string = os.homedir()): string {
28
- return path.join(home, SOURCE_PATHS.native.userAgent, "managed-skills");
25
+ export function getManagedSkillsDir(agentDir: string = getAgentDir()): string {
26
+ return path.join(agentDir, "managed-skills");
29
27
  }
30
28
 
31
29
  /**
@@ -38,13 +38,14 @@ export interface MCPServer {
38
38
  clientSecret?: string;
39
39
  resource?: string;
40
40
  };
41
- /** OAuth configuration (clientId, clientSecret, redirectUri, callbackPort, callbackPath) for servers requiring explicit client credentials */
41
+ /** OAuth configuration (clientId, clientSecret, redirectUri, callbackPort, callbackPath, prompt) for servers requiring explicit client credentials */
42
42
  oauth?: {
43
43
  clientId?: string;
44
44
  clientSecret?: string;
45
45
  redirectUri?: string;
46
46
  callbackPort?: number;
47
47
  callbackPath?: string;
48
+ prompt?: string;
48
49
  };
49
50
  /** Transport type */
50
51
  transport?: "stdio" | "sse" | "http";
package/src/cli/args.ts CHANGED
@@ -5,12 +5,22 @@ import { type Effort, THINKING_EFFORTS } from "@oh-my-pi/pi-catalog/effort";
5
5
  import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
6
6
  import chalk from "chalk";
7
7
  import { parseEffort } from "../thinking";
8
- import { BUILTIN_TOOLS } from "../tools";
8
+ import { BUILTIN_TOOL_NAMES } from "../tools/builtin-names";
9
+ import {
10
+ OPTIONAL_FLAGS,
11
+ OPTIONAL_VALUE_FLAGS,
12
+ type ParseDeps,
13
+ PROFILE_BOOTSTRAP_BOUNDARY_ARG,
14
+ STRING_SETTERS,
15
+ STRING_VALUE_FLAGS,
16
+ } from "./flag-tables";
9
17
 
10
18
  export type Mode = "text" | "json" | "rpc" | "acp" | "rpc-ui";
11
19
 
12
20
  export interface Args {
13
21
  cwd?: string;
22
+ profile?: string;
23
+ alias?: string;
14
24
  allowHome?: boolean;
15
25
  provider?: string;
16
26
  model?: string;
@@ -68,6 +78,19 @@ export interface Args {
68
78
  unrecognizedFlags: string[];
69
79
  }
70
80
 
81
+ /**
82
+ * Runtime dependencies the data-driven setters need. Constructed once at
83
+ * module load and passed to every {@link STRING_SETTERS} call so the
84
+ * setter table itself can stay free of `@oh-my-pi/pi-utils` runtime imports
85
+ * (which would otherwise trip the profile bootstrap's env-init ordering).
86
+ */
87
+ const PARSE_DEPS: ParseDeps = {
88
+ logger,
89
+ parseEffort,
90
+ builtinToolNames: BUILTIN_TOOL_NAMES,
91
+ thinkingEfforts: THINKING_EFFORTS,
92
+ };
93
+
71
94
  export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { type: "boolean" | "string" }>): Args {
72
95
  // Work on a copy: the `--option=value` handling below splices the value
73
96
  // into the array, and callers reuse the same argv (the post-extension
@@ -81,19 +104,19 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
81
104
  unrecognizedFlags: [],
82
105
  };
83
106
 
107
+ // `--` ends option parsing (POSIX end-of-options). Everything after it is
108
+ // literal positional text, so flag-shaped messages are not parsed or rejected.
84
109
  let sawSeparator = false;
85
110
  for (let i = 0; i < args.length; i++) {
86
111
  let arg = args[i];
87
- const flagIndex = i;
88
-
89
- // POSIX positional separator: once `--` lands, every remaining token is
90
- // a positional regardless of shape. Without this, a flag-looking message
91
- // (`omp -p -- --explain-this`) would be re-validated by the loop below
92
- // and rejected by the unknown-flag guard (#2461 review).
93
112
  if (sawSeparator) {
94
113
  result.messages.push(arg);
95
114
  continue;
96
115
  }
116
+ if (arg === PROFILE_BOOTSTRAP_BOUNDARY_ARG) {
117
+ continue;
118
+ }
119
+ const flagIndex = i;
97
120
 
98
121
  // Support --flag=value syntax (e.g. --tools=ask,read). The value is
99
122
  // spliced in as the next token so value-consuming flags pick it up via
@@ -120,112 +143,59 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
120
143
  if (extFlag.type === "boolean") {
121
144
  result.unknownFlags.set(flagName, true);
122
145
  } else if (extFlag.type === "string" && i + 1 < args.length) {
123
- // Consume the value in `--flag=value` form, or when the next token is
124
- // not flag-looking. A `-`-prefixed token in space form is left to be
125
- // its own flag; pass a flag-looking value as `--flag=value`.
146
+ // Consume the value in `--flag=value` form or when the next token is not
147
+ // flag-looking. A standalone `--` remains the end-of-options marker; use
148
+ // `--flag=--` when an extension needs a literal "--" string value.
126
149
  if (equalsValueIndex !== -1 || !args[i + 1].startsWith("-")) {
127
150
  result.unknownFlags.set(flagName, args[++i]);
128
151
  }
129
152
  }
153
+ } else if (STRING_VALUE_FLAGS.has(arg)) {
154
+ // Built-in string flags consume the next token even when it is flag-looking
155
+ // (`--system-prompt --profile foo` ⇒ the prompt is the literal "--profile").
156
+ // The one token they must never absorb is the profile bootstrap's internal
157
+ // boundary sentinel: an extension-shadowable built-in like `--plan` (parsed
158
+ // here only when its boolean extension is NOT loaded) would otherwise swallow
159
+ // the marker as its value and drop the user's trailing message.
160
+ if (i + 1 < args.length && args[i + 1] !== PROFILE_BOOTSTRAP_BOUNDARY_ARG) {
161
+ STRING_SETTERS[arg](result, args[++i], PARSE_DEPS);
162
+ }
163
+ } else if (OPTIONAL_VALUE_FLAGS.has(arg)) {
164
+ const config = OPTIONAL_FLAGS[arg];
165
+ const next = args[i + 1];
166
+ const consume =
167
+ next !== undefined && !next.startsWith("-") && !(config.rejectEmpty === true && next.length === 0);
168
+ config.set(result, consume ? args[++i] : undefined);
130
169
  } else if (arg === "--help" || arg === "-h") {
131
170
  result.help = true;
132
171
  } else if (arg === "--version" || arg === "-v") {
133
172
  result.version = true;
134
173
  } else if (arg === "--allow-home") {
135
174
  result.allowHome = true;
136
- } else if (arg === "--cwd" && i + 1 < args.length) {
137
- result.cwd = args[++i];
138
- } else if (arg === "--config" && i + 1 < args.length) {
139
- result.config = [...(result.config ?? []), args[++i]];
140
- } else if (arg === "--mode" && i + 1 < args.length) {
141
- const mode = args[++i];
142
- if (mode === "text" || mode === "json" || mode === "rpc" || mode === "acp" || mode === "rpc-ui") {
143
- result.mode = mode;
144
- }
175
+ } else if (arg === "--profile" && i + 1 < args.length) {
176
+ // Normally stripped by `extractProfileFlags` before parseArgs sees it;
177
+ // kept here as a fallback for direct parseArgs callers.
178
+ result.profile = args[++i];
179
+ } else if (arg.startsWith("--profile=")) {
180
+ result.profile = arg.slice("--profile=".length);
181
+ } else if (arg === "--alias" && i + 1 < args.length) {
182
+ result.alias = args[++i];
183
+ } else if (arg.startsWith("--alias=")) {
184
+ result.alias = arg.slice("--alias=".length);
145
185
  } else if (arg === "--continue" || arg === "-c") {
146
186
  result.continue = true;
147
- } else if (arg === "--resume" || arg === "-r" || arg === "--session") {
148
- const next = args[i + 1];
149
- if (next && !next.startsWith("-")) {
150
- result.resume = args[++i];
151
- } else {
152
- result.resume = true;
153
- }
154
- } else if (arg === "--fork" && i + 1 < args.length) {
155
- result.fork = args[++i];
156
- } else if (arg === "--provider" && i + 1 < args.length) {
157
- result.provider = args[++i];
158
- } else if (arg === "--model" && i + 1 < args.length) {
159
- result.model = args[++i];
160
- } else if (arg === "--smol" && i + 1 < args.length) {
161
- result.smol = args[++i];
162
- } else if (arg === "--slow" && i + 1 < args.length) {
163
- result.slow = args[++i];
164
- } else if (arg === "--plan" && i + 1 < args.length) {
165
- result.plan = args[++i];
166
- } else if (arg === "--api-key" && i + 1 < args.length) {
167
- result.apiKey = args[++i];
168
- } else if (arg === "--system-prompt" && i + 1 < args.length) {
169
- result.systemPrompt = args[++i];
170
- } else if (arg === "--append-system-prompt" && i + 1 < args.length) {
171
- result.appendSystemPrompt = args[++i];
172
- } else if (arg === "--provider-session-id" && i + 1 < args.length) {
173
- result.providerSessionId = args[++i];
174
187
  } else if (arg === "--no-session") {
175
188
  result.noSession = true;
176
- } else if (arg === "--session-dir" && i + 1 < args.length) {
177
- result.sessionDir = args[++i];
178
- } else if (arg === "--models" && i + 1 < args.length) {
179
- result.models = args[++i].split(",").map(s => s.trim());
180
189
  } else if (arg === "--no-tools") {
181
190
  result.noTools = true;
182
191
  } else if (arg === "--no-lsp") {
183
192
  result.noLsp = true;
184
193
  } else if (arg === "--no-pty") {
185
194
  result.noPty = true;
186
- } else if (arg === "--tools" && i + 1 < args.length) {
187
- const toolNames = args[++i]
188
- .split(",")
189
- .map(s => s.trim().toLowerCase())
190
- .filter(Boolean);
191
- const validTools: string[] = [];
192
- for (const name of toolNames) {
193
- if (name in BUILTIN_TOOLS) {
194
- validTools.push(name);
195
- } else {
196
- logger.warn("Unknown tool passed to --tools", {
197
- tool: name,
198
- validTools: Object.keys(BUILTIN_TOOLS),
199
- });
200
- }
201
- }
202
- result.tools = validTools;
203
- } else if (arg === "--thinking" && i + 1 < args.length) {
204
- const rawThinking = args[++i];
205
- const thinking = parseEffort(rawThinking);
206
- if (thinking !== undefined) {
207
- result.thinking = thinking;
208
- } else {
209
- logger.warn("Invalid thinking level passed to --thinking", {
210
- level: rawThinking,
211
- validThinkingLevels: THINKING_EFFORTS,
212
- });
213
- }
214
195
  } else if (arg === "--hide-thinking") {
215
196
  result.hideThinking = true;
216
197
  } else if (arg === "--print" || arg === "-p") {
217
198
  result.print = true;
218
- } else if (arg === "--export" && i + 1 < args.length) {
219
- result.export = args[++i];
220
- } else if (arg === "--hook" && i + 1 < args.length) {
221
- result.hooks = result.hooks ?? [];
222
- result.hooks.push(args[++i]);
223
- } else if ((arg === "--extension" || arg === "-e") && i + 1 < args.length) {
224
- result.extensions = result.extensions ?? [];
225
- result.extensions.push(args[++i]);
226
- } else if (arg === "--plugin-dir" && i + 1 < args.length) {
227
- result.pluginDirs = result.pluginDirs ?? [];
228
- result.pluginDirs.push(args[++i]);
229
199
  } else if (arg === "--no-extensions") {
230
200
  result.noExtensions = true;
231
201
  } else if (arg === "--no-skills") {
@@ -236,19 +206,6 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
236
206
  result.noTitle = true;
237
207
  } else if (arg === "--auto-approve" || arg === "--yolo") {
238
208
  result.autoApprove = true;
239
- } else if (arg === "--approval-mode" && i + 1 < args.length) {
240
- const mode = args[++i];
241
- if (mode === "always-ask" || mode === "write" || mode === "yolo") {
242
- result.approvalMode = mode;
243
- } else {
244
- logger.warn("Invalid value passed to --approval-mode", {
245
- value: mode,
246
- validValues: ["always-ask", "write", "yolo"],
247
- });
248
- }
249
- } else if (arg === "--skills" && i + 1 < args.length) {
250
- // Comma-separated glob patterns for skill filtering
251
- result.skills = args[++i].split(",").map(s => s.trim());
252
209
  } else if (arg.startsWith("@")) {
253
210
  result.fileArgs.push(arg.slice(1)); // Remove @ prefix
254
211
  } else if (!arg.startsWith("-") || arg === "-") {
@@ -344,13 +301,14 @@ export function getExtraHelpText(): string {
344
301
  ANTHROPIC_SEARCH_BASE_URL - Anthropic web search base URL (override; pairs with ANTHROPIC_SEARCH_API_KEY)
345
302
 
346
303
  ${chalk.dim("# Configuration")}
304
+ OMP_PROFILE - Named profile for isolated agent state (same as --profile)
305
+ Use \`omp --profile <name> --alias <command>\` to create a shell shortcut for a profile
347
306
  PI_CODING_AGENT_DIR - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
348
307
  PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)
349
308
  PI_SMOL_MODEL - Override smol/fast model (see --smol)
350
309
  PI_SLOW_MODEL - Override slow/reasoning model (see --slow)
351
310
  PI_PLAN_MODEL - Override planning model (see --plan)
352
311
  PI_NO_PTY - Disable PTY-based interactive bash execution
353
-
354
312
  For complete environment variable reference, see:
355
313
  ${chalk.dim("docs/environment-variables.md")}
356
314
  ${chalk.bold("Available Tools (default-enabled unless noted):")}
@@ -15,7 +15,7 @@
15
15
  * knob and is keyed by flag name so it stays stable as flags are added.
16
16
  */
17
17
  import type { ArgDescriptor, CliConfig, CommandCtor, FlagDescriptor } from "@oh-my-pi/pi-utils/cli";
18
- import { BUILTIN_TOOLS } from "../tools";
18
+ import { BUILTIN_TOOL_NAMES } from "../tools/builtin-names";
19
19
 
20
20
  export type Shell = "bash" | "zsh" | "fish";
21
21
 
@@ -77,7 +77,7 @@ function flagValue(name: string, desc: FlagDescriptor): ValueSource {
77
77
  if (MODEL_FLAGS[name]) return { kind: "models", multiple: false };
78
78
  if (name === "models") return { kind: "models", multiple: true };
79
79
  if (SESSION_FLAGS[name]) return { kind: "sessions" };
80
- if (name === "tools") return { kind: "list", values: Object.keys(BUILTIN_TOOLS) };
80
+ if (name === "tools") return { kind: "list", values: BUILTIN_TOOL_NAMES };
81
81
  if (DIR_FLAGS[name]) return { kind: "dir" };
82
82
  if (desc.kind === "integer") return { kind: "value" };
83
83
  return { kind: "file" };
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Single source of truth for argv flag classification, shared by:
3
+ * - `parseArgs` in `./args.ts` (the launch-time CLI parser)
4
+ * - `extractProfileFlags` in `./profile-bootstrap.ts` (the early
5
+ * `--profile` / `--alias` pre-parser)
6
+ *
7
+ * `parseArgs` dispatches string-valued flags by looking up their setter in
8
+ * {@link STRING_SETTERS}. Optional-value flags use {@link OPTIONAL_FLAGS} so
9
+ * per-flag quirks (currently empty-string rejection for `--resume`) live here
10
+ * instead of being hard-coded in the dispatch loop.
11
+ *
12
+ * The bootstrap doesn't dispatch — it only needs to know which flags consume
13
+ * a value — so it consults {@link STRING_VALUE_FLAGS} and
14
+ * {@link OPTIONAL_VALUE_FLAGS}, both derived from `Object.keys(...)` on the
15
+ * setter/config records below.
16
+ *
17
+ * The deliberate consequence: a string-valued flag exists in this CLI surface
18
+ * iff it has an entry here. Adding a new string-valued flag means adding a
19
+ * setter/config entry in this file; both `args.ts` and the bootstrap pick it
20
+ * up automatically, so the two cannot drift out of sync.
21
+ *
22
+ * IMPORT RULE: this module MUST NOT import any runtime value from
23
+ * `@oh-my-pi/pi-utils` (or anything that transitively does). That package's
24
+ * `env.ts` eagerly loads `.env` files from `getAgentDir()` during module
25
+ * initialization, which would race the profile bootstrap. Type-only imports
26
+ * are erased at runtime and are therefore safe.
27
+ *
28
+ * If a setter needs runtime dependencies (logging, validators, lookup
29
+ * tables), they're passed in through {@link ParseDeps} and `args.ts` wires the
30
+ * real implementations at the dispatch site.
31
+ */
32
+
33
+ import type { Effort } from "@oh-my-pi/pi-ai";
34
+ import type { Args } from "./args";
35
+
36
+ /**
37
+ * Runtime dependencies injected into setters that need to validate input or
38
+ * warn about bad values. `args.ts` constructs one object at module load and
39
+ * passes it to each {@link STRING_SETTERS} call.
40
+ *
41
+ * Keeping these out of the setter closures means this module stays free of
42
+ * runtime imports from `@oh-my-pi/pi-utils`, which is the whole reason it can
43
+ * be safely imported by `profile-bootstrap.ts` before `setProfile` runs.
44
+ */
45
+ export interface ParseDeps {
46
+ logger: { warn: (message: string, meta?: Record<string, unknown>) => void };
47
+ parseEffort: (value: string | null | undefined) => Effort | undefined;
48
+ builtinToolNames: readonly string[];
49
+ thinkingEfforts: readonly string[];
50
+ }
51
+
52
+ export type StringSetter = (result: Args, value: string, deps: ParseDeps) => void;
53
+
54
+ /**
55
+ * Setter for a flag that may or may not consume the next argv token.
56
+ * Receives `undefined` for the bare form (`--resume` with no value, etc.).
57
+ */
58
+ export type OptionalSetter = (result: Args, value: string | undefined) => void;
59
+
60
+ /**
61
+ * Per-flag optional-value consumption policy.
62
+ *
63
+ * Every optional flag always rejects tokens that start with `-` — that shared
64
+ * rule lives in the dispatch site. These booleans capture the *additional*
65
+ * per-flag quirks:
66
+ *
67
+ * - `rejectEmpty`: treat `""` like “no value provided”. Needed for
68
+ * `--resume` / `-r` / `--session`. Without it, an empty string
69
+ * gets consumed as the session prefix and downstream resolution can match
70
+ * every session.
71
+ */
72
+ export interface OptionalFlagConfig {
73
+ set: OptionalSetter;
74
+ rejectEmpty?: boolean;
75
+ }
76
+
77
+ // Shared setters for flags that alias the same field.
78
+ const setExtension: StringSetter = (result, value) => {
79
+ result.extensions = result.extensions ?? [];
80
+ result.extensions.push(value);
81
+ };
82
+
83
+ const setResume: OptionalSetter = (result, value) => {
84
+ result.resume = value !== undefined ? value : true;
85
+ };
86
+
87
+ /**
88
+ * Setters for flags with string values. Most built-ins consume the next argv
89
+ * token even when it starts with `-`; flags listed in
90
+ * {@link EXTENSION_SHADOWABLE_STRING_FLAGS} use extension-style consumption so
91
+ * a registered boolean extension can shadow them before profile bootstrap.
92
+ */
93
+ export const STRING_SETTERS: Record<string, StringSetter> = {
94
+ "--cwd": (result, value) => {
95
+ result.cwd = value;
96
+ },
97
+ "--config": (result, value) => {
98
+ result.config = [...(result.config ?? []), value];
99
+ },
100
+ "--mode": (result, value) => {
101
+ if (value === "text" || value === "json" || value === "rpc" || value === "acp" || value === "rpc-ui") {
102
+ result.mode = value;
103
+ }
104
+ },
105
+ "--fork": (result, value) => {
106
+ result.fork = value;
107
+ },
108
+ "--provider": (result, value) => {
109
+ result.provider = value;
110
+ },
111
+ "--model": (result, value) => {
112
+ result.model = value;
113
+ },
114
+ "--smol": (result, value) => {
115
+ result.smol = value;
116
+ },
117
+ "--slow": (result, value) => {
118
+ result.slow = value;
119
+ },
120
+ "--plan": (result, value) => {
121
+ result.plan = value;
122
+ },
123
+ "--api-key": (result, value) => {
124
+ result.apiKey = value;
125
+ },
126
+ "--system-prompt": (result, value) => {
127
+ result.systemPrompt = value;
128
+ },
129
+ "--append-system-prompt": (result, value) => {
130
+ result.appendSystemPrompt = value;
131
+ },
132
+ "--provider-session-id": (result, value) => {
133
+ result.providerSessionId = value;
134
+ },
135
+ "--session-dir": (result, value) => {
136
+ result.sessionDir = value;
137
+ },
138
+ "--models": (result, value) => {
139
+ result.models = value.split(",").map(s => s.trim());
140
+ },
141
+ "--tools": (result, value, deps) => {
142
+ const names = value
143
+ .split(",")
144
+ .map(s => s.trim().toLowerCase())
145
+ .filter(Boolean);
146
+ const valid: string[] = [];
147
+ for (const name of names) {
148
+ if (deps.builtinToolNames.includes(name)) {
149
+ valid.push(name);
150
+ } else {
151
+ deps.logger.warn("Unknown tool passed to --tools", {
152
+ tool: name,
153
+ validTools: deps.builtinToolNames,
154
+ });
155
+ }
156
+ }
157
+ result.tools = valid;
158
+ },
159
+ "--thinking": (result, value, deps) => {
160
+ const thinking = deps.parseEffort(value);
161
+ if (thinking !== undefined) {
162
+ result.thinking = thinking;
163
+ } else {
164
+ deps.logger.warn("Invalid thinking level passed to --thinking", {
165
+ level: value,
166
+ validThinkingLevels: deps.thinkingEfforts,
167
+ });
168
+ }
169
+ },
170
+ "--export": (result, value) => {
171
+ result.export = value;
172
+ },
173
+ "--hook": (result, value) => {
174
+ result.hooks = result.hooks ?? [];
175
+ result.hooks.push(value);
176
+ },
177
+ "--extension": setExtension,
178
+ "-e": setExtension,
179
+ "--plugin-dir": (result, value) => {
180
+ result.pluginDirs = result.pluginDirs ?? [];
181
+ result.pluginDirs.push(value);
182
+ },
183
+ "--skills": (result, value) => {
184
+ result.skills = value.split(",").map(s => s.trim());
185
+ },
186
+ "--approval-mode": (result, value, deps) => {
187
+ if (value === "always-ask" || value === "write" || value === "yolo") {
188
+ result.approvalMode = value;
189
+ } else {
190
+ deps.logger.warn("Invalid value passed to --approval-mode", {
191
+ value,
192
+ validValues: ["always-ask", "write", "yolo"],
193
+ });
194
+ }
195
+ },
196
+ };
197
+
198
+ /**
199
+ * Optional-value flags. Setters receive `undefined` for the bare form.
200
+ *
201
+ * The dispatch in `args.ts` applies the shared "doesn't start with `-`"
202
+ * check for every flag, then consults the per-flag booleans below for the
203
+ * remaining quirks.
204
+ */
205
+ export const OPTIONAL_FLAGS: Record<string, OptionalFlagConfig> = {
206
+ "--resume": { set: setResume, rejectEmpty: true },
207
+ "-r": { set: setResume, rejectEmpty: true },
208
+ "--session": { set: setResume, rejectEmpty: true },
209
+ };
210
+
211
+ /**
212
+ * Derived from {@link STRING_SETTERS}. A flag is in this set if and only if
213
+ * it has a setter — by construction, drift between "the bootstrap thinks
214
+ * this flag accepts a value" and "the launch parser can set one" is
215
+ * structurally impossible.
216
+ */
217
+ export const STRING_VALUE_FLAGS: ReadonlySet<string> = new Set(Object.keys(STRING_SETTERS));
218
+
219
+ /**
220
+ * Built-in string flags known to be shadowed by bundled/common boolean
221
+ * extensions before extension metadata is available. They still accept a
222
+ * value-like successor for the built-in form (`--plan opus`), but a
223
+ * flag-looking successor remains a fresh flag (`--plan --profile work`).
224
+ */
225
+ export const EXTENSION_SHADOWABLE_STRING_FLAGS: ReadonlySet<string> = new Set(["--plan"]);
226
+
227
+ /**
228
+ * Derived from {@link OPTIONAL_FLAGS}. Same single-source contract as
229
+ * {@link STRING_VALUE_FLAGS}.
230
+ */
231
+ export const OPTIONAL_VALUE_FLAGS: ReadonlySet<string> = new Set(Object.keys(OPTIONAL_FLAGS));
232
+
233
+ /**
234
+ * Internal marker inserted by the profile bootstrap when removing `--profile`
235
+ * or `--alias` would otherwise make the following value-like token become the
236
+ * value of a preceding optional/extension flag. `parseArgs` ignores it, but its
237
+ * flag-looking shape preserves argv boundaries during the second parse.
238
+ */
239
+ export const PROFILE_BOOTSTRAP_BOUNDARY_ARG = "--omp-profile-boundary";
240
+
241
+ /**
242
+ * Long-form launch flags that take NO value (booleans). The bootstrap pre-parser
243
+ * needs this to tell a known value-less flag (whose successor is a fresh
244
+ * argument — `omp --print --profile work` still selects a profile) apart from an
245
+ * UNKNOWN long option that might be an extension string flag consuming the next
246
+ * token as its value (so the bootstrap must not steal that token as a global
247
+ * `--profile`/`--alias`). MUST mirror the value-less flag arms of `parseArgs`
248
+ * in `./args.ts`: adding a new boolean launch flag there means adding it here,
249
+ * or `--<newflag> --profile X` stops selecting a profile. Short aliases
250
+ * (`-h`/`-v`/`-c`/`-p`) are intentionally omitted — the protection rule only
251
+ * fires for `--`-prefixed tokens.
252
+ */
253
+ export const VALUELESS_FLAGS: ReadonlySet<string> = new Set([
254
+ "--help",
255
+ "--version",
256
+ "--allow-home",
257
+ "--continue",
258
+ "--no-session",
259
+ "--no-tools",
260
+ "--no-lsp",
261
+ "--no-pty",
262
+ "--hide-thinking",
263
+ "--print",
264
+ "--no-extensions",
265
+ "--no-skills",
266
+ "--no-rules",
267
+ "--no-title",
268
+ "--auto-approve",
269
+ "--yolo",
270
+ ]);