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.
Files changed (60) hide show
  1. package/README.md +208 -0
  2. package/dist/acp/commands.js +770 -7
  3. package/dist/acp/protocol.d.ts +11 -2
  4. package/dist/acp/server.js +179 -11
  5. package/dist/acp/session.d.ts +3 -0
  6. package/dist/acp/session.js +5 -0
  7. package/dist/api/index.js +39 -6
  8. package/dist/config/index.d.ts +13 -0
  9. package/dist/config/index.js +45 -0
  10. package/dist/config/providers.js +76 -1
  11. package/dist/renderer/App.d.ts +12 -0
  12. package/dist/renderer/App.js +109 -4
  13. package/dist/renderer/agentExecution.js +5 -0
  14. package/dist/renderer/commands.js +638 -2
  15. package/dist/renderer/components/Help.js +28 -0
  16. package/dist/renderer/components/Login.d.ts +1 -0
  17. package/dist/renderer/components/Login.js +24 -9
  18. package/dist/renderer/handlers.d.ts +11 -1
  19. package/dist/renderer/handlers.js +30 -0
  20. package/dist/renderer/main.js +73 -0
  21. package/dist/utils/agent.d.ts +17 -0
  22. package/dist/utils/agent.js +91 -7
  23. package/dist/utils/agentChat.d.ts +10 -2
  24. package/dist/utils/agentChat.js +48 -9
  25. package/dist/utils/agentStream.js +6 -2
  26. package/dist/utils/checkpoints.d.ts +93 -0
  27. package/dist/utils/checkpoints.js +205 -0
  28. package/dist/utils/context.d.ts +24 -0
  29. package/dist/utils/context.js +57 -0
  30. package/dist/utils/customCommands.d.ts +62 -0
  31. package/dist/utils/customCommands.js +201 -0
  32. package/dist/utils/hooks.d.ts +97 -0
  33. package/dist/utils/hooks.js +223 -0
  34. package/dist/utils/mcpClient.d.ts +229 -0
  35. package/dist/utils/mcpClient.js +497 -0
  36. package/dist/utils/mcpConfig.d.ts +55 -0
  37. package/dist/utils/mcpConfig.js +177 -0
  38. package/dist/utils/mcpMarketplace.d.ts +49 -0
  39. package/dist/utils/mcpMarketplace.js +175 -0
  40. package/dist/utils/mcpRegistry.d.ts +129 -0
  41. package/dist/utils/mcpRegistry.js +427 -0
  42. package/dist/utils/mcpSamplingBridge.d.ts +32 -0
  43. package/dist/utils/mcpSamplingBridge.js +88 -0
  44. package/dist/utils/mcpStreamableHttp.d.ts +65 -0
  45. package/dist/utils/mcpStreamableHttp.js +207 -0
  46. package/dist/utils/openrouterPrefs.d.ts +36 -0
  47. package/dist/utils/openrouterPrefs.js +83 -0
  48. package/dist/utils/skillBundles.d.ts +84 -0
  49. package/dist/utils/skillBundles.js +257 -0
  50. package/dist/utils/skillBundlesCloud.d.ts +69 -0
  51. package/dist/utils/skillBundlesCloud.js +202 -0
  52. package/dist/utils/tokenTracker.d.ts +14 -2
  53. package/dist/utils/tokenTracker.js +59 -41
  54. package/dist/utils/toolExecution.d.ts +17 -1
  55. package/dist/utils/toolExecution.js +184 -6
  56. package/dist/utils/tools.d.ts +22 -6
  57. package/dist/utils/tools.js +83 -8
  58. package/package.json +3 -2
  59. package/bin/codeep-macos-arm64 +0 -0
  60. package/bin/codeep-macos-x64 +0 -0
