@nexrall/code-core 1.4.12 → 1.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,243 @@
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.MEMORY_HARD_CAP_BYTES = exports.MEMORY_COMPACT_TRIGGER_BYTES = exports.MEMORY_MAX_BYTES = exports.MEMORY_ENTRY_MAX_CHARS = void 0;
37
+ exports.memoryFilePath = memoryFilePath;
38
+ exports.writeMemory = writeMemory;
39
+ exports.readMemory = readMemory;
40
+ exports.readAllMemory = readAllMemory;
41
+ exports.clearMemory = clearMemory;
42
+ exports.memoryStats = memoryStats;
43
+ exports.compactMemoryIfNeeded = compactMemoryIfNeeded;
44
+ const fs = __importStar(require("fs"));
45
+ const os = __importStar(require("os"));
46
+ const path = __importStar(require("path"));
47
+ const crypto = __importStar(require("crypto"));
48
+ // ─── Persistent agent memory ───────────────────────────────────────────────
49
+ //
50
+ // Two scopes, mirroring nexrall.md's own global/project split:
51
+ // - 'project' (default) — facts specific to the repo currently open
52
+ // (tech stack, build/deploy commands, project-specific conventions).
53
+ // Stored per-workDir so an unrelated project never sees it.
54
+ // - 'global' — facts that hold across EVERY project (communication-style
55
+ // preferences, cross-cutting habits like "always confirm before force-push").
56
+ //
57
+ // Previously everything lived in one un-scoped ~/.nexrall/memory.md, read into
58
+ // EVERY session regardless of which project was open — a fact about one repo's
59
+ // deploy command showed up as context noise in a totally unrelated project, and
60
+ // the single file grew without bound (a real one measured 312KB / ~245 entries
61
+ // after a few weeks, entries averaging ~1000 chars despite the tool's own
62
+ // instruction to keep each one to "1-2 sentences").
63
+ //
64
+ // Fixes applied here (see MEMORY_MAX_BYTES / MEMORY_ENTRY_MAX_CHARS / compaction
65
+ // below): hard per-entry length cap (not just a prompt instruction), hard total
66
+ // file-size cap with LLM-summarizing compaction of the oldest half instead of
67
+ // blind truncation (which would silently discard facts instead of condensing
68
+ // them), and a real file lock around the read-dedupe-write sequence so two
69
+ // concurrent CLI/VS Code sessions can't race into a duplicate entry.
70
+ // NEXRALL_MEMORY_DIR overrides the store root — used by tests to avoid touching
71
+ // the real ~/.nexrall/memory (mirrors CheckpointManager's NEXRALL_CHECKPOINT_DIR).
72
+ const MEMORY_ROOT = process.env.NEXRALL_MEMORY_DIR || path.join(os.homedir(), '.nexrall', 'memory');
73
+ const GLOBAL_FILE = path.join(MEMORY_ROOT, 'global.md');
74
+ /** Per-workDir file, keyed the same way CheckpointManager keys its store
75
+ * (sha1 of the resolved absolute path) so two differently-named checkouts of
76
+ * the same repo don't collide, and the same repo always maps to the same file
77
+ * regardless of cwd casing/trailing-slash quirks. */
78
+ function memoryFilePath(scope, workDir) {
79
+ if (scope === 'global' || !workDir)
80
+ return GLOBAL_FILE;
81
+ const key = crypto.createHash('sha1').update(path.resolve(workDir)).digest('hex').slice(0, 16);
82
+ return path.join(MEMORY_ROOT, `project-${key}.md`);
83
+ }
84
+ exports.MEMORY_ENTRY_MAX_CHARS = 400; // hard cap per entry — the "1-2 sentences" instruction is a request, this is the backstop
85
+ exports.MEMORY_MAX_BYTES = 12000; // ~3k tokens — target size compaction summarizes DOWN TO
86
+ exports.MEMORY_COMPACT_TRIGGER_BYTES = 16000; // LLM compaction (see compactMemoryIfNeeded) fires once past this
87
+ // Emergency-only synchronous fallback inside writeMemory itself, MUCH higher than the compact
88
+ // trigger — gives compactMemoryIfNeeded plenty of headroom to run (it's called opportunistically,
89
+ // fire-and-forget, from loop.ts right after a memory_write) before this ever kicks in. Only bites
90
+ // if compaction has been unavailable/failing for a while (e.g. offline, API errors), so the file
91
+ // still can't grow completely unbounded even in that worst case.
92
+ exports.MEMORY_HARD_CAP_BYTES = 4 * exports.MEMORY_COMPACT_TRIGGER_BYTES;
93
+ const FINGERPRINT_LEN = 60; // near-duplicate heuristic: same as before, first N chars, case-insensitive
94
+ // ─── Simple per-file async lock ────────────────────────────────────────────
95
+ // Fixes the read→dedupe-check→append race: two concurrent memoryWrite() calls
96
+ // (two CLI processes, or a sub-agent + the main agent) could both read "fact
97
+ // not present yet" before either had written, and both append duplicates.
98
+ // In-process locking is enough for the common case (same CLI/VS Code process);
99
+ // cross-process races are rarer and, worst case, produce a duplicate line that
100
+ // the NEXT memory_write's dedupe check or a compaction pass will clean up.
101
+ const _locks = new Map();
102
+ async function withLock(key, fn) {
103
+ const prev = _locks.get(key) ?? Promise.resolve();
104
+ let release;
105
+ const next = new Promise((res) => { release = res; });
106
+ _locks.set(key, prev.then(() => next));
107
+ try {
108
+ await prev;
109
+ return await fn();
110
+ }
111
+ finally {
112
+ release();
113
+ if (_locks.get(key) === next)
114
+ _locks.delete(key);
115
+ }
116
+ }
117
+ function readMemoryFile(file) {
118
+ try {
119
+ return fs.readFileSync(file, 'utf-8');
120
+ }
121
+ catch {
122
+ return '';
123
+ }
124
+ }
125
+ function writeMemoryFile(file, content) {
126
+ fs.mkdirSync(path.dirname(file), { recursive: true });
127
+ fs.writeFileSync(file, content, 'utf-8');
128
+ }
129
+ /** Evict the OLDEST entries (top of file) until under the byte cap. Used as a
130
+ * cheap fallback when LLM compaction is unavailable/fails — see compactMemory
131
+ * below for the preferred summarizing path. */
132
+ function evictOldest(content, maxBytes) {
133
+ if (Buffer.byteLength(content, 'utf-8') <= maxBytes)
134
+ return content;
135
+ const lines = content.split('\n').filter((l) => l.trim().length > 0);
136
+ while (lines.length > 1 && Buffer.byteLength(lines.join('\n'), 'utf-8') > maxBytes)
137
+ lines.shift();
138
+ return '\n' + lines.join('\n');
139
+ }
140
+ async function writeMemory(content, scope, workDir) {
141
+ let trimmed = content.trim();
142
+ if (!trimmed)
143
+ return { ok: false, already: false, scope, file: '' };
144
+ if (trimmed.length > exports.MEMORY_ENTRY_MAX_CHARS)
145
+ trimmed = trimmed.slice(0, exports.MEMORY_ENTRY_MAX_CHARS - 1).trimEnd() + '…';
146
+ const file = memoryFilePath(scope, workDir);
147
+ return withLock(file, async () => {
148
+ const existing = readMemoryFile(file);
149
+ const fingerprint = trimmed.toLowerCase().slice(0, FINGERPRINT_LEN);
150
+ if (existing.toLowerCase().includes(fingerprint)) {
151
+ return { ok: true, already: true, scope, file };
152
+ }
153
+ const today = new Date().toISOString().slice(0, 10);
154
+ let next = existing + `\n- [${today}] ${trimmed}`;
155
+ // Emergency-only backstop — see MEMORY_HARD_CAP_BYTES. The EXPECTED bounding
156
+ // mechanism is compactMemoryIfNeeded (LLM summarization, called opportunistically
157
+ // right after this from loop.ts); this only fires if compaction hasn't kept up.
158
+ if (Buffer.byteLength(next, 'utf-8') > exports.MEMORY_HARD_CAP_BYTES) {
159
+ next = evictOldest(next, exports.MEMORY_MAX_BYTES);
160
+ }
161
+ writeMemoryFile(file, next);
162
+ return { ok: true, already: false, scope, file };
163
+ });
164
+ }
165
+ function readMemory(scope, workDir) {
166
+ const file = memoryFilePath(scope, workDir);
167
+ return readMemoryFile(file).trim();
168
+ }
169
+ /** Merge BOTH scopes for prompt injection — project-specific first (most
170
+ * relevant to what the agent is doing right now), global preferences after. */
171
+ function readAllMemory(workDir) {
172
+ const parts = [];
173
+ const globalMem = readMemory('global');
174
+ if (globalMem)
175
+ parts.push(`[Global memories — apply to every project]\n${globalMem}`);
176
+ if (workDir) {
177
+ const projMem = readMemory('project', workDir);
178
+ if (projMem)
179
+ parts.push(`[Project memories — ${path.basename(path.resolve(workDir))}]\n${projMem}`);
180
+ }
181
+ return parts.join('\n\n');
182
+ }
183
+ function clearMemory(scope, workDir) {
184
+ const file = memoryFilePath(scope, workDir);
185
+ try {
186
+ fs.rmSync(file, { force: true });
187
+ }
188
+ catch { /* ignore */ }
189
+ }
190
+ function memoryStats(scope, workDir) {
191
+ const file = memoryFilePath(scope, workDir);
192
+ const content = readMemoryFile(file);
193
+ const entries = content.split('\n').filter((l) => /^-\s\[\d{4}-\d{2}-\d{2}\]/.test(l)).length;
194
+ return { file, bytes: Buffer.byteLength(content, 'utf-8'), entries };
195
+ }
196
+ /**
197
+ * Summarize the OLDEST half of a memory file into a few consolidated bullets via
198
+ * an LLM call, keeping the most recent entries verbatim — same "compact, don't
199
+ * silently drop" instinct as the conversation-history auto-compactor, applied to
200
+ * memory instead of a transcript. This is the EXPECTED bounding mechanism for
201
+ * memory file size — called opportunistically (fire-and-forget) right after a
202
+ * successful memory_write (see maybeCompactMemory in loop.ts) once a file crosses
203
+ * MEMORY_COMPACT_TRIGGER_BYTES. writeMemory's own emergency synchronous eviction
204
+ * (MEMORY_HARD_CAP_BYTES) sits well above this threshold specifically so this
205
+ * summarizing pass gets the chance to run first in the common case; the blunt
206
+ * eviction is a last resort for if compaction has been failing/unavailable.
207
+ *
208
+ * `summarize` is injected (rather than importing streamChat directly) to avoid
209
+ * pulling api/client.ts's network/auth machinery into every consumer of this
210
+ * module and to keep this file trivially unit-testable.
211
+ */
212
+ async function compactMemoryIfNeeded(scope, workDir, summarize) {
213
+ const file = memoryFilePath(scope, workDir);
214
+ return withLock(file, async () => {
215
+ const content = readMemoryFile(file);
216
+ if (Buffer.byteLength(content, 'utf-8') <= exports.MEMORY_COMPACT_TRIGGER_BYTES)
217
+ return false;
218
+ const lines = content.split('\n').filter((l) => l.trim().length > 0);
219
+ if (lines.length < 8)
220
+ return false; // too few entries to bother — evictOldest already keeps it bounded
221
+ const cut = Math.floor(lines.length / 2);
222
+ const older = lines.slice(0, cut);
223
+ const recent = lines.slice(cut);
224
+ const prompt = `Consolidate these persistent-memory bullet entries into a shorter set of bullets, ` +
225
+ `merging duplicates/near-duplicates and dropping anything clearly stale or superseded ` +
226
+ `by a later entry. Keep each resulting bullet under ${exports.MEMORY_ENTRY_MAX_CHARS} characters, ` +
227
+ `one fact per line, prefixed "- [YYYY-MM-DD] " using the LATEST date among the entries it ` +
228
+ `draws from. Output ONLY the bullet lines, nothing else.\n\n${older.join('\n')}`;
229
+ let summarized;
230
+ try {
231
+ summarized = (await summarize(prompt)).trim();
232
+ }
233
+ catch {
234
+ return false; // summarization failed — leave the file as-is; the sync evictOldest cap still bounds it
235
+ }
236
+ if (!summarized)
237
+ return false;
238
+ const next = summarized + '\n' + recent.join('\n');
239
+ writeMemoryFile(file, Buffer.byteLength(next, 'utf-8') > exports.MEMORY_MAX_BYTES ? evictOldest(next, exports.MEMORY_MAX_BYTES) : next);
240
+ return true;
241
+ });
242
+ }
243
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1,35 @@
1
+ export interface Skill {
2
+ name: string;
3
+ description: string;
4
+ model?: 'turbo' | 'pro' | 'ultra';
5
+ mode?: string;
6
+ body: string;
7
+ source: 'project' | 'global' | 'builtin' | 'plugin';
8
+ /** Absolute path to the skill's own directory, for resolving supporting files. Undefined for flat command-style skills (no directory). */
9
+ dir?: string;
10
+ /** true → only `/name` invokes it; the model must not auto-trigger it (side-effecting workflows like /deploy). */
11
+ disableModelInvocation: boolean;
12
+ /** false → only the model may invoke it (auto-load only, not meant to be typed as a command). */
13
+ userInvocable: boolean;
14
+ }
15
+ /** Discover all skills (project overrides global overrides plugin overrides builtin).
16
+ * Directory-style `.nexrall/skills/<name>/SKILL.md` beats a flat `.nexrall/commands/<name>.md`
17
+ * of the same name within the same precedence tier. */
18
+ export declare function loadSkills(workDir: string): Skill[];
19
+ export declare function findSkill(skills: Skill[], name: string): Skill | undefined;
20
+ /** Skills the MODEL may auto-invoke via the use_skill tool (excludes disable-model-invocation:true ones). */
21
+ export declare function autoInvokableSkills(skills: Skill[]): Skill[];
22
+ /** Skills the USER may invoke via /name (excludes user-invocable:false ones). */
23
+ export declare function userInvokableSkills(skills: Skill[]): Skill[];
24
+ /** A compact catalogue injected into the system prompt so the model knows what it can auto-invoke via use_skill. */
25
+ export declare function summariseSkills(skills: Skill[]): string;
26
+ /**
27
+ * Expand a skill body into a final prompt (same $ARGUMENTS / @file / !`cmd`
28
+ * substitution as slash commands — see commands/loader.ts#expandBody), then
29
+ * prepend a short header pointing at the skill's own directory (if any) so
30
+ * the model knows where to find supporting files it may need to read_file
31
+ * on demand, mirroring Claude Code's "reference files from your SKILL.md"
32
+ * guidance without loading them into context up front.
33
+ */
34
+ export declare function expandSkill(skill: Skill, argString: string, workDir: string): string;
35
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/agent/skills.ts"],"names":[],"mappings":"AA2CA,MAAM,WAAW,KAAK;IACpB,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;IACpD,0IAA0I;IAC1I,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kHAAkH;IAClH,sBAAsB,EAAE,OAAO,CAAC;IAChC,iGAAiG;IACjG,aAAa,EAAE,OAAO,CAAC;CACxB;AAyID;;wDAEwD;AACxD,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,EAAE,CAanD;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAG1E;AAED,6GAA6G;AAC7G,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAE5D;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAE5D;AAED,oHAAoH;AACpH,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAIvD;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAcpF"}
@@ -0,0 +1,253 @@
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.loadSkills = loadSkills;
37
+ exports.findSkill = findSkill;
38
+ exports.autoInvokableSkills = autoInvokableSkills;
39
+ exports.userInvokableSkills = userInvokableSkills;
40
+ exports.summariseSkills = summariseSkills;
41
+ exports.expandSkill = expandSkill;
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const os = __importStar(require("os"));
45
+ const index_1 = require("../plugins/index");
46
+ const loader_1 = require("../commands/loader");
47
+ function parseFrontmatter(raw) {
48
+ const m = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
49
+ if (!m)
50
+ return { meta: {}, body: raw.trim() };
51
+ const meta = {};
52
+ for (const line of m[1].split(/\r?\n/)) {
53
+ const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
54
+ if (kv)
55
+ meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, '');
56
+ }
57
+ return { meta, body: (m[2] ?? '').trim() };
58
+ }
59
+ function parseBool(v, fallback) {
60
+ if (v === undefined)
61
+ return fallback;
62
+ const s = v.trim().toLowerCase();
63
+ if (s === 'true' || s === '1' || s === 'yes')
64
+ return true;
65
+ if (s === 'false' || s === '0' || s === 'no')
66
+ return false;
67
+ return fallback;
68
+ }
69
+ function toSkill(meta, body, name, source, dir) {
70
+ const model = ['turbo', 'pro', 'ultra'].find((x) => x === (meta.model ?? '').toLowerCase());
71
+ return {
72
+ name,
73
+ description: meta.description || `Custom /${name} skill`,
74
+ model,
75
+ mode: meta.mode || undefined,
76
+ body,
77
+ source,
78
+ dir,
79
+ disableModelInvocation: parseBool(meta['disable-model-invocation'], false),
80
+ userInvocable: parseBool(meta['user-invocable'], true),
81
+ };
82
+ }
83
+ // Legacy flat-file skills: .nexrall/commands/<name>.md (and plugin commands/).
84
+ // Kept as its own loader (rather than merged into loadSkillDir below) since the
85
+ // naming/precedence rule ("skill directory wins over a same-name flat command")
86
+ // requires knowing about both shapes before the caller merges them.
87
+ function loadFlatCommandDir(dir, source, into) {
88
+ let files;
89
+ try {
90
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
91
+ }
92
+ catch {
93
+ return;
94
+ }
95
+ for (const file of files) {
96
+ try {
97
+ const raw = fs.readFileSync(path.join(dir, file), 'utf-8');
98
+ const { meta, body } = parseFrontmatter(raw);
99
+ const name = (meta.name || path.basename(file, '.md')).trim().toLowerCase();
100
+ if (!name)
101
+ continue;
102
+ if (source !== 'project' && into.has(name))
103
+ continue; // earlier tiers win
104
+ into.set(name, toSkill(meta, body, name, source));
105
+ }
106
+ catch {
107
+ /* skip malformed */
108
+ }
109
+ }
110
+ }
111
+ // Directory-style skills: .nexrall/skills/<name>/SKILL.md (+ optional supporting files).
112
+ function loadSkillDir(root, source, into) {
113
+ let entries;
114
+ try {
115
+ entries = fs.readdirSync(root, { withFileTypes: true });
116
+ }
117
+ catch {
118
+ return;
119
+ }
120
+ for (const entry of entries) {
121
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
122
+ continue;
123
+ const skillDir = path.join(root, entry.name);
124
+ const skillFile = path.join(skillDir, 'SKILL.md');
125
+ try {
126
+ if (!fs.statSync(skillFile).isFile())
127
+ continue;
128
+ }
129
+ catch {
130
+ continue;
131
+ }
132
+ try {
133
+ const raw = fs.readFileSync(skillFile, 'utf-8');
134
+ const { meta, body } = parseFrontmatter(raw);
135
+ const name = (meta.name || entry.name).trim().toLowerCase();
136
+ if (!name)
137
+ continue;
138
+ // A directory-style skill always wins over an earlier flat command of
139
+ // the same name (from the SAME precedence tier), and — like commands —
140
+ // project tier always wins outright regardless of load order.
141
+ if (source !== 'project' && into.has(name))
142
+ continue;
143
+ into.set(name, toSkill(meta, body, name, source, skillDir));
144
+ }
145
+ catch {
146
+ /* skip malformed */
147
+ }
148
+ }
149
+ }
150
+ // ── Built-in skills ───────────────────────────────────────────────────────────
151
+ // Shipped defaults; lowest precedence (project > global > plugin > builtin), so
152
+ // a user can override any of them with a same-name skill or command file.
153
+ const BUILTIN_SKILLS = [
154
+ {
155
+ name: 'review',
156
+ description: 'Review uncommitted changes (or a PR/branch diff) for correctness bugs, edge cases, and security issues. ' +
157
+ 'Use when the user asks to review a diff, check their changes before committing, or audit a PR.',
158
+ source: 'builtin',
159
+ disableModelInvocation: false,
160
+ userInvocable: true,
161
+ body: [
162
+ 'Review the following diff like a meticulous senior engineer. Target: $ARGUMENTS',
163
+ '(If no target given, review the uncommitted working-tree changes below. If a branch or PR',
164
+ 'number is given, run the appropriate `git diff <base>...` or `gh pr diff <n>` yourself first.)',
165
+ '',
166
+ 'Branch: !`git branch --show-current`',
167
+ 'Status: !`git status --short`',
168
+ '',
169
+ 'Diff (uncommitted):',
170
+ '```diff',
171
+ '!`git diff HEAD --unified=5 --no-color | head -4000`',
172
+ '```',
173
+ '',
174
+ 'Review methodology:',
175
+ '1. Read the surrounding code of every changed hunk (read_file with offset/limit) — never judge a hunk in isolation.',
176
+ '2. Look for: correctness bugs, edge cases (empty/null/unicode/concurrency), security issues',
177
+ ' (injection, path traversal, secrets), breaking API changes (find_references / search callers),',
178
+ ' silent behaviour changes, and missing error handling.',
179
+ '3. Check tests: do existing tests cover the change? Are assertions weakened?',
180
+ '',
181
+ 'Output format:',
182
+ '- 🔴 Critical (must fix before merge) — with file:line and a concrete fix',
183
+ '- 🟡 Warning (should fix) — with file:line',
184
+ '- 🟢 Suggestion (nice to have)',
185
+ '- Verdict: APPROVE / REQUEST CHANGES with a one-paragraph summary.',
186
+ 'Do NOT modify any files — this is a read-only review.',
187
+ ].join('\n'),
188
+ },
189
+ ];
190
+ /** Discover all skills (project overrides global overrides plugin overrides builtin).
191
+ * Directory-style `.nexrall/skills/<name>/SKILL.md` beats a flat `.nexrall/commands/<name>.md`
192
+ * of the same name within the same precedence tier. */
193
+ function loadSkills(workDir) {
194
+ const out = new Map();
195
+ // Flat commands first (so a same-tier skill directory below can override them).
196
+ loadFlatCommandDir(path.join(workDir, '.nexrall', 'commands'), 'project', out);
197
+ loadSkillDir(path.join(workDir, '.nexrall', 'skills'), 'project', out);
198
+ loadFlatCommandDir(path.join(os.homedir(), '.nexrall', 'commands'), 'global', out);
199
+ loadSkillDir(path.join(os.homedir(), '.nexrall', 'skills'), 'global', out);
200
+ for (const dir of (0, index_1.pluginAssetDirs)(workDir, 'commands'))
201
+ loadFlatCommandDir(dir, 'plugin', out);
202
+ for (const dir of (0, index_1.pluginAssetDirs)(workDir, 'skills'))
203
+ loadSkillDir(dir, 'plugin', out);
204
+ for (const skill of BUILTIN_SKILLS) {
205
+ if (!out.has(skill.name))
206
+ out.set(skill.name, skill);
207
+ }
208
+ return [...out.values()];
209
+ }
210
+ function findSkill(skills, name) {
211
+ const want = name.replace(/^\//, '').trim().toLowerCase();
212
+ return skills.find((s) => s.name === want);
213
+ }
214
+ /** Skills the MODEL may auto-invoke via the use_skill tool (excludes disable-model-invocation:true ones). */
215
+ function autoInvokableSkills(skills) {
216
+ return skills.filter((s) => !s.disableModelInvocation);
217
+ }
218
+ /** Skills the USER may invoke via /name (excludes user-invocable:false ones). */
219
+ function userInvokableSkills(skills) {
220
+ return skills.filter((s) => s.userInvocable);
221
+ }
222
+ /** A compact catalogue injected into the system prompt so the model knows what it can auto-invoke via use_skill. */
223
+ function summariseSkills(skills) {
224
+ const list = autoInvokableSkills(skills);
225
+ if (!list.length)
226
+ return '';
227
+ return list.map((s) => `- ${s.name}: ${s.description}`).join('\n');
228
+ }
229
+ /**
230
+ * Expand a skill body into a final prompt (same $ARGUMENTS / @file / !`cmd`
231
+ * substitution as slash commands — see commands/loader.ts#expandBody), then
232
+ * prepend a short header pointing at the skill's own directory (if any) so
233
+ * the model knows where to find supporting files it may need to read_file
234
+ * on demand, mirroring Claude Code's "reference files from your SKILL.md"
235
+ * guidance without loading them into context up front.
236
+ */
237
+ function expandSkill(skill, argString, workDir) {
238
+ const expanded = (0, loader_1.expandBody)(skill.body, argString, workDir);
239
+ if (!skill.dir)
240
+ return expanded;
241
+ let siblings = [];
242
+ try {
243
+ siblings = fs.readdirSync(skill.dir).filter((f) => f !== 'SKILL.md');
244
+ }
245
+ catch {
246
+ /* directory unreadable — proceed without the file listing */
247
+ }
248
+ if (!siblings.length)
249
+ return expanded;
250
+ return (`[Skill "${skill.name}" — supporting files available in ${skill.dir} (use read_file with an absolute path to load ` +
251
+ `any of these only if the instructions below need them): ${siblings.join(', ')}]\n\n${expanded}`);
252
+ }
253
+ //# sourceMappingURL=skills.js.map
@@ -1,5 +1,5 @@
1
1
  import type { Message, SSEEvent, AuthConfig, EnvContext, EditorContext } from '../types';
2
- export declare const API_BASE = "https://api.nexrall.com";
2
+ export declare const API_BASE: string;
3
3
  /**
4
4
  * Decide the final assistant `content` array to store in history for a completed
5
5
  * turn. Pure + exported so it can be unit-tested without a live SSE stream.
@@ -42,8 +42,40 @@ export interface StreamChatOptions {
42
42
  }>;
43
43
  /** Catalogue of custom sub-agent types — injected into the system prompt. */
44
44
  agents?: string;
45
+ /** Catalogue of auto-invokable skills (description only) — injected into the system prompt so the model can call use_skill when relevant. */
46
+ skills?: string;
47
+ /**
48
+ * Opt in to restarting a turn that died AFTER partial output was rendered.
49
+ *
50
+ * Historically any transport failure past the first token was fatal: retrying
51
+ * would have re-streamed text the user could already see, duplicating it. That
52
+ * rule is correct but it protects almost nothing in practice — a long turn emits
53
+ * a thinking delta within seconds, so the retry machinery was effectively dead
54
+ * code exactly when it mattered most (deep into an expensive agentic run).
55
+ *
56
+ * When the caller can UNDO its rendered output for the current turn, it sets
57
+ * this and handles the `stream_restart` event by discarding that output. Then a
58
+ * mid-render disconnect becomes recoverable instead of turn-fatal. This is safe
59
+ * for side effects: tool calls are dispatched by the agent loop only after
60
+ * `message_complete`, and a restart by definition means that never arrived — so
61
+ * no tool ran and nothing but rendered characters is rolled back.
62
+ */
63
+ allowRestartAfterRender?: boolean;
45
64
  }
