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
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Codeep skill marketplace — CLI ↔ codeep.dev/api/skills bridge.
3
+ *
4
+ * Talks to the REST endpoints in `Codeep-web/src/app/api/skills/`:
5
+ * GET /api/skills?q=&mine=1 → browse
6
+ * POST /api/skills → publish (auth required)
7
+ * GET /api/skills/:owner/:slug → fetch one (auth req for private)
8
+ * DELETE /api/skills/:owner/:slug → unpublish (owner only)
9
+ *
10
+ * Auth uses the same `x-sync-token` header `codeepCloud.ts` already sends
11
+ * for /api/tasks and friends.
12
+ */
13
+ export interface RemoteSkill {
14
+ id: number;
15
+ github_id: string;
16
+ owner_username: string | null;
17
+ slug: string;
18
+ name: string;
19
+ description: string;
20
+ body: string;
21
+ version: string | null;
22
+ visibility: 'public' | 'private';
23
+ install_count: number;
24
+ updated_at: string;
25
+ }
26
+ /**
27
+ * Publish a skill bundle to codeep.dev. The bundle may be project-scoped
28
+ * (`<workspaceRoot>/.codeep/skills/<slug>/SKILL.md`) or global
29
+ * (`~/.codeep/skills/<slug>/SKILL.md`) — either is publishable. The
30
+ * `--public` flag (translated into `opts.isPublic`) is the user's
31
+ * explicit consent gate; we don't gate further on bundle scope. If both
32
+ * exist with the same slug, the project copy wins (mirrors the rest of
33
+ * the bundle-loading flow).
34
+ */
35
+ export declare function publishBundle(workspaceRoot: string, slug: string, opts?: {
36
+ isPublic?: boolean;
37
+ }): Promise<{
38
+ ok: boolean;
39
+ skill?: RemoteSkill;
40
+ error?: string;
41
+ }>;
42
+ /**
43
+ * Install a skill from the marketplace into the project's
44
+ * `.codeep/skills/<slug>/SKILL.md`. `idOrPath` may be either
45
+ * `<owner>/<slug>` (preferred) or a numeric id.
46
+ */
47
+ export declare function installBundle(workspaceRoot: string, idOrPath: string): Promise<{
48
+ ok: boolean;
49
+ name?: string;
50
+ error?: string;
51
+ }>;
52
+ /** List public skills (or own when `mine=true`). Returns up to 100. */
53
+ export declare function browseSkills(opts?: {
54
+ query?: string;
55
+ mine?: boolean;
56
+ }): Promise<{
57
+ ok: boolean;
58
+ skills?: RemoteSkill[];
59
+ error?: string;
60
+ }>;
61
+ /** Unpublish a skill (owner only). */
62
+ export declare function unpublishBundle(idOrPath: string): Promise<{
63
+ ok: boolean;
64
+ error?: string;
65
+ }>;
66
+ /** Read raw SKILL.md from disk — used when we want the unmodified bytes. */
67
+ export declare function readRawSkillMd(workspaceRoot: string, slug: string): string | null;
68
+ /** Delete the local copy of an installed skill bundle (for /skills uninstall). */
69
+ export declare function uninstallLocalBundle(workspaceRoot: string, slug: string): boolean;
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Codeep skill marketplace — CLI ↔ codeep.dev/api/skills bridge.
3
+ *
4
+ * Talks to the REST endpoints in `Codeep-web/src/app/api/skills/`:
5
+ * GET /api/skills?q=&mine=1 → browse
6
+ * POST /api/skills → publish (auth required)
7
+ * GET /api/skills/:owner/:slug → fetch one (auth req for private)
8
+ * DELETE /api/skills/:owner/:slug → unpublish (owner only)
9
+ *
10
+ * Auth uses the same `x-sync-token` header `codeepCloud.ts` already sends
11
+ * for /api/tasks and friends.
12
+ */
13
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'fs';
14
+ import { join } from 'path';
15
+ import { homedir } from 'os';
16
+ import { getSyncToken } from '../config/index.js';
17
+ import { findSkillBundle } from './skillBundles.js';
18
+ const API_BASE = 'https://codeep.dev';
19
+ /**
20
+ * Publish a skill bundle to codeep.dev. The bundle may be project-scoped
21
+ * (`<workspaceRoot>/.codeep/skills/<slug>/SKILL.md`) or global
22
+ * (`~/.codeep/skills/<slug>/SKILL.md`) — either is publishable. The
23
+ * `--public` flag (translated into `opts.isPublic`) is the user's
24
+ * explicit consent gate; we don't gate further on bundle scope. If both
25
+ * exist with the same slug, the project copy wins (mirrors the rest of
26
+ * the bundle-loading flow).
27
+ */
28
+ export async function publishBundle(workspaceRoot, slug, opts = {}) {
29
+ const token = getSyncToken();
30
+ if (!token)
31
+ return { ok: false, error: 'Not linked to codeep.dev — run `codeep account` first.' };
32
+ const bundle = findSkillBundle(slug, workspaceRoot);
33
+ if (!bundle) {
34
+ return {
35
+ ok: false,
36
+ error: `Skill bundle "${slug}" not found in either \`.codeep/skills/${slug}/\` (project) or \`~/.codeep/skills/${slug}/\` (global). Run \`/skills create-bundle ${slug}\` to scaffold one, or check \`/skills bundles\` to see what's available.`,
37
+ };
38
+ }
39
+ // Build the SKILL.md text we'll publish. We re-serialise from the loaded
40
+ // bundle rather than reading the file again — that way frontmatter
41
+ // normalisation (e.g. defaulted name from dir) is reflected on the
42
+ // server and the next install round-trips correctly.
43
+ const skillMd = serialiseSkillMd(bundle);
44
+ try {
45
+ const res = await fetch(`${API_BASE}/api/skills`, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json', 'x-sync-token': token },
48
+ body: JSON.stringify({
49
+ slug: bundle.name,
50
+ name: bundle.name,
51
+ description: bundle.description,
52
+ body: skillMd,
53
+ version: bundle.version ?? null,
54
+ visibility: opts.isPublic ? 'public' : 'private',
55
+ }),
56
+ });
57
+ const data = await res.json().catch(() => ({}));
58
+ if (!res.ok || !data.ok) {
59
+ return { ok: false, error: data.error ?? `HTTP ${res.status}` };
60
+ }
61
+ return { ok: true, skill: data.skill };
62
+ }
63
+ catch (err) {
64
+ return { ok: false, error: err.message };
65
+ }
66
+ }
67
+ /**
68
+ * Install a skill from the marketplace into the project's
69
+ * `.codeep/skills/<slug>/SKILL.md`. `idOrPath` may be either
70
+ * `<owner>/<slug>` (preferred) or a numeric id.
71
+ */
72
+ export async function installBundle(workspaceRoot, idOrPath) {
73
+ const token = getSyncToken(); // optional — public skills are readable without auth
74
+ const headers = {};
75
+ if (token)
76
+ headers['x-sync-token'] = token;
77
+ try {
78
+ // ?install=1 so the server bumps install_count
79
+ const url = `${API_BASE}/api/skills/${encodeURIComponent(idOrPath)}?install=1`;
80
+ const res = await fetch(url, { headers });
81
+ const data = await res.json().catch(() => ({}));
82
+ if (!res.ok || !data.ok || !data.skill) {
83
+ return { ok: false, error: data.error ?? `HTTP ${res.status}` };
84
+ }
85
+ const skill = data.skill;
86
+ const dir = join(workspaceRoot, '.codeep', 'skills', skill.slug);
87
+ if (existsSync(dir)) {
88
+ return { ok: false, error: `A bundle already exists at .codeep/skills/${skill.slug}/ — remove it before re-installing.` };
89
+ }
90
+ mkdirSync(dir, { recursive: true });
91
+ writeFileSync(join(dir, 'SKILL.md'), skill.body);
92
+ return { ok: true, name: skill.slug };
93
+ }
94
+ catch (err) {
95
+ return { ok: false, error: err.message };
96
+ }
97
+ }
98
+ /** List public skills (or own when `mine=true`). Returns up to 100. */
99
+ export async function browseSkills(opts = {}) {
100
+ const token = getSyncToken();
101
+ const headers = {};
102
+ if (token)
103
+ headers['x-sync-token'] = token;
104
+ if (opts.mine && !token)
105
+ return { ok: false, error: '`mine=1` requires `codeep account` login.' };
106
+ try {
107
+ const u = new URL(`${API_BASE}/api/skills`);
108
+ if (opts.query)
109
+ u.searchParams.set('q', opts.query);
110
+ if (opts.mine)
111
+ u.searchParams.set('mine', '1');
112
+ const res = await fetch(u.toString(), { headers });
113
+ const data = await res.json().catch(() => ({}));
114
+ if (!res.ok || !data.ok)
115
+ return { ok: false, error: data.error ?? `HTTP ${res.status}` };
116
+ return { ok: true, skills: data.skills ?? [] };
117
+ }
118
+ catch (err) {
119
+ return { ok: false, error: err.message };
120
+ }
121
+ }
122
+ /** Unpublish a skill (owner only). */
123
+ export async function unpublishBundle(idOrPath) {
124
+ const token = getSyncToken();
125
+ if (!token)
126
+ return { ok: false, error: 'Not linked to codeep.dev — run `codeep account` first.' };
127
+ try {
128
+ const res = await fetch(`${API_BASE}/api/skills/${encodeURIComponent(idOrPath)}`, {
129
+ method: 'DELETE',
130
+ headers: { 'x-sync-token': token },
131
+ });
132
+ if (res.status === 404)
133
+ return { ok: false, error: 'Skill not found (or not yours).' };
134
+ if (!res.ok)
135
+ return { ok: false, error: `HTTP ${res.status}` };
136
+ return { ok: true };
137
+ }
138
+ catch (err) {
139
+ return { ok: false, error: err.message };
140
+ }
141
+ }
142
+ /**
143
+ * Re-serialise a loaded SkillBundle back into the SKILL.md text format.
144
+ * Used by publish so the round-trip is lossless (sort of — we drop
145
+ * unknown frontmatter keys for now to keep the published format stable).
146
+ */
147
+ function serialiseSkillMd(bundle) {
148
+ const meta = ['---'];
149
+ meta.push(`name: ${bundle.name}`);
150
+ meta.push(`description: ${bundle.description}`);
151
+ if (bundle.version)
152
+ meta.push(`version: ${bundle.version}`);
153
+ if (bundle.author)
154
+ meta.push(`author: ${bundle.author}`);
155
+ if (bundle.codeepMinVersion)
156
+ meta.push(`codeep-min-version: ${bundle.codeepMinVersion}`);
157
+ if (bundle.requiresMcp.length) {
158
+ meta.push(`codeep-requires-mcp:`);
159
+ for (const s of bundle.requiresMcp)
160
+ meta.push(` - ${s}`);
161
+ }
162
+ if (bundle.allowedTools.length) {
163
+ meta.push(`allowed-tools:`);
164
+ for (const s of bundle.allowedTools)
165
+ meta.push(` - ${s}`);
166
+ }
167
+ if (bundle.triggers.length) {
168
+ meta.push(`triggers:`);
169
+ for (const s of bundle.triggers)
170
+ meta.push(` - ${s}`);
171
+ }
172
+ meta.push('---', '');
173
+ return meta.join('\n') + bundle.body;
174
+ }
175
+ /** Read raw SKILL.md from disk — used when we want the unmodified bytes. */
176
+ export function readRawSkillMd(workspaceRoot, slug) {
177
+ const file = join(workspaceRoot, '.codeep', 'skills', slug, 'SKILL.md');
178
+ if (!existsSync(file))
179
+ return null;
180
+ try {
181
+ return readFileSync(file, 'utf-8');
182
+ }
183
+ catch {
184
+ return null;
185
+ }
186
+ }
187
+ /** Delete the local copy of an installed skill bundle (for /skills uninstall). */
188
+ export function uninstallLocalBundle(workspaceRoot, slug) {
189
+ const dir = join(workspaceRoot, '.codeep', 'skills', slug);
190
+ if (!existsSync(dir))
191
+ return false;
192
+ try {
193
+ rmSync(dir, { recursive: true, force: true });
194
+ return true;
195
+ }
196
+ catch {
197
+ return false;
198
+ }
199
+ }
200
+ // Silence unused-import warning when builds strip unused — homedir is here
201
+ // for future global-install support (planned 2.1).
202
+ void homedir;
@@ -20,6 +20,8 @@ interface TokenRecord {
20
20
  totalTokens: number;
21
21
  model: string;
22
22
  provider: string;
23
+ /** Authoritative per-call USD from the provider (OpenRouter), if available. */
24
+ actualCostUsd?: number;
23
25
  }
