@aragon-agent/cli 0.5.12 → 0.6.2

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 (63) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +322 -6
  3. package/dist/agent/agent-mode.d.ts +39 -0
  4. package/dist/agent/agent-mode.js +36 -0
  5. package/dist/agent/controller.d.ts +32 -0
  6. package/dist/agent/controller.js +25 -0
  7. package/dist/agent/system-prompt.d.ts +16 -0
  8. package/dist/agent/system-prompt.js +5 -0
  9. package/dist/cli.js +143 -2
  10. package/dist/config/ui-state.d.ts +29 -0
  11. package/dist/config/ui-state.js +14 -4
  12. package/dist/diagnostics/cli-commands.d.ts +10 -0
  13. package/dist/diagnostics/cli-commands.js +61 -0
  14. package/dist/diagnostics/doctor.d.ts +29 -0
  15. package/dist/diagnostics/doctor.js +207 -0
  16. package/dist/diagnostics/info.d.ts +30 -0
  17. package/dist/diagnostics/info.js +99 -0
  18. package/dist/exec/cli-commands.d.ts +31 -0
  19. package/dist/exec/cli-commands.js +63 -0
  20. package/dist/exec/emitter.d.ts +80 -0
  21. package/dist/exec/emitter.js +127 -0
  22. package/dist/exec/events.d.ts +226 -0
  23. package/dist/exec/events.js +43 -0
  24. package/dist/exec/index.d.ts +56 -0
  25. package/dist/exec/index.js +439 -0
  26. package/dist/exec/options.d.ts +76 -0
  27. package/dist/exec/options.js +221 -0
  28. package/dist/exec/permission.d.ts +66 -0
  29. package/dist/exec/permission.js +96 -0
  30. package/dist/exec/runner.d.ts +204 -0
  31. package/dist/exec/runner.js +559 -0
  32. package/dist/exec/stdin-stream.d.ts +46 -0
  33. package/dist/exec/stdin-stream.js +144 -0
  34. package/dist/logging/install.d.ts +22 -0
  35. package/dist/logging/install.js +22 -0
  36. package/dist/session/cli-commands.d.ts +30 -0
  37. package/dist/session/cli-commands.js +184 -0
  38. package/dist/session/persist.d.ts +46 -0
  39. package/dist/session/persist.js +8 -0
  40. package/dist/session/store.d.ts +147 -0
  41. package/dist/session/store.js +442 -0
  42. package/dist/team/runtime.d.ts +12 -0
  43. package/dist/team/runtime.js +6 -0
  44. package/dist/team/subagent.d.ts +12 -0
  45. package/dist/team/subagent.js +4 -0
  46. package/dist/tools/index.d.ts +20 -0
  47. package/dist/tools/index.js +26 -0
  48. package/dist/ui/ActivityLine.d.ts +16 -0
  49. package/dist/ui/ActivityLine.js +23 -1
  50. package/dist/ui/App.d.ts +14 -1
  51. package/dist/ui/App.js +61 -7
  52. package/dist/ui/BottomStatusRow.d.ts +33 -1
  53. package/dist/ui/BottomStatusRow.js +20 -6
  54. package/dist/ui/Composer.d.ts +14 -1
  55. package/dist/ui/Composer.js +5 -5
  56. package/dist/ui/overlays/HelpOverlay.js +12 -1
  57. package/dist/ui/use-startup-notices.d.ts +98 -4
  58. package/dist/ui/use-startup-notices.js +108 -2
  59. package/dist/ui/win-vt-force.d.ts +136 -0
  60. package/dist/ui/win-vt-force.js +237 -0
  61. package/dist/ui/win-vt-input.d.ts +57 -0
  62. package/dist/ui/win-vt-input.js +62 -0
  63. package/package.json +7 -3
