@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
@@ -0,0 +1,191 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ import { sessionsDb } from '@/modules/database/index.js';
4
+ import type { IProviderModels } from '@/shared/interfaces.js';
5
+ import type {
6
+ ProviderChangeActiveModelInput,
7
+ ProviderCurrentActiveModel,
8
+ ProviderModelsDefinition,
9
+ ProviderSessionActiveModelChange,
10
+ } from '@/shared/types.js';
11
+ import {
12
+ buildDefaultProviderCurrentActiveModel,
13
+ writeProviderSessionActiveModelChange,
14
+ } from '@/shared/utils.js';
15
+
16
+ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
17
+ OPTIONS: [
18
+ {
19
+ value: 'default',
20
+ label: 'Default (recommended)',
21
+ description: 'Use the default model (currently Opus 4.7 (1M context)) · $5/$25 per Mtok',
22
+ },
23
+ {
24
+ value: 'sonnet',
25
+ label: 'Sonnet',
26
+ description: 'Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok',
27
+ },
28
+ {
29
+ value: 'sonnet[1m]',
30
+ label: 'Sonnet (1M context)',
31
+ description: 'Sonnet 4.6 for long sessions · $3/$15 per Mtok',
32
+ },
33
+ {
34
+ value: 'haiku',
35
+ label: 'Haiku',
36
+ description: 'Haiku 4.5 · Fastest for quick answers · $1/$5 per Mtok',
37
+ },
38
+ ],
39
+ DEFAULT: 'default',
40
+ };
41
+ type ClaudeInitEvent = {
42
+ sessionId?: string;
43
+ session_id?: string;
44
+ type?: string;
45
+ subtype?: string;
46
+ model?: string;
47
+ message?: {
48
+ content?: unknown;
49
+ model?: string;
50
+ };
51
+ };
52
+
53
+ const ANSI_PATTERN = new RegExp(
54
+ '[\\u001B\\u009B][[\\]()#;?]*(?:'
55
+ + '(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]'
56
+ + '|(?:[\\dA-PR-TZcf-ntqry=><~]))',
57
+ 'g',
58
+ );
59
+
60
+ const extractClaudeEventModel = (event: ClaudeInitEvent, sessionId: string): string | null => {
61
+ const eventSessionId = event.sessionId ?? event.session_id;
62
+ if (eventSessionId && eventSessionId !== sessionId) {
63
+ return null;
64
+ }
65
+
66
+ const contentModel = extractClaudeModelFromMessageContent(event.message?.content);
67
+ if (contentModel) {
68
+ return contentModel;
69
+ }
70
+
71
+ const directModel = event.model?.trim();
72
+ if (directModel) {
73
+ return directModel;
74
+ }
75
+
76
+ const messageModel = event.message?.model?.trim();
77
+ return messageModel || null;
78
+ };
79
+
80
+ const stripAnsi = (value: string): string => value.replace(ANSI_PATTERN, '');
81
+
82
+ const extractTaggedContent = (content: string, tagName: string): string | null => {
83
+ const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
84
+ const match = new RegExp(`<${escapedTagName}>([\\s\\S]*?)<\\/${escapedTagName}>`).exec(content);
85
+ return match ? match[1] : null;
86
+ };
87
+
88
+ const extractClaudeModelFromTextContent = (content: string): string | null => {
89
+ const localCommandStdout = extractTaggedContent(content, 'local-command-stdout');
90
+ if (localCommandStdout !== null) {
91
+ const cleanedStdout = stripAnsi(localCommandStdout).replace(/\s+/g, ' ').trim();
92
+ const changedModel = /(?:set|changed|switched)\s+model\s+to\s+(.+?)\.?$/i.exec(cleanedStdout);
93
+ if (changedModel?.[1]?.trim()) {
94
+ return changedModel[1].trim();
95
+ }
96
+ }
97
+
98
+ const modelTag = extractTaggedContent(content, 'model')?.trim();
99
+ return modelTag || null;
100
+ };
101
+
102
+ const extractClaudeModelFromMessageContent = (content: unknown): string | null => {
103
+ if (typeof content === 'string') {
104
+ return extractClaudeModelFromTextContent(content);
105
+ }
106
+
107
+ if (!Array.isArray(content)) {
108
+ return null;
109
+ }
110
+
111
+ for (const part of content) {
112
+ if (!part || typeof part !== 'object' || !('text' in part) || typeof part.text !== 'string') {
113
+ continue;
114
+ }
115
+
116
+ const model = extractClaudeModelFromTextContent(part.text);
117
+ if (model) {
118
+ return model;
119
+ }
120
+ }
121
+
122
+ return null;
123
+ };
124
+
125
+ const readClaudeSessionModelFromJsonl = async (
126
+ sessionId: string,
127
+ jsonlPath: string,
128
+ ): Promise<ProviderCurrentActiveModel | null> => {
129
+ const content = await readFile(jsonlPath, 'utf8');
130
+ const lines = content
131
+ .split(/\r?\n/)
132
+ .map((line) => line.trim())
133
+ .filter(Boolean);
134
+
135
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
136
+ try {
137
+ const event = JSON.parse(lines[index]) as ClaudeInitEvent;
138
+ const model = extractClaudeEventModel(event, sessionId);
139
+ if (model) {
140
+ return { model };
141
+ }
142
+ } catch {
143
+ // Skip malformed JSONL lines that can happen during concurrent writes.
144
+ }
145
+ }
146
+
147
+ return null;
148
+ };
149
+
150
+ export class ClaudeProviderModels implements IProviderModels {
151
+ async getSupportedModels(): Promise<ProviderModelsDefinition> {
152
+ // claude creates a new jsonl file as a separate session for this request.
153
+ // As a result, it lists the workspace where this is invoked when it shouldn't.
154
+ //
155
+ // Disabled for now:
156
+ // const queryInstance = query({
157
+ // prompt: 'Get supported models',
158
+ // options: buildClaudeQueryOptions(),
159
+ // });
160
+ // const supportedModels = await queryInstance.supportedModels();
161
+ // queryInstance.close();
162
+ // return buildClaudeModelsDefinition(supportedModels);
163
+ return CLAUDE_FALLBACK_MODELS;
164
+ }
165
+
166
+ async getCurrentActiveModel(sessionId?: string): Promise<ProviderCurrentActiveModel> {
167
+ if (!sessionId?.trim()) {
168
+ return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
169
+ }
170
+
171
+ try {
172
+ const jsonlPath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
173
+ const activeModel = jsonlPath
174
+ ? await readClaudeSessionModelFromJsonl(sessionId, jsonlPath)
175
+ : null;
176
+ if (activeModel?.model) {
177
+ return activeModel;
178
+ }
179
+ } catch {
180
+ // Fall through to the provider default when the session-backed lookup fails.
181
+ }
182
+
183
+ return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
184
+ }
185
+
186
+ async changeActiveModel(
187
+ input: ProviderChangeActiveModelInput,
188
+ ): Promise<ProviderSessionActiveModelChange> {
189
+ return writeProviderSessionActiveModelChange('claude', input);
190
+ }
191
+ }
@@ -1,5 +1,6 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
+ import { readFile } from 'node:fs/promises';
3
4
 
