@burtson-labs/agent-core 1.6.16 → 1.6.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/activation.js +16 -8
- package/dist/mcp/activation.js.map +1 -1
- package/dist/mcp/clientPool.js +40 -22
- package/dist/mcp/clientPool.js.map +1 -1
- package/dist/mcp/server.js +16 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/toolAdapter.js +21 -11
- package/dist/mcp/toolAdapter.js.map +1 -1
- package/dist/providers/deterministic-provider.d.ts +1 -1
- package/dist/providers/deterministic-provider.d.ts.map +1 -1
- package/dist/runtime/AgentRuntime.d.ts +2 -2
- package/dist/runtime/AgentRuntime.d.ts.map +1 -1
- package/dist/security/secretPatterns.js +4 -2
- package/dist/security/secretPatterns.js.map +1 -1
- package/dist/telemetry/otlpExporter.d.ts +69 -0
- package/dist/telemetry/otlpExporter.d.ts.map +1 -0
- package/dist/telemetry/otlpExporter.js +321 -0
- package/dist/telemetry/otlpExporter.js.map +1 -0
- package/dist/tools/ask-user-tool.js +8 -4
- package/dist/tools/ask-user-tool.js.map +1 -1
- package/dist/tools/compactMessages.js +6 -3
- package/dist/tools/compactMessages.js.map +1 -1
- package/dist/tools/core-tools.js +151 -81
- package/dist/tools/core-tools.js.map +1 -1
- package/dist/tools/git-tools.js +22 -11
- package/dist/tools/git-tools.js.map +1 -1
- package/dist/tools/language-adapters.d.ts +1 -1
- package/dist/tools/language-adapters.d.ts.map +1 -1
- package/dist/tools/language-adapters.js +36 -18
- package/dist/tools/language-adapters.js.map +1 -1
- package/dist/tools/loop/finalAnswerNudges.js +12 -6
- package/dist/tools/loop/finalAnswerNudges.js.map +1 -1
- package/dist/tools/loop/goalAnchor.d.ts.map +1 -1
- package/dist/tools/loop/goalAnchor.js +2 -1
- package/dist/tools/loop/goalAnchor.js.map +1 -1
- package/dist/tools/loop/llmStream.js +11 -8
- package/dist/tools/loop/llmStream.js.map +1 -1
- package/dist/tools/loop/loopShared.d.ts +20 -0
- package/dist/tools/loop/loopShared.d.ts.map +1 -0
- package/dist/tools/loop/loopShared.js +105 -0
- package/dist/tools/loop/loopShared.js.map +1 -0
- package/dist/tools/loop/parallelExecute.d.ts +1 -1
- package/dist/tools/loop/parallelExecute.js +2 -1
- package/dist/tools/loop/parallelExecute.js.map +1 -1
- package/dist/tools/loop/singleToolExecute.js +8 -4
- package/dist/tools/loop/singleToolExecute.js.map +1 -1
- package/dist/tools/loop/turnSetup.js +9 -6
- package/dist/tools/loop/turnSetup.js.map +1 -1
- package/dist/tools/ocr.d.ts.map +1 -1
- package/dist/tools/ocr.js +7 -5
- package/dist/tools/ocr.js.map +1 -1
- package/dist/tools/post-edit-checks.js +25 -13
- package/dist/tools/post-edit-checks.js.map +1 -1
- package/dist/tools/skill-loader.d.ts +1 -1
- package/dist/tools/skill-loader.d.ts.map +1 -1
- package/dist/tools/skill-loader.js +14 -7
- package/dist/tools/skill-loader.js.map +1 -1
- package/dist/tools/skill-registry.js +2 -1
- package/dist/tools/skill-registry.js.map +1 -1
- package/dist/tools/skills/mail-search-skill.js +16 -9
- package/dist/tools/skills/mail-search-skill.js.map +1 -1
- package/dist/tools/skills/plan-skill.js +4 -2
- package/dist/tools/skills/plan-skill.js.map +1 -1
- package/dist/tools/skills/semantic-search-skill.js +12 -6
- package/dist/tools/skills/semantic-search-skill.js.map +1 -1
- package/dist/tools/skills/test-gen-skill.js +8 -4
- package/dist/tools/skills/test-gen-skill.js.map +1 -1
- package/dist/tools/tool-registry.d.ts +17 -0
- package/dist/tools/tool-registry.d.ts.map +1 -1
- package/dist/tools/tool-registry.js +110 -30
- package/dist/tools/tool-registry.js.map +1 -1
- package/dist/tools/tool-use-loop.d.ts +16 -8
- package/dist/tools/tool-use-loop.d.ts.map +1 -1
- package/dist/tools/tool-use-loop.js +144 -160
- package/dist/tools/tool-use-loop.js.map +1 -1
- package/dist/tools/tool-use-parser.d.ts +33 -0
- package/dist/tools/tool-use-parser.d.ts.map +1 -1
- package/dist/tools/tool-use-parser.js +105 -28
- package/dist/tools/tool-use-parser.js.map +1 -1
- package/dist/tools/toolAvailabilityDetector.d.ts +0 -24
- package/dist/tools/toolAvailabilityDetector.d.ts.map +1 -1
- package/dist/tools/toolAvailabilityDetector.js +26 -12
- package/dist/tools/toolAvailabilityDetector.js.map +1 -1
- package/dist/tools/unified-patch.js +16 -8
- package/dist/tools/unified-patch.js.map +1 -1
- package/dist/utils/event-emitter.d.ts +1 -1
- package/dist/utils/event-emitter.d.ts.map +1 -1
- package/package.json +20 -1
package/dist/tools/core-tools.js
CHANGED
|
@@ -51,9 +51,10 @@ function validatePostWrite(absolutePath, content) {
|
|
|
51
51
|
// run it — there's no perf reason to skip. We tolerate the
|
|
52
52
|
// common BOM + trim leading whitespace because some hosts
|
|
53
53
|
// prepend a UTF-8 BOM to written files.
|
|
54
|
-
const trimmed = content.replace(
|
|
55
|
-
if (trimmed.length === 0)
|
|
56
|
-
return null;
|
|
54
|
+
const trimmed = content.replace(/^\uFEFF/, '').trimStart();
|
|
55
|
+
if (trimmed.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
} // empty file is valid JSON-zero
|
|
57
58
|
try {
|
|
58
59
|
JSON.parse(content);
|
|
59
60
|
return null;
|
|
@@ -68,17 +69,21 @@ function validatePostWrite(absolutePath, content) {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
function isAbsolutePath(p) {
|
|
71
|
-
if (p.startsWith('/') || p.startsWith('~'))
|
|
72
|
+
if (p.startsWith('/') || p.startsWith('~')) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (/^[A-Za-z]:[\\/]/.test(p)) {
|
|
72
76
|
return true;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
} // C:\foo or C:/foo
|
|
78
|
+
if (p.startsWith('\\\\')) {
|
|
79
|
+
return true;
|
|
80
|
+
} // UNC \\server\share
|
|
77
81
|
return false;
|
|
78
82
|
}
|
|
79
83
|
function truncate(text, max, label) {
|
|
80
|
-
if (text.length <= max)
|
|
84
|
+
if (text.length <= max) {
|
|
81
85
|
return text;
|
|
86
|
+
}
|
|
82
87
|
return `${text.slice(0, max)}\n\n[${label}: truncated — ${text.length - max} chars omitted]`;
|
|
83
88
|
}
|
|
84
89
|
function stableContentHash(text) {
|
|
@@ -126,8 +131,9 @@ const readFileTool = {
|
|
|
126
131
|
],
|
|
127
132
|
async execute(params, ctx) {
|
|
128
133
|
const relPath = params.path?.trim();
|
|
129
|
-
if (!relPath)
|
|
134
|
+
if (!relPath) {
|
|
130
135
|
return { output: 'Error: path parameter is required', isError: true };
|
|
136
|
+
}
|
|
131
137
|
// Extension check first so we don't burn bytes decoding a binary blob.
|
|
132
138
|
const lastDot = relPath.lastIndexOf('.');
|
|
133
139
|
const ext = lastDot >= 0 ? relPath.slice(lastDot).toLowerCase() : '';
|
|
@@ -136,7 +142,7 @@ const readFileTool = {
|
|
|
136
142
|
}
|
|
137
143
|
// A path starting with "~" is home-relative — let the host's tool context
|
|
138
144
|
// expand it rather than prepending the workspace root (which would produce
|
|
139
|
-
// nonsense like /Users/
|
|
145
|
+
// nonsense like /Users/name/~/Desktop/file.md).
|
|
140
146
|
const absPath = isAbsolutePath(relPath)
|
|
141
147
|
? relPath
|
|
142
148
|
: `${ctx.workspaceRoot}/${relPath}`;
|
|
@@ -175,7 +181,7 @@ const readFileTool = {
|
|
|
175
181
|
// models (4B-class) routinely copy-paste those prefix bytes
|
|
176
182
|
// into apply_edit `find` strings, where they never match the
|
|
177
183
|
// real file content and the edit silently no-ops. Observed
|
|
178
|
-
// 2026-05-01 on a
|
|
184
|
+
// 2026-05-01 on a React/TS sandbox with gemma4:e4b: model
|
|
179
185
|
// emitted `Find: " 10 │ <link href=..."` and the loop
|
|
180
186
|
// terminated with no edit landed. The header note + the
|
|
181
187
|
// explicit reminder in apply_edit's `find` parameter
|
|
@@ -232,10 +238,12 @@ const writeFileTool = {
|
|
|
232
238
|
async execute(params, ctx) {
|
|
233
239
|
const relPath = params.path?.trim();
|
|
234
240
|
const content = params.content;
|
|
235
|
-
if (!relPath)
|
|
241
|
+
if (!relPath) {
|
|
236
242
|
return { output: 'Error: path parameter is required', isError: true };
|
|
237
|
-
|
|
243
|
+
}
|
|
244
|
+
if (content === undefined || content === null) {
|
|
238
245
|
return { output: 'Error: content parameter is required', isError: true };
|
|
246
|
+
}
|
|
239
247
|
// Same rule as read_file: a "~" path is home-relative, not workspace-
|
|
240
248
|
// relative. Leave the "~" for the host context (CliToolExecutionContext
|
|
241
249
|
// expands it via os.homedir) rather than creating a literal "~" dir.
|
|
@@ -330,8 +338,9 @@ const deleteFileTool = {
|
|
|
330
338
|
],
|
|
331
339
|
async execute(params, ctx) {
|
|
332
340
|
const relPath = params.path?.trim();
|
|
333
|
-
if (!relPath)
|
|
341
|
+
if (!relPath) {
|
|
334
342
|
return { output: 'Error: path parameter is required', isError: true };
|
|
343
|
+
}
|
|
335
344
|
const absPath = isAbsolutePath(relPath)
|
|
336
345
|
? relPath
|
|
337
346
|
: `${ctx.workspaceRoot}/${relPath}`;
|
|
@@ -408,16 +417,21 @@ const applyEditTool = {
|
|
|
408
417
|
// the data we needed.
|
|
409
418
|
const find = params.find ?? params.old_text ?? params.old_string;
|
|
410
419
|
const replace = params.replace ?? params.new_text ?? params.new_string;
|
|
411
|
-
if (!relPath)
|
|
420
|
+
if (!relPath) {
|
|
412
421
|
return { output: 'Error: path parameter is required', isError: true };
|
|
413
|
-
|
|
422
|
+
}
|
|
423
|
+
if (find === undefined || find === null) {
|
|
414
424
|
return { output: 'Error: find parameter is required (also accepts old_text, old_string)', isError: true };
|
|
415
|
-
|
|
425
|
+
}
|
|
426
|
+
if (replace === undefined || replace === null) {
|
|
416
427
|
return { output: 'Error: replace parameter is required (also accepts new_text, new_string)', isError: true };
|
|
417
|
-
|
|
428
|
+
}
|
|
429
|
+
if (find === '') {
|
|
418
430
|
return { output: 'Error: find parameter must not be empty — use write_file to create a new file', isError: true };
|
|
419
|
-
|
|
431
|
+
}
|
|
432
|
+
if (find === replace) {
|
|
420
433
|
return { output: 'Error: find and replace are identical — no edit to apply', isError: true };
|
|
434
|
+
}
|
|
421
435
|
// Scratchpad-placeholder detector. Small models occasionally dump their
|
|
422
436
|
// own internal reasoning into `replace` as a bracketed "token" where
|
|
423
437
|
// code should go, e.g.
|
|
@@ -545,13 +559,15 @@ const applyEditTool = {
|
|
|
545
559
|
let scanIdx = 0;
|
|
546
560
|
while (true) {
|
|
547
561
|
const idx = before.indexOf(find, scanIdx);
|
|
548
|
-
if (idx === -1)
|
|
562
|
+
if (idx === -1) {
|
|
549
563
|
break;
|
|
564
|
+
}
|
|
550
565
|
const lineNum = before.slice(0, idx).split('\n').length;
|
|
551
566
|
matchPositions.push({ lineNum, charIdx: idx });
|
|
552
567
|
scanIdx = idx + find.length;
|
|
553
|
-
if (matchPositions.length >= 32)
|
|
568
|
+
if (matchPositions.length >= 32) {
|
|
554
569
|
break;
|
|
570
|
+
}
|
|
555
571
|
}
|
|
556
572
|
if (Number.isFinite(nearLine) && matchPositions.length > 0) {
|
|
557
573
|
// Pick the candidate whose start line is closest to near_line.
|
|
@@ -607,21 +623,27 @@ const applyEditTool = {
|
|
|
607
623
|
// match could have a different indent) and on edits where the model
|
|
608
624
|
// already supplied absolute indent on the first line.
|
|
609
625
|
const finalReplace = (() => {
|
|
610
|
-
if (replaceAll)
|
|
626
|
+
if (replaceAll) {
|
|
611
627
|
return replace;
|
|
612
|
-
|
|
628
|
+
}
|
|
629
|
+
if (find.includes('\n')) {
|
|
613
630
|
return replace;
|
|
614
|
-
|
|
631
|
+
}
|
|
632
|
+
if (!replace.includes('\n')) {
|
|
615
633
|
return replace;
|
|
616
|
-
|
|
634
|
+
}
|
|
635
|
+
if (/^\s/.test(replace)) {
|
|
617
636
|
return replace;
|
|
637
|
+
}
|
|
618
638
|
const matchIndex = before.indexOf(find);
|
|
619
|
-
if (matchIndex === -1)
|
|
639
|
+
if (matchIndex === -1) {
|
|
620
640
|
return replace;
|
|
641
|
+
}
|
|
621
642
|
const lineStart = before.lastIndexOf('\n', matchIndex - 1) + 1;
|
|
622
643
|
const indent = before.slice(lineStart, matchIndex);
|
|
623
|
-
if (indent.length === 0 || !/^[ \t]+$/.test(indent))
|
|
644
|
+
if (indent.length === 0 || !/^[ \t]+$/.test(indent)) {
|
|
624
645
|
return replace;
|
|
646
|
+
}
|
|
625
647
|
const lines = replace.split('\n');
|
|
626
648
|
return lines
|
|
627
649
|
.map((line, i) => (i === 0 || line.length === 0 ? line : indent + line))
|
|
@@ -647,10 +669,12 @@ const applyEditTool = {
|
|
|
647
669
|
spliceReplace = finalReplace
|
|
648
670
|
.split('\n')
|
|
649
671
|
.map((line) => {
|
|
650
|
-
if (line.length === 0)
|
|
672
|
+
if (line.length === 0) {
|
|
651
673
|
return line;
|
|
652
|
-
|
|
674
|
+
}
|
|
675
|
+
if (delta > 0) {
|
|
653
676
|
return ' '.repeat(delta) + line;
|
|
677
|
+
}
|
|
654
678
|
const leading = (line.match(/^[ \t]*/) ?? [''])[0].length;
|
|
655
679
|
return line.slice(Math.min(-delta, leading));
|
|
656
680
|
})
|
|
@@ -762,10 +786,12 @@ const replaceRangeTool = {
|
|
|
762
786
|
async execute(params, ctx) {
|
|
763
787
|
const relPath = params.path?.trim();
|
|
764
788
|
const content = params.content ?? params.replace ?? params.new_text;
|
|
765
|
-
if (!relPath)
|
|
789
|
+
if (!relPath) {
|
|
766
790
|
return { output: 'Error: path parameter is required', isError: true };
|
|
767
|
-
|
|
791
|
+
}
|
|
792
|
+
if (content === undefined || content === null) {
|
|
768
793
|
return { output: 'Error: content parameter is required', isError: true };
|
|
794
|
+
}
|
|
769
795
|
const parsedStart = parseInt(params.start_line ?? params.start ?? params.from_line ?? '', 10);
|
|
770
796
|
const parsedEnd = params.end_line !== undefined && params.end_line !== null && params.end_line !== ''
|
|
771
797
|
? parseInt(params.end_line, 10)
|
|
@@ -813,7 +839,7 @@ const replaceRangeTool = {
|
|
|
813
839
|
// into a replace_range(43-50) call, hashes diverge (because they
|
|
814
840
|
// cover different bytes), edit rejected, model re-reads, picks
|
|
815
841
|
// up a still-wrong hash from the wider read, retries — repeat
|
|
816
|
-
// indefinitely. Captured 2026-05-26
|
|
842
|
+
// indefinitely. Captured 2026-05-26 real CLI session: 3-5
|
|
817
843
|
// iterations spinning on a single 8-line replacement.
|
|
818
844
|
//
|
|
819
845
|
// The hash was always weaker safety than the read-tracking guard
|
|
@@ -921,8 +947,9 @@ const applyPatchTool = {
|
|
|
921
947
|
],
|
|
922
948
|
async execute(params, ctx) {
|
|
923
949
|
const raw = (params.patch ?? params.input ?? '').trim();
|
|
924
|
-
if (!raw)
|
|
950
|
+
if (!raw) {
|
|
925
951
|
return { output: 'Error: patch parameter is required', isError: true };
|
|
952
|
+
}
|
|
926
953
|
// Auto-detect format. Unified-diff patches start with one of the
|
|
927
954
|
// standard headers (`---`, `+++`, `diff `, `@@`); the Codex format
|
|
928
955
|
// starts with `*** Begin Patch`. When neither pattern matches we
|
|
@@ -959,29 +986,33 @@ const applyPatchTool = {
|
|
|
959
986
|
const addMatch = /^\*\*\* Add File:\s+(.+?)\s*$/.exec(line);
|
|
960
987
|
const deleteMatch = /^\*\*\* Delete File:\s+(.+?)\s*$/.exec(line);
|
|
961
988
|
if (updateMatch) {
|
|
962
|
-
if (current)
|
|
989
|
+
if (current) {
|
|
963
990
|
actions.push(current);
|
|
991
|
+
}
|
|
964
992
|
current = { kind: 'update', path: updateMatch[1], hunks: [] };
|
|
965
993
|
currentHunk = null;
|
|
966
994
|
continue;
|
|
967
995
|
}
|
|
968
996
|
if (addMatch) {
|
|
969
|
-
if (current)
|
|
997
|
+
if (current) {
|
|
970
998
|
actions.push(current);
|
|
999
|
+
}
|
|
971
1000
|
current = { kind: 'add', path: addMatch[1], lines: [] };
|
|
972
1001
|
currentHunk = null;
|
|
973
1002
|
continue;
|
|
974
1003
|
}
|
|
975
1004
|
if (deleteMatch) {
|
|
976
|
-
if (current)
|
|
1005
|
+
if (current) {
|
|
977
1006
|
actions.push(current);
|
|
1007
|
+
}
|
|
978
1008
|
actions.push({ kind: 'delete', path: deleteMatch[1] });
|
|
979
1009
|
current = null;
|
|
980
1010
|
currentHunk = null;
|
|
981
1011
|
continue;
|
|
982
1012
|
}
|
|
983
|
-
if (!current)
|
|
1013
|
+
if (!current) {
|
|
984
1014
|
continue;
|
|
1015
|
+
}
|
|
985
1016
|
if (current.kind === 'add') {
|
|
986
1017
|
// Add file: every line should start with `+ ` (or be empty).
|
|
987
1018
|
if (line.startsWith('+')) {
|
|
@@ -1019,8 +1050,9 @@ const applyPatchTool = {
|
|
|
1019
1050
|
continue;
|
|
1020
1051
|
}
|
|
1021
1052
|
}
|
|
1022
|
-
if (current)
|
|
1053
|
+
if (current) {
|
|
1023
1054
|
actions.push(current);
|
|
1055
|
+
}
|
|
1024
1056
|
if (actions.length === 0) {
|
|
1025
1057
|
return { output: 'apply_patch rejected: envelope contained no action blocks. Use `*** Update File:`, `*** Add File:`, or `*** Delete File:` headers.', isError: true };
|
|
1026
1058
|
}
|
|
@@ -1249,8 +1281,9 @@ exports.applyPatchTool = applyPatchTool;
|
|
|
1249
1281
|
*/
|
|
1250
1282
|
function maybeParseJsonArrayArgs(argsString) {
|
|
1251
1283
|
const trimmed = argsString.trim();
|
|
1252
|
-
if (!trimmed.startsWith('[') || !trimmed.endsWith(']'))
|
|
1284
|
+
if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) {
|
|
1253
1285
|
return null;
|
|
1286
|
+
}
|
|
1254
1287
|
try {
|
|
1255
1288
|
const parsed = JSON.parse(trimmed);
|
|
1256
1289
|
if (Array.isArray(parsed) && parsed.every((v) => typeof v === 'string')) {
|
|
@@ -1264,11 +1297,13 @@ function maybeParseJsonArrayArgs(argsString) {
|
|
|
1264
1297
|
}
|
|
1265
1298
|
function introducedNewErrors(before, after) {
|
|
1266
1299
|
const afterText = after ?? '';
|
|
1267
|
-
if (!afterText.trim())
|
|
1300
|
+
if (!afterText.trim()) {
|
|
1268
1301
|
return false;
|
|
1302
|
+
}
|
|
1269
1303
|
const beforeText = before ?? '';
|
|
1270
|
-
if (!beforeText.trim())
|
|
1271
|
-
return true;
|
|
1304
|
+
if (!beforeText.trim()) {
|
|
1305
|
+
return true;
|
|
1306
|
+
} // before was clean, after isn't — definitely introduced.
|
|
1272
1307
|
// Strip position-bearing tokens that change with file content shifts
|
|
1273
1308
|
// even when the underlying error is unchanged. Without this, renaming
|
|
1274
1309
|
// "foo" → "foo-renamed" in a file that already had a JSON parse error
|
|
@@ -1290,8 +1325,9 @@ function introducedNewErrors(before, after) {
|
|
|
1290
1325
|
const beforeSet = normalize(beforeText);
|
|
1291
1326
|
const afterSet = normalize(afterText);
|
|
1292
1327
|
for (const line of afterSet) {
|
|
1293
|
-
if (!beforeSet.has(line))
|
|
1328
|
+
if (!beforeSet.has(line)) {
|
|
1294
1329
|
return true;
|
|
1330
|
+
}
|
|
1295
1331
|
}
|
|
1296
1332
|
return false;
|
|
1297
1333
|
}
|
|
@@ -1393,14 +1429,17 @@ async function executeUnifiedDiffPatch(patchText, pathOverride, ctx) {
|
|
|
1393
1429
|
*/
|
|
1394
1430
|
function findIndentationHint(source, find) {
|
|
1395
1431
|
const firstLine = find.split('\n', 1)[0].trim();
|
|
1396
|
-
if (firstLine.length < 4)
|
|
1432
|
+
if (firstLine.length < 4) {
|
|
1397
1433
|
return '';
|
|
1434
|
+
}
|
|
1398
1435
|
const candidateLine = source.split('\n').find(line => line.trim() === firstLine);
|
|
1399
|
-
if (!candidateLine || candidateLine === firstLine)
|
|
1436
|
+
if (!candidateLine || candidateLine === firstLine) {
|
|
1400
1437
|
return '';
|
|
1438
|
+
}
|
|
1401
1439
|
const indent = candidateLine.match(/^\s*/)?.[0] ?? '';
|
|
1402
|
-
if (!indent)
|
|
1440
|
+
if (!indent) {
|
|
1403
1441
|
return '';
|
|
1442
|
+
}
|
|
1404
1443
|
return `Hint: the target line exists in the file but begins with "${indent.replace(/\t/g, '\\t')}" whitespace — your \`find\` is missing that indent. `;
|
|
1405
1444
|
}
|
|
1406
1445
|
/**
|
|
@@ -1408,9 +1447,9 @@ function findIndentationHint(source, find) {
|
|
|
1408
1447
|
* snippet of what the file ACTUALLY contains around the closest fuzzy
|
|
1409
1448
|
* match. Saves the model a re-read round-trip and prevents the failure
|
|
1410
1449
|
* mode where it retries the same wrong `find` 3+ times before giving up
|
|
1411
|
-
* (observed
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1450
|
+
* (observed when the model assumed `<title>Vite + React</title>` still
|
|
1451
|
+
* existed when the file already had `<title>my-app</title>` from a prior
|
|
1452
|
+
* edit — three iterations wasted before re-reading).
|
|
1414
1453
|
*
|
|
1415
1454
|
* Strategy: tokenize the first non-empty line of `find`, score every line
|
|
1416
1455
|
* in the source by token-overlap, return ±3 lines of context around the
|
|
@@ -1420,22 +1459,27 @@ function findIndentationHint(source, find) {
|
|
|
1420
1459
|
*/
|
|
1421
1460
|
function nearestMatchSnippet(source, find) {
|
|
1422
1461
|
const findFirstLine = find.split('\n').find(l => l.trim().length > 0)?.trim();
|
|
1423
|
-
if (!findFirstLine || findFirstLine.length < 8)
|
|
1462
|
+
if (!findFirstLine || findFirstLine.length < 8) {
|
|
1424
1463
|
return '';
|
|
1464
|
+
}
|
|
1425
1465
|
const findTokens = new Set(findFirstLine.split(/[^\w]+/).filter(t => t.length >= 3));
|
|
1426
|
-
if (findTokens.size < 2)
|
|
1466
|
+
if (findTokens.size < 2) {
|
|
1427
1467
|
return '';
|
|
1468
|
+
}
|
|
1428
1469
|
const lines = source.split('\n');
|
|
1429
1470
|
let bestLine = -1;
|
|
1430
1471
|
let bestScore = 0;
|
|
1431
1472
|
for (let i = 0; i < lines.length; i++) {
|
|
1432
1473
|
const lineTokens = lines[i].split(/[^\w]+/).filter(t => t.length >= 3);
|
|
1433
|
-
if (lineTokens.length === 0)
|
|
1474
|
+
if (lineTokens.length === 0) {
|
|
1434
1475
|
continue;
|
|
1476
|
+
}
|
|
1435
1477
|
let hits = 0;
|
|
1436
|
-
for (const t of lineTokens)
|
|
1437
|
-
if (findTokens.has(t))
|
|
1478
|
+
for (const t of lineTokens) {
|
|
1479
|
+
if (findTokens.has(t)) {
|
|
1438
1480
|
hits++;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1439
1483
|
// Normalize by max so a long line with a couple matches doesn't beat
|
|
1440
1484
|
// a short line where everything matches. Tie-broken by earlier line.
|
|
1441
1485
|
const score = hits / Math.max(findTokens.size, lineTokens.length);
|
|
@@ -1446,8 +1490,9 @@ function nearestMatchSnippet(source, find) {
|
|
|
1446
1490
|
}
|
|
1447
1491
|
// 0.4 token-overlap threshold is the empirical "this is probably the
|
|
1448
1492
|
// line you meant" cutoff. Lower than that and the snippet is noise.
|
|
1449
|
-
if (bestLine < 0 || bestScore < 0.4)
|
|
1493
|
+
if (bestLine < 0 || bestScore < 0.4) {
|
|
1450
1494
|
return '';
|
|
1495
|
+
}
|
|
1451
1496
|
const start = Math.max(0, bestLine - 3);
|
|
1452
1497
|
const end = Math.min(lines.length, bestLine + 4);
|
|
1453
1498
|
const widest = String(end).length;
|
|
@@ -1471,15 +1516,17 @@ const listFilesTool = {
|
|
|
1471
1516
|
],
|
|
1472
1517
|
async execute(params, ctx) {
|
|
1473
1518
|
const pattern = params.pattern?.trim();
|
|
1474
|
-
if (!pattern)
|
|
1519
|
+
if (!pattern) {
|
|
1475
1520
|
return { output: 'Error: pattern parameter is required', isError: true };
|
|
1521
|
+
}
|
|
1476
1522
|
const cwd = params.cwd
|
|
1477
1523
|
? (isAbsolutePath(params.cwd) ? params.cwd : `${ctx.workspaceRoot}/${params.cwd}`)
|
|
1478
1524
|
: ctx.workspaceRoot;
|
|
1479
1525
|
try {
|
|
1480
1526
|
const files = await ctx.listFiles(pattern, cwd);
|
|
1481
|
-
if (!files.length)
|
|
1527
|
+
if (!files.length) {
|
|
1482
1528
|
return { output: `No files matched pattern "${pattern}"` };
|
|
1529
|
+
}
|
|
1483
1530
|
const list = files.slice(0, 200).join('\n');
|
|
1484
1531
|
const suffix = files.length > 200 ? `\n\n[list_files: showing first 200 of ${files.length} files]` : '';
|
|
1485
1532
|
return { output: `${files.length} file(s) matched "${pattern}":\n\n${list}${suffix}` };
|
|
@@ -1503,8 +1550,9 @@ const lsTool = {
|
|
|
1503
1550
|
],
|
|
1504
1551
|
async execute(params, ctx) {
|
|
1505
1552
|
const raw = params.path?.trim();
|
|
1506
|
-
if (!raw)
|
|
1553
|
+
if (!raw) {
|
|
1507
1554
|
return { output: 'Error: path parameter is required', isError: true };
|
|
1555
|
+
}
|
|
1508
1556
|
// Resolve relative paths against the workspace root. Hosts handle
|
|
1509
1557
|
// ~ expansion themselves.
|
|
1510
1558
|
const resolved = isAbsolutePath(raw)
|
|
@@ -1519,15 +1567,17 @@ const lsTool = {
|
|
|
1519
1567
|
// only the files directly in Desktop, never the folder itself.
|
|
1520
1568
|
if (ctx.listDirectoryEntries) {
|
|
1521
1569
|
const names = await ctx.listDirectoryEntries(resolved);
|
|
1522
|
-
if (!names.length)
|
|
1570
|
+
if (!names.length) {
|
|
1523
1571
|
return { output: `(empty or not found: ${raw})` };
|
|
1572
|
+
}
|
|
1524
1573
|
return { output: `${names.length} entr${names.length === 1 ? 'y' : 'ies'} in ${raw}:\n${names.join('\n')}` };
|
|
1525
1574
|
}
|
|
1526
1575
|
// Fallback path for hosts that predate listDirectoryEntries —
|
|
1527
1576
|
// files-only, but better than nothing.
|
|
1528
1577
|
const files = await ctx.listFiles('*', resolved);
|
|
1529
|
-
if (!files.length)
|
|
1578
|
+
if (!files.length) {
|
|
1530
1579
|
return { output: `(empty or not found: ${raw})` };
|
|
1580
|
+
}
|
|
1531
1581
|
const prefix = resolved.endsWith('/') ? resolved : resolved + '/';
|
|
1532
1582
|
const names = files.map(f => f.startsWith(prefix) ? f.slice(prefix.length) : f).sort();
|
|
1533
1583
|
return { output: `${names.length} entr${names.length === 1 ? 'y' : 'ies'} in ${raw} (files only — host does not support directory listing):\n${names.join('\n')}` };
|
|
@@ -1572,17 +1622,20 @@ function repoTokenize(s) {
|
|
|
1572
1622
|
function repoMatchScore(name, query) {
|
|
1573
1623
|
const lowerName = name.toLowerCase();
|
|
1574
1624
|
const lowerQuery = query.toLowerCase();
|
|
1575
|
-
if (lowerName === lowerQuery)
|
|
1625
|
+
if (lowerName === lowerQuery) {
|
|
1576
1626
|
return 1000;
|
|
1627
|
+
}
|
|
1577
1628
|
const queryTokens = repoTokenize(query);
|
|
1578
1629
|
const nameTokens = repoTokenize(name);
|
|
1579
|
-
if (queryTokens.length === 0)
|
|
1630
|
+
if (queryTokens.length === 0) {
|
|
1580
1631
|
return 0;
|
|
1632
|
+
}
|
|
1581
1633
|
// Every query token must appear as a substring of some name token.
|
|
1582
1634
|
let matched = 0;
|
|
1583
1635
|
for (const qt of queryTokens) {
|
|
1584
|
-
if (nameTokens.some((nt) => nt.includes(qt)))
|
|
1636
|
+
if (nameTokens.some((nt) => nt.includes(qt))) {
|
|
1585
1637
|
matched++;
|
|
1638
|
+
}
|
|
1586
1639
|
}
|
|
1587
1640
|
if (matched === queryTokens.length) {
|
|
1588
1641
|
// All tokens accounted for. Bonus when the token sets are equal
|
|
@@ -1592,8 +1645,9 @@ function repoMatchScore(name, query) {
|
|
|
1592
1645
|
}
|
|
1593
1646
|
// Fall back to plain substring on the joined string so partial
|
|
1594
1647
|
// queries still surface candidates.
|
|
1595
|
-
if (lowerName.includes(lowerQuery))
|
|
1648
|
+
if (lowerName.includes(lowerQuery)) {
|
|
1596
1649
|
return 100;
|
|
1650
|
+
}
|
|
1597
1651
|
return 0;
|
|
1598
1652
|
}
|
|
1599
1653
|
const findDirectoryTool = {
|
|
@@ -1604,8 +1658,9 @@ const findDirectoryTool = {
|
|
|
1604
1658
|
],
|
|
1605
1659
|
async execute(params, ctx) {
|
|
1606
1660
|
const query = params.name?.trim();
|
|
1607
|
-
if (!query)
|
|
1661
|
+
if (!query) {
|
|
1608
1662
|
return { output: 'Error: name parameter is required', isError: true };
|
|
1663
|
+
}
|
|
1609
1664
|
if (!ctx.listDirectoryEntries) {
|
|
1610
1665
|
return { output: 'Error: this host does not support directory enumeration. Fall back to `run_command find ~ -maxdepth 4 -type d -iname "*<name>*"`.', isError: true };
|
|
1611
1666
|
}
|
|
@@ -1633,15 +1688,17 @@ const findDirectoryTool = {
|
|
|
1633
1688
|
try {
|
|
1634
1689
|
const entries = await ctx.listDirectoryEntries(parent);
|
|
1635
1690
|
for (const entry of entries) {
|
|
1636
|
-
if (!entry.endsWith('/'))
|
|
1691
|
+
if (!entry.endsWith('/')) {
|
|
1637
1692
|
continue;
|
|
1693
|
+
}
|
|
1638
1694
|
const name = entry.slice(0, -1);
|
|
1639
1695
|
const lower = name.toLowerCase();
|
|
1640
1696
|
// Dedup by lowercased basename — tilde paths and the workspace
|
|
1641
1697
|
// parent often resolve to overlapping directories; reporting the
|
|
1642
1698
|
// same hit twice is noise.
|
|
1643
|
-
if (seen.has(lower))
|
|
1699
|
+
if (seen.has(lower)) {
|
|
1644
1700
|
continue;
|
|
1701
|
+
}
|
|
1645
1702
|
const score = repoMatchScore(name, query);
|
|
1646
1703
|
if (score > 0) {
|
|
1647
1704
|
seen.add(lower);
|
|
@@ -1669,25 +1726,31 @@ const findDirectoryTool = {
|
|
|
1669
1726
|
const lines = [];
|
|
1670
1727
|
if (exact.length) {
|
|
1671
1728
|
lines.push(`Exact match${exact.length === 1 ? '' : 'es'} for "${query}":`);
|
|
1672
|
-
for (const h of exact)
|
|
1729
|
+
for (const h of exact) {
|
|
1673
1730
|
lines.push(h.path);
|
|
1731
|
+
}
|
|
1674
1732
|
}
|
|
1675
1733
|
if (tokenMatch.length) {
|
|
1676
|
-
if (lines.length)
|
|
1734
|
+
if (lines.length) {
|
|
1677
1735
|
lines.push('');
|
|
1736
|
+
}
|
|
1678
1737
|
lines.push(`Token match${tokenMatch.length === 1 ? '' : 'es'} for "${query}":`);
|
|
1679
|
-
for (const h of tokenMatch)
|
|
1738
|
+
for (const h of tokenMatch) {
|
|
1680
1739
|
lines.push(h.path);
|
|
1740
|
+
}
|
|
1681
1741
|
}
|
|
1682
1742
|
if (substring.length) {
|
|
1683
|
-
if (lines.length)
|
|
1743
|
+
if (lines.length) {
|
|
1684
1744
|
lines.push('');
|
|
1745
|
+
}
|
|
1685
1746
|
lines.push(`Substring match${substring.length === 1 ? '' : 'es'} for "${query}":`);
|
|
1686
|
-
for (const h of substring)
|
|
1747
|
+
for (const h of substring) {
|
|
1687
1748
|
lines.push(h.path);
|
|
1749
|
+
}
|
|
1688
1750
|
}
|
|
1689
|
-
if (omitted > 0)
|
|
1751
|
+
if (omitted > 0) {
|
|
1690
1752
|
lines.push(`\n[find_directory: showing first ${MAX} of ${hits.length} matches — narrow the query to see the rest]`);
|
|
1753
|
+
}
|
|
1691
1754
|
return { output: lines.join('\n') };
|
|
1692
1755
|
}
|
|
1693
1756
|
};
|
|
@@ -1703,15 +1766,17 @@ const searchCodeTool = {
|
|
|
1703
1766
|
],
|
|
1704
1767
|
async execute(params, ctx) {
|
|
1705
1768
|
const pattern = params.pattern?.trim();
|
|
1706
|
-
if (!pattern)
|
|
1769
|
+
if (!pattern) {
|
|
1707
1770
|
return { output: 'Error: pattern parameter is required', isError: true };
|
|
1771
|
+
}
|
|
1708
1772
|
const cwd = params.cwd
|
|
1709
1773
|
? (isAbsolutePath(params.cwd) ? params.cwd : `${ctx.workspaceRoot}/${params.cwd}`)
|
|
1710
1774
|
: ctx.workspaceRoot;
|
|
1711
1775
|
try {
|
|
1712
1776
|
const results = await ctx.searchCode(pattern, cwd, params.file_glob);
|
|
1713
|
-
if (!results.trim())
|
|
1777
|
+
if (!results.trim()) {
|
|
1714
1778
|
return { output: `No matches found for "${pattern}"` };
|
|
1779
|
+
}
|
|
1715
1780
|
return { output: truncate(results, MAX_SEARCH_CHARS, 'search_code') };
|
|
1716
1781
|
}
|
|
1717
1782
|
catch (err) {
|
|
@@ -1953,8 +2018,9 @@ function shellTokenize(input) {
|
|
|
1953
2018
|
let inDouble = false;
|
|
1954
2019
|
let hasToken = false;
|
|
1955
2020
|
const push = () => {
|
|
1956
|
-
if (hasToken || current.length > 0)
|
|
2021
|
+
if (hasToken || current.length > 0) {
|
|
1957
2022
|
out.push(current);
|
|
2023
|
+
}
|
|
1958
2024
|
current = '';
|
|
1959
2025
|
hasToken = false;
|
|
1960
2026
|
};
|
|
@@ -2000,15 +2066,17 @@ function shellTokenize(input) {
|
|
|
2000
2066
|
continue;
|
|
2001
2067
|
}
|
|
2002
2068
|
if (/\s/.test(ch)) {
|
|
2003
|
-
if (current.length > 0 || hasToken)
|
|
2069
|
+
if (current.length > 0 || hasToken) {
|
|
2004
2070
|
push();
|
|
2071
|
+
}
|
|
2005
2072
|
continue;
|
|
2006
2073
|
}
|
|
2007
2074
|
current += ch;
|
|
2008
2075
|
hasToken = true;
|
|
2009
2076
|
}
|
|
2010
|
-
if (current.length > 0 || hasToken)
|
|
2077
|
+
if (current.length > 0 || hasToken) {
|
|
2011
2078
|
push();
|
|
2079
|
+
}
|
|
2012
2080
|
return out;
|
|
2013
2081
|
}
|
|
2014
2082
|
const runCommandTool = {
|
|
@@ -2021,8 +2089,9 @@ const runCommandTool = {
|
|
|
2021
2089
|
],
|
|
2022
2090
|
async execute(params, ctx) {
|
|
2023
2091
|
const rawCmd = params.cmd?.trim();
|
|
2024
|
-
if (!rawCmd)
|
|
2092
|
+
if (!rawCmd) {
|
|
2025
2093
|
return { output: 'Error: cmd parameter is required', isError: true };
|
|
2094
|
+
}
|
|
2026
2095
|
// Some models squish the entire command line into `cmd` ("npx create
|
|
2027
2096
|
// @angular/cli mqtt-app") instead of splitting it across `cmd` /
|
|
2028
2097
|
// `args` per the schema. Normalize before the allow-list check —
|
|
@@ -2118,8 +2187,9 @@ const watchCommandTool = {
|
|
|
2118
2187
|
],
|
|
2119
2188
|
async execute(params, ctx) {
|
|
2120
2189
|
const rawCmd = params.cmd?.trim();
|
|
2121
|
-
if (!rawCmd)
|
|
2190
|
+
if (!rawCmd) {
|
|
2122
2191
|
return { output: 'Error: cmd parameter is required', isError: true };
|
|
2192
|
+
}
|
|
2123
2193
|
// Mirror the run_command normalization — accept both
|
|
2124
2194
|
// cmd="npm" args="run dev" AND cmd="npm run dev" args="" shapes.
|
|
2125
2195
|
let cmd = rawCmd;
|