@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
@@ -17,11 +17,18 @@ import readline from 'node:readline';
17
17
 
18
18
  import type { NextFunction, Request, RequestHandler, Response } from 'express';
19
19
 
20
+ import { parseFrontMatter } from '@/shared/frontmatter.js';
20
21
  import type {
21
22
  AnyRecord,
22
23
  ApiSuccessShape,
23
24
  AppErrorOptions,
25
+ LLMProvider,
24
26
  NormalizedMessage,
27
+ ProviderChangeActiveModelInput,
28
+ ProviderCurrentActiveModel,
29
+ ProviderModelsDefinition,
30
+ ProviderSessionActiveModelChange,
31
+ ProviderSkillSource,
25
32
  WorkspacePathValidationResult,
26
33
  } from '@/shared/types.js';
27
34
 
@@ -412,6 +419,231 @@ export const readStringRecord = (value: unknown): Record<string, string> | undef
412
419
  return Object.keys(normalized).length > 0 ? normalized : undefined;
413
420
  };
414
421
 
422
+ // ---------------------------
423
+ //----------------- PROVIDER MODEL LOOKUP UTILITIES ------------
424
+ /**
425
+ * Builds the standard "default current model" result used when a provider
426
+ * cannot resolve a session-backed active model.
427
+ *
428
+ * Provider model adapters should call this after loading their supported model
429
+ * catalog so the fallback stays aligned with the provider's current `DEFAULT`
430
+ * selection instead of drifting to a hard-coded duplicate.
431
+ */
432
+ export function buildDefaultProviderCurrentActiveModel(
433
+ models: ProviderModelsDefinition,
434
+ ): ProviderCurrentActiveModel {
435
+ return {
436
+ model: models.DEFAULT,
437
+ };
438
+ }
439
+
440
+ // ---------------------------
441
+ //----------------- PROVIDER SESSION MODEL CHANGE UTILITIES ------------
442
+ type ProviderSessionActiveModelChangeCacheEntry = ProviderSessionActiveModelChange & {
443
+ updatedAt: string;
444
+ };
445
+
446
+ type ProviderSessionActiveModelChangeCacheFile = {
447
+ version: number;
448
+ entries: Record<string, ProviderSessionActiveModelChangeCacheEntry>;
449
+ };
450
+
451
+ const PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION = 1;
452
+
453
+ /**
454
+ * Resolves the backend-owned cache file used for session-scoped resume model
455
+ * overrides.
456
+ *
457
+ * The file lives under `~/.cloudcli` because these overrides are an application
458
+ * concern rather than a provider-native config file. Providers, routes, and
459
+ * runtime command launchers should all use this helper instead of re-creating
460
+ * the path so the storage location stays consistent.
461
+ */
462
+ export function getProviderSessionActiveModelChangesPath(): string {
463
+ return path.join(os.homedir(), '.cloudcli', 'provider-session-active-model-changes.json');
464
+ }
465
+
466
+ const buildProviderSessionActiveModelChangeKey = (
467
+ provider: LLMProvider,
468
+ sessionId: string,
469
+ ): string => `${provider}:${sessionId}`;
470
+
471
+ const isProviderSessionActiveModelChangeCacheEntry = (
472
+ value: unknown,
473
+ ): value is ProviderSessionActiveModelChangeCacheEntry => {
474
+ const record = readObjectRecord(value);
475
+ return Boolean(
476
+ record
477
+ && typeof record.provider === 'string'
478
+ && typeof record.sessionId === 'string'
479
+ && typeof record.supported === 'boolean'
480
+ && typeof record.changed === 'boolean'
481
+ && (typeof record.model === 'string' || record.model === null)
482
+ && typeof record.updatedAt === 'string',
483
+ );
484
+ };
485
+
486
+ const readProviderSessionActiveModelChangeCacheFile = async (
487
+ filePath: string,
488
+ ): Promise<ProviderSessionActiveModelChangeCacheFile> => {
489
+ try {
490
+ const raw = await readFile(filePath, 'utf8');
491
+ const parsed = readObjectRecord(JSON.parse(raw));
492
+ if (
493
+ !parsed
494
+ || parsed.version !== PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION
495
+ || !readObjectRecord(parsed.entries)
496
+ ) {
497
+ return {
498
+ version: PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION,
499
+ entries: {},
500
+ };
501
+ }
502
+
503
+ const entries = Object.fromEntries(
504
+ Object.entries(parsed.entries).filter((entry): entry is [string, ProviderSessionActiveModelChangeCacheEntry] =>
505
+ isProviderSessionActiveModelChangeCacheEntry(entry[1]),
506
+ ),
507
+ );
508
+
509
+ return {
510
+ version: PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION,
511
+ entries,
512
+ };
513
+ } catch {
514
+ return {
515
+ version: PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION,
516
+ entries: {},
517
+ };
518
+ }
519
+ };
520
+
521
+ const writeProviderSessionActiveModelChangeCacheFile = async (
522
+ filePath: string,
523
+ payload: ProviderSessionActiveModelChangeCacheFile,
524
+ ): Promise<void> => {
525
+ await mkdir(path.dirname(filePath), { recursive: true });
526
+ await writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
527
+ };
528
+
529
+ const buildUnsupportedProviderSessionActiveModelChange = (
530
+ provider: LLMProvider,
531
+ sessionId: string,
532
+ ): ProviderSessionActiveModelChange => ({
533
+ provider,
534
+ sessionId,
535
+ supported: false,
536
+ changed: false,
537
+ model: null,
538
+ });
539
+
540
+ /**
541
+ * Reads the persisted session model-change state for one provider session.
542
+ *
543
+ * Runtime resume paths use this to decide whether they should inject a
544
+ * provider-specific model argument/thread option for the next resumed turn.
545
+ * Missing cache entries are normalized to `{ changed: false }` so callers can
546
+ * treat absence as "use the ordinary model selection flow".
547
+ */
548
+ export async function readProviderSessionActiveModelChange(
549
+ provider: LLMProvider,
550
+ sessionId: string,
551
+ options: {
552
+ filePath?: string;
553
+ supported?: boolean;
554
+ } = {},
555
+ ): Promise<ProviderSessionActiveModelChange> {
556
+ const normalizedSessionId = sessionId.trim();
557
+ if (!normalizedSessionId) {
558
+ return buildUnsupportedProviderSessionActiveModelChange(provider, normalizedSessionId);
559
+ }
560
+
561
+ const supported = options.supported ?? true;
562
+ if (!supported) {
563
+ return buildUnsupportedProviderSessionActiveModelChange(provider, normalizedSessionId);
564
+ }
565
+
566
+ const filePath = options.filePath ?? getProviderSessionActiveModelChangesPath();
567
+ const cacheFile = await readProviderSessionActiveModelChangeCacheFile(filePath);
568
+ const cacheEntry = cacheFile.entries[
569
+ buildProviderSessionActiveModelChangeKey(provider, normalizedSessionId)
570
+ ];
571
+
572
+ if (!cacheEntry || !cacheEntry.changed || !cacheEntry.model?.trim()) {
573
+ return {
574
+ provider,
575
+ sessionId: normalizedSessionId,
576
+ supported: true,
577
+ changed: false,
578
+ model: null,
579
+ };
580
+ }
581
+
582
+ return {
583
+ provider,
584
+ sessionId: normalizedSessionId,
585
+ supported: true,
586
+ changed: true,
587
+ model: cacheEntry.model.trim(),
588
+ };
589
+ }
590
+
591
+ /**
592
+ * Persists a session model-change request for one provider.
593
+ *
594
+ * Provider adapters call this when the frontend explicitly selects a different
595
+ * model for an existing session. The stored `changed: true` flag is the single
596
+ * source of truth used later by resume paths to decide whether they should add
597
+ * a provider-native model override on the next invocation.
598
+ */
599
+ export async function writeProviderSessionActiveModelChange(
600
+ provider: LLMProvider,
601
+ input: ProviderChangeActiveModelInput,
602
+ options: {
603
+ filePath?: string;
604
+ supported?: boolean;
605
+ } = {},
606
+ ): Promise<ProviderSessionActiveModelChange> {
607
+ const normalizedSessionId = input.sessionId.trim();
608
+ const normalizedModel = input.model.trim();
609
+ const supported = options.supported ?? true;
610
+
611
+ if (!supported) {
612
+ return buildUnsupportedProviderSessionActiveModelChange(provider, normalizedSessionId);
613
+ }
614
+
615
+ if (!normalizedSessionId || !normalizedModel) {
616
+ return {
617
+ provider,
618
+ sessionId: normalizedSessionId,
619
+ supported: true,
620
+ changed: false,
621
+ model: null,
622
+ };
623
+ }
624
+
625
+ const filePath = options.filePath ?? getProviderSessionActiveModelChangesPath();
626
+ const cacheFile = await readProviderSessionActiveModelChangeCacheFile(filePath);
627
+ cacheFile.entries[buildProviderSessionActiveModelChangeKey(provider, normalizedSessionId)] = {
628
+ provider,
629
+ sessionId: normalizedSessionId,
630
+ supported: true,
631
+ changed: true,
632
+ model: normalizedModel,
633
+ updatedAt: new Date().toISOString(),
634
+ };
635
+
636
+ await writeProviderSessionActiveModelChangeCacheFile(filePath, cacheFile);
637
+
638
+ return {
639
+ provider,
640
+ sessionId: normalizedSessionId,
641
+ supported: true,
642
+ changed: true,
643
+ model: normalizedModel,
644
+ };
645
+ }
646
+
415
647
  // ---------------------------
