skyloom 1.16.2 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +15 -3
  2. package/dist/cli/loom_chat.d.ts.map +1 -1
  3. package/dist/cli/loom_chat.js +17 -0
  4. package/dist/cli/loom_chat.js.map +1 -1
  5. package/dist/cli/main.js +37 -1
  6. package/dist/cli/main.js.map +1 -1
  7. package/dist/core/agent.d.ts +2 -0
  8. package/dist/core/agent.d.ts.map +1 -1
  9. package/dist/core/agent.js +21 -5
  10. package/dist/core/agent.js.map +1 -1
  11. package/dist/core/bgproc.d.ts +59 -0
  12. package/dist/core/bgproc.d.ts.map +1 -0
  13. package/dist/core/bgproc.js +135 -0
  14. package/dist/core/bgproc.js.map +1 -0
  15. package/dist/core/commands.d.ts.map +1 -1
  16. package/dist/core/commands.js +20 -0
  17. package/dist/core/commands.js.map +1 -1
  18. package/dist/core/diagnostics.d.ts +39 -0
  19. package/dist/core/diagnostics.d.ts.map +1 -0
  20. package/dist/core/diagnostics.js +206 -0
  21. package/dist/core/diagnostics.js.map +1 -0
  22. package/dist/core/diff.d.ts +31 -0
  23. package/dist/core/diff.d.ts.map +1 -0
  24. package/dist/core/diff.js +82 -0
  25. package/dist/core/diff.js.map +1 -0
  26. package/dist/core/envcontext.d.ts +25 -0
  27. package/dist/core/envcontext.d.ts.map +1 -0
  28. package/dist/core/envcontext.js +112 -0
  29. package/dist/core/envcontext.js.map +1 -0
  30. package/dist/core/factory.d.ts +2 -0
  31. package/dist/core/factory.d.ts.map +1 -1
  32. package/dist/core/factory.js +35 -2
  33. package/dist/core/factory.js.map +1 -1
  34. package/dist/core/patch.d.ts +59 -0
  35. package/dist/core/patch.d.ts.map +1 -0
  36. package/dist/core/patch.js +220 -0
  37. package/dist/core/patch.js.map +1 -0
  38. package/dist/core/protocol.d.ts +11 -0
  39. package/dist/core/protocol.d.ts.map +1 -0
  40. package/dist/core/protocol.js +39 -0
  41. package/dist/core/protocol.js.map +1 -0
  42. package/dist/core/sandbox.d.ts +1 -0
  43. package/dist/core/sandbox.d.ts.map +1 -1
  44. package/dist/core/sandbox.js +1 -0
  45. package/dist/core/sandbox.js.map +1 -1
  46. package/dist/core/search.d.ts +41 -0
  47. package/dist/core/search.d.ts.map +1 -0
  48. package/dist/core/search.js +156 -0
  49. package/dist/core/search.js.map +1 -0
  50. package/dist/core/security.d.ts +22 -2
  51. package/dist/core/security.d.ts.map +1 -1
  52. package/dist/core/security.js +55 -24
  53. package/dist/core/security.js.map +1 -1
  54. package/dist/core/skill.d.ts +4 -0
  55. package/dist/core/skill.d.ts.map +1 -1
  56. package/dist/core/skill.js +1 -0
  57. package/dist/core/skill.js.map +1 -1
  58. package/dist/core/subagent.d.ts +75 -0
  59. package/dist/core/subagent.d.ts.map +1 -0
  60. package/dist/core/subagent.js +287 -0
  61. package/dist/core/subagent.js.map +1 -0
  62. package/dist/core/tool.d.ts +23 -1
  63. package/dist/core/tool.d.ts.map +1 -1
  64. package/dist/core/tool.js +95 -30
  65. package/dist/core/tool.js.map +1 -1
  66. package/dist/plugins/loader.d.ts +49 -8
  67. package/dist/plugins/loader.d.ts.map +1 -1
  68. package/dist/plugins/loader.js +129 -16
  69. package/dist/plugins/loader.js.map +1 -1
  70. package/dist/tools/builtin.d.ts.map +1 -1
  71. package/dist/tools/builtin.js +183 -17
  72. package/dist/tools/builtin.js.map +1 -1
  73. package/dist/tools/spawn.d.ts +23 -0
  74. package/dist/tools/spawn.d.ts.map +1 -0
  75. package/dist/tools/spawn.js +77 -0
  76. package/dist/tools/spawn.js.map +1 -0
  77. package/docs/OPTIMIZATION_PLAN.md +21 -4
  78. package/package.json +1 -1
  79. package/src/cli/loom_chat.ts +11 -0
  80. package/src/cli/main.ts +31 -1
  81. package/src/core/agent.ts +20 -5
  82. package/src/core/bgproc.ts +153 -0
  83. package/src/core/commands.ts +20 -0
  84. package/src/core/diagnostics.ts +178 -0
  85. package/src/core/diff.ts +98 -0
  86. package/src/core/envcontext.ts +79 -0
  87. package/src/core/factory.ts +31 -2
  88. package/src/core/patch.ts +176 -0
  89. package/src/core/protocol.ts +36 -0
  90. package/src/core/sandbox.ts +1 -1
  91. package/src/core/search.ts +138 -0
  92. package/src/core/security.ts +63 -21
  93. package/src/core/skill.ts +1 -1
  94. package/src/core/subagent.ts +272 -0
  95. package/src/core/tool.ts +101 -31
  96. package/src/plugins/loader.ts +145 -18
  97. package/src/tools/builtin.ts +167 -17
  98. package/src/tools/spawn.ts +92 -0
  99. package/tests/bgproc.test.ts +65 -0
  100. package/tests/diagnostics.test.ts +86 -0
  101. package/tests/edit_diff.test.ts +102 -0
  102. package/tests/envcontext.test.ts +67 -0
  103. package/tests/patch.test.ts +128 -0
  104. package/tests/plugins.test.ts +84 -0
  105. package/tests/protocol.test.ts +27 -0
  106. package/tests/search.test.ts +87 -0
  107. package/tests/security.test.ts +87 -0
  108. package/tests/subagent.test.ts +211 -0
  109. package/tests/tool.test.ts +120 -0
