codeep 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +576 -0
- package/dist/api/index.d.ts +8 -0
- package/dist/api/index.js +421 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.js +1406 -0
- package/dist/components/AgentProgress.d.ts +33 -0
- package/dist/components/AgentProgress.js +97 -0
- package/dist/components/Export.d.ts +8 -0
- package/dist/components/Export.js +27 -0
- package/dist/components/Help.d.ts +2 -0
- package/dist/components/Help.js +3 -0
- package/dist/components/Input.d.ts +9 -0
- package/dist/components/Input.js +89 -0
- package/dist/components/Loading.d.ts +9 -0
- package/dist/components/Loading.js +31 -0
- package/dist/components/Login.d.ts +7 -0
- package/dist/components/Login.js +77 -0
- package/dist/components/Logo.d.ts +8 -0
- package/dist/components/Logo.js +89 -0
- package/dist/components/LogoutPicker.d.ts +8 -0
- package/dist/components/LogoutPicker.js +61 -0
- package/dist/components/Message.d.ts +10 -0
- package/dist/components/Message.js +234 -0
- package/dist/components/MessageList.d.ts +10 -0
- package/dist/components/MessageList.js +8 -0
- package/dist/components/ProjectPermission.d.ts +7 -0
- package/dist/components/ProjectPermission.js +52 -0
- package/dist/components/Search.d.ts +10 -0
- package/dist/components/Search.js +30 -0
- package/dist/components/SessionPicker.d.ts +9 -0
- package/dist/components/SessionPicker.js +88 -0
- package/dist/components/Sessions.d.ts +12 -0
- package/dist/components/Sessions.js +102 -0
- package/dist/components/Settings.d.ts +7 -0
- package/dist/components/Settings.js +162 -0
- package/dist/components/Status.d.ts +2 -0
- package/dist/components/Status.js +12 -0
- package/dist/config/config.test.d.ts +1 -0
- package/dist/config/config.test.js +157 -0
- package/dist/config/index.d.ts +121 -0
- package/dist/config/index.js +555 -0
- package/dist/config/providers.d.ts +43 -0
- package/dist/config/providers.js +82 -0
- package/dist/config/providers.test.d.ts +1 -0
- package/dist/config/providers.test.js +132 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +38 -0
- package/dist/utils/agent.d.ts +37 -0
- package/dist/utils/agent.js +627 -0
- package/dist/utils/codeReview.d.ts +36 -0
- package/dist/utils/codeReview.js +390 -0
- package/dist/utils/context.d.ts +49 -0
- package/dist/utils/context.js +216 -0
- package/dist/utils/diffPreview.d.ts +57 -0
- package/dist/utils/diffPreview.js +335 -0
- package/dist/utils/export.d.ts +19 -0
- package/dist/utils/export.js +94 -0
- package/dist/utils/git.d.ts +85 -0
- package/dist/utils/git.js +399 -0
- package/dist/utils/git.test.d.ts +1 -0
- package/dist/utils/git.test.js +193 -0
- package/dist/utils/history.d.ts +93 -0
- package/dist/utils/history.js +348 -0
- package/dist/utils/interactive.d.ts +34 -0
- package/dist/utils/interactive.js +206 -0
- package/dist/utils/keychain.d.ts +17 -0
- package/dist/utils/keychain.js +160 -0
- package/dist/utils/learning.d.ts +89 -0
- package/dist/utils/learning.js +330 -0
- package/dist/utils/logger.d.ts +33 -0
- package/dist/utils/logger.js +130 -0
- package/dist/utils/project.d.ts +86 -0
- package/dist/utils/project.js +415 -0
- package/dist/utils/project.test.d.ts +1 -0
- package/dist/utils/project.test.js +212 -0
- package/dist/utils/ratelimit.d.ts +26 -0
- package/dist/utils/ratelimit.js +132 -0
- package/dist/utils/ratelimit.test.d.ts +1 -0
- package/dist/utils/ratelimit.test.js +131 -0
- package/dist/utils/retry.d.ts +28 -0
- package/dist/utils/retry.js +109 -0
- package/dist/utils/retry.test.d.ts +1 -0
- package/dist/utils/retry.test.js +163 -0
- package/dist/utils/search.d.ts +11 -0
- package/dist/utils/search.js +29 -0
- package/dist/utils/shell.d.ts +45 -0
- package/dist/utils/shell.js +242 -0
- package/dist/utils/skills.d.ts +144 -0
- package/dist/utils/skills.js +1137 -0
- package/dist/utils/smartContext.d.ts +29 -0
- package/dist/utils/smartContext.js +441 -0
- package/dist/utils/tools.d.ts +224 -0
- package/dist/utils/tools.js +731 -0
- package/dist/utils/update.d.ts +22 -0
- package/dist/utils/update.js +128 -0
- package/dist/utils/validation.d.ts +28 -0
- package/dist/utils/validation.js +141 -0
- package/dist/utils/validation.test.d.ts +1 -0
- package/dist/utils/validation.test.js +164 -0
- package/dist/utils/verify.d.ts +78 -0
- package/dist/utils/verify.js +464 -0
- package/package.json +68 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tools - definitions and execution
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, rmSync } from 'fs';
|
|
5
|
+
import { join, dirname, relative, resolve, isAbsolute } from 'path';
|
|
6
|
+
import { executeCommand } from './shell';
|
|
7
|
+
import { recordWrite, recordEdit, recordDelete, recordMkdir, recordCommand } from './history';
|
|
8
|
+
// Tool definitions for system prompt
|
|
9
|
+
export const AGENT_TOOLS = {
|
|
10
|
+
read_file: {
|
|
11
|
+
name: 'read_file',
|
|
12
|
+
description: 'Read the contents of a file. Use this to examine existing code.',
|
|
13
|
+
parameters: {
|
|
14
|
+
path: { type: 'string', description: 'Path to the file relative to project root', required: true },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
write_file: {
|
|
18
|
+
name: 'write_file',
|
|
19
|
+
description: 'Create a new file or completely overwrite an existing file with new content.',
|
|
20
|
+
parameters: {
|
|
21
|
+
path: { type: 'string', description: 'Path to the file relative to project root', required: true },
|
|
22
|
+
content: { type: 'string', description: 'The complete content to write to the file', required: true },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
edit_file: {
|
|
26
|
+
name: 'edit_file',
|
|
27
|
+
description: 'Edit an existing file by replacing specific text. Use for targeted changes.',
|
|
28
|
+
parameters: {
|
|
29
|
+
path: { type: 'string', description: 'Path to the file relative to project root', required: true },
|
|
30
|
+
old_text: { type: 'string', description: 'The exact text to find and replace', required: true },
|
|
31
|
+
new_text: { type: 'string', description: 'The new text to replace with', required: true },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
delete_file: {
|
|
35
|
+
name: 'delete_file',
|
|
36
|
+
description: 'Delete a file or directory from the project. For directories, deletes recursively.',
|
|
37
|
+
parameters: {
|
|
38
|
+
path: { type: 'string', description: 'Path to the file or directory relative to project root', required: true },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
list_files: {
|
|
42
|
+
name: 'list_files',
|
|
43
|
+
description: 'List files and directories in a path. Use to explore project structure.',
|
|
44
|
+
parameters: {
|
|
45
|
+
path: { type: 'string', description: 'Path to directory relative to project root (use "." for root)', required: true },
|
|
46
|
+
recursive: { type: 'boolean', description: 'Whether to list recursively (default: false)', required: false },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
create_directory: {
|
|
50
|
+
name: 'create_directory',
|
|
51
|
+
description: 'Create a new directory (folder). Creates parent directories if needed.',
|
|
52
|
+
parameters: {
|
|
53
|
+
path: { type: 'string', description: 'Path to the directory to create, relative to project root', required: true },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
execute_command: {
|
|
57
|
+
name: 'execute_command',
|
|
58
|
+
description: 'Execute a shell command. Use for npm, git, build tools, tests, etc.',
|
|
59
|
+
parameters: {
|
|
60
|
+
command: { type: 'string', description: 'The command to run (e.g., npm, git, node)', required: true },
|
|
61
|
+
args: { type: 'array', description: 'Command arguments as array (e.g., ["install", "lodash"])', required: false },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
search_code: {
|
|
65
|
+
name: 'search_code',
|
|
66
|
+
description: 'Search for a text pattern in the codebase. Returns matching files and lines.',
|
|
67
|
+
parameters: {
|
|
68
|
+
pattern: { type: 'string', description: 'Text or regex pattern to search for', required: true },
|
|
69
|
+
path: { type: 'string', description: 'Path to search in (default: entire project)', required: false },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
fetch_url: {
|
|
73
|
+
name: 'fetch_url',
|
|
74
|
+
description: 'Fetch content from a URL (documentation, APIs, web pages). Returns text content.',
|
|
75
|
+
parameters: {
|
|
76
|
+
url: { type: 'string', description: 'The URL to fetch content from', required: true },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Format tool definitions for system prompt (text-based fallback)
|
|
82
|
+
*/
|
|
83
|
+
export function formatToolDefinitions() {
|
|
84
|
+
const lines = [];
|
|
85
|
+
for (const [name, tool] of Object.entries(AGENT_TOOLS)) {
|
|
86
|
+
lines.push(`### ${name}`);
|
|
87
|
+
lines.push(tool.description);
|
|
88
|
+
lines.push('Parameters:');
|
|
89
|
+
for (const [param, info] of Object.entries(tool.parameters)) {
|
|
90
|
+
const required = info.required ? '(required)' : '(optional)';
|
|
91
|
+
lines.push(` - ${param}: ${info.type} ${required} - ${info.description}`);
|
|
92
|
+
}
|
|
93
|
+
lines.push('');
|
|
94
|
+
}
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get tools in OpenAI Function Calling format
|
|
99
|
+
*/
|
|
100
|
+
export function getOpenAITools() {
|
|
101
|
+
return Object.entries(AGENT_TOOLS).map(([name, tool]) => {
|
|
102
|
+
const properties = {};
|
|
103
|
+
const required = [];
|
|
104
|
+
for (const [param, info] of Object.entries(tool.parameters)) {
|
|
105
|
+
const paramInfo = info;
|
|
106
|
+
if (paramInfo.type === 'array') {
|
|
107
|
+
properties[param] = {
|
|
108
|
+
type: 'array',
|
|
109
|
+
description: paramInfo.description,
|
|
110
|
+
items: { type: 'string' },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
properties[param] = {
|
|
115
|
+
type: paramInfo.type,
|
|
116
|
+
description: paramInfo.description,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (paramInfo.required) {
|
|
120
|
+
required.push(param);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
type: 'function',
|
|
125
|
+
function: {
|
|
126
|
+
name,
|
|
127
|
+
description: tool.description,
|
|
128
|
+
parameters: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties,
|
|
131
|
+
required,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get tools in Anthropic Tool Use format
|
|
139
|
+
*/
|
|
140
|
+
export function getAnthropicTools() {
|
|
141
|
+
return Object.entries(AGENT_TOOLS).map(([name, tool]) => {
|
|
142
|
+
const properties = {};
|
|
143
|
+
const required = [];
|
|
144
|
+
for (const [param, info] of Object.entries(tool.parameters)) {
|
|
145
|
+
const paramInfo = info;
|
|
146
|
+
if (paramInfo.type === 'array') {
|
|
147
|
+
properties[param] = {
|
|
148
|
+
type: 'array',
|
|
149
|
+
description: paramInfo.description,
|
|
150
|
+
items: { type: 'string' },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
properties[param] = {
|
|
155
|
+
type: paramInfo.type,
|
|
156
|
+
description: paramInfo.description,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (paramInfo.required) {
|
|
160
|
+
required.push(param);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
name,
|
|
165
|
+
description: tool.description,
|
|
166
|
+
input_schema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties,
|
|
169
|
+
required,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Parse tool calls from OpenAI response
|
|
176
|
+
*/
|
|
177
|
+
export function parseOpenAIToolCalls(toolCalls) {
|
|
178
|
+
if (!toolCalls || !Array.isArray(toolCalls))
|
|
179
|
+
return [];
|
|
180
|
+
return toolCalls.map(tc => {
|
|
181
|
+
let parameters = {};
|
|
182
|
+
try {
|
|
183
|
+
parameters = JSON.parse(tc.function?.arguments || '{}');
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
parameters = {};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
tool: tc.function?.name || '',
|
|
190
|
+
parameters,
|
|
191
|
+
id: tc.id,
|
|
192
|
+
};
|
|
193
|
+
}).filter(tc => tc.tool);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Parse tool calls from Anthropic response
|
|
197
|
+
*/
|
|
198
|
+
export function parseAnthropicToolCalls(content) {
|
|
199
|
+
if (!content || !Array.isArray(content))
|
|
200
|
+
return [];
|
|
201
|
+
return content
|
|
202
|
+
.filter(block => block.type === 'tool_use')
|
|
203
|
+
.map(block => ({
|
|
204
|
+
tool: block.name || '',
|
|
205
|
+
parameters: block.input || {},
|
|
206
|
+
id: block.id,
|
|
207
|
+
}))
|
|
208
|
+
.filter(tc => tc.tool);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Parse tool calls from LLM response
|
|
212
|
+
* Supports multiple formats:
|
|
213
|
+
* - <tool_call>{"tool": "name", "parameters": {...}}</tool_call>
|
|
214
|
+
* - <toolcall>{"tool": "name", "parameters": {...}}</toolcall>
|
|
215
|
+
* - <toolcall>toolname{"parameters": {...}}</toolcall>
|
|
216
|
+
* - ```tool\n{"tool": "name", "parameters": {...}}\n```
|
|
217
|
+
* - Direct JSON blocks with tool property
|
|
218
|
+
*/
|
|
219
|
+
export function parseToolCalls(response) {
|
|
220
|
+
const toolCalls = [];
|
|
221
|
+
// Format 1: <tool_call>...</tool_call> or <toolcall>...</toolcall> with JSON inside
|
|
222
|
+
const toolCallRegex = /<tool_?call>\s*([\s\S]*?)\s*<\/tool_?call>/gi;
|
|
223
|
+
let match;
|
|
224
|
+
while ((match = toolCallRegex.exec(response)) !== null) {
|
|
225
|
+
const parsed = tryParseToolCall(match[1].trim());
|
|
226
|
+
if (parsed)
|
|
227
|
+
toolCalls.push(parsed);
|
|
228
|
+
}
|
|
229
|
+
// Format 2: <toolcall>toolname{...} or <toolcall>toolname, "parameters": {...}
|
|
230
|
+
const malformedRegex = /<toolcall>(\w+)[\s,]*(?:"parameters"\s*:\s*)?(\{[\s\S]*?\})/gi;
|
|
231
|
+
while ((match = malformedRegex.exec(response)) !== null) {
|
|
232
|
+
const toolName = match[1].toLowerCase();
|
|
233
|
+
let jsonPart = match[2];
|
|
234
|
+
// Map common variations to actual tool names
|
|
235
|
+
const toolNameMap = {
|
|
236
|
+
'executecommand': 'execute_command',
|
|
237
|
+
'execute_command': 'execute_command',
|
|
238
|
+
'readfile': 'read_file',
|
|
239
|
+
'read_file': 'read_file',
|
|
240
|
+
'writefile': 'write_file',
|
|
241
|
+
'write_file': 'write_file',
|
|
242
|
+
'editfile': 'edit_file',
|
|
243
|
+
'edit_file': 'edit_file',
|
|
244
|
+
'deletefile': 'delete_file',
|
|
245
|
+
'delete_file': 'delete_file',
|
|
246
|
+
'listfiles': 'list_files',
|
|
247
|
+
'list_files': 'list_files',
|
|
248
|
+
'searchcode': 'search_code',
|
|
249
|
+
'search_code': 'search_code',
|
|
250
|
+
};
|
|
251
|
+
const actualToolName = toolNameMap[toolName] || toolName;
|
|
252
|
+
try {
|
|
253
|
+
const parsed = JSON.parse(jsonPart);
|
|
254
|
+
const params = parsed.parameters || parsed;
|
|
255
|
+
toolCalls.push({
|
|
256
|
+
tool: actualToolName,
|
|
257
|
+
parameters: params,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// Try to extract parameters manually
|
|
262
|
+
const params = tryExtractParams(jsonPart);
|
|
263
|
+
if (params) {
|
|
264
|
+
toolCalls.push({
|
|
265
|
+
tool: actualToolName,
|
|
266
|
+
parameters: params,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Format 2b: Even more malformed - toolname followed by loose JSON-like content
|
|
272
|
+
const looseRegex = /<toolcall>(\w+)[,\s]+["']?parameters["']?\s*:\s*(\{[\s\S]*?\})(?:<\/toolcall>|<|$)/gi;
|
|
273
|
+
while ((match = looseRegex.exec(response)) !== null) {
|
|
274
|
+
// Skip if already parsed
|
|
275
|
+
const toolName = match[1].toLowerCase();
|
|
276
|
+
const toolNameMap = {
|
|
277
|
+
'executecommand': 'execute_command',
|
|
278
|
+
'readfile': 'read_file',
|
|
279
|
+
'writefile': 'write_file',
|
|
280
|
+
'editfile': 'edit_file',
|
|
281
|
+
'deletefile': 'delete_file',
|
|
282
|
+
'listfiles': 'list_files',
|
|
283
|
+
'searchcode': 'search_code',
|
|
284
|
+
};
|
|
285
|
+
const actualToolName = toolNameMap[toolName] || toolName;
|
|
286
|
+
// Check if we already have this tool call
|
|
287
|
+
if (toolCalls.some(t => t.tool === actualToolName))
|
|
288
|
+
continue;
|
|
289
|
+
const params = tryExtractParams(match[2]);
|
|
290
|
+
if (params) {
|
|
291
|
+
toolCalls.push({
|
|
292
|
+
tool: actualToolName,
|
|
293
|
+
parameters: params,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Format 3: ```tool or ```json with tool calls
|
|
298
|
+
const codeBlockRegex = /```(?:tool|json)?\s*\n?([\s\S]*?)\n?```/g;
|
|
299
|
+
while ((match = codeBlockRegex.exec(response)) !== null) {
|
|
300
|
+
const content = match[1].trim();
|
|
301
|
+
// Only parse if it looks like a tool call JSON
|
|
302
|
+
if (content.includes('"tool"') || content.includes('"parameters"')) {
|
|
303
|
+
const parsed = tryParseToolCall(content);
|
|
304
|
+
if (parsed && !toolCalls.some(t => t.tool === parsed.tool && JSON.stringify(t.parameters) === JSON.stringify(parsed.parameters))) {
|
|
305
|
+
toolCalls.push(parsed);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Format 4: Inline JSON objects with tool property (fallback)
|
|
310
|
+
if (toolCalls.length === 0) {
|
|
311
|
+
const jsonRegex = /\{[^{}]*"tool"\s*:\s*"[^"]+"\s*,\s*"parameters"\s*:\s*\{[^{}]*\}[^{}]*\}/g;
|
|
312
|
+
while ((match = jsonRegex.exec(response)) !== null) {
|
|
313
|
+
const parsed = tryParseToolCall(match[0]);
|
|
314
|
+
if (parsed)
|
|
315
|
+
toolCalls.push(parsed);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return toolCalls;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Try to extract parameters from a malformed JSON string
|
|
322
|
+
*/
|
|
323
|
+
function tryExtractParams(str) {
|
|
324
|
+
const params = {};
|
|
325
|
+
// Extract "args": [...]
|
|
326
|
+
const argsMatch = str.match(/"args"\s*:\s*\[([\s\S]*?)\]/);
|
|
327
|
+
if (argsMatch) {
|
|
328
|
+
try {
|
|
329
|
+
params.args = JSON.parse(`[${argsMatch[1]}]`);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// Try to extract string array manually
|
|
333
|
+
const items = argsMatch[1].match(/"([^"]*)"/g);
|
|
334
|
+
if (items) {
|
|
335
|
+
params.args = items.map(i => i.replace(/"/g, ''));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Extract "command": "..."
|
|
340
|
+
const cmdMatch = str.match(/"command"\s*:\s*"([^"]*)"/);
|
|
341
|
+
if (cmdMatch) {
|
|
342
|
+
params.command = cmdMatch[1];
|
|
343
|
+
}
|
|
344
|
+
// Extract "path": "..."
|
|
345
|
+
const pathMatch = str.match(/"path"\s*:\s*"([^"]*)"/);
|
|
346
|
+
if (pathMatch) {
|
|
347
|
+
params.path = pathMatch[1];
|
|
348
|
+
}
|
|
349
|
+
// Extract "content": "..."
|
|
350
|
+
const contentMatch = str.match(/"content"\s*:\s*"([\s\S]*?)(?<!\\)"/);
|
|
351
|
+
if (contentMatch) {
|
|
352
|
+
params.content = contentMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
|
|
353
|
+
}
|
|
354
|
+
// Extract "old_text" and "new_text"
|
|
355
|
+
const oldTextMatch = str.match(/"old_text"\s*:\s*"([\s\S]*?)(?<!\\)"/);
|
|
356
|
+
if (oldTextMatch) {
|
|
357
|
+
params.old_text = oldTextMatch[1];
|
|
358
|
+
}
|
|
359
|
+
const newTextMatch = str.match(/"new_text"\s*:\s*"([\s\S]*?)(?<!\\)"/);
|
|
360
|
+
if (newTextMatch) {
|
|
361
|
+
params.new_text = newTextMatch[1];
|
|
362
|
+
}
|
|
363
|
+
// Extract "pattern": "..."
|
|
364
|
+
const patternMatch = str.match(/"pattern"\s*:\s*"([^"]*)"/);
|
|
365
|
+
if (patternMatch) {
|
|
366
|
+
params.pattern = patternMatch[1];
|
|
367
|
+
}
|
|
368
|
+
// Extract "recursive": true/false
|
|
369
|
+
const recursiveMatch = str.match(/"recursive"\s*:\s*(true|false)/i);
|
|
370
|
+
if (recursiveMatch) {
|
|
371
|
+
params.recursive = recursiveMatch[1].toLowerCase() === 'true';
|
|
372
|
+
}
|
|
373
|
+
return Object.keys(params).length > 0 ? params : null;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Try to parse a string as a tool call
|
|
377
|
+
*/
|
|
378
|
+
function tryParseToolCall(str) {
|
|
379
|
+
try {
|
|
380
|
+
// Clean up common issues
|
|
381
|
+
let cleaned = str
|
|
382
|
+
.replace(/[\r\n]+/g, ' ') // Remove newlines
|
|
383
|
+
.replace(/,\s*}/g, '}') // Remove trailing commas
|
|
384
|
+
.replace(/,\s*]/g, ']') // Remove trailing commas in arrays
|
|
385
|
+
.trim();
|
|
386
|
+
const parsed = JSON.parse(cleaned);
|
|
387
|
+
if (parsed.tool && typeof parsed.tool === 'string') {
|
|
388
|
+
return {
|
|
389
|
+
tool: parsed.tool,
|
|
390
|
+
parameters: parsed.parameters || {},
|
|
391
|
+
id: parsed.id,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// Try to extract tool name and parameters manually for malformed JSON
|
|
397
|
+
const toolMatch = str.match(/"tool"\s*:\s*"([^"]+)"/);
|
|
398
|
+
if (toolMatch) {
|
|
399
|
+
const tool = toolMatch[1];
|
|
400
|
+
const params = {};
|
|
401
|
+
// Extract simple string parameters
|
|
402
|
+
const paramMatches = str.matchAll(/"(\w+)"\s*:\s*"([^"]*)"/g);
|
|
403
|
+
for (const m of paramMatches) {
|
|
404
|
+
if (m[1] !== 'tool') {
|
|
405
|
+
params[m[1]] = m[2];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// Extract boolean parameters
|
|
409
|
+
const boolMatches = str.matchAll(/"(\w+)"\s*:\s*(true|false)/gi);
|
|
410
|
+
for (const m of boolMatches) {
|
|
411
|
+
params[m[1]] = m[2].toLowerCase() === 'true';
|
|
412
|
+
}
|
|
413
|
+
if (Object.keys(params).length > 0 || AGENT_TOOLS[tool]) {
|
|
414
|
+
return { tool, parameters: params };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Validate path is within project
|
|
422
|
+
*/
|
|
423
|
+
function validatePath(path, projectRoot) {
|
|
424
|
+
const absolutePath = isAbsolute(path) ? path : resolve(projectRoot, path);
|
|
425
|
+
const relativePath = relative(projectRoot, absolutePath);
|
|
426
|
+
if (relativePath.startsWith('..')) {
|
|
427
|
+
return { valid: false, absolutePath, error: `Path '${path}' is outside project directory` };
|
|
428
|
+
}
|
|
429
|
+
return { valid: true, absolutePath };
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Execute a tool call
|
|
433
|
+
*/
|
|
434
|
+
export function executeTool(toolCall, projectRoot) {
|
|
435
|
+
const { tool, parameters } = toolCall;
|
|
436
|
+
try {
|
|
437
|
+
switch (tool) {
|
|
438
|
+
case 'read_file': {
|
|
439
|
+
const path = parameters.path;
|
|
440
|
+
if (!path) {
|
|
441
|
+
return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
|
|
442
|
+
}
|
|
443
|
+
const validation = validatePath(path, projectRoot);
|
|
444
|
+
if (!validation.valid) {
|
|
445
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
446
|
+
}
|
|
447
|
+
if (!existsSync(validation.absolutePath)) {
|
|
448
|
+
return { success: false, output: '', error: `File not found: ${path}`, tool, parameters };
|
|
449
|
+
}
|
|
450
|
+
const stat = statSync(validation.absolutePath);
|
|
451
|
+
if (stat.isDirectory()) {
|
|
452
|
+
return { success: false, output: '', error: `Path is a directory, not a file: ${path}`, tool, parameters };
|
|
453
|
+
}
|
|
454
|
+
// Limit file size
|
|
455
|
+
if (stat.size > 100 * 1024) { // 100KB
|
|
456
|
+
return { success: false, output: '', error: `File too large (${stat.size} bytes). Max: 100KB`, tool, parameters };
|
|
457
|
+
}
|
|
458
|
+
const content = readFileSync(validation.absolutePath, 'utf-8');
|
|
459
|
+
return { success: true, output: content, tool, parameters };
|
|
460
|
+
}
|
|
461
|
+
case 'write_file': {
|
|
462
|
+
const path = parameters.path;
|
|
463
|
+
const content = parameters.content;
|
|
464
|
+
if (!path) {
|
|
465
|
+
return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
|
|
466
|
+
}
|
|
467
|
+
if (content === undefined) {
|
|
468
|
+
return { success: false, output: '', error: 'Missing required parameter: content', tool, parameters };
|
|
469
|
+
}
|
|
470
|
+
const validation = validatePath(path, projectRoot);
|
|
471
|
+
if (!validation.valid) {
|
|
472
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
473
|
+
}
|
|
474
|
+
// Create directory if needed
|
|
475
|
+
const dir = dirname(validation.absolutePath);
|
|
476
|
+
if (!existsSync(dir)) {
|
|
477
|
+
mkdirSync(dir, { recursive: true });
|
|
478
|
+
}
|
|
479
|
+
// Record for undo
|
|
480
|
+
recordWrite(validation.absolutePath);
|
|
481
|
+
const existed = existsSync(validation.absolutePath);
|
|
482
|
+
writeFileSync(validation.absolutePath, content, 'utf-8');
|
|
483
|
+
const action = existed ? 'Updated' : 'Created';
|
|
484
|
+
return { success: true, output: `${action} file: ${path}`, tool, parameters };
|
|
485
|
+
}
|
|
486
|
+
case 'edit_file': {
|
|
487
|
+
const path = parameters.path;
|
|
488
|
+
const oldText = parameters.old_text;
|
|
489
|
+
const newText = parameters.new_text;
|
|
490
|
+
if (!path || oldText === undefined || newText === undefined) {
|
|
491
|
+
return { success: false, output: '', error: 'Missing required parameters', tool, parameters };
|
|
492
|
+
}
|
|
493
|
+
const validation = validatePath(path, projectRoot);
|
|
494
|
+
if (!validation.valid) {
|
|
495
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
496
|
+
}
|
|
497
|
+
if (!existsSync(validation.absolutePath)) {
|
|
498
|
+
return { success: false, output: '', error: `File not found: ${path}`, tool, parameters };
|
|
499
|
+
}
|
|
500
|
+
const content = readFileSync(validation.absolutePath, 'utf-8');
|
|
501
|
+
if (!content.includes(oldText)) {
|
|
502
|
+
return { success: false, output: '', error: `Text not found in file. Make sure old_text matches exactly.`, tool, parameters };
|
|
503
|
+
}
|
|
504
|
+
// Record for undo
|
|
505
|
+
recordEdit(validation.absolutePath);
|
|
506
|
+
const newContent = content.replace(oldText, newText);
|
|
507
|
+
writeFileSync(validation.absolutePath, newContent, 'utf-8');
|
|
508
|
+
return { success: true, output: `Edited file: ${path}`, tool, parameters };
|
|
509
|
+
}
|
|
510
|
+
case 'delete_file': {
|
|
511
|
+
const path = parameters.path;
|
|
512
|
+
if (!path) {
|
|
513
|
+
return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
|
|
514
|
+
}
|
|
515
|
+
const validation = validatePath(path, projectRoot);
|
|
516
|
+
if (!validation.valid) {
|
|
517
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
518
|
+
}
|
|
519
|
+
if (!existsSync(validation.absolutePath)) {
|
|
520
|
+
return { success: false, output: '', error: `Path not found: ${path}`, tool, parameters };
|
|
521
|
+
}
|
|
522
|
+
// Record for undo (before delete)
|
|
523
|
+
recordDelete(validation.absolutePath);
|
|
524
|
+
const stat = statSync(validation.absolutePath);
|
|
525
|
+
if (stat.isDirectory()) {
|
|
526
|
+
// Delete directory recursively
|
|
527
|
+
rmSync(validation.absolutePath, { recursive: true, force: true });
|
|
528
|
+
return { success: true, output: `Deleted directory: ${path}`, tool, parameters };
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
unlinkSync(validation.absolutePath);
|
|
532
|
+
return { success: true, output: `Deleted file: ${path}`, tool, parameters };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
case 'list_files': {
|
|
536
|
+
const path = parameters.path || '.';
|
|
537
|
+
const recursive = parameters.recursive || false;
|
|
538
|
+
const validation = validatePath(path, projectRoot);
|
|
539
|
+
if (!validation.valid) {
|
|
540
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
541
|
+
}
|
|
542
|
+
if (!existsSync(validation.absolutePath)) {
|
|
543
|
+
return { success: false, output: '', error: `Directory not found: ${path}`, tool, parameters };
|
|
544
|
+
}
|
|
545
|
+
const stat = statSync(validation.absolutePath);
|
|
546
|
+
if (!stat.isDirectory()) {
|
|
547
|
+
return { success: false, output: '', error: `Path is not a directory: ${path}`, tool, parameters };
|
|
548
|
+
}
|
|
549
|
+
const files = listDirectory(validation.absolutePath, projectRoot, recursive);
|
|
550
|
+
return { success: true, output: files.join('\n'), tool, parameters };
|
|
551
|
+
}
|
|
552
|
+
case 'create_directory': {
|
|
553
|
+
const path = parameters.path;
|
|
554
|
+
if (!path) {
|
|
555
|
+
return { success: false, output: '', error: 'Missing required parameter: path', tool, parameters };
|
|
556
|
+
}
|
|
557
|
+
const validation = validatePath(path, projectRoot);
|
|
558
|
+
if (!validation.valid) {
|
|
559
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
560
|
+
}
|
|
561
|
+
if (existsSync(validation.absolutePath)) {
|
|
562
|
+
const stat = statSync(validation.absolutePath);
|
|
563
|
+
if (stat.isDirectory()) {
|
|
564
|
+
return { success: true, output: `Directory already exists: ${path}`, tool, parameters };
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
return { success: false, output: '', error: `Path exists but is a file: ${path}`, tool, parameters };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// Record for undo
|
|
571
|
+
recordMkdir(validation.absolutePath);
|
|
572
|
+
mkdirSync(validation.absolutePath, { recursive: true });
|
|
573
|
+
return { success: true, output: `Created directory: ${path}`, tool, parameters };
|
|
574
|
+
}
|
|
575
|
+
case 'execute_command': {
|
|
576
|
+
const command = parameters.command;
|
|
577
|
+
const args = parameters.args || [];
|
|
578
|
+
if (!command) {
|
|
579
|
+
return { success: false, output: '', error: 'Missing required parameter: command', tool, parameters };
|
|
580
|
+
}
|
|
581
|
+
// Record command (can't undo but tracked)
|
|
582
|
+
recordCommand(command, args);
|
|
583
|
+
const result = executeCommand(command, args, {
|
|
584
|
+
cwd: projectRoot,
|
|
585
|
+
projectRoot,
|
|
586
|
+
timeout: 120000, // 2 minutes for commands
|
|
587
|
+
});
|
|
588
|
+
if (result.success) {
|
|
589
|
+
return { success: true, output: result.stdout || '(no output)', tool, parameters };
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
return { success: false, output: result.stdout, error: result.stderr, tool, parameters };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
case 'search_code': {
|
|
596
|
+
const pattern = parameters.pattern;
|
|
597
|
+
const searchPath = parameters.path || '.';
|
|
598
|
+
if (!pattern) {
|
|
599
|
+
return { success: false, output: '', error: 'Missing required parameter: pattern', tool, parameters };
|
|
600
|
+
}
|
|
601
|
+
const validation = validatePath(searchPath, projectRoot);
|
|
602
|
+
if (!validation.valid) {
|
|
603
|
+
return { success: false, output: '', error: validation.error, tool, parameters };
|
|
604
|
+
}
|
|
605
|
+
// Use grep for search
|
|
606
|
+
const result = executeCommand('grep', ['-rn', '--include=*.{ts,tsx,js,jsx,json,md,css,html,py,go,rs}', pattern, validation.absolutePath], {
|
|
607
|
+
cwd: projectRoot,
|
|
608
|
+
projectRoot,
|
|
609
|
+
timeout: 30000,
|
|
610
|
+
});
|
|
611
|
+
if (result.exitCode === 0) {
|
|
612
|
+
// Limit output
|
|
613
|
+
const lines = result.stdout.split('\n').slice(0, 50);
|
|
614
|
+
return { success: true, output: lines.join('\n') || 'No matches found', tool, parameters };
|
|
615
|
+
}
|
|
616
|
+
else if (result.exitCode === 1) {
|
|
617
|
+
return { success: true, output: 'No matches found', tool, parameters };
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
return { success: false, output: '', error: result.stderr || 'Search failed', tool, parameters };
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
case 'fetch_url': {
|
|
624
|
+
const url = parameters.url;
|
|
625
|
+
if (!url) {
|
|
626
|
+
return { success: false, output: '', error: 'Missing required parameter: url', tool, parameters };
|
|
627
|
+
}
|
|
628
|
+
// Validate URL
|
|
629
|
+
try {
|
|
630
|
+
new URL(url);
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
return { success: false, output: '', error: 'Invalid URL format', tool, parameters };
|
|
634
|
+
}
|
|
635
|
+
// Use curl to fetch URL
|
|
636
|
+
const result = executeCommand('curl', [
|
|
637
|
+
'-s', '-L',
|
|
638
|
+
'-m', '30', // 30 second timeout
|
|
639
|
+
'-A', 'Codeep/1.0',
|
|
640
|
+
'--max-filesize', '1000000', // 1MB max
|
|
641
|
+
url
|
|
642
|
+
], {
|
|
643
|
+
cwd: projectRoot,
|
|
644
|
+
projectRoot,
|
|
645
|
+
timeout: 35000,
|
|
646
|
+
});
|
|
647
|
+
if (result.success) {
|
|
648
|
+
// Try to extract text content (strip HTML tags for basic display)
|
|
649
|
+
let content = result.stdout;
|
|
650
|
+
// If it looks like HTML, try to extract text
|
|
651
|
+
if (content.includes('<html') || content.includes('<!DOCTYPE')) {
|
|
652
|
+
// Simple HTML to text - remove script/style and tags
|
|
653
|
+
content = content
|
|
654
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
655
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
656
|
+
.replace(/<[^>]+>/g, ' ')
|
|
657
|
+
.replace(/\s+/g, ' ')
|
|
658
|
+
.trim();
|
|
659
|
+
}
|
|
660
|
+
// Limit output
|
|
661
|
+
if (content.length > 10000) {
|
|
662
|
+
content = content.substring(0, 10000) + '\n\n... (truncated)';
|
|
663
|
+
}
|
|
664
|
+
return { success: true, output: content, tool, parameters };
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
return { success: false, output: '', error: result.stderr || 'Failed to fetch URL', tool, parameters };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
default:
|
|
671
|
+
return { success: false, output: '', error: `Unknown tool: ${tool}`, tool, parameters };
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
const err = error;
|
|
676
|
+
return { success: false, output: '', error: err.message, tool, parameters };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* List directory contents
|
|
681
|
+
*/
|
|
682
|
+
function listDirectory(dir, projectRoot, recursive, prefix = '') {
|
|
683
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
684
|
+
const files = [];
|
|
685
|
+
// Skip common directories
|
|
686
|
+
const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.venv', 'venv']);
|
|
687
|
+
for (const entry of entries) {
|
|
688
|
+
const relativePath = relative(projectRoot, join(dir, entry.name));
|
|
689
|
+
if (entry.isDirectory()) {
|
|
690
|
+
if (skipDirs.has(entry.name))
|
|
691
|
+
continue;
|
|
692
|
+
files.push(`${prefix}${entry.name}/`);
|
|
693
|
+
if (recursive) {
|
|
694
|
+
const subFiles = listDirectory(join(dir, entry.name), projectRoot, true, prefix + ' ');
|
|
695
|
+
files.push(...subFiles);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
files.push(`${prefix}${entry.name}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return files;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Create action log from tool result
|
|
706
|
+
*/
|
|
707
|
+
export function createActionLog(toolCall, result) {
|
|
708
|
+
const typeMap = {
|
|
709
|
+
read_file: 'read',
|
|
710
|
+
write_file: 'write',
|
|
711
|
+
edit_file: 'edit',
|
|
712
|
+
delete_file: 'delete',
|
|
713
|
+
execute_command: 'command',
|
|
714
|
+
search_code: 'search',
|
|
715
|
+
list_files: 'list',
|
|
716
|
+
create_directory: 'mkdir',
|
|
717
|
+
fetch_url: 'fetch',
|
|
718
|
+
};
|
|
719
|
+
const target = toolCall.parameters.path ||
|
|
720
|
+
toolCall.parameters.command ||
|
|
721
|
+
toolCall.parameters.pattern ||
|
|
722
|
+
toolCall.parameters.url ||
|
|
723
|
+
'unknown';
|
|
724
|
+
return {
|
|
725
|
+
type: typeMap[toolCall.tool] || 'command',
|
|
726
|
+
target,
|
|
727
|
+
result: result.success ? 'success' : 'error',
|
|
728
|
+
details: result.success ? result.output.slice(0, 200) : result.error,
|
|
729
|
+
timestamp: Date.now(),
|
|
730
|
+
};
|
|
731
|
+
}
|