@cloudcli-ai/cloudcli 1.32.0 → 1.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-BsyL_xmI.js +1348 -0
- package/dist/assets/index-FSP9Hz7V.css +32 -0
- package/dist/index.html +2 -2
- package/dist/modelConstants.js +848 -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 +234 -67
- 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-auth.provider.js +28 -6
- 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 +146 -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 +226 -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 +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 +298 -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 +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 +18 -3
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/websocket-auth.service.js +6 -2
- package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/websocket-server.service.js +18 -0
- package/dist-server/server/modules/websocket/services/websocket-server.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 +5 -2
- package/public/api-docs.html +878 -0
- package/public/modelConstants.js +848 -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 +259 -75
- 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-auth.provider.ts +34 -6
- package/server/modules/providers/list/claude/claude-models.provider.ts +196 -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 +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 +62 -2
- package/server/modules/providers/services/mcp.service.ts +1 -12
- package/server/modules/providers/services/provider-models.service.ts +358 -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 +334 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +349 -0
- package/server/modules/providers/tests/skills.test.ts +66 -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 +22 -2
- package/server/modules/websocket/services/websocket-auth.service.ts +7 -2
- package/server/modules/websocket/services/websocket-server.service.ts +18 -0
- package/server/openai-codex.js +41 -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-AaSE7-3t.js +0 -1336
- package/dist/assets/index-CKaku72N.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/index.js
CHANGED
|
@@ -10,8 +10,9 @@ 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 { AppError, WORKSPACES_ROOT, validateWorkspacePath } from '@/shared/utils.js';
|
|
15
|
+
import { AppError, WORKSPACES_ROOT, getOpenCodeDatabasePath, validateWorkspacePath } from '@/shared/utils.js';
|
|
15
16
|
import { closeSessionsWatcher, initializeSessionsWatcher } from '@/modules/providers/index.js';
|
|
16
17
|
import { createWebSocketServer } from '@/modules/websocket/index.js';
|
|
17
18
|
|
|
@@ -45,6 +46,12 @@ import {
|
|
|
45
46
|
isGeminiSessionActive,
|
|
46
47
|
getActiveGeminiSessions,
|
|
47
48
|
} from './gemini-cli.js';
|
|
49
|
+
import {
|
|
50
|
+
spawnOpenCode,
|
|
51
|
+
abortOpenCodeSession,
|
|
52
|
+
isOpenCodeSessionActive,
|
|
53
|
+
getActiveOpenCodeSessions,
|
|
54
|
+
} from './opencode-cli.js';
|
|
48
55
|
import sessionManager from './sessionManager.js';
|
|
49
56
|
import {
|
|
50
57
|
stripAnsiSequences,
|
|
@@ -66,7 +73,7 @@ import geminiRoutes from './routes/gemini.js';
|
|
|
66
73
|
import pluginsRoutes from './routes/plugins.js';
|
|
67
74
|
import providerRoutes from './modules/providers/provider.routes.js';
|
|
68
75
|
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
|
|
69
|
-
import { initializeDatabase, projectsDb } from './modules/database/index.js';
|
|
76
|
+
import { initializeDatabase, projectsDb, sessionsDb } from './modules/database/index.js';
|
|
70
77
|
import { configureWebPush } from './services/vapid-keys.js';
|
|
71
78
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
72
79
|
import { IS_PLATFORM } from './constants/config.js';
|
|
@@ -94,21 +101,25 @@ const wss = createWebSocketServer(server, {
|
|
|
94
101
|
spawnCursor,
|
|
95
102
|
queryCodex,
|
|
96
103
|
spawnGemini,
|
|
104
|
+
spawnOpenCode,
|
|
97
105
|
abortClaudeSDKSession,
|
|
98
106
|
abortCursorSession,
|
|
99
107
|
abortCodexSession,
|
|
100
108
|
abortGeminiSession,
|
|
109
|
+
abortOpenCodeSession,
|
|
101
110
|
resolveToolApproval,
|
|
102
111
|
isClaudeSDKSessionActive,
|
|
103
112
|
isCursorSessionActive,
|
|
104
113
|
isCodexSessionActive,
|
|
105
114
|
isGeminiSessionActive,
|
|
115
|
+
isOpenCodeSessionActive,
|
|
106
116
|
reconnectSessionWriter,
|
|
107
117
|
getPendingApprovalsForSession,
|
|
108
118
|
getActiveClaudeSDKSessions,
|
|
109
119
|
getActiveCursorSessions,
|
|
110
120
|
getActiveCodexSessions,
|
|
111
121
|
getActiveGeminiSessions,
|
|
122
|
+
getActiveOpenCodeSessions,
|
|
112
123
|
},
|
|
113
124
|
shell: {
|
|
114
125
|
getSessionById: (sessionId) => sessionManager.getSession(sessionId),
|
|
@@ -1131,23 +1142,129 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1131
1142
|
return res.json({
|
|
1132
1143
|
used: 0,
|
|
1133
1144
|
total: 0,
|
|
1134
|
-
|
|
1145
|
+
inputTokens: 0,
|
|
1146
|
+
outputTokens: 0,
|
|
1147
|
+
breakdown: { input: 0, output: 0 },
|
|
1135
1148
|
unsupported: true,
|
|
1136
1149
|
message: 'Token usage tracking not available for Cursor sessions'
|
|
1137
1150
|
});
|
|
1138
1151
|
}
|
|
1139
1152
|
|
|
1140
|
-
// Handle Gemini sessions - they are raw logs in our current setup
|
|
1141
1153
|
if (provider === 'gemini') {
|
|
1154
|
+
const session = sessionsDb.getSessionById(safeSessionId);
|
|
1155
|
+
const sessionFilePath = session?.jsonl_path;
|
|
1156
|
+
if (!sessionFilePath) {
|
|
1157
|
+
return res.json({
|
|
1158
|
+
used: 0,
|
|
1159
|
+
inputTokens: 0,
|
|
1160
|
+
outputTokens: 0,
|
|
1161
|
+
breakdown: { input: 0, output: 0 },
|
|
1162
|
+
unsupported: true,
|
|
1163
|
+
message: 'Token usage tracking not available for this Gemini session'
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
let fileContent;
|
|
1168
|
+
try {
|
|
1169
|
+
fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
if (error.code === 'ENOENT') {
|
|
1172
|
+
return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
|
|
1173
|
+
}
|
|
1174
|
+
throw error;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const lines = fileContent.trim().split('\n');
|
|
1178
|
+
let inputTokens = 0;
|
|
1179
|
+
let outputTokens = 0;
|
|
1180
|
+
let totalTokens = 0;
|
|
1181
|
+
|
|
1182
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1183
|
+
try {
|
|
1184
|
+
const entry = JSON.parse(lines[i]);
|
|
1185
|
+
if (!entry.tokens || typeof entry.tokens !== 'object') {
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
inputTokens = Number(entry.tokens.input || 0);
|
|
1190
|
+
outputTokens = Number(entry.tokens.output || 0);
|
|
1191
|
+
totalTokens = Number(entry.tokens.total || inputTokens + outputTokens || 0);
|
|
1192
|
+
break;
|
|
1193
|
+
} catch {
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1142
1198
|
return res.json({
|
|
1143
|
-
used:
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1199
|
+
used: totalTokens,
|
|
1200
|
+
inputTokens,
|
|
1201
|
+
outputTokens,
|
|
1202
|
+
breakdown: {
|
|
1203
|
+
input: inputTokens,
|
|
1204
|
+
output: outputTokens
|
|
1205
|
+
}
|
|
1148
1206
|
});
|
|
1149
1207
|
}
|
|
1150
1208
|
|
|
1209
|
+
if (provider === 'opencode') {
|
|
1210
|
+
const dbPath = getOpenCodeDatabasePath();
|
|
1211
|
+
if (!fs.existsSync(dbPath)) {
|
|
1212
|
+
return res.status(404).json({ error: 'OpenCode database not found' });
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
1216
|
+
try {
|
|
1217
|
+
const columns = db.prepare('PRAGMA table_info(session)').all();
|
|
1218
|
+
const columnNames = new Set(columns.map((column) => column.name));
|
|
1219
|
+
const requiredColumns = ['tokens_input', 'tokens_output', 'tokens_reasoning', 'tokens_cache_read', 'tokens_cache_write'];
|
|
1220
|
+
if (!requiredColumns.every((column) => columnNames.has(column))) {
|
|
1221
|
+
return res.json({
|
|
1222
|
+
used: 0,
|
|
1223
|
+
inputTokens: 0,
|
|
1224
|
+
outputTokens: 0,
|
|
1225
|
+
breakdown: { input: 0, output: 0 },
|
|
1226
|
+
unsupported: true,
|
|
1227
|
+
message: 'Token usage tracking is not available in this OpenCode database schema'
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const row = db.prepare(`
|
|
1232
|
+
SELECT
|
|
1233
|
+
tokens_input AS inputTokens,
|
|
1234
|
+
tokens_output AS outputTokens,
|
|
1235
|
+
tokens_reasoning AS reasoningTokens,
|
|
1236
|
+
tokens_cache_read AS cacheReadTokens,
|
|
1237
|
+
tokens_cache_write AS cacheWriteTokens
|
|
1238
|
+
FROM session
|
|
1239
|
+
WHERE id = ?
|
|
1240
|
+
`).get(safeSessionId);
|
|
1241
|
+
|
|
1242
|
+
if (!row) {
|
|
1243
|
+
return res.status(404).json({ error: 'OpenCode session not found', sessionId: safeSessionId });
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const inputTokens = Number(row.inputTokens || 0) + Number(row.cacheReadTokens || 0);
|
|
1247
|
+
const outputTokens = Number(row.outputTokens || 0);
|
|
1248
|
+
const totalUsed = Number(row.inputTokens || 0)
|
|
1249
|
+
+ outputTokens
|
|
1250
|
+
+ Number(row.reasoningTokens || 0)
|
|
1251
|
+
+ Number(row.cacheReadTokens || 0)
|
|
1252
|
+
+ Number(row.cacheWriteTokens || 0);
|
|
1253
|
+
|
|
1254
|
+
return res.json({
|
|
1255
|
+
used: totalUsed,
|
|
1256
|
+
inputTokens,
|
|
1257
|
+
outputTokens,
|
|
1258
|
+
breakdown: {
|
|
1259
|
+
input: inputTokens,
|
|
1260
|
+
output: outputTokens
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
} finally {
|
|
1264
|
+
db.close();
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1151
1268
|
// Handle Codex sessions
|
|
1152
1269
|
if (provider === 'codex') {
|
|
1153
1270
|
const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
|
|
@@ -1188,6 +1305,8 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1188
1305
|
throw error;
|
|
1189
1306
|
}
|
|
1190
1307
|
const lines = fileContent.trim().split('\n');
|
|
1308
|
+
let inputTokens = 0;
|
|
1309
|
+
let outputTokens = 0;
|
|
1191
1310
|
let totalTokens = 0;
|
|
1192
1311
|
let contextWindow = 200000; // Default for Codex/OpenAI
|
|
1193
1312
|
|
|
@@ -1200,7 +1319,9 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1200
1319
|
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
|
1201
1320
|
const tokenInfo = entry.payload.info;
|
|
1202
1321
|
if (tokenInfo.total_token_usage) {
|
|
1203
|
-
|
|
1322
|
+
inputTokens = tokenInfo.total_token_usage.input_tokens || 0;
|
|
1323
|
+
outputTokens = tokenInfo.total_token_usage.output_tokens || 0;
|
|
1324
|
+
totalTokens = tokenInfo.total_token_usage.total_tokens || inputTokens + outputTokens;
|
|
1204
1325
|
}
|
|
1205
1326
|
if (tokenInfo.model_context_window) {
|
|
1206
1327
|
contextWindow = tokenInfo.model_context_window;
|
|
@@ -1215,7 +1336,13 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1215
1336
|
|
|
1216
1337
|
return res.json({
|
|
1217
1338
|
used: totalTokens,
|
|
1218
|
-
total: contextWindow
|
|
1339
|
+
total: contextWindow,
|
|
1340
|
+
inputTokens,
|
|
1341
|
+
outputTokens,
|
|
1342
|
+
breakdown: {
|
|
1343
|
+
input: inputTokens,
|
|
1344
|
+
output: outputTokens
|
|
1345
|
+
}
|
|
1219
1346
|
});
|
|
1220
1347
|
}
|
|
1221
1348
|
|
|
@@ -1258,8 +1385,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1258
1385
|
const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
|
|
1259
1386
|
const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
|
|
1260
1387
|
let inputTokens = 0;
|
|
1261
|
-
let
|
|
1262
|
-
let cacheReadTokens = 0;
|
|
1388
|
+
let outputTokens = 0;
|
|
1263
1389
|
|
|
1264
1390
|
// Find the latest assistant message with usage data (scan from end)
|
|
1265
1391
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -1272,8 +1398,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1272
1398
|
|
|
1273
1399
|
// Use token counts from latest assistant message only
|
|
1274
1400
|
inputTokens = usage.input_tokens || 0;
|
|
1275
|
-
|
|
1276
|
-
cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
1401
|
+
outputTokens = usage.output_tokens || 0;
|
|
1277
1402
|
|
|
1278
1403
|
break; // Stop after finding the latest assistant message
|
|
1279
1404
|
}
|
|
@@ -1283,16 +1408,16 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1283
1408
|
}
|
|
1284
1409
|
}
|
|
1285
1410
|
|
|
1286
|
-
|
|
1287
|
-
const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
|
|
1411
|
+
const totalUsed = inputTokens + outputTokens;
|
|
1288
1412
|
|
|
1289
1413
|
res.json({
|
|
1290
1414
|
used: totalUsed,
|
|
1291
1415
|
total: contextWindow,
|
|
1416
|
+
inputTokens,
|
|
1417
|
+
outputTokens,
|
|
1292
1418
|
breakdown: {
|
|
1293
1419
|
input: inputTokens,
|
|
1294
|
-
|
|
1295
|
-
cacheRead: cacheReadTokens
|
|
1420
|
+
output: outputTokens
|
|
1296
1421
|
}
|
|
1297
1422
|
});
|
|
1298
1423
|
} catch (error) {
|
|
@@ -1358,74 +1483,133 @@ function permToRwx(perm) {
|
|
|
1358
1483
|
return r + w + x;
|
|
1359
1484
|
}
|
|
1360
1485
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1486
|
+
// Directories that are almost never interesting for a project tree but can
|
|
1487
|
+
// contain tens of thousands of files. Skipping them before recursion keeps
|
|
1488
|
+
// traversal time bounded on large monorepos and high-latency filesystems
|
|
1489
|
+
// (NFS / SMB).
|
|
1490
|
+
const IGNORED_DIRS = new Set([
|
|
1491
|
+
// JS / TS toolchains
|
|
1492
|
+
'node_modules', 'dist', 'build', '.next', '.nuxt', '.cache', '.parcel-cache',
|
|
1493
|
+
// VCS
|
|
1494
|
+
'.git', '.svn', '.hg',
|
|
1495
|
+
// Python
|
|
1496
|
+
'__pycache__', '.pytest_cache', '.mypy_cache', '.tox', 'venv', '.venv',
|
|
1497
|
+
// Rust / Go / Java / Ruby
|
|
1498
|
+
'target', 'vendor',
|
|
1499
|
+
// Build output / IDE
|
|
1500
|
+
'.gradle', '.idea', 'coverage', '.nyc_output'
|
|
1501
|
+
]);
|
|
1502
|
+
|
|
1503
|
+
const DEFAULT_FS_CONCURRENCY = 64;
|
|
1504
|
+
const parsedFsConcurrency = Number.parseInt(process.env.FS_CONCURRENCY || '', 10);
|
|
1505
|
+
const FS_CONCURRENCY = Number.isFinite(parsedFsConcurrency) && parsedFsConcurrency > 0
|
|
1506
|
+
? parsedFsConcurrency
|
|
1507
|
+
: DEFAULT_FS_CONCURRENCY;
|
|
1508
|
+
let activeFsOperations = 0;
|
|
1509
|
+
const pendingFsOperations = [];
|
|
1510
|
+
|
|
1511
|
+
async function acquire() {
|
|
1512
|
+
if (activeFsOperations < FS_CONCURRENCY) {
|
|
1513
|
+
activeFsOperations += 1;
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1379
1516
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
type: entry.isDirectory() ? 'directory' : 'file'
|
|
1385
|
-
};
|
|
1517
|
+
await new Promise((resolve) => {
|
|
1518
|
+
pendingFsOperations.push(resolve);
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1386
1521
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
// Convert permissions to rwx format
|
|
1394
|
-
const mode = stats.mode;
|
|
1395
|
-
const ownerPerm = (mode >> 6) & 7;
|
|
1396
|
-
const groupPerm = (mode >> 3) & 7;
|
|
1397
|
-
const otherPerm = mode & 7;
|
|
1398
|
-
item.permissions = ((mode >> 6) & 7).toString() + ((mode >> 3) & 7).toString() + (mode & 7).toString();
|
|
1399
|
-
item.permissionsRwx = permToRwx(ownerPerm) + permToRwx(groupPerm) + permToRwx(otherPerm);
|
|
1400
|
-
} catch (statError) {
|
|
1401
|
-
// If stat fails, provide default values
|
|
1402
|
-
item.size = 0;
|
|
1403
|
-
item.modified = null;
|
|
1404
|
-
item.permissions = '000';
|
|
1405
|
-
item.permissionsRwx = '---------';
|
|
1406
|
-
}
|
|
1522
|
+
function release() {
|
|
1523
|
+
const next = pendingFsOperations.shift();
|
|
1524
|
+
if (next) {
|
|
1525
|
+
next();
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1407
1528
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
try {
|
|
1411
|
-
// Check if we can access the directory before trying to read it
|
|
1412
|
-
await fsPromises.access(item.path, fs.constants.R_OK);
|
|
1413
|
-
item.children = await getFileTree(item.path, maxDepth, currentDepth + 1, showHidden);
|
|
1414
|
-
} catch (e) {
|
|
1415
|
-
// Silently skip directories we can't access (permission denied, etc.)
|
|
1416
|
-
item.children = [];
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1529
|
+
activeFsOperations = Math.max(0, activeFsOperations - 1);
|
|
1530
|
+
}
|
|
1419
1531
|
|
|
1420
|
-
|
|
1532
|
+
async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true) {
|
|
1533
|
+
// Using fsPromises from import
|
|
1534
|
+
let entries;
|
|
1535
|
+
try {
|
|
1536
|
+
await acquire();
|
|
1537
|
+
try {
|
|
1538
|
+
entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
|
|
1539
|
+
} finally {
|
|
1540
|
+
release();
|
|
1421
1541
|
}
|
|
1422
1542
|
} catch (error) {
|
|
1423
1543
|
// Only log non-permission errors to avoid spam
|
|
1424
1544
|
if (error.code !== 'EACCES' && error.code !== 'EPERM') {
|
|
1425
1545
|
console.error('Error reading directory:', error);
|
|
1426
1546
|
}
|
|
1547
|
+
return [];
|
|
1427
1548
|
}
|
|
1428
1549
|
|
|
1550
|
+
const filteredEntries = entries.filter((entry) => !(entry.isDirectory() && IGNORED_DIRS.has(entry.name)));
|
|
1551
|
+
|
|
1552
|
+
// Process every entry in parallel. On high-latency filesystems (NFS/SMB)
|
|
1553
|
+
// serial stat() was the real bottleneck — issuing them concurrently lets
|
|
1554
|
+
// the kernel pipeline the round-trips and the recursive calls overlap too.
|
|
1555
|
+
const items = await Promise.all(filteredEntries.map(async (entry) => {
|
|
1556
|
+
const itemPath = path.join(dirPath, entry.name);
|
|
1557
|
+
const item = {
|
|
1558
|
+
name: entry.name,
|
|
1559
|
+
path: itemPath,
|
|
1560
|
+
type: entry.isDirectory() ? 'directory' : 'file'
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
// Get file stats for additional metadata
|
|
1564
|
+
try {
|
|
1565
|
+
await acquire();
|
|
1566
|
+
try {
|
|
1567
|
+
const stats = await fsPromises.lstat(itemPath);
|
|
1568
|
+
item.size = stats.size;
|
|
1569
|
+
item.modified = stats.mtime.toISOString();
|
|
1570
|
+
|
|
1571
|
+
// Mark symlinks so UI can distinguish them
|
|
1572
|
+
if (stats.isSymbolicLink()) {
|
|
1573
|
+
item.isSymlink = true;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// Convert permissions to rwx format
|
|
1577
|
+
const mode = stats.mode;
|
|
1578
|
+
const ownerPerm = (mode >> 6) & 7;
|
|
1579
|
+
const groupPerm = (mode >> 3) & 7;
|
|
1580
|
+
const otherPerm = mode & 7;
|
|
1581
|
+
item.permissions =
|
|
1582
|
+
((mode >> 6) & 7).toString() +
|
|
1583
|
+
((mode >> 3) & 7).toString() +
|
|
1584
|
+
(mode & 7).toString();
|
|
1585
|
+
item.permissionsRwx =
|
|
1586
|
+
permToRwx(ownerPerm) +
|
|
1587
|
+
permToRwx(groupPerm) +
|
|
1588
|
+
permToRwx(otherPerm);
|
|
1589
|
+
} finally {
|
|
1590
|
+
release();
|
|
1591
|
+
}
|
|
1592
|
+
} catch (statError) {
|
|
1593
|
+
// If stat fails, provide default values
|
|
1594
|
+
item.size = 0;
|
|
1595
|
+
item.modified = null;
|
|
1596
|
+
item.permissions = '000';
|
|
1597
|
+
item.permissionsRwx = '---------';
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if (entry.isDirectory() && currentDepth < maxDepth) {
|
|
1601
|
+
// Recurse. Let readdir's own EACCES bubble up through the catch in
|
|
1602
|
+
// the recursive call rather than doing a separate access() probe
|
|
1603
|
+
// (which doubled the round-trip count on SMB without adding info).
|
|
1604
|
+
// The recursive call starts with a bounded readdir; holding a permit
|
|
1605
|
+
// for the whole subtree can deadlock when sibling directories are
|
|
1606
|
+
// waiting on their own children.
|
|
1607
|
+
item.children = await getFileTree(itemPath, maxDepth, currentDepth + 1, showHidden);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
return item;
|
|
1611
|
+
}));
|
|
1612
|
+
|
|
1429
1613
|
return items.sort((a, b) => {
|
|
1430
1614
|
if (a.type !== b.type) {
|
|
1431
1615
|
return a.type === 'directory' ? -1 : 1;
|
|
@@ -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';
|
|
@@ -33,6 +33,7 @@ type ProjectApiView = {
|
|
|
33
33
|
cursorSessions: [];
|
|
34
34
|
codexSessions: [];
|
|
35
35
|
geminiSessions: [];
|
|
36
|
+
opencodeSessions: [];
|
|
36
37
|
sessionMeta: {
|
|
37
38
|
hasMore: false;
|
|
38
39
|
total: 0;
|
|
@@ -84,6 +85,7 @@ function mapProjectRowToApiView(projectRow: ProjectRepositoryRow): ProjectApiVie
|
|
|
84
85
|
cursorSessions: [],
|
|
85
86
|
codexSessions: [],
|
|
86
87
|
geminiSessions: [],
|
|
88
|
+
opencodeSessions: [],
|
|
87
89
|
sessionMeta: {
|
|
88
90
|
hasMore: false,
|
|
89
91
|
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) {
|
|
@@ -253,6 +256,7 @@ export async function getProjectsWithSessions(
|
|
|
253
256
|
cursorSessions: sessionsPage.sessionsByProvider.cursor,
|
|
254
257
|
codexSessions: sessionsPage.sessionsByProvider.codex,
|
|
255
258
|
geminiSessions: sessionsPage.sessionsByProvider.gemini,
|
|
259
|
+
opencodeSessions: sessionsPage.sessionsByProvider.opencode,
|
|
256
260
|
sessionMeta: {
|
|
257
261
|
hasMore: sessionsPage.hasMore,
|
|
258
262
|
total: sessionsPage.total,
|
|
@@ -309,6 +313,7 @@ export async function getArchivedProjectsWithSessions(
|
|
|
309
313
|
cursorSessions: sessionsPage.sessionsByProvider.cursor,
|
|
310
314
|
codexSessions: sessionsPage.sessionsByProvider.codex,
|
|
311
315
|
geminiSessions: sessionsPage.sessionsByProvider.gemini,
|
|
316
|
+
opencodeSessions: sessionsPage.sessionsByProvider.opencode,
|
|
312
317
|
sessionMeta: {
|
|
313
318
|
hasMore: sessionsPage.hasMore,
|
|
314
319
|
total: sessionsPage.total,
|
|
@@ -341,6 +346,7 @@ export async function getProjectSessionsPage(
|
|
|
341
346
|
cursorSessions: sessionsPage.sessionsByProvider.cursor,
|
|
342
347
|
codexSessions: sessionsPage.sessionsByProvider.codex,
|
|
343
348
|
geminiSessions: sessionsPage.sessionsByProvider.gemini,
|
|
349
|
+
opencodeSessions: sessionsPage.sessionsByProvider.opencode,
|
|
344
350
|
sessionMeta: {
|
|
345
351
|
hasMore: sessionsPage.hasMore,
|
|
346
352
|
total: sessionsPage.total,
|
|
@@ -37,6 +37,7 @@ Current provider ids in this repo are:
|
|
|
37
37
|
- `codex`
|
|
38
38
|
- `cursor`
|
|
39
39
|
- `gemini`
|
|
40
|
+
- `opencode`
|
|
40
41
|
|
|
41
42
|
Those ids are mirrored in backend unions and frontend provider constants. If
|
|
42
43
|
adding a new provider, update every place that hardcodes this list.
|
|
@@ -55,7 +56,8 @@ server/modules/providers/list/<provider>/
|
|
|
55
56
|
<provider>-session-synchronizer.provider.ts
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
The existing provider folders are `claude`, `codex`, `cursor`,
|
|
59
|
+
The existing provider folders are `claude`, `codex`, `cursor`, `gemini`, and
|
|
60
|
+
`opencode`.
|
|
59
61
|
|
|
60
62
|
## What Each Facet Does
|
|
61
63
|
|
|
@@ -81,7 +83,7 @@ The existing provider folders are `claude`, `codex`, `cursor`, and `gemini`.
|
|
|
81
83
|
- Update `server/modules/providers/provider.routes.ts`.
|
|
82
84
|
- Update `server/routes/agent.js` if the provider is launchable from the agent runtime.
|
|
83
85
|
- Update `server/index.js` if the provider needs runtime boot or shutdown wiring.
|
|
84
|
-
- Update `
|
|
86
|
+
- Update `public/modelConstants.js` if the provider appears in README or public API docs.
|
|
85
87
|
- Update `src/components/chat/hooks/useChatProviderState.ts` and
|
|
86
88
|
`src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx` if
|
|
87
89
|
the provider should be selectable in chat.
|
|
@@ -122,6 +124,7 @@ Current MCP formats in this repo are:
|
|
|
122
124
|
| Codex | `.codex/config.toml` | `user`, `project` | `stdio`, `http` |
|
|
123
125
|
| Cursor | `.cursor/mcp.json` | `user`, `project` | `stdio`, `http` |
|
|
124
126
|
| Gemini | `.gemini/settings.json` | `user`, `project` | `stdio`, `http` |
|
|
127
|
+
| OpenCode | `~/.config/opencode/opencode.json` or `<workspace>/opencode.json` (`.jsonc` is read when present) | `user`, `project` | `stdio`, `http` |
|
|
125
128
|
|
|
126
129
|
5. Implement skills.
|
|
127
130
|
|
|
@@ -142,6 +145,7 @@ Current skill discovery roots are:
|
|
|
142
145
|
| Codex | `~/.agents/skills`, `~/.codex/skills/.system`, `/etc/codex/skills` | `<workspace>/.agents/skills`, `path.dirname(workspacePath)/.agents/skills`, topmost git root `.agents/skills` | `$` | Overlapping roots are deduplicated before scanning. |
|
|
143
146
|
| Cursor | `~/.cursor/skills` | `<workspace>/.cursor/skills`, `<workspace>/.agents/skills` | `/` | Uses slash-style commands. |
|
|
144
147
|
| Gemini | `~/.gemini/skills`, `~/.agents/skills` | `<workspace>/.gemini/skills`, `<workspace>/.agents/skills` | `/` | Uses slash-style commands. |
|
|
148
|
+
| OpenCode | `~/.config/opencode/skills`, `~/.claude/skills`, `~/.agents/skills` | Cwd-to-topmost-git-root `.opencode/skills`, `.claude/skills`, and `.agents/skills` | `/` | Reuses OpenCode, Claude, and Agents skill locations. Overlapping roots are deduplicated before scanning. |
|
|
145
149
|
|
|
146
150
|
Command forms currently used by the providers are:
|
|
147
151
|
|
|
@@ -150,6 +154,7 @@ Command forms currently used by the providers are:
|
|
|
150
154
|
- Codex skills: `$skill-name`
|
|
151
155
|
- Cursor skills: `/skill-name`
|
|
152
156
|
- Gemini skills: `/skill-name`
|
|
157
|
+
- OpenCode skills: `/skill-name`
|
|
153
158
|
|
|
154
159
|
6. Implement sessions.
|
|
155
160
|
|
|
@@ -187,6 +192,7 @@ Current session sync roots are:
|
|
|
187
192
|
| Codex | `~/.codex/sessions/**/*.jsonl` | Uses `~/.codex/session_index.jsonl` for title lookup and the last `task_complete` message for a fallback title. |
|
|
188
193
|
| Cursor | `~/.cursor/projects/**/*.jsonl` | Uses sibling `worker.log` to recover `workspacePath`, then derives the session title from the first user prompt. |
|
|
189
194
|
| Gemini | `~/.gemini/tmp/**/*.jsonl` | Current full scans only index temp JSONL chat artifacts. Single-file sync also accepts legacy `.json` files. |
|
|
195
|
+
| OpenCode | `~/.local/share/opencode/opencode.db` | Reads active sessions/messages/parts from OpenCode's shared SQLite database and stores `jsonl_path` as `null` so deleting one app session cannot remove the shared DB. |
|
|
190
196
|
|
|
191
197
|
8. Register the provider.
|
|
192
198
|
|
|
@@ -203,10 +209,11 @@ If the provider can run live chat sessions, update the runtime entrypoints too:
|
|
|
203
209
|
|
|
204
210
|
If the provider is visible in the UI, update:
|
|
205
211
|
|
|
206
|
-
- `
|
|
212
|
+
- provider model fallback files under `server/modules/providers/list/<provider>/`
|
|
207
213
|
- `src/components/chat/hooks/useChatProviderState.ts`
|
|
208
214
|
- `src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx`
|
|
209
215
|
- `src/components/provider-auth/view/ProviderLoginModal.tsx`
|
|
216
|
+
- `src/components/mcp/constants.ts`
|
|
210
217
|
|
|
211
218
|
## Minimal Wrapper Template
|
|
212
219
|
|
|
@@ -324,6 +331,7 @@ Useful tests in this repo:
|
|
|
324
331
|
|
|
325
332
|
- `server/modules/providers/tests/mcp.test.ts`
|
|
326
333
|
- `server/modules/providers/tests/skills.test.ts`
|
|
334
|
+
- `server/modules/providers/tests/opencode-sessions.test.ts`
|
|
327
335
|
|
|
328
336
|
If you touch sessions or session synchronization, add or update focused tests
|
|
329
337
|
alongside the implementation.
|