codeep 1.2.17 → 1.2.19

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 (64) hide show
  1. package/README.md +20 -7
  2. package/dist/api/index.d.ts +7 -0
  3. package/dist/api/index.js +21 -17
  4. package/dist/config/providers.d.ts +6 -0
  5. package/dist/config/providers.js +11 -0
  6. package/dist/renderer/App.d.ts +1 -5
  7. package/dist/renderer/App.js +106 -486
  8. package/dist/renderer/agentExecution.d.ts +36 -0
  9. package/dist/renderer/agentExecution.js +394 -0
  10. package/dist/renderer/commands.d.ts +16 -0
  11. package/dist/renderer/commands.js +838 -0
  12. package/dist/renderer/handlers.d.ts +87 -0
  13. package/dist/renderer/handlers.js +260 -0
  14. package/dist/renderer/highlight.d.ts +18 -0
  15. package/dist/renderer/highlight.js +130 -0
  16. package/dist/renderer/main.d.ts +4 -2
  17. package/dist/renderer/main.js +103 -1550
  18. package/dist/utils/agent.d.ts +5 -15
  19. package/dist/utils/agent.js +9 -693
  20. package/dist/utils/agentChat.d.ts +46 -0
  21. package/dist/utils/agentChat.js +343 -0
  22. package/dist/utils/agentStream.d.ts +23 -0
  23. package/dist/utils/agentStream.js +216 -0
  24. package/dist/utils/keychain.js +3 -2
  25. package/dist/utils/learning.js +9 -3
  26. package/dist/utils/mcpIntegration.d.ts +61 -0
  27. package/dist/utils/mcpIntegration.js +154 -0
  28. package/dist/utils/project.js +8 -3
  29. package/dist/utils/skills.js +21 -11
  30. package/dist/utils/smartContext.d.ts +4 -0
  31. package/dist/utils/smartContext.js +51 -14
  32. package/dist/utils/toolExecution.d.ts +27 -0
  33. package/dist/utils/toolExecution.js +525 -0
  34. package/dist/utils/toolParsing.d.ts +18 -0
  35. package/dist/utils/toolParsing.js +302 -0
  36. package/dist/utils/tools.d.ts +11 -24
  37. package/dist/utils/tools.js +22 -1187
  38. package/package.json +3 -1
  39. package/dist/config/config.test.d.ts +0 -1
  40. package/dist/config/config.test.js +0 -157
  41. package/dist/config/providers.test.d.ts +0 -1
  42. package/dist/config/providers.test.js +0 -187
  43. package/dist/hooks/index.d.ts +0 -4
  44. package/dist/hooks/index.js +0 -4
  45. package/dist/hooks/useAgent.d.ts +0 -29
  46. package/dist/hooks/useAgent.js +0 -148
  47. package/dist/utils/agent.test.d.ts +0 -1
  48. package/dist/utils/agent.test.js +0 -315
  49. package/dist/utils/git.test.d.ts +0 -1
  50. package/dist/utils/git.test.js +0 -193
  51. package/dist/utils/gitignore.test.d.ts +0 -1
  52. package/dist/utils/gitignore.test.js +0 -167
  53. package/dist/utils/project.test.d.ts +0 -1
  54. package/dist/utils/project.test.js +0 -212
  55. package/dist/utils/ratelimit.test.d.ts +0 -1
  56. package/dist/utils/ratelimit.test.js +0 -131
  57. package/dist/utils/retry.test.d.ts +0 -1
  58. package/dist/utils/retry.test.js +0 -163
  59. package/dist/utils/smartContext.test.d.ts +0 -1
  60. package/dist/utils/smartContext.test.js +0 -382
  61. package/dist/utils/tools.test.d.ts +0 -1
  62. package/dist/utils/tools.test.js +0 -681
  63. package/dist/utils/validation.test.d.ts +0 -1
  64. package/dist/utils/validation.test.js +0 -164
