@nexrall/code-core 1.4.11 → 1.4.13

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.
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AAsKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AA4TD,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAM7D;AAsBD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,aAA+G,CAAC;AAC7I,gGAAgG;AAChG,eAAO,MAAM,aAAa,QAA2J,CAAC;AAEtL;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AAUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAwBxD;AAoBD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,qFAAqF;IACrF,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,IAAI,cAAc,CAE7C;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,EAAE,EAAE,OAAO,EACX,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CAoDN;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA6B5D;AAmBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,SAAI,GAAG,MAAM,CAgCpF;AAsHD,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAE/D;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,GACA,OAAO,CAAC,OAAO,CAAC,CAqElB;AAID,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CA4nBpB"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AAwKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AA+TD,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAM7D;AAsBD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,aAA+G,CAAC;AAC7I,gGAAgG;AAChG,eAAO,MAAM,aAAa,QAA2J,CAAC;AAEtL;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AAUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAwBxD;AAoBD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,qFAAqF;IACrF,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,IAAI,cAAc,CAE7C;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,EAAE,EAAE,OAAO,EACX,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CAoDN;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA6B5D;AAmBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,SAAI,GAAG,MAAM,CAgCpF;AA8JD,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAE/D;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,GACA,OAAO,CAAC,OAAO,CAAC,CAqElB;AAID,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CA2oBpB"}
@@ -48,12 +48,14 @@ exports.runAgentLoop = runAgentLoop;
48
48
  const client_1 = require("../api/client");
49
49
  const executor_1 = require("../tools/executor");
50
50
  const agentTypes_1 = require("./agentTypes");
51
+ const skills_1 = require("./skills");
51
52
  const rules_1 = require("../permissions/rules");
52
53
  const sandbox_1 = require("../tools/sandbox");
53
54
  const index_1 = require("../plugins/index");
54
55
  const testIntegrity_1 = require("./testIntegrity");
55
56
  const flaky_1 = require("./flaky");
56
57
  const claimEvidence_1 = require("./claimEvidence");
58
+ const memory_1 = require("./memory");
57
59
  const fs = __importStar(require("fs"));
58
60
  const path = __importStar(require("path"));
59
61
  const child_process_1 = require("child_process");
@@ -299,6 +301,8 @@ function humanDescription(name, input) {
299
301
  const preview = typeof input.prompt === 'string' ? input.prompt.slice(0, 60) : '';
300
302
  return `Sub-task: ${desc || preview}${!desc && preview.length === 60 ? '…' : ''}`;
301
303
  }
304
+ case 'use_skill':
305
+ return `Use skill: /${input.name ?? '(unknown)'}`;
302
306
  case 'get_diagnostics':
303
307
  return input.path ? `Get diagnostics: ${input.path}` : 'Get workspace diagnostics';
304
308
  case 'go_to_definition':
@@ -831,6 +835,42 @@ async function autoCompactMessages(messages, options, ledger) {
831
835
  void kept;
832
836
  return true;
833
837
  }
838
+ // ─── Periodic memory compaction ────────────────────────────────────────────
839
+ // A memory file (project or global — see agent/memory.ts) can grow large over
840
+ // many sessions since memory_write only ever appends. writeMemory() already
841
+ // applies an immediate, synchronous byte-cap eviction (oldest entries dropped)
842
+ // as a hard backstop, but that's a blunt instrument — this periodically
843
+ // consolidates the file with a real LLM summarization pass instead, so old
844
+ // facts are condensed into fewer, denser bullets rather than silently lost.
845
+ // Checked opportunistically right after a successful memory_write (see the
846
+ // call site below) rather than on every tool call — cheap to check (a single
847
+ // file stat), and memory_write is the only thing that can push a file over
848
+ // the trigger threshold in the first place.
849
+ async function maybeCompactMemory(scope, options) {
850
+ try {
851
+ await (0, memory_1.compactMemoryIfNeeded)(scope, options.workDir, async (prompt) => {
852
+ const reply = await (0, client_1.streamChat)([{ role: 'user', content: [{ type: 'text', text: prompt }] }], {
853
+ // Same reasoning as autoCompactMessages' summariser: a mechanical
854
+ // consolidation task, always run on the cheapest 1M-context tier
855
+ // regardless of the user's chosen model for the actual conversation.
856
+ model: 'turbo',
857
+ mode: 'ask',
858
+ env: options.env,
859
+ clientType: options.clientType,
860
+ abortSignal: options.abortSignal,
861
+ }, () => { });
862
+ return reply.content
863
+ .filter((b) => b.type === 'text')
864
+ .map((b) => b.text ?? '')
865
+ .join('');
866
+ });
867
+ }
868
+ catch {
869
+ // Best-effort — a failed/aborted compaction just means the file stays as-is
870
+ // until the next memory_write call tries again; writeMemory's synchronous
871
+ // byte cap already bounds worst-case growth in the meantime.
872
+ }
873
+ }
834
874
  // ─── Resume-time proactive compaction ─────────────────────────────────────────