4
5
  import { sessionsDb } from '@/modules/database/index.js';
5
6
  import {
@@ -91,7 +92,7 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
91
92
  filePath: string,
92
93
  nameMap: Map<string, string>
93
94
  ): Promise<ParsedSession | null> {
94
- return extractFirstValidJsonlData(filePath, (rawData) => {
95
+ const parsed = await extractFirstValidJsonlData(filePath, (rawData) => {
95
96
  const data = rawData as Record<string, unknown>;
96
97
  const sessionId = typeof data.sessionId === 'string' ? data.sessionId : undefined;
97
98
  const projectPath = typeof data.cwd === 'string' ? data.cwd : undefined;
@@ -103,8 +104,73 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
103
104
  return {
104
105
  sessionId,
105
106
  projectPath,
106
- sessionName: normalizeSessionName(nameMap.get(sessionId), 'Untitled Claude Session'),
107
107
  };
108
108
  });
109
+
110
+ if (!parsed) {
111
+ return null;
112
+ }
113
+
114
+ const existingSession = sessionsDb.getSessionById(parsed.sessionId);
115
+ const existingSessionName = existingSession?.custom_name;
116
+ if (existingSessionName && existingSessionName !== 'Untitled Claude Session') {
117
+ return {
118
+ ...parsed,
119
+ sessionName: normalizeSessionName(existingSessionName, 'Untitled Claude Session'),
120
+ };
121
+ }
122
+
123
+ let sessionName = nameMap.get(parsed.sessionId);
124
+ if (!sessionName) {
125
+ sessionName = await this.extractSessionAiTitleFromEnd(filePath, parsed.sessionId);
126
+ }
127
+
128
+ return {
129
+ ...parsed,
130
+ sessionName: normalizeSessionName(sessionName, 'Untitled Claude Session'),
131
+ };
132
+ }
133
+
134
+ private async extractSessionAiTitleFromEnd(
135
+ filePath: string,
136
+ sessionId: string
137
+ ): Promise<string | undefined> {
138
+ try {
139
+ const content = await readFile(filePath, 'utf8');
140
+ const lines = content.split(/\r?\n/);
141
+
142
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
143
+ const line = lines[index]?.trim();
144
+ if (!line) {
145
+ continue;
146
+ }
147
+
148
+ let parsed: unknown;
149
+ try {
150
+ parsed = JSON.parse(line);
151
+ } catch {
152
+ continue;
153
+ }
154
+
155
+ const data = parsed as Record<string, unknown>;
156
+ const eventType = typeof data.type === 'string' ? data.type : undefined;
157
+ const eventSessionId = typeof data.sessionId === 'string' ? data.sessionId : undefined;
158
+ const aiTitle = typeof data.aiTitle === 'string' ? data.aiTitle : undefined;
159
+ const lastPrompt = typeof data.lastPrompt === 'string' ? data.lastPrompt : undefined;
160
+ const claudeRenamedTitle = typeof data.customTitle === 'string' ? data.customTitle : undefined;
161
+
162
+ if (
163
+ (eventType === 'ai-title' && eventSessionId === sessionId && aiTitle?.trim()) ||
164
+ (eventType === 'last-prompt' && eventSessionId === sessionId && lastPrompt?.trim()) ||
165
+ (eventType === "custom-title" && eventSessionId === sessionId && claudeRenamedTitle?.trim())
166
+ ) {
167
+ return aiTitle || lastPrompt || claudeRenamedTitle;
168
+ }
169
+ }
170
+ } catch {
171
+ // Ignore missing/unreadable files so sync can continue.
172
+ }
173
+
174
+ return undefined;
109
175
  }
110
176
  }
