aegiscode 6.0.0 → 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/src/commands.js CHANGED
@@ -1,84 +1,123 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Slash commands — the CLI's face on the shared tool registry.
4
+ * Slash commands — the CLI's face on the shared tool registry, using the
5
+ * command vocabulary of the sibling `aegiscodex-dev` client.
5
6
  *
6
- * Every command that talks to AEGIS names a tool from `mcp/tools.js`, and
7
- * `test/cli-tools.test.mjs` asserts both directions: no command points at a
8
- * tool that does not exist, and no tool is unreachable from the prompt. A new
9
- * capability added to the registry therefore shows up here or fails the build,
10
- * which is the failure mode we want (the alternative is a capability that
11
- * exists in the MCP host and is invisible in the terminal).
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.
12
27
  *
13
28
  * Plain text (no leading `/`) is a prompt: it goes to the pooled brain.
14
29
  */
15
30
 
16
31
  const COMMANDS = [
32
+ // ── AEGIS tool-backed family (aegiscodex-dev's `aegis` category) ──────────
17
33
  {
18
- name: 'ask',
19
- args: '<prompt>',
20
- help: 'Send a prompt to the AEGIS pool (same as typing it plainly).',
34
+ name: 'aegis-ask',
35
+ aliases: ['ask'],
36
+ args: '<question>',
37
+ category: 'aegis',
38
+ desc: 'Ask ÆGIS pooled inference a question (auto-routed)',
21
39
  tool: 'aegis_ask',
22
40
  build: (arg) => ({ prompt: arg }),
23
41
  },
24
42
  {
25
- name: 'status',
26
- help: 'Validate the API key; show plan, account and memory state.',
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',
27
51
  tool: 'aegis_status',
28
52
  build: () => ({}),
29
53
  },
30
54
  {
31
- name: 'models',
32
- aliases: ['model-list'],
33
- help: 'List the model ids you can pin with /model.',
34
- tool: 'aegis_list_models',
35
- build: () => ({}),
36
- },
37
- {
38
- name: 'balance',
39
- aliases: ['spend'], // not `cost` — that name is the local session tally below
40
- help: 'Token-bank balance and recent spend (tokens beside €).',
41
- tool: 'aegis_balance',
42
- build: () => ({}),
43
- },
44
- {
45
- name: 'recall',
46
- args: '<query>',
47
- help: 'Search your AEGIS cloud memory.',
55
+ name: 'aegis-recall',
56
+ aliases: ['recall'],
57
+ args: '<topic>',
58
+ category: 'aegis',
59
+ desc: 'Recall cross-session memory about a topic',
48
60
  tool: 'aegis_memory_search',
49
61
  build: (arg) => ({ query: arg }),
50
62
  },
51
63
  {
52
- name: 'remember',
53
- args: '<text>',
54
- help: 'Save a durable note to cloud memory.',
64
+ name: 'aegis-remember',
65
+ aliases: ['remember'],
66
+ args: '<note>',
67
+ category: 'aegis',
68
+ desc: 'Save a note or decision to cross-session memory',
55
69
  tool: 'aegis_memory_save',
56
70
  build: (arg) => ({ content: arg }),
57
71
  },
58
72
  {
59
73
  name: 'memory',
60
74
  aliases: ['memories'],
61
- help: 'List the most recent cloud-memory entries.',
75
+ args: '',
76
+ category: 'aegis',
77
+ desc: 'List the most recent AEGIS cloud-memory entries',
62
78
  tool: 'aegis_memory_list',
63
79
  build: () => ({}),
64
80
  },
65
81
  {
66
- name: 'import',
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'],
67
87
  args: '[--confirm]',
68
- help: 'Import memory from other tools on this machine (dry run unless --confirm).',
88
+ category: 'aegis',
89
+ desc: 'Import memory from other AI tools on this machine (dry run unless --confirm)',
69
90
  tool: 'aegis_memory_import',
70
91
  build: (arg) => ({ confirm: /--confirm\b/.test(arg) }),
71
92
  },
72
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.
73
109
  name: 'byok',
74
- help: 'Show which providers have your own key configured.',
110
+ args: '',
111
+ category: 'auth',
112
+ desc: 'Show which providers have your own key configured',
75
113
  tool: 'aegis_byok_status',
76
114
  build: () => ({}),
77
115
  },