@@ -0,0 +1,838 @@
1
+ /**
2
+ * Command dispatch for all /command handlers.
3
+ *
4
+ * Extracted from main.ts. Receives an AppCommandContext so it remains
5
+ * decoupled from global state. Import-heavy commands use dynamic imports
6
+ * to keep startup time low.
7
+ */
8
+ import { config, getCurrentProvider, getModelsForCurrentProvider, PROTOCOLS, LANGUAGES, setProvider, setApiKey, clearApiKey, getApiKey, saveSession, startNewSession, loadSession, listSessionsWithInfo, deleteSession, renameSession, setProjectPermission, } from '../config/index.js';
9
+ import { getProjectContext } from '../utils/project.js';
10
+ import { getCurrentVersion } from '../utils/update.js';
11
+ import { getProviderList, getProvider } from '../config/providers.js';
12
+ import { setProjectContext } from '../api/index.js';
13
+ import { runSkill, runCommandChain } from './agentExecution.js';
14
+ // ─── Main dispatch ────────────────────────────────────────────────────────────
15
+ export async function handleCommand(command, args, ctx) {
16
+ // Handle skill chaining (e.g., /commit+push)
17
+ if (command.includes('+')) {
18
+ const commands = command.split('+').filter(c => c.trim());
19
+ runCommandChain(commands, 0, ctx);
20
+ return;
21
+ }
22
+ switch (command) {
23
+ case 'version': {
24
+ const version = getCurrentVersion();
25
+ const provider = getCurrentProvider();
26
+ const providers = getProviderList();
27
+ const providerInfo = providers.find(p => p.id === provider.id);
28
+ ctx.app.notify(`Codeep v${version} • ${providerInfo?.name} • ${config.get('model')}`);
29
+ break;
30
+ }
31
+ case 'provider': {
32
+ const providers = getProviderList();
33
+ const providerItems = providers.map(p => ({
34
+ key: p.id,
35
+ label: p.name,
36
+ description: p.description || '',
37
+ }));
38
+ const currentProvider = getCurrentProvider();
39
+ ctx.app.showSelect('Select Provider', providerItems, currentProvider.id, (item) => {
40
+ if (setProvider(item.key)) {
41
+ ctx.app.notify(`Provider: ${item.label}`);
42
+ }
43
+ });
44
+ break;
45
+ }
46
+ case 'model': {
47
+ const models = getModelsForCurrentProvider();
48
+ const modelItems = Object.entries(models).map(([name, info]) => ({
49
+ key: name,
50
+ label: name,
51
+ description: typeof info === 'object' && info !== null ? info.description || '' : '',
52
+ }));
53
+ const currentModel = config.get('model');
54
+ ctx.app.showSelect('Select Model', modelItems, currentModel, (item) => {
55
+ config.set('model', item.key);
56
+ ctx.app.notify(`Model: ${item.label}`);
57
+ });
58
+ break;
59
+ }
60
+ case 'grant': {
61
+ setProjectPermission(ctx.projectPath, true, true);
62
+ ctx.setHasWriteAccess(true);
63
+ const newCtx = getProjectContext(ctx.projectPath);
64
+ if (newCtx) {
65
+ newCtx.hasWriteAccess = true;
66
+ setProjectContext(newCtx);
67
+ }
68
+ ctx.setProjectContext(newCtx);
69
+ ctx.app.notify('Write access granted');
70
+ break;
71
+ }
72
+ case 'agent': {
73
+ if (!args.length) {
74
+ ctx.app.notify('Usage: /agent <task>');
75
+ return;
76
+ }
77
+ if (ctx.isAgentRunning()) {
78
+ ctx.app.notify('Agent already running. Use /stop to cancel.');
79
+ return;
80
+ }
81
+ const { runAgentTask } = await import('./agentExecution.js');
82
+ runAgentTask(args.join(' '), false, ctx, () => null, () => { });
83
+ break;
84
+ }
85
+ case 'agent-dry': {
86
+ if (!args.length) {
87
+ ctx.app.notify('Usage: /agent-dry <task>');
88
+ return;
89
+ }
90
+ if (ctx.isAgentRunning()) {
91
+ ctx.app.notify('Agent already running. Use /stop to cancel.');
92
+ return;
93
+ }
94
+ const { runAgentTask } = await import('./agentExecution.js');
95
+ runAgentTask(args.join(' '), true, ctx, () => null, () => { });
96
+ break;
97
+ }
98
+ case 'stop': {
99
+ if (ctx.isAgentRunning() && ctx.abortController) {
100
+ ctx.abortController.abort();
101
+ ctx.app.notify('Stopping agent...');
102
+ }
103
+ else {
104
+ ctx.app.notify('No agent running');
105
+ }
106
+ break;
107
+ }
108
+ case 'sessions': {
109
+ const sessions = listSessionsWithInfo(ctx.projectPath);
110
+ if (sessions.length === 0) {
111
+ ctx.app.notify('No saved sessions');
112
+ return;
113
+ }
114
+ ctx.app.showList('Load Session', sessions.map(s => s.name), (index) => {
115
+ const selected = sessions[index];
116
+ const loaded = loadSession(selected.name, ctx.projectPath);
117
+ if (loaded) {
118
+ ctx.app.setMessages(loaded);
119
+ ctx.setSessionId(selected.name);
120
+ ctx.app.notify(`Loaded: ${selected.name}`);
121
+ }
122
+ else {
123
+ ctx.app.notify('Failed to load session');
124
+ }
125
+ });
126
+ break;
127
+ }
128
+ case 'new': {
129
+ ctx.app.clearMessages();
130
+ ctx.setSessionId(startNewSession());
131
+ ctx.app.notify('New session started');
132
+ break;
133
+ }
134
+ case 'settings': {
135
+ ctx.app.showSettings();
136
+ break;
137
+ }
138
+ case 'diff': {
139
+ if (!ctx.projectContext) {
140
+ ctx.app.notify('No project context');
141
+ return;
142
+ }
143
+ const staged = args.includes('--staged') || args.includes('-s');
144
+ ctx.app.notify(staged ? 'Getting staged diff...' : 'Getting diff...');
145
+ import('../utils/git.js').then(({ getGitDiff, formatDiffForDisplay }) => {
146
+ const result = getGitDiff(staged, ctx.projectPath);
147
+ if (!result.success || !result.diff) {
148
+ ctx.app.notify(result.error || 'No changes');
149
+ return;
150
+ }
151
+ const preview = formatDiffForDisplay(result.diff, 50);
152
+ ctx.app.addMessage({ role: 'user', content: `/diff ${staged ? '--staged' : ''}` });
153
+ import('../api/index.js').then(({ chat }) => {
154
+ ctx.app.startStreaming();
155
+ const history = ctx.app.getChatHistory();
156
+ chat(`Review this git diff and provide feedback:\n\n\`\`\`diff\n${preview}\n\`\`\``, history, (chunk) => ctx.app.addStreamChunk(chunk), undefined, ctx.projectContext, undefined).then(() => ctx.app.endStreaming()).catch(() => ctx.app.endStreaming());
157
+ });
158
+ });
159
+ break;
160
+ }
161
+ case 'undo': {
162
+ import('../utils/agent.js').then(({ undoLastAction }) => {
163
+ const result = undoLastAction();
164
+ ctx.app.notify(result.success ? `Undo: ${result.message}` : `Cannot undo: ${result.message}`);
165
+ });
166
+ break;
167
+ }
168
+ case 'undo-all': {
169
+ import('../utils/agent.js').then(({ undoAllActions }) => {
170
+ const result = undoAllActions();
171
+ ctx.app.notify(result.success ? `Undone ${result.results.length} action(s)` : 'Nothing to undo');
172
+ });
173
+ break;
174
+ }
175
+ case 'scan': {
176
+ if (!ctx.projectContext) {
177
+ ctx.app.notify('No project context');
178
+ return;
179
+ }
180
+ ctx.app.notify('Scanning project...');
181
+ import('../utils/projectIntelligence.js').then(({ scanProject, saveProjectIntelligence, generateContextFromIntelligence }) => {
182
+ scanProject(ctx.projectContext.root).then(intelligence => {
183
+ saveProjectIntelligence(ctx.projectContext.root, intelligence);
184
+ const context = generateContextFromIntelligence(intelligence);
185
+ ctx.app.addMessage({ role: 'assistant', content: `# Project Scan Complete\n\n${context}` });
186
+ ctx.app.notify(`Scanned: ${intelligence.structure.totalFiles} files`);
187
+ }).catch(err => {
188
+ ctx.app.notify(`Scan failed: ${err.message}`);
189
+ });
190
+ });
191
+ break;
192
+ }
193
+ case 'review': {
194
+ if (!ctx.projectContext) {
195
+ ctx.app.notify('No project context');
196
+ return;
197
+ }
198
+ import('../utils/codeReview.js').then(({ performCodeReview, formatReviewResult }) => {
199
+ const reviewFiles = args.length > 0 ? args : undefined;
200
+ const result = performCodeReview(ctx.projectContext, reviewFiles);
201
+ ctx.app.addMessage({ role: 'assistant', content: formatReviewResult(result) });
202
+ });
203
+ break;
204
+ }
205
+ case 'update': {
206
+ ctx.app.notify('Checking for updates...');
207
+ import('../utils/update.js').then(({ checkForUpdates, formatVersionInfo }) => {
208
+ checkForUpdates().then(info => {
209
+ ctx.app.notify(formatVersionInfo(info).split('\n')[0], 5000);
210
+ }).catch(() => {
211
+ ctx.app.notify('Failed to check for updates');
212
+ });
213
+ });
214
+ break;
215
+ }
216
+ case 'rename': {
217
+ if (!args.length) {
218
+ ctx.app.notify('Usage: /rename <new-name>');
219
+ return;
220
+ }
221
+ const newName = args.join('-');
222
+ const messages = ctx.app.getMessages();
223
+ if (messages.length === 0) {
224
+ ctx.app.notify('No messages to save. Start a conversation first.');
225
+ return;
226
+ }
227
+ saveSession(ctx.sessionId, messages, ctx.projectPath);
228
+ if (renameSession(ctx.sessionId, newName, ctx.projectPath)) {
229
+ ctx.setSessionId(newName);
230
+ ctx.app.notify(`Session renamed to: ${newName}`);
231
+ }
232
+ else {
233
+ ctx.app.notify('Failed to rename session');
234
+ }
235
+ break;
236
+ }
237
+ case 'search': {
238
+ if (!args.length) {
239
+ ctx.app.notify('Usage: /search <term>');
240
+ return;
241
+ }
242
+ const searchTerm = args.join(' ').toLowerCase();
243
+ const messages = ctx.app.getMessages();
244
+ const searchResults = [];
245
+ messages.forEach((m, index) => {
246
+ if (m.content.toLowerCase().includes(searchTerm)) {
247
+ const lowerContent = m.content.toLowerCase();
248
+ const matchStart = Math.max(0, lowerContent.indexOf(searchTerm) - 30);
249
+ const matchEnd = Math.min(m.content.length, lowerContent.indexOf(searchTerm) + searchTerm.length + 50);
250
+ const matchedText = (matchStart > 0 ? '...' : '') +
251
+ m.content.slice(matchStart, matchEnd).replace(/\n/g, ' ') +
252
+ (matchEnd < m.content.length ? '...' : '');
253
+ searchResults.push({ role: m.role, messageIndex: index, matchedText });
254
+ }
255
+ });
256
+ if (searchResults.length === 0) {
257
+ ctx.app.notify(`No matches for "${searchTerm}"`);
258
+ }
259
+ else {
260
+ ctx.app.showSearch(searchTerm, searchResults, (messageIndex) => ctx.app.scrollToMessage(messageIndex));
261
+ }
262
+ break;
263
+ }
264
+ case 'export': {
265
+ const messages = ctx.app.getMessages();
266
+ if (messages.length === 0) {
267
+ ctx.app.notify('No messages to export');
268
+ return;
269
+ }
270
+ ctx.app.showExport((format) => {
271
+ import('fs').then(fs => {
272
+ import('path').then(path => {
273
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
274
+ let filename;
275
+ let content;
276
+ if (format === 'json') {
277
+ filename = `codeep-export-${timestamp}.json`;
278
+ content = JSON.stringify(messages, null, 2);
279
+ }
280
+ else if (format === 'txt') {
281
+ filename = `codeep-export-${timestamp}.txt`;
282
+ content = messages.map(m => `[${m.role.toUpperCase()}]\n${m.content}\n`).join('\n---\n\n');
283
+ }
284
+ else {
285
+ filename = `codeep-export-${timestamp}.md`;
286
+ 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')}`;
287
+ }
288
+ const exportPath = path.join(ctx.projectPath, filename);
289
+ fs.promises.writeFile(exportPath, content).then(() => {
290
+ ctx.app.notify(`Exported to ${filename}`);
291
+ }).catch((err) => {
292
+ ctx.app.notify(`Export failed: ${err.message}`);
293
+ });
294
+ });
295
+ });
296
+ });
297
+ break;
298
+ }
299
+ case 'protocol': {
300
+ const currentProvider = getCurrentProvider();
301
+ const providerConfig = getProvider(currentProvider.id);
302
+ const protocols = Object.entries(PROTOCOLS)
303
+ .filter(([key]) => providerConfig?.protocols[key])
304
+ .map(([key, name]) => ({ key, label: name }));
305
+ if (protocols.length <= 1) {
306
+ ctx.app.notify(`${currentProvider.name} only supports ${protocols[0]?.label || 'one'} protocol`);
307
+ break;
308
+ }
309
+ const currentProtocol = config.get('protocol') || 'openai';
310
+ ctx.app.showSelect('Select Protocol', protocols, currentProtocol, (item) => {
311
+ config.set('protocol', item.key);
312
+ ctx.app.notify(`Protocol: ${item.label}`);
313
+ });
314
+ break;
315
+ }
316
+ case 'lang': {
317
+ const languages = Object.entries(LANGUAGES).map(([key, name]) => ({ key, label: name }));
318
+ const currentLang = config.get('language') || 'auto';
319
+ ctx.app.showSelect('Select Language', languages, currentLang, (item) => {
320
+ config.set('language', item.key);
321
+ ctx.app.notify(`Language: ${item.label}`);
322
+ });
323
+ break;
324
+ }
325
+ case 'login': {
326
+ const providers = getProviderList();
327
+ ctx.app.showLogin(providers.map(p => ({ id: p.id, name: p.name, subscribeUrl: p.subscribeUrl })), async (result) => {
328
+ if (result) {
329
+ setProvider(result.providerId);
330
+ await setApiKey(result.apiKey);
331
+ ctx.app.notify('Logged in successfully');
332
+ }
333
+ });
334
+ break;
335
+ }
336
+ case 'logout': {
337
+ const providers = getProviderList();
338
+ const currentProvider = getCurrentProvider();
339
+ const configuredProviders = providers
340
+ .filter(p => !!getApiKey(p.id))
341
+ .map(p => ({ id: p.id, name: p.name, isCurrent: p.id === currentProvider.id }));
342
+ if (configuredProviders.length === 0) {
343
+ ctx.app.notify('No providers configured');
344
+ return;
345
+ }
346
+ ctx.app.showLogoutPicker(configuredProviders, (result) => {
347
+ if (result === null)
348
+ return;
349
+ if (result === 'all') {
350
+ for (const p of configuredProviders)
351
+ clearApiKey(p.id);
352
+ ctx.app.notify('Logged out from all providers. Use /login to sign in.');
353
+ }
354
+ else {
355
+ clearApiKey(result);
356
+ const provider = configuredProviders.find(p => p.id === result);
357
+ ctx.app.notify(`Logged out from ${provider?.name || result}`);
358
+ if (result === currentProvider.id) {
359
+ const remaining = configuredProviders.filter(p => p.id !== result);
360
+ if (remaining.length > 0) {
361
+ setProvider(remaining[0].id);
362
+ ctx.app.notify(`Switched to ${remaining[0].name}`);
363
+ }
364
+ else {
365
+ ctx.app.notify('No providers configured. Use /login to sign in.');
366
+ }
367
+ }
368
+ }
369
+ });
370
+ break;
371
+ }
372
+ case 'git-commit': {
373
+ const message = args.join(' ');
374
+ if (!message) {
375
+ ctx.app.notify('Usage: /git-commit <message>');
376
+ return;
377
+ }
378
+ // Use execFile to avoid shell injection — pass commit message as a direct argument
379
+ import('child_process').then(({ execFile }) => {
380
+ execFile('git', ['commit', '-m', message], { cwd: ctx.projectPath, encoding: 'utf-8' }, (err) => {
381
+ if (err) {
382
+ ctx.app.notify(`Commit failed: ${err.message}`);
383
+ }
384
+ else {
385
+ ctx.app.notify('Committed successfully');
386
+ }
387
+ });
388
+ });
389
+ break;
390
+ }
391
+ case 'copy': {
392
+ const blockNum = args[0] ? parseInt(args[0], 10) : -1;
393
+ const messages = ctx.app.getMessages();
394
+ const codeBlocks = [];
395
+ for (const msg of messages) {
396
+ for (const match of msg.content.matchAll(/```[\w]*\n([\s\S]*?)```/g)) {
397
+ codeBlocks.push(match[1]);
398
+ }
399
+ }
400
+ if (codeBlocks.length === 0) {
401
+ ctx.app.notify('No code blocks found');
402
+ return;
403
+ }
404
+ const index = blockNum === -1 ? codeBlocks.length - 1 : blockNum - 1;
405
+ if (index < 0 || index >= codeBlocks.length) {
406
+ ctx.app.notify(`Invalid block number. Available: 1-${codeBlocks.length}`);
407
+ return;
408
+ }
409
+ import('../utils/clipboard.js').then(({ copyToClipboard }) => {
410
+ if (copyToClipboard(codeBlocks[index])) {
411
+ ctx.app.notify(`Copied block ${index + 1} to clipboard`);
412
+ }
413
+ else {
414
+ ctx.app.notify('Failed to copy to clipboard');
415
+ }
416
+ }).catch(() => ctx.app.notify('Clipboard not available'));
417
+ break;
418
+ }
419
+ case 'paste': {
420
+ import('clipboardy').then((clipboardy) => {
421
+ try {
422
+ const content = clipboardy.default.readSync();
423
+ if (content && content.trim()) {
424
+ ctx.app.handlePaste(content.trim());
425
+ }
426
+ else {
427
+ ctx.app.notify('Clipboard is empty');
428
+ }
429
+ }
430
+ catch {
431
+ ctx.app.notify('Could not read clipboard');
432
+ }
433
+ }).catch(() => ctx.app.notify('Clipboard not available'));
434
+ break;
435
+ }
436
+ case 'apply': {
437
+ const messages = ctx.app.getMessages();
438
+ const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant');
439
+ if (!lastAssistant) {
440
+ ctx.app.notify('No assistant response to apply');
441
+ return;
442
+ }
443
+ const changes = [];
444
+ const fenceFilePattern = /```\w*\s+([\w./\\-]+(?:\.\w+))\n([\s\S]*?)```/g;
445
+ let match;
446
+ while ((match = fenceFilePattern.exec(lastAssistant.content)) !== null) {
447
+ const p = match[1].trim();
448
+ if (p.includes('.') && !p.includes(' '))
449
+ changes.push({ path: p, content: match[2] });
450
+ }
451
+ if (changes.length === 0) {
452
+ const commentPattern = /```(\w+)?\s*\n(?:\/\/|#|--|\/\*)\s*(?:File|Path|file|path):\s*([^\n*]+)\n([\s\S]*?)```/g;
453
+ while ((match = commentPattern.exec(lastAssistant.content)) !== null) {
454
+ changes.push({ path: match[2].trim(), content: match[3] });
455
+ }
456
+ }
457
+ if (changes.length === 0) {
458
+ ctx.app.notify('No file changes found in response');
459
+ return;
460
+ }
461
+ if (!ctx.hasWriteAccess) {
462
+ ctx.app.notify('Write access required. Use /grant first.');
463
+ return;
464
+ }
465
+ import('fs').then(async (fs) => {
466
+ import('path').then(async (pathModule) => {
467
+ const diffLines = [];
468
+ for (const change of changes) {
469
+ const fullPath = pathModule.isAbsolute(change.path)
470
+ ? change.path
471
+ : pathModule.join(ctx.projectPath, change.path);
472
+ const shortPath = change.path.length > 40 ? '...' + change.path.slice(-37) : change.path;
473
+ let existingContent = '';
474
+ try {
475
+ existingContent = await fs.promises.readFile(fullPath, 'utf-8');
476
+ }
477
+ catch { }
478
+ if (!existingContent) {
479
+ diffLines.push(`+ CREATE: ${shortPath}`);
480
+ diffLines.push(` (${change.content.split('\n').length} lines)`);
481
+ }
482
+ else {
483
+ const oldLines = existingContent.split('\n').length;
484
+ const newLines = change.content.split('\n').length;
485
+ const lineDiff = newLines - oldLines;
486
+ diffLines.push(`~ MODIFY: ${shortPath}`);
487
+ diffLines.push(` ${oldLines} → ${newLines} lines (${lineDiff >= 0 ? '+' : ''}${lineDiff})`);
488
+ }
489
+ }
490
+ ctx.app.showConfirm({
491
+ title: '📝 Apply Changes',
492
+ message: [
493
+ `Found ${changes.length} file(s) to apply:`,
494
+ '',
495
+ ...diffLines.slice(0, 10),
496
+ ...(diffLines.length > 10 ? [` ...and ${diffLines.length - 10} more`] : []),
497
+ '',
498
+ 'Apply these changes?',
499
+ ],
500
+ confirmLabel: 'Apply',
501
+ cancelLabel: 'Cancel',
502
+ onConfirm: () => {
503
+ (async () => {
504
+ let applied = 0;
505
+ for (const change of changes) {
506
+ try {
507
+ const fullPath = pathModule.isAbsolute(change.path)
508
+ ? change.path
509
+ : pathModule.join(ctx.projectPath, change.path);
510
+ await fs.promises.mkdir(pathModule.dirname(fullPath), { recursive: true });
511
+ await fs.promises.writeFile(fullPath, change.content);
512
+ applied++;
513
+ }
514
+ catch { }
515
+ }
516
+ ctx.app.notify(`Applied ${applied}/${changes.length} file(s)`);
517
+ })();
518
+ },
519
+ onCancel: () => ctx.app.notify('Apply cancelled'),
520
+ });
521
+ });
522
+ });
523
+ break;
524
+ }
525
+ case 'add': {
526
+ if (!args.length) {
527
+ if (ctx.addedFiles.size === 0) {
528
+ ctx.app.notify('Usage: /add <file-path> [file2] ... | No files added');
529
+ }
530
+ else {
531
+ const fileList = Array.from(ctx.addedFiles.values()).map(f => f.relativePath).join(', ');
532
+ ctx.app.notify(`Added files (${ctx.addedFiles.size}): ${fileList}`);
533
+ }
534
+ return;
535
+ }
536
+ const pathMod = await import('path');
537
+ const fsMod = await import('fs');
538
+ const root = ctx.projectContext?.root || ctx.projectPath;
539
+ let added = 0;
540
+ const errors = [];
541
+ for (const filePath of args) {
542
+ const fullPath = pathMod.isAbsolute(filePath) ? filePath : pathMod.join(root, filePath);
543
+ const relativePath = pathMod.isAbsolute(filePath) ? pathMod.relative(root, filePath) : filePath;
544
+ try {
545
+ const stat = await fsMod.promises.stat(fullPath);
546
+ if (!stat.isFile()) {
547
+ errors.push(`${filePath}: not a file`);
548
+ continue;
549
+ }
550
+ if (stat.size > 100000) {
551
+ errors.push(`${filePath}: too large (${Math.round(stat.size / 1024)}KB, max 100KB)`);
552
+ continue;
553
+ }
554
+ const content = await fsMod.promises.readFile(fullPath, 'utf-8');
555
+ ctx.addedFiles.set(fullPath, { relativePath, content });
556
+ added++;
557
+ }
558
+ catch {
559
+ errors.push(`${filePath}: file not found`);
560
+ }
561
+ }
562
+ if (added > 0)
563
+ ctx.app.notify(`Added ${added} file(s) to context (${ctx.addedFiles.size} total)`);
564
+ if (errors.length > 0)
565
+ ctx.app.notify(errors.join(', '));
566
+ break;
567
+ }
568
+ case 'drop': {
569
+ if (!args.length) {
570
+ if (ctx.addedFiles.size === 0) {
571
+ ctx.app.notify('No files in context');
572
+ }
573
+ else {
574
+ const count = ctx.addedFiles.size;
575
+ ctx.addedFiles.clear();
576
+ ctx.app.notify(`Dropped all ${count} file(s) from context`);
577
+ }
578
+ return;
579
+ }
580
+ const pathMod = await import('path');
581
+ const root = ctx.projectContext?.root || ctx.projectPath;
582
+ let dropped = 0;
583
+ for (const filePath of args) {
584
+ const fullPath = pathMod.isAbsolute(filePath) ? filePath : pathMod.join(root, filePath);
585
+ if (ctx.addedFiles.delete(fullPath))
586
+ dropped++;
587
+ }
588
+ if (dropped > 0) {
589
+ ctx.app.notify(`Dropped ${dropped} file(s) (${ctx.addedFiles.size} remaining)`);
590
+ }
591
+ else {
592
+ ctx.app.notify('File not found in context. Use /add to see added files.');
593
+ }
594
+ break;
595
+ }
596
+ case 'history': {
597
+ import('../utils/agent.js').then(({ getAgentHistory }) => {
598
+ const history = getAgentHistory();
599
+ if (history.length === 0) {
600
+ ctx.app.notify('No agent history');
601
+ return;
602
+ }
603
+ const items = history.slice(0, 10).map(h => `${new Date(h.timestamp).toLocaleString()} - ${h.task.slice(0, 30)}...`);
604
+ ctx.app.showList('Agent History', items, (index) => {
605
+ const selected = history[index];
606
+ ctx.app.addMessage({
607
+ role: 'system',
608
+ content: `# Agent Session\n\n**Task:** ${selected.task}\n**Actions:** ${selected.actions.length}\n**Status:** ${selected.success ? '✓ Success' : '✗ Failed'}`,
609
+ });
610
+ });
611
+ }).catch(() => ctx.app.notify('No agent history available'));
612
+ break;
613
+ }
614
+ case 'changes': {
615
+ import('../utils/agent.js').then(({ getCurrentSessionActions }) => {
616
+ const actions = getCurrentSessionActions();
617
+ if (actions.length === 0) {
618
+ ctx.app.notify('No changes in current session');
619
+ return;
620
+ }
621
+ const summary = actions.map(a => `• ${a.type}: ${a.target} (${a.result})`).join('\n');
622
+ ctx.app.addMessage({ role: 'system', content: `# Session Changes\n\n${summary}` });
623
+ }).catch(() => ctx.app.notify('No changes tracked'));
624
+ break;
625
+ }
626
+ case 'context-save': {
627
+ const messages = ctx.app.getMessages();
628
+ if (saveSession(`context-${ctx.sessionId}`, messages, ctx.projectPath)) {
629
+ ctx.app.notify('Context saved');
630
+ }
631
+ else {
632
+ ctx.app.notify('Failed to save context');
633
+ }
634
+ break;
635
+ }
636
+ case 'context-load': {
637
+ const loaded = loadSession(`context-${ctx.sessionId}`, ctx.projectPath);
638
+ if (loaded) {
639
+ ctx.app.setMessages(loaded);
640
+ ctx.app.notify('Context loaded');
641
+ }
642
+ else {
643
+ ctx.app.notify('No saved context found');
644
+ }
645
+ break;
646
+ }
647
+ case 'context-clear': {
648
+ deleteSession(`context-${ctx.sessionId}`, ctx.projectPath);
649
+ ctx.app.notify('Context cleared');
650
+ break;
651
+ }
652
+ case 'learn': {
653
+ if (args[0] === 'status') {
654
+ import('../utils/learning.js').then(({ getLearningStatus }) => {
655
+ const status = getLearningStatus(ctx.projectPath);
656
+ ctx.app.addMessage({ role: 'system', content: `# Learning Status\n\n${status}` });
657
+ }).catch(() => ctx.app.notify('Learning module not available'));
658
+ return;
659
+ }
660
+ if (args[0] === 'rule' && args.length > 1) {
661
+ import('../utils/learning.js').then(({ addCustomRule }) => {
662
+ addCustomRule(ctx.projectPath, args.slice(1).join(' '));
663
+ ctx.app.notify('Custom rule added');
664
+ }).catch(() => ctx.app.notify('Learning module not available'));
665
+ return;
666
+ }
667
+ if (!ctx.projectContext) {
668
+ ctx.app.notify('No project context');
669
+ return;
670
+ }
671
+ ctx.app.notify('Learning from project...');
672
+ import('../utils/learning.js').then(({ learnFromProject, formatPreferencesForPrompt }) => {
673
+ import('fs').then(async (fs) => {
674
+ import('path').then(async (path) => {
675
+ const files = [];
676
+ const extensions = ['.ts', '.js', '.tsx', '.jsx', '.py', '.go', '.rs'];
677
+ const walkDir = async (dir, depth = 0) => {
678
+ if (depth > 3 || files.length >= 20)
679
+ return;
680
+ try {
681
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
682
+ for (const entry of entries) {
683
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
684
+ continue;
685
+ const fullPath = path.join(dir, entry.name);
686
+ if (entry.isDirectory())
687
+ await walkDir(fullPath, depth + 1);
688
+ else if (extensions.some(ext => entry.name.endsWith(ext))) {
689
+ files.push(path.relative(ctx.projectContext.root, fullPath));
690
+ }
691
+ if (files.length >= 20)
692
+ break;
693
+ }
694
+ }
695
+ catch { }
696
+ };
697
+ await walkDir(ctx.projectContext.root);
698
+ if (files.length === 0) {
699
+ ctx.app.notify('No source files found to learn from');
700
+ return;
701
+ }
702
+ const prefs = learnFromProject(ctx.projectContext.root, files);
703
+ const formatted = formatPreferencesForPrompt(prefs);
704
+ ctx.app.addMessage({ role: 'system', content: `# Learned Preferences\n\n${formatted}` });
705
+ ctx.app.notify(`Learned from ${files.length} files`);
706
+ });
707
+ });
708
+ }).catch(() => ctx.app.notify('Learning module not available'));
709
+ break;
710
+ }
711
+ // Built-in skill shortcuts
712
+ case 'c':
713
+ case 'commit':
714
+ case 't':
715
+ case 'test':
716
+ case 'd':
717
+ case 'docs':
718
+ case 'r':
719
+ case 'refactor':
720
+ case 'f':
721
+ case 'fix':
722
+ case 'e':
723
+ case 'explain':
724
+ case 'o':
725
+ case 'optimize':
726
+ case 'b':
727
+ case 'debug':
728
+ case 'p':
729
+ case 'push':
730
+ case 'pull':
731
+ case 'amend':
732
+ case 'pr':
733
+ case 'changelog':
734
+ case 'branch':
735
+ case 'stash':
736
+ case 'unstash':
737
+ case 'build':
738
+ case 'deploy':
739
+ case 'release':
740
+ case 'publish': {
741
+ runSkill(command, args, ctx).catch((err) => {
742
+ ctx.app.notify(`Skill error: ${err.message}`);
743
+ });
744
+ break;
745
+ }
746
+ case 'skills': {
747
+ import('../utils/skills.js').then(({ getAllSkills, searchSkills, formatSkillsList, getSkillStats }) => {
748
+ const query = args.join(' ').toLowerCase();
749
+ if (query === 'stats') {
750
+ const stats = getSkillStats();
751
+ ctx.app.addMessage({
752
+ role: 'system',
753
+ content: `# Skill Statistics\n\n- Total usage: ${stats.totalUsage}\n- Unique skills used: ${stats.uniqueSkills}\n- Success rate: ${stats.successRate}%`,
754
+ });
755
+ return;
756
+ }
757
+ const skills = query ? searchSkills(query) : getAllSkills();
758
+ if (skills.length === 0) {
759
+ ctx.app.notify(`No skills matching "${query}"`);
760
+ return;
761
+ }
762
+ ctx.app.addMessage({ role: 'system', content: formatSkillsList(skills) });
763
+ });
764
+ break;
765
+ }
766
+ case 'skill': {
767
+ import('../utils/skills.js').then(({ findSkill, formatSkillHelp, createSkillTemplate, saveCustomSkill, deleteCustomSkill, }) => {
768
+ const subCommand = args[0]?.toLowerCase();
769
+ const skillName = args[1];
770
+ if (!subCommand) {
771
+ ctx.app.notify('Usage: /skill <help|create|delete> <name>');
772
+ return;
773
+ }
774
+ switch (subCommand) {
775
+ case 'help': {
776
+ if (!skillName) {
777
+ ctx.app.notify('Usage: /skill help <skill-name>');
778
+ return;
779
+ }
780
+ const skill = findSkill(skillName);
781
+ if (!skill) {
782
+ ctx.app.notify(`Skill not found: ${skillName}`);
783
+ return;
784
+ }
785
+ ctx.app.addMessage({ role: 'system', content: formatSkillHelp(skill) });
786
+ break;
787
+ }
788
+ case 'create': {
789
+ if (!skillName) {
790
+ ctx.app.notify('Usage: /skill create <name>');
791
+ return;
792
+ }
793
+ if (findSkill(skillName)) {
794
+ ctx.app.notify(`Skill "${skillName}" already exists`);
795
+ return;
796
+ }
797
+ const template = createSkillTemplate(skillName);
798
+ saveCustomSkill(template);
799
+ ctx.app.addMessage({
800
+ role: 'system',
801
+ 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\`\`\``,
802
+ });
803
+ break;
804
+ }
805
+ case 'delete': {
806
+ if (!skillName) {
807
+ ctx.app.notify('Usage: /skill delete <name>');
808
+ return;
809
+ }
810
+ if (deleteCustomSkill(skillName)) {
811
+ ctx.app.notify(`Deleted skill: ${skillName}`);
812
+ }
813
+ else {
814
+ ctx.app.notify(`Could not delete skill: ${skillName}`);
815
+ }
816
+ break;
817
+ }
818
+ default: {
819
+ const skill = findSkill(subCommand);
820
+ if (skill) {
821
+ ctx.app.notify(`Running skill: ${skill.name}`);
822
+ ctx.app.addMessage({ role: 'system', content: `**/${skill.name}**: ${skill.description}` });
823
+ }
824
+ else {
825
+ ctx.app.notify(`Unknown skill command: ${subCommand}`);
826
+ }
827
+ }
828
+ }
829
+ });
830
+ break;
831
+ }
832
+ default:
833
+ runSkill(command, args, ctx).then(handled => {
834
+ if (!handled)
835
+ ctx.app.notify(`Unknown command: /${command}`);
836
+ });
837
+ }
838
+ }