codeep 1.2.16 → 1.2.18

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