sam-coder-cli 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/README.md +59 -0
- package/ai-assistant-0.0.1.vsix +0 -0
- package/bin/agi-cli.js +815 -0
- package/bin/agi-cli.js.bak +352 -0
- package/bin/agi-cli.js.new +328 -0
- package/bin/config.json +3 -0
- package/bin/ui.js +42 -0
- package/dist/agentUtils.js +539 -0
- package/dist/agentUtils.js.map +1 -0
- package/dist/aiAssistantViewProvider.js +2098 -0
- package/dist/aiAssistantViewProvider.js.map +1 -0
- package/dist/extension.js +117 -0
- package/dist/extension.js.map +1 -0
- package/dist/fetch-polyfill.js +9 -0
- package/dist/fetch-polyfill.js.map +1 -0
- package/foldersnake/snake_game.py +125 -0
- package/media/ai-icon.png +0 -0
- package/media/ai-icon.svg +5 -0
- package/media/infinity-icon.svg +4 -0
- package/out/agentUtils.d.ts +28 -0
- package/out/agentUtils.d.ts.map +1 -0
- package/out/agentUtils.js +539 -0
- package/out/agentUtils.js.map +1 -0
- package/out/aiAssistantViewProvider.d.ts +58 -0
- package/out/aiAssistantViewProvider.d.ts.map +1 -0
- package/out/aiAssistantViewProvider.js +2098 -0
- package/out/aiAssistantViewProvider.js.map +1 -0
- package/out/extension.d.ts +4 -0
- package/out/extension.d.ts.map +1 -0
- package/out/extension.js +117 -0
- package/out/extension.js.map +1 -0
- package/out/fetch-polyfill.d.ts +11 -0
- package/out/fetch-polyfill.d.ts.map +1 -0
- package/out/fetch-polyfill.js +9 -0
- package/out/fetch-polyfill.js.map +1 -0
- package/package.json +31 -0
- package/src/agentUtils.ts +583 -0
- package/src/aiAssistantViewProvider.ts +2264 -0
- package/src/cliAgentUtils.js +73 -0
- package/src/extension.ts +112 -0
- package/src/fetch-polyfill.ts +11 -0
- package/tsconfig.json +24 -0
- package/webpack.config.js +45 -0
package/bin/agi-cli.js
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const ui = require('./ui.js');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs').promises;
|
|
7
|
+
const { exec } = require('child_process');
|
|
8
|
+
const util = require('util');
|
|
9
|
+
const execAsync = util.promisify(exec);
|
|
10
|
+
|
|
11
|
+
// Configuration
|
|
12
|
+
const CONFIG_PATH = path.join(__dirname, 'config.json');
|
|
13
|
+
let OPENROUTER_API_KEY;
|
|
14
|
+
const MODEL = 'deepseek/deepseek-chat-v3-0324:free';
|
|
15
|
+
const API_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
16
|
+
|
|
17
|
+
// Tool/Function definitions for the AI
|
|
18
|
+
const tools = [
|
|
19
|
+
{
|
|
20
|
+
type: 'function',
|
|
21
|
+
function: {
|
|
22
|
+
name: 'readFile',
|
|
23
|
+
description: 'Read the contents of a file',
|
|
24
|
+
parameters: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
path: { type: 'string', description: 'Path to the file to read' }
|
|
28
|
+
},
|
|
29
|
+
required: ['path']
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'function',
|
|
35
|
+
function: {
|
|
36
|
+
name: 'writeFile',
|
|
37
|
+
description: 'Write content to a file',
|
|
38
|
+
parameters: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
path: { type: 'string', description: 'Path to the file to write' },
|
|
42
|
+
content: { type: 'string', description: 'Content to write to the file' }
|
|
43
|
+
},
|
|
44
|
+
required: ['path', 'content']
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'function',
|
|
50
|
+
function: {
|
|
51
|
+
name: 'editFile',
|
|
52
|
+
description: 'Edit specific parts of a file',
|
|
53
|
+
parameters: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
path: { type: 'string', description: 'Path to the file to edit' },
|
|
57
|
+
edits: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
operations: {
|
|
61
|
+
type: 'array',
|
|
62
|
+
items: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
type: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
enum: ['replace', 'insert', 'delete'],
|
|
68
|
+
description: 'Type of edit operation'
|
|
69
|
+
},
|
|
70
|
+
startLine: {
|
|
71
|
+
type: 'number',
|
|
72
|
+
description: 'Starting line number (1-based)'
|
|
73
|
+
},
|
|
74
|
+
endLine: {
|
|
75
|
+
type: 'number',
|
|
76
|
+
description: 'Ending line number (1-based, inclusive)'
|
|
77
|
+
},
|
|
78
|
+
newText: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
description: 'New text to insert or replace with'
|
|
81
|
+
},
|
|
82
|
+
pattern: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Pattern to search for (for replace operations)'
|
|
85
|
+
},
|
|
86
|
+
replacement: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'Replacement text (for pattern-based replace)'
|
|
89
|
+
},
|
|
90
|
+
flags: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
description: 'Regex flags (e.g., "g" for global)'
|
|
93
|
+
},
|
|
94
|
+
position: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
enum: ['start', 'end'],
|
|
97
|
+
description: 'Where to insert (only for insert operations)'
|
|
98
|
+
},
|
|
99
|
+
line: {
|
|
100
|
+
type: 'number',
|
|
101
|
+
description: 'Line number to insert at (for line-based insert)'
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
required: ['type'],
|
|
105
|
+
oneOf: [
|
|
106
|
+
{
|
|
107
|
+
properties: {
|
|
108
|
+
type: { const: 'replace' },
|
|
109
|
+
startLine: { type: 'number' },
|
|
110
|
+
endLine: { type: 'number' },
|
|
111
|
+
newText: { type: 'string' }
|
|
112
|
+
},
|
|
113
|
+
required: ['startLine', 'endLine', 'newText']
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
properties: {
|
|
117
|
+
type: { const: 'replace' },
|
|
118
|
+
pattern: { type: 'string' },
|
|
119
|
+
replacement: { type: 'string' },
|
|
120
|
+
flags: { type: 'string' }
|
|
121
|
+
},
|
|
122
|
+
required: ['pattern', 'replacement']
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
properties: {
|
|
126
|
+
type: { const: 'insert' },
|
|
127
|
+
position: { type: 'string', enum: ['start', 'end'] },
|
|
128
|
+
text: { type: 'string' }
|
|
129
|
+
},
|
|
130
|
+
required: ['position', 'text']
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
properties: {
|
|
134
|
+
type: { const: 'insert' },
|
|
135
|
+
line: { type: 'number' },
|
|
136
|
+
text: { type: 'string' }
|
|
137
|
+
},
|
|
138
|
+
required: ['line', 'text']
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
properties: {
|
|
142
|
+
type: { const: 'delete' },
|
|
143
|
+
startLine: { type: 'number' },
|
|
144
|
+
endLine: { type: 'number' }
|
|
145
|
+
},
|
|
146
|
+
required: ['startLine', 'endLine']
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
required: ['operations']
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
required: ['path', 'edits']
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
type: 'function',
|
|
161
|
+
function: {
|
|
162
|
+
name: 'runCommand',
|
|
163
|
+
description: 'Execute a shell command',
|
|
164
|
+
parameters: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
command: { type: 'string', description: 'Command to execute' }
|
|
168
|
+
},
|
|
169
|
+
required: ['command']
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: 'function',
|
|
175
|
+
function: {
|
|
176
|
+
name: 'searchFiles',
|
|
177
|
+
description: 'Search for files using a glob pattern',
|
|
178
|
+
parameters: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {
|
|
181
|
+
pattern: { type: 'string', description: 'Glob pattern to search for' }
|
|
182
|
+
},
|
|
183
|
+
required: ['pattern']
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// System prompt for the AI Assistant when using tool calling
|
|
190
|
+
const TOOL_CALLING_PROMPT = `You are a helpful AI assistant with agency capabilities. You can perform actions on the user's system using the provided tools.
|
|
191
|
+
|
|
192
|
+
TOOLS AVAILABLE:
|
|
193
|
+
1. readFile - Read the contents of a file
|
|
194
|
+
2. writeFile - Write content to a file
|
|
195
|
+
3. editFile - Edit specific parts of a file
|
|
196
|
+
4. runCommand - Execute a shell command
|
|
197
|
+
5. searchFiles - Search for files using a glob pattern
|
|
198
|
+
|
|
199
|
+
ENVIRONMENT:
|
|
200
|
+
- OS: ${process.platform}
|
|
201
|
+
- Current directory: ${process.cwd()}
|
|
202
|
+
|
|
203
|
+
INSTRUCTIONS:
|
|
204
|
+
- Use the provided tools to accomplish the user's request
|
|
205
|
+
- Be concise but thorough in your responses
|
|
206
|
+
- When executing commands or making changes, explain what you're doing
|
|
207
|
+
- If you're unsure about a command or action, ask for clarification
|
|
208
|
+
- Be careful with destructive operations - warn before making changes
|
|
209
|
+
|
|
210
|
+
Always think step by step and explain your reasoning before taking actions that could affect the system.`;
|
|
211
|
+
|
|
212
|
+
// System prompt for the AI Assistant when using legacy function calling (JSON actions)
|
|
213
|
+
const FUNCTION_CALLING_PROMPT = `You are a helpful AI assistant with agency capabilities. You can perform actions on the user's system.
|
|
214
|
+
|
|
215
|
+
By default, you will continue to take actions in a loop until you decide to stop with the 'stop' action type.
|
|
216
|
+
Always wrap your JSON in markdown code blocks with the json language specifier.
|
|
217
|
+
When executing code or commands that might be potentially harmful, explain what the code does before executing it.`;
|
|
218
|
+
|
|
219
|
+
// Agent utilities
|
|
220
|
+
const agentUtils = {
|
|
221
|
+
async readFile(filePath) {
|
|
222
|
+
try {
|
|
223
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
224
|
+
return content;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
throw new Error(`Failed to read file ${filePath}: ${error.message}`);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
async writeFile(filePath, content) {
|
|
231
|
+
try {
|
|
232
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
233
|
+
return `Successfully wrote to ${filePath}`;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw new Error(`Failed to write to file ${filePath}: ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
async editFile(path, edits) {
|
|
240
|
+
try {
|
|
241
|
+
// Read the current file content
|
|
242
|
+
let content = await fs.readFile(path, 'utf-8');
|
|
243
|
+
const lines = content.split('\n');
|
|
244
|
+
|
|
245
|
+
// Process each edit operation
|
|
246
|
+
for (const op of edits.operations) {
|
|
247
|
+
switch (op.type) {
|
|
248
|
+
case 'replace':
|
|
249
|
+
if (op.startLine !== undefined && op.endLine !== undefined) {
|
|
250
|
+
// Line-based replacement
|
|
251
|
+
if (op.startLine < 1 || op.endLine > lines.length) {
|
|
252
|
+
throw new Error(`Line numbers out of range (1-${lines.length})`);
|
|
253
|
+
}
|
|
254
|
+
const before = lines.slice(0, op.startLine - 1);
|
|
255
|
+
const after = lines.slice(op.endLine);
|
|
256
|
+
const newLines = op.newText.split('\n');
|
|
257
|
+
lines.splice(0, lines.length, ...before, ...newLines, ...after);
|
|
258
|
+
} else if (op.pattern) {
|
|
259
|
+
// Pattern-based replacement
|
|
260
|
+
const regex = new RegExp(op.pattern, op.flags || '');
|
|
261
|
+
content = content.replace(regex, op.replacement);
|
|
262
|
+
// Update lines array for subsequent operations
|
|
263
|
+
lines.length = 0;
|
|
264
|
+
lines.push(...content.split('\n'));
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
|
|
268
|
+
case 'insert':
|
|
269
|
+
if (op.position === 'start') {
|
|
270
|
+
lines.unshift(...op.text.split('\n'));
|
|
271
|
+
} else if (op.position === 'end') {
|
|
272
|
+
lines.push(...op.text.split('\n'));
|
|
273
|
+
} else if (op.line !== undefined) {
|
|
274
|
+
if (op.line < 1 || op.line > lines.length + 1) {
|
|
275
|
+
throw new Error(`Line number out of range (1-${lines.length + 1})`);
|
|
276
|
+
}
|
|
277
|
+
const insertLines = op.text.split('\n');
|
|
278
|
+
lines.splice(op.line - 1, 0, ...insertLines);
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
|
|
282
|
+
case 'delete':
|
|
283
|
+
if (op.startLine < 1 || op.endLine > lines.length) {
|
|
284
|
+
throw new Error(`Line numbers out of range (1-${lines.length})`);
|
|
285
|
+
}
|
|
286
|
+
lines.splice(op.startLine - 1, op.endLine - op.startLine + 1);
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
default:
|
|
290
|
+
throw new Error(`Unknown operation type: ${op.type}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Write the modified content back to the file
|
|
295
|
+
await fs.writeFile(path, lines.join('\n'), 'utf-8');
|
|
296
|
+
return `Successfully edited ${path}`;
|
|
297
|
+
|
|
298
|
+
} catch (error) {
|
|
299
|
+
throw new Error(`Failed to edit file ${path}: ${error.message}`);
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
async runCommand(command) {
|
|
304
|
+
try {
|
|
305
|
+
const { stdout, stderr } = await execAsync(command, { cwd: process.cwd() });
|
|
306
|
+
if (stderr) {
|
|
307
|
+
console.error('Command stderr:', stderr);
|
|
308
|
+
}
|
|
309
|
+
return stdout || 'Command executed successfully (no output)';
|
|
310
|
+
} catch (error) {
|
|
311
|
+
throw new Error(`Command failed: ${error.message}`);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
async searchFiles(pattern) {
|
|
316
|
+
try {
|
|
317
|
+
// Convert glob pattern to regex for Windows compatibility
|
|
318
|
+
const globToRegex = (pattern) => {
|
|
319
|
+
const escaped = pattern.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
320
|
+
return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const regex = globToRegex(pattern);
|
|
324
|
+
const searchDir = process.cwd();
|
|
325
|
+
const results = [];
|
|
326
|
+
|
|
327
|
+
// Recursive directory search
|
|
328
|
+
const search = async (dir) => {
|
|
329
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
330
|
+
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
const fullPath = path.join(dir, entry.name);
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
if (entry.isDirectory()) {
|
|
336
|
+
await search(fullPath);
|
|
337
|
+
} else if (regex.test(entry.name)) {
|
|
338
|
+
results.push(fullPath);
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
// Skip files/directories we can't access
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
await search(searchDir);
|
|
348
|
+
return results.length > 0
|
|
349
|
+
? `Found ${results.length} files:\n${results.join('\n')}`
|
|
350
|
+
: 'No files found';
|
|
351
|
+
} catch (error) {
|
|
352
|
+
throw new Error(`Search failed: ${error.message}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// Extract JSON from markdown code blocks
|
|
358
|
+
function extractJsonFromMarkdown(text) {
|
|
359
|
+
// Try to find a markdown code block with JSON content
|
|
360
|
+
const codeBlockRegex = /```json\s*([\s\S]*?)\s*```/;
|
|
361
|
+
const match = text.match(codeBlockRegex);
|
|
362
|
+
|
|
363
|
+
if (match) {
|
|
364
|
+
try {
|
|
365
|
+
return JSON.parse(match[1]);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.error('Error parsing JSON from markdown:', error);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// If no code block, try to parse the entire text as JSON
|
|
372
|
+
try {
|
|
373
|
+
return JSON.parse(text);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error('Error parsing JSON:', error);
|
|
376
|
+
ui.stopThinking();
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Call OpenRouter API with tool calling
|
|
382
|
+
async function callOpenRouterWithTools(messages) {
|
|
383
|
+
const apiKey = OPENROUTER_API_KEY;
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
const response = await fetch(API_BASE_URL + '/chat/completions', {
|
|
387
|
+
method: 'POST',
|
|
388
|
+
headers: {
|
|
389
|
+
'Content-Type': 'application/json',
|
|
390
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
391
|
+
'HTTP-Referer': 'https://github.com/yourusername/agi-cli'
|
|
392
|
+
},
|
|
393
|
+
body: JSON.stringify({
|
|
394
|
+
model: MODEL,
|
|
395
|
+
messages: messages,
|
|
396
|
+
tools: tools,
|
|
397
|
+
tool_choice: 'auto'
|
|
398
|
+
})
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
if (!response.ok) {
|
|
402
|
+
const error = await response.json();
|
|
403
|
+
throw new Error(`API error: ${error.error?.message || response.statusText}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return await response.json();
|
|
407
|
+
} catch (error) {
|
|
408
|
+
console.error('API call failed:', error);
|
|
409
|
+
ui.stopThinking();
|
|
410
|
+
throw new Error(`Failed to call OpenRouter API: ${error.message}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Call OpenRouter API with function calling (legacy)
|
|
415
|
+
async function callOpenRouterWithFunctions(messages) {
|
|
416
|
+
const apiKey = OPENROUTER_API_KEY;
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const response = await fetch(API_BASE_URL + '/chat/completions', {
|
|
420
|
+
method: 'POST',
|
|
421
|
+
headers: {
|
|
422
|
+
'Content-Type': 'application/json',
|
|
423
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
424
|
+
'HTTP-Referer': 'https://github.com/yourusername/agi-cli'
|
|
425
|
+
},
|
|
426
|
+
body: JSON.stringify({
|
|
427
|
+
model: MODEL,
|
|
428
|
+
messages: messages
|
|
429
|
+
})
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
if (!response.ok) {
|
|
433
|
+
const error = await response.json();
|
|
434
|
+
throw new Error(`API error: ${error.error?.message || response.statusText}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return await response.json();
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error('API call failed:', error);
|
|
440
|
+
ui.stopThinking();
|
|
441
|
+
throw new Error(`Failed to call OpenRouter API: ${error.message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Process tool calls from the AI response
|
|
446
|
+
async function handleToolCalls(toolCalls, messages) {
|
|
447
|
+
const results = [];
|
|
448
|
+
|
|
449
|
+
for (const toolCall of toolCalls) {
|
|
450
|
+
const functionName = toolCall.function.name;
|
|
451
|
+
let args;
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
args = JSON.parse(toolCall.function.arguments);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
console.error('❌ Failed to parse tool arguments:', error);
|
|
457
|
+
results.push({
|
|
458
|
+
tool_call_id: toolCall.id,
|
|
459
|
+
role: 'tool',
|
|
460
|
+
name: functionName,
|
|
461
|
+
content: JSON.stringify({ error: `Invalid arguments format: ${error.message}` })
|
|
462
|
+
});
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
console.log(`🔧 Executing ${functionName} with args:`, args);
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
if (!agentUtils[functionName]) {
|
|
470
|
+
throw new Error(`Tool '${functionName}' not found`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const result = await agentUtils[functionName](...Object.values(args));
|
|
474
|
+
console.log('✅ Tool executed successfully');
|
|
475
|
+
|
|
476
|
+
// Stringify the result if it's not already a string
|
|
477
|
+
const resultContent = typeof result === 'string' ? result : JSON.stringify(result);
|
|
478
|
+
|
|
479
|
+
results.push({
|
|
480
|
+
tool_call_id: toolCall.id,
|
|
481
|
+
role: 'tool',
|
|
482
|
+
name: functionName,
|
|
483
|
+
content: resultContent
|
|
484
|
+
});
|
|
485
|
+
} catch (error) {
|
|
486
|
+
console.error('❌ Tool execution failed:', error);
|
|
487
|
+
|
|
488
|
+
results.push({
|
|
489
|
+
tool_call_id: toolCall.id,
|
|
490
|
+
role: 'tool',
|
|
491
|
+
name: functionName,
|
|
492
|
+
content: JSON.stringify({
|
|
493
|
+
error: error.message,
|
|
494
|
+
stack: process.env.DEBUG ? error.stack : undefined
|
|
495
|
+
})
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return results;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Process a query with tool calling
|
|
504
|
+
async function processQueryWithTools(query, conversation = [], maxIterations = 5) {
|
|
505
|
+
// Add user message to conversation
|
|
506
|
+
const userMessage = { role: 'user', content: query };
|
|
507
|
+
let messages = [...conversation, userMessage];
|
|
508
|
+
let iteration = 0;
|
|
509
|
+
let finalResponse = null;
|
|
510
|
+
|
|
511
|
+
// Add system message if this is the first message
|
|
512
|
+
if (conversation.length === 0) {
|
|
513
|
+
messages.unshift({
|
|
514
|
+
role: 'system',
|
|
515
|
+
content: `You are a helpful AI assistant with access to tools. Use the tools when needed.
|
|
516
|
+
You can use multiple tools in sequence if needed to complete the task.
|
|
517
|
+
When using tools, make sure to provide all required parameters.
|
|
518
|
+
If a tool fails, you can try again with different parameters or a different approach.`
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Process in a loop to handle multiple tool calls
|
|
523
|
+
while (iteration < maxIterations) {
|
|
524
|
+
ui.startThinking();
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const response = await callOpenRouterWithTools(messages);
|
|
528
|
+
const assistantMessage = response.choices[0].message;
|
|
529
|
+
messages.push(assistantMessage);
|
|
530
|
+
|
|
531
|
+
// If there are no tool calls, we're done
|
|
532
|
+
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
|
|
533
|
+
ui.stopThinking();
|
|
534
|
+
finalResponse = assistantMessage.content || 'No content in response';
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Process tool calls
|
|
539
|
+
console.log(`🛠️ Executing ${assistantMessage.tool_calls.length} tools...`);
|
|
540
|
+
const toolResults = await handleToolCalls(assistantMessage.tool_calls, messages);
|
|
541
|
+
|
|
542
|
+
// Add tool results to messages
|
|
543
|
+
messages = [...messages, ...toolResults];
|
|
544
|
+
|
|
545
|
+
// If we've reached max iterations, get a final response
|
|
546
|
+
if (iteration === maxIterations - 1) {
|
|
547
|
+
console.log('ℹ️ Reached maximum number of iterations. Getting final response...');
|
|
548
|
+
const finalResponseObj = await callOpenRouterWithTools(messages);
|
|
549
|
+
finalResponse = finalResponseObj.choices[0].message.content || 'No content in final response';
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
iteration++;
|
|
553
|
+
} catch (error) {
|
|
554
|
+
ui.stopThinking();
|
|
555
|
+
console.error('❌ Error during processing:', error);
|
|
556
|
+
finalResponse = `An error occurred: ${error.message}`;
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// If we don't have a final response yet (shouldn't happen, but just in case)
|
|
562
|
+
if (!finalResponse) {
|
|
563
|
+
finalResponse = 'No response generated. The operation may have timed out or encountered an error.';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
response: finalResponse,
|
|
568
|
+
conversation: messages
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Execute a single action from the action system
|
|
573
|
+
async function executeAction(action) {
|
|
574
|
+
const { type, data } = action;
|
|
575
|
+
|
|
576
|
+
switch (type) {
|
|
577
|
+
case 'read':
|
|
578
|
+
return await agentUtils.readFile(data.path);
|
|
579
|
+
|
|
580
|
+
case 'write':
|
|
581
|
+
return await agentUtils.writeFile(data.path, data.content);
|
|
582
|
+
|
|
583
|
+
case 'edit':
|
|
584
|
+
return await agentUtils.editFile(data.path, data.edits);
|
|
585
|
+
|
|
586
|
+
case 'command':
|
|
587
|
+
return await agentUtils.runCommand(data.command);
|
|
588
|
+
|
|
589
|
+
case 'search':
|
|
590
|
+
if (data.type === 'files') {
|
|
591
|
+
return await agentUtils.searchFiles(data.pattern);
|
|
592
|
+
}
|
|
593
|
+
throw new Error('Text search is not implemented yet');
|
|
594
|
+
|
|
595
|
+
case 'execute':
|
|
596
|
+
// For execute action, we'll run it as a command
|
|
597
|
+
const cmd = data.language === 'bash'
|
|
598
|
+
? data.code
|
|
599
|
+
: `node -e "${data.code.replace(/"/g, '\\"')}"`;
|
|
600
|
+
return await agentUtils.runCommand(cmd);
|
|
601
|
+
|
|
602
|
+
case 'browse':
|
|
603
|
+
throw new Error('Web browsing is not implemented yet');
|
|
604
|
+
|
|
605
|
+
case 'analyze':
|
|
606
|
+
// For analyze action, we'll just return the question for now
|
|
607
|
+
return `Analysis requested for code: ${data.code}\nQuestion: ${data.question}`;
|
|
608
|
+
|
|
609
|
+
case 'stop':
|
|
610
|
+
return 'Stopping action execution';
|
|
611
|
+
|
|
612
|
+
default:
|
|
613
|
+
throw new Error(`Unknown action type: ${type}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Process a query with action handling (legacy function calling)
|
|
618
|
+
async function processQuery(query, conversation = []) {
|
|
619
|
+
try {
|
|
620
|
+
// Add user message to conversation
|
|
621
|
+
const userMessage = { role: 'user', content: query };
|
|
622
|
+
const messages = [...conversation, userMessage];
|
|
623
|
+
|
|
624
|
+
// Add system message if this is the first message
|
|
625
|
+
if (conversation.length === 0) {
|
|
626
|
+
messages.unshift({
|
|
627
|
+
role: 'system',
|
|
628
|
+
content: FUNCTION_CALLING_PROMPT
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
ui.startThinking();
|
|
633
|
+
|
|
634
|
+
const response = await callOpenRouterWithFunctions(messages);
|
|
635
|
+
const assistantMessage = response.choices[0].message;
|
|
636
|
+
messages.push(assistantMessage);
|
|
637
|
+
|
|
638
|
+
// Try to extract JSON from the response
|
|
639
|
+
const actionData = extractJsonFromMarkdown(assistantMessage.content);
|
|
640
|
+
|
|
641
|
+
if (actionData && actionData.actions) {
|
|
642
|
+
ui.stopThinking();
|
|
643
|
+
ui.showAction('Executing actions...');
|
|
644
|
+
const results = [];
|
|
645
|
+
|
|
646
|
+
for (const action of actionData.actions) {
|
|
647
|
+
ui.showAction(` → ${action.type} action`);
|
|
648
|
+
try {
|
|
649
|
+
const result = await executeAction(action);
|
|
650
|
+
results.push({ type: action.type, success: true, result });
|
|
651
|
+
} catch (error) {
|
|
652
|
+
results.push({ type: action.type, success: false, error: error.message });
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Add action results to the conversation
|
|
657
|
+
messages.push({
|
|
658
|
+
role: 'system',
|
|
659
|
+
content: `Action results: ${JSON.stringify(results, null, 2)}`
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// Continue the conversation with the results
|
|
663
|
+
return {
|
|
664
|
+
response: `Actions executed. Results: ${JSON.stringify(results, null, 2)}`,
|
|
665
|
+
conversation: messages
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
ui.stopThinking();
|
|
670
|
+
return {
|
|
671
|
+
response: assistantMessage.content || 'No content in response',
|
|
672
|
+
conversation: messages
|
|
673
|
+
};
|
|
674
|
+
} catch (error) {
|
|
675
|
+
ui.stopThinking();
|
|
676
|
+
ui.showError(`Error processing query: ${error.message}`);
|
|
677
|
+
return {
|
|
678
|
+
response: `Error: ${error.message}`,
|
|
679
|
+
conversation: conversation || []
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Main chat loop
|
|
685
|
+
async function chat(useToolCalling) {
|
|
686
|
+
const conversation = [];
|
|
687
|
+
ui.showHeader();
|
|
688
|
+
console.log('Type your message, or "exit" to quit.');
|
|
689
|
+
|
|
690
|
+
const rl = readline.createInterface({
|
|
691
|
+
input: process.stdin,
|
|
692
|
+
output: process.stdout,
|
|
693
|
+
prompt: '> '
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
rl.prompt();
|
|
697
|
+
|
|
698
|
+
rl.on('line', async (input) => {
|
|
699
|
+
if (input.toLowerCase() === '/setup') {
|
|
700
|
+
await runSetup(true);
|
|
701
|
+
console.log('\nSetup complete. Please restart the application to apply changes.');
|
|
702
|
+
rl.close();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (input.toLowerCase() === 'exit') {
|
|
707
|
+
rl.close();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const result = useToolCalling
|
|
712
|
+
? await processQueryWithTools(input, conversation)
|
|
713
|
+
: await processQuery(input, conversation);
|
|
714
|
+
ui.stopThinking();
|
|
715
|
+
ui.showResponse(result.response);
|
|
716
|
+
|
|
717
|
+
// Update conversation with the full context
|
|
718
|
+
conversation.length = 0; // Clear the array
|
|
719
|
+
result.conversation.forEach(msg => conversation.push(msg));
|
|
720
|
+
|
|
721
|
+
rl.prompt();
|
|
722
|
+
}).on('close', () => {
|
|
723
|
+
ui.showResponse('Goodbye!');
|
|
724
|
+
process.exit(0);
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Ask user for mode selection
|
|
729
|
+
function askForMode() {
|
|
730
|
+
const rl = readline.createInterface({
|
|
731
|
+
input: process.stdin,
|
|
732
|
+
output: process.stdout
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
return new Promise((resolve) => {
|
|
736
|
+
rl.question('Select mode (1 for tool calling, 2 for function calling): ', (answer) => {
|
|
737
|
+
rl.close();
|
|
738
|
+
resolve(answer.trim() === '1');
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Start the application
|
|
744
|
+
async function readConfig() {
|
|
745
|
+
try {
|
|
746
|
+
const data = await fs.readFile(CONFIG_PATH, 'utf-8');
|
|
747
|
+
return JSON.parse(data);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
if (error.code === 'ENOENT') {
|
|
750
|
+
return null; // Config file doesn't exist
|
|
751
|
+
}
|
|
752
|
+
throw error;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
async function writeConfig(config) {
|
|
757
|
+
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function runSetup(isReconfig = false) {
|
|
761
|
+
const rl = readline.createInterface({
|
|
762
|
+
input: process.stdin,
|
|
763
|
+
output: process.stdout
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
const askQuestion = (query) => new Promise(resolve => rl.question(query, resolve));
|
|
767
|
+
|
|
768
|
+
if (!isReconfig) {
|
|
769
|
+
ui.showHeader();
|
|
770
|
+
console.log('Welcome to SAM-CODER Setup!');
|
|
771
|
+
console.log('Please provide your OpenRouter API key to get started.');
|
|
772
|
+
} else {
|
|
773
|
+
console.log('\n--- Re-running Setup ---');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const apiKey = await askQuestion('Enter OpenRouter API Key: ');
|
|
777
|
+
|
|
778
|
+
const config = { OPENROUTER_API_KEY: apiKey };
|
|
779
|
+
await writeConfig(config);
|
|
780
|
+
|
|
781
|
+
console.log('✅ Configuration saved successfully!');
|
|
782
|
+
rl.close();
|
|
783
|
+
return config;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Start the application
|
|
787
|
+
async function start() {
|
|
788
|
+
try {
|
|
789
|
+
let config = await readConfig();
|
|
790
|
+
if (!config || !config.OPENROUTER_API_KEY) {
|
|
791
|
+
config = await runSetup();
|
|
792
|
+
console.log('\nSetup complete. Please start the application again.');
|
|
793
|
+
process.exit(0);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
OPENROUTER_API_KEY = config.OPENROUTER_API_KEY;
|
|
797
|
+
|
|
798
|
+
ui.showHeader();
|
|
799
|
+
console.log('Select Mode:');
|
|
800
|
+
console.log('1. Tool Calling (for models that support it)');
|
|
801
|
+
console.log('2. Function Calling (legacy)');
|
|
802
|
+
|
|
803
|
+
const useToolCalling = await askForMode();
|
|
804
|
+
ui.showResponse(`\nStarting in ${useToolCalling ? 'Tool Calling' : 'Function Calling'} mode...\n`);
|
|
805
|
+
|
|
806
|
+
// Start the chat with the selected mode
|
|
807
|
+
await chat(useToolCalling);
|
|
808
|
+
} catch (error) {
|
|
809
|
+
ui.showError(error);
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Start the application
|
|
815
|
+
start().catch(console.error);
|