835
875
  //
836
876
  // The in-loop auto-compact above only reacts to `lastPromptTokens`, which is
@@ -940,6 +980,11 @@ async function runAgentLoop(initialMessages, options) {
940
980
  // catalogue (sub-agents can't spawn further), but every level resolves types.
941
981
  const agentTypes = (0, agentTypes_1.loadAgentTypes)(options.workDir);
942
982
  const agentsCatalogue = depth === 0 ? (0, agentTypes_1.summariseAgents)(agentTypes) : '';
983
+ // Skills catalogue — unlike agentsCatalogue, available at every depth: a skill is
984
+ // just a reusable prompt template (via use_skill), not another spawn point, so
985
+ // sub-agents benefit from the same playbooks without the recursion concerns that
986
+ // gate agentsCatalogue to the top level.
987
+ const skillsCatalogue = (0, skills_1.summariseSkills)((0, skills_1.loadSkills)(options.workDir));
943
988
  // Optional OS-level bash sandbox (opt-in via settings.json "sandbox").
944
989
  const settings = (0, rules_1.loadSettings)(options.workDir);
945
990
  const sandboxCfg = (0, sandbox_1.parseSandboxConfig)(settings.raw.sandbox) ?? undefined;
@@ -1130,6 +1175,7 @@ async function runAgentLoop(initialMessages, options) {
1130
1175
  abortSignal: options.abortSignal,
1131
1176
  extraTools: options.mcpManager?.getAnthropicTools(),
1132
1177
  agents: agentsCatalogue || undefined,
1178
+ skills: skillsCatalogue || undefined,
1133
1179
  }, onEvent);
1134
1180
  }
