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