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.
@@ -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
- export declare function handleCommand(input: string, session: AcpSession): CommandResult;
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>;
@@ -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
- export function handleCommand(input, session) {
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: `Session loaded: \`${args[1]}\` (${session.history.length} messages)` };
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` or `/session load <name>`' };
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
- default:
156
- return {
157
- handled: true,
158
- response: `Unknown command: \`/${cmd}\`\n\nType \`/help\` to see available commands.`,
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
- '| `/help` | Show this help |',
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 available providers |',
173
- '| `/provider <id>` | Switch provider (e.g. `/provider anthropic`) |',
174
- '| `/model` | List models for current provider |',
175
- '| `/model <id>` | Switch model (e.g. `/model claude-opus-4-5`) |',
176
- '| `/login <providerId> <apiKey>` | Set API key for a provider |',
177
- '| `/apikey` | Show masked API key for current provider |',
178
- '| `/apikey <key>` | Set API key for current provider |',
179
- '| `/sessions` | List saved sessions |',
180
- '| `/session new` | Start a new session |',
181
- '| `/session load <name>` | Load a saved session |',
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
- '| `/grant` | Grant write access for current workspace |',
184
- '| `/lang <code>` | Set response language (e.g. `en`, `hr`, `auto`) |',
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)
@@ -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 so the client sees it immediately after session/new
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
- // Handle slash commands — no agent loop needed
105
- const cmd = handleCommand(prompt, session);
106
- if (cmd.handled) {
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: cmd.response },
142
+ content: { type: 'text', text },
112
143
  },
113
144
  });
114
- transport.respond(msg.id, { stopReason: 'end_turn' });
115
- return;
116
- }
117
- const abortController = new AbortController();
118
- session.abortController = abortController;
119
- runAgentSession({
120
- prompt,
121
- workspaceRoot: session.workspaceRoot,
122
- conversationId: params.sessionId,
123
- abortSignal: abortController.signal,
124
- onChunk: (text) => {
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
- else {
146
- transport.error(msg.id, -32000, err.message);
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
- }).finally(() => {
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
  });
@@ -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
  *
@@ -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.29",
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",