@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,11 +17,40 @@ import { Codex } from '@openai/codex-sdk';
17
17
  import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
18
18
  import { sessionsService } from './modules/providers/services/sessions.service.js';
19
19
  import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
20
+ import { providerModelsService } from './modules/providers/services/provider-models.service.js';
20
21
  import { createNormalizedMessage } from './shared/utils.js';
21
22
 
22
23
  // Track active sessions
23
24
  const activeCodexSessions = new Map();
24
25
 
26
+ function readUsageNumber(value) {
27
+ const parsed = Number(value);
28
+ return Number.isFinite(parsed) ? parsed : 0;
29
+ }
30
+
31
+ function extractCodexTokenBudget(event) {
32
+ const info = event?.info || event?.payload?.info || event?.usage?.info;
33
+ const usage = info?.total_token_usage || event?.usage?.total_token_usage || event?.usage;
34
+ if (!usage || typeof usage !== 'object') {
35
+ return null;
36
+ }
37
+
38
+ const inputTokens = readUsageNumber(usage.input_tokens);
39
+ const outputTokens = readUsageNumber(usage.output_tokens);
40
+ const used = readUsageNumber(usage.total_tokens) || inputTokens + outputTokens;
41
+
42
+ return {
43
+ used,
44
+ total: readUsageNumber(info?.model_context_window || event?.usage?.model_context_window) || 200000,
45
+ inputTokens,
46
+ outputTokens,
47
+ breakdown: {
48
+ input: inputTokens,
49
+ output: outputTokens,
50
+ },
51
+ };
52
+ }
53
+
25
54
  /**
26
55
  * Transform Codex SDK event to WebSocket message format
27
56
  * @param {object} event - SDK event
@@ -143,7 +172,7 @@ function transformCodexEvent(event) {
143
172
  case 'thread.started':
144
173
  return {
145
174
  type: 'thread_started',
146
- threadId: event.id
175
+ threadId: event.thread_id || event.id
147
176
  };
148
177
 
149
178
  case 'error':
@@ -202,12 +231,19 @@ export async function queryCodex(command, options = {}, ws) {
202
231
  permissionMode = 'default'
203
232
  } = options;
204
233
 
234
+ const resolvedModel = await providerModelsService.resolveResumeModel(
235
+ 'codex',
236
+ sessionId,
237
+ model,
238
+ );
239
+
205
240
  const workingDirectory = cwd || projectPath || process.cwd();
206
241
  const { sandboxMode, approvalPolicy } = mapPermissionModeToCodexOptions(permissionMode);
207
242
 
208
243
  let codex;
209
244
  let thread;
210
- let currentSessionId = sessionId;
245
+ let capturedSessionId = sessionId;
246
+ let sessionCreatedSent = false;
211
247
  let terminalFailure = null;
212
248
  const abortController = new AbortController();
213
249
 
@@ -221,7 +257,7 @@ export async function queryCodex(command, options = {}, ws) {
221
257
  skipGitRepoCheck: true,
222
258
  sandboxMode,
223
259
  approvalPolicy,
224
- model
260
+ model: resolvedModel
225
261
  };
226
262
 
227
263
  // Start or resume thread
@@ -231,20 +267,23 @@ export async function queryCodex(command, options = {}, ws) {
231
267
  thread = codex.startThread(threadOptions);
232
268
  }
233
269
 
234
- // Get the thread ID
235
- currentSessionId = thread.id || sessionId || `codex-${Date.now()}`;
236
-
237
- // Track the session
238
- activeCodexSessions.set(currentSessionId, {
239
- thread,
240
- codex,
241
- status: 'running',
242
- abortController,
243
- startedAt: new Date().toISOString()
244
- });
270
+ const registerSession = (id) => {
271
+ if (!id) {
272
+ return;
273
+ }
274
+ activeCodexSessions.set(id, {
275
+ thread,
276
+ codex,
277
+ status: 'running',
278
+ abortController,
279
+ startedAt: new Date().toISOString()
280
+ });
281
+ };
245
282
 
246
- // Send session created event
247
- sendMessage(ws, createNormalizedMessage({ kind: 'session_created', newSessionId: currentSessionId, sessionId: currentSessionId, provider: 'codex' }));
283
+ // Existing sessions can be tracked immediately; new sessions are tracked after thread.started.
284
+ if (capturedSessionId) {
285
+ registerSession(capturedSessionId);
286
+ }
248
287
 
249
288
  // Execute with streaming
250
289
  const streamedTurn = await thread.runStreamed(command, {
@@ -252,11 +291,34 @@ export async function queryCodex(command, options = {}, ws) {
252
291
  });
253
292
 
254
293
  for await (const event of streamedTurn.events) {
294
+ // Capture thread/session id lazily from the stream (Codex emits this asynchronously).
295
+ if (event.type === 'thread.started') {
296
+ const discoveredSessionId = event.thread_id || event.id || null;
297
+ if (discoveredSessionId && !capturedSessionId) {
298
+ capturedSessionId = discoveredSessionId;
299
+ registerSession(capturedSessionId);
300
+
301
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
302
+ ws.setSessionId(capturedSessionId);
303
+ }
304
+
305
+ if (!sessionId && !sessionCreatedSent) {
306
+ sessionCreatedSent = true;
307
+ sendMessage(ws, createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'codex' }));
308
+ }
309
+ }
310
+ }
311
+
255
312
  // Check if session was aborted
256
- const session = activeCodexSessions.get(currentSessionId);
257
- if (!session || session.status === 'aborted') {
313
+ if (abortController.signal.aborted) {
258
314
  break;
259
315
  }
316
+ if (capturedSessionId) {
317
+ const session = activeCodexSessions.get(capturedSessionId);
318
+ if (session?.status === 'aborted') {
319
+ break;
320
+ }
321
+ }
260
322
 
261
323
  if (event.type === 'item.started' || event.type === 'item.updated') {
262
324
  continue;
@@ -265,7 +327,7 @@ export async function queryCodex(command, options = {}, ws) {
265
327
  const transformed = transformCodexEvent(event);
266
328
 
267
329
  // Normalize the transformed event into NormalizedMessage(s) via adapter
268
- const normalizedMsgs = sessionsService.normalizeMessage('codex', transformed, currentSessionId);
330
+ const normalizedMsgs = sessionsService.normalizeMessage('codex', transformed, capturedSessionId || sessionId || null);
269
331
  for (const msg of normalizedMsgs) {
270
332
  sendMessage(ws, msg);
271
333
  }
@@ -275,33 +337,40 @@ export async function queryCodex(command, options = {}, ws) {
275
337
  notifyRunFailed({
276
338
  userId: ws?.userId || null,
277
339
  provider: 'codex',
278
- sessionId: currentSessionId,
340
+ sessionId: capturedSessionId || sessionId || null,
279
341
  sessionName: sessionSummary,
280
342
  error: terminalFailure
281
343
  });
282
344
  }
283
345
 
284
346
  // Extract and send token usage if available (normalized to match Claude format)
285
- if (event.type === 'turn.completed' && event.usage) {
286
- const totalTokens = (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0);
287
- sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: { used: totalTokens, total: 200000 }, sessionId: currentSessionId, provider: 'codex' }));
347
+ if (event.type === 'turn.completed') {
348
+ const tokenBudget = extractCodexTokenBudget(event);
349
+ if (tokenBudget) {
350
+ sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget, sessionId: capturedSessionId || sessionId || null, provider: 'codex' }));
351
+ }
288
352
  }
289
353
  }
290
354
 
291
355
  // Send completion event
292
356
  if (!terminalFailure) {
293
- sendMessage(ws, createNormalizedMessage({ kind: 'complete', actualSessionId: thread.id, sessionId: currentSessionId, provider: 'codex' }));
357
+ sendMessage(ws, createNormalizedMessage({
358
+ kind: 'complete',
359
+ actualSessionId: capturedSessionId || thread.id || sessionId || null,
360
+ sessionId: capturedSessionId || sessionId || null,
361
+ provider: 'codex'
362
+ }));
294
363
  notifyRunStopped({
295
364
  userId: ws?.userId || null,
296
365
  provider: 'codex',
297
- sessionId: currentSessionId,
366
+ sessionId: capturedSessionId || sessionId || null,
298
367
  sessionName: sessionSummary,
299
368
  stopReason: 'completed'
300
369
  });
301
370
  }
302
371
 
303
372
  } catch (error) {
304
- const session = currentSessionId ? activeCodexSessions.get(currentSessionId) : null;
373
+ const session = capturedSessionId ? activeCodexSessions.get(capturedSessionId) : null;
305
374
  const wasAborted =
306
375
  session?.status === 'aborted' ||
307
376
  error?.name === 'AbortError' ||
@@ -316,12 +385,12 @@ export async function queryCodex(command, options = {}, ws) {
316
385
  ? 'Codex CLI is not configured. Please set up authentication first.'
317
386
  : error.message;
318
387
 
319
- sendMessage(ws, createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: currentSessionId, provider: 'codex' }));
388
+ sendMessage(ws, createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'codex' }));
320
389
  if (!terminalFailure) {
321
390
  notifyRunFailed({
322
391
  userId: ws?.userId || null,
323
392
  provider: 'codex',
324
- sessionId: currentSessionId,
393
+ sessionId: capturedSessionId || sessionId || null,
325
394
  sessionName: sessionSummary,
326
395
  error
327
396
  });
@@ -330,8 +399,8 @@ export async function queryCodex(command, options = {}, ws) {
330
399
 
331
400
  } finally {
332
401
  // Update session status
333
- if (currentSessionId) {
334
- const session = activeCodexSessions.get(currentSessionId);
402
+ if (capturedSessionId) {
403
+ const session = activeCodexSessions.get(capturedSessionId);
335
404
  if (session) {
336
405
  session.status = session.status === 'aborted' ? 'aborted' : 'completed';
337
406
  }
@@ -0,0 +1,336 @@
1
+ import { spawn } from 'child_process';
2
+ import fsSync from 'node:fs';
3
+
4
+ import crossSpawn from 'cross-spawn';
5
+ import Database from 'better-sqlite3';
6
+
7
+ import { sessionsService } from './modules/providers/services/sessions.service.js';
8
+ import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
9
+ import { providerModelsService } from './modules/providers/services/provider-models.service.js';
10
+ import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
11
+ import { createNormalizedMessage, getOpenCodeDatabasePath } from './shared/utils.js';
12
+
13
+ const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
14
+
15
+ const activeOpenCodeProcesses = new Map();
16
+
17
+ function readOpenCodeSessionId(event) {
18
+ if (!event || typeof event !== 'object') {
19
+ return null;
20
+ }
21
+
22
+ return event.sessionID || event.sessionId || null;
23
+ }
24
+
25
+ function readOpenCodeTokenUsage(sessionId) {
26
+ const dbPath = getOpenCodeDatabasePath();
27
+ if (!sessionId || !fsSync.existsSync(dbPath)) {
28
+ return null;
29
+ }
30
+
31
+ let db = null;
32
+ try {
33
+ db = new Database(dbPath, { readonly: true, fileMustExist: true });
34
+ const columns = db.prepare('PRAGMA table_info(session)').all();
35
+ const columnNames = new Set(columns.map((column) => column.name));
36
+ const requiredColumns = ['tokens_input', 'tokens_output', 'tokens_reasoning', 'tokens_cache_read', 'tokens_cache_write'];
37
+ if (!requiredColumns.every((column) => columnNames.has(column))) {
38
+ return null;
39
+ }
40
+
41
+ const row = db.prepare(`
42
+ SELECT
43
+ tokens_input AS inputTokens,
44
+ tokens_output AS outputTokens,
45
+ tokens_reasoning AS reasoningTokens,
46
+ tokens_cache_read AS cacheReadTokens,
47
+ tokens_cache_write AS cacheWriteTokens
48
+ FROM session
49
+ WHERE id = ?
50
+ `).get(sessionId);
51
+
52
+ if (!row) {
53
+ return null;
54
+ }
55
+
56
+ const inputTokens = Number(row.inputTokens || 0) + Number(row.cacheReadTokens || 0);
57
+ const outputTokens = Number(row.outputTokens || 0);
58
+ const used = Number(row.inputTokens || 0)
59
+ + outputTokens
60
+ + Number(row.reasoningTokens || 0)
61
+ + Number(row.cacheReadTokens || 0)
62
+ + Number(row.cacheWriteTokens || 0);
63
+ if (used <= 0) {
64
+ return null;
65
+ }
66
+
67
+ return {
68
+ used,
69
+ inputTokens,
70
+ outputTokens,
71
+ breakdown: {
72
+ input: inputTokens,
73
+ output: outputTokens,
74
+ },
75
+ };
76
+ } catch {
77
+ return null;
78
+ } finally {
79
+ if (db) {
80
+ db.close();
81
+ }
82
+ }
83
+ }
84
+
85
+ async function spawnOpenCode(command, options = {}, ws) {
86
+ return new Promise((resolve, reject) => {
87
+ const { sessionId, projectPath, cwd, model, sessionSummary } = options;
88
+ const workingDir = cwd || projectPath || process.cwd();
89
+ const processKey = sessionId || Date.now().toString();
90
+ let capturedSessionId = sessionId || null;
91
+ let sessionCreatedSent = false;
92
+ let stdoutLineBuffer = '';
93
+ let terminalNotificationSent = false;
94
+ let opencodeProcess = null;
95
+
96
+ const notifyTerminalState = ({ code = null, error = null } = {}) => {
97
+ if (terminalNotificationSent) {
98
+ return;
99
+ }
100
+
101
+ terminalNotificationSent = true;
102
+ const finalSessionId = capturedSessionId || sessionId || processKey;
103
+ if (code === 0 && !error) {
104
+ notifyRunStopped({
105
+ userId: ws?.userId || null,
106
+ provider: 'opencode',
107
+ sessionId: finalSessionId,
108
+ sessionName: sessionSummary,
109
+ stopReason: 'completed',
110
+ });
111
+ return;
112
+ }
113
+
114
+ notifyRunFailed({
115
+ userId: ws?.userId || null,
116
+ provider: 'opencode',
117
+ sessionId: finalSessionId,
118
+ sessionName: sessionSummary,
119
+ error: error || `OpenCode CLI exited with code ${code}`,
120
+ });
121
+ };
122
+
123
+ const registerSession = (nextSessionId) => {
124
+ if (!nextSessionId || capturedSessionId === nextSessionId) {
125
+ return;
126
+ }
127
+
128
+ capturedSessionId = nextSessionId;
129
+ if (processKey !== capturedSessionId && opencodeProcess) {
130
+ activeOpenCodeProcesses.delete(processKey);
131
+ activeOpenCodeProcesses.set(capturedSessionId, opencodeProcess);
132
+ }
133
+ if (opencodeProcess) {
134
+ opencodeProcess.sessionId = capturedSessionId;
135
+ }
136
+
137
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
138
+ ws.setSessionId(capturedSessionId);
139
+ }
140
+
141
+ if (!sessionId && !sessionCreatedSent) {
142
+ sessionCreatedSent = true;
143
+ ws.send(createNormalizedMessage({
144
+ kind: 'session_created',
145
+ newSessionId: capturedSessionId,
146
+ sessionId: capturedSessionId,
147
+ provider: 'opencode',
148
+ }));
149
+ }
150
+ };
151
+
152
+ const processOpenCodeOutputLine = (line) => {
153
+ if (!line || !line.trim()) {
154
+ return;
155
+ }
156
+
157
+ let response;
158
+ try {
159
+ response = JSON.parse(line);
160
+ } catch {
161
+ ws.send(createNormalizedMessage({
162
+ kind: 'stream_delta',
163
+ content: line,
164
+ sessionId: capturedSessionId || sessionId || null,
165
+ provider: 'opencode',
166
+ }));
167
+ return;
168
+ }
169
+
170
+ try {
171
+ registerSession(readOpenCodeSessionId(response));
172
+ const normalized = sessionsService.normalizeMessage(
173
+ 'opencode',
174
+ response,
175
+ capturedSessionId || sessionId || null,
176
+ );
177
+ for (const msg of normalized) {
178
+ ws.send(msg);
179
+ }
180
+ } catch (error) {
181
+ const errorContent = error instanceof Error ? error.message : String(error);
182
+ console.error('[OpenCode] Failed to process JSON output:', errorContent);
183
+ ws.send(createNormalizedMessage({
184
+ kind: 'error',
185
+ content: errorContent,
186
+ sessionId: capturedSessionId || sessionId || null,
187
+ provider: 'opencode',
188
+ }));
189
+ }
190
+ };
191
+
192
+ void providerModelsService.resolveResumeModel('opencode', sessionId, model).then((resolvedModel) => {
193
+ const args = ['run', '--format', 'json'];
194
+ if (sessionId) {
195
+ args.push('--session', sessionId);
196
+ }
197
+ if (resolvedModel) {
198
+ args.push('--model', resolvedModel);
199
+ }
200
+ if (command && command.trim()) {
201
+ args.push(command.trim());
202
+ }
203
+
204
+ opencodeProcess = spawnFunction('opencode', args, {
205
+ cwd: workingDir,
206
+ stdio: ['pipe', 'pipe', 'pipe'],
207
+ env: { ...process.env },
208
+ });
209
+
210
+ activeOpenCodeProcesses.set(processKey, opencodeProcess);
211
+ opencodeProcess.sessionId = processKey;
212
+ opencodeProcess.stdin.end();
213
+
214
+ opencodeProcess.stdout.on('data', (data) => {
215
+ stdoutLineBuffer += data.toString();
216
+ const completeLines = stdoutLineBuffer.split(/\r?\n/);
217
+ stdoutLineBuffer = completeLines.pop() || '';
218
+
219
+ completeLines.forEach((line) => {
220
+ processOpenCodeOutputLine(line.trim());
221
+ });
222
+ });
223
+
224
+ opencodeProcess.stderr.on('data', (data) => {
225
+ const stderrText = data.toString();
226
+ if (!stderrText.trim()) {
227
+ return;
228
+ }
229
+
230
+ ws.send(createNormalizedMessage({
231
+ kind: 'error',
232
+ content: stderrText,
233
+ sessionId: capturedSessionId || sessionId || null,
234
+ provider: 'opencode',
235
+ }));
236
+ });
237
+
238
+ opencodeProcess.on('close', async (code) => {
239
+ const finalSessionId = capturedSessionId || sessionId || processKey;
240
+ activeOpenCodeProcesses.delete(finalSessionId);
241
+ activeOpenCodeProcesses.delete(processKey);
242
+
243
+ if (stdoutLineBuffer.trim()) {
244
+ processOpenCodeOutputLine(stdoutLineBuffer.trim());
245
+ stdoutLineBuffer = '';
246
+ }
247
+
248
+ const tokenBudget = readOpenCodeTokenUsage(finalSessionId);
249
+ if (tokenBudget) {
250
+ ws.send(createNormalizedMessage({
251
+ kind: 'status',
252
+ text: 'token_budget',
253
+ tokenBudget,
254
+ sessionId: finalSessionId,
255
+ provider: 'opencode',
256
+ }));
257
+ }
258
+
259
+ ws.send(createNormalizedMessage({
260
+ kind: 'complete',
261
+ exitCode: code,
262
+ isNewSession: !sessionId && !!command,
263
+ sessionId: finalSessionId,
264
+ provider: 'opencode',
265
+ }));
266
+
267
+ if (code === 0) {
268
+ notifyTerminalState({ code });
269
+ resolve();
270
+ return;
271
+ }
272
+
273
+ if (code === 127 || code === null) {
274
+ const installed = await providerAuthService.isProviderInstalled('opencode');
275
+ if (!installed) {
276
+ ws.send(createNormalizedMessage({
277
+ kind: 'error',
278
+ content: 'OpenCode CLI is not installed. Install it from https://opencode.ai/docs/',
279
+ sessionId: finalSessionId,
280
+ provider: 'opencode',
281
+ }));
282
+ }
283
+ }
284
+
285
+ notifyTerminalState({ code });
286
+ reject(new Error(code === null ? 'OpenCode CLI process was terminated' : `OpenCode CLI exited with code ${code}`));
287
+ });
288
+
289
+ opencodeProcess.on('error', async (error) => {
290
+ const finalSessionId = capturedSessionId || sessionId || processKey;
291
+ activeOpenCodeProcesses.delete(finalSessionId);
292
+ activeOpenCodeProcesses.delete(processKey);
293
+
294
+ const installed = await providerAuthService.isProviderInstalled('opencode');
295
+ const errorContent = !installed
296
+ ? 'OpenCode CLI is not installed. Install it from https://opencode.ai/docs/'
297
+ : error.message;
298
+
299
+ ws.send(createNormalizedMessage({
300
+ kind: 'error',
301
+ content: errorContent,
302
+ sessionId: finalSessionId,
303
+ provider: 'opencode',
304
+ }));
305
+ notifyTerminalState({ error });
306
+ reject(error);
307
+ });
308
+ }).catch(reject);
309
+ });
310
+ }
311
+
312
+ function abortOpenCodeSession(sessionId) {
313
+ const process = activeOpenCodeProcesses.get(sessionId);
314
+ if (!process) {
315
+ return false;
316
+ }
317
+
318
+ process.kill('SIGTERM');
319
+ activeOpenCodeProcesses.delete(sessionId);
320
+ return true;
321
+ }
322
+
323
+ function isOpenCodeSessionActive(sessionId) {
324
+ return activeOpenCodeProcesses.has(sessionId);
325
+ }
326
+
327
+ function getActiveOpenCodeSessions() {
328
+ return Array.from(activeOpenCodeProcesses.keys());
329
+ }
330
+
331
+ export {
332
+ spawnOpenCode,
333
+ abortOpenCodeSession,
334
+ isOpenCodeSessionActive,
335
+ getActiveOpenCodeSessions,
336
+ };
@@ -0,0 +1,95 @@
1
+ import assert from 'node:assert/strict';
2
+ import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { spawnOpenCode } from './opencode-cli.js';
8
+
9
+ const findEnvKey = (name) =>
10
+ Object.keys(process.env).find((key) => key.toLowerCase() === name.toLowerCase()) || name;
11
+
12
+ async function createFakeOpenCodeExecutable(binDir) {
13
+ const scriptPath = path.join(binDir, 'opencode.js');
14
+ await writeFile(scriptPath, `
15
+ const events = [
16
+ { type: 'text', sessionID: 'open-live-1', text: 'assistant response' },
17
+ { type: 'step_finish', sessionID: 'open-live-1' },
18
+ ];
19
+
20
+ for (const event of events) {
21
+ console.log(JSON.stringify(event));
22
+ }
23
+ `, 'utf8');
24
+
25
+ if (process.platform === 'win32') {
26
+ const commandPath = path.join(binDir, 'opencode.cmd');
27
+ await writeFile(commandPath, '@echo off\r\nnode "%~dp0opencode.js" %*\r\n', 'utf8');
28
+ return;
29
+ }
30
+
31
+ const commandPath = path.join(binDir, 'opencode');
32
+ await writeFile(commandPath, '#!/bin/sh\nnode "$(dirname "$0")/opencode.js" "$@"\n', 'utf8');
33
+ await chmod(commandPath, 0o755);
34
+ }
35
+
36
+ test('spawnOpenCode emits session_created before normalized live messages for new sessions', async () => {
37
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-cli-live-'));
38
+ const pathKey = findEnvKey('PATH');
39
+ const pathExtKey = findEnvKey('PATHEXT');
40
+ const previousPath = process.env[pathKey];
41
+ const previousPathExt = process.env[pathExtKey];
42
+ const messages = [];
43
+ const writer = {
44
+ userId: null,
45
+ sessionId: null,
46
+ send(message) {
47
+ messages.push(message);
48
+ },
49
+ setSessionId(sessionId) {
50
+ this.sessionId = sessionId;
51
+ },
52
+ };
53
+
54
+ try {
55
+ await createFakeOpenCodeExecutable(tempRoot);
56
+ process.env[pathKey] = `${tempRoot}${path.delimiter}${previousPath || ''}`;
57
+ if (process.platform === 'win32') {
58
+ process.env[pathExtKey] = previousPathExt?.toUpperCase().includes('.CMD')
59
+ ? previousPathExt
60
+ : `.COM;.EXE;.BAT;.CMD${previousPathExt ? `;${previousPathExt}` : ''}`;
61
+ }
62
+
63
+ await spawnOpenCode('Hi', { cwd: tempRoot }, writer);
64
+
65
+ const sessionCreatedIndex = messages.findIndex((message) => message.kind === 'session_created');
66
+ const assistantDeltaIndex = messages.findIndex((message) =>
67
+ message.kind === 'stream_delta' && message.content === 'assistant response',
68
+ );
69
+ const streamEnd = messages.find((message) => message.kind === 'stream_end');
70
+ const complete = messages.find((message) => message.kind === 'complete');
71
+
72
+ assert.notEqual(sessionCreatedIndex, -1);
73
+ assert.notEqual(assistantDeltaIndex, -1);
74
+ assert.ok(sessionCreatedIndex < assistantDeltaIndex);
75
+ assert.equal(messages[sessionCreatedIndex].newSessionId, 'open-live-1');
76
+ assert.equal(writer.sessionId, 'open-live-1');
77
+ assert.equal(streamEnd?.sessionId, 'open-live-1');
78
+ assert.equal(complete?.sessionId, 'open-live-1');
79
+ assert.equal(messages.some((message) => message.kind === 'error'), false);
80
+ } finally {
81
+ if (previousPath === undefined) {
82
+ delete process.env[pathKey];
83
+ } else {
84
+ process.env[pathKey] = previousPath;
85
+ }
86
+
87
+ if (previousPathExt === undefined) {
88
+ delete process.env[pathExtKey];
89
+ } else {
90
+ process.env[pathExtKey] = previousPathExt;
91
+ }
92
+
93
+ await rm(tempRoot, { recursive: true, force: true });
94
+ }
95
+ });