codeep 2.0.0 → 2.0.2
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/dist/acp/commands.js +56 -0
- package/dist/acp/server.js +3 -0
- package/dist/api/index.js +25 -5
- package/dist/renderer/App.js +17 -0
- package/dist/renderer/commands.js +349 -0
- package/dist/renderer/components/Help.js +30 -0
- package/dist/utils/agentChat.js +24 -3
- package/dist/utils/agentStream.js +21 -4
- package/dist/utils/planMode.d.ts +45 -0
- package/dist/utils/planMode.js +94 -0
- package/dist/utils/skillBundlesCloud.d.ts +7 -4
- package/dist/utils/skillBundlesCloud.js +12 -6
- package/dist/utils/tokenTracker.d.ts +23 -0
- package/dist/utils/tokenTracker.js +56 -4
- package/package.json +1 -1
package/dist/acp/commands.js
CHANGED
|
@@ -600,6 +600,62 @@ Anything else the agent should know — edge cases, gotchas, things to double-ch
|
|
|
600
600
|
}
|
|
601
601
|
}
|
|
602
602
|
// ─── Export ────────────────────────────────────────────────────────────────
|
|
603
|
+
// ─── Plan mode (2.0.2) ────────────────────────────────────────────────────
|
|
604
|
+
case 'plan': {
|
|
605
|
+
// Identical contract to TUI /plan: generate a pre-execution plan,
|
|
606
|
+
// surface it, hold as pending so /go can execute it without re-planning.
|
|
607
|
+
if (!args.length) {
|
|
608
|
+
const { getPendingPlan } = await import('../utils/planMode.js');
|
|
609
|
+
const cur = getPendingPlan();
|
|
610
|
+
return {
|
|
611
|
+
handled: true,
|
|
612
|
+
response: cur
|
|
613
|
+
? `**Pending plan for:** _${cur.task}_\n\n${cur.plan}\n\n---\nRun \`/go\` to execute, or \`/plan <revised task>\` to revise.`
|
|
614
|
+
: 'Usage: `/plan <task>` — generates a plan you can review, then `/go` to execute.',
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const task = args.join(' ');
|
|
618
|
+
onChunk(`_Generating plan for: ${task.slice(0, 80)}${task.length > 80 ? '…' : ''}_\n\n`);
|
|
619
|
+
try {
|
|
620
|
+
const { generatePlan } = await import('../utils/planMode.js');
|
|
621
|
+
const plan = await generatePlan(task);
|
|
622
|
+
return {
|
|
623
|
+
handled: true,
|
|
624
|
+
response: `${plan}\n\n---\nRun \`/go\` to execute this plan, or \`/plan <revised task>\` to refine it.`,
|
|
625
|
+
streaming: true,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
return { handled: true, response: `Plan generation failed: ${err.message}`, streaming: true };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
case 'go': {
|
|
633
|
+
const { getPendingPlan, composeExecutionPrompt, clearPendingPlan } = await import('../utils/planMode.js');
|
|
634
|
+
const cur = getPendingPlan();
|
|
635
|
+
if (!cur) {
|
|
636
|
+
return { handled: true, response: 'No pending plan. Run `/plan <task>` first.' };
|
|
637
|
+
}
|
|
638
|
+
const prompt = composeExecutionPrompt(cur);
|
|
639
|
+
clearPendingPlan();
|
|
640
|
+
onChunk(`_Executing approved plan…_\n\n`);
|
|
641
|
+
try {
|
|
642
|
+
const { buildProjectContext } = await import('./session.js');
|
|
643
|
+
const ctx = buildProjectContext(session.workspaceRoot);
|
|
644
|
+
const agentResult = await runAgent(prompt, ctx, {
|
|
645
|
+
abortSignal,
|
|
646
|
+
onIteration: (_i, msg) => { onChunk(msg + '\n'); },
|
|
647
|
+
onThinking: (text) => { onChunk(text); },
|
|
648
|
+
});
|
|
649
|
+
return {
|
|
650
|
+
handled: true,
|
|
651
|
+
response: agentResult.finalResponse || '_(plan executed; no final summary)_',
|
|
652
|
+
streaming: true,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
return { handled: true, response: `Plan execution failed: ${err.message}`, streaming: true };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
603
659
|
case 'export': {
|
|
604
660
|
if (!session.history.length)
|
|
605
661
|
return { handled: true, response: 'No messages to export.' };
|
package/dist/acp/server.js
CHANGED
|
@@ -52,6 +52,9 @@ const AVAILABLE_COMMANDS = [
|
|
|
52
52
|
{ name: 'mcp', description: 'Manage MCP servers, marketplace, resources, prompts', input: { hint: '[browse | install <id> | add | remove | reload | resources | read <uri> | prompts | prompt <server> <name>]' } },
|
|
53
53
|
{ name: 'openrouter', description: 'OpenRouter routing preferences (prefer/ignore/fallbacks/privacy/clear)', input: { hint: '[show | prefer <p,...> | ignore <p,...> | fallbacks on|off | privacy strict|allow | clear]' } },
|
|
54
54
|
{ name: 'export', description: 'Export conversation', input: { hint: 'json | md | txt' } },
|
|
55
|
+
// Plan mode (2.0.2)
|
|
56
|
+
{ name: 'plan', description: 'Generate a numbered plan for a task — review before /go executes', input: { hint: '<task>' } },
|
|
57
|
+
{ name: 'go', description: 'Execute the pending plan from /plan' },
|
|
55
58
|
// Project intelligence
|
|
56
59
|
{ name: 'scan', description: 'Scan project structure and generate summary' },
|
|
57
60
|
{ name: 'review', description: 'Run code review on project or specific files', input: { hint: '[file…]' } },
|
package/dist/api/index.js
CHANGED
|
@@ -593,6 +593,13 @@ async function chatAnthropic(message, history, model, apiKey, onChunk, abortSign
|
|
|
593
593
|
headers['x-api-key'] = apiKey;
|
|
594
594
|
}
|
|
595
595
|
try {
|
|
596
|
+
// Anthropic prompt caching: wrap system as an array with a
|
|
597
|
+
// `cache_control` marker so the static system prompt (typically large
|
|
598
|
+
// and stable across a session) is cached. Below 1024 input tokens
|
|
599
|
+
// Anthropic silently skips caching — no error.
|
|
600
|
+
const cachedSystem = useNativeSystem
|
|
601
|
+
? { system: [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }] }
|
|
602
|
+
: {};
|
|
596
603
|
const response = await fetch(`${baseUrl}/v1/messages`, {
|
|
597
604
|
method: 'POST',
|
|
598
605
|
headers,
|
|
@@ -602,7 +609,7 @@ async function chatAnthropic(message, history, model, apiKey, onChunk, abortSign
|
|
|
602
609
|
max_tokens: maxTokens,
|
|
603
610
|
temperature,
|
|
604
611
|
stream,
|
|
605
|
-
...
|
|
612
|
+
...cachedSystem,
|
|
606
613
|
}),
|
|
607
614
|
signal: controller.signal,
|
|
608
615
|
});
|
|
@@ -649,6 +656,8 @@ async function handleAnthropicStream(body, onChunk) {
|
|
|
649
656
|
let buffer = '';
|
|
650
657
|
let inputTokens = 0;
|
|
651
658
|
let outputTokens = 0;
|
|
659
|
+
let cacheCreationTokens = 0;
|
|
660
|
+
let cacheReadTokens = 0;
|
|
652
661
|
let streamModel = '';
|
|
653
662
|
while (true) {
|
|
654
663
|
const { done, value } = await reader.read();
|
|
@@ -669,9 +678,13 @@ async function handleAnthropicStream(body, onChunk) {
|
|
|
669
678
|
onChunk(text);
|
|
670
679
|
}
|
|
671
680
|
}
|
|
672
|
-
// message_start contains input_tokens
|
|
681
|
+
// message_start contains input_tokens (and cache create/read
|
|
682
|
+
// when prompt caching is in play).
|
|
673
683
|
if (parsed.type === 'message_start' && parsed.message?.usage) {
|
|
674
|
-
|
|
684
|
+
const u = parsed.message.usage;
|
|
685
|
+
inputTokens = u.input_tokens || 0;
|
|
686
|
+
cacheCreationTokens = u.cache_creation_input_tokens || 0;
|
|
687
|
+
cacheReadTokens = u.cache_read_input_tokens || 0;
|
|
675
688
|
streamModel = parsed.message.model || '';
|
|
676
689
|
}
|
|
677
690
|
// message_delta contains output_tokens
|
|
@@ -686,8 +699,15 @@ async function handleAnthropicStream(body, onChunk) {
|
|
|
686
699
|
}
|
|
687
700
|
}
|
|
688
701
|
// Record token usage
|
|
689
|
-
if (inputTokens > 0 || outputTokens > 0) {
|
|
690
|
-
|
|
702
|
+
if (inputTokens > 0 || outputTokens > 0 || cacheReadTokens > 0 || cacheCreationTokens > 0) {
|
|
703
|
+
const totalPrompt = inputTokens + cacheCreationTokens + cacheReadTokens;
|
|
704
|
+
recordTokenUsage({
|
|
705
|
+
promptTokens: totalPrompt,
|
|
706
|
+
completionTokens: outputTokens,
|
|
707
|
+
totalTokens: totalPrompt + outputTokens,
|
|
708
|
+
cacheCreationTokens: cacheCreationTokens || undefined,
|
|
709
|
+
cacheReadTokens: cacheReadTokens || undefined,
|
|
710
|
+
}, streamModel || 'unknown', config.get('provider'));
|
|
691
711
|
}
|
|
692
712
|
// Strip <think> tags from MiniMax responses
|
|
693
713
|
return stripThinkTags(chunks.join(''));
|
package/dist/renderer/App.js
CHANGED
|
@@ -82,6 +82,17 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
82
82
|
'profile': 'Save/load settings profiles',
|
|
83
83
|
'tasks': 'Show pending tasks from codeep.dev dashboard',
|
|
84
84
|
'sync': 'Sync learning preferences and profiles to codeep.dev',
|
|
85
|
+
// 2.0 — surfaced for `/` autocomplete; documented in /help too.
|
|
86
|
+
'compact': 'Summarize older messages to free up context',
|
|
87
|
+
'commands': 'List custom slash commands in .codeep/commands/*.md',
|
|
88
|
+
'checkpoint': 'Snapshot the session (conversation + provider/model + git HEAD)',
|
|
89
|
+
'checkpoints': 'List saved checkpoints for this workspace',
|
|
90
|
+
'rewind': 'Restore conversation from a saved checkpoint',
|
|
91
|
+
'hooks': 'List installed lifecycle hooks (.codeep/hooks/<event>.sh)',
|
|
92
|
+
'mcp': 'Manage MCP servers (browse, install, add, remove, resources, prompts)',
|
|
93
|
+
'openrouter': 'Tune OpenRouter routing (preferred / ignore providers, fallbacks, privacy)',
|
|
94
|
+
'plan': 'Generate a numbered plan for a task — review before /go executes it',
|
|
95
|
+
'go': 'Execute the pending plan from /plan',
|
|
85
96
|
};
|
|
86
97
|
import { helpCategories, keyboardShortcuts } from './components/Help.js';
|
|
87
98
|
import { handleSettingsKey, SETTINGS } from './components/Settings.js';
|
|
@@ -217,6 +228,12 @@ export class App {
|
|
|
217
228
|
'provider', 'model', 'protocol', 'lang', 'grant', 'login', 'logout',
|
|
218
229
|
'context-save', 'context-load', 'context-clear', 'learn',
|
|
219
230
|
'cost', 'tasks', 'account', 'sync',
|
|
231
|
+
// 2.0 — extensions, checkpoints, MCP, custom commands, OpenRouter prefs.
|
|
232
|
+
// Keep in lockstep with COMMAND_DESCRIPTIONS below and helpCategories.
|
|
233
|
+
'compact', 'commands', 'checkpoint', 'checkpoints', 'rewind',
|
|
234
|
+
'hooks', 'mcp', 'openrouter',
|
|
235
|
+
// 2.0.2 — plan mode.
|
|
236
|
+
'plan', 'go',
|
|
220
237
|
'c', 't', 'd', 'r', 'f', 'e', 'o', 'b', 'p',
|
|
221
238
|
];
|
|
222
239
|
constructor(options) {
|
|
@@ -216,6 +216,65 @@ export async function handleCommand(command, args, ctx) {
|
|
|
216
216
|
runAgentTask(args.join(' '), true, ctx, () => null, () => { });
|
|
217
217
|
break;
|
|
218
218
|
}
|
|
219
|
+
case 'plan': {
|
|
220
|
+
// Plan mode: ask the model for a plan, surface it, hold as pending.
|
|
221
|
+
// The user runs /go to execute or /plan <revised> to revise. See
|
|
222
|
+
// src/utils/planMode.ts for the rationale + system prompt.
|
|
223
|
+
if (!args.length) {
|
|
224
|
+
const { getPendingPlan } = await import('../utils/planMode.js');
|
|
225
|
+
const cur = getPendingPlan();
|
|
226
|
+
if (cur) {
|
|
227
|
+
ctx.app.addMessage({
|
|
228
|
+
role: 'system',
|
|
229
|
+
content: `**Pending plan for:** _${cur.task}_\n\n${cur.plan}\n\n---\nRun \`/go\` to execute, or \`/plan <revised task>\` to revise.`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
ctx.app.notify('Usage: /plan <task> — generates a plan you can review, then /go to execute.');
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (ctx.isAgentRunning()) {
|
|
238
|
+
ctx.app.notify('Agent already running. Use /stop first.');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const task = args.join(' ');
|
|
242
|
+
ctx.app.addMessage({ role: 'user', content: `/plan ${task}` });
|
|
243
|
+
ctx.app.notify('Generating plan…');
|
|
244
|
+
try {
|
|
245
|
+
const { generatePlan } = await import('../utils/planMode.js');
|
|
246
|
+
const plan = await generatePlan(task);
|
|
247
|
+
ctx.app.addMessage({
|
|
248
|
+
role: 'assistant',
|
|
249
|
+
content: `${plan}\n\n---\nRun \`/go\` to execute this plan, or \`/plan <revised task>\` to refine it.`,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
ctx.app.notify(`Plan generation failed: ${err.message}`);
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case 'go': {
|
|
258
|
+
// Execute the pending plan from /plan. The agent loop sees the
|
|
259
|
+
// task + plan as a single prompt, so MCP tools, hooks, permissions,
|
|
260
|
+
// and verification all apply unchanged.
|
|
261
|
+
const { getPendingPlan, composeExecutionPrompt, clearPendingPlan } = await import('../utils/planMode.js');
|
|
262
|
+
const cur = getPendingPlan();
|
|
263
|
+
if (!cur) {
|
|
264
|
+
ctx.app.notify('No pending plan. Run `/plan <task>` first.');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (ctx.isAgentRunning()) {
|
|
268
|
+
ctx.app.notify('Agent already running. Use /stop first.');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const prompt = composeExecutionPrompt(cur);
|
|
272
|
+
clearPendingPlan();
|
|
273
|
+
ctx.app.notify(`Executing plan for: ${cur.task.slice(0, 80)}${cur.task.length > 80 ? '…' : ''}`);
|
|
274
|
+
const { runAgentTask } = await import('./agentExecution.js');
|
|
275
|
+
runAgentTask(prompt, false, ctx, () => null, () => { });
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
219
278
|
case 'stop': {
|
|
220
279
|
if (ctx.isAgentRunning() && ctx.abortController) {
|
|
221
280
|
ctx.abortController.abort();
|
|
@@ -1685,6 +1744,296 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
|
|
|
1685
1744
|
ctx.app.addMessage({ role: 'system', content: formatCommandList(loadCustomCommands(ctx.projectPath)) });
|
|
1686
1745
|
break;
|
|
1687
1746
|
}
|
|
1747
|
+
case 'mcp': {
|
|
1748
|
+
// Mirrors the ACP `/mcp` handler in src/acp/commands.ts. In TUI the
|
|
1749
|
+
// session id is the constant `codeep-tui` (the same one main.ts uses
|
|
1750
|
+
// when it spawns project MCP servers in the background) and the
|
|
1751
|
+
// workspace root is ctx.projectPath. Without a project we can still
|
|
1752
|
+
// browse the marketplace, but anything that mutates project config
|
|
1753
|
+
// refuses with a clear message.
|
|
1754
|
+
const sub = args[0]?.toLowerCase();
|
|
1755
|
+
const TUI_SESSION = 'codeep-tui';
|
|
1756
|
+
const projectPath = ctx.projectPath;
|
|
1757
|
+
const requireProject = () => {
|
|
1758
|
+
if (!projectPath) {
|
|
1759
|
+
ctx.app.notify('Open a project (cd into it before running codeep) to add or modify MCP servers.');
|
|
1760
|
+
return false;
|
|
1761
|
+
}
|
|
1762
|
+
return true;
|
|
1763
|
+
};
|
|
1764
|
+
const { addProjectMcpServer, removeProjectMcpServer, loadMcpServerConfig } = await import('../utils/mcpConfig.js');
|
|
1765
|
+
const { registerSessionServers } = await import('../utils/mcpRegistry.js');
|
|
1766
|
+
if (sub === 'add') {
|
|
1767
|
+
if (!requireProject())
|
|
1768
|
+
break;
|
|
1769
|
+
const name = args[1];
|
|
1770
|
+
const command = args[2];
|
|
1771
|
+
if (!name || !command) {
|
|
1772
|
+
ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp add <name> <command> [args...]` — e.g. `/mcp add fs npx @modelcontextprotocol/server-filesystem /path`' });
|
|
1773
|
+
break;
|
|
1774
|
+
}
|
|
1775
|
+
const extraArgs = args.slice(3);
|
|
1776
|
+
addProjectMcpServer(projectPath, { name, command, args: extraArgs });
|
|
1777
|
+
ctx.app.notify(`Saved MCP server ${name} to .codeep/mcp_servers.json. Spawning…`);
|
|
1778
|
+
const merged = loadMcpServerConfig(projectPath);
|
|
1779
|
+
const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
|
|
1780
|
+
const ok = registered.filter(t => t.serverName === name);
|
|
1781
|
+
const failed = errors.find(e => e.server === name);
|
|
1782
|
+
ctx.app.addMessage({
|
|
1783
|
+
role: 'system',
|
|
1784
|
+
content: failed
|
|
1785
|
+
? `Saved \`${name}\` but spawn failed: \`${failed.error}\``
|
|
1786
|
+
: `Added \`${name}\` (${ok.length} tool${ok.length === 1 ? '' : 's'} available).`,
|
|
1787
|
+
});
|
|
1788
|
+
break;
|
|
1789
|
+
}
|
|
1790
|
+
if (sub === 'remove') {
|
|
1791
|
+
if (!requireProject())
|
|
1792
|
+
break;
|
|
1793
|
+
const name = args[1];
|
|
1794
|
+
if (!name) {
|
|
1795
|
+
ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp remove <name>`' });
|
|
1796
|
+
break;
|
|
1797
|
+
}
|
|
1798
|
+
const removed = removeProjectMcpServer(projectPath, name);
|
|
1799
|
+
if (!removed) {
|
|
1800
|
+
ctx.app.addMessage({ role: 'system', content: `No project-scoped MCP server named \`${name}\`.` });
|
|
1801
|
+
break;
|
|
1802
|
+
}
|
|
1803
|
+
const merged = loadMcpServerConfig(projectPath);
|
|
1804
|
+
await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
|
|
1805
|
+
ctx.app.addMessage({ role: 'system', content: `Removed \`${name}\` from project config and stopped its process.` });
|
|
1806
|
+
break;
|
|
1807
|
+
}
|
|
1808
|
+
if (sub === 'browse') {
|
|
1809
|
+
const { formatMarketplaceList, findMarketplaceEntry, formatMarketplaceEntry, MCP_MARKETPLACE } = await import('../utils/mcpMarketplace.js');
|
|
1810
|
+
const detail = args[1];
|
|
1811
|
+
if (detail) {
|
|
1812
|
+
const entry = findMarketplaceEntry(detail);
|
|
1813
|
+
if (!entry) {
|
|
1814
|
+
ctx.app.addMessage({ role: 'system', content: `Marketplace id not found: \`${detail}\`. Run \`/mcp browse\` for the list.` });
|
|
1815
|
+
}
|
|
1816
|
+
else {
|
|
1817
|
+
const argHints = entry.argHints?.map(h => `<${h.placeholder ?? 'arg'}>`).join(' ') ?? '';
|
|
1818
|
+
ctx.app.addMessage({ role: 'system', content: formatMarketplaceEntry(entry) + `\n\nInstall with \`/mcp install ${entry.id} ${argHints}\`` });
|
|
1819
|
+
}
|
|
1820
|
+
break;
|
|
1821
|
+
}
|
|
1822
|
+
ctx.app.addMessage({ role: 'system', content: formatMarketplaceList() + `\n\nRun \`/mcp browse <id>\` for details or \`/mcp install <id> [args]\` to install. Total: ${MCP_MARKETPLACE.length}.` });
|
|
1823
|
+
break;
|
|
1824
|
+
}
|
|
1825
|
+
if (sub === 'install') {
|
|
1826
|
+
if (!requireProject())
|
|
1827
|
+
break;
|
|
1828
|
+
const id = args[1];
|
|
1829
|
+
if (!id) {
|
|
1830
|
+
ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp install <id> [extra args...]` — run `/mcp browse` to see ids.' });
|
|
1831
|
+
break;
|
|
1832
|
+
}
|
|
1833
|
+
const { findMarketplaceEntry } = await import('../utils/mcpMarketplace.js');
|
|
1834
|
+
const entry = findMarketplaceEntry(id);
|
|
1835
|
+
if (!entry) {
|
|
1836
|
+
ctx.app.addMessage({ role: 'system', content: `Marketplace id not found: \`${id}\`. Run \`/mcp browse\` for the list.` });
|
|
1837
|
+
break;
|
|
1838
|
+
}
|
|
1839
|
+
const extraArgs = args.slice(2);
|
|
1840
|
+
const fullArgs = [...(entry.server.args ?? []), ...extraArgs];
|
|
1841
|
+
addProjectMcpServer(projectPath, {
|
|
1842
|
+
name: entry.id,
|
|
1843
|
+
command: entry.server.command,
|
|
1844
|
+
args: fullArgs,
|
|
1845
|
+
env: entry.server.env,
|
|
1846
|
+
url: entry.server.url,
|
|
1847
|
+
headers: entry.server.headers,
|
|
1848
|
+
});
|
|
1849
|
+
ctx.app.notify(`Saved ${entry.id} to project config. Spawning…`);
|
|
1850
|
+
const merged = loadMcpServerConfig(projectPath);
|
|
1851
|
+
const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
|
|
1852
|
+
const failed = errors.find(e => e.server === entry.id);
|
|
1853
|
+
const lines = [];
|
|
1854
|
+
if (failed) {
|
|
1855
|
+
lines.push(`Saved \`${entry.id}\` but spawn failed: \`${failed.error}\``);
|
|
1856
|
+
}
|
|
1857
|
+
else {
|
|
1858
|
+
const ok = registered.filter(t => t.serverName === entry.id);
|
|
1859
|
+
lines.push(`Installed **${entry.name}** (\`${entry.id}\`) — ${ok.length} tool${ok.length === 1 ? '' : 's'} available.`);
|
|
1860
|
+
}
|
|
1861
|
+
if (entry.envNotes?.length) {
|
|
1862
|
+
lines.push('', '**Environment variables you may need:**');
|
|
1863
|
+
for (const e of entry.envNotes) {
|
|
1864
|
+
const req = e.required ? ' (required)' : '';
|
|
1865
|
+
lines.push(`- \`${e.name}\`${req} — ${e.description}`);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
if (sub === 'reload') {
|
|
1872
|
+
if (!requireProject())
|
|
1873
|
+
break;
|
|
1874
|
+
ctx.app.notify('Reloading MCP server config…');
|
|
1875
|
+
const merged = loadMcpServerConfig(projectPath);
|
|
1876
|
+
const { registered, errors } = await registerSessionServers(TUI_SESSION, merged, { workspaceRoot: projectPath });
|
|
1877
|
+
const lines = [`## MCP reloaded`, '', `**${registered.length}** tool${registered.length === 1 ? '' : 's'} from **${merged.length}** server${merged.length === 1 ? '' : 's'}.`];
|
|
1878
|
+
if (errors.length > 0) {
|
|
1879
|
+
lines.push('', '### Failed servers');
|
|
1880
|
+
for (const e of errors)
|
|
1881
|
+
lines.push(`- **${e.server}** — \`${e.error}\``);
|
|
1882
|
+
}
|
|
1883
|
+
ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
|
|
1884
|
+
break;
|
|
1885
|
+
}
|
|
1886
|
+
if (sub === 'resources') {
|
|
1887
|
+
const { getSessionResources, awaitSessionReady } = await import('../utils/mcpRegistry.js');
|
|
1888
|
+
await awaitSessionReady(TUI_SESSION);
|
|
1889
|
+
const groups = await getSessionResources(TUI_SESSION);
|
|
1890
|
+
if (groups.length === 0) {
|
|
1891
|
+
ctx.app.addMessage({ role: 'system', content: '_No MCP server in this session exposes resources._' });
|
|
1892
|
+
break;
|
|
1893
|
+
}
|
|
1894
|
+
const lines = ['## MCP resources', ''];
|
|
1895
|
+
for (const g of groups) {
|
|
1896
|
+
lines.push(`**${g.serverName}** — ${g.resources.length} resource${g.resources.length === 1 ? '' : 's'}`);
|
|
1897
|
+
for (const r of g.resources) {
|
|
1898
|
+
const label = r.name ? `${r.name} — ` : '';
|
|
1899
|
+
const mime = r.mimeType ? ` (${r.mimeType})` : '';
|
|
1900
|
+
lines.push(`- ${label}\`${r.uri}\`${mime}${r.description ? ` — ${r.description}` : ''}`);
|
|
1901
|
+
}
|
|
1902
|
+
lines.push('');
|
|
1903
|
+
}
|
|
1904
|
+
lines.push('Read one with `/mcp read <uri>`.');
|
|
1905
|
+
ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
|
|
1906
|
+
break;
|
|
1907
|
+
}
|
|
1908
|
+
if (sub === 'read') {
|
|
1909
|
+
const uri = args[1];
|
|
1910
|
+
if (!uri) {
|
|
1911
|
+
ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp read <uri>` — run `/mcp resources` to see available URIs.' });
|
|
1912
|
+
break;
|
|
1913
|
+
}
|
|
1914
|
+
const { readSessionResource } = await import('../utils/mcpRegistry.js');
|
|
1915
|
+
try {
|
|
1916
|
+
const contents = await readSessionResource(TUI_SESSION, uri);
|
|
1917
|
+
if (contents.length === 0) {
|
|
1918
|
+
ctx.app.addMessage({ role: 'system', content: `_No content returned for \`${uri}\`._` });
|
|
1919
|
+
break;
|
|
1920
|
+
}
|
|
1921
|
+
const lines = [`## Resource: \`${uri}\``, ''];
|
|
1922
|
+
for (const c of contents) {
|
|
1923
|
+
if (c.text !== undefined) {
|
|
1924
|
+
const fence = c.mimeType?.includes('json') ? 'json' : c.mimeType?.includes('markdown') ? 'markdown' : '';
|
|
1925
|
+
lines.push('```' + fence);
|
|
1926
|
+
lines.push(c.text);
|
|
1927
|
+
lines.push('```');
|
|
1928
|
+
}
|
|
1929
|
+
else if (c.blob) {
|
|
1930
|
+
lines.push(`_(${c.mimeType ?? 'binary'} blob, ${c.blob.length} base64 chars — not rendered)_`);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
|
|
1934
|
+
}
|
|
1935
|
+
catch (err) {
|
|
1936
|
+
ctx.app.addMessage({ role: 'system', content: `Failed to read \`${uri}\`: ${err.message}` });
|
|
1937
|
+
}
|
|
1938
|
+
break;
|
|
1939
|
+
}
|
|
1940
|
+
if (sub === 'prompts') {
|
|
1941
|
+
const { getSessionPrompts, awaitSessionReady } = await import('../utils/mcpRegistry.js');
|
|
1942
|
+
await awaitSessionReady(TUI_SESSION);
|
|
1943
|
+
const groups = await getSessionPrompts(TUI_SESSION);
|
|
1944
|
+
if (groups.length === 0) {
|
|
1945
|
+
ctx.app.addMessage({ role: 'system', content: '_No MCP server in this session exposes prompt templates._' });
|
|
1946
|
+
break;
|
|
1947
|
+
}
|
|
1948
|
+
const lines = ['## MCP prompt templates', ''];
|
|
1949
|
+
for (const g of groups) {
|
|
1950
|
+
lines.push(`**${g.serverName}** — ${g.prompts.length} prompt${g.prompts.length === 1 ? '' : 's'}`);
|
|
1951
|
+
for (const p of g.prompts) {
|
|
1952
|
+
const argList = p.arguments?.length
|
|
1953
|
+
? ` (${p.arguments.map(a => a.required ? a.name : `[${a.name}]`).join(', ')})`
|
|
1954
|
+
: '';
|
|
1955
|
+
lines.push(`- \`${p.name}\`${argList}${p.description ? ` — ${p.description}` : ''}`);
|
|
1956
|
+
}
|
|
1957
|
+
lines.push('');
|
|
1958
|
+
}
|
|
1959
|
+
lines.push('Materialise one with `/mcp prompt <server> <name> [key=value...]`.');
|
|
1960
|
+
ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
|
|
1961
|
+
break;
|
|
1962
|
+
}
|
|
1963
|
+
if (sub === 'prompt') {
|
|
1964
|
+
const serverName = args[1];
|
|
1965
|
+
const name = args[2];
|
|
1966
|
+
if (!serverName || !name) {
|
|
1967
|
+
ctx.app.addMessage({ role: 'system', content: 'Usage: `/mcp prompt <server> <name> [key=value ...]`' });
|
|
1968
|
+
break;
|
|
1969
|
+
}
|
|
1970
|
+
const promptArgs = {};
|
|
1971
|
+
for (const tok of args.slice(3)) {
|
|
1972
|
+
const eq = tok.indexOf('=');
|
|
1973
|
+
if (eq > 0)
|
|
1974
|
+
promptArgs[tok.slice(0, eq)] = tok.slice(eq + 1);
|
|
1975
|
+
}
|
|
1976
|
+
const { getSessionPrompt } = await import('../utils/mcpRegistry.js');
|
|
1977
|
+
try {
|
|
1978
|
+
const { description, messages } = await getSessionPrompt(TUI_SESSION, serverName, name, promptArgs);
|
|
1979
|
+
const lines = [`## Prompt \`${serverName}/${name}\``];
|
|
1980
|
+
if (description)
|
|
1981
|
+
lines.push(`_${description}_`);
|
|
1982
|
+
lines.push('');
|
|
1983
|
+
for (const m of messages) {
|
|
1984
|
+
const text = typeof m.content?.text === 'string' ? m.content.text : JSON.stringify(m.content);
|
|
1985
|
+
lines.push(`**${m.role}:** ${text}`);
|
|
1986
|
+
lines.push('');
|
|
1987
|
+
}
|
|
1988
|
+
ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
|
|
1989
|
+
}
|
|
1990
|
+
catch (err) {
|
|
1991
|
+
ctx.app.addMessage({ role: 'system', content: `Failed to materialise prompt: ${err.message}` });
|
|
1992
|
+
}
|
|
1993
|
+
break;
|
|
1994
|
+
}
|
|
1995
|
+
// Default: list servers + tools for the current session.
|
|
1996
|
+
const { getSessionTools, getSessionRegistrationErrors, awaitSessionReady } = await import('../utils/mcpRegistry.js');
|
|
1997
|
+
await awaitSessionReady(TUI_SESSION);
|
|
1998
|
+
const tools = await getSessionTools(TUI_SESSION);
|
|
1999
|
+
const mcpErrors = getSessionRegistrationErrors(TUI_SESSION);
|
|
2000
|
+
if (tools.length === 0 && mcpErrors.length === 0) {
|
|
2001
|
+
ctx.app.addMessage({
|
|
2002
|
+
role: 'system',
|
|
2003
|
+
content: [
|
|
2004
|
+
'_No MCP servers connected to this session._',
|
|
2005
|
+
'',
|
|
2006
|
+
'Add one with `/mcp add <name> <command> [args...]` — it persists to `.codeep/mcp_servers.json`.',
|
|
2007
|
+
'Or browse the marketplace with `/mcp browse` and install with `/mcp install <id>`.',
|
|
2008
|
+
].join('\n'),
|
|
2009
|
+
});
|
|
2010
|
+
break;
|
|
2011
|
+
}
|
|
2012
|
+
const lines = ['## MCP servers', ''];
|
|
2013
|
+
if (tools.length > 0) {
|
|
2014
|
+
const byServer = new Map();
|
|
2015
|
+
for (const t of tools) {
|
|
2016
|
+
if (!byServer.has(t.serverName))
|
|
2017
|
+
byServer.set(t.serverName, []);
|
|
2018
|
+
byServer.get(t.serverName).push(t);
|
|
2019
|
+
}
|
|
2020
|
+
for (const [serverName, serverTools] of byServer) {
|
|
2021
|
+
lines.push(`**${serverName}** — ${serverTools.length} tool${serverTools.length === 1 ? '' : 's'}`);
|
|
2022
|
+
for (const t of serverTools) {
|
|
2023
|
+
const desc = t.description ? ` — ${t.description}` : '';
|
|
2024
|
+
lines.push(`- \`${t.agentName}\`${desc}`);
|
|
2025
|
+
}
|
|
2026
|
+
lines.push('');
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
if (mcpErrors.length > 0) {
|
|
2030
|
+
lines.push('### Failed servers');
|
|
2031
|
+
for (const e of mcpErrors)
|
|
2032
|
+
lines.push(`- **${e.server}** — \`${e.error}\``);
|
|
2033
|
+
}
|
|
2034
|
+
ctx.app.addMessage({ role: 'system', content: lines.join('\n').trim() });
|
|
2035
|
+
break;
|
|
2036
|
+
}
|
|
1688
2037
|
default: {
|
|
1689
2038
|
// 1. Try custom user command (project + global Markdown templates).
|
|
1690
2039
|
const { findCustomCommand, expandCommand } = await import('../utils/customCommands.js');
|
|
@@ -29,6 +29,16 @@ export const helpCategories = [
|
|
|
29
29
|
{ key: '/rename <name>', description: 'Rename current session' },
|
|
30
30
|
{ key: '/search <term>', description: 'Search chat history' },
|
|
31
31
|
{ key: '/export [md|json|txt]', description: 'Export chat' },
|
|
32
|
+
{ key: '/compact [keepN]', description: 'AI-summarize older messages to free up context (keeps last N)' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
title: 'Checkpoints (2.0)',
|
|
37
|
+
items: [
|
|
38
|
+
{ key: '/checkpoint [name]', description: 'Snapshot conversation + provider/model + git HEAD' },
|
|
39
|
+
{ key: '/checkpoints', description: 'List saved checkpoints in this workspace' },
|
|
40
|
+
{ key: '/rewind <id>', description: 'Restore conversation from a checkpoint' },
|
|
41
|
+
{ key: '/checkpoint delete <id>', description: 'Delete a saved checkpoint' },
|
|
32
42
|
],
|
|
33
43
|
},
|
|
34
44
|
{
|
|
@@ -36,6 +46,8 @@ export const helpCategories = [
|
|
|
36
46
|
items: [
|
|
37
47
|
{ key: '/agent <task>', description: 'Run agent with task' },
|
|
38
48
|
{ key: '/agent-dry <task>', description: 'Dry run (no changes)' },
|
|
49
|
+
{ key: '/plan <task>', description: 'Generate a plan first — review before /go executes' },
|
|
50
|
+
{ key: '/go', description: 'Execute the pending plan from /plan' },
|
|
39
51
|
{ key: '/stop', description: 'Stop running agent' },
|
|
40
52
|
{ key: '/undo', description: 'Undo last agent action' },
|
|
41
53
|
{ key: '/undo-all', description: 'Undo all agent actions' },
|
|
@@ -110,6 +122,24 @@ export const helpCategories = [
|
|
|
110
122
|
{ key: '/logout', description: 'Logout from provider' },
|
|
111
123
|
{ key: '/profile save <name>', description: 'Save current provider+model as profile' },
|
|
112
124
|
{ key: '/profile list', description: 'List saved profiles' },
|
|
125
|
+
{ key: '/openrouter', description: 'OpenRouter routing prefs (prefer/ignore providers, fallbacks, privacy)' },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
title: 'Extensions & MCP (2.0)',
|
|
130
|
+
items: [
|
|
131
|
+
{ key: '/mcp', description: 'List connected MCP servers + their tools' },
|
|
132
|
+
{ key: '/mcp browse [id]', description: 'Browse marketplace (12 servers) or show one' },
|
|
133
|
+
{ key: '/mcp install <id> [args]', description: 'Install a marketplace server into this project' },
|
|
134
|
+
{ key: '/mcp add <name> <cmd>', description: 'Add a custom MCP server (npx, binary, etc.)' },
|
|
135
|
+
{ key: '/mcp remove <name>', description: 'Remove a project-scoped MCP server' },
|
|
136
|
+
{ key: '/mcp reload', description: 'Re-read .codeep/mcp_servers.json (after manual edit)' },
|
|
137
|
+
{ key: '/mcp resources', description: 'List resources exposed by connected servers' },
|
|
138
|
+
{ key: '/mcp read <uri>', description: 'Read one MCP resource' },
|
|
139
|
+
{ key: '/mcp prompts', description: 'List prompt templates exposed by servers' },
|
|
140
|
+
{ key: '/mcp prompt <server> <name>', description: 'Materialize a prompt with arguments (key=value)' },
|
|
141
|
+
{ key: '/hooks', description: 'List installed lifecycle hooks (.codeep/hooks/<event>.sh)' },
|
|
142
|
+
{ key: '/commands', description: 'List custom slash commands (.codeep/commands/*.md)' },
|
|
113
143
|
],
|
|
114
144
|
},
|
|
115
145
|
{
|
package/dist/utils/agentChat.js
CHANGED
|
@@ -301,9 +301,25 @@ additionalTools) {
|
|
|
301
301
|
}
|
|
302
302
|
else {
|
|
303
303
|
endpoint = `${baseUrl}/v1/messages`;
|
|
304
|
+
// Anthropic prompt caching. Two cache breakpoints:
|
|
305
|
+
// 1. `system` (largest stable block — system prompt + skills catalog)
|
|
306
|
+
// 2. last tool in `tools` (Anthropic caches everything up to and
|
|
307
|
+
// including the marker, so this caches the entire tools array)
|
|
308
|
+
// Cache hits cost 0.1× input. Misses ("cache creation") cost 1.25×.
|
|
309
|
+
// Net win after the 2nd same-shape request. Below 1024 input tokens
|
|
310
|
+
// Anthropic silently skips caching — no error path to handle.
|
|
311
|
+
const anthropicTools = getAnthropicTools(additionalTools);
|
|
312
|
+
const cachedTools = anthropicTools.length > 0
|
|
313
|
+
? [
|
|
314
|
+
...anthropicTools.slice(0, -1),
|
|
315
|
+
{ ...anthropicTools[anthropicTools.length - 1], cache_control: { type: 'ephemeral' } },
|
|
316
|
+
]
|
|
317
|
+
: anthropicTools;
|
|
304
318
|
body = {
|
|
305
|
-
model,
|
|
306
|
-
|
|
319
|
+
model,
|
|
320
|
+
system: [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }],
|
|
321
|
+
messages,
|
|
322
|
+
tools: cachedTools, stream: useStreaming,
|
|
307
323
|
...tempParam, max_tokens: getEffectiveMaxTokens(providerId, Math.max(config.get('maxTokens'), 16384)),
|
|
308
324
|
};
|
|
309
325
|
}
|
|
@@ -435,10 +451,15 @@ export async function agentChatFallback(messages, systemPrompt, onChunk, abortSi
|
|
|
435
451
|
}
|
|
436
452
|
else {
|
|
437
453
|
endpoint = `${baseUrl}/v1/messages`;
|
|
454
|
+
// Fallback path injects system+tools as the first user message
|
|
455
|
+
// (no native tool API). Cache that block — it's large and stable.
|
|
438
456
|
body = {
|
|
439
457
|
model,
|
|
440
458
|
messages: [
|
|
441
|
-
{
|
|
459
|
+
{
|
|
460
|
+
role: 'user',
|
|
461
|
+
content: [{ type: 'text', text: fallbackPrompt, cache_control: { type: 'ephemeral' } }],
|
|
462
|
+
},
|
|
442
463
|
{ role: 'assistant', content: 'Understood. I will use the tools as specified.' },
|
|
443
464
|
...messages,
|
|
444
465
|
],
|
|
@@ -169,13 +169,30 @@ export async function handleAnthropicAgentStream(body, onChunk, model, providerI
|
|
|
169
169
|
const data = line.slice(6);
|
|
170
170
|
try {
|
|
171
171
|
const parsed = JSON.parse(data);
|
|
172
|
-
// message_start has input tokens
|
|
172
|
+
// message_start has input tokens (incl. cache create/read fields if
|
|
173
|
+
// prompt caching is in use); message_delta has output tokens —
|
|
174
|
+
// merge both so extractAnthropicUsage sees the full picture.
|
|
173
175
|
if (parsed.type === 'message_start' && parsed.message?.usage) {
|
|
174
|
-
|
|
176
|
+
const u = parsed.message.usage;
|
|
177
|
+
usageData = {
|
|
178
|
+
usage: {
|
|
179
|
+
input_tokens: u.input_tokens || 0,
|
|
180
|
+
output_tokens: 0,
|
|
181
|
+
cache_creation_input_tokens: u.cache_creation_input_tokens || 0,
|
|
182
|
+
cache_read_input_tokens: u.cache_read_input_tokens || 0,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
175
185
|
}
|
|
176
186
|
else if (parsed.type === 'message_delta' && parsed.usage) {
|
|
177
|
-
const
|
|
178
|
-
usageData = {
|
|
187
|
+
const prev = usageData?.usage ?? {};
|
|
188
|
+
usageData = {
|
|
189
|
+
usage: {
|
|
190
|
+
input_tokens: prev.input_tokens || 0,
|
|
191
|
+
output_tokens: parsed.usage.output_tokens || 0,
|
|
192
|
+
cache_creation_input_tokens: prev.cache_creation_input_tokens || 0,
|
|
193
|
+
cache_read_input_tokens: prev.cache_read_input_tokens || 0,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
179
196
|
}
|
|
180
197
|
if (parsed.type === 'content_block_start') {
|
|
181
198
|
const block = parsed.content_block;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan mode — explicit pre-execution preview.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. User runs `/plan <task>` — we ask the LLM for a numbered plan
|
|
6
|
+
* (no tool calls, no file changes) and surface it to the user.
|
|
7
|
+
* 2. We hold the (task, plan) pair as the *pending* plan, scoped to
|
|
8
|
+
* the current process.
|
|
9
|
+
* 3. User runs `/go` to execute, or `/plan <revised task>` to refine.
|
|
10
|
+
* `/go` hands the original task + approved plan to the regular
|
|
11
|
+
* agent loop as a single prompt, so the existing tool execution,
|
|
12
|
+
* verification, and permission paths apply unchanged.
|
|
13
|
+
*
|
|
14
|
+
* Why this design (MVP):
|
|
15
|
+
* - Zero changes to the agent loop, MCP wiring, or ACP server.
|
|
16
|
+
* - Plan rendering reuses the chat markdown renderer (no new TUI
|
|
17
|
+
* panel to maintain).
|
|
18
|
+
* - Edit flow is just "/plan <revised task>" — generates a new plan
|
|
19
|
+
* that replaces the pending one; user pays one extra LLM call but
|
|
20
|
+
* gets human-readable revision history in the chat above.
|
|
21
|
+
* - When we ship a proper plan-mode UI later (TUI panel with
|
|
22
|
+
* Accept/Edit/Reject buttons + per-step progress), it can keep
|
|
23
|
+
* using this module as the backend.
|
|
24
|
+
*/
|
|
25
|
+
export interface PendingPlan {
|
|
26
|
+
task: string;
|
|
27
|
+
plan: string;
|
|
28
|
+
createdAt: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Ask the model for a plan for the given task. Stores the (task, plan)
|
|
32
|
+
* pair as the pending plan so a subsequent `/go` can execute it.
|
|
33
|
+
* Throws on chat failure — caller renders the error.
|
|
34
|
+
*/
|
|
35
|
+
export declare function generatePlan(task: string, onChunk?: (text: string) => void): Promise<string>;
|
|
36
|
+
export declare function getPendingPlan(): PendingPlan | null;
|
|
37
|
+
export declare function clearPendingPlan(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Compose the prompt the agent loop receives when the user runs `/go`.
|
|
40
|
+
* The agent treats this as a normal task, so tool calls / verification /
|
|
41
|
+
* permissions / hooks all flow through the existing paths — we just
|
|
42
|
+
* front-load the plan as context so the model doesn't re-plan
|
|
43
|
+
* implicitly.
|
|
44
|
+
*/
|
|
45
|
+
export declare function composeExecutionPrompt(plan: PendingPlan): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan mode — explicit pre-execution preview.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. User runs `/plan <task>` — we ask the LLM for a numbered plan
|
|
6
|
+
* (no tool calls, no file changes) and surface it to the user.
|
|
7
|
+
* 2. We hold the (task, plan) pair as the *pending* plan, scoped to
|
|
8
|
+
* the current process.
|
|
9
|
+
* 3. User runs `/go` to execute, or `/plan <revised task>` to refine.
|
|
10
|
+
* `/go` hands the original task + approved plan to the regular
|
|
11
|
+
* agent loop as a single prompt, so the existing tool execution,
|
|
12
|
+
* verification, and permission paths apply unchanged.
|
|
13
|
+
*
|
|
14
|
+
* Why this design (MVP):
|
|
15
|
+
* - Zero changes to the agent loop, MCP wiring, or ACP server.
|
|
16
|
+
* - Plan rendering reuses the chat markdown renderer (no new TUI
|
|
17
|
+
* panel to maintain).
|
|
18
|
+
* - Edit flow is just "/plan <revised task>" — generates a new plan
|
|
19
|
+
* that replaces the pending one; user pays one extra LLM call but
|
|
20
|
+
* gets human-readable revision history in the chat above.
|
|
21
|
+
* - When we ship a proper plan-mode UI later (TUI panel with
|
|
22
|
+
* Accept/Edit/Reject buttons + per-step progress), it can keep
|
|
23
|
+
* using this module as the backend.
|
|
24
|
+
*/
|
|
25
|
+
import { chat } from '../api/index.js';
|
|
26
|
+
const PLAN_SYSTEM_PROMPT = `You are in PLAN MODE.
|
|
27
|
+
|
|
28
|
+
The user has given you a task. **Do not execute anything.** Do not call
|
|
29
|
+
tools. Do not modify files. Do not run shell commands. Instead, produce
|
|
30
|
+
a numbered plan of the steps you would take, so the user can review and
|
|
31
|
+
approve before any changes happen.
|
|
32
|
+
|
|
33
|
+
Format your response as Markdown:
|
|
34
|
+
|
|
35
|
+
## Plan: <one-line summary of the task>
|
|
36
|
+
|
|
37
|
+
1. **<short step name>** — what you'd do (e.g. read file, edit lines, run command).
|
|
38
|
+
_Why:_ rationale (1 sentence).
|
|
39
|
+
_Expected outcome:_ what the user should see after this step.
|
|
40
|
+
|
|
41
|
+
2. **<step name>** — …
|
|
42
|
+
|
|
43
|
+
(continue numbering)
|
|
44
|
+
|
|
45
|
+
End with these three lines (verbatim labels, fill in values):
|
|
46
|
+
|
|
47
|
+
**Risk:** <low | medium | high> — <one-sentence reason; e.g. "schema change, requires migration">
|
|
48
|
+
**Files affected:** <comma-separated list of paths you'd touch, or "none">
|
|
49
|
+
**Commands run:** <comma-separated shell commands, or "none">
|
|
50
|
+
|
|
51
|
+
Rules:
|
|
52
|
+
- Be concrete. Reference real file paths from the project if you know them.
|
|
53
|
+
- Keep steps small enough that each maps to one or two tool calls when
|
|
54
|
+
later executed.
|
|
55
|
+
- If the task is ambiguous, list the assumption(s) you're making at the
|
|
56
|
+
top of the plan ("Assumes: ...") so the user can correct before /go.
|
|
57
|
+
- Do NOT produce code — describe what you'd change, not the change
|
|
58
|
+
itself. Code generation belongs in execution, not planning.
|
|
59
|
+
- If the task is trivial (single-file rename, single-line edit), say so
|
|
60
|
+
in one sentence and skip the formal plan — don't bloat tiny work.`;
|
|
61
|
+
let pending = null;
|
|
62
|
+
/**
|
|
63
|
+
* Ask the model for a plan for the given task. Stores the (task, plan)
|
|
64
|
+
* pair as the pending plan so a subsequent `/go` can execute it.
|
|
65
|
+
* Throws on chat failure — caller renders the error.
|
|
66
|
+
*/
|
|
67
|
+
export async function generatePlan(task, onChunk) {
|
|
68
|
+
const history = [
|
|
69
|
+
{ role: 'system', content: PLAN_SYSTEM_PROMPT },
|
|
70
|
+
];
|
|
71
|
+
const plan = await chat(task, history, onChunk);
|
|
72
|
+
pending = { task, plan, createdAt: Date.now() };
|
|
73
|
+
return plan;
|
|
74
|
+
}
|
|
75
|
+
export function getPendingPlan() {
|
|
76
|
+
return pending;
|
|
77
|
+
}
|
|
78
|
+
export function clearPendingPlan() {
|
|
79
|
+
pending = null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Compose the prompt the agent loop receives when the user runs `/go`.
|
|
83
|
+
* The agent treats this as a normal task, so tool calls / verification /
|
|
84
|
+
* permissions / hooks all flow through the existing paths — we just
|
|
85
|
+
* front-load the plan as context so the model doesn't re-plan
|
|
86
|
+
* implicitly.
|
|
87
|
+
*/
|
|
88
|
+
export function composeExecutionPrompt(plan) {
|
|
89
|
+
return `${plan.task}
|
|
90
|
+
|
|
91
|
+
I've reviewed the following plan and approved it. Execute it step by step. If any step turns out to be wrong or impossible mid-execution, stop and report — don't silently improvise.
|
|
92
|
+
|
|
93
|
+
${plan.plan}`;
|
|
94
|
+
}
|
|
@@ -24,10 +24,13 @@ export interface RemoteSkill {
|
|
|
24
24
|
updated_at: string;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* Publish a
|
|
28
|
-
*
|
|
29
|
-
* —
|
|
30
|
-
*
|
|
27
|
+
* Publish a skill bundle to codeep.dev. The bundle may be project-scoped
|
|
28
|
+
* (`<workspaceRoot>/.codeep/skills/<slug>/SKILL.md`) or global
|
|
29
|
+
* (`~/.codeep/skills/<slug>/SKILL.md`) — either is publishable. The
|
|
30
|
+
* `--public` flag (translated into `opts.isPublic`) is the user's
|
|
31
|
+
* explicit consent gate; we don't gate further on bundle scope. If both
|
|
32
|
+
* exist with the same slug, the project copy wins (mirrors the rest of
|
|
33
|
+
* the bundle-loading flow).
|
|
31
34
|
*/
|
|
32
35
|
export declare function publishBundle(workspaceRoot: string, slug: string, opts?: {
|
|
33
36
|
isPublic?: boolean;
|
|
@@ -17,18 +17,24 @@ import { getSyncToken } from '../config/index.js';
|
|
|
17
17
|
import { findSkillBundle } from './skillBundles.js';
|
|
18
18
|
const API_BASE = 'https://codeep.dev';
|
|
19
19
|
/**
|
|
20
|
-
* Publish a
|
|
21
|
-
*
|
|
22
|
-
* —
|
|
23
|
-
*
|
|
20
|
+
* Publish a skill bundle to codeep.dev. The bundle may be project-scoped
|
|
21
|
+
* (`<workspaceRoot>/.codeep/skills/<slug>/SKILL.md`) or global
|
|
22
|
+
* (`~/.codeep/skills/<slug>/SKILL.md`) — either is publishable. The
|
|
23
|
+
* `--public` flag (translated into `opts.isPublic`) is the user's
|
|
24
|
+
* explicit consent gate; we don't gate further on bundle scope. If both
|
|
25
|
+
* exist with the same slug, the project copy wins (mirrors the rest of
|
|
26
|
+
* the bundle-loading flow).
|
|
24
27
|
*/
|
|
25
28
|
export async function publishBundle(workspaceRoot, slug, opts = {}) {
|
|
26
29
|
const token = getSyncToken();
|
|
27
30
|
if (!token)
|
|
28
31
|
return { ok: false, error: 'Not linked to codeep.dev — run `codeep account` first.' };
|
|
29
32
|
const bundle = findSkillBundle(slug, workspaceRoot);
|
|
30
|
-
if (!bundle
|
|
31
|
-
return {
|
|
33
|
+
if (!bundle) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: `Skill bundle "${slug}" not found in either \`.codeep/skills/${slug}/\` (project) or \`~/.codeep/skills/${slug}/\` (global). Run \`/skills create-bundle ${slug}\` to scaffold one, or check \`/skills bundles\` to see what's available.`,
|
|
37
|
+
};
|
|
32
38
|
}
|
|
33
39
|
// Build the SKILL.md text we'll publish. We re-serialise from the loaded
|
|
34
40
|
// bundle rather than reading the file again — that way frontmatter
|
|
@@ -5,6 +5,13 @@ export interface TokenUsage {
|
|
|
5
5
|
promptTokens: number;
|
|
6
6
|
completionTokens: number;
|
|
7
7
|
totalTokens: number;
|
|
8
|
+
/** Anthropic prompt caching: tokens written to the cache on this call
|
|
9
|
+
* (billed at ~1.25× input rate). Undefined for providers that don't
|
|
10
|
+
* support caching or for calls below the cache size threshold. */
|
|
11
|
+
cacheCreationTokens?: number;
|
|
12
|
+
/** Anthropic prompt caching: tokens read from cache on this call
|
|
13
|
+
* (billed at ~0.1× input rate — the big savings live here). */
|
|
14
|
+
cacheReadTokens?: number;
|
|
8
15
|
}
|
|
9
16
|
export interface SessionTokenStats {
|
|
10
17
|
totalPromptTokens: number;
|
|
@@ -18,6 +25,9 @@ interface TokenRecord {
|
|
|
18
25
|
promptTokens: number;
|
|
19
26
|
completionTokens: number;
|
|
20
27
|
totalTokens: number;
|
|
28
|
+
/** Anthropic prompt caching breakdown — see TokenUsage. */
|
|
29
|
+
cacheCreationTokens?: number;
|
|
30
|
+
cacheReadTokens?: number;
|
|
21
31
|
model: string;
|
|
22
32
|
provider: string;
|
|
23
33
|
/** Authoritative per-call USD from the provider (OpenRouter), if available. */
|
|
@@ -59,6 +69,19 @@ export interface ProviderCostBreakdown {
|
|
|
59
69
|
* Get cost breakdown grouped by provider/model
|
|
60
70
|
*/
|
|
61
71
|
export declare function getCostBreakdown(): ProviderCostBreakdown[];
|
|
72
|
+
/**
|
|
73
|
+
* Aggregate Anthropic prompt-caching stats for the current session.
|
|
74
|
+
* Returns the breakdown plus an estimate of what the input billing would
|
|
75
|
+
* have been *without* caching, so we can surface "you saved $X" to the
|
|
76
|
+
* user.
|
|
77
|
+
*/
|
|
78
|
+
export interface CacheStats {
|
|
79
|
+
cacheCreationTokens: number;
|
|
80
|
+
cacheReadTokens: number;
|
|
81
|
+
/** Sum of estimatedSavings across all Anthropic-priced records. */
|
|
82
|
+
estimatedSavingsUsd: number;
|
|
83
|
+
}
|
|
84
|
+
export declare function getCacheStats(): CacheStats;
|
|
62
85
|
/**
|
|
63
86
|
* Get session stats
|
|
64
87
|
*/
|
|
@@ -81,6 +81,8 @@ export function recordTokenUsage(usage, model, provider, actualCostUsd) {
|
|
|
81
81
|
promptTokens: usage.promptTokens,
|
|
82
82
|
completionTokens: usage.completionTokens,
|
|
83
83
|
totalTokens: usage.totalTokens,
|
|
84
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
85
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
84
86
|
model,
|
|
85
87
|
provider,
|
|
86
88
|
actualCostUsd,
|
|
@@ -104,10 +106,19 @@ export function extractOpenAIUsage(data) {
|
|
|
104
106
|
*/
|
|
105
107
|
export function extractAnthropicUsage(data) {
|
|
106
108
|
if (data?.usage) {
|
|
109
|
+
const inputTokens = data.usage.input_tokens || 0;
|
|
110
|
+
const outputTokens = data.usage.output_tokens || 0;
|
|
111
|
+
const cacheCreation = data.usage.cache_creation_input_tokens || 0;
|
|
112
|
+
const cacheRead = data.usage.cache_read_input_tokens || 0;
|
|
113
|
+
// Anthropic returns input_tokens EXCLUSIVE of cache creation and cache
|
|
114
|
+
// read tokens — they're reported separately. Total prompt = sum of all
|
|
115
|
+
// three so our context window math doesn't undercount.
|
|
107
116
|
return {
|
|
108
|
-
promptTokens:
|
|
109
|
-
completionTokens:
|
|
110
|
-
totalTokens:
|
|
117
|
+
promptTokens: inputTokens + cacheCreation + cacheRead,
|
|
118
|
+
completionTokens: outputTokens,
|
|
119
|
+
totalTokens: inputTokens + cacheCreation + cacheRead + outputTokens,
|
|
120
|
+
cacheCreationTokens: cacheCreation || undefined,
|
|
121
|
+
cacheReadTokens: cacheRead || undefined,
|
|
111
122
|
};
|
|
112
123
|
}
|
|
113
124
|
return null;
|
|
@@ -132,13 +143,42 @@ export function getCostBreakdown() {
|
|
|
132
143
|
else {
|
|
133
144
|
const pricing = MODEL_PRICING[record.model];
|
|
134
145
|
if (pricing) {
|
|
135
|
-
|
|
146
|
+
// Anthropic prompt caching: cache_creation_input is billed at 1.25×
|
|
147
|
+
// the base input rate, cache_read_input at 0.1×. The remaining
|
|
148
|
+
// (uncached) prompt tokens bill at the standard 1.0× rate.
|
|
149
|
+
const cacheCreate = record.cacheCreationTokens ?? 0;
|
|
150
|
+
const cacheRead = record.cacheReadTokens ?? 0;
|
|
151
|
+
const uncachedPrompt = Math.max(0, record.promptTokens - cacheCreate - cacheRead);
|
|
152
|
+
existing.estimatedCost +=
|
|
153
|
+
(uncachedPrompt / 1_000_000) * pricing.inputPer1M
|
|
154
|
+
+ (cacheCreate / 1_000_000) * pricing.inputPer1M * 1.25
|
|
155
|
+
+ (cacheRead / 1_000_000) * pricing.inputPer1M * 0.1
|
|
156
|
+
+ (record.completionTokens / 1_000_000) * pricing.outputPer1M;
|
|
136
157
|
}
|
|
137
158
|
}
|
|
138
159
|
grouped.set(key, existing);
|
|
139
160
|
}
|
|
140
161
|
return Array.from(grouped.values());
|
|
141
162
|
}
|
|
163
|
+
export function getCacheStats() {
|
|
164
|
+
let cacheCreate = 0;
|
|
165
|
+
let cacheRead = 0;
|
|
166
|
+
let savings = 0;
|
|
167
|
+
for (const record of records) {
|
|
168
|
+
cacheCreate += record.cacheCreationTokens ?? 0;
|
|
169
|
+
cacheRead += record.cacheReadTokens ?? 0;
|
|
170
|
+
// Savings = what cache-read tokens would have cost at full input rate,
|
|
171
|
+
// minus what they actually cost at 0.1×. (Cache creation is a slight
|
|
172
|
+
// *penalty* of 0.25× — netted in for honest reporting.)
|
|
173
|
+
const pricing = MODEL_PRICING[record.model];
|
|
174
|
+
if (pricing) {
|
|
175
|
+
const cReadSaved = ((record.cacheReadTokens ?? 0) / 1_000_000) * pricing.inputPer1M * 0.9;
|
|
176
|
+
const cCreateCost = ((record.cacheCreationTokens ?? 0) / 1_000_000) * pricing.inputPer1M * 0.25;
|
|
177
|
+
savings += cReadSaved - cCreateCost;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { cacheCreationTokens: cacheCreate, cacheReadTokens: cacheRead, estimatedSavingsUsd: Math.max(0, savings) };
|
|
181
|
+
}
|
|
142
182
|
/**
|
|
143
183
|
* Get session stats
|
|
144
184
|
*/
|
|
@@ -207,6 +247,18 @@ export function formatCostReport() {
|
|
|
207
247
|
lines.push(`| \`${b.provider}\` / \`${b.model}\` | ${formatTokenCount(b.promptTokens)} | ${formatTokenCount(b.completionTokens)} | $${b.estimatedCost.toFixed(4)} |`);
|
|
208
248
|
}
|
|
209
249
|
}
|
|
250
|
+
// Prompt caching summary — only shown if at least one cached call landed.
|
|
251
|
+
const cache = getCacheStats();
|
|
252
|
+
if (cache.cacheReadTokens > 0 || cache.cacheCreationTokens > 0) {
|
|
253
|
+
lines.push('', '### Prompt caching');
|
|
254
|
+
lines.push(`**Cache reads:** ${formatTokenCount(cache.cacheReadTokens)} tokens (billed at 0.1× input rate)`);
|
|
255
|
+
if (cache.cacheCreationTokens > 0) {
|
|
256
|
+
lines.push(`**Cache writes:** ${formatTokenCount(cache.cacheCreationTokens)} tokens (billed at 1.25× input rate)`);
|
|
257
|
+
}
|
|
258
|
+
if (cache.estimatedSavingsUsd > 0) {
|
|
259
|
+
lines.push(`**Estimated savings vs no caching:** $${cache.estimatedSavingsUsd.toFixed(4)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
210
262
|
// Models with no pricing entry don't contribute to cost — flag so users
|
|
211
263
|
// aren't surprised the total looks low.
|
|
212
264
|
const untracked = breakdown.filter(b => b.estimatedCost === 0 && (b.promptTokens + b.completionTokens) > 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|