aegiscode 6.1.0 → 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 +522 -164
- 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
package/src/export.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transcript → markdown/json export for /export, and the last-response
|
|
5
|
+
* extraction for /copy. Exports are written to the current directory (or the
|
|
6
|
+
* clipboard) and the resulting path is reported in the transcript.
|
|
7
|
+
*
|
|
8
|
+
* Ported from aegiscodex-dev/src/export.js (ESM → CommonJS). Output strings
|
|
9
|
+
* (the # Aegiscodex session header, the ## Claude / ## User role headers, the
|
|
10
|
+
* aegiscodex-export- filename) are kept as in the reference.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
|
|
16
|
+
/** Render the transcript as a markdown document. */
|
|
17
|
+
function transcriptToMarkdown(transcript) {
|
|
18
|
+
const lines = ['# Aegiscodex session', '', `> exported ${new Date().toISOString()}`, ''];
|
|
19
|
+
for (const m of transcript) {
|
|
20
|
+
const text = (m.text || '').replace(/\n{3,}/g, '\n\n').trim();
|
|
21
|
+
if (m.role === 'user') lines.push(`## User\n\n${text || '_(empty)_'}`, '');
|
|
22
|
+
else if (m.role === 'assistant') lines.push(`## Claude\n\n${text || '_(empty)_'}`, '');
|
|
23
|
+
else if (m.role === 'note') lines.push(`> ${text}`, '');
|
|
24
|
+
}
|
|
25
|
+
return lines.join('\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Render the transcript as a JSON document (array of messages). */
|
|
29
|
+
function transcriptToJSON(transcript) {
|
|
30
|
+
const clean = transcript.map((m) => ({
|
|
31
|
+
role: m.role,
|
|
32
|
+
text: m.text || '',
|
|
33
|
+
...(m.tool ? { tool: m.tool } : {}),
|
|
34
|
+
}));
|
|
35
|
+
return JSON.stringify(clean, null, 2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Write exported text to a file. target: absolute path, 'clipboard' handled by
|
|
40
|
+
* the caller (clipboard.js) — here we only do files.
|
|
41
|
+
* Returns the absolute path written.
|
|
42
|
+
*/
|
|
43
|
+
function writeExportFile(text, format, cwd = process.cwd()) {
|
|
44
|
+
const ext = format === 'json' ? 'json' : 'md';
|
|
45
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
46
|
+
const name = `aegiscodex-export-${ts}.${ext}`;
|
|
47
|
+
const p = path.join(cwd, name);
|
|
48
|
+
fs.writeFileSync(p, text, 'utf8');
|
|
49
|
+
return p;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The most recent assistant response (last N when given a count). */
|
|
53
|
+
function lastAssistantText(transcript, count = 1) {
|
|
54
|
+
const asst = transcript.filter((m) => m.role === 'assistant');
|
|
55
|
+
const n = Math.max(1, Math.min(count, asst.length));
|
|
56
|
+
return asst.slice(asst.length - n).map((m) => m.text || '').join('\n\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
transcriptToMarkdown,
|
|
61
|
+
transcriptToJSON,
|
|
62
|
+
writeExportFile,
|
|
63
|
+
lastAssistantText,
|
|
64
|
+
};
|
package/src/history.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session persistence — one JSON line per exchange in ~/.aegiscode/history.jsonl
|
|
5
|
+
* (or $AEGISCODE_HOME). Keeps the file bounded (oldest entries dropped past
|
|
6
|
+
* HISTORY_LIMIT lines).
|
|
7
|
+
*
|
|
8
|
+
* Ported from aegiscodex-dev/src/history.js (ESM → CommonJS). The data dir now
|
|
9
|
+
* comes from config.js's shared `aegisDir()` helper.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const { aegisDir } = require('./config.js');
|
|
16
|
+
const { estimateTokens } = require('./tokens.js');
|
|
17
|
+
|
|
18
|
+
const HISTORY_LIMIT = 500;
|
|
19
|
+
|
|
20
|
+
function historyPath() {
|
|
21
|
+
return path.join(aegisDir(), 'history.jsonl');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ensureHistoryDir() {
|
|
25
|
+
fs.mkdirSync(path.dirname(historyPath()), { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Record one exchange. `reply` is stored in full so /resume can restore the
|
|
30
|
+
* transcript exactly. Token accounting (Phase 4):
|
|
31
|
+
* live — `usage` carries the real numbers from the provider stream-json
|
|
32
|
+
* (input/output/cacheRead/cacheWrite + costUsd), stored verbatim;
|
|
33
|
+
* demo — estimated from text length (~4 chars/token), labeled real:false.
|
|
34
|
+
* /cost aggregates these records, so compacted-away exchanges still count.
|
|
35
|
+
*/
|
|
36
|
+
function appendHistory({ sessionId, prompt, reply, status, usage }) {
|
|
37
|
+
try {
|
|
38
|
+
ensureHistoryDir();
|
|
39
|
+
const entry = {
|
|
40
|
+
ts: new Date().toISOString(),
|
|
41
|
+
sessionId,
|
|
42
|
+
cwd: process.cwd(),
|
|
43
|
+
prompt,
|
|
44
|
+
reply,
|
|
45
|
+
status, // 'done' | 'stopped' | 'error'
|
|
46
|
+
tokens: usage
|
|
47
|
+
? {
|
|
48
|
+
input: usage.input || 0,
|
|
49
|
+
output: usage.output || 0,
|
|
50
|
+
cacheRead: usage.cacheRead || 0,
|
|
51
|
+
cacheWrite: usage.cacheWrite || 0,
|
|
52
|
+
real: true,
|
|
53
|
+
}
|
|
54
|
+
: { input: estimateTokens(prompt), output: estimateTokens(reply || ''), real: false },
|
|
55
|
+
};
|
|
56
|
+
if (usage && typeof usage.costUsd === 'number') entry.costUsd = usage.costUsd;
|
|
57
|
+
const p = historyPath();
|
|
58
|
+
const prev = fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split('\n').filter(Boolean) : [];
|
|
59
|
+
const lines = [...prev, JSON.stringify(entry)];
|
|
60
|
+
const trimmed = lines.slice(Math.max(0, lines.length - HISTORY_LIMIT));
|
|
61
|
+
fs.writeFileSync(p, trimmed.join('\n') + '\n');
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// Persistence is best-effort; never crash the session over it.
|
|
64
|
+
if (process.env.AEGIS_HIST_DEBUG) console.error('[history] write failed:', e);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readEntries() {
|
|
69
|
+
try {
|
|
70
|
+
const raw = fs.readFileSync(historyPath(), 'utf8');
|
|
71
|
+
return raw
|
|
72
|
+
.split('\n')
|
|
73
|
+
.map((l) => l.trim())
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Newest-first list of own sessions, one per distinct sessionId. */
|
|
83
|
+
function readOwnSessions(limit = 8) {
|
|
84
|
+
const entries = readEntries();
|
|
85
|
+
const byId = new Map();
|
|
86
|
+
for (const e of entries) {
|
|
87
|
+
if (!byId.has(e.sessionId)) byId.set(e.sessionId, e);
|
|
88
|
+
}
|
|
89
|
+
const items = [...byId.values()]
|
|
90
|
+
.sort((a, b) => (a.ts < b.ts ? 1 : -1))
|
|
91
|
+
.slice(0, limit)
|
|
92
|
+
.map((e) => ({
|
|
93
|
+
id: e.sessionId,
|
|
94
|
+
cwd: (e.cwd || '').split('/').filter(Boolean).pop() || '~',
|
|
95
|
+
summary: (e.prompt || '').slice(0, 60),
|
|
96
|
+
time: e.ts,
|
|
97
|
+
own: true,
|
|
98
|
+
}));
|
|
99
|
+
return items;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Rebuild the transcript (user/assistant pairs) of a session, oldest first. */
|
|
103
|
+
function readSessionTranscript(sessionId) {
|
|
104
|
+
return readEntries()
|
|
105
|
+
.filter((e) => e.sessionId === sessionId)
|
|
106
|
+
.flatMap((e) => [
|
|
107
|
+
{ role: 'user', text: e.prompt },
|
|
108
|
+
{ role: 'assistant', text: e.reply || '(no response)' },
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** All history records for one session, oldest first (power /cost). */
|
|
113
|
+
function sessionHistoryEntries(sessionId) {
|
|
114
|
+
return readEntries().filter((e) => e.sessionId === sessionId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Drop every history record for a session (Phase 10b, 2.1.228 port). /clear
|
|
119
|
+
* calls this so /cost stops summing old rows — the sessionId stays stable, so
|
|
120
|
+
* rewind checkpoints and the transcript lineage are unaffected.
|
|
121
|
+
*/
|
|
122
|
+
function pruneSessionHistory(sessionId) {
|
|
123
|
+
try {
|
|
124
|
+
const p = historyPath();
|
|
125
|
+
if (!fs.existsSync(p)) return;
|
|
126
|
+
const kept = fs
|
|
127
|
+
.readFileSync(p, 'utf8')
|
|
128
|
+
.split('\n')
|
|
129
|
+
.map((l) => l.trim())
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.filter((l) => {
|
|
132
|
+
try { return JSON.parse(l).sessionId !== sessionId; } catch { return true; }
|
|
133
|
+
});
|
|
134
|
+
fs.writeFileSync(p, kept.length ? kept.join('\n') + '\n' : '');
|
|
135
|
+
} catch (e) {
|
|
136
|
+
if (process.env.AEGIS_HIST_DEBUG) console.error('[history] prune failed:', e);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Sum a session's token records across history.jsonl. Compacted-away exchanges
|
|
142
|
+
* still count because /compact writes a summary exchange, it never deletes the
|
|
143
|
+
* file. `real` is true only when every record carries live CLI usage numbers.
|
|
144
|
+
*/
|
|
145
|
+
function aggregateSessionUsage(sessionId) {
|
|
146
|
+
const entries = sessionHistoryEntries(sessionId);
|
|
147
|
+
const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
148
|
+
let real = entries.length > 0;
|
|
149
|
+
let costUsd = 0;
|
|
150
|
+
for (const e of entries) {
|
|
151
|
+
const t = e.tokens || {};
|
|
152
|
+
if (!t.real) real = false;
|
|
153
|
+
usage.input += t.input || 0;
|
|
154
|
+
usage.output += t.output || 0;
|
|
155
|
+
usage.cacheRead += t.cacheRead || 0;
|
|
156
|
+
usage.cacheWrite += t.cacheWrite || 0;
|
|
157
|
+
if (typeof e.costUsd === 'number') costUsd += e.costUsd;
|
|
158
|
+
}
|
|
159
|
+
return { entries: entries.length, usage, real, costUsd };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Sessions for the /resume overlay: own Aegiscode sessions merged with real
|
|
164
|
+
* Claude Code sessions from ~/.claude/history.jsonl, newest first.
|
|
165
|
+
*/
|
|
166
|
+
function readResumeList(limit = 8, ownLimit = 5, claudeLimit = 8) {
|
|
167
|
+
const items = readOwnSessions(ownLimit);
|
|
168
|
+
const claudeItems = [];
|
|
169
|
+
const histPath = `${os.homedir()}/.claude/history.jsonl`;
|
|
170
|
+
try {
|
|
171
|
+
const raw = fs.readFileSync(histPath, 'utf8');
|
|
172
|
+
const lines = raw.trim().split('\n').reverse().slice(0, claudeLimit);
|
|
173
|
+
for (const l of lines) {
|
|
174
|
+
try {
|
|
175
|
+
const j = JSON.parse(l);
|
|
176
|
+
const meta = j.extra && JSON.parse(j.extra);
|
|
177
|
+
if (meta && meta.sessionId) {
|
|
178
|
+
const c = (j.cwd || '').split('/').filter(Boolean).pop() || '~';
|
|
179
|
+
const t = (j.summary || '').slice(0, 60);
|
|
180
|
+
claudeItems.push({ id: meta.sessionId, cwd: c, summary: t, time: j.timestamp, own: false });
|
|
181
|
+
}
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
} catch {}
|
|
185
|
+
return [...items, ...claudeItems]
|
|
186
|
+
.sort((a, b) => (String(a.time || '') < String(b.time || '') ? 1 : -1))
|
|
187
|
+
.slice(0, limit);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
HISTORY_LIMIT,
|
|
192
|
+
historyPath,
|
|
193
|
+
ensureHistoryDir,
|
|
194
|
+
appendHistory,
|
|
195
|
+
readOwnSessions,
|
|
196
|
+
readSessionTranscript,
|
|
197
|
+
sessionHistoryEntries,
|
|
198
|
+
pruneSessionHistory,
|
|
199
|
+
aggregateSessionUsage,
|
|
200
|
+
readResumeList,
|
|
201
|
+
};
|
package/src/init.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* /init support: sniff a project (language, build/test/lint commands, layout)
|
|
5
|
+
* and generate an AEGIS.md with real project instructions. Never overwrites an
|
|
6
|
+
* existing file — the handler reports and leaves the file alone.
|
|
7
|
+
*
|
|
8
|
+
* Ported from aegiscodex-dev/src/init.js (ESM → CommonJS). The reference wrote
|
|
9
|
+
* a CLAUDE.md; this build writes AEGIS.md and the generated document is headed
|
|
10
|
+
* "# AEGIS.md" and references "AEGIS Code". The `buildClaudeMd` export is kept
|
|
11
|
+
* as an alias of `buildAegisMd` so callers ported from the reference still
|
|
12
|
+
* resolve.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const { execSync } = require('node:child_process');
|
|
18
|
+
|
|
19
|
+
function readJson(p, cwd) {
|
|
20
|
+
try { return JSON.parse(fs.readFileSync(path.join(cwd, p), 'utf8')); } catch { return null; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasFile(cwd, ...names) {
|
|
24
|
+
return names.some((n) => fs.existsSync(path.join(cwd, n)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function gitName(cwd) {
|
|
28
|
+
try {
|
|
29
|
+
const remote = execSync('git config --get remote.origin.url', { encoding: 'utf8', cwd, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
30
|
+
const base = remote.split('/').pop().replace(/\.git$/, '');
|
|
31
|
+
if (base) return base;
|
|
32
|
+
} catch {}
|
|
33
|
+
try {
|
|
34
|
+
const dir = cwd.split('/').filter(Boolean).pop();
|
|
35
|
+
return dir || 'this project';
|
|
36
|
+
} catch { return 'this project'; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Describe a project enough to write a useful AEGIS.md. */
|
|
40
|
+
function sniffProject(cwd = process.cwd()) {
|
|
41
|
+
const pkg = readJson('package.json', cwd);
|
|
42
|
+
const out = {
|
|
43
|
+
name: pkg && pkg.name ? pkg.name : gitName(cwd),
|
|
44
|
+
lang: 'unknown',
|
|
45
|
+
build: null,
|
|
46
|
+
test: null,
|
|
47
|
+
lint: null,
|
|
48
|
+
dev: null,
|
|
49
|
+
structure: [],
|
|
50
|
+
toolchain: [],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (pkg) {
|
|
54
|
+
out.lang = 'JavaScript/TypeScript (Node.js)';
|
|
55
|
+
out.toolchain.push('Node.js');
|
|
56
|
+
const scripts = pkg.scripts || {};
|
|
57
|
+
if (scripts.build) out.build = `npm run build`;
|
|
58
|
+
if (scripts.test) out.test = `npm test`;
|
|
59
|
+
if (scripts.lint) out.lint = `npm run lint`;
|
|
60
|
+
if (scripts.dev || scripts.start || scripts.watch) out.dev = scripts.dev ? `npm run dev` : `npm start`;
|
|
61
|
+
if (pkg.packageManager) out.toolchain.push(pkg.packageManager);
|
|
62
|
+
} else if (hasFile(cwd, 'go.mod')) {
|
|
63
|
+
out.lang = 'Go';
|
|
64
|
+
out.toolchain.push('Go');
|
|
65
|
+
out.build = 'go build ./...';
|
|
66
|
+
out.test = 'go test ./...';
|
|
67
|
+
out.lint = 'golangci-lint run' ;
|
|
68
|
+
out.dev = 'go run .';
|
|
69
|
+
} else if (hasFile(cwd, 'Cargo.toml')) {
|
|
70
|
+
out.lang = 'Rust';
|
|
71
|
+
out.toolchain.push('Cargo');
|
|
72
|
+
out.build = 'cargo build';
|
|
73
|
+
out.test = 'cargo test';
|
|
74
|
+
out.lint = 'cargo clippy -- -D warnings';
|
|
75
|
+
out.dev = 'cargo run';
|
|
76
|
+
} else if (hasFile(cwd, 'pyproject.toml', 'setup.py', 'requirements.txt')) {
|
|
77
|
+
out.lang = 'Python';
|
|
78
|
+
out.toolchain.push('Python');
|
|
79
|
+
out.test = hasFile(cwd, 'pytest.ini', 'tox.ini') ? 'pytest' : 'python -m pytest';
|
|
80
|
+
out.lint = 'ruff check .';
|
|
81
|
+
} else if (hasFile(cwd, 'Makefile')) {
|
|
82
|
+
out.lang = 'C/C++ or make-driven';
|
|
83
|
+
out.toolchain.push('make');
|
|
84
|
+
out.build = 'make';
|
|
85
|
+
out.test = 'make test';
|
|
86
|
+
out.dev = 'make run';
|
|
87
|
+
} else if (hasFile(cwd, 'pom.xml', 'build.gradle')) {
|
|
88
|
+
out.lang = 'Java';
|
|
89
|
+
out.toolchain.push('Maven/Gradle');
|
|
90
|
+
out.build = hasFile(cwd, 'pom.xml') ? 'mvn package' : 'gradle build';
|
|
91
|
+
out.test = hasFile(cwd, 'pom.xml') ? 'mvn test' : 'gradle test';
|
|
92
|
+
} else if (hasFile(cwd, 'Gemfile')) {
|
|
93
|
+
out.lang = 'Ruby';
|
|
94
|
+
out.toolchain.push('Ruby');
|
|
95
|
+
out.test = 'bundle exec rspec';
|
|
96
|
+
out.build = 'bundle exec rake build';
|
|
97
|
+
} else if (hasFile(cwd, 'composer.json')) {
|
|
98
|
+
out.lang = 'PHP';
|
|
99
|
+
out.toolchain.push('Composer');
|
|
100
|
+
out.test = 'composer test';
|
|
101
|
+
} else if (hasFile(cwd, '.csproj')) {
|
|
102
|
+
out.lang = 'C#/.NET';
|
|
103
|
+
out.toolchain.push('.NET');
|
|
104
|
+
out.build = 'dotnet build';
|
|
105
|
+
out.test = 'dotnet test';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Layout conventions.
|
|
109
|
+
for (const d of ['src', 'lib', 'app', 'tests', 'test', 'spec', 'docs', 'scripts']) {
|
|
110
|
+
if (fs.existsSync(path.join(cwd, d))) out.structure.push(d);
|
|
111
|
+
}
|
|
112
|
+
for (const f of ['eslint.config.js', '.eslintrc', '.prettierrc', 'biome.json', 'tsconfig.json', '.github/workflows']) {
|
|
113
|
+
if (fs.existsSync(path.join(cwd, f))) out.toolchain.push(f.split('/')[0]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Render an AEGIS.md from a sniff result. */
|
|
120
|
+
function buildAegisMd(sniff) {
|
|
121
|
+
const s = sniff;
|
|
122
|
+
const lines = [];
|
|
123
|
+
lines.push(`# AEGIS.md`);
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push(`Generated by AEGIS Code /init — edit freely. This file teaches AI agents how to work in this repo.`);
|
|
126
|
+
lines.push('');
|
|
127
|
+
lines.push('## Project');
|
|
128
|
+
lines.push('');
|
|
129
|
+
lines.push(`- Project: ${s.name}`);
|
|
130
|
+
lines.push(`- Language: ${s.lang}`);
|
|
131
|
+
if (s.toolchain.length) lines.push(`- Toolchain: ${[...new Set(s.toolchain)].join(', ')}`);
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push('## Commands');
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push('Run these to build, test, and lint:');
|
|
136
|
+
lines.push('```bash');
|
|
137
|
+
if (s.build) lines.push(s.build + ' # build');
|
|
138
|
+
if (s.test) lines.push(s.test + ' # test');
|
|
139
|
+
if (s.lint) lines.push(s.lint + ' # lint');
|
|
140
|
+
if (s.dev) lines.push(s.dev + ' # dev server');
|
|
141
|
+
lines.push('```');
|
|
142
|
+
lines.push('');
|
|
143
|
+
if (s.structure.length) {
|
|
144
|
+
lines.push('## Project structure');
|
|
145
|
+
lines.push('');
|
|
146
|
+
for (const d of s.structure) lines.push(`- \`${d}/\` — ${d === 'src' || d === 'lib' ? 'source code' : d === 'tests' || d === 'test' || d === 'spec' ? 'tests' : d}`);
|
|
147
|
+
lines.push('');
|
|
148
|
+
}
|
|
149
|
+
lines.push('## Coding conventions');
|
|
150
|
+
lines.push('');
|
|
151
|
+
lines.push('- Keep changes minimal and focused; match the surrounding style.');
|
|
152
|
+
lines.push('- Before editing a file, read it; verify with the test command above.');
|
|
153
|
+
lines.push('- Prefer the project\'s own scripts over ad-hoc shell commands.');
|
|
154
|
+
lines.push('');
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
sniffProject,
|
|
160
|
+
buildAegisMd,
|
|
161
|
+
buildClaudeMd: buildAegisMd, // reference name kept for compatibility
|
|
162
|
+
};
|
package/src/input.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Line editor with history — the input line's buffer, cursor and recall.
|
|
5
|
+
*
|
|
6
|
+
* Ported from `aegiscodex-dev/src/input.js`. The one thing worth knowing before
|
|
7
|
+
* touching it: `cursor` is a **codepoint** index, not a UTF-16 index. Slicing
|
|
8
|
+
* `buf` directly corrupts a surrogate pair the moment one has been inserted
|
|
9
|
+
* (the next insert's slice lands mid-pair and leaves a lone surrogate, which
|
|
10
|
+
* renders as U+FFFD), so every mutation splices on `[...buf]` instead.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class LineEditor {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.buf = '';
|
|
16
|
+
this.cursor = 0;
|
|
17
|
+
this.history = [];
|
|
18
|
+
this.hi = -1;
|
|
19
|
+
this.draft = '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
reset() {
|
|
23
|
+
this.buf = '';
|
|
24
|
+
this.cursor = 0;
|
|
25
|
+
this.hi = -1;
|
|
26
|
+
this.draft = '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
insert(ch) {
|
|
30
|
+
const arr = [...this.buf];
|
|
31
|
+
arr.splice(this.cursor, 0, ...ch);
|
|
32
|
+
this.buf = arr.join('');
|
|
33
|
+
this.cursor += [...ch].length;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
left() {
|
|
37
|
+
if (this.cursor > 0) this.cursor--;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
right() {
|
|
41
|
+
if (this.cursor < [...this.buf].length) this.cursor++;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
home() {
|
|
45
|
+
this.cursor = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
end() {
|
|
49
|
+
this.cursor = [...this.buf].length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
backspace() {
|
|
53
|
+
if (this.cursor === 0) return;
|
|
54
|
+
const arr = [...this.buf];
|
|
55
|
+
arr.splice(this.cursor - 1, 1);
|
|
56
|
+
this.buf = arr.join('');
|
|
57
|
+
this.cursor--;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
delete() {
|
|
61
|
+
if (this.cursor >= [...this.buf].length) return;
|
|
62
|
+
const arr = [...this.buf];
|
|
63
|
+
arr.splice(this.cursor, 1);
|
|
64
|
+
this.buf = arr.join('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** vim `s` — delete the char under the cursor (caller switches to insert). */
|
|
68
|
+
substChar() {
|
|
69
|
+
if (this.cursor >= [...this.buf].length) return false;
|
|
70
|
+
this.delete();
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** vim `S` — clear the whole line (caller switches to insert). */
|
|
75
|
+
substLine() {
|
|
76
|
+
const had = this.buf.length > 0;
|
|
77
|
+
this.buf = '';
|
|
78
|
+
this.cursor = 0;
|
|
79
|
+
return had;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
killToEnd() {
|
|
83
|
+
this.buf = [...this.buf].slice(0, this.cursor).join('');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
wordBack() {
|
|
87
|
+
const arr = [...this.buf];
|
|
88
|
+
let i = this.cursor;
|
|
89
|
+
while (i > 0 && arr[i - 1] === ' ') i--;
|
|
90
|
+
while (i > 0 && arr[i - 1] !== ' ') i--;
|
|
91
|
+
this.cursor = i;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
wordDelete() {
|
|
95
|
+
const arr = [...this.buf];
|
|
96
|
+
let i = this.cursor;
|
|
97
|
+
while (i < arr.length && arr[i] === ' ') i++;
|
|
98
|
+
while (i < arr.length && arr[i] !== ' ') i++;
|
|
99
|
+
this.buf = arr.slice(0, this.cursor).join('') + arr.slice(i).join('');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
historyUp() {
|
|
103
|
+
if (!this.history.length) return;
|
|
104
|
+
if (this.hi === -1) {
|
|
105
|
+
this.draft = this.buf;
|
|
106
|
+
this.hi = this.history.length - 1;
|
|
107
|
+
} else if (this.hi > 0) this.hi--;
|
|
108
|
+
this.buf = this.history[this.hi];
|
|
109
|
+
this.end();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
historyDown() {
|
|
113
|
+
if (this.hi === -1) return;
|
|
114
|
+
if (this.hi < this.history.length - 1) {
|
|
115
|
+
this.hi++;
|
|
116
|
+
this.buf = this.history[this.hi];
|
|
117
|
+
} else {
|
|
118
|
+
this.hi = -1;
|
|
119
|
+
this.buf = this.draft;
|
|
120
|
+
}
|
|
121
|
+
this.end();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Commit the buffer. Returns the trimmed text and clears the line, or null
|
|
125
|
+
* when there was nothing to submit (a bare Enter must not become a turn). */
|
|
126
|
+
submit() {
|
|
127
|
+
const text = this.buf.trim();
|
|
128
|
+
if (!text) return null;
|
|
129
|
+
this.history.push(text);
|
|
130
|
+
if (this.history.length > 500) this.history.shift();
|
|
131
|
+
this.reset();
|
|
132
|
+
return text;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { LineEditor };
|
package/src/keys.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Raw key decoding for the terminal (raw-mode stdin).
|
|
5
|
+
*
|
|
6
|
+
* Ported from `aegiscodex-dev/src/keys.js` so the aegiscode CLI and the
|
|
7
|
+
* reference client decode the same bytes into the same key names — the
|
|
8
|
+
* chatflow's key handling (history recall, vim motions, Esc-to-interrupt) is
|
|
9
|
+
* only correct if the decoder agrees with it.
|
|
10
|
+
*
|
|
11
|
+
* Two entry points:
|
|
12
|
+
* decodePlain(c) one character with no ESC prefix
|
|
13
|
+
* decodeEscSequence(s) a complete CSI (\x1b[…X) or SS3 (\x1bOX) sequence
|
|
14
|
+
*
|
|
15
|
+
* Both return either `null` (nothing to do) or a key object `{name, ch?}`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const KEY = Object.freeze({
|
|
19
|
+
UP: 'up',
|
|
20
|
+
DOWN: 'down',
|
|
21
|
+
LEFT: 'left',
|
|
22
|
+
RIGHT: 'right',
|
|
23
|
+
ENTER: 'enter',
|
|
24
|
+
TAB: 'tab',
|
|
25
|
+
ESC: 'esc',
|
|
26
|
+
BACKSPACE: 'backspace',
|
|
27
|
+
DELETE: 'delete',
|
|
28
|
+
HOME: 'home',
|
|
29
|
+
END: 'end',
|
|
30
|
+
PAGE_UP: 'pageup',
|
|
31
|
+
PAGE_DOWN: 'pagedown',
|
|
32
|
+
CTRL_C: 'ctrl-c',
|
|
33
|
+
CTRL_D: 'ctrl-d',
|
|
34
|
+
CTRL_L: 'ctrl-l',
|
|
35
|
+
CTRL_R: 'ctrl-r',
|
|
36
|
+
CTRL_U: 'ctrl-u',
|
|
37
|
+
CTRL_A: 'ctrl-a',
|
|
38
|
+
CTRL_E: 'ctrl-e',
|
|
39
|
+
CTRL_K: 'ctrl-k',
|
|
40
|
+
CTRL_W: 'ctrl-w',
|
|
41
|
+
CTRL_T: 'ctrl-t',
|
|
42
|
+
CTRL_N: 'ctrl-n',
|
|
43
|
+
CTRL_P: 'ctrl-p',
|
|
44
|
+
CTRL_O: 'ctrl-o',
|
|
45
|
+
CTRL_LEFT: 'ctrl-left',
|
|
46
|
+
CTRL_RIGHT: 'ctrl-right',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Decode a bare character (no escape prefix) into a key object. */
|
|
50
|
+
function decodePlain(chunk) {
|
|
51
|
+
switch (chunk) {
|
|
52
|
+
case '\r':
|
|
53
|
+
case '\n':
|
|
54
|
+
return { name: KEY.ENTER };
|
|
55
|
+
case '\t':
|
|
56
|
+
return { name: KEY.TAB };
|
|
57
|
+
case '\x03':
|
|
58
|
+
return { name: KEY.CTRL_C };
|
|
59
|
+
case '\x04':
|
|
60
|
+
return { name: KEY.CTRL_D };
|
|
61
|
+
case '\x0c':
|
|
62
|
+
return { name: KEY.CTRL_L };
|
|
63
|
+
case '\x12':
|
|
64
|
+
return { name: KEY.CTRL_R };
|
|
65
|
+
case '\x15':
|
|
66
|
+
return { name: KEY.CTRL_U };
|
|
67
|
+
case '\x01':
|
|
68
|
+
return { name: KEY.CTRL_A };
|
|
69
|
+
case '\x05':
|
|
70
|
+
return { name: KEY.CTRL_E };
|
|
71
|
+
case '\x0b':
|
|
72
|
+
return { name: KEY.CTRL_K };
|
|
73
|
+
case '\x17':
|
|
74
|
+
return { name: KEY.CTRL_W };
|
|
75
|
+
case '\x14':
|
|
76
|
+
return { name: KEY.CTRL_T };
|
|
77
|
+
case '\x0e':
|
|
78
|
+
return { name: KEY.CTRL_N };
|
|
79
|
+
case '\x10':
|
|
80
|
+
return { name: KEY.CTRL_P };
|
|
81
|
+
case '\x0f':
|
|
82
|
+
return { name: KEY.CTRL_O };
|
|
83
|
+
case '\x7f':
|
|
84
|
+
case '\x08':
|
|
85
|
+
return { name: KEY.BACKSPACE };
|
|
86
|
+
default: {
|
|
87
|
+
// Any other control char is meaningless here; printable is a char.
|
|
88
|
+
const code = chunk.charCodeAt(0);
|
|
89
|
+
if (code < 32 || code === 127) return { name: KEY.ESC };
|
|
90
|
+
return { name: 'char', ch: chunk };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Decode a complete CSI/SS3 escape sequence into a key object (or null). */
|
|
96
|
+
function decodeEscSequence(seq) {
|
|
97
|
+
if (seq.startsWith('\x1b[')) {
|
|
98
|
+
const body = seq.slice(2);
|
|
99
|
+
// CSI with modifier params, e.g. 1;5C = ctrl+right.
|
|
100
|
+
const m = body.match(/^([0-9;]*)([A-Za-z~])$/);
|
|
101
|
+
if (m) {
|
|
102
|
+
const params = m[1] ? m[1].split(';') : [];
|
|
103
|
+
const mod = params.length > 1 ? parseInt(params[1], 10) : 0;
|
|
104
|
+
switch (m[2]) {
|
|
105
|
+
case 'A':
|
|
106
|
+
return { name: KEY.UP };
|
|
107
|
+
case 'B':
|
|
108
|
+
return { name: KEY.DOWN };
|
|
109
|
+
case 'C':
|
|
110
|
+
return mod === 5 ? { name: KEY.CTRL_RIGHT } : { name: KEY.RIGHT };
|
|
111
|
+
case 'D':
|
|
112
|
+
return mod === 5 ? { name: KEY.CTRL_LEFT } : { name: KEY.LEFT };
|
|
113
|
+
case 'H':
|
|
114
|
+
return { name: KEY.HOME };
|
|
115
|
+
case 'F':
|
|
116
|
+
return { name: KEY.END };
|
|
117
|
+
case '~': {
|
|
118
|
+
const n = parseInt(params[0] || '0', 10);
|
|
119
|
+
if (n === 3) return { name: KEY.DELETE };
|
|
120
|
+
if (n === 5) return { name: KEY.PAGE_UP };
|
|
121
|
+
if (n === 6) return { name: KEY.PAGE_DOWN };
|
|
122
|
+
if (n === 1 || n === 7) return { name: KEY.HOME };
|
|
123
|
+
if (n === 4 || n === 8) return { name: KEY.END };
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (seq.startsWith('\x1bO')) {
|
|
133
|
+
const c = seq[2];
|
|
134
|
+
if (c === 'H') return { name: KEY.HOME };
|
|
135
|
+
if (c === 'F') return { name: KEY.END };
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return { name: KEY.ESC };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { KEY, decodePlain, decodeEscSequence };
|