@@ -200,17 +200,18 @@ async function getSessionMessages(
200
200
  }
201
201
 
202
202
  /**
203
- * Claude writes internal command and system reminder entries into history.
204
- * Those are useful for the CLI but should not appear in the user-facing chat.
203
+ * Claude writes a mix of truly internal transcript rows and "UI-hidden" local
204
+ * command artifacts into the same JSONL stream.
205
+ *
206
+ * Important distinction:
207
+ * - system reminders / caveats / interruption banners should stay hidden
208
+ * - local command payloads (`<command-name>...`) and stdout wrappers
209
+ * (`<local-command-stdout>...`) should be remapped into normal chat messages
210
+ * instead of being discarded as internal content
205
211
  */
206
212
  const INTERNAL_CONTENT_PREFIXES = [
207
- '<command-name>',
208
- '<command-message>',
209
- '<command-args>',
210
- '<local-command-stdout>',
211
213
  '<system-reminder>',
212
214
  'Caveat:',
213
- 'This session is being continued from a previous',
214
215
  '[Request interrupted',
215
216
  ] as const;
216
217
 
@@ -218,6 +219,73 @@ function isInternalContent(content: string): boolean {
218
219
  return INTERNAL_CONTENT_PREFIXES.some((prefix) => content.startsWith(prefix));
219
220
  }
220
221
 
