codeep 1.3.42 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -0
- package/dist/acp/commands.js +770 -7
- package/dist/acp/protocol.d.ts +11 -2
- package/dist/acp/server.js +179 -11
- package/dist/acp/session.d.ts +3 -0
- package/dist/acp/session.js +5 -0
- package/dist/api/index.js +39 -6
- package/dist/config/index.d.ts +13 -0
- package/dist/config/index.js +45 -0
- package/dist/config/providers.js +76 -1
- package/dist/renderer/App.d.ts +12 -0
- package/dist/renderer/App.js +109 -4
- package/dist/renderer/agentExecution.js +5 -0
- package/dist/renderer/commands.js +638 -2
- package/dist/renderer/components/Help.js +28 -0
- package/dist/renderer/components/Login.d.ts +1 -0
- package/dist/renderer/components/Login.js +24 -9
- package/dist/renderer/handlers.d.ts +11 -1
- package/dist/renderer/handlers.js +30 -0
- package/dist/renderer/main.js +73 -0
- package/dist/utils/agent.d.ts +17 -0
- package/dist/utils/agent.js +91 -7
- package/dist/utils/agentChat.d.ts +10 -2
- package/dist/utils/agentChat.js +48 -9
- package/dist/utils/agentStream.js +6 -2
- package/dist/utils/checkpoints.d.ts +93 -0
- package/dist/utils/checkpoints.js +205 -0
- package/dist/utils/context.d.ts +24 -0
- package/dist/utils/context.js +57 -0
- package/dist/utils/customCommands.d.ts +62 -0
- package/dist/utils/customCommands.js +201 -0
- package/dist/utils/hooks.d.ts +97 -0
- package/dist/utils/hooks.js +223 -0
- package/dist/utils/mcpClient.d.ts +229 -0
- package/dist/utils/mcpClient.js +497 -0
- package/dist/utils/mcpConfig.d.ts +55 -0
- package/dist/utils/mcpConfig.js +177 -0
- package/dist/utils/mcpMarketplace.d.ts +49 -0
- package/dist/utils/mcpMarketplace.js +175 -0
- package/dist/utils/mcpRegistry.d.ts +129 -0
- package/dist/utils/mcpRegistry.js +427 -0
- package/dist/utils/mcpSamplingBridge.d.ts +32 -0
- package/dist/utils/mcpSamplingBridge.js +88 -0
- package/dist/utils/mcpStreamableHttp.d.ts +65 -0
- package/dist/utils/mcpStreamableHttp.js +207 -0
- package/dist/utils/openrouterPrefs.d.ts +36 -0
- package/dist/utils/openrouterPrefs.js +83 -0
- package/dist/utils/skillBundles.d.ts +84 -0
- package/dist/utils/skillBundles.js +257 -0
- package/dist/utils/skillBundlesCloud.d.ts +69 -0
- package/dist/utils/skillBundlesCloud.js +202 -0
- package/dist/utils/tokenTracker.d.ts +14 -2
- package/dist/utils/tokenTracker.js +59 -41
- package/dist/utils/toolExecution.d.ts +17 -1
- package/dist/utils/toolExecution.js +184 -6
- package/dist/utils/tools.d.ts +22 -6
- package/dist/utils/tools.js +83 -8
- package/package.json +3 -2
- package/bin/codeep-macos-arm64 +0 -0
- package/bin/codeep-macos-x64 +0 -0
|
@@ -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
|
{
|
|
@@ -110,6 +120,24 @@ export const helpCategories = [
|
|
|
110
120
|
{ key: '/logout', description: 'Logout from provider' },
|
|
111
121
|
{ key: '/profile save <name>', description: 'Save current provider+model as profile' },
|
|
112
122
|
{ key: '/profile list', description: 'List saved profiles' },
|
|
123
|
+
{ key: '/openrouter', description: 'OpenRouter routing prefs (prefer/ignore providers, fallbacks, privacy)' },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
title: 'Extensions & MCP (2.0)',
|
|
128
|
+
items: [
|
|
129
|
+
{ key: '/mcp', description: 'List connected MCP servers + their tools' },
|
|
130
|
+
{ key: '/mcp browse [id]', description: 'Browse marketplace (12 servers) or show one' },
|
|
131
|
+
{ key: '/mcp install <id> [args]', description: 'Install a marketplace server into this project' },
|
|
132
|
+
{ key: '/mcp add <name> <cmd>', description: 'Add a custom MCP server (npx, binary, etc.)' },
|
|
133
|
+
{ key: '/mcp remove <name>', description: 'Remove a project-scoped MCP server' },
|
|
134
|
+
{ key: '/mcp reload', description: 'Re-read .codeep/mcp_servers.json (after manual edit)' },
|
|
135
|
+
{ key: '/mcp resources', description: 'List resources exposed by connected servers' },
|
|
136
|
+
{ key: '/mcp read <uri>', description: 'Read one MCP resource' },
|
|
137
|
+
{ key: '/mcp prompts', description: 'List prompt templates exposed by servers' },
|
|
138
|
+
{ key: '/mcp prompt <server> <name>', description: 'Materialize a prompt with arguments (key=value)' },
|
|
139
|
+
{ key: '/hooks', description: 'List installed lifecycle hooks (.codeep/hooks/<event>.sh)' },
|
|
140
|
+
{ key: '/commands', description: 'List custom slash commands (.codeep/commands/*.md)' },
|
|
113
141
|
],
|
|
114
142
|
},
|
|
115
143
|
{
|
|
@@ -190,15 +190,17 @@ export function renderProviderSelect(screen, providers, selectedIndex) {
|
|
|
190
190
|
const { width, height } = screen.getSize();
|
|
191
191
|
screen.clear();
|
|
192
192
|
// Title
|
|
193
|
-
const title = '═══ Codeep
|
|
193
|
+
const title = '═══ Welcome to Codeep ═══';
|
|
194
194
|
const titleX = Math.floor((width - title.length) / 2);
|
|
195
195
|
screen.write(titleX, 1, title, PRIMARY_COLOR + style.bold);
|
|
196
196
|
// Subtitle
|
|
197
|
-
const subtitle = '
|
|
197
|
+
const subtitle = 'Pick an AI provider — you can switch later with /provider';
|
|
198
198
|
const subtitleX = Math.floor((width - subtitle.length) / 2);
|
|
199
199
|
screen.write(subtitleX, 3, subtitle, fg.white);
|
|
200
|
-
// Box
|
|
201
|
-
const
|
|
200
|
+
// Box — wider so we can show name + description on a single row.
|
|
201
|
+
const longestName = providers.reduce((m, p) => Math.max(m, p.name.length), 0);
|
|
202
|
+
const longestDesc = providers.reduce((m, p) => Math.max(m, (p.description ?? '').length), 0);
|
|
203
|
+
const boxWidth = Math.min(width - 4, Math.max(60, 6 + longestName + 3 + longestDesc));
|
|
202
204
|
const boxHeight = providers.length + 4;
|
|
203
205
|
const { x: boxX, y: boxY } = centerBox(width, height, boxWidth, boxHeight);
|
|
204
206
|
const boxLines = createBox({
|
|
@@ -212,19 +214,32 @@ export function renderProviderSelect(screen, providers, selectedIndex) {
|
|
|
212
214
|
for (const line of boxLines) {
|
|
213
215
|
screen.writeLine(line.y, line.text, line.style);
|
|
214
216
|
}
|
|
215
|
-
// Provider list
|
|
217
|
+
// Provider list — name in white/bold-on-selected, dim description beside it.
|
|
218
|
+
// Description is clipped to the room remaining inside the box so it never
|
|
219
|
+
// overwrites the right border on narrow terminals (80-col laptop splits).
|
|
216
220
|
const contentX = boxX + 3;
|
|
217
|
-
|
|
221
|
+
const contentY = boxY + 2;
|
|
222
|
+
const nameColWidth = longestName + 2;
|
|
223
|
+
const descStartX = contentX + 2 + nameColWidth;
|
|
224
|
+
const boxInnerRight = boxX + boxWidth - 2;
|
|
225
|
+
const descBudget = Math.max(0, boxInnerRight - descStartX);
|
|
218
226
|
for (let i = 0; i < providers.length; i++) {
|
|
219
227
|
const provider = providers[i];
|
|
220
228
|
const isSelected = i === selectedIndex;
|
|
221
229
|
const prefix = isSelected ? '► ' : ' ';
|
|
222
|
-
const
|
|
223
|
-
|
|
230
|
+
const nameStyle = isSelected ? PRIMARY_BRIGHT + style.bold : fg.white;
|
|
231
|
+
const descStyle = isSelected ? fg.white : fg.gray;
|
|
232
|
+
screen.write(contentX, contentY + i, prefix + provider.name.padEnd(nameColWidth), nameStyle);
|
|
233
|
+
if (provider.description && descBudget > 0) {
|
|
234
|
+
const desc = provider.description.length > descBudget
|
|
235
|
+
? provider.description.slice(0, Math.max(1, descBudget - 1)) + '…'
|
|
236
|
+
: provider.description;
|
|
237
|
+
screen.write(descStartX, contentY + i, desc, descStyle);
|
|
238
|
+
}
|
|
224
239
|
}
|
|
225
240
|
// Footer
|
|
226
241
|
const footerY = height - 2;
|
|
227
|
-
screen.write(2, footerY, '↑↓ Navigate
|
|
242
|
+
screen.write(2, footerY, '↑↓ Navigate · Enter Select · Esc skip (provider chosen later)', fg.gray);
|
|
228
243
|
screen.showCursor(false);
|
|
229
244
|
screen.fullRender();
|
|
230
245
|
}
|
|
@@ -26,7 +26,17 @@ export interface MenuHandlerContext {
|
|
|
26
26
|
close(callback: ((item: SelectItem) => void) | null, selected: SelectItem | null): void;
|
|
27
27
|
render(): void;
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Optional context for type-to-filter support. Callers that supply these
|
|
31
|
+
* get inline filtering: letters/digits append to `filter`, Backspace
|
|
32
|
+
* deletes, first Esc clears a non-empty filter, second Esc closes.
|
|
33
|
+
* Callers that omit them behave exactly as before (back-compat).
|
|
34
|
+
*/
|
|
35
|
+
export interface MenuFilterContext {
|
|
36
|
+
filter: string;
|
|
37
|
+
setFilter(v: string): void;
|
|
38
|
+
}
|
|
39
|
+
export declare function handleMenuKey(event: KeyEvent, ctx: MenuHandlerContext & Partial<MenuFilterContext>): void;
|
|
30
40
|
declare const PERMISSION_OPTIONS: readonly ["read", "write", "none"];
|
|
31
41
|
type PermissionLevel = typeof PERMISSION_OPTIONS[number];
|
|
32
42
|
export interface PermissionHandlerContext {
|
|
@@ -42,7 +42,14 @@ export function handleInlineHelpKey(event, ctx) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
export function handleMenuKey(event, ctx) {
|
|
45
|
+
const supportsFilter = typeof ctx.setFilter === 'function';
|
|
45
46
|
if (event.key === 'escape') {
|
|
47
|
+
// Two-stage Esc when filter is on: first clears the filter, second closes.
|
|
48
|
+
if (supportsFilter && ctx.filter) {
|
|
49
|
+
ctx.setFilter('');
|
|
50
|
+
ctx.render();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
46
53
|
ctx.close(null, null);
|
|
47
54
|
ctx.render();
|
|
48
55
|
return;
|
|
@@ -71,6 +78,29 @@ export function handleMenuKey(event, ctx) {
|
|
|
71
78
|
const selected = ctx.items[ctx.index];
|
|
72
79
|
ctx.close(null, selected);
|
|
73
80
|
ctx.render();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!supportsFilter)
|
|
84
|
+
return;
|
|
85
|
+
// Backspace removes the last filter char.
|
|
86
|
+
if (event.key === 'backspace') {
|
|
87
|
+
if (ctx.filter) {
|
|
88
|
+
ctx.setFilter(ctx.filter.slice(0, -1));
|
|
89
|
+
ctx.render();
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Single printable char (letters, digits, punctuation). Modifier-free,
|
|
94
|
+
// non-paste. We deliberately exclude bare-space at empty filter so a
|
|
95
|
+
// stray space doesn't kick the user into an unintended filtered view.
|
|
96
|
+
if (!event.ctrl && !event.alt && !event.isPaste && event.key.length === 1) {
|
|
97
|
+
const ch = event.key;
|
|
98
|
+
if (ch === ' ' && !ctx.filter)
|
|
99
|
+
return;
|
|
100
|
+
if (/^[\S ]$/.test(ch)) {
|
|
101
|
+
ctx.setFilter((ctx.filter ?? '') + ch);
|
|
102
|
+
ctx.render();
|
|
103
|
+
}
|
|
74
104
|
}
|
|
75
105
|
}
|
|
76
106
|
// ─── Permission ──────────────────────────────────────────────────────────────
|
package/dist/renderer/main.js
CHANGED
|
@@ -540,10 +540,83 @@ Commands (in chat):
|
|
|
540
540
|
welcomeLines.push(githubId
|
|
541
541
|
? ` Account codeep.dev linked`
|
|
542
542
|
: ` Account not linked · run: codeep account`);
|
|
543
|
+
// Warn before first use if this workspace defines project-scoped custom
|
|
544
|
+
// slash commands. They run as user prompts — a hostile or unfamiliar repo
|
|
545
|
+
// could ship `.codeep/commands/refactor.md` whose body silently sends
|
|
546
|
+
// something the user didn't intend. The banner is informed-consent;
|
|
547
|
+
// `/commands` shows the full bodies.
|
|
548
|
+
if (projectPath) {
|
|
549
|
+
try {
|
|
550
|
+
const { loadCustomCommands } = await import('../utils/customCommands.js');
|
|
551
|
+
const projectCustom = loadCustomCommands(projectPath).filter(c => c.scope === 'project');
|
|
552
|
+
if (projectCustom.length > 0) {
|
|
553
|
+
const list = projectCustom.slice(0, 6).map(c => `/${c.name}`).join(', ');
|
|
554
|
+
const more = projectCustom.length > 6 ? ` (+${projectCustom.length - 6} more)` : '';
|
|
555
|
+
welcomeLines.push('');
|
|
556
|
+
welcomeLines.push(` ⚠ This workspace defines ${projectCustom.length} custom slash command${projectCustom.length === 1 ? '' : 's'}: ${list}${more}`);
|
|
557
|
+
welcomeLines.push(' Type /commands to review before invoking');
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
// Loading must never block the welcome banner.
|
|
562
|
+
}
|
|
563
|
+
// Same flag for lifecycle hooks — they're arbitrary shell that fires on
|
|
564
|
+
// tool calls. A surprise post_edit / pre_tool_call from a freshly cloned
|
|
565
|
+
// repo is exactly the kind of thing a user should be told up front.
|
|
566
|
+
try {
|
|
567
|
+
const { summarizeHooks } = await import('../utils/hooks.js');
|
|
568
|
+
const summary = summarizeHooks(projectPath);
|
|
569
|
+
if (summary) {
|
|
570
|
+
welcomeLines.push('');
|
|
571
|
+
welcomeLines.push(` ⚠ ${summary} — shell hooks run automatically. Type /hooks to inspect.`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
// Don't block on hook discovery failure.
|
|
576
|
+
}
|
|
577
|
+
// Skill bundles — less dangerous than hooks but worth surfacing
|
|
578
|
+
// because the agent will invoke them autonomously.
|
|
579
|
+
try {
|
|
580
|
+
const { summarizeBundles } = await import('../utils/skillBundles.js');
|
|
581
|
+
const summary = summarizeBundles(projectPath);
|
|
582
|
+
if (summary) {
|
|
583
|
+
welcomeLines.push('');
|
|
584
|
+
welcomeLines.push(` ℹ This workspace ships ${summary}. Type /skills bundles to inspect.`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
// Don't block on skill discovery failure.
|
|
589
|
+
}
|
|
590
|
+
}
|
|
543
591
|
welcomeLines.push('');
|
|
544
592
|
welcomeLines.push(' /help · Ctrl+L clear · Esc cancel');
|
|
545
593
|
app.addMessage({ role: 'welcome', content: welcomeLines.join('\n') });
|
|
546
594
|
app.start();
|
|
595
|
+
// Spawn MCP servers in the background. They register against the fixed
|
|
596
|
+
// session id `codeep-tui` that runAgentTask passes into runAgent's
|
|
597
|
+
// `mcpSessionId` — so the agent picks up `.codeep/mcp_servers.json`
|
|
598
|
+
// entries (project + global) the same way an ACP client would.
|
|
599
|
+
if (projectPath) {
|
|
600
|
+
(async () => {
|
|
601
|
+
try {
|
|
602
|
+
const { loadMcpServerConfig } = await import('../utils/mcpConfig.js');
|
|
603
|
+
const { registerSessionServers } = await import('../utils/mcpRegistry.js');
|
|
604
|
+
const servers = loadMcpServerConfig(projectPath);
|
|
605
|
+
if (servers.length === 0)
|
|
606
|
+
return;
|
|
607
|
+
const { registered, errors } = await registerSessionServers('codeep-tui', servers, { workspaceRoot: projectPath });
|
|
608
|
+
if (registered.length > 0) {
|
|
609
|
+
app.notify(`MCP: ${registered.length} tool(s) from ${servers.length} server(s) ready. Type /mcp.`);
|
|
610
|
+
}
|
|
611
|
+
for (const e of errors) {
|
|
612
|
+
app.notifyWarn(`MCP server "${e.server}" failed: ${e.error}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
// Loading MCP must never block the TUI.
|
|
617
|
+
}
|
|
618
|
+
})();
|
|
619
|
+
}
|
|
547
620
|
// Check for updates in background — show notify if new version available
|
|
548
621
|
checkForUpdates().then(info => {
|
|
549
622
|
if (info.hasUpdate) {
|
package/dist/utils/agent.d.ts
CHANGED
|
@@ -30,6 +30,23 @@ export interface AgentOptions {
|
|
|
30
30
|
stderr: string;
|
|
31
31
|
exitCode: number;
|
|
32
32
|
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Optional filesystem callbacks. When the ACP client advertises `fs`
|
|
35
|
+
* capability, the server populates these so read_file/write_file/edit_file
|
|
36
|
+
* tools route through the client (preserving dirty buffers and undo
|
|
37
|
+
* history) instead of touching disk directly. Falls back to disk if not
|
|
38
|
+
* provided or if a delegated call throws.
|
|
39
|
+
*/
|
|
40
|
+
fs?: import('./toolExecution').FsCallbacks;
|
|
41
|
+
/**
|
|
42
|
+
* Optional ACP session id used to route MCP-prefixed tool calls
|
|
43
|
+
* (`<server>__<tool>`) to the per-session `mcpRegistry`. Not set in TUI
|
|
44
|
+
* mode (no MCP support there yet); set by `runAgentSession` in ACP mode.
|
|
45
|
+
* When set, the agent loop also fetches the session's MCP tool list and
|
|
46
|
+
* passes it into the provider's tool catalog so the model can invoke
|
|
47
|
+
* those tools natively.
|
|
48
|
+
*/
|
|
49
|
+
mcpSessionId?: string;
|
|
33
50
|
abortSignal?: AbortSignal;
|
|
34
51
|
dryRun?: boolean;
|
|
35
52
|
autoVerify?: 'off' | 'build' | 'typecheck' | 'test' | 'all' | boolean;
|
package/dist/utils/agent.js
CHANGED
|
@@ -165,10 +165,66 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
165
165
|
const protocol = config.get('protocol');
|
|
166
166
|
const providerId = config.get('provider');
|
|
167
167
|
const useNativeTools = supportsNativeTools(providerId, protocol);
|
|
168
|
+
// Fetch the MCP tool catalog once per agent run. The session id keys into
|
|
169
|
+
// mcpRegistry; if no MCP servers are registered (or mcpSessionId is unset,
|
|
170
|
+
// e.g. TUI mode) we get back an empty array and the agent behaves as
|
|
171
|
+
// before. We do this before building the system prompt so the fallback
|
|
172
|
+
// text path can include MCP tools in its catalog too.
|
|
173
|
+
//
|
|
174
|
+
// We also append per-server "virtual" tools that wrap resource_list /
|
|
175
|
+
// resource_read / prompt_list / prompt_get so the agent can discover and
|
|
176
|
+
// pull MCP resources & prompts without the user having to type `/mcp
|
|
177
|
+
// read <uri>` manually. Servers that don't expose resources or prompts
|
|
178
|
+
// get no virtual tools — the wrappers are only emitted where useful.
|
|
179
|
+
let mcpToolDefs = [];
|
|
180
|
+
if (opts.mcpSessionId) {
|
|
181
|
+
try {
|
|
182
|
+
const { getSessionTools, getSessionVirtualTools } = await import('./mcpRegistry.js');
|
|
183
|
+
const [registered, virtuals] = await Promise.all([
|
|
184
|
+
getSessionTools(opts.mcpSessionId),
|
|
185
|
+
getSessionVirtualTools(opts.mcpSessionId),
|
|
186
|
+
]);
|
|
187
|
+
mcpToolDefs = [...registered, ...virtuals].map(t => ({
|
|
188
|
+
name: t.agentName,
|
|
189
|
+
description: t.description,
|
|
190
|
+
inputSchema: t.inputSchema,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Don't let a registry blip kill the whole agent run.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Skill bundles — structured `.codeep/skills/<name>/SKILL.md` directories
|
|
198
|
+
// the agent can discover and invoke via the `invoke_skill` tool. We just
|
|
199
|
+
// add the tool def here; the catalog block is appended to systemPrompt
|
|
200
|
+
// below alongside project rules / progress / etc. so we don't clobber
|
|
201
|
+
// those.
|
|
202
|
+
let skillCatalogBlock = '';
|
|
203
|
+
try {
|
|
204
|
+
const { loadSkillBundles, formatBundlesForSysprompt } = await import('./skillBundles.js');
|
|
205
|
+
const bundles = loadSkillBundles(projectContext.root);
|
|
206
|
+
if (bundles.length > 0) {
|
|
207
|
+
mcpToolDefs.push({
|
|
208
|
+
name: 'invoke_skill',
|
|
209
|
+
description: 'Invoke a Codeep skill bundle (curated workflow). Returns the SKILL.md body — follow its instructions step by step. Use when the user\'s request matches a skill\'s purpose.',
|
|
210
|
+
inputSchema: {
|
|
211
|
+
type: 'object',
|
|
212
|
+
properties: {
|
|
213
|
+
name: { type: 'string', description: 'Skill name from the catalog (e.g. "deploy").' },
|
|
214
|
+
},
|
|
215
|
+
required: ['name'],
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
skillCatalogBlock = formatBundlesForSysprompt(bundles);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Skill loading failure shouldn't fail the whole agent run.
|
|
223
|
+
}
|
|
168
224
|
// Build system prompt - use fallback format if native tools not supported
|
|
169
225
|
let systemPrompt = useNativeTools
|
|
170
226
|
? getAgentSystemPrompt(projectContext)
|
|
171
|
-
: getFallbackSystemPrompt(projectContext);
|
|
227
|
+
: getFallbackSystemPrompt(projectContext, mcpToolDefs);
|
|
172
228
|
// Inject project rules (from .codeep/rules.md or CODEEP.md)
|
|
173
229
|
const projectRules = loadProjectRules(projectContext.root);
|
|
174
230
|
if (projectRules) {
|
|
@@ -191,6 +247,12 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
191
247
|
if (chatHistoryStr) {
|
|
192
248
|
systemPrompt += chatHistoryStr;
|
|
193
249
|
}
|
|
250
|
+
// Skill bundles catalog goes last — closest to the user prompt so the
|
|
251
|
+
// model is most likely to remember the available skills when matching
|
|
252
|
+
// intent. Empty string when there are none.
|
|
253
|
+
if (skillCatalogBlock) {
|
|
254
|
+
systemPrompt += '\n\n' + skillCatalogBlock;
|
|
255
|
+
}
|
|
194
256
|
// Initial user message with optional task plan
|
|
195
257
|
let initialPrompt = prompt;
|
|
196
258
|
if (taskPlan) {
|
|
@@ -282,13 +344,35 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
282
344
|
// Calculate dynamic timeout based on task complexity
|
|
283
345
|
const dynamicTimeout = calculateDynamicTimeout(iteration, baseTimeout);
|
|
284
346
|
debug(`Using timeout: ${dynamicTimeout}ms (base: ${baseTimeout}ms)`);
|
|
347
|
+
// Refresh MCP tool list if a server flagged its catalog as changed
|
|
348
|
+
// (e.g. via `tools/list_changed` notification, or after an
|
|
349
|
+
// auto-restart). This keeps the agent in sync mid-run instead of
|
|
350
|
+
// requiring a session restart to see new tools.
|
|
351
|
+
if (opts.mcpSessionId) {
|
|
352
|
+
try {
|
|
353
|
+
const { consumeSessionCatalogChanges, getSessionTools } = await import('./mcpRegistry.js');
|
|
354
|
+
const dirty = consumeSessionCatalogChanges(opts.mcpSessionId);
|
|
355
|
+
if (dirty.has('tools')) {
|
|
356
|
+
const refreshed = await getSessionTools(opts.mcpSessionId);
|
|
357
|
+
mcpToolDefs = refreshed.map(t => ({
|
|
358
|
+
name: t.agentName,
|
|
359
|
+
description: t.description,
|
|
360
|
+
inputSchema: t.inputSchema,
|
|
361
|
+
}));
|
|
362
|
+
debug(`MCP tool catalog refreshed mid-run: ${mcpToolDefs.length} tool(s)`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// Don't let a refresh hiccup break the iteration.
|
|
367
|
+
}
|
|
368
|
+
}
|
|
285
369
|
// Get AI response with retry logic for timeouts
|
|
286
370
|
let chatResponse = null;
|
|
287
371
|
let retryCount = 0;
|
|
288
372
|
while (true) {
|
|
289
373
|
try {
|
|
290
|
-
chatResponse = await agentChat(messages, systemPrompt, opts.onChunk, opts.abortSignal, dynamicTimeout * (1 + retryCount * 0.5) // Increase timeout on retry
|
|
291
|
-
);
|
|
374
|
+
chatResponse = await agentChat(messages, systemPrompt, opts.onChunk, opts.abortSignal, dynamicTimeout * (1 + retryCount * 0.5), // Increase timeout on retry
|
|
375
|
+
mcpToolDefs);
|
|
292
376
|
consecutiveTimeouts = 0; // Reset consecutive count on success
|
|
293
377
|
consecutiveRateLimits = 0;
|
|
294
378
|
break;
|
|
@@ -559,12 +643,12 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
559
643
|
catch (err) {
|
|
560
644
|
debug('onExecuteCommand callback threw, falling back to local execution:', err);
|
|
561
645
|
// Fallback to local execution if callback throws
|
|
562
|
-
toolResult = await executeTool(toolCall, cwd);
|
|
646
|
+
toolResult = await executeTool(toolCall, cwd, opts.fs, opts.mcpSessionId);
|
|
563
647
|
}
|
|
564
648
|
}
|
|
565
649
|
}
|
|
566
650
|
else {
|
|
567
|
-
toolResult = await executeTool(toolCall, projectContext.root || process.cwd());
|
|
651
|
+
toolResult = await executeTool(toolCall, projectContext.root || process.cwd(), opts.fs, opts.mcpSessionId);
|
|
568
652
|
}
|
|
569
653
|
opts.onToolResult?.(toolResult, toolCall);
|
|
570
654
|
// Log action
|
|
@@ -737,7 +821,7 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
737
821
|
}
|
|
738
822
|
// Get AI response to fix errors
|
|
739
823
|
try {
|
|
740
|
-
const fixResponse = await agentChat(messages, systemPrompt, opts.onChunk, opts.abortSignal);
|
|
824
|
+
const fixResponse = await agentChat(messages, systemPrompt, opts.onChunk, opts.abortSignal, undefined, mcpToolDefs);
|
|
741
825
|
const { content: fixContent, toolCalls: fixToolCalls } = fixResponse;
|
|
742
826
|
if (fixToolCalls.length === 0) {
|
|
743
827
|
// Agent gave up or thinks it's fixed
|
|
@@ -749,7 +833,7 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
749
833
|
const fixResults = [];
|
|
750
834
|
for (const toolCall of fixToolCalls) {
|
|
751
835
|
opts.onToolCall?.(toolCall);
|
|
752
|
-
const toolResult = await executeTool(toolCall, projectContext.root || process.cwd());
|
|
836
|
+
const toolResult = await executeTool(toolCall, projectContext.root || process.cwd(), opts.fs, opts.mcpSessionId);
|
|
753
837
|
opts.onToolResult?.(toolResult, toolCall);
|
|
754
838
|
const actionLog = createActionLog(toolCall, toolResult);
|
|
755
839
|
actions.push(actionLog);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { ProjectContext } from './project';
|
|
15
15
|
import { Message } from '../config/index';
|
|
16
|
+
import { AdditionalToolDef } from './tools';
|
|
16
17
|
import type { AgentChatResponse } from './agentStream';
|
|
17
18
|
export type { AgentChatResponse };
|
|
18
19
|
/**
|
|
@@ -52,12 +53,19 @@ export declare function formatChatHistoryForAgent(history?: Array<{
|
|
|
52
53
|
content: string;
|
|
53
54
|
}>, maxChars?: number): string;
|
|
54
55
|
export declare function getAgentSystemPrompt(projectContext: ProjectContext): string;
|
|
55
|
-
export declare function getFallbackSystemPrompt(projectContext: ProjectContext): string;
|
|
56
|
+
export declare function getFallbackSystemPrompt(projectContext: ProjectContext, additionalTools?: AdditionalToolDef[]): string;
|
|
56
57
|
/**
|
|
57
58
|
* Make a chat API call for agent mode with native tool support.
|
|
58
59
|
* Falls back to agentChatFallback() if provider doesn't support tools.
|
|
59
60
|
*/
|
|
60
|
-
export declare function agentChat(messages: Message[], systemPrompt: string, onChunk?: (chunk: string) => void, abortSignal?: AbortSignal, dynamicTimeout?: number
|
|
61
|
+
export declare function agentChat(messages: Message[], systemPrompt: string, onChunk?: (chunk: string) => void, abortSignal?: AbortSignal, dynamicTimeout?: number,
|
|
62
|
+
/**
|
|
63
|
+
* Extra tool definitions appended to the catalog (currently used to
|
|
64
|
+
* surface MCP-registered tools as first-class entries the model can
|
|
65
|
+
* invoke). Optional — built-in tools work the same whether this is
|
|
66
|
+
* omitted or an empty array.
|
|
67
|
+
*/
|
|
68
|
+
additionalTools?: AdditionalToolDef[]): Promise<AgentChatResponse>;
|
|
61
69
|
/**
|
|
62
70
|
* Fallback chat without native tools (text-based tool format)
|
|
63
71
|
*/
|
package/dist/utils/agentChat.js
CHANGED
|
@@ -20,6 +20,7 @@ import { getProviderBaseUrl, getProviderAuthHeader, supportsNativeTools, getEffe
|
|
|
20
20
|
import { recordTokenUsage, extractOpenAIUsage, extractAnthropicUsage } from './tokenTracker.js';
|
|
21
21
|
import { parseOpenAIToolCalls, parseAnthropicToolCalls, parseToolCalls } from './toolParsing.js';
|
|
22
22
|
import { formatToolDefinitions, getOpenAITools, getAnthropicTools } from './tools.js';
|
|
23
|
+
import { readOpenRouterPreferences } from './openrouterPrefs.js';
|
|
23
24
|
import { handleStream, handleOpenAIAgentStream, handleAnthropicAgentStream } from './agentStream.js';
|
|
24
25
|
import { logger } from './logger.js';
|
|
25
26
|
const debug = (...args) => {
|
|
@@ -212,14 +213,21 @@ ${projectContext.structure ? `\n## Project Structure\n${projectContext.structure
|
|
|
212
213
|
return intelligence ? `\n\n${generateContextFromIntelligence(intelligence)}` : '';
|
|
213
214
|
})()}`;
|
|
214
215
|
}
|
|
215
|
-
export function getFallbackSystemPrompt(projectContext) {
|
|
216
|
-
return getAgentSystemPrompt(projectContext) + '\n\n' + formatToolDefinitions();
|
|
216
|
+
export function getFallbackSystemPrompt(projectContext, additionalTools) {
|
|
217
|
+
return getAgentSystemPrompt(projectContext) + '\n\n' + formatToolDefinitions(additionalTools);
|
|
217
218
|
}
|
|
218
219
|
/**
|
|
219
220
|
* Make a chat API call for agent mode with native tool support.
|
|
220
221
|
* Falls back to agentChatFallback() if provider doesn't support tools.
|
|
221
222
|
*/
|
|
222
|
-
export async function agentChat(messages, systemPrompt, onChunk, abortSignal, dynamicTimeout
|
|
223
|
+
export async function agentChat(messages, systemPrompt, onChunk, abortSignal, dynamicTimeout,
|
|
224
|
+
/**
|
|
225
|
+
* Extra tool definitions appended to the catalog (currently used to
|
|
226
|
+
* surface MCP-registered tools as first-class entries the model can
|
|
227
|
+
* invoke). Optional — built-in tools work the same whether this is
|
|
228
|
+
* omitted or an empty array.
|
|
229
|
+
*/
|
|
230
|
+
additionalTools) {
|
|
223
231
|
const protocol = config.get('protocol');
|
|
224
232
|
const model = config.get('model');
|
|
225
233
|
const providerId = config.get('provider');
|
|
@@ -255,6 +263,14 @@ export async function agentChat(messages, systemPrompt, onChunk, abortSignal, dy
|
|
|
255
263
|
}
|
|
256
264
|
if (protocol === 'anthropic')
|
|
257
265
|
headers['anthropic-version'] = '2023-06-01';
|
|
266
|
+
// OpenRouter branding — surfaces "Codeep" in the OpenRouter dashboard
|
|
267
|
+
// attribution so users (and OpenRouter itself, for any partnership
|
|
268
|
+
// tracking) see which app generated the traffic. Spec is informal; both
|
|
269
|
+
// headers are documented at openrouter.ai/docs#headers.
|
|
270
|
+
if (providerId === 'openrouter') {
|
|
271
|
+
headers['HTTP-Referer'] = 'https://codeep.dev';
|
|
272
|
+
headers['X-Title'] = 'Codeep';
|
|
273
|
+
}
|
|
258
274
|
try {
|
|
259
275
|
let endpoint;
|
|
260
276
|
let body;
|
|
@@ -264,18 +280,30 @@ export async function agentChat(messages, systemPrompt, onChunk, abortSignal, dy
|
|
|
264
280
|
const maxTok = getEffectiveMaxTokens(providerId, Math.max(config.get('maxTokens'), 16384));
|
|
265
281
|
const tokParam = usesMaxCompletionTokens(providerId) ? { max_completion_tokens: maxTok } : { max_tokens: maxTok };
|
|
266
282
|
endpoint = `${baseUrl}/chat/completions`;
|
|
283
|
+
// OpenRouter-specific extras: request `usage` block in the response
|
|
284
|
+
// body so we get per-call cost (skips our local pricing lookup), and
|
|
285
|
+
// optionally a provider-routing preferences object the user set via
|
|
286
|
+
// `/openrouter prefer …`.
|
|
287
|
+
const openRouterExtras = {};
|
|
288
|
+
if (providerId === 'openrouter') {
|
|
289
|
+
openRouterExtras.usage = { include: true };
|
|
290
|
+
const prefs = readOpenRouterPreferences();
|
|
291
|
+
if (prefs)
|
|
292
|
+
openRouterExtras.provider = prefs;
|
|
293
|
+
}
|
|
267
294
|
body = {
|
|
268
295
|
model, messages: [{ role: 'system', content: systemPrompt }, ...messages],
|
|
269
|
-
tools: getOpenAITools(), tool_choice: 'auto', stream: useStreaming,
|
|
296
|
+
tools: getOpenAITools(additionalTools), tool_choice: 'auto', stream: useStreaming,
|
|
270
297
|
...tempParam, ...tokParam,
|
|
271
298
|
...(useStreaming && providerId === 'openai' ? { stream_options: { include_usage: true } } : {}),
|
|
299
|
+
...openRouterExtras,
|
|
272
300
|
};
|
|
273
301
|
}
|
|
274
302
|
else {
|
|
275
303
|
endpoint = `${baseUrl}/v1/messages`;
|
|
276
304
|
body = {
|
|
277
305
|
model, system: systemPrompt, messages,
|
|
278
|
-
tools: getAnthropicTools(), stream: useStreaming,
|
|
306
|
+
tools: getAnthropicTools(additionalTools), stream: useStreaming,
|
|
279
307
|
...tempParam, max_tokens: getEffectiveMaxTokens(providerId, Math.max(config.get('maxTokens'), 16384)),
|
|
280
308
|
};
|
|
281
309
|
}
|
|
@@ -298,8 +326,15 @@ export async function agentChat(messages, systemPrompt, onChunk, abortSignal, dy
|
|
|
298
326
|
const data = await response.json();
|
|
299
327
|
const usageExtractor = protocol === 'openai' ? extractOpenAIUsage : extractAnthropicUsage;
|
|
300
328
|
const usage = usageExtractor(data);
|
|
301
|
-
if (usage)
|
|
302
|
-
|
|
329
|
+
if (usage) {
|
|
330
|
+
// OpenRouter returns the authoritative per-call cost in
|
|
331
|
+
// `usage.cost` (USD). Use it instead of our local pricing table
|
|
332
|
+
// since the catalog has 100+ models we don't track ourselves.
|
|
333
|
+
const reportedCost = providerId === 'openrouter' && typeof data?.usage?.cost === 'number'
|
|
334
|
+
? data.usage.cost
|
|
335
|
+
: undefined;
|
|
336
|
+
recordTokenUsage(usage, model, providerId, reportedCost);
|
|
337
|
+
}
|
|
303
338
|
if (protocol === 'openai') {
|
|
304
339
|
const message = data.choices?.[0]?.message;
|
|
305
340
|
const content = message?.content || '';
|
|
@@ -426,8 +461,12 @@ export async function agentChatFallback(messages, systemPrompt, onChunk, abortSi
|
|
|
426
461
|
const data = await response.json();
|
|
427
462
|
const fallbackUsageExtractor = protocol === 'openai' ? extractOpenAIUsage : extractAnthropicUsage;
|
|
428
463
|
const fallbackUsage = fallbackUsageExtractor(data);
|
|
429
|
-
if (fallbackUsage)
|
|
430
|
-
|
|
464
|
+
if (fallbackUsage) {
|
|
465
|
+
const reportedCost = providerId === 'openrouter' && typeof data?.usage?.cost === 'number'
|
|
466
|
+
? data.usage.cost
|
|
467
|
+
: undefined;
|
|
468
|
+
recordTokenUsage(fallbackUsage, model, providerId, reportedCost);
|
|
469
|
+
}
|
|
431
470
|
content = protocol === 'openai' ? (data.choices?.[0]?.message?.content || '') : (data.content?.[0]?.text || '');
|
|
432
471
|
}
|
|
433
472
|
const toolCalls = parseToolCalls(content);
|
|
@@ -120,8 +120,12 @@ export async function handleOpenAIAgentStream(body, onChunk, model, providerId)
|
|
|
120
120
|
}
|
|
121
121
|
if (usageData) {
|
|
122
122
|
const usage = extractOpenAIUsage(usageData);
|
|
123
|
-
if (usage)
|
|
124
|
-
|
|
123
|
+
if (usage) {
|
|
124
|
+
const reportedCost = providerId === 'openrouter' && typeof usageData?.usage?.cost === 'number'
|
|
125
|
+
? usageData.usage.cost
|
|
126
|
+
: undefined;
|
|
127
|
+
recordTokenUsage(usage, model, providerId, reportedCost);
|
|
128
|
+
}
|
|
125
129
|
}
|
|
126
130
|
const rawToolCalls = Array.from(toolCallMap.values()).map(tc => ({
|
|
127
131
|
id: tc.id,
|