@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,7 +2,7 @@ import express from 'express';
2
2
  import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
- import { CURSOR_MODELS } from '../../shared/modelConstants.js';
5
+ import { CURSOR_FALLBACK_MODELS } from '../modules/providers/list/cursor/cursor-models.provider.js';
6
6
 
7
7
  const router = express.Router();
8
8
 
@@ -29,7 +29,7 @@ router.get('/config', async (req, res) => {
29
29
  config: {
30
30
  version: 1,
31
31
  model: {
32
- modelId: CURSOR_MODELS.DEFAULT,
32
+ modelId: CURSOR_FALLBACK_MODELS.DEFAULT,
33
33
  displayName: 'GPT-5',
34
34
  },
35
35
  permissions: {
@@ -1,4 +1,5 @@
1
1
  import express from 'express';
2
+
2
3
  import { apiKeysDb, credentialsDb, notificationPreferencesDb, pushSubscriptionsDb } from '../modules/database/index.js';
3
4
  import { getPublicKey } from '../services/vapid-keys.js';
4
5
  import { createNotificationEvent, notifyUserIfEnabled } from '../services/notification-orchestrator.js';
@@ -273,14 +274,4 @@ router.post('/push/unsubscribe', async (req, res) => {
273
274
  }
274
275
  });
275
276
 
276
- // Host OS for UI (e.g. hide Cursor agent when the backend runs on Windows).
277
- router.get('/server-env', async (req, res) => {
278
- try {
279
- res.json({ platform: process.platform });
280
- } catch (error) {
281
- console.error('Error reading server environment:', error);
282
- res.status(500).json({ error: 'Failed to read server environment' });
283
- }
284
- });
285
-
286
277
  export default router;
@@ -0,0 +1,82 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { executeModelsCommand } from '../commands.js';
5
+ import { providerModelsService } from '../../modules/providers/services/provider-models.service.js';
6
+
7
+ test('models command returns available models only for the active provider', async () => {
8
+ const originalGetProviderModels = providerModelsService.getProviderModels;
9
+ const originalGetCurrentActiveModel = providerModelsService.getCurrentActiveModel;
10
+ let getCurrentActiveModelCalls = 0;
11
+
12
+ providerModelsService.getProviderModels = async () => ({
13
+ models: {
14
+ OPTIONS: [{ value: 'gpt-5.4', label: 'gpt-5.4' }],
15
+ DEFAULT: 'gpt-5.4',
16
+ },
17
+ cache: {
18
+ updatedAt: '2026-01-01T00:00:00.000Z',
19
+ expiresAt: '2026-01-04T00:00:00.000Z',
20
+ source: 'fresh',
21
+ },
22
+ });
23
+ providerModelsService.getCurrentActiveModel = async () => {
24
+ getCurrentActiveModelCalls += 1;
25
+ return {
26
+ model: 'gpt-5.3-codex',
27
+ };
28
+ };
29
+
30
+ try {
31
+ const result = await executeModelsCommand([], {
32
+ provider: 'codex',
33
+ model: 'gpt-5.4',
34
+ });
35
+
36
+ assert.equal(result.type, 'builtin');
37
+ assert.equal(result.action, 'models');
38
+ assert.equal(result.data.current.provider, 'codex');
39
+ assert.equal(result.data.current.model, 'gpt-5.4');
40
+ assert.deepEqual(Object.keys(result.data.available), ['codex']);
41
+ assert.deepEqual(result.data.available.codex, result.data.availableModels);
42
+ assert.ok(result.data.availableModels.includes('gpt-5.4'));
43
+ assert.equal(result.data.available.claude, undefined);
44
+ assert.equal(result.data.available.cursor, undefined);
45
+ assert.equal(getCurrentActiveModelCalls, 0);
46
+ } finally {
47
+ providerModelsService.getProviderModels = originalGetProviderModels;
48
+ providerModelsService.getCurrentActiveModel = originalGetCurrentActiveModel;
49
+ }
50
+ });
51
+
52
+ test('models command falls back to claude for unsupported providers', async () => {
53
+ const originalGetProviderModels = providerModelsService.getProviderModels;
54
+ const originalGetCurrentActiveModel = providerModelsService.getCurrentActiveModel;
55
+
56
+ providerModelsService.getProviderModels = async () => ({
57
+ models: {
58
+ OPTIONS: [{ value: 'default', label: 'Default (recommended)' }],
59
+ DEFAULT: 'default',
60
+ },
61
+ cache: {
62
+ updatedAt: '2026-01-01T00:00:00.000Z',
63
+ expiresAt: '2026-01-04T00:00:00.000Z',
64
+ source: 'fresh',
65
+ },
66
+ });
67
+ providerModelsService.getCurrentActiveModel = async () => ({
68
+ model: 'default',
69
+ });
70
+
71
+ try {
72
+ const result = await executeModelsCommand([], {
73
+ provider: 'unknown-provider',
74
+ });
75
+
76
+ assert.equal(result.data.current.provider, 'claude');
77
+ assert.deepEqual(Object.keys(result.data.available), ['claude']);
78
+ } finally {
79
+ providerModelsService.getProviderModels = originalGetProviderModels;
80
+ providerModelsService.getCurrentActiveModel = originalGetCurrentActiveModel;
81
+ }
82
+ });
@@ -0,0 +1,61 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ resolveClaudeCodeExecutablePath,
6
+ type ResolveClaudeCodeExecutablePathDependencies,
7
+ } from '@/shared/claude-cli-path.js';
8
+
9
+ test('resolveClaudeCodeExecutablePath resolves the npm Claude wrapper to its native exe on Windows', () => {
10
+ const wrapperDir = 'C:\\nvm4w\\nodejs';
11
+ const nativePath = `${wrapperDir}\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe`;
12
+ const execFileSync =
13
+ (() => `${wrapperDir}\\claude\r\n${wrapperDir}\\claude.cmd\r\n`) as unknown as ResolveClaudeCodeExecutablePathDependencies['execFileSync'];
14
+ const readFileSync = (() => '') as unknown as ResolveClaudeCodeExecutablePathDependencies['readFileSync'];
15
+
16
+ const resolved = resolveClaudeCodeExecutablePath('claude', {
17
+ platform: 'win32',
18
+ execFileSync,
19
+ existsSync: (candidate) => candidate === nativePath,
20
+ readFileSync,
21
+ });
22
+
23
+ assert.equal(resolved, nativePath);
24
+ });
25
+
26
+ test('resolveClaudeCodeExecutablePath keeps an explicit JavaScript launcher path unchanged', () => {
27
+ const scriptPath = 'C:\\tools\\claude.js';
28
+
29
+ const resolved = resolveClaudeCodeExecutablePath(scriptPath, {
30
+ platform: 'win32',
31
+ });
32
+
33
+ assert.equal(resolved, scriptPath);
34
+ });
35
+
36
+ test('resolveClaudeCodeExecutablePath can parse a wrapper file path containing letters r and n before claude.exe', () => {
37
+ const wrapperPath = 'C:\\tools\\claude';
38
+ const nativePath = 'C:\\tools\\custom\\bin\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe';
39
+ const readFileSync = (() => `exec "$basedir/custom/bin/node_modules/@anthropic-ai/claude-code/bin/claude.exe" "$@"`) as unknown as ResolveClaudeCodeExecutablePathDependencies['readFileSync'];
40
+
41
+ const resolved = resolveClaudeCodeExecutablePath(wrapperPath, {
42
+ platform: 'win32',
43
+ existsSync: (candidate) => candidate === nativePath,
44
+ readFileSync,
45
+ });
46
+
47
+ assert.equal(resolved, nativePath);
48
+ });
49
+
50
+ test('resolveClaudeCodeExecutablePath falls back to the configured command when PATH lookup fails', () => {
51
+ const execFileSync = (() => {
52
+ throw new Error('not found');
53
+ }) as unknown as ResolveClaudeCodeExecutablePathDependencies['execFileSync'];
54
+
55
+ const resolved = resolveClaudeCodeExecutablePath('claude', {
56
+ platform: 'win32',
57
+ execFileSync,
58
+ });
59
+
60
+ assert.equal(resolved, 'claude');
61
+ });
@@ -0,0 +1,139 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const DEFAULT_CLAUDE_COMMAND = 'claude';
6
+ const CLAUDE_SCRIPT_EXTENSIONS = new Set(['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx']);
7
+ const CLAUDE_WRAPPER_SEGMENTS = ['node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'] as const;
8
+
9
+ export type ResolveClaudeCodeExecutablePathDependencies = {
10
+ execFileSync?: typeof execFileSync;
11
+ existsSync?: typeof fs.existsSync;
12
+ platform?: NodeJS.Platform;
13
+ readFileSync?: typeof fs.readFileSync;
14
+ };
15
+
16
+ function getPathApi(platform: NodeJS.Platform) {
17
+ return platform === 'win32' ? path.win32 : path;
18
+ }
19
+
20
+ function stripWrappingQuotes(value: string): string {
21
+ const trimmed = value.trim();
22
+ if (
23
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
24
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
25
+ ) {
26
+ return trimmed.slice(1, -1);
27
+ }
28
+ return trimmed;
29
+ }
30
+
31
+ function isPathLike(value: string): boolean {
32
+ return value.includes('/') || value.includes('\\');
33
+ }
34
+
35
+ function resolveClaudeWrapperBinary(
36
+ wrapperPath: string,
37
+ deps: Required<ResolveClaudeCodeExecutablePathDependencies>,
38
+ ): string | null {
39
+ const pathApi = getPathApi(deps.platform);
40
+ const directCandidate = pathApi.resolve(pathApi.dirname(wrapperPath), ...CLAUDE_WRAPPER_SEGMENTS);
41
+
42
+ if (deps.existsSync(directCandidate)) {
43
+ return directCandidate;
44
+ }
45
+
46
+ let content: string;
47
+ try {
48
+ content = deps.readFileSync(wrapperPath, 'utf8');
49
+ } catch {
50
+ return null;
51
+ }
52
+
53
+ const matches = content.matchAll(/["']([^"'\\\r\n]*claude\.exe)["']/gi);
54
+ for (const match of matches) {
55
+ const rawTarget = match[1]
56
+ .replace(/^\$basedir[\\/]/i, '')
57
+ .replace(/^%dp0%[\\/]/i, '')
58
+ .replace(/^%~dp0[\\/]/i, '');
59
+ const normalizedTarget = rawTarget.replace(/[\\/]/g, pathApi.sep);
60
+ const candidate = pathApi.isAbsolute(normalizedTarget)
61
+ ? normalizedTarget
62
+ : pathApi.resolve(pathApi.dirname(wrapperPath), normalizedTarget);
63
+
64
+ if (deps.existsSync(candidate)) {
65
+ return candidate;
66
+ }
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ function resolveWindowsClaudeExecutablePath(
73
+ configuredPath: string,
74
+ deps: Required<ResolveClaudeCodeExecutablePathDependencies>,
75
+ ): string {
76
+ const pathApi = getPathApi(deps.platform);
77
+ const extension = pathApi.extname(configuredPath).toLowerCase();
78
+ const explicitPath = isPathLike(configuredPath) || pathApi.isAbsolute(configuredPath);
79
+
80
+ if (CLAUDE_SCRIPT_EXTENSIONS.has(extension)) {
81
+ return configuredPath;
82
+ }
83
+
84
+ if (explicitPath && extension === '.exe') {
85
+ return configuredPath;
86
+ }
87
+
88
+ if (explicitPath) {
89
+ return resolveClaudeWrapperBinary(configuredPath, deps) ?? configuredPath;
90
+ }
91
+
92
+ try {
93
+ const stdout = deps.execFileSync('where.exe', [configuredPath], {
94
+ encoding: 'utf8',
95
+ stdio: ['ignore', 'pipe', 'ignore'],
96
+ windowsHide: true,
97
+ });
98
+ const candidates = stdout
99
+ .split(/\r?\n/)
100
+ .map((entry) => entry.trim())
101
+ .filter(Boolean);
102
+
103
+ for (const candidate of candidates) {
104
+ if (pathApi.extname(candidate).toLowerCase() === '.exe') {
105
+ return candidate;
106
+ }
107
+ }
108
+
109
+ for (const candidate of candidates) {
110
+ const resolved = resolveClaudeWrapperBinary(candidate, deps);
111
+ if (resolved) {
112
+ return resolved;
113
+ }
114
+ }
115
+ } catch {
116
+ return configuredPath;
117
+ }
118
+
119
+ return configuredPath;
120
+ }
121
+
122
+ export function resolveClaudeCodeExecutablePath(
123
+ configuredPath: string | undefined = process.env.CLAUDE_CLI_PATH,
124
+ dependencies: ResolveClaudeCodeExecutablePathDependencies = {},
125
+ ): string {
126
+ const deps: Required<ResolveClaudeCodeExecutablePathDependencies> = {
127
+ execFileSync: dependencies.execFileSync ?? execFileSync,
128
+ existsSync: dependencies.existsSync ?? fs.existsSync,
129
+ platform: dependencies.platform ?? process.platform,
130
+ readFileSync: dependencies.readFileSync ?? fs.readFileSync,
131
+ };
132
+
133
+ const normalizedPath = stripWrappingQuotes(configuredPath || DEFAULT_CLAUDE_COMMAND);
134
+ if (deps.platform !== 'win32') {
135
+ return normalizedPath;
136
+ }
137
+
138
+ return resolveWindowsClaudeExecutablePath(normalizedPath, deps);
139
+ }
@@ -9,10 +9,10 @@ const frontmatterOptions = {
9
9
  engines: {
10
10
  js: disabledFrontmatterEngine,
11
11
  javascript: disabledFrontmatterEngine,
12
- json: disabledFrontmatterEngine
13
- }
12
+ json: disabledFrontmatterEngine,
13
+ },
14
14
  };
15
15
 
16
- export function parseFrontmatter(content) {
16
+ export function parseFrontMatter(content: string) {
17
17
  return matter(content, frontmatterOptions);
18
18
  }
@@ -4,8 +4,14 @@ import type {
4
4
  LLMProvider,
5
5
  McpScope,
6
6
  NormalizedMessage,
7
+ ProviderSkill,
8
+ ProviderSkillListOptions,
7
9
  ProviderAuthStatus,
10
+ ProviderChangeActiveModelInput,
11
+ ProviderCurrentActiveModel,
12
+ ProviderModelsDefinition,
8
13
  ProviderMcpServer,
14
+ ProviderSessionActiveModelChange,
9
15
  UpsertProviderMcpServerInput,
10
16
  } from '@/shared/types.js';
11
17
 
@@ -18,12 +24,54 @@ import type {
18
24
  */
19
25
  export interface IProvider {
20
26
  readonly id: LLMProvider;
27
+ readonly models: IProviderModels;
21
28
  readonly mcp: IProviderMcp;
22
29
  readonly auth: IProviderAuth;
30
+ readonly skills: IProviderSkills;
23
31
  readonly sessions: IProviderSessions;
24
32
  readonly sessionSynchronizer: IProviderSessionSynchronizer;
25
33
  }
26
34
 
35
+ // ---------------------------
36
+ //----------------- PROVIDER MODEL INTERFACE ------------
37
+ /**
38
+ * Model catalog contract for one provider.
39
+ *
40
+ * Implementations are responsible for resolving the provider's currently
41
+ * supported models and converting them into the shared
42
+ * `ProviderModelsDefinition` shape used by backend routes and frontend model
43
+ * pickers. The `DEFAULT` field should be the most appropriate default selection
44
+ * for that provider at the time the catalog is read.
45
+ */
46
+ export interface IProviderModels {
47
+ /**
48
+ * Returns the provider's currently supported model catalog.
49
+ */
50
+ getSupportedModels(): Promise<ProviderModelsDefinition>;
51
+
52
+ /**
53
+ * Returns the currently active model for one session or provider runtime.
54
+ *
55
+ * Implementations must use the provider-specific lookup mechanism approved
56
+ * for that provider and fall back only to the provider catalog default when
57
+ * no active model can be resolved.
58
+ */
59
+ getCurrentActiveModel(sessionId?: string): Promise<ProviderCurrentActiveModel>;
60
+
61
+ /**
62
+ * Persists a session-scoped model override that the next resumed turn should
63
+ * honor for this provider.
64
+ *
65
+ * This does not require the provider to mutate an already running remote
66
+ * session in-place. Instead, adapters store the user's explicit model choice
67
+ * so the backend resume path can add the correct provider-native model option
68
+ * on the next CLI/SDK invocation for the same session.
69
+ */
70
+ changeActiveModel(
71
+ input: ProviderChangeActiveModelInput,
72
+ ): Promise<ProviderSessionActiveModelChange>;
73
+ }
74
+
27
75
  // ---------------------------
28
76
  //----------------- PROVIDER AUTH INTERFACE ------------
29
77
  /**
@@ -39,6 +87,22 @@ export interface IProviderAuth {
39
87
  getStatus(): Promise<ProviderAuthStatus>;
40
88
  }
41
89
 
90
+ // ---------------------------
91
+ //----------------- PROVIDER SKILLS INTERFACE ------------
92
+ /**
93
+ * Skills contract for one provider.
94
+ *
95
+ * Implementations discover provider-native skill markdown locations and return
96
+ * normalized skill records with the exact command syntax expected by that
97
+ * provider. Each skill is read from a `SKILL.md` file under its skill directory.
98
+ */
99
+ export interface IProviderSkills {
100
+ /**
101
+ * Lists all skills visible to this provider for the optional workspace.
102
+ */
103
+ listSkills(options?: ProviderSkillListOptions): Promise<ProviderSkill[]>;
104
+ }
105
+
42
106
  // ---------------------------
43
107
  //----------------- PROVIDER MCP INTERFACE ------------
44
108
  /**
@@ -65,7 +65,94 @@ export type AuthenticatedWebSocketRequest = IncomingMessage & {
65
65
  * Use this as the source of truth whenever a function or payload needs to identify
66
66
  * a specific LLM integration.
67
67
  */
68
- export type LLMProvider = 'claude' | 'codex' | 'gemini' | 'cursor';
68
+ export type LLMProvider = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode';
69
+
70
+ /**
71
+ * One selectable model row (matches the documentation `public/modelConstants.js` option shape).
72
+ */
73
+ export type ProviderModelOption = {
74
+ value: string;
75
+ label: string;
76
+ description?: string;
77
+ };
78
+
79
+ /**
80
+ * Provider model catalog returned by `GET /api/providers/:provider/models`.
81
+ */
82
+ export type ProviderModelsDefinition = {
83
+ OPTIONS: ProviderModelOption[];
84
+ DEFAULT: string;
85
+ };
86
+
87
+ /**
88
+ * Cache metadata returned alongside one provider model catalog.
89
+ *
90
+ * `updatedAt` is when the current cached snapshot was last refreshed from the
91
+ * provider itself. `expiresAt` is the backend cache expiry timestamp, and
92
+ * `source` tells callers whether the current response came from in-memory cache,
93
+ * persisted disk cache, or a fresh provider fetch.
94
+ */
95
+ export type ProviderModelsCacheInfo = {
96
+ updatedAt: string;
97
+ expiresAt: string;
98
+ source: 'memory' | 'disk' | 'fresh';
99
+ };
100
+
101
+ /**
102
+ * Full provider model lookup result returned by the backend service layer.
103
+ *
104
+ * Use this shape when a caller needs both the selectable model catalog and the
105
+ * cache metadata that explains how current the catalog is.
106
+ */
107
+ export type ProviderModelsResult = {
108
+ models: ProviderModelsDefinition;
109
+ cache: ProviderModelsCacheInfo;
110
+ };
111
+
112
+ // ---------------------------
113
+ //----------------- PROVIDER ACTIVE MODEL TYPES ------------
114
+ /**
115
+ * Provider-neutral result for the model that is actively driving a session or
116
+ * provider runtime at the time of lookup.
117
+ *
118
+ * `model` must always be populated. Provider adapters should use the
119
+ * provider-specific lookup method requested by the caller, and only fall back
120
+ * to the provider catalog `DEFAULT` value when the active model cannot be read.
121
+ */
122
+ export type ProviderCurrentActiveModel = {
123
+ model: string;
124
+ };
125
+
126
+ /**
127
+ * Input payload used when one session needs to use a different model on its
128
+ * next resumed turn.
129
+ *
130
+ * This is a backend-owned session override, not a claim that the provider has
131
+ * already switched the currently running session in-place. Provider adapters
132
+ * persist this request so the next CLI/SDK resume can inject the chosen model
133
+ * using the provider-specific mechanism supported by that runtime.
134
+ */
135
+ export type ProviderChangeActiveModelInput = {
136
+ sessionId: string;
137
+ model: string;
138
+ };
139
+
140
+ /**
141
+ * Provider-neutral session model-change state.
142
+ *
143
+ * `supported` indicates whether the provider adapter supports the app's
144
+ * session-scoped resume override flow. `changed` is the persisted boolean the
145
+ * resume layer checks before forcing a model on the next resumed turn. When
146
+ * `changed` is `false`, `model` is `null` and the runtime should use the
147
+ * normal request/default model selection path.
148
+ */
149
+ export type ProviderSessionActiveModelChange = {
150
+ provider: LLMProvider;
151
+ sessionId: string;
152
+ supported: boolean;
153
+ changed: boolean;
154
+ model: string | null;
155
+ };
69
156
 
70
157
  /**
71
158
  * Message/event variants emitted by provider adapters and normalized transports.
@@ -102,6 +189,21 @@ export type NormalizedMessage = {
102
189
  kind: MessageKind;
103
190
  role?: 'user' | 'assistant';
104
191
  content?: string;
192
+ /**
193
+ * Optional display-oriented metadata used by providers that need to expose
194
+ * richer transcript artifacts without introducing a brand-new message kind.
195
+ *
196
+ * Current Claude usage:
197
+ * - local slash commands expose parsed command fields
198
+ * - compact summaries are flagged so the UI can treat them differently later
199
+ */
200
+ displayText?: string;
201
+ commandName?: string;
202
+ commandMessage?: string;
203
+ commandArgs?: string;
204
+ isLocalCommand?: boolean;
205
+ isLocalCommandStdout?: boolean;
206
+ isCompactSummary?: boolean;
105
207
  images?: unknown;
106
208
  toolName?: string;
107
209
  toolInput?: unknown;
@@ -156,6 +258,69 @@ export type FetchHistoryResult = {
156
258
  tokenUsage?: unknown;
157
259
  };
158
260
 
261
+ // ---------------------------
262
+ //----------------- PROVIDER SKILL TYPES ------------
263
+ /**
264
+ * Scope where a provider skill definition was discovered.
265
+ *
266
+ * Provider skill adapters should use this to describe the origin of each
267
+ * skill markdown file without leaking provider-specific folder names into route
268
+ * contracts. `repo` is used for Codex repository lookup locations, while
269
+ * `project` is used for providers that treat workspace-local skills as project
270
+ * scoped.
271
+ */
272
+ export type ProviderSkillScope = 'user' | 'project' | 'plugin' | 'repo' | 'admin' | 'system';
273
+
274
+ /**
275
+ * Shared input accepted by provider skill listing operations.
276
+ *
277
+ * Routes pass `workspacePath` when a caller wants project/repository skills for
278
+ * a specific folder. Providers should fall back to the backend process cwd when
279
+ * this option is omitted.
280
+ */
281
+ export type ProviderSkillListOptions = {
282
+ workspacePath?: string;
283
+ };
284
+
285
+ /**
286
+ * Normalized skill record returned by provider skill adapters.
287
+ *
288
+ * The `command` value is the exact invocation text the selected provider expects
289
+ * for this skill. Claude plugin skills use a namespaced command such as
290
+ * `/plugin-name:skill-name`, while Codex skills use the `$skill-name` form.
291
+ * `sourcePath` points to the skill markdown file that produced the record so
292
+ * callers can distinguish duplicate skill names across scopes.
293
+ */
294
+ export type ProviderSkill = {
295
+ provider: LLMProvider;
296
+ name: string;
297
+ description: string;
298
+ command: string;
299
+ scope: ProviderSkillScope;
300
+ sourcePath: string;
301
+ pluginName?: string;
302
+ pluginId?: string;
303
+ };
304
+
305
+ /**
306
+ * Internal source descriptor consumed by shared provider skill discovery logic.
307
+ *
308
+ * Concrete provider adapters build these records from their native lookup rules.
309
+ * The shared skills provider then scans `rootDir` for child skill markdown files
310
+ * and uses `commandForSkill` or `commandPrefix` to produce the provider-specific
311
+ * invocation command. Set `recursive` only when a provider stores skills under
312
+ * arbitrary nested folders below the source root.
313
+ */
314
+ export type ProviderSkillSource = {
315
+ scope: ProviderSkillScope;
316
+ rootDir: string;
317
+ recursive?: boolean;
318
+ commandPrefix?: '/' | '$';
319
+ commandForSkill?: (skillName: string) => string;
320
+ pluginName?: string;
321
+ pluginId?: string;
322
+ };
323
+
159
324
  // ---------------------------
160
325
  //----------------- SHARED ERROR TYPES ------------
161
326
  /**