codeep 1.3.41 → 2.0.0

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 (59) 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 +46 -1
  10. package/dist/config/providers.js +76 -1
  11. package/dist/renderer/App.d.ts +12 -0
  12. package/dist/renderer/App.js +96 -4
  13. package/dist/renderer/agentExecution.js +5 -0
  14. package/dist/renderer/commands.js +348 -2
  15. package/dist/renderer/components/Login.d.ts +1 -0
  16. package/dist/renderer/components/Login.js +24 -9
  17. package/dist/renderer/handlers.d.ts +11 -1
  18. package/dist/renderer/handlers.js +30 -0
  19. package/dist/renderer/main.js +73 -0
  20. package/dist/utils/agent.d.ts +17 -0
  21. package/dist/utils/agent.js +91 -7
  22. package/dist/utils/agentChat.d.ts +10 -2
  23. package/dist/utils/agentChat.js +48 -9
  24. package/dist/utils/agentStream.js +6 -2
  25. package/dist/utils/checkpoints.d.ts +93 -0
  26. package/dist/utils/checkpoints.js +205 -0
  27. package/dist/utils/context.d.ts +24 -0
  28. package/dist/utils/context.js +57 -0
  29. package/dist/utils/customCommands.d.ts +62 -0
  30. package/dist/utils/customCommands.js +201 -0
  31. package/dist/utils/hooks.d.ts +97 -0
  32. package/dist/utils/hooks.js +223 -0
  33. package/dist/utils/mcpClient.d.ts +229 -0
  34. package/dist/utils/mcpClient.js +497 -0
  35. package/dist/utils/mcpConfig.d.ts +55 -0
  36. package/dist/utils/mcpConfig.js +177 -0
  37. package/dist/utils/mcpMarketplace.d.ts +49 -0
  38. package/dist/utils/mcpMarketplace.js +175 -0
  39. package/dist/utils/mcpRegistry.d.ts +129 -0
  40. package/dist/utils/mcpRegistry.js +427 -0
  41. package/dist/utils/mcpSamplingBridge.d.ts +32 -0
  42. package/dist/utils/mcpSamplingBridge.js +88 -0
  43. package/dist/utils/mcpStreamableHttp.d.ts +65 -0
  44. package/dist/utils/mcpStreamableHttp.js +207 -0
  45. package/dist/utils/openrouterPrefs.d.ts +36 -0
  46. package/dist/utils/openrouterPrefs.js +83 -0
  47. package/dist/utils/skillBundles.d.ts +84 -0
  48. package/dist/utils/skillBundles.js +257 -0
  49. package/dist/utils/skillBundlesCloud.d.ts +66 -0
  50. package/dist/utils/skillBundlesCloud.js +196 -0
  51. package/dist/utils/tokenTracker.d.ts +14 -2
  52. package/dist/utils/tokenTracker.js +59 -45
  53. package/dist/utils/toolExecution.d.ts +17 -1
  54. package/dist/utils/toolExecution.js +184 -6
  55. package/dist/utils/tools.d.ts +22 -6
  56. package/dist/utils/tools.js +83 -8
  57. package/package.json +3 -2
  58. package/bin/codeep-macos-arm64 +0 -0
  59. package/bin/codeep-macos-x64 +0 -0
