@aragon-agent/cli 0.5.2 → 0.5.4

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 (87) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +193 -3
  3. package/dist/agent/controller.d.ts +51 -1
  4. package/dist/agent/controller.js +177 -2
  5. package/dist/agent/headless.d.ts +12 -0
  6. package/dist/agent/headless.js +41 -1
  7. package/dist/agent/reducer.d.ts +48 -0
  8. package/dist/agent/reducer.js +91 -2
  9. package/dist/agent/system-prompt.d.ts +17 -0
  10. package/dist/agent/system-prompt.js +4 -0
  11. package/dist/cli.js +98 -7
  12. package/dist/commands/builtins.d.ts +2 -2
  13. package/dist/commands/builtins.js +133 -3
  14. package/dist/config/app-paths.d.ts +17 -0
  15. package/dist/config/app-paths.js +21 -0
  16. package/dist/config/cli-commands.d.ts +26 -2
  17. package/dist/config/cli-commands.js +83 -6
  18. package/dist/config/env.d.ts +3 -2
  19. package/dist/config/env.js +44 -6
  20. package/dist/config/history-commands.d.ts +20 -0
  21. package/dist/config/history-commands.js +71 -0
  22. package/dist/config/load.d.ts +13 -0
  23. package/dist/config/load.js +107 -6
  24. package/dist/config/migrate-state-out-of-config.d.ts +32 -0
  25. package/dist/config/migrate-state-out-of-config.js +85 -0
  26. package/dist/config/prompt-history.d.ts +86 -0
  27. package/dist/config/prompt-history.js +332 -0
  28. package/dist/config/schema.d.ts +176 -18
  29. package/dist/config/schema.js +162 -8
  30. package/dist/config/store.js +40 -4
  31. package/dist/config/ui-state.d.ts +52 -0
  32. package/dist/config/ui-state.js +149 -0
  33. package/dist/logging/install.d.ts +85 -0
  34. package/dist/logging/install.js +94 -0
  35. package/dist/logging/logger.d.ts +1 -1
  36. package/dist/session/persist.d.ts +16 -0
  37. package/dist/session/persist.js +19 -1
  38. package/dist/team/activity.d.ts +67 -0
  39. package/dist/team/activity.js +130 -0
  40. package/dist/team/bus.d.ts +190 -0
  41. package/dist/team/bus.js +366 -0
  42. package/dist/team/comm-tools.d.ts +61 -0
  43. package/dist/team/comm-tools.js +221 -0
  44. package/dist/team/human-queue.d.ts +55 -0
  45. package/dist/team/human-queue.js +72 -0
  46. package/dist/team/limits.d.ts +123 -0
  47. package/dist/team/limits.js +128 -0
  48. package/dist/team/normalize.d.ts +39 -0
  49. package/dist/team/normalize.js +106 -0
  50. package/dist/team/panel-rows.d.ts +48 -0
  51. package/dist/team/panel-rows.js +75 -0
  52. package/dist/team/prompt.d.ts +49 -0
  53. package/dist/team/prompt.js +87 -0
  54. package/dist/team/report.d.ts +58 -0
  55. package/dist/team/report.js +205 -0
  56. package/dist/team/retry.d.ts +48 -0
  57. package/dist/team/retry.js +56 -0
  58. package/dist/team/runtime.d.ts +122 -0
  59. package/dist/team/runtime.js +406 -0
  60. package/dist/team/subagent.d.ts +90 -0
  61. package/dist/team/subagent.js +294 -0
  62. package/dist/team/task-tool.d.ts +47 -0
  63. package/dist/team/task-tool.js +118 -0
  64. package/dist/team/types.d.ts +172 -0
  65. package/dist/team/types.js +13 -0
  66. package/dist/tools/index.d.ts +48 -2
  67. package/dist/tools/index.js +26 -1
  68. package/dist/ui/App.d.ts +22 -1
  69. package/dist/ui/App.js +144 -18
  70. package/dist/ui/StatusBar.d.ts +21 -0
  71. package/dist/ui/StatusBar.js +5 -2
  72. package/dist/ui/TeamPanel.d.ts +57 -0
  73. package/dist/ui/TeamPanel.js +134 -0
  74. package/dist/ui/Transcript.js +10 -0
  75. package/dist/ui/entries/TeamCard.d.ts +31 -0
  76. package/dist/ui/entries/TeamCard.js +39 -0
  77. package/dist/ui/glyphs.d.ts +10 -0
  78. package/dist/ui/glyphs.js +6 -0
  79. package/dist/ui/layout/AppShell.d.ts +11 -1
  80. package/dist/ui/layout/AppShell.js +3 -3
  81. package/dist/ui/overlays/HelpOverlay.js +13 -0
  82. package/dist/ui/overlays/SettingsScreen.d.ts +12 -1
  83. package/dist/ui/overlays/SettingsScreen.js +38 -1
  84. package/dist/ui/transcript-text.js +20 -0
  85. package/dist/ui/use-startup-notices.d.ts +1 -1
  86. package/dist/ui/use-startup-notices.js +2 -1
  87. package/package.json +2 -2
