@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
@@ -9,7 +9,8 @@ import { spawn } from 'child_process';
9
9
  import express from 'express';
10
10
  import cors from 'cors';
11
11
  import mime from 'mime-types';
12
- import { AppError, WORKSPACES_ROOT, validateWorkspacePath } from './shared/utils.js';
12
+ import Database from 'better-sqlite3';
13
+ import { AppError, WORKSPACES_ROOT, getOpenCodeDatabasePath, validateWorkspacePath } from './shared/utils.js';
13
14
  import { closeSessionsWatcher, initializeSessionsWatcher } from './modules/providers/index.js';
14
15
  import { createWebSocketServer } from './modules/websocket/index.js';
15
16
  import { getConnectableHost } from '../shared/networkHosts.js';
@@ -18,6 +19,7 @@ import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getAct
18
19
  import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions, } from './cursor-cli.js';
19
20
  import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions, } from './openai-codex.js';
20
21
  import { spawnGemini, abortGeminiSession, isGeminiSessionActive, getActiveGeminiSessions, } from './gemini-cli.js';
22
+ import { spawnOpenCode, abortOpenCodeSession, isOpenCodeSessionActive, getActiveOpenCodeSessions, } from './opencode-cli.js';
21
23
  import sessionManager from './sessionManager.js';
22
24
  import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput, } from './utils/url-detection.js';
23
25
  import gitRoutes from './routes/git.js';
@@ -34,7 +36,7 @@ import geminiRoutes from './routes/gemini.js';
34
36
  import pluginsRoutes from './routes/plugins.js';
35
37
  import providerRoutes from './modules/providers/provider.routes.js';
36
38
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
37
- import { initializeDatabase, projectsDb } from './modules/database/index.js';
39
+ import { initializeDatabase, projectsDb, sessionsDb } from './modules/database/index.js';
38
40
  import { configureWebPush } from './services/vapid-keys.js';
39
41
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
40
42
  import { IS_PLATFORM } from './constants/config.js';
