@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.
- package/CHANGELOG.md +127 -0
- package/README.md +193 -3
- package/dist/agent/controller.d.ts +51 -1
- package/dist/agent/controller.js +177 -2
- package/dist/agent/headless.d.ts +12 -0
- package/dist/agent/headless.js +41 -1
- package/dist/agent/reducer.d.ts +48 -0
- package/dist/agent/reducer.js +91 -2
- package/dist/agent/system-prompt.d.ts +17 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/cli.js +98 -7
- package/dist/commands/builtins.d.ts +2 -2
- package/dist/commands/builtins.js +133 -3
- package/dist/config/app-paths.d.ts +17 -0
- package/dist/config/app-paths.js +21 -0
- package/dist/config/cli-commands.d.ts +26 -2
- package/dist/config/cli-commands.js +83 -6
- package/dist/config/env.d.ts +3 -2
- package/dist/config/env.js +44 -6
- package/dist/config/history-commands.d.ts +20 -0
- package/dist/config/history-commands.js +71 -0
- package/dist/config/load.d.ts +13 -0
- package/dist/config/load.js +107 -6
- package/dist/config/migrate-state-out-of-config.d.ts +32 -0
- package/dist/config/migrate-state-out-of-config.js +85 -0
- package/dist/config/prompt-history.d.ts +86 -0
- package/dist/config/prompt-history.js +332 -0
- package/dist/config/schema.d.ts +176 -18
- package/dist/config/schema.js +162 -8
- package/dist/config/store.js +40 -4
- package/dist/config/ui-state.d.ts +52 -0
- package/dist/config/ui-state.js +149 -0
- package/dist/logging/install.d.ts +85 -0
- package/dist/logging/install.js +94 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.d.ts +16 -0
- package/dist/session/persist.js +19 -1
- package/dist/team/activity.d.ts +67 -0
- package/dist/team/activity.js +130 -0
- package/dist/team/bus.d.ts +190 -0
- package/dist/team/bus.js +366 -0
- package/dist/team/comm-tools.d.ts +61 -0
- package/dist/team/comm-tools.js +221 -0
- package/dist/team/human-queue.d.ts +55 -0
- package/dist/team/human-queue.js +72 -0
- package/dist/team/limits.d.ts +123 -0
- package/dist/team/limits.js +128 -0
- package/dist/team/normalize.d.ts +39 -0
- package/dist/team/normalize.js +106 -0
- package/dist/team/panel-rows.d.ts +48 -0
- package/dist/team/panel-rows.js +75 -0
- package/dist/team/prompt.d.ts +49 -0
- package/dist/team/prompt.js +87 -0
- package/dist/team/report.d.ts +58 -0
- package/dist/team/report.js +205 -0
- package/dist/team/retry.d.ts +48 -0
- package/dist/team/retry.js +56 -0
- package/dist/team/runtime.d.ts +122 -0
- package/dist/team/runtime.js +406 -0
- package/dist/team/subagent.d.ts +90 -0
- package/dist/team/subagent.js +294 -0
- package/dist/team/task-tool.d.ts +47 -0
- package/dist/team/task-tool.js +118 -0
- package/dist/team/types.d.ts +172 -0
- package/dist/team/types.js +13 -0
- package/dist/tools/index.d.ts +48 -2
- package/dist/tools/index.js +26 -1
- package/dist/ui/App.d.ts +22 -1
- package/dist/ui/App.js +144 -18
- package/dist/ui/StatusBar.d.ts +21 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/TeamPanel.d.ts +57 -0
- package/dist/ui/TeamPanel.js +134 -0
- package/dist/ui/Transcript.js +10 -0
- package/dist/ui/entries/TeamCard.d.ts +31 -0
- package/dist/ui/entries/TeamCard.js +39 -0
- package/dist/ui/glyphs.d.ts +10 -0
- package/dist/ui/glyphs.js +6 -0
- package/dist/ui/layout/AppShell.d.ts +11 -1
- package/dist/ui/layout/AppShell.js +3 -3
- package/dist/ui/overlays/HelpOverlay.js +13 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +12 -1
- package/dist/ui/overlays/SettingsScreen.js +38 -1
- package/dist/ui/transcript-text.js +20 -0
- package/dist/ui/use-startup-notices.d.ts +1 -1
- package/dist/ui/use-startup-notices.js +2 -1
- package/package.json +2 -2
package/dist/config/env.js
CHANGED
|
@@ -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
|
|
9
|
-
*
|
|
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,
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
}
|
package/dist/config/load.d.ts
CHANGED
|
@@ -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. */
|
package/dist/config/load.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
promptHistory: file.promptHistory ?? [],
|
|
265
|
+
historyEnabled,
|
|
200
266
|
density,
|
|
201
267
|
hints,
|
|
202
268
|
mouse: resolveMouse(flags, env.partial, file),
|
|
203
|
-
|
|
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;
|