@@ -1,7 +1,36 @@
1
1
  /**
2
- * Plugin loader — loads external plugins that register additional tools.
2
+ * Plugin loader — discovers external plugins and runs them through an ordered
3
+ * hook lifecycle.
4
+ *
5
+ * A plugin is a directory with an `index.js` exporting either:
6
+ * - activate(ctx): the lifecycle form. `ctx` scopes every registration to the
7
+ * plugin (registerTool / on(hook, fn)), so unload(name) cleanly removes
8
+ * exactly what the plugin added.
9
+ * - register(registry): the legacy form. Still supported — tools it adds are
10
+ * diffed against the registry so they're tracked for unload too.
11
+ *
12
+ * Hooks fire in registration order. Core hooks: `init` (after all plugins
13
+ * load), `tool.register` (a tool was added), `provider.update` (model/provider
14
+ * config changed). Plugins may define and emit their own hook names.
3
15
  */
4
- import { ToolRegistry } from '../core/tool';
16
+ import { ToolRegistry, type ToolDefinition } from '../core/tool';
17
+ import { getLogger } from '../core/logger';
18
+ export type PluginHook = 'init' | 'tool.register' | 'provider.update' | string;
19
+ export type HookHandler = (payload?: any) => void | Promise<void>;
20
+ /** Scoped API handed to a plugin's activate(); every call is tracked for unload. */
21
+ export interface PluginContext {
22
+ readonly name: string;
23
+ readonly config: any;
24
+ readonly log: ReturnType<typeof getLogger>;
25
+ registerTool(def: ToolDefinition): void;
26
+ on(hook: PluginHook, handler: HookHandler): void;
27
+ }
28
+ export interface Plugin {
29
+ name?: string;
30
+ activate?(ctx: PluginContext): void | Promise<void>;
31
+ register?(registry: ToolRegistry): void;
32
+ deactivate?(): void | Promise<void>;
33
+ }
5
34
  /**
6
35
  * A plugin path is safe to `require` only if neither it nor (on POSIX) its
7
36
  * permissions allow group/world write — otherwise a less-privileged user could
@@ -11,14 +40,26 @@ import { ToolRegistry } from '../core/tool';
11
40
  export declare function isSafePluginPath(target: string): boolean;
12
41
  export declare class PluginLoader {
13
42
  private toolRegistry;
14
- constructor(toolRegistry: ToolRegistry);
15
- /**
16
- * Load plugins from specified directories.
17
- */
43
+ private config;
44
+ private plugins;
45
+ /** hook name -> handlers in registration order, each tagged with its plugin. */
46
+ private hookHandlers;
47
+ constructor(toolRegistry: ToolRegistry, config?: any);
48
+ /** Load plugins from specified directories. Returns the number activated. */
18
49
  loadFromDirectories(directories: string[]): number;
