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.
- package/README.md +20 -7
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.js +21 -17
- package/dist/config/providers.d.ts +6 -0
- package/dist/config/providers.js +11 -0
- package/dist/renderer/App.d.ts +1 -5
- package/dist/renderer/App.js +106 -486
- package/dist/renderer/agentExecution.d.ts +36 -0
- package/dist/renderer/agentExecution.js +394 -0
- package/dist/renderer/commands.d.ts +16 -0
- package/dist/renderer/commands.js +838 -0
- package/dist/renderer/handlers.d.ts +87 -0
- package/dist/renderer/handlers.js +260 -0
- package/dist/renderer/highlight.d.ts +18 -0
- package/dist/renderer/highlight.js +130 -0
- package/dist/renderer/main.d.ts +4 -2
- package/dist/renderer/main.js +103 -1550
- package/dist/utils/agent.d.ts +5 -15
- package/dist/utils/agent.js +9 -693
- package/dist/utils/agentChat.d.ts +46 -0
- package/dist/utils/agentChat.js +343 -0
- package/dist/utils/agentStream.d.ts +23 -0
- package/dist/utils/agentStream.js +216 -0
- package/dist/utils/keychain.js +3 -2
- package/dist/utils/learning.js +9 -3
- package/dist/utils/mcpIntegration.d.ts +61 -0
- package/dist/utils/mcpIntegration.js +154 -0
- package/dist/utils/project.js +8 -3
- package/dist/utils/skills.js +21 -11
- package/dist/utils/smartContext.d.ts +4 -0
- package/dist/utils/smartContext.js +51 -14
- package/dist/utils/toolExecution.d.ts +27 -0
- package/dist/utils/toolExecution.js +525 -0
- package/dist/utils/toolParsing.d.ts +18 -0
- package/dist/utils/toolParsing.js +302 -0
- package/dist/utils/tools.d.ts +11 -24
- package/dist/utils/tools.js +22 -1187
- package/package.json +3 -1
- package/dist/config/config.test.d.ts +0 -1
- package/dist/config/config.test.js +0 -157
- package/dist/config/providers.test.d.ts +0 -1
- package/dist/config/providers.test.js +0 -187
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -4
- package/dist/hooks/useAgent.d.ts +0 -29
- package/dist/hooks/useAgent.js +0 -148
- package/dist/utils/agent.test.d.ts +0 -1
- package/dist/utils/agent.test.js +0 -315
- package/dist/utils/git.test.d.ts +0 -1
- package/dist/utils/git.test.js +0 -193
- package/dist/utils/gitignore.test.d.ts +0 -1
- package/dist/utils/gitignore.test.js +0 -167
- package/dist/utils/project.test.d.ts +0 -1
- package/dist/utils/project.test.js +0 -212
- package/dist/utils/ratelimit.test.d.ts +0 -1
- package/dist/utils/ratelimit.test.js +0 -131
- package/dist/utils/retry.test.d.ts +0 -1
- package/dist/utils/retry.test.js +0 -163
- package/dist/utils/smartContext.test.d.ts +0 -1
- package/dist/utils/smartContext.test.js +0 -382
- package/dist/utils/tools.test.d.ts +0 -1
- package/dist/utils/tools.test.js +0 -681
- package/dist/utils/validation.test.d.ts +0 -1
- package/dist/utils/validation.test.js +0 -164
package/dist/renderer/main.js
CHANGED
|
@@ -1,33 +1,59 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Codeep
|
|
4
|
-
*
|
|
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 {
|
|
14
|
-
import {
|
|
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
|
|
17
|
+
import { getProviderList } from '../config/providers.js';
|
|
18
18
|
import { getSessionStats } from '../utils/tokenTracker.js';
|
|
19
19
|
import { checkApiRateLimit } from '../utils/ratelimit.js';
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
132
|
+
// Auto agent mode
|
|
125
133
|
const agentMode = config.get('agentMode') || 'off';
|
|
126
|
-
if (agentMode === 'on' && projectContext && hasWriteAccess && !
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
161
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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(
|
|
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
|
-
|
|
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);
|