@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.
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +54 -0
- package/dist/agent/memory.d.ts +45 -0
- package/dist/agent/memory.d.ts.map +1 -0
- package/dist/agent/memory.js +243 -0
- package/dist/agent/skills.d.ts +35 -0
- package/dist/agent/skills.d.ts.map +1 -0
- package/dist/agent/skills.js +253 -0
- package/dist/api/client.d.ts +2 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +2 -2
- package/dist/commands/loader.d.ts +7 -3
- package/dist/commands/loader.d.ts.map +1 -1
- package/dist/commands/loader.js +13 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +137 -69
- package/package.json +1 -1
package/dist/agent/loop.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -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
|
package/dist/api/client.d.ts
CHANGED
|
@@ -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>;
|
package/dist/api/client.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/api/client.js
CHANGED
|
@@ -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,
|
|
14
|
-
* inlining, and !`cmd` shell capture. Shell/file errors are inlined as
|
|
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
|
|
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"}
|
package/dist/commands/loader.js
CHANGED
|
@@ -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,
|
|
144
|
-
* inlining, and !`cmd` shell capture. Shell/file errors are inlined as
|
|
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
|
|
150
|
+
function expandBody(body, argString, workDir) {
|
|
148
151
|
const args = argString.trim();
|
|
149
152
|
const positional = args ? args.split(/\s+/) : [];
|
|
150
|
-
let out =
|
|
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(
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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);
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -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":"
|
|
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;
|
|
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"}
|
package/dist/tools/executor.js
CHANGED
|
@@ -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
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
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
|
|
255
|
+
async function readFileWindowed(resolved, offset, requestedLimit) {
|
|
227
256
|
return new Promise((resolve) => {
|
|
228
|
-
const
|
|
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) =>
|
|
242
|
-
const
|
|
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 —
|
|
245
|
-
resolve({ output: `[File: ${resolved} —
|
|
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
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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 (
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
1987
|
-
|
|
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
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
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(
|
|
2048
|
+
async function memoryRead(input, workDir) {
|
|
2009
2049
|
try {
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
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.
|
|
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)",
|