@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
@@ -0,0 +1,338 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import { normalizeProfileName } from "@oh-my-pi/pi-utils/dirs";
4
+
5
+ export type ProfileAliasShell = "bash" | "zsh" | "fish" | "powershell" | "pwsh";
6
+
7
+ function quoteForShell(pathValue: string): string {
8
+ return `'${pathValue.replace(/'/g, `'"'"'`)}'`;
9
+ }
10
+
11
+ function quoteForPowerShell(pathValue: string): string {
12
+ return `'${pathValue.replace(/'/g, `''`)}'`;
13
+ }
14
+
15
+ export interface ProfileAliasCommand {
16
+ display: string;
17
+ posix: string;
18
+ fish: string;
19
+ powerShell: string;
20
+ }
21
+
22
+ const DEFAULT_ALIAS_COMMAND: ProfileAliasCommand = {
23
+ display: "omp",
24
+ posix: "omp",
25
+ fish: "omp",
26
+ powerShell: "omp",
27
+ };
28
+
29
+ export interface ProfileAliasInstallOptions {
30
+ profile: string;
31
+ aliasName: string;
32
+ shellPath?: string;
33
+ platform?: NodeJS.Platform;
34
+ homeDir?: string;
35
+ env?: NodeJS.ProcessEnv;
36
+ readFile?: (filePath: string) => Promise<string>;
37
+ command?: ProfileAliasCommand;
38
+ writeFile?: (filePath: string, content: string) => Promise<void>;
39
+ }
40
+
41
+ export interface ProfileAliasInstallResult {
42
+ shell: ProfileAliasShell;
43
+ configPath: string;
44
+ aliasName: string;
45
+ profile: string;
46
+ command: string;
47
+ reloadedWith: string;
48
+ }
49
+
50
+ const ALIAS_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
51
+ const POSIX_RESERVED_ALIAS_NAMES: ReadonlySet<string> = new Set([
52
+ "case",
53
+ "coproc",
54
+ "do",
55
+ "done",
56
+ "elif",
57
+ "else",
58
+ "esac",
59
+ "fi",
60
+ "for",
61
+ "function",
62
+ "if",
63
+ "in",
64
+ "select",
65
+ "then",
66
+ "time",
67
+ "until",
68
+ "while",
69
+ ]);
70
+ const FISH_RESERVED_ALIAS_NAMES: ReadonlySet<string> = new Set([
71
+ "and",
72
+ "begin",
73
+ "break",
74
+ "builtin",
75
+ "case",
76
+ "command",
77
+ "continue",
78
+ "else",
79
+ "end",
80
+ "exec",
81
+ "for",
82
+ "function",
83
+ "if",
84
+ "not",
85
+ "or",
86
+ "return",
87
+ "switch",
88
+ "while",
89
+ ]);
90
+ const POWERSHELL_RESERVED_ALIAS_NAMES: ReadonlySet<string> = new Set([
91
+ "begin",
92
+ "break",
93
+ "catch",
94
+ "class",
95
+ "continue",
96
+ "data",
97
+ "do",
98
+ "dynamicparam",
99
+ "else",
100
+ "elseif",
101
+ "end",
102
+ "enum",
103
+ "exit",
104
+ "filter",
105
+ "finally",
106
+ "for",
107
+ "foreach",
108
+ "from",
109
+ "function",
110
+ "if",
111
+ "in",
112
+ "param",
113
+ "process",
114
+ "return",
115
+ "switch",
116
+ "throw",
117
+ "trap",
118
+ "try",
119
+ "until",
120
+ "using",
121
+ "var",
122
+ "while",
123
+ "workflow",
124
+ ]);
125
+
126
+ // Keep local: importing the pi-utils root here would eagerly load env before
127
+ // cli.ts has applied --profile, regressing profile-specific .env loading.
128
+ function isEnoentError(error: unknown): boolean {
129
+ return typeof error === "object" && error !== null && (error as { code?: unknown }).code === "ENOENT";
130
+ }
131
+
132
+ function getReservedAliasNames(shell: ProfileAliasShell): ReadonlySet<string> {
133
+ switch (shell) {
134
+ case "bash":
135
+ case "zsh":
136
+ return POSIX_RESERVED_ALIAS_NAMES;
137
+ case "fish":
138
+ return FISH_RESERVED_ALIAS_NAMES;
139
+ case "powershell":
140
+ case "pwsh":
141
+ return POWERSHELL_RESERVED_ALIAS_NAMES;
142
+ }
143
+ }
144
+
145
+ function validateAliasName(aliasName: string, shell: ProfileAliasShell): string {
146
+ const normalized = aliasName.trim();
147
+ if (!ALIAS_NAME_RE.test(normalized)) {
148
+ throw new Error(`Invalid alias "${aliasName}". Alias names must match ${ALIAS_NAME_RE.source}.`);
149
+ }
150
+ if (normalized.toLowerCase() === "omp") {
151
+ throw new Error('Invalid alias "omp". Refusing to shadow the base omp command.');
152
+ }
153
+ if (getReservedAliasNames(shell).has(normalized.toLowerCase())) {
154
+ throw new Error(`Invalid alias "${aliasName}". Refusing to create a ${shell} reserved word.`);
155
+ }
156
+ return normalized;
157
+ }
158
+
159
+ // On Windows the launching shell is rarely exported through $SHELL, so when it
160
+ // is missing we infer the PowerShell edition from the inherited environment.
161
+ // PowerShell 7 (pwsh) always seeds PSModulePath with separator-delimited
162
+ // ".../PowerShell/..." module directories (plus the Windows PowerShell ones for
163
+ // back-compat), whereas Windows PowerShell 5.1 only ever lists
164
+ // ".../WindowsPowerShell/...". The separator anchors keep "WindowsPowerShell"
165
+ // from matching. POWERSHELL_DISTRIBUTION_CHANNEL is set only by some pwsh
166
+ // distributions, so it stays a secondary hint rather than the primary signal.
167
+ function detectWindowsPowerShell(env: NodeJS.ProcessEnv): ProfileAliasShell {
168
+ const modulePath = env.PSModulePath ?? env.PSMODULEPATH ?? env.psmodulepath ?? "";
169
+ if (/[\\/]PowerShell[\\/]/i.test(modulePath)) return "pwsh";
170
+ if (env.POWERSHELL_DISTRIBUTION_CHANNEL) return "pwsh";
171
+ return "powershell";
172
+ }
173
+
174
+ function normalizeShellName(
175
+ shellPath: string | undefined,
176
+ platform: NodeJS.Platform,
177
+ env: NodeJS.ProcessEnv,
178
+ ): ProfileAliasShell {
179
+ const shell = path
180
+ .basename(shellPath ?? "")
181
+ .toLowerCase()
182
+ .replace(/\.exe$/, "");
183
+ if (shell === "zsh") return "zsh";
184
+ if (shell === "bash") return "bash";
185
+ if (shell === "fish") return "fish";
186
+ if (shell === "pwsh") return "pwsh";
187
+ if (shell === "powershell") return "powershell";
188
+ if (platform === "win32") return detectWindowsPowerShell(env);
189
+ throw new Error(`Unsupported shell${shell ? ` "${shell}"` : ""}. Supported shells: bash, zsh, fish, PowerShell.`);
190
+ }
191
+
192
+ export function resolveProfileAliasCommandFromProcess(
193
+ argv: readonly string[] = process.argv,
194
+ cwd: string = process.cwd(),
195
+ ): ProfileAliasCommand {
196
+ const runtime = argv[0];
197
+ const script = argv[1];
198
+ if (!runtime || !script || !/\.[cm]?[jt]s$/.test(script)) return DEFAULT_ALIAS_COMMAND;
199
+
200
+ const scriptPath = path.resolve(cwd, script);
201
+ const posix = `${quoteForShell(runtime)} ${quoteForShell(scriptPath)}`;
202
+ return {
203
+ display: `${runtime} ${scriptPath}`,
204
+ posix,
205
+ fish: posix,
206
+ powerShell: `${quoteForPowerShell(runtime)} ${quoteForPowerShell(scriptPath)}`,
207
+ };
208
+ }
209
+
210
+ function resolveShellConfigPath(
211
+ shell: ProfileAliasShell,
212
+ homeDir: string,
213
+ platform: NodeJS.Platform,
214
+ env: NodeJS.ProcessEnv,
215
+ ): string {
216
+ switch (shell) {
217
+ case "zsh":
218
+ return path.join(env.ZDOTDIR || homeDir, ".zshrc");
219
+ case "bash":
220
+ return platform === "darwin" ? path.join(homeDir, ".bash_profile") : path.join(homeDir, ".bashrc");
221
+ case "fish": {
222
+ // fish sources conf.d from $XDG_CONFIG_HOME/fish (default ~/.config/fish);
223
+ // a hard-coded ~/.config would be silently ignored when the user relocates
224
+ // their XDG config root, leaving the alias unsourced after a restart.
225
+ const configHome = env.XDG_CONFIG_HOME || path.join(homeDir, ".config");
226
+ return path.join(configHome, "fish", "conf.d", "omp-profiles.fish");
227
+ }
228
+ case "pwsh":
229
+ return platform === "win32"
230
+ ? path.join(homeDir, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1")
231
+ : path.join(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1");
232
+ case "powershell":
233
+ return path.join(homeDir, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
234
+ }
235
+ }
236
+
237
+ function renderAliasBlock(
238
+ shell: ProfileAliasShell,
239
+ aliasName: string,
240
+ profile: string,
241
+ command: ProfileAliasCommand,
242
+ ): { block: string; command: string } {
243
+ const profiledCommand = `${command.display} --profile=${profile}`;
244
+ const start = `# >>> omp profile alias: ${aliasName} >>>`;
245
+ const end = `# <<< omp profile alias: ${aliasName} <<<`;
246
+ let body: string;
247
+ switch (shell) {
248
+ case "fish":
249
+ body = [
250
+ `function ${aliasName} --wraps omp --description 'OMP profile ${profile}'`,
251
+ ` command ${command.fish} --profile=${profile} $argv`,
252
+ "end",
253
+ ].join("\n");
254
+ break;
255
+ case "powershell":
256
+ case "pwsh":
257
+ body = [`function ${aliasName} {`, ` & ${command.powerShell} --profile=${profile} @args`, "}"].join("\n");
258
+ break;
259
+ default:
260
+ body = [`${aliasName}() {`, ` command ${command.posix} --profile=${profile} "$@"`, "}"].join("\n");
261
+ break;
262
+ }
263
+ return { block: `${start}\n${body}\n${end}`, command: profiledCommand };
264
+ }
265
+
266
+ function upsertBlock(content: string, aliasName: string, block: string): string {
267
+ const start = `# >>> omp profile alias: ${aliasName} >>>`;
268
+ const end = `# <<< omp profile alias: ${aliasName} <<<`;
269
+ const startIndex = content.indexOf(start);
270
+ if (startIndex !== -1) {
271
+ const endIndex = content.indexOf(end, startIndex + start.length);
272
+ if (endIndex === -1) {
273
+ throw new Error(
274
+ `Found "${start}" without a matching "${end}" in the shell config. ` +
275
+ `The managed alias block is malformed; remove the stale marker line and rerun --alias.`,
276
+ );
277
+ }
278
+ const afterEnd = endIndex + end.length;
279
+ const prefix = content.slice(0, startIndex).replace(/[\t ]*\n?$/, "");
280
+ const suffix = content.slice(afterEnd).replace(/^\n?/, "");
281
+ return [prefix, block, suffix].filter(Boolean).join("\n\n").replace(/\n*$/, "\n");
282
+ }
283
+ const trimmed = content.replace(/\s*$/, "");
284
+ return `${trimmed}${trimmed ? "\n\n" : ""}${block}\n`;
285
+ }
286
+
287
+ function readAliasConfigText(filePath: string): Promise<string> {
288
+ return Bun.file(filePath).text();
289
+ }
290
+
291
+ export async function readProfileAliasConfigFile(
292
+ filePath: string,
293
+ readText: (filePath: string) => Promise<string> = readAliasConfigText,
294
+ ): Promise<string> {
295
+ try {
296
+ return await readText(filePath);
297
+ } catch (error) {
298
+ if (isEnoentError(error)) return "";
299
+ throw error;
300
+ }
301
+ }
302
+
303
+ export async function installProfileAlias(options: ProfileAliasInstallOptions): Promise<ProfileAliasInstallResult> {
304
+ const profile = normalizeProfileName(options.profile);
305
+ if (!profile) {
306
+ throw new Error("--alias requires a named --profile value.");
307
+ }
308
+ const platform = options.platform ?? process.platform;
309
+ const homeDir = options.homeDir ?? os.homedir();
310
+ const env = options.env ?? process.env;
311
+ const shell = normalizeShellName(options.shellPath ?? env.SHELL, platform, env);
312
+ const aliasName = validateAliasName(options.aliasName, shell);
313
+ const configPath = resolveShellConfigPath(shell, homeDir, platform, env);
314
+ const { block, command } = renderAliasBlock(shell, aliasName, profile, options.command ?? DEFAULT_ALIAS_COMMAND);
315
+ const readFile = options.readFile ?? readProfileAliasConfigFile;
316
+ const writeFile =
317
+ options.writeFile ??
318
+ (async (filePath, content) => {
319
+ await Bun.write(filePath, content);
320
+ });
321
+
322
+ const current = await readFile(configPath);
323
+ await writeFile(configPath, upsertBlock(current, aliasName, block));
324
+
325
+ return {
326
+ shell,
327
+ configPath,
328
+ aliasName,
329
+ profile,
330
+ command,
331
+ reloadedWith:
332
+ shell === "fish"
333
+ ? `source ${quoteForShell(configPath)}`
334
+ : shell === "powershell" || shell === "pwsh"
335
+ ? `. ${quoteForPowerShell(configPath)}`
336
+ : `. ${quoteForShell(configPath)}`,
337
+ };
338
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Bootstrap-time argv preparser for the global `--profile` / `--alias` flags.
3
+ *
4
+ * Profile selection MUST happen before any module reads `getAgentDir()` (notably
5
+ * `@oh-my-pi/pi-utils/env`, which eagerly loads `.env` from the agent directory
6
+ * during its own import). The full `parseArgs` from `./args.ts` lives downstream
7
+ * of those imports, so we can't rely on it for profile bootstrap — we have to
8
+ * crack open argv before the lazy command modules load.
9
+ *
10
+ * Because of that, this preparser must respect the same value-consumption
11
+ * contract as `args.ts`: known string-valued flags usually consume the next
12
+ * token even when it starts with `-`, except for string flags that can be
13
+ * shadowed by preloaded boolean extensions (currently `--plan`). Optional-value
14
+ * flags (`--resume`, `--session`, `-r`) consume the next token only when it
15
+ * doesn't look like another flag. Without this, `omp --system-prompt --profile
16
+ * foo` silently activates profile `foo`
17
+ * instead of passing the literal `--profile` to the system prompt and `foo`
18
+ * as a positional message.
19
+ *
20
+ * The shared classification lives in {@link ./flag-tables}, imported below,
21
+ * so the bootstrap and `args.ts` reference one source of truth instead of
22
+ * maintaining parallel constants.
23
+ *
24
+ * An unclassified bare long option (one not in any flag table) is treated as a
25
+ * possible extension string flag, but the bootstrap mirrors `parseArgs`'
26
+ * extension-flag rules ({@link ./args}): a string extension flag consumes its
27
+ * successor ONLY when that successor is value-like (does not start with `-`), and
28
+ * a boolean extension flag consumes nothing. So the successor is forwarded
29
+ * untouched (and never read as a global `--profile`/`--alias`) only when it is
30
+ * value-like; a flag-looking successor is left for normal processing, so
31
+ * `omp --some-ext-flag --profile work` still selects a profile. Known value-less
32
+ * launch flags ({@link VALUELESS_FLAGS}) are exempt so a trailing profile after
33
+ * them also activates (`omp --print --profile work`).
34
+ */
35
+
36
+ import { isSubcommand } from "../cli-commands";
37
+ import {
38
+ EXTENSION_SHADOWABLE_STRING_FLAGS,
39
+ OPTIONAL_FLAGS,
40
+ OPTIONAL_VALUE_FLAGS,
41
+ PROFILE_BOOTSTRAP_BOUNDARY_ARG,
42
+ STRING_VALUE_FLAGS,
43
+ VALUELESS_FLAGS,
44
+ } from "./flag-tables";
45
+
46
+ function isProfileBootstrapSubcommand(arg: string): boolean {
47
+ return arg === "launch" || arg === "acp";
48
+ }
49
+
50
+ function isUnknownLongValueCandidate(arg: string): boolean {
51
+ return (
52
+ arg.startsWith("--") &&
53
+ !arg.includes("=") &&
54
+ !STRING_VALUE_FLAGS.has(arg) &&
55
+ !OPTIONAL_VALUE_FLAGS.has(arg) &&
56
+ !VALUELESS_FLAGS.has(arg)
57
+ );
58
+ }
59
+
60
+ function needsBoundaryAfterGlobalStrip(stripped: readonly string[]): boolean {
61
+ const previous = stripped[stripped.length - 1];
62
+ return (
63
+ previous !== undefined &&
64
+ (OPTIONAL_VALUE_FLAGS.has(previous) ||
65
+ EXTENSION_SHADOWABLE_STRING_FLAGS.has(previous) ||
66
+ isUnknownLongValueCandidate(previous))
67
+ );
68
+ }
69
+
70
+ export interface ProfileBootstrapResult {
71
+ argv: string[];
72
+ profile?: string;
73
+ aliasName?: string;
74
+ }
75
+
76
+ /**
77
+ * Strip `--profile` / `--alias` from argv while preserving the surrounding
78
+ * argument structure, returning the residual argv to hand to the launch parser
79
+ * and the captured flag values.
80
+ *
81
+ * Global flag extraction stops only when the first residual argv token names a
82
+ * registered command that owns its own flags (e.g. `grep`): everything from
83
+ * that token onward is forwarded verbatim so a subcommand's own flags and
84
+ * positionals are never stolen (`omp grep --profile <path>` greps for
85
+ * `--profile`; it does not select a profile). `launch` and `acp` are explicit
86
+ * spellings of launch-shaped commands, so `omp launch --profile work` and
87
+ * `omp acp --profile work` still select profile `work`.
88
+ *
89
+ * Throws when either flag is supplied without a value.
90
+ */
91
+ export function extractProfileFlags(argv: readonly string[]): ProfileBootstrapResult {
92
+ const stripped: string[] = [];
93
+ let profile: string | undefined;
94
+ let aliasName: string | undefined;
95
+ let passThrough = false;
96
+ let sawSubcommand = false;
97
+ let canDispatchSubcommand = true;
98
+ let insertBoundaryBeforeNextValue = false;
99
+ for (let index = 0; index < argv.length; index += 1) {
100
+ const arg = argv[index];
101
+
102
+ if (passThrough || sawSubcommand) {
103
+ stripped.push(arg);
104
+ continue;
105
+ }
106
+
107
+ if (insertBoundaryBeforeNextValue) {
108
+ if (!arg.startsWith("-")) {
109
+ stripped.push(PROFILE_BOOTSTRAP_BOUNDARY_ARG);
110
+ }
111
+ insertBoundaryBeforeNextValue = false;
112
+ }
113
+
114
+ // `--` ends option processing. Anything that follows is forwarded verbatim
115
+ // so users can pass arbitrary tokens (including a literal `--profile`) to
116
+ // downstream tools without the bootstrap stealing them.
117
+ if (arg === "--") {
118
+ passThrough = true;
119
+ stripped.push(arg);
120
+ continue;
121
+ }
122
+
123
+ if (arg === "--profile") {
124
+ const value = argv[index + 1];
125
+ if (!value || value.startsWith("-")) {
126
+ throw new Error("--profile requires a profile name");
127
+ }
128
+ profile = value;
129
+ insertBoundaryBeforeNextValue = needsBoundaryAfterGlobalStrip(stripped);
130
+ index += 1;
131
+ continue;
132
+ }
133
+ if (arg.startsWith("--profile=")) {
134
+ const value = arg.slice("--profile=".length);
135
+ if (!value) {
136
+ throw new Error("--profile requires a profile name");
137
+ }
138
+ profile = value;
139
+ insertBoundaryBeforeNextValue = needsBoundaryAfterGlobalStrip(stripped);
140
+ continue;
141
+ }
142
+ if (arg === "--alias") {
143
+ const value = argv[index + 1];
144
+ if (!value || value.startsWith("-")) {
145
+ throw new Error("--alias requires a command name");
146
+ }
147
+ aliasName = value;
148
+ insertBoundaryBeforeNextValue = needsBoundaryAfterGlobalStrip(stripped);
149
+ index += 1;
150
+ continue;
151
+ }
152
+ if (arg.startsWith("--alias=")) {
153
+ const value = arg.slice("--alias=".length);
154
+ if (!value) {
155
+ throw new Error("--alias requires a command name");
156
+ }
157
+ aliasName = value;
158
+ insertBoundaryBeforeNextValue = needsBoundaryAfterGlobalStrip(stripped);
159
+ continue;
160
+ }
161
+
162
+ // Known string flags normally consume flag-looking values (for example
163
+ // `--system-prompt --profile foo` means the system prompt is literally
164
+ // `--profile`). A small allow-list of built-ins can be shadowed by boolean
165
+ // extensions before extension metadata is loaded; those mirror extension
166
+ // consumption here so `--plan --profile work` still activates `work`.
167
+ if (EXTENSION_SHADOWABLE_STRING_FLAGS.has(arg)) {
168
+ canDispatchSubcommand = false;
169
+ stripped.push(arg);
170
+ const next = argv[index + 1];
171
+ if (next !== undefined && !next.startsWith("-")) {
172
+ stripped.push(next);
173
+ index += 1;
174
+ }
175
+ continue;
176
+ }
177
+
178
+ // Forward both the flag and its value untouched so the downstream parser
179
+ // gets exactly what the user typed. Critical for `--system-prompt
180
+ // --profile foo`: the bootstrap must NOT interpret `--profile` here, it
181
+ // belongs to `--system-prompt`.
182
+ if (STRING_VALUE_FLAGS.has(arg)) {
183
+ canDispatchSubcommand = false;
184
+ stripped.push(arg);
185
+ if (index + 1 < argv.length) {
186
+ stripped.push(argv[index + 1]);
187
+ index += 1;
188
+ }
189
+ continue;
190
+ }
191
+
192
+ if (OPTIONAL_VALUE_FLAGS.has(arg)) {
193
+ canDispatchSubcommand = false;
194
+ stripped.push(arg);
195
+ const config = OPTIONAL_FLAGS[arg];
196
+ const next = argv[index + 1];
197
+ if (next !== undefined && !next.startsWith("-") && !(config.rejectEmpty === true && next.length === 0)) {
198
+ stripped.push(next);
199
+ index += 1;
200
+ }
201
+ continue;
202
+ }
203
+
204
+ // An unclassified bare long option (`--xxx` with no `=`) may be an extension
205
+ // string flag that consumes the next token as its value. The bootstrap runs
206
+ // before extensions load, so it cannot consult the extension flag table; it
207
+ // therefore mirrors the value-consumption rule `parseArgs` applies to
208
+ // extension flags (./args.ts): a string extension flag consumes its successor
209
+ // ONLY when that successor is value-like (does not start with `-`), and a
210
+ // boolean extension flag consumes nothing. So protect (forward + skip) the
211
+ // successor only when it is value-like — `omp --bar val --profile work` keeps
212
+ // `val` with `--bar` and still extracts the trailing profile — and otherwise
213
+ // forward just the flag, letting the loop process a flag-looking successor so
214
+ // a trailing global flag still applies (`omp --some-ext-bool --profile work`
215
+ // selects profile `work`). A `--` successor is deliberately NOT protected
216
+ // here: it falls through to the end-of-options arm above, keeping `--` a
217
+ // single, consistent meaning instead of being swallowed as a flag value.
218
+ // Known value-less launch flags are exempt so a trailing profile still
219
+ // activates (`omp --print --profile work`).
220
+ if (isUnknownLongValueCandidate(arg)) {
221
+ canDispatchSubcommand = false;
222
+ stripped.push(arg);
223
+ const next = argv[index + 1];
224
+ if (next !== undefined && !next.startsWith("-")) {
225
+ stripped.push(next);
226
+ index += 1;
227
+ }
228
+ continue;
229
+ }
230
+
231
+ // Only the first residual argv token can be the dispatched subcommand. Once
232
+ // any other token has been forwarded, later subcommand names are launch text.
233
+ // `launch` and `acp` are explicit spellings of launch-shaped commands, so
234
+ // global launch flags that follow them must still be extracted.
235
+ if (canDispatchSubcommand && isSubcommand(arg) && !isProfileBootstrapSubcommand(arg)) {
236
+ sawSubcommand = true;
237
+ }
238
+ canDispatchSubcommand = false;
239
+ stripped.push(arg);
240
+ }
241
+
242
+ return { argv: stripped, profile, aliasName };
243
+ }