aegiscode 6.1.1 → 6.2.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 +121 -78
- package/bin/aegiscode.js +9 -1
- package/package.json +3 -3
- package/scripts/predist.mjs +11 -1
- package/src/agents.js +136 -0
- package/src/app.js +520 -162
- package/src/chatflow.js +1475 -0
- package/src/checkpoint.js +85 -0
- package/src/clipboard.js +62 -0
- package/src/commands.js +1234 -150
- package/src/config.js +163 -0
- package/src/deps.js +14 -1
- package/src/devrun.js +110 -0
- package/src/engine.js +62 -0
- package/src/events.js +278 -0
- package/src/export.js +64 -0
- package/src/history.js +201 -0
- package/src/init.js +162 -0
- package/src/input.js +136 -0
- package/src/keys.js +141 -0
- package/src/panels.js +1171 -0
- package/src/permissions.js +102 -0
- package/src/render.js +33 -1
- package/src/summarize.js +90 -0
- package/src/system.js +37 -0
- package/src/tokens.js +166 -0
- package/vendor/desktop/lib/local/agents.js +102 -0
- package/vendor/desktop/lib/local/engine.js +972 -0
- package/vendor/desktop/lib/local/prompt.js +91 -0
- package/vendor/desktop/lib/local/shell.js +208 -0
- package/vendor/desktop/lib/local/tools.js +882 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Real enforcement for the /permissions rules (src/config.js) in the
|
|
5
|
+
* direct-provider tool loop. The `claude` CLI path already has its own real
|
|
6
|
+
* approval UX from the real binary, so this only applies to
|
|
7
|
+
* Bash/Read/Write/Edit/Glob/Grep run by a provider model.
|
|
8
|
+
*
|
|
9
|
+
* Scope decision (Phase 12): the multi-directory-change Bash heuristic,
|
|
10
|
+
* explicit /permissions allow|deny|ask rules, and `defaultMode: 'ask'` when
|
|
11
|
+
* the permissions file *explicitly* writes it (rules.explicitAsk) are
|
|
12
|
+
* enforced here. The file-free default ('ask' in DEFAULT_PERMISSIONS) is
|
|
13
|
+
* NOT consulted — honoring the implicit default would prompt on every tool
|
|
14
|
+
* call out of the box and make the agentic loop unusable by default.
|
|
15
|
+
* /confirm and /yolo still toggle defaultMode for the explicit case.
|
|
16
|
+
*
|
|
17
|
+
* Not tools.js's Glob-tool globToRegex: that one is filesystem-glob (`*`
|
|
18
|
+
* stops at `/`, matching one path segment) because it walks a directory
|
|
19
|
+
* tree. Rule subjects here are just as often a Bash command string, where
|
|
20
|
+
* `/` is ordinary text ("rm -rf /tmp/x") — a `Bash(npm run *)` rule (see
|
|
21
|
+
* DEFAULT_PERMISSIONS's own example) needs `*` to match the rest of the
|
|
22
|
+
* line, slashes included. So `*` and `?` here are plain "match anything"
|
|
23
|
+
* wildcards, not path-segment-bounded ones; reimplemented rather than
|
|
24
|
+
* imported so this module has no import-cycle with tools.js (executeTool's
|
|
25
|
+
* home, which calls into this one).
|
|
26
|
+
*
|
|
27
|
+
* Ported from aegiscodex-dev/src/permissions.js (ESM → CommonJS).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const RULE_RE = /^(\w+)(?:\((.*)\))?$/;
|
|
31
|
+
|
|
32
|
+
function globToRegex(pattern) {
|
|
33
|
+
let re = '^';
|
|
34
|
+
for (const c of pattern) {
|
|
35
|
+
if (c === '*') re += '.*';
|
|
36
|
+
else if (c === '?') re += '.';
|
|
37
|
+
else if (/[.+^${}()|[\]\\]/.test(c)) re += `\\${c}`;
|
|
38
|
+
else re += c;
|
|
39
|
+
}
|
|
40
|
+
return new RegExp(re + '$');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The subject a rule's pattern matches against, per tool. */
|
|
44
|
+
function subjectFor(toolName, args = {}) {
|
|
45
|
+
if (toolName === 'Bash') return String(args.command || '');
|
|
46
|
+
if (toolName === 'Read' || toolName === 'Write' || toolName === 'Edit') return String(args.file_path || '');
|
|
47
|
+
if (toolName === 'Glob' || toolName === 'Grep') return String(args.pattern || '');
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function matchRule(rule, toolName, subject) {
|
|
52
|
+
const m = RULE_RE.exec(String(rule || '').trim());
|
|
53
|
+
if (!m) return false;
|
|
54
|
+
const [, tool, pattern] = m;
|
|
55
|
+
if (tool !== toolName) return false;
|
|
56
|
+
if (pattern === undefined) return true; // bare "Bash" matches every call
|
|
57
|
+
try { return globToRegex(pattern).test(subject); } catch { return false; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const matchesAny = (list, toolName, subject) =>
|
|
61
|
+
Array.isArray(list) && list.some((r) => matchRule(r, toolName, subject));
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A Bash command that changes directory more than once needs a second look:
|
|
65
|
+
* each `&&`/`;`/`|`/newline-separated segment starting with `cd` counts.
|
|
66
|
+
* Mirrors the captured Claude Code 2.1.228 heuristic ("Multiple directory
|
|
67
|
+
* changes in one command require approval for clarity") — a hardcoded UX
|
|
68
|
+
* safety rail, not a configurable rule.
|
|
69
|
+
*/
|
|
70
|
+
function isMultiDirCommand(command) {
|
|
71
|
+
const segments = String(command || '').split(/&&|\|\||;|\n/).map((s) => s.trim());
|
|
72
|
+
const cdCount = segments.filter((s) => /^cd(\s|$)/.test(s)).length;
|
|
73
|
+
return cdCount > 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Evaluate a tool call against the persisted /permissions rules.
|
|
78
|
+
* Returns 'allow' | 'deny' | 'ask'. Precedence: an explicit deny rule always
|
|
79
|
+
* wins (a security floor). An explicit allow rule is the real "always
|
|
80
|
+
* allow" escape hatch, so it's checked next and suppresses the multi-cd
|
|
81
|
+
* heuristic below it — same as upstream, where an always-allow rule for
|
|
82
|
+
* Bash stops it asking again. With no allow rule, the multi-cd heuristic
|
|
83
|
+
* forces 'ask' on its own; then explicit ask rules. Anything left
|
|
84
|
+
* unmatched falls through to rules.explicitAsk (an explicitly written
|
|
85
|
+
* `defaultMode: 'ask'` prompts on the residual); otherwise 'allow' — see
|
|
86
|
+
* the scope note above, the implicit defaultMode is intentionally not
|
|
87
|
+
* consulted.
|
|
88
|
+
*/
|
|
89
|
+
function evalPermission(toolName, args, rules = {}) {
|
|
90
|
+
const subject = subjectFor(toolName, args);
|
|
91
|
+
if (matchesAny(rules.deny, toolName, subject)) return 'deny';
|
|
92
|
+
if (matchesAny(rules.allow, toolName, subject)) return 'allow';
|
|
93
|
+
if (toolName === 'Bash' && isMultiDirCommand(args && args.command)) return 'ask';
|
|
94
|
+
if (matchesAny(rules.ask, toolName, subject)) return 'ask';
|
|
95
|
+
if (rules.explicitAsk && rules.defaultMode === 'ask') return 'ask';
|
|
96
|
+
return 'allow';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
isMultiDirCommand,
|
|
101
|
+
evalPermission,
|
|
102
|
+
};
|
package/src/render.js
CHANGED
|
@@ -245,7 +245,11 @@ function renderTurn(ctx, turn, width = 80) {
|
|
|
245
245
|
if (turn.streaming) body[body.length - 1] += `${t.white}${GLYPH.block}${RESET}`;
|
|
246
246
|
lines.push(...body);
|
|
247
247
|
} else if (role === 'tool') {
|
|
248
|
-
|
|
248
|
+
// turn.ok is only known once the call has actually run (the agent loop's
|
|
249
|
+
// tool-activity event fires after execution); undefined means "no result
|
|
250
|
+
// yet to report" (the plain registry-tool path, which never set it).
|
|
251
|
+
const status = turn.ok === undefined ? '' : turn.ok ? ` ${t.green}✓${RESET}` : ` ${t.red}${ERR}${RESET}`;
|
|
252
|
+
lines.push(`${t.white}${GLYPH.block}${RESET} ${t.gray}${turn.label || 'tool'}${RESET}${status}`);
|
|
249
253
|
const args =
|
|
250
254
|
turn.args == null
|
|
251
255
|
? ''
|
|
@@ -357,6 +361,33 @@ function renderToolResult(ctx, name, text, width = 80) {
|
|
|
357
361
|
return lines;
|
|
358
362
|
}
|
|
359
363
|
|
|
364
|
+
/**
|
|
365
|
+
* The tool-approval card: the mutating call the model wants to run (a diff
|
|
366
|
+
* for writeFile/editFile, the command for exec) and the three answers. One
|
|
367
|
+
* inline question, matching aegiscodex-dev's Bash/edit approval dialog.
|
|
368
|
+
*/
|
|
369
|
+
function renderApproval(ctx, info = {}, width = 80) {
|
|
370
|
+
const t = themeOf(ctx);
|
|
371
|
+
const lines = [renderHeading(ctx, `confirm ${info.tool || 'tool'}`, width)];
|
|
372
|
+
const body = info.diff
|
|
373
|
+
? info.diff
|
|
374
|
+
: info.args == null
|
|
375
|
+
? ''
|
|
376
|
+
: typeof info.args === 'string'
|
|
377
|
+
? info.args
|
|
378
|
+
: JSON.stringify(info.args);
|
|
379
|
+
for (const l of wrapBlock(body, Math.max(8, width - 2))) {
|
|
380
|
+
lines.push(l ? ` ${t.gray}${l}${RESET}` : '');
|
|
381
|
+
}
|
|
382
|
+
lines.push('');
|
|
383
|
+
lines.push(
|
|
384
|
+
` ${t.coral}${GLYPH.bullet}${RESET} ${t.white}y${RESET}es once` +
|
|
385
|
+
` ${t.dim}${GLYPH.bullet}${RESET} ${t.white}s${RESET}ession` +
|
|
386
|
+
` ${t.dim}${GLYPH.bullet}${RESET} ${t.white}n${RESET}o ${t.dim}(default)${RESET}`
|
|
387
|
+
);
|
|
388
|
+
return lines;
|
|
389
|
+
}
|
|
390
|
+
|
|
360
391
|
/** A transient notice. Kinds map gray / coral / red / green with the matching
|
|
361
392
|
* glyph: info `·`, warn `⚠`, error `✗`, ok `✔`. */
|
|
362
393
|
const NOTICE = {
|
|
@@ -381,6 +412,7 @@ module.exports = {
|
|
|
381
412
|
renderWorking,
|
|
382
413
|
renderHeading,
|
|
383
414
|
renderToolResult,
|
|
415
|
+
renderApproval,
|
|
384
416
|
renderNotice,
|
|
385
417
|
mdLines,
|
|
386
418
|
inline,
|
package/src/summarize.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transcript summarization for /compact and /recap.
|
|
5
|
+
*
|
|
6
|
+
* Dependency-free port of aegiscodex-dev/src/summarize.js. The reference
|
|
7
|
+
* spawns `claude -p "Summarize…"` (via platform.resolveExecutable); here the
|
|
8
|
+
* backend is *injected* instead, so this module pulls in nothing but tokens.js:
|
|
9
|
+
*
|
|
10
|
+
* async summarizeTranscript(transcript, { callModel, model, signal } = {})
|
|
11
|
+
* async recapLine(transcript, { callModel, model, signal } = {})
|
|
12
|
+
*
|
|
13
|
+
* `callModel(prompt) -> string` (async) is the caller's own model seam. When
|
|
14
|
+
* `callModel` is not a function the module falls back to a purely local
|
|
15
|
+
* extraction — the first user text, the last assistant text and the turn count
|
|
16
|
+
* — and never throws. On abort (signal.aborted) it resolves null so handlers
|
|
17
|
+
* can report "cancelled"; on any other backend failure it resolves the local
|
|
18
|
+
* extraction.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { estimateTokens } = require('./tokens.js');
|
|
22
|
+
|
|
23
|
+
// Guard: don't push more than this many chars into a prompt.
|
|
24
|
+
const MAX_INPUT_CHARS = 120_000;
|
|
25
|
+
const MAX_REPLY_CHARS = 2_000;
|
|
26
|
+
|
|
27
|
+
function extractiveSummary(transcript) {
|
|
28
|
+
const users = transcript.filter((m) => m.role === 'user').map((m) => m.text || '');
|
|
29
|
+
const lastAsst = [...transcript].reverse().find((m) => m.role === 'assistant');
|
|
30
|
+
const topics = users.slice(0, 3).map((t) => `“${t.length > 48 ? t.slice(0, 45) + '…' : t}”`);
|
|
31
|
+
const parts = [];
|
|
32
|
+
if (users.length) parts.push(`Covered ${users.length} prompt${users.length > 1 ? 's' : ''}: ${topics.join('; ')}.`);
|
|
33
|
+
if (lastAsst) {
|
|
34
|
+
const tail = (lastAsst.text || '').split(/\s+/).slice(0, 30).join(' ');
|
|
35
|
+
parts.push(`Last reply began: ${tail.length > 100 ? tail.slice(0, 97) + '…' : tail}`);
|
|
36
|
+
}
|
|
37
|
+
if (!parts.length) return 'Session with no exchanges yet.';
|
|
38
|
+
return parts.join(' ');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function demoSummary(transcript) {
|
|
42
|
+
return `[demo summary] ${extractiveSummary(transcript)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Summarize the whole transcript in a few sentences. Resolves null on abort. */
|
|
46
|
+
async function summarizeTranscript(transcript, { callModel, model, signal } = {}) {
|
|
47
|
+
if (!transcript || !transcript.length) return 'Session with no exchanges yet.';
|
|
48
|
+
if (typeof callModel !== 'function') return demoSummary(transcript);
|
|
49
|
+
return callBackend('summarize', transcript, model, signal, callModel, () => demoSummary(transcript));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A single-line recap. Resolves null on abort. */
|
|
53
|
+
async function recapLine(transcript, { callModel, model, signal } = {}) {
|
|
54
|
+
if (!transcript || !transcript.length) return 'No exchanges yet.';
|
|
55
|
+
if (typeof callModel !== 'function') return `[demo recap] ${extractiveSummary(transcript)}`;
|
|
56
|
+
return callBackend('recap', transcript, model, signal, callModel, () => `[demo recap] ${extractiveSummary(transcript)}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function callBackend(kind, transcript, model, signal, callModel, fallback) {
|
|
60
|
+
const text = transcriptToText(transcript).slice(0, MAX_INPUT_CHARS);
|
|
61
|
+
// NB: the ternary binds exactly as in the reference — the recap branch does
|
|
62
|
+
// not append the transcript text. Ported verbatim.
|
|
63
|
+
const prompt = kind === 'recap'
|
|
64
|
+
? 'Recap the following conversation in one sentence. Be factual and specific.'
|
|
65
|
+
: 'Summarize the following conversation. Preserve every requirement, decision, and open question, in 2-4 sentences.'
|
|
66
|
+
+ '\n\n' + text;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
if (signal && signal.aborted) return null;
|
|
70
|
+
const out = await callModel(prompt, { model, signal });
|
|
71
|
+
if (signal && signal.aborted) return null;
|
|
72
|
+
if (typeof out === 'string' && out.trim()) return out.trim().slice(0, MAX_REPLY_CHARS);
|
|
73
|
+
return fallback();
|
|
74
|
+
} catch {
|
|
75
|
+
return fallback();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function transcriptToText(transcript) {
|
|
80
|
+
return transcript
|
|
81
|
+
.filter((m) => m.role === 'user' || m.role === 'assistant' || m.role === 'note')
|
|
82
|
+
.map((m) => `${m.role}: ${m.text || ''}`)
|
|
83
|
+
.join('\n\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
summarizeTranscript,
|
|
88
|
+
recapLine,
|
|
89
|
+
estimateTokens,
|
|
90
|
+
};
|
package/src/system.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Small system helpers: best-effort URL opening (never blocks or errors) and
|
|
5
|
+
* a few constants shared by the support/easter-egg commands.
|
|
6
|
+
*
|
|
7
|
+
* Ported from aegiscodex-dev/src/system.js (ESM → CommonJS). The URLS table
|
|
8
|
+
* pointed at Anthropic/Claude support endpoints; those are re-homed to the
|
|
9
|
+
* AEGIS base (https://aegiscloud.org) and the project repo. Deep paths that
|
|
10
|
+
* had no AEGIS equivalent collapse to the base rather than inventing a route.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { spawn } = require('node:child_process');
|
|
14
|
+
|
|
15
|
+
/** Best-effort open of a URL in the system browser. Returns true if launched. */
|
|
16
|
+
function openUrl(url) {
|
|
17
|
+
try {
|
|
18
|
+
const platform = process.platform;
|
|
19
|
+
const cmd = platform === 'darwin' ? ['open'] : platform === 'win32' ? ['cmd', '/c', 'start', ''] : ['xdg-open'];
|
|
20
|
+
const child = spawn(cmd[0], [...cmd.slice(1), url], { stdio: 'ignore', detached: true });
|
|
21
|
+
child.unref();
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// AEGIS support URLs (used by /troubleshooting, /feedback…).
|
|
29
|
+
const URLS = {
|
|
30
|
+
troubleshooting: 'https://aegiscloud.org',
|
|
31
|
+
feedback: 'https://aegiscloud.org',
|
|
32
|
+
issues: 'https://github.com/aegisinfo/aegiscode-plugin/issues/new',
|
|
33
|
+
docs: 'https://aegiscloud.org',
|
|
34
|
+
radio: 'https://www.youtube.com/watch?v=cP8jB6YQWbQ',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
module.exports = { openUrl, URLS };
|
package/src/tokens.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Token + cost estimation. Tokens are estimated from text length (~4
|
|
5
|
+
* chars/token, the usual rule of thumb) and priced against the per-model rates
|
|
6
|
+
* below. Everything is labeled approximate in the UI.
|
|
7
|
+
*
|
|
8
|
+
* Ported from aegiscodex-dev/src/tokens.js (ESM → CommonJS).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Per-million-token USD rates. Cache-read/write matter for long sessions.
|
|
12
|
+
const RATES = {
|
|
13
|
+
sonnet: { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },
|
|
14
|
+
default: { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },
|
|
15
|
+
fable: { input: 5.00, output: 25.00, cacheRead: 0.50, cacheWrite: 6.25 },
|
|
16
|
+
opus: { input: 5.00, output: 25.00, cacheRead: 0.50, cacheWrite: 6.25 },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// System prompt + tool definitions overhead, roughly, in tokens.
|
|
20
|
+
const SYSTEM_TOKENS = 18000;
|
|
21
|
+
const TOOL_TOKENS = 12000;
|
|
22
|
+
// Default context window we budget against (Sonnet 5 class).
|
|
23
|
+
const CONTEXT_WINDOW = 200000;
|
|
24
|
+
|
|
25
|
+
// Real per-provider context budgets for the /context meter. The default above
|
|
26
|
+
// is the Claude-class window the UI was built around; providers with a
|
|
27
|
+
// different window map here so the meter shows the truth instead of a
|
|
28
|
+
// Sonnet-flavored estimate.
|
|
29
|
+
//
|
|
30
|
+
// DeepSeek V4 — verified live 2026-09-11 against api.deepseek.com:
|
|
31
|
+
// - a 942,031-token prompt was ACCEPTED (HTTP 200)
|
|
32
|
+
// - a 1,122,032-token prompt was REJECTED, verbatim: "This model's maximum
|
|
33
|
+
// context length is 1048576 tokens."
|
|
34
|
+
// so the window is 1M. The old 256k figure here understated it 4x and made the
|
|
35
|
+
// /context meter lie. Max output is 393216 — the API rejects anything larger
|
|
36
|
+
// with "the valid range of max_tokens is [1, 393216]".
|
|
37
|
+
// Keyed by provider name or model-id prefix (longest prefix wins via the
|
|
38
|
+
// iteration order below — exact id matches take priority).
|
|
39
|
+
const CONTEXT_WINDOWS = {
|
|
40
|
+
deepseek: 1_048_576,
|
|
41
|
+
openrouter: 256_000,
|
|
42
|
+
together: 256_000,
|
|
43
|
+
xai: 256_000,
|
|
44
|
+
groq: 128_000,
|
|
45
|
+
ollama: 128_000,
|
|
46
|
+
openai: 200_000,
|
|
47
|
+
anthropic: 200_000,
|
|
48
|
+
google: 1_000_000,
|
|
49
|
+
gemini: 1_000_000,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** The context budget to show/guard against for a model id, provider, or raw model string. */
|
|
53
|
+
function contextWindowFor(model) {
|
|
54
|
+
const id = String(model || '').toLowerCase();
|
|
55
|
+
if (!id) return CONTEXT_WINDOW;
|
|
56
|
+
if (CONTEXT_WINDOWS[id]) return CONTEXT_WINDOWS[id];
|
|
57
|
+
for (const [prefix, win] of Object.entries(CONTEXT_WINDOWS)) {
|
|
58
|
+
if (id.startsWith(prefix)) return win;
|
|
59
|
+
}
|
|
60
|
+
return CONTEXT_WINDOW;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Rough token estimate: ~4 chars per token (heuristic, labeled approximate). */
|
|
64
|
+
function estimateTokens(text) {
|
|
65
|
+
if (!text) return 0;
|
|
66
|
+
return Math.max(1, Math.ceil([...String(text)].length / 4));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Bucket the transcript's token usage.
|
|
71
|
+
* input — user prompts + system + tools (per exchange)
|
|
72
|
+
* output — assistant replies
|
|
73
|
+
* cacheRead — what a resumed session reads back (prior context, approximated
|
|
74
|
+
* as all prior user+assistant text)
|
|
75
|
+
* cacheWrite — the newest user+assistant chunk written to cache
|
|
76
|
+
*/
|
|
77
|
+
function transcriptUsage(transcript, model = 'sonnet') {
|
|
78
|
+
const userMsgs = transcript.filter((m) => m.role === 'user');
|
|
79
|
+
const asstMsgs = transcript.filter((m) => m.role === 'assistant');
|
|
80
|
+
const input = userMsgs.reduce((a, m) => a + estimateTokens(m.text || ''), 0);
|
|
81
|
+
const output = asstMsgs.reduce((a, m) => a + estimateTokens(m.text || ''), 0);
|
|
82
|
+
const cacheRead = Math.max(0, input - (userMsgs.length ? estimateTokens(userMsgs[userMsgs.length - 1].text || '') : 0));
|
|
83
|
+
const cacheWrite = userMsgs.length ? estimateTokens(userMsgs[userMsgs.length - 1].text || '') : 0;
|
|
84
|
+
return { input, output, cacheRead, cacheWrite };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Dollar cost of a usage record at the given model's rates. */
|
|
88
|
+
function usageCost(usage, model = 'sonnet') {
|
|
89
|
+
const r = RATES[model] || RATES.sonnet;
|
|
90
|
+
const toD = (n, rate) => (n / 1_000_000) * rate;
|
|
91
|
+
return toD(usage.input, r.input)
|
|
92
|
+
+ toD(usage.output, r.output)
|
|
93
|
+
+ toD(usage.cacheRead, r.cacheRead)
|
|
94
|
+
+ toD(usage.cacheWrite, r.cacheWrite);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Full session accounting: per-bucket tokens, cost, and context used %. */
|
|
98
|
+
function sessionAccounting(transcript, model = 'sonnet') {
|
|
99
|
+
const usage = transcriptUsage(transcript, model);
|
|
100
|
+
const system = SYSTEM_TOKENS;
|
|
101
|
+
const tools = TOOL_TOKENS;
|
|
102
|
+
const history = usage.input + usage.output;
|
|
103
|
+
const used = system + tools + history;
|
|
104
|
+
const contextWindow = contextWindowFor(model);
|
|
105
|
+
return {
|
|
106
|
+
usage,
|
|
107
|
+
system,
|
|
108
|
+
tools,
|
|
109
|
+
history,
|
|
110
|
+
used,
|
|
111
|
+
contextWindow,
|
|
112
|
+
pct: Math.min(100, Math.round((used / contextWindow) * 100)),
|
|
113
|
+
cost: usageCost(usage, model),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Phase 4: build the same accounting shape from summed history.jsonl token
|
|
119
|
+
* records (live usage numbers when `real`, estimated otherwise). Real usage
|
|
120
|
+
* already includes system prompt + tools in cacheRead, so `used` counts
|
|
121
|
+
* input + output + cache instead of re-adding the constants.
|
|
122
|
+
*/
|
|
123
|
+
function accountingFromUsage(usage, model = 'sonnet', { exchanges = 0, real = false, costUsd } = {}) {
|
|
124
|
+
const cost = typeof costUsd === 'number' ? costUsd : usageCost(usage, model);
|
|
125
|
+
const used = real
|
|
126
|
+
? usage.input + usage.output + usage.cacheRead + usage.cacheWrite
|
|
127
|
+
: SYSTEM_TOKENS + TOOL_TOKENS + usage.input + usage.output;
|
|
128
|
+
const contextWindow = contextWindowFor(model);
|
|
129
|
+
return {
|
|
130
|
+
usage,
|
|
131
|
+
system: SYSTEM_TOKENS,
|
|
132
|
+
tools: TOOL_TOKENS,
|
|
133
|
+
history: usage.input + usage.output,
|
|
134
|
+
used,
|
|
135
|
+
contextWindow,
|
|
136
|
+
pct: Math.min(100, Math.round((used / contextWindow) * 100)),
|
|
137
|
+
cost,
|
|
138
|
+
exchanges,
|
|
139
|
+
real,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function fmtTokens(n) {
|
|
144
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
145
|
+
return String(n);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function fmtCost(d) {
|
|
149
|
+
return `$${d.toFixed(4)}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
RATES,
|
|
154
|
+
SYSTEM_TOKENS,
|
|
155
|
+
TOOL_TOKENS,
|
|
156
|
+
CONTEXT_WINDOW,
|
|
157
|
+
CONTEXT_WINDOWS,
|
|
158
|
+
contextWindowFor,
|
|
159
|
+
estimateTokens,
|
|
160
|
+
transcriptUsage,
|
|
161
|
+
usageCost,
|
|
162
|
+
sessionAccounting,
|
|
163
|
+
accountingFromUsage,
|
|
164
|
+
fmtTokens,
|
|
165
|
+
fmtCost,
|
|
166
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* agents.js — subagent prompt presets for the desktop `task` tool, ported
|
|
5
|
+
* from aegiscodex-dev's src/agents.js (same roles, same synthesis presets).
|
|
6
|
+
* A preset is a system prompt: `task` runs it through the normal engine.chat
|
|
7
|
+
* loop as a nested turn (its own tool rounds, same model class), not a
|
|
8
|
+
* separate runtime, so the presets are pure prompt composition.
|
|
9
|
+
*
|
|
10
|
+
* Text is adapted to the desktop's own tool vocabulary (readFile/writeFile/
|
|
11
|
+
* editFile/listDir/glob/grep/exec/task — see tools.js) so a subagent's
|
|
12
|
+
* instructions never name a tool the desktop doesn't actually advertise.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Role → system prompt. */
|
|
16
|
+
const AGENT_PRESETS = {
|
|
17
|
+
synthesizer: `You are a senior technical lead. Given analysis from multiple specialist agents, synthesize their findings into a clear, actionable summary.
|
|
18
|
+
Structure your response as: key findings, recommended approach, top action items.
|
|
19
|
+
Be direct, concrete, and avoid repeating everything the agents said.
|
|
20
|
+
Focus on delivering a decision-ready synthesis.`,
|
|
21
|
+
|
|
22
|
+
architect: `You are a System Architect. Design the new application architecture.
|
|
23
|
+
Define: project structure, tech stack, directory layout, key modules, data flow, API design.
|
|
24
|
+
Consider: scalability, maintainability, testing strategy, deployment.
|
|
25
|
+
Output a concrete file tree and architecture decisions log. Be specific.`,
|
|
26
|
+
|
|
27
|
+
scaffolder: `You are a Project Scaffolder. Build the complete application from scratch.
|
|
28
|
+
|
|
29
|
+
YOUR JOB IS TO CREATE ALL PROJECT FILES - not just describe them.
|
|
30
|
+
|
|
31
|
+
Use writeFile to create: package.json, tsconfig.json, source files, configs, tests.
|
|
32
|
+
Generate COMPLETE, WORKING code - not stubs or placeholders.
|
|
33
|
+
Set up build scripts, lint config, and any necessary tooling.
|
|
34
|
+
|
|
35
|
+
After creating files, use exec to run: npm/pnpm install, then build/compile.
|
|
36
|
+
Fix any errors until the project builds successfully.
|
|
37
|
+
|
|
38
|
+
Be thorough - a real, runnable project is the goal.`,
|
|
39
|
+
|
|
40
|
+
planner: `You are a Refactoring Planner. Given the analyzer findings, create a step-by-step plan.
|
|
41
|
+
Each step: file path, what to change, why, risk level (LOW/MEDIUM/HIGH).
|
|
42
|
+
Include before/after snippets. Order by impact. Be concrete.`,
|
|
43
|
+
|
|
44
|
+
implementer: `You are an Implementation Engineer. Execute the refactoring plan.
|
|
45
|
+
Use editFile and writeFile to make actual code changes.
|
|
46
|
+
After each change, use readFile to verify correctness. Keep existing code style.
|
|
47
|
+
Run build commands with exec to ensure nothing is broken.`,
|
|
48
|
+
|
|
49
|
+
reviewer: `You are a Code Reviewer. Review the approach and code.
|
|
50
|
+
Check: logic errors, type safety, error handling, performance, security.
|
|
51
|
+
Be critical but constructive. Report specific issues with file paths.`,
|
|
52
|
+
|
|
53
|
+
debugger: `You are a Debugging Specialist. Analyze potential issues and edge cases.
|
|
54
|
+
Identify: failure modes, error handling gaps, testing considerations.
|
|
55
|
+
Think about what could go wrong and how to prevent it.`,
|
|
56
|
+
|
|
57
|
+
scanner: `You are a Security Vulnerability Scanner.
|
|
58
|
+
Scan for: hardcoded API keys/secrets, SQL injection, XSS, unsafe eval/exec, path traversal.
|
|
59
|
+
Use grep with targeted patterns. Report every finding with: file path, severity (CRITICAL/HIGH/MEDIUM/LOW), line number.`,
|
|
60
|
+
|
|
61
|
+
analyzer: `You are a Code Analyzer. Find refactoring opportunities.
|
|
62
|
+
Look for: duplicated code, long functions (>20 lines), complex conditionals, unused imports,
|
|
63
|
+
circular dependencies, inconsistent patterns. Report with file paths and line numbers.`,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** System prompt for a `general` (or unrecognized) subagent type. */
|
|
67
|
+
const GENERAL_AGENT_PROMPT =
|
|
68
|
+
'You are a capable autonomous coding subagent. Complete the assigned task end to end using ' +
|
|
69
|
+
'the available tools (exec, readFile, writeFile, editFile, listDir, glob, grep). Work in the ' +
|
|
70
|
+
'current repository, verify your work, and finish with a concise report of what you did and ' +
|
|
71
|
+
'what you found. You cannot ask follow-up questions — make reasonable assumptions and proceed. ' +
|
|
72
|
+
'When a large sub-task is better handled by a focused specialist, delegate it with the task tool.';
|
|
73
|
+
|
|
74
|
+
/** Human-readable role label for a preset id (falls back to the id). */
|
|
75
|
+
function agentRoleLabel(role) {
|
|
76
|
+
const labels = {
|
|
77
|
+
synthesizer: 'Technical Lead', architect: 'System Architect',
|
|
78
|
+
scaffolder: 'Project Scaffolder', planner: 'Refactoring Planner',
|
|
79
|
+
implementer: 'Implementation Engineer', reviewer: 'Code Reviewer',
|
|
80
|
+
debugger: 'Debugging Specialist', scanner: 'Vulnerability Scanner',
|
|
81
|
+
analyzer: 'Code Analyzer', general: 'General',
|
|
82
|
+
};
|
|
83
|
+
return labels[role] || role;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Every preset id, in palette order (excludes the 'general' fallback). */
|
|
87
|
+
function agentRoles() {
|
|
88
|
+
return Object.keys(AGENT_PRESETS);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Resolve a subagent_type to its system prompt (unknown/absent → general). */
|
|
92
|
+
function agentSystemPrompt(role) {
|
|
93
|
+
return AGENT_PRESETS[role] || GENERAL_AGENT_PROMPT;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
AGENT_PRESETS,
|
|
98
|
+
GENERAL_AGENT_PROMPT,
|
|
99
|
+
agentRoleLabel,
|
|
100
|
+
agentRoles,
|
|
101
|
+
agentSystemPrompt,
|
|
102
|
+
};
|