codeep 1.2.30 → 1.2.32

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,
@@ -73,7 +75,14 @@ export function initWorkspace(workspaceRoot) {
73
75
  return { codeepSessionId, history, welcomeText: lines.join('\n') };
74
76
  }
75
77
  // ─── Command dispatch ─────────────────────────────────────────────────────────
76
- 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) {
77
86
  const trimmed = input.trim();
78
87
  if (!trimmed.startsWith('/'))
79
88
  return { handled: false, response: '' };
@@ -105,8 +114,6 @@ export function handleCommand(input, session) {
105
114
  return { handled: true, response: setApiKeyCmd(args[0]) };
106
115
  }
107
116
  case 'login': {
108
- // In ACP context login = set API key for a provider
109
- // Usage: /login <providerId> <apiKey>
110
117
  const [providerId, apiKey] = args;
111
118
  if (!providerId || !apiKey) {
112
119
  return { handled: true, response: 'Usage: `/login <providerId> <apiKey>`\n\n' + buildProviderList() };
@@ -116,10 +123,8 @@ export function handleCommand(input, session) {
116
123
  case 'sessions':
117
124
  case 'session': {
118
125
  const sub = args[0];
119
- if (!sub) {
120
- // No argument — show list with usage hint
126
+ if (!sub)
121
127
  return { handled: true, response: buildSessionList(session.workspaceRoot) };
122
- }
123
128
  if (sub === 'new') {
124
129
  const id = startNewSession();
125
130
  session.codeepSessionId = id;
@@ -158,11 +163,241 @@ export function handleCommand(input, session) {
158
163
  config.set('language', args[0]);
159
164
  return { handled: true, response: `Language set to \`${args[0]}\`` };
160
165
  }
161
- default:
162
- return {
163
- handled: true,
164
- 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 */ }
165
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
+ }
166
401
  }
167
402
  }
168
403
  // ─── Renderers ────────────────────────────────────────────────────────────────
@@ -170,24 +405,44 @@ function buildHelp() {
170
405
  return [
171
406
  '## Codeep Commands',
172
407
  '',
408
+ '**Configuration**',
173
409
  '| Command | Description |',
174
410
  '|---------|-------------|',
175
- '| `/help` | Show this help |',
176
- '| `/status` | Show current configuration and session info |',
411
+ '| `/status` | Show current config and session info |',
177
412
  '| `/version` | Show version and current model |',
178
- '| `/provider` | List available providers |',
179
- '| `/provider <id>` | Switch provider (e.g. `/provider anthropic`) |',
180
- '| `/model` | List models for current provider |',
181
- '| `/model <id>` | Switch model (e.g. `/model claude-opus-4-5`) |',
182
- '| `/login <providerId> <apiKey>` | Set API key for a provider |',
183
- '| `/apikey` | Show masked API key for current provider |',
184
- '| `/apikey <key>` | Set API key for current provider |',
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
+ '|---------|-------------|',
185
423
  '| `/session` | List saved sessions |',
186
- '| `/session new` | Start a new session |',
187
- '| `/session load <name>` | Load a saved session |',
424
+ '| `/session new` | Start new session |',
425
+ '| `/session load <name>` | Load a session |',
188
426
  '| `/save [name]` | Save current session |',
189
- '| `/grant` | Grant write access for current workspace |',
190
- '| `/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` …',
191
446
  ].join('\n');
192
447
  }
193
448
  function buildStatus(session) {
@@ -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,30 +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: 'session', description: 'List sessions, or: new / load <name>', input: { hint: 'new | load <name>' } },
76
- { name: 'save', description: 'Save current session', input: { hint: '[name]' } },
77
- { name: 'grant', description: 'Grant write access for current workspace' },
78
- { name: 'lang', description: 'Set response language', input: { hint: '<code> (e.g. en, hr, auto)' } },
79
- ],
111
+ availableCommands: AVAILABLE_COMMANDS,
80
112
  },
81
113
  });