@@ -96,6 +96,30 @@ export async function handleCommand(command, args, ctx) {
96
96
  const providerId = config.get('provider');
97
97
  const { isDynamicModelsProvider, isNoApiKeyProvider: _noKey } = await import('../config/providers.js');
98
98
  if (isDynamicModelsProvider(providerId)) {
99
+ if (providerId === 'openrouter') {
100
+ ctx.app.notify('Fetching OpenRouter catalog…');
101
+ const { fetchOpenRouterModels } = await import('../config/index.js');
102
+ const apiKey = (await import('../config/index.js')).getApiKey('openrouter');
103
+ const models = await fetchOpenRouterModels(apiKey || undefined);
104
+ if (!models || models.length === 0) {
105
+ ctx.app.notify('Could not fetch OpenRouter models (network? key?). Using built-in shortlist.');
106
+ const fallback = getModelsForCurrentProvider();
107
+ const currentModel = config.get('model');
108
+ const modelItems = Object.keys(fallback).map(id => ({ key: id, label: fallback[id], description: '' }));
109
+ ctx.app.showSelect('Select OpenRouter Model', modelItems, currentModel, (item) => {
110
+ config.set('model', item.key);
111
+ ctx.app.notify(`Model: ${item.key}`);
112
+ });
113
+ break;
114
+ }
115
+ const modelItems = models.map(m => ({ key: m.id, label: m.id, description: m.description }));
116
+ const currentModel = config.get('model');
117
+ ctx.app.showSelect(`Select OpenRouter Model (${models.length})`, modelItems, currentModel, (item) => {
118
+ config.set('model', item.key);
119
+ ctx.app.notify(`Model: ${item.key}`);
120
+ });
121
+ break;
122
+ }
99
123
  const { fetchOllamaModels } = await import('../config/index.js');
100
124
  ctx.app.notify('Fetching models from Ollama...');
101
125
  const ollamaModels = await fetchOllamaModels();
@@ -491,7 +515,7 @@ Format: use headers per category, only include categories where you found issues
491
515
  }
492
516
  case 'login': {
493
517
  const providers = getProviderList();
494
- ctx.app.showLogin(providers.map(p => ({ id: p.id, name: p.name, subscribeUrl: p.subscribeUrl, noApiKey: p.noApiKey })), async (result) => {
518
+ ctx.app.showLogin(providers.map(p => ({ id: p.id, name: p.name, description: p.description, subscribeUrl: p.subscribeUrl, noApiKey: p.noApiKey })), async (result) => {
495
519
  if (result) {
496
520
  setProvider(result.providerId);
497
521
  await setApiKey(result.apiKey);
@@ -790,6 +814,168 @@ Format: use headers per category, only include categories where you found issues
790
814
  }).catch(() => ctx.app.notify('No changes tracked'));
791
815
  break;
792
816
  }
817
+ case 'cost': {
818
+ const { formatCostReport } = await import('../utils/tokenTracker.js');
819
+ ctx.app.addMessage({ role: 'system', content: formatCostReport() });
820
+ break;
821
+ }
822
+ case 'compact': {
823
+ const messages = ctx.app.getMessages();
824
+ const keepRecent = args[0] ? Math.max(2, parseInt(args[0], 10) || 4) : 4;
825
+ if (messages.length <= keepRecent + 2) {
826
+ ctx.app.notify(`Nothing to compact — only ${messages.length} message(s) in this session.`);
827
+ break;
828
+ }
829
+ ctx.app.notify(`Compacting ${messages.length - keepRecent} older message(s)…`);
830
+ const { compactHistory } = await import('../utils/context.js');
831
+ try {
832
+ const result = await compactHistory(messages, { keepRecent, projectContext: ctx.projectContext });
833
+ if (result.replaced === 0) {
834
+ ctx.app.notify('Nothing to compact');
835
+ break;
836
+ }
837
+ ctx.app.setMessages(result.compacted);
838
+ // Persist so the compacted history survives a restart.
839
+ saveSession(ctx.sessionId, result.compacted, ctx.projectPath);
840
+ ctx.app.addMessage({
841
+ role: 'system',
842
+ content: `# Conversation Compacted\n\nReplaced ${result.replaced} earlier message${result.replaced === 1 ? '' : 's'} with a summary. Kept the last ${keepRecent} message${keepRecent === 1 ? '' : 's'} verbatim.\n\n**Summary:**\n\n${result.summary}`,
843
+ });
844
+ }
845
+ catch (err) {
846
+ ctx.app.notify(`Compaction failed: ${err.message}`);
847
+ }
848
+ break;
849
+ }
850
+ case 'checkpoint': {
851
+ const sub = args[0]?.toLowerCase();
852
+ const { createCheckpoint, deleteCheckpoint, listCheckpoints, formatCheckpointList } = await import('../utils/checkpoints.js');
853
+ const { getCurrentSessionActions } = await import('../utils/agent.js');
854
+ if (sub === 'delete') {
855
+ const id = args[1];
856
+ if (!id) {
857
+ ctx.app.notify('Usage: /checkpoint delete <id>');
858
+ break;
859
+ }
860
+ ctx.app.notify(deleteCheckpoint(ctx.projectPath, id) ? `Deleted checkpoint ${id}` : `Checkpoint not found: ${id}`);
861
+ break;
862
+ }
863
+ if (sub === 'list') {
864
+ ctx.app.addMessage({ role: 'system', content: formatCheckpointList(listCheckpoints(ctx.projectPath)) });
865
+ break;
866
+ }
867
+ const name = args.join(' ').trim() || undefined;
868
+ const provider = getCurrentProvider();
869
+ const filesTouched = Array.from(new Set(getCurrentSessionActions()
870
+ .filter(a => a.target && (a.type === 'write' || a.type === 'edit' || a.type === 'delete' || a.type === 'mkdir'))
871
+ .map(a => a.target)));
872
+ const cp = createCheckpoint({
873
+ workspaceRoot: ctx.projectPath,
874
+ sessionId: ctx.sessionId,
875
+ provider: provider.id,
876
+ model: config.get('model'),
877
+ messages: ctx.app.getMessages(),
878
+ filesTouched,
879
+ name,
880
+ });
881
+ ctx.app.addMessage({
882
+ role: 'system',
883
+ content: `# Checkpoint created\n\n\`${cp.id}\`${cp.name ? ` — **${cp.name}**` : ''}\n\nCaptured ${cp.messages.length} message${cp.messages.length === 1 ? '' : 's'}, ${cp.filesTouched.length} file${cp.filesTouched.length === 1 ? '' : 's'} touched${cp.gitHead ? `, git \`${cp.gitHead}\`` : ''}.\n\nUse \`/rewind ${cp.id}\` to restore.`,
884
+ });
885
+ break;
886
+ }
887
+ case 'checkpoints': {
888
+ const { listCheckpoints, formatCheckpointList } = await import('../utils/checkpoints.js');
889
+ ctx.app.addMessage({ role: 'system', content: formatCheckpointList(listCheckpoints(ctx.projectPath)) });
890
+ break;
891
+ }
892
+ case 'openrouter': {
893
+ const { readOpenRouterPreferences, writeOpenRouterPreferences, formatOpenRouterPreferences } = await import('../utils/openrouterPrefs.js');
894
+ const sub = args[0]?.toLowerCase();
895
+ const current = readOpenRouterPreferences();
896
+ if (!sub || sub === 'show') {
897
+ ctx.app.addMessage({ role: 'system', content: formatOpenRouterPreferences(current) });
898
+ break;
899
+ }
900
+ if (sub === 'clear') {
901
+ writeOpenRouterPreferences(null);
902
+ ctx.app.notify('OpenRouter preferences cleared');
903
+ break;
904
+ }
905
+ if (sub === 'prefer') {
906
+ const list = args.slice(1).join(' ').split(',').map(s => s.trim()).filter(Boolean);
907
+ if (list.length === 0) {
908
+ ctx.app.notify('Usage: /openrouter prefer <p1>[,<p2>...]');
909
+ break;
910
+ }
911
+ writeOpenRouterPreferences({ ...current, order: list });
912
+ ctx.app.notify(`OpenRouter preference: ${list.join(' → ')}`);
913
+ break;
914
+ }
915
+ if (sub === 'ignore') {
916
+ const list = args.slice(1).join(' ').split(',').map(s => s.trim()).filter(Boolean);
917
+ if (list.length === 0) {
918
+ ctx.app.notify('Usage: /openrouter ignore <p1>[,<p2>...]');
919
+ break;
920
+ }
921
+ writeOpenRouterPreferences({ ...current, ignore: list });
922
+ ctx.app.notify(`OpenRouter ignoring: ${list.join(', ')}`);
923
+ break;
924
+ }
925
+ if (sub === 'fallbacks') {
926
+ const val = args[1]?.toLowerCase();
927
+ if (val !== 'on' && val !== 'off') {
928
+ ctx.app.notify('Usage: /openrouter fallbacks on|off');
929
+ break;
930
+ }
931
+ writeOpenRouterPreferences({ ...current, allow_fallbacks: val === 'on' });
932
+ ctx.app.notify(`Fallbacks ${val}`);
933
+ break;
934
+ }
935
+ if (sub === 'privacy') {
936
+ const val = args[1]?.toLowerCase();
937
+ if (val !== 'strict' && val !== 'allow') {
938
+ ctx.app.notify('Usage: /openrouter privacy strict|allow');
939
+ break;
940
+ }
941
+ writeOpenRouterPreferences({ ...current, data_collection: val === 'strict' ? 'deny' : 'allow' });
942
+ ctx.app.notify(`Privacy: ${val}`);
943
+ break;
944
+ }
945
+ ctx.app.notify(`Unknown subcommand: ${sub}`);
946
+ break;
947
+ }
948
+ case 'hooks': {
949
+ const { listInstalledHooks, formatHookList } = await import('../utils/hooks.js');
950
+ ctx.app.addMessage({ role: 'system', content: formatHookList(listInstalledHooks(ctx.projectPath)) });
951
+ break;
952
+ }
953
+ case 'rewind': {
954
+ const id = args[0];
955
+ if (!id) {
956
+ ctx.app.notify('Usage: /rewind <id> — run /checkpoints to see ids');
957
+ break;
958
+ }
959
+ const { loadCheckpoint, buildRewindGitHint } = await import('../utils/checkpoints.js');
960
+ const cp = loadCheckpoint(ctx.projectPath, id);
961
+ if (!cp) {
962
+ ctx.app.notify(`Checkpoint not found: ${id}`);
963
+ break;
964
+ }
965
+ const replacedCount = ctx.app.getMessages().length;
966
+ ctx.app.setMessages(cp.messages);
967
+ saveSession(ctx.sessionId, cp.messages, ctx.projectPath);
968
+ // Switch provider/model back to checkpoint state if different.
969
+ if (cp.provider && cp.provider !== getCurrentProvider().id)
970
+ setProvider(cp.provider);
971
+ if (cp.model && cp.model !== config.get('model'))
972
+ config.set('model', cp.model);
973
+ ctx.app.addMessage({
974
+ role: 'system',
975
+ content: `# Rewound to ${cp.name ? `**${cp.name}**` : `\`${cp.id}\``}\n\nRestored ${cp.messages.length} message${cp.messages.length === 1 ? '' : 's'} (was ${replacedCount}). Provider: \`${cp.provider}\` · Model: \`${cp.model}\`\n\n${buildRewindGitHint(cp)}`,
976
+ });
977
+ break;
978
+ }
793
979
  case 'context-save': {
794
980
  const messages = ctx.app.getMessages();
795
981
  if (saveSession(`context-${ctx.sessionId}`, messages, ctx.projectPath)) {
@@ -911,6 +1097,148 @@ Format: use headers per category, only include categories where you found issues
911
1097
  break;
912
1098
  }
913
1099
  case 'skills': {
1100
+ const sub = args[0]?.toLowerCase();
1101
+ // Structured skill bundles (separate from the JSON skills in skills.ts).
1102
+ if (sub === 'bundles' || sub === 'list-bundles') {
1103
+ const { loadSkillBundles, formatBundleList } = await import('../utils/skillBundles.js');
1104
+ ctx.app.addMessage({ role: 'system', content: formatBundleList(loadSkillBundles(ctx.projectPath)) });
1105
+ break;
1106
+ }
1107
+ if (sub === 'create-bundle') {
1108
+ const name = (args[1] ?? '').toLowerCase();
1109
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
1110
+ ctx.app.notify('Usage: /skills create-bundle <name> — lowercase letters/digits/hyphens');
1111
+ break;
1112
+ }
1113
+ const { mkdirSync, writeFileSync, existsSync } = await import('fs');
1114
+ const { join } = await import('path');
1115
+ const dir = join(ctx.projectPath, '.codeep', 'skills', name);
1116
+ if (existsSync(dir)) {
1117
+ ctx.app.notify(`Skill ${name} already exists`);
1118
+ break;
1119
+ }
1120
+ mkdirSync(dir, { recursive: true });
1121
+ writeFileSync(join(dir, 'SKILL.md'), `---
1122
+ name: ${name}
1123
+ description: One-sentence summary shown in the agent's catalog.
1124
+ triggers:
1125
+ - keyword
1126
+ - phrase
1127
+ ---
1128
+
1129
+ # ${name}
1130
+
1131
+ Describe what this skill does. The agent reads this body verbatim when it invokes the skill.
1132
+
1133
+ ## Steps
1134
+
1135
+ 1. First step
1136
+ 2. Second step
1137
+ 3. …
1138
+ `);
1139
+ ctx.app.addMessage({
1140
+ role: 'system',
1141
+ content: `Created skill bundle at \`.codeep/skills/${name}/SKILL.md\`. Edit it, then \`/skills bundles\` to confirm.`,
1142
+ });
1143
+ break;
1144
+ }
1145
+ if (sub === 'show' || sub === 'detail') {
1146
+ const name = args[1];
1147
+ if (!name) {
1148
+ ctx.app.notify('Usage: /skills show <name>');
1149
+ break;
1150
+ }
1151
+ const { findSkillBundle } = await import('../utils/skillBundles.js');
1152
+ const bundle = findSkillBundle(name, ctx.projectPath);
1153
+ if (!bundle) {
1154
+ ctx.app.notify(`Skill ${name} not found`);
1155
+ break;
1156
+ }
1157
+ ctx.app.addMessage({
1158
+ role: 'system',
1159
+ content: `# ${bundle.name}\n_${bundle.description}_\n\n**Source:** ${bundle.source}\n\n---\n\n${bundle.body}`,
1160
+ });
1161
+ break;
1162
+ }
1163
+ // Marketplace operations against codeep.dev.
1164
+ if (sub === 'publish') {
1165
+ const slug = args[1];
1166
+ if (!slug) {
1167
+ ctx.app.notify('Usage: /skills publish <slug> [--public]');
1168
+ break;
1169
+ }
1170
+ const isPublic = args.includes('--public');
1171
+ ctx.app.notify(`Publishing ${slug} to codeep.dev…`);
1172
+ const { publishBundle } = await import('../utils/skillBundlesCloud.js');
1173
+ const result = await publishBundle(ctx.projectPath, slug, { isPublic });
1174
+ if (!result.ok) {
1175
+ ctx.app.notify(`Publish failed: ${result.error}`);
1176
+ break;
1177
+ }
1178
+ ctx.app.addMessage({
1179
+ role: 'system',
1180
+ content: `Published \`${slug}\` (${isPublic ? 'public' : 'private'}) to codeep.dev. Install elsewhere with \`/skills install ${result.skill?.owner_username ?? '<you>'}/${slug}\`.`,
1181
+ });
1182
+ break;
1183
+ }
1184
+ if (sub === 'install') {
1185
+ const target = args[1];
1186
+ if (!target) {
1187
+ ctx.app.notify('Usage: /skills install <owner>/<slug>');
1188
+ break;
1189
+ }
1190
+ const { installBundle } = await import('../utils/skillBundlesCloud.js');
1191
+ ctx.app.notify(`Fetching ${target}…`);
1192
+ const result = await installBundle(ctx.projectPath, target);
1193
+ if (!result.ok) {
1194
+ ctx.app.notify(`Install failed: ${result.error}`);
1195
+ break;
1196
+ }
1197
+ ctx.app.notify(`Installed ${result.name} to .codeep/skills/${result.name}/`);
1198
+ break;
1199
+ }
1200
+ if (sub === 'browse') {
1201
+ const query = args.slice(1).join(' ').trim();
1202
+ const { browseSkills } = await import('../utils/skillBundlesCloud.js');
1203
+ ctx.app.notify('Fetching marketplace…');
1204
+ const result = await browseSkills({ query });
1205
+ if (!result.ok) {
1206
+ ctx.app.notify(`Browse failed: ${result.error}`);
1207
+ break;
1208
+ }
1209
+ const skills = result.skills ?? [];
1210
+ if (skills.length === 0) {
1211
+ ctx.app.addMessage({ role: 'system', content: query ? `_No public skills matching "${query}"._` : '_No public skills published yet._' });
1212
+ break;
1213
+ }
1214
+ const lines = [`# ${query ? `Skills matching "${query}"` : 'Public skills'}`, ''];
1215
+ for (const s of skills.slice(0, 30)) {
1216
+ const owner = s.owner_username ?? s.github_id;
1217
+ const ver = s.version ? ` v${s.version}` : '';
1218
+ lines.push(`- **${s.name}** \`${owner}/${s.slug}\`${ver} — ${s.description} _(${s.install_count} installs)_`);
1219
+ }
1220
+ if (skills.length > 30)
1221
+ lines.push('', `_(showing first 30 of ${skills.length})_`);
1222
+ lines.push('', 'Install with `/skills install <owner>/<slug>`.');
1223
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
1224
+ break;
1225
+ }
1226
+ if (sub === 'unpublish') {
1227
+ const target = args[1];
1228
+ if (!target) {
1229
+ ctx.app.notify('Usage: /skills unpublish <owner>/<slug>');
1230
+ break;
1231
+ }
1232
+ const { unpublishBundle } = await import('../utils/skillBundlesCloud.js');
1233
+ const result = await unpublishBundle(target);
1234
+ if (!result.ok) {
1235
+ ctx.app.notify(`Unpublish failed: ${result.error}`);
1236
+ break;
1237
+ }
1238
+ ctx.app.notify(`Unpublished ${target} from codeep.dev`);
1239
+ break;
1240
+ }
1241
+ // Default: built-in JSON skills (legacy behaviour).
914
1242
  import('../utils/skills.js').then(({ getAllSkills, searchSkills, formatSkillsList, getSkillStats }) => {
915
1243
  const query = args.join(' ').toLowerCase();
916
1244
  if (query === 'stats') {
@@ -1352,10 +1680,318 @@ Format: use headers per category, only include categories where you found issues
1352
1680
  ctx.app.notify(`Memory saved (${intelligence.notes.length} total): "${note}"`);
1353
1681
  break;
1354
1682
  }
1355
- default:
1683
+ case 'commands': {
1684
+ const { loadCustomCommands, formatCommandList } = await import('../utils/customCommands.js');
1685
+ ctx.app.addMessage({ role: 'system', content: formatCommandList(loadCustomCommands(ctx.projectPath)) });
1686
+ break;
1687
+ }
1688
+ case 'mcp': {
1689
+ // Mirrors the ACP `/mcp` handler in src/acp/commands.ts. In TUI the
1690
+ // session id is the constant `codeep-tui` (the same one main.ts uses
1691
+ // when it spawns project MCP servers in the background) and the
1692
+ // workspace root is ctx.projectPath. Without a project we can still
1693
+ // browse the marketplace, but anything that mutates project config
1694
+ // refuses with a clear message.
1695
+ const sub = args[0]?.toLowerCase();
1696
+ const TUI_SESSION = 'codeep-tui';
1697
+ const projectPath = ctx.projectPath;
1698
+ const requireProject = () => {
1699
+ if (!projectPath) {
1700
+ ctx.app.notify('Open a project (cd into it before running codeep) to add or modify MCP servers.');
1701
+ return false;
1702
+ }
1703
+ return true;
1704
+ };
1705
+ const { addProjectMcpServer, removeProjectMcpServer, loadMcpServerConfig } = await import('../utils/mcpConfig.js');
1706
+ const { registerSessionServers } = await import('../utils/mcpRegistry.js');
1707
+ if (sub === 'add') {
1708
+ if (!requireProject())
1709
+ break;
1710
+ const name = args[1];
1711
+ const command = args[2];
1712
+ if (!name || !command) {
1713
+ ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp add <name> <command> [args...]` — e.g. `/mcp add fs npx @modelcontextprotocol/server-filesystem /path`' });
1714
+ break;
1715
+ }
1716
+ const extraArgs = args.slice(3);
1717
+ addProjectMcpServer(projectPath, { name, command, args: extraArgs });
1718
+ ctx.app.notify(`Saved MCP server ${name} to .codeep/mcp_servers.json. Spawning…`);
1719
+ const merged = loadMcpServerConfig(projectPath);
1720
+ const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
1721
+ const ok = registered.filter(t => t.serverName === name);
1722
+ const failed = errors.find(e => e.server === name);
1723
+ ctx.app.addMessage({
1724
+ role: 'system',
1725
+ content: failed
1726
+ ? `Saved \`${name}\` but spawn failed: \`${failed.error}\``
1727
+ : `Added \`${name}\` (${ok.length} tool${ok.length === 1 ? '' : 's'} available).`,
1728
+ });
1729
+ break;
1730
+ }
1731
+ if (sub === 'remove') {
1732
+ if (!requireProject())
1733
+ break;
1734
+ const name = args[1];
1735
+ if (!name) {
1736
+ ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp remove <name>`' });
1737
+ break;
1738
+ }
1739
+ const removed = removeProjectMcpServer(projectPath, name);
1740
+ if (!removed) {
1741
+ ctx.app.addMessage({ role: 'system', content: `No project-scoped MCP server named \`${name}\`.` });
1742
+ break;
1743
+ }
1744
+ const merged = loadMcpServerConfig(projectPath);
1745
+ await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
1746
+ ctx.app.addMessage({ role: 'system', content: `Removed \`${name}\` from project config and stopped its process.` });
1747
+ break;
1748
+ }
1749
+ if (sub === 'browse') {
1750
+ const { formatMarketplaceList, findMarketplaceEntry, formatMarketplaceEntry, MCP_MARKETPLACE } = await import('../utils/mcpMarketplace.js');
1751
+ const detail = args[1];
1752
+ if (detail) {
1753
+ const entry = findMarketplaceEntry(detail);
1754
+ if (!entry) {
1755
+ ctx.app.addMessage({ role: 'system', content: `Marketplace id not found: \`${detail}\`. Run \`/mcp browse\` for the list.` });
1756
+ }
1757
+ else {
1758
+ const argHints = entry.argHints?.map(h => `<${h.placeholder ?? 'arg'}>`).join(' ') ?? '';
1759
+ ctx.app.addMessage({ role: 'system', content: formatMarketplaceEntry(entry) + `\n\nInstall with \`/mcp install ${entry.id} ${argHints}\`` });
1760
+ }
1761
+ break;
1762
+ }
1763
+ ctx.app.addMessage({ role: 'system', content: formatMarketplaceList() + `\n\nRun \`/mcp browse <id>\` for details or \`/mcp install <id> [args]\` to install. Total: ${MCP_MARKETPLACE.length}.` });
1764
+ break;
1765
+ }
1766
+ if (sub === 'install') {
1767
+ if (!requireProject())
1768
+ break;
1769
+ const id = args[1];
1770
+ if (!id) {
1771
+ ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp install <id> [extra args...]` — run `/mcp browse` to see ids.' });
1772
+ break;
1773
+ }
1774
+ const { findMarketplaceEntry } = await import('../utils/mcpMarketplace.js');
1775
+ const entry = findMarketplaceEntry(id);
1776
+ if (!entry) {
1777
+ ctx.app.addMessage({ role: 'system', content: `Marketplace id not found: \`${id}\`. Run \`/mcp browse\` for the list.` });
1778
+ break;
1779
+ }
1780
+ const extraArgs = args.slice(2);
1781
+ const fullArgs = [...(entry.server.args ?? []), ...extraArgs];
1782
+ addProjectMcpServer(projectPath, {
1783
+ name: entry.id,
1784
+ command: entry.server.command,
1785
+ args: fullArgs,
1786
+ env: entry.server.env,
1787
+ url: entry.server.url,
1788
+ headers: entry.server.headers,
1789
+ });
1790
+ ctx.app.notify(`Saved ${entry.id} to project config. Spawning…`);
1791
+ const merged = loadMcpServerConfig(projectPath);
1792
+ const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
1793
+ const failed = errors.find(e => e.server === entry.id);
1794
+ const lines = [];
1795
+ if (failed) {
1796
+ lines.push(`Saved \`${entry.id}\` but spawn failed: \`${failed.error}\``);
1797
+ }
1798
+ else {
1799
+ const ok = registered.filter(t => t.serverName === entry.id);
1800
+ lines.push(`Installed **${entry.name}** (\`${entry.id}\`) — ${ok.length} tool${ok.length === 1 ? '' : 's'} available.`);
1801
+ }
1802
+ if (entry.envNotes?.length) {
1803
+ lines.push('', '**Environment variables you may need:**');
1804
+ for (const e of entry.envNotes) {
1805
+ const req = e.required ? ' (required)' : '';
1806
+ lines.push(`- \`${e.name}\`${req} — ${e.description}`);
1807
+ }
1808
+ }
1809
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
1810
+ break;
1811
+ }
1812
+ if (sub === 'reload') {
1813
+ if (!requireProject())
1814
+ break;
1815
+ ctx.app.notify('Reloading MCP server config…');
1816
+ const merged = loadMcpServerConfig(projectPath);
1817
+ const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
1818
+ const lines = [`## MCP reloaded`, '', `**${registered.length}** tool${registered.length === 1 ? '' : 's'} from **${merged.length}** server${merged.length === 1 ? '' : 's'}.`];
1819
+ if (errors.length > 0) {
1820
+ lines.push('', '### Failed servers');
1821
+ for (const e of errors)
1822
+ lines.push(`- **${e.server}** — \`${e.error}\``);
1823
+ }
1824
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
1825
+ break;
1826
+ }
1827
+ if (sub === 'resources') {
1828
+ const { getSessionResources, awaitSessionReady } = await import('../utils/mcpRegistry.js');
1829
+ await awaitSessionReady(TUI_SESSION);
1830
+ const groups = await getSessionResources(TUI_SESSION);
1831
+ if (groups.length === 0) {
1832
+ ctx.app.addMessage({ role: 'system', content: '_No MCP server in this session exposes resources._' });
1833
+ break;
1834
+ }
1835
+ const lines = ['## MCP resources', ''];
1836
+ for (const g of groups) {
1837
+ lines.push(`**${g.serverName}** — ${g.resources.length} resource${g.resources.length === 1 ? '' : 's'}`);
1838
+ for (const r of g.resources) {
1839
+ const label = r.name ? `${r.name} — ` : '';
1840
+ const mime = r.mimeType ? ` (${r.mimeType})` : '';
1841
+ lines.push(`- ${label}\`${r.uri}\`${mime}${r.description ? ` — ${r.description}` : ''}`);
1842
+ }
1843
+ lines.push('');
1844
+ }
1845
+ lines.push('Read one with `/mcp read <uri>`.');
1846
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
1847
+ break;
1848
+ }
1849
+ if (sub === 'read') {
1850
+ const uri = args[1];
1851
+ if (!uri) {
1852
+ ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp read <uri>` — run `/mcp resources` to see available URIs.' });
1853
+ break;
1854
+ }
1855
+ const { readSessionResource } = await import('../utils/mcpRegistry.js');
1856
+ try {
1857
+ const contents = await readSessionResource(TUI_SESSION, uri);
1858
+ if (contents.length === 0) {
1859
+ ctx.app.addMessage({ role: 'system', content: `_No content returned for \`${uri}\`._` });
1860
+ break;
1861
+ }
1862
+ const lines = [`## Resource: \`${uri}\``, ''];
1863
+ for (const c of contents) {
1864
+ if (c.text !== undefined) {
1865
+ const fence = c.mimeType?.includes('json') ? 'json' : c.mimeType?.includes('markdown') ? 'markdown' : '';
1866
+ lines.push('```' + fence);
1867
+ lines.push(c.text);
1868
+ lines.push('```');
1869
+ }
1870
+ else if (c.blob) {
1871
+ lines.push(`_(${c.mimeType ?? 'binary'} blob, ${c.blob.length} base64 chars — not rendered)_`);
1872
+ }
1873
+ }
1874
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
1875
+ }
1876
+ catch (err) {
1877
+ ctx.app.addMessage({ role: 'system', content: `Failed to read \`${uri}\`: ${err.message}` });
1878
+ }
1879
+ break;
1880
+ }
1881
+ if (sub === 'prompts') {
1882
+ const { getSessionPrompts, awaitSessionReady } = await import('../utils/mcpRegistry.js');
1883
+ await awaitSessionReady(TUI_SESSION);
1884
+ const groups = await getSessionPrompts(TUI_SESSION);
1885
+ if (groups.length === 0) {
1886
+ ctx.app.addMessage({ role: 'system', content: '_No MCP server in this session exposes prompt templates._' });
1887
+ break;
1888
+ }
1889
+ const lines = ['## MCP prompt templates', ''];
1890
+ for (const g of groups) {
1891
+ lines.push(`**${g.serverName}** — ${g.prompts.length} prompt${g.prompts.length === 1 ? '' : 's'}`);
1892
+ for (const p of g.prompts) {
1893
+ const argList = p.arguments?.length
1894
+ ? ` (${p.arguments.map(a => a.required ? a.name : `[${a.name}]`).join(', ')})`
1895
+ : '';
1896
+ lines.push(`- \`${p.name}\`${argList}${p.description ? ` — ${p.description}` : ''}`);
1897
+ }
1898
+ lines.push('');
1899
+ }
1900
+ lines.push('Materialise one with `/mcp prompt <server> <name> [key=value...]`.');
1901
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
1902
+ break;
1903
+ }
1904
+ if (sub === 'prompt') {
1905
+ const serverName = args[1];
1906
+ const name = args[2];
1907
+ if (!serverName || !name) {
1908
+ ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp prompt <server> <name> [key=value ...]`' });
1909
+ break;
1910
+ }
1911
+ const promptArgs = {};
1912
+ for (const tok of args.slice(3)) {
1913
+ const eq = tok.indexOf('=');
1914
+ if (eq > 0)
1915
+ promptArgs[tok.slice(0, eq)] = tok.slice(eq + 1);
1916
+ }
1917
+ const { getSessionPrompt } = await import('../utils/mcpRegistry.js');
1918
+ try {
1919
+ const { description, messages } = await getSessionPrompt(TUI_SESSION, serverName, name, promptArgs);
1920
+ const lines = [`## Prompt \`${serverName}/${name}\``];
1921
+ if (description)
1922
+ lines.push(`_${description}_`);
1923
+ lines.push('');
1924
+ for (const m of messages) {
1925
+ const text = typeof m.content?.text === 'string' ? m.content.text : JSON.stringify(m.content);
1926
+ lines.push(`**${m.role}:** ${text}`);
1927
+ lines.push('');
1928
+ }
1929
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
1930
+ }
1931
+ catch (err) {
1932
+ ctx.app.addMessage({ role: 'system', content: `Failed to materialise prompt: ${err.message}` });
1933
+ }
1934
+ break;
1935
+ }
1936
+ // Default: list servers + tools for the current session.
1937
+ const { getSessionTools, getSessionRegistrationErrors, awaitSessionReady } = await import('../utils/mcpRegistry.js');
1938
+ await awaitSessionReady(TUI_SESSION);
1939
+ const tools = await getSessionTools(TUI_SESSION);
1940
+ const mcpErrors = getSessionRegistrationErrors(TUI_SESSION);
1941
+ if (tools.length === 0 && mcpErrors.length === 0) {
1942
+ ctx.app.addMessage({
1943
+ role: 'system',
1944
+ content: [
1945
+ '_No MCP servers connected to this session._',
1946
+ '',
1947
+ 'Add one with `/mcp add <name> <command> [args...]` — it persists to `.codeep/mcp_servers.json`.',
1948
+ 'Or browse the marketplace with `/mcp browse` and install with `/mcp install <id>`.',
1949
+ ].join('\n'),
1950
+ });
1951
+ break;
1952
+ }
1953
+ const lines = ['## MCP servers', ''];
1954
+ if (tools.length > 0) {
1955
+ const byServer = new Map();
1956
+ for (const t of tools) {
1957
+ if (!byServer.has(t.serverName))
1958
+ byServer.set(t.serverName, []);
1959
+ byServer.get(t.serverName).push(t);
1960
+ }
1961
+ for (const [serverName, serverTools] of byServer) {
1962
+ lines.push(`**${serverName}** — ${serverTools.length} tool${serverTools.length === 1 ? '' : 's'}`);
1963
+ for (const t of serverTools) {
1964
+ const desc = t.description ? ` — ${t.description}` : '';
1965
+ lines.push(`- \`${t.agentName}\`${desc}`);
1966
+ }
1967
+ lines.push('');
1968
+ }
1969
+ }
1970
+ if (mcpErrors.length > 0) {
1971
+ lines.push('### Failed servers');
1972
+ for (const e of mcpErrors)
1973
+ lines.push(`- **${e.server}** — \`${e.error}\``);
1974
+ }
1975
+ ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
1976
+ break;
1977
+ }
1978
+ default: {
1979
+ // 1. Try custom user command (project + global Markdown templates).
1980
+ const { findCustomCommand, expandCommand } = await import('../utils/customCommands.js');
1981
+ const custom = findCustomCommand(command, ctx.projectPath);
1982
+ if (custom) {
1983
+ const expandedPrompt = expandCommand(custom, args);
1984
+ ctx.app.notify(`Running custom command /${command} (${custom.scope})`);
1985
+ ctx.app.addMessage({ role: 'user', content: expandedPrompt });
1986
+ const { runAgentTask } = await import('./agentExecution.js');
1987
+ runAgentTask(expandedPrompt, false, ctx, () => null, () => { });
1988
+ break;
1989
+ }
1990
+ // 2. Fall through to skill registry.
1356
1991
  runSkill(command, args, ctx).then(handled => {
1357
1992
  if (!handled)
1358
1993
  ctx.app.notify(`Unknown command: /${command}`);
1359
1994
  });
1995
+ }
1360
1996
  }
1361
1997
  }