@aragon-agent/cli 0.5.12 → 0.6.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 +100 -0
- package/README.md +301 -6
- package/dist/agent/controller.d.ts +32 -0
- package/dist/agent/controller.js +25 -0
- package/dist/agent/system-prompt.d.ts +16 -0
- package/dist/agent/system-prompt.js +5 -0
- package/dist/cli.js +71 -2
- package/dist/config/ui-state.d.ts +18 -0
- package/dist/config/ui-state.js +10 -0
- package/dist/diagnostics/cli-commands.d.ts +10 -0
- package/dist/diagnostics/cli-commands.js +61 -0
- package/dist/diagnostics/doctor.d.ts +29 -0
- package/dist/diagnostics/doctor.js +167 -0
- package/dist/diagnostics/info.d.ts +30 -0
- package/dist/diagnostics/info.js +99 -0
- package/dist/exec/cli-commands.d.ts +31 -0
- package/dist/exec/cli-commands.js +63 -0
- package/dist/exec/emitter.d.ts +80 -0
- package/dist/exec/emitter.js +127 -0
- package/dist/exec/events.d.ts +226 -0
- package/dist/exec/events.js +43 -0
- package/dist/exec/index.d.ts +56 -0
- package/dist/exec/index.js +435 -0
- package/dist/exec/options.d.ts +76 -0
- package/dist/exec/options.js +221 -0
- package/dist/exec/permission.d.ts +66 -0
- package/dist/exec/permission.js +96 -0
- package/dist/exec/runner.d.ts +204 -0
- package/dist/exec/runner.js +559 -0
- package/dist/exec/stdin-stream.d.ts +46 -0
- package/dist/exec/stdin-stream.js +144 -0
- package/dist/logging/install.d.ts +22 -0
- package/dist/logging/install.js +22 -0
- package/dist/session/cli-commands.d.ts +30 -0
- package/dist/session/cli-commands.js +162 -0
- package/dist/session/persist.d.ts +46 -0
- package/dist/session/persist.js +8 -0
- package/dist/session/store.d.ts +129 -0
- package/dist/session/store.js +385 -0
- package/dist/team/runtime.d.ts +12 -0
- package/dist/team/runtime.js +6 -0
- package/dist/team/subagent.d.ts +12 -0
- package/dist/team/subagent.js +4 -0
- package/dist/tools/index.d.ts +20 -0
- package/dist/tools/index.js +26 -0
- package/dist/ui/ActivityLine.d.ts +16 -0
- package/dist/ui/ActivityLine.js +23 -1
- package/dist/ui/App.d.ts +14 -1
- package/dist/ui/App.js +38 -3
- package/dist/ui/BottomStatusRow.d.ts +33 -1
- package/dist/ui/BottomStatusRow.js +20 -6
- package/dist/ui/use-startup-notices.d.ts +67 -4
- package/dist/ui/use-startup-notices.js +76 -2
- package/dist/ui/win-vt-input.d.ts +57 -0
- package/dist/ui/win-vt-input.js +62 -0
- package/package.json +7 -3
package/dist/cli.js
CHANGED
|
@@ -32,6 +32,13 @@ import { makeGetApiKey } from './config/load.js';
|
|
|
32
32
|
import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
|
|
33
33
|
import { runSkillsCommand } from './skills/cli-commands.js';
|
|
34
34
|
import { runHeadless } from './agent/headless.js';
|
|
35
|
+
// The four `register*Command` entry points of cli-integration-surface §3.1. The
|
|
36
|
+
// declarations live in those modules rather than here; see the header of
|
|
37
|
+
// `exec/cli-commands.ts` for why this new code departs from the `logs` /
|
|
38
|
+
// `history` / `skills` convention.
|
|
39
|
+
import { registerExecCommand } from './exec/cli-commands.js';
|
|
40
|
+
import { registerSessionsCommand } from './session/cli-commands.js';
|
|
41
|
+
import { registerDoctorCommand, registerInfoCommand } from './diagnostics/cli-commands.js';
|
|
35
42
|
import { App } from './ui/App.js';
|
|
36
43
|
import { decideRenderMode } from './ui/layout/frame.js';
|
|
37
44
|
import { createFrameDiffer } from './ui/frame-differ.js';
|
|
@@ -39,6 +46,7 @@ import { wrapStdoutForFrames } from './ui/stdout-frame-writer.js';
|
|
|
39
46
|
import { setFrameStatsProvider } from './commands/perf.js';
|
|
40
47
|
import { tryCreateMouseFilter } from './input/stdin-mouse-filter.js';
|
|
41
48
|
import { enterAltScreen, writeExitTranscript } from './ui/screen.js';
|
|
49
|
+
import { supportsWindowsVtInput } from './ui/win-vt-input.js';
|
|
42
50
|
import { readExitSnapshot } from './ui/exit-snapshot.js';
|
|
43
51
|
import { renderTranscriptText } from './ui/transcript-text.js';
|
|
44
52
|
import { detectCapabilities } from './ui/capabilities.js';
|
|
@@ -234,9 +242,35 @@ function makeController(flags, opts = { interactive: true }) {
|
|
|
234
242
|
: Promise.resolve(null),
|
|
235
243
|
}
|
|
236
244
|
: DENY_ALL_HUMAN_INPUT,
|
|
245
|
+
...(opts.permission ? { permission: opts.permission } : {}),
|
|
246
|
+
...(opts.appendSystemPrompt ? { appendSystemPrompt: opts.appendSystemPrompt } : {}),
|
|
237
247
|
});
|
|
238
248
|
return { controller, confirmBridge, humanInputBridge };
|
|
239
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* The controller factory `aragon exec` runs on.
|
|
252
|
+
*
|
|
253
|
+
* IT REPRODUCES `runOneShot`'s SETUP EXACTLY, and that is the whole reason it
|
|
254
|
+
* lives here rather than inside `exec/`: the deny-all approval gate, the
|
|
255
|
+
* deny-all human-input channel, the two log attachments and the untrusted-skill
|
|
256
|
+
* notices are decisions `makeController`'s own comment says must have ONE
|
|
257
|
+
* definition. `exec/index.ts` receives this as a parameter, which also keeps
|
|
258
|
+
* `cli.tsx -> exec/cli-commands.ts -> exec/index.ts` acyclic.
|
|
259
|
+
*/
|
|
260
|
+
function makeExecController(flags, deps) {
|
|
261
|
+
const { controller } = makeController(flags, { interactive: false, ...deps });
|
|
262
|
+
const logger = getLogger();
|
|
263
|
+
logger.info('cli', 'run_exec', {});
|
|
264
|
+
logger.onFailure((reason) => process.stderr.write(`[log] ${reason}\n`));
|
|
265
|
+
attachAgentEvents(logger, controller);
|
|
266
|
+
attachTeamEvents(logger, controller);
|
|
267
|
+
for (const dir of controller.getSkillService().untrustedDirs()) {
|
|
268
|
+
// STDERR, NEVER STDOUT. In `json` / `stream-json` mode stdout carries
|
|
269
|
+
// nothing but the schema, and a notice on it would corrupt every consumer.
|
|
270
|
+
process.stderr.write(`[skills] project skills in ${dir} skipped (untrusted). Run: aragon skills trust ${dir}\n`);
|
|
271
|
+
}
|
|
272
|
+
return controller;
|
|
273
|
+
}
|
|
240
274
|
/**
|
|
241
275
|
* Raised once per session, on the 0->1 edge of the writer's `fallbacks` counter
|
|
242
276
|
* (tui-input-flicker-fix §5.6 / AC-15).
|
|
@@ -294,7 +328,23 @@ function runInteractive(flags, extras = {}) {
|
|
|
294
328
|
// reaches `runInteractive` without the stdin check the default action makes)
|
|
295
329
|
// would wrap a stream no report can ever arrive on and ask the terminal to
|
|
296
330
|
// start reporting anyway. §4.2's "non-TTY ⇒ no filter" covers both ends.
|
|
297
|
-
|
|
331
|
+
//
|
|
332
|
+
// R-1 IS DECIDED HERE, AND ONLY HERE (shift-tab-and-mouse-wheel-dead-on-some-
|
|
333
|
+
// terminals, F1'). On a Windows console whose Node never sets
|
|
334
|
+
// `ENABLE_VIRTUAL_TERMINAL_INPUT`, libuv drops every non-KEY_EVENT record
|
|
335
|
+
// before it reaches this process, so `\x1b[?1000h` is a pure no-op and the
|
|
336
|
+
// filter has nothing to filter. Gating `wantMouse` — rather than the three
|
|
337
|
+
// things downstream of it — is what keeps the three in agreement: the
|
|
338
|
+
// `?1000h` write (`enterAltScreen(..., { mouse: filter !== null })`), the
|
|
339
|
+
// filter itself, and the `mouseSource` prop that decides whether the user is
|
|
340
|
+
// advised about a mode that is not running. Wiring any of them separately
|
|
341
|
+
// re-opens the gap where the app claims a capability it does not have.
|
|
342
|
+
const vtInputSupported = supportsWindowsVtInput(process.platform, process.versions.node);
|
|
343
|
+
const wantMouse = mode === 'fullscreen' &&
|
|
344
|
+
config.mouse &&
|
|
345
|
+
!!process.stdout.isTTY &&
|
|
346
|
+
!!process.stdin.isTTY &&
|
|
347
|
+
vtInputSupported;
|
|
298
348
|
const mouseFilter = wantMouse
|
|
299
349
|
? tryCreateMouseFilter(process.stdin, (reason) => logger.warn('cli', 'mouse_filter_failed', { reason }))
|
|
300
350
|
: null;
|
|
@@ -400,7 +450,13 @@ function runInteractive(flags, extras = {}) {
|
|
|
400
450
|
!!process.stdin.isTTY &&
|
|
401
451
|
!process.env.CI &&
|
|
402
452
|
config.update.mode !== 'off';
|
|
403
|
-
const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay, mouseSource: mouseFilter?.source,
|
|
453
|
+
const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay, mouseSource: mouseFilter?.source,
|
|
454
|
+
// Present ONLY when this console swallows `CSI Z` and mouse reports.
|
|
455
|
+
// Passed in rather than probed inside `App` so that every test renders
|
|
456
|
+
// the same tree on every machine — reading `process.versions.node` from
|
|
457
|
+
// a component would make the notice appear or vanish according to which
|
|
458
|
+
// Node the developer happened to have installed.
|
|
459
|
+
vtInputWarning: vtInputSupported ? undefined : { nodeVersion: process.versions.node }, updateBridge: updateEligible ? updateBridge : undefined }),
|
|
404
460
|
// Full-screen owns console.* itself (I-4): Ink's patchConsole writes
|
|
405
461
|
// straight to stdout and permanently shifts the fixed frame's accounting.
|
|
406
462
|
{
|
|
@@ -1266,6 +1322,19 @@ function buildProgram() {
|
|
|
1266
1322
|
const provider = opts.provider ?? program.opts().provider;
|
|
1267
1323
|
await runModels(toFlags(program.opts()), provider);
|
|
1268
1324
|
});
|
|
1325
|
+
// --- The machine-facing face (cli-integration-surface §3.1). --------------
|
|
1326
|
+
//
|
|
1327
|
+
// FOUR CALL SITES, ~10 LINES. Each module owns its own `.command()` and
|
|
1328
|
+
// `.option()` declarations, because this file is already 60% over the
|
|
1329
|
+
// repository's 1000-line guideline and twenty more declarations would make
|
|
1330
|
+
// that worse for the audience `aragon --help` exists for.
|
|
1331
|
+
registerExecCommand(program, toFlags, {
|
|
1332
|
+
version: VERSION,
|
|
1333
|
+
makeController: makeExecController,
|
|
1334
|
+
});
|
|
1335
|
+
registerSessionsCommand(program);
|
|
1336
|
+
registerInfoCommand(program, toFlags, VERSION);
|
|
1337
|
+
registerDoctorCommand(program, toFlags);
|
|
1269
1338
|
return program;
|
|
1270
1339
|
}
|
|
1271
1340
|
async function main() {
|
|
@@ -34,6 +34,22 @@ export interface UiState {
|
|
|
34
34
|
submitCount: number;
|
|
35
35
|
/** Whether the one-shot "hold Shift to select text" notice has been shown. */
|
|
36
36
|
mouseNoticeSeen: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the one-shot "this Windows console cannot deliver Shift+Tab or the
|
|
39
|
+
* wheel" notice has been shown.
|
|
40
|
+
*
|
|
41
|
+
* A SEPARATE KEY FROM `mouseNoticeSeen`, AND THAT IS THE WHOLE POINT. The
|
|
42
|
+
* people this notice exists for are precisely the ones who have already
|
|
43
|
+
* started aragon, already been shown the mouse notice that does not apply to
|
|
44
|
+
* them, and therefore already have `mouseNoticeSeen: true` on disk. Folding
|
|
45
|
+
* the two would make the fix inert for its entire audience, in silence.
|
|
46
|
+
*
|
|
47
|
+
* NOT ADDED TO `UI_STATE_SCHEMA`: bumping the schema makes `readFromDisk`
|
|
48
|
+
* discard the whole object, which would reset everyone's submit counter and
|
|
49
|
+
* re-show the mouse notice. A new field simply reads as its default on an
|
|
50
|
+
* older file, which is the behaviour wanted here anyway.
|
|
51
|
+
*/
|
|
52
|
+
vtInputNoticeSeen: boolean;
|
|
37
53
|
}
|
|
38
54
|
/** Read the state, caching it for the life of the process. Never throws. */
|
|
39
55
|
export declare function loadUiState(): UiState;
|
|
@@ -42,6 +58,8 @@ export declare function readSubmitCount(): number;
|
|
|
42
58
|
export declare function bumpSubmitCount(): number;
|
|
43
59
|
export declare function getMouseNoticeSeen(): boolean;
|
|
44
60
|
export declare function setMouseNoticeSeen(seen: boolean): void;
|
|
61
|
+
export declare function getVtInputNoticeSeen(): boolean;
|
|
62
|
+
export declare function setVtInputNoticeSeen(seen: boolean): void;
|
|
45
63
|
/**
|
|
46
64
|
* Migration-only: fill in fields that are still at their default, and never
|
|
47
65
|
* overwrite a value already here. A second import — after a half-finished
|
package/dist/config/ui-state.js
CHANGED
|
@@ -35,6 +35,7 @@ const DEFAULT_UI_STATE = {
|
|
|
35
35
|
schema: UI_STATE_SCHEMA,
|
|
36
36
|
submitCount: 0,
|
|
37
37
|
mouseNoticeSeen: false,
|
|
38
|
+
vtInputNoticeSeen: false,
|
|
38
39
|
};
|
|
39
40
|
let memory = null;
|
|
40
41
|
function coerceCount(value) {
|
|
@@ -64,6 +65,9 @@ function readFromDisk() {
|
|
|
64
65
|
mouseNoticeSeen: typeof parsed.mouseNoticeSeen === 'boolean'
|
|
65
66
|
? parsed.mouseNoticeSeen
|
|
66
67
|
: DEFAULT_UI_STATE.mouseNoticeSeen,
|
|
68
|
+
vtInputNoticeSeen: typeof parsed.vtInputNoticeSeen === 'boolean'
|
|
69
|
+
? parsed.vtInputNoticeSeen
|
|
70
|
+
: DEFAULT_UI_STATE.vtInputNoticeSeen,
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
catch {
|
|
@@ -123,6 +127,12 @@ export function getMouseNoticeSeen() {
|
|
|
123
127
|
export function setMouseNoticeSeen(seen) {
|
|
124
128
|
update({ mouseNoticeSeen: seen });
|
|
125
129
|
}
|
|
130
|
+
export function getVtInputNoticeSeen() {
|
|
131
|
+
return loadUiState().vtInputNoticeSeen;
|
|
132
|
+
}
|
|
133
|
+
export function setVtInputNoticeSeen(seen) {
|
|
134
|
+
update({ vtInputNoticeSeen: seen });
|
|
135
|
+
}
|
|
126
136
|
/**
|
|
127
137
|
* Migration-only: fill in fields that are still at their default, and never
|
|
128
138
|
* overwrite a value already here. A second import — after a half-finished
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aragon info` / `aragon doctor` commander wiring
|
|
3
|
+
* (cli-integration-surface section 4.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/diagnostics/**` is inside the glyph scanner's scope.
|
|
6
|
+
*/
|
|
7
|
+
import type { Command } from 'commander';
|
|
8
|
+
import type { CliFlags } from '../config/load.js';
|
|
9
|
+
export declare function registerInfoCommand(program: Command, toFlags: (opts: Record<string, unknown>) => CliFlags, version: string): void;
|
|
10
|
+
export declare function registerDoctorCommand(program: Command, toFlags: (opts: Record<string, unknown>) => CliFlags): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aragon info` / `aragon doctor` commander wiring
|
|
3
|
+
* (cli-integration-surface section 4.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/diagnostics/**` is inside the glyph scanner's scope.
|
|
6
|
+
*/
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import { buildInfo } from './info.js';
|
|
9
|
+
import { runDoctorChecks } from './doctor.js';
|
|
10
|
+
export function registerInfoCommand(program, toFlags, version) {
|
|
11
|
+
program
|
|
12
|
+
.command('info')
|
|
13
|
+
.description('Print what this installation supports (for wrappers and CI)')
|
|
14
|
+
.option('--json', 'Machine-readable output')
|
|
15
|
+
.action((opts) => {
|
|
16
|
+
const info = buildInfo(toFlags(program.opts()), version);
|
|
17
|
+
if (opts.json) {
|
|
18
|
+
process.stdout.write(`${JSON.stringify(info, null, 2)}\n`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
process.stdout.write([
|
|
22
|
+
`aragon ${info.cli} (core ${info.core}, node ${info.node})`,
|
|
23
|
+
`schema ${info.schemaVersion}`,
|
|
24
|
+
`home ${info.home}`,
|
|
25
|
+
`config ${info.configPath}`,
|
|
26
|
+
`sessions ${info.sessionsDir}`,
|
|
27
|
+
`model ${info.provider}/${info.model}${info.hasApiKey ? '' : ' (no API key)'}`,
|
|
28
|
+
`formats out: ${info.outputFormats.join(', ')} | in: ${info.inputFormats.join(', ')}`,
|
|
29
|
+
`modes ${info.permissionModes.join(', ')}`,
|
|
30
|
+
`features ${info.features.join(', ')}`,
|
|
31
|
+
'',
|
|
32
|
+
].join('\n'));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function registerDoctorCommand(program, toFlags) {
|
|
36
|
+
program
|
|
37
|
+
.command('doctor')
|
|
38
|
+
.description('Check that this machine is configured to run aragon (exit 0 = all clear)')
|
|
39
|
+
.option('--json', 'Machine-readable output')
|
|
40
|
+
.option('--probe', 'Also make one minimal API call against the configured provider')
|
|
41
|
+
.action(async (opts) => {
|
|
42
|
+
const report = await runDoctorChecks(toFlags(program.opts()), {
|
|
43
|
+
...(opts.probe ? { probe: true } : {}),
|
|
44
|
+
});
|
|
45
|
+
if (opts.json) {
|
|
46
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
for (const check of report.checks)
|
|
50
|
+
process.stdout.write(`${renderCheck(check)}\n`);
|
|
51
|
+
}
|
|
52
|
+
// ONLY `fail` MOVES THE EXIT CODE. A `warn` is information; failing on one
|
|
53
|
+
// would make `aragon doctor` unusable in exactly the CI job it is for.
|
|
54
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function renderCheck(check) {
|
|
58
|
+
const label = check.verdict.toUpperCase().padEnd(4);
|
|
59
|
+
const head = `[${label}] ${check.name.padEnd(9)} ${check.detail}`;
|
|
60
|
+
return check.remedy ? `${head}\n ${check.remedy}` : head;
|
|
61
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 { type CliFlags } from '../config/load.js';
|
|
13
|
+
export type CheckVerdict = 'pass' | 'warn' | 'fail';
|
|
14
|
+
export interface DoctorCheck {
|
|
15
|
+
name: string;
|
|
16
|
+
verdict: CheckVerdict;
|
|
17
|
+
detail: string;
|
|
18
|
+
/** What to do about it. Empty only for a `pass`. */
|
|
19
|
+
remedy: string;
|
|
20
|
+
}
|
|
21
|
+
export interface DoctorReport {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
checks: DoctorCheck[];
|
|
24
|
+
}
|
|
25
|
+
export interface DoctorOptions {
|
|
26
|
+
/** One minimal API call against the configured provider. */
|
|
27
|
+
probe?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function runDoctorChecks(flags: CliFlags, opts?: DoctorOptions): Promise<DoctorReport>;
|
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
const MIN_NODE_MAJOR = 18;
|
|
21
|
+
export async function runDoctorChecks(flags, opts = {}) {
|
|
22
|
+
const checks = [checkNode(), checkHome(), checkConfigFile()];
|
|
23
|
+
const config = loadConfig(flags);
|
|
24
|
+
checks.push(checkProviderKey(config));
|
|
25
|
+
checks.push(checkModel(config));
|
|
26
|
+
checks.push(checkSessionsDir());
|
|
27
|
+
if (opts.probe)
|
|
28
|
+
checks.push(await probeProvider(config));
|
|
29
|
+
return { ok: checks.every((c) => c.verdict !== 'fail'), checks };
|
|
30
|
+
}
|
|
31
|
+
function pass(name, detail) {
|
|
32
|
+
return { name, verdict: 'pass', detail, remedy: '' };
|
|
33
|
+
}
|
|
34
|
+
function checkNode() {
|
|
35
|
+
const major = Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10);
|
|
36
|
+
if (major >= MIN_NODE_MAJOR)
|
|
37
|
+
return pass('node', `v${process.versions.node}`);
|
|
38
|
+
return {
|
|
39
|
+
name: 'node',
|
|
40
|
+
verdict: 'fail',
|
|
41
|
+
detail: `v${process.versions.node} is below the supported floor`,
|
|
42
|
+
remedy: `Install Node ${MIN_NODE_MAJOR} or newer.`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function checkHome() {
|
|
46
|
+
const home = getUserDataDir();
|
|
47
|
+
const warning = getHomeResolutionWarning();
|
|
48
|
+
try {
|
|
49
|
+
if (!existsSync(home))
|
|
50
|
+
mkdirSync(home, { recursive: true });
|
|
51
|
+
const probe = join(home, '.doctor-write-probe');
|
|
52
|
+
writeFileSync(probe, 'ok', 'utf-8');
|
|
53
|
+
unlinkSync(probe);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return {
|
|
57
|
+
name: 'home',
|
|
58
|
+
verdict: 'fail',
|
|
59
|
+
detail: `${home} is not writable (${err.message})`,
|
|
60
|
+
remedy: 'Fix the permissions, or point ARAGON_HOME at a directory you own.',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (warning) {
|
|
64
|
+
return {
|
|
65
|
+
name: 'home',
|
|
66
|
+
verdict: 'warn',
|
|
67
|
+
detail: `${home} (${warning})`,
|
|
68
|
+
remedy: 'Set ARAGON_HOME to an absolute path if this is not where you want state kept.',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return pass('home', home);
|
|
72
|
+
}
|
|
73
|
+
function checkConfigFile() {
|
|
74
|
+
const { parseError } = readConfigFile();
|
|
75
|
+
if (!parseError)
|
|
76
|
+
return pass('config', 'parses');
|
|
77
|
+
return {
|
|
78
|
+
name: 'config',
|
|
79
|
+
verdict: 'warn',
|
|
80
|
+
detail: `using defaults, could not parse ${parseError}`,
|
|
81
|
+
remedy: 'Run `aragon config edit` and fix the JSON, or delete the file to start over.',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function checkProviderKey(config) {
|
|
85
|
+
const key = makeGetApiKey(config)(config.provider);
|
|
86
|
+
if (key)
|
|
87
|
+
return pass('api-key', `resolved for ${config.provider}`);
|
|
88
|
+
return {
|
|
89
|
+
name: 'api-key',
|
|
90
|
+
verdict: 'fail',
|
|
91
|
+
detail: `no API key for provider "${config.provider}"`,
|
|
92
|
+
remedy: `Run \`aragon config\` and add one, or set the provider's key in the environment.`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function checkModel(config) {
|
|
96
|
+
if (!isAdapterProvider(config.provider)) {
|
|
97
|
+
return {
|
|
98
|
+
name: 'model',
|
|
99
|
+
verdict: 'fail',
|
|
100
|
+
detail: `unknown provider "${config.provider}"`,
|
|
101
|
+
remedy: 'Run `aragon models` to see the providers this build supports.',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const registry = new ModelRegistry(initProviders({ retryPolicy: toRetryPolicy(config.retry) }));
|
|
105
|
+
const known = registry.getModels(config.provider).some((m) => m.id === config.model);
|
|
106
|
+
if (known)
|
|
107
|
+
return pass('model', `${config.provider}/${config.model}`);
|
|
108
|
+
// WARN, NOT FAIL. The registry is a builtin list plus whatever an account can
|
|
109
|
+
// discover, so a model it does not know is frequently one this account has and
|
|
110
|
+
// the shipped table does not - the cost table is unknown, the run is fine.
|
|
111
|
+
return {
|
|
112
|
+
name: 'model',
|
|
113
|
+
verdict: 'warn',
|
|
114
|
+
detail: `${config.provider}/${config.model} is not in the builtin registry`,
|
|
115
|
+
remedy: 'Run `aragon models` to list what this key can reach; cost reporting will be off.',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function checkSessionsDir() {
|
|
119
|
+
const dir = getSessionsDir();
|
|
120
|
+
try {
|
|
121
|
+
if (!existsSync(dir))
|
|
122
|
+
mkdirSync(dir, { recursive: true });
|
|
123
|
+
const probe = join(dir, '.doctor-write-probe');
|
|
124
|
+
writeFileSync(probe, 'ok', 'utf-8');
|
|
125
|
+
unlinkSync(probe);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
return {
|
|
129
|
+
name: 'sessions',
|
|
130
|
+
verdict: 'fail',
|
|
131
|
+
detail: `${dir} is not writable (${err.message})`,
|
|
132
|
+
remedy: 'Fix the permissions, or run `aragon exec --no-save-session`.',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return pass('sessions', dir);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* `--probe`: one minimal API call.
|
|
139
|
+
*
|
|
140
|
+
* MODEL DISCOVERY IS THE CHEAPEST HONEST PROBE. It exercises the base URL, the
|
|
141
|
+
* key and the TLS chain - which is the whole of what "can this machine reach the
|
|
142
|
+
* provider" means - without spending a single completion token.
|
|
143
|
+
*/
|
|
144
|
+
async function probeProvider(config) {
|
|
145
|
+
const key = makeGetApiKey(config)(config.provider);
|
|
146
|
+
if (!key || !isAdapterProvider(config.provider)) {
|
|
147
|
+
return {
|
|
148
|
+
name: 'probe',
|
|
149
|
+
verdict: 'warn',
|
|
150
|
+
detail: 'skipped: no key or unknown provider',
|
|
151
|
+
remedy: 'Fix the api-key check above, then run `aragon doctor --probe` again.',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const registry = new ModelRegistry(initProviders({ retryPolicy: toRetryPolicy(config.retry) }));
|
|
155
|
+
try {
|
|
156
|
+
const models = await registry.discoverModels(config.provider, key, config.baseUrl);
|
|
157
|
+
return pass('probe', `${config.provider} answered with ${models.length} model(s)`);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
return {
|
|
161
|
+
name: 'probe',
|
|
162
|
+
verdict: 'fail',
|
|
163
|
+
detail: `${config.provider} did not answer (${err.message})`,
|
|
164
|
+
remedy: 'Check the key, the base URL and any proxy between this machine and the provider.',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -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;
|