@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.
- package/CHANGELOG.md +292 -0
- package/README.md +558 -0
- package/dist/agent/coalesce.d.ts +14 -0
- package/dist/agent/coalesce.js +41 -0
- package/dist/agent/controller.d.ts +132 -0
- package/dist/agent/controller.js +332 -0
- package/dist/agent/headless.d.ts +22 -0
- package/dist/agent/headless.js +74 -0
- package/dist/agent/reducer.d.ts +181 -0
- package/dist/agent/reducer.js +420 -0
- package/dist/agent/system-prompt.d.ts +30 -0
- package/dist/agent/system-prompt.js +58 -0
- package/dist/agent/usage.d.ts +13 -0
- package/dist/agent/usage.js +41 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +482 -0
- package/dist/commands/builtins.d.ts +16 -0
- package/dist/commands/builtins.js +223 -0
- package/dist/commands/registry.d.ts +48 -0
- package/dist/commands/registry.js +45 -0
- package/dist/commands/skills.d.ts +22 -0
- package/dist/commands/skills.js +625 -0
- package/dist/config/app-paths.d.ts +11 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/env.d.ts +28 -0
- package/dist/config/env.js +92 -0
- package/dist/config/load.d.ts +48 -0
- package/dist/config/load.js +158 -0
- package/dist/config/migrate-legacy-state.d.ts +78 -0
- package/dist/config/migrate-legacy-state.js +181 -0
- package/dist/config/schema.d.ts +249 -0
- package/dist/config/schema.js +247 -0
- package/dist/config/store.d.ts +44 -0
- package/dist/config/store.js +126 -0
- package/dist/input/keymap.d.ts +20 -0
- package/dist/input/keymap.js +85 -0
- package/dist/session/persist.d.ts +24 -0
- package/dist/session/persist.js +51 -0
- package/dist/skills/archive.d.ts +41 -0
- package/dist/skills/archive.js +152 -0
- package/dist/skills/cli-commands.d.ts +52 -0
- package/dist/skills/cli-commands.js +502 -0
- package/dist/skills/fetch-source.d.ts +104 -0
- package/dist/skills/fetch-source.js +448 -0
- package/dist/skills/installer.d.ts +117 -0
- package/dist/skills/installer.js +414 -0
- package/dist/skills/lock.d.ts +75 -0
- package/dist/skills/lock.js +223 -0
- package/dist/skills/manifest.d.ts +47 -0
- package/dist/skills/manifest.js +95 -0
- package/dist/skills/node-host.d.ts +23 -0
- package/dist/skills/node-host.js +59 -0
- package/dist/skills/paths.d.ts +61 -0
- package/dist/skills/paths.js +107 -0
- package/dist/skills/service.d.ts +256 -0
- package/dist/skills/service.js +730 -0
- package/dist/skills/tools.d.ts +35 -0
- package/dist/skills/tools.js +164 -0
- package/dist/skills/updater.d.ts +101 -0
- package/dist/skills/updater.js +330 -0
- package/dist/skills/usage.d.ts +72 -0
- package/dist/skills/usage.js +216 -0
- package/dist/tools/bash-tool.d.ts +12 -0
- package/dist/tools/bash-tool.js +131 -0
- package/dist/tools/diff.d.ts +15 -0
- package/dist/tools/diff.js +50 -0
- package/dist/tools/fs-tools.d.ts +16 -0
- package/dist/tools/fs-tools.js +215 -0
- package/dist/tools/index.d.ts +93 -0
- package/dist/tools/index.js +157 -0
- package/dist/tools/search-tools.d.ts +11 -0
- package/dist/tools/search-tools.js +225 -0
- package/dist/ui/App.d.ts +24 -0
- package/dist/ui/App.js +522 -0
- package/dist/ui/AutocompletePopup.d.ts +21 -0
- package/dist/ui/AutocompletePopup.js +19 -0
- package/dist/ui/Composer.d.ts +40 -0
- package/dist/ui/Composer.js +61 -0
- package/dist/ui/Header.d.ts +30 -0
- package/dist/ui/Header.js +25 -0
- package/dist/ui/Logo.d.ts +50 -0
- package/dist/ui/Logo.js +85 -0
- package/dist/ui/Markdown.d.ts +23 -0
- package/dist/ui/Markdown.js +122 -0
- package/dist/ui/PromptInput.d.ts +81 -0
- package/dist/ui/PromptInput.js +383 -0
- package/dist/ui/SessionOpener.d.ts +30 -0
- package/dist/ui/SessionOpener.js +12 -0
- package/dist/ui/StatusBar.d.ts +39 -0
- package/dist/ui/StatusBar.js +43 -0
- package/dist/ui/ToastStack.d.ts +24 -0
- package/dist/ui/ToastStack.js +38 -0
- package/dist/ui/Transcript.d.ts +62 -0
- package/dist/ui/Transcript.js +128 -0
- package/dist/ui/capabilities.d.ts +25 -0
- package/dist/ui/capabilities.js +59 -0
- package/dist/ui/console-bridge.d.ts +19 -0
- package/dist/ui/console-bridge.js +52 -0
- package/dist/ui/density.d.ts +26 -0
- package/dist/ui/density.js +34 -0
- package/dist/ui/entries/AssistantEntry.d.ts +23 -0
- package/dist/ui/entries/AssistantEntry.js +13 -0
- package/dist/ui/entries/EntryFrame.d.ts +23 -0
- package/dist/ui/entries/EntryFrame.js +6 -0
- package/dist/ui/entries/ToolCard.d.ts +38 -0
- package/dist/ui/entries/ToolCard.js +72 -0
- package/dist/ui/entries/ToolPreview.d.ts +57 -0
- package/dist/ui/entries/ToolPreview.js +144 -0
- package/dist/ui/entries/UserEntry.d.ts +14 -0
- package/dist/ui/entries/UserEntry.js +5 -0
- package/dist/ui/exit-snapshot.d.ts +27 -0
- package/dist/ui/exit-snapshot.js +26 -0
- package/dist/ui/gauge.d.ts +24 -0
- package/dist/ui/gauge.js +36 -0
- package/dist/ui/glyphs.d.ts +87 -0
- package/dist/ui/glyphs.js +127 -0
- package/dist/ui/gradient.d.ts +11 -0
- package/dist/ui/gradient.js +50 -0
- package/dist/ui/layout/AppShell.d.ts +24 -0
- package/dist/ui/layout/AppShell.js +9 -0
- package/dist/ui/layout/Gutter.d.ts +67 -0
- package/dist/ui/layout/Gutter.js +42 -0
- package/dist/ui/layout/OverlayFrame.d.ts +62 -0
- package/dist/ui/layout/OverlayFrame.js +92 -0
- package/dist/ui/layout/ScrollViewport.d.ts +33 -0
- package/dist/ui/layout/ScrollViewport.js +71 -0
- package/dist/ui/layout/budget.d.ts +42 -0
- package/dist/ui/layout/budget.js +47 -0
- package/dist/ui/layout/frame.d.ts +58 -0
- package/dist/ui/layout/frame.js +75 -0
- package/dist/ui/layout/overlay-window.d.ts +43 -0
- package/dist/ui/layout/overlay-window.js +51 -0
- package/dist/ui/layout/scroll.d.ts +28 -0
- package/dist/ui/layout/scroll.js +76 -0
- package/dist/ui/layout/useTerminalSize.d.ts +13 -0
- package/dist/ui/layout/useTerminalSize.js +50 -0
- package/dist/ui/markdown-blocks.d.ts +62 -0
- package/dist/ui/markdown-blocks.js +120 -0
- package/dist/ui/overlays/ConfirmDialog.d.ts +30 -0
- package/dist/ui/overlays/ConfirmDialog.js +25 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +17 -0
- package/dist/ui/overlays/HelpOverlay.js +60 -0
- package/dist/ui/overlays/ModelPicker.d.ts +29 -0
- package/dist/ui/overlays/ModelPicker.js +29 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +37 -0
- package/dist/ui/overlays/SettingsScreen.js +106 -0
- package/dist/ui/palettes.d.ts +79 -0
- package/dist/ui/palettes.js +115 -0
- package/dist/ui/screen.d.ts +27 -0
- package/dist/ui/screen.js +57 -0
- package/dist/ui/theme.d.ts +78 -0
- package/dist/ui/theme.js +127 -0
- package/dist/ui/transcript-text.d.ts +27 -0
- package/dist/ui/transcript-text.js +110 -0
- package/package.json +73 -0
- package/skills/skill-creator/SKILL.md +96 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment + `.env` loading and mapping to config fields.
|
|
3
|
+
*
|
|
4
|
+
* Recognized keys (spec §3.5):
|
|
5
|
+
* - Provider secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY /
|
|
6
|
+
* GEMINI_API_KEY.
|
|
7
|
+
* - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
|
|
8
|
+
* ARAGON_MAX_TOKENS, ARAGON_THEME.
|
|
9
|
+
*/
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import process from 'node:process';
|
|
12
|
+
import dotenv from 'dotenv';
|
|
13
|
+
import { clampTheme, clampThinkingLevel, coerceMaxTokens, } from './schema.js';
|
|
14
|
+
/**
|
|
15
|
+
* Load a project `.env` from the given directory into `process.env` (existing
|
|
16
|
+
* process env wins — dotenv never overrides already-set variables).
|
|
17
|
+
*/
|
|
18
|
+
export function loadDotenv(cwd) {
|
|
19
|
+
dotenv.config({ path: join(cwd, '.env') });
|
|
20
|
+
}
|
|
21
|
+
/** Provider -> the env vars that may carry its key (first non-empty wins). */
|
|
22
|
+
const PROVIDER_ENV_KEYS = {
|
|
23
|
+
anthropic: ['ANTHROPIC_API_KEY'],
|
|
24
|
+
openai: ['OPENAI_API_KEY'],
|
|
25
|
+
google: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
|
|
26
|
+
};
|
|
27
|
+
function firstEnv(names) {
|
|
28
|
+
for (const name of names) {
|
|
29
|
+
const v = process.env[name];
|
|
30
|
+
if (v && v.trim().length > 0)
|
|
31
|
+
return v.trim();
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read ARAGON_* overrides and provider secrets from the current environment.
|
|
37
|
+
* Must be called after `loadDotenv()` so `.env` values are visible.
|
|
38
|
+
*/
|
|
39
|
+
export function readEnvConfig() {
|
|
40
|
+
const partial = {};
|
|
41
|
+
const provider = process.env.ARAGON_PROVIDER?.trim();
|
|
42
|
+
if (provider)
|
|
43
|
+
partial.provider = provider;
|
|
44
|
+
const model = process.env.ARAGON_MODEL?.trim();
|
|
45
|
+
if (model)
|
|
46
|
+
partial.model = model;
|
|
47
|
+
const baseUrl = process.env.ARAGON_BASE_URL?.trim();
|
|
48
|
+
if (baseUrl)
|
|
49
|
+
partial.baseUrl = baseUrl;
|
|
50
|
+
if (process.env.ARAGON_THINKING) {
|
|
51
|
+
partial.thinkingLevel = clampThinkingLevel(process.env.ARAGON_THINKING.trim(), 'off');
|
|
52
|
+
}
|
|
53
|
+
if (process.env.ARAGON_MAX_TOKENS) {
|
|
54
|
+
const mt = coerceMaxTokens(process.env.ARAGON_MAX_TOKENS);
|
|
55
|
+
partial.maxTokens = mt ?? null;
|
|
56
|
+
}
|
|
57
|
+
if (process.env.ARAGON_THEME) {
|
|
58
|
+
partial.theme = clampTheme(process.env.ARAGON_THEME.trim(), 'auto');
|
|
59
|
+
}
|
|
60
|
+
// ARAGON_FULLSCREEN=0|1 — the one channel that works where neither a flag nor
|
|
61
|
+
// a config file is reachable (containers, SSH, being spawned by another tool).
|
|
62
|
+
const fullscreen = process.env.ARAGON_FULLSCREEN?.trim().toLowerCase();
|
|
63
|
+
if (fullscreen !== undefined && fullscreen.length > 0) {
|
|
64
|
+
partial.fullscreen = fullscreen === '1' || fullscreen === 'true' || fullscreen === 'on' || fullscreen === 'yes';
|
|
65
|
+
}
|
|
66
|
+
// ARAGON_SKILLS=0 is the kill switch for the whole skill subsystem; the other
|
|
67
|
+
// two skill env vars (ARAGON_SKILLS_PATH, ARAGON_SKILLS_DISABLED) are read where
|
|
68
|
+
// they are used — the search-root resolver and the service respectively —
|
|
69
|
+
// because neither maps onto a single persisted field.
|
|
70
|
+
const skills = process.env.ARAGON_SKILLS?.trim().toLowerCase();
|
|
71
|
+
if (skills !== undefined && skills.length > 0) {
|
|
72
|
+
partial.skills = {
|
|
73
|
+
...(partial.skills ?? {}),
|
|
74
|
+
enabled: !(skills === '0' || skills === 'false' || skills === 'off' || skills === 'no'),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const apiKeys = {};
|
|
78
|
+
for (const [prov, names] of Object.entries(PROVIDER_ENV_KEYS)) {
|
|
79
|
+
apiKeys[prov] = firstEnv(names);
|
|
80
|
+
}
|
|
81
|
+
return { partial, apiKeys };
|
|
82
|
+
}
|
|
83
|
+
/** Skill names disabled for this run via `ARAGON_SKILLS_DISABLED` (comma-separated). */
|
|
84
|
+
export function readEnvDisabledSkills() {
|
|
85
|
+
const raw = process.env.ARAGON_SKILLS_DISABLED;
|
|
86
|
+
if (!raw || raw.trim().length === 0)
|
|
87
|
+
return [];
|
|
88
|
+
return raw
|
|
89
|
+
.split(',')
|
|
90
|
+
.map((s) => s.trim())
|
|
91
|
+
.filter((s) => s.length > 0);
|
|
92
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layered config resolution: defaults › user file › env/.env › CLI flags.
|
|
3
|
+
*
|
|
4
|
+
* Also enforces the timeout invariant (spec §3.8 / R1): after merging all
|
|
5
|
+
* layers, `idleTimeout` is re-derived to be strictly ≥ `toolTimeout` so a
|
|
6
|
+
* legitimately long single tool run can never be watchdog-killed.
|
|
7
|
+
*/
|
|
8
|
+
import { type CliConfig } from './schema.js';
|
|
9
|
+
export interface CliFlags {
|
|
10
|
+
provider?: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
thinking?: string;
|
|
15
|
+
maxTokens?: string | number;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
theme?: string;
|
|
18
|
+
color?: boolean;
|
|
19
|
+
confirm?: boolean;
|
|
20
|
+
toolTimeout?: string | number;
|
|
21
|
+
idleTimeout?: string | number;
|
|
22
|
+
/** `--fullscreen` ⇒ true, `--no-fullscreen` ⇒ false, absent ⇒ undefined. */
|
|
23
|
+
fullscreen?: boolean;
|
|
24
|
+
/** `--no-exit-transcript` ⇒ false. */
|
|
25
|
+
exitTranscript?: boolean;
|
|
26
|
+
/** `--compact` ⇒ true, `--no-compact` ⇒ false, absent ⇒ undefined. */
|
|
27
|
+
compact?: boolean;
|
|
28
|
+
/** `--hints` ⇒ true, `--no-hints` ⇒ false, absent ⇒ undefined. */
|
|
29
|
+
hints?: boolean;
|
|
30
|
+
/** `--no-skills` ⇒ false. Turns the whole skill subsystem off (§7.5). */
|
|
31
|
+
skills?: boolean;
|
|
32
|
+
/** `--skill <name>` ×N — force Level 2 injection for this run. */
|
|
33
|
+
skill?: string[];
|
|
34
|
+
/** `--skills-yes` — approve skill installs for this run only. */
|
|
35
|
+
skillsYes?: boolean;
|
|
36
|
+
/** `--skill-tool-policy <mode>` — override the tool ceiling for this run (§5.5). */
|
|
37
|
+
skillToolPolicy?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the effective config by merging all layers, then derive the runtime
|
|
41
|
+
* (non-persisted) fields and enforce the timeout invariant.
|
|
42
|
+
*/
|
|
43
|
+
export declare function loadConfig(flags?: CliFlags): CliConfig;
|
|
44
|
+
/**
|
|
45
|
+
* Build the `getApiKey(providerId)` resolver the core `Agent` consumes.
|
|
46
|
+
* The one-shot `--api-key` override wins, but only for the active provider.
|
|
47
|
+
*/
|
|
48
|
+
export declare function makeGetApiKey(config: CliConfig): (providerId: string) => string | undefined;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layered config resolution: defaults › user file › env/.env › CLI flags.
|
|
3
|
+
*
|
|
4
|
+
* Also enforces the timeout invariant (spec §3.8 / R1): after merging all
|
|
5
|
+
* layers, `idleTimeout` is re-derived to be strictly ≥ `toolTimeout` so a
|
|
6
|
+
* legitimately long single tool run can never be watchdog-killed.
|
|
7
|
+
*/
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampDensity, clampSkillsConfig, clampTheme, clampThinkingLevel, clampTranscriptWindow, coerceMaxTokens, coercePositiveInt, isSkillsToolPolicyMode, } from './schema.js';
|
|
10
|
+
import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
|
|
11
|
+
import { readConfigFile } from './store.js';
|
|
12
|
+
import { detectCapabilities } from '../ui/capabilities.js';
|
|
13
|
+
/** Interpret a boolean-ish env value (`1/true/on/yes`) as `true`. */
|
|
14
|
+
function envFlagTrue(value) {
|
|
15
|
+
if (!value)
|
|
16
|
+
return false;
|
|
17
|
+
const v = value.trim().toLowerCase();
|
|
18
|
+
return v === '1' || v === 'true' || v === 'on' || v === 'yes';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the tri-state full-screen preference (see `CliConfig.fullscreen`).
|
|
22
|
+
* Only a flag or the env var counts as an explicit FORCE; a config file holding
|
|
23
|
+
* the default `true` must not disable the automatic downgrades.
|
|
24
|
+
*/
|
|
25
|
+
function resolveFullscreen(flags, env, file) {
|
|
26
|
+
if (flags.fullscreen !== undefined)
|
|
27
|
+
return flags.fullscreen;
|
|
28
|
+
if (env.fullscreen !== undefined)
|
|
29
|
+
return env.fullscreen;
|
|
30
|
+
if (file.fullscreen === false)
|
|
31
|
+
return false;
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// loadConfig
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
function pick(...vals) {
|
|
38
|
+
for (const v of vals) {
|
|
39
|
+
if (v !== undefined && v !== null && v !== '')
|
|
40
|
+
return v;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the effective config by merging all layers, then derive the runtime
|
|
46
|
+
* (non-persisted) fields and enforce the timeout invariant.
|
|
47
|
+
*/
|
|
48
|
+
export function loadConfig(flags = {}) {
|
|
49
|
+
const cwd = flags.cwd ? flags.cwd : process.cwd();
|
|
50
|
+
// `.env` must be loaded before reading env overrides.
|
|
51
|
+
loadDotenv(cwd);
|
|
52
|
+
const file = readConfigFile() ?? {};
|
|
53
|
+
const env = readEnvConfig();
|
|
54
|
+
const provider = pick(flags.provider, env.partial.provider, file.provider) ?? DEFAULT_CONFIG.provider;
|
|
55
|
+
const model = pick(flags.model, env.partial.model, file.model) ?? DEFAULT_CONFIG.model;
|
|
56
|
+
const baseUrl = pick(flags.baseUrl, env.partial.baseUrl ?? undefined, file.baseUrl ?? undefined);
|
|
57
|
+
const thinkingLevel = clampThinkingLevel(pick(flags.thinking, env.partial.thinkingLevel, file.thinkingLevel), DEFAULT_CONFIG.thinkingLevel);
|
|
58
|
+
const maxTokens = coerceMaxTokens(pick(flags.maxTokens, env.partial.maxTokens, file.maxTokens));
|
|
59
|
+
// `clampTheme` is also where the v0.3.0 `dark` name is migrated to `cool`,
|
|
60
|
+
// so reading a legacy config file needs no extra step here (§4.5 / R-4).
|
|
61
|
+
const theme = clampTheme(pick(flags.theme, env.partial.theme, file.theme), DEFAULT_CONFIG.theme);
|
|
62
|
+
const density = flags.compact !== undefined
|
|
63
|
+
? flags.compact
|
|
64
|
+
? 'compact'
|
|
65
|
+
: 'comfortable'
|
|
66
|
+
: clampDensity(file.density, DEFAULT_CONFIG.density);
|
|
67
|
+
const hints = flags.hints !== undefined ? flags.hints : file.hints ?? DEFAULT_CONFIG.hints;
|
|
68
|
+
const confirmTools = flags.confirm !== undefined ? flags.confirm : file.confirmTools ?? DEFAULT_CONFIG.confirmTools;
|
|
69
|
+
const toolTimeoutMs = coercePositiveInt(pick(flags.toolTimeout, file.toolTimeoutMs), DEFAULT_CONFIG.toolTimeoutMs);
|
|
70
|
+
let idleTimeoutMs = coercePositiveInt(pick(flags.idleTimeout, file.idleTimeoutMs), DEFAULT_CONFIG.idleTimeoutMs);
|
|
71
|
+
// Timeout invariant (R1): idle must be strictly greater than the tool ceiling
|
|
72
|
+
// so a long single tool run is never aborted by the idle watchdog.
|
|
73
|
+
idleTimeoutMs = Math.max(idleTimeoutMs, toolTimeoutMs + IDLE_TIMEOUT_MARGIN_MS);
|
|
74
|
+
// Resolve API keys: config-file key wins over the provider env var.
|
|
75
|
+
const fileKeys = file.apiKeys ?? {};
|
|
76
|
+
const apiKeys = {};
|
|
77
|
+
const providerSet = new Set([
|
|
78
|
+
...Object.keys(env.apiKeys),
|
|
79
|
+
...Object.keys(fileKeys),
|
|
80
|
+
provider,
|
|
81
|
+
]);
|
|
82
|
+
for (const prov of providerSet) {
|
|
83
|
+
const fileKey = fileKeys[prov];
|
|
84
|
+
apiKeys[prov] =
|
|
85
|
+
fileKey && fileKey.trim().length > 0 ? fileKey.trim() : env.apiKeys[prov];
|
|
86
|
+
}
|
|
87
|
+
// Skills: file › env › flags, with `--no-skills` as the final word. The env
|
|
88
|
+
// layer only ever carries `enabled`, so the file's other fields survive it.
|
|
89
|
+
const skills = clampSkillsConfig({
|
|
90
|
+
...DEFAULT_CONFIG.skills,
|
|
91
|
+
...(file.skills ?? {}),
|
|
92
|
+
...(env.partial.skills ?? {}),
|
|
93
|
+
...(flags.skills === false ? { enabled: false } : {}),
|
|
94
|
+
disabled: [
|
|
95
|
+
...new Set([...(file.skills?.disabled ?? []), ...readEnvDisabledSkills()]),
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
const color = flags.color !== undefined ? flags.color : !process.env.NO_COLOR;
|
|
99
|
+
// `--no-color` implies calmer chrome; env / file may also opt in explicitly.
|
|
100
|
+
const reducedMotion = envFlagTrue(process.env.ARAGON_REDUCED_MOTION) ||
|
|
101
|
+
(file.reducedMotion ?? DEFAULT_CONFIG.reducedMotion) ||
|
|
102
|
+
!color;
|
|
103
|
+
// Detect terminal capabilities once; force monochrome when color is disabled.
|
|
104
|
+
const caps = detectCapabilities(process.env, process.stdout);
|
|
105
|
+
const colorLevel = color ? caps.colorLevel : 0;
|
|
106
|
+
return {
|
|
107
|
+
provider,
|
|
108
|
+
model,
|
|
109
|
+
baseUrl,
|
|
110
|
+
thinkingLevel,
|
|
111
|
+
maxTokens,
|
|
112
|
+
theme,
|
|
113
|
+
reducedMotion,
|
|
114
|
+
fullscreen: resolveFullscreen(flags, env.partial, file),
|
|
115
|
+
exitTranscript: flags.exitTranscript !== undefined
|
|
116
|
+
? flags.exitTranscript
|
|
117
|
+
: file.exitTranscript ?? DEFAULT_CONFIG.exitTranscript,
|
|
118
|
+
transcriptWindow: clampTranscriptWindow(file.transcriptWindow, DEFAULT_CONFIG.transcriptWindow),
|
|
119
|
+
confirmTools,
|
|
120
|
+
toolTimeoutMs,
|
|
121
|
+
idleTimeoutMs,
|
|
122
|
+
apiKeys,
|
|
123
|
+
recentModels: file.recentModels ?? [],
|
|
124
|
+
promptHistory: file.promptHistory ?? [],
|
|
125
|
+
density,
|
|
126
|
+
hints,
|
|
127
|
+
submitCount: coercePositiveInt(file.submitCount, 0),
|
|
128
|
+
skills,
|
|
129
|
+
skillsRuntime: {
|
|
130
|
+
forcedSkills: flags.skill ?? [],
|
|
131
|
+
approveAll: flags.skillsYes === true,
|
|
132
|
+
// Left ABSENT when the flag was not given (or was given a bad value), so
|
|
133
|
+
// `effectiveToolPolicy()` can fall through to the config layer. Writing a
|
|
134
|
+
// resolved default here instead would make the flag look permanently
|
|
135
|
+
// supplied and mask the config file for the whole session.
|
|
136
|
+
...(isSkillsToolPolicyMode(flags.skillToolPolicy)
|
|
137
|
+
? { toolPolicy: flags.skillToolPolicy }
|
|
138
|
+
: {}),
|
|
139
|
+
},
|
|
140
|
+
cwd,
|
|
141
|
+
color,
|
|
142
|
+
colorLevel,
|
|
143
|
+
unicode: caps.unicode,
|
|
144
|
+
apiKeyOverride: flags.apiKey && flags.apiKey.trim().length > 0 ? flags.apiKey.trim() : undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Build the `getApiKey(providerId)` resolver the core `Agent` consumes.
|
|
149
|
+
* The one-shot `--api-key` override wins, but only for the active provider.
|
|
150
|
+
*/
|
|
151
|
+
export function makeGetApiKey(config) {
|
|
152
|
+
return (providerId) => {
|
|
153
|
+
if (config.apiKeyOverride && providerId === config.provider) {
|
|
154
|
+
return config.apiKeyOverride;
|
|
155
|
+
}
|
|
156
|
+
return config.apiKeys[providerId];
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot migration of the user's on-disk state from the pre-rename
|
|
3
|
+
* `argon-agent` namespace to `aragon-agent`.
|
|
4
|
+
*
|
|
5
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
* WHY THIS EXISTS: `envPaths()` derives the config and data directories from
|
|
7
|
+
* the application name, so changing the name relocates them. Without this
|
|
8
|
+
* module a user upgrading from 0.4.x would find their API key, their sessions
|
|
9
|
+
* and every installed skill gone — not renamed, gone, with the old directory
|
|
10
|
+
* still on disk where nobody thinks to look. That is data loss wearing a
|
|
11
|
+
* rename's clothing.
|
|
12
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
*
|
|
14
|
+
* The dividing line (spec §3.4): what the user cannot see, the code migrates
|
|
15
|
+
* for them; what the user can see — the project-level `.argon/skills` directory
|
|
16
|
+
* inside their own repository — the CHANGELOG tells them to `git mv`.
|
|
17
|
+
*
|
|
18
|
+
* THIS FUNCTION NEVER THROWS. It runs as the first statement of `main()`, and a
|
|
19
|
+
* migration failure must never be the reason the CLI will not start: a user who
|
|
20
|
+
* has to re-enter an API key is inconvenienced, a user whose CLI refuses to
|
|
21
|
+
* launch is blocked. Every failure path degrades to `{ mode: 'none', error }`.
|
|
22
|
+
*
|
|
23
|
+
* IDEMPOTENCE KEY is "does the new directory already exist" — not the
|
|
24
|
+
* breadcrumb file, which is written for support diagnosis only. Two cold starts
|
|
25
|
+
* racing each other therefore both observe `existsSync(next) === false` and
|
|
26
|
+
* both attempt the rename; the loser gets ENOENT (source already moved) or
|
|
27
|
+
* EPERM (Windows, target now busy), which the outer catch absorbs. That is the
|
|
28
|
+
* designed outcome, not an accident: the winner completed the whole migration,
|
|
29
|
+
* and the loser's view of the world is identical to the already-migrated
|
|
30
|
+
* branch. No lock file is introduced — a stale lock is precisely the class of
|
|
31
|
+
* cross-version residue step 5 exists to delete.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* The pre-rename `env-paths` application name. Deliberately declared here and
|
|
35
|
+
* not alongside `APP_NAME`: this module is the only consumer, and it is the
|
|
36
|
+
* only file in the package that is *supposed* to spell the old brand.
|
|
37
|
+
*/
|
|
38
|
+
export declare const LEGACY_APP_NAME = "argon-agent";
|
|
39
|
+
export declare const legacyAppPaths: import("env-paths").Paths;
|
|
40
|
+
/** Filesystem surface this module needs, so tests never touch a real user directory. */
|
|
41
|
+
export interface MigrationFsPort {
|
|
42
|
+
existsSync(path: string): boolean;
|
|
43
|
+
mkdirSync(path: string, options: {
|
|
44
|
+
recursive: true;
|
|
45
|
+
}): void;
|
|
46
|
+
renameSync(from: string, to: string): void;
|
|
47
|
+
cpSync(from: string, to: string, options: {
|
|
48
|
+
recursive: true;
|
|
49
|
+
}): void;
|
|
50
|
+
readdirSync(path: string): string[];
|
|
51
|
+
rmSync(path: string, options: {
|
|
52
|
+
force: true;
|
|
53
|
+
}): void;
|
|
54
|
+
writeFileSync(path: string, data: string): void;
|
|
55
|
+
}
|
|
56
|
+
export declare const nodeFsPort: MigrationFsPort;
|
|
57
|
+
export interface MigrationResult {
|
|
58
|
+
mode: 'none' | 'renamed' | 'copied';
|
|
59
|
+
/** Which roots actually moved — `'config'` and/or `'data'`. */
|
|
60
|
+
movedRoots: string[];
|
|
61
|
+
/** How many `.argon-skill.json` files were renamed to `.aragon-skill.json`. */
|
|
62
|
+
manifestsRenamed: number;
|
|
63
|
+
/** Populated instead of throwing. */
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
66
|
+
export declare function migrateLegacyState(fs?: MigrationFsPort): MigrationResult;
|
|
67
|
+
/**
|
|
68
|
+
* The one-line notice for a migration that did something.
|
|
69
|
+
*
|
|
70
|
+
* Callers MUST write this to stderr. `screen.ts` holds stdout to the invariant
|
|
71
|
+
* that `aragon -p "..." > out.txt` yields model output and nothing else, and
|
|
72
|
+
* one stray line here would break every scripted invocation.
|
|
73
|
+
*
|
|
74
|
+
* ASCII-only, and `->` rather than an arrow glyph: this prints before any
|
|
75
|
+
* terminal capability probe runs, so a legacy console would render a nicer
|
|
76
|
+
* character as mojibake. Same rule `glyphs.test.ts` enforces across the CLI.
|
|
77
|
+
*/
|
|
78
|
+
export declare function formatMigrationNotice(result: MigrationResult): string | null;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot migration of the user's on-disk state from the pre-rename
|
|
3
|
+
* `argon-agent` namespace to `aragon-agent`.
|
|
4
|
+
*
|
|
5
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
* WHY THIS EXISTS: `envPaths()` derives the config and data directories from
|
|
7
|
+
* the application name, so changing the name relocates them. Without this
|
|
8
|
+
* module a user upgrading from 0.4.x would find their API key, their sessions
|
|
9
|
+
* and every installed skill gone — not renamed, gone, with the old directory
|
|
10
|
+
* still on disk where nobody thinks to look. That is data loss wearing a
|
|
11
|
+
* rename's clothing.
|
|
12
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
*
|
|
14
|
+
* The dividing line (spec §3.4): what the user cannot see, the code migrates
|
|
15
|
+
* for them; what the user can see — the project-level `.argon/skills` directory
|
|
16
|
+
* inside their own repository — the CHANGELOG tells them to `git mv`.
|
|
17
|
+
*
|
|
18
|
+
* THIS FUNCTION NEVER THROWS. It runs as the first statement of `main()`, and a
|
|
19
|
+
* migration failure must never be the reason the CLI will not start: a user who
|
|
20
|
+
* has to re-enter an API key is inconvenienced, a user whose CLI refuses to
|
|
21
|
+
* launch is blocked. Every failure path degrades to `{ mode: 'none', error }`.
|
|
22
|
+
*
|
|
23
|
+
* IDEMPOTENCE KEY is "does the new directory already exist" — not the
|
|
24
|
+
* breadcrumb file, which is written for support diagnosis only. Two cold starts
|
|
25
|
+
* racing each other therefore both observe `existsSync(next) === false` and
|
|
26
|
+
* both attempt the rename; the loser gets ENOENT (source already moved) or
|
|
27
|
+
* EPERM (Windows, target now busy), which the outer catch absorbs. That is the
|
|
28
|
+
* designed outcome, not an accident: the winner completed the whole migration,
|
|
29
|
+
* and the loser's view of the world is identical to the already-migrated
|
|
30
|
+
* branch. No lock file is introduced — a stale lock is precisely the class of
|
|
31
|
+
* cross-version residue step 5 exists to delete.
|
|
32
|
+
*/
|
|
33
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
34
|
+
import { dirname, join } from 'node:path';
|
|
35
|
+
import envPaths from 'env-paths';
|
|
36
|
+
import { appPaths } from './app-paths.js';
|
|
37
|
+
/**
|
|
38
|
+
* The pre-rename `env-paths` application name. Deliberately declared here and
|
|
39
|
+
* not alongside `APP_NAME`: this module is the only consumer, and it is the
|
|
40
|
+
* only file in the package that is *supposed* to spell the old brand.
|
|
41
|
+
*/
|
|
42
|
+
export const LEGACY_APP_NAME = 'argon-agent';
|
|
43
|
+
export const legacyAppPaths = envPaths(LEGACY_APP_NAME);
|
|
44
|
+
export const nodeFsPort = {
|
|
45
|
+
existsSync,
|
|
46
|
+
mkdirSync: (path, options) => {
|
|
47
|
+
mkdirSync(path, options);
|
|
48
|
+
},
|
|
49
|
+
renameSync,
|
|
50
|
+
cpSync,
|
|
51
|
+
readdirSync: (path) => readdirSync(path),
|
|
52
|
+
rmSync,
|
|
53
|
+
writeFileSync: (path, data) => writeFileSync(path, data, 'utf-8'),
|
|
54
|
+
};
|
|
55
|
+
const LEGACY_MANIFEST_FILENAME = '.argon-skill.json';
|
|
56
|
+
const MANIFEST_FILENAME = '.aragon-skill.json';
|
|
57
|
+
const LEGACY_LOCK_FILENAME = '.argon-skills.lock';
|
|
58
|
+
const BREADCRUMB_FILENAME = '.migrated-from-argon-agent';
|
|
59
|
+
/**
|
|
60
|
+
* The roots the CLI actually uses. `envPaths` also exposes `cache`, `log` and
|
|
61
|
+
* `temp`; none of them hold anything the user would miss, and migrating a
|
|
62
|
+
* directory nobody reads is a way to move a bug rather than fix one. A future
|
|
63
|
+
* root must be added here explicitly.
|
|
64
|
+
*/
|
|
65
|
+
const ROOTS = ['config', 'data'];
|
|
66
|
+
export function migrateLegacyState(fs = nodeFsPort) {
|
|
67
|
+
const result = { mode: 'none', movedRoots: [], manifestsRenamed: 0 };
|
|
68
|
+
try {
|
|
69
|
+
let copied = false;
|
|
70
|
+
const from = {};
|
|
71
|
+
for (const root of ROOTS) {
|
|
72
|
+
const legacy = legacyAppPaths[root];
|
|
73
|
+
const next = appPaths[root];
|
|
74
|
+
// Already migrated, or the user built the new directory by hand. Either
|
|
75
|
+
// way: do not migrate, and above all do not overwrite.
|
|
76
|
+
if (fs.existsSync(next))
|
|
77
|
+
continue;
|
|
78
|
+
if (!fs.existsSync(legacy))
|
|
79
|
+
continue;
|
|
80
|
+
fs.mkdirSync(dirname(next), { recursive: true });
|
|
81
|
+
try {
|
|
82
|
+
fs.renameSync(legacy, next);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// EXDEV (separate volumes) or EPERM. Copy instead, and KEEP the legacy
|
|
86
|
+
// tree: a half-finished copy plus a deleted original is the one outcome
|
|
87
|
+
// worse than not migrating at all.
|
|
88
|
+
fs.cpSync(legacy, next, { recursive: true });
|
|
89
|
+
copied = true;
|
|
90
|
+
}
|
|
91
|
+
result.movedRoots.push(root);
|
|
92
|
+
from[root] = legacy;
|
|
93
|
+
}
|
|
94
|
+
if (result.movedRoots.length === 0)
|
|
95
|
+
return result;
|
|
96
|
+
result.mode = copied ? 'copied' : 'renamed';
|
|
97
|
+
if (result.movedRoots.includes('data')) {
|
|
98
|
+
result.manifestsRenamed = renameSkillManifests(fs, join(appPaths.data, 'skills'));
|
|
99
|
+
}
|
|
100
|
+
writeBreadcrumb(fs, result, from);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return {
|
|
105
|
+
mode: 'none',
|
|
106
|
+
movedRoots: [],
|
|
107
|
+
manifestsRenamed: 0,
|
|
108
|
+
error: err instanceof Error ? err.message : String(err),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Rename each installed skill's manifest and drop the stale install lock.
|
|
114
|
+
*
|
|
115
|
+
* Without the rename an installed skill keeps a manifest the new version cannot
|
|
116
|
+
* see, so `skills update` and `skills uninstall` stop recognising it and the
|
|
117
|
+
* skill becomes an orphan directory that still loads. The lock is transient
|
|
118
|
+
* state; carrying one across versions only lets the new build conclude that
|
|
119
|
+
* some other process is mid-install.
|
|
120
|
+
*/
|
|
121
|
+
function renameSkillManifests(fs, skillsDir) {
|
|
122
|
+
if (!fs.existsSync(skillsDir))
|
|
123
|
+
return 0;
|
|
124
|
+
let renamed = 0;
|
|
125
|
+
for (const entry of fs.readdirSync(skillsDir)) {
|
|
126
|
+
const legacyManifest = join(skillsDir, entry, LEGACY_MANIFEST_FILENAME);
|
|
127
|
+
if (!fs.existsSync(legacyManifest))
|
|
128
|
+
continue;
|
|
129
|
+
fs.renameSync(legacyManifest, join(skillsDir, entry, MANIFEST_FILENAME));
|
|
130
|
+
renamed += 1;
|
|
131
|
+
}
|
|
132
|
+
const staleLock = join(skillsDir, LEGACY_LOCK_FILENAME);
|
|
133
|
+
if (fs.existsSync(staleLock))
|
|
134
|
+
fs.rmSync(staleLock, { force: true });
|
|
135
|
+
return renamed;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Support-diagnosis breadcrumb. Deliberately NOT the idempotence key.
|
|
139
|
+
*
|
|
140
|
+
* Swallows its own failures, and that is the whole point of it being a separate
|
|
141
|
+
* function. It writes into the CONFIG root while the migration that just
|
|
142
|
+
* succeeded may only have moved the DATA root — on Windows those are two
|
|
143
|
+
* different trees (`%APPDATA%` vs `%LOCALAPPDATA%`), so the config directory can
|
|
144
|
+
* legitimately not exist yet. Letting an ENOENT here reach the caller's outer
|
|
145
|
+
* catch would report `mode: 'none'` for a migration that in fact moved the
|
|
146
|
+
* user's data, suppressing the stderr notice and telling support the exact
|
|
147
|
+
* opposite of what happened. A missing breadcrumb is a lost diagnostic; a
|
|
148
|
+
* mislabelled result is a lost afternoon.
|
|
149
|
+
*/
|
|
150
|
+
function writeBreadcrumb(fs, result, from) {
|
|
151
|
+
const payload = {
|
|
152
|
+
migratedAt: new Date().toISOString(),
|
|
153
|
+
mode: result.mode,
|
|
154
|
+
from,
|
|
155
|
+
manifestsRenamed: result.manifestsRenamed,
|
|
156
|
+
};
|
|
157
|
+
try {
|
|
158
|
+
fs.mkdirSync(appPaths.config, { recursive: true });
|
|
159
|
+
fs.writeFileSync(join(appPaths.config, BREADCRUMB_FILENAME), `${JSON.stringify(payload, null, 2)}\n`);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Intentionally ignored — see the note above.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The one-line notice for a migration that did something.
|
|
167
|
+
*
|
|
168
|
+
* Callers MUST write this to stderr. `screen.ts` holds stdout to the invariant
|
|
169
|
+
* that `aragon -p "..." > out.txt` yields model output and nothing else, and
|
|
170
|
+
* one stray line here would break every scripted invocation.
|
|
171
|
+
*
|
|
172
|
+
* ASCII-only, and `->` rather than an arrow glyph: this prints before any
|
|
173
|
+
* terminal capability probe runs, so a legacy console would render a nicer
|
|
174
|
+
* character as mojibake. Same rule `glyphs.test.ts` enforces across the CLI.
|
|
175
|
+
*/
|
|
176
|
+
export function formatMigrationNotice(result) {
|
|
177
|
+
if (result.mode === 'none')
|
|
178
|
+
return null;
|
|
179
|
+
const skills = result.manifestsRenamed > 0 ? ` (${result.manifestsRenamed} skills)` : '';
|
|
180
|
+
return `migrated settings from argon-agent -> aragon-agent${skills}`;
|
|
181
|
+
}
|