@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
@@ -22,6 +22,7 @@ export const sessionSynchronizerService = {
22
22
  codex: 0,
23
23
  cursor: 0,
24
24
  gemini: 0,
25
+ opencode: 0,
25
26
  };
26
27
  const failures: string[] = [];
27
28
 
@@ -18,19 +18,25 @@ const PROVIDER_WATCH_PATHS: Array<{ provider: LLMProvider; rootPath: string }> =
18
18
  },
19
19
  {
20
20
  provider: 'cursor',
21
- rootPath: path.join(os.homedir(), '.cursor', 'chats'),
21
+ rootPath: path.join(os.homedir(), '.cursor', 'projects'),
22
22
  },
23
23
  {
24
24
  provider: 'codex',
25
25
  rootPath: path.join(os.homedir(), '.codex', 'sessions'),
26
26
  },
27
+ // {
28
+ // provider: 'gemini',
29
+ // rootPath: path.join(os.homedir(), '.gemini', 'sessions'),
30
+ // },
31
+ // Keep `sessions/` watcher disabled: Gemini also mirrors artifacts there,
32
+ // which causes duplicate synchronization events.
27
33
  {
28
34
  provider: 'gemini',
29
- rootPath: path.join(os.homedir(), '.gemini', 'sessions'),
35
+ rootPath: path.join(os.homedir(), '.gemini', 'tmp'),
30
36
  },
31
37
  {
32
- provider: 'gemini',
33
- rootPath: path.join(os.homedir(), '.gemini', 'tmp'),
38
+ provider: 'opencode',
39
+ rootPath: path.join(os.homedir(), '.local', 'share', 'opencode'),
34
40
  },
35
41
  ];
36
42
 
@@ -65,6 +71,10 @@ let watcherRescheduleAfterRefresh = false;
65
71
  * Filters watcher events to provider-specific session artifact file types.
66
72
  */
