aegiscode 6.5.3 → 6.5.5
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/package.json +2 -2
- package/scripts/predist.mjs +1 -0
- package/src/app.js +14 -0
- package/src/art.js +63 -14
- package/src/chatflow.js +113 -3
- package/src/commands.js +19 -0
- package/src/deps.js +7 -1
- package/src/render.js +11 -3
- package/src/theme.js +21 -7
- package/src/update.js +8 -132
- package/vendor/client/aegis.js +12 -1
- package/vendor/client/update.js +140 -0
- package/vendor/desktop/lib/local/engine.js +67 -6
- package/vendor/desktop/lib/local/tools.js +24 -39
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegiscode",
|
|
3
3
|
"productName": "AEGIS Code",
|
|
4
|
-
"version": "6.5.
|
|
5
|
-
"description": "aegiscode
|
|
4
|
+
"version": "6.5.5",
|
|
5
|
+
"description": "aegiscode — the command-line version of AEGIS Desktop. The shared tool surface in your shell, over the same thin transport and tool registry as the MCP plugin and the desktop app. Ships transport + UI only; no brain.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
|
8
8
|
"email": "nborneklint@gmail.com"
|
package/scripts/predist.mjs
CHANGED
|
@@ -37,6 +37,7 @@ const VENDOR = path.join(CLI_DIR, 'vendor');
|
|
|
37
37
|
/** repo-relative -> staged location (same relative shape, under vendor/) */
|
|
38
38
|
const FILES = [
|
|
39
39
|
'client/aegis.js',
|
|
40
|
+
'client/update.js',
|
|
40
41
|
'client/foreign-memory.js',
|
|
41
42
|
// The account credential store and the unified session store. Shared with the
|
|
42
43
|
// desktop app and the MCP plugin so all three see one key and one session
|
package/src/app.js
CHANGED
|
@@ -125,6 +125,15 @@ function createApp(options = {}) {
|
|
|
125
125
|
// pinning one would make every turn cost what that rung grants.
|
|
126
126
|
effort: null,
|
|
127
127
|
thinking: false,
|
|
128
|
+
// Deep recall (`aegis_recall_deep`) — the metered tier of the read: brain
|
|
129
|
+
// corrections plus the semantic answer cache, and the answer cache costs
|
|
130
|
+
// one provider embedding per turn (aegis1 app.py:8367 →
|
|
131
|
+
// services/brain_memory.py find_cached_answer). This host recalls on every
|
|
132
|
+
// turn already and pays per turn, so the deep tier is NEVER inferred: it is
|
|
133
|
+
// false here, `/memory-deep on` is the only thing that flips it, and that
|
|
134
|
+
// choice is deliberately NOT persisted — a flag that meters every turn must
|
|
135
|
+
// not survive into a session where nobody asked for it.
|
|
136
|
+
recallDeep: false,
|
|
128
137
|
themeIndex: opts.light ? 2 : 1,
|
|
129
138
|
vim: false,
|
|
130
139
|
stream: opts.stream,
|
|
@@ -497,6 +506,11 @@ function createApp(options = {}) {
|
|
|
497
506
|
// about what the turn cost. `null` (auto) is omitted so the server
|
|
498
507
|
// infers the rung from the ask.
|
|
499
508
|
effort: commandCtx.effort || undefined,
|
|
509
|
+
// The deep-recall opt-in travels only when this session turned it on
|
|
510
|
+
// (`/memory-deep on`). Sent as a literal `true` or not at all: the
|
|
511
|
+
// engine treats anything but `true` as off, and an omitted field is
|
|
512
|
+
// what keeps a stray value from ever buying a per-turn embedding.
|
|
513
|
+
recallDeep: commandCtx.recallDeep === true ? true : undefined,
|
|
500
514
|
// false asks the engine for the buffered (non-stream) wire form, so
|
|
501
515
|
// `--no-stream` and piped runs get a single body rather than SSE.
|
|
502
516
|
stream: commandCtx.stream !== false,
|
package/src/art.js
CHANGED
|
@@ -89,6 +89,15 @@ const TAGLINE = 'Cloud brain in your shell.';
|
|
|
89
89
|
// mangle █▓▒░ entirely; on macOS ✦ renders as a double-width emoji. Each pass
|
|
90
90
|
// swaps offending code points 1:1 (same character count per row) so the width
|
|
91
91
|
// maths stays exact and the mark keeps its shape.
|
|
92
|
+
//
|
|
93
|
+
// This table is the ONE stencil source of truth for the product. `theme.js`
|
|
94
|
+
// resolves `GLYPH.star` through `stencilGlyph(STAR, platform)` rather than
|
|
95
|
+
// carrying its own `'✦' → '*'` rule, and `render.js` builds the mark's tint map
|
|
96
|
+
// by stencilling its runes — both had a private copy of the same decision, which
|
|
97
|
+
// is how a stencil edit here could leave the star wide on macOS or the whale
|
|
98
|
+
// untinted on Windows. `test/cli-art-platform.test.mjs` pins all of it.
|
|
99
|
+
const STAR = '✦';
|
|
100
|
+
|
|
92
101
|
const ASCII_STENCIL = new Map([
|
|
93
102
|
['█', '#'],
|
|
94
103
|
['▓', '@'],
|
|
@@ -100,7 +109,7 @@ const ASCII_STENCIL = new Map([
|
|
|
100
109
|
['▌', '#'],
|
|
101
110
|
['▝', '#'],
|
|
102
111
|
['▘', '#'],
|
|
103
|
-
[
|
|
112
|
+
[STAR, '*'],
|
|
104
113
|
['·', '.'],
|
|
105
114
|
]);
|
|
106
115
|
|
|
@@ -111,18 +120,45 @@ const DARWIN_STENCIL = new Map([
|
|
|
111
120
|
['▌', '#'],
|
|
112
121
|
['▝', '#'],
|
|
113
122
|
['▘', '#'],
|
|
114
|
-
[
|
|
123
|
+
[STAR, '*'],
|
|
115
124
|
]);
|
|
116
125
|
|
|
117
|
-
/**
|
|
126
|
+
/** The platforms this mark has a pass for. Linux needs none (see `stencilFor`). */
|
|
127
|
+
const PLATFORMS = ['linux', 'darwin', 'win32'];
|
|
128
|
+
|
|
129
|
+
/** Every pass, keyed by `process.platform`; a platform with no entry is native. */
|
|
130
|
+
const STENCILS = { darwin: DARWIN_STENCIL, win32: ASCII_STENCIL };
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The stencil for a platform, or `null` when its runes need no pass.
|
|
134
|
+
* @param {string} platform a `process.platform` string
|
|
135
|
+
*/
|
|
136
|
+
function stencilFor(platform) {
|
|
137
|
+
return STENCILS[platform] || null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* One rune as `platform` renders it — the identity when that platform is native.
|
|
142
|
+
* Exported so `theme.js`'s glyph table and `render.js`'s tint map are derived
|
|
143
|
+
* from the stencil instead of restating it.
|
|
144
|
+
* @param {string} ch
|
|
145
|
+
* @param {string} [platform]
|
|
146
|
+
*/
|
|
147
|
+
function stencilGlyph(ch, platform = process.platform) {
|
|
148
|
+
const s = stencilFor(platform);
|
|
149
|
+
return (s && s.get(ch)) || ch;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** One platform pass over an art block, for an explicit platform. */
|
|
153
|
+
function portabilityFor(rows, platform) {
|
|
154
|
+
const s = stencilFor(platform);
|
|
155
|
+
if (!s) return rows;
|
|
156
|
+
return rows.map((r) => [...r].map((ch) => s.get(ch) ?? ch).join(''));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The mark as the terminal running this process will render it. */
|
|
118
160
|
function portability(rows) {
|
|
119
|
-
|
|
120
|
-
return rows.map((r) => [...r].map((ch) => ASCII_STENCIL.get(ch) ?? ch).join(''));
|
|
121
|
-
}
|
|
122
|
-
if (process.platform === 'darwin') {
|
|
123
|
-
return rows.map((r) => [...r].map((ch) => DARWIN_STENCIL.get(ch) ?? ch).join(''));
|
|
124
|
-
}
|
|
125
|
-
return rows;
|
|
161
|
+
return portabilityFor(rows, process.platform);
|
|
126
162
|
}
|
|
127
163
|
|
|
128
164
|
/** The mark that fits the current terminal: side-by-side when there's room. */
|
|
@@ -151,8 +187,9 @@ function welcomeArtParts(cols) {
|
|
|
151
187
|
const n = Math.max(mascot.length, moonWhale.length);
|
|
152
188
|
const rows = [];
|
|
153
189
|
for (let i = 0; i < n; i++) {
|
|
154
|
-
|
|
155
|
-
const
|
|
190
|
+
// Same bottom-align rule as joinSideBySide — subtract the offset.
|
|
191
|
+
const li = i - (n - mascot.length);
|
|
192
|
+
const ri = i - (n - moonWhale.length);
|
|
156
193
|
rows.push([(mascot[li] ?? '').padEnd(L), (moonWhale[ri] ?? '').padEnd(R)]);
|
|
157
194
|
}
|
|
158
195
|
return { rows, width: L + gutter + R, gutter };
|
|
@@ -188,8 +225,14 @@ function joinSideBySide(left, right, gutter = 2, align = 'bottom') {
|
|
|
188
225
|
const R = Math.max(...right.map((r) => [...r].length));
|
|
189
226
|
const n = Math.max(left.length, right.length);
|
|
190
227
|
return Array.from({ length: n }, (_, i) => {
|
|
191
|
-
|
|
192
|
-
|
|
228
|
+
// Bottom-align puts each block's LAST row on the output's last row, so a
|
|
229
|
+
// shorter block starts lower and its index runs *behind* the output index.
|
|
230
|
+
// Adding the offset instead of subtracting it pushed the shorter block off
|
|
231
|
+
// the bottom edge and silently dropped its top rows: the moon+whale half,
|
|
232
|
+
// two rows shorter than the mascot, lost its spout and its top star and
|
|
233
|
+
// hung two rows below the baseline.
|
|
234
|
+
const li = align === 'bottom' ? i - (n - left.length) : i;
|
|
235
|
+
const ri = align === 'bottom' ? i - (n - right.length) : i;
|
|
193
236
|
const l = (left[li] ?? '').padEnd(L);
|
|
194
237
|
const r = (right[ri] ?? '').padEnd(R);
|
|
195
238
|
return l + ' '.repeat(gutter) + r;
|
|
@@ -225,7 +268,13 @@ module.exports = {
|
|
|
225
268
|
WELCOME_TITLE,
|
|
226
269
|
WELCOME_BACK,
|
|
227
270
|
TAGLINE,
|
|
271
|
+
STAR,
|
|
272
|
+
PLATFORMS,
|
|
273
|
+
STENCILS,
|
|
274
|
+
stencilFor,
|
|
275
|
+
stencilGlyph,
|
|
228
276
|
portability,
|
|
277
|
+
portabilityFor,
|
|
229
278
|
welcomeArtFor,
|
|
230
279
|
welcomeArtParts,
|
|
231
280
|
stackVertical,
|
package/src/chatflow.js
CHANGED
|
@@ -131,6 +131,50 @@ function resolveToolDone(transcript, t) {
|
|
|
131
131
|
return m;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Fold one turn's tool events into the counts the end-of-turn summary prints.
|
|
136
|
+
*
|
|
137
|
+
* Deterministic and purely local, on purpose. The tail of every turn is the
|
|
138
|
+
* wrong place to spend a model call: it would bill the reader for a restatement
|
|
139
|
+
* of work they have already paid to have done, and it would arrive after a
|
|
140
|
+
* network round-trip instead of at the instant the turn ends. The data needed
|
|
141
|
+
* is already in the tool events — the engine hands us `ok` on every call and
|
|
142
|
+
* the args name every file and command — so the summary is arithmetic, not
|
|
143
|
+
* inference.
|
|
144
|
+
*
|
|
145
|
+
* @param {Array<{ok?:boolean,args?:object}>} tools This turn's tool events, in order.
|
|
146
|
+
* @returns {{tools:number,failed:number,files:string[],names:string[],more:number,commands:string[]}}
|
|
147
|
+
*/
|
|
148
|
+
function summarizeTurnTools(tools, { maxNames = 3 } = {}) {
|
|
149
|
+
const files = [];
|
|
150
|
+
const commands = [];
|
|
151
|
+
let failed = 0;
|
|
152
|
+
for (const tool of tools || []) {
|
|
153
|
+
if (!tool) continue;
|
|
154
|
+
if (tool.ok === false) failed += 1;
|
|
155
|
+
const args = tool.args;
|
|
156
|
+
if (!args || typeof args !== 'object') continue;
|
|
157
|
+
const file = args.file_path || args.path || args.notebook_path;
|
|
158
|
+
if (typeof file === 'string' && file && !files.includes(file)) files.push(file);
|
|
159
|
+
const cmd = args.command;
|
|
160
|
+
if (typeof cmd === 'string' && cmd.trim()) {
|
|
161
|
+
// First word only — the binary or builtin that ran, not its whole argv.
|
|
162
|
+
// "node /a/b/c.mjs --flag" reports as "node", which is what a one-line
|
|
163
|
+
// summary has room for.
|
|
164
|
+
const head = cmd.trim().split(/\s+/)[0].split('/').pop();
|
|
165
|
+
if (head && !commands.includes(head)) commands.push(head);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
tools: (tools || []).length,
|
|
170
|
+
failed,
|
|
171
|
+
files,
|
|
172
|
+
names: files.slice(0, maxNames).map((f) => f.split('/').pop()),
|
|
173
|
+
more: Math.max(0, files.length - maxNames),
|
|
174
|
+
commands,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
134
178
|
/**
|
|
135
179
|
* Finalize an assistant message's text: append a "(backend error: …)" marker
|
|
136
180
|
* when the turn failed mid-stream, a single "(stopped)" marker on abort, and
|
|
@@ -358,6 +402,36 @@ function rowLines(msg, cols, ctx, now = Date.now()) {
|
|
|
358
402
|
out.push(line);
|
|
359
403
|
return out;
|
|
360
404
|
}
|
|
405
|
+
if (msg.role === 'summary') {
|
|
406
|
+
// The end-of-turn delta line. Rendered as the LAST row of the turn so the
|
|
407
|
+
// bottom of the screen answers "what did that actually do?" without the
|
|
408
|
+
// reader scrolling back up through the tool rows that produced it.
|
|
409
|
+
const s = msg.counts || {};
|
|
410
|
+
if (!s.tools) return out;
|
|
411
|
+
const line = [span(t.dim, GLYPH.hook), span('', ' ')];
|
|
412
|
+
const bits = [
|
|
413
|
+
span(t.white, String(s.tools)),
|
|
414
|
+
span(t.gray, ` ${s.tools === 1 ? 'command' : 'commands'}`),
|
|
415
|
+
];
|
|
416
|
+
if (s.files && s.files.length) {
|
|
417
|
+
bits.push(span(t.gray, ` ${GLYPH.bullet} `));
|
|
418
|
+
bits.push(
|
|
419
|
+
span(t.white, String(s.files.length)),
|
|
420
|
+
span(t.gray, ` ${s.files.length === 1 ? 'file changed' : 'files changed'}`)
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
if (s.failed) {
|
|
424
|
+
bits.push(span(t.gray, ` ${GLYPH.bullet} `));
|
|
425
|
+
bits.push(span(t.coral, `${s.failed} failed`));
|
|
426
|
+
}
|
|
427
|
+
if (s.names && s.names.length) {
|
|
428
|
+
const more = s.more ? ` +${s.more}` : '';
|
|
429
|
+
bits.push(span(t.dim, ` ${s.names.join(', ')}${more}`));
|
|
430
|
+
}
|
|
431
|
+
line.push(...bits);
|
|
432
|
+
out.push(line);
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
361
435
|
if (msg.role === 'meta') {
|
|
362
436
|
// Deliberate divergence from the reference: this client's reason to exist
|
|
363
437
|
// is showing what a turn consumed, so the accounting line is a transcript
|
|
@@ -591,6 +665,10 @@ async function runSession(host) {
|
|
|
591
665
|
let verb = VERBS[0];
|
|
592
666
|
let turnCount = 0;
|
|
593
667
|
let toolSeq = 0;
|
|
668
|
+
// This turn's raw tool events, kept so the end-of-turn summary can report the
|
|
669
|
+
// deltas (commands run, files touched, failures) without re-reading the
|
|
670
|
+
// transcript — the rows are display state, the events are the record.
|
|
671
|
+
let turnTools = [];
|
|
594
672
|
let suggestionIdx = 0;
|
|
595
673
|
let scroll = 0;
|
|
596
674
|
let anchorEnd = null;
|
|
@@ -730,7 +808,6 @@ async function runSession(host) {
|
|
|
730
808
|
|
|
731
809
|
let streamedChars = 0;
|
|
732
810
|
const streamedCells = () => streamedChars;
|
|
733
|
-
|
|
734
811
|
// ── the turn ──
|
|
735
812
|
|
|
736
813
|
const confirmTool = (info) =>
|
|
@@ -779,6 +856,7 @@ async function runSession(host) {
|
|
|
779
856
|
startedAt = Date.now();
|
|
780
857
|
streamedChars = 0;
|
|
781
858
|
toolSeq = 0;
|
|
859
|
+
turnTools = [];
|
|
782
860
|
verb = VERBS[turnCount % VERBS.length];
|
|
783
861
|
abort = new AbortController();
|
|
784
862
|
spinnerTimer = setInterval(() => {
|
|
@@ -803,6 +881,7 @@ async function runSession(host) {
|
|
|
803
881
|
// from one that produced nothing at all.
|
|
804
882
|
reasoning: () => { sawReasoning = true; },
|
|
805
883
|
tool: (tool) => {
|
|
884
|
+
turnTools.push(tool);
|
|
806
885
|
if (tool.phase === 'run') {
|
|
807
886
|
const n = ++toolSeq;
|
|
808
887
|
const prefix = tool.agent ? `${tool.agent} ▸ ` : '';
|
|
@@ -819,8 +898,27 @@ async function runSession(host) {
|
|
|
819
898
|
},
|
|
820
899
|
{ follow: false }
|
|
821
900
|
);
|
|
822
|
-
} else {
|
|
823
|
-
|
|
901
|
+
} else if (!resolveToolDone(transcript, tool)) {
|
|
902
|
+
// A host that reports a tool once, after it ran, never opens a
|
|
903
|
+
// `phase: 'run'` row for resolveToolDone to close — the vendored
|
|
904
|
+
// local engine does exactly this (one `tool: {name,args,ok}` frame
|
|
905
|
+
// post-execution). Falling through left the row unwritten, so the
|
|
906
|
+
// turn silently showed no tool activity at all. Record the finished
|
|
907
|
+
// tool directly instead.
|
|
908
|
+
const n = ++toolSeq;
|
|
909
|
+
push(
|
|
910
|
+
{
|
|
911
|
+
role: 'tool',
|
|
912
|
+
phase: 'done',
|
|
913
|
+
name: tool.name,
|
|
914
|
+
args: tool.args,
|
|
915
|
+
agent: tool.agent,
|
|
916
|
+
id: tool.id,
|
|
917
|
+
ok: tool.ok,
|
|
918
|
+
label: `Ran ${n} ${tool.agent ? `${tool.agent} ▸ ` : ''}${toolLabel(tool.name, n)}`,
|
|
919
|
+
},
|
|
920
|
+
{ follow: false }
|
|
921
|
+
);
|
|
824
922
|
}
|
|
825
923
|
scheduleRender();
|
|
826
924
|
},
|
|
@@ -909,6 +1007,17 @@ async function runSession(host) {
|
|
|
909
1007
|
},
|
|
910
1008
|
{ follow: false }
|
|
911
1009
|
);
|
|
1010
|
+
// The turn's last row: a one-line delta of what the tools actually did.
|
|
1011
|
+
// Without it the tail of a tool-heavy turn was the accounting row, and
|
|
1012
|
+
// the answer to "what did that change?" was only reachable by scrolling
|
|
1013
|
+
// back up through the tool rows. Follows the viewport EXCEPT when the
|
|
1014
|
+
// reader has scrolled up themselves — scroll is non-zero exactly when
|
|
1015
|
+
// they are mid-read, and yanking them to the bottom then would undo the
|
|
1016
|
+
// scroll they asked for.
|
|
1017
|
+
const counts = summarizeTurnTools(turnTools);
|
|
1018
|
+
if (counts.tools > 0) {
|
|
1019
|
+
push({ role: 'summary', counts }, { follow: scroll === 0 });
|
|
1020
|
+
}
|
|
912
1021
|
render();
|
|
913
1022
|
}
|
|
914
1023
|
};
|
|
@@ -1645,6 +1754,7 @@ module.exports = {
|
|
|
1645
1754
|
FRAME_MS,
|
|
1646
1755
|
toolLabel,
|
|
1647
1756
|
resolveToolDone,
|
|
1757
|
+
summarizeTurnTools,
|
|
1648
1758
|
finalizeTurnText,
|
|
1649
1759
|
historyPairs,
|
|
1650
1760
|
paletteQuery,
|
package/src/commands.js
CHANGED
|
@@ -1211,6 +1211,25 @@ const COMMANDS = [
|
|
|
1211
1211
|
tool: 'aegis_memory_list',
|
|
1212
1212
|
build: () => ({}),
|
|
1213
1213
|
},
|
|
1214
|
+
{
|
|
1215
|
+
name: 'memory-deep', aliases: ['recall-deep'], args: ['mode'], hint: '[on|off]', category: 'model',
|
|
1216
|
+
desc: 'Toggle DEEP recall for this session — brain corrections + cached answers (metered: one embedding per turn)',
|
|
1217
|
+
handler: async (c, args) => {
|
|
1218
|
+
const mode = (args.mode || '').toLowerCase();
|
|
1219
|
+
if (mode && !['on', 'off'].includes(mode)) { note(c, 'Usage: /memory-deep [on|off]'); c.render(); return true; }
|
|
1220
|
+
const want = mode === 'on' ? true : mode === 'off' ? false : !(c.ctx.recallDeep === true);
|
|
1221
|
+
c.ctx.recallDeep = want;
|
|
1222
|
+
// Deliberately session-scoped: nothing is written to config.json, because
|
|
1223
|
+
// this flag meters every turn (aegis1 services/brain_memory.py embeds the
|
|
1224
|
+
// query once per turn) and a silent restore on a later launch would bill
|
|
1225
|
+
// for a decision made in a session that has ended. Contrast /thinking,
|
|
1226
|
+
// which persists — it costs nothing. The cheap read half (`aegis_recall`)
|
|
1227
|
+
// is always on and has no toggle.
|
|
1228
|
+
note(c, `Deep recall: ${want ? 'on — brain corrections + cached answers, one embedding per turn' : 'off (only the cheap recall read)'}${want ? ' · this session only' : ''}`);
|
|
1229
|
+
c.render();
|
|
1230
|
+
return true;
|
|
1231
|
+
},
|
|
1232
|
+
},
|
|
1214
1233
|
{
|
|
1215
1234
|
name: 'confirm', aliases: ['confirmations'], args: ['mode'], hint: '[on|off]', category: 'model',
|
|
1216
1235
|
desc: 'Toggle tool-call confirmation prompts',
|
package/src/deps.js
CHANGED
|
@@ -65,6 +65,9 @@ const agentsPath = resolveShared(path.join('desktop', 'lib', 'local', 'agents.js
|
|
|
65
65
|
// running shell commands. Same file as the GUI, for the same reason engine.js
|
|
66
66
|
// is: two personas would drift, and only one of them would get fixed.
|
|
67
67
|
const promptPath = resolveShared(path.join('desktop', 'lib', 'local', 'prompt.js'));
|
|
68
|
+
// The npm update checker, shared with the desktop host: two package names, one
|
|
69
|
+
// implementation, so a fix to the check reaches both.
|
|
70
|
+
const updatePath = resolveShared(path.join('client', 'update.js'));
|
|
68
71
|
|
|
69
72
|
const { createClient } = require(clientPath);
|
|
70
73
|
const { createTools } = require(toolsPath);
|
|
@@ -72,8 +75,10 @@ const { usageTokens } = require(usagePath);
|
|
|
72
75
|
const { createLocalEngine } = require(enginePath);
|
|
73
76
|
const { agentRoles, agentRoleLabel } = require(agentsPath);
|
|
74
77
|
const { buildSystemPrompt } = require(promptPath);
|
|
78
|
+
const updater = require(updatePath);
|
|
75
79
|
|
|
76
80
|
module.exports = {
|
|
81
|
+
resolveShared,
|
|
77
82
|
createClient,
|
|
78
83
|
createTools,
|
|
79
84
|
usageTokens,
|
|
@@ -81,6 +86,7 @@ module.exports = {
|
|
|
81
86
|
agentRoles,
|
|
82
87
|
agentRoleLabel,
|
|
83
88
|
buildSystemPrompt,
|
|
84
|
-
|
|
89
|
+
updater,
|
|
90
|
+
paths: { client: clientPath, tools: toolsPath, usage: usagePath, engine: enginePath, agents: agentsPath, prompt: promptPath, update: updatePath },
|
|
85
91
|
roots,
|
|
86
92
|
};
|
package/src/render.js
CHANGED
|
@@ -31,7 +31,7 @@ const {
|
|
|
31
31
|
DONE_VERBS,
|
|
32
32
|
themeOf,
|
|
33
33
|
} = require('./theme.js');
|
|
34
|
-
const { WELCOME_TITLE, WELCOME_BACK, TAGLINE, welcomeArtParts } = require('./art.js');
|
|
34
|
+
const { WELCOME_TITLE, WELCOME_BACK, TAGLINE, STAR, stencilGlyph, welcomeArtParts } = require('./art.js');
|
|
35
35
|
const { w, pad, padStart, wrapBlock, clip, span } = require('./screen.js');
|
|
36
36
|
const { fmtTokens, fmtEur, fmtElapsed } = require('./format.js');
|
|
37
37
|
|
|
@@ -63,9 +63,17 @@ function centerStyled(text, width, style) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/** The right (moon + whale) half of the mark: ▓ blue, ▒ lavender, ░ dim,
|
|
66
|
-
* eye white, stars gold.
|
|
66
|
+
* eye white, stars gold.
|
|
67
|
+
*
|
|
68
|
+
* The keys are the runes as `art.js` drew them, then run through the stencil:
|
|
69
|
+
* `artRow` is handed rows that have *already* been stencilled for this
|
|
70
|
+
* platform, so a raw-rune table silently matched nothing on win32/darwin and
|
|
71
|
+
* the whole moon/whale half rendered uncoloured there — the mark looked
|
|
72
|
+
* different on two of the three platforms it ships to. */
|
|
67
73
|
function tintWhale(str, t) {
|
|
68
|
-
const
|
|
74
|
+
const byRune = { '▓': t.blue, '▒': t.lavender, '░': t.dim, '█': t.white, [STAR]: t.gold, '·': t.dim };
|
|
75
|
+
const map = {};
|
|
76
|
+
for (const rune of Object.keys(byRune)) map[stencilGlyph(rune)] = byRune[rune];
|
|
69
77
|
const spans = [];
|
|
70
78
|
let cur = null;
|
|
71
79
|
let style = '';
|
package/src/theme.js
CHANGED
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* Zero dependencies: SGR escapes are hand-built, like the rest of this repo.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
// The stencil is the mark's single source of truth for its own runes — this
|
|
20
|
+
// file resolves `star` through it rather than restating the platform rule.
|
|
21
|
+
// `art.js` requires nothing, so this import cannot cycle.
|
|
22
|
+
const { STAR, stencilGlyph } = require('./art.js');
|
|
23
|
+
|
|
19
24
|
const RESET = '\x1b[0m';
|
|
20
25
|
const BOLD = '\x1b[1m';
|
|
21
26
|
const DIM = '\x1b[2m';
|
|
@@ -66,8 +71,16 @@ const LIGHT = {
|
|
|
66
71
|
* or render as double-width emoji (✦ U+2726, ✻/✢/✽/✶), which breaks row
|
|
67
72
|
* alignment exactly like the welcome art — those platforms get single-width
|
|
68
73
|
* stand-ins instead.
|
|
74
|
+
*
|
|
75
|
+
* `star` is NOT restated here: it is resolved through `art.js`'s stencil, the
|
|
76
|
+
* single source of truth for the mark's runes. This file used to carry its own
|
|
77
|
+
* `'✦' → '*'` rule on win32/darwin, which is precisely how the star could stay
|
|
78
|
+
* wide on one platform while the art got stencilled on another — the mark and
|
|
79
|
+
* the glyph table would then disagree about the same character. Every other
|
|
80
|
+
* entry below is a *text* glyph (cursor, hook, spinner frames) that no art row
|
|
81
|
+
* contains, so it keeps its own definition.
|
|
69
82
|
*/
|
|
70
|
-
|
|
83
|
+
function glyphsFor(platform = process.platform) {
|
|
71
84
|
const base = {
|
|
72
85
|
cursor: '❯', // menu selection marker, prompt prefix
|
|
73
86
|
check: '✔', // selected / completed
|
|
@@ -75,7 +88,7 @@ const GLYPH = (() => {
|
|
|
75
88
|
bullet: '·', // inline separators
|
|
76
89
|
hint: '❯', // "Try ..." suggestion marker
|
|
77
90
|
pause: '⏸', // bottom status line (manual mode)
|
|
78
|
-
star:
|
|
91
|
+
star: stencilGlyph(STAR, platform), // own-session marker, decorations
|
|
79
92
|
leftarrow: '←', // hints on the status line
|
|
80
93
|
pointer: '▸', // tips list bullets
|
|
81
94
|
hook: '⎿', // inline command / tip rows (tool commands, usage hints)
|
|
@@ -84,26 +97,26 @@ const GLYPH = (() => {
|
|
|
84
97
|
spin: ['✢', '·', '✻', '*', '✽', '✶'], // working spinner
|
|
85
98
|
ellipse: '…',
|
|
86
99
|
};
|
|
87
|
-
if (
|
|
100
|
+
if (platform === 'win32') {
|
|
88
101
|
return {
|
|
89
102
|
...base,
|
|
90
103
|
pause: '❚❚', // ⏸ U+23F8 missing from many Windows fonts
|
|
91
104
|
hook: '_|', // ⎿ U+23BF almost never present in Windows fonts
|
|
92
|
-
star: '*', // ✦ U+2726 renders wide/emoji
|
|
93
105
|
bloom: '*', // ✻ U+273B
|
|
94
106
|
spin: ['|', '/', '-', '\\', '*', '-'],
|
|
95
107
|
};
|
|
96
108
|
}
|
|
97
|
-
if (
|
|
109
|
+
if (platform === 'darwin') {
|
|
98
110
|
return {
|
|
99
111
|
...base,
|
|
100
112
|
pause: '❚❚', // ⏸ has an emoji presentation on Apple fonts
|
|
101
|
-
star: '*', // ✦ renders as a double-width emoji in Terminal.app
|
|
102
113
|
hook: '_|', // ⎿ U+23BF not in Apple monospace fonts
|
|
103
114
|
};
|
|
104
115
|
}
|
|
105
116
|
return base;
|
|
106
|
-
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const GLYPH = glyphsFor(process.platform);
|
|
107
120
|
|
|
108
121
|
/** Working-line verbs, verbatim from the capture-backed table. */
|
|
109
122
|
const VERBS = [
|
|
@@ -216,6 +229,7 @@ module.exports = {
|
|
|
216
229
|
THEME_TABLE,
|
|
217
230
|
themeForIndex,
|
|
218
231
|
GLYPH,
|
|
232
|
+
glyphsFor,
|
|
219
233
|
VERBS,
|
|
220
234
|
DONE_VERBS,
|
|
221
235
|
themeOf,
|
package/src/update.js
CHANGED
|
@@ -1,137 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
* "There is a newer version" — the notice this CLI never had.
|
|
3
|
-
*
|
|
4
|
-
* Checked on 2026-09-15: there was no update check anywhere in the client, so
|
|
5
|
-
* a user learned about a release only by guessing to run
|
|
6
|
-
* `npm i -g aegiscode@latest`. Publishing therefore reached almost nobody, and
|
|
7
|
-
* the installed base drifted months behind the registry.
|
|
8
|
-
*
|
|
9
|
-
* Three rules, because an update check is a background nicety that must never
|
|
10
|
-
* become a liability:
|
|
11
|
-
* · it never blocks — the caller gets a cached answer immediately and the
|
|
12
|
-
* network call settles whenever it settles;
|
|
13
|
-
* · it never throws — offline, proxied, rate-limited or garbage JSON all
|
|
14
|
-
* mean "no notice", not a broken CLI;
|
|
15
|
-
* · it never nags — one check a day, cached, and nothing printed when the
|
|
16
|
-
* user is already current.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const https = require('node:https');
|
|
20
|
-
|
|
21
|
-
const REGISTRY = 'https://registry.npmjs.org';
|
|
22
|
-
const PKG = 'aegiscode';
|
|
23
|
-
/** One check a day. A CLI that pings the registry every launch is spyware. */
|
|
24
|
-
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
25
|
-
/** The registry is not on the critical path; give up quickly and silently. */
|
|
26
|
-
const TIMEOUT_MS = 2500;
|
|
27
|
-
|
|
28
|
-
/** Compare two semver-ish strings. Returns true when `latest` is newer. */
|
|
29
|
-
function isNewer(latest, current) {
|
|
30
|
-
const parse = (v) =>
|
|
31
|
-
String(v || '')
|
|
32
|
-
.trim()
|
|
33
|
-
.replace(/^v/, '')
|
|
34
|
-
.split('-')[0] // a prerelease never counts as newer than its release
|
|
35
|
-
.split('.')
|
|
36
|
-
.map((n) => Number.parseInt(n, 10) || 0);
|
|
37
|
-
const a = parse(latest);
|
|
38
|
-
const b = parse(current);
|
|
39
|
-
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
40
|
-
const x = a[i] || 0;
|
|
41
|
-
const y = b[i] || 0;
|
|
42
|
-
if (x !== y) return x > y;
|
|
43
|
-
}
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Fetch the registry's `latest` dist-tag. Resolves null on any failure. */
|
|
48
|
-
function fetchLatest({ pkg = PKG, timeoutMs = TIMEOUT_MS } = {}) {
|
|
49
|
-
return new Promise((resolve) => {
|
|
50
|
-
let settled = false;
|
|
51
|
-
const done = (v) => {
|
|
52
|
-
if (!settled) {
|
|
53
|
-
settled = true;
|
|
54
|
-
resolve(v);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
try {
|
|
58
|
-
// The abbreviated metadata document: a few KB instead of the full
|
|
59
|
-
// packument, which for a package with this many releases is megabytes.
|
|
60
|
-
const req = https.get(
|
|
61
|
-
`${REGISTRY}/${pkg}`,
|
|
62
|
-
{ headers: { accept: 'application/vnd.npm.install-v1+json' }, timeout: timeoutMs },
|
|
63
|
-
(res) => {
|
|
64
|
-
if (res.statusCode !== 200) {
|
|
65
|
-
res.resume();
|
|
66
|
-
return done(null);
|
|
67
|
-
}
|
|
68
|
-
let body = '';
|
|
69
|
-
res.setEncoding('utf8');
|
|
70
|
-
res.on('data', (c) => {
|
|
71
|
-
body += c;
|
|
72
|
-
if (body.length > 2_000_000) {
|
|
73
|
-
req.destroy();
|
|
74
|
-
done(null);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
res.on('end', () => {
|
|
78
|
-
try {
|
|
79
|
-
const tags = JSON.parse(body)['dist-tags'];
|
|
80
|
-
done((tags && tags.latest) || null);
|
|
81
|
-
} catch {
|
|
82
|
-
done(null);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
);
|
|
87
|
-
req.on('timeout', () => {
|
|
88
|
-
req.destroy();
|
|
89
|
-
done(null);
|
|
90
|
-
});
|
|
91
|
-
req.on('error', () => done(null));
|
|
92
|
-
} catch {
|
|
93
|
-
done(null);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
1
|
+
'use strict';
|
|
97
2
|
|
|
98
3
|
/**
|
|
99
|
-
* The
|
|
4
|
+
* The CLI's view of the shared npm update checker (client/update.js).
|
|
100
5
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
6
|
+
* It lives at the repo root rather than here because the desktop host needs
|
|
7
|
+
* the same thing for `aegis-desktop`: two hosts, two package names, one
|
|
8
|
+
* checker. Resolved through deps.js so the in-repo and published-vendor
|
|
9
|
+
* layouts both work.
|
|
104
10
|
*/
|
|
105
|
-
function updateNotice({
|
|
106
|
-
current,
|
|
107
|
-
cache,
|
|
108
|
-
save,
|
|
109
|
-
now = Date.now(),
|
|
110
|
-
fetchImpl = fetchLatest,
|
|
111
|
-
intervalMs = CHECK_INTERVAL_MS,
|
|
112
|
-
} = {}) {
|
|
113
|
-
const c = cache || {};
|
|
114
|
-
const fresh = typeof c.checkedAt === 'number' && now - c.checkedAt < intervalMs;
|
|
115
|
-
|
|
116
|
-
if (!fresh) {
|
|
117
|
-
// Fire and forget: this run reports on what was already known, and the
|
|
118
|
-
// answer lands for the next one. A first run therefore never shows a
|
|
119
|
-
// notice, which is correct — it has nothing to compare against yet.
|
|
120
|
-
Promise.resolve(fetchImpl())
|
|
121
|
-
.then((latest) => {
|
|
122
|
-
if (latest && typeof save === 'function') save({ checkedAt: now, latest });
|
|
123
|
-
})
|
|
124
|
-
.catch(() => {});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const latest = c.latest || null;
|
|
128
|
-
return { latest, behind: !!(latest && isNewer(latest, current)) };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** One line for the welcome box, or null when there is nothing to say. */
|
|
132
|
-
function updateLine({ current, latest, behind }) {
|
|
133
|
-
if (!behind) return null;
|
|
134
|
-
return `Update available: ${current} → ${latest} run: npm i -g ${PKG}@latest`;
|
|
135
|
-
}
|
|
136
11
|
|
|
137
|
-
|
|
12
|
+
const { resolveShared } = require('./deps.js');
|
|
13
|
+
module.exports = require(resolveShared('client/update.js'));
|
package/vendor/client/aegis.js
CHANGED
|
@@ -664,11 +664,21 @@ function createClient(opts = {}) {
|
|
|
664
664
|
return apiGet('/api/token-bank/balance');
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
-
/** Start a token-bank top-up; resolves to { url } for the payment page.
|
|
667
|
+
/** Start a token-bank top-up; resolves to { url } for the payment page.
|
|
668
|
+
* aegis1 requires a signed-in account (403/401 without a key) and rejects
|
|
669
|
+
* an amount outside 2..1000 EUR with 400. */
|
|
668
670
|
async function tokenBankTopup(amountEur) {
|
|
669
671
|
return apiPost('/api/token-bank/topup', { amount_eur: amountEur });
|
|
670
672
|
}
|
|
671
673
|
|
|
674
|
+
/** Start a plan checkout; resolves to { url } for Stripe's hosted page.
|
|
675
|
+
* aegis1 `/api/billing/checkout` is deliberately public (the Stripe page
|
|
676
|
+
* collects the email), so this works with no key configured — but answers
|
|
677
|
+
* 503 `setup_required` while the price id is unset on the server. */
|
|
678
|
+
async function billingCheckout() {
|
|
679
|
+
return apiPost('/api/billing/checkout', {});
|
|
680
|
+
}
|
|
681
|
+
|
|
672
682
|
async function byokStatus() {
|
|
673
683
|
return apiGet('/api/user/api-keys');
|
|
674
684
|
}
|
|
@@ -797,6 +807,7 @@ function createClient(opts = {}) {
|
|
|
797
807
|
listModels,
|
|
798
808
|
tokenBankBalance,
|
|
799
809
|
tokenBankTopup,
|
|
810
|
+
billingCheckout,
|
|
800
811
|
byokStatus,
|
|
801
812
|
byokSet,
|
|
802
813
|
getMemoryToken,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "There is a newer version" — the notice this CLI never had.
|
|
3
|
+
*
|
|
4
|
+
* Checked on 2026-09-15: there was no update check anywhere in the client, so
|
|
5
|
+
* a user learned about a release only by guessing to run
|
|
6
|
+
* `npm i -g aegiscode@latest`. Publishing therefore reached almost nobody, and
|
|
7
|
+
* the installed base drifted months behind the registry.
|
|
8
|
+
*
|
|
9
|
+
* Three rules, because an update check is a background nicety that must never
|
|
10
|
+
* become a liability:
|
|
11
|
+
* · it never blocks — the caller gets a cached answer immediately and the
|
|
12
|
+
* network call settles whenever it settles;
|
|
13
|
+
* · it never throws — offline, proxied, rate-limited or garbage JSON all
|
|
14
|
+
* mean "no notice", not a broken CLI;
|
|
15
|
+
* · it never nags — one check a day, cached, and nothing printed when the
|
|
16
|
+
* user is already current.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const https = require('node:https');
|
|
20
|
+
|
|
21
|
+
const REGISTRY = 'https://registry.npmjs.org';
|
|
22
|
+
//: Default package. Both hosts ship from npm — the CLI as `aegiscode`, the
|
|
23
|
+
//: desktop as `aegis-desktop` — so one checker serves both; the caller names
|
|
24
|
+
//: which one it is.
|
|
25
|
+
const PKG = 'aegiscode';
|
|
26
|
+
/** One check a day. A CLI that pings the registry every launch is spyware. */
|
|
27
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
28
|
+
/** The registry is not on the critical path; give up quickly and silently. */
|
|
29
|
+
const TIMEOUT_MS = 2500;
|
|
30
|
+
|
|
31
|
+
/** Compare two semver-ish strings. Returns true when `latest` is newer. */
|
|
32
|
+
function isNewer(latest, current) {
|
|
33
|
+
const parse = (v) =>
|
|
34
|
+
String(v || '')
|
|
35
|
+
.trim()
|
|
36
|
+
.replace(/^v/, '')
|
|
37
|
+
.split('-')[0] // a prerelease never counts as newer than its release
|
|
38
|
+
.split('.')
|
|
39
|
+
.map((n) => Number.parseInt(n, 10) || 0);
|
|
40
|
+
const a = parse(latest);
|
|
41
|
+
const b = parse(current);
|
|
42
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
43
|
+
const x = a[i] || 0;
|
|
44
|
+
const y = b[i] || 0;
|
|
45
|
+
if (x !== y) return x > y;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Fetch the registry's `latest` dist-tag. Resolves null on any failure. */
|
|
51
|
+
function fetchLatest({ pkg = PKG, timeoutMs = TIMEOUT_MS } = {}) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
let settled = false;
|
|
54
|
+
const done = (v) => {
|
|
55
|
+
if (!settled) {
|
|
56
|
+
settled = true;
|
|
57
|
+
resolve(v);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
// The abbreviated metadata document: a few KB instead of the full
|
|
62
|
+
// packument, which for a package with this many releases is megabytes.
|
|
63
|
+
const req = https.get(
|
|
64
|
+
`${REGISTRY}/${pkg}`,
|
|
65
|
+
{ headers: { accept: 'application/vnd.npm.install-v1+json' }, timeout: timeoutMs },
|
|
66
|
+
(res) => {
|
|
67
|
+
if (res.statusCode !== 200) {
|
|
68
|
+
res.resume();
|
|
69
|
+
return done(null);
|
|
70
|
+
}
|
|
71
|
+
let body = '';
|
|
72
|
+
res.setEncoding('utf8');
|
|
73
|
+
res.on('data', (c) => {
|
|
74
|
+
body += c;
|
|
75
|
+
if (body.length > 2_000_000) {
|
|
76
|
+
req.destroy();
|
|
77
|
+
done(null);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
res.on('end', () => {
|
|
81
|
+
try {
|
|
82
|
+
const tags = JSON.parse(body)['dist-tags'];
|
|
83
|
+
done((tags && tags.latest) || null);
|
|
84
|
+
} catch {
|
|
85
|
+
done(null);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
req.on('timeout', () => {
|
|
91
|
+
req.destroy();
|
|
92
|
+
done(null);
|
|
93
|
+
});
|
|
94
|
+
req.on('error', () => done(null));
|
|
95
|
+
} catch {
|
|
96
|
+
done(null);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The notice to show, from cache. Never waits on the network.
|
|
103
|
+
*
|
|
104
|
+
* `cache` is the persisted `{ checkedAt, latest }` blob; `save` persists a new
|
|
105
|
+
* one. Returns `{ latest, behind }`, and kicks off a refresh in the background
|
|
106
|
+
* when the cache is stale.
|
|
107
|
+
*/
|
|
108
|
+
function updateNotice({
|
|
109
|
+
current,
|
|
110
|
+
cache,
|
|
111
|
+
save,
|
|
112
|
+
now = Date.now(),
|
|
113
|
+
fetchImpl = fetchLatest,
|
|
114
|
+
intervalMs = CHECK_INTERVAL_MS,
|
|
115
|
+
} = {}) {
|
|
116
|
+
const c = cache || {};
|
|
117
|
+
const fresh = typeof c.checkedAt === 'number' && now - c.checkedAt < intervalMs;
|
|
118
|
+
|
|
119
|
+
if (!fresh) {
|
|
120
|
+
// Fire and forget: this run reports on what was already known, and the
|
|
121
|
+
// answer lands for the next one. A first run therefore never shows a
|
|
122
|
+
// notice, which is correct — it has nothing to compare against yet.
|
|
123
|
+
Promise.resolve(fetchImpl())
|
|
124
|
+
.then((latest) => {
|
|
125
|
+
if (latest && typeof save === 'function') save({ checkedAt: now, latest });
|
|
126
|
+
})
|
|
127
|
+
.catch(() => {});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const latest = c.latest || null;
|
|
131
|
+
return { latest, behind: !!(latest && isNewer(latest, current)) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** One line for the welcome box, or null when there is nothing to say. */
|
|
135
|
+
function updateLine({ current, latest, behind, pkg = PKG }) {
|
|
136
|
+
if (!behind) return null;
|
|
137
|
+
return `Update available: ${current} → ${latest} run: npm i -g ${pkg}@latest`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { isNewer, fetchLatest, updateNotice, updateLine, PKG, CHECK_INTERVAL_MS };
|
|
@@ -598,12 +598,53 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
598
598
|
// AUTONOMOUS_IDLE_TIMEOUT_MS). Undefined elsewhere -> 60s default.
|
|
599
599
|
idleTimeoutMs: opts.idleTimeoutMs,
|
|
600
600
|
signal: opts.signal,
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
601
|
+
// aegis_recall — the read half of aegis_memory, and the only half a
|
|
602
|
+
// client on this engine should send.
|
|
603
|
+
//
|
|
604
|
+
// aegis_memory is both halves: it injects the account's synced memory
|
|
605
|
+
// into context AND persists this turn back into it, charging sync
|
|
606
|
+
// quota for the write. That pairing is right for aegis-online, whose
|
|
607
|
+
// chat has nowhere else to live. It is wrong here, because this engine
|
|
608
|
+
// is shared by the GUI and the CLI (cli/src/deps.js loads this file):
|
|
609
|
+
// either would be billed for every "hey" and would fill the user's
|
|
610
|
+
// memory with greetings. Verified against aegis1 app.py: every
|
|
611
|
+
// write-back site (the note/upsert calls) is gated on aegis_memory,
|
|
612
|
+
// and aegis_memory implies aegis_recall there — so dropping the write
|
|
613
|
+
// half leaves /online unchanged and costs nothing on the read side.
|
|
614
|
+
//
|
|
615
|
+
// Writing is still available, explicitly: the aegis_memory_save tool
|
|
616
|
+
// (mcp/tools.js; the CLI exposes it as /memory). Recall on every turn,
|
|
617
|
+
// store only what the user asks for — which is what aegiscodex-dev's
|
|
618
|
+
// own cross-session memory does, and what this comment claimed to
|
|
619
|
+
// match while sending both halves.
|
|
620
|
+
//
|
|
621
|
+
// Recall was previously unreachable for a client that would not pay
|
|
622
|
+
// for it: services/tiered_recall.py only ran behind a flag that also
|
|
623
|
+
// bought a write, so the tiered path existed with no caller able to
|
|
624
|
+
// afford it. The split is what makes it reachable. The DEEP tier of the
|
|
625
|
+
// same read is a third flag with its own price — see `opts.recallDeep`
|
|
626
|
+
// below, which is off unless the session opted in.
|
|
605
627
|
extra: {
|
|
606
|
-
|
|
628
|
+
aegis_recall: true,
|
|
629
|
+
// The DEEP tier of that read — brain corrections plus the semantic
|
|
630
|
+
// answer cache — is not the same price, so it does not ride along.
|
|
631
|
+
// aegis1 app.py:8367 reads `aegis_recall_deep` (or the
|
|
632
|
+
// X-AEGIS-Recall-Deep header) and services/brain_memory.py
|
|
633
|
+
// find_cached_answer embeds the query: one provider embedding per
|
|
634
|
+
// turn, metered. The server deliberately implies it from
|
|
635
|
+
// `aegis_memory` and NOT from `aegis_recall`, so that a terminal
|
|
636
|
+
// client can buy the cheap read without the embedding.
|
|
637
|
+
//
|
|
638
|
+
// This client is that terminal client (the CLI loads this file via
|
|
639
|
+
// cli/src/deps.js), so it must not opt itself in: the flag is sent
|
|
640
|
+
// only when the SESSION asked for it — CLI `/memory-deep on`, a
|
|
641
|
+
// desktop payload with `recallDeep: true` — and it defaults false
|
|
642
|
+
// everywhere. It also travels only on the user's own turn
|
|
643
|
+
// (`opts.recallDeep` is cleared for every other dispatch below): a
|
|
644
|
+
// tool round, the doubled-budget retry and the write-up re-dispatch
|
|
645
|
+
// all re-send a context whose embedding the first round already
|
|
646
|
+
// bought, which would turn one embedding per turn into one per round.
|
|
647
|
+
...(opts.recallDeep ? { aegis_recall_deep: true } : {}),
|
|
607
648
|
session: opts.sessionId,
|
|
608
649
|
// The fan-out is opt-in per dispatch. `brain` is sent EXPLICITLY
|
|
609
650
|
// whenever this dispatch is not the autonomous one, because the
|
|
@@ -759,6 +800,14 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
759
800
|
cls, model, mode: payload && payload.mode, maxTokens, statedMaxTokens, autonomous, sessionId, signal, onDelta, cfg, apiKey, toolChoice,
|
|
760
801
|
effort: payload && payload.effort,
|
|
761
802
|
workers: payload && payload.workers,
|
|
803
|
+
// Deep recall (`aegis_recall_deep`) is an explicit per-session opt-in
|
|
804
|
+
// and never a default: it costs one provider embedding per turn
|
|
805
|
+
// server-side (aegis1 services/brain_memory.py find_cached_answer), so
|
|
806
|
+
// a client that pays per turn must not turn it on for itself. Only a
|
|
807
|
+
// literal `true` from the caller counts — an absent or `undefined`
|
|
808
|
+
// field is off, which is what keeps every existing caller (the
|
|
809
|
+
// renderer's IPC payloads included) on the cheap read.
|
|
810
|
+
recallDeep: payload && payload.recallDeep === true,
|
|
762
811
|
onReasoning,
|
|
763
812
|
idleTimeoutMs: autonomous ? AUTONOMOUS_IDLE_TIMEOUT_MS : undefined,
|
|
764
813
|
// A caller with no live streaming surface (a `--no-stream` CLI flag, a
|
|
@@ -873,7 +922,12 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
873
922
|
});
|
|
874
923
|
}
|
|
875
924
|
round += 1;
|
|
876
|
-
|
|
925
|
+
// `round === 1` is the user's own ask, and the ONLY dispatch allowed to
|
|
926
|
+
// carry the deep-recall opt-in: the deep tier embeds the query once per
|
|
927
|
+
// dispatch, so leaving it on for an agentic turn would charge one
|
|
928
|
+
// embedding per tool round instead of one per turn (the retries below
|
|
929
|
+
// clear it explicitly, being re-dispatches inside round 1).
|
|
930
|
+
const opts = { ...base, system, messages: history, prompt, tools: toolSchemas, recallDeep: base.recallDeep && round === 1 };
|
|
877
931
|
let res;
|
|
878
932
|
try {
|
|
879
933
|
res = await dispatch(cls, opts);
|
|
@@ -917,6 +971,10 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
917
971
|
res = await dispatch(cls, {
|
|
918
972
|
...opts,
|
|
919
973
|
singlePass: true,
|
|
974
|
+
// A re-dispatch, not a new ask: the deep tier's embedding was
|
|
975
|
+
// bought by round 1, and buying it again here would charge a
|
|
976
|
+
// second one for the same context.
|
|
977
|
+
recallDeep: false,
|
|
920
978
|
maxTokens: doubledBudget(opts.maxTokens),
|
|
921
979
|
// Doubling applies to the pooled path only when the caller stated a
|
|
922
980
|
// number. With none stated, the server's effort ladder IS the
|
|
@@ -948,6 +1006,9 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
|
|
|
948
1006
|
res = await dispatch(cls, {
|
|
949
1007
|
...opts,
|
|
950
1008
|
singlePass: true,
|
|
1009
|
+
// Same as the truncation retry above: this pass writes up findings
|
|
1010
|
+
// already in `history`, and a fresh embedding buys it nothing.
|
|
1011
|
+
recallDeep: false,
|
|
951
1012
|
messages: history,
|
|
952
1013
|
prompt: '',
|
|
953
1014
|
tools: [],
|
|
@@ -81,14 +81,12 @@ const fail = (error) => ({ ok: false, error: cap(error) });
|
|
|
81
81
|
const SCHEMAS = {
|
|
82
82
|
readFile: {
|
|
83
83
|
name: 'readFile',
|
|
84
|
-
description:
|
|
85
|
-
'Reads a file from the local filesystem. The file_path parameter must be an absolute path. ' +
|
|
86
|
-
'Returns line-numbered content.',
|
|
84
|
+
description: 'Read a file. Absolute path. Returns line-numbered content.',
|
|
87
85
|
parameters: {
|
|
88
86
|
type: 'object',
|
|
89
87
|
properties: {
|
|
90
|
-
file_path: { type: 'string', description: '
|
|
91
|
-
offset: { type: 'number', description: '
|
|
88
|
+
file_path: { type: 'string', description: 'Absolute path' },
|
|
89
|
+
offset: { type: 'number', description: 'Start line (0-based)' },
|
|
92
90
|
limit: { type: 'number', description: `The number of lines to read (max 10000, default ${READ_LINE_CAP})` },
|
|
93
91
|
},
|
|
94
92
|
required: ['file_path'],
|
|
@@ -98,13 +96,12 @@ const SCHEMAS = {
|
|
|
98
96
|
writeFile: {
|
|
99
97
|
name: 'writeFile',
|
|
100
98
|
description:
|
|
101
|
-
'
|
|
102
|
-
'Use it to create or replace a whole file; it overwrites whatever was there.',
|
|
99
|
+
'Write a whole file, overwriting it. Parent dirs are created. Use editFile to change part of one.',
|
|
103
100
|
parameters: {
|
|
104
101
|
type: 'object',
|
|
105
102
|
properties: {
|
|
106
|
-
file_path: { type: 'string', description: '
|
|
107
|
-
content: { type: 'string', description: '
|
|
103
|
+
file_path: { type: 'string', description: 'Absolute path' },
|
|
104
|
+
content: { type: 'string', description: 'Full file contents' },
|
|
108
105
|
},
|
|
109
106
|
required: ['file_path', 'content'],
|
|
110
107
|
additionalProperties: false,
|
|
@@ -113,16 +110,15 @@ const SCHEMAS = {
|
|
|
113
110
|
editFile: {
|
|
114
111
|
name: 'editFile',
|
|
115
112
|
description:
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'existing file — it fails loudly on a non-unique or missing match instead of guessing.',
|
|
113
|
+
'Exact string replacement. old_string must be unique unless replace_all. Fails loudly on a ' +
|
|
114
|
+
'missing or ambiguous match rather than guessing.',
|
|
119
115
|
parameters: {
|
|
120
116
|
type: 'object',
|
|
121
117
|
properties: {
|
|
122
|
-
file_path: { type: 'string', description: '
|
|
123
|
-
old_string: { type: 'string', description: '
|
|
124
|
-
new_string: { type: 'string', description: '
|
|
125
|
-
replace_all: { type: 'boolean', description: '
|
|
118
|
+
file_path: { type: 'string', description: 'Absolute path' },
|
|
119
|
+
old_string: { type: 'string', description: 'Text to replace; must be unique unless replace_all' },
|
|
120
|
+
new_string: { type: 'string', description: 'Replacement text' },
|
|
121
|
+
replace_all: { type: 'boolean', description: 'Replace every occurrence' },
|
|
126
122
|
},
|
|
127
123
|
required: ['file_path', 'old_string', 'new_string'],
|
|
128
124
|
additionalProperties: false,
|
|
@@ -130,9 +126,7 @@ const SCHEMAS = {
|
|
|
130
126
|
},
|
|
131
127
|
listDir: {
|
|
132
128
|
name: 'listDir',
|
|
133
|
-
description:
|
|
134
|
-
'Lists one directory (non-recursive). Directories are marked with a trailing slash. ' +
|
|
135
|
-
'Skips node_modules, .git and dist.',
|
|
129
|
+
description: 'List one directory (non-recursive). Skips node_modules, .git, dist.',
|
|
136
130
|
parameters: {
|
|
137
131
|
type: 'object',
|
|
138
132
|
properties: {
|
|
@@ -145,12 +139,12 @@ const SCHEMAS = {
|
|
|
145
139
|
glob: {
|
|
146
140
|
name: 'glob',
|
|
147
141
|
description:
|
|
148
|
-
'Find files
|
|
142
|
+
'Find files by glob (**, *, ?). Skips node_modules, .git, dist.',
|
|
149
143
|
parameters: {
|
|
150
144
|
type: 'object',
|
|
151
145
|
properties: {
|
|
152
|
-
pattern: { type: 'string', description: '
|
|
153
|
-
path: { type: 'string', description: '
|
|
146
|
+
pattern: { type: 'string', description: 'Glob, e.g. "**/*.test.js"' },
|
|
147
|
+
path: { type: 'string', description: 'Search root (default: cwd)' },
|
|
154
148
|
},
|
|
155
149
|
required: ['pattern'],
|
|
156
150
|
additionalProperties: false,
|
|
@@ -158,9 +152,7 @@ const SCHEMAS = {
|
|
|
158
152
|
},
|
|
159
153
|
grep: {
|
|
160
154
|
name: 'grep',
|
|
161
|
-
description:
|
|
162
|
-
'Search file contents using a regular expression. Returns file:line matches. ' +
|
|
163
|
-
'Skips node_modules, .git and dist by default.',
|
|
155
|
+
description: 'Search file contents by regex. Returns file:line matches. Skips node_modules, .git, dist.',
|
|
164
156
|
parameters: {
|
|
165
157
|
type: 'object',
|
|
166
158
|
properties: {
|
|
@@ -174,15 +166,13 @@ const SCHEMAS = {
|
|
|
174
166
|
exec: {
|
|
175
167
|
name: 'exec',
|
|
176
168
|
description:
|
|
177
|
-
'
|
|
178
|
-
'stdout+stderr
|
|
179
|
-
'within the same turn — it is a real session, not a fresh process each time. ' +
|
|
180
|
-
'Use for system operations, git commands and package management.',
|
|
169
|
+
'Run a shell command in a PERSISTENT session: cd and exported env carry across calls in ' +
|
|
170
|
+
'this turn. Returns stdout+stderr and the exit code. Use for git, packages, system ops.',
|
|
181
171
|
parameters: {
|
|
182
172
|
type: 'object',
|
|
183
173
|
properties: {
|
|
184
|
-
command: { type: 'string', description: '
|
|
185
|
-
cwd: { type: 'string', description: 'Run this one command
|
|
174
|
+
command: { type: 'string', description: 'Command to run' },
|
|
175
|
+
cwd: { type: 'string', description: 'Run this one command elsewhere; session cwd unchanged' },
|
|
186
176
|
timeout: { type: 'number', description: `Timeout in milliseconds (max ${EXEC_TIMEOUT_CAP}, default ${EXEC_TIMEOUT_DEFAULT})` },
|
|
187
177
|
description: { type: 'string', description: 'A brief description of what the command does (for display)' },
|
|
188
178
|
},
|
|
@@ -193,17 +183,12 @@ const SCHEMAS = {
|
|
|
193
183
|
task: {
|
|
194
184
|
name: 'task',
|
|
195
185
|
description:
|
|
196
|
-
'
|
|
197
|
-
'
|
|
198
|
-
'on the same model and returns a final report as the tool result. Use it to delegate work ' +
|
|
199
|
-
'like scanning for vulnerabilities, reviewing code, planning a refactor, or scaffolding a ' +
|
|
200
|
-
'component — give it a complete, self-contained prompt since it cannot ask follow-up ' +
|
|
201
|
-
'questions. Subagents can delegate further with task, so a large job can be split ' +
|
|
202
|
-
'hierarchically as deep as useful.',
|
|
186
|
+
'Delegate a focused multi-step sub-task to a subagent with its own tool loop; it returns a ' +
|
|
187
|
+
'final report. Give it a complete, self-contained prompt — it cannot ask follow-ups.',
|
|
203
188
|
parameters: {
|
|
204
189
|
type: 'object',
|
|
205
190
|
properties: {
|
|
206
|
-
description: { type: 'string', description: '
|
|
191
|
+
description: { type: 'string', description: 'Short label (3-5 words)' },
|
|
207
192
|
subagent_type: {
|
|
208
193
|
type: 'string',
|
|
209
194
|
enum: [...agentRoles(), 'general'],
|