@@ -5,8 +5,9 @@
5
5
  * - Provider secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY /
6
6
  * GEMINI_API_KEY.
7
7
  * - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
8
- * ARAGON_MAX_TOKENS, ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN,
9
- * ARAGON_LOG_LEVEL, ARAGON_LOG_FILE, ARAGON_LOG_DIR.
8
+ * ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
9
+ * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
10
+ * ARAGON_TEAM_MAX, ARAGON_LOG_LEVEL, ARAGON_LOG_FILE, ARAGON_LOG_DIR.
10
11
  *
11
12
  * NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
12
13
  * evaluated, which is long before `loadDotenv()` runs, so a `.env` can never
@@ -18,7 +19,7 @@ import process from 'node:process';
18
19
  import dotenv from 'dotenv';
19
20
  import { registerSecretsFrom } from '../logging/secret-registry.js';
20
21
  import { clampLogLevel } from '../logging/levels.js';
21
- import { clampTheme, clampThinkingLevel, coerceMaxTokens, } from './schema.js';
22
+ import { clampMaxTokens, clampTheme, clampThinkingLevel, isAutoToken, } from './schema.js';
22
23
  /**
23
24
  * Load a project `.env` from the given directory into `process.env` (existing
24
25
  * process env wins — dotenv never overrides already-set variables).
@@ -58,9 +59,20 @@ export function readEnvConfig() {
58
59
  if (process.env.ARAGON_THINKING) {
59
60
  partial.thinkingLevel = clampThinkingLevel(process.env.ARAGON_THINKING.trim(), 'off');
60
61
  }
61
- if (process.env.ARAGON_MAX_TOKENS) {
62
- const mt = coerceMaxTokens(process.env.ARAGON_MAX_TOKENS);
63
- partial.maxTokens = mt ?? null;
62
+ // `auto` / `0` mean AUTO (`null`); a number is clamped to the accepted range.
63
+ // Anything else is LEFT ABSENT rather than written as `null`: a typo here used
64
+ // to become AUTO and silently discard the number in the config file under it,
65
+ // which is the opposite of what the user asked for.
66
+ const maxTokens = process.env.ARAGON_MAX_TOKENS?.trim();
67
+ if (maxTokens !== undefined && maxTokens.length > 0) {
68
+ if (isAutoToken(maxTokens)) {
69
+ partial.maxTokens = null;
70
+ }
71
+ else {
72
+ const clamped = clampMaxTokens(maxTokens);
73
+ if (clamped !== undefined)
74
+ partial.maxTokens = clamped;
75
+ }
64
76
  }
65
77
  if (process.env.ARAGON_THEME) {
66
78
  partial.theme = clampTheme(process.env.ARAGON_THEME.trim(), 'auto');
@@ -96,6 +108,32 @@ export function readEnvConfig() {
96
108
  enabled: !(skills === '0' || skills === 'false' || skills === 'off' || skills === 'no'),
97
109
  };
98
110
  }
111
+ // ARAGON_TEAM=0 turns team mode off; ARAGON_TEAM_MAX=<n> overrides the fan-out
112
+ // width. Parsed by the POSITIVE list, exactly as ARAGON_FULLSCREEN /
113
+ // ARAGON_MOUSE / ARAGON_PLAN are — NOT by `envBool` below, which uses a
114
+ // negative list. The two readers disagree on `ARAGON_TEAM=disable`, and the
115
+ // documented-but-dead outcome of copying the wrong one is what `load.ts`
116
+ // records for `ARAGON_MOUSE`.
117
+ const team = process.env.ARAGON_TEAM?.trim().toLowerCase();
118
+ const teamMax = process.env.ARAGON_TEAM_MAX?.trim();
119
+ if ((team !== undefined && team.length > 0) || (teamMax !== undefined && teamMax.length > 0)) {
120
+ const section = {};
121
+ if (team !== undefined && team.length > 0) {
122
+ section.enabled = team === '1' || team === 'true' || team === 'on' || team === 'yes';
123
+ }
124
+ if (teamMax !== undefined && teamMax.length > 0) {
125
+ const parsed = Number.parseInt(teamMax, 10);
126
+ // Left ABSENT when unparseable rather than written as a default, so the
127
+ // config file can still win. A resolved default here would make the env
128
+ // var look permanently supplied.
129
+ if (Number.isFinite(parsed) && parsed > 0)
130
+ section.maxSubagents = parsed;
131
+ }
132
+ // A PARTIAL section, safe only because every consumer deep-merges it — the
133
+ // same contract `partial.skills` and `partial.log` rely on.
134
+ if (Object.keys(section).length > 0)
135
+ partial.team = section;
136
+ }
99
137
  const log = readEnvLogConfig();
100
138
  if (log)
101
139
  partial.log = log;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `aragon history path | list | clear` — the prompt history's operations
3
+ * surface (config-state-separation §5.5).
4
+ *
5
+ * Exit codes follow the existing convention: 0 success · 1 run-time failure ·
6
+ * 2 usage error.
7
+ *
8
+ * EXEMPT from the "every subcommand records one info line" rule, for the same
9
+ * reason `logging/cli-commands.ts` is: recording a line opens today's log file,
10
+ * and a command whose job is to delete the user's data should not be creating
11
+ * files as a side effect of announcing itself.
12
+ */
13
+ export interface HistoryCliOptions {
14
+ /** `-n <N>` — entries printed by `list` (default 20). */
15
+ lines?: string;
16
+ json?: boolean;
17
+ /** Required by `clear`; without it the command refuses. */
18
+ yes?: boolean;
19
+ }
20
+ export declare function runHistoryCommand(subcommand: string, opts: HistoryCliOptions): number;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `aragon history path | list | clear` — the prompt history's operations
3
+ * surface (config-state-separation §5.5).
4
+ *
5
+ * Exit codes follow the existing convention: 0 success · 1 run-time failure ·
6
+ * 2 usage error.
7
+ *
8
+ * EXEMPT from the "every subcommand records one info line" rule, for the same
9
+ * reason `logging/cli-commands.ts` is: recording a line opens today's log file,
10
+ * and a command whose job is to delete the user's data should not be creating
11
+ * files as a side effect of announcing itself.
12
+ */
13
+ import { existsSync } from 'node:fs';
14
+ import process from 'node:process';
15
+ import { getPromptHistoryPath } from './app-paths.js';
16
+ import { clearPromptHistory, listPromptHistory } from './prompt-history.js';
17
+ const EXIT_OK = 0;
18
+ const EXIT_USAGE = 2;
19
+ const DEFAULT_LIST_ENTRIES = 20;
20
+ export function runHistoryCommand(subcommand, opts) {
21
+ switch (subcommand) {
22
+ case 'path':
23
+ return runPath();
24
+ case 'list':
25
+ return runList(opts);
26
+ case 'clear':
27
+ return runClear(opts);
28
+ default:
29
+ process.stderr.write(`Unknown history subcommand "${subcommand}". Try: path | list | clear.\n`);
30
+ return EXIT_USAGE;
31
+ }
32
+ }
33
+ function runPath() {
34
+ const path = getPromptHistoryPath();
35
+ process.stdout.write(`${path}\n`);
36
+ if (!existsSync(path))
37
+ process.stdout.write('(no history recorded yet)\n');
38
+ // The file holds prompts verbatim — the one thing to say out loud before
39
+ // someone attaches it to a bug report or copies it to another machine.
40
+ process.stdout.write('entries are stored verbatim; clear them with: aragon history clear --yes\n');
41
+ return EXIT_OK;
42
+ }
43
+ function runList(opts) {
44
+ const entries = listPromptHistory();
45
+ if (entries.length === 0) {
46
+ process.stdout.write('No prompt history recorded.\n');
47
+ return EXIT_OK;
48
+ }
49
+ const parsed = Number.parseInt(opts.lines ?? '', 10);
50
+ const wanted = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_LIST_ENTRIES;
51
+ const shown = entries.slice(0, wanted);
52
+ if (opts.json) {
53
+ process.stdout.write(`${JSON.stringify(shown, null, 2)}\n`);
54
+ return EXIT_OK;
55
+ }
56
+ for (const entry of shown) {
57
+ // Newlines would break the one-entry-per-line reading this output invites.
58
+ const text = entry.text.replace(/\r?\n/g, ' ');
59
+ process.stdout.write(`${new Date(entry.ts).toISOString()} ${text}\n`);
60
+ }
61
+ return EXIT_OK;
62
+ }
63
+ function runClear(opts) {
64
+ if (!opts.yes) {
65
+ process.stderr.write('Refusing to delete the prompt history without --yes.\n');
66
+ return EXIT_USAGE;
67
+ }
68
+ const removed = clearPromptHistory();
69
+ process.stdout.write(`Removed ${removed} entries.\n`);
70
+ return EXIT_OK;
71
+ }
@@ -39,6 +39,19 @@ export interface CliFlags {
39
39
  skillToolPolicy?: string;
40
40
  /** `--plan` ⇒ true, `--no-plan` ⇒ false, absent ⇒ undefined (plan-mode §4.4). */
41
41
  plan?: boolean;
42
+ /**
43
+ * `--team` ⇒ true, `--no-team` ⇒ false, absent ⇒ undefined (team-subagents §4.4).
44
+ *
45
+ * BOTH commander forms must be declared and this field must be carried through
46
+ * `toFlags`, or the flag is inert with no compile error and no runtime error —
47
+ * the failure `cli.tsx` already records in its own words for `--no-mouse`
48
+ * (P1-4). Declaring only the negative form additionally makes commander
49
+ * default `opts.team` to `true`, at which point the flag is indistinguishable
50
+ * from its own default and silently overrides `config.json` on EVERY run.
51
+ */
52
+ team?: boolean;
53
+ /** `--team-max <n>` — override `team.maxSubagents` for this run (still clamped). */
54
+ teamMax?: string | number;
42
55
  /** `--log-level <lv>` — this run only, never persisted. */
43
56
  logLevel?: string;
44
57
  /** `--log-file` ⇒ true, `--no-log-file` ⇒ false, absent ⇒ undefined. */
@@ -6,9 +6,12 @@
6
6
  * legitimately long single tool run can never be watchdog-killed.
7
7
  */
8
8
  import process from 'node:process';
9
- import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampLogConfig, clampSkillsConfig, clampTheme, clampThinkingLevel, clampTranscriptWindow, coerceMaxTokens, coercePositiveInt, isSkillsToolPolicyMode, } from './schema.js';
9
+ import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampLogConfig, clampSkillsConfig, clampTeamConfig, clampTheme, clampThinkingLevel, clampMaxTokens, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, } from './schema.js';
10
10
  import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