@@ -58,21 +60,25 @@ const wss = createWebSocketServer(server, {
58
60
  spawnCursor,
59
61
  queryCodex,
60
62
  spawnGemini,
63
+ spawnOpenCode,
61
64
  abortClaudeSDKSession,
62
65
  abortCursorSession,
63
66
  abortCodexSession,
64
67
  abortGeminiSession,
68
+ abortOpenCodeSession,
65
69
  resolveToolApproval,
66
70
  isClaudeSDKSessionActive,
67
71
  isCursorSessionActive,
68
72
  isCodexSessionActive,
69
73
  isGeminiSessionActive,
74
+ isOpenCodeSessionActive,
70
75
  reconnectSessionWriter,
71
76
  getPendingApprovalsForSession,
72
77
  getActiveClaudeSDKSessions,
73
78
  getActiveCursorSessions,
74
79
  getActiveCodexSessions,
75
80
  getActiveGeminiSessions,
81
+ getActiveOpenCodeSessions,
76
82
  },
77
83
  shell: {
78
84
  getSessionById: (sessionId) => sessionManager.getSession(sessionId),
@@ -1004,21 +1010,119 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1004
1010
  return res.json({
1005
1011
  used: 0,
1006
1012
  total: 0,
1007
- breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
1013
+ inputTokens: 0,
1014
+ outputTokens: 0,
1015
+ breakdown: { input: 0, output: 0 },
1008
1016
  unsupported: true,
1009
1017
  message: 'Token usage tracking not available for Cursor sessions'
1010
1018
  });
1011
1019
  }
1012
- // Handle Gemini sessions - they are raw logs in our current setup
1013
1020
  if (provider === 'gemini') {
1021
+ const session = sessionsDb.getSessionById(safeSessionId);
1022
+ const sessionFilePath = session?.jsonl_path;
1023
+ if (!sessionFilePath) {
1024
+ return res.json({
1025
+ used: 0,
1026
+ inputTokens: 0,
1027
+ outputTokens: 0,
1028
+ breakdown: { input: 0, output: 0 },
1029
+ unsupported: true,
1030
+ message: 'Token usage tracking not available for this Gemini session'
1031
+ });
1032
+ }
1033
+ let fileContent;
1034
+ try {
1035
+ fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
1036
+ }
1037
+ catch (error) {
1038
+ if (error.code === 'ENOENT') {
1039
+ return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
1040
+ }
1041
+ throw error;
1042
+ }
1043
+ const lines = fileContent.trim().split('\n');
1044
+ let inputTokens = 0;
1045
+ let outputTokens = 0;
1046
+ let totalTokens = 0;
1047
+ for (let i = lines.length - 1; i >= 0; i--) {
1048
+ try {
1049
+ const entry = JSON.parse(lines[i]);
1050
+ if (!entry.tokens || typeof entry.tokens !== 'object') {
1051
+ continue;
1052
+ }
1053
+ inputTokens = Number(entry.tokens.input || 0);
1054
+ outputTokens = Number(entry.tokens.output || 0);
1055
+ totalTokens = Number(entry.tokens.total || inputTokens + outputTokens || 0);
1056
+ break;
1057
+ }
1058
+ catch {
1059
+ continue;
1060
+ }
1061
+ }
1014
1062
  return res.json({
1015
- used: 0,
1016
- total: 0,
1017
- breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
1018
- unsupported: true,
1019
- message: 'Token usage tracking not available for Gemini sessions'
1063
+ used: totalTokens,
1064
+ inputTokens,
1065
+ outputTokens,
1066
+ breakdown: {
1067
+ input: inputTokens,
1068
+ output: outputTokens
1069
+ }
1020
1070
  });
1021
1071
  }
1072
+ if (provider === 'opencode') {
1073
+ const dbPath = getOpenCodeDatabasePath();
1074
+ if (!fs.existsSync(dbPath)) {
1075
+ return res.status(404).json({ error: 'OpenCode database not found' });
1076
+ }
1077
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
1078
+ try {
1079
+ const columns = db.prepare('PRAGMA table_info(session)').all();
1080
+ const columnNames = new Set(columns.map((column) => column.name));
1081
+ const requiredColumns = ['tokens_input', 'tokens_output', 'tokens_reasoning', 'tokens_cache_read', 'tokens_cache_write'];
1082
+ if (!requiredColumns.every((column) => columnNames.has(column))) {
1083
+ return res.json({
1084
+ used: 0,
1085
+ inputTokens: 0,
1086
+ outputTokens: 0,
1087
+ breakdown: { input: 0, output: 0 },
1088
+ unsupported: true,
1089
+ message: 'Token usage tracking is not available in this OpenCode database schema'
1090
+ });
1091
+ }
1092
+ const row = db.prepare(`
1093
+ SELECT
1094
+ tokens_input AS inputTokens,
1095
+ tokens_output AS outputTokens,
1096
+ tokens_reasoning AS reasoningTokens,
1097
+ tokens_cache_read AS cacheReadTokens,
1098
+ tokens_cache_write AS cacheWriteTokens
1099
+ FROM session
1100
+ WHERE id = ?
1101
+ `).get(safeSessionId);
1102
+ if (!row) {
1103
+ return res.status(404).json({ error: 'OpenCode session not found', sessionId: safeSessionId });
1104
+ }
1105
+ const inputTokens = Number(row.inputTokens || 0) + Number(row.cacheReadTokens || 0);
1106
+ const outputTokens = Number(row.outputTokens || 0);
1107
+ const totalUsed = Number(row.inputTokens || 0)
1108
+ + outputTokens
1109
+ + Number(row.reasoningTokens || 0)
1110
+ + Number(row.cacheReadTokens || 0)
1111
+ + Number(row.cacheWriteTokens || 0);
1112
+ return res.json({
1113
+ used: totalUsed,
1114
+ inputTokens,
1115
+ outputTokens,
1116
+ breakdown: {
1117
+ input: inputTokens,
1118
+ output: outputTokens
1119
+ }
1120
+ });
1121
+ }
1122
+ finally {
1123
+ db.close();
1124
+ }
1125
+ }
1022
1126
  // Handle Codex sessions
1023
1127
  if (provider === 'codex') {
1024
1128
  const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
@@ -1059,6 +1163,8 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1059
1163
  throw error;
1060
1164
  }
1061
1165
  const lines = fileContent.trim().split('\n');
1166
+ let inputTokens = 0;
1167
+ let outputTokens = 0;
1062
1168
  let totalTokens = 0;
1063
1169
  let contextWindow = 200000; // Default for Codex/OpenAI
1064
1170
  // Find the latest token_count event with info (scan from end)
@@ -1069,7 +1175,9 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1069
1175
  if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
1070
1176
  const tokenInfo = entry.payload.info;
1071
1177
  if (tokenInfo.total_token_usage) {
1072
- totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
1178
+ inputTokens = tokenInfo.total_token_usage.input_tokens || 0;
1179
+ outputTokens = tokenInfo.total_token_usage.output_tokens || 0;
1180
+ totalTokens = tokenInfo.total_token_usage.total_tokens || inputTokens + outputTokens;
1073
1181
  }
1074
1182
  if (tokenInfo.model_context_window) {
1075
1183
  contextWindow = tokenInfo.model_context_window;
@@ -1084,7 +1192,13 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1084
1192
  }
1085
1193
  return res.json({
1086
1194
  used: totalTokens,
1087
- total: contextWindow
1195
+ total: contextWindow,
1196
+ inputTokens,
1197
+ outputTokens,
1198
+ breakdown: {
1199
+ input: inputTokens,
1200
+ output: outputTokens
1201
+ }
1088
1202
  });
1089
1203
  }
