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
|
@@ -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
|
-
|
|
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
|
}
|