@oxecli/oxe 1.0.50 → 1.0.52

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/tools.js CHANGED
@@ -11,9 +11,6 @@ export { toolLoadSkill };
11
11
  export function naturalSortKey(s) {
12
12
  return s.replace(/\d+/g, (m) => m.padStart(12, "0")).toLowerCase();
13
13
  }
14
- function isGlob(s) {
15
- return /[*?[]/.test(s);
16
- }
17
14
  function truncateDiffLine(line) {
18
15
  const ending = line.endsWith("\n") ? "\n" : "";
19
16
  const body = ending ? line.slice(0, -1) : line;
@@ -24,8 +21,8 @@ function truncateDiffLine(line) {
24
21
  }
25
22
  function displayDiff(pathName, oldContent, newContent) {
26
23
  if (oldContent.length + newContent.length > max_diff_source_chars) {
27
- console.log(`\x1b[2m${pathName}: ${oldContent.length.toLocaleString()} chars -> ${newContent.length.toLocaleString()} chars ` +
28
- `(diff hidden: exceeds ${max_diff_source_chars.toLocaleString()} char limit)\x1b[0m`);
24
+ process.stdout.write(`\x1b[2m${pathName}: ${oldContent.length.toLocaleString()} chars -> ${newContent.length.toLocaleString()} chars ` +
25
+ `(diff hidden: exceeds ${max_diff_source_chars.toLocaleString()} char limit)\x1b[0m\n`);
29
26
  return;
30
27
  }
31
28
  const patch = structuredPatch(pathName, pathName, oldContent, newContent, "", "", { context: max_diff_context_lines });
@@ -44,8 +41,7 @@ function displayDiff(pathName, oldContent, newContent) {
44
41
  const removed = contentLines.filter((c) => c.kind === "-").length;
45
42
  if (!added && !removed)
46
43
  return;
47
- console.log(`\x1b[2m${pathName}\x1b[0m`);
48
- console.log(`\x1b[31m-${removed}\x1b[0m \x1b[32m+${added}\x1b[0m`);
44
+ process.stdout.write(`\x1b[90m┌─\x1b[0m \x1b[1m${pathName}\x1b[0m \x1b[32m+${added}\x1b[0m \x1b[31m-${removed}\x1b[0m\n`);
49
45
  const shown = [];
50
46
  let run = [];
51
47
  let lastKind = null;
@@ -70,17 +66,23 @@ function displayDiff(pathName, oldContent, newContent) {
70
66
  run.push(c);
71
67
  }
72
68
  flush();
73
- const diffText = shown.join("\n");
74
- const colored = diffText
75
- .split("\n")
76
- .map((l) => l.startsWith("+") ? `\x1b[32m${l}\x1b[0m` : l.startsWith("-") ? `\x1b[31m${l}\x1b[0m` : l)
77
- .join("\n");
78
- process.stdout.write(colored + "\n");
69
+ for (const l of shown) {
70
+ if (l.startsWith("+")) {
71
+ process.stdout.write(`\x1b[90m│\x1b[0m \x1b[32m${l}\x1b[0m\n`);
72
+ }
73
+ else if (l.startsWith("-")) {
74
+ process.stdout.write(`\x1b[90m│\x1b[0m \x1b[31m${l}\x1b[0m\n`);
75
+ }
76
+ else {
77
+ process.stdout.write(`\x1b[90m│\x1b[0m \x1b[2m${l}\x1b[0m\n`);
78
+ }
79
+ }
80
+ process.stdout.write(`\x1b[90m└────────────────────────────────────────\x1b[0m\n`);
79
81
  }
80
82
  export function truncateToolOutput(output, maxChars = max_output_chars) {
81
83
  if (output.length > maxChars) {
82
84
  return (output.slice(0, maxChars) +
83
- `\n\n[... Output truncated! Total characters: ${output.length} ...]`);
85
+ `\n\n[... Output truncated! Total characters: ${output.length.toLocaleString()} ...]`);
84
86
  }
85
87
  return output;
86
88
  }
@@ -129,11 +131,14 @@ function buildLineIndex(p) {
129
131
  return null;
130
132
  }
131
133
  finally {
132
- if (fd !== null)
134
+ if (fd !== null) {
133
135
  try {
134
136
  fs.closeSync(fd);
135
137
  }
136
- catch { /* ignore */ }
138
+ catch {
139
+ /* ignore */
140
+ }
141
+ }
137
142
  }
138
143
  if (base === 0)
139
144
  return [[0], 0];
@@ -224,13 +229,30 @@ export function toolReadFile(pathName, startLine = 1, endLine) {
224
229
  }
225
230
  if (size > max_read_file_bytes) {
226
231
  return (`Error: file too large (${size.toLocaleString()} bytes > ${max_read_file_bytes.toLocaleString()} cap). ` +
227
- `Use grep or bash to inspect it instead of reading it whole.`);
232
+ "Use grep or bash to inspect it instead of reading it whole.");
228
233
  }
229
234
  if (size === 0)
230
235
  return "(empty file)";
231
- const start = Math.max(1, startLine);
232
- if (endLine != null && endLine < start) {
233
- return `Error: end_line ${endLine} is before start_line ${start}`;
236
+ try {
237
+ const head = fs.readFileSync(p, { flag: "r" }).subarray(0, 8192);
238
+ if (head.includes(0))
239
+ return `Error: refusing to read binary file: ${pathName}`;
240
+ }
241
+ catch {
242
+ /* ignore */
243
+ }
244
+ const sVal = typeof startLine === "number"
245
+ ? startLine
246
+ : parseInt(String(startLine || 1), 10) || 1;
247
+ const start = Math.max(1, sVal);
248
+ let eVal = null;
249
+ if (endLine != null) {
250
+ const parsed = typeof endLine === "number" ? endLine : parseInt(String(endLine), 10);
251
+ if (!Number.isNaN(parsed))
252
+ eVal = parsed;
253
+ }
254
+ if (eVal != null && eVal < start) {
255
+ return `Error: end_line ${eVal} is before start_line ${start}`;
234
256
  }
235
257
  let end = null;
236
258
  let total = 0;
@@ -240,9 +262,10 @@ export function toolReadFile(pathName, startLine = 1, endLine) {
240
262
  if (idx) {
241
263
  const [offsets, t] = idx;
242
264
  total = t;
243
- if (start > total)
265
+ if (start > total) {
244
266
  return `Error: start_line ${start} exceeds file length (${total} lines)`;
245
- end = endLine == null ? total : Math.min(total, endLine);
267
+ }
268
+ end = eVal == null ? total : Math.min(total, eVal);
246
269
  capped = end - start + 1 > max_read_lines;
247
270
  if (capped)
248
271
  end = start + max_read_lines - 1;
@@ -253,11 +276,12 @@ export function toolReadFile(pathName, startLine = 1, endLine) {
253
276
  }
254
277
  }
255
278
  else {
256
- const collectEnd = Math.min(Math.max(1, endLine != null ? endLine : start + max_read_lines - 1), start + max_read_lines - 1);
279
+ const collectEnd = Math.min(Math.max(1, eVal != null ? eVal : start + max_read_lines - 1), start + max_read_lines - 1);
257
280
  [total, numbered] = streamReadRange(p, start, collectEnd);
258
- if (start > total)
281
+ if (start > total) {
259
282
  return `Error: start_line ${start} exceeds file length (${total} lines)`;
260
- end = endLine == null ? total : Math.min(total, endLine);
283
+ }
284
+ end = eVal == null ? total : Math.min(total, eVal);
261
285
  capped = end - start + 1 > max_read_lines;
262
286
  if (capped) {
263
287
  end = start + max_read_lines - 1;
@@ -286,8 +310,9 @@ export function toolWriteFile(pathName, content) {
286
310
  fs.mkdirSync(path.dirname(p), { recursive: true });
287
311
  if (fs.existsSync(p)) {
288
312
  const head = fs.readFileSync(p).subarray(0, 8192);
289
- if (head.includes(0))
313
+ if (head.includes(0)) {
290
314
  return `Error: refusing to overwrite binary file: ${pathName}`;
315
+ }
291
316
  const oldContent = fs.readFileSync(p, "utf-8");
292
317
  displayDiff(pathName, oldContent, content);
293
318
  }
@@ -299,7 +324,7 @@ export function toolWriteFile(pathName, content) {
299
324
  catch (err) {
300
325
  return `Error: ${err}`;
301
326
  }
302
- return `Wrote ${content.length} characters to ${pathName}`;
327
+ return `Wrote ${content.length.toLocaleString()} characters to ${pathName}`;
303
328
  }
304
329
  // ---------------------------------------------------------------------------
305
330
  // edit_file
@@ -322,9 +347,12 @@ function withNewlines(text, nl) {
322
347
  return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r\n");
323
348
  }
324
349
  function findWsBlocks(text, needle) {
325
- const needleLines = withNewlines(needle, "\n").split("\n").map((l) => l.trim());
326
- while (needleLines.length && needleLines[needleLines.length - 1] === "")
350
+ const needleLines = withNewlines(needle, "\n")
351
+ .split("\n")
352
+ .map((l) => l.trim());
353
+ while (needleLines.length && needleLines[needleLines.length - 1] === "") {
327
354
  needleLines.pop();
355
+ }
328
356
  if (!needleLines.length)
329
357
  return [];
330
358
  const textLines = text.split("\n");
@@ -391,7 +419,7 @@ export function toolEditFile(pathName, oldString, newString, replaceAll = false)
391
419
  }
392
420
  const count = text.split(match).length - 1;
393
421
  if (count > 1 && !replaceAll) {
394
- return (`Error: old_string matches ${count} locations; pass replace_all=true or make old_string more specific`);
422
+ return `Error: old_string matches ${count} locations; pass replace_all=true or make old_string more specific`;
395
423
  }
396
424
  const insert = withNewlines(newString, newline);
397
425
  const newContent = replaceAll
@@ -412,7 +440,7 @@ export function toolEditFile(pathName, oldString, newString, replaceAll = false)
412
440
  // ---------------------------------------------------------------------------
413
441
  function terminateProcessTree(proc) {
414
442
  try {
415
- if (isWindowsTool()) {
443
+ if (process.platform === "win32") {
416
444
  execFile("taskkill", ["/F", "/T", "/PID", String(proc.pid)], { timeout: 10000 });
417
445
  }
418
446
  else {
@@ -420,22 +448,19 @@ function terminateProcessTree(proc) {
420
448
  process.kill(-proc.pid, "SIGKILL");
421
449
  }
422
450
  catch {
423
- proc.kill();
451
+ proc.kill("SIGKILL");
424
452
  }
425
453
  }
426
454
  }
427
455
  catch {
428
456
  try {
429
- proc.kill();
457
+ proc.kill("SIGKILL");
430
458
  }
431
459
  catch {
432
460
  /* ignore */
433
461
  }
434
462
  }
435
463
  }
436
- function isWindowsTool() {
437
- return process.platform === "win32";
438
- }
439
464
  export function toolBash(command, timeout = 60, cwd) {
440
465
  try {
441
466
  let t = typeof timeout === "number" ? timeout : parseInt(String(timeout), 10);
@@ -445,7 +470,7 @@ export function toolBash(command, timeout = 60, cwd) {
445
470
  return new Promise((resolve) => {
446
471
  let child;
447
472
  try {
448
- if (isWindowsTool()) {
473
+ if (process.platform === "win32") {
449
474
  child = spawn(command, {
450
475
  shell: true,
451
476
  cwd: cwd || undefined,
@@ -470,7 +495,10 @@ export function toolBash(command, timeout = 60, cwd) {
470
495
  const timer = setTimeout(() => {
471
496
  if (finished)
472
497
  return;
473
- terminateProcessTree({ pid: child.pid, kill: () => child.kill("SIGKILL") });
498
+ terminateProcessTree({
499
+ pid: child.pid,
500
+ kill: (sig) => child.kill(sig),
501
+ });
474
502
  const output = (stdout + stderr).trim();
475
503
  let msg = `Error: command timed out after ${t}s`;
476
504
  if (output)
@@ -504,17 +532,6 @@ export function toolBash(command, timeout = 60, cwd) {
504
532
  // ---------------------------------------------------------------------------
505
533
  // glob / grep
506
534
  // ---------------------------------------------------------------------------
507
- function pathUnderIgnoredDir(base, f) {
508
- const rel = path.relative(base, f);
509
- if (rel.startsWith("..") || path.isAbsolute(rel))
510
- return false;
511
- const parts = rel.split(/[\\/]/);
512
- for (let i = 0; i < parts.length - 1; i++) {
513
- if (ignoredDirs.has(parts[i]))
514
- return true;
515
- }
516
- return false;
517
- }
518
535
  function matchParts(parts, relParts) {
519
536
  if (!parts.length)
520
537
  return relParts.length === 0;
@@ -529,13 +546,12 @@ function matchParts(parts, relParts) {
529
546
  globMatch(relParts[0], parts[0]) &&
530
547
  matchParts(parts.slice(1), relParts.slice(1)));
531
548
  }
532
- // minimal glob matcher (supports * ? [..])
533
549
  function globMatch(name, pattern) {
534
550
  const regex = pattern
535
551
  .replace(/[.+^${}()|[\]\\]/g, "\\$&")
536
552
  .replace(/\*/g, ".*")
537
553
  .replace(/\?/g, ".");
538
- return new RegExp(`^${regex}$`).test(name);
554
+ return new RegExp(`^${regex}$`, "i").test(name);
539
555
  }
540
556
  function walkFiles(root, current, rel, parts, out) {
541
557
  let entries;
@@ -557,7 +573,10 @@ function walkFiles(root, current, rel, parts, out) {
557
573
  }
558
574
  else if (entry.isFile()) {
559
575
  if (matchParts(parts, rel.concat(name))) {
560
- out.push([path.join(root, ...rel, name), fs.statSync(path.join(root, ...rel, name)).size]);
576
+ out.push([
577
+ path.join(root, ...rel, name),
578
+ fs.statSync(path.join(root, ...rel, name)).size,
579
+ ]);
561
580
  }
562
581
  }
563
582
  }
@@ -614,7 +633,7 @@ export function toolGlob(pattern, pathName = ".", limit = 200) {
614
633
  const base = pathName;
615
634
  if (!fs.existsSync(base))
616
635
  return `Error: path not found: ${pathName}`;
617
- limit = Math.max(1, limit);
636
+ const lim = typeof limit === "number" ? limit : parseInt(String(limit), 10) || 200;
618
637
  let matches;
619
638
  try {
620
639
  matches = rglobPruned(base, pattern);
@@ -626,9 +645,9 @@ export function toolGlob(pattern, pathName = ".", limit = 200) {
626
645
  if (!matches.length)
627
646
  return "No files matched";
628
647
  const total = matches.length;
629
- let result = matches.slice(0, limit).map(([p]) => p).join("\n");
630
- if (total > limit) {
631
- result += `\n\n[... ${total - limit} more matches not shown ...]`;
648
+ let result = matches.slice(0, lim).map(([p]) => p).join("\n");
649
+ if (total > lim) {
650
+ result += `\n\n[... ${total - lim} more matches not shown ...]`;
632
651
  }
633
652
  return result;
634
653
  }
@@ -638,12 +657,13 @@ export function toolGrep(pattern, pathName = ".", glob = "*", limit = 200) {
638
657
  return `Error: path not found: ${pathName}`;
639
658
  let regex;
640
659
  try {
641
- regex = new RegExp(pattern);
660
+ regex = new RegExp(pattern, "i");
642
661
  }
643
662
  catch (err) {
644
663
  return `Error: invalid regex pattern: ${err}`;
645
664
  }
646
665
  glob = glob || "*";
666
+ const lim = typeof limit === "number" ? limit : parseInt(String(limit), 10) || 200;
647
667
  let targets;
648
668
  if (fs.statSync(base).isFile()) {
649
669
  targets =
@@ -670,7 +690,7 @@ export function toolGrep(pattern, pathName = ".", glob = "*", limit = 200) {
670
690
  const line = capLine(lines[i]);
671
691
  if (regex.test(line)) {
672
692
  hits.push(`${fpath}:${i + 1}:${line}`);
673
- if (hits.length >= limit)
693
+ if (hits.length >= lim)
674
694
  break;
675
695
  }
676
696
  }
@@ -678,7 +698,7 @@ export function toolGrep(pattern, pathName = ".", glob = "*", limit = 200) {
678
698
  catch {
679
699
  continue;
680
700
  }
681
- if (hits.length >= limit)
701
+ if (hits.length >= lim)
682
702
  break;
683
703
  }
684
704
  let result = hits.length ? hits.join("\n") : "No matches found";
@@ -710,7 +730,10 @@ export const RESPONSES_TOOLS = [
710
730
  type: "object",
711
731
  properties: {
712
732
  path: { type: "string", description: "Path to the file" },
713
- start_line: { type: "integer", description: "First line to read, 1-indexed (default 1)" },
733
+ start_line: {
734
+ type: "integer",
735
+ description: "First line to read, 1-indexed (default 1)",
736
+ },
714
737
  end_line: {
715
738
  type: "integer",
716
739
  description: `Last line to read, inclusive (default: end of file, capped at ${max_read_lines} lines from start_line)`,
@@ -784,8 +807,14 @@ export const RESPONSES_TOOLS = [
784
807
  type: "object",
785
808
  properties: {
786
809
  pattern: { type: "string", description: "Glob pattern, e.g. '*.py'" },
787
- path: { type: "string", description: "Directory to search from (default '.')" },
788
- limit: { type: "integer", description: "Max number of matches to return (default 200)" },
810
+ path: {
811
+ type: "string",
812
+ description: "Directory to search from (default '.')",
813
+ },
814
+ limit: {
815
+ type: "integer",
816
+ description: "Max number of matches to return (default 200)",
817
+ },
789
818
  },
790
819
  required: ["pattern"],
791
820
  },
@@ -798,13 +827,22 @@ export const RESPONSES_TOOLS = [
798
827
  parameters: {
799
828
  type: "object",
800
829
  properties: {
801
- pattern: { type: "string", description: "Regular expression to search for" },
802
- path: { type: "string", description: "Directory to search from (default '.')" },
830
+ pattern: {
831
+ type: "string",
832
+ description: "Regular expression to search for",
833
+ },
834
+ path: {
835
+ type: "string",
836
+ description: "Directory to search from (default '.')",
837
+ },
803
838
  glob: {
804
839
  type: "string",
805
840
  description: "Only search files matching this glob (default '*')",
806
841
  },
807
- limit: { type: "integer", description: "Max number of matches to return (default 200)" },
842
+ limit: {
843
+ type: "integer",
844
+ description: "Max number of matches to return (default 200)",
845
+ },
808
846
  },
809
847
  required: ["pattern"],
810
848
  },