codeep 1.3.42 → 2.0.0
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 +96 -4
- package/dist/renderer/agentExecution.js +5 -0
- package/dist/renderer/commands.js +348 -2
- 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 +66 -0
- package/dist/utils/skillBundlesCloud.js +196 -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,28 @@ 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
|
+
default: {
|
|
1689
|
+
// 1. Try custom user command (project + global Markdown templates).
|
|
1690
|
+
const { findCustomCommand, expandCommand } = await import('../utils/customCommands.js');
|
|
1691
|
+
const custom = findCustomCommand(command, ctx.projectPath);
|
|
1692
|
+
if (custom) {
|
|
1693
|
+
const expandedPrompt = expandCommand(custom, args);
|
|
1694
|
+
ctx.app.notify(`Running custom command /${command} (${custom.scope})`);
|
|
1695
|
+
ctx.app.addMessage({ role: 'user', content: expandedPrompt });
|
|
1696
|
+
const { runAgentTask } = await import('./agentExecution.js');
|
|
1697
|
+
runAgentTask(expandedPrompt, false, ctx, () => null, () => { });
|
|
1698
|
+
break;
|
|
1699
|
+
}
|
|
1700
|
+
// 2. Fall through to skill registry.
|
|
1356
1701
|
runSkill(command, args, ctx).then(handled => {
|
|
1357
1702
|
if (!handled)
|
|
1358
1703
|
ctx.app.notify(`Unknown command: /${command}`);
|
|
1359
1704
|
});
|
|
1705
|
+
}
|
|
1360
1706
|
}
|
|
1361
1707
|
}
|
|
@@ -190,15 +190,17 @@ export function renderProviderSelect(screen, providers, selectedIndex) {
|
|
|
190
190
|
const { width, height } = screen.getSize();
|
|
191
191
|
screen.clear();
|
|
192
192
|
// Title
|
|
193
|
-
const title = '═══ Codeep
|
|
193
|
+
const title = '═══ Welcome to Codeep ═══';
|
|
194
194
|
const titleX = Math.floor((width - title.length) / 2);
|
|
195
195
|
screen.write(titleX, 1, title, PRIMARY_COLOR + style.bold);
|
|
196
196
|
// Subtitle
|
|
197
|
-
const subtitle = '
|
|
197
|
+
const subtitle = 'Pick an AI provider — you can switch later with /provider';
|
|
198
198
|
const subtitleX = Math.floor((width - subtitle.length) / 2);
|
|
199
199
|
screen.write(subtitleX, 3, subtitle, fg.white);
|
|
200
|
-
// Box
|
|
201
|
-
const
|
|
200
|
+
// Box — wider so we can show name + description on a single row.
|
|
201
|
+
const longestName = providers.reduce((m, p) => Math.max(m, p.name.length), 0);
|
|
202
|
+
const longestDesc = providers.reduce((m, p) => Math.max(m, (p.description ?? '').length), 0);
|
|
203
|
+
const boxWidth = Math.min(width - 4, Math.max(60, 6 + longestName + 3 + longestDesc));
|
|
202
204
|
const boxHeight = providers.length + 4;
|
|
203
205
|
const { x: boxX, y: boxY } = centerBox(width, height, boxWidth, boxHeight);
|
|
204
206
|
const boxLines = createBox({
|
|
@@ -212,19 +214,32 @@ export function renderProviderSelect(screen, providers, selectedIndex) {
|
|
|
212
214
|
for (const line of boxLines) {
|
|
213
215
|
screen.writeLine(line.y, line.text, line.style);
|
|
214
216
|
}
|
|
215
|
-
// Provider list
|
|
217
|
+
// Provider list — name in white/bold-on-selected, dim description beside it.
|
|
218
|
+
// Description is clipped to the room remaining inside the box so it never
|
|
219
|
+
// overwrites the right border on narrow terminals (80-col laptop splits).
|
|
216
220
|
const contentX = boxX + 3;
|
|
217
|
-
|
|
221
|
+
const contentY = boxY + 2;
|
|
222
|
+
const nameColWidth = longestName + 2;
|
|
223
|
+
const descStartX = contentX + 2 + nameColWidth;
|
|
224
|
+
const boxInnerRight = boxX + boxWidth - 2;
|
|
225
|
+
const descBudget = Math.max(0, boxInnerRight - descStartX);
|
|
218
226
|
for (let i = 0; i < providers.length; i++) {
|
|
219
227
|
const provider = providers[i];
|
|
220
228
|
const isSelected = i === selectedIndex;
|
|
221
229
|
const prefix = isSelected ? '► ' : ' ';
|
|
222
|
-
const
|
|
223
|
-
|
|
230
|
+
const nameStyle = isSelected ? PRIMARY_BRIGHT + style.bold : fg.white;
|
|
231
|
+
const descStyle = isSelected ? fg.white : fg.gray;
|
|
232
|
+
screen.write(contentX, contentY + i, prefix + provider.name.padEnd(nameColWidth), nameStyle);
|
|
233
|
+
if (provider.description && descBudget > 0) {
|
|
234
|
+
const desc = provider.description.length > descBudget
|
|
235
|
+
? provider.description.slice(0, Math.max(1, descBudget - 1)) + '…'
|
|
236
|
+
: provider.description;
|
|
237
|
+
screen.write(descStartX, contentY + i, desc, descStyle);
|
|
238
|
+
}
|
|
224
239
|
}
|
|
225
240
|
// Footer
|
|
226
241
|
const footerY = height - 2;
|
|
227
|
-
screen.write(2, footerY, '↑↓ Navigate
|
|
242
|
+
screen.write(2, footerY, '↑↓ Navigate · Enter Select · Esc skip (provider chosen later)', fg.gray);
|
|
228
243
|
screen.showCursor(false);
|
|
229
244
|
screen.fullRender();
|
|
230
245
|
}
|
|
@@ -26,7 +26,17 @@ export interface MenuHandlerContext {
|
|
|
26
26
|
close(callback: ((item: SelectItem) => void) | null, selected: SelectItem | null): void;
|
|
27
27
|
render(): void;
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Optional context for type-to-filter support. Callers that supply these
|
|
31
|
+
* get inline filtering: letters/digits append to `filter`, Backspace
|
|
32
|
+
* deletes, first Esc clears a non-empty filter, second Esc closes.
|
|
33
|
+
* Callers that omit them behave exactly as before (back-compat).
|
|
34
|
+
*/
|
|
35
|
+
export interface MenuFilterContext {
|
|
36
|
+
filter: string;
|
|
37
|
+
setFilter(v: string): void;
|
|
38
|
+
}
|
|
39
|
+
export declare function handleMenuKey(event: KeyEvent, ctx: MenuHandlerContext & Partial<MenuFilterContext>): void;
|
|
30
40
|
declare const PERMISSION_OPTIONS: readonly ["read", "write", "none"];
|
|
31
41
|
type PermissionLevel = typeof PERMISSION_OPTIONS[number];
|
|
32
42
|
export interface PermissionHandlerContext {
|
|
@@ -42,7 +42,14 @@ export function handleInlineHelpKey(event, ctx) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
export function handleMenuKey(event, ctx) {
|
|
45
|
+
const supportsFilter = typeof ctx.setFilter === 'function';
|
|
45
46
|
if (event.key === 'escape') {
|
|
47
|
+
// Two-stage Esc when filter is on: first clears the filter, second closes.
|
|
48
|
+
if (supportsFilter && ctx.filter) {
|
|
49
|
+
ctx.setFilter('');
|
|
50
|
+
ctx.render();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
46
53
|
ctx.close(null, null);
|
|
47
54
|
ctx.render();
|
|
48
55
|
return;
|
|
@@ -71,6 +78,29 @@ export function handleMenuKey(event, ctx) {
|
|
|
71
78
|
const selected = ctx.items[ctx.index];
|
|
72
79
|
ctx.close(null, selected);
|
|
73
80
|
ctx.render();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!supportsFilter)
|
|
84
|
+
return;
|
|
85
|
+
// Backspace removes the last filter char.
|
|
86
|
+
if (event.key === 'backspace') {
|
|
87
|
+
if (ctx.filter) {
|
|
88
|
+
ctx.setFilter(ctx.filter.slice(0, -1));
|
|
89
|
+
ctx.render();
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Single printable char (letters, digits, punctuation). Modifier-free,
|
|
94
|
+
// non-paste. We deliberately exclude bare-space at empty filter so a
|
|
95
|
+
// stray space doesn't kick the user into an unintended filtered view.
|
|
96
|
+
if (!event.ctrl && !event.alt && !event.isPaste && event.key.length === 1) {
|
|
97
|
+
const ch = event.key;
|
|
98
|
+
if (ch === ' ' && !ctx.filter)
|
|
99
|
+
return;
|
|
100
|
+
if (/^[\S ]$/.test(ch)) {
|
|
101
|
+
ctx.setFilter((ctx.filter ?? '') + ch);
|
|
102
|
+
ctx.render();
|
|
103
|
+
}
|
|
74
104
|
}
|
|
75
105
|
}
|
|
76
106
|
// ─── Permission ──────────────────────────────────────────────────────────────
|
package/dist/renderer/main.js
CHANGED
|
@@ -540,10 +540,83 @@ Commands (in chat):
|
|
|
540
540
|
welcomeLines.push(githubId
|
|
541
541
|
? ` Account codeep.dev linked`
|
|
542
542
|
: ` Account not linked · run: codeep account`);
|
|
543
|
+
// Warn before first use if this workspace defines project-scoped custom
|
|
544
|
+
// slash commands. They run as user prompts — a hostile or unfamiliar repo
|
|
545
|
+
// could ship `.codeep/commands/refactor.md` whose body silently sends
|
|
546
|
+
// something the user didn't intend. The banner is informed-consent;
|
|
547
|
+
// `/commands` shows the full bodies.
|
|
548
|
+
if (projectPath) {
|
|
549
|
+
try {
|
|
550
|
+
const { loadCustomCommands } = await import('../utils/customCommands.js');
|
|
551
|
+
const projectCustom = loadCustomCommands(projectPath).filter(c => c.scope === 'project');
|
|
552
|
+
if (projectCustom.length > 0) {
|
|
553
|
+
const list = projectCustom.slice(0, 6).map(c => `/${c.name}`).join(', ');
|
|
554
|
+
const more = projectCustom.length > 6 ? ` (+${projectCustom.length - 6} more)` : '';
|
|
555
|
+
welcomeLines.push('');
|
|
556
|
+
welcomeLines.push(` ⚠ This workspace defines ${projectCustom.length} custom slash command${projectCustom.length === 1 ? '' : 's'}: ${list}${more}`);
|
|
557
|
+
welcomeLines.push(' Type /commands to review before invoking');
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
// Loading must never block the welcome banner.
|
|
562
|
+
}
|
|
563
|
+
// Same flag for lifecycle hooks — they're arbitrary shell that fires on
|
|
564
|
+
// tool calls. A surprise post_edit / pre_tool_call from a freshly cloned
|
|
565
|
+
// repo is exactly the kind of thing a user should be told up front.
|
|
566
|
+
try {
|
|
567
|
+
const { summarizeHooks } = await import('../utils/hooks.js');
|
|
568
|
+
const summary = summarizeHooks(projectPath);
|
|
569
|
+
if (summary) {
|
|
570
|
+
welcomeLines.push('');
|
|
571
|
+
welcomeLines.push(` ⚠ ${summary} — shell hooks run automatically. Type /hooks to inspect.`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
// Don't block on hook discovery failure.
|
|
576
|
+
}
|
|
577
|
+
// Skill bundles — less dangerous than hooks but worth surfacing
|
|
578
|
+
// because the agent will invoke them autonomously.
|
|
579
|
+
try {
|
|
580
|
+
const { summarizeBundles } = await import('../utils/skillBundles.js');
|
|
581
|
+
const summary = summarizeBundles(projectPath);
|
|
582
|
+
if (summary) {
|
|
583
|
+
welcomeLines.push('');
|
|
584
|
+
welcomeLines.push(` ℹ This workspace ships ${summary}. Type /skills bundles to inspect.`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
// Don't block on skill discovery failure.
|
|
589
|
+
}
|
|
590
|
+
}
|
|
543
591
|
welcomeLines.push('');
|
|
544
592
|
welcomeLines.push(' /help · Ctrl+L clear · Esc cancel');
|
|
545
593
|
app.addMessage({ role: 'welcome', content: welcomeLines.join('\n') });
|
|
546
594
|
app.start();
|
|
595
|
+
// Spawn MCP servers in the background. They register against the fixed
|
|
596
|
+
// session id `codeep-tui` that runAgentTask passes into runAgent's
|
|
597
|
+
// `mcpSessionId` — so the agent picks up `.codeep/mcp_servers.json`
|
|
598
|
+
// entries (project + global) the same way an ACP client would.
|
|
599
|
+
if (projectPath) {
|
|
600
|
+
(async () => {
|
|
601
|
+
try {
|
|
602
|
+
const { loadMcpServerConfig } = await import('../utils/mcpConfig.js');
|
|
603
|
+
const { registerSessionServers } = await import('../utils/mcpRegistry.js');
|
|
604
|
+
const servers = loadMcpServerConfig(projectPath);
|
|
605
|
+
if (servers.length === 0)
|
|
606
|
+
return;
|
|
607
|
+
const { registered, errors } = await registerSessionServers('codeep-tui', servers, { workspaceRoot: projectPath });
|
|
608
|
+
if (registered.length > 0) {
|
|
609
|
+
app.notify(`MCP: ${registered.length} tool(s) from ${servers.length} server(s) ready. Type /mcp.`);
|
|
610
|
+
}
|
|
611
|
+
for (const e of errors) {
|
|
612
|
+
app.notifyWarn(`MCP server "${e.server}" failed: ${e.error}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
// Loading MCP must never block the TUI.
|
|
617
|
+
}
|
|
618
|
+
})();
|
|
619
|
+
}
|
|
547
620
|
// Check for updates in background — show notify if new version available
|
|
548
621
|
checkForUpdates().then(info => {
|
|
549
622
|
if (info.hasUpdate) {
|
package/dist/utils/agent.d.ts
CHANGED
|
@@ -30,6 +30,23 @@ export interface AgentOptions {
|
|
|
30
30
|
stderr: string;
|
|
31
31
|
exitCode: number;
|
|
32
32
|
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Optional filesystem callbacks. When the ACP client advertises `fs`
|
|
35
|
+
* capability, the server populates these so read_file/write_file/edit_file
|
|
36
|
+
* tools route through the client (preserving dirty buffers and undo
|
|
37
|
+
* history) instead of touching disk directly. Falls back to disk if not
|
|
38
|
+
* provided or if a delegated call throws.
|
|
39
|
+
*/
|
|
40
|
+
fs?: import('./toolExecution').FsCallbacks;
|
|
41
|
+
/**
|
|
42
|
+
* Optional ACP session id used to route MCP-prefixed tool calls
|
|
43
|
+
* (`<server>__<tool>`) to the per-session `mcpRegistry`. Not set in TUI
|
|
44
|
+
* mode (no MCP support there yet); set by `runAgentSession` in ACP mode.
|
|
45
|
+
* When set, the agent loop also fetches the session's MCP tool list and
|
|
46
|
+
* passes it into the provider's tool catalog so the model can invoke
|
|
47
|
+
* those tools natively.
|
|
48
|
+
*/
|
|
49
|
+
mcpSessionId?: string;
|
|
33
50
|
abortSignal?: AbortSignal;
|
|
34
51
|
dryRun?: boolean;
|
|
35
52
|
autoVerify?: 'off' | 'build' | 'typecheck' | 'test' | 'all' | boolean;
|