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,89 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
export const HITLConfirmation = ({ request, onResolve }) => {
|
|
4
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
5
|
+
const options = ['Accept', 'Accept for Session', 'Deny'];
|
|
6
|
+
useInput((input, key) => {
|
|
7
|
+
if (key.leftArrow || key.upArrow) {
|
|
8
|
+
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
9
|
+
}
|
|
10
|
+
if (key.rightArrow || key.downArrow) {
|
|
11
|
+
setSelectedIndex(prev => Math.min(options.length - 1, prev + 1));
|
|
12
|
+
}
|
|
13
|
+
// Number shortcuts
|
|
14
|
+
if (input === '1')
|
|
15
|
+
handleSelect(0);
|
|
16
|
+
if (input === '2')
|
|
17
|
+
handleSelect(1);
|
|
18
|
+
if (input === '3')
|
|
19
|
+
handleSelect(2);
|
|
20
|
+
if (key.return) {
|
|
21
|
+
handleSelect(selectedIndex);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const handleSelect = (index) => {
|
|
25
|
+
if (index === 0)
|
|
26
|
+
onResolve(true, 'once');
|
|
27
|
+
if (index === 1)
|
|
28
|
+
onResolve(true, 'session');
|
|
29
|
+
if (index === 2)
|
|
30
|
+
onResolve(false, 'once');
|
|
31
|
+
};
|
|
32
|
+
const renderDiff = (diff) => {
|
|
33
|
+
const lines = diff.split('\n');
|
|
34
|
+
// Find hunks (lines starting with @@)
|
|
35
|
+
const hunkIndices = [];
|
|
36
|
+
lines.forEach((line, idx) => {
|
|
37
|
+
if (line.startsWith('@@'))
|
|
38
|
+
hunkIndices.push(idx);
|
|
39
|
+
});
|
|
40
|
+
// Limit to first 2 hunks
|
|
41
|
+
const maxHunks = 2;
|
|
42
|
+
const showTruncated = hunkIndices.length > maxHunks;
|
|
43
|
+
let endLineIdx = lines.length;
|
|
44
|
+
if (showTruncated && hunkIndices.length > maxHunks) {
|
|
45
|
+
// Find where 3rd hunk starts
|
|
46
|
+
endLineIdx = hunkIndices[maxHunks];
|
|
47
|
+
}
|
|
48
|
+
const displayLines = lines.slice(0, endLineIdx);
|
|
49
|
+
return (React.createElement(React.Fragment, null,
|
|
50
|
+
displayLines.map((line, i) => {
|
|
51
|
+
if (line.startsWith('+++') || line.startsWith('---')) {
|
|
52
|
+
return React.createElement(Text, { key: i, color: "cyan", dimColor: true }, line);
|
|
53
|
+
}
|
|
54
|
+
if (line.startsWith('@@')) {
|
|
55
|
+
return React.createElement(Text, { key: i, color: "blue", bold: true }, line);
|
|
56
|
+
}
|
|
57
|
+
if (line.startsWith('+')) {
|
|
58
|
+
return React.createElement(Text, { key: i, color: "green" }, line);
|
|
59
|
+
}
|
|
60
|
+
if (line.startsWith('-')) {
|
|
61
|
+
return React.createElement(Text, { key: i, color: "red" }, line);
|
|
62
|
+
}
|
|
63
|
+
// Context lines (no prefix or space prefix)
|
|
64
|
+
return React.createElement(Text, { key: i, color: "gray", dimColor: true }, line);
|
|
65
|
+
}),
|
|
66
|
+
showTruncated && (React.createElement(Text, { color: "yellow", italic: true },
|
|
67
|
+
"... (",
|
|
68
|
+
hunkIndices.length - maxHunks,
|
|
69
|
+
" more hunk",
|
|
70
|
+
hunkIndices.length - maxHunks > 1 ? 's' : '',
|
|
71
|
+
")"))));
|
|
72
|
+
};
|
|
73
|
+
return (React.createElement(Box, { borderStyle: "double", borderColor: "white", flexDirection: "column", paddingX: 1, marginBottom: 1 },
|
|
74
|
+
React.createElement(Text, { bold: true, backgroundColor: "white", color: "black" }, " CRITICAL OPERATION REQUIRED "),
|
|
75
|
+
React.createElement(Text, { color: "white" },
|
|
76
|
+
"Action: ",
|
|
77
|
+
React.createElement(Text, { bold: true }, request.type)),
|
|
78
|
+
React.createElement(Text, null, request.command ? `Command: ${request.command}` : `File: ${request.path}`),
|
|
79
|
+
React.createElement(Text, { italic: true, color: "gray" },
|
|
80
|
+
"Reason: ",
|
|
81
|
+
request.reason),
|
|
82
|
+
request.diff && (React.createElement(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1 },
|
|
83
|
+
React.createElement(Text, { bold: true }, "Changes:"),
|
|
84
|
+
renderDiff(request.diff))),
|
|
85
|
+
React.createElement(Box, { marginTop: 1, gap: 2 }, options.map((opt, i) => (React.createElement(Text, { key: opt, color: i === selectedIndex ? "black" : "white", backgroundColor: i === selectedIndex ? "white" : undefined },
|
|
86
|
+
i + 1,
|
|
87
|
+
". ",
|
|
88
|
+
opt))))));
|
|
89
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { config } from './App.js';
|
|
5
|
+
const PROVIDERS = [
|
|
6
|
+
{ id: 'google', name: 'Google', keyName: 'googleApiKey' },
|
|
7
|
+
{ id: 'anthropic', name: 'Anthropic', keyName: 'anthropicApiKey' },
|
|
8
|
+
{ id: 'openai', name: 'OpenAI', keyName: 'openaiApiKey' },
|
|
9
|
+
{ id: 'openrouter', name: 'OpenRouter', keyName: 'openrouterApiKey' },
|
|
10
|
+
{ id: 'custom', name: 'Custom', keyName: 'customApiKey' }, // Custom needs URL too, handled specially
|
|
11
|
+
];
|
|
12
|
+
export const ModelSelector = ({ onClose }) => {
|
|
13
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
14
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
15
|
+
const [editValue, setEditValue] = useState('');
|
|
16
|
+
// Custom specific
|
|
17
|
+
const [isEditingUrl, setIsEditingUrl] = useState(false);
|
|
18
|
+
const [customUrl, setCustomUrl] = useState(config.get('customBaseUrl') || '');
|
|
19
|
+
// Effect to load current key when selection changes
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
// optional logic
|
|
22
|
+
}, [selectedIndex]);
|
|
23
|
+
useInput((input, key) => {
|
|
24
|
+
if (isEditing) {
|
|
25
|
+
// Handled by TextInput, but we need to catch Escape to cancel
|
|
26
|
+
if (key.escape) {
|
|
27
|
+
setIsEditing(false);
|
|
28
|
+
setIsEditingUrl(false);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (key.upArrow) {
|
|
33
|
+
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
34
|
+
}
|
|
35
|
+
if (key.downArrow) {
|
|
36
|
+
setSelectedIndex(prev => Math.min(PROVIDERS.length - 1, prev + 1));
|
|
37
|
+
}
|
|
38
|
+
if (key.return) {
|
|
39
|
+
startEditing();
|
|
40
|
+
}
|
|
41
|
+
if (key.escape) {
|
|
42
|
+
onClose();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const startEditing = () => {
|
|
46
|
+
const provider = PROVIDERS[selectedIndex];
|
|
47
|
+
const currentKey = config.get(provider.keyName) || '';
|
|
48
|
+
setEditValue(currentKey);
|
|
49
|
+
setIsEditing(true);
|
|
50
|
+
// For custom, we might want to edit URL first or Key?
|
|
51
|
+
// Let's Keep it simple: Enter -> Edit Key.
|
|
52
|
+
// If Custom, maybe cycle Key -> URL? Or show both inputs.
|
|
53
|
+
// For simplicity: Custom just edits Key now. URL is advanced.
|
|
54
|
+
// Wait, requirement: "Bei 'Custom' kann man eine Basis-URL und einen API-Key hinterlegen."
|
|
55
|
+
// I'll handle Custom differently if selected.
|
|
56
|
+
};
|
|
57
|
+
const saveKey = (value) => {
|
|
58
|
+
const provider = PROVIDERS[selectedIndex];
|
|
59
|
+
if (provider.id === 'custom' && isEditingUrl) {
|
|
60
|
+
config.set('customBaseUrl', value);
|
|
61
|
+
setIsEditingUrl(false);
|
|
62
|
+
// Then edit key
|
|
63
|
+
setEditValue(config.get('customApiKey') || '');
|
|
64
|
+
setIsEditing(true); // Stay editing, but now for key
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (value.trim()) {
|
|
68
|
+
config.set(provider.keyName, value.trim());
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
config.delete(provider.keyName);
|
|
72
|
+
}
|
|
73
|
+
setIsEditing(false);
|
|
74
|
+
};
|
|
75
|
+
const renderProvider = (p, index) => {
|
|
76
|
+
const isSelected = index === selectedIndex;
|
|
77
|
+
const hasKey = config.has(p.keyName);
|
|
78
|
+
// Visuals: "Provider mit hinterlegtem Key sind hellgrau, ohne Key dunkelgrau."
|
|
79
|
+
// Ink colors: 'white', 'gray', 'bgWhite', etc.
|
|
80
|
+
// Selected: Invert or Highlight.
|
|
81
|
+
let color = hasKey ? 'whiteBright' : 'gray';
|
|
82
|
+
let label = ` ${p.name} `;
|
|
83
|
+
if (isSelected) {
|
|
84
|
+
label = `> ${p.name} <`;
|
|
85
|
+
color = 'white'; // Highlight
|
|
86
|
+
}
|
|
87
|
+
return (React.createElement(Box, { key: p.id, flexDirection: "row", justifyContent: "space-between" },
|
|
88
|
+
React.createElement(Text, { color: color, bold: isSelected }, label),
|
|
89
|
+
React.createElement(Text, { color: "gray" }, hasKey ? '[*]' : '')));
|
|
90
|
+
};
|
|
91
|
+
return (React.createElement(Box, { borderStyle: "single", flexDirection: "column", paddingX: 1, borderColor: "white", marginBottom: 1 },
|
|
92
|
+
React.createElement(Text, { bold: true }, "Select Model Provider"),
|
|
93
|
+
React.createElement(Box, { flexDirection: "column", marginTop: 1 }, PROVIDERS.map((p, i) => renderProvider(p, i))),
|
|
94
|
+
isEditing && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
95
|
+
React.createElement(Text, null,
|
|
96
|
+
"API Key for ",
|
|
97
|
+
PROVIDERS[selectedIndex].name,
|
|
98
|
+
":"),
|
|
99
|
+
React.createElement(TextInput, { value: editValue, onChange: setEditValue, onSubmit: saveKey, mask: PROVIDERS[selectedIndex].id === 'custom' ? undefined : '*', focus: true })))));
|
|
100
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { config } from '../config/store.js';
|
|
5
|
+
export const SetupScreen = ({ onComplete }) => {
|
|
6
|
+
const [apiKey, setApiKey] = useState('');
|
|
7
|
+
const [error, setError] = useState('');
|
|
8
|
+
useInput((_, key) => {
|
|
9
|
+
if (key.return) {
|
|
10
|
+
handleSubmit();
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
const handleSubmit = () => {
|
|
14
|
+
const key = apiKey.trim();
|
|
15
|
+
if (!key) {
|
|
16
|
+
setError('API Key is required');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (!key.startsWith('AIza')) {
|
|
20
|
+
setError('Invalid Google API Key (must start with AIza)');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
config.set('googleApiKey', key);
|
|
24
|
+
// Clear others to ensure clean state
|
|
25
|
+
config.delete('anthropicApiKey');
|
|
26
|
+
config.delete('openaiApiKey');
|
|
27
|
+
onComplete();
|
|
28
|
+
};
|
|
29
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "green" },
|
|
30
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
31
|
+
React.createElement(Text, { bold: true, color: "green" }, "\uD83E\uDD8E CAMO Setup")),
|
|
32
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
33
|
+
React.createElement(Text, null, "Welcome to CAMO. To get started, please connect your Google Gemini API Key.")),
|
|
34
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
35
|
+
React.createElement(Text, { color: "gray" }, "Get your key at: https://aistudio.google.com/app/apikey")),
|
|
36
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
37
|
+
React.createElement(Text, { bold: true }, "Google API Key:"),
|
|
38
|
+
React.createElement(Box, { borderStyle: "single", borderColor: error ? "red" : "gray" },
|
|
39
|
+
React.createElement(TextInput, { value: apiKey, onChange: setApiKey, onSubmit: handleSubmit, mask: "*", placeholder: "Paste your AIza... key here" })),
|
|
40
|
+
error && React.createElement(Text, { color: "red" }, error)),
|
|
41
|
+
React.createElement(Box, { marginTop: 1 },
|
|
42
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "Press Enter to save and start"))));
|
|
43
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration constants for CAMO agent
|
|
3
|
+
*/
|
|
4
|
+
// Timeouts
|
|
5
|
+
export const SHELL_TIMEOUT_MS = 30000; // 30 seconds
|
|
6
|
+
export const MAX_LOOPS = 15;
|
|
7
|
+
// Limits
|
|
8
|
+
export const MAX_FILE_SIZE = 8000; // Characters for readFile
|
|
9
|
+
export const MAX_SHELL_OUTPUT = 2000; // Characters for shell output
|
|
10
|
+
export const MAX_GREP_RESULTS = 20; // Lines for grep
|
|
11
|
+
export const MAX_FILE_LIST = 10; // Files shown in listFiles
|
|
12
|
+
// Excluded directories
|
|
13
|
+
export const EXCLUDED_DIRS = ['node_modules', '.git', '.DS_Store', 'dist', 'build', '.next'];
|
|
14
|
+
// Security: Blocked shell commands (never allowed, even with HITL)
|
|
15
|
+
export const BLOCKED_COMMANDS = [
|
|
16
|
+
/^sudo\s+rm\s+-rf\s+\/$/,
|
|
17
|
+
/^rm\s+-rf\s+\/$/,
|
|
18
|
+
/^rm\s+-rf\s+~$/,
|
|
19
|
+
/^chmod\s+777\s+\/$/,
|
|
20
|
+
/^:(){ :|:& };:/, // Fork bomb
|
|
21
|
+
/^dd\s+if=.*of=\/dev\//,
|
|
22
|
+
/^mkfs\./,
|
|
23
|
+
/^format\s/
|
|
24
|
+
];
|
|
25
|
+
// Safe shell patterns (auto-approved)
|
|
26
|
+
export const SAFE_SHELL_PATTERNS = [
|
|
27
|
+
// Read operations
|
|
28
|
+
/^(ls|cat|head|tail|less|more|grep|find|which|pwd|echo|date)\s/,
|
|
29
|
+
/^git\s+(status|log|diff|branch|show)/,
|
|
30
|
+
// Package managers (read-only)
|
|
31
|
+
/^(npm|yarn|pnpm)\s+(list|ls|outdated|view|info)/,
|
|
32
|
+
// Test runners
|
|
33
|
+
/^(jest|vitest|mocha|ava)\s/,
|
|
34
|
+
// Build tools (read-only check)
|
|
35
|
+
/^(tsc|eslint|prettier)\s+--noEmit/,
|
|
36
|
+
// Other safe commands
|
|
37
|
+
/^(date|echo|printf|which|type|command\s+-v)/
|
|
38
|
+
];
|
|
39
|
+
// Critical shell patterns (require HITL)
|
|
40
|
+
export const CRITICAL_SHELL_PATTERNS = [
|
|
41
|
+
/^(rm|mv|cp)\s/,
|
|
42
|
+
/^git\s+(push|commit|add|reset|rebase|merge|cherry-pick|pull)/,
|
|
43
|
+
/^(npm|yarn|pnpm)\s+(install|i|add|remove|uninstall|publish|run\s+(?!test))/,
|
|
44
|
+
/^(curl|wget|fetch)\s/,
|
|
45
|
+
/^chmod\s/,
|
|
46
|
+
/^sudo\s/,
|
|
47
|
+
/>/, // Output redirection
|
|
48
|
+
/>>/ // Append redirection
|
|
49
|
+
];
|
|
50
|
+
// Safe tools (auto-approved without HITL)
|
|
51
|
+
export const SAFE_TOOLS = new Set([
|
|
52
|
+
'readFile',
|
|
53
|
+
'listFiles',
|
|
54
|
+
'grep',
|
|
55
|
+
'webFetch',
|
|
56
|
+
'internal_thought',
|
|
57
|
+
'ToDoWrite'
|
|
58
|
+
]);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System prompts for CAMO agent
|
|
3
|
+
* Based on OpenAI Codex CLI + Claude Code best practices
|
|
4
|
+
*/
|
|
5
|
+
export const ELITE_SYSTEM_PROMPT = `You are CAMO, an autonomous coding agent running in a terminal environment.
|
|
6
|
+
|
|
7
|
+
# CONSTITUTION
|
|
8
|
+
|
|
9
|
+
## Your Role
|
|
10
|
+
You are an AI coding assistant integrated into the user's development workflow. You can:
|
|
11
|
+
- Read and write files
|
|
12
|
+
- Execute terminal commands
|
|
13
|
+
- Search codebases
|
|
14
|
+
- Apply code patches
|
|
15
|
+
- Navigate directory structures
|
|
16
|
+
|
|
17
|
+
You communicate through streaming text responses and emit function calls to interact with the system.
|
|
18
|
+
|
|
19
|
+
## Tone & Communication
|
|
20
|
+
- **Concise**: Brief, direct answers. No preambles or disclaimers.
|
|
21
|
+
- **Action-First**: Execute immediately. Never ask "Soll ich...?" or "Should I...?"
|
|
22
|
+
- **Clear**: State what you're doing, not what you could do.
|
|
23
|
+
- **Adaptive**: Respond in the user's language (detect from input).
|
|
24
|
+
|
|
25
|
+
## Core Behaviors
|
|
26
|
+
|
|
27
|
+
### 1. Autonomous Execution
|
|
28
|
+
✓ Persist until task is fully resolved before yielding
|
|
29
|
+
✓ Handle errors autonomously - retry, adapt, or escalate
|
|
30
|
+
✓ Break complex requests into sub-problems automatically
|
|
31
|
+
✗ Never stop mid-task to ask for permission
|
|
32
|
+
✗ Don't suggest solutions - implement them
|
|
33
|
+
|
|
34
|
+
### 2. Automatic Error Recovery (NON-NEGOTIABLE)
|
|
35
|
+
You MUST autonomously recover from errors without stopping.
|
|
36
|
+
|
|
37
|
+
- **IF \`readFile\` -> EISDIR**: usage error! DO NOT apologize. IMMEDIATELY call \`listFiles\` on that path in the SAME turn if possible, or the very next.
|
|
38
|
+
- **IF \`readFile\` -> ENOENT**: wrong path! Search parent dir with \`listFiles\` or \`find\`.
|
|
39
|
+
- **IF tool fails**: Ask yourself "Can I fix this?" If yes, EXECUTE the fix. Do not ask the user "Should I try...?"
|
|
40
|
+
|
|
41
|
+
### 3. Tool Usage Protocol
|
|
42
|
+
When executing tools:
|
|
43
|
+
1. State your action briefly (one line)
|
|
44
|
+
2. Call the tool ONCE
|
|
45
|
+
3. [System provides result]
|
|
46
|
+
4. Summarize outcome clearly and STOP
|
|
47
|
+
|
|
48
|
+
### 4. Path Resolution (CRITICAL)
|
|
49
|
+
**ALWAYS prefer paths relative to the current working directory:**
|
|
50
|
+
- ✓ Use \`.\`, \`./subdir/file.txt\`, \`src/component.tsx\`
|
|
51
|
+
- ✓ When user says "camo test dir", interpret as \`./camo-test/\` (local)
|
|
52
|
+
- ✗ Do NOT use absolute paths like \`/sessions/\`, \`/Users/...\` unless explicitly given
|
|
53
|
+
- The working directory is the user's project root - ALL paths are relative to it
|
|
54
|
+
|
|
55
|
+
CRITICAL: After calling a tool and receiving its result, ALWAYS:
|
|
56
|
+
- Provide a brief summary of what was found/done
|
|
57
|
+
- Mark task as complete
|
|
58
|
+
- DO NOT call the same tool multiple times
|
|
59
|
+
- DO NOT continue exploring unless explicitly asked
|
|
60
|
+
|
|
61
|
+
IMPORTANT: When the task is done, ALWAYS provide a final natural language response summarizing the findings or actions (e.g. "I found the issue in file X..."). Do not just stop after a tool output.
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
User: "list files in src/"
|
|
65
|
+
You: "Listing source files"
|
|
66
|
+
[listFiles called, result returned]
|
|
67
|
+
You: "Found 12 TypeScript files: index.ts, agent.ts, utils.ts, and 9 others"
|
|
68
|
+
[STOP - task complete]
|
|
69
|
+
|
|
70
|
+
### 3. Communication Standards
|
|
71
|
+
DO:
|
|
72
|
+
- ✓ "Creating test.txt" → [calls writeFile]
|
|
73
|
+
- ✓ "Running tests" → [calls executeShellCommand]
|
|
74
|
+
- ✓ "Found 3 errors in app.ts" (specific, actionable)
|
|
75
|
+
|
|
76
|
+
DON'T:
|
|
77
|
+
- ✗ "I can list the files for you" (just do it)
|
|
78
|
+
- ✗ "Would you like me to..." (never ask permission)
|
|
79
|
+
- ✗ Long explanations before acting (act first, explain after if needed)
|
|
80
|
+
|
|
81
|
+
### 4. Safety & Verification
|
|
82
|
+
- Verify critical changes by reading back modified files
|
|
83
|
+
- For destructive operations (rm, git push), acknowledge action clearly
|
|
84
|
+
- If unsure about scope, ask clarifying questions BEFORE acting, not during
|
|
85
|
+
|
|
86
|
+
## Available Tools
|
|
87
|
+
|
|
88
|
+
\`writeFile(path, content)\` - Create or overwrite file
|
|
89
|
+
\`readFile(path)\` - Read file content (returns full content)
|
|
90
|
+
\`editFile(path, unifiedDiff)\` - Apply unified diff patch
|
|
91
|
+
\`executeShellCommand(command)\` - Run terminal command
|
|
92
|
+
\`listFiles(path)\` - List directory contents
|
|
93
|
+
\`grep(pattern, path)\` - Search in files
|
|
94
|
+
|
|
95
|
+
## Important Context
|
|
96
|
+
{{PROJECT_CONTEXT}}
|
|
97
|
+
{{TASK_STATE}}
|
|
98
|
+
`;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
|
+
import { config } from '../config/store.js';
|
|
3
|
+
import { ELITE_SYSTEM_PROMPT } from '../config/prompts.js';
|
|
4
|
+
import { TaskState } from '../utils/TaskState.js';
|
|
5
|
+
import { ToolRegistry } from '../tools/ToolRegistry.js';
|
|
6
|
+
import { formatToolCall, formatToolOutput } from '../utils/uiFormatter.js';
|
|
7
|
+
import { MAX_LOOPS } from '../config/constants.js';
|
|
8
|
+
import { MemoryManager } from '../memory/MemoryManager.js';
|
|
9
|
+
// Tool definitions for Google Native SDK (add memory tools)
|
|
10
|
+
const googleToolDefinitions = [
|
|
11
|
+
{ name: 'writeFile', description: 'Create or overwrite a file', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } },
|
|
12
|
+
{ name: 'readFile', description: 'Read file contents', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
|
|
13
|
+
{ name: 'listFiles', description: 'List files in directory', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
|
|
14
|
+
{ name: 'executeShellCommand', description: 'Execute shell command', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } },
|
|
15
|
+
{ name: 'updateMemory', description: 'Store learned project facts', parameters: { type: 'object', properties: { category: { type: 'string' }, content: { type: 'string' } }, required: ['category', 'content'] } },
|
|
16
|
+
{ name: 'syncPlan', description: 'Update task status', parameters: { type: 'object', properties: { taskId: { type: 'string' }, status: { type: 'string', enum: ['pending', 'in_progress', 'complete'] } }, required: ['taskId', 'status'] } },
|
|
17
|
+
{ name: 'recallContext', description: 'Retrieve stored knowledge', parameters: { type: 'object', properties: { query: { type: 'string' } } } },
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Core Autonomous Agent Loop (Google Native SDK)
|
|
21
|
+
*/
|
|
22
|
+
export async function runAgentLoop(context, callbacks) {
|
|
23
|
+
const { onChunk } = callbacks;
|
|
24
|
+
const { userMessage, history, googleKey, activePlan, projectContextCache } = context;
|
|
25
|
+
if (!googleKey) {
|
|
26
|
+
onChunk('\n❗ Error: No Google API Key found.\n');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Initialize memory system
|
|
30
|
+
const memory = MemoryManager.getInstance();
|
|
31
|
+
await memory.initialize();
|
|
32
|
+
await memory.incrementSession();
|
|
33
|
+
// Load persistent memory
|
|
34
|
+
const plan = await memory.loadPlan();
|
|
35
|
+
const memoryContext = await memory.loadContext();
|
|
36
|
+
const pendingTasks = await memory.getPendingTasks();
|
|
37
|
+
// Build memory-aware system prompt
|
|
38
|
+
let enhancedPrompt = ELITE_SYSTEM_PROMPT;
|
|
39
|
+
if (pendingTasks.length > 0) {
|
|
40
|
+
enhancedPrompt += `\n\n📋 **ACTIVE PLAN** (from .camo/plan.md):\n${pendingTasks.map(t => `- ${t}`).join('\n')}\n`;
|
|
41
|
+
onChunk(`\n⏺ Resuming work on: ${pendingTasks[0]}\n`);
|
|
42
|
+
}
|
|
43
|
+
if (memoryContext && memoryContext.trim().length > 100) {
|
|
44
|
+
enhancedPrompt += `\n\n📚 **PROJECT KNOWLEDGE** (from .camo/context.md):\n${memoryContext.slice(0, 1000)}\n`;
|
|
45
|
+
}
|
|
46
|
+
let loopCount = 0;
|
|
47
|
+
const client = new GoogleGenerativeAI(googleKey);
|
|
48
|
+
// Select Model - map UI names to Google model IDs
|
|
49
|
+
let selectedModelId = config.get('selectedModel') || 'gemini-2-flash';
|
|
50
|
+
const modelMap = {
|
|
51
|
+
'gemini-3-flash': 'gemini-exp-1206',
|
|
52
|
+
'gemini-3-pro': 'gemini-2.0-pro-exp',
|
|
53
|
+
'gemini-2-flash': 'gemini-2.0-flash-exp',
|
|
54
|
+
'gemini-2-pro': 'gemini-2.5-pro',
|
|
55
|
+
};
|
|
56
|
+
const actualModelId = modelMap[selectedModelId] || selectedModelId;
|
|
57
|
+
const model = client.getGenerativeModel({
|
|
58
|
+
model: actualModelId,
|
|
59
|
+
tools: [{ functionDeclarations: googleToolDefinitions }]
|
|
60
|
+
});
|
|
61
|
+
// Prepare Plan Context
|
|
62
|
+
let planContext = "";
|
|
63
|
+
if (activePlan.length > 0) {
|
|
64
|
+
planContext = `\n**AKTUELLER PLAN**:\n${activePlan.map(t => `- [${t.status === 'completed' ? 'x' : t.status === 'in_progress' ? '/' : ' '}] ${t.title} (${t.id})`).join('\n')}\n`;
|
|
65
|
+
}
|
|
66
|
+
// Initialize Message History
|
|
67
|
+
const contents = history.map(msg => ({
|
|
68
|
+
role: msg.role === 'user' ? 'user' : 'model',
|
|
69
|
+
parts: [{ text: msg.content }]
|
|
70
|
+
})).concat([{
|
|
71
|
+
role: 'user',
|
|
72
|
+
parts: [{ text: userMessage + planContext }]
|
|
73
|
+
}]);
|
|
74
|
+
const systemInstruction = enhancedPrompt
|
|
75
|
+
.replace('{{PROJECT_CONTEXT}}', projectContextCache)
|
|
76
|
+
.replace('{{TASK_STATE}}', TaskState.getContextString());
|
|
77
|
+
let lastToolSignature = '';
|
|
78
|
+
let repetitionCount = 0;
|
|
79
|
+
while (loopCount < MAX_LOOPS) {
|
|
80
|
+
loopCount++;
|
|
81
|
+
try {
|
|
82
|
+
const response = await model.generateContent({
|
|
83
|
+
contents: contents,
|
|
84
|
+
systemInstruction: systemInstruction
|
|
85
|
+
});
|
|
86
|
+
const responseText = response.response.text() || '';
|
|
87
|
+
const toolCalls = response.response.functionCalls?.();
|
|
88
|
+
// Output text if present
|
|
89
|
+
if (responseText.trim()) {
|
|
90
|
+
onChunk(responseText + '\n');
|
|
91
|
+
}
|
|
92
|
+
// Check for completion
|
|
93
|
+
if (responseText.includes('[TASK_COMPLETE]') || responseText.includes('[done]')) {
|
|
94
|
+
onChunk('\n[done]\n');
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
98
|
+
onChunk('\n[done]\n');
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
// Add model turn
|
|
102
|
+
contents.push({
|
|
103
|
+
role: 'model',
|
|
104
|
+
parts: [{ text: responseText }]
|
|
105
|
+
});
|
|
106
|
+
// Loop detection
|
|
107
|
+
const currentSignature = toolCalls.map(tc => `${tc.name}:${JSON.stringify(tc.args)}`).join('|');
|
|
108
|
+
if (currentSignature === lastToolSignature) {
|
|
109
|
+
repetitionCount++;
|
|
110
|
+
if (repetitionCount >= 3) {
|
|
111
|
+
onChunk('\n[Repetition Detected]\n');
|
|
112
|
+
if (repetitionCount > 5)
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
repetitionCount = 0;
|
|
118
|
+
lastToolSignature = currentSignature;
|
|
119
|
+
}
|
|
120
|
+
// Execute tools
|
|
121
|
+
const toolResponses = [];
|
|
122
|
+
for (const call of toolCalls) {
|
|
123
|
+
onChunk(formatToolCall(call.name, call.args));
|
|
124
|
+
let resultString = '';
|
|
125
|
+
try {
|
|
126
|
+
const toolFn = ToolRegistry[call.name];
|
|
127
|
+
if (toolFn) {
|
|
128
|
+
resultString = await toolFn(call.args, callbacks);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
resultString = JSON.stringify({ error: `Unknown tool: ${call.name}` });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
resultString = JSON.stringify({ error: e.message });
|
|
136
|
+
}
|
|
137
|
+
onChunk('\n' + formatToolOutput(resultString) + '\n');
|
|
138
|
+
toolResponses.push({
|
|
139
|
+
functionResponse: {
|
|
140
|
+
name: call.name,
|
|
141
|
+
response: { name: call.name, content: resultString }
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
// Add tool responses to history
|
|
146
|
+
if (toolResponses.length > 0) {
|
|
147
|
+
contents.push({
|
|
148
|
+
role: 'function',
|
|
149
|
+
parts: toolResponses
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
onChunk(`\n❗ Error: ${e.message}\n`);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
onChunk('\n[done]\n');
|
|
159
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
2
|
+
export const useAutocomplete = (input) => {
|
|
3
|
+
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
4
|
+
// Available slash commands
|
|
5
|
+
const commands = useMemo(() => [
|
|
6
|
+
{ name: 'help', description: 'Show help' },
|
|
7
|
+
{ name: 'key', description: 'Set API key' },
|
|
8
|
+
{ name: 'model', description: 'Switch model' },
|
|
9
|
+
{ name: 'connect', description: 'Connect to API' },
|
|
10
|
+
{ name: 'config', description: 'Configuration' },
|
|
11
|
+
{ name: 'mcp', description: 'MCP servers' },
|
|
12
|
+
{ name: 'clear', description: 'Clear screen' },
|
|
13
|
+
{ name: 'exit', description: 'Exit' }
|
|
14
|
+
], []);
|
|
15
|
+
const suggestions = useMemo(() => {
|
|
16
|
+
if (!input.startsWith('/')) {
|
|
17
|
+
return { suggestions: [], commonPrefix: '', isUnique: false };
|
|
18
|
+
}
|
|
19
|
+
const commandPart = input.slice(1).split(' ')[0];
|
|
20
|
+
const matches = commands.filter(cmd => cmd.name.startsWith(commandPart));
|
|
21
|
+
if (matches.length === 0) {
|
|
22
|
+
return { suggestions: [], commonPrefix: '', isUnique: false };
|
|
23
|
+
}
|
|
24
|
+
if (matches.length === 1) {
|
|
25
|
+
const cmd = matches[0];
|
|
26
|
+
return {
|
|
27
|
+
suggestions: [cmd.name],
|
|
28
|
+
commonPrefix: `/${cmd.name} `,
|
|
29
|
+
isUnique: true
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// Find common prefix
|
|
33
|
+
const names = matches.map(m => m.name);
|
|
34
|
+
let commonPrefix = names[0];
|
|
35
|
+
for (let i = 1; i < names.length; i++) {
|
|
36
|
+
while (!names[i].startsWith(commonPrefix)) {
|
|
37
|
+
commonPrefix = commonPrefix.slice(0, -1);
|
|
38
|
+
if (commonPrefix === '')
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
suggestions: names,
|
|
44
|
+
commonPrefix: `/${commonPrefix}`,
|
|
45
|
+
isUnique: false
|
|
46
|
+
};
|
|
47
|
+
}, [input, commands]);
|
|
48
|
+
const toggleSuggestions = useCallback(() => {
|
|
49
|
+
setShowSuggestions(!showSuggestions);
|
|
50
|
+
}, [showSuggestions]);
|
|
51
|
+
return { suggestions: suggestions.suggestions, showSuggestions, commonPrefix: suggestions.commonPrefix };
|
|
52
|
+
};
|