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.
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- 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
|