aegiscode 5.2.32 → 6.0.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.
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Pure token-usage → display-number mapping.
5
+ *
6
+ * Standalone from app.js (same reason as max-tokens.js/stream-policy.js): it is
7
+ * requireable from a plain Node test without window.aegis. app.js only calls
8
+ * into it.
9
+ *
10
+ * Why this exists at all: the two wire formats spell the same quantity
11
+ * differently, and the renderer used to read exactly one of the spellings.
12
+ *
13
+ * OpenAI-compatible { prompt_tokens, completion_tokens, total_tokens }
14
+ * Anthropic-compatible { input_tokens, output_tokens } ← no total
15
+ *
16
+ * Reading only `total_tokens` — the field OpenAI happens to provide — meant
17
+ * every Anthropic-compatible model rendered with no token count at all, while
18
+ * the call was silently being billed. Accepting both spellings, and deriving
19
+ * the total when the provider doesn't state one, is what makes the spend
20
+ * visible regardless of which endpoint answered.
21
+ */
22
+
23
+ /**
24
+ * Number of tokens to show for a completed turn, or `null` when the provider
25
+ * reported none (an unknown count must render as nothing, never as `0`).
26
+ *
27
+ * @param {{total_tokens?: number, prompt_tokens?: number, completion_tokens?: number,
28
+ * input_tokens?: number, output_tokens?: number}|null|undefined} usage
29
+ * @returns {number|null}
30
+ */
31
+ function usageTokens(usage) {
32
+ if (!usage || typeof usage !== 'object') return null;
33
+ if (typeof usage.total_tokens === 'number') return usage.total_tokens;
34
+ const input = usage.input_tokens ?? usage.prompt_tokens;
35
+ const output = usage.output_tokens ?? usage.completion_tokens;
36
+ if (typeof input !== 'number' && typeof output !== 'number') return null;
37
+ return (input || 0) + (output || 0);
38
+ }
39
+
40
+ if (typeof module !== 'undefined' && module.exports) {
41
+ module.exports = { usageTokens };
42
+ }
@@ -0,0 +1,356 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * AEGIS tool registry — the one definition of what the AEGIS surface can do.
5
+ *
6
+ * Both hosts build from this file: `mcp/server.js` (the MCP host) and `cli/`
7
+ * (the terminal host). A capability can therefore never exist in one host and
8
+ * be silently missing from the other, and the JSON schemas the model sees are
9
+ * literally the same objects the CLI's /commands dispatch against.
10
+ *
11
+ * This module is pure: names, schemas, and the text each tool returns. It holds
12
+ * no JSON-RPC, no ANSI, and no terminal state. The transport is injected rather
13
+ * than created here, so each host passes its own configured client — and a test
14
+ * can pass a stub.
15
+ *
16
+ * The tool bodies are indented one level deeper than they were when they lived
17
+ * in mcp/server.js; the shift is whitespace only (no template literal in the
18
+ * block spans a line).
19
+ */
20
+
21
+ const foreignMemory = require('../client/foreign-memory.js');
22
+
23
+ /**
24
+ * Build the registry against a client instance.
25
+ *
26
+ * @param {object} client A client from client/aegis.js (or a stub with the same
27
+ * method surface). Required — a host owns its own key handling, so this
28
+ * module never reads the environment itself.
29
+ * @returns {{TOOLS: object, toolList: () => Array<{name: string, description: string, inputSchema: object}>}}
30
+ */
31
+ function createTools(client) {
32
+ if (!client || typeof client !== 'object') {
33
+ throw new TypeError('createTools(client): a client instance is required');
34
+ }
35
+
36
+ // The moved bodies call through this name, so the registry cannot reach any
37
+ // other module state.
38
+ const aegis = client;
39
+
40
+ const TOOLS = {
41
+ aegis_status: {
42
+ description:
43
+ "Check the current AEGIS account: validates the API key and reports the plan, email, and whether cloud memory is enabled. Call this first to confirm setup.",
44
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
45
+ async run() {
46
+ const info = await aegis.verifyApiKey();
47
+ const lines = [
48
+ `Key valid: ${info.valid ? 'yes' : 'no'}`,
49
+ `Plan: ${info.plan || 'unknown'}`,
50
+ info.email ? `Account: ${info.email}` : null,
51
+ `Memory: ${info.memory_token ? 'enabled (cloud sync available)' : 'not enabled'}`,
52
+ `API base: ${aegis.apiBase}`,
53
+ ].filter(Boolean);
54
+ return lines.join('\n');
55
+ },
56
+ },
57
+
58
+ aegis_ask: {
59
+ description:
60
+ "Send a prompt to AEGIS pooled inference, billed against the account's token bank. List models with aegis_list_models and pass any id as `model`; omit `model` to let the server pick its default. `mode` is a legacy server-side shorthand and is never required.",
61
+ inputSchema: {
62
+ type: 'object',
63
+ properties: {
64
+ prompt: { type: 'string', description: 'The user prompt / question.' },
65
+ model: {
66
+ type: 'string',
67
+ description:
68
+ 'Pin any model id from aegis_list_models — or omit for the server default.',
69
+ },
70
+ mode: {
71
+ type: 'string',
72
+ description:
73
+ "Legacy server-side shorthand (e.g. 'fast', 'smart', 'neo'). Optional — never defaulted client-side.",
74
+ },
75
+ system: { type: 'string', description: 'Optional system instruction.' },
76
+ max_tokens: {
77
+ type: 'integer',
78
+ description:
79
+ 'Max output tokens. The server enforces the true ceiling; omit for the server default.',
80
+ minimum: 1,
81
+ maximum: 64000,
82
+ },
83
+ },
84
+ required: ['prompt'],
85
+ additionalProperties: false,
86
+ },
87
+ async run(args) {
88
+ // Use the OpenAI-compatible endpoint (stream:false): it returns structured
89
+ // JSON errors (e.g. 402 insufficient_quota) instead of the opaque 502 that
90
+ // /api/v1/complete emits. An exact `model` id pins that provider directly;
91
+ // omitting `model` lets the server route to its default.
92
+ const data = await aegis.chatCompletion({
93
+ prompt: args.prompt,
94
+ system: args.system,
95
+ model: args.model,
96
+ mode: args.mode,
97
+ maxTokens: args.max_tokens,
98
+ });
99
+ const choice = (data.choices && data.choices[0]) || {};
100
+ const text = (choice.message && choice.message.content) || '(empty response)';
101
+ const meta = [
102
+ data.model ? `model: ${data.model}` : null,
103
+ data.usage ? `tokens: ${data.usage.total_tokens}` : null,
104
+ ]
105
+ .filter(Boolean)
106
+ .join(' · ');
107
+ return `${text}\n\n— ${meta}`;
108
+ },
109
+ },
110
+
111
+ aegis_list_models: {
112
+ description:
113
+ "List the exact models available to pin with aegis_ask's `model` argument — the server's model list is the truth, and omitting `model` uses the server default.",
114
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
115
+ async run() {
116
+ const data = await aegis.listModels();
117
+ const models = data.models || [];
118
+ if (!models.length) return 'No pinnable models are currently configured on AEGIS.';
119
+ return [
120
+ 'Pass one of these as aegis_ask\'s `model` argument:',
121
+ '',
122
+ ...models.map((m) => ` ${m.id} (${(m.capabilities || []).join(', ')})`),
123
+ ].join('\n');
124
+ },
125
+ },
126
+
127
+ aegis_balance: {
128
+ description:
129
+ "Check the AEGIS token bank balance and recent spend. Call this before aegis_ask if you're unsure whether the account has funds, or to see what recent calls cost.",
130
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
131
+ async run() {
132
+ const data = await aegis.tokenBankBalance();
133
+ const lines = [`Balance: €${Number(data.balance_eur || 0).toFixed(2)}`];
134
+ const ledger = (data.ledger || []).slice(0, 5);
135
+ if (ledger.length) {
136
+ lines.push('', 'Recent activity:');
137
+ for (const l of ledger) {
138
+ // The balance endpoint returns `amount_eur`, signed from the user's
139
+ // side: negative = spent on a call, positive = top-up/rebate. This
140
+ // used to read `charged_micros`, which the endpoint does not return
141
+ // at all — every row rendered as "-€0.0000" no matter how many
142
+ // tokens the call consumed. Only fall back to the raw micros column
143
+ // (opposite sign) if a future server drops amount_eur.
144
+ const eur = l.amount_eur != null
145
+ ? Number(l.amount_eur)
146
+ : -Number(l.charged_micros || 0) / 1_000_000;
147
+ // Sub-cent calls are the norm here, so keep 4dp below a cent and
148
+ // fall back to 2dp for cash-sized rows.
149
+ const abs = Math.abs(eur);
150
+ const amount = `${eur < 0 ? '-' : '+'}€${abs < 0.01 ? abs.toFixed(4) : abs.toFixed(2)}`;
151
+ const kind = l.kind === 'topup' ? 'top-up' : (l.kind || '');
152
+ const model = l.model_key || kind || '';
153
+ const tokens = (l.tokens_in || l.tokens_out)
154
+ ? ` ${l.tokens_in || 0}/${l.tokens_out || 0} tok`
155
+ : '';
156
+ lines.push(` ${l.created_at || ''} ${model}${tokens} ${amount}`);
157
+ }
158
+ } else {
159
+ lines.push('No spend yet.');
160
+ }
161
+ if ((data.balance_eur || 0) <= 0) {
162
+ lines.push(
163
+ '',
164
+ 'Balance is empty — top up at https://aegiscloud.org/subscribe, or set your own ' +
165
+ 'provider key with aegis_byok_set to use aegis_ask for free at cost.'
166
+ );
167
+ }
168
+ return lines.join('\n');
169
+ },
170
+ },
171
+
172
+ aegis_byok_status: {
173
+ description:
174
+ "List which providers have a Bring-Your-Own-Key configured on this AEGIS account (known ids: openai, anthropic, groq, openrouter, together, deepseek, gemini, …). A configured BYOK key is used automatically by aegis_ask instead of the pooled balance for that provider, so calls no longer cost AEGIS token-bank funds.",
175
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
176
+ async run() {
177
+ const data = await aegis.byokStatus();
178
+ const rows = Object.entries(data || {}).map(([provider, info]) => {
179
+ const set = info && info.set;
180
+ return ` ${provider}: ${set ? `set (${info.masked})` : 'not set'}`;
181
+ });
182
+ return `BYOK keys:\n${rows.join('\n')}`;
183
+ },
184
+ },
185
+
186
+ aegis_byok_set: {
187
+ description:
188
+ "Set or remove a Bring-Your-Own-Key API key for a provider on this AEGIS account (known ids: openai, anthropic, groq, openrouter, together, deepseek, gemini, …; the server validates what it supports). Once set, aegis_ask uses that key directly for that provider instead of the pooled AEGIS balance — unlimited use at the user's own cost. Omit api_key to remove a previously set key.",
189
+ inputSchema: {
190
+ type: 'object',
191
+ properties: {
192
+ provider: {
193
+ type: 'string',
194
+ description:
195
+ "Which provider this key is for (known ids: openai, anthropic, groq, openrouter, together, deepseek, gemini, …).",
196
+ },
197
+ api_key: { type: 'string', description: "The provider's own API key. Omit to delete the stored key instead." },
198
+ },
199
+ required: ['provider'],
200
+ additionalProperties: false,
201
+ },
202
+ async run(args) {
203
+ const data = await aegis.byokSet(args.provider, args.api_key);
204
+ return data.message || (args.api_key ? `${args.provider} key saved.` : `${args.provider} key removed.`);
205
+ },
206
+ },
207
+
208
+ aegis_memory_search: {
209
+ description:
210
+ "Search the user's AEGIS cloud memory (persists across machines and sessions). Returns the most relevant stored entries for a query.",
211
+ inputSchema: {
212
+ type: 'object',
213
+ properties: {
214
+ query: { type: 'string', description: 'Search text. Empty returns most recent.' },
215
+ limit: { type: 'integer', description: 'Max entries (1-50). Default 5.', minimum: 1, maximum: 50 },
216
+ },
217
+ required: ['query'],
218
+ additionalProperties: false,
219
+ },
220
+ async run(args) {
221
+ const data = await aegis.memorySearch(args.query, args.limit || 5);
222
+ const entries = data.entries || [];
223
+ if (!entries.length) return `No memory entries matched "${args.query}".`;
224
+ return entries
225
+ .map((e, i) => {
226
+ const when = e.timestamp || e.createdAt || '';
227
+ const tags = (e.tags && e.tags.length) ? ` [${e.tags.join(', ')}]` : '';
228
+ return `${i + 1}. ${e.content}${tags}${when ? `\n (${when})` : ''}`;
229
+ })
230
+ .join('\n');
231
+ },
232
+ },
233
+
234
+ aegis_memory_save: {
235
+ description:
236
+ "Save a note to the user's AEGIS cloud memory so it persists across machines and future sessions. Use for durable facts, decisions, or preferences the user asks you to remember.",
237
+ inputSchema: {
238
+ type: 'object',
239
+ properties: {
240
+ content: { type: 'string', description: 'The fact/note to store.' },
241
+ tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags.' },
242
+ importance: { type: 'integer', description: 'Optional 1-10 salience. Default 5.', minimum: 1, maximum: 10 },
243
+ session: { type: 'string', description: 'Optional session/label to group under.' },
244
+ },
245
+ required: ['content'],
246
+ additionalProperties: false,
247
+ },
248
+ async run(args) {
249
+ const entry = {
250
+ id: aegis.randomUUID(),
251
+ content: args.content,
252
+ role: 'assistant',
253
+ source: 'claude-code',
254
+ session: args.session || 'claude-code',
255
+ tags: args.tags || [],
256
+ importance: typeof args.importance === 'number' ? args.importance : 5,
257
+ timestamp: new Date().toISOString(),
258
+ };
259
+ const data = await aegis.memorySave(entry);
260
+ return `Saved ${data.saved || 1} memory entry (id ${entry.id}).`;
261
+ },
262
+ },
263
+
264
+ aegis_memory_import: {
265
+ description:
266
+ "Import memory and past conversation context from OTHER AI coding tools installed on this machine (Claude Code, Codex, Cursor, Gemini CLI, Continue, Goose, opencode, Windsurf, Zed, and a local AEGIS engine) into the user's AEGIS cloud memory. Scans read-only; nothing in the other tools' directories is modified. Defaults to a DRY RUN that only reports what it found — pass confirm:true to actually save. Entries are content-addressed, so re-running never duplicates.",
267
+ inputSchema: {
268
+ type: 'object',
269
+ properties: {
270
+ confirm: {
271
+ type: 'boolean',
272
+ description: 'false (default) = report only. true = actually write the entries to AEGIS memory.',
273
+ },
274
+ sources: {
275
+ type: 'array',
276
+ items: { type: 'string' },
277
+ description:
278
+ 'Optional subset of source ids to import (e.g. ["claude-code"]). Omit for every source found on this machine.',
279
+ },
280
+ limit: { type: 'integer', description: 'Max entries to import (1-5000). Default 1000.', minimum: 1, maximum: 5000 },
281
+ },
282
+ additionalProperties: false,
283
+ },
284
+ async run(args) {
285
+ const limit = args.limit || 1000;
286
+ const report = foreignMemory.scan({ sources: args.sources, limit });
287
+ const summary = foreignMemory.describe(report);
288
+
289
+ if (!report.entries.length) {
290
+ return `${summary}\n\nNothing to import.`;
291
+ }
292
+
293
+ if (!args.confirm) {
294
+ return [
295
+ summary,
296
+ '',
297
+ `Dry run — ${report.totals.entries} entries would be saved to AEGIS memory under ${report.totals.sourcesWithEntries} session(s) (${report.totals.skipped} skipped as too short/noise).`,
298
+ 'Call this tool again with confirm:true to save them.',
299
+ ].join('\n');
300
+ }
301
+
302
+ let saved = 0;
303
+ const failures = [];
304
+ for (const batch of foreignMemory.chunk(report.entries, 200)) {
305
+ try {
306
+ const data = await aegis.memorySaveBatch(batch);
307
+ saved += (data && data.saved) || batch.length;
308
+ } catch (err) {
309
+ // A 402 here means the free-tier session cap; surface it verbatim
310
+ // rather than pretending part of the import worked.
311
+ failures.push(err.message);
312
+ break;
313
+ }
314
+ }
315
+
316
+ if (failures.length) {
317
+ return `${summary}\n\nSaved ${saved} of ${report.totals.entries} entries, then stopped: ${failures[0]}`;
318
+ }
319
+ return `${summary}\n\nSaved ${saved} entries to AEGIS memory. They are searchable now with aegis_memory_search.`;
320
+ },
321
+ },
322
+
323
+ aegis_memory_list: {
324
+ description:
325
+ "List the most recent entries in the user's AEGIS cloud memory. Useful to review what AEGIS already remembers.",
326
+ inputSchema: {
327
+ type: 'object',
328
+ properties: {
329
+ limit: { type: 'integer', description: 'Max entries (1-50). Default 10.', minimum: 1, maximum: 50 },
330
+ },
331
+ additionalProperties: false,
332
+ },
333
+ async run(args) {
334
+ const data = await aegis.memoryList(args.limit || 10);
335
+ const entries = data.entries || [];
336
+ if (!entries.length) return 'AEGIS cloud memory is empty.';
337
+ const body = entries.map((e, i) => `${i + 1}. ${e.content}`).join('\n');
338
+ return `Recent ${entries.length} entries:\n${body}`;
339
+ },
340
+ },
341
+ };
342
+
343
+
344
+ /** The MCP `tools/list` payload — and the CLI's command index. */
345
+ function toolList() {
346
+ return Object.entries(TOOLS).map(([name, t]) => ({
347
+ name,
348
+ description: t.description,
349
+ inputSchema: t.inputSchema,
350
+ }));
351
+ }
352
+
353
+ return { TOOLS, toolList };
354
+ }
355
+
356
+ module.exports = { createTools };
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Niklas Borneklint
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.