@calliopelabs/cli 0.4.7 ā 0.6.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 +258 -105
- package/dist/branching.d.ts +56 -0
- package/dist/branching.d.ts.map +1 -0
- package/dist/branching.js +211 -0
- package/dist/branching.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +235 -2
- package/dist/cli.js.map +1 -1
- package/dist/completion.d.ts +75 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +234 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +90 -0
- package/dist/config.js.map +1 -1
- package/dist/diff.d.ts +73 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +317 -0
- package/dist/diff.js.map +1 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +199 -0
- package/dist/errors.js.map +1 -0
- package/dist/file-watcher.d.ts +91 -0
- package/dist/file-watcher.d.ts.map +1 -0
- package/dist/file-watcher.js +269 -0
- package/dist/file-watcher.js.map +1 -0
- package/dist/files.d.ts +49 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +191 -0
- package/dist/files.js.map +1 -0
- package/dist/fuzzy-search.d.ts +75 -0
- package/dist/fuzzy-search.d.ts.map +1 -0
- package/dist/fuzzy-search.js +240 -0
- package/dist/fuzzy-search.js.map +1 -0
- package/dist/hooks.d.ts +79 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +271 -0
- package/dist/hooks.js.map +1 -0
- package/dist/keyboard.d.ts +57 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +265 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/markdown.d.ts +14 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +248 -0
- package/dist/markdown.js.map +1 -0
- package/dist/mcp.d.ts +90 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +290 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory.d.ts +104 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +394 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +67 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +289 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-tools.d.ts +51 -0
- package/dist/parallel-tools.d.ts.map +1 -0
- package/dist/parallel-tools.js +278 -0
- package/dist/parallel-tools.js.map +1 -0
- package/dist/project-config.d.ts +84 -0
- package/dist/project-config.d.ts.map +1 -0
- package/dist/project-config.js +250 -0
- package/dist/project-config.js.map +1 -0
- package/dist/providers.d.ts +10 -2
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +240 -38
- package/dist/providers.js.map +1 -1
- package/dist/risk.d.ts +31 -0
- package/dist/risk.d.ts.map +1 -0
- package/dist/risk.js +367 -0
- package/dist/risk.js.map +1 -0
- package/dist/sandbox.d.ts +49 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +347 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/skills.d.ts +71 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +383 -0
- package/dist/skills.js.map +1 -0
- package/dist/storage.d.ts +139 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +508 -0
- package/dist/storage.js.map +1 -0
- package/dist/streaming.d.ts +94 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +305 -0
- package/dist/streaming.js.map +1 -0
- package/dist/summarization.d.ts +76 -0
- package/dist/summarization.d.ts.map +1 -0
- package/dist/summarization.js +242 -0
- package/dist/summarization.js.map +1 -0
- package/dist/themes.d.ts +110 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +329 -0
- package/dist/themes.js.map +1 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +335 -1
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +56 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +105 -0
- package/dist/types.js.map +1 -1
- package/dist/ui-cli.d.ts.map +1 -1
- package/dist/ui-cli.js +1144 -24
- package/dist/ui-cli.js.map +1 -1
- package/package.json +1 -1
package/dist/ui-cli.js
CHANGED
|
@@ -14,12 +14,25 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
15
15
|
import { render, Box, Text, useInput, useApp, useStdout, Static } from 'ink';
|
|
16
16
|
import TextInput from 'ink-text-input';
|
|
17
|
+
import * as fs from 'fs';
|
|
17
18
|
import * as config from './config.js';
|
|
18
19
|
import { chat, getAvailableProviders, selectProvider } from './providers.js';
|
|
19
20
|
import { TOOLS, executeTool } from './tools.js';
|
|
20
|
-
import { getSystemPrompt, DEFAULT_MODELS } from './types.js';
|
|
21
|
+
import { getSystemPrompt, DEFAULT_MODELS, MODE_CONFIG, RISK_CONFIG, supportsVision, calculateCost } from './types.js';
|
|
21
22
|
import { getVersion, getLatestVersion, performUpgrade } from './version-check.js';
|
|
22
23
|
import { getAvailableModels } from './model-detection.js';
|
|
24
|
+
import { assessToolRisk } from './risk.js';
|
|
25
|
+
import { formatError } from './errors.js';
|
|
26
|
+
import * as storage from './storage.js';
|
|
27
|
+
import { parseFileReferences, processFilesForMessage, formatFileInfo } from './files.js';
|
|
28
|
+
import { renderMarkdown } from './markdown.js';
|
|
29
|
+
import * as mcp from './mcp.js';
|
|
30
|
+
import * as skills from './skills.js';
|
|
31
|
+
import * as memory from './memory.js';
|
|
32
|
+
import * as hooks from './hooks.js';
|
|
33
|
+
import * as modelRouter from './model-router.js';
|
|
34
|
+
import * as summarization from './summarization.js';
|
|
35
|
+
import { requiresConfirmation } from './risk.js';
|
|
23
36
|
// ============================================================================
|
|
24
37
|
// Constants
|
|
25
38
|
// ============================================================================
|
|
@@ -38,6 +51,10 @@ const TOOL_ICONS = {
|
|
|
38
51
|
write_file: 'āļø',
|
|
39
52
|
list_files: 'š',
|
|
40
53
|
think: 'š',
|
|
54
|
+
execute_code: 'ā¶ļø',
|
|
55
|
+
web_search: 'š',
|
|
56
|
+
git: 'š',
|
|
57
|
+
mermaid: 'š',
|
|
41
58
|
};
|
|
42
59
|
// ============================================================================
|
|
43
60
|
// Utility Components
|
|
@@ -47,6 +64,17 @@ function Separator() {
|
|
|
47
64
|
const width = stdout?.columns || 80;
|
|
48
65
|
return _jsx(Text, { dimColor: true, children: 'ā'.repeat(width) });
|
|
49
66
|
}
|
|
67
|
+
function ThinkingDisplay({ state }) {
|
|
68
|
+
const [frame, setFrame] = useState(0);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const timer = setInterval(() => {
|
|
71
|
+
setFrame(f => (f + 1) % SPINNER_FRAMES.length);
|
|
72
|
+
}, 80);
|
|
73
|
+
return () => clearInterval(timer);
|
|
74
|
+
}, []);
|
|
75
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { children: [" ", state.status] }), state.iteration && state.maxIterations && (_jsxs(Text, { dimColor: true, children: [" (", state.iteration, "/", state.maxIterations, ")"] }))] }), state.detail && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u21B3 ", state.detail] }) })), state.thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [_jsx(Text, { color: "magenta", children: "\uD83D\uDCAD Thinking:" }), state.thinking.split('\n').slice(0, 5).map((line, i) => (_jsxs(Text, { dimColor: true, children: [" ", line.substring(0, 80)] }, i))), state.thinking.split('\n').length > 5 && (_jsx(Text, { dimColor: true, children: " ..." }))] }))] }));
|
|
76
|
+
}
|
|
77
|
+
// Legacy simple indicator for non-agent operations
|
|
50
78
|
function ProcessingIndicator({ label }) {
|
|
51
79
|
const [frame, setFrame] = useState(0);
|
|
52
80
|
useEffect(() => {
|
|
@@ -65,7 +93,9 @@ function MessageItem({ msg }) {
|
|
|
65
93
|
case 'user':
|
|
66
94
|
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "\u203A" }), " ", msg.content] }) }));
|
|
67
95
|
case 'assistant': {
|
|
68
|
-
|
|
96
|
+
// Render markdown with syntax highlighting
|
|
97
|
+
const rendered = renderMarkdown(msg.content);
|
|
98
|
+
const lines = rendered.split('\n');
|
|
69
99
|
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "\u2727 Calliope:" }), _jsx(Text, { children: " " }), lines.map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "\u2502" }), " ", line] }, i)))] }));
|
|
70
100
|
}
|
|
71
101
|
case 'tool': {
|
|
@@ -78,7 +108,29 @@ function MessageItem({ msg }) {
|
|
|
78
108
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u256D\u2500" }), " ", icon, " ", _jsx(Text, { color: "yellow", children: toolName })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: preview })] })] }));
|
|
79
109
|
}
|
|
80
110
|
}
|
|
81
|
-
//
|
|
111
|
+
// Check for diff output from write_file
|
|
112
|
+
const isDiff = msg.content.startsWith('DIFF:');
|
|
113
|
+
if (isDiff) {
|
|
114
|
+
const lines = msg.content.split('\n');
|
|
115
|
+
const header = lines[0];
|
|
116
|
+
const isNewFile = header.includes('NEW_FILE:');
|
|
117
|
+
const filePath = isNewFile
|
|
118
|
+
? header.replace('DIFF:NEW_FILE:', '')
|
|
119
|
+
: header.replace('DIFF:', '');
|
|
120
|
+
const diffLines = lines.slice(1, 12);
|
|
121
|
+
const hasMore = lines.length > 12;
|
|
122
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u251C\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: [" ", isNewFile ? '(new file)' : '(modified)'] })] }), diffLines.map((line, i) => {
|
|
123
|
+
let color;
|
|
124
|
+
if (line.startsWith('+ '))
|
|
125
|
+
color = 'green';
|
|
126
|
+
else if (line.startsWith('- '))
|
|
127
|
+
color = 'red';
|
|
128
|
+
else if (line.startsWith('@@'))
|
|
129
|
+
color = 'cyan';
|
|
130
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), _jsxs(Text, { color: color, children: [" ", line.substring(0, 80)] })] }, i));
|
|
131
|
+
}), hasMore && _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: "..." })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2570\u2500" }), " ", _jsx(Text, { color: "green", children: "\u2713" }), " ", _jsx(Text, { dimColor: true, children: filePath })] })] }));
|
|
132
|
+
}
|
|
133
|
+
// Regular tool result
|
|
82
134
|
const lines = msg.content.split('\n').slice(0, 5);
|
|
83
135
|
const hasMore = msg.content.split('\n').length > 5;
|
|
84
136
|
const hasError = msg.content.toLowerCase().includes('error');
|
|
@@ -130,19 +182,60 @@ function UpgradePrompt({ currentVersion, latestVersion, onConfirm, onCancel }) {
|
|
|
130
182
|
});
|
|
131
183
|
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { color: "yellow", children: ["Update available: v", currentVersion, " \u2192 ", _jsxs(Text, { color: "green", children: ["v", latestVersion] })] }), _jsxs(Text, { children: ["Upgrade now? ", _jsx(Text, { color: "cyan", children: "(y/N)" })] })] }));
|
|
132
184
|
}
|
|
185
|
+
function ToolConfirmation({ toolCall, riskLevel, reason, onConfirm, onDeny }) {
|
|
186
|
+
useInput((input, key) => {
|
|
187
|
+
if (input === 'y' || input === 'Y')
|
|
188
|
+
onConfirm();
|
|
189
|
+
else if (input === 'n' || input === 'N' || key.escape)
|
|
190
|
+
onDeny();
|
|
191
|
+
});
|
|
192
|
+
const args = toolCall.arguments;
|
|
193
|
+
const preview = String(args.command || args.path || args.operation || '...');
|
|
194
|
+
const riskColor = riskLevel === 'critical' ? 'red' : 'yellow';
|
|
195
|
+
const riskIcon = riskLevel === 'critical' ? 'ā ļø' : 'ā”';
|
|
196
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: riskColor, paddingX: 1, children: [_jsxs(Text, { color: riskColor, bold: true, children: [riskIcon, " ", riskLevel.toUpperCase(), " RISK OPERATION"] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Tool: ", _jsx(Text, { color: "cyan", children: toolCall.name })] }), _jsxs(Text, { children: ["Command: ", _jsx(Text, { dimColor: true, children: preview.substring(0, 60) })] }), _jsxs(Text, { children: ["Reason: ", _jsx(Text, { dimColor: true, children: reason })] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Execute this operation? ", _jsx(Text, { color: "cyan", children: "(y/N)" })] })] }));
|
|
197
|
+
}
|
|
133
198
|
// ============================================================================
|
|
134
199
|
// Input Components
|
|
135
200
|
// ============================================================================
|
|
136
|
-
function ChatInput({ value, onChange, onSubmit, disabled }) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
201
|
+
function ChatInput({ value, onChange, onSubmit, disabled, mode }) {
|
|
202
|
+
const modeConfig = MODE_CONFIG[mode];
|
|
203
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), _jsxs(Box, { children: [_jsx(Text, { color: disabled ? 'gray' : 'cyan', children: "calliope " }), _jsx(Text, { dimColor: disabled, children: modeConfig.icon }), _jsx(Text, { dimColor: true, children: "> " }), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, focus: !disabled })] })] }));
|
|
204
|
+
}
|
|
205
|
+
// Context window limits by model (approximate)
|
|
206
|
+
const CONTEXT_LIMITS = {
|
|
207
|
+
'claude-sonnet-4': 200000,
|
|
208
|
+
'claude-opus-4': 200000,
|
|
209
|
+
'claude-3': 200000,
|
|
210
|
+
'gpt-4o': 128000,
|
|
211
|
+
'gpt-4-turbo': 128000,
|
|
212
|
+
'gpt-4': 8192,
|
|
213
|
+
'gemini-2': 1000000,
|
|
214
|
+
'gemini-1.5-pro': 1000000,
|
|
215
|
+
'gemini-1.5-flash': 1000000,
|
|
216
|
+
'llama-3.3': 128000,
|
|
217
|
+
'llama-3.1': 128000,
|
|
218
|
+
'mistral-large': 128000,
|
|
219
|
+
'default': 32000,
|
|
220
|
+
};
|
|
221
|
+
function getContextLimit(model) {
|
|
222
|
+
for (const [key, limit] of Object.entries(CONTEXT_LIMITS)) {
|
|
223
|
+
if (model.toLowerCase().includes(key.toLowerCase())) {
|
|
224
|
+
return limit;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return CONTEXT_LIMITS.default;
|
|
140
228
|
}
|
|
141
|
-
function StatusBar({ provider, model, stats }) {
|
|
229
|
+
function StatusBar({ provider, model, stats, mode, contextTokens, }) {
|
|
142
230
|
const formatTokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
|
|
143
231
|
const formatCost = (c) => c < 0.01 ? '<$0.01' : `$${c.toFixed(2)}`;
|
|
144
232
|
const displayModel = model.length > 25 ? model.slice(0, 22) + '...' : model;
|
|
145
|
-
|
|
233
|
+
const modeConfig = MODE_CONFIG[mode];
|
|
234
|
+
// Context usage indicator
|
|
235
|
+
const contextLimit = getContextLimit(model);
|
|
236
|
+
const contextPct = Math.min(100, Math.round((contextTokens / contextLimit) * 100));
|
|
237
|
+
const contextColor = contextPct > 80 ? 'red' : contextPct > 50 ? 'yellow' : 'green';
|
|
238
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), _jsxs(Text, { dimColor: true, children: [modeConfig.icon, " ", modeConfig.label, ' ā ', provider, ":", displayModel, ' ā ', _jsxs(Text, { color: contextColor, children: [formatTokens(contextTokens), "/", formatTokens(contextLimit)] }), ' ā ', formatTokens(stats.inputTokens + stats.outputTokens), " used", ' ā ', formatCost(stats.cost), ' ā ', _jsx(Text, { dimColor: true, children: "Esc: exit" })] })] }));
|
|
146
239
|
}
|
|
147
240
|
// ============================================================================
|
|
148
241
|
// Main Chat Component
|
|
@@ -155,12 +248,17 @@ function TerminalChat() {
|
|
|
155
248
|
const [input, setInput] = useState('');
|
|
156
249
|
const [messages, setMessages] = useState([]);
|
|
157
250
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
251
|
+
const [thinkingState, setThinkingState] = useState(null);
|
|
252
|
+
const [streamingResponse, setStreamingResponse] = useState('');
|
|
158
253
|
// Config state
|
|
159
254
|
const [provider, setProvider] = useState(config.get('defaultProvider'));
|
|
160
255
|
const [model, setModel] = useState(config.get('defaultModel'));
|
|
161
256
|
const [persona, setPersona] = useState(config.get('persona'));
|
|
257
|
+
const [mode, setMode] = useState('hybrid'); // Default to hybrid mode
|
|
258
|
+
const [confirmMode, setConfirmMode] = useState(true); // Require confirmation for risky ops
|
|
162
259
|
// Modal state
|
|
163
260
|
const [modalMode, setModalMode] = useState('none');
|
|
261
|
+
const [pendingToolCall, setPendingToolCall] = useState(null);
|
|
164
262
|
const [availableModels, setAvailableModels] = useState([]);
|
|
165
263
|
const [latestVersion, setLatestVersion] = useState(null);
|
|
166
264
|
// Stats
|
|
@@ -170,10 +268,61 @@ function TerminalChat() {
|
|
|
170
268
|
cost: 0,
|
|
171
269
|
messageCount: 0,
|
|
172
270
|
});
|
|
271
|
+
const [contextTokens, setContextTokens] = useState(0);
|
|
173
272
|
// LLM conversation history
|
|
174
273
|
const llmMessages = useRef([
|
|
175
274
|
{ role: 'system', content: getSystemPrompt(persona) }
|
|
176
275
|
]);
|
|
276
|
+
// Estimate context tokens (rough: ~4 chars per token)
|
|
277
|
+
const estimateContextTokens = useCallback(() => {
|
|
278
|
+
let chars = 0;
|
|
279
|
+
for (const msg of llmMessages.current) {
|
|
280
|
+
if (typeof msg.content === 'string') {
|
|
281
|
+
chars += msg.content.length;
|
|
282
|
+
}
|
|
283
|
+
else if (Array.isArray(msg.content)) {
|
|
284
|
+
for (const block of msg.content) {
|
|
285
|
+
if (block.type === 'text') {
|
|
286
|
+
chars += block.text.length;
|
|
287
|
+
}
|
|
288
|
+
else if (block.type === 'image') {
|
|
289
|
+
chars += 1000; // Images count as ~250 tokens
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return Math.round(chars / 4);
|
|
295
|
+
}, []);
|
|
296
|
+
// Session state
|
|
297
|
+
const sessionRef = useRef(null);
|
|
298
|
+
const [autoRoute, setAutoRoute] = useState(false); // Auto model routing
|
|
299
|
+
const [memoryLoaded, setMemoryLoaded] = useState(false);
|
|
300
|
+
// Initialize session and load memory on mount
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
const session = storage.getOrCreateSession(process.cwd());
|
|
303
|
+
sessionRef.current = session;
|
|
304
|
+
// Load memory context into system prompt
|
|
305
|
+
if (!memoryLoaded) {
|
|
306
|
+
const cwd = process.cwd();
|
|
307
|
+
const memoryContext = memory.buildMemoryContext(cwd);
|
|
308
|
+
if (memoryContext.trim()) {
|
|
309
|
+
// Append memory context to system prompt
|
|
310
|
+
const currentSystem = llmMessages.current[0];
|
|
311
|
+
if (currentSystem && currentSystem.role === 'system') {
|
|
312
|
+
const systemContent = typeof currentSystem.content === 'string'
|
|
313
|
+
? currentSystem.content
|
|
314
|
+
: '';
|
|
315
|
+
llmMessages.current[0] = {
|
|
316
|
+
role: 'system',
|
|
317
|
+
content: systemContent + '\n\n--- Project Context ---\n' + memoryContext,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
setMemoryLoaded(true);
|
|
322
|
+
// Execute session start hooks
|
|
323
|
+
hooks.executeHooks('session-start', {}).catch(() => { });
|
|
324
|
+
}
|
|
325
|
+
}, [memoryLoaded]);
|
|
177
326
|
// Derived values
|
|
178
327
|
const actualProvider = selectProvider(provider);
|
|
179
328
|
const actualModel = model || DEFAULT_MODELS[actualProvider];
|
|
@@ -193,7 +342,42 @@ function TerminalChat() {
|
|
|
193
342
|
switch (command) {
|
|
194
343
|
case '/help':
|
|
195
344
|
case '/h':
|
|
196
|
-
addMessage('system',
|
|
345
|
+
addMessage('system', `Commands:
|
|
346
|
+
/mode [plan|hybrid|work] - Switch modes (Shift+Tab to cycle)
|
|
347
|
+
/provider [name] - Switch AI provider
|
|
348
|
+
/model [name] - Switch model
|
|
349
|
+
/route [on|off|test] - Auto model routing by complexity
|
|
350
|
+
/persona [name] - Switch personality
|
|
351
|
+
/todo [add|done|list] - Manage TODOs
|
|
352
|
+
/plans [list|view] - View plan history
|
|
353
|
+
/session [list|info] - Session management
|
|
354
|
+
/history [search] - Chat history
|
|
355
|
+
/context [load|summary] - Context management
|
|
356
|
+
/summarize [context|compact] - Summarize/compact context
|
|
357
|
+
/clear - Clear conversation
|
|
358
|
+
/copy - Copy last response to clipboard
|
|
359
|
+
/export [file.md] - Export conversation to markdown
|
|
360
|
+
/edit - Edit and resend last message
|
|
361
|
+
/undo - Remove last exchange
|
|
362
|
+
/confirm [on|off] - Toggle risky op confirmation
|
|
363
|
+
/profile [name|save|del] - Switch/save/delete profiles
|
|
364
|
+
/mcp [add|remove|tools] - Manage MCP servers
|
|
365
|
+
/skills [add|remove] - Manage agent skills
|
|
366
|
+
/memory [init|add|show] - Project memory (CALLIOPE.md)
|
|
367
|
+
/project [init|show|run] - Project config (.calliope)
|
|
368
|
+
/find <pattern> - Fuzzy file search
|
|
369
|
+
/branch [new|switch] - Conversation branches
|
|
370
|
+
/theme [name|list] - Color themes
|
|
371
|
+
/hooks [list|add] - Pre/post tool hooks
|
|
372
|
+
/search <query> - Search conversation
|
|
373
|
+
/status - Show status
|
|
374
|
+
/config - Show config
|
|
375
|
+
/upgrade - Check for updates
|
|
376
|
+
/exit - Exit
|
|
377
|
+
|
|
378
|
+
File references: @filename, ./path, /absolute/path
|
|
379
|
+
Modes: š Plan | š Hybrid | š§ Work
|
|
380
|
+
Auto-route: ${autoRoute ? 'ON' : 'OFF'}`);
|
|
197
381
|
break;
|
|
198
382
|
case '/provider':
|
|
199
383
|
case '/p':
|
|
@@ -245,6 +429,17 @@ function TerminalChat() {
|
|
|
245
429
|
addMessage('error', `Failed to fetch models: ${e instanceof Error ? e.message : String(e)}`);
|
|
246
430
|
}
|
|
247
431
|
break;
|
|
432
|
+
case '/mode':
|
|
433
|
+
if (parts[1] && ['plan', 'hybrid', 'work'].includes(parts[1])) {
|
|
434
|
+
const m = parts[1];
|
|
435
|
+
setMode(m);
|
|
436
|
+
addMessage('system', `Mode: ${MODE_CONFIG[m].icon} ${MODE_CONFIG[m].label} - ${MODE_CONFIG[m].description}`);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
const currentConfig = MODE_CONFIG[mode];
|
|
440
|
+
addMessage('system', `Mode: ${currentConfig.icon} ${currentConfig.label}\nOptions: plan (š), hybrid (š), work (š§)\nUse Shift+Tab to cycle`);
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
248
443
|
case '/persona':
|
|
249
444
|
if (parts[1] && ['calliope', 'professional', 'minimal'].includes(parts[1])) {
|
|
250
445
|
const p = parts[1];
|
|
@@ -262,6 +457,113 @@ function TerminalChat() {
|
|
|
262
457
|
llmMessages.current = [{ role: 'system', content: getSystemPrompt(persona) }];
|
|
263
458
|
setStats({ inputTokens: 0, outputTokens: 0, cost: 0, messageCount: 0 });
|
|
264
459
|
break;
|
|
460
|
+
case '/copy': {
|
|
461
|
+
// Copy last assistant response to clipboard
|
|
462
|
+
const lastAssistant = [...messages].reverse().find(m => m.type === 'assistant');
|
|
463
|
+
if (lastAssistant) {
|
|
464
|
+
try {
|
|
465
|
+
const { execSync } = await import('child_process');
|
|
466
|
+
// Try different clipboard commands based on platform
|
|
467
|
+
const content = lastAssistant.content;
|
|
468
|
+
if (process.platform === 'darwin') {
|
|
469
|
+
execSync('pbcopy', { input: content });
|
|
470
|
+
}
|
|
471
|
+
else if (process.platform === 'win32') {
|
|
472
|
+
execSync('clip', { input: content });
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
// Linux - try xclip, xsel, or wl-copy
|
|
476
|
+
try {
|
|
477
|
+
execSync('xclip -selection clipboard', { input: content });
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
try {
|
|
481
|
+
execSync('xsel --clipboard --input', { input: content });
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
execSync('wl-copy', { input: content });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
addMessage('system', 'ā Copied to clipboard');
|
|
489
|
+
}
|
|
490
|
+
catch (e) {
|
|
491
|
+
addMessage('error', `Clipboard not available: ${e instanceof Error ? e.message : String(e)}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
addMessage('system', 'No assistant message to copy');
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
case '/export': {
|
|
500
|
+
// Export conversation to markdown
|
|
501
|
+
const filename = parts[1] || `calliope-export-${Date.now()}.md`;
|
|
502
|
+
const fs = await import('fs');
|
|
503
|
+
const path = await import('path');
|
|
504
|
+
let markdown = `# Calliope Conversation Export\n\n`;
|
|
505
|
+
markdown += `**Date:** ${new Date().toLocaleString()}\n`;
|
|
506
|
+
markdown += `**Provider:** ${actualProvider}\n`;
|
|
507
|
+
markdown += `**Model:** ${actualModel}\n\n---\n\n`;
|
|
508
|
+
for (const msg of messages) {
|
|
509
|
+
if (msg.type === 'user') {
|
|
510
|
+
markdown += `## š¤ User\n\n${msg.content}\n\n`;
|
|
511
|
+
}
|
|
512
|
+
else if (msg.type === 'assistant') {
|
|
513
|
+
markdown += `## š¤ Assistant\n\n${msg.content}\n\n`;
|
|
514
|
+
}
|
|
515
|
+
else if (msg.type === 'tool') {
|
|
516
|
+
markdown += `> š§ Tool: ${msg.content}\n\n`;
|
|
517
|
+
}
|
|
518
|
+
else if (msg.type === 'system') {
|
|
519
|
+
markdown += `> ā¹ļø ${msg.content}\n\n`;
|
|
520
|
+
}
|
|
521
|
+
else if (msg.type === 'error') {
|
|
522
|
+
markdown += `> ā ļø Error: ${msg.content}\n\n`;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const filepath = path.resolve(process.cwd(), filename);
|
|
526
|
+
fs.writeFileSync(filepath, markdown);
|
|
527
|
+
addMessage('system', `ā Exported to ${filename}`);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
case '/edit': {
|
|
531
|
+
// Edit last user message
|
|
532
|
+
const lastUserIdx = [...messages].reverse().findIndex(m => m.type === 'user');
|
|
533
|
+
if (lastUserIdx >= 0) {
|
|
534
|
+
const lastUser = messages[messages.length - 1 - lastUserIdx];
|
|
535
|
+
setInput(lastUser.content);
|
|
536
|
+
addMessage('system', 'Edit the message above and press Enter to resend');
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
addMessage('system', 'No user message to edit');
|
|
540
|
+
}
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
case '/undo': {
|
|
544
|
+
// Remove last exchange (user message + assistant response)
|
|
545
|
+
let removed = 0;
|
|
546
|
+
const newMessages = [...messages];
|
|
547
|
+
// Remove from the end until we've removed a user message
|
|
548
|
+
while (newMessages.length > 0 && removed < 10) {
|
|
549
|
+
const last = newMessages.pop();
|
|
550
|
+
removed++;
|
|
551
|
+
if (last?.type === 'user')
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
// Also remove from LLM context
|
|
555
|
+
while (llmMessages.current.length > 1) {
|
|
556
|
+
const last = llmMessages.current[llmMessages.current.length - 1];
|
|
557
|
+
if (last.role === 'user') {
|
|
558
|
+
llmMessages.current.pop();
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
llmMessages.current.pop();
|
|
562
|
+
}
|
|
563
|
+
setMessages(newMessages);
|
|
564
|
+
addMessage('system', `ā Removed last ${removed} message(s)`);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
265
567
|
case '/status':
|
|
266
568
|
case '/s':
|
|
267
569
|
addMessage('system', `${actualProvider}:${actualModel} | ${stats.messageCount} msgs | ${stats.inputTokens + stats.outputTokens} tokens`);
|
|
@@ -273,6 +575,509 @@ function TerminalChat() {
|
|
|
273
575
|
case '/loop':
|
|
274
576
|
addMessage('system', 'This feature requires legacy CLI. Run: calliope --legacy');
|
|
275
577
|
break;
|
|
578
|
+
case '/confirm':
|
|
579
|
+
if (parts[1] === 'on') {
|
|
580
|
+
setConfirmMode(true);
|
|
581
|
+
addMessage('system', 'ā Confirmation mode ON - will ask before risky operations');
|
|
582
|
+
}
|
|
583
|
+
else if (parts[1] === 'off') {
|
|
584
|
+
setConfirmMode(false);
|
|
585
|
+
addMessage('system', 'ā ļø Confirmation mode OFF - risky operations will auto-execute');
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
addMessage('system', `Confirm mode: ${confirmMode ? 'ON' : 'OFF'}\nUsage: /confirm [on|off]`);
|
|
589
|
+
}
|
|
590
|
+
break;
|
|
591
|
+
case '/profile': {
|
|
592
|
+
const subCmd = parts[1];
|
|
593
|
+
if (subCmd === 'list' || !subCmd) {
|
|
594
|
+
const profiles = config.listProfiles();
|
|
595
|
+
const active = config.getActiveProfile();
|
|
596
|
+
const list = profiles.map(p => {
|
|
597
|
+
const marker = p.name === active ? 'ā ' : ' ';
|
|
598
|
+
const tag = p.builtin ? '(built-in)' : '(custom)';
|
|
599
|
+
return `${marker}${p.name}: ${p.profile.provider}/${p.profile.model || 'default'} ${tag}`;
|
|
600
|
+
}).join('\n');
|
|
601
|
+
addMessage('system', `Profiles:\n${list}\n\nUsage: /profile <name> | /profile save <name>`);
|
|
602
|
+
}
|
|
603
|
+
else if (subCmd === 'save' && parts[2]) {
|
|
604
|
+
const name = parts[2];
|
|
605
|
+
config.saveProfile(name, {
|
|
606
|
+
provider: provider,
|
|
607
|
+
model: model,
|
|
608
|
+
persona: persona,
|
|
609
|
+
confirmMode: confirmMode,
|
|
610
|
+
});
|
|
611
|
+
addMessage('system', `ā Saved profile: ${name}`);
|
|
612
|
+
}
|
|
613
|
+
else if (subCmd === 'delete' && parts[2]) {
|
|
614
|
+
const name = parts[2];
|
|
615
|
+
if (config.deleteProfile(name)) {
|
|
616
|
+
addMessage('system', `ā Deleted profile: ${name}`);
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
addMessage('error', `Cannot delete profile: ${name} (built-in or not found)`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
// Load profile
|
|
624
|
+
const profile = config.getProfile(subCmd);
|
|
625
|
+
if (profile) {
|
|
626
|
+
setProvider(profile.provider);
|
|
627
|
+
if (profile.model)
|
|
628
|
+
setModel(profile.model);
|
|
629
|
+
setPersona(profile.persona);
|
|
630
|
+
if (profile.confirmMode !== undefined)
|
|
631
|
+
setConfirmMode(profile.confirmMode);
|
|
632
|
+
config.setActiveProfile(subCmd);
|
|
633
|
+
addMessage('system', `ā Loaded profile: ${subCmd} (${profile.provider}/${profile.model || 'default'})`);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
addMessage('error', `Profile not found: ${subCmd}\nBuilt-in: fast, smart, cheap, local`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
case '/mcp': {
|
|
642
|
+
const subCmd = parts[1];
|
|
643
|
+
if (subCmd === 'list' || !subCmd) {
|
|
644
|
+
const servers = mcp.listServers();
|
|
645
|
+
if (servers.length === 0) {
|
|
646
|
+
addMessage('system', 'No MCP servers registered.\n\nUsage:\n /mcp add <url> - Register MCP server\n /mcp remove <id> - Remove server');
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
const list = servers.map(s => {
|
|
650
|
+
const status = s.status === 'connected' ? 'š¢' : s.status === 'error' ? 'š“' : 'āŖ';
|
|
651
|
+
return `${status} ${s.name} (${s.tools.length} tools)\n ${s.url}`;
|
|
652
|
+
}).join('\n\n');
|
|
653
|
+
addMessage('system', `MCP Servers:\n\n${list}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
else if (subCmd === 'add' && parts[2]) {
|
|
657
|
+
const url = parts[2];
|
|
658
|
+
addMessage('system', `Registering MCP server: ${url}...`);
|
|
659
|
+
try {
|
|
660
|
+
const server = await mcp.registerServer(url);
|
|
661
|
+
addMessage('system', `ā Registered: ${server.name} (${server.tools.length} tools)`);
|
|
662
|
+
}
|
|
663
|
+
catch (e) {
|
|
664
|
+
addMessage('error', `Failed to register: ${e instanceof Error ? e.message : String(e)}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
|
|
668
|
+
if (mcp.unregisterServer(parts[2])) {
|
|
669
|
+
addMessage('system', 'ā Server removed');
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
addMessage('error', 'Server not found');
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
else if (subCmd === 'refresh') {
|
|
676
|
+
const servers = mcp.listServers();
|
|
677
|
+
let connected = 0;
|
|
678
|
+
for (const s of servers) {
|
|
679
|
+
const updated = await mcp.refreshServer(s.id);
|
|
680
|
+
if (updated?.status === 'connected')
|
|
681
|
+
connected++;
|
|
682
|
+
}
|
|
683
|
+
addMessage('system', `Refreshed ${servers.length} servers (${connected} connected)`);
|
|
684
|
+
}
|
|
685
|
+
else if (subCmd === 'tools') {
|
|
686
|
+
const tools = mcp.getMCPTools();
|
|
687
|
+
if (tools.length === 0) {
|
|
688
|
+
addMessage('system', 'No MCP tools available. Add servers with /mcp add <url>');
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
const list = tools.map(t => `⢠${t.name}\n ${t.description}`).join('\n\n');
|
|
692
|
+
addMessage('system', `MCP Tools:\n\n${list}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
addMessage('system', 'Usage: /mcp [list|add <url>|remove <id>|refresh|tools]');
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
case '/skills': {
|
|
701
|
+
const subCmd = parts[1];
|
|
702
|
+
if (subCmd === 'list' || !subCmd) {
|
|
703
|
+
const allSkills = skills.getSkills();
|
|
704
|
+
if (allSkills.length === 0) {
|
|
705
|
+
addMessage('system', 'No skills installed.\n\nUsage:\n /skills add <name> - Install from agentskills.io\n /skills add <github-url> - Install from GitHub\n /skills add <path> - Install from local directory');
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
const list = allSkills.map(s => {
|
|
709
|
+
const src = s.source === 'github' ? '(GitHub)' : s.source === 'registry' ? '(agentskills.io)' : '(local)';
|
|
710
|
+
return `⢠${s.metadata.name} ${src}\n ${s.metadata.description.substring(0, 80)}...`;
|
|
711
|
+
}).join('\n\n');
|
|
712
|
+
addMessage('system', `Installed Skills:\n\n${list}`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
else if (subCmd === 'add' && parts[2]) {
|
|
716
|
+
const source = parts[2];
|
|
717
|
+
addMessage('system', `Installing skill: ${source}...`);
|
|
718
|
+
try {
|
|
719
|
+
let skill;
|
|
720
|
+
if (source.startsWith('http')) {
|
|
721
|
+
skill = await skills.installFromGithub(source);
|
|
722
|
+
}
|
|
723
|
+
else if (fs.existsSync(source)) {
|
|
724
|
+
skill = skills.installLocalSkill(source);
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
skill = await skills.installFromRegistry(source);
|
|
728
|
+
}
|
|
729
|
+
if (skill) {
|
|
730
|
+
addMessage('system', `ā Installed: ${skill.metadata.name}`);
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
addMessage('error', 'Failed to install skill');
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
catch (e) {
|
|
737
|
+
addMessage('error', `Failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
|
|
741
|
+
if (skills.uninstallSkill(parts[2])) {
|
|
742
|
+
addMessage('system', 'ā Skill removed');
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
addMessage('error', 'Skill not found');
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
else if (subCmd === 'info' && parts[2]) {
|
|
749
|
+
const skill = skills.getSkill(parts[2]);
|
|
750
|
+
if (skill) {
|
|
751
|
+
let info = `# ${skill.metadata.name}\n\n`;
|
|
752
|
+
info += `${skill.metadata.description}\n\n`;
|
|
753
|
+
if (skill.metadata.compatibility)
|
|
754
|
+
info += `Compatibility: ${skill.metadata.compatibility}\n`;
|
|
755
|
+
if (skill.metadata.license)
|
|
756
|
+
info += `License: ${skill.metadata.license}\n`;
|
|
757
|
+
if (skill.sourceUrl)
|
|
758
|
+
info += `Source: ${skill.sourceUrl}\n`;
|
|
759
|
+
addMessage('system', info);
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
addMessage('error', 'Skill not found');
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
addMessage('system', 'Usage: /skills [list|add <source>|remove <name>|info <name>]');
|
|
767
|
+
}
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
case '/memory': {
|
|
771
|
+
const memory = await import('./memory.js');
|
|
772
|
+
const subCmd = parts[1];
|
|
773
|
+
const cwd = process.cwd();
|
|
774
|
+
if (subCmd === 'init') {
|
|
775
|
+
const memPath = memory.initProjectMemory(cwd);
|
|
776
|
+
addMessage('system', `Created: ${memPath}\nEdit the file to add context and preferences.`);
|
|
777
|
+
}
|
|
778
|
+
else if (subCmd === 'show' || !subCmd) {
|
|
779
|
+
const memPath = memory.findProjectMemory(cwd);
|
|
780
|
+
if (!memPath) {
|
|
781
|
+
addMessage('system', 'No CALLIOPE.md found.\nRun /memory init to create one.');
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
const mem = memory.loadMemory(memPath);
|
|
785
|
+
let info = `Memory: ${memPath}\n\n`;
|
|
786
|
+
if (mem.context.length)
|
|
787
|
+
info += `**Context:**\n${mem.context.map(c => ` - ${c}`).join('\n')}\n\n`;
|
|
788
|
+
if (mem.preferences.length)
|
|
789
|
+
info += `**Preferences:**\n${mem.preferences.map(p => ` - ${p}`).join('\n')}\n\n`;
|
|
790
|
+
if (mem.history.length)
|
|
791
|
+
info += `**History:**\n${mem.history.slice(-5).map(h => ` - ${h}`).join('\n')}\n`;
|
|
792
|
+
addMessage('system', info);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
else if (subCmd === 'add' && parts[2]) {
|
|
796
|
+
const type = parts[2];
|
|
797
|
+
const content = parts.slice(3).join(' ');
|
|
798
|
+
if (!content) {
|
|
799
|
+
addMessage('error', 'Usage: /memory add <type> <content>');
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
let memPath = memory.findProjectMemory(cwd);
|
|
803
|
+
if (!memPath) {
|
|
804
|
+
memPath = memory.initProjectMemory(cwd);
|
|
805
|
+
}
|
|
806
|
+
memory.addMemoryEntry(memPath, {
|
|
807
|
+
type,
|
|
808
|
+
content,
|
|
809
|
+
timestamp: new Date().toISOString().split('T')[0],
|
|
810
|
+
});
|
|
811
|
+
addMessage('system', `Added ${type}: ${content}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
else if (subCmd === 'remove' && parts[2]) {
|
|
815
|
+
const type = parts[2];
|
|
816
|
+
const content = parts.slice(3).join(' ');
|
|
817
|
+
const memPath = memory.findProjectMemory(cwd);
|
|
818
|
+
if (memPath && memory.removeMemoryEntry(memPath, type, content)) {
|
|
819
|
+
addMessage('system', `Removed matching ${type}`);
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
addMessage('error', 'Entry not found');
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
else if (subCmd === 'global') {
|
|
826
|
+
const globalMem = memory.getGlobalMemory();
|
|
827
|
+
let info = 'Global Memory:\n\n';
|
|
828
|
+
if (globalMem.preferences.length)
|
|
829
|
+
info += `**Preferences:**\n${globalMem.preferences.map(p => ` - ${p}`).join('\n')}\n`;
|
|
830
|
+
if (globalMem.notes.length)
|
|
831
|
+
info += `**Notes:**\n${globalMem.notes.map(n => ` - ${n}`).join('\n')}\n`;
|
|
832
|
+
addMessage('system', info || 'No global memories yet.');
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
addMessage('system', 'Usage: /memory [init|show|add <type> <text>|remove <type> <text>|global]');
|
|
836
|
+
}
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
case '/find': {
|
|
840
|
+
const fuzzy = await import('./fuzzy-search.js');
|
|
841
|
+
const query = parts.slice(1).join(' ');
|
|
842
|
+
if (!query) {
|
|
843
|
+
addMessage('system', 'Usage: /find <pattern>\nFuzzy search for files');
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
const results = fuzzy.searchWithHighlight(process.cwd(), query, { maxResults: 20 });
|
|
847
|
+
if (results.length === 0) {
|
|
848
|
+
addMessage('system', 'No files found');
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
const list = results.map((r, i) => `${i + 1}. ${r.highlighted}`).join('\n');
|
|
852
|
+
addMessage('system', `Found ${results.length} files:\n\n${list}`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
case '/branch': {
|
|
858
|
+
const branching = await import('./branching.js');
|
|
859
|
+
const subCmd = parts[1];
|
|
860
|
+
const sessionId = `session_${Date.now()}`; // Would use actual session ID
|
|
861
|
+
if (subCmd === 'list' || !subCmd) {
|
|
862
|
+
const tree = branching.getBranchTree(sessionId);
|
|
863
|
+
addMessage('system', `Branches:\n${tree}`);
|
|
864
|
+
}
|
|
865
|
+
else if (subCmd === 'new' && parts[2]) {
|
|
866
|
+
const branch = branching.createBranch(sessionId, parts[2], llmMessages.current, parts.slice(3).join(' '));
|
|
867
|
+
addMessage('system', `Created branch: ${branch.name}`);
|
|
868
|
+
}
|
|
869
|
+
else if (subCmd === 'switch' && parts[2]) {
|
|
870
|
+
const msgs = branching.switchBranch(sessionId, parts[2], llmMessages.current);
|
|
871
|
+
if (msgs) {
|
|
872
|
+
llmMessages.current = msgs;
|
|
873
|
+
addMessage('system', `Switched to branch: ${parts[2]}`);
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
addMessage('error', 'Branch not found');
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
else if (subCmd === 'delete' && parts[2]) {
|
|
880
|
+
if (branching.deleteBranch(sessionId, parts[2])) {
|
|
881
|
+
addMessage('system', 'Branch deleted');
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
addMessage('error', 'Cannot delete branch');
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
addMessage('system', 'Usage: /branch [list|new <name>|switch <name>|delete <name>]');
|
|
889
|
+
}
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
case '/theme': {
|
|
893
|
+
const themes = await import('./themes.js');
|
|
894
|
+
const subCmd = parts[1];
|
|
895
|
+
if (subCmd === 'list' || !subCmd) {
|
|
896
|
+
const list = themes.listThemes();
|
|
897
|
+
const current = themes.getCurrentThemeName();
|
|
898
|
+
const formatted = list.map(t => {
|
|
899
|
+
const marker = t.name === current ? ' *' : '';
|
|
900
|
+
const custom = t.custom ? ' (custom)' : '';
|
|
901
|
+
return ` ${t.name}${marker}${custom} - ${t.description || 'No description'}`;
|
|
902
|
+
}).join('\n');
|
|
903
|
+
addMessage('system', `Available themes:\n${formatted}`);
|
|
904
|
+
}
|
|
905
|
+
else if (themes.setCurrentTheme(subCmd)) {
|
|
906
|
+
themes.clearThemeCache();
|
|
907
|
+
addMessage('system', `Theme set to: ${subCmd}`);
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
addMessage('error', `Theme not found: ${subCmd}`);
|
|
911
|
+
}
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
case '/hooks': {
|
|
915
|
+
const hooks = await import('./hooks.js');
|
|
916
|
+
const subCmd = parts[1];
|
|
917
|
+
if (subCmd === 'list' || !subCmd) {
|
|
918
|
+
addMessage('system', hooks.listHooksFormatted());
|
|
919
|
+
}
|
|
920
|
+
else if (subCmd === 'add' && parts[2]) {
|
|
921
|
+
const event = parts[2];
|
|
922
|
+
const command = parts.slice(3).join(' ');
|
|
923
|
+
if (!command) {
|
|
924
|
+
addMessage('system', 'Usage: /hooks add <event> <command>');
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
hooks.addHook({ event, name: `Hook for ${event}`, command, enabled: true, async: false });
|
|
928
|
+
addMessage('system', 'Hook added');
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
else if (subCmd === 'init') {
|
|
932
|
+
hooks.initDefaultHooks();
|
|
933
|
+
addMessage('system', 'Default hooks initialized');
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
addMessage('system', 'Usage: /hooks [list|add <event> <command>|init]');
|
|
937
|
+
}
|
|
938
|
+
break;
|
|
939
|
+
}
|
|
940
|
+
case '/search': {
|
|
941
|
+
const query = parts.slice(1).join(' ');
|
|
942
|
+
if (!query) {
|
|
943
|
+
addMessage('system', 'Usage: /search <query>\nSearch conversation history');
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
const lower = query.toLowerCase();
|
|
947
|
+
const matches = messages.filter(m => m.content.toLowerCase().includes(lower));
|
|
948
|
+
if (matches.length === 0) {
|
|
949
|
+
addMessage('system', 'No matches found');
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
const results = matches.slice(-10).map(m => {
|
|
953
|
+
const preview = m.content.slice(0, 100).replace(/\n/g, ' ');
|
|
954
|
+
return `[${m.type}] ${preview}...`;
|
|
955
|
+
}).join('\n\n');
|
|
956
|
+
addMessage('system', `Found ${matches.length} matches:\n\n${results}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
case '/project': {
|
|
962
|
+
const projectConfig = await import('./project-config.js');
|
|
963
|
+
const subCmd = parts[1];
|
|
964
|
+
const cwd = process.cwd();
|
|
965
|
+
if (subCmd === 'init') {
|
|
966
|
+
const configPath = projectConfig.createProjectConfig(cwd);
|
|
967
|
+
addMessage('system', `Created project config: ${configPath}\nEdit the file to customize settings.`);
|
|
968
|
+
}
|
|
969
|
+
else if (subCmd === 'show' || !subCmd) {
|
|
970
|
+
const configPath = projectConfig.findProjectConfig(cwd);
|
|
971
|
+
if (!configPath) {
|
|
972
|
+
addMessage('system', 'No project config found.\nRun /project init to create one.');
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
const cfg = projectConfig.loadProjectConfig(configPath);
|
|
976
|
+
if (cfg) {
|
|
977
|
+
let info = `Config: ${configPath}\n\n`;
|
|
978
|
+
if (cfg.project)
|
|
979
|
+
info += `Project: ${cfg.project}\n`;
|
|
980
|
+
if (cfg.provider)
|
|
981
|
+
info += `Provider: ${cfg.provider}\n`;
|
|
982
|
+
if (cfg.model)
|
|
983
|
+
info += `Model: ${cfg.model}\n`;
|
|
984
|
+
if (cfg.tech?.length)
|
|
985
|
+
info += `Tech: ${cfg.tech.join(', ')}\n`;
|
|
986
|
+
if (cfg.conventions?.length)
|
|
987
|
+
info += `\nConventions:\n${cfg.conventions.map(c => ` - ${c}`).join('\n')}\n`;
|
|
988
|
+
if (cfg.commands)
|
|
989
|
+
info += `\nCommands: ${Object.keys(cfg.commands).join(', ')}\n`;
|
|
990
|
+
addMessage('system', info);
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
addMessage('error', 'Failed to parse config');
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
else if (subCmd === 'run' && parts[2]) {
|
|
998
|
+
const configPath = projectConfig.findProjectConfig(cwd);
|
|
999
|
+
const cfg = configPath ? projectConfig.loadProjectConfig(configPath) : null;
|
|
1000
|
+
const cmdName = parts[2];
|
|
1001
|
+
if (cfg?.commands?.[cmdName]) {
|
|
1002
|
+
addMessage('system', `Running: ${cfg.commands[cmdName]}`);
|
|
1003
|
+
// Queue the command to run
|
|
1004
|
+
const { spawn } = await import('child_process');
|
|
1005
|
+
const proc = spawn('sh', ['-c', cfg.commands[cmdName]], { cwd, stdio: 'pipe' });
|
|
1006
|
+
let output = '';
|
|
1007
|
+
proc.stdout?.on('data', (d) => output += d.toString());
|
|
1008
|
+
proc.stderr?.on('data', (d) => output += d.toString());
|
|
1009
|
+
proc.on('close', (code) => {
|
|
1010
|
+
addMessage('system', `Exit ${code}\n${output}`);
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
addMessage('error', `Command not found: ${cmdName}`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
addMessage('system', 'Usage: /project [init|show|run <cmd>]');
|
|
1019
|
+
}
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
case '/route':
|
|
1023
|
+
case '/autoroute': {
|
|
1024
|
+
if (parts[1] === 'on') {
|
|
1025
|
+
setAutoRoute(true);
|
|
1026
|
+
addMessage('system', 'ā Auto-routing ON - model selected based on task complexity');
|
|
1027
|
+
}
|
|
1028
|
+
else if (parts[1] === 'off') {
|
|
1029
|
+
setAutoRoute(false);
|
|
1030
|
+
addMessage('system', 'ā Auto-routing OFF - using fixed model');
|
|
1031
|
+
}
|
|
1032
|
+
else if (parts[1] === 'test' && parts[2]) {
|
|
1033
|
+
const testMsg = parts.slice(2).join(' ');
|
|
1034
|
+
const decision = modelRouter.routeRequest(testMsg, actualProvider);
|
|
1035
|
+
addMessage('system', `Route test: ${decision.tier} tier (${decision.complexity})\nModel: ${decision.model.model}\nReason: ${decision.reason}\nConfidence: ${Math.round(decision.confidence * 100)}%`);
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
const tiers = modelRouter.getAllTiers(actualProvider);
|
|
1039
|
+
addMessage('system', `Auto-route: ${autoRoute ? 'ON' : 'OFF'}\n\nModel tiers for ${actualProvider}:\n fast: ${tiers.fast.model}\n balanced: ${tiers.balanced.model}\n smart: ${tiers.smart.model}\n\nUsage: /route [on|off|test <message>]`);
|
|
1040
|
+
}
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
case '/summarize': {
|
|
1044
|
+
const subCmd = parts[1];
|
|
1045
|
+
if (subCmd === 'context' || !subCmd) {
|
|
1046
|
+
const msgCount = llmMessages.current.length;
|
|
1047
|
+
if (msgCount < 5) {
|
|
1048
|
+
addMessage('system', 'Not enough messages to summarize.');
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
const summary = summarization.extractKeyInfo(llmMessages.current);
|
|
1052
|
+
let info = 'Context Summary:\n\n';
|
|
1053
|
+
if (summary.topics.length)
|
|
1054
|
+
info += `**Topics:** ${summary.topics.join(', ')}\n`;
|
|
1055
|
+
if (summary.decisions.length)
|
|
1056
|
+
info += `**Decisions:**\n${summary.decisions.map(d => ` - ${d}`).join('\n')}\n`;
|
|
1057
|
+
if (summary.actions.length)
|
|
1058
|
+
info += `**Actions:**\n${summary.actions.map(a => ` - ${a}`).join('\n')}\n`;
|
|
1059
|
+
if (summary.codeChanges.length)
|
|
1060
|
+
info += `**Code Changes:**\n${summary.codeChanges.slice(0, 5).map(c => ` - ${c}`).join('\n')}\n`;
|
|
1061
|
+
addMessage('system', info || 'No key information extracted.');
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
else if (subCmd === 'compact') {
|
|
1065
|
+
// Summarize and compact the conversation
|
|
1066
|
+
const result = summarization.summarizeConversation(llmMessages.current, { maxTokens: 50000 });
|
|
1067
|
+
if (result.summarizedCount > 0) {
|
|
1068
|
+
llmMessages.current = result.messages;
|
|
1069
|
+
setContextTokens(estimateContextTokens());
|
|
1070
|
+
addMessage('system', `ā Compacted ${result.summarizedCount} messages (${result.originalTokens} ā ${result.reducedTokens} tokens)`);
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
addMessage('system', 'Context already within limits, no compaction needed.');
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
addMessage('system', 'Usage: /summarize [context|compact]');
|
|
1078
|
+
}
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
276
1081
|
case '/upgrade':
|
|
277
1082
|
addMessage('system', 'Checking for updates...');
|
|
278
1083
|
try {
|
|
@@ -297,6 +1102,166 @@ function TerminalChat() {
|
|
|
297
1102
|
addMessage('error', `Failed to check for updates: ${e instanceof Error ? e.message : String(e)}`);
|
|
298
1103
|
}
|
|
299
1104
|
break;
|
|
1105
|
+
case '/session':
|
|
1106
|
+
if (parts[1] === 'list') {
|
|
1107
|
+
const sessions = storage.listSessions(5);
|
|
1108
|
+
if (sessions.length === 0) {
|
|
1109
|
+
addMessage('system', 'No previous sessions found.');
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
const list = sessions.map(s => `${s.projectName} (${new Date(s.lastAccessedAt).toLocaleDateString()}) - ${s.messageCount} msgs`).join('\n');
|
|
1113
|
+
addMessage('system', `Recent sessions:\n${list}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
else if (parts[1] === 'info') {
|
|
1117
|
+
const session = sessionRef.current;
|
|
1118
|
+
if (session) {
|
|
1119
|
+
addMessage('system', `Session: ${session.projectName}\nCreated: ${new Date(session.createdAt).toLocaleString()}\nMessages: ${session.messageCount}`);
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
addMessage('system', 'No active session.');
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
addMessage('system', 'Usage: /session [list|info]');
|
|
1127
|
+
}
|
|
1128
|
+
break;
|
|
1129
|
+
case '/todo': {
|
|
1130
|
+
const subCommand = parts[1];
|
|
1131
|
+
if (subCommand === 'add' && parts.length > 2) {
|
|
1132
|
+
const content = parts.slice(2).join(' ');
|
|
1133
|
+
const isGlobal = content.includes('--global');
|
|
1134
|
+
const isHigh = content.includes('--priority') && content.includes('high');
|
|
1135
|
+
const cleanContent = content.replace(/--global|--priority\s*\w+/g, '').trim();
|
|
1136
|
+
const todo = storage.addTodo(cleanContent, {
|
|
1137
|
+
global: isGlobal,
|
|
1138
|
+
priority: isHigh ? 'high' : 'normal',
|
|
1139
|
+
});
|
|
1140
|
+
addMessage('system', `ā TODO added (#${todo.id.slice(-4)}${isGlobal ? ', global' : ''})`);
|
|
1141
|
+
}
|
|
1142
|
+
else if (subCommand === 'done' && parts[2]) {
|
|
1143
|
+
const id = parts[2];
|
|
1144
|
+
const todos = [...storage.getSessionTodos(), ...storage.getGlobalTodos()];
|
|
1145
|
+
const todo = todos.find(t => t.id.endsWith(id) || t.id === id);
|
|
1146
|
+
if (todo) {
|
|
1147
|
+
storage.updateTodo(todo.id, { status: 'completed' });
|
|
1148
|
+
addMessage('system', `ā TODO #${id} marked done`);
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
addMessage('error', `TODO #${id} not found`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
else if (subCommand === 'list' || !subCommand) {
|
|
1155
|
+
const sessionTodos = storage.getSessionTodos();
|
|
1156
|
+
const globalTodos = storage.getGlobalTodos();
|
|
1157
|
+
const pending = [...sessionTodos, ...globalTodos].filter(t => t.status !== 'completed');
|
|
1158
|
+
const completed = [...sessionTodos, ...globalTodos].filter(t => t.status === 'completed').slice(-3);
|
|
1159
|
+
if (pending.length === 0 && completed.length === 0) {
|
|
1160
|
+
addMessage('system', 'No TODOs. Use /todo add <task> to create one.');
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
let output = 'š TODOs:\n';
|
|
1164
|
+
if (pending.length > 0) {
|
|
1165
|
+
output += pending.map(t => ` ${t.priority === 'high' ? '!' : 'ā”'} #${t.id.slice(-4)} ${t.content}`).join('\n');
|
|
1166
|
+
}
|
|
1167
|
+
if (completed.length > 0) {
|
|
1168
|
+
output += '\n\nCompleted:\n' + completed.map(t => ` ā #${t.id.slice(-4)} ${t.content}`).join('\n');
|
|
1169
|
+
}
|
|
1170
|
+
addMessage('system', output);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
addMessage('system', 'Usage: /todo [add <task>|done <id>|list]');
|
|
1175
|
+
}
|
|
1176
|
+
break;
|
|
1177
|
+
}
|
|
1178
|
+
case '/plans': {
|
|
1179
|
+
const subCommand = parts[1];
|
|
1180
|
+
if (subCommand === 'list' || !subCommand) {
|
|
1181
|
+
const plans = storage.getPlans();
|
|
1182
|
+
if (plans.length === 0) {
|
|
1183
|
+
addMessage('system', 'No plans yet. Plans are created in hybrid mode.');
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
const list = plans.slice(0, 5).map(p => `${p.status === 'completed' ? 'ā' : 'ā'} ${p.id.slice(-4)}: ${p.title}`).join('\n');
|
|
1187
|
+
addMessage('system', `š Plans:\n${list}`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
else if (subCommand === 'view' && parts[2]) {
|
|
1191
|
+
const plans = storage.getPlans();
|
|
1192
|
+
const plan = plans.find(p => p.id.endsWith(parts[2]) || p.id === parts[2]);
|
|
1193
|
+
if (plan) {
|
|
1194
|
+
const phases = plan.phases.map(ph => ` ${ph.status === 'completed' ? 'ā' : 'ā'} ${ph.name} (${ph.risk} risk)`).join('\n');
|
|
1195
|
+
addMessage('system', `Plan: ${plan.title}\nStatus: ${plan.status}\n\nPhases:\n${phases}`);
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
addMessage('error', `Plan #${parts[2]} not found`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
addMessage('system', 'Usage: /plans [list|view <id>]');
|
|
1203
|
+
}
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
case '/history': {
|
|
1207
|
+
const subCommand = parts[1];
|
|
1208
|
+
if (subCommand === 'search' && parts[2]) {
|
|
1209
|
+
const query = parts.slice(2).join(' ');
|
|
1210
|
+
const results = storage.searchChatHistory(query);
|
|
1211
|
+
if (results.length === 0) {
|
|
1212
|
+
addMessage('system', `No matches for "${query}"`);
|
|
1213
|
+
}
|
|
1214
|
+
else {
|
|
1215
|
+
const list = results.slice(-5).map(m => `${new Date(m.timestamp).toLocaleTimeString()}: ${m.content.substring(0, 60)}...`).join('\n');
|
|
1216
|
+
addMessage('system', `š Found ${results.length} matches:\n${list}`);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
else if (subCommand === 'clear') {
|
|
1220
|
+
addMessage('system', 'History is preserved per session. Start a new session for fresh history.');
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
const history = storage.getChatHistory(5);
|
|
1224
|
+
if (history.length === 0) {
|
|
1225
|
+
addMessage('system', 'No chat history yet.');
|
|
1226
|
+
}
|
|
1227
|
+
else {
|
|
1228
|
+
const list = history.map(m => `${m.role}: ${m.content.substring(0, 50)}...`).join('\n');
|
|
1229
|
+
addMessage('system', `Recent history:\n${list}\n\nUse /history search <query> to search.`);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
case '/context': {
|
|
1235
|
+
const subCommand = parts[1];
|
|
1236
|
+
if (subCommand === 'load') {
|
|
1237
|
+
const limit = parseInt(parts[2]) || 20;
|
|
1238
|
+
const history = storage.getChatHistory(limit);
|
|
1239
|
+
if (history.length > 0) {
|
|
1240
|
+
// Load history into LLM context
|
|
1241
|
+
for (const msg of history) {
|
|
1242
|
+
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
1243
|
+
llmMessages.current.push({
|
|
1244
|
+
role: msg.role,
|
|
1245
|
+
content: msg.content,
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
addMessage('system', `ā Loaded ${history.length} messages into context`);
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
addMessage('system', 'No history to load.');
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
else if (subCommand === 'summary') {
|
|
1256
|
+
const msgCount = llmMessages.current.length;
|
|
1257
|
+
const estTokens = llmMessages.current.reduce((sum, m) => sum + (m.content?.length || 0) / 4, 0);
|
|
1258
|
+
addMessage('system', `Context: ${msgCount} messages (~${Math.round(estTokens)} tokens)`);
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
addMessage('system', 'Usage: /context [load [n]|summary]');
|
|
1262
|
+
}
|
|
1263
|
+
break;
|
|
1264
|
+
}
|
|
300
1265
|
case '/exit':
|
|
301
1266
|
case '/quit':
|
|
302
1267
|
case '/q':
|
|
@@ -307,19 +1272,55 @@ function TerminalChat() {
|
|
|
307
1272
|
}
|
|
308
1273
|
}, [actualProvider, actualModel, persona, stats, addMessage, exit]);
|
|
309
1274
|
// Run agent with user prompt
|
|
310
|
-
const runAgent = useCallback(async (
|
|
311
|
-
llmMessages.current.push({ role: 'user', content
|
|
1275
|
+
const runAgent = useCallback(async (content) => {
|
|
1276
|
+
llmMessages.current.push({ role: 'user', content });
|
|
312
1277
|
setStats(s => ({ ...s, messageCount: s.messageCount + 1 }));
|
|
1278
|
+
setStreamingResponse('');
|
|
1279
|
+
// Auto-route to appropriate model based on task complexity
|
|
1280
|
+
let effectiveModel = model;
|
|
1281
|
+
if (autoRoute && typeof content === 'string') {
|
|
1282
|
+
const routeDecision = modelRouter.routeRequest(content, provider, {
|
|
1283
|
+
messageCount: stats.messageCount,
|
|
1284
|
+
hasCode: content.includes('```') || /\.(ts|js|py|go|rs|java)/.test(content),
|
|
1285
|
+
});
|
|
1286
|
+
effectiveModel = routeDecision.model.model;
|
|
1287
|
+
if (effectiveModel !== model) {
|
|
1288
|
+
addMessage('system', `[Auto-route: ${routeDecision.tier} tier - ${routeDecision.reason}]`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
313
1291
|
const maxIterations = config.get('maxIterations');
|
|
314
1292
|
for (let i = 0; i < maxIterations; i++) {
|
|
315
1293
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
1294
|
+
// Update thinking state for LLM call
|
|
1295
|
+
setThinkingState({
|
|
1296
|
+
status: i === 0 ? 'Analyzing request...' : 'Processing response...',
|
|
1297
|
+
detail: `Iteration ${i + 1}/${maxIterations}`,
|
|
1298
|
+
iteration: i + 1,
|
|
1299
|
+
maxIterations,
|
|
1300
|
+
});
|
|
1301
|
+
// Streaming callback for final response
|
|
1302
|
+
const onToken = (token) => {
|
|
1303
|
+
setThinkingState(null); // Clear thinking when streaming starts
|
|
1304
|
+
setStreamingResponse(prev => prev + token);
|
|
1305
|
+
};
|
|
1306
|
+
// Retry callback for error recovery
|
|
1307
|
+
const onRetry = (attempt, error, delayMs) => {
|
|
1308
|
+
setThinkingState({
|
|
1309
|
+
status: `Retrying... (attempt ${attempt + 1})`,
|
|
1310
|
+
detail: `${error.message.substring(0, 40)}... Waiting ${Math.round(delayMs / 1000)}s`,
|
|
1311
|
+
iteration: i + 1,
|
|
1312
|
+
maxIterations,
|
|
1313
|
+
});
|
|
1314
|
+
};
|
|
1315
|
+
const response = await chat(provider, llmMessages.current, TOOLS, effectiveModel, onToken, onRetry);
|
|
1316
|
+
// Update token stats and cost
|
|
318
1317
|
if (response.usage) {
|
|
1318
|
+
const usageCost = calculateCost(model || DEFAULT_MODELS[provider], response.usage.inputTokens, response.usage.outputTokens);
|
|
319
1319
|
setStats(s => ({
|
|
320
1320
|
...s,
|
|
321
1321
|
inputTokens: s.inputTokens + response.usage.inputTokens,
|
|
322
1322
|
outputTokens: s.outputTokens + response.usage.outputTokens,
|
|
1323
|
+
cost: s.cost + usageCost,
|
|
323
1324
|
}));
|
|
324
1325
|
}
|
|
325
1326
|
// Handle tool calls
|
|
@@ -330,8 +1331,76 @@ function TerminalChat() {
|
|
|
330
1331
|
toolCalls: response.toolCalls,
|
|
331
1332
|
});
|
|
332
1333
|
for (const toolCall of response.toolCalls) {
|
|
333
|
-
|
|
1334
|
+
const args = toolCall.arguments;
|
|
1335
|
+
const toolPreview = String(args.command || args.path || '...');
|
|
1336
|
+
// Assess risk
|
|
1337
|
+
const risk = assessToolRisk(toolCall);
|
|
1338
|
+
const riskConfig = RISK_CONFIG[risk.level];
|
|
1339
|
+
const riskDisplay = risk.level !== 'none' ? ` [${riskConfig.bar}]` : '';
|
|
1340
|
+
// Special handling for think tool
|
|
1341
|
+
if (toolCall.name === 'think') {
|
|
1342
|
+
const thought = String(args.thought || '');
|
|
1343
|
+
setThinkingState({
|
|
1344
|
+
status: 'Reasoning...',
|
|
1345
|
+
detail: thought.substring(0, 60) + (thought.length > 60 ? '...' : ''),
|
|
1346
|
+
thinking: thought,
|
|
1347
|
+
iteration: i + 1,
|
|
1348
|
+
maxIterations,
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
setThinkingState({
|
|
1353
|
+
status: `Executing ${toolCall.name}...`,
|
|
1354
|
+
detail: toolPreview.substring(0, 60),
|
|
1355
|
+
thinking: undefined,
|
|
1356
|
+
iteration: i + 1,
|
|
1357
|
+
maxIterations,
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
// In plan mode, don't execute tools (except think)
|
|
1361
|
+
if (mode === 'plan' && toolCall.name !== 'think') {
|
|
1362
|
+
addMessage('tool', `š ${toolCall.name}: ${toolPreview}${riskDisplay} (plan mode - not executed)`);
|
|
1363
|
+
llmMessages.current.push({
|
|
1364
|
+
role: 'tool',
|
|
1365
|
+
content: '[Plan mode: Tool not executed. Describe what this would do.]',
|
|
1366
|
+
toolCallId: toolCall.id,
|
|
1367
|
+
});
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
// Check if confirmation is required for risky operations
|
|
1371
|
+
if (confirmMode && requiresConfirmation(risk, false) && toolCall.name !== 'think') {
|
|
1372
|
+
// Show warning and skip execution
|
|
1373
|
+
const riskIcon = risk.level === 'critical' ? 'š' : 'ā ļø';
|
|
1374
|
+
addMessage('tool', `${riskIcon} ${toolCall.name}: ${toolPreview}${riskDisplay}\n ā Requires confirmation (use /confirm off to disable)`);
|
|
1375
|
+
llmMessages.current.push({
|
|
1376
|
+
role: 'tool',
|
|
1377
|
+
content: `[Operation blocked - ${risk.level} risk: ${risk.reason}. User confirmation required.]`,
|
|
1378
|
+
toolCallId: toolCall.id,
|
|
1379
|
+
});
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
addMessage('tool', `ā” ${toolCall.name}: ${toolPreview}${riskDisplay}`);
|
|
1383
|
+
// Execute pre-tool hooks
|
|
1384
|
+
const preHookResult = await hooks.checkHooksAllow('pre-tool', {
|
|
1385
|
+
tool: toolCall.name,
|
|
1386
|
+
toolArgs: args,
|
|
1387
|
+
});
|
|
1388
|
+
if (!preHookResult.allowed) {
|
|
1389
|
+
addMessage('tool', `š Blocked by hook: ${preHookResult.reason}`);
|
|
1390
|
+
llmMessages.current.push({
|
|
1391
|
+
role: 'tool',
|
|
1392
|
+
content: `[Blocked by hook: ${preHookResult.reason}]`,
|
|
1393
|
+
toolCallId: toolCall.id,
|
|
1394
|
+
});
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
334
1397
|
const result = await executeTool(toolCall, process.cwd());
|
|
1398
|
+
// Execute post-tool hooks
|
|
1399
|
+
hooks.executeHooks('post-tool', {
|
|
1400
|
+
tool: toolCall.name,
|
|
1401
|
+
toolArgs: args,
|
|
1402
|
+
toolResult: result.result,
|
|
1403
|
+
}).catch(() => { });
|
|
335
1404
|
const preview = result.result.split('\n').slice(0, 3).join('\n');
|
|
336
1405
|
addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''));
|
|
337
1406
|
llmMessages.current.push({
|
|
@@ -342,17 +1411,30 @@ function TerminalChat() {
|
|
|
342
1411
|
}
|
|
343
1412
|
continue;
|
|
344
1413
|
}
|
|
345
|
-
// Final response
|
|
1414
|
+
// Final response - move streaming content to message history
|
|
1415
|
+
setThinkingState(null);
|
|
346
1416
|
llmMessages.current.push({ role: 'assistant', content: response.content });
|
|
347
1417
|
addMessage('assistant', response.content);
|
|
1418
|
+
setStreamingResponse('');
|
|
1419
|
+
setContextTokens(estimateContextTokens());
|
|
1420
|
+
// Auto-continue if response was truncated due to length
|
|
1421
|
+
if (response.finishReason === 'length') {
|
|
1422
|
+
addMessage('system', '(auto-continuing...)');
|
|
1423
|
+
llmMessages.current.push({ role: 'user', content: 'Please continue where you left off.' });
|
|
1424
|
+
continue; // Loop again to get continuation
|
|
1425
|
+
}
|
|
348
1426
|
break;
|
|
349
1427
|
}
|
|
350
1428
|
catch (error) {
|
|
351
|
-
|
|
1429
|
+
setThinkingState(null);
|
|
1430
|
+
setStreamingResponse('');
|
|
1431
|
+
addMessage('error', formatError(error));
|
|
352
1432
|
break;
|
|
353
1433
|
}
|
|
354
1434
|
}
|
|
355
|
-
|
|
1435
|
+
// Update context tokens after agent run
|
|
1436
|
+
setContextTokens(estimateContextTokens());
|
|
1437
|
+
}, [provider, model, addMessage, mode, estimateContextTokens]);
|
|
356
1438
|
// Handle input submission
|
|
357
1439
|
const handleSubmit = useCallback(async (value) => {
|
|
358
1440
|
const trimmed = value.trim();
|
|
@@ -363,15 +1445,40 @@ function TerminalChat() {
|
|
|
363
1445
|
await handleCommand(trimmed);
|
|
364
1446
|
return;
|
|
365
1447
|
}
|
|
366
|
-
|
|
1448
|
+
// Parse file references from input
|
|
1449
|
+
const { text: cleanText, files } = parseFileReferences(trimmed, process.cwd());
|
|
1450
|
+
// Show user message (with file info if any)
|
|
1451
|
+
if (files.length > 0) {
|
|
1452
|
+
const fileInfo = formatFileInfo(files);
|
|
1453
|
+
addMessage('user', `${cleanText}\nš ${fileInfo}`);
|
|
1454
|
+
}
|
|
1455
|
+
else {
|
|
1456
|
+
addMessage('user', trimmed);
|
|
1457
|
+
}
|
|
367
1458
|
setIsProcessing(true);
|
|
368
1459
|
try {
|
|
369
|
-
|
|
1460
|
+
// Build message content (with file/image support)
|
|
1461
|
+
let messageContent;
|
|
1462
|
+
if (files.length > 0) {
|
|
1463
|
+
const visionSupported = supportsVision(provider, model);
|
|
1464
|
+
const { content, warnings } = processFilesForMessage(cleanText || trimmed, files, visionSupported);
|
|
1465
|
+
// Show any warnings about files
|
|
1466
|
+
for (const warning of warnings) {
|
|
1467
|
+
addMessage('system', warning);
|
|
1468
|
+
}
|
|
1469
|
+
messageContent = content;
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
messageContent = trimmed;
|
|
1473
|
+
}
|
|
1474
|
+
await runAgent(messageContent);
|
|
370
1475
|
}
|
|
371
1476
|
finally {
|
|
372
1477
|
setIsProcessing(false);
|
|
1478
|
+
setThinkingState(null);
|
|
1479
|
+
setStreamingResponse('');
|
|
373
1480
|
}
|
|
374
|
-
}, [isProcessing, handleCommand, runAgent, addMessage]);
|
|
1481
|
+
}, [isProcessing, handleCommand, runAgent, addMessage, provider, model]);
|
|
375
1482
|
// Modal handlers
|
|
376
1483
|
const handleModelSelect = useCallback((selectedModel) => {
|
|
377
1484
|
setModel(selectedModel);
|
|
@@ -408,13 +1515,26 @@ function TerminalChat() {
|
|
|
408
1515
|
}
|
|
409
1516
|
setLatestVersion(null);
|
|
410
1517
|
}, [addMessage]);
|
|
411
|
-
//
|
|
412
|
-
|
|
1518
|
+
// Cycle through modes
|
|
1519
|
+
const cycleMode = useCallback(() => {
|
|
1520
|
+
setMode(current => {
|
|
1521
|
+
const modes = ['plan', 'hybrid', 'work'];
|
|
1522
|
+
const idx = modes.indexOf(current);
|
|
1523
|
+
const next = modes[(idx + 1) % modes.length];
|
|
1524
|
+
return next;
|
|
1525
|
+
});
|
|
1526
|
+
}, []);
|
|
1527
|
+
// Keyboard shortcuts (Escape to exit, Shift+Tab to cycle mode)
|
|
1528
|
+
useInput((input, key) => {
|
|
413
1529
|
if (key.escape && !isModalActive)
|
|
414
1530
|
exit();
|
|
1531
|
+
// Shift+Tab to cycle mode (key.shift && key.tab)
|
|
1532
|
+
if (key.shift && key.tab && !isProcessing) {
|
|
1533
|
+
cycleMode();
|
|
1534
|
+
}
|
|
415
1535
|
}, { isActive: !isModalActive });
|
|
416
1536
|
// Render
|
|
417
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsx(MessageHistory, { messages: messages }), isProcessing && _jsx(ProcessingIndicator, { label: "
|
|
1537
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsx(MessageHistory, { messages: messages }), streamingResponse && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "\u2727 Calliope:" }), _jsx(Text, { children: " " }), streamingResponse.split('\n').map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "\u2502" }), " ", line] }, i))), _jsx(Text, { color: "blue", children: "\u2502" }), _jsx(Text, { color: "cyan", children: "\u258C" })] })), isProcessing && thinkingState && !streamingResponse && _jsx(ThinkingDisplay, { state: thinkingState }), isProcessing && !thinkingState && !streamingResponse && _jsx(ProcessingIndicator, { label: "Processing..." }), modalMode === 'model' && availableModels.length > 0 && (_jsx(ModelSelector, { models: availableModels, onSelect: handleModelSelect, onCancel: handleModalCancel })), modalMode === 'upgrade' && latestVersion && (_jsx(UpgradePrompt, { currentVersion: getVersion(), latestVersion: latestVersion, onConfirm: handleUpgradeConfirm, onCancel: handleModalCancel })), _jsx(ChatInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: isModalActive || isProcessing, mode: mode }), _jsx(StatusBar, { provider: actualProvider, model: actualModel, mode: mode, stats: stats, contextTokens: contextTokens })] }));
|
|
418
1538
|
}
|
|
419
1539
|
// ============================================================================
|
|
420
1540
|
// App Wrapper & Entry Point
|