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.
Files changed (60) hide show
  1. package/README.md +208 -0
  2. package/dist/acp/commands.js +770 -7
  3. package/dist/acp/protocol.d.ts +11 -2
  4. package/dist/acp/server.js +179 -11
  5. package/dist/acp/session.d.ts +3 -0
  6. package/dist/acp/session.js +5 -0
  7. package/dist/api/index.js +39 -6
  8. package/dist/config/index.d.ts +13 -0
  9. package/dist/config/index.js +45 -0
  10. package/dist/config/providers.js +76 -1
  11. package/dist/renderer/App.d.ts +12 -0
  12. package/dist/renderer/App.js +109 -4
  13. package/dist/renderer/agentExecution.js +5 -0
  14. package/dist/renderer/commands.js +638 -2
  15. package/dist/renderer/components/Help.js +28 -0
  16. package/dist/renderer/components/Login.d.ts +1 -0
  17. package/dist/renderer/components/Login.js +24 -9
  18. package/dist/renderer/handlers.d.ts +11 -1
  19. package/dist/renderer/handlers.js +30 -0
  20. package/dist/renderer/main.js +73 -0
  21. package/dist/utils/agent.d.ts +17 -0
  22. package/dist/utils/agent.js +91 -7
  23. package/dist/utils/agentChat.d.ts +10 -2
  24. package/dist/utils/agentChat.js +48 -9
  25. package/dist/utils/agentStream.js +6 -2
  26. package/dist/utils/checkpoints.d.ts +93 -0
  27. package/dist/utils/checkpoints.js +205 -0
  28. package/dist/utils/context.d.ts +24 -0
  29. package/dist/utils/context.js +57 -0
  30. package/dist/utils/customCommands.d.ts +62 -0
  31. package/dist/utils/customCommands.js +201 -0
  32. package/dist/utils/hooks.d.ts +97 -0
  33. package/dist/utils/hooks.js +223 -0
  34. package/dist/utils/mcpClient.d.ts +229 -0
  35. package/dist/utils/mcpClient.js +497 -0
  36. package/dist/utils/mcpConfig.d.ts +55 -0
  37. package/dist/utils/mcpConfig.js +177 -0
  38. package/dist/utils/mcpMarketplace.d.ts +49 -0
  39. package/dist/utils/mcpMarketplace.js +175 -0
  40. package/dist/utils/mcpRegistry.d.ts +129 -0
  41. package/dist/utils/mcpRegistry.js +427 -0
  42. package/dist/utils/mcpSamplingBridge.d.ts +32 -0
  43. package/dist/utils/mcpSamplingBridge.js +88 -0
  44. package/dist/utils/mcpStreamableHttp.d.ts +65 -0
  45. package/dist/utils/mcpStreamableHttp.js +207 -0
  46. package/dist/utils/openrouterPrefs.d.ts +36 -0
  47. package/dist/utils/openrouterPrefs.js +83 -0
  48. package/dist/utils/skillBundles.d.ts +84 -0
  49. package/dist/utils/skillBundles.js +257 -0
  50. package/dist/utils/skillBundlesCloud.d.ts +69 -0
  51. package/dist/utils/skillBundlesCloud.js +202 -0
  52. package/dist/utils/tokenTracker.d.ts +14 -2
  53. package/dist/utils/tokenTracker.js +59 -41
  54. package/dist/utils/toolExecution.d.ts +17 -1
  55. package/dist/utils/toolExecution.js +184 -6
  56. package/dist/utils/tools.d.ts +22 -6
  57. package/dist/utils/tools.js +83 -8
  58. package/package.json +3 -2
  59. package/bin/codeep-macos-arm64 +0 -0
  60. 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
  {
@@ -44,4 +44,5 @@ export declare class LoginScreen {
44
44
  export declare function renderProviderSelect(screen: Screen, providers: Array<{
45
45
  id: string;
46
46
  name: string;
47
+ description?: string;
47
48
  }>, selectedIndex: number): void;
@@ -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 Setup ═══';
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 = 'Select your AI provider';
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 boxWidth = Math.min(40, width - 4);
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
- let contentY = boxY + 2;
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 itemStyle = isSelected ? PRIMARY_BRIGHT + style.bold : fg.white;
223
- screen.write(contentX, contentY + i, prefix + provider.name, itemStyle);
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 | Enter Select', fg.gray);
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
- export declare function handleMenuKey(event: KeyEvent, ctx: MenuHandlerContext): void;
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 ──────────────────────────────────────────────────────────────
@@ -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) {
@@ -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;
@@ -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): Promise<AgentChatResponse>;
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
  */
@@ -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
- recordTokenUsage(usage, model, providerId);
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
- recordTokenUsage(fallbackUsage, model, providerId);
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
- recordTokenUsage(usage, model, providerId);
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,