openfox 2.0.134 → 2.0.136
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/CHANGELOG.md +45 -0
- package/dist/CHANGELOG.md +45 -0
- package/dist/agent-defaults/builder.agent.md +1 -0
- package/dist/agent-defaults/planner.agent.md +1 -0
- package/dist/{ask-UBIRH3SO.js → ask-46J5WYRL.js} +2 -2
- package/dist/{auto-config-5VSNLVNI.js → auto-config-2A7Y2WOK.js} +25 -6
- package/dist/{backend-NDBY3DAQ.js → backend-24FTDSHF.js} +4 -2
- package/dist/{chat-handler-YCZE2XEJ.js → chat-handler-SYU4HJMI.js} +30 -26
- package/dist/{chunk-CSYOWFPU.js → chunk-342BKOTE.js} +29 -5
- package/dist/{chunk-7QMWYNVF.js → chunk-4LLGI372.js} +2 -2
- package/dist/chunk-4SBVW4UH.js +24 -0
- package/dist/{chunk-FAQDLL2X.js → chunk-5YPHN5NC.js} +8 -8
- package/dist/{chunk-ZMK4VX7V.js → chunk-64YDOCQR.js} +53 -21
- package/dist/{chunk-UV6IH6HK.js → chunk-AKWJMMEJ.js} +15 -1
- package/dist/{chunk-ORCQ3EXQ.js → chunk-BHVR2YS5.js} +2 -2
- package/dist/{chunk-7HGBJFIJ.js → chunk-CPVEONNS.js} +12 -10
- package/dist/{chunk-QDARWVQ2.js → chunk-DG2YSWMC.js} +40 -6
- package/dist/{chunk-J5HOUEPE.js → chunk-EXLCFEML.js} +86 -1010
- package/dist/chunk-FSSUPWYO.js +336 -0
- package/dist/{chunk-5UQ4CKOA.js → chunk-G5GUQKLS.js} +27 -6
- package/dist/{chunk-M4J2L4VH.js → chunk-GP5BYQMF.js} +23 -23
- package/dist/{chunk-7ET46SNE.js → chunk-HEYU7FAY.js} +3 -3
- package/dist/{chunk-MRE4OYKI.js → chunk-MILEZY2J.js} +4 -4
- package/dist/{chunk-O3RCSKUH.js → chunk-MOYWDP73.js} +2 -2
- package/dist/{chunk-J3NMNYGM.js → chunk-N5JKVNLH.js} +2 -2
- package/dist/{chunk-YLVOU7T7.js → chunk-PQKLOLGC.js} +162 -6
- package/dist/{chunk-DWA4IE65.js → chunk-Q33DL67W.js} +2 -2
- package/dist/{chunk-OZWHRBDY.js → chunk-QBWJCO5K.js} +12 -29
- package/dist/{chunk-EUOWCVU6.js → chunk-QMU3ZY2C.js} +37 -341
- package/dist/{chunk-5IXCCMFU.js → chunk-QQQBNL7A.js} +350 -157
- package/dist/{chunk-CTWZPN7G.js → chunk-QUAEH3MP.js} +29 -1
- package/dist/{chunk-5UMXFLET.js → chunk-SAGDTQXD.js} +3 -3
- package/dist/chunk-U6FHHT2B.js +1155 -0
- package/dist/{chunk-YOPXAI2W.js → chunk-UL7776XB.js} +21 -21
- package/dist/{chunk-JURM3RPZ.js → chunk-V5AC2TV7.js} +8 -13
- package/dist/{chunk-BZND27IB.js → chunk-VA2TYWQR.js} +4 -4
- package/dist/{chunk-JE7LW7Y6.js → chunk-VIJVLLPT.js} +12 -3
- package/dist/{chunk-2AI7VYPN.js → chunk-VKMPR25F.js} +9 -3
- package/dist/chunk-VWBPQZLO.js +7 -0
- package/dist/{chunk-ZTBKMVV6.js → chunk-X4PVR6AW.js} +15 -2
- package/dist/chunk-XG3AUMPI.js +72 -0
- package/dist/{chunk-MEEUXRFL.js → chunk-XGG4XCKP.js} +2 -2
- package/dist/{chunk-QICVOQNX.js → chunk-YDPC4FLR.js} +17 -11
- package/dist/cli/dev.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/client-MVI3J7SN.js +18 -0
- package/dist/{client-pure-FLKP6PTI.js → client-pure-YZ4IGCU4.js} +4 -2
- package/dist/{compactor-PD6DQ4NY.js → compactor-LBK53WAK.js} +14 -12
- package/dist/describe-image-4XDLSKAB.js +30 -0
- package/dist/{dynamic-context-WKEXQCTK.js → dynamic-context-RDBNQPPA.js} +12 -10
- package/dist/{events-3WSNI7XC.js → events-YTYYT3PR.js} +6 -6
- package/dist/export-import-GHRCOQHF.js +137 -0
- package/dist/{folding-H7JTX6KI.js → folding-3MMOPS4W.js} +4 -4
- package/dist/{http-client-A445WYZU.js → http-client-C6UYWVWZ.js} +3 -3
- package/dist/launch-YZ46OEFP.js +58 -0
- package/dist/{manager-ZACEUL5P.js → manager-FSPRJCYT.js} +4 -4
- package/dist/{model-catalog-2PCKTYGW.js → model-catalog-Y5MA5OZP.js} +2 -2
- package/dist/{orchestrator-OT66EDXO.js → orchestrator-6QGQ55NT.js} +29 -25
- package/dist/package.json +1 -1
- package/dist/{path-security-5CFMMULR.js → path-security-EC4Z2ZZG.js} +8 -8
- package/dist/{platform-XPMZKAUB.js → platform-KPKYBU54.js} +3 -3
- package/dist/{processor-CHMHMMIE.js → processor-B2RV7MVC.js} +29 -25
- package/dist/{provider-JWUNG2TD.js → provider-SOC6Z25R.js} +10 -8
- package/dist/{provider-manager-2KKZ37XJ.js → provider-manager-Y53SVVQ3.js} +10 -8
- package/dist/{registry-BEIR7Y5G.js → registry-FMDMGIUB.js} +3 -3
- package/dist/{serve-HSDVY37K.js → serve-EWA5HXC7.js} +41 -32
- package/dist/server/index.d.ts +16 -0
- package/dist/server/index.js +34 -30
- package/dist/server-ZAPAB3FS.js +61 -0
- package/dist/{service-XIUNS5XV.js → service-NKLQH647.js} +12 -9
- package/dist/{session-overrides-Y5B4XQPU.js → session-overrides-ANQBKV74.js} +11 -7
- package/dist/{sessions-IGINPPBP.js → sessions-GLYACE5R.js} +6 -4
- package/dist/{settings-ZJPM6PQV.js → settings-WH6BVQCP.js} +4 -2
- package/dist/{tasks-3JJWJJ3X.js → tasks-GPM3JAE6.js} +14 -10
- package/dist/tool-adapter-ZY4FC4C4.js +9 -0
- package/dist/{tools-WWIWD3O7.js → tools-EJOABHVO.js} +27 -23
- package/dist/{update-Q6SH7UP2.js → update-IPHJMRQZ.js} +2 -2
- package/dist/web/assets/index-CfFwerN2.js +313 -0
- package/dist/web/assets/{index-Y300llHY.css → index-ChJjYovg.css} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-QTLYD2FF.js +0 -7
- package/dist/client-VVYOWXRR.js +0 -16
- package/dist/launch-FJ65HRV4.js +0 -54
- package/dist/server-3UVLECD5.js +0 -57
- package/dist/tool-adapter-FJAFLDZA.js +0 -8
- package/dist/web/assets/index-C48qmBAI.js +0 -313
|
@@ -0,0 +1,1155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
serverT
|
|
3
|
+
} from "./chunk-4SBVW4UH.js";
|
|
4
|
+
import {
|
|
5
|
+
AskUserInterrupt
|
|
6
|
+
} from "./chunk-V5AC2TV7.js";
|
|
7
|
+
import {
|
|
8
|
+
PathAccessDeniedError,
|
|
9
|
+
registerPathConfirmation,
|
|
10
|
+
requestPathAccess
|
|
11
|
+
} from "./chunk-PQKLOLGC.js";
|
|
12
|
+
import {
|
|
13
|
+
getEventStore
|
|
14
|
+
} from "./chunk-342BKOTE.js";
|
|
15
|
+
import {
|
|
16
|
+
getRuntimeConfig
|
|
17
|
+
} from "./chunk-BHVR2YS5.js";
|
|
18
|
+
import {
|
|
19
|
+
createChatPathConfirmationMessage
|
|
20
|
+
} from "./chunk-AATMPL7S.js";
|
|
21
|
+
import {
|
|
22
|
+
modelSupportsVision
|
|
23
|
+
} from "./chunk-TS5WKTFB.js";
|
|
24
|
+
import {
|
|
25
|
+
OUTPUT_LIMITS,
|
|
26
|
+
cacheSet,
|
|
27
|
+
contentHash,
|
|
28
|
+
decodeDataUrl,
|
|
29
|
+
extractPdfContent,
|
|
30
|
+
extractPdfText,
|
|
31
|
+
formatPdfErrorMessage,
|
|
32
|
+
isPdfBuffer,
|
|
33
|
+
processPdfContent
|
|
34
|
+
} from "./chunk-FSSUPWYO.js";
|
|
35
|
+
import {
|
|
36
|
+
logger
|
|
37
|
+
} from "./chunk-K44MW7JJ.js";
|
|
38
|
+
|
|
39
|
+
// src/server/tools/describe-image.ts
|
|
40
|
+
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
41
|
+
import { relative as relative3 } from "path";
|
|
42
|
+
|
|
43
|
+
// src/server/tools/tool-helpers.ts
|
|
44
|
+
import { randomUUID } from "crypto";
|
|
45
|
+
import { resolve } from "path";
|
|
46
|
+
var confirmationSeqCounter = 0;
|
|
47
|
+
async function requestUserConfirmation(context, toolLabel, desc) {
|
|
48
|
+
if (context.dangerLevel === "dangerous") return Promise.resolve(true);
|
|
49
|
+
if (toolLabel === "workspace" || toolLabel === "command") {
|
|
50
|
+
try {
|
|
51
|
+
const { getSetting, SETTINGS_KEYS } = await import("./settings-WH6BVQCP.js");
|
|
52
|
+
if (getSetting(SETTINGS_KEYS.CONFIRM_ON_WORKSPACE_ACTIONS) !== "true") {
|
|
53
|
+
return Promise.resolve(true);
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (context.isSubAgent) return Promise.resolve(false);
|
|
59
|
+
if (typeof context.onEvent !== "function") return Promise.resolve(false);
|
|
60
|
+
const seq = ++confirmationSeqCounter;
|
|
61
|
+
const callId = context.toolCallId ? `${context.toolCallId}-${seq}` : randomUUID();
|
|
62
|
+
try {
|
|
63
|
+
const eventStore = getEventStore();
|
|
64
|
+
eventStore.append(context.sessionId, {
|
|
65
|
+
type: "path.confirmation_pending",
|
|
66
|
+
data: { callId, tool: toolLabel, paths: [desc], workdir: context.workdir, reason: "dangerous_command" }
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
const promise = registerPathConfirmation(
|
|
71
|
+
callId,
|
|
72
|
+
[desc],
|
|
73
|
+
context.sessionId,
|
|
74
|
+
toolLabel,
|
|
75
|
+
context.workdir,
|
|
76
|
+
"dangerous_command"
|
|
77
|
+
);
|
|
78
|
+
context.onEvent(
|
|
79
|
+
createChatPathConfirmationMessage(callId, toolLabel, [desc], context.workdir, "dangerous_command")
|
|
80
|
+
);
|
|
81
|
+
return promise;
|
|
82
|
+
}
|
|
83
|
+
function buildSignal(timeoutMs, contextSignal) {
|
|
84
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
85
|
+
if (contextSignal) {
|
|
86
|
+
return AbortSignal.any([timeoutSignal, contextSignal]);
|
|
87
|
+
}
|
|
88
|
+
return timeoutSignal;
|
|
89
|
+
}
|
|
90
|
+
function validateAction(action, allowed, startTime) {
|
|
91
|
+
if (!action || !allowed.includes(action)) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
error: serverT(
|
|
95
|
+
{
|
|
96
|
+
en: "Invalid action: {{action}}. Must be one of: {{allowed}}",
|
|
97
|
+
fr: "Action invalide : {{action}}. Doit \xEAtre l\u2019une des actions suivantes : {{allowed}}"
|
|
98
|
+
},
|
|
99
|
+
{ action: action ?? "", allowed: allowed.join(", ") }
|
|
100
|
+
),
|
|
101
|
+
durationMs: Date.now() - startTime,
|
|
102
|
+
truncated: false
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
function checkActionPermission(action, permittedActions, startTime) {
|
|
108
|
+
if (action && permittedActions && permittedActions.length > 0 && !permittedActions.includes(action)) {
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
error: serverT(
|
|
112
|
+
{
|
|
113
|
+
en: "Action '{{action}}' not allowed. Available: {{available}}",
|
|
114
|
+
fr: "Action \xAB {{action}} \xBB non autoris\xE9e. Disponibles : {{available}}"
|
|
115
|
+
},
|
|
116
|
+
{ action: action ?? "", available: permittedActions.join(", ") }
|
|
117
|
+
),
|
|
118
|
+
durationMs: Date.now() - startTime,
|
|
119
|
+
truncated: false
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
function requireSession(sessionManager, sessionId) {
|
|
125
|
+
return sessionManager.requireSession(sessionId);
|
|
126
|
+
}
|
|
127
|
+
function validateActionWithPermission(action, allowedActions, toolName, permittedActions, startTime) {
|
|
128
|
+
const actionError = validateAction(action, allowedActions, startTime ?? Date.now());
|
|
129
|
+
if (actionError) return actionError;
|
|
130
|
+
const permittedToolActions = permittedActions?.[toolName];
|
|
131
|
+
const permissionError = checkActionPermission(action, permittedToolActions, startTime ?? Date.now());
|
|
132
|
+
if (permissionError) return permissionError;
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
function createTool(name, definition, handler) {
|
|
136
|
+
return {
|
|
137
|
+
name,
|
|
138
|
+
definition,
|
|
139
|
+
async execute(args, context) {
|
|
140
|
+
const startTime = Date.now();
|
|
141
|
+
const helpers = {
|
|
142
|
+
resolvePath: (path) => resolve(context.workdir, path),
|
|
143
|
+
checkPathAccess: async (paths, command) => {
|
|
144
|
+
if (context.onEvent) {
|
|
145
|
+
await requestPathAccess(
|
|
146
|
+
paths,
|
|
147
|
+
context.workdir,
|
|
148
|
+
context.sessionId,
|
|
149
|
+
context.toolCallId ?? crypto.randomUUID(),
|
|
150
|
+
name,
|
|
151
|
+
context.onEvent,
|
|
152
|
+
context.dangerLevel,
|
|
153
|
+
command,
|
|
154
|
+
context.isSubAgent
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
error: (message, truncated = false) => ({
|
|
159
|
+
success: false,
|
|
160
|
+
error: message,
|
|
161
|
+
durationMs: Date.now() - startTime,
|
|
162
|
+
truncated
|
|
163
|
+
}),
|
|
164
|
+
success: (output, truncated = false, extra) => ({
|
|
165
|
+
success: true,
|
|
166
|
+
output,
|
|
167
|
+
durationMs: Date.now() - startTime,
|
|
168
|
+
truncated,
|
|
169
|
+
...extra
|
|
170
|
+
})
|
|
171
|
+
};
|
|
172
|
+
try {
|
|
173
|
+
return await handler(args, context, helpers);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error instanceof PathAccessDeniedError || error instanceof AskUserInterrupt) {
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
success: false,
|
|
180
|
+
error: error instanceof Error ? error.message : serverT({
|
|
181
|
+
en: "Unknown error in tool execution",
|
|
182
|
+
fr: "Erreur inconnue lors de l\u2019ex\xE9cution de l\u2019outil"
|
|
183
|
+
}),
|
|
184
|
+
durationMs: Date.now() - startTime,
|
|
185
|
+
truncated: false
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/server/tools/read.ts
|
|
193
|
+
import { readFile as readFile2, stat, readdir } from "fs/promises";
|
|
194
|
+
import { extname, join, relative as relative2 } from "path";
|
|
195
|
+
|
|
196
|
+
// src/server/tools/file-tracker.ts
|
|
197
|
+
import { createHash } from "crypto";
|
|
198
|
+
import { readFile } from "fs/promises";
|
|
199
|
+
import { relative, resolve as resolve2 } from "path";
|
|
200
|
+
var FileNotReadError = class extends Error {
|
|
201
|
+
constructor(path) {
|
|
202
|
+
super(`File "${path}" must be read before writing. Use read_file first.`);
|
|
203
|
+
this.name = "FileNotReadError";
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
var FileChangedExternallyError = class extends Error {
|
|
207
|
+
constructor(path) {
|
|
208
|
+
super(
|
|
209
|
+
`File "${path}" was modified externally and must be read before writing. Use read_file to see current contents.`
|
|
210
|
+
);
|
|
211
|
+
this.name = "FileChangedExternallyError";
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
async function computeFileHash(filePath) {
|
|
215
|
+
try {
|
|
216
|
+
const content = await readFile(filePath);
|
|
217
|
+
const hash = createHash("sha256");
|
|
218
|
+
hash.update(content);
|
|
219
|
+
return hash.digest("hex");
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function validateFileForWrite(filePath, readFiles, baseDir) {
|
|
228
|
+
const normalizedPath = resolve2(filePath);
|
|
229
|
+
const currentHash = await computeFileHash(normalizedPath);
|
|
230
|
+
if (currentHash === null) {
|
|
231
|
+
return { valid: true };
|
|
232
|
+
}
|
|
233
|
+
const readEntry = readFiles[normalizedPath];
|
|
234
|
+
if (readEntry) {
|
|
235
|
+
if (readEntry.hash !== currentHash) {
|
|
236
|
+
return {
|
|
237
|
+
valid: false,
|
|
238
|
+
error: new FileChangedExternallyError(filePath)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return { valid: true };
|
|
242
|
+
}
|
|
243
|
+
if (baseDir) {
|
|
244
|
+
const relPath = relative(baseDir, normalizedPath);
|
|
245
|
+
if (!relPath.startsWith("..")) {
|
|
246
|
+
const relMatches = Object.values(readFiles).filter((entry) => entry.relPath === relPath);
|
|
247
|
+
if (relMatches.some((entry) => entry.hash === currentHash)) {
|
|
248
|
+
return { valid: true };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
valid: false,
|
|
254
|
+
error: new FileNotReadError(filePath)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function isNodeError(error) {
|
|
258
|
+
return error instanceof Error && "code" in error;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/server/utils/encoding.ts
|
|
262
|
+
import { detect } from "jschardet";
|
|
263
|
+
import * as iconv from "iconv-lite";
|
|
264
|
+
function detectEncoding(buffer) {
|
|
265
|
+
if (buffer.length === 0) {
|
|
266
|
+
return { encoding: "utf-8", confidence: 1, bomSize: 0 };
|
|
267
|
+
}
|
|
268
|
+
const bomSize = detectBOM(buffer);
|
|
269
|
+
const bomStripped = bomSize > 0 ? buffer.subarray(bomSize) : buffer;
|
|
270
|
+
if (bomSize > 0) {
|
|
271
|
+
const bomEncoding = bomEncodingForSize(buffer);
|
|
272
|
+
return { encoding: bomEncoding, confidence: 1, bomSize };
|
|
273
|
+
}
|
|
274
|
+
if (isValidUtf8(bomStripped)) {
|
|
275
|
+
return { encoding: "utf-8", confidence: 0.95, bomSize: 0 };
|
|
276
|
+
}
|
|
277
|
+
const detected = detect(bomStripped);
|
|
278
|
+
if (detected && detected.encoding && detected.confidence > 0.2) {
|
|
279
|
+
return { encoding: normalizeEncoding(detected.encoding), confidence: detected.confidence, bomSize: 0 };
|
|
280
|
+
}
|
|
281
|
+
return { encoding: "ISO-8859-1", confidence: 0.1, bomSize: 0 };
|
|
282
|
+
}
|
|
283
|
+
function detectBOM(buffer) {
|
|
284
|
+
if (buffer.length >= 4 && buffer[0] === 0 && buffer[1] === 0 && buffer[2] === 254 && buffer[3] === 255)
|
|
285
|
+
return 4;
|
|
286
|
+
if (buffer.length >= 4 && buffer[0] === 255 && buffer[1] === 254 && buffer[2] === 0 && buffer[3] === 0)
|
|
287
|
+
return 4;
|
|
288
|
+
if (buffer.length >= 2 && buffer[0] === 254 && buffer[1] === 255) return 2;
|
|
289
|
+
if (buffer.length >= 2 && buffer[0] === 255 && buffer[1] === 254) return 2;
|
|
290
|
+
if (buffer.length >= 3 && buffer[0] === 239 && buffer[1] === 187 && buffer[2] === 191) return 3;
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
function bomEncodingForSize(buffer) {
|
|
294
|
+
if (buffer.length >= 2) {
|
|
295
|
+
if (buffer[0] === 254 && buffer[1] === 255) return "utf-16be";
|
|
296
|
+
if (buffer[0] === 255 && buffer[1] === 254) {
|
|
297
|
+
if (buffer.length >= 4 && buffer[2] === 0 && buffer[3] === 0) return "utf-32";
|
|
298
|
+
return "utf-16le";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (buffer.length >= 3 && buffer[0] === 239 && buffer[1] === 187 && buffer[2] === 191) return "utf-8";
|
|
302
|
+
if (buffer.length >= 4 && buffer[0] === 0 && buffer[1] === 0 && buffer[2] === 254 && buffer[3] === 255)
|
|
303
|
+
return "utf-32be";
|
|
304
|
+
return "utf-8";
|
|
305
|
+
}
|
|
306
|
+
function isValidUtf8(buffer) {
|
|
307
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
308
|
+
try {
|
|
309
|
+
decoder.decode(buffer);
|
|
310
|
+
return true;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function normalizeEncoding(enc) {
|
|
316
|
+
const lower = enc.toLowerCase().replace(/_/g, "-");
|
|
317
|
+
const map = {
|
|
318
|
+
"utf-8": "utf-8",
|
|
319
|
+
utf8: "utf-8",
|
|
320
|
+
ascii: "utf-8",
|
|
321
|
+
"iso-8859-1": "ISO-8859-1",
|
|
322
|
+
"iso8859-1": "ISO-8859-1",
|
|
323
|
+
latin1: "ISO-8859-1",
|
|
324
|
+
"windows-1252": "windows-1252",
|
|
325
|
+
"shift-jis": "Shift_JIS",
|
|
326
|
+
shift_jis: "Shift_JIS",
|
|
327
|
+
"euc-jp": "EUC-JP",
|
|
328
|
+
"euc-kr": "EUC-KR",
|
|
329
|
+
gb2312: "GB2312",
|
|
330
|
+
gb18030: "GB18030",
|
|
331
|
+
big5: "Big5",
|
|
332
|
+
"utf-16": "utf-16le",
|
|
333
|
+
"utf-16le": "utf-16le",
|
|
334
|
+
"utf-16be": "utf-16be",
|
|
335
|
+
"utf-32": "utf-32le",
|
|
336
|
+
"utf-32le": "utf-32le",
|
|
337
|
+
"utf-32be": "utf-32be",
|
|
338
|
+
"koi8-r": "KOI8-R",
|
|
339
|
+
"koi8-u": "KOI8-U"
|
|
340
|
+
};
|
|
341
|
+
return map[lower] ?? enc;
|
|
342
|
+
}
|
|
343
|
+
function decodeContent(buffer, encoding) {
|
|
344
|
+
const bomSize = detectBOM(buffer);
|
|
345
|
+
const contentBuf = bomSize > 0 ? buffer.subarray(bomSize) : buffer;
|
|
346
|
+
const normalized = normalizeEncoding(encoding);
|
|
347
|
+
if (normalized === "utf-8") {
|
|
348
|
+
return contentBuf.toString("utf-8");
|
|
349
|
+
}
|
|
350
|
+
if (normalized === "utf-16le") {
|
|
351
|
+
return contentBuf.toString("utf-16le");
|
|
352
|
+
}
|
|
353
|
+
if (normalized === "utf-16be") {
|
|
354
|
+
return swapUtf16Endianness(contentBuf).toString("utf-16le");
|
|
355
|
+
}
|
|
356
|
+
if (normalized === "utf-32le") {
|
|
357
|
+
return utf32ToString(contentBuf, true);
|
|
358
|
+
}
|
|
359
|
+
if (normalized === "utf-32be") {
|
|
360
|
+
return utf32ToString(contentBuf, false);
|
|
361
|
+
}
|
|
362
|
+
return iconv.decode(contentBuf, normalized);
|
|
363
|
+
}
|
|
364
|
+
function encodeContent(content, encoding, addBom = false) {
|
|
365
|
+
const normalized = normalizeEncoding(encoding);
|
|
366
|
+
let encoded;
|
|
367
|
+
if (normalized === "utf-8") {
|
|
368
|
+
encoded = Buffer.from(content, "utf-8");
|
|
369
|
+
} else if (normalized === "utf-16le") {
|
|
370
|
+
encoded = Buffer.from(content, "utf-16le");
|
|
371
|
+
} else if (normalized === "utf-16be") {
|
|
372
|
+
encoded = swapUtf16Endianness(Buffer.from(content, "utf-16le"));
|
|
373
|
+
} else if (normalized === "utf-32le") {
|
|
374
|
+
encoded = stringToUtf32(content, true);
|
|
375
|
+
} else if (normalized === "utf-32be") {
|
|
376
|
+
encoded = stringToUtf32(content, false);
|
|
377
|
+
} else {
|
|
378
|
+
encoded = iconv.encode(content, normalized);
|
|
379
|
+
}
|
|
380
|
+
if (addBom) {
|
|
381
|
+
const bom = bomForEncoding(normalized);
|
|
382
|
+
if (bom) {
|
|
383
|
+
encoded = Buffer.concat([bom, encoded]);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return encoded;
|
|
387
|
+
}
|
|
388
|
+
function bomForEncoding(encoding) {
|
|
389
|
+
switch (encoding) {
|
|
390
|
+
case "utf-8":
|
|
391
|
+
return Buffer.from([239, 187, 191]);
|
|
392
|
+
case "utf-16le":
|
|
393
|
+
return Buffer.from([255, 254]);
|
|
394
|
+
case "utf-16be":
|
|
395
|
+
return Buffer.from([254, 255]);
|
|
396
|
+
case "utf-32le":
|
|
397
|
+
return Buffer.from([255, 254, 0, 0]);
|
|
398
|
+
case "utf-32be":
|
|
399
|
+
return Buffer.from([0, 0, 254, 255]);
|
|
400
|
+
default:
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function swapUtf16Endianness(buf) {
|
|
405
|
+
const out = Buffer.alloc(buf.length);
|
|
406
|
+
for (let i = 0; i + 1 < buf.length; i += 2) {
|
|
407
|
+
out[i] = buf[i + 1];
|
|
408
|
+
out[i + 1] = buf[i];
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
function utf32ToString(buffer, littleEndian) {
|
|
413
|
+
const chars = [];
|
|
414
|
+
for (let i = 0; i + 3 < buffer.length; i += 4) {
|
|
415
|
+
const code = littleEndian ? buffer.readUInt32LE(i) : buffer.readUInt32BE(i);
|
|
416
|
+
if (code > 1114111) continue;
|
|
417
|
+
chars.push(String.fromCodePoint(code));
|
|
418
|
+
}
|
|
419
|
+
return chars.join("");
|
|
420
|
+
}
|
|
421
|
+
function stringToUtf32(str, littleEndian) {
|
|
422
|
+
const codes = [];
|
|
423
|
+
for (const ch of str) {
|
|
424
|
+
codes.push(ch.codePointAt(0));
|
|
425
|
+
}
|
|
426
|
+
const buf = Buffer.alloc(codes.length * 4);
|
|
427
|
+
for (let i = 0; i < codes.length; i++) {
|
|
428
|
+
if (littleEndian) {
|
|
429
|
+
buf.writeUInt32LE(codes[i], i * 4);
|
|
430
|
+
} else {
|
|
431
|
+
buf.writeUInt32BE(codes[i], i * 4);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return buf;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/server/tools/read.ts
|
|
438
|
+
import { fileTypeFromBuffer } from "file-type";
|
|
439
|
+
async function detectImageType(buffer, filePath) {
|
|
440
|
+
const fileType = await fileTypeFromBuffer(buffer);
|
|
441
|
+
const imageMimeTypes = ["image/png", "image/jpeg", "image/gif", "image/webp", "image/bmp", "image/svg+xml"];
|
|
442
|
+
if (fileType?.mime && imageMimeTypes.includes(fileType.mime)) {
|
|
443
|
+
return fileType.mime;
|
|
444
|
+
}
|
|
445
|
+
const ext = extname(filePath).toLowerCase();
|
|
446
|
+
if (ext === ".svg" && buffer.length > 0) {
|
|
447
|
+
const content = buffer.toString("utf-8");
|
|
448
|
+
const trimmedStart = content.trimStart();
|
|
449
|
+
if (trimmedStart.startsWith("<?xml") || trimmedStart.startsWith("<svg")) {
|
|
450
|
+
return "image/svg+xml";
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
function formatSize(bytes) {
|
|
456
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
457
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
458
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
459
|
+
}
|
|
460
|
+
async function listDirectory(dirPath, relativePath) {
|
|
461
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
462
|
+
const sorted = [...entries].sort((a, b) => {
|
|
463
|
+
if (a.isDirectory() !== b.isDirectory()) {
|
|
464
|
+
return a.isDirectory() ? -1 : 1;
|
|
465
|
+
}
|
|
466
|
+
return a.name.localeCompare(b.name);
|
|
467
|
+
});
|
|
468
|
+
const lines = [`${relativePath}/`];
|
|
469
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
470
|
+
const entry = sorted[i];
|
|
471
|
+
const isLast = i === sorted.length - 1;
|
|
472
|
+
const prefix = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
473
|
+
const suffix = entry.isDirectory() ? "/" : "";
|
|
474
|
+
let sizeStr = "";
|
|
475
|
+
if (!entry.isDirectory()) {
|
|
476
|
+
try {
|
|
477
|
+
const entryStat = await stat(join(dirPath, entry.name));
|
|
478
|
+
sizeStr = ` ${formatSize(entryStat.size).padStart(7)}`;
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
lines.push(`${prefix}${entry.name}${suffix}${sizeStr}`);
|
|
483
|
+
}
|
|
484
|
+
return lines.join("\n");
|
|
485
|
+
}
|
|
486
|
+
var readFileTool = createTool(
|
|
487
|
+
"read_file",
|
|
488
|
+
{
|
|
489
|
+
type: "function",
|
|
490
|
+
function: {
|
|
491
|
+
name: "read_file",
|
|
492
|
+
description: "Read the contents of a file or list directory contents. For text files: returns file content. For images (PNG, JPEG, GIF, WebP, BMP, SVG): returns base64-encoded data with MIME type metadata. For PDF files: extracts text content page by page. For directories: returns a tree-formatted listing of entries with file sizes.",
|
|
493
|
+
parameters: {
|
|
494
|
+
type: "object",
|
|
495
|
+
properties: {
|
|
496
|
+
path: {
|
|
497
|
+
type: "string",
|
|
498
|
+
description: "Path to the file (relative to workdir or absolute)"
|
|
499
|
+
},
|
|
500
|
+
offset: {
|
|
501
|
+
type: "number",
|
|
502
|
+
description: "Line number to start from (1-indexed). Default: 1. Only applies to text files."
|
|
503
|
+
},
|
|
504
|
+
limit: {
|
|
505
|
+
type: "number",
|
|
506
|
+
description: `Maximum number of lines to read. Default: ${OUTPUT_LIMITS.read_file.maxLines}. Only applies to text files.`
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
required: ["path"]
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
async (args, context, helpers) => {
|
|
514
|
+
const offset = args.offset ?? 1;
|
|
515
|
+
const limit = Math.min(args.limit ?? OUTPUT_LIMITS.read_file.maxLines, OUTPUT_LIMITS.read_file.maxLines);
|
|
516
|
+
const fullPath = helpers.resolvePath(args.path);
|
|
517
|
+
await helpers.checkPathAccess([fullPath]);
|
|
518
|
+
try {
|
|
519
|
+
const stats = await stat(fullPath);
|
|
520
|
+
if (stats.isDirectory()) {
|
|
521
|
+
const listing = await listDirectory(fullPath, args.path);
|
|
522
|
+
return helpers.success(listing);
|
|
523
|
+
}
|
|
524
|
+
if (stats.size > OUTPUT_LIMITS.read_file.maxFileBytes) {
|
|
525
|
+
return helpers.error(
|
|
526
|
+
serverT(
|
|
527
|
+
{
|
|
528
|
+
en: "File size ({{size}} bytes) exceeds maximum file size (20MB). Use shell command to process large files.",
|
|
529
|
+
fr: "La taille du fichier ({{size}} octets) d\xE9passe la taille maximale (20 Mo). Utilisez une commande shell pour traiter les fichiers volumineux."
|
|
530
|
+
},
|
|
531
|
+
{ size: stats.size }
|
|
532
|
+
)
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (stats.size > OUTPUT_LIMITS.read_file.maxImageBytes) {
|
|
536
|
+
return helpers.error(
|
|
537
|
+
serverT(
|
|
538
|
+
{
|
|
539
|
+
en: "File size ({{size}} bytes) exceeds image size limit (2MB). Use shell command to process large files.",
|
|
540
|
+
fr: "La taille du fichier ({{size}} octets) d\xE9passe la limite pour les images (2 Mo). Utilisez une commande shell pour traiter les fichiers volumineux."
|
|
541
|
+
},
|
|
542
|
+
{ size: stats.size }
|
|
543
|
+
)
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
} catch {
|
|
547
|
+
return helpers.error(
|
|
548
|
+
serverT({ en: "File not found: {{path}}", fr: "Fichier introuvable : {{path}}" }, { path: args.path })
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
const rawBuffer = await readFile2(fullPath);
|
|
552
|
+
const mimeType = await detectImageType(rawBuffer, args.path);
|
|
553
|
+
if (mimeType) {
|
|
554
|
+
const base64Data = rawBuffer.toString("base64");
|
|
555
|
+
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
|
556
|
+
const contentHash3 = await computeFileHash(fullPath);
|
|
557
|
+
if (contentHash3) {
|
|
558
|
+
context.sessionManager.recordFileRead(
|
|
559
|
+
context.sessionId,
|
|
560
|
+
fullPath,
|
|
561
|
+
contentHash3,
|
|
562
|
+
relative2(context.workdir, fullPath)
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
return helpers.success(
|
|
566
|
+
serverT(
|
|
567
|
+
{ en: "[Image: {{path}} ({{mime}}, {{size}} bytes)]", fr: "[Image : {{path}} ({{mime}}, {{size}} octets)]" },
|
|
568
|
+
{ path: args.path, mime: mimeType, size: rawBuffer.length }
|
|
569
|
+
),
|
|
570
|
+
false,
|
|
571
|
+
{
|
|
572
|
+
metadata: {
|
|
573
|
+
mimeType,
|
|
574
|
+
size: rawBuffer.length,
|
|
575
|
+
base64Data,
|
|
576
|
+
dataUrl,
|
|
577
|
+
path: fullPath
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
if (isPdfBuffer(rawBuffer)) {
|
|
583
|
+
try {
|
|
584
|
+
const { text, pageCount, title, author } = await extractPdfText(rawBuffer);
|
|
585
|
+
const { output: output2, truncated: truncated2, isScanned } = processPdfContent(text, OUTPUT_LIMITS.read_file.maxBytes);
|
|
586
|
+
const contentHash3 = await computeFileHash(fullPath);
|
|
587
|
+
if (contentHash3) {
|
|
588
|
+
context.sessionManager.recordFileRead(
|
|
589
|
+
context.sessionId,
|
|
590
|
+
fullPath,
|
|
591
|
+
contentHash3,
|
|
592
|
+
relative2(context.workdir, fullPath)
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
if (isScanned) {
|
|
596
|
+
return helpers.success(
|
|
597
|
+
`[PDF: ${args.path} \u2014 This PDF has no text layer (scanned or image-only). Use a shell command with OCR to extract text.]`,
|
|
598
|
+
false,
|
|
599
|
+
{
|
|
600
|
+
metadata: {
|
|
601
|
+
format: "pdf",
|
|
602
|
+
pageCount,
|
|
603
|
+
title,
|
|
604
|
+
author,
|
|
605
|
+
path: fullPath
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
return helpers.success(output2, truncated2, {
|
|
611
|
+
metadata: {
|
|
612
|
+
format: "pdf",
|
|
613
|
+
pageCount,
|
|
614
|
+
title,
|
|
615
|
+
author,
|
|
616
|
+
path: fullPath
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
} catch (err) {
|
|
620
|
+
return helpers.error(formatPdfErrorMessage(err));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const { encoding, confidence } = detectEncoding(rawBuffer);
|
|
624
|
+
const content = decodeContent(rawBuffer, encoding);
|
|
625
|
+
const lines = content.split("\n");
|
|
626
|
+
const totalLines = lines.length;
|
|
627
|
+
const startLine = Math.max(1, offset);
|
|
628
|
+
const endLine = Math.min(startLine + limit - 1, totalLines);
|
|
629
|
+
const selectedLines = lines.slice(startLine - 1, endLine);
|
|
630
|
+
const formatted = selectedLines.join("\n");
|
|
631
|
+
const truncated = endLine < totalLines;
|
|
632
|
+
let output = formatted;
|
|
633
|
+
if (truncated) {
|
|
634
|
+
output += `
|
|
635
|
+
|
|
636
|
+
[Showing lines ${startLine}-${endLine} of ${totalLines} total. Use offset to read more.]`;
|
|
637
|
+
}
|
|
638
|
+
if (output.length > OUTPUT_LIMITS.read_file.maxBytes) {
|
|
639
|
+
output = output.slice(0, OUTPUT_LIMITS.read_file.maxBytes);
|
|
640
|
+
output += "\n\n[Output truncated due to size limit]";
|
|
641
|
+
}
|
|
642
|
+
const contentHash2 = await computeFileHash(fullPath);
|
|
643
|
+
if (contentHash2) {
|
|
644
|
+
context.sessionManager.recordFileRead(
|
|
645
|
+
context.sessionId,
|
|
646
|
+
fullPath,
|
|
647
|
+
contentHash2,
|
|
648
|
+
relative2(context.workdir, fullPath)
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
return helpers.success(output, truncated, {
|
|
652
|
+
metadata: {
|
|
653
|
+
encoding,
|
|
654
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
655
|
+
lineCount: totalLines,
|
|
656
|
+
startLine,
|
|
657
|
+
endLine,
|
|
658
|
+
path: fullPath
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// src/server/llm/vision-fallback.ts
|
|
665
|
+
var IMAGE_PROMPT = `Describe this image thoroughly and accurately. Include:
|
|
666
|
+
- What the image shows (UI, diagram, photo, illustration, etc.)
|
|
667
|
+
- All visible text, transcribed verbatim \u2014 every label, heading, number, and value
|
|
668
|
+
- Spatial layout: where elements are positioned and how they relate
|
|
669
|
+
- Colors, visual hierarchy, and any distinctive visual features
|
|
670
|
+
|
|
671
|
+
Be exhaustive rather than concise. Prioritize precision \u2014 if there are numbers, lists, or structured data, capture them completely. If the image contains a table or grid, reproduce it as a markdown table with exact values.`;
|
|
672
|
+
function buildPrompt(context, question) {
|
|
673
|
+
if (question) {
|
|
674
|
+
return `Answer the following question about this image. Be precise and concise, and if the answer is not visible in the image, say so explicitly:
|
|
675
|
+
|
|
676
|
+
${question}`;
|
|
677
|
+
}
|
|
678
|
+
return context ? `${IMAGE_PROMPT}
|
|
679
|
+
|
|
680
|
+
Context: ${context}` : IMAGE_PROMPT;
|
|
681
|
+
}
|
|
682
|
+
function buildOllamaRequest(base64Data, model, context, question) {
|
|
683
|
+
return {
|
|
684
|
+
model,
|
|
685
|
+
messages: [
|
|
686
|
+
{
|
|
687
|
+
role: "user",
|
|
688
|
+
content: buildPrompt(context, question),
|
|
689
|
+
images: [base64Data]
|
|
690
|
+
}
|
|
691
|
+
],
|
|
692
|
+
stream: false,
|
|
693
|
+
think: false
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function buildOpenAIRequest(base64Data, model, context, question) {
|
|
697
|
+
return {
|
|
698
|
+
model,
|
|
699
|
+
messages: [
|
|
700
|
+
{
|
|
701
|
+
role: "user",
|
|
702
|
+
content: [
|
|
703
|
+
{ type: "text", text: buildPrompt(context, question) },
|
|
704
|
+
{ type: "image_url", image_url: { url: `data:image/png;base64,${base64Data}` } }
|
|
705
|
+
]
|
|
706
|
+
}
|
|
707
|
+
]
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function parseOllamaResponse(data) {
|
|
711
|
+
const resp = data;
|
|
712
|
+
return resp.message?.content?.trim() ?? null;
|
|
713
|
+
}
|
|
714
|
+
function parseOpenAIResponse(data) {
|
|
715
|
+
const resp = data;
|
|
716
|
+
const choice = resp.choices?.[0];
|
|
717
|
+
if (!choice) return null;
|
|
718
|
+
return choice.message?.content?.trim() ?? null;
|
|
719
|
+
}
|
|
720
|
+
async function describeImage(base64Data, visionModel, options) {
|
|
721
|
+
const timeout = visionModel.timeout;
|
|
722
|
+
try {
|
|
723
|
+
const isOpenAI = visionModel.backend === "openai";
|
|
724
|
+
const url = isOpenAI ? `${visionModel.baseUrl.replace(/\/+$/, "")}/chat/completions` : `${visionModel.baseUrl.replace(/\/+$/, "")}/api/chat`;
|
|
725
|
+
const requestBody = isOpenAI ? buildOpenAIRequest(base64Data, visionModel.model, options?.context, options?.question) : buildOllamaRequest(base64Data, visionModel.model, options?.context, options?.question);
|
|
726
|
+
const timeoutController = new AbortController();
|
|
727
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), timeout);
|
|
728
|
+
const signal = options?.signal ? AbortSignal.any([timeoutController.signal, options.signal]) : timeoutController.signal;
|
|
729
|
+
const headers = {
|
|
730
|
+
"Content-Type": "application/json"
|
|
731
|
+
};
|
|
732
|
+
if (isOpenAI && visionModel.apiKey) {
|
|
733
|
+
headers["Authorization"] = `Bearer ${visionModel.apiKey}`;
|
|
734
|
+
}
|
|
735
|
+
const response = await fetch(url, {
|
|
736
|
+
method: "POST",
|
|
737
|
+
headers,
|
|
738
|
+
body: JSON.stringify(requestBody),
|
|
739
|
+
signal
|
|
740
|
+
});
|
|
741
|
+
clearTimeout(timeoutId);
|
|
742
|
+
if (!response.ok) {
|
|
743
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
744
|
+
logger.error("Vision fallback API error", { status: response.status, error: errorText });
|
|
745
|
+
return `[Image description failed: HTTP ${response.status}]`;
|
|
746
|
+
}
|
|
747
|
+
const data = await response.json();
|
|
748
|
+
const description = isOpenAI ? parseOpenAIResponse(data) : parseOllamaResponse(data);
|
|
749
|
+
if (!description) {
|
|
750
|
+
logger.warn("Vision fallback returned empty description");
|
|
751
|
+
return "[Image - could not describe]";
|
|
752
|
+
}
|
|
753
|
+
return description;
|
|
754
|
+
} catch (error) {
|
|
755
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
756
|
+
logger.error("Vision fallback error", { error: message });
|
|
757
|
+
if (message.includes("abort")) {
|
|
758
|
+
return "[Image description timed out]";
|
|
759
|
+
}
|
|
760
|
+
return `[Image description failed: ${message}]`;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function describeImageFromDataUrl(dataUrl, visionModel, options) {
|
|
764
|
+
const base64Match = dataUrl.match(/^data:image\/[^;]+;base64,(.+)$/);
|
|
765
|
+
if (!base64Match || !base64Match[1]) {
|
|
766
|
+
return "[Invalid image data URL]";
|
|
767
|
+
}
|
|
768
|
+
return describeImage(base64Match[1], visionModel, options);
|
|
769
|
+
}
|
|
770
|
+
function isVisionFallbackFailure(description) {
|
|
771
|
+
return description.startsWith("[Image description failed:") || description === "[Image description timed out]" || description === "[Image - could not describe]" || description === "[Invalid image data URL]";
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/server/context/image-processor.ts
|
|
775
|
+
async function loadVisionModelFromGlobalConfig() {
|
|
776
|
+
try {
|
|
777
|
+
const { loadGlobalConfig, resolveVisionFallback } = await import("./config-LOK3LXFH.js");
|
|
778
|
+
const runtimeConfig = getRuntimeConfig();
|
|
779
|
+
const mode = runtimeConfig.mode ?? "production";
|
|
780
|
+
const globalConfig = await loadGlobalConfig(mode);
|
|
781
|
+
return resolveVisionFallback(globalConfig);
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
784
|
+
return void 0;
|
|
785
|
+
}
|
|
786
|
+
async function loadResolvedVisionModel() {
|
|
787
|
+
const runtimeConfig = getRuntimeConfig();
|
|
788
|
+
const llm = runtimeConfig.llm;
|
|
789
|
+
if (llm?.visionModel) {
|
|
790
|
+
return {
|
|
791
|
+
baseUrl: llm.baseUrl,
|
|
792
|
+
model: llm.visionModel,
|
|
793
|
+
timeout: llm.timeout,
|
|
794
|
+
backend: llm.backend === "ollama" ? "ollama" : "openai"
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
return loadVisionModelFromGlobalConfig();
|
|
798
|
+
}
|
|
799
|
+
var descriptionCache = /* @__PURE__ */ new Map();
|
|
800
|
+
function isImageAttachment(att) {
|
|
801
|
+
return att.mimeType.startsWith("image/");
|
|
802
|
+
}
|
|
803
|
+
function isPdfAttachment(att) {
|
|
804
|
+
return att.mimeType === "application/pdf";
|
|
805
|
+
}
|
|
806
|
+
function hasImageMetadata(result) {
|
|
807
|
+
const meta = result.metadata;
|
|
808
|
+
if (!meta) return false;
|
|
809
|
+
const dataUrl = meta["dataUrl"];
|
|
810
|
+
const mimeType = meta["mimeType"];
|
|
811
|
+
return typeof dataUrl === "string" && typeof mimeType === "string" && mimeType.startsWith("image/");
|
|
812
|
+
}
|
|
813
|
+
async function describeImageDataUrl(dataUrl, attachmentId, messageId, options, descriptions, filename) {
|
|
814
|
+
const cacheKey = contentHash(dataUrl);
|
|
815
|
+
if (descriptionCache.has(cacheKey)) {
|
|
816
|
+
const cached = descriptionCache.get(cacheKey);
|
|
817
|
+
descriptions.set(attachmentId, cached);
|
|
818
|
+
return cached;
|
|
819
|
+
}
|
|
820
|
+
if (options.visionModel) {
|
|
821
|
+
const startData = {
|
|
822
|
+
messageId,
|
|
823
|
+
attachmentId
|
|
824
|
+
};
|
|
825
|
+
if (filename !== void 0) {
|
|
826
|
+
startData.filename = filename;
|
|
827
|
+
}
|
|
828
|
+
options.onEvent?.({ type: "vision_fallback.start", data: startData });
|
|
829
|
+
const description = await describeImageFromDataUrl(dataUrl, options.visionModel, {
|
|
830
|
+
context: filename ? `File: ${filename}` : void 0,
|
|
831
|
+
signal: options.signal
|
|
832
|
+
});
|
|
833
|
+
cacheSet(descriptionCache, cacheKey, description);
|
|
834
|
+
descriptions.set(attachmentId, description);
|
|
835
|
+
options.onEvent?.({ type: "vision_fallback.done", data: { messageId, attachmentId, description } });
|
|
836
|
+
return description;
|
|
837
|
+
}
|
|
838
|
+
const placeholder = filename ? `[Image: ${filename}]` : "[Image]";
|
|
839
|
+
descriptions.set(attachmentId, placeholder);
|
|
840
|
+
return placeholder;
|
|
841
|
+
}
|
|
842
|
+
async function describeAttachment(att, messageId, options, descriptions) {
|
|
843
|
+
return describeImageDataUrl(att.data, att.id, messageId, options, descriptions, att.filename);
|
|
844
|
+
}
|
|
845
|
+
async function describePdfAttachment(att, messageId, options, descriptions) {
|
|
846
|
+
const hasVision = !!options.visionModel;
|
|
847
|
+
const cacheKey = `pdf:${hasVision}:${contentHash(att.data)}`;
|
|
848
|
+
const filenameLabel = att.filename || "document.pdf";
|
|
849
|
+
if (descriptionCache.has(cacheKey)) {
|
|
850
|
+
const cached = descriptionCache.get(cacheKey);
|
|
851
|
+
const output2 = `[PDF: ${filenameLabel}]${cached}`;
|
|
852
|
+
descriptions.set(att.id, output2);
|
|
853
|
+
return output2;
|
|
854
|
+
}
|
|
855
|
+
const buffer = decodeDataUrl(att.data);
|
|
856
|
+
if (!buffer) {
|
|
857
|
+
const fallback = `[PDF: ${filenameLabel}] (could not decode)`;
|
|
858
|
+
cacheSet(descriptionCache, cacheKey, fallback);
|
|
859
|
+
descriptions.set(att.id, fallback);
|
|
860
|
+
return fallback;
|
|
861
|
+
}
|
|
862
|
+
let blocks;
|
|
863
|
+
try {
|
|
864
|
+
const result = await extractPdfContent(buffer);
|
|
865
|
+
blocks = result.blocks;
|
|
866
|
+
} catch {
|
|
867
|
+
const errorText = `[PDF: ${filenameLabel}] (could not extract content)`;
|
|
868
|
+
descriptions.set(att.id, errorText);
|
|
869
|
+
return errorText;
|
|
870
|
+
}
|
|
871
|
+
let body = "";
|
|
872
|
+
let imageIndex = 0;
|
|
873
|
+
for (const block of blocks) {
|
|
874
|
+
if (block.type === "text" && block.content) {
|
|
875
|
+
body += "\n\n" + block.content;
|
|
876
|
+
} else if (block.type === "image" && block.dataUrl) {
|
|
877
|
+
const imgAttachmentId = `${att.id}/image-${imageIndex}`;
|
|
878
|
+
const description = await describeImageDataUrl(block.dataUrl, imgAttachmentId, messageId, options, descriptions);
|
|
879
|
+
body += "\n\n" + description;
|
|
880
|
+
imageIndex++;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const output = `[PDF: ${filenameLabel}]${body}`;
|
|
884
|
+
cacheSet(descriptionCache, cacheKey, body);
|
|
885
|
+
descriptions.set(att.id, output);
|
|
886
|
+
return output;
|
|
887
|
+
}
|
|
888
|
+
async function processContextImages(events, options) {
|
|
889
|
+
if (options.modelSupportsVision) {
|
|
890
|
+
return { events, descriptions: /* @__PURE__ */ new Map() };
|
|
891
|
+
}
|
|
892
|
+
const descriptions = /* @__PURE__ */ new Map();
|
|
893
|
+
const modifiedEvents = events.map((event) => structuredClone(event));
|
|
894
|
+
for (const event of modifiedEvents) {
|
|
895
|
+
if (event.type === "message.start") {
|
|
896
|
+
const data = event.data;
|
|
897
|
+
if (!data.attachments || data.attachments.length === 0) continue;
|
|
898
|
+
let enriched = false;
|
|
899
|
+
const imageAtts = data.attachments.filter(isImageAttachment);
|
|
900
|
+
for (const att of imageAtts) {
|
|
901
|
+
if (att.description) {
|
|
902
|
+
descriptions.set(att.id, att.description);
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
const description = await describeAttachment(att, data.messageId, options, descriptions);
|
|
906
|
+
att.description = description;
|
|
907
|
+
enriched = true;
|
|
908
|
+
}
|
|
909
|
+
const pdfAtts = data.attachments.filter(isPdfAttachment);
|
|
910
|
+
for (const att of pdfAtts) {
|
|
911
|
+
if (att.pdfContent) {
|
|
912
|
+
descriptions.set(att.id, att.pdfContent);
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const pdfContent = await describePdfAttachment(att, data.messageId, options, descriptions);
|
|
916
|
+
att.pdfContent = pdfContent;
|
|
917
|
+
enriched = true;
|
|
918
|
+
}
|
|
919
|
+
if (enriched && options.persistEvent) {
|
|
920
|
+
options.persistEvent(event.sessionId, event.seq, data);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (event.type === "tool.result") {
|
|
924
|
+
const data = event.data;
|
|
925
|
+
if (!data.result.metadata || !hasImageMetadata(data.result)) continue;
|
|
926
|
+
const meta = data.result.metadata;
|
|
927
|
+
if (meta["description"]) {
|
|
928
|
+
descriptions.set(data.toolCallId, meta["description"]);
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
const dataUrl = meta["dataUrl"];
|
|
932
|
+
const path = meta["path"];
|
|
933
|
+
const description = await describeImageDataUrl(
|
|
934
|
+
dataUrl,
|
|
935
|
+
data.toolCallId,
|
|
936
|
+
data.messageId,
|
|
937
|
+
options,
|
|
938
|
+
descriptions,
|
|
939
|
+
path
|
|
940
|
+
);
|
|
941
|
+
meta["description"] = description;
|
|
942
|
+
if (options.persistEvent) {
|
|
943
|
+
options.persistEvent(event.sessionId, event.seq, data);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (event.type === "turn.snapshot") {
|
|
947
|
+
const snapshot = event.data;
|
|
948
|
+
let enriched = false;
|
|
949
|
+
for (const message of snapshot.messages) {
|
|
950
|
+
if (message.role === "user" && message.attachments && message.attachments.length > 0) {
|
|
951
|
+
for (const att of message.attachments) {
|
|
952
|
+
if (isImageAttachment(att)) {
|
|
953
|
+
if (att.description) {
|
|
954
|
+
descriptions.set(att.id, att.description);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
const description = await describeAttachment(att, message.id, options, descriptions);
|
|
958
|
+
att.description = description;
|
|
959
|
+
enriched = true;
|
|
960
|
+
} else if (isPdfAttachment(att)) {
|
|
961
|
+
if (att.pdfContent) {
|
|
962
|
+
descriptions.set(att.id, att.pdfContent);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
const pdfContent = await describePdfAttachment(att, message.id, options, descriptions);
|
|
966
|
+
att.pdfContent = pdfContent;
|
|
967
|
+
enriched = true;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (message.role === "assistant" && message.toolCalls) {
|
|
972
|
+
for (const toolCall of message.toolCalls) {
|
|
973
|
+
if (!toolCall.result || !toolCall.result.metadata || !hasImageMetadata(toolCall.result)) continue;
|
|
974
|
+
const meta = toolCall.result.metadata;
|
|
975
|
+
if (meta["description"]) {
|
|
976
|
+
descriptions.set(toolCall.id, meta["description"]);
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
const dataUrl = meta["dataUrl"];
|
|
980
|
+
const path = meta["path"];
|
|
981
|
+
const description = await describeImageDataUrl(
|
|
982
|
+
dataUrl,
|
|
983
|
+
toolCall.id,
|
|
984
|
+
message.id,
|
|
985
|
+
options,
|
|
986
|
+
descriptions,
|
|
987
|
+
path
|
|
988
|
+
);
|
|
989
|
+
meta["description"] = description;
|
|
990
|
+
enriched = true;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (enriched && options.persistEvent) {
|
|
995
|
+
options.persistEvent(event.sessionId, event.seq, snapshot);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return { events: modifiedEvents, descriptions };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/server/tools/describe-image.ts
|
|
1003
|
+
async function isDescribeImageEligible(modelName) {
|
|
1004
|
+
if (!modelName) return false;
|
|
1005
|
+
if (modelSupportsVision(modelName)) return false;
|
|
1006
|
+
const visionModel = await loadResolvedVisionModel();
|
|
1007
|
+
return !!visionModel;
|
|
1008
|
+
}
|
|
1009
|
+
var describeImageTool = createTool(
|
|
1010
|
+
"describe_image",
|
|
1011
|
+
{
|
|
1012
|
+
type: "function",
|
|
1013
|
+
function: {
|
|
1014
|
+
name: "describe_image",
|
|
1015
|
+
description: "Ask a vision fallback model a specific question about an image file.",
|
|
1016
|
+
parameters: {
|
|
1017
|
+
type: "object",
|
|
1018
|
+
properties: {
|
|
1019
|
+
path: {
|
|
1020
|
+
type: "string",
|
|
1021
|
+
description: "Path to the image file (relative to workdir or absolute)"
|
|
1022
|
+
},
|
|
1023
|
+
question: {
|
|
1024
|
+
type: "string",
|
|
1025
|
+
description: 'The precise detail to extract from the image (e.g. "What does the button in the top-right corner say?")'
|
|
1026
|
+
}
|
|
1027
|
+
},
|
|
1028
|
+
required: ["path", "question"]
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
async (args, context, helpers) => {
|
|
1033
|
+
const question = String(args.question ?? "").trim();
|
|
1034
|
+
if (!question) {
|
|
1035
|
+
return helpers.error(
|
|
1036
|
+
serverT({
|
|
1037
|
+
en: "A question is required for describe_image.",
|
|
1038
|
+
fr: "Une question est requise pour describe_image."
|
|
1039
|
+
})
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
const fullPath = helpers.resolvePath(args.path);
|
|
1043
|
+
await helpers.checkPathAccess([fullPath]);
|
|
1044
|
+
const activeModel = context.llmClient?.getModel();
|
|
1045
|
+
if (activeModel && modelSupportsVision(activeModel)) {
|
|
1046
|
+
return helpers.error(
|
|
1047
|
+
serverT({
|
|
1048
|
+
en: "describe_image is only available when the active model cannot see images. The active model supports vision, so read the image directly with read_file instead.",
|
|
1049
|
+
fr: "describe_image n\u2019est disponible que lorsque le mod\xE8le actif ne voit pas les images. Le mod\xE8le actif supporte la vision : lisez plut\xF4t l\u2019image directement avec read_file."
|
|
1050
|
+
})
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
let size;
|
|
1054
|
+
try {
|
|
1055
|
+
const stats = await stat2(fullPath);
|
|
1056
|
+
size = stats.size;
|
|
1057
|
+
} catch {
|
|
1058
|
+
return helpers.error(
|
|
1059
|
+
serverT({ en: "File not found: {{path}}", fr: "Fichier introuvable : {{path}}" }, { path: args.path })
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
if (size > OUTPUT_LIMITS.read_file.maxFileBytes) {
|
|
1063
|
+
return helpers.error(
|
|
1064
|
+
serverT(
|
|
1065
|
+
{
|
|
1066
|
+
en: "File size ({{size}} bytes) exceeds maximum file size (20MB). Use a shell command to process large files.",
|
|
1067
|
+
fr: "La taille du fichier ({{size}} octets) d\xE9passe la taille maximale (20 Mo). Utilisez une commande shell pour traiter les fichiers volumineux."
|
|
1068
|
+
},
|
|
1069
|
+
{ size }
|
|
1070
|
+
)
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
if (size > OUTPUT_LIMITS.read_file.maxImageBytes) {
|
|
1074
|
+
return helpers.error(
|
|
1075
|
+
serverT(
|
|
1076
|
+
{
|
|
1077
|
+
en: "File size ({{size}} bytes) exceeds image size limit (2MB). Use a shell command to process large files.",
|
|
1078
|
+
fr: "La taille du fichier ({{size}} octets) d\xE9passe la limite pour les images (2 Mo). Utilisez une commande shell pour traiter les fichiers volumineux."
|
|
1079
|
+
},
|
|
1080
|
+
{ size }
|
|
1081
|
+
)
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
const rawBuffer = await readFile3(fullPath);
|
|
1085
|
+
const mimeType = await detectImageType(rawBuffer, args.path);
|
|
1086
|
+
if (!mimeType) {
|
|
1087
|
+
return helpers.error(
|
|
1088
|
+
serverT(
|
|
1089
|
+
{
|
|
1090
|
+
en: "Not an image file: {{path}}. describe_image only works on images (PNG, JPEG, GIF, WebP, BMP, SVG).",
|
|
1091
|
+
fr: "Ce n\u2019est pas une image : {{path}}. describe_image ne fonctionne que sur des images (PNG, JPEG, GIF, WebP, BMP, SVG)."
|
|
1092
|
+
},
|
|
1093
|
+
{ path: args.path }
|
|
1094
|
+
)
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
const visionModel = await loadResolvedVisionModel();
|
|
1098
|
+
if (!visionModel) {
|
|
1099
|
+
return helpers.error(
|
|
1100
|
+
serverT({
|
|
1101
|
+
en: "No vision fallback is configured. Configure a vision fallback to use describe_image.",
|
|
1102
|
+
fr: "Aucun fallback vision n\u2019est configur\xE9. Configurez un fallback vision pour utiliser describe_image."
|
|
1103
|
+
})
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
const base64Data = rawBuffer.toString("base64");
|
|
1107
|
+
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
|
1108
|
+
const contentHash2 = await computeFileHash(fullPath);
|
|
1109
|
+
if (contentHash2) {
|
|
1110
|
+
context.sessionManager.recordFileRead(
|
|
1111
|
+
context.sessionId,
|
|
1112
|
+
fullPath,
|
|
1113
|
+
contentHash2,
|
|
1114
|
+
relative3(context.workdir, fullPath)
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
const answer = await describeImageFromDataUrl(dataUrl, visionModel, {
|
|
1118
|
+
question,
|
|
1119
|
+
signal: context.signal
|
|
1120
|
+
});
|
|
1121
|
+
if (isVisionFallbackFailure(answer)) {
|
|
1122
|
+
return helpers.error(answer);
|
|
1123
|
+
}
|
|
1124
|
+
return helpers.success(answer, false, {
|
|
1125
|
+
metadata: {
|
|
1126
|
+
mimeType,
|
|
1127
|
+
size,
|
|
1128
|
+
base64Data,
|
|
1129
|
+
dataUrl,
|
|
1130
|
+
path: fullPath,
|
|
1131
|
+
question,
|
|
1132
|
+
description: answer
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
export {
|
|
1139
|
+
requestUserConfirmation,
|
|
1140
|
+
buildSignal,
|
|
1141
|
+
requireSession,
|
|
1142
|
+
validateActionWithPermission,
|
|
1143
|
+
createTool,
|
|
1144
|
+
computeFileHash,
|
|
1145
|
+
validateFileForWrite,
|
|
1146
|
+
detectEncoding,
|
|
1147
|
+
decodeContent,
|
|
1148
|
+
encodeContent,
|
|
1149
|
+
readFileTool,
|
|
1150
|
+
loadResolvedVisionModel,
|
|
1151
|
+
processContextImages,
|
|
1152
|
+
isDescribeImageEligible,
|
|
1153
|
+
describeImageTool
|
|
1154
|
+
};
|
|
1155
|
+
//# sourceMappingURL=chunk-U6FHHT2B.js.map
|