24
26
  /**
25
27
  * Get context window size for a model (falls back to 128k if unknown)
@@ -31,9 +33,13 @@ export declare function getPricingTable(): {
31
33
  outputPer1M: number;
32
34
  }[];
33
35
  /**
34
- * Record token usage from an API response
36
+ * Record token usage from an API response. The optional `actualCostUsd`
37
+ * argument lets aggregator providers (OpenRouter) pass through the
38
+ * authoritative per-call cost they returned in `usage.cost`, instead of
39
+ * forcing us to look it up in `MODEL_PRICING` (which we don't maintain
40
+ * for every OpenRouter-listed model — there are 100+).
35
41
  */
36
- export declare function recordTokenUsage(usage: TokenUsage, model: string, provider: string): void;
42
+ export declare function recordTokenUsage(usage: TokenUsage, model: string, provider: string, actualCostUsd?: number): void;
37
43
  /**
38
44
  * Extract token usage from OpenAI-format API response
39
45
  */
@@ -69,4 +75,10 @@ export declare function formatTokenCount(tokens: number): string;
69
75
  * Reset session tracking
70
76
  */
71
77
  export declare function resetTokenTracking(): void;
78
+ /**
79
+ * Format a session cost report as a Markdown block. Used by `/cost` in both
80
+ * the TUI and ACP command handlers. Returns a "no usage yet" message if the
81
+ * session hasn't made any API calls.
82
+ */
83
+ export declare function formatCostReport(): string;
72
84
  export {};
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Token and cost tracking for API usage
3
3
  */
