ikarmic-forge 0.0.0-stage → 4.4.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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -2
  3. package/dist/acp/server.d.ts +53 -0
  4. package/dist/acp/server.d.ts.map +1 -0
  5. package/dist/acp/server.js +235 -0
  6. package/dist/acp/server.js.map +1 -0
  7. package/dist/app.d.ts +22 -0
  8. package/dist/app.d.ts.map +1 -0
  9. package/dist/app.js +159 -0
  10. package/dist/app.js.map +1 -0
  11. package/dist/bin/forge.d.ts +3 -0
  12. package/dist/bin/forge.d.ts.map +1 -0
  13. package/dist/bin/forge.js +747 -0
  14. package/dist/bin/forge.js.map +1 -0
  15. package/dist/chat-files.d.ts +37 -0
  16. package/dist/chat-files.d.ts.map +1 -0
  17. package/dist/chat-files.js +152 -0
  18. package/dist/chat-files.js.map +1 -0
  19. package/dist/commands/acp.d.ts +4 -0
  20. package/dist/commands/acp.d.ts.map +1 -0
  21. package/dist/commands/acp.js +36 -0
  22. package/dist/commands/acp.js.map +1 -0
  23. package/dist/commands/ai-comments.d.ts +9 -0
  24. package/dist/commands/ai-comments.d.ts.map +1 -0
  25. package/dist/commands/ai-comments.js +43 -0
  26. package/dist/commands/ai-comments.js.map +1 -0
  27. package/dist/commands/architect.d.ts +9 -0
  28. package/dist/commands/architect.d.ts.map +1 -0
  29. package/dist/commands/architect.js +27 -0
  30. package/dist/commands/architect.js.map +1 -0
  31. package/dist/commands/checkpoint.d.ts +10 -0
  32. package/dist/commands/checkpoint.d.ts.map +1 -0
  33. package/dist/commands/checkpoint.js +168 -0
  34. package/dist/commands/checkpoint.js.map +1 -0
  35. package/dist/commands/config.d.ts +15 -0
  36. package/dist/commands/config.d.ts.map +1 -0
  37. package/dist/commands/config.js +180 -0
  38. package/dist/commands/config.js.map +1 -0
  39. package/dist/commands/diff.d.ts +29 -0
  40. package/dist/commands/diff.d.ts.map +1 -0
  41. package/dist/commands/diff.js +90 -0
  42. package/dist/commands/diff.js.map +1 -0
  43. package/dist/commands/hook.d.ts +10 -0
  44. package/dist/commands/hook.d.ts.map +1 -0
  45. package/dist/commands/hook.js +121 -0
  46. package/dist/commands/hook.js.map +1 -0
  47. package/dist/commands/mcp.d.ts +10 -0
  48. package/dist/commands/mcp.d.ts.map +1 -0
  49. package/dist/commands/mcp.js +110 -0
  50. package/dist/commands/mcp.js.map +1 -0
  51. package/dist/commands/orchestrator.d.ts +22 -0
  52. package/dist/commands/orchestrator.d.ts.map +1 -0
  53. package/dist/commands/orchestrator.js +113 -0
  54. package/dist/commands/orchestrator.js.map +1 -0
  55. package/dist/commands/plugin.d.ts +3 -0
  56. package/dist/commands/plugin.d.ts.map +1 -0
  57. package/dist/commands/plugin.js +62 -0
  58. package/dist/commands/plugin.js.map +1 -0
  59. package/dist/commands/policy.d.ts +2 -0
  60. package/dist/commands/policy.d.ts.map +1 -0
  61. package/dist/commands/policy.js +21 -0
  62. package/dist/commands/policy.js.map +1 -0
  63. package/dist/commands/skill.d.ts +13 -0
  64. package/dist/commands/skill.d.ts.map +1 -0
  65. package/dist/commands/skill.js +194 -0
  66. package/dist/commands/skill.js.map +1 -0
  67. package/dist/commands/spec.d.ts +13 -0
  68. package/dist/commands/spec.d.ts.map +1 -0
  69. package/dist/commands/spec.js +76 -0
  70. package/dist/commands/spec.js.map +1 -0
  71. package/dist/commands/watch.d.ts +15 -0
  72. package/dist/commands/watch.d.ts.map +1 -0
  73. package/dist/commands/watch.js +83 -0
  74. package/dist/commands/watch.js.map +1 -0
  75. package/dist/commands/worktree.d.ts +22 -0
  76. package/dist/commands/worktree.d.ts.map +1 -0
  77. package/dist/commands/worktree.js +187 -0
  78. package/dist/commands/worktree.js.map +1 -0
  79. package/dist/components/Approval.d.ts +9 -0
  80. package/dist/components/Approval.d.ts.map +1 -0
  81. package/dist/components/Approval.js +37 -0
  82. package/dist/components/Approval.js.map +1 -0
  83. package/dist/components/Footer.d.ts +22 -0
  84. package/dist/components/Footer.d.ts.map +1 -0
  85. package/dist/components/Footer.js +59 -0
  86. package/dist/components/Footer.js.map +1 -0
  87. package/dist/components/Header.d.ts +11 -0
  88. package/dist/components/Header.d.ts.map +1 -0
  89. package/dist/components/Header.js +23 -0
  90. package/dist/components/Header.js.map +1 -0
  91. package/dist/components/Spinner.d.ts +3 -0
  92. package/dist/components/Spinner.d.ts.map +1 -0
  93. package/dist/components/Spinner.js +8 -0
  94. package/dist/components/Spinner.js.map +1 -0
  95. package/dist/components/Stream.d.ts +10 -0
  96. package/dist/components/Stream.d.ts.map +1 -0
  97. package/dist/components/Stream.js +13 -0
  98. package/dist/components/Stream.js.map +1 -0
  99. package/dist/components/ToolEvent.d.ts +13 -0
  100. package/dist/components/ToolEvent.d.ts.map +1 -0
  101. package/dist/components/ToolEvent.js +54 -0
  102. package/dist/components/ToolEvent.js.map +1 -0
  103. package/dist/credential-store.d.ts +8 -0
  104. package/dist/credential-store.d.ts.map +1 -0
  105. package/dist/credential-store.js +37 -0
  106. package/dist/credential-store.js.map +1 -0
  107. package/dist/git-workflow.d.ts +16 -0
  108. package/dist/git-workflow.d.ts.map +1 -0
  109. package/dist/git-workflow.js +38 -0
  110. package/dist/git-workflow.js.map +1 -0
  111. package/dist/mcp-loader.d.ts +25 -0
  112. package/dist/mcp-loader.d.ts.map +1 -0
  113. package/dist/mcp-loader.js +99 -0
  114. package/dist/mcp-loader.js.map +1 -0
  115. package/dist/notify.d.ts +12 -0
  116. package/dist/notify.d.ts.map +1 -0
  117. package/dist/notify.js +47 -0
  118. package/dist/notify.js.map +1 -0
  119. package/dist/options.d.ts +407 -0
  120. package/dist/options.d.ts.map +1 -0
  121. package/dist/options.js +792 -0
  122. package/dist/options.js.map +1 -0
  123. package/dist/repl-commands.d.ts +53 -0
  124. package/dist/repl-commands.d.ts.map +1 -0
  125. package/dist/repl-commands.js +141 -0
  126. package/dist/repl-commands.js.map +1 -0
  127. package/dist/repl.d.ts +52 -0
  128. package/dist/repl.d.ts.map +1 -0
  129. package/dist/repl.js +1174 -0
  130. package/dist/repl.js.map +1 -0
  131. package/dist/runner.d.ts +64 -0
  132. package/dist/runner.d.ts.map +1 -0
  133. package/dist/runner.js +61 -0
  134. package/dist/runner.js.map +1 -0
  135. package/dist/session-checkpoints.d.ts +6 -0
  136. package/dist/session-checkpoints.d.ts.map +1 -0
  137. package/dist/session-checkpoints.js +25 -0
  138. package/dist/session-checkpoints.js.map +1 -0
  139. package/dist/specialists-loader.d.ts +22 -0
  140. package/dist/specialists-loader.d.ts.map +1 -0
  141. package/dist/specialists-loader.js +38 -0
  142. package/dist/specialists-loader.js.map +1 -0
  143. package/dist/tool-display.d.ts +19 -0
  144. package/dist/tool-display.d.ts.map +1 -0
  145. package/dist/tool-display.js +57 -0
  146. package/dist/tool-display.js.map +1 -0
  147. package/package.json +70 -3
