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.
Files changed (63) 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/Input.js +8 -1
  7. package/dist/renderer/agentExecution.d.ts +36 -0
  8. package/dist/renderer/agentExecution.js +394 -0
  9. package/dist/renderer/commands.d.ts +16 -0
  10. package/dist/renderer/commands.js +838 -0
  11. package/dist/renderer/handlers.d.ts +87 -0
  12. package/dist/renderer/handlers.js +260 -0
  13. package/dist/renderer/highlight.d.ts +18 -0
  14. package/dist/renderer/highlight.js +130 -0
  15. package/dist/renderer/main.d.ts +4 -2
  16. package/dist/renderer/main.js +103 -1550
  17. package/dist/utils/agent.d.ts +5 -15
  18. package/dist/utils/agent.js +9 -693
  19. package/dist/utils/agentChat.d.ts +46 -0
  20. package/dist/utils/agentChat.js +343 -0
  21. package/dist/utils/agentStream.d.ts +23 -0
  22. package/dist/utils/agentStream.js +216 -0
  23. package/dist/utils/keychain.js +3 -2
  24. package/dist/utils/learning.js +9 -3
  25. package/dist/utils/mcpIntegration.d.ts +61 -0
  26. package/dist/utils/mcpIntegration.js +154 -0
  27. package/dist/utils/project.js +8 -3
  28. package/dist/utils/skills.js +21 -11
  29. package/dist/utils/smartContext.d.ts +4 -0
  30. package/dist/utils/smartContext.js +51 -14
  31. package/dist/utils/toolExecution.d.ts +27 -0
  32. package/dist/utils/toolExecution.js +525 -0
  33. package/dist/utils/toolParsing.d.ts +18 -0
  34. package/dist/utils/toolParsing.js +302 -0
  35. package/dist/utils/tools.d.ts +27 -24
  36. package/dist/utils/tools.js +30 -1169
  37. package/package.json +3 -1
  38. package/dist/config/config.test.d.ts +0 -1
  39. package/dist/config/config.test.js +0 -157
  40. package/dist/config/providers.test.d.ts +0 -1
  41. package/dist/config/providers.test.js +0 -187
  42. package/dist/hooks/index.d.ts +0 -4
  43. package/dist/hooks/index.js +0 -4
  44. package/dist/hooks/useAgent.d.ts +0 -29
  45. package/dist/hooks/useAgent.js +0 -148
  46. package/dist/utils/agent.test.d.ts +0 -1
  47. package/dist/utils/agent.test.js +0 -315
  48. package/dist/utils/git.test.d.ts +0 -1
  49. package/dist/utils/git.test.js +0 -193
  50. package/dist/utils/gitignore.test.d.ts +0 -1
  51. package/dist/utils/gitignore.test.js +0 -167
  52. package/dist/utils/project.test.d.ts +0 -1
  53. package/dist/utils/project.test.js +0 -212
  54. package/dist/utils/ratelimit.test.d.ts +0 -1
  55. package/dist/utils/ratelimit.test.js +0 -131
  56. package/dist/utils/retry.test.d.ts +0 -1
  57. package/dist/utils/retry.test.js +0 -163
  58. package/dist/utils/smartContext.test.d.ts +0 -1
  59. package/dist/utils/smartContext.test.js +0 -382
  60. package/dist/utils/tools.test.d.ts +0 -1
  61. package/dist/utils/tools.test.js +0 -676
  62. package/dist/utils/validation.test.d.ts +0 -1
  63. 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'];
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
- * Parse tool calls from OpenAI response
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(/&amp;/g, '&')
1284
- .replace(/&lt;/g, '<')
1285
- .replace(/&gt;/g, '>')
1286
- .replace(/&quot;/g, '"')
1287
- .replace(/&#39;/g, "'")
1288
- .replace(/&nbsp;/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';