4
- // Context window sizes per model (in tokens)
4
+ // Context window sizes per model (in tokens).
5
+ // Keep this table in lockstep with `providers.ts` — entries for models that
6
+ // aren't in the provider catalogue only show up if a user types an id by hand
7
+ // and produce phantom estimates against the wrong context size.
5
8
  const MODEL_CONTEXT_WINDOWS = {
6
9
  // Z.AI / ZhipuAI
7
10
  'glm-5.1': 131_072,
@@ -12,18 +15,10 @@ const MODEL_CONTEXT_WINDOWS = {
12
15
  'gpt-5.4': 1_050_000,
13
16
  'gpt-5.4-mini': 400_000,
14
17
  'gpt-5.4-nano': 400_000,
15
- 'gpt-4.1': 1_000_000,
16
- 'gpt-4.1-mini': 1_000_000,
17
- 'gpt-4.1-nano': 1_000_000,
18
- 'o3': 200_000,
19
- 'o4-mini': 200_000,
20
- 'gpt-4o': 128_000,
21
18
  // Anthropic
22
- 'claude-mythos-preview': 1_000_000,
23
19
  'claude-opus-4-7': 1_000_000,
24
20
  'claude-opus-4-6': 1_000_000,
25
21
  'claude-sonnet-4-6': 1_000_000,
26
- 'claude-sonnet-4-5-20250929': 200_000,
27
22
  'claude-haiku-4-5-20251001': 200_000,
28
23
  // DeepSeek
29
24
  'deepseek-v4-pro': 1_000_000,
@@ -31,17 +26,8 @@ const MODEL_CONTEXT_WINDOWS = {
31
26
  // Google
32
27
  'gemini-3.1-pro-preview': 1_048_576,
33
28
  'gemini-3-flash-preview': 1_000_000,
34
- 'gemini-3.1-flash-lite-preview': 1_000_000,
35
- 'gemini-2.5-pro': 1_000_000,
36
- 'gemini-2.5-flash': 1_000_000,
37
- 'gemini-2.5-flash-lite': 1_000_000,
38
29
  // MiniMax
39
30
  'MiniMax-M2.7': 204_800,
40
- 'MiniMax-M2.5': 196_608,
41
- 'MiniMax-M2.5-highspeed': 196_608,
42
- 'MiniMax-M2.1': 196_608,
43
- 'MiniMax-M2.1-highspeed': 196_608,
44
- 'MiniMax-M2': 196_608,
45
31
  };
46
32
  const DEFAULT_CONTEXT_WINDOW = 128_000;
47
33
  /**
@@ -50,7 +36,9 @@ const DEFAULT_CONTEXT_WINDOW = 128_000;
50
36
  export function getModelContextWindow(model) {
51
37
  return MODEL_CONTEXT_WINDOWS[model] ?? DEFAULT_CONTEXT_WINDOW;
52
38
  }
53
- // Pricing table — USD per 1M tokens
39
+ // Pricing table — USD per 1M tokens. Same rule as MODEL_CONTEXT_WINDOWS:
40
+ // only list model ids that exist in `providers.ts`, otherwise typing an id
41
+ // by hand can produce phantom cost estimates against stale rates.
54
42
  const MODEL_PRICING = {
55
43
  // Z.AI / ZhipuAI
56
44
  'glm-5.1': { inputPer1M: 1.00, outputPer1M: 3.20 },
@@ -61,18 +49,10 @@ const MODEL_PRICING = {
61
49
  'gpt-5.4': { inputPer1M: 2.50, outputPer1M: 15.00 },
62
50
  'gpt-5.4-mini': { inputPer1M: 0.75, outputPer1M: 4.50 },
63
51
  'gpt-5.4-nano': { inputPer1M: 0.20, outputPer1M: 1.25 },
64
- 'gpt-4.1': { inputPer1M: 2.00, outputPer1M: 8.00 },
65
- 'gpt-4.1-mini': { inputPer1M: 0.40, outputPer1M: 1.60 },
66
- 'gpt-4.1-nano': { inputPer1M: 0.10, outputPer1M: 0.40 },
67
- 'o3': { inputPer1M: 2.00, outputPer1M: 8.00 },
68
- 'o4-mini': { inputPer1M: 0.55, outputPer1M: 2.20 },
69
- 'gpt-4o': { inputPer1M: 2.50, outputPer1M: 10.00 },
70
52
  // Anthropic
71
- 'claude-mythos-preview': { inputPer1M: 6.00, outputPer1M: 30.00 },
72
53
  'claude-opus-4-7': { inputPer1M: 5.00, outputPer1M: 25.00 },
73
54
  'claude-opus-4-6': { inputPer1M: 5.00, outputPer1M: 25.00 },
74
55
  'claude-sonnet-4-6': { inputPer1M: 3.00, outputPer1M: 15.00 },
75
- 'claude-sonnet-4-5-20250929': { inputPer1M: 3.00, outputPer1M: 15.00 },
76
56
  'claude-haiku-4-5-20251001': { inputPer1M: 1.00, outputPer1M: 5.00 },
77
57
  // DeepSeek (cache-miss input pricing)
78
58
  'deepseek-v4-pro': { inputPer1M: 1.74, outputPer1M: 3.48 },
@@ -80,17 +60,8 @@ const MODEL_PRICING = {
80
60
  // Google
81
61
  'gemini-3.1-pro-preview': { inputPer1M: 2.00, outputPer1M: 12.00 },
82
62
  'gemini-3-flash-preview': { inputPer1M: 0.50, outputPer1M: 3.00 },
83
- 'gemini-3.1-flash-lite-preview': { inputPer1M: 0.25, outputPer1M: 1.50 },
84
- 'gemini-2.5-pro': { inputPer1M: 1.00, outputPer1M: 10.00 },
85
- 'gemini-2.5-flash': { inputPer1M: 0.30, outputPer1M: 2.50 },
86
- 'gemini-2.5-flash-lite': { inputPer1M: 0.10, outputPer1M: 0.40 },
87
63
  // MiniMax
88
64
  'MiniMax-M2.7': { inputPer1M: 0.30, outputPer1M: 1.20 },
89
- 'MiniMax-M2.5': { inputPer1M: 0.118, outputPer1M: 0.99 },
90
- 'MiniMax-M2.5-highspeed': { inputPer1M: 0.30, outputPer1M: 2.40 },
91
- 'MiniMax-M2.1': { inputPer1M: 0.27, outputPer1M: 0.95 },
92
- 'MiniMax-M2.1-highspeed': { inputPer1M: 0.27, outputPer1M: 0.95 },
93
- 'MiniMax-M2': { inputPer1M: 0.30, outputPer1M: 1.20 },
94
65
  };
95
66
  export function getPricingTable() {
96
67
  return Object.entries(MODEL_PRICING).map(([model, p]) => ({ model, ...p }));
@@ -98,9 +69,13 @@ export function getPricingTable() {
98
69
  // Session-level accumulator
99
70
  const records = [];
100
71
  /**
101
- * Record token usage from an API response
72
+ * Record token usage from an API response. The optional `actualCostUsd`
73
+ * argument lets aggregator providers (OpenRouter) pass through the
74
+ * authoritative per-call cost they returned in `usage.cost`, instead of
75
+ * forcing us to look it up in `MODEL_PRICING` (which we don't maintain
76
+ * for every OpenRouter-listed model — there are 100+).
102
77
  */
103
- export function recordTokenUsage(usage, model, provider) {
78
+ export function recordTokenUsage(usage, model, provider, actualCostUsd) {
104
79
  records.push({
105
80
  timestamp: Date.now(),
106
81
  promptTokens: usage.promptTokens,
@@ -108,6 +83,7 @@ export function recordTokenUsage(usage, model, provider) {
108
83
  totalTokens: usage.totalTokens,
109
84
  model,
110
85
  provider,
86
+ actualCostUsd,
111
87
  });
112
88
  }
113
89
  /**
@@ -144,11 +120,20 @@ export function getCostBreakdown() {
144
120
  for (const record of records) {
145
121
  const key = `${record.provider}/${record.model}`;
146
122
  const existing = grouped.get(key) ?? { provider: record.provider, model: record.model, promptTokens: 0, completionTokens: 0, estimatedCost: 0 };
147
- const pricing = MODEL_PRICING[record.model];
148
123
  existing.promptTokens += record.promptTokens;
149
124
  existing.completionTokens += record.completionTokens;
150
- if (pricing) {
151
- existing.estimatedCost += (record.promptTokens / 1_000_000) * pricing.inputPer1M + (record.completionTokens / 1_000_000) * pricing.outputPer1M;
125
+ // Cost source priority:
126
+ // 1. Provider-reported USD (OpenRouter, MaxiCloud, etc.) most accurate.
127
+ // 2. Our MODEL_PRICING table — for built-in providers we maintain rates for.
128
+ // 3. Zero — model isn't in our table; flag in formatCostReport.
129
+ if (typeof record.actualCostUsd === 'number' && Number.isFinite(record.actualCostUsd)) {
130
+ existing.estimatedCost += record.actualCostUsd;
131
+ }
132
+ else {
133
+ const pricing = MODEL_PRICING[record.model];
134
+ if (pricing) {
135
+ existing.estimatedCost += (record.promptTokens / 1_000_000) * pricing.inputPer1M + (record.completionTokens / 1_000_000) * pricing.outputPer1M;
136
+ }
152
137
  }
153
138
  grouped.set(key, existing);
154
139
  }
@@ -197,3 +182,36 @@ export function formatTokenCount(tokens) {
197
182
  export function resetTokenTracking() {
198
183
  records.length = 0;
199
184
  }
185
+ /**
186
+ * Format a session cost report as a Markdown block. Used by `/cost` in both
187
+ * the TUI and ACP command handlers. Returns a "no usage yet" message if the
188
+ * session hasn't made any API calls.
189
+ */
190
+ export function formatCostReport() {
191
+ const stats = getSessionStats();
192
+ if (stats.requestCount === 0) {
193
+ return '_No API requests in this session yet._';
194
+ }
195
+ const breakdown = getCostBreakdown();
196
+ const lines = [
197
+ '## Session Cost',
198
+ '',
199
+ `**Requests:** ${stats.requestCount} · **Input:** ${formatTokenCount(stats.totalPromptTokens)} · **Output:** ${formatTokenCount(stats.totalCompletionTokens)} · **Total:** ${formatTokenCount(stats.totalTokens)}`,
200
+ `**Estimated cost:** $${stats.estimatedCost.toFixed(4)}`,
201
+ '',
202
+ ];
203
+ if (breakdown.length > 1 || (breakdown.length === 1 && breakdown[0].estimatedCost > 0)) {
204
+ lines.push('| Provider / Model | Input | Output | Cost |');
205
+ lines.push('|---|---:|---:|---:|');
206
+ for (const b of breakdown) {
207
+ lines.push(`| \`${b.provider}\` / \`${b.model}\` | ${formatTokenCount(b.promptTokens)} | ${formatTokenCount(b.completionTokens)} | $${b.estimatedCost.toFixed(4)} |`);
208
+ }
209
+ }
210
+ // Models with no pricing entry don't contribute to cost — flag so users
211
+ // aren't surprised the total looks low.
212
+ const untracked = breakdown.filter(b => b.estimatedCost === 0 && (b.promptTokens + b.completionTokens) > 0);
213
+ if (untracked.length > 0) {
214
+ lines.push('', `_Note: ${untracked.length} model${untracked.length === 1 ? '' : 's'} (${untracked.map(u => `\`${u.model}\``).join(', ')}) have no pricing entry — token counts are tracked but not priced._`);
215
+ }
216
+ return lines.join('\n');
217
+ }
@@ -17,10 +17,26 @@ export declare function validatePath(path: string, projectRoot: string): {
17
17
  absolutePath: string;
18
18
  error?: string;
19
19
  };
20
+ /**
21
+ * Optional filesystem delegation. When an ACP client advertises `fs`
22
+ * capability (Zed always does, VS Code may), the server should route
23
+ * read/write through the client instead of touching disk directly — that
24
+ * way the client's unsaved buffers, undo history, and virtual filesystems
25
+ * stay authoritative. Callbacks must already use absolute paths.
26
+ */
27
+ export interface FsCallbacks {
28
+ readTextFile?: (absolutePath: string) => Promise<string>;
29
+ writeTextFile?: (absolutePath: string, content: string) => Promise<void>;
30
+ }
20
31
  /**
21
32
  * Execute a tool call and return the result.
33
+ *
34
+ * `fs` is optional — if provided and the relevant method is defined, file
35
+ * read/write is delegated to the client. Otherwise we fall back to direct
36
+ * disk I/O. A delegated call that throws also falls back to disk so a
37
+ * single client hiccup doesn't kill the agent loop.
22
38
  */
23
- export declare function executeTool(toolCall: ToolCall, projectRoot: string): Promise<ToolResult>;
39
+ export declare function executeTool(toolCall: ToolCall, projectRoot: string, fs?: FsCallbacks, mcpSessionId?: string): Promise<ToolResult>;
24
40
  /**
25
41
  * Create action log from tool result
26
42
  */