@cloudcli-ai/cloudcli 1.32.0 → 1.33.1

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 (180) 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-BsyL_xmI.js +1348 -0
  9. package/dist/assets/index-FSP9Hz7V.css +32 -0
  10. package/dist/index.html +2 -2
  11. package/dist/modelConstants.js +848 -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 +234 -67
  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 +28 -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 +146 -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 +226 -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 +298 -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 +18 -3
  94. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  95. package/dist-server/server/modules/websocket/services/websocket-auth.service.js +6 -2
  96. package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -1
  97. package/dist-server/server/modules/websocket/services/websocket-server.service.js +18 -0
  98. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -1
  99. package/dist-server/server/openai-codex.js +32 -4
  100. package/dist-server/server/openai-codex.js.map +1 -1
  101. package/dist-server/server/opencode-cli.js +287 -0
  102. package/dist-server/server/opencode-cli.js.map +1 -0
  103. package/dist-server/server/opencode-cli.test.js +84 -0
  104. package/dist-server/server/opencode-cli.test.js.map +1 -0
  105. package/dist-server/server/routes/agent.js +21 -8
  106. package/dist-server/server/routes/agent.js.map +1 -1
  107. package/dist-server/server/routes/commands.js +202 -209
  108. package/dist-server/server/routes/commands.js.map +1 -1
  109. package/dist-server/server/routes/cursor.js +2 -2
  110. package/dist-server/server/routes/cursor.js.map +1 -1
  111. package/dist-server/server/routes/settings.js +0 -10
  112. package/dist-server/server/routes/settings.js.map +1 -1
  113. package/dist-server/server/routes/tests/commands.test.js +76 -0
  114. package/dist-server/server/routes/tests/commands.test.js.map +1 -0
  115. package/dist-server/server/shared/utils.js +286 -0
  116. package/dist-server/server/shared/utils.js.map +1 -1
  117. package/package.json +5 -2
  118. package/public/api-docs.html +878 -0
  119. package/public/modelConstants.js +848 -0
  120. package/server/claude-sdk.js +64 -35
  121. package/server/cursor-cli.js +6 -3
  122. package/server/gemini-cli.js +7 -1
  123. package/server/gemini-response-handler.js +38 -0
  124. package/server/index.js +259 -75
  125. package/server/modules/database/index.ts +1 -0
  126. package/server/modules/projects/services/project-management.service.ts +2 -0
  127. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +7 -1
  128. package/server/modules/providers/README.md +11 -3
  129. package/server/modules/providers/list/claude/claude-auth.provider.ts +34 -6
  130. package/server/modules/providers/list/claude/claude-models.provider.ts +196 -0
  131. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  132. package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
  133. package/server/modules/providers/list/codex/codex-skills.provider.ts +10 -50
  134. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  135. package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
  136. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +7 -20
  137. package/server/modules/providers/list/cursor/cursor.provider.ts +3 -0
  138. package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
  139. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +3 -10
  140. package/server/modules/providers/list/gemini/gemini.provider.ts +3 -0
  141. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
  142. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
  143. package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
  144. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +157 -0
  145. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
  146. package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
  147. package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
  148. package/server/modules/providers/provider.registry.ts +2 -0
  149. package/server/modules/providers/provider.routes.ts +62 -2
  150. package/server/modules/providers/services/mcp.service.ts +1 -12
  151. package/server/modules/providers/services/provider-models.service.ts +358 -0
  152. package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
  153. package/server/modules/providers/services/sessions-watcher.service.ts +8 -0
  154. package/server/modules/providers/shared/base/abstract.provider.ts +2 -0
  155. package/server/modules/providers/tests/mcp.test.ts +93 -6
  156. package/server/modules/providers/tests/opencode-models.test.ts +73 -0
  157. package/server/modules/providers/tests/opencode-sessions.test.ts +334 -0
  158. package/server/modules/providers/tests/provider-models.service.test.ts +349 -0
  159. package/server/modules/providers/tests/skills.test.ts +66 -0
  160. package/server/modules/websocket/services/chat-websocket.service.ts +15 -1
  161. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +4 -4
  162. package/server/modules/websocket/services/shell-websocket.service.ts +22 -2
  163. package/server/modules/websocket/services/websocket-auth.service.ts +7 -2
  164. package/server/modules/websocket/services/websocket-server.service.ts +18 -0
  165. package/server/openai-codex.js +41 -4
  166. package/server/opencode-cli.js +336 -0
  167. package/server/opencode-cli.test.js +95 -0
  168. package/server/routes/agent.js +22 -8
  169. package/server/routes/commands.js +254 -233
  170. package/server/routes/cursor.js +2 -2
  171. package/server/routes/settings.js +1 -10
  172. package/server/routes/tests/commands.test.js +82 -0
  173. package/server/shared/interfaces.ts +45 -0
  174. package/server/shared/types.ts +88 -1
  175. package/server/shared/utils.ts +384 -0
  176. package/dist/assets/index-AaSE7-3t.js +0 -1336
  177. package/dist/assets/index-CKaku72N.css +0 -32
  178. package/dist-server/shared/modelConstants.js +0 -99
  179. package/dist-server/shared/modelConstants.js.map +0 -1
  180. package/shared/modelConstants.js +0 -107
@@ -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
@@ -202,6 +231,12 @@ 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
 
