@glwhappen/web-code 1.32.9 → 1.32.10

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 (167) hide show
  1. package/README.de.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/README.ru.md +1 -1
  5. package/README.tr.md +1 -1
  6. package/README.zh-CN.md +1 -1
  7. package/dist/api-docs.html +6 -7
  8. package/dist/assets/{index-D_7CSvqO.js → index-BLLsK3sG.js} +276 -261
  9. package/dist/assets/index-Dl5QP21C.css +32 -0
  10. package/dist/index.html +2 -2
  11. package/dist/modelConstants.js +841 -0
  12. package/dist-server/server/claude-sdk.js +57 -34
  13. package/dist-server/server/claude-sdk.js.map +1 -1
  14. package/dist-server/server/cursor-cli.js +6 -3
  15. package/dist-server/server/cursor-cli.js.map +1 -1
  16. package/dist-server/server/gemini-cli.js +3 -1
  17. package/dist-server/server/gemini-cli.js.map +1 -1
  18. package/dist-server/server/gemini-response-handler.js +34 -0
  19. package/dist-server/server/gemini-response-handler.js.map +1 -1
  20. package/dist-server/server/index.js +131 -19
  21. package/dist-server/server/index.js.map +1 -1
  22. package/dist-server/server/modules/database/index.js +1 -0
  23. package/dist-server/server/modules/database/index.js.map +1 -1
  24. package/dist-server/server/modules/projects/services/project-management.service.js +1 -0
  25. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
  26. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +4 -0
  27. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  28. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +143 -0
  29. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
  30. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  31. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  32. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +84 -0
  33. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
  34. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -39
  35. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
  36. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  37. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  38. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +754 -0
  39. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
  40. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +2 -15
  41. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  42. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
  43. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +27 -0
  45. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -0
  46. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +3 -9
  47. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  48. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
  49. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +92 -0
  51. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
  52. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
  53. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
  54. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +267 -0
  55. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
  56. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +115 -0
  57. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
  58. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +410 -0
  59. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
  60. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
  61. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
  62. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +19 -0
  63. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
  64. package/dist-server/server/modules/providers/provider.registry.js +2 -0
  65. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  66. package/dist-server/server/modules/providers/provider.routes.js +42 -1
  67. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  68. package/dist-server/server/modules/providers/services/mcp.service.js +1 -9
  69. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  70. package/dist-server/server/modules/providers/services/provider-models.service.js +199 -0
  71. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
  72. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
  73. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  74. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +7 -0
  75. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  76. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  77. package/dist-server/server/modules/providers/tests/mcp.test.js +73 -6
  78. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  79. package/dist-server/server/modules/providers/tests/opencode-models.test.js +66 -0
  80. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
  81. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +264 -0
  82. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
  83. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +270 -0
  84. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
  85. package/dist-server/server/modules/providers/tests/skills.test.js +33 -0
  86. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  87. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +18 -1
  88. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  89. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -1
  90. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  91. package/dist-server/server/openai-codex.js +32 -4
  92. package/dist-server/server/openai-codex.js.map +1 -1
  93. package/dist-server/server/opencode-cli.js +287 -0
  94. package/dist-server/server/opencode-cli.js.map +1 -0
  95. package/dist-server/server/opencode-cli.test.js +84 -0
  96. package/dist-server/server/opencode-cli.test.js.map +1 -0
  97. package/dist-server/server/routes/agent.js +21 -8
  98. package/dist-server/server/routes/agent.js.map +1 -1
  99. package/dist-server/server/routes/commands.js +202 -209
  100. package/dist-server/server/routes/commands.js.map +1 -1
  101. package/dist-server/server/routes/cursor.js +2 -2
  102. package/dist-server/server/routes/cursor.js.map +1 -1
  103. package/dist-server/server/routes/settings.js +0 -10
  104. package/dist-server/server/routes/settings.js.map +1 -1
  105. package/dist-server/server/routes/tests/commands.test.js +76 -0
  106. package/dist-server/server/routes/tests/commands.test.js.map +1 -0
  107. package/dist-server/server/shared/utils.js +286 -0
  108. package/dist-server/server/shared/utils.js.map +1 -1
  109. package/package.json +3 -1
  110. package/public/api-docs.html +878 -0
  111. package/public/modelConstants.js +841 -0
  112. package/server/claude-sdk.js +64 -35
  113. package/server/cursor-cli.js +6 -3
  114. package/server/gemini-cli.js +7 -1
  115. package/server/gemini-response-handler.js +38 -0
  116. package/server/index.js +150 -19
  117. package/server/modules/database/index.ts +1 -0
  118. package/server/modules/projects/services/project-management.service.ts +2 -0
  119. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +7 -1
  120. package/server/modules/providers/README.md +11 -3
  121. package/server/modules/providers/list/claude/claude-models.provider.ts +193 -0
  122. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  123. package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
  124. package/server/modules/providers/list/codex/codex-skills.provider.ts +10 -50
  125. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  126. package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
  127. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +7 -20
  128. package/server/modules/providers/list/cursor/cursor.provider.ts +3 -0
  129. package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
  130. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +3 -10
  131. package/server/modules/providers/list/gemini/gemini.provider.ts +3 -0
  132. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
  133. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
  134. package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
  135. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +158 -0
  136. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
  137. package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
  138. package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
  139. package/server/modules/providers/provider.registry.ts +2 -0
  140. package/server/modules/providers/provider.routes.ts +62 -2
  141. package/server/modules/providers/services/mcp.service.ts +1 -12
  142. package/server/modules/providers/services/provider-models.service.ts +325 -0
  143. package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
  144. package/server/modules/providers/services/sessions-watcher.service.ts +8 -0
  145. package/server/modules/providers/shared/base/abstract.provider.ts +2 -0
  146. package/server/modules/providers/tests/mcp.test.ts +93 -6
  147. package/server/modules/providers/tests/opencode-models.test.ts +73 -0
  148. package/server/modules/providers/tests/opencode-sessions.test.ts +336 -0
  149. package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
  150. package/server/modules/providers/tests/skills.test.ts +66 -0
  151. package/server/modules/websocket/services/chat-websocket.service.ts +21 -1
  152. package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
  153. package/server/openai-codex.js +40 -4
  154. package/server/opencode-cli.js +336 -0
  155. package/server/opencode-cli.test.js +95 -0
  156. package/server/routes/agent.js +22 -8
  157. package/server/routes/commands.js +254 -233
  158. package/server/routes/cursor.js +2 -2
  159. package/server/routes/settings.js +1 -10
  160. package/server/routes/tests/commands.test.js +82 -0
  161. package/server/shared/interfaces.ts +45 -0
  162. package/server/shared/types.ts +88 -1
  163. package/server/shared/utils.ts +384 -0
  164. package/dist/assets/index-DdxLnCfK.css +0 -32
  165. package/dist-server/shared/modelConstants.js +0 -99
  166. package/dist-server/shared/modelConstants.js.map +0 -1
  167. package/shared/modelConstants.js +0 -107
@@ -17,7 +17,8 @@ import crypto from 'crypto';
17
17
  import { promises as fs } from 'fs';
18
18
  import path from 'path';
19
19
  import os from 'os';
20
- import { CLAUDE_MODELS } from '../shared/modelConstants.js';
20
+ import { CLAUDE_FALLBACK_MODELS } from './modules/providers/list/claude/claude-models.provider.js';
21
+ import { providerModelsService } from './modules/providers/services/provider-models.service.js';
21
22
  import { resolveClaudeCodeExecutablePath } from './shared/claude-cli-path.js';
22
23
  import {
23
24
  createNotificationEvent,
@@ -210,7 +211,7 @@ function mapCliOptionsToSDK(options = {}) {
210
211
 
211
212
  // Map model (default to sonnet)
212
213
  // Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
213
- sdkOptions.model = options.model || CLAUDE_MODELS.DEFAULT;
214
+ sdkOptions.model = options.model || CLAUDE_FALLBACK_MODELS.DEFAULT;
214
215
  // Model logged at query start below
215
216
 
216
217
  // Map system prompt configuration
@@ -306,43 +307,68 @@ function transformMessage(sdkMessage) {
306
307
  return sdkMessage;
307
308
  }
308
309
 
310
+ function readNumber(value) {
311
+ const parsed = Number(value);
312
+ return Number.isFinite(parsed) ? parsed : 0;
313
+ }
314
+
309
315
  /**
310
- * Extracts token usage from SDK result messages
311
- * @param {Object} resultMessage - SDK result message
316
+ * Extracts token usage from SDK messages.
317
+ * Prefers per-step `message.usage` (Claude message payload), then falls back
318
+ * to result-level usage/modelUsage for compatibility across SDK versions.
319
+ * @param {Object} sdkMessage - SDK stream message
312
320
  * @returns {Object|null} Token budget object or null
313
321
  */
314
- function extractTokenBudget(resultMessage) {
315
- if (resultMessage.type !== 'result' || !resultMessage.modelUsage) {
322
+ function extractTokenBudget(sdkMessage) {
323
+ if (!sdkMessage || typeof sdkMessage !== 'object') {
316
324
  return null;
317
325
  }
318
326
 
319
- // Get the first model's usage data
320
- const modelKey = Object.keys(resultMessage.modelUsage)[0];
321
- const modelData = resultMessage.modelUsage[modelKey];
327
+ const messageUsage = sdkMessage.message?.usage || sdkMessage.usage;
328
+ if (messageUsage && typeof messageUsage === 'object') {
329
+ const inputTokens = readNumber(messageUsage.input_tokens ?? messageUsage.inputTokens);
330
+ const outputTokens = readNumber(messageUsage.output_tokens ?? messageUsage.outputTokens);
331
+ const totalUsed = inputTokens + outputTokens;
332
+ const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
322
333
 
323
- if (!modelData) {
324
- return null;
334
+ return {
335
+ used: totalUsed,
336
+ total: contextWindow,
337
+ inputTokens,
338
+ outputTokens,
339
+ breakdown: {
340
+ input: inputTokens,
341
+ output: outputTokens,
342
+ },
343
+ };
325
344
  }
326
345
 
327
- // Use cumulative tokens if available (tracks total for the session)
328
- // Otherwise fall back to per-request tokens
329
- const inputTokens = modelData.cumulativeInputTokens || modelData.inputTokens || 0;
330
- const outputTokens = modelData.cumulativeOutputTokens || modelData.outputTokens || 0;
331
- const cacheReadTokens = modelData.cumulativeCacheReadInputTokens || modelData.cacheReadInputTokens || 0;
332
- const cacheCreationTokens = modelData.cumulativeCacheCreationInputTokens || modelData.cacheCreationInputTokens || 0;
346
+ if (!sdkMessage.modelUsage || typeof sdkMessage.modelUsage !== 'object') {
347
+ return null;
348
+ }
333
349
 
334
- // Total used = input + output + cache tokens
335
- const totalUsed = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
350
+ // Fallback for older SDK messages with only modelUsage
351
+ const modelKey = Object.keys(sdkMessage.modelUsage)[0];
352
+ const modelData = sdkMessage.modelUsage[modelKey];
336
353
 
337
- // Use configured context window budget from environment (default 160000)
338
- // This is the user's budget limit, not the model's context window
339
- const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000;
354
+ if (!modelData || typeof modelData !== 'object') {
355
+ return null;
356
+ }
340
357
 
341
- // Token calc logged via token-budget WS event
358
+ const inputTokens = readNumber(modelData.cumulativeInputTokens ?? modelData.inputTokens);
359
+ const outputTokens = readNumber(modelData.cumulativeOutputTokens ?? modelData.outputTokens);
360
+ const totalUsed = inputTokens + outputTokens;
361
+ const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
342
362
 
343
363
  return {
344
364
  used: totalUsed,
345
- total: contextWindow
365
+ total: contextWindow,
366
+ inputTokens,
367
+ outputTokens,
368
+ breakdown: {
369
+ input: inputTokens,
370
+ output: outputTokens,
371
+ },
346
372
  };
347
373
  }
348
374
 
@@ -514,8 +540,17 @@ async function queryClaudeSDK(command, options = {}, ws) {
514
540
  };
515
541
 
516
542
  try {
543
+ const resolvedModel = await providerModelsService.resolveResumeModel(
544
+ 'claude',
545
+ sessionId,
546
+ options.model,
547
+ );
548
+
517
549
  // Map CLI options to SDK format
518
- const sdkOptions = mapCliOptionsToSDK(options);
550
+ const sdkOptions = mapCliOptionsToSDK({
551
+ ...options,
552
+ model: resolvedModel || options.model,
553
+ });
519
554
 
520
555
  // Load MCP configuration
521
556
  const mcpServers = await loadMcpConfig(options.cwd);
@@ -697,16 +732,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
697
732
  ws.send(msg);
698
733
  }
699
734
 
700
- // Extract and send token budget updates from result messages
701
- if (message.type === 'result') {
702
- const models = Object.keys(message.modelUsage || {});
703
- if (models.length > 0) {
704
- // Model info available in result message
705
- }
706
- const tokenBudgetData = extractTokenBudget(message);
707
- if (tokenBudgetData) {
708
- ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
709
- }
735
+ // Extract and send token budget updates from assistant/result usage payloads
736
+ const tokenBudgetData = extractTokenBudget(message);
737
+ if (tokenBudgetData) {
738
+ ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
710
739
  }
711
740
  }
712
741
 
@@ -3,6 +3,7 @@ import crossSpawn from 'cross-spawn';
3
3
  import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
4
4
  import { sessionsService } from './modules/providers/services/sessions.service.js';
5
5
  import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
6
+ import { providerModelsService } from './modules/providers/services/provider-models.service.js';
6
7
  import { createNormalizedMessage } from './shared/utils.js';
7
8
 
8
9
  // Use cross-spawn on Windows for better command execution
@@ -40,6 +41,7 @@ function isWorkspaceTrustPrompt(text = '') {
40
41
  async function spawnCursor(command, options = {}, ws) {
41
42
  return new Promise(async (resolve, reject) => {
42
43
  const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model, sessionSummary } = options;
44
+ const resolvedModel = await providerModelsService.resolveResumeModel('cursor', sessionId, model);
43
45
  let capturedSessionId = sessionId; // Track session ID throughout the process
44
46
  let sessionCreatedSent = false; // Track if we've already sent session-created event
45
47
  let hasRetriedWithTrust = false;
@@ -64,9 +66,10 @@ async function spawnCursor(command, options = {}, ws) {
64
66
  // Provide a prompt (works for both new and resumed sessions)
65
67
  baseArgs.push('-p', command);
66
68
 
67
- // Add model flag if specified (only meaningful for new sessions; harmless on resume)
68
- if (!sessionId && model) {
69
- baseArgs.push('--model', model);
69
+ // Model overrides are applied to both new and resumed sessions so a
70
+ // session-scoped change request can take effect on the next turn.
71
+ if (resolvedModel) {
72
+ baseArgs.push('--model', resolvedModel);
70
73
  }
71
74
 
72
75
  // Request streaming JSON when we are providing a prompt
@@ -9,6 +9,7 @@ import sessionManager from './sessionManager.js';
9
9
  import GeminiResponseHandler from './gemini-response-handler.js';
10
10
  import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
11
11
  import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
12
+ import { providerModelsService } from './modules/providers/services/provider-models.service.js';
12
13
  import { createNormalizedMessage } from './shared/utils.js';
13
14
 
14
15
  // Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
@@ -133,6 +134,11 @@ async function buildGeminiProcessEnv() {
133
134
  async function spawnGemini(command, options = {}, ws) {
134
135
  const { sessionId, projectPath, cwd, toolsSettings, permissionMode, images, sessionSummary } = options;
135
136
  const userId = ws?.userId ?? null;
137
+ const resolvedModel = await providerModelsService.resolveResumeModel(
138
+ 'gemini',
139
+ sessionId,
140
+ options.model
141
+ );
136
142
  let capturedSessionId = sessionId; // Track session ID throughout the process
137
143
  let sessionCreatedSent = false; // Track if we've already sent session-created event
138
144
  let assistantBlocks = []; // Accumulate the full response blocks including tools
@@ -257,7 +263,7 @@ async function spawnGemini(command, options = {}, ws) {
257
263
  }
258
264
 
259
265
  // Add model for all sessions (both new and resumed)
260
- let modelToUse = options.model || 'gemini-2.5-flash';
266
+ let modelToUse = resolvedModel || 'gemini-2.5-flash';
261
267
  args.push('--model', modelToUse);
262
268
  args.push('--output-format', 'stream-json');
263
269
 
@@ -1,5 +1,32 @@
1
1
  // Gemini Response Handler - JSON Stream processing
2
2
  import { sessionsService } from './modules/providers/services/sessions.service.js';
3
+ import { createNormalizedMessage } from './shared/utils.js';
4
+
5
+ function buildGeminiTokenBudget(tokens) {
6
+ if (!tokens || typeof tokens !== 'object') {
7
+ return null;
8
+ }
9
+
10
+ const parsedInputTokens = Number(tokens.input);
11
+ const parsedOutputTokens = Number(tokens.output);
12
+ const inputTokens = Number.isFinite(parsedInputTokens) ? parsedInputTokens : 0;
13
+ const outputTokens = Number.isFinite(parsedOutputTokens) ? parsedOutputTokens : 0;
14
+ const parsedUsed = Number(tokens.total);
15
+ const used = Number.isFinite(parsedUsed) ? parsedUsed : inputTokens + outputTokens;
16
+ if (!Number.isFinite(used) || used <= 0) {
17
+ return null;
18
+ }
19
+
20
+ return {
21
+ used,
22
+ inputTokens,
23
+ outputTokens,
24
+ breakdown: {
25
+ input: inputTokens,
26
+ output: outputTokens,
27
+ },
28
+ };
29
+ }
3
30
 
4
31
  class GeminiResponseHandler {
5
32
  constructor(ws, options = {}) {
@@ -60,6 +87,17 @@ class GeminiResponseHandler {
60
87
  for (const msg of normalized) {
61
88
  this.ws.send(msg);
62
89
  }
90
+
91
+ const tokenBudget = buildGeminiTokenBudget(event.tokens);
92
+ if (tokenBudget) {
93
+ this.ws.send(createNormalizedMessage({
94
+ kind: 'status',
95
+ text: 'token_budget',
96
+ tokenBudget,
97
+ sessionId: sid,
98
+ provider: 'gemini',
99
+ }));
100
+ }
63
101
  }
64
102
 
65
103
  forceFlush() {
package/server/index.js CHANGED
@@ -10,8 +10,15 @@ import { spawn } from 'child_process';
10
10
  import express from 'express';
11
11
  import cors from 'cors';
12
12
  import mime from 'mime-types';
13
+ import Database from 'better-sqlite3';
13
14
 
14
- import { AppError, WORKSPACES_ROOT, ensureUserWorkspaceRoot, validateWorkspacePath } from '@/shared/utils.js';
15
+ import {
16
+ AppError,
17
+ WORKSPACES_ROOT,
18
+ ensureUserWorkspaceRoot,
19
+ getOpenCodeDatabasePath,
20
+ validateWorkspacePath,
21
+ } from '@/shared/utils.js';
15
22
  import { closeSessionsWatcher, initializeSessionsWatcher } from '@/modules/providers/index.js';
16
23
  import { createWebSocketServer } from '@/modules/websocket/index.js';
17
24
 
@@ -45,6 +52,12 @@ import {
45
52
  isGeminiSessionActive,
46
53
  getActiveGeminiSessions,
47
54
  } from './gemini-cli.js';
55
+ import {
56
+ spawnOpenCode,
57
+ abortOpenCodeSession,
58
+ isOpenCodeSessionActive,
59
+ getActiveOpenCodeSessions,
60
+ } from './opencode-cli.js';
48
61
  import sessionManager from './sessionManager.js';
49
62
  import {
50
63
  stripAnsiSequences,
@@ -67,7 +80,7 @@ import geminiRoutes from './routes/gemini.js';
67
80
  import pluginsRoutes from './routes/plugins.js';
68
81
  import providerRoutes from './modules/providers/provider.routes.js';
69
82
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
70
- import { initializeDatabase, projectsDb } from './modules/database/index.js';
83
+ import { initializeDatabase, projectsDb, sessionsDb } from './modules/database/index.js';
71
84
  import { configureWebPush } from './services/vapid-keys.js';
72
85
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
73
86
  import { IS_PLATFORM } from './constants/config.js';
@@ -95,21 +108,25 @@ const wss = createWebSocketServer(server, {
95
108
  spawnCursor,
96
109
  queryCodex,
97
110
  spawnGemini,
111
+ spawnOpenCode,
98
112
  abortClaudeSDKSession,
99
113
  abortCursorSession,
100
114
  abortCodexSession,
101
115
  abortGeminiSession,
116
+ abortOpenCodeSession,
102
117
  resolveToolApproval,
103
118
  isClaudeSDKSessionActive,
104
119
  isCursorSessionActive,
105
120
  isCodexSessionActive,
106
121
  isGeminiSessionActive,
122
+ isOpenCodeSessionActive,
107
123
  reconnectSessionWriter,
108
124
  getPendingApprovalsForSession,
109
125
  getActiveClaudeSDKSessions,
110
126
  getActiveCursorSessions,
111
127
  getActiveCodexSessions,
112
128
  getActiveGeminiSessions,
129
+ getActiveOpenCodeSessions,
113
130
  },
114
131
  shell: {
115
132
  getSessionById: (userId, sessionId) => sessionManager.getSession(userId, sessionId),
@@ -1139,23 +1156,129 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1139
1156
  return res.json({
1140
1157
  used: 0,
1141
1158
  total: 0,
1142
- breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
1159
+ inputTokens: 0,
1160
+ outputTokens: 0,
1161
+ breakdown: { input: 0, output: 0 },
1143
1162
  unsupported: true,
1144
1163
  message: 'Token usage tracking not available for Cursor sessions'
1145
1164
  });
1146
1165
  }
1147
1166
 
1148
- // Handle Gemini sessions - they are raw logs in our current setup
1149
1167
  if (provider === 'gemini') {
1168
+ const session = sessionsDb.getSessionById(safeSessionId);
1169
+ const sessionFilePath = session?.jsonl_path;
1170
+ if (!sessionFilePath) {
1171
+ return res.json({
1172
+ used: 0,
1173
+ inputTokens: 0,
1174
+ outputTokens: 0,
1175
+ breakdown: { input: 0, output: 0 },
1176
+ unsupported: true,
1177
+ message: 'Token usage tracking not available for this Gemini session'
1178
+ });
1179
+ }
1180
+
1181
+ let fileContent;
1182
+ try {
1183
+ fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
1184
+ } catch (error) {
1185
+ if (error.code === 'ENOENT') {
1186
+ return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
1187
+ }
1188
+ throw error;
1189
+ }
1190
+
1191
+ const lines = fileContent.trim().split('\n');
1192
+ let inputTokens = 0;
1193
+ let outputTokens = 0;
1194
+ let totalTokens = 0;
1195
+
1196
+ for (let i = lines.length - 1; i >= 0; i--) {
1197
+ try {
1198
+ const entry = JSON.parse(lines[i]);
1199
+ if (!entry.tokens || typeof entry.tokens !== 'object') {
1200
+ continue;
1201
+ }
1202
+
1203
+ inputTokens = Number(entry.tokens.input || 0);
1204
+ outputTokens = Number(entry.tokens.output || 0);
1205
+ totalTokens = Number(entry.tokens.total || inputTokens + outputTokens || 0);
1206
+ break;
1207
+ } catch {
1208
+ continue;
1209
+ }
1210
+ }
1211
+
1150
1212
  return res.json({
1151
- used: 0,
1152
- total: 0,
1153
- breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
1154
- unsupported: true,
1155
- message: 'Token usage tracking not available for Gemini sessions'
1213
+ used: totalTokens,
1214
+ inputTokens,
1215
+ outputTokens,
1216
+ breakdown: {
1217
+ input: inputTokens,
1218
+ output: outputTokens
1219
+ }
1156
1220
  });
1157
1221
  }
1158
1222
 
1223
+ if (provider === 'opencode') {
1224
+ const dbPath = getOpenCodeDatabasePath();
1225
+ if (!fs.existsSync(dbPath)) {
1226
+ return res.status(404).json({ error: 'OpenCode database not found' });
1227
+ }
1228
+
1229
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
1230
+ try {
1231
+ const columns = db.prepare('PRAGMA table_info(session)').all();
1232
+ const columnNames = new Set(columns.map((column) => column.name));
1233
+ const requiredColumns = ['tokens_input', 'tokens_output', 'tokens_reasoning', 'tokens_cache_read', 'tokens_cache_write'];
1234
+ if (!requiredColumns.every((column) => columnNames.has(column))) {
1235
+ return res.json({
1236
+ used: 0,
1237
+ inputTokens: 0,
1238
+ outputTokens: 0,
1239
+ breakdown: { input: 0, output: 0 },
1240
+ unsupported: true,
1241
+ message: 'Token usage tracking is not available in this OpenCode database schema'
1242
+ });
1243
+ }
1244
+
1245
+ const row = db.prepare(`
1246
+ SELECT
1247
+ tokens_input AS inputTokens,
1248
+ tokens_output AS outputTokens,
1249
+ tokens_reasoning AS reasoningTokens,
1250
+ tokens_cache_read AS cacheReadTokens,
1251
+ tokens_cache_write AS cacheWriteTokens
1252
+ FROM session
1253
+ WHERE id = ?
1254
+ `).get(safeSessionId);
1255
+
1256
+ if (!row) {
1257
+ return res.status(404).json({ error: 'OpenCode session not found', sessionId: safeSessionId });
1258
+ }
1259
+
1260
+ const inputTokens = Number(row.inputTokens || 0) + Number(row.cacheReadTokens || 0);
1261
+ const outputTokens = Number(row.outputTokens || 0);
1262
+ const totalUsed = Number(row.inputTokens || 0)
1263
+ + outputTokens
1264
+ + Number(row.reasoningTokens || 0)
1265
+ + Number(row.cacheReadTokens || 0)
1266
+ + Number(row.cacheWriteTokens || 0);
1267
+
1268
+ return res.json({
1269
+ used: totalUsed,
1270
+ inputTokens,
1271
+ outputTokens,
1272
+ breakdown: {
1273
+ input: inputTokens,
1274
+ output: outputTokens
1275
+ }
1276
+ });
1277
+ } finally {
1278
+ db.close();
1279
+ }
1280
+ }
1281
+
1159
1282
  // Handle Codex sessions
1160
1283
  if (provider === 'codex') {
1161
1284
  const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
@@ -1196,6 +1319,8 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1196
1319
  throw error;
1197
1320
  }
1198
1321
  const lines = fileContent.trim().split('\n');
1322
+ let inputTokens = 0;
1323
+ let outputTokens = 0;
1199
1324
  let totalTokens = 0;
1200
1325
  let contextWindow = 200000; // Default for Codex/OpenAI
1201
1326
 
@@ -1208,7 +1333,9 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1208
1333
  if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
1209
1334
  const tokenInfo = entry.payload.info;
1210
1335
  if (tokenInfo.total_token_usage) {
1211
- totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
1336
+ inputTokens = tokenInfo.total_token_usage.input_tokens || 0;
1337
+ outputTokens = tokenInfo.total_token_usage.output_tokens || 0;
1338
+ totalTokens = tokenInfo.total_token_usage.total_tokens || inputTokens + outputTokens;
1212
1339
  }
1213
1340
  if (tokenInfo.model_context_window) {
1214
1341
  contextWindow = tokenInfo.model_context_window;
@@ -1223,7 +1350,13 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1223
1350
 
1224
1351
  return res.json({
1225
1352
  used: totalTokens,
1226
- total: contextWindow
1353
+ total: contextWindow,
1354
+ inputTokens,
1355
+ outputTokens,
1356
+ breakdown: {
1357
+ input: inputTokens,
1358
+ output: outputTokens
1359
+ }
1227
1360
  });
1228
1361
  }
1229
1362
 
@@ -1266,8 +1399,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1266
1399
  const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
1267
1400
  const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
1268
1401
  let inputTokens = 0;
1269
- let cacheCreationTokens = 0;
1270
- let cacheReadTokens = 0;
1402
+ let outputTokens = 0;
1271
1403
 
1272
1404
  // Find the latest assistant message with usage data (scan from end)
1273
1405
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -1280,8 +1412,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1280
1412
 
1281
1413
  // Use token counts from latest assistant message only
1282
1414
  inputTokens = usage.input_tokens || 0;
1283
- cacheCreationTokens = usage.cache_creation_input_tokens || 0;
1284
- cacheReadTokens = usage.cache_read_input_tokens || 0;
1415
+ outputTokens = usage.output_tokens || 0;
1285
1416
 
1286
1417
  break; // Stop after finding the latest assistant message
1287
1418
  }
@@ -1291,16 +1422,16 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1291
1422
  }
1292
1423
  }
1293
1424
 
1294
- // Calculate total context usage (excluding output_tokens, as per ccusage)
1295
- const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
1425
+ const totalUsed = inputTokens + outputTokens;
1296
1426
 
1297
1427
  res.json({
1298
1428
  used: totalUsed,
1299
1429
  total: contextWindow,
1430
+ inputTokens,
1431
+ outputTokens,
1300
1432
  breakdown: {
1301
1433
  input: inputTokens,
1302
- cacheCreation: cacheCreationTokens,
1303
- cacheRead: cacheReadTokens
1434
+ output: outputTokens
1304
1435
  }
1305
1436
  });
1306
1437
  } catch (error) {
@@ -1,4 +1,5 @@
1
1
  export { initializeDatabase } from '@/modules/database/init-db.js';
2
+ export { closeConnection, getConnection, getDatabasePath } from '@/modules/database/connection.js';
2
3
  export { apiKeysDb } from '@/modules/database/repositories/api-keys.js';
3
4
  export { appConfigDb } from '@/modules/database/repositories/app-config.js';
4
5
  export { credentialsDb } from '@/modules/database/repositories/credentials.js';
@@ -41,6 +41,7 @@ type ProjectApiView = {
41
41
  cursorSessions: [];
42
42
  codexSessions: [];
43
43
  geminiSessions: [];
44
+ opencodeSessions: [];
44
45
  sessionMeta: {
45
46
  hasMore: false;
46
47
  total: 0;
@@ -93,6 +94,7 @@ function mapProjectRowToApiView(projectRow: ProjectRepositoryRow): ProjectApiVie
93
94
  cursorSessions: [],
94
95
  codexSessions: [],
95
96
  geminiSessions: [],
97
+ opencodeSessions: [],
96
98
  sessionMeta: {
97
99
  hasMore: false,
98
100
  total: 0,
@@ -14,7 +14,7 @@ type SessionSummary = {
14
14
  lastActivity: string;
15
15
  };
16
16
 
17
- type SessionsByProvider = Record<'claude' | 'cursor' | 'codex' | 'gemini', SessionSummary[]>;
17
+ type SessionsByProvider = Record<'claude' | 'cursor' | 'codex' | 'gemini' | 'opencode', SessionSummary[]>;
18
18
 
19
19
  type SessionRepositoryRow = {
20
20
  provider: string;
@@ -34,6 +34,7 @@ export type ProjectListItem = {
34
34
  cursorSessions: SessionSummary[];
35
35
  codexSessions: SessionSummary[];
36
36
  geminiSessions: SessionSummary[];
37
+ opencodeSessions: SessionSummary[];
37
38
  sessionMeta: {
38
39
  hasMore: boolean;
39
40
  total: number;
@@ -74,6 +75,7 @@ export type ProjectSessionsPageApiView = {
74
75
  cursorSessions: SessionSummary[];
75
76
  codexSessions: SessionSummary[];
76
77
  geminiSessions: SessionSummary[];
78
+ opencodeSessions: SessionSummary[];
77
79
  sessionMeta: {
78
80
  hasMore: boolean;
79
81
  total: number;
@@ -139,6 +141,7 @@ function bucketSessionRowsByProvider(rows: SessionRepositoryRow[]): SessionsByPr
139
141
  cursor: [],
140
142
  codex: [],
141
143
  gemini: [],
144
+ opencode: [],
142
145
  };
143
146
 
144
147
  for (const row of rows) {
@@ -257,6 +260,7 @@ export async function getProjectsWithSessions(
257
260
  cursorSessions: sessionsPage.sessionsByProvider.cursor,
258
261
  codexSessions: sessionsPage.sessionsByProvider.codex,
259
262
  geminiSessions: sessionsPage.sessionsByProvider.gemini,
263
+ opencodeSessions: sessionsPage.sessionsByProvider.opencode,
260
264
  sessionMeta: {
261
265
  hasMore: sessionsPage.hasMore,
262
266
  total: sessionsPage.total,
@@ -314,6 +318,7 @@ export async function getArchivedProjectsWithSessions(
314
318
  cursorSessions: sessionsPage.sessionsByProvider.cursor,
315
319
  codexSessions: sessionsPage.sessionsByProvider.codex,
316
320
  geminiSessions: sessionsPage.sessionsByProvider.gemini,
321
+ opencodeSessions: sessionsPage.sessionsByProvider.opencode,
317
322
  sessionMeta: {
318
323
  hasMore: sessionsPage.hasMore,
319
324
  total: sessionsPage.total,
@@ -347,6 +352,7 @@ export async function getProjectSessionsPage(
347
352
  cursorSessions: sessionsPage.sessionsByProvider.cursor,
348
353
  codexSessions: sessionsPage.sessionsByProvider.codex,
349
354
  geminiSessions: sessionsPage.sessionsByProvider.gemini,
355
+ opencodeSessions: sessionsPage.sessionsByProvider.opencode,
350
356
  sessionMeta: {
351
357
  hasMore: sessionsPage.hasMore,
352
358
  total: sessionsPage.total,