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.
Files changed (45) hide show
  1. package/bin/codeep.js +1 -1
  2. package/dist/config/index.js +10 -10
  3. package/dist/renderer/App.d.ts +430 -0
  4. package/dist/renderer/App.js +2712 -0
  5. package/dist/renderer/ChatUI.d.ts +71 -0
  6. package/dist/renderer/ChatUI.js +286 -0
  7. package/dist/renderer/Input.d.ts +72 -0
  8. package/dist/renderer/Input.js +371 -0
  9. package/dist/renderer/Screen.d.ts +79 -0
  10. package/dist/renderer/Screen.js +278 -0
  11. package/dist/renderer/ansi.d.ts +99 -0
  12. package/dist/renderer/ansi.js +176 -0
  13. package/dist/renderer/components/Box.d.ts +64 -0
  14. package/dist/renderer/components/Box.js +90 -0
  15. package/dist/renderer/components/Help.d.ts +30 -0
  16. package/dist/renderer/components/Help.js +195 -0
  17. package/dist/renderer/components/Intro.d.ts +12 -0
  18. package/dist/renderer/components/Intro.js +128 -0
  19. package/dist/renderer/components/Login.d.ts +42 -0
  20. package/dist/renderer/components/Login.js +178 -0
  21. package/dist/renderer/components/Modal.d.ts +43 -0
  22. package/dist/renderer/components/Modal.js +207 -0
  23. package/dist/renderer/components/Permission.d.ts +20 -0
  24. package/dist/renderer/components/Permission.js +113 -0
  25. package/dist/renderer/components/SelectScreen.d.ts +26 -0
  26. package/dist/renderer/components/SelectScreen.js +101 -0
  27. package/dist/renderer/components/Settings.d.ts +37 -0
  28. package/dist/renderer/components/Settings.js +333 -0
  29. package/dist/renderer/components/Status.d.ts +18 -0
  30. package/dist/renderer/components/Status.js +78 -0
  31. package/dist/renderer/demo-app.d.ts +6 -0
  32. package/dist/renderer/demo-app.js +85 -0
  33. package/dist/renderer/demo.d.ts +6 -0
  34. package/dist/renderer/demo.js +52 -0
  35. package/dist/renderer/index.d.ts +16 -0
  36. package/dist/renderer/index.js +17 -0
  37. package/dist/renderer/main.d.ts +6 -0
  38. package/dist/renderer/main.js +1634 -0
  39. package/dist/utils/agent.d.ts +21 -0
  40. package/dist/utils/agent.js +29 -0
  41. package/dist/utils/clipboard.d.ts +15 -0
  42. package/dist/utils/clipboard.js +95 -0
  43. package/package.json +7 -11
  44. package/dist/utils/console.d.ts +0 -55
  45. 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
+ });