11
11
  import { readConfigFile } from './store.js';
12
+ import { getConfigPath } from './app-paths.js';
13
+ import { setHistoryEnabled } from './prompt-history.js';
14
+ import { readSubmitCount } from './ui-state.js';
12
15
  import { getLogger } from '../logging/logger.js';
13
16
  import { registerSecret, registerSecretsFrom } from '../logging/secret-registry.js';
14
17
  import { detectCapabilities } from '../ui/capabilities.js';
@@ -73,6 +76,27 @@ function resolveLogToFile(flags, env, file) {
73
76
  return false;
74
77
  return true;
75
78
  }
79
+ /**
80
+ * Merge the `team` section across all four layers (defaults › file › env ›
81
+ * flags), team-subagents §3.10.
82
+ *
83
+ * `flags.team !== undefined` rather than a truthiness check, and that is the
84
+ * whole point (P1-4): a truthiness check cannot tell `--no-team` from "not
85
+ * passed", so `--no-team` would resolve to the config file's value and the off
86
+ * switch would not be off. `--no-skills` gets away with a one-directional test
87
+ * only because nothing ever needs to distinguish `--skills` from silence.
88
+ */
89
+ function resolveTeamConfig(flags, env, file) {
90
+ return clampTeamConfig({
91
+ ...DEFAULT_CONFIG.team,
92
+ ...(file.team ?? {}),
93
+ ...(env.team ?? {}),
94
+ ...(flags.team !== undefined ? { enabled: flags.team } : {}),
95
+ ...(flags.teamMax !== undefined && flags.teamMax !== ''
96
+ ? { maxSubagents: flags.teamMax }
97
+ : {}),
98
+ });
99
+ }
76
100
  /** Merge the `log` section across all four layers (defaults › file › env › flags). */
