@glwhappen/web-code 1.32.9 → 1.32.11
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-D_7CSvqO.js → index-CBo8yakG.js} +276 -261
- 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/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/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
package/server/claude-sdk.js
CHANGED
|
@@ -17,7 +17,8 @@ import crypto from 'crypto';
|
|
|
17
17
|
import { promises as fs } from 'fs';
|
|
18
18
|
import path from 'path';
|
|
19
19
|
import os from 'os';
|
|
20
|
-
import {
|
|
20
|
+
import { CLAUDE_FALLBACK_MODELS } from './modules/providers/list/claude/claude-models.provider.js';
|
|
21
|
+
import { providerModelsService } from './modules/providers/services/provider-models.service.js';
|
|
21
22
|
import { resolveClaudeCodeExecutablePath } from './shared/claude-cli-path.js';
|
|
22
23
|
import {
|
|
23
24
|
createNotificationEvent,
|
|
@@ -210,7 +211,7 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
210
211
|
|
|
211
212
|
// Map model (default to sonnet)
|
|
212
213
|
// Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
|
|
213
|
-
sdkOptions.model = options.model ||
|
|
214
|
+
sdkOptions.model = options.model || CLAUDE_FALLBACK_MODELS.DEFAULT;
|
|
214
215
|
// Model logged at query start below
|
|
215
216
|
|
|
216
217
|
// Map system prompt configuration
|
|
@@ -306,43 +307,68 @@ function transformMessage(sdkMessage) {
|
|
|
306
307
|
return sdkMessage;
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
function readNumber(value) {
|
|
311
|
+
const parsed = Number(value);
|
|
312
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
309
315
|
/**
|
|
310
|
-
* Extracts token usage from SDK
|
|
311
|
-
*
|
|
316
|
+
* Extracts token usage from SDK messages.
|
|
317
|
+
* Prefers per-step `message.usage` (Claude message payload), then falls back
|
|
318
|
+
* to result-level usage/modelUsage for compatibility across SDK versions.
|
|
319
|
+
* @param {Object} sdkMessage - SDK stream message
|
|
312
320
|
* @returns {Object|null} Token budget object or null
|
|
313
321
|
*/
|
|
314
|
-
function extractTokenBudget(
|
|
315
|
-
if (
|
|
322
|
+
function extractTokenBudget(sdkMessage) {
|
|
323
|
+
if (!sdkMessage || typeof sdkMessage !== 'object') {
|
|
316
324
|
return null;
|
|
317
325
|
}
|
|
318
326
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
327
|
+
const messageUsage = sdkMessage.message?.usage || sdkMessage.usage;
|
|
328
|
+
if (messageUsage && typeof messageUsage === 'object') {
|
|
329
|
+
const inputTokens = readNumber(messageUsage.input_tokens ?? messageUsage.inputTokens);
|
|
330
|
+
const outputTokens = readNumber(messageUsage.output_tokens ?? messageUsage.outputTokens);
|
|
331
|
+
const totalUsed = inputTokens + outputTokens;
|
|
332
|
+
const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
|
|
322
333
|
|
|
323
|
-
|
|
324
|
-
|
|
334
|
+
return {
|
|
335
|
+
used: totalUsed,
|
|
336
|
+
total: contextWindow,
|
|
337
|
+
inputTokens,
|
|
338
|
+
outputTokens,
|
|
339
|
+
breakdown: {
|
|
340
|
+
input: inputTokens,
|
|
341
|
+
output: outputTokens,
|
|
342
|
+
},
|
|
343
|
+
};
|
|
325
344
|
}
|
|
326
345
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const outputTokens = modelData.cumulativeOutputTokens || modelData.outputTokens || 0;
|
|
331
|
-
const cacheReadTokens = modelData.cumulativeCacheReadInputTokens || modelData.cacheReadInputTokens || 0;
|
|
332
|
-
const cacheCreationTokens = modelData.cumulativeCacheCreationInputTokens || modelData.cacheCreationInputTokens || 0;
|
|
346
|
+
if (!sdkMessage.modelUsage || typeof sdkMessage.modelUsage !== 'object') {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
333
349
|
|
|
334
|
-
//
|
|
335
|
-
const
|
|
350
|
+
// Fallback for older SDK messages with only modelUsage
|
|
351
|
+
const modelKey = Object.keys(sdkMessage.modelUsage)[0];
|
|
352
|
+
const modelData = sdkMessage.modelUsage[modelKey];
|
|
336
353
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
354
|
+
if (!modelData || typeof modelData !== 'object') {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
340
357
|
|
|
341
|
-
|
|
358
|
+
const inputTokens = readNumber(modelData.cumulativeInputTokens ?? modelData.inputTokens);
|
|
359
|
+
const outputTokens = readNumber(modelData.cumulativeOutputTokens ?? modelData.outputTokens);
|
|
360
|
+
const totalUsed = inputTokens + outputTokens;
|
|
361
|
+
const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
|
|
342
362
|
|
|
343
363
|
return {
|
|
344
364
|
used: totalUsed,
|
|
345
|
-
total: contextWindow
|
|
365
|
+
total: contextWindow,
|
|
366
|
+
inputTokens,
|
|
367
|
+
outputTokens,
|
|
368
|
+
breakdown: {
|
|
369
|
+
input: inputTokens,
|
|
370
|
+
output: outputTokens,
|
|
371
|
+
},
|
|
346
372
|
};
|
|
347
373
|
}
|
|
348
374
|
|
|
@@ -514,8 +540,17 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
514
540
|
};
|
|
515
541
|
|
|
516
542
|
try {
|
|
543
|
+
const resolvedModel = await providerModelsService.resolveResumeModel(
|
|
544
|
+
'claude',
|
|
545
|
+
sessionId,
|
|
546
|
+
options.model,
|
|
547
|
+
);
|
|
548
|
+
|
|
517
549
|
// Map CLI options to SDK format
|
|
518
|
-
const sdkOptions = mapCliOptionsToSDK(
|
|
550
|
+
const sdkOptions = mapCliOptionsToSDK({
|
|
551
|
+
...options,
|
|
552
|
+
model: resolvedModel || options.model,
|
|
553
|
+
});
|
|
519
554
|
|
|
520
555
|
// Load MCP configuration
|
|
521
556
|
const mcpServers = await loadMcpConfig(options.cwd);
|
|
@@ -697,16 +732,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
697
732
|
ws.send(msg);
|
|
698
733
|
}
|
|
699
734
|
|
|
700
|
-
// Extract and send token budget updates from result
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
// Model info available in result message
|
|
705
|
-
}
|
|
706
|
-
const tokenBudgetData = extractTokenBudget(message);
|
|
707
|
-
if (tokenBudgetData) {
|
|
708
|
-
ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
|
|
709
|
-
}
|
|
735
|
+
// Extract and send token budget updates from assistant/result usage payloads
|
|
736
|
+
const tokenBudgetData = extractTokenBudget(message);
|
|
737
|
+
if (tokenBudgetData) {
|
|
738
|
+
ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
|
|
710
739
|
}
|
|
711
740
|
}
|
|
712
741
|
|
package/server/cursor-cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import crossSpawn from 'cross-spawn';
|
|
|
3
3
|
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
4
4
|
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
|
5
5
|
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
|
6
|
+
import { providerModelsService } from './modules/providers/services/provider-models.service.js';
|
|
6
7
|
import { createNormalizedMessage } from './shared/utils.js';
|
|
7
8
|
|
|
8
9
|
// Use cross-spawn on Windows for better command execution
|
|
@@ -40,6 +41,7 @@ function isWorkspaceTrustPrompt(text = '') {
|
|
|
40
41
|
async function spawnCursor(command, options = {}, ws) {
|
|
41
42
|
return new Promise(async (resolve, reject) => {
|
|
42
43
|
const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model, sessionSummary } = options;
|
|
44
|
+
const resolvedModel = await providerModelsService.resolveResumeModel('cursor', sessionId, model);
|
|
43
45
|
let capturedSessionId = sessionId; // Track session ID throughout the process
|
|
44
46
|
let sessionCreatedSent = false; // Track if we've already sent session-created event
|
|
45
47
|
let hasRetriedWithTrust = false;
|
|
@@ -64,9 +66,10 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
64
66
|
// Provide a prompt (works for both new and resumed sessions)
|
|
65
67
|
baseArgs.push('-p', command);
|
|
66
68
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
// Model overrides are applied to both new and resumed sessions so a
|
|
70
|
+
// session-scoped change request can take effect on the next turn.
|
|
71
|
+
if (resolvedModel) {
|
|
72
|
+
baseArgs.push('--model', resolvedModel);
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
// Request streaming JSON when we are providing a prompt
|
package/server/gemini-cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import sessionManager from './sessionManager.js';
|
|
|
9
9
|
import GeminiResponseHandler from './gemini-response-handler.js';
|
|
10
10
|
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
11
11
|
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
|
12
|
+
import { providerModelsService } from './modules/providers/services/provider-models.service.js';
|
|
12
13
|
import { createNormalizedMessage } from './shared/utils.js';
|
|
13
14
|
|
|
14
15
|
// Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
|
|
@@ -133,6 +134,11 @@ async function buildGeminiProcessEnv() {
|
|
|
133
134
|
async function spawnGemini(command, options = {}, ws) {
|
|
134
135
|
const { sessionId, projectPath, cwd, toolsSettings, permissionMode, images, sessionSummary } = options;
|
|
135
136
|
const userId = ws?.userId ?? null;
|
|
137
|
+
const resolvedModel = await providerModelsService.resolveResumeModel(
|
|
138
|
+
'gemini',
|
|
139
|
+
sessionId,
|
|
140
|
+
options.model
|
|
141
|
+
);
|
|
136
142
|
let capturedSessionId = sessionId; // Track session ID throughout the process
|
|
137
143
|
let sessionCreatedSent = false; // Track if we've already sent session-created event
|
|
138
144
|
let assistantBlocks = []; // Accumulate the full response blocks including tools
|
|
@@ -257,7 +263,7 @@ async function spawnGemini(command, options = {}, ws) {
|
|
|
257
263
|
}
|
|
258
264
|
|
|
259
265
|
// Add model for all sessions (both new and resumed)
|
|
260
|
-
let modelToUse =
|
|
266
|
+
let modelToUse = resolvedModel || 'gemini-2.5-flash';
|
|
261
267
|
args.push('--model', modelToUse);
|
|
262
268
|
args.push('--output-format', 'stream-json');
|
|
263
269
|
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
// Gemini Response Handler - JSON Stream processing
|
|
2
2
|
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
|
3
|
+
import { createNormalizedMessage } from './shared/utils.js';
|
|
4
|
+
|
|
5
|
+
function buildGeminiTokenBudget(tokens) {
|
|
6
|
+
if (!tokens || typeof tokens !== 'object') {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const parsedInputTokens = Number(tokens.input);
|
|
11
|
+
const parsedOutputTokens = Number(tokens.output);
|
|
12
|
+
const inputTokens = Number.isFinite(parsedInputTokens) ? parsedInputTokens : 0;
|
|
13
|
+
const outputTokens = Number.isFinite(parsedOutputTokens) ? parsedOutputTokens : 0;
|
|
14
|
+
const parsedUsed = Number(tokens.total);
|
|
15
|
+
const used = Number.isFinite(parsedUsed) ? parsedUsed : inputTokens + outputTokens;
|
|
16
|
+
if (!Number.isFinite(used) || used <= 0) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
used,
|
|
22
|
+
inputTokens,
|
|
23
|
+
outputTokens,
|
|
24
|
+
breakdown: {
|
|
25
|
+
input: inputTokens,
|
|
26
|
+
output: outputTokens,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
3
30
|
|
|
4
31
|
class GeminiResponseHandler {
|
|
5
32
|
constructor(ws, options = {}) {
|
|
@@ -60,6 +87,17 @@ class GeminiResponseHandler {
|
|
|
60
87
|
for (const msg of normalized) {
|
|
61
88
|
this.ws.send(msg);
|
|
62
89
|
}
|
|
90
|
+
|
|
91
|
+
const tokenBudget = buildGeminiTokenBudget(event.tokens);
|
|
92
|
+
if (tokenBudget) {
|
|
93
|
+
this.ws.send(createNormalizedMessage({
|
|
94
|
+
kind: 'status',
|
|
95
|
+
text: 'token_budget',
|
|
96
|
+
tokenBudget,
|
|
97
|
+
sessionId: sid,
|
|
98
|
+
provider: 'gemini',
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
63
101
|
}
|
|
64
102
|
|
|
65
103
|
forceFlush() {
|
package/server/index.js
CHANGED
|
@@ -10,8 +10,15 @@ import { spawn } from 'child_process';
|
|
|
10
10
|
import express from 'express';
|
|
11
11
|
import cors from 'cors';
|
|
12
12
|
import mime from 'mime-types';
|
|
13
|
+
import Database from 'better-sqlite3';
|
|
13
14
|
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
AppError,
|
|
17
|
+
WORKSPACES_ROOT,
|
|
18
|
+
ensureUserWorkspaceRoot,
|
|
19
|
+
getOpenCodeDatabasePath,
|
|
20
|
+
validateWorkspacePath,
|
|
21
|
+
} from '@/shared/utils.js';
|
|
15
22
|
import { closeSessionsWatcher, initializeSessionsWatcher } from '@/modules/providers/index.js';
|
|
16
23
|
import { createWebSocketServer } from '@/modules/websocket/index.js';
|
|
17
24
|
|
|
@@ -45,6 +52,12 @@ import {
|
|
|
45
52
|
isGeminiSessionActive,
|
|
46
53
|
getActiveGeminiSessions,
|
|
47
54
|
} from './gemini-cli.js';
|
|
55
|
+
import {
|
|
56
|
+
spawnOpenCode,
|
|
57
|
+
abortOpenCodeSession,
|
|
58
|
+
isOpenCodeSessionActive,
|
|
59
|
+
getActiveOpenCodeSessions,
|
|
60
|
+
} from './opencode-cli.js';
|
|
48
61
|
import sessionManager from './sessionManager.js';
|
|
49
62
|
import {
|
|
50
63
|
stripAnsiSequences,
|
|
@@ -67,7 +80,7 @@ import geminiRoutes from './routes/gemini.js';
|
|
|
67
80
|
import pluginsRoutes from './routes/plugins.js';
|
|
68
81
|
import providerRoutes from './modules/providers/provider.routes.js';
|
|
69
82
|
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
|
|
70
|
-
import { initializeDatabase, projectsDb } from './modules/database/index.js';
|
|
83
|
+
import { initializeDatabase, projectsDb, sessionsDb } from './modules/database/index.js';
|
|
71
84
|
import { configureWebPush } from './services/vapid-keys.js';
|
|
72
85
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
73
86
|
import { IS_PLATFORM } from './constants/config.js';
|
|
@@ -95,21 +108,25 @@ const wss = createWebSocketServer(server, {
|
|
|
95
108
|
spawnCursor,
|
|
96
109
|
queryCodex,
|
|
97
110
|
spawnGemini,
|
|
111
|
+
spawnOpenCode,
|
|
98
112
|
abortClaudeSDKSession,
|
|
99
113
|
abortCursorSession,
|
|
100
114
|
abortCodexSession,
|
|
101
115
|
abortGeminiSession,
|
|
116
|
+
abortOpenCodeSession,
|
|
102
117
|
resolveToolApproval,
|
|
103
118
|
isClaudeSDKSessionActive,
|
|
104
119
|
isCursorSessionActive,
|
|
105
120
|
isCodexSessionActive,
|
|
106
121
|
isGeminiSessionActive,
|
|
122
|
+
isOpenCodeSessionActive,
|
|
107
123
|
reconnectSessionWriter,
|
|
108
124
|
getPendingApprovalsForSession,
|
|
109
125
|
getActiveClaudeSDKSessions,
|
|
110
126
|
getActiveCursorSessions,
|
|
111
127
|
getActiveCodexSessions,
|
|
112
128
|
getActiveGeminiSessions,
|
|
129
|
+
getActiveOpenCodeSessions,
|
|
113
130
|
},
|
|
114
131
|
shell: {
|
|
115
132
|
getSessionById: (userId, sessionId) => sessionManager.getSession(userId, sessionId),
|
|
@@ -1139,23 +1156,129 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1139
1156
|
return res.json({
|
|
1140
1157
|
used: 0,
|
|
1141
1158
|
total: 0,
|
|
1142
|
-
|
|
1159
|
+
inputTokens: 0,
|
|
1160
|
+
outputTokens: 0,
|
|
1161
|
+
breakdown: { input: 0, output: 0 },
|
|
1143
1162
|
unsupported: true,
|
|
1144
1163
|
message: 'Token usage tracking not available for Cursor sessions'
|
|
1145
1164
|
});
|
|
1146
1165
|
}
|
|
1147
1166
|
|
|
1148
|
-
// Handle Gemini sessions - they are raw logs in our current setup
|
|
1149
1167
|
if (provider === 'gemini') {
|
|
1168
|
+
const session = sessionsDb.getSessionById(safeSessionId);
|
|
1169
|
+
const sessionFilePath = session?.jsonl_path;
|
|
1170
|
+
if (!sessionFilePath) {
|
|
1171
|
+
return res.json({
|
|
1172
|
+
used: 0,
|
|
1173
|
+
inputTokens: 0,
|
|
1174
|
+
outputTokens: 0,
|
|
1175
|
+
breakdown: { input: 0, output: 0 },
|
|
1176
|
+
unsupported: true,
|
|
1177
|
+
message: 'Token usage tracking not available for this Gemini session'
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
let fileContent;
|
|
1182
|
+
try {
|
|
1183
|
+
fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
if (error.code === 'ENOENT') {
|
|
1186
|
+
return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
|
|
1187
|
+
}
|
|
1188
|
+
throw error;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const lines = fileContent.trim().split('\n');
|
|
1192
|
+
let inputTokens = 0;
|
|
1193
|
+
let outputTokens = 0;
|
|
1194
|
+
let totalTokens = 0;
|
|
1195
|
+
|
|
1196
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1197
|
+
try {
|
|
1198
|
+
const entry = JSON.parse(lines[i]);
|
|
1199
|
+
if (!entry.tokens || typeof entry.tokens !== 'object') {
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
inputTokens = Number(entry.tokens.input || 0);
|
|
1204
|
+
outputTokens = Number(entry.tokens.output || 0);
|
|
1205
|
+
totalTokens = Number(entry.tokens.total || inputTokens + outputTokens || 0);
|
|
1206
|
+
break;
|
|
1207
|
+
} catch {
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1150
1212
|
return res.json({
|
|
1151
|
-
used:
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1213
|
+
used: totalTokens,
|
|
1214
|
+
inputTokens,
|
|
1215
|
+
outputTokens,
|
|
1216
|
+
breakdown: {
|
|
1217
|
+
input: inputTokens,
|
|
1218
|
+
output: outputTokens
|
|
1219
|
+
}
|
|
1156
1220
|
});
|
|
1157
1221
|
}
|
|
1158
1222
|
|
|
1223
|
+
if (provider === 'opencode') {
|
|
1224
|
+
const dbPath = getOpenCodeDatabasePath();
|
|
1225
|
+
if (!fs.existsSync(dbPath)) {
|
|
1226
|
+
return res.status(404).json({ error: 'OpenCode database not found' });
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
1230
|
+
try {
|
|
1231
|
+
const columns = db.prepare('PRAGMA table_info(session)').all();
|
|
1232
|
+
const columnNames = new Set(columns.map((column) => column.name));
|
|
1233
|
+
const requiredColumns = ['tokens_input', 'tokens_output', 'tokens_reasoning', 'tokens_cache_read', 'tokens_cache_write'];
|
|
1234
|
+
if (!requiredColumns.every((column) => columnNames.has(column))) {
|
|
1235
|
+
return res.json({
|
|
1236
|
+
used: 0,
|
|
1237
|
+
inputTokens: 0,
|
|
1238
|
+
outputTokens: 0,
|
|
1239
|
+
breakdown: { input: 0, output: 0 },
|
|
1240
|
+
unsupported: true,
|
|
1241
|
+
message: 'Token usage tracking is not available in this OpenCode database schema'
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const row = db.prepare(`
|
|
1246
|
+
SELECT
|
|
1247
|
+
tokens_input AS inputTokens,
|
|
1248
|
+
tokens_output AS outputTokens,
|
|
1249
|
+
tokens_reasoning AS reasoningTokens,
|
|
1250
|
+
tokens_cache_read AS cacheReadTokens,
|
|
1251
|
+
tokens_cache_write AS cacheWriteTokens
|
|
1252
|
+
FROM session
|
|
1253
|
+
WHERE id = ?
|
|
1254
|
+
`).get(safeSessionId);
|
|
1255
|
+
|
|
1256
|
+
if (!row) {
|
|
1257
|
+
return res.status(404).json({ error: 'OpenCode session not found', sessionId: safeSessionId });
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const inputTokens = Number(row.inputTokens || 0) + Number(row.cacheReadTokens || 0);
|
|
1261
|
+
const outputTokens = Number(row.outputTokens || 0);
|
|
1262
|
+
const totalUsed = Number(row.inputTokens || 0)
|
|
1263
|
+
+ outputTokens
|
|
1264
|
+
+ Number(row.reasoningTokens || 0)
|
|
1265
|
+
+ Number(row.cacheReadTokens || 0)
|
|
1266
|
+
+ Number(row.cacheWriteTokens || 0);
|
|
1267
|
+
|
|
1268
|
+
return res.json({
|
|
1269
|
+
used: totalUsed,
|
|
1270
|
+
inputTokens,
|
|
1271
|
+
outputTokens,
|
|
1272
|
+
breakdown: {
|
|
1273
|
+
input: inputTokens,
|
|
1274
|
+
output: outputTokens
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
} finally {
|
|
1278
|
+
db.close();
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1159
1282
|
// Handle Codex sessions
|
|
1160
1283
|
if (provider === 'codex') {
|
|
1161
1284
|
const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
|
|
@@ -1196,6 +1319,8 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1196
1319
|
throw error;
|
|
1197
1320
|
}
|
|
1198
1321
|
const lines = fileContent.trim().split('\n');
|
|
1322
|
+
let inputTokens = 0;
|
|
1323
|
+
let outputTokens = 0;
|
|
1199
1324
|
let totalTokens = 0;
|
|
1200
1325
|
let contextWindow = 200000; // Default for Codex/OpenAI
|
|
1201
1326
|
|
|
@@ -1208,7 +1333,9 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1208
1333
|
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
|
1209
1334
|
const tokenInfo = entry.payload.info;
|
|
1210
1335
|
if (tokenInfo.total_token_usage) {
|
|
1211
|
-
|
|
1336
|
+
inputTokens = tokenInfo.total_token_usage.input_tokens || 0;
|
|
1337
|
+
outputTokens = tokenInfo.total_token_usage.output_tokens || 0;
|
|
1338
|
+
totalTokens = tokenInfo.total_token_usage.total_tokens || inputTokens + outputTokens;
|
|
1212
1339
|
}
|
|
1213
1340
|
if (tokenInfo.model_context_window) {
|
|
1214
1341
|
contextWindow = tokenInfo.model_context_window;
|
|
@@ -1223,7 +1350,13 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1223
1350
|
|
|
1224
1351
|
return res.json({
|
|
1225
1352
|
used: totalTokens,
|
|
1226
|
-
total: contextWindow
|
|
1353
|
+
total: contextWindow,
|
|
1354
|
+
inputTokens,
|
|
1355
|
+
outputTokens,
|
|
1356
|
+
breakdown: {
|
|
1357
|
+
input: inputTokens,
|
|
1358
|
+
output: outputTokens
|
|
1359
|
+
}
|
|
1227
1360
|
});
|
|
1228
1361
|
}
|
|
1229
1362
|
|
|
@@ -1266,8 +1399,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1266
1399
|
const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
|
|
1267
1400
|
const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
|
|
1268
1401
|
let inputTokens = 0;
|
|
1269
|
-
let
|
|
1270
|
-
let cacheReadTokens = 0;
|
|
1402
|
+
let outputTokens = 0;
|
|
1271
1403
|
|
|
1272
1404
|
// Find the latest assistant message with usage data (scan from end)
|
|
1273
1405
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -1280,8 +1412,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1280
1412
|
|
|
1281
1413
|
// Use token counts from latest assistant message only
|
|
1282
1414
|
inputTokens = usage.input_tokens || 0;
|
|
1283
|
-
|
|
1284
|
-
cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
1415
|
+
outputTokens = usage.output_tokens || 0;
|
|
1285
1416
|
|
|
1286
1417
|
break; // Stop after finding the latest assistant message
|
|
1287
1418
|
}
|
|
@@ -1291,16 +1422,16 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1291
1422
|
}
|
|
1292
1423
|
}
|
|
1293
1424
|
|
|
1294
|
-
|
|
1295
|
-
const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
|
|
1425
|
+
const totalUsed = inputTokens + outputTokens;
|
|
1296
1426
|
|
|
1297
1427
|
res.json({
|
|
1298
1428
|
used: totalUsed,
|
|
1299
1429
|
total: contextWindow,
|
|
1430
|
+
inputTokens,
|
|
1431
|
+
outputTokens,
|
|
1300
1432
|
breakdown: {
|
|
1301
1433
|
input: inputTokens,
|
|
1302
|
-
|
|
1303
|
-
cacheRead: cacheReadTokens
|
|
1434
|
+
output: outputTokens
|
|
1304
1435
|
}
|
|
1305
1436
|
});
|
|
1306
1437
|
} catch (error) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { initializeDatabase } from '@/modules/database/init-db.js';
|
|
2
|
+
export { closeConnection, getConnection, getDatabasePath } from '@/modules/database/connection.js';
|
|
2
3
|
export { apiKeysDb } from '@/modules/database/repositories/api-keys.js';
|
|
3
4
|
export { appConfigDb } from '@/modules/database/repositories/app-config.js';
|
|
4
5
|
export { credentialsDb } from '@/modules/database/repositories/credentials.js';
|
|
@@ -41,6 +41,7 @@ type ProjectApiView = {
|
|
|
41
41
|
cursorSessions: [];
|
|
42
42
|
codexSessions: [];
|
|
43
43
|
geminiSessions: [];
|
|
44
|
+
opencodeSessions: [];
|
|
44
45
|
sessionMeta: {
|
|
45
46
|
hasMore: false;
|
|
46
47
|
total: 0;
|
|
@@ -93,6 +94,7 @@ function mapProjectRowToApiView(projectRow: ProjectRepositoryRow): ProjectApiVie
|
|
|
93
94
|
cursorSessions: [],
|
|
94
95
|
codexSessions: [],
|
|
95
96
|
geminiSessions: [],
|
|
97
|
+
opencodeSessions: [],
|
|
96
98
|
sessionMeta: {
|
|
97
99
|
hasMore: false,
|
|
98
100
|
total: 0,
|
|
@@ -14,7 +14,7 @@ type SessionSummary = {
|
|
|
14
14
|
lastActivity: string;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
type SessionsByProvider = Record<'claude' | 'cursor' | 'codex' | 'gemini', SessionSummary[]>;
|
|
17
|
+
type SessionsByProvider = Record<'claude' | 'cursor' | 'codex' | 'gemini' | 'opencode', SessionSummary[]>;
|
|
18
18
|
|
|
19
19
|
type SessionRepositoryRow = {
|
|
20
20
|
provider: string;
|
|
@@ -34,6 +34,7 @@ export type ProjectListItem = {
|
|
|
34
34
|
cursorSessions: SessionSummary[];
|
|
35
35
|
codexSessions: SessionSummary[];
|
|
36
36
|
geminiSessions: SessionSummary[];
|
|
37
|
+
opencodeSessions: SessionSummary[];
|
|
37
38
|
sessionMeta: {
|
|
38
39
|
hasMore: boolean;
|
|
39
40
|
total: number;
|
|
@@ -74,6 +75,7 @@ export type ProjectSessionsPageApiView = {
|
|
|
74
75
|
cursorSessions: SessionSummary[];
|
|
75
76
|
codexSessions: SessionSummary[];
|
|
76
77
|
geminiSessions: SessionSummary[];
|
|
78
|
+
opencodeSessions: SessionSummary[];
|
|
77
79
|
sessionMeta: {
|
|
78
80
|
hasMore: boolean;
|
|
79
81
|
total: number;
|
|
@@ -139,6 +141,7 @@ function bucketSessionRowsByProvider(rows: SessionRepositoryRow[]): SessionsByPr
|
|
|
139
141
|
cursor: [],
|
|
140
142
|
codex: [],
|
|
141
143
|
gemini: [],
|
|
144
|
+
opencode: [],
|
|
142
145
|
};
|
|
143
146
|
|
|
144
147
|
for (const row of rows) {
|
|
@@ -257,6 +260,7 @@ export async function getProjectsWithSessions(
|
|
|
257
260
|
cursorSessions: sessionsPage.sessionsByProvider.cursor,
|
|
258
261
|
codexSessions: sessionsPage.sessionsByProvider.codex,
|
|
259
262
|
geminiSessions: sessionsPage.sessionsByProvider.gemini,
|
|
263
|
+
opencodeSessions: sessionsPage.sessionsByProvider.opencode,
|
|
260
264
|
sessionMeta: {
|
|
261
265
|
hasMore: sessionsPage.hasMore,
|
|
262
266
|
total: sessionsPage.total,
|
|
@@ -314,6 +318,7 @@ export async function getArchivedProjectsWithSessions(
|
|
|
314
318
|
cursorSessions: sessionsPage.sessionsByProvider.cursor,
|
|
315
319
|
codexSessions: sessionsPage.sessionsByProvider.codex,
|
|
316
320
|
geminiSessions: sessionsPage.sessionsByProvider.gemini,
|
|
321
|
+
opencodeSessions: sessionsPage.sessionsByProvider.opencode,
|
|
317
322
|
sessionMeta: {
|
|
318
323
|
hasMore: sessionsPage.hasMore,
|
|
319
324
|
total: sessionsPage.total,
|
|
@@ -347,6 +352,7 @@ export async function getProjectSessionsPage(
|
|
|
347
352
|
cursorSessions: sessionsPage.sessionsByProvider.cursor,
|
|
348
353
|
codexSessions: sessionsPage.sessionsByProvider.codex,
|
|
349
354
|
geminiSessions: sessionsPage.sessionsByProvider.gemini,
|
|
355
|
+
opencodeSessions: sessionsPage.sessionsByProvider.opencode,
|
|
350
356
|
sessionMeta: {
|
|
351
357
|
hasMore: sessionsPage.hasMore,
|
|
352
358
|
total: sessionsPage.total,
|