@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,207 @@
1
+ /**
2
+ * `aragon doctor` - "is this machine configured to run me?", in one exit code
3
+ * (cli-integration-surface section 4.3).
4
+ *
5
+ * ASCII ONLY - `src/diagnostics/**` is inside the glyph scanner's scope.
6
+ *
7
+ * EVERY CHECK CARRIES A ONE-LINE REMEDY, and only `fail` affects the exit code.
8
+ * A `warn` that failed the command would make the command useless on the many
9
+ * machines where something is unusual but working; a `fail` with no remedy makes
10
+ * the caller guess, which is the state this command exists to end.
11
+ */
12
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import process from 'node:process';
15
+ import { initProviders, ModelRegistry } from '@aragon-agent/core';
16
+ import { getHomeResolutionWarning, getSessionsDir, getUserDataDir } from '../config/app-paths.js';
17
+ import { readConfigFile } from '../config/store.js';
18
+ import { loadConfig, makeGetApiKey } from '../config/load.js';
19
+ import { isAdapterProvider, toRetryPolicy } from '../config/schema.js';
20
+ import { MODE_TOGGLE_KEYS } from '../agent/agent-mode.js';
21
+ import { supportsWindowsVtInput } from '../ui/win-vt-input.js';
22
+ const MIN_NODE_MAJOR = 18;
23
+ export async function runDoctorChecks(flags, opts = {}) {
24
+ const checks = [checkNode(), ...checkVtInput(), checkHome(), checkConfigFile()];
25
+ const config = loadConfig(flags);
26
+ checks.push(checkProviderKey(config));
27
+ checks.push(checkModel(config));
28
+ checks.push(checkSessionsDir());
29
+ if (opts.probe)
30
+ checks.push(await probeProvider(config));
31
+ return { ok: checks.every((c) => c.verdict !== 'fail'), checks };
32
+ }
33
+ function pass(name, detail) {
34
+ return { name, verdict: 'pass', detail, remedy: '' };
35
+ }
36
+ function checkNode() {
37
+ const major = Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10);
38
+ if (major >= MIN_NODE_MAJOR)
39
+ return pass('node', `v${process.versions.node}`);
40
+ return {
41
+ name: 'node',
42
+ verdict: 'fail',
43
+ detail: `v${process.versions.node} is below the supported floor`,
44
+ remedy: `Install Node ${MIN_NODE_MAJOR} or newer.`,
45
+ };
46
+ }
47
+ /**
48
+ * Whether this console can deliver `Shift+Tab`
49
+ * (shift-tab-mode-toggle-still-dead-on-windows, C3-2).
50
+ *
51
+ * `MIN_NODE_MAJOR` STAYS AT 18 and this is a separate check on purpose. The
52
+ * install floor is about whether aragon runs; this is about one key, and raising
53
+ * the floor to 22.17 would lock out a large, otherwise perfectly healthy Node 20
54
+ * population over it.
55
+ *
56
+ * `warn`, never `fail`: aragon turns the console bit on itself at startup, and
57
+ * `MODE_TOGGLE_KEYS.fallback` works whether or not that succeeded, so nothing
58
+ * here is actually broken - the exit code must not say it is.
59
+ *
60
+ * WHY IT EXISTS AT ALL: `checkNode` passes every version this bug affects, so a
61
+ * user whose mode toggle is dead ran the tool built to explain their machine and
62
+ * was told it was fine (analysis section 3.7). A diagnostic that is confidently
63
+ * wrong is worse than one that is silent - it ends the investigation.
64
+ *
65
+ * Returned as a list so the row simply does not exist off Windows, where there
66
+ * is no such thing as a console input mode to report on.
67
+ */
68
+ function checkVtInput() {
69
+ if (process.platform !== 'win32')
70
+ return [];
71
+ if (supportsWindowsVtInput(process.platform, process.versions.node)) {
72
+ return [pass('vt-input', `v${process.versions.node} delivers Shift+Tab and mouse reports`)];
73
+ }
74
+ return [
75
+ {
76
+ name: 'vt-input',
77
+ verdict: 'warn',
78
+ detail: `Node v${process.versions.node} never enables ENABLE_VIRTUAL_TERMINAL_INPUT, so this ` +
79
+ 'console reports Shift+Tab as a plain Tab and sends no mouse events',
80
+ remedy: `aragon turns that bit on at startup; if a policy blocks it, ${MODE_TOGGLE_KEYS.fallback} ` +
81
+ 'and /plan toggle the mode too. Node 22.17+ (or 24.2+) removes the problem.',
82
+ },
83
+ ];
84
+ }
85
+ function checkHome() {
86
+ const home = getUserDataDir();
87
+ const warning = getHomeResolutionWarning();
88
+ try {
89
+ if (!existsSync(home))
90
+ mkdirSync(home, { recursive: true });
91
+ const probe = join(home, '.doctor-write-probe');
92
+ writeFileSync(probe, 'ok', 'utf-8');
93
+ unlinkSync(probe);
94
+ }
95
+ catch (err) {
96
+ return {
97
+ name: 'home',
98
+ verdict: 'fail',
99
+ detail: `${home} is not writable (${err.message})`,
100
+ remedy: 'Fix the permissions, or point ARAGON_HOME at a directory you own.',
101
+ };
102
+ }
103
+ if (warning) {
104
+ return {
105
+ name: 'home',
106
+ verdict: 'warn',
107
+ detail: `${home} (${warning})`,
108
+ remedy: 'Set ARAGON_HOME to an absolute path if this is not where you want state kept.',
109
+ };
110
+ }
111
+ return pass('home', home);
112
+ }
113
+ function checkConfigFile() {
114
+ const { parseError } = readConfigFile();
115
+ if (!parseError)
116
+ return pass('config', 'parses');
117
+ return {
118
+ name: 'config',
119
+ verdict: 'warn',
120
+ detail: `using defaults, could not parse ${parseError}`,
121
+ remedy: 'Run `aragon config edit` and fix the JSON, or delete the file to start over.',
122
+ };
123
+ }
124
+ function checkProviderKey(config) {
125
+ const key = makeGetApiKey(config)(config.provider);
126
+ if (key)
127
+ return pass('api-key', `resolved for ${config.provider}`);
128
+ return {
129
+ name: 'api-key',
130
+ verdict: 'fail',
131
+ detail: `no API key for provider "${config.provider}"`,
132
+ remedy: `Run \`aragon config\` and add one, or set the provider's key in the environment.`,
133
+ };
134
+ }
135
+ function checkModel(config) {
136
+ if (!isAdapterProvider(config.provider)) {
137
+ return {
138
+ name: 'model',
139
+ verdict: 'fail',
140
+ detail: `unknown provider "${config.provider}"`,
141
+ remedy: 'Run `aragon models` to see the providers this build supports.',
142
+ };
143
+ }
144
+ const registry = new ModelRegistry(initProviders({ retryPolicy: toRetryPolicy(config.retry) }));
145
+ const known = registry.getModels(config.provider).some((m) => m.id === config.model);
146
+ if (known)
147
+ return pass('model', `${config.provider}/${config.model}`);
148
+ // WARN, NOT FAIL. The registry is a builtin list plus whatever an account can
149
+ // discover, so a model it does not know is frequently one this account has and
150
+ // the shipped table does not - the cost table is unknown, the run is fine.
151
+ return {
152
+ name: 'model',
153
+ verdict: 'warn',
154
+ detail: `${config.provider}/${config.model} is not in the builtin registry`,
155
+ remedy: 'Run `aragon models` to list what this key can reach; cost reporting will be off.',
156
+ };
157
+ }
158
+ function checkSessionsDir() {
159
+ const dir = getSessionsDir();
160
+ try {
161
+ if (!existsSync(dir))
162
+ mkdirSync(dir, { recursive: true });
163
+ const probe = join(dir, '.doctor-write-probe');
164
+ writeFileSync(probe, 'ok', 'utf-8');
165
+ unlinkSync(probe);
166
+ }
167
+ catch (err) {
168
+ return {
169
+ name: 'sessions',
170
+ verdict: 'fail',
171
+ detail: `${dir} is not writable (${err.message})`,
172
+ remedy: 'Fix the permissions, or run `aragon exec --no-save-session`.',
173
+ };
174
+ }
175
+ return pass('sessions', dir);
176
+ }
177
+ /**
178
+ * `--probe`: one minimal API call.
179
+ *
180
+ * MODEL DISCOVERY IS THE CHEAPEST HONEST PROBE. It exercises the base URL, the
181
+ * key and the TLS chain - which is the whole of what "can this machine reach the
182
+ * provider" means - without spending a single completion token.
183
+ */
184
+ async function probeProvider(config) {
185
+ const key = makeGetApiKey(config)(config.provider);
186
+ if (!key || !isAdapterProvider(config.provider)) {
187
+ return {
188
+ name: 'probe',
189
+ verdict: 'warn',
190
+ detail: 'skipped: no key or unknown provider',
191
+ remedy: 'Fix the api-key check above, then run `aragon doctor --probe` again.',
192
+ };
193
+ }
194
+ const registry = new ModelRegistry(initProviders({ retryPolicy: toRetryPolicy(config.retry) }));
195
+ try {
196
+ const models = await registry.discoverModels(config.provider, key, config.baseUrl);
197
+ return pass('probe', `${config.provider} answered with ${models.length} model(s)`);
198
+ }
199
+ catch (err) {
200
+ return {
201
+ name: 'probe',
202
+ verdict: 'fail',
203
+ detail: `${config.provider} did not answer (${err.message})`,
204
+ remedy: 'Check the key, the base URL and any proxy between this machine and the provider.',
205
+ };
206
+ }
207
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `aragon info` - capability discovery (cli-integration-surface section 5.4).
3
+ *
4
+ * ASCII ONLY - `src/diagnostics/**` is inside the glyph scanner's scope.
5
+ *
6
+ * "INSTALL AND CALL" ACROSS VERSIONS IS ONLY ROBUST IF THE CALLER CAN ASK WHAT
7
+ * IT JUST INSTALLED (G6). Everything a wrapper might branch on is here, and
8
+ * `features` is a FLAT STRING LIST on purpose: a wrapper testing
9
+ * `info.features.includes('sessions')` keeps working across versions that add
10
+ * capabilities, which a nested shape with renamed keys would not.
11
+ */
12
+ import { type CliFlags } from '../config/load.js';
13
+ export interface ExecInfo {
14
+ cli: string;
15
+ core: string;
16
+ node: string;
17
+ schemaVersion: number;
18
+ home: string;
19
+ configPath: string;
20
+ sessionsDir: string;
21
+ provider: string;
22
+ model: string;
23
+ hasApiKey: boolean;
24
+ tools: string[];
25
+ outputFormats: string[];
26
+ inputFormats: string[];
27
+ permissionModes: string[];
28
+ features: string[];
29
+ }
30
+ export declare function buildInfo(flags: CliFlags, version: string): ExecInfo;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `aragon info` - capability discovery (cli-integration-surface section 5.4).
3
+ *
4
+ * ASCII ONLY - `src/diagnostics/**` is inside the glyph scanner's scope.
5
+ *
6
+ * "INSTALL AND CALL" ACROSS VERSIONS IS ONLY ROBUST IF THE CALLER CAN ASK WHAT
7
+ * IT JUST INSTALLED (G6). Everything a wrapper might branch on is here, and
8
+ * `features` is a FLAT STRING LIST on purpose: a wrapper testing
9
+ * `info.features.includes('sessions')` keeps working across versions that add
10
+ * capabilities, which a nested shape with renamed keys would not.
11
+ */
12
+ import { dirname, join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { readFileSync } from 'node:fs';
15
+ import process from 'node:process';
16
+ import { getConfigPath, getSessionsDir, getUserDataDir } from '../config/app-paths.js';
17
+ import { loadConfig, makeGetApiKey } from '../config/load.js';
18
+ import { HOST_TOOL_NAMES } from '../tools/index.js';
19
+ import { EXEC_SCHEMA_VERSION } from '../exec/events.js';
20
+ /**
21
+ * Everything this build can do, as names a wrapper can test for.
22
+ *
23
+ * ADD, NEVER RENAME. A removed name is a breaking change for every consumer that
24
+ * guarded on it, and the whole value of the list is that guarding is cheap.
25
+ */
26
+ const FEATURES = [
27
+ 'exec',
28
+ 'sessions',
29
+ 'permissions',
30
+ 'budgets',
31
+ 'team',
32
+ 'todo',
33
+ 'skills',
34
+ 'fast',
35
+ ];
36
+ export function buildInfo(flags, version) {
37
+ const config = loadConfig(flags);
38
+ const getKey = makeGetApiKey(config);
39
+ return {
40
+ cli: version,
41
+ core: readCoreVersion(),
42
+ node: process.version,
43
+ schemaVersion: EXEC_SCHEMA_VERSION,
44
+ home: getUserDataDir(),
45
+ configPath: getConfigPath(),
46
+ sessionsDir: getSessionsDir(),
47
+ provider: config.provider,
48
+ model: config.model,
49
+ hasApiKey: Boolean(getKey(config.provider)),
50
+ // THE SUPERSET, not "what a run would register right now". This answers
51
+ // "which names can `--allow-tool` take?", which is exactly what
52
+ // `HOST_TOOL_NAMES` means and exactly what `--allow-tool` validates against.
53
+ tools: [...HOST_TOOL_NAMES],
54
+ outputFormats: ['text', 'json', 'stream-json'],
55
+ inputFormats: ['text', 'stream-json'],
56
+ permissionModes: ['auto', 'plan', 'strict'],
57
+ features: [...FEATURES],
58
+ };
59
+ }
60
+ /**
61
+ * The installed `@aragon-agent/core` version, or `'unknown'`.
62
+ *
63
+ * RESOLVED, NOT READ OFF THE DEPENDENCY RANGE. `"^0.2.12"` in this package's
64
+ * own `package.json` is what was ASKED FOR; a consumer asking `aragon info`
65
+ * wants what is actually loaded, and the two differ on every machine that
66
+ * installed a patch release.
67
+ *
68
+ * A DIRECTORY WALK, AND NEITHER `createRequire().resolve()` NOR
69
+ * `import.meta.resolve()` (implementation finding IF-2). Core's `exports` map
70
+ * declares only an `import` condition, so `createRequire(...).resolve()` fails
71
+ * with `ERR_PACKAGE_PATH_NOT_EXPORTED` - and it fails on the DEVELOPER's machine
72
+ * as readily as on a user's, which is how this was caught. `import.meta.resolve`
73
+ * is not synchronously available on Node 18, and `engines` says `>=18`.
74
+ *
75
+ * The walk covers all three real layouts: the monorepo (a symlink in the root
76
+ * `node_modules`), a flat npm install (core beside cli under the project's
77
+ * `node_modules`), and a nested one (core under the cli package's own
78
+ * `node_modules`).
79
+ */
80
+ function readCoreVersion() {
81
+ let dir = dirname(fileURLToPath(import.meta.url));
82
+ for (let depth = 0; depth < 10; depth += 1) {
83
+ try {
84
+ const pkg = JSON.parse(readFileSync(join(dir, 'node_modules', '@aragon-agent', 'core', 'package.json'), 'utf-8'));
85
+ if (pkg.version)
86
+ return pkg.version;
87
+ }
88
+ catch {
89
+ // Not this level. Keep walking.
90
+ }
91
+ const parent = dirname(dir);
92
+ if (parent === dir)
93
+ break;
94
+ dir = parent;
95
+ }
96
+ // A bundled or unusual layout. A missing version is worth reporting honestly;
97
+ // it is not worth failing a discovery command over.
98
+ return 'unknown';
99
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `aragon exec` commander wiring (cli-integration-surface section 3.1).
3
+ *
4
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
5
+ *
6
+ * THE DECLARATIONS LIVE HERE, NOT IN `cli.tsx`, AND THAT IS A DELIBERATE
7
+ * DEPARTURE FROM THE EXISTING CONVENTION. `logs` / `history` / `skills` /
8
+ * `update` declare their options in `cli.tsx` and delegate to a `run*Command`.
9
+ * `cli.tsx` is already 1600 lines against this repository's 1000-line guideline,
10
+ * and four new subcommands carrying about twenty declarations between them is
11
+ * another 120 lines on a file that is 60% over budget. So `cli.tsx` gains four
12
+ * imports and four `register*Command(...)` call sites instead. This is a rule
13
+ * for the NEW code, not a refactor of the old (which is out of scope).
14
+ *
15
+ * NOT `src/commands/` EITHER: that directory is the SLASH-command tree
16
+ * (`/help`, `/model`, `/save`, `/resume`), and it already owns the two names
17
+ * exec's session work sits closest to.
18
+ */
19
+ import { type Command } from 'commander';
20
+ import type { CliFlags } from '../config/load.js';
21
+ import { type ExecControllerFactory } from './index.js';
22
+ export interface ExecCommandDeps {
23
+ version: string;
24
+ /**
25
+ * Supplied by `cli.tsx`. It cannot be imported: `cli.tsx` imports this module,
26
+ * so reaching back for `makeController` would be a cycle, and re-deriving it
27
+ * here would give the headless approval gates a second definition.
28
+ */
29
+ makeController: ExecControllerFactory;
30
+ }
31
+ export declare function registerExecCommand(program: Command, toFlags: (opts: Record<string, unknown>) => CliFlags, deps: ExecCommandDeps): void;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * `aragon exec` commander wiring (cli-integration-surface section 3.1).
3
+ *
4
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
5
+ *
6
+ * THE DECLARATIONS LIVE HERE, NOT IN `cli.tsx`, AND THAT IS A DELIBERATE
7
+ * DEPARTURE FROM THE EXISTING CONVENTION. `logs` / `history` / `skills` /
8
+ * `update` declare their options in `cli.tsx` and delegate to a `run*Command`.
9
+ * `cli.tsx` is already 1600 lines against this repository's 1000-line guideline,
10
+ * and four new subcommands carrying about twenty declarations between them is
11
+ * another 120 lines on a file that is 60% over budget. So `cli.tsx` gains four
12
+ * imports and four `register*Command(...)` call sites instead. This is a rule
13
+ * for the NEW code, not a refactor of the old (which is out of scope).
14
+ *
15
+ * NOT `src/commands/` EITHER: that directory is the SLASH-command tree
16
+ * (`/help`, `/model`, `/save`, `/resume`), and it already owns the two names
17
+ * exec's session work sits closest to.
18
+ */
19
+ import { Option } from 'commander';
20
+ import { runExec } from './index.js';
21
+ export function registerExecCommand(program, toFlags, deps) {
22
+ const exec = program
23
+ .command('exec')
24
+ .description('Headless, machine-facing run: JSON events, sessions, tool permissions, budgets')
25
+ .argument('[prompt]', 'The prompt (or use --prompt-file, or pipe it on stdin)')
26
+ .option('--output-format <fmt>', 'text | json | stream-json (default: text)')
27
+ .option('--input-format <fmt>', 'text | stream-json (needs --output-format stream-json)')
28
+ .option('--partial-messages', 'Emit text_delta events (stream-json only)')
29
+ .option('--include-thinking', 'Emit thinking events (stream-json only)')
30
+ .option('--prompt-file <path>', 'Read the prompt from a file')
31
+ .option('--session-id <id>', 'Create or resume a session by id')
32
+ .option('--resume <id|path>', 'Resume an existing session; fail if it is absent')
33
+ .option('-c, --continue', 'Resume the newest session for this working directory')
34
+ .option('--no-save-session', 'Run statelessly: write nothing to the sessions directory')
35
+ .option('--max-turns <n>', 'Stop after n assistant turns (exit 3)')
36
+ .option('--max-duration <ms>', 'Stop after ms of wall clock (exit 3). --idle-timeout still applies to a silent ' +
37
+ 'stretch inside the run, and --tool-timeout to a single tool call')
38
+ .option('--permission-mode <m>', 'auto | plan | strict (default: auto)')
39
+ .option('--allow-tool <names>', 'Add tools to the baseline (repeatable; comma-separated accepted)', collect)
40
+ .option('--deny-tool <names>', 'Remove tools; wins over --allow-tool (repeatable; comma-separated accepted)', collect)
41
+ .option('--append-system-prompt <text>', 'Append text to the builtin system prompt')
42
+ .option('--append-system-prompt-file <path>', 'Append the contents of a file')
43
+ // HIDDEN, AND KEPT ONLY SO A GUESS IS NOT PUNISHED (P2-1 / D-16). The root
44
+ // command already carries `--tool-timeout` (a single tool call) and
45
+ // `--idle-timeout` (the engine watchdog); a third option called plainly
46
+ // `--timeout` would be the most generic of the three names attached to the
47
+ // newest and least familiar of them.
48
+ .addOption(new Option('--timeout <ms>', 'Alias of --max-duration').hideHelp());
49
+ exec.action(async (prompt, opts) => {
50
+ const rootOpts = program.opts();
51
+ process.exitCode = await runExec(toFlags(program.opts()), {
52
+ ...opts,
53
+ // Hoisted from the root by commander, and read here because `exec` does
54
+ // not redeclare them. `--print` is REFUSED rather than ignored (P2-5).
55
+ ...(rootOpts.quiet !== undefined ? { quiet: rootOpts.quiet } : {}),
56
+ ...(rootOpts.print !== undefined ? { print: rootOpts.print } : {}),
57
+ }, prompt, { version: deps.version, makeController: deps.makeController });
58
+ });
59
+ }
60
+ /** Repeatable option accumulator. Comma splitting happens in `options.ts`. */
61
+ function collect(value, previous = []) {
62
+ return [...previous, value];
63
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The three output faces of `aragon exec` (cli-integration-surface section 5.1).
3
+ *
4
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
5
+ *
6
+ * THE EMITTER IS THE ONLY OBJECT HANDED A STDOUT STREAM (R-2). Everything else
7
+ * on the exec path writes to stderr: migration notices, config-parse warnings,
8
+ * untrusted-skill notices, retry lines, `[usage]`, `[team]`, `[todo]` and every
9
+ * log record. One stray `process.stdout.write` corrupts every consumer, silently
10
+ * and only in the field, so the containment is structural rather than a rule
11
+ * anyone has to remember.
12
+ *
13
+ * `result()` IS IDEMPOTENT ON ALL THREE (AC-6). A `finally` and an error handler
14
+ * racing must not produce two `result` lines, and the second call being a no-op
15
+ * is what makes "exactly one, always last" true on every exit path including
16
+ * error, budget and signal - rather than true for the paths someone remembered.
17
+ */
18
+ import type { ExecEvent, ExecInitParams, ExecResultEvent, ExecResultParams } from './events.js';
19
+ export interface ExecEmitter {
20
+ init(params: ExecInitParams): void;
21
+ /** Every non-`result` event. Ignored wholesale by `text` and `json`. */
22
+ emit(event: ExecEvent): void;
23
+ /** Exactly once per run; the second call is a no-op. */
24
+ result(params: ExecResultParams): void;
25
+ /** What `result()` actually wrote, or `null`. For tests and for `runExec`. */
26
+ emitted(): ExecResultEvent | null;
27
+ }
28
+ /**
29
+ * `--output-format text`: WRITES NOTHING (D-1).
30
+ *
31
+ * Text mode delegates to `runHeadless` VERBATIM - same controller, same
32
+ * `quiet`, same `followThrough` - and this object exists only so `runExec` has
33
+ * one shape to talk to. Reimplementing text rendering here would make "the human
34
+ * CLI is unchanged" an argument someone has to trust instead of a fact anyone
35
+ * can check, and the two implementations would drift the first time a retry line
36
+ * changed. AC-1 asserts byte equality on BOTH streams; this class is what makes
37
+ * it true by construction.
38
+ */
39
+ export declare class TextEmitter implements ExecEmitter {
40
+ private done;
41
+ init(_params: ExecInitParams): void;
42
+ emit(_event: ExecEvent): void;
43
+ result(params: ExecResultParams): void;
44
+ emitted(): ExecResultEvent | null;
45
+ }
46
+ /**
47
+ * `--output-format json`: the `result` object alone, pretty-printed, on stdout.
48
+ *
49
+ * `aragon exec --output-format json "..." | jq -r .result` is the intended
50
+ * one-liner, which is why the intermediate events are dropped rather than
51
+ * buffered into an array: a wrapper that wants the stream asks for the stream.
52
+ */
53
+ export declare class JsonEmitter implements ExecEmitter {
54
+ private readonly out;
55
+ private done;
56
+ constructor(out: NodeJS.WritableStream);
57
+ init(_params: ExecInitParams): void;
58
+ emit(_event: ExecEvent): void;
59
+ result(params: ExecResultParams): void;
60
+ emitted(): ExecResultEvent | null;
61
+ }
62
+ /**
63
+ * `--output-format stream-json`: one JSON object per line, `\n`-terminated, no
64
+ * pretty-printing, UTF-8.
65
+ *
66
+ * EVENTS AFTER `result` ARE DROPPED, not appended. "Exactly one `result`, always
67
+ * last" is the property consumers build their read loop around, and a late
68
+ * `tool_result` arriving from a torn-down subscription would break it in the
69
+ * rarest and least reproducible circumstances there are.
70
+ */
71
+ export declare class StreamJsonEmitter implements ExecEmitter {
72
+ private readonly out;
73
+ private done;
74
+ constructor(out: NodeJS.WritableStream);
75
+ init(params: ExecInitParams): void;
76
+ emit(event: ExecEvent): void;
77
+ result(params: ExecResultParams): void;
78
+ emitted(): ExecResultEvent | null;
79
+ }
80
+ export declare function createEmitter(format: 'text' | 'json' | 'stream-json', out: NodeJS.WritableStream): ExecEmitter;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The three output faces of `aragon exec` (cli-integration-surface section 5.1).
3
+ *
4
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
5
+ *
6
+ * THE EMITTER IS THE ONLY OBJECT HANDED A STDOUT STREAM (R-2). Everything else
7
+ * on the exec path writes to stderr: migration notices, config-parse warnings,
8
+ * untrusted-skill notices, retry lines, `[usage]`, `[team]`, `[todo]` and every
9
+ * log record. One stray `process.stdout.write` corrupts every consumer, silently
10
+ * and only in the field, so the containment is structural rather than a rule
11
+ * anyone has to remember.
12
+ *
13
+ * `result()` IS IDEMPOTENT ON ALL THREE (AC-6). A `finally` and an error handler
14
+ * racing must not produce two `result` lines, and the second call being a no-op
15
+ * is what makes "exactly one, always last" true on every exit path including
16
+ * error, budget and signal - rather than true for the paths someone remembered.
17
+ */
18
+ import { buildInitEvent, buildResultEvent, toNdjsonLine } from './events.js';
19
+ /**
20
+ * `--output-format text`: WRITES NOTHING (D-1).
21
+ *
22
+ * Text mode delegates to `runHeadless` VERBATIM - same controller, same
23
+ * `quiet`, same `followThrough` - and this object exists only so `runExec` has
24
+ * one shape to talk to. Reimplementing text rendering here would make "the human
25
+ * CLI is unchanged" an argument someone has to trust instead of a fact anyone
26
+ * can check, and the two implementations would drift the first time a retry line
27
+ * changed. AC-1 asserts byte equality on BOTH streams; this class is what makes
28
+ * it true by construction.
29
+ */
30
+ export class TextEmitter {
31
+ done = null;
32
+ // The parameters are DECLARED even though they are unused, so a caller
33
+ // holding a concrete `TextEmitter` can call it exactly as it calls the other
34
+ // two. A zero-arity method satisfies the interface and then makes the class
35
+ // unusable through its own type, which is the sort of asymmetry that turns
36
+ // into a cast at the call site.
37
+ init(_params) {
38
+ // Deliberately empty. `runHeadless` owns every byte of the text stream.
39
+ }
40
+ emit(_event) {
41
+ // Deliberately empty, same reason.
42
+ }
43
+ result(params) {
44
+ if (this.done)
45
+ return;
46
+ // RECORDED BUT NOT WRITTEN. `runExec` reads it for the exit code and the
47
+ // session bookkeeping; writing it would append a JSON line to prose that
48
+ // `aragon -p` does not produce.
49
+ this.done = buildResultEvent(params);
50
+ }
51
+ emitted() {
52
+ return this.done;
53
+ }
54
+ }
55
+ /**
56
+ * `--output-format json`: the `result` object alone, pretty-printed, on stdout.
57
+ *
58
+ * `aragon exec --output-format json "..." | jq -r .result` is the intended
59
+ * one-liner, which is why the intermediate events are dropped rather than
60
+ * buffered into an array: a wrapper that wants the stream asks for the stream.
61
+ */
62
+ export class JsonEmitter {
63
+ out;
64
+ done = null;
65
+ constructor(out) {
66
+ this.out = out;
67
+ }
68
+ init(_params) {
69
+ // The init event is carried by `result.sessionId` / `result.model` for this
70
+ // format. Writing it here would put two objects on a stream documented to
71
+ // carry one.
72
+ }
73
+ emit(_event) {
74
+ // Dropped by design; see the class comment.
75
+ }
76
+ result(params) {
77
+ if (this.done)
78
+ return;
79
+ this.done = buildResultEvent(params);
80
+ this.out.write(`${JSON.stringify(this.done, null, 2)}\n`);
81
+ }
82
+ emitted() {
83
+ return this.done;
84
+ }
85
+ }
86
+ /**
87
+ * `--output-format stream-json`: one JSON object per line, `\n`-terminated, no
88
+ * pretty-printing, UTF-8.
89
+ *
90
+ * EVENTS AFTER `result` ARE DROPPED, not appended. "Exactly one `result`, always
91
+ * last" is the property consumers build their read loop around, and a late
92
+ * `tool_result` arriving from a torn-down subscription would break it in the
93
+ * rarest and least reproducible circumstances there are.
94
+ */
95
+ export class StreamJsonEmitter {
96
+ out;
97
+ done = null;
98
+ constructor(out) {
99
+ this.out = out;
100
+ }
101
+ init(params) {
102
+ if (this.done)
103
+ return;
104
+ this.out.write(toNdjsonLine(buildInitEvent(params)));
105
+ }
106
+ emit(event) {
107
+ if (this.done)
108
+ return;
109
+ this.out.write(toNdjsonLine(event));
110
+ }
111
+ result(params) {
112
+ if (this.done)
113
+ return;
114
+ this.done = buildResultEvent(params);
115
+ this.out.write(toNdjsonLine(this.done));
116
+ }
117
+ emitted() {
118
+ return this.done;
119
+ }
120
+ }
121
+ export function createEmitter(format, out) {
122
+ if (format === 'json')
123
+ return new JsonEmitter(out);
124
+ if (format === 'stream-json')
125
+ return new StreamJsonEmitter(out);
126
+ return new TextEmitter();
127
+ }