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
package/dist/acp/protocol.d.ts
CHANGED
|
@@ -61,9 +61,18 @@ export interface InitializeResult {
|
|
|
61
61
|
}
|
|
62
62
|
export interface McpServer {
|
|
63
63
|
name: string;
|
|
64
|
-
command
|
|
65
|
-
|
|
64
|
+
/** Spawn command (stdio transport). Mutually exclusive with `url`. */
|
|
65
|
+
command?: string;
|
|
66
|
+
args?: string[];
|
|
66
67
|
env?: Record<string, string>;
|
|
68
|
+
/**
|
|
69
|
+
* If set, the client uses MCP Streamable HTTP transport against this URL
|
|
70
|
+
* instead of spawning a child process. Per spec, the same endpoint
|
|
71
|
+
* accepts POST (request) and GET (server-side SSE stream).
|
|
72
|
+
*/
|
|
73
|
+
url?: string;
|
|
74
|
+
/** Optional headers for the HTTP transport (Authorization etc.). */
|
|
75
|
+
headers?: Record<string, string>;
|
|
67
76
|
}
|
|
68
77
|
export interface SessionMode {
|
|
69
78
|
id: string;
|
package/dist/acp/server.js
CHANGED
|
@@ -6,6 +6,10 @@ import { readFile } from 'fs/promises';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { StdioTransport } from './transport.js';
|
|
8
8
|
import { runAgentSession } from './session.js';
|
|
9
|
+
import { loadCustomCommands } from '../utils/customCommands.js';
|
|
10
|
+
import { registerSessionServers, disposeSession as disposeMcpSession, disposeAllSessions as disposeAllMcpSessions } from '../utils/mcpRegistry.js';
|
|
11
|
+
import { loadMcpServerConfig, mergeMcpServers } from '../utils/mcpConfig.js';
|
|
12
|
+
import { handleMcpSamplingRequest } from '../utils/mcpSamplingBridge.js';
|
|
9
13
|
import { executeCommandAsync } from '../utils/shell.js';
|
|
10
14
|
import { initWorkspace, loadWorkspace, handleCommand } from './commands.js';
|
|
11
15
|
import { autoSaveSession, config, setProvider, setApiKey, listSessionsWithInfo, deleteSession as deleteSessionFile } from '../config/index.js';
|
|
@@ -22,6 +26,7 @@ const AVAILABLE_COMMANDS = [
|
|
|
22
26
|
{ name: 'help', description: 'Show available commands' },
|
|
23
27
|
{ name: 'status', description: 'Show current config and session info' },
|
|
24
28
|
{ name: 'version', description: 'Show version and current model' },
|
|
29
|
+
{ name: 'provider', description: 'List or switch provider', input: { hint: '<provider-id>' } },
|
|
25
30
|
{ name: 'model', description: 'List or switch model', input: { hint: '<model-id>' } },
|
|
26
31
|
{ name: 'login', description: 'Set API key for a provider', input: { hint: '<providerId> <apiKey>' } },
|
|
27
32
|
{ name: 'apikey', description: 'Show or set API key for current provider', input: { hint: '<key>' } },
|
|
@@ -38,13 +43,24 @@ const AVAILABLE_COMMANDS = [
|
|
|
38
43
|
{ name: 'undo', description: 'Undo last agent action' },
|
|
39
44
|
{ name: 'undo-all', description: 'Undo all agent actions in session' },
|
|
40
45
|
{ name: 'changes', description: 'Show all changes made in session' },
|
|
46
|
+
{ name: 'cost', description: 'Show per-session token usage and estimated cost' },
|
|
47
|
+
{ name: 'compact', description: 'Summarize older messages to free up context', input: { hint: '[keepN]' } },
|
|
48
|
+
{ name: 'checkpoint', description: 'Save a named snapshot of the current session (or `delete <id>`)', input: { hint: '[name] | delete <id>' } },
|
|
49
|
+
{ name: 'checkpoints', description: 'List saved checkpoints in this workspace' },
|
|
50
|
+
{ name: 'rewind', description: 'Restore a session checkpoint by id', input: { hint: '<id>' } },
|
|
51
|
+
{ name: 'hooks', description: 'List installed lifecycle hooks in .codeep/hooks/' },
|
|
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
|
+
{ 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]' } },
|
|
41
54
|
{ name: 'export', description: 'Export conversation', input: { hint: 'json | md | txt' } },
|
|
42
55
|
// Project intelligence
|
|
43
56
|
{ name: 'scan', description: 'Scan project structure and generate summary' },
|
|
44
57
|
{ name: 'review', description: 'Run code review on project or specific files', input: { hint: '[file…]' } },
|
|
45
58
|
{ name: 'learn', description: 'Learn coding preferences from project files' },
|
|
46
|
-
|
|
47
|
-
{ name: '
|
|
59
|
+
{ name: 'memory', description: 'Project memory notes — add / list / remove / clear', input: { hint: '<note> | list | remove <n> | clear' } },
|
|
60
|
+
{ name: 'profile', description: 'Save / load / delete provider+model presets', input: { hint: 'save | load | delete | list | <name>' } },
|
|
61
|
+
// Skills + custom commands
|
|
62
|
+
{ name: 'skills', description: 'List/create/share skill bundles. Subcommands: bundles, create-bundle, show, publish, install, browse, unpublish', input: { hint: '[query] | bundles | create-bundle <name> | show <name> | publish <slug> [--public] | install <owner>/<slug> | browse [q] | unpublish <owner>/<slug>' } },
|
|
63
|
+
{ name: 'commands', description: 'List user-authored commands from .codeep/commands/*.md' },
|
|
48
64
|
{ name: 'commit', description: 'Generate commit message and commit' },
|
|
49
65
|
{ name: 'fix', description: 'Fix bugs or issues' },
|
|
50
66
|
{ name: 'test', description: 'Write or run tests' },
|
|
@@ -313,6 +329,29 @@ export function startAcpServer() {
|
|
|
313
329
|
const transport = new StdioTransport();
|
|
314
330
|
// ACP sessionId → full AcpSession (includes history + codeep session tracking)
|
|
315
331
|
const sessions = new Map();
|
|
332
|
+
// Tear down all MCP child processes when the CLI dies. Without this,
|
|
333
|
+
// killing `codeep acp` with Ctrl+C orphans any servers we spawned —
|
|
334
|
+
// they keep running until the user hunts them down with `ps`.
|
|
335
|
+
// Register only once per process; if the user starts multiple ACP servers
|
|
336
|
+
// in the same process (we don't but be defensive) the second listener
|
|
337
|
+
// would double-fire.
|
|
338
|
+
const shutdownSignals = ['SIGINT', 'SIGTERM'];
|
|
339
|
+
let shuttingDown = false;
|
|
340
|
+
const onShutdown = (signal) => {
|
|
341
|
+
if (shuttingDown)
|
|
342
|
+
return;
|
|
343
|
+
shuttingDown = true;
|
|
344
|
+
disposeAllMcpSessions().finally(() => {
|
|
345
|
+
// Mimic default Node exit behaviour after our cleanup runs.
|
|
346
|
+
process.exit(signal === 'SIGINT' ? 130 : 143);
|
|
347
|
+
});
|
|
348
|
+
};
|
|
349
|
+
for (const sig of shutdownSignals) {
|
|
350
|
+
// Only attach if nothing else has claimed the signal — Node prints
|
|
351
|
+
// a warning when listener count > 10 per signal.
|
|
352
|
+
if (process.listenerCount(sig) === 0)
|
|
353
|
+
process.on(sig, onShutdown);
|
|
354
|
+
}
|
|
316
355
|
transport.start((msg) => {
|
|
317
356
|
// Notifications have no id — handle separately
|
|
318
357
|
if (!('id' in msg)) {
|
|
@@ -325,6 +364,9 @@ export function startAcpServer() {
|
|
|
325
364
|
handleInitialize(req);
|
|
326
365
|
break;
|
|
327
366
|
case 'initialized': /* no-op acknowledgment */ break;
|
|
367
|
+
case 'authenticate':
|
|
368
|
+
handleAuthenticate(req);
|
|
369
|
+
break;
|
|
328
370
|
case 'session/new':
|
|
329
371
|
handleSessionNew(req);
|
|
330
372
|
break;
|
|
@@ -397,10 +439,31 @@ export function startAcpServer() {
|
|
|
397
439
|
name: 'codeep',
|
|
398
440
|
version: getCurrentVersion(),
|
|
399
441
|
},
|
|
400
|
-
|
|
442
|
+
// We advertise a single "agent"-typed auth method even though Codeep
|
|
443
|
+
// authenticates out-of-band (env var, `codeep` CLI `/login`, or the
|
|
444
|
+
// VS Code "Codeep: Set API Key" command). The acp-registry CI check
|
|
445
|
+
// requires at least one method with type `agent` or `terminal` — and
|
|
446
|
+
// having an entry here also gives Zed something to render in its
|
|
447
|
+
// "agent settings" surface so users discover where to put their key.
|
|
448
|
+
authMethods: [
|
|
449
|
+
{
|
|
450
|
+
id: 'codeep-cli',
|
|
451
|
+
name: 'Codeep CLI',
|
|
452
|
+
description: 'Authenticate via the codeep CLI: run `codeep` and use `/login <provider> <key>`, ' +
|
|
453
|
+
'set the provider\'s env var (e.g. ZAI_API_KEY), or use "Codeep: Set API Key" in VS Code.',
|
|
454
|
+
},
|
|
455
|
+
],
|
|
401
456
|
};
|
|
402
457
|
transport.respond(msg.id, result);
|
|
403
458
|
}
|
|
459
|
+
// ── authenticate ────────────────────────────────────────────────────────────
|
|
460
|
+
// We don't actually run anything here — auth is handled out-of-band (env
|
|
461
|
+
// var, CLI /login, VS Code command). But per ACP spec the client may still
|
|
462
|
+
// dispatch authenticate after reading our advertised methods. Reply with
|
|
463
|
+
// empty success so the client unblocks and proceeds to session/new.
|
|
464
|
+
function handleAuthenticate(msg) {
|
|
465
|
+
transport.respond(msg.id, {});
|
|
466
|
+
}
|
|
404
467
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
405
468
|
// Title hint to the client. The ACP spec does NOT define a
|
|
406
469
|
// `session_info_update` variant — sending it caused Zed's internally-tagged
|
|
@@ -414,10 +477,51 @@ export function startAcpServer() {
|
|
|
414
477
|
function sendSessionTitle(_sessionId, _history, _fallback) {
|
|
415
478
|
// intentionally empty — see comment above
|
|
416
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* Background-spawn MCP servers for a session, merging two sources:
|
|
482
|
+
*
|
|
483
|
+
* 1. On-disk config (`.codeep/mcp_servers.json` project + global) —
|
|
484
|
+
* so a user who just runs `codeep acp` (no Zed-style settings UI)
|
|
485
|
+
* can still drive MCP setup by editing a file.
|
|
486
|
+
* 2. `mcpServers` passed in the session/* params — clients that have
|
|
487
|
+
* their own config (Zed, Claude Desktop) keep using that. ACP-
|
|
488
|
+
* provided servers override file entries with the same name.
|
|
489
|
+
*
|
|
490
|
+
* Don't block the session/* response on process startup (a hung MCP
|
|
491
|
+
* server would otherwise keep the chat from opening). Errors are logged
|
|
492
|
+
* + cached for /mcp display. `mcpRegistry.callSessionTool` awaits the
|
|
493
|
+
* in-flight registration so tool calls don't race the startup.
|
|
494
|
+
*/
|
|
495
|
+
function spawnMcpServersForSession(acpSessionId, cwd, acpServers, label) {
|
|
496
|
+
const fromConfig = loadMcpServerConfig(cwd);
|
|
497
|
+
const merged = mergeMcpServers(fromConfig, acpServers);
|
|
498
|
+
if (merged.length === 0)
|
|
499
|
+
return;
|
|
500
|
+
registerSessionServers(acpSessionId, merged, {
|
|
501
|
+
workspaceRoot: cwd,
|
|
502
|
+
// Servers that opted into the `sampling` capability can ask us to
|
|
503
|
+
// run a completion on their behalf. We bridge to chat() through
|
|
504
|
+
// mcpSamplingBridge — using the user's currently active provider
|
|
505
|
+
// and key. The bridge enforces a per-server rate limit + budget cap
|
|
506
|
+
// so a misbehaving server can't drain the user's API credits.
|
|
507
|
+
onSamplingRequest: (params, serverName) => handleMcpSamplingRequest(params, serverName),
|
|
508
|
+
})
|
|
509
|
+
.then(({ registered, errors }) => {
|
|
510
|
+
if (registered.length > 0) {
|
|
511
|
+
process.stderr.write(`[codeep-acp] MCP (${label}): registered ${registered.length} tool(s) from ${merged.length} server(s)\n`);
|
|
512
|
+
}
|
|
513
|
+
for (const e of errors) {
|
|
514
|
+
process.stderr.write(`[codeep-acp] MCP server "${e.server}" failed (${label}): ${e.error}\n`);
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
.catch(err => process.stderr.write(`[codeep-acp] MCP registration crashed (${label}): ${err.message}\n`));
|
|
518
|
+
}
|
|
417
519
|
// ── session/new ─────────────────────────────────────────────────────────────
|
|
418
520
|
function handleSessionNew(msg) {
|
|
419
521
|
const params = msg.params;
|
|
420
522
|
const acpSessionId = randomUUID();
|
|
523
|
+
// Spin up MCP servers in the background. Errors surface via /mcp.
|
|
524
|
+
spawnMcpServersForSession(acpSessionId, params.cwd, params.mcpServers, 'session/new');
|
|
421
525
|
const { codeepSessionId, history, welcomeText } = initWorkspace(params.cwd, params.fresh);
|
|
422
526
|
sessions.set(acpSessionId, {
|
|
423
527
|
sessionId: acpSessionId,
|
|
@@ -443,7 +547,7 @@ export function startAcpServer() {
|
|
|
443
547
|
// in a separate task). Without the delay the notification arrives ~1 ms
|
|
444
548
|
// after the response and gets lost, which manifests as
|
|
445
549
|
// "Available commands: none" in the slash menu.
|
|
446
|
-
sendCommandsDelayed(acpSessionId);
|
|
550
|
+
sendCommandsDelayed(acpSessionId, params.cwd);
|
|
447
551
|
// Send title immediately so Zed "Recent" panel shows something useful
|
|
448
552
|
sendSessionTitle(acpSessionId, history, pathBasename(params.cwd));
|
|
449
553
|
// Send welcome message
|
|
@@ -459,13 +563,36 @@ export function startAcpServer() {
|
|
|
459
563
|
// 200 ms is comfortably above the observed ~1 ms race window without
|
|
460
564
|
// making the slash menu feel laggy on first paint.
|
|
461
565
|
const COMMANDS_DELAY_MS = Number(process.env.CODEEP_ACP_COMMANDS_DELAY_MS ?? 200);
|
|
462
|
-
|
|
566
|
+
/**
|
|
567
|
+
* Build the autocomplete catalog for a session: built-in commands plus any
|
|
568
|
+
* user-authored Markdown templates under `.codeep/commands/`. Custom ones
|
|
569
|
+
* are tagged in the description so the user can tell them apart from
|
|
570
|
+
* built-ins in Zed / VS Code dropdowns.
|
|
571
|
+
*/
|
|
572
|
+
function getAvailableCommandsForSession(workspaceRoot) {
|
|
573
|
+
try {
|
|
574
|
+
const custom = loadCustomCommands(workspaceRoot).map(c => ({
|
|
575
|
+
name: c.name,
|
|
576
|
+
description: `[${c.scope === 'project' ? 'project' : 'global'}] ${c.description}`,
|
|
577
|
+
input: { hint: '[args]' },
|
|
578
|
+
}));
|
|
579
|
+
// Custom commands can't override built-ins (would break /help, /status etc.)
|
|
580
|
+
const builtinNames = new Set(AVAILABLE_COMMANDS.map(c => c.name));
|
|
581
|
+
const safeCustom = custom.filter(c => !builtinNames.has(c.name));
|
|
582
|
+
return [...AVAILABLE_COMMANDS, ...safeCustom];
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// Custom-command loading must never block the autocomplete catalog.
|
|
586
|
+
return AVAILABLE_COMMANDS;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function sendCommandsDelayed(sessionId, workspaceRoot) {
|
|
463
590
|
setTimeout(() => {
|
|
464
591
|
transport.notify('session/update', {
|
|
465
592
|
sessionId,
|
|
466
593
|
update: {
|
|
467
594
|
sessionUpdate: 'available_commands_update',
|
|
468
|
-
availableCommands:
|
|
595
|
+
availableCommands: getAvailableCommandsForSession(workspaceRoot),
|
|
469
596
|
},
|
|
470
597
|
});
|
|
471
598
|
}, COMMANDS_DELAY_MS);
|
|
@@ -478,6 +605,9 @@ export function startAcpServer() {
|
|
|
478
605
|
if (existing) {
|
|
479
606
|
// Session already in memory — update cwd if changed
|
|
480
607
|
existing.workspaceRoot = params.cwd;
|
|
608
|
+
// Re-spawn any MCP servers the client passed in (they may have changed
|
|
609
|
+
// since session/new; old ones get disposed by registerSessionServers).
|
|
610
|
+
spawnMcpServersForSession(params.sessionId, params.cwd, params.mcpServers, 'session/load (warm)');
|
|
481
611
|
const result = {
|
|
482
612
|
modes: AGENT_MODES,
|
|
483
613
|
configOptions: buildConfigOptions(),
|
|
@@ -488,6 +618,7 @@ export function startAcpServer() {
|
|
|
488
618
|
// Session not in memory — try to load from disk
|
|
489
619
|
const { codeepSessionId, history, welcomeText } = loadWorkspace(params.cwd, params.sessionId);
|
|
490
620
|
const acpSessionId = randomUUID();
|
|
621
|
+
spawnMcpServersForSession(acpSessionId, params.cwd, params.mcpServers, 'session/load (cold)');
|
|
491
622
|
sessions.set(acpSessionId, {
|
|
492
623
|
sessionId: acpSessionId,
|
|
493
624
|
workspaceRoot: params.cwd,
|
|
@@ -508,7 +639,7 @@ export function startAcpServer() {
|
|
|
508
639
|
transport.respond(msg.id, result);
|
|
509
640
|
// Re-advertise commands (delayed for the same race-condition reason as
|
|
510
641
|
// session/new — see sendCommandsDelayed comment).
|
|
511
|
-
sendCommandsDelayed(acpSessionId);
|
|
642
|
+
sendCommandsDelayed(acpSessionId, params.cwd);
|
|
512
643
|
// Send title immediately so Zed "Recent" panel shows something useful
|
|
513
644
|
sendSessionTitle(params.sessionId, history, pathBasename(params.cwd));
|
|
514
645
|
// Send restored session welcome
|
|
@@ -530,6 +661,9 @@ export function startAcpServer() {
|
|
|
530
661
|
const existing = sessions.get(params.sessionId);
|
|
531
662
|
if (existing) {
|
|
532
663
|
existing.workspaceRoot = params.cwd;
|
|
664
|
+
// Resume can carry an updated mcpServers list (e.g. workspace switched
|
|
665
|
+
// config) — re-register so old servers are torn down and new ones spawn.
|
|
666
|
+
spawnMcpServersForSession(params.sessionId, params.cwd, params.mcpServers, 'session/resume (warm)');
|
|
533
667
|
const result = {
|
|
534
668
|
sessionId: params.sessionId,
|
|
535
669
|
modes: AGENT_MODES,
|
|
@@ -537,13 +671,14 @@ export function startAcpServer() {
|
|
|
537
671
|
};
|
|
538
672
|
transport.respond(msg.id, result);
|
|
539
673
|
// Delayed — see sendCommandsDelayed comment for the race-condition rationale.
|
|
540
|
-
sendCommandsDelayed(params.sessionId);
|
|
674
|
+
sendCommandsDelayed(params.sessionId, params.cwd);
|
|
541
675
|
return;
|
|
542
676
|
}
|
|
543
677
|
// Session not in memory — load from disk but skip the welcome banner and
|
|
544
678
|
// the history echo (resume contract: client already has history).
|
|
545
679
|
const { codeepSessionId, history } = loadWorkspace(params.cwd, params.sessionId);
|
|
546
680
|
const acpSessionId = randomUUID();
|
|
681
|
+
spawnMcpServersForSession(acpSessionId, params.cwd, params.mcpServers, 'session/resume (cold)');
|
|
547
682
|
sessions.set(acpSessionId, {
|
|
548
683
|
sessionId: acpSessionId,
|
|
549
684
|
workspaceRoot: params.cwd,
|
|
@@ -561,7 +696,7 @@ export function startAcpServer() {
|
|
|
561
696
|
configOptions: buildConfigOptions(),
|
|
562
697
|
};
|
|
563
698
|
transport.respond(msg.id, result);
|
|
564
|
-
sendCommandsDelayed(acpSessionId);
|
|
699
|
+
sendCommandsDelayed(acpSessionId, params.cwd);
|
|
565
700
|
}
|
|
566
701
|
// ── session/set_mode ────────────────────────────────────────────────────────
|
|
567
702
|
function handleSetMode(msg) {
|
|
@@ -672,6 +807,9 @@ export function startAcpServer() {
|
|
|
672
807
|
const { sessionId, cwd } = (msg.params ?? {});
|
|
673
808
|
// Remove from in-memory sessions map if present
|
|
674
809
|
sessions.delete(sessionId);
|
|
810
|
+
// Tear down any MCP server processes attached to this session — leaks
|
|
811
|
+
// children otherwise. Fire-and-forget; client doesn't wait on stop().
|
|
812
|
+
disposeMcpSession(sessionId).catch(() => { });
|
|
675
813
|
// Try project dir first, then global
|
|
676
814
|
const deleted = deleteSessionFile(sessionId, cwd || undefined);
|
|
677
815
|
if (!deleted && cwd)
|
|
@@ -707,12 +845,13 @@ export function startAcpServer() {
|
|
|
707
845
|
// `available_commands_update` from session/new because the thread_view
|
|
708
846
|
// isn't registered yet on Zed's side (race against the session/new
|
|
709
847
|
// response). Re-sending here guarantees `/` autocomplete works by the
|
|
710
|
-
// time the user could plausibly type the next prompt.
|
|
848
|
+
// time the user could plausibly type the next prompt. Also picks up any
|
|
849
|
+
// custom command Markdown files the user added since session start.
|
|
711
850
|
transport.notify('session/update', {
|
|
712
851
|
sessionId: params.sessionId,
|
|
713
852
|
update: {
|
|
714
853
|
sessionUpdate: 'available_commands_update',
|
|
715
|
-
availableCommands:
|
|
854
|
+
availableCommands: getAvailableCommandsForSession(session.workspaceRoot),
|
|
716
855
|
},
|
|
717
856
|
});
|
|
718
857
|
// Extract text from ContentBlock[]
|
|
@@ -927,6 +1066,35 @@ export function startAcpServer() {
|
|
|
927
1066
|
return result.outcome.optionId;
|
|
928
1067
|
}
|
|
929
1068
|
: undefined,
|
|
1069
|
+
// Per ACP spec, `fs/read_text_file` and `fs/write_text_file` are
|
|
1070
|
+
// CLIENT methods — only safe to call when the client advertised
|
|
1071
|
+
// the capability in `initialize`. Routing through the client
|
|
1072
|
+
// means the editor's dirty buffers + undo history stay correct
|
|
1073
|
+
// (otherwise an in-editor unsaved change would be invisible to
|
|
1074
|
+
// the agent, or worse, silently overwritten).
|
|
1075
|
+
fs: {
|
|
1076
|
+
readTextFile: clientSupportsFsRead
|
|
1077
|
+
? async (absolutePath) => {
|
|
1078
|
+
const result = await transport.request('fs/read_text_file', {
|
|
1079
|
+
sessionId: params.sessionId,
|
|
1080
|
+
path: absolutePath,
|
|
1081
|
+
});
|
|
1082
|
+
if (!result || typeof result.content !== 'string') {
|
|
1083
|
+
throw new Error('fs/read_text_file returned no content');
|
|
1084
|
+
}
|
|
1085
|
+
return result.content;
|
|
1086
|
+
}
|
|
1087
|
+
: undefined,
|
|
1088
|
+
writeTextFile: clientSupportsFsWrite
|
|
1089
|
+
? async (absolutePath, content) => {
|
|
1090
|
+
await transport.request('fs/write_text_file', {
|
|
1091
|
+
sessionId: params.sessionId,
|
|
1092
|
+
path: absolutePath,
|
|
1093
|
+
content,
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
: undefined,
|
|
1097
|
+
},
|
|
930
1098
|
onExecuteCommand: async (command, args, cwd) => {
|
|
931
1099
|
// Per ACP spec, only call terminal/* if the client advertised the
|
|
932
1100
|
// capability in initialize. Otherwise execute locally.
|
package/dist/acp/session.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PermissionOutcome } from '../utils/agent.js';
|
|
2
2
|
import { ProjectContext } from '../utils/project.js';
|
|
3
3
|
import { ToolCall } from '../utils/tools.js';
|
|
4
|
+
import type { FsCallbacks } from '../utils/toolExecution.js';
|
|
4
5
|
export interface AgentSessionOptions {
|
|
5
6
|
prompt: string;
|
|
6
7
|
workspaceRoot: string;
|
|
@@ -15,6 +16,8 @@ export interface AgentSessionOptions {
|
|
|
15
16
|
stderr: string;
|
|
16
17
|
exitCode: number;
|
|
17
18
|
}>;
|
|
19
|
+
/** Optional fs delegation when the ACP client advertises `fs` capability. */
|
|
20
|
+
fs?: FsCallbacks;
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
20
23
|
* Build a ProjectContext from a workspace root directory.
|
package/dist/acp/session.js
CHANGED
|
@@ -146,6 +146,11 @@ export async function runAgentSession(opts) {
|
|
|
146
146
|
},
|
|
147
147
|
onRequestPermission: opts.onRequestPermission,
|
|
148
148
|
onExecuteCommand: opts.onExecuteCommand,
|
|
149
|
+
fs: opts.fs,
|
|
150
|
+
// Route MCP-prefixed tool calls through the per-session registry.
|
|
151
|
+
// `conversationId` is the ACP session id, which is what
|
|
152
|
+
// registerSessionServers keyed by in server.ts handleSessionNew.
|
|
153
|
+
mcpSessionId: opts.conversationId,
|
|
149
154
|
});
|
|
150
155
|
// result.finalResponse is already emitted via onChunk streaming above;
|
|
151
156
|
// only emit it here if nothing was streamed (e.g. non-streaming fallback path)
|
package/dist/api/index.js
CHANGED
|
@@ -7,6 +7,19 @@ import { logApiRequest, logApiResponse } from '../utils/logger.js';
|
|
|
7
7
|
import { loadProjectIntelligence, generateContextFromIntelligence } from '../utils/projectIntelligence.js';
|
|
8
8
|
import { loadProjectRules } from '../utils/agent.js';
|
|
9
9
|
import { recordTokenUsage, extractOpenAIUsage, extractAnthropicUsage } from '../utils/tokenTracker.js';
|
|
10
|
+
/**
|
|
11
|
+
* OpenRouter returns the authoritative per-call USD in `usage.cost` when
|
|
12
|
+
* the request opts in via `usage: { include: true }`. Pull it here so
|
|
13
|
+
* every chat() / streamChat() / etc. path records the real cost instead
|
|
14
|
+
* of our local pricing estimate. Returns undefined for non-OpenRouter
|
|
15
|
+
* providers or when the field is missing (older OpenRouter API responses).
|
|
16
|
+
*/
|
|
17
|
+
function openRouterReportedCost(providerId, data) {
|
|
18
|
+
if (providerId !== 'openrouter')
|
|
19
|
+
return undefined;
|
|
20
|
+
const cost = data?.usage?.cost;
|
|
21
|
+
return typeof cost === 'number' && Number.isFinite(cost) ? cost : undefined;
|
|
22
|
+
}
|
|
10
23
|
import { getTaskContextPrompt } from '../utils/taskContext.js';
|
|
11
24
|
// Error messages by language
|
|
12
25
|
const ERROR_MESSAGES = {
|
|
@@ -331,6 +344,21 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
|
|
|
331
344
|
else {
|
|
332
345
|
headers['x-api-key'] = apiKey;
|
|
333
346
|
}
|
|
347
|
+
// OpenRouter: branding headers + opt in to `usage.cost` so the
|
|
348
|
+
// chat path reports authoritative per-call cost just like agentChat
|
|
349
|
+
// does. Kept identical to the agentChat block so the two paths stay
|
|
350
|
+
// in lockstep.
|
|
351
|
+
if (providerId === 'openrouter') {
|
|
352
|
+
headers['HTTP-Referer'] = 'https://codeep.dev';
|
|
353
|
+
headers['X-Title'] = 'Codeep';
|
|
354
|
+
}
|
|
355
|
+
// Lazy-loaded preferences object — only attached for openrouter so we
|
|
356
|
+
// never send the field to providers that don't understand it.
|
|
357
|
+
let openRouterProvider = undefined;
|
|
358
|
+
if (providerId === 'openrouter') {
|
|
359
|
+
const { readOpenRouterPreferences } = await import('../utils/openrouterPrefs.js');
|
|
360
|
+
openRouterProvider = readOpenRouterPreferences() ?? undefined;
|
|
361
|
+
}
|
|
334
362
|
const requestBody = JSON.stringify({
|
|
335
363
|
model,
|
|
336
364
|
messages,
|
|
@@ -338,6 +366,8 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
|
|
|
338
366
|
...(stream ? { stream_options: { include_usage: true } } : {}),
|
|
339
367
|
...(omitTemperature ? {} : { temperature }),
|
|
340
368
|
...(useCompletionTokens ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens }),
|
|
369
|
+
...(providerId === 'openrouter' ? { usage: { include: true } } : {}),
|
|
370
|
+
...(openRouterProvider ? { provider: openRouterProvider } : {}),
|
|
341
371
|
});
|
|
342
372
|
try {
|
|
343
373
|
// Use node:http for Ollama — bypasses undici connection pooling (AggregateError in Node v24)
|
|
@@ -358,7 +388,7 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
|
|
|
358
388
|
const parsed = JSON.parse(text);
|
|
359
389
|
const usage = extractOpenAIUsage(parsed);
|
|
360
390
|
if (usage)
|
|
361
|
-
recordTokenUsage(usage, model, providerId);
|
|
391
|
+
recordTokenUsage(usage, model, providerId, openRouterReportedCost(providerId, parsed));
|
|
362
392
|
return stripThinkTags(parsed.choices[0]?.message?.content || '');
|
|
363
393
|
}
|
|
364
394
|
}
|
|
@@ -373,13 +403,13 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
|
|
|
373
403
|
throw new ApiError(`${getErrorMessage('apiError')}: ${parseApiError(response.status, body)}`, response.status);
|
|
374
404
|
}
|
|
375
405
|
if (stream && response.body) {
|
|
376
|
-
return handleOpenAIStream(response.body, onChunk);
|
|
406
|
+
return handleOpenAIStream(response.body, onChunk, providerId, model);
|
|
377
407
|
}
|
|
378
408
|
else {
|
|
379
409
|
const data = await response.json();
|
|
380
410
|
const usage = extractOpenAIUsage(data);
|
|
381
411
|
if (usage)
|
|
382
|
-
recordTokenUsage(usage, model,
|
|
412
|
+
recordTokenUsage(usage, model, providerId, openRouterReportedCost(providerId, data));
|
|
383
413
|
const content = data.choices[0]?.message?.content || '';
|
|
384
414
|
return stripThinkTags(content);
|
|
385
415
|
}
|
|
@@ -450,8 +480,9 @@ async function handleNodeStream(nodeStream, onChunk, model) {
|
|
|
450
480
|
}
|
|
451
481
|
if (parsed.usage) {
|
|
452
482
|
const usage = extractOpenAIUsage(parsed);
|
|
483
|
+
const provider = config.get('provider');
|
|
453
484
|
if (usage)
|
|
454
|
-
recordTokenUsage(usage, parsed.model || model,
|
|
485
|
+
recordTokenUsage(usage, parsed.model || model, provider, openRouterReportedCost(provider, parsed));
|
|
455
486
|
}
|
|
456
487
|
}
|
|
457
488
|
catch { /* ignore parse errors */ }
|
|
@@ -466,7 +497,7 @@ async function handleNodeStream(nodeStream, onChunk, model) {
|
|
|
466
497
|
});
|
|
467
498
|
});
|
|
468
499
|
}
|
|
469
|
-
async function handleOpenAIStream(body, onChunk) {
|
|
500
|
+
async function handleOpenAIStream(body, onChunk, providerId, modelOverride) {
|
|
470
501
|
const reader = body.getReader();
|
|
471
502
|
const decoder = new TextDecoder();
|
|
472
503
|
const chunks = [];
|
|
@@ -494,8 +525,10 @@ async function handleOpenAIStream(body, onChunk) {
|
|
|
494
525
|
// Capture usage from final chunk (stream_options: include_usage)
|
|
495
526
|
if (parsed.usage) {
|
|
496
527
|
const usage = extractOpenAIUsage(parsed);
|
|
528
|
+
const provider = providerId ?? config.get('provider');
|
|
529
|
+
const m = parsed.model || modelOverride || 'unknown';
|
|
497
530
|
if (usage)
|
|
498
|
-
recordTokenUsage(usage,
|
|
531
|
+
recordTokenUsage(usage, m, provider, openRouterReportedCost(provider, parsed));
|
|
499
532
|
}
|
|
500
533
|
}
|
|
501
534
|
catch {
|
package/dist/config/index.d.ts
CHANGED
|
@@ -49,6 +49,14 @@ interface ConfigSchema {
|
|
|
49
49
|
githubUsername: string;
|
|
50
50
|
syncToken: string;
|
|
51
51
|
deviceId: string;
|
|
52
|
+
/** OpenRouter provider-routing preferences (see utils/openrouterPrefs.ts). */
|
|
53
|
+
openrouterPreferences?: {
|
|
54
|
+
order?: string[];
|
|
55
|
+
allow_fallbacks?: boolean;
|
|
56
|
+
ignore?: string[];
|
|
57
|
+
data_collection?: 'allow' | 'deny';
|
|
58
|
+
require_parameters?: boolean;
|
|
59
|
+
};
|
|
52
60
|
}
|
|
53
61
|
export type { AgentMode };
|
|
54
62
|
export type { LanguageCode };
|
|
@@ -105,6 +113,11 @@ export declare function getCurrentProvider(): {
|
|
|
105
113
|
};
|
|
106
114
|
export declare function setProvider(providerId: string): boolean;
|
|
107
115
|
export declare function getModelsForCurrentProvider(): Record<string, string>;
|
|
116
|
+
export declare function fetchOpenRouterModels(apiKey?: string): Promise<{
|
|
117
|
+
id: string;
|
|
118
|
+
name: string;
|
|
119
|
+
description: string;
|
|
120
|
+
}[] | null>;
|
|
108
121
|
export declare function fetchOllamaModels(baseUrl?: string): Promise<{
|
|
109
122
|
id: string;
|
|
110
123
|
name: string;
|
package/dist/config/index.js
CHANGED
|
@@ -466,6 +466,51 @@ export function getModelsForCurrentProvider() {
|
|
|
466
466
|
}
|
|
467
467
|
return models;
|
|
468
468
|
}
|
|
469
|
+
let openRouterCache = null;
|
|
470
|
+
const OPENROUTER_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
471
|
+
export async function fetchOpenRouterModels(apiKey) {
|
|
472
|
+
if (openRouterCache && Date.now() - openRouterCache.fetchedAt < OPENROUTER_CACHE_TTL_MS) {
|
|
473
|
+
return openRouterCache.models;
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
const headers = { 'Accept': 'application/json' };
|
|
477
|
+
if (apiKey)
|
|
478
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
479
|
+
const res = await fetch('https://openrouter.ai/api/v1/models', {
|
|
480
|
+
headers,
|
|
481
|
+
signal: AbortSignal.timeout(10_000),
|
|
482
|
+
});
|
|
483
|
+
if (!res.ok)
|
|
484
|
+
return null;
|
|
485
|
+
const data = await res.json();
|
|
486
|
+
if (!Array.isArray(data.data))
|
|
487
|
+
return null;
|
|
488
|
+
// Format pricing for the dropdown description so users can pick cheap
|
|
489
|
+
// models without leaving the picker. OpenRouter returns USD-per-token
|
|
490
|
+
// as strings like "0.000003" — multiply by 1M for the per-1M figure
|
|
491
|
+
// most users think in.
|
|
492
|
+
const models = data.data
|
|
493
|
+
.filter(m => typeof m.id === 'string')
|
|
494
|
+
.map(m => {
|
|
495
|
+
const inPrice = m.pricing?.prompt ? Number(m.pricing.prompt) * 1_000_000 : null;
|
|
496
|
+
const outPrice = m.pricing?.completion ? Number(m.pricing.completion) * 1_000_000 : null;
|
|
497
|
+
const priceStr = inPrice !== null && outPrice !== null
|
|
498
|
+
? ` · $${inPrice.toFixed(2)} in / $${outPrice.toFixed(2)} out per 1M`
|
|
499
|
+
: '';
|
|
500
|
+
const ctxStr = m.context_length ? ` · ${Math.round(m.context_length / 1000)}K ctx` : '';
|
|
501
|
+
return {
|
|
502
|
+
id: m.id,
|
|
503
|
+
name: m.name ?? m.id,
|
|
504
|
+
description: (m.description?.slice(0, 80) ?? 'OpenRouter model') + priceStr + ctxStr,
|
|
505
|
+
};
|
|
506
|
+
});
|
|
507
|
+
openRouterCache = { fetchedAt: Date.now(), models };
|
|
508
|
+
return models;
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
469
514
|
export async function fetchOllamaModels(baseUrl) {
|
|
470
515
|
const url = (baseUrl || config.get('ollamaUrl') || 'http://localhost:11434').replace(/\/$/, '');
|
|
471
516
|
try {
|