@cloudcli-ai/cloudcli 1.32.0 → 1.33.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 (174) 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-MOIvpy53.css +32 -0
  9. package/dist/assets/index-skXSjBje.js +1351 -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-auth.provider.js +25 -6
  29. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  30. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +141 -0
  31. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
  32. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  33. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  34. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +84 -0
  35. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
  36. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -39
  37. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
  38. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  39. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  40. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +754 -0
  41. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
  42. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +2 -15
  43. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
  45. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  46. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +27 -0
  47. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -0
  48. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +3 -9
  49. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
  51. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  52. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +92 -0
  53. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
  54. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
  55. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
  56. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +267 -0
  57. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
  58. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +115 -0
  59. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
  60. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +410 -0
  61. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
  62. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
  63. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
  64. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +19 -0
  65. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
  66. package/dist-server/server/modules/providers/provider.registry.js +2 -0
  67. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  68. package/dist-server/server/modules/providers/provider.routes.js +42 -1
  69. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  70. package/dist-server/server/modules/providers/services/mcp.service.js +1 -9
  71. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  72. package/dist-server/server/modules/providers/services/provider-models.service.js +199 -0
  73. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
  74. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
  75. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  76. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +7 -0
  77. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  78. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  79. package/dist-server/server/modules/providers/tests/mcp.test.js +73 -6
  80. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  81. package/dist-server/server/modules/providers/tests/opencode-models.test.js +66 -0
  82. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
  83. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +262 -0
  84. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
  85. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +270 -0
  86. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
  87. package/dist-server/server/modules/providers/tests/skills.test.js +33 -0
  88. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  89. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +12 -1
  90. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  91. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +4 -4
  92. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -1
  93. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -1
  94. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  95. package/dist-server/server/openai-codex.js +32 -4
  96. package/dist-server/server/openai-codex.js.map +1 -1
  97. package/dist-server/server/opencode-cli.js +287 -0
  98. package/dist-server/server/opencode-cli.js.map +1 -0
  99. package/dist-server/server/opencode-cli.test.js +84 -0
  100. package/dist-server/server/opencode-cli.test.js.map +1 -0
  101. package/dist-server/server/routes/agent.js +21 -8
  102. package/dist-server/server/routes/agent.js.map +1 -1
  103. package/dist-server/server/routes/commands.js +202 -209
  104. package/dist-server/server/routes/commands.js.map +1 -1
  105. package/dist-server/server/routes/cursor.js +2 -2
  106. package/dist-server/server/routes/cursor.js.map +1 -1
  107. package/dist-server/server/routes/settings.js +0 -10
  108. package/dist-server/server/routes/settings.js.map +1 -1
  109. package/dist-server/server/routes/tests/commands.test.js +76 -0
  110. package/dist-server/server/routes/tests/commands.test.js.map +1 -0
  111. package/dist-server/server/shared/utils.js +286 -0
  112. package/dist-server/server/shared/utils.js.map +1 -1
  113. package/package.json +3 -1
  114. package/public/api-docs.html +878 -0
  115. package/public/modelConstants.js +841 -0
  116. package/server/claude-sdk.js +64 -35
  117. package/server/cursor-cli.js +6 -3
  118. package/server/gemini-cli.js +7 -1
  119. package/server/gemini-response-handler.js +38 -0
  120. package/server/index.js +144 -19
  121. package/server/modules/database/index.ts +1 -0
  122. package/server/modules/projects/services/project-management.service.ts +2 -0
  123. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +7 -1
  124. package/server/modules/providers/README.md +11 -3
  125. package/server/modules/providers/list/claude/claude-auth.provider.ts +30 -6
  126. package/server/modules/providers/list/claude/claude-models.provider.ts +191 -0
  127. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  128. package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
  129. package/server/modules/providers/list/codex/codex-skills.provider.ts +10 -50
  130. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  131. package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
  132. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +7 -20
  133. package/server/modules/providers/list/cursor/cursor.provider.ts +3 -0
  134. package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
  135. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +3 -10
  136. package/server/modules/providers/list/gemini/gemini.provider.ts +3 -0
  137. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
  138. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
  139. package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
  140. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +157 -0
  141. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
  142. package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
  143. package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
  144. package/server/modules/providers/provider.registry.ts +2 -0
  145. package/server/modules/providers/provider.routes.ts +62 -2
  146. package/server/modules/providers/services/mcp.service.ts +1 -12
  147. package/server/modules/providers/services/provider-models.service.ts +325 -0
  148. package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
  149. package/server/modules/providers/services/sessions-watcher.service.ts +8 -0
  150. package/server/modules/providers/shared/base/abstract.provider.ts +2 -0
  151. package/server/modules/providers/tests/mcp.test.ts +93 -6
  152. package/server/modules/providers/tests/opencode-models.test.ts +73 -0
  153. package/server/modules/providers/tests/opencode-sessions.test.ts +334 -0
  154. package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
  155. package/server/modules/providers/tests/skills.test.ts +66 -0
  156. package/server/modules/websocket/services/chat-websocket.service.ts +15 -1
  157. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +4 -4
  158. package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
  159. package/server/openai-codex.js +41 -4
  160. package/server/opencode-cli.js +336 -0
  161. package/server/opencode-cli.test.js +95 -0
  162. package/server/routes/agent.js +22 -8
  163. package/server/routes/commands.js +254 -233
  164. package/server/routes/cursor.js +2 -2
  165. package/server/routes/settings.js +1 -10
  166. package/server/routes/tests/commands.test.js +82 -0
  167. package/server/shared/interfaces.ts +45 -0
  168. package/server/shared/types.ts +88 -1
  169. package/server/shared/utils.ts +384 -0
  170. package/dist/assets/index-AaSE7-3t.js +0 -1336
  171. package/dist/assets/index-CKaku72N.css +0 -32
  172. package/dist-server/shared/modelConstants.js +0 -99
  173. package/dist-server/shared/modelConstants.js.map +0 -1
  174. 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,
