@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
@@ -2,9 +2,17 @@ import express, { type Request, type Response } from 'express';
2
2
 
3
3
  import { providerAuthService } from '@/modules/providers/services/provider-auth.service.js';
4
4
  import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
5
+ import { providerModelsService } from '@/modules/providers/services/provider-models.service.js';
6
+ import { providerSkillsService } from '@/modules/providers/services/skills.service.js';
5
7
  import { sessionConversationsSearchService } from '@/modules/providers/services/session-conversations-search.service.js';
6
8
  import { sessionsService } from '@/modules/providers/services/sessions.service.js';
7
- import type { LLMProvider, McpScope, McpTransport, UpsertProviderMcpServerInput } from '@/shared/types.js';
9
+ import type {
10
+ LLMProvider,
11
+ McpScope,
12
+ McpTransport,
13
+ ProviderChangeActiveModelInput,
14
+ UpsertProviderMcpServerInput,
15
+ } from '@/shared/types.js';
8
16
  import { AppError, asyncHandler, createApiSuccessResponse } from '@/shared/utils.js';
9
17
 
10
18
  const router = express.Router();
@@ -172,7 +180,13 @@ const parseMcpUpsertPayload = (payload: unknown): UpsertProviderMcpServerInput =
172
180
 
173
181
  const parseProvider = (value: unknown): LLMProvider => {
174
182
  const normalized = normalizeProviderParam(value);
175
- if (normalized === 'claude' || normalized === 'codex' || normalized === 'cursor' || normalized === 'gemini') {
183
+ if (
184
+ normalized === 'claude'
185
+ || normalized === 'codex'
186
+ || normalized === 'cursor'
187
+ || normalized === 'gemini'
188
+ || normalized === 'opencode'
189
+ ) {
176
190
  return normalized;
177
191
  }
178
192
 
@@ -238,6 +252,29 @@ const parseSessionSearchLimit = (value: unknown): number => {
238
252
  return Math.max(1, Math.min(parsed, 100));
239
253
  };
240
254
 
255
+ const parseChangeActiveModelPayload = (payload: unknown): ProviderChangeActiveModelInput => {
256
+ if (!payload || typeof payload !== 'object') {
257
+ throw new AppError('Request body must be an object.', {
258
+ code: 'INVALID_REQUEST_BODY',
259
+ statusCode: 400,
260
+ });
261
+ }
262
+
263
+ const body = payload as Record<string, unknown>;
264
+ const model = readOptionalQueryString(body.model);
265
+ if (!model) {
266
+ throw new AppError('model is required.', {
267
+ code: 'MODEL_REQUIRED',
268
+ statusCode: 400,
269
+ });
270
+ }
271
+
272
+ return {
273
+ sessionId: '',
274
+ model,
275
+ };
276
+ };
277
+
241
278
  router.get(
242
279
  '/:provider/auth/status',
243
280
  asyncHandler(async (req: Request, res: Response) => {
@@ -247,6 +284,41 @@ router.get(
247
284
  }),
248
285
  );
249
286
 
287
+ router.get(
288
+ '/:provider/models',
289
+ asyncHandler(async (req: Request, res: Response) => {
290
+ const provider = parseProvider(req.params.provider);
291
+ const bypassCache = parseOptionalBooleanQuery(req.query.bypassCache, 'bypassCache') ?? false;
292
+ const result = await providerModelsService.getProviderModels(provider, { bypassCache });
293
+ res.json(createApiSuccessResponse({ provider, models: result.models, cache: result.cache }));
294
+ }),
295
+ );
296
+
297
+ router.post(
298
+ '/:provider/sessions/:sessionId/active-model',
299
+ asyncHandler(async (req: Request, res: Response) => {
300
+ const provider = parseProvider(req.params.provider);
301
+ const sessionId = parseSessionId(req.params.sessionId);
302
+ const payload = parseChangeActiveModelPayload(req.body);
303
+ const result = await providerModelsService.changeActiveModel(provider, {
304
+ ...payload,
305
+ sessionId,
306
+ });
307
+ res.json(createApiSuccessResponse(result));
308
+ }),
309
+ );
310
+
311
+ // ----------------- Skills routes -----------------
312
+ router.get(
313
+ '/:provider/skills',
314
+ asyncHandler(async (req: Request, res: Response) => {
315
+ const provider = parseProvider(req.params.provider);
316
+ const workspacePath = readOptionalQueryString(req.query.workspacePath);
317
+ const skills = await providerSkillsService.listProviderSkills(provider, { workspacePath });
318
+ res.json(createApiSuccessResponse({ provider, skills }));
319
+ }),
320
+ );
321
+
250
322
  // ----------------- MCP routes -----------------
251
323
  router.get(
252
324
  '/:provider/mcp/servers',
@@ -311,12 +383,33 @@ router.post(
311
383
  );
312
384
 
313
385
  // ----------------- Session routes -----------------
386
+ router.get(
387
+ '/sessions/archived',
388
+ asyncHandler(async (_req: Request, res: Response) => {
389
+ const sessions = sessionsService.listArchivedSessions();
390
+ res.json(createApiSuccessResponse({ sessions }));
391
+ }),
392
+ );
393
+
314
394
  router.delete(
315
395
  '/sessions/:sessionId',
316
396
  asyncHandler(async (req: Request, res: Response) => {
317
397
  const sessionId = parseSessionId(req.params.sessionId);
318
- const deletedFromDisk = parseOptionalBooleanQuery(req.query.deletedFromDisk, 'deletedFromDisk') ?? false;
319
- const result = await sessionsService.deleteSessionById(sessionId, deletedFromDisk);
398
+ const force = parseOptionalBooleanQuery(req.query.force, 'force') ?? false;
399
+ const deletedFromDisk = parseOptionalBooleanQuery(req.query.deletedFromDisk, 'deletedFromDisk') ?? force;
400
+ const result = await sessionsService.deleteOrArchiveSessionById(sessionId, {
401
+ force,
402
+ deletedFromDisk,
403
+ });
404
+ res.json(createApiSuccessResponse(result));
405
+ }),
406
+ );
407
+
408
+ router.post(
409
+ '/sessions/:sessionId/restore',
410
+ asyncHandler(async (req: Request, res: Response) => {
411
+ const sessionId = parseSessionId(req.params.sessionId);
412
+ const result = sessionsService.restoreSessionById(sessionId);
320
413
  res.json(createApiSuccessResponse(result));
321
414
  }),
322
415
  );
@@ -1,18 +1,7 @@
1
- import os from 'node:os';
2
-
3
1
  import { providerRegistry } from '@/modules/providers/provider.registry.js';
4
2
  import type { LLMProvider, McpScope, ProviderMcpServer, UpsertProviderMcpServerInput } from '@/shared/types.js';
5
3
  import { AppError } from '@/shared/utils.js';
6
4
 
7
- /** Cursor MCP is not supported on Windows hosts (no Cursor CLI integration). */
8
- function includeProviderInGlobalMcp(providerId: LLMProvider): boolean {
9
- if (providerId === 'cursor' && os.platform() === 'win32') {
10
- return false;
11
- }
12
-
13
- return true;
14
- }
15
-
16
5
 
17
6
  export const providerMcpService = {
18
7
  /**
@@ -75,7 +64,7 @@ export const providerMcpService = {
75
64
 
76
65
  const scope = input.scope ?? 'project';
77
66
  const results: Array<{ provider: LLMProvider; created: boolean; error?: string }> = [];
78
- const providers = providerRegistry.listProviders().filter((p) => includeProviderInGlobalMcp(p.id));
67
+ const providers = providerRegistry.listProviders();
79
68
  for (const provider of providers) {
80
69
  try {
81
70
  await provider.mcp.upsertServer({ ...input, scope });
@@ -0,0 +1,325 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { providerRegistry } from '@/modules/providers/provider.registry.js';
6
+ import type { IProvider } from '@/shared/interfaces.js';
7
+ import type {
8
+ LLMProvider,
9
+ ProviderChangeActiveModelInput,
10
+ ProviderCurrentActiveModel,
11
+ ProviderModelsCacheInfo,
12
+ ProviderModelsDefinition,
13
+ ProviderModelsResult,
14
+ ProviderSessionActiveModelChange,
15
+ } from '@/shared/types.js';
16
+ import { readProviderSessionActiveModelChange } from '@/shared/utils.js';
17
+
18
+ export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000;
19
+ const PROVIDER_MODELS_CACHE_VERSION = 1;
20
+
21
+ type ProviderModelsServiceDependencies = {
22
+ resolveProvider?: (provider: LLMProvider) => Pick<IProvider, 'models'>;
23
+ cachePath?: string;
24
+ activeModelChangesPath?: string;
25
+ now?: () => number;
26
+ };
27
+
28
+ type ProviderModelsOptions = {
29
+ bypassCache?: boolean;
30
+ };
31
+
32
+ type ProviderModelsCacheEntry = {
33
+ updatedAt: number;
34
+ expiresAt: number;
35
+ models: ProviderModelsDefinition;
36
+ };
37
+
38
+ type ProviderModelsCacheFile = {
39
+ version: number;
40
+ entries: Record<string, ProviderModelsCacheEntry>;
41
+ };
42
+
43
+ const getProviderModelsCachePath = (): string => path.join(
44
+ os.homedir(),
45
+ '.cloudcli',
46
+ 'provider-models-cache.json',
47
+ );
48
+
49
+ const toProviderModelsCacheInfo = (
50
+ entry: ProviderModelsCacheEntry,
51
+ source: ProviderModelsCacheInfo['source'],
52
+ ): ProviderModelsCacheInfo => ({
53
+ updatedAt: new Date(entry.updatedAt).toISOString(),
54
+ expiresAt: new Date(entry.expiresAt).toISOString(),
55
+ source,
56
+ });
57
+
58
+ const isProviderModelOption = (
59
+ value: unknown,
60
+ ): value is ProviderModelsDefinition['OPTIONS'][number] => (
61
+ Boolean(value)
62
+ && typeof value === 'object'
63
+ && typeof (value as ProviderModelsDefinition['OPTIONS'][number]).value === 'string'
64
+ && typeof (value as ProviderModelsDefinition['OPTIONS'][number]).label === 'string'
65
+ && (
66
+ typeof (value as ProviderModelsDefinition['OPTIONS'][number]).description === 'undefined'
67
+ || typeof (value as ProviderModelsDefinition['OPTIONS'][number]).description === 'string'
68
+ )
69
+ );
70
+
71
+ const isProviderModelsDefinition = (value: unknown): value is ProviderModelsDefinition => (
72
+ Boolean(value)
73
+ && typeof value === 'object'
74
+ && Array.isArray((value as ProviderModelsDefinition).OPTIONS)
75
+ && (value as ProviderModelsDefinition).OPTIONS.every(isProviderModelOption)
76
+ && typeof (value as ProviderModelsDefinition).DEFAULT === 'string'
77
+ );
78
+
79
+ const isProviderModelsCacheEntry = (value: unknown): value is ProviderModelsCacheEntry => (
80
+ Boolean(value)
81
+ && typeof value === 'object'
82
+ && typeof (value as ProviderModelsCacheEntry).updatedAt === 'number'
83
+ && typeof (value as ProviderModelsCacheEntry).expiresAt === 'number'
84
+ && isProviderModelsDefinition((value as ProviderModelsCacheEntry).models)
85
+ );
86
+
87
+ const readProviderModelsCacheFile = async (
88
+ cachePath: string,
89
+ ): Promise<ProviderModelsCacheFile | null> => {
90
+ try {
91
+ const raw = await readFile(cachePath, 'utf8');
92
+ const parsed = JSON.parse(raw) as Partial<ProviderModelsCacheFile>;
93
+ if (parsed.version !== PROVIDER_MODELS_CACHE_VERSION || !parsed.entries || typeof parsed.entries !== 'object') {
94
+ return null;
95
+ }
96
+
97
+ const entries = Object.fromEntries(
98
+ Object.entries(parsed.entries).filter((entry): entry is [string, ProviderModelsCacheEntry] =>
99
+ isProviderModelsCacheEntry(entry[1]),
100
+ ),
101
+ );
102
+
103
+ return {
104
+ version: PROVIDER_MODELS_CACHE_VERSION,
105
+ entries,
106
+ };
107
+ } catch {
108
+ return null;
109
+ }
110
+ };
111
+
112
+ const writeProviderModelsCacheFile = async (
113
+ cachePath: string,
114
+ entries: Map<LLMProvider, ProviderModelsCacheEntry>,
115
+ now: number,
116
+ ): Promise<void> => {
117
+ const serializableEntries = Object.fromEntries(
118
+ [...entries.entries()].filter(([, entry]) => entry.expiresAt > now),
119
+ );
120
+ const payload: ProviderModelsCacheFile = {
121
+ version: PROVIDER_MODELS_CACHE_VERSION,
122
+ entries: serializableEntries,
123
+ };
124
+
125
+ await mkdir(path.dirname(cachePath), { recursive: true });
126
+ await writeFile(cachePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
127
+ };
128
+
129
+ /**
130
+ * Provider model lookup service.
131
+ *
132
+ * Routes and other service callers use this layer instead of resolving provider
133
+ * classes directly so the provider-registry dependency stays centralized in one
134
+ * place.
135
+ */
136
+ export const createProviderModelsService = (dependencies: ProviderModelsServiceDependencies = {}) => {
137
+ const resolveProvider = dependencies.resolveProvider ?? providerRegistry.resolveProvider;
138
+ const cachePath = dependencies.cachePath ?? getProviderModelsCachePath();
139
+ const activeModelChangesPath = dependencies.activeModelChangesPath;
140
+ const now = dependencies.now ?? (() => Date.now());
141
+ const memoryCache = new Map<LLMProvider, ProviderModelsCacheEntry>();
142
+ const pendingRequests = new Map<LLMProvider, Promise<ProviderModelsResult>>();
143
+ let persistedCacheLoaded = false;
144
+ let persistedCacheLoadPromise: Promise<void> | null = null;
145
+
146
+ const pruneExpiredMemoryEntry = (
147
+ provider: LLMProvider,
148
+ currentTime: number,
149
+ source: ProviderModelsCacheInfo['source'],
150
+ ): ProviderModelsResult | null => {
151
+ const cachedEntry = memoryCache.get(provider);
152
+ if (!cachedEntry) {
153
+ return null;
154
+ }
155
+
156
+ if (cachedEntry.expiresAt > currentTime) {
157
+ return {
158
+ models: cachedEntry.models,
159
+ cache: toProviderModelsCacheInfo(cachedEntry, source),
160
+ };
161
+ }
162
+
163
+ memoryCache.delete(provider);
164
+ return null;
165
+ };
166
+
167
+ const loadPersistedCache = async (): Promise<void> => {
168
+ if (persistedCacheLoaded) {
169
+ return;
170
+ }
171
+
172
+ if (!persistedCacheLoadPromise) {
173
+ persistedCacheLoadPromise = (async () => {
174
+ const cacheFile = await readProviderModelsCacheFile(cachePath);
175
+ const currentTime = now();
176
+
177
+ for (const [provider, entry] of Object.entries(cacheFile?.entries ?? {})) {
178
+ if (entry.expiresAt > currentTime) {
179
+ memoryCache.set(provider as LLMProvider, entry);
180
+ }
181
+ }
182
+
183
+ persistedCacheLoaded = true;
184
+ })().finally(() => {
185
+ persistedCacheLoadPromise = null;
186
+ });
187
+ }
188
+
189
+ await persistedCacheLoadPromise;
190
+ };
191
+
192
+ const persistCache = async (): Promise<void> => {
193
+ try {
194
+ await writeProviderModelsCacheFile(cachePath, memoryCache, now());
195
+ } catch (error) {
196
+ console.warn('Unable to persist provider models cache:', error);
197
+ }
198
+ };
199
+
200
+ const setCacheEntry = async (
201
+ provider: LLMProvider,
202
+ models: ProviderModelsDefinition,
203
+ ): Promise<ProviderModelsCacheEntry> => {
204
+ const currentTime = now();
205
+ const entry: ProviderModelsCacheEntry = {
206
+ updatedAt: currentTime,
207
+ expiresAt: currentTime + PROVIDER_MODELS_CACHE_TTL_MS,
208
+ models,
209
+ };
210
+
211
+ memoryCache.set(provider, entry);
212
+ await persistCache();
213
+ return entry;
214
+ };
215
+
216
+ const loadAndCacheModels = (
217
+ provider: LLMProvider,
218
+ ): Promise<ProviderModelsResult> => {
219
+ const request = resolveProvider(provider).models.getSupportedModels()
220
+ .then(async (models) => {
221
+ const entry = await setCacheEntry(provider, models);
222
+ return {
223
+ models,
224
+ cache: toProviderModelsCacheInfo(entry, 'fresh'),
225
+ };
226
+ })
227
+ .finally(() => {
228
+ pendingRequests.delete(provider);
229
+ });
230
+
231
+ pendingRequests.set(provider, request);
232
+ return request;
233
+ };
234
+
235
+ const getProviderModels = async (
236
+ provider: LLMProvider,
237
+ options: ProviderModelsOptions = {},
238
+ ): Promise<ProviderModelsResult> => {
239
+ if (options.bypassCache) {
240
+ const pendingRequest = pendingRequests.get(provider);
241
+ if (pendingRequest) {
242
+ return pendingRequest;
243
+ }
244
+
245
+ return loadAndCacheModels(provider);
246
+ }
247
+
248
+ const cachedModels = pruneExpiredMemoryEntry(provider, now(), 'memory');
249
+ if (cachedModels) {
250
+ return cachedModels;
251
+ }
252
+
253
+ const pendingRequest = pendingRequests.get(provider);
254
+ if (pendingRequest) {
255
+ return pendingRequest;
256
+ }
257
+
258
+ await loadPersistedCache();
259
+
260
+ const persistedModels = pruneExpiredMemoryEntry(provider, now(), 'disk');
261
+ if (persistedModels) {
262
+ return persistedModels;
263
+ }
264
+
265
+ const postLoadPendingRequest = pendingRequests.get(provider);
266
+ if (postLoadPendingRequest) {
267
+ return postLoadPendingRequest;
268
+ }
269
+
270
+ return loadAndCacheModels(provider);
271
+ };
272
+
273
+ const getCurrentActiveModel = async (
274
+ provider: LLMProvider,
275
+ sessionId?: string,
276
+ ): Promise<ProviderCurrentActiveModel> => resolveProvider(provider).models.getCurrentActiveModel(sessionId);
277
+
278
+ const changeActiveModel = async (
279
+ provider: LLMProvider,
280
+ input: ProviderChangeActiveModelInput,
281
+ ): Promise<ProviderSessionActiveModelChange> => resolveProvider(provider).models.changeActiveModel(input);
282
+
283
+ const getChangedActiveModel = async (
284
+ provider: LLMProvider,
285
+ sessionId: string,
286
+ ): Promise<ProviderSessionActiveModelChange> => readProviderSessionActiveModelChange(provider, sessionId, {
287
+ filePath: activeModelChangesPath,
288
+ });
289
+
290
+ const resolveResumeModel = async (
291
+ provider: LLMProvider,
292
+ sessionId: string | undefined,
293
+ requestedModel?: string | null,
294
+ ): Promise<string | undefined> => {
295
+ const normalizedRequestedModel = typeof requestedModel === 'string' ? requestedModel.trim() : '';
296
+ if (!sessionId?.trim()) {
297
+ return normalizedRequestedModel || undefined;
298
+ }
299
+
300
+ const changedModel = await getChangedActiveModel(provider, sessionId);
301
+ if (changedModel.supported && changedModel.changed && changedModel.model?.trim()) {
302
+ return changedModel.model.trim();
303
+ }
304
+
305
+ return normalizedRequestedModel || undefined;
306
+ };
307
+
308
+ const clearCache = (): void => {
309
+ memoryCache.clear();
310
+ pendingRequests.clear();
311
+ persistedCacheLoaded = false;
312
+ persistedCacheLoadPromise = null;
313
+ };
314
+
315
+ return {
316
+ getProviderModels,
317
+ getCurrentActiveModel,
318
+ getChangedActiveModel,
319
+ changeActiveModel,
320
+ resolveResumeModel,
321
+ clearCache,
322
+ };
323
+ };
324
+
325
+ export const providerModelsService = createProviderModelsService();
@@ -89,13 +89,8 @@ const RIPGREP_CHUNK_CONCURRENCY = 6;
89
89
  const UNKNOWN_PROJECT_KEY = '__unknown_project__';
90
90
 
91
91
  const INTERNAL_CONTENT_PREFIXES = [
92
- '<command-name>',
93
- '<command-message>',
94
- '<command-args>',
95
- '<local-command-stdout>',
96
92
  '<system-reminder>',
97
93
  'Caveat:',
98
- 'This session is being continued from a previous',
99
94
  'Invalid API key',
100
95
  '[Request interrupted',
101
96
  ] as const;
@@ -302,6 +297,135 @@ function extractClaudeText(content: unknown): string {
302
297
  .join(' ');
303
298
  }
304
299
 
300
+ function extractTaggedContent(content: string, tagName: string): string | null {
301
+ const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
302
+ const match = new RegExp(`<${escapedTagName}>([\\s\\S]*?)<\\/${escapedTagName}>`).exec(content);
303
+ return match ? match[1] : null;
304
+ }
305
+
306
+ type ClaudeLocalCommandPayload = {
307
+ commandName: string;
308
+ commandMessage: string;
309
+ commandArgs: string;
310
+ };
311
+
312
+ function parseClaudeLocalCommandPayload(content: string): ClaudeLocalCommandPayload | null {
313
+ const commandName = extractTaggedContent(content, 'command-name');
314
+ const commandMessage = extractTaggedContent(content, 'command-message');
315
+ const commandArgs = extractTaggedContent(content, 'command-args');
316
+
317
+ if (commandName === null && commandMessage === null && commandArgs === null) {
318
+ return null;
319
+ }
320
+
321
+ return {
322
+ commandName: commandName ?? '',
323
+ commandMessage: commandMessage ?? '',
324
+ commandArgs: commandArgs ?? '',
325
+ };
326
+ }
327
+
328
+ function buildClaudeLocalCommandDisplayText(payload: ClaudeLocalCommandPayload): string {
329
+ const commandName = payload.commandName.trim();
330
+ const commandMessage = payload.commandMessage.trim();
331
+ const commandArgs = payload.commandArgs.trim();
332
+ const baseCommand = commandName || commandMessage;
333
+
334
+ if (!baseCommand) {
335
+ return '';
336
+ }
337
+
338
+ return commandArgs ? `${baseCommand} ${commandArgs}` : baseCommand;
339
+ }
340
+
341
+ function stripAnsiFormatting(text: string): string {
342
+ return text.replace(/\u001B\[[0-9;?]*[ -/]*[@-~]/g, '');
343
+ }
344
+
345
+ type ClaudeSearchableMessage = {
346
+ text: string;
347
+ role: 'user' | 'assistant';
348
+ };
349
+
350
+ /**
351
+ * Claude mixes visible chat, compact summaries, and local command wrappers into
352
+ * the same transcript stream. Search should operate on the user-visible meaning
353
+ * of those rows rather than the raw wrapper syntax.
354
+ */
355
+ function extractClaudeSearchableMessage(entry: AnyRecord): ClaudeSearchableMessage | null {
356
+ if (!entry.message?.content || entry.isApiErrorMessage) {
357
+ return null;
358
+ }
359
+
360
+ const rawRole = entry.message.role;
361
+ if (rawRole !== 'user' && rawRole !== 'assistant') {
362
+ return null;
363
+ }
364
+
365
+ if (typeof entry.message.content === 'string') {
366
+ const content = String(entry.message.content);
367
+
368
+ if (entry.isCompactSummary === true && content.trim()) {
369
+ return {
370
+ text: content,
371
+ role: 'assistant',
372
+ };
373
+ }
374
+
375
+ const localCommand = parseClaudeLocalCommandPayload(content);
376
+ if (localCommand) {
377
+ const displayText = buildClaudeLocalCommandDisplayText(localCommand);
378
+ return displayText
379
+ ? {
380
+ text: displayText,
381
+ role: 'user',
382
+ }
383
+ : null;
384
+ }
385
+
386
+ const localCommandStdout = extractTaggedContent(content, 'local-command-stdout');
387
+ if (localCommandStdout !== null) {
388
+ const stdoutText = stripAnsiFormatting(localCommandStdout).trim();
389
+ return stdoutText
390
+ ? {
391
+ text: stdoutText,
392
+ role: 'assistant',
393
+ }
394
+ : null;
395
+ }
396
+
397
+ if (!content || isInternalContent(content)) {
398
+ return null;
399
+ }
400
+
401
+ return {
402
+ text: content,
403
+ role: rawRole,
404
+ };
405
+ }
406
+
407
+ const text = extractClaudeText(entry.message.content);
408
+ if (!text) {
409
+ return null;
410
+ }
411
+
412
+ if (entry.isCompactSummary === true) {
413
+ return {
414
+ text,
415
+ role: 'assistant',
416
+ };
417
+ }
418
+
419
+ if (isInternalContent(text)) {
420
+ return null;
421
+ }
422
+
423
+ return {
424
+ text,
425
+ role: rawRole,
426
+ };
427
+ }
428
+
305
429
  function extractCodexText(content: unknown): string {
306
430
  if (typeof content === 'string') {
307
431
  return content;
@@ -348,6 +472,7 @@ function extractGeminiText(content: unknown): string {
348
472
 
349
473
  function normalizeSearchableSessions(rows: SessionRepositoryRow[]): SearchableSessionRow[] {
350
474
  const normalizedRows: SearchableSessionRow[] = [];
475
+ const projectArchiveStateByPath = new Map<string, boolean>();
351
476
 
352
477
  for (const row of rows) {
353
478
  const provider = row.provider as SearchableProvider;
@@ -365,6 +490,27 @@ function normalizeSearchableSessions(rows: SessionRepositoryRow[]): SearchableSe
365
490
  continue;
366
491
  }
367
492
 
493
+ /**
494
+ * Active session rows can still belong to an archived project because
495
+ * project archiving intentionally preserves the underlying session data.
496
+ * Global conversation search should follow the visible workspace model,
497
+ * which means excluding any session whose owning project is archived.
498
+ *
499
+ * Cache the archive lookup per normalized project path so one search pass
500
+ * does not re-query the same project row for every session in that folder.
501
+ */
502
+ const normalizedProjectPath = typeof row.project_path === 'string' ? row.project_path.trim() : '';
503
+ if (normalizedProjectPath) {
504
+ if (!projectArchiveStateByPath.has(normalizedProjectPath)) {
505
+ const projectRow = projectsDb.getProjectPath(normalizedProjectPath);
506
+ projectArchiveStateByPath.set(normalizedProjectPath, Boolean(projectRow?.isArchived));
507
+ }
508
+
509
+ if (projectArchiveStateByPath.get(normalizedProjectPath) === true) {
510
+ continue;
511
+ }
512
+ }
513
+
368
514
  normalizedRows.push({
369
515
  ...row,
370
516
  provider,
@@ -733,18 +879,21 @@ async function parseClaudeSessionMatches(
733
879
  }
734
880
  }
735
881
 
736
- if (!entry.message?.content || entry.isApiErrorMessage) {
882
+ const searchableMessage = extractClaudeSearchableMessage(entry);
883
+ if (!searchableMessage) {
737
884
  continue;
738
885
  }
739
886
 
740
- const role = entry.message.role;
741
- if (role !== 'user' && role !== 'assistant') {
742
- continue;
743
- }
887
+ const { text, role } = searchableMessage;
744
888
 
745
- const text = extractClaudeText(entry.message.content);
746
- if (!text || isInternalContent(text)) {
747
- continue;
889
+ /**
890
+ * Claude compact summaries are the most faithful session-summary source
891
+ * after a `/compact` because they describe the post-compaction state that
892
+ * the resumed session actually continues from. Prefer them over generic
893
+ * fallback user text when present.
894
+ */
895
+ if (entry.isCompactSummary === true) {
896
+ state.resolvedSummary = text;
748
897
  }
749
898
 
750
899
  if (role === 'user') {