@@ -222,7 +257,7 @@ export async function queryCodex(command, options = {}, ws) {
222
257
  skipGitRepoCheck: true,
223
258
  sandboxMode,
224
259
  approvalPolicy,
225
- model
260
+ model: resolvedModel
226
261
  };
227
262
 
228
263
  // Start or resume thread
@@ -309,9 +344,11 @@ export async function queryCodex(command, options = {}, ws) {
309
344
  }
310
345
 
311
346
  // Extract and send token usage if available (normalized to match Claude format)
312
- if (event.type === 'turn.completed' && event.usage) {
313
- const totalTokens = (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0);
314
- sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: { used: totalTokens, total: 200000 }, sessionId: capturedSessionId || sessionId || null, 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
+ }
315
352
  }
316
353
  }
317
354
 
@@ -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
+ });
@@ -9,8 +9,9 @@ import { queryClaudeSDK } from '../claude-sdk.js';
9
9
  import { spawnCursor } from '../cursor-cli.js';
10
10
  import { queryCodex } from '../openai-codex.js';
11
11
  import { spawnGemini } from '../gemini-cli.js';
12
+ import { spawnOpenCode } from '../opencode-cli.js';
12
13
  import { Octokit } from '@octokit/rest';
13
- import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
14
+ import { providerModelsService } from '../modules/providers/services/provider-models.service.js';
14
15
  import { IS_PLATFORM } from '../constants/config.js';
15
16
  import { normalizeProjectPath } from '../shared/utils.js';
16
17
 
@@ -608,7 +609,7 @@ class ResponseCollector {
608
609
  /**
609
610
  * POST /api/agent
610
611
  *
611
- * Trigger an AI agent (Claude or Cursor) to work on a project.
612
+ * Trigger an AI agent to work on a project.
612
613
  * Supports automatic GitHub branch and pull request creation after successful completion.
613
614
  *
614
615
  * ================================================================================================
@@ -633,7 +634,7 @@ class ResponseCollector {
633
634
  * - Source for auto-generated branch names (if createBranch=true and no branchName)
634
635
  * - Fallback for PR title if no commits are made
635
636
  *
636
- * @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | 'gemini'
637
+ * @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | 'gemini' | 'opencode'
637
638
  * Default: 'claude'
638
639
  *
639
640
  * @param {boolean} stream - (Optional) Enable Server-Sent Events (SSE) streaming for real-time updates.
@@ -751,7 +752,7 @@ class ResponseCollector {
751
752
  * Input Validations (400 Bad Request):
752
753
  * - Either githubUrl OR projectPath must be provided (not neither)
753
754
  * - message must be non-empty string
754
- * - provider must be 'claude', 'cursor', 'codex', or 'gemini'
755
+ * - provider must be 'claude', 'cursor', 'codex', 'gemini', or 'opencode'
755
756
  * - createBranch/createPR requires githubUrl OR projectPath (not neither)
756
757
  * - branchName must pass Git naming rules (if provided)
757
758
  *
@@ -859,8 +860,8 @@ router.post('/', validateExternalApiKey, async (req, res) => {
859
860
  return res.status(400).json({ error: 'message is required' });
860
861
  }
861
862
 
862
- if (!['claude', 'cursor', 'codex', 'gemini'].includes(provider)) {
863
- return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", or "gemini"' });
863
+ if (!['claude', 'cursor', 'codex', 'gemini', 'opencode'].includes(provider)) {
864
+ return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", "gemini", or "opencode"' });
864
865
  }
865
866
 
866
867
  // Validate GitHub branch/PR creation requirements
@@ -938,6 +939,10 @@ router.post('/', validateExternalApiKey, async (req, res) => {
938
939
  });
939
940
  }
940
941
 
942
+ const codexModels = (await providerModelsService.getProviderModels('codex')).models;
943
+ const geminiModels = (await providerModelsService.getProviderModels('gemini')).models;
944
+ const opencodeModels = (await providerModelsService.getProviderModels('opencode')).models;
945
+
941
946
  // Start the appropriate session
942
947
  if (provider === 'claude') {
943
948
  console.log('🤖 Starting Claude SDK session');
@@ -967,7 +972,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
967
972
  projectPath: finalProjectPath,
968
973
  cwd: finalProjectPath,
969
974
  sessionId: sessionId || null,
970
- model: model || CODEX_MODELS.DEFAULT,
975
+ model: model || codexModels.DEFAULT,
971
976
  permissionMode: 'bypassPermissions'
972
977
  }, writer);
973
978
  } else if (provider === 'gemini') {
@@ -977,9 +982,18 @@ router.post('/', validateExternalApiKey, async (req, res) => {
977
982
  projectPath: finalProjectPath,
978
983
  cwd: finalProjectPath,
979
984
  sessionId: sessionId || null,
980
- model: model,
985
+ model: model || geminiModels.DEFAULT,
981
986
  skipPermissions: true // CLI mode bypasses permissions
982
987
  }, writer);
988
+ } else if (provider === 'opencode') {
989
+ console.log('Starting OpenCode CLI session');
990
+
991
+ await spawnOpenCode(message.trim(), {
992
+ projectPath: finalProjectPath,
993
+ cwd: finalProjectPath,
994
+ sessionId: sessionId || null,
995
+ model: model || opencodeModels.DEFAULT
996
+ }, writer);
983
997
  }
984
998
 
985
999
  // Handle GitHub branch and PR creation after successful agent completion