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.
Files changed (44) hide show
  1. package/README.md +184 -0
  2. package/dist/agent.js +977 -0
  3. package/dist/art.js +33 -0
  4. package/dist/components/App.js +71 -0
  5. package/dist/components/Chat.js +509 -0
  6. package/dist/components/HITLConfirmation.js +89 -0
  7. package/dist/components/ModelSelector.js +100 -0
  8. package/dist/components/SetupScreen.js +43 -0
  9. package/dist/config/constants.js +58 -0
  10. package/dist/config/prompts.js +98 -0
  11. package/dist/config/store.js +5 -0
  12. package/dist/core/AgentLoop.js +159 -0
  13. package/dist/hooks/useAutocomplete.js +52 -0
  14. package/dist/hooks/useKeyboard.js +73 -0
  15. package/dist/index.js +31 -0
  16. package/dist/mcp.js +95 -0
  17. package/dist/memory/MemoryManager.js +228 -0
  18. package/dist/providers/index.js +85 -0
  19. package/dist/providers/registry.js +121 -0
  20. package/dist/providers/types.js +5 -0
  21. package/dist/theme.js +45 -0
  22. package/dist/tools/FileTools.js +88 -0
  23. package/dist/tools/MemoryTools.js +53 -0
  24. package/dist/tools/SearchTools.js +45 -0
  25. package/dist/tools/ShellTools.js +40 -0
  26. package/dist/tools/TaskTools.js +52 -0
  27. package/dist/tools/ToolDefinitions.js +102 -0
  28. package/dist/tools/ToolRegistry.js +30 -0
  29. package/dist/types/Agent.js +6 -0
  30. package/dist/types/ink.js +1 -0
  31. package/dist/types/message.js +1 -0
  32. package/dist/types/ui.js +1 -0
  33. package/dist/utils/CriticAgent.js +88 -0
  34. package/dist/utils/DecisionLogger.js +156 -0
  35. package/dist/utils/MessageHistory.js +55 -0
  36. package/dist/utils/PermissionManager.js +253 -0
  37. package/dist/utils/SessionManager.js +180 -0
  38. package/dist/utils/TaskState.js +108 -0
  39. package/dist/utils/debug.js +35 -0
  40. package/dist/utils/execAsync.js +3 -0
  41. package/dist/utils/retry.js +50 -0
  42. package/dist/utils/tokenCounter.js +24 -0
  43. package/dist/utils/uiFormatter.js +106 -0
  44. 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
+ }