@nexrall/code-core 1.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.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/agent/agentTypes.d.ts +17 -0
- package/dist/agent/agentTypes.d.ts.map +1 -0
- package/dist/agent/agentTypes.js +156 -0
- package/dist/agent/loop.d.ts +3 -0
- package/dist/agent/loop.d.ts.map +1 -0
- package/dist/agent/loop.js +775 -0
- package/dist/api/client.d.ts +27 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +414 -0
- package/dist/auth/index.d.ts +7 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +95 -0
- package/dist/checkpoint/manager.d.ts +86 -0
- package/dist/checkpoint/manager.d.ts.map +1 -0
- package/dist/checkpoint/manager.js +409 -0
- package/dist/commands/loader.d.ts +18 -0
- package/dist/commands/loader.d.ts.map +1 -0
- package/dist/commands/loader.js +183 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/mcp/client.d.ts +46 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +128 -0
- package/dist/mcp/httpClient.d.ts +25 -0
- package/dist/mcp/httpClient.d.ts.map +1 -0
- package/dist/mcp/httpClient.js +143 -0
- package/dist/mcp/manager.d.ts +56 -0
- package/dist/mcp/manager.d.ts.map +1 -0
- package/dist/mcp/manager.js +234 -0
- package/dist/mcp/sseClient.d.ts +30 -0
- package/dist/mcp/sseClient.d.ts.map +1 -0
- package/dist/mcp/sseClient.js +224 -0
- package/dist/permissions/rules.d.ts +20 -0
- package/dist/permissions/rules.d.ts.map +1 -0
- package/dist/permissions/rules.js +218 -0
- package/dist/plugins/index.d.ts +18 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +139 -0
- package/dist/tools/executor.d.ts +6 -0
- package/dist/tools/executor.d.ts.map +1 -0
- package/dist/tools/executor.js +1458 -0
- package/dist/tools/sandbox.d.ts +13 -0
- package/dist/tools/sandbox.d.ts.map +1 -0
- package/dist/tools/sandbox.js +140 -0
- package/dist/tools/symbols.d.ts +13 -0
- package/dist/tools/symbols.d.ts.map +1 -0
- package/dist/tools/symbols.js +279 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/package.json +92 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CheckpointManager = void 0;
|
|
37
|
+
exports.isMutatingTool = isMutatingTool;
|
|
38
|
+
exports.isPotentiallyMutatingBash = isPotentiallyMutatingBash;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const crypto = __importStar(require("crypto"));
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
44
|
+
// ─── Checkpoint / Rewind ────────────────────────────────────────────────────
|
|
45
|
+
//
|
|
46
|
+
// Snapshots the pre-mutation state of every file a tool is about to change, so
|
|
47
|
+
// the user can rewind the filesystem (and conversation) back to any earlier
|
|
48
|
+
// turn — the same safety net Claude Code's /rewind provides.
|
|
49
|
+
//
|
|
50
|
+
// A "turn" maps 1:1 to a user message. The client calls beginTurn() before the
|
|
51
|
+
// agent runs, the loop records file snapshots as mutating tools fire, and
|
|
52
|
+
// commitTurn() seals the turn into history. restore(id) rolls the working tree
|
|
53
|
+
// back to the state it had just BEFORE that turn ran.
|
|
54
|
+
//
|
|
55
|
+
// Persistence: turns are written to ~/.nexrall/checkpoints/<key>/ on commit,
|
|
56
|
+
// where <key> hashes workDir + an optional session scope. A reload of the CLI
|
|
57
|
+
// or the VS Code window therefore no longer wipes rewind history — resuming
|
|
58
|
+
// the same session in the same directory restores the full checkpoint list.
|
|
59
|
+
//
|
|
60
|
+
// Bash awareness: file snapshots can only cover tool-driven edits. Shell
|
|
61
|
+
// commands (sed -i, git checkout, codemods…) mutate files invisibly. The loop
|
|
62
|
+
// calls recordBashCommand() for potentially-mutating bash; we (a) count them
|
|
63
|
+
// so the UI can warn that a rewind does NOT undo them, and (b) if the workDir
|
|
64
|
+
// is a git repo with uncommitted changes, capture a `git stash create` commit
|
|
65
|
+
// hash as a manual recovery point (it creates a dangling commit WITHOUT
|
|
66
|
+
// touching the working tree).
|
|
67
|
+
const MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024; // 2 MB/file — larger files are noted but not stored
|
|
68
|
+
const MAX_PERSISTED_TURNS = 50; // pruned oldest-first on commit
|
|
69
|
+
const MUTATING_TOOLS = new Set([
|
|
70
|
+
'write_file',
|
|
71
|
+
'edit_file',
|
|
72
|
+
'multi_edit',
|
|
73
|
+
'delete_file',
|
|
74
|
+
'create_directory',
|
|
75
|
+
'notebook_edit',
|
|
76
|
+
'move_file',
|
|
77
|
+
'copy_file',
|
|
78
|
+
]);
|
|
79
|
+
function isMutatingTool(name) {
|
|
80
|
+
return MUTATING_TOOLS.has(name);
|
|
81
|
+
}
|
|
82
|
+
// Read-only bash commands that can never mutate the working tree. Anything not
|
|
83
|
+
// matching this (or containing redirects / in-place edit flags) is treated as
|
|
84
|
+
// potentially mutating. Deliberately conservative — false "mutating" only adds
|
|
85
|
+
// a warning, false "read-only" hides one.
|
|
86
|
+
const READONLY_BASH = /^\s*(ls|cat|head|tail|wc|pwd|echo|printf|grep|rg|fd|find|which|whoami|date|env|printenv|du|df|file|stat|tree|less|more|diff|cmp|md5|md5sum|shasum|basename|dirname|realpath|uname|ps|top|history|type|command|git\s+(status|log|diff|show|branch|remote|rev-parse|ls-files|blame|describe|shortlog|stash\s+list)|npm\s+(ls|list|view|info|outdated|test|run\s+test\S*)|npx\s+tsc(\s|$)|tsc(\s|$)|node\s+--version|python3?\s+--version|go\s+(vet|version)|cargo\s+(check|--version))\b[^>|;&]*$/;
|
|
87
|
+
function isPotentiallyMutatingBash(command) {
|
|
88
|
+
const cmd = command.trim();
|
|
89
|
+
if (!cmd)
|
|
90
|
+
return false;
|
|
91
|
+
// Multi-line or chained commands: assume mutating unless every segment is read-only.
|
|
92
|
+
const segments = cmd.split(/\n|&&|\|\||;/).map((s) => s.trim()).filter(Boolean);
|
|
93
|
+
return segments.some((seg) => !READONLY_BASH.test(seg));
|
|
94
|
+
}
|
|
95
|
+
function str(v) {
|
|
96
|
+
return typeof v === 'string' ? v : '';
|
|
97
|
+
}
|
|
98
|
+
/** Paths a tool is about to create/modify/remove — used to snapshot pre-state. */
|
|
99
|
+
function affectedPaths(name, input) {
|
|
100
|
+
switch (name) {
|
|
101
|
+
case 'write_file':
|
|
102
|
+
case 'edit_file':
|
|
103
|
+
case 'multi_edit':
|
|
104
|
+
case 'delete_file':
|
|
105
|
+
case 'create_directory':
|
|
106
|
+
case 'notebook_edit':
|
|
107
|
+
return [str(input.path)].filter(Boolean);
|
|
108
|
+
case 'move_file':
|
|
109
|
+
return [str(input.source), str(input.dest)].filter(Boolean);
|
|
110
|
+
case 'copy_file':
|
|
111
|
+
return [str(input.destination)].filter(Boolean);
|
|
112
|
+
default:
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
class CheckpointManager {
|
|
117
|
+
/**
|
|
118
|
+
* @param workDir project root the snapshots are relative to
|
|
119
|
+
* @param scope optional namespace (e.g. session id) so different sessions in
|
|
120
|
+
* the same directory keep independent rewind histories
|
|
121
|
+
*/
|
|
122
|
+
constructor(workDir, scope) {
|
|
123
|
+
this.workDir = workDir;
|
|
124
|
+
this.turns = [];
|
|
125
|
+
this.current = null;
|
|
126
|
+
this.nextId = 1;
|
|
127
|
+
const key = crypto
|
|
128
|
+
.createHash('sha1')
|
|
129
|
+
.update(path.resolve(workDir) + '\u0000' + (scope ?? ''))
|
|
130
|
+
.digest('hex')
|
|
131
|
+
.slice(0, 16);
|
|
132
|
+
// NEXRALL_CHECKPOINT_DIR overrides the store root (used by tests to avoid
|
|
133
|
+
// polluting the real ~/.nexrall/checkpoints).
|
|
134
|
+
const base = process.env.NEXRALL_CHECKPOINT_DIR || path.join(os.homedir(), '.nexrall', 'checkpoints');
|
|
135
|
+
this.storeDir = path.join(base, key);
|
|
136
|
+
this.loadFromDisk();
|
|
137
|
+
}
|
|
138
|
+
resolve(p) {
|
|
139
|
+
return path.isAbsolute(p) ? p : path.resolve(this.workDir, p);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Open a new turn. `label` is a short description (usually the user prompt),
|
|
143
|
+
* `messageIndex` is the conversation length the turn should rewind to.
|
|
144
|
+
*/
|
|
145
|
+
beginTurn(label, messageIndex) {
|
|
146
|
+
// Seal any turn left open by an aborted run.
|
|
147
|
+
this.commitTurn();
|
|
148
|
+
this.current = {
|
|
149
|
+
id: this.nextId++,
|
|
150
|
+
label: label.replace(/\s+/g, ' ').trim().slice(0, 80) || '(untitled turn)',
|
|
151
|
+
messageIndex,
|
|
152
|
+
fileCount: 0,
|
|
153
|
+
createdAt: new Date().toISOString(),
|
|
154
|
+
bashCount: 0,
|
|
155
|
+
snapshots: new Map(),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Snapshot the pre-mutation state of the files a tool is about to touch.
|
|
160
|
+
* Must be called BEFORE the tool executes. No-op for non-mutating tools or
|
|
161
|
+
* when no turn is open. First snapshot of a path within a turn wins (so the
|
|
162
|
+
* earliest pre-state is preserved even if the file is edited repeatedly).
|
|
163
|
+
*/
|
|
164
|
+
recordBeforeMutation(name, input) {
|
|
165
|
+
if (!this.current)
|
|
166
|
+
return;
|
|
167
|
+
if (name === 'bash') {
|
|
168
|
+
this.recordBashCommand(str(input.command));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!isMutatingTool(name))
|
|
172
|
+
return;
|
|
173
|
+
for (const rel of affectedPaths(name, input)) {
|
|
174
|
+
const abs = this.resolve(rel);
|
|
175
|
+
if (this.current.snapshots.has(abs))
|
|
176
|
+
continue;
|
|
177
|
+
let existed = false;
|
|
178
|
+
let content = null;
|
|
179
|
+
try {
|
|
180
|
+
const st = fs.statSync(abs);
|
|
181
|
+
if (st.isFile()) {
|
|
182
|
+
existed = true;
|
|
183
|
+
// Read as Buffer (binary-safe) — utf-8 readFileSync corrupts binary files
|
|
184
|
+
content = st.size <= MAX_SNAPSHOT_BYTES ? fs.readFileSync(abs) : null;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Directory or special file — record existence so we don't delete it on restore.
|
|
188
|
+
existed = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
existed = false; // path is absent → restore = delete
|
|
193
|
+
}
|
|
194
|
+
this.current.snapshots.set(abs, { absPath: abs, existed, content });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Record a bash command about to run in this turn. If it looks mutating,
|
|
199
|
+
* bump the turn's bash counter (so rewind can warn) and — on the FIRST
|
|
200
|
+
* mutating bash of the turn, if workDir is a dirty git repo — capture a
|
|
201
|
+
* `git stash create` hash as a manual recovery point. `git stash create`
|
|
202
|
+
* writes a dangling commit and does NOT touch the working tree or the stash
|
|
203
|
+
* list, so it is side-effect-free from the user's perspective.
|
|
204
|
+
*/
|
|
205
|
+
recordBashCommand(command) {
|
|
206
|
+
if (!this.current || !isPotentiallyMutatingBash(command))
|
|
207
|
+
return;
|
|
208
|
+
this.current.bashCount++;
|
|
209
|
+
if (this.current.bashCount === 1 && !this.current.gitStashHash) {
|
|
210
|
+
this.current.gitStashHash = this.tryGitStashCreate() ?? undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
tryGitStashCreate() {
|
|
214
|
+
try {
|
|
215
|
+
const r = (0, child_process_1.spawnSync)('git', ['stash', 'create', 'nexrall-checkpoint'], {
|
|
216
|
+
cwd: this.workDir,
|
|
217
|
+
encoding: 'utf-8',
|
|
218
|
+
timeout: 10000,
|
|
219
|
+
});
|
|
220
|
+
if (r.status !== 0)
|
|
221
|
+
return null;
|
|
222
|
+
const hash = (r.stdout ?? '').trim();
|
|
223
|
+
// Empty output = clean tree (nothing to snapshot). A dangling commit is
|
|
224
|
+
// GC-eligible after ~2 weeks, which is plenty for a recovery window.
|
|
225
|
+
return /^[0-9a-f]{7,40}$/.test(hash) ? hash : null;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Seal the open turn into history. Empty turns (no mutations, no bash) are discarded. */
|
|
232
|
+
commitTurn() {
|
|
233
|
+
if (this.current && (this.current.snapshots.size > 0 || this.current.bashCount > 0)) {
|
|
234
|
+
this.current.fileCount = this.current.snapshots.size;
|
|
235
|
+
this.turns.push(this.current);
|
|
236
|
+
this.persistTurn(this.current);
|
|
237
|
+
this.prune();
|
|
238
|
+
this.persistIndex();
|
|
239
|
+
}
|
|
240
|
+
this.current = null;
|
|
241
|
+
}
|
|
242
|
+
list() {
|
|
243
|
+
return this.turns.map((t) => ({
|
|
244
|
+
id: t.id,
|
|
245
|
+
label: t.label,
|
|
246
|
+
messageIndex: t.messageIndex,
|
|
247
|
+
fileCount: t.fileCount,
|
|
248
|
+
createdAt: t.createdAt,
|
|
249
|
+
bashCount: t.bashCount,
|
|
250
|
+
gitStashHash: t.gitStashHash,
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
hasCheckpoints() {
|
|
254
|
+
return this.turns.length > 0;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Roll the working tree back to the state it had just before the checkpoint
|
|
258
|
+
* turn ran. Snapshots are applied newest → target so the earliest pre-state
|
|
259
|
+
* wins for any path touched in multiple turns. Returns the conversation
|
|
260
|
+
* truncation point, or null if the id is unknown.
|
|
261
|
+
*/
|
|
262
|
+
restore(checkpointId) {
|
|
263
|
+
const idx = this.turns.findIndex((t) => t.id === checkpointId);
|
|
264
|
+
if (idx === -1)
|
|
265
|
+
return null;
|
|
266
|
+
const restored = new Set();
|
|
267
|
+
let bashCount = 0;
|
|
268
|
+
const gitStashHashes = [];
|
|
269
|
+
for (let i = this.turns.length - 1; i >= idx; i--) {
|
|
270
|
+
const turn = this.turns[i];
|
|
271
|
+
for (const snap of turn.snapshots.values()) {
|
|
272
|
+
this.applySnapshot(snap);
|
|
273
|
+
restored.add(snap.absPath);
|
|
274
|
+
}
|
|
275
|
+
bashCount += turn.bashCount;
|
|
276
|
+
if (turn.gitStashHash)
|
|
277
|
+
gitStashHashes.push(turn.gitStashHash);
|
|
278
|
+
}
|
|
279
|
+
const messageIndex = this.turns[idx].messageIndex;
|
|
280
|
+
const removed = this.turns.slice(idx);
|
|
281
|
+
this.turns = this.turns.slice(0, idx); // forget rewound (and later) turns
|
|
282
|
+
this.current = null;
|
|
283
|
+
for (const t of removed)
|
|
284
|
+
this.deleteTurnFile(t.id);
|
|
285
|
+
this.persistIndex();
|
|
286
|
+
return { restored: [...restored], messageIndex, bashCount, gitStashHashes };
|
|
287
|
+
}
|
|
288
|
+
applySnapshot(snap) {
|
|
289
|
+
try {
|
|
290
|
+
if (!snap.existed) {
|
|
291
|
+
// Path was absent before the mutation → remove whatever is there now.
|
|
292
|
+
if (fs.existsSync(snap.absPath)) {
|
|
293
|
+
const st = fs.statSync(snap.absPath);
|
|
294
|
+
if (st.isFile())
|
|
295
|
+
fs.unlinkSync(snap.absPath);
|
|
296
|
+
else {
|
|
297
|
+
try {
|
|
298
|
+
fs.rmdirSync(snap.absPath);
|
|
299
|
+
}
|
|
300
|
+
catch { /* non-empty dir — leave it */ }
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else if (snap.content !== null) {
|
|
305
|
+
fs.mkdirSync(path.dirname(snap.absPath), { recursive: true });
|
|
306
|
+
// Write as Buffer — content was captured as Buffer to handle binary files
|
|
307
|
+
fs.writeFileSync(snap.absPath, snap.content);
|
|
308
|
+
}
|
|
309
|
+
// existed but content === null (dir / oversized file): nothing safe to restore.
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
/* best-effort restore */
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// ─── Disk persistence (best-effort — checkpointing must never crash the agent) ──
|
|
316
|
+
turnFile(id) {
|
|
317
|
+
return path.join(this.storeDir, `turn-${id}.json`);
|
|
318
|
+
}
|
|
319
|
+
persistTurn(turn) {
|
|
320
|
+
try {
|
|
321
|
+
fs.mkdirSync(this.storeDir, { recursive: true });
|
|
322
|
+
const persisted = {
|
|
323
|
+
id: turn.id,
|
|
324
|
+
label: turn.label,
|
|
325
|
+
messageIndex: turn.messageIndex,
|
|
326
|
+
fileCount: turn.fileCount,
|
|
327
|
+
createdAt: turn.createdAt,
|
|
328
|
+
bashCount: turn.bashCount,
|
|
329
|
+
gitStashHash: turn.gitStashHash,
|
|
330
|
+
snapshots: [...turn.snapshots.values()].map((s) => ({
|
|
331
|
+
absPath: s.absPath,
|
|
332
|
+
existed: s.existed,
|
|
333
|
+
content: s.content ? s.content.toString('base64') : null,
|
|
334
|
+
})),
|
|
335
|
+
};
|
|
336
|
+
const tmp = this.turnFile(turn.id) + '.tmp';
|
|
337
|
+
fs.writeFileSync(tmp, JSON.stringify(persisted));
|
|
338
|
+
fs.renameSync(tmp, this.turnFile(turn.id));
|
|
339
|
+
}
|
|
340
|
+
catch { /* best-effort */ }
|
|
341
|
+
}
|
|
342
|
+
persistIndex() {
|
|
343
|
+
try {
|
|
344
|
+
fs.mkdirSync(this.storeDir, { recursive: true });
|
|
345
|
+
const index = {
|
|
346
|
+
version: 1,
|
|
347
|
+
workDir: path.resolve(this.workDir),
|
|
348
|
+
nextId: this.nextId,
|
|
349
|
+
turnIds: this.turns.map((t) => t.id),
|
|
350
|
+
};
|
|
351
|
+
const tmp = path.join(this.storeDir, 'index.json.tmp');
|
|
352
|
+
fs.writeFileSync(tmp, JSON.stringify(index));
|
|
353
|
+
fs.renameSync(tmp, path.join(this.storeDir, 'index.json'));
|
|
354
|
+
}
|
|
355
|
+
catch { /* best-effort */ }
|
|
356
|
+
}
|
|
357
|
+
deleteTurnFile(id) {
|
|
358
|
+
try {
|
|
359
|
+
fs.unlinkSync(this.turnFile(id));
|
|
360
|
+
}
|
|
361
|
+
catch { /* already gone */ }
|
|
362
|
+
}
|
|
363
|
+
prune() {
|
|
364
|
+
while (this.turns.length > MAX_PERSISTED_TURNS) {
|
|
365
|
+
const dropped = this.turns.shift();
|
|
366
|
+
this.deleteTurnFile(dropped.id);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
loadFromDisk() {
|
|
370
|
+
try {
|
|
371
|
+
const raw = fs.readFileSync(path.join(this.storeDir, 'index.json'), 'utf-8');
|
|
372
|
+
const index = JSON.parse(raw);
|
|
373
|
+
if (index.version !== 1 || !Array.isArray(index.turnIds))
|
|
374
|
+
return;
|
|
375
|
+
this.nextId = Number.isFinite(index.nextId) && index.nextId > 0 ? index.nextId : 1;
|
|
376
|
+
for (const id of index.turnIds) {
|
|
377
|
+
try {
|
|
378
|
+
const t = JSON.parse(fs.readFileSync(this.turnFile(id), 'utf-8'));
|
|
379
|
+
const snapshots = new Map();
|
|
380
|
+
for (const s of t.snapshots ?? []) {
|
|
381
|
+
snapshots.set(s.absPath, {
|
|
382
|
+
absPath: s.absPath,
|
|
383
|
+
existed: !!s.existed,
|
|
384
|
+
content: typeof s.content === 'string' ? Buffer.from(s.content, 'base64') : null,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
this.turns.push({
|
|
388
|
+
id: t.id,
|
|
389
|
+
label: t.label ?? '(untitled turn)',
|
|
390
|
+
messageIndex: t.messageIndex ?? 0,
|
|
391
|
+
fileCount: t.fileCount ?? snapshots.size,
|
|
392
|
+
createdAt: t.createdAt ?? new Date().toISOString(),
|
|
393
|
+
bashCount: t.bashCount ?? 0,
|
|
394
|
+
gitStashHash: t.gitStashHash,
|
|
395
|
+
snapshots,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
catch { /* skip corrupt turn file */ }
|
|
399
|
+
}
|
|
400
|
+
// Guard against a stale index: nextId must exceed every loaded id.
|
|
401
|
+
const maxId = this.turns.reduce((m, t) => Math.max(m, t.id), 0);
|
|
402
|
+
if (this.nextId <= maxId)
|
|
403
|
+
this.nextId = maxId + 1;
|
|
404
|
+
}
|
|
405
|
+
catch { /* no persisted state — fresh start */ }
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
exports.CheckpointManager = CheckpointManager;
|
|
409
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SlashCommand {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
model?: 'turbo' | 'pro' | 'ultra';
|
|
5
|
+
mode?: string;
|
|
6
|
+
body: string;
|
|
7
|
+
source: 'project' | 'global' | 'builtin' | 'plugin';
|
|
8
|
+
}
|
|
9
|
+
/** Discover all custom slash commands (project overrides global overrides builtin). */
|
|
10
|
+
export declare function loadSlashCommands(workDir: string): SlashCommand[];
|
|
11
|
+
export declare function findSlashCommand(cmds: SlashCommand[], name: string): SlashCommand | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Expand a command body into a final prompt: argument substitution, @file
|
|
14
|
+
* inlining, and !`cmd` shell capture. Shell/file errors are inlined as notes
|
|
15
|
+
* rather than thrown so the prompt is always produced.
|
|
16
|
+
*/
|
|
17
|
+
export declare function expandCommand(cmd: SlashCommand, argString: string, workDir: string): string;
|
|
18
|
+
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/commands/loader.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;CACrD;AAiFD,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,CASjE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAG7F;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAuC3F"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadSlashCommands = loadSlashCommands;
|
|
37
|
+
exports.findSlashCommand = findSlashCommand;
|
|
38
|
+
exports.expandCommand = expandCommand;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const index_1 = require("../plugins/index");
|
|
44
|
+
// ── Built-in commands ─────────────────────────────────────────────────────────
|
|
45
|
+
// Shipped with the product; lowest precedence (project > global > builtin), so
|
|
46
|
+
// a user can override any of them by creating a file with the same name.
|
|
47
|
+
const BUILTIN_COMMANDS = [
|
|
48
|
+
{
|
|
49
|
+
name: 'review',
|
|
50
|
+
description: 'Review uncommitted changes (or a PR/branch diff) for bugs and risks',
|
|
51
|
+
source: 'builtin',
|
|
52
|
+
body: [
|
|
53
|
+
'Review the following diff like a meticulous senior engineer. Target: $ARGUMENTS',
|
|
54
|
+
'(If no target given, review the uncommitted working-tree changes below. If a branch or PR',
|
|
55
|
+
'number is given, run the appropriate `git diff <base>...` or `gh pr diff <n>` yourself first.)',
|
|
56
|
+
'',
|
|
57
|
+
'Branch: !`git branch --show-current`',
|
|
58
|
+
'Status: !`git status --short`',
|
|
59
|
+
'',
|
|
60
|
+
'Diff (uncommitted):',
|
|
61
|
+
'```diff',
|
|
62
|
+
'!`git diff HEAD --unified=5 --no-color | head -4000`',
|
|
63
|
+
'```',
|
|
64
|
+
'',
|
|
65
|
+
'Review methodology:',
|
|
66
|
+
'1. Read the surrounding code of every changed hunk (read_file with offset/limit) — never judge a hunk in isolation.',
|
|
67
|
+
'2. Look for: correctness bugs, edge cases (empty/null/unicode/concurrency), security issues',
|
|
68
|
+
' (injection, path traversal, secrets), breaking API changes (find_references / search callers),',
|
|
69
|
+
' silent behaviour changes, and missing error handling.',
|
|
70
|
+
'3. Check tests: do existing tests cover the change? Are assertions weakened?',
|
|
71
|
+
'',
|
|
72
|
+
'Output format:',
|
|
73
|
+
'- 🔴 Critical (must fix before merge) — with file:line and a concrete fix',
|
|
74
|
+
'- 🟡 Warning (should fix) — with file:line',
|
|
75
|
+
'- 🟢 Suggestion (nice to have)',
|
|
76
|
+
'- Verdict: APPROVE / REQUEST CHANGES with a one-paragraph summary.',
|
|
77
|
+
'Do NOT modify any files — this is a read-only review.',
|
|
78
|
+
].join('\n'),
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
function parseFrontmatter(raw) {
|
|
82
|
+
const m = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
|
|
83
|
+
if (!m)
|
|
84
|
+
return { meta: {}, body: raw.trim() };
|
|
85
|
+
const meta = {};
|
|
86
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
87
|
+
const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
|
|
88
|
+
if (kv)
|
|
89
|
+
meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, '');
|
|
90
|
+
}
|
|
91
|
+
return { meta, body: (m[2] ?? '').trim() };
|
|
92
|
+
}
|
|
93
|
+
function loadDir(dir, source, into) {
|
|
94
|
+
let files;
|
|
95
|
+
try {
|
|
96
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
104
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
105
|
+
const name = (meta.name || path.basename(file, '.md')).trim().toLowerCase();
|
|
106
|
+
if (!name)
|
|
107
|
+
continue;
|
|
108
|
+
if (source !== 'project' && into.has(name))
|
|
109
|
+
continue; // earlier tiers win
|
|
110
|
+
const model = ['turbo', 'pro', 'ultra'].find((x) => x === (meta.model ?? '').toLowerCase());
|
|
111
|
+
into.set(name, {
|
|
112
|
+
name,
|
|
113
|
+
description: meta.description || `Custom /${name} command`,
|
|
114
|
+
model,
|
|
115
|
+
mode: meta.mode || undefined,
|
|
116
|
+
body,
|
|
117
|
+
source,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* skip malformed */
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Discover all custom slash commands (project overrides global overrides builtin). */
|
|
126
|
+
function loadSlashCommands(workDir) {
|
|
127
|
+
const out = new Map();
|
|
128
|
+
loadDir(path.join(workDir, '.nexrall', 'commands'), 'project', out);
|
|
129
|
+
loadDir(path.join(os.homedir(), '.nexrall', 'commands'), 'global', out);
|
|
130
|
+
for (const dir of (0, index_1.pluginAssetDirs)(workDir, 'commands'))
|
|
131
|
+
loadDir(dir, 'plugin', out);
|
|
132
|
+
for (const cmd of BUILTIN_COMMANDS) {
|
|
133
|
+
if (!out.has(cmd.name))
|
|
134
|
+
out.set(cmd.name, cmd);
|
|
135
|
+
}
|
|
136
|
+
return [...out.values()];
|
|
137
|
+
}
|
|
138
|
+
function findSlashCommand(cmds, name) {
|
|
139
|
+
const want = name.replace(/^\//, '').trim().toLowerCase();
|
|
140
|
+
return cmds.find((c) => c.name === want);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Expand a command body into a final prompt: argument substitution, @file
|
|
144
|
+
* inlining, and !`cmd` shell capture. Shell/file errors are inlined as notes
|
|
145
|
+
* rather than thrown so the prompt is always produced.
|
|
146
|
+
*/
|
|
147
|
+
function expandCommand(cmd, argString, workDir) {
|
|
148
|
+
const args = argString.trim();
|
|
149
|
+
const positional = args ? args.split(/\s+/) : [];
|
|
150
|
+
let out = cmd.body;
|
|
151
|
+
// !`shell command` → captured stdout
|
|
152
|
+
out = out.replace(/!`([^`]+)`/g, (_m, c) => {
|
|
153
|
+
try {
|
|
154
|
+
const stdout = (0, child_process_1.execSync)(c, { cwd: workDir, encoding: 'utf-8', timeout: 15000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
155
|
+
return stdout.trim();
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
return `[command failed: ${c} — ${err.message}]`;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
// @path → inlined file contents
|
|
162
|
+
out = out.replace(/(^|\s)@([^\s]+)/g, (_m, lead, rel) => {
|
|
163
|
+
const abs = path.isAbsolute(rel) ? rel : path.join(workDir, rel);
|
|
164
|
+
try {
|
|
165
|
+
const content = fs.readFileSync(abs, 'utf-8').slice(0, 12000);
|
|
166
|
+
return `${lead}\n[File: ${rel}]\n\`\`\`\n${content}\n\`\`\`\n`;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return `${lead}[missing file: ${rel}]`;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
// $1, $2 … positional args
|
|
173
|
+
out = out.replace(/\$(\d+)/g, (_m, n) => positional[Number(n) - 1] ?? '');
|
|
174
|
+
// $ARGUMENTS → all args
|
|
175
|
+
const hadArgsToken = /\$ARGUMENTS/.test(out);
|
|
176
|
+
out = out.replace(/\$ARGUMENTS/g, args);
|
|
177
|
+
// If the template never referenced its args, append them so they aren't lost.
|
|
178
|
+
if (!hadArgsToken && !/\$\d+/.test(cmd.body) && args) {
|
|
179
|
+
out = `${out}\n\n${args}`;
|
|
180
|
+
}
|
|
181
|
+
return out.trim();
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=loader.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './auth/index';
|
|
3
|
+
export * from './api/client';
|
|
4
|
+
export * from './tools/executor';
|
|
5
|
+
export * from './agent/loop';
|
|
6
|
+
export * from './mcp/client';
|
|
7
|
+
export * from './mcp/httpClient';
|
|
8
|
+
export * from './mcp/manager';
|
|
9
|
+
export * from './checkpoint/manager';
|
|
10
|
+
export * from './commands/loader';
|
|
11
|
+
export * from './agent/agentTypes';
|
|
12
|
+
export * from './permissions/rules';
|
|
13
|
+
export * from './plugins/index';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./auth/index"), exports);
|
|
19
|
+
__exportStar(require("./api/client"), exports);
|
|
20
|
+
__exportStar(require("./tools/executor"), exports);
|
|
21
|
+
__exportStar(require("./agent/loop"), exports);
|
|
22
|
+
__exportStar(require("./mcp/client"), exports);
|
|
23
|
+
__exportStar(require("./mcp/httpClient"), exports);
|
|
24
|
+
__exportStar(require("./mcp/manager"), exports);
|
|
25
|
+
__exportStar(require("./checkpoint/manager"), exports);
|
|
26
|
+
__exportStar(require("./commands/loader"), exports);
|
|
27
|
+
__exportStar(require("./agent/agentTypes"), exports);
|
|
28
|
+
__exportStar(require("./permissions/rules"), exports);
|
|
29
|
+
__exportStar(require("./plugins/index"), exports);
|
|
30
|
+
//# sourceMappingURL=index.js.map
|