@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.
- package/README.de.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.tr.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/api-docs.html +6 -7
- package/dist/assets/index-MOIvpy53.css +32 -0
- package/dist/assets/{index-DqMVUeZS.js → index-skXSjBje.js} +290 -270
- package/dist/index.html +2 -2
- package/dist/modelConstants.js +841 -0
- package/dist-server/server/claude-sdk.js +67 -39
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cursor-cli.js +6 -9
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +163 -35
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/gemini-response-handler.js +34 -0
- package/dist-server/server/gemini-response-handler.js.map +1 -1
- package/dist-server/server/index.js +131 -19
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/database/index.js +1 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +11 -0
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/projects.db.js +12 -0
- package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -1
- package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js +61 -0
- package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +49 -8
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +1 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/projects/projects.routes.js +12 -3
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js +14 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-management.service.js +1 -0
- package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +47 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
- package/dist-server/server/modules/providers/index.js +1 -0
- package/dist-server/server/modules/providers/index.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +27 -7
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +141 -0
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +55 -2
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +153 -14
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +198 -0
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +4 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +84 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -2
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +21 -6
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +50 -0
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +4 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +754 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +8 -30
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +187 -24
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +28 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +4 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +125 -2
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +27 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +24 -8
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +10 -10
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +33 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +4 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +92 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +267 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +115 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +410 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +19 -0
- package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
- package/dist-server/server/modules/providers/provider.registry.js +2 -0
- package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +65 -3
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/mcp.service.js +1 -9
- package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +199 -0
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +129 -13
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +13 -4
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +81 -7
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/skills.service.js +11 -0
- package/dist-server/server/modules/providers/services/skills.service.js.map +1 -0
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
- package/dist-server/server/modules/providers/shared/skills/skills.provider.js +45 -0
- package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/tests/mcp.test.js +73 -6
- package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +66 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +262 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +270 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/skills.test.js +259 -0
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +12 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +4 -4
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/openai-codex.js +87 -29
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/opencode-cli.js +287 -0
- package/dist-server/server/opencode-cli.js.map +1 -0
- package/dist-server/server/opencode-cli.test.js +84 -0
- package/dist-server/server/opencode-cli.test.js.map +1 -0
- package/dist-server/server/routes/agent.js +21 -8
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/commands.js +204 -211
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/cursor.js +2 -2
- package/dist-server/server/routes/cursor.js.map +1 -1
- package/dist-server/server/routes/settings.js +0 -10
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/routes/tests/commands.test.js +76 -0
- package/dist-server/server/routes/tests/commands.test.js.map +1 -0
- package/dist-server/server/shared/claude-cli-path.js +103 -0
- package/dist-server/server/shared/claude-cli-path.js.map +1 -0
- package/dist-server/server/shared/claude-cli-path.test.js +45 -0
- package/dist-server/server/shared/claude-cli-path.test.js.map +1 -0
- package/dist-server/server/{utils → shared}/frontmatter.js +3 -3
- package/dist-server/server/shared/frontmatter.js.map +1 -0
- package/dist-server/server/shared/utils.js +368 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/utils/commandParser.js +3 -3
- package/dist-server/server/utils/commandParser.js.map +1 -1
- package/package.json +3 -1
- package/public/api-docs.html +878 -0
- package/public/modelConstants.js +841 -0
- package/server/claude-sdk.js +74 -40
- package/server/cursor-cli.js +6 -11
- package/server/gemini-cli.js +197 -43
- package/server/gemini-response-handler.js +38 -0
- package/server/index.js +144 -19
- package/server/modules/database/index.ts +1 -0
- package/server/modules/database/migrations.ts +12 -0
- package/server/modules/database/repositories/projects.db.ts +13 -0
- package/server/modules/database/repositories/sessions.db.integration.test.ts +72 -0
- package/server/modules/database/repositories/sessions.db.ts +60 -9
- package/server/modules/database/schema.ts +1 -0
- package/server/modules/projects/projects.routes.ts +20 -3
- package/server/modules/projects/services/project-delete.service.ts +16 -1
- package/server/modules/projects/services/project-management.service.ts +2 -0
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +71 -1
- package/server/modules/providers/README.md +354 -0
- package/server/modules/providers/index.ts +2 -1
- package/server/modules/providers/list/claude/claude-auth.provider.ts +38 -13
- package/server/modules/providers/list/claude/claude-models.provider.ts +191 -0
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +68 -2
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +175 -14
- package/server/modules/providers/list/claude/claude-skills.provider.ts +257 -0
- package/server/modules/providers/list/claude/claude.provider.ts +11 -1
- package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +62 -2
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +25 -6
- package/server/modules/providers/list/codex/codex-skills.provider.ts +60 -0
- package/server/modules/providers/list/codex/codex.provider.ts +11 -1
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +17 -40
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +216 -26
- package/server/modules/providers/list/cursor/cursor-skills.provider.ts +31 -0
- package/server/modules/providers/list/cursor/cursor.provider.ts +11 -1
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +158 -2
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +24 -20
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +10 -11
- package/server/modules/providers/list/gemini/gemini-skills.provider.ts +36 -0
- package/server/modules/providers/list/gemini/gemini.provider.ts +11 -1
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
- package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +157 -0
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
- package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
- package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
- package/server/modules/providers/provider.registry.ts +2 -0
- package/server/modules/providers/provider.routes.ts +97 -4
- package/server/modules/providers/services/mcp.service.ts +1 -12
- package/server/modules/providers/services/provider-models.service.ts +325 -0
- package/server/modules/providers/services/session-conversations-search.service.ts +162 -13
- package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
- package/server/modules/providers/services/sessions-watcher.service.ts +14 -4
- package/server/modules/providers/services/sessions.service.ts +112 -9
- package/server/modules/providers/services/skills.service.ts +15 -0
- package/server/modules/providers/shared/base/abstract.provider.ts +4 -0
- package/server/modules/providers/shared/skills/skills.provider.ts +64 -0
- package/server/modules/providers/tests/mcp.test.ts +93 -6
- package/server/modules/providers/tests/opencode-models.test.ts +73 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +334 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
- package/server/modules/providers/tests/skills.test.ts +512 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +15 -1
- package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +4 -4
- package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
- package/server/openai-codex.js +99 -30
- package/server/opencode-cli.js +336 -0
- package/server/opencode-cli.test.js +95 -0
- package/server/routes/agent.js +22 -8
- package/server/routes/commands.js +256 -233
- package/server/routes/cursor.js +2 -2
- package/server/routes/settings.js +1 -10
- package/server/routes/tests/commands.test.js +82 -0
- package/server/shared/claude-cli-path.test.ts +61 -0
- package/server/shared/claude-cli-path.ts +139 -0
- package/server/{utils/frontmatter.js → shared/frontmatter.ts} +3 -3
- package/server/shared/interfaces.ts +64 -0
- package/server/shared/types.ts +166 -1
- package/server/shared/utils.ts +478 -0
- package/server/utils/commandParser.js +5 -3
- package/dist/assets/index-6rYG_mRo.css +0 -32
- package/dist-server/server/utils/frontmatter.js.map +0 -1
- package/dist-server/shared/modelConstants.js +0 -98
- package/dist-server/shared/modelConstants.js.map +0 -1
- package/shared/modelConstants.js +0 -106
package/server/openai-codex.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
//
|
|
247
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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'
|
|
286
|
-
const
|
|
287
|
-
|
|
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({
|
|
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:
|
|
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 =
|
|
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:
|
|
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:
|
|
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 (
|
|
334
|
-
const session = activeCodexSessions.get(
|
|
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
|
+
});
|