codeep 2.14.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 +47 -27
- package/dist/acp/commands.js +22 -1
- package/dist/acp/server.js +13 -2
- package/dist/acp/session.js +22 -1
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +2 -2
- package/dist/config/providers.js +35 -24
- package/dist/renderer/App.d.ts +77 -30
- package/dist/renderer/App.js +429 -659
- package/dist/renderer/agentExecution.d.ts +1 -0
- package/dist/renderer/agentExecution.js +3 -2
- package/dist/renderer/commands/helpers.d.ts +251 -0
- package/dist/renderer/commands/helpers.js +450 -0
- package/dist/renderer/commands/registry.js +7 -1
- package/dist/renderer/commands.d.ts +4 -0
- package/dist/renderer/commands.js +363 -318
- package/dist/renderer/components/ActionFormatting.d.ts +17 -0
- package/dist/renderer/components/ActionFormatting.js +67 -0
- package/dist/renderer/components/Autocomplete.d.ts +58 -0
- package/dist/renderer/components/Autocomplete.js +75 -0
- package/dist/renderer/components/Intro.d.ts +9 -0
- package/dist/renderer/components/Intro.js +5 -15
- package/dist/renderer/components/MessageFormatter.d.ts +96 -0
- package/dist/renderer/components/MessageFormatter.js +375 -0
- package/dist/renderer/components/Permission.d.ts +4 -0
- package/dist/renderer/components/Permission.js +1 -1
- package/dist/renderer/components/Status.d.ts +4 -0
- package/dist/renderer/components/Status.js +2 -3
- package/dist/renderer/components/WelcomeFormatter.d.ts +19 -0
- package/dist/renderer/components/WelcomeFormatter.js +79 -0
- package/dist/renderer/components/uiConstants.d.ts +8 -0
- package/dist/renderer/components/uiConstants.js +24 -0
- package/dist/renderer/inputParsing.d.ts +22 -0
- package/dist/renderer/inputParsing.js +28 -0
- package/dist/renderer/layout.d.ts +219 -0
- package/dist/renderer/layout.js +338 -0
- package/dist/renderer/main.d.ts +2 -1
- package/dist/renderer/main.js +79 -11
- package/dist/renderer/ollamaHint.d.ts +12 -0
- package/dist/renderer/ollamaHint.js +29 -0
- package/dist/utils/agentChat.js +23 -1
- package/dist/utils/codeepCloud.d.ts +54 -0
- package/dist/utils/codeepCloud.js +95 -0
- package/dist/utils/diffPreview.d.ts +31 -0
- package/dist/utils/diffPreview.js +102 -0
- package/dist/utils/export.d.ts +12 -0
- package/dist/utils/export.js +3 -3
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +111 -1
- package/dist/utils/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +69 -1
- package/dist/utils/keychain.js +45 -29
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/mcpConfig.d.ts +26 -0
- package/dist/utils/mcpConfig.js +109 -4
- package/dist/utils/mentions.d.ts +195 -0
- package/dist/utils/mentions.js +672 -0
- package/dist/utils/skillBundles.d.ts +14 -0
- package/dist/utils/skillBundles.js +3 -3
- package/dist/utils/skillBundlesCloud.d.ts +7 -0
- package/dist/utils/skillBundlesCloud.js +1 -1
- package/dist/utils/tokenTracker.js +21 -5
- package/dist/utils/toolParsing.d.ts +11 -0
- package/dist/utils/toolParsing.js +6 -0
- 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 -2
|
@@ -12,31 +12,27 @@ import { getProviderList, getProvider, modelSupportsReasoningEffort, reasoningPa
|
|
|
12
12
|
import { setProjectContext } from '../api/index.js';
|
|
13
13
|
import { runSkill, runCommandChain } from './agentExecution.js';
|
|
14
14
|
import { loadProjectIntelligence, saveProjectIntelligence } from '../utils/projectIntelligence.js';
|
|
15
|
+
import { ollamaModelHint } from './ollamaHint.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';
|
|
15
18
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
16
19
|
/**
|
|
17
20
|
* Returns a hint for an Ollama model name based on parameter count.
|
|
18
21
|
* Models ≥7B are suitable for agent mode; smaller ones are chat-only.
|
|
19
22
|
*/
|
|
20
|
-
function ollamaModelHint(modelId) {
|
|
21
|
-
const lower = modelId.toLowerCase();
|
|
22
|
-
// Extract number before 'b' (e.g. "7b", "1.5b", "14b", "72b")
|
|
23
|
-
const match = lower.match(/(\d+(?:\.\d+)?)b/);
|
|
24
|
-
if (!match)
|
|
25
|
-
return '';
|
|
26
|
-
const params = parseFloat(match[1]);
|
|
27
|
-
if (params >= 7)
|
|
28
|
-
return '✓ agent mode';
|
|
29
|
-
return '⚠ chat only (< 7B)';
|
|
30
|
-
}
|
|
31
23
|
// ─── Main dispatch ────────────────────────────────────────────────────────────
|
|
32
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;
|
|
33
29
|
// Handle skill chaining (e.g., /commit+push)
|
|
34
|
-
if (
|
|
35
|
-
const commands =
|
|
30
|
+
if (canonical.includes('+')) {
|
|
31
|
+
const commands = canonical.split('+').filter(c => c.trim());
|
|
36
32
|
runCommandChain(commands, 0, ctx);
|
|
37
33
|
return;
|
|
38
34
|
}
|
|
39
|
-
switch (
|
|
35
|
+
switch (canonical) {
|
|
40
36
|
case 'version': {
|
|
41
37
|
const version = getCurrentVersion();
|
|
42
38
|
const provider = getCurrentProvider();
|
|
@@ -317,7 +313,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
317
313
|
ctx.app.notify('Thinking effort: auto — each model uses its own default.');
|
|
318
314
|
}
|
|
319
315
|
else if (!supported) {
|
|
320
|
-
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).`);
|
|
321
317
|
}
|
|
322
318
|
else {
|
|
323
319
|
// Tell the user what THIS model will actually run (the tier may
|
|
@@ -433,21 +429,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
433
429
|
}
|
|
434
430
|
case 'insights': {
|
|
435
431
|
const { formatInsights } = await import('../utils/insights.js');
|
|
436
|
-
|
|
437
|
-
let days = 7;
|
|
438
|
-
for (let i = 0; i < args.length; i++) {
|
|
439
|
-
const a = args[i];
|
|
440
|
-
if (a === '--days' && args[i + 1]) {
|
|
441
|
-
const n = parseInt(args[i + 1], 10);
|
|
442
|
-
if (Number.isFinite(n))
|
|
443
|
-
days = n;
|
|
444
|
-
}
|
|
445
|
-
else if (a.startsWith('--days=')) {
|
|
446
|
-
const n = parseInt(a.slice('--days='.length), 10);
|
|
447
|
-
if (Number.isFinite(n))
|
|
448
|
-
days = n;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
432
|
+
const days = parseInsightsDays(args);
|
|
451
433
|
ctx.app.addMessage({ role: 'system', content: formatInsights({ days }) });
|
|
452
434
|
break;
|
|
453
435
|
}
|
|
@@ -522,12 +504,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
522
504
|
break;
|
|
523
505
|
}
|
|
524
506
|
const file = scope === 'global' ? '~/.codeep/profile.learned.md' : '.codeep/profile.learned.md';
|
|
525
|
-
ctx.app.addMessage({
|
|
526
|
-
role: 'system',
|
|
527
|
-
content: res.updated
|
|
528
|
-
? `Updated your ${scope} learned profile (\`${file}\`):\n\n${res.facts}\n\nClear it anytime with \`/me forget\`.`
|
|
529
|
-
: `No changes — your ${scope} learned profile already covers this:\n\n${res.facts}`,
|
|
530
|
-
});
|
|
507
|
+
ctx.app.addMessage({ role: 'system', content: formatMeLearnResult(scope, file, res) });
|
|
531
508
|
break;
|
|
532
509
|
}
|
|
533
510
|
if (sub === 'forget') {
|
|
@@ -546,14 +523,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
546
523
|
ctx.app.notify('Syncing your profile with codeep.dev…');
|
|
547
524
|
const pushed = await pushUserProfile();
|
|
548
525
|
const pulled = await pullUserProfile();
|
|
549
|
-
|
|
550
|
-
if (pushed)
|
|
551
|
-
lines.push('✓ Profile pushed to the dashboard');
|
|
552
|
-
if (pulled === 1)
|
|
553
|
-
lines.push('✓ Profile pulled to this machine');
|
|
554
|
-
if (lines.length === 0)
|
|
555
|
-
lines.push('Nothing to sync yet — run `/me init` and fill in your profile first.');
|
|
556
|
-
ctx.app.addMessage({ role: 'system', content: `## Profile sync\n\n${lines.join('\n')}` });
|
|
526
|
+
ctx.app.addMessage({ role: 'system', content: formatMeSyncReport(pushed, pulled) });
|
|
557
527
|
break;
|
|
558
528
|
}
|
|
559
529
|
if (sub === 'init') {
|
|
@@ -567,12 +537,7 @@ export async function handleCommand(command, args, ctx) {
|
|
|
567
537
|
ctx.app.notify('Could not create the profile file.');
|
|
568
538
|
break;
|
|
569
539
|
}
|
|
570
|
-
ctx.app.addMessage({
|
|
571
|
-
role: 'system',
|
|
572
|
-
content: res.created
|
|
573
|
-
? `Created ${scope} profile: \`${res.path}\`\n\nEdit it in your editor — Codeep uses it automatically. View anytime with \`/me\`.`
|
|
574
|
-
: `${scope === 'global' ? 'Global' : 'Project'} profile already exists: \`${res.path}\`\n\nEdit it directly, or view it with \`/me\`.`,
|
|
575
|
-
});
|
|
540
|
+
ctx.app.addMessage({ role: 'system', content: formatMeInitResult(scope, res) });
|
|
576
541
|
break;
|
|
577
542
|
}
|
|
578
543
|
// Default: show the profile view.
|
|
@@ -676,6 +641,142 @@ export async function handleCommand(command, args, ctx) {
|
|
|
676
641
|
});
|
|
677
642
|
break;
|
|
678
643
|
}
|
|
644
|
+
case 'cloud': {
|
|
645
|
+
// Cross-device resume: list sessions synced from other devices/Mac app
|
|
646
|
+
// and pull the selected one into the local store, then load it.
|
|
647
|
+
//
|
|
648
|
+
// Not linked → friendly prompt to run `codeep account`.
|
|
649
|
+
// Network/empty → notify (no crash).
|
|
650
|
+
//
|
|
651
|
+
// We scope to the current project when one is open, so a user on their
|
|
652
|
+
// laptop sees the sessions they ran on the desktop for the same repo.
|
|
653
|
+
// BUT: project identity is a hash of the LOCAL absolute path, so the
|
|
654
|
+
// same repo cloned at a different path (the normal cross-device case)
|
|
655
|
+
// has a different projectId. When the scoped list comes back empty we
|
|
656
|
+
// fall back to listing everything, and a non-empty scoped list still
|
|
657
|
+
// offers a "show all" escape hatch — otherwise cross-device resume
|
|
658
|
+
// only works when both machines use identical directory layouts.
|
|
659
|
+
const { listCloudSessions, pullCloudSession, generateProjectId } = await import('../utils/codeepCloud.js');
|
|
660
|
+
const projectId = ctx.projectPath ? generateProjectId(ctx.projectPath) : undefined;
|
|
661
|
+
ctx.app.notify('Fetching cloud sessions…');
|
|
662
|
+
let summaries = await listCloudSessions(projectId);
|
|
663
|
+
if (summaries === null) {
|
|
664
|
+
ctx.app.notify('Not linked — run `codeep account` to enable cloud sync.');
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
let scopedToProject = Boolean(projectId);
|
|
668
|
+
if (summaries.length === 0 && projectId) {
|
|
669
|
+
// Nothing under this project's path-hash — try the unscoped list so
|
|
670
|
+
// sessions synced from a machine with a different path still show up.
|
|
671
|
+
const all = await listCloudSessions();
|
|
672
|
+
if (all && all.length > 0) {
|
|
673
|
+
summaries = all;
|
|
674
|
+
scopedToProject = false;
|
|
675
|
+
ctx.app.notify('No sessions matched this project — showing all cloud sessions.');
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (summaries.length === 0) {
|
|
679
|
+
ctx.app.notify(projectId
|
|
680
|
+
? 'No cloud sessions for this project yet.'
|
|
681
|
+
: 'No cloud sessions yet.');
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
// Non-null binding for the picker closure — TS can't carry the null
|
|
685
|
+
// narrowing of a reassigned `let` into the callback.
|
|
686
|
+
let sessionList = summaries;
|
|
687
|
+
const SHOW_ALL = 'Show all cloud sessions…';
|
|
688
|
+
const labels = summaries.map(formatCloudSessionLabel);
|
|
689
|
+
if (scopedToProject)
|
|
690
|
+
labels.push(SHOW_ALL);
|
|
691
|
+
// Named so the "Show all" branch can re-present the picker with the
|
|
692
|
+
// same handler (a const arrow can reference itself; the binding is
|
|
693
|
+
// initialized long before the callback can fire).
|
|
694
|
+
const onPickCloudSession = async (index) => {
|
|
695
|
+
if (scopedToProject && index === sessionList.length) {
|
|
696
|
+
// "Show all" — re-list unscoped. Re-dispatching /cloud would
|
|
697
|
+
// re-scope to the project, so fetch + present inline instead.
|
|
698
|
+
const all = await listCloudSessions();
|
|
699
|
+
if (!all || all.length === 0) {
|
|
700
|
+
ctx.app.notify('No other cloud sessions.');
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
sessionList = all;
|
|
704
|
+
scopedToProject = false;
|
|
705
|
+
const allLabels = all.map(s => {
|
|
706
|
+
const date = new Date(s.updatedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
707
|
+
const title = s.sessionName || s.sessionId.slice(0, 8);
|
|
708
|
+
const projectTag = s.projectName ? ` · ${s.projectName}` : '';
|
|
709
|
+
return `${title} · ${date} · ${s.messageCount} msg${projectTag}`;
|
|
710
|
+
});
|
|
711
|
+
ctx.app.showList('Cloud Sessions (all)', allLabels, onPickCloudSession);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const selected = sessionList[index];
|
|
715
|
+
// The cloud id becomes a local FILENAME (saveSession joins it into
|
|
716
|
+
// .codeep/sessions/<name>.json) — whitelist it so a hostile or
|
|
717
|
+
// corrupted server response can't traverse outside the sessions dir.
|
|
718
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(selected.sessionId)) {
|
|
719
|
+
ctx.app.notify('Cloud session has an unexpected id format — refusing to save it locally.');
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
ctx.app.notify(`Pulling ${selected.sessionName || selected.sessionId.slice(0, 8)}…`);
|
|
723
|
+
const full = await pullCloudSession(selected.sessionId);
|
|
724
|
+
if (!full) {
|
|
725
|
+
ctx.app.notify('Failed to pull session (network or not found).');
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
// Cloud messages are {role, content}; local Message is the same shape
|
|
729
|
+
// plus 'system'. Filter to user/assistant (the server already does,
|
|
730
|
+
// but be defensive) and coerce.
|
|
731
|
+
const history = full.messages
|
|
732
|
+
.filter(m => m.role === 'user' || m.role === 'assistant')
|
|
733
|
+
.map(m => ({ role: m.role, content: m.content }));
|
|
734
|
+
if (history.length === 0) {
|
|
735
|
+
ctx.app.notify('Cloud session has no loadable messages.');
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
// Persist locally so the resumed session is first-class: it appears
|
|
739
|
+
// in /sessions, autosaves on next change, and re-syncs on next turn.
|
|
740
|
+
// We reuse the cloud sessionId as the local name so a subsequent
|
|
741
|
+
// push updates the same cloud record (ON DUPLICATE KEY UPDATE).
|
|
742
|
+
const localName = selected.sessionId;
|
|
743
|
+
// If a local copy of this session exists and is NEWER than the cloud
|
|
744
|
+
// record (continued locally since the last sync), load it instead of
|
|
745
|
+
// clobbering the newer history with the older cloud copy.
|
|
746
|
+
try {
|
|
747
|
+
const { statSync, existsSync } = await import('fs');
|
|
748
|
+
const { join } = await import('path');
|
|
749
|
+
const { getSessionsDir } = await import('../config/index.js');
|
|
750
|
+
const localPath = join(getSessionsDir(ctx.projectPath), `${localName}.json`);
|
|
751
|
+
const cloudUpdatedAt = Date.parse(selected.updatedAt);
|
|
752
|
+
if (existsSync(localPath) && Number.isFinite(cloudUpdatedAt)
|
|
753
|
+
&& statSync(localPath).mtimeMs > cloudUpdatedAt) {
|
|
754
|
+
const local = loadSession(localName, ctx.projectPath);
|
|
755
|
+
if (local && local.length > 0) {
|
|
756
|
+
ctx.app.setMessages(local);
|
|
757
|
+
ctx.setSessionId(localName);
|
|
758
|
+
config.set('currentSessionId', localName);
|
|
759
|
+
ctx.setSessionDisplayName?.(selected.sessionName ?? null);
|
|
760
|
+
ctx.app.notify('Local copy is newer than the cloud record — loaded the local session instead.');
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
catch { /* mtime probe is best-effort — fall through to cloud copy */ }
|
|
766
|
+
saveSession(localName, history, ctx.projectPath);
|
|
767
|
+
ctx.app.setMessages(history);
|
|
768
|
+
// Keep ALL session-identity state in step, not just the renderer's
|
|
769
|
+
// copy: autosave + agent-mode sync read config.currentSessionId, and
|
|
770
|
+
// the next syncSession reads the display name — leaving either stale
|
|
771
|
+
// writes/renames the pulled history under the PREVIOUS session.
|
|
772
|
+
ctx.setSessionId(localName);
|
|
773
|
+
config.set('currentSessionId', localName);
|
|
774
|
+
ctx.setSessionDisplayName?.(selected.sessionName ?? null);
|
|
775
|
+
ctx.app.notify(`Resumed from cloud: ${selected.sessionName || localName}`);
|
|
776
|
+
};
|
|
777
|
+
ctx.app.showList('Cloud Sessions', labels, onPickCloudSession);
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
679
780
|
case 'new': {
|
|
680
781
|
ctx.app.clearMessages();
|
|
681
782
|
ctx.setSessionId(startNewSession());
|
|
@@ -829,7 +930,7 @@ Format: use headers per category, only include categories where you found issues
|
|
|
829
930
|
ctx.app.notify('Usage: /rename <new-name>');
|
|
830
931
|
return;
|
|
831
932
|
}
|
|
832
|
-
const newName = args
|
|
933
|
+
const newName = joinSessionName(args);
|
|
833
934
|
const messages = ctx.app.getMessages();
|
|
834
935
|
if (messages.length === 0) {
|
|
835
936
|
ctx.app.notify('No messages to save. Start a conversation first.');
|
|
@@ -865,18 +966,7 @@ Format: use headers per category, only include categories where you found issues
|
|
|
865
966
|
}
|
|
866
967
|
const searchTerm = args.join(' ').toLowerCase();
|
|
867
968
|
const messages = ctx.app.getMessages();
|
|
868
|
-
const searchResults =
|
|
869
|
-
messages.forEach((m, index) => {
|
|
870
|
-
if (m.content.toLowerCase().includes(searchTerm)) {
|
|
871
|
-
const lowerContent = m.content.toLowerCase();
|
|
872
|
-
const matchStart = Math.max(0, lowerContent.indexOf(searchTerm) - 30);
|
|
873
|
-
const matchEnd = Math.min(m.content.length, lowerContent.indexOf(searchTerm) + searchTerm.length + 50);
|
|
874
|
-
const matchedText = (matchStart > 0 ? '...' : '') +
|
|
875
|
-
m.content.slice(matchStart, matchEnd).replace(/\n/g, ' ') +
|
|
876
|
-
(matchEnd < m.content.length ? '...' : '');
|
|
877
|
-
searchResults.push({ role: m.role, messageIndex: index, matchedText });
|
|
878
|
-
}
|
|
879
|
-
});
|
|
969
|
+
const searchResults = buildSearchSnippets(messages, searchTerm);
|
|
880
970
|
if (searchResults.length === 0) {
|
|
881
971
|
ctx.app.notify(`No matches for "${searchTerm}"`);
|
|
882
972
|
}
|
|
@@ -1063,18 +1153,13 @@ Format: use headers per category, only include categories where you found issues
|
|
|
1063
1153
|
case 'copy': {
|
|
1064
1154
|
const blockNum = args[0] ? parseInt(args[0], 10) : -1;
|
|
1065
1155
|
const messages = ctx.app.getMessages();
|
|
1066
|
-
const codeBlocks =
|
|
1067
|
-
for (const msg of messages) {
|
|
1068
|
-
for (const match of msg.content.matchAll(/```[\w]*\n([\s\S]*?)```/g)) {
|
|
1069
|
-
codeBlocks.push(match[1]);
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1156
|
+
const codeBlocks = messages.flatMap(m => extractCodeBlocks(m.content));
|
|
1072
1157
|
if (codeBlocks.length === 0) {
|
|
1073
1158
|
ctx.app.notify('No code blocks found');
|
|
1074
1159
|
return;
|
|
1075
1160
|
}
|
|
1076
|
-
const index = blockNum
|
|
1077
|
-
if (
|
|
1161
|
+
const index = resolveBlockIndex(blockNum, codeBlocks.length);
|
|
1162
|
+
if (index === null) {
|
|
1078
1163
|
ctx.app.notify(`Invalid block number. Available: 1-${codeBlocks.length}`);
|
|
1079
1164
|
return;
|
|
1080
1165
|
}
|
|
@@ -1112,20 +1197,7 @@ Format: use headers per category, only include categories where you found issues
|
|
|
1112
1197
|
ctx.app.notify('No assistant response to apply');
|
|
1113
1198
|
return;
|
|
1114
1199
|
}
|
|
1115
|
-
const changes =
|
|
1116
|
-
const fenceFilePattern = /```\w*\s+([\w./\\-]+(?:\.\w+))\n([\s\S]*?)```/g;
|
|
1117
|
-
let match;
|
|
1118
|
-
while ((match = fenceFilePattern.exec(lastAssistant.content)) !== null) {
|
|
1119
|
-
const p = match[1].trim();
|
|
1120
|
-
if (p.includes('.') && !p.includes(' '))
|
|
1121
|
-
changes.push({ path: p, content: match[2] });
|
|
1122
|
-
}
|
|
1123
|
-
if (changes.length === 0) {
|
|
1124
|
-
const commentPattern = /```(\w+)?\s*\n(?:\/\/|#|--|\/\*)\s*(?:File|Path|file|path):\s*([^\n*]+)\n([\s\S]*?)```/g;
|
|
1125
|
-
while ((match = commentPattern.exec(lastAssistant.content)) !== null) {
|
|
1126
|
-
changes.push({ path: match[2].trim(), content: match[3] });
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1200
|
+
const changes = extractFileChanges(lastAssistant.content);
|
|
1129
1201
|
if (changes.length === 0) {
|
|
1130
1202
|
ctx.app.notify('No file changes found in response');
|
|
1131
1203
|
return;
|
|
@@ -1134,64 +1206,168 @@ Format: use headers per category, only include categories where you found issues
|
|
|
1134
1206
|
ctx.app.notify('Write access required. Use /grant first.');
|
|
1135
1207
|
return;
|
|
1136
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
|
+
}
|
|
1137
1233
|
import('fs').then(async (fs) => {
|
|
1138
1234
|
import('path').then(async (pathModule) => {
|
|
1235
|
+
const { createFileDiff, applyHunksToFiles, countChangeHunks } = await import('../utils/diffPreview.js');
|
|
1139
1236
|
const diffLines = [];
|
|
1237
|
+
const fileDiffs = [];
|
|
1140
1238
|
for (const change of changes) {
|
|
1141
1239
|
const fullPath = pathModule.isAbsolute(change.path)
|
|
1142
1240
|
? change.path
|
|
1143
1241
|
: pathModule.join(ctx.projectPath, change.path);
|
|
1144
|
-
const shortPath = change.path.length > 40 ? '...' + change.path.slice(-37) : change.path;
|
|
1145
1242
|
let existingContent = '';
|
|
1146
1243
|
try {
|
|
1147
1244
|
existingContent = await fs.promises.readFile(fullPath, 'utf-8');
|
|
1148
1245
|
}
|
|
1149
1246
|
catch { }
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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`);
|
|
1153
1253
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
+
}
|
|
1160
1274
|
}
|
|
1275
|
+
if (items.length === 0) {
|
|
1276
|
+
ctx.app.notify('No change hunks to review');
|
|
1277
|
+
return;
|
|
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;
|
|
1161
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`;
|
|
1162
1320
|
ctx.app.showConfirm({
|
|
1163
1321
|
title: '📝 Apply Changes',
|
|
1164
1322
|
message: [
|
|
1165
|
-
|
|
1323
|
+
summary,
|
|
1166
1324
|
'',
|
|
1167
|
-
...diffLines.slice(0,
|
|
1168
|
-
...(diffLines.length >
|
|
1325
|
+
...diffLines.slice(0, 12),
|
|
1326
|
+
...(diffLines.length > 12 ? [` ...and ${diffLines.length - 12} more`] : []),
|
|
1169
1327
|
'',
|
|
1170
|
-
'Apply these changes?',
|
|
1328
|
+
selective && hunkSpecs.size > 0 ? 'Apply selected hunks?' : 'Apply these changes?',
|
|
1171
1329
|
],
|
|
1172
1330
|
confirmLabel: 'Apply',
|
|
1173
1331
|
cancelLabel: 'Cancel',
|
|
1174
1332
|
onConfirm: () => {
|
|
1175
1333
|
(async () => {
|
|
1176
1334
|
let applied = 0;
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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 { }
|
|
1185
1348
|
}
|
|
1186
|
-
catch { }
|
|
1187
1349
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
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)}`));
|
|
1190
1366
|
},
|
|
1191
1367
|
onCancel: () => ctx.app.notify('Apply cancelled'),
|
|
1192
1368
|
});
|
|
1193
1369
|
});
|
|
1194
|
-
});
|
|
1370
|
+
}).catch((e) => ctx.app.notify(`Apply failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
1195
1371
|
break;
|
|
1196
1372
|
}
|
|
1197
1373
|
case 'add': {
|
|
@@ -1302,7 +1478,7 @@ Format: use headers per category, only include categories where you found issues
|
|
|
1302
1478
|
}
|
|
1303
1479
|
case 'compact': {
|
|
1304
1480
|
const messages = ctx.app.getMessages();
|
|
1305
|
-
const keepRecent =
|
|
1481
|
+
const keepRecent = parseKeepRecent(args[0]);
|
|
1306
1482
|
if (messages.length <= keepRecent + 2) {
|
|
1307
1483
|
ctx.app.notify(`Nothing to compact — only ${messages.length} message(s) in this session.`);
|
|
1308
1484
|
break;
|
|
@@ -1646,10 +1822,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1646
1822
|
ctx.app.notify(`Skill ${name} not found`);
|
|
1647
1823
|
break;
|
|
1648
1824
|
}
|
|
1649
|
-
ctx.app.addMessage({
|
|
1650
|
-
role: 'system',
|
|
1651
|
-
content: `# ${bundle.name}\n_${bundle.description}_\n\n**Source:** ${bundle.source}\n\n---\n\n${bundle.body}`,
|
|
1652
|
-
});
|
|
1825
|
+
ctx.app.addMessage({ role: 'system', content: formatSkillsShow(bundle) });
|
|
1653
1826
|
break;
|
|
1654
1827
|
}
|
|
1655
1828
|
// Marketplace operations against codeep.dev.
|
|
@@ -1667,10 +1840,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1667
1840
|
ctx.app.notify(`Publish failed: ${result.error}`);
|
|
1668
1841
|
break;
|
|
1669
1842
|
}
|
|
1670
|
-
ctx.app.addMessage({
|
|
1671
|
-
role: 'system',
|
|
1672
|
-
content: `Published \`${slug}\` (${isPublic ? 'public' : 'private'}) to codeep.dev. Install elsewhere with \`/skills install ${result.skill?.owner_username ?? '<you>'}/${slug}\`.`,
|
|
1673
|
-
});
|
|
1843
|
+
ctx.app.addMessage({ role: 'system', content: formatSkillsPublishResult(slug, isPublic, result.skill?.owner_username) });
|
|
1674
1844
|
break;
|
|
1675
1845
|
}
|
|
1676
1846
|
if (sub === 'install') {
|
|
@@ -1700,7 +1870,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1700
1870
|
}
|
|
1701
1871
|
const skills = result.skills ?? [];
|
|
1702
1872
|
if (skills.length === 0) {
|
|
1703
|
-
ctx.app.addMessage({ role: 'system', content: query
|
|
1873
|
+
ctx.app.addMessage({ role: 'system', content: formatSkillsBrowseEmpty(query) });
|
|
1704
1874
|
break;
|
|
1705
1875
|
}
|
|
1706
1876
|
const lines = [`# ${query ? `Skills matching "${query}"` : 'Public skills'}`, ''];
|
|
@@ -1894,31 +2064,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1894
2064
|
// as the description — the same field the dashboard + macOS app set, and
|
|
1895
2065
|
// which the list view and the agent task-context prompt already render.
|
|
1896
2066
|
if (subCmd === 'add') {
|
|
1897
|
-
const
|
|
1898
|
-
let type = 'task';
|
|
1899
|
-
const titleWords = [];
|
|
1900
|
-
const descWords = [];
|
|
1901
|
-
let capturingDesc = false;
|
|
1902
|
-
for (const w of args.slice(1)) {
|
|
1903
|
-
const flag = /^--([\w-]+)$/.exec(w);
|
|
1904
|
-
if (flag) {
|
|
1905
|
-
const name = flag[1].toLowerCase();
|
|
1906
|
-
if (name === 'desc' || name === 'description') {
|
|
1907
|
-
capturingDesc = true;
|
|
1908
|
-
continue;
|
|
1909
|
-
}
|
|
1910
|
-
if (TASK_TYPES.includes(name))
|
|
1911
|
-
type = name;
|
|
1912
|
-
capturingDesc = false; // any non-desc flag ends description capture
|
|
1913
|
-
continue;
|
|
1914
|
-
}
|
|
1915
|
-
if (capturingDesc)
|
|
1916
|
-
descWords.push(w);
|
|
1917
|
-
else
|
|
1918
|
-
titleWords.push(w);
|
|
1919
|
-
}
|
|
1920
|
-
const title = titleWords.join(' ').trim();
|
|
1921
|
-
const description = descWords.join(' ').trim();
|
|
2067
|
+
const { title, description, type } = parseTaskAddArgs(args.slice(1));
|
|
1922
2068
|
if (!title) {
|
|
1923
2069
|
ctx.app.notify('Usage: /tasks add <title> [--bug | --feature] [--desc <text>]');
|
|
1924
2070
|
break;
|
|
@@ -1963,18 +2109,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1963
2109
|
break;
|
|
1964
2110
|
}
|
|
1965
2111
|
setTaskContext(tasks);
|
|
1966
|
-
|
|
1967
|
-
const lines = [`## Tasks${projectName ? ` — ${projectName}` : ''}`, ''];
|
|
1968
|
-
tasks.forEach((t, i) => {
|
|
1969
|
-
const icon = TYPE_ICON[t.type] ?? '[task]';
|
|
1970
|
-
// In a global listing (not scoped to one project) tag each row with its
|
|
1971
|
-
// project so a mixed list is legible — matches the macOS/web task rows.
|
|
1972
|
-
const proj = !projectName && t.project_name ? ` _(${t.project_name})_` : '';
|
|
1973
|
-
lines.push(`${i + 1}. ${icon} ${t.title}${proj}${t.description ? `\n ${t.description}` : ''}`);
|
|
1974
|
-
});
|
|
1975
|
-
lines.push('', `*${tasks.length} pending task${tasks.length > 1 ? 's' : ''}. Use /tasks done <n> to mark complete.*`);
|
|
1976
|
-
lines.push('*Tasks loaded into agent context — agent will see them in the next message.*');
|
|
1977
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
|
|
2112
|
+
ctx.app.addMessage({ role: 'system', content: formatTaskList(tasks, projectName) });
|
|
1978
2113
|
break;
|
|
1979
2114
|
}
|
|
1980
2115
|
case 'profile': {
|
|
@@ -1986,7 +2121,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1986
2121
|
ctx.app.notify('No profiles saved. Use /profile save <name>');
|
|
1987
2122
|
}
|
|
1988
2123
|
else {
|
|
1989
|
-
ctx.app.addMessage({ role: 'system', content:
|
|
2124
|
+
ctx.app.addMessage({ role: 'system', content: formatProfileList(profiles) });
|
|
1990
2125
|
}
|
|
1991
2126
|
break;
|
|
1992
2127
|
}
|
|
@@ -2127,54 +2262,15 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2127
2262
|
case 'stats': {
|
|
2128
2263
|
const { getCostBreakdown, getSessionStats, formatTokenCount, getPricingTable, getCacheStats } = await import('../utils/tokenTracker.js');
|
|
2129
2264
|
const stats = getSessionStats();
|
|
2130
|
-
const
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
if (breakdown.length > 0) {
|
|
2140
|
-
lines.push('');
|
|
2141
|
-
lines.push('### By model');
|
|
2142
|
-
for (const b of breakdown) {
|
|
2143
|
-
const isFree = b.provider === 'ollama';
|
|
2144
|
-
const costStr = isFree ? 'free' : b.estimatedCost > 0 ? `~$${b.estimatedCost.toFixed(4)}` : '(no pricing data)';
|
|
2145
|
-
lines.push(`- **${b.model}** (${b.provider}): ${formatTokenCount(b.promptTokens)} in / ${formatTokenCount(b.completionTokens)} out — ${costStr}`);
|
|
2146
|
-
}
|
|
2147
|
-
lines.push('');
|
|
2148
|
-
const currentProvider = config.get('provider');
|
|
2149
|
-
if (currentProvider === 'ollama') {
|
|
2150
|
-
lines.push(`**Total: free · ${formatTokenCount(stats.totalTokens)} tokens**`);
|
|
2151
|
-
}
|
|
2152
|
-
else if (stats.estimatedCost > 0) {
|
|
2153
|
-
lines.push(`**Total: ~$${stats.estimatedCost.toFixed(4)}**`);
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
// Prompt caching — parity with /cost (the 2.0.2 caching section was
|
|
2157
|
-
// only wired into formatCostReport). Shown only when caching landed.
|
|
2158
|
-
const cache = getCacheStats();
|
|
2159
|
-
if (cache.cacheReadTokens > 0 || cache.cacheCreationTokens > 0) {
|
|
2160
|
-
lines.push('', '### Prompt caching');
|
|
2161
|
-
lines.push(`Cache reads: ${formatTokenCount(cache.cacheReadTokens)} tokens (billed at 0.1× input rate)`);
|
|
2162
|
-
if (cache.cacheCreationTokens > 0) {
|
|
2163
|
-
lines.push(`Cache writes: ${formatTokenCount(cache.cacheCreationTokens)} tokens (billed at 1.25× input rate)`);
|
|
2164
|
-
}
|
|
2165
|
-
if (cache.estimatedSavingsUsd > 0) {
|
|
2166
|
-
lines.push(`Estimated savings vs no caching: $${cache.estimatedSavingsUsd.toFixed(4)}`);
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
lines.push('');
|
|
2170
|
-
}
|
|
2171
|
-
lines.push('### Pricing (per 1M tokens)');
|
|
2172
|
-
lines.push('| Model | Input | Output |');
|
|
2173
|
-
lines.push('|---|---|---|');
|
|
2174
|
-
for (const p of getPricingTable()) {
|
|
2175
|
-
lines.push(`| ${p.model} | $${p.inputPer1M.toFixed(3)} | $${p.outputPer1M.toFixed(3)} |`);
|
|
2176
|
-
}
|
|
2177
|
-
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 });
|
|
2178
2274
|
break;
|
|
2179
2275
|
}
|
|
2180
2276
|
case 'memory': {
|
|
@@ -2192,8 +2288,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2192
2288
|
ctx.app.notify('No memory notes. Add one with: /memory <note>');
|
|
2193
2289
|
}
|
|
2194
2290
|
else {
|
|
2195
|
-
|
|
2196
|
-
ctx.app.addMessage({ role: 'assistant', content: `**Project memory notes:**\n${lines}` });
|
|
2291
|
+
ctx.app.addMessage({ role: 'assistant', content: formatMemoryList(intelligence.notes) });
|
|
2197
2292
|
}
|
|
2198
2293
|
break;
|
|
2199
2294
|
}
|
|
@@ -2234,6 +2329,29 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2234
2329
|
ctx.app.addMessage({ role: 'system', content: formatCommandList(loadCustomCommands(ctx.projectPath)) });
|
|
2235
2330
|
break;
|
|
2236
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
|
+
}
|
|
2237
2355
|
case 'mcp': {
|
|
2238
2356
|
// Mirrors the ACP `/mcp` handler in src/acp/commands.ts. In TUI the
|
|
2239
2357
|
// session id is the constant `codeep-tui` (the same one main.ts uses
|
|
@@ -2251,8 +2369,36 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2251
2369
|
}
|
|
2252
2370
|
return true;
|
|
2253
2371
|
};
|
|
2254
|
-
const { addProjectMcpServer, removeProjectMcpServer, loadMcpServerConfig } = await import('../utils/mcpConfig.js');
|
|
2372
|
+
const { addProjectMcpServer, removeProjectMcpServer, loadMcpServerConfig, loadMcpServerConfigSplit, isWorkspaceMcpTrusted, trustWorkspaceMcp, untrustWorkspaceMcp } = await import('../utils/mcpConfig.js');
|
|
2255
2373
|
const { registerSessionServers } = await import('../utils/mcpRegistry.js');
|
|
2374
|
+
if (sub === 'trust') {
|
|
2375
|
+
if (!requireProject())
|
|
2376
|
+
break;
|
|
2377
|
+
if (isWorkspaceMcpTrusted(projectPath)) {
|
|
2378
|
+
ctx.app.notify('Workspace MCP servers are already trusted here.');
|
|
2379
|
+
break;
|
|
2380
|
+
}
|
|
2381
|
+
trustWorkspaceMcp(projectPath);
|
|
2382
|
+
const { workspace } = loadMcpServerConfigSplit(projectPath);
|
|
2383
|
+
if (workspace.length === 0) {
|
|
2384
|
+
ctx.app.notify('Workspace trusted — no workspace MCP servers defined yet.');
|
|
2385
|
+
break;
|
|
2386
|
+
}
|
|
2387
|
+
ctx.app.notify(`Workspace trusted. Spawning ${workspace.length} MCP server(s)…`);
|
|
2388
|
+
const { registered, errors } = await registerSessionServers(TUI_SESSION, workspace, { workspaceRoot: projectPath });
|
|
2389
|
+
if (registered.length > 0)
|
|
2390
|
+
ctx.app.notify(`MCP: ${registered.length} tool(s) ready. Type /mcp.`);
|
|
2391
|
+
for (const e of errors)
|
|
2392
|
+
ctx.app.notifyWarn(`MCP server "${e.server}" failed: ${e.error}`);
|
|
2393
|
+
break;
|
|
2394
|
+
}
|
|
2395
|
+
if (sub === 'untrust') {
|
|
2396
|
+
if (!requireProject())
|
|
2397
|
+
break;
|
|
2398
|
+
untrustWorkspaceMcp(projectPath);
|
|
2399
|
+
ctx.app.notify('Workspace MCP trust revoked — workspace servers won\'t spawn on next start. (Running servers stop when you exit.)');
|
|
2400
|
+
break;
|
|
2401
|
+
}
|
|
2256
2402
|
if (sub === 'add') {
|
|
2257
2403
|
if (!requireProject())
|
|
2258
2404
|
break;
|
|
@@ -2364,35 +2510,14 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2364
2510
|
ctx.app.notify('Reloading MCP server config…');
|
|
2365
2511
|
const merged = loadMcpServerConfig(projectPath);
|
|
2366
2512
|
const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
|
|
2367
|
-
|
|
2368
|
-
if (errors.length > 0) {
|
|
2369
|
-
lines.push('', '### Failed servers');
|
|
2370
|
-
for (const e of errors)
|
|
2371
|
-
lines.push(`- **${e.server}** — \`${e.error}\``);
|
|
2372
|
-
}
|
|
2373
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
|
|
2513
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpReloadReport(registered.length, merged.length, errors) });
|
|
2374
2514
|
break;
|
|
2375
2515
|
}
|
|
2376
2516
|
if (sub === 'resources') {
|
|
2377
2517
|
const { getSessionResources, awaitSessionReady } = await import('../utils/mcpRegistry.js');
|
|
2378
2518
|
await awaitSessionReady(TUI_SESSION);
|
|
2379
2519
|
const groups = await getSessionResources(TUI_SESSION);
|
|
2380
|
-
|
|
2381
|
-
ctx.app.addMessage({ role: 'system', content: '_No MCP server in this session exposes resources._' });
|
|
2382
|
-
break;
|
|
2383
|
-
}
|
|
2384
|
-
const lines = ['## MCP resources', ''];
|
|
2385
|
-
for (const g of groups) {
|
|
2386
|
-
lines.push(`**${g.serverName}** — ${g.resources.length} resource${g.resources.length === 1 ? '' : 's'}`);
|
|
2387
|
-
for (const r of g.resources) {
|
|
2388
|
-
const label = r.name ? `${r.name} — ` : '';
|
|
2389
|
-
const mime = r.mimeType ? ` (${r.mimeType})` : '';
|
|
2390
|
-
lines.push(`- ${label}\`${r.uri}\`${mime}${r.description ? ` — ${r.description}` : ''}`);
|
|
2391
|
-
}
|
|
2392
|
-
lines.push('');
|
|
2393
|
-
}
|
|
2394
|
-
lines.push('Read one with `/mcp read <uri>`.');
|
|
2395
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
|
|
2520
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpResourcesList(groups) });
|
|
2396
2521
|
break;
|
|
2397
2522
|
}
|
|
2398
2523
|
if (sub === 'read') {
|
|
@@ -2404,23 +2529,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2404
2529
|
const { readSessionResource } = await import('../utils/mcpRegistry.js');
|
|
2405
2530
|
try {
|
|
2406
2531
|
const contents = await readSessionResource(TUI_SESSION, uri);
|
|
2407
|
-
|
|
2408
|
-
ctx.app.addMessage({ role: 'system', content: `_No content returned for \`${uri}\`._` });
|
|
2409
|
-
break;
|
|
2410
|
-
}
|
|
2411
|
-
const lines = [`## Resource: \`${uri}\``, ''];
|
|
2412
|
-
for (const c of contents) {
|
|
2413
|
-
if (c.text !== undefined) {
|
|
2414
|
-
const fence = c.mimeType?.includes('json') ? 'json' : c.mimeType?.includes('markdown') ? 'markdown' : '';
|
|
2415
|
-
lines.push('```' + fence);
|
|
2416
|
-
lines.push(c.text);
|
|
2417
|
-
lines.push('```');
|
|
2418
|
-
}
|
|
2419
|
-
else if (c.blob) {
|
|
2420
|
-
lines.push(`_(${c.mimeType ?? 'binary'} blob, ${c.blob.length} base64 chars — not rendered)_`);
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
|
|
2532
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpResourceRead(uri, contents) });
|
|
2424
2533
|
}
|
|
2425
2534
|
catch (err) {
|
|
2426
2535
|
ctx.app.addMessage({ role: 'system', content: `Failed to read \`${uri}\`: ${err.message}` });
|
|
@@ -2431,23 +2540,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2431
2540
|
const { getSessionPrompts, awaitSessionReady } = await import('../utils/mcpRegistry.js');
|
|
2432
2541
|
await awaitSessionReady(TUI_SESSION);
|
|
2433
2542
|
const groups = await getSessionPrompts(TUI_SESSION);
|
|
2434
|
-
|
|
2435
|
-
ctx.app.addMessage({ role: 'system', content: '_No MCP server in this session exposes prompt templates._' });
|
|
2436
|
-
break;
|
|
2437
|
-
}
|
|
2438
|
-
const lines = ['## MCP prompt templates', ''];
|
|
2439
|
-
for (const g of groups) {
|
|
2440
|
-
lines.push(`**${g.serverName}** — ${g.prompts.length} prompt${g.prompts.length === 1 ? '' : 's'}`);
|
|
2441
|
-
for (const p of g.prompts) {
|
|
2442
|
-
const argList = p.arguments?.length
|
|
2443
|
-
? ` (${p.arguments.map(a => a.required ? a.name : `[${a.name}]`).join(', ')})`
|
|
2444
|
-
: '';
|
|
2445
|
-
lines.push(`- \`${p.name}\`${argList}${p.description ? ` — ${p.description}` : ''}`);
|
|
2446
|
-
}
|
|
2447
|
-
lines.push('');
|
|
2448
|
-
}
|
|
2449
|
-
lines.push('Materialise one with `/mcp prompt <server> <name> [key=value...]`.');
|
|
2450
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
|
|
2543
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpPromptsList(groups) });
|
|
2451
2544
|
break;
|
|
2452
2545
|
}
|
|
2453
2546
|
if (sub === 'prompt') {
|
|
@@ -2457,25 +2550,11 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2457
2550
|
ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp prompt <server> <name> [key=value ...]`' });
|
|
2458
2551
|
break;
|
|
2459
2552
|
}
|
|
2460
|
-
const promptArgs =
|
|
2461
|
-
for (const tok of args.slice(3)) {
|
|
2462
|
-
const eq = tok.indexOf('=');
|
|
2463
|
-
if (eq > 0)
|
|
2464
|
-
promptArgs[tok.slice(0, eq)] = tok.slice(eq + 1);
|
|
2465
|
-
}
|
|
2553
|
+
const promptArgs = parsePromptArgs(args.slice(3));
|
|
2466
2554
|
const { getSessionPrompt } = await import('../utils/mcpRegistry.js');
|
|
2467
2555
|
try {
|
|
2468
2556
|
const { description, messages } = await getSessionPrompt(TUI_SESSION, serverName, name, promptArgs);
|
|
2469
|
-
|
|
2470
|
-
if (description)
|
|
2471
|
-
lines.push(`_${description}_`);
|
|
2472
|
-
lines.push('');
|
|
2473
|
-
for (const m of messages) {
|
|
2474
|
-
const text = typeof m.content?.text === 'string' ? m.content.text : JSON.stringify(m.content);
|
|
2475
|
-
lines.push(`**${m.role}:** ${text}`);
|
|
2476
|
-
lines.push('');
|
|
2477
|
-
}
|
|
2478
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
|
|
2557
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpPromptResult(serverName, name, description, messages) });
|
|
2479
2558
|
}
|
|
2480
2559
|
catch (err) {
|
|
2481
2560
|
ctx.app.addMessage({ role: 'system', content: `Failed to materialise prompt: ${err.message}` });
|
|
@@ -2487,41 +2566,7 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
2487
2566
|
await awaitSessionReady(TUI_SESSION);
|
|
2488
2567
|
const tools = await getSessionTools(TUI_SESSION);
|
|
2489
2568
|
const mcpErrors = getSessionRegistrationErrors(TUI_SESSION);
|
|
2490
|
-
|
|
2491
|
-
ctx.app.addMessage({
|
|
2492
|
-
role: 'system',
|
|
2493
|
-
content: [
|
|
2494
|
-
'_No MCP servers connected to this session._',
|
|
2495
|
-
'',
|
|
2496
|
-
'Add one with `/mcp add <name> <command> [args...]` — it persists to `.codeep/mcp_servers.json`.',
|
|
2497
|
-
'Or browse the marketplace with `/mcp browse` and install with `/mcp install <id>`.',
|
|
2498
|
-
].join('\n'),
|
|
2499
|
-
});
|
|
2500
|
-
break;
|
|
2501
|
-
}
|
|
2502
|
-
const lines = ['## MCP servers', ''];
|
|
2503
|
-
if (tools.length > 0) {
|
|
2504
|
-
const byServer = new Map();
|
|
2505
|
-
for (const t of tools) {
|
|
2506
|
-
if (!byServer.has(t.serverName))
|
|
2507
|
-
byServer.set(t.serverName, []);
|
|
2508
|
-
byServer.get(t.serverName).push(t);
|
|
2509
|
-
}
|
|
2510
|
-
for (const [serverName, serverTools] of byServer) {
|
|
2511
|
-
lines.push(`**${serverName}** — ${serverTools.length} tool${serverTools.length === 1 ? '' : 's'}`);
|
|
2512
|
-
for (const t of serverTools) {
|
|
2513
|
-
const desc = t.description ? ` — ${t.description}` : '';
|
|
2514
|
-
lines.push(`- \`${t.agentName}\`${desc}`);
|
|
2515
|
-
}
|
|
2516
|
-
lines.push('');
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
if (mcpErrors.length > 0) {
|
|
2520
|
-
lines.push('### Failed servers');
|
|
2521
|
-
for (const e of mcpErrors)
|
|
2522
|
-
lines.push(`- **${e.server}** — \`${e.error}\``);
|
|
2523
|
-
}
|
|
2524
|
-
ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
|
|
2569
|
+
ctx.app.addMessage({ role: 'system', content: formatMcpServerList(tools, mcpErrors) });
|
|
2525
2570
|
break;
|
|
2526
2571
|
}
|
|
2527
2572
|
default: {
|