@cloudcli-ai/cloudcli 1.31.5 → 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 (252) 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-DqMVUeZS.js → index-skXSjBje.js} +290 -270
  10. package/dist/index.html +2 -2
  11. package/dist/modelConstants.js +841 -0
  12. package/dist-server/server/claude-sdk.js +67 -39
  13. package/dist-server/server/claude-sdk.js.map +1 -1
  14. package/dist-server/server/cursor-cli.js +6 -9
  15. package/dist-server/server/cursor-cli.js.map +1 -1
  16. package/dist-server/server/gemini-cli.js +163 -35
  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/database/migrations.js +11 -0
  25. package/dist-server/server/modules/database/migrations.js.map +1 -1
  26. package/dist-server/server/modules/database/repositories/projects.db.js +12 -0
  27. package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -1
  28. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js +61 -0
  29. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +1 -0
  30. package/dist-server/server/modules/database/repositories/sessions.db.js +49 -8
  31. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  32. package/dist-server/server/modules/database/schema.js +1 -0
  33. package/dist-server/server/modules/database/schema.js.map +1 -1
  34. package/dist-server/server/modules/projects/projects.routes.js +12 -3
  35. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  36. package/dist-server/server/modules/projects/services/project-delete.service.js +14 -1
  37. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  38. package/dist-server/server/modules/projects/services/project-management.service.js +1 -0
  39. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
  40. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +47 -0
  41. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  42. package/dist-server/server/modules/providers/index.js +1 -0
  43. package/dist-server/server/modules/providers/index.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +27 -7
  45. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  46. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +141 -0
  47. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
  48. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +55 -2
  49. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +153 -14
  51. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  52. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +198 -0
  53. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -0
  54. package/dist-server/server/modules/providers/list/claude/claude.provider.js +4 -0
  55. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  56. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +84 -0
  57. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
  58. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -2
  59. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  60. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +21 -6
  61. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  62. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +50 -0
  63. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -0
  64. package/dist-server/server/modules/providers/list/codex/codex.provider.js +4 -0
  65. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  66. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +754 -0
  67. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
  68. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +8 -30
  69. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  70. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +187 -24
  71. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  72. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +28 -0
  73. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -0
  74. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +4 -0
  75. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  76. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +125 -2
  77. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
  78. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +27 -0
  79. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -0
  80. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +24 -8
  81. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -1
  82. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +10 -10
  83. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  84. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +33 -0
  85. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -0
  86. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +4 -0
  87. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  88. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +92 -0
  89. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
  90. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
  91. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
  92. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +267 -0
  93. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
  94. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +115 -0
  95. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
  96. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +410 -0
  97. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
  98. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
  99. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
  100. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +19 -0
  101. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
  102. package/dist-server/server/modules/providers/provider.registry.js +2 -0
  103. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  104. package/dist-server/server/modules/providers/provider.routes.js +65 -3
  105. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  106. package/dist-server/server/modules/providers/services/mcp.service.js +1 -9
  107. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  108. package/dist-server/server/modules/providers/services/provider-models.service.js +199 -0
  109. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
  110. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +129 -13
  111. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  112. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
  113. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  114. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +13 -4
  115. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  116. package/dist-server/server/modules/providers/services/sessions.service.js +81 -7
  117. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  118. package/dist-server/server/modules/providers/services/skills.service.js +11 -0
  119. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -0
  120. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  121. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +45 -0
  122. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -0
  123. package/dist-server/server/modules/providers/tests/mcp.test.js +73 -6
  124. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  125. package/dist-server/server/modules/providers/tests/opencode-models.test.js +66 -0
  126. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
  127. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +262 -0
  128. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
  129. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +270 -0
  130. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
  131. package/dist-server/server/modules/providers/tests/skills.test.js +259 -0
  132. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -0
  133. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +12 -1
  134. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  135. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +4 -4
  136. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -1
  137. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -1
  138. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  139. package/dist-server/server/openai-codex.js +87 -29
  140. package/dist-server/server/openai-codex.js.map +1 -1
  141. package/dist-server/server/opencode-cli.js +287 -0
  142. package/dist-server/server/opencode-cli.js.map +1 -0
  143. package/dist-server/server/opencode-cli.test.js +84 -0
  144. package/dist-server/server/opencode-cli.test.js.map +1 -0
  145. package/dist-server/server/routes/agent.js +21 -8
  146. package/dist-server/server/routes/agent.js.map +1 -1
  147. package/dist-server/server/routes/commands.js +204 -211
  148. package/dist-server/server/routes/commands.js.map +1 -1
  149. package/dist-server/server/routes/cursor.js +2 -2
  150. package/dist-server/server/routes/cursor.js.map +1 -1
  151. package/dist-server/server/routes/settings.js +0 -10
  152. package/dist-server/server/routes/settings.js.map +1 -1
  153. package/dist-server/server/routes/tests/commands.test.js +76 -0
  154. package/dist-server/server/routes/tests/commands.test.js.map +1 -0
  155. package/dist-server/server/shared/claude-cli-path.js +103 -0
  156. package/dist-server/server/shared/claude-cli-path.js.map +1 -0
  157. package/dist-server/server/shared/claude-cli-path.test.js +45 -0
  158. package/dist-server/server/shared/claude-cli-path.test.js.map +1 -0
  159. package/dist-server/server/{utils → shared}/frontmatter.js +3 -3
  160. package/dist-server/server/shared/frontmatter.js.map +1 -0
  161. package/dist-server/server/shared/utils.js +368 -0
  162. package/dist-server/server/shared/utils.js.map +1 -1
  163. package/dist-server/server/utils/commandParser.js +3 -3
  164. package/dist-server/server/utils/commandParser.js.map +1 -1
  165. package/package.json +3 -1
  166. package/public/api-docs.html +878 -0
  167. package/public/modelConstants.js +841 -0
  168. package/server/claude-sdk.js +74 -40
  169. package/server/cursor-cli.js +6 -11
  170. package/server/gemini-cli.js +197 -43
  171. package/server/gemini-response-handler.js +38 -0
  172. package/server/index.js +144 -19
  173. package/server/modules/database/index.ts +1 -0
  174. package/server/modules/database/migrations.ts +12 -0
  175. package/server/modules/database/repositories/projects.db.ts +13 -0
  176. package/server/modules/database/repositories/sessions.db.integration.test.ts +72 -0
  177. package/server/modules/database/repositories/sessions.db.ts +60 -9
  178. package/server/modules/database/schema.ts +1 -0
  179. package/server/modules/projects/projects.routes.ts +20 -3
  180. package/server/modules/projects/services/project-delete.service.ts +16 -1
  181. package/server/modules/projects/services/project-management.service.ts +2 -0
  182. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +71 -1
  183. package/server/modules/providers/README.md +354 -0
  184. package/server/modules/providers/index.ts +2 -1
  185. package/server/modules/providers/list/claude/claude-auth.provider.ts +38 -13
  186. package/server/modules/providers/list/claude/claude-models.provider.ts +191 -0
  187. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +68 -2
  188. package/server/modules/providers/list/claude/claude-sessions.provider.ts +175 -14
  189. package/server/modules/providers/list/claude/claude-skills.provider.ts +257 -0
  190. package/server/modules/providers/list/claude/claude.provider.ts +11 -1
  191. package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
  192. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +62 -2
  193. package/server/modules/providers/list/codex/codex-sessions.provider.ts +25 -6
  194. package/server/modules/providers/list/codex/codex-skills.provider.ts +60 -0
  195. package/server/modules/providers/list/codex/codex.provider.ts +11 -1
  196. package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
  197. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +17 -40
  198. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +216 -26
  199. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +31 -0
  200. package/server/modules/providers/list/cursor/cursor.provider.ts +11 -1
  201. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +158 -2
  202. package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
  203. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +24 -20
  204. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +10 -11
  205. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +36 -0
  206. package/server/modules/providers/list/gemini/gemini.provider.ts +11 -1
  207. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
  208. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
  209. package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
  210. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +157 -0
  211. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
  212. package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
  213. package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
  214. package/server/modules/providers/provider.registry.ts +2 -0
  215. package/server/modules/providers/provider.routes.ts +97 -4
  216. package/server/modules/providers/services/mcp.service.ts +1 -12
  217. package/server/modules/providers/services/provider-models.service.ts +325 -0
  218. package/server/modules/providers/services/session-conversations-search.service.ts +162 -13
  219. package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
  220. package/server/modules/providers/services/sessions-watcher.service.ts +14 -4
  221. package/server/modules/providers/services/sessions.service.ts +112 -9
  222. package/server/modules/providers/services/skills.service.ts +15 -0
  223. package/server/modules/providers/shared/base/abstract.provider.ts +4 -0
  224. package/server/modules/providers/shared/skills/skills.provider.ts +64 -0
  225. package/server/modules/providers/tests/mcp.test.ts +93 -6
  226. package/server/modules/providers/tests/opencode-models.test.ts +73 -0
  227. package/server/modules/providers/tests/opencode-sessions.test.ts +334 -0
  228. package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
  229. package/server/modules/providers/tests/skills.test.ts +512 -0
  230. package/server/modules/websocket/services/chat-websocket.service.ts +15 -1
  231. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +4 -4
  232. package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
  233. package/server/openai-codex.js +99 -30
  234. package/server/opencode-cli.js +336 -0
  235. package/server/opencode-cli.test.js +95 -0
  236. package/server/routes/agent.js +22 -8
  237. package/server/routes/commands.js +256 -233
  238. package/server/routes/cursor.js +2 -2
  239. package/server/routes/settings.js +1 -10
  240. package/server/routes/tests/commands.test.js +82 -0
  241. package/server/shared/claude-cli-path.test.ts +61 -0
  242. package/server/shared/claude-cli-path.ts +139 -0
  243. package/server/{utils/frontmatter.js → shared/frontmatter.ts} +3 -3
  244. package/server/shared/interfaces.ts +64 -0
  245. package/server/shared/types.ts +166 -1
  246. package/server/shared/utils.ts +478 -0
  247. package/server/utils/commandParser.js +5 -3
  248. package/dist/assets/index-6rYG_mRo.css +0 -32
  249. package/dist-server/server/utils/frontmatter.js.map +0 -1
  250. package/dist-server/shared/modelConstants.js +0 -98
  251. package/dist-server/shared/modelConstants.js.map +0 -1
  252. package/shared/modelConstants.js +0 -106
