@cloudcli-ai/cloudcli 1.31.5 → 1.33.0

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 (252) 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-MOIvpy53.css +32 -0
  9. package/dist/assets/{index-DqMVUeZS.js → index-skXSjBje.js} +290 -270
  10. package/dist/index.html +2 -2
  11. package/dist/modelConstants.js +841 -0
  12. package/dist-server/server/claude-sdk.js +67 -39
  13. package/dist-server/server/claude-sdk.js.map +1 -1
  14. package/dist-server/server/cursor-cli.js +6 -9
  15. package/dist-server/server/cursor-cli.js.map +1 -1
  16. package/dist-server/server/gemini-cli.js +163 -35
  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 +131 -19
  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/database/migrations.js +11 -0
  25. package/dist-server/server/modules/database/migrations.js.map +1 -1
  26. package/dist-server/server/modules/database/repositories/projects.db.js +12 -0
  27. package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -1
  28. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js +61 -0
  29. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +1 -0
  30. package/dist-server/server/modules/database/repositories/sessions.db.js +49 -8
  31. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  32. package/dist-server/server/modules/database/schema.js +1 -0
  33. package/dist-server/server/modules/database/schema.js.map +1 -1
  34. package/dist-server/server/modules/projects/projects.routes.js +12 -3
  35. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  36. package/dist-server/server/modules/projects/services/project-delete.service.js +14 -1
  37. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  38. package/dist-server/server/modules/projects/services/project-management.service.js +1 -0
  39. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
  40. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +47 -0
  41. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  42. package/dist-server/server/modules/providers/index.js +1 -0
  43. package/dist-server/server/modules/providers/index.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +27 -7
  45. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  46. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +141 -0
  47. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
  48. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +55 -2
  49. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +153 -14
  51. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  52. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +198 -0
  53. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -0
  54. package/dist-server/server/modules/providers/list/claude/claude.provider.js +4 -0
  55. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  56. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +84 -0
  57. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
  58. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -2
  59. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  60. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +21 -6
  61. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  62. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +50 -0
  63. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -0
  64. package/dist-server/server/modules/providers/list/codex/codex.provider.js +4 -0
  65. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  66. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +754 -0
  67. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
  68. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +8 -30
  69. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  70. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +187 -24
  71. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  72. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +28 -0
  73. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -0
  74. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +4 -0
  75. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  76. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +125 -2
  77. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
  78. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +27 -0
  79. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -0
  80. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +24 -8
  81. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -1
  82. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +10 -10
  83. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  84. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +33 -0
  85. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -0
  86. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +4 -0
  87. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  88. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +92 -0
  89. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
  90. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
  91. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
  92. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +267 -0
  93. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
  94. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +115 -0
  95. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
  96. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +410 -0
  97. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
  98. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
  99. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
  100. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +19 -0
  101. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
  102. package/dist-server/server/modules/providers/provider.registry.js +2 -0
  103. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  104. package/dist-server/server/modules/providers/provider.routes.js +65 -3
  105. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  106. package/dist-server/server/modules/providers/services/mcp.service.js +1 -9
  107. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  108. package/dist-server/server/modules/providers/services/provider-models.service.js +199 -0
  109. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
  110. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +129 -13
  111. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  112. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
  113. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  114. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +13 -4
  115. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  116. package/dist-server/server/modules/providers/services/sessions.service.js +81 -7
  117. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  118. package/dist-server/server/modules/providers/services/skills.service.js +11 -0
  119. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -0
  120. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  121. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +45 -0
  122. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -0
  123. package/dist-server/server/modules/providers/tests/mcp.test.js +73 -6
  124. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  125. package/dist-server/server/modules/providers/tests/opencode-models.test.js +66 -0
  126. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
  127. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +262 -0
  128. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
  129. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +270 -0
  130. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
  131. package/dist-server/server/modules/providers/tests/skills.test.js +259 -0
  132. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -0
  133. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +12 -1
  134. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  135. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +4 -4
  136. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -1
  137. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -1
  138. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  139. package/dist-server/server/openai-codex.js +87 -29
  140. package/dist-server/server/openai-codex.js.map +1 -1
  141. package/dist-server/server/opencode-cli.js +287 -0
  142. package/dist-server/server/opencode-cli.js.map +1 -0
  143. package/dist-server/server/opencode-cli.test.js +84 -0
  144. package/dist-server/server/opencode-cli.test.js.map +1 -0
  145. package/dist-server/server/routes/agent.js +21 -8
  146. package/dist-server/server/routes/agent.js.map +1 -1
  147. package/dist-server/server/routes/commands.js +204 -211
  148. package/dist-server/server/routes/commands.js.map +1 -1
  149. package/dist-server/server/routes/cursor.js +2 -2
  150. package/dist-server/server/routes/cursor.js.map +1 -1
  151. package/dist-server/server/routes/settings.js +0 -10
  152. package/dist-server/server/routes/settings.js.map +1 -1
  153. package/dist-server/server/routes/tests/commands.test.js +76 -0
  154. package/dist-server/server/routes/tests/commands.test.js.map +1 -0
  155. package/dist-server/server/shared/claude-cli-path.js +103 -0
  156. package/dist-server/server/shared/claude-cli-path.js.map +1 -0
  157. package/dist-server/server/shared/claude-cli-path.test.js +45 -0
  158. package/dist-server/server/shared/claude-cli-path.test.js.map +1 -0
  159. package/dist-server/server/{utils → shared}/frontmatter.js +3 -3
  160. package/dist-server/server/shared/frontmatter.js.map +1 -0
  161. package/dist-server/server/shared/utils.js +368 -0
  162. package/dist-server/server/shared/utils.js.map +1 -1
  163. package/dist-server/server/utils/commandParser.js +3 -3
  164. package/dist-server/server/utils/commandParser.js.map +1 -1
  165. package/package.json +3 -1
  166. package/public/api-docs.html +878 -0
  167. package/public/modelConstants.js +841 -0
  168. package/server/claude-sdk.js +74 -40
  169. package/server/cursor-cli.js +6 -11
  170. package/server/gemini-cli.js +197 -43
  171. package/server/gemini-response-handler.js +38 -0
  172. package/server/index.js +144 -19
  173. package/server/modules/database/index.ts +1 -0
  174. package/server/modules/database/migrations.ts +12 -0
  175. package/server/modules/database/repositories/projects.db.ts +13 -0
  176. package/server/modules/database/repositories/sessions.db.integration.test.ts +72 -0
  177. package/server/modules/database/repositories/sessions.db.ts +60 -9
  178. package/server/modules/database/schema.ts +1 -0
  179. package/server/modules/projects/projects.routes.ts +20 -3
  180. package/server/modules/projects/services/project-delete.service.ts +16 -1
  181. package/server/modules/projects/services/project-management.service.ts +2 -0
  182. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +71 -1
  183. package/server/modules/providers/README.md +354 -0
  184. package/server/modules/providers/index.ts +2 -1
  185. package/server/modules/providers/list/claude/claude-auth.provider.ts +38 -13
  186. package/server/modules/providers/list/claude/claude-models.provider.ts +191 -0
  187. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +68 -2
  188. package/server/modules/providers/list/claude/claude-sessions.provider.ts +175 -14
  189. package/server/modules/providers/list/claude/claude-skills.provider.ts +257 -0
  190. package/server/modules/providers/list/claude/claude.provider.ts +11 -1
  191. package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
  192. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +62 -2
  193. package/server/modules/providers/list/codex/codex-sessions.provider.ts +25 -6
  194. package/server/modules/providers/list/codex/codex-skills.provider.ts +60 -0
  195. package/server/modules/providers/list/codex/codex.provider.ts +11 -1
  196. package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
  197. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +17 -40
  198. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +216 -26
  199. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +31 -0
  200. package/server/modules/providers/list/cursor/cursor.provider.ts +11 -1
  201. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +158 -2
  202. package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
  203. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +24 -20
  204. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +10 -11
  205. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +36 -0
  206. package/server/modules/providers/list/gemini/gemini.provider.ts +11 -1
  207. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
  208. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
  209. package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
  210. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +157 -0
  211. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
  212. package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
  213. package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
  214. package/server/modules/providers/provider.registry.ts +2 -0
  215. package/server/modules/providers/provider.routes.ts +97 -4
  216. package/server/modules/providers/services/mcp.service.ts +1 -12
  217. package/server/modules/providers/services/provider-models.service.ts +325 -0
  218. package/server/modules/providers/services/session-conversations-search.service.ts +162 -13
  219. package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
  220. package/server/modules/providers/services/sessions-watcher.service.ts +14 -4
  221. package/server/modules/providers/services/sessions.service.ts +112 -9
  222. package/server/modules/providers/services/skills.service.ts +15 -0
  223. package/server/modules/providers/shared/base/abstract.provider.ts +4 -0
  224. package/server/modules/providers/shared/skills/skills.provider.ts +64 -0
  225. package/server/modules/providers/tests/mcp.test.ts +93 -6
  226. package/server/modules/providers/tests/opencode-models.test.ts +73 -0
  227. package/server/modules/providers/tests/opencode-sessions.test.ts +334 -0
  228. package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
  229. package/server/modules/providers/tests/skills.test.ts +512 -0
  230. package/server/modules/websocket/services/chat-websocket.service.ts +15 -1
  231. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +4 -4
  232. package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
  233. package/server/openai-codex.js +99 -30
  234. package/server/opencode-cli.js +336 -0
  235. package/server/opencode-cli.test.js +95 -0
  236. package/server/routes/agent.js +22 -8
  237. package/server/routes/commands.js +256 -233
  238. package/server/routes/cursor.js +2 -2
  239. package/server/routes/settings.js +1 -10
  240. package/server/routes/tests/commands.test.js +82 -0
  241. package/server/shared/claude-cli-path.test.ts +61 -0
  242. package/server/shared/claude-cli-path.ts +139 -0
  243. package/server/{utils/frontmatter.js → shared/frontmatter.ts} +3 -3
  244. package/server/shared/interfaces.ts +64 -0
  245. package/server/shared/types.ts +166 -1
  246. package/server/shared/utils.ts +478 -0
  247. package/server/utils/commandParser.js +5 -3
  248. package/dist/assets/index-6rYG_mRo.css +0 -32
  249. package/dist-server/server/utils/frontmatter.js.map +0 -1
  250. package/dist-server/shared/modelConstants.js +0 -98
  251. package/dist-server/shared/modelConstants.js.map +0 -1
  252. package/shared/modelConstants.js +0 -106
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) {
@@ -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';
@@ -257,8 +257,10 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
257
257
 
258
258
  if (!shouldRebuild) {
259
259
  addColumnToTableIfNotExists(db, 'sessions', columnNames, 'jsonl_path', 'TEXT');
260
+ addColumnToTableIfNotExists(db, 'sessions', columnNames, 'isArchived', 'BOOLEAN DEFAULT 0');
260
261
  addColumnToTableIfNotExists(db, 'sessions', columnNames, 'created_at', 'DATETIME');
261
262
  addColumnToTableIfNotExists(db, 'sessions', columnNames, 'updated_at', 'DATETIME');
263
+ db.exec('UPDATE sessions SET isArchived = COALESCE(isArchived, 0)');
262
264
  db.exec('UPDATE sessions SET created_at = COALESCE(created_at, CURRENT_TIMESTAMP)');
263
265
  db.exec('UPDATE sessions SET updated_at = COALESCE(updated_at, CURRENT_TIMESTAMP)');
264
266
  return;
@@ -284,6 +286,10 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
284
286
  ? 'jsonl_path'
285
287
  : 'NULL';
286
288
 
289
+ const isArchivedExpression = columnNames.includes('isArchived')
290
+ ? 'COALESCE(isArchived, 0)'
291
+ : '0';
292
+
287
293
  const createdAtExpression = columnNames.includes('created_at')
288
294
  ? 'COALESCE(created_at, CURRENT_TIMESTAMP)'
289
295
  : 'CURRENT_TIMESTAMP';
@@ -303,6 +309,7 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
303
309
  custom_name TEXT,
304
310
  project_path TEXT,
305
311
  jsonl_path TEXT,
312
+ isArchived BOOLEAN DEFAULT 0,
306
313
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
307
314
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
308
315
  PRIMARY KEY (session_id),
@@ -319,6 +326,7 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
319
326
  ${customNameExpression} AS custom_name,
320
327
  ${projectPathExpression} AS project_path,
321
328
  ${jsonlPathExpression} AS jsonl_path,
329
+ ${isArchivedExpression} AS isArchived,
322
330
  ${createdAtExpression} AS created_at,
323
331
  ${updatedAtExpression} AS updated_at,
324
332
  rowid AS source_rowid
@@ -332,6 +340,7 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
332
340
  custom_name,
333
341
  project_path,
334
342
  jsonl_path,
343
+ isArchived,
335
344
  created_at,
336
345
  updated_at,
337
346
  ROW_NUMBER() OVER (
@@ -346,6 +355,7 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
346
355
  custom_name,
347
356
  project_path,
348
357
  jsonl_path,
358
+ isArchived,
349
359
  created_at,
350
360
  updated_at
351
361
  )
@@ -355,6 +365,7 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
355
365
  custom_name,
356
366
  project_path,
357
367
  jsonl_path,
368
+ isArchived,
358
369
  created_at,
359
370
  updated_at
360
371
  FROM ranked_rows
@@ -421,6 +432,7 @@ export const runMigrations = (db: Database) => {
421
432
 
422
433
  db.exec('CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id)');
423
434
  db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_project_path ON sessions(project_path)');
435
+ db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_is_archived ON sessions(isArchived)');
424
436
  db.exec('CREATE INDEX IF NOT EXISTS idx_projects_is_starred ON projects(isStarred)');
425
437
  db.exec('CREATE INDEX IF NOT EXISTS idx_projects_is_archived ON projects(isArchived)');
426
438
 
@@ -95,6 +95,19 @@ export const projectsDb = {
95
95
  `).all() as ProjectRepositoryRow[];
96
96
  },
97
97
 
98
+ /**
99
+ * Archived rows are queried separately so archive-focused UIs can present
100
+ * hidden workspaces without reintroducing them into the active sidebar list.
101
+ */
102
+ getArchivedProjectPaths(): ProjectRepositoryRow[] {
103
+ const db = getConnection();
104
+ return db.prepare(`
105
+ SELECT project_id, project_path, custom_project_name, isStarred, isArchived
106
+ FROM projects
107
+ WHERE isArchived = 1
108
+ `).all() as ProjectRepositoryRow[];
109
+ },
110
+
98
111
  getCustomProjectName(projectPath: string): string | null {
99
112
  const db = getConnection();
100
113
  const normalizedProjectPath = normalizeProjectPath(projectPath);
@@ -0,0 +1,72 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { closeConnection } from '@/modules/database/connection.js';
8
+ import { initializeDatabase } from '@/modules/database/init-db.js';
9
+ import { sessionsDb } from '@/modules/database/repositories/sessions.db.js';
10
+
11
+ async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
12
+ const previousDatabasePath = process.env.DATABASE_PATH;
13
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), 'sessions-db-'));
14
+ const databasePath = path.join(tempDirectory, 'auth.db');
15
+
16
+ closeConnection();
17
+ process.env.DATABASE_PATH = databasePath;
18
+ await initializeDatabase();
19
+
20
+ try {
21
+ await runTest();
22
+ } finally {
23
+ closeConnection();
24
+ if (previousDatabasePath === undefined) {
25
+ delete process.env.DATABASE_PATH;
26
+ } else {
27
+ process.env.DATABASE_PATH = previousDatabasePath;
28
+ }
29
+ await rm(tempDirectory, { recursive: true, force: true });
30
+ }
31
+ }
32
+
33
+ test('session archive queries hide archived rows from active project views', async () => {
34
+ await withIsolatedDatabase(() => {
35
+ sessionsDb.createSession('session-active', 'claude', '/workspace/demo-project', 'Active Session');
36
+ sessionsDb.createSession('session-archived', 'claude', '/workspace/demo-project', 'Archived Session');
37
+ sessionsDb.updateSessionIsArchived('session-archived', true);
38
+
39
+ const activeSessions = sessionsDb.getAllSessions();
40
+ const archivedSessions = sessionsDb.getArchivedSessions();
41
+ const activeProjectSessions = sessionsDb.getSessionsByProjectPath('/workspace/demo-project');
42
+ const allProjectSessions = sessionsDb.getSessionsByProjectPathIncludingArchived('/workspace/demo-project');
43
+
44
+ assert.deepEqual(activeSessions.map((session) => session.session_id), ['session-active']);
45
+ assert.deepEqual(archivedSessions.map((session) => session.session_id), ['session-archived']);
46
+ assert.deepEqual(activeProjectSessions.map((session) => session.session_id), ['session-active']);
47
+ assert.deepEqual(
48
+ allProjectSessions.map((session) => session.session_id).sort(),
49
+ ['session-active', 'session-archived'],
50
+ );
51
+ assert.equal(sessionsDb.countSessionsByProjectPath('/workspace/demo-project'), 1);
52
+ });
53
+ });
54
+
55
+ test('createSession reactivates archived rows when the session becomes active again', async () => {
56
+ await withIsolatedDatabase(() => {
57
+ sessionsDb.createSession('session-reused', 'claude', '/workspace/demo-project', 'First Name');
58
+ sessionsDb.updateSessionIsArchived('session-reused', true);
59
+
60
+ sessionsDb.createSession('session-reused', 'claude', '/workspace/demo-project', 'Updated Name');
61
+
62
+ const activeSessions = sessionsDb.getAllSessions();
63
+ const archivedSessions = sessionsDb.getArchivedSessions();
64
+ const restoredSession = sessionsDb.getSessionById('session-reused');
65
+
66
+ assert.equal(activeSessions.length, 1);
67
+ assert.equal(activeSessions[0]?.session_id, 'session-reused');
68
+ assert.equal(activeSessions[0]?.custom_name, 'Updated Name');
69
+ assert.equal(archivedSessions.length, 0);
70
+ assert.equal(restoredSession?.isArchived, 0);
71
+ });
72
+ });
@@ -8,13 +8,14 @@ type SessionRow = {
8
8
  project_path: string | null;
9
9
  jsonl_path: string | null;
10
10
  custom_name: string | null;
11
+ isArchived: number;
11
12
  created_at: string;
12
13
  updated_at: string;
13
14
  };
14
15
 
15
16
  type SessionMetadataLookupRow = Pick<
16
17
  SessionRow,
17
- 'session_id' | 'provider' | 'project_path' | 'jsonl_path' | 'custom_name' | 'created_at' | 'updated_at'
18
+ 'session_id' | 'provider' | 'project_path' | 'jsonl_path' | 'custom_name' | 'isArchived' | 'created_at' | 'updated_at'
18
19
  >;
19
20
 
20
21
  function normalizeTimestamp(value?: string): string | null {
@@ -53,13 +54,14 @@ export const sessionsDb = {
53
54
  projectsDb.createProjectPath(normalizedProjectPath);
54
55
 
55
56
  db.prepare(
56
- `INSERT INTO sessions (session_id, provider, custom_name, project_path, jsonl_path, created_at, updated_at)
57
- VALUES (?, ?, ?, ?, ?, COALESCE(?, CURRENT_TIMESTAMP), COALESCE(?, CURRENT_TIMESTAMP))
57
+ `INSERT INTO sessions (session_id, provider, custom_name, project_path, jsonl_path, isArchived, created_at, updated_at)
58
+ VALUES (?, ?, ?, ?, ?, 0, COALESCE(?, CURRENT_TIMESTAMP), COALESCE(?, CURRENT_TIMESTAMP))
58
59
  ON CONFLICT(session_id) DO UPDATE SET
59
60
  provider = excluded.provider,
60
61
  updated_at = excluded.updated_at,
61
62
  project_path = excluded.project_path,
62
63
  jsonl_path = excluded.jsonl_path,
64
+ isArchived = 0,
63
65
  custom_name = COALESCE(excluded.custom_name, sessions.custom_name)`
64
66
  ).run(
65
67
  sessionId,
@@ -87,7 +89,7 @@ export const sessionsDb = {
87
89
  const db = getConnection();
88
90
  const row = db
89
91
  .prepare(
90
- `SELECT session_id, provider, project_path, jsonl_path, custom_name, created_at, updated_at
92
+ `SELECT session_id, provider, project_path, jsonl_path, custom_name, isArchived, created_at, updated_at
91
93
  FROM sessions
92
94
  WHERE session_id = ?
93
95
  ORDER BY updated_at DESC
@@ -102,8 +104,25 @@ export const sessionsDb = {
102
104
  const db = getConnection();
103
105
  return db
104
106
  .prepare(
105
- `SELECT session_id, provider, project_path, jsonl_path, custom_name, created_at, updated_at
106
- FROM sessions`
107
+ `SELECT session_id, provider, project_path, jsonl_path, custom_name, isArchived, created_at, updated_at
108
+ FROM sessions
109
+ WHERE isArchived = 0`
110
+ )
111
+ .all() as SessionRow[];
112
+ },
113
+
114
+ /**
115
+ * Archived rows are intentionally queried separately so the caller can render
116
+ * them in a dedicated view without reintroducing them into active session lists.
117
+ */
118
+ getArchivedSessions(): SessionRow[] {
119
+ const db = getConnection();
120
+ return db
121
+ .prepare(
122
+ `SELECT session_id, provider, project_path, jsonl_path, custom_name, isArchived, created_at, updated_at
123
+ FROM sessions
124
+ WHERE isArchived = 1
125
+ ORDER BY datetime(COALESCE(updated_at, created_at)) DESC, session_id DESC`
107
126
  )
108
127
  .all() as SessionRow[];
109
128
  },
@@ -113,7 +132,24 @@ export const sessionsDb = {
113
132
  const normalizedProjectPath = normalizeProjectPath(projectPath);
114
133
  return db
115
134
  .prepare(
116
- `SELECT session_id, provider, project_path, jsonl_path, custom_name, created_at, updated_at
135
+ `SELECT session_id, provider, project_path, jsonl_path, custom_name, isArchived, created_at, updated_at
136
+ FROM sessions
137
+ WHERE project_path = ?
138
+ AND isArchived = 0`
139
+ )
140
+ .all(normalizedProjectPath) as SessionRow[];
141
+ },
142
+
143
+ /**
144
+ * Permanent project deletion must see every session row for the path,
145
+ * including archived ones, so their transcript files can be cleaned up.
146
+ */
147
+ getSessionsByProjectPathIncludingArchived(projectPath: string): SessionRow[] {
148
+ const db = getConnection();
149
+ const normalizedProjectPath = normalizeProjectPath(projectPath);
150
+ return db
151
+ .prepare(
152
+ `SELECT session_id, provider, project_path, jsonl_path, custom_name, isArchived, created_at, updated_at
117
153
  FROM sessions
118
154
  WHERE project_path = ?`
119
155
  )
@@ -125,9 +161,10 @@ export const sessionsDb = {
125
161
  const normalizedProjectPath = normalizeProjectPath(projectPath);
126
162
  return db
127
163
  .prepare(
128
- `SELECT session_id, provider, project_path, jsonl_path, custom_name, created_at, updated_at
164
+ `SELECT session_id, provider, project_path, jsonl_path, custom_name, isArchived, created_at, updated_at
129
165
  FROM sessions
130
166
  WHERE project_path = ?
167
+ AND isArchived = 0
131
168
  ORDER BY datetime(COALESCE(updated_at, created_at)) DESC, session_id DESC
132
169
  LIMIT ? OFFSET ?`
133
170
  )
@@ -141,7 +178,8 @@ export const sessionsDb = {
141
178
  .prepare(
142
179
  `SELECT COUNT(*) AS count
143
180
  FROM sessions
144
- WHERE project_path = ?`
181
+ WHERE project_path = ?
182
+ AND isArchived = 0`
145
183
  )
146
184
  .get(normalizedProjectPath) as { count: number } | undefined;
147
185
 
@@ -167,6 +205,19 @@ export const sessionsDb = {
167
205
  return row?.custom_name ?? null;
168
206
  },
169
207
 
208
+ /**
209
+ * Soft-delete and restore both use the same flag update so callers keep the
210
+ * row, metadata, and file path intact while toggling visibility.
211
+ */
212
+ updateSessionIsArchived(sessionId: string, isArchived: boolean): void {
213
+ const db = getConnection();
214
+ db.prepare(
215
+ `UPDATE sessions
216
+ SET isArchived = ?
217
+ WHERE session_id = ?`
218
+ ).run(isArchived ? 1 : 0, sessionId);
219
+ },
220
+
170
221
  deleteSessionById(sessionId: string): boolean {
171
222
  const db = getConnection();
172
223
  return db.prepare('DELETE FROM sessions WHERE session_id = ?').run(sessionId).changes > 0;
@@ -86,6 +86,7 @@ CREATE TABLE IF NOT EXISTS sessions (
86
86
  custom_name TEXT,
87
87
  project_path TEXT,
88
88
  jsonl_path TEXT,
89
+ isArchived BOOLEAN DEFAULT 0,
89
90
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
90
91
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
91
92
  PRIMARY KEY (session_id),
@@ -3,9 +3,9 @@ import express from 'express';
3
3
  import { createProject, updateProjectDisplayName } from '@/modules/projects/services/project-management.service.js';
4
4
  import { startCloneProject } from '@/modules/projects/services/project-clone.service.js';
5
5
  import { getProjectTaskMaster } from '@/modules/projects/services/projects-has-taskmaster.service.js';
6
- import { AppError, asyncHandler } from '@/shared/utils.js';
7
- import { getProjectSessionsPage, getProjectsWithSessions } from '@/modules/projects/services/projects-with-sessions-fetch.service.js';
8
- import { deleteOrArchiveProject } from '@/modules/projects/services/project-delete.service.js';
6
+ import { AppError, asyncHandler, createApiSuccessResponse } from '@/shared/utils.js';
7
+ import { getArchivedProjectsWithSessions, getProjectSessionsPage, getProjectsWithSessions } from '@/modules/projects/services/projects-with-sessions-fetch.service.js';
8
+ import { deleteOrArchiveProject, restoreArchivedProject } from '@/modules/projects/services/project-delete.service.js';
9
9
  import { applyLegacyStarredProjectIds, toggleProjectStar } from '@/modules/projects/services/project-star.service.js';
10
10
 
11
11
  const router = express.Router();
@@ -73,6 +73,14 @@ router.get(
73
73
  }),
74
74
  );
75
75
 
76
+ router.get(
77
+ '/archived',
78
+ asyncHandler(async (_req, res) => {
79
+ const projects = await getArchivedProjectsWithSessions();
80
+ res.json(createApiSuccessResponse({ projects }));
81
+ }),
82
+ );
83
+
76
84
  router.get(
77
85
  '/:projectId/sessions',
78
86
  asyncHandler(async (req, res) => {
@@ -230,6 +238,15 @@ router.post(
230
238
  }),
231
239
  );
232
240
 
241
+ router.post(
242
+ '/:projectId/restore',
243
+ asyncHandler(async (req, res) => {
244
+ const projectId = typeof req.params.projectId === 'string' ? req.params.projectId : '';
245
+ restoreArchivedProject(projectId);
246
+ res.json(createApiSuccessResponse({ projectId, isArchived: false }));
247
+ }),
248
+ );
249
+
233
250
  /**
234
251
  * - `force` not set / false: archive project in DB only (`isArchived` = 1; hidden from active list).
235
252
  * - `force=true`: remove DB row, delete session rows for that path, remove all `*.jsonl` under the Claude project dir.
@@ -42,7 +42,7 @@ async function unlinkJsonlIfExists(filePath: string): Promise<void> {
42
42
  * Loads all session rows for the project path and removes each distinct `jsonl_path` file on disk.
43
43
  */
44
44
  export async function deleteSessionJsonlFilesForProjectPath(projectPath: string): Promise<void> {
45
- const sessions = sessionsDb.getSessionsByProjectPath(projectPath);
45
+ const sessions = sessionsDb.getSessionsByProjectPathIncludingArchived(projectPath);
46
46
  const paths = uniqueJsonlPathsFromSessions(sessions);
47
47
 
48
48
  for (const filePath of paths) {
@@ -73,3 +73,18 @@ export async function deleteOrArchiveProject(projectId: string, force: boolean):
73
73
  sessionsDb.deleteSessionsByProjectPath(row.project_path);
74
74
  projectsDb.deleteProjectById(projectId);
75
75
  }
76
+
77
+ /**
78
+ * Restores one archived project row back into the active project list.
79
+ */
80
+ export function restoreArchivedProject(projectId: string): void {
81
+ const row = projectsDb.getProjectById(projectId);
82
+ if (!row) {
83
+ throw new AppError(`Unknown projectId: ${projectId}`, {
84
+ code: 'PROJECT_NOT_FOUND',
85
+ statusCode: 404,
86
+ });
87
+ }
88
+
89
+ projectsDb.updateProjectIsArchivedById(projectId, false);
90
+ }
@@ -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,