@@ -0,0 +1,792 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { Command, Option } from 'commander';
5
+ import { createExploreEditRouter, createFileAuditSink, detectProvider, defaultModelFor, expandModelAlias, HooksBus, loadForgeConfigFile, resolveAutoApprove, loadHooksConfig, loadSkillsRegistryAsync, loadSteering, loadToolSandboxConfig, mergeToolSandbox, resolveModelAlias, SPEC_READONLY_TOOLS, splitModelRef, } from '@ikarmic/forge-core';
6
+ const REASONING_EFFORTS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'];
7
+ /**
8
+ * Build the `forge` commander program. Kept in its own module so tests can
9
+ * poke at parsing without spawning a process.
10
+ */
11
+ export function buildProgram() {
12
+ const program = new Command();
13
+ const pkg = readPackageVersion();
14
+ program
15
+ .name('forge')
16
+ .description('IKarmic AI Forge: an agentic AI coding assistant.')
17
+ .version(pkg)
18
+ .argument('[task...]', 'plain-English task for the agent (omit to launch the interactive REPL)')
19
+ .option('-p, --provider <name>', 'provider: openai | anthropic | google | openrouter | ollama | minimax | bedrock | vertex | azure | lmstudio | openai-compatible')
20
+ .option('-m, --model <id>', 'model id within the chosen provider')
21
+ .option('-a, --auto', 'run autonomously without approval prompts', false)
22
+ .option('--allow-destructive', 'allow destructive tools (e.g. run_command) in --auto mode', false)
23
+ .option('-c, --cwd <path>', 'working directory for file tools', process.cwd())
24
+ .option('--max-iterations <n>', 'max agent iterations', (v) => parseInt(v, 10), 50)
25
+ .option('--max-duration <ms>', 'max wall-clock ms', (v) => parseInt(v, 10), 10 * 60 * 1000)
26
+ .option('--max-cost <usd>', 'stop the run once its cost reaches this many USD', (v) => parseFloat(v))
27
+ .option('--verify <cmd>', 'run this check after edits and self-fix on failure (repeatable)', (v, prev = []) => [...prev, v])
28
+ .option('--force', 'with `plugin install`: replace an installed plugin of the same name', false)
29
+ .option('--project', 'with `plugin install|remove`: use the project (.forge/plugins) instead of ~/.forge/plugins', false)
30
+ .option('--confirm-reads', 'ask before read-only tools too (by default read, list and search run without a prompt)', false)
31
+ .option('--verify-lsp', 'after edits, check the changed files with their language server and self-fix on errors', false)
32
+ .option('--no-project-instructions', 'do not load AGENTS.md / CLAUDE.md / CONVENTIONS.md into the system prompt')
33
+ .option('--dry-run', 'read-only run: the agent can read and search but cannot edit files, run commands or write memory; it describes what it would change', false)
34
+ .option('--notifications', 'send a desktop notification when a run finishes', false)
35
+ .option('--notifications-command <cmd>', 'run this command instead of the OS notifier (message in $FORGE_NOTIFY_MESSAGE); implies --notifications')
36
+ .option('--ai-comments', 'with `forge watch`: act on AI! (apply) and AI? (answer) comments when a file is saved', false)
37
+ .option('--audit', 'append a redacted record of every run and tool call to ~/.forge/audit.log')
38
+ .option('--browser', 'add browser tools: open a URL, screenshot, read console errors (needs Chrome/Edge/Chromium; FORGE_CHROME_PATH overrides)')
39
+ .option('--semantic-search', 'add the codebase_search tool (set FORGE_EMBED_PROVIDER=openai|ollama for meaning-based ranking)')
40
+ .option('--cheap-model <id>', 'route read-only exploration steps to this cheaper model (id, alias, or provider:model)')
41
+ .option('--no-cache-prompts', 'do not add prompt-cache breakpoints for Anthropic models')
42
+ .option('--architect', 'two-phase run: the main model plans read-only, then --editor-model applies the plan', false)
43
+ .option('--editor-model <id>', 'model (id, alias, or provider:model) that applies the architect plan')
44
+ .option('--editor-reasoning <level>', `reasoning effort for the editor model (${REASONING_EFFORTS.join(', ')}); requires --architect`)
45
+ .option('--auto-commit', 'commit what the agent changed after each run, with a drafted message (undo with /undo in the REPL)', false)
46
+ .option('--no-dirty-commits', 'with --auto-commit: do not commit your uncommitted work before the run; it stays out of the agent commit')
47
+ .option('--no-attribute', 'with --auto-commit: do not append "(forge)" to the commit author name')
48
+ .option('--git-commit-verify', 'with --auto-commit: run pre-commit hooks on Forge commits (skipped by default)', false)
49
+ .option('--map-tokens <n>', 'put a ranked map of the codebase (about <n> tokens) in the system prompt; 0 turns it off')
50
+ .option('--auto-lint', 'syntax-check each file the agent writes and feed problems back to it', false)
51
+ .option('--lint-cmd <cmd>', 'linter run on each file the agent writes, e.g. "eslint --fix" or "python: flake8" (repeatable; implies --auto-lint)', (v, prev = []) => [...prev, v])
52
+ .option('--system <prompt>', 'override the default system prompt')
53
+ .option('--mcp-config <path>', 'path to a forge.mcp.json file describing MCP servers to connect to')
54
+ .option('--resume [id]', "resume a saved session by id, or omit the value / pass 'latest' for the most recent")
55
+ .option('--json', 'emit structured JSON events instead of a TUI (useful for scripting)', false)
56
+ .option('-v, --verbose', 'enable verbose logging to stderr', false)
57
+ .option('--no-cost', 'disable cost tracking and display')
58
+ .option('--no-redact', 'disable secret redaction in tool output')
59
+ .option('--no-memory', 'disable project memory loading and the memory_read/memory_write tools')
60
+ .option('--plan', 'produce a numbered plan as text and stop (no tool execution); CLI prompts to approve/edit/cancel', false)
61
+ .option('--spec', 'generate requirements.md/design.md/tasks.md for the task (read-only exploration, prints text, exits 0)', false)
62
+ .option('--enable-compaction', 'enable automatic context compaction when approaching the model window', false)
63
+ .option('--compact-at <fraction>', 'compaction threshold (fraction of model window); default 0.7', (v) => parseFloat(v))
64
+ // Tool sandbox (allow/deny + per-tool policies)
65
+ .option('--allow-tools <list>', 'comma-separated whitelist of tool names (only these are exposed to the model)')
66
+ .option('--deny-tools <list>', 'comma-separated blacklist of tool names (these are removed from the model)')
67
+ .option('--allow-paths <list>', 'comma-separated glob patterns; file tools may only operate on matching paths')
68
+ .option('--deny-paths <list>', 'comma-separated glob patterns; file tools refuse paths matching any')
69
+ .option('--allow-commands <list>', 'comma-separated regexes; run_command requires command to match at least one')
70
+ .option('--deny-commands <list>', 'comma-separated regexes; run_command refuses commands matching any')
71
+ .option('--allow-urls <list>', 'comma-separated regexes; fetch_url requires URL to match at least one')
72
+ .option('--deny-urls <list>', 'comma-separated regexes; fetch_url refuses URLs matching any')
73
+ .option('--schema <path>', 'structured-output mode: path to a Zod schema (.ts/.js/.mjs) or JSON Schema (.json). ' +
74
+ 'Agent retries its final response until it matches. Combine with --json for batch/CI use.')
75
+ .option('--schema-retries <n>', 'max validation retries in --schema mode (default 3)', (v) => parseInt(v, 10), 3)
76
+ .option('--orchestrator-worktrees <mode>', 'git worktree isolation for parallel specialists: auto | always | never (default auto). ' +
77
+ 'When auto, only steps in a parallelGroup (>1 step) get a worktree.', (v) => {
78
+ if (v === 'auto' || v === 'always' || v === 'never')
79
+ return v;
80
+ throw new Error(`--orchestrator-worktrees must be auto|always|never (got "${v}")`);
81
+ }, 'auto')
82
+ .option('--orchestrator-commit', 'commit each worktree specialist\'s edits onto its branch and keep the branch, so the work can be reviewed (git diff) and merged (git merge --no-ff)')
83
+ .option('--orchestrator-cleanup-branches', 'after the orchestrator runs, delete the auto-created forge/<label>-<sha> branches ' +
84
+ 'alongside their worktrees (skipped if another worktree still uses the branch). Default enabled. Pass --no-... to keep them.', true)
85
+ .option('--with-branches', 'forge worktree cleanup: also delete the auto-created forge/<label>-<sha> branches alongside their worktrees. Default enabled. Pass --no-branches to keep them. v2.14.', true)
86
+ .option('--format <fmt>', 'output format for `forge session export` (md | json)', 'md')
87
+ .option('--output <path>', 'write to a file instead of stdout (used by `forge session export`)')
88
+ .option('--context <n>', 'context lines for `forge diff` (default 3)', (v) => parseInt(v, 10))
89
+ .option('--summary', 'print only the one-line summary for `forge diff`', false)
90
+ .option('--from-stdin', 'read the second input from stdin (used by `forge diff`)', false)
91
+ .addOption(new Option('--list-models', 'print the resolved model and exit').conflicts(['task']));
92
+ // We don't register `session` as a commander subcommand: commander's
93
+ // strict subcommand routing would reject positional task arguments that
94
+ // happen to start with words like "summarize". Instead, we treat
95
+ // `forge session …` as a soft prefix and inspect `program.args` manually
96
+ // after parsing.
97
+ return program;
98
+ }
99
+ /**
100
+ * Parse argv into typed options. Model resolution is **lazy**: the caller
101
+ * invokes {@link resolveModelOrThrow} only when an actual model is needed
102
+ * (i.e. when an agent run is about to start). This lets `forge session list`,
103
+ * `forge session show`, etc. work without an AI provider configured.
104
+ */
105
+ /** `--map-tokens`: a non-negative integer; 0 or absent means off. */
106
+ export function parseMapTokens(raw) {
107
+ if (raw === undefined)
108
+ return null;
109
+ const n = typeof raw === 'string' && /^\d+$/.test(raw.trim()) ? Number(raw) : NaN;
110
+ if (!Number.isInteger(n) || n < 0)
111
+ throw new Error(`--map-tokens must be a non-negative integer, got "${String(raw)}"`);
112
+ return n === 0 ? null : n;
113
+ }
114
+ /** Language names accepted before the colon in `--lint-cmd "python: flake8"`. */
115
+ const LINT_LANGUAGE_EXTENSIONS = {
116
+ typescript: ['ts', 'tsx'],
117
+ javascript: ['js', 'jsx', 'mjs', 'cjs'],
118
+ python: ['py'],
119
+ go: ['go'],
120
+ rust: ['rs'],
121
+ java: ['java'],
122
+ };
123
+ /**
124
+ * `--lint-cmd` values into extension -> command. `"ts: eslint"` and `"typescript: eslint"`
125
+ * apply to those files only; a value with no `name:` prefix applies to every file (`*`).
126
+ * A colon not followed by a space (a Windows path) is part of the command.
127
+ */
128
+ export function parseLintCommands(values) {
129
+ const out = {};
130
+ for (const value of values) {
131
+ const m = /^([A-Za-z][A-Za-z0-9]*):\s+(\S.*)$/.exec(value.trim());
132
+ if (!m) {
133
+ const cmd = value.trim();
134
+ if (!cmd)
135
+ throw new Error('--lint-cmd needs a command');
136
+ out['*'] = cmd;
137
+ continue;
138
+ }
139
+ const key = m[1].toLowerCase();
140
+ for (const ext of LINT_LANGUAGE_EXTENSIONS[key] ?? [key])
141
+ out[ext] = m[2].trim();
142
+ }
143
+ return out;
144
+ }
145
+ export function parseOptions(argv) {
146
+ const program = buildProgram();
147
+ program.parse(argv);
148
+ const opts = program.opts();
149
+ const listModels = opts.listModels === true;
150
+ const taskParts = program.args;
151
+ const task = taskParts.length > 0 ? taskParts.join(' ') : null;
152
+ // commander's `.conflicts()` can only reference other registered Options,
153
+ // never a positional `.argument(...)`: so `--list-models` combined with
154
+ // a task string would otherwise silently discard the task. Check by hand.
155
+ if (listModels && task) {
156
+ throw new Error(`--list-models cannot be combined with a task ("${task}")`);
157
+ }
158
+ if (opts.editorReasoning !== undefined) {
159
+ if (opts.architect !== true)
160
+ throw new Error('--editor-reasoning requires --architect');
161
+ if (!REASONING_EFFORTS.includes(opts.editorReasoning)) {
162
+ throw new Error(`--editor-reasoning must be one of: ${REASONING_EFFORTS.join(', ')}`);
163
+ }
164
+ }
165
+ if (opts.architect === true) {
166
+ if (typeof opts.editorModel !== 'string' || opts.editorModel.length === 0) {
167
+ throw new Error('--architect requires --editor-model <id|alias>');
168
+ }
169
+ if (opts.plan === true || opts.spec === true) {
170
+ throw new Error('--architect cannot be combined with --plan or --spec');
171
+ }
172
+ if (opts.resume !== undefined || opts.schema !== undefined) {
173
+ throw new Error('--architect cannot be combined with --resume or --schema');
174
+ }
175
+ if (!task) {
176
+ throw new Error('--architect needs a task (it does not apply to the interactive REPL)');
177
+ }
178
+ }
179
+ const cliOptions = {
180
+ task,
181
+ provider: opts.provider ?? null,
182
+ model: opts.model ?? null,
183
+ auto: opts.auto === true,
184
+ allowDestructive: opts.allowDestructive === true,
185
+ cwd: opts.cwd ?? null,
186
+ maxIterations: opts.maxIterations ?? null,
187
+ maxDurationMs: opts.maxDuration ?? null,
188
+ maxCostUsd: typeof opts.maxCost === 'number' && Number.isFinite(opts.maxCost) && opts.maxCost > 0 ? opts.maxCost : null,
189
+ cheapModel: opts.cheapModel ?? null,
190
+ noCachePrompts: opts.cachePrompts === false,
191
+ architect: opts.architect === true,
192
+ editorModel: opts.editorModel ?? null,
193
+ editorReasoning: opts.editorReasoning ?? null,
194
+ autoCommit: opts.autoCommit === true,
195
+ noDirtyCommits: opts.dirtyCommits === false,
196
+ noAttribute: opts.attribute === false,
197
+ gitCommitVerify: opts.gitCommitVerify === true,
198
+ mapTokens: parseMapTokens(opts.mapTokens),
199
+ lintCommands: parseLintCommands(Array.isArray(opts.lintCmd) ? opts.lintCmd : []),
200
+ autoLint: opts.autoLint === true || (Array.isArray(opts.lintCmd) && opts.lintCmd.length > 0),
201
+ verify: Array.isArray(opts.verify) ? opts.verify : [],
202
+ verifyLsp: opts.verifyLsp === true,
203
+ confirmReads: opts.confirmReads === true,
204
+ dryRun: opts.dryRun === true,
205
+ notifications: opts.notifications === true || typeof opts.notificationsCommand === 'string',
206
+ notificationsCommand: opts.notificationsCommand ?? null,
207
+ aiComments: opts.aiComments === true,
208
+ noProjectInstructions: opts.projectInstructions === false,
209
+ semanticSearch: opts.semanticSearch === true,
210
+ browser: opts.browser === true,
211
+ audit: opts.audit === true,
212
+ verbose: opts.verbose === true,
213
+ systemPrompt: opts.system ?? null,
214
+ json: opts.json === true,
215
+ mcpConfig: opts.mcpConfig ?? null,
216
+ noCost: opts.cost === false,
217
+ noRedact: opts.redact === false,
218
+ noMemory: opts.memory === false,
219
+ plan: opts.plan === true,
220
+ spec: opts.spec === true,
221
+ enableCompaction: opts.enableCompaction === true,
222
+ compactAt: typeof opts.compactAt === 'number' ? opts.compactAt : null,
223
+ allowTools: opts.allowTools ?? null,
224
+ denyTools: opts.denyTools ?? null,
225
+ allowPaths: opts.allowPaths ?? null,
226
+ denyPaths: opts.denyPaths ?? null,
227
+ allowCommands: opts.allowCommands ?? null,
228
+ denyCommands: opts.denyCommands ?? null,
229
+ allowUrls: opts.allowUrls ?? null,
230
+ denyUrls: opts.denyUrls ?? null,
231
+ schema: opts.schema ?? null,
232
+ schemaRetries: opts.schemaRetries ?? 3,
233
+ orchestratorWorktrees: opts.orchestratorWorktrees ?? 'auto',
234
+ orchestratorCleanupBranches: opts.orchestratorCleanupBranches ?? true,
235
+ orchestratorCommit: opts.orchestratorCommit === true,
236
+ };
237
+ const sessionCommand = extractSessionCommand(program);
238
+ const skillCommand = extractSkillCommand(program, opts);
239
+ const pluginCommand = extractPluginCommand(program, opts);
240
+ const hookCommand = extractHookCommand(program);
241
+ const mcpCommand = extractMcpCommand(program, opts);
242
+ const checkpointCommand = extractCheckpointCommand(program, opts);
243
+ const specCommand = extractSpecCommand(program, opts);
244
+ // `forge watch` has no sub-forms (see options.ts's header note on
245
+ // ParsedWatchCommand vs. a plain boolean): a single positional-word
246
+ // check, not a soft-prefix parser function.
247
+ const watchCommand = program.args[0] === 'watch';
248
+ const diffCommand = extractDiffCommand(program, opts);
249
+ const orchestratorCommand = extractOrchestratorCommand(program);
250
+ const worktreeCommand = extractWorktreeCommand(program, opts);
251
+ const configCommand = extractConfigCommand(program);
252
+ const acpCommand = program.args[0] === 'acp';
253
+ const policyCommand = program.args[0] === 'policy';
254
+ const resolveModel = () => resolveModelNow(cliOptions);
255
+ return {
256
+ options: cliOptions,
257
+ resolveModel,
258
+ listModels,
259
+ sessionCommand,
260
+ skillCommand,
261
+ pluginCommand,
262
+ hookCommand,
263
+ mcpCommand,
264
+ checkpointCommand,
265
+ specCommand,
266
+ watchCommand,
267
+ diffCommand,
268
+ orchestratorCommand,
269
+ worktreeCommand,
270
+ configCommand,
271
+ acpCommand,
272
+ policyCommand,
273
+ version: readPackageVersion(),
274
+ };
275
+ }
276
+ /** Soft-prefix parser for `forge config` (no sub-forms: always just runs the wizard). */
277
+ function extractConfigCommand(program) {
278
+ const args = program.args;
279
+ if (args[0] !== 'config')
280
+ return null;
281
+ return { run: true };
282
+ }
283
+ /**
284
+ * Rewrite `--model` through the user's aliases (`~/.forge/config.json`).
285
+ * A `provider:` prefix (typed directly or from an alias) also sets the provider. Cheap and
286
+ * editor models are resolved later, against the main model's provider.
287
+ */
288
+ export function applyModelAliases(options, aliases) {
289
+ if (!options.model)
290
+ return;
291
+ const { provider, model } = splitModelRef(expandModelAlias(options.model, aliases));
292
+ options.model = model;
293
+ if (provider)
294
+ options.provider = provider;
295
+ }
296
+ function resolveModelNow(opts) {
297
+ if (opts.provider && opts.model) {
298
+ return { provider: opts.provider, model: opts.model };
299
+ }
300
+ const provider = opts.provider ?? detectProvider();
301
+ if (!provider) {
302
+ throw new Error('No AI provider configured. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, ' +
303
+ 'OPENROUTER_API_KEY, or MINIMAX_API_KEY (or run `ollama serve` for local models), or pass --provider and --model explicitly.');
304
+ }
305
+ const model = opts.model ?? process.env.FORGE_DEFAULT_MODEL ?? defaultModelFor(provider);
306
+ return { provider, model };
307
+ }
308
+ /**
309
+ * Read the package version from package.json so `forge --version` stays in
310
+ * sync with whatever we shipped. Falls back to "0.0.0" if package.json is
311
+ * missing or unreadable (e.g. someone ran the CLI directly from a checked-out
312
+ * tree without `pnpm build`).
313
+ *
314
+ * Note: we walk the path manually rather than `new URL('../package.json',
315
+ * import.meta.url)`. The URL resolver mishandles Windows drive letters: for
316
+ * `file:///D:/.../dist/options.js`, `..` crosses the `/D:/.../` boundary and
317
+ * lands at the wrong directory. Same goes for `path.join` on Windows when
318
+ * the path is reached via a non-`path.resolve` call.
319
+ */
320
+ function readPackageVersion() {
321
+ try {
322
+ const here = fileURLToPath(import.meta.url);
323
+ // here = `<...>/packages/cli/dist/options.js`
324
+ // pkgPath = `<...>/packages/cli/package.json`
325
+ const pkgPath = join(dirname(here), '..', 'package.json');
326
+ const text = readFileSync(pkgPath, 'utf8');
327
+ const parsed = JSON.parse(text);
328
+ return typeof parsed.version === 'string' ? parsed.version : '0.0.0';
329
+ }
330
+ catch {
331
+ return '0.0.0';
332
+ }
333
+ }
334
+ function extractSessionCommand(program) {
335
+ const args = program.args;
336
+ const opts = program.opts();
337
+ const resumeFlag = parseResumeFlag(opts.resume);
338
+ // Path 1: explicit `forge session <subcommand> [id] [--flag]` invocation.
339
+ // We handle this as a "soft prefix" so it doesn't collide with positional
340
+ // task arguments (`forge summarize files` must keep working).
341
+ if (args[0] === 'session') {
342
+ const sub = args[1];
343
+ const id = args[2];
344
+ if (sub === 'list' || sub === 'ls') {
345
+ // Tail flags go to the program-level opts (e.g. --limit). We pull
346
+ // `--limit` out of opts after the soft prefix; everything after the
347
+ // subcommand is appended to opts by `parseOptions` via `parse()`.
348
+ const limit = opts.limit ?? 20;
349
+ return { list: { limit } };
350
+ }
351
+ if (sub === 'show' && id) {
352
+ return { show: { id, full: opts.full === true } };
353
+ }
354
+ if (sub === 'delete' && id) {
355
+ return { delete: id };
356
+ }
357
+ if (sub === 'export' && id) {
358
+ const formatRaw = opts.format ?? 'md';
359
+ const format = formatRaw === 'json' ? 'json' : 'md';
360
+ const output = opts.output ?? undefined;
361
+ return { export: { id, format, output } };
362
+ }
363
+ if (sub === 'tail')
364
+ return { tail: { id: id ?? 'latest' } };
365
+ if (sub === 'resume') {
366
+ if (opts.latest === true)
367
+ return { resume: { kind: 'latest' } };
368
+ if (id)
369
+ return { resume: { kind: 'id', id } };
370
+ return { resume: { kind: 'latest' } };
371
+ }
372
+ return {};
373
+ }
374
+ // Path 2: top-level --resume flag.
375
+ if (resumeFlag.kind !== 'none') {
376
+ return { resume: resumeFlag };
377
+ }
378
+ return null;
379
+ }
380
+ function parseResumeFlag(raw) {
381
+ if (raw === undefined || raw === false)
382
+ return { kind: 'none' };
383
+ if (raw === true || raw === '')
384
+ return { kind: 'latest' };
385
+ if (raw === 'latest')
386
+ return { kind: 'latest' };
387
+ if (typeof raw === 'string')
388
+ return { kind: 'id', id: raw };
389
+ return { kind: 'none' };
390
+ }
391
+ /**
392
+ * Soft-prefix parser for `forge skill list|show|install|search|browse`.
393
+ * `forge skill install <source>`: `<source>` is a skill name (looked
394
+ * up in the marketplace catalog), a GitHub URL, or `owner/repo[#subdir]`
395
+ * shorthand.
396
+ */
397
+ function extractSkillCommand(program, opts) {
398
+ const args = program.args;
399
+ if (args[0] !== 'skill')
400
+ return null;
401
+ const sub = args[1];
402
+ if (sub === 'list' || sub === 'ls')
403
+ return { list: true };
404
+ if (sub === 'show' && args[2])
405
+ return { show: { name: args[2] } };
406
+ if (sub === 'install' && args[2]) {
407
+ return {
408
+ install: {
409
+ url: args[2],
410
+ overwrite: opts.force === true || opts.overwrite === true,
411
+ },
412
+ };
413
+ }
414
+ if (sub === 'search') {
415
+ const query = args.slice(2).join(' ').trim();
416
+ if (!query)
417
+ return {};
418
+ const limit = typeof opts.limit === 'number' ? opts.limit : null;
419
+ return { search: { query, limit } };
420
+ }
421
+ if (sub === 'browse')
422
+ return { browse: true };
423
+ return {};
424
+ }
425
+ /** Soft-prefix parser for `forge plugin …`. Returns `null` when the first positional arg isn't `plugin`. */
426
+ function extractPluginCommand(program, opts) {
427
+ const args = program.args;
428
+ if (args[0] !== 'plugin')
429
+ return null;
430
+ const sub = args[1];
431
+ const project = opts.project === true;
432
+ if (sub === 'list' || sub === 'ls')
433
+ return { list: true };
434
+ if (sub === 'show' && args[2])
435
+ return { show: { name: args[2] } };
436
+ if (sub === 'install' && args[2])
437
+ return { install: { source: args[2], overwrite: opts.force === true, project } };
438
+ if ((sub === 'remove' || sub === 'rm') && args[2])
439
+ return { remove: { name: args[2], project } };
440
+ return {};
441
+ }
442
+ /**
443
+ * Soft-prefix parser for `forge hook list|test|log`. Returns `null` when
444
+ * the first positional arg isn't `hook`.
445
+ */
446
+ function extractHookCommand(program) {
447
+ const args = program.args;
448
+ const opts = program.opts();
449
+ if (args[0] !== 'hook')
450
+ return null;
451
+ const sub = args[1];
452
+ if (sub === 'list' || sub === 'ls')
453
+ return { list: true };
454
+ if (sub === 'log') {
455
+ const limit = opts.limit ?? 20;
456
+ return { log: { limit } };
457
+ }
458
+ if (sub === 'test' && args[2]) {
459
+ return { test: { event: args[2], tool: args[3] } };
460
+ }
461
+ return {};
462
+ }
463
+ /**
464
+ * Soft-prefix parser for `forge mcp list|install|remove|browse`.
465
+ * `forge mcp install <source> [--overwrite]`: `<source>` is a GitHub
466
+ * URL or `owner/repo[#subdir]` shorthand. `--force` / `--overwrite`
467
+ * replaces an existing install.
468
+ */
469
+ function extractMcpCommand(program, opts) {
470
+ const args = program.args;
471
+ if (args[0] !== 'mcp')
472
+ return null;
473
+ const sub = args[1];
474
+ if (sub === 'list' || sub === 'ls')
475
+ return { list: true };
476
+ if (sub === 'browse')
477
+ return { browse: true };
478
+ if (sub === 'install' && args[2]) {
479
+ return {
480
+ install: {
481
+ source: args[2],
482
+ overwrite: opts.force === true || opts.overwrite === true,
483
+ },
484
+ };
485
+ }
486
+ if (sub === 'remove' && args[2]) {
487
+ return { remove: { name: args[2] } };
488
+ }
489
+ return {};
490
+ }
491
+ /**
492
+ * Soft-prefix parser for `forge checkpoint list|show|restore|prune`.
493
+ * Restore requires `--yes` (set via the parsed opts); we read it from
494
+ * the program-level option bag the same way `session delete` does.
495
+ */
496
+ function extractCheckpointCommand(program, opts) {
497
+ const args = program.args;
498
+ if (args[0] !== 'checkpoint')
499
+ return null;
500
+ const sub = args[1];
501
+ const sessionId = opts.session ?? undefined;
502
+ const base = sessionId ? { sessionId } : {};
503
+ if (sub === 'list' || sub === 'ls') {
504
+ const limit = typeof opts.limit === 'number' ? opts.limit : null;
505
+ return { ...base, list: { limit } };
506
+ }
507
+ if (sub === 'show' && args[2])
508
+ return { ...base, show: { id: args[2] } };
509
+ if (sub === 'restore' && args[2]) {
510
+ return {
511
+ ...base,
512
+ restore: { id: args[2], yes: opts.yes === true },
513
+ };
514
+ }
515
+ if (sub === 'prune') {
516
+ const keep = typeof opts.keep === 'number' ? opts.keep : null;
517
+ return { ...base, prune: { keep } };
518
+ }
519
+ return base;
520
+ }
521
+ /**
522
+ * Soft-prefix parser for `forge spec list|show|cancel`.
523
+ */
524
+ function extractSpecCommand(program, opts) {
525
+ const args = program.args;
526
+ if (args[0] !== 'spec')
527
+ return null;
528
+ const sub = args[1];
529
+ if (sub === 'list' || sub === 'ls')
530
+ return { list: true };
531
+ if (sub === 'show' && args[2])
532
+ return { show: { id: args[2] } };
533
+ if (sub === 'cancel' && args[2]) {
534
+ return { cancel: { id: args[2], yes: opts.yes === true } };
535
+ }
536
+ return {};
537
+ }
538
+ /**
539
+ * Soft-prefix parser for `forge orchestrator list|run|plan`.
540
+ * `run <task...>` and `plan <task...>` take the rest of argv as the task.
541
+ */
542
+ function extractOrchestratorCommand(program) {
543
+ const args = program.args;
544
+ if (args[0] !== 'orchestrator')
545
+ return null;
546
+ const sub = args[1];
547
+ if (sub === 'list' || sub === 'ls')
548
+ return { list: true };
549
+ const rest = args.slice(2).join(' ').trim();
550
+ if (sub === 'run' && rest)
551
+ return { run: { task: rest } };
552
+ if (sub === 'plan' && rest)
553
+ return { plan: { task: rest } };
554
+ return {};
555
+ }
556
+ /**
557
+ * Soft-prefix parser for `forge diff <fileA> <fileB> [--from-stdin]`.
558
+ * Supports comparing a file to another file, or a file to stdin
559
+ * (the latter for `forge diff --from-stdin package.json < new.json`).
560
+ */
561
+ function extractDiffCommand(program, opts) {
562
+ const args = program.args;
563
+ if (args[0] !== 'diff')
564
+ return null;
565
+ const fromStdin = opts.fromStdin === true;
566
+ const fileA = args[1];
567
+ if (!fileA)
568
+ return null;
569
+ const fileB = fromStdin ? undefined : args[2];
570
+ const ctxRaw = opts.context;
571
+ const contextLines = typeof ctxRaw === 'number' ? ctxRaw : undefined;
572
+ const summaryOnly = opts.summary === true;
573
+ return {
574
+ fileA,
575
+ ...(fileB ? { fileB } : {}),
576
+ ...(contextLines !== undefined ? { contextLines } : {}),
577
+ ...(fromStdin ? { fromStdin: true } : {}),
578
+ ...(summaryOnly ? { summaryOnly: true } : {}),
579
+ };
580
+ }
581
+ /**
582
+ * Soft-prefix parser for `forge worktree list | cleanup | prune | sweep`.
583
+ * v2.11 + v2.14. Matches the other subcommand parsers in style.
584
+ */
585
+ function extractWorktreeCommand(program, opts) {
586
+ const args = program.args;
587
+ if (args[0] !== 'worktree')
588
+ return null;
589
+ const sub = args[1];
590
+ if (sub === 'list' || sub === 'ls')
591
+ return { list: true };
592
+ if (sub === 'cleanup') {
593
+ return {
594
+ cleanup: {
595
+ force: opts.force === true,
596
+ // v2.14: default to cleaning up branches. Pass --no-branches
597
+ // to keep them. The flag is registered as a boolean on the
598
+ // root program so commander normalises `--no-...` flips for us.
599
+ withBranches: opts.withBranches !== false,
600
+ },
601
+ };
602
+ }
603
+ if (sub === 'prune')
604
+ return { prune: true };
605
+ // v2.14: new one-shot cleanup.
606
+ if (sub === 'sweep') {
607
+ return { sweep: { force: opts.force === true } };
608
+ }
609
+ return {};
610
+ }
611
+ /**
612
+ * Convert parsed CLI options into an AgentConfig. The approval callback is
613
+ * supplied by the UI layer (it varies between TUI and JSON modes), so it's
614
+ * left as a parameter. `extraTools` (from MCP, sub-agents, etc.) are merged
615
+ * in here.
616
+ *
617
+ * Always loads skills + hooks from disk when not opted out. Skills are
618
+ * scanned from `<cwd>/skills/`, `<cwd>/.forge/skills/`, `~/.forge/skills/`.
619
+ * Hooks are loaded from `~/.forge/settings.json` + `<cwd>/.forge/settings.json`.
620
+ */
621
+ export async function toAgentConfig(options, model, approve, extraTools = []) {
622
+ const cwd = options.cwd ?? process.cwd();
623
+ const [skillsRegistry, hooksConfig, sandboxFromFile, sharedConfig, steering] = await Promise.all([
624
+ loadSkillsRegistryAsync(cwd),
625
+ loadHooksConfig(cwd),
626
+ // CLI never trusts project-level settings: matches the existing
627
+ // hooks precedent (trustedProjectHooks is never true in the CLI).
628
+ loadToolSandboxConfig(cwd, { trustedProjectSettings: false }),
629
+ loadForgeConfigFile(),
630
+ loadSteering(cwd),
631
+ ]);
632
+ for (const w of steering.warnings)
633
+ process.stderr.write(`forge: ${w}\n`);
634
+ const cliSandbox = buildSandbox(options) ?? {};
635
+ const sandbox = mergeToolSandbox(sandboxFromFile.sandbox, cliSandbox);
636
+ // Populate reasoningEffort onto the actual ModelConfig handed to Agent:
637
+ // it's stored/editable via both settings UIs but nothing wired it onto
638
+ // the running model until now. Mutates the caller's `model` object (the
639
+ // same reference used for both AgentConfig.model and the model prop
640
+ // threaded to App/Repl) so the effect reaches both.
641
+ if (!model.reasoningEffort && sharedConfig.config.reasoningEffort) {
642
+ model.reasoningEffort = sharedConfig.config.reasoningEffort;
643
+ }
644
+ return {
645
+ model,
646
+ cwd: options.cwd ?? undefined,
647
+ auto: options.auto,
648
+ ...(options.confirmReads ? {} : { autoApproveTools: resolveAutoApprove(sharedConfig.config.autoApproveTools) }),
649
+ allowDestructive: options.allowDestructive,
650
+ maxIterations: options.maxIterations ?? undefined,
651
+ maxDurationMs: options.maxDurationMs ?? undefined,
652
+ ...(options.maxCostUsd !== null ? { maxCostUsd: options.maxCostUsd } : {}),
653
+ ...(options.audit ? { audit: createFileAuditSink() } : {}),
654
+ ...(options.browser ? { browser: true } : {}),
655
+ ...(options.semanticSearch ? { semanticSearch: {} } : {}),
656
+ ...(options.verify.length > 0 || options.verifyLsp
657
+ ? { verify: { commands: options.verify, ...(options.verifyLsp ? { lsp: true } : {}) } }
658
+ : {}),
659
+ ...(options.mapTokens ? { repoMap: { tokens: options.mapTokens } } : {}),
660
+ ...(options.autoLint ? { autoLint: { commands: options.lintCommands } } : {}),
661
+ ...(options.noProjectInstructions ? { disableProjectInstructions: true } : {}),
662
+ ...(options.cheapModel
663
+ ? {
664
+ modelRouter: createExploreEditRouter({
665
+ strong: model,
666
+ cheap: { ...model, ...resolveModelAlias(options.cheapModel, sharedConfig.config.aliases, model.provider) },
667
+ }),
668
+ }
669
+ : {}),
670
+ ...(options.noCachePrompts ? { cachePrompts: false } : {}),
671
+ systemPrompt: options.systemPrompt ?? undefined,
672
+ extraTools,
673
+ approve,
674
+ redact: options.noRedact ? { enabled: false } : {},
675
+ disableCost: options.noCost,
676
+ disableMemory: options.noMemory,
677
+ planMode: options.plan,
678
+ enableCompaction: options.enableCompaction,
679
+ ...(options.compactAt !== null ? { compactionThreshold: options.compactAt } : {}),
680
+ toolSandbox: sandbox,
681
+ ...(steering.docs.length > 0 ? { steering: { docs: steering.docs } } : {}),
682
+ // One-shot --spec generation needs the read-only sandbox + specMode
683
+ // wired together: same "caller owns the sandbox" split as core's
684
+ // AgentConfig.specMode itself (see specs/generate.ts's header comment).
685
+ // Only takes effect when --spec was actually passed; a plain task run
686
+ // is unaffected. Comes after the plain `sandbox` spread above so it
687
+ // unconditionally overrides toolSandbox: spec mode's read-only
688
+ // restriction always wins over --allow-tools/--deny-tools, exactly
689
+ // like the REPL's /spec wiring (repl.tsx's runAgent overrides).
690
+ ...(options.spec
691
+ ? { specMode: true, toolSandbox: { allowTools: [...SPEC_READONLY_TOOLS] } }
692
+ : {}),
693
+ skills: { registry: skillsRegistry },
694
+ hooks: new HooksBus(hooksConfig.config),
695
+ };
696
+ }
697
+ /**
698
+ * Build a {@link ToolSandbox} from CLI flags. Returns undefined when no
699
+ * sandbox-related flag was supplied (so the agent runs unrestricted).
700
+ *
701
+ * Per-tool policies are layered on top of the path/command/url lists: any
702
+ * field that matches a file tool gets the path policy; run_command gets
703
+ * both the path policy (so --allow-paths/--deny-paths also cover it) and
704
+ * the command policy; fetch_url gets the URL policy. This keeps the CLI
705
+ * surface small while letting users do most of what they'd want.
706
+ */
707
+ export function buildSandbox(options) {
708
+ const allowTools = parseCsv(options.allowTools);
709
+ const denyTools = [...parseCsv(options.denyTools), ...(options.dryRun ? DRY_RUN_DENIED_TOOLS : [])];
710
+ const policies = {};
711
+ // Path policy: applied to every file tool. Maps allowedPaths/deniedPaths.
712
+ const allowedPaths = parseCsv(options.allowPaths);
713
+ const deniedPaths = parseCsv(options.denyPaths);
714
+ if (allowedPaths.length > 0 || deniedPaths.length > 0) {
715
+ const pathPolicy = {};
716
+ if (allowedPaths.length > 0)
717
+ pathPolicy.allowedPaths = allowedPaths;
718
+ if (deniedPaths.length > 0)
719
+ pathPolicy.deniedPaths = deniedPaths;
720
+ for (const name of FILE_TOOL_NAMES) {
721
+ policies[name] = { ...(policies[name] ?? {}), ...pathPolicy };
722
+ }
723
+ // --allow-paths/--deny-paths previously only reached the 5 file tools,
724
+ // so e.g. `--deny-paths "**/.env"` didn't stop `run_command({command:
725
+ // "cat .env"})`. Attach the same path policy here too: the sandbox
726
+ // check (checkPolicyViolation) evaluates deniedPaths/allowedPaths
727
+ // whenever the tool call supplies a resolvedPath, and this ensures
728
+ // run_command's policy entry carries the same patterns as the file
729
+ // tools even before/if run_command starts resolving paths out of its
730
+ // command text.
731
+ policies.run_command = { ...(policies.run_command ?? {}), ...pathPolicy };
732
+ }
733
+ // Command policy: applied to run_command.
734
+ const allowedCommands = parseRegexCsv(options.allowCommands);
735
+ const deniedCommands = parseRegexCsv(options.denyCommands);
736
+ if (allowedCommands.length > 0 || deniedCommands.length > 0) {
737
+ const cmdPolicy = {};
738
+ if (allowedCommands.length > 0)
739
+ cmdPolicy.allowedCommands = allowedCommands;
740
+ if (deniedCommands.length > 0)
741
+ cmdPolicy.deniedCommands = deniedCommands;
742
+ policies.run_command = { ...(policies.run_command ?? {}), ...cmdPolicy };
743
+ }
744
+ // URL policy: applied to fetch_url.
745
+ const allowedUrls = parseRegexCsv(options.allowUrls);
746
+ const deniedUrls = parseRegexCsv(options.denyUrls);
747
+ if (allowedUrls.length > 0 || deniedUrls.length > 0) {
748
+ const urlPolicy = {};
749
+ if (allowedUrls.length > 0)
750
+ urlPolicy.allowedUrls = allowedUrls;
751
+ if (deniedUrls.length > 0)
752
+ urlPolicy.deniedUrls = deniedUrls;
753
+ policies.fetch_url = { ...(policies.fetch_url ?? {}), ...urlPolicy };
754
+ }
755
+ if (allowTools.length === 0 && denyTools.length === 0 && Object.keys(policies).length === 0) {
756
+ return undefined;
757
+ }
758
+ const sandbox = {};
759
+ if (allowTools.length > 0)
760
+ sandbox.allowTools = allowTools;
761
+ if (denyTools.length > 0)
762
+ sandbox.denyTools = denyTools;
763
+ if (Object.keys(policies).length > 0)
764
+ sandbox.policies = policies;
765
+ return sandbox;
766
+ }
767
+ const FILE_TOOL_NAMES = ['read_file', 'write_file', 'edit_file', 'list_files', 'search_files'];
768
+ function parseCsv(raw) {
769
+ if (!raw)
770
+ return [];
771
+ return raw
772
+ .split(',')
773
+ .map((s) => s.trim())
774
+ .filter((s) => s.length > 0);
775
+ }
776
+ function parseRegexCsv(raw) {
777
+ if (!raw)
778
+ return [];
779
+ const out = [];
780
+ for (const part of parseCsv(raw)) {
781
+ try {
782
+ out.push(new RegExp(part));
783
+ }
784
+ catch (err) {
785
+ throw new Error(`Invalid regex in sandbox flag: "${part}": ${err.message}`);
786
+ }
787
+ }
788
+ return out;
789
+ }
790
+ /** Tools that change anything outside the process; denied under `--dry-run`. */
791
+ export const DRY_RUN_DENIED_TOOLS = ['write_file', 'edit_file', 'run_command', 'browser_act', 'memory_write'];
792
+ //# sourceMappingURL=options.js.map