neuro-cli 4.1.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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,564 @@
1
+ // ============================================================
2
+ // NeuroCLI - Custom Tools Loader
3
+ // Loads custom tool definitions from .neuro/tools/ directories
4
+ // Supports JSON configs and JS/TS module exports
5
+ // ============================================================
6
+ import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
7
+ import { join, basename, extname } from 'path';
8
+ import { homedir } from 'os';
9
+ import { execFileSync } from 'child_process';
10
+ // ---------------------------------------------------------------------------
11
+ // Sandbox: restricted globals available to custom tool execute functions
12
+ // ---------------------------------------------------------------------------
13
+ const SANDBOX_ALLOWED_GLOBALS = new Set([
14
+ 'JSON',
15
+ 'Math',
16
+ 'Date',
17
+ 'String',
18
+ 'Number',
19
+ 'Boolean',
20
+ 'Array',
21
+ 'Object',
22
+ 'Map',
23
+ 'Set',
24
+ 'RegExp',
25
+ 'Error',
26
+ 'TypeError',
27
+ 'RangeError',
28
+ 'parseInt',
29
+ 'parseFloat',
30
+ 'isNaN',
31
+ 'isFinite',
32
+ 'encodeURIComponent',
33
+ 'decodeURIComponent',
34
+ 'encodeURI',
35
+ 'decodeURI',
36
+ 'undefined',
37
+ 'null',
38
+ 'NaN',
39
+ 'Infinity',
40
+ ]);
41
+ // ---------------------------------------------------------------------------
42
+ // CustomToolLoader
43
+ // ---------------------------------------------------------------------------
44
+ export class CustomToolLoader {
45
+ tools;
46
+ projectToolsDir;
47
+ globalToolsDir;
48
+ constructor(projectRoot) {
49
+ this.projectToolsDir = join(projectRoot, '.neuro', 'tools');
50
+ this.globalToolsDir = join(homedir(), '.neuro', 'tools');
51
+ this.tools = new Map();
52
+ }
53
+ // -------------------------------------------------------------------------
54
+ // Public API
55
+ // -------------------------------------------------------------------------
56
+ /** Discover and load all custom tools (global then project, project wins). */
57
+ discover() {
58
+ this.tools.clear();
59
+ // Load in order: global -> project (project overrides global)
60
+ this.loadFromDirectory(this.globalToolsDir);
61
+ this.loadFromDirectory(this.projectToolsDir);
62
+ return this.getAll();
63
+ }
64
+ /** Retrieve a single tool definition by name. */
65
+ get(name) {
66
+ return this.tools.get(name);
67
+ }
68
+ /** Return all discovered tool definitions, sorted by name. */
69
+ getAll() {
70
+ return Array.from(this.tools.values()).sort((a, b) => a.name.localeCompare(b.name));
71
+ }
72
+ /**
73
+ * Create an executor function for a tool definition.
74
+ *
75
+ * If the tool defines `execute`, the source is run inside a sandboxed
76
+ * function body with only safe globals. The `args` object is passed as
77
+ * the first argument and the executor must return a string.
78
+ *
79
+ * If the tool defines `command`, the template string has {{arg}} tokens
80
+ * replaced with the corresponding argument values and is executed via
81
+ * a restricted shell invocation.
82
+ */
83
+ createExecutor(def) {
84
+ if (def.execute) {
85
+ return this.createJsExecutor(def);
86
+ }
87
+ if (def.command) {
88
+ return this.createCommandExecutor(def);
89
+ }
90
+ // No execution strategy -- return a fallback that explains the issue
91
+ return async () => `[custom-tools] Tool "${def.name}" has no execute or command definition.`;
92
+ }
93
+ /** Print a human-readable list of all discovered custom tools. */
94
+ printTools() {
95
+ const all = this.getAll();
96
+ // eslint-disable-next-line no-console
97
+ console.log('\n=== Custom Tools ===\n');
98
+ if (all.length === 0) {
99
+ // eslint-disable-next-line no-console
100
+ console.log(' (no custom tools found)\n');
101
+ return;
102
+ }
103
+ for (const tool of all) {
104
+ const riskLabel = tool.risk ? ` [${tool.risk} risk]` : '';
105
+ const mode = tool.execute ? 'js' : tool.command ? 'shell' : 'none';
106
+ const paramCount = Object.keys(tool.parameters.properties).length;
107
+ // eslint-disable-next-line no-console
108
+ console.log(` ${tool.name}${riskLabel} - ${tool.description}`);
109
+ // eslint-disable-next-line no-console
110
+ console.log(` params: ${paramCount} | mode: ${mode} | source: ${tool.source}`);
111
+ if (tool.parameters.required.length > 0) {
112
+ // eslint-disable-next-line no-console
113
+ console.log(` required: ${tool.parameters.required.join(', ')}`);
114
+ }
115
+ }
116
+ // eslint-disable-next-line no-console
117
+ console.log('');
118
+ }
119
+ // -------------------------------------------------------------------------
120
+ // Private: directory loading
121
+ // -------------------------------------------------------------------------
122
+ /** Scan a directory for .json, .js, .mjs, .ts tool definition files. */
123
+ loadFromDirectory(dir) {
124
+ if (!existsSync(dir)) {
125
+ return;
126
+ }
127
+ let entries;
128
+ try {
129
+ const stat = statSync(dir);
130
+ if (!stat.isDirectory()) {
131
+ return;
132
+ }
133
+ entries = readdirSync(dir);
134
+ }
135
+ catch {
136
+ return;
137
+ }
138
+ for (const entry of entries) {
139
+ const filePath = join(dir, entry);
140
+ try {
141
+ const fileStat = statSync(filePath);
142
+ if (!fileStat.isFile()) {
143
+ continue;
144
+ }
145
+ const ext = extname(entry).toLowerCase();
146
+ let tool = null;
147
+ if (ext === '.json') {
148
+ tool = this.loadJsonTool(filePath);
149
+ }
150
+ else if (ext === '.js' || ext === '.mjs' || ext === '.ts') {
151
+ tool = this.loadJsTool(filePath);
152
+ }
153
+ else {
154
+ // Skip unsupported file types
155
+ continue;
156
+ }
157
+ if (tool) {
158
+ // Project tools override global tools with the same name
159
+ this.tools.set(tool.name, tool);
160
+ }
161
+ }
162
+ catch (err) {
163
+ const message = err instanceof Error ? err.message : String(err);
164
+ // eslint-disable-next-line no-console
165
+ console.warn(`[custom-tools] Failed to load tool from "${filePath}": ${message}`);
166
+ }
167
+ }
168
+ }
169
+ // -------------------------------------------------------------------------
170
+ // Private: JSON tool loader
171
+ // -------------------------------------------------------------------------
172
+ /**
173
+ * Load a tool definition from a JSON file.
174
+ *
175
+ * Expected format:
176
+ * {
177
+ * "name": "my-tool",
178
+ * "description": "Does something useful",
179
+ * "parameters": {
180
+ * "type": "object",
181
+ * "properties": { ... },
182
+ * "required": [ ... ]
183
+ * },
184
+ * "risk": "low",
185
+ * "command": "echo {{input}}"
186
+ * }
187
+ */
188
+ loadJsonTool(filePath) {
189
+ const raw = readFileSync(filePath, 'utf-8');
190
+ let parsed;
191
+ try {
192
+ parsed = JSON.parse(raw);
193
+ }
194
+ catch {
195
+ // eslint-disable-next-line no-console
196
+ console.warn(`[custom-tools] Invalid JSON in "${filePath}"`);
197
+ return null;
198
+ }
199
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
200
+ // eslint-disable-next-line no-console
201
+ console.warn(`[custom-tools] Expected a JSON object in "${filePath}"`);
202
+ return null;
203
+ }
204
+ const def = parsed;
205
+ // Derive name from filename if not set
206
+ if (!def.name) {
207
+ const base = basename(filePath, '.json');
208
+ def.name = base.replace(/[^a-zA-Z0-9_-]/g, '-');
209
+ }
210
+ if (!this.validateDefinition(def)) {
211
+ return null;
212
+ }
213
+ return {
214
+ name: def.name,
215
+ description: def.description,
216
+ parameters: def.parameters,
217
+ risk: def.risk,
218
+ execute: def.execute,
219
+ command: def.command,
220
+ source: filePath,
221
+ };
222
+ }
223
+ // -------------------------------------------------------------------------
224
+ // Private: JS/TS tool loader
225
+ // -------------------------------------------------------------------------
226
+ /**
227
+ * Load a tool definition from a JS/TS file.
228
+ *
229
+ * The file should export a `default` or named `toolDefinition` object
230
+ * conforming to CustomToolDefinition (without `source`).
231
+ *
232
+ * Example (ESM):
233
+ * export const toolDefinition = { name: ..., ... };
234
+ * export default toolDefinition;
235
+ *
236
+ * Because we cannot natively import arbitrary ESM at runtime without
237
+ * a dynamic import that may fail on TS files, we take a pragmatic
238
+ * approach: read the file source and attempt to extract the exported
239
+ * definition via a sandboxed evaluation for .js/.mjs, or parse the
240
+ * object literal for .ts files.
241
+ */
242
+ loadJsTool(filePath) {
243
+ const raw = readFileSync(filePath, 'utf-8');
244
+ const ext = extname(filePath).toLowerCase();
245
+ let def = null;
246
+ if (ext === '.ts') {
247
+ // For TypeScript files, strip type annotations naively and
248
+ // attempt to extract the exported object.
249
+ def = this.extractDefinitionFromSource(raw, filePath);
250
+ }
251
+ else {
252
+ // For JS/MJS files, evaluate in a sandboxed context
253
+ def = this.evaluateJsSource(raw, filePath);
254
+ }
255
+ if (!def) {
256
+ return null;
257
+ }
258
+ // Derive name from filename if not set
259
+ if (!def.name) {
260
+ const base = basename(filePath, ext);
261
+ def.name = base.replace(/[^a-zA-Z0-9_-]/g, '-');
262
+ }
263
+ if (!this.validateDefinition(def)) {
264
+ return null;
265
+ }
266
+ return {
267
+ name: def.name,
268
+ description: def.description,
269
+ parameters: def.parameters,
270
+ risk: def.risk,
271
+ execute: def.execute,
272
+ command: def.command,
273
+ source: filePath,
274
+ };
275
+ }
276
+ /**
277
+ * Attempt to extract a tool definition object from source code text.
278
+ * Looks for common export patterns and parses the object literal.
279
+ */
280
+ extractDefinitionFromSource(source, filePath) {
281
+ // Try to find: export const toolDefinition = { ... }
282
+ // or: export default { ... }
283
+ const patterns = [
284
+ // export const toolDefinition = { ... }
285
+ /export\s+const\s+toolDefinition\s*=\s*(\{[\s\S]*\})\s*;?\s*$/,
286
+ // export default { ... }
287
+ /export\s+default\s+(\{[\s\S]*\})\s*;?\s*$/,
288
+ // const toolDefinition = { ... } (no export keyword on same line)
289
+ /const\s+toolDefinition\s*=\s*(\{[\s\S]*\})\s*;?\s*$/,
290
+ ];
291
+ for (const pattern of patterns) {
292
+ const match = source.match(pattern);
293
+ if (match && match[1]) {
294
+ const objStr = this.sanitizeTsObjectLiteral(match[1]);
295
+ try {
296
+ // eslint-disable-next-line no-new-func
297
+ const fn = new Function(`return (${objStr});`);
298
+ const result = fn();
299
+ if (typeof result === 'object' && result !== null) {
300
+ return result;
301
+ }
302
+ }
303
+ catch {
304
+ // eslint-disable-next-line no-console
305
+ console.warn(`[custom-tools] Could not parse exported definition in "${filePath}"`);
306
+ }
307
+ }
308
+ }
309
+ // eslint-disable-next-line no-console
310
+ console.warn(`[custom-tools] No toolDefinition export found in "${filePath}"`);
311
+ return null;
312
+ }
313
+ /**
314
+ * Strip TypeScript-specific syntax from an object literal string
315
+ * so it can be parsed as plain JavaScript.
316
+ */
317
+ sanitizeTsObjectLiteral(src) {
318
+ return src
319
+ .replace(/\bas\s+\w+(\[\])?\b/g, '') // remove "as Type" casts
320
+ .replace(/:\s*\w+(\[\])?\s*([,=}])/g, '$2') // remove type annotations like ": string"
321
+ .replace(/<[^>]+>/g, '') // remove generic type params
322
+ .replace(/\?\s*:/g, ':') // remove optional marker
323
+ .replace(/readonly\s+/g, '') // remove readonly modifier
324
+ .replace(/\/\/.*$/gm, '') // remove single-line comments
325
+ .replace(/\/\*[\s\S]*?\*\//g, ''); // remove multi-line comments
326
+ }
327
+ /**
328
+ * Evaluate a JS source file in a sandboxed context and extract
329
+ * the exported tool definition.
330
+ */
331
+ evaluateJsSource(source, filePath) {
332
+ // Build a sandboxed module-like wrapper
333
+ // We extract the default export or toolDefinition export
334
+ const wrappedSource = `
335
+ "use strict";
336
+ const __exports = {};
337
+ const module = { exports: __exports };
338
+ const exports = __exports;
339
+
340
+ ${source}
341
+
342
+ if (typeof toolDefinition !== 'undefined') {
343
+ return toolDefinition;
344
+ }
345
+ if (module.exports && typeof module.exports === 'object') {
346
+ if (module.exports.default) return module.exports.default;
347
+ if (module.exports.toolDefinition) return module.exports.toolDefinition;
348
+ return module.exports;
349
+ }
350
+ return null;
351
+ `;
352
+ try {
353
+ // eslint-disable-next-line no-new-func
354
+ const fn = new Function(wrappedSource);
355
+ const result = fn();
356
+ if (typeof result === 'object' && result !== null) {
357
+ return result;
358
+ }
359
+ }
360
+ catch (err) {
361
+ const message = err instanceof Error ? err.message : String(err);
362
+ // eslint-disable-next-line no-console
363
+ console.warn(`[custom-tools] Error evaluating "${filePath}": ${message}`);
364
+ }
365
+ return null;
366
+ }
367
+ // -------------------------------------------------------------------------
368
+ // Private: schema validation
369
+ // -------------------------------------------------------------------------
370
+ /**
371
+ * Validate that a partial tool definition has all required fields
372
+ * and that their shapes are correct.
373
+ */
374
+ validateDefinition(def) {
375
+ // Name
376
+ if (!def.name || typeof def.name !== 'string') {
377
+ // eslint-disable-next-line no-console
378
+ console.warn('[custom-tools] Tool definition missing valid "name".');
379
+ return false;
380
+ }
381
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(def.name)) {
382
+ // eslint-disable-next-line no-console
383
+ console.warn(`[custom-tools] Tool name "${def.name}" must start with a letter and contain only letters, digits, hyphens, and underscores.`);
384
+ return false;
385
+ }
386
+ // Description
387
+ if (!def.description || typeof def.description !== 'string') {
388
+ // eslint-disable-next-line no-console
389
+ console.warn(`[custom-tools] Tool "${def.name}" missing valid "description".`);
390
+ return false;
391
+ }
392
+ // Parameters
393
+ if (!def.parameters || typeof def.parameters !== 'object') {
394
+ // eslint-disable-next-line no-console
395
+ console.warn(`[custom-tools] Tool "${def.name}" missing "parameters" object.`);
396
+ return false;
397
+ }
398
+ if (def.parameters.type !== 'object') {
399
+ // eslint-disable-next-line no-console
400
+ console.warn(`[custom-tools] Tool "${def.name}" parameters.type must be "object".`);
401
+ return false;
402
+ }
403
+ if (!def.parameters.properties ||
404
+ typeof def.parameters.properties !== 'object') {
405
+ // eslint-disable-next-line no-console
406
+ console.warn(`[custom-tools] Tool "${def.name}" missing "parameters.properties".`);
407
+ return false;
408
+ }
409
+ if (!Array.isArray(def.parameters.required)) {
410
+ // eslint-disable-next-line no-console
411
+ console.warn(`[custom-tools] Tool "${def.name}" "parameters.required" must be an array.`);
412
+ return false;
413
+ }
414
+ // Validate each property in parameters.properties
415
+ for (const [key, prop] of Object.entries(def.parameters.properties)) {
416
+ if (!prop.type || typeof prop.type !== 'string') {
417
+ // eslint-disable-next-line no-console
418
+ console.warn(`[custom-tools] Tool "${def.name}" parameter "${key}" missing valid "type".`);
419
+ return false;
420
+ }
421
+ if (!prop.description || typeof prop.description !== 'string') {
422
+ // eslint-disable-next-line no-console
423
+ console.warn(`[custom-tools] Tool "${def.name}" parameter "${key}" missing valid "description".`);
424
+ return false;
425
+ }
426
+ if (prop.enum !== undefined && !Array.isArray(prop.enum)) {
427
+ // eslint-disable-next-line no-console
428
+ console.warn(`[custom-tools] Tool "${def.name}" parameter "${key}" has invalid "enum" (must be array).`);
429
+ return false;
430
+ }
431
+ }
432
+ // Validate required entries reference existing properties
433
+ for (const reqKey of def.parameters.required) {
434
+ if (!(reqKey in def.parameters.properties)) {
435
+ // eslint-disable-next-line no-console
436
+ console.warn(`[custom-tools] Tool "${def.name}" required parameter "${reqKey}" not found in properties.`);
437
+ return false;
438
+ }
439
+ }
440
+ // Validate risk level if present
441
+ if (def.risk !== undefined &&
442
+ !['low', 'medium', 'high'].includes(def.risk)) {
443
+ // eslint-disable-next-line no-console
444
+ console.warn(`[custom-tools] Tool "${def.name}" has invalid risk level "${def.risk}". Must be low, medium, or high.`);
445
+ return false;
446
+ }
447
+ // Must have at least one execution strategy
448
+ if (!def.execute && !def.command) {
449
+ // eslint-disable-next-line no-console
450
+ console.warn(`[custom-tools] Tool "${def.name}" must define either "execute" or "command".`);
451
+ return false;
452
+ }
453
+ return true;
454
+ }
455
+ // -------------------------------------------------------------------------
456
+ // Private: executor creation
457
+ // -------------------------------------------------------------------------
458
+ /**
459
+ * Create an executor that runs the tool's JavaScript source in a sandbox.
460
+ *
461
+ * The sandbox restricts access to only safe, deterministic globals.
462
+ * Network access, filesystem, and process APIs are not available.
463
+ */
464
+ createJsExecutor(def) {
465
+ const source = def.execute;
466
+ return async (args) => {
467
+ try {
468
+ // Build a sandboxed function that only has access to allowed globals
469
+ const sandboxKeys = Array.from(SANDBOX_ALLOWED_GLOBALS);
470
+ const sandboxValues = sandboxKeys.map((key) => {
471
+ // eslint-disable-next-line no-eval
472
+ return globalThis[key];
473
+ });
474
+ // Wrap the user source so it receives `args` and must return a string
475
+ const wrappedSource = `
476
+ "use strict";
477
+ const __fn = (function(${sandboxKeys.join(', ')}) {
478
+ return function(args) {
479
+ ${source}
480
+ };
481
+ })(${sandboxKeys.join(', ')});
482
+ return __fn(args);
483
+ `;
484
+ // eslint-disable-next-line no-new-func
485
+ const fn = new Function(wrappedSource);
486
+ const result = fn(args);
487
+ // Coerce result to string
488
+ if (result === undefined || result === null) {
489
+ return '';
490
+ }
491
+ return String(result);
492
+ }
493
+ catch (err) {
494
+ const message = err instanceof Error ? err.message : String(err);
495
+ throw new Error(`[custom-tools] Execution error in tool "${def.name}": ${message}`);
496
+ }
497
+ };
498
+ }
499
+ /**
500
+ * Create an executor that runs a shell command template.
501
+ *
502
+ * Template tokens like {{input}} are replaced with the corresponding
503
+ * argument values. Shell metacharacters in argument values are escaped
504
+ * to reduce injection risk.
505
+ */
506
+ createCommandExecutor(def) {
507
+ const template = def.command;
508
+ return async (args) => {
509
+ // Replace {{key}} placeholders with escaped argument values
510
+ let command = template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
511
+ const value = args[key];
512
+ if (value === undefined || value === null) {
513
+ return '';
514
+ }
515
+ return this.escapeShellArg(String(value));
516
+ });
517
+ // Enforce risk-based restrictions
518
+ if (def.risk === 'high') {
519
+ throw new Error(`[custom-tools] Tool "${def.name}" is marked as high-risk and cannot be executed via shell command.`);
520
+ }
521
+ // Disallow obviously dangerous patterns regardless of risk level
522
+ const dangerousPatterns = [
523
+ /rm\s+-rf\s+\//,
524
+ />\s*\/dev\//,
525
+ /mkfs/,
526
+ /dd\s+if=/,
527
+ /:\s*\(\)\{\s*:\|\:&\s*\}/, // fork bomb
528
+ /curl\s+.*\|\s*(ba)?sh/,
529
+ /wget\s+.*\|\s*(ba)?sh/,
530
+ ];
531
+ for (const pattern of dangerousPatterns) {
532
+ if (pattern.test(command)) {
533
+ throw new Error(`[custom-tools] Command for tool "${def.name}" contains a disallowed pattern.`);
534
+ }
535
+ }
536
+ try {
537
+ const output = execFileSync('sh', ['-c', command], {
538
+ encoding: 'utf-8',
539
+ timeout: 30_000,
540
+ maxBuffer: 1024 * 1024, // 1 MB
541
+ shell: false,
542
+ });
543
+ return output.trim();
544
+ }
545
+ catch (err) {
546
+ if (err instanceof Error) {
547
+ const stderr = err.stderr || err.message;
548
+ throw new Error(`[custom-tools] Shell command failed for tool "${def.name}": ${stderr}`);
549
+ }
550
+ throw new Error(`[custom-tools] Unknown error executing tool "${def.name}".`);
551
+ }
552
+ };
553
+ }
554
+ /**
555
+ * Escape a string for safe inclusion in a shell command argument.
556
+ * Uses single-quoting with internal single-quotes escaped.
557
+ */
558
+ escapeShellArg(value) {
559
+ // Replace embedded single quotes with '\'' (end quote, escaped quote, new quote)
560
+ const escaped = value.replace(/'/g, "'\\''");
561
+ return `'${escaped}'`;
562
+ }
563
+ }
564
+ //# sourceMappingURL=custom-tools.js.map
@@ -0,0 +1,52 @@
1
+ export interface Checkpoint {
2
+ id: string;
3
+ timestamp: number;
4
+ message: string;
5
+ files: string[];
6
+ hash?: string;
7
+ }
8
+ export declare class GitCheckpointSystem {
9
+ private workingDirectory;
10
+ private shadowRepo;
11
+ private autoCommit;
12
+ private checkpoints;
13
+ constructor(workingDirectory: string, autoCommit?: boolean);
14
+ /**
15
+ * Initialize the checkpoint system
16
+ */
17
+ initialize(): boolean;
18
+ /**
19
+ * Create a checkpoint before risky operations
20
+ */
21
+ createCheckpoint(message: string): Checkpoint | null;
22
+ /**
23
+ * Restore to a checkpoint
24
+ */
25
+ restore(checkpointId: string): boolean;
26
+ /**
27
+ * Undo the last checkpoint
28
+ */
29
+ undo(): boolean;
30
+ /**
31
+ * List all checkpoints
32
+ */
33
+ listCheckpoints(): Checkpoint[];
34
+ /**
35
+ * Get changed files since last commit
36
+ */
37
+ getChangedFiles(): string[];
38
+ /**
39
+ * Get diff of changes
40
+ */
41
+ getDiff(): string;
42
+ /**
43
+ * Auto-commit changes with a message
44
+ */
45
+ autoCommitChanges(message: string): string | null;
46
+ private isGitRepo;
47
+ private ensureShadowRepo;
48
+ private saveShadowSnapshot;
49
+ private restoreShadowSnapshot;
50
+ private hashString;
51
+ }
52
+ //# sourceMappingURL=git-checkpoint.d.ts.map