camo-cli 2.0.1
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 +184 -0
- package/dist/agent.js +977 -0
- package/dist/art.js +33 -0
- package/dist/components/App.js +71 -0
- package/dist/components/Chat.js +509 -0
- package/dist/components/HITLConfirmation.js +89 -0
- package/dist/components/ModelSelector.js +100 -0
- package/dist/components/SetupScreen.js +43 -0
- package/dist/config/constants.js +58 -0
- package/dist/config/prompts.js +98 -0
- package/dist/config/store.js +5 -0
- package/dist/core/AgentLoop.js +159 -0
- package/dist/hooks/useAutocomplete.js +52 -0
- package/dist/hooks/useKeyboard.js +73 -0
- package/dist/index.js +31 -0
- package/dist/mcp.js +95 -0
- package/dist/memory/MemoryManager.js +228 -0
- package/dist/providers/index.js +85 -0
- package/dist/providers/registry.js +121 -0
- package/dist/providers/types.js +5 -0
- package/dist/theme.js +45 -0
- package/dist/tools/FileTools.js +88 -0
- package/dist/tools/MemoryTools.js +53 -0
- package/dist/tools/SearchTools.js +45 -0
- package/dist/tools/ShellTools.js +40 -0
- package/dist/tools/TaskTools.js +52 -0
- package/dist/tools/ToolDefinitions.js +102 -0
- package/dist/tools/ToolRegistry.js +30 -0
- package/dist/types/Agent.js +6 -0
- package/dist/types/ink.js +1 -0
- package/dist/types/message.js +1 -0
- package/dist/types/ui.js +1 -0
- package/dist/utils/CriticAgent.js +88 -0
- package/dist/utils/DecisionLogger.js +156 -0
- package/dist/utils/MessageHistory.js +55 -0
- package/dist/utils/PermissionManager.js +253 -0
- package/dist/utils/SessionManager.js +180 -0
- package/dist/utils/TaskState.js +108 -0
- package/dist/utils/debug.js +35 -0
- package/dist/utils/execAsync.js +3 -0
- package/dist/utils/retry.js +50 -0
- package/dist/utils/tokenCounter.js +24 -0
- package/dist/utils/uiFormatter.js +106 -0
- package/package.json +92 -0
package/dist/agent.js
ADDED
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { createPatch, parsePatch, applyPatch } from 'diff';
|
|
8
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
9
|
+
import { JSDOM } from 'jsdom';
|
|
10
|
+
import { Readability } from '@mozilla/readability';
|
|
11
|
+
import TurndownService from 'turndown';
|
|
12
|
+
import { config } from './config/store.js';
|
|
13
|
+
import { PermissionManager } from './utils/PermissionManager.js';
|
|
14
|
+
import { SessionManager } from './utils/SessionManager.js';
|
|
15
|
+
import { DecisionLogger } from './utils/DecisionLogger.js';
|
|
16
|
+
import { SHELL_TIMEOUT_MS } from './config/constants.js';
|
|
17
|
+
import { runAgentLoop } from './core/AgentLoop.js';
|
|
18
|
+
const execAsync = promisify(exec);
|
|
19
|
+
// Context Helpers (Managed here but used in PermissionManager via pass-through)
|
|
20
|
+
let lastUserMessage = '';
|
|
21
|
+
let projectContextCache = '';
|
|
22
|
+
// Singletons for MVP features
|
|
23
|
+
const sessionManager = SessionManager.getInstance();
|
|
24
|
+
const decisionLogger = DecisionLogger.getInstance();
|
|
25
|
+
let criticAgent = null;
|
|
26
|
+
let activePlan = [];
|
|
27
|
+
// Helper to generate hash ID from title + scope
|
|
28
|
+
function generateTaskId(title, scope = 'global') {
|
|
29
|
+
const input = title + ":" + scope;
|
|
30
|
+
let hash = 0;
|
|
31
|
+
for (let i = 0; i < input.length; i++) {
|
|
32
|
+
const char = input.charCodeAt(i);
|
|
33
|
+
hash = ((hash << 5) - hash) + char;
|
|
34
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
35
|
+
}
|
|
36
|
+
return Math.abs(hash).toString(16).substring(0, 6);
|
|
37
|
+
}
|
|
38
|
+
// Tool Output Buffer
|
|
39
|
+
// Tool Output Buffer
|
|
40
|
+
class ToolOutputBuffer {
|
|
41
|
+
constructor(callbacks, maxChars = 8000) {
|
|
42
|
+
this.buffer = '';
|
|
43
|
+
this.lineBuffer = '';
|
|
44
|
+
this.callbacks = callbacks;
|
|
45
|
+
this.maxChars = maxChars;
|
|
46
|
+
}
|
|
47
|
+
append(data) {
|
|
48
|
+
this.buffer += data;
|
|
49
|
+
this.lineBuffer += data;
|
|
50
|
+
let newlineIndex;
|
|
51
|
+
while ((newlineIndex = this.lineBuffer.indexOf('\n')) !== -1) {
|
|
52
|
+
const line = this.lineBuffer.substring(0, newlineIndex).trim();
|
|
53
|
+
this.lineBuffer = this.lineBuffer.substring(newlineIndex + 1);
|
|
54
|
+
if (line.length > 0) {
|
|
55
|
+
this.callbacks.onChunk(`\n[BUFFER_LINE]${line}[/BUFFER_LINE]\n`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
getOutput() {
|
|
60
|
+
// Flush remaining line buffer
|
|
61
|
+
if (this.lineBuffer.trim().length > 0) {
|
|
62
|
+
this.callbacks.onChunk(`\n[BUFFER_LINE]${this.lineBuffer.trim()}[/BUFFER_LINE]\n`);
|
|
63
|
+
}
|
|
64
|
+
if (this.buffer.length <= this.maxChars) {
|
|
65
|
+
return this.buffer;
|
|
66
|
+
}
|
|
67
|
+
const keep = Math.floor(this.maxChars * 0.4);
|
|
68
|
+
const head = this.buffer.substring(0, keep);
|
|
69
|
+
const tail = this.buffer.substring(this.buffer.length - keep);
|
|
70
|
+
const skipped = this.buffer.length - (keep * 2);
|
|
71
|
+
return `${head}\n... [Output truncated (${skipped} chars)] ...\n${tail}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function initializeAgentContext() {
|
|
75
|
+
try {
|
|
76
|
+
const contextParts = [];
|
|
77
|
+
const cwd = process.cwd();
|
|
78
|
+
// 1. Scan package.json
|
|
79
|
+
try {
|
|
80
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
81
|
+
const pkgContent = await fs.readFile(pkgPath, 'utf-8');
|
|
82
|
+
const pkg = JSON.parse(pkgContent);
|
|
83
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
84
|
+
const importantTech = Object.keys(deps).filter(d => ['react', 'vue', 'next', 'vite', 'typescript', 'tailwindcss', 'ink', 'zod', 'express', 'nest', 'ai-sdk'].some(t => d.includes(t)));
|
|
85
|
+
if (importantTech.length > 0) {
|
|
86
|
+
contextParts.push(`**PROJEKT TECH STACK**:\n- Dependencies: ${importantTech.join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (e) { }
|
|
90
|
+
// 2. Scan Directory Structure (Depth 2)
|
|
91
|
+
try {
|
|
92
|
+
const getDirStructure = async (dir, depth) => {
|
|
93
|
+
if (depth > 2)
|
|
94
|
+
return [];
|
|
95
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
96
|
+
const lines = [];
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (['node_modules', '.git', '.DS_Store', 'dist', 'build', '.next'].includes(entry.name))
|
|
99
|
+
continue;
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
lines.push(`${' '.repeat(2 - depth)}/${entry.name}`);
|
|
102
|
+
const subLines = await getDirStructure(path.join(dir, entry.name), depth + 1);
|
|
103
|
+
lines.push(...subLines);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
lines.push(`${' '.repeat(2 - depth)}${entry.name}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return lines;
|
|
110
|
+
};
|
|
111
|
+
const structure = await getDirStructure(cwd, 1);
|
|
112
|
+
if (structure.length > 0) {
|
|
113
|
+
contextParts.push(`**DATEI STRUKTUR (Auszug)**:\n${structure.slice(0, 30).join('\n')}${structure.length > 30 ? '\n...(truncated)' : ''}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (e) { }
|
|
117
|
+
if (contextParts.length > 0) {
|
|
118
|
+
projectContextCache = `\n\n**AUTOMATISCHE PROJEKT ANALYSE**:\n${contextParts.join('\n\n')}\n`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) { }
|
|
122
|
+
}
|
|
123
|
+
// Google tools with correct format
|
|
124
|
+
// Google tools defined in src/tools/ToolDefinitions.ts
|
|
125
|
+
// Tool implementations
|
|
126
|
+
const toolImplementations = {
|
|
127
|
+
async readFile(args, callbacks) {
|
|
128
|
+
try {
|
|
129
|
+
const p = path.resolve(process.cwd(), args.path);
|
|
130
|
+
const content = await fs.readFile(p, 'utf-8');
|
|
131
|
+
const lines = content.split('\n');
|
|
132
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Read ${lines.length} lines from ${args.path}[/TOOL_STATUS]\n`);
|
|
133
|
+
return JSON.stringify({ content: content.slice(0, 8000) });
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
callbacks.onChunk(`\n[TOOL_OUTPUT]Error: ${e.message}[/TOOL_OUTPUT]\n`);
|
|
137
|
+
return JSON.stringify({ error: e.message });
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
async writeFile(args, callbacks) {
|
|
141
|
+
const p = path.resolve(process.cwd(), args.path);
|
|
142
|
+
let diff = '';
|
|
143
|
+
try {
|
|
144
|
+
const existingContent = await fs.readFile(p, 'utf-8');
|
|
145
|
+
diff = createPatch(args.path, existingContent, args.content);
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
diff = createPatch(args.path, '', args.content);
|
|
149
|
+
}
|
|
150
|
+
// Permission Check
|
|
151
|
+
const allowed = await PermissionManager.getInstance().validate('FILE_WRITE', {
|
|
152
|
+
path: args.path,
|
|
153
|
+
diff: diff
|
|
154
|
+
}, callbacks);
|
|
155
|
+
if (!allowed) {
|
|
156
|
+
return JSON.stringify({ error: 'User denied file write.' });
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await fs.writeFile(p, args.content, 'utf-8');
|
|
160
|
+
callbacks.onChunk(`\n[TOOL_STATUS][ok] File written: ${args.path}[/TOOL_STATUS]\n`);
|
|
161
|
+
return JSON.stringify({ success: true, path: args.path });
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
callbacks.onChunk(`\n[TOOL_OUTPUT]Error: ${e.message}[/TOOL_OUTPUT]\n`);
|
|
165
|
+
return JSON.stringify({ error: e.message });
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
async listFiles(args, callbacks) {
|
|
169
|
+
try {
|
|
170
|
+
const p = path.resolve(process.cwd(), args.path || '.');
|
|
171
|
+
const files = await fs.readdir(p);
|
|
172
|
+
const clean = files.filter(f => !['node_modules', '.git', '.DS_Store'].includes(f));
|
|
173
|
+
const output = clean.slice(0, 10).join(', ') + (clean.length > 10 ? '...' : '');
|
|
174
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Files: ${output}[/TOOL_STATUS]\n`);
|
|
175
|
+
return JSON.stringify({ files: clean });
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
callbacks.onChunk(`\n[TOOL_OUTPUT]Error: ${e.message}[/TOOL_OUTPUT]\n`);
|
|
179
|
+
return JSON.stringify({ error: e.message });
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
async executeShellCommand(args, callbacks) {
|
|
183
|
+
// Permission Check
|
|
184
|
+
const allowed = await PermissionManager.getInstance().validate('SHELL', {
|
|
185
|
+
command: args.command
|
|
186
|
+
}, callbacks);
|
|
187
|
+
if (!allowed) {
|
|
188
|
+
return JSON.stringify({ error: 'User denied command execution.' });
|
|
189
|
+
}
|
|
190
|
+
// Execute with output display
|
|
191
|
+
try {
|
|
192
|
+
const { stdout, stderr } = await execAsync(args.command, {
|
|
193
|
+
cwd: process.cwd(),
|
|
194
|
+
timeout: SHELL_TIMEOUT_MS
|
|
195
|
+
});
|
|
196
|
+
const output = stdout + (stderr ? '\n' + stderr : '');
|
|
197
|
+
const preview = output.trim().slice(0, 200);
|
|
198
|
+
if (preview.length > 0) {
|
|
199
|
+
callbacks.onChunk(`\n[TOOL_STATUS]${preview}${output.length > 200 ? '...' : ''}[/TOOL_STATUS]\n`);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
callbacks.onChunk(`\n[TOOL_STATUS][ok] Command executed[/TOOL_STATUS]\n`);
|
|
203
|
+
}
|
|
204
|
+
return JSON.stringify({ exitCode: 0, output: output.slice(0, 2000) });
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
const errorMsg = e.stderr || e.message;
|
|
208
|
+
callbacks.onChunk(`\n[TOOL_OUTPUT]Error: ${errorMsg.slice(0, 200)}[/TOOL_OUTPUT]\n`);
|
|
209
|
+
return JSON.stringify({ exitCode: e.code || 1, error: errorMsg });
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
async grep(args, callbacks) {
|
|
213
|
+
try {
|
|
214
|
+
const searchPath = args.path || '.';
|
|
215
|
+
const command = `grep -rIn "${args.pattern.replace(/"/g, '\\"')}" ${searchPath} | head -n 20`;
|
|
216
|
+
const { stdout } = await execAsync(command);
|
|
217
|
+
const output = stdout.trim();
|
|
218
|
+
if (!output) {
|
|
219
|
+
const findCmd = `find ${searchPath} -name "*${args.pattern}*" -not -path "*/node_modules/*" | head -n 5`;
|
|
220
|
+
const { stdout: findOut } = await execAsync(findCmd);
|
|
221
|
+
if (findOut.trim()) {
|
|
222
|
+
const files = findOut.trim().split('\n');
|
|
223
|
+
const msg = files.map(f => `[BUFFER_LINE]Found: ${f}[/BUFFER_LINE]`).join('\n');
|
|
224
|
+
callbacks.onChunk(`\n${msg}\n`);
|
|
225
|
+
return JSON.stringify({ files: files });
|
|
226
|
+
}
|
|
227
|
+
callbacks.onChunk(`\n[TOOL_STATUS]No matches found for "${args.pattern}"[/TOOL_STATUS]\n`);
|
|
228
|
+
return JSON.stringify({ results: [] });
|
|
229
|
+
}
|
|
230
|
+
callbacks.onChunk(`\n[BUFFER_LINE]${output.split('\n').slice(0, 3).join('\n')}[/BUFFER_LINE]\n[TOOL_STATUS]Grep complete (${output.split('\n').length} lines)[/TOOL_STATUS]\n`);
|
|
231
|
+
return JSON.stringify({ results: output });
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
if (e.code === 1) {
|
|
235
|
+
callbacks.onChunk(`\n[TOOL_OUTPUT]No matches found[/TOOL_OUTPUT]\n`);
|
|
236
|
+
return JSON.stringify({ results: [] });
|
|
237
|
+
}
|
|
238
|
+
callbacks.onChunk(`\n[TOOL_OUTPUT]Error: ${e.message}[/TOOL_OUTPUT]\n`);
|
|
239
|
+
return JSON.stringify({ error: e.message });
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
async editFile(args, callbacks) {
|
|
243
|
+
const p = path.resolve(process.cwd(), args.path);
|
|
244
|
+
try {
|
|
245
|
+
// Read current file content
|
|
246
|
+
const originalContent = await fs.readFile(p, 'utf-8');
|
|
247
|
+
// Parse the unified diff
|
|
248
|
+
const patches = parsePatch(args.unifiedDiff);
|
|
249
|
+
if (patches.length === 0) {
|
|
250
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Error: Invalid unified diff format[/TOOL_STATUS]\n`);
|
|
251
|
+
return JSON.stringify({ error: 'Invalid unified diff format' });
|
|
252
|
+
}
|
|
253
|
+
// Apply patch with strict matching (no fuzzy match)
|
|
254
|
+
const patchResult = applyPatch(originalContent, patches[0], { fuzzFactor: 0 });
|
|
255
|
+
if (patchResult === false) {
|
|
256
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Error: Diff context mismatch. Re-read file with readFile first.[/TOOL_STATUS]\n`);
|
|
257
|
+
return JSON.stringify({
|
|
258
|
+
error: 'Diff context mismatch. The file content does not match the expected context. Use readFile to get current state.'
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const newContent = patchResult;
|
|
262
|
+
// Generate visual diff for HITL
|
|
263
|
+
const visualDiff = createPatch(args.path, originalContent, newContent);
|
|
264
|
+
// Permission Check with diff preview
|
|
265
|
+
const allowed = await PermissionManager.getInstance().validate('FILE_WRITE', {
|
|
266
|
+
path: args.path,
|
|
267
|
+
diff: visualDiff
|
|
268
|
+
}, callbacks);
|
|
269
|
+
if (!allowed)
|
|
270
|
+
return JSON.stringify({ error: 'Denied' });
|
|
271
|
+
// Apply changes
|
|
272
|
+
await fs.writeFile(p, newContent, 'utf-8');
|
|
273
|
+
callbacks.onChunk(`\n[TOOL_STATUS][ok] Patch applied to ${args.path}[/TOOL_STATUS]\n`);
|
|
274
|
+
// Auto-lint for TS/JS files
|
|
275
|
+
if (/\.(ts|tsx|js|jsx)$/.test(args.path)) {
|
|
276
|
+
const lintResult = await toolImplementations.autoLint({ path: args.path, fix: false }, callbacks);
|
|
277
|
+
const parsed = JSON.parse(lintResult);
|
|
278
|
+
if (!parsed.success && parsed.errors) {
|
|
279
|
+
return JSON.stringify({
|
|
280
|
+
success: true,
|
|
281
|
+
path: args.path,
|
|
282
|
+
lintErrors: parsed.errors
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return JSON.stringify({ success: true, path: args.path });
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Error: ${e.message}[/TOOL_STATUS]\n`);
|
|
290
|
+
return JSON.stringify({ error: e.message });
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
async ToDoWrite(args, callbacks) {
|
|
294
|
+
const { action, taskId, title, scope } = args;
|
|
295
|
+
if (action === 'create') {
|
|
296
|
+
if (!title)
|
|
297
|
+
return JSON.stringify({ error: 'Title required' });
|
|
298
|
+
const taskScope = scope || 'global';
|
|
299
|
+
const id = generateTaskId(title, taskScope);
|
|
300
|
+
activePlan.push({ id, title, scope: taskScope, status: 'pending' });
|
|
301
|
+
callbacks.onChunk(`\n[PLAN_DELTA]+ New Task: ${title} (${id}) [${taskScope}][/PLAN_DELTA]\n`);
|
|
302
|
+
return JSON.stringify({ success: true, id });
|
|
303
|
+
}
|
|
304
|
+
if (action === 'update') {
|
|
305
|
+
const task = activePlan.find(t => t.id === taskId);
|
|
306
|
+
if (!task)
|
|
307
|
+
return JSON.stringify({ error: 'Task not found' });
|
|
308
|
+
const running = activePlan.find(t => t.status === 'in_progress');
|
|
309
|
+
if (running && running.id !== taskId)
|
|
310
|
+
return JSON.stringify({ error: `Task ${running.id} is already in_progress` });
|
|
311
|
+
task.status = 'in_progress';
|
|
312
|
+
// Silent update for STATE SYNC (no chatty output if possible, but delta is useful for UI)
|
|
313
|
+
callbacks.onChunk(`\n[PLAN_DELTA]→ Started: ${task.title}[/PLAN_DELTA]\n`);
|
|
314
|
+
return JSON.stringify({ success: true });
|
|
315
|
+
}
|
|
316
|
+
if (action === 'complete') {
|
|
317
|
+
const task = activePlan.find(t => t.id === taskId);
|
|
318
|
+
if (!task)
|
|
319
|
+
return JSON.stringify({ error: 'Task not found' });
|
|
320
|
+
task.status = 'completed';
|
|
321
|
+
callbacks.onChunk(`\n[PLAN_DELTA]✓ Completed: ${task.title}[/PLAN_DELTA]\n`);
|
|
322
|
+
// Check if all tasks are done
|
|
323
|
+
const allDone = activePlan.every(t => t.status === 'completed' || t.status === 'failed');
|
|
324
|
+
if (allDone && activePlan.length > 0) {
|
|
325
|
+
const completed = activePlan.filter(t => t.status === 'completed');
|
|
326
|
+
const failed = activePlan.filter(t => t.status === 'failed');
|
|
327
|
+
let summary = `\n⏺ Plan Summary:\n`;
|
|
328
|
+
summary += ` ✓ ${completed.length} completed`;
|
|
329
|
+
if (failed.length > 0)
|
|
330
|
+
summary += `, ✗ ${failed.length} failed`;
|
|
331
|
+
summary += `\n`;
|
|
332
|
+
completed.forEach(t => summary += ` • ${t.title}\n`);
|
|
333
|
+
callbacks.onChunk(summary);
|
|
334
|
+
// Clear plan for next task
|
|
335
|
+
activePlan.length = 0;
|
|
336
|
+
}
|
|
337
|
+
return JSON.stringify({ success: true });
|
|
338
|
+
}
|
|
339
|
+
if (action === 'fail') {
|
|
340
|
+
const task = activePlan.find(t => t.id === taskId);
|
|
341
|
+
if (!task)
|
|
342
|
+
return JSON.stringify({ error: 'Task not found' });
|
|
343
|
+
task.status = 'failed';
|
|
344
|
+
callbacks.onChunk(`\n[PLAN_DELTA][x] Failed: ${task.title}[/PLAN_DELTA]\n`);
|
|
345
|
+
return JSON.stringify({ success: true });
|
|
346
|
+
}
|
|
347
|
+
return JSON.stringify({ error: 'Invalid action' });
|
|
348
|
+
},
|
|
349
|
+
async webFetch(args, callbacks) {
|
|
350
|
+
const url = args.url;
|
|
351
|
+
const allowed = await PermissionManager.getInstance().validate('WEB_ACCESS', {
|
|
352
|
+
url: url,
|
|
353
|
+
userMessage: lastUserMessage,
|
|
354
|
+
projectContext: projectContextCache
|
|
355
|
+
}, callbacks);
|
|
356
|
+
if (!allowed) {
|
|
357
|
+
return JSON.stringify({ error: 'User denied web access.' });
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Fetching ${url}...[/TOOL_STATUS]`);
|
|
361
|
+
const response = await fetch(url);
|
|
362
|
+
if (!response.ok)
|
|
363
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
364
|
+
const html = await response.text();
|
|
365
|
+
const doc = new JSDOM(html, { url });
|
|
366
|
+
const reader = new Readability(doc.window.document);
|
|
367
|
+
const article = reader.parse();
|
|
368
|
+
if (!article)
|
|
369
|
+
return JSON.stringify({ error: 'Failed to parse page content.' });
|
|
370
|
+
const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
|
|
371
|
+
const markdown = turndownService.turndown(article.content || '');
|
|
372
|
+
callbacks.onChunk(`\n[TOOL_STATUS][ok] Fetched and converted to Markdown.[/TOOL_STATUS]\n`);
|
|
373
|
+
return JSON.stringify({ title: article.title, content: markdown.slice(0, 15000) });
|
|
374
|
+
}
|
|
375
|
+
catch (e) {
|
|
376
|
+
callbacks.onChunk(`\n[TOOL_OUTPUT]Error: ${e.message}[/TOOL_OUTPUT]\n`);
|
|
377
|
+
return JSON.stringify({ error: e.message });
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
async internal_thought(args, callbacks) {
|
|
381
|
+
callbacks.onChunk(`\n[INTERNAL_THOUGHT]${args.analysis}[/INTERNAL_THOUGHT]\n`);
|
|
382
|
+
return JSON.stringify({ acknowledged: true });
|
|
383
|
+
},
|
|
384
|
+
async listSymbols(args, callbacks) {
|
|
385
|
+
try {
|
|
386
|
+
const filePath = path.resolve(process.cwd(), args.path);
|
|
387
|
+
const sourceCode = await fs.readFile(filePath, 'utf-8');
|
|
388
|
+
const ts = await import('typescript');
|
|
389
|
+
const sourceFile = ts.createSourceFile(args.path, sourceCode, ts.ScriptTarget.Latest, true);
|
|
390
|
+
const symbols = [];
|
|
391
|
+
function visit(node) {
|
|
392
|
+
const filter = args.symbolType || 'all';
|
|
393
|
+
if ((filter === 'all' || filter === 'functions') && ts.isFunctionDeclaration(node) && node.name) {
|
|
394
|
+
symbols.push({
|
|
395
|
+
type: 'function',
|
|
396
|
+
name: node.name.text,
|
|
397
|
+
signature: node.getText().split('{')[0].trim(),
|
|
398
|
+
line: sourceFile.getLineAndCharacterOfPosition(node.pos).line + 1
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if ((filter === 'all' || filter === 'classes') && ts.isClassDeclaration(node) && node.name) {
|
|
402
|
+
symbols.push({
|
|
403
|
+
type: 'class',
|
|
404
|
+
name: node.name.text,
|
|
405
|
+
methods: node.members.filter((m) => ts.isMethodDeclaration(m)).map((m) => m.name?.getText()),
|
|
406
|
+
line: sourceFile.getLineAndCharacterOfPosition(node.pos).line + 1
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if ((filter === 'all' || filter === 'interfaces') && ts.isInterfaceDeclaration(node)) {
|
|
410
|
+
symbols.push({
|
|
411
|
+
type: 'interface',
|
|
412
|
+
name: node.name.text,
|
|
413
|
+
properties: node.members.map((m) => m.name?.getText()).filter(Boolean),
|
|
414
|
+
line: sourceFile.getLineAndCharacterOfPosition(node.pos).line + 1
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
ts.forEachChild(node, visit);
|
|
418
|
+
}
|
|
419
|
+
visit(sourceFile);
|
|
420
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Found ${symbols.length} symbols in ${args.path}[/TOOL_STATUS]\n`);
|
|
421
|
+
return JSON.stringify({ symbols, count: symbols.length });
|
|
422
|
+
}
|
|
423
|
+
catch (e) {
|
|
424
|
+
callbacks.onChunk(`\n[TOOL_OUTPUT]Error: ${e.message}[/TOOL_OUTPUT]\n`);
|
|
425
|
+
return JSON.stringify({ error: e.message });
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
async autoLint(args, callbacks) {
|
|
429
|
+
try {
|
|
430
|
+
const util = await import('util');
|
|
431
|
+
const execAsync = util.promisify(exec);
|
|
432
|
+
const targetPath = path.resolve(process.cwd(), args.path);
|
|
433
|
+
const errors = [];
|
|
434
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Linting ${args.path}...[/TOOL_STATUS]\n`);
|
|
435
|
+
// TypeScript check
|
|
436
|
+
try {
|
|
437
|
+
await execAsync(`npx tsc --noEmit ${targetPath}`, { cwd: process.cwd() });
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
if (e.stderr)
|
|
441
|
+
errors.push(`[TypeScript]\n${e.stderr.slice(0, 1000)}`);
|
|
442
|
+
}
|
|
443
|
+
// ESLint check
|
|
444
|
+
const eslintCmd = args.fix ? `npx eslint ${targetPath} --fix` : `npx eslint ${targetPath}`;
|
|
445
|
+
try {
|
|
446
|
+
await execAsync(eslintCmd, { cwd: process.cwd() });
|
|
447
|
+
}
|
|
448
|
+
catch (e) {
|
|
449
|
+
if (e.stdout)
|
|
450
|
+
errors.push(`[ESLint]\n${e.stdout.slice(0, 1000)}`);
|
|
451
|
+
}
|
|
452
|
+
if (errors.length === 0) {
|
|
453
|
+
callbacks.onChunk(`\n[TOOL_STATUS][ok] No lint errors[/TOOL_STATUS]\n`);
|
|
454
|
+
return JSON.stringify({ success: true, errors: [] });
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
const summary = errors.join('\n\n');
|
|
458
|
+
callbacks.onChunk(`\n[TOOL_OUTPUT]${summary.slice(0, 500)}[/TOOL_OUTPUT]\n`);
|
|
459
|
+
return JSON.stringify({ success: false, errors: summary });
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
return JSON.stringify({ error: e.message });
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
async verifyChange(args, callbacks) {
|
|
467
|
+
try {
|
|
468
|
+
const util = await import('util');
|
|
469
|
+
const execAsync = util.promisify(exec);
|
|
470
|
+
const fileName = path.basename(args.changedFile, path.extname(args.changedFile));
|
|
471
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Searching for tests for ${fileName}...[/TOOL_STATUS]\n`);
|
|
472
|
+
// Find test files
|
|
473
|
+
const patterns = args.testPattern
|
|
474
|
+
? [args.testPattern]
|
|
475
|
+
: [`${fileName}.test.*`, `${fileName}.spec.*`, `__tests__/${fileName}*`];
|
|
476
|
+
const foundTests = [];
|
|
477
|
+
for (const pattern of patterns) {
|
|
478
|
+
try {
|
|
479
|
+
const { stdout } = await execAsync(`find . -name "${pattern}" -not -path "*/node_modules/*"`);
|
|
480
|
+
foundTests.push(...stdout.trim().split('\n').filter(f => f.length > 0));
|
|
481
|
+
}
|
|
482
|
+
catch (e) { /* Pattern not found */ }
|
|
483
|
+
}
|
|
484
|
+
if (foundTests.length === 0) {
|
|
485
|
+
callbacks.onChunk(`\n[TOOL_STATUS]No tests found for ${fileName}[/TOOL_STATUS]\n`);
|
|
486
|
+
return JSON.stringify({ testsFound: 0, message: 'No related tests' });
|
|
487
|
+
}
|
|
488
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Found ${foundTests.length} test(s), running...[/TOOL_STATUS]\n`);
|
|
489
|
+
// Execute tests
|
|
490
|
+
const testResults = [];
|
|
491
|
+
for (const testFile of foundTests) {
|
|
492
|
+
try {
|
|
493
|
+
const { stdout } = await execAsync(`npm test -- ${testFile}`, {
|
|
494
|
+
cwd: process.cwd(),
|
|
495
|
+
timeout: 30000
|
|
496
|
+
});
|
|
497
|
+
testResults.push({ file: testFile, status: 'PASS', output: stdout });
|
|
498
|
+
callbacks.onChunk(`\n[TOOL_STATUS][ok] ${testFile} passed[/TOOL_STATUS]\n`);
|
|
499
|
+
}
|
|
500
|
+
catch (e) {
|
|
501
|
+
testResults.push({ file: testFile, status: 'FAIL', output: e.stderr || e.stdout });
|
|
502
|
+
callbacks.onChunk(`\n[TOOL_STATUS][x] ${testFile} failed[/TOOL_STATUS]\n`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const passed = testResults.filter(r => r.status === 'PASS').length;
|
|
506
|
+
const failed = testResults.filter(r => r.status === 'FAIL').length;
|
|
507
|
+
return JSON.stringify({
|
|
508
|
+
testsFound: foundTests.length,
|
|
509
|
+
passed,
|
|
510
|
+
failed,
|
|
511
|
+
results: testResults
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
return JSON.stringify({ error: e.message });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
// Google-specific agent
|
|
520
|
+
async function runGoogleAgent(userMessage, history, googleKey, callbacks) {
|
|
521
|
+
await runAgentLoop({
|
|
522
|
+
userMessage,
|
|
523
|
+
history,
|
|
524
|
+
googleKey,
|
|
525
|
+
activePlan,
|
|
526
|
+
projectContextCache,
|
|
527
|
+
sessionManager,
|
|
528
|
+
decisionLogger,
|
|
529
|
+
criticAgent
|
|
530
|
+
}, callbacks);
|
|
531
|
+
return; /*
|
|
532
|
+
|
|
533
|
+
const MAX_LOOPS = 15;
|
|
534
|
+
let loopCount = 0;
|
|
535
|
+
let taskComplete = false;
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
const client = new GoogleGenerativeAI(googleKey);
|
|
539
|
+
const model = client.getGenerativeModel({
|
|
540
|
+
model: 'gemini-2.0-flash',
|
|
541
|
+
tools: [{ functionDeclarations: googleToolDefinitions as any }]
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
let planContext = "";
|
|
545
|
+
if (activePlan.length > 0) {
|
|
546
|
+
planContext = `\n**AKTUELLER PLAN**:\n${activePlan.map(t => `- [${t.status === 'completed' ? 'x' : t.status === 'in_progress' ? '/' : ' '}] ${t.title} (${t.id})`).join('\n')}\n`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const contents: any[] = history.map(msg => ({
|
|
550
|
+
role: msg.role === 'user' ? 'user' : 'model',
|
|
551
|
+
parts: [{ text: msg.content }]
|
|
552
|
+
})).concat([{
|
|
553
|
+
role: 'user',
|
|
554
|
+
parts: [{ text: userMessage + planContext }]
|
|
555
|
+
}]);
|
|
556
|
+
|
|
557
|
+
const systemInstruction = ELITE_SYSTEM_PROMPT
|
|
558
|
+
.replace('{{PROJECT_CONTEXT}}', projectContextCache)
|
|
559
|
+
.replace('{{TASK_STATE}}', TaskState.getContextString());
|
|
560
|
+
|
|
561
|
+
// Track tools used in this loop for critic validation
|
|
562
|
+
const toolsUsedInLoop: string[] = [];
|
|
563
|
+
|
|
564
|
+
// Recursive Agentic Loop (n0 Pattern) - wrapped in try-catch
|
|
565
|
+
try {
|
|
566
|
+
while (loopCount < MAX_LOOPS && !taskComplete) {
|
|
567
|
+
loopCount++;
|
|
568
|
+
|
|
569
|
+
// Validate contents has at least one message
|
|
570
|
+
if (contents.length === 0) {
|
|
571
|
+
onChunk('\n[done]\n');
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Validate each content has non-empty parts
|
|
576
|
+
const validContents = contents.filter(c => {
|
|
577
|
+
if (!c.parts || c.parts.length === 0) return false;
|
|
578
|
+
if (c.parts[0]?.text === '' && !c.parts[0]?.functionResponse) return false;
|
|
579
|
+
return true;
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
if (validContents.length === 0) {
|
|
583
|
+
onChunk('\n[done]\n');
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let response;
|
|
588
|
+
try {
|
|
589
|
+
response = await model.generateContent({
|
|
590
|
+
contents: validContents,
|
|
591
|
+
systemInstruction: systemInstruction
|
|
592
|
+
});
|
|
593
|
+
} catch (e: any) {
|
|
594
|
+
// Handle empty model output or any API errors
|
|
595
|
+
if (e.message && (e.message.includes('empty') || e.message.includes('output text or tool calls'))) {
|
|
596
|
+
onChunk('\n[done]\n');
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
// For other errors, show error and break
|
|
600
|
+
onChunk(`\n[error] ${e.message}\n`);
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const responseText = response.response.text() || '';
|
|
605
|
+
const toolCalls = response.response.functionCalls?.();
|
|
606
|
+
|
|
607
|
+
// Exit 1: Task complete marker - WITH SELF-REFLECTION
|
|
608
|
+
if (responseText.includes('[TASK_COMPLETE]')) {
|
|
609
|
+
const cleanText = responseText.replace('[TASK_COMPLETE]', '').trim();
|
|
610
|
+
|
|
611
|
+
// SELF-REFLECTION: Validate completion
|
|
612
|
+
if (criticAgent && toolsUsedInLoop.length > 0) {
|
|
613
|
+
try {
|
|
614
|
+
const criticResult = await criticAgent.validate(
|
|
615
|
+
lastUserMessage,
|
|
616
|
+
cleanText || 'Task marked complete',
|
|
617
|
+
toolsUsedInLoop
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
// Log the verification
|
|
621
|
+
const session = sessionManager.getCurrentSession();
|
|
622
|
+
if (session) {
|
|
623
|
+
await decisionLogger.logVerification(
|
|
624
|
+
session.sessionId,
|
|
625
|
+
'Task completion',
|
|
626
|
+
'Critic validation',
|
|
627
|
+
criticResult.isValid ? 'Valid' : `Invalid: ${criticResult.reasoning}`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// If critic says invalid, continue working
|
|
632
|
+
if (!criticResult.isValid) {
|
|
633
|
+
onChunk(`\n[reflection] ${criticResult.reasoning}\n`);
|
|
634
|
+
if (criticResult.suggestions && criticResult.suggestions.length > 0) {
|
|
635
|
+
const suggestion = criticResult.suggestions[0];
|
|
636
|
+
contents.push({ role: 'model', parts: [{ text: responseText }] });
|
|
637
|
+
contents.push({
|
|
638
|
+
role: 'user',
|
|
639
|
+
parts: [{ text: `The task is not complete. ${suggestion}` }]
|
|
640
|
+
});
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
} catch (e) {
|
|
645
|
+
// Critic failed, proceed anyway
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
taskComplete = true;
|
|
650
|
+
if (cleanText.length > 0) {
|
|
651
|
+
onChunk(cleanText);
|
|
652
|
+
}
|
|
653
|
+
onChunk('\n[done]\n');
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Exit 2: Explicit user intervention request or plan ready
|
|
658
|
+
if (responseText.includes('[ASK_USER]') || responseText.includes('[PLAN_READY]')) {
|
|
659
|
+
const cleanText = responseText
|
|
660
|
+
.replace('[ASK_USER]', '')
|
|
661
|
+
.replace('[PLAN_READY]', '')
|
|
662
|
+
.trim();
|
|
663
|
+
if (cleanText.length > 0) {
|
|
664
|
+
onChunk(cleanText);
|
|
665
|
+
}
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// No tool calls - output text and end
|
|
670
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
671
|
+
if (responseText.trim().length > 0) {
|
|
672
|
+
onChunk(responseText);
|
|
673
|
+
}
|
|
674
|
+
onChunk('\n[done]\n');
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Execute tools and collect results
|
|
679
|
+
const toolResults: any[] = [];
|
|
680
|
+
|
|
681
|
+
for (const call of toolCalls) {
|
|
682
|
+
const toolName = call.name;
|
|
683
|
+
const toolArgs = call.args;
|
|
684
|
+
|
|
685
|
+
if (toolName in toolImplementations) {
|
|
686
|
+
// Log decision for observability
|
|
687
|
+
const session = sessionManager.getCurrentSession();
|
|
688
|
+
if (session) {
|
|
689
|
+
await decisionLogger.logToolChoice(
|
|
690
|
+
session.sessionId,
|
|
691
|
+
`User: "${lastUserMessage}"`,
|
|
692
|
+
toolName,
|
|
693
|
+
`Chose ${toolName} to accomplish the task`,
|
|
694
|
+
Object.keys(toolImplementations).filter(t => t !== toolName).slice(0, 3)
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Display tool execution with bullet
|
|
699
|
+
let toolDisplay = `⏺ ${toolName}`;
|
|
700
|
+
const args = toolArgs as any;
|
|
701
|
+
if (args.path) toolDisplay += ` (${args.path})`;
|
|
702
|
+
if (args.pattern) toolDisplay += ` ("${args.pattern}")`;
|
|
703
|
+
if (args.command) toolDisplay += ` (${args.command.slice(0, 40)}${args.command.length > 40 ? '...' : ''})`;
|
|
704
|
+
|
|
705
|
+
if (toolName !== 'ToDoWrite' && toolName !== 'internal_thought') {
|
|
706
|
+
onChunk(`\n${toolDisplay}\n`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Execute and capture result
|
|
710
|
+
const result = await toolImplementations[toolName](toolArgs, callbacks);
|
|
711
|
+
|
|
712
|
+
// Track tool usage for critic
|
|
713
|
+
toolsUsedInLoop.push(toolName);
|
|
714
|
+
|
|
715
|
+
// Update session metadata
|
|
716
|
+
if (session) {
|
|
717
|
+
sessionManager.updateMetadata({
|
|
718
|
+
toolsUsed: [...new Set([...(session.metadata.toolsUsed || []), toolName])]
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
toolResults.push({
|
|
723
|
+
functionResponse: {
|
|
724
|
+
name: toolName,
|
|
725
|
+
response: { result: result }
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Feed results back as observations
|
|
732
|
+
contents.push({ role: 'model', parts: [{ text: responseText }] });
|
|
733
|
+
contents.push({ role: 'user', parts: toolResults });
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// After loop ends: Show completion if tools were executed
|
|
737
|
+
if (toolsUsedInLoop.length > 0 && !taskComplete) {
|
|
738
|
+
onChunk('\n[done]\n');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Max loops reached - return control to user
|
|
742
|
+
if (loopCount >= MAX_LOOPS && !taskComplete) {
|
|
743
|
+
onChunk('\n[limit] Stopped after max iterations.\n');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
} catch (e: any) {
|
|
747
|
+
onChunk(`\n[error] ${e.message}\n`);
|
|
748
|
+
}
|
|
749
|
+
*/
|
|
750
|
+
}
|
|
751
|
+
// Vercel SDK tools
|
|
752
|
+
function createVercelTools() {
|
|
753
|
+
return {
|
|
754
|
+
readFile: tool({
|
|
755
|
+
description: 'Read file content',
|
|
756
|
+
parameters: z.object({ path: z.string() }),
|
|
757
|
+
execute: async ({ path: filePath }) => {
|
|
758
|
+
try {
|
|
759
|
+
const p = path.resolve(process.cwd(), filePath);
|
|
760
|
+
const content = await fs.readFile(p, 'utf-8');
|
|
761
|
+
return { content: content.slice(0, 8000) };
|
|
762
|
+
}
|
|
763
|
+
catch (e) {
|
|
764
|
+
return { error: e.message };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}),
|
|
768
|
+
writeFile: tool({
|
|
769
|
+
description: 'Write/Create file',
|
|
770
|
+
parameters: z.object({ path: z.string(), content: z.string() }),
|
|
771
|
+
execute: async ({ path: filePath }) => { return { success: true, path: filePath }; }
|
|
772
|
+
}),
|
|
773
|
+
listFiles: tool({
|
|
774
|
+
description: 'List files in directory',
|
|
775
|
+
parameters: z.object({ path: z.string().optional() }),
|
|
776
|
+
execute: async ({ path: dirPath }) => {
|
|
777
|
+
try {
|
|
778
|
+
const p = path.resolve(process.cwd(), dirPath || '.');
|
|
779
|
+
const files = await fs.readdir(p);
|
|
780
|
+
const clean = files.filter(f => !['node_modules', '.git'].includes(f));
|
|
781
|
+
return { files: clean };
|
|
782
|
+
}
|
|
783
|
+
catch (e) {
|
|
784
|
+
return { error: e.message };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}),
|
|
788
|
+
executeShellCommand: tool({
|
|
789
|
+
description: 'Execute shell command',
|
|
790
|
+
parameters: z.object({ command: z.string() }),
|
|
791
|
+
execute: async ({ command }) => {
|
|
792
|
+
const { stdout, stderr } = await execAsync(command);
|
|
793
|
+
return { output: stdout || stderr };
|
|
794
|
+
}
|
|
795
|
+
}),
|
|
796
|
+
grep: tool({
|
|
797
|
+
description: 'Search for content in files OR filenames. Returns "File:Line:Preview".',
|
|
798
|
+
parameters: z.object({ pattern: z.string(), path: z.string().optional() }),
|
|
799
|
+
execute: async ({ pattern, path: searchPath }) => {
|
|
800
|
+
// Implementation for Vercel SDK (similar logic)
|
|
801
|
+
return { results: 'To be implemented for Vercel SDK fully if needed' };
|
|
802
|
+
}
|
|
803
|
+
}),
|
|
804
|
+
editFile: tool({
|
|
805
|
+
description: 'Apply unified diff patch',
|
|
806
|
+
parameters: z.object({ path: z.string(), unifiedDiff: z.string() }),
|
|
807
|
+
execute: async () => { return { success: true }; }
|
|
808
|
+
}),
|
|
809
|
+
addTask: tool({
|
|
810
|
+
description: 'Add a new task to the todo list',
|
|
811
|
+
parameters: z.object({ description: z.string() }),
|
|
812
|
+
execute: async () => { return { success: true }; }
|
|
813
|
+
}),
|
|
814
|
+
updateTask: tool({
|
|
815
|
+
description: 'Update task status',
|
|
816
|
+
parameters: z.object({
|
|
817
|
+
id: z.string(),
|
|
818
|
+
status: z.enum(['pending', 'in_progress', 'completed', 'failed'])
|
|
819
|
+
}),
|
|
820
|
+
execute: async () => { return { success: true }; }
|
|
821
|
+
}),
|
|
822
|
+
listTasks: tool({
|
|
823
|
+
description: 'List all tasks',
|
|
824
|
+
parameters: z.object({}),
|
|
825
|
+
execute: async () => { return { success: true }; }
|
|
826
|
+
}),
|
|
827
|
+
webFetch: tool({
|
|
828
|
+
description: 'Fetch content from a URL safely.',
|
|
829
|
+
parameters: z.object({ url: z.string() }),
|
|
830
|
+
execute: async () => { return { error: 'Not implemented for Vercel SDK yet' }; }
|
|
831
|
+
}),
|
|
832
|
+
internal_thought: tool({
|
|
833
|
+
description: 'Document internal reasoning',
|
|
834
|
+
parameters: z.object({ analysis: z.string() }),
|
|
835
|
+
execute: async () => { return { acknowledged: true }; }
|
|
836
|
+
})
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
// Initialize session and critic agent
|
|
840
|
+
async function initializeSession(apiKey) {
|
|
841
|
+
// Create session if not exists
|
|
842
|
+
if (!sessionManager.getCurrentSession()) {
|
|
843
|
+
const sessionId = await sessionManager.createSession();
|
|
844
|
+
decisionLogger.clearDecisions();
|
|
845
|
+
await decisionLogger.logStrategy(sessionId, 'Session start', 'New session created', 'Starting fresh agent session');
|
|
846
|
+
}
|
|
847
|
+
// Initialize critic agent if not exists
|
|
848
|
+
// DISABLED: Critic was blocking agent execution
|
|
849
|
+
// if (!criticAgent && apiKey) {
|
|
850
|
+
// criticAgent = new CriticAgent(apiKey);
|
|
851
|
+
// }
|
|
852
|
+
}
|
|
853
|
+
export async function runEliteAgent(userMessage, history, callbacks) {
|
|
854
|
+
const googleKey = config.get('googleApiKey');
|
|
855
|
+
let selectedModel = config.get('selectedModel') || 'gemini-3-flash-preview';
|
|
856
|
+
// Map aliases
|
|
857
|
+
if (selectedModel === 'gemini-3-flash' || selectedModel === 'gemini-3.0-flash') {
|
|
858
|
+
selectedModel = 'gemini-3-flash-preview';
|
|
859
|
+
}
|
|
860
|
+
else if (selectedModel === 'gemini-3.0-pro-preview') {
|
|
861
|
+
selectedModel = 'gemini-3-pro-preview';
|
|
862
|
+
}
|
|
863
|
+
const { onChunk } = callbacks;
|
|
864
|
+
lastUserMessage = userMessage;
|
|
865
|
+
// Initialize session
|
|
866
|
+
await initializeSession(googleKey);
|
|
867
|
+
// Add user message to session
|
|
868
|
+
sessionManager.addMessage({
|
|
869
|
+
role: 'user',
|
|
870
|
+
content: userMessage,
|
|
871
|
+
timestamp: Date.now()
|
|
872
|
+
});
|
|
873
|
+
if (!googleKey) {
|
|
874
|
+
onChunk('\n[error] No Google API key found. Please run setup.\n');
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
// Use the AgentLoop for autonomous execution
|
|
878
|
+
try {
|
|
879
|
+
// Pass the selected model to the agent loop
|
|
880
|
+
// Note: AgentLoop needs to be updated to accept model param or read config
|
|
881
|
+
// For now, we'll patch AgentLoop next, but here we just call it.
|
|
882
|
+
// Wait, AgentLoop currently hardcodes 'gemini-2.0-flash'.
|
|
883
|
+
// We pass the config via context or modify AgentLoop.
|
|
884
|
+
// Let's modify AgentLoop signature or context to include 'model'.
|
|
885
|
+
// Since I can't change AgentLoop signature in this tool call, I will assume I'll update AgentLoop next.
|
|
886
|
+
// I will add 'model' to the context object passed to runAgentLoop.
|
|
887
|
+
// I need to check if runAgentLoop accepts extra context properties.
|
|
888
|
+
// It takes `AgentLoopContext`. I should update that interface first.
|
|
889
|
+
// Actually, runAgentLoop is imported. I should stick to the existing signature for now
|
|
890
|
+
// and assume AgentLoop reads the config or I update AgentLoop to read `config.get('selectedModel')`.
|
|
891
|
+
await runGoogleAgent(userMessage, history, googleKey, callbacks);
|
|
892
|
+
}
|
|
893
|
+
catch (e) {
|
|
894
|
+
onChunk(`\n[error] ${e.message}\n`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
export async function compressContext(history, callbacks) {
|
|
898
|
+
const { onChunk } = callbacks;
|
|
899
|
+
onChunk('\n[SYSTEM] Compressing Context & Archiving Insights...\n');
|
|
900
|
+
const googleKey = config.get('googleApiKey');
|
|
901
|
+
let model;
|
|
902
|
+
try {
|
|
903
|
+
if (googleKey) {
|
|
904
|
+
const google = createGoogleGenerativeAI({ apiKey: googleKey });
|
|
905
|
+
// Use efficient model for compression
|
|
906
|
+
model = google('gemini-3-flash-preview');
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
throw new Error('No API Key for compression');
|
|
910
|
+
}
|
|
911
|
+
const historyText = history.map(m => `${m.role.toUpperCase()}: ${m.content}`).join('\n');
|
|
912
|
+
const prompt = `
|
|
913
|
+
You are a Context Compressor.
|
|
914
|
+
Analyze the following conversation history.
|
|
915
|
+
|
|
916
|
+
1. **Extract Important Insights**: Identify architecture decisions, file paths, user preferences, and key technical learnings.
|
|
917
|
+
2. **Summarize Context**: Create a concise summary of the conversation state so the agent can continue seamlessly.
|
|
918
|
+
|
|
919
|
+
OUTPUT FORMAT (JSON):
|
|
920
|
+
{
|
|
921
|
+
"insights": "Markdown list of insights to save to long-term memory",
|
|
922
|
+
"summary": "Concise summary of the conversation history to serve as new start point"
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
HISTORY:
|
|
926
|
+
${historyText.slice(-20000)}
|
|
927
|
+
`;
|
|
928
|
+
const { generateText } = await import('ai');
|
|
929
|
+
const result = await generateText({
|
|
930
|
+
model: model,
|
|
931
|
+
prompt: prompt
|
|
932
|
+
});
|
|
933
|
+
const text = result.text;
|
|
934
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
935
|
+
if (!jsonMatch)
|
|
936
|
+
return "Context Reset. (Compression failed to parse)";
|
|
937
|
+
const data = JSON.parse(jsonMatch[0]);
|
|
938
|
+
if (data.insights) {
|
|
939
|
+
const memoryPath = path.resolve(process.cwd(), 'memory.md');
|
|
940
|
+
const entry = `\n## Archive [${new Date().toISOString()}]\n${data.insights}\n`;
|
|
941
|
+
await fs.appendFile(memoryPath, entry, 'utf-8');
|
|
942
|
+
onChunk(`\n[SYSTEM] Insights saved to memory.md\n`);
|
|
943
|
+
}
|
|
944
|
+
return `[CONTEXT_SUMMARY]: ${data.summary}`;
|
|
945
|
+
}
|
|
946
|
+
catch (e) {
|
|
947
|
+
onChunk(`\n[SYSTEM_ERROR] Compression failed: ${e.message}\n`);
|
|
948
|
+
return "Context Reset (Error during compression)";
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
// ===== SESSION MANAGEMENT EXPORTS =====
|
|
952
|
+
export async function saveCurrentSession() {
|
|
953
|
+
await sessionManager.saveSession();
|
|
954
|
+
}
|
|
955
|
+
export async function resumeLastSession() {
|
|
956
|
+
const latestSession = await sessionManager.getLatestSession();
|
|
957
|
+
if (!latestSession)
|
|
958
|
+
return null;
|
|
959
|
+
await sessionManager.resumeSession(latestSession.sessionId);
|
|
960
|
+
return latestSession.messages;
|
|
961
|
+
}
|
|
962
|
+
export async function listAllSessions() {
|
|
963
|
+
return await sessionManager.listSessions();
|
|
964
|
+
}
|
|
965
|
+
export async function getSessionReport() {
|
|
966
|
+
const session = sessionManager.getCurrentSession();
|
|
967
|
+
if (!session)
|
|
968
|
+
return 'No active session';
|
|
969
|
+
const report = await decisionLogger.generateReport(session.sessionId);
|
|
970
|
+
return report;
|
|
971
|
+
}
|
|
972
|
+
export async function cleanupAgent() {
|
|
973
|
+
await sessionManager.cleanup();
|
|
974
|
+
}
|
|
975
|
+
export function getDecisionLog() {
|
|
976
|
+
return decisionLogger.getRecentDecisions(20);
|
|
977
|
+
}
|