67
73
  function isWatcherTargetFile(provider: LLMProvider, filePath: string): boolean {
74
+ if (provider === 'opencode') {
75
+ return path.basename(filePath) === 'opencode.db';
76
+ }
77
+
68
78
  if (provider === 'gemini') {
69
79
  return filePath.endsWith('.json') || filePath.endsWith('.jsonl');
70
80
  }
@@ -1,6 +1,7 @@
1
1
  import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
2
3
 
3
- import { sessionsDb } from '@/modules/database/index.js';
4
+ import { projectsDb, sessionsDb } from '@/modules/database/index.js';
4
5
  import { providerRegistry } from '@/modules/providers/provider.registry.js';
5
6
  import type {
6
7
  FetchHistoryOptions,
@@ -10,6 +11,19 @@ import type {
10
11
  } from '@/shared/types.js';
11
12
  import { AppError } from '@/shared/utils.js';
12
13
 
14
+ type ArchivedSessionListItem = {
15
+ sessionId: string;
16
+ provider: LLMProvider;
17
+ projectId: string | null;
18
+ projectPath: string | null;
19
+ projectDisplayName: string;
20
+ sessionTitle: string;
21
+ createdAt: string | null;
22
+ updatedAt: string | null;
23
+ lastActivity: string | null;
24
+ isProjectArchived: boolean;
25
+ };
26
+
13
27
  /**
14
28
  * Removes one file if it exists.
15
29
  */
@@ -26,6 +40,28 @@ async function removeFileIfExists(filePath: string): Promise<boolean> {
26
40
  }
27
41
  }
28
42
 
43
+ /**
44
+ * Archive rows need a stable project label even when the owning project is not
45
+ * part of the active sidebar payload. This lightweight resolver keeps the
46
+ * archive API self-contained while still matching the project's stored display
47
+ * name when one exists.
48
+ */
49
+ function resolveProjectDisplayName(
50
+ projectPath: string | null,
51
+ customProjectName: string | null | undefined,
52
+ ): string {
53
+ const trimmedCustomName = typeof customProjectName === 'string' ? customProjectName.trim() : '';
54
+ if (trimmedCustomName.length > 0) {
55
+ return trimmedCustomName;
56
+ }
57
+
58
+ if (!projectPath) {
59
+ return 'Unknown Project';
60
+ }
61
+
62
+ return path.basename(projectPath) || projectPath;
63
+ }
64
+
29
65
  /**
30
66
  * Application service for provider-backed session message operations.
31
67
  *
@@ -79,15 +115,53 @@ export const sessionsService = {
79
115
  },
80
116
 
81
117
  /**
82
- * Deletes one persisted session row by id.
118
+ * Returns archived sessions with enough project metadata for the sidebar to
119
+ * group, filter, open, and restore them without a per-row follow-up query.
120
+ */
121
+ listArchivedSessions(): ArchivedSessionListItem[] {
122
+ const archivedSessions = sessionsDb.getArchivedSessions();
123
+ const projectCache = new Map<string, ReturnType<typeof projectsDb.getProjectPath>>();
124
+
125
+ return archivedSessions.map((session) => {
126
+ const projectPath = session.project_path?.trim() ? session.project_path : null;
127
+ let project = null;
128
+
129
+ if (projectPath) {
130
+ if (!projectCache.has(projectPath)) {
131
+ projectCache.set(projectPath, projectsDb.getProjectPath(projectPath));
132
+ }
133
+ project = projectCache.get(projectPath) ?? null;
134
+ }
135
+
136
+ return {
137
+ sessionId: session.session_id,
138
+ provider: session.provider as LLMProvider,
139
+ projectId: project?.project_id ?? null,
140
+ projectPath,
141
+ projectDisplayName: resolveProjectDisplayName(projectPath, project?.custom_project_name),
142
+ sessionTitle: session.custom_name?.trim() || session.session_id,
143
+ createdAt: session.created_at ?? null,
144
+ updatedAt: session.updated_at ?? null,
145
+ lastActivity: session.updated_at ?? session.created_at ?? null,
146
+ isProjectArchived: Boolean(project?.isArchived),
147
+ };
148
+ });
149
+ },
150
+
151
+ /**
152
+ * Archives or permanently deletes one persisted session row by id.
83
153
  *
84
- * When `deletedFromDisk` is true and a session `jsonl_path` exists, the path
85
- * is deleted from disk before the DB row is removed.
154
+ * Soft-delete mirrors the project behavior by toggling `isArchived` so the
155
+ * row disappears from active lists but remains restorable. Force-delete
156
+ * optionally removes the transcript file before deleting the database row.
86
157
  */
87
- async deleteSessionById(
158
+ async deleteOrArchiveSessionById(
88
159
  sessionId: string,
89
- deletedFromDisk = false,
90
- ): Promise<{ sessionId: string; deletedFromDisk: boolean }> {
160
+ options: {
161
+ force?: boolean;
162
+ deletedFromDisk?: boolean;
163
+ } = {},
164
+ ): Promise<{ sessionId: string; action: 'archived' | 'deleted'; deletedFromDisk: boolean }> {
91
165
  const session = sessionsDb.getSessionById(sessionId);
92
166
  if (!session) {
93
167
  throw new AppError(`Session "${sessionId}" was not found.`, {
@@ -96,8 +170,17 @@ export const sessionsService = {
96
170
  });
97
171
  }
98
172
 
173
+ if (!options.force) {
174
+ sessionsDb.updateSessionIsArchived(sessionId, true);
175
+ return {
176
+ sessionId,
177
+ action: 'archived',
178
+ deletedFromDisk: false,
179
+ };
180
+ }
181
+
99
182
  let removedFromDisk = false;
100
- if (deletedFromDisk && session.jsonl_path) {
183
+ if (options.deletedFromDisk && session.jsonl_path) {
101
184
  removedFromDisk = await removeFileIfExists(session.jsonl_path);
102
185
  }
103
186
 
@@ -109,7 +192,27 @@ export const sessionsService = {
109
192
  });
110
193
  }
111
194
 
112
- return { sessionId, deletedFromDisk: removedFromDisk };
195
+ return {
196
+ sessionId,
197
+ action: 'deleted',
198
+ deletedFromDisk: removedFromDisk,
199
+ };
200
+ },
201
+
202
+ /**
203
+ * Restores one archived session back into the active sidebar lists.
204
+ */
205
+ restoreSessionById(sessionId: string): { sessionId: string; isArchived: false } {
206
+ const session = sessionsDb.getSessionById(sessionId);
207
+ if (!session) {
208
+ throw new AppError(`Session "${sessionId}" was not found.`, {
209
+ code: 'SESSION_NOT_FOUND',
210
+ statusCode: 404,
211
+ });
212
+ }
213
+
214
+ sessionsDb.updateSessionIsArchived(sessionId, false);
215
+ return { sessionId, isArchived: false };
113
216
  },
114
217
 
115
218
  /**
@@ -0,0 +1,15 @@
1
+ import { providerRegistry } from '@/modules/providers/provider.registry.js';
2
+ import type { ProviderSkill, ProviderSkillListOptions } from '@/shared/types.js';
3
+
4
+ export const providerSkillsService = {
5
+ /**
6
+ * Lists normalized skills visible to one provider.
7
+ */
8
+ async listProviderSkills(
9
+ providerName: string,
10
+ options?: ProviderSkillListOptions,
11
+ ): Promise<ProviderSkill[]> {
12
+ const provider = providerRegistry.resolveProvider(providerName);
13
+ return provider.skills.listSkills(options);
14
+ },
15
+ };
@@ -2,7 +2,9 @@ import type {
2
2
  IProvider,
3
3
  IProviderAuth,
4
4
  IProviderMcp,
5
+ IProviderModels,
5
6
  IProviderSessionSynchronizer,
7
+ IProviderSkills,
6
8
  IProviderSessions,
7
9
  } from '@/shared/interfaces.js';
8
10
  import type { LLMProvider } from '@/shared/types.js';
@@ -16,8 +18,10 @@ import type { LLMProvider } from '@/shared/types.js';
16
18
  */
17
19
  export abstract class AbstractProvider implements IProvider {
18
20
  readonly id: LLMProvider;
21
+ abstract readonly models: IProviderModels;
19
22
  abstract readonly mcp: IProviderMcp;
20
23
  abstract readonly auth: IProviderAuth;
24
+ abstract readonly skills: IProviderSkills;
21
25
  abstract readonly sessions: IProviderSessions;
22
26
  abstract readonly sessionSynchronizer: IProviderSessionSynchronizer;
23
27
 
@@ -0,0 +1,64 @@
1
+ import path from 'node:path';
2
+
3
+ import type { IProviderSkills } from '@/shared/interfaces.js';
4
+ import type {
5
+ LLMProvider,
6
+ ProviderSkill,
7
+ ProviderSkillListOptions,
8
+ ProviderSkillSource,
9
+ } from '@/shared/types.js';
10
+ import {
11
+ findProviderSkillMarkdownFiles,
12
+ readProviderSkillMarkdownDefinition,
13
+ } from '@/shared/utils.js';
14
+
15
+ const resolveWorkspacePath = (workspacePath?: string): string =>
16
+ path.resolve(workspacePath ?? process.cwd());
17
+
18
+ /**
19
+ * Shared skills provider for provider-specific skill source discovery.
20
+ */
21
+ export abstract class SkillsProvider implements IProviderSkills {
22
+ protected readonly provider: LLMProvider;
23
+
24
+ protected constructor(provider: LLMProvider) {
25
+ this.provider = provider;
26
+ }
27
+
28
+ async listSkills(options?: ProviderSkillListOptions): Promise<ProviderSkill[]> {
29
+ const workspacePath = resolveWorkspacePath(options?.workspacePath);
30
+ const sources = await this.getSkillSources(workspacePath);
31
+ const skills: ProviderSkill[] = [];
32
+
33
+ for (const source of sources) {
34
+ const skillFiles = await findProviderSkillMarkdownFiles(source.rootDir, {
35
+ recursive: source.recursive,
36
+ });
37
+ for (const skillPath of skillFiles) {
38
+ try {
39
+ const definition = await readProviderSkillMarkdownDefinition(skillPath);
40
+ const command = source.commandForSkill
41
+ ? source.commandForSkill(definition.name)
42
+ : `${source.commandPrefix ?? '/'}${definition.name}`;
43
+
44
+ skills.push({
45
+ provider: this.provider,
46
+ name: definition.name,
47
+ description: definition.description,
48
+ command,
49
+ scope: source.scope,
50
+ sourcePath: skillPath,
51
+ pluginName: source.pluginName,
52
+ pluginId: source.pluginId,
53
+ });
54
+ } catch {
55
+ // A malformed or unreadable skill markdown file should not hide other valid skills.
56
+ }
57
+ }
58
+ }
59
+
60
+ return skills;
61
+ }
62
+
63
+ protected abstract getSkillSources(workspacePath: string): Promise<ProviderSkillSource[]>;
64
+ }
@@ -169,6 +169,93 @@ test('providerMcpService handles codex MCP TOML config and capability validation
169
169
  }
170
170
  });