77
101
  function resolveLogConfig(flags, env, file) {
78
102
  const merged = clampLogConfig({
@@ -94,6 +118,44 @@ function pick(...vals) {
94
118
  }
95
119
  return undefined;
96
120
  }
121
+ function hasOwn(obj, key) {
122
+ return Object.prototype.hasOwnProperty.call(obj, key);
123
+ }
124
+ /**
125
+ * Resolve the output cap: flag › env › file › default, where an EXPLICIT `null`
126
+ * at any layer means AUTO (`undefined` — let the resolver pick per model).
127
+ *
128
+ * THIS CANNOT USE `pick()`, and that is the whole point. `pick` skips `null`, so
129
+ * a config file holding `"maxTokens": null` silently resolves to 64000 and AUTO
130
+ * is unreachable through every supported write path. Detecting it needs an
131
+ * own-property test, not a truthiness test — `null` is exactly the value we are
132
+ * looking for.
133
+ *
134
+ * An unparseable layer falls through to the next one rather than becoming AUTO:
135
+ * a typo in `ARAGON_MAX_TOKENS` must not silently discard the number in the
136
+ * config file underneath it.
137
+ */
138
+ function resolveMaxTokens(flags, env, file) {
139
+ const layers = [
140
+ { name: '--max-tokens', value: flags.maxTokens },
141
+ { name: 'ARAGON_MAX_TOKENS', value: hasOwn(env, 'maxTokens') ? env.maxTokens : undefined },
142
+ { name: 'config.json maxTokens', value: hasOwn(file, 'maxTokens') ? file.maxTokens : undefined },
143
+ ];
144
+ for (const layer of layers) {
145
+ if (layer.value === undefined)
146
+ continue; // this layer said nothing
147
+ if (layer.value === null || isAutoToken(layer.value))
148
+ return undefined; // AUTO
149
+ const n = clampMaxTokens(layer.value);
150
+ if (n !== undefined)
151
+ return n;
152
+ getLogger().warn('config', 'max_tokens_unusable', {
153
+ source: layer.name,
154
+ value: String(layer.value),
155
+ });
156
+ }
157
+ return DEFAULT_CONFIG.maxTokens ?? undefined;
158
+ }
97
159
  /**
98
160
  * Resolve the effective config by merging all layers, then derive the runtime
99
161
  * (non-persisted) fields and enforce the timeout invariant.
@@ -118,7 +180,7 @@ export function loadConfig(flags = {}) {
118
180
  const model = pick(flags.model, env.partial.model, file.model) ?? DEFAULT_CONFIG.model;
119
181
  const baseUrl = pick(flags.baseUrl, env.partial.baseUrl ?? undefined, file.baseUrl ?? undefined);
120
182
  const thinkingLevel = clampThinkingLevel(pick(flags.thinking, env.partial.thinkingLevel, file.thinkingLevel), DEFAULT_CONFIG.thinkingLevel);
121
- const maxTokens = coerceMaxTokens(pick(flags.maxTokens, env.partial.maxTokens, file.maxTokens));
183
+ const maxTokens = resolveMaxTokens(flags, env.partial, file);
122
184
  // `clampTheme` is also where the v0.3.0 `dark` name is migrated to `cool`,
123
185
  // so reading a legacy config file needs no extra step here (§4.5 / R-4).
124
186
  const theme = clampTheme(pick(flags.theme, env.partial.theme, file.theme), DEFAULT_CONFIG.theme);
@@ -178,7 +240,12 @@ export function loadConfig(flags = {}) {
178
240
  // Detect terminal capabilities once; force monochrome when color is disabled.
179
241
  const caps = detectCapabilities(process.env, process.stdout);
180
242
  const colorLevel = color ? caps.colorLevel : 0;
181
- return {
243
+ // No flag and no env var: a privacy switch you set once, not a per-run knob.
244
+ const historyEnabled = file.historyEnabled ?? DEFAULT_CONFIG.historyEnabled;
245
+ // The prompt-history store is a module-level singleton, so the resolved value
246
+ // has to be pushed into it — this is the ONLY place that happens.
247
+ setHistoryEnabled(historyEnabled);
248
+ const config = {
182
249
  provider,
183
250
  model,
184
251
  baseUrl,
@@ -195,12 +262,12 @@ export function loadConfig(flags = {}) {
195
262
  toolTimeoutMs,
196
263
  idleTimeoutMs,
197
264
  apiKeys,
198
- recentModels: file.recentModels ?? [],
199
- promptHistory: file.promptHistory ?? [],
265
+ historyEnabled,
200
266
  density,
201
267
  hints,
202
268
  mouse: resolveMouse(flags, env.partial, file),
203
- submitCount: coercePositiveInt(file.submitCount, 0),
269
+ // From `<home>/state.json`, not the config file — see `ui-state.ts`.
270
+ submitCount: readSubmitCount(),
204
271
  startInPlanMode,
205
272
  planModeMaxAskRounds,
206
273
  planModeHumanTimeoutMs,
@@ -217,12 +284,46 @@ export function loadConfig(flags = {}) {
217
284
  : {}),
218
285
  },
219
286
  log: resolveLogConfig(flags, env.partial, file),
287
+ team: resolveTeamConfig(flags, env.partial, file),
220
288
  cwd,
221
289
  color,
222
290
  colorLevel,
223
291
  unicode: caps.unicode,
224
292
  apiKeyOverride: flags.apiKey && flags.apiKey.trim().length > 0 ? flags.apiKey.trim() : undefined,
225
293
  };
294
+ recordResolvedConfig(config);
295
+ return config;
296
+ }
297
+ /**
298
+ * One record naming the values this run actually resolved to.
299
+ *
300
+ * "I changed the model in config.json and it still uses the old one" was, until
301
+ * this line existed, unanswerable after the fact: four layers feed this
302
+ * function and none of them left a trace. Now the answer is in the log next to
303
+ * the path the values came from.
304
+ *
305
+ * NO SECRET VALUES — only whether a key was found. `logging/redact.ts` would
306
+ * catch one anyway, but a record that never carries the secret does not depend
307
+ * on that backstop being configured.
308
+ */
309
+ function recordResolvedConfig(config) {
310
+ getLogger().info('config', 'config_loaded', {
311
+ provider: config.provider,
312
+ model: config.model,
313
+ baseUrl: config.baseUrl,
314
+ thinkingLevel: config.thinkingLevel,
315
+ // Four layers feed this one and none of them used to leave a trace, so
316
+ // "why is my output truncated" was unanswerable after the fact.
317
+ maxTokens: config.maxTokens ?? 'auto',
318
+ theme: config.theme,
319
+ fullscreen: config.fullscreen,
320
+ toolTimeoutMs: config.toolTimeoutMs,
321
+ idleTimeoutMs: config.idleTimeoutMs,
322
+ historyEnabled: config.historyEnabled,
323
+ logLevel: config.log.level,
324
+ hasApiKey: Boolean(config.apiKeys[config.provider]),
325
+ configPath: getConfigPath(),
326
+ });
226
327
  }
227
328
  /**
228
329
  * Build the `getApiKey(providerId)` resolver the core `Agent` consumes.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * One-shot move of the four runtime-state keys out of `config.json`
3
+ * (config-state-separation §4.6).
4
+ *
5
+ * ══════════════════════════════════════════════════════════════════════════
6
+ * WHY THIS EXISTS: the schema change alone would strip `promptHistory`,
7
+ * `submitCount` and `mouseNoticeSeen` from an existing user's config file on
8
+ * the first write after upgrading, with nowhere for the values to go. That is
9
+ * the only path in this feature that can actually LOSE data, which is why the
10
+ * strip and this migration must ship together.
11
+ * ══════════════════════════════════════════════════════════════════════════
12
+ *
13
+ * THE IDEMPOTENCE KEY IS THE SOURCE DATA — "are those keys still in
14
+ * config.json" — not a breadcrumb and not "does prompt-history.jsonl exist".
15
+ * Same reasoning as `migrate-home.ts`: a breadcrumb written before a failed
16
+ * step makes a half-finished migration skip itself forever. Keyed on the source,
17
+ * a failure simply retries next launch, and the two import functions are
18
+ * written so that retrying imports nothing twice.
19
+ *
20
+ * NEVER THROWS (C3) and never prints (D-8). The user sees no difference —
21
+ * history still recalls, the counter still counts — so a line on stderr would
22
+ * be noise, and `aragon config home` names the new files for anyone curious.
23
+ */
24
+ export interface StateMigrationResult {
25
+ /** The keys actually carried out of the file, e.g. `['promptHistory']`. */
26
+ moved: string[];
27
+ /** History entries imported, for the log record. */
28
+ promptEntries: number;
29
+ /** Returned rather than thrown (C3). */
30
+ error?: string;
31
+ }
32
+ export declare function migrateStateOutOfConfig(): StateMigrationResult;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * One-shot move of the four runtime-state keys out of `config.json`
3
+ * (config-state-separation §4.6).
4
+ *
5
+ * ══════════════════════════════════════════════════════════════════════════
6
+ * WHY THIS EXISTS: the schema change alone would strip `promptHistory`,
7
+ * `submitCount` and `mouseNoticeSeen` from an existing user's config file on
8
+ * the first write after upgrading, with nowhere for the values to go. That is
9
+ * the only path in this feature that can actually LOSE data, which is why the
10
+ * strip and this migration must ship together.
11
+ * ══════════════════════════════════════════════════════════════════════════
12
+ *
13
+ * THE IDEMPOTENCE KEY IS THE SOURCE DATA — "are those keys still in
14
+ * config.json" — not a breadcrumb and not "does prompt-history.jsonl exist".
15
+ * Same reasoning as `migrate-home.ts`: a breadcrumb written before a failed
16
+ * step makes a half-finished migration skip itself forever. Keyed on the source,
17
+ * a failure simply retries next launch, and the two import functions are
18
+ * written so that retrying imports nothing twice.
19
+ *
20
+ * NEVER THROWS (C3) and never prints (D-8). The user sees no difference —
21
+ * history still recalls, the counter still counts — so a line on stderr would
22
+ * be noise, and `aragon config home` names the new files for anyone curious.
23
+ */
24
+ import { readConfigFile, updatePersistedConfig } from './store.js';
25
+ import { importLegacyPromptHistory } from './prompt-history.js';
26
+ import { importLegacyUiState } from './ui-state.js';
27
+ import { LEGACY_STATE_KEYS } from './schema.js';
28
+ import { getLogger } from '../logging/logger.js';
29
+ export function migrateStateOutOfConfig() {
30
+ const result = { moved: [], promptEntries: 0 };
31
+ try {
32
+ const { config } = readConfigFile();
33
+ // A missing file has nothing to move; an UNPARSEABLE one must be left
34
+ // exactly as it is, or the user's chance to fix their typo is gone.
35
+ if (!config)
36
+ return result;
37
+ const legacy = config;
38
+ const present = LEGACY_STATE_KEYS.filter((key) => legacy[key] !== undefined);
39
+ if (present.length === 0)
40
+ return result;
41
+ result.promptEntries = importHistory(legacy);
42
+ importCounters(legacy);
43
+ // An empty patch still goes read-strip-merge-write, so this is what makes
44
+ // the legacy keys leave the file — `stripLegacyStateKeys` does the work.
45
+ updatePersistedConfig({});
46
+ result.moved = [...present];
47
+ getLogger().info('migrate', 'migrate_state_split', {
48
+ moved: result.moved,
49
+ promptEntries: result.promptEntries,
50
+ });
51
+ return result;
52
+ }
53
+ catch (err) {
54
+ return { moved: [], promptEntries: 0, error: err instanceof Error ? err.message : String(err) };
55
+ }
56
+ }
57
+ /**
58
+ * Import the recall list, unless the user turned recording off.
59
+ *
60
+ * THE `historyEnabled` CHECK IS NOT REDUNDANT with the one inside
61
+ * `appendPrompt`. This runs before any `loadConfig()`, so the module-level
62
+ * switch is still at its `true` default; the only place the user's real answer
63
+ * exists at this moment is the config object read above. The path that makes it
64
+ * matter: upgrade, switch recording off, downgrade to 0.5.x (which writes
65
+ * `promptHistory` back into config.json), upgrade again. Without this, prompts
66
+ * the user explicitly refused to have recorded get copied into the new file.
67
+ */
68
+ function importHistory(legacy) {
69
+ if (!Array.isArray(legacy.promptHistory) || legacy.promptHistory.length === 0)
70
+ return 0;
71
+ if (legacy.historyEnabled === false)
72
+ return 0;
73
+ const entries = legacy.promptHistory.filter((item) => typeof item === 'string');
74
+ return importLegacyPromptHistory(entries);
75
+ }
76
+ function importCounters(legacy) {
77
+ const patch = {};
78
+ if (typeof legacy.submitCount === 'number')
79
+ patch.submitCount = legacy.submitCount;
80
+ if (typeof legacy.mouseNoticeSeen === 'boolean')
81
+ patch.mouseNoticeSeen = legacy.mouseNoticeSeen;
82
+ if (Object.keys(patch).length === 0)
83
+ return;
84
+ importLegacyUiState(patch);
85
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The prompts the user submitted, as an append-only JSON Lines file at
3
+ * `<home>/prompt-history.jsonl` (config-state-separation §4.3).
4
+ *
5
+ * ══════════════════════════════════════════════════════════════════════════
6
+ * WHY THIS IS NOT IN config.json ANY MORE: it used to be a `promptHistory`
7
+ * array inside the config file, which meant every single submit rewrote the
8
+ * whole file — API key section included — and left a user who opened
9
+ * `config.json` to change their model scrolling past their own typing. A
10
+ * config file holds what the user DECLARED; this holds what they DID.
11
+ * ══════════════════════════════════════════════════════════════════════════
12
+ *
13
+ * APPEND, never read-modify-write. Two `aragon` windows are an ordinary setup,
14
+ * and `O_APPEND` + one `writeSync` per entry means they interleave instead of
15
+ * overwriting each other's snapshots. It also means a process killed mid-write
16
+ * costs one broken line rather than a truncated array that reads back as "your
17
+ * history is gone" — hence the per-line tolerance in `parseLine`.
18
+ *
19
+ * FAILURE POLICY (C2), the same one `skills/usage.ts` documents: every function
20
+ * here swallows its errors. This is recall convenience; a read-only home
21
+ * directory must degrade to "no history", never to a CLI that will not start.
22
+ * The one thing that policy makes dangerous is a MISSING WRITE — it would be
23
+ * invisible — which is why the write path creates `<home>` first and why a
24
+ * failed append is logged rather than merely ignored.
25
+ */
26
+ export declare const PROMPT_HISTORY_FILENAME = "prompt-history.jsonl";
27
+ export declare const PROMPT_HISTORY_LINE_VERSION = 1;
28
+ /**
29
+ * Longer submissions are not recorded at all (D-6).
30
+ *
31
+ * Storing a TRUNCATED copy would be worse than storing nothing: the user
32
+ * presses `↑`, gets back something that looks complete, and sends half a
33
+ * prompt. 16 KiB is far past anything typed by hand.
34
+ */
35
+ export declare const PROMPT_ENTRY_MAX_CHARS = 16384;
36
+ /** Physical lines tolerated before the file is rewritten down to the cap. */
37
+ export declare const HISTORY_COMPACT_AT_LINES = 400;
38
+ export interface PromptHistoryEntry {
39
+ /** Line schema version. Lines with any other value are skipped, not fatal. */
40
+ v: number;
41
+ /** `Date.now()` at submit time. */
42
+ ts: number;
43
+ /** The prompt verbatim — never redacted (D-5). */
44
+ text: string;
45
+ }
46
+ /**
47
+ * The recall list: oldest first, de-duplicated, capped. Cached for the life of
48
+ * the process so `/reload` — which calls `loadConfig()` again — cannot discard
49
+ * entries added during this session.
50
+ *
51
+ * Reads even when `historyEnabled` is false (RV-12): turning recording off
52
+ * stops new writes, but someone who wants to see what is already stored before
53
+ * deleting it must not be told "nothing here" while the file still exists.
54
+ */
55
+ export declare function loadPromptHistory(): string[];
56
+ /** Every entry on disk with its timestamp, newest first. For `history list`. */
57
+ export declare function listPromptHistory(): PromptHistoryEntry[];
58
+ /**
59
+ * Record one submitted prompt and return THE UPDATED LIST.
60
+ *
61
+ * The return value is the whole point of the signature (RV-1). `App.tsx` writes
62
+ * `setPromptHistory(appendPrompt(text))`: the recall list rendered by the
63
+ * composer is React state, and a module-level array updated in here re-renders
64
+ * nothing. Drop the return value and `↑` stops recalling what was just
65
+ * submitted — but only until the next restart, which makes it about as hard to
66
+ * diagnose as a bug in this area gets.
67
+ *
68
+ * Returns the current list unchanged when recording is off, the text is blank,
69
+ * or the text is oversized, so callers never need a branch.
70
+ */
71
+ export declare function appendPrompt(text: string, now?: number): string[];
72
+ /**
73
+ * Migration-only: append the texts that are not in the file yet, returning how
74
+ * many were written.
75
+ *
76
+ * Ignores `historyEnabled` on purpose — the caller decides, because it is the
77
+ * only code that can see an explicit `historyEnabled: false` in the config file
78
+ * before `loadConfig()` has run (§4.6 step 3).
79
+ */
80
+ export declare function importLegacyPromptHistory(entries: string[]): number;
81
+ /** Delete the file and empty the cache. Returns how many entries were removed. */
82
+ export declare function clearPromptHistory(): number;
83
+ /** Called once by `loadConfig()` after `historyEnabled` has been resolved. */
84
+ export declare function setHistoryEnabled(enabled: boolean): void;
85
+ /** Test-only: forget the cache, the line counters and the switch. */
86
+ export declare function resetPromptHistoryForTests(): void;