@@ -0,0 +1,439 @@
1
+ /**
2
+ * `aragon exec` - wiring only (cli-integration-surface section 3.3 / 6.1).
3
+ *
4
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
5
+ *
6
+ * NO PROCESS-GLOBAL STATE, AND THAT IS A RULE RATHER THAN A HABIT (section 2.2).
7
+ * A programmatic Node API is the deferral most likely to be asked for first, and
8
+ * the only thing that makes it cheap later is `runExec()` holding nothing the
9
+ * process owns: the controller arrives through a factory, both streams are
10
+ * parameters, and the exit code is RETURNED rather than assigned.
11
+ *
12
+ * THE CONTROLLER COMES FROM A FACTORY BECAUSE `makeController` LIVES IN
13
+ * `cli.tsx` (see the note on `ExecControllerFactory`). Reaching for it directly
14
+ * would make `cli.tsx -> exec/cli-commands.ts -> exec/index.ts -> cli.tsx` a
15
+ * cycle, and duplicating it here would give the headless approval gates a second
16
+ * definition - the one thing `makeController`'s own comment says must not
17
+ * happen.
18
+ */
19
+ import { readFileSync } from 'node:fs';
20
+ import { randomUUID } from 'node:crypto';
21
+ import process from 'node:process';
22
+ import { runHeadless } from '../agent/headless.js';
23
+ import { computeCost } from '../agent/usage.js';
24
+ import { loadConfig } from '../config/load.js';
25
+ import { latestSessionForCwd, readSessionFile, resolveSessionRef, resumedSessionId, sessionPathFor, tryAcquireSessionLock, writeSession, } from '../session/store.js';
26
+ import { createEmitter } from './emitter.js';
27
+ import { resolveExecOptions } from './options.js';
28
+ import { resolvePermission } from './permission.js';
29
+ import { ExecRunner, singlePrompt } from './runner.js';
30
+ import { createStdinPromptSource, prefixSource } from './stdin-stream.js';
31
+ /**
32
+ * Run one `aragon exec` invocation and resolve with the process exit code.
33
+ *
34
+ * EXIT 2 MEANS THE RUN NEVER STARTED, AND STDOUT CARRIES NOTHING ON THAT PATH.
35
+ * Option errors, prompt errors and session errors are all reported on stderr
36
+ * alone: emitting a lone `result` line for them would break the guarantee
37
+ * `system/init` is the FIRST stream-json line (AC-5), and a wrapper that got a
38
+ * `result` without an `init` could not tell a run that failed to start from one
39
+ * that failed. A preflight failure is different - the format is settled and the
40
+ * schema has begun - so that one DOES emit a `result` with `exitCode: 2`.
41
+ */
42
+ export async function runExec(flags, raw, promptArg, ctx) {
43
+ const out = ctx.stdout ?? process.stdout;
44
+ const err = ctx.stderr ?? process.stderr;
45
+ const now = ctx.now ?? Date.now;
46
+ const resolved = resolveExecOptions(raw);
47
+ if (!resolved.ok) {
48
+ err.write(`${resolved.message}\n`);
49
+ return 2;
50
+ }
51
+ const options = resolved.options;
52
+ for (const warning of options.warnings)
53
+ err.write(`${warning}\n`);
54
+ const appended = readOptionalText(options.appendSystemPrompt, options.appendSystemPromptFile, err, '--append-system-prompt-file');
55
+ if (appended === null)
56
+ return 2;
57
+ const cwd = loadConfig(flags).cwd;
58
+ const plan = planSession(options, cwd, err);
59
+ if (!plan)
60
+ return 2;
61
+ const prompts = resolvePromptSource(options, promptArg, ctx, err);
62
+ if (!prompts)
63
+ return 2;
64
+ const lock = plan.saving ? tryAcquireSessionLock(plan.id) : null;
65
+ if (plan.saving && !lock) {
66
+ err.write(`session_busy: another aragon exec run holds session "${plan.id}". ` +
67
+ 'Retry, or use a different --session-id.\n');
68
+ return 2;
69
+ }
70
+ try {
71
+ return await execute({ flags, options, plan, prompts, ctx, out, err, now, appended });
72
+ }
73
+ finally {
74
+ lock?.release();
75
+ }
76
+ }
77
+ async function execute(input) {
78
+ const { options, plan, ctx, out, err, now } = input;
79
+ const startedAt = now();
80
+ const permission = resolvePermission({
81
+ mode: options.permissionMode,
82
+ allow: options.allowTools,
83
+ deny: options.denyTools,
84
+ });
85
+ const controller = ctx.makeController(withPermissionMode(input.flags, options.permissionMode), {
86
+ ...(permission ? { permission } : {}),
87
+ ...(input.appended ? { appendSystemPrompt: input.appended } : {}),
88
+ });
89
+ const emitter = createEmitter(options.outputFormat, out);
90
+ const runner = new ExecRunner({
91
+ emitter,
92
+ sessionId: plan.id,
93
+ ...(options.maxTurns !== undefined ? { maxTurns: options.maxTurns } : {}),
94
+ ...(options.maxDurationMs !== undefined ? { maxDurationMs: options.maxDurationMs } : {}),
95
+ partialMessages: options.partialMessages,
96
+ includeThinking: options.includeThinking,
97
+ followThrough: controller.getTodoConfig().followThrough,
98
+ quiet: options.quiet,
99
+ stderr: err,
100
+ now,
101
+ });
102
+ if (plan.saved) {
103
+ controller.replaceMessages(plan.saved.messages);
104
+ controller.restoreTodos(plan.saved.todos);
105
+ }
106
+ const config = controller.getConfig();
107
+ emitter.init({
108
+ sessionId: plan.id,
109
+ cli: ctx.version,
110
+ cwd: controller.getCwd(),
111
+ startedAt,
112
+ model: {
113
+ provider: config.provider,
114
+ id: config.model,
115
+ baseUrl: config.baseUrl ?? null,
116
+ },
117
+ permissionMode: options.permissionMode,
118
+ tools: controller.listTools().map((t) => t.name),
119
+ resumed: plan.saved !== null,
120
+ });
121
+ runner.attach(controller);
122
+ const pre = controller.preflight();
123
+ if (!pre.ok) {
124
+ runner.detach();
125
+ controller.dispose();
126
+ err.write(`${pre.message ?? 'Configuration error.'}\n`);
127
+ emitter.result(failedResult(plan.id, config, pre.message ?? 'Configuration error.', 'config', now() - startedAt));
128
+ return 2;
129
+ }
130
+ let exitCode = 0;
131
+ try {
132
+ exitCode = await drive(input, controller, runner);
133
+ }
134
+ finally {
135
+ runner.detach();
136
+ controller.dispose();
137
+ }
138
+ // PERSIST FIRST, ANNOUNCE SECOND (D-2). A caller that reads `result` and
139
+ // immediately respawns with the same `--session-id` must not race the writer
140
+ // of the file it is about to read.
141
+ const stats = runner.stats();
142
+ if (plan.saving)
143
+ persist(plan, controller, stats.turns, stats.usage, ctx.version, now());
144
+ const todo = controller.getTodoSnapshot?.() ?? null;
145
+ emitter.result({
146
+ sessionId: plan.id,
147
+ isError: exitCode === 1,
148
+ stopReason: stats.stopReason,
149
+ exitCode,
150
+ result: stats.lastAssistantText,
151
+ turns: stats.turns,
152
+ durationMs: now() - startedAt,
153
+ usage: {
154
+ inputTokens: stats.usage.inputTokens,
155
+ outputTokens: stats.usage.outputTokens,
156
+ totalTokens: stats.usage.inputTokens + stats.usage.outputTokens,
157
+ },
158
+ cost: buildCost(controller, stats.usage),
159
+ model: { provider: config.provider, id: config.model },
160
+ todos: todo ? { total: todo.total, done: todo.doneCount } : null,
161
+ error: stats.errorCode && stats.errorMessage
162
+ ? { code: stats.errorCode, message: stats.errorMessage }
163
+ : null,
164
+ });
165
+ return exitCode;
166
+ }
167
+ /**
168
+ * `text` DELEGATES TO `runHeadless` VERBATIM (D-1 / AC-1).
169
+ *
170
+ * Same controller, same `quiet`, same `followThrough`, same streams. That is the
171
+ * entire text path: reimplementing it inside the emitter would make "the human
172
+ * CLI is unchanged" an argument someone has to trust instead of a fact anyone
173
+ * can check, and the two would drift the first time a retry line changed.
174
+ */
175
+ async function drive(input, controller, runner) {
176
+ const { options, prompts, out, err } = input;
177
+ if (options.outputFormat !== 'text') {
178
+ await runner.run(prompts);
179
+ return runner.exitCode();
180
+ }
181
+ const first = await prompts.next();
182
+ if (first === null) {
183
+ err.write('No prompt provided for `aragon exec`.\n');
184
+ return 2;
185
+ }
186
+ const code = await runHeadless(controller, first, {
187
+ quiet: options.quiet,
188
+ followThrough: controller.getTodoConfig().followThrough,
189
+ stdout: out,
190
+ stderr: err,
191
+ });
192
+ // An interrupt still wins: `runHeadless` cannot know a signal arrived, and
193
+ // reporting 0 for a run the user stopped would be a lie a shell script acts on.
194
+ return runner.stats().stopReason === 'interrupted' ? runner.exitCode() : code;
195
+ }
196
+ /**
197
+ * `--permission-mode plan` IS `--plan`, and something has to carry it there.
198
+ *
199
+ * `resolvePermission` returns `undefined` for `plan` ON PURPOSE (P0-2): the mode
200
+ * contributes NOTHING to the registration filter, because `--plan` does not
201
+ * unregister the five in `PLAN_MODE_BLOCKED_TOOLS` - it registers them and
202
+ * refuses at call time, which is what keeps the refusal text that steers the
203
+ * model to `submit_plan`. But that leaves the mode with no carrier at all unless
204
+ * something turns it on, and `AgentController` reads it from exactly ONE place:
205
+ * `config.startInPlanMode`, which `loadConfig` resolves from `flags.plan`.
206
+ *
207
+ * WITHOUT THIS LINE THE FLAG IS A SILENT NO-OP OF THE WORST KIND (IF-6). The run
208
+ * would report `permissionMode: "plan"` in `system/init` while the plan gate was
209
+ * never armed and the system prompt carried no plan block - so a caller who
210
+ * asked for "read-only research and a written plan", the README's own example,
211
+ * would get a run that edits files, with nothing anywhere reporting it. Note
212
+ * that comparing `listTools()` against `-p --plan` does NOT catch it: both
213
+ * spellings register the same names either way, because the plan gate wraps
214
+ * rather than removes.
215
+ *
216
+ * ONLY `plan` TOUCHES THE FLAGS. `auto` and `strict` return the SAME object, so
217
+ * every other path builds today's config exactly - which is what keeps text mode
218
+ * byte-identical to `-p` (AC-1), and leaves a user whose `config.json` sets
219
+ * `planModeDefault: true` with the plan mode `-p` would have given them.
220
+ */
221
+ function withPermissionMode(flags, mode) {
222
+ return mode === 'plan' ? { ...flags, plan: true } : flags;
223
+ }
224
+ function buildCost(controller, usage) {
225
+ const config = controller.getConfig();
226
+ const known = controller.isPricedModel({
227
+ providerId: config.provider,
228
+ modelId: config.model,
229
+ });
230
+ // `amount` is `0` when the price is unknown, mirroring the fast tier's rule
231
+ // that a feature which looks free while it is spending money is worse than one
232
+ // that admits it does not know.
233
+ return {
234
+ amount: known ? computeCost(usage, controller.getModelInfo().cost) : 0,
235
+ currency: 'USD',
236
+ known,
237
+ };
238
+ }
239
+ function failedResult(sessionId, config, message, code, durationMs) {
240
+ return {
241
+ sessionId,
242
+ isError: true,
243
+ stopReason: 'error',
244
+ exitCode: 2,
245
+ result: '',
246
+ turns: 0,
247
+ durationMs,
248
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
249
+ cost: { amount: 0, currency: 'USD', known: false },
250
+ model: { provider: config.provider, id: config.model },
251
+ todos: null,
252
+ error: { code, message },
253
+ };
254
+ }
255
+ // ---------------------------------------------------------------------------
256
+ // Prompt resolution (section 4.1)
257
+ // ---------------------------------------------------------------------------
258
+ /**
259
+ * Positional argument, then `--prompt-file`, then stdin - first match wins.
260
+ *
261
+ * Returns `null` after writing the reason to stderr, so the caller can exit 2
262
+ * without a second error vocabulary.
263
+ */
264
+ function resolvePromptSource(options, promptArg, ctx, err) {
265
+ const stdin = ctx.stdin ?? process.stdin;
266
+ const opening = [];
267
+ const positional = promptArg?.trim();
268
+ if (positional)
269
+ opening.push(positional);
270
+ else if (options.promptFile) {
271
+ const text = readTextFile(options.promptFile, err, '--prompt-file');
272
+ if (text === null)
273
+ return null;
274
+ if (text.trim().length > 0)
275
+ opening.push(text);
276
+ }
277
+ if (options.inputFormat === 'stream-json') {
278
+ return prefixSource(opening, createStdinPromptSource({
279
+ input: stdin,
280
+ // Reported on stderr rather than as a schema event, because the runner
281
+ // owns the emitter and this source is constructed before it exists. The
282
+ // line is skipped either way, which is the property AC-20 pins.
283
+ onBadLine: (message) => err.write(`${message}\n`),
284
+ // The runner installs the signal terminator; a caller-requested
285
+ // interrupt is the same intent arriving through a different channel.
286
+ onInterrupt: () => process.kill(process.pid, 'SIGINT'),
287
+ }));
288
+ }
289
+ if (opening.length === 0 && !stdinIsTty(stdin)) {
290
+ // Piped stdin, `--input-format text`: the whole of stdin is the prompt.
291
+ return { next: makeOnce(() => readAllSync(stdin)) };
292
+ }
293
+ if (opening.length === 0) {
294
+ err.write('no_prompt: pass a prompt argument, --prompt-file <path>, or pipe one on stdin.\n');
295
+ return null;
296
+ }
297
+ return singlePrompt(opening[0]);
298
+ }
299
+ function makeOnce(read) {
300
+ let served = false;
301
+ return async () => {
302
+ if (served)
303
+ return null;
304
+ served = true;
305
+ return read();
306
+ };
307
+ }
308
+ function stdinIsTty(stdin) {
309
+ return stdin.isTTY === true;
310
+ }
311
+ async function readAllSync(stdin) {
312
+ const chunks = [];
313
+ for await (const chunk of stdin) {
314
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
315
+ }
316
+ const text = Buffer.concat(chunks).toString('utf-8').trim();
317
+ return text.length > 0 ? text : null;
318
+ }
319
+ /** `''` when neither source is set, `null` when a file could not be read. */
320
+ function readOptionalText(inline, file, err, flag) {
321
+ const parts = [];
322
+ if (inline)
323
+ parts.push(inline);
324
+ if (file) {
325
+ const text = readTextFile(file, err, flag);
326
+ if (text === null)
327
+ return null;
328
+ parts.push(text);
329
+ }
330
+ return parts.join('\n\n');
331
+ }
332
+ function readTextFile(path, err, flag) {
333
+ try {
334
+ return readFileSync(path, 'utf-8');
335
+ }
336
+ catch (e) {
337
+ err.write(`${flag}: could not read ${path} (${e.message}).\n`);
338
+ return null;
339
+ }
340
+ }
341
+ /**
342
+ * The three resume forms, and how they differ:
343
+ *
344
+ * `--session-id <id>` resume it, or CREATE it (the wrapper-friendly upsert)
345
+ * `--resume <id|path>` resume it, or exit 2 `session_not_found`
346
+ * `--continue` the newest session for this cwd, or exit 2
347
+ *
348
+ * Returns `null` after writing the reason to stderr.
349
+ */
350
+ function planSession(options, cwd, err) {
351
+ const saving = options.saveSession;
352
+ if (options.resume !== undefined) {
353
+ const path = resolveSessionRef(options.resume, cwd);
354
+ const saved = path ? readSessionFile(path) : null;
355
+ if (!path || !saved) {
356
+ err.write(`session_not_found: no readable session for --resume "${options.resume}".\n`);
357
+ return null;
358
+ }
359
+ // NOT `options.resume` AND NOT `meta.id` UNCHECKED (R-7). The flag accepts
360
+ // `<id|path>`, so the value the caller typed is frequently a path, and
361
+ // `meta.id` comes out of a file - neither is usable as a filename without
362
+ // being checked. `resumedSessionId` is the one place that decision is made.
363
+ return { id: resumedSessionId(path, saved), saved, saving, cwd };
364
+ }
365
+ if (options.continueSession) {
366
+ const entry = latestSessionForCwd(cwd);
367
+ const saved = entry ? readSessionFile(entry.path) : null;
368
+ if (!entry || !saved) {
369
+ err.write(`no_session_for_cwd: no previous aragon exec session for ${cwd}.\n`);
370
+ return null;
371
+ }
372
+ return { id: resumedSessionId(entry.path, saved), saved, saving, cwd };
373
+ }
374
+ if (options.sessionId !== undefined) {
375
+ // THE UPSERT. A wrapper that mints its own id must not have to ask whether
376
+ // this is the first turn; that is the whole reason this form exists.
377
+ return { id: options.sessionId, saved: readSessionFile(sessionPathFor(options.sessionId)), saving, cwd };
378
+ }
379
+ return { id: randomUUID(), saved: null, saving, cwd };
380
+ }
381
+ /**
382
+ * Write the session, or leave the disk untouched.
383
+ *
384
+ * `entries: []` IS THE DECISION, NOT AN OMISSION (D-14 / P1-6). `Entry[]` is
385
+ * produced by the renderer's reducer driven by `App.tsx`; `AgentController`
386
+ * exposes `getMessages()` and no `getEntries()`, and the team / todo / fast /
387
+ * retry entries arrive through separate dispatch paths inside the App.
388
+ * Reconstructing a faithful transcript here means porting a meaningful slice of
389
+ * the renderer into the headless path, for a payoff nobody asked for.
390
+ *
391
+ * The alternative was never "leave it undecided": `[]` PASSES `loadSession`'s
392
+ * validation, so an implementer who skipped the work would ship a session that
393
+ * resumes in the TUI with full model memory and a BLANK TRANSCRIPT, with no
394
+ * error anywhere. Choosing that deliberately - and documenting it in the
395
+ * README's Limits - is better than arriving at it by accident.
396
+ */
397
+ function persist(plan, controller, turns, usage, cli, at) {
398
+ const previous = plan.saved?.meta;
399
+ const config = controller.getConfig();
400
+ const meta = {
401
+ id: plan.id,
402
+ cwd: controller.getCwd(),
403
+ createdAt: previous?.createdAt ?? at,
404
+ updatedAt: at,
405
+ // CUMULATIVE ACROSS INVOCATIONS. A wrapper budgeting a conversation cares
406
+ // about the conversation, not about the process that happened to run one
407
+ // turn of it.
408
+ turns: (previous?.turns ?? 0) + turns,
409
+ cli,
410
+ provider: config.provider,
411
+ model: config.model,
412
+ usage: {
413
+ inputTokens: (previous?.usage.inputTokens ?? 0) + usage.inputTokens,
414
+ outputTokens: (previous?.usage.outputTokens ?? 0) + usage.outputTokens,
415
+ },
416
+ };
417
+ try {
418
+ writeSession({
419
+ id: plan.id,
420
+ session: {
421
+ model: {
422
+ providerId: config.provider,
423
+ modelId: config.model,
424
+ ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
425
+ },
426
+ messages: controller.getMessages(),
427
+ entries: [],
428
+ todos: controller.getTodoSnapshot?.()?.items ?? [],
429
+ },
430
+ meta,
431
+ });
432
+ }
433
+ catch (e) {
434
+ // A run that produced an answer must not be reported as a failure because
435
+ // the home directory turned read-only. The caller still has the answer on
436
+ // stdout; the next invocation will simply start fresh.
437
+ process.stderr.write(`[session] could not save "${plan.id}": ${e.message}\n`);
438
+ }
439
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `aragon exec` option resolution (cli-integration-surface section 4.1).
3
+ *
4
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
5
+ *
6
+ * PURE. Every branch here is a decision about the FLAGS, never about the world:
7
+ * no file is read, no session is resolved, no stdin is consumed. Prompt
8
+ * resolution and file reads live in `exec/index.ts`, which is the module allowed
9
+ * to touch the process.
10
+ *
11
+ * A USAGE ERROR IS EXIT 2 AND NOTHING ON STDOUT. That is the whole reason this
12
+ * module returns a discriminated union instead of throwing: `--output-format
13
+ * json` promises exactly one JSON object on stdout, and a stack trace or a
14
+ * half-written event would corrupt every consumer. The caller writes the message
15
+ * to stderr and exits.
16
+ */
17
+ import type { ExecInputFormat, ExecOutputFormat, ExecPermissionMode } from './events.js';
18
+ /** Raw commander output. Every field is whatever the user typed, or absent. */
19
+ export interface RawExecOptions {
20
+ outputFormat?: string;
21
+ inputFormat?: string;
22
+ partialMessages?: boolean;
23
+ includeThinking?: boolean;
24
+ promptFile?: string;
25
+ sessionId?: string;
26
+ resume?: string;
27
+ continue?: boolean;
28
+ /** `--no-save-session` => `false`; absent => `undefined`. */
29
+ saveSession?: boolean;
30
+ maxTurns?: string;
31
+ maxDuration?: string;
32
+ /** The hidden `--timeout` alias of `--max-duration` (P2-1 / D-16). */
33
+ timeout?: string;
34
+ permissionMode?: string;
35
+ allowTool?: string[];
36
+ denyTool?: string[];
37
+ appendSystemPrompt?: string;
38
+ appendSystemPromptFile?: string;
39
+ /** The ROOT `-p/--print`, hoisted by commander. Refused (P2-5). */
40
+ print?: boolean;
41
+ quiet?: boolean;
42
+ }
43
+ export interface ExecOptions {
44
+ outputFormat: ExecOutputFormat;
45
+ inputFormat: ExecInputFormat;
46
+ partialMessages: boolean;
47
+ includeThinking: boolean;
48
+ promptFile?: string;
49
+ sessionId?: string;
50
+ resume?: string;
51
+ continueSession: boolean;
52
+ saveSession: boolean;
53
+ maxTurns?: number;
54
+ maxDurationMs?: number;
55
+ permissionMode: ExecPermissionMode;
56
+ allowTools: string[];
57
+ denyTools: string[];
58
+ appendSystemPrompt?: string;
59
+ appendSystemPromptFile?: string;
60
+ quiet: boolean;
61
+ /**
62
+ * Non-fatal notes for stderr. Empty on every well-formed invocation, so a
63
+ * caller can treat a non-empty list as "you asked for something inert".
64
+ */
65
+ warnings: string[];
66
+ }
67
+ export type ExecUsageCode = 'invalid_output_format' | 'invalid_input_format' | 'invalid_permission_mode' | 'invalid_max_turns' | 'invalid_max_duration' | 'invalid_session_id' | 'unknown_tool' | 'unsupported_in_text_mode' | 'input_format_requires_stream_json' | 'stream_only_flag' | 'session_flag_conflict' | 'print_not_supported';
68
+ export type ExecOptionsResult = {
69
+ ok: true;
70
+ options: ExecOptions;
71
+ } | {
72
+ ok: false;
73
+ code: ExecUsageCode;
74
+ message: string;
75
+ };
76
+ export declare function resolveExecOptions(raw: RawExecOptions): ExecOptionsResult;