1090
1204
  // Handle Claude sessions (default)
@@ -1122,8 +1236,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1122
1236
  const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
1123
1237
  const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
1124
1238
  let inputTokens = 0;
1125
- let cacheCreationTokens = 0;
1126
- let cacheReadTokens = 0;
1239
+ let outputTokens = 0;
1127
1240
  // Find the latest assistant message with usage data (scan from end)
1128
1241
  for (let i = lines.length - 1; i >= 0; i--) {
1129
1242
  try {
@@ -1133,8 +1246,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1133
1246
  const usage = entry.message.usage;
1134
1247
  // Use token counts from latest assistant message only
1135
1248
  inputTokens = usage.input_tokens || 0;
1136
- cacheCreationTokens = usage.cache_creation_input_tokens || 0;
1137
- cacheReadTokens = usage.cache_read_input_tokens || 0;
1249
+ outputTokens = usage.output_tokens || 0;
1138
1250
  break; // Stop after finding the latest assistant message
1139
1251
  }
1140
1252
  }
@@ -1143,15 +1255,15 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1143
1255
  continue;
1144
1256
  }
1145
1257
  }
1146
- // Calculate total context usage (excluding output_tokens, as per ccusage)
1147
- const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
1258
+ const totalUsed = inputTokens + outputTokens;
1148
1259
  res.json({
1149
1260
  used: totalUsed,
1150
1261
  total: contextWindow,
1262
+ inputTokens,
1263
+ outputTokens,
1151
1264
  breakdown: {
1152
1265
  input: inputTokens,
1153
- cacheCreation: cacheCreationTokens,
1154
- cacheRead: cacheReadTokens
1266
+ output: outputTokens
1155
1267
  }
1156
1268
  });
1157
1269
  }
@@ -1211,68 +1323,123 @@ function permToRwx(perm) {
1211
1323
  const x = perm & 1 ? 'x' : '-';
1212
1324
  return r + w + x;
1213
1325
  }