222
+ /**
223
+ * Claude wraps local slash-command metadata in lightweight XML-like tags inside
224
+ * a plain string payload. We intentionally parse only the small tag surface we
225
+ * care about instead of introducing a generic XML parser for untrusted history.
226
+ */
227
+ function extractTaggedContent(content: string, tagName: string): string | null {
228
+ const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
229
+ const match = new RegExp(`<${escapedTagName}>([\\s\\S]*?)<\\/${escapedTagName}>`).exec(content);
230
+ return match ? match[1] : null;
231
+ }
232
+
233
+ type ClaudeLocalCommandPayload = {
234
+ commandName: string;
235
+ commandMessage: string;
236
+ commandArgs: string;
237
+ };
238
+
239
+ /**
240
+ * Converts Claude's hidden local command wrapper into structured metadata.
241
+ *
242
+ * The three tags often coexist in one string payload. Returning `null` lets the
243
+ * normal text path continue untouched for unrelated messages.
244
+ */
245
+ function parseLocalCommandPayload(content: string): ClaudeLocalCommandPayload | null {
246
+ const commandName = extractTaggedContent(content, 'command-name');
247
+ const commandMessage = extractTaggedContent(content, 'command-message');
248
+ const commandArgs = extractTaggedContent(content, 'command-args');
249
+
250
+ if (commandName === null && commandMessage === null && commandArgs === null) {
251
+ return null;
252
+ }
253
+
254
+ return {
255
+ commandName: commandName ?? '',
256
+ commandMessage: commandMessage ?? '',
257
+ commandArgs: commandArgs ?? '',
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Produces the short user-visible command string that should appear in chat.
263
+ *
264
+ * We prefer the slash-prefixed command name because that most closely matches
265
+ * what the user actually typed, and only fall back to the message body when the
266
+ * command name is unavailable in older transcript variants.
267
+ */
268
+ function buildLocalCommandDisplayText(payload: ClaudeLocalCommandPayload): string {
269
+ const commandName = payload.commandName.trim();
270
+ const commandMessage = payload.commandMessage.trim();
271
+ const commandArgs = payload.commandArgs.trim();
272
+ const baseCommand = commandName || commandMessage;
273
+
274
+ if (!baseCommand) {
275
+ return '';
276
+ }
277
+
278
+ return commandArgs ? `${baseCommand} ${commandArgs}` : baseCommand;
279
+ }
280
+
281
+ /**
282
+ * Claude local-command stdout may contain ANSI styling codes because it was
283
+ * captured from the terminal. The web chat should receive readable plain text.
284
+ */
285
+ function stripAnsiFormatting(text: string): string {
286
+ return text.replace(/\u001B\[[0-9;?]*[ -/]*[@-~]/g, '');
287
+ }
288
+
221
289
  export class ClaudeSessionsProvider implements IProviderSessions {
222
290
  /**
223
291
  * Normalizes one Claude JSONL entry or live SDK stream event into the shared
@@ -240,7 +308,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
240
308
  const ts = raw.timestamp || new Date().toISOString();
241
309
  const baseId = raw.uuid || generateMessageId('claude');
242
310
 
243
- if (raw.message?.role === 'user' && raw.message?.content) {
311
+ if (raw.message?.role === 'user' && raw.message?.content && raw.isMeta !== true) {
244
312
  if (Array.isArray(raw.message.content)) {
245
313
  for (let partIndex = 0; partIndex < raw.message.content.length; partIndex++) {
246
314
  const part = raw.message.content[partIndex];
@@ -293,6 +361,80 @@ export class ClaudeSessionsProvider implements IProviderSessions {
293
361
  }
294
362
  } else if (typeof raw.message.content === 'string') {
295
363
  const text = raw.message.content;
364
+
365
+ /**
366
+ * Claude stores compact summaries as synthetic "user" rows so the CLI
367
+ * can resume the next session turn with the summary in-context.
368
+ *
369
+ * For the web UI this is much more useful as assistant-authored summary
370
+ * text; otherwise it is both filtered by the generic internal-prefix
371
+ * check and visually mislabeled as a user message.
372
+ */
373
+ if (raw.isCompactSummary === true && text.trim()) {
374
+ messages.push(createNormalizedMessage({
375
+ id: baseId,
376
+ sessionId,
377
+ timestamp: ts,
378
+ provider: PROVIDER,
379
+ kind: 'text',
380
+ role: 'assistant',
381
+ content: text,
382
+ isCompactSummary: true,
383
+ }));
384
+ return messages;
385
+ }
386
+
387
+ /**
388
+ * Local slash commands are serialized as tagged text even though they
389
+ * are semantically a user action. Expose the parsed fields to the
390
+ * frontend and emit a plain user-visible command string so the command
391
+ * no longer disappears from history.
392
+ */
393
+ const localCommandPayload = parseLocalCommandPayload(text);
394
+ if (localCommandPayload) {
395
+ const displayText = buildLocalCommandDisplayText(localCommandPayload);
396
+ if (displayText) {
397
+ messages.push(createNormalizedMessage({
398
+ id: baseId,
399
+ sessionId,
400
+ timestamp: ts,
401
+ provider: PROVIDER,
402
+ kind: 'text',
403
+ role: 'user',
404
+ content: displayText,
405
+ commandName: localCommandPayload.commandName,
406
+ commandMessage: localCommandPayload.commandMessage,
407
+ commandArgs: localCommandPayload.commandArgs,
408
+ isLocalCommand: true,
409
+ }));
410
+ }
411
+ return messages;
412
+ }
413
+
414
+ /**
415
+ * Local command stdout is also written as a "user" row in Claude's
416
+ * transcript, but it is terminal output produced in response to the
417
+ * command. Re-label it as assistant text so the chat transcript matches
418
+ * the actual conversational flow seen by the user.
419
+ */
420
+ const localCommandStdout = extractTaggedContent(text, 'local-command-stdout');
421
+ if (localCommandStdout !== null) {
422
+ const stdoutText = stripAnsiFormatting(localCommandStdout).trim();
423
+ if (stdoutText) {
424
+ messages.push(createNormalizedMessage({
425
+ id: baseId,
426
+ sessionId,
427
+ timestamp: ts,
428
+ provider: PROVIDER,
429
+ kind: 'text',
430
+ role: 'assistant',
431
+ content: stdoutText,
432
+ isLocalCommandStdout: true,
433
+ }));
434
+ }
435
+ return messages;
436
+ }
437
+
296
438
  if (text && !isInternalContent(text)) {
297
439
  messages.push(createNormalizedMessage({
298
440
  id: baseId,
@@ -414,7 +556,9 @@ export class ClaudeSessionsProvider implements IProviderSessions {
414
556
 
415
557
  let result: ClaudeHistoryResult;
416
558
  try {
417
- result = await getSessionMessages(sessionId, limit, offset);
559
+ // Load full history first so `total` reflects frontend-normalized messages,
560
+ // not raw JSONL records.
561
+ result = await getSessionMessages(sessionId, null, 0);
418
562
  } catch (error) {
419
563
  const message = error instanceof Error ? error.message : String(error);
420
564
  console.warn(`[ClaudeProvider] Failed to load session ${sessionId}:`, message);
@@ -422,8 +566,6 @@ export class ClaudeSessionsProvider implements IProviderSessions {
422
566
  }
423
567
 
424
568
  const rawMessages = Array.isArray(result) ? result : (result.messages || []);
425
- const total = Array.isArray(result) ? rawMessages.length : (result.total || 0);
426
- const hasMore = Array.isArray(result) ? false : Boolean(result.hasMore);
427
569
 
428
570
  const toolResultMap = new Map<string, ClaudeToolResult>();
429
571
  for (const raw of rawMessages) {
@@ -464,12 +606,31 @@ export class ClaudeSessionsProvider implements IProviderSessions {
464
606
  }
465
607
  }
466
608
 
609
+ const totalNormalized = normalized.length;
610
+ let total = 0;
611
+ for (const msg of normalized) {
612
+ if (msg.kind !== 'tool_result') {
613
+ total += 1;
614
+ }
615
+ }
616
+ const normalizedOffset = Math.max(0, offset);
617
+ const normalizedLimit = limit === null ? null : Math.max(0, limit);
618
+ const messages = normalizedLimit === null
619
+ ? normalized
620
+ : normalized.slice(
621
+ Math.max(0, totalNormalized - normalizedOffset - normalizedLimit),
622
+ Math.max(0, totalNormalized - normalizedOffset),
623
+ );
624
+ const hasMore = normalizedLimit === null
625
+ ? false
626
+ : Math.max(0, totalNormalized - normalizedOffset - normalizedLimit) > 0;
627
+
467
628
  return {
468
- messages: normalized,
629
+ messages,
469
630
  total,
470
631
  hasMore,
471
- offset,
472
- limit,
632
+ offset: normalizedOffset,
633
+ limit: normalizedLimit,
473
634
  };
474
635
  }
475
636
  }