codeep 1.2.16 → 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/Input.js +8 -1
- 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 +27 -24
- package/dist/utils/tools.js +30 -1169
- 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 -676
- package/dist/utils/validation.test.d.ts +0 -1
- package/dist/utils/validation.test.js +0 -164
package/dist/utils/tools.js
CHANGED
|
@@ -1,170 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent tools - definitions and
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
11
|
-
import { join, dirname, relative, resolve, isAbsolute } from 'path';
|
|
12
|
-
import { executeCommand } from './shell.js';
|
|
13
|
-
import { recordWrite, recordEdit, recordDelete, recordMkdir, recordCommand } from './history.js';
|
|
14
|
-
import { loadIgnoreRules, isIgnored } from './gitignore.js';
|
|
15
|
-
import { config, getApiKey } from '../config/index.js';
|
|
16
|
-
import { getProviderMcpEndpoints, PROVIDERS } from '../config/providers.js';
|
|
17
|
-
// Z.AI MCP tool names (available when user has any Z.AI API key)
|
|
18
|
-
const ZAI_MCP_TOOLS = ['web_search', 'web_read', 'github_read'];
|
|
19
|
-
// Z.AI provider IDs that have MCP endpoints
|
|
20
|
-
const ZAI_PROVIDER_IDS = ['z.ai', 'z.ai-cn'];
|
|
21
|
-
// MiniMax MCP tool names (available when user has any MiniMax API key)
|
|
22
|
-
const MINIMAX_MCP_TOOLS = ['minimax_web_search'];
|
|
23
|
-
// MiniMax provider IDs
|
|
24
|
-
const MINIMAX_PROVIDER_IDS = ['minimax', 'minimax-cn'];
|
|
25
|
-
/**
|
|
26
|
-
* Find a Z.AI provider that has an API key configured.
|
|
27
|
-
* Returns the provider ID and API key, or null if none found.
|
|
28
|
-
* Prefers the active provider if it's Z.AI, otherwise checks all Z.AI providers.
|
|
29
|
-
*/
|
|
30
|
-
function getZaiMcpConfig() {
|
|
31
|
-
// First check if active provider is Z.AI
|
|
32
|
-
const activeProvider = config.get('provider');
|
|
33
|
-
if (ZAI_PROVIDER_IDS.includes(activeProvider)) {
|
|
34
|
-
const key = getApiKey(activeProvider);
|
|
35
|
-
const endpoints = getProviderMcpEndpoints(activeProvider);
|
|
36
|
-
if (key && endpoints?.webSearch && endpoints?.webReader && endpoints?.zread) {
|
|
37
|
-
return { providerId: activeProvider, apiKey: key, endpoints: endpoints };
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// Otherwise check all Z.AI providers for a configured key
|
|
41
|
-
for (const pid of ZAI_PROVIDER_IDS) {
|
|
42
|
-
const key = getApiKey(pid);
|
|
43
|
-
const endpoints = getProviderMcpEndpoints(pid);
|
|
44
|
-
if (key && endpoints?.webSearch && endpoints?.webReader && endpoints?.zread) {
|
|
45
|
-
return { providerId: pid, apiKey: key, endpoints: endpoints };
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Check if Z.AI MCP tools are available (user has any Z.AI API key)
|
|
52
|
-
*/
|
|
53
|
-
function hasZaiMcpAccess() {
|
|
54
|
-
return getZaiMcpConfig() !== null;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Find a MiniMax provider that has an API key configured.
|
|
58
|
-
* Returns the base host URL and API key, or null if none found.
|
|
59
|
-
*/
|
|
60
|
-
function getMinimaxMcpConfig() {
|
|
61
|
-
// First check if active provider is MiniMax
|
|
62
|
-
const activeProvider = config.get('provider');
|
|
63
|
-
if (MINIMAX_PROVIDER_IDS.includes(activeProvider)) {
|
|
64
|
-
const key = getApiKey(activeProvider);
|
|
65
|
-
if (key) {
|
|
66
|
-
const provider = PROVIDERS[activeProvider];
|
|
67
|
-
const baseUrl = provider?.protocols?.openai?.baseUrl;
|
|
68
|
-
if (baseUrl) {
|
|
69
|
-
// Extract host from baseUrl (e.g. https://api.minimax.io/v1 -> https://api.minimax.io)
|
|
70
|
-
const host = baseUrl.replace(/\/v1\/?$/, '');
|
|
71
|
-
return { host, apiKey: key };
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// Otherwise check all MiniMax providers
|
|
76
|
-
for (const pid of MINIMAX_PROVIDER_IDS) {
|
|
77
|
-
const key = getApiKey(pid);
|
|
78
|
-
if (key) {
|
|
79
|
-
const provider = PROVIDERS[pid];
|
|
80
|
-
const baseUrl = provider?.protocols?.openai?.baseUrl;
|
|
81
|
-
if (baseUrl) {
|
|
82
|
-
const host = baseUrl.replace(/\/v1\/?$/, '');
|
|
83
|
-
return { host, apiKey: key };
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Check if MiniMax MCP tools are available
|
|
91
|
-
*/
|
|
92
|
-
function hasMinimaxMcpAccess() {
|
|
93
|
-
return getMinimaxMcpConfig() !== null;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Call a MiniMax MCP REST API endpoint
|
|
97
|
-
*/
|
|
98
|
-
async function callMinimaxApi(host, path, body, apiKey) {
|
|
99
|
-
const controller = new AbortController();
|
|
100
|
-
const timeout = setTimeout(() => controller.abort(), 60000);
|
|
101
|
-
try {
|
|
102
|
-
const response = await fetch(`${host}${path}`, {
|
|
103
|
-
method: 'POST',
|
|
104
|
-
headers: {
|
|
105
|
-
'Content-Type': 'application/json',
|
|
106
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
107
|
-
},
|
|
108
|
-
body: JSON.stringify(body),
|
|
109
|
-
signal: controller.signal,
|
|
110
|
-
});
|
|
111
|
-
if (!response.ok) {
|
|
112
|
-
const errorText = await response.text().catch(() => '');
|
|
113
|
-
throw new Error(`MiniMax API error ${response.status}: ${errorText || response.statusText}`);
|
|
114
|
-
}
|
|
115
|
-
const data = await response.json();
|
|
116
|
-
// MiniMax returns content array like MCP
|
|
117
|
-
if (data.content && Array.isArray(data.content)) {
|
|
118
|
-
return data.content.map((c) => c.text || '').join('\n');
|
|
119
|
-
}
|
|
120
|
-
// Fallback: return raw result
|
|
121
|
-
return typeof data === 'string' ? data : JSON.stringify(data);
|
|
122
|
-
}
|
|
123
|
-
finally {
|
|
124
|
-
clearTimeout(timeout);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Call a Z.AI MCP endpoint via JSON-RPC 2.0
|
|
129
|
-
*/
|
|
130
|
-
async function callZaiMcp(endpoint, toolName, args, apiKey) {
|
|
131
|
-
const controller = new AbortController();
|
|
132
|
-
const timeout = setTimeout(() => controller.abort(), 60000);
|
|
133
|
-
try {
|
|
134
|
-
const response = await fetch(endpoint, {
|
|
135
|
-
method: 'POST',
|
|
136
|
-
headers: {
|
|
137
|
-
'Content-Type': 'application/json',
|
|
138
|
-
'Accept': 'application/json',
|
|
139
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
140
|
-
},
|
|
141
|
-
body: JSON.stringify({
|
|
142
|
-
jsonrpc: '2.0',
|
|
143
|
-
id: Date.now().toString(),
|
|
144
|
-
method: 'tools/call',
|
|
145
|
-
params: { name: toolName, arguments: args },
|
|
146
|
-
}),
|
|
147
|
-
signal: controller.signal,
|
|
148
|
-
});
|
|
149
|
-
if (!response.ok) {
|
|
150
|
-
const errorText = await response.text().catch(() => '');
|
|
151
|
-
throw new Error(`MCP error ${response.status}: ${errorText || response.statusText}`);
|
|
152
|
-
}
|
|
153
|
-
const data = await response.json();
|
|
154
|
-
if (data.error) {
|
|
155
|
-
throw new Error(data.error.message || JSON.stringify(data.error));
|
|
156
|
-
}
|
|
157
|
-
// MCP returns result.content as array of {type, text} blocks
|
|
158
|
-
const content = data.result?.content;
|
|
159
|
-
if (Array.isArray(content)) {
|
|
160
|
-
return content.map((c) => c.text || '').join('\n');
|
|
161
|
-
}
|
|
162
|
-
return typeof data.result === 'string' ? data.result : JSON.stringify(data.result);
|
|
163
|
-
}
|
|
164
|
-
finally {
|
|
165
|
-
clearTimeout(timeout);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
2
|
+
* Agent tools - definitions, interfaces, and re-exports.
|
|
3
|
+
*
|
|
4
|
+
* Heavy implementation is split into:
|
|
5
|
+
* mcpIntegration.ts — Z.AI and MiniMax MCP API helpers
|
|
6
|
+
* toolParsing.ts — parseToolCalls, parseOpenAIToolCalls, parseAnthropicToolCalls
|
|
7
|
+
* toolExecution.ts — executeTool, validatePath, createActionLog
|
|
8
|
+
*/
|
|
9
|
+
import { hasZaiMcpAccess, hasMinimaxMcpAccess, ZAI_MCP_TOOLS, MINIMAX_MCP_TOOLS } from './mcpIntegration.js';
|
|
168
10
|
// Tool definitions for system prompt
|
|
169
11
|
export const AGENT_TOOLS = {
|
|
170
12
|
read_file: {
|
|
@@ -278,10 +120,15 @@ export const AGENT_TOOLS = {
|
|
|
278
120
|
query: { type: 'string', description: 'Search query', required: true },
|
|
279
121
|
},
|
|
280
122
|
},
|
|
123
|
+
minimax_understand_image: {
|
|
124
|
+
name: 'minimax_understand_image',
|
|
125
|
+
description: 'Analyze and understand an image using MiniMax vision model. Can describe images, read text from screenshots, understand diagrams, and answer questions about visual content. Requires a MiniMax API key.',
|
|
126
|
+
parameters: {
|
|
127
|
+
prompt: { type: 'string', description: 'Question or instruction about the image (e.g. "Describe this image", "What text is in this screenshot?")', required: true },
|
|
128
|
+
image_url: { type: 'string', description: 'URL of the image or base64-encoded image data', required: true },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
281
131
|
};
|
|
282
|
-
/**
|
|
283
|
-
* Format tool definitions for system prompt (text-based fallback)
|
|
284
|
-
*/
|
|
285
132
|
/**
|
|
286
133
|
* Get filtered tool entries (excludes provider-specific tools when API key not available)
|
|
287
134
|
*/
|
|
@@ -296,6 +143,9 @@ function getFilteredToolEntries() {
|
|
|
296
143
|
return true;
|
|
297
144
|
});
|
|
298
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Format tool definitions for system prompt (text-based fallback)
|
|
148
|
+
*/
|
|
299
149
|
export function formatToolDefinitions() {
|
|
300
150
|
const lines = [];
|
|
301
151
|
for (const [name, tool] of getFilteredToolEntries()) {
|
|
@@ -320,32 +170,20 @@ export function getOpenAITools() {
|
|
|
320
170
|
for (const [param, info] of Object.entries(tool.parameters)) {
|
|
321
171
|
const paramInfo = info;
|
|
322
172
|
if (paramInfo.type === 'array') {
|
|
323
|
-
properties[param] = {
|
|
324
|
-
type: 'array',
|
|
325
|
-
description: paramInfo.description,
|
|
326
|
-
items: { type: 'string' },
|
|
327
|
-
};
|
|
173
|
+
properties[param] = { type: 'array', description: paramInfo.description, items: { type: 'string' } };
|
|
328
174
|
}
|
|
329
175
|
else {
|
|
330
|
-
properties[param] = {
|
|
331
|
-
type: paramInfo.type,
|
|
332
|
-
description: paramInfo.description,
|
|
333
|
-
};
|
|
176
|
+
properties[param] = { type: paramInfo.type, description: paramInfo.description };
|
|
334
177
|
}
|
|
335
|
-
if (paramInfo.required)
|
|
178
|
+
if (paramInfo.required)
|
|
336
179
|
required.push(param);
|
|
337
|
-
}
|
|
338
180
|
}
|
|
339
181
|
return {
|
|
340
182
|
type: 'function',
|
|
341
183
|
function: {
|
|
342
184
|
name,
|
|
343
185
|
description: tool.description,
|
|
344
|
-
parameters: {
|
|
345
|
-
type: 'object',
|
|
346
|
-
properties,
|
|
347
|
-
required,
|
|
348
|
-
},
|
|
186
|
+
parameters: { type: 'object', properties, required },
|
|
349
187
|
},
|
|
350
188
|
};
|
|
351
189
|
});
|
|
@@ -360,998 +198,21 @@ export function getAnthropicTools() {
|
|
|
360
198
|
for (const [param, info] of Object.entries(tool.parameters)) {
|
|
361
199
|
const paramInfo = info;
|
|
362
200
|
if (paramInfo.type === 'array') {
|
|
363
|
-
properties[param] = {
|
|
364
|
-
type: 'array',
|
|
365
|
-
description: paramInfo.description,
|
|
366
|
-
items: { type: 'string' },
|
|
367
|
-
};
|
|
201
|
+
properties[param] = { type: 'array', description: paramInfo.description, items: { type: 'string' } };
|
|
368
202
|
}
|
|
369
203
|
else {
|
|
370
|
-
properties[param] = {
|
|
371
|
-
type: paramInfo.type,
|
|
372
|
-
description: paramInfo.description,
|
|
373
|
-
};
|
|
204
|
+
properties[param] = { type: paramInfo.type, description: paramInfo.description };
|
|
374
205
|
}
|
|
375
|
-
if (paramInfo.required)
|
|
206
|
+
if (paramInfo.required)
|
|
376
207
|
required.push(param);
|
|
377
|
-
}
|
|
378
208
|
}
|
|
379
209
|
return {
|
|
380
210
|
name,
|
|
381
211
|
description: tool.description,
|
|
382
|
-
input_schema: {
|
|
383
|
-
type: 'object',
|
|
384
|
-
properties,
|
|
385
|
-
required,
|
|
386
|
-
},
|
|
212
|
+
input_schema: { type: 'object', properties, required },
|
|
387
213
|
};
|
|
388
214
|
});
|
|
389
215
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Normalize tool name to lowercase with underscores
|
|
395
|
-
*/
|
|
396
|
-
function normalizeToolName(name) {
|
|
397
|
-
const toolNameMap = {
|
|
398
|
-
'executecommand': 'execute_command',
|
|
399
|
-
'execute_command': 'execute_command',
|
|
400
|
-
'readfile': 'read_file',
|
|
401
|
-
'read_file': 'read_file',
|
|
402
|
-
'writefile': 'write_file',
|
|
403
|
-
'write_file': 'write_file',
|
|
404
|
-
'editfile': 'edit_file',
|
|
405
|
-
'edit_file': 'edit_file',
|
|
406
|
-
'deletefile': 'delete_file',
|
|
407
|
-
'delete_file': 'delete_file',
|
|
408
|
-
'listfiles': 'list_files',
|
|
409
|
-
'list_files': 'list_files',
|
|
410
|
-
'searchcode': 'search_code',
|
|
411
|
-
'search_code': 'search_code',
|
|
412
|
-
'createdirectory': 'create_directory',
|
|
413
|
-
'create_directory': 'create_directory',
|
|
414
|
-
'findfiles': 'find_files',
|
|
415
|
-
'find_files': 'find_files',
|
|
416
|
-
'fetchurl': 'fetch_url',
|
|
417
|
-
'fetch_url': 'fetch_url',
|
|
418
|
-
};
|
|
419
|
-
const lower = name.toLowerCase().replace(/-/g, '_');
|
|
420
|
-
return toolNameMap[lower] || lower;
|
|
421
|
-
}
|
|
422
|
-
export function parseOpenAIToolCalls(toolCalls) {
|
|
423
|
-
if (!toolCalls || !Array.isArray(toolCalls))
|
|
424
|
-
return [];
|
|
425
|
-
const parsed = [];
|
|
426
|
-
for (const tc of toolCalls) {
|
|
427
|
-
const toolName = normalizeToolName(tc.function?.name || '');
|
|
428
|
-
if (!toolName)
|
|
429
|
-
continue;
|
|
430
|
-
let parameters = {};
|
|
431
|
-
const rawArgs = tc.function?.arguments || '{}';
|
|
432
|
-
try {
|
|
433
|
-
parameters = JSON.parse(rawArgs);
|
|
434
|
-
}
|
|
435
|
-
catch (e) {
|
|
436
|
-
// JSON parsing failed - likely truncated response
|
|
437
|
-
// Try to extract what we can from partial JSON
|
|
438
|
-
debug(`Failed to parse tool arguments for ${toolName}, attempting partial extraction...`);
|
|
439
|
-
debug('Raw args preview:', rawArgs.substring(0, 200));
|
|
440
|
-
const partialParams = extractPartialToolParams(toolName, rawArgs);
|
|
441
|
-
if (partialParams) {
|
|
442
|
-
debug(`Successfully extracted partial params for ${toolName}:`, Object.keys(partialParams));
|
|
443
|
-
parameters = partialParams;
|
|
444
|
-
}
|
|
445
|
-
else {
|
|
446
|
-
debug(`Could not extract params, skipping ${toolName}`);
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
// Validate required parameters for specific tools
|
|
451
|
-
// For write_file, we need at least a path (content can be empty string or placeholder)
|
|
452
|
-
if (toolName === 'write_file' && !parameters.path) {
|
|
453
|
-
debug(`Skipping write_file - missing path. Raw args:`, rawArgs.substring(0, 200));
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
if (toolName === 'read_file' && !parameters.path) {
|
|
457
|
-
debug(`Skipping read_file - missing path`);
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
if (toolName === 'edit_file' && (!parameters.path || parameters.old_text === undefined || parameters.new_text === undefined)) {
|
|
461
|
-
debug(`Skipping edit_file - missing required params`);
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
parsed.push({
|
|
465
|
-
tool: toolName,
|
|
466
|
-
parameters,
|
|
467
|
-
id: tc.id,
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
return parsed;
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Extract parameters from truncated/partial JSON for tool calls
|
|
474
|
-
* This is a fallback when JSON.parse fails due to API truncation
|
|
475
|
-
*/
|
|
476
|
-
function extractPartialToolParams(toolName, rawArgs) {
|
|
477
|
-
try {
|
|
478
|
-
// For write_file, try to extract path and content
|
|
479
|
-
if (toolName === 'write_file') {
|
|
480
|
-
const pathMatch = rawArgs.match(/"path"\s*:\s*"([^"]+)"/);
|
|
481
|
-
if (pathMatch) {
|
|
482
|
-
// Try to extract content - it may be truncated
|
|
483
|
-
const contentMatch = rawArgs.match(/"content"\s*:\s*"([\s\S]*?)(?:"|$)/);
|
|
484
|
-
if (contentMatch) {
|
|
485
|
-
// Unescape the content
|
|
486
|
-
let content = contentMatch[1];
|
|
487
|
-
content = content
|
|
488
|
-
.replace(/\\n/g, '\n')
|
|
489
|
-
.replace(/\\t/g, '\t')
|
|
490
|
-
.replace(/\\r/g, '\r')
|
|
491
|
-
.replace(/\\"/g, '"')
|
|
492
|
-
.replace(/\\\\/g, '\\');
|
|
493
|
-
// If content appears truncated (doesn't end properly), add a comment
|
|
494
|
-
if (!content.endsWith('\n') && !content.endsWith('}') && !content.endsWith(';') && !content.endsWith('>')) {
|
|
495
|
-
content += '\n<!-- Content may be truncated -->\n';
|
|
496
|
-
}
|
|
497
|
-
return { path: pathMatch[1], content };
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
// Path found but content completely missing or malformed
|
|
501
|
-
// Return with empty content placeholder so the file is at least created
|
|
502
|
-
return { path: pathMatch[1], content: '<!-- Content was truncated by API -->\n' };
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
// For read_file, just need path
|
|
507
|
-
if (toolName === 'read_file') {
|
|
508
|
-
const pathMatch = rawArgs.match(/"path"\s*:\s*"([^"]+)"/);
|
|
509
|
-
if (pathMatch) {
|
|
510
|
-
return { path: pathMatch[1] };
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
// For list_files, just need path
|
|
514
|
-
if (toolName === 'list_files') {
|
|
515
|
-
const pathMatch = rawArgs.match(/"path"\s*:\s*"([^"]+)"/);
|
|
516
|
-
if (pathMatch) {
|
|
517
|
-
return { path: pathMatch[1] };
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
// For create_directory, just need path
|
|
521
|
-
if (toolName === 'create_directory') {
|
|
522
|
-
const pathMatch = rawArgs.match(/"path"\s*:\s*"([^"]+)"/);
|
|
523
|
-
if (pathMatch) {
|
|
524
|
-
return { path: pathMatch[1] };
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
// For edit_file, need path, old_text, new_text
|
|
528
|
-
if (toolName === 'edit_file') {
|
|
529
|
-
const pathMatch = rawArgs.match(/"path"\s*:\s*"([^"]+)"/);
|
|
530
|
-
const oldTextMatch = rawArgs.match(/"old_text"\s*:\s*"([\s\S]*?)(?:"|$)/);
|
|
531
|
-
const newTextMatch = rawArgs.match(/"new_text"\s*:\s*"([\s\S]*?)(?:"|$)/);
|
|
532
|
-
if (pathMatch && oldTextMatch && newTextMatch) {
|
|
533
|
-
return {
|
|
534
|
-
path: pathMatch[1],
|
|
535
|
-
old_text: oldTextMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"'),
|
|
536
|
-
new_text: newTextMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"'),
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
// For execute_command
|
|
541
|
-
if (toolName === 'execute_command') {
|
|
542
|
-
const commandMatch = rawArgs.match(/"command"\s*:\s*"([^"]+)"/);
|
|
543
|
-
if (commandMatch) {
|
|
544
|
-
const argsMatch = rawArgs.match(/"args"\s*:\s*\[([\s\S]*?)\]/);
|
|
545
|
-
let args = [];
|
|
546
|
-
if (argsMatch) {
|
|
547
|
-
const argStrings = argsMatch[1].match(/"([^"]+)"/g);
|
|
548
|
-
if (argStrings) {
|
|
549
|
-
args = argStrings.map(s => s.replace(/"/g, ''));
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
return { command: commandMatch[1], args };
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
catch (e) {
|
|
558
|
-
debug('Error in extractPartialToolParams:', e);
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* Parse tool calls from Anthropic response
|
|
564
|
-
*/
|
|
565
|
-
export function parseAnthropicToolCalls(content) {
|
|
566
|
-
if (!content || !Array.isArray(content))
|
|
567
|
-
return [];
|
|
568
|
-
return content
|
|
569
|
-
.filter(block => block.type === 'tool_use')
|
|
570
|
-
.map(block => ({
|
|
571
|
-
tool: normalizeToolName(block.name || ''),
|
|
572
|
-
parameters: block.input || {},
|
|
573
|
-
id: block.id,
|
|
574
|
-
}))
|
|
575
|
-
.filter(tc => tc.tool);
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Parse tool calls from LLM response
|
|
579
|
-
* Supports multiple formats:
|
|
580
|
-
* - <tool_call>{"tool": "name", "parameters": {...}}</tool_call>
|
|
581
|
-
* - <toolcall>{"tool": "name", "parameters": {...}}</toolcall>
|
|
582
|
-
* - <toolcall>toolname{"parameters": {...}}</toolcall>
|
|
583
|
-
* - ```tool\n{"tool": "name", "parameters": {...}}\n```
|
|
584
|
-
* - Direct JSON blocks with tool property
|
|
585
|
-
*/
|
|
586
|
-
export function parseToolCalls(response) {
|
|
587
|
-
const toolCalls = [];
|
|
588
|
-
// Format 1: <tool_call>...</tool_call> or <toolcall>...</toolcall> with JSON inside
|
|
589
|
-
const toolCallRegex = /<tool_?call>\s*([\s\S]*?)\s*<\/tool_?call>/gi;
|
|
590
|
-
let match;
|
|
591
|
-
while ((match = toolCallRegex.exec(response)) !== null) {
|
|
592
|
-
const parsed = tryParseToolCall(match[1].trim());
|
|
593
|
-
if (parsed)
|
|
594
|
-
toolCalls.push(parsed);
|
|
595
|
-
}
|
|
596
|
-
// Format 2: <toolcall>toolname{...} or <toolcall>toolname, "parameters": {...}
|
|
597
|
-
const malformedRegex = /<toolcall>(\w+)[\s,]*(?:"parameters"\s*:\s*)?(\{[\s\S]*?\})/gi;
|
|
598
|
-
while ((match = malformedRegex.exec(response)) !== null) {
|
|
599
|
-
const toolName = match[1].toLowerCase();
|
|
600
|
-
let jsonPart = match[2];
|
|
601
|
-
// Map common variations to actual tool names
|
|
602
|
-
const toolNameMap = {
|
|
603
|
-
'executecommand': 'execute_command',
|
|
604
|
-
'execute_command': 'execute_command',
|
|
605
|
-
'readfile': 'read_file',
|
|
606
|
-
'read_file': 'read_file',
|
|
607
|
-
'writefile': 'write_file',
|
|
608
|
-
'write_file': 'write_file',
|
|
609
|
-
'editfile': 'edit_file',
|
|
610
|
-
'edit_file': 'edit_file',
|
|
611
|
-
'deletefile': 'delete_file',
|
|
612
|
-
'delete_file': 'delete_file',
|
|
613
|
-
'listfiles': 'list_files',
|
|
614
|
-
'list_files': 'list_files',
|
|
615
|
-
'searchcode': 'search_code',
|
|
616
|
-
'search_code': 'search_code',
|
|
617
|
-
'findfiles': 'find_files',
|
|
618
|
-
'find_files': 'find_files',
|
|
619
|
-
};
|
|
620
|
-
const actualToolName = toolNameMap[toolName] || toolName;
|
|
621
|
-
try {
|
|
622
|
-
const parsed = JSON.parse(jsonPart);
|
|
623
|
-
const params = parsed.parameters || parsed;
|
|
624
|
-
toolCalls.push({
|
|
625
|
-
tool: actualToolName,
|
|
626
|
-
parameters: params,
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
catch {
|
|
630
|
-
// Try to extract parameters manually
|
|
631
|
-
const params = tryExtractParams(jsonPart);
|
|
632
|
-
if (params) {
|
|
633
|
-
toolCalls.push({
|
|
634
|
-
tool: actualToolName,
|
|
635
|
-
parameters: params,
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
// Format 2b: Even more malformed - toolname followed by loose JSON-like content
|
|
641
|
-
const looseRegex = /<toolcall>(\w+)[,\s]+["']?parameters["']?\s*:\s*(\{[\s\S]*?\})(?:<\/toolcall>|<|$)/gi;
|
|
642
|
-
while ((match = looseRegex.exec(response)) !== null) {
|
|
643
|
-
// Skip if already parsed
|
|
644
|
-
const toolName = match[1].toLowerCase();
|
|
645
|
-
const toolNameMap = {
|
|
646
|
-
'executecommand': 'execute_command',
|
|
647
|
-
'readfile': 'read_file',
|
|
648
|
-
'writefile': 'write_file',
|
|
649
|
-
'editfile': 'edit_file',
|
|
650
|
-
'deletefile': 'delete_file',
|
|
651
|
-
'listfiles': 'list_files',
|
|
652
|
-
'searchcode': 'search_code',
|
|
653
|
-
'findfiles': 'find_files',
|
|
654
|
-
};
|
|
655
|
-
const actualToolName = toolNameMap[toolName] || toolName;
|
|
656
|
-
// Check if we already have this tool call
|
|
657
|
-
if (toolCalls.some(t => t.tool === actualToolName))
|
|
658
|
-
continue;
|
|
659
|
-
const params = tryExtractParams(match[2]);
|
|
660
|
-
if (params) {
|
|
661
|
-
toolCalls.push({
|
|
662
|
-
tool: actualToolName,
|
|
663
|
-
parameters: params,
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
// Format 3: ```tool or ```json with tool calls
|
|
668
|
-
const codeBlockRegex = /```(?:tool|json)?\s*\n?([\s\S]*?)\n?```/g;
|
|
669
|
-
while ((match = codeBlockRegex.exec(response)) !== null) {
|
|
670
|
-
const content = match[1].trim();
|
|
671
|
-
// Only parse if it looks like a tool call JSON
|
|
672
|
-
if (content.includes('"tool"') || content.includes('"parameters"')) {
|
|
673
|
-
const parsed = tryParseToolCall(content);
|
|
674
|
-
if (parsed && !toolCalls.some(t => t.tool === parsed.tool && JSON.stringify(t.parameters) === JSON.stringify(parsed.parameters))) {
|
|
675
|
-
toolCalls.push(parsed);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
// Format 4: Inline JSON objects with tool property (fallback)
|
|
680
|
-
if (toolCalls.length === 0) {
|
|
681
|
-
const jsonRegex = /\{[^{}]*"tool"\s*:\s*"[^"]+"\s*,\s*"parameters"\s*:\s*\{[^{}]*\}[^{}]*\}/g;
|
|
682
|
-
while ((match = jsonRegex.exec(response)) !== null) {
|
|
683
|
-
const parsed = tryParseToolCall(match[0]);
|
|
684
|
-
if (parsed)
|
|
685
|
-
toolCalls.push(parsed);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
return toolCalls;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Try to extract parameters from a malformed JSON string
|
|
692
|
-
*/
|
|
693
|
-
function tryExtractParams(str) {
|
|
694
|
-
const params = {};
|
|
695
|
-
// Extract "args": [...]
|
|
696
|
-
const argsMatch = str.match(/"args"\s*:\s*\[([\s\S]*?)\]/);
|
|
697
|
-
if (argsMatch) {
|
|
698
|
-
try {
|
|
699
|
-
params.args = JSON.parse(`[${argsMatch[1]}]`);
|
|
700
|
-
}
|
|
701
|
-
catch {
|
|
702
|
-
// Try to extract string array manually
|
|
703
|
-
const items = argsMatch[1].match(/"([^"]*)"/g);
|
|
704
|
-
if (items) {
|
|
705
|
-
params.args = items.map(i => i.replace(/"/g, ''));
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
// Extract "command": "..."
|
|
710
|
-
const cmdMatch = str.match(/"command"\s*:\s*"([^"]*)"/);
|
|
711
|
-
if (cmdMatch) {
|
|
712
|
-
params.command = cmdMatch[1];
|
|
713
|
-
}
|
|
714
|
-
// Extract "path": "..."
|
|
715
|
-
const pathMatch = str.match(/"path"\s*:\s*"([^"]*)"/);
|
|
716
|
-
if (pathMatch) {
|
|
717
|
-
params.path = pathMatch[1];
|
|
718
|
-
}
|
|
719
|
-
// Extract "content": "..."
|
|
720
|
-
const contentMatch = str.match(/"content"\s*:\s*"([\s\S]*?)(?<!\\)"/);
|
|
721
|
-
if (contentMatch) {
|
|
722
|
-
params.content = contentMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
|
|
723
|
-
}
|
|
724
|
-
// Extract "old_text" and "new_text"
|
|
725
|
-
const oldTextMatch = str.match(/"old_text"\s*:\s*"([\s\S]*?)(?<!\\)"/);
|
|
726
|
-
if (oldTextMatch) {
|
|
727
|
-
params.old_text = oldTextMatch[1];
|
|
728
|
-
}
|
|
729
|
-
const newTextMatch = str.match(/"new_text"\s*:\s*"([\s\S]*?)(?<!\\)"/);
|
|
730
|
-
if (newTextMatch) {
|
|
731
|
-
params.new_text = newTextMatch[1];
|
|
732
|
-
}
|
|
733
|
-
// Extract "pattern": "..."
|
|
734
|
-
const patternMatch = str.match(/"pattern"\s*:\s*"([^"]*)"/);
|
|
735
|
-
if (patternMatch) {
|
|
736
|
-
params.pattern = patternMatch[1];
|
|
737
|
-
}
|
|
738
|
-
// Extract "recursive": true/false
|
|
739
|
-
const recursiveMatch = str.match(/"recursive"\s*:\s*(true|false)/i);
|
|
740
|
-
if (recursiveMatch) {
|
|
741
|
-
params.recursive = recursiveMatch[1].toLowerCase() === 'true';
|
|
742
|
-
}
|
|
743
|
-
return Object.keys(params).length > 0 ? params : null;
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Try to parse a string as a tool call
|
|
747
|
-
*/
|
|
748
|
-
function tryParseToolCall(str) {
|
|
749
|
-
try {
|
|
750
|
-
// Clean up common issues
|
|
751
|
-
let cleaned = str
|
|
752
|
-
.replace(/[\r\n]+/g, ' ') // Remove newlines
|
|
753
|
-
.replace(/,\s*}/g, '}') // Remove trailing commas
|
|
754
|
-
.replace(/,\s*]/g, ']') // Remove trailing commas in arrays
|
|
755
|
-
.trim();
|
|
756
|
-
const parsed = JSON.parse(cleaned);
|
|
757
|
-
if (parsed.tool && typeof parsed.tool === 'string') {
|
|
758
|
-
return {
|
|
759
|
-
tool: normalizeToolName(parsed.tool),
|
|
760
|
-
parameters: parsed.parameters || {},
|
|
761
|
-
id: parsed.id,
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
catch {
|
|
766
|
-
// Try to extract tool name and parameters manually for malformed JSON
|
|
767
|
-
const toolMatch = str.match(/"tool"\s*:\s*"([^"]+)"/i);
|
|
768
|
-
if (toolMatch) {
|
|
769
|
-
const tool = normalizeToolName(toolMatch[1]);
|
|
770
|
-
const params = {};
|
|
771
|
-
// Extract simple string parameters
|
|
772
|
-
const paramMatches = str.matchAll(/"(\w+)"\s*:\s*"([^"]*)"/g);
|
|
773
|
-
for (const m of paramMatches) {
|
|
774
|
-
if (m[1] !== 'tool') {
|
|
775
|
-
params[m[1]] = m[2];
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
// Extract boolean parameters
|
|
779
|
-
const boolMatches = str.matchAll(/"(\w+)"\s*:\s*(true|false)/gi);
|
|
780
|
-
for (const m of boolMatches) {
|
|
781
|
-
params[m[1]] = m[2].toLowerCase() === 'true';
|
|
782
|
-
}
|
|
783
|
-
if (Object.keys(params).length > 0 || AGENT_TOOLS[tool]) {
|
|
784
|
-
return { tool, parameters: params };
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
return null;
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Validate path is within project
|
|
792
|
-
*/
|
|
793
|
-
function validatePath(path, projectRoot) {
|
|
794
|
-
// If path is absolute but starts with projectRoot, convert to relative
|
|
795
|
-
let normalizedPath = path;
|
|
796
|
-
if (isAbsolute(path) && path.startsWith(projectRoot)) {
|
|
797
|
-
normalizedPath = relative(projectRoot, path);
|
|
798
|
-
}
|
|
799
|
-
// If still absolute and doesn't match projectRoot, reject it
|
|
800
|
-
if (isAbsolute(normalizedPath)) {
|
|
801
|
-
return { valid: false, absolutePath: normalizedPath, error: `Absolute path '${path}' not allowed. Use relative paths.` };
|
|
802
|
-
}
|
|
803
|
-
const absolutePath = resolve(projectRoot, normalizedPath);
|
|
804
|
-
const relativePath = relative(projectRoot, absolutePath);
|
|
805
|
-
if (relativePath.startsWith('..')) {
|
|
806
|
-
return { valid: false, absolutePath, error: `Path '${path}' is outside project directory` };
|
|
807
|
-
}
|
|
808
|
-
return { valid: true, absolutePath };
|
|
809
|
-
}
|
|
810
|
-
/**
|
|
811
|
-
* Execute a tool call
|
|
812
|
-
*/
|
|
813
|
-
export async function executeTool(toolCall, projectRoot) {
|
|
814
|
-
// Normalize tool name to handle case variations (WRITE_FILE -> write_file)
|
|
815
|
-
const tool = normalizeToolName(toolCall.tool);
|
|
816
|
-
const parameters = toolCall.parameters;
|
|
817
|
-
debug(`Executing tool: ${tool}`, parameters.path || parameters.command || '');
|
|
818
|
-
try {
|
|
819
|
-
switch (tool) {
|
|
820
|
-
case 'read_file': {
|
|
821
|
-
const path = parameters.path;
|
|
822
|
-
if (!path) {
|
|
823
|
-
return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
|
|
824
|
-
}
|
|
825
|
-
const validation = validatePath(path, projectRoot);
|
|
826
|
-
if (!validation.valid) {
|
|
827
|
-
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
828
|
-
}
|
|
829
|
-
if (!existsSync(validation.absolutePath)) {
|
|
830
|
-
return { success: false, output: '', error: `File not found: ${path}`, tool, parameters };
|
|
831
|
-
}
|
|
832
|
-
const stat = statSync(validation.absolutePath);
|
|
833
|
-
if (stat.isDirectory()) {
|
|
834
|
-
return { success: false, output: '', error: `Path is a directory, not a file: ${path}`, tool, parameters };
|
|
835
|
-
}
|
|
836
|
-
// Limit file size
|
|
837
|
-
if (stat.size > 100 * 1024) { // 100KB
|
|
838
|
-
return { success: false, output: '', error: `File too large (${stat.size} bytes). Max: 100KB`, tool, parameters };
|
|
839
|
-
}
|
|
840
|
-
const content = readFileSync(validation.absolutePath, 'utf-8');
|
|
841
|
-
return { success: true, output: content, tool, parameters };
|
|
842
|
-
}
|
|
843
|
-
case 'write_file': {
|
|
844
|
-
const path = parameters.path;
|
|
845
|
-
let content = parameters.content;
|
|
846
|
-
if (!path) {
|
|
847
|
-
debug('write_file failed: missing path');
|
|
848
|
-
return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
|
|
849
|
-
}
|
|
850
|
-
// Allow empty content or provide placeholder for truncated responses
|
|
851
|
-
if (content === undefined || content === null) {
|
|
852
|
-
debug('write_file: content was undefined, using placeholder');
|
|
853
|
-
content = '<!-- Content was not provided -->\n';
|
|
854
|
-
}
|
|
855
|
-
const validation = validatePath(path, projectRoot);
|
|
856
|
-
if (!validation.valid) {
|
|
857
|
-
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
858
|
-
}
|
|
859
|
-
// Create directory if needed
|
|
860
|
-
const dir = dirname(validation.absolutePath);
|
|
861
|
-
if (!existsSync(dir)) {
|
|
862
|
-
mkdirSync(dir, { recursive: true });
|
|
863
|
-
}
|
|
864
|
-
// Record for undo
|
|
865
|
-
recordWrite(validation.absolutePath);
|
|
866
|
-
const existed = existsSync(validation.absolutePath);
|
|
867
|
-
writeFileSync(validation.absolutePath, content, 'utf-8');
|
|
868
|
-
const action = existed ? 'Updated' : 'Created';
|
|
869
|
-
return { success: true, output: `${action} file: ${path}`, tool, parameters };
|
|
870
|
-
}
|
|
871
|
-
case 'edit_file': {
|
|
872
|
-
const path = parameters.path;
|
|
873
|
-
const oldText = parameters.old_text;
|
|
874
|
-
const newText = parameters.new_text;
|
|
875
|
-
if (!path || oldText === undefined || newText === undefined) {
|
|
876
|
-
return { success: false, output: '', error: 'Missing required parameters', tool, parameters };
|
|
877
|
-
}
|
|
878
|
-
const validation = validatePath(path, projectRoot);
|
|
879
|
-
if (!validation.valid) {
|
|
880
|
-
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
881
|
-
}
|
|
882
|
-
if (!existsSync(validation.absolutePath)) {
|
|
883
|
-
return { success: false, output: '', error: `File not found: ${path}`, tool, parameters };
|
|
884
|
-
}
|
|
885
|
-
const content = readFileSync(validation.absolutePath, 'utf-8');
|
|
886
|
-
if (!content.includes(oldText)) {
|
|
887
|
-
return { success: false, output: '', error: `Text not found in file. Make sure old_text matches exactly.`, tool, parameters };
|
|
888
|
-
}
|
|
889
|
-
// Count occurrences to prevent ambiguous replacements
|
|
890
|
-
let matchCount = 0;
|
|
891
|
-
let searchPos = 0;
|
|
892
|
-
while ((searchPos = content.indexOf(oldText, searchPos)) !== -1) {
|
|
893
|
-
matchCount++;
|
|
894
|
-
searchPos += oldText.length;
|
|
895
|
-
}
|
|
896
|
-
if (matchCount > 1) {
|
|
897
|
-
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 };
|
|
898
|
-
}
|
|
899
|
-
// Record for undo
|
|
900
|
-
recordEdit(validation.absolutePath);
|
|
901
|
-
const newContent = content.replace(oldText, newText);
|
|
902
|
-
writeFileSync(validation.absolutePath, newContent, 'utf-8');
|
|
903
|
-
return { success: true, output: `Edited file: ${path}`, tool, parameters };
|
|
904
|
-
}
|
|
905
|
-
case 'delete_file': {
|
|
906
|
-
const path = parameters.path;
|
|
907
|
-
if (!path) {
|
|
908
|
-
return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
|
|
909
|
-
}
|
|
910
|
-
const validation = validatePath(path, projectRoot);
|
|
911
|
-
if (!validation.valid) {
|
|
912
|
-
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
913
|
-
}
|
|
914
|
-
if (!existsSync(validation.absolutePath)) {
|
|
915
|
-
return { success: false, output: '', error: `Path not found: ${path}`, tool, parameters };
|
|
916
|
-
}
|
|
917
|
-
// Record for undo (before delete)
|
|
918
|
-
recordDelete(validation.absolutePath);
|
|
919
|
-
const stat = statSync(validation.absolutePath);
|
|
920
|
-
if (stat.isDirectory()) {
|
|
921
|
-
// Delete directory recursively
|
|
922
|
-
rmSync(validation.absolutePath, { recursive: true, force: true });
|
|
923
|
-
return { success: true, output: `Deleted directory: ${path}`, tool, parameters };
|
|
924
|
-
}
|
|
925
|
-
else {
|
|
926
|
-
unlinkSync(validation.absolutePath);
|
|
927
|
-
return { success: true, output: `Deleted file: ${path}`, tool, parameters };
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
case 'list_files': {
|
|
931
|
-
const path = parameters.path || '.';
|
|
932
|
-
const recursive = parameters.recursive || false;
|
|
933
|
-
const validation = validatePath(path, projectRoot);
|
|
934
|
-
if (!validation.valid) {
|
|
935
|
-
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
936
|
-
}
|
|
937
|
-
if (!existsSync(validation.absolutePath)) {
|
|
938
|
-
return { success: false, output: '', error: `Directory not found: ${path}`, tool, parameters };
|
|
939
|
-
}
|
|
940
|
-
const stat = statSync(validation.absolutePath);
|
|
941
|
-
if (!stat.isDirectory()) {
|
|
942
|
-
return { success: false, output: '', error: `Path is not a directory: ${path}`, tool, parameters };
|
|
943
|
-
}
|
|
944
|
-
const files = listDirectory(validation.absolutePath, projectRoot, recursive);
|
|
945
|
-
return { success: true, output: files.join('\n'), tool, parameters };
|
|
946
|
-
}
|
|
947
|
-
case 'create_directory': {
|
|
948
|
-
const path = parameters.path;
|
|
949
|
-
if (!path) {
|
|
950
|
-
return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
|
|
951
|
-
}
|
|
952
|
-
const validation = validatePath(path, projectRoot);
|
|
953
|
-
if (!validation.valid) {
|
|
954
|
-
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
955
|
-
}
|
|
956
|
-
if (existsSync(validation.absolutePath)) {
|
|
957
|
-
const stat = statSync(validation.absolutePath);
|
|
958
|
-
if (stat.isDirectory()) {
|
|
959
|
-
return { success: true, output: `Directory already exists: ${path}`, tool, parameters };
|
|
960
|
-
}
|
|
961
|
-
else {
|
|
962
|
-
return { success: false, output: '', error: `Path exists but is a file: ${path}`, tool, parameters };
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
// Record for undo
|
|
966
|
-
recordMkdir(validation.absolutePath);
|
|
967
|
-
mkdirSync(validation.absolutePath, { recursive: true });
|
|
968
|
-
return { success: true, output: `Created directory: ${path}`, tool, parameters };
|
|
969
|
-
}
|
|
970
|
-
case 'execute_command': {
|
|
971
|
-
const command = parameters.command;
|
|
972
|
-
const args = parameters.args || [];
|
|
973
|
-
if (!command) {
|
|
974
|
-
return { success: false, output: '', error: 'Missing required parameter: command', tool, parameters };
|
|
975
|
-
}
|
|
976
|
-
// Record command (can't undo but tracked)
|
|
977
|
-
recordCommand(command, args);
|
|
978
|
-
const result = executeCommand(command, args, {
|
|
979
|
-
cwd: projectRoot,
|
|
980
|
-
projectRoot,
|
|
981
|
-
timeout: 120000, // 2 minutes for commands
|
|
982
|
-
});
|
|
983
|
-
if (result.success) {
|
|
984
|
-
return { success: true, output: result.stdout || '(no output)', tool, parameters };
|
|
985
|
-
}
|
|
986
|
-
else {
|
|
987
|
-
return { success: false, output: result.stdout, error: result.stderr, tool, parameters };
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
case 'search_code': {
|
|
991
|
-
const pattern = parameters.pattern;
|
|
992
|
-
const searchPath = parameters.path || '.';
|
|
993
|
-
if (!pattern) {
|
|
994
|
-
return { success: false, output: '', error: 'Missing required parameter: pattern', tool, parameters };
|
|
995
|
-
}
|
|
996
|
-
const validation = validatePath(searchPath, projectRoot);
|
|
997
|
-
if (!validation.valid) {
|
|
998
|
-
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
999
|
-
}
|
|
1000
|
-
// Use grep for search
|
|
1001
|
-
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], {
|
|
1002
|
-
cwd: projectRoot,
|
|
1003
|
-
projectRoot,
|
|
1004
|
-
timeout: 30000,
|
|
1005
|
-
});
|
|
1006
|
-
if (result.exitCode === 0) {
|
|
1007
|
-
// Limit output
|
|
1008
|
-
const lines = result.stdout.split('\n').slice(0, 50);
|
|
1009
|
-
return { success: true, output: lines.join('\n') || 'No matches found', tool, parameters };
|
|
1010
|
-
}
|
|
1011
|
-
else if (result.exitCode === 1) {
|
|
1012
|
-
return { success: true, output: 'No matches found', tool, parameters };
|
|
1013
|
-
}
|
|
1014
|
-
else {
|
|
1015
|
-
return { success: false, output: '', error: result.stderr || 'Search failed', tool, parameters };
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
case 'find_files': {
|
|
1019
|
-
const pattern = parameters.pattern;
|
|
1020
|
-
const searchPath = parameters.path || '.';
|
|
1021
|
-
if (!pattern) {
|
|
1022
|
-
return { success: false, output: '', error: 'Missing required parameter: pattern', tool, parameters };
|
|
1023
|
-
}
|
|
1024
|
-
const validation = validatePath(searchPath, projectRoot);
|
|
1025
|
-
if (!validation.valid) {
|
|
1026
|
-
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
1027
|
-
}
|
|
1028
|
-
// Use find with -name or -path for glob matching
|
|
1029
|
-
// Convert glob pattern to find-compatible format
|
|
1030
|
-
const findArgs = [validation.absolutePath];
|
|
1031
|
-
// Ignore common directories
|
|
1032
|
-
findArgs.push('(', '-name', 'node_modules', '-o', '-name', '.git', '-o', '-name', '.codeep', '-o', '-name', 'dist', '-o', '-name', 'build', '-o', '-name', '.next', ')', '-prune', '-o');
|
|
1033
|
-
if (pattern.includes('/')) {
|
|
1034
|
-
// Path-based pattern: use -path
|
|
1035
|
-
findArgs.push('-path', `*/${pattern}`, '-print');
|
|
1036
|
-
}
|
|
1037
|
-
else {
|
|
1038
|
-
// Name-based pattern: use -name
|
|
1039
|
-
findArgs.push('-name', pattern, '-print');
|
|
1040
|
-
}
|
|
1041
|
-
const result = executeCommand('find', findArgs, {
|
|
1042
|
-
cwd: projectRoot,
|
|
1043
|
-
projectRoot,
|
|
1044
|
-
timeout: 15000,
|
|
1045
|
-
});
|
|
1046
|
-
if (result.exitCode === 0 || result.stdout) {
|
|
1047
|
-
const files = result.stdout.split('\n').filter(Boolean);
|
|
1048
|
-
// Make paths relative to project root
|
|
1049
|
-
const relativePaths = files.map(f => {
|
|
1050
|
-
const rel = relative(projectRoot, f);
|
|
1051
|
-
return rel || f;
|
|
1052
|
-
}).slice(0, 100); // Limit to 100 results
|
|
1053
|
-
if (relativePaths.length === 0) {
|
|
1054
|
-
return { success: true, output: `No files matching "${pattern}"`, tool, parameters };
|
|
1055
|
-
}
|
|
1056
|
-
return { success: true, output: `Found ${relativePaths.length} file(s):\n${relativePaths.join('\n')}`, tool, parameters };
|
|
1057
|
-
}
|
|
1058
|
-
else {
|
|
1059
|
-
return { success: false, output: '', error: result.stderr || 'Find failed', tool, parameters };
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
case 'fetch_url': {
|
|
1063
|
-
const url = parameters.url;
|
|
1064
|
-
if (!url) {
|
|
1065
|
-
return { success: false, output: '', error: 'Missing required parameter: url', tool, parameters };
|
|
1066
|
-
}
|
|
1067
|
-
// Validate URL
|
|
1068
|
-
try {
|
|
1069
|
-
new URL(url);
|
|
1070
|
-
}
|
|
1071
|
-
catch {
|
|
1072
|
-
return { success: false, output: '', error: 'Invalid URL format', tool, parameters };
|
|
1073
|
-
}
|
|
1074
|
-
// Use curl to fetch URL
|
|
1075
|
-
const result = executeCommand('curl', [
|
|
1076
|
-
'-s', '-L',
|
|
1077
|
-
'-m', '30', // 30 second timeout
|
|
1078
|
-
'-A', 'Codeep/1.0',
|
|
1079
|
-
'--max-filesize', '1000000', // 1MB max
|
|
1080
|
-
url
|
|
1081
|
-
], {
|
|
1082
|
-
cwd: projectRoot,
|
|
1083
|
-
projectRoot,
|
|
1084
|
-
timeout: 35000,
|
|
1085
|
-
});
|
|
1086
|
-
if (result.success) {
|
|
1087
|
-
// Try to extract text content (strip HTML tags for basic display)
|
|
1088
|
-
let content = result.stdout;
|
|
1089
|
-
// If it looks like HTML, convert to readable text
|
|
1090
|
-
if (content.includes('<html') || content.includes('<!DOCTYPE')) {
|
|
1091
|
-
content = htmlToText(content);
|
|
1092
|
-
}
|
|
1093
|
-
// Limit output
|
|
1094
|
-
if (content.length > 10000) {
|
|
1095
|
-
content = content.substring(0, 10000) + '\n\n... (truncated)';
|
|
1096
|
-
}
|
|
1097
|
-
return { success: true, output: content, tool, parameters };
|
|
1098
|
-
}
|
|
1099
|
-
else {
|
|
1100
|
-
return { success: false, output: '', error: result.stderr || 'Failed to fetch URL', tool, parameters };
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
// === Z.AI MCP Tools ===
|
|
1104
|
-
case 'web_search': {
|
|
1105
|
-
const mcpConfig = getZaiMcpConfig();
|
|
1106
|
-
if (!mcpConfig) {
|
|
1107
|
-
return { success: false, output: '', error: 'web_search requires a Z.AI API key. Configure one via /provider z.ai', tool, parameters };
|
|
1108
|
-
}
|
|
1109
|
-
const query = parameters.query;
|
|
1110
|
-
if (!query) {
|
|
1111
|
-
return { success: false, output: '', error: 'Missing required parameter: query', tool, parameters };
|
|
1112
|
-
}
|
|
1113
|
-
const args = { search_query: query };
|
|
1114
|
-
if (parameters.domain_filter)
|
|
1115
|
-
args.search_domain_filter = parameters.domain_filter;
|
|
1116
|
-
if (parameters.recency)
|
|
1117
|
-
args.search_recency_filter = parameters.recency;
|
|
1118
|
-
const result = await callZaiMcp(mcpConfig.endpoints.webSearch, 'webSearchPrime', args, mcpConfig.apiKey);
|
|
1119
|
-
const output = result.length > 15000 ? result.substring(0, 15000) + '\n\n... (truncated)' : result;
|
|
1120
|
-
return { success: true, output, tool, parameters };
|
|
1121
|
-
}
|
|
1122
|
-
case 'web_read': {
|
|
1123
|
-
const mcpConfig = getZaiMcpConfig();
|
|
1124
|
-
if (!mcpConfig) {
|
|
1125
|
-
return { success: false, output: '', error: 'web_read requires a Z.AI API key. Configure one via /provider z.ai', tool, parameters };
|
|
1126
|
-
}
|
|
1127
|
-
const url = parameters.url;
|
|
1128
|
-
if (!url) {
|
|
1129
|
-
return { success: false, output: '', error: 'Missing required parameter: url', tool, parameters };
|
|
1130
|
-
}
|
|
1131
|
-
try {
|
|
1132
|
-
new URL(url);
|
|
1133
|
-
}
|
|
1134
|
-
catch {
|
|
1135
|
-
return { success: false, output: '', error: 'Invalid URL format', tool, parameters };
|
|
1136
|
-
}
|
|
1137
|
-
const args = { url };
|
|
1138
|
-
if (parameters.format)
|
|
1139
|
-
args.return_format = parameters.format;
|
|
1140
|
-
const result = await callZaiMcp(mcpConfig.endpoints.webReader, 'webReader', args, mcpConfig.apiKey);
|
|
1141
|
-
const output = result.length > 15000 ? result.substring(0, 15000) + '\n\n... (truncated)' : result;
|
|
1142
|
-
return { success: true, output, tool, parameters };
|
|
1143
|
-
}
|
|
1144
|
-
case 'github_read': {
|
|
1145
|
-
const mcpConfig = getZaiMcpConfig();
|
|
1146
|
-
if (!mcpConfig) {
|
|
1147
|
-
return { success: false, output: '', error: 'github_read requires a Z.AI API key. Configure one via /provider z.ai', tool, parameters };
|
|
1148
|
-
}
|
|
1149
|
-
const repo = parameters.repo;
|
|
1150
|
-
const action = parameters.action;
|
|
1151
|
-
if (!repo) {
|
|
1152
|
-
return { success: false, output: '', error: 'Missing required parameter: repo', tool, parameters };
|
|
1153
|
-
}
|
|
1154
|
-
if (!repo.includes('/')) {
|
|
1155
|
-
return { success: false, output: '', error: 'Invalid repo format. Use owner/repo (e.g. facebook/react)', tool, parameters };
|
|
1156
|
-
}
|
|
1157
|
-
if (!action || !['search', 'tree', 'read_file'].includes(action)) {
|
|
1158
|
-
return { success: false, output: '', error: 'Invalid action. Must be: search, tree, or read_file', tool, parameters };
|
|
1159
|
-
}
|
|
1160
|
-
let mcpToolName;
|
|
1161
|
-
const args = { repo_name: repo };
|
|
1162
|
-
if (action === 'search') {
|
|
1163
|
-
mcpToolName = 'search_doc';
|
|
1164
|
-
const query = parameters.query;
|
|
1165
|
-
if (!query) {
|
|
1166
|
-
return { success: false, output: '', error: 'Missing required parameter: query (for action=search)', tool, parameters };
|
|
1167
|
-
}
|
|
1168
|
-
args.query = query;
|
|
1169
|
-
}
|
|
1170
|
-
else if (action === 'tree') {
|
|
1171
|
-
mcpToolName = 'get_repo_structure';
|
|
1172
|
-
if (parameters.path)
|
|
1173
|
-
args.dir_path = parameters.path;
|
|
1174
|
-
}
|
|
1175
|
-
else {
|
|
1176
|
-
mcpToolName = 'read_file';
|
|
1177
|
-
const filePath = parameters.path;
|
|
1178
|
-
if (!filePath) {
|
|
1179
|
-
return { success: false, output: '', error: 'Missing required parameter: path (for action=read_file)', tool, parameters };
|
|
1180
|
-
}
|
|
1181
|
-
args.file_path = filePath;
|
|
1182
|
-
}
|
|
1183
|
-
const result = await callZaiMcp(mcpConfig.endpoints.zread, mcpToolName, args, mcpConfig.apiKey);
|
|
1184
|
-
const output = result.length > 15000 ? result.substring(0, 15000) + '\n\n... (truncated)' : result;
|
|
1185
|
-
return { success: true, output, tool, parameters };
|
|
1186
|
-
}
|
|
1187
|
-
// === MiniMax MCP Tools ===
|
|
1188
|
-
case 'minimax_web_search': {
|
|
1189
|
-
const mmConfig = getMinimaxMcpConfig();
|
|
1190
|
-
if (!mmConfig) {
|
|
1191
|
-
return { success: false, output: '', error: 'minimax_web_search requires a MiniMax API key. Configure one via /provider minimax', tool, parameters };
|
|
1192
|
-
}
|
|
1193
|
-
const query = parameters.query;
|
|
1194
|
-
if (!query) {
|
|
1195
|
-
return { success: false, output: '', error: 'Missing required parameter: query', tool, parameters };
|
|
1196
|
-
}
|
|
1197
|
-
const result = await callMinimaxApi(mmConfig.host, '/v1/coding_plan/search', { q: query }, mmConfig.apiKey);
|
|
1198
|
-
const output = result.length > 15000 ? result.substring(0, 15000) + '\n\n... (truncated)' : result;
|
|
1199
|
-
return { success: true, output, tool, parameters };
|
|
1200
|
-
}
|
|
1201
|
-
default:
|
|
1202
|
-
return { success: false, output: '', error: `Unknown tool: ${tool}`, tool, parameters };
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
catch (error) {
|
|
1206
|
-
const err = error;
|
|
1207
|
-
return { success: false, output: '', error: err.message, tool, parameters };
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
/**
|
|
1211
|
-
* List directory contents
|
|
1212
|
-
*/
|
|
1213
|
-
function listDirectory(dir, projectRoot, recursive, prefix = '', ignoreRules) {
|
|
1214
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1215
|
-
const files = [];
|
|
1216
|
-
const rules = ignoreRules || loadIgnoreRules(projectRoot);
|
|
1217
|
-
for (const entry of entries) {
|
|
1218
|
-
const fullPath = join(dir, entry.name);
|
|
1219
|
-
// Skip ignored paths
|
|
1220
|
-
if (isIgnored(fullPath, rules))
|
|
1221
|
-
continue;
|
|
1222
|
-
if (entry.isDirectory()) {
|
|
1223
|
-
files.push(`${prefix}${entry.name}/`);
|
|
1224
|
-
if (recursive) {
|
|
1225
|
-
const subFiles = listDirectory(fullPath, projectRoot, true, prefix + ' ', rules);
|
|
1226
|
-
files.push(...subFiles);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
else {
|
|
1230
|
-
files.push(`${prefix}${entry.name}`);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
return files;
|
|
1234
|
-
}
|
|
1235
|
-
/**
|
|
1236
|
-
* Convert HTML to readable plain text, preserving structure.
|
|
1237
|
-
*/
|
|
1238
|
-
function htmlToText(html) {
|
|
1239
|
-
// Remove invisible elements
|
|
1240
|
-
let text = html
|
|
1241
|
-
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
1242
|
-
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
1243
|
-
.replace(/<noscript[^>]*>[\s\S]*?<\/noscript>/gi, '')
|
|
1244
|
-
.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, '')
|
|
1245
|
-
.replace(/<!--[\s\S]*?-->/g, '');
|
|
1246
|
-
// Try to extract <main>, <article>, or <body> content for less noise
|
|
1247
|
-
const mainMatch = text.match(/<main[^>]*>([\s\S]*?)<\/main>/i);
|
|
1248
|
-
const articleMatch = text.match(/<article[^>]*>([\s\S]*?)<\/article>/i);
|
|
1249
|
-
const bodyMatch = text.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
1250
|
-
text = mainMatch?.[1] || articleMatch?.[1] || bodyMatch?.[1] || text;
|
|
1251
|
-
// Headings → prefix with # markers + newlines
|
|
1252
|
-
text = text.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, '\n\n# $1\n\n');
|
|
1253
|
-
text = text.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, '\n\n## $1\n\n');
|
|
1254
|
-
text = text.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, '\n\n### $1\n\n');
|
|
1255
|
-
text = text.replace(/<h[4-6][^>]*>([\s\S]*?)<\/h[4-6]>/gi, '\n\n#### $1\n\n');
|
|
1256
|
-
// Links → [text](href)
|
|
1257
|
-
text = text.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)');
|
|
1258
|
-
// Code blocks
|
|
1259
|
-
text = text.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, '\n```\n$1\n```\n');
|
|
1260
|
-
text = text.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, '\n```\n$1\n```\n');
|
|
1261
|
-
text = text.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, '`$1`');
|
|
1262
|
-
// Lists
|
|
1263
|
-
text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, '\n- $1');
|
|
1264
|
-
text = text.replace(/<\/[uo]l>/gi, '\n');
|
|
1265
|
-
text = text.replace(/<[uo]l[^>]*>/gi, '\n');
|
|
1266
|
-
// Block elements → newlines
|
|
1267
|
-
text = text.replace(/<\/p>/gi, '\n\n');
|
|
1268
|
-
text = text.replace(/<br\s*\/?>/gi, '\n');
|
|
1269
|
-
text = text.replace(/<\/div>/gi, '\n');
|
|
1270
|
-
text = text.replace(/<\/tr>/gi, '\n');
|
|
1271
|
-
text = text.replace(/<\/th>/gi, '\t');
|
|
1272
|
-
text = text.replace(/<\/td>/gi, '\t');
|
|
1273
|
-
text = text.replace(/<hr[^>]*>/gi, '\n---\n');
|
|
1274
|
-
text = text.replace(/<\/blockquote>/gi, '\n');
|
|
1275
|
-
text = text.replace(/<blockquote[^>]*>/gi, '\n> ');
|
|
1276
|
-
// Bold/italic
|
|
1277
|
-
text = text.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, '**$2**');
|
|
1278
|
-
text = text.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, '*$2*');
|
|
1279
|
-
// Strip remaining tags
|
|
1280
|
-
text = text.replace(/<[^>]+>/g, '');
|
|
1281
|
-
// Decode common HTML entities
|
|
1282
|
-
text = text
|
|
1283
|
-
.replace(/&/g, '&')
|
|
1284
|
-
.replace(/</g, '<')
|
|
1285
|
-
.replace(/>/g, '>')
|
|
1286
|
-
.replace(/"/g, '"')
|
|
1287
|
-
.replace(/'/g, "'")
|
|
1288
|
-
.replace(/ /g, ' ')
|
|
1289
|
-
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code)))
|
|
1290
|
-
.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCharCode(parseInt(code, 16)));
|
|
1291
|
-
// Clean up whitespace
|
|
1292
|
-
text = text
|
|
1293
|
-
.replace(/[ \t]+/g, ' ') // Collapse horizontal whitespace
|
|
1294
|
-
.replace(/ *\n/g, '\n') // Trim trailing spaces on lines
|
|
1295
|
-
.replace(/\n{3,}/g, '\n\n') // Max 2 consecutive newlines
|
|
1296
|
-
.trim();
|
|
1297
|
-
return text;
|
|
1298
|
-
}
|
|
1299
|
-
/**
|
|
1300
|
-
* Create action log from tool result
|
|
1301
|
-
*/
|
|
1302
|
-
export function createActionLog(toolCall, result) {
|
|
1303
|
-
// Normalize tool name to handle case variations
|
|
1304
|
-
const normalizedTool = normalizeToolName(toolCall.tool);
|
|
1305
|
-
const typeMap = {
|
|
1306
|
-
read_file: 'read',
|
|
1307
|
-
write_file: 'write',
|
|
1308
|
-
edit_file: 'edit',
|
|
1309
|
-
delete_file: 'delete',
|
|
1310
|
-
execute_command: 'command',
|
|
1311
|
-
search_code: 'search',
|
|
1312
|
-
list_files: 'list',
|
|
1313
|
-
create_directory: 'mkdir',
|
|
1314
|
-
find_files: 'search',
|
|
1315
|
-
fetch_url: 'fetch',
|
|
1316
|
-
web_search: 'fetch',
|
|
1317
|
-
web_read: 'fetch',
|
|
1318
|
-
github_read: 'fetch',
|
|
1319
|
-
minimax_web_search: 'fetch',
|
|
1320
|
-
};
|
|
1321
|
-
const target = toolCall.parameters.path ||
|
|
1322
|
-
toolCall.parameters.command ||
|
|
1323
|
-
toolCall.parameters.pattern ||
|
|
1324
|
-
toolCall.parameters.url ||
|
|
1325
|
-
toolCall.parameters.query ||
|
|
1326
|
-
toolCall.parameters.repo ||
|
|
1327
|
-
'unknown';
|
|
1328
|
-
// For write/edit actions, include FULL content in details for live code view
|
|
1329
|
-
let details;
|
|
1330
|
-
if (result.success) {
|
|
1331
|
-
if (normalizedTool === 'write_file' && toolCall.parameters.content) {
|
|
1332
|
-
// Show FULL content being written - for live code tracking
|
|
1333
|
-
details = toolCall.parameters.content;
|
|
1334
|
-
}
|
|
1335
|
-
else if (normalizedTool === 'edit_file' && toolCall.parameters.new_text) {
|
|
1336
|
-
// Show FULL new text being inserted - for live code tracking
|
|
1337
|
-
details = toolCall.parameters.new_text;
|
|
1338
|
-
}
|
|
1339
|
-
else if (normalizedTool === 'execute_command') {
|
|
1340
|
-
// Show command output (limited)
|
|
1341
|
-
details = result.output.slice(0, 1000);
|
|
1342
|
-
}
|
|
1343
|
-
else {
|
|
1344
|
-
details = result.output.slice(0, 500);
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
else {
|
|
1348
|
-
details = result.error;
|
|
1349
|
-
}
|
|
1350
|
-
return {
|
|
1351
|
-
type: typeMap[normalizedTool] || 'command',
|
|
1352
|
-
target,
|
|
1353
|
-
result: result.success ? 'success' : 'error',
|
|
1354
|
-
details,
|
|
1355
|
-
timestamp: Date.now(),
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
216
|
+
// Re-export from sub-modules so existing imports don't break
|
|
217
|
+
export { normalizeToolName, parseOpenAIToolCalls, parseAnthropicToolCalls, parseToolCalls } from './toolParsing.js';
|
|
218
|
+
export { executeTool, validatePath, createActionLog } from './toolExecution.js';
|