aegiscode 5.2.33 → 6.1.0
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/README.md +147 -392
- package/bin/aegiscode.js +197 -0
- package/package.json +30 -83
- package/scripts/demo.mjs +92 -0
- package/scripts/predist.mjs +86 -0
- package/src/app.js +619 -0
- package/src/art.js +235 -0
- package/src/commands.js +263 -0
- package/src/deps.js +65 -0
- package/src/format.js +54 -0
- package/src/fuzzy.js +116 -0
- package/src/markdown.js +303 -0
- package/src/overlays.js +286 -0
- package/src/render.js +396 -0
- package/src/screen.js +352 -0
- package/src/theme.js +156 -0
- package/vendor/client/aegis.js +809 -0
- package/vendor/client/foreign-memory.js +666 -0
- package/vendor/desktop/renderer/usage.js +42 -0
- package/vendor/mcp/tools.js +356 -0
- package/LICENSE +0 -21
- package/bin/cli.js +0 -2362
- package/scripts/download-binary.mjs +0 -155
- package/scripts/empty-stub.mjs +0 -1
- package/scripts/ensure-node-version.mjs +0 -27
- package/scripts/install-alacritty.ps1 +0 -149
- package/scripts/install-alacritty.sh +0 -164
- package/scripts/install.sh +0 -121
- package/scripts/make-bin.mjs +0 -17
- package/scripts/release-local.sh +0 -68
- package/scripts/repro-compact.ts +0 -59
- package/scripts/repro-tokencount.ts +0 -45
- package/scripts/repro-toolcontext.ts +0 -74
- package/scripts/sharp-stub/index.js +0 -9
- package/scripts/sharp-stub/package.json +0 -5
package/src/art.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Welcome art — the aegiscodex-dev mark, adopted wholesale.
|
|
5
|
+
*
|
|
6
|
+
* The composition is copied from `aegiscodex-dev/src/art.js`, which took the
|
|
7
|
+
* glyph rows from the shipped Claude Code binary: a feather plume (░), a top hat
|
|
8
|
+
* (█ ▓ ▒), the spark face (▐▛███▜▌ / ▝▜█████▛▘ / ▘▘ ▝▝), plus a crescent moon and
|
|
9
|
+
* a diving whale for the second brain. Side-by-side when the terminal is wide
|
|
10
|
+
* enough, stacked when it isn't.
|
|
11
|
+
*
|
|
12
|
+
* Every row of a block is the same display width, so `center()` and
|
|
13
|
+
* `joinSideBySide()` stay exact. `test/cli-conformance.test.mjs` asserts both
|
|
14
|
+
* that property and that these rows still match aegiscodex-dev's source, so the
|
|
15
|
+
* mark can't drift.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// The mascot face — exact glyphs from the binary (msS template).
|
|
19
|
+
const FACE = [' ▐▛███▜▌', '▝▜█████▛▘', ' ▘▘ ▝▝'];
|
|
20
|
+
|
|
21
|
+
// Top hat + feather plume, composed from the extracted row glyphs.
|
|
22
|
+
const HAT = [
|
|
23
|
+
' ✦ ░░░░░░░░░░ ✦',
|
|
24
|
+
' ░░░░░░░░░░░░░░░░',
|
|
25
|
+
' █████▓▓░',
|
|
26
|
+
' ███▓░',
|
|
27
|
+
' ███▓░',
|
|
28
|
+
' ███▓░',
|
|
29
|
+
' ░▓▓███▓▓░',
|
|
30
|
+
' ▒▒░░▒▒ ▒ ▒▒',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Plume + hat + face: the left half of the welcome mark.
|
|
34
|
+
const MASCOT_ART = [
|
|
35
|
+
' ✦ █████▓▓░',
|
|
36
|
+
' ✦ ███▓░ ░░',
|
|
37
|
+
' ░░░░ ███▓░',
|
|
38
|
+
' ░░░░░░░░ ███▓░',
|
|
39
|
+
' ░░░░░░░░░░░░ ███▓░',
|
|
40
|
+
' ██▓░░ ▓',
|
|
41
|
+
' ░▓▓███▓▓░',
|
|
42
|
+
...FACE,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Crescent moon in a starry night sky — lit rim (▓) with a faint earthshine
|
|
46
|
+
// body (░). Rows are equal code-point length (13).
|
|
47
|
+
const MOON = [
|
|
48
|
+
' ✦ ▓▓▓ ✦ ',
|
|
49
|
+
' ▓▓▓▓▓ ✦',
|
|
50
|
+
' ▓▓▓▓▓▓▓▓▓ ✦',
|
|
51
|
+
' ▓▓▓▓▓▓▓░░░ ',
|
|
52
|
+
' ▓▓▓▓░░░░░░ ',
|
|
53
|
+
' ░░░░░░ ',
|
|
54
|
+
' ✦ · · ',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// The diving whale: spout (░), eye (██) on the rounded head, body (▒) sweeping
|
|
58
|
+
// right to a small forked fluke (▓), water line (░) beneath. Rows are equal
|
|
59
|
+
// code-point length (18).
|
|
60
|
+
const WHALE = [
|
|
61
|
+
' ✦ ░ ░ ░ ',
|
|
62
|
+
' ✦ ░░░░░ ',
|
|
63
|
+
' ▒▒▒▒▒▒▒▒▒▒▒▒ ',
|
|
64
|
+
' ▒▒▒▒▒▒▒▒▒▒▒▒▒ ',
|
|
65
|
+
' ██▒▒▒▒▒▒▒▒▒▒▒▒▓▓',
|
|
66
|
+
' ██▒▒▒▒▒▒▒▒▒▒▒▒▓ ',
|
|
67
|
+
' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ',
|
|
68
|
+
' ░░░░░░░░░░░░ ',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Moon above the whale — the right half of the mark, 2-col gutter. The moon is
|
|
72
|
+
// padded with a trailing blank row so joinSideBySide keeps both blocks aligned.
|
|
73
|
+
const MOON_WHALE = joinSideBySide([...MOON, ''], WHALE, 2);
|
|
74
|
+
|
|
75
|
+
/** Full welcome mark: mascot + (moon + whale) side by side, bottom-aligned. */
|
|
76
|
+
const WELCOME_ART = joinSideBySide(MASCOT_ART, MOON_WHALE, 2);
|
|
77
|
+
|
|
78
|
+
/** Narrow-terminal fallback: mascot stacked above (moon + whale). */
|
|
79
|
+
const WELCOME_ART_STACKED = stackVertical(MASCOT_ART, MOON_WHALE, 1);
|
|
80
|
+
|
|
81
|
+
/** Welcome copy, in aegiscode-dev's frame: coral title over the mark. */
|
|
82
|
+
const WELCOME_TITLE = 'Welcome to AEGIS Code';
|
|
83
|
+
const WELCOME_BACK = 'Welcome back!';
|
|
84
|
+
const TAGLINE = 'Cloud brain in your shell.';
|
|
85
|
+
|
|
86
|
+
// ── Terminal portability ─────────────────────────────────────────────────────
|
|
87
|
+
// The block-glyph palette and ✦ stars are single-width on Linux terminals, but
|
|
88
|
+
// not everywhere: Windows fonts often lack the U+259B..U+259F face edges and
|
|
89
|
+
// mangle █▓▒░ entirely; on macOS ✦ renders as a double-width emoji. Each pass
|
|
90
|
+
// swaps offending code points 1:1 (same character count per row) so the width
|
|
91
|
+
// maths stays exact and the mark keeps its shape.
|
|
92
|
+
const ASCII_STENCIL = new Map([
|
|
93
|
+
['█', '#'],
|
|
94
|
+
['▓', '@'],
|
|
95
|
+
['▒', '='],
|
|
96
|
+
['░', '.'],
|
|
97
|
+
['▐', '#'],
|
|
98
|
+
['▛', '#'],
|
|
99
|
+
['▜', '#'],
|
|
100
|
+
['▌', '#'],
|
|
101
|
+
['▝', '#'],
|
|
102
|
+
['▘', '#'],
|
|
103
|
+
['✦', '*'],
|
|
104
|
+
['·', '.'],
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const DARWIN_STENCIL = new Map([
|
|
108
|
+
['▐', '#'],
|
|
109
|
+
['▛', '#'],
|
|
110
|
+
['▜', '#'],
|
|
111
|
+
['▌', '#'],
|
|
112
|
+
['▝', '#'],
|
|
113
|
+
['▘', '#'],
|
|
114
|
+
['✦', '*'],
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
/** One platform pass over an art block. */
|
|
118
|
+
function portability(rows) {
|
|
119
|
+
if (process.platform === 'win32') {
|
|
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;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The mark that fits the current terminal: side-by-side when there's room. */
|
|
129
|
+
function welcomeArtFor(cols) {
|
|
130
|
+
const wide = portability(WELCOME_ART);
|
|
131
|
+
const stacked = portability(WELCOME_ART_STACKED);
|
|
132
|
+
const W = Math.max(...wide.map((r) => [...r].length));
|
|
133
|
+
return cols >= W + 4 ? wide : stacked;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Split the mark into its two halves so each can carry its own theme ink.
|
|
138
|
+
* @returns {{rows: Array<[string,string]>, width: number, gutter: number}}
|
|
139
|
+
*/
|
|
140
|
+
function welcomeArtParts(cols) {
|
|
141
|
+
const mascot = portability(MASCOT_ART);
|
|
142
|
+
const moonWhale = portability(MOON_WHALE);
|
|
143
|
+
const L = Math.max(...mascot.map((r) => [...r].length));
|
|
144
|
+
const R = Math.max(...moonWhale.map((r) => [...r].length));
|
|
145
|
+
const wide = cols >= L + R + 2 + 4;
|
|
146
|
+
const gutter = wide ? 2 : 1;
|
|
147
|
+
if (wide) {
|
|
148
|
+
// The right half carries the moon *and* the whale. Using WHALE alone here —
|
|
149
|
+
// which the reference does, because it only wants the per-mascot ink split —
|
|
150
|
+
// would silently drop the crescent from the welcome screen this CLI renders.
|
|
151
|
+
const n = Math.max(mascot.length, moonWhale.length);
|
|
152
|
+
const rows = [];
|
|
153
|
+
for (let i = 0; i < n; i++) {
|
|
154
|
+
const li = i + (n - mascot.length);
|
|
155
|
+
const ri = i + (n - moonWhale.length);
|
|
156
|
+
rows.push([(mascot[li] ?? '').padEnd(L), (moonWhale[ri] ?? '').padEnd(R)]);
|
|
157
|
+
}
|
|
158
|
+
return { rows, width: L + gutter + R, gutter };
|
|
159
|
+
}
|
|
160
|
+
const w = Math.max(L, R);
|
|
161
|
+
const centerBlock = (rows) =>
|
|
162
|
+
rows.map((r) => ' '.repeat(Math.max(0, Math.floor((w - [...r].length) / 2))) + r);
|
|
163
|
+
return {
|
|
164
|
+
rows: [
|
|
165
|
+
...centerBlock(mascot).map((c) => [c, '']),
|
|
166
|
+
['', ''],
|
|
167
|
+
...centerBlock(moonWhale).map((r) => ['', r]),
|
|
168
|
+
],
|
|
169
|
+
width: w,
|
|
170
|
+
gutter,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Stack two art blocks vertically, each centred, separated by `gap` rows. */
|
|
175
|
+
function stackVertical(top, bottom, gap = 1) {
|
|
176
|
+
const w = Math.max(...[...top, ...bottom].map((r) => [...r].length));
|
|
177
|
+
const centerBlock = (rows) =>
|
|
178
|
+
rows.map((r) => {
|
|
179
|
+
const pad = Math.max(0, Math.floor((w - [...r].length) / 2));
|
|
180
|
+
return ' '.repeat(pad) + r;
|
|
181
|
+
});
|
|
182
|
+
return [...centerBlock(top), ...Array(gap).fill(''), ...centerBlock(bottom)];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Join two art stacks row-aligned (bottom-aligned by default). */
|
|
186
|
+
function joinSideBySide(left, right, gutter = 2, align = 'bottom') {
|
|
187
|
+
const L = Math.max(...left.map((r) => [...r].length));
|
|
188
|
+
const R = Math.max(...right.map((r) => [...r].length));
|
|
189
|
+
const n = Math.max(left.length, right.length);
|
|
190
|
+
return Array.from({ length: n }, (_, i) => {
|
|
191
|
+
const li = align === 'bottom' ? i + (n - left.length) : i;
|
|
192
|
+
const ri = align === 'bottom' ? i + (n - right.length) : i;
|
|
193
|
+
const l = (left[li] ?? '').padEnd(L);
|
|
194
|
+
const r = (right[ri] ?? '').padEnd(R);
|
|
195
|
+
return l + ' '.repeat(gutter) + r;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Centre every row of an art block inside `width` (left pad + right fill). */
|
|
200
|
+
function center(rows, width) {
|
|
201
|
+
return rows.map((r) => {
|
|
202
|
+
const pad = Math.max(0, Math.floor((width - [...r].length) / 2));
|
|
203
|
+
return ' '.repeat(pad) + r + ' '.repeat(Math.max(0, width - [...r].length - pad));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Centre each row against the block's own max width (no trailing fill). */
|
|
208
|
+
function padBoth(rows, width) {
|
|
209
|
+
const w = Math.max(...rows.map((r) => [...r].length));
|
|
210
|
+
return rows.map((r) => {
|
|
211
|
+
const pad = Math.floor((width - w) / 2);
|
|
212
|
+
return ' '.repeat(Math.max(0, pad)) + r;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
FACE,
|
|
218
|
+
HAT,
|
|
219
|
+
MASCOT_ART,
|
|
220
|
+
MOON,
|
|
221
|
+
WHALE,
|
|
222
|
+
MOON_WHALE,
|
|
223
|
+
WELCOME_ART,
|
|
224
|
+
WELCOME_ART_STACKED,
|
|
225
|
+
WELCOME_TITLE,
|
|
226
|
+
WELCOME_BACK,
|
|
227
|
+
TAGLINE,
|
|
228
|
+
portability,
|
|
229
|
+
welcomeArtFor,
|
|
230
|
+
welcomeArtParts,
|
|
231
|
+
stackVertical,
|
|
232
|
+
joinSideBySide,
|
|
233
|
+
center,
|
|
234
|
+
padBoth,
|
|
235
|
+
};
|
package/src/commands.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Slash commands — the CLI's face on the shared tool registry, using the
|
|
5
|
+
* command vocabulary of the sibling `aegiscodex-dev` client.
|
|
6
|
+
*
|
|
7
|
+
* Three kinds of entry live in one table:
|
|
8
|
+
*
|
|
9
|
+
* (a) tool-backed — names a tool from `mcp/tools.js` and carries a
|
|
10
|
+
* `build(arg) -> object` that turns the typed argument into the tool's
|
|
11
|
+
* JSON. `test/cli-tools.test.mjs` asserts both directions: no command
|
|
12
|
+
* points at a tool that does not exist, and no tool in the registry is
|
|
13
|
+
* unreachable from the prompt, so a new capability added to the registry
|
|
14
|
+
* shows up here or fails the build.
|
|
15
|
+
* (b) local — no server call; handled in `app.js` by `cmd.local`.
|
|
16
|
+
* (c) unavailable — a `aegiscodex-dev` command whose capability this client
|
|
17
|
+
* genuinely does not have (it needs a local agent loop, Claude Code auth,
|
|
18
|
+
* or a repo tool). These carry an honest `why` and are *not* fakes:
|
|
19
|
+
* `parseLine` returns `{ kind: 'unavailable' }` and `app.js` prints the
|
|
20
|
+
* reason plus a working alternative.
|
|
21
|
+
*
|
|
22
|
+
* The names, aliases, categories and descriptions come from
|
|
23
|
+
* `aegiscodex-dev/src/registry.js` (`COMMANDS`, `CATEGORIES`). Where our
|
|
24
|
+
* capability differs from the reference's (e.g. `memory` here lists *cloud*
|
|
25
|
+
* memory, not the reference's local tier store) the description says what this
|
|
26
|
+
* client actually does rather than copying a sentence that would be untrue.
|
|
27
|
+
*
|
|
28
|
+
* Plain text (no leading `/`) is a prompt: it goes to the pooled brain.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const COMMANDS = [
|
|
32
|
+
// ── AEGIS tool-backed family (aegiscodex-dev's `aegis` category) ──────────
|
|
33
|
+
{
|
|
34
|
+
name: 'aegis-ask',
|
|
35
|
+
aliases: ['ask'],
|
|
36
|
+
args: '<question>',
|
|
37
|
+
category: 'aegis',
|
|
38
|
+
desc: 'Ask ÆGIS pooled inference a question (auto-routed)',
|
|
39
|
+
tool: 'aegis_ask',
|
|
40
|
+
build: (arg) => ({ prompt: arg }),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
// aegiscodex-dev splits this into `aegis-status` (local memory stats) and
|
|
44
|
+
// `status` (session status); this client has one account-status tool, so
|
|
45
|
+
// both spellings route to it — `status`/`st` as aliases.
|
|
46
|
+
name: 'aegis-status',
|
|
47
|
+
aliases: ['status', 'st'],
|
|
48
|
+
args: '',
|
|
49
|
+
category: 'aegis',
|
|
50
|
+
desc: 'Show account status — API key, plan, account and cloud memory',
|
|
51
|
+
tool: 'aegis_status',
|
|
52
|
+
build: () => ({}),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'aegis-recall',
|
|
56
|
+
aliases: ['recall'],
|
|
57
|
+
args: '<topic>',
|
|
58
|
+
category: 'aegis',
|
|
59
|
+
desc: 'Recall cross-session memory about a topic',
|
|
60
|
+
tool: 'aegis_memory_search',
|
|
61
|
+
build: (arg) => ({ query: arg }),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'aegis-remember',
|
|
65
|
+
aliases: ['remember'],
|
|
66
|
+
args: '<note>',
|
|
67
|
+
category: 'aegis',
|
|
68
|
+
desc: 'Save a note or decision to cross-session memory',
|
|
69
|
+
tool: 'aegis_memory_save',
|
|
70
|
+
build: (arg) => ({ content: arg }),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'memory',
|
|
74
|
+
aliases: ['memories'],
|
|
75
|
+
args: '',
|
|
76
|
+
category: 'aegis',
|
|
77
|
+
desc: 'List the most recent AEGIS cloud-memory entries',
|
|
78
|
+
tool: 'aegis_memory_list',
|
|
79
|
+
build: () => ({}),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
// No reference equivalent (the reference has no importer); the name follows
|
|
83
|
+
// the `/aegis-*` family. Keeps the `--confirm` dry-run semantics: without
|
|
84
|
+
// the flag the tool only reports what it found.
|
|
85
|
+
name: 'aegis-import',
|
|
86
|
+
aliases: ['import'],
|
|
87
|
+
args: '[--confirm]',
|
|
88
|
+
category: 'aegis',
|
|
89
|
+
desc: 'Import memory from other AI tools on this machine (dry run unless --confirm)',
|
|
90
|
+
tool: 'aegis_memory_import',
|
|
91
|
+
build: (arg) => ({ confirm: /--confirm\b/.test(arg) }),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
// aegiscodex-dev's `model` switches the brain; this client pins a model id
|
|
95
|
+
// (local) and lists the pinnable ids with a tool. `models` is the list
|
|
96
|
+
// action — aegiscodex-dev's `/model list`, surfaced as its own command.
|
|
97
|
+
name: 'models',
|
|
98
|
+
aliases: ['model-list'],
|
|
99
|
+
args: '',
|
|
100
|
+
category: 'model',
|
|
101
|
+
desc: 'List the model ids you can pin with /model',
|
|
102
|
+
tool: 'aegis_list_models',
|
|
103
|
+
build: () => ({}),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
// aegiscodex-dev has no BYOK surface; these are carried over from the
|
|
107
|
+
// registry's byok tools. `billing` below is the reference's name for the
|
|
108
|
+
// balance read the tool performs.
|
|
109
|
+
name: 'byok',
|
|
110
|
+
args: '',
|
|
111
|
+
category: 'auth',
|
|
112
|
+
desc: 'Show which providers have your own key configured',
|
|
113
|
+
tool: 'aegis_byok_status',
|
|
114
|
+
build: () => ({}),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'byok-set',
|
|
118
|
+
args: '<provider>',
|
|
119
|
+
category: 'auth',
|
|
120
|
+
desc: 'Set YOUR provider key (prompted, never echoed, never in history)',
|
|
121
|
+
tool: 'aegis_byok_set',
|
|
122
|
+
secret: 'key',
|
|
123
|
+
build: (arg) => ({ provider: arg.trim() }),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'byok-rm',
|
|
127
|
+
args: '<provider>',
|
|
128
|
+
category: 'auth',
|
|
129
|
+
desc: 'Remove a stored provider key',
|
|
130
|
+
tool: 'aegis_byok_set',
|
|
131
|
+
build: (arg) => ({ provider: arg.trim() }),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
// aegiscodex-dev's `billing`; `balance` and `spend` stay routable for
|
|
135
|
+
// muscle memory. Not `cost` — that name is this client's local tally below.
|
|
136
|
+
name: 'billing',
|
|
137
|
+
aliases: ['balance', 'spend'],
|
|
138
|
+
args: '',
|
|
139
|
+
category: 'support',
|
|
140
|
+
desc: 'Show billing info — token-bank balance and recent spend',
|
|
141
|
+
tool: 'aegis_balance',
|
|
142
|
+
build: () => ({}),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
// The escape hatch for any registry tool, including ones added later.
|
|
146
|
+
name: 'tool',
|
|
147
|
+
args: '<name> [json]',
|
|
148
|
+
category: 'aegis',
|
|
149
|
+
desc: 'Call any registry tool directly (escape hatch for new tools)',
|
|
150
|
+
generic: true,
|
|
151
|
+
build: (arg) => {
|
|
152
|
+
const sp = arg.indexOf(' ');
|
|
153
|
+
const name = (sp === -1 ? arg : arg.slice(0, sp)).trim();
|
|
154
|
+
const rest = sp === -1 ? '' : arg.slice(sp + 1).trim();
|
|
155
|
+
let args = {};
|
|
156
|
+
if (rest) {
|
|
157
|
+
try {
|
|
158
|
+
args = JSON.parse(rest);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
throw new Error(`/tool: arguments must be JSON — ${err.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { tool: name, args };
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// ── local commands: no server call, handled by app.js ─────────────────────
|
|
168
|
+
{
|
|
169
|
+
name: 'model',
|
|
170
|
+
aliases: ['m'],
|
|
171
|
+
args: '[id]',
|
|
172
|
+
category: 'model',
|
|
173
|
+
desc: 'Switch AI model — pin an id (no argument shows the pin; `-` clears it)',
|
|
174
|
+
local: 'model',
|
|
175
|
+
},
|
|
176
|
+
{ name: 'stream', args: '[on|off]', category: 'model', desc: 'Toggle streaming output', local: 'stream' },
|
|
177
|
+
{ name: 'theme', aliases: ['t'], args: '[dark|light]', category: 'model', desc: 'Change the color theme', local: 'theme' },
|
|
178
|
+
{ name: 'version', aliases: ['v'], args: '', category: 'session', desc: 'Show version', local: 'version' },
|
|
179
|
+
{ name: 'cost', args: '', category: 'data', desc: 'Show the cost of the current session', local: 'cost' },
|
|
180
|
+
{
|
|
181
|
+
name: 'tokens',
|
|
182
|
+
aliases: ['tok'],
|
|
183
|
+
args: '',
|
|
184
|
+
category: 'data',
|
|
185
|
+
desc: 'Show token usage breakdown and estimated spend',
|
|
186
|
+
local: 'tokens',
|
|
187
|
+
},
|
|
188
|
+
{ name: 'clear', aliases: ['cls'], args: '', category: 'session', desc: 'Start a new session with empty context', local: 'clear' },
|
|
189
|
+
{ name: 'help', aliases: ['?', 'h'], args: '', category: 'support', desc: 'Show help', local: 'help' },
|
|
190
|
+
{ name: 'exit', aliases: ['quit'], args: '', category: 'session', desc: 'Exit the CLI', local: 'exit' },
|
|
191
|
+
|
|
192
|
+
// ── unavailable: aegiscodex-dev vocabulary this client cannot honour ───────
|
|
193
|
+
// Not fakes — each names why it is absent and, where one exists, the nearest
|
|
194
|
+
// working command. `parseLine` surfaces these as `kind: 'unavailable'`.
|
|
195
|
+
{ name: 'login', args: '', category: 'auth', desc: 'Sign in to Claude Code', unavailable: true, why: 'sign-in uses Claude Code auth, which this client does not have — it authenticates with your AEGIS key', alt: '/byok-set' },
|
|
196
|
+
{ name: 'logout', args: '', category: 'auth', desc: 'Sign out', unavailable: true, why: 'there is no Claude Code sign-in here to end' },
|
|
197
|
+
{ name: 'doctor', args: '', category: 'support', desc: 'Run diagnostic checks on this environment', unavailable: true, why: 'the diagnostic suite belongs to aegiscodex-dev, not this client', alt: '/status' },
|
|
198
|
+
{ name: 'permissions', args: '[mode] [pattern]', category: 'model', desc: 'Set permissions for tool use', unavailable: true, why: 'tool-permission prompts require a local agent loop this client does not run' },
|
|
199
|
+
{ name: 'mcp', args: '[sub]', category: 'model', desc: 'Show MCP server configuration', unavailable: true, why: 'this CLI is itself an MCP host, so it has no MCP servers to configure', alt: '/tool' },
|
|
200
|
+
{ name: 'skills', aliases: ['sk'], args: '[name|refresh]', category: 'session', desc: 'List skills (SKILL.md in the standard skill dirs)', unavailable: true, why: 'skills run inside the agent loop, which this client does not host' },
|
|
201
|
+
{ name: 'hooks', args: '[status|list]', category: 'model', desc: 'View hooks configuration status and configured hook list', unavailable: true, why: 'hooks are a local agent-loop feature this client does not run' },
|
|
202
|
+
{ name: 'agents', aliases: ['sessions'], args: '[role] [task]', category: 'session', desc: 'Show the agents panel, or run a sub-agent role preset — /agents <role> <task>', unavailable: true, why: 'sub-agents need a local agent loop this client does not host' },
|
|
203
|
+
{ name: 'resume', args: '', category: 'session', desc: 'Switch to a previous session', unavailable: true, why: 'this client keeps no session history to resume' },
|
|
204
|
+
{ name: 'rewind', args: '[N]', category: 'session', desc: 'Revert the conversation to a checkpoint', unavailable: true, why: 'checkpoints belong to a session history this client does not keep' },
|
|
205
|
+
{ name: 'compact', args: '', category: 'session', desc: 'Compact the conversation history', unavailable: true, why: 'there is no local transcript to compact' },
|
|
206
|
+
{ name: 'init', args: '[file]', category: 'workspace', desc: 'Create a CLAUDE.md file in the project', unavailable: true, why: 'project scaffolding is a workspace feature this client does not have', alt: '/aegis-remember' },
|
|
207
|
+
{ name: 'export', args: '[markdown|json] [file|clipboard]', category: 'data', desc: 'Export the current conversation to a file or clipboard', unavailable: true, why: 'this client keeps no transcript to export' },
|
|
208
|
+
{ name: 'vim', args: '[on|off]', category: 'model', desc: 'Toggle vim keymap', unavailable: true, why: 'input is your terminal readline, which has no vim keymap' },
|
|
209
|
+
{ name: 'yolo', args: '[on|off]', category: 'model', desc: 'Toggle YOLO mode — auto-approve all tool executions', unavailable: true, why: 'there is no tool-approval prompt here to auto-approve' },
|
|
210
|
+
{ name: 'confirm', aliases: ['confirmations'], args: '[on|off]', category: 'model', desc: 'Toggle tool-call confirmation prompts', unavailable: true, why: 'there is no tool-approval prompt here to toggle' },
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
// ── Index + the one invariant a table like this must hold ────────────────────
|
|
214
|
+
// A name or alias registered twice would silently shadow the earlier entry, so
|
|
215
|
+
// the collision is a module-load error rather than a runtime surprise. (The
|
|
216
|
+
// reference keeps `builtinNames()` for exactly this; its test asserts the same.)
|
|
217
|
+
|
|
218
|
+
const BY_NAME = new Map();
|
|
219
|
+
for (const c of COMMANDS) {
|
|
220
|
+
for (const n of [c.name, ...(c.aliases || [])]) {
|
|
221
|
+
if (BY_NAME.has(n)) {
|
|
222
|
+
throw new Error(`aegiscode: command name or alias /${n} is registered twice`);
|
|
223
|
+
}
|
|
224
|
+
BY_NAME.set(n, c);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function findCommand(name) {
|
|
229
|
+
return BY_NAME.get(String(name || '').toLowerCase()) || null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Classify one input line.
|
|
234
|
+
* @returns {{kind:'empty'}
|
|
235
|
+
* |{kind:'command',command:object,arg:string}
|
|
236
|
+
* |{kind:'unavailable',command:object,arg:string}
|
|
237
|
+
* |{kind:'unknown',name:string,text:string}
|
|
238
|
+
* |{kind:'prompt',text:string}}
|
|
239
|
+
*/
|
|
240
|
+
function parseLine(line) {
|
|
241
|
+
const raw = String(line == null ? '' : line);
|
|
242
|
+
const trimmed = raw.trim();
|
|
243
|
+
if (!trimmed) return { kind: 'empty' };
|
|
244
|
+
if (trimmed[0] !== '/') return { kind: 'prompt', text: trimmed };
|
|
245
|
+
const sp = trimmed.indexOf(' ');
|
|
246
|
+
const name = (sp === -1 ? trimmed.slice(1) : trimmed.slice(1, sp)).trim();
|
|
247
|
+
const arg = sp === -1 ? '' : trimmed.slice(sp + 1).trim();
|
|
248
|
+
const command = findCommand(name);
|
|
249
|
+
if (!command) {
|
|
250
|
+
return { kind: 'unknown', name, text: trimmed };
|
|
251
|
+
}
|
|
252
|
+
if (command.unavailable) {
|
|
253
|
+
return { kind: 'unavailable', command, arg };
|
|
254
|
+
}
|
|
255
|
+
return { kind: 'command', command, arg };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Commands that map to a registry tool, for the coverage test. */
|
|
259
|
+
function toolBackedCommands() {
|
|
260
|
+
return COMMANDS.filter((c) => c.tool);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = { COMMANDS, findCommand, parseLine, toolBackedCommands };
|
package/src/deps.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where the CLI finds the modules it shares with the rest of the repo.
|
|
5
|
+
*
|
|
6
|
+
* Two layouts must work:
|
|
7
|
+
*
|
|
8
|
+
* in-repo <repo>/cli/src/deps.js -> <repo>/client/aegis.js, <repo>/mcp/tools.js
|
|
9
|
+
* npm <pkg>/src/deps.js -> <pkg>/vendor/{client,mcp,desktop}/...
|
|
10
|
+
*
|
|
11
|
+
* The published package is staged by `scripts/predist.mjs` into `cli/vendor/`
|
|
12
|
+
* keeping the repo's own relative shape, so nothing here (or in the copied
|
|
13
|
+
* files) needs a path rewrite — `vendor/mcp/tools.js` requiring
|
|
14
|
+
* `../client/foreign-memory.js` resolves inside the vendor tree by construction.
|
|
15
|
+
*
|
|
16
|
+
* Resolution is by existence, and a missing module is a loud error rather than
|
|
17
|
+
* a silent fallback to a second copy: duplicate transports are how the two
|
|
18
|
+
* hosts would drift.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
|
|
24
|
+
function roots() {
|
|
25
|
+
const srcDir = __dirname; // <root>/cli/src
|
|
26
|
+
const cliDir = path.join(srcDir, '..');
|
|
27
|
+
return [
|
|
28
|
+
path.join(srcDir, '..', '..'), // repo root (in-repo layout)
|
|
29
|
+
path.join(cliDir, 'vendor'), // staged tree (published layout)
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolve a repo-relative module path against whichever root has it. */
|
|
34
|
+
function resolveShared(relPath) {
|
|
35
|
+
for (const root of roots()) {
|
|
36
|
+
const candidate = path.join(root, relPath);
|
|
37
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
38
|
+
}
|
|
39
|
+
throw new Error(
|
|
40
|
+
`aegiscode: cannot find ${relPath}. Expected it beside cli/ (in the repo) or ` +
|
|
41
|
+
'under cli/vendor/ (installed package). Reinstall the package, or run ' +
|
|
42
|
+
'`node scripts/predist.mjs` from cli/ if this is a source checkout.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const clientPath = resolveShared(path.join('client', 'aegis.js'));
|
|
47
|
+
const toolsPath = resolveShared(path.join('mcp', 'tools.js'));
|
|
48
|
+
// The desktop's pure usage module is the single source of truth for turning a
|
|
49
|
+
// provider's usage object into a display number. The CLI reuses the same file
|
|
50
|
+
// instead of re-implementing the mapping, so the terminal and the GUI can never
|
|
51
|
+
// disagree about what a call consumed (test/cli-tools.test.mjs asserts the
|
|
52
|
+
// function identity).
|
|
53
|
+
const usagePath = resolveShared(path.join('desktop', 'renderer', 'usage.js'));
|
|
54
|
+
|
|
55
|
+
const { createClient } = require(clientPath);
|
|
56
|
+
const { createTools } = require(toolsPath);
|
|
57
|
+
const { usageTokens } = require(usagePath);
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
createClient,
|
|
61
|
+
createTools,
|
|
62
|
+
usageTokens,
|
|
63
|
+
paths: { client: clientPath, tools: toolsPath, usage: usagePath },
|
|
64
|
+
roots,
|
|
65
|
+
};
|
package/src/format.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Number/text formatting for the CLI — one place, so the transcript, the status
|
|
5
|
+
* bar and the headless `--print` output always agree.
|
|
6
|
+
*
|
|
7
|
+
* The sub-cent rule mirrors the server-side ledger: AEGIS pooled calls settle
|
|
8
|
+
* around €0.0007, so a 2dp rendering shows a real charge as "€0.00" next to its
|
|
9
|
+
* token count and reads as free usage.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Group digits: 1562 -> "1,562". */
|
|
13
|
+
function fmtTokens(n) {
|
|
14
|
+
const v = Number(n);
|
|
15
|
+
if (!Number.isFinite(v) || v <= 0) return '0';
|
|
16
|
+
return Math.round(v)
|
|
17
|
+
.toString()
|
|
18
|
+
.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** € amount, 4dp below a cent (per-call spend) and 2dp above it (top-ups). */
|
|
22
|
+
function fmtEur(n) {
|
|
23
|
+
const v = Number(n);
|
|
24
|
+
if (!Number.isFinite(v)) return '€?';
|
|
25
|
+
const abs = Math.abs(v);
|
|
26
|
+
return `€${abs < 0.01 ? abs.toFixed(4) : abs.toFixed(2)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Signed € amount, from the user's side: -€0.0007 spent, +€5.00 added. */
|
|
30
|
+
function fmtEurSigned(n) {
|
|
31
|
+
const v = Number(n);
|
|
32
|
+
if (!Number.isFinite(v)) return '€?';
|
|
33
|
+
return `${v < 0 ? '-' : '+'}${fmtEur(Math.abs(v))}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Compact duration: 850 -> "0.9s", 65000 -> "1m05s". */
|
|
37
|
+
function fmtElapsed(ms) {
|
|
38
|
+
const s = Math.max(0, Number(ms) || 0) / 1000;
|
|
39
|
+
if (s < 10) return `${s.toFixed(1)}s`;
|
|
40
|
+
if (s < 60) return `${Math.round(s)}s`;
|
|
41
|
+
const m = Math.floor(s / 60);
|
|
42
|
+
const rest = Math.round(s - m * 60);
|
|
43
|
+
return `${m}m${String(rest).padStart(2, '0')}s`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Mask an API key for display: never echo enough to be useful if leaked. */
|
|
47
|
+
function maskKey(key) {
|
|
48
|
+
const s = String(key || '');
|
|
49
|
+
if (!s) return 'not set';
|
|
50
|
+
if (s.length <= 8) return '•'.repeat(s.length);
|
|
51
|
+
return `${s.slice(0, 6)}${'•'.repeat(4)}${s.slice(-4)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { fmtTokens, fmtEur, fmtEurSigned, fmtElapsed, maskKey };
|