50
+ private loadDirectory;
19
51
  /**
20
- * Load a single plugin directory.
52
+ * Activate a plugin module under a name. Reactivating an already-loaded name
53
+ * unloads the previous instance first. Returns true if anything registered.
21
54
  */
22
- private loadDirectory;
55
+ activatePlugin(name: string, mod: Plugin): boolean;
56
+ /** Fire a hook; handlers run in registration order. Errors are isolated. */
57
+ emit(hook: PluginHook, payload?: any): Promise<void>;
58
+ /** Unload a plugin: remove its tools and hook handlers, call deactivate. */
59
+ unload(name: string): boolean;
60
+ /** Names of currently loaded plugins. */
61
+ list(): string[];
62
+ /** Number of handlers registered for a hook (for diagnostics/tests). */
63
+ hookCount(hook: PluginHook): number;
23
64
  }
24
65
  //# sourceMappingURL=loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/plugins/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAK5C;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CASxD;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,YAAY,CAAe;gBAEvB,YAAY,EAAE,YAAY;IAItC;;OAEG;IACH,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM;IAQlD;;OAEG;IACH,OAAO,CAAC,aAAa;CAyCtB"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/plugins/loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAI3C,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,eAAe,GAAG,iBAAiB,GAAG,MAAM,CAAC;AAC/E,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAElE,oFAAoF;AACpF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;IAC3C,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAAC;IACxC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,QAAQ,CAAC,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IACxC,UAAU,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AASD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CASxD;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,OAAO,CAAmC;IAClD,gFAAgF;IAChF,OAAO,CAAC,YAAY,CAAiE;gBAEzE,YAAY,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,GAAG;IAKpD,6EAA6E;IAC7E,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM;IAQlD,OAAO,CAAC,aAAa;IAqCrB;;;OAGG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAqDlD,4EAA4E;IACtE,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1D,4EAA4E;IAC5E,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAiB7B,yCAAyC;IACzC,IAAI,IAAI,MAAM,EAAE;IAEhB,wEAAwE;IACxE,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM;CACpC"}
@@ -1,6 +1,18 @@
1
1
  "use strict";
2
2
  /**
3
- * Plugin loader — loads external plugins that register additional tools.
3
+ * Plugin loader — discovers external plugins and runs them through an ordered
4
+ * hook lifecycle.
5
+ *
6
+ * A plugin is a directory with an `index.js` exporting either:
7
+ * - activate(ctx): the lifecycle form. `ctx` scopes every registration to the
8
+ * plugin (registerTool / on(hook, fn)), so unload(name) cleanly removes
9
+ * exactly what the plugin added.
10
+ * - register(registry): the legacy form. Still supported — tools it adds are
11
+ * diffed against the registry so they're tracked for unload too.
12
+ *
13
+ * Hooks fire in registration order. Core hooks: `init` (after all plugins
14
+ * load), `tool.register` (a tool was added), `provider.update` (model/provider
15
+ * config changed). Plugins may define and emit their own hook names.
4
16
  */
5
17
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
18
  if (k2 === undefined) k2 = k;
@@ -62,12 +74,14 @@ function isSafePluginPath(target) {
62
74
  }
63
75
  }