1326
+ // Directories that are almost never interesting for a project tree but can
1327
+ // contain tens of thousands of files. Skipping them before recursion keeps
1328
+ // traversal time bounded on large monorepos and high-latency filesystems
1329
+ // (NFS / SMB).
1330
+ const IGNORED_DIRS = new Set([
1331
+ // JS / TS toolchains
1332
+ 'node_modules', 'dist', 'build', '.next', '.nuxt', '.cache', '.parcel-cache',
1333
+ // VCS
1334
+ '.git', '.svn', '.hg',
1335
+ // Python
1336
+ '__pycache__', '.pytest_cache', '.mypy_cache', '.tox', 'venv', '.venv',
1337
+ // Rust / Go / Java / Ruby
1338
+ 'target', 'vendor',
1339
+ // Build output / IDE
1340
+ '.gradle', '.idea', 'coverage', '.nyc_output'
1341
+ ]);
1342
+ const DEFAULT_FS_CONCURRENCY = 64;
1343
+ const parsedFsConcurrency = Number.parseInt(process.env.FS_CONCURRENCY || '', 10);
1344
+ const FS_CONCURRENCY = Number.isFinite(parsedFsConcurrency) && parsedFsConcurrency > 0
1345
+ ? parsedFsConcurrency
1346
+ : DEFAULT_FS_CONCURRENCY;
1347
+ let activeFsOperations = 0;
1348
+ const pendingFsOperations = [];
1349
+ async function acquire() {
1350
+ if (activeFsOperations < FS_CONCURRENCY) {
1351
+ activeFsOperations += 1;
1352
+ return;
1353
+ }
1354
+ await new Promise((resolve) => {
1355
+ pendingFsOperations.push(resolve);
1356
+ });
1357
+ }
1358
+ function release() {
1359
+ const next = pendingFsOperations.shift();
1360
+ if (next) {
1361
+ next();
1362
+ return;
1363
+ }
1364
+ activeFsOperations = Math.max(0, activeFsOperations - 1);
1365
+ }
1214
1366
  async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true) {
1215
1367
  // Using fsPromises from import
1216
- const items = [];
1368
+ let entries;
1217
1369
  try {
1218
- const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
1219
- for (const entry of entries) {
1220
- // Debug: log all entries including hidden files
1221
- // Skip heavy build directories and VCS directories
1222
- if (entry.name === 'node_modules' ||
1223
- entry.name === 'dist' ||
1224
- entry.name === 'build' ||
1225
- entry.name === '.git' ||
1226
- entry.name === '.svn' ||
1227
- entry.name === '.hg')
1228
- continue;
1229
- const itemPath = path.join(dirPath, entry.name);
1230
- const item = {
1231
- name: entry.name,
1232
- path: itemPath,
1233
- type: entry.isDirectory() ? 'directory' : 'file'
1234
- };
1235
- // Get file stats for additional metadata
1370
+ await acquire();
1371
+ try {
1372
+ entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
1373
+ }
1374
+ finally {
1375
+ release();
1376
+ }
1377
+ }
1378
+ catch (error) {
1379
+ // Only log non-permission errors to avoid spam
1380
+ if (error.code !== 'EACCES' && error.code !== 'EPERM') {
1381
+ console.error('Error reading directory:', error);
1382
+ }
1383
+ return [];
1384
+ }
1385
+ const filteredEntries = entries.filter((entry) => !(entry.isDirectory() && IGNORED_DIRS.has(entry.name)));
1386
+ // Process every entry in parallel. On high-latency filesystems (NFS/SMB)
1387
+ // serial stat() was the real bottleneck — issuing them concurrently lets
1388
+ // the kernel pipeline the round-trips and the recursive calls overlap too.
1389
+ const items = await Promise.all(filteredEntries.map(async (entry) => {
1390
+ const itemPath = path.join(dirPath, entry.name);
1391
+ const item = {
1392
+ name: entry.name,
1393
+ path: itemPath,
1394
+ type: entry.isDirectory() ? 'directory' : 'file'
1395
+ };
1396
+ // Get file stats for additional metadata
1397
+ try {
1398
+ await acquire();
1236
1399
  try {
1237
- const stats = await fsPromises.stat(itemPath);
1400
+ const stats = await fsPromises.lstat(itemPath);
1238
1401
  item.size = stats.size;
1239
1402
  item.modified = stats.mtime.toISOString();
1403
+ // Mark symlinks so UI can distinguish them
1404
+ if (stats.isSymbolicLink()) {
1405
+ item.isSymlink = true;
1406
+ }
1240
1407
  // Convert permissions to rwx format
1241
1408
  const mode = stats.mode;
1242
1409
  const ownerPerm = (mode >> 6) & 7;
1243
1410
  const groupPerm = (mode >> 3) & 7;
1244
1411
  const otherPerm = mode & 7;
1245
- item.permissions = ((mode >> 6) & 7).toString() + ((mode >> 3) & 7).toString() + (mode & 7).toString();
1246
- item.permissionsRwx = permToRwx(ownerPerm) + permToRwx(groupPerm) + permToRwx(otherPerm);
1412
+ item.permissions =
1413
+ ((mode >> 6) & 7).toString() +
1414
+ ((mode >> 3) & 7).toString() +
1415
+ (mode & 7).toString();
1416
+ item.permissionsRwx =
1417
+ permToRwx(ownerPerm) +
1418
+ permToRwx(groupPerm) +
1419
+ permToRwx(otherPerm);
1247
1420
  }
1248
- catch (statError) {
1249
- // If stat fails, provide default values
1250
- item.size = 0;
1251
- item.modified = null;
1252
- item.permissions = '000';
1253
- item.permissionsRwx = '---------';
1421
+ finally {
1422
+ release();
1254
1423
  }
1255
- if (entry.isDirectory() && currentDepth < maxDepth) {
1256
- // Recursively get subdirectories but limit depth
1257
- try {
1258
- // Check if we can access the directory before trying to read it
1259
- await fsPromises.access(item.path, fs.constants.R_OK);
1260
- item.children = await getFileTree(item.path, maxDepth, currentDepth + 1, showHidden);
1261
- }
1262
- catch (e) {
1263
- // Silently skip directories we can't access (permission denied, etc.)
1264
- item.children = [];
1265
- }
1266
- }
1267
- items.push(item);
1268
- }
1269
- }
1270
- catch (error) {
1271
- // Only log non-permission errors to avoid spam
1272
- if (error.code !== 'EACCES' && error.code !== 'EPERM') {
1273
- console.error('Error reading directory:', error);
1274
1424
  }
1275
- }
1425
+ catch (statError) {
1426
+ // If stat fails, provide default values
1427
+ item.size = 0;
1428
+ item.modified = null;
1429
+ item.permissions = '000';
1430
+ item.permissionsRwx = '---------';
1431
+ }
1432
+ if (entry.isDirectory() && currentDepth < maxDepth) {
1433
+ // Recurse. Let readdir's own EACCES bubble up through the catch in
1434
+ // the recursive call rather than doing a separate access() probe
1435
+ // (which doubled the round-trip count on SMB without adding info).
1436
+ // The recursive call starts with a bounded readdir; holding a permit
1437
+ // for the whole subtree can deadlock when sibling directories are
1438
+ // waiting on their own children.
1439
+ item.children = await getFileTree(itemPath, maxDepth, currentDepth + 1, showHidden);
1440
+ }
1441
+ return item;
1442
+ }));
1276
1443
  return items.sort((a, b) => {
1277
1444
  if (a.type !== b.type) {
1278
1445
  return a.type === 'directory' ? -1 : 1;