@@ -17,7 +17,9 @@ 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';
22
+ import { resolveClaudeCodeExecutablePath } from './shared/claude-cli-path.js';
21
23
  import {
22
24
  createNotificationEvent,
23
25
  notifyRunFailed,
@@ -153,11 +155,9 @@ function mapCliOptionsToSDK(options = {}) {
153
155
  // Since SDK 0.2.113, options.env replaces process.env instead of overlaying it.
154
156
  sdkOptions.env = { ...process.env };
155
157
 
156
- // Use CLAUDE_CLI_PATH if explicitly set, otherwise fall back to 'claude' on PATH.
157
- // The SDK 0.2.113+ looks for a bundled native binary optional dep by default;
158
- // this fallback ensures users who installed via the official installer still work
159
- // even when npm prune --production has removed those optional deps.
160
- sdkOptions.pathToClaudeCodeExecutable = process.env.CLAUDE_CLI_PATH || 'claude';
158
+ // Resolve the executable eagerly on Windows because the SDK uses raw child_process.spawn,
159
+ // which does not reliably follow npm's shell wrappers like cross-spawn does.
160
+ sdkOptions.pathToClaudeCodeExecutable = resolveClaudeCodeExecutablePath(process.env.CLAUDE_CLI_PATH);
161
161
 
162
162
  // Map working directory
163
163
  if (cwd) {
@@ -205,7 +205,7 @@ function mapCliOptionsToSDK(options = {}) {
205
205
 
206
206
  // Map model (default to sonnet)
207
207
  // Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
208
- sdkOptions.model = options.model || CLAUDE_MODELS.DEFAULT;
208
+ sdkOptions.model = options.model || CLAUDE_FALLBACK_MODELS.DEFAULT;
209
209
  // Model logged at query start below
210
210
 
211
211
  // Map system prompt configuration
@@ -285,43 +285,68 @@ function transformMessage(sdkMessage) {
285
285
  return sdkMessage;
286
286
  }
287
287
 
288
+ function readNumber(value) {
289
+ const parsed = Number(value);
290
+ return Number.isFinite(parsed) ? parsed : 0;
291
+ }
292
+
288
293
  /**
289
- * Extracts token usage from SDK result messages
290
- * @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
291
298
  * @returns {Object|null} Token budget object or null
292
299
  */
293
- function extractTokenBudget(resultMessage) {
294
- if (resultMessage.type !== 'result' || !resultMessage.modelUsage) {
300
+ function extractTokenBudget(sdkMessage) {
301
+ if (!sdkMessage || typeof sdkMessage !== 'object') {
295
302
  return null;
296
303
  }
297
304
 
298
- // Get the first model's usage data
299
- const modelKey = Object.keys(resultMessage.modelUsage)[0];
300
- 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;
301
311
 
302
- if (!modelData) {
303
- return null;
312
+ return {
313
+ used: totalUsed,
314
+ total: contextWindow,
315
+ inputTokens,
316
+ outputTokens,
317
+ breakdown: {
318
+ input: inputTokens,
319
+ output: outputTokens,
320
+ },
321
+ };
304
322
  }
305
323
 
306
- // Use cumulative tokens if available (tracks total for the session)
307
- // Otherwise fall back to per-request tokens
308
- const inputTokens = modelData.cumulativeInputTokens || modelData.inputTokens || 0;
309
- const outputTokens = modelData.cumulativeOutputTokens || modelData.outputTokens || 0;
310
- const cacheReadTokens = modelData.cumulativeCacheReadInputTokens || modelData.cacheReadInputTokens || 0;
311
- const cacheCreationTokens = modelData.cumulativeCacheCreationInputTokens || modelData.cacheCreationInputTokens || 0;
324
+ if (!sdkMessage.modelUsage || typeof sdkMessage.modelUsage !== 'object') {
325
+ return null;
326
+ }
312
327
 
313
- // Total used = input + output + cache tokens
314
- 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];
315
331
 
316
- // Use configured context window budget from environment (default 160000)
317
- // This is the user's budget limit, not the model's context window
318
- const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000;
332
+ if (!modelData || typeof modelData !== 'object') {
333
+ return null;
334
+ }
319
335
 
320
- // 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;
321
340
 
322
341
  return {
323
342
  used: totalUsed,
324
- total: contextWindow
343
+ total: contextWindow,
344
+ inputTokens,
345
+ outputTokens,
346
+ breakdown: {
347
+ input: inputTokens,
348
+ output: outputTokens,
349
+ },
325
350
  };
326
351
  }
327
352
 
@@ -492,8 +517,17 @@ async function queryClaudeSDK(command, options = {}, ws) {
492
517
  };
493
518
 
494
519
  try {
520
+ const resolvedModel = await providerModelsService.resolveResumeModel(
521
+ 'claude',
522
+ sessionId,
523
+ options.model,
524
+ );
525
+
495
526
  // Map CLI options to SDK format
496
- const sdkOptions = mapCliOptionsToSDK(options);
527
+ const sdkOptions = mapCliOptionsToSDK({
528
+ ...options,
529
+ model: resolvedModel || options.model,
530
+ });
497
531
 
498
532
  // Load MCP configuration
499
533
  const mcpServers = await loadMcpConfig(options.cwd);
@@ -527,6 +561,12 @@ async function queryClaudeSDK(command, options = {}, ws) {
527
561
  }]
528
562
  };
529
563
 
564
+ // Caveat: in 'auto' and 'bypassPermissions' modes the SDK resolves approval
565
+ // at the permission-mode step and skips this callback, so interactive tools
566
+ // (AskUserQuestion, ExitPlanMode) won't reach the UI — the classifier/bypass
567
+ // auto-approves them and the model acts on a generated answer. Move these
568
+ // tools to a PreToolUse hook (runs before the mode check) if we need them
569
+ // to work in those modes.
530
570
  sdkOptions.canUseTool = async (toolName, input, context) => {
531
571
  const requiresInteraction = TOOLS_REQUIRING_INTERACTION.has(toolName);
532
572
 
@@ -669,16 +709,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
669
709
  ws.send(msg);
670
710
  }
671
711
 
672
- // Extract and send token budget updates from result messages
673
- if (message.type === 'result') {
674
- const models = Object.keys(message.modelUsage || {});
675
- if (models.length > 0) {
676
- // Model info available in result message
677
- }
678
- const tokenBudgetData = extractTokenBudget(message);
679
- if (tokenBudgetData) {
680
- ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
681
- }
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' }));
682
716
  }
683
717
  }
684
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
@@ -150,7 +153,6 @@ async function spawnCursor(command, options = {}, ws) {
150
153
 
151
154
  try {
152
155
  const response = JSON.parse(line);
153
- console.log('Parsed JSON response:', response);
154
156
 
155
157
  // Handle different message types
156
158
  switch (response.type) {
@@ -159,7 +161,6 @@ async function spawnCursor(command, options = {}, ws) {
159
161
  // Capture session ID
160
162
  if (response.session_id && !capturedSessionId) {
161
163
  capturedSessionId = response.session_id;
162
- console.log('Captured session ID:', capturedSessionId);
163
164
 
164
165
  // Update process key with captured session ID
165
166
  if (processKey !== capturedSessionId) {
@@ -197,7 +198,6 @@ async function spawnCursor(command, options = {}, ws) {
197
198
 
198
199
  case 'result': {
199
200
  // Session complete — send stream end + lifecycle complete with result payload
200
- console.log('Cursor session result:', response);
201
201
  const resultText = typeof response.result === 'string' ? response.result : '';
202
202
  ws.send(createNormalizedMessage({
203
203
  kind: 'complete',
@@ -213,8 +213,6 @@ async function spawnCursor(command, options = {}, ws) {
213
213
  // Unknown message types — ignore.
214
214
  }
215
215
  } catch (parseError) {
216
- console.log('Non-JSON response:', line);
217
-
218
216
  if (shouldSuppressForTrustRetry(line)) {
219
217
  return;
220
218
  }
@@ -228,7 +226,6 @@ async function spawnCursor(command, options = {}, ws) {
228
226
  // Handle stdout (streaming JSON responses)
229
227
  cursorProcess.stdout.on('data', (data) => {
230
228
  const rawOutput = data.toString();
231
- console.log('Cursor CLI stdout:', rawOutput);
232
229
 
233
230
  // Stream chunks can split JSON objects across packets; keep trailing partial line.
234
231
  stdoutLineBuffer += rawOutput;
@@ -254,8 +251,6 @@ async function spawnCursor(command, options = {}, ws) {
254
251
 
255
252
  // Handle process completion
256
253
  cursorProcess.on('close', async (code) => {
257
- console.log(`Cursor CLI process exited with code ${code}`);
258
-
259
254
  const finalSessionId = capturedSessionId || sessionId || processKey;
260
255
  activeCursorProcesses.delete(finalSessionId);
261
256
 
@@ -1,21 +1,131 @@
1
1
  import { spawn } from 'child_process';
2
- import crossSpawn from 'cross-spawn';
3
-
4
- // Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
5
- const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
6
2
  import { promises as fs } from 'fs';
7
- import path from 'path';
8
3
  import os from 'os';
4
+ import path from 'path';
5
+
6
+ import crossSpawn from 'cross-spawn';
7
+
9
8
  import sessionManager from './sessionManager.js';
10
9
  import GeminiResponseHandler from './gemini-response-handler.js';
11
10
  import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
12
11
  import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
12
+ import { providerModelsService } from './modules/providers/services/provider-models.service.js';
13
13
  import { createNormalizedMessage } from './shared/utils.js';
14
14
 
15
+ // Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
16
+ const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
17
+
15
18
  let activeGeminiProcesses = new Map(); // Track active processes by session ID
16
19
 
20
+ function mapGeminiExitCodeToMessage(exitCode) {
21
+ switch (exitCode) {
22
+ case 42:
23
+ return 'Gemini rejected the request input (exit code 42).';
24
+ case 44:
25
+ return 'Gemini sandbox error (exit code 44). Check local sandbox/container settings.';
26
+ case 52:
27
+ return 'Gemini configuration error (exit code 52). Check your Gemini settings files for invalid JSON/config.';
28
+ case 53:
29
+ return 'Gemini conversation turn limit reached (exit code 53). Start a new Gemini session.';
30
+ default:
31
+ return null;
32
+ }
33
+ }
34
+
35
+ const GEMINI_AUTH_ENV_KEYS = [
36
+ 'GEMINI_API_KEY',
37
+ 'GOOGLE_API_KEY',
38
+ 'GOOGLE_CLOUD_PROJECT',
39
+ 'GOOGLE_CLOUD_PROJECT_ID',
40
+ 'GOOGLE_CLOUD_LOCATION',
41
+ 'GOOGLE_APPLICATION_CREDENTIALS'
42
+ ];
43
+
44
+ function parseEnvFileContent(content) {
45
+ const parsed = {};
46
+
47
+ for (const rawLine of content.split(/\r?\n/)) {
48
+ const line = rawLine.trim();
49
+ if (!line || line.startsWith('#')) {
50
+ continue;
51
+ }
52
+
53
+ const exportPrefix = 'export ';
54
+ const normalizedLine = line.startsWith(exportPrefix) ? line.slice(exportPrefix.length).trim() : line;
55
+ const separatorIndex = normalizedLine.indexOf('=');
56
+
57
+ if (separatorIndex <= 0) {
58
+ continue;
59
+ }
60
+
61
+ const key = normalizedLine.slice(0, separatorIndex).trim();
62
+ if (!key) {
63
+ continue;
64
+ }
65
+
66
+ let value = normalizedLine.slice(separatorIndex + 1).trim();
67
+ const hasDoubleQuotes = value.startsWith('"') && value.endsWith('"');
68
+ const hasSingleQuotes = value.startsWith('\'') && value.endsWith('\'');
69
+
70
+ if (hasDoubleQuotes || hasSingleQuotes) {
71
+ value = value.slice(1, -1);
72
+ } else {
73
+ // Support inline comments in unquoted values: KEY=value # comment
74
+ value = value.replace(/\s+#.*$/, '').trim();
75
+ }
76
+
77
+ parsed[key] = value;
78
+ }
79
+
80
+ return parsed;
81
+ }
82
+
83
+ async function loadGeminiUserLevelEnv() {
84
+ const geminiCliHome = (process.env.GEMINI_CLI_HOME || '').trim() || os.homedir();
85
+ const envCandidates = [
86
+ path.join(geminiCliHome, '.gemini', '.env'),
87
+ path.join(geminiCliHome, '.env')
88
+ ];
89
+
90
+ for (const envPath of envCandidates) {
91
+ try {
92
+ await fs.access(envPath);
93
+ const content = await fs.readFile(envPath, 'utf8');
94
+ return parseEnvFileContent(content);
95
+ } catch {
96
+ // Keep scanning for the next candidate.
97
+ }
98
+ }
99
+
100
+ return {};
101
+ }
102
+
103
+ async function buildGeminiProcessEnv() {
104
+ const processEnv = { ...process.env };
105
+ if (processEnv.GEMINI_API_KEY || processEnv.GOOGLE_API_KEY || processEnv.GOOGLE_APPLICATION_CREDENTIALS) {
106
+ return processEnv;
107
+ }
108
+
109
+ // Gemini CLI docs recommend ~/.gemini/.env for persistent headless auth settings.
110
+ // When the server process was launched without shell profile variables, we still
111
+ // want the spawned CLI process to inherit those user-level credentials.
112
+ const userEnv = await loadGeminiUserLevelEnv();
113
+ for (const key of GEMINI_AUTH_ENV_KEYS) {
114
+ if (!processEnv[key] && userEnv[key]) {
115
+ processEnv[key] = userEnv[key];
116
+ }
117
+ }
118
+
119
+ return processEnv;
120
+ }
121
+
17
122
  async function spawnGemini(command, options = {}, ws) {
18
123
  const { sessionId, projectPath, cwd, toolsSettings, permissionMode, images, sessionSummary } = options;
124
+ const resolvedModel = await providerModelsService.resolveResumeModel(
125
+ 'gemini',
126
+ sessionId,
127
+ options.model
128
+ );
19
129
  let capturedSessionId = sessionId; // Track session ID throughout the process
20
130
  let sessionCreatedSent = false; // Track if we've already sent session-created event
21
131
  let assistantBlocks = []; // Accumulate the full response blocks including tools
@@ -100,6 +210,11 @@ async function spawnGemini(command, options = {}, ws) {
100
210
  args.push('--debug');
101
211
  }
102
212
 
213
+ // This integration runs Gemini in headless mode and cannot answer trust prompts.
214
+ // Skip folder-trust interactivity so authenticated runs don't fail with
215
+ // FatalUntrustedWorkspaceError in previously unseen directories.
216
+ args.push('--skip-trust');
217
+
103
218
  // Add MCP config flag only if MCP servers are configured
104
219
  try {
105
220
  const geminiConfigPath = path.join(os.homedir(), '.gemini.json');
@@ -135,7 +250,7 @@ async function spawnGemini(command, options = {}, ws) {
135
250
  }
136
251
 
137
252
  // Add model for all sessions (both new and resumed)
138
- let modelToUse = options.model || 'gemini-2.5-flash';
253
+ let modelToUse = resolvedModel || 'gemini-2.5-flash';
139
254
  args.push('--model', modelToUse);
140
255
  args.push('--output-format', 'stream-json');
141
256
 
@@ -154,9 +269,6 @@ async function spawnGemini(command, options = {}, ws) {
154
269
 
155
270
  // Try to find gemini in PATH first, then fall back to environment variable
156
271
  const geminiPath = process.env.GEMINI_PATH || 'gemini';
157
- console.log('Spawning Gemini CLI:', geminiPath, args.join(' '));
158
- console.log('Working directory:', workingDir);
159
-
160
272
  let spawnCmd = geminiPath;
161
273
  let spawnArgs = args;
162
274
 
@@ -168,11 +280,13 @@ async function spawnGemini(command, options = {}, ws) {
168
280
  spawnArgs = ['-c', 'exec "$0" "$@"', geminiPath, ...args];
169
281
  }
170
282
 
283
+ const spawnEnv = await buildGeminiProcessEnv();
284
+
171
285
  return new Promise((resolve, reject) => {
172
286
  const geminiProcess = spawnFunction(spawnCmd, spawnArgs, {
173
287
  cwd: workingDir,
174
288
  stdio: ['pipe', 'pipe', 'pipe'],
175
- env: { ...process.env } // Inherit all environment variables
289
+ env: spawnEnv
176
290
  });
177
291
  let terminalNotificationSent = false;
178
292
  let terminalFailureReason = null;
@@ -276,13 +390,44 @@ async function spawnGemini(command, options = {}, ws) {
276
390
  }
277
391
  },
278
392
  onInit: (event) => {
279
- if (capturedSessionId) {
280
- const sess = sessionManager.getSession(capturedSessionId);
281
- if (sess && !sess.cliSessionId) {
282
- sess.cliSessionId = event.session_id;
283
- sessionManager.saveSession(capturedSessionId);
393
+ const discoveredSessionId = event?.session_id;
394
+ if (!discoveredSessionId) {
395
+ return;
396
+ }
397
+
398
+ // New Gemini sessions announce their canonical ID asynchronously via the
399
+ // initial `init` stream event. Avoid synthetic IDs and only register
400
+ // the session once that real ID is known (same model used by Claude/Codex).
401
+ if (!capturedSessionId) {
402
+ capturedSessionId = discoveredSessionId;
403
+
404
+ sessionManager.createSession(capturedSessionId, cwd || process.cwd());
405
+ if (command) {
406
+ sessionManager.addMessage(capturedSessionId, 'user', command);
407
+ }
408
+
409
+ if (processKey !== capturedSessionId) {
410
+ activeGeminiProcesses.delete(processKey);
411
+ activeGeminiProcesses.set(capturedSessionId, geminiProcess);
412
+ }
413
+
414
+ geminiProcess.sessionId = capturedSessionId;
415
+
416
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
417
+ ws.setSessionId(capturedSessionId);
418
+ }
419
+
420
+ if (!sessionId && !sessionCreatedSent) {
421
+ sessionCreatedSent = true;
422
+ ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'gemini' }));
284
423
  }
285
424
  }
425
+
426
+ const sess = sessionManager.getSession(capturedSessionId);
427
+ if (sess && !sess.cliSessionId) {
428
+ sess.cliSessionId = discoveredSessionId;
429
+ sessionManager.saveSession(capturedSessionId);
430
+ }
286
431
  }
287
432
  });
288
433
  }
@@ -292,30 +437,6 @@ async function spawnGemini(command, options = {}, ws) {
292
437
  const rawOutput = data.toString();
293
438
  startTimeout(); // Re-arm the timeout
294
439
 
295
- // For new sessions, create a session ID FIRST
296
- if (!sessionId && !sessionCreatedSent && !capturedSessionId) {
297
- capturedSessionId = `gemini_${Date.now()}`;
298
- sessionCreatedSent = true;
299
-
300
- // Create session in session manager
301
- sessionManager.createSession(capturedSessionId, cwd || process.cwd());
302
-
303
- // Save the user message now that we have a session ID
304
- if (command) {
305
- sessionManager.addMessage(capturedSessionId, 'user', command);
306
- }
307
-
308
- // Update process key with captured session ID
309
- if (processKey !== capturedSessionId) {
310
- activeGeminiProcesses.delete(processKey);
311
- activeGeminiProcesses.set(capturedSessionId, geminiProcess);
312
- }
313
-
314
- ws.setSessionId && typeof ws.setSessionId === 'function' && ws.setSessionId(capturedSessionId);
315
-
316
- ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'gemini' }));
317
- }
318
-
319
440
  if (responseHandler) {
320
441
  responseHandler.processData(rawOutput);
321
442
  } else if (rawOutput) {
@@ -381,12 +502,38 @@ async function spawnGemini(command, options = {}, ws) {
381
502
  notifyTerminalState({ code });
382
503
  resolve();
383
504
  } else {
384
- // code 127 = shell "command not found" check installation
505
+ const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
506
+
507
+ // code 127 = shell "command not found" - check installation
385
508
  if (code === 127) {
386
509
  const installed = await providerAuthService.isProviderInstalled('gemini');
387
510
  if (!installed) {
388
- const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
389
- ws.send(createNormalizedMessage({ kind: 'error', content: 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli', sessionId: socketSessionId, provider: 'gemini' }));
511
+ terminalFailureReason = 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli';
512
+ ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' }));
513
+ }
514
+ } else if (code === 41) {
515
+ // Gemini CLI documents exit code 41 as FatalAuthenticationError.
516
+ // Surface an actionable auth error instead of a generic exit-code message.
517
+ let authErrorSuffix = '';
518
+ try {
519
+ const authStatus = await providerAuthService.getProviderAuthStatus('gemini');
520
+ if (!authStatus?.authenticated && authStatus?.error) {
521
+ authErrorSuffix = ` Details: ${authStatus.error}`;
522
+ }
523
+ } catch {
524
+ // Keep base remediation text when auth status lookup fails.
525
+ }
526
+
527
+ terminalFailureReason =
528
+ 'Gemini authentication failed (exit code 41). '
529
+ + 'Run `gemini` in a terminal to choose an auth method, or configure a valid `GEMINI_API_KEY`.'
530
+ + authErrorSuffix;
531
+ ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' }));
532
+ } else {
533
+ const mappedError = mapGeminiExitCodeToMessage(code);
534
+ if (mappedError) {
535
+ terminalFailureReason = mappedError;
536
+ ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' }));
390
537
  }
391
538
  }
392
539
 
@@ -394,7 +541,14 @@ async function spawnGemini(command, options = {}, ws) {
394
541
  code,
395
542
  error: code === null ? 'Gemini CLI process was terminated or timed out' : null
396
543
  });
397
- reject(new Error(code === null ? 'Gemini CLI process was terminated or timed out' : `Gemini CLI exited with code ${code}`));
544
+ reject(
545
+ new Error(
546
+ terminalFailureReason
547
+ || (code === null
548
+ ? 'Gemini CLI process was terminated or timed out'
549
+ : `Gemini CLI exited with code ${code}`)
550
+ )
551
+ );
398
552
  }
399
553
  });
400
554
 
@@ -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() {