82
- // Stream welcome message so the client sees it immediately after session/new
114
+ // Stream welcome message
83
115
  transport.notify('session/update', {
84
116
  sessionId: acpSessionId,
85
117
  update: {
@@ -100,51 +132,93 @@ export function startAcpServer() {
100
132
  .filter((b) => b.type === 'text')
101
133
  .map((b) => b.text)
102
134
  .join('\n');
103
- // Handle slash commands — no agent loop needed
104
- const cmd = handleCommand(prompt, session);
105
- if (cmd.handled) {
135
+ const abortController = new AbortController();
136
+ session.abortController = abortController;
137
+ const sendChunk = (text) => {
106
138
  transport.notify('session/update', {
107
139
  sessionId: params.sessionId,
108
140
  update: {
109
141
  sessionUpdate: 'agent_message_chunk',
110
- content: { type: 'text', text: cmd.response },
142
+ content: { type: 'text', text },
111
143
  },
112
144
  });
113
- transport.respond(msg.id, { stopReason: 'end_turn' });
114
- return;
115
- }
116
- const abortController = new AbortController();
117
- session.abortController = abortController;
118
- runAgentSession({
119
- prompt,
120
- workspaceRoot: session.workspaceRoot,
121
- conversationId: params.sessionId,
122
- abortSignal: abortController.signal,
123
- onChunk: (text) => {
124
- transport.notify('session/update', {
125
- sessionId: params.sessionId,
126
- update: {
127
- sessionUpdate: 'agent_message_chunk',
128
- content: { type: 'text', text },
129
- },
130
- });
131
- },
132
- onFileEdit: (_uri, _newText) => {
133
- // file edits are streamed via onChunk for now
134
- },
135
- }).then(() => {
136
- // Persist conversation history after each agent turn
137
- session.history.push({ role: 'user', content: prompt });
138
- autoSaveSession(session.history, session.workspaceRoot);
139
- transport.respond(msg.id, { stopReason: 'end_turn' });
140
- }).catch((err) => {
141
- if (err.name === 'AbortError') {
142
- 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;
143
156
  }
144
- else {
145
- 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;
146
166
  }
147
- }).finally(() => {
167
+ runAgentSession({
168
+ prompt: enrichedPrompt,
169
+ workspaceRoot: session.workspaceRoot,
170
+ conversationId: params.sessionId,
171
+ abortSignal: abortController.signal,
172
+ onChunk: sendChunk,
173
+ onThought: (text) => {
174
+ transport.notify('session/update', {
175
+ sessionId: params.sessionId,
176
+ update: {
177
+ sessionUpdate: 'agent_thought_chunk',
178
+ content: { type: 'text', text },
179
+ },
180
+ });
181
+ },
182
+ onToolCall: (toolCallId, _toolName, kind, title, status, locations) => {
183
+ transport.notify('session/update', {
184
+ sessionId: params.sessionId,
185
+ update: {
186
+ sessionUpdate: 'tool_call',
187
+ toolCallId,
188
+ title,
189
+ kind,
190
+ status,
191
+ ...(locations?.length ? { locations: locations.map(uri => ({ uri })) } : {}),
192
+ },
193
+ });
194
+ },
195
+ onFileEdit: (uri, newText) => {
196
+ // ACP structured file/edit notification — lets the editor apply changes
197
+ transport.notify('file/edit', {
198
+ uri,
199
+ textChanges: newText
200
+ ? [{ range: { start: { line: 0, character: 0 }, end: { line: 999999, character: 0 } }, text: newText }]
201
+ : [],
202
+ });
203
+ },
204
+ }).then(() => {
205
+ session.history.push({ role: 'user', content: prompt });
206
+ autoSaveSession(session.history, session.workspaceRoot);
207
+ transport.respond(msg.id, { stopReason: 'end_turn' });
208
+ }).catch((err) => {
209
+ if (err.name === 'AbortError') {
210
+ transport.respond(msg.id, { stopReason: 'cancelled' });
211
+ }
212
+ else {
213
+ transport.error(msg.id, -32000, err.message);
214
+ }
215
+ }).finally(() => {
216
+ if (session)
217
+ session.abortController = null;
218
+ });
219
+ })
220
+ .catch((err) => {
221
+ transport.error(msg.id, -32000, err.message);
148
222
  if (session)
149
223
  session.abortController = null;
150
224
  });
@@ -1,15 +1,18 @@
1
+ import { ProjectContext } from '../utils/project.js';
1
2
  export interface AgentSessionOptions {
2
3
  prompt: string;
3
4
  workspaceRoot: string;
4
5
  conversationId: string;
5
6
  abortSignal: AbortSignal;
6
7
  onChunk: (text: string) => void;
8
+ onThought?: (text: string) => void;
9
+ onToolCall?: (toolCallId: string, toolName: string, kind: string, title: string, status: 'pending' | 'running' | 'finished' | 'error', locations?: string[]) => void;
7
10
  onFileEdit: (uri: string, newText: string) => void;
8
11
  }
9
12
  /**
10
- * Run a single agent session driven by ACP parameters.
11
- *
12
- * onFileEdit is reserved for future use (v1 emits everything via onChunk).
13
+ * Build a ProjectContext from a workspace root directory.
14
+ * Falls back to a minimal synthetic context if scanning fails.
13
15
  */
16
+ export declare function buildProjectContext(workspaceRoot: string): ProjectContext;
14
17
  export declare function runAgentSession(opts: AgentSessionOptions): Promise<void>;
15
18
  export declare function pathToUri(absolutePath: string): string;
@@ -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;
@@ -23,21 +24,62 @@ function buildProjectContext(workspaceRoot) {
23
24
  summary: `Workspace at ${workspaceRoot}`,
24
25
  };
25
26
  }
26
- /**
27
- * Run a single agent session driven by ACP parameters.
28
- *
29
- * onFileEdit is reserved for future use (v1 emits everything via onChunk).
30
- */
27
+ // Maps internal tool names to ACP tool_call kind values and human titles.
28
+ function toolCallMeta(toolName, params) {
29
+ const file = params.path ?? params.file ?? '';
30
+ const label = file ? ` ${file.split('/').pop()}` : '';
31
+ switch (toolName) {
32
+ case 'read_file': return { kind: 'read', title: `Reading${label}` };
33
+ case 'write_file': return { kind: 'edit', title: `Writing${label}` };
34
+ case 'edit_file': return { kind: 'edit', title: `Editing${label}` };
35
+ case 'delete_file': return { kind: 'delete', title: `Deleting${label}` };
36
+ case 'move_file': return { kind: 'move', title: `Moving${label}` };
37
+ case 'list_files': return { kind: 'read', title: `Listing files${label}` };
38
+ case 'search_files': return { kind: 'search', title: `Searching${label || ' files'}` };
39
+ case 'run_command': return { kind: 'execute', title: `Running: ${params.command ?? ''}` };
40
+ case 'web_fetch': return { kind: 'fetch', title: `Fetching ${params.url ?? ''}` };
41
+ default: return { kind: 'other', title: toolName };
42
+ }
43
+ }
31
44
  export async function runAgentSession(opts) {
32
45
  const projectContext = buildProjectContext(opts.workspaceRoot);
46
+ let toolCallCounter = 0;
33
47
  const result = await runAgent(opts.prompt, projectContext, {
34
48
  abortSignal: opts.abortSignal,
35
- // Stream iteration status and thinking text back to the caller via onChunk
36
- onIteration: (_iteration, message) => {
37
- opts.onChunk(message + '\n');
49
+ onIteration: (_iteration, _message) => {
50
+ // Intentionally not forwarded — iteration count is internal detail
38
51
  },
39
52
  onThinking: (text) => {
40
- opts.onChunk(text);
53
+ if (opts.onThought) {
54
+ opts.onThought(text);
55
+ }
56
+ },
57
+ onToolCall: (toolCall) => {
58
+ const name = toolCall.tool;
59
+ const params = (toolCall.parameters ?? {});
60
+ const { kind, title } = toolCallMeta(name, params);
61
+ const toolCallId = `tc_${++toolCallCounter}`;
62
+ // Resolve file locations for edit/read/delete/move tools
63
+ const locations = [];
64
+ const filePath = params.path ?? params.file ?? '';
65
+ if (filePath) {
66
+ const absPath = isAbsolute(filePath)
67
+ ? filePath
68
+ : join(opts.workspaceRoot, filePath);
69
+ locations.push(pathToFileURL(absPath).href);
70
+ }
71
+ // Emit tool_call notification (running state)
72
+ opts.onToolCall?.(toolCallId, name, kind, title, 'running', locations.length ? locations : undefined);
73
+ // For file edits, also send structured file/edit notification
74
+ if (name === 'write_file' || name === 'edit_file') {
75
+ if (filePath) {
76
+ const absPath = isAbsolute(filePath)
77
+ ? filePath
78
+ : join(opts.workspaceRoot, filePath);
79
+ const newText = params.content ?? params.new_text ?? '';
80
+ opts.onFileEdit(pathToFileURL(absPath).href, newText);
81
+ }
82
+ }
41
83
  },
42
84
  });
43
85
  // Emit the final response text if present
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "1.2.30",
3
+ "version": "1.2.32",
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",