78
116
  {
79
117
  name: 'byok-set',
80
118
  args: '<provider>',
81
- help: 'Set YOUR provider key (prompted, never echoed, never in history).',
119
+ category: 'auth',
120
+ desc: 'Set YOUR provider key (prompted, never echoed, never in history)',
82
121
  tool: 'aegis_byok_set',
83
122
  secret: 'key',
84
123
  build: (arg) => ({ provider: arg.trim() }),
@@ -86,14 +125,28 @@ const COMMANDS = [
86
125
  {
87
126
  name: 'byok-rm',
88
127
  args: '<provider>',
89
- help: 'Remove a stored provider key.',
128
+ category: 'auth',
129
+ desc: 'Remove a stored provider key',
90
130
  tool: 'aegis_byok_set',
91
131
  build: (arg) => ({ provider: arg.trim() }),
92
132
  },
93
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.
94
146
  name: 'tool',
95
147
  args: '<name> [json]',
96
- help: 'Call any registry tool directly (escape hatch for new tools).',
148
+ category: 'aegis',
149
+ desc: 'Call any registry tool directly (escape hatch for new tools)',
97
150
  generic: true,
98
151
  build: (arg) => {
99
152
  const sp = arg.indexOf(' ');
@@ -111,25 +164,65 @@ const COMMANDS = [
111
164
  },
112
165
  },
113
166
 
114
- // --- local commands: no server call, handled by app.js --------------------
167
+ // ── local commands: no server call, handled by app.js ─────────────────────
115
168
  {
116
169
  name: 'model',
170
+ aliases: ['m'],
117
171
  args: '[id]',
118
- help: 'Pin a model id (no argument shows the current pin and clears it with `-`).',
172
+ category: 'model',
173
+ desc: 'Switch AI model — pin an id (no argument shows the pin; `-` clears it)',
119
174
  local: 'model',
120
175
  },
121
- { name: 'stream', args: '[on|off]', help: 'Toggle streaming output.', local: 'stream' },
122
- { name: 'theme', args: '[dark|light]', help: 'Switch the colour theme.', local: 'theme' },
123
- { name: 'cost', aliases: ['usage'], help: 'This session: tokens, spend and call count.', local: 'cost' },
124
- { name: 'clear', aliases: ['cls'], help: 'Clear the screen.', local: 'clear' },
125
- { name: 'help', aliases: ['?', 'h'], help: 'Show this list.', local: 'help' },
126
- { name: 'quit', aliases: ['exit', 'q'], help: 'Exit.', local: 'quit' },
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' },
127
211
  ];
128
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
+
129
218
  const BY_NAME = new Map();
130
219
  for (const c of COMMANDS) {
131
- BY_NAME.set(c.name, c);
132
- for (const a of c.aliases || []) BY_NAME.set(a, c);
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
+ }
133
226
  }
134
227
 
