osai-agent 4.0.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 +7 -0
- package/package.json +72 -0
- package/src/agent/context.js +141 -0
- package/src/agent/loop/context-summary.js +196 -0
- package/src/agent/loop/directory-utils.js +102 -0
- package/src/agent/loop/local.js +196 -0
- package/src/agent/loop/loop-detection.js +288 -0
- package/src/agent/loop/stream-parser.js +515 -0
- package/src/agent/loop/tool-executor.js +470 -0
- package/src/agent/loop/verification.js +263 -0
- package/src/agent/loop/websocket.js +80 -0
- package/src/agent/prompt.js +259 -0
- package/src/agent/react-loop.js +697 -0
- package/src/agent/subagent.js +263 -0
- package/src/commands/config.js +53 -0
- package/src/commands/connect.js +190 -0
- package/src/commands/devices.js +121 -0
- package/src/commands/login.js +77 -0
- package/src/commands/logout.js +31 -0
- package/src/commands/mcp.js +258 -0
- package/src/commands/provider.js +633 -0
- package/src/commands/register.js +74 -0
- package/src/commands/run.js +150 -0
- package/src/commands/search.js +64 -0
- package/src/commands/session.js +57 -0
- package/src/commands/skills.js +54 -0
- package/src/commands/stop-subagent.js +58 -0
- package/src/index.js +208 -0
- package/src/llm/direct.js +317 -0
- package/src/memory/store.js +215 -0
- package/src/mock-readline.js +27 -0
- package/src/parser/dependencies.js +71 -0
- package/src/parser/markdown.js +505 -0
- package/src/parser/stream.js +96 -0
- package/src/prompts/modes/CODING.js +160 -0
- package/src/prompts/modes/GENERAL.js +105 -0
- package/src/prompts/modes/NETWORK.js +69 -0
- package/src/prompts/modes/SSH.js +53 -0
- package/src/prompts/systemPrompt.js +85 -0
- package/src/safety/check.js +210 -0
- package/src/services/crypto.js +78 -0
- package/src/services/executor.js +68 -0
- package/src/services/history.js +58 -0
- package/src/services/server-url.js +11 -0
- package/src/services/session.js +194 -0
- package/src/services/ssh.js +176 -0
- package/src/services/websocket.js +112 -0
- package/src/skills/loader.js +231 -0
- package/src/tools/browser.js +434 -0
- package/src/tools/local.js +1254 -0
- package/src/tools/mcp-client.js +209 -0
- package/src/tools/registry.js +132 -0
- package/src/tools/search-providers.js +237 -0
- package/src/tools/ssh.js +74 -0
- package/src/ui/App.js +2031 -0
- package/src/ui/animation.js +47 -0
- package/src/ui/components/AskUserDialog.js +33 -0
- package/src/ui/components/ConfirmationDialog.js +45 -0
- package/src/ui/components/DiffView.js +201 -0
- package/src/ui/components/Header.js +157 -0
- package/src/ui/components/HistoryPicker.js +130 -0
- package/src/ui/components/InputShell.js +22 -0
- package/src/ui/components/MessageHistory.js +1200 -0
- package/src/ui/components/ModalPanel.js +40 -0
- package/src/ui/components/ModePicker.js +161 -0
- package/src/ui/components/PlanDialog.js +48 -0
- package/src/ui/components/ProviderMenu.js +1095 -0
- package/src/ui/components/SavePicker.js +106 -0
- package/src/ui/components/SelectMenu.js +194 -0
- package/src/ui/components/SlashMenu.js +168 -0
- package/src/ui/components/SubagentPanel.js +138 -0
- package/src/ui/components/TextInputSafe.js +117 -0
- package/src/ui/components/TodoPanel.js +54 -0
- package/src/ui/components/ToolExecution.js +261 -0
- package/src/ui/components/TranscriptViewport.js +99 -0
- package/src/ui/diff.js +249 -0
- package/src/ui/h.js +7 -0
- package/src/ui/mouse-scroll.js +63 -0
- package/src/ui/slash-picker.js +58 -0
- package/src/ui/terminal.js +41 -0
- package/src/ui/theme.js +5 -0
- package/src/ui/welcome.js +12 -0
- package/src/utils/constants.js +231 -0
- package/src/utils/helpers.js +154 -0
- package/src/utils/logger.js +81 -0
- package/src/utils/sound.js +33 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import {
|
|
2
|
+
executeLocal, writeFile, editFile, listDir,
|
|
3
|
+
searchInFiles, appendFile, deleteFile, createDir, treeView, runScript,
|
|
4
|
+
moveFile, copyFile, getFileInfo, fetchUrl, webSearch,
|
|
5
|
+
todoAdd, todoComplete, todoUpdate, todoList, todoClear, todoStore,
|
|
6
|
+
globFiles, grepFiles, gitOp, readFileRange, diagPostEdit,
|
|
7
|
+
getDependencies, askUserQuestion, reloadTodos,
|
|
8
|
+
} from '../../tools/local.js';
|
|
9
|
+
import { executeSSH, closeAllConnections } from '../../tools/ssh.js';
|
|
10
|
+
import { browse, searchAndBrowse, extractStructuredData } from '../../tools/browser.js';
|
|
11
|
+
import { checkSafety } from '../../safety/check.js';
|
|
12
|
+
import { memory } from '../../memory/store.js';
|
|
13
|
+
import { mcpClientManager } from '../../tools/mcp-client.js';
|
|
14
|
+
import { TOOLS, SAFETY_TIERS, MODES, EXECUTION_MODES, READ_ONLY_TOOLS, READ_FRESHNESS_INTERACTIONS, SUBAGENT_ALLOWED_TOOLS } from '../../utils/constants.js';
|
|
15
|
+
import { discoverSkills, loadSkill, createSkill, formatSkillsList } from '../../skills/loader.js';
|
|
16
|
+
import { runSubagent } from '../subagent.js';
|
|
17
|
+
import { logger } from '../../utils/logger.js';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
|
|
20
|
+
// Helper: yield rapide pour ne pas bloquer l'event loop
|
|
21
|
+
// Utilise setImmediate car ces yields sont fréquents (avant/après chaque tool)
|
|
22
|
+
const yieldToEventLoop = () => new Promise(r => setImmediate(r));
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
async _executeTool(toolCall) {
|
|
26
|
+
const tool = toolCall.tool || TOOLS.LOCAL_CMD;
|
|
27
|
+
let result;
|
|
28
|
+
this._toolInteractionCounter += 1;
|
|
29
|
+
|
|
30
|
+
if (this.isSubagent) {
|
|
31
|
+
if (tool === TOOLS.TASK) {
|
|
32
|
+
return { success: false, output: '', error: 'Subagents cannot spawn other subagents.' };
|
|
33
|
+
}
|
|
34
|
+
if (!SUBAGENT_ALLOWED_TOOLS.has(tool)) {
|
|
35
|
+
return { success: false, output: '', error: `Subagent is read-only. Tool "${tool}" is not allowed.` };
|
|
36
|
+
}
|
|
37
|
+
if (tool === TOOLS.LOCAL_CMD) {
|
|
38
|
+
const safety = checkSafety(toolCall, this.mode);
|
|
39
|
+
if (safety.tier !== SAFETY_TIERS.READ) {
|
|
40
|
+
return { success: false, output: '', error: `Subagent can only run read-only LOCAL_CMD. Blocked: ${toolCall.cmd || ''}` };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (tool === TOOLS.GIT) {
|
|
44
|
+
const op = String(toolCall.op || '').toLowerCase();
|
|
45
|
+
const writeOps = ['commit', 'add', 'push', 'pull', 'checkout', 'merge', 'rebase', 'reset', 'clean', 'stash'];
|
|
46
|
+
if (writeOps.some((w) => op.startsWith(w))) {
|
|
47
|
+
return { success: false, output: '', error: `Subagent can only run read-only GIT ops (status, diff, log, branch). Blocked: ${op}` };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const criticalReadBlock = this._subagentCriticalReadBlock(toolCall);
|
|
52
|
+
if (criticalReadBlock) {
|
|
53
|
+
try { await memory.recordFailure({ tool, target: toolCall.path || toolCall.cmd || '', reason: criticalReadBlock }); } catch {}
|
|
54
|
+
return { success: false, output: '', error: criticalReadBlock };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Wrappers qui yielden avant/après pour que Ink puisse re-render
|
|
59
|
+
const emitToolStart = async (tc, safety) => {
|
|
60
|
+
await yieldToEventLoop();
|
|
61
|
+
this.onToolStart(tc, safety);
|
|
62
|
+
await yieldToEventLoop();
|
|
63
|
+
};
|
|
64
|
+
const emitToolResult = async (tc, res) => {
|
|
65
|
+
await yieldToEventLoop();
|
|
66
|
+
if (typeof this.onToolResult === 'function') await this.onToolResult(tc, res);
|
|
67
|
+
await yieldToEventLoop();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (tool === TOOLS.LOCAL_CMD) {
|
|
71
|
+
const cdTargets = this._extractCommandDirectoryTargets(toolCall.cmd);
|
|
72
|
+
if (cdTargets.length > 0) {
|
|
73
|
+
const dirTarget = cdTargets[0];
|
|
74
|
+
|
|
75
|
+
if (this.mode === MODES.CODING && !this.noConfirm) {
|
|
76
|
+
const confirmed = await this._confirmExecution(
|
|
77
|
+
{ tool: TOOLS.LOCAL_CMD, cmd: toolCall.cmd, description: `Change working directory to "${dirTarget}" ?` },
|
|
78
|
+
{ tier: SAFETY_TIERS.WRITE, requiresConfirmation: true, isDangerous: false },
|
|
79
|
+
);
|
|
80
|
+
if (!confirmed) {
|
|
81
|
+
return { success: false, output: '', error: `Cancelled by user: directory change to "${dirTarget}" was not approved` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.onMarkdown(`\n_cd ${dirTarget}_\n`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this._isFirstReadWithRange(toolCall)) {
|
|
90
|
+
const safety = { tier: SAFETY_TIERS.READ, requiresConfirmation: false, isDangerous: false };
|
|
91
|
+
await emitToolStart(toolCall, safety);
|
|
92
|
+
result = {
|
|
93
|
+
success: false,
|
|
94
|
+
output: '',
|
|
95
|
+
error: 'SYSTEM RULE: First READ_FILE for a file in this session must be full-file (no startLine/endLine). Call READ_FILE with only "path" first, then you may use line ranges.',
|
|
96
|
+
};
|
|
97
|
+
await emitToolResult(toolCall, result);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const directoryApproval = this._getDirectoryApprovalRequirement(toolCall);
|
|
102
|
+
if (directoryApproval.required) {
|
|
103
|
+
const approvalCall = {
|
|
104
|
+
...toolCall,
|
|
105
|
+
description: `${toolCall.description || 'Directory change/request'} ${directoryApproval.reason} Requires user approval before continuing.`,
|
|
106
|
+
};
|
|
107
|
+
const approvalSafety = { tier: SAFETY_TIERS.WRITE, requiresConfirmation: true, isDangerous: false };
|
|
108
|
+
const approved = await this._confirmExecution(approvalCall, approvalSafety);
|
|
109
|
+
if (!approved) {
|
|
110
|
+
result = { success: false, output: '', error: `Cancelled by user (directory access not approved): ${directoryApproval.targetPath}` };
|
|
111
|
+
await emitToolResult(toolCall, result);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
this._approvedExternalDirs.add(directoryApproval.targetPath);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this._requiresReadBeforeModify(toolCall) && this._isExistingFileTarget(toolCall)) {
|
|
118
|
+
const freshness = this._getReadFreshnessForTarget(toolCall);
|
|
119
|
+
if (!freshness.ok) {
|
|
120
|
+
const safety = { tier: SAFETY_TIERS.WRITE, requiresConfirmation: false, isDangerous: false };
|
|
121
|
+
await emitToolStart(toolCall, safety);
|
|
122
|
+
const staleDetails = freshness.reason === 'stale_read'
|
|
123
|
+
? ' File was modified externally since last read.'
|
|
124
|
+
: '';
|
|
125
|
+
result = {
|
|
126
|
+
success: false,
|
|
127
|
+
output: '',
|
|
128
|
+
error: `SYSTEM RULE: You must read the target file with READ_FILE before ${tool}. Target: ${toolCall.path || '(unknown)'}.${staleDetails}\nNext step: call READ_FILE on this exact path (same target), then retry.`,
|
|
129
|
+
};
|
|
130
|
+
await emitToolResult(toolCall, result);
|
|
131
|
+
this._blockedWritesWithoutRead += 1;
|
|
132
|
+
await this._persistVerificationState();
|
|
133
|
+
try {
|
|
134
|
+
await memory.recordFailure({
|
|
135
|
+
tool,
|
|
136
|
+
target: toolCall.path || '',
|
|
137
|
+
reason: freshness.reason === 'stale_read'
|
|
138
|
+
? `Verification failed: stale READ_FILE context (${freshness.age} interactions old)`
|
|
139
|
+
: 'Verification failed: missing READ_FILE before file modification',
|
|
140
|
+
});
|
|
141
|
+
} catch {}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (this._requiresReadBeforeModify(toolCall) && toolCall.path) {
|
|
147
|
+
const deps = this._getFileDependencies(toolCall.path);
|
|
148
|
+
const missingDeps = [];
|
|
149
|
+
for (const depPath of deps) {
|
|
150
|
+
const depToolCall = { tool: TOOLS.READ_FILE, path: depPath };
|
|
151
|
+
if (this._isExistingFileTarget(depToolCall)) {
|
|
152
|
+
const freshness = this._getReadFreshnessForTarget(depToolCall);
|
|
153
|
+
if (!freshness.ok) {
|
|
154
|
+
missingDeps.push(depPath);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (missingDeps.length > 0) {
|
|
159
|
+
this.onMarkdown(`\n_Reading ${missingDeps.length} file(s) referenced by "${path.basename(toolCall.path)}"..._\n`);
|
|
160
|
+
const depResults = await Promise.all(missingDeps.map(async (depPath) => {
|
|
161
|
+
const depCall = { tool: TOOLS.READ_FILE, path: depPath };
|
|
162
|
+
const depSafety = { tier: SAFETY_TIERS.READ, requiresConfirmation: false, isDangerous: false };
|
|
163
|
+
await emitToolStart(depCall, depSafety);
|
|
164
|
+
const depResult = await this._dispatchTool(depCall);
|
|
165
|
+
await emitToolResult(depCall, depResult);
|
|
166
|
+
await this._trackReadFile(depCall, depResult);
|
|
167
|
+
return depResult;
|
|
168
|
+
}));
|
|
169
|
+
const allOk = depResults.every(r => r.success);
|
|
170
|
+
if (!allOk) {
|
|
171
|
+
const failed = depResults.filter(r => !r.success).map((r, i) => missingDeps[i]).join(', ');
|
|
172
|
+
this.onMarkdown(`\n_Warning: could not read dependencies: ${failed}_\n`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (tool === TOOLS.READ_FILE && this._isCriticalEnvFile(toolCall.path)) {
|
|
178
|
+
if (this.isSubagent) {
|
|
179
|
+
result = {
|
|
180
|
+
success: false,
|
|
181
|
+
output: '',
|
|
182
|
+
error: 'Subagent blocked: critical environment/secret files require direct parent-agent user confirmation.',
|
|
183
|
+
};
|
|
184
|
+
await emitToolResult(toolCall, result);
|
|
185
|
+
try { await memory.recordFailure({ tool, target: toolCall.path, reason: 'Subagent blocked from reading critical env file' }); } catch {}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const criticalSafety = {
|
|
190
|
+
tier: SAFETY_TIERS.WRITE,
|
|
191
|
+
requiresConfirmation: true,
|
|
192
|
+
isDangerous: false,
|
|
193
|
+
};
|
|
194
|
+
toolCall = { ...toolCall, description: 'This file may contain sensitive environment variables, API keys, or credentials. Confirm reading?' };
|
|
195
|
+
const confirmed = await this._confirmExecution(toolCall, criticalSafety);
|
|
196
|
+
if (!confirmed) {
|
|
197
|
+
result = {
|
|
198
|
+
success: false,
|
|
199
|
+
output: '',
|
|
200
|
+
error: 'Cancelled by user — reading critical environment/secret file was not approved',
|
|
201
|
+
};
|
|
202
|
+
await emitToolResult(toolCall, result);
|
|
203
|
+
try { await memory.recordFailure({ tool, target: toolCall.path, reason: 'Cancelled by user (critical env file)' }); } catch {}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (READ_ONLY_TOOLS.has(tool)) {
|
|
209
|
+
await emitToolStart(toolCall, { tier: SAFETY_TIERS.READ, requiresConfirmation: false, isDangerous: false });
|
|
210
|
+
result = await this._dispatchTool(toolCall);
|
|
211
|
+
await emitToolResult(toolCall, result);
|
|
212
|
+
this.commandsExecuted++;
|
|
213
|
+
try { await memory.recordAction({ tool, target: toolCall.path || toolCall.url || toolCall.query, status: result.success ? 'success' : 'error' }); } catch {}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (tool.startsWith('TODO_') && tool !== 'TODO_CLEAR') {
|
|
218
|
+
await emitToolStart(toolCall, { tier: SAFETY_TIERS.READ, requiresConfirmation: false, isDangerous: false });
|
|
219
|
+
result = await this._dispatchTool(toolCall);
|
|
220
|
+
await emitToolResult(toolCall, result);
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// PLAN mode: block any write or dangerous operation
|
|
225
|
+
if (this.executionMode === EXECUTION_MODES.PLAN) {
|
|
226
|
+
const planSafety = checkSafety(toolCall, this.mode);
|
|
227
|
+
if (planSafety.tier !== SAFETY_TIERS.READ && planSafety.tier !== SAFETY_TIERS.ASK) {
|
|
228
|
+
result = {
|
|
229
|
+
success: true,
|
|
230
|
+
output: 'Blocked by PLAN mode. Ask the user to press Tab to switch to EXEC, then retry.',
|
|
231
|
+
error: null,
|
|
232
|
+
};
|
|
233
|
+
await emitToolResult(toolCall, result);
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const safety = checkSafety(toolCall, this.mode);
|
|
239
|
+
|
|
240
|
+
if ((safety.tier === SAFETY_TIERS.READ && this.noConfirm) || !safety.requiresConfirmation) {
|
|
241
|
+
await emitToolStart(toolCall, safety);
|
|
242
|
+
result = await this._dispatchTool(toolCall);
|
|
243
|
+
await emitToolResult(toolCall, result);
|
|
244
|
+
await this._trackWriteFile(toolCall, result);
|
|
245
|
+
this.commandsExecuted++;
|
|
246
|
+
try { await memory.recordAction({ tool, target: toolCall.cmd || toolCall.path, status: result.success ? 'success' : 'error' }); } catch {}
|
|
247
|
+
if (tool === TOOLS.LOCAL_CMD && result?.success) {
|
|
248
|
+
const cdTargets = this._extractCommandDirectoryTargets(toolCall.cmd);
|
|
249
|
+
if (cdTargets.length > 0) {
|
|
250
|
+
reloadTodos();
|
|
251
|
+
logger.debug('Todos reloaded for new cwd', { cwd: process.cwd() });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (safety.requiresConfirmation) {
|
|
258
|
+
const confirmed = await this._confirmExecution(toolCall, safety);
|
|
259
|
+
if (!confirmed) {
|
|
260
|
+
result = { success: false, output: '', error: 'Cancelled by user' };
|
|
261
|
+
await emitToolResult(toolCall, result);
|
|
262
|
+
try { await memory.recordFailure({ tool, target: toolCall.cmd || toolCall.path, reason: 'Cancelled by user' }); } catch {}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
await emitToolStart(toolCall, safety);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
result = await this._dispatchTool(toolCall);
|
|
269
|
+
await emitToolResult(toolCall, result);
|
|
270
|
+
await this._trackReadFile(toolCall, result);
|
|
271
|
+
await this._trackWriteFile(toolCall, result);
|
|
272
|
+
this.commandsExecuted++;
|
|
273
|
+
|
|
274
|
+
if (tool === TOOLS.LOCAL_CMD && result?.success) {
|
|
275
|
+
const cdTargets = this._extractCommandDirectoryTargets(toolCall.cmd);
|
|
276
|
+
if (cdTargets.length > 0) {
|
|
277
|
+
reloadTodos();
|
|
278
|
+
logger.debug('Todos reloaded for new cwd', { cwd: process.cwd() });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
await memory.recordAction({
|
|
284
|
+
tool, target: toolCall.cmd || toolCall.path || toolCall.url || toolCall.query,
|
|
285
|
+
status: result.success ? 'success' : 'error', error: result.success ? null : result.error,
|
|
286
|
+
});
|
|
287
|
+
} catch {}
|
|
288
|
+
|
|
289
|
+
return result;
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
async _dispatchTool(toolCall) {
|
|
293
|
+
const tool = toolCall.tool || TOOLS.LOCAL_CMD;
|
|
294
|
+
try {
|
|
295
|
+
switch (tool) {
|
|
296
|
+
case TOOLS.WRITE_FILE:
|
|
297
|
+
return await writeFile(toolCall.path, toolCall.content || '');
|
|
298
|
+
case TOOLS.EDIT_FILE:
|
|
299
|
+
return await editFile(toolCall.path, toolCall.find || '', toolCall.replace || '');
|
|
300
|
+
case TOOLS.LIST_DIR:
|
|
301
|
+
return await listDir(toolCall.path);
|
|
302
|
+
case TOOLS.SSH_CMD:
|
|
303
|
+
if (this.device) return await executeSSH(this.device, toolCall.cmd);
|
|
304
|
+
return { success: false, output: '', error: 'No device connected for SSH' };
|
|
305
|
+
|
|
306
|
+
case TOOLS.SEARCH_FILE:
|
|
307
|
+
return await searchInFiles(toolCall.path || '.', toolCall.pattern || '', toolCall.filePattern);
|
|
308
|
+
case TOOLS.APPEND_FILE:
|
|
309
|
+
return await appendFile(toolCall.path, toolCall.content || '');
|
|
310
|
+
case TOOLS.DELETE_FILE:
|
|
311
|
+
return await deleteFile(toolCall.path);
|
|
312
|
+
case TOOLS.CREATE_DIR:
|
|
313
|
+
return await createDir(toolCall.path);
|
|
314
|
+
case TOOLS.TREE_VIEW:
|
|
315
|
+
return await treeView(toolCall.path, toolCall.maxDepth, toolCall.includeHidden);
|
|
316
|
+
case TOOLS.RUN_SCRIPT:
|
|
317
|
+
return await runScript(toolCall.path, toolCall.args || '', toolCall.interpreter);
|
|
318
|
+
case TOOLS.MOVE_FILE:
|
|
319
|
+
return await moveFile(toolCall.source, toolCall.destination);
|
|
320
|
+
case TOOLS.COPY_FILE:
|
|
321
|
+
return await copyFile(toolCall.source, toolCall.destination);
|
|
322
|
+
case TOOLS.FILE_INFO:
|
|
323
|
+
return await getFileInfo(toolCall.path);
|
|
324
|
+
|
|
325
|
+
case TOOLS.FETCH_URL:
|
|
326
|
+
return await fetchUrl(toolCall.url, toolCall.description);
|
|
327
|
+
case TOOLS.WEB_SEARCH:
|
|
328
|
+
return await webSearch(toolCall.query, this.server, this.token);
|
|
329
|
+
case TOOLS.BROWSE:
|
|
330
|
+
return await browse(toolCall.url);
|
|
331
|
+
case TOOLS.BROWSE_SEARCH:
|
|
332
|
+
return await searchAndBrowse(toolCall.query);
|
|
333
|
+
case TOOLS.BROWSE_EXTRACT:
|
|
334
|
+
return await extractStructuredData(toolCall.selectors, toolCall.url);
|
|
335
|
+
|
|
336
|
+
case TOOLS.GLOB:
|
|
337
|
+
return await globFiles(toolCall.pattern, toolCall.cwd || process.cwd(), toolCall.ignore || []);
|
|
338
|
+
case TOOLS.GREP:
|
|
339
|
+
return await grepFiles(toolCall.pattern, toolCall.path || '.', toolCall.filePattern, toolCall.caseSensitive ?? false);
|
|
340
|
+
case TOOLS.GIT:
|
|
341
|
+
return await gitOp(toolCall.op, toolCall.args || '', toolCall.path || process.cwd());
|
|
342
|
+
case TOOLS.READ_FILE:
|
|
343
|
+
if (toolCall.startLine || toolCall.endLine) {
|
|
344
|
+
return await readFileRange(toolCall.path, toolCall.startLine, toolCall.endLine);
|
|
345
|
+
}
|
|
346
|
+
return await readFileRange(toolCall.path);
|
|
347
|
+
case TOOLS.GET_DEPENDENCIES:
|
|
348
|
+
return await getDependencies(toolCall.path);
|
|
349
|
+
case TOOLS.DIAG_POST_EDIT:
|
|
350
|
+
return await diagPostEdit(toolCall.path, toolCall.lang || 'auto');
|
|
351
|
+
case TOOLS.ASK_USER: {
|
|
352
|
+
const result = askUserQuestion(toolCall.question, toolCall.options || []);
|
|
353
|
+
if (result.isQuestion) {
|
|
354
|
+
const answer = await this._askUserInline(toolCall.question, toolCall.options || []);
|
|
355
|
+
return { success: true, output: `User answered: "${answer}"`, answer };
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
case TOOLS.PLAN_MODE: {
|
|
360
|
+
const plan = toolCall.plan || toolCall.steps || '';
|
|
361
|
+
const approved = await this._showPlanAndConfirm(plan);
|
|
362
|
+
return approved
|
|
363
|
+
? { success: true, output: 'Plan approved. Proceeding with execution.' }
|
|
364
|
+
: { success: false, output: '', error: 'Plan rejected by user. Stopping.' };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
case TOOLS.TODO_ADD:
|
|
368
|
+
return todoAdd(toolCall.text, toolCall.priority, this.mode);
|
|
369
|
+
case TOOLS.TODO_COMPLETE:
|
|
370
|
+
return todoComplete(toolCall.id);
|
|
371
|
+
case TOOLS.TODO_UPDATE:
|
|
372
|
+
return todoUpdate(toolCall.id, { text: toolCall.text, priority: toolCall.priority, status: toolCall.status });
|
|
373
|
+
case TOOLS.TODO_LIST: {
|
|
374
|
+
const items = todoStore.list(toolCall.mode || this.mode);
|
|
375
|
+
const scopeLine = `Project: ${process.cwd()}`;
|
|
376
|
+
const header = `Todos for ${scopeLine}`;
|
|
377
|
+
return {
|
|
378
|
+
success: true,
|
|
379
|
+
output: `${header}\n${'─'.repeat(header.length)}\n${items.length === 0 ? 'No todos.' : items.map(t => `[${t.status === 'done' ? 'x' : ' '}] #${t.id} [${t.priority}] ${t.text}`).join('\n')}`,
|
|
380
|
+
todos: items,
|
|
381
|
+
stats: todoStore.getStats(toolCall.mode || this.mode),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
case TOOLS.TODO_CLEAR:
|
|
385
|
+
return todoClear();
|
|
386
|
+
|
|
387
|
+
case TOOLS.MCP_TOOL:
|
|
388
|
+
return await mcpClientManager.executeTool(toolCall.server, toolCall.mcpTool, toolCall.params || {});
|
|
389
|
+
|
|
390
|
+
case TOOLS.SKILL_LIST: {
|
|
391
|
+
const skills = await discoverSkills();
|
|
392
|
+
return { success: true, output: formatSkillsList(skills), skills };
|
|
393
|
+
}
|
|
394
|
+
case TOOLS.LOAD_SKILL:
|
|
395
|
+
return await loadSkill(toolCall.name || toolCall.skill || '');
|
|
396
|
+
|
|
397
|
+
case TOOLS.CREATE_SKILL:
|
|
398
|
+
return await createSkill({
|
|
399
|
+
name: toolCall.name || toolCall.skill || '',
|
|
400
|
+
description: toolCall.description || '',
|
|
401
|
+
content: toolCall.content || '',
|
|
402
|
+
scope: toolCall.scope || 'project',
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
case TOOLS.TASK:
|
|
406
|
+
return await runSubagent(this, {
|
|
407
|
+
prompt: toolCall.prompt || '',
|
|
408
|
+
description: toolCall.description || '',
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
case TOOLS.LOCAL_CMD:
|
|
412
|
+
default:
|
|
413
|
+
return await executeLocal(toolCall.cmd || '');
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
logger.error('Tool dispatch error', { tool, error: error.message });
|
|
417
|
+
return { success: false, output: '', error: error.message };
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
_readStdinLine(prompt, color) {
|
|
422
|
+
return new Promise((resolve) => {
|
|
423
|
+
const output = this.readline ? this.readline.output : process.stdout;
|
|
424
|
+
const cleanPrompt = typeof prompt === 'string' ? prompt.replace(/\x1b\[[0-9;]*m/g, '') : prompt;
|
|
425
|
+
this.readline.question(cleanPrompt, (answer) => {
|
|
426
|
+
resolve(answer.trim());
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
async _confirmExecution(toolCall, safety) {
|
|
432
|
+
const tier = safety.tier || (safety.isDangerous ? SAFETY_TIERS.DANGEROUS : SAFETY_TIERS.WRITE);
|
|
433
|
+
|
|
434
|
+
if (this.isSubagent) {
|
|
435
|
+
return tier !== SAFETY_TIERS.DANGEROUS;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const tool = toolCall.tool || TOOLS.LOCAL_CMD;
|
|
439
|
+
const identifier = toolCall.cmd || toolCall.path || toolCall.url || '(operation)';
|
|
440
|
+
const desc = toolCall.description || (safety.isDangerous ? 'This will perform a dangerous operation' : 'This will modify the system');
|
|
441
|
+
|
|
442
|
+
this.onConfirmPrompt({ tool, identifier, description: desc, tier, isDangerous: safety.isDangerous });
|
|
443
|
+
|
|
444
|
+
if (tier === SAFETY_TIERS.DANGEROUS) {
|
|
445
|
+
const answer = await this._readStdinLine(' Type "confirm" to proceed: ', () => '');
|
|
446
|
+
return answer.toLowerCase() === 'confirm';
|
|
447
|
+
} else {
|
|
448
|
+
const answer = await this._readStdinLine(' Run this? (y/N): ', () => '');
|
|
449
|
+
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
async _askUserInline(question, options = []) {
|
|
454
|
+
this.onAskUserPrompt({ question, options });
|
|
455
|
+
const hint = options.length > 0 ? ` Answer (number or text): ` : ` Your answer: `;
|
|
456
|
+
const answer = await this._readStdinLine(hint, () => '');
|
|
457
|
+
if (options.length > 0) {
|
|
458
|
+
const n = parseInt(answer.trim(), 10);
|
|
459
|
+
if (!isNaN(n) && n >= 1 && n <= options.length) return options[n - 1];
|
|
460
|
+
}
|
|
461
|
+
return answer.trim();
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
async _showPlanAndConfirm(plan) {
|
|
465
|
+
const planText = typeof plan === 'string' ? plan : JSON.stringify(plan, null, 2);
|
|
466
|
+
this.onPlanPrompt({ plan: planText });
|
|
467
|
+
const answer = await this._readStdinLine(' Approve plan? (y/N): ', () => '');
|
|
468
|
+
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
469
|
+
},
|
|
470
|
+
};
|