@burtson-labs/agent-core 1.6.16 → 1.6.17

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.
Files changed (77) hide show
  1. package/README.md +2 -0
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +8 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/mcp/activation.js +16 -8
  7. package/dist/mcp/activation.js.map +1 -1
  8. package/dist/mcp/clientPool.js +40 -22
  9. package/dist/mcp/clientPool.js.map +1 -1
  10. package/dist/mcp/server.js +16 -10
  11. package/dist/mcp/server.js.map +1 -1
  12. package/dist/mcp/toolAdapter.js +21 -11
  13. package/dist/mcp/toolAdapter.js.map +1 -1
  14. package/dist/providers/deterministic-provider.d.ts +1 -1
  15. package/dist/providers/deterministic-provider.d.ts.map +1 -1
  16. package/dist/runtime/AgentRuntime.d.ts +2 -2
  17. package/dist/runtime/AgentRuntime.d.ts.map +1 -1
  18. package/dist/security/secretPatterns.js +4 -2
  19. package/dist/security/secretPatterns.js.map +1 -1
  20. package/dist/telemetry/otlpExporter.d.ts +69 -0
  21. package/dist/telemetry/otlpExporter.d.ts.map +1 -0
  22. package/dist/telemetry/otlpExporter.js +321 -0
  23. package/dist/telemetry/otlpExporter.js.map +1 -0
  24. package/dist/tools/ask-user-tool.js +8 -4
  25. package/dist/tools/ask-user-tool.js.map +1 -1
  26. package/dist/tools/compactMessages.js +6 -3
  27. package/dist/tools/compactMessages.js.map +1 -1
  28. package/dist/tools/core-tools.js +145 -75
  29. package/dist/tools/core-tools.js.map +1 -1
  30. package/dist/tools/git-tools.js +22 -11
  31. package/dist/tools/git-tools.js.map +1 -1
  32. package/dist/tools/language-adapters.js +24 -12
  33. package/dist/tools/language-adapters.js.map +1 -1
  34. package/dist/tools/loop/finalAnswerNudges.js +12 -6
  35. package/dist/tools/loop/finalAnswerNudges.js.map +1 -1
  36. package/dist/tools/loop/llmStream.js +6 -3
  37. package/dist/tools/loop/llmStream.js.map +1 -1
  38. package/dist/tools/loop/parallelExecute.js +2 -1
  39. package/dist/tools/loop/parallelExecute.js.map +1 -1
  40. package/dist/tools/loop/singleToolExecute.js +8 -4
  41. package/dist/tools/loop/singleToolExecute.js.map +1 -1
  42. package/dist/tools/loop/turnSetup.js +6 -3
  43. package/dist/tools/loop/turnSetup.js.map +1 -1
  44. package/dist/tools/ocr.d.ts.map +1 -1
  45. package/dist/tools/ocr.js +7 -5
  46. package/dist/tools/ocr.js.map +1 -1
  47. package/dist/tools/post-edit-checks.js +25 -13
  48. package/dist/tools/post-edit-checks.js.map +1 -1
  49. package/dist/tools/skill-loader.d.ts +1 -1
  50. package/dist/tools/skill-loader.d.ts.map +1 -1
  51. package/dist/tools/skill-loader.js +14 -7
  52. package/dist/tools/skill-loader.js.map +1 -1
  53. package/dist/tools/skill-registry.js +2 -1
  54. package/dist/tools/skill-registry.js.map +1 -1
  55. package/dist/tools/skills/mail-search-skill.js +14 -7
  56. package/dist/tools/skills/mail-search-skill.js.map +1 -1
  57. package/dist/tools/skills/plan-skill.js +4 -2
  58. package/dist/tools/skills/plan-skill.js.map +1 -1
  59. package/dist/tools/skills/semantic-search-skill.js +12 -6
  60. package/dist/tools/skills/semantic-search-skill.js.map +1 -1
  61. package/dist/tools/skills/test-gen-skill.js +8 -4
  62. package/dist/tools/skills/test-gen-skill.js.map +1 -1
  63. package/dist/tools/tool-registry.js +10 -5
  64. package/dist/tools/tool-registry.js.map +1 -1
  65. package/dist/tools/tool-use-loop.d.ts +1 -1
  66. package/dist/tools/tool-use-loop.d.ts.map +1 -1
  67. package/dist/tools/tool-use-loop.js +26 -14
  68. package/dist/tools/tool-use-loop.js.map +1 -1
  69. package/dist/tools/tool-use-parser.js +56 -28
  70. package/dist/tools/tool-use-parser.js.map +1 -1
  71. package/dist/tools/toolAvailabilityDetector.js +2 -1
  72. package/dist/tools/toolAvailabilityDetector.js.map +1 -1
  73. package/dist/tools/unified-patch.js +16 -8
  74. package/dist/tools/unified-patch.js.map +1 -1
  75. package/dist/utils/event-emitter.d.ts +1 -1
  76. package/dist/utils/event-emitter.d.ts.map +1 -1
  77. package/package.json +1 -1
@@ -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(/^/, '').trimStart();
55
- if (trimmed.length === 0)
56
- return null; // empty file is valid JSON-zero
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
- if (/^[A-Za-z]:[\\/]/.test(p))
74
- return true; // C:\foo or C:/foo
75
- if (p.startsWith('\\\\'))
76
- return true; // UNC \\server\share
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() : '';
@@ -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
- if (content === undefined || content === null)
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
- if (find === undefined || find === null)
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
- if (replace === undefined || replace === null)
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
- if (find === '')
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
- if (find === replace)
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
- if (find.includes('\n'))
628
+ }
629
+ if (find.includes('\n')) {
613
630
  return replace;
614
- if (!replace.includes('\n'))
631
+ }
632
+ if (!replace.includes('\n')) {
615
633
  return replace;
616
- if (/^\s/.test(replace))
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
- if (delta > 0)
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
- if (content === undefined || content === null)
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)
@@ -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; // before was clean, after isn't — definitely introduced.
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
  /**
@@ -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;