codeep 1.2.17 → 1.2.18
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/README.md +20 -7
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.js +21 -17
- package/dist/renderer/App.d.ts +1 -5
- package/dist/renderer/App.js +106 -486
- package/dist/renderer/agentExecution.d.ts +36 -0
- package/dist/renderer/agentExecution.js +394 -0
- package/dist/renderer/commands.d.ts +16 -0
- package/dist/renderer/commands.js +838 -0
- package/dist/renderer/handlers.d.ts +87 -0
- package/dist/renderer/handlers.js +260 -0
- package/dist/renderer/highlight.d.ts +18 -0
- package/dist/renderer/highlight.js +130 -0
- package/dist/renderer/main.d.ts +4 -2
- package/dist/renderer/main.js +103 -1550
- package/dist/utils/agent.d.ts +5 -15
- package/dist/utils/agent.js +9 -693
- package/dist/utils/agentChat.d.ts +46 -0
- package/dist/utils/agentChat.js +343 -0
- package/dist/utils/agentStream.d.ts +23 -0
- package/dist/utils/agentStream.js +216 -0
- package/dist/utils/keychain.js +3 -2
- package/dist/utils/learning.js +9 -3
- package/dist/utils/mcpIntegration.d.ts +61 -0
- package/dist/utils/mcpIntegration.js +154 -0
- package/dist/utils/project.js +8 -3
- package/dist/utils/skills.js +21 -11
- package/dist/utils/smartContext.d.ts +4 -0
- package/dist/utils/smartContext.js +51 -14
- package/dist/utils/toolExecution.d.ts +27 -0
- package/dist/utils/toolExecution.js +525 -0
- package/dist/utils/toolParsing.d.ts +18 -0
- package/dist/utils/toolParsing.js +302 -0
- package/dist/utils/tools.d.ts +11 -24
- package/dist/utils/tools.js +22 -1187
- package/package.json +3 -1
- package/dist/config/config.test.d.ts +0 -1
- package/dist/config/config.test.js +0 -157
- package/dist/config/providers.test.d.ts +0 -1
- package/dist/config/providers.test.js +0 -187
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -4
- package/dist/hooks/useAgent.d.ts +0 -29
- package/dist/hooks/useAgent.js +0 -148
- package/dist/utils/agent.test.d.ts +0 -1
- package/dist/utils/agent.test.js +0 -315
- package/dist/utils/git.test.d.ts +0 -1
- package/dist/utils/git.test.js +0 -193
- package/dist/utils/gitignore.test.d.ts +0 -1
- package/dist/utils/gitignore.test.js +0 -167
- package/dist/utils/project.test.d.ts +0 -1
- package/dist/utils/project.test.js +0 -212
- package/dist/utils/ratelimit.test.d.ts +0 -1
- package/dist/utils/ratelimit.test.js +0 -131
- package/dist/utils/retry.test.d.ts +0 -1
- package/dist/utils/retry.test.js +0 -163
- package/dist/utils/smartContext.test.d.ts +0 -1
- package/dist/utils/smartContext.test.js +0 -382
- package/dist/utils/tools.test.d.ts +0 -1
- package/dist/utils/tools.test.js +0 -681
- package/dist/utils/validation.test.d.ts +0 -1
- package/dist/utils/validation.test.js +0 -164
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool execution - runs agent tool calls against the filesystem and shell.
|
|
3
|
+
*
|
|
4
|
+
* validatePath() ensures all file operations stay within the project root.
|
|
5
|
+
* executeTool() dispatches to individual tool handlers.
|
|
6
|
+
* listDirectory() and htmlToText() are private helpers.
|
|
7
|
+
* createActionLog() converts a ToolCall+ToolResult into a history ActionLog.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, rmSync, realpathSync } from 'fs';
|
|
10
|
+
import { join, dirname, relative, resolve, isAbsolute } from 'path';
|
|
11
|
+
import { executeCommand } from './shell.js';
|
|
12
|
+
import { recordWrite, recordEdit, recordDelete, recordMkdir, recordCommand } from './history.js';
|
|
13
|
+
import { loadIgnoreRules, isIgnored } from './gitignore.js';
|
|
14
|
+
import { normalizeToolName } from './toolParsing.js';
|
|
15
|
+
import { getZaiMcpConfig, getMinimaxMcpConfig, callZaiMcp, callMinimaxApi } from './mcpIntegration.js';
|
|
16
|
+
const debug = (...args) => {
|
|
17
|
+
if (process.env.CODEEP_DEBUG === '1') {
|
|
18
|
+
console.error('[DEBUG]', ...args);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Validate path is within project root.
|
|
23
|
+
* Uses realpathSync to resolve symlinks, preventing symlink traversal attacks
|
|
24
|
+
* where a symlink inside the project could point to files outside it.
|
|
25
|
+
*/
|
|
26
|
+
export function validatePath(path, projectRoot) {
|
|
27
|
+
let normalizedPath = path;
|
|
28
|
+
if (isAbsolute(path) && path.startsWith(projectRoot)) {
|
|
29
|
+
normalizedPath = relative(projectRoot, path);
|
|
30
|
+
}
|
|
31
|
+
if (isAbsolute(normalizedPath)) {
|
|
32
|
+
return { valid: false, absolutePath: normalizedPath, error: `Absolute path '${path}' not allowed. Use relative paths.` };
|
|
33
|
+
}
|
|
34
|
+
const absolutePath = resolve(projectRoot, normalizedPath);
|
|
35
|
+
const relativePath = relative(projectRoot, absolutePath);
|
|
36
|
+
if (relativePath.startsWith('..')) {
|
|
37
|
+
return { valid: false, absolutePath, error: `Path '${path}' is outside project directory` };
|
|
38
|
+
}
|
|
39
|
+
// Resolve symlinks to prevent traversal attacks (only if path exists)
|
|
40
|
+
if (existsSync(absolutePath)) {
|
|
41
|
+
try {
|
|
42
|
+
const realPath = realpathSync(absolutePath);
|
|
43
|
+
const realRoot = realpathSync(projectRoot);
|
|
44
|
+
if (!realPath.startsWith(realRoot + '/') && realPath !== realRoot) {
|
|
45
|
+
return { valid: false, absolutePath, error: `Path '${path}' resolves outside project directory (symlink traversal)` };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// realpathSync can fail on broken symlinks — treat as invalid
|
|
50
|
+
return { valid: false, absolutePath, error: `Path '${path}' could not be resolved` };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { valid: true, absolutePath };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* List directory contents, respecting .gitignore rules.
|
|
57
|
+
* Tracks visited inodes to prevent infinite loops caused by circular symlinks.
|
|
58
|
+
*/
|
|
59
|
+
function listDirectory(dir, projectRoot, recursive, prefix = '', ignoreRules, visitedInodes = new Set()) {
|
|
60
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
61
|
+
const files = [];
|
|
62
|
+
const rules = ignoreRules || loadIgnoreRules(projectRoot);
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const fullPath = join(dir, entry.name);
|
|
65
|
+
if (isIgnored(fullPath, rules))
|
|
66
|
+
continue;
|
|
67
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
68
|
+
try {
|
|
69
|
+
const st = statSync(fullPath); // follows symlinks
|
|
70
|
+
if (st.isDirectory()) {
|
|
71
|
+
if (visitedInodes.has(st.ino))
|
|
72
|
+
continue; // circular symlink — skip
|
|
73
|
+
files.push(`${prefix}${entry.name}/`);
|
|
74
|
+
if (recursive) {
|
|
75
|
+
visitedInodes.add(st.ino);
|
|
76
|
+
files.push(...listDirectory(fullPath, projectRoot, true, prefix + ' ', rules, visitedInodes));
|
|
77
|
+
visitedInodes.delete(st.ino);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
files.push(`${prefix}${entry.name}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Broken symlink or permission error — skip silently
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
files.push(`${prefix}${entry.name}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return files;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Convert HTML to readable plain text, preserving structure.
|
|
96
|
+
*/
|
|
97
|
+
function htmlToText(html) {
|
|
98
|
+
let text = html
|
|
99
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
100
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
101
|
+
.replace(/<noscript[^>]*>[\s\S]*?<\/noscript>/gi, '')
|
|
102
|
+
.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, '')
|
|
103
|
+
.replace(/<!--[\s\S]*?-->/g, '');
|
|
104
|
+
const mainMatch = text.match(/<main[^>]*>([\s\S]*?)<\/main>/i);
|
|
105
|
+
const articleMatch = text.match(/<article[^>]*>([\s\S]*?)<\/article>/i);
|
|
106
|
+
const bodyMatch = text.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
107
|
+
text = mainMatch?.[1] || articleMatch?.[1] || bodyMatch?.[1] || text;
|
|
108
|
+
text = text.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, '\n\n# $1\n\n');
|
|
109
|
+
text = text.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, '\n\n## $1\n\n');
|
|
110
|
+
text = text.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, '\n\n### $1\n\n');
|
|
111
|
+
text = text.replace(/<h[4-6][^>]*>([\s\S]*?)<\/h[4-6]>/gi, '\n\n#### $1\n\n');
|
|
112
|
+
text = text.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)');
|
|
113
|
+
text = text.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, '\n```\n$1\n```\n');
|
|
114
|
+
text = text.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, '\n```\n$1\n```\n');
|
|
115
|
+
text = text.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, '`$1`');
|
|
116
|
+
text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, '\n- $1');
|
|
117
|
+
text = text.replace(/<\/[uo]l>/gi, '\n');
|
|
118
|
+
text = text.replace(/<[uo]l[^>]*>/gi, '\n');
|
|
119
|
+
text = text.replace(/<\/p>/gi, '\n\n');
|
|
120
|
+
text = text.replace(/<br\s*\/?>/gi, '\n');
|
|
121
|
+
text = text.replace(/<\/div>/gi, '\n');
|
|
122
|
+
text = text.replace(/<\/tr>/gi, '\n');
|
|
123
|
+
text = text.replace(/<\/th>/gi, '\t');
|
|
124
|
+
text = text.replace(/<\/td>/gi, '\t');
|
|
125
|
+
text = text.replace(/<hr[^>]*>/gi, '\n---\n');
|
|
126
|
+
text = text.replace(/<\/blockquote>/gi, '\n');
|
|
127
|
+
text = text.replace(/<blockquote[^>]*>/gi, '\n> ');
|
|
128
|
+
text = text.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, '**$2**');
|
|
129
|
+
text = text.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, '*$2*');
|
|
130
|
+
text = text.replace(/<[^>]+>/g, '');
|
|
131
|
+
text = text
|
|
132
|
+
.replace(/&/g, '&')
|
|
133
|
+
.replace(/</g, '<')
|
|
134
|
+
.replace(/>/g, '>')
|
|
135
|
+
.replace(/"/g, '"')
|
|
136
|
+
.replace(/'/g, "'")
|
|
137
|
+
.replace(/ /g, ' ')
|
|
138
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code)))
|
|
139
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCharCode(parseInt(code, 16)));
|
|
140
|
+
return text
|
|
141
|
+
.replace(/[ \t]+/g, ' ')
|
|
142
|
+
.replace(/ *\n/g, '\n')
|
|
143
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
144
|
+
.trim();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Execute a tool call and return the result.
|
|
148
|
+
*/
|
|
149
|
+
export async function executeTool(toolCall, projectRoot) {
|
|
150
|
+
const tool = normalizeToolName(toolCall.tool);
|
|
151
|
+
const parameters = toolCall.parameters;
|
|
152
|
+
debug(`Executing tool: ${tool}`, parameters.path || parameters.command || '');
|
|
153
|
+
try {
|
|
154
|
+
switch (tool) {
|
|
155
|
+
case 'read_file': {
|
|
156
|
+
const path = parameters.path;
|
|
157
|
+
if (!path)
|
|
158
|
+
return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
|
|
159
|
+
const validation = validatePath(path, projectRoot);
|
|
160
|
+
if (!validation.valid)
|
|
161
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
162
|
+
if (!existsSync(validation.absolutePath))
|
|
163
|
+
return { success: false, output: '', error: `File not found: ${path}`, tool, parameters };
|
|
164
|
+
const stat = statSync(validation.absolutePath);
|
|
165
|
+
if (stat.isDirectory())
|
|
166
|
+
return { success: false, output: '', error: `Path is a directory, not a file: ${path}`, tool, parameters };
|
|
167
|
+
if (stat.size > 100 * 1024)
|
|
168
|
+
return { success: false, output: '', error: `File too large (${stat.size} bytes). Max: 100KB`, tool, parameters };
|
|
169
|
+
return { success: true, output: readFileSync(validation.absolutePath, 'utf-8'), tool, parameters };
|
|
170
|
+
}
|
|
171
|
+
case 'write_file': {
|
|
172
|
+
const path = parameters.path;
|
|
173
|
+
let content = parameters.content;
|
|
174
|
+
if (!path) {
|
|
175
|
+
debug('write_file failed: missing path');
|
|
176
|
+
return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
|
|
177
|
+
}
|
|
178
|
+
if (content === undefined || content === null) {
|
|
179
|
+
debug('write_file: content was undefined, using placeholder');
|
|
180
|
+
content = '<!-- Content was not provided -->\n';
|
|
181
|
+
}
|
|
182
|
+
const validation = validatePath(path, projectRoot);
|
|
183
|
+
if (!validation.valid)
|
|
184
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
185
|
+
const dir = dirname(validation.absolutePath);
|
|
186
|
+
if (!existsSync(dir))
|
|
187
|
+
mkdirSync(dir, { recursive: true });
|
|
188
|
+
recordWrite(validation.absolutePath);
|
|
189
|
+
const existed = existsSync(validation.absolutePath);
|
|
190
|
+
writeFileSync(validation.absolutePath, content, 'utf-8');
|
|
191
|
+
return { success: true, output: `${existed ? 'Updated' : 'Created'} file: ${path}`, tool, parameters };
|
|
192
|
+
}
|
|
193
|
+
case 'edit_file': {
|
|
194
|
+
const path = parameters.path;
|
|
195
|
+
const oldText = parameters.old_text;
|
|
196
|
+
const newText = parameters.new_text;
|
|
197
|
+
if (!path || oldText === undefined || newText === undefined) {
|
|
198
|
+
return { success: false, output: '', error: 'Missing required parameters', tool, parameters };
|
|
199
|
+
}
|
|
200
|
+
const validation = validatePath(path, projectRoot);
|
|
201
|
+
if (!validation.valid)
|
|
202
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
203
|
+
if (!existsSync(validation.absolutePath))
|
|
204
|
+
return { success: false, output: '', error: `File not found: ${path}`, tool, parameters };
|
|
205
|
+
const content = readFileSync(validation.absolutePath, 'utf-8');
|
|
206
|
+
if (!content.includes(oldText)) {
|
|
207
|
+
return { success: false, output: '', error: 'Text not found in file. Make sure old_text matches exactly.', tool, parameters };
|
|
208
|
+
}
|
|
209
|
+
let matchCount = 0;
|
|
210
|
+
let searchPos = 0;
|
|
211
|
+
while ((searchPos = content.indexOf(oldText, searchPos)) !== -1) {
|
|
212
|
+
matchCount++;
|
|
213
|
+
searchPos += oldText.length;
|
|
214
|
+
}
|
|
215
|
+
if (matchCount > 1) {
|
|
216
|
+
return { success: false, output: '', error: `old_text matches ${matchCount} locations in the file. Provide more surrounding context to make it unique (only 1 match allowed).`, tool, parameters };
|
|
217
|
+
}
|
|
218
|
+
recordEdit(validation.absolutePath);
|
|
219
|
+
writeFileSync(validation.absolutePath, content.replace(oldText, newText), 'utf-8');
|
|
220
|
+
return { success: true, output: `Edited file: ${path}`, tool, parameters };
|
|
221
|
+
}
|
|
222
|
+
case 'delete_file': {
|
|
223
|
+
const path = parameters.path;
|
|
224
|
+
if (!path)
|
|
225
|
+
return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
|
|
226
|
+
const validation = validatePath(path, projectRoot);
|
|
227
|
+
if (!validation.valid)
|
|
228
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
229
|
+
if (!existsSync(validation.absolutePath))
|
|
230
|
+
return { success: false, output: '', error: `Path not found: ${path}`, tool, parameters };
|
|
231
|
+
recordDelete(validation.absolutePath);
|
|
232
|
+
const stat = statSync(validation.absolutePath);
|
|
233
|
+
if (stat.isDirectory()) {
|
|
234
|
+
rmSync(validation.absolutePath, { recursive: true, force: true });
|
|
235
|
+
return { success: true, output: `Deleted directory: ${path}`, tool, parameters };
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
unlinkSync(validation.absolutePath);
|
|
239
|
+
return { success: true, output: `Deleted file: ${path}`, tool, parameters };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
case 'list_files': {
|
|
243
|
+
const path = parameters.path || '.';
|
|
244
|
+
const recursive = parameters.recursive || false;
|
|
245
|
+
const validation = validatePath(path, projectRoot);
|
|
246
|
+
if (!validation.valid)
|
|
247
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
248
|
+
if (!existsSync(validation.absolutePath))
|
|
249
|
+
return { success: false, output: '', error: `Directory not found: ${path}`, tool, parameters };
|
|
250
|
+
const stat = statSync(validation.absolutePath);
|
|
251
|
+
if (!stat.isDirectory())
|
|
252
|
+
return { success: false, output: '', error: `Path is not a directory: ${path}`, tool, parameters };
|
|
253
|
+
const files = listDirectory(validation.absolutePath, projectRoot, recursive);
|
|
254
|
+
return { success: true, output: files.join('\n'), tool, parameters };
|
|
255
|
+
}
|
|
256
|
+
case 'create_directory': {
|
|
257
|
+
const path = parameters.path;
|
|
258
|
+
if (!path)
|
|
259
|
+
return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
|
|
260
|
+
const validation = validatePath(path, projectRoot);
|
|
261
|
+
if (!validation.valid)
|
|
262
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
263
|
+
if (existsSync(validation.absolutePath)) {
|
|
264
|
+
const stat = statSync(validation.absolutePath);
|
|
265
|
+
if (stat.isDirectory())
|
|
266
|
+
return { success: true, output: `Directory already exists: ${path}`, tool, parameters };
|
|
267
|
+
return { success: false, output: '', error: `Path exists but is a file: ${path}`, tool, parameters };
|
|
268
|
+
}
|
|
269
|
+
recordMkdir(validation.absolutePath);
|
|
270
|
+
mkdirSync(validation.absolutePath, { recursive: true });
|
|
271
|
+
return { success: true, output: `Created directory: ${path}`, tool, parameters };
|
|
272
|
+
}
|
|
273
|
+
case 'execute_command': {
|
|
274
|
+
const command = parameters.command;
|
|
275
|
+
const args = parameters.args || [];
|
|
276
|
+
if (!command)
|
|
277
|
+
return { success: false, output: '', error: 'Missing required parameter: command', tool, parameters };
|
|
278
|
+
recordCommand(command, args);
|
|
279
|
+
const result = executeCommand(command, args, {
|
|
280
|
+
cwd: projectRoot,
|
|
281
|
+
projectRoot,
|
|
282
|
+
timeout: 120000,
|
|
283
|
+
});
|
|
284
|
+
if (result.success)
|
|
285
|
+
return { success: true, output: result.stdout || '(no output)', tool, parameters };
|
|
286
|
+
return { success: false, output: result.stdout, error: result.stderr, tool, parameters };
|
|
287
|
+
}
|
|
288
|
+
case 'search_code': {
|
|
289
|
+
const pattern = parameters.pattern;
|
|
290
|
+
const searchPath = parameters.path || '.';
|
|
291
|
+
if (!pattern)
|
|
292
|
+
return { success: false, output: '', error: 'Missing required parameter: pattern', tool, parameters };
|
|
293
|
+
const validation = validatePath(searchPath, projectRoot);
|
|
294
|
+
if (!validation.valid)
|
|
295
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
296
|
+
const result = executeCommand('grep', ['-rn', '--include=*.{ts,tsx,js,jsx,json,md,css,html,py,go,rs,rb,kt,kts,swift,php,java,cs,c,cpp,h,hpp,vue,svelte,yaml,yml,toml,sh,sql,xml,scss,less}', pattern, validation.absolutePath], {
|
|
297
|
+
cwd: projectRoot,
|
|
298
|
+
projectRoot,
|
|
299
|
+
timeout: 30000,
|
|
300
|
+
});
|
|
301
|
+
if (result.exitCode === 0) {
|
|
302
|
+
const lines = result.stdout.split('\n').slice(0, 50);
|
|
303
|
+
return { success: true, output: lines.join('\n') || 'No matches found', tool, parameters };
|
|
304
|
+
}
|
|
305
|
+
else if (result.exitCode === 1) {
|
|
306
|
+
return { success: true, output: 'No matches found', tool, parameters };
|
|
307
|
+
}
|
|
308
|
+
return { success: false, output: '', error: result.stderr || 'Search failed', tool, parameters };
|
|
309
|
+
}
|
|
310
|
+
case 'find_files': {
|
|
311
|
+
const pattern = parameters.pattern;
|
|
312
|
+
const searchPath = parameters.path || '.';
|
|
313
|
+
if (!pattern)
|
|
314
|
+
return { success: false, output: '', error: 'Missing required parameter: pattern', tool, parameters };
|
|
315
|
+
const validation = validatePath(searchPath, projectRoot);
|
|
316
|
+
if (!validation.valid)
|
|
317
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
318
|
+
const findArgs = [validation.absolutePath, '(', '-name', 'node_modules', '-o', '-name', '.git', '-o', '-name', '.codeep', '-o', '-name', 'dist', '-o', '-name', 'build', '-o', '-name', '.next', ')', '-prune', '-o'];
|
|
319
|
+
if (pattern.includes('/')) {
|
|
320
|
+
findArgs.push('-path', `*/${pattern}`, '-print');
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
findArgs.push('-name', pattern, '-print');
|
|
324
|
+
}
|
|
325
|
+
const result = executeCommand('find', findArgs, { cwd: projectRoot, projectRoot, timeout: 15000 });
|
|
326
|
+
if (result.exitCode === 0 || result.stdout) {
|
|
327
|
+
const files = result.stdout.split('\n').filter(Boolean);
|
|
328
|
+
const relativePaths = files.map(f => relative(projectRoot, f) || f).slice(0, 100);
|
|
329
|
+
if (relativePaths.length === 0)
|
|
330
|
+
return { success: true, output: `No files matching "${pattern}"`, tool, parameters };
|
|
331
|
+
return { success: true, output: `Found ${relativePaths.length} file(s):\n${relativePaths.join('\n')}`, tool, parameters };
|
|
332
|
+
}
|
|
333
|
+
return { success: false, output: '', error: result.stderr || 'Find failed', tool, parameters };
|
|
334
|
+
}
|
|
335
|
+
case 'fetch_url': {
|
|
336
|
+
const url = parameters.url;
|
|
337
|
+
if (!url)
|
|
338
|
+
return { success: false, output: '', error: 'Missing required parameter: url', tool, parameters };
|
|
339
|
+
try {
|
|
340
|
+
new URL(url);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return { success: false, output: '', error: 'Invalid URL format', tool, parameters };
|
|
344
|
+
}
|
|
345
|
+
const result = executeCommand('curl', ['-s', '-L', '-m', '30', '-A', 'Codeep/1.0', '--max-filesize', '1000000', url], {
|
|
346
|
+
cwd: projectRoot,
|
|
347
|
+
projectRoot,
|
|
348
|
+
timeout: 35000,
|
|
349
|
+
});
|
|
350
|
+
if (result.success) {
|
|
351
|
+
let content = result.stdout;
|
|
352
|
+
if (content.includes('<html') || content.includes('<!DOCTYPE')) {
|
|
353
|
+
content = htmlToText(content);
|
|
354
|
+
}
|
|
355
|
+
if (content.length > 10000)
|
|
356
|
+
content = content.substring(0, 10000) + '\n\n... (truncated)';
|
|
357
|
+
return { success: true, output: content, tool, parameters };
|
|
358
|
+
}
|
|
359
|
+
return { success: false, output: '', error: result.stderr || 'Failed to fetch URL', tool, parameters };
|
|
360
|
+
}
|
|
361
|
+
// === Z.AI MCP Tools ===
|
|
362
|
+
case 'web_search': {
|
|
363
|
+
const mcpConfig = getZaiMcpConfig();
|
|
364
|
+
if (!mcpConfig)
|
|
365
|
+
return { success: false, output: '', error: 'web_search requires a Z.AI API key. Configure one via /provider z.ai', tool, parameters };
|
|
366
|
+
const query = parameters.query;
|
|
367
|
+
if (!query)
|
|
368
|
+
return { success: false, output: '', error: 'Missing required parameter: query', tool, parameters };
|
|
369
|
+
const args = { search_query: query };
|
|
370
|
+
if (parameters.domain_filter)
|
|
371
|
+
args.search_domain_filter = parameters.domain_filter;
|
|
372
|
+
if (parameters.recency)
|
|
373
|
+
args.search_recency_filter = parameters.recency;
|
|
374
|
+
const result = await callZaiMcp(mcpConfig.endpoints.webSearch, 'webSearchPrime', args, mcpConfig.apiKey);
|
|
375
|
+
const output = result.length > 15000 ? result.substring(0, 15000) + '\n\n... (truncated)' : result;
|
|
376
|
+
return { success: true, output, tool, parameters };
|
|
377
|
+
}
|
|
378
|
+
case 'web_read': {
|
|
379
|
+
const mcpConfig = getZaiMcpConfig();
|
|
380
|
+
if (!mcpConfig)
|
|
381
|
+
return { success: false, output: '', error: 'web_read requires a Z.AI API key. Configure one via /provider z.ai', tool, parameters };
|
|
382
|
+
const url = parameters.url;
|
|
383
|
+
if (!url)
|
|
384
|
+
return { success: false, output: '', error: 'Missing required parameter: url', tool, parameters };
|
|
385
|
+
try {
|
|
386
|
+
new URL(url);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return { success: false, output: '', error: 'Invalid URL format', tool, parameters };
|
|
390
|
+
}
|
|
391
|
+
const args = { url };
|
|
392
|
+
if (parameters.format)
|
|
393
|
+
args.return_format = parameters.format;
|
|
394
|
+
const result = await callZaiMcp(mcpConfig.endpoints.webReader, 'webReader', args, mcpConfig.apiKey);
|
|
395
|
+
const output = result.length > 15000 ? result.substring(0, 15000) + '\n\n... (truncated)' : result;
|
|
396
|
+
return { success: true, output, tool, parameters };
|
|
397
|
+
}
|
|
398
|
+
case 'github_read': {
|
|
399
|
+
const mcpConfig = getZaiMcpConfig();
|
|
400
|
+
if (!mcpConfig)
|
|
401
|
+
return { success: false, output: '', error: 'github_read requires a Z.AI API key. Configure one via /provider z.ai', tool, parameters };
|
|
402
|
+
const repo = parameters.repo;
|
|
403
|
+
const action = parameters.action;
|
|
404
|
+
if (!repo)
|
|
405
|
+
return { success: false, output: '', error: 'Missing required parameter: repo', tool, parameters };
|
|
406
|
+
if (!repo.includes('/'))
|
|
407
|
+
return { success: false, output: '', error: 'Invalid repo format. Use owner/repo (e.g. facebook/react)', tool, parameters };
|
|
408
|
+
if (!action || !['search', 'tree', 'read_file'].includes(action)) {
|
|
409
|
+
return { success: false, output: '', error: 'Invalid action. Must be: search, tree, or read_file', tool, parameters };
|
|
410
|
+
}
|
|
411
|
+
let mcpToolName;
|
|
412
|
+
const args = { repo_name: repo };
|
|
413
|
+
if (action === 'search') {
|
|
414
|
+
mcpToolName = 'search_doc';
|
|
415
|
+
const query = parameters.query;
|
|
416
|
+
if (!query)
|
|
417
|
+
return { success: false, output: '', error: 'Missing required parameter: query (for action=search)', tool, parameters };
|
|
418
|
+
args.query = query;
|
|
419
|
+
}
|
|
420
|
+
else if (action === 'tree') {
|
|
421
|
+
mcpToolName = 'get_repo_structure';
|
|
422
|
+
if (parameters.path)
|
|
423
|
+
args.dir_path = parameters.path;
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
mcpToolName = 'read_file';
|
|
427
|
+
const filePath = parameters.path;
|
|
428
|
+
if (!filePath)
|
|
429
|
+
return { success: false, output: '', error: 'Missing required parameter: path (for action=read_file)', tool, parameters };
|
|
430
|
+
args.file_path = filePath;
|
|
431
|
+
}
|
|
432
|
+
const result = await callZaiMcp(mcpConfig.endpoints.zread, mcpToolName, args, mcpConfig.apiKey);
|
|
433
|
+
const output = result.length > 15000 ? result.substring(0, 15000) + '\n\n... (truncated)' : result;
|
|
434
|
+
return { success: true, output, tool, parameters };
|
|
435
|
+
}
|
|
436
|
+
// === MiniMax MCP Tools ===
|
|
437
|
+
case 'minimax_web_search': {
|
|
438
|
+
const mmConfig = getMinimaxMcpConfig();
|
|
439
|
+
if (!mmConfig)
|
|
440
|
+
return { success: false, output: '', error: 'minimax_web_search requires a MiniMax API key. Configure one via /provider minimax', tool, parameters };
|
|
441
|
+
const query = parameters.query;
|
|
442
|
+
if (!query)
|
|
443
|
+
return { success: false, output: '', error: 'Missing required parameter: query', tool, parameters };
|
|
444
|
+
const result = await callMinimaxApi(mmConfig.host, '/v1/coding_plan/search', { q: query }, mmConfig.apiKey);
|
|
445
|
+
const output = result.length > 15000 ? result.substring(0, 15000) + '\n\n... (truncated)' : result;
|
|
446
|
+
return { success: true, output, tool, parameters };
|
|
447
|
+
}
|
|
448
|
+
case 'minimax_understand_image': {
|
|
449
|
+
const mmConfig = getMinimaxMcpConfig();
|
|
450
|
+
if (!mmConfig)
|
|
451
|
+
return { success: false, output: '', error: 'minimax_understand_image requires a MiniMax API key. Configure one via /provider minimax', tool, parameters };
|
|
452
|
+
const prompt = parameters.prompt;
|
|
453
|
+
const imageUrl = parameters.image_url;
|
|
454
|
+
if (!prompt)
|
|
455
|
+
return { success: false, output: '', error: 'Missing required parameter: prompt', tool, parameters };
|
|
456
|
+
if (!imageUrl)
|
|
457
|
+
return { success: false, output: '', error: 'Missing required parameter: image_url', tool, parameters };
|
|
458
|
+
const result = await callMinimaxApi(mmConfig.host, '/v1/coding_plan/vlm', { prompt, image_url: imageUrl }, mmConfig.apiKey);
|
|
459
|
+
const output = result.length > 15000 ? result.substring(0, 15000) + '\n\n... (truncated)' : result;
|
|
460
|
+
return { success: true, output, tool, parameters };
|
|
461
|
+
}
|
|
462
|
+
default:
|
|
463
|
+
return { success: false, output: '', error: `Unknown tool: ${tool}`, tool, parameters };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
const err = error;
|
|
468
|
+
return { success: false, output: '', error: err.message, tool, parameters };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Create action log from tool result
|
|
473
|
+
*/
|
|
474
|
+
export function createActionLog(toolCall, result) {
|
|
475
|
+
const normalizedTool = normalizeToolName(toolCall.tool);
|
|
476
|
+
const typeMap = {
|
|
477
|
+
read_file: 'read',
|
|
478
|
+
write_file: 'write',
|
|
479
|
+
edit_file: 'edit',
|
|
480
|
+
delete_file: 'delete',
|
|
481
|
+
execute_command: 'command',
|
|
482
|
+
search_code: 'search',
|
|
483
|
+
list_files: 'list',
|
|
484
|
+
create_directory: 'mkdir',
|
|
485
|
+
find_files: 'search',
|
|
486
|
+
fetch_url: 'fetch',
|
|
487
|
+
web_search: 'fetch',
|
|
488
|
+
web_read: 'fetch',
|
|
489
|
+
github_read: 'fetch',
|
|
490
|
+
minimax_web_search: 'fetch',
|
|
491
|
+
minimax_understand_image: 'fetch',
|
|
492
|
+
};
|
|
493
|
+
const target = toolCall.parameters.path ||
|
|
494
|
+
toolCall.parameters.command ||
|
|
495
|
+
toolCall.parameters.pattern ||
|
|
496
|
+
toolCall.parameters.url ||
|
|
497
|
+
toolCall.parameters.query ||
|
|
498
|
+
toolCall.parameters.repo ||
|
|
499
|
+
'unknown';
|
|
500
|
+
let details;
|
|
501
|
+
if (result.success) {
|
|
502
|
+
if (normalizedTool === 'write_file' && toolCall.parameters.content) {
|
|
503
|
+
details = toolCall.parameters.content;
|
|
504
|
+
}
|
|
505
|
+
else if (normalizedTool === 'edit_file' && toolCall.parameters.new_text) {
|
|
506
|
+
details = toolCall.parameters.new_text;
|
|
507
|
+
}
|
|
508
|
+
else if (normalizedTool === 'execute_command') {
|
|
509
|
+
details = result.output.slice(0, 1000);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
details = result.output.slice(0, 500);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
details = result.error;
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
type: typeMap[normalizedTool] || 'command',
|
|
520
|
+
target,
|
|
521
|
+
result: result.success ? 'success' : 'error',
|
|
522
|
+
details,
|
|
523
|
+
timestamp: Date.now(),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool call parsing from LLM responses.
|
|
3
|
+
*
|
|
4
|
+
* Handles OpenAI function-calling format, Anthropic tool_use format,
|
|
5
|
+
* and legacy text-based tool call formats.
|
|
6
|
+
*/
|
|
7
|
+
import { ToolCall } from './tools';
|
|
8
|
+
/**
|
|
9
|
+
* Normalize tool name to lowercase with underscores
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeToolName(name: string): string;
|
|
12
|
+
export declare function parseOpenAIToolCalls(toolCalls: unknown[]): ToolCall[];
|
|
13
|
+
export declare function parseAnthropicToolCalls(content: unknown[]): ToolCall[];
|
|
14
|
+
/**
|
|
15
|
+
* Parse tool calls from LLM response text.
|
|
16
|
+
* Supports: <tool_call>, <toolcall>, ```tool blocks, inline JSON.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseToolCalls(response: string): ToolCall[];
|