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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createPatch } from 'diff';
|
|
4
|
+
import { PermissionManager } from '../utils/PermissionManager.js';
|
|
5
|
+
/**
|
|
6
|
+
* File operation tools
|
|
7
|
+
*/
|
|
8
|
+
export const FileTools = {
|
|
9
|
+
async readFile(args, callbacks) {
|
|
10
|
+
try {
|
|
11
|
+
// Smart path resolution
|
|
12
|
+
let p = args.path;
|
|
13
|
+
if (!path.isAbsolute(p)) {
|
|
14
|
+
// Remove project root prefix if present in relative path to avoid duplication
|
|
15
|
+
const cwdName = path.basename(process.cwd());
|
|
16
|
+
if (p.startsWith(cwdName + '/')) {
|
|
17
|
+
p = p.substring(cwdName.length + 1);
|
|
18
|
+
}
|
|
19
|
+
p = path.resolve(process.cwd(), p);
|
|
20
|
+
}
|
|
21
|
+
const content = await fs.readFile(p, 'utf-8');
|
|
22
|
+
const lines = content.split('\n');
|
|
23
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Read ${lines.length} lines from ${path.basename(p)}[/TOOL_STATUS]\n`);
|
|
24
|
+
return JSON.stringify({ content: content.slice(0, 8000) });
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Error: ${e.message}[/TOOL_STATUS]\n`);
|
|
28
|
+
return JSON.stringify({ error: `Failed to read file: ${e.message}` });
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
async writeFile(args, callbacks) {
|
|
32
|
+
// Normalize path - remove leading slashes to make it relative
|
|
33
|
+
let targetPath = args.path;
|
|
34
|
+
// If path starts with /, treat it as relative from cwd
|
|
35
|
+
if (targetPath.startsWith('/')) {
|
|
36
|
+
targetPath = targetPath.substring(1);
|
|
37
|
+
}
|
|
38
|
+
const p = path.resolve(process.cwd(), targetPath);
|
|
39
|
+
let diff = '';
|
|
40
|
+
try {
|
|
41
|
+
const existingContent = await fs.readFile(p, 'utf-8');
|
|
42
|
+
diff = createPatch(targetPath, existingContent, args.content);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
diff = createPatch(targetPath, '', args.content);
|
|
46
|
+
}
|
|
47
|
+
// Permission Check
|
|
48
|
+
const allowed = await PermissionManager.getInstance().validate('FILE_WRITE', {
|
|
49
|
+
path: targetPath,
|
|
50
|
+
diff: diff
|
|
51
|
+
}, callbacks);
|
|
52
|
+
if (!allowed) {
|
|
53
|
+
return JSON.stringify({ error: 'User denied file write.' });
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
// Create parent directory if it doesn't exist
|
|
57
|
+
const dir = path.dirname(p);
|
|
58
|
+
await fs.mkdir(dir, { recursive: true });
|
|
59
|
+
await fs.writeFile(p, args.content, 'utf-8');
|
|
60
|
+
callbacks.onChunk(`\n[TOOL_STATUS][ok] File written: ${targetPath}[/TOOL_STATUS]\n`);
|
|
61
|
+
return JSON.stringify({ success: true, path: targetPath });
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
callbacks.onChunk(`\n[TOOL_OUTPUT]Error: ${e.message}[/TOOL_OUTPUT]\n`);
|
|
65
|
+
return JSON.stringify({ error: e.message });
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
async listFiles(args, callbacks) {
|
|
69
|
+
try {
|
|
70
|
+
let p = args.path || '.';
|
|
71
|
+
if (!path.isAbsolute(p)) {
|
|
72
|
+
const cwdName = path.basename(process.cwd());
|
|
73
|
+
if (p.startsWith(cwdName + '/')) {
|
|
74
|
+
p = p.substring(cwdName.length + 1);
|
|
75
|
+
}
|
|
76
|
+
p = path.resolve(process.cwd(), p);
|
|
77
|
+
}
|
|
78
|
+
const files = await fs.readdir(p);
|
|
79
|
+
const clean = files.filter(f => !['node_modules', '.git', '.DS_Store'].includes(f));
|
|
80
|
+
// Return concise list directly
|
|
81
|
+
return JSON.stringify({ files: clean });
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Error: ${e.message}[/TOOL_STATUS]\n`);
|
|
85
|
+
return JSON.stringify({ error: `Failed to list files: ${e.message}` });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { MemoryManager } from '../memory/MemoryManager.js';
|
|
2
|
+
/**
|
|
3
|
+
* Memory Tools - Persistent working memory operations
|
|
4
|
+
*/
|
|
5
|
+
export const MemoryTools = {
|
|
6
|
+
/**
|
|
7
|
+
* Store learned facts in context.md
|
|
8
|
+
*/
|
|
9
|
+
async updateMemory(args, callbacks) {
|
|
10
|
+
try {
|
|
11
|
+
const memory = MemoryManager.getInstance();
|
|
12
|
+
await memory.updateContext(args.category, args.content);
|
|
13
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Saved to memory: ${args.category}[/TOOL_STATUS]\n`);
|
|
14
|
+
return JSON.stringify({ success: true, category: args.category });
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
return JSON.stringify({ error: e.message });
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
/**
|
|
21
|
+
* Update task status in plan.md
|
|
22
|
+
*/
|
|
23
|
+
async syncPlan(args, callbacks) {
|
|
24
|
+
try {
|
|
25
|
+
const memory = MemoryManager.getInstance();
|
|
26
|
+
await memory.updateTaskStatus(args.taskId, args.status);
|
|
27
|
+
const statusText = args.status === 'complete' ? 'completed' : args.status === 'in_progress' ? 'started' : 'pending';
|
|
28
|
+
callbacks.onChunk(`\n[TOOL_STATUS]Plan updated: ${args.taskId} → ${statusText}[/TOOL_STATUS]\n`);
|
|
29
|
+
return JSON.stringify({ success: true, taskId: args.taskId, status: args.status });
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return JSON.stringify({ error: e.message });
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Retrieve context from memory
|
|
37
|
+
*/
|
|
38
|
+
async recallContext(args, callbacks) {
|
|
39
|
+
try {
|
|
40
|
+
const memory = MemoryManager.getInstance();
|
|
41
|
+
const context = await memory.loadContext();
|
|
42
|
+
if (args.query) {
|
|
43
|
+
// Simple filtering by query
|
|
44
|
+
const lines = context.split('\n').filter(line => line.toLowerCase().includes(args.query.toLowerCase()));
|
|
45
|
+
return JSON.stringify({ context: lines.join('\n') });
|
|
46
|
+
}
|
|
47
|
+
return JSON.stringify({ context });
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return JSON.stringify({ error: e.message });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execAsync } from '../utils/execAsync.js';
|
|
2
|
+
import { PermissionManager } from '../utils/PermissionManager.js';
|
|
3
|
+
export const SearchTools = {
|
|
4
|
+
/**
|
|
5
|
+
* Search for a pattern in files
|
|
6
|
+
*/
|
|
7
|
+
grep: async (args, callbacks) => {
|
|
8
|
+
const pattern = args.pattern || args.query;
|
|
9
|
+
const path = args.path || '.';
|
|
10
|
+
if (!pattern)
|
|
11
|
+
return "Error: 'pattern' argument is required";
|
|
12
|
+
// Permission check
|
|
13
|
+
const allowed = await PermissionManager.getInstance().validate('SHELL', { command: `grep ${pattern} ${path}` }, callbacks);
|
|
14
|
+
if (!allowed)
|
|
15
|
+
return "Error: Permission denied for grep";
|
|
16
|
+
callbacks.onChunk(`\n[Running grep search for "${pattern}" in "${path}"]\n`);
|
|
17
|
+
try {
|
|
18
|
+
// Use git grep if available as it respects .gitignore, otherwise fallback to grep
|
|
19
|
+
// But for simplicity/robustness, standard grep -r is good.
|
|
20
|
+
// Let's try git grep first if in a git repo, else standard grep.
|
|
21
|
+
// Actually, let's just use grep -rn for now to be safe and standard.
|
|
22
|
+
// -r: recursive, -n: line numbers, -I: ignore binary
|
|
23
|
+
// exclude .git, node_modules, dist
|
|
24
|
+
const exclude = "--exclude-dir={.git,node_modules,dist,.next,build,.camo}";
|
|
25
|
+
// Use execAsync helper
|
|
26
|
+
const { stdout, stderr } = await execAsync(`grep -rnI ${exclude} "${pattern}" "${path}"`);
|
|
27
|
+
if (!stdout && !stderr)
|
|
28
|
+
return "No matches found.";
|
|
29
|
+
if (stderr && !stdout)
|
|
30
|
+
return `Error: ${stderr}`;
|
|
31
|
+
// Limit output to prevent overflow
|
|
32
|
+
const lines = stdout.split('\n');
|
|
33
|
+
if (lines.length > 50) {
|
|
34
|
+
return lines.slice(0, 50).join('\n') + `\n... and ${lines.length - 50} more matches.`;
|
|
35
|
+
}
|
|
36
|
+
return stdout;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
// grep returns exit code 1 if no matches found, which execAsync throws as error
|
|
40
|
+
if (error.code === 1)
|
|
41
|
+
return "No matches found.";
|
|
42
|
+
return `Error executing grep: ${error.message}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { PermissionManager } from '../utils/PermissionManager.js';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
const SHELL_TIMEOUT_MS = 30000;
|
|
6
|
+
/**
|
|
7
|
+
* Shell command execution tool
|
|
8
|
+
*/
|
|
9
|
+
export const ShellTools = {
|
|
10
|
+
async executeShellCommand(args, callbacks) {
|
|
11
|
+
// Permission Check
|
|
12
|
+
const allowed = await PermissionManager.getInstance().validate('SHELL', {
|
|
13
|
+
command: args.command
|
|
14
|
+
}, callbacks);
|
|
15
|
+
if (!allowed) {
|
|
16
|
+
return JSON.stringify({ error: 'User denied command execution.' });
|
|
17
|
+
}
|
|
18
|
+
// Execute with output display
|
|
19
|
+
try {
|
|
20
|
+
const { stdout, stderr } = await execAsync(args.command, {
|
|
21
|
+
cwd: process.cwd(),
|
|
22
|
+
timeout: SHELL_TIMEOUT_MS
|
|
23
|
+
});
|
|
24
|
+
const output = stdout + (stderr ? '\n' + stderr : '');
|
|
25
|
+
const preview = output.trim().slice(0, 200);
|
|
26
|
+
if (preview.length > 0) {
|
|
27
|
+
callbacks.onChunk(`\n[TOOL_STATUS]${preview}${output.length > 200 ? '...' : ''}[/TOOL_STATUS]\n`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
callbacks.onChunk(`\n[TOOL_STATUS][ok] Command executed[/TOOL_STATUS]\n`);
|
|
31
|
+
}
|
|
32
|
+
return JSON.stringify({ exitCode: 0, output: output.slice(0, 2000) });
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
const errorMsg = e.stderr || e.message;
|
|
36
|
+
callbacks.onChunk(`\n[TOOL_OUTPUT]Error: ${errorMsg.slice(0, 200)}[/TOOL_OUTPUT]\n`);
|
|
37
|
+
return JSON.stringify({ exitCode: e.code || 1, error: errorMsg });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Simple in-memory task store for the session
|
|
2
|
+
// In a real app, this might persist to .camo/tasks.json
|
|
3
|
+
let sessionTasks = [];
|
|
4
|
+
export const TaskTools = {
|
|
5
|
+
/**
|
|
6
|
+
* Add a new task
|
|
7
|
+
*/
|
|
8
|
+
addTask: async (args, callbacks) => {
|
|
9
|
+
const description = args.description || args.task;
|
|
10
|
+
if (!description)
|
|
11
|
+
return "Error: 'description' is required";
|
|
12
|
+
const id = (sessionTasks.length + 1).toString();
|
|
13
|
+
const newTask = {
|
|
14
|
+
id,
|
|
15
|
+
description,
|
|
16
|
+
status: 'pending'
|
|
17
|
+
};
|
|
18
|
+
sessionTasks.push(newTask);
|
|
19
|
+
callbacks.onChunk(`\n[Task Added] #${id}: ${description}\n`);
|
|
20
|
+
return JSON.stringify({ id, status: 'pending' });
|
|
21
|
+
},
|
|
22
|
+
/**
|
|
23
|
+
* Update a task status
|
|
24
|
+
*/
|
|
25
|
+
updateTask: async (args, callbacks) => {
|
|
26
|
+
const id = args.id;
|
|
27
|
+
const status = args.status;
|
|
28
|
+
if (!id || !status)
|
|
29
|
+
return "Error: 'id' and 'status' are required";
|
|
30
|
+
if (!['pending', 'in_progress', 'completed', 'failed'].includes(status)) {
|
|
31
|
+
return "Error: Invalid status. Use pending, in_progress, completed, or failed";
|
|
32
|
+
}
|
|
33
|
+
const task = sessionTasks.find(t => t.id === id);
|
|
34
|
+
if (!task)
|
|
35
|
+
return `Error: Task #${id} not found`;
|
|
36
|
+
task.status = status;
|
|
37
|
+
callbacks.onChunk(`\n[Task Updated] #${id} -> ${status}\n`);
|
|
38
|
+
return JSON.stringify({ id, status });
|
|
39
|
+
},
|
|
40
|
+
/**
|
|
41
|
+
* List all tasks
|
|
42
|
+
*/
|
|
43
|
+
listTasks: async (args, callbacks) => {
|
|
44
|
+
if (sessionTasks.length === 0)
|
|
45
|
+
return "No active tasks.";
|
|
46
|
+
const list = sessionTasks.map(t => {
|
|
47
|
+
const symbol = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[/]' : t.status === 'failed' ? '[!]' : '[ ]';
|
|
48
|
+
return `${symbol} #${t.id} ${t.description}`;
|
|
49
|
+
}).join('\n');
|
|
50
|
+
return list;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Tool Definitions for Vercel AI SDK (Zod)
|
|
4
|
+
*/
|
|
5
|
+
export const tools = {
|
|
6
|
+
writeFile: {
|
|
7
|
+
description: 'Create or overwrite a file with new content. Returns the path of the created file.',
|
|
8
|
+
parameters: z.object({
|
|
9
|
+
path: z.string().describe('The absolute or relative path of the file to write'),
|
|
10
|
+
content: z.string().describe('The full text content to write to the file')
|
|
11
|
+
})
|
|
12
|
+
},
|
|
13
|
+
readFile: {
|
|
14
|
+
description: 'Read the contents of a file. Returns the file content or an error if not found.',
|
|
15
|
+
parameters: z.object({
|
|
16
|
+
path: z.string().describe('The absolute or relative path of the file to read')
|
|
17
|
+
})
|
|
18
|
+
},
|
|
19
|
+
editFile: {
|
|
20
|
+
description: 'Apply a unified diff to a file. Used for modifying existing files surgically. Returns success status.',
|
|
21
|
+
parameters: z.object({
|
|
22
|
+
path: z.string().describe('The file to edit'),
|
|
23
|
+
unifiedDiff: z.string().describe('The unified diff content to apply')
|
|
24
|
+
})
|
|
25
|
+
},
|
|
26
|
+
executeShellCommand: {
|
|
27
|
+
description: 'Execute a shell command. Returns stdout/stderr. Use for running tests, installing deps, etc.',
|
|
28
|
+
parameters: z.object({
|
|
29
|
+
command: z.string().describe('The shell command to execute')
|
|
30
|
+
})
|
|
31
|
+
},
|
|
32
|
+
listFiles: {
|
|
33
|
+
description: 'List files in a directory recursively. Returns a list of file paths. Limits depth and breadth.',
|
|
34
|
+
parameters: z.object({
|
|
35
|
+
path: z.string().describe('The directory path to list (default: .)')
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
grep: {
|
|
39
|
+
description: 'Search for a string pattern in files using grep. Returns matching lines and filenames.',
|
|
40
|
+
parameters: z.object({
|
|
41
|
+
pattern: z.string().describe('regex or string pattern to search for'),
|
|
42
|
+
path: z.string().describe('directory/file to search (default: .)')
|
|
43
|
+
})
|
|
44
|
+
},
|
|
45
|
+
webFetch: {
|
|
46
|
+
description: 'Fetch content from a URL. Useful for reading docs or external resources. Returns text content.',
|
|
47
|
+
parameters: z.object({
|
|
48
|
+
url: z.string().describe('The URL to fetch')
|
|
49
|
+
})
|
|
50
|
+
},
|
|
51
|
+
internal_thought: {
|
|
52
|
+
description: 'Log a private thought or plan. Not visible to user. Use for complex reasoning.',
|
|
53
|
+
parameters: z.object({
|
|
54
|
+
thought: z.string().describe('The thought content')
|
|
55
|
+
})
|
|
56
|
+
},
|
|
57
|
+
ToDoWrite: {
|
|
58
|
+
description: 'Manage the task plan. Add, update, or complete tasks.',
|
|
59
|
+
parameters: z.object({
|
|
60
|
+
action: z.enum(['add', 'update', 'complete', 'list']).describe('add | update | complete | list'),
|
|
61
|
+
task: z.string().optional().describe('Task description or ID'),
|
|
62
|
+
status: z.enum(['pending', 'in_progress', 'completed', 'failed']).optional().describe('For update: pending | in_progress | completed | failed')
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
readCodeContext: {
|
|
66
|
+
description: 'Analyze code complexity and structure using AST. Returns functions, classes, and imports.',
|
|
67
|
+
parameters: z.object({
|
|
68
|
+
path: z.string().describe('File to analyze'),
|
|
69
|
+
filter: z.string().optional().describe('Filter: "all", "functions", "classes", "interfaces", "types" (default: all)')
|
|
70
|
+
})
|
|
71
|
+
},
|
|
72
|
+
verifyChange: {
|
|
73
|
+
description: 'Search for tests related to a modified file and execute them. Returns test results.',
|
|
74
|
+
parameters: z.object({
|
|
75
|
+
changedFile: z.string().describe('The file that was changed'),
|
|
76
|
+
testPattern: z.string().optional().describe('Custom test file pattern (optional)')
|
|
77
|
+
})
|
|
78
|
+
},
|
|
79
|
+
// Memory tools
|
|
80
|
+
updateMemory: {
|
|
81
|
+
description: 'Store learned facts about the project in persistent memory. Use when you discover important information.',
|
|
82
|
+
parameters: z.object({
|
|
83
|
+
category: z.string().describe('Category for the fact (e.g., "Architecture", "Key Files", "Decisions")'),
|
|
84
|
+
content: z.string().describe('The fact to store (e.g., "Database uses PostgreSQL")')
|
|
85
|
+
})
|
|
86
|
+
},
|
|
87
|
+
syncPlan: {
|
|
88
|
+
description: 'Update task status in the persistent plan. Call after completing or starting a sub-task.',
|
|
89
|
+
parameters: z.object({
|
|
90
|
+
taskId: z.string().describe('The task ID to update'),
|
|
91
|
+
status: z.enum(['pending', 'in_progress', 'complete']).describe('New status for the task')
|
|
92
|
+
})
|
|
93
|
+
},
|
|
94
|
+
recallContext: {
|
|
95
|
+
description: 'Retrieve stored project knowledge from memory. Use to avoid redundant searches.',
|
|
96
|
+
parameters: z.object({
|
|
97
|
+
query: z.string().optional().describe('Optional filter to search for specific knowledge')
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
// Legacy Export for Google Native (optional, but keeping for safety if needed, mapped from Zod if complex, but manual here is fine for now if we fully switch)
|
|
102
|
+
// Actually AgentLoop refactor will remove usage of googleToolDefinitions, so we can deprecate it.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { FileTools } from './FileTools.js';
|
|
2
|
+
import { ShellTools } from './ShellTools.js';
|
|
3
|
+
import { SearchTools } from './SearchTools.js';
|
|
4
|
+
import { TaskTools } from './TaskTools.js';
|
|
5
|
+
import { MemoryTools } from './MemoryTools.js';
|
|
6
|
+
export const ToolRegistry = {
|
|
7
|
+
// File operations
|
|
8
|
+
readFile: FileTools.readFile,
|
|
9
|
+
writeFile: FileTools.writeFile,
|
|
10
|
+
listFiles: FileTools.listFiles,
|
|
11
|
+
// Shell operations
|
|
12
|
+
executeShellCommand: ShellTools.executeShellCommand,
|
|
13
|
+
// Search operations
|
|
14
|
+
grep: SearchTools.grep,
|
|
15
|
+
// Task operations
|
|
16
|
+
addTask: TaskTools.addTask,
|
|
17
|
+
updateTask: TaskTools.updateTask,
|
|
18
|
+
listTasks: TaskTools.listTasks,
|
|
19
|
+
// Memory operations
|
|
20
|
+
updateMemory: MemoryTools.updateMemory,
|
|
21
|
+
syncPlan: MemoryTools.syncPlan,
|
|
22
|
+
recallContext: MemoryTools.recallContext,
|
|
23
|
+
// Note: Other tools (webFetch, etc.) will be migrated later
|
|
24
|
+
};
|
|
25
|
+
export function getToolImplementation(name) {
|
|
26
|
+
return ToolRegistry[name];
|
|
27
|
+
}
|
|
28
|
+
export function getAllToolNames() {
|
|
29
|
+
return Object.keys(ToolRegistry);
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/ui.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
|
+
export class CriticAgent {
|
|
3
|
+
constructor(apiKey) {
|
|
4
|
+
this.client = new GoogleGenerativeAI(apiKey);
|
|
5
|
+
this.model = this.client.getGenerativeModel({
|
|
6
|
+
model: 'gemini-2.0-flash'
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
async validate(userRequest, agentResponse, toolsUsed) {
|
|
10
|
+
const prompt = `You are a critic reviewing an AI agent's work.
|
|
11
|
+
|
|
12
|
+
**USER REQUEST:**
|
|
13
|
+
${userRequest}
|
|
14
|
+
|
|
15
|
+
**AGENT RESPONSE:**
|
|
16
|
+
${agentResponse}
|
|
17
|
+
|
|
18
|
+
**TOOLS USED:**
|
|
19
|
+
${toolsUsed.join(', ')}
|
|
20
|
+
|
|
21
|
+
**YOUR TASK:**
|
|
22
|
+
Critically evaluate if the agent's response fulfills the user's request.
|
|
23
|
+
|
|
24
|
+
Check for:
|
|
25
|
+
1. Did the agent actually solve the problem?
|
|
26
|
+
2. Are there logical errors?
|
|
27
|
+
3. Did the agent use the right tools?
|
|
28
|
+
4. Is the solution complete or half-baked?
|
|
29
|
+
5. Are there any security issues?
|
|
30
|
+
|
|
31
|
+
**OUTPUT FORMAT:**
|
|
32
|
+
VALID: yes/no
|
|
33
|
+
REASONING: [explain your verdict in 1-2 sentences]
|
|
34
|
+
SUGGESTIONS: [if invalid, what should be fixed?]`;
|
|
35
|
+
try {
|
|
36
|
+
const response = await this.model.generateContent(prompt);
|
|
37
|
+
const text = response.response.text();
|
|
38
|
+
// Parse response
|
|
39
|
+
const validMatch = text.match(/VALID:\s*(yes|no)/i);
|
|
40
|
+
const reasoningMatch = text.match(/REASONING:\s*(.+?)(?=SUGGESTIONS:|$)/s);
|
|
41
|
+
const suggestionsMatch = text.match(/SUGGESTIONS:\s*(.+?)$/s);
|
|
42
|
+
const isValid = validMatch?.[1].toLowerCase() === 'yes';
|
|
43
|
+
const reasoning = reasoningMatch?.[1].trim() || 'No reasoning provided';
|
|
44
|
+
const suggestions = suggestionsMatch?.[1]
|
|
45
|
+
? suggestionsMatch[1].split('\n').map((s) => s.trim()).filter(Boolean)
|
|
46
|
+
: [];
|
|
47
|
+
return {
|
|
48
|
+
isValid,
|
|
49
|
+
reasoning,
|
|
50
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
// If critic fails, assume valid to not block the agent
|
|
55
|
+
return {
|
|
56
|
+
isValid: true,
|
|
57
|
+
reasoning: `Critic failed: ${e.message}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async validateToolChoice(userRequest, availableTools, chosenTool, toolArgs) {
|
|
62
|
+
const prompt = `You are a tool-choice critic.
|
|
63
|
+
|
|
64
|
+
**USER REQUEST:** ${userRequest}
|
|
65
|
+
**AVAILABLE TOOLS:** ${availableTools.join(', ')}
|
|
66
|
+
**CHOSEN TOOL:** ${chosenTool}
|
|
67
|
+
**ARGS:** ${JSON.stringify(toolArgs)}
|
|
68
|
+
|
|
69
|
+
Is this the right tool for the job?
|
|
70
|
+
|
|
71
|
+
OUTPUT FORMAT:
|
|
72
|
+
CORRECT: yes/no
|
|
73
|
+
BETTER_TOOL: [if no, suggest better tool]`;
|
|
74
|
+
try {
|
|
75
|
+
const response = await this.model.generateContent(prompt);
|
|
76
|
+
const text = response.response.text();
|
|
77
|
+
const correctMatch = text.match(/CORRECT:\s*(yes|no)/i);
|
|
78
|
+
const betterToolMatch = text.match(/BETTER_TOOL:\s*(\w+)/);
|
|
79
|
+
return {
|
|
80
|
+
isCorrect: correctMatch?.[1].toLowerCase() === 'yes',
|
|
81
|
+
betterTool: betterToolMatch?.[1] || undefined
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return { isCorrect: true }; // Default to accepting on error
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export class DecisionLogger {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.decisions = [];
|
|
6
|
+
const logDir = path.join(process.cwd(), '.camo', 'logs');
|
|
7
|
+
this.logPath = path.join(logDir, 'decisions.jsonl');
|
|
8
|
+
this.humanLogPath = path.join(logDir, 'decisions-human.log');
|
|
9
|
+
this.ensureLogDir();
|
|
10
|
+
}
|
|
11
|
+
static getInstance() {
|
|
12
|
+
if (!DecisionLogger.instance) {
|
|
13
|
+
DecisionLogger.instance = new DecisionLogger();
|
|
14
|
+
}
|
|
15
|
+
return DecisionLogger.instance;
|
|
16
|
+
}
|
|
17
|
+
async ensureLogDir() {
|
|
18
|
+
try {
|
|
19
|
+
await fs.mkdir(path.dirname(this.logPath), { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
catch (e) { /* ignore */ }
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Log a decision (machine-readable)
|
|
25
|
+
*/
|
|
26
|
+
async logDecision(decision) {
|
|
27
|
+
this.decisions.push(decision);
|
|
28
|
+
try {
|
|
29
|
+
// Append to JSONL file
|
|
30
|
+
const jsonLine = JSON.stringify(decision) + '\n';
|
|
31
|
+
await fs.appendFile(this.logPath, jsonLine, 'utf-8');
|
|
32
|
+
// Also write human-readable format
|
|
33
|
+
await this.logHumanReadable(decision);
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
console.error('Failed to log decision:', e);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Log in human-readable format
|
|
41
|
+
*/
|
|
42
|
+
async logHumanReadable(decision) {
|
|
43
|
+
const timestamp = new Date(decision.timestamp).toISOString();
|
|
44
|
+
const humanLog = `
|
|
45
|
+
[${timestamp}] [${decision.type.toUpperCase()}]
|
|
46
|
+
Context: ${decision.context}
|
|
47
|
+
Reasoning: ${decision.reasoning}
|
|
48
|
+
Action: ${decision.action}
|
|
49
|
+
${decision.alternatives ? `Alternatives: ${decision.alternatives.join(', ')}` : ''}
|
|
50
|
+
${decision.result ? `Result: ${decision.result}` : ''}
|
|
51
|
+
---
|
|
52
|
+
`;
|
|
53
|
+
try {
|
|
54
|
+
await fs.appendFile(this.humanLogPath, humanLog, 'utf-8');
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
// Ignore
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Log tool choice decision
|
|
62
|
+
*/
|
|
63
|
+
async logToolChoice(sessionId, context, chosenTool, reasoning, alternatives) {
|
|
64
|
+
await this.logDecision({
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
sessionId,
|
|
67
|
+
type: 'tool_choice',
|
|
68
|
+
context,
|
|
69
|
+
reasoning,
|
|
70
|
+
action: `Chose tool: ${chosenTool}`,
|
|
71
|
+
alternatives
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Log strategy decision
|
|
76
|
+
*/
|
|
77
|
+
async logStrategy(sessionId, context, strategy, reasoning) {
|
|
78
|
+
await this.logDecision({
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
sessionId,
|
|
81
|
+
type: 'strategy',
|
|
82
|
+
context,
|
|
83
|
+
reasoning,
|
|
84
|
+
action: strategy
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Log verification step
|
|
89
|
+
*/
|
|
90
|
+
async logVerification(sessionId, context, checkPerformed, result) {
|
|
91
|
+
await this.logDecision({
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
sessionId,
|
|
94
|
+
type: 'verification',
|
|
95
|
+
context,
|
|
96
|
+
reasoning: `Verifying: ${checkPerformed}`,
|
|
97
|
+
action: checkPerformed,
|
|
98
|
+
result
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Log error recovery
|
|
103
|
+
*/
|
|
104
|
+
async logErrorRecovery(sessionId, error, recoveryAction, reasoning) {
|
|
105
|
+
await this.logDecision({
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
sessionId,
|
|
108
|
+
type: 'error_recovery',
|
|
109
|
+
context: `Error: ${error}`,
|
|
110
|
+
reasoning,
|
|
111
|
+
action: recoveryAction
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get recent decisions
|
|
116
|
+
*/
|
|
117
|
+
getRecentDecisions(count = 10) {
|
|
118
|
+
return this.decisions.slice(-count);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Generate a summary report
|
|
122
|
+
*/
|
|
123
|
+
async generateReport(sessionId) {
|
|
124
|
+
const sessionDecisions = this.decisions.filter(d => d.sessionId === sessionId);
|
|
125
|
+
if (sessionDecisions.length === 0) {
|
|
126
|
+
return 'No decisions logged for this session.';
|
|
127
|
+
}
|
|
128
|
+
let report = `DECISION REPORT - Session: ${sessionId}\n`;
|
|
129
|
+
report += `Total Decisions: ${sessionDecisions.length}\n\n`;
|
|
130
|
+
const byType = sessionDecisions.reduce((acc, d) => {
|
|
131
|
+
acc[d.type] = (acc[d.type] || 0) + 1;
|
|
132
|
+
return acc;
|
|
133
|
+
}, {});
|
|
134
|
+
report += 'Decisions by Type:\n';
|
|
135
|
+
Object.entries(byType).forEach(([type, count]) => {
|
|
136
|
+
report += ` ${type}: ${count}\n`;
|
|
137
|
+
});
|
|
138
|
+
report += '\n\nRecent Decisions:\n';
|
|
139
|
+
sessionDecisions.slice(-5).forEach((d, i) => {
|
|
140
|
+
const time = new Date(d.timestamp).toLocaleTimeString();
|
|
141
|
+
report += `\n[${time}] ${d.type}\n`;
|
|
142
|
+
report += ` Context: ${d.context}\n`;
|
|
143
|
+
report += ` Action: ${d.action}\n`;
|
|
144
|
+
if (d.reasoning) {
|
|
145
|
+
report += ` Reasoning: ${d.reasoning}\n`;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return report;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Clear decisions (for new session)
|
|
152
|
+
*/
|
|
153
|
+
clearDecisions() {
|
|
154
|
+
this.decisions = [];
|
|
155
|
+
}
|
|
156
|
+
}
|