@@ -0,0 +1,66 @@
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 project-scoped skill bundle to codeep.dev.
28
+ * The bundle MUST exist under `<workspaceRoot>/.codeep/skills/<slug>/SKILL.md`
29
+ * — global skills aren't publishable from this helper (intentional: users
30
+ * publish work from a project they're owning, not from their global hoard).
31
+ */
32
+ export declare function publishBundle(workspaceRoot: string, slug: string, opts?: {
33
+ isPublic?: boolean;
34
+ }): Promise<{
35
+ ok: boolean;
36
+ skill?: RemoteSkill;
37
+ error?: string;
38
+ }>;
39
+ /**
40
+ * Install a skill from the marketplace into the project's
41
+ * `.codeep/skills/<slug>/SKILL.md`. `idOrPath` may be either
42
+ * `<owner>/<slug>` (preferred) or a numeric id.
43
+ */
44
+ export declare function installBundle(workspaceRoot: string, idOrPath: string): Promise<{
45
+ ok: boolean;
46
+ name?: string;
47
+ error?: string;
48
+ }>;
49
+ /** List public skills (or own when `mine=true`). Returns up to 100. */
50
+ export declare function browseSkills(opts?: {
51
+ query?: string;
52
+ mine?: boolean;
53
+ }): Promise<{
54
+ ok: boolean;
55
+ skills?: RemoteSkill[];
56
+ error?: string;
57
+ }>;
58
+ /** Unpublish a skill (owner only). */
59
+ export declare function unpublishBundle(idOrPath: string): Promise<{
60
+ ok: boolean;
61
+ error?: string;
62
+ }>;
63
+ /** Read raw SKILL.md from disk — used when we want the unmodified bytes. */
64
+ export declare function readRawSkillMd(workspaceRoot: string, slug: string): string | null;
65
+ /** Delete the local copy of an installed skill bundle (for /skills uninstall). */
66
+ export declare function uninstallLocalBundle(workspaceRoot: string, slug: string): boolean;
@@ -0,0 +1,196 @@
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 project-scoped skill bundle to codeep.dev.
21
+ * The bundle MUST exist under `<workspaceRoot>/.codeep/skills/<slug>/SKILL.md`
22
+ * — global skills aren't publishable from this helper (intentional: users
23
+ * publish work from a project they're owning, not from their global hoard).
24
+ */
25
+ export async function publishBundle(workspaceRoot, slug, opts = {}) {
26
+ const token = getSyncToken();
27
+ if (!token)
28
+ return { ok: false, error: 'Not linked to codeep.dev — run `codeep account` first.' };
29
+ const bundle = findSkillBundle(slug, workspaceRoot);
30
+ if (!bundle || bundle.scope !== 'project') {
31
+ return { ok: false, error: `Skill bundle "${slug}" not found in this project. Run \`/skills create-bundle ${slug}\` first.` };
32
+ }
33
+ // Build the SKILL.md text we'll publish. We re-serialise from the loaded
34
+ // bundle rather than reading the file again — that way frontmatter
35
+ // normalisation (e.g. defaulted name from dir) is reflected on the
36
+ // server and the next install round-trips correctly.
37
+ const skillMd = serialiseSkillMd(bundle);
38
+ try {
39
+ const res = await fetch(`${API_BASE}/api/skills`, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json', 'x-sync-token': token },
42
+ body: JSON.stringify({
43
+ slug: bundle.name,
44
+ name: bundle.name,
45
+ description: bundle.description,
46
+ body: skillMd,
47
+ version: bundle.version ?? null,
48
+ visibility: opts.isPublic ? 'public' : 'private',
49
+ }),
50
+ });
51
+ const data = await res.json().catch(() => ({}));
52
+ if (!res.ok || !data.ok) {
53
+ return { ok: false, error: data.error ?? `HTTP ${res.status}` };
54
+ }
55
+ return { ok: true, skill: data.skill };
56
+ }
57
+ catch (err) {
58
+ return { ok: false, error: err.message };
59
+ }
60
+ }
61
+ /**
62
+ * Install a skill from the marketplace into the project's
63
+ * `.codeep/skills/<slug>/SKILL.md`. `idOrPath` may be either
64
+ * `<owner>/<slug>` (preferred) or a numeric id.
65
+ */
66
+ export async function installBundle(workspaceRoot, idOrPath) {
67
+ const token = getSyncToken(); // optional — public skills are readable without auth
68
+ const headers = {};
69
+ if (token)
70
+ headers['x-sync-token'] = token;
71
+ try {
72
+ // ?install=1 so the server bumps install_count
73
+ const url = `${API_BASE}/api/skills/${encodeURIComponent(idOrPath)}?install=1`;
74
+ const res = await fetch(url, { headers });
75
+ const data = await res.json().catch(() => ({}));
76
+ if (!res.ok || !data.ok || !data.skill) {
77
+ return { ok: false, error: data.error ?? `HTTP ${res.status}` };
78
+ }
79
+ const skill = data.skill;
80
+ const dir = join(workspaceRoot, '.codeep', 'skills', skill.slug);
81
+ if (existsSync(dir)) {
82
+ return { ok: false, error: `A bundle already exists at .codeep/skills/${skill.slug}/ — remove it before re-installing.` };
83
+ }
84
+ mkdirSync(dir, { recursive: true });
85
+ writeFileSync(join(dir, 'SKILL.md'), skill.body);
86
+ return { ok: true, name: skill.slug };
87
+ }
88
+ catch (err) {
89
+ return { ok: false, error: err.message };
90
+ }
91
+ }
92
+ /** List public skills (or own when `mine=true`). Returns up to 100. */
93
+ export async function browseSkills(opts = {}) {
94
+ const token = getSyncToken();
95
+ const headers = {};
96
+ if (token)
97
+ headers['x-sync-token'] = token;
98
+ if (opts.mine && !token)
99
+ return { ok: false, error: '`mine=1` requires `codeep account` login.' };
100
+ try {
101
+ const u = new URL(`${API_BASE}/api/skills`);
102
+ if (opts.query)
103
+ u.searchParams.set('q', opts.query);
104
+ if (opts.mine)
105
+ u.searchParams.set('mine', '1');
106
+ const res = await fetch(u.toString(), { headers });
107
+ const data = await res.json().catch(() => ({}));
108
+ if (!res.ok || !data.ok)
109
+ return { ok: false, error: data.error ?? `HTTP ${res.status}` };
110
+ return { ok: true, skills: data.skills ?? [] };
111
+ }
112
+ catch (err) {
113
+ return { ok: false, error: err.message };
114
+ }
115
+ }
116
+ /** Unpublish a skill (owner only). */
117
+ export async function unpublishBundle(idOrPath) {
118
+ const token = getSyncToken();
119
+ if (!token)
120
+ return { ok: false, error: 'Not linked to codeep.dev — run `codeep account` first.' };
121
+ try {
122
+ const res = await fetch(`${API_BASE}/api/skills/${encodeURIComponent(idOrPath)}`, {
123
+ method: 'DELETE',
124
+ headers: { 'x-sync-token': token },
125
+ });
126
+ if (res.status === 404)
127
+ return { ok: false, error: 'Skill not found (or not yours).' };
128
+ if (!res.ok)
129
+ return { ok: false, error: `HTTP ${res.status}` };
130
+ return { ok: true };
131
+ }
132
+ catch (err) {
133
+ return { ok: false, error: err.message };
134
+ }
135
+ }
136
+ /**
137
+ * Re-serialise a loaded SkillBundle back into the SKILL.md text format.
138
+ * Used by publish so the round-trip is lossless (sort of — we drop
139
+ * unknown frontmatter keys for now to keep the published format stable).
140
+ */
141
+ function serialiseSkillMd(bundle) {
142
+ const meta = ['---'];
143
+ meta.push(`name: ${bundle.name}`);
144
+ meta.push(`description: ${bundle.description}`);
145
+ if (bundle.version)
146
+ meta.push(`version: ${bundle.version}`);
147
+ if (bundle.author)
148
+ meta.push(`author: ${bundle.author}`);
149
+ if (bundle.codeepMinVersion)
150
+ meta.push(`codeep-min-version: ${bundle.codeepMinVersion}`);
151
+ if (bundle.requiresMcp.length) {
152
+ meta.push(`codeep-requires-mcp:`);
153
+ for (const s of bundle.requiresMcp)
154
+ meta.push(` - ${s}`);
155
+ }
156
+ if (bundle.allowedTools.length) {
157
+ meta.push(`allowed-tools:`);
158
+ for (const s of bundle.allowedTools)
159
+ meta.push(` - ${s}`);
160
+ }
161
+ if (bundle.triggers.length) {
162
+ meta.push(`triggers:`);
163
+ for (const s of bundle.triggers)
164
+ meta.push(` - ${s}`);
165
+ }
166
+ meta.push('---', '');
167
+ return meta.join('\n') + bundle.body;
168
+ }
169
+ /** Read raw SKILL.md from disk — used when we want the unmodified bytes. */
170
+ export function readRawSkillMd(workspaceRoot, slug) {
171
+ const file = join(workspaceRoot, '.codeep', 'skills', slug, 'SKILL.md');
172
+ if (!existsSync(file))
173
+ return null;
174
+ try {
175
+ return readFileSync(file, 'utf-8');
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ }
181
+ /** Delete the local copy of an installed skill bundle (for /skills uninstall). */
182
+ export function uninstallLocalBundle(workspaceRoot, slug) {
183
+ const dir = join(workspaceRoot, '.codeep', 'skills', slug);
184
+ if (!existsSync(dir))
185
+ return false;
186
+ try {
187
+ rmSync(dir, { recursive: true, force: true });
188
+ return true;
189
+ }
190
+ catch {
191
+ return false;
192
+ }
193
+ }
194
+ // Silence unused-import warning when builds strip unused — homedir is here
195
+ // for future global-install support (planned 2.1).
196
+ 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,31 +1,24 @@
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,
8
11
  'glm-5': 80_000,
9
12
  'glm-5-turbo': 202_752,
10
- 'glm-4.5-air': 131_072,
11
- 'glm-4.7-flash': 202_752,
12
13
  // OpenAI
13
14
  'gpt-5.5': 1_200_000,
14
15
  'gpt-5.4': 1_050_000,
15
16
  'gpt-5.4-mini': 400_000,
16
17
  'gpt-5.4-nano': 400_000,
17
- 'gpt-4.1': 1_000_000,
18
- 'gpt-4.1-mini': 1_000_000,
19
- 'gpt-4.1-nano': 1_000_000,
20
- 'o3': 200_000,
21
- 'o4-mini': 200_000,
22
- 'gpt-4o': 128_000,
23
18
  // Anthropic
24
- 'claude-mythos-preview': 1_000_000,
25
19
  'claude-opus-4-7': 1_000_000,
26
20
  'claude-opus-4-6': 1_000_000,
27
21
  'claude-sonnet-4-6': 1_000_000,
28
- 'claude-sonnet-4-5-20250929': 200_000,
29
22
  'claude-haiku-4-5-20251001': 200_000,
30
23
  // DeepSeek
31
24
  'deepseek-v4-pro': 1_000_000,
@@ -33,17 +26,8 @@ const MODEL_CONTEXT_WINDOWS = {
33
26
  // Google
34
27
  'gemini-3.1-pro-preview': 1_048_576,
35
28
  'gemini-3-flash-preview': 1_000_000,
36
- 'gemini-3.1-flash-lite-preview': 1_000_000,
37
- 'gemini-2.5-pro': 1_000_000,
38
- 'gemini-2.5-flash': 1_000_000,
39
- 'gemini-2.5-flash-lite': 1_000_000,
40
29
  // MiniMax
41
30
  'MiniMax-M2.7': 204_800,
42
- 'MiniMax-M2.5': 196_608,
43
- 'MiniMax-M2.5-highspeed': 196_608,
44
- 'MiniMax-M2.1': 196_608,
45
- 'MiniMax-M2.1-highspeed': 196_608,
46
- 'MiniMax-M2': 196_608,
47
31
  };
48
32
  const DEFAULT_CONTEXT_WINDOW = 128_000;
49
33
  /**
@@ -52,31 +36,23 @@ const DEFAULT_CONTEXT_WINDOW = 128_000;
52
36
  export function getModelContextWindow(model) {
53
37
  return MODEL_CONTEXT_WINDOWS[model] ?? DEFAULT_CONTEXT_WINDOW;
54
38
  }
55
- // 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.
56
42
  const MODEL_PRICING = {
57
43
  // Z.AI / ZhipuAI
58
44
  'glm-5.1': { inputPer1M: 1.00, outputPer1M: 3.20 },
59
45
  'glm-5': { inputPer1M: 0.72, outputPer1M: 2.30 },
60
46
  'glm-5-turbo': { inputPer1M: 1.20, outputPer1M: 4.00 },
61
- 'glm-4.5-air': { inputPer1M: 0.20, outputPer1M: 1.10 },
62
- 'glm-4.7-flash': { inputPer1M: 0.06, outputPer1M: 0.40 },
63
47
  // OpenAI
64
48
  'gpt-5.5': { inputPer1M: 5.00, outputPer1M: 30.00 },
65
49
  'gpt-5.4': { inputPer1M: 2.50, outputPer1M: 15.00 },
66
50
  'gpt-5.4-mini': { inputPer1M: 0.75, outputPer1M: 4.50 },
67
51
  'gpt-5.4-nano': { inputPer1M: 0.20, outputPer1M: 1.25 },
68
- 'gpt-4.1': { inputPer1M: 2.00, outputPer1M: 8.00 },
69
- 'gpt-4.1-mini': { inputPer1M: 0.40, outputPer1M: 1.60 },
70
- 'gpt-4.1-nano': { inputPer1M: 0.10, outputPer1M: 0.40 },
71
- 'o3': { inputPer1M: 2.00, outputPer1M: 8.00 },
72
- 'o4-mini': { inputPer1M: 0.55, outputPer1M: 2.20 },
73
- 'gpt-4o': { inputPer1M: 2.50, outputPer1M: 10.00 },
74
52
  // Anthropic
75
- 'claude-mythos-preview': { inputPer1M: 6.00, outputPer1M: 30.00 },
76
53
  'claude-opus-4-7': { inputPer1M: 5.00, outputPer1M: 25.00 },
77
54
  'claude-opus-4-6': { inputPer1M: 5.00, outputPer1M: 25.00 },
78
55
  'claude-sonnet-4-6': { inputPer1M: 3.00, outputPer1M: 15.00 },
79
- 'claude-sonnet-4-5-20250929': { inputPer1M: 3.00, outputPer1M: 15.00 },
80
56
  'claude-haiku-4-5-20251001': { inputPer1M: 1.00, outputPer1M: 5.00 },
81
57
  // DeepSeek (cache-miss input pricing)
82
58
  'deepseek-v4-pro': { inputPer1M: 1.74, outputPer1M: 3.48 },
@@ -84,17 +60,8 @@ const MODEL_PRICING = {
84
60
  // Google
85
61
  'gemini-3.1-pro-preview': { inputPer1M: 2.00, outputPer1M: 12.00 },
86
62
  'gemini-3-flash-preview': { inputPer1M: 0.50, outputPer1M: 3.00 },
87
- 'gemini-3.1-flash-lite-preview': { inputPer1M: 0.25, outputPer1M: 1.50 },
88
- 'gemini-2.5-pro': { inputPer1M: 1.00, outputPer1M: 10.00 },
89
- 'gemini-2.5-flash': { inputPer1M: 0.30, outputPer1M: 2.50 },
90
- 'gemini-2.5-flash-lite': { inputPer1M: 0.10, outputPer1M: 0.40 },
91
63
  // MiniMax
92
64
  'MiniMax-M2.7': { inputPer1M: 0.30, outputPer1M: 1.20 },
93
- 'MiniMax-M2.5': { inputPer1M: 0.118, outputPer1M: 0.99 },
94
- 'MiniMax-M2.5-highspeed': { inputPer1M: 0.30, outputPer1M: 2.40 },
95
- 'MiniMax-M2.1': { inputPer1M: 0.27, outputPer1M: 0.95 },
96
- 'MiniMax-M2.1-highspeed': { inputPer1M: 0.27, outputPer1M: 0.95 },
97
- 'MiniMax-M2': { inputPer1M: 0.30, outputPer1M: 1.20 },
98
65
  };
99
66
  export function getPricingTable() {
100
67
  return Object.entries(MODEL_PRICING).map(([model, p]) => ({ model, ...p }));
@@ -102,9 +69,13 @@ export function getPricingTable() {
102
69
  // Session-level accumulator
103
70
  const records = [];
104
71
  /**
105
- * 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+).
106
77
  */
107
- export function recordTokenUsage(usage, model, provider) {
78
+ export function recordTokenUsage(usage, model, provider, actualCostUsd) {
108
79
  records.push({
109
80
  timestamp: Date.now(),
110
81
  promptTokens: usage.promptTokens,
@@ -112,6 +83,7 @@ export function recordTokenUsage(usage, model, provider) {
112
83
  totalTokens: usage.totalTokens,
113
84
  model,
114
85
  provider,
86
+ actualCostUsd,
115
87
  });
116
88
  }
117
89
  /**
@@ -148,11 +120,20 @@ export function getCostBreakdown() {
148
120
  for (const record of records) {
149
121
  const key = `${record.provider}/${record.model}`;
150
122
  const existing = grouped.get(key) ?? { provider: record.provider, model: record.model, promptTokens: 0, completionTokens: 0, estimatedCost: 0 };
151
- const pricing = MODEL_PRICING[record.model];
152
123
  existing.promptTokens += record.promptTokens;
153
124
  existing.completionTokens += record.completionTokens;
154
- if (pricing) {
155
- 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
+ }
156
137
  }
157
138
  grouped.set(key, existing);
158
139
  }
@@ -201,3 +182,36 @@ export function formatTokenCount(tokens) {
201
182
  export function resetTokenTracking() {
202
183
  records.length = 0;
203
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
  */