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
@@ -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,5 @@
1
+ import Conf from 'conf';
2
+ export const config = new Conf({
3
+ projectName: 'camo-cli',
4
+ projectSuffix: '',
5
+ });
@@ -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
+ };