camo-cli 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -0
- package/dist/agent.js +977 -0
- package/dist/art.js +33 -0
- package/dist/components/App.js +71 -0
- package/dist/components/Chat.js +509 -0
- package/dist/components/HITLConfirmation.js +89 -0
- package/dist/components/ModelSelector.js +100 -0
- package/dist/components/SetupScreen.js +43 -0
- package/dist/config/constants.js +58 -0
- package/dist/config/prompts.js +98 -0
- package/dist/config/store.js +5 -0
- package/dist/core/AgentLoop.js +159 -0
- package/dist/hooks/useAutocomplete.js +52 -0
- package/dist/hooks/useKeyboard.js +73 -0
- package/dist/index.js +31 -0
- package/dist/mcp.js +95 -0
- package/dist/memory/MemoryManager.js +228 -0
- package/dist/providers/index.js +85 -0
- package/dist/providers/registry.js +121 -0
- package/dist/providers/types.js +5 -0
- package/dist/theme.js +45 -0
- package/dist/tools/FileTools.js +88 -0
- package/dist/tools/MemoryTools.js +53 -0
- package/dist/tools/SearchTools.js +45 -0
- package/dist/tools/ShellTools.js +40 -0
- package/dist/tools/TaskTools.js +52 -0
- package/dist/tools/ToolDefinitions.js +102 -0
- package/dist/tools/ToolRegistry.js +30 -0
- package/dist/types/Agent.js +6 -0
- package/dist/types/ink.js +1 -0
- package/dist/types/message.js +1 -0
- package/dist/types/ui.js +1 -0
- package/dist/utils/CriticAgent.js +88 -0
- package/dist/utils/DecisionLogger.js +156 -0
- package/dist/utils/MessageHistory.js +55 -0
- package/dist/utils/PermissionManager.js +253 -0
- package/dist/utils/SessionManager.js +180 -0
- package/dist/utils/TaskState.js +108 -0
- package/dist/utils/debug.js +35 -0
- package/dist/utils/execAsync.js +3 -0
- package/dist/utils/retry.js +50 -0
- package/dist/utils/tokenCounter.js +24 -0
- package/dist/utils/uiFormatter.js +106 -0
- package/package.json +92 -0
package/dist/art.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function getEliteHeaderArt() {
|
|
3
|
+
const c = {
|
|
4
|
+
g1: chalk.hex('#4ade80'), // Skin Light (Bright Green)
|
|
5
|
+
g2: chalk.hex('#22c55e'), // Skin Mid
|
|
6
|
+
g3: chalk.hex('#166534'), // Skin Dark (Shadows)
|
|
7
|
+
eye: chalk.hex('#fbbf24'), // Eye (Amber)
|
|
8
|
+
pupil: chalk.hex('#000000'), // Pupil
|
|
9
|
+
br1: chalk.hex('#a0522d'), // Branch Light
|
|
10
|
+
br2: chalk.hex('#8b4513'), // Branch Mid
|
|
11
|
+
leaf: chalk.hex('#86efac'), // Leaf
|
|
12
|
+
dim: chalk.dim,
|
|
13
|
+
};
|
|
14
|
+
const lines = [
|
|
15
|
+
``,
|
|
16
|
+
` ${c.g1('▄▄')}`,
|
|
17
|
+
` ${c.g1('████')}${c.g2('▄')}`,
|
|
18
|
+
` ${c.g2('▄')}${c.g1('██████')}${c.g2('▄')}`,
|
|
19
|
+
` ${c.g2('████████')}${c.g1('█')}${c.g2('▄')} ${c.leaf('▄▄')}`,
|
|
20
|
+
` ${c.g1('▄▄▄▄')} ${c.g2('▐██')}${c.eye('███')}${c.pupil('█')}${c.eye('█')}${c.g2('██▌')} ${c.leaf('████')}`,
|
|
21
|
+
` ${c.g1('▄██████')}${c.g2('▄')} ${c.g2('▀██████▀')} ${c.leaf('▄██████')}`,
|
|
22
|
+
` ${c.g1('▐██▀ ▀██▌')} ${c.leaf('▀▀')}${c.br1('__')}${c.leaf('▀▀')}`,
|
|
23
|
+
` ${c.g2('██')} ${c.g2('▀')} ${c.br1('.-´')}`,
|
|
24
|
+
` ${c.g2('▐█▄')} ${c.g3('▄▄▄')}${c.g2('██████▄')}${c.g3('▄')} ${c.br1('.-´')}`,
|
|
25
|
+
` ${c.g2('▀██▄▄▄▄██████████████')}${c.g3('██▄')} ${c.br1('.-´')}`,
|
|
26
|
+
` ${c.g3('▀▀▀▀▀▀')} ${c.g3('▐████')}${c.g2('████')}${c.br2('██▄´')}`,
|
|
27
|
+
` ${c.br1('================')}${c.g3('██')}${c.g2('██')}${c.g3('█')}${c.br1('====')}${c.br2('▀▀')}${c.br1('======')}`,
|
|
28
|
+
``,
|
|
29
|
+
` ${chalk.bold.hex('#4ade80')('🦎 CAMO CLI')} ${chalk.dim('v2.1')} ${chalk.dim('• Adaptive Terminal Intelligence')}`,
|
|
30
|
+
``
|
|
31
|
+
];
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { Box, useApp } from 'ink';
|
|
3
|
+
import { SetupScreen } from './SetupScreen.js';
|
|
4
|
+
import { ChatInterface } from './Chat.js';
|
|
5
|
+
import { config } from '../config/store.js';
|
|
6
|
+
// View States
|
|
7
|
+
var ViewState;
|
|
8
|
+
(function (ViewState) {
|
|
9
|
+
ViewState["CHAT_VIEW"] = "CHAT_VIEW";
|
|
10
|
+
ViewState["SETUP_VIEW"] = "SETUP_VIEW";
|
|
11
|
+
ViewState["HITL_VIEW"] = "HITL_VIEW";
|
|
12
|
+
})(ViewState || (ViewState = {}));
|
|
13
|
+
export const App = ({ initialInput, verbose }) => {
|
|
14
|
+
const { exit } = useApp();
|
|
15
|
+
// Determine initial view based on API key presence
|
|
16
|
+
const hasApiKey = Boolean(config.get('googleApiKey'));
|
|
17
|
+
// State
|
|
18
|
+
const [view, setView] = useState(hasApiKey ? ViewState.CHAT_VIEW : ViewState.SETUP_VIEW);
|
|
19
|
+
const [apiKey, setApiKey] = useState(config.get('googleApiKey') || null);
|
|
20
|
+
const [hitlRequest, setHitlRequest] = useState(null);
|
|
21
|
+
// HITL Resolver logic
|
|
22
|
+
const hitlResolveRef = useRef(null);
|
|
23
|
+
const createHITLRequest = useCallback((request) => {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
hitlResolveRef.current = resolve;
|
|
26
|
+
setHitlRequest(request);
|
|
27
|
+
setView(ViewState.HITL_VIEW);
|
|
28
|
+
});
|
|
29
|
+
}, []);
|
|
30
|
+
const resolveHITL = useCallback((approved, scope) => {
|
|
31
|
+
if (hitlResolveRef.current) {
|
|
32
|
+
const resolver = hitlResolveRef.current;
|
|
33
|
+
hitlResolveRef.current = null;
|
|
34
|
+
setHitlRequest(null);
|
|
35
|
+
setView(ViewState.CHAT_VIEW);
|
|
36
|
+
resolver({ approved, scope });
|
|
37
|
+
}
|
|
38
|
+
}, []);
|
|
39
|
+
// Initialize Agent Context only once
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
import('../agent.js').then(module => {
|
|
42
|
+
module.initializeAgentContext();
|
|
43
|
+
});
|
|
44
|
+
// Cleanup on unmount
|
|
45
|
+
return () => {
|
|
46
|
+
import('../agent.js').then(module => {
|
|
47
|
+
module.cleanupAgent();
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
const handleCommand = useCallback((cmd) => {
|
|
52
|
+
// App-level interception removed for /model so Chat.tsx can handle it
|
|
53
|
+
if (cmd.trim() === '/exit') {
|
|
54
|
+
exit();
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}, [exit]);
|
|
59
|
+
const handleSetupComplete = useCallback(() => {
|
|
60
|
+
const key = config.get('googleApiKey');
|
|
61
|
+
setApiKey(key);
|
|
62
|
+
if (key) {
|
|
63
|
+
setView(ViewState.CHAT_VIEW);
|
|
64
|
+
}
|
|
65
|
+
}, []);
|
|
66
|
+
return (React.createElement(Box, { flexDirection: "column", height: "100%" },
|
|
67
|
+
view === ViewState.SETUP_VIEW && (React.createElement(Box, { flexDirection: "column", justifyContent: "center", alignItems: "center", flexGrow: 1, marginBottom: 1 },
|
|
68
|
+
React.createElement(SetupScreen, { onComplete: handleSetupComplete }))),
|
|
69
|
+
React.createElement(Box, { flexGrow: 1, flexDirection: "column" },
|
|
70
|
+
React.createElement(ChatInterface, { active: view === ViewState.CHAT_VIEW, onCommand: handleCommand, apiKey: apiKey, verbose: verbose, onRequestHITL: createHITLRequest, hitlRequest: view === ViewState.HITL_VIEW ? hitlRequest : null, onResolveHITL: view === ViewState.HITL_VIEW ? resolveHITL : undefined }))));
|
|
71
|
+
};
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
2
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { HITLConfirmation } from './HITLConfirmation.js';
|
|
5
|
+
import { getContextStats, formatTokenUsage } from '../utils/tokenCounter.js';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import { config } from '../config/store.js';
|
|
8
|
+
export const ChatInterface = ({ active, onCommand, apiKey, verbose, onRequestHITL, hitlRequest, onResolveHITL }) => {
|
|
9
|
+
const [input, setInput] = useState('');
|
|
10
|
+
const [messages, setMessages] = useState([]);
|
|
11
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
12
|
+
const [status, setStatus] = useState('');
|
|
13
|
+
const [mode, setMode] = useState('manual');
|
|
14
|
+
// Command history
|
|
15
|
+
const [commandHistory, setCommandHistory] = useState([]);
|
|
16
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
17
|
+
// Ctrl+C tracking
|
|
18
|
+
const lastCtrlCRef = useRef(0);
|
|
19
|
+
const { exit } = useApp();
|
|
20
|
+
// Refs
|
|
21
|
+
const messagesRef = useRef([]);
|
|
22
|
+
const pendingChunksRef = useRef('');
|
|
23
|
+
const assistantIdRef = useRef(null);
|
|
24
|
+
const updateIntervalRef = useRef(null);
|
|
25
|
+
// Thinking animation
|
|
26
|
+
const [dots, setDots] = useState('');
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
let interval;
|
|
29
|
+
if (isProcessing && status === 'thinking...') {
|
|
30
|
+
interval = setInterval(() => {
|
|
31
|
+
setDots(prev => prev.length >= 3 ? '' : prev + '.');
|
|
32
|
+
}, 500);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
setDots('');
|
|
36
|
+
}
|
|
37
|
+
return () => clearInterval(interval);
|
|
38
|
+
}, [isProcessing, status]);
|
|
39
|
+
const COMMANDS = useMemo(() => [
|
|
40
|
+
{ name: '/context', description: 'Show token usage' },
|
|
41
|
+
{ name: '/report', description: 'Show decision report' },
|
|
42
|
+
{ name: '/model', description: 'Change AI model' },
|
|
43
|
+
{ name: '/provider', description: 'Manage API provider and keys' },
|
|
44
|
+
{ name: '/help', description: 'Show help' },
|
|
45
|
+
{ name: '/exit', description: 'Exit CAMO' }
|
|
46
|
+
], []);
|
|
47
|
+
// Suggestions state
|
|
48
|
+
const [suggestionIndex, setSuggestionIndex] = useState(0);
|
|
49
|
+
const [suggestionsDismissed, setSuggestionsDismissed] = useState(false);
|
|
50
|
+
const [suggestionScrollTop, setSuggestionScrollTop] = useState(0);
|
|
51
|
+
const suggestions = useMemo(() => {
|
|
52
|
+
// Special case for /model
|
|
53
|
+
if (input.startsWith('/model ')) {
|
|
54
|
+
const search = input.replace('/model ', '').toLowerCase();
|
|
55
|
+
const currentModel = config.get('selectedModel') || 'gemini-2-flash';
|
|
56
|
+
const models = [
|
|
57
|
+
{ name: 'gemini-3-flash', description: 'Am schnellsten' },
|
|
58
|
+
{ name: 'gemini-3-pro', description: 'Am besten' },
|
|
59
|
+
{ name: 'gemini-2-flash', description: 'Schnell & stabil' },
|
|
60
|
+
{ name: 'gemini-2-pro', description: 'Fortgeschritten & komplex' }
|
|
61
|
+
];
|
|
62
|
+
return models
|
|
63
|
+
.filter(m => m.name.toLowerCase().includes(search))
|
|
64
|
+
.map(m => ({
|
|
65
|
+
name: m.name,
|
|
66
|
+
description: m.description
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
return COMMANDS.filter(cmd => cmd.name.toLowerCase().startsWith(input.toLowerCase()));
|
|
70
|
+
}, [input, COMMANDS]);
|
|
71
|
+
const showSuggestions = input.startsWith('/') && suggestions.length > 0 && !suggestionsDismissed;
|
|
72
|
+
// Reset suggestions
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setSuggestionIndex(0);
|
|
75
|
+
setSuggestionScrollTop(0);
|
|
76
|
+
setSuggestionsDismissed(false);
|
|
77
|
+
}, [input]);
|
|
78
|
+
// Keyboard handling
|
|
79
|
+
useInput((inputChar, key) => {
|
|
80
|
+
if (!active)
|
|
81
|
+
return;
|
|
82
|
+
if (key.ctrl && inputChar === 'c') {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (now - lastCtrlCRef.current < 500) {
|
|
85
|
+
exit();
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
setInput('');
|
|
89
|
+
setHistoryIndex(-1);
|
|
90
|
+
lastCtrlCRef.current = now;
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (showSuggestions) {
|
|
95
|
+
if (key.escape) {
|
|
96
|
+
setSuggestionsDismissed(true);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (key.upArrow) {
|
|
100
|
+
const newIndex = suggestionIndex > 0 ? suggestionIndex - 1 : suggestions.length - 1;
|
|
101
|
+
setSuggestionIndex(newIndex);
|
|
102
|
+
if (newIndex < suggestionScrollTop) {
|
|
103
|
+
setSuggestionScrollTop(newIndex);
|
|
104
|
+
}
|
|
105
|
+
else if (newIndex >= suggestionScrollTop + 4) {
|
|
106
|
+
setSuggestionScrollTop(newIndex - 3);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (key.downArrow) {
|
|
111
|
+
const newIndex = suggestionIndex < suggestions.length - 1 ? suggestionIndex + 1 : 0;
|
|
112
|
+
setSuggestionIndex(newIndex);
|
|
113
|
+
if (newIndex >= suggestionScrollTop + 4) {
|
|
114
|
+
setSuggestionScrollTop(newIndex - 3);
|
|
115
|
+
}
|
|
116
|
+
else if (newIndex < suggestionScrollTop) {
|
|
117
|
+
setSuggestionScrollTop(newIndex);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (key.tab || key.return) {
|
|
122
|
+
if (suggestionIndex >= 0 && suggestionIndex < suggestions.length) {
|
|
123
|
+
const selected = suggestions[suggestionIndex];
|
|
124
|
+
if (input.startsWith('/model ')) {
|
|
125
|
+
setInput(`/model ${selected.name}`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// If completing a command, add space so arguments can be typed
|
|
129
|
+
setInput(selected.name + ' ');
|
|
130
|
+
}
|
|
131
|
+
setSuggestionIndex(0);
|
|
132
|
+
setSuggestionsDismissed(true);
|
|
133
|
+
}
|
|
134
|
+
else if (suggestions.length > 0) {
|
|
135
|
+
const selected = suggestions[0];
|
|
136
|
+
if (input.startsWith('/model ')) {
|
|
137
|
+
setInput(`/model ${selected.name}`);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
setInput(selected.name + ' ');
|
|
141
|
+
}
|
|
142
|
+
setSuggestionIndex(0);
|
|
143
|
+
setSuggestionsDismissed(true);
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!showSuggestions) {
|
|
149
|
+
if (key.upArrow && commandHistory.length > 0 && !isProcessing) {
|
|
150
|
+
const newIndex = historyIndex < commandHistory.length - 1 ? historyIndex + 1 : historyIndex;
|
|
151
|
+
setHistoryIndex(newIndex);
|
|
152
|
+
setInput(commandHistory[commandHistory.length - 1 - newIndex] || '');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (key.downArrow && historyIndex > 0) {
|
|
156
|
+
const newIndex = historyIndex - 1;
|
|
157
|
+
setHistoryIndex(newIndex);
|
|
158
|
+
setInput(commandHistory[commandHistory.length - 1 - newIndex] || '');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
// Initialize mode
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
import('../utils/PermissionManager.js').then(({ PermissionManager }) => {
|
|
166
|
+
setMode(PermissionManager.getInstance().getMode());
|
|
167
|
+
});
|
|
168
|
+
}, []);
|
|
169
|
+
const handleCommand = useCallback(async (value) => {
|
|
170
|
+
const [cmd, ...args] = value.slice(1).split(' ');
|
|
171
|
+
if (cmd === 'context') {
|
|
172
|
+
const stats = getContextStats(messages.map(m => ({ role: m.role, content: m.content })));
|
|
173
|
+
const systemMsg = {
|
|
174
|
+
id: Date.now().toString(),
|
|
175
|
+
role: 'system',
|
|
176
|
+
content: `⏺ Context: ${formatTokenUsage(stats)}`,
|
|
177
|
+
timestamp: Date.now()
|
|
178
|
+
};
|
|
179
|
+
setMessages(prev => [...prev, systemMsg]);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
if (cmd === 'report') {
|
|
183
|
+
const { getSessionReport } = await import('../agent.js');
|
|
184
|
+
const report = await getSessionReport();
|
|
185
|
+
const systemMsg = {
|
|
186
|
+
id: Date.now().toString(),
|
|
187
|
+
role: 'system',
|
|
188
|
+
content: `⏺ ${report}`,
|
|
189
|
+
timestamp: Date.now()
|
|
190
|
+
};
|
|
191
|
+
setMessages(prev => [...prev, systemMsg]);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
// Handle /model (show current)
|
|
195
|
+
if (cmd === 'model' && args.length === 0) {
|
|
196
|
+
const current = config.get('selectedModel') || 'gemini-2-flash';
|
|
197
|
+
const systemMsg = {
|
|
198
|
+
id: Date.now().toString(),
|
|
199
|
+
role: 'system',
|
|
200
|
+
content: `⏺ Current: **${current}**`,
|
|
201
|
+
timestamp: Date.now()
|
|
202
|
+
};
|
|
203
|
+
setMessages(prev => [...prev, systemMsg]);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
// Handle /model [name]
|
|
207
|
+
if (cmd === 'model' && args.length > 0) {
|
|
208
|
+
const targetModel = args[0];
|
|
209
|
+
if (['gemini-3-flash', 'gemini-3-pro', 'gemini-2-flash', 'gemini-2-pro'].includes(targetModel)) {
|
|
210
|
+
config.set('selectedModel', targetModel);
|
|
211
|
+
const systemMsg = {
|
|
212
|
+
id: Date.now().toString(),
|
|
213
|
+
role: 'system',
|
|
214
|
+
content: `⏺ Model set to **${targetModel}**`,
|
|
215
|
+
timestamp: Date.now()
|
|
216
|
+
};
|
|
217
|
+
setMessages(prev => [...prev, systemMsg]);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const systemMsg = {
|
|
221
|
+
id: Date.now().toString(),
|
|
222
|
+
role: 'system',
|
|
223
|
+
content: `⏺ Invalid model. Available: gemini-3-flash, gemini-3-pro, gemini-2-flash, gemini-2-pro`,
|
|
224
|
+
timestamp: Date.now()
|
|
225
|
+
};
|
|
226
|
+
setMessages(prev => [...prev, systemMsg]);
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
// Handle /provider (show info)
|
|
231
|
+
if (cmd === 'provider' && (args.length === 0 || args[0] !== 'key')) {
|
|
232
|
+
const googleKey = config.get('googleApiKey');
|
|
233
|
+
const maskedKey = googleKey ? `...${googleKey.slice(-4)}` : 'Not set';
|
|
234
|
+
const providerInfo = [
|
|
235
|
+
'⏺ **Provider Management**',
|
|
236
|
+
'',
|
|
237
|
+
'**Current Provider:** Google',
|
|
238
|
+
`**API Key:** ${maskedKey}`,
|
|
239
|
+
'',
|
|
240
|
+
'**Commands:**',
|
|
241
|
+
'• `/provider key [new-key]` - Update Google API key',
|
|
242
|
+
'• `/provider` - Show this info'
|
|
243
|
+
].join('\n');
|
|
244
|
+
const systemMsg = {
|
|
245
|
+
id: Date.now().toString(),
|
|
246
|
+
role: 'system',
|
|
247
|
+
content: providerInfo,
|
|
248
|
+
timestamp: Date.now()
|
|
249
|
+
};
|
|
250
|
+
setMessages(prev => [...prev, systemMsg]);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
// Handle /provider key [new-key]
|
|
254
|
+
if (cmd === 'provider' && args.length >= 2 && args[0] === 'key') {
|
|
255
|
+
const newKey = args.slice(1).join(' ');
|
|
256
|
+
config.set('googleApiKey', newKey);
|
|
257
|
+
const maskedNew = `...${newKey.slice(-4)}`;
|
|
258
|
+
const systemMsg = {
|
|
259
|
+
id: Date.now().toString(),
|
|
260
|
+
role: 'system',
|
|
261
|
+
content: `⏺ Google API key updated to ${maskedNew}`,
|
|
262
|
+
timestamp: Date.now()
|
|
263
|
+
};
|
|
264
|
+
setMessages(prev => [...prev, systemMsg]);
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
if (value.trim() === '/help' || value.trim() === '/') {
|
|
268
|
+
const helpText = `⏺ Commands:
|
|
269
|
+
/context - Show token usage
|
|
270
|
+
/report - Show decision report
|
|
271
|
+
/model - Change Model (Gemini 3)
|
|
272
|
+
/exit - Exit CAMO
|
|
273
|
+
/help - Show this help`;
|
|
274
|
+
const systemMsg = {
|
|
275
|
+
id: Date.now().toString(),
|
|
276
|
+
role: 'system',
|
|
277
|
+
content: helpText,
|
|
278
|
+
timestamp: Date.now()
|
|
279
|
+
};
|
|
280
|
+
setMessages(prev => [...prev, systemMsg]);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
if (onCommand(value)) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}, [config, messages, onCommand]);
|
|
288
|
+
const handleSubmit = useCallback(async (value) => {
|
|
289
|
+
if (!value.trim())
|
|
290
|
+
return;
|
|
291
|
+
if (value.startsWith('/')) {
|
|
292
|
+
if (await handleCommand(value)) {
|
|
293
|
+
setInput('');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
setCommandHistory(prev => [...prev, value]);
|
|
298
|
+
setHistoryIndex(-1);
|
|
299
|
+
const lines = value.split('\n');
|
|
300
|
+
const isPaste = lines.length > 10;
|
|
301
|
+
const displayContent = isPaste ? `[Pasted Text: ${lines.length} lines]` : value;
|
|
302
|
+
const userMsg = {
|
|
303
|
+
id: Date.now().toString(),
|
|
304
|
+
role: 'user',
|
|
305
|
+
content: value,
|
|
306
|
+
displayContent: displayContent,
|
|
307
|
+
timestamp: Date.now(),
|
|
308
|
+
};
|
|
309
|
+
const currentMessages = [...messagesRef.current, userMsg];
|
|
310
|
+
setIsProcessing(true);
|
|
311
|
+
setInput('');
|
|
312
|
+
setMessages(prev => [...prev, userMsg]);
|
|
313
|
+
messagesRef.current = currentMessages;
|
|
314
|
+
try {
|
|
315
|
+
const { runEliteAgent } = await import('../agent.js');
|
|
316
|
+
await fs.appendFile('debug_chat.log', `[${new Date().toISOString()}] Starting agent for: ${value}\n`);
|
|
317
|
+
setStatus('thinking...');
|
|
318
|
+
const assistantId = (Date.now() + 1).toString();
|
|
319
|
+
assistantIdRef.current = assistantId;
|
|
320
|
+
pendingChunksRef.current = '';
|
|
321
|
+
const assistantPlaceholder = {
|
|
322
|
+
id: assistantId,
|
|
323
|
+
role: 'assistant',
|
|
324
|
+
content: '',
|
|
325
|
+
timestamp: Date.now()
|
|
326
|
+
};
|
|
327
|
+
setMessages(prev => [...prev, assistantPlaceholder]);
|
|
328
|
+
messagesRef.current = [...messagesRef.current, assistantPlaceholder];
|
|
329
|
+
updateIntervalRef.current = setInterval(() => {
|
|
330
|
+
if (pendingChunksRef.current && assistantIdRef.current) {
|
|
331
|
+
const id = assistantIdRef.current;
|
|
332
|
+
const chunks = pendingChunksRef.current;
|
|
333
|
+
pendingChunksRef.current = '';
|
|
334
|
+
setMessages(prev => prev.map(m => m.id === id ? { ...m, content: m.content + chunks } : m));
|
|
335
|
+
messagesRef.current = messagesRef.current.map(m => m.id === id ? { ...m, content: m.content + chunks } : m);
|
|
336
|
+
}
|
|
337
|
+
}, 100);
|
|
338
|
+
await runEliteAgent(value, currentMessages, {
|
|
339
|
+
onChunk: (chunk) => {
|
|
340
|
+
if (status === 'thinking...')
|
|
341
|
+
setStatus('');
|
|
342
|
+
fs.appendFile('debug_chat.log', `[CHUNK] ${chunk}\n`).catch(() => { });
|
|
343
|
+
pendingChunksRef.current += chunk;
|
|
344
|
+
},
|
|
345
|
+
onHITL: onRequestHITL
|
|
346
|
+
});
|
|
347
|
+
// Clear interval before final flush
|
|
348
|
+
if (updateIntervalRef.current) {
|
|
349
|
+
clearInterval(updateIntervalRef.current);
|
|
350
|
+
updateIntervalRef.current = null;
|
|
351
|
+
}
|
|
352
|
+
// Final flush of any remaining chunks
|
|
353
|
+
if (pendingChunksRef.current && assistantIdRef.current) {
|
|
354
|
+
const id = assistantIdRef.current;
|
|
355
|
+
const pendingContent = pendingChunksRef.current;
|
|
356
|
+
await fs.appendFile('debug_chat.log', `[FINAL FLUSH] Content length: ${pendingContent.length}\n`).catch(() => { });
|
|
357
|
+
setMessages(prev => {
|
|
358
|
+
const updated = prev.map(m => m.id === id ? { ...m, content: m.content + pendingContent } : m);
|
|
359
|
+
fs.appendFile('debug_chat.log', `[FINAL FLUSH] Updated message count: ${updated.length}\n`).catch(() => { });
|
|
360
|
+
return updated;
|
|
361
|
+
});
|
|
362
|
+
messagesRef.current = messagesRef.current.map(m => m.id === id ? { ...m, content: m.content + pendingContent } : m);
|
|
363
|
+
pendingChunksRef.current = '';
|
|
364
|
+
}
|
|
365
|
+
await fs.appendFile('debug_chat.log', `[FLUSH] ${messagesRef.current[messagesRef.current.length - 1]?.content || 'NO CONTENT'}\n`).catch(() => { });
|
|
366
|
+
}
|
|
367
|
+
catch (e) {
|
|
368
|
+
await fs.appendFile('debug_chat.log', `[ERROR] ${e.message}\n`);
|
|
369
|
+
setStatus(`Error: ${e.message}`);
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
if (updateIntervalRef.current) {
|
|
373
|
+
clearInterval(updateIntervalRef.current);
|
|
374
|
+
updateIntervalRef.current = null;
|
|
375
|
+
}
|
|
376
|
+
assistantIdRef.current = null;
|
|
377
|
+
setIsProcessing(false);
|
|
378
|
+
setStatus(prev => prev.startsWith('Error') ? prev : '');
|
|
379
|
+
}
|
|
380
|
+
}, [handleCommand, onRequestHITL]);
|
|
381
|
+
const handleInputChange = useCallback((value) => {
|
|
382
|
+
setInput(value);
|
|
383
|
+
}, []);
|
|
384
|
+
const renderMessageContent = useCallback((content) => {
|
|
385
|
+
const lines = content.trim().split('\n');
|
|
386
|
+
let bufferLineCount = 0;
|
|
387
|
+
let isTextBlock = false;
|
|
388
|
+
return lines.map((line, i) => {
|
|
389
|
+
if (line.includes('[LOOP_STATUS]'))
|
|
390
|
+
return null;
|
|
391
|
+
if (line.includes('[done]'))
|
|
392
|
+
return null;
|
|
393
|
+
if (line.includes('[TOOL_STATUS]')) {
|
|
394
|
+
const text = line.replace(/\[TOOL_STATUS\]|\[\/TOOL_STATUS\]/g, '');
|
|
395
|
+
isTextBlock = false;
|
|
396
|
+
return React.createElement(Text, { key: i, color: "gray", dimColor: true },
|
|
397
|
+
" \u21B3 ",
|
|
398
|
+
text);
|
|
399
|
+
}
|
|
400
|
+
if (line.includes('[BUFFER_LINE]')) {
|
|
401
|
+
const text = line.replace(/\[BUFFER_LINE\]|\[\/BUFFER_LINE\]/g, '');
|
|
402
|
+
bufferLineCount++;
|
|
403
|
+
isTextBlock = false;
|
|
404
|
+
if (bufferLineCount <= 3) {
|
|
405
|
+
return React.createElement(Text, { key: i, color: "gray", dimColor: true },
|
|
406
|
+
" \u21B3 ",
|
|
407
|
+
text.slice(0, 70));
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
if (line.includes('[PLAN_DELTA]')) {
|
|
412
|
+
const delta = line.replace(/\[PLAN_DELTA\]|\[\/PLAN_DELTA\]/g, '');
|
|
413
|
+
isTextBlock = false;
|
|
414
|
+
return React.createElement(Text, { key: i, color: "cyan" },
|
|
415
|
+
"\u23FA ",
|
|
416
|
+
delta);
|
|
417
|
+
}
|
|
418
|
+
if (line.includes('[INTERNAL_THOUGHT]')) {
|
|
419
|
+
const thought = line.replace(/\[INTERNAL_THOUGHT\]|\[\/INTERNAL_THOUGHT\]/g, '');
|
|
420
|
+
isTextBlock = false;
|
|
421
|
+
return React.createElement(Text, { key: i, color: "gray", dimColor: true, italic: true },
|
|
422
|
+
" \uD83D\uDCAD ",
|
|
423
|
+
thought.slice(0, 60),
|
|
424
|
+
"...");
|
|
425
|
+
}
|
|
426
|
+
if (line.startsWith('⏺')) {
|
|
427
|
+
const textWithoutBullet = line.slice(1).trim();
|
|
428
|
+
isTextBlock = false;
|
|
429
|
+
return (React.createElement(Box, { key: i },
|
|
430
|
+
React.createElement(Text, { color: "green" }, "\u23FA "),
|
|
431
|
+
React.createElement(Text, { color: "white" }, textWithoutBullet)));
|
|
432
|
+
}
|
|
433
|
+
// Regular text
|
|
434
|
+
if (line.trim() && !line.includes('↳')) {
|
|
435
|
+
// Filter only specific internal tags
|
|
436
|
+
const hiddenTags = [
|
|
437
|
+
'[LOOP_STATUS]',
|
|
438
|
+
'[TOOL_STATUS]',
|
|
439
|
+
'[BUFFER_LINE]',
|
|
440
|
+
'[PLAN_DELTA]',
|
|
441
|
+
'[INTERNAL_THOUGHT]',
|
|
442
|
+
'[done]',
|
|
443
|
+
'[TASK_COMPLETE]',
|
|
444
|
+
'[ASK_USER]',
|
|
445
|
+
'[PLAN_READY]'
|
|
446
|
+
];
|
|
447
|
+
// If line starts with a known hidden tag, skip it
|
|
448
|
+
if (hiddenTags.some(tag => line.startsWith(tag)))
|
|
449
|
+
return null;
|
|
450
|
+
if (!isTextBlock) {
|
|
451
|
+
// Start of new text block - Always use large bullet
|
|
452
|
+
isTextBlock = true;
|
|
453
|
+
return (React.createElement(Box, { key: i },
|
|
454
|
+
React.createElement(Text, { color: "white" }, "\u2022 "),
|
|
455
|
+
React.createElement(Text, { color: "white" }, line)));
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
// Continuation line - no bullet, just indent
|
|
459
|
+
return (React.createElement(Box, { key: i, paddingLeft: 2 },
|
|
460
|
+
React.createElement(Text, { color: "white" }, line)));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return null;
|
|
464
|
+
}).filter(Boolean);
|
|
465
|
+
}, []);
|
|
466
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
467
|
+
React.createElement(Box, { flexDirection: "column" }, messages.map((msg, i) => (React.createElement(Box, { key: msg.id, paddingBottom: msg.role === 'assistant' ? 1 : 0, flexDirection: "column" }, msg.role === 'user' ? (React.createElement(Text, { color: "gray" },
|
|
468
|
+
"> ",
|
|
469
|
+
msg.displayContent || msg.content)) : (React.createElement(Box, { flexDirection: "column" }, renderMessageContent(msg.content))))))),
|
|
470
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
471
|
+
hitlRequest && onResolveHITL ? (React.createElement(HITLConfirmation, { request: hitlRequest, onResolve: onResolveHITL })) : (React.createElement(Box, { borderStyle: "single", borderColor: active ? "white" : "gray", flexDirection: "column", paddingX: 1 },
|
|
472
|
+
(showSuggestions || input.trim() === '?') && (React.createElement(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginBottom: 0 }, input.trim() === '?' ? (React.createElement(React.Fragment, null,
|
|
473
|
+
React.createElement(Text, { color: "white", bold: true }, "Shortcuts"),
|
|
474
|
+
React.createElement(Box, { flexDirection: "column", marginLeft: 1 },
|
|
475
|
+
React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
|
|
476
|
+
React.createElement(Text, { color: "gray" }, "Double Ctrl+C"),
|
|
477
|
+
React.createElement(Text, { color: "white" }, "Exit session")),
|
|
478
|
+
React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
|
|
479
|
+
React.createElement(Text, { color: "gray" }, "Shift+Enter"),
|
|
480
|
+
React.createElement(Text, { color: "white" }, "New line")),
|
|
481
|
+
React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
|
|
482
|
+
React.createElement(Text, { color: "gray" }, "Tab"),
|
|
483
|
+
React.createElement(Text, { color: "white" }, "Autocomplete")),
|
|
484
|
+
React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
|
|
485
|
+
React.createElement(Text, { color: "gray" }, "Ctrl+C"),
|
|
486
|
+
React.createElement(Text, { color: "white" }, "Clear input")),
|
|
487
|
+
React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
|
|
488
|
+
React.createElement(Text, { color: "gray" }, "\u2191/\u2193"),
|
|
489
|
+
React.createElement(Text, { color: "white" }, "History")),
|
|
490
|
+
React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
|
|
491
|
+
React.createElement(Text, { color: "gray" }, "/"),
|
|
492
|
+
React.createElement(Text, { color: "white" }, "Commands"))))) : (suggestions.slice(suggestionScrollTop, suggestionScrollTop + 4).map((cmd, i) => {
|
|
493
|
+
const realIndex = suggestionScrollTop + i;
|
|
494
|
+
const isSelected = realIndex === suggestionIndex;
|
|
495
|
+
return (React.createElement(Box, { key: cmd.name, flexDirection: "row", justifyContent: "space-between" },
|
|
496
|
+
React.createElement(Box, null,
|
|
497
|
+
React.createElement(Text, { color: isSelected ? 'white' : 'gray' },
|
|
498
|
+
isSelected ? '> ' : ' ',
|
|
499
|
+
cmd.name)),
|
|
500
|
+
React.createElement(Box, { marginLeft: 2 },
|
|
501
|
+
React.createElement(Text, { color: "gray", dimColor: true }, cmd.description))));
|
|
502
|
+
})))),
|
|
503
|
+
(isProcessing || status.startsWith('Error')) && status && (React.createElement(Text, { color: status.startsWith('Error') ? 'red' : 'gray', dimColor: true }, status === 'thinking...' ? `Thinking ${dots}` : status)),
|
|
504
|
+
React.createElement(Box, null,
|
|
505
|
+
React.createElement(Text, { color: active ? "white" : "gray" }, "> "),
|
|
506
|
+
active ? (React.createElement(TextInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, placeholder: "Type a message..." })) : (React.createElement(Text, { color: "gray" }, input))))),
|
|
507
|
+
!hitlRequest && active && (React.createElement(Box, { marginTop: 0, paddingLeft: 1 },
|
|
508
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "? for shortcuts"))))));
|
|
509
|
+
};
|