open-agents-ai 0.11.8 → 0.12.1

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 (2) hide show
  1. package/dist/index.js +1266 -192
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,6 +2,12 @@
2
2
  import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
6
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
7
+ }) : x)(function(x) {
8
+ if (typeof require !== "undefined") return require.apply(this, arguments);
9
+ throw Error('Dynamic require of "' + x + '" is not supported');
10
+ });
5
11
  var __esm = (fn, res) => function __init() {
6
12
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
13
  };
@@ -1164,7 +1170,7 @@ var init_shell = __esm({
1164
1170
  const timeout = args["timeout"] ?? this.defaultTimeout;
1165
1171
  const stdinInput = args["stdin"];
1166
1172
  const start = performance.now();
1167
- return new Promise((resolve14) => {
1173
+ return new Promise((resolve15) => {
1168
1174
  const child = spawn("bash", ["-c", command], {
1169
1175
  cwd: this.workingDir,
1170
1176
  env: {
@@ -1217,7 +1223,7 @@ var init_shell = __esm({
1217
1223
  const combined = stdout + stderr;
1218
1224
  const looksInteractive = /\? .+[›>]|y\/n|yes\/no|\(Y\/n\)|\[y\/N\]/i.test(combined);
1219
1225
  const hint = looksInteractive ? " The command appears to be waiting for interactive input. Use non-interactive flags (e.g., --yes, --no-input) or provide input via the stdin parameter." : "";
1220
- resolve14({
1226
+ resolve15({
1221
1227
  success: false,
1222
1228
  output: stdout,
1223
1229
  error: `Command timed out after ${timeout}ms.${hint}`,
@@ -1226,7 +1232,7 @@ var init_shell = __esm({
1226
1232
  return;
1227
1233
  }
1228
1234
  const success = code === 0;
1229
- resolve14({
1235
+ resolve15({
1230
1236
  success,
1231
1237
  output: stdout + (stderr && success ? `
1232
1238
  STDERR:
@@ -1237,7 +1243,7 @@ ${stderr}` : ""),
1237
1243
  });
1238
1244
  child.on("error", (err) => {
1239
1245
  clearTimeout(timer);
1240
- resolve14({
1246
+ resolve15({
1241
1247
  success: false,
1242
1248
  output: stdout,
1243
1249
  error: err.message,
@@ -1741,24 +1747,50 @@ var init_web_search = __esm({
1741
1747
  // packages/execution/dist/tools/file-edit.js
1742
1748
  import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
1743
1749
  import { resolve as resolve5 } from "node:path";
1750
+ function countOccurrences(haystack, needle) {
1751
+ let count = 0;
1752
+ let pos = 0;
1753
+ while ((pos = haystack.indexOf(needle, pos)) !== -1) {
1754
+ count++;
1755
+ pos += needle.length;
1756
+ }
1757
+ return count;
1758
+ }
1759
+ function findMatchLines(haystack, needle) {
1760
+ const lines = [];
1761
+ let pos = 0;
1762
+ while ((pos = haystack.indexOf(needle, pos)) !== -1) {
1763
+ const lineNumber = haystack.slice(0, pos).split("\n").length;
1764
+ lines.push(lineNumber);
1765
+ pos += needle.length;
1766
+ }
1767
+ return lines;
1768
+ }
1769
+ function replaceAllOccurrences(haystack, needle, replacement) {
1770
+ return haystack.split(needle).join(replacement);
1771
+ }
1744
1772
  var FileEditTool;
1745
1773
  var init_file_edit = __esm({
1746
1774
  "packages/execution/dist/tools/file-edit.js"() {
1747
1775
  "use strict";
1748
1776
  FileEditTool = class {
1749
1777
  name = "file_edit";
1750
- description = "Make a precise edit to a file by replacing an exact string match. More efficient than rewriting the entire file.";
1778
+ description = "Make a precise edit to a file by replacing an exact string match. The old_string must be unique in the file unless replace_all is true. Use replace_all to rename variables or change repeated patterns throughout the file.";
1751
1779
  parameters = {
1752
1780
  type: "object",
1753
1781
  properties: {
1754
1782
  path: { type: "string", description: "Absolute or relative file path" },
1755
1783
  old_string: {
1756
1784
  type: "string",
1757
- description: "The exact string to search for and replace"
1785
+ description: "The exact string to search for and replace. Must be unique in the file (or use replace_all)."
1758
1786
  },
1759
1787
  new_string: {
1760
1788
  type: "string",
1761
1789
  description: "The replacement string"
1790
+ },
1791
+ replace_all: {
1792
+ type: "boolean",
1793
+ description: "Replace ALL occurrences instead of just the first. Use for variable renames, import path changes, etc. Default: false"
1762
1794
  }
1763
1795
  },
1764
1796
  required: ["path", "old_string", "new_string"]
@@ -1771,25 +1803,45 @@ var init_file_edit = __esm({
1771
1803
  const filePath = args["path"];
1772
1804
  const oldString = args["old_string"];
1773
1805
  const newString = args["new_string"];
1806
+ const replaceAll = args["replace_all"] === true;
1774
1807
  const start = performance.now();
1775
1808
  try {
1776
1809
  const fullPath = resolve5(this.workingDir, filePath);
1777
1810
  const content = await readFile2(fullPath, "utf-8");
1778
- const index = content.indexOf(oldString);
1779
- if (index === -1) {
1811
+ const occurrences = countOccurrences(content, oldString);
1812
+ if (occurrences === 0) {
1813
+ return {
1814
+ success: false,
1815
+ output: "",
1816
+ error: `old_string not found in ${filePath}. Read the file first to verify the exact content.`,
1817
+ durationMs: performance.now() - start
1818
+ };
1819
+ }
1820
+ if (!replaceAll && occurrences > 1) {
1821
+ const matchLines = findMatchLines(content, oldString);
1780
1822
  return {
1781
1823
  success: false,
1782
1824
  output: "",
1783
- error: `old_string not found in ${fullPath}`,
1825
+ error: `old_string is not unique \u2014 found ${occurrences} occurrences at lines ${matchLines.join(", ")}. Either include more surrounding context to make old_string unique, or set replace_all=true to replace all ${occurrences} occurrences.`,
1784
1826
  durationMs: performance.now() - start
1785
1827
  };
1786
1828
  }
1787
- const lineNumber = content.slice(0, index).split("\n").length;
1788
- const updated = content.slice(0, index) + newString + content.slice(index + oldString.length);
1829
+ let updated;
1830
+ let editedLines;
1831
+ if (replaceAll) {
1832
+ editedLines = findMatchLines(content, oldString);
1833
+ updated = replaceAllOccurrences(content, oldString, newString);
1834
+ } else {
1835
+ const index = content.indexOf(oldString);
1836
+ const lineNumber = content.slice(0, index).split("\n").length;
1837
+ editedLines = [lineNumber];
1838
+ updated = content.slice(0, index) + newString + content.slice(index + oldString.length);
1839
+ }
1789
1840
  await writeFile2(fullPath, updated, "utf-8");
1841
+ const linesInfo = editedLines.length === 1 ? `line ${editedLines[0]}` : `${editedLines.length} locations (lines ${editedLines.join(", ")})`;
1790
1842
  return {
1791
1843
  success: true,
1792
- output: `Edited ${fullPath} at line ${lineNumber}`,
1844
+ output: `Edited ${filePath} at ${linesInfo}`,
1793
1845
  durationMs: performance.now() - start
1794
1846
  };
1795
1847
  } catch (error) {
@@ -2431,15 +2483,24 @@ var init_aiwg_workflow = __esm({
2431
2483
  });
2432
2484
 
2433
2485
  // packages/execution/dist/tools/batch-edit.js
2434
- import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
2435
- import { resolve as resolve9, dirname as dirname2 } from "node:path";
2486
+ import { readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
2487
+ import { resolve as resolve9 } from "node:path";
2488
+ function countOccurrences2(haystack, needle) {
2489
+ let count = 0;
2490
+ let pos = 0;
2491
+ while ((pos = haystack.indexOf(needle, pos)) !== -1) {
2492
+ count++;
2493
+ pos += needle.length;
2494
+ }
2495
+ return count;
2496
+ }
2436
2497
  var BatchEditTool;
2437
2498
  var init_batch_edit = __esm({
2438
2499
  "packages/execution/dist/tools/batch-edit.js"() {
2439
2500
  "use strict";
2440
2501
  BatchEditTool = class {
2441
2502
  name = "batch_edit";
2442
- description = "Make multiple precise edits across one or more files in a single call. More efficient than calling file_edit repeatedly. Each edit replaces an exact string match. Edits are applied in order within each file.";
2503
+ description = "Make multiple precise edits across one or more files in a single call. More efficient than calling file_edit repeatedly. Each edit replaces an exact string match with uniqueness validation. Edits are applied in order within each file. Set replace_all on individual edits for bulk renames.";
2443
2504
  parameters = {
2444
2505
  type: "object",
2445
2506
  properties: {
@@ -2449,9 +2510,22 @@ var init_batch_edit = __esm({
2449
2510
  items: {
2450
2511
  type: "object",
2451
2512
  properties: {
2452
- path: { type: "string", description: "File path relative to project root" },
2453
- old_string: { type: "string", description: "Exact string to find and replace" },
2454
- new_string: { type: "string", description: "Replacement string" }
2513
+ path: {
2514
+ type: "string",
2515
+ description: "File path relative to project root"
2516
+ },
2517
+ old_string: {
2518
+ type: "string",
2519
+ description: "Exact string to find and replace (must be unique unless replace_all)"
2520
+ },
2521
+ new_string: {
2522
+ type: "string",
2523
+ description: "Replacement string"
2524
+ },
2525
+ replace_all: {
2526
+ type: "boolean",
2527
+ description: "Replace all occurrences (for renames). Default: false"
2528
+ }
2455
2529
  },
2456
2530
  required: ["path", "old_string", "new_string"]
2457
2531
  }
@@ -2479,7 +2553,12 @@ var init_batch_edit = __esm({
2479
2553
  const fullPath = resolve9(this.workingDir, edit.path);
2480
2554
  if (!byFile.has(fullPath))
2481
2555
  byFile.set(fullPath, []);
2482
- byFile.get(fullPath).push({ old_string: edit.old_string, new_string: edit.new_string });
2556
+ byFile.get(fullPath).push({
2557
+ old_string: edit.old_string,
2558
+ new_string: edit.new_string,
2559
+ replace_all: edit.replace_all,
2560
+ relPath: edit.path
2561
+ });
2483
2562
  }
2484
2563
  const results = [];
2485
2564
  let successCount = 0;
@@ -2488,15 +2567,26 @@ var init_batch_edit = __esm({
2488
2567
  try {
2489
2568
  let content = await readFile5(fullPath, "utf-8");
2490
2569
  for (const edit of fileEdits) {
2491
- const index = content.indexOf(edit.old_string);
2492
- if (index === -1) {
2493
- results.push(`SKIP: old_string not found in ${fullPath}`);
2570
+ const occurrences = countOccurrences2(content, edit.old_string);
2571
+ if (occurrences === 0) {
2572
+ results.push(`SKIP: old_string not found in ${edit.relPath}`);
2573
+ failCount++;
2574
+ continue;
2575
+ }
2576
+ if (!edit.replace_all && occurrences > 1) {
2577
+ results.push(`AMBIGUOUS: old_string has ${occurrences} matches in ${edit.relPath} \u2014 add more context or use replace_all`);
2494
2578
  failCount++;
2495
2579
  continue;
2496
2580
  }
2497
- const lineNumber = content.slice(0, index).split("\n").length;
2498
- content = content.slice(0, index) + edit.new_string + content.slice(index + edit.old_string.length);
2499
- results.push(`EDIT: ${fullPath} line ${lineNumber}`);
2581
+ if (edit.replace_all) {
2582
+ content = content.split(edit.old_string).join(edit.new_string);
2583
+ results.push(`EDIT: ${edit.relPath} (${occurrences} replacements)`);
2584
+ } else {
2585
+ const index = content.indexOf(edit.old_string);
2586
+ const lineNumber = content.slice(0, index).split("\n").length;
2587
+ content = content.slice(0, index) + edit.new_string + content.slice(index + edit.old_string.length);
2588
+ results.push(`EDIT: ${edit.relPath} line ${lineNumber}`);
2589
+ }
2500
2590
  successCount++;
2501
2591
  }
2502
2592
  await writeFile4(fullPath, content, "utf-8");
@@ -2518,6 +2608,178 @@ ${results.join("\n")}`,
2518
2608
  }
2519
2609
  });
2520
2610
 
2611
+ // packages/execution/dist/tools/file-patch.js
2612
+ import { readFile as readFile6, writeFile as writeFile5, copyFile } from "node:fs/promises";
2613
+ import { resolve as resolve10 } from "node:path";
2614
+ var FilePatchTool;
2615
+ var init_file_patch = __esm({
2616
+ "packages/execution/dist/tools/file-patch.js"() {
2617
+ "use strict";
2618
+ FilePatchTool = class {
2619
+ name = "file_patch";
2620
+ description = "Edit specific line ranges in a file. More precise than string matching for large files. Modes: 'replace' replaces lines start_line..end_line with new_content, 'insert_before' inserts before start_line, 'insert_after' inserts after start_line, 'delete' removes lines start_line..end_line. Use dry_run to preview changes.";
2621
+ parameters = {
2622
+ type: "object",
2623
+ properties: {
2624
+ path: {
2625
+ type: "string",
2626
+ description: "File path relative to project root"
2627
+ },
2628
+ start_line: {
2629
+ type: "number",
2630
+ description: "Starting line number (1-based, inclusive)"
2631
+ },
2632
+ end_line: {
2633
+ type: "number",
2634
+ description: "Ending line number (1-based, inclusive). Required for replace and delete modes. For single-line edits, set equal to start_line."
2635
+ },
2636
+ new_content: {
2637
+ type: "string",
2638
+ description: "Replacement content (for replace mode) or content to insert (for insert modes). Not needed for delete mode."
2639
+ },
2640
+ mode: {
2641
+ type: "string",
2642
+ enum: ["replace", "insert_before", "insert_after", "delete"],
2643
+ description: "Edit mode. Default: replace"
2644
+ },
2645
+ dry_run: {
2646
+ type: "boolean",
2647
+ description: "Preview the diff without writing. Returns what would change. Default: false"
2648
+ }
2649
+ },
2650
+ required: ["path", "start_line"]
2651
+ };
2652
+ workingDir;
2653
+ constructor(workingDir) {
2654
+ this.workingDir = workingDir;
2655
+ }
2656
+ async execute(args) {
2657
+ const filePath = args["path"];
2658
+ const startLine = args["start_line"];
2659
+ const endLine = args["end_line"] ?? startLine;
2660
+ const newContent = args["new_content"] ?? "";
2661
+ const mode = args["mode"] ?? "replace";
2662
+ const dryRun = args["dry_run"] === true;
2663
+ const start = performance.now();
2664
+ try {
2665
+ if (!Number.isInteger(startLine) || startLine < 1) {
2666
+ return {
2667
+ success: false,
2668
+ output: "",
2669
+ error: "start_line must be a positive integer (1-based)",
2670
+ durationMs: performance.now() - start
2671
+ };
2672
+ }
2673
+ if (!Number.isInteger(endLine) || endLine < startLine) {
2674
+ return {
2675
+ success: false,
2676
+ output: "",
2677
+ error: `end_line (${endLine}) must be >= start_line (${startLine})`,
2678
+ durationMs: performance.now() - start
2679
+ };
2680
+ }
2681
+ const fullPath = resolve10(this.workingDir, filePath);
2682
+ const content = await readFile6(fullPath, "utf-8");
2683
+ const lines = content.split("\n");
2684
+ const totalLines = lines.length;
2685
+ if (startLine > totalLines) {
2686
+ return {
2687
+ success: false,
2688
+ output: "",
2689
+ error: `start_line ${startLine} exceeds file length (${totalLines} lines)`,
2690
+ durationMs: performance.now() - start
2691
+ };
2692
+ }
2693
+ const effectiveEnd = Math.min(endLine, totalLines);
2694
+ const startIdx = startLine - 1;
2695
+ const endIdx = effectiveEnd;
2696
+ const newLines = newContent.length > 0 ? newContent.split("\n") : [];
2697
+ let resultLines;
2698
+ let description;
2699
+ switch (mode) {
2700
+ case "replace": {
2701
+ const removedLines = lines.slice(startIdx, endIdx);
2702
+ resultLines = [
2703
+ ...lines.slice(0, startIdx),
2704
+ ...newLines,
2705
+ ...lines.slice(endIdx)
2706
+ ];
2707
+ description = `Replaced lines ${startLine}-${effectiveEnd} (${removedLines.length} lines \u2192 ${newLines.length} lines)`;
2708
+ break;
2709
+ }
2710
+ case "insert_before": {
2711
+ resultLines = [
2712
+ ...lines.slice(0, startIdx),
2713
+ ...newLines,
2714
+ ...lines.slice(startIdx)
2715
+ ];
2716
+ description = `Inserted ${newLines.length} lines before line ${startLine}`;
2717
+ break;
2718
+ }
2719
+ case "insert_after": {
2720
+ resultLines = [
2721
+ ...lines.slice(0, startIdx + 1),
2722
+ ...newLines,
2723
+ ...lines.slice(startIdx + 1)
2724
+ ];
2725
+ description = `Inserted ${newLines.length} lines after line ${startLine}`;
2726
+ break;
2727
+ }
2728
+ case "delete": {
2729
+ const removedLines = lines.slice(startIdx, endIdx);
2730
+ resultLines = [...lines.slice(0, startIdx), ...lines.slice(endIdx)];
2731
+ description = `Deleted lines ${startLine}-${effectiveEnd} (${removedLines.length} lines removed)`;
2732
+ break;
2733
+ }
2734
+ default:
2735
+ return {
2736
+ success: false,
2737
+ output: "",
2738
+ error: `Unknown mode: ${mode}. Use replace, insert_before, insert_after, or delete.`,
2739
+ durationMs: performance.now() - start
2740
+ };
2741
+ }
2742
+ const oldSection = lines.slice(Math.max(0, startIdx - 2), Math.min(totalLines, endIdx + 2)).map((l, i) => `- ${String(Math.max(1, startLine - 2) + i).padStart(4)} | ${l}`).join("\n");
2743
+ const newSectionStart = Math.max(0, startIdx - 2);
2744
+ const newSectionEnd = Math.min(resultLines.length, startIdx + newLines.length + 2);
2745
+ const newSection = resultLines.slice(newSectionStart, newSectionEnd).map((l, i) => `+ ${String(newSectionStart + 1 + i).padStart(4)} | ${l}`).join("\n");
2746
+ const diff = `${description}
2747
+
2748
+ Before:
2749
+ ${oldSection}
2750
+
2751
+ After:
2752
+ ${newSection}`;
2753
+ if (dryRun) {
2754
+ return {
2755
+ success: true,
2756
+ output: `[DRY RUN] ${diff}
2757
+
2758
+ File NOT modified. Remove dry_run to apply.`,
2759
+ durationMs: performance.now() - start
2760
+ };
2761
+ }
2762
+ await writeFile5(fullPath, resultLines.join("\n"), "utf-8");
2763
+ return {
2764
+ success: true,
2765
+ output: `${filePath}: ${description} (${totalLines} \u2192 ${resultLines.length} total lines)
2766
+
2767
+ ${diff}`,
2768
+ durationMs: performance.now() - start
2769
+ };
2770
+ } catch (error) {
2771
+ return {
2772
+ success: false,
2773
+ output: "",
2774
+ error: error instanceof Error ? error.message : String(error),
2775
+ durationMs: performance.now() - start
2776
+ };
2777
+ }
2778
+ }
2779
+ };
2780
+ }
2781
+ });
2782
+
2521
2783
  // packages/execution/dist/tools/codebase-map.js
2522
2784
  import { readdirSync as readdirSync3, statSync as statSync3, readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
2523
2785
  import { join as join7, relative, extname } from "node:path";
@@ -3372,7 +3634,7 @@ Exit code: ${task.exitCode ?? "N/A"}`,
3372
3634
 
3373
3635
  // packages/execution/dist/tools/image.js
3374
3636
  import { existsSync as existsSync7, readFileSync as readFileSync6, statSync as statSync4 } from "node:fs";
3375
- import { resolve as resolve10, extname as extname2, basename } from "node:path";
3637
+ import { resolve as resolve11, extname as extname2, basename } from "node:path";
3376
3638
  import { execSync as execSync7 } from "node:child_process";
3377
3639
  import { tmpdir } from "node:os";
3378
3640
  import { join as join10 } from "node:path";
@@ -3481,7 +3743,7 @@ var init_image = __esm({
3481
3743
  if (!rawPath) {
3482
3744
  return { success: false, output: "", error: "path is required", durationMs: 0 };
3483
3745
  }
3484
- const fullPath = resolve10(this.workingDir, rawPath);
3746
+ const fullPath = resolve11(this.workingDir, rawPath);
3485
3747
  if (!existsSync7(fullPath)) {
3486
3748
  return { success: false, output: "", error: `File not found: ${rawPath}`, durationMs: Date.now() - start };
3487
3749
  }
@@ -3550,7 +3812,7 @@ ${ocrText}`);
3550
3812
  }
3551
3813
  async execute(args) {
3552
3814
  const start = Date.now();
3553
- const outputPath = args["output_path"] ? resolve10(this.workingDir, String(args["output_path"])) : join10(tmpdir(), `oa-screenshot-${Date.now()}.png`);
3815
+ const outputPath = args["output_path"] ? resolve11(this.workingDir, String(args["output_path"])) : join10(tmpdir(), `oa-screenshot-${Date.now()}.png`);
3554
3816
  const delayMs = typeof args["delay_ms"] === "number" ? args["delay_ms"] : 0;
3555
3817
  const region = String(args["region"] ?? "full");
3556
3818
  if (delayMs > 0) {
@@ -3673,7 +3935,7 @@ ${ocrText}`);
3673
3935
  if (!rawPath) {
3674
3936
  return { success: false, output: "", error: "path is required", durationMs: 0 };
3675
3937
  }
3676
- const fullPath = resolve10(this.workingDir, rawPath);
3938
+ const fullPath = resolve11(this.workingDir, rawPath);
3677
3939
  if (!existsSync7(fullPath)) {
3678
3940
  return { success: false, output: "", error: `File not found: ${rawPath}`, durationMs: Date.now() - start };
3679
3941
  }
@@ -3723,6 +3985,510 @@ ${text}`,
3723
3985
  }
3724
3986
  });
3725
3987
 
3988
+ // packages/execution/dist/tools/custom-tool.js
3989
+ var custom_tool_exports = {};
3990
+ __export(custom_tool_exports, {
3991
+ CustomTool: () => CustomTool,
3992
+ buildCustomTools: () => buildCustomTools,
3993
+ deleteCustomToolDefinition: () => deleteCustomToolDefinition,
3994
+ listCustomToolFiles: () => listCustomToolFiles,
3995
+ loadCustomTools: () => loadCustomTools,
3996
+ saveCustomToolDefinition: () => saveCustomToolDefinition
3997
+ });
3998
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync7, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
3999
+ import { join as join11 } from "node:path";
4000
+ import { homedir as homedir3 } from "node:os";
4001
+ import { spawn as spawn3 } from "node:child_process";
4002
+ function projectToolsDir(repoRoot) {
4003
+ return join11(repoRoot, ".oa", "tools");
4004
+ }
4005
+ function loadCustomTools(repoRoot) {
4006
+ const definitions = /* @__PURE__ */ new Map();
4007
+ for (const def of loadFromDirectory(GLOBAL_TOOLS_DIR)) {
4008
+ definitions.set(def.name, def);
4009
+ }
4010
+ const projectDir = projectToolsDir(repoRoot);
4011
+ for (const def of loadFromDirectory(projectDir)) {
4012
+ definitions.set(def.name, def);
4013
+ }
4014
+ return Array.from(definitions.values());
4015
+ }
4016
+ function buildCustomTools(repoRoot) {
4017
+ const definitions = loadCustomTools(repoRoot);
4018
+ return definitions.map((def) => new CustomTool(def, repoRoot));
4019
+ }
4020
+ function saveCustomToolDefinition(definition, scope, repoRoot) {
4021
+ const dir = scope === "project" && repoRoot ? projectToolsDir(repoRoot) : GLOBAL_TOOLS_DIR;
4022
+ mkdirSync3(dir, { recursive: true });
4023
+ const filePath = join11(dir, `${definition.name}.json`);
4024
+ writeFileSync3(filePath, JSON.stringify(definition, null, 2), "utf-8");
4025
+ return filePath;
4026
+ }
4027
+ function deleteCustomToolDefinition(name, scope, repoRoot) {
4028
+ const dir = scope === "project" && repoRoot ? projectToolsDir(repoRoot) : GLOBAL_TOOLS_DIR;
4029
+ const filePath = join11(dir, `${name}.json`);
4030
+ if (existsSync8(filePath)) {
4031
+ const { unlinkSync: unlinkSync2 } = __require("node:fs");
4032
+ unlinkSync2(filePath);
4033
+ return true;
4034
+ }
4035
+ return false;
4036
+ }
4037
+ function listCustomToolFiles(repoRoot) {
4038
+ const result = [];
4039
+ const seen = /* @__PURE__ */ new Set();
4040
+ for (const def of loadFromDirectory(projectToolsDir(repoRoot))) {
4041
+ result.push({
4042
+ name: def.name,
4043
+ scope: "project",
4044
+ description: def.description,
4045
+ stepsCount: def.steps.length,
4046
+ version: def.version
4047
+ });
4048
+ seen.add(def.name);
4049
+ }
4050
+ for (const def of loadFromDirectory(GLOBAL_TOOLS_DIR)) {
4051
+ if (!seen.has(def.name)) {
4052
+ result.push({
4053
+ name: def.name,
4054
+ scope: "global",
4055
+ description: def.description,
4056
+ stepsCount: def.steps.length,
4057
+ version: def.version
4058
+ });
4059
+ }
4060
+ }
4061
+ return result;
4062
+ }
4063
+ function loadFromDirectory(dir) {
4064
+ if (!existsSync8(dir))
4065
+ return [];
4066
+ const definitions = [];
4067
+ try {
4068
+ const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
4069
+ for (const file of files) {
4070
+ try {
4071
+ const content = readFileSync7(join11(dir, file), "utf-8");
4072
+ const def = JSON.parse(content);
4073
+ if (def.name && def.description && Array.isArray(def.steps) && def.steps.length > 0) {
4074
+ if (!def.parameters || typeof def.parameters !== "object") {
4075
+ def.parameters = { type: "object", properties: {}, required: [] };
4076
+ }
4077
+ definitions.push(def);
4078
+ }
4079
+ } catch {
4080
+ }
4081
+ }
4082
+ } catch {
4083
+ }
4084
+ return definitions;
4085
+ }
4086
+ var GLOBAL_TOOLS_DIR, CustomTool;
4087
+ var init_custom_tool = __esm({
4088
+ "packages/execution/dist/tools/custom-tool.js"() {
4089
+ "use strict";
4090
+ GLOBAL_TOOLS_DIR = join11(homedir3(), ".open-agents", "tools");
4091
+ CustomTool = class {
4092
+ name;
4093
+ description;
4094
+ parameters;
4095
+ steps;
4096
+ workingDir;
4097
+ constructor(definition, workingDir) {
4098
+ this.name = definition.name;
4099
+ this.description = definition.description;
4100
+ this.parameters = definition.parameters;
4101
+ this.steps = definition.steps;
4102
+ this.workingDir = workingDir;
4103
+ }
4104
+ async execute(args) {
4105
+ const start = performance.now();
4106
+ const outputs = [];
4107
+ let allSuccess = true;
4108
+ for (let i = 0; i < this.steps.length; i++) {
4109
+ const step = this.steps[i];
4110
+ const command = this.interpolate(step.command, args);
4111
+ outputs.push(`--- Step ${i + 1}: ${step.description} ---`);
4112
+ outputs.push(`$ ${command}`);
4113
+ try {
4114
+ const result = await this.runCommand(command);
4115
+ outputs.push(result.output);
4116
+ if (!result.success) {
4117
+ allSuccess = false;
4118
+ if (result.error)
4119
+ outputs.push(`Error: ${result.error}`);
4120
+ if (!step.continueOnError) {
4121
+ outputs.push(`Step ${i + 1} failed. Stopping.`);
4122
+ break;
4123
+ }
4124
+ outputs.push(`Step ${i + 1} failed but continueOnError=true, proceeding.`);
4125
+ }
4126
+ } catch (err) {
4127
+ allSuccess = false;
4128
+ outputs.push(`Step ${i + 1} error: ${err instanceof Error ? err.message : String(err)}`);
4129
+ if (!step.continueOnError)
4130
+ break;
4131
+ }
4132
+ }
4133
+ return {
4134
+ success: allSuccess,
4135
+ output: outputs.join("\n"),
4136
+ error: allSuccess ? void 0 : "One or more steps failed",
4137
+ durationMs: performance.now() - start
4138
+ };
4139
+ }
4140
+ /** Replace {{param}} placeholders with actual argument values */
4141
+ interpolate(template, args) {
4142
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
4143
+ const val = args[key];
4144
+ if (val === void 0 || val === null)
4145
+ return "";
4146
+ return String(val).replace(/[`$\\!"]/g, "\\$&");
4147
+ });
4148
+ }
4149
+ /** Execute a single shell command and return output */
4150
+ runCommand(command) {
4151
+ return new Promise((resolve15) => {
4152
+ const child = spawn3("bash", ["-c", command], {
4153
+ cwd: this.workingDir,
4154
+ env: { ...process.env, CI: "true", NO_COLOR: "1" },
4155
+ stdio: ["pipe", "pipe", "pipe"]
4156
+ });
4157
+ let stdout = "";
4158
+ let stderr = "";
4159
+ const maxBuf = 512 * 1024;
4160
+ child.stdout.on("data", (data) => {
4161
+ stdout += data.toString();
4162
+ if (stdout.length > maxBuf)
4163
+ stdout = stdout.slice(0, maxBuf);
4164
+ });
4165
+ child.stderr.on("data", (data) => {
4166
+ stderr += data.toString();
4167
+ if (stderr.length > maxBuf)
4168
+ stderr = stderr.slice(0, maxBuf);
4169
+ });
4170
+ child.stdin.end();
4171
+ const timer = setTimeout(() => {
4172
+ try {
4173
+ child.kill("SIGTERM");
4174
+ } catch {
4175
+ }
4176
+ resolve15({ success: false, output: stdout, error: "Command timed out after 60s" });
4177
+ }, 6e4);
4178
+ child.on("close", (code) => {
4179
+ clearTimeout(timer);
4180
+ resolve15({
4181
+ success: code === 0,
4182
+ output: stdout + (stderr && code === 0 ? `
4183
+ STDERR:
4184
+ ${stderr}` : ""),
4185
+ error: code !== 0 ? stderr || `Exit code ${code}` : void 0
4186
+ });
4187
+ });
4188
+ child.on("error", (err) => {
4189
+ clearTimeout(timer);
4190
+ resolve15({ success: false, output: stdout, error: err.message });
4191
+ });
4192
+ });
4193
+ }
4194
+ };
4195
+ }
4196
+ });
4197
+
4198
+ // packages/execution/dist/tools/tool-creator.js
4199
+ var CreateToolTool, ManageToolsTool;
4200
+ var init_tool_creator = __esm({
4201
+ "packages/execution/dist/tools/tool-creator.js"() {
4202
+ "use strict";
4203
+ init_custom_tool();
4204
+ CreateToolTool = class {
4205
+ name = "create_tool";
4206
+ description = "Create a reusable custom tool from a multi-step workflow. Use this when you notice you're performing the same sequence of operations repeatedly (3+ times). The tool will be saved and available in future sessions. Parameters in step commands use {{param}} interpolation.";
4207
+ parameters = {
4208
+ type: "object",
4209
+ properties: {
4210
+ tool_name: {
4211
+ type: "string",
4212
+ description: "Unique tool name in snake_case (e.g., deploy_staging, run_full_test_suite)"
4213
+ },
4214
+ tool_description: {
4215
+ type: "string",
4216
+ description: "Clear description of what this tool does (shown to the agent)"
4217
+ },
4218
+ tool_parameters: {
4219
+ type: "object",
4220
+ description: `JSON Schema for the tool's parameters. Example: {"type":"object","properties":{"branch":{"type":"string","description":"Branch name"}},"required":["branch"]}`
4221
+ },
4222
+ steps: {
4223
+ type: "array",
4224
+ description: "Ordered list of steps. Each step has: command (shell command with {{param}} placeholders), description (what this step does), continueOnError (optional boolean).",
4225
+ items: {
4226
+ type: "object",
4227
+ properties: {
4228
+ command: { type: "string" },
4229
+ description: { type: "string" },
4230
+ continueOnError: { type: "boolean" }
4231
+ },
4232
+ required: ["command", "description"]
4233
+ }
4234
+ },
4235
+ scope: {
4236
+ type: "string",
4237
+ enum: ["project", "global"],
4238
+ description: "Where to save: 'project' saves to .oa/tools/ (this workspace only), 'global' saves to ~/.open-agents/tools/ (available in all projects). Use 'project' for project-specific workflows, 'global' for cross-project patterns."
4239
+ },
4240
+ source_pattern: {
4241
+ type: "string",
4242
+ description: "Description of the repeated pattern that motivated creating this tool"
4243
+ }
4244
+ },
4245
+ required: ["tool_name", "tool_description", "steps", "scope"]
4246
+ };
4247
+ repoRoot;
4248
+ constructor(repoRoot) {
4249
+ this.repoRoot = repoRoot;
4250
+ }
4251
+ async execute(args) {
4252
+ const start = performance.now();
4253
+ const toolName = String(args["tool_name"] ?? "").trim();
4254
+ const toolDescription = String(args["tool_description"] ?? "").trim();
4255
+ const rawSteps = args["steps"];
4256
+ const scope = args["scope"] === "global" ? "global" : "project";
4257
+ const toolParameters = args["tool_parameters"] ?? {
4258
+ type: "object",
4259
+ properties: {},
4260
+ required: []
4261
+ };
4262
+ const sourcePattern = args["source_pattern"];
4263
+ if (!toolName) {
4264
+ return {
4265
+ success: false,
4266
+ output: "",
4267
+ error: "tool_name is required",
4268
+ durationMs: performance.now() - start
4269
+ };
4270
+ }
4271
+ if (!/^[a-z][a-z0-9_]*$/.test(toolName)) {
4272
+ return {
4273
+ success: false,
4274
+ output: "",
4275
+ error: "tool_name must be snake_case (lowercase letters, numbers, underscores, starting with a letter)",
4276
+ durationMs: performance.now() - start
4277
+ };
4278
+ }
4279
+ const CORE_TOOLS = /* @__PURE__ */ new Set([
4280
+ "file_read",
4281
+ "file_write",
4282
+ "file_edit",
4283
+ "shell",
4284
+ "grep_search",
4285
+ "find_files",
4286
+ "list_directory",
4287
+ "web_fetch",
4288
+ "web_search",
4289
+ "memory_read",
4290
+ "memory_write",
4291
+ "task_complete",
4292
+ "sub_agent",
4293
+ "background_run",
4294
+ "task_status",
4295
+ "task_output",
4296
+ "task_stop",
4297
+ "batch_edit",
4298
+ "codebase_map",
4299
+ "diagnostic",
4300
+ "git_info",
4301
+ "image_read",
4302
+ "screenshot",
4303
+ "ocr",
4304
+ "create_tool",
4305
+ "manage_tools",
4306
+ "aiwg_setup",
4307
+ "aiwg_health",
4308
+ "aiwg_workflow"
4309
+ ]);
4310
+ if (CORE_TOOLS.has(toolName)) {
4311
+ return {
4312
+ success: false,
4313
+ output: "",
4314
+ error: `Cannot override core tool: ${toolName}`,
4315
+ durationMs: performance.now() - start
4316
+ };
4317
+ }
4318
+ if (!toolDescription) {
4319
+ return {
4320
+ success: false,
4321
+ output: "",
4322
+ error: "tool_description is required",
4323
+ durationMs: performance.now() - start
4324
+ };
4325
+ }
4326
+ if (!rawSteps || !Array.isArray(rawSteps) || rawSteps.length === 0) {
4327
+ return {
4328
+ success: false,
4329
+ output: "",
4330
+ error: "steps must be a non-empty array of {command, description} objects",
4331
+ durationMs: performance.now() - start
4332
+ };
4333
+ }
4334
+ const steps = [];
4335
+ for (let i = 0; i < rawSteps.length; i++) {
4336
+ const raw = rawSteps[i];
4337
+ const command = String(raw["command"] ?? "").trim();
4338
+ const description = String(raw["description"] ?? "").trim();
4339
+ if (!command) {
4340
+ return {
4341
+ success: false,
4342
+ output: "",
4343
+ error: `Step ${i + 1} is missing a command`,
4344
+ durationMs: performance.now() - start
4345
+ };
4346
+ }
4347
+ steps.push({
4348
+ command,
4349
+ description: description || `Step ${i + 1}`,
4350
+ continueOnError: Boolean(raw["continueOnError"])
4351
+ });
4352
+ }
4353
+ const existing = loadCustomTools(this.repoRoot);
4354
+ const existingTool = existing.find((t) => t.name === toolName);
4355
+ const isUpdate = !!existingTool;
4356
+ const definition = {
4357
+ name: toolName,
4358
+ description: toolDescription,
4359
+ version: isUpdate ? existingTool.version + 1 : 1,
4360
+ parameters: toolParameters,
4361
+ steps,
4362
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4363
+ createdBy: "agent",
4364
+ sourcePattern,
4365
+ triggerCount: void 0
4366
+ };
4367
+ try {
4368
+ const filePath = saveCustomToolDefinition(definition, scope, this.repoRoot);
4369
+ const output = [
4370
+ `Custom tool ${isUpdate ? "updated" : "created"}: ${toolName}`,
4371
+ ` Scope: ${scope}`,
4372
+ ` Saved to: ${filePath}`,
4373
+ ` Steps: ${steps.length}`,
4374
+ ` Version: ${definition.version}`,
4375
+ "",
4376
+ "The tool will be available in future sessions.",
4377
+ isUpdate ? "Note: This updates the existing tool definition." : "",
4378
+ "",
4379
+ "Definition:",
4380
+ JSON.stringify(definition, null, 2)
4381
+ ].join("\n");
4382
+ return {
4383
+ success: true,
4384
+ output,
4385
+ durationMs: performance.now() - start
4386
+ };
4387
+ } catch (err) {
4388
+ return {
4389
+ success: false,
4390
+ output: "",
4391
+ error: `Failed to save tool: ${err instanceof Error ? err.message : String(err)}`,
4392
+ durationMs: performance.now() - start
4393
+ };
4394
+ }
4395
+ }
4396
+ };
4397
+ ManageToolsTool = class {
4398
+ name = "manage_tools";
4399
+ description = "List, inspect, or delete custom tools. Actions: 'list' (show all custom tools), 'inspect <name>' (show definition), 'delete <name> <scope>' (remove a tool).";
4400
+ parameters = {
4401
+ type: "object",
4402
+ properties: {
4403
+ action: {
4404
+ type: "string",
4405
+ enum: ["list", "inspect", "delete"],
4406
+ description: "Action to perform"
4407
+ },
4408
+ tool_name: {
4409
+ type: "string",
4410
+ description: "Tool name (for inspect/delete)"
4411
+ },
4412
+ scope: {
4413
+ type: "string",
4414
+ enum: ["project", "global"],
4415
+ description: "Scope for delete action"
4416
+ }
4417
+ },
4418
+ required: ["action"]
4419
+ };
4420
+ repoRoot;
4421
+ constructor(repoRoot) {
4422
+ this.repoRoot = repoRoot;
4423
+ }
4424
+ async execute(args) {
4425
+ const start = performance.now();
4426
+ const action = String(args["action"] ?? "list");
4427
+ const toolName = args["tool_name"];
4428
+ switch (action) {
4429
+ case "list": {
4430
+ const { listCustomToolFiles: listCustomToolFiles2 } = await Promise.resolve().then(() => (init_custom_tool(), custom_tool_exports));
4431
+ const tools = listCustomToolFiles2(this.repoRoot);
4432
+ if (tools.length === 0) {
4433
+ return {
4434
+ success: true,
4435
+ output: "No custom tools installed.\n\nUse create_tool to create one from a repeated workflow.",
4436
+ durationMs: performance.now() - start
4437
+ };
4438
+ }
4439
+ const lines = ["Custom Tools:", ""];
4440
+ for (const t of tools) {
4441
+ lines.push(` ${t.name} (${t.scope}, v${t.version}, ${t.stepsCount} steps)`);
4442
+ lines.push(` ${t.description}`);
4443
+ }
4444
+ return {
4445
+ success: true,
4446
+ output: lines.join("\n"),
4447
+ durationMs: performance.now() - start
4448
+ };
4449
+ }
4450
+ case "inspect": {
4451
+ if (!toolName) {
4452
+ return { success: false, output: "", error: "tool_name required for inspect", durationMs: performance.now() - start };
4453
+ }
4454
+ const tools = loadCustomTools(this.repoRoot);
4455
+ const tool = tools.find((t) => t.name === toolName);
4456
+ if (!tool) {
4457
+ return { success: false, output: "", error: `Tool not found: ${toolName}`, durationMs: performance.now() - start };
4458
+ }
4459
+ return {
4460
+ success: true,
4461
+ output: JSON.stringify(tool, null, 2),
4462
+ durationMs: performance.now() - start
4463
+ };
4464
+ }
4465
+ case "delete": {
4466
+ if (!toolName) {
4467
+ return { success: false, output: "", error: "tool_name required for delete", durationMs: performance.now() - start };
4468
+ }
4469
+ const scope = args["scope"] === "global" ? "global" : "project";
4470
+ const { deleteCustomToolDefinition: deleteCustomToolDefinition2 } = await Promise.resolve().then(() => (init_custom_tool(), custom_tool_exports));
4471
+ const deleted = deleteCustomToolDefinition2(toolName, scope, this.repoRoot);
4472
+ return {
4473
+ success: deleted,
4474
+ output: deleted ? `Deleted custom tool: ${toolName} (${scope})` : `Tool not found: ${toolName} (${scope})`,
4475
+ error: deleted ? void 0 : "Tool not found",
4476
+ durationMs: performance.now() - start
4477
+ };
4478
+ }
4479
+ default:
4480
+ return {
4481
+ success: false,
4482
+ output: "",
4483
+ error: `Unknown action: ${action}. Use list, inspect, or delete.`,
4484
+ durationMs: performance.now() - start
4485
+ };
4486
+ }
4487
+ }
4488
+ };
4489
+ }
4490
+ });
4491
+
3726
4492
  // packages/execution/dist/shellRunner.js
3727
4493
  var init_shellRunner = __esm({
3728
4494
  "packages/execution/dist/shellRunner.js"() {
@@ -3817,11 +4583,14 @@ var init_dist2 = __esm({
3817
4583
  init_aiwg_health();
3818
4584
  init_aiwg_workflow();
3819
4585
  init_batch_edit();
4586
+ init_file_patch();
3820
4587
  init_codebase_map();
3821
4588
  init_diagnostic();
3822
4589
  init_git_info();
3823
4590
  init_background_task();
3824
4591
  init_image();
4592
+ init_custom_tool();
4593
+ init_tool_creator();
3825
4594
  init_shellRunner();
3826
4595
  init_gitWorktree();
3827
4596
  init_patchApplier();
@@ -4936,8 +5705,8 @@ var init_code_retriever = __esm({
4936
5705
  });
4937
5706
  }
4938
5707
  async getFileContent(filePath, startLine, endLine) {
4939
- const { readFile: readFile9 } = await import("node:fs/promises");
4940
- const content = await readFile9(filePath, "utf-8");
5708
+ const { readFile: readFile10 } = await import("node:fs/promises");
5709
+ const content = await readFile10(filePath, "utf-8");
4941
5710
  if (startLine === void 0)
4942
5711
  return content;
4943
5712
  const lines = content.split("\n");
@@ -4952,8 +5721,8 @@ var init_code_retriever = __esm({
4952
5721
  // packages/retrieval/dist/lexicalSearch.js
4953
5722
  import { execFile as execFile4 } from "node:child_process";
4954
5723
  import { promisify as promisify4 } from "node:util";
4955
- import { readFile as readFile6, readdir, stat } from "node:fs/promises";
4956
- import { join as join11, extname as extname3 } from "node:path";
5724
+ import { readFile as readFile7, readdir, stat } from "node:fs/promises";
5725
+ import { join as join12, extname as extname3 } from "node:path";
4957
5726
  async function searchByPath(pathPattern, options) {
4958
5727
  const allFiles = await collectFiles(options.rootDir, options.includeGlobs ?? DEFAULT_INCLUDE_GLOBS, options.excludeGlobs ?? DEFAULT_EXCLUDE_GLOBS);
4959
5728
  const pattern = options.caseInsensitive ? pathPattern.toLowerCase() : pathPattern;
@@ -5058,7 +5827,7 @@ async function searchWithNodeFallback(pattern, kind, options) {
5058
5827
  if (results.length >= maxMatches)
5059
5828
  break;
5060
5829
  try {
5061
- const content = await readFile6(filePath, "utf-8");
5830
+ const content = await readFile7(filePath, "utf-8");
5062
5831
  const contentLines = content.split("\n");
5063
5832
  for (let i = 0; i < contentLines.length; i++) {
5064
5833
  if (results.length >= maxMatches)
@@ -5095,7 +5864,7 @@ async function walkForFiles(rootDir, dir, excludeGlobs, results) {
5095
5864
  continue;
5096
5865
  if (excludeGlobs.some((g) => entry.name === g || matchesGlob(entry.name, g)))
5097
5866
  continue;
5098
- const absPath = join11(dir, entry.name);
5867
+ const absPath = join12(dir, entry.name);
5099
5868
  if (entry.isDirectory()) {
5100
5869
  await walkForFiles(rootDir, absPath, excludeGlobs, results);
5101
5870
  } else if (entry.isFile()) {
@@ -5269,8 +6038,8 @@ var init_graphExpand = __esm({
5269
6038
  });
5270
6039
 
5271
6040
  // packages/retrieval/dist/snippetPacker.js
5272
- import { readFile as readFile7 } from "node:fs/promises";
5273
- import { join as join12 } from "node:path";
6041
+ import { readFile as readFile8 } from "node:fs/promises";
6042
+ import { join as join13 } from "node:path";
5274
6043
  async function packSnippets(requests, opts = {}) {
5275
6044
  const maxTokens = opts.maxTokens ?? DEFAULT_MAX_TOKENS;
5276
6045
  const contextLines = opts.contextLines ?? DEFAULT_CONTEXT_LINES;
@@ -5296,10 +6065,10 @@ async function packSnippets(requests, opts = {}) {
5296
6065
  return { packed, dropped, totalTokens };
5297
6066
  }
5298
6067
  async function extractSnippet(req, repoRoot, contextLines = DEFAULT_CONTEXT_LINES) {
5299
- const absPath = req.filePath.startsWith("/") ? req.filePath : join12(repoRoot, req.filePath);
6068
+ const absPath = req.filePath.startsWith("/") ? req.filePath : join13(repoRoot, req.filePath);
5300
6069
  let content;
5301
6070
  try {
5302
- content = await readFile7(absPath, "utf-8");
6071
+ content = await readFile8(absPath, "utf-8");
5303
6072
  } catch {
5304
6073
  return null;
5305
6074
  }
@@ -6458,7 +7227,8 @@ var init_agenticRunner = __esm({
6458
7227
 
6459
7228
  - file_read: Read file contents (always read before editing). Supports path, offset, limit.
6460
7229
  - file_write: Create or overwrite a file with complete content
6461
- - file_edit: Make a precise string replacement in a file (preferred over rewriting). Uses old_string/new_string.
7230
+ - file_edit: Make a precise string replacement in a file (preferred over rewriting). Uses old_string/new_string. old_string must be unique unless replace_all=true. Use replace_all for variable renames.
7231
+ - file_patch: Edit specific line ranges in large files. Modes: replace (swap lines), insert_before, insert_after, delete. Use dry_run to preview. Best for large files (500+ lines) where string matching is fragile.
6462
7232
  - find_files: Find files by name pattern (glob). Searches recursively, excludes node_modules/.git.
6463
7233
  - grep_search: Search file contents with regex. Returns matching lines with paths and line numbers.
6464
7234
  - shell: Execute any shell command (tests, builds, git, npm, etc.). Supports stdin parameter for input. Commands run with CI=true for non-interactive mode.
@@ -6564,11 +7334,39 @@ Commands run non-interactively (CI=true). When running scaffolding tools:
6564
7334
  - If a command needs specific answers, use the stdin parameter
6565
7335
  - If a command times out, it likely hit an interactive prompt \u2014 retry with --yes
6566
7336
 
7337
+ ## Custom Tools
7338
+
7339
+ - create_tool: Create a reusable custom tool from a repeated multi-step workflow. Saves to .oa/tools/ (project) or ~/.open-agents/tools/ (global).
7340
+ - manage_tools: List, inspect, or delete custom tools.
7341
+
7342
+ Custom tools are agent-created shell command sequences that automate repeated workflows.
7343
+ They appear alongside core tools and can be invoked just like any built-in tool.
7344
+
7345
+ ### When to Create a Custom Tool
7346
+
7347
+ If you notice you're performing the SAME multi-step sequence for the 3rd time or more:
7348
+ 1. Recognize the repeated pattern (e.g., "bump version \u2192 build \u2192 publish \u2192 commit \u2192 push")
7349
+ 2. Identify what varies between runs (these become parameters)
7350
+ 3. Call create_tool with the steps and parameters
7351
+ 4. Choose scope: 'project' for project-specific workflows, 'global' for cross-project patterns
7352
+
7353
+ ### Custom Tool Guidelines
7354
+
7355
+ - Name tools descriptively in snake_case (e.g., run_full_validation, deploy_to_staging)
7356
+ - Use {{param}} syntax in step commands for interpolation
7357
+ - Set continueOnError=true on steps that may fail but shouldn't stop the pipeline
7358
+ - Test the tool mentally before creating \u2014 ensure the steps would work in order
7359
+ - Prefer 'project' scope unless the pattern genuinely applies to all projects
7360
+
6567
7361
  ## Context Efficiency
6568
7362
 
6569
7363
  - Use grep_search to find specific code instead of reading many files
6570
7364
  - Use file_edit for targeted changes instead of full file rewrites
6571
- - When files are long, use file_read with offset/limit to read specific sections
7365
+ - Use file_edit with replace_all=true for variable/function renames across a file
7366
+ - If file_edit fails with "not unique", include more surrounding context in old_string
7367
+ - For large files (500+ lines): use file_read with offset/limit, then file_patch with line numbers
7368
+ - file_patch with dry_run=true lets you preview changes before applying them
7369
+ - batch_edit to apply multiple edits across files in one call (reduces turns)
6572
7370
  - Focus on error messages in shell output \u2014 skip verbose build logs
6573
7371
  - Don't read files you don't need to modify`;
6574
7372
  AgenticRunner = class {
@@ -7708,6 +8506,7 @@ function renderSlashHelp() {
7708
8506
  ["/voice <model>", "Set voice: glados, overwatch"],
7709
8507
  ["/stream", "Toggle real-time token streaming (pastel syntax highlighting)"],
7710
8508
  ["/bruteforce", "Toggle brute-force mode (auto re-engage on turn limit)"],
8509
+ ["/tools", "List agent-created custom tools"],
7711
8510
  ["/verbose", "Toggle verbose mode"],
7712
8511
  ["/clear", "Clear the screen"],
7713
8512
  ["/help", "Show this help"],
@@ -7953,6 +8752,7 @@ var init_render = __esm({
7953
8752
  "file_read",
7954
8753
  "file_write",
7955
8754
  "file_edit",
8755
+ "file_patch",
7956
8756
  "shell",
7957
8757
  "grep_search",
7958
8758
  "find_files",
@@ -7976,6 +8776,8 @@ var init_render = __esm({
7976
8776
  "aiwg_setup",
7977
8777
  "aiwg_health",
7978
8778
  "aiwg_workflow",
8779
+ "create_tool",
8780
+ "manage_tools",
7979
8781
  "task_complete"
7980
8782
  ];
7981
8783
  COMMAND_NAMES = [
@@ -7988,6 +8790,7 @@ var init_render = __esm({
7988
8790
  "/stream",
7989
8791
  "/verbose",
7990
8792
  "/bruteforce",
8793
+ "/tools",
7991
8794
  "/clear",
7992
8795
  "/help",
7993
8796
  "/quit"
@@ -8080,6 +8883,27 @@ async function handleSlashCommand(input, ctx) {
8080
8883
  renderInfo(`Token streaming: ${isOn ? "on" : "off"}${hasLocal ? " (project-local)" : ""}` + (isOn ? " \u2014 thinking tokens in grey italics, responses with pastel syntax highlighting" : ""));
8081
8884
  return "handled";
8082
8885
  }
8886
+ case "tools": {
8887
+ const tools = listCustomToolFiles(ctx.repoRoot);
8888
+ if (tools.length === 0) {
8889
+ renderInfo("No custom tools installed.");
8890
+ renderInfo("The agent will automatically create tools when it detects repeated workflows (3+ times).");
8891
+ renderInfo('Or ask the agent: "create a tool for [workflow]"');
8892
+ } else {
8893
+ process.stdout.write(`
8894
+ ${c2.bold("Custom Tools:")}
8895
+
8896
+ `);
8897
+ for (const t of tools) {
8898
+ process.stdout.write(` ${c2.cyan(t.name.padEnd(28))} ${c2.dim(`(${t.scope}, v${t.version}, ${t.stepsCount} steps)`)}
8899
+ `);
8900
+ process.stdout.write(` ${"".padEnd(28)} ${t.description}
8901
+ `);
8902
+ }
8903
+ process.stdout.write("\n");
8904
+ }
8905
+ return "handled";
8906
+ }
8083
8907
  case "bruteforce":
8084
8908
  case "brute": {
8085
8909
  const isOn = ctx.bruteForceToggle();
@@ -8208,17 +9032,17 @@ async function handleUpdate() {
8208
9032
  try {
8209
9033
  const { createRequire: createRequire4 } = await import("node:module");
8210
9034
  const { fileURLToPath: fileURLToPath3 } = await import("node:url");
8211
- const { dirname: dirname5, join: join22 } = await import("node:path");
8212
- const { existsSync: existsSync14 } = await import("node:fs");
9035
+ const { dirname: dirname4, join: join23 } = await import("node:path");
9036
+ const { existsSync: existsSync15 } = await import("node:fs");
8213
9037
  const req = createRequire4(import.meta.url);
8214
- const thisDir = dirname5(fileURLToPath3(import.meta.url));
9038
+ const thisDir = dirname4(fileURLToPath3(import.meta.url));
8215
9039
  const candidates = [
8216
- join22(thisDir, "..", "package.json"),
8217
- join22(thisDir, "..", "..", "package.json"),
8218
- join22(thisDir, "..", "..", "..", "package.json")
9040
+ join23(thisDir, "..", "package.json"),
9041
+ join23(thisDir, "..", "..", "package.json"),
9042
+ join23(thisDir, "..", "..", "..", "package.json")
8219
9043
  ];
8220
9044
  for (const pkgPath of candidates) {
8221
- if (existsSync14(pkgPath)) {
9045
+ if (existsSync15(pkgPath)) {
8222
9046
  const pkg = req(pkgPath);
8223
9047
  if (pkg.name === "open-agents-ai" || pkg.name === "@open-agents/cli") {
8224
9048
  currentVersion = pkg.version ?? "0.0.0";
@@ -8286,6 +9110,7 @@ var init_commands = __esm({
8286
9110
  "use strict";
8287
9111
  init_model_picker();
8288
9112
  init_render();
9113
+ init_dist2();
8289
9114
  init_config();
8290
9115
  init_updater();
8291
9116
  }
@@ -8294,9 +9119,9 @@ var init_commands = __esm({
8294
9119
  // packages/cli/dist/tui/setup.js
8295
9120
  import * as readline from "node:readline";
8296
9121
  import { execSync as execSync8 } from "node:child_process";
8297
- import { existsSync as existsSync8, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
8298
- import { join as join13 } from "node:path";
8299
- import { homedir as homedir3 } from "node:os";
9122
+ import { existsSync as existsSync9, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "node:fs";
9123
+ import { join as join14 } from "node:path";
9124
+ import { homedir as homedir4 } from "node:os";
8300
9125
  function detectSystemSpecs() {
8301
9126
  let totalRamGB = 0;
8302
9127
  let availableRamGB = 0;
@@ -8379,8 +9204,8 @@ function modelSupportsToolCalling(modelName) {
8379
9204
  return false;
8380
9205
  }
8381
9206
  function ask(rl, question) {
8382
- return new Promise((resolve14) => {
8383
- rl.question(question, (answer) => resolve14(answer.trim()));
9207
+ return new Promise((resolve15) => {
9208
+ rl.question(question, (answer) => resolve15(answer.trim()));
8384
9209
  });
8385
9210
  }
8386
9211
  function pullModelWithAutoUpdate(tag) {
@@ -8591,10 +9416,10 @@ async function doSetup(config, rl) {
8591
9416
  `PARAMETER num_predict 16384`,
8592
9417
  `PARAMETER stop "<|endoftext|>"`
8593
9418
  ].join("\n");
8594
- const modelDir2 = join13(homedir3(), ".open-agents", "models");
8595
- mkdirSync3(modelDir2, { recursive: true });
8596
- const modelfilePath = join13(modelDir2, `Modelfile.${customName}`);
8597
- writeFileSync3(modelfilePath, modelfileContent + "\n", "utf8");
9419
+ const modelDir2 = join14(homedir4(), ".open-agents", "models");
9420
+ mkdirSync4(modelDir2, { recursive: true });
9421
+ const modelfilePath = join14(modelDir2, `Modelfile.${customName}`);
9422
+ writeFileSync4(modelfilePath, modelfileContent + "\n", "utf8");
8598
9423
  process.stdout.write(` ${c2.dim("Creating model...")} `);
8599
9424
  execSync8(`ollama create ${customName} -f ${modelfilePath}`, {
8600
9425
  stdio: "pipe",
@@ -8639,7 +9464,7 @@ async function isModelAvailable(config) {
8639
9464
  }
8640
9465
  function isFirstRun() {
8641
9466
  try {
8642
- return !existsSync8(join13(homedir3(), ".open-agents", "config.json"));
9467
+ return !existsSync9(join14(homedir4(), ".open-agents", "config.json"));
8643
9468
  } catch {
8644
9469
  return true;
8645
9470
  }
@@ -8679,52 +9504,52 @@ var init_setup = __esm({
8679
9504
  });
8680
9505
 
8681
9506
  // packages/cli/dist/tui/oa-directory.js
8682
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4, readdirSync as readdirSync4, statSync as statSync5 } from "node:fs";
8683
- import { join as join14, relative as relative2, basename as basename2, extname as extname4 } from "node:path";
8684
- import { homedir as homedir4 } from "node:os";
9507
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
9508
+ import { join as join15, relative as relative2, basename as basename2, extname as extname4 } from "node:path";
9509
+ import { homedir as homedir5 } from "node:os";
8685
9510
  function initOaDirectory(repoRoot) {
8686
- const oaPath = join14(repoRoot, OA_DIR);
9511
+ const oaPath = join15(repoRoot, OA_DIR);
8687
9512
  for (const sub of SUBDIRS) {
8688
- mkdirSync4(join14(oaPath, sub), { recursive: true });
9513
+ mkdirSync5(join15(oaPath, sub), { recursive: true });
8689
9514
  }
8690
9515
  return oaPath;
8691
9516
  }
8692
9517
  function hasOaDirectory(repoRoot) {
8693
- return existsSync9(join14(repoRoot, OA_DIR, "index"));
9518
+ return existsSync10(join15(repoRoot, OA_DIR, "index"));
8694
9519
  }
8695
9520
  function loadProjectSettings(repoRoot) {
8696
- const settingsPath = join14(repoRoot, OA_DIR, "settings.json");
9521
+ const settingsPath = join15(repoRoot, OA_DIR, "settings.json");
8697
9522
  try {
8698
- if (existsSync9(settingsPath)) {
8699
- return JSON.parse(readFileSync7(settingsPath, "utf-8"));
9523
+ if (existsSync10(settingsPath)) {
9524
+ return JSON.parse(readFileSync8(settingsPath, "utf-8"));
8700
9525
  }
8701
9526
  } catch {
8702
9527
  }
8703
9528
  return {};
8704
9529
  }
8705
9530
  function saveProjectSettings(repoRoot, settings) {
8706
- const oaPath = join14(repoRoot, OA_DIR);
8707
- mkdirSync4(oaPath, { recursive: true });
9531
+ const oaPath = join15(repoRoot, OA_DIR);
9532
+ mkdirSync5(oaPath, { recursive: true });
8708
9533
  const existing = loadProjectSettings(repoRoot);
8709
9534
  const merged = { ...existing, ...settings };
8710
- writeFileSync4(join14(oaPath, "settings.json"), JSON.stringify(merged, null, 2) + "\n", "utf-8");
9535
+ writeFileSync5(join15(oaPath, "settings.json"), JSON.stringify(merged, null, 2) + "\n", "utf-8");
8711
9536
  }
8712
9537
  function loadGlobalSettings() {
8713
- const settingsPath = join14(homedir4(), ".open-agents", "settings.json");
9538
+ const settingsPath = join15(homedir5(), ".open-agents", "settings.json");
8714
9539
  try {
8715
- if (existsSync9(settingsPath)) {
8716
- return JSON.parse(readFileSync7(settingsPath, "utf-8"));
9540
+ if (existsSync10(settingsPath)) {
9541
+ return JSON.parse(readFileSync8(settingsPath, "utf-8"));
8717
9542
  }
8718
9543
  } catch {
8719
9544
  }
8720
9545
  return {};
8721
9546
  }
8722
9547
  function saveGlobalSettings(settings) {
8723
- const dir = join14(homedir4(), ".open-agents");
8724
- mkdirSync4(dir, { recursive: true });
9548
+ const dir = join15(homedir5(), ".open-agents");
9549
+ mkdirSync5(dir, { recursive: true });
8725
9550
  const existing = loadGlobalSettings();
8726
9551
  const merged = { ...existing, ...settings };
8727
- writeFileSync4(join14(dir, "settings.json"), JSON.stringify(merged, null, 2) + "\n", "utf-8");
9552
+ writeFileSync5(join15(dir, "settings.json"), JSON.stringify(merged, null, 2) + "\n", "utf-8");
8728
9553
  }
8729
9554
  function resolveSettings(repoRoot) {
8730
9555
  const global = loadGlobalSettings();
@@ -8739,12 +9564,12 @@ function discoverContextFiles(repoRoot, maxContentLen = 8e3) {
8739
9564
  while (dir && !visited.has(dir)) {
8740
9565
  visited.add(dir);
8741
9566
  for (const name of CONTEXT_FILES) {
8742
- const filePath = join14(dir, name);
9567
+ const filePath = join15(dir, name);
8743
9568
  const normalizedName = name.toLowerCase();
8744
- if (existsSync9(filePath) && !seen.has(filePath)) {
9569
+ if (existsSync10(filePath) && !seen.has(filePath)) {
8745
9570
  seen.add(filePath);
8746
9571
  try {
8747
- let content = readFileSync7(filePath, "utf-8");
9572
+ let content = readFileSync8(filePath, "utf-8");
8748
9573
  if (content.length > maxContentLen) {
8749
9574
  content = content.slice(0, maxContentLen) + "\n\n...(truncated)";
8750
9575
  }
@@ -8758,11 +9583,11 @@ function discoverContextFiles(repoRoot, maxContentLen = 8e3) {
8758
9583
  }
8759
9584
  }
8760
9585
  }
8761
- const projectMap = join14(dir, OA_DIR, "context", "project-map.md");
8762
- if (existsSync9(projectMap) && !seen.has(projectMap)) {
9586
+ const projectMap = join15(dir, OA_DIR, "context", "project-map.md");
9587
+ if (existsSync10(projectMap) && !seen.has(projectMap)) {
8763
9588
  seen.add(projectMap);
8764
9589
  try {
8765
- let content = readFileSync7(projectMap, "utf-8");
9590
+ let content = readFileSync8(projectMap, "utf-8");
8766
9591
  if (content.length > maxContentLen) {
8767
9592
  content = content.slice(0, maxContentLen) + "\n\n...(truncated)";
8768
9593
  }
@@ -8774,7 +9599,7 @@ function discoverContextFiles(repoRoot, maxContentLen = 8e3) {
8774
9599
  } catch {
8775
9600
  }
8776
9601
  }
8777
- const parent = join14(dir, "..");
9602
+ const parent = join15(dir, "..");
8778
9603
  if (parent === dir)
8779
9604
  break;
8780
9605
  dir = parent;
@@ -8792,9 +9617,9 @@ function discoverContextFiles(repoRoot, maxContentLen = 8e3) {
8792
9617
  return found;
8793
9618
  }
8794
9619
  function readIndexMeta(repoRoot) {
8795
- const metaPath = join14(repoRoot, OA_DIR, "index", "meta.json");
9620
+ const metaPath = join15(repoRoot, OA_DIR, "index", "meta.json");
8796
9621
  try {
8797
- return JSON.parse(readFileSync7(metaPath, "utf-8"));
9622
+ return JSON.parse(readFileSync8(metaPath, "utf-8"));
8798
9623
  } catch {
8799
9624
  return null;
8800
9625
  }
@@ -8845,28 +9670,28 @@ ${tree}\`\`\`
8845
9670
  sections.push("");
8846
9671
  }
8847
9672
  const content = sections.join("\n");
8848
- const contextDir = join14(repoRoot, OA_DIR, "context");
8849
- mkdirSync4(contextDir, { recursive: true });
8850
- writeFileSync4(join14(contextDir, "project-map.md"), content, "utf-8");
9673
+ const contextDir = join15(repoRoot, OA_DIR, "context");
9674
+ mkdirSync5(contextDir, { recursive: true });
9675
+ writeFileSync5(join15(contextDir, "project-map.md"), content, "utf-8");
8851
9676
  return content;
8852
9677
  }
8853
9678
  function saveSession(repoRoot, session) {
8854
- const historyDir = join14(repoRoot, OA_DIR, "history");
8855
- mkdirSync4(historyDir, { recursive: true });
8856
- writeFileSync4(join14(historyDir, `${session.id}.json`), JSON.stringify(session, null, 2), "utf-8");
9679
+ const historyDir = join15(repoRoot, OA_DIR, "history");
9680
+ mkdirSync5(historyDir, { recursive: true });
9681
+ writeFileSync5(join15(historyDir, `${session.id}.json`), JSON.stringify(session, null, 2), "utf-8");
8857
9682
  }
8858
9683
  function loadRecentSessions(repoRoot, limit = 5) {
8859
- const historyDir = join14(repoRoot, OA_DIR, "history");
8860
- if (!existsSync9(historyDir))
9684
+ const historyDir = join15(repoRoot, OA_DIR, "history");
9685
+ if (!existsSync10(historyDir))
8861
9686
  return [];
8862
9687
  try {
8863
- const files = readdirSync4(historyDir).filter((f) => f.endsWith(".json")).map((f) => {
8864
- const stat3 = statSync5(join14(historyDir, f));
9688
+ const files = readdirSync5(historyDir).filter((f) => f.endsWith(".json")).map((f) => {
9689
+ const stat3 = statSync5(join15(historyDir, f));
8865
9690
  return { file: f, mtime: stat3.mtimeMs };
8866
9691
  }).sort((a, b) => b.mtime - a.mtime).slice(0, limit);
8867
9692
  return files.map((f) => {
8868
9693
  try {
8869
- return JSON.parse(readFileSync7(join14(historyDir, f.file), "utf-8"));
9694
+ return JSON.parse(readFileSync8(join15(historyDir, f.file), "utf-8"));
8870
9695
  } catch {
8871
9696
  return null;
8872
9697
  }
@@ -8893,12 +9718,12 @@ function detectManifests(repoRoot) {
8893
9718
  { file: "docker-compose.yaml", type: "Docker Compose" }
8894
9719
  ];
8895
9720
  for (const check of checks) {
8896
- const filePath = join14(repoRoot, check.file);
8897
- if (existsSync9(filePath)) {
9721
+ const filePath = join15(repoRoot, check.file);
9722
+ if (existsSync10(filePath)) {
8898
9723
  let name;
8899
9724
  if (check.nameField) {
8900
9725
  try {
8901
- const data = JSON.parse(readFileSync7(filePath, "utf-8"));
9726
+ const data = JSON.parse(readFileSync8(filePath, "utf-8"));
8902
9727
  name = data[check.nameField];
8903
9728
  } catch {
8904
9729
  }
@@ -8927,7 +9752,7 @@ function findKeyFiles(repoRoot) {
8927
9752
  { pattern: "CLAUDE.md", description: "Claude Code context" }
8928
9753
  ];
8929
9754
  for (const check of checks) {
8930
- if (existsSync9(join14(repoRoot, check.pattern))) {
9755
+ if (existsSync10(join15(repoRoot, check.pattern))) {
8931
9756
  keyFiles.push({ path: check.pattern, description: check.description });
8932
9757
  }
8933
9758
  }
@@ -8938,7 +9763,7 @@ function buildDirTree(root, maxDepth, prefix = "", depth = 0) {
8938
9763
  return "";
8939
9764
  let result = "";
8940
9765
  try {
8941
- const entries = readdirSync4(root, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") || e.name === ".github").filter((e) => !SKIP_DIRS.has(e.name)).sort((a, b) => {
9766
+ const entries = readdirSync5(root, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") || e.name === ".github").filter((e) => !SKIP_DIRS.has(e.name)).sort((a, b) => {
8942
9767
  if (a.isDirectory() && !b.isDirectory())
8943
9768
  return -1;
8944
9769
  if (!a.isDirectory() && b.isDirectory())
@@ -8953,12 +9778,12 @@ function buildDirTree(root, maxDepth, prefix = "", depth = 0) {
8953
9778
  if (entry.isDirectory()) {
8954
9779
  let fileCount = 0;
8955
9780
  try {
8956
- fileCount = readdirSync4(join14(root, entry.name)).filter((f) => !f.startsWith(".")).length;
9781
+ fileCount = readdirSync5(join15(root, entry.name)).filter((f) => !f.startsWith(".")).length;
8957
9782
  } catch {
8958
9783
  }
8959
9784
  result += `${prefix}${connector}${entry.name}/ (${fileCount})
8960
9785
  `;
8961
- result += buildDirTree(join14(root, entry.name), maxDepth, childPrefix, depth + 1);
9786
+ result += buildDirTree(join15(root, entry.name), maxDepth, childPrefix, depth + 1);
8962
9787
  } else if (depth < maxDepth) {
8963
9788
  result += `${prefix}${connector}${entry.name}
8964
9789
  `;
@@ -8973,7 +9798,7 @@ var init_oa_directory = __esm({
8973
9798
  "packages/cli/dist/tui/oa-directory.js"() {
8974
9799
  "use strict";
8975
9800
  OA_DIR = ".oa";
8976
- SUBDIRS = ["memory", "index", "context", "history", "notes", "embedded", "provenance"];
9801
+ SUBDIRS = ["memory", "index", "context", "history", "notes", "embedded", "provenance", "tools"];
8977
9802
  CONTEXT_FILES = [
8978
9803
  "AGENTS.md",
8979
9804
  "OA.md",
@@ -9007,10 +9832,10 @@ var init_oa_directory = __esm({
9007
9832
  });
9008
9833
 
9009
9834
  // packages/cli/dist/tui/project-context.js
9010
- import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync5 } from "node:fs";
9011
- import { join as join15, basename as basename3 } from "node:path";
9835
+ import { existsSync as existsSync11, readFileSync as readFileSync9, readdirSync as readdirSync6 } from "node:fs";
9836
+ import { join as join16, basename as basename3 } from "node:path";
9012
9837
  import { execSync as execSync9 } from "node:child_process";
9013
- import { homedir as homedir5, platform, release } from "node:os";
9838
+ import { homedir as homedir6, platform, release } from "node:os";
9014
9839
  function loadProjectFiles(repoRoot) {
9015
9840
  const discovered = discoverContextFiles(repoRoot);
9016
9841
  if (discovered.length === 0)
@@ -9028,10 +9853,10 @@ function loadProjectMap(repoRoot) {
9028
9853
  if (!hasOaDirectory(repoRoot)) {
9029
9854
  initOaDirectory(repoRoot);
9030
9855
  }
9031
- const mapPath = join15(repoRoot, OA_DIR, "context", "project-map.md");
9032
- if (existsSync10(mapPath)) {
9856
+ const mapPath = join16(repoRoot, OA_DIR, "context", "project-map.md");
9857
+ if (existsSync11(mapPath)) {
9033
9858
  try {
9034
- const content = readFileSync8(mapPath, "utf-8");
9859
+ const content = readFileSync9(mapPath, "utf-8");
9035
9860
  return content;
9036
9861
  } catch {
9037
9862
  }
@@ -9072,31 +9897,31 @@ ${log}`);
9072
9897
  }
9073
9898
  function loadMemoryContext(repoRoot) {
9074
9899
  const sections = [];
9075
- const oaMemDir = join15(repoRoot, OA_DIR, "memory");
9900
+ const oaMemDir = join16(repoRoot, OA_DIR, "memory");
9076
9901
  const oaEntries = loadMemoryDir(oaMemDir, "project");
9077
9902
  if (oaEntries)
9078
9903
  sections.push(oaEntries);
9079
- const legacyMemDir = join15(repoRoot, ".open-agents", "memory");
9080
- if (legacyMemDir !== oaMemDir && existsSync10(legacyMemDir)) {
9904
+ const legacyMemDir = join16(repoRoot, ".open-agents", "memory");
9905
+ if (legacyMemDir !== oaMemDir && existsSync11(legacyMemDir)) {
9081
9906
  const legacyEntries = loadMemoryDir(legacyMemDir, "project/legacy");
9082
9907
  if (legacyEntries)
9083
9908
  sections.push(legacyEntries);
9084
9909
  }
9085
- const globalMemDir = join15(homedir5(), ".open-agents", "memory");
9910
+ const globalMemDir = join16(homedir6(), ".open-agents", "memory");
9086
9911
  const globalEntries = loadMemoryDir(globalMemDir, "global");
9087
9912
  if (globalEntries)
9088
9913
  sections.push(globalEntries);
9089
9914
  return sections.join("\n\n");
9090
9915
  }
9091
9916
  function loadMemoryDir(memDir, scope) {
9092
- if (!existsSync10(memDir))
9917
+ if (!existsSync11(memDir))
9093
9918
  return "";
9094
9919
  const lines = [];
9095
9920
  try {
9096
- const files = readdirSync5(memDir).filter((f) => f.endsWith(".json"));
9921
+ const files = readdirSync6(memDir).filter((f) => f.endsWith(".json"));
9097
9922
  for (const file of files.slice(0, 10)) {
9098
9923
  try {
9099
- const raw = readFileSync8(join15(memDir, file), "utf-8");
9924
+ const raw = readFileSync9(join16(memDir, file), "utf-8");
9100
9925
  const entries = JSON.parse(raw);
9101
9926
  const topic = basename3(file, ".json");
9102
9927
  const keys = Object.keys(entries);
@@ -9186,6 +10011,31 @@ function loadFailurePatterns(store) {
9186
10011
  return "";
9187
10012
  }
9188
10013
  }
10014
+ function loadPatternSuggestions(repoRoot, store) {
10015
+ try {
10016
+ const projectPatterns = store.detectPatterns({ repoRoot, minOccurrences: 3, minLen: 3 });
10017
+ const globalPatterns = store.detectPatterns({ minOccurrences: 3, minLen: 3 });
10018
+ const all = [...projectPatterns];
10019
+ for (const gp of globalPatterns) {
10020
+ const fp = gp.pattern.map((e) => e.tool).join("->");
10021
+ const exists = all.some((p) => p.pattern.map((e) => e.tool).join("->") === fp);
10022
+ if (!exists)
10023
+ all.push(gp);
10024
+ }
10025
+ if (all.length === 0)
10026
+ return "";
10027
+ const lines = [
10028
+ "Repeated workflow patterns detected (consider creating custom tools with create_tool):"
10029
+ ];
10030
+ for (const p of all.slice(0, 3)) {
10031
+ const steps = p.pattern.map((e) => e.tool).join(" \u2192 ");
10032
+ lines.push(`- [${p.occurrences}x, ${p.scope}] ${steps}`);
10033
+ }
10034
+ return lines.join("\n");
10035
+ } catch {
10036
+ return "";
10037
+ }
10038
+ }
9189
10039
  function buildProjectContext(repoRoot, stores) {
9190
10040
  return {
9191
10041
  projectInstructions: loadProjectFiles(repoRoot),
@@ -9195,7 +10045,8 @@ function buildProjectContext(repoRoot, stores) {
9195
10045
  sessionHistory: loadSessionHistory(repoRoot),
9196
10046
  environment: getEnvironment(repoRoot),
9197
10047
  taskMemories: stores?.taskMemoryStore ? loadTaskMemories(repoRoot, stores.taskMemoryStore) : "",
9198
- failurePatterns: stores?.failureStore ? loadFailurePatterns(stores.failureStore) : ""
10048
+ failurePatterns: stores?.failureStore ? loadFailurePatterns(stores.failureStore) : "",
10049
+ patternSuggestions: stores?.toolPatternStore ? loadPatternSuggestions(repoRoot, stores.toolPatternStore) : ""
9199
10050
  };
9200
10051
  }
9201
10052
  function formatContextForPrompt(ctx) {
@@ -9243,6 +10094,13 @@ Use this history to avoid re-doing completed work and to learn from past approac
9243
10094
  ${ctx.failurePatterns}
9244
10095
 
9245
10096
  Avoid approaches that led to these failures. If you encounter these errors, try a different strategy.`);
10097
+ }
10098
+ if (ctx.patternSuggestions) {
10099
+ sections.push(`## Tool Creation Suggestions
10100
+
10101
+ ${ctx.patternSuggestions}
10102
+
10103
+ These patterns have been repeated 3+ times. Consider using create_tool to automate them.`);
9246
10104
  }
9247
10105
  return sections.join("\n\n");
9248
10106
  }
@@ -9587,6 +10445,189 @@ var init_validationStore = __esm({
9587
10445
  }
9588
10446
  });
9589
10447
 
10448
+ // packages/memory/dist/toolPatternStore.js
10449
+ import { randomUUID as randomUUID3 } from "node:crypto";
10450
+ var ToolPatternStore;
10451
+ var init_toolPatternStore = __esm({
10452
+ "packages/memory/dist/toolPatternStore.js"() {
10453
+ "use strict";
10454
+ ToolPatternStore = class {
10455
+ db;
10456
+ constructor(db) {
10457
+ this.db = db;
10458
+ this.ensureTable();
10459
+ }
10460
+ ensureTable() {
10461
+ this.db.exec(`
10462
+ CREATE TABLE IF NOT EXISTS tool_sequences (
10463
+ id TEXT PRIMARY KEY,
10464
+ task_id TEXT NOT NULL,
10465
+ session_id TEXT NOT NULL,
10466
+ repo_root TEXT NOT NULL,
10467
+ sequence TEXT NOT NULL DEFAULT '[]',
10468
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
10469
+ );
10470
+
10471
+ CREATE INDEX IF NOT EXISTS idx_tool_sequences_repo
10472
+ ON tool_sequences (repo_root);
10473
+
10474
+ CREATE INDEX IF NOT EXISTS idx_tool_sequences_created
10475
+ ON tool_sequences (created_at);
10476
+
10477
+ CREATE TABLE IF NOT EXISTS custom_tools (
10478
+ name TEXT PRIMARY KEY,
10479
+ definition TEXT NOT NULL,
10480
+ scope TEXT NOT NULL DEFAULT 'project',
10481
+ repo_root TEXT,
10482
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
10483
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
10484
+ );
10485
+
10486
+ CREATE INDEX IF NOT EXISTS idx_custom_tools_scope
10487
+ ON custom_tools (scope);
10488
+ `);
10489
+ }
10490
+ /** Record a tool call sequence from a completed task */
10491
+ insert(input) {
10492
+ const id = randomUUID3();
10493
+ this.db.prepare(`
10494
+ INSERT INTO tool_sequences (id, task_id, session_id, repo_root, sequence)
10495
+ VALUES (?, ?, ?, ?, ?)
10496
+ `).run(id, input.taskId, input.sessionId, input.repoRoot, JSON.stringify(input.sequence));
10497
+ return id;
10498
+ }
10499
+ /** Get all sequences for a given repo */
10500
+ listByRepo(repoRoot) {
10501
+ const rows = this.db.prepare(`
10502
+ SELECT * FROM tool_sequences WHERE repo_root = ? ORDER BY created_at DESC
10503
+ `).all(repoRoot);
10504
+ return rows.map(this.hydrate);
10505
+ }
10506
+ /** Get recent sequences globally */
10507
+ recent(limit = 20) {
10508
+ const rows = this.db.prepare(`
10509
+ SELECT * FROM tool_sequences ORDER BY created_at DESC LIMIT ?
10510
+ `).all(limit);
10511
+ return rows.map(this.hydrate);
10512
+ }
10513
+ /**
10514
+ * Detect repeated patterns across recorded sequences.
10515
+ * Looks for subsequences of length >= minLen that appear in >= minOccurrences tasks.
10516
+ *
10517
+ * Algorithm:
10518
+ * 1. Extract all contiguous subsequences of length minLen..maxLen
10519
+ * 2. Fingerprint each subsequence (tool names + arg keys)
10520
+ * 3. Count occurrences across tasks
10521
+ * 4. Return patterns that meet the threshold
10522
+ */
10523
+ detectPatterns(opts = {}) {
10524
+ const minOccurrences = opts.minOccurrences ?? 3;
10525
+ const minLen = opts.minLen ?? 3;
10526
+ const maxLen = opts.maxLen ?? 10;
10527
+ const sequences = opts.repoRoot ? this.listByRepo(opts.repoRoot) : this.recent(50);
10528
+ if (sequences.length < minOccurrences)
10529
+ return [];
10530
+ const fingerprints = /* @__PURE__ */ new Map();
10531
+ for (const seq of sequences) {
10532
+ const entries = seq.sequence;
10533
+ for (let len = minLen; len <= Math.min(maxLen, entries.length); len++) {
10534
+ for (let start = 0; start <= entries.length - len; start++) {
10535
+ const sub = entries.slice(start, start + len);
10536
+ const fp = this.fingerprint(sub);
10537
+ if (!fingerprints.has(fp)) {
10538
+ fingerprints.set(fp, { pattern: sub, taskIds: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set() });
10539
+ }
10540
+ const entry = fingerprints.get(fp);
10541
+ entry.taskIds.add(seq.taskId);
10542
+ entry.repos.add(seq.repoRoot);
10543
+ }
10544
+ }
10545
+ }
10546
+ const suggestions = [];
10547
+ for (const [, data] of fingerprints) {
10548
+ if (data.taskIds.size >= minOccurrences) {
10549
+ suggestions.push({
10550
+ pattern: data.pattern,
10551
+ occurrences: data.taskIds.size,
10552
+ scope: data.repos.size === 1 ? "project" : "global",
10553
+ taskIds: Array.from(data.taskIds)
10554
+ });
10555
+ }
10556
+ }
10557
+ suggestions.sort((a, b) => b.occurrences - a.occurrences || b.pattern.length - a.pattern.length);
10558
+ return this.deduplicatePatterns(suggestions);
10559
+ }
10560
+ /** Create a fingerprint for a tool call subsequence */
10561
+ fingerprint(entries) {
10562
+ return entries.map((e) => `${e.tool}(${e.argKeys.sort().join(",")})`).join(" -> ");
10563
+ }
10564
+ /** Remove patterns that are strict subsets of longer detected patterns */
10565
+ deduplicatePatterns(suggestions) {
10566
+ const result = [];
10567
+ const kept = /* @__PURE__ */ new Set();
10568
+ for (let i = 0; i < suggestions.length; i++) {
10569
+ const fpI = this.fingerprint(suggestions[i].pattern);
10570
+ let isSubset = false;
10571
+ for (let j = 0; j < suggestions.length; j++) {
10572
+ if (i === j)
10573
+ continue;
10574
+ const fpJ = this.fingerprint(suggestions[j].pattern);
10575
+ if (fpJ.includes(fpI) && fpJ.length > fpI.length) {
10576
+ if (suggestions[j].occurrences >= suggestions[i].occurrences) {
10577
+ isSubset = true;
10578
+ break;
10579
+ }
10580
+ }
10581
+ }
10582
+ if (!isSubset) {
10583
+ result.push(suggestions[i]);
10584
+ kept.add(i);
10585
+ }
10586
+ }
10587
+ return result.slice(0, 5);
10588
+ }
10589
+ // -- Custom tool tracking in DB -----------------------------------------
10590
+ /** Record a custom tool in the database */
10591
+ saveCustomTool(name, definition, scope, repoRoot) {
10592
+ this.db.prepare(`
10593
+ INSERT OR REPLACE INTO custom_tools (name, definition, scope, repo_root, updated_at)
10594
+ VALUES (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
10595
+ `).run(name, definition, scope, repoRoot ?? null);
10596
+ }
10597
+ /** List all custom tools */
10598
+ listCustomTools(scope, repoRoot) {
10599
+ let query = "SELECT name, scope, created_at FROM custom_tools";
10600
+ const params = [];
10601
+ if (scope && repoRoot) {
10602
+ query += " WHERE (scope = ? AND repo_root = ?) OR scope = 'global'";
10603
+ params.push(scope, repoRoot);
10604
+ } else if (scope) {
10605
+ query += " WHERE scope = ?";
10606
+ params.push(scope);
10607
+ }
10608
+ query += " ORDER BY created_at DESC";
10609
+ const rows = this.db.prepare(query).all(...params);
10610
+ return rows.map((r) => ({
10611
+ name: r["name"],
10612
+ scope: r["scope"],
10613
+ createdAt: r["created_at"]
10614
+ }));
10615
+ }
10616
+ // -- Helpers -------------------------------------------------------------
10617
+ hydrate(row) {
10618
+ return {
10619
+ id: row["id"],
10620
+ taskId: row["task_id"],
10621
+ sessionId: row["session_id"],
10622
+ repoRoot: row["repo_root"],
10623
+ sequence: JSON.parse(row["sequence"] || "[]"),
10624
+ createdAt: row["created_at"]
10625
+ };
10626
+ }
10627
+ };
10628
+ }
10629
+ });
10630
+
9590
10631
  // packages/memory/dist/index.js
9591
10632
  var init_dist6 = __esm({
9592
10633
  "packages/memory/dist/index.js"() {
@@ -9598,6 +10639,7 @@ var init_dist6 = __esm({
9598
10639
  init_patchHistoryStore();
9599
10640
  init_failureStore();
9600
10641
  init_validationStore();
10642
+ init_toolPatternStore();
9601
10643
  }
9602
10644
  });
9603
10645
 
@@ -9885,19 +10927,19 @@ var init_carousel = __esm({
9885
10927
  });
9886
10928
 
9887
10929
  // packages/cli/dist/tui/voice.js
9888
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, readFileSync as readFileSync9, unlinkSync } from "node:fs";
9889
- import { join as join16 } from "node:path";
9890
- import { homedir as homedir6, tmpdir as tmpdir2, platform as platform2 } from "node:os";
10930
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, readFileSync as readFileSync10, unlinkSync } from "node:fs";
10931
+ import { join as join17 } from "node:path";
10932
+ import { homedir as homedir7, tmpdir as tmpdir2, platform as platform2 } from "node:os";
9891
10933
  import { execSync as execSync10, spawn as nodeSpawn } from "node:child_process";
9892
10934
  import { createRequire } from "node:module";
9893
10935
  function modelDir(id) {
9894
- return join16(MODELS_DIR, id);
10936
+ return join17(MODELS_DIR, id);
9895
10937
  }
9896
10938
  function modelOnnxPath(id) {
9897
- return join16(modelDir(id), "model.onnx");
10939
+ return join17(modelDir(id), "model.onnx");
9898
10940
  }
9899
10941
  function modelConfigPath(id) {
9900
- return join16(modelDir(id), "config.json");
10942
+ return join17(modelDir(id), "config.json");
9901
10943
  }
9902
10944
  function describeToolCall(toolName, args) {
9903
10945
  const path = args["path"];
@@ -9909,6 +10951,8 @@ function describeToolCall(toolName, args) {
9909
10951
  return `Writing ${file}`;
9910
10952
  case "file_edit":
9911
10953
  return `Editing ${file}`;
10954
+ case "file_patch":
10955
+ return `Patching ${file}`;
9912
10956
  case "shell": {
9913
10957
  const cmd = String(args["command"] ?? "");
9914
10958
  if (/npm\s+test|vitest|jest|mocha/.test(cmd))
@@ -9971,6 +11015,10 @@ function describeToolCall(toolName, args) {
9971
11015
  return `Taking screenshot`;
9972
11016
  case "ocr":
9973
11017
  return `Extracting text from image`;
11018
+ case "create_tool":
11019
+ return `Creating custom tool`;
11020
+ case "manage_tools":
11021
+ return `Managing custom tools`;
9974
11022
  default:
9975
11023
  return `Using ${toolName}`;
9976
11024
  }
@@ -10006,8 +11054,8 @@ var init_voice = __esm({
10006
11054
  configUrl: "https://raw.githubusercontent.com/robit-man/combine_overwatch_onnx/main/overwatch.onnx.json"
10007
11055
  }
10008
11056
  };
10009
- VOICE_DIR = join16(homedir6(), ".open-agents", "voice");
10010
- MODELS_DIR = join16(VOICE_DIR, "models");
11057
+ VOICE_DIR = join17(homedir7(), ".open-agents", "voice");
11058
+ MODELS_DIR = join17(VOICE_DIR, "models");
10011
11059
  VoiceEngine = class {
10012
11060
  enabled = false;
10013
11061
  modelId = "glados";
@@ -10166,7 +11214,7 @@ var init_voice = __esm({
10166
11214
  const audioData = result["output"].data;
10167
11215
  if (audioData.length === 0)
10168
11216
  return;
10169
- const wavPath = join16(tmpdir2(), `oa-voice-${Date.now()}.wav`);
11217
+ const wavPath = join17(tmpdir2(), `oa-voice-${Date.now()}.wav`);
10170
11218
  this.writeWav(audioData, this.config.audio.sample_rate, wavPath);
10171
11219
  await this.playWav(wavPath);
10172
11220
  try {
@@ -10246,7 +11294,7 @@ var init_voice = __esm({
10246
11294
  buffer.write("data", 36);
10247
11295
  buffer.writeUInt32LE(dataSize, 40);
10248
11296
  Buffer.from(int16.buffer, int16.byteOffset, int16.byteLength).copy(buffer, 44);
10249
- writeFileSync5(path, buffer);
11297
+ writeFileSync6(path, buffer);
10250
11298
  }
10251
11299
  // -------------------------------------------------------------------------
10252
11300
  // Audio playback (system default speakers)
@@ -10255,7 +11303,7 @@ var init_voice = __esm({
10255
11303
  const cmd = this.getPlayCommand(path);
10256
11304
  if (!cmd)
10257
11305
  return;
10258
- return new Promise((resolve14) => {
11306
+ return new Promise((resolve15) => {
10259
11307
  const child = nodeSpawn(cmd[0], cmd.slice(1), {
10260
11308
  stdio: "ignore",
10261
11309
  detached: false
@@ -10264,12 +11312,12 @@ var init_voice = __esm({
10264
11312
  child.on("close", () => {
10265
11313
  if (this.currentPlayback === child)
10266
11314
  this.currentPlayback = null;
10267
- resolve14();
11315
+ resolve15();
10268
11316
  });
10269
11317
  child.on("error", () => {
10270
11318
  if (this.currentPlayback === child)
10271
11319
  this.currentPlayback = null;
10272
- resolve14();
11320
+ resolve15();
10273
11321
  });
10274
11322
  setTimeout(() => {
10275
11323
  if (this.currentPlayback === child) {
@@ -10279,7 +11327,7 @@ var init_voice = __esm({
10279
11327
  }
10280
11328
  this.currentPlayback = null;
10281
11329
  }
10282
- resolve14();
11330
+ resolve15();
10283
11331
  }, 15e3);
10284
11332
  });
10285
11333
  }
@@ -10318,30 +11366,30 @@ var init_voice = __esm({
10318
11366
  async ensureRuntime() {
10319
11367
  if (this.ort)
10320
11368
  return;
10321
- mkdirSync5(VOICE_DIR, { recursive: true });
10322
- const pkgPath = join16(VOICE_DIR, "package.json");
11369
+ mkdirSync6(VOICE_DIR, { recursive: true });
11370
+ const pkgPath = join17(VOICE_DIR, "package.json");
10323
11371
  const expectedDeps = {
10324
11372
  "onnxruntime-node": "^1.21.0",
10325
11373
  "phonemizer": "^1.2.1"
10326
11374
  };
10327
- if (existsSync11(pkgPath)) {
11375
+ if (existsSync12(pkgPath)) {
10328
11376
  try {
10329
- const existing = JSON.parse(readFileSync9(pkgPath, "utf8"));
11377
+ const existing = JSON.parse(readFileSync10(pkgPath, "utf8"));
10330
11378
  if (!existing.dependencies?.["phonemizer"]) {
10331
11379
  existing.dependencies = { ...existing.dependencies, ...expectedDeps };
10332
- writeFileSync5(pkgPath, JSON.stringify(existing, null, 2));
11380
+ writeFileSync6(pkgPath, JSON.stringify(existing, null, 2));
10333
11381
  }
10334
11382
  } catch {
10335
11383
  }
10336
11384
  }
10337
- if (!existsSync11(pkgPath)) {
10338
- writeFileSync5(pkgPath, JSON.stringify({
11385
+ if (!existsSync12(pkgPath)) {
11386
+ writeFileSync6(pkgPath, JSON.stringify({
10339
11387
  name: "open-agents-voice",
10340
11388
  private: true,
10341
11389
  dependencies: expectedDeps
10342
11390
  }, null, 2));
10343
11391
  }
10344
- const voiceRequire = createRequire(join16(VOICE_DIR, "index.js"));
11392
+ const voiceRequire = createRequire(join17(VOICE_DIR, "index.js"));
10345
11393
  try {
10346
11394
  this.ort = voiceRequire("onnxruntime-node");
10347
11395
  } catch {
@@ -10387,18 +11435,18 @@ Error: ${err instanceof Error ? err.message : String(err)}`);
10387
11435
  const dir = modelDir(id);
10388
11436
  const onnxPath = modelOnnxPath(id);
10389
11437
  const configPath = modelConfigPath(id);
10390
- if (existsSync11(onnxPath) && existsSync11(configPath))
11438
+ if (existsSync12(onnxPath) && existsSync12(configPath))
10391
11439
  return;
10392
- mkdirSync5(dir, { recursive: true });
10393
- if (!existsSync11(configPath)) {
11440
+ mkdirSync6(dir, { recursive: true });
11441
+ if (!existsSync12(configPath)) {
10394
11442
  renderInfo(`Downloading ${model.label} voice config...`);
10395
11443
  const configResp = await fetch(model.configUrl);
10396
11444
  if (!configResp.ok)
10397
11445
  throw new Error(`Failed to download config: HTTP ${configResp.status}`);
10398
11446
  const configText = await configResp.text();
10399
- writeFileSync5(configPath, configText);
11447
+ writeFileSync6(configPath, configText);
10400
11448
  }
10401
- if (!existsSync11(onnxPath)) {
11449
+ if (!existsSync12(onnxPath)) {
10402
11450
  renderInfo(`Downloading ${model.label} voice model (this may take a minute)...`);
10403
11451
  const onnxResp = await fetch(model.onnxUrl);
10404
11452
  if (!onnxResp.ok)
@@ -10422,7 +11470,7 @@ Error: ${err instanceof Error ? err.message : String(err)}`);
10422
11470
  }
10423
11471
  process.stdout.write("\r" + " ".repeat(60) + "\r");
10424
11472
  const fullBuffer = Buffer.concat(chunks);
10425
- writeFileSync5(onnxPath, fullBuffer);
11473
+ writeFileSync6(onnxPath, fullBuffer);
10426
11474
  renderInfo(`${model.label} model downloaded (${formatBytes2(fullBuffer.length)}).`);
10427
11475
  }
10428
11476
  }
@@ -10434,10 +11482,10 @@ Error: ${err instanceof Error ? err.message : String(err)}`);
10434
11482
  throw new Error("ONNX runtime not loaded");
10435
11483
  const onnxPath = modelOnnxPath(this.modelId);
10436
11484
  const configPath = modelConfigPath(this.modelId);
10437
- if (!existsSync11(onnxPath) || !existsSync11(configPath)) {
11485
+ if (!existsSync12(onnxPath) || !existsSync12(configPath)) {
10438
11486
  throw new Error(`Model files not found for ${this.modelId}`);
10439
11487
  }
10440
- this.config = JSON.parse(readFileSync9(configPath, "utf8"));
11488
+ this.config = JSON.parse(readFileSync10(configPath, "utf8"));
10441
11489
  renderInfo("Loading voice model...");
10442
11490
  this.session = await this.ort.InferenceSession.create(onnxPath, {
10443
11491
  executionProviders: ["cpu"],
@@ -10712,23 +11760,23 @@ var init_stream_renderer = __esm({
10712
11760
  // packages/cli/dist/tui/interactive.js
10713
11761
  import * as readline2 from "node:readline";
10714
11762
  import { cwd } from "node:process";
10715
- import { resolve as resolve11, join as join17, dirname as dirname3 } from "node:path";
11763
+ import { resolve as resolve12, join as join18, dirname as dirname2 } from "node:path";
10716
11764
  import { createRequire as createRequire2 } from "node:module";
10717
11765
  import { fileURLToPath } from "node:url";
10718
- import { readFileSync as readFileSync10 } from "node:fs";
10719
- import { existsSync as existsSync12 } from "node:fs";
11766
+ import { readFileSync as readFileSync11 } from "node:fs";
11767
+ import { existsSync as existsSync13 } from "node:fs";
10720
11768
  import { extname as extname5 } from "node:path";
10721
11769
  function getVersion() {
10722
11770
  try {
10723
11771
  const require2 = createRequire2(import.meta.url);
10724
- const thisDir = dirname3(fileURLToPath(import.meta.url));
11772
+ const thisDir = dirname2(fileURLToPath(import.meta.url));
10725
11773
  const candidates = [
10726
- join17(thisDir, "..", "package.json"),
10727
- join17(thisDir, "..", "..", "package.json"),
10728
- join17(thisDir, "..", "..", "..", "package.json")
11774
+ join18(thisDir, "..", "package.json"),
11775
+ join18(thisDir, "..", "..", "package.json"),
11776
+ join18(thisDir, "..", "..", "..", "package.json")
10729
11777
  ];
10730
11778
  for (const pkgPath of candidates) {
10731
- if (existsSync12(pkgPath)) {
11779
+ if (existsSync13(pkgPath)) {
10732
11780
  const pkg = require2(pkgPath);
10733
11781
  if (pkg.name === "open-agents-ai" || pkg.name === "@open-agents/cli") {
10734
11782
  return pkg.version ?? "0.0.0";
@@ -10785,6 +11833,7 @@ function buildTools(repoRoot, config) {
10785
11833
  new AiwgWorkflowTool(repoRoot),
10786
11834
  // Advanced tools (mirroring Claude Code capabilities)
10787
11835
  new BatchEditTool(repoRoot),
11836
+ new FilePatchTool(repoRoot),
10788
11837
  new CodebaseMapTool(repoRoot),
10789
11838
  new DiagnosticTool(repoRoot),
10790
11839
  new GitInfoTool(repoRoot),
@@ -10796,7 +11845,12 @@ function buildTools(repoRoot, config) {
10796
11845
  // Image tools (multimodal context)
10797
11846
  new ImageReadTool(repoRoot),
10798
11847
  new ScreenshotTool(repoRoot),
10799
- new OCRTool(repoRoot)
11848
+ new OCRTool(repoRoot),
11849
+ // Custom tool system (agent-created tools)
11850
+ new CreateToolTool(repoRoot),
11851
+ new ManageToolsTool(repoRoot),
11852
+ // Load agent-created custom tools from .oa/tools/ and ~/.open-agents/tools/
11853
+ ...buildCustomTools(repoRoot)
10800
11854
  ];
10801
11855
  return [
10802
11856
  ...executionTools.map(adaptTool),
@@ -10893,9 +11947,14 @@ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce
10893
11947
  });
10894
11948
  runner.registerTools(buildTools(repoRoot, config));
10895
11949
  const filesTouched = /* @__PURE__ */ new Set();
11950
+ const toolSequence = [];
10896
11951
  runner.onEvent((event) => {
10897
11952
  switch (event.type) {
10898
11953
  case "tool_call":
11954
+ toolSequence.push({
11955
+ tool: event.toolName ?? "unknown",
11956
+ argKeys: Object.keys(event.toolArgs ?? {})
11957
+ });
10899
11958
  if (event.toolArgs?.path && typeof event.toolArgs.path === "string") {
10900
11959
  const name = event.toolName ?? "";
10901
11960
  if (name === "file_write" || name === "file_edit" || name === "batch_edit") {
@@ -10993,11 +12052,22 @@ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce
10993
12052
  } catch {
10994
12053
  }
10995
12054
  }
12055
+ if (taskStores?.toolPatternStore && toolSequence.length > 0) {
12056
+ try {
12057
+ taskStores.toolPatternStore.insert({
12058
+ taskId: sessionId,
12059
+ sessionId,
12060
+ repoRoot,
12061
+ sequence: toolSequence
12062
+ });
12063
+ } catch {
12064
+ }
12065
+ }
10996
12066
  });
10997
12067
  return { runner, promise };
10998
12068
  }
10999
12069
  async function startInteractive(config, repoPath) {
11000
- const repoRoot = resolve11(repoPath ?? cwd());
12070
+ const repoRoot = resolve12(repoPath ?? cwd());
11001
12071
  const isResumed = !!process.env.__OA_RESUMED;
11002
12072
  if (isResumed) {
11003
12073
  delete process.env.__OA_RESUMED;
@@ -11007,12 +12077,14 @@ async function startInteractive(config, repoPath) {
11007
12077
  let memoryDb = null;
11008
12078
  let taskMemoryStore = null;
11009
12079
  let failureStore = null;
12080
+ let toolPatternStore = null;
11010
12081
  let contextStores;
11011
12082
  try {
11012
12083
  memoryDb = initDb(config.dbPath);
11013
12084
  taskMemoryStore = new TaskMemoryStore(memoryDb);
11014
12085
  failureStore = new FailureStore(memoryDb);
11015
- contextStores = { taskMemoryStore, failureStore };
12086
+ toolPatternStore = new ToolPatternStore(memoryDb);
12087
+ contextStores = { taskMemoryStore, failureStore, toolPatternStore };
11016
12088
  } catch {
11017
12089
  }
11018
12090
  if (savedSettings.model)
@@ -11124,6 +12196,7 @@ async function startInteractive(config, repoPath) {
11124
12196
  get config() {
11125
12197
  return currentConfig;
11126
12198
  },
12199
+ repoRoot,
11127
12200
  setModel(model) {
11128
12201
  currentConfig = { ...currentConfig, model };
11129
12202
  },
@@ -11208,12 +12281,12 @@ ${c2.dim("Goodbye!")}
11208
12281
  }
11209
12282
  }
11210
12283
  const cleanPath = input.replace(/^['"]|['"]$/g, "").trim();
11211
- const isImage = isImagePath(cleanPath) && existsSync12(resolve11(repoRoot, cleanPath));
12284
+ const isImage = isImagePath(cleanPath) && existsSync13(resolve12(repoRoot, cleanPath));
11212
12285
  if (activeTask) {
11213
12286
  if (isImage) {
11214
12287
  try {
11215
- const imgPath = resolve11(repoRoot, cleanPath);
11216
- const imgBuffer = readFileSync10(imgPath);
12288
+ const imgPath = resolve12(repoRoot, cleanPath);
12289
+ const imgBuffer = readFileSync11(imgPath);
11217
12290
  const base64 = imgBuffer.toString("base64");
11218
12291
  const ext = extname5(cleanPath).toLowerCase();
11219
12292
  const mime = ext === ".png" ? "image/png" : ext === ".gif" ? "image/gif" : ext === ".webp" ? "image/webp" : "image/jpeg";
@@ -11250,7 +12323,8 @@ ${c2.dim("Goodbye!")}
11250
12323
  }, {
11251
12324
  contextStores,
11252
12325
  taskMemoryStore: taskMemoryStore ?? void 0,
11253
- failureStore: failureStore ?? void 0
12326
+ failureStore: failureStore ?? void 0,
12327
+ toolPatternStore: toolPatternStore ?? void 0
11254
12328
  }, bruteForceEnabled);
11255
12329
  activeTask = task;
11256
12330
  showPrompt();
@@ -11322,7 +12396,7 @@ ${c2.dim("(Use /quit to exit)")}
11322
12396
  });
11323
12397
  }
11324
12398
  async function runWithTUI(task, config, repoPath) {
11325
- const repoRoot = resolve11(repoPath ?? cwd());
12399
+ const repoRoot = resolve12(repoPath ?? cwd());
11326
12400
  const needsSetup = isFirstRun() || !await isModelAvailable(config);
11327
12401
  if (needsSetup && config.backendType === "ollama") {
11328
12402
  const setupModel = await runSetupWizard(config);
@@ -11402,9 +12476,9 @@ var init_run = __esm({
11402
12476
  // packages/indexer/dist/codebase-indexer.js
11403
12477
  import { glob } from "glob";
11404
12478
  import ignore from "ignore";
11405
- import { readFile as readFile8, stat as stat2 } from "node:fs/promises";
12479
+ import { readFile as readFile9, stat as stat2 } from "node:fs/promises";
11406
12480
  import { createHash } from "node:crypto";
11407
- import { join as join18, relative as relative3, extname as extname6, basename as basename4 } from "node:path";
12481
+ import { join as join19, relative as relative3, extname as extname6, basename as basename4 } from "node:path";
11408
12482
  var DEFAULT_EXCLUDE, LANGUAGE_MAP, CodebaseIndexer;
11409
12483
  var init_codebase_indexer = __esm({
11410
12484
  "packages/indexer/dist/codebase-indexer.js"() {
@@ -11448,7 +12522,7 @@ var init_codebase_indexer = __esm({
11448
12522
  const ig = ignore.default();
11449
12523
  if (this.config.respectGitignore) {
11450
12524
  try {
11451
- const gitignoreContent = await readFile8(join18(this.config.rootDir, ".gitignore"), "utf-8");
12525
+ const gitignoreContent = await readFile9(join19(this.config.rootDir, ".gitignore"), "utf-8");
11452
12526
  ig.add(gitignoreContent);
11453
12527
  } catch {
11454
12528
  }
@@ -11463,12 +12537,12 @@ var init_codebase_indexer = __esm({
11463
12537
  for (const relativePath of files) {
11464
12538
  if (ig.ignores(relativePath))
11465
12539
  continue;
11466
- const fullPath = join18(this.config.rootDir, relativePath);
12540
+ const fullPath = join19(this.config.rootDir, relativePath);
11467
12541
  try {
11468
12542
  const fileStat = await stat2(fullPath);
11469
12543
  if (fileStat.size > this.config.maxFileSize)
11470
12544
  continue;
11471
- const content = await readFile8(fullPath);
12545
+ const content = await readFile9(fullPath);
11472
12546
  const hash = createHash("sha256").update(content).digest("hex");
11473
12547
  const ext = extname6(relativePath);
11474
12548
  indexed.push({
@@ -11509,7 +12583,7 @@ var init_codebase_indexer = __esm({
11509
12583
  if (!child) {
11510
12584
  child = {
11511
12585
  name: part,
11512
- path: join18(current.path, part),
12586
+ path: join19(current.path, part),
11513
12587
  type: "directory",
11514
12588
  children: []
11515
12589
  };
@@ -11583,14 +12657,14 @@ var index_repo_exports = {};
11583
12657
  __export(index_repo_exports, {
11584
12658
  indexRepoCommand: () => indexRepoCommand
11585
12659
  });
11586
- import { resolve as resolve12 } from "node:path";
11587
- import { existsSync as existsSync13, statSync as statSync6 } from "node:fs";
12660
+ import { resolve as resolve13 } from "node:path";
12661
+ import { existsSync as existsSync14, statSync as statSync6 } from "node:fs";
11588
12662
  import { cwd as cwd2 } from "node:process";
11589
12663
  async function indexRepoCommand(opts, _config) {
11590
- const repoRoot = resolve12(opts.repoPath ?? cwd2());
12664
+ const repoRoot = resolve13(opts.repoPath ?? cwd2());
11591
12665
  printHeader("Index Repository");
11592
12666
  printInfo(`Indexing: ${repoRoot}`);
11593
- if (!existsSync13(repoRoot)) {
12667
+ if (!existsSync14(repoRoot)) {
11594
12668
  printError(`Path does not exist: ${repoRoot}`);
11595
12669
  process.exit(1);
11596
12670
  }
@@ -11836,8 +12910,8 @@ var config_exports = {};
11836
12910
  __export(config_exports, {
11837
12911
  configCommand: () => configCommand
11838
12912
  });
11839
- import { join as join19, resolve as resolve13 } from "node:path";
11840
- import { homedir as homedir7 } from "node:os";
12913
+ import { join as join20, resolve as resolve14 } from "node:path";
12914
+ import { homedir as homedir8 } from "node:os";
11841
12915
  import { cwd as cwd3 } from "node:process";
11842
12916
  function coerceForSettings(key, value) {
11843
12917
  if (INT_KEYS.has(key))
@@ -11857,7 +12931,7 @@ async function configCommand(opts, config) {
11857
12931
  return handleShow(opts, config);
11858
12932
  }
11859
12933
  function handleShow(opts, config) {
11860
- const repoRoot = resolve13(opts.repoPath ?? cwd3());
12934
+ const repoRoot = resolve14(opts.repoPath ?? cwd3());
11861
12935
  printHeader("Configuration");
11862
12936
  printSection("Active Settings (merged)");
11863
12937
  printKeyValue("backendUrl", config.backendUrl, 2);
@@ -11889,7 +12963,7 @@ function handleShow(opts, config) {
11889
12963
  }
11890
12964
  }
11891
12965
  printSection("Config File");
11892
- printInfo(`~/.open-agents/config.json (${join19(homedir7(), ".open-agents", "config.json")})`);
12966
+ printInfo(`~/.open-agents/config.json (${join20(homedir8(), ".open-agents", "config.json")})`);
11893
12967
  printSection("Priority Chain");
11894
12968
  printInfo(" 1. CLI flags (--model, --backend-url, etc.)");
11895
12969
  printInfo(" 2. Project .oa/settings.json (--local)");
@@ -11922,13 +12996,13 @@ function handleSet(opts, _config) {
11922
12996
  process.exit(1);
11923
12997
  }
11924
12998
  if (opts.local) {
11925
- const repoRoot = resolve13(opts.repoPath ?? cwd3());
12999
+ const repoRoot = resolve14(opts.repoPath ?? cwd3());
11926
13000
  try {
11927
13001
  initOaDirectory(repoRoot);
11928
13002
  const coerced = coerceForSettings(key, value);
11929
13003
  saveProjectSettings(repoRoot, { [key]: coerced });
11930
13004
  printSuccess(`Project override set: ${key} = ${value}`);
11931
- printInfo(`Saved to ${join19(repoRoot, ".oa", "settings.json")}`);
13005
+ printInfo(`Saved to ${join20(repoRoot, ".oa", "settings.json")}`);
11932
13006
  printInfo("This override applies only when running in this workspace.");
11933
13007
  } catch (err) {
11934
13008
  printError(`Failed to save: ${err instanceof Error ? err.message : String(err)}`);
@@ -11988,7 +13062,7 @@ var serve_exports = {};
11988
13062
  __export(serve_exports, {
11989
13063
  serveCommand: () => serveCommand
11990
13064
  });
11991
- import { spawn as spawn3 } from "node:child_process";
13065
+ import { spawn as spawn4 } from "node:child_process";
11992
13066
  async function serveCommand(opts, config) {
11993
13067
  const backendType = config.backendType;
11994
13068
  if (backendType === "ollama") {
@@ -12080,8 +13154,8 @@ async function serveVllm(opts, config) {
12080
13154
  await runVllmServer(args, opts.verbose ?? false);
12081
13155
  }
12082
13156
  async function runVllmServer(args, verbose) {
12083
- return new Promise((resolve14, reject) => {
12084
- const child = spawn3("python", args, {
13157
+ return new Promise((resolve15, reject) => {
13158
+ const child = spawn4("python", args, {
12085
13159
  stdio: verbose ? "inherit" : ["ignore", "pipe", "pipe"],
12086
13160
  env: { ...process.env }
12087
13161
  });
@@ -12115,10 +13189,10 @@ async function runVllmServer(args, verbose) {
12115
13189
  child.once("exit", (code, signal) => {
12116
13190
  if (signal) {
12117
13191
  printInfo(`vLLM server stopped by signal ${signal}`);
12118
- resolve14();
13192
+ resolve15();
12119
13193
  } else if (code === 0) {
12120
13194
  printSuccess("vLLM server exited cleanly");
12121
- resolve14();
13195
+ resolve15();
12122
13196
  } else {
12123
13197
  printError(`vLLM server exited with code ${code}`);
12124
13198
  reject(new Error(`vLLM exited with code ${code}`));
@@ -12146,8 +13220,8 @@ __export(eval_exports, {
12146
13220
  evalCommand: () => evalCommand
12147
13221
  });
12148
13222
  import { tmpdir as tmpdir3 } from "node:os";
12149
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
12150
- import { join as join20 } from "node:path";
13223
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
13224
+ import { join as join21 } from "node:path";
12151
13225
  async function evalCommand(opts, config) {
12152
13226
  const suiteName = opts.suite ?? "basic";
12153
13227
  const suite = SUITES[suiteName];
@@ -12268,9 +13342,9 @@ async function evalCommand(opts, config) {
12268
13342
  process.exit(failed > 0 ? 1 : 0);
12269
13343
  }
12270
13344
  function createTempEvalRepo() {
12271
- const dir = join20(tmpdir3(), `open-agents-eval-${Date.now()}`);
12272
- mkdirSync6(dir, { recursive: true });
12273
- writeFileSync6(join20(dir, "package.json"), JSON.stringify({ name: "eval-repo", version: "0.0.0" }, null, 2) + "\n", "utf8");
13345
+ const dir = join21(tmpdir3(), `open-agents-eval-${Date.now()}`);
13346
+ mkdirSync7(dir, { recursive: true });
13347
+ writeFileSync7(join21(dir, "package.json"), JSON.stringify({ name: "eval-repo", version: "0.0.0" }, null, 2) + "\n", "utf8");
12274
13348
  return dir;
12275
13349
  }
12276
13350
  var BASIC_SUITE, FULL_SUITE, SUITES;
@@ -12330,7 +13404,7 @@ init_updater();
12330
13404
  import { parseArgs as nodeParseArgs2 } from "node:util";
12331
13405
  import { createRequire as createRequire3 } from "node:module";
12332
13406
  import { fileURLToPath as fileURLToPath2 } from "node:url";
12333
- import { dirname as dirname4, join as join21 } from "node:path";
13407
+ import { dirname as dirname3, join as join22 } from "node:path";
12334
13408
 
12335
13409
  // packages/cli/dist/cli.js
12336
13410
  import { createInterface } from "node:readline";
@@ -12437,7 +13511,7 @@ init_output();
12437
13511
  function getVersion2() {
12438
13512
  try {
12439
13513
  const require2 = createRequire3(import.meta.url);
12440
- const pkgPath = join21(dirname4(fileURLToPath2(import.meta.url)), "..", "package.json");
13514
+ const pkgPath = join22(dirname3(fileURLToPath2(import.meta.url)), "..", "package.json");
12441
13515
  const pkg = require2(pkgPath);
12442
13516
  return pkg.version;
12443
13517
  } catch {