171
171
 
172
+ /**
173
+ * This test covers OpenCode MCP support for user/project config files, JSONC-compatible
174
+ * reads, and validation for unsupported scope/transport combinations.
175
+ */
176
+ test('providerMcpService handles opencode MCP config and capability validation', { concurrency: false }, async () => {
177
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-opencode-'));
178
+ const workspacePath = path.join(tempRoot, 'workspace');
179
+ await fs.mkdir(workspacePath, { recursive: true });
180
+ await fs.mkdir(path.join(tempRoot, '.config', 'opencode'), { recursive: true });
181
+ await fs.writeFile(
182
+ path.join(tempRoot, '.config', 'opencode', 'opencode.jsonc'),
183
+ `{
184
+ // Existing comments should not block OpenCode MCP reads.
185
+ "mcp": {}
186
+ }\n`,
187
+ 'utf8',
188
+ );
189
+
190
+ const restoreHomeDir = patchHomeDir(tempRoot);
191
+ try {
192
+ await providerMcpService.upsertProviderMcpServer('opencode', {
193
+ name: 'opencode-user-stdio',
194
+ scope: 'user',
195
+ transport: 'stdio',
196
+ command: 'node',
197
+ args: ['server.js'],
198
+ env: { API_KEY: 'x' },
199
+ });
200
+
201
+ await providerMcpService.upsertProviderMcpServer('opencode', {
202
+ name: 'opencode-project-http',
203
+ scope: 'project',
204
+ transport: 'http',
205
+ url: 'https://opencode.example.com/mcp',
206
+ headers: { Authorization: 'Bearer token' },
207
+ workspacePath,
208
+ });
209
+
210
+ const userConfig = await readJson(path.join(tempRoot, '.config', 'opencode', 'opencode.jsonc'));
211
+ const userServers = userConfig.mcp as Record<string, unknown>;
212
+ const userStdio = userServers['opencode-user-stdio'] as Record<string, unknown>;
213
+ assert.equal(userStdio.type, 'local');
214
+ assert.deepEqual(userStdio.command, ['node', 'server.js']);
215
+ assert.deepEqual(userStdio.environment, { API_KEY: 'x' });
216
+
217
+ const projectConfig = await readJson(path.join(workspacePath, 'opencode.json'));
218
+ const projectServers = projectConfig.mcp as Record<string, unknown>;
219
+ const projectHttp = projectServers['opencode-project-http'] as Record<string, unknown>;
220
+ assert.equal(projectHttp.type, 'remote');
221
+ assert.equal(projectHttp.url, 'https://opencode.example.com/mcp');
222
+
223
+ const grouped = await providerMcpService.listProviderMcpServers('opencode', { workspacePath });
224
+ assert.ok(grouped.user.some((server) => server.name === 'opencode-user-stdio' && server.transport === 'stdio'));
225
+ assert.ok(grouped.project.some((server) => server.name === 'opencode-project-http' && server.transport === 'http'));
226
+
227
+ await assert.rejects(
228
+ providerMcpService.upsertProviderMcpServer('opencode', {
229
+ name: 'opencode-local',
230
+ scope: 'local',
231
+ transport: 'stdio',
232
+ command: 'node',
233
+ }),
234
+ (error: unknown) =>
235
+ error instanceof AppError &&
236
+ error.code === 'MCP_SCOPE_NOT_SUPPORTED' &&
237
+ error.statusCode === 400,
238
+ );
239
+
240
+ await assert.rejects(
241
+ providerMcpService.upsertProviderMcpServer('opencode', {
242
+ name: 'opencode-sse',
243
+ scope: 'project',
244
+ transport: 'sse',
245
+ url: 'https://example.com/sse',
246
+ workspacePath,
247
+ }),
248
+ (error: unknown) =>
249
+ error instanceof AppError &&
250
+ error.code === 'MCP_TRANSPORT_NOT_SUPPORTED' &&
251
+ error.statusCode === 400,
252
+ );
253
+ } finally {
254
+ restoreHomeDir();
255
+ await fs.rm(tempRoot, { recursive: true, force: true });
256
+ }
257
+ });
258
+
172
259
  /**
173
260
  * This test covers Gemini/Cursor MCP JSON formats and user/project scope persistence.
174
261
  */
