@cloudcli-ai/cloudcli 1.32.0 → 1.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.de.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/README.ru.md +1 -1
  5. package/README.tr.md +1 -1
  6. package/README.zh-CN.md +1 -1
  7. package/dist/api-docs.html +6 -7
  8. package/dist/assets/index-BsyL_xmI.js +1348 -0
  9. package/dist/assets/index-FSP9Hz7V.css +32 -0
  10. package/dist/index.html +2 -2
  11. package/dist/modelConstants.js +848 -0
  12. package/dist-server/server/claude-sdk.js +57 -34
  13. package/dist-server/server/claude-sdk.js.map +1 -1
  14. package/dist-server/server/cursor-cli.js +6 -3
  15. package/dist-server/server/cursor-cli.js.map +1 -1
  16. package/dist-server/server/gemini-cli.js +3 -1
  17. package/dist-server/server/gemini-cli.js.map +1 -1
  18. package/dist-server/server/gemini-response-handler.js +34 -0
  19. package/dist-server/server/gemini-response-handler.js.map +1 -1
  20. package/dist-server/server/index.js +234 -67
  21. package/dist-server/server/index.js.map +1 -1
  22. package/dist-server/server/modules/database/index.js +1 -0
  23. package/dist-server/server/modules/database/index.js.map +1 -1
  24. package/dist-server/server/modules/projects/services/project-management.service.js +1 -0
  25. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
  26. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +4 -0
  27. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  28. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +28 -6
  29. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  30. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +146 -0
  31. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
  32. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  33. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  34. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +84 -0
  35. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
  36. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -39
  37. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
  38. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  39. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  40. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +754 -0
  41. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
  42. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +2 -15
  43. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
  45. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  46. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +27 -0
  47. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -0
  48. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +3 -9
  49. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
  51. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  52. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +92 -0
  53. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
  54. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
  55. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
  56. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +267 -0
  57. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
  58. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +115 -0
  59. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
  60. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +410 -0
  61. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
  62. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
  63. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
  64. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +19 -0
  65. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
  66. package/dist-server/server/modules/providers/provider.registry.js +2 -0
  67. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  68. package/dist-server/server/modules/providers/provider.routes.js +42 -1
  69. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  70. package/dist-server/server/modules/providers/services/mcp.service.js +1 -9
  71. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  72. package/dist-server/server/modules/providers/services/provider-models.service.js +226 -0
  73. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
  74. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
  75. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  76. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +7 -0
  77. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  78. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  79. package/dist-server/server/modules/providers/tests/mcp.test.js +73 -6
  80. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  81. package/dist-server/server/modules/providers/tests/opencode-models.test.js +66 -0
  82. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
  83. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +262 -0
  84. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
  85. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +298 -0
  86. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
  87. package/dist-server/server/modules/providers/tests/skills.test.js +33 -0
  88. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  89. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +12 -1
  90. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  91. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +4 -4
  92. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -1
  93. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +18 -3
  94. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  95. package/dist-server/server/modules/websocket/services/websocket-auth.service.js +6 -2
  96. package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -1
  97. package/dist-server/server/modules/websocket/services/websocket-server.service.js +18 -0
  98. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -1
  99. package/dist-server/server/openai-codex.js +32 -4
  100. package/dist-server/server/openai-codex.js.map +1 -1
  101. package/dist-server/server/opencode-cli.js +287 -0
  102. package/dist-server/server/opencode-cli.js.map +1 -0
  103. package/dist-server/server/opencode-cli.test.js +84 -0
  104. package/dist-server/server/opencode-cli.test.js.map +1 -0
  105. package/dist-server/server/routes/agent.js +21 -8
  106. package/dist-server/server/routes/agent.js.map +1 -1
  107. package/dist-server/server/routes/commands.js +202 -209
  108. package/dist-server/server/routes/commands.js.map +1 -1
  109. package/dist-server/server/routes/cursor.js +2 -2
  110. package/dist-server/server/routes/cursor.js.map +1 -1
  111. package/dist-server/server/routes/settings.js +0 -10
  112. package/dist-server/server/routes/settings.js.map +1 -1
  113. package/dist-server/server/routes/tests/commands.test.js +76 -0
  114. package/dist-server/server/routes/tests/commands.test.js.map +1 -0
  115. package/dist-server/server/shared/utils.js +286 -0
  116. package/dist-server/server/shared/utils.js.map +1 -1
  117. package/package.json +5 -2
  118. package/public/api-docs.html +878 -0
  119. package/public/modelConstants.js +848 -0
  120. package/server/claude-sdk.js +64 -35
  121. package/server/cursor-cli.js +6 -3
  122. package/server/gemini-cli.js +7 -1
  123. package/server/gemini-response-handler.js +38 -0
  124. package/server/index.js +259 -75
  125. package/server/modules/database/index.ts +1 -0
  126. package/server/modules/projects/services/project-management.service.ts +2 -0
  127. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +7 -1
  128. package/server/modules/providers/README.md +11 -3
  129. package/server/modules/providers/list/claude/claude-auth.provider.ts +34 -6
  130. package/server/modules/providers/list/claude/claude-models.provider.ts +196 -0
  131. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  132. package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
  133. package/server/modules/providers/list/codex/codex-skills.provider.ts +10 -50
  134. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  135. package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
  136. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +7 -20
  137. package/server/modules/providers/list/cursor/cursor.provider.ts +3 -0
  138. package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
  139. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +3 -10
  140. package/server/modules/providers/list/gemini/gemini.provider.ts +3 -0
  141. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
  142. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
  143. package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
  144. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +157 -0
  145. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
  146. package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
  147. package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
  148. package/server/modules/providers/provider.registry.ts +2 -0
  149. package/server/modules/providers/provider.routes.ts +62 -2
  150. package/server/modules/providers/services/mcp.service.ts +1 -12
  151. package/server/modules/providers/services/provider-models.service.ts +358 -0
  152. package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
  153. package/server/modules/providers/services/sessions-watcher.service.ts +8 -0
  154. package/server/modules/providers/shared/base/abstract.provider.ts +2 -0
  155. package/server/modules/providers/tests/mcp.test.ts +93 -6
  156. package/server/modules/providers/tests/opencode-models.test.ts +73 -0
  157. package/server/modules/providers/tests/opencode-sessions.test.ts +334 -0
  158. package/server/modules/providers/tests/provider-models.service.test.ts +349 -0
  159. package/server/modules/providers/tests/skills.test.ts +66 -0
  160. package/server/modules/websocket/services/chat-websocket.service.ts +15 -1
  161. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +4 -4
  162. package/server/modules/websocket/services/shell-websocket.service.ts +22 -2
  163. package/server/modules/websocket/services/websocket-auth.service.ts +7 -2
  164. package/server/modules/websocket/services/websocket-server.service.ts +18 -0
  165. package/server/openai-codex.js +41 -4
  166. package/server/opencode-cli.js +336 -0
  167. package/server/opencode-cli.test.js +95 -0
  168. package/server/routes/agent.js +22 -8
  169. package/server/routes/commands.js +254 -233
  170. package/server/routes/cursor.js +2 -2
  171. package/server/routes/settings.js +1 -10
  172. package/server/routes/tests/commands.test.js +82 -0
  173. package/server/shared/interfaces.ts +45 -0
  174. package/server/shared/types.ts +88 -1
  175. package/server/shared/utils.ts +384 -0
  176. package/dist/assets/index-AaSE7-3t.js +0 -1336
  177. package/dist/assets/index-CKaku72N.css +0 -32
  178. package/dist-server/shared/modelConstants.js +0 -99
  179. package/dist-server/shared/modelConstants.js.map +0 -1
  180. package/shared/modelConstants.js +0 -107
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
- breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
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: 0,
1144
- total: 0,
1145
- breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
1146
- unsupported: true,
1147
- message: 'Token usage tracking not available for Gemini sessions'
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
- totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
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 cacheCreationTokens = 0;
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
- cacheCreationTokens = usage.cache_creation_input_tokens || 0;
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
- // Calculate total context usage (excluding output_tokens, as per ccusage)
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
- cacheCreation: cacheCreationTokens,
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
- async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true) {
1362
- // Using fsPromises from import
1363
- const items = [];
1364
-
1365
- try {
1366
- const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
1367
-
1368
- for (const entry of entries) {
1369
- // Debug: log all entries including hidden files
1370
-
1371
-
1372
- // Skip heavy build directories and VCS directories
1373
- if (entry.name === 'node_modules' ||
1374
- entry.name === 'dist' ||
1375
- entry.name === 'build' ||
1376
- entry.name === '.git' ||
1377
- entry.name === '.svn' ||
1378
- entry.name === '.hg') continue;
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
- const itemPath = path.join(dirPath, entry.name);
1381
- const item = {
1382
- name: entry.name,
1383
- path: itemPath,
1384
- type: entry.isDirectory() ? 'directory' : 'file'
1385
- };
1517
+ await new Promise((resolve) => {
1518
+ pendingFsOperations.push(resolve);
1519
+ });
1520
+ }
1386
1521
 
1387
- // Get file stats for additional metadata
1388
- try {
1389
- const stats = await fsPromises.stat(itemPath);
1390
- item.size = stats.size;
1391
- item.modified = stats.mtime.toISOString();
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
- if (entry.isDirectory() && currentDepth < maxDepth) {
1409
- // Recursively get subdirectories but limit depth
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
- items.push(item);
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`, and `gemini`.
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 `shared/modelConstants.js` if the provider appears in UI provider pickers.
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
- - `shared/modelConstants.js`
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.