1135
1181
  catch (err) {
@@ -1393,6 +1439,14 @@ async function runAgentLoop(initialMessages, options) {
1393
1439
  catch (err) {
1394
1440
  result = { error: `Tool execution failed: ${err.message}` };
1395
1441
  }
1442
+ // Opportunistic memory compaction — memory_write is the only tool that can
1443
+ // grow a memory file, so this is the cheapest point to check whether it just
1444
+ // crossed the compaction threshold. Fire-and-forget: never blocks the turn,
1445
+ // never surfaces its own errors to the model (see maybeCompactMemory).
1446
+ if (name === 'memory_write' && result.error === undefined) {
1447
+ const scope = input.scope === 'global' ? 'global' : 'project';
1448
+ void maybeCompactMemory(scope, options);
1449
+ }
1396
1450
  // PostToolUse can inject context for the model or flag a problem.
1397
1451
  const post = runToolHooks(hooks.PostToolUse, 'PostToolUse', name, input, options.workDir, result);
1398
1452
  const injected = [pre.context, post.context].filter(Boolean).join('\n');
@@ -0,0 +1,45 @@
1
+ export type MemoryScope = 'project' | 'global';
2
+ /** Per-workDir file, keyed the same way CheckpointManager keys its store
3
+ * (sha1 of the resolved absolute path) so two differently-named checkouts of
4
+ * the same repo don't collide, and the same repo always maps to the same file
5
+ * regardless of cwd casing/trailing-slash quirks. */
6
+ export declare function memoryFilePath(scope: MemoryScope, workDir?: string): string;
7
+ export declare const MEMORY_ENTRY_MAX_CHARS = 400;
8
+ export declare const MEMORY_MAX_BYTES = 12000;
9
+ export declare const MEMORY_COMPACT_TRIGGER_BYTES = 16000;
10
+ export declare const MEMORY_HARD_CAP_BYTES: number;
11
+ export interface MemoryWriteResult {
12
+ ok: boolean;
13
+ already: boolean;
14
+ scope: MemoryScope;
15
+ file: string;
16
+ }
17
+ export declare function writeMemory(content: string, scope: MemoryScope, workDir?: string): Promise<MemoryWriteResult>;
18
+ export declare function readMemory(scope: MemoryScope, workDir?: string): string;
19
+ /** Merge BOTH scopes for prompt injection — project-specific first (most
20
+ * relevant to what the agent is doing right now), global preferences after. */
21
+ export declare function readAllMemory(workDir?: string): string;
22
+ export declare function clearMemory(scope: MemoryScope, workDir?: string): void;
23
+ export declare function memoryStats(scope: MemoryScope, workDir?: string): {
24
+ file: string;
25
+ bytes: number;
26
+ entries: number;
27
+ };
28
+ /**
29
+ * Summarize the OLDEST half of a memory file into a few consolidated bullets via
30
+ * an LLM call, keeping the most recent entries verbatim — same "compact, don't
31
+ * silently drop" instinct as the conversation-history auto-compactor, applied to
32
+ * memory instead of a transcript. This is the EXPECTED bounding mechanism for
33
+ * memory file size — called opportunistically (fire-and-forget) right after a
34
+ * successful memory_write (see maybeCompactMemory in loop.ts) once a file crosses
35
+ * MEMORY_COMPACT_TRIGGER_BYTES. writeMemory's own emergency synchronous eviction
36
+ * (MEMORY_HARD_CAP_BYTES) sits well above this threshold specifically so this
37
+ * summarizing pass gets the chance to run first in the common case; the blunt
38
+ * eviction is a last resort for if compaction has been failing/unavailable.
39
+ *
40
+ * `summarize` is injected (rather than importing streamChat directly) to avoid
41
+ * pulling api/client.ts's network/auth machinery into every consumer of this
42
+ * module and to keep this file trivially unit-testable.
43
+ */
44
+ export declare function compactMemoryIfNeeded(scope: MemoryScope, workDir: string | undefined, summarize: (prompt: string) => Promise<string>): Promise<boolean>;
45
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/agent/memory.ts"],"names":[],"mappings":"AAiCA,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE/C;;;sDAGsD;AACtD,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAI3E;AAED,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C,eAAO,MAAM,gBAAgB,QAAS,CAAC;AACvC,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAMnD,eAAO,MAAM,qBAAqB,QAAmC,CAAC;AA4CtE,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuBnH;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAGvE;AAED;gFACgF;AAChF,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAStD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAGtE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAKlH;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAC7C,OAAO,CAAC,OAAO,CAAC,CA+BlB"}
@@ -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
@@ -42,6 +42,8 @@ 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;
45
47
  }
46
48
  export declare function streamChat(messages: Message[], options: StreamChatOptions, onEvent: (e: SSEEvent) => void): Promise<Message>;
47
49
  export declare function getBalance(): Promise<number>;
@@ -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":"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;IAChB,6IAA6I;IAC7I,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"}
@@ -69,7 +69,7 @@ function sleep(ms) {
69
69
  * meaningful "attempt N of M" to the UI — not a hard limit by itself. */
70
70
  const MAX_TOTAL_ATTEMPTS = (MAX_RETRIES + 1) * (MAX_RETRIES + 1);
71
71
  async function streamChat(messages, options, onEvent) {
72
- const { model, env, editorContext, nexrallMd, mode, effort, clientType, abortSignal, extraTools, agents } = options;
72
+ const { model, env, editorContext, nexrallMd, mode, effort, clientType, abortSignal, extraTools, agents, skills } = options;
73
73
  // Bridge custom abort signal → native AbortController so fetch is cancelled immediately
74
74
  const controller = new AbortController();
75
75
  if (abortSignal?.aborted)
@@ -84,7 +84,7 @@ async function streamChat(messages, options, onEvent) {
84
84
  {
85
85
  method: 'POST',
86
86
  headers: authHeaders(),
87
- body: JSON.stringify({ messages, model, env, editorContext, nexrallMd, mode, effort, clientType, extraTools, agents }),
87
+ body: JSON.stringify({ messages, model, env, editorContext, nexrallMd, mode, effort, clientType, extraTools, agents, skills }),
88
88
  signal: controller.signal,
89
89
  },
90
90
  ];
@@ -10,9 +10,13 @@ export interface SlashCommand {
10
10
  export declare function loadSlashCommands(workDir: string): SlashCommand[];
11
11
  export declare function findSlashCommand(cmds: SlashCommand[], name: string): SlashCommand | undefined;
12
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.
13
+ * Expand a command/skill body into a final prompt: argument substitution,
14
+ * @file inlining, and !`cmd` shell capture. Shell/file errors are inlined as
15
+ * notes rather than thrown so the prompt is always produced. Shared by both
16
+ * SlashCommand (this file) and Skill (agent/skills.ts) — they have the same
17
+ * `body` shape, just a richer frontmatter set on the Skill side.
16
18
  */
19
+ export declare function expandBody(body: string, argString: string, workDir: string): string;
20
+ /** @deprecated kept for backward compatibility — use expandBody(cmd.body, ...) directly. */
17
21
  export declare function expandCommand(cmd: SlashCommand, argString: string, workDir: string): string;
18
22
  //# sourceMappingURL=loader.d.ts.map
@@ -1 +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"}
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;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAuCnF;AAED,4FAA4F;AAC5F,wBAAgB,aAAa,CAAC,GAAG,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE3F"}
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadSlashCommands = loadSlashCommands;
37
37
  exports.findSlashCommand = findSlashCommand;
38
+ exports.expandBody = expandBody;
38
39
  exports.expandCommand = expandCommand;
39
40
  const fs = __importStar(require("fs"));
40
41
  const path = __importStar(require("path"));
@@ -140,14 +141,16 @@ function findSlashCommand(cmds, name) {
140
141
  return cmds.find((c) => c.name === want);
141
142
  }
142
143
  /**
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.
144
+ * Expand a command/skill body into a final prompt: argument substitution,
145
+ * @file inlining, and !`cmd` shell capture. Shell/file errors are inlined as
146
+ * notes rather than thrown so the prompt is always produced. Shared by both
147
+ * SlashCommand (this file) and Skill (agent/skills.ts) — they have the same
148
+ * `body` shape, just a richer frontmatter set on the Skill side.
146
149
  */
147
- function expandCommand(cmd, argString, workDir) {
150
+ function expandBody(body, argString, workDir) {
148
151
  const args = argString.trim();
149
152
  const positional = args ? args.split(/\s+/) : [];
150
- let out = cmd.body;
153
+ let out = body;
151
154
  // !`shell command` → captured stdout
152
155
  out = out.replace(/!`([^`]+)`/g, (_m, c) => {
153
156
  try {
@@ -175,9 +178,13 @@ function expandCommand(cmd, argString, workDir) {
175
178
  const hadArgsToken = /\$ARGUMENTS/.test(out);
176
179
  out = out.replace(/\$ARGUMENTS/g, args);
177
180
  // If the template never referenced its args, append them so they aren't lost.
178
- if (!hadArgsToken && !/\$\d+/.test(cmd.body) && args) {
181
+ if (!hadArgsToken && !/\$\d+/.test(body) && args) {
179
182
  out = `${out}\n\n${args}`;
180
183
  }
181
184
  return out.trim();
182
185
  }
186
+ /** @deprecated kept for backward compatibility — use expandBody(cmd.body, ...) directly. */
187
+ function expandCommand(cmd, argString, workDir) {
188
+ return expandBody(cmd.body, argString, workDir);
189
+ }
183
190
  //# sourceMappingURL=loader.js.map
package/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@ export * from './agent/editCompleteness';
8
8
  export * from './agent/crossFile';
9
9
  export * from './agent/flaky';
10
10
  export * from './agent/claimEvidence';
11
+ export * from './agent/memory';
12
+ export * from './agent/skills';
11
13
  export * from './mcp/client';
12
14
  export * from './mcp/httpClient';
13
15
  export * from './mcp/manager';
@@ -1 +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,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,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,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
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,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,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,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -24,6 +24,8 @@ __exportStar(require("./agent/editCompleteness"), exports);
24
24
  __exportStar(require("./agent/crossFile"), exports);
25
25
  __exportStar(require("./agent/flaky"), exports);
26
26
  __exportStar(require("./agent/claimEvidence"), exports);
27
+ __exportStar(require("./agent/memory"), exports);
28
+ __exportStar(require("./agent/skills"), exports);
27
29
  __exportStar(require("./mcp/client"), exports);
28
30
  __exportStar(require("./mcp/httpClient"), exports);
29
31
  __exportStar(require("./mcp/manager"), exports);
@@ -10,7 +10,7 @@ export interface PluginInfo {
10
10
  /** Discover installed plugins (project scope shadows global on name clash). */
11
11
  export declare function loadPlugins(workDir: string): PluginInfo[];
12
12
  /** Subdirectories of every installed plugin that hold `kind` assets (existing only). */
13
- export declare function pluginAssetDirs(workDir: string, kind: 'commands' | 'agents'): string[];
13
+ export declare function pluginAssetDirs(workDir: string, kind: 'commands' | 'agents' | 'skills'): string[];
14
14
  /** Merge hooks.json from every plugin into a single hooks config (arrays concatenated). */
15
15
  export declare function pluginHooks(workDir: string): Record<string, unknown[]>;
16
16
  /** MCP server maps declared by plugins: { serverName → config } (first plugin wins on clash). */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;CAC7B;AAsCD,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAKzD;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,MAAM,EAAE,CAMtF;AAED,2FAA2F;AAC3F,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAetE;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAazE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;CAC7B;AAsCD,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAKzD;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,EAAE,CAMjG;AAED,2FAA2F;AAC3F,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAetE;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAazE"}
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAsgEtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,CAAC,CAiBrB"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAkkEtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,CAAC,CAiBrB"}
@@ -44,6 +44,8 @@ const child_process_1 = require("child_process");
44
44
  const sandbox_1 = require("./sandbox");
45
45
  const auth_1 = require("../auth");
46
46
  const editCompleteness_1 = require("../agent/editCompleteness");
47
+ const memory_1 = require("../agent/memory");
48
+ const skills_1 = require("../agent/skills");
47
49
  const testIntegrity_1 = require("../agent/testIntegrity");
48
50
  const crossFile_1 = require("../agent/crossFile");
49
51
  const client_1 = require("../api/client");
@@ -52,8 +54,24 @@ const tsLangService_1 = require("./tsLangService");
52
54
  // ─── Constants ────────────────────────────────────────────────────────────────
53
55
  const DEFAULT_TIMEOUT_MS = 60000;
54
56
  const MAX_FETCH_BYTES = 200 * 1024; // 200 KB
55
- const MAX_READ_BYTES = 500 * 1024; // 500 KB
57
+ const MAX_READ_BYTES = 500 * 1024; // 500 KB — still used by notebook_read (JSON.parse needs it whole)
56
58
  const MAX_OUTPUT_CHARS = 100000; // bash / grep output cap (~100 KB)
59
+ // read_file has NO byte-size gate (unlike the old 500KB cutoff) — like Claude
60
+ // Code, a file of any size can be read; it's always streamed line-by-line so
61
+ // memory is bounded regardless of file size. Instead there are two independent
62
+ // caps per call, mirroring Claude Code's Read tool:
63
+ // - MAX_READ_LINES: a hard line-count ceiling (default window AND clamp on
64
+ // an explicit limit) so one call never floods context with thousands of
65
+ // short lines (JSON/minified/logs) just because the file is small in bytes.
66
+ // - MAX_READ_TOKENS: a hard token budget (rough chars/4 estimate). If the
67
+ // requested window would exceed it, we either return a PARTIAL page (no
68
+ // explicit limit — same UX as Claude Code's "first page + how to continue"
69
+ // notice) or an error (explicit limit given and it still doesn't fit —
70
+ // caller must shrink the limit or use search_files instead).
71
+ const MAX_READ_LINES = 2000;
72
+ const MAX_READ_TOKENS = 25000;
73
+ const READ_CHARS_PER_TOKEN = 4; // rough, conservative — consistent with loop.ts's resume estimator
74
+ const MAX_READ_CHARS = MAX_READ_TOKENS * READ_CHARS_PER_TOKEN; // 100,000 chars
57
75
  // GAP B — output spillover. The inline bash result keeps only HEAD+TAIL (~100 KB),
58
76
  // which loses the MIDDLE of a large log — often exactly where a stack trace's root
59
77
  // cause or a failing assertion lives. To make the full log recoverable WITHOUT
@@ -217,51 +235,99 @@ function isBinaryFile(filePath) {
217
235
  return false;
218
236
  }
219
237
  }
238
+ function formatNumberedLine(offset, indexInKept, line) {
239
+ return `${String(offset + indexInKept + 1).padStart(4, ' ')}\t${line}`;
240
+ }
220
241
  /**
221
- * Read a bounded line window [offset, offset+limit) from a file too large to load
222
- * whole, by streaming it in chunks and keeping only the requested lines. Memory is
223
- * bounded by (limit lines actually kept) + one chunk, never the whole file so a
224
- * 20MB bash-output spill file can be inspected section-by-section. (GAP B.)
242
+ * Read a bounded window [offset, offset+limit) of a file by streaming it in
243
+ * chunks memory is bounded by what's actually kept, never the whole file, so
244
+ * this is safe for files of ANY size (there is no byte-size gate on read_file
245
+ * at all, matching Claude Code). Two independent hard caps apply per call:
246
+ * - MAX_READ_LINES lines
247
+ * - MAX_READ_TOKENS tokens (~MAX_READ_CHARS chars) of formatted output
248
+ * If an explicit `requestedLimit` was given and the token cap is hit before
249
+ * that many lines were produced, the call fails with an actionable error
250
+ * (mirrors Claude Code: "a read that passes an explicit offset or limit and
251
+ * still exceeds the token limit returns an error"). Otherwise (default,
252
+ * no explicit limit) it silently returns a partial page with a continuation
253
+ * note — never an error just for being a big file.
225
254
  */
226
- async function readLargeFileWindow(resolved, offset, limit, kb) {
255
+ async function readFileWindowed(resolved, offset, requestedLimit) {
227
256
  return new Promise((resolve) => {
228
- const endLine = offset + limit; // exclusive, 0-based
257
+ const explicitLimit = requestedLimit > 0;
258
+ const lineScanCap = explicitLimit ? Math.min(requestedLimit, MAX_READ_LINES) : MAX_READ_LINES;
259
+ const hardEndLine = offset + lineScanCap; // exclusive, 0-based
229
260
  const kept = [];
261
+ let keptChars = 0;
230
262
  let lineNo = 0; // 0-based index of the NEXT line to be completed
231
263
  let carry = ''; // partial line spanning chunk boundaries
232
264
  let stopped = false;
265
+ let hitTokenCap = false;
266
+ let sawEof = false;
233
267
  const stream = fs.createReadStream(resolved, { encoding: 'utf-8', highWaterMark: 256 * 1024 });
268
+ // Returns false if adding this line would exceed the token budget (line NOT kept).
269
+ const tryPushLine = (line) => {
270
+ const formatted = formatNumberedLine(offset, kept.length, line);
271
+ const added = formatted.length + 1; // +1 for the join newline
272
+ if (keptChars + added > MAX_READ_CHARS) {
273
+ hitTokenCap = true;
274
+ return false;
275
+ }
276
+ kept.push(line);
277
+ keptChars += added;
278
+ return true;
279
+ };
234
280
  const finish = () => {
235
281
  if (stopped)
236
282
  return;
237
283
  stopped = true;
238
284
  stream.destroy();
285
+ if (hitTokenCap && explicitLimit) {
286
+ resolve({
287
+ error: `Requested range (offset:${offset}, limit:${requestedLimit}) is too large — exceeds ${MAX_READ_TOKENS} tokens. ` +
288
+ `Pass a smaller limit, or use search_files to locate the relevant part first.`,
289
+ });
290
+ return;
291
+ }
239
292
  const first = offset + 1;
240
293
  const last = offset + kept.length;
241
- const numbered = kept.map((l, i) => `${String(offset + i + 1).padStart(4, ' ')}\t${l}`).join('\n');
242
- const note = kept.length < limit
294
+ const numbered = kept.map((l, i) => formatNumberedLine(offset, i, l)).join('\n');
295
+ const totalNote = sawEof ? `/${lineNo}` : '';
296
+ const note = sawEof
243
297
  ? ` (reached end of file at line ${last})`
244
- : ` (more lines follow — increase offset to continue)`;
245
- resolve({ output: `[File: ${resolved} — ${kb} KB, showing lines ${first}-${last}${note}]\n${numbered}` });
298
+ : ` (more lines follow — pass offset:${last} to continue, or search_files to locate the relevant part)`;
299
+ resolve({ output: `[File: ${resolved} — lines ${first}-${last}${totalNote}${note}]\n${numbered}` });
246
300
  };
247
301
  stream.on('data', (chunk) => {
248
302
  const text = carry + (typeof chunk === 'string' ? chunk : chunk.toString('utf-8'));
249
303
  const lines = text.split('\n');
250
304
  carry = lines.pop() ?? ''; // last element is an incomplete line (or '')
251
305
  for (const line of lines) {
252
- if (lineNo >= offset && lineNo < endLine)
253
- kept.push(line);
254
- lineNo++;
255
- if (lineNo >= endLine) {
256
- finish();
257
- return;
306
+ if (lineNo >= offset) {
307
+ if (lineNo >= hardEndLine) {
308
+ finish();
309
+ return;
310
+ }
311
+ if (!tryPushLine(line)) {
312
+ finish();
313
+ return;
314
+ }
258
315
  }
316
+ lineNo++;
259
317
  }
260
318
  });
261
319
  stream.on('end', () => {
320
+ if (stopped)
321
+ return;
262
322
  // Flush the final carry (file not ending in newline) if it's in range.
263
- if (!stopped && carry !== '' && lineNo >= offset && lineNo < endLine)
264
- kept.push(carry);
323
+ if (carry !== '' && lineNo >= offset && lineNo < hardEndLine) {
324
+ if (!tryPushLine(carry)) {
325
+ finish();
326
+ return;
327
+ }
328
+ lineNo++;
329
+ }
330
+ sawEof = true;
265
331
  finish();
266
332
  });
267
333
  stream.on('error', (err) => { if (!stopped) {
@@ -278,38 +344,19 @@ async function readFile(input, workDir) {
278
344
  return { error: 'Missing required parameter: path' };
279
345
  try {
280
346
  const resolved = resolvePath(filePath, workDir);
281
- const stat = fs.statSync(resolved);
347
+ fs.statSync(resolved); // throws ENOENT/EISDIR etc with a clear message if the path is bad
282
348
  // Reject binary files early — reading them as UTF-8 produces garbage.
349
+ // (isBinaryFile only samples the first 8KB, so this is cheap regardless of file size.)
283
350
  if (isBinaryFile(resolved)) {
284
351
  const ext = path.extname(resolved).toLowerCase();
285
352
  return { error: `Cannot read binary file: ${resolved} (${ext || 'no extension'}). Use a text-based tool or convert it first.` };
286
353
  }
287
- // Large-file path: when the file exceeds the in-memory cap, a full readFileSync
288
- // would OOM but we must still be able to inspect SECTIONS (this is what makes a
289
- // 20MB bash-output spill file usable, GAP B). If offset+limit are given we stream
290
- // line-by-line and materialise only the requested window; otherwise we require it.
291
- if (stat.size > MAX_READ_BYTES) {
292
- const kb = (stat.size / 1024).toFixed(0);
293
- if (limit <= 0) {
294
- return { error: `File too large (${kb} KB, max ${MAX_READ_BYTES / 1024} KB). Pass offset + limit to read a section (e.g. {offset:0, limit:500}), or search_files to locate the relevant part first.` };
295
- }
296
- return await readLargeFileWindow(resolved, offset, limit, kb);
297
- }
298
- const content = fs.readFileSync(resolved, 'utf-8');
299
- const allLines = content.split('\n');
300
- const totalLines = allLines.length;
301
- const startLine = offset; // 0-based
302
- const endLine = limit > 0 ? Math.min(startLine + limit, totalLines) : totalLines;
303
- const slice = allLines.slice(startLine, endLine);
304
- // Prefix each line with its 1-based line number (like cat -n / Claude Code view)
305
- const numbered = slice.map((l, i) => {
306
- const lineNo = String(startLine + i + 1).padStart(4, ' ');
307
- return `${lineNo}\t${l}`;
308
- }).join('\n');
309
- const rangeNote = (offset > 0 || limit > 0)
310
- ? ` lines ${startLine + 1}-${endLine}/${totalLines}`
311
- : ` ${totalLines} lines`;
312
- return { output: `[File: ${resolved} (${rangeNote})]\n${numbered}` };
354
+ // No byte-size gate a file of any size can be read; it's always streamed
355
+ // line-by-line so memory is bounded regardless of file size (matches Claude
356
+ // Code, which has no separate "too large" error either). Instead, one call
357
+ // is capped at MAX_READ_LINES lines and MAX_READ_TOKENS tokens (see
358
+ // readFileWindowed for exact semantics of each cap).
359
+ return await readFileWindowed(resolved, offset, limit);
313
360
  }
314
361
  catch (err) {
315
362
  return { error: err.message };
@@ -1983,39 +2030,58 @@ async function stockPhoto(input, workDir) {
1983
2030
  return { output: `Found ${photos.length} photo(s) for "${query}":\n${lines.join('\n')}` };
1984
2031
  }
1985
2032
  // ─── Memory tools ─────────────────────────────────────────────────────────────
1986
- const MEMORY_FILE = path.join(os.homedir(), '.nexrall', 'memory.md');
1987
- async function memoryWrite(input) {
2033
+ // Implementation lives in agent/memory.ts (project vs global scope, size caps,
2034
+ // LLM-summarizing compaction, file locking) — this is just the tool-call glue.
2035
+ function memoryScopeOf(input) {
2036
+ return input.scope === 'global' ? 'global' : 'project';
2037
+ }
2038
+ async function memoryWrite(input, workDir) {
1988
2039
  const content = typeof input.content === 'string' ? input.content.trim() : '';
1989
2040
  if (!content)
1990
2041
  return { error: 'content is required' };
1991
- const dir = path.dirname(MEMORY_FILE);
1992
- fs.mkdirSync(dir, { recursive: true });
1993
- // Dedup: skip if an entry with the same first 60 chars already exists.
1994
- // The old check used full substring match which blocked "React + TypeScript"
1995
- // if "React" was already stored. Now we only deduplicate near-identical entries.
1996
- if (fs.existsSync(MEMORY_FILE)) {
1997
- const existing = fs.readFileSync(MEMORY_FILE, 'utf-8').toLowerCase();
1998
- const fingerprint = content.toLowerCase().slice(0, 60);
1999
- if (existing.includes(fingerprint)) {
2000
- return { output: `Memory already recorded (skipped duplicate): ${content}` };
2001
- }
2002
- }
2003
- const timestamp = new Date().toISOString().slice(0, 10);
2004
- const entry = `\n- [${timestamp}] ${content}`;
2005
- fs.appendFileSync(MEMORY_FILE, entry, 'utf-8');
2006
- return { output: `Memory saved: ${content}` };
2042
+ const scope = memoryScopeOf(input);
2043
+ const r = await (0, memory_1.writeMemory)(content, scope, workDir);
2044
+ if (!r.ok)
2045
+ return { error: 'failed to save memory' };
2046
+ return { output: r.already ? `Memory already recorded (skipped duplicate): ${content}` : `Memory saved (${scope}): ${content}` };
2007
2047
  }
2008
- async function memoryRead(_input) {
2048
+ async function memoryRead(input, workDir) {
2009
2049
  try {
2010
- if (!fs.existsSync(MEMORY_FILE))
2011
- return { output: 'No memories saved yet.' };
2012
- const content = fs.readFileSync(MEMORY_FILE, 'utf-8').trim();
2013
- return { output: content || 'No memories saved yet.' };
2050
+ // No explicit scope requested → merge both (what the model actually needs
2051
+ // when re-orienting mid-conversation); an explicit scope narrows to just that file.
2052
+ if (input && (input.scope === 'project' || input.scope === 'global')) {
2053
+ const mem = (0, memory_1.readMemory)(input.scope, workDir);
2054
+ return { output: mem || 'No memories saved yet.' };
2055
+ }
2056
+ const mem = (0, memory_1.readAllMemory)(workDir);
2057
+ return { output: mem || 'No memories saved yet.' };
2014
2058
  }
2015
2059
  catch (err) {
2016
2060
  return { error: err.message };
2017
2061
  }
2018
2062
  }
2063
+ // ─── Skills ───────────────────────────────────────────────────────────────────
2064
+ //
2065
+ // use_skill loads a skill's playbook into context so the model can follow it —
2066
+ // same idea as memory_read returning saved notes, just for a named, reusable
2067
+ // procedure instead of accumulated learnings. The expanded body (with
2068
+ // $ARGUMENTS/@file/!`cmd` substitution already applied) comes back as the tool
2069
+ // result; the model then proceeds using its normal tools to carry it out.
2070
+ async function useSkill(input, workDir) {
2071
+ const name = typeof input.name === 'string' ? input.name.trim() : '';
2072
+ if (!name)
2073
+ return { error: 'name is required (the skill to invoke)' };
2074
+ const dir = workDir ?? process.cwd();
2075
+ const skills = (0, skills_1.loadSkills)(dir);
2076
+ const skill = (0, skills_1.findSkill)(skills, name);
2077
+ if (!skill) {
2078
+ const known = skills.map((s) => s.name).join(', ') || '(none defined)';
2079
+ return { error: `Unknown skill "${name}". Available: ${known}.` };
2080
+ }
2081
+ const args = typeof input.args === 'string' ? input.args : '';
2082
+ const expanded = (0, skills_1.expandSkill)(skill, args, dir);
2083
+ return { output: `[Skill "${skill.name}" loaded — follow these instructions:]\n\n${expanded}` };
2084
+ }
2019
2085
  function semanticPreamble(input, workDir) {
2020
2086
  const p = typeof input.path === 'string' ? input.path : '';
2021
2087
  const line = typeof input.line === 'number' ? Math.floor(input.line) : 0;
@@ -2078,6 +2144,7 @@ const TOOL_MAP = {
2078
2144
  notebook_edit: notebookEdit,
2079
2145
  memory_write: memoryWrite,
2080
2146
  memory_read: memoryRead,
2147
+ use_skill: useSkill,
2081
2148
  bash_output: bashOutput,
2082
2149
  kill_shell: killShell,
2083
2150
  // LSP-lite fallback (regex-based). In VS Code these names are intercepted by
@@ -2099,6 +2166,7 @@ const WORKDIR_TOOLS = new Set([
2099
2166
  'generate_image', 'stock_photo',
2100
2167
  'get_symbols', 'get_workspace_symbols',
2101
2168
  'go_to_definition', 'find_references', 'get_hover',
2169
+ 'memory_write', 'memory_read', 'use_skill',
2102
2170
  ]);
2103
2171
  async function executeTool(name, input, abortSignal, sandbox, workDir, agentScope) {
2104
2172
  if (!TOOL_MAP[name])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexrall/code-core",
3
- "version": "1.4.11",
3
+ "version": "1.4.13",
4
4
  "description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
5
5
  "license": "MIT",
6
6
  "author": "Nexrall <support@nexrall.com> (https://nexrall.com)",