@@ -254,8 +341,7 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
254
341
  workspacePath,
255
342
  });
256
343
 
257
- const expectCursorGlobal = process.platform !== 'win32';
258
- assert.equal(globalResult.length, expectCursorGlobal ? 4 : 3);
344
+ assert.equal(globalResult.length, 5);
259
345
  assert.ok(globalResult.every((entry) => entry.created === true));
260
346
 
261
347
  const claudeProject = await readJson(path.join(workspacePath, '.mcp.json'));
@@ -267,10 +353,11 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
267
353
  const geminiProject = await readJson(path.join(workspacePath, '.gemini', 'settings.json'));
268
354
  assert.ok((geminiProject.mcpServers as Record<string, unknown>)['global-http']);
269
355
 
270
- if (expectCursorGlobal) {
271
- const cursorProject = await readJson(path.join(workspacePath, '.cursor', 'mcp.json'));
272
- assert.ok((cursorProject.mcpServers as Record<string, unknown>)['global-http']);
273
- }
356
+ const opencodeProject = await readJson(path.join(workspacePath, 'opencode.json'));
357
+ assert.ok((opencodeProject.mcp as Record<string, unknown>)['global-http']);
358
+
359
+ const cursorProject = await readJson(path.join(workspacePath, '.cursor', 'mcp.json'));
360
+ assert.ok((cursorProject.mcpServers as Record<string, unknown>)['global-http']);
274
361
 
