codeep 1.2.29 → 1.2.31
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/dist/acp/commands.d.ts +17 -1
- package/dist/acp/commands.js +312 -26
- package/dist/acp/server.js +104 -53
- package/dist/acp/session.d.ts +6 -0
- package/dist/acp/session.js +18 -2
- package/package.json +1 -1
package/dist/acp/commands.d.ts
CHANGED
|
@@ -6,10 +6,19 @@ export interface AcpSession {
|
|
|
6
6
|
history: Message[];
|
|
7
7
|
/** Codeep session name (maps to .codeep/sessions/<name>.json) */
|
|
8
8
|
codeepSessionId: string;
|
|
9
|
+
/** Files added to context via /add */
|
|
10
|
+
addedFiles: Map<string, {
|
|
11
|
+
relativePath: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}>;
|
|
9
14
|
}
|
|
10
15
|
export interface CommandResult {
|
|
16
|
+
/** true if the input was a slash command (even if it failed) */
|
|
11
17
|
handled: boolean;
|
|
18
|
+
/** Markdown text to stream back to the client */
|
|
12
19
|
response: string;
|
|
20
|
+
/** If true, server should stream response chunks as they arrive (skills) */
|
|
21
|
+
streaming?: boolean;
|
|
13
22
|
}
|
|
14
23
|
/**
|
|
15
24
|
* Ensure workspace has a .codeep folder, initialise it as a project if needed,
|
|
@@ -22,4 +31,11 @@ export declare function initWorkspace(workspaceRoot: string): {
|
|
|
22
31
|
history: Message[];
|
|
23
32
|
welcomeText: string;
|
|
24
33
|
};
|
|
25
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Try to handle a slash command. Async because skills and diff/review
|
|
36
|
+
* need to call the AI API or run shell commands.
|
|
37
|
+
*
|
|
38
|
+
* onChunk is called for streaming output (skills). For simple commands
|
|
39
|
+
* the full response is returned in CommandResult.response.
|
|
40
|
+
*/
|
|
41
|
+
export declare function handleCommand(input: string, session: AcpSession, onChunk: (text: string) => void, abortSignal?: AbortSignal): Promise<CommandResult>;
|
package/dist/acp/commands.js
CHANGED
|
@@ -7,6 +7,8 @@ import { getProviderList, getProvider } from '../config/providers.js';
|
|
|
7
7
|
import { getProjectContext } from '../utils/project.js';
|
|
8
8
|
import { existsSync, mkdirSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
|
+
import { chat } from '../api/index.js';
|
|
11
|
+
import { runAgent } from '../utils/agent.js';
|
|
10
12
|
// ─── Workspace / session init (called on session/new) ─────────────────────────
|
|
11
13
|
/**
|
|
12
14
|
* Ensure workspace has a .codeep folder, initialise it as a project if needed,
|
|
@@ -66,10 +68,21 @@ export function initWorkspace(workspaceRoot) {
|
|
|
66
68
|
'',
|
|
67
69
|
'Type `/help` to see available commands.',
|
|
68
70
|
];
|
|
71
|
+
if (history.length > 0) {
|
|
72
|
+
lines.push('', '---', '');
|
|
73
|
+
lines.push(...formatSessionPreviewLines(history));
|
|
74
|
+
}
|
|
69
75
|
return { codeepSessionId, history, welcomeText: lines.join('\n') };
|
|
70
76
|
}
|
|
71
77
|
// ─── Command dispatch ─────────────────────────────────────────────────────────
|
|
72
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Try to handle a slash command. Async because skills and diff/review
|
|
80
|
+
* need to call the AI API or run shell commands.
|
|
81
|
+
*
|
|
82
|
+
* onChunk is called for streaming output (skills). For simple commands
|
|
83
|
+
* the full response is returned in CommandResult.response.
|
|
84
|
+
*/
|
|
85
|
+
export async function handleCommand(input, session, onChunk, abortSignal) {
|
|
73
86
|
const trimmed = input.trim();
|
|
74
87
|
if (!trimmed.startsWith('/'))
|
|
75
88
|
return { handled: false, response: '' };
|
|
@@ -101,19 +114,17 @@ export function handleCommand(input, session) {
|
|
|
101
114
|
return { handled: true, response: setApiKeyCmd(args[0]) };
|
|
102
115
|
}
|
|
103
116
|
case 'login': {
|
|
104
|
-
// In ACP context login = set API key for a provider
|
|
105
|
-
// Usage: /login <providerId> <apiKey>
|
|
106
117
|
const [providerId, apiKey] = args;
|
|
107
118
|
if (!providerId || !apiKey) {
|
|
108
119
|
return { handled: true, response: 'Usage: `/login <providerId> <apiKey>`\n\n' + buildProviderList() };
|
|
109
120
|
}
|
|
110
121
|
return { handled: true, response: loginCmd(providerId, apiKey) };
|
|
111
122
|
}
|
|
112
|
-
case 'sessions':
|
|
113
|
-
return { handled: true, response: buildSessionList(session.workspaceRoot) };
|
|
114
|
-
}
|
|
123
|
+
case 'sessions':
|
|
115
124
|
case 'session': {
|
|
116
125
|
const sub = args[0];
|
|
126
|
+
if (!sub)
|
|
127
|
+
return { handled: true, response: buildSessionList(session.workspaceRoot) };
|
|
117
128
|
if (sub === 'new') {
|
|
118
129
|
const id = startNewSession();
|
|
119
130
|
session.codeepSessionId = id;
|
|
@@ -125,11 +136,11 @@ export function handleCommand(input, session) {
|
|
|
125
136
|
if (loaded) {
|
|
126
137
|
session.codeepSessionId = args[1];
|
|
127
138
|
session.history = loaded;
|
|
128
|
-
return { handled: true, response:
|
|
139
|
+
return { handled: true, response: formatSessionPreview(args[1], session.history) };
|
|
129
140
|
}
|
|
130
141
|
return { handled: true, response: `Session not found: \`${args[1]}\`` };
|
|
131
142
|
}
|
|
132
|
-
return { handled: true, response: 'Usage: `/session new`
|
|
143
|
+
return { handled: true, response: 'Usage: `/session` · `/session new` · `/session load <name>`' };
|
|
133
144
|
}
|
|
134
145
|
case 'save': {
|
|
135
146
|
const name = args.length ? args.join('-') : session.codeepSessionId;
|
|
@@ -152,11 +163,241 @@ export function handleCommand(input, session) {
|
|
|
152
163
|
config.set('language', args[0]);
|
|
153
164
|
return { handled: true, response: `Language set to \`${args[0]}\`` };
|
|
154
165
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
// ─── File context ──────────────────────────────────────────────────────────
|
|
167
|
+
case 'add': {
|
|
168
|
+
if (!args.length) {
|
|
169
|
+
if (session.addedFiles.size === 0)
|
|
170
|
+
return { handled: true, response: 'No files in context. Usage: `/add <file> [file2...]`' };
|
|
171
|
+
const list = [...session.addedFiles.values()].map(f => `- \`${f.relativePath}\``).join('\n');
|
|
172
|
+
return { handled: true, response: `**Files in context (${session.addedFiles.size}):**\n${list}` };
|
|
173
|
+
}
|
|
174
|
+
const { promises: fs } = await import('fs');
|
|
175
|
+
const pathMod = await import('path');
|
|
176
|
+
const root = session.workspaceRoot;
|
|
177
|
+
const added = [];
|
|
178
|
+
const errors = [];
|
|
179
|
+
for (const filePath of args) {
|
|
180
|
+
const fullPath = pathMod.isAbsolute(filePath) ? filePath : pathMod.join(root, filePath);
|
|
181
|
+
const relativePath = pathMod.relative(root, fullPath);
|
|
182
|
+
try {
|
|
183
|
+
const stat = await fs.stat(fullPath);
|
|
184
|
+
if (!stat.isFile()) {
|
|
185
|
+
errors.push(`\`${filePath}\`: not a file`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (stat.size > 100_000) {
|
|
189
|
+
errors.push(`\`${filePath}\`: too large (max 100KB)`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
193
|
+
session.addedFiles.set(fullPath, { relativePath, content });
|
|
194
|
+
added.push(`\`${relativePath}\``);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
errors.push(`\`${filePath}\`: not found`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const parts = [];
|
|
201
|
+
if (added.length)
|
|
202
|
+
parts.push(`Added to context: ${added.join(', ')}`);
|
|
203
|
+
if (errors.length)
|
|
204
|
+
parts.push(`Errors: ${errors.join(', ')}`);
|
|
205
|
+
return { handled: true, response: parts.join('\n') };
|
|
206
|
+
}
|
|
207
|
+
case 'drop': {
|
|
208
|
+
if (!args.length) {
|
|
209
|
+
const count = session.addedFiles.size;
|
|
210
|
+
session.addedFiles.clear();
|
|
211
|
+
return { handled: true, response: count ? `Dropped all ${count} file(s) from context.` : 'No files in context.' };
|
|
212
|
+
}
|
|
213
|
+
const pathMod = await import('path');
|
|
214
|
+
const root = session.workspaceRoot;
|
|
215
|
+
let dropped = 0;
|
|
216
|
+
for (const filePath of args) {
|
|
217
|
+
const fullPath = pathMod.isAbsolute(filePath) ? filePath : pathMod.join(root, filePath);
|
|
218
|
+
if (session.addedFiles.delete(fullPath))
|
|
219
|
+
dropped++;
|
|
220
|
+
}
|
|
221
|
+
return { handled: true, response: dropped ? `Dropped ${dropped} file(s). ${session.addedFiles.size} remaining.` : 'File not found in context.' };
|
|
222
|
+
}
|
|
223
|
+
// ─── Undo ──────────────────────────────────────────────────────────────────
|
|
224
|
+
case 'undo': {
|
|
225
|
+
const { undoLastAction } = await import('../utils/agent.js');
|
|
226
|
+
const result = undoLastAction();
|
|
227
|
+
return { handled: true, response: result.success ? `Undo: ${result.message}` : `Cannot undo: ${result.message}` };
|
|
228
|
+
}
|
|
229
|
+
case 'undo-all': {
|
|
230
|
+
const { undoAllActions } = await import('../utils/agent.js');
|
|
231
|
+
const result = undoAllActions();
|
|
232
|
+
return { handled: true, response: result.success ? `Undone ${result.results.length} action(s).` : 'Nothing to undo.' };
|
|
233
|
+
}
|
|
234
|
+
case 'skills': {
|
|
235
|
+
const { getAllSkills, searchSkills, formatSkillsList } = await import('../utils/skills.js');
|
|
236
|
+
const query = args.join(' ').toLowerCase();
|
|
237
|
+
const skills = query ? searchSkills(query) : getAllSkills();
|
|
238
|
+
if (!skills.length)
|
|
239
|
+
return { handled: true, response: `No skills matching \`${query}\`.` };
|
|
240
|
+
return { handled: true, response: formatSkillsList(skills) };
|
|
241
|
+
}
|
|
242
|
+
case 'scan': {
|
|
243
|
+
onChunk('_Scanning project…_\n\n');
|
|
244
|
+
const { scanProject, saveProjectIntelligence, generateContextFromIntelligence } = await import('../utils/projectIntelligence.js');
|
|
245
|
+
try {
|
|
246
|
+
const intelligence = await scanProject(session.workspaceRoot);
|
|
247
|
+
saveProjectIntelligence(session.workspaceRoot, intelligence);
|
|
248
|
+
const context = generateContextFromIntelligence(intelligence);
|
|
249
|
+
onChunk(`## Project Scan\n\n${context}`);
|
|
250
|
+
return { handled: true, response: '', streaming: true };
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
return { handled: true, response: `Scan failed: ${err.message}` };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
case 'review': {
|
|
257
|
+
onChunk('_Running code review…_\n\n');
|
|
258
|
+
const { performCodeReview, formatReviewResult } = await import('../utils/codeReview.js');
|
|
259
|
+
const projectCtx = getProjectContext(session.workspaceRoot);
|
|
260
|
+
if (!projectCtx)
|
|
261
|
+
return { handled: true, response: 'No project context available.' };
|
|
262
|
+
const reviewFiles = args.length ? args : undefined;
|
|
263
|
+
const result = performCodeReview(projectCtx, reviewFiles);
|
|
264
|
+
return { handled: true, response: formatReviewResult(result) };
|
|
265
|
+
}
|
|
266
|
+
case 'learn': {
|
|
267
|
+
onChunk('_Learning from project…_\n\n');
|
|
268
|
+
const { learnFromProject, formatPreferencesForPrompt } = await import('../utils/learning.js');
|
|
269
|
+
const { promises: fs } = await import('fs');
|
|
270
|
+
const pathMod = await import('path');
|
|
271
|
+
const extensions = ['.ts', '.js', '.tsx', '.jsx', '.py', '.go', '.rs'];
|
|
272
|
+
const files = [];
|
|
273
|
+
const walkDir = async (dir, depth = 0) => {
|
|
274
|
+
if (depth > 3 || files.length >= 20)
|
|
275
|
+
return;
|
|
276
|
+
try {
|
|
277
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
280
|
+
continue;
|
|
281
|
+
const fullPath = pathMod.join(dir, entry.name);
|
|
282
|
+
if (entry.isDirectory()) {
|
|
283
|
+
await walkDir(fullPath, depth + 1);
|
|
284
|
+
}
|
|
285
|
+
else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
286
|
+
files.push(pathMod.relative(session.workspaceRoot, fullPath));
|
|
287
|
+
}
|
|
288
|
+
if (files.length >= 20)
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch { /* skip unreadable dirs */ }
|
|
159
293
|
};
|
|
294
|
+
await walkDir(session.workspaceRoot);
|
|
295
|
+
if (!files.length)
|
|
296
|
+
return { handled: true, response: 'No source files found to learn from.' };
|
|
297
|
+
const prefs = learnFromProject(session.workspaceRoot, files);
|
|
298
|
+
const formatted = formatPreferencesForPrompt(prefs);
|
|
299
|
+
return { handled: true, response: `## Learned Preferences\n\n${formatted}\n\n_Learned from ${files.length} file(s)._` };
|
|
300
|
+
}
|
|
301
|
+
case 'changes': {
|
|
302
|
+
const { getCurrentSessionActions } = await import('../utils/agent.js');
|
|
303
|
+
const actions = getCurrentSessionActions();
|
|
304
|
+
if (!actions.length)
|
|
305
|
+
return { handled: true, response: 'No changes in current session.' };
|
|
306
|
+
const lines = ['## Session Changes', '', ...actions.map(a => `- **${a.type}**: \`${a.target}\` — ${a.result}`)];
|
|
307
|
+
return { handled: true, response: lines.join('\n') };
|
|
308
|
+
}
|
|
309
|
+
// ─── Export ────────────────────────────────────────────────────────────────
|
|
310
|
+
case 'export': {
|
|
311
|
+
if (!session.history.length)
|
|
312
|
+
return { handled: true, response: 'No messages to export.' };
|
|
313
|
+
const format = (args[0] || 'md').toLowerCase();
|
|
314
|
+
if (format === 'json') {
|
|
315
|
+
return { handled: true, response: `\`\`\`json\n${JSON.stringify(session.history, null, 2)}\n\`\`\`` };
|
|
316
|
+
}
|
|
317
|
+
if (format === 'txt') {
|
|
318
|
+
const txt = session.history.map(m => `[${m.role.toUpperCase()}]\n${m.content}`).join('\n\n---\n\n');
|
|
319
|
+
return { handled: true, response: `\`\`\`\n${txt}\n\`\`\`` };
|
|
320
|
+
}
|
|
321
|
+
// default: markdown
|
|
322
|
+
const md = ['# Codeep Session Export', '', ...session.history.map(m => `## ${m.role === 'user' ? 'You' : m.role === 'assistant' ? 'Codeep' : 'System'}\n\n${m.content}`)].join('\n\n---\n\n');
|
|
323
|
+
return { handled: true, response: md };
|
|
324
|
+
}
|
|
325
|
+
// ─── Git diff ──────────────────────────────────────────────────────────────
|
|
326
|
+
case 'diff': {
|
|
327
|
+
const { getGitDiff, formatDiffForDisplay } = await import('../utils/git.js');
|
|
328
|
+
const staged = args.includes('--staged') || args.includes('-s');
|
|
329
|
+
const result = getGitDiff(staged, session.workspaceRoot);
|
|
330
|
+
if (!result.success || !result.diff) {
|
|
331
|
+
return { handled: true, response: result.error || 'No changes found.' };
|
|
332
|
+
}
|
|
333
|
+
const preview = formatDiffForDisplay(result.diff, 60);
|
|
334
|
+
// Ask AI to review
|
|
335
|
+
onChunk('_Reviewing diff…_\n\n');
|
|
336
|
+
const projectCtx = getProjectContext(session.workspaceRoot);
|
|
337
|
+
let reviewText = '';
|
|
338
|
+
await chat(`Review this git diff and provide concise feedback:\n\n\`\`\`diff\n${preview}\n\`\`\``, session.history, (chunk) => { reviewText += chunk; onChunk(chunk); }, undefined, projectCtx, undefined);
|
|
339
|
+
return { handled: true, response: '', streaming: true };
|
|
340
|
+
}
|
|
341
|
+
// ─── Skills ────────────────────────────────────────────────────────────────
|
|
342
|
+
default: {
|
|
343
|
+
const { findSkill, parseSkillArgs, executeSkill, trackSkillUsage } = await import('../utils/skills.js');
|
|
344
|
+
const skill = findSkill(cmd);
|
|
345
|
+
if (!skill) {
|
|
346
|
+
return { handled: true, response: `Unknown command: \`/${cmd}\`\n\nType \`/help\` for available commands or \`/skills\` to list all skills.` };
|
|
347
|
+
}
|
|
348
|
+
if (skill.requiresWriteAccess && !hasWritePermission(session.workspaceRoot)) {
|
|
349
|
+
return { handled: true, response: 'This skill requires write access. Use `/grant` first.' };
|
|
350
|
+
}
|
|
351
|
+
const params = parseSkillArgs(args.join(' '), skill);
|
|
352
|
+
trackSkillUsage(skill.name);
|
|
353
|
+
onChunk(`_Running skill **${skill.name}**…_\n\n`);
|
|
354
|
+
const { spawnSync } = await import('child_process');
|
|
355
|
+
const projectCtx = getProjectContext(session.workspaceRoot);
|
|
356
|
+
const skillResult = await executeSkill(skill, params, {
|
|
357
|
+
onCommand: async (shellCmd) => {
|
|
358
|
+
const proc = spawnSync(shellCmd, {
|
|
359
|
+
cwd: session.workspaceRoot,
|
|
360
|
+
encoding: 'utf-8',
|
|
361
|
+
timeout: 60_000,
|
|
362
|
+
shell: true,
|
|
363
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
364
|
+
});
|
|
365
|
+
const out = ((proc.stdout || '') + (proc.stderr || '')).trim();
|
|
366
|
+
const block = `\`${shellCmd}\`\n\`\`\`\n${out || '(no output)'}\n\`\`\`\n`;
|
|
367
|
+
onChunk(block);
|
|
368
|
+
if ((proc.status ?? 1) !== 0)
|
|
369
|
+
throw new Error(out || `Exit code ${proc.status}`);
|
|
370
|
+
return out;
|
|
371
|
+
},
|
|
372
|
+
onPrompt: async (prompt) => {
|
|
373
|
+
let response = '';
|
|
374
|
+
await chat(prompt, session.history, (chunk) => { response += chunk; onChunk(chunk); }, undefined, projectCtx, undefined);
|
|
375
|
+
return response;
|
|
376
|
+
},
|
|
377
|
+
onAgent: async (task) => {
|
|
378
|
+
const { buildProjectContext } = await import('./session.js');
|
|
379
|
+
const ctx = buildProjectContext(session.workspaceRoot);
|
|
380
|
+
let output = '';
|
|
381
|
+
const agentResult = await runAgent(task, ctx, {
|
|
382
|
+
abortSignal,
|
|
383
|
+
onIteration: (_i, msg) => { onChunk(msg + '\n'); },
|
|
384
|
+
onThinking: (text) => { onChunk(text); },
|
|
385
|
+
});
|
|
386
|
+
if (agentResult.finalResponse) {
|
|
387
|
+
output = agentResult.finalResponse;
|
|
388
|
+
onChunk(output);
|
|
389
|
+
}
|
|
390
|
+
return output;
|
|
391
|
+
},
|
|
392
|
+
// Skills in ACP auto-confirm (no TUI)
|
|
393
|
+
onConfirm: async (_message) => true,
|
|
394
|
+
onNotify: (message) => { onChunk(`> ${message}\n`); },
|
|
395
|
+
});
|
|
396
|
+
if (!skillResult.success) {
|
|
397
|
+
return { handled: true, response: `Skill **${skill.name}** failed: ${skillResult.output}`, streaming: true };
|
|
398
|
+
}
|
|
399
|
+
return { handled: true, response: '', streaming: true };
|
|
400
|
+
}
|
|
160
401
|
}
|
|
161
402
|
}
|
|
162
403
|
// ─── Renderers ────────────────────────────────────────────────────────────────
|
|
@@ -164,24 +405,44 @@ function buildHelp() {
|
|
|
164
405
|
return [
|
|
165
406
|
'## Codeep Commands',
|
|
166
407
|
'',
|
|
408
|
+
'**Configuration**',
|
|
167
409
|
'| Command | Description |',
|
|
168
410
|
'|---------|-------------|',
|
|
169
|
-
'| `/
|
|
170
|
-
'| `/status` | Show current configuration and session info |',
|
|
411
|
+
'| `/status` | Show current config and session info |',
|
|
171
412
|
'| `/version` | Show version and current model |',
|
|
172
|
-
'| `/provider` | List
|
|
173
|
-
'| `/
|
|
174
|
-
'| `/
|
|
175
|
-
'| `/
|
|
176
|
-
'| `/
|
|
177
|
-
'| `/
|
|
178
|
-
'
|
|
179
|
-
'
|
|
180
|
-
'|
|
|
181
|
-
'
|
|
413
|
+
'| `/provider [id]` | List or switch provider |',
|
|
414
|
+
'| `/model [id]` | List or switch model |',
|
|
415
|
+
'| `/login <provider> <key>` | Set API key for a provider |',
|
|
416
|
+
'| `/apikey [key]` | Show or set API key |',
|
|
417
|
+
'| `/lang [code]` | Set response language (`en`, `hr`, `auto`…) |',
|
|
418
|
+
'| `/grant` | Grant write access for workspace |',
|
|
419
|
+
'',
|
|
420
|
+
'**Sessions**',
|
|
421
|
+
'| Command | Description |',
|
|
422
|
+
'|---------|-------------|',
|
|
423
|
+
'| `/session` | List saved sessions |',
|
|
424
|
+
'| `/session new` | Start new session |',
|
|
425
|
+
'| `/session load <name>` | Load a session |',
|
|
182
426
|
'| `/save [name]` | Save current session |',
|
|
183
|
-
'
|
|
184
|
-
'
|
|
427
|
+
'',
|
|
428
|
+
'**Context & Files**',
|
|
429
|
+
'| Command | Description |',
|
|
430
|
+
'|---------|-------------|',
|
|
431
|
+
'| `/add <file...>` | Add files to agent context |',
|
|
432
|
+
'| `/drop [file...]` | Remove files from context (no args = clear all) |',
|
|
433
|
+
'',
|
|
434
|
+
'**Actions**',
|
|
435
|
+
'| Command | Description |',
|
|
436
|
+
'|---------|-------------|',
|
|
437
|
+
'| `/diff [--staged]` | Git diff with AI review |',
|
|
438
|
+
'| `/undo` | Undo last agent action |',
|
|
439
|
+
'| `/undo-all` | Undo all actions in session |',
|
|
440
|
+
'| `/changes` | Show session changes |',
|
|
441
|
+
'| `/export [json\\|md\\|txt]` | Export conversation |',
|
|
442
|
+
'',
|
|
443
|
+
'**Skills** (type `/skills` to list all)',
|
|
444
|
+
'`/commit` · `/fix` · `/test` · `/docs` · `/refactor` · `/explain`',
|
|
445
|
+
'`/optimize` · `/debug` · `/push` · `/pr` · `/build` · `/deploy` …',
|
|
185
446
|
].join('\n');
|
|
186
447
|
}
|
|
187
448
|
function buildStatus(session) {
|
|
@@ -259,6 +520,31 @@ function loginCmd(providerId, apiKey) {
|
|
|
259
520
|
setApiKey(apiKey, providerId);
|
|
260
521
|
return `Logged in as **${provider.name}** (\`${providerId}\`). Model: \`${provider.defaultModel}\`.`;
|
|
261
522
|
}
|
|
523
|
+
const PREVIEW_MESSAGES = 6; // last N messages to show on session restore
|
|
524
|
+
const PREVIEW_MAX_CHARS = 300; // truncate long messages
|
|
525
|
+
function formatSessionPreviewLines(history) {
|
|
526
|
+
const recent = history.slice(-PREVIEW_MESSAGES);
|
|
527
|
+
const lines = [`*Last ${recent.length} message${recent.length !== 1 ? 's' : ''}:*`, ''];
|
|
528
|
+
for (const msg of recent) {
|
|
529
|
+
const role = msg.role === 'user' ? '**You**' : msg.role === 'assistant' ? '**Codeep**' : '_system_';
|
|
530
|
+
const text = msg.content.length > PREVIEW_MAX_CHARS
|
|
531
|
+
? msg.content.slice(0, PREVIEW_MAX_CHARS) + '…'
|
|
532
|
+
: msg.content;
|
|
533
|
+
// Collapse newlines to keep preview compact
|
|
534
|
+
lines.push(`${role}: ${text.replace(/\n+/g, ' ')}`);
|
|
535
|
+
}
|
|
536
|
+
return lines;
|
|
537
|
+
}
|
|
538
|
+
function formatSessionPreview(name, history) {
|
|
539
|
+
const lines = [
|
|
540
|
+
`Session loaded: \`${name}\` (${history.length} messages)`,
|
|
541
|
+
'',
|
|
542
|
+
'---',
|
|
543
|
+
'',
|
|
544
|
+
...formatSessionPreviewLines(history),
|
|
545
|
+
];
|
|
546
|
+
return lines.join('\n');
|
|
547
|
+
}
|
|
262
548
|
function buildSessionList(workspaceRoot) {
|
|
263
549
|
const sessions = listSessionsWithInfo(workspaceRoot);
|
|
264
550
|
if (sessions.length === 0)
|
package/dist/acp/server.js
CHANGED
|
@@ -5,6 +5,49 @@ import { StdioTransport } from './transport.js';
|
|
|
5
5
|
import { runAgentSession } from './session.js';
|
|
6
6
|
import { initWorkspace, handleCommand } from './commands.js';
|
|
7
7
|
import { autoSaveSession } from '../config/index.js';
|
|
8
|
+
// All advertised slash commands (shown in Zed autocomplete)
|
|
9
|
+
const AVAILABLE_COMMANDS = [
|
|
10
|
+
// Configuration
|
|
11
|
+
{ name: 'help', description: 'Show available commands' },
|
|
12
|
+
{ name: 'status', description: 'Show current config and session info' },
|
|
13
|
+
{ name: 'version', description: 'Show version and current model' },
|
|
14
|
+
{ name: 'provider', description: 'List or switch AI provider', input: { hint: '<provider-id>' } },
|
|
15
|
+
{ name: 'model', description: 'List or switch model', input: { hint: '<model-id>' } },
|
|
16
|
+
{ name: 'login', description: 'Set API key for a provider', input: { hint: '<providerId> <apiKey>' } },
|
|
17
|
+
{ name: 'apikey', description: 'Show or set API key for current provider', input: { hint: '<key>' } },
|
|
18
|
+
{ name: 'lang', description: 'Set response language', input: { hint: '<code> (en, hr, auto…)' } },
|
|
19
|
+
{ name: 'grant', description: 'Grant write access for workspace' },
|
|
20
|
+
// Sessions
|
|
21
|
+
{ name: 'session', description: 'List sessions, or: new / load <name>', input: { hint: 'new | load <name>' } },
|
|
22
|
+
{ name: 'save', description: 'Save current session', input: { hint: '[name]' } },
|
|
23
|
+
// Context
|
|
24
|
+
{ name: 'add', description: 'Add files to agent context', input: { hint: '<file> [file2…]' } },
|
|
25
|
+
{ name: 'drop', description: 'Remove files from context (no args = clear all)', input: { hint: '[file…]' } },
|
|
26
|
+
// Actions
|
|
27
|
+
{ name: 'diff', description: 'Git diff with AI review', input: { hint: '[--staged]' } },
|
|
28
|
+
{ name: 'undo', description: 'Undo last agent action' },
|
|
29
|
+
{ name: 'undo-all', description: 'Undo all agent actions in session' },
|
|
30
|
+
{ name: 'changes', description: 'Show all changes made in session' },
|
|
31
|
+
{ name: 'export', description: 'Export conversation', input: { hint: 'json | md | txt' } },
|
|
32
|
+
// Project intelligence
|
|
33
|
+
{ name: 'scan', description: 'Scan project structure and generate summary' },
|
|
34
|
+
{ name: 'review', description: 'Run code review on project or specific files', input: { hint: '[file…]' } },
|
|
35
|
+
{ name: 'learn', description: 'Learn coding preferences from project files' },
|
|
36
|
+
// Skills
|
|
37
|
+
{ name: 'skills', description: 'List all available skills', input: { hint: '[query]' } },
|
|
38
|
+
{ name: 'commit', description: 'Generate commit message and commit' },
|
|
39
|
+
{ name: 'fix', description: 'Fix bugs or issues' },
|
|
40
|
+
{ name: 'test', description: 'Write or run tests' },
|
|
41
|
+
{ name: 'docs', description: 'Generate documentation' },
|
|
42
|
+
{ name: 'refactor', description: 'Refactor code' },
|
|
43
|
+
{ name: 'explain', description: 'Explain code' },
|
|
44
|
+
{ name: 'optimize', description: 'Optimize code for performance' },
|
|
45
|
+
{ name: 'debug', description: 'Debug an issue' },
|
|
46
|
+
{ name: 'push', description: 'Git push' },
|
|
47
|
+
{ name: 'pr', description: 'Create a pull request' },
|
|
48
|
+
{ name: 'build', description: 'Build the project' },
|
|
49
|
+
{ name: 'deploy', description: 'Deploy the project' },
|
|
50
|
+
];
|
|
8
51
|
export function startAcpServer() {
|
|
9
52
|
const transport = new StdioTransport();
|
|
10
53
|
// ACP sessionId → full AcpSession (includes history + codeep session tracking)
|
|
@@ -56,31 +99,19 @@ export function startAcpServer() {
|
|
|
56
99
|
workspaceRoot: params.cwd,
|
|
57
100
|
history,
|
|
58
101
|
codeepSessionId,
|
|
102
|
+
addedFiles: new Map(),
|
|
59
103
|
abortController: null,
|
|
60
104
|
});
|
|
61
105
|
transport.respond(msg.id, { sessionId: acpSessionId });
|
|
62
|
-
// Advertise available slash commands to Zed
|
|
106
|
+
// Advertise all available slash commands to Zed
|
|
63
107
|
transport.notify('session/update', {
|
|
64
108
|
sessionId: acpSessionId,
|
|
65
109
|
update: {
|
|
66
110
|
sessionUpdate: 'available_commands_update',
|
|
67
|
-
availableCommands:
|
|
68
|
-
{ name: 'help', description: 'Show available commands' },
|
|
69
|
-
{ name: 'status', description: 'Show current configuration and session info' },
|
|
70
|
-
{ name: 'version', description: 'Show version and current model' },
|
|
71
|
-
{ name: 'provider', description: 'List or switch AI provider', input: { hint: '<provider-id>' } },
|
|
72
|
-
{ name: 'model', description: 'List or switch model', input: { hint: '<model-id>' } },
|
|
73
|
-
{ name: 'login', description: 'Set API key for a provider', input: { hint: '<providerId> <apiKey>' } },
|
|
74
|
-
{ name: 'apikey', description: 'Show or set API key for current provider', input: { hint: '<key>' } },
|
|
75
|
-
{ name: 'sessions', description: 'List saved sessions' },
|
|
76
|
-
{ name: 'session', description: 'Manage sessions: new or load', input: { hint: 'new | load <name>' } },
|
|
77
|
-
{ name: 'save', description: 'Save current session', input: { hint: '[name]' } },
|
|
78
|
-
{ name: 'grant', description: 'Grant write access for current workspace' },
|
|
79
|
-
{ name: 'lang', description: 'Set response language', input: { hint: '<code> (e.g. en, hr, auto)' } },
|
|
80
|
-
],
|
|
111
|
+
availableCommands: AVAILABLE_COMMANDS,
|
|
81
112
|
},
|
|
82
113
|
});
|
|
83
|
-
// Stream welcome message
|
|
114
|
+
// Stream welcome message
|
|
84
115
|
transport.notify('session/update', {
|
|
85
116
|
sessionId: acpSessionId,
|
|
86
117
|
update: {
|
|
@@ -101,51 +132,71 @@ export function startAcpServer() {
|
|
|
101
132
|
.filter((b) => b.type === 'text')
|
|
102
133
|
.map((b) => b.text)
|
|
103
134
|
.join('\n');
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
135
|
+
const abortController = new AbortController();
|
|
136
|
+
session.abortController = abortController;
|
|
137
|
+
const sendChunk = (text) => {
|
|
107
138
|
transport.notify('session/update', {
|
|
108
139
|
sessionId: params.sessionId,
|
|
109
140
|
update: {
|
|
110
141
|
sessionUpdate: 'agent_message_chunk',
|
|
111
|
-
content: { type: 'text', text
|
|
142
|
+
content: { type: 'text', text },
|
|
112
143
|
},
|
|
113
144
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
transport.notify('session/update', {
|
|
126
|
-
sessionId: params.sessionId,
|
|
127
|
-
update: {
|
|
128
|
-
sessionUpdate: 'agent_message_chunk',
|
|
129
|
-
content: { type: 'text', text },
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
},
|
|
133
|
-
onFileEdit: (_uri, _newText) => {
|
|
134
|
-
// file edits are streamed via onChunk for now
|
|
135
|
-
},
|
|
136
|
-
}).then(() => {
|
|
137
|
-
// Persist conversation history after each agent turn
|
|
138
|
-
session.history.push({ role: 'user', content: prompt });
|
|
139
|
-
autoSaveSession(session.history, session.workspaceRoot);
|
|
140
|
-
transport.respond(msg.id, { stopReason: 'end_turn' });
|
|
141
|
-
}).catch((err) => {
|
|
142
|
-
if (err.name === 'AbortError') {
|
|
143
|
-
transport.respond(msg.id, { stopReason: 'cancelled' });
|
|
145
|
+
};
|
|
146
|
+
// Try slash commands first (async — skills, diff, scan, etc.)
|
|
147
|
+
handleCommand(prompt, session, sendChunk, abortController.signal)
|
|
148
|
+
.then((cmd) => {
|
|
149
|
+
if (cmd.handled) {
|
|
150
|
+
// For streaming commands (skills, diff), chunks were already sent via onChunk.
|
|
151
|
+
// For simple commands, send the response now.
|
|
152
|
+
if (cmd.response)
|
|
153
|
+
sendChunk(cmd.response);
|
|
154
|
+
transport.respond(msg.id, { stopReason: 'end_turn' });
|
|
155
|
+
return;
|
|
144
156
|
}
|
|
145
|
-
|
|
146
|
-
|
|
157
|
+
// Not a command — run agent loop
|
|
158
|
+
// Prepend any added-files context to the prompt
|
|
159
|
+
let enrichedPrompt = prompt;
|
|
160
|
+
if (session.addedFiles.size > 0) {
|
|
161
|
+
const parts = ['[Attached files]'];
|
|
162
|
+
for (const [, f] of session.addedFiles) {
|
|
163
|
+
parts.push(`\nFile: ${f.relativePath}\n\`\`\`\n${f.content}\n\`\`\``);
|
|
164
|
+
}
|
|
165
|
+
enrichedPrompt = parts.join('\n') + '\n\n' + prompt;
|
|
147
166
|
}
|
|
148
|
-
|
|
167
|
+
runAgentSession({
|
|
168
|
+
prompt: enrichedPrompt,
|
|
169
|
+
workspaceRoot: session.workspaceRoot,
|
|
170
|
+
conversationId: params.sessionId,
|
|
171
|
+
abortSignal: abortController.signal,
|
|
172
|
+
onChunk: sendChunk,
|
|
173
|
+
onFileEdit: (uri, newText) => {
|
|
174
|
+
// ACP structured file/edit notification — lets the editor track changed files
|
|
175
|
+
transport.notify('file/edit', {
|
|
176
|
+
uri,
|
|
177
|
+
textChanges: newText
|
|
178
|
+
? [{ range: { start: { line: 0, character: 0 }, end: { line: 999999, character: 0 } }, text: newText }]
|
|
179
|
+
: [],
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
}).then(() => {
|
|
183
|
+
session.history.push({ role: 'user', content: prompt });
|
|
184
|
+
autoSaveSession(session.history, session.workspaceRoot);
|
|
185
|
+
transport.respond(msg.id, { stopReason: 'end_turn' });
|
|
186
|
+
}).catch((err) => {
|
|
187
|
+
if (err.name === 'AbortError') {
|
|
188
|
+
transport.respond(msg.id, { stopReason: 'cancelled' });
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
transport.error(msg.id, -32000, err.message);
|
|
192
|
+
}
|
|
193
|
+
}).finally(() => {
|
|
194
|
+
if (session)
|
|
195
|
+
session.abortController = null;
|
|
196
|
+
});
|
|
197
|
+
})
|
|
198
|
+
.catch((err) => {
|
|
199
|
+
transport.error(msg.id, -32000, err.message);
|
|
149
200
|
if (session)
|
|
150
201
|
session.abortController = null;
|
|
151
202
|
});
|
package/dist/acp/session.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProjectContext } from '../utils/project.js';
|
|
1
2
|
export interface AgentSessionOptions {
|
|
2
3
|
prompt: string;
|
|
3
4
|
workspaceRoot: string;
|
|
@@ -6,6 +7,11 @@ export interface AgentSessionOptions {
|
|
|
6
7
|
onChunk: (text: string) => void;
|
|
7
8
|
onFileEdit: (uri: string, newText: string) => void;
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Build a ProjectContext from a workspace root directory.
|
|
12
|
+
* Falls back to a minimal synthetic context if scanning fails.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildProjectContext(workspaceRoot: string): ProjectContext;
|
|
9
15
|
/**
|
|
10
16
|
* Run a single agent session driven by ACP parameters.
|
|
11
17
|
*
|
package/dist/acp/session.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// acp/session.ts
|
|
2
2
|
// Bridges ACP parameters to the Codeep agent loop.
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
|
+
import { join, isAbsolute } from 'path';
|
|
4
5
|
import { runAgent } from '../utils/agent.js';
|
|
5
6
|
import { getProjectContext } from '../utils/project.js';
|
|
6
7
|
/**
|
|
7
8
|
* Build a ProjectContext from a workspace root directory.
|
|
8
9
|
* Falls back to a minimal synthetic context if scanning fails.
|
|
9
10
|
*/
|
|
10
|
-
function buildProjectContext(workspaceRoot) {
|
|
11
|
+
export function buildProjectContext(workspaceRoot) {
|
|
11
12
|
const ctx = getProjectContext(workspaceRoot);
|
|
12
13
|
if (ctx) {
|
|
13
14
|
return ctx;
|
|
@@ -32,13 +33,28 @@ export async function runAgentSession(opts) {
|
|
|
32
33
|
const projectContext = buildProjectContext(opts.workspaceRoot);
|
|
33
34
|
const result = await runAgent(opts.prompt, projectContext, {
|
|
34
35
|
abortSignal: opts.abortSignal,
|
|
35
|
-
// Stream iteration status and thinking text back to the caller via onChunk
|
|
36
36
|
onIteration: (_iteration, message) => {
|
|
37
37
|
opts.onChunk(message + '\n');
|
|
38
38
|
},
|
|
39
39
|
onThinking: (text) => {
|
|
40
40
|
opts.onChunk(text);
|
|
41
41
|
},
|
|
42
|
+
onToolCall: (toolCall) => {
|
|
43
|
+
// Notify the caller when agent writes or edits a file so ACP can
|
|
44
|
+
// send a structured file/edit notification to the editor.
|
|
45
|
+
const name = toolCall.tool;
|
|
46
|
+
if (name === 'write_file' || name === 'edit_file') {
|
|
47
|
+
const params = toolCall.parameters;
|
|
48
|
+
const filePath = params.path ?? '';
|
|
49
|
+
if (filePath) {
|
|
50
|
+
const absPath = isAbsolute(filePath)
|
|
51
|
+
? filePath
|
|
52
|
+
: join(opts.workspaceRoot, filePath);
|
|
53
|
+
const newText = params.content ?? params.new_text ?? '';
|
|
54
|
+
opts.onFileEdit(pathToFileURL(absPath).href, newText);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
42
58
|
});
|
|
43
59
|
// Emit the final response text if present
|
|
44
60
|
if (result.finalResponse) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.31",
|
|
4
4
|
"description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|