@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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/dist/agent/agentTypes.d.ts +17 -0
  4. package/dist/agent/agentTypes.d.ts.map +1 -0
  5. package/dist/agent/agentTypes.js +156 -0
  6. package/dist/agent/loop.d.ts +3 -0
  7. package/dist/agent/loop.d.ts.map +1 -0
  8. package/dist/agent/loop.js +775 -0
  9. package/dist/api/client.d.ts +27 -0
  10. package/dist/api/client.d.ts.map +1 -0
  11. package/dist/api/client.js +414 -0
  12. package/dist/auth/index.d.ts +7 -0
  13. package/dist/auth/index.d.ts.map +1 -0
  14. package/dist/auth/index.js +95 -0
  15. package/dist/checkpoint/manager.d.ts +86 -0
  16. package/dist/checkpoint/manager.d.ts.map +1 -0
  17. package/dist/checkpoint/manager.js +409 -0
  18. package/dist/commands/loader.d.ts +18 -0
  19. package/dist/commands/loader.d.ts.map +1 -0
  20. package/dist/commands/loader.js +183 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +30 -0
  24. package/dist/mcp/client.d.ts +46 -0
  25. package/dist/mcp/client.d.ts.map +1 -0
  26. package/dist/mcp/client.js +128 -0
  27. package/dist/mcp/httpClient.d.ts +25 -0
  28. package/dist/mcp/httpClient.d.ts.map +1 -0
  29. package/dist/mcp/httpClient.js +143 -0
  30. package/dist/mcp/manager.d.ts +56 -0
  31. package/dist/mcp/manager.d.ts.map +1 -0
  32. package/dist/mcp/manager.js +234 -0
  33. package/dist/mcp/sseClient.d.ts +30 -0
  34. package/dist/mcp/sseClient.d.ts.map +1 -0
  35. package/dist/mcp/sseClient.js +224 -0
  36. package/dist/permissions/rules.d.ts +20 -0
  37. package/dist/permissions/rules.d.ts.map +1 -0
  38. package/dist/permissions/rules.js +218 -0
  39. package/dist/plugins/index.d.ts +18 -0
  40. package/dist/plugins/index.d.ts.map +1 -0
  41. package/dist/plugins/index.js +139 -0
  42. package/dist/tools/executor.d.ts +6 -0
  43. package/dist/tools/executor.d.ts.map +1 -0
  44. package/dist/tools/executor.js +1458 -0
  45. package/dist/tools/sandbox.d.ts +13 -0
  46. package/dist/tools/sandbox.d.ts.map +1 -0
  47. package/dist/tools/sandbox.js +140 -0
  48. package/dist/tools/symbols.d.ts +13 -0
  49. package/dist/tools/symbols.d.ts.map +1 -0
  50. package/dist/tools/symbols.js +279 -0
  51. package/dist/types.d.ts +186 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +4 -0
  54. 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
@@ -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