64
76
  class PluginLoader {
65
- constructor(toolRegistry) {
77
+ constructor(toolRegistry, config) {
78
+ this.plugins = new Map();
79
+ /** hook name -> handlers in registration order, each tagged with its plugin. */
80
+ this.hookHandlers = new Map();
66
81
  this.toolRegistry = toolRegistry;
82
+ this.config = config ?? {};
67
83
  }
68
- /**
69
- * Load plugins from specified directories.
70
- */
84
+ /** Load plugins from specified directories. Returns the number activated. */
71
85
  loadFromDirectories(directories) {
72
86
  let total = 0;
73
87
  for (const dir of directories) {
@@ -75,9 +89,6 @@ class PluginLoader {
75
89
  }
76
90
  return total;
77
91
  }
78
- /**
79
- * Load a single plugin directory.
80
- */
81
92
  loadDirectory(dir) {
82
93
  if (!fs.existsSync(dir)) {
83
94
  log.debug('plugin_dir_not_found', { dir });
@@ -85,11 +96,15 @@ class PluginLoader {
85
96
  }
86
97
  let count = 0;
87
98
  try {
88
- const entries = fs.readdirSync(dir);
89
- for (const entry of entries) {
99
+ for (const entry of fs.readdirSync(dir)) {
90
100
  const pluginPath = path.join(dir, entry);
91
- if (!fs.statSync(pluginPath).isDirectory())
101
+ try {
102
+ if (!fs.statSync(pluginPath).isDirectory())
103
+ continue;
104
+ }
105
+ catch {
92
106
  continue;
107
+ }
93
108
  const pluginFile = path.join(pluginPath, 'index.js');
94
109
  if (!fs.existsSync(pluginFile))
95
110
  continue;
@@ -101,12 +116,9 @@ class PluginLoader {
101
116
  continue;
102
117
  }
103
118
  try {
104
- const plugin = require(pluginFile);
105
- if (typeof plugin.register === 'function') {
106
- plugin.register(this.toolRegistry);
119
+ const mod = require(pluginFile);
120
+ if (this.activatePlugin(entry, mod))
107
121
  count++;
108
- log.info('plugin_loaded', { name: entry });
109
- }
110
122
  }
111
123
  catch (e) {
112
124
  log.warn('plugin_load_failed', { name: entry, error: String(e) });
@@ -118,6 +130,107 @@ class PluginLoader {
118
130
  }
119
131
  return count;
120
132
  }
133
+ /**
134
+ * Activate a plugin module under a name. Reactivating an already-loaded name
135
+ * unloads the previous instance first. Returns true if anything registered.
136
+ */
137
+ activatePlugin(name, mod) {
138
+ const pluginName = mod.name || name;
139
+ if (this.plugins.has(pluginName))
140
+ this.unload(pluginName);
141
+ const record = { name: pluginName, module: mod, tools: [], hooks: [] };
142
+ const self = this;
143
+ if (typeof mod.activate === 'function') {
144
+ const ctx = {
145
+ name: pluginName,
146
+ config: this.config,
147
+ log: (0, logger_1.getLogger)(`plugin:${pluginName}`),
148
+ registerTool(def) {
149
+ self.toolRegistry.register(def);
150
+ record.tools.push(def.name);
151
+ void self.emit('tool.register', { plugin: pluginName, tool: def.name });
152
+ },
153
+ on(hook, handler) {
154
+ record.hooks.push({ hook, fn: handler });
155
+ const arr = self.hookHandlers.get(hook) || [];
156
+ arr.push({ plugin: pluginName, fn: handler });
157
+ self.hookHandlers.set(hook, arr);
158
+ },
159
+ };
160
+ try {
161
+ void mod.activate(ctx);
162
+ }
163
+ catch (e) {
164
+ log.warn('plugin_activate_failed', { name: pluginName, error: String(e) });
165
+ this.unload(pluginName);
166
+ return false;
167
+ }
168
+ }
169
+ else if (typeof mod.register === 'function') {
170
+ // Legacy: diff the registry to learn which tools the plugin added.
171
+ const before = new Set(this.toolRegistry.listNames());
172
+ try {
173
+ mod.register(this.toolRegistry);
174
+ }
175
+ catch (e) {
176
+ log.warn('plugin_register_failed', { name: pluginName, error: String(e) });
177
+ return false;
178
+ }
179
+ for (const n of this.toolRegistry.listNames()) {
180
+ if (!before.has(n))
181
+ record.tools.push(n);
182
+ }
183
+ }
184
+ else {
185
+ log.warn('plugin_no_entrypoint', { name: pluginName });
186
+ return false;
187
+ }
188
+ this.plugins.set(pluginName, record);
189
+ log.info('plugin_loaded', { name: pluginName, tools: record.tools.length, hooks: record.hooks.length });
190
+ return true;
191
+ }
192
+ /** Fire a hook; handlers run in registration order. Errors are isolated. */
193
+ async emit(hook, payload) {
194
+ const handlers = this.hookHandlers.get(hook);
195
+ if (!handlers || handlers.length === 0)
196
+ return;
197
+ for (const { plugin, fn } of [...handlers]) {
198
+ try {
199
+ await fn(payload);
200
+ }
201
+ catch (e) {
202
+ log.warn('plugin_hook_failed', { hook, plugin, error: String(e) });
203
+ }
204
+ }
205
+ }
206
+ /** Unload a plugin: remove its tools and hook handlers, call deactivate. */
207
+ unload(name) {
208
+ const record = this.plugins.get(name);
209
+ if (!record)
210
+ return false;
211
+ for (const tool of record.tools)
212
+ this.toolRegistry.unregister(tool);
213
+ for (const [hook, arr] of this.hookHandlers) {
214
+ const filtered = arr.filter((h) => h.plugin !== name);
215
+ if (filtered.length)
216
+ this.hookHandlers.set(hook, filtered);
217
+ else
218
+ this.hookHandlers.delete(hook);
219
+ }
220
+ try {
221
+ record.module.deactivate?.();
222
+ }
223
+ catch (e) {
224
+ log.warn('plugin_deactivate_failed', { name, error: String(e) });
225
+ }
226
+ this.plugins.delete(name);
227
+ log.info('plugin_unloaded', { name });
228
+ return true;
229
+ }
230
+ /** Names of currently loaded plugins. */
231
+ list() { return [...this.plugins.keys()]; }
232
+ /** Number of handlers registered for a hook (for diagnostics/tests). */
233
+ hookCount(hook) { return this.hookHandlers.get(hook)?.length ?? 0; }
121
234
  }
122
235
  exports.PluginLoader = PluginLoader;
123
236
  //# sourceMappingURL=loader.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/plugins/loader.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/plugins/loader.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"builtin.d.ts","sourceRoot":"","sources":["../../src/tools/builtin.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAIlF,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;AAIlE;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAgajE;AAID,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAKrD"}
1
+ {"version":3,"file":"builtin.d.ts","sourceRoot":"","sources":["../../src/tools/builtin.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAQlF,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;AAIlE;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAkjBjE;AAID,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAKrD"}
@@ -50,6 +50,10 @@ Object.defineProperty(exports, "assertFetchAllowed", { enumerable: true, get: fu
50
50
  Object.defineProperty(exports, "fenceRoot", { enumerable: true, get: function () { return guards_1.fenceRoot; } });
51
51
  Object.defineProperty(exports, "fenceCheck", { enumerable: true, get: function () { return guards_1.fenceCheck; } });
52
52
  const websearch_1 = require("./websearch");
53
+ const diff_1 = require("../core/diff");
54
+ const diagnostics_1 = require("../core/diagnostics");
55
+ const search_1 = require("../core/search");
56
+ const patch_1 = require("../core/patch");
53
57
  const log = (0, logger_1.getLogger)('builtin-tools');
54
58
  /**
55
59
  * Register all built-in tools into the given registry.
@@ -116,11 +120,12 @@ function registerBuiltinTools(registry) {
116
120
  });
117
121
  registry.register({
118
122
  name: 'edit_file',
119
- description: 'Edit a file by replacing old_text with new_text. Use this for targeted edits.',
123
+ description: 'Edit a file by replacing an exact occurrence of old_text with new_text. old_text must match the file exactly (including whitespace and indentation) and must be UNIQUE — include enough surrounding context to disambiguate, or set replace_all to change every occurrence. Returns a unified diff of the change.',
120
124
  parameters: [
121
125
  { name: 'path', type: 'string', description: 'Path to the file to edit', required: true },
122
- { name: 'old_text', type: 'string', description: 'Text to search for and replace', required: true },
123
- { name: 'new_text', type: 'string', description: 'Text to replace with', required: true },
126
+ { name: 'old_text', type: 'string', description: 'Exact text to replace (must match the file verbatim, and be unique unless replace_all is set)', required: true },
127
+ { name: 'new_text', type: 'string', description: 'Replacement text (must differ from old_text)', required: true },
128
+ { name: 'replace_all', type: 'boolean', description: 'Replace every occurrence instead of requiring a unique match (default false)', required: false },
124
129
  ],
125
130
  handler: async (params) => {
126
131
  const filePath = path.resolve(params.path);
@@ -129,22 +134,94 @@ function registerBuiltinTools(registry) {
129
134
  return fenced;
130
135
  if (!fs.existsSync(filePath))
131
136
  return `Error: File not found: ${filePath}`;
137
+ const oldText = params.old_text;
138
+ const newText = params.new_text;
139
+ const replaceAll = params.replace_all === true || params.replace_all === 'true';
140
+ if (oldText === newText)
141
+ return 'Error: old_text and new_text are identical — nothing to change.';
132
142
  try {
133
- let content = fs.readFileSync(filePath, 'utf-8');
134
- const oldText = params.old_text;
135
- const newText = params.new_text;
136
- if (!content.includes(oldText)) {
137
- return `Error: old_text not found in file. Searched for: ${oldText.slice(0, 50)}...`;
143
+ const orig = fs.readFileSync(filePath, 'utf-8');
144
+ const n = (0, diff_1.countOccurrences)(orig, oldText);
145
+ if (n === 0) {
146
+ return `Error: old_text not found in ${filePath}. It must match exactly (including whitespace). Searched for: ${JSON.stringify(oldText.slice(0, 80))}`;
138
147
  }
139
- content = content.replace(oldText, newText);
140
- fs.writeFileSync(filePath, content, 'utf-8');
141
- return `Successfully edited ${filePath}`;
148
+ if (n > 1 && !replaceAll) {
149
+ return `Error: old_text appears ${n} times in ${filePath} — the edit is ambiguous. Add more surrounding context to make it unique, or set replace_all=true to change all ${n} occurrences.`;
150
+ }
151
+ // Literal replacement: split/join (replace_all) and a function replacer
152
+ // (single) both avoid String.replace interpreting `$&`/`$1` in new_text.
153
+ const updated = replaceAll
154
+ ? orig.split(oldText).join(newText)
155
+ : orig.replace(oldText, () => newText);
156
+ fs.writeFileSync(filePath, updated, 'utf-8');
157
+ const rel = path.relative(process.cwd(), filePath) || filePath;
158
+ const diff = (0, diff_1.unifiedDiff)(orig, updated, { path: rel, context: 3 });
159
+ const occ = replaceAll ? ` (${n} occurrence${n > 1 ? 's' : ''})` : '';
160
+ const body = diff.text ? `\n${diff.text}` : '';
161
+ return `Successfully edited ${filePath}${occ} · +${diff.stat.added} -${diff.stat.removed}${body}`;
142
162
  }
143
163
  catch (e) {
144
164
  return `Error editing file: ${e}`;
145
165
  }
146
166
  },
147
167
  });
168
+ registry.register({
169
+ name: 'get_diagnostics',
170
+ idempotent: true,
171
+ description: 'Get LSP-style diagnostics (type errors, lint issues with line:col) for a source file. TS/JS work out of the box via the workspace TypeScript; other languages use a configured checker (config.yaml diagnostics map). Call this after editing code to confirm it is error-free, or to locate the root cause of a type error.',
172
+ parameters: [
173
+ { name: 'path', type: 'string', description: 'Path to the source file to check', required: true },
174
+ ],
175
+ handler: async (params) => {
176
+ const filePath = path.resolve(params.path);
177
+ const fenced = (0, guards_1.fenceCheck)(filePath);
178
+ if (fenced)
179
+ return fenced;
180
+ let config = {};
181
+ try {
182
+ const { loadConfig } = require('../core/config');
183
+ config = loadConfig();
184
+ }
185
+ catch { /* defaults */ }
186
+ try {
187
+ const result = (0, diagnostics_1.getDiagnostics)(filePath, config);
188
+ if (!Array.isArray(result))
189
+ return `[diagnostics unavailable] ${result.unavailable}`;
190
+ return (0, diagnostics_1.formatDiagnostics)(path.relative(process.cwd(), filePath) || filePath, result);
191
+ }
192
+ catch (e) {
193
+ return `Error getting diagnostics: ${e}`;
194
+ }
195
+ },
196
+ });
197
+ registry.register({
198
+ name: 'apply_patch',
199
+ description: 'Apply an atomic, multi-file edit in one call — ideal for larger refactors touching several places/files. The whole patch is validated before anything is written, so a bad block aborts cleanly with no half-applied changes. Each SEARCH must match the file exactly and uniquely. Format:\n*** Update File: path\n<<<<<<< SEARCH\nold exact text\n=======\nnew text\n>>>>>>> REPLACE\n(repeat blocks; also *** Add File: path / full content, and *** Delete File: path)',
200
+ parameters: [
201
+ { name: 'patch', type: 'string', description: 'The patch text in the *** Update/Add/Delete File + SEARCH/REPLACE format', required: true },
202
+ ],
203
+ handler: async (params) => {
204
+ let snapshot;
205
+ try {
206
+ const { getFileCheckpoints } = require('../core/file_checkpoint');
207
+ const cp = getFileCheckpoints();
208
+ snapshot = (abs) => { try {
209
+ cp.snapshot(abs);
210
+ }
211
+ catch { /* best-effort */ } };
212
+ }
213
+ catch { /* checkpointing optional */ }
214
+ try {
215
+ return (0, patch_1.applyPatch)(String(params.patch || ''), {
216
+ fenceCheck: (abs) => (0, guards_1.fenceCheck)(abs),
217
+ snapshot,
218
+ });
219
+ }
220
+ catch (e) {
221
+ return `Error applying patch: ${e}`;
222
+ }
223
+ },
224
+ });
148
225
  registry.register({
149
226
  name: 'delete_file',
150
227
  description: 'Delete a file at the given path.',
@@ -222,13 +299,27 @@ function registerBuiltinTools(registry) {
222
299
  // ── Shell Tool ──
223
300
  registry.register({
224
301
  name: 'run_bash',
225
- description: 'Execute a shell command and return its output.',
302
+ description: 'Execute a shell command and return its output. Set background=true for long-running processes (dev servers, watchers, builds): it returns a job id immediately — read its output later with bash_output, stop it with kill_bash.',
226
303
  parameters: [
227
304
  { name: 'command', type: 'string', description: 'Command to execute', required: true },
228
- { name: 'timeout', type: 'number', description: 'Timeout in milliseconds (default: 30000)', required: false },
305
+ { name: 'timeout', type: 'number', description: 'Timeout in milliseconds (default: 30000). Ignored when background=true.', required: false },
306
+ { name: 'background', type: 'boolean', description: 'Run detached in the background and return a job id instead of blocking (default false)', required: false },
229
307
  ],
230
308
  handler: async (params) => {
231
309
  const cmd = params.command;
310
+ const background = params.background === true || params.background === 'true';
311
+ if (background) {
312
+ try {
313
+ const { getBackgroundManager } = require('../core/bgproc');
314
+ const { id, error } = getBackgroundManager().start(cmd);
315
+ if (error)
316
+ return error;
317
+ return `[background job ${id} started] pid running. Use bash_output("${id}") to read output, kill_bash("${id}") to stop, list_bash to see all jobs.`;
318
+ }
319
+ catch (e) {
320
+ return `Error: ${e.message || e}`;
321
+ }
322
+ }
232
323
  const timeout = params.timeout || 30000;
233
324
  try {
234
325
  const { runInSandbox, formatSandboxResult } = require('../core/sandbox');
@@ -241,6 +332,50 @@ function registerBuiltinTools(registry) {
241
332
  },
242
333
  dangerous: true,
243
334
  });
335
+ registry.register({
336
+ name: 'bash_output',
337
+ description: 'Read new output from a background shell job (started by run_bash with background=true) since the last read, plus its current status.',
338
+ parameters: [
339
+ { name: 'id', type: 'string', description: 'The background job id', required: true },
340
+ ],
341
+ handler: async (params) => {
342
+ const { getBackgroundManager } = require('../core/bgproc');
343
+ const r = getBackgroundManager().read(String(params.id || ''));
344
+ if (!r.ok)
345
+ return r.error || 'Error reading background job.';
346
+ const statusLine = `[job ${params.id} · ${r.status}${r.exitCode != null ? ` · exit ${r.exitCode}` : ''}]`;
347
+ const out = r.text && r.text.length ? r.text : '(no new output)';
348
+ return `${statusLine}\n${out}`;
349
+ },
350
+ });
351
+ registry.register({
352
+ name: 'list_bash',
353
+ description: 'List background shell jobs with their status, pid, and runtime.',
354
+ parameters: [],
355
+ handler: async () => {
356
+ const { getBackgroundManager } = require('../core/bgproc');
357
+ const jobs = getBackgroundManager().list();
358
+ if (!jobs.length)
359
+ return 'No background jobs.';
360
+ return jobs.map((j) => {
361
+ const dur = ((j.endedAt || Date.now()) - j.startedAt) / 1000;
362
+ const ex = j.exitCode != null ? ` exit ${j.exitCode}` : '';
363
+ return `${j.id} · ${j.status}${ex} · pid ${j.pid ?? '?'} · ${dur.toFixed(1)}s · ${j.command.slice(0, 60)}`;
364
+ }).join('\n');
365
+ },
366
+ });
367
+ registry.register({
368
+ name: 'kill_bash',
369
+ description: 'Terminate a running background shell job.',
370
+ parameters: [
371
+ { name: 'id', type: 'string', description: 'The background job id to kill', required: true },
372
+ ],
373
+ handler: async (params) => {
374
+ const { getBackgroundManager } = require('../core/bgproc');
375
+ const r = getBackgroundManager().kill(String(params.id || ''));
376
+ return r.ok ? `[job ${params.id} killed]` : (r.error || 'Error killing background job.');
377
+ },
378
+ });
244
379
  // ── HTTP Tools ──
245
380
  registry.register({
246
381
  name: 'http_get',
@@ -419,10 +554,40 @@ function registerBuiltinTools(registry) {
419
554
  dangerous: true,
420
555
  });
421
556
  // ── Utility Tools ──
557
+ registry.register({
558
+ name: 'code_search',
559
+ idempotent: true,
560
+ description: 'Search source code for a regex pattern across files. Returns file:line matches with optional surrounding context. Use this to find where a symbol/string is defined or used. Restrict scope with glob (e.g. "**/*.ts") and add context lines to read around hits.',
561
+ parameters: [
562
+ { name: 'pattern', type: 'string', description: 'Regex (or literal if regex=false) to search for', required: true },
563
+ { name: 'path', type: 'string', description: 'Root directory to search (default: cwd)', required: false },
564
+ { name: 'glob', type: 'string', description: 'Restrict to files matching this glob, e.g. "**/*.ts"', required: false },
565
+ { name: 'context', type: 'number', description: 'Lines of context around each match (default 0)', required: false },
566
+ { name: 'ignore_case', type: 'boolean', description: 'Case-insensitive match (default false)', required: false },
567
+ { name: 'regex', type: 'boolean', description: 'Treat pattern as regex (default true; false = literal substring)', required: false },
568
+ { name: 'max_results', type: 'number', description: 'Max matches to return (default 200)', required: false },
569
+ ],
570
+ handler: async (params) => {
571
+ const root = params.path ? path.resolve(params.path) : process.cwd();
572
+ const fenced = (0, guards_1.fenceCheck)(root);
573
+ if (fenced)
574
+ return fenced;
575
+ const res = (0, search_1.searchCode)({
576
+ pattern: String(params.pattern || ''),
577
+ root,
578
+ glob: params.glob ? String(params.glob) : undefined,
579
+ context: params.context != null ? Number(params.context) : 0,
580
+ ignoreCase: params.ignore_case === true,
581
+ regex: params.regex !== false,
582
+ maxResults: params.max_results != null ? Number(params.max_results) : 200,
583
+ });
584
+ return (0, search_1.formatSearchResult)(res);
585
+ },
586
+ });
422
587
  registry.register({
423
588
  name: 'grep',
424
589
  idempotent: true,
425
- description: 'Search for a pattern in files using ripgrep or grep.',
590
+ description: 'Search for a regex pattern in files using ripgrep/grep, with a built-in fallback when neither is installed. For richer control (glob, context, ignore-case) prefer code_search.',
426
591
  parameters: [
427
592
  { name: 'pattern', type: 'string', description: 'Regex pattern to search for', required: true },
428
593
  { name: 'path', type: 'string', description: 'Directory to search in', required: false },
@@ -448,13 +613,14 @@ function registerBuiltinTools(registry) {
448
613
  return out || 'No matches found.';
449
614
  }
450
615
  catch (e) {
451
- // exit status 1 = ran successfully, zero matches; anything else
452
- // (e.g. binary not installed) falls through to the next variant.
616
+ // exit status 1 = ran successfully, zero matches. Any other failure
617
+ // (e.g. binary not installed) falls through to the next variant, then
618
+ // to the pure-JS engine so search works even with no rg/grep.
453
619
  if (e?.status === 1)
454
620
  return 'No matches found.';
455
621
  }
456
622
  }
457
- return 'No matches found.';
623
+ return (0, search_1.formatSearchResult)((0, search_1.searchCode)({ pattern: pat, root: searchDir, maxResults: 200 }));
458
624
  },
459
625
  });
460
626
  registry.register({