275
362
  await assert.rejects(
276
363
  providerMcpService.addMcpServerToAllProviders({
@@ -0,0 +1,73 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ buildOpenCodeDefinitionFromIds,
6
+ parseOpenCodeModelsStdout,
7
+ } from '@/modules/providers/list/opencode/opencode-models.provider.js';
8
+
9
+ test('OpenCode models provider parses plain CLI output and removes duplicates', () => {
10
+ const ids = parseOpenCodeModelsStdout(`
11
+ opencode/big-pickle
12
+ not a model
13
+ anthropic/claude-opus-4-7-fast
14
+ anthropic/claude-opus-4-7-fast
15
+ openai/gpt-5.5-pro
16
+ `);
17
+
18
+ assert.deepEqual(ids, [
19
+ 'opencode/big-pickle',
20
+ 'anthropic/claude-opus-4-7-fast',
21
+ 'openai/gpt-5.5-pro',
22
+ ]);
23
+ });
24
+
25
+ test('OpenCode models provider formats frontend labels from provider-prefixed ids', () => {
26
+ const definition = buildOpenCodeDefinitionFromIds([
27
+ 'opencode/deepseek-v4-flash-free',
28
+ 'opencode/nemotron-3-super-free',
29
+ 'anthropic/claude-3-5-sonnet-20241022',
30
+ 'anthropic/claude-opus-4-7-fast',
31
+ 'openai/gpt-5.4-mini-fast',
32
+ 'openai/gpt-5.5-pro',
33
+ 'newprovider/alpha-v12-special-20261231',
34
+ ]);
35
+
36
+ assert.deepEqual(definition.OPTIONS, [
37
+ {
38
+ value: 'opencode/deepseek-v4-flash-free',
39
+ label: 'Deepseek V4 Flash Free',
40
+ description: 'opencode - opencode/deepseek-v4-flash-free',
41
+ },
42
+ {
43
+ value: 'opencode/nemotron-3-super-free',
44
+ label: 'Nemotron 3 Super Free',
45
+ description: 'opencode - opencode/nemotron-3-super-free',
46
+ },
47
+ {
48
+ value: 'anthropic/claude-3-5-sonnet-20241022',
49
+ label: 'Claude 3.5 Sonnet (2024-10-22)',
50
+ description: 'anthropic - anthropic/claude-3-5-sonnet-20241022',
51
+ },
52
+ {
53
+ value: 'anthropic/claude-opus-4-7-fast',
54
+ label: 'Claude Opus 4.7 Fast',
55
+ description: 'anthropic - anthropic/claude-opus-4-7-fast',
56
+ },
57
+ {
58
+ value: 'openai/gpt-5.4-mini-fast',
59
+ label: 'GPT-5.4 Mini Fast',
60
+ description: 'openai - openai/gpt-5.4-mini-fast',
61
+ },
62
+ {
63
+ value: 'openai/gpt-5.5-pro',
64
+ label: 'GPT-5.5 Pro',
65
+ description: 'openai - openai/gpt-5.5-pro',
66
+ },
67
+ {
68
+ value: 'newprovider/alpha-v12-special-20261231',
69
+ label: 'Alpha V12 Special (2026-12-31)',
70
+ description: 'newprovider - newprovider/alpha-v12-special-20261231',
71
+ },
72
+ ]);
73
+ });