codeep 2.15.0 → 2.17.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 +41 -7
- package/dist/acp/serverHandlers.js +1 -1
- package/dist/acp/session.js +22 -1
- package/dist/config/index.js +20 -4
- package/dist/config/providers.d.ts +3 -2
- package/dist/config/providers.js +163 -69
- package/dist/renderer/App.d.ts +89 -0
- package/dist/renderer/App.js +637 -43
- package/dist/renderer/Screen.d.ts +1 -0
- package/dist/renderer/Screen.js +8 -3
- package/dist/renderer/commands/helpers.d.ts +189 -0
- package/dist/renderer/commands/helpers.js +345 -0
- package/dist/renderer/commands/registry.js +2 -1
- package/dist/renderer/commands.js +218 -267
- package/dist/renderer/components/AgentTimeline.d.ts +44 -0
- package/dist/renderer/components/AgentTimeline.js +157 -0
- package/dist/renderer/components/Autocomplete.d.ts +25 -0
- package/dist/renderer/components/Autocomplete.js +35 -0
- package/dist/renderer/components/Status.d.ts +2 -0
- package/dist/renderer/layout.d.ts +5 -1
- package/dist/renderer/layout.js +12 -0
- package/dist/renderer/main.js +110 -30
- package/dist/utils/agent.js +1 -1
- package/dist/utils/agents.d.ts +1 -1
- package/dist/utils/agents.js +1 -1
- package/dist/utils/checkpoints.d.ts +1 -1
- package/dist/utils/checkpoints.js +1 -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/resourceImpact.d.ts +25 -0
- package/dist/utils/resourceImpact.js +54 -0
- package/dist/utils/tokenTracker.js +52 -37
- 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 +2 -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();
|
|
@@ -161,6 +166,26 @@ export async function handleCommand(command, args, ctx) {
|
|
|
161
166
|
});
|
|
162
167
|
break;
|
|
163
168
|
}
|
|
169
|
+
if (providerId === 'modelscope') {
|
|
170
|
+
ctx.app.notify('Fetching ModelScope catalog…');
|
|
171
|
+
const { fetchOpenAiCompatibleModels, getApiKey: _getKey } = await import('../config/index.js');
|
|
172
|
+
const base = 'https://api-inference.modelscope.cn/v1';
|
|
173
|
+
const models = await fetchOpenAiCompatibleModels(base, _getKey('modelscope') || undefined);
|
|
174
|
+
const fallback = getModelsForCurrentProvider();
|
|
175
|
+
const available = models && models.length > 0
|
|
176
|
+
? models
|
|
177
|
+
: Object.keys(fallback).map(id => ({ id, name: id, description: 'Built-in fallback' }));
|
|
178
|
+
if (!models || models.length === 0) {
|
|
179
|
+
ctx.app.notify('Could not fetch the ModelScope catalog. Using the built-in fallback model.');
|
|
180
|
+
}
|
|
181
|
+
const modelItems = available.map(m => ({ key: m.id, label: m.name, description: m.description }));
|
|
182
|
+
const currentModel = config.get('model');
|
|
183
|
+
ctx.app.showSelect(`Select ModelScope Model (${available.length})`, modelItems, currentModel, (item) => {
|
|
184
|
+
config.set('model', item.key);
|
|
185
|
+
ctx.app.notify(`Model: ${item.key}`);
|
|
186
|
+
});
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
164
189
|
if (providerId === 'custom') {
|
|
165
190
|
const base = config.get('customBaseUrl') || 'http://localhost:8000/v1';
|
|
166
191
|
ctx.app.notify(`Fetching models from ${base}…`);
|
|
@@ -299,7 +324,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
299
324
|
const providerId = config.get('provider');
|
|
300
325
|
const model = config.get('model');
|
|
301
326
|
const supported = modelSupportsReasoningEffort(providerId, model);
|
|
302
|
-
// Tiers THIS model actually distinguishes (e.g.
|
|
327
|
+
// Tiers THIS model actually distinguishes (e.g. Kimi K3 → auto/low/high/max).
|
|
303
328
|
const available = availableReasoningTiers(providerId, model);
|
|
304
329
|
const sub = args[0]?.toLowerCase();
|
|
305
330
|
if (sub && REASONING_TIERS.includes(sub)) {
|
|
@@ -308,11 +333,11 @@ export async function handleCommand(command, args, ctx) {
|
|
|
308
333
|
ctx.app.notify('Thinking effort: auto — each model uses its own default.');
|
|
309
334
|
}
|
|
310
335
|
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
|
|
336
|
+
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, Kimi K3).`);
|
|
312
337
|
}
|
|
313
338
|
else {
|
|
314
339
|
// Tell the user what THIS model will actually run (the tier may
|
|
315
|
-
// collapse onto a level the model distinguishes, e.g.
|
|
340
|
+
// collapse onto a level the model distinguishes, e.g. medium→high on Kimi K3).
|
|
316
341
|
const resolved = resolveReasoningTier(providerId, model, sub);
|
|
317
342
|
const note = resolved === sub ? '' : ` (${model} runs this as "${resolved}")`;
|
|
318
343
|
ctx.app.notify(`Thinking effort: ${sub}${note} — sending ${JSON.stringify(reasoningParamsFor(providerId, model, sub))}.`);
|
|
@@ -341,7 +366,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
341
366
|
if (supported)
|
|
342
367
|
tLines.push(`**Available** ${available.join(' · ')}`);
|
|
343
368
|
tLines.push('');
|
|
344
|
-
tLines.push('Sets how hard the model reasons. Each model offers only the levels it distinguishes (
|
|
369
|
+
tLines.push('Sets how hard the model reasons. Each model offers only the levels it distinguishes (DeepSeek → high · max; Kimi K3 → low · high · max; Gemini → low · high; Opus/Sonnet & GPT-5.x → the full set). The setting is global and clamps to the active model, so it never sends a value the API rejects. `/effort` is an alias.');
|
|
345
370
|
ctx.app.addMessage({ role: 'system', content: tLines.join('\n') });
|
|
346
371
|
break;
|
|
347
372
|
}
|
|
@@ -424,21 +449,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
424
449
|
}
|
|
425
450
|
case 'insights': {
|
|
426
451
|
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
|
-
}
|
|
452
|
+
const days = parseInsightsDays(args);
|
|
442
453
|
ctx.app.addMessage({ role: 'system', content: formatInsights({ days }) });
|
|
443
454
|
break;
|
|
444
455
|
}
|
|
@@ -513,12 +524,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
513
524
|
break;
|
|
514
525
|
}
|
|
515
526
|
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
|
-
});
|
|
527
|
+
ctx.app.addMessage({ role: 'system', content: formatMeLearnResult(scope, file, res) });
|
|
522
528
|
break;
|
|
523
529
|
}
|
|
524
530
|
if (sub === 'forget') {
|
|
@@ -537,14 +543,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
537
543
|
ctx.app.notify('Syncing your profile with codeep.dev…');
|
|
538
544
|
const pushed = await pushUserProfile();
|
|
539
545
|
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')}` });
|
|
546
|
+
ctx.app.addMessage({ role: 'system', content: formatMeSyncReport(pushed, pulled) });
|
|
548
547
|
break;
|
|
549
548
|
}
|
|
550
549
|
if (sub === 'init') {
|
|
@@ -558,12 +557,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
558
557
|
ctx.app.notify('Could not create the profile file.');
|
|
559
558
|
break;
|
|
560
559
|
}
|
|
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
|
-
});
|
|
560
|
+
ctx.app.addMessage({ role: 'system', content: formatMeInitResult(scope, res) });
|
|
567
561
|
break;
|
|
568
562
|
}
|
|
569
563
|
// Default: show the profile view.
|
|
@@ -710,15 +704,8 @@ export async function handleCommand(command, args, ctx) {
|
|
|
710
704
|
// Non-null binding for the picker closure — TS can't carry the null
|
|
711
705
|
// narrowing of a reassigned `let` into the callback.
|
|
712
706
|
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
707
|
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
|
-
});
|
|
708
|
+
const labels = summaries.map(formatCloudSessionLabel);
|
|
722
709
|
if (scopedToProject)
|
|
723
710
|
labels.push(SHOW_ALL);
|
|
724
711
|
// Named so the "Show all" branch can re-present the picker with the
|
|
@@ -1186,18 +1173,13 @@ Format: use headers per category, only include categories where you found issues
|
|
|
1186
1173
|
case 'copy': {
|
|
1187
1174
|
const blockNum = args[0] ? parseInt(args[0], 10) : -1;
|
|
1188
1175
|
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
|
-
}
|
|
1176
|
+
const codeBlocks = messages.flatMap(m => extractCodeBlocks(m.content));
|
|
1195
1177
|
if (codeBlocks.length === 0) {
|
|
1196
1178
|
ctx.app.notify('No code blocks found');
|
|
1197
1179
|
return;
|
|
1198
1180
|
}
|
|
1199
|
-
const index = blockNum
|
|
1200
|
-
if (
|
|
1181
|
+
const index = resolveBlockIndex(blockNum, codeBlocks.length);
|
|
1182
|
+
if (index === null) {
|
|
1201
1183
|
ctx.app.notify(`Invalid block number. Available: 1-${codeBlocks.length}`);
|
|
1202
1184
|
return;
|
|
1203
1185
|
}
|
|
@@ -1235,20 +1217,7 @@ Format: use headers per category, only include categories where you found issues
|
|
|
1235
1217
|
ctx.app.notify('No assistant response to apply');
|
|
1236
1218
|
return;
|
|
1237
1219
|
}
|
|
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
|
-
}
|
|
1220
|
+
const changes = extractFileChanges(lastAssistant.content);
|
|
1252
1221
|
if (changes.length === 0) {
|
|
1253
1222
|
ctx.app.notify('No file changes found in response');
|
|
1254
1223
|
return;
|
|
@@ -1257,64 +1226,168 @@ Format: use headers per category, only include categories where you found issues
|
|
|
1257
1226
|
ctx.app.notify('Write access required. Use /grant first.');
|
|
1258
1227
|
return;
|
|
1259
1228
|
}
|
|
1229
|
+
// Parse optional selective hunk spec: /apply --only file.ts:0,1 other.ts:2
|
|
1230
|
+
// Without --only, all hunks are applied (existing behavior).
|
|
1231
|
+
const selective = args.includes('--only') || args.includes('-o');
|
|
1232
|
+
const interactive = args.includes('--interactive') || args.includes('-i');
|
|
1233
|
+
const hunkSpecs = new Map();
|
|
1234
|
+
if (selective) {
|
|
1235
|
+
for (const a of args) {
|
|
1236
|
+
if (a === '--only' || a === '-o')
|
|
1237
|
+
continue;
|
|
1238
|
+
const m = a.match(/^(.+):([\d,]+)$/);
|
|
1239
|
+
if (m) {
|
|
1240
|
+
const [, file, idxStr] = m;
|
|
1241
|
+
const idxs = new Set(idxStr.split(',').map((n) => parseInt(n, 10)).filter((n) => !isNaN(n)));
|
|
1242
|
+
hunkSpecs.set(file, idxs);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
// A spec that parsed to nothing must NOT fall through to the
|
|
1246
|
+
// apply-everything branch below — the user asked to restrict the
|
|
1247
|
+
// apply, so writing every change is the opposite of the request.
|
|
1248
|
+
if (hunkSpecs.size === 0) {
|
|
1249
|
+
ctx.app.notify('Invalid --only spec. Expected `--only <file>:<hunk>[,<hunk>]` (e.g. --only src/a.ts:0,2). Nothing applied.');
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1260
1253
|
import('fs').then(async (fs) => {
|
|
1261
1254
|
import('path').then(async (pathModule) => {
|
|
1255
|
+
const { createFileDiff, applyHunksToFiles, countChangeHunks } = await import('../utils/diffPreview.js');
|
|
1262
1256
|
const diffLines = [];
|
|
1257
|
+
const fileDiffs = [];
|
|
1263
1258
|
for (const change of changes) {
|
|
1264
1259
|
const fullPath = pathModule.isAbsolute(change.path)
|
|
1265
1260
|
? change.path
|
|
1266
1261
|
: pathModule.join(ctx.projectPath, change.path);
|
|
1267
|
-
const shortPath = change.path.length > 40 ? '...' + change.path.slice(-37) : change.path;
|
|
1268
1262
|
let existingContent = '';
|
|
1269
1263
|
try {
|
|
1270
1264
|
existingContent = await fs.promises.readFile(fullPath, 'utf-8');
|
|
1271
1265
|
}
|
|
1272
1266
|
catch { }
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1267
|
+
const fd = createFileDiff(change.path, change.content, ctx.projectPath);
|
|
1268
|
+
fileDiffs.push(fd);
|
|
1269
|
+
const hunkCount = countChangeHunks(fd);
|
|
1270
|
+
diffLines.push(...formatApplyDiffLine(change, existingContent));
|
|
1271
|
+
if (hunkCount > 0) {
|
|
1272
|
+
diffLines.push(` ↳ ${hunkCount} hunk(s) — use /apply --only ${change.path}:0,1 to select`);
|
|
1276
1273
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1274
|
+
}
|
|
1275
|
+
// Interactive mode: open the hunk picker (`git add -p` style).
|
|
1276
|
+
if (interactive) {
|
|
1277
|
+
const items = [];
|
|
1278
|
+
for (const fd of fileDiffs) {
|
|
1279
|
+
for (let hi = 0; hi < fd.hunks.length; hi++) {
|
|
1280
|
+
const hunk = fd.hunks[hi];
|
|
1281
|
+
// Skip pure-context hunks (no add/remove).
|
|
1282
|
+
if (!hunk.lines.some((l) => l.type === 'add' || l.type === 'remove'))
|
|
1283
|
+
continue;
|
|
1284
|
+
const header = `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
|
|
1285
|
+
const lines = hunk.lines.map((l) => {
|
|
1286
|
+
if (l.type === 'add')
|
|
1287
|
+
return `+${l.content}`;
|
|
1288
|
+
if (l.type === 'remove')
|
|
1289
|
+
return `-${l.content}`;
|
|
1290
|
+
return ` ${l.content}`;
|
|
1291
|
+
});
|
|
1292
|
+
items.push({ path: fd.path, hunkIndex: hi, header, lines });
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (items.length === 0) {
|
|
1296
|
+
ctx.app.notify('No change hunks to review');
|
|
1297
|
+
return;
|
|
1283
1298
|
}
|
|
1299
|
+
ctx.app.showHunkPicker({
|
|
1300
|
+
title: '🎯 Review hunks',
|
|
1301
|
+
items,
|
|
1302
|
+
onComplete: (accepted) => {
|
|
1303
|
+
if (accepted.length === 0) {
|
|
1304
|
+
ctx.app.notify('No hunks applied');
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
(async () => {
|
|
1308
|
+
// Group accepted hunk indices by file path.
|
|
1309
|
+
const byPath = new Map();
|
|
1310
|
+
for (const a of accepted) {
|
|
1311
|
+
let set = byPath.get(a.path);
|
|
1312
|
+
if (!set) {
|
|
1313
|
+
set = new Set();
|
|
1314
|
+
byPath.set(a.path, set);
|
|
1315
|
+
}
|
|
1316
|
+
set.add(a.hunkIndex);
|
|
1317
|
+
}
|
|
1318
|
+
const results = applyHunksToFiles(fileDiffs, byPath);
|
|
1319
|
+
let applied = 0;
|
|
1320
|
+
for (const r of results) {
|
|
1321
|
+
try {
|
|
1322
|
+
const fullPath = pathModule.isAbsolute(r.path)
|
|
1323
|
+
? r.path
|
|
1324
|
+
: pathModule.join(ctx.projectPath, r.path);
|
|
1325
|
+
await fs.promises.mkdir(pathModule.dirname(fullPath), { recursive: true });
|
|
1326
|
+
await fs.promises.writeFile(fullPath, r.content);
|
|
1327
|
+
applied++;
|
|
1328
|
+
}
|
|
1329
|
+
catch { }
|
|
1330
|
+
}
|
|
1331
|
+
ctx.app.notify(`Applied ${accepted.length} hunk(s) across ${applied} file(s)`);
|
|
1332
|
+
})().catch((e) => ctx.app.notify(`Apply failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
1333
|
+
},
|
|
1334
|
+
});
|
|
1335
|
+
return;
|
|
1284
1336
|
}
|
|
1337
|
+
const summary = selective && hunkSpecs.size > 0
|
|
1338
|
+
? `Selective apply (${hunkSpecs.size} file(s) with chosen hunks)`
|
|
1339
|
+
: `Found ${changes.length} file(s) to apply`;
|
|
1285
1340
|
ctx.app.showConfirm({
|
|
1286
1341
|
title: '📝 Apply Changes',
|
|
1287
1342
|
message: [
|
|
1288
|
-
|
|
1343
|
+
summary,
|
|
1289
1344
|
'',
|
|
1290
|
-
...diffLines.slice(0,
|
|
1291
|
-
...(diffLines.length >
|
|
1345
|
+
...diffLines.slice(0, 12),
|
|
1346
|
+
...(diffLines.length > 12 ? [` ...and ${diffLines.length - 12} more`] : []),
|
|
1292
1347
|
'',
|
|
1293
|
-
'Apply these changes?',
|
|
1348
|
+
selective && hunkSpecs.size > 0 ? 'Apply selected hunks?' : 'Apply these changes?',
|
|
1294
1349
|
],
|
|
1295
1350
|
confirmLabel: 'Apply',
|
|
1296
1351
|
cancelLabel: 'Cancel',
|
|
1297
1352
|
onConfirm: () => {
|
|
1298
1353
|
(async () => {
|
|
1299
1354
|
let applied = 0;
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1355
|
+
if (selective && hunkSpecs.size > 0) {
|
|
1356
|
+
// Per-hunk selective apply.
|
|
1357
|
+
const results = applyHunksToFiles(fileDiffs, hunkSpecs);
|
|
1358
|
+
for (const r of results) {
|
|
1359
|
+
try {
|
|
1360
|
+
const fullPath = pathModule.isAbsolute(r.path)
|
|
1361
|
+
? r.path
|
|
1362
|
+
: pathModule.join(ctx.projectPath, r.path);
|
|
1363
|
+
await fs.promises.mkdir(pathModule.dirname(fullPath), { recursive: true });
|
|
1364
|
+
await fs.promises.writeFile(fullPath, r.content);
|
|
1365
|
+
applied++;
|
|
1366
|
+
}
|
|
1367
|
+
catch { }
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
else {
|
|
1371
|
+
// All-or-nothing apply (original behavior).
|
|
1372
|
+
for (const change of changes) {
|
|
1373
|
+
try {
|
|
1374
|
+
const fullPath = pathModule.isAbsolute(change.path)
|
|
1375
|
+
? change.path
|
|
1376
|
+
: pathModule.join(ctx.projectPath, change.path);
|
|
1377
|
+
await fs.promises.mkdir(pathModule.dirname(fullPath), { recursive: true });
|
|
1378
|
+
await fs.promises.writeFile(fullPath, change.content);
|
|
1379
|
+
applied++;
|
|
1380
|
+
}
|
|
1381
|
+
catch { }
|
|
1308
1382
|
}
|
|
1309
|
-
catch { }
|
|
1310
1383
|
}
|
|
1311
|
-
ctx.app.notify(`Applied ${applied}/${changes.length} file(s)`);
|
|
1312
|
-
})();
|
|
1384
|
+
ctx.app.notify(`Applied ${applied}/${selective ? hunkSpecs.size : changes.length} file(s)`);
|
|
1385
|
+
})().catch((e) => ctx.app.notify(`Apply failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
1313
1386
|
},
|
|
1314
1387
|
onCancel: () => ctx.app.notify('Apply cancelled'),
|
|
1315
1388
|
});
|
|
1316
1389
|
});
|
|
1317
|
-
});
|
|
1390
|
+
}).catch((e) => ctx.app.notify(`Apply failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
1318
1391
|
break;
|
|
1319
1392
|
}
|
|
1320
1393
|
case 'add': {
|
|
@@ -1769,10 +1842,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1769
1842
|
ctx.app.notify(`Skill ${name} not found`);
|
|
1770
1843
|
break;
|
|
1771
1844
|
}
|
|
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
|
-
});
|
|
1845
|
+
ctx.app.addMessage({ role: 'system', content: formatSkillsShow(bundle) });
|
|
1776
1846
|
break;
|
|
1777
1847
|
}
|
|
1778
1848
|
// Marketplace operations against codeep.dev.
|
|
@@ -1790,10 +1860,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1790
1860
|
ctx.app.notify(`Publish failed: ${result.error}`);
|
|
1791
1861
|
break;
|
|
1792
1862
|
}
|
|
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
|
-
});
|
|
1863
|
+
ctx.app.addMessage({ role: 'system', content: formatSkillsPublishResult(slug, isPublic, result.skill?.owner_username) });
|
|
1797
1864
|
break;
|
|
1798
1865
|
}
|
|
1799
1866
|
if (sub === 'install') {
|
|
@@ -1823,7 +1890,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1823
1890
|
}
|
|
1824
1891
|
const skills = result.skills ?? [];
|
|
1825
1892
|
if (skills.length === 0) {
|
|
1826
|
-
ctx.app.addMessage({ role: 'system', content: query
|
|
1893
|
+
ctx.app.addMessage({ role: 'system', content: formatSkillsBrowseEmpty(query) });
|
|
1827
1894
|
break;
|
|
1828
1895
|
}
|
|
1829
1896
|
const lines = [`# ${query ? `Skills matching "${query}"` : 'Public skills'}`, ''];
|
|
@@ -2074,7 +2141,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2074
2141
|
ctx.app.notify('No profiles saved. Use /profile save <name>');
|
|
2075
2142
|
}
|
|
2076
2143
|
else {
|
|
2077
|
-
ctx.app.addMessage({ role: 'system', content:
|
|
2144
|
+
ctx.app.addMessage({ role: 'system', content: formatProfileList(profiles) });
|
|
2078
2145
|
}
|
|
2079
2146
|
break;
|
|
2080
2147
|
}
|
|
@@ -2214,55 +2281,18 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2214
2281
|
// case no longer also claims 'cost' (which always hit the handler above).
|
|
2215
2282
|
case 'stats': {
|
|
2216
2283
|
const { getCostBreakdown, getSessionStats, formatTokenCount, getPricingTable, getCacheStats } = await import('../utils/tokenTracker.js');
|
|
2284
|
+
const { formatResourceImpactReport } = await import('../utils/resourceImpact.js');
|
|
2217
2285
|
const stats = getSessionStats();
|
|
2218
|
-
const
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
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') });
|
|
2286
|
+
const content = formatStatsReport({
|
|
2287
|
+
totals: stats,
|
|
2288
|
+
breakdown: getCostBreakdown(),
|
|
2289
|
+
cache: getCacheStats(),
|
|
2290
|
+
pricing: getPricingTable(),
|
|
2291
|
+
currentProvider: config.get('provider'),
|
|
2292
|
+
fmt: formatTokenCount,
|
|
2293
|
+
impactLines: formatResourceImpactReport(stats.totalTokens),
|
|
2294
|
+
});
|
|
2295
|
+
ctx.app.addMessage({ role: 'system', content });
|
|
2266
2296
|
break;
|
|
2267
2297
|
}
|
|
2268
2298
|
case 'memory': {
|
|
@@ -2280,8 +2310,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2280
2310
|
ctx.app.notify('No memory notes. Add one with: /memory <note>');
|
|
2281
2311
|
}
|
|
2282
2312
|
else {
|
|
2283
|
-
|
|
2284
|
-
ctx.app.addMessage({ role: 'assistant', content: `**Project memory notes:**\n${lines}` });
|
|
2313
|
+
ctx.app.addMessage({ role: 'assistant', content: formatMemoryList(intelligence.notes) });
|
|
2285
2314
|
}
|
|
2286
2315
|
break;
|
|
2287
2316
|
}
|
|
@@ -2322,6 +2351,29 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2322
2351
|
ctx.app.addMessage({ role: 'system', content: formatCommandList(loadCustomCommands(ctx.projectPath)) });
|
|
2323
2352
|
break;
|
|
2324
2353
|
}
|
|
2354
|
+
case 'web-cache': {
|
|
2355
|
+
const sub = args[0]?.toLowerCase();
|
|
2356
|
+
const { clearWebCache, webCacheStats } = await import('../utils/webFetch.js');
|
|
2357
|
+
if (sub === 'clear' || sub === 'reset' || sub === 'flush') {
|
|
2358
|
+
clearWebCache();
|
|
2359
|
+
ctx.app.notify('Web cache cleared');
|
|
2360
|
+
}
|
|
2361
|
+
else {
|
|
2362
|
+
const stats = webCacheStats();
|
|
2363
|
+
ctx.app.addMessage({
|
|
2364
|
+
role: 'system',
|
|
2365
|
+
content: [
|
|
2366
|
+
'🌐 Web fetch cache',
|
|
2367
|
+
'',
|
|
2368
|
+
` Entries: ${stats.entries}/${stats.maxEntries}`,
|
|
2369
|
+
` TTL: ${stats.ttlMinutes} min`,
|
|
2370
|
+
'',
|
|
2371
|
+
'Usage: /web-cache clear',
|
|
2372
|
+
].join('\n'),
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
break;
|
|
2376
|
+
}
|
|
2325
2377
|
case 'mcp': {
|
|
2326
2378
|
// Mirrors the ACP `/mcp` handler in src/acp/commands.ts. In TUI the
|
|
2327
2379
|
// session id is the constant `codeep-tui` (the same one main.ts uses
|
|
@@ -2480,35 +2532,14 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2480
2532
|
ctx.app.notify('Reloading MCP server config…');
|
|
2481
2533
|
const merged = loadMcpServerConfig(projectPath);
|
|
2482
2534
|
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') });
|
|
2535
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpReloadReport(registered.length, merged.length, errors) });
|
|
2490
2536
|
break;
|
|
2491
2537
|
}
|
|
2492
2538
|
if (sub === 'resources') {
|
|
2493
2539
|
const { getSessionResources, awaitSessionReady } = await import('../utils/mcpRegistry.js');
|
|
2494
2540
|
await awaitSessionReady(TUI_SESSION);
|
|
2495
2541
|
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() });
|
|
2542
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpResourcesList(groups) });
|
|
2512
2543
|
break;
|
|
2513
2544
|
}
|
|
2514
2545
|
if (sub === 'read') {
|
|
@@ -2520,23 +2551,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2520
2551
|
const { readSessionResource } = await import('../utils/mcpRegistry.js');
|
|
2521
2552
|
try {
|
|
2522
2553
|
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') });
|
|
2554
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpResourceRead(uri, contents) });
|
|
2540
2555
|
}
|
|
2541
2556
|
catch (err) {
|
|
2542
2557
|
ctx.app.addMessage({ role: 'system', content: `Failed to read \`${uri}\`: ${err.message}` });
|
|
@@ -2547,23 +2562,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2547
2562
|
const { getSessionPrompts, awaitSessionReady } = await import('../utils/mcpRegistry.js');
|
|
2548
2563
|
await awaitSessionReady(TUI_SESSION);
|
|
2549
2564
|
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() });
|
|
2565
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpPromptsList(groups) });
|
|
2567
2566
|
break;
|
|
2568
2567
|
}
|
|
2569
2568
|
if (sub === 'prompt') {
|
|
@@ -2573,25 +2572,11 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2573
2572
|
ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp prompt <server> <name> [key=value ...]`' });
|
|
2574
2573
|
break;
|
|
2575
2574
|
}
|
|
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
|
-
}
|
|
2575
|
+
const promptArgs = parsePromptArgs(args.slice(3));
|
|
2582
2576
|
const { getSessionPrompt } = await import('../utils/mcpRegistry.js');
|
|
2583
2577
|
try {
|
|
2584
2578
|
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() });
|
|
2579
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpPromptResult(serverName, name, description, messages) });
|
|
2595
2580
|
}
|
|
2596
2581
|
catch (err) {
|
|
2597
2582
|
ctx.app.addMessage({ role: 'system', content: `Failed to materialise prompt: ${err.message}` });
|
|
@@ -2603,41 +2588,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2603
2588
|
await awaitSessionReady(TUI_SESSION);
|
|
2604
2589
|
const tools = await getSessionTools(TUI_SESSION);
|
|
2605
2590
|
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() });
|
|
2591
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpServerList(tools, mcpErrors) });
|
|
2641
2592
|
break;
|
|
2642
2593
|
}
|
|
2643
2594
|
default: {
|