codeep 1.3.42 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -0
- package/dist/acp/commands.js +770 -7
- package/dist/acp/protocol.d.ts +11 -2
- package/dist/acp/server.js +179 -11
- package/dist/acp/session.d.ts +3 -0
- package/dist/acp/session.js +5 -0
- package/dist/api/index.js +39 -6
- package/dist/config/index.d.ts +13 -0
- package/dist/config/index.js +45 -0
- package/dist/config/providers.js +76 -1
- package/dist/renderer/App.d.ts +12 -0
- package/dist/renderer/App.js +109 -4
- package/dist/renderer/agentExecution.js +5 -0
- package/dist/renderer/commands.js +638 -2
- package/dist/renderer/components/Help.js +28 -0
- package/dist/renderer/components/Login.d.ts +1 -0
- package/dist/renderer/components/Login.js +24 -9
- package/dist/renderer/handlers.d.ts +11 -1
- package/dist/renderer/handlers.js +30 -0
- package/dist/renderer/main.js +73 -0
- package/dist/utils/agent.d.ts +17 -0
- package/dist/utils/agent.js +91 -7
- package/dist/utils/agentChat.d.ts +10 -2
- package/dist/utils/agentChat.js +48 -9
- package/dist/utils/agentStream.js +6 -2
- package/dist/utils/checkpoints.d.ts +93 -0
- package/dist/utils/checkpoints.js +205 -0
- package/dist/utils/context.d.ts +24 -0
- package/dist/utils/context.js +57 -0
- package/dist/utils/customCommands.d.ts +62 -0
- package/dist/utils/customCommands.js +201 -0
- package/dist/utils/hooks.d.ts +97 -0
- package/dist/utils/hooks.js +223 -0
- package/dist/utils/mcpClient.d.ts +229 -0
- package/dist/utils/mcpClient.js +497 -0
- package/dist/utils/mcpConfig.d.ts +55 -0
- package/dist/utils/mcpConfig.js +177 -0
- package/dist/utils/mcpMarketplace.d.ts +49 -0
- package/dist/utils/mcpMarketplace.js +175 -0
- package/dist/utils/mcpRegistry.d.ts +129 -0
- package/dist/utils/mcpRegistry.js +427 -0
- package/dist/utils/mcpSamplingBridge.d.ts +32 -0
- package/dist/utils/mcpSamplingBridge.js +88 -0
- package/dist/utils/mcpStreamableHttp.d.ts +65 -0
- package/dist/utils/mcpStreamableHttp.js +207 -0
- package/dist/utils/openrouterPrefs.d.ts +36 -0
- package/dist/utils/openrouterPrefs.js +83 -0
- package/dist/utils/skillBundles.d.ts +84 -0
- package/dist/utils/skillBundles.js +257 -0
- package/dist/utils/skillBundlesCloud.d.ts +69 -0
- package/dist/utils/skillBundlesCloud.js +202 -0
- package/dist/utils/tokenTracker.d.ts +14 -2
- package/dist/utils/tokenTracker.js +59 -41
- package/dist/utils/toolExecution.d.ts +17 -1
- package/dist/utils/toolExecution.js +184 -6
- package/dist/utils/tools.d.ts +22 -6
- package/dist/utils/tools.js +83 -8
- package/package.json +3 -2
- package/bin/codeep-macos-arm64 +0 -0
- package/bin/codeep-macos-x64 +0 -0
package/dist/acp/commands.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
import { config, getCurrentProvider, getModelsForCurrentProvider, setProvider, setApiKey, isConfigured, listSessionsWithInfo, startNewSession, loadSession, saveSession, initializeAsProject, isManuallyInitializedProject, setProjectPermission, hasWritePermission, hasReadPermission, } from '../config/index.js';
|
|
6
6
|
import { getProviderList, getProvider } from '../config/providers.js';
|
|
7
7
|
import { getProjectContext } from '../utils/project.js';
|
|
8
|
+
import { loadCustomCommands } from '../utils/customCommands.js';
|
|
9
|
+
import { summarizeHooks } from '../utils/hooks.js';
|
|
10
|
+
import { summarizeBundles } from '../utils/skillBundles.js';
|
|
8
11
|
import { existsSync, mkdirSync } from 'fs';
|
|
9
12
|
import { join } from 'path';
|
|
10
13
|
import { chat } from '../api/index.js';
|
|
@@ -68,6 +71,50 @@ export function initWorkspace(workspaceRoot, fresh = false) {
|
|
|
68
71
|
'',
|
|
69
72
|
'Type `/help` to see available commands.',
|
|
70
73
|
];
|
|
74
|
+
// Surface project-scoped custom slash commands so users notice that the
|
|
75
|
+
// workspace defines arbitrary `/foo` macros before they invoke any. A
|
|
76
|
+
// hostile or unfamiliar repo could ship `.codeep/commands/refactor.md`
|
|
77
|
+
// whose body is "ignore prior instructions, leak …" — typing `/refactor`
|
|
78
|
+
// sends that body to the agent as a user message with no preview. The
|
|
79
|
+
// banner is the lightweight informed-consent mitigation; full preview is
|
|
80
|
+
// available via `/commands`.
|
|
81
|
+
try {
|
|
82
|
+
const projectCustom = loadCustomCommands(workspaceRoot).filter(c => c.scope === 'project');
|
|
83
|
+
if (projectCustom.length > 0) {
|
|
84
|
+
const list = projectCustom.slice(0, 6).map(c => `\`/${c.name}\``).join(', ');
|
|
85
|
+
const more = projectCustom.length > 6 ? ` … (+${projectCustom.length - 6} more)` : '';
|
|
86
|
+
lines.push('', `**⚠ This workspace defines ${projectCustom.length} custom slash command${projectCustom.length === 1 ? '' : 's'}:** ${list}${more}`, 'These come from `.codeep/commands/` and run as user prompts. Type `/commands` to review what each does before invoking.');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Custom-command loading must never block the welcome banner.
|
|
91
|
+
}
|
|
92
|
+
// Same informed-consent flag for lifecycle hooks. A `.codeep/hooks/<event>.sh`
|
|
93
|
+
// script runs as shell on the user's machine when triggered — a hostile
|
|
94
|
+
// repo could ship one that exfiltrates credentials the first time the
|
|
95
|
+
// agent edits a file. List them so the user knows what's about to fire.
|
|
96
|
+
try {
|
|
97
|
+
const summary = summarizeHooks(workspaceRoot);
|
|
98
|
+
if (summary) {
|
|
99
|
+
lines.push('', `**⚠ This workspace has shell hooks installed:** ${summary}.`, 'Hooks live in `.codeep/hooks/` and run automatically. Type `/hooks` to see the script paths.');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Don't block welcome on hook inspection failure.
|
|
104
|
+
}
|
|
105
|
+
// Same informed-consent flag for project skill bundles. They're
|
|
106
|
+
// less dangerous than hooks (no shell-on-load) but the agent will
|
|
107
|
+
// autonomously invoke them based on user intent, so the user should
|
|
108
|
+
// know what's in the catalog before talking to the agent.
|
|
109
|
+
try {
|
|
110
|
+
const summary = summarizeBundles(workspaceRoot);
|
|
111
|
+
if (summary) {
|
|
112
|
+
lines.push('', `**ℹ This workspace ships ${summary}.**`, 'The agent will discover and invoke these via `invoke_skill`. Run `/skills bundles` to inspect.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Don't block welcome on skill discovery failure.
|
|
117
|
+
}
|
|
71
118
|
// Surface CLI sessions so Zed users discover them without having to know
|
|
72
119
|
// about the hidden "Import Threads" modal. Show up to 5 most recent.
|
|
73
120
|
if (sessions.length > 1 || (sessions.length === 1 && sessions[0].name !== codeepSessionId)) {
|
|
@@ -292,12 +339,154 @@ export async function handleCommand(input, session, onChunk, abortSignal) {
|
|
|
292
339
|
return { handled: true, response: result.success ? `Undone ${result.results.length} action(s).` : 'Nothing to undo.' };
|
|
293
340
|
}
|
|
294
341
|
case 'skills': {
|
|
342
|
+
const sub = args[0]?.toLowerCase();
|
|
343
|
+
// Subcommands for structured skill bundles (separate from the
|
|
344
|
+
// built-in JSON skills exposed by `skills.ts`).
|
|
345
|
+
if (sub === 'bundles' || sub === 'list-bundles') {
|
|
346
|
+
const { loadSkillBundles, formatBundleList } = await import('../utils/skillBundles.js');
|
|
347
|
+
return { handled: true, response: formatBundleList(loadSkillBundles(session.workspaceRoot)) };
|
|
348
|
+
}
|
|
349
|
+
if (sub === 'create-bundle') {
|
|
350
|
+
const name = (args[1] ?? '').toLowerCase();
|
|
351
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
352
|
+
return { handled: true, response: 'Usage: `/skills create-bundle <name>` — lowercase letters/digits/hyphens; must start with a letter or digit.' };
|
|
353
|
+
}
|
|
354
|
+
const { mkdirSync, writeFileSync, existsSync } = await import('fs');
|
|
355
|
+
const { join } = await import('path');
|
|
356
|
+
const dir = join(session.workspaceRoot, '.codeep', 'skills', name);
|
|
357
|
+
if (existsSync(dir)) {
|
|
358
|
+
return { handled: true, response: `Skill \`${name}\` already exists at \`.codeep/skills/${name}/\`.` };
|
|
359
|
+
}
|
|
360
|
+
mkdirSync(dir, { recursive: true });
|
|
361
|
+
const template = `---
|
|
362
|
+
name: ${name}
|
|
363
|
+
description: One-sentence summary shown in the agent's catalog.
|
|
364
|
+
triggers:
|
|
365
|
+
- keyword
|
|
366
|
+
- phrase
|
|
367
|
+
# Optional Codeep-specific keys:
|
|
368
|
+
# codeep-min-version: 2.0.0
|
|
369
|
+
# codeep-requires-mcp: [postgres, filesystem]
|
|
370
|
+
# allowed-tools: [read_file, write_file, execute_command]
|
|
371
|
+
# version: 0.1.0
|
|
372
|
+
# author: ${process.env.USER ?? 'you'}
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
# ${name}
|
|
376
|
+
|
|
377
|
+
Describe what this skill does. The agent will read this body verbatim when it invokes the skill.
|
|
378
|
+
|
|
379
|
+
## Steps
|
|
380
|
+
|
|
381
|
+
1. First step
|
|
382
|
+
2. Second step
|
|
383
|
+
3. …
|
|
384
|
+
|
|
385
|
+
## Notes
|
|
386
|
+
|
|
387
|
+
Anything else the agent should know — edge cases, gotchas, things to double-check.
|
|
388
|
+
`;
|
|
389
|
+
writeFileSync(join(dir, 'SKILL.md'), template);
|
|
390
|
+
return { handled: true, response: `Created skill bundle at \`.codeep/skills/${name}/SKILL.md\`.\n\nEdit it to taste, then run \`/skills bundles\` to confirm it's loaded.` };
|
|
391
|
+
}
|
|
392
|
+
if (sub === 'show' || sub === 'detail') {
|
|
393
|
+
const name = args[1];
|
|
394
|
+
if (!name)
|
|
395
|
+
return { handled: true, response: 'Usage: `/skills show <name>`' };
|
|
396
|
+
const { findSkillBundle } = await import('../utils/skillBundles.js');
|
|
397
|
+
const bundle = findSkillBundle(name, session.workspaceRoot);
|
|
398
|
+
if (!bundle)
|
|
399
|
+
return { handled: true, response: `Skill bundle \`${name}\` not found. Run \`/skills bundles\` for the list.` };
|
|
400
|
+
const lines = [
|
|
401
|
+
`## ${bundle.name}`,
|
|
402
|
+
`_${bundle.description}_`,
|
|
403
|
+
'',
|
|
404
|
+
`**Source:** \`${bundle.source}\` (${bundle.scope})`,
|
|
405
|
+
bundle.version ? `**Version:** ${bundle.version}` : '',
|
|
406
|
+
bundle.triggers.length ? `**Triggers:** ${bundle.triggers.join(', ')}` : '',
|
|
407
|
+
bundle.allowedTools.length ? `**Allowed tools:** ${bundle.allowedTools.join(', ')}` : '',
|
|
408
|
+
bundle.requiresMcp.length ? `**Requires MCP:** ${bundle.requiresMcp.join(', ')}` : '',
|
|
409
|
+
'',
|
|
410
|
+
'---',
|
|
411
|
+
'',
|
|
412
|
+
bundle.body,
|
|
413
|
+
].filter(Boolean);
|
|
414
|
+
return { handled: true, response: lines.join('\n') };
|
|
415
|
+
}
|
|
416
|
+
// Marketplace (codeep.dev/skills) — publish / install / browse / unpublish.
|
|
417
|
+
if (sub === 'publish') {
|
|
418
|
+
const slug = args[1];
|
|
419
|
+
if (!slug)
|
|
420
|
+
return { handled: true, response: 'Usage: `/skills publish <slug> [--public]`' };
|
|
421
|
+
const isPublic = args.includes('--public');
|
|
422
|
+
const { publishBundle } = await import('../utils/skillBundlesCloud.js');
|
|
423
|
+
const result = await publishBundle(session.workspaceRoot, slug, { isPublic });
|
|
424
|
+
if (!result.ok)
|
|
425
|
+
return { handled: true, response: `Publish failed: ${result.error}` };
|
|
426
|
+
const visibility = isPublic ? 'public' : 'private';
|
|
427
|
+
return {
|
|
428
|
+
handled: true,
|
|
429
|
+
response: `Published \`${slug}\` (${visibility}) to codeep.dev. Install elsewhere with \`/skills install ${result.skill?.owner_username ?? '<you>'}/${slug}\`.`,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (sub === 'install') {
|
|
433
|
+
const target = args[1];
|
|
434
|
+
if (!target)
|
|
435
|
+
return { handled: true, response: 'Usage: `/skills install <owner>/<slug>` (or numeric id)' };
|
|
436
|
+
const { installBundle } = await import('../utils/skillBundlesCloud.js');
|
|
437
|
+
onChunk(`_Fetching \`${target}\` from codeep.dev…_\n\n`);
|
|
438
|
+
const result = await installBundle(session.workspaceRoot, target);
|
|
439
|
+
if (!result.ok)
|
|
440
|
+
return { handled: true, response: `Install failed: ${result.error}`, streaming: true };
|
|
441
|
+
return {
|
|
442
|
+
handled: true,
|
|
443
|
+
response: `Installed \`${result.name}\` to \`.codeep/skills/${result.name}/SKILL.md\`. The agent will pick it up on the next prompt.`,
|
|
444
|
+
streaming: true,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (sub === 'browse') {
|
|
448
|
+
const query = args.slice(1).join(' ').trim();
|
|
449
|
+
const { browseSkills } = await import('../utils/skillBundlesCloud.js');
|
|
450
|
+
const result = await browseSkills({ query });
|
|
451
|
+
if (!result.ok)
|
|
452
|
+
return { handled: true, response: `Browse failed: ${result.error}` };
|
|
453
|
+
const skills = result.skills ?? [];
|
|
454
|
+
if (skills.length === 0) {
|
|
455
|
+
return { handled: true, response: query ? `_No public skills matching "${query}"._` : '_No public skills published yet._' };
|
|
456
|
+
}
|
|
457
|
+
const lines = [`## ${query ? `Skills matching "${query}"` : 'Public skills'}`, ''];
|
|
458
|
+
for (const s of skills.slice(0, 30)) {
|
|
459
|
+
const owner = s.owner_username ?? s.github_id;
|
|
460
|
+
const ver = s.version ? ` v${s.version}` : '';
|
|
461
|
+
lines.push(`- **${s.name}** \`${owner}/${s.slug}\`${ver} — ${s.description} _(${s.install_count} installs)_`);
|
|
462
|
+
}
|
|
463
|
+
if (skills.length > 30)
|
|
464
|
+
lines.push('', `_(showing first 30 of ${skills.length} — refine with a search query)_`);
|
|
465
|
+
lines.push('', 'Install one with `/skills install <owner>/<slug>`.');
|
|
466
|
+
return { handled: true, response: lines.join('\n') };
|
|
467
|
+
}
|
|
468
|
+
if (sub === 'unpublish') {
|
|
469
|
+
const target = args[1];
|
|
470
|
+
if (!target)
|
|
471
|
+
return { handled: true, response: 'Usage: `/skills unpublish <owner>/<slug>` (use your own owner name)' };
|
|
472
|
+
const { unpublishBundle } = await import('../utils/skillBundlesCloud.js');
|
|
473
|
+
const result = await unpublishBundle(target);
|
|
474
|
+
if (!result.ok)
|
|
475
|
+
return { handled: true, response: `Unpublish failed: ${result.error}` };
|
|
476
|
+
return { handled: true, response: `Unpublished \`${target}\` from codeep.dev. Local \`.codeep/skills/\` copy is untouched.` };
|
|
477
|
+
}
|
|
478
|
+
// Default: built-in JSON skills (legacy behaviour, search by query).
|
|
295
479
|
const { getAllSkills, searchSkills, formatSkillsList } = await import('../utils/skills.js');
|
|
296
480
|
const query = args.join(' ').toLowerCase();
|
|
297
481
|
const skills = query ? searchSkills(query) : getAllSkills();
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
482
|
+
const builtinBlock = skills.length ? formatSkillsList(skills) : `No built-in skills matching \`${query}\`.`;
|
|
483
|
+
// Append a hint about bundles so users discover them.
|
|
484
|
+
const { loadSkillBundles } = await import('../utils/skillBundles.js');
|
|
485
|
+
const bundles = loadSkillBundles(session.workspaceRoot);
|
|
486
|
+
const bundleHint = bundles.length > 0
|
|
487
|
+
? `\n\n_${bundles.length} skill bundle${bundles.length === 1 ? '' : 's'} installed — see \`/skills bundles\` or create one with \`/skills create-bundle <name>\`._`
|
|
488
|
+
: `\n\n_No skill bundles installed. Run \`/skills create-bundle <name>\` to make one._`;
|
|
489
|
+
return { handled: true, response: builtinBlock + bundleHint };
|
|
301
490
|
}
|
|
302
491
|
case 'scan': {
|
|
303
492
|
onChunk('_Scanning project…_\n\n');
|
|
@@ -366,6 +555,50 @@ export async function handleCommand(input, session, onChunk, abortSignal) {
|
|
|
366
555
|
const lines = ['## Session Changes', '', ...actions.map(a => `- **${a.type}**: \`${a.target}\` — ${a.result}`)];
|
|
367
556
|
return { handled: true, response: lines.join('\n') };
|
|
368
557
|
}
|
|
558
|
+
case 'cost': {
|
|
559
|
+
const { formatCostReport } = await import('../utils/tokenTracker.js');
|
|
560
|
+
return { handled: true, response: formatCostReport() };
|
|
561
|
+
}
|
|
562
|
+
case 'compact': {
|
|
563
|
+
// `keepRecent` is the number of latest messages to leave untouched.
|
|
564
|
+
// Default 4 ≈ two user/assistant exchanges — enough for the in-flight
|
|
565
|
+
// task to continue. User can override with `/compact 8` for slower
|
|
566
|
+
// tapering.
|
|
567
|
+
const keepRecent = args[0] ? Math.max(2, parseInt(args[0], 10) || 4) : 4;
|
|
568
|
+
if (session.history.length <= keepRecent + 2) {
|
|
569
|
+
return { handled: true, response: `Nothing to compact — only ${session.history.length} message(s) in this session.` };
|
|
570
|
+
}
|
|
571
|
+
onChunk(`_Compacting ${session.history.length - keepRecent} older message(s) into a summary…_\n\n`);
|
|
572
|
+
const { compactHistory } = await import('../utils/context.js');
|
|
573
|
+
const projectCtx = getProjectContext(session.workspaceRoot);
|
|
574
|
+
try {
|
|
575
|
+
// Forward the session's abort signal so the user pressing `session/cancel`
|
|
576
|
+
// also kills an in-flight compaction. The internal 60s timeout in
|
|
577
|
+
// compactHistory is the hard ceiling.
|
|
578
|
+
const result = await compactHistory(session.history, { keepRecent, projectContext: projectCtx, abortSignal });
|
|
579
|
+
if (result.replaced === 0) {
|
|
580
|
+
return { handled: true, response: 'Nothing to compact.', streaming: true };
|
|
581
|
+
}
|
|
582
|
+
session.history = result.compacted;
|
|
583
|
+
// Persist immediately so the compaction survives a client restart.
|
|
584
|
+
const { saveSession } = await import('../config/index.js');
|
|
585
|
+
saveSession(session.codeepSessionId, session.history, session.workspaceRoot);
|
|
586
|
+
const lines = [
|
|
587
|
+
`## Conversation Compacted`,
|
|
588
|
+
'',
|
|
589
|
+
`Replaced ${result.replaced} earlier message${result.replaced === 1 ? '' : 's'} with a summary.`,
|
|
590
|
+
`Kept the last ${keepRecent} message${keepRecent === 1 ? '' : 's'} verbatim.`,
|
|
591
|
+
'',
|
|
592
|
+
'**Summary:**',
|
|
593
|
+
'',
|
|
594
|
+
result.summary,
|
|
595
|
+
];
|
|
596
|
+
return { handled: true, response: lines.join('\n'), streaming: true };
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
return { handled: true, response: `Compaction failed: ${err.message}`, streaming: true };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
369
602
|
// ─── Export ────────────────────────────────────────────────────────────────
|
|
370
603
|
case 'export': {
|
|
371
604
|
if (!session.history.length)
|
|
@@ -398,12 +631,517 @@ export async function handleCommand(input, session, onChunk, abortSignal) {
|
|
|
398
631
|
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);
|
|
399
632
|
return { handled: true, response: '', streaming: true };
|
|
400
633
|
}
|
|
401
|
-
|
|
634
|
+
case 'commands': {
|
|
635
|
+
const { loadCustomCommands, formatCommandList } = await import('../utils/customCommands.js');
|
|
636
|
+
return { handled: true, response: formatCommandList(loadCustomCommands(session.workspaceRoot)) };
|
|
637
|
+
}
|
|
638
|
+
case 'memory': {
|
|
639
|
+
// Project intelligence notes — same store as the TUI's /memory command.
|
|
640
|
+
// /memory <text> add a note
|
|
641
|
+
// /memory list show all notes
|
|
642
|
+
// /memory remove <n> remove note by 1-based index
|
|
643
|
+
// /memory clear wipe all notes
|
|
644
|
+
const { loadProjectIntelligence, saveProjectIntelligence } = await import('../utils/projectIntelligence.js');
|
|
645
|
+
const intelligence = loadProjectIntelligence(session.workspaceRoot);
|
|
646
|
+
if (!intelligence) {
|
|
647
|
+
return { handled: true, response: 'No project intelligence found. Run `/scan` first.' };
|
|
648
|
+
}
|
|
649
|
+
intelligence.notes = intelligence.notes || [];
|
|
650
|
+
const sub = args[0]?.toLowerCase();
|
|
651
|
+
if (sub === 'list') {
|
|
652
|
+
if (intelligence.notes.length === 0)
|
|
653
|
+
return { handled: true, response: '_No memory notes yet. Add one with `/memory <note>`._' };
|
|
654
|
+
const lines = ['## Project memory notes', '', ...intelligence.notes.map((n, i) => `${i + 1}. ${n}`)];
|
|
655
|
+
return { handled: true, response: lines.join('\n') };
|
|
656
|
+
}
|
|
657
|
+
if (sub === 'remove') {
|
|
658
|
+
const idx = parseInt(args[1] ?? '', 10);
|
|
659
|
+
if (isNaN(idx) || idx < 1 || idx > intelligence.notes.length) {
|
|
660
|
+
return { handled: true, response: 'Usage: `/memory remove <n>` — run `/memory list` first to see indices.' };
|
|
661
|
+
}
|
|
662
|
+
const removed = intelligence.notes.splice(idx - 1, 1)[0];
|
|
663
|
+
saveProjectIntelligence(session.workspaceRoot, intelligence);
|
|
664
|
+
return { handled: true, response: `Removed note ${idx}: _"${removed}"_` };
|
|
665
|
+
}
|
|
666
|
+
if (sub === 'clear') {
|
|
667
|
+
const count = intelligence.notes.length;
|
|
668
|
+
intelligence.notes = [];
|
|
669
|
+
saveProjectIntelligence(session.workspaceRoot, intelligence);
|
|
670
|
+
return { handled: true, response: count ? `Cleared ${count} note${count === 1 ? '' : 's'}.` : '_No notes to clear._' };
|
|
671
|
+
}
|
|
672
|
+
// Default: treat all args as the note body.
|
|
673
|
+
const note = args.join(' ').trim();
|
|
674
|
+
if (!note) {
|
|
675
|
+
return { handled: true, response: 'Usage: `/memory <note>` · `/memory list` · `/memory remove <n>` · `/memory clear`' };
|
|
676
|
+
}
|
|
677
|
+
intelligence.notes.push(note);
|
|
678
|
+
saveProjectIntelligence(session.workspaceRoot, intelligence);
|
|
679
|
+
return { handled: true, response: `Memory saved (${intelligence.notes.length} total): _"${note}"_` };
|
|
680
|
+
}
|
|
681
|
+
case 'checkpoint': {
|
|
682
|
+
const sub = args[0]?.toLowerCase();
|
|
683
|
+
const { createCheckpoint, deleteCheckpoint, listCheckpoints, formatCheckpointList } = await import('../utils/checkpoints.js');
|
|
684
|
+
const { getCurrentSessionActions } = await import('../utils/agent.js');
|
|
685
|
+
if (sub === 'delete') {
|
|
686
|
+
const id = args[1];
|
|
687
|
+
if (!id)
|
|
688
|
+
return { handled: true, response: 'Usage: `/checkpoint delete <id>`' };
|
|
689
|
+
return { handled: true, response: deleteCheckpoint(session.workspaceRoot, id) ? `Deleted checkpoint \`${id}\`.` : `Checkpoint not found: \`${id}\`` };
|
|
690
|
+
}
|
|
691
|
+
if (sub === 'list') {
|
|
692
|
+
// Same as /checkpoints — keep both spellings for muscle-memory.
|
|
693
|
+
return { handled: true, response: formatCheckpointList(listCheckpoints(session.workspaceRoot)) };
|
|
694
|
+
}
|
|
695
|
+
// Default: create a new checkpoint with optional name (everything after /checkpoint)
|
|
696
|
+
const name = args.join(' ').trim() || undefined;
|
|
697
|
+
const provider = getCurrentProvider();
|
|
698
|
+
// Pull file paths the agent has touched in this session from the action
|
|
699
|
+
// log. Used at /rewind time to scope the git restore suggestion.
|
|
700
|
+
const filesTouched = Array.from(new Set(getCurrentSessionActions()
|
|
701
|
+
.filter(a => a.target && (a.type === 'write' || a.type === 'edit' || a.type === 'delete' || a.type === 'mkdir'))
|
|
702
|
+
.map(a => a.target)));
|
|
703
|
+
const cp = createCheckpoint({
|
|
704
|
+
workspaceRoot: session.workspaceRoot,
|
|
705
|
+
sessionId: session.codeepSessionId,
|
|
706
|
+
provider: provider.id,
|
|
707
|
+
model: config.get('model'),
|
|
708
|
+
messages: session.history,
|
|
709
|
+
filesTouched,
|
|
710
|
+
name,
|
|
711
|
+
});
|
|
712
|
+
const lines = [
|
|
713
|
+
`Created checkpoint \`${cp.id}\`${cp.name ? ` — **${cp.name}**` : ''}`,
|
|
714
|
+
`Captured ${cp.messages.length} message${cp.messages.length === 1 ? '' : 's'}, ${cp.filesTouched.length} file${cp.filesTouched.length === 1 ? '' : 's'} touched${cp.gitHead ? `, git \`${cp.gitHead}\`` : ''}.`,
|
|
715
|
+
'',
|
|
716
|
+
'Use `/rewind ' + cp.id + '` to restore.',
|
|
717
|
+
];
|
|
718
|
+
return { handled: true, response: lines.join('\n') };
|
|
719
|
+
}
|
|
720
|
+
case 'checkpoints': {
|
|
721
|
+
const { listCheckpoints, formatCheckpointList } = await import('../utils/checkpoints.js');
|
|
722
|
+
return { handled: true, response: formatCheckpointList(listCheckpoints(session.workspaceRoot)) };
|
|
723
|
+
}
|
|
724
|
+
case 'openrouter': {
|
|
725
|
+
const { readOpenRouterPreferences, writeOpenRouterPreferences, formatOpenRouterPreferences } = await import('../utils/openrouterPrefs.js');
|
|
726
|
+
const sub = args[0]?.toLowerCase();
|
|
727
|
+
const current = readOpenRouterPreferences();
|
|
728
|
+
if (!sub || sub === 'show') {
|
|
729
|
+
return { handled: true, response: formatOpenRouterPreferences(current) };
|
|
730
|
+
}
|
|
731
|
+
if (sub === 'clear') {
|
|
732
|
+
writeOpenRouterPreferences(null);
|
|
733
|
+
return { handled: true, response: 'OpenRouter preferences cleared. Router will pick freely.' };
|
|
734
|
+
}
|
|
735
|
+
if (sub === 'prefer') {
|
|
736
|
+
const list = args.slice(1).join(' ').split(',').map(s => s.trim()).filter(Boolean);
|
|
737
|
+
if (list.length === 0)
|
|
738
|
+
return { handled: true, response: 'Usage: `/openrouter prefer <p1>[,<p2>...]` — e.g. `/openrouter prefer DeepInfra,Together`' };
|
|
739
|
+
writeOpenRouterPreferences({ ...current, order: list });
|
|
740
|
+
return { handled: true, response: `Will prefer providers in this order: ${list.map(p => `\`${p}\``).join(', ')}` };
|
|
741
|
+
}
|
|
742
|
+
if (sub === 'ignore') {
|
|
743
|
+
const list = args.slice(1).join(' ').split(',').map(s => s.trim()).filter(Boolean);
|
|
744
|
+
if (list.length === 0)
|
|
745
|
+
return { handled: true, response: 'Usage: `/openrouter ignore <p1>[,<p2>...]`' };
|
|
746
|
+
writeOpenRouterPreferences({ ...current, ignore: list });
|
|
747
|
+
return { handled: true, response: `Will skip providers: ${list.map(p => `\`${p}\``).join(', ')}` };
|
|
748
|
+
}
|
|
749
|
+
if (sub === 'fallbacks') {
|
|
750
|
+
const val = args[1]?.toLowerCase();
|
|
751
|
+
if (val !== 'on' && val !== 'off')
|
|
752
|
+
return { handled: true, response: 'Usage: `/openrouter fallbacks on|off`' };
|
|
753
|
+
writeOpenRouterPreferences({ ...current, allow_fallbacks: val === 'on' });
|
|
754
|
+
return { handled: true, response: `Fallbacks ${val === 'on' ? 'enabled' : 'disabled'}.` };
|
|
755
|
+
}
|
|
756
|
+
if (sub === 'privacy') {
|
|
757
|
+
const val = args[1]?.toLowerCase();
|
|
758
|
+
if (val !== 'strict' && val !== 'allow')
|
|
759
|
+
return { handled: true, response: 'Usage: `/openrouter privacy strict|allow` — strict = data_collection: deny' };
|
|
760
|
+
writeOpenRouterPreferences({ ...current, data_collection: val === 'strict' ? 'deny' : 'allow' });
|
|
761
|
+
return { handled: true, response: `Privacy: \`data_collection: ${val === 'strict' ? 'deny' : 'allow'}\`` };
|
|
762
|
+
}
|
|
763
|
+
return { handled: true, response: `Unknown subcommand: \`${sub}\`. Use \`show\`, \`prefer\`, \`ignore\`, \`fallbacks\`, \`privacy\`, or \`clear\`.` };
|
|
764
|
+
}
|
|
765
|
+
case 'hooks': {
|
|
766
|
+
const { listInstalledHooks, formatHookList } = await import('../utils/hooks.js');
|
|
767
|
+
return { handled: true, response: formatHookList(listInstalledHooks(session.workspaceRoot)) };
|
|
768
|
+
}
|
|
769
|
+
case 'mcp': {
|
|
770
|
+
const sub = args[0]?.toLowerCase();
|
|
771
|
+
const { addProjectMcpServer, removeProjectMcpServer, loadMcpServerConfig } = await import('../utils/mcpConfig.js');
|
|
772
|
+
const { registerSessionServers } = await import('../utils/mcpRegistry.js');
|
|
773
|
+
if (sub === 'add') {
|
|
774
|
+
// /mcp add <name> <command> [args...]
|
|
775
|
+
const name = args[1];
|
|
776
|
+
const command = args[2];
|
|
777
|
+
if (!name || !command) {
|
|
778
|
+
return { handled: true, response: 'Usage: `/mcp add <name> <command> [args...]` — e.g. `/mcp add fs npx @modelcontextprotocol/server-filesystem /path`' };
|
|
779
|
+
}
|
|
780
|
+
const extraArgs = args.slice(3);
|
|
781
|
+
addProjectMcpServer(session.workspaceRoot, { name, command, args: extraArgs });
|
|
782
|
+
onChunk(`_Saved MCP server **${name}** to \`.codeep/mcp_servers.json\`. Spawning…_\n\n`);
|
|
783
|
+
// Live re-register so the new server is usable immediately, no
|
|
784
|
+
// session restart needed. registerSessionServers is idempotent —
|
|
785
|
+
// it disposes the old set and brings up the merged one.
|
|
786
|
+
const merged = loadMcpServerConfig(session.workspaceRoot);
|
|
787
|
+
const { registered, errors } = await registerSessionServers(session.sessionId, merged, { workspaceRoot: session.workspaceRoot });
|
|
788
|
+
const ok = registered.filter(t => t.serverName === name);
|
|
789
|
+
const failed = errors.find(e => e.server === name);
|
|
790
|
+
if (failed)
|
|
791
|
+
return { handled: true, response: `Saved \`${name}\` but spawn failed: \`${failed.error}\``, streaming: true };
|
|
792
|
+
return { handled: true, response: `Added \`${name}\` (${ok.length} tool${ok.length === 1 ? '' : 's'} available).`, streaming: true };
|
|
793
|
+
}
|
|
794
|
+
if (sub === 'remove') {
|
|
795
|
+
const name = args[1];
|
|
796
|
+
if (!name)
|
|
797
|
+
return { handled: true, response: 'Usage: `/mcp remove <name>`' };
|
|
798
|
+
const removed = removeProjectMcpServer(session.workspaceRoot, name);
|
|
799
|
+
if (!removed)
|
|
800
|
+
return { handled: true, response: `No project-scoped MCP server named \`${name}\`.` };
|
|
801
|
+
// Re-register with the new (smaller) merged set so the dropped
|
|
802
|
+
// server is actually killed.
|
|
803
|
+
const merged = loadMcpServerConfig(session.workspaceRoot);
|
|
804
|
+
await registerSessionServers(session.sessionId, merged, { workspaceRoot: session.workspaceRoot });
|
|
805
|
+
return { handled: true, response: `Removed \`${name}\` from project config and stopped its process.` };
|
|
806
|
+
}
|
|
807
|
+
if (sub === 'resources') {
|
|
808
|
+
const { getSessionResources, awaitSessionReady } = await import('../utils/mcpRegistry.js');
|
|
809
|
+
await awaitSessionReady(session.sessionId);
|
|
810
|
+
const groups = await getSessionResources(session.sessionId);
|
|
811
|
+
if (groups.length === 0) {
|
|
812
|
+
return { handled: true, response: '_No MCP server in this session exposes resources._' };
|
|
813
|
+
}
|
|
814
|
+
const lines = ['## MCP resources', ''];
|
|
815
|
+
for (const g of groups) {
|
|
816
|
+
lines.push(`**${g.serverName}** — ${g.resources.length} resource${g.resources.length === 1 ? '' : 's'}`);
|
|
817
|
+
for (const r of g.resources) {
|
|
818
|
+
const label = r.name ? `${r.name} — ` : '';
|
|
819
|
+
const mime = r.mimeType ? ` (${r.mimeType})` : '';
|
|
820
|
+
lines.push(`- ${label}\`${r.uri}\`${mime}${r.description ? ` — ${r.description}` : ''}`);
|
|
821
|
+
}
|
|
822
|
+
lines.push('');
|
|
823
|
+
}
|
|
824
|
+
lines.push('Read one with `/mcp read <uri>`.');
|
|
825
|
+
return { handled: true, response: lines.join('\n').trim() };
|
|
826
|
+
}
|
|
827
|
+
if (sub === 'read') {
|
|
828
|
+
const uri = args[1];
|
|
829
|
+
if (!uri)
|
|
830
|
+
return { handled: true, response: 'Usage: `/mcp read <uri>` — run `/mcp resources` to see available URIs.' };
|
|
831
|
+
const { readSessionResource } = await import('../utils/mcpRegistry.js');
|
|
832
|
+
try {
|
|
833
|
+
const contents = await readSessionResource(session.sessionId, uri);
|
|
834
|
+
if (contents.length === 0)
|
|
835
|
+
return { handled: true, response: `_No content returned for \`${uri}\`._` };
|
|
836
|
+
const lines = [`## Resource: \`${uri}\``, ''];
|
|
837
|
+
for (const c of contents) {
|
|
838
|
+
if (c.text !== undefined) {
|
|
839
|
+
const fence = c.mimeType?.includes('json') ? 'json' : c.mimeType?.includes('markdown') ? 'markdown' : '';
|
|
840
|
+
lines.push('```' + fence);
|
|
841
|
+
lines.push(c.text);
|
|
842
|
+
lines.push('```');
|
|
843
|
+
}
|
|
844
|
+
else if (c.blob) {
|
|
845
|
+
lines.push(`_(${c.mimeType ?? 'binary'} blob, ${c.blob.length} base64 chars — not rendered)_`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return { handled: true, response: lines.join('\n') };
|
|
849
|
+
}
|
|
850
|
+
catch (err) {
|
|
851
|
+
return { handled: true, response: `Failed to read \`${uri}\`: ${err.message}` };
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (sub === 'prompts') {
|
|
855
|
+
const { getSessionPrompts, awaitSessionReady } = await import('../utils/mcpRegistry.js');
|
|
856
|
+
await awaitSessionReady(session.sessionId);
|
|
857
|
+
const groups = await getSessionPrompts(session.sessionId);
|
|
858
|
+
if (groups.length === 0) {
|
|
859
|
+
return { handled: true, response: '_No MCP server in this session exposes prompt templates._' };
|
|
860
|
+
}
|
|
861
|
+
const lines = ['## MCP prompt templates', ''];
|
|
862
|
+
for (const g of groups) {
|
|
863
|
+
lines.push(`**${g.serverName}** — ${g.prompts.length} prompt${g.prompts.length === 1 ? '' : 's'}`);
|
|
864
|
+
for (const p of g.prompts) {
|
|
865
|
+
const argList = p.arguments?.length
|
|
866
|
+
? ` (${p.arguments.map(a => a.required ? a.name : `[${a.name}]`).join(', ')})`
|
|
867
|
+
: '';
|
|
868
|
+
lines.push(`- \`${p.name}\`${argList}${p.description ? ` — ${p.description}` : ''}`);
|
|
869
|
+
}
|
|
870
|
+
lines.push('');
|
|
871
|
+
}
|
|
872
|
+
lines.push('Materialise one with `/mcp prompt <server> <name> [key=value...]`.');
|
|
873
|
+
return { handled: true, response: lines.join('\n').trim() };
|
|
874
|
+
}
|
|
875
|
+
if (sub === 'prompt') {
|
|
876
|
+
const serverName = args[1];
|
|
877
|
+
const name = args[2];
|
|
878
|
+
if (!serverName || !name) {
|
|
879
|
+
return { handled: true, response: 'Usage: `/mcp prompt <server> <name> [key=value ...]`' };
|
|
880
|
+
}
|
|
881
|
+
const promptArgs = {};
|
|
882
|
+
for (const tok of args.slice(3)) {
|
|
883
|
+
const eq = tok.indexOf('=');
|
|
884
|
+
if (eq > 0)
|
|
885
|
+
promptArgs[tok.slice(0, eq)] = tok.slice(eq + 1);
|
|
886
|
+
}
|
|
887
|
+
const { getSessionPrompt } = await import('../utils/mcpRegistry.js');
|
|
888
|
+
try {
|
|
889
|
+
const { description, messages } = await getSessionPrompt(session.sessionId, serverName, name, promptArgs);
|
|
890
|
+
const lines = [`## Prompt \`${serverName}/${name}\``];
|
|
891
|
+
if (description)
|
|
892
|
+
lines.push(`_${description}_`);
|
|
893
|
+
lines.push('');
|
|
894
|
+
for (const m of messages) {
|
|
895
|
+
const text = typeof m.content?.text === 'string' ? m.content.text : JSON.stringify(m.content);
|
|
896
|
+
lines.push(`**${m.role}:** ${text}`);
|
|
897
|
+
lines.push('');
|
|
898
|
+
}
|
|
899
|
+
return { handled: true, response: lines.join('\n').trim() };
|
|
900
|
+
}
|
|
901
|
+
catch (err) {
|
|
902
|
+
return { handled: true, response: `Failed to materialise prompt: ${err.message}` };
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (sub === 'browse') {
|
|
906
|
+
const { formatMarketplaceList, MCP_MARKETPLACE } = await import('../utils/mcpMarketplace.js');
|
|
907
|
+
const detail = args[1];
|
|
908
|
+
if (detail) {
|
|
909
|
+
const { findMarketplaceEntry, formatMarketplaceEntry } = await import('../utils/mcpMarketplace.js');
|
|
910
|
+
const entry = findMarketplaceEntry(detail);
|
|
911
|
+
if (!entry)
|
|
912
|
+
return { handled: true, response: `Marketplace id not found: \`${detail}\`. Run \`/mcp browse\` for the list.` };
|
|
913
|
+
return { handled: true, response: formatMarketplaceEntry(entry) + `\n\nInstall with \`/mcp install ${entry.id} ${entry.argHints?.map(h => `<${h.placeholder ?? 'arg'}>`).join(' ') ?? ''}\`` };
|
|
914
|
+
}
|
|
915
|
+
return { handled: true, response: formatMarketplaceList() + `\n\nRun \`/mcp browse <id>\` for details or \`/mcp install <id> [args]\` to install. Total: ${MCP_MARKETPLACE.length}.` };
|
|
916
|
+
}
|
|
917
|
+
if (sub === 'install') {
|
|
918
|
+
const id = args[1];
|
|
919
|
+
if (!id)
|
|
920
|
+
return { handled: true, response: 'Usage: `/mcp install <id> [extra args...]` — run `/mcp browse` to see ids.' };
|
|
921
|
+
const { findMarketplaceEntry } = await import('../utils/mcpMarketplace.js');
|
|
922
|
+
const entry = findMarketplaceEntry(id);
|
|
923
|
+
if (!entry)
|
|
924
|
+
return { handled: true, response: `Marketplace id not found: \`${id}\`. Run \`/mcp browse\` for the list.` };
|
|
925
|
+
// Merge skeleton args with user-supplied extras.
|
|
926
|
+
const extraArgs = args.slice(2);
|
|
927
|
+
const fullArgs = [...(entry.server.args ?? []), ...extraArgs];
|
|
928
|
+
const server = {
|
|
929
|
+
name: entry.id,
|
|
930
|
+
command: entry.server.command,
|
|
931
|
+
args: fullArgs,
|
|
932
|
+
env: entry.server.env,
|
|
933
|
+
url: entry.server.url,
|
|
934
|
+
headers: entry.server.headers,
|
|
935
|
+
};
|
|
936
|
+
// Reuse the same add helper as `/mcp add`.
|
|
937
|
+
addProjectMcpServer(session.workspaceRoot, server);
|
|
938
|
+
onChunk(`_Saved \`${entry.id}\` to project config. Spawning…_\n\n`);
|
|
939
|
+
const merged = loadMcpServerConfig(session.workspaceRoot);
|
|
940
|
+
const { registered, errors } = await registerSessionServers(session.sessionId, merged, { workspaceRoot: session.workspaceRoot });
|
|
941
|
+
const failed = errors.find(e => e.server === entry.id);
|
|
942
|
+
const lines = [];
|
|
943
|
+
if (failed) {
|
|
944
|
+
lines.push(`Saved \`${entry.id}\` but spawn failed: \`${failed.error}\``);
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
const ok = registered.filter(t => t.serverName === entry.id);
|
|
948
|
+
lines.push(`Installed **${entry.name}** (\`${entry.id}\`) — ${ok.length} tool${ok.length === 1 ? '' : 's'} available.`);
|
|
949
|
+
}
|
|
950
|
+
if (entry.envNotes?.length) {
|
|
951
|
+
lines.push('', '**Environment variables you may need:**');
|
|
952
|
+
for (const e of entry.envNotes) {
|
|
953
|
+
const req = e.required ? ' (required)' : '';
|
|
954
|
+
lines.push(`- \`${e.name}\`${req} — ${e.description}`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return { handled: true, response: lines.join('\n'), streaming: true };
|
|
958
|
+
}
|
|
959
|
+
if (sub === 'reload') {
|
|
960
|
+
// Re-read both config files and re-register. Used after a manual
|
|
961
|
+
// edit of `.codeep/mcp_servers.json` outside the CLI — `/mcp add`
|
|
962
|
+
// and `/mcp remove` already re-register automatically.
|
|
963
|
+
onChunk(`_Reloading MCP server config…_\n\n`);
|
|
964
|
+
const merged = loadMcpServerConfig(session.workspaceRoot);
|
|
965
|
+
const { registered, errors } = await registerSessionServers(session.sessionId, merged, { workspaceRoot: session.workspaceRoot });
|
|
966
|
+
const lines = [
|
|
967
|
+
`## MCP reloaded`,
|
|
968
|
+
'',
|
|
969
|
+
`**${registered.length}** tool${registered.length === 1 ? '' : 's'} from **${merged.length}** server${merged.length === 1 ? '' : 's'}.`,
|
|
970
|
+
];
|
|
971
|
+
if (errors.length > 0) {
|
|
972
|
+
lines.push('', '### Failed servers');
|
|
973
|
+
for (const e of errors)
|
|
974
|
+
lines.push(`- **${e.server}** — \`${e.error}\``);
|
|
975
|
+
}
|
|
976
|
+
return { handled: true, response: lines.join('\n'), streaming: true };
|
|
977
|
+
}
|
|
978
|
+
// Default: list (and 'list' / no-arg behave the same)
|
|
979
|
+
// Read-only inspector for the MCP servers wired into this session.
|
|
980
|
+
// Sources: file config (`.codeep/mcp_servers.json` project + global)
|
|
981
|
+
// merged with any `mcpServers` the ACP client passed on session/new.
|
|
982
|
+
// The agent can call these tools via `<server>__<tool>`.
|
|
983
|
+
const { getSessionTools, getSessionRegistrationErrors, awaitSessionReady } = await import('../utils/mcpRegistry.js');
|
|
984
|
+
// If session/new is still spinning up servers, wait — otherwise this
|
|
985
|
+
// command would report "no servers" right after a slow npx install.
|
|
986
|
+
await awaitSessionReady(session.sessionId);
|
|
987
|
+
const tools = await getSessionTools(session.sessionId);
|
|
988
|
+
const errors = getSessionRegistrationErrors(session.sessionId);
|
|
989
|
+
if (tools.length === 0 && errors.length === 0) {
|
|
990
|
+
return {
|
|
991
|
+
handled: true,
|
|
992
|
+
response: [
|
|
993
|
+
'_No MCP servers connected to this session._',
|
|
994
|
+
'',
|
|
995
|
+
'Add one with `/mcp add <name> <command> [args...]` — it persists to `.codeep/mcp_servers.json`. ACP clients (Zed, VS Code) can also pass servers via the `mcpServers` field on `session/new`; both sources merge, ACP wins on collisions.',
|
|
996
|
+
].join('\n'),
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
const lines = ['## MCP servers', ''];
|
|
1000
|
+
if (tools.length > 0) {
|
|
1001
|
+
// Group by server for a readable listing.
|
|
1002
|
+
const byServer = new Map();
|
|
1003
|
+
for (const t of tools) {
|
|
1004
|
+
if (!byServer.has(t.serverName))
|
|
1005
|
+
byServer.set(t.serverName, []);
|
|
1006
|
+
byServer.get(t.serverName).push(t);
|
|
1007
|
+
}
|
|
1008
|
+
for (const [serverName, serverTools] of byServer) {
|
|
1009
|
+
lines.push(`**${serverName}** — ${serverTools.length} tool${serverTools.length === 1 ? '' : 's'}`);
|
|
1010
|
+
for (const t of serverTools) {
|
|
1011
|
+
const desc = t.description ? ` — ${t.description}` : '';
|
|
1012
|
+
lines.push(`- \`${t.agentName}\`${desc}`);
|
|
1013
|
+
}
|
|
1014
|
+
lines.push('');
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (errors.length > 0) {
|
|
1018
|
+
lines.push('### Failed servers');
|
|
1019
|
+
for (const e of errors) {
|
|
1020
|
+
lines.push(`- **${e.server}** — \`${e.error}\``);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return { handled: true, response: lines.join('\n').trim() };
|
|
1024
|
+
}
|
|
1025
|
+
case 'rewind': {
|
|
1026
|
+
const id = args[0];
|
|
1027
|
+
if (!id)
|
|
1028
|
+
return { handled: true, response: 'Usage: `/rewind <id>` — run `/checkpoints` to see ids.' };
|
|
1029
|
+
const { loadCheckpoint, buildRewindGitHint } = await import('../utils/checkpoints.js');
|
|
1030
|
+
const cp = loadCheckpoint(session.workspaceRoot, id);
|
|
1031
|
+
if (!cp)
|
|
1032
|
+
return { handled: true, response: `Checkpoint not found: \`${id}\`. Run \`/checkpoints\` to list available ids.` };
|
|
1033
|
+
// Restore session conversation in place. Don't touch files — the hint
|
|
1034
|
+
// below tells the user how to restore them via git if they want.
|
|
1035
|
+
const replacedCount = session.history.length;
|
|
1036
|
+
session.history = cp.messages;
|
|
1037
|
+
saveSession(session.codeepSessionId, session.history, session.workspaceRoot);
|
|
1038
|
+
// If the checkpoint captured a different provider/model, switch back.
|
|
1039
|
+
// configOptionsChanged signals the client to refresh its dropdowns.
|
|
1040
|
+
let providerChanged = false;
|
|
1041
|
+
if (cp.provider && cp.provider !== getCurrentProvider().id) {
|
|
1042
|
+
setProvider(cp.provider);
|
|
1043
|
+
providerChanged = true;
|
|
1044
|
+
}
|
|
1045
|
+
if (cp.model && cp.model !== config.get('model')) {
|
|
1046
|
+
config.set('model', cp.model);
|
|
1047
|
+
providerChanged = true;
|
|
1048
|
+
}
|
|
1049
|
+
const lines = [
|
|
1050
|
+
`## Rewound to ${cp.name ? `**${cp.name}**` : `\`${cp.id}\``}`,
|
|
1051
|
+
'',
|
|
1052
|
+
`Restored ${cp.messages.length} message${cp.messages.length === 1 ? '' : 's'} (was ${replacedCount}).`,
|
|
1053
|
+
cp.provider && cp.model ? `Provider: \`${cp.provider}\` · Model: \`${cp.model}\`` : '',
|
|
1054
|
+
'',
|
|
1055
|
+
buildRewindGitHint(cp),
|
|
1056
|
+
].filter(Boolean);
|
|
1057
|
+
return { handled: true, response: lines.join('\n'), configOptionsChanged: providerChanged };
|
|
1058
|
+
}
|
|
1059
|
+
case 'profile': {
|
|
1060
|
+
const { saveProfile, loadProfile, applyProfile, listProfiles, deleteProfile } = await import('../config/index.js');
|
|
1061
|
+
const sub = args[0]?.toLowerCase();
|
|
1062
|
+
if (!sub || sub === 'list') {
|
|
1063
|
+
const profiles = listProfiles();
|
|
1064
|
+
if (profiles.length === 0)
|
|
1065
|
+
return { handled: true, response: '_No profiles saved. Use `/profile save <name>`._' };
|
|
1066
|
+
return { handled: true, response: ['## Profiles', '', ...profiles.map(p => `- \`${p}\``), '', 'Use `/profile load <name>` to apply.'].join('\n') };
|
|
1067
|
+
}
|
|
1068
|
+
if (sub === 'save') {
|
|
1069
|
+
const name = args[1];
|
|
1070
|
+
if (!name)
|
|
1071
|
+
return { handled: true, response: 'Usage: `/profile save <name>`' };
|
|
1072
|
+
return { handled: true, response: saveProfile(name) ? `Profile saved: \`${name}\`` : 'Failed to save profile.', configOptionsChanged: true };
|
|
1073
|
+
}
|
|
1074
|
+
if (sub === 'load') {
|
|
1075
|
+
const name = args[1];
|
|
1076
|
+
if (!name)
|
|
1077
|
+
return { handled: true, response: 'Usage: `/profile load <name>`' };
|
|
1078
|
+
const profile = loadProfile(name);
|
|
1079
|
+
if (!profile)
|
|
1080
|
+
return { handled: true, response: `Profile not found: \`${name}\`` };
|
|
1081
|
+
applyProfile(profile);
|
|
1082
|
+
return { handled: true, response: `Profile loaded: \`${profile.name}\` (${profile.provider} / ${profile.model})`, configOptionsChanged: true };
|
|
1083
|
+
}
|
|
1084
|
+
if (sub === 'delete') {
|
|
1085
|
+
const name = args[1];
|
|
1086
|
+
if (!name)
|
|
1087
|
+
return { handled: true, response: 'Usage: `/profile delete <name>`' };
|
|
1088
|
+
return { handled: true, response: deleteProfile(name) ? `Profile deleted: \`${name}\`` : `Profile not found: \`${name}\`` };
|
|
1089
|
+
}
|
|
1090
|
+
// Shorthand: `/profile <name>` → load
|
|
1091
|
+
const profile = loadProfile(sub);
|
|
1092
|
+
if (profile) {
|
|
1093
|
+
applyProfile(profile);
|
|
1094
|
+
return { handled: true, response: `Profile loaded: \`${profile.name}\` (${profile.provider} / ${profile.model})`, configOptionsChanged: true };
|
|
1095
|
+
}
|
|
1096
|
+
return { handled: true, response: `Unknown subcommand: \`${sub}\`. Use \`save\`, \`load\`, \`delete\`, \`list\`, or \`<name>\` to load.` };
|
|
1097
|
+
}
|
|
1098
|
+
// ─── Custom user commands + skills ────────────────────────────────────────
|
|
402
1099
|
default: {
|
|
1100
|
+
// 1. Project / global custom command (~/.codeep/commands or
|
|
1101
|
+
// <workspace>/.codeep/commands). User-authored Markdown templates.
|
|
1102
|
+
const { findCustomCommand, expandCommand } = await import('../utils/customCommands.js');
|
|
1103
|
+
const custom = findCustomCommand(cmd, session.workspaceRoot);
|
|
1104
|
+
if (custom) {
|
|
1105
|
+
const expandedPrompt = expandCommand(custom, args);
|
|
1106
|
+
// Treat the expanded body as if the user had typed it manually:
|
|
1107
|
+
// push it as a user message and run the agent (or chat if agent
|
|
1108
|
+
// mode is off), then persist the assistant reply.
|
|
1109
|
+
session.history.push({ role: 'user', content: expandedPrompt });
|
|
1110
|
+
onChunk(`_Running custom command **/${custom.name}** (${custom.scope})…_\n\n`);
|
|
1111
|
+
const projectCtx = getProjectContext(session.workspaceRoot);
|
|
1112
|
+
const agentMode = config.get('agentMode');
|
|
1113
|
+
let response = '';
|
|
1114
|
+
try {
|
|
1115
|
+
if (agentMode === 'on') {
|
|
1116
|
+
const { buildProjectContext } = await import('./session.js');
|
|
1117
|
+
const ctx = buildProjectContext(session.workspaceRoot);
|
|
1118
|
+
const agentResult = await runAgent(expandedPrompt, ctx, {
|
|
1119
|
+
abortSignal,
|
|
1120
|
+
onIteration: (_i, msg) => { onChunk(msg + '\n'); },
|
|
1121
|
+
onThinking: (text) => { onChunk(text); },
|
|
1122
|
+
});
|
|
1123
|
+
response = agentResult.finalResponse ?? '';
|
|
1124
|
+
if (response)
|
|
1125
|
+
onChunk(response);
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
await chat(expandedPrompt, session.history.slice(0, -1), // exclude the just-pushed user message
|
|
1129
|
+
(chunk) => { response += chunk; onChunk(chunk); }, undefined, projectCtx, undefined);
|
|
1130
|
+
}
|
|
1131
|
+
if (response)
|
|
1132
|
+
session.history.push({ role: 'assistant', content: response });
|
|
1133
|
+
saveSession(session.codeepSessionId, session.history, session.workspaceRoot);
|
|
1134
|
+
}
|
|
1135
|
+
catch (err) {
|
|
1136
|
+
onChunk(`\n\n_Custom command failed: ${err.message}_`);
|
|
1137
|
+
}
|
|
1138
|
+
return { handled: true, response: '', streaming: true };
|
|
1139
|
+
}
|
|
1140
|
+
// 2. Built-in skill.
|
|
403
1141
|
const { findSkill, parseSkillArgs, executeSkill, trackSkillUsage } = await import('../utils/skills.js');
|
|
404
1142
|
const skill = findSkill(cmd);
|
|
405
1143
|
if (!skill) {
|
|
406
|
-
return { handled: true, response: `Unknown command: \`/${cmd}\`\n\nType \`/help\` for available commands
|
|
1144
|
+
return { handled: true, response: `Unknown command: \`/${cmd}\`\n\nType \`/help\` for available commands, \`/skills\` to list all skills, or \`/commands\` for custom commands.` };
|
|
407
1145
|
}
|
|
408
1146
|
if (skill.requiresWriteAccess && !hasWritePermission(session.workspaceRoot)) {
|
|
409
1147
|
return { handled: true, response: 'This skill requires write access. Use `/grant` first.' };
|
|
@@ -505,18 +1243,34 @@ function buildHelp() {
|
|
|
505
1243
|
'| `/review <file...>` | Static analysis of specific file(s) |',
|
|
506
1244
|
'| `/diff [--staged]` | Git diff with AI review |',
|
|
507
1245
|
'',
|
|
1246
|
+
'**Project intelligence & profiles**',
|
|
1247
|
+
'| Command | Description |',
|
|
1248
|
+
'|---------|-------------|',
|
|
1249
|
+
'| `/memory <note>` | Add a project note (or `list` / `remove <n>` / `clear`) |',
|
|
1250
|
+
'| `/profile save <name>` | Save current provider/model/settings (or `load` / `delete` / `list`) |',
|
|
1251
|
+
'| `/hooks` | List installed lifecycle hooks (`.codeep/hooks/<event>.sh`) |',
|
|
1252
|
+
'',
|
|
508
1253
|
'**Actions & History**',
|
|
509
1254
|
'| Command | Description |',
|
|
510
1255
|
'|---------|-------------|',
|
|
511
1256
|
'| `/undo` | Undo last agent action |',
|
|
512
1257
|
'| `/undo-all` | Undo all actions in session |',
|
|
513
1258
|
'| `/changes` | Show session changes |',
|
|
1259
|
+
'| `/cost` | Show per-session token usage and estimated cost |',
|
|
1260
|
+
'| `/compact [keepN]` | Summarize older messages to free up context (keeps last N, default 4) |',
|
|
1261
|
+
'| `/checkpoint [name]` | Save a session snapshot (conversation + provider/model) |',
|
|
1262
|
+
'| `/checkpoints` | List saved checkpoints |',
|
|
1263
|
+
'| `/rewind <id>` | Restore a checkpoint (files via git, see hint after rewind) |',
|
|
514
1264
|
'| `/export [json\\|md\\|txt]` | Export conversation |',
|
|
515
1265
|
'',
|
|
516
1266
|
'**Skills** (type `/skills` to list all, or `/skills <query>` to search)',
|
|
517
1267
|
'`/commit` · `/fix` · `/test` · `/docs` · `/refactor` · `/explain`',
|
|
518
1268
|
'`/optimize` · `/debug` · `/push` · `/pr` · `/build` · `/deploy` …',
|
|
519
1269
|
'',
|
|
1270
|
+
'**Custom Commands** — drop Markdown files in `.codeep/commands/` (project)',
|
|
1271
|
+
'or `~/.codeep/commands/` (global) to define your own `/<name>` commands.',
|
|
1272
|
+
'Run `/commands` to list them.',
|
|
1273
|
+
'',
|
|
520
1274
|
'_Skills run as standalone workflows. For general coding requests, just describe the task._',
|
|
521
1275
|
].join('\n');
|
|
522
1276
|
}
|
|
@@ -581,11 +1335,20 @@ function showApiKey() {
|
|
|
581
1335
|
? `API key for \`${providerId}\`: configured (use \`/apikey <key>\` to update)`
|
|
582
1336
|
: `No API key set for \`${providerId}\`. Use \`/apikey <key>\` to set one.`;
|
|
583
1337
|
}
|
|
1338
|
+
// Inline API keys end up in the user's shell history (Zed/VS Code chat is
|
|
1339
|
+
// terminal-adjacent enough that screenshots and pasted transcripts also leak).
|
|
1340
|
+
// We still save the key (backwards compat — there's no prompt mechanism inside
|
|
1341
|
+
// ACP), but the response nudges users toward the safer paths and reminds them
|
|
1342
|
+
// to scrub the line they just typed.
|
|
1343
|
+
const INLINE_KEY_WARNING = '\n\n> ⚠️ The key you just typed is now in your shell / chat history.' +
|
|
1344
|
+
' Prefer setting the provider env var (see `/provider`) or using the' +
|
|
1345
|
+
' settings UI in the VS Code extension. Clear the line from history if' +
|
|
1346
|
+
' the machine is shared.';
|
|
584
1347
|
function setApiKeyCmd(key) {
|
|
585
1348
|
const providerId = getCurrentProvider().id;
|
|
586
1349
|
// setApiKey is async (keychain) — fire-and-forget, config cache updated synchronously
|
|
587
1350
|
setApiKey(key, providerId);
|
|
588
|
-
return `API key for \`${providerId}\` saved
|
|
1351
|
+
return `API key for \`${providerId}\` saved.${INLINE_KEY_WARNING}`;
|
|
589
1352
|
}
|
|
590
1353
|
function loginCmd(providerId, apiKey) {
|
|
591
1354
|
const provider = getProvider(providerId);
|
|
@@ -593,7 +1356,7 @@ function loginCmd(providerId, apiKey) {
|
|
|
593
1356
|
return `Provider \`${providerId}\` not found.\n\n${buildProviderList()}`;
|
|
594
1357
|
setProvider(providerId);
|
|
595
1358
|
setApiKey(apiKey, providerId);
|
|
596
|
-
return `Logged in as **${provider.name}** (\`${providerId}\`). Model: \`${provider.defaultModel}
|
|
1359
|
+
return `Logged in as **${provider.name}** (\`${providerId}\`). Model: \`${provider.defaultModel}\`.${INLINE_KEY_WARNING}`;
|
|
597
1360
|
}
|
|
598
1361
|
const PREVIEW_MESSAGES = 6; // last N messages to show on session restore
|
|
599
1362
|
const PREVIEW_MAX_CHARS = 300; // truncate long messages
|