@aragon-agent/cli 0.4.3

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 (156) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +558 -0
  3. package/dist/agent/coalesce.d.ts +14 -0
  4. package/dist/agent/coalesce.js +41 -0
  5. package/dist/agent/controller.d.ts +132 -0
  6. package/dist/agent/controller.js +332 -0
  7. package/dist/agent/headless.d.ts +22 -0
  8. package/dist/agent/headless.js +74 -0
  9. package/dist/agent/reducer.d.ts +181 -0
  10. package/dist/agent/reducer.js +420 -0
  11. package/dist/agent/system-prompt.d.ts +30 -0
  12. package/dist/agent/system-prompt.js +58 -0
  13. package/dist/agent/usage.d.ts +13 -0
  14. package/dist/agent/usage.js +41 -0
  15. package/dist/cli.d.ts +11 -0
  16. package/dist/cli.js +482 -0
  17. package/dist/commands/builtins.d.ts +16 -0
  18. package/dist/commands/builtins.js +223 -0
  19. package/dist/commands/registry.d.ts +48 -0
  20. package/dist/commands/registry.js +45 -0
  21. package/dist/commands/skills.d.ts +22 -0
  22. package/dist/commands/skills.js +625 -0
  23. package/dist/config/app-paths.d.ts +11 -0
  24. package/dist/config/app-paths.js +16 -0
  25. package/dist/config/env.d.ts +28 -0
  26. package/dist/config/env.js +92 -0
  27. package/dist/config/load.d.ts +48 -0
  28. package/dist/config/load.js +158 -0
  29. package/dist/config/migrate-legacy-state.d.ts +78 -0
  30. package/dist/config/migrate-legacy-state.js +181 -0
  31. package/dist/config/schema.d.ts +249 -0
  32. package/dist/config/schema.js +247 -0
  33. package/dist/config/store.d.ts +44 -0
  34. package/dist/config/store.js +126 -0
  35. package/dist/input/keymap.d.ts +20 -0
  36. package/dist/input/keymap.js +85 -0
  37. package/dist/session/persist.d.ts +24 -0
  38. package/dist/session/persist.js +51 -0
  39. package/dist/skills/archive.d.ts +41 -0
  40. package/dist/skills/archive.js +152 -0
  41. package/dist/skills/cli-commands.d.ts +52 -0
  42. package/dist/skills/cli-commands.js +502 -0
  43. package/dist/skills/fetch-source.d.ts +104 -0
  44. package/dist/skills/fetch-source.js +448 -0
  45. package/dist/skills/installer.d.ts +117 -0
  46. package/dist/skills/installer.js +414 -0
  47. package/dist/skills/lock.d.ts +75 -0
  48. package/dist/skills/lock.js +223 -0
  49. package/dist/skills/manifest.d.ts +47 -0
  50. package/dist/skills/manifest.js +95 -0
  51. package/dist/skills/node-host.d.ts +23 -0
  52. package/dist/skills/node-host.js +59 -0
  53. package/dist/skills/paths.d.ts +61 -0
  54. package/dist/skills/paths.js +107 -0
  55. package/dist/skills/service.d.ts +256 -0
  56. package/dist/skills/service.js +730 -0
  57. package/dist/skills/tools.d.ts +35 -0
  58. package/dist/skills/tools.js +164 -0
  59. package/dist/skills/updater.d.ts +101 -0
  60. package/dist/skills/updater.js +330 -0
  61. package/dist/skills/usage.d.ts +72 -0
  62. package/dist/skills/usage.js +216 -0
  63. package/dist/tools/bash-tool.d.ts +12 -0
  64. package/dist/tools/bash-tool.js +131 -0
  65. package/dist/tools/diff.d.ts +15 -0
  66. package/dist/tools/diff.js +50 -0
  67. package/dist/tools/fs-tools.d.ts +16 -0
  68. package/dist/tools/fs-tools.js +215 -0
  69. package/dist/tools/index.d.ts +93 -0
  70. package/dist/tools/index.js +157 -0
  71. package/dist/tools/search-tools.d.ts +11 -0
  72. package/dist/tools/search-tools.js +225 -0
  73. package/dist/ui/App.d.ts +24 -0
  74. package/dist/ui/App.js +522 -0
  75. package/dist/ui/AutocompletePopup.d.ts +21 -0
  76. package/dist/ui/AutocompletePopup.js +19 -0
  77. package/dist/ui/Composer.d.ts +40 -0
  78. package/dist/ui/Composer.js +61 -0
  79. package/dist/ui/Header.d.ts +30 -0
  80. package/dist/ui/Header.js +25 -0
  81. package/dist/ui/Logo.d.ts +50 -0
  82. package/dist/ui/Logo.js +85 -0
  83. package/dist/ui/Markdown.d.ts +23 -0
  84. package/dist/ui/Markdown.js +122 -0
  85. package/dist/ui/PromptInput.d.ts +81 -0
  86. package/dist/ui/PromptInput.js +383 -0
  87. package/dist/ui/SessionOpener.d.ts +30 -0
  88. package/dist/ui/SessionOpener.js +12 -0
  89. package/dist/ui/StatusBar.d.ts +39 -0
  90. package/dist/ui/StatusBar.js +43 -0
  91. package/dist/ui/ToastStack.d.ts +24 -0
  92. package/dist/ui/ToastStack.js +38 -0
  93. package/dist/ui/Transcript.d.ts +62 -0
  94. package/dist/ui/Transcript.js +128 -0
  95. package/dist/ui/capabilities.d.ts +25 -0
  96. package/dist/ui/capabilities.js +59 -0
  97. package/dist/ui/console-bridge.d.ts +19 -0
  98. package/dist/ui/console-bridge.js +52 -0
  99. package/dist/ui/density.d.ts +26 -0
  100. package/dist/ui/density.js +34 -0
  101. package/dist/ui/entries/AssistantEntry.d.ts +23 -0
  102. package/dist/ui/entries/AssistantEntry.js +13 -0
  103. package/dist/ui/entries/EntryFrame.d.ts +23 -0
  104. package/dist/ui/entries/EntryFrame.js +6 -0
  105. package/dist/ui/entries/ToolCard.d.ts +38 -0
  106. package/dist/ui/entries/ToolCard.js +72 -0
  107. package/dist/ui/entries/ToolPreview.d.ts +57 -0
  108. package/dist/ui/entries/ToolPreview.js +144 -0
  109. package/dist/ui/entries/UserEntry.d.ts +14 -0
  110. package/dist/ui/entries/UserEntry.js +5 -0
  111. package/dist/ui/exit-snapshot.d.ts +27 -0
  112. package/dist/ui/exit-snapshot.js +26 -0
  113. package/dist/ui/gauge.d.ts +24 -0
  114. package/dist/ui/gauge.js +36 -0
  115. package/dist/ui/glyphs.d.ts +87 -0
  116. package/dist/ui/glyphs.js +127 -0
  117. package/dist/ui/gradient.d.ts +11 -0
  118. package/dist/ui/gradient.js +50 -0
  119. package/dist/ui/layout/AppShell.d.ts +24 -0
  120. package/dist/ui/layout/AppShell.js +9 -0
  121. package/dist/ui/layout/Gutter.d.ts +67 -0
  122. package/dist/ui/layout/Gutter.js +42 -0
  123. package/dist/ui/layout/OverlayFrame.d.ts +62 -0
  124. package/dist/ui/layout/OverlayFrame.js +92 -0
  125. package/dist/ui/layout/ScrollViewport.d.ts +33 -0
  126. package/dist/ui/layout/ScrollViewport.js +71 -0
  127. package/dist/ui/layout/budget.d.ts +42 -0
  128. package/dist/ui/layout/budget.js +47 -0
  129. package/dist/ui/layout/frame.d.ts +58 -0
  130. package/dist/ui/layout/frame.js +75 -0
  131. package/dist/ui/layout/overlay-window.d.ts +43 -0
  132. package/dist/ui/layout/overlay-window.js +51 -0
  133. package/dist/ui/layout/scroll.d.ts +28 -0
  134. package/dist/ui/layout/scroll.js +76 -0
  135. package/dist/ui/layout/useTerminalSize.d.ts +13 -0
  136. package/dist/ui/layout/useTerminalSize.js +50 -0
  137. package/dist/ui/markdown-blocks.d.ts +62 -0
  138. package/dist/ui/markdown-blocks.js +120 -0
  139. package/dist/ui/overlays/ConfirmDialog.d.ts +30 -0
  140. package/dist/ui/overlays/ConfirmDialog.js +25 -0
  141. package/dist/ui/overlays/HelpOverlay.d.ts +17 -0
  142. package/dist/ui/overlays/HelpOverlay.js +60 -0
  143. package/dist/ui/overlays/ModelPicker.d.ts +29 -0
  144. package/dist/ui/overlays/ModelPicker.js +29 -0
  145. package/dist/ui/overlays/SettingsScreen.d.ts +37 -0
  146. package/dist/ui/overlays/SettingsScreen.js +106 -0
  147. package/dist/ui/palettes.d.ts +79 -0
  148. package/dist/ui/palettes.js +115 -0
  149. package/dist/ui/screen.d.ts +27 -0
  150. package/dist/ui/screen.js +57 -0
  151. package/dist/ui/theme.d.ts +78 -0
  152. package/dist/ui/theme.js +127 -0
  153. package/dist/ui/transcript-text.d.ts +27 -0
  154. package/dist/ui/transcript-text.js +110 -0
  155. package/package.json +73 -0
  156. package/skills/skill-creator/SKILL.md +96 -0
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Token + cost accounting derived from `TokenUsage` and `ModelInfo.cost`
3
+ * (which is expressed in USD per 1M tokens). See spec §6.2 status bar.
4
+ */
5
+ import type { ModelCost, TokenUsage } from '@aragon-agent/core';
6
+ /** Compute the USD cost of a single `TokenUsage` given a model's cost table. */
7
+ export declare function computeCost(usage: TokenUsage, cost?: ModelCost): number;
8
+ /** Format a USD amount for the status bar (`$0.0000`, or `$1.23` when large). */
9
+ export declare function formatCost(usd: number): string;
10
+ /** Format a token count compactly (`1.2k`, `3.4M`). */
11
+ export declare function formatTokens(n: number): string;
12
+ /** Format a millisecond duration compactly (`820ms`, `3.4s`, `1m02s`). */
13
+ export declare function formatDuration(ms: number): string;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Token + cost accounting derived from `TokenUsage` and `ModelInfo.cost`
3
+ * (which is expressed in USD per 1M tokens). See spec §6.2 status bar.
4
+ */
5
+ /** Compute the USD cost of a single `TokenUsage` given a model's cost table. */
6
+ export function computeCost(usage, cost) {
7
+ if (!cost)
8
+ return 0;
9
+ const perM = (tokens, rate) => rate ? (tokens / 1_000_000) * rate : 0;
10
+ return (perM(usage.inputTokens, cost.input) +
11
+ perM(usage.outputTokens, cost.output) +
12
+ perM(usage.cacheReadTokens ?? 0, cost.cacheRead) +
13
+ perM(usage.cacheWriteTokens ?? 0, cost.cacheWrite));
14
+ }
15
+ /** Format a USD amount for the status bar (`$0.0000`, or `$1.23` when large). */
16
+ export function formatCost(usd) {
17
+ if (usd <= 0)
18
+ return '$0.00';
19
+ if (usd < 1)
20
+ return `$${usd.toFixed(4)}`;
21
+ return `$${usd.toFixed(2)}`;
22
+ }
23
+ /** Format a token count compactly (`1.2k`, `3.4M`). */
24
+ export function formatTokens(n) {
25
+ if (n < 1000)
26
+ return String(n);
27
+ if (n < 1_000_000)
28
+ return `${(n / 1000).toFixed(1)}k`;
29
+ return `${(n / 1_000_000).toFixed(1)}M`;
30
+ }
31
+ /** Format a millisecond duration compactly (`820ms`, `3.4s`, `1m02s`). */
32
+ export function formatDuration(ms) {
33
+ if (ms < 1000)
34
+ return `${Math.round(ms)}ms`;
35
+ const s = ms / 1000;
36
+ if (s < 60)
37
+ return `${s.toFixed(1)}s`;
38
+ const m = Math.floor(s / 60);
39
+ const rem = Math.floor(s % 60);
40
+ return `${m}m${String(rem).padStart(2, '0')}s`;
41
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Bin entry (spec §3.1 / §5.1). Parses argv, resolves layered config, and
3
+ * branches:
4
+ * --help/--version → commander prints & exits
5
+ * config / models subcommand → utility & exit
6
+ * -p/--print or piped stdin → one-shot headless run
7
+ * else → render the interactive Ink TUI
8
+ *
9
+ * The shebang is prepended post-build by scripts/prepend-shebang.mjs.
10
+ */
11
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,482 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * Bin entry (spec §3.1 / §5.1). Parses argv, resolves layered config, and
5
+ * branches:
6
+ * --help/--version → commander prints & exits
7
+ * config / models subcommand → utility & exit
8
+ * -p/--print or piped stdin → one-shot headless run
9
+ * else → render the interactive Ink TUI
10
+ *
11
+ * The shebang is prepended post-build by scripts/prepend-shebang.mjs.
12
+ */
13
+ import { readFileSync } from 'node:fs';
14
+ import { dirname, join } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import process from 'node:process';
17
+ import { render } from 'ink';
18
+ import { Command } from 'commander';
19
+ import { loadConfig } from './config/load.js';
20
+ import { getConfigPath, updatePersistedConfig } from './config/store.js';
21
+ import { formatMigrationNotice, migrateLegacyState } from './config/migrate-legacy-state.js';
22
+ import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampSkillsIntegrity, clampSkillsToolPolicy, clampTranscriptWindow, coerceMaxTokens, coercePositiveInt, isAdapterProvider, DEFAULT_SKILLS_CONFIG, } from './config/schema.js';
23
+ import { makeGetApiKey } from './config/load.js';
24
+ import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
25
+ import { runSkillsCommand } from './skills/cli-commands.js';
26
+ import { runHeadless } from './agent/headless.js';
27
+ import { App } from './ui/App.js';
28
+ import { decideRenderMode } from './ui/layout/frame.js';
29
+ import { enterAltScreen, writeExitTranscript } from './ui/screen.js';
30
+ import { readExitSnapshot } from './ui/exit-snapshot.js';
31
+ import { renderTranscriptText } from './ui/transcript-text.js';
32
+ import { detectCapabilities } from './ui/capabilities.js';
33
+ import { pickGlyphs } from './ui/glyphs.js';
34
+ // ---------------------------------------------------------------------------
35
+ // Version (read from the shipped package.json next to dist/)
36
+ // ---------------------------------------------------------------------------
37
+ function readVersion() {
38
+ try {
39
+ const here = dirname(fileURLToPath(import.meta.url));
40
+ const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf-8'));
41
+ return pkg.version ?? '0.0.0';
42
+ }
43
+ catch {
44
+ return '0.0.0';
45
+ }
46
+ }
47
+ const VERSION = readVersion();
48
+ function toFlags(opts) {
49
+ return {
50
+ provider: opts.provider,
51
+ model: opts.model,
52
+ baseUrl: opts.baseUrl,
53
+ apiKey: opts.apiKey,
54
+ thinking: opts.thinking,
55
+ maxTokens: opts.maxTokens,
56
+ cwd: opts.cwd,
57
+ theme: opts.theme,
58
+ color: opts.color,
59
+ confirm: opts.confirm,
60
+ toolTimeout: opts.toolTimeout,
61
+ idleTimeout: opts.idleTimeout,
62
+ fullscreen: opts.fullscreen,
63
+ exitTranscript: opts.exitTranscript,
64
+ compact: opts.compact,
65
+ hints: opts.hints,
66
+ skills: opts.skills,
67
+ skill: opts.skill,
68
+ skillsYes: opts.skillsYes,
69
+ skillToolPolicy: opts.skillToolPolicy,
70
+ };
71
+ }
72
+ // ---------------------------------------------------------------------------
73
+ // Modes
74
+ // ---------------------------------------------------------------------------
75
+ /**
76
+ * Build the controller for a mode.
77
+ *
78
+ * TWO DIFFERENT APPROVAL CHANNELS, AND THEY MUST NOT BE CONFLATED:
79
+ *
80
+ * `confirm` — the `--confirm` gate for mutating tools. Its `handler === null`
81
+ * fallback is `Promise.resolve(true)`, which is fine because that
82
+ * path is opt-in and only reachable with the TUI mounted.
83
+ *
84
+ * `approval` — the skill-install gate. It PROBES for a human first and denies
85
+ * when there is none (D17 / §8.3.1). Passing the `confirm`
86
+ * closure here instead would inherit the auto-approve fallback
87
+ * and make `requireApproval: true` a no-op under `-p`, silently.
88
+ *
89
+ * `interactive: false` (the headless path) gets the deny-all gate, so a model
90
+ * calling `skill_install` in `-p` mode is refused with an actionable message
91
+ * instead of writing to disk unattended.
92
+ */
93
+ function makeController(flags, opts = { interactive: true }) {
94
+ const config = loadConfig(flags);
95
+ const confirmBridge = { handler: null };
96
+ const controller = new AgentController(config, {
97
+ version: VERSION,
98
+ confirm: (req) => confirmBridge.handler ? confirmBridge.handler(req) : Promise.resolve(true),
99
+ approval: opts.interactive
100
+ ? {
101
+ // Read live on every call: the App's effect cleanup nulls the handler
102
+ // on unmount, and a cached `true` would keep the gate open after the
103
+ // human channel is gone.
104
+ canPrompt: () => confirmBridge.handler !== null,
105
+ request: (req) => confirmBridge.handler ? confirmBridge.handler(req) : Promise.resolve(false),
106
+ }
107
+ : DENY_ALL_APPROVAL,
108
+ notify: (level, text) => {
109
+ if (!opts.interactive)
110
+ process.stderr.write(`[skills] ${level}: ${text}\n`);
111
+ },
112
+ });
113
+ return { controller, confirmBridge };
114
+ }
115
+ /** POSIX signal numbers for the `128 + signo` exit-code convention. */
116
+ const SIGNAL_NUMBERS = { SIGINT: 2, SIGTERM: 15, SIGHUP: 1 };
117
+ /**
118
+ * The interactive TUI — and the ONLY place that may touch the screen.
119
+ *
120
+ * Screen take-over stays inside this function on purpose (§4.4). Hoisting it up
121
+ * to `buildProgram()` / `parseAsync` would push `\x1b[?1049h` into `aragon -p
122
+ * "…" > out.txt`, `aragon config set`, and `aragon --version`, because the
123
+ * interactive and headless paths are disjoint branches: `runInteractive()`
124
+ * renders Ink, `runOneShot()` goes to `runHeadless()`, which never does.
125
+ */
126
+ function runInteractive(flags, extras = {}) {
127
+ const { controller, confirmBridge } = makeController(flags);
128
+ const config = controller.getConfig();
129
+ const mode = decideRenderMode({ fullscreen: config.fullscreen }, process.env, process.stdout);
130
+ let screen = null;
131
+ let replayed = false;
132
+ const replayTranscript = () => {
133
+ if (replayed || mode !== 'fullscreen' || !config.exitTranscript)
134
+ return;
135
+ replayed = true;
136
+ const snapshot = readExitSnapshot();
137
+ if (!snapshot)
138
+ return; // Never published (instant exit) — skip, do not throw.
139
+ writeExitTranscript(process.stdout, renderTranscriptText(snapshot.entries, {
140
+ // Same terminal the TUI just left, so the same glyph tier applies.
141
+ glyphs: pickGlyphs(detectCapabilities(process.env, process.stdout)),
142
+ usageTotal: snapshot.usageTotal,
143
+ provider: snapshot.provider,
144
+ model: snapshot.model,
145
+ elapsedMs: Date.now() - snapshot.startedAt,
146
+ }));
147
+ };
148
+ if (mode === 'fullscreen') {
149
+ screen = enterAltScreen(process.stdout);
150
+ const restore = () => screen?.restore();
151
+ // Four idempotent restore paths (§4.4). The signal hook is NOT redundant:
152
+ // - `waitUntilExit().then()` is a microtask and may never be reached when
153
+ // the process is killed;
154
+ // - Node does not emit `'exit'` at all on signal termination;
155
+ // - Ink's own `signalExit` only unmounts the component tree — it knows
156
+ // nothing about the alternate screen.
157
+ // Without it, `kill <pid>`, closing the terminal window (SIGHUP), or a dying
158
+ // parent all leave the user staring at a blank alternate screen with `reset`
159
+ // as their only way out.
160
+ process.on('exit', restore);
161
+ for (const [name, signo] of Object.entries(SIGNAL_NUMBERS)) {
162
+ process.on(name, () => {
163
+ restore(); // Synchronous: stdout.write on a TTY is sync on every platform.
164
+ process.exit(128 + signo);
165
+ });
166
+ }
167
+ }
168
+ const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay }),
169
+ // Full-screen owns console.* itself (I-4): Ink's patchConsole writes
170
+ // straight to stdout and permanently shifts the fixed frame's accounting.
171
+ { exitOnCtrlC: false, patchConsole: mode === 'inline' });
172
+ // The return value MUST be captured — `waitUntilExit()` is the normal-exit
173
+ // restore path and it cannot be registered otherwise.
174
+ void instance
175
+ .waitUntilExit()
176
+ .then(() => {
177
+ screen?.restore();
178
+ replayTranscript();
179
+ })
180
+ .catch(() => {
181
+ screen?.restore();
182
+ });
183
+ }
184
+ async function runOneShot(flags, prompt, quiet) {
185
+ // Headless: no App is ever rendered, so there is no human to approve a skill
186
+ // install. The gate must know that up front rather than discovering a null
187
+ // handler and defaulting to "yes" (§7.4 / AC-13).
188
+ const { controller } = makeController(flags, { interactive: false });
189
+ const untrusted = controller.getSkillService().untrustedDirs();
190
+ for (const dir of untrusted) {
191
+ process.stderr.write(`[skills] project skills in ${dir} skipped (untrusted). Run: aragon skills trust ${dir}\n`);
192
+ }
193
+ const code = await runHeadless(controller, prompt, { quiet });
194
+ process.exitCode = code;
195
+ }
196
+ async function readStdin() {
197
+ const chunks = [];
198
+ for await (const chunk of process.stdin) {
199
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
200
+ }
201
+ return Buffer.concat(chunks).toString('utf-8').trim();
202
+ }
203
+ async function runModels(flags, providerArg) {
204
+ const config = loadConfig(flags);
205
+ const { controller } = makeController(flags);
206
+ const registry = controller.getModelRegistry();
207
+ const getKey = makeGetApiKey(config);
208
+ const providers = providerArg && isAdapterProvider(providerArg) ? [providerArg] : [...ADAPTER_PROVIDERS];
209
+ if (providerArg && !isAdapterProvider(providerArg)) {
210
+ process.stderr.write(`Unknown provider "${providerArg}". Choose: ${ADAPTER_PROVIDERS.join(', ')}.\n`);
211
+ process.exitCode = 2;
212
+ return;
213
+ }
214
+ for (const provider of providers) {
215
+ process.stdout.write(`\n${provider}:\n`);
216
+ for (const model of registry.getModels(provider)) {
217
+ process.stdout.write(` ${model.id} - ${model.name}\n`);
218
+ }
219
+ const key = getKey(provider);
220
+ if (key) {
221
+ const discovered = await registry.discoverModels(provider, key, config.baseUrl);
222
+ const builtinIds = new Set(registry.getModels(provider).map((m) => m.id));
223
+ const extra = discovered.filter((m) => !builtinIds.has(m.id));
224
+ if (extra.length > 0) {
225
+ process.stdout.write(' (discovered):\n');
226
+ for (const model of extra) {
227
+ process.stdout.write(` ${model.id} - ${model.name}\n`);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+ const CONFIG_SET_KEYS = new Set([
234
+ 'provider',
235
+ 'model',
236
+ 'baseUrl',
237
+ 'thinkingLevel',
238
+ 'maxTokens',
239
+ 'theme',
240
+ 'confirmTools',
241
+ 'toolTimeoutMs',
242
+ 'idleTimeoutMs',
243
+ 'fullscreen',
244
+ 'exitTranscript',
245
+ 'transcriptWindow',
246
+ // Without these two, `density` and `hints` would be settable only as one-shot
247
+ // flags and never persist -- inconsistent with every other config field.
248
+ 'density',
249
+ 'hints',
250
+ // Dotted keys route into the nested `skills` object; see the switch below.
251
+ 'skills.enabled',
252
+ 'skills.requireApproval',
253
+ 'skills.catalogMaxBytes',
254
+ 'skills.bodyMaxBytes',
255
+ 'skills.integrity',
256
+ 'skills.usageTracking',
257
+ 'skills.toolPolicy',
258
+ ]);
259
+ function runConfigSet(key, value) {
260
+ if (!CONFIG_SET_KEYS.has(key)) {
261
+ process.stderr.write(`Unknown config key "${key}". Known keys: ${[...CONFIG_SET_KEYS].join(', ')}.\n`);
262
+ process.exitCode = 2;
263
+ return;
264
+ }
265
+ const patch = {};
266
+ switch (key) {
267
+ case 'provider':
268
+ patch.provider = value;
269
+ break;
270
+ case 'model':
271
+ patch.model = value;
272
+ break;
273
+ case 'baseUrl':
274
+ patch.baseUrl = value.trim().length > 0 ? value : null;
275
+ break;
276
+ case 'thinkingLevel':
277
+ patch.thinkingLevel = clampThinkingLevel(value, 'off');
278
+ break;
279
+ case 'maxTokens':
280
+ patch.maxTokens = coerceMaxTokens(value) ?? null;
281
+ break;
282
+ case 'theme':
283
+ patch.theme = clampTheme(value, 'auto');
284
+ break;
285
+ case 'confirmTools':
286
+ patch.confirmTools = value === 'true' || value === '1';
287
+ break;
288
+ case 'toolTimeoutMs':
289
+ patch.toolTimeoutMs = coercePositiveInt(value, 180_000);
290
+ break;
291
+ case 'idleTimeoutMs':
292
+ patch.idleTimeoutMs = coercePositiveInt(value, 210_000);
293
+ break;
294
+ case 'fullscreen':
295
+ patch.fullscreen = value === 'true' || value === '1';
296
+ break;
297
+ case 'exitTranscript':
298
+ patch.exitTranscript = value === 'true' || value === '1';
299
+ break;
300
+ case 'transcriptWindow':
301
+ patch.transcriptWindow = clampTranscriptWindow(value, 300);
302
+ break;
303
+ case 'density':
304
+ patch.density = clampDensity(value, 'comfortable');
305
+ break;
306
+ case 'hints':
307
+ patch.hints = value === 'true' || value === '1';
308
+ break;
309
+ // The dotted keys write a PARTIAL `skills` object. That is safe only
310
+ // because `updatePersistedConfig` deep-merges this section (§10.3.2) —
311
+ // with the old shallow merge each of these would wipe trustedProjectDirs.
312
+ case 'skills.enabled':
313
+ patch.skills = { enabled: value === 'true' || value === '1' };
314
+ break;
315
+ case 'skills.requireApproval':
316
+ patch.skills = {
317
+ requireApproval: value === 'true' || value === '1',
318
+ };
319
+ break;
320
+ case 'skills.catalogMaxBytes':
321
+ patch.skills = { catalogMaxBytes: coercePositiveInt(value, 6000) };
322
+ break;
323
+ case 'skills.bodyMaxBytes':
324
+ patch.skills = { bodyMaxBytes: coercePositiveInt(value, 30_000) };
325
+ break;
326
+ // Clamped rather than rejected, exactly like `theme` and `thinkingLevel`:
327
+ // hardening only the READ path leaves a bad value on disk that reverts to
328
+ // the default on every launch, which presents as "my setting won't stick".
329
+ case 'skills.integrity':
330
+ patch.skills = {
331
+ integrity: clampSkillsIntegrity(value, DEFAULT_SKILLS_CONFIG.integrity),
332
+ };
333
+ break;
334
+ case 'skills.usageTracking':
335
+ patch.skills = {
336
+ usageTracking: value === 'true' || value === '1',
337
+ };
338
+ break;
339
+ case 'skills.toolPolicy':
340
+ patch.skills = {
341
+ toolPolicy: clampSkillsToolPolicy(value, DEFAULT_SKILLS_CONFIG.toolPolicy),
342
+ };
343
+ break;
344
+ }
345
+ updatePersistedConfig(patch);
346
+ process.stdout.write(`Set ${key} = ${value}\n`);
347
+ }
348
+ // ---------------------------------------------------------------------------
349
+ // CLI definition
350
+ // ---------------------------------------------------------------------------
351
+ function buildProgram() {
352
+ const program = new Command();
353
+ program
354
+ .name('aragon')
355
+ .description('AragonAgent - a Claude-Code / Codex-style terminal UI for the AragonAgent engine.')
356
+ .version(VERSION, '-v, --version', 'Print the version')
357
+ .argument('[prompt]', 'Task prompt (starts the TUI, or a one-shot run with -p / piped stdin)')
358
+ .option('-p, --print', 'Headless: stream the answer to stdout, then exit')
359
+ .option('--provider <id>', 'anthropic | openai | google')
360
+ .option('--model <id>', 'Model id')
361
+ .option('--base-url <url>', 'Override the provider base URL')
362
+ .option('--api-key <key>', 'One-shot API key override (not persisted)')
363
+ .option('--thinking <level>', 'off|minimal|low|medium|high|xhigh')
364
+ .option('--max-tokens <n>', 'Output token cap')
365
+ .option('--cwd <dir>', 'Working directory for tools')
366
+ .option('--confirm', 'Confirm each mutating tool call')
367
+ .option('--tool-timeout <ms>', 'Per-tool executor ceiling (default 180000)')
368
+ .option('--idle-timeout <ms>', 'Watchdog idle timeout (auto-raised to >= tool-timeout+30s)')
369
+ .option('--theme <name>', 'auto|warm|cool|light ("dark" is an alias for "cool")')
370
+ // Same tri-state shape as `--fullscreen`: declaring the positive form first
371
+ // keeps the default `undefined`, so "no opinion" stays distinguishable from
372
+ // an explicit choice and the config layer below can still win.
373
+ .option('--compact', 'Compact transcript density (no blank rows between turns)')
374
+ .option('--no-compact', 'Comfortable transcript density')
375
+ .option('--hints', 'Always show the composer hint row')
376
+ .option('--no-hints', 'Hide the composer hint row')
377
+ // Declaring `--fullscreen` BEFORE `--no-fullscreen` keeps the default
378
+ // `undefined` instead of `true`, which is what makes the tri-state work:
379
+ // "no opinion" must stay distinguishable from "force it".
380
+ .option('--fullscreen', 'Force the full-screen TUI (overrides auto-downgrade, except non-TTY)')
381
+ .option('--no-fullscreen', 'Force the inline renderer (v0.2.0 behavior)')
382
+ .option('--no-exit-transcript', 'Do not replay the session summary after exiting')
383
+ .option('--no-skills', 'Disable the skill system entirely (no catalog, no skill tools)')
384
+ .option('--skill <name>', 'Force-load a skill at Level 2 for this run (repeatable)', (value, previous = []) => [...previous, value])
385
+ .option('--skills-yes', 'Approve skill installs for this run (CI / headless)')
386
+ .option('--skill-tool-policy <mode>', 'Tool ceiling from allowed-tools for this run: off | warn | enforce (not persisted)')
387
+ .option('--no-color', 'Disable ANSI color')
388
+ .option('--quiet', '(print mode) suppress tool/usage lines on stderr')
389
+ .action(async (prompt, opts) => {
390
+ const flags = toFlags(opts);
391
+ const piped = !process.stdin.isTTY;
392
+ const oneShot = !!opts.print || piped;
393
+ if (oneShot) {
394
+ let text = prompt;
395
+ if ((!text || text.trim().length === 0) && piped) {
396
+ text = await readStdin();
397
+ }
398
+ if (!text || text.trim().length === 0) {
399
+ process.stderr.write('No prompt provided for -p/--print mode.\n');
400
+ process.exitCode = 2;
401
+ return;
402
+ }
403
+ await runOneShot(flags, text, !!opts.quiet);
404
+ return;
405
+ }
406
+ if (!process.stdout.isTTY) {
407
+ // No interactive terminal available — fall back to headless if we have a prompt.
408
+ if (prompt && prompt.trim().length > 0) {
409
+ await runOneShot(flags, prompt, !!opts.quiet);
410
+ }
411
+ else {
412
+ process.stderr.write('Not a TTY and no prompt given - nothing to do. Use -p "<prompt>".\n');
413
+ process.exitCode = 2;
414
+ }
415
+ return;
416
+ }
417
+ runInteractive(flags, { initialPrompt: prompt });
418
+ });
419
+ // aragon config [set <key> <value> | path]
420
+ const configCmd = program
421
+ .command('config')
422
+ .description('Open the settings screen')
423
+ .action(() => {
424
+ runInteractive(toFlags(program.opts()), { initialOverlay: 'settings' });
425
+ });
426
+ configCmd
427
+ .command('set <key> <value>')
428
+ .description('Write a single config value')
429
+ .action((key, value) => runConfigSet(key, value));
430
+ configCmd
431
+ .command('path')
432
+ .description('Print the config file path')
433
+ .action(() => {
434
+ process.stdout.write(`${getConfigPath()}\n`);
435
+ });
436
+ // aragon skills <sub> [arg] [--scope] [--name] [--description] [--yes] [--json]
437
+ program
438
+ .command('skills [subcommand] [argument]')
439
+ .description('Manage skills: list | info | install | update | remove | create | path | doctor | usage | trust | untrust')
440
+ .option('--scope <scope>', 'user | project')
441
+ .option('--name <name>', 'Override the installed skill name')
442
+ .option('--description <text>', 'Description for `skills create`')
443
+ .option('--yes', 'Approve without prompting (required for non-interactive installs)')
444
+ .option('--json', 'Machine-readable output for list / info')
445
+ .option('--all', 'Apply `skills update` to every skill that has an upstream')
446
+ .option('--force', 'Let `skills update` overwrite local edits to an installed skill')
447
+ .option('--dry-run', 'Show what `skills update` would change without writing anything')
448
+ .option('--check', 'Ask whether an update exists, without downloading it')
449
+ .option('--sort <order>', 'name | recent (for `skills list`)')
450
+ .option('--reset', 'Delete the local usage counters (`skills usage`; needs --yes)')
451
+ .action(async (subcommand, argument, opts) => {
452
+ const code = await runSkillsCommand(subcommand ?? 'list', argument, opts, toFlags(program.opts()), VERSION);
453
+ process.exitCode = code;
454
+ });
455
+ // aragon models [--provider p]
456
+ program
457
+ .command('models')
458
+ .description('List builtin + discovered models')
459
+ .option('--provider <id>', 'anthropic | openai | google')
460
+ .action(async (opts) => {
461
+ // The root program also declares `--provider`, so commander routes
462
+ // `aragon models --provider x` onto the parent's options; fall back to it.
463
+ const provider = opts.provider ?? program.opts().provider;
464
+ await runModels(toFlags(program.opts()), provider);
465
+ });
466
+ return program;
467
+ }
468
+ async function main() {
469
+ // First statement on purpose: everything below reads config or skills, and
470
+ // both resolve through `envPaths`. Runs before `loadDotenv()` for the same
471
+ // reason. Never throws — see migrate-legacy-state.ts.
472
+ const migration = migrateLegacyState();
473
+ const notice = formatMigrationNotice(migration);
474
+ if (notice)
475
+ process.stderr.write(`${notice}\n`);
476
+ const program = buildProgram();
477
+ await program.parseAsync(process.argv);
478
+ }
479
+ main().catch((err) => {
480
+ process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
481
+ process.exitCode = 1;
482
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Built-in slash commands (spec §5.2):
3
+ * /help /model /settings /thinking /tools /clear /reset /cwd /save /resume
4
+ * /copy /exit (/quit).
5
+ */
6
+ import { CommandRegistry } from './registry.js';
7
+ export declare function registerBuiltinCommands(registry: CommandRegistry): void;
8
+ /**
9
+ * Every built-in command name INCLUDING aliases (`quit`), for documentation and
10
+ * tests.
11
+ *
12
+ * Not for run-time conflict detection — `registerSkillCommands` probes
13
+ * `registry.get(name)` instead, because that is the only check that stays
14
+ * correct when commands are registered dynamically (§7.1 / P1-2).
15
+ */
16
+ export declare const BUILTIN_COMMAND_NAMES: string[];