@aragon-agent/cli 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +131 -0
- package/README.md +227 -3
- package/dist/agent/agent-mode.d.ts +53 -0
- package/dist/agent/agent-mode.js +69 -0
- package/dist/agent/controller.d.ts +69 -0
- package/dist/agent/controller.js +154 -1
- package/dist/agent/plan-prompt.d.ts +18 -0
- package/dist/agent/plan-prompt.js +93 -0
- package/dist/agent/reducer.d.ts +14 -1
- package/dist/agent/reducer.js +9 -0
- package/dist/agent/system-prompt.d.ts +15 -0
- package/dist/agent/system-prompt.js +8 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +183 -20
- package/dist/commands/builtins.js +60 -0
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +71 -4
- package/dist/config/app-paths.js +139 -8
- package/dist/config/cli-commands.d.ts +43 -0
- package/dist/config/cli-commands.js +196 -0
- package/dist/config/env.d.ts +7 -1
- package/dist/config/env.js +45 -1
- package/dist/config/load.d.ts +8 -0
- package/dist/config/load.js +59 -2
- package/dist/config/migrate-home.d.ts +63 -0
- package/dist/config/migrate-home.js +165 -0
- package/dist/config/migrate-legacy-state.d.ts +10 -0
- package/dist/config/migrate-legacy-state.js +15 -6
- package/dist/config/schema.d.ts +61 -0
- package/dist/config/schema.js +83 -0
- package/dist/config/store.d.ts +30 -12
- package/dist/config/store.js +47 -35
- package/dist/logging/cli-commands.d.ts +25 -0
- package/dist/logging/cli-commands.js +221 -0
- package/dist/logging/file-sink.d.ts +114 -0
- package/dist/logging/file-sink.js +326 -0
- package/dist/logging/install.d.ts +163 -0
- package/dist/logging/install.js +302 -0
- package/dist/logging/levels.d.ts +40 -0
- package/dist/logging/levels.js +59 -0
- package/dist/logging/logger.d.ts +110 -0
- package/dist/logging/logger.js +237 -0
- package/dist/logging/redact.d.ts +28 -0
- package/dist/logging/redact.js +90 -0
- package/dist/logging/secret-registry.d.ts +30 -0
- package/dist/logging/secret-registry.js +55 -0
- package/dist/skills/paths.d.ts +2 -4
- package/dist/skills/paths.js +7 -9
- package/dist/tools/human-input.d.ts +179 -0
- package/dist/tools/human-input.js +227 -0
- package/dist/tools/index.d.ts +35 -3
- package/dist/tools/index.js +96 -9
- package/dist/tools/plan-tools.d.ts +64 -0
- package/dist/tools/plan-tools.js +198 -0
- package/dist/ui/App.d.ts +4 -1
- package/dist/ui/App.js +182 -11
- package/dist/ui/Composer.d.ts +4 -1
- package/dist/ui/Composer.js +26 -7
- package/dist/ui/ModeChip.d.ts +29 -0
- package/dist/ui/ModeChip.js +15 -0
- package/dist/ui/PromptInput.d.ts +8 -1
- package/dist/ui/PromptInput.js +12 -2
- package/dist/ui/StatusBar.d.ts +15 -0
- package/dist/ui/StatusBar.js +10 -2
- package/dist/ui/entries/ToolPreview.d.ts +2 -0
- package/dist/ui/entries/ToolPreview.js +64 -0
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +8 -0
- package/dist/ui/layout/wrap-rows.d.ts +24 -0
- package/dist/ui/layout/wrap-rows.js +74 -0
- package/dist/ui/overlays/HelpOverlay.js +13 -0
- package/dist/ui/overlays/PlanReviewOverlay.d.ts +50 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +117 -0
- package/dist/ui/overlays/QuestionOverlay.d.ts +30 -0
- package/dist/ui/overlays/QuestionOverlay.js +193 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
- package/dist/ui/overlays/SettingsScreen.js +8 -1
- package/dist/ui/use-startup-notices.d.ts +15 -0
- package/dist/ui/use-startup-notices.js +38 -0
- package/package.json +3 -2
- package/runtime/insecure-tls-warning.cjs +62 -0
- package/runtime/insecure-tls-warning.d.cts +5 -0
package/dist/cli.js
CHANGED
|
@@ -14,12 +14,18 @@ import { readFileSync } from 'node:fs';
|
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import process from 'node:process';
|
|
17
|
+
import '../runtime/insecure-tls-warning.cjs';
|
|
17
18
|
import { render } from 'ink';
|
|
18
19
|
import { Command } from 'commander';
|
|
19
20
|
import { loadConfig } from './config/load.js';
|
|
20
|
-
import { getConfigPath, updatePersistedConfig } from './config/store.js';
|
|
21
|
+
import { getConfigPath, readConfigFile, updatePersistedConfig } from './config/store.js';
|
|
21
22
|
import { formatMigrationNotice, migrateLegacyState } from './config/migrate-legacy-state.js';
|
|
22
|
-
import {
|
|
23
|
+
import { formatHomeMigrationNotice, migrateToHome } from './config/migrate-home.js';
|
|
24
|
+
import { LOG_CONFIG_SET_KEYS, applyLogConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
|
|
25
|
+
import { attachAgentEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
|
|
26
|
+
import { getLogger } from './logging/logger.js';
|
|
27
|
+
import { runLogsCommand } from './logging/cli-commands.js';
|
|
28
|
+
import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampTranscriptWindow, coerceMaxTokens, coercePositiveInt, isAdapterProvider, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SKILLS_CONFIG, } from './config/schema.js';
|
|
23
29
|
import { makeGetApiKey } from './config/load.js';
|
|
24
30
|
import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
|
|
25
31
|
import { runSkillsCommand } from './skills/cli-commands.js';
|
|
@@ -31,6 +37,7 @@ import { readExitSnapshot } from './ui/exit-snapshot.js';
|
|
|
31
37
|
import { renderTranscriptText } from './ui/transcript-text.js';
|
|
32
38
|
import { detectCapabilities } from './ui/capabilities.js';
|
|
33
39
|
import { pickGlyphs } from './ui/glyphs.js';
|
|
40
|
+
import { DENY_ALL_HUMAN_INPUT } from './tools/human-input.js';
|
|
34
41
|
// ---------------------------------------------------------------------------
|
|
35
42
|
// Version (read from the shipped package.json next to dist/)
|
|
36
43
|
// ---------------------------------------------------------------------------
|
|
@@ -67,6 +74,12 @@ function toFlags(opts) {
|
|
|
67
74
|
skill: opts.skill,
|
|
68
75
|
skillsYes: opts.skillsYes,
|
|
69
76
|
skillToolPolicy: opts.skillToolPolicy,
|
|
77
|
+
plan: opts.plan,
|
|
78
|
+
// `--verbose` is just a shorthand for `--log-level debug`; an explicit
|
|
79
|
+
// `--log-level` still wins, so the two can be combined without surprise.
|
|
80
|
+
logLevel: opts.logLevel ?? (opts.verbose ? 'debug' : undefined),
|
|
81
|
+
logFile: opts.logFile,
|
|
82
|
+
logDir: opts.logDir,
|
|
70
83
|
};
|
|
71
84
|
}
|
|
72
85
|
// ---------------------------------------------------------------------------
|
|
@@ -92,7 +105,14 @@ function toFlags(opts) {
|
|
|
92
105
|
*/
|
|
93
106
|
function makeController(flags, opts = { interactive: true }) {
|
|
94
107
|
const config = loadConfig(flags);
|
|
108
|
+
// The authoritative log level lands here (§4.4.5). `installLogging()` only
|
|
109
|
+
// resolved a bootstrap level — it ran before commander and before `.env` were
|
|
110
|
+
// readable — and without this hand-off the fully resolved one never applies.
|
|
111
|
+
getLogger().reconfigure(config.log);
|
|
95
112
|
const confirmBridge = { handler: null };
|
|
113
|
+
// `cancelPending` is replaced by the App's own implementation on mount; this
|
|
114
|
+
// no-op keeps the object total so nothing has to null-check it.
|
|
115
|
+
const humanInputBridge = { handler: null, cancelPending: () => { } };
|
|
96
116
|
const controller = new AgentController(config, {
|
|
97
117
|
version: VERSION,
|
|
98
118
|
confirm: (req) => confirmBridge.handler ? confirmBridge.handler(req) : Promise.resolve(true),
|
|
@@ -109,11 +129,23 @@ function makeController(flags, opts = { interactive: true }) {
|
|
|
109
129
|
if (!opts.interactive)
|
|
110
130
|
process.stderr.write(`[skills] ${level}: ${text}\n`);
|
|
111
131
|
},
|
|
132
|
+
// A THIRD approval channel, and it must not be conflated with the other two.
|
|
133
|
+
// `confirm` auto-approves when unattached and `approval` denies; neither can
|
|
134
|
+
// render a question wizard or a plan card. Headless gets the fail-closed
|
|
135
|
+
// gate, whose STATIC `neverPrompts` is also what stops `createPlanTools`
|
|
136
|
+
// registering two tools the model could never use there.
|
|
137
|
+
humanInput: opts.interactive
|
|
138
|
+
? {
|
|
139
|
+
// Live probe, never cached — same contract as `approval` above.
|
|
140
|
+
canPrompt: () => humanInputBridge.handler !== null,
|
|
141
|
+
request: (req, signal) => humanInputBridge.handler
|
|
142
|
+
? humanInputBridge.handler(req, signal)
|
|
143
|
+
: Promise.resolve(null),
|
|
144
|
+
}
|
|
145
|
+
: DENY_ALL_HUMAN_INPUT,
|
|
112
146
|
});
|
|
113
|
-
return { controller, confirmBridge };
|
|
147
|
+
return { controller, confirmBridge, humanInputBridge };
|
|
114
148
|
}
|
|
115
|
-
/** POSIX signal numbers for the `128 + signo` exit-code convention. */
|
|
116
|
-
const SIGNAL_NUMBERS = { SIGINT: 2, SIGTERM: 15, SIGHUP: 1 };
|
|
117
149
|
/**
|
|
118
150
|
* The interactive TUI — and the ONLY place that may touch the screen.
|
|
119
151
|
*
|
|
@@ -124,9 +156,12 @@ const SIGNAL_NUMBERS = { SIGINT: 2, SIGTERM: 15, SIGHUP: 1 };
|
|
|
124
156
|
* renders Ink, `runOneShot()` goes to `runHeadless()`, which never does.
|
|
125
157
|
*/
|
|
126
158
|
function runInteractive(flags, extras = {}) {
|
|
127
|
-
const { controller, confirmBridge } = makeController(flags);
|
|
159
|
+
const { controller, confirmBridge, humanInputBridge } = makeController(flags);
|
|
128
160
|
const config = controller.getConfig();
|
|
129
161
|
const mode = decideRenderMode({ fullscreen: config.fullscreen }, process.env, process.stdout);
|
|
162
|
+
const logger = getLogger();
|
|
163
|
+
logger.info('cli', 'run_interactive', { mode, provider: config.provider, model: config.model });
|
|
164
|
+
attachAgentEvents(logger, controller);
|
|
130
165
|
let screen = null;
|
|
131
166
|
let replayed = false;
|
|
132
167
|
const replayTranscript = () => {
|
|
@@ -158,14 +193,19 @@ function runInteractive(flags, extras = {}) {
|
|
|
158
193
|
// parent all leave the user staring at a blank alternate screen with `reset`
|
|
159
194
|
// as their only way out.
|
|
160
195
|
process.on('exit', restore);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
196
|
+
// The signal listeners themselves now belong to `installLogging()`, which
|
|
197
|
+
// registered them before this function ever ran — and a second listener per
|
|
198
|
+
// signal would race the first. This branch owns the SCREEN, so it supplies
|
|
199
|
+
// the two screen-shaped pieces and nothing else: the crash handler calls
|
|
200
|
+
// `screenRestore` before printing a stack (otherwise the stack vanishes with
|
|
201
|
+
// the alternate screen), and the signal path restores before exiting.
|
|
202
|
+
setScreenRestore(restore);
|
|
203
|
+
setSignalTerminator((signo) => {
|
|
204
|
+
restore(); // Synchronous: stdout.write on a TTY is sync on every platform.
|
|
205
|
+
process.exit(128 + signo);
|
|
206
|
+
});
|
|
167
207
|
}
|
|
168
|
-
const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay }),
|
|
208
|
+
const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay }),
|
|
169
209
|
// Full-screen owns console.* itself (I-4): Ink's patchConsole writes
|
|
170
210
|
// straight to stdout and permanently shifts the fixed frame's accounting.
|
|
171
211
|
{ exitOnCtrlC: false, patchConsole: mode === 'inline' });
|
|
@@ -186,11 +226,23 @@ async function runOneShot(flags, prompt, quiet) {
|
|
|
186
226
|
// install. The gate must know that up front rather than discovering a null
|
|
187
227
|
// handler and defaulting to "yes" (§7.4 / AC-13).
|
|
188
228
|
const { controller } = makeController(flags, { interactive: false });
|
|
229
|
+
const logger = getLogger();
|
|
230
|
+
logger.info('cli', 'run_oneshot', { quiet });
|
|
231
|
+
// Headless has no frame, so a logging failure can safely reach stderr — the
|
|
232
|
+
// toast channel `App.tsx` uses does not exist here.
|
|
233
|
+
logger.onFailure((reason) => process.stderr.write(`[log] ${reason}\n`));
|
|
234
|
+
attachAgentEvents(logger, controller);
|
|
189
235
|
const untrusted = controller.getSkillService().untrustedDirs();
|
|
190
236
|
for (const dir of untrusted) {
|
|
191
237
|
process.stderr.write(`[skills] project skills in ${dir} skipped (untrusted). Run: aragon skills trust ${dir}\n`);
|
|
192
238
|
}
|
|
193
239
|
const code = await runHeadless(controller, prompt, { quiet });
|
|
240
|
+
// Flushed HERE and not inside `runHeadless`: that module is deliberately
|
|
241
|
+
// injectable (its own controller interface, its own stdout/stderr) and has an
|
|
242
|
+
// early `return 2`, so a singleton dependency in there would be both a new
|
|
243
|
+
// coupling and a branch that skips the flush. `process.on('exit')` remains the
|
|
244
|
+
// real backstop; this just makes the common path immediate.
|
|
245
|
+
getLogger().flushSync();
|
|
194
246
|
process.exitCode = code;
|
|
195
247
|
}
|
|
196
248
|
async function readStdin() {
|
|
@@ -247,6 +299,9 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
247
299
|
// flags and never persist -- inconsistent with every other config field.
|
|
248
300
|
'density',
|
|
249
301
|
'hints',
|
|
302
|
+
'planModeDefault',
|
|
303
|
+
'planModeMaxAskRounds',
|
|
304
|
+
'planModeHumanTimeoutMs',
|
|
250
305
|
// Dotted keys route into the nested `skills` object; see the switch below.
|
|
251
306
|
'skills.enabled',
|
|
252
307
|
'skills.requireApproval',
|
|
@@ -255,6 +310,9 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
255
310
|
'skills.integrity',
|
|
256
311
|
'skills.usageTracking',
|
|
257
312
|
'skills.toolPolicy',
|
|
313
|
+
// `log.*` keys are handled by `applyLogConfigSet` below, NOT by the switch:
|
|
314
|
+
// that switch is already past this package's complexity ceiling.
|
|
315
|
+
...LOG_CONFIG_SET_KEYS,
|
|
258
316
|
]);
|
|
259
317
|
function runConfigSet(key, value) {
|
|
260
318
|
if (!CONFIG_SET_KEYS.has(key)) {
|
|
@@ -262,6 +320,13 @@ function runConfigSet(key, value) {
|
|
|
262
320
|
process.exitCode = 2;
|
|
263
321
|
return;
|
|
264
322
|
}
|
|
323
|
+
const logPatch = applyLogConfigSet(key, value);
|
|
324
|
+
if (logPatch) {
|
|
325
|
+
updatePersistedConfig(logPatch);
|
|
326
|
+
warnIfRedactionDisabled(key, value);
|
|
327
|
+
process.stdout.write(`Set ${key} = ${value}\n`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
265
330
|
const patch = {};
|
|
266
331
|
switch (key) {
|
|
267
332
|
case 'provider':
|
|
@@ -306,6 +371,19 @@ function runConfigSet(key, value) {
|
|
|
306
371
|
case 'hints':
|
|
307
372
|
patch.hints = value === 'true' || value === '1';
|
|
308
373
|
break;
|
|
374
|
+
// Clamped rather than rejected, for the same reason as `theme` and
|
|
375
|
+
// `skills.integrity`: hardening only the READ path leaves a bad value on
|
|
376
|
+
// disk that reverts to the default on every launch, which presents as
|
|
377
|
+
// "my setting won't stick".
|
|
378
|
+
case 'planModeDefault':
|
|
379
|
+
patch.planModeDefault = value === 'true' || value === '1';
|
|
380
|
+
break;
|
|
381
|
+
case 'planModeMaxAskRounds':
|
|
382
|
+
patch.planModeMaxAskRounds = clampAskRounds(value, DEFAULT_PLAN_MAX_ASK_ROUNDS);
|
|
383
|
+
break;
|
|
384
|
+
case 'planModeHumanTimeoutMs':
|
|
385
|
+
patch.planModeHumanTimeoutMs = clampHumanTimeout(value, DEFAULT_PLAN_HUMAN_TIMEOUT_MS);
|
|
386
|
+
break;
|
|
309
387
|
// The dotted keys write a PARTIAL `skills` object. That is safe only
|
|
310
388
|
// because `updatePersistedConfig` deep-merges this section (§10.3.2) —
|
|
311
389
|
// with the old shallow merge each of these would wipe trustedProjectDirs.
|
|
@@ -380,10 +458,25 @@ function buildProgram() {
|
|
|
380
458
|
.option('--fullscreen', 'Force the full-screen TUI (overrides auto-downgrade, except non-TTY)')
|
|
381
459
|
.option('--no-fullscreen', 'Force the inline renderer (v0.2.0 behavior)')
|
|
382
460
|
.option('--no-exit-transcript', 'Do not replay the session summary after exiting')
|
|
461
|
+
// Same tri-state shape as `--fullscreen`: the positive form is declared
|
|
462
|
+
// FIRST so the default stays `undefined` and "no opinion" remains
|
|
463
|
+
// distinguishable from an explicit `--no-plan`, which is what lets the
|
|
464
|
+
// config layer win when neither flag was given.
|
|
465
|
+
.option('--plan', 'Start the session in PLAN mode (read-only research + review)')
|
|
466
|
+
.option('--no-plan', 'Start the session in BUILD mode (overrides planModeDefault)')
|
|
383
467
|
.option('--no-skills', 'Disable the skill system entirely (no catalog, no skill tools)')
|
|
384
468
|
.option('--skill <name>', 'Force-load a skill at Level 2 for this run (repeatable)', (value, previous = []) => [...previous, value])
|
|
385
469
|
.option('--skills-yes', 'Approve skill installs for this run (CI / headless)')
|
|
386
470
|
.option('--skill-tool-policy <mode>', 'Tool ceiling from allowed-tools for this run: off | warn | enforce (not persisted)')
|
|
471
|
+
// `--log-file` is declared BEFORE `--no-log-file` for the tri-state reason
|
|
472
|
+
// recorded twice above: negative-only would default it to `true` and make a
|
|
473
|
+
// persisted `log.toFile: false` permanently unreachable.
|
|
474
|
+
.option('--log-file', 'Write this run to a log file')
|
|
475
|
+
.option('--no-log-file', 'Do not write this run to a log file')
|
|
476
|
+
.option('--log-level <level>', 'silent|error|warn|info|debug|trace (this run only)')
|
|
477
|
+
// No short form: `-v` is already `--version`.
|
|
478
|
+
.option('--verbose', 'Shorthand for --log-level debug')
|
|
479
|
+
.option('--log-dir <dir>', 'Absolute log directory for this run')
|
|
387
480
|
.option('--no-color', 'Disable ANSI color')
|
|
388
481
|
.option('--quiet', '(print mode) suppress tool/usage lines on stderr')
|
|
389
482
|
.action(async (prompt, opts) => {
|
|
@@ -433,6 +526,37 @@ function buildProgram() {
|
|
|
433
526
|
.action(() => {
|
|
434
527
|
process.stdout.write(`${getConfigPath()}\n`);
|
|
435
528
|
});
|
|
529
|
+
configCmd
|
|
530
|
+
.command('get <key>')
|
|
531
|
+
.description('Print a single config value (secrets are masked)')
|
|
532
|
+
.action((key) => runConfigGet(key));
|
|
533
|
+
configCmd
|
|
534
|
+
.command('list')
|
|
535
|
+
.description('Print the whole config (secrets are masked)')
|
|
536
|
+
.option('--json', 'Machine-readable output')
|
|
537
|
+
.action((opts) => runConfigList(opts));
|
|
538
|
+
configCmd
|
|
539
|
+
.command('edit')
|
|
540
|
+
.description('Open config.json in $VISUAL / $EDITOR (backs it up first)')
|
|
541
|
+
.action(async () => {
|
|
542
|
+
await runConfigEdit();
|
|
543
|
+
});
|
|
544
|
+
configCmd
|
|
545
|
+
.command('home')
|
|
546
|
+
.description('Print the user-state root directory')
|
|
547
|
+
.action(() => runConfigHome());
|
|
548
|
+
// aragon logs <sub> [-n N] [--follow] [--level lv] [--json] [--yes]
|
|
549
|
+
program
|
|
550
|
+
.command('logs [subcommand]')
|
|
551
|
+
.description('Inspect the log files: path | list | tail | clear | open')
|
|
552
|
+
.option('-n, --lines <n>', 'Lines for `logs tail` (default 100)')
|
|
553
|
+
.option('--follow', 'Keep printing new records (`logs tail`)')
|
|
554
|
+
.option('--level <level>', 'Only show records at this level or more severe')
|
|
555
|
+
.option('--json', 'Print raw JSONL instead of rendered lines')
|
|
556
|
+
.option('--yes', 'Confirm destructive actions (required by `logs clear`)')
|
|
557
|
+
.action(async (subcommand, opts) => {
|
|
558
|
+
process.exitCode = await runLogsCommand(subcommand ?? 'path', opts, toFlags(program.opts()));
|
|
559
|
+
});
|
|
436
560
|
// aragon skills <sub> [arg] [--scope] [--name] [--description] [--yes] [--json]
|
|
437
561
|
program
|
|
438
562
|
.command('skills [subcommand] [argument]')
|
|
@@ -466,16 +590,55 @@ function buildProgram() {
|
|
|
466
590
|
return program;
|
|
467
591
|
}
|
|
468
592
|
async function main() {
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
593
|
+
// ① and ② are the first two statements on purpose: everything below reads
|
|
594
|
+
// config or skills. THE ORDER IS NOT INTERCHANGEABLE — ① moves 0.4.x data out
|
|
595
|
+
// of the pre-rename env-paths tree, and only then does ② have anything to
|
|
596
|
+
// move into `~/.aragon-agent`. Run the other way round, `config.json` is
|
|
597
|
+
// stranded permanently (see migrate-home.ts). Neither ever throws.
|
|
598
|
+
const legacy = migrateLegacyState();
|
|
599
|
+
const home = migrateToHome();
|
|
600
|
+
for (const notice of [formatMigrationNotice(legacy), formatHomeMigrationNotice(home)]) {
|
|
601
|
+
if (notice)
|
|
602
|
+
process.stderr.write(`${notice}\n`);
|
|
603
|
+
}
|
|
604
|
+
// ③ comes after both because the log directory lives inside a home that ②
|
|
605
|
+
// may just have created. What ① and ② did is replayed from `pending` rather
|
|
606
|
+
// than lost.
|
|
607
|
+
installLogging({ pending: migrationNotes(legacy, home) });
|
|
608
|
+
// An unparseable config.json still falls back to defaults — the CLI has to
|
|
609
|
+
// start — but it must not do so in silence, or a stray comma presents as
|
|
610
|
+
// "every setting I ever chose is gone". stderr, never stdout, so
|
|
611
|
+
// `aragon -p "…" > out.txt` keeps carrying model output and nothing else.
|
|
612
|
+
const { parseError } = readConfigFile();
|
|
613
|
+
if (parseError) {
|
|
614
|
+
process.stderr.write(`config: using defaults, could not parse ${parseError}\n`);
|
|
615
|
+
}
|
|
476
616
|
const program = buildProgram();
|
|
477
617
|
await program.parseAsync(process.argv);
|
|
478
618
|
}
|
|
619
|
+
/** The ①/② outcomes, as records for `installLogging` to write once it exists. */
|
|
620
|
+
function migrationNotes(legacy, home) {
|
|
621
|
+
const notes = [
|
|
622
|
+
{ level: 'info', scope: 'cli', msg: 'cli_start', data: { version: VERSION } },
|
|
623
|
+
];
|
|
624
|
+
if (legacy.mode !== 'none' || legacy.error) {
|
|
625
|
+
notes.push({
|
|
626
|
+
level: legacy.error ? 'warn' : 'info',
|
|
627
|
+
scope: 'migrate',
|
|
628
|
+
msg: 'migrate_brand',
|
|
629
|
+
data: { ...legacy },
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
if (home.mode !== 'none' || home.error) {
|
|
633
|
+
notes.push({
|
|
634
|
+
level: home.error ? 'warn' : 'info',
|
|
635
|
+
scope: 'migrate',
|
|
636
|
+
msg: 'migrate_home',
|
|
637
|
+
data: { ...home },
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
return notes;
|
|
641
|
+
}
|
|
479
642
|
main().catch((err) => {
|
|
480
643
|
process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
481
644
|
process.exitCode = 1;
|
|
@@ -8,7 +8,10 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
8
8
|
import { isAbsolute, resolve } from 'node:path';
|
|
9
9
|
import process from 'node:process';
|
|
10
10
|
import { clampThinkingLevel, clampTheme, isThemeName, LEGACY_DARK_THEME, THEME_NAMES, THINKING_LEVELS, } from '../config/schema.js';
|
|
11
|
+
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
12
|
+
import { loadConfig } from '../config/load.js';
|
|
11
13
|
import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
|
|
14
|
+
import { getLogger } from '../logging/logger.js';
|
|
12
15
|
const COMMANDS = [
|
|
13
16
|
{
|
|
14
17
|
name: 'help',
|
|
@@ -63,6 +66,30 @@ const COMMANDS = [
|
|
|
63
66
|
ctx.toast('success', `Theme set to ${name}.`);
|
|
64
67
|
},
|
|
65
68
|
},
|
|
69
|
+
{
|
|
70
|
+
name: 'plan',
|
|
71
|
+
description: 'Toggle plan mode (on | off | status)',
|
|
72
|
+
// Exists next to Shift+Tab for two reasons: a handful of terminal
|
|
73
|
+
// multiplexer and remote-desktop stacks never send `CSI Z` at all (R-P1),
|
|
74
|
+
// and slash commands are the discoverable surface — `/` opens the palette.
|
|
75
|
+
run: (ctx) => {
|
|
76
|
+
const arg = ctx.args.trim().toLowerCase();
|
|
77
|
+
if (arg === 'status') {
|
|
78
|
+
const s = ctx.controller.getPlanStatus();
|
|
79
|
+
const pending = s.pending ? ` (pending ${MODE_LABEL[s.pending]} after this run)` : '';
|
|
80
|
+
ctx.notify('info', `Mode: ${MODE_LABEL[s.effective]}${pending}. ` +
|
|
81
|
+
`Ask rounds used: ${s.askRoundsUsed}/${s.maxAskRounds}.`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (arg.length > 0 && arg !== 'on' && arg !== 'off') {
|
|
85
|
+
ctx.notify('warn', `Unknown argument "${arg}" - use /plan [on|off|status].`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const current = ctx.controller.getAgentMode();
|
|
89
|
+
const target = arg === 'on' ? 'plan' : arg === 'off' ? 'build' : nextMode(current);
|
|
90
|
+
ctx.applyAgentMode(target);
|
|
91
|
+
},
|
|
92
|
+
},
|
|
66
93
|
{
|
|
67
94
|
name: 'expand',
|
|
68
95
|
description: 'Expand / collapse a recent tool card (default: last)',
|
|
@@ -179,6 +206,39 @@ const COMMANDS = [
|
|
|
179
206
|
ctx.toast(ok ? 'success' : 'warn', ok ? 'Copied to clipboard.' : 'Clipboard not available.');
|
|
180
207
|
},
|
|
181
208
|
},
|
|
209
|
+
{
|
|
210
|
+
name: 'logs',
|
|
211
|
+
description: 'Show where this session is being logged',
|
|
212
|
+
run: (ctx) => {
|
|
213
|
+
const logger = getLogger();
|
|
214
|
+
const suffix = logger.sinkDisabled ? `\nLogging is OFF: ${logger.sinkError}` : '';
|
|
215
|
+
ctx.notify('info', `Log file: ${logger.currentLogPath()}\nLevel: ${logger.level}` +
|
|
216
|
+
' (debug/trace record prompt and tool content)' +
|
|
217
|
+
suffix);
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'reload',
|
|
222
|
+
description: 'Re-read config.json (after editing it in another window)',
|
|
223
|
+
run: (ctx) => {
|
|
224
|
+
// REFUSED WHILE RUNNING, and not out of caution: swapping provider or
|
|
225
|
+
// model mid-turn would let the second half of an answer be produced under
|
|
226
|
+
// different settings than the first, with nothing afterwards able to
|
|
227
|
+
// explain why that answer looks the way it does.
|
|
228
|
+
if (ctx.state.status === 'running') {
|
|
229
|
+
ctx.notify('warn', 'Cannot reload the config while a run is in progress.');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const config = loadConfig({ cwd: ctx.controller.getCwd() });
|
|
233
|
+
ctx.controller.setModel(config.provider, config.model, config.baseUrl);
|
|
234
|
+
ctx.controller.setThinkingLevel(config.thinkingLevel);
|
|
235
|
+
ctx.controller.setMaxTokens(config.maxTokens);
|
|
236
|
+
ctx.controller.setTheme(config.theme);
|
|
237
|
+
getLogger().reconfigure(config.log);
|
|
238
|
+
getLogger().info('config', 'reload', { provider: config.provider, model: config.model });
|
|
239
|
+
ctx.toast('success', `Reloaded config: ${config.provider}:${config.model}.`);
|
|
240
|
+
},
|
|
241
|
+
},
|
|
182
242
|
{
|
|
183
243
|
name: 'exit',
|
|
184
244
|
aliases: ['quit'],
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* input is routed here instead of to the agent; `//` is the literal-`/` escape.
|
|
4
4
|
*/
|
|
5
5
|
import type { AgentController } from '../agent/controller.js';
|
|
6
|
+
import type { AgentMode } from '../agent/agent-mode.js';
|
|
6
7
|
import type { NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } from '../agent/reducer.js';
|
|
7
8
|
import type { PersistedConfig } from '../config/schema.js';
|
|
8
9
|
export interface CommandContext {
|
|
@@ -27,6 +28,22 @@ export interface CommandContext {
|
|
|
27
28
|
submit: (text: string) => void;
|
|
28
29
|
/** Rebuild the system prompt and the dynamic command list after a skill change. */
|
|
29
30
|
refreshSkills: () => void;
|
|
31
|
+
/**
|
|
32
|
+
* Apply a mode change through the App's SINGLE write path (§3.1 / R-P7).
|
|
33
|
+
*
|
|
34
|
+
* `controller` and `dispatch` are both already in this context, so `/plan`
|
|
35
|
+
* COULD write the controller and dispatch the mirror itself — and that would
|
|
36
|
+
* be a second mode write path, which is the one thing the `setAgentMode`
|
|
37
|
+
* signature was shaped to forbid. One line here instead keeps `/plan` and
|
|
38
|
+
* `Shift+Tab` literally the same code.
|
|
39
|
+
*
|
|
40
|
+
* Returns the ADOPTED state, because `plan -> build` may be deferred and the
|
|
41
|
+
* command has to report what actually happened, not what it asked for.
|
|
42
|
+
*/
|
|
43
|
+
applyAgentMode: (next: AgentMode) => {
|
|
44
|
+
effective: AgentMode;
|
|
45
|
+
pending: AgentMode | null;
|
|
46
|
+
};
|
|
30
47
|
}
|
|
31
48
|
export interface SlashCommand {
|
|
32
49
|
name: string;
|
|
@@ -1,11 +1,78 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The user-state root, in one place.
|
|
3
3
|
*
|
|
4
4
|
* `config/store.ts` and `skills/paths.ts` each used to carry their own copy of
|
|
5
|
-
*
|
|
5
|
+
* the path arithmetic. Two copies of a name is exactly the shape that lets a
|
|
6
6
|
* rename land in one of them and not the other, and the failure is silent: the
|
|
7
7
|
* config file and the skills directory simply stop agreeing about where the
|
|
8
|
-
* user's data lives.
|
|
8
|
+
* user's data lives. Everything below derives from `homeRoot`.
|
|
9
|
+
*
|
|
10
|
+
* LAYOUT (aragon-home-config-and-logging §4.1):
|
|
11
|
+
*
|
|
12
|
+
* ~/.aragon-agent/
|
|
13
|
+
* ├── config.json persisted config (0600 on POSIX)
|
|
14
|
+
* ├── config.json.bak `aragon config edit` pre-edit backup
|
|
15
|
+
* ├── logs/ JSONL logs (0700 on POSIX)
|
|
16
|
+
* ├── sessions/ /save · /resume
|
|
17
|
+
* ├── skills/ `aragon skills install` target
|
|
18
|
+
* └── skill-usage.json
|
|
19
|
+
*
|
|
20
|
+
* The same path on all three platforms, deliberately. Claude Code, git and npm
|
|
21
|
+
* all put their dot-directory in the user's home; one path means documentation,
|
|
22
|
+
* support scripts and community answers have exactly one thing to say. A user
|
|
23
|
+
* who wants XDG has `ARAGON_HOME=$XDG_CONFIG_HOME/aragon-agent`.
|
|
24
|
+
*
|
|
25
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
26
|
+
* TEST-ISOLATION CONTRACT — read before editing anything here or in the tests.
|
|
27
|
+
*
|
|
28
|
+
* `getHomeRoot()` returns the WHOLE user root, not just a config directory.
|
|
29
|
+
* Under the old `env-paths` layout the config directory held one file; here it
|
|
30
|
+
* holds the API key, every saved session and every installed skill. That makes
|
|
31
|
+
* a careless `rmSync(<root>, { recursive: true })` in a test an unrecoverable
|
|
32
|
+
* data loss on the developer's own machine, at a moment when nobody expects the
|
|
33
|
+
* filesystem to be touched at all. Four rules keep that impossible:
|
|
34
|
+
*
|
|
35
|
+
* 1. There is NO `getConfigDir()`. A function whose name says "config dir"
|
|
36
|
+
* must never hand back the user root. Code that needs the root calls
|
|
37
|
+
* `getHomeRoot()`, where the size of what you are holding is obvious.
|
|
38
|
+
* 2. `config.test.ts::clearEnv()` must EXEMPT `ARAGON_HOME` — deleting it
|
|
39
|
+
* would send any later, non-module-level resolution at the developer's
|
|
40
|
+
* real home.
|
|
41
|
+
* 3. Tests never recursively delete a path a function returned. Delete a
|
|
42
|
+
* single file (`rmSync(getConfigPath(), { force: true })`), or delete a
|
|
43
|
+
* directory the test itself built under `os.tmpdir()`.
|
|
44
|
+
* 4. The VITEST branch below. In a test process with no explicit
|
|
45
|
+
* `ARAGON_HOME`, the root is redirected into `os.tmpdir()`. Redirecting
|
|
46
|
+
* rather than throwing is deliberate: throwing would turn every unrelated
|
|
47
|
+
* test that merely imports this module red, and that cost is what tempts
|
|
48
|
+
* someone to delete the guard.
|
|
49
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
9
50
|
*/
|
|
10
51
|
export declare const APP_NAME = "aragon-agent";
|
|
11
|
-
export declare const
|
|
52
|
+
export declare const HOME_DIR_NAME = ".aragon-agent";
|
|
53
|
+
/**
|
|
54
|
+
* The pre-0.6.0 `env-paths` roots.
|
|
55
|
+
*
|
|
56
|
+
* ONLY the two migration modules may read this. Any other reference is a bug:
|
|
57
|
+
* it would point new code at the directory the migration exists to empty.
|
|
58
|
+
*/
|
|
59
|
+
export declare const legacyEnvPaths: import("env-paths").Paths;
|
|
60
|
+
/** `~/.aragon-agent` (or `$ARAGON_HOME`). The root of everything below. */
|
|
61
|
+
export declare function getHomeRoot(): string;
|
|
62
|
+
export declare function getConfigPath(): string;
|
|
63
|
+
/** Written by `aragon config edit` before spawning the editor. */
|
|
64
|
+
export declare function getConfigBackupPath(): string;
|
|
65
|
+
export declare function getLogsDir(): string;
|
|
66
|
+
export declare function getSessionsDir(): string;
|
|
67
|
+
/** `<home>` — parent of `skills/`, and where sidecar state such as usage lives. */
|
|
68
|
+
export declare function getUserDataDir(): string;
|
|
69
|
+
export declare function getUserSkillsDir(): string;
|
|
70
|
+
/** Whether `ARAGON_HOME` was honoured. Captured at load — see `resolution`. */
|
|
71
|
+
export declare function isHomeOverridden(): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Why an `ARAGON_HOME` override was ignored, if it was.
|
|
74
|
+
*
|
|
75
|
+
* Path resolution happens before the logger exists, so the reason is parked
|
|
76
|
+
* here and written out by `installLogging()` once there is somewhere to put it.
|
|
77
|
+
*/
|
|
78
|
+
export declare function getHomeResolutionWarning(): string | undefined;
|