@aragon-agent/cli 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +558 -0
  3. package/dist/agent/coalesce.d.ts +14 -0
  4. package/dist/agent/coalesce.js +41 -0
  5. package/dist/agent/controller.d.ts +132 -0
  6. package/dist/agent/controller.js +332 -0
  7. package/dist/agent/headless.d.ts +22 -0
  8. package/dist/agent/headless.js +74 -0
  9. package/dist/agent/reducer.d.ts +181 -0
  10. package/dist/agent/reducer.js +420 -0
  11. package/dist/agent/system-prompt.d.ts +30 -0
  12. package/dist/agent/system-prompt.js +58 -0
  13. package/dist/agent/usage.d.ts +13 -0
  14. package/dist/agent/usage.js +41 -0
  15. package/dist/cli.d.ts +11 -0
  16. package/dist/cli.js +482 -0
  17. package/dist/commands/builtins.d.ts +16 -0
  18. package/dist/commands/builtins.js +223 -0
  19. package/dist/commands/registry.d.ts +48 -0
  20. package/dist/commands/registry.js +45 -0
  21. package/dist/commands/skills.d.ts +22 -0
  22. package/dist/commands/skills.js +625 -0
  23. package/dist/config/app-paths.d.ts +11 -0
  24. package/dist/config/app-paths.js +16 -0
  25. package/dist/config/env.d.ts +28 -0
  26. package/dist/config/env.js +92 -0
  27. package/dist/config/load.d.ts +48 -0
  28. package/dist/config/load.js +158 -0
  29. package/dist/config/migrate-legacy-state.d.ts +78 -0
  30. package/dist/config/migrate-legacy-state.js +181 -0
  31. package/dist/config/schema.d.ts +249 -0
  32. package/dist/config/schema.js +247 -0
  33. package/dist/config/store.d.ts +44 -0
  34. package/dist/config/store.js +126 -0
  35. package/dist/input/keymap.d.ts +20 -0
  36. package/dist/input/keymap.js +85 -0
  37. package/dist/session/persist.d.ts +24 -0
  38. package/dist/session/persist.js +51 -0
  39. package/dist/skills/archive.d.ts +41 -0
  40. package/dist/skills/archive.js +152 -0
  41. package/dist/skills/cli-commands.d.ts +52 -0
  42. package/dist/skills/cli-commands.js +502 -0
  43. package/dist/skills/fetch-source.d.ts +104 -0
  44. package/dist/skills/fetch-source.js +448 -0
  45. package/dist/skills/installer.d.ts +117 -0
  46. package/dist/skills/installer.js +414 -0
  47. package/dist/skills/lock.d.ts +75 -0
  48. package/dist/skills/lock.js +223 -0
  49. package/dist/skills/manifest.d.ts +47 -0
  50. package/dist/skills/manifest.js +95 -0
  51. package/dist/skills/node-host.d.ts +23 -0
  52. package/dist/skills/node-host.js +59 -0
  53. package/dist/skills/paths.d.ts +61 -0
  54. package/dist/skills/paths.js +107 -0
  55. package/dist/skills/service.d.ts +256 -0
  56. package/dist/skills/service.js +730 -0
  57. package/dist/skills/tools.d.ts +35 -0
  58. package/dist/skills/tools.js +164 -0
  59. package/dist/skills/updater.d.ts +101 -0
  60. package/dist/skills/updater.js +330 -0
  61. package/dist/skills/usage.d.ts +72 -0
  62. package/dist/skills/usage.js +216 -0
  63. package/dist/tools/bash-tool.d.ts +12 -0
  64. package/dist/tools/bash-tool.js +131 -0
  65. package/dist/tools/diff.d.ts +15 -0
  66. package/dist/tools/diff.js +50 -0
  67. package/dist/tools/fs-tools.d.ts +16 -0
  68. package/dist/tools/fs-tools.js +215 -0
  69. package/dist/tools/index.d.ts +93 -0
  70. package/dist/tools/index.js +157 -0
  71. package/dist/tools/search-tools.d.ts +11 -0
  72. package/dist/tools/search-tools.js +225 -0
  73. package/dist/ui/App.d.ts +24 -0
  74. package/dist/ui/App.js +522 -0
  75. package/dist/ui/AutocompletePopup.d.ts +21 -0
  76. package/dist/ui/AutocompletePopup.js +19 -0
  77. package/dist/ui/Composer.d.ts +40 -0
  78. package/dist/ui/Composer.js +61 -0
  79. package/dist/ui/Header.d.ts +30 -0
  80. package/dist/ui/Header.js +25 -0
  81. package/dist/ui/Logo.d.ts +50 -0
  82. package/dist/ui/Logo.js +85 -0
  83. package/dist/ui/Markdown.d.ts +23 -0
  84. package/dist/ui/Markdown.js +122 -0
  85. package/dist/ui/PromptInput.d.ts +81 -0
  86. package/dist/ui/PromptInput.js +383 -0
  87. package/dist/ui/SessionOpener.d.ts +30 -0
  88. package/dist/ui/SessionOpener.js +12 -0
  89. package/dist/ui/StatusBar.d.ts +39 -0
  90. package/dist/ui/StatusBar.js +43 -0
  91. package/dist/ui/ToastStack.d.ts +24 -0
  92. package/dist/ui/ToastStack.js +38 -0
  93. package/dist/ui/Transcript.d.ts +62 -0
  94. package/dist/ui/Transcript.js +128 -0
  95. package/dist/ui/capabilities.d.ts +25 -0
  96. package/dist/ui/capabilities.js +59 -0
  97. package/dist/ui/console-bridge.d.ts +19 -0
  98. package/dist/ui/console-bridge.js +52 -0
  99. package/dist/ui/density.d.ts +26 -0
  100. package/dist/ui/density.js +34 -0
  101. package/dist/ui/entries/AssistantEntry.d.ts +23 -0
  102. package/dist/ui/entries/AssistantEntry.js +13 -0
  103. package/dist/ui/entries/EntryFrame.d.ts +23 -0
  104. package/dist/ui/entries/EntryFrame.js +6 -0
  105. package/dist/ui/entries/ToolCard.d.ts +38 -0
  106. package/dist/ui/entries/ToolCard.js +72 -0
  107. package/dist/ui/entries/ToolPreview.d.ts +57 -0
  108. package/dist/ui/entries/ToolPreview.js +144 -0
  109. package/dist/ui/entries/UserEntry.d.ts +14 -0
  110. package/dist/ui/entries/UserEntry.js +5 -0
  111. package/dist/ui/exit-snapshot.d.ts +27 -0
  112. package/dist/ui/exit-snapshot.js +26 -0
  113. package/dist/ui/gauge.d.ts +24 -0
  114. package/dist/ui/gauge.js +36 -0
  115. package/dist/ui/glyphs.d.ts +87 -0
  116. package/dist/ui/glyphs.js +127 -0
  117. package/dist/ui/gradient.d.ts +11 -0
  118. package/dist/ui/gradient.js +50 -0
  119. package/dist/ui/layout/AppShell.d.ts +24 -0
  120. package/dist/ui/layout/AppShell.js +9 -0
  121. package/dist/ui/layout/Gutter.d.ts +67 -0
  122. package/dist/ui/layout/Gutter.js +42 -0
  123. package/dist/ui/layout/OverlayFrame.d.ts +62 -0
  124. package/dist/ui/layout/OverlayFrame.js +92 -0
  125. package/dist/ui/layout/ScrollViewport.d.ts +33 -0
  126. package/dist/ui/layout/ScrollViewport.js +71 -0
  127. package/dist/ui/layout/budget.d.ts +42 -0
  128. package/dist/ui/layout/budget.js +47 -0
  129. package/dist/ui/layout/frame.d.ts +58 -0
  130. package/dist/ui/layout/frame.js +75 -0
  131. package/dist/ui/layout/overlay-window.d.ts +43 -0
  132. package/dist/ui/layout/overlay-window.js +51 -0
  133. package/dist/ui/layout/scroll.d.ts +28 -0
  134. package/dist/ui/layout/scroll.js +76 -0
  135. package/dist/ui/layout/useTerminalSize.d.ts +13 -0
  136. package/dist/ui/layout/useTerminalSize.js +50 -0
  137. package/dist/ui/markdown-blocks.d.ts +62 -0
  138. package/dist/ui/markdown-blocks.js +120 -0
  139. package/dist/ui/overlays/ConfirmDialog.d.ts +30 -0
  140. package/dist/ui/overlays/ConfirmDialog.js +25 -0
  141. package/dist/ui/overlays/HelpOverlay.d.ts +17 -0
  142. package/dist/ui/overlays/HelpOverlay.js +60 -0
  143. package/dist/ui/overlays/ModelPicker.d.ts +29 -0
  144. package/dist/ui/overlays/ModelPicker.js +29 -0
  145. package/dist/ui/overlays/SettingsScreen.d.ts +37 -0
  146. package/dist/ui/overlays/SettingsScreen.js +106 -0
  147. package/dist/ui/palettes.d.ts +79 -0
  148. package/dist/ui/palettes.js +115 -0
  149. package/dist/ui/screen.d.ts +27 -0
  150. package/dist/ui/screen.js +57 -0
  151. package/dist/ui/theme.d.ts +78 -0
  152. package/dist/ui/theme.js +127 -0
  153. package/dist/ui/transcript-text.d.ts +27 -0
  154. package/dist/ui/transcript-text.js +110 -0
  155. package/package.json +73 -0
  156. package/skills/skill-creator/SKILL.md +96 -0
@@ -0,0 +1,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
+ }