@nexrall/code-core 1.4.63 → 1.4.66
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/README.md +2 -2
- package/dist/agent/agentRegistry.d.ts +14 -3
- package/dist/agent/agentRegistry.d.ts.map +1 -1
- package/dist/agent/agentRegistry.js +136 -6
- package/dist/agent/agentTypes.d.ts +53 -3
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +200 -153
- package/dist/agent/backgroundAgents.d.ts +66 -0
- package/dist/agent/backgroundAgents.d.ts.map +1 -0
- package/dist/agent/backgroundAgents.js +145 -0
- package/dist/agent/loop.d.ts +44 -7
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +946 -263
- package/dist/agent/memory.d.ts +11 -0
- package/dist/agent/memory.d.ts.map +1 -1
- package/dist/agent/memory.js +23 -3
- package/dist/agent/modelCatalogue.d.ts +15 -0
- package/dist/agent/modelCatalogue.d.ts.map +1 -1
- package/dist/agent/modelCatalogue.js +46 -0
- package/dist/agent/planMode.d.ts +14 -0
- package/dist/agent/planMode.d.ts.map +1 -1
- package/dist/agent/planMode.js +145 -17
- package/dist/agent/readDedupe.d.ts +15 -0
- package/dist/agent/readDedupe.d.ts.map +1 -0
- package/dist/agent/readDedupe.js +146 -0
- package/dist/agent/securityLint.js +2 -2
- package/dist/agent/toolPrefetch.d.ts +44 -0
- package/dist/agent/toolPrefetch.d.ts.map +1 -0
- package/dist/agent/toolPrefetch.js +101 -0
- package/dist/agent/trust.d.ts +0 -5
- package/dist/agent/trust.d.ts.map +1 -1
- package/dist/agent/trust.js +41 -0
- package/dist/api/client.d.ts +15 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +88 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/permissions/destructive.d.ts +9 -7
- package/dist/permissions/destructive.d.ts.map +1 -1
- package/dist/permissions/destructive.js +55 -8
- package/dist/permissions/destructiveTokens.d.ts +29 -0
- package/dist/permissions/destructiveTokens.d.ts.map +1 -0
- package/dist/permissions/destructiveTokens.js +469 -0
- package/dist/permissions/modePolicy.d.ts +22 -8
- package/dist/permissions/modePolicy.d.ts.map +1 -1
- package/dist/permissions/modePolicy.js +25 -10
- package/dist/tools/executor.d.ts +36 -0
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +408 -85
- package/dist/tools/tsLangService.d.ts.map +1 -1
- package/dist/tools/tsLangService.js +107 -21
- package/dist/types.d.ts +101 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +12 -1
- package/dist/util/miniYaml.d.ts +10 -0
- package/dist/util/miniYaml.d.ts.map +1 -0
- package/dist/util/miniYaml.js +149 -0
- package/package.json +2 -10
package/dist/agent/memory.d.ts
CHANGED
|
@@ -48,6 +48,17 @@ export declare function memoryStats(scope: MemoryScope, workDir?: string): {
|
|
|
48
48
|
entries: number;
|
|
49
49
|
archived: number;
|
|
50
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* Keep only the lines of a consolidation reply that are real memory entries.
|
|
53
|
+
*
|
|
54
|
+
* The prompt says "Output ONLY the bullet lines", but models do not always comply: a reply
|
|
55
|
+
* that opens with "I'll read the full, untruncated memory text before consolidating…" (seen
|
|
56
|
+
* in a user's global.md and a project file) used to be saved verbatim as an ACTIVE fact, then
|
|
57
|
+
* injected into every later session as ground truth. An entry is a line of the exact shape
|
|
58
|
+
* memory_write produces — `- [YYYY-MM-DD] …` — so anything else (preamble, "Here are the
|
|
59
|
+
* merged bullets:", a code fence, a trailing remark) is dropped rather than persisted.
|
|
60
|
+
*/
|
|
61
|
+
export declare function extractConsolidatedEntries(reply: string): string[];
|
|
51
62
|
/**
|
|
52
63
|
* Summarize the OLDEST half of a memory file's ACTIVE entries into a few consolidated
|
|
53
64
|
* bullets via an LLM call, keeping the most recent entries verbatim — same "compact,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/agent/memory.ts"],"names":[],"mappings":"AAiEA,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;AAKtE,eAAO,MAAM,wBAAwB,QAAmC,CAAC;AAoFzE,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;kCAI8B;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;6DAEyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE,MAAM,EAChB,IAAI,GAAE,kBAAuB,GAC5B,OAAO,CAAC,iBAAiB,CAAC,CA2D5B;AAED,MAAM,WAAW,iBAAiB;IAChC;;;iDAG6C;IAC7C,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB,GAAG,MAAM,CAKrG;AAED;gFACgF;AAChF,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB,GAAG,MAAM,CASpF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAGtE;AAED,wBAAgB,WAAW,CACzB,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE,MAAM,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAUpE;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;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,
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/agent/memory.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/memory.js
CHANGED
|
@@ -40,6 +40,7 @@ exports.readMemory = readMemory;
|
|
|
40
40
|
exports.readAllMemory = readAllMemory;
|
|
41
41
|
exports.clearMemory = clearMemory;
|
|
42
42
|
exports.memoryStats = memoryStats;
|
|
43
|
+
exports.extractConsolidatedEntries = extractConsolidatedEntries;
|
|
43
44
|
exports.compactMemoryIfNeeded = compactMemoryIfNeeded;
|
|
44
45
|
exports.isSafeAgentName = isSafeAgentName;
|
|
45
46
|
exports.agentMemoryPath = agentMemoryPath;
|
|
@@ -318,6 +319,22 @@ function memoryStats(scope, workDir) {
|
|
|
318
319
|
archived: archived.filter((l) => /^-\s\[\d{4}-\d{2}-\d{2}\]/.test(l)).length,
|
|
319
320
|
};
|
|
320
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Keep only the lines of a consolidation reply that are real memory entries.
|
|
324
|
+
*
|
|
325
|
+
* The prompt says "Output ONLY the bullet lines", but models do not always comply: a reply
|
|
326
|
+
* that opens with "I'll read the full, untruncated memory text before consolidating…" (seen
|
|
327
|
+
* in a user's global.md and a project file) used to be saved verbatim as an ACTIVE fact, then
|
|
328
|
+
* injected into every later session as ground truth. An entry is a line of the exact shape
|
|
329
|
+
* memory_write produces — `- [YYYY-MM-DD] …` — so anything else (preamble, "Here are the
|
|
330
|
+
* merged bullets:", a code fence, a trailing remark) is dropped rather than persisted.
|
|
331
|
+
*/
|
|
332
|
+
function extractConsolidatedEntries(reply) {
|
|
333
|
+
return reply
|
|
334
|
+
.split('\n')
|
|
335
|
+
.map((l) => l.trimEnd())
|
|
336
|
+
.filter((l) => /^-\s\[\d{4}-\d{2}-\d{2}\]\s*\S/.test(l));
|
|
337
|
+
}
|
|
321
338
|
/**
|
|
322
339
|
* Summarize the OLDEST half of a memory file's ACTIVE entries into a few consolidated
|
|
323
340
|
* bullets via an LLM call, keeping the most recent entries verbatim — same "compact,
|
|
@@ -370,9 +387,12 @@ async function compactMemoryIfNeeded(scope, workDir, summarize) {
|
|
|
370
387
|
`draws from, and preserve any leading "[src: ...]" provenance tag verbatim when present. ` +
|
|
371
388
|
`Output ONLY the bullet lines, nothing else.\n\n${older.join('\n')}`;
|
|
372
389
|
try {
|
|
373
|
-
const summarized = (await summarize(prompt))
|
|
374
|
-
|
|
375
|
-
|
|
390
|
+
const summarized = extractConsolidatedEntries(await summarize(prompt));
|
|
391
|
+
// Zero valid bullets means the model answered in prose (or refused): treat it
|
|
392
|
+
// exactly like a failed call and keep the original entries. Writing the reply
|
|
393
|
+
// anyway would REPLACE the older half with chatter.
|
|
394
|
+
if (summarized.length > 0) {
|
|
395
|
+
active = [...summarized, ...recent];
|
|
376
396
|
changed = true;
|
|
377
397
|
}
|
|
378
398
|
}
|
|
@@ -64,4 +64,19 @@ export declare function liveCostMultiplierFor(id: string | undefined, fallback:
|
|
|
64
64
|
export declare function liveSelectableModelIds(): string[] | null;
|
|
65
65
|
/** Test-only: reset the module-level cache between test cases. */
|
|
66
66
|
export declare function _resetModelCatalogueCacheForTests(): void;
|
|
67
|
+
/**
|
|
68
|
+
* `model: cheapest` — Claude Code's Explore-on-Haiku idea, without leaving the
|
|
69
|
+
* provider the user chose. Resolves to the lowest-costMultiplier AVAILABLE model
|
|
70
|
+
* of the parent session's vendor (Opus → Sonnet, GPT-6 Sol → GPT-6 Luna, …), never
|
|
71
|
+
* a pricier one. Returns the parent model unchanged when the catalogue is not
|
|
72
|
+
* loaded or does not know it — inheriting is always a safe fallback; guessing a
|
|
73
|
+
* model on another provider (the bug that made explorer bill Anthropic for
|
|
74
|
+
* DeepSeek sessions) is not.
|
|
75
|
+
*/
|
|
76
|
+
export declare const CHEAPEST_MODEL_ALIAS = "cheapest";
|
|
77
|
+
export declare function cheapestSameVendorModel(parentModel: string | undefined): string | undefined;
|
|
78
|
+
/** Resolve a sub-agent's `model:` against its parent's: undefined/inherit → parent, cheapest → see above. */
|
|
79
|
+
export declare function resolveSubAgentModel(agentModel: string | undefined, parentModel: string | undefined): string | undefined;
|
|
80
|
+
/** Test hook: install a catalogue without a network fetch. */
|
|
81
|
+
export declare function _setModelCatalogueForTest(models: CodeModelInfo[] | null): void;
|
|
67
82
|
//# sourceMappingURL=modelCatalogue.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modelCatalogue.d.ts","sourceRoot":"","sources":["../../src/agent/modelCatalogue.ts"],"names":[],"mappings":"AAgDA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAiBlE;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,OAAO,CAAC;IAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAuBnE;AAED,iFAAiF;AACjF,wBAAgB,uBAAuB,IAAI;IAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAElG;AAOD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErF;AAED,sDAAsD;AACtD,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEvF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAGzF;AAED,wEAAwE;AACxE,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAGvF;AAED,uFAAuF;AACvF,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAEnF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAExE;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,0EAA0E;AAC1E,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAG9G;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,GAAG,IAAI,CAExD;AAED,kEAAkE;AAClE,wBAAgB,iCAAiC,IAAI,IAAI,CAGxD"}
|
|
1
|
+
{"version":3,"file":"modelCatalogue.d.ts","sourceRoot":"","sources":["../../src/agent/modelCatalogue.ts"],"names":[],"mappings":"AAgDA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAiBlE;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,OAAO,CAAC;IAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAuBnE;AAED,iFAAiF;AACjF,wBAAgB,uBAAuB,IAAI;IAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAElG;AAOD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErF;AAED,sDAAsD;AACtD,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEvF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAGzF;AAED,wEAAwE;AACxE,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAGvF;AAED,uFAAuF;AACvF,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAEnF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAExE;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,0EAA0E;AAC1E,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAG9G;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,GAAG,IAAI,CAExD;AAED,kEAAkE;AAClE,wBAAgB,iCAAiC,IAAI,IAAI,CAGxD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,aAAa,CAAC;AAO/C,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAW3F;AAED,6GAA6G;AAC7G,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIxH;AAED,8DAA8D;AAC9D,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,IAAI,GAAG,IAAI,CAE9E"}
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
// added mid-session is vanishingly rare and would need a restart to use
|
|
48
48
|
// safely anyway, since the picker itself is also static per-render).
|
|
49
49
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.CHEAPEST_MODEL_ALIAS = void 0;
|
|
50
51
|
exports.getModelCatalogue = getModelCatalogue;
|
|
51
52
|
exports.getCachedModelCatalogue = getCachedModelCatalogue;
|
|
52
53
|
exports.liveContextWindowFor = liveContextWindowFor;
|
|
@@ -59,6 +60,9 @@ exports.liveLabelFor = liveLabelFor;
|
|
|
59
60
|
exports.liveCostMultiplierFor = liveCostMultiplierFor;
|
|
60
61
|
exports.liveSelectableModelIds = liveSelectableModelIds;
|
|
61
62
|
exports._resetModelCatalogueCacheForTests = _resetModelCatalogueCacheForTests;
|
|
63
|
+
exports.cheapestSameVendorModel = cheapestSameVendorModel;
|
|
64
|
+
exports.resolveSubAgentModel = resolveSubAgentModel;
|
|
65
|
+
exports._setModelCatalogueForTest = _setModelCatalogueForTest;
|
|
62
66
|
const client_1 = require("../api/client");
|
|
63
67
|
/** How long a successful fetch is trusted before a caller should re-fetch. */
|
|
64
68
|
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes — matches backend's own getPricing() TTL
|
|
@@ -180,4 +184,46 @@ function _resetModelCatalogueCacheForTests() {
|
|
|
180
184
|
_state = null;
|
|
181
185
|
_inFlight = null;
|
|
182
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* `model: cheapest` — Claude Code's Explore-on-Haiku idea, without leaving the
|
|
189
|
+
* provider the user chose. Resolves to the lowest-costMultiplier AVAILABLE model
|
|
190
|
+
* of the parent session's vendor (Opus → Sonnet, GPT-6 Sol → GPT-6 Luna, …), never
|
|
191
|
+
* a pricier one. Returns the parent model unchanged when the catalogue is not
|
|
192
|
+
* loaded or does not know it — inheriting is always a safe fallback; guessing a
|
|
193
|
+
* model on another provider (the bug that made explorer bill Anthropic for
|
|
194
|
+
* DeepSeek sessions) is not.
|
|
195
|
+
*/
|
|
196
|
+
exports.CHEAPEST_MODEL_ALIAS = 'cheapest';
|
|
197
|
+
// Legacy tier aliases the backend still resolves (see agentTypes.ts VALID_MODELS).
|
|
198
|
+
const LEGACY_ALIAS_IDS = {
|
|
199
|
+
turbo: 'claude-sonnet-5-5', fast: 'claude-sonnet-5-5', pro: 'claude-opus-5-5', ultra: 'claude-fable-5-1',
|
|
200
|
+
};
|
|
201
|
+
function cheapestSameVendorModel(parentModel) {
|
|
202
|
+
if (!_state || !parentModel)
|
|
203
|
+
return parentModel;
|
|
204
|
+
const parent = findModel(parentModel) ?? findModel(LEGACY_ALIAS_IDS[parentModel]);
|
|
205
|
+
if (!parent)
|
|
206
|
+
return parentModel;
|
|
207
|
+
const parentCost = parent.costMultiplier ?? Infinity;
|
|
208
|
+
let best = parent;
|
|
209
|
+
for (const m of _state.models) {
|
|
210
|
+
if (m.vendor !== parent.vendor || m.available === false || typeof m.costMultiplier !== 'number')
|
|
211
|
+
continue;
|
|
212
|
+
if (m.costMultiplier < (best.costMultiplier ?? Infinity) && m.costMultiplier <= parentCost)
|
|
213
|
+
best = m;
|
|
214
|
+
}
|
|
215
|
+
return best === parent ? parentModel : best.id;
|
|
216
|
+
}
|
|
217
|
+
/** Resolve a sub-agent's `model:` against its parent's: undefined/inherit → parent, cheapest → see above. */
|
|
218
|
+
function resolveSubAgentModel(agentModel, parentModel) {
|
|
219
|
+
if (!agentModel || agentModel === 'inherit')
|
|
220
|
+
return parentModel;
|
|
221
|
+
if (agentModel === exports.CHEAPEST_MODEL_ALIAS)
|
|
222
|
+
return cheapestSameVendorModel(parentModel);
|
|
223
|
+
return agentModel;
|
|
224
|
+
}
|
|
225
|
+
/** Test hook: install a catalogue without a network fetch. */
|
|
226
|
+
function _setModelCatalogueForTest(models) {
|
|
227
|
+
_state = models ? { models, defaultModel: models[0]?.id ?? '', fetchedAt: Date.now() } : null;
|
|
228
|
+
}
|
|
183
229
|
//# sourceMappingURL=modelCatalogue.js.map
|
package/dist/agent/planMode.d.ts
CHANGED
|
@@ -10,6 +10,20 @@ export interface PlanModeRefusal {
|
|
|
10
10
|
* "Provably" is doing real work: unknown verbs are refused, not guessed at.
|
|
11
11
|
*/
|
|
12
12
|
export declare function isReadOnlyCommand(command: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Is a (possibly piped / chained) command read-only on every segment?
|
|
15
|
+
*
|
|
16
|
+
* This is what the permission GATE uses to skip a prompt in ask/edit mode. Plan mode
|
|
17
|
+
* keeps the stricter whole-shape refusal above (`checkPlanMode`): a temporary lock can
|
|
18
|
+
* afford to say "no pipelines", an approval gate that says it about `git log | head`
|
|
19
|
+
* just trains people to click through prompts.
|
|
20
|
+
*
|
|
21
|
+
* Fail-closed throughout: anything that is not plainly a chain of read-only commands —
|
|
22
|
+
* a write redirect, `$(…)`, backticks, a lone `&`, an unknown verb, a segment split
|
|
23
|
+
* wrongly by a quote — returns false, which means "ask the user". The cost of a false
|
|
24
|
+
* false is one extra keypress; the cost of a false true is `rm` running unasked.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isReadOnlyPipeline(command: string): boolean;
|
|
13
27
|
/**
|
|
14
28
|
* Should this tool call be refused because the session is in plan mode?
|
|
15
29
|
* Returns null when the call is allowed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planMode.d.ts","sourceRoot":"","sources":["../../src/agent/planMode.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"planMode.d.ts","sourceRoot":"","sources":["../../src/agent/planMode.ts"],"names":[],"mappings":"AAgJA,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,MAAM,EAAE,eAAe,GAAG,oBAAoB,CAAC;IAC/C,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;CACjB;AAqFD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CA8D1D;AAQD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO3D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,eAAe,GAAG,IAAI,CAoCxB;AAED,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,QAqBvB,CAAC"}
|
package/dist/agent/planMode.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
20
|
exports.PLAN_MODE_INSTRUCTIONS = void 0;
|
|
21
21
|
exports.isReadOnlyCommand = isReadOnlyCommand;
|
|
22
|
+
exports.isReadOnlyPipeline = isReadOnlyPipeline;
|
|
22
23
|
exports.checkPlanMode = checkPlanMode;
|
|
23
24
|
/**
|
|
24
25
|
* Tools that cannot alter anything outside the conversation.
|
|
@@ -71,10 +72,31 @@ const READ_ONLY_BASH = new Set([
|
|
|
71
72
|
'grep', 'egrep', 'fgrep', 'rg', 'ag', 'ack', 'find', 'fd', 'locate',
|
|
72
73
|
'diff', 'comm', 'cmp', 'sort', 'uniq', 'cut', 'tr', 'column', 'jq', 'yq',
|
|
73
74
|
'date', 'whoami', 'hostname', 'uname', 'env', 'printenv', 'id', 'groups',
|
|
74
|
-
'ps', '
|
|
75
|
-
|
|
75
|
+
'ps', 'uptime', 'free', 'node', 'python', 'python3',
|
|
76
|
+
// `less`/`more`/`top` are deliberately NOT here: pagers run `!cmd` from inside and
|
|
77
|
+
// `top` never exits. `env` is listed but only bare — see toolFlagsOk.
|
|
78
|
+
'tree', 'nl', 'tac', 'strings', 'md5sum', 'sha256sum',
|
|
76
79
|
'true', 'false', 'test', 'sleep', 'seq', 'expr',
|
|
80
|
+
// Changes this shell's cwd only. Needed so `cd x && ls` is not refused.
|
|
81
|
+
'cd',
|
|
77
82
|
]);
|
|
83
|
+
/**
|
|
84
|
+
* A verb written with a path is only trusted when it lives in a system bin dir.
|
|
85
|
+
* Stripping to the basename (the old behaviour) meant `./ls`, `/tmp/evil/cat` and
|
|
86
|
+
* `build/git` — attacker-written files — were classified by their NAME and waved
|
|
87
|
+
* through as read-only.
|
|
88
|
+
*/
|
|
89
|
+
const SYSTEM_BIN_DIRS = new Set([
|
|
90
|
+
'/bin', '/usr/bin', '/usr/local/bin', '/sbin', '/usr/sbin', '/opt/homebrew/bin',
|
|
91
|
+
]);
|
|
92
|
+
/**
|
|
93
|
+
* Environment assignments that change WHAT a read-only verb runs: the search path,
|
|
94
|
+
* loader, pager/editor/external-diff hooks, and the interpreters' option vars.
|
|
95
|
+
* `GIT_EXTERNAL_DIFF=./x git diff` and `PATH=/tmp ls` run the attacker's program
|
|
96
|
+
* while every word the classifier looks at is innocent.
|
|
97
|
+
*/
|
|
98
|
+
const UNSAFE_ENV = /^(PATH|HOME|SHELL|IFS|ENV|BASH_ENV|PAGER|MANPAGER|EDITOR|VISUAL|BROWSER|LD_\w*|DYLD_\w*|GIT_\w*|NODE_\w*|NPM_\w*|npm_\w*|PYTHON\w*|BASH\w*|RUBY\w*|PERL\w*|LESS\w*|XDG_\w*|\w*OPTS?)=/i;
|
|
99
|
+
const GH_NOUNS = new Set(['pr', 'issue', 'run', 'repo', 'release', 'workflow', 'gist', 'label']);
|
|
78
100
|
/**
|
|
79
101
|
* Subcommands that are read-only for tools whose safety depends on the verb.
|
|
80
102
|
*
|
|
@@ -117,6 +139,83 @@ const READ_ONLY_NESTED = {
|
|
|
117
139
|
* `|` is included: `cat x | tee y` writes, and so does `... | sh`.
|
|
118
140
|
*/
|
|
119
141
|
const SHELL_CONTROL = /[\n\r;&|`]|\$\(|>>|>|<\(/;
|
|
142
|
+
const isShort = (f) => f.startsWith('-') && !f.startsWith('--');
|
|
143
|
+
const flagName = (f) => f.split('=')[0];
|
|
144
|
+
/**
|
|
145
|
+
* Flags of an allowlisted VERB that turn it into a writer or an executor. The verb
|
|
146
|
+
* list says `sort` is read-only; `sort -o f` writes f. Each entry below is a real way
|
|
147
|
+
* to mutate through a verb the list trusts — the list alone was the hole.
|
|
148
|
+
*/
|
|
149
|
+
function toolFlagsOk(verb, args) {
|
|
150
|
+
const flags = args.filter((a) => a.startsWith('-'));
|
|
151
|
+
const pos = args.filter((a) => !a.startsWith('-'));
|
|
152
|
+
switch (verb) {
|
|
153
|
+
case 'env': return args.length === 0; // `env CMD` RUNS CMD
|
|
154
|
+
case 'sort':
|
|
155
|
+
return !flags.some((f) => (isShort(f) && /^-[A-Za-z]*o/.test(f)) || f.startsWith('--output') || f.startsWith('--compress-program'));
|
|
156
|
+
case 'uniq': return pos.length <= 1; // second positional is an OUTPUT file
|
|
157
|
+
case 'find':
|
|
158
|
+
return !args.some((a) => ['-exec', '-execdir', '-delete', '-ok', '-okdir', '-fprint', '-fprint0', '-fprintf', '-fls'].includes(a));
|
|
159
|
+
case 'fd':
|
|
160
|
+
return !flags.some((f) => f.startsWith('--exec') || (isShort(f) && /^-[A-Za-z]*[xX]/.test(f)));
|
|
161
|
+
case 'rg': return !flags.some((f) => flagName(f) === '--pre' || f.startsWith('--hostname-bin'));
|
|
162
|
+
case 'ag':
|
|
163
|
+
case 'ack': return !flags.some((f) => f.startsWith('--pager'));
|
|
164
|
+
case 'yq': return !flags.some((f) => (isShort(f) && /^-[A-Za-z]*i/.test(f)) || f.startsWith('--inplace'));
|
|
165
|
+
case 'tree': return !flags.some((f) => isShort(f) && /^-[A-Za-z]*o/.test(f));
|
|
166
|
+
case 'date': return !flags.some((f) => (isShort(f) && /^-[A-Za-z]*s/.test(f)) || f.startsWith('--set'));
|
|
167
|
+
case 'hostname': return pos.length === 0; // `hostname NAME` renames the machine
|
|
168
|
+
case 'file': return !flags.some((f) => (isShort(f) && /^-[A-Za-z]*C/.test(f)) || f.startsWith('--compile'));
|
|
169
|
+
default: return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Every flag must be a known read-only one; anything unfamiliar fails closed. */
|
|
173
|
+
function flagsWithin(flags, shorts, longs) {
|
|
174
|
+
return flags.every((f) => {
|
|
175
|
+
if (isShort(f))
|
|
176
|
+
return [...f.slice(1)].every((c) => shorts.includes(c) || /\d/.test(c));
|
|
177
|
+
return longs.includes(flagName(f));
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const LISTISH = ['-l', '--list', '--contains', '--no-contains', '--merged', '--no-merged', '--points-at'];
|
|
181
|
+
/**
|
|
182
|
+
* Same idea one level down: `git branch` lists, `git branch newbranch` creates,
|
|
183
|
+
* `git branch -D main` deletes — one subcommand, three effects.
|
|
184
|
+
*/
|
|
185
|
+
function subcommandFlagsOk(verb, sub, args) {
|
|
186
|
+
const flags = args.filter((a) => a.startsWith('-'));
|
|
187
|
+
const pos = args.filter((a) => !a.startsWith('-')); // pos[0] === sub
|
|
188
|
+
const listish = flags.some((f) => LISTISH.includes(flagName(f)) || (isShort(f) && f.includes('l')));
|
|
189
|
+
if (verb === 'git') {
|
|
190
|
+
if (flags.some((f) => f.startsWith('--output') || f === '--ext-diff' || f === '-O' || f.startsWith('--open-files-in-pager')))
|
|
191
|
+
return false;
|
|
192
|
+
switch (sub) {
|
|
193
|
+
case 'branch':
|
|
194
|
+
return flagsWithin(flags, 'arvlq', ['--all', '--remotes', '--verbose', '--list', '--show-current', '--contains', '--no-contains', '--merged', '--no-merged', '--points-at', '--sort', '--format', '--column', '--no-column', '--color', '--no-color', '--abbrev', '--no-abbrev', '--ignore-case', '--omit-empty'])
|
|
195
|
+
&& (pos.length <= 1 || listish);
|
|
196
|
+
case 'tag':
|
|
197
|
+
return flagsWithin(flags, 'lnv', ['--list', '--contains', '--no-contains', '--merged', '--no-merged', '--points-at', '--sort', '--format', '--column', '--no-column', '--color', '--no-color', '--verify', '--ignore-case', '--omit-empty'])
|
|
198
|
+
&& (pos.length <= 1 || listish || flags.some((f) => f === '-v' || f === '--verify'));
|
|
199
|
+
case 'remote':
|
|
200
|
+
return flagsWithin(flags, 'vn', ['--verbose']) && (pos[1] === undefined || pos[1] === 'show' || pos[1] === 'get-url');
|
|
201
|
+
case 'config':
|
|
202
|
+
return flagsWithin(flags, 'lz', ['--get', '--get-all', '--get-regexp', '--list', '--show-origin', '--show-scope', '--global', '--local', '--system', '--worktree', '--name-only', '--null', '--includes', '--no-includes', '--type'])
|
|
203
|
+
&& pos.length <= 2; // `config` itself + at most one key
|
|
204
|
+
case 'reflog': return pos[1] !== 'expire' && pos[1] !== 'delete';
|
|
205
|
+
default: return true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (sub === 'config' && (verb === 'npm' || verb === 'pnpm' || verb === 'yarn')) {
|
|
209
|
+
return pos[1] === undefined || pos[1] === 'get' || pos[1] === 'list' || pos[1] === 'ls';
|
|
210
|
+
}
|
|
211
|
+
if (verb === 'go') {
|
|
212
|
+
return !flags.some((f) => {
|
|
213
|
+
const n = f.replace(/^--?/, '-');
|
|
214
|
+
return n === '-w' || n === '-u' || n.startsWith('-vettool') || n.startsWith('-toolexec') || n === '-exec';
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
120
219
|
function firstWords(command) {
|
|
121
220
|
return command.trim().split(/\s+/).filter(Boolean);
|
|
122
221
|
}
|
|
@@ -137,15 +236,21 @@ function isReadOnlyCommand(command) {
|
|
|
137
236
|
return false;
|
|
138
237
|
// `VAR=x cmd` — skip leading environment assignments to find the real verb.
|
|
139
238
|
let i = 0;
|
|
140
|
-
while (i < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i]))
|
|
239
|
+
while (i < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i])) {
|
|
240
|
+
if (UNSAFE_ENV.test(words[i]))
|
|
241
|
+
return false;
|
|
141
242
|
i++;
|
|
243
|
+
}
|
|
142
244
|
if (i >= words.length)
|
|
143
245
|
return false;
|
|
144
246
|
let verb = words[i];
|
|
145
|
-
//
|
|
247
|
+
// /usr/bin/git → git, but ONLY for a system bin dir (see SYSTEM_BIN_DIRS).
|
|
146
248
|
const slash = verb.lastIndexOf('/');
|
|
147
|
-
if (slash >= 0)
|
|
249
|
+
if (slash >= 0) {
|
|
250
|
+
if (!SYSTEM_BIN_DIRS.has(verb.slice(0, slash)))
|
|
251
|
+
return false;
|
|
148
252
|
verb = verb.slice(slash + 1);
|
|
253
|
+
}
|
|
149
254
|
// `sudo anything` is refused outright regardless of the verb behind it: plan
|
|
150
255
|
// mode is a promise about this machine, and privilege escalation is exactly
|
|
151
256
|
// where a mistaken allow is least recoverable.
|
|
@@ -159,16 +264,12 @@ function isReadOnlyCommand(command) {
|
|
|
159
264
|
// Bare `git` / `npm` just prints help — harmless.
|
|
160
265
|
return true;
|
|
161
266
|
}
|
|
162
|
-
if (subs.has(sub))
|
|
163
|
-
|
|
164
|
-
if (verb === 'git' && sub === 'config') {
|
|
165
|
-
const args = words.slice(i + 2).filter((w) => !w.startsWith('-'));
|
|
166
|
-
return args.length <= 1;
|
|
167
|
-
}
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
267
|
+
if (subs.has(sub))
|
|
268
|
+
return subcommandFlagsOk(verb, sub, words.slice(i + 1));
|
|
170
269
|
const nested = READ_ONLY_NESTED[verb];
|
|
171
|
-
|
|
270
|
+
// `gh api list -X DELETE` has "list" in second position too — the noun must be a
|
|
271
|
+
// real resource noun, and `api` (arbitrary HTTP verbs) is not one.
|
|
272
|
+
if (nested && rest[1] && nested.has(rest[1]) && (verb !== 'gh' || GH_NOUNS.has(sub)))
|
|
172
273
|
return true;
|
|
173
274
|
return false;
|
|
174
275
|
}
|
|
@@ -179,15 +280,42 @@ function isReadOnlyCommand(command) {
|
|
|
179
280
|
if (verb === 'node' || verb === 'python' || verb === 'python3') {
|
|
180
281
|
return words.slice(i + 1).every((w) => w === '--version' || w === '-V' || w === '-v');
|
|
181
282
|
}
|
|
182
|
-
|
|
183
|
-
if (verb === 'find' && words.some((w) => w === '-exec' || w === '-execdir' || w === '-delete')) {
|
|
283
|
+
if (!toolFlagsOk(verb, words.slice(i + 1)))
|
|
184
284
|
return false;
|
|
185
|
-
}
|
|
186
285
|
// Reading a file with `test`/`stat` is fine, but `tee` is not in the list at
|
|
187
286
|
// all, and `echo`/`printf` are only safe because redirection is already
|
|
188
287
|
// rejected by SHELL_CONTROL above.
|
|
189
288
|
return true;
|
|
190
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Output redirections that cannot write anywhere interesting. Without this `ls 2>&1`
|
|
292
|
+
* and `cmd 2>/dev/null` — extremely common — would all count as "redirection".
|
|
293
|
+
*/
|
|
294
|
+
const HARMLESS_REDIRECT = /\s*\d?>\s*&\d\b|\s*\d?>>?\s*\/dev\/null(?=\s|$)/g;
|
|
295
|
+
/**
|
|
296
|
+
* Is a (possibly piped / chained) command read-only on every segment?
|
|
297
|
+
*
|
|
298
|
+
* This is what the permission GATE uses to skip a prompt in ask/edit mode. Plan mode
|
|
299
|
+
* keeps the stricter whole-shape refusal above (`checkPlanMode`): a temporary lock can
|
|
300
|
+
* afford to say "no pipelines", an approval gate that says it about `git log | head`
|
|
301
|
+
* just trains people to click through prompts.
|
|
302
|
+
*
|
|
303
|
+
* Fail-closed throughout: anything that is not plainly a chain of read-only commands —
|
|
304
|
+
* a write redirect, `$(…)`, backticks, a lone `&`, an unknown verb, a segment split
|
|
305
|
+
* wrongly by a quote — returns false, which means "ask the user". The cost of a false
|
|
306
|
+
* false is one extra keypress; the cost of a false true is `rm` running unasked.
|
|
307
|
+
*/
|
|
308
|
+
function isReadOnlyPipeline(command) {
|
|
309
|
+
const cmd = command.replace(HARMLESS_REDIRECT, ' ').trim();
|
|
310
|
+
if (!cmd)
|
|
311
|
+
return false;
|
|
312
|
+
if (/[>`]|\$\(|<\(/.test(cmd))
|
|
313
|
+
return false;
|
|
314
|
+
const segs = cmd.split(/\|\||&&|[;|\n\r]/).map((x) => x.trim()).filter(Boolean);
|
|
315
|
+
if (!segs.length)
|
|
316
|
+
return false;
|
|
317
|
+
return segs.every((seg) => !seg.includes('&') && isReadOnlyCommand(seg));
|
|
318
|
+
}
|
|
191
319
|
/**
|
|
192
320
|
* Should this tool call be refused because the session is in plan mode?
|
|
193
321
|
* Returns null when the call is allowed.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Message } from '../types';
|
|
2
|
+
export declare class ReadDedupe {
|
|
3
|
+
private readonly workDir?;
|
|
4
|
+
private readonly records;
|
|
5
|
+
constructor(workDir?: string | undefined);
|
|
6
|
+
private resolve;
|
|
7
|
+
private key;
|
|
8
|
+
/** The stub to return instead of re-reading, or null to read normally. */
|
|
9
|
+
check(input: Record<string, unknown>, messages: Message[]): string | null;
|
|
10
|
+
/** Remember a successful full read. Call only with a real (non-stub) read_file output. */
|
|
11
|
+
record(input: Record<string, unknown>, toolUseId: string): void;
|
|
12
|
+
/** Forget every recorded range of these absolute paths (a write tool touched them). */
|
|
13
|
+
invalidate(absPaths: string[]): void;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=readDedupe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readDedupe.d.ts","sourceRoot":"","sources":["../../src/agent/readDedupe.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAMxC,qBAAa,UAAU;IAGT,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAFrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;gBAE5B,OAAO,CAAC,EAAE,MAAM,YAAA;IAE7C,OAAO,CAAC,OAAO;IAMf,OAAO,CAAC,GAAG;IAQX,0EAA0E;IAC1E,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,GAAG,IAAI;IAezE,0FAA0F;IAC1F,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAS/D,uFAAuF;IACvF,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI;CAOrC"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// read_file de-duplication — Claude Code's "file unchanged since last read" stub.
|
|
3
|
+
//
|
|
4
|
+
// Agents re-read the same file constantly (before an edit, after a test run, while
|
|
5
|
+
// re-checking a plan). Each re-read re-sent up to 25K tokens that were ALREADY in the
|
|
6
|
+
// conversation, and every later request then carried both copies. When the file has not
|
|
7
|
+
// changed and the earlier result is still intact in the history, a one-line stub
|
|
8
|
+
// pointing at it is equivalent for the model and costs ~40 tokens.
|
|
9
|
+
//
|
|
10
|
+
// Safety is the whole design: the stub is only correct if the model can still SEE the
|
|
11
|
+
// earlier content. So a hit requires ALL of:
|
|
12
|
+
// • same resolved path, offset and limit as a previous successful read in THIS run
|
|
13
|
+
// (one instance per runAgentLoop — a sub-agent starts with a fresh context and a
|
|
14
|
+
// fresh instance, so it never gets pointed at its parent's reads);
|
|
15
|
+
// • the file's mtime and size are unchanged since that read, and no write tool has
|
|
16
|
+
// touched the path since (explicit invalidation — mtime alone can miss a same-ms edit);
|
|
17
|
+
// • that read's tool_result is still in the live message array, unpruned — compaction,
|
|
18
|
+
// pruneOldToolResults or a rewind all remove or stub it, which turns the hit off.
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.ReadDedupe = void 0;
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
const PRUNED_TAIL = 'pruned to conserve context. Re-run the tool if you need the full result.]';
|
|
57
|
+
class ReadDedupe {
|
|
58
|
+
constructor(workDir) {
|
|
59
|
+
this.workDir = workDir;
|
|
60
|
+
this.records = new Map();
|
|
61
|
+
}
|
|
62
|
+
resolve(p) {
|
|
63
|
+
if (typeof p !== 'string' || !p)
|
|
64
|
+
return null;
|
|
65
|
+
const expanded = p.startsWith('~/') ? path.join(process.env.HOME ?? '', p.slice(2)) : p;
|
|
66
|
+
return this.workDir ? path.resolve(this.workDir, expanded) : path.resolve(expanded);
|
|
67
|
+
}
|
|
68
|
+
key(input) {
|
|
69
|
+
const abs = this.resolve(input.path);
|
|
70
|
+
if (!abs)
|
|
71
|
+
return null;
|
|
72
|
+
const offset = typeof input.offset === 'number' ? Math.max(0, Math.floor(input.offset)) : 0;
|
|
73
|
+
const limit = typeof input.limit === 'number' ? Math.max(1, Math.floor(input.limit)) : 0;
|
|
74
|
+
return { key: `${abs}\0${offset}\0${limit}`, abs };
|
|
75
|
+
}
|
|
76
|
+
/** The stub to return instead of re-reading, or null to read normally. */
|
|
77
|
+
check(input, messages) {
|
|
78
|
+
const k = this.key(input);
|
|
79
|
+
if (!k)
|
|
80
|
+
return null;
|
|
81
|
+
const rec = this.records.get(k.key);
|
|
82
|
+
if (!rec)
|
|
83
|
+
return null;
|
|
84
|
+
let st;
|
|
85
|
+
try {
|
|
86
|
+
st = fs.statSync(k.abs);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
this.records.delete(k.key);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (st.mtimeMs !== rec.mtimeMs || st.size !== rec.size) {
|
|
93
|
+
this.records.delete(k.key);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (!resultStillIntact(messages, rec.toolUseId)) {
|
|
97
|
+
this.records.delete(k.key);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return (`File unchanged since your last read_file of it (same range). The content in that earlier ` +
|
|
101
|
+
`tool result is still current — refer to it instead of re-reading.`);
|
|
102
|
+
}
|
|
103
|
+
/** Remember a successful full read. Call only with a real (non-stub) read_file output. */
|
|
104
|
+
record(input, toolUseId) {
|
|
105
|
+
const k = this.key(input);
|
|
106
|
+
if (!k)
|
|
107
|
+
return;
|
|
108
|
+
try {
|
|
109
|
+
const st = fs.statSync(k.abs);
|
|
110
|
+
this.records.set(k.key, { toolUseId, mtimeMs: st.mtimeMs, size: st.size });
|
|
111
|
+
}
|
|
112
|
+
catch { /* unreadable now — nothing to remember */ }
|
|
113
|
+
}
|
|
114
|
+
/** Forget every recorded range of these absolute paths (a write tool touched them). */
|
|
115
|
+
invalidate(absPaths) {
|
|
116
|
+
if (!absPaths.length || !this.records.size)
|
|
117
|
+
return;
|
|
118
|
+
const set = new Set(absPaths);
|
|
119
|
+
for (const key of [...this.records.keys()]) {
|
|
120
|
+
if (set.has(key.slice(0, key.indexOf('\0'))))
|
|
121
|
+
this.records.delete(key);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.ReadDedupe = ReadDedupe;
|
|
126
|
+
function resultStillIntact(messages, toolUseId) {
|
|
127
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
128
|
+
const m = messages[i];
|
|
129
|
+
if (m.role !== 'user' || !Array.isArray(m.content))
|
|
130
|
+
continue;
|
|
131
|
+
for (const b of m.content) {
|
|
132
|
+
if (b?.type !== 'tool_result' || b.tool_use_id !== toolUseId)
|
|
133
|
+
continue;
|
|
134
|
+
if (b.is_error)
|
|
135
|
+
return false;
|
|
136
|
+
const text = typeof b.content === 'string'
|
|
137
|
+
? b.content
|
|
138
|
+
: Array.isArray(b.content)
|
|
139
|
+
? b.content.map((c) => c.text ?? '').join('')
|
|
140
|
+
: '';
|
|
141
|
+
return text.startsWith('[File:') && !text.trimEnd().endsWith(PRUNED_TAIL);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=readDedupe.js.map
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Consequently every pattern here is tuned for PRECISION over recall. A check that
|
|
21
21
|
// fires on ordinary code was removed rather than loosened. Deep analysis is the
|
|
22
|
-
//
|
|
22
|
+
// a dedicated security review's job; this only catches the flagrant cases at the
|
|
23
23
|
// moment they are written.
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
exports.checkSecurity = checkSecurity;
|
|
@@ -116,7 +116,7 @@ const RULES = [
|
|
|
116
116
|
// So the interpolation must name something that plausibly came from the
|
|
117
117
|
// outside: req/request/params/query/body/input/user/args, or a bare
|
|
118
118
|
// `'...' + ident`. This trades recall for precision on purpose — thorough SQL
|
|
119
|
-
// review is
|
|
119
|
+
// review is a dedicated security review's job, not an inline regex's.
|
|
120
120
|
//
|
|
121
121
|
// THE GAP IS PER-ALTERNATIVE, and that asymmetry is load-bearing. The concat
|
|
122
122
|
// branch originally shared the no-quote gap, which made it miss the QUOTED
|