qlogicagent 0.2.1 → 0.3.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/agent.js +1 -0
- package/dist/cli.js +9 -0
- package/dist/contracts.js +1 -0
- package/dist/index.js +5 -15
- package/dist/orchestration.js +118 -0
- package/package.json +56 -42
- package/dist/agent/agent.js +0 -113
- package/dist/agent/tool-loop.js +0 -575
- package/dist/agent/types.js +0 -14
- package/dist/cli/main.js +0 -23
- package/dist/cli/stdio-server.js +0 -463
- package/dist/config/config.js +0 -21
- package/dist/contracts/hooks.js +0 -7
- package/dist/contracts/index.js +0 -10
- package/dist/contracts/planner.js +0 -2
- package/dist/contracts/skill-candidate.js +0 -195
- package/dist/contracts/todo.js +0 -9
- package/dist/llm/builtin-providers.js +0 -531
- package/dist/llm/index.js +0 -14
- package/dist/llm/llm-client.js +0 -67
- package/dist/llm/model-catalog.js +0 -191
- package/dist/llm/provider-def.js +0 -12
- package/dist/llm/provider-registry.js +0 -147
- package/dist/llm/transport.js +0 -27
- package/dist/llm/transports/anthropic-messages.js +0 -293
- package/dist/llm/transports/openai-chat.js +0 -165
- package/dist/orchestration/agent-registry.js +0 -116
- package/dist/orchestration/approval-aware-tool-plan.js +0 -87
- package/dist/orchestration/context-compression.js +0 -583
- package/dist/orchestration/conversation-repair.js +0 -429
- package/dist/orchestration/curator-scheduler.js +0 -135
- package/dist/orchestration/embedded-failover-policy.js +0 -168
- package/dist/orchestration/error-classification.js +0 -77
- package/dist/orchestration/failover-classification.js +0 -381
- package/dist/orchestration/failover-error.js +0 -198
- package/dist/orchestration/fork-subagent.js +0 -98
- package/dist/orchestration/index.js +0 -267
- package/dist/orchestration/memory-flush-policy.js +0 -85
- package/dist/orchestration/memory-provider.js +0 -2
- package/dist/orchestration/parallel-tool-calls.js +0 -59
- package/dist/orchestration/prompt-cache-strategy.js +0 -228
- package/dist/orchestration/reactive-compact.js +0 -78
- package/dist/orchestration/retry-loop.js +0 -24
- package/dist/orchestration/skill-candidate.js +0 -141
- package/dist/orchestration/skill-consolidation.js +0 -220
- package/dist/orchestration/skill-improvement.js +0 -66
- package/dist/orchestration/skill-similarity.js +0 -131
- package/dist/orchestration/streaming-tool-executor.js +0 -96
- package/dist/orchestration/team-orchestration.js +0 -369
- package/dist/orchestration/team-tool-loop-wiring.js +0 -147
- package/dist/orchestration/tool-choice-policy.js +0 -164
- package/dist/orchestration/tool-loop-state.js +0 -133
- package/dist/orchestration/tool-schema.js +0 -297
- package/dist/orchestration/transcript-repair.js +0 -426
- package/dist/orchestration/turn-loop-guard.js +0 -92
- package/dist/orchestration/web-browser-policy.js +0 -39
- package/dist/runtime/context-compression.js +0 -274
- package/dist/runtime/hook-registry.js +0 -53
- package/dist/runtime/memory-hooks.js +0 -65
- package/dist/runtime/tool-eligibility.js +0 -111
- package/dist/skills/index.js +0 -82
- package/dist/skills/memory-extractor.js +0 -173
- package/dist/skills/memory-query-tool.js +0 -127
- package/dist/skills/memory-store.js +0 -228
- package/dist/skills/memory-tool.js +0 -192
- package/dist/skills/portable-tool.js +0 -14
- package/dist/skills/qmemory-adapter.js +0 -165
- package/dist/skills/skill-frontmatter.js +0 -344
- package/dist/skills/skill-guard.js +0 -229
- package/dist/skills/skill-loader.js +0 -303
- package/dist/skills/skill-source.js +0 -126
- package/dist/skills/skill-types.js +0 -6
- package/dist/skills/think-tool.js +0 -59
- package/dist/skills/todo-tool.js +0 -114
- package/dist/skills/tools/agent-tool.js +0 -142
- package/dist/skills/tools/apply-patch-tool.js +0 -184
- package/dist/skills/tools/ask-user-tool.js +0 -121
- package/dist/skills/tools/brief-tool.js +0 -95
- package/dist/skills/tools/browser-tool.js +0 -155
- package/dist/skills/tools/checkpoint-tool.js +0 -102
- package/dist/skills/tools/config-tool.js +0 -143
- package/dist/skills/tools/cron-tool.js +0 -175
- package/dist/skills/tools/edit-tool.js +0 -70
- package/dist/skills/tools/exec-tool.js +0 -133
- package/dist/skills/tools/image-generate-tool.js +0 -67
- package/dist/skills/tools/instructions-tool.js +0 -187
- package/dist/skills/tools/lsp-tool.js +0 -227
- package/dist/skills/tools/mcp-client-types.js +0 -53
- package/dist/skills/tools/mcp-tool.js +0 -503
- package/dist/skills/tools/memory-tool.js +0 -88
- package/dist/skills/tools/monitor-tool.js +0 -131
- package/dist/skills/tools/music-generate-tool.js +0 -62
- package/dist/skills/tools/notify-tool.js +0 -62
- package/dist/skills/tools/patch-tool.js +0 -505
- package/dist/skills/tools/pdf-tool.js +0 -88
- package/dist/skills/tools/plan-mode-tool.js +0 -122
- package/dist/skills/tools/read-tool.js +0 -84
- package/dist/skills/tools/repl-tool.js +0 -69
- package/dist/skills/tools/search-tool.js +0 -225
- package/dist/skills/tools/send-message-tool.js +0 -76
- package/dist/skills/tools/skill-list-tool.js +0 -54
- package/dist/skills/tools/skill-manage-tool.js +0 -153
- package/dist/skills/tools/skill-view-tool.js +0 -72
- package/dist/skills/tools/sleep-tool.js +0 -81
- package/dist/skills/tools/structured-output-tool.js +0 -176
- package/dist/skills/tools/task-tool.js +0 -161
- package/dist/skills/tools/team-tool.js +0 -105
- package/dist/skills/tools/tool-search-tool.js +0 -110
- package/dist/skills/tools/tts-tool.js +0 -45
- package/dist/skills/tools/video-edit-tool.js +0 -74
- package/dist/skills/tools/video-generate-tool.js +0 -66
- package/dist/skills/tools/video-merge-tool.js +0 -92
- package/dist/skills/tools/video-upscale-tool.js +0 -52
- package/dist/skills/tools/web-fetch-tool.js +0 -92
- package/dist/skills/tools/web-search-tool.js +0 -86
- package/dist/skills/tools/worktree-tool.js +0 -147
- package/dist/skills/tools/write-tool.js +0 -81
- /package/dist/{agent → types/agent}/agent.d.ts +0 -0
- /package/dist/{agent → types/agent}/tool-loop.d.ts +0 -0
- /package/dist/{agent → types/agent}/types.d.ts +0 -0
- /package/dist/{cli → types/cli}/main.d.ts +0 -0
- /package/dist/{cli → types/cli}/stdio-server.d.ts +0 -0
- /package/dist/{config → types/config}/config.d.ts +0 -0
- /package/dist/{contracts → types/contracts}/hooks.d.ts +0 -0
- /package/dist/{contracts → types/contracts}/index.d.ts +0 -0
- /package/dist/{contracts → types/contracts}/planner.d.ts +0 -0
- /package/dist/{contracts → types/contracts}/skill-candidate.d.ts +0 -0
- /package/dist/{contracts → types/contracts}/todo.d.ts +0 -0
- /package/dist/{index.d.ts → types/index.d.ts} +0 -0
- /package/dist/{llm → types/llm}/builtin-providers.d.ts +0 -0
- /package/dist/{llm → types/llm}/index.d.ts +0 -0
- /package/dist/{llm → types/llm}/llm-client.d.ts +0 -0
- /package/dist/{llm → types/llm}/model-catalog.d.ts +0 -0
- /package/dist/{llm → types/llm}/provider-def.d.ts +0 -0
- /package/dist/{llm → types/llm}/provider-registry.d.ts +0 -0
- /package/dist/{llm → types/llm}/transport.d.ts +0 -0
- /package/dist/{llm → types/llm}/transports/anthropic-messages.d.ts +0 -0
- /package/dist/{llm → types/llm}/transports/openai-chat.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/agent-registry.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/approval-aware-tool-plan.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/context-compression.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/conversation-repair.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/curator-scheduler.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/embedded-failover-policy.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/error-classification.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/failover-classification.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/failover-error.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/fork-subagent.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/index.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/memory-flush-policy.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/memory-provider.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/parallel-tool-calls.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/prompt-cache-strategy.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/reactive-compact.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/retry-loop.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/skill-candidate.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/skill-consolidation.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/skill-improvement.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/skill-similarity.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/streaming-tool-executor.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/team-orchestration.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/team-tool-loop-wiring.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/tool-choice-policy.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/tool-loop-state.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/tool-schema.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/transcript-repair.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/turn-loop-guard.d.ts +0 -0
- /package/dist/{orchestration → types/orchestration}/web-browser-policy.d.ts +0 -0
- /package/dist/{runtime → types/runtime}/context-compression.d.ts +0 -0
- /package/dist/{runtime → types/runtime}/hook-registry.d.ts +0 -0
- /package/dist/{runtime → types/runtime}/memory-hooks.d.ts +0 -0
- /package/dist/{runtime → types/runtime}/tool-eligibility.d.ts +0 -0
- /package/dist/{skills → types/skills}/index.d.ts +0 -0
- /package/dist/{skills → types/skills}/memory-extractor.d.ts +0 -0
- /package/dist/{skills → types/skills}/memory-query-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/memory-store.d.ts +0 -0
- /package/dist/{skills → types/skills}/memory-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/portable-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/qmemory-adapter.d.ts +0 -0
- /package/dist/{skills → types/skills}/skill-frontmatter.d.ts +0 -0
- /package/dist/{skills → types/skills}/skill-guard.d.ts +0 -0
- /package/dist/{skills → types/skills}/skill-loader.d.ts +0 -0
- /package/dist/{skills → types/skills}/skill-source.d.ts +0 -0
- /package/dist/{skills → types/skills}/skill-types.d.ts +0 -0
- /package/dist/{skills → types/skills}/think-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/todo-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/agent-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/apply-patch-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/ask-user-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/brief-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/browser-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/checkpoint-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/config-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/cron-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/edit-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/exec-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/image-generate-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/instructions-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/lsp-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/mcp-client-types.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/mcp-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/memory-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/monitor-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/music-generate-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/notify-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/patch-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/pdf-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/plan-mode-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/read-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/repl-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/search-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/send-message-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/skill-list-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/skill-manage-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/skill-view-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/sleep-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/structured-output-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/task-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/team-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/tool-search-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/tts-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/video-edit-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/video-generate-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/video-merge-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/video-upscale-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/web-fetch-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/web-search-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/worktree-tool.d.ts +0 -0
- /package/dist/{skills → types/skills}/tools/write-tool.d.ts +0 -0
|
@@ -1,505 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Patch Tool — fuzzy-match file editing with 9-strategy degradation.
|
|
3
|
-
// Reference: hermes-agent-main/tools/fuzzy_match.py (9-strategy chain)
|
|
4
|
-
// hermes-agent-main/tools/patch_parser.py (V4A format)
|
|
5
|
-
// Category: coding
|
|
6
|
-
// ============================================================
|
|
7
|
-
export const PATCH_TOOL_NAME = "patch";
|
|
8
|
-
export const PATCH_TOOL_SCHEMA = {
|
|
9
|
-
type: "object",
|
|
10
|
-
properties: {
|
|
11
|
-
input: {
|
|
12
|
-
type: "string",
|
|
13
|
-
description: "Patch content. Supports two formats:\n" +
|
|
14
|
-
"1) V4A unified diff (*** Begin Patch / *** End Patch markers)\n" +
|
|
15
|
-
"2) Simple find-replace: first line is file path, then <<<< SEARCH / ==== / >>>> REPLACE blocks.\n" +
|
|
16
|
-
"Fuzzy matching automatically handles whitespace/indent/unicode drift in the search text.",
|
|
17
|
-
},
|
|
18
|
-
replaceAll: {
|
|
19
|
-
type: "boolean",
|
|
20
|
-
description: "Replace all occurrences instead of first only (default: false).",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
required: ["input"],
|
|
24
|
-
};
|
|
25
|
-
function escapeNormalize(s) {
|
|
26
|
-
return s.replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\'/g, "'").replace(/\\"/g, '"');
|
|
27
|
-
}
|
|
28
|
-
function unicodeNormalize(s) {
|
|
29
|
-
return s
|
|
30
|
-
.replace(/[\u201c\u201d\u201e\u201f]/g, '"')
|
|
31
|
-
.replace(/[\u2018\u2019\u201a\u201b]/g, "'")
|
|
32
|
-
.replace(/\u2014/g, "--")
|
|
33
|
-
.replace(/\u2013/g, "-")
|
|
34
|
-
.replace(/\u2026/g, "...")
|
|
35
|
-
.replace(/\u00a0/g, " ");
|
|
36
|
-
}
|
|
37
|
-
function lineSimilarity(a, b) {
|
|
38
|
-
if (a === b)
|
|
39
|
-
return 1;
|
|
40
|
-
const longer = Math.max(a.length, b.length);
|
|
41
|
-
if (longer === 0)
|
|
42
|
-
return 1;
|
|
43
|
-
let dist = 0;
|
|
44
|
-
const la = a.length;
|
|
45
|
-
const lb = b.length;
|
|
46
|
-
const maxLen = Math.max(la, lb);
|
|
47
|
-
// Simplified: character-level distance ratio
|
|
48
|
-
for (let i = 0; i < Math.min(la, lb); i++) {
|
|
49
|
-
if (a[i] !== b[i])
|
|
50
|
-
dist++;
|
|
51
|
-
}
|
|
52
|
-
dist += Math.abs(la - lb);
|
|
53
|
-
return 1 - dist / maxLen;
|
|
54
|
-
}
|
|
55
|
-
function fuzzyFind(content, search, replaceAll) {
|
|
56
|
-
const results = [];
|
|
57
|
-
// Strategy 1: exact
|
|
58
|
-
let idx = content.indexOf(search);
|
|
59
|
-
if (idx !== -1) {
|
|
60
|
-
if (replaceAll) {
|
|
61
|
-
let pos = 0;
|
|
62
|
-
while ((idx = content.indexOf(search, pos)) !== -1) {
|
|
63
|
-
results.push({ start: idx, end: idx + search.length, strategy: "exact" });
|
|
64
|
-
pos = idx + search.length;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
results.push({ start: idx, end: idx + search.length, strategy: "exact" });
|
|
69
|
-
}
|
|
70
|
-
return results;
|
|
71
|
-
}
|
|
72
|
-
// Strategy 2: line_trimmed
|
|
73
|
-
const searchLines = search.split("\n").map((l) => l.trim());
|
|
74
|
-
const contentLines = content.split("\n");
|
|
75
|
-
const trimmedMatch = findLineBlock(contentLines, searchLines, (a, b) => a.trim() === b);
|
|
76
|
-
if (trimmedMatch) {
|
|
77
|
-
return [{ ...lineBlockToRange(content, contentLines, trimmedMatch), strategy: "line_trimmed" }];
|
|
78
|
-
}
|
|
79
|
-
// Strategy 3: whitespace_normalized
|
|
80
|
-
const normSearch = search.replace(/[ \t]+/g, " ");
|
|
81
|
-
const normContent = content.replace(/[ \t]+/g, " ");
|
|
82
|
-
idx = normContent.indexOf(normSearch);
|
|
83
|
-
if (idx !== -1) {
|
|
84
|
-
const mapped = mapNormalizedPos(content, normContent, idx, normSearch.length);
|
|
85
|
-
if (mapped)
|
|
86
|
-
return [{ ...mapped, strategy: "whitespace_normalized" }];
|
|
87
|
-
}
|
|
88
|
-
// Strategy 4: indentation_flexible
|
|
89
|
-
const flexMatch = findLineBlock(contentLines, searchLines, (a, b) => a.trimStart() === b.trimStart());
|
|
90
|
-
if (flexMatch) {
|
|
91
|
-
return [{ ...lineBlockToRange(content, contentLines, flexMatch), strategy: "indentation_flexible" }];
|
|
92
|
-
}
|
|
93
|
-
// Strategy 5: escape_normalized
|
|
94
|
-
const escSearch = escapeNormalize(search);
|
|
95
|
-
if (escSearch !== search) {
|
|
96
|
-
idx = content.indexOf(escSearch);
|
|
97
|
-
if (idx !== -1) {
|
|
98
|
-
return [{ start: idx, end: idx + escSearch.length, strategy: "escape_normalized" }];
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// Strategy 6: trimmed_boundary
|
|
102
|
-
const sLines = search.split("\n");
|
|
103
|
-
if (sLines.length >= 3) {
|
|
104
|
-
const innerSearch = sLines.slice(1, -1).join("\n");
|
|
105
|
-
const firstTrimmed = sLines[0].trim();
|
|
106
|
-
const lastTrimmed = sLines[sLines.length - 1].trim();
|
|
107
|
-
for (let i = 0; i < contentLines.length; i++) {
|
|
108
|
-
if (contentLines[i].trim() === firstTrimmed) {
|
|
109
|
-
const innerStart = contentLines.slice(0, i + 1).join("\n").length + 1;
|
|
110
|
-
const innerIdx = content.indexOf(innerSearch, innerStart);
|
|
111
|
-
if (innerIdx !== -1) {
|
|
112
|
-
const afterInner = innerIdx + innerSearch.length;
|
|
113
|
-
const nextNewline = content.indexOf("\n", afterInner);
|
|
114
|
-
const lastLine = nextNewline === -1 ? content.slice(afterInner) : content.slice(afterInner, nextNewline);
|
|
115
|
-
if (lastLine.trim() === lastTrimmed || (nextNewline !== -1 && content.slice(afterInner + 1, content.indexOf("\n", afterInner + 1) === -1 ? undefined : content.indexOf("\n", afterInner + 1)).trim() === lastTrimmed)) {
|
|
116
|
-
const rangeStart = contentLines.slice(0, i).join("\n").length + (i > 0 ? 1 : 0);
|
|
117
|
-
// Find end after last line
|
|
118
|
-
let endI = i;
|
|
119
|
-
const needed = sLines.length;
|
|
120
|
-
if (i + needed <= contentLines.length) {
|
|
121
|
-
endI = i + needed;
|
|
122
|
-
const rangeEnd = contentLines.slice(0, endI).join("\n").length;
|
|
123
|
-
return [{ start: rangeStart, end: rangeEnd, strategy: "trimmed_boundary" }];
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// Strategy 7: unicode_normalized
|
|
131
|
-
const uniSearch = unicodeNormalize(search);
|
|
132
|
-
const uniContent = unicodeNormalize(content);
|
|
133
|
-
if (uniSearch !== search || uniContent !== content) {
|
|
134
|
-
idx = uniContent.indexOf(uniSearch);
|
|
135
|
-
if (idx !== -1) {
|
|
136
|
-
const mapped = mapNormalizedPos(content, uniContent, idx, uniSearch.length);
|
|
137
|
-
if (mapped)
|
|
138
|
-
return [{ ...mapped, strategy: "unicode_normalized" }];
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
// Strategy 8: block_anchor
|
|
142
|
-
if (sLines.length >= 3) {
|
|
143
|
-
const firstLine = sLines[0];
|
|
144
|
-
const lastLine = sLines[sLines.length - 1];
|
|
145
|
-
for (let i = 0; i < contentLines.length; i++) {
|
|
146
|
-
if (contentLines[i] === firstLine) {
|
|
147
|
-
for (let j = i + sLines.length - 1; j < Math.min(i + sLines.length + 2, contentLines.length); j++) {
|
|
148
|
-
if (contentLines[j] === lastLine) {
|
|
149
|
-
const blockLen = j - i + 1;
|
|
150
|
-
const middleLines = contentLines.slice(i + 1, j);
|
|
151
|
-
const searchMiddle = sLines.slice(1, -1);
|
|
152
|
-
const matchCount = searchMiddle.filter((sl) => middleLines.some((cl) => lineSimilarity(cl, sl) >= 0.7)).length;
|
|
153
|
-
const ratio = searchMiddle.length > 0 ? matchCount / searchMiddle.length : 1;
|
|
154
|
-
if (ratio >= 0.5 && blockLen <= sLines.length + 2) {
|
|
155
|
-
const rangeStart = contentLines.slice(0, i).join("\n").length + (i > 0 ? 1 : 0);
|
|
156
|
-
const rangeEnd = contentLines.slice(0, j + 1).join("\n").length;
|
|
157
|
-
return [{ start: rangeStart, end: rangeEnd, strategy: "block_anchor" }];
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
// Strategy 9: context_aware
|
|
165
|
-
if (sLines.length >= 2) {
|
|
166
|
-
for (let i = 0; i <= contentLines.length - sLines.length; i++) {
|
|
167
|
-
const candidateLines = contentLines.slice(i, i + sLines.length);
|
|
168
|
-
const similarities = sLines.map((sl, k) => lineSimilarity(sl, candidateLines[k]));
|
|
169
|
-
const goodLines = similarities.filter((s) => s >= 0.8).length;
|
|
170
|
-
if (goodLines / sLines.length >= 0.5) {
|
|
171
|
-
const rangeStart = contentLines.slice(0, i).join("\n").length + (i > 0 ? 1 : 0);
|
|
172
|
-
const rangeEnd = contentLines.slice(0, i + sLines.length).join("\n").length;
|
|
173
|
-
return [{ start: rangeStart, end: rangeEnd, strategy: "context_aware" }];
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return [];
|
|
178
|
-
}
|
|
179
|
-
function findLineBlock(contentLines, searchLines, compareFn) {
|
|
180
|
-
for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
|
|
181
|
-
let match = true;
|
|
182
|
-
for (let j = 0; j < searchLines.length; j++) {
|
|
183
|
-
if (!compareFn(contentLines[i + j], searchLines[j])) {
|
|
184
|
-
match = false;
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (match)
|
|
189
|
-
return { startIdx: i, endIdx: i + searchLines.length };
|
|
190
|
-
}
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
function lineBlockToRange(content, contentLines, block) {
|
|
194
|
-
const start = contentLines.slice(0, block.startIdx).join("\n").length + (block.startIdx > 0 ? 1 : 0);
|
|
195
|
-
const end = contentLines.slice(0, block.endIdx).join("\n").length;
|
|
196
|
-
return { start, end };
|
|
197
|
-
}
|
|
198
|
-
function mapNormalizedPos(original, normalized, normStart, normLen) {
|
|
199
|
-
// Simple character-length mapping (works for whitespace collapse and unicode transforms)
|
|
200
|
-
// Walk both strings in sync to find positional correspondence
|
|
201
|
-
let oi = 0;
|
|
202
|
-
let ni = 0;
|
|
203
|
-
let origStart = -1;
|
|
204
|
-
let origEnd = -1;
|
|
205
|
-
while (oi <= original.length && ni <= normalized.length) {
|
|
206
|
-
if (ni === normStart && origStart === -1)
|
|
207
|
-
origStart = oi;
|
|
208
|
-
if (ni === normStart + normLen) {
|
|
209
|
-
origEnd = oi;
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
if (ni >= normalized.length)
|
|
213
|
-
break;
|
|
214
|
-
if (oi >= original.length)
|
|
215
|
-
break;
|
|
216
|
-
ni++;
|
|
217
|
-
oi++;
|
|
218
|
-
// Skip extra chars in original that were collapsed
|
|
219
|
-
while (oi < original.length && ni < normalized.length && original[oi] !== normalized[ni]) {
|
|
220
|
-
oi++;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
if (origStart !== -1 && origEnd === -1)
|
|
224
|
-
origEnd = original.length;
|
|
225
|
-
if (origStart === -1)
|
|
226
|
-
return null;
|
|
227
|
-
return { start: origStart, end: origEnd };
|
|
228
|
-
}
|
|
229
|
-
function parseV4A(input) {
|
|
230
|
-
const lines = input.split("\n");
|
|
231
|
-
const ops = [];
|
|
232
|
-
let current = null;
|
|
233
|
-
let currentHunk = null;
|
|
234
|
-
let inPatch = false;
|
|
235
|
-
for (const line of lines) {
|
|
236
|
-
if (line.startsWith("*** Begin Patch")) {
|
|
237
|
-
inPatch = true;
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
if (line.startsWith("*** End Patch"))
|
|
241
|
-
break;
|
|
242
|
-
if (!inPatch)
|
|
243
|
-
continue;
|
|
244
|
-
if (line.startsWith("*** Add File: ")) {
|
|
245
|
-
if (current)
|
|
246
|
-
ops.push(current);
|
|
247
|
-
current = { type: "add", path: line.slice(14).trim(), hunks: [] };
|
|
248
|
-
currentHunk = { lines: [] };
|
|
249
|
-
current.hunks.push(currentHunk);
|
|
250
|
-
}
|
|
251
|
-
else if (line.startsWith("*** Update File: ")) {
|
|
252
|
-
if (current)
|
|
253
|
-
ops.push(current);
|
|
254
|
-
current = { type: "update", path: line.slice(17).trim(), hunks: [] };
|
|
255
|
-
currentHunk = null;
|
|
256
|
-
}
|
|
257
|
-
else if (line.startsWith("*** Delete File: ")) {
|
|
258
|
-
if (current)
|
|
259
|
-
ops.push(current);
|
|
260
|
-
current = { type: "delete", path: line.slice(17).trim(), hunks: [] };
|
|
261
|
-
currentHunk = null;
|
|
262
|
-
}
|
|
263
|
-
else if (line.startsWith("*** Move File: ")) {
|
|
264
|
-
if (current)
|
|
265
|
-
ops.push(current);
|
|
266
|
-
const parts = line.slice(15).trim().split(" -> ");
|
|
267
|
-
current = { type: "move", path: parts[0].trim(), newPath: parts[1]?.trim(), hunks: [] };
|
|
268
|
-
currentHunk = null;
|
|
269
|
-
}
|
|
270
|
-
else if (line.startsWith("@@ ") && current) {
|
|
271
|
-
currentHunk = { contextHint: line.slice(3).replace(/ @@$/, "").trim() || undefined, lines: [] };
|
|
272
|
-
current.hunks.push(currentHunk);
|
|
273
|
-
}
|
|
274
|
-
else if (currentHunk) {
|
|
275
|
-
if (line.startsWith("+")) {
|
|
276
|
-
currentHunk.lines.push({ prefix: "+", content: line.slice(1) });
|
|
277
|
-
}
|
|
278
|
-
else if (line.startsWith("-")) {
|
|
279
|
-
currentHunk.lines.push({ prefix: "-", content: line.slice(1) });
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
currentHunk.lines.push({ prefix: " ", content: line.startsWith(" ") ? line.slice(1) : line });
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (current)
|
|
287
|
-
ops.push(current);
|
|
288
|
-
return ops;
|
|
289
|
-
}
|
|
290
|
-
function parseSimpleFormat(input) {
|
|
291
|
-
const ops = [];
|
|
292
|
-
const blocks = input.split(/^(<<<< SEARCH)$/m);
|
|
293
|
-
if (blocks.length < 2)
|
|
294
|
-
return [];
|
|
295
|
-
// First element before first <<<< SEARCH is the file path
|
|
296
|
-
const pathLine = blocks[0].trim().split("\n").pop()?.trim();
|
|
297
|
-
if (!pathLine)
|
|
298
|
-
return [];
|
|
299
|
-
for (let i = 1; i < blocks.length; i += 2) {
|
|
300
|
-
const rest = blocks[i + 1] || "";
|
|
301
|
-
const sepIdx = rest.indexOf("\n====\n");
|
|
302
|
-
if (sepIdx === -1)
|
|
303
|
-
continue;
|
|
304
|
-
const search = rest.slice(0, sepIdx).replace(/^\n/, "");
|
|
305
|
-
const afterSep = rest.slice(sepIdx + 6);
|
|
306
|
-
const replaceEnd = afterSep.indexOf("\n>>>> REPLACE");
|
|
307
|
-
const replace = replaceEnd === -1 ? afterSep : afterSep.slice(0, replaceEnd);
|
|
308
|
-
ops.push({ path: pathLine, search, replace });
|
|
309
|
-
}
|
|
310
|
-
return ops;
|
|
311
|
-
}
|
|
312
|
-
// ── Main tool function ──────────────────────────────────────
|
|
313
|
-
function applyV4AHunk(content, hunk, replaceAll) {
|
|
314
|
-
const contextLines = hunk.lines.filter((l) => l.prefix === " ").map((l) => l.content);
|
|
315
|
-
const removeLines = hunk.lines.filter((l) => l.prefix === "-").map((l) => l.content);
|
|
316
|
-
const addLines = hunk.lines.filter((l) => l.prefix === "+").map((l) => l.content);
|
|
317
|
-
// Build the search text from context + removals (what should exist in file)
|
|
318
|
-
const searchParts = [];
|
|
319
|
-
let phase = "context";
|
|
320
|
-
for (const line of hunk.lines) {
|
|
321
|
-
if (line.prefix === " " || line.prefix === "-") {
|
|
322
|
-
searchParts.push(line.content);
|
|
323
|
-
}
|
|
324
|
-
// Skip additions in search
|
|
325
|
-
}
|
|
326
|
-
const searchText = searchParts.join("\n");
|
|
327
|
-
if (!searchText) {
|
|
328
|
-
// Pure addition — append at end or after context hint
|
|
329
|
-
return { content: content + "\n" + addLines.join("\n"), strategy: "exact" };
|
|
330
|
-
}
|
|
331
|
-
const matches = fuzzyFind(content, searchText, replaceAll);
|
|
332
|
-
if (matches.length === 0)
|
|
333
|
-
return null;
|
|
334
|
-
// Build replacement: context lines + additions (removals excluded)
|
|
335
|
-
const replaceParts = [];
|
|
336
|
-
for (const line of hunk.lines) {
|
|
337
|
-
if (line.prefix === " " || line.prefix === "+") {
|
|
338
|
-
replaceParts.push(line.content);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const replaceText = replaceParts.join("\n");
|
|
342
|
-
// Apply first match
|
|
343
|
-
const m = matches[0];
|
|
344
|
-
const newContent = content.slice(0, m.start) + replaceText + content.slice(m.end);
|
|
345
|
-
return { content: newContent, strategy: m.strategy };
|
|
346
|
-
}
|
|
347
|
-
export function createPatchTool(deps) {
|
|
348
|
-
return {
|
|
349
|
-
name: PATCH_TOOL_NAME,
|
|
350
|
-
label: "Patch",
|
|
351
|
-
description: "Apply edits to files using fuzzy matching. Supports V4A unified diff format " +
|
|
352
|
-
"(*** Begin Patch / *** End Patch) for multi-file operations, and simple search/replace " +
|
|
353
|
-
"blocks. The fuzzy matcher handles whitespace, indentation, unicode, and escape drift " +
|
|
354
|
-
"— LLM output with minor formatting differences will still match correctly.",
|
|
355
|
-
parameters: PATCH_TOOL_SCHEMA,
|
|
356
|
-
execute: async (_toolCallId, params) => {
|
|
357
|
-
const replaceAll = params.replaceAll ?? false;
|
|
358
|
-
const result = { filesModified: [], filesAdded: [], filesDeleted: [], strategies: {}, errors: [] };
|
|
359
|
-
// Detect format: V4A or simple
|
|
360
|
-
const isV4A = params.input.includes("*** Begin Patch");
|
|
361
|
-
const isSimple = params.input.includes("<<<< SEARCH");
|
|
362
|
-
if (isV4A) {
|
|
363
|
-
const ops = parseV4A(params.input);
|
|
364
|
-
if (ops.length === 0) {
|
|
365
|
-
return {
|
|
366
|
-
content: [{ type: "text", text: "Error: No valid V4A operations found. Ensure *** Begin Patch / *** End Patch markers are present." }],
|
|
367
|
-
details: { type: "patch", error: "parse_failed" },
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
for (const op of ops) {
|
|
371
|
-
const resolved = deps.resolvePath(op.path);
|
|
372
|
-
try {
|
|
373
|
-
switch (op.type) {
|
|
374
|
-
case "add": {
|
|
375
|
-
const addContent = op.hunks.flatMap((h) => h.lines.filter((l) => l.prefix === "+").map((l) => l.content)).join("\n");
|
|
376
|
-
await deps.writeFile(resolved, addContent);
|
|
377
|
-
result.filesAdded.push(op.path);
|
|
378
|
-
result.strategies[op.path] = "exact";
|
|
379
|
-
break;
|
|
380
|
-
}
|
|
381
|
-
case "delete": {
|
|
382
|
-
await deps.deleteFile(resolved);
|
|
383
|
-
result.filesDeleted.push(op.path);
|
|
384
|
-
break;
|
|
385
|
-
}
|
|
386
|
-
case "move": {
|
|
387
|
-
const existing = await deps.readFile(resolved);
|
|
388
|
-
if (op.newPath) {
|
|
389
|
-
const dest = deps.resolvePath(op.newPath);
|
|
390
|
-
let content = existing;
|
|
391
|
-
for (const hunk of op.hunks) {
|
|
392
|
-
const hr = applyV4AHunk(content, hunk, replaceAll);
|
|
393
|
-
if (hr) {
|
|
394
|
-
content = hr.content;
|
|
395
|
-
result.strategies[op.path] = hr.strategy;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
await deps.writeFile(dest, content);
|
|
399
|
-
await deps.deleteFile(resolved);
|
|
400
|
-
result.filesModified.push(`${op.path} → ${op.newPath}`);
|
|
401
|
-
}
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
case "update": {
|
|
405
|
-
let content = await deps.readFile(resolved);
|
|
406
|
-
let lastStrategy = "exact";
|
|
407
|
-
let allSuccess = true;
|
|
408
|
-
for (const hunk of op.hunks) {
|
|
409
|
-
const hr = applyV4AHunk(content, hunk, replaceAll);
|
|
410
|
-
if (hr) {
|
|
411
|
-
content = hr.content;
|
|
412
|
-
lastStrategy = hr.strategy;
|
|
413
|
-
}
|
|
414
|
-
else {
|
|
415
|
-
allSuccess = false;
|
|
416
|
-
// Build readable error
|
|
417
|
-
const searchSnippet = hunk.lines
|
|
418
|
-
.filter((l) => l.prefix === " " || l.prefix === "-")
|
|
419
|
-
.map((l) => l.content)
|
|
420
|
-
.slice(0, 5)
|
|
421
|
-
.join("\n");
|
|
422
|
-
result.errors.push(`${op.path}: hunk not matched. Search begins with:\n${searchSnippet}`);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
if (allSuccess || content !== await deps.readFile(resolved)) {
|
|
426
|
-
await deps.writeFile(resolved, content);
|
|
427
|
-
result.filesModified.push(op.path);
|
|
428
|
-
result.strategies[op.path] = lastStrategy;
|
|
429
|
-
}
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
catch (err) {
|
|
435
|
-
result.errors.push(`${op.type} ${op.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
else if (isSimple) {
|
|
440
|
-
const ops = parseSimpleFormat(params.input);
|
|
441
|
-
if (ops.length === 0) {
|
|
442
|
-
return {
|
|
443
|
-
content: [{ type: "text", text: "Error: Invalid search/replace format. Use <<<< SEARCH / ==== / >>>> REPLACE blocks." }],
|
|
444
|
-
details: { type: "patch", error: "parse_failed" },
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
for (const op of ops) {
|
|
448
|
-
const resolved = deps.resolvePath(op.path);
|
|
449
|
-
try {
|
|
450
|
-
let content = await deps.readFile(resolved);
|
|
451
|
-
const matches = fuzzyFind(content, op.search, replaceAll);
|
|
452
|
-
if (matches.length === 0) {
|
|
453
|
-
result.errors.push(`${op.path}: search text not matched (tried all 9 strategies)`);
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
// Apply in reverse order to preserve positions
|
|
457
|
-
const sorted = [...matches].sort((a, b) => b.start - a.start);
|
|
458
|
-
for (const m of sorted) {
|
|
459
|
-
content = content.slice(0, m.start) + op.replace + content.slice(m.end);
|
|
460
|
-
}
|
|
461
|
-
await deps.writeFile(resolved, content);
|
|
462
|
-
result.filesModified.push(op.path);
|
|
463
|
-
result.strategies[op.path] = matches[0].strategy;
|
|
464
|
-
}
|
|
465
|
-
catch (err) {
|
|
466
|
-
result.errors.push(`${op.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
else {
|
|
471
|
-
// Try parsing as a plain old/new pair in V4A-like format (backwards compat with apply_patch)
|
|
472
|
-
const ops = parseV4A("*** Begin Patch\n" + params.input + "\n*** End Patch");
|
|
473
|
-
if (ops.length > 0) {
|
|
474
|
-
// Re-run through V4A path
|
|
475
|
-
return createPatchTool(deps).execute(_toolCallId, { input: "*** Begin Patch\n" + params.input + "\n*** End Patch", replaceAll });
|
|
476
|
-
}
|
|
477
|
-
return {
|
|
478
|
-
content: [{ type: "text", text: "Error: Unrecognized patch format. Use V4A (*** Begin Patch) or search/replace (<<<< SEARCH / ==== / >>>> REPLACE)." }],
|
|
479
|
-
details: { type: "patch", error: "unknown_format" },
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
// Format output
|
|
483
|
-
const parts = [];
|
|
484
|
-
if (result.filesAdded.length)
|
|
485
|
-
parts.push(`Added: ${result.filesAdded.join(", ")}`);
|
|
486
|
-
if (result.filesModified.length) {
|
|
487
|
-
const strategyInfo = result.filesModified
|
|
488
|
-
.map((f) => `${f} (${result.strategies[f] || "exact"})`)
|
|
489
|
-
.join(", ");
|
|
490
|
-
parts.push(`Modified: ${strategyInfo}`);
|
|
491
|
-
}
|
|
492
|
-
if (result.filesDeleted.length)
|
|
493
|
-
parts.push(`Deleted: ${result.filesDeleted.join(", ")}`);
|
|
494
|
-
if (result.errors.length)
|
|
495
|
-
parts.push(`\nErrors:\n${result.errors.join("\n")}`);
|
|
496
|
-
const success = result.errors.length === 0;
|
|
497
|
-
return {
|
|
498
|
-
content: [{ type: "text", text: parts.join("\n") || "No changes applied." }],
|
|
499
|
-
details: { type: "patch", ...result, success },
|
|
500
|
-
};
|
|
501
|
-
},
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
// Export the fuzzy engine for unit testing
|
|
505
|
-
export { fuzzyFind };
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// PDF Tool — analyze PDF documents via relay/backend pipeline.
|
|
3
|
-
// Category: document
|
|
4
|
-
// ============================================================
|
|
5
|
-
export const PDF_TOOL_NAME = "pdf";
|
|
6
|
-
export const PDF_TOOL_SCHEMA = {
|
|
7
|
-
type: "object",
|
|
8
|
-
properties: {
|
|
9
|
-
prompt: {
|
|
10
|
-
type: "string",
|
|
11
|
-
description: "Describe what to analyze or extract from the PDF(s).",
|
|
12
|
-
},
|
|
13
|
-
pdf: {
|
|
14
|
-
type: "string",
|
|
15
|
-
description: "Single PDF path or URL.",
|
|
16
|
-
},
|
|
17
|
-
pdfs: {
|
|
18
|
-
type: "array",
|
|
19
|
-
items: { type: "string" },
|
|
20
|
-
description: "Multiple PDF paths or URLs (up to 10).",
|
|
21
|
-
},
|
|
22
|
-
pages: {
|
|
23
|
-
type: "string",
|
|
24
|
-
description: 'Page range to process, e.g. "1-5", "1,3,5-7". Defaults to all pages.',
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
required: [],
|
|
28
|
-
};
|
|
29
|
-
const MAX_PDF_COUNT = 10;
|
|
30
|
-
export function createPdfTool(deps) {
|
|
31
|
-
return {
|
|
32
|
-
name: PDF_TOOL_NAME,
|
|
33
|
-
label: "PDF",
|
|
34
|
-
description: "Analyze one or more PDF documents. Use `pdf` for a single path/URL, or `pdfs` for multiple (up to 10). " +
|
|
35
|
-
"Provide a `prompt` describing what to analyze. Use `pages` to limit which pages to process.",
|
|
36
|
-
parameters: PDF_TOOL_SCHEMA,
|
|
37
|
-
execute: async (_toolCallId, params) => {
|
|
38
|
-
// Normalize sources
|
|
39
|
-
const sources = [];
|
|
40
|
-
if (params.pdf)
|
|
41
|
-
sources.push(params.pdf);
|
|
42
|
-
if (params.pdfs)
|
|
43
|
-
sources.push(...params.pdfs);
|
|
44
|
-
// Deduplicate
|
|
45
|
-
const unique = [...new Set(sources)];
|
|
46
|
-
if (unique.length === 0) {
|
|
47
|
-
return { content: [{ type: "text", text: "Error: provide at least one PDF via `pdf` or `pdfs`." }] };
|
|
48
|
-
}
|
|
49
|
-
if (unique.length > MAX_PDF_COUNT) {
|
|
50
|
-
return { content: [{ type: "text", text: `Error: maximum ${MAX_PDF_COUNT} PDFs per call.` }] };
|
|
51
|
-
}
|
|
52
|
-
const allTexts = [];
|
|
53
|
-
const allImages = [];
|
|
54
|
-
const errors = [];
|
|
55
|
-
for (const src of unique) {
|
|
56
|
-
try {
|
|
57
|
-
const result = await deps.extractPdf(src, { pages: params.pages });
|
|
58
|
-
allTexts.push(result.text);
|
|
59
|
-
if (result.pageImages) {
|
|
60
|
-
for (const img of result.pageImages) {
|
|
61
|
-
allImages.push({ base64: img.base64, mimeType: img.mimeType });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
catch (err) {
|
|
66
|
-
errors.push(`${src}: ${err instanceof Error ? err.message : String(err)}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (allTexts.length === 0) {
|
|
70
|
-
return { content: [{ type: "text", text: `Failed to extract any PDFs: ${errors.join("; ")}` }] };
|
|
71
|
-
}
|
|
72
|
-
const combinedText = allTexts.join("\n\n---\n\n");
|
|
73
|
-
// If LLM analysis is available and prompt given, use it
|
|
74
|
-
if (deps.analyzePdfContent && params.prompt) {
|
|
75
|
-
const analysis = await deps.analyzePdfContent(params.prompt, { text: combinedText, images: allImages });
|
|
76
|
-
return {
|
|
77
|
-
content: [{ type: "text", text: analysis.text }],
|
|
78
|
-
details: { type: "pdf", model: analysis.model, provider: analysis.provider, pdfCount: unique.length, errors },
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
// Otherwise return raw extracted text
|
|
82
|
-
return {
|
|
83
|
-
content: [{ type: "text", text: combinedText }],
|
|
84
|
-
details: { type: "pdf", pdfCount: unique.length, errors },
|
|
85
|
-
};
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|