@glwhappen/web-code 1.32.7 → 1.32.10
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-CCGk0QgG.js → index-BLLsK3sG.js} +277 -262
- package/dist/assets/index-Dl5QP21C.css +32 -0
- package/dist/index.html +2 -2
- package/dist/modelConstants.js +841 -0
- package/dist-server/server/claude-sdk.js +57 -34
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cursor-cli.js +6 -3
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +3 -1
- 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/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 +4 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +143 -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.provider.js +2 -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-skills.provider.js +7 -39
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -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-sessions.provider.js +2 -15
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.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-sessions.provider.js +3 -9
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -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 +42 -1
- 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-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 +7 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
- 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 +264 -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 +33 -0
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +18 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.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/modules/websocket/services/websocket-writer.service.js +6 -0
- package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -1
- package/dist-server/server/openai-codex.js +32 -4
- 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 +202 -209
- 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/utils.js +286 -0
- package/dist-server/server/shared/utils.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 +64 -35
- package/server/cursor-cli.js +6 -3
- package/server/gemini-cli.js +7 -1
- package/server/gemini-response-handler.js +38 -0
- package/server/index.js +150 -19
- package/server/modules/database/index.ts +1 -0
- package/server/modules/projects/services/project-management.service.ts +2 -0
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +7 -1
- package/server/modules/providers/README.md +11 -3
- package/server/modules/providers/list/claude/claude-models.provider.ts +193 -0
- package/server/modules/providers/list/claude/claude.provider.ts +3 -0
- package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
- package/server/modules/providers/list/codex/codex-skills.provider.ts +10 -50
- package/server/modules/providers/list/codex/codex.provider.ts +3 -0
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +7 -20
- package/server/modules/providers/list/cursor/cursor.provider.ts +3 -0
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +3 -10
- package/server/modules/providers/list/gemini/gemini.provider.ts +3 -0
- 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 +158 -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 +62 -2
- 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-synchronizer.service.ts +1 -0
- package/server/modules/providers/services/sessions-watcher.service.ts +8 -0
- package/server/modules/providers/shared/base/abstract.provider.ts +2 -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 +336 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
- package/server/modules/providers/tests/skills.test.ts +66 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +21 -1
- package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
- package/server/modules/websocket/services/websocket-writer.service.ts +7 -0
- package/server/openai-codex.js +40 -4
- 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 +254 -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/interfaces.ts +45 -0
- package/server/shared/types.ts +88 -1
- package/server/shared/utils.ts +384 -0
- package/dist/assets/index-DdxLnCfK.css +0 -32
- package/dist-server/shared/modelConstants.js +0 -99
- package/dist-server/shared/modelConstants.js.map +0 -1
- package/shared/modelConstants.js +0 -107
|
@@ -377,6 +377,72 @@ test('providerSkillsService lists codex repository, user, and system skills', {
|
|
|
377
377
|
}
|
|
378
378
|
});
|
|
379
379
|
|
|
380
|
+
/**
|
|
381
|
+
* This test covers OpenCode skill lookup across cwd-to-git-root project folders
|
|
382
|
+
* plus the global OpenCode/Claude/Agents compatibility locations.
|
|
383
|
+
*/
|
|
384
|
+
test('providerSkillsService lists opencode project and user compatibility skills', { concurrency: false }, async () => {
|
|
385
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-opencode-'));
|
|
386
|
+
const repoRoot = path.join(tempRoot, 'repo');
|
|
387
|
+
const workspacePath = path.join(repoRoot, 'packages', 'app');
|
|
388
|
+
await fs.mkdir(path.join(repoRoot, '.git'), { recursive: true });
|
|
389
|
+
await fs.mkdir(workspacePath, { recursive: true });
|
|
390
|
+
|
|
391
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
392
|
+
try {
|
|
393
|
+
await writeSkill(
|
|
394
|
+
path.join(workspacePath, '.opencode', 'skills'),
|
|
395
|
+
'opencode-cwd-dir',
|
|
396
|
+
'opencode-cwd',
|
|
397
|
+
'OpenCode cwd skill',
|
|
398
|
+
);
|
|
399
|
+
await writeSkill(
|
|
400
|
+
path.join(repoRoot, 'packages', '.claude', 'skills'),
|
|
401
|
+
'opencode-claude-parent-dir',
|
|
402
|
+
'opencode-claude-parent',
|
|
403
|
+
'OpenCode Claude parent skill',
|
|
404
|
+
);
|
|
405
|
+
await writeSkill(
|
|
406
|
+
path.join(repoRoot, '.agents', 'skills'),
|
|
407
|
+
'opencode-agents-root-dir',
|
|
408
|
+
'opencode-agents-root',
|
|
409
|
+
'OpenCode Agents root skill',
|
|
410
|
+
);
|
|
411
|
+
await writeSkill(
|
|
412
|
+
path.join(tempRoot, '.config', 'opencode', 'skills'),
|
|
413
|
+
'opencode-user-dir',
|
|
414
|
+
'opencode-user',
|
|
415
|
+
'OpenCode user skill',
|
|
416
|
+
);
|
|
417
|
+
await writeSkill(
|
|
418
|
+
path.join(tempRoot, '.claude', 'skills'),
|
|
419
|
+
'opencode-claude-user-dir',
|
|
420
|
+
'opencode-claude-user',
|
|
421
|
+
'OpenCode Claude user skill',
|
|
422
|
+
);
|
|
423
|
+
await writeSkill(
|
|
424
|
+
path.join(tempRoot, '.agents', 'skills'),
|
|
425
|
+
'opencode-agents-user-dir',
|
|
426
|
+
'opencode-agents-user',
|
|
427
|
+
'OpenCode Agents user skill',
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const skills = await providerSkillsService.listProviderSkills('opencode', { workspacePath });
|
|
431
|
+
const byName = new Map(skills.map((skill) => [skill.name, skill]));
|
|
432
|
+
|
|
433
|
+
assert.equal(byName.get('opencode-cwd')?.scope, 'project');
|
|
434
|
+
assert.equal(byName.get('opencode-claude-parent')?.scope, 'project');
|
|
435
|
+
assert.equal(byName.get('opencode-agents-root')?.scope, 'project');
|
|
436
|
+
assert.equal(byName.get('opencode-user')?.scope, 'user');
|
|
437
|
+
assert.equal(byName.get('opencode-claude-user')?.scope, 'user');
|
|
438
|
+
assert.equal(byName.get('opencode-agents-user')?.scope, 'user');
|
|
439
|
+
assert.equal(byName.get('opencode-cwd')?.command, '/opencode-cwd');
|
|
440
|
+
} finally {
|
|
441
|
+
restoreHomeDir();
|
|
442
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
380
446
|
/**
|
|
381
447
|
* This test covers Gemini and Cursor skill directory rules, including shared
|
|
382
448
|
* `.agents/skills` project support.
|
|
@@ -30,10 +30,12 @@ type ChatWebSocketDependencies = {
|
|
|
30
30
|
spawnCursor: (command: string, options: unknown, writer: WebSocketWriter) => Promise<unknown>;
|
|
31
31
|
queryCodex: (command: string, options: unknown, writer: WebSocketWriter) => Promise<unknown>;
|
|
32
32
|
spawnGemini: (command: string, options: unknown, writer: WebSocketWriter) => Promise<unknown>;
|
|
33
|
+
spawnOpenCode: (command: string, options: unknown, writer: WebSocketWriter) => Promise<unknown>;
|
|
33
34
|
abortClaudeSDKSession: (sessionId: string, userId?: string | number | null) => Promise<boolean>;
|
|
34
35
|
abortCursorSession: (sessionId: string, userId?: string | number | null) => boolean;
|
|
35
36
|
abortCodexSession: (sessionId: string, userId?: string | number | null) => boolean;
|
|
36
37
|
abortGeminiSession: (sessionId: string, userId?: string | number | null) => boolean;
|
|
38
|
+
abortOpenCodeSession: (sessionId: string) => boolean;
|
|
37
39
|
resolveToolApproval: (
|
|
38
40
|
requestId: string,
|
|
39
41
|
payload: {
|
|
@@ -47,19 +49,21 @@ type ChatWebSocketDependencies = {
|
|
|
47
49
|
isCursorSessionActive: (sessionId: string, userId?: string | number | null) => boolean;
|
|
48
50
|
isCodexSessionActive: (sessionId: string, userId?: string | number | null) => boolean;
|
|
49
51
|
isGeminiSessionActive: (sessionId: string, userId?: string | number | null) => boolean;
|
|
52
|
+
isOpenCodeSessionActive: (sessionId: string) => boolean;
|
|
50
53
|
reconnectSessionWriter: (sessionId: string, ws: WebSocket, userId?: string | number | null) => boolean;
|
|
51
54
|
getPendingApprovalsForSession: (sessionId: string, userId?: string | number | null) => unknown[];
|
|
52
55
|
getActiveClaudeSDKSessions: (userId?: string | number | null) => unknown;
|
|
53
56
|
getActiveCursorSessions: (userId?: string | number | null) => unknown;
|
|
54
57
|
getActiveCodexSessions: (userId?: string | number | null) => unknown;
|
|
55
58
|
getActiveGeminiSessions: (userId?: string | number | null) => unknown;
|
|
59
|
+
getActiveOpenCodeSessions: () => unknown;
|
|
56
60
|
};
|
|
57
61
|
|
|
58
62
|
/**
|
|
59
63
|
* Normalizes potentially invalid provider names coming from websocket payloads.
|
|
60
64
|
*/
|
|
61
65
|
function readProvider(value: unknown): LLMProvider {
|
|
62
|
-
if (value === 'claude' || value === 'cursor' || value === 'codex' || value === 'gemini') {
|
|
66
|
+
if (value === 'claude' || value === 'cursor' || value === 'codex' || value === 'gemini' || value === 'opencode') {
|
|
63
67
|
return value;
|
|
64
68
|
}
|
|
65
69
|
|
|
@@ -199,6 +203,17 @@ export function handleChatConnection(
|
|
|
199
203
|
return;
|
|
200
204
|
}
|
|
201
205
|
|
|
206
|
+
if (messageType === 'opencode-command') {
|
|
207
|
+
const authorization = authorizeOptionsCwd(writer.userId, data.options);
|
|
208
|
+
if (!authorization.authorized) {
|
|
209
|
+
sendAuthorizationError(authorization.error, 'opencode');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const sanitizedOptions = { ...(data.options ?? {}), cwd: authorization.cwd };
|
|
213
|
+
await dependencies.spawnOpenCode(data.command ?? '', sanitizedOptions, writer);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
202
217
|
if (messageType === 'cursor-resume') {
|
|
203
218
|
const authorization = authorizeOptionsCwd(writer.userId, data.options);
|
|
204
219
|
if (!authorization.authorized) {
|
|
@@ -229,6 +244,8 @@ export function handleChatConnection(
|
|
|
229
244
|
success = dependencies.abortCodexSession(sessionId, userId);
|
|
230
245
|
} else if (provider === 'gemini') {
|
|
231
246
|
success = dependencies.abortGeminiSession(sessionId, userId);
|
|
247
|
+
} else if (provider === 'opencode') {
|
|
248
|
+
success = dependencies.abortOpenCodeSession(sessionId);
|
|
232
249
|
} else {
|
|
233
250
|
success = await dependencies.abortClaudeSDKSession(sessionId, userId);
|
|
234
251
|
}
|
|
@@ -286,6 +303,8 @@ export function handleChatConnection(
|
|
|
286
303
|
isActive = dependencies.isCodexSessionActive(sessionId, userId);
|
|
287
304
|
} else if (provider === 'gemini') {
|
|
288
305
|
isActive = dependencies.isGeminiSessionActive(sessionId, userId);
|
|
306
|
+
} else if (provider === 'opencode') {
|
|
307
|
+
isActive = dependencies.isOpenCodeSessionActive(sessionId);
|
|
289
308
|
} else {
|
|
290
309
|
isActive = dependencies.isClaudeSDKSessionActive(sessionId, userId);
|
|
291
310
|
if (isActive) {
|
|
@@ -325,6 +344,7 @@ export function handleChatConnection(
|
|
|
325
344
|
cursor: dependencies.getActiveCursorSessions(userId),
|
|
326
345
|
codex: dependencies.getActiveCodexSessions(userId),
|
|
327
346
|
gemini: dependencies.getActiveGeminiSessions(userId),
|
|
347
|
+
opencode: dependencies.getActiveOpenCodeSessions(),
|
|
328
348
|
},
|
|
329
349
|
});
|
|
330
350
|
}
|
|
@@ -138,6 +138,13 @@ function buildShellCommand(
|
|
|
138
138
|
return command;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
if (provider === 'opencode') {
|
|
142
|
+
if (hasSession && sessionId) {
|
|
143
|
+
return `opencode --session "${sessionId}"`;
|
|
144
|
+
}
|
|
145
|
+
return initialCommand || 'opencode';
|
|
146
|
+
}
|
|
147
|
+
|
|
141
148
|
const command = initialCommand || 'claude';
|
|
142
149
|
if (hasSession && sessionId) {
|
|
143
150
|
if (os.platform() === 'win32') {
|
|
@@ -425,6 +432,8 @@ export function handleShellConnection(
|
|
|
425
432
|
? 'Codex'
|
|
426
433
|
: provider === 'gemini'
|
|
427
434
|
? 'Gemini'
|
|
435
|
+
: provider === 'opencode'
|
|
436
|
+
? 'OpenCode'
|
|
428
437
|
: 'Claude';
|
|
429
438
|
welcomeMsg = hasSession
|
|
430
439
|
? `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n`
|
|
@@ -21,6 +21,13 @@ export class WebSocketWriter {
|
|
|
21
21
|
send(data: unknown): void {
|
|
22
22
|
if (this.ws.readyState === WS_OPEN_STATE) {
|
|
23
23
|
this.ws.send(JSON.stringify(data));
|
|
24
|
+
} else {
|
|
25
|
+
const kind = (data as Record<string, unknown>)?.kind;
|
|
26
|
+
if (kind === 'complete' || kind === 'error') {
|
|
27
|
+
console.warn(
|
|
28
|
+
`[WebSocketWriter] Dropped "${kind}" event — socket not open (readyState=${this.ws.readyState}, session=${this.sessionId})`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
33
|
|
package/server/openai-codex.js
CHANGED
|
@@ -17,6 +17,7 @@ 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 – keys are namespaced as `${userId}:${sessionId}`
|
|
@@ -27,6 +28,34 @@ function buildCodexSessionKey(userId, sessionId) {
|
|
|
27
28
|
return `${safeUser}:${sessionId}`;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
function readUsageNumber(value) {
|
|
32
|
+
const parsed = Number(value);
|
|
33
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractCodexTokenBudget(event) {
|
|
37
|
+
const info = event?.info || event?.payload?.info || event?.usage?.info;
|
|
38
|
+
const usage = info?.total_token_usage || event?.usage?.total_token_usage || event?.usage;
|
|
39
|
+
if (!usage || typeof usage !== 'object') {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const inputTokens = readUsageNumber(usage.input_tokens);
|
|
44
|
+
const outputTokens = readUsageNumber(usage.output_tokens);
|
|
45
|
+
const used = readUsageNumber(usage.total_tokens) || inputTokens + outputTokens;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
used,
|
|
49
|
+
total: readUsageNumber(info?.model_context_window || event?.usage?.model_context_window) || 200000,
|
|
50
|
+
inputTokens,
|
|
51
|
+
outputTokens,
|
|
52
|
+
breakdown: {
|
|
53
|
+
input: inputTokens,
|
|
54
|
+
output: outputTokens,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
30
59
|
/**
|
|
31
60
|
* Transform Codex SDK event to WebSocket message format
|
|
32
61
|
* @param {object} event - SDK event
|
|
@@ -207,6 +236,11 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
207
236
|
permissionMode = 'default'
|
|
208
237
|
} = options;
|
|
209
238
|
|
|
239
|
+
const resolvedModel = await providerModelsService.resolveResumeModel(
|
|
240
|
+
'codex',
|
|
241
|
+
sessionId,
|
|
242
|
+
model,
|
|
243
|
+
);
|
|
210
244
|
const userId = ws?.userId || null;
|
|
211
245
|
const workingDirectory = cwd || projectPath || process.cwd();
|
|
212
246
|
const { sandboxMode, approvalPolicy } = mapPermissionModeToCodexOptions(permissionMode);
|
|
@@ -228,7 +262,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
228
262
|
skipGitRepoCheck: true,
|
|
229
263
|
sandboxMode,
|
|
230
264
|
approvalPolicy,
|
|
231
|
-
model
|
|
265
|
+
model: resolvedModel
|
|
232
266
|
};
|
|
233
267
|
|
|
234
268
|
// Start or resume thread
|
|
@@ -318,9 +352,11 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
318
352
|
}
|
|
319
353
|
|
|
320
354
|
// Extract and send token usage if available (normalized to match Claude format)
|
|
321
|
-
if (event.type === 'turn.completed'
|
|
322
|
-
const
|
|
323
|
-
|
|
355
|
+
if (event.type === 'turn.completed') {
|
|
356
|
+
const tokenBudget = extractCodexTokenBudget(event);
|
|
357
|
+
if (tokenBudget) {
|
|
358
|
+
sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget, sessionId: capturedSessionId || sessionId || null, provider: 'codex' }));
|
|
359
|
+
}
|
|
324
360
|
}
|
|
325
361
|
}
|
|
326
362
|
|
|
@@ -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
|
+
};
|