135
228
  function findCommand(name) {
@@ -138,7 +231,11 @@ function findCommand(name) {
138
231
 
139
232
  /**
140
233
  * Classify one input line.
141
- * @returns {{kind:'empty'}|{kind:'command',command:object,arg:string}|{kind:'prompt',text:string}}
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}}
142
239
  */
143
240
  function parseLine(line) {
144
241
  const raw = String(line == null ? '' : line);
@@ -152,6 +249,9 @@ function parseLine(line) {
152
249
  if (!command) {
153
250
  return { kind: 'unknown', name, text: trimmed };
154
251
  }
252
+ if (command.unavailable) {
253
+ return { kind: 'unavailable', command, arg };
254
+ }
155
255
  return { kind: 'command', command, arg };
156
256
  }
157
257
 
package/src/fuzzy.js ADDED
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ // Fuzzy command matching for the "/" palette (and anything else that needs it).
4
+ //
5
+ // The real Claude Code palette ranks matches by relevance rather than plain
6
+ // prefix: an exact match beats a prefix, a prefix beats a substring, and a
7
+ // substring beats characters scattered through the name. Ties fall back to
8
+ // the source (registry) order so the palette stays stable for empty queries.
9
+ //
10
+ // Scoring tiers (deliberately spaced so no position/length detail in a lower
11
+ // tier can overtake a higher tier):
12
+ // exact match 1000
13
+ // prefix match 900
14
+ // substring 700 − position (earlier substring wins)
15
+ // scattered 50 + bonuses (consecutive runs, name-start hits)
16
+ //
17
+ // Ported verbatim from aegiscodex-dev/src/fuzzy.js (ESM → CommonJS). The tier
18
+ // numbers are a contract: cli-fuzzy.test.mjs asserts the ordering they encode.
19
+
20
+ function fuzzyScore(query, name) {
21
+ const q = String(query).toLowerCase();
22
+ const n = String(name).toLowerCase();
23
+ if (!q) return { matched: true, score: 0 };
24
+ if (n === q) return { matched: true, score: 1000 };
25
+ if (n.startsWith(q)) return { matched: true, score: 900 };
26
+ const at = n.indexOf(q);
27
+ if (at !== -1) return { matched: true, score: 700 - at };
28
+
29
+ // Scattered subsequence: every query char must appear in order.
30
+ let score = 50;
31
+ let prev = -2;
32
+ let last = -1;
33
+ for (const ch of q) {
34
+ const k = n.indexOf(ch, last + 1);
35
+ if (k === -1) return { matched: false, score: 0 };
36
+ if (k === prev + 1) score += 15; // consecutive run — strong signal
37
+ else score += 5;
38
+ if (k === 0) score += 10; // starts at the name — weak signal
39
+ prev = k;
40
+ last = k;
41
+ }
42
+ return { matched: true, score };
43
+ }
44
+
45
+ /**
46
+ * The indices (in the original `name`) of the characters that matched
47
+ * `query`, mirroring fuzzyScore's tiers — exact = every index, prefix =
48
+ * leading run, substring = that run, scattered = the scattered picks.
49
+ * Returns [] for an empty query and null when nothing matches.
50
+ * Drives the palette's bold-matched-chars rendering.
51
+ */
52
+ function fuzzyMatchPositions(query, name) {
53
+ const q = String(query).toLowerCase();
54
+ const n = String(name).toLowerCase();
55
+ if (!q) return [];
56
+ if (n === q) return Array.from({ length: q.length }, (_, i) => i);
57
+ if (n.startsWith(q)) return Array.from({ length: q.length }, (_, i) => i);
58
+ const at = n.indexOf(q);
59
+ if (at !== -1) return Array.from({ length: q.length }, (_, i) => at + i);
60
+ const pos = [];
61
+ let last = -1;
62
+ for (const ch of q) {
63
+ const k = n.indexOf(ch, last + 1);
64
+ if (k === -1) return null;
65
+ pos.push(k);
66
+ last = k;
67
+ }
68
+ return pos;
69
+ }
70
+
71
+ /**
72
+ * Rank `items` by how well `nameOf(item)` matches `query`.
73
+ * Empty query returns the items unchanged (preserves registry/palette order).
74
+ * Non-matches are dropped; matches sort by score desc, then source order.
75
+ */
76
+ function fuzzyRank(query, items, nameOf) {
77
+ const q = String(query || '');
78
+ if (!q) return items;
79
+ return items
80
+ .map((item, idx) => ({ item, idx, m: fuzzyScore(q, nameOf(item)) }))
81
+ .filter((x) => x.m.matched)
82
+ .sort((a, b) => b.m.score - a.m.score || a.idx - b.idx)
83
+ .map((x) => x.item);
84
+ }
85
+
86
+ /**
87
+ * Alias-aware ranking. The reference palette searches the command name and its
88
+ * aliases (name weight 3, aliases weight 2.5 — Fuse.js in the reference; here
89
+ * the best fuzzy score across both wins). An item matches when the query scores
90
+ * against at least one of its names; ties keep source order so the palette
91
+ * stays stable.
92
+ */
93
+ function fuzzyRankWithAliases(query, items, nameOf, aliasesOf) {
94
+ const q = String(query || '');
95
+ if (!q) return items;
96
+ return items
97
+ .map((item, idx) => {
98
+ const names = [nameOf(item), ...((aliasesOf && aliasesOf(item)) || [])];
99
+ let best = null;
100
+ for (const n of names) {
101
+ const m = fuzzyScore(q, n);
102
+ if (m.matched && (!best || m.score > best.score)) best = m;
103
+ }
104
+ return { item, idx, m: best };
105
+ })
106
+ .filter((x) => x.m)
107
+ .sort((a, b) => b.m.score - a.m.score || a.idx - b.idx)
108
+ .map((x) => x.item);
109
+ }
110
+
111
+ module.exports = {
112
+ fuzzyScore,
113
+ fuzzyMatchPositions,
114
+ fuzzyRank,
115
+ fuzzyRankWithAliases,
116
+ };