codeep 2.15.0 → 2.16.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 +12 -3
- package/dist/acp/session.js +22 -1
- package/dist/config/providers.js +20 -14
- package/dist/renderer/App.d.ts +77 -0
- package/dist/renderer/App.js +283 -3
- package/dist/renderer/commands/helpers.d.ts +188 -0
- package/dist/renderer/commands/helpers.js +342 -0
- package/dist/renderer/commands/registry.js +2 -1
- package/dist/renderer/commands.js +193 -264
- package/dist/renderer/components/Autocomplete.d.ts +25 -0
- package/dist/renderer/components/Autocomplete.js +35 -0
- package/dist/renderer/layout.d.ts +5 -1
- package/dist/renderer/layout.js +12 -0
- package/dist/renderer/main.js +34 -1
- package/dist/utils/diffPreview.d.ts +31 -0
- package/dist/utils/diffPreview.js +102 -0
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +111 -1
- package/dist/utils/mentions.d.ts +195 -0
- package/dist/utils/mentions.js +672 -0
- package/dist/utils/tokenTracker.js +9 -3
- package/dist/utils/webFetch.d.ts +101 -0
- package/dist/utils/webFetch.js +375 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -13,7 +13,8 @@ import { setProjectContext } from '../api/index.js';
|
|
|
13
13
|
import { runSkill, runCommandChain } from './agentExecution.js';
|
|
14
14
|
import { loadProjectIntelligence, saveProjectIntelligence } from '../utils/projectIntelligence.js';
|
|
15
15
|
import { ollamaModelHint } from './ollamaHint.js';
|
|
16
|
-
import { buildSearchSnippets, parseKeepRecent, joinSessionName, parseTaskAddArgs, formatTaskList } from './commands/helpers.js';
|
|
16
|
+
import { buildSearchSnippets, parseKeepRecent, joinSessionName, parseTaskAddArgs, formatTaskList, formatProfileList, formatMemoryList, formatStatsReport, extractCodeBlocks, resolveBlockIndex, extractFileChanges, formatApplyDiffLine, parsePromptArgs, formatMcpReloadReport, formatMcpResourcesList, formatMcpResourceRead, formatMcpPromptsList, formatMcpPromptResult, formatMcpServerList, parseInsightsDays, formatCloudSessionLabel, formatMeSyncReport, formatMeLearnResult, formatMeInitResult, formatSkillsShow, formatSkillsBrowseEmpty, formatSkillsPublishResult } from './commands/helpers.js';
|
|
17
|
+
import { resolveCommand } from './commands/registry.js';
|
|
17
18
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
18
19
|
/**
|
|
19
20
|
* Returns a hint for an Ollama model name based on parameter count.
|
|
@@ -21,13 +22,17 @@ import { buildSearchSnippets, parseKeepRecent, joinSessionName, parseTaskAddArgs
|
|
|
21
22
|
*/
|
|
22
23
|
// ─── Main dispatch ────────────────────────────────────────────────────────────
|
|
23
24
|
export async function handleCommand(command, args, ctx) {
|
|
25
|
+
// Resolve command aliases (e.g. `webcache` → `web-cache`) to their
|
|
26
|
+
// canonical name before dispatching.
|
|
27
|
+
const resolved = resolveCommand(command);
|
|
28
|
+
const canonical = resolved?.name ?? command;
|
|
24
29
|
// Handle skill chaining (e.g., /commit+push)
|
|
25
|
-
if (
|
|
26
|
-
const commands =
|
|
30
|
+
if (canonical.includes('+')) {
|
|
31
|
+
const commands = canonical.split('+').filter(c => c.trim());
|
|
27
32
|
runCommandChain(commands, 0, ctx);
|
|
28
33
|
return;
|
|
29
34
|
}
|
|
30
|
-
switch (
|
|
35
|
+
switch (canonical) {
|
|
31
36
|
case 'version': {
|
|
32
37
|
const version = getCurrentVersion();
|
|
33
38
|
const provider = getCurrentProvider();
|
|
@@ -308,7 +313,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
308
313
|
ctx.app.notify('Thinking effort: auto — each model uses its own default.');
|
|
309
314
|
}
|
|
310
315
|
else if (!supported) {
|
|
311
|
-
ctx.app.notify(`Thinking effort set to "${sub}", but ${model} has no graded thinking control — it will be ignored until you switch to a model that does (e.g. Opus
|
|
316
|
+
ctx.app.notify(`Thinking effort set to "${sub}", but ${model} has no graded thinking control — it will be ignored until you switch to a model that does (e.g. Opus 5, GPT-5.x, Gemini 3, DeepSeek V4, GLM-5.2).`);
|
|
312
317
|
}
|
|
313
318
|
else {
|
|
314
319
|
// Tell the user what THIS model will actually run (the tier may
|
|
@@ -424,21 +429,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
424
429
|
}
|
|
425
430
|
case 'insights': {
|
|
426
431
|
const { formatInsights } = await import('../utils/insights.js');
|
|
427
|
-
|
|
428
|
-
let days = 7;
|
|
429
|
-
for (let i = 0; i < args.length; i++) {
|
|
430
|
-
const a = args[i];
|
|
431
|
-
if (a === '--days' && args[i + 1]) {
|
|
432
|
-
const n = parseInt(args[i + 1], 10);
|
|
433
|
-
if (Number.isFinite(n))
|
|
434
|
-
days = n;
|
|
435
|
-
}
|
|
436
|
-
else if (a.startsWith('--days=')) {
|
|
437
|
-
const n = parseInt(a.slice('--days='.length), 10);
|
|
438
|
-
if (Number.isFinite(n))
|
|
439
|
-
days = n;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
432
|
+
const days = parseInsightsDays(args);
|
|
442
433
|
ctx.app.addMessage({ role: 'system', content: formatInsights({ days }) });
|
|
443
434
|
break;
|
|
444
435
|
}
|
|
@@ -513,12 +504,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
513
504
|
break;
|
|
514
505
|
}
|
|
515
506
|
const file = scope === 'global' ? '~/.codeep/profile.learned.md' : '.codeep/profile.learned.md';
|
|
516
|
-
ctx.app.addMessage({
|
|
517
|
-
role: 'system',
|
|
518
|
-
content: res.updated
|
|
519
|
-
? `Updated your ${scope} learned profile (\`${file}\`):\n\n${res.facts}\n\nClear it anytime with \`/me forget\`.`
|
|
520
|
-
: `No changes — your ${scope} learned profile already covers this:\n\n${res.facts}`,
|
|
521
|
-
});
|
|
507
|
+
ctx.app.addMessage({ role: 'system', content: formatMeLearnResult(scope, file, res) });
|
|
522
508
|
break;
|
|
523
509
|
}
|
|
524
510
|
if (sub === 'forget') {
|
|
@@ -537,14 +523,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
537
523
|
ctx.app.notify('Syncing your profile with codeep.dev…');
|
|
538
524
|
const pushed = await pushUserProfile();
|
|
539
525
|
const pulled = await pullUserProfile();
|
|
540
|
-
|
|
541
|
-
if (pushed)
|
|
542
|
-
lines.push('✓ Profile pushed to the dashboard');
|
|
543
|
-
if (pulled === 1)
|
|
544
|
-
lines.push('✓ Profile pulled to this machine');
|
|
545
|
-
if (lines.length === 0)
|
|
546
|
-
lines.push('Nothing to sync yet — run `/me init` and fill in your profile first.');
|
|
547
|
-
ctx.app.addMessage({ role: 'system', content: `## Profile sync\n\n${lines.join('\n')}` });
|
|
526
|
+
ctx.app.addMessage({ role: 'system', content: formatMeSyncReport(pushed, pulled) });
|
|
548
527
|
break;
|
|
549
528
|
}
|
|
550
529
|
if (sub === 'init') {
|
|
@@ -558,12 +537,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
558
537
|
ctx.app.notify('Could not create the profile file.');
|
|
559
538
|
break;
|
|
560
539
|
}
|
|
561
|
-
ctx.app.addMessage({
|
|
562
|
-
role: 'system',
|
|
563
|
-
content: res.created
|
|
564
|
-
? `Created ${scope} profile: \`${res.path}\`\n\nEdit it in your editor — Codeep uses it automatically. View anytime with \`/me\`.`
|
|
565
|
-
: `${scope === 'global' ? 'Global' : 'Project'} profile already exists: \`${res.path}\`\n\nEdit it directly, or view it with \`/me\`.`,
|
|
566
|
-
});
|
|
540
|
+
ctx.app.addMessage({ role: 'system', content: formatMeInitResult(scope, res) });
|
|
567
541
|
break;
|
|
568
542
|
}
|
|
569
543
|
// Default: show the profile view.
|
|
@@ -710,15 +684,8 @@ export async function handleCommand(command, args, ctx) {
|
|
|
710
684
|
// Non-null binding for the picker closure — TS can't carry the null
|
|
711
685
|
// narrowing of a reassigned `let` into the callback.
|
|
712
686
|
let sessionList = summaries;
|
|
713
|
-
// Render each row as: title · date · N msg · [project?]
|
|
714
|
-
// Mirrors the /sessions list layout so the picker feels familiar.
|
|
715
687
|
const SHOW_ALL = 'Show all cloud sessions…';
|
|
716
|
-
const labels = summaries.map(
|
|
717
|
-
const date = new Date(s.updatedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
718
|
-
const title = s.sessionName || s.sessionId.slice(0, 8);
|
|
719
|
-
const projectTag = s.projectName ? ` · ${s.projectName}` : '';
|
|
720
|
-
return `${title} · ${date} · ${s.messageCount} msg${projectTag}`;
|
|
721
|
-
});
|
|
688
|
+
const labels = summaries.map(formatCloudSessionLabel);
|
|
722
689
|
if (scopedToProject)
|
|
723
690
|
labels.push(SHOW_ALL);
|
|
724
691
|
// Named so the "Show all" branch can re-present the picker with the
|
|
@@ -1186,18 +1153,13 @@ Format: use headers per category, only include categories where you found issues
|
|
|
1186
1153
|
case 'copy': {
|
|
1187
1154
|
const blockNum = args[0] ? parseInt(args[0], 10) : -1;
|
|
1188
1155
|
const messages = ctx.app.getMessages();
|
|
1189
|
-
const codeBlocks =
|
|
1190
|
-
for (const msg of messages) {
|
|
1191
|
-
for (const match of msg.content.matchAll(/```[\w]*\n([\s\S]*?)```/g)) {
|
|
1192
|
-
codeBlocks.push(match[1]);
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1156
|
+
const codeBlocks = messages.flatMap(m => extractCodeBlocks(m.content));
|
|
1195
1157
|
if (codeBlocks.length === 0) {
|
|
1196
1158
|
ctx.app.notify('No code blocks found');
|
|
1197
1159
|
return;
|
|
1198
1160
|
}
|
|
1199
|
-
const index = blockNum
|
|
1200
|
-
if (
|
|
1161
|
+
const index = resolveBlockIndex(blockNum, codeBlocks.length);
|
|
1162
|
+
if (index === null) {
|
|
1201
1163
|
ctx.app.notify(`Invalid block number. Available: 1-${codeBlocks.length}`);
|
|
1202
1164
|
return;
|
|
1203
1165
|
}
|
|
@@ -1235,20 +1197,7 @@ Format: use headers per category, only include categories where you found issues
|
|
|
1235
1197
|
ctx.app.notify('No assistant response to apply');
|
|
1236
1198
|
return;
|
|
1237
1199
|
}
|
|
1238
|
-
const changes =
|
|
1239
|
-
const fenceFilePattern = /```\w*\s+([\w./\\-]+(?:\.\w+))\n([\s\S]*?)```/g;
|
|
1240
|
-
let match;
|
|
1241
|
-
while ((match = fenceFilePattern.exec(lastAssistant.content)) !== null) {
|
|
1242
|
-
const p = match[1].trim();
|
|
1243
|
-
if (p.includes('.') && !p.includes(' '))
|
|
1244
|
-
changes.push({ path: p, content: match[2] });
|
|
1245
|
-
}
|
|
1246
|
-
if (changes.length === 0) {
|
|
1247
|
-
const commentPattern = /```(\w+)?\s*\n(?:\/\/|#|--|\/\*)\s*(?:File|Path|file|path):\s*([^\n*]+)\n([\s\S]*?)```/g;
|
|
1248
|
-
while ((match = commentPattern.exec(lastAssistant.content)) !== null) {
|
|
1249
|
-
changes.push({ path: match[2].trim(), content: match[3] });
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1200
|
+
const changes = extractFileChanges(lastAssistant.content);
|
|
1252
1201
|
if (changes.length === 0) {
|
|
1253
1202
|
ctx.app.notify('No file changes found in response');
|
|
1254
1203
|
return;
|
|
@@ -1257,64 +1206,168 @@ Format: use headers per category, only include categories where you found issues
|
|
|
1257
1206
|
ctx.app.notify('Write access required. Use /grant first.');
|
|
1258
1207
|
return;
|
|
1259
1208
|
}
|
|
1209
|
+
// Parse optional selective hunk spec: /apply --only file.ts:0,1 other.ts:2
|
|
1210
|
+
// Without --only, all hunks are applied (existing behavior).
|
|
1211
|
+
const selective = args.includes('--only') || args.includes('-o');
|
|
1212
|
+
const interactive = args.includes('--interactive') || args.includes('-i');
|
|
1213
|
+
const hunkSpecs = new Map();
|
|
1214
|
+
if (selective) {
|
|
1215
|
+
for (const a of args) {
|
|
1216
|
+
if (a === '--only' || a === '-o')
|
|
1217
|
+
continue;
|
|
1218
|
+
const m = a.match(/^(.+):([\d,]+)$/);
|
|
1219
|
+
if (m) {
|
|
1220
|
+
const [, file, idxStr] = m;
|
|
1221
|
+
const idxs = new Set(idxStr.split(',').map((n) => parseInt(n, 10)).filter((n) => !isNaN(n)));
|
|
1222
|
+
hunkSpecs.set(file, idxs);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
// A spec that parsed to nothing must NOT fall through to the
|
|
1226
|
+
// apply-everything branch below — the user asked to restrict the
|
|
1227
|
+
// apply, so writing every change is the opposite of the request.
|
|
1228
|
+
if (hunkSpecs.size === 0) {
|
|
1229
|
+
ctx.app.notify('Invalid --only spec. Expected `--only <file>:<hunk>[,<hunk>]` (e.g. --only src/a.ts:0,2). Nothing applied.');
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1260
1233
|
import('fs').then(async (fs) => {
|
|
1261
1234
|
import('path').then(async (pathModule) => {
|
|
1235
|
+
const { createFileDiff, applyHunksToFiles, countChangeHunks } = await import('../utils/diffPreview.js');
|
|
1262
1236
|
const diffLines = [];
|
|
1237
|
+
const fileDiffs = [];
|
|
1263
1238
|
for (const change of changes) {
|
|
1264
1239
|
const fullPath = pathModule.isAbsolute(change.path)
|
|
1265
1240
|
? change.path
|
|
1266
1241
|
: pathModule.join(ctx.projectPath, change.path);
|
|
1267
|
-
const shortPath = change.path.length > 40 ? '...' + change.path.slice(-37) : change.path;
|
|
1268
1242
|
let existingContent = '';
|
|
1269
1243
|
try {
|
|
1270
1244
|
existingContent = await fs.promises.readFile(fullPath, 'utf-8');
|
|
1271
1245
|
}
|
|
1272
1246
|
catch { }
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1247
|
+
const fd = createFileDiff(change.path, change.content, ctx.projectPath);
|
|
1248
|
+
fileDiffs.push(fd);
|
|
1249
|
+
const hunkCount = countChangeHunks(fd);
|
|
1250
|
+
diffLines.push(...formatApplyDiffLine(change, existingContent));
|
|
1251
|
+
if (hunkCount > 0) {
|
|
1252
|
+
diffLines.push(` ↳ ${hunkCount} hunk(s) — use /apply --only ${change.path}:0,1 to select`);
|
|
1276
1253
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1254
|
+
}
|
|
1255
|
+
// Interactive mode: open the hunk picker (`git add -p` style).
|
|
1256
|
+
if (interactive) {
|
|
1257
|
+
const items = [];
|
|
1258
|
+
for (const fd of fileDiffs) {
|
|
1259
|
+
for (let hi = 0; hi < fd.hunks.length; hi++) {
|
|
1260
|
+
const hunk = fd.hunks[hi];
|
|
1261
|
+
// Skip pure-context hunks (no add/remove).
|
|
1262
|
+
if (!hunk.lines.some((l) => l.type === 'add' || l.type === 'remove'))
|
|
1263
|
+
continue;
|
|
1264
|
+
const header = `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
|
|
1265
|
+
const lines = hunk.lines.map((l) => {
|
|
1266
|
+
if (l.type === 'add')
|
|
1267
|
+
return `+${l.content}`;
|
|
1268
|
+
if (l.type === 'remove')
|
|
1269
|
+
return `-${l.content}`;
|
|
1270
|
+
return ` ${l.content}`;
|
|
1271
|
+
});
|
|
1272
|
+
items.push({ path: fd.path, hunkIndex: hi, header, lines });
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (items.length === 0) {
|
|
1276
|
+
ctx.app.notify('No change hunks to review');
|
|
1277
|
+
return;
|
|
1283
1278
|
}
|
|
1279
|
+
ctx.app.showHunkPicker({
|
|
1280
|
+
title: '🎯 Review hunks',
|
|
1281
|
+
items,
|
|
1282
|
+
onComplete: (accepted) => {
|
|
1283
|
+
if (accepted.length === 0) {
|
|
1284
|
+
ctx.app.notify('No hunks applied');
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
(async () => {
|
|
1288
|
+
// Group accepted hunk indices by file path.
|
|
1289
|
+
const byPath = new Map();
|
|
1290
|
+
for (const a of accepted) {
|
|
1291
|
+
let set = byPath.get(a.path);
|
|
1292
|
+
if (!set) {
|
|
1293
|
+
set = new Set();
|
|
1294
|
+
byPath.set(a.path, set);
|
|
1295
|
+
}
|
|
1296
|
+
set.add(a.hunkIndex);
|
|
1297
|
+
}
|
|
1298
|
+
const results = applyHunksToFiles(fileDiffs, byPath);
|
|
1299
|
+
let applied = 0;
|
|
1300
|
+
for (const r of results) {
|
|
1301
|
+
try {
|
|
1302
|
+
const fullPath = pathModule.isAbsolute(r.path)
|
|
1303
|
+
? r.path
|
|
1304
|
+
: pathModule.join(ctx.projectPath, r.path);
|
|
1305
|
+
await fs.promises.mkdir(pathModule.dirname(fullPath), { recursive: true });
|
|
1306
|
+
await fs.promises.writeFile(fullPath, r.content);
|
|
1307
|
+
applied++;
|
|
1308
|
+
}
|
|
1309
|
+
catch { }
|
|
1310
|
+
}
|
|
1311
|
+
ctx.app.notify(`Applied ${accepted.length} hunk(s) across ${applied} file(s)`);
|
|
1312
|
+
})().catch((e) => ctx.app.notify(`Apply failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
1313
|
+
},
|
|
1314
|
+
});
|
|
1315
|
+
return;
|
|
1284
1316
|
}
|
|
1317
|
+
const summary = selective && hunkSpecs.size > 0
|
|
1318
|
+
? `Selective apply (${hunkSpecs.size} file(s) with chosen hunks)`
|
|
1319
|
+
: `Found ${changes.length} file(s) to apply`;
|
|
1285
1320
|
ctx.app.showConfirm({
|
|
1286
1321
|
title: '📝 Apply Changes',
|
|
1287
1322
|
message: [
|
|
1288
|
-
|
|
1323
|
+
summary,
|
|
1289
1324
|
'',
|
|
1290
|
-
...diffLines.slice(0,
|
|
1291
|
-
...(diffLines.length >
|
|
1325
|
+
...diffLines.slice(0, 12),
|
|
1326
|
+
...(diffLines.length > 12 ? [` ...and ${diffLines.length - 12} more`] : []),
|
|
1292
1327
|
'',
|
|
1293
|
-
'Apply these changes?',
|
|
1328
|
+
selective && hunkSpecs.size > 0 ? 'Apply selected hunks?' : 'Apply these changes?',
|
|
1294
1329
|
],
|
|
1295
1330
|
confirmLabel: 'Apply',
|
|
1296
1331
|
cancelLabel: 'Cancel',
|
|
1297
1332
|
onConfirm: () => {
|
|
1298
1333
|
(async () => {
|
|
1299
1334
|
let applied = 0;
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1335
|
+
if (selective && hunkSpecs.size > 0) {
|
|
1336
|
+
// Per-hunk selective apply.
|
|
1337
|
+
const results = applyHunksToFiles(fileDiffs, hunkSpecs);
|
|
1338
|
+
for (const r of results) {
|
|
1339
|
+
try {
|
|
1340
|
+
const fullPath = pathModule.isAbsolute(r.path)
|
|
1341
|
+
? r.path
|
|
1342
|
+
: pathModule.join(ctx.projectPath, r.path);
|
|
1343
|
+
await fs.promises.mkdir(pathModule.dirname(fullPath), { recursive: true });
|
|
1344
|
+
await fs.promises.writeFile(fullPath, r.content);
|
|
1345
|
+
applied++;
|
|
1346
|
+
}
|
|
1347
|
+
catch { }
|
|
1308
1348
|
}
|
|
1309
|
-
catch { }
|
|
1310
1349
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1350
|
+
else {
|
|
1351
|
+
// All-or-nothing apply (original behavior).
|
|
1352
|
+
for (const change of changes) {
|
|
1353
|
+
try {
|
|
1354
|
+
const fullPath = pathModule.isAbsolute(change.path)
|
|
1355
|
+
? change.path
|
|
1356
|
+
: pathModule.join(ctx.projectPath, change.path);
|
|
1357
|
+
await fs.promises.mkdir(pathModule.dirname(fullPath), { recursive: true });
|
|
1358
|
+
await fs.promises.writeFile(fullPath, change.content);
|
|
1359
|
+
applied++;
|
|
1360
|
+
}
|
|
1361
|
+
catch { }
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
ctx.app.notify(`Applied ${applied}/${selective ? hunkSpecs.size : changes.length} file(s)`);
|
|
1365
|
+
})().catch((e) => ctx.app.notify(`Apply failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
1313
1366
|
},
|
|
1314
1367
|
onCancel: () => ctx.app.notify('Apply cancelled'),
|
|
1315
1368
|
});
|
|
1316
1369
|
});
|
|
1317
|
-
});
|
|
1370
|
+
}).catch((e) => ctx.app.notify(`Apply failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
1318
1371
|
break;
|
|
1319
1372
|
}
|
|
1320
1373
|
case 'add': {
|
|
@@ -1769,10 +1822,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1769
1822
|
ctx.app.notify(`Skill ${name} not found`);
|
|
1770
1823
|
break;
|
|
1771
1824
|
}
|
|
1772
|
-
ctx.app.addMessage({
|
|
1773
|
-
role: 'system',
|
|
1774
|
-
content: `# ${bundle.name}\n_${bundle.description}_\n\n**Source:** ${bundle.source}\n\n---\n\n${bundle.body}`,
|
|
1775
|
-
});
|
|
1825
|
+
ctx.app.addMessage({ role: 'system', content: formatSkillsShow(bundle) });
|
|
1776
1826
|
break;
|
|
1777
1827
|
}
|
|
1778
1828
|
// Marketplace operations against codeep.dev.
|
|
@@ -1790,10 +1840,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1790
1840
|
ctx.app.notify(`Publish failed: ${result.error}`);
|
|
1791
1841
|
break;
|
|
1792
1842
|
}
|
|
1793
|
-
ctx.app.addMessage({
|
|
1794
|
-
role: 'system',
|
|
1795
|
-
content: `Published \`${slug}\` (${isPublic ? 'public' : 'private'}) to codeep.dev. Install elsewhere with \`/skills install ${result.skill?.owner_username ?? '<you>'}/${slug}\`.`,
|
|
1796
|
-
});
|
|
1843
|
+
ctx.app.addMessage({ role: 'system', content: formatSkillsPublishResult(slug, isPublic, result.skill?.owner_username) });
|
|
1797
1844
|
break;
|
|
1798
1845
|
}
|
|
1799
1846
|
if (sub === 'install') {
|
|
@@ -1823,7 +1870,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1823
1870
|
}
|
|
1824
1871
|
const skills = result.skills ?? [];
|
|
1825
1872
|
if (skills.length === 0) {
|
|
1826
|
-
ctx.app.addMessage({ role: 'system', content: query
|
|
1873
|
+
ctx.app.addMessage({ role: 'system', content: formatSkillsBrowseEmpty(query) });
|
|
1827
1874
|
break;
|
|
1828
1875
|
}
|
|
1829
1876
|
const lines = [`# ${query ? `Skills matching "${query}"` : 'Public skills'}`, ''];
|
|
@@ -2074,7 +2121,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2074
2121
|
ctx.app.notify('No profiles saved. Use /profile save <name>');
|
|
2075
2122
|
}
|
|
2076
2123
|
else {
|
|
2077
|
-
ctx.app.addMessage({ role: 'system', content:
|
|
2124
|
+
ctx.app.addMessage({ role: 'system', content: formatProfileList(profiles) });
|
|
2078
2125
|
}
|
|
2079
2126
|
break;
|
|
2080
2127
|
}
|
|
@@ -2215,54 +2262,15 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2215
2262
|
case 'stats': {
|
|
2216
2263
|
const { getCostBreakdown, getSessionStats, formatTokenCount, getPricingTable, getCacheStats } = await import('../utils/tokenTracker.js');
|
|
2217
2264
|
const stats = getSessionStats();
|
|
2218
|
-
const
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
if (breakdown.length > 0) {
|
|
2228
|
-
lines.push('');
|
|
2229
|
-
lines.push('### By model');
|
|
2230
|
-
for (const b of breakdown) {
|
|
2231
|
-
const isFree = b.provider === 'ollama';
|
|
2232
|
-
const costStr = isFree ? 'free' : b.estimatedCost > 0 ? `~$${b.estimatedCost.toFixed(4)}` : '(no pricing data)';
|
|
2233
|
-
lines.push(`- **${b.model}** (${b.provider}): ${formatTokenCount(b.promptTokens)} in / ${formatTokenCount(b.completionTokens)} out — ${costStr}`);
|
|
2234
|
-
}
|
|
2235
|
-
lines.push('');
|
|
2236
|
-
const currentProvider = config.get('provider');
|
|
2237
|
-
if (currentProvider === 'ollama') {
|
|
2238
|
-
lines.push(`**Total: free · ${formatTokenCount(stats.totalTokens)} tokens**`);
|
|
2239
|
-
}
|
|
2240
|
-
else if (stats.estimatedCost > 0) {
|
|
2241
|
-
lines.push(`**Total: ~$${stats.estimatedCost.toFixed(4)}**`);
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
// Prompt caching — parity with /cost (the 2.0.2 caching section was
|
|
2245
|
-
// only wired into formatCostReport). Shown only when caching landed.
|
|
2246
|
-
const cache = getCacheStats();
|
|
2247
|
-
if (cache.cacheReadTokens > 0 || cache.cacheCreationTokens > 0) {
|
|
2248
|
-
lines.push('', '### Prompt caching');
|
|
2249
|
-
lines.push(`Cache reads: ${formatTokenCount(cache.cacheReadTokens)} tokens (billed at 0.1× input rate)`);
|
|
2250
|
-
if (cache.cacheCreationTokens > 0) {
|
|
2251
|
-
lines.push(`Cache writes: ${formatTokenCount(cache.cacheCreationTokens)} tokens (billed at 1.25× input rate)`);
|
|
2252
|
-
}
|
|
2253
|
-
if (cache.estimatedSavingsUsd > 0) {
|
|
2254
|
-
lines.push(`Estimated savings vs no caching: $${cache.estimatedSavingsUsd.toFixed(4)}`);
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
lines.push('');
|
|
2258
|
-
}
|
|
2259
|
-
lines.push('### Pricing (per 1M tokens)');
|
|
2260
|
-
lines.push('| Model | Input | Output |');
|
|
2261
|
-
lines.push('|---|---|---|');
|
|
2262
|
-
for (const p of getPricingTable()) {
|
|
2263
|
-
lines.push(`| ${p.model} | $${p.inputPer1M.toFixed(3)} | $${p.outputPer1M.toFixed(3)} |`);
|
|
2264
|
-
}
|
|
2265
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
|
|
2265
|
+
const content = formatStatsReport({
|
|
2266
|
+
totals: stats,
|
|
2267
|
+
breakdown: getCostBreakdown(),
|
|
2268
|
+
cache: getCacheStats(),
|
|
2269
|
+
pricing: getPricingTable(),
|
|
2270
|
+
currentProvider: config.get('provider'),
|
|
2271
|
+
fmt: formatTokenCount,
|
|
2272
|
+
});
|
|
2273
|
+
ctx.app.addMessage({ role: 'system', content });
|
|
2266
2274
|
break;
|
|
2267
2275
|
}
|
|
2268
2276
|
case 'memory': {
|
|
@@ -2280,8 +2288,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2280
2288
|
ctx.app.notify('No memory notes. Add one with: /memory <note>');
|
|
2281
2289
|
}
|
|
2282
2290
|
else {
|
|
2283
|
-
|
|
2284
|
-
ctx.app.addMessage({ role: 'assistant', content: `**Project memory notes:**\n${lines}` });
|
|
2291
|
+
ctx.app.addMessage({ role: 'assistant', content: formatMemoryList(intelligence.notes) });
|
|
2285
2292
|
}
|
|
2286
2293
|
break;
|
|
2287
2294
|
}
|
|
@@ -2322,6 +2329,29 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2322
2329
|
ctx.app.addMessage({ role: 'system', content: formatCommandList(loadCustomCommands(ctx.projectPath)) });
|
|
2323
2330
|
break;
|
|
2324
2331
|
}
|
|
2332
|
+
case 'web-cache': {
|
|
2333
|
+
const sub = args[0]?.toLowerCase();
|
|
2334
|
+
const { clearWebCache, webCacheStats } = await import('../utils/webFetch.js');
|
|
2335
|
+
if (sub === 'clear' || sub === 'reset' || sub === 'flush') {
|
|
2336
|
+
clearWebCache();
|
|
2337
|
+
ctx.app.notify('Web cache cleared');
|
|
2338
|
+
}
|
|
2339
|
+
else {
|
|
2340
|
+
const stats = webCacheStats();
|
|
2341
|
+
ctx.app.addMessage({
|
|
2342
|
+
role: 'system',
|
|
2343
|
+
content: [
|
|
2344
|
+
'🌐 Web fetch cache',
|
|
2345
|
+
'',
|
|
2346
|
+
` Entries: ${stats.entries}/${stats.maxEntries}`,
|
|
2347
|
+
` TTL: ${stats.ttlMinutes} min`,
|
|
2348
|
+
'',
|
|
2349
|
+
'Usage: /web-cache clear',
|
|
2350
|
+
].join('\n'),
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
break;
|
|
2354
|
+
}
|
|
2325
2355
|
case 'mcp': {
|
|
2326
2356
|
// Mirrors the ACP `/mcp` handler in src/acp/commands.ts. In TUI the
|
|
2327
2357
|
// session id is the constant `codeep-tui` (the same one main.ts uses
|
|
@@ -2480,35 +2510,14 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2480
2510
|
ctx.app.notify('Reloading MCP server config…');
|
|
2481
2511
|
const merged = loadMcpServerConfig(projectPath);
|
|
2482
2512
|
const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
|
|
2483
|
-
|
|
2484
|
-
if (errors.length > 0) {
|
|
2485
|
-
lines.push('', '### Failed servers');
|
|
2486
|
-
for (const e of errors)
|
|
2487
|
-
lines.push(`- **${e.server}** — \`${e.error}\``);
|
|
2488
|
-
}
|
|
2489
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
|
|
2513
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpReloadReport(registered.length, merged.length, errors) });
|
|
2490
2514
|
break;
|
|
2491
2515
|
}
|
|
2492
2516
|
if (sub === 'resources') {
|
|
2493
2517
|
const { getSessionResources, awaitSessionReady } = await import('../utils/mcpRegistry.js');
|
|
2494
2518
|
await awaitSessionReady(TUI_SESSION);
|
|
2495
2519
|
const groups = await getSessionResources(TUI_SESSION);
|
|
2496
|
-
|
|
2497
|
-
ctx.app.addMessage({ role: 'system', content: '_No MCP server in this session exposes resources._' });
|
|
2498
|
-
break;
|
|
2499
|
-
}
|
|
2500
|
-
const lines = ['## MCP resources', ''];
|
|
2501
|
-
for (const g of groups) {
|
|
2502
|
-
lines.push(`**${g.serverName}** — ${g.resources.length} resource${g.resources.length === 1 ? '' : 's'}`);
|
|
2503
|
-
for (const r of g.resources) {
|
|
2504
|
-
const label = r.name ? `${r.name} — ` : '';
|
|
2505
|
-
const mime = r.mimeType ? ` (${r.mimeType})` : '';
|
|
2506
|
-
lines.push(`- ${label}\`${r.uri}\`${mime}${r.description ? ` — ${r.description}` : ''}`);
|
|
2507
|
-
}
|
|
2508
|
-
lines.push('');
|
|
2509
|
-
}
|
|
2510
|
-
lines.push('Read one with `/mcp read <uri>`.');
|
|
2511
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
|
|
2520
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpResourcesList(groups) });
|
|
2512
2521
|
break;
|
|
2513
2522
|
}
|
|
2514
2523
|
if (sub === 'read') {
|
|
@@ -2520,23 +2529,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2520
2529
|
const { readSessionResource } = await import('../utils/mcpRegistry.js');
|
|
2521
2530
|
try {
|
|
2522
2531
|
const contents = await readSessionResource(TUI_SESSION, uri);
|
|
2523
|
-
|
|
2524
|
-
ctx.app.addMessage({ role: 'system', content: `_No content returned for \`${uri}\`._` });
|
|
2525
|
-
break;
|
|
2526
|
-
}
|
|
2527
|
-
const lines = [`## Resource: \`${uri}\``, ''];
|
|
2528
|
-
for (const c of contents) {
|
|
2529
|
-
if (c.text !== undefined) {
|
|
2530
|
-
const fence = c.mimeType?.includes('json') ? 'json' : c.mimeType?.includes('markdown') ? 'markdown' : '';
|
|
2531
|
-
lines.push('```' + fence);
|
|
2532
|
-
lines.push(c.text);
|
|
2533
|
-
lines.push('```');
|
|
2534
|
-
}
|
|
2535
|
-
else if (c.blob) {
|
|
2536
|
-
lines.push(`_(${c.mimeType ?? 'binary'} blob, ${c.blob.length} base64 chars — not rendered)_`);
|
|
2537
|
-
}
|
|
2538
|
-
}
|
|
2539
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
|
|
2532
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpResourceRead(uri, contents) });
|
|
2540
2533
|
}
|
|
2541
2534
|
catch (err) {
|
|
2542
2535
|
ctx.app.addMessage({ role: 'system', content: `Failed to read \`${uri}\`: ${err.message}` });
|
|
@@ -2547,23 +2540,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2547
2540
|
const { getSessionPrompts, awaitSessionReady } = await import('../utils/mcpRegistry.js');
|
|
2548
2541
|
await awaitSessionReady(TUI_SESSION);
|
|
2549
2542
|
const groups = await getSessionPrompts(TUI_SESSION);
|
|
2550
|
-
|
|
2551
|
-
ctx.app.addMessage({ role: 'system', content: '_No MCP server in this session exposes prompt templates._' });
|
|
2552
|
-
break;
|
|
2553
|
-
}
|
|
2554
|
-
const lines = ['## MCP prompt templates', ''];
|
|
2555
|
-
for (const g of groups) {
|
|
2556
|
-
lines.push(`**${g.serverName}** — ${g.prompts.length} prompt${g.prompts.length === 1 ? '' : 's'}`);
|
|
2557
|
-
for (const p of g.prompts) {
|
|
2558
|
-
const argList = p.arguments?.length
|
|
2559
|
-
? ` (${p.arguments.map(a => a.required ? a.name : `[${a.name}]`).join(', ')})`
|
|
2560
|
-
: '';
|
|
2561
|
-
lines.push(`- \`${p.name}\`${argList}${p.description ? ` — ${p.description}` : ''}`);
|
|
2562
|
-
}
|
|
2563
|
-
lines.push('');
|
|
2564
|
-
}
|
|
2565
|
-
lines.push('Materialise one with `/mcp prompt <server> <name> [key=value...]`.');
|
|
2566
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
|
|
2543
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpPromptsList(groups) });
|
|
2567
2544
|
break;
|
|
2568
2545
|
}
|
|
2569
2546
|
if (sub === 'prompt') {
|
|
@@ -2573,25 +2550,11 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2573
2550
|
ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp prompt <server> <name> [key=value ...]`' });
|
|
2574
2551
|
break;
|
|
2575
2552
|
}
|
|
2576
|
-
const promptArgs =
|
|
2577
|
-
for (const tok of args.slice(3)) {
|
|
2578
|
-
const eq = tok.indexOf('=');
|
|
2579
|
-
if (eq > 0)
|
|
2580
|
-
promptArgs[tok.slice(0, eq)] = tok.slice(eq + 1);
|
|
2581
|
-
}
|
|
2553
|
+
const promptArgs = parsePromptArgs(args.slice(3));
|
|
2582
2554
|
const { getSessionPrompt } = await import('../utils/mcpRegistry.js');
|
|
2583
2555
|
try {
|
|
2584
2556
|
const { description, messages } = await getSessionPrompt(TUI_SESSION, serverName, name, promptArgs);
|
|
2585
|
-
|
|
2586
|
-
if (description)
|
|
2587
|
-
lines.push(`_${description}_`);
|
|
2588
|
-
lines.push('');
|
|
2589
|
-
for (const m of messages) {
|
|
2590
|
-
const text = typeof m.content?.text === 'string' ? m.content.text : JSON.stringify(m.content);
|
|
2591
|
-
lines.push(`**${m.role}:** ${text}`);
|
|
2592
|
-
lines.push('');
|
|
2593
|
-
}
|
|
2594
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
|
|
2557
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpPromptResult(serverName, name, description, messages) });
|
|
2595
2558
|
}
|
|
2596
2559
|
catch (err) {
|
|
2597
2560
|
ctx.app.addMessage({ role: 'system', content: `Failed to materialise prompt: ${err.message}` });
|
|
@@ -2603,41 +2566,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2603
2566
|
await awaitSessionReady(TUI_SESSION);
|
|
2604
2567
|
const tools = await getSessionTools(TUI_SESSION);
|
|
2605
2568
|
const mcpErrors = getSessionRegistrationErrors(TUI_SESSION);
|
|
2606
|
-
|
|
2607
|
-
ctx.app.addMessage({
|
|
2608
|
-
role: 'system',
|
|
2609
|
-
content: [
|
|
2610
|
-
'_No MCP servers connected to this session._',
|
|
2611
|
-
'',
|
|
2612
|
-
'Add one with `/mcp add <name> <command> [args...]` — it persists to `.codeep/mcp_servers.json`.',
|
|
2613
|
-
'Or browse the marketplace with `/mcp browse` and install with `/mcp install <id>`.',
|
|
2614
|
-
].join('\n'),
|
|
2615
|
-
});
|
|
2616
|
-
break;
|
|
2617
|
-
}
|
|
2618
|
-
const lines = ['## MCP servers', ''];
|
|
2619
|
-
if (tools.length > 0) {
|
|
2620
|
-
const byServer = new Map();
|
|
2621
|
-
for (const t of tools) {
|
|
2622
|
-
if (!byServer.has(t.serverName))
|
|
2623
|
-
byServer.set(t.serverName, []);
|
|
2624
|
-
byServer.get(t.serverName).push(t);
|
|
2625
|
-
}
|
|
2626
|
-
for (const [serverName, serverTools] of byServer) {
|
|
2627
|
-
lines.push(`**${serverName}** — ${serverTools.length} tool${serverTools.length === 1 ? '' : 's'}`);
|
|
2628
|
-
for (const t of serverTools) {
|
|
2629
|
-
const desc = t.description ? ` — ${t.description}` : '';
|
|
2630
|
-
lines.push(`- \`${t.agentName}\`${desc}`);
|
|
2631
|
-
}
|
|
2632
|
-
lines.push('');
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
if (mcpErrors.length > 0) {
|
|
2636
|
-
lines.push('### Failed servers');
|
|
2637
|
-
for (const e of mcpErrors)
|
|
2638
|
-
lines.push(`- **${e.server}** — \`${e.error}\``);
|
|
2639
|
-
}
|
|
2640
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
|
|
2569
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpServerList(tools, mcpErrors) });
|
|
2641
2570
|
break;
|
|
2642
2571
|
}
|
|
2643
2572
|
default: {
|