codeep 1.1.12 → 1.1.13
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/bin/codeep.js +1 -1
- package/dist/config/index.js +10 -10
- package/dist/renderer/App.d.ts +430 -0
- package/dist/renderer/App.js +2712 -0
- package/dist/renderer/ChatUI.d.ts +71 -0
- package/dist/renderer/ChatUI.js +286 -0
- package/dist/renderer/Input.d.ts +72 -0
- package/dist/renderer/Input.js +371 -0
- package/dist/renderer/Screen.d.ts +79 -0
- package/dist/renderer/Screen.js +278 -0
- package/dist/renderer/ansi.d.ts +99 -0
- package/dist/renderer/ansi.js +176 -0
- package/dist/renderer/components/Box.d.ts +64 -0
- package/dist/renderer/components/Box.js +90 -0
- package/dist/renderer/components/Help.d.ts +30 -0
- package/dist/renderer/components/Help.js +195 -0
- package/dist/renderer/components/Intro.d.ts +12 -0
- package/dist/renderer/components/Intro.js +128 -0
- package/dist/renderer/components/Login.d.ts +42 -0
- package/dist/renderer/components/Login.js +178 -0
- package/dist/renderer/components/Modal.d.ts +43 -0
- package/dist/renderer/components/Modal.js +207 -0
- package/dist/renderer/components/Permission.d.ts +20 -0
- package/dist/renderer/components/Permission.js +113 -0
- package/dist/renderer/components/SelectScreen.d.ts +26 -0
- package/dist/renderer/components/SelectScreen.js +101 -0
- package/dist/renderer/components/Settings.d.ts +37 -0
- package/dist/renderer/components/Settings.js +333 -0
- package/dist/renderer/components/Status.d.ts +18 -0
- package/dist/renderer/components/Status.js +78 -0
- package/dist/renderer/demo-app.d.ts +6 -0
- package/dist/renderer/demo-app.js +85 -0
- package/dist/renderer/demo.d.ts +6 -0
- package/dist/renderer/demo.js +52 -0
- package/dist/renderer/index.d.ts +16 -0
- package/dist/renderer/index.js +17 -0
- package/dist/renderer/main.d.ts +6 -0
- package/dist/renderer/main.js +1634 -0
- package/dist/utils/agent.d.ts +21 -0
- package/dist/utils/agent.js +29 -0
- package/dist/utils/clipboard.d.ts +15 -0
- package/dist/utils/clipboard.js +95 -0
- package/package.json +7 -11
- package/dist/utils/console.d.ts +0 -55
- package/dist/utils/console.js +0 -188
|
@@ -0,0 +1,1634 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Codeep with Custom Renderer
|
|
4
|
+
* Main entry point using the new ANSI-based renderer instead of Ink
|
|
5
|
+
*/
|
|
6
|
+
import { App } from './App.js';
|
|
7
|
+
import { Screen } from './Screen.js';
|
|
8
|
+
import { Input } from './Input.js';
|
|
9
|
+
import { LoginScreen, renderProviderSelect } from './components/Login.js';
|
|
10
|
+
import { renderPermissionScreen, getPermissionOptions } from './components/Permission.js';
|
|
11
|
+
// Intro animation is now handled by App.startIntro()
|
|
12
|
+
import { chat, setProjectContext } from '../api/index.js';
|
|
13
|
+
import { runAgent } from '../utils/agent.js';
|
|
14
|
+
import { config, loadApiKey, loadAllApiKeys, getCurrentProvider, getModelsForCurrentProvider, PROTOCOLS, LANGUAGES, setProvider, setApiKey, clearApiKey, autoSaveSession, saveSession, startNewSession, getCurrentSessionId, loadSession, listSessionsWithInfo, deleteSession, renameSession, hasReadPermission, hasWritePermission, setProjectPermission, } from '../config/index.js';
|
|
15
|
+
import { isProjectDirectory, getProjectContext } from '../utils/project.js';
|
|
16
|
+
import { getCurrentVersion } from '../utils/update.js';
|
|
17
|
+
import { getProviderList } from '../config/providers.js';
|
|
18
|
+
// State
|
|
19
|
+
let projectPath = process.cwd();
|
|
20
|
+
let projectContext = null;
|
|
21
|
+
let hasWriteAccess = false;
|
|
22
|
+
let sessionId = getCurrentSessionId();
|
|
23
|
+
let app;
|
|
24
|
+
/**
|
|
25
|
+
* Get current status
|
|
26
|
+
*/
|
|
27
|
+
function getStatus() {
|
|
28
|
+
const provider = getCurrentProvider();
|
|
29
|
+
const providers = getProviderList();
|
|
30
|
+
const providerInfo = providers.find(p => p.id === provider.id);
|
|
31
|
+
return {
|
|
32
|
+
version: getCurrentVersion(),
|
|
33
|
+
provider: providerInfo?.name || 'Unknown',
|
|
34
|
+
model: config.get('model'),
|
|
35
|
+
agentMode: config.get('agentMode') || 'off',
|
|
36
|
+
projectPath,
|
|
37
|
+
hasWriteAccess,
|
|
38
|
+
sessionId,
|
|
39
|
+
messageCount: 0, // Will be updated
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Agent state
|
|
43
|
+
let isAgentRunning = false;
|
|
44
|
+
let agentAbortController = null;
|
|
45
|
+
/**
|
|
46
|
+
* Handle chat submission
|
|
47
|
+
*/
|
|
48
|
+
async function handleSubmit(message) {
|
|
49
|
+
// Check if we're waiting for interactive mode answers
|
|
50
|
+
if (pendingInteractiveContext) {
|
|
51
|
+
const { parseAnswers, enhancePromptWithAnswers } = await import('../utils/interactive');
|
|
52
|
+
const answers = parseAnswers(message, pendingInteractiveContext.context);
|
|
53
|
+
// Enhance the original prompt with user's answers
|
|
54
|
+
const enhancedTask = enhancePromptWithAnswers(pendingInteractiveContext.context, answers);
|
|
55
|
+
const dryRun = pendingInteractiveContext.dryRun;
|
|
56
|
+
pendingInteractiveContext = null;
|
|
57
|
+
// Now run the agent with the enhanced task
|
|
58
|
+
// Skip interactive analysis this time by going straight to confirmation check
|
|
59
|
+
const confirmationMode = config.get('agentConfirmation') || 'dangerous';
|
|
60
|
+
if (confirmationMode === 'never' || dryRun) {
|
|
61
|
+
executeAgentTask(enhancedTask, dryRun);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// For 'always' or 'dangerous', show confirmation if needed
|
|
65
|
+
if (confirmationMode === 'always') {
|
|
66
|
+
const shortTask = enhancedTask.length > 60 ? enhancedTask.slice(0, 57) + '...' : enhancedTask;
|
|
67
|
+
app.showConfirm({
|
|
68
|
+
title: '⚠️ Confirm Agent Task',
|
|
69
|
+
message: [
|
|
70
|
+
'Run agent with enhanced task?',
|
|
71
|
+
'',
|
|
72
|
+
` "${shortTask}"`,
|
|
73
|
+
],
|
|
74
|
+
confirmLabel: 'Run Agent',
|
|
75
|
+
cancelLabel: 'Cancel',
|
|
76
|
+
onConfirm: () => executeAgentTask(enhancedTask, dryRun),
|
|
77
|
+
onCancel: () => app.notify('Agent task cancelled'),
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// 'dangerous' mode - check for dangerous keywords
|
|
82
|
+
const dangerousKeywords = ['delete', 'remove', 'drop', 'reset', 'force', 'overwrite', 'replace all', 'rm ', 'clear'];
|
|
83
|
+
const taskLower = enhancedTask.toLowerCase();
|
|
84
|
+
const hasDangerousKeyword = dangerousKeywords.some(k => taskLower.includes(k));
|
|
85
|
+
if (hasDangerousKeyword) {
|
|
86
|
+
const shortTask = enhancedTask.length > 60 ? enhancedTask.slice(0, 57) + '...' : enhancedTask;
|
|
87
|
+
app.showConfirm({
|
|
88
|
+
title: '⚠️ Potentially Dangerous Task',
|
|
89
|
+
message: [
|
|
90
|
+
'This task contains potentially dangerous operations:',
|
|
91
|
+
'',
|
|
92
|
+
` "${shortTask}"`,
|
|
93
|
+
],
|
|
94
|
+
confirmLabel: 'Proceed',
|
|
95
|
+
cancelLabel: 'Cancel',
|
|
96
|
+
onConfirm: () => executeAgentTask(enhancedTask, dryRun),
|
|
97
|
+
onCancel: () => app.notify('Agent task cancelled'),
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
executeAgentTask(enhancedTask, dryRun);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Check if Agent Mode is ON - auto run agent for every message
|
|
105
|
+
const agentMode = config.get('agentMode') || 'off';
|
|
106
|
+
if (agentMode === 'on' && projectContext && hasWriteAccess && !isAgentRunning) {
|
|
107
|
+
// Auto-run agent mode
|
|
108
|
+
runAgentTask(message, false);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
app.startStreaming();
|
|
113
|
+
// Get conversation history for context
|
|
114
|
+
const history = app.getChatHistory();
|
|
115
|
+
const response = await chat(message, history, (chunk) => {
|
|
116
|
+
app.addStreamChunk(chunk);
|
|
117
|
+
}, undefined, projectContext, undefined);
|
|
118
|
+
app.endStreaming();
|
|
119
|
+
// Auto-save session
|
|
120
|
+
autoSaveSession(app.getMessages(), projectPath);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
app.endStreaming();
|
|
124
|
+
const err = error;
|
|
125
|
+
app.notify(`Error: ${err.message}`, 5000);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Dangerous tool patterns that require confirmation
|
|
129
|
+
const DANGEROUS_TOOLS = ['write', 'edit', 'delete', 'command', 'execute', 'shell', 'rm', 'mv'];
|
|
130
|
+
/**
|
|
131
|
+
* Check if a tool call is considered dangerous
|
|
132
|
+
*/
|
|
133
|
+
function isDangerousTool(toolName, parameters) {
|
|
134
|
+
const lowerName = toolName.toLowerCase();
|
|
135
|
+
// Check for dangerous tool names
|
|
136
|
+
if (DANGEROUS_TOOLS.some(d => lowerName.includes(d))) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
// Check for dangerous commands
|
|
140
|
+
const command = parameters.command || '';
|
|
141
|
+
const dangerousCommands = ['rm ', 'rm -', 'rmdir', 'del ', 'delete', 'drop ', 'truncate'];
|
|
142
|
+
if (dangerousCommands.some(c => command.toLowerCase().includes(c))) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Request confirmation for a tool call
|
|
149
|
+
*/
|
|
150
|
+
function requestToolConfirmation(tool, parameters, onConfirm, onCancel) {
|
|
151
|
+
const target = parameters.path ||
|
|
152
|
+
parameters.command ||
|
|
153
|
+
parameters.pattern ||
|
|
154
|
+
'unknown';
|
|
155
|
+
const shortTarget = target.length > 50 ? '...' + target.slice(-47) : target;
|
|
156
|
+
app.showConfirm({
|
|
157
|
+
title: '⚠️ Confirm Action',
|
|
158
|
+
message: [
|
|
159
|
+
`The agent wants to execute:`,
|
|
160
|
+
'',
|
|
161
|
+
` ${tool}`,
|
|
162
|
+
` ${shortTarget}`,
|
|
163
|
+
'',
|
|
164
|
+
'Allow this action?',
|
|
165
|
+
],
|
|
166
|
+
confirmLabel: 'Allow',
|
|
167
|
+
cancelLabel: 'Deny',
|
|
168
|
+
onConfirm,
|
|
169
|
+
onCancel,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// Store context for interactive mode follow-up
|
|
173
|
+
let pendingInteractiveContext = null;
|
|
174
|
+
/**
|
|
175
|
+
* Run agent with task - handles confirmation dialogs based on settings
|
|
176
|
+
*/
|
|
177
|
+
async function runAgentTask(task, dryRun = false) {
|
|
178
|
+
if (!projectContext) {
|
|
179
|
+
app.notify('Agent requires project context');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!hasWriteAccess && !dryRun) {
|
|
183
|
+
app.notify('Agent requires write access. Use /grant first.');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (isAgentRunning) {
|
|
187
|
+
app.notify('Agent already running. Use /stop to cancel.');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Check interactive mode setting
|
|
191
|
+
const interactiveMode = config.get('agentInteractive') !== false;
|
|
192
|
+
if (interactiveMode) {
|
|
193
|
+
// Analyze task for ambiguity
|
|
194
|
+
const { analyzeForClarification, formatQuestions } = await import('../utils/interactive');
|
|
195
|
+
const interactiveContext = analyzeForClarification(task);
|
|
196
|
+
if (interactiveContext.needsClarification) {
|
|
197
|
+
// Store context for follow-up
|
|
198
|
+
pendingInteractiveContext = {
|
|
199
|
+
originalTask: task,
|
|
200
|
+
context: interactiveContext,
|
|
201
|
+
dryRun,
|
|
202
|
+
};
|
|
203
|
+
// Show questions to user
|
|
204
|
+
const questionsText = formatQuestions(interactiveContext);
|
|
205
|
+
app.addMessage({
|
|
206
|
+
role: 'assistant',
|
|
207
|
+
content: questionsText,
|
|
208
|
+
});
|
|
209
|
+
app.notify('Answer questions or type "proceed" to continue');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Check agentConfirmation setting
|
|
214
|
+
const confirmationMode = config.get('agentConfirmation') || 'dangerous';
|
|
215
|
+
// 'never' - no confirmation needed
|
|
216
|
+
if (confirmationMode === 'never' || dryRun) {
|
|
217
|
+
executeAgentTask(task, dryRun);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// 'always' - confirm before running any agent task
|
|
221
|
+
if (confirmationMode === 'always') {
|
|
222
|
+
const shortTask = task.length > 60 ? task.slice(0, 57) + '...' : task;
|
|
223
|
+
app.showConfirm({
|
|
224
|
+
title: '⚠️ Confirm Agent Task',
|
|
225
|
+
message: [
|
|
226
|
+
'The agent will execute the following task:',
|
|
227
|
+
'',
|
|
228
|
+
` "${shortTask}"`,
|
|
229
|
+
'',
|
|
230
|
+
'This may modify files in your project.',
|
|
231
|
+
'Do you want to proceed?',
|
|
232
|
+
],
|
|
233
|
+
confirmLabel: 'Run Agent',
|
|
234
|
+
cancelLabel: 'Cancel',
|
|
235
|
+
onConfirm: () => {
|
|
236
|
+
executeAgentTask(task, dryRun);
|
|
237
|
+
},
|
|
238
|
+
onCancel: () => {
|
|
239
|
+
app.notify('Agent task cancelled');
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// 'dangerous' - confirm only for tasks with dangerous keywords
|
|
245
|
+
const dangerousKeywords = ['delete', 'remove', 'drop', 'reset', 'force', 'overwrite', 'replace all', 'rm ', 'clear'];
|
|
246
|
+
const taskLower = task.toLowerCase();
|
|
247
|
+
const hasDangerousKeyword = dangerousKeywords.some(k => taskLower.includes(k));
|
|
248
|
+
if (hasDangerousKeyword) {
|
|
249
|
+
const shortTask = task.length > 60 ? task.slice(0, 57) + '...' : task;
|
|
250
|
+
app.showConfirm({
|
|
251
|
+
title: '⚠️ Potentially Dangerous Task',
|
|
252
|
+
message: [
|
|
253
|
+
'This task contains potentially dangerous operations:',
|
|
254
|
+
'',
|
|
255
|
+
` "${shortTask}"`,
|
|
256
|
+
'',
|
|
257
|
+
'Files may be deleted or overwritten.',
|
|
258
|
+
'Do you want to proceed?',
|
|
259
|
+
],
|
|
260
|
+
confirmLabel: 'Proceed',
|
|
261
|
+
cancelLabel: 'Cancel',
|
|
262
|
+
onConfirm: () => {
|
|
263
|
+
executeAgentTask(task, dryRun);
|
|
264
|
+
},
|
|
265
|
+
onCancel: () => {
|
|
266
|
+
app.notify('Agent task cancelled');
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// No dangerous keywords detected, run directly
|
|
272
|
+
executeAgentTask(task, dryRun);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Run agent with task (internal - called after confirmation if needed)
|
|
276
|
+
*/
|
|
277
|
+
async function executeAgentTask(task, dryRun = false) {
|
|
278
|
+
// Guard - should never happen since runAgentTask checks this
|
|
279
|
+
if (!projectContext) {
|
|
280
|
+
app.notify('Agent requires project context');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
isAgentRunning = true;
|
|
284
|
+
agentAbortController = new AbortController();
|
|
285
|
+
// Add user message
|
|
286
|
+
const prefix = dryRun ? '[DRY RUN] ' : '[AGENT] ';
|
|
287
|
+
app.addMessage({ role: 'user', content: prefix + task });
|
|
288
|
+
// Start agent progress UI
|
|
289
|
+
app.setAgentRunning(true);
|
|
290
|
+
// Store context in local variable for TypeScript narrowing
|
|
291
|
+
const context = projectContext;
|
|
292
|
+
try {
|
|
293
|
+
const result = await runAgent(task, context, {
|
|
294
|
+
dryRun,
|
|
295
|
+
onIteration: (iteration) => {
|
|
296
|
+
app.updateAgentProgress(iteration);
|
|
297
|
+
},
|
|
298
|
+
onToolCall: (tool) => {
|
|
299
|
+
const toolName = tool.tool.toLowerCase();
|
|
300
|
+
const target = tool.parameters.path ||
|
|
301
|
+
tool.parameters.command ||
|
|
302
|
+
tool.parameters.pattern || '';
|
|
303
|
+
// Determine action type
|
|
304
|
+
const actionType = toolName.includes('write') ? 'write' :
|
|
305
|
+
toolName.includes('edit') ? 'edit' :
|
|
306
|
+
toolName.includes('read') ? 'read' :
|
|
307
|
+
toolName.includes('delete') ? 'delete' :
|
|
308
|
+
toolName.includes('list') ? 'list' :
|
|
309
|
+
toolName.includes('search') || toolName.includes('grep') ? 'search' :
|
|
310
|
+
toolName.includes('mkdir') ? 'mkdir' :
|
|
311
|
+
toolName.includes('fetch') ? 'fetch' : 'command';
|
|
312
|
+
// Update agent thinking
|
|
313
|
+
const shortTarget = target.length > 50 ? '...' + target.slice(-47) : target;
|
|
314
|
+
app.setAgentThinking(`${actionType}: ${shortTarget}`);
|
|
315
|
+
},
|
|
316
|
+
onToolResult: (result, toolCall) => {
|
|
317
|
+
const toolName = toolCall.tool.toLowerCase();
|
|
318
|
+
const target = toolCall.parameters.path || toolCall.parameters.command || '';
|
|
319
|
+
// Track action with result
|
|
320
|
+
const actionType = toolName.includes('write') ? 'write' :
|
|
321
|
+
toolName.includes('edit') ? 'edit' :
|
|
322
|
+
toolName.includes('read') ? 'read' :
|
|
323
|
+
toolName.includes('delete') ? 'delete' :
|
|
324
|
+
toolName.includes('list') ? 'list' :
|
|
325
|
+
toolName.includes('search') || toolName.includes('grep') ? 'search' :
|
|
326
|
+
toolName.includes('mkdir') ? 'mkdir' :
|
|
327
|
+
toolName.includes('fetch') ? 'fetch' : 'command';
|
|
328
|
+
app.updateAgentProgress(0, {
|
|
329
|
+
type: actionType,
|
|
330
|
+
target: target,
|
|
331
|
+
result: result.success ? 'success' : 'error',
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
onThinking: (text) => {
|
|
335
|
+
if (text) {
|
|
336
|
+
app.setAgentThinking(text);
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
abortSignal: agentAbortController.signal,
|
|
340
|
+
});
|
|
341
|
+
// Show result
|
|
342
|
+
if (result.success) {
|
|
343
|
+
const summary = result.finalResponse || `Completed ${result.actions.length} actions in ${result.iterations} steps.`;
|
|
344
|
+
app.addMessage({ role: 'assistant', content: summary });
|
|
345
|
+
app.notify(`Agent completed: ${result.actions.length} actions`);
|
|
346
|
+
}
|
|
347
|
+
else if (result.aborted) {
|
|
348
|
+
app.addMessage({ role: 'assistant', content: 'Agent stopped by user.' });
|
|
349
|
+
app.notify('Agent stopped');
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
app.addMessage({ role: 'assistant', content: `Agent failed: ${result.error}` });
|
|
353
|
+
app.notify(`Agent failed: ${result.error}`);
|
|
354
|
+
}
|
|
355
|
+
// Auto-save
|
|
356
|
+
autoSaveSession(app.getMessages(), projectPath);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
const err = error;
|
|
360
|
+
app.addMessage({ role: 'assistant', content: `Agent error: ${err.message}` });
|
|
361
|
+
app.notify(`Agent error: ${err.message}`, 5000);
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
isAgentRunning = false;
|
|
365
|
+
agentAbortController = null;
|
|
366
|
+
app.setAgentRunning(false);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Run a chain of commands sequentially
|
|
371
|
+
*/
|
|
372
|
+
function runCommandChain(commands, index) {
|
|
373
|
+
if (index >= commands.length) {
|
|
374
|
+
app.notify(`Completed ${commands.length} commands`);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const cmd = commands[index].toLowerCase();
|
|
378
|
+
app.notify(`Running /${cmd}... (${index + 1}/${commands.length})`);
|
|
379
|
+
// Run the command
|
|
380
|
+
handleCommand(cmd, []);
|
|
381
|
+
// Schedule next command with a delay to allow current to complete
|
|
382
|
+
setTimeout(() => {
|
|
383
|
+
runCommandChain(commands, index + 1);
|
|
384
|
+
}, 500);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Handle commands
|
|
388
|
+
*/
|
|
389
|
+
function handleCommand(command, args) {
|
|
390
|
+
// Handle skill chaining (e.g., /commit+push)
|
|
391
|
+
if (command.includes('+')) {
|
|
392
|
+
const commands = command.split('+').filter(c => c.trim());
|
|
393
|
+
runCommandChain(commands, 0);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
switch (command) {
|
|
397
|
+
case 'version': {
|
|
398
|
+
const version = getCurrentVersion();
|
|
399
|
+
const provider = getCurrentProvider();
|
|
400
|
+
const providers = getProviderList();
|
|
401
|
+
const providerInfo = providers.find(p => p.id === provider.id);
|
|
402
|
+
app.notify(`Codeep v${version} • ${providerInfo?.name} • ${config.get('model')}`);
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case 'provider': {
|
|
406
|
+
const providers = getProviderList();
|
|
407
|
+
const providerItems = providers.map(p => ({
|
|
408
|
+
key: p.id,
|
|
409
|
+
label: p.name,
|
|
410
|
+
description: p.description || '',
|
|
411
|
+
}));
|
|
412
|
+
const currentProvider = getCurrentProvider();
|
|
413
|
+
app.showSelect('Select Provider', providerItems, currentProvider.id, (item) => {
|
|
414
|
+
if (setProvider(item.key)) {
|
|
415
|
+
app.notify(`Provider: ${item.label}`);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case 'model': {
|
|
421
|
+
const models = getModelsForCurrentProvider();
|
|
422
|
+
const modelItems = Object.entries(models).map(([name, info]) => ({
|
|
423
|
+
key: name,
|
|
424
|
+
label: name,
|
|
425
|
+
description: typeof info === 'object' && info !== null ? info.description || '' : '',
|
|
426
|
+
}));
|
|
427
|
+
const currentModel = config.get('model');
|
|
428
|
+
app.showSelect('Select Model', modelItems, currentModel, (item) => {
|
|
429
|
+
config.set('model', item.key);
|
|
430
|
+
app.notify(`Model: ${item.label}`);
|
|
431
|
+
});
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
case 'grant': {
|
|
435
|
+
// Grant write permission
|
|
436
|
+
setProjectPermission(projectPath, true, true);
|
|
437
|
+
hasWriteAccess = true;
|
|
438
|
+
projectContext = getProjectContext(projectPath);
|
|
439
|
+
if (projectContext) {
|
|
440
|
+
projectContext.hasWriteAccess = true;
|
|
441
|
+
setProjectContext(projectContext);
|
|
442
|
+
}
|
|
443
|
+
app.notify('Write access granted');
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
case 'agent': {
|
|
447
|
+
if (!args.length) {
|
|
448
|
+
app.notify('Usage: /agent <task>');
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (isAgentRunning) {
|
|
452
|
+
app.notify('Agent already running. Use /stop to cancel.');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
runAgentTask(args.join(' '), false);
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
case 'agent-dry': {
|
|
459
|
+
if (!args.length) {
|
|
460
|
+
app.notify('Usage: /agent-dry <task>');
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (isAgentRunning) {
|
|
464
|
+
app.notify('Agent already running. Use /stop to cancel.');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
runAgentTask(args.join(' '), true);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
case 'stop': {
|
|
471
|
+
if (isAgentRunning && agentAbortController) {
|
|
472
|
+
agentAbortController.abort();
|
|
473
|
+
app.notify('Stopping agent...');
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
app.notify('No agent running');
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
case 'sessions': {
|
|
481
|
+
// List recent sessions
|
|
482
|
+
const sessions = listSessionsWithInfo(projectPath);
|
|
483
|
+
if (sessions.length === 0) {
|
|
484
|
+
app.notify('No saved sessions');
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
app.showList('Load Session', sessions.map(s => s.name), (index) => {
|
|
488
|
+
const selected = sessions[index];
|
|
489
|
+
const loaded = loadSession(selected.name, projectPath);
|
|
490
|
+
if (loaded) {
|
|
491
|
+
app.setMessages(loaded);
|
|
492
|
+
sessionId = selected.name;
|
|
493
|
+
app.notify(`Loaded: ${selected.name}`);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
app.notify('Failed to load session');
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
case 'new': {
|
|
502
|
+
app.clearMessages();
|
|
503
|
+
sessionId = startNewSession();
|
|
504
|
+
app.notify('New session started');
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case 'settings': {
|
|
508
|
+
app.showSettings();
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
case 'diff': {
|
|
512
|
+
if (!projectContext) {
|
|
513
|
+
app.notify('No project context');
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const staged = args.includes('--staged') || args.includes('-s');
|
|
517
|
+
app.notify(staged ? 'Getting staged diff...' : 'Getting diff...');
|
|
518
|
+
// Import dynamically to avoid circular deps
|
|
519
|
+
import('../utils/git').then(({ getGitDiff, formatDiffForDisplay }) => {
|
|
520
|
+
const result = getGitDiff(staged, projectPath);
|
|
521
|
+
if (!result.success || !result.diff) {
|
|
522
|
+
app.notify(result.error || 'No changes');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const preview = formatDiffForDisplay(result.diff, 50);
|
|
526
|
+
app.addMessage({ role: 'user', content: `/diff ${staged ? '--staged' : ''}` });
|
|
527
|
+
// Send to AI for review
|
|
528
|
+
handleSubmit(`Review this git diff and provide feedback:\n\n\`\`\`diff\n${preview}\n\`\`\``);
|
|
529
|
+
});
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
case 'commit': {
|
|
533
|
+
if (!projectContext) {
|
|
534
|
+
app.notify('No project context');
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
import('../utils/git').then(({ getGitDiff, getGitStatus, suggestCommitMessage }) => {
|
|
538
|
+
const status = getGitStatus(projectPath);
|
|
539
|
+
if (!status.isRepo) {
|
|
540
|
+
app.notify('Not a git repository');
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const diff = getGitDiff(true, projectPath);
|
|
544
|
+
if (!diff.success || !diff.diff) {
|
|
545
|
+
app.notify('No staged changes. Use git add first.');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const suggestion = suggestCommitMessage(diff.diff);
|
|
549
|
+
app.addMessage({ role: 'user', content: '/commit' });
|
|
550
|
+
handleSubmit(`Generate a commit message for these staged changes. Suggestion: "${suggestion}"\n\nDiff:\n\`\`\`diff\n${diff.diff.slice(0, 2000)}\n\`\`\``);
|
|
551
|
+
});
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case 'undo': {
|
|
555
|
+
import('../utils/agent').then(({ undoLastAction }) => {
|
|
556
|
+
const result = undoLastAction();
|
|
557
|
+
app.notify(result.success ? `Undo: ${result.message}` : `Cannot undo: ${result.message}`);
|
|
558
|
+
});
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case 'undo-all': {
|
|
562
|
+
import('../utils/agent').then(({ undoAllActions }) => {
|
|
563
|
+
const result = undoAllActions();
|
|
564
|
+
app.notify(result.success ? `Undone ${result.results.length} action(s)` : 'Nothing to undo');
|
|
565
|
+
});
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
case 'scan': {
|
|
569
|
+
if (!projectContext) {
|
|
570
|
+
app.notify('No project context');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
app.notify('Scanning project...');
|
|
574
|
+
import('../utils/projectIntelligence').then(({ scanProject, saveProjectIntelligence, generateContextFromIntelligence }) => {
|
|
575
|
+
scanProject(projectContext.root).then(intelligence => {
|
|
576
|
+
saveProjectIntelligence(projectContext.root, intelligence);
|
|
577
|
+
const context = generateContextFromIntelligence(intelligence);
|
|
578
|
+
app.addMessage({
|
|
579
|
+
role: 'assistant',
|
|
580
|
+
content: `# Project Scan Complete\n\n${context}`,
|
|
581
|
+
});
|
|
582
|
+
app.notify(`Scanned: ${intelligence.structure.totalFiles} files`);
|
|
583
|
+
}).catch(err => {
|
|
584
|
+
app.notify(`Scan failed: ${err.message}`);
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
case 'review': {
|
|
590
|
+
if (!projectContext) {
|
|
591
|
+
app.notify('No project context');
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
import('../utils/codeReview').then(({ performCodeReview, formatReviewResult }) => {
|
|
595
|
+
const reviewFiles = args.length > 0 ? args : undefined;
|
|
596
|
+
const result = performCodeReview(projectContext, reviewFiles);
|
|
597
|
+
app.addMessage({
|
|
598
|
+
role: 'assistant',
|
|
599
|
+
content: formatReviewResult(result),
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
case 'update': {
|
|
605
|
+
app.notify('Checking for updates...');
|
|
606
|
+
import('../utils/update').then(({ checkForUpdates, formatVersionInfo }) => {
|
|
607
|
+
checkForUpdates().then(info => {
|
|
608
|
+
const message = formatVersionInfo(info);
|
|
609
|
+
app.notify(message.split('\n')[0], 5000);
|
|
610
|
+
}).catch(() => {
|
|
611
|
+
app.notify('Failed to check for updates');
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
// Session management
|
|
617
|
+
case 'rename': {
|
|
618
|
+
if (!args.length) {
|
|
619
|
+
app.notify('Usage: /rename <new-name>');
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const newName = args.join('-');
|
|
623
|
+
if (renameSession(sessionId, newName, projectPath)) {
|
|
624
|
+
sessionId = newName;
|
|
625
|
+
app.notify(`Session renamed to: ${newName}`);
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
app.notify('Failed to rename session');
|
|
629
|
+
}
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
case 'search': {
|
|
633
|
+
if (!args.length) {
|
|
634
|
+
app.notify('Usage: /search <term>');
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const searchTerm = args.join(' ').toLowerCase();
|
|
638
|
+
const messages = app.getMessages();
|
|
639
|
+
const searchResults = [];
|
|
640
|
+
messages.forEach((m, index) => {
|
|
641
|
+
if (m.content.toLowerCase().includes(searchTerm)) {
|
|
642
|
+
// Find the matched text with some context
|
|
643
|
+
const lowerContent = m.content.toLowerCase();
|
|
644
|
+
const matchStart = Math.max(0, lowerContent.indexOf(searchTerm) - 30);
|
|
645
|
+
const matchEnd = Math.min(m.content.length, lowerContent.indexOf(searchTerm) + searchTerm.length + 50);
|
|
646
|
+
const matchedText = (matchStart > 0 ? '...' : '') +
|
|
647
|
+
m.content.slice(matchStart, matchEnd).replace(/\n/g, ' ') +
|
|
648
|
+
(matchEnd < m.content.length ? '...' : '');
|
|
649
|
+
searchResults.push({
|
|
650
|
+
role: m.role,
|
|
651
|
+
messageIndex: index,
|
|
652
|
+
matchedText,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
if (searchResults.length === 0) {
|
|
657
|
+
app.notify(`No matches for "${searchTerm}"`);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
app.showSearch(searchTerm, searchResults, (messageIndex) => {
|
|
661
|
+
// Scroll to the message
|
|
662
|
+
app.scrollToMessage(messageIndex);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
case 'export': {
|
|
668
|
+
const messages = app.getMessages();
|
|
669
|
+
if (messages.length === 0) {
|
|
670
|
+
app.notify('No messages to export');
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
app.showExport((format) => {
|
|
674
|
+
import('fs').then(fs => {
|
|
675
|
+
import('path').then(path => {
|
|
676
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
677
|
+
let filename;
|
|
678
|
+
let content;
|
|
679
|
+
if (format === 'json') {
|
|
680
|
+
filename = `codeep-export-${timestamp}.json`;
|
|
681
|
+
content = JSON.stringify(messages, null, 2);
|
|
682
|
+
}
|
|
683
|
+
else if (format === 'txt') {
|
|
684
|
+
filename = `codeep-export-${timestamp}.txt`;
|
|
685
|
+
content = messages.map(m => `[${m.role.toUpperCase()}]\n${m.content}\n`).join('\n---\n\n');
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
filename = `codeep-export-${timestamp}.md`;
|
|
689
|
+
content = `# Codeep Chat Export\n\n${messages.map(m => `## ${m.role === 'user' ? '👤 User' : m.role === 'assistant' ? '🤖 Assistant' : '⚙️ System'}\n\n${m.content}\n`).join('\n---\n\n')}`;
|
|
690
|
+
}
|
|
691
|
+
const exportPath = path.join(projectPath, filename);
|
|
692
|
+
fs.writeFileSync(exportPath, content);
|
|
693
|
+
app.notify(`Exported to ${filename}`);
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
// Protocol and language
|
|
700
|
+
case 'protocol': {
|
|
701
|
+
const protocols = Object.entries(PROTOCOLS).map(([key, name]) => ({
|
|
702
|
+
key,
|
|
703
|
+
label: name,
|
|
704
|
+
}));
|
|
705
|
+
const currentProtocol = config.get('protocol') || 'openai';
|
|
706
|
+
app.showSelect('Select Protocol', protocols, currentProtocol, (item) => {
|
|
707
|
+
config.set('protocol', item.key);
|
|
708
|
+
app.notify(`Protocol: ${item.label}`);
|
|
709
|
+
});
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
case 'lang': {
|
|
713
|
+
const languages = Object.entries(LANGUAGES).map(([key, name]) => ({
|
|
714
|
+
key,
|
|
715
|
+
label: name,
|
|
716
|
+
}));
|
|
717
|
+
const currentLang = config.get('language') || 'auto';
|
|
718
|
+
app.showSelect('Select Language', languages, currentLang, (item) => {
|
|
719
|
+
config.set('language', item.key);
|
|
720
|
+
app.notify(`Language: ${item.label}`);
|
|
721
|
+
});
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
// Login/Logout
|
|
725
|
+
case 'login': {
|
|
726
|
+
const providers = getProviderList();
|
|
727
|
+
app.showLogin(providers.map(p => ({ id: p.id, name: p.name })), async (result) => {
|
|
728
|
+
if (result) {
|
|
729
|
+
setProvider(result.providerId);
|
|
730
|
+
await setApiKey(result.apiKey);
|
|
731
|
+
app.notify('Logged in successfully');
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
case 'logout': {
|
|
737
|
+
const providers = getProviderList();
|
|
738
|
+
const currentProvider = getCurrentProvider();
|
|
739
|
+
const configuredProviders = providers
|
|
740
|
+
.filter(p => {
|
|
741
|
+
// Check if provider has an API key configured
|
|
742
|
+
try {
|
|
743
|
+
const key = config.get(`apiKey_${p.id}`) || config.get('apiKey');
|
|
744
|
+
return !!key;
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
})
|
|
750
|
+
.map(p => ({
|
|
751
|
+
id: p.id,
|
|
752
|
+
name: p.name,
|
|
753
|
+
isCurrent: p.id === currentProvider.id,
|
|
754
|
+
}));
|
|
755
|
+
if (configuredProviders.length === 0) {
|
|
756
|
+
app.notify('No providers configured');
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
app.showLogoutPicker(configuredProviders, (result) => {
|
|
760
|
+
if (result === null) {
|
|
761
|
+
// Cancelled
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (result === 'all') {
|
|
765
|
+
for (const p of configuredProviders) {
|
|
766
|
+
clearApiKey(p.id);
|
|
767
|
+
}
|
|
768
|
+
app.notify('Logged out from all providers');
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
clearApiKey(result);
|
|
772
|
+
const provider = configuredProviders.find(p => p.id === result);
|
|
773
|
+
app.notify(`Logged out from ${provider?.name || result}`);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
// Git commit
|
|
779
|
+
case 'git-commit': {
|
|
780
|
+
const message = args.join(' ');
|
|
781
|
+
if (!message) {
|
|
782
|
+
app.notify('Usage: /git-commit <message>');
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
import('child_process').then(({ execSync }) => {
|
|
786
|
+
try {
|
|
787
|
+
execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
|
|
788
|
+
cwd: projectPath,
|
|
789
|
+
encoding: 'utf-8',
|
|
790
|
+
});
|
|
791
|
+
app.notify('Committed successfully');
|
|
792
|
+
}
|
|
793
|
+
catch (err) {
|
|
794
|
+
app.notify(`Commit failed: ${err.message}`);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
// Code block operations
|
|
800
|
+
case 'copy': {
|
|
801
|
+
const blockNum = args[0] ? parseInt(args[0], 10) : -1;
|
|
802
|
+
const messages = app.getMessages();
|
|
803
|
+
// Find code blocks in messages
|
|
804
|
+
const codeBlocks = [];
|
|
805
|
+
for (const msg of messages) {
|
|
806
|
+
const matches = msg.content.matchAll(/```[\w]*\n([\s\S]*?)```/g);
|
|
807
|
+
for (const match of matches) {
|
|
808
|
+
codeBlocks.push(match[1]);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (codeBlocks.length === 0) {
|
|
812
|
+
app.notify('No code blocks found');
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const index = blockNum === -1 ? codeBlocks.length - 1 : blockNum - 1;
|
|
816
|
+
if (index < 0 || index >= codeBlocks.length) {
|
|
817
|
+
app.notify(`Invalid block number. Available: 1-${codeBlocks.length}`);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
import('../utils/clipboard').then(({ copyToClipboard }) => {
|
|
821
|
+
if (copyToClipboard(codeBlocks[index])) {
|
|
822
|
+
app.notify(`Copied block ${index + 1} to clipboard`);
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
app.notify('Failed to copy to clipboard');
|
|
826
|
+
}
|
|
827
|
+
}).catch(() => {
|
|
828
|
+
app.notify('Clipboard not available');
|
|
829
|
+
});
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
case 'paste': {
|
|
833
|
+
// Same as Ctrl+V - use App's handlePaste
|
|
834
|
+
import('clipboardy').then((clipboardy) => {
|
|
835
|
+
try {
|
|
836
|
+
const content = clipboardy.default.readSync();
|
|
837
|
+
if (content && content.trim()) {
|
|
838
|
+
app.handlePaste(content.trim());
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
app.notify('Clipboard is empty');
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
app.notify('Could not read clipboard');
|
|
846
|
+
}
|
|
847
|
+
}).catch(() => {
|
|
848
|
+
app.notify('Clipboard not available');
|
|
849
|
+
});
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
case 'apply': {
|
|
853
|
+
const messages = app.getMessages();
|
|
854
|
+
const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant');
|
|
855
|
+
if (!lastAssistant) {
|
|
856
|
+
app.notify('No assistant response to apply');
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
// Find file changes in the response
|
|
860
|
+
const filePattern = /```(\w+)?\s*\n\/\/\s*(?:File:|Path:)\s*([^\n]+)\n([\s\S]*?)```/g;
|
|
861
|
+
const changes = [];
|
|
862
|
+
let match;
|
|
863
|
+
while ((match = filePattern.exec(lastAssistant.content)) !== null) {
|
|
864
|
+
changes.push({ path: match[2].trim(), content: match[3] });
|
|
865
|
+
}
|
|
866
|
+
if (changes.length === 0) {
|
|
867
|
+
app.notify('No file changes found in response');
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (!hasWriteAccess) {
|
|
871
|
+
app.notify('Write access required. Use /grant first.');
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
// Show diff preview before applying
|
|
875
|
+
import('fs').then(fs => {
|
|
876
|
+
import('path').then(pathModule => {
|
|
877
|
+
// Generate diff preview
|
|
878
|
+
const diffLines = [];
|
|
879
|
+
for (const change of changes) {
|
|
880
|
+
const fullPath = pathModule.isAbsolute(change.path)
|
|
881
|
+
? change.path
|
|
882
|
+
: pathModule.join(projectPath, change.path);
|
|
883
|
+
const shortPath = change.path.length > 40
|
|
884
|
+
? '...' + change.path.slice(-37)
|
|
885
|
+
: change.path;
|
|
886
|
+
// Check if file exists (create vs modify)
|
|
887
|
+
let existingContent = '';
|
|
888
|
+
try {
|
|
889
|
+
existingContent = fs.readFileSync(fullPath, 'utf-8');
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
// File doesn't exist - will be created
|
|
893
|
+
}
|
|
894
|
+
if (!existingContent) {
|
|
895
|
+
diffLines.push(`+ CREATE: ${shortPath}`);
|
|
896
|
+
diffLines.push(` (${change.content.split('\n').length} lines)`);
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
// Simple diff: count lines added/removed
|
|
900
|
+
const oldLines = existingContent.split('\n').length;
|
|
901
|
+
const newLines = change.content.split('\n').length;
|
|
902
|
+
const lineDiff = newLines - oldLines;
|
|
903
|
+
diffLines.push(`~ MODIFY: ${shortPath}`);
|
|
904
|
+
diffLines.push(` ${oldLines} → ${newLines} lines (${lineDiff >= 0 ? '+' : ''}${lineDiff})`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
// Show confirmation with diff preview
|
|
908
|
+
app.showConfirm({
|
|
909
|
+
title: '📝 Apply Changes',
|
|
910
|
+
message: [
|
|
911
|
+
`Found ${changes.length} file(s) to apply:`,
|
|
912
|
+
'',
|
|
913
|
+
...diffLines.slice(0, 10),
|
|
914
|
+
...(diffLines.length > 10 ? [` ...and ${diffLines.length - 10} more`] : []),
|
|
915
|
+
'',
|
|
916
|
+
'Apply these changes?',
|
|
917
|
+
],
|
|
918
|
+
confirmLabel: 'Apply',
|
|
919
|
+
cancelLabel: 'Cancel',
|
|
920
|
+
onConfirm: () => {
|
|
921
|
+
let applied = 0;
|
|
922
|
+
for (const change of changes) {
|
|
923
|
+
try {
|
|
924
|
+
const fullPath = pathModule.isAbsolute(change.path)
|
|
925
|
+
? change.path
|
|
926
|
+
: pathModule.join(projectPath, change.path);
|
|
927
|
+
fs.mkdirSync(pathModule.dirname(fullPath), { recursive: true });
|
|
928
|
+
fs.writeFileSync(fullPath, change.content);
|
|
929
|
+
applied++;
|
|
930
|
+
}
|
|
931
|
+
catch (err) {
|
|
932
|
+
// Skip failed writes
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
app.notify(`Applied ${applied}/${changes.length} file(s)`);
|
|
936
|
+
},
|
|
937
|
+
onCancel: () => {
|
|
938
|
+
app.notify('Apply cancelled');
|
|
939
|
+
},
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
// Agent history and changes
|
|
946
|
+
case 'history': {
|
|
947
|
+
import('../utils/agent').then(({ getAgentHistory }) => {
|
|
948
|
+
const history = getAgentHistory();
|
|
949
|
+
if (history.length === 0) {
|
|
950
|
+
app.notify('No agent history');
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const items = history.slice(0, 10).map(h => `${new Date(h.timestamp).toLocaleString()} - ${h.task.slice(0, 30)}...`);
|
|
954
|
+
app.showList('Agent History', items, (index) => {
|
|
955
|
+
const selected = history[index];
|
|
956
|
+
app.addMessage({
|
|
957
|
+
role: 'system',
|
|
958
|
+
content: `# Agent Session\n\n**Task:** ${selected.task}\n**Actions:** ${selected.actions.length}\n**Status:** ${selected.success ? '✓ Success' : '✗ Failed'}`,
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
}).catch(() => {
|
|
962
|
+
app.notify('No agent history available');
|
|
963
|
+
});
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
case 'changes': {
|
|
967
|
+
import('../utils/agent').then(({ getCurrentSessionActions }) => {
|
|
968
|
+
const actions = getCurrentSessionActions();
|
|
969
|
+
if (actions.length === 0) {
|
|
970
|
+
app.notify('No changes in current session');
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
const summary = actions.map(a => `• ${a.type}: ${a.target} (${a.result})`).join('\n');
|
|
974
|
+
app.addMessage({
|
|
975
|
+
role: 'system',
|
|
976
|
+
content: `# Session Changes\n\n${summary}`,
|
|
977
|
+
});
|
|
978
|
+
}).catch(() => {
|
|
979
|
+
app.notify('No changes tracked');
|
|
980
|
+
});
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
// Context persistence
|
|
984
|
+
case 'context-save': {
|
|
985
|
+
const messages = app.getMessages();
|
|
986
|
+
if (saveSession(`context-${sessionId}`, messages, projectPath)) {
|
|
987
|
+
app.notify('Context saved');
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
app.notify('Failed to save context');
|
|
991
|
+
}
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
case 'context-load': {
|
|
995
|
+
const contextName = `context-${sessionId}`;
|
|
996
|
+
const loaded = loadSession(contextName, projectPath);
|
|
997
|
+
if (loaded) {
|
|
998
|
+
app.setMessages(loaded);
|
|
999
|
+
app.notify('Context loaded');
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
app.notify('No saved context found');
|
|
1003
|
+
}
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
case 'context-clear': {
|
|
1007
|
+
deleteSession(`context-${sessionId}`, projectPath);
|
|
1008
|
+
app.notify('Context cleared');
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
// Learning mode
|
|
1012
|
+
case 'learn': {
|
|
1013
|
+
if (args[0] === 'status') {
|
|
1014
|
+
import('../utils/learning').then(({ getLearningStatus }) => {
|
|
1015
|
+
const status = getLearningStatus(projectPath);
|
|
1016
|
+
app.addMessage({
|
|
1017
|
+
role: 'system',
|
|
1018
|
+
content: `# Learning Status\n\n${status}`,
|
|
1019
|
+
});
|
|
1020
|
+
}).catch(() => {
|
|
1021
|
+
app.notify('Learning module not available');
|
|
1022
|
+
});
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (args[0] === 'rule' && args.length > 1) {
|
|
1026
|
+
import('../utils/learning').then(({ addCustomRule }) => {
|
|
1027
|
+
addCustomRule(projectPath, args.slice(1).join(' '));
|
|
1028
|
+
app.notify('Custom rule added');
|
|
1029
|
+
}).catch(() => {
|
|
1030
|
+
app.notify('Learning module not available');
|
|
1031
|
+
});
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (!projectContext) {
|
|
1035
|
+
app.notify('No project context');
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
app.notify('Learning from project...');
|
|
1039
|
+
import('../utils/learning').then(({ learnFromProject, formatPreferencesForPrompt }) => {
|
|
1040
|
+
// Get some source files to learn from
|
|
1041
|
+
import('fs').then(fs => {
|
|
1042
|
+
import('path').then(path => {
|
|
1043
|
+
const files = [];
|
|
1044
|
+
const extensions = ['.ts', '.js', '.tsx', '.jsx', '.py', '.go', '.rs'];
|
|
1045
|
+
const walkDir = (dir, depth = 0) => {
|
|
1046
|
+
if (depth > 3 || files.length >= 20)
|
|
1047
|
+
return;
|
|
1048
|
+
try {
|
|
1049
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1050
|
+
for (const entry of entries) {
|
|
1051
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
1052
|
+
continue;
|
|
1053
|
+
const fullPath = path.join(dir, entry.name);
|
|
1054
|
+
if (entry.isDirectory()) {
|
|
1055
|
+
walkDir(fullPath, depth + 1);
|
|
1056
|
+
}
|
|
1057
|
+
else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
1058
|
+
files.push(path.relative(projectContext.root, fullPath));
|
|
1059
|
+
}
|
|
1060
|
+
if (files.length >= 20)
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
catch { }
|
|
1065
|
+
};
|
|
1066
|
+
walkDir(projectContext.root);
|
|
1067
|
+
if (files.length === 0) {
|
|
1068
|
+
app.notify('No source files found to learn from');
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const prefs = learnFromProject(projectContext.root, files);
|
|
1072
|
+
const formatted = formatPreferencesForPrompt(prefs);
|
|
1073
|
+
app.addMessage({
|
|
1074
|
+
role: 'system',
|
|
1075
|
+
content: `# Learned Preferences\n\n${formatted}`,
|
|
1076
|
+
});
|
|
1077
|
+
app.notify(`Learned from ${files.length} files`);
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
}).catch(() => {
|
|
1081
|
+
app.notify('Learning module not available');
|
|
1082
|
+
});
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
// Skills shortcuts
|
|
1086
|
+
case 'c': {
|
|
1087
|
+
handleCommand('commit', []);
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
case 't': {
|
|
1091
|
+
if (!projectContext) {
|
|
1092
|
+
app.notify('No project context');
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
app.addMessage({ role: 'user', content: '/test' });
|
|
1096
|
+
handleSubmit('Generate and run tests for the current project. Focus on untested code.');
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
case 'd': {
|
|
1100
|
+
if (!projectContext) {
|
|
1101
|
+
app.notify('No project context');
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
app.addMessage({ role: 'user', content: '/docs' });
|
|
1105
|
+
handleSubmit('Add documentation to the code. Focus on functions and classes that lack proper documentation.');
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
case 'r': {
|
|
1109
|
+
if (!projectContext) {
|
|
1110
|
+
app.notify('No project context');
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
app.addMessage({ role: 'user', content: '/refactor' });
|
|
1114
|
+
handleSubmit('Refactor the code to improve quality, readability, and maintainability.');
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
1117
|
+
case 'f': {
|
|
1118
|
+
if (!projectContext) {
|
|
1119
|
+
app.notify('No project context');
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
app.addMessage({ role: 'user', content: '/fix' });
|
|
1123
|
+
handleSubmit('Debug and fix any issues in the current code. Look for bugs, errors, and potential problems.');
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
case 'e': {
|
|
1127
|
+
if (!args.length) {
|
|
1128
|
+
app.notify('Usage: /e <file or code to explain>');
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
app.addMessage({ role: 'user', content: `/explain ${args.join(' ')}` });
|
|
1132
|
+
handleSubmit(`Explain this code or concept: ${args.join(' ')}`);
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
case 'o': {
|
|
1136
|
+
if (!projectContext) {
|
|
1137
|
+
app.notify('No project context');
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
app.addMessage({ role: 'user', content: '/optimize' });
|
|
1141
|
+
handleSubmit('Optimize the code for better performance. Focus on efficiency and speed improvements.');
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
case 'b': {
|
|
1145
|
+
if (!projectContext) {
|
|
1146
|
+
app.notify('No project context');
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
app.addMessage({ role: 'user', content: '/debug' });
|
|
1150
|
+
handleSubmit('Help debug the current issue. Analyze the code and identify the root cause of problems.');
|
|
1151
|
+
break;
|
|
1152
|
+
}
|
|
1153
|
+
case 'p': {
|
|
1154
|
+
// Push shortcut
|
|
1155
|
+
import('child_process').then(({ execSync }) => {
|
|
1156
|
+
try {
|
|
1157
|
+
execSync('git push', { cwd: projectPath, encoding: 'utf-8' });
|
|
1158
|
+
app.notify('Pushed successfully');
|
|
1159
|
+
}
|
|
1160
|
+
catch (err) {
|
|
1161
|
+
app.notify(`Push failed: ${err.message}`);
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
// Full skill names
|
|
1167
|
+
case 'test':
|
|
1168
|
+
case 'docs':
|
|
1169
|
+
case 'refactor':
|
|
1170
|
+
case 'fix':
|
|
1171
|
+
case 'explain':
|
|
1172
|
+
case 'optimize':
|
|
1173
|
+
case 'debug': {
|
|
1174
|
+
const skillMap = {
|
|
1175
|
+
test: 't',
|
|
1176
|
+
docs: 'd',
|
|
1177
|
+
refactor: 'r',
|
|
1178
|
+
fix: 'f',
|
|
1179
|
+
explain: 'e',
|
|
1180
|
+
optimize: 'o',
|
|
1181
|
+
debug: 'b',
|
|
1182
|
+
};
|
|
1183
|
+
handleCommand(skillMap[command], args);
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
case 'push': {
|
|
1187
|
+
handleCommand('p', args);
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
case 'pull': {
|
|
1191
|
+
import('child_process').then(({ execSync }) => {
|
|
1192
|
+
try {
|
|
1193
|
+
execSync('git pull', { cwd: projectPath, encoding: 'utf-8' });
|
|
1194
|
+
app.notify('Pulled successfully');
|
|
1195
|
+
}
|
|
1196
|
+
catch (err) {
|
|
1197
|
+
app.notify(`Pull failed: ${err.message}`);
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1202
|
+
case 'skills': {
|
|
1203
|
+
import('../utils/skills').then(({ getAllSkills, searchSkills, formatSkillsList, getSkillStats }) => {
|
|
1204
|
+
const query = args.join(' ').toLowerCase();
|
|
1205
|
+
// Check for stats subcommand
|
|
1206
|
+
if (query === 'stats') {
|
|
1207
|
+
const stats = getSkillStats();
|
|
1208
|
+
app.addMessage({
|
|
1209
|
+
role: 'system',
|
|
1210
|
+
content: `# Skill Statistics\n\n- Total usage: ${stats.totalUsage}\n- Unique skills used: ${stats.uniqueSkills}\n- Success rate: ${stats.successRate}%`,
|
|
1211
|
+
});
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const skills = query ? searchSkills(query) : getAllSkills();
|
|
1215
|
+
if (skills.length === 0) {
|
|
1216
|
+
app.notify(`No skills matching "${query}"`);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
app.addMessage({
|
|
1220
|
+
role: 'system',
|
|
1221
|
+
content: formatSkillsList(skills),
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
case 'skill': {
|
|
1227
|
+
import('../utils/skills').then(({ findSkill, formatSkillHelp, createSkillTemplate, saveCustomSkill, deleteCustomSkill }) => {
|
|
1228
|
+
const subCommand = args[0]?.toLowerCase();
|
|
1229
|
+
const skillName = args[1];
|
|
1230
|
+
if (!subCommand) {
|
|
1231
|
+
app.notify('Usage: /skill <help|create|delete> <name>');
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
switch (subCommand) {
|
|
1235
|
+
case 'help': {
|
|
1236
|
+
if (!skillName) {
|
|
1237
|
+
app.notify('Usage: /skill help <skill-name>');
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const skill = findSkill(skillName);
|
|
1241
|
+
if (!skill) {
|
|
1242
|
+
app.notify(`Skill not found: ${skillName}`);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
app.addMessage({
|
|
1246
|
+
role: 'system',
|
|
1247
|
+
content: formatSkillHelp(skill),
|
|
1248
|
+
});
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
case 'create': {
|
|
1252
|
+
if (!skillName) {
|
|
1253
|
+
app.notify('Usage: /skill create <name>');
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if (findSkill(skillName)) {
|
|
1257
|
+
app.notify(`Skill "${skillName}" already exists`);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const template = createSkillTemplate(skillName);
|
|
1261
|
+
saveCustomSkill(template);
|
|
1262
|
+
app.addMessage({
|
|
1263
|
+
role: 'system',
|
|
1264
|
+
content: `# Custom Skill Created: ${skillName}\n\nEdit the skill file at:\n~/.codeep/skills/${skillName}.json\n\nTemplate:\n\`\`\`json\n${JSON.stringify(template, null, 2)}\n\`\`\``,
|
|
1265
|
+
});
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
case 'delete': {
|
|
1269
|
+
if (!skillName) {
|
|
1270
|
+
app.notify('Usage: /skill delete <name>');
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (deleteCustomSkill(skillName)) {
|
|
1274
|
+
app.notify(`Deleted skill: ${skillName}`);
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
app.notify(`Could not delete skill: ${skillName}`);
|
|
1278
|
+
}
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
default: {
|
|
1282
|
+
// Try to run the skill by name
|
|
1283
|
+
const skill = findSkill(subCommand);
|
|
1284
|
+
if (skill) {
|
|
1285
|
+
app.notify(`Running skill: ${skill.name}`);
|
|
1286
|
+
// For now just show the description
|
|
1287
|
+
app.addMessage({
|
|
1288
|
+
role: 'system',
|
|
1289
|
+
content: `**/${skill.name}**: ${skill.description}`,
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
else {
|
|
1293
|
+
app.notify(`Unknown skill command: ${subCommand}`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
default:
|
|
1301
|
+
app.notify(`Unknown command: /${command}`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Show login flow for API key setup
|
|
1306
|
+
*/
|
|
1307
|
+
async function showLoginFlow() {
|
|
1308
|
+
return new Promise((resolve) => {
|
|
1309
|
+
const screen = new Screen();
|
|
1310
|
+
const input = new Input();
|
|
1311
|
+
const providers = getProviderList();
|
|
1312
|
+
let currentStep = 'provider';
|
|
1313
|
+
let selectedProviderIndex = 0;
|
|
1314
|
+
let selectedProvider = providers[0];
|
|
1315
|
+
let loginScreen = null;
|
|
1316
|
+
let loginError = '';
|
|
1317
|
+
screen.init();
|
|
1318
|
+
input.start();
|
|
1319
|
+
const cleanup = () => {
|
|
1320
|
+
input.stop();
|
|
1321
|
+
screen.cleanup();
|
|
1322
|
+
};
|
|
1323
|
+
const renderCurrentStep = () => {
|
|
1324
|
+
if (currentStep === 'provider') {
|
|
1325
|
+
renderProviderSelect(screen, providers, selectedProviderIndex);
|
|
1326
|
+
}
|
|
1327
|
+
else if (loginScreen) {
|
|
1328
|
+
loginScreen.render();
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
input.onKey((event) => {
|
|
1332
|
+
if (currentStep === 'provider') {
|
|
1333
|
+
// Provider selection
|
|
1334
|
+
if (event.key === 'up') {
|
|
1335
|
+
selectedProviderIndex = Math.max(0, selectedProviderIndex - 1);
|
|
1336
|
+
renderCurrentStep();
|
|
1337
|
+
}
|
|
1338
|
+
else if (event.key === 'down') {
|
|
1339
|
+
selectedProviderIndex = Math.min(providers.length - 1, selectedProviderIndex + 1);
|
|
1340
|
+
renderCurrentStep();
|
|
1341
|
+
}
|
|
1342
|
+
else if (event.key === 'enter') {
|
|
1343
|
+
selectedProvider = providers[selectedProviderIndex];
|
|
1344
|
+
setProvider(selectedProvider.id);
|
|
1345
|
+
// Move to API key entry
|
|
1346
|
+
currentStep = 'apikey';
|
|
1347
|
+
loginScreen = new LoginScreen(screen, input, {
|
|
1348
|
+
providerName: selectedProvider.name,
|
|
1349
|
+
error: loginError,
|
|
1350
|
+
onSubmit: async (key) => {
|
|
1351
|
+
// Validate and save key
|
|
1352
|
+
if (key.length < 10) {
|
|
1353
|
+
loginError = 'API key too short';
|
|
1354
|
+
loginScreen = new LoginScreen(screen, input, {
|
|
1355
|
+
providerName: selectedProvider.name,
|
|
1356
|
+
error: loginError,
|
|
1357
|
+
onSubmit: () => { },
|
|
1358
|
+
onCancel: () => {
|
|
1359
|
+
cleanup();
|
|
1360
|
+
resolve(null);
|
|
1361
|
+
},
|
|
1362
|
+
});
|
|
1363
|
+
renderCurrentStep();
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
// Save the key
|
|
1367
|
+
await setApiKey(key);
|
|
1368
|
+
cleanup();
|
|
1369
|
+
resolve(key);
|
|
1370
|
+
},
|
|
1371
|
+
onCancel: () => {
|
|
1372
|
+
// Go back to provider selection
|
|
1373
|
+
currentStep = 'provider';
|
|
1374
|
+
loginScreen = null;
|
|
1375
|
+
loginError = '';
|
|
1376
|
+
renderCurrentStep();
|
|
1377
|
+
},
|
|
1378
|
+
});
|
|
1379
|
+
renderCurrentStep();
|
|
1380
|
+
}
|
|
1381
|
+
else if (event.key === 'escape') {
|
|
1382
|
+
cleanup();
|
|
1383
|
+
resolve(null);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
else if (loginScreen) {
|
|
1387
|
+
loginScreen.handleKey(event);
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
// Initial render
|
|
1391
|
+
renderCurrentStep();
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Show permission screen
|
|
1396
|
+
*/
|
|
1397
|
+
async function showPermissionFlow() {
|
|
1398
|
+
return new Promise((resolve) => {
|
|
1399
|
+
const screen = new Screen();
|
|
1400
|
+
const input = new Input();
|
|
1401
|
+
let selectedIndex = 0;
|
|
1402
|
+
const options = getPermissionOptions();
|
|
1403
|
+
const isProject = isProjectDirectory(projectPath);
|
|
1404
|
+
const currentPermission = hasWritePermission(projectPath)
|
|
1405
|
+
? 'write'
|
|
1406
|
+
: hasReadPermission(projectPath)
|
|
1407
|
+
? 'read'
|
|
1408
|
+
: 'none';
|
|
1409
|
+
screen.init();
|
|
1410
|
+
input.start();
|
|
1411
|
+
const cleanup = () => {
|
|
1412
|
+
input.stop();
|
|
1413
|
+
screen.cleanup();
|
|
1414
|
+
};
|
|
1415
|
+
const render = () => {
|
|
1416
|
+
renderPermissionScreen(screen, {
|
|
1417
|
+
projectPath,
|
|
1418
|
+
isProject,
|
|
1419
|
+
currentPermission,
|
|
1420
|
+
onSelect: () => { },
|
|
1421
|
+
onCancel: () => { },
|
|
1422
|
+
}, selectedIndex);
|
|
1423
|
+
};
|
|
1424
|
+
input.onKey((event) => {
|
|
1425
|
+
if (event.key === 'up') {
|
|
1426
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
1427
|
+
render();
|
|
1428
|
+
}
|
|
1429
|
+
else if (event.key === 'down') {
|
|
1430
|
+
selectedIndex = Math.min(options.length - 1, selectedIndex + 1);
|
|
1431
|
+
render();
|
|
1432
|
+
}
|
|
1433
|
+
else if (event.key === 'enter') {
|
|
1434
|
+
const selected = options[selectedIndex];
|
|
1435
|
+
cleanup();
|
|
1436
|
+
resolve(selected);
|
|
1437
|
+
}
|
|
1438
|
+
else if (event.key === 'escape') {
|
|
1439
|
+
cleanup();
|
|
1440
|
+
resolve('none');
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
render();
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Initialize and start
|
|
1448
|
+
*/
|
|
1449
|
+
async function main() {
|
|
1450
|
+
// Handle CLI flags
|
|
1451
|
+
const args = process.argv.slice(2);
|
|
1452
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
1453
|
+
console.log(`Codeep v${getCurrentVersion()}`);
|
|
1454
|
+
process.exit(0);
|
|
1455
|
+
}
|
|
1456
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
1457
|
+
console.log(`
|
|
1458
|
+
Codeep - AI-powered coding assistant TUI
|
|
1459
|
+
|
|
1460
|
+
Usage:
|
|
1461
|
+
codeep Start interactive chat
|
|
1462
|
+
codeep --version Show version
|
|
1463
|
+
codeep --help Show this help
|
|
1464
|
+
|
|
1465
|
+
Commands (in chat):
|
|
1466
|
+
/help Show all available commands
|
|
1467
|
+
/status Show current status
|
|
1468
|
+
/version Show version and current model
|
|
1469
|
+
/exit Quit application
|
|
1470
|
+
`);
|
|
1471
|
+
process.exit(0);
|
|
1472
|
+
}
|
|
1473
|
+
// Load API keys
|
|
1474
|
+
await loadAllApiKeys();
|
|
1475
|
+
let apiKey = await loadApiKey();
|
|
1476
|
+
// If no API key, show login screen
|
|
1477
|
+
if (!apiKey) {
|
|
1478
|
+
const newKey = await showLoginFlow();
|
|
1479
|
+
if (!newKey) {
|
|
1480
|
+
console.log('\nSetup cancelled.');
|
|
1481
|
+
process.exit(0);
|
|
1482
|
+
}
|
|
1483
|
+
apiKey = newKey;
|
|
1484
|
+
}
|
|
1485
|
+
// Check project permissions
|
|
1486
|
+
const isProject = isProjectDirectory(projectPath);
|
|
1487
|
+
let hasRead = hasReadPermission(projectPath);
|
|
1488
|
+
const needsPermissionDialog = !hasRead && isProject;
|
|
1489
|
+
// If already has permission, load context
|
|
1490
|
+
if (hasRead) {
|
|
1491
|
+
hasWriteAccess = hasWritePermission(projectPath);
|
|
1492
|
+
projectContext = getProjectContext(projectPath);
|
|
1493
|
+
if (projectContext) {
|
|
1494
|
+
projectContext.hasWriteAccess = hasWriteAccess;
|
|
1495
|
+
setProjectContext(projectContext);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
// Create and start app
|
|
1499
|
+
app = new App({
|
|
1500
|
+
onSubmit: handleSubmit,
|
|
1501
|
+
onCommand: handleCommand,
|
|
1502
|
+
onExit: () => {
|
|
1503
|
+
console.log('\nGoodbye!');
|
|
1504
|
+
process.exit(0);
|
|
1505
|
+
},
|
|
1506
|
+
getStatus,
|
|
1507
|
+
hasWriteAccess: () => hasWriteAccess,
|
|
1508
|
+
hasProjectContext: () => projectContext !== null,
|
|
1509
|
+
});
|
|
1510
|
+
// Welcome message with contextual info
|
|
1511
|
+
const provider = getCurrentProvider();
|
|
1512
|
+
const providers = getProviderList();
|
|
1513
|
+
const providerInfo = providers.find(p => p.id === provider.id);
|
|
1514
|
+
const version = getCurrentVersion();
|
|
1515
|
+
const model = config.get('model');
|
|
1516
|
+
const agentMode = config.get('agentMode') || 'off';
|
|
1517
|
+
// Build welcome message
|
|
1518
|
+
let welcomeLines = [
|
|
1519
|
+
`Codeep v${version} • ${providerInfo?.name} • ${model}`,
|
|
1520
|
+
'',
|
|
1521
|
+
];
|
|
1522
|
+
// Add access level info
|
|
1523
|
+
if (projectContext) {
|
|
1524
|
+
if (hasWriteAccess) {
|
|
1525
|
+
welcomeLines.push(`Project: ${projectPath}`);
|
|
1526
|
+
welcomeLines.push(`Access: Read & Write (Agent enabled)`);
|
|
1527
|
+
}
|
|
1528
|
+
else {
|
|
1529
|
+
welcomeLines.push(`Project: ${projectPath}`);
|
|
1530
|
+
welcomeLines.push(`Access: Read Only (/grant to enable Agent)`);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
else {
|
|
1534
|
+
welcomeLines.push(`Mode: Chat only (no project context)`);
|
|
1535
|
+
}
|
|
1536
|
+
// Add agent mode warning if enabled
|
|
1537
|
+
if (agentMode === 'on' && hasWriteAccess) {
|
|
1538
|
+
welcomeLines.push('');
|
|
1539
|
+
welcomeLines.push('⚠ Agent Mode ON: Messages will auto-execute as agent tasks');
|
|
1540
|
+
}
|
|
1541
|
+
// Add shortcuts hint
|
|
1542
|
+
welcomeLines.push('');
|
|
1543
|
+
welcomeLines.push('Shortcuts: /help commands • Ctrl+L clear • Esc cancel');
|
|
1544
|
+
app.addMessage({
|
|
1545
|
+
role: 'system',
|
|
1546
|
+
content: welcomeLines.join('\n'),
|
|
1547
|
+
});
|
|
1548
|
+
app.start();
|
|
1549
|
+
// Show intro animation first (if terminal is large enough)
|
|
1550
|
+
const showIntroAnimation = process.stdout.rows >= 20;
|
|
1551
|
+
const continueStartup = () => {
|
|
1552
|
+
// Show permission dialog inline if needed
|
|
1553
|
+
if (needsPermissionDialog) {
|
|
1554
|
+
app.showPermission(projectPath, isProject, (permission) => {
|
|
1555
|
+
if (permission === 'read') {
|
|
1556
|
+
setProjectPermission(projectPath, true, false);
|
|
1557
|
+
hasWriteAccess = false;
|
|
1558
|
+
projectContext = getProjectContext(projectPath);
|
|
1559
|
+
if (projectContext) {
|
|
1560
|
+
projectContext.hasWriteAccess = false;
|
|
1561
|
+
setProjectContext(projectContext);
|
|
1562
|
+
}
|
|
1563
|
+
app.notify('Read-only access granted');
|
|
1564
|
+
}
|
|
1565
|
+
else if (permission === 'write') {
|
|
1566
|
+
setProjectPermission(projectPath, true, true);
|
|
1567
|
+
hasWriteAccess = true;
|
|
1568
|
+
projectContext = getProjectContext(projectPath);
|
|
1569
|
+
if (projectContext) {
|
|
1570
|
+
projectContext.hasWriteAccess = true;
|
|
1571
|
+
setProjectContext(projectContext);
|
|
1572
|
+
}
|
|
1573
|
+
app.notify('Read & Write access granted');
|
|
1574
|
+
}
|
|
1575
|
+
else {
|
|
1576
|
+
app.notify('No project access - chat only mode');
|
|
1577
|
+
}
|
|
1578
|
+
// After permission, show session picker
|
|
1579
|
+
showSessionPickerInline();
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
else {
|
|
1583
|
+
// No permission needed, show session picker directly
|
|
1584
|
+
showSessionPickerInline();
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
if (showIntroAnimation) {
|
|
1588
|
+
app.startIntro(continueStartup);
|
|
1589
|
+
}
|
|
1590
|
+
else {
|
|
1591
|
+
continueStartup();
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Show session picker inline
|
|
1596
|
+
*/
|
|
1597
|
+
function showSessionPickerInline() {
|
|
1598
|
+
const sessions = listSessionsWithInfo(projectPath);
|
|
1599
|
+
if (sessions.length === 0) {
|
|
1600
|
+
// No sessions, start new one
|
|
1601
|
+
sessionId = startNewSession();
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
app.showSessionPicker(sessions,
|
|
1605
|
+
// Select callback
|
|
1606
|
+
(selectedName) => {
|
|
1607
|
+
if (selectedName === null) {
|
|
1608
|
+
// New session
|
|
1609
|
+
sessionId = startNewSession();
|
|
1610
|
+
app.notify('New session started');
|
|
1611
|
+
}
|
|
1612
|
+
else {
|
|
1613
|
+
// Load existing session
|
|
1614
|
+
const messages = loadSession(selectedName, projectPath);
|
|
1615
|
+
if (messages) {
|
|
1616
|
+
sessionId = selectedName;
|
|
1617
|
+
app.setMessages(messages);
|
|
1618
|
+
app.notify(`Loaded: ${selectedName}`);
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
sessionId = startNewSession();
|
|
1622
|
+
app.notify('Session not found, started new');
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
// Delete callback
|
|
1627
|
+
(sessionName) => {
|
|
1628
|
+
deleteSession(sessionName, projectPath);
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
main().catch((error) => {
|
|
1632
|
+
console.error('Fatal error:', error);
|
|
1633
|
+
process.exit(1);
|
|
1634
|
+
});
|