46
65
  export declare function streamChat(messages: Message[], options: StreamChatOptions, onEvent: (e: SSEEvent) => void): Promise<Message>;
66
+ /**
67
+ * Ask the server to stop generating a turn.
68
+ *
69
+ * This is REQUIRED for Stop to work, not an optimisation. A dropped socket used to abort
70
+ * the model; now it detaches and keeps generating so a network blip doesn't discard the
71
+ * turn (see the resume machinery above). Since a deliberate Stop looks identical to a
72
+ * blip from the server's side, it has to be signalled explicitly — otherwise the model
73
+ * would run to completion and bill in full after the user pressed Stop.
74
+ *
75
+ * Best-effort and never throws: Stop must feel instant, and the server's detach grace
76
+ * bounds the cost if this request never lands.
77
+ */
78
+ export declare function cancelTurn(turnId: string): Promise<void>;
47
79
  export declare function getBalance(): Promise<number>;
48
80
  export declare function exchangeVscodeCode(code: string): Promise<AuthConfig>;
49
81
  export declare function login(email: string, password: string): Promise<AuthConfig>;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;AAIlD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAML;AAeD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA0BD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CA4clB;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAalD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBhF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiErH,eAAO,MAAM,QAAQ,QAAmB,CAAC;AAIzC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAeL;AAeD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6IAA6I;IAC7I,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;;OAeG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAiDD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CA80BlB;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU9D;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAalD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBhF"}