@@ -204,7 +205,7 @@ function mapCliOptionsToSDK(options = {}) {
204
205
 
205
206
  // Map model (default to sonnet)
206
207
  // Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
207
- sdkOptions.model = options.model || CLAUDE_MODELS.DEFAULT;
208
+ sdkOptions.model = options.model || CLAUDE_FALLBACK_MODELS.DEFAULT;
208
209
  // Model logged at query start below
209
210
 
210
211
  // Map system prompt configuration
@@ -284,43 +285,68 @@ function transformMessage(sdkMessage) {
284
285
  return sdkMessage;
285
286
  }
286
287
 
288
+ function readNumber(value) {
289
+ const parsed = Number(value);
290
+ return Number.isFinite(parsed) ? parsed : 0;
291
+ }
292
+
287
293
  /**
288
- * Extracts token usage from SDK result messages
289
- * @param {Object} resultMessage - SDK result message
294
+ * Extracts token usage from SDK messages.
295
+ * Prefers per-step `message.usage` (Claude message payload), then falls back
296
+ * to result-level usage/modelUsage for compatibility across SDK versions.
297
+ * @param {Object} sdkMessage - SDK stream message
290
298
  * @returns {Object|null} Token budget object or null
291
299
  */
292
- function extractTokenBudget(resultMessage) {
293
- if (resultMessage.type !== 'result' || !resultMessage.modelUsage) {
300
+ function extractTokenBudget(sdkMessage) {
301
+ if (!sdkMessage || typeof sdkMessage !== 'object') {
294
302
  return null;
295
303
  }
296
304
 
297
- // Get the first model's usage data
298
- const modelKey = Object.keys(resultMessage.modelUsage)[0];
299
- const modelData = resultMessage.modelUsage[modelKey];
305
+ const messageUsage = sdkMessage.message?.usage || sdkMessage.usage;
306
+ if (messageUsage && typeof messageUsage === 'object') {
307
+ const inputTokens = readNumber(messageUsage.input_tokens ?? messageUsage.inputTokens);
308
+ const outputTokens = readNumber(messageUsage.output_tokens ?? messageUsage.outputTokens);
309
+ const totalUsed = inputTokens + outputTokens;
310
+ const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
300
311
 
301
- if (!modelData) {
302
- return null;
312
+ return {
313
+ used: totalUsed,
314
+ total: contextWindow,
315
+ inputTokens,
316
+ outputTokens,
317
+ breakdown: {
318
+ input: inputTokens,
319
+ output: outputTokens,
320
+ },
321
+ };
303
322
  }
304
323
 
305
- // Use cumulative tokens if available (tracks total for the session)
306
- // Otherwise fall back to per-request tokens
307
- const inputTokens = modelData.cumulativeInputTokens || modelData.inputTokens || 0;
308
- const outputTokens = modelData.cumulativeOutputTokens || modelData.outputTokens || 0;
309
- const cacheReadTokens = modelData.cumulativeCacheReadInputTokens || modelData.cacheReadInputTokens || 0;
310
- const cacheCreationTokens = modelData.cumulativeCacheCreationInputTokens || modelData.cacheCreationInputTokens || 0;
324
+ if (!sdkMessage.modelUsage || typeof sdkMessage.modelUsage !== 'object') {
325
+ return null;
326
+ }
311
327
 
312
- // Total used = input + output + cache tokens
313
- const totalUsed = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
328
+ // Fallback for older SDK messages with only modelUsage
329
+ const modelKey = Object.keys(sdkMessage.modelUsage)[0];
330
+ const modelData = sdkMessage.modelUsage[modelKey];
314
331
 
315
- // Use configured context window budget from environment (default 160000)
316
- // This is the user's budget limit, not the model's context window
317
- const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000;
332
+ if (!modelData || typeof modelData !== 'object') {
333
+ return null;
334
+ }
318
335
 
319
- // Token calc logged via token-budget WS event
336
+ const inputTokens = readNumber(modelData.cumulativeInputTokens ?? modelData.inputTokens);
337
+ const outputTokens = readNumber(modelData.cumulativeOutputTokens ?? modelData.outputTokens);
338
+ const totalUsed = inputTokens + outputTokens;
339
+ const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
320
340
 
321
341
  return {
322
342
  used: totalUsed,
323
- total: contextWindow
343
+ total: contextWindow,
344
+ inputTokens,
345
+ outputTokens,
346
+ breakdown: {
347
+ input: inputTokens,
348
+ output: outputTokens,
349
+ },
324
350
  };
325
351
  }
326
352
 
@@ -491,8 +517,17 @@ async function queryClaudeSDK(command, options = {}, ws) {
491
517
  };
492
518
 
493
519
  try {
520
+ const resolvedModel = await providerModelsService.resolveResumeModel(
521
+ 'claude',
522
+ sessionId,
523
+ options.model,
524
+ );
525
+
494
526
  // Map CLI options to SDK format
495
- const sdkOptions = mapCliOptionsToSDK(options);
527
+ const sdkOptions = mapCliOptionsToSDK({
528
+ ...options,
529
+ model: resolvedModel || options.model,
530
+ });
496
531
 
497
532
  // Load MCP configuration
498
533
  const mcpServers = await loadMcpConfig(options.cwd);
@@ -674,16 +709,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
674
709
  ws.send(msg);
675
710
  }
676
711
 
677
- // Extract and send token budget updates from result messages
678
- if (message.type === 'result') {
679
- const models = Object.keys(message.modelUsage || {});
680
- if (models.length > 0) {
681
- // Model info available in result message
682
- }
683
- const tokenBudgetData = extractTokenBudget(message);
684
- if (tokenBudgetData) {
685
- ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
686
- }
712
+ // Extract and send token budget updates from assistant/result usage payloads
713
+ const tokenBudgetData = extractTokenBudget(message);
714
+ if (tokenBudgetData) {
715
+ ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
687
716
  }
688
717
  }
689
718
 
@@ -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
@@ -28,6 +29,7 @@ function isWorkspaceTrustPrompt(text = '') {
28
29
  async function spawnCursor(command, options = {}, ws) {
29
30
  return new Promise(async (resolve, reject) => {
30
31
  const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model, sessionSummary } = options;
32
+ const resolvedModel = await providerModelsService.resolveResumeModel('cursor', sessionId, model);
31
33
  let capturedSessionId = sessionId; // Track session ID throughout the process
32
34
  let sessionCreatedSent = false; // Track if we've already sent session-created event
33
35
  let hasRetriedWithTrust = false;
@@ -52,9 +54,10 @@ async function spawnCursor(command, options = {}, ws) {
52
54
  // Provide a prompt (works for both new and resumed sessions)
53
55
  baseArgs.push('-p', command);
54
56
 
55
- // Add model flag if specified (only meaningful for new sessions; harmless on resume)
56
- if (!sessionId && model) {
57
- baseArgs.push('--model', model);
57
+ // Model overrides are applied to both new and resumed sessions so a
58
+ // session-scoped change request can take effect on the next turn.
59
+ if (resolvedModel) {
60
+ baseArgs.push('--model', resolvedModel);
58
61
  }
59
62
 
60
63
  // 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)
@@ -120,6 +121,11 @@ async function buildGeminiProcessEnv() {
120
121
 
121
122
  async function spawnGemini(command, options = {}, ws) {
122
123
  const { sessionId, projectPath, cwd, toolsSettings, permissionMode, images, sessionSummary } = options;
124
+ const resolvedModel = await providerModelsService.resolveResumeModel(
125
+ 'gemini',
126
+ sessionId,
127
+ options.model
128
+ );
123
129
  let capturedSessionId = sessionId; // Track session ID throughout the process
124
130
  let sessionCreatedSent = false; // Track if we've already sent session-created event
125
131
  let assistantBlocks = []; // Accumulate the full response blocks including tools
@@ -244,7 +250,7 @@ async function spawnGemini(command, options = {}, ws) {
244
250
  }
245
251
 
246
252
  // Add model for all sessions (both new and resumed)
247
- let modelToUse = options.model || 'gemini-2.5-flash';
253
+ let modelToUse = resolvedModel || 'gemini-2.5-flash';
248
254
  args.push('--model', modelToUse);
249
255
  args.push('--output-format', 'stream-json');
250
256
 
@@ -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,9 @@ 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, validateWorkspacePath } from '@/shared/utils.js';
15
+ import { AppError, WORKSPACES_ROOT, getOpenCodeDatabasePath, validateWorkspacePath } from '@/shared/utils.js';
15
16
  import { closeSessionsWatcher, initializeSessionsWatcher } from '@/modules/providers/index.js';
16
17
  import { createWebSocketServer } from '@/modules/websocket/index.js';
17
18
 
@@ -45,6 +46,12 @@ import {
45
46
  isGeminiSessionActive,
46
47
  getActiveGeminiSessions,
47
48
  } from './gemini-cli.js';
49
+ import {
50
+ spawnOpenCode,
51
+ abortOpenCodeSession,
52
+ isOpenCodeSessionActive,
53
+ getActiveOpenCodeSessions,
54
+ } from './opencode-cli.js';
48
55
  import sessionManager from './sessionManager.js';
49
56
  import {
50
57
  stripAnsiSequences,
@@ -66,7 +73,7 @@ import geminiRoutes from './routes/gemini.js';
66
73
  import pluginsRoutes from './routes/plugins.js';
67
74
  import providerRoutes from './modules/providers/provider.routes.js';
68
75
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
69
- import { initializeDatabase, projectsDb } from './modules/database/index.js';
76
+ import { initializeDatabase, projectsDb, sessionsDb } from './modules/database/index.js';
70
77
  import { configureWebPush } from './services/vapid-keys.js';
71
78
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
72
79
  import { IS_PLATFORM } from './constants/config.js';
@@ -94,21 +101,25 @@ const wss = createWebSocketServer(server, {
94
101
  spawnCursor,
95
102
  queryCodex,
96
103
  spawnGemini,
104
+ spawnOpenCode,
97
105
  abortClaudeSDKSession,
98
106
  abortCursorSession,
99
107
  abortCodexSession,
100
108
  abortGeminiSession,
109
+ abortOpenCodeSession,
101
110
  resolveToolApproval,
102
111
  isClaudeSDKSessionActive,
103
112
  isCursorSessionActive,
104
113
  isCodexSessionActive,
105
114
  isGeminiSessionActive,
115
+ isOpenCodeSessionActive,
106
116
  reconnectSessionWriter,
107
117
  getPendingApprovalsForSession,
108
118
  getActiveClaudeSDKSessions,
109
119
  getActiveCursorSessions,
110
120
  getActiveCodexSessions,
111
121
  getActiveGeminiSessions,
122
+ getActiveOpenCodeSessions,
112
123
  },
113
124
  shell: {
114
125
  getSessionById: (sessionId) => sessionManager.getSession(sessionId),
@@ -1131,23 +1142,129 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1131
1142
  return res.json({
1132
1143
  used: 0,
1133
1144
  total: 0,
1134
- breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
1145
+ inputTokens: 0,
1146
+ outputTokens: 0,
1147
+ breakdown: { input: 0, output: 0 },
1135
1148
  unsupported: true,
1136
1149
  message: 'Token usage tracking not available for Cursor sessions'
1137
1150
  });
1138
1151
  }
1139
1152
 
1140
- // Handle Gemini sessions - they are raw logs in our current setup
1141
1153
  if (provider === 'gemini') {
1154
+ const session = sessionsDb.getSessionById(safeSessionId);
1155
+ const sessionFilePath = session?.jsonl_path;
1156
+ if (!sessionFilePath) {
1157
+ return res.json({
1158
+ used: 0,
1159
+ inputTokens: 0,
1160
+ outputTokens: 0,
1161
+ breakdown: { input: 0, output: 0 },
1162
+ unsupported: true,
1163
+ message: 'Token usage tracking not available for this Gemini session'
1164
+ });
1165
+ }
1166
+
1167
+ let fileContent;
1168
+ try {
1169
+ fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
1170
+ } catch (error) {
1171
+ if (error.code === 'ENOENT') {
1172
+ return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
1173
+ }
1174
+ throw error;
1175
+ }
1176
+
1177
+ const lines = fileContent.trim().split('\n');
1178
+ let inputTokens = 0;
1179
+ let outputTokens = 0;
1180
+ let totalTokens = 0;
1181
+
1182
+ for (let i = lines.length - 1; i >= 0; i--) {
1183
+ try {
1184
+ const entry = JSON.parse(lines[i]);
1185
+ if (!entry.tokens || typeof entry.tokens !== 'object') {
1186
+ continue;
1187
+ }
1188
+
1189
+ inputTokens = Number(entry.tokens.input || 0);
1190
+ outputTokens = Number(entry.tokens.output || 0);
1191
+ totalTokens = Number(entry.tokens.total || inputTokens + outputTokens || 0);
1192
+ break;
1193
+ } catch {
1194
+ continue;
1195
+ }
1196
+ }
1197
+
1142
1198
  return res.json({
1143
- used: 0,
1144
- total: 0,
1145
- breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
1146
- unsupported: true,
1147
- message: 'Token usage tracking not available for Gemini sessions'
1199
+ used: totalTokens,
1200
+ inputTokens,
1201
+ outputTokens,
1202
+ breakdown: {
1203
+ input: inputTokens,
1204
+ output: outputTokens
1205
+ }
1148
1206
  });
1149
1207
  }
1150
1208
 
1209
+ if (provider === 'opencode') {
1210
+ const dbPath = getOpenCodeDatabasePath();
1211
+ if (!fs.existsSync(dbPath)) {
1212
+ return res.status(404).json({ error: 'OpenCode database not found' });
1213
+ }
1214
+
1215
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
1216
+ try {
1217
+ const columns = db.prepare('PRAGMA table_info(session)').all();
1218
+ const columnNames = new Set(columns.map((column) => column.name));
1219
+ const requiredColumns = ['tokens_input', 'tokens_output', 'tokens_reasoning', 'tokens_cache_read', 'tokens_cache_write'];
1220
+ if (!requiredColumns.every((column) => columnNames.has(column))) {
1221
+ return res.json({
1222
+ used: 0,
1223
+ inputTokens: 0,
1224
+ outputTokens: 0,
1225
+ breakdown: { input: 0, output: 0 },
1226
+ unsupported: true,
1227
+ message: 'Token usage tracking is not available in this OpenCode database schema'
1228
+ });
1229
+ }
1230
+
1231
+ const row = db.prepare(`
1232
+ SELECT
1233
+ tokens_input AS inputTokens,
1234
+ tokens_output AS outputTokens,
1235
+ tokens_reasoning AS reasoningTokens,
1236
+ tokens_cache_read AS cacheReadTokens,
1237
+ tokens_cache_write AS cacheWriteTokens
1238
+ FROM session
1239
+ WHERE id = ?
1240
+ `).get(safeSessionId);
1241
+
1242
+ if (!row) {
1243
+ return res.status(404).json({ error: 'OpenCode session not found', sessionId: safeSessionId });
1244
+ }
1245
+
1246
+ const inputTokens = Number(row.inputTokens || 0) + Number(row.cacheReadTokens || 0);
1247
+ const outputTokens = Number(row.outputTokens || 0);
1248
+ const totalUsed = Number(row.inputTokens || 0)
1249
+ + outputTokens
1250
+ + Number(row.reasoningTokens || 0)
1251
+ + Number(row.cacheReadTokens || 0)
1252
+ + Number(row.cacheWriteTokens || 0);
1253
+
1254
+ return res.json({
1255
+ used: totalUsed,
1256
+ inputTokens,
1257
+ outputTokens,
1258
+ breakdown: {
1259
+ input: inputTokens,
1260
+ output: outputTokens
1261
+ }
1262
+ });
1263
+ } finally {
1264
+ db.close();
1265
+ }
1266
+ }
1267
+
1151
1268
  // Handle Codex sessions
1152
1269
  if (provider === 'codex') {
1153
1270
  const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
@@ -1188,6 +1305,8 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1188
1305
  throw error;
1189
1306
  }
1190
1307
  const lines = fileContent.trim().split('\n');
1308
+ let inputTokens = 0;
1309
+ let outputTokens = 0;
1191
1310
  let totalTokens = 0;
1192
1311
  let contextWindow = 200000; // Default for Codex/OpenAI
1193
1312
 
@@ -1200,7 +1319,9 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1200
1319
  if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
1201
1320
  const tokenInfo = entry.payload.info;
1202
1321
  if (tokenInfo.total_token_usage) {
1203
- totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
1322
+ inputTokens = tokenInfo.total_token_usage.input_tokens || 0;
1323
+ outputTokens = tokenInfo.total_token_usage.output_tokens || 0;
1324
+ totalTokens = tokenInfo.total_token_usage.total_tokens || inputTokens + outputTokens;
1204
1325
  }
1205
1326
  if (tokenInfo.model_context_window) {
1206
1327
  contextWindow = tokenInfo.model_context_window;
@@ -1215,7 +1336,13 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1215
1336
 
1216
1337
  return res.json({
1217
1338
  used: totalTokens,
1218
- total: contextWindow
1339
+ total: contextWindow,
1340
+ inputTokens,
1341
+ outputTokens,
1342
+ breakdown: {
1343
+ input: inputTokens,
1344
+ output: outputTokens
1345
+ }
1219
1346
  });
1220
1347
  }
1221
1348
 
@@ -1258,8 +1385,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1258
1385
  const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
1259
1386
  const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
1260
1387
  let inputTokens = 0;
1261
- let cacheCreationTokens = 0;
1262
- let cacheReadTokens = 0;
1388
+ let outputTokens = 0;
1263
1389
 
1264
1390
  // Find the latest assistant message with usage data (scan from end)
1265
1391
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -1272,8 +1398,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1272
1398
 
1273
1399
  // Use token counts from latest assistant message only
1274
1400
  inputTokens = usage.input_tokens || 0;
1275
- cacheCreationTokens = usage.cache_creation_input_tokens || 0;
1276
- cacheReadTokens = usage.cache_read_input_tokens || 0;
1401
+ outputTokens = usage.output_tokens || 0;
1277
1402
 
1278
1403
  break; // Stop after finding the latest assistant message
1279
1404
  }
@@ -1283,16 +1408,16 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1283
1408
  }
1284
1409
  }
1285
1410
 
1286
- // Calculate total context usage (excluding output_tokens, as per ccusage)
1287
- const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
1411
+ const totalUsed = inputTokens + outputTokens;
1288
1412
 
1289
1413
  res.json({
1290
1414
  used: totalUsed,
1291
1415
  total: contextWindow,
1416
+ inputTokens,
1417
+ outputTokens,
1292
1418
  breakdown: {
1293
1419
  input: inputTokens,
1294
- cacheCreation: cacheCreationTokens,
1295
- cacheRead: cacheReadTokens
1420
+ output: outputTokens
1296
1421
  }
1297
1422
  });
1298
1423
  } 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';
@@ -33,6 +33,7 @@ type ProjectApiView = {
33
33
  cursorSessions: [];
34
34
  codexSessions: [];
35
35
  geminiSessions: [];
36
+ opencodeSessions: [];
36
37
  sessionMeta: {
37
38
  hasMore: false;
38
39
  total: 0;
@@ -84,6 +85,7 @@ function mapProjectRowToApiView(projectRow: ProjectRepositoryRow): ProjectApiVie
84
85
  cursorSessions: [],
85
86
  codexSessions: [],
86
87
  geminiSessions: [],
88
+ opencodeSessions: [],
87
89
  sessionMeta: {
88
90
  hasMore: false,
89
91
  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) {
@@ -253,6 +256,7 @@ export async function getProjectsWithSessions(
253
256
  cursorSessions: sessionsPage.sessionsByProvider.cursor,
254
257
  codexSessions: sessionsPage.sessionsByProvider.codex,
255
258
  geminiSessions: sessionsPage.sessionsByProvider.gemini,
259
+ opencodeSessions: sessionsPage.sessionsByProvider.opencode,
256
260
  sessionMeta: {
257
261
  hasMore: sessionsPage.hasMore,
258
262
  total: sessionsPage.total,
@@ -309,6 +313,7 @@ export async function getArchivedProjectsWithSessions(
309
313
  cursorSessions: sessionsPage.sessionsByProvider.cursor,
310
314
  codexSessions: sessionsPage.sessionsByProvider.codex,
311
315
  geminiSessions: sessionsPage.sessionsByProvider.gemini,
316
+ opencodeSessions: sessionsPage.sessionsByProvider.opencode,
312
317
  sessionMeta: {
313
318
  hasMore: sessionsPage.hasMore,
314
319
  total: sessionsPage.total,
@@ -341,6 +346,7 @@ export async function getProjectSessionsPage(
341
346
  cursorSessions: sessionsPage.sessionsByProvider.cursor,
342
347
  codexSessions: sessionsPage.sessionsByProvider.codex,
343
348
  geminiSessions: sessionsPage.sessionsByProvider.gemini,
349
+ opencodeSessions: sessionsPage.sessionsByProvider.opencode,
344
350
  sessionMeta: {
345
351
  hasMore: sessionsPage.hasMore,
346
352
  total: sessionsPage.total,