416
648
  //----------------- WEBSOCKET PAYLOAD PARSING UTILITIES ------------
417
649
  /**
@@ -503,6 +735,160 @@ export const writeJsonConfig = async (filePath: string, data: Record<string, unk
503
735
  await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
504
736
  };
505
737
 
738
+ // ---------------------------
739
+ //----------------- PROVIDER SKILL FILE UTILITIES ------------
740
+ async function hasGitMarker(dirPath: string): Promise<boolean> {
741
+ try {
742
+ const gitMarkerStats = await stat(path.join(dirPath, '.git'));
743
+ return gitMarkerStats.isDirectory() || gitMarkerStats.isFile();
744
+ } catch {
745
+ return false;
746
+ }
747
+ }
748
+
749
+ /**
750
+ * Finds the highest git worktree root visible from a starting directory.
751
+ *
752
+ * Provider skill systems such as Codex and OpenCode walk upward through parent
753
+ * folders when resolving repository/project skills. Use this helper when a
754
+ * provider needs the topmost `.git` marker instead of only the nearest one, so
755
+ * monorepos and nested package folders discover shared root-level skills once.
756
+ */
757
+ export async function findTopmostGitRoot(startPath: string): Promise<string | null> {
758
+ let currentPath = path.resolve(startPath);
759
+ let topmostGitRoot: string | null = null;
760
+
761
+ while (true) {
762
+ if (await hasGitMarker(currentPath)) {
763
+ topmostGitRoot = currentPath;
764
+ }
765
+
766
+ const parentPath = path.dirname(currentPath);
767
+ if (parentPath === currentPath) {
768
+ break;
769
+ }
770
+
771
+ currentPath = parentPath;
772
+ }
773
+
774
+ return topmostGitRoot;
775
+ }
776
+
777
+ /**
778
+ * Adds one provider skill source after normalizing and de-duplicating its root.
779
+ *
780
+ * Provider skill lookup rules often point at overlapping folders (for example a
781
+ * workspace folder can also be the git root). Use this helper while building a
782
+ * provider's `ProviderSkillSource[]` so the shared skills scanner reads each
783
+ * physical root once and still preserves provider-specific scope/command data.
784
+ */
785
+ export function addUniqueProviderSkillSource(
786
+ sources: ProviderSkillSource[],
787
+ seenRootDirs: Set<string>,
788
+ source: ProviderSkillSource,
789
+ ): void {
790
+ const normalizedRootDir = path.resolve(source.rootDir);
791
+ if (seenRootDirs.has(normalizedRootDir)) {
792
+ return;
793
+ }
794
+
795
+ seenRootDirs.add(normalizedRootDir);
796
+ sources.push({ ...source, rootDir: normalizedRootDir });
797
+ }
798
+
799
+ // ---------------------------
800
+ //----------------- PROVIDER SKILL MARKDOWN UTILITIES ------------
801
+ /**
802
+ * Finds direct child skill markdown files under a provider skill root.
803
+ *
804
+ * Skill systems usually store one skill per child directory, so direct mode
805
+ * scans only `<root>/<skill-name>/SKILL.md`. Recursive mode is reserved for
806
+ * provider sources that can nest skills arbitrarily, and it returns every
807
+ * descendant `SKILL.md`. Missing or unreadable roots return an empty list
808
+ * because users may not have every provider installed or configured.
809
+ */
810
+ export async function findProviderSkillMarkdownFiles(
811
+ rootDir: string,
812
+ options: { recursive?: boolean } = {},
813
+ ): Promise<string[]> {
814
+ const skillFiles: string[] = [];
815
+
816
+ const collectRecursive = async (dirPath: string): Promise<void> => {
817
+ let entries;
818
+ try {
819
+ entries = await readdir(dirPath, { withFileTypes: true });
820
+ } catch {
821
+ return;
822
+ }
823
+
824
+ try {
825
+ const skillPath = path.join(dirPath, 'SKILL.md');
826
+ const skillStats = await stat(skillPath);
827
+ if (skillStats.isFile()) {
828
+ skillFiles.push(skillPath);
829
+ }
830
+ } catch {
831
+ // Directories without SKILL.md are expected while walking plugin trees.
832
+ }
833
+
834
+ for (const entry of entries) {
835
+ if (entry.isDirectory()) {
836
+ await collectRecursive(path.join(dirPath, entry.name));
837
+ }
838
+ }
839
+ };
840
+
841
+ if (options.recursive) {
842
+ await collectRecursive(rootDir);
843
+ return skillFiles.sort((left, right) => left.localeCompare(right));
844
+ }
845
+
846
+ try {
847
+ const entries = await readdir(rootDir, { withFileTypes: true });
848
+
849
+ for (const entry of entries) {
850
+ if (!entry.isDirectory()) {
851
+ continue;
852
+ }
853
+
854
+ const skillPath = path.join(rootDir, entry.name, 'SKILL.md');
855
+ try {
856
+ const skillStats = await stat(skillPath);
857
+ if (skillStats.isFile()) {
858
+ skillFiles.push(skillPath);
859
+ }
860
+ } catch {
861
+ // A partial skill directory should not block discovery of sibling skills.
862
+ }
863
+ }
864
+
865
+ return skillFiles.sort((left, right) => left.localeCompare(right));
866
+ } catch {
867
+ return [];
868
+ }
869
+ }
870
+
871
+ /**
872
+ * Reads the `name` and `description` fields from a provider skill markdown file.
873
+ *
874
+ * The metadata is expected in markdown front matter. If a skill omits `name`, the
875
+ * parent directory name is used as a stable fallback so providers can still
876
+ * expose the skill. Missing descriptions are normalized to an empty string.
877
+ */
878
+ export async function readProviderSkillMarkdownDefinition(
879
+ skillPath: string,
880
+ ): Promise<{ name: string; description: string }> {
881
+ const content = await readFile(skillPath, 'utf8');
882
+ const parsed = parseFrontMatter(content);
883
+ const data = readObjectRecord(parsed.data) ?? {};
884
+ const fallbackName = path.basename(path.dirname(skillPath));
885
+
886
+ return {
887
+ name: readOptionalString(data.name) ?? fallbackName,
888
+ description: readOptionalString(data.description) ?? '',
889
+ };
890
+ }
891
+
506
892
  // ---------------------------
507
893
  //----------------- SESSION SYNCHRONIZER TITLE HELPERS ------------
508
894
  /**
@@ -522,6 +908,98 @@ export function normalizeSessionName(rawValue: string | undefined, fallback: str
522
908
  return normalized.slice(0, 120);
523
909
  }
524
910
 
911
+ // ---------------------------
912
+ //----------------- PROVIDER SESSION VALUE NORMALIZATION UTILITIES ------------
913
+ /**
914
+ * Converts provider-native timestamps into ISO strings.
915
+ *
916
+ * Provider CLIs commonly persist epoch timestamps as milliseconds, seconds, or
917
+ * already-formatted date strings. Use this helper when normalizing session
918
+ * metadata or transcript events so every provider writes the same ISO timestamp
919
+ * shape to API responses and database rows.
920
+ */
921
+ export function normalizeProviderTimestamp(value: unknown): string {
922
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
923
+ const millis = value < 1_000_000_000_000 ? value * 1000 : value;
924
+ return new Date(millis).toISOString();
925
+ }
926
+
927
+ if (typeof value === 'string' && value.trim()) {
928
+ const parsed = Number(value);
929
+ if (Number.isFinite(parsed)) {
930
+ return normalizeProviderTimestamp(parsed);
931
+ }
932
+
933
+ const date = new Date(value);
934
+ if (!Number.isNaN(date.getTime())) {
935
+ return date.toISOString();
936
+ }
937
+ }
938
+
939
+ return new Date().toISOString();
940
+ }
941
+
942
+ /**
943
+ * Parses a JSON string or narrows an existing object into a plain record.
944
+ *
945
+ * Use this when provider databases store structured JSON inside text columns.
946
+ * Invalid JSON, arrays, and primitive values return `null` so callers can skip
947
+ * malformed optional metadata without hiding the rest of a session transcript.
948
+ */
949
+ export function readJsonRecord(value: unknown): AnyRecord | null {
950
+ if (typeof value !== 'string') {
951
+ return readObjectRecord(value);
952
+ }
953
+
954
+ try {
955
+ return readObjectRecord(JSON.parse(value));
956
+ } catch {
957
+ return null;
958
+ }
959
+ }
960
+
961
+ // ---------------------------
962
+ //----------------- OPENCODE SESSION STORAGE UTILITIES ------------
963
+ /**
964
+ * Resolves the OpenCode SQLite session database path.
965
+ *
966
+ * OpenCode stores session, message, part, and project metadata in one shared
967
+ * `opencode.db` file under its XDG data directory. Provider readers and
968
+ * synchronizers should use this path for read-only access and should never store
969
+ * it as a deletable transcript path for an individual app session row.
970
+ */
971
+ export function getOpenCodeDatabasePath(): string {
972
+ return path.join(os.homedir(), '.local', 'share', 'opencode', 'opencode.db');
973
+ }
974
+
975
+ // ---------------------------
976
+ //----------------- SAFE DIRECTORY NAME UTILITIES ------------
977
+ /**
978
+ * Validates that a user or provider supplied identifier can safely be treated
979
+ * as one leaf directory name under an existing root folder.
980
+ *
981
+ * Use this before composing paths like `<root>/<session-id>/file.db>` to block
982
+ * path traversal and accidental nested paths. The returned string is trimmed but
983
+ * otherwise unchanged so callers can still match the provider's on-disk naming.
984
+ */
985
+ export function sanitizeLeafDirectoryName(inputName: string, label = 'directory name'): string {
986
+ const normalized = inputName.trim();
987
+ if (!normalized) {
988
+ throw new Error(`${label} is required.`);
989
+ }
990
+
991
+ if (
992
+ normalized.includes('..')
993
+ || normalized.includes(path.posix.sep)
994
+ || normalized.includes(path.win32.sep)
995
+ || normalized !== path.basename(normalized)
996
+ ) {
997
+ throw new Error(`Invalid ${label} "${inputName}".`);
998
+ }
999
+
1000
+ return normalized;
1001
+ }
1002
+
525
1003
  // ---------------------------
526
1004
  //----------------- SESSION SYNCHRONIZER FILESYSTEM HELPERS ------------
527
1005
  /**
@@ -1,9 +1,11 @@
1
+ import { execFile } from 'child_process';
1
2
  import { promises as fs } from 'fs';
2
3
  import path from 'path';
3
- import { execFile } from 'child_process';
4
4
  import { promisify } from 'util';
5
+
5
6
  import { parse as parseShellCommand } from 'shell-quote';
6
- import { parseFrontmatter } from './frontmatter.js';
7
+
8
+ import { parseFrontMatter } from '../shared/frontmatter.js';
7
9
 
8
10
  const execFileAsync = promisify(execFile);
9
11
 
@@ -32,7 +34,7 @@ const BASH_COMMAND_ALLOWLIST = [
32
34
  */
33
35
  export function parseCommand(content) {
34
36
  try {
35
- const parsed = parseFrontmatter(content);
37
+ const parsed = parseFrontMatter(content);
36
38
  return {
37
39
  data: parsed.data || {},
38
40
  content: parsed.content || '',