@librechat/agents 3.1.77-dev.1 → 3.1.78-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/common/enum.cjs +54 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +148 -4
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +291 -0
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -0
- package/dist/cjs/llm/openai/index.cjs +317 -1
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +90 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/anthropicToolCache.cjs +102 -0
- package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -0
- package/dist/cjs/messages/prune.cjs +27 -0
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/recency.cjs +99 -0
- package/dist/cjs/messages/recency.cjs.map +1 -0
- package/dist/cjs/run.cjs +30 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +100 -6
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +635 -23
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/local/CompileCheckTool.cjs +227 -0
- package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -0
- package/dist/cjs/tools/local/FileCheckpointer.cjs +90 -0
- package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalCodingTools.cjs +1098 -0
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +1042 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalExecutionTools.cjs +122 -0
- package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +453 -0
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/local/attachments.cjs +183 -0
- package/dist/cjs/tools/local/attachments.cjs.map +1 -0
- package/dist/cjs/tools/local/bashAst.cjs +129 -0
- package/dist/cjs/tools/local/bashAst.cjs.map +1 -0
- package/dist/cjs/tools/local/editStrategies.cjs +188 -0
- package/dist/cjs/tools/local/editStrategies.cjs.map +1 -0
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +141 -0
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -0
- package/dist/cjs/tools/local/syntaxCheck.cjs +182 -0
- package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -0
- package/dist/cjs/tools/local/textEncoding.cjs +30 -0
- package/dist/cjs/tools/local/textEncoding.cjs.map +1 -0
- package/dist/cjs/tools/local/workspaceFS.cjs +51 -0
- package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +53 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +149 -5
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +289 -0
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -0
- package/dist/esm/llm/openai/index.mjs +318 -2
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +17 -2
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/anthropicToolCache.mjs +99 -0
- package/dist/esm/messages/anthropicToolCache.mjs.map +1 -0
- package/dist/esm/messages/prune.mjs +26 -1
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/recency.mjs +97 -0
- package/dist/esm/messages/recency.mjs.map +1 -0
- package/dist/esm/run.mjs +30 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +100 -6
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +635 -23
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/local/CompileCheckTool.mjs +223 -0
- package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -0
- package/dist/esm/tools/local/FileCheckpointer.mjs +87 -0
- package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -0
- package/dist/esm/tools/local/LocalCodingTools.mjs +1075 -0
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -0
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +1022 -0
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -0
- package/dist/esm/tools/local/LocalExecutionTools.mjs +117 -0
- package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -0
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +448 -0
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/local/attachments.mjs +180 -0
- package/dist/esm/tools/local/attachments.mjs.map +1 -0
- package/dist/esm/tools/local/bashAst.mjs +126 -0
- package/dist/esm/tools/local/bashAst.mjs.map +1 -0
- package/dist/esm/tools/local/editStrategies.mjs +185 -0
- package/dist/esm/tools/local/editStrategies.mjs.map +1 -0
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +137 -0
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -0
- package/dist/esm/tools/local/syntaxCheck.mjs +179 -0
- package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -0
- package/dist/esm/tools/local/textEncoding.mjs +27 -0
- package/dist/esm/tools/local/textEncoding.mjs.map +1 -0
- package/dist/esm/tools/local/workspaceFS.mjs +49 -0
- package/dist/esm/tools/local/workspaceFS.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +39 -1
- package/dist/types/graphs/Graph.d.ts +34 -0
- package/dist/types/hooks/createWorkspacePolicyHook.d.ts +95 -0
- package/dist/types/hooks/index.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/llm/openai/index.d.ts +17 -0
- package/dist/types/messages/anthropicToolCache.d.ts +51 -0
- package/dist/types/messages/index.d.ts +2 -0
- package/dist/types/messages/prune.d.ts +11 -0
- package/dist/types/messages/recency.d.ts +64 -0
- package/dist/types/run.d.ts +21 -0
- package/dist/types/tools/ToolNode.d.ts +145 -2
- package/dist/types/tools/local/CompileCheckTool.d.ts +31 -0
- package/dist/types/tools/local/FileCheckpointer.d.ts +39 -0
- package/dist/types/tools/local/LocalCodingTools.d.ts +57 -0
- package/dist/types/tools/local/LocalExecutionEngine.d.ts +149 -0
- package/dist/types/tools/local/LocalExecutionTools.d.ts +9 -0
- package/dist/types/tools/local/LocalProgrammaticToolCalling.d.ts +21 -0
- package/dist/types/tools/local/attachments.d.ts +84 -0
- package/dist/types/tools/local/bashAst.d.ts +11 -0
- package/dist/types/tools/local/editStrategies.d.ts +28 -0
- package/dist/types/tools/local/index.d.ts +12 -0
- package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +38 -0
- package/dist/types/tools/local/syntaxCheck.d.ts +42 -0
- package/dist/types/tools/local/textEncoding.d.ts +21 -0
- package/dist/types/tools/local/workspaceFS.d.ts +49 -0
- package/dist/types/types/hitl.d.ts +56 -27
- package/dist/types/types/run.d.ts +8 -1
- package/dist/types/types/summarize.d.ts +30 -0
- package/dist/types/types/tools.d.ts +341 -6
- package/package.json +21 -2
- package/src/common/enum.ts +54 -0
- package/src/graphs/Graph.ts +164 -6
- package/src/hooks/__tests__/compactHooks.test.ts +38 -2
- package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +393 -0
- package/src/hooks/createWorkspacePolicyHook.ts +355 -0
- package/src/hooks/index.ts +6 -0
- package/src/index.ts +1 -0
- package/src/llm/openai/deepseek.test.ts +479 -0
- package/src/llm/openai/index.ts +484 -1
- package/src/messages/__tests__/anthropicToolCache.test.ts +125 -0
- package/src/messages/__tests__/recency.test.ts +267 -0
- package/src/messages/anthropicToolCache.ts +116 -0
- package/src/messages/index.ts +2 -0
- package/src/messages/prune.ts +27 -1
- package/src/messages/recency.ts +155 -0
- package/src/run.ts +31 -0
- package/src/scripts/compare_pi_vs_ours.ts +840 -0
- package/src/scripts/local_engine.ts +166 -0
- package/src/scripts/local_engine_checkpointer.ts +205 -0
- package/src/scripts/local_engine_compile.ts +263 -0
- package/src/scripts/local_engine_hooks.ts +226 -0
- package/src/scripts/local_engine_image.ts +201 -0
- package/src/scripts/local_engine_ptc.ts +151 -0
- package/src/scripts/local_engine_workspace.ts +258 -0
- package/src/scripts/summarization-recency.ts +462 -0
- package/src/specs/prune.test.ts +39 -0
- package/src/summarization/__tests__/node.test.ts +499 -3
- package/src/summarization/node.ts +124 -7
- package/src/tools/ToolNode.ts +769 -20
- package/src/tools/__tests__/LocalExecutionTools.test.ts +2647 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +175 -0
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +114 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +84 -0
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +467 -0
- package/src/tools/__tests__/directToolHooks.test.ts +411 -0
- package/src/tools/__tests__/localToolNames.test.ts +73 -0
- package/src/tools/__tests__/workspaceSeam.test.ts +134 -0
- package/src/tools/local/CompileCheckTool.ts +278 -0
- package/src/tools/local/FileCheckpointer.ts +93 -0
- package/src/tools/local/LocalCodingTools.ts +1342 -0
- package/src/tools/local/LocalExecutionEngine.ts +1329 -0
- package/src/tools/local/LocalExecutionTools.ts +167 -0
- package/src/tools/local/LocalProgrammaticToolCalling.ts +594 -0
- package/src/tools/local/__tests__/FileCheckpointer.test.ts +120 -0
- package/src/tools/local/__tests__/editStrategies.test.ts +134 -0
- package/src/tools/local/attachments.ts +251 -0
- package/src/tools/local/bashAst.ts +151 -0
- package/src/tools/local/editStrategies.ts +188 -0
- package/src/tools/local/index.ts +12 -0
- package/src/tools/local/resolveLocalExecutionTools.ts +208 -0
- package/src/tools/local/syntaxCheck.ts +243 -0
- package/src/tools/local/textEncoding.ts +37 -0
- package/src/tools/local/workspaceFS.ts +89 -0
- package/src/types/hitl.ts +56 -27
- package/src/types/run.ts +12 -1
- package/src/types/summarize.ts +31 -0
- package/src/types/tools.ts +359 -7
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var promises = require('fs/promises');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects whether a file on disk is an LLM-renderable attachment
|
|
7
|
+
* (image / PDF) and produces the LangChain `MessageContentComplex[]`
|
|
8
|
+
* payload a `ToolMessage` needs to actually surface those bytes to
|
|
9
|
+
* the vision-capable model.
|
|
10
|
+
*
|
|
11
|
+
* Same approach as LibreChat's `api/server/utils/files.js`: sniff the
|
|
12
|
+
* magic bytes (NOT the extension) so a mislabelled `.png` that's
|
|
13
|
+
* really a binary blob doesn't get embedded as an image. Inlined for
|
|
14
|
+
* the five formats we actually care about (PNG / JPEG / GIF / WebP /
|
|
15
|
+
* PDF) instead of pulling the ESM-only `file-type` package — keeps
|
|
16
|
+
* the test setup CJS-clean.
|
|
17
|
+
*
|
|
18
|
+
* Provider compatibility:
|
|
19
|
+
* - Anthropic: tool_result content arrays accept `image` / `image_url`
|
|
20
|
+
* blocks; LangChain's anthropic adapter at
|
|
21
|
+
* `node_modules/@langchain/anthropic/dist/utils/message_inputs.js`
|
|
22
|
+
* converts them to native `image` source blocks.
|
|
23
|
+
* - OpenAI Chat Completions: image_url blocks in tool messages are
|
|
24
|
+
* accepted on vision-capable models.
|
|
25
|
+
* - OpenAI Responses API: tool messages are flattened to plain text;
|
|
26
|
+
* image_url blocks degrade to a JSON description (still useful as
|
|
27
|
+
* a textual hint to the model).
|
|
28
|
+
* - Google: image blocks in tool responses are accepted on Gemini
|
|
29
|
+
* vision models.
|
|
30
|
+
*
|
|
31
|
+
* Configuration:
|
|
32
|
+
* - `local.attachReadAttachments` (default `'images-only'`) controls
|
|
33
|
+
* which file kinds are returned as inline attachments. Other kinds
|
|
34
|
+
* fall through to the existing binary-stub path.
|
|
35
|
+
* - `local.maxAttachmentBytes` (default 5 MB) caps the pre-encoding
|
|
36
|
+
* size; oversize attachments degrade to a stub describing the
|
|
37
|
+
* refusal so the model isn't surprised.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Magic-byte sniff for the small set of image/PDF formats we care
|
|
41
|
+
* about. We avoided pulling in `file-type` (ESM-only, awkward under
|
|
42
|
+
* ts-jest) since the universe of attachments we want to embed is
|
|
43
|
+
* tiny: PNG, JPEG, GIF, WebP, PDF. All have well-known signatures in
|
|
44
|
+
* the first 12 bytes.
|
|
45
|
+
*
|
|
46
|
+
* Returns `undefined` on no match — caller treats as text/unknown.
|
|
47
|
+
*/
|
|
48
|
+
function sniffMime(buffer) {
|
|
49
|
+
if (buffer.length < 4)
|
|
50
|
+
return undefined;
|
|
51
|
+
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
|
52
|
+
if (buffer.length >= 8 &&
|
|
53
|
+
buffer[0] === 0x89 &&
|
|
54
|
+
buffer[1] === 0x50 &&
|
|
55
|
+
buffer[2] === 0x4e &&
|
|
56
|
+
buffer[3] === 0x47 &&
|
|
57
|
+
buffer[4] === 0x0d &&
|
|
58
|
+
buffer[5] === 0x0a &&
|
|
59
|
+
buffer[6] === 0x1a &&
|
|
60
|
+
buffer[7] === 0x0a) {
|
|
61
|
+
return 'image/png';
|
|
62
|
+
}
|
|
63
|
+
// JPEG: FF D8 FF
|
|
64
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
|
65
|
+
return 'image/jpeg';
|
|
66
|
+
}
|
|
67
|
+
// GIF: "GIF87a" or "GIF89a"
|
|
68
|
+
if (buffer.length >= 6 &&
|
|
69
|
+
buffer[0] === 0x47 &&
|
|
70
|
+
buffer[1] === 0x49 &&
|
|
71
|
+
buffer[2] === 0x46 &&
|
|
72
|
+
buffer[3] === 0x38 &&
|
|
73
|
+
(buffer[4] === 0x37 || buffer[4] === 0x39) &&
|
|
74
|
+
buffer[5] === 0x61) {
|
|
75
|
+
return 'image/gif';
|
|
76
|
+
}
|
|
77
|
+
// WebP: "RIFF" .... "WEBP"
|
|
78
|
+
if (buffer.length >= 12 &&
|
|
79
|
+
buffer[0] === 0x52 &&
|
|
80
|
+
buffer[1] === 0x49 &&
|
|
81
|
+
buffer[2] === 0x46 &&
|
|
82
|
+
buffer[3] === 0x46 &&
|
|
83
|
+
buffer[8] === 0x57 &&
|
|
84
|
+
buffer[9] === 0x45 &&
|
|
85
|
+
buffer[10] === 0x42 &&
|
|
86
|
+
buffer[11] === 0x50) {
|
|
87
|
+
return 'image/webp';
|
|
88
|
+
}
|
|
89
|
+
// PDF: "%PDF-"
|
|
90
|
+
if (buffer.length >= 5 &&
|
|
91
|
+
buffer[0] === 0x25 &&
|
|
92
|
+
buffer[1] === 0x50 &&
|
|
93
|
+
buffer[2] === 0x44 &&
|
|
94
|
+
buffer[3] === 0x46 &&
|
|
95
|
+
buffer[4] === 0x2d) {
|
|
96
|
+
return 'application/pdf';
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const SUPPORTED_IMAGE_MIMES = new Set([
|
|
101
|
+
'image/png',
|
|
102
|
+
'image/jpeg',
|
|
103
|
+
'image/gif',
|
|
104
|
+
'image/webp',
|
|
105
|
+
]);
|
|
106
|
+
/** Mime types that get returned to the model as inline attachments. */
|
|
107
|
+
new Set([
|
|
108
|
+
...SUPPORTED_IMAGE_MIMES,
|
|
109
|
+
'application/pdf',
|
|
110
|
+
]);
|
|
111
|
+
async function classifyAttachment(args) {
|
|
112
|
+
if (args.bytes === 0) {
|
|
113
|
+
return { kind: 'text-or-unknown', bytes: 0 };
|
|
114
|
+
}
|
|
115
|
+
// MIME sniffing only needs the first 12 bytes — read just the
|
|
116
|
+
// header so a 9 MB PNG (under the 10 MB read cap, over the 5 MB
|
|
117
|
+
// attachment cap) doesn't pull the whole buffer into memory before
|
|
118
|
+
// we discover it's oversize. Full read happens only when we're
|
|
119
|
+
// about to base64-embed.
|
|
120
|
+
const open = args.fs?.open ?? promises.open;
|
|
121
|
+
const handle = await open(args.path, 'r');
|
|
122
|
+
const header = Buffer.alloc(12);
|
|
123
|
+
let mime;
|
|
124
|
+
try {
|
|
125
|
+
await handle.read(header, 0, 12, 0);
|
|
126
|
+
mime = sniffMime(header);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await handle.close();
|
|
130
|
+
}
|
|
131
|
+
if (mime == null) {
|
|
132
|
+
return { kind: 'text-or-unknown', bytes: args.bytes };
|
|
133
|
+
}
|
|
134
|
+
const wantsImage = args.mode === 'images-only' || args.mode === 'images-and-pdf';
|
|
135
|
+
const wantsPdf = args.mode === 'images-and-pdf';
|
|
136
|
+
const isImage = wantsImage && SUPPORTED_IMAGE_MIMES.has(mime);
|
|
137
|
+
const isPdf = wantsPdf && mime === 'application/pdf';
|
|
138
|
+
if (!isImage && !isPdf) {
|
|
139
|
+
// Both branches returned identical values pre-fix (audit-of-audit
|
|
140
|
+
// finding #3). The SUPPORTED_ATTACHMENT_MIMES check was dead code —
|
|
141
|
+
// collapsing to a single return.
|
|
142
|
+
return { kind: 'binary', mime, bytes: args.bytes };
|
|
143
|
+
}
|
|
144
|
+
if (args.bytes > args.maxBytes) {
|
|
145
|
+
return {
|
|
146
|
+
kind: 'oversize',
|
|
147
|
+
mime,
|
|
148
|
+
bytes: args.bytes,
|
|
149
|
+
maxBytes: args.maxBytes,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const readFile = args.fs?.readFile ?? promises.readFile;
|
|
153
|
+
const buffer = (await readFile(args.path));
|
|
154
|
+
const base64 = buffer.toString('base64');
|
|
155
|
+
const dataUrl = `data:${mime};base64,${base64}`;
|
|
156
|
+
if (isImage) {
|
|
157
|
+
return { kind: 'image', mime, bytes: args.bytes, dataUrl };
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
kind: 'pdf',
|
|
161
|
+
mime: 'application/pdf',
|
|
162
|
+
bytes: args.bytes,
|
|
163
|
+
dataUrl,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Build the LangChain content array for an image attachment. */
|
|
167
|
+
function imageAttachmentContent(path, attachment) {
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
type: 'text',
|
|
171
|
+
text: `Read ${path} (${attachment.mime}, ${attachment.bytes} bytes). ` +
|
|
172
|
+
'The image is attached below for vision-capable models.',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
type: 'image_url',
|
|
176
|
+
image_url: { url: attachment.dataUrl },
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
exports.classifyAttachment = classifyAttachment;
|
|
182
|
+
exports.imageAttachmentContent = imageAttachmentContent;
|
|
183
|
+
//# sourceMappingURL=attachments.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachments.cjs","sources":["../../../../src/tools/local/attachments.ts"],"sourcesContent":["/**\n * Detects whether a file on disk is an LLM-renderable attachment\n * (image / PDF) and produces the LangChain `MessageContentComplex[]`\n * payload a `ToolMessage` needs to actually surface those bytes to\n * the vision-capable model.\n *\n * Same approach as LibreChat's `api/server/utils/files.js`: sniff the\n * magic bytes (NOT the extension) so a mislabelled `.png` that's\n * really a binary blob doesn't get embedded as an image. Inlined for\n * the five formats we actually care about (PNG / JPEG / GIF / WebP /\n * PDF) instead of pulling the ESM-only `file-type` package — keeps\n * the test setup CJS-clean.\n *\n * Provider compatibility:\n * - Anthropic: tool_result content arrays accept `image` / `image_url`\n * blocks; LangChain's anthropic adapter at\n * `node_modules/@langchain/anthropic/dist/utils/message_inputs.js`\n * converts them to native `image` source blocks.\n * - OpenAI Chat Completions: image_url blocks in tool messages are\n * accepted on vision-capable models.\n * - OpenAI Responses API: tool messages are flattened to plain text;\n * image_url blocks degrade to a JSON description (still useful as\n * a textual hint to the model).\n * - Google: image blocks in tool responses are accepted on Gemini\n * vision models.\n *\n * Configuration:\n * - `local.attachReadAttachments` (default `'images-only'`) controls\n * which file kinds are returned as inline attachments. Other kinds\n * fall through to the existing binary-stub path.\n * - `local.maxAttachmentBytes` (default 5 MB) caps the pre-encoding\n * size; oversize attachments degrade to a stub describing the\n * refusal so the model isn't surprised.\n */\n\nimport { open as fsOpen, readFile as fsReadFile } from 'fs/promises';\nimport type { WorkspaceFS } from './workspaceFS';\n\n/**\n * Magic-byte sniff for the small set of image/PDF formats we care\n * about. We avoided pulling in `file-type` (ESM-only, awkward under\n * ts-jest) since the universe of attachments we want to embed is\n * tiny: PNG, JPEG, GIF, WebP, PDF. All have well-known signatures in\n * the first 12 bytes.\n *\n * Returns `undefined` on no match — caller treats as text/unknown.\n */\nfunction sniffMime(buffer: Buffer): string | undefined {\n if (buffer.length < 4) return undefined;\n // PNG: 89 50 4E 47 0D 0A 1A 0A\n if (\n buffer.length >= 8 &&\n buffer[0] === 0x89 &&\n buffer[1] === 0x50 &&\n buffer[2] === 0x4e &&\n buffer[3] === 0x47 &&\n buffer[4] === 0x0d &&\n buffer[5] === 0x0a &&\n buffer[6] === 0x1a &&\n buffer[7] === 0x0a\n ) {\n return 'image/png';\n }\n // JPEG: FF D8 FF\n if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {\n return 'image/jpeg';\n }\n // GIF: \"GIF87a\" or \"GIF89a\"\n if (\n buffer.length >= 6 &&\n buffer[0] === 0x47 &&\n buffer[1] === 0x49 &&\n buffer[2] === 0x46 &&\n buffer[3] === 0x38 &&\n (buffer[4] === 0x37 || buffer[4] === 0x39) &&\n buffer[5] === 0x61\n ) {\n return 'image/gif';\n }\n // WebP: \"RIFF\" .... \"WEBP\"\n if (\n buffer.length >= 12 &&\n buffer[0] === 0x52 &&\n buffer[1] === 0x49 &&\n buffer[2] === 0x46 &&\n buffer[3] === 0x46 &&\n buffer[8] === 0x57 &&\n buffer[9] === 0x45 &&\n buffer[10] === 0x42 &&\n buffer[11] === 0x50\n ) {\n return 'image/webp';\n }\n // PDF: \"%PDF-\"\n if (\n buffer.length >= 5 &&\n buffer[0] === 0x25 &&\n buffer[1] === 0x50 &&\n buffer[2] === 0x44 &&\n buffer[3] === 0x46 &&\n buffer[4] === 0x2d\n ) {\n return 'application/pdf';\n }\n return undefined;\n}\n\nconst SUPPORTED_IMAGE_MIMES = new Set<string>([\n 'image/png',\n 'image/jpeg',\n 'image/gif',\n 'image/webp',\n]);\n\n/** Mime types that get returned to the model as inline attachments. */\nconst SUPPORTED_ATTACHMENT_MIMES = new Set<string>([\n ...SUPPORTED_IMAGE_MIMES,\n 'application/pdf',\n]);\n\nexport type AttachmentMode = 'images-only' | 'images-and-pdf' | 'off';\n\nexport type Attachment =\n | {\n kind: 'image';\n mime: string;\n bytes: number;\n dataUrl: string;\n }\n | {\n kind: 'pdf';\n mime: 'application/pdf';\n bytes: number;\n dataUrl: string;\n }\n | {\n kind: 'binary';\n mime: string;\n bytes: number;\n }\n | {\n kind: 'oversize';\n mime: string;\n bytes: number;\n maxBytes: number;\n }\n | {\n kind: 'text-or-unknown';\n bytes: number;\n };\n\nexport async function classifyAttachment(args: {\n path: string;\n bytes: number;\n mode: AttachmentMode;\n maxBytes: number;\n /**\n * WorkspaceFS to route I/O through — defaults to host fs/promises\n * for backward compat. Manual review (finding F): without this\n * routing, custom/remote FS implementations could either fail to\n * embed valid attachments or accidentally read a host path with\n * the same absolute name (since `read_file` itself does go through\n * the configured WorkspaceFS).\n */\n fs?: WorkspaceFS;\n}): Promise<Attachment> {\n if (args.bytes === 0) {\n return { kind: 'text-or-unknown', bytes: 0 };\n }\n\n // MIME sniffing only needs the first 12 bytes — read just the\n // header so a 9 MB PNG (under the 10 MB read cap, over the 5 MB\n // attachment cap) doesn't pull the whole buffer into memory before\n // we discover it's oversize. Full read happens only when we're\n // about to base64-embed.\n const open = args.fs?.open ?? fsOpen;\n const handle = await open(args.path, 'r');\n const header = Buffer.alloc(12);\n let mime: string | undefined;\n try {\n await handle.read(header, 0, 12, 0);\n mime = sniffMime(header);\n } finally {\n await handle.close();\n }\n\n if (mime == null) {\n return { kind: 'text-or-unknown', bytes: args.bytes };\n }\n\n const wantsImage =\n args.mode === 'images-only' || args.mode === 'images-and-pdf';\n const wantsPdf = args.mode === 'images-and-pdf';\n\n const isImage = wantsImage && SUPPORTED_IMAGE_MIMES.has(mime);\n const isPdf = wantsPdf && mime === 'application/pdf';\n\n if (!isImage && !isPdf) {\n // Both branches returned identical values pre-fix (audit-of-audit\n // finding #3). The SUPPORTED_ATTACHMENT_MIMES check was dead code —\n // collapsing to a single return.\n return { kind: 'binary', mime, bytes: args.bytes };\n }\n\n if (args.bytes > args.maxBytes) {\n return {\n kind: 'oversize',\n mime,\n bytes: args.bytes,\n maxBytes: args.maxBytes,\n };\n }\n\n const readFile = args.fs?.readFile ?? fsReadFile;\n const buffer = (await readFile(args.path)) as Buffer;\n const base64 = buffer.toString('base64');\n const dataUrl = `data:${mime};base64,${base64}`;\n\n if (isImage) {\n return { kind: 'image', mime, bytes: args.bytes, dataUrl };\n }\n return {\n kind: 'pdf',\n mime: 'application/pdf' as const,\n bytes: args.bytes,\n dataUrl,\n };\n}\n\n/** Build the LangChain content array for an image attachment. */\nexport function imageAttachmentContent(\n path: string,\n attachment: Extract<Attachment, { kind: 'image' }>\n): Array<{\n type: 'text' | 'image_url';\n text?: string;\n image_url?: { url: string };\n}> {\n return [\n {\n type: 'text',\n text:\n `Read ${path} (${attachment.mime}, ${attachment.bytes} bytes). ` +\n 'The image is attached below for vision-capable models.',\n },\n {\n type: 'image_url',\n image_url: { url: attachment.dataUrl },\n },\n ];\n}\n"],"names":["fsOpen","fsReadFile"],"mappings":"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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Categorical-hazard checks layered on top of the existing dangerous-
|
|
5
|
+
* command regex set. These match command-shape signatures that
|
|
6
|
+
* claude-code's tree-sitter AST validator catches via categorical
|
|
7
|
+
* deny-lists (command substitution, zsh-only privileged commands,
|
|
8
|
+
* /proc/<pid>/environ access, IFS injection, etc.).
|
|
9
|
+
*
|
|
10
|
+
* This is *not* a real AST parser. It is a deliberately conservative
|
|
11
|
+
* heuristic pass intended for the local engine's `bashAst: 'auto' |
|
|
12
|
+
* 'strict'` modes; a future PR can swap in a true tree-sitter-bash
|
|
13
|
+
* pass behind the same config field without changing the public API.
|
|
14
|
+
*
|
|
15
|
+
* `runBashAstChecks` runs on the *quote-stripped* command (so quoted
|
|
16
|
+
* strings inside the script don't generate false positives) and
|
|
17
|
+
* returns one finding per matched category.
|
|
18
|
+
*/
|
|
19
|
+
const COMMAND_SUBSTITUTION_PATTERNS = [
|
|
20
|
+
{ code: 'cmd-subst-dollar-paren', rx: /\$\(/ },
|
|
21
|
+
{ code: 'cmd-subst-backtick', rx: /`[^`]*`/ },
|
|
22
|
+
{ code: 'cmd-subst-process-sub', rx: /[<>]\(/ },
|
|
23
|
+
{ code: 'cmd-subst-zsh-eq', rx: /(?:^|\s)=[A-Za-z_]/ },
|
|
24
|
+
];
|
|
25
|
+
const ZSH_DANGEROUS_BUILTINS = [
|
|
26
|
+
'zmodload',
|
|
27
|
+
'emulate',
|
|
28
|
+
'sysopen',
|
|
29
|
+
'sysread',
|
|
30
|
+
'syswrite',
|
|
31
|
+
'ztcp',
|
|
32
|
+
'zsocket',
|
|
33
|
+
'zf_rm',
|
|
34
|
+
'zselect',
|
|
35
|
+
];
|
|
36
|
+
const STRICT_DENIED_BUILTINS = [
|
|
37
|
+
'eval',
|
|
38
|
+
'exec',
|
|
39
|
+
];
|
|
40
|
+
function rxForBuiltin(name) {
|
|
41
|
+
return new RegExp(`\\b${name}\\b`);
|
|
42
|
+
}
|
|
43
|
+
const PROC_ENVIRON_RX = /\/proc\/(?:\d+|self|\$[A-Za-z_])\/environ\b/;
|
|
44
|
+
const IFS_INJECTION_RX = /\bIFS\s*=/;
|
|
45
|
+
const HEX_ESCAPE_OBFUSCATION_RX = /\\x[0-9a-fA-F]{2}/;
|
|
46
|
+
const SOURCE_FROM_VAR_RX = /(?:^|\s)(?:source|\.)\s+["']?\$[A-Za-z_]/;
|
|
47
|
+
function runBashAstChecks(command, mode = 'off') {
|
|
48
|
+
if (mode === 'off') {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const findings = [];
|
|
52
|
+
const strict = mode === 'strict';
|
|
53
|
+
for (const { code, rx } of COMMAND_SUBSTITUTION_PATTERNS) {
|
|
54
|
+
if (rx.test(command)) {
|
|
55
|
+
findings.push({
|
|
56
|
+
code,
|
|
57
|
+
message: 'Command substitution can mask intent and exfiltrate variables; not allowed under bashAst.',
|
|
58
|
+
severity: strict ? 'deny' : 'warn',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const builtin of ZSH_DANGEROUS_BUILTINS) {
|
|
63
|
+
if (rxForBuiltin(builtin).test(command)) {
|
|
64
|
+
findings.push({
|
|
65
|
+
code: `zsh-builtin-${builtin}`,
|
|
66
|
+
message: `Zsh privileged builtin "${builtin}" is denied.`,
|
|
67
|
+
severity: 'deny',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (PROC_ENVIRON_RX.test(command)) {
|
|
72
|
+
findings.push({
|
|
73
|
+
code: 'proc-environ-read',
|
|
74
|
+
message: 'Reads from /proc/<pid>/environ are denied — leaks host secrets.',
|
|
75
|
+
severity: 'deny',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (IFS_INJECTION_RX.test(command)) {
|
|
79
|
+
findings.push({
|
|
80
|
+
code: 'ifs-injection',
|
|
81
|
+
message: 'Inline IFS reassignment is suspicious; review the command.',
|
|
82
|
+
severity: strict ? 'deny' : 'warn',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (HEX_ESCAPE_OBFUSCATION_RX.test(command)) {
|
|
86
|
+
findings.push({
|
|
87
|
+
code: 'hex-escape',
|
|
88
|
+
message: 'Hex-escaped bytes (\\xNN) often hide intent; review the command.',
|
|
89
|
+
severity: strict ? 'deny' : 'warn',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (SOURCE_FROM_VAR_RX.test(command)) {
|
|
93
|
+
findings.push({
|
|
94
|
+
code: 'source-from-variable',
|
|
95
|
+
message: 'Sourcing a script from an unbound variable is denied.',
|
|
96
|
+
severity: 'deny',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (strict) {
|
|
100
|
+
for (const builtin of STRICT_DENIED_BUILTINS) {
|
|
101
|
+
if (rxForBuiltin(builtin).test(command)) {
|
|
102
|
+
findings.push({
|
|
103
|
+
code: `strict-${builtin}`,
|
|
104
|
+
message: `In strict mode, "${builtin}" is denied.`,
|
|
105
|
+
severity: 'deny',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return findings;
|
|
111
|
+
}
|
|
112
|
+
function bashAstFindingsToErrors(findings) {
|
|
113
|
+
const errors = [];
|
|
114
|
+
const warnings = [];
|
|
115
|
+
for (const f of findings) {
|
|
116
|
+
const formatted = `[bashAst:${f.code}] ${f.message}`;
|
|
117
|
+
if (f.severity === 'deny') {
|
|
118
|
+
errors.push(formatted);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
warnings.push(formatted);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { errors, warnings };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
exports.bashAstFindingsToErrors = bashAstFindingsToErrors;
|
|
128
|
+
exports.runBashAstChecks = runBashAstChecks;
|
|
129
|
+
//# sourceMappingURL=bashAst.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bashAst.cjs","sources":["../../../../src/tools/local/bashAst.ts"],"sourcesContent":["import type * as t from '@/types';\n\nexport type BashAstFinding = {\n code: string;\n message: string;\n severity: 'warn' | 'deny';\n};\n\n/**\n * Categorical-hazard checks layered on top of the existing dangerous-\n * command regex set. These match command-shape signatures that\n * claude-code's tree-sitter AST validator catches via categorical\n * deny-lists (command substitution, zsh-only privileged commands,\n * /proc/<pid>/environ access, IFS injection, etc.).\n *\n * This is *not* a real AST parser. It is a deliberately conservative\n * heuristic pass intended for the local engine's `bashAst: 'auto' |\n * 'strict'` modes; a future PR can swap in a true tree-sitter-bash\n * pass behind the same config field without changing the public API.\n *\n * `runBashAstChecks` runs on the *quote-stripped* command (so quoted\n * strings inside the script don't generate false positives) and\n * returns one finding per matched category.\n */\n\nconst COMMAND_SUBSTITUTION_PATTERNS: { code: string; rx: RegExp }[] = [\n { code: 'cmd-subst-dollar-paren', rx: /\\$\\(/ },\n { code: 'cmd-subst-backtick', rx: /`[^`]*`/ },\n { code: 'cmd-subst-process-sub', rx: /[<>]\\(/ },\n { code: 'cmd-subst-zsh-eq', rx: /(?:^|\\s)=[A-Za-z_]/ },\n];\n\nconst ZSH_DANGEROUS_BUILTINS = [\n 'zmodload',\n 'emulate',\n 'sysopen',\n 'sysread',\n 'syswrite',\n 'ztcp',\n 'zsocket',\n 'zf_rm',\n 'zselect',\n];\n\nconst STRICT_DENIED_BUILTINS = [\n 'eval',\n 'exec',\n];\n\nfunction rxForBuiltin(name: string): RegExp {\n return new RegExp(`\\\\b${name}\\\\b`);\n}\n\nconst PROC_ENVIRON_RX = /\\/proc\\/(?:\\d+|self|\\$[A-Za-z_])\\/environ\\b/;\nconst IFS_INJECTION_RX = /\\bIFS\\s*=/;\nconst HEX_ESCAPE_OBFUSCATION_RX = /\\\\x[0-9a-fA-F]{2}/;\nconst SOURCE_FROM_VAR_RX = /(?:^|\\s)(?:source|\\.)\\s+[\"']?\\$[A-Za-z_]/;\n\nexport function runBashAstChecks(\n command: string,\n mode: t.LocalBashAstMode = 'off'\n): BashAstFinding[] {\n if (mode === 'off') {\n return [];\n }\n const findings: BashAstFinding[] = [];\n const strict = mode === 'strict';\n\n for (const { code, rx } of COMMAND_SUBSTITUTION_PATTERNS) {\n if (rx.test(command)) {\n findings.push({\n code,\n message:\n 'Command substitution can mask intent and exfiltrate variables; not allowed under bashAst.',\n severity: strict ? 'deny' : 'warn',\n });\n }\n }\n\n for (const builtin of ZSH_DANGEROUS_BUILTINS) {\n if (rxForBuiltin(builtin).test(command)) {\n findings.push({\n code: `zsh-builtin-${builtin}`,\n message: `Zsh privileged builtin \"${builtin}\" is denied.`,\n severity: 'deny',\n });\n }\n }\n\n if (PROC_ENVIRON_RX.test(command)) {\n findings.push({\n code: 'proc-environ-read',\n message: 'Reads from /proc/<pid>/environ are denied — leaks host secrets.',\n severity: 'deny',\n });\n }\n\n if (IFS_INJECTION_RX.test(command)) {\n findings.push({\n code: 'ifs-injection',\n message: 'Inline IFS reassignment is suspicious; review the command.',\n severity: strict ? 'deny' : 'warn',\n });\n }\n\n if (HEX_ESCAPE_OBFUSCATION_RX.test(command)) {\n findings.push({\n code: 'hex-escape',\n message: 'Hex-escaped bytes (\\\\xNN) often hide intent; review the command.',\n severity: strict ? 'deny' : 'warn',\n });\n }\n\n if (SOURCE_FROM_VAR_RX.test(command)) {\n findings.push({\n code: 'source-from-variable',\n message: 'Sourcing a script from an unbound variable is denied.',\n severity: 'deny',\n });\n }\n\n if (strict) {\n for (const builtin of STRICT_DENIED_BUILTINS) {\n if (rxForBuiltin(builtin).test(command)) {\n findings.push({\n code: `strict-${builtin}`,\n message: `In strict mode, \"${builtin}\" is denied.`,\n severity: 'deny',\n });\n }\n }\n }\n\n return findings;\n}\n\nexport function bashAstFindingsToErrors(\n findings: BashAstFinding[]\n): { errors: string[]; warnings: string[] } {\n const errors: string[] = [];\n const warnings: string[] = [];\n for (const f of findings) {\n const formatted = `[bashAst:${f.code}] ${f.message}`;\n if (f.severity === 'deny') {\n errors.push(formatted);\n } else {\n warnings.push(formatted);\n }\n }\n return { errors, warnings };\n}\n"],"names":[],"mappings":";;AAQA;;;;;;;;;;;;;;;AAeG;AAEH,MAAM,6BAA6B,GAAmC;AACpE,IAAA,EAAE,IAAI,EAAE,wBAAwB,EAAE,EAAE,EAAE,MAAM,EAAE;AAC9C,IAAA,EAAE,IAAI,EAAE,oBAAoB,EAAE,EAAE,EAAE,SAAS,EAAE;AAC7C,IAAA,EAAE,IAAI,EAAE,uBAAuB,EAAE,EAAE,EAAE,QAAQ,EAAE;AAC/C,IAAA,EAAE,IAAI,EAAE,kBAAkB,EAAE,EAAE,EAAE,oBAAoB,EAAE;CACvD;AAED,MAAM,sBAAsB,GAAG;IAC7B,UAAU;IACV,SAAS;IACT,SAAS;IACT,SAAS;IACT,UAAU;IACV,MAAM;IACN,SAAS;IACT,OAAO;IACP,SAAS;CACV;AAED,MAAM,sBAAsB,GAAG;IAC7B,MAAM;IACN,MAAM;CACP;AAED,SAAS,YAAY,CAAC,IAAY,EAAA;AAChC,IAAA,OAAO,IAAI,MAAM,CAAC,MAAM,IAAI,CAAA,GAAA,CAAK,CAAC;AACpC;AAEA,MAAM,eAAe,GAAG,6CAA6C;AACrE,MAAM,gBAAgB,GAAG,WAAW;AACpC,MAAM,yBAAyB,GAAG,mBAAmB;AACrD,MAAM,kBAAkB,GAAG,0CAA0C;SAErD,gBAAgB,CAC9B,OAAe,EACf,OAA2B,KAAK,EAAA;AAEhC,IAAA,IAAI,IAAI,KAAK,KAAK,EAAE;AAClB,QAAA,OAAO,EAAE;IACX;IACA,MAAM,QAAQ,GAAqB,EAAE;AACrC,IAAA,MAAM,MAAM,GAAG,IAAI,KAAK,QAAQ;IAEhC,KAAK,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,6BAA6B,EAAE;AACxD,QAAA,IAAI,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;YACpB,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI;AACJ,gBAAA,OAAO,EACL,2FAA2F;gBAC7F,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM;AACnC,aAAA,CAAC;QACJ;IACF;AAEA,IAAA,KAAK,MAAM,OAAO,IAAI,sBAAsB,EAAE;QAC5C,IAAI,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;YACvC,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,CAAA,YAAA,EAAe,OAAO,CAAA,CAAE;gBAC9B,OAAO,EAAE,CAAA,wBAAA,EAA2B,OAAO,CAAA,YAAA,CAAc;AACzD,gBAAA,QAAQ,EAAE,MAAM;AACjB,aAAA,CAAC;QACJ;IACF;AAEA,IAAA,IAAI,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;QACjC,QAAQ,CAAC,IAAI,CAAC;AACZ,YAAA,IAAI,EAAE,mBAAmB;AACzB,YAAA,OAAO,EAAE,iEAAiE;AAC1E,YAAA,QAAQ,EAAE,MAAM;AACjB,SAAA,CAAC;IACJ;AAEA,IAAA,IAAI,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;QAClC,QAAQ,CAAC,IAAI,CAAC;AACZ,YAAA,IAAI,EAAE,eAAe;AACrB,YAAA,OAAO,EAAE,4DAA4D;YACrE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM;AACnC,SAAA,CAAC;IACJ;AAEA,IAAA,IAAI,yBAAyB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;QAC3C,QAAQ,CAAC,IAAI,CAAC;AACZ,YAAA,IAAI,EAAE,YAAY;AAClB,YAAA,OAAO,EAAE,kEAAkE;YAC3E,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM;AACnC,SAAA,CAAC;IACJ;AAEA,IAAA,IAAI,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;QACpC,QAAQ,CAAC,IAAI,CAAC;AACZ,YAAA,IAAI,EAAE,sBAAsB;AAC5B,YAAA,OAAO,EAAE,uDAAuD;AAChE,YAAA,QAAQ,EAAE,MAAM;AACjB,SAAA,CAAC;IACJ;IAEA,IAAI,MAAM,EAAE;AACV,QAAA,KAAK,MAAM,OAAO,IAAI,sBAAsB,EAAE;YAC5C,IAAI,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;gBACvC,QAAQ,CAAC,IAAI,CAAC;oBACZ,IAAI,EAAE,CAAA,OAAA,EAAU,OAAO,CAAA,CAAE;oBACzB,OAAO,EAAE,CAAA,iBAAA,EAAoB,OAAO,CAAA,YAAA,CAAc;AAClD,oBAAA,QAAQ,EAAE,MAAM;AACjB,iBAAA,CAAC;YACJ;QACF;IACF;AAEA,IAAA,OAAO,QAAQ;AACjB;AAEM,SAAU,uBAAuB,CACrC,QAA0B,EAAA;IAE1B,MAAM,MAAM,GAAa,EAAE;IAC3B,MAAM,QAAQ,GAAa,EAAE;AAC7B,IAAA,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE;QACxB,MAAM,SAAS,GAAG,CAAA,SAAA,EAAY,CAAC,CAAC,IAAI,CAAA,EAAA,EAAK,CAAC,CAAC,OAAO,CAAA,CAAE;AACpD,QAAA,IAAI,CAAC,CAAC,QAAQ,KAAK,MAAM,EAAE;AACzB,YAAA,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;QACxB;aAAO;AACL,YAAA,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC;QAC1B;IACF;AACA,IAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE;AAC7B;;;;;"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Single-occurrence string-replacement strategies for `edit_file`.
|
|
5
|
+
*
|
|
6
|
+
* The LLM frequently emits an `oldString` whose whitespace, indentation,
|
|
7
|
+
* or escape sequences are slightly off from the on-disk content. Rather
|
|
8
|
+
* than failing the call (which forces a re-read + retry round-trip),
|
|
9
|
+
* we walk a chain of progressively looser matchers, stopping at the
|
|
10
|
+
* first one that locates exactly one match. The matched on-disk slice
|
|
11
|
+
* is then literally replaced with `newString` — we never modify
|
|
12
|
+
* `newString`, only the search.
|
|
13
|
+
*
|
|
14
|
+
* Strategies are ordered from strict to lenient so we don't accidentally
|
|
15
|
+
* over-match a more specific pattern with a looser one. Inspired by
|
|
16
|
+
* opencode's nine-strategy chain (sst/opencode), trimmed to the four
|
|
17
|
+
* highest-yield strategies for a first cut. Add more (block-anchor +
|
|
18
|
+
* Levenshtein, escape-normalized, etc.) as needed.
|
|
19
|
+
*/
|
|
20
|
+
const exactStrategy = (source, oldString) => {
|
|
21
|
+
if (oldString === '')
|
|
22
|
+
return null;
|
|
23
|
+
const first = source.indexOf(oldString);
|
|
24
|
+
if (first === -1)
|
|
25
|
+
return null;
|
|
26
|
+
const second = source.indexOf(oldString, first + oldString.length);
|
|
27
|
+
if (second !== -1)
|
|
28
|
+
return null;
|
|
29
|
+
return { strategy: 'exact', start: first, end: first + oldString.length };
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Match per-line, ignoring trailing whitespace differences. Useful for
|
|
33
|
+
* the very common case where the LLM stripped trailing spaces or added
|
|
34
|
+
* an extra blank.
|
|
35
|
+
*/
|
|
36
|
+
const lineTrimmedStrategy = (source, oldString) => {
|
|
37
|
+
if (oldString === '')
|
|
38
|
+
return null;
|
|
39
|
+
const sourceLines = source.split('\n');
|
|
40
|
+
const oldLines = oldString.split('\n');
|
|
41
|
+
if (oldLines.length === 0 || oldLines.length > sourceLines.length) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
let foundAt = -1;
|
|
45
|
+
for (let i = 0; i <= sourceLines.length - oldLines.length; i++) {
|
|
46
|
+
let ok = true;
|
|
47
|
+
for (let j = 0; j < oldLines.length; j++) {
|
|
48
|
+
if (sourceLines[i + j].trimEnd() !== oldLines[j].trimEnd()) {
|
|
49
|
+
ok = false;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!ok)
|
|
54
|
+
continue;
|
|
55
|
+
if (foundAt !== -1)
|
|
56
|
+
return null; // multiple matches
|
|
57
|
+
foundAt = i;
|
|
58
|
+
}
|
|
59
|
+
if (foundAt === -1)
|
|
60
|
+
return null;
|
|
61
|
+
let start = 0;
|
|
62
|
+
for (let i = 0; i < foundAt; i++)
|
|
63
|
+
start += sourceLines[i].length + 1;
|
|
64
|
+
let end = start;
|
|
65
|
+
for (let i = 0; i < oldLines.length; i++) {
|
|
66
|
+
end += sourceLines[foundAt + i].length;
|
|
67
|
+
if (i < oldLines.length - 1)
|
|
68
|
+
end += 1;
|
|
69
|
+
}
|
|
70
|
+
return { strategy: 'line-trimmed', start, end };
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Collapse all runs of whitespace to a single space and match. Catches
|
|
74
|
+
* cases where the LLM normalised tabs to spaces or vice-versa.
|
|
75
|
+
*/
|
|
76
|
+
const whitespaceNormalizedStrategy = (source, oldString) => {
|
|
77
|
+
if (oldString === '')
|
|
78
|
+
return null;
|
|
79
|
+
const norm = (s) => s.replace(/\s+/g, ' ').trim();
|
|
80
|
+
const normalizedNeedle = norm(oldString);
|
|
81
|
+
if (normalizedNeedle === '')
|
|
82
|
+
return null;
|
|
83
|
+
const sourceLines = source.split('\n');
|
|
84
|
+
const needleLines = oldString.split('\n');
|
|
85
|
+
if (needleLines.length > sourceLines.length)
|
|
86
|
+
return null;
|
|
87
|
+
let foundAt = -1;
|
|
88
|
+
for (let i = 0; i <= sourceLines.length - needleLines.length; i++) {
|
|
89
|
+
const candidate = sourceLines
|
|
90
|
+
.slice(i, i + needleLines.length)
|
|
91
|
+
.join('\n');
|
|
92
|
+
if (norm(candidate) !== normalizedNeedle)
|
|
93
|
+
continue;
|
|
94
|
+
if (foundAt !== -1)
|
|
95
|
+
return null;
|
|
96
|
+
foundAt = i;
|
|
97
|
+
}
|
|
98
|
+
if (foundAt === -1)
|
|
99
|
+
return null;
|
|
100
|
+
let start = 0;
|
|
101
|
+
for (let i = 0; i < foundAt; i++)
|
|
102
|
+
start += sourceLines[i].length + 1;
|
|
103
|
+
let end = start;
|
|
104
|
+
for (let i = 0; i < needleLines.length; i++) {
|
|
105
|
+
end += sourceLines[foundAt + i].length;
|
|
106
|
+
if (i < needleLines.length - 1)
|
|
107
|
+
end += 1;
|
|
108
|
+
}
|
|
109
|
+
return { strategy: 'whitespace-normalized', start, end };
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Strip the common leading-indent from each line of the needle and
|
|
113
|
+
* each candidate window of the source. Catches the very common case
|
|
114
|
+
* where the LLM omitted the indentation it should have copied.
|
|
115
|
+
*/
|
|
116
|
+
const indentationFlexibleStrategy = (source, oldString) => {
|
|
117
|
+
if (oldString === '')
|
|
118
|
+
return null;
|
|
119
|
+
const stripCommonIndent = (block) => {
|
|
120
|
+
const lines = block.split('\n');
|
|
121
|
+
let common = Number.POSITIVE_INFINITY;
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
if (line.trim() === '')
|
|
124
|
+
continue;
|
|
125
|
+
const m = /^(\s*)/.exec(line);
|
|
126
|
+
const indent = m ? m[1].length : 0;
|
|
127
|
+
if (indent < common)
|
|
128
|
+
common = indent;
|
|
129
|
+
if (common === 0)
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
if (!Number.isFinite(common) || common === 0)
|
|
133
|
+
return block;
|
|
134
|
+
return lines
|
|
135
|
+
.map((l) => (l.length >= common ? l.slice(common) : l))
|
|
136
|
+
.join('\n');
|
|
137
|
+
};
|
|
138
|
+
const normalizedNeedle = stripCommonIndent(oldString);
|
|
139
|
+
if (normalizedNeedle === '')
|
|
140
|
+
return null;
|
|
141
|
+
const sourceLines = source.split('\n');
|
|
142
|
+
const needleLines = oldString.split('\n');
|
|
143
|
+
if (needleLines.length > sourceLines.length)
|
|
144
|
+
return null;
|
|
145
|
+
let foundAt = -1;
|
|
146
|
+
for (let i = 0; i <= sourceLines.length - needleLines.length; i++) {
|
|
147
|
+
const window = sourceLines.slice(i, i + needleLines.length).join('\n');
|
|
148
|
+
if (stripCommonIndent(window) !== normalizedNeedle)
|
|
149
|
+
continue;
|
|
150
|
+
if (foundAt !== -1)
|
|
151
|
+
return null;
|
|
152
|
+
foundAt = i;
|
|
153
|
+
}
|
|
154
|
+
if (foundAt === -1)
|
|
155
|
+
return null;
|
|
156
|
+
let start = 0;
|
|
157
|
+
for (let i = 0; i < foundAt; i++)
|
|
158
|
+
start += sourceLines[i].length + 1;
|
|
159
|
+
let end = start;
|
|
160
|
+
for (let i = 0; i < needleLines.length; i++) {
|
|
161
|
+
end += sourceLines[foundAt + i].length;
|
|
162
|
+
if (i < needleLines.length - 1)
|
|
163
|
+
end += 1;
|
|
164
|
+
}
|
|
165
|
+
return { strategy: 'indentation-flexible', start, end };
|
|
166
|
+
};
|
|
167
|
+
const STRATEGY_CHAIN = [
|
|
168
|
+
exactStrategy,
|
|
169
|
+
lineTrimmedStrategy,
|
|
170
|
+
whitespaceNormalizedStrategy,
|
|
171
|
+
indentationFlexibleStrategy,
|
|
172
|
+
];
|
|
173
|
+
function locateEdit(source, oldString) {
|
|
174
|
+
for (const strategy of STRATEGY_CHAIN) {
|
|
175
|
+
const match = strategy(source, oldString);
|
|
176
|
+
if (match != null) {
|
|
177
|
+
return match;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
function applyEdit(source, match, newString) {
|
|
183
|
+
return source.slice(0, match.start) + newString + source.slice(match.end);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
exports.applyEdit = applyEdit;
|
|
187
|
+
exports.locateEdit = locateEdit;
|
|
188
|
+
//# sourceMappingURL=editStrategies.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editStrategies.cjs","sources":["../../../../src/tools/local/editStrategies.ts"],"sourcesContent":["/**\n * Single-occurrence string-replacement strategies for `edit_file`.\n *\n * The LLM frequently emits an `oldString` whose whitespace, indentation,\n * or escape sequences are slightly off from the on-disk content. Rather\n * than failing the call (which forces a re-read + retry round-trip),\n * we walk a chain of progressively looser matchers, stopping at the\n * first one that locates exactly one match. The matched on-disk slice\n * is then literally replaced with `newString` — we never modify\n * `newString`, only the search.\n *\n * Strategies are ordered from strict to lenient so we don't accidentally\n * over-match a more specific pattern with a looser one. Inspired by\n * opencode's nine-strategy chain (sst/opencode), trimmed to the four\n * highest-yield strategies for a first cut. Add more (block-anchor +\n * Levenshtein, escape-normalized, etc.) as needed.\n */\n\nexport interface EditMatch {\n /** Strategy name that produced the match, for telemetry/diagnostics. */\n strategy: string;\n /** Starting offset in the source. */\n start: number;\n /** Ending offset (exclusive). */\n end: number;\n}\n\nexport type EditStrategy = (\n source: string,\n oldString: string\n) => EditMatch | null;\n\nconst exactStrategy: EditStrategy = (source, oldString) => {\n if (oldString === '') return null;\n const first = source.indexOf(oldString);\n if (first === -1) return null;\n const second = source.indexOf(oldString, first + oldString.length);\n if (second !== -1) return null;\n return { strategy: 'exact', start: first, end: first + oldString.length };\n};\n\n/**\n * Match per-line, ignoring trailing whitespace differences. Useful for\n * the very common case where the LLM stripped trailing spaces or added\n * an extra blank.\n */\nconst lineTrimmedStrategy: EditStrategy = (source, oldString) => {\n if (oldString === '') return null;\n const sourceLines = source.split('\\n');\n const oldLines = oldString.split('\\n');\n if (oldLines.length === 0 || oldLines.length > sourceLines.length) {\n return null;\n }\n\n let foundAt = -1;\n for (let i = 0; i <= sourceLines.length - oldLines.length; i++) {\n let ok = true;\n for (let j = 0; j < oldLines.length; j++) {\n if (sourceLines[i + j].trimEnd() !== oldLines[j].trimEnd()) {\n ok = false;\n break;\n }\n }\n if (!ok) continue;\n if (foundAt !== -1) return null; // multiple matches\n foundAt = i;\n }\n if (foundAt === -1) return null;\n\n let start = 0;\n for (let i = 0; i < foundAt; i++) start += sourceLines[i].length + 1;\n let end = start;\n for (let i = 0; i < oldLines.length; i++) {\n end += sourceLines[foundAt + i].length;\n if (i < oldLines.length - 1) end += 1;\n }\n return { strategy: 'line-trimmed', start, end };\n};\n\n/**\n * Collapse all runs of whitespace to a single space and match. Catches\n * cases where the LLM normalised tabs to spaces or vice-versa.\n */\nconst whitespaceNormalizedStrategy: EditStrategy = (source, oldString) => {\n if (oldString === '') return null;\n const norm = (s: string): string => s.replace(/\\s+/g, ' ').trim();\n const normalizedNeedle = norm(oldString);\n if (normalizedNeedle === '') return null;\n\n const sourceLines = source.split('\\n');\n const needleLines = oldString.split('\\n');\n if (needleLines.length > sourceLines.length) return null;\n\n let foundAt = -1;\n for (let i = 0; i <= sourceLines.length - needleLines.length; i++) {\n const candidate = sourceLines\n .slice(i, i + needleLines.length)\n .join('\\n');\n if (norm(candidate) !== normalizedNeedle) continue;\n if (foundAt !== -1) return null;\n foundAt = i;\n }\n if (foundAt === -1) return null;\n\n let start = 0;\n for (let i = 0; i < foundAt; i++) start += sourceLines[i].length + 1;\n let end = start;\n for (let i = 0; i < needleLines.length; i++) {\n end += sourceLines[foundAt + i].length;\n if (i < needleLines.length - 1) end += 1;\n }\n return { strategy: 'whitespace-normalized', start, end };\n};\n\n/**\n * Strip the common leading-indent from each line of the needle and\n * each candidate window of the source. Catches the very common case\n * where the LLM omitted the indentation it should have copied.\n */\nconst indentationFlexibleStrategy: EditStrategy = (source, oldString) => {\n if (oldString === '') return null;\n\n const stripCommonIndent = (block: string): string => {\n const lines = block.split('\\n');\n let common = Number.POSITIVE_INFINITY;\n for (const line of lines) {\n if (line.trim() === '') continue;\n const m = /^(\\s*)/.exec(line);\n const indent = m ? m[1].length : 0;\n if (indent < common) common = indent;\n if (common === 0) break;\n }\n if (!Number.isFinite(common) || common === 0) return block;\n return lines\n .map((l) => (l.length >= common ? l.slice(common) : l))\n .join('\\n');\n };\n\n const normalizedNeedle = stripCommonIndent(oldString);\n if (normalizedNeedle === '') return null;\n\n const sourceLines = source.split('\\n');\n const needleLines = oldString.split('\\n');\n if (needleLines.length > sourceLines.length) return null;\n\n let foundAt = -1;\n for (let i = 0; i <= sourceLines.length - needleLines.length; i++) {\n const window = sourceLines.slice(i, i + needleLines.length).join('\\n');\n if (stripCommonIndent(window) !== normalizedNeedle) continue;\n if (foundAt !== -1) return null;\n foundAt = i;\n }\n if (foundAt === -1) return null;\n\n let start = 0;\n for (let i = 0; i < foundAt; i++) start += sourceLines[i].length + 1;\n let end = start;\n for (let i = 0; i < needleLines.length; i++) {\n end += sourceLines[foundAt + i].length;\n if (i < needleLines.length - 1) end += 1;\n }\n return { strategy: 'indentation-flexible', start, end };\n};\n\nconst STRATEGY_CHAIN: EditStrategy[] = [\n exactStrategy,\n lineTrimmedStrategy,\n whitespaceNormalizedStrategy,\n indentationFlexibleStrategy,\n];\n\nexport function locateEdit(source: string, oldString: string): EditMatch | null {\n for (const strategy of STRATEGY_CHAIN) {\n const match = strategy(source, oldString);\n if (match != null) {\n return match;\n }\n }\n return null;\n}\n\nexport function applyEdit(\n source: string,\n match: EditMatch,\n newString: string\n): string {\n return source.slice(0, match.start) + newString + source.slice(match.end);\n}\n"],"names":[],"mappings":"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"}
|