@cloudcli-ai/cloudcli 1.35.1 → 1.36.1

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 (223) hide show
  1. package/README.de.md +17 -18
  2. package/README.ja.md +16 -17
  3. package/README.ko.md +17 -18
  4. package/README.md +19 -20
  5. package/README.ru.md +17 -18
  6. package/README.tr.md +17 -18
  7. package/README.zh-CN.md +17 -18
  8. package/README.zh-TW.md +17 -18
  9. package/dist/api-docs.html +8 -3
  10. package/dist/assets/{index-D1IdCTT6.js → index-Bz9vXV2m.js} +265 -260
  11. package/dist/assets/index-DLmhQ15L.css +32 -0
  12. package/dist/assets/{vendor-xterm-CJZjLICi.js → vendor-xterm-CS4rQ5Mr.js} +12 -12
  13. package/dist/index.html +3 -3
  14. package/dist-server/server/claude-sdk.js +62 -116
  15. package/dist-server/server/claude-sdk.js.map +1 -1
  16. package/dist-server/server/cli.js +1 -4
  17. package/dist-server/server/cli.js.map +1 -1
  18. package/dist-server/server/cursor-cli.js +12 -11
  19. package/dist-server/server/cursor-cli.js.map +1 -1
  20. package/dist-server/server/index.js +10 -141
  21. package/dist-server/server/index.js.map +1 -1
  22. package/dist-server/server/modules/assets/assets.routes.js +89 -0
  23. package/dist-server/server/modules/assets/assets.routes.js.map +1 -0
  24. package/dist-server/server/modules/assets/index.js +4 -0
  25. package/dist-server/server/modules/assets/index.js.map +1 -0
  26. package/dist-server/server/modules/assets/services/image-assets.service.js +57 -0
  27. package/dist-server/server/modules/assets/services/image-assets.service.js.map +1 -0
  28. package/dist-server/server/modules/assets/tests/image-assets.service.test.js +36 -0
  29. package/dist-server/server/modules/assets/tests/image-assets.service.test.js.map +1 -0
  30. package/dist-server/server/modules/browser-use/browser-use.service.js +6 -3
  31. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
  32. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -2
  33. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
  34. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
  35. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
  36. package/dist-server/server/modules/projects/services/project-clone.service.js +2 -1
  37. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
  38. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +70 -1
  39. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  40. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +28 -0
  41. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  42. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +52 -11
  43. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -1
  45. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  46. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +36 -1
  47. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  48. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +3 -17
  49. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +7 -1
  51. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  52. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +49 -13
  53. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  54. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +0 -1
  55. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -1
  56. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +145 -14
  57. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  58. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +24 -5
  59. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  60. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +10 -21
  61. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
  62. package/dist-server/server/modules/providers/provider.registry.js +0 -2
  63. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  64. package/dist-server/server/modules/providers/provider.routes.js +0 -1
  65. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  66. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +11 -13
  67. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  68. package/dist-server/server/modules/providers/services/provider-models.service.js +2 -2
  69. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  70. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +3 -77
  71. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  72. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +0 -1
  73. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  74. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +0 -13
  75. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  76. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +96 -0
  77. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -0
  78. package/dist-server/server/modules/providers/tests/mcp.test.js +3 -29
  79. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  80. package/dist-server/server/modules/providers/tests/opencode-models.test.js +65 -1
  81. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  82. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +117 -0
  83. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  84. package/dist-server/server/modules/providers/tests/provider-image-history.test.js +147 -0
  85. package/dist-server/server/modules/providers/tests/provider-image-history.test.js.map +1 -0
  86. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +8 -8
  87. package/dist-server/server/modules/providers/tests/skills.test.js +3 -26
  88. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  89. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +14 -0
  90. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  91. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +36 -2
  92. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  93. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +3 -12
  94. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  95. package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js +33 -0
  96. package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js.map +1 -0
  97. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +37 -4
  98. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  99. package/dist-server/server/openai-codex.js +16 -9
  100. package/dist-server/server/openai-codex.js.map +1 -1
  101. package/dist-server/server/opencode-cli.js +62 -7
  102. package/dist-server/server/opencode-cli.js.map +1 -1
  103. package/dist-server/server/opencode-cli.test.js +103 -3
  104. package/dist-server/server/opencode-cli.test.js.map +1 -1
  105. package/dist-server/server/routes/agent.js +22 -21
  106. package/dist-server/server/routes/agent.js.map +1 -1
  107. package/dist-server/server/routes/commands.js +1 -2
  108. package/dist-server/server/routes/commands.js.map +1 -1
  109. package/dist-server/server/routes/git.js +179 -53
  110. package/dist-server/server/routes/git.js.map +1 -1
  111. package/dist-server/server/routes/git.test.js +80 -0
  112. package/dist-server/server/routes/git.test.js.map +1 -0
  113. package/dist-server/server/routes/taskmaster.js +3 -1
  114. package/dist-server/server/routes/taskmaster.js.map +1 -1
  115. package/dist-server/server/routes/user.js +2 -1
  116. package/dist-server/server/routes/user.js.map +1 -1
  117. package/dist-server/server/shared/image-attachments.js +270 -0
  118. package/dist-server/server/shared/image-attachments.js.map +1 -0
  119. package/dist-server/server/shared/tests/image-attachments.test.js +220 -0
  120. package/dist-server/server/shared/tests/image-attachments.test.js.map +1 -0
  121. package/dist-server/server/shared/utils.js +43 -0
  122. package/dist-server/server/shared/utils.js.map +1 -1
  123. package/dist-server/server/utils/gitConfig.js +2 -1
  124. package/dist-server/server/utils/gitConfig.js.map +1 -1
  125. package/dist-server/server/utils/plugin-process-manager.js +2 -1
  126. package/dist-server/server/utils/plugin-process-manager.js.map +1 -1
  127. package/electron/launcher/launcher.js +1 -1
  128. package/package.json +3 -3
  129. package/public/api-docs.html +8 -3
  130. package/server/claude-sdk.js +69 -131
  131. package/server/cli.js +1 -4
  132. package/server/cursor-cli.js +13 -12
  133. package/server/index.js +11 -159
  134. package/server/modules/assets/assets.routes.ts +103 -0
  135. package/server/modules/assets/index.ts +3 -0
  136. package/server/modules/assets/services/image-assets.service.ts +82 -0
  137. package/server/modules/assets/tests/image-assets.service.test.ts +46 -0
  138. package/server/modules/browser-use/browser-use.service.ts +7 -3
  139. package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -2
  140. package/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
  141. package/server/modules/projects/services/project-clone.service.ts +3 -1
  142. package/server/modules/providers/README.md +1 -7
  143. package/server/modules/providers/list/claude/claude-models.provider.ts +73 -1
  144. package/server/modules/providers/list/claude/claude-sessions.provider.ts +30 -0
  145. package/server/modules/providers/list/codex/codex-models.provider.ts +59 -11
  146. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +60 -1
  147. package/server/modules/providers/list/codex/codex-sessions.provider.ts +41 -1
  148. package/server/modules/providers/list/cursor/cursor-models.provider.ts +3 -17
  149. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +7 -1
  150. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +56 -15
  151. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +0 -1
  152. package/server/modules/providers/list/opencode/opencode-models.provider.ts +193 -18
  153. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +25 -4
  154. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +10 -21
  155. package/server/modules/providers/provider.registry.ts +0 -2
  156. package/server/modules/providers/provider.routes.ts +0 -1
  157. package/server/modules/providers/services/provider-capabilities.service.ts +13 -13
  158. package/server/modules/providers/services/provider-models.service.ts +2 -2
  159. package/server/modules/providers/services/session-conversations-search.service.ts +3 -93
  160. package/server/modules/providers/services/session-synchronizer.service.ts +0 -1
  161. package/server/modules/providers/services/sessions-watcher.service.ts +0 -14
  162. package/server/modules/providers/tests/codex-sessions.test.ts +111 -0
  163. package/server/modules/providers/tests/mcp.test.ts +3 -34
  164. package/server/modules/providers/tests/opencode-models.test.ts +69 -0
  165. package/server/modules/providers/tests/opencode-sessions.test.ts +139 -0
  166. package/server/modules/providers/tests/provider-image-history.test.ts +180 -0
  167. package/server/modules/providers/tests/provider-models.service.test.ts +8 -8
  168. package/server/modules/providers/tests/skills.test.ts +3 -42
  169. package/server/modules/websocket/services/chat-run-registry.service.ts +16 -0
  170. package/server/modules/websocket/services/chat-websocket.service.ts +41 -2
  171. package/server/modules/websocket/services/shell-websocket.service.ts +1 -11
  172. package/server/modules/websocket/tests/chat-image-filter.test.ts +44 -0
  173. package/server/modules/websocket/tests/chat-run-registry.test.ts +42 -4
  174. package/server/openai-codex.js +18 -8
  175. package/server/opencode-cli.js +64 -7
  176. package/server/opencode-cli.test.js +112 -3
  177. package/server/routes/agent.js +22 -21
  178. package/server/routes/commands.js +1 -2
  179. package/server/routes/git.js +201 -60
  180. package/server/routes/git.test.js +106 -0
  181. package/server/routes/taskmaster.js +3 -1
  182. package/server/routes/user.js +2 -1
  183. package/server/shared/image-attachments.ts +341 -0
  184. package/server/shared/tests/image-attachments.test.ts +298 -0
  185. package/server/shared/types.ts +8 -1
  186. package/server/shared/utils.ts +45 -0
  187. package/server/utils/gitConfig.js +2 -1
  188. package/server/utils/plugin-process-manager.js +3 -1
  189. package/dist/assets/index-BvX30NW5.css +0 -32
  190. package/dist/icons/gemini-ai-icon.svg +0 -1
  191. package/dist-server/server/gemini-cli.js +0 -551
  192. package/dist-server/server/gemini-cli.js.map +0 -1
  193. package/dist-server/server/gemini-response-handler.js +0 -106
  194. package/dist-server/server/gemini-response-handler.js.map +0 -1
  195. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +0 -254
  196. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +0 -1
  197. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +0 -82
  198. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +0 -1
  199. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +0 -24
  200. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +0 -1
  201. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +0 -312
  202. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +0 -1
  203. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +0 -474
  204. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +0 -1
  205. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +0 -40
  206. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +0 -1
  207. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +0 -19
  208. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +0 -1
  209. package/dist-server/server/routes/gemini.js +0 -21
  210. package/dist-server/server/routes/gemini.js.map +0 -1
  211. package/dist-server/server/sessionManager.js +0 -194
  212. package/dist-server/server/sessionManager.js.map +0 -1
  213. package/server/gemini-cli.js +0 -638
  214. package/server/gemini-response-handler.js +0 -117
  215. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +0 -307
  216. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +0 -110
  217. package/server/modules/providers/list/gemini/gemini-models.provider.ts +0 -39
  218. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +0 -405
  219. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +0 -540
  220. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +0 -44
  221. package/server/modules/providers/list/gemini/gemini.provider.ts +0 -27
  222. package/server/routes/gemini.js +0 -25
  223. package/server/sessionManager.js +0 -226
@@ -0,0 +1,111 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
8
+ import { CodexSessionSynchronizer } from '@/modules/providers/list/codex/codex-session-synchronizer.provider.js';
9
+
10
+ const patchHomeDir = (nextHomeDir: string) => {
11
+ const original = os.homedir;
12
+ (os as any).homedir = () => nextHomeDir;
13
+ return () => {
14
+ (os as any).homedir = original;
15
+ };
16
+ };
17
+
18
+ async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
19
+ const previousDatabasePath = process.env.DATABASE_PATH;
20
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'codex-provider-db-'));
21
+ const databasePath = path.join(tempDirectory, 'auth.db');
22
+
23
+ closeConnection();
24
+ process.env.DATABASE_PATH = databasePath;
25
+ await initializeDatabase();
26
+
27
+ try {
28
+ await runTest();
29
+ } finally {
30
+ closeConnection();
31
+ if (previousDatabasePath === undefined) {
32
+ delete process.env.DATABASE_PATH;
33
+ } else {
34
+ process.env.DATABASE_PATH = previousDatabasePath;
35
+ }
36
+ await rm(tempDirectory, { recursive: true, force: true });
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Writes one Codex rollout transcript. `firstUserMessage` mirrors the
42
+ * `event_msg`/`user_message` payload the runtime records for the prompt the
43
+ * user typed; omitting it produces a transcript with no user turn.
44
+ */
45
+ const writeCodexTranscript = async (
46
+ homeDir: string,
47
+ codexSessionId: string,
48
+ workspacePath: string,
49
+ firstUserMessage?: string,
50
+ ): Promise<string> => {
51
+ const sessionsDir = path.join(homeDir, '.codex', 'sessions', '2026', '07', '07');
52
+ await mkdir(sessionsDir, { recursive: true });
53
+
54
+ const lines: string[] = [
55
+ JSON.stringify({ type: 'session_meta', payload: { id: codexSessionId, cwd: workspacePath } }),
56
+ ];
57
+ if (firstUserMessage !== undefined) {
58
+ lines.push(JSON.stringify({ type: 'event_msg', payload: { type: 'user_message', message: firstUserMessage } }));
59
+ }
60
+
61
+ const filePath = path.join(sessionsDir, `rollout-${codexSessionId}.jsonl`);
62
+ await writeFile(filePath, `${lines.join('\n')}\n`, 'utf8');
63
+ return filePath;
64
+ };
65
+
66
+ test('Codex synchronizer titles app-created sessions from the first user message', { concurrency: false }, async () => {
67
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'codex-session-sync-app-'));
68
+ const workspacePath = path.join(tempRoot, 'workspace');
69
+ await mkdir(workspacePath, { recursive: true });
70
+ const restoreHomeDir = patchHomeDir(tempRoot);
71
+
72
+ try {
73
+ await writeCodexTranscript(tempRoot, 'codex-app-1', workspacePath, 'Fix the login redirect bug');
74
+ await withIsolatedDatabase(async () => {
75
+ // The app allocates its own id and later maps the provider id onto it,
76
+ // exactly as a message sent from cloudcli does.
77
+ sessionsDb.createAppSession('app-1', 'codex', workspacePath);
78
+ sessionsDb.assignProviderSessionId('app-1', 'codex-app-1');
79
+
80
+ const synchronizer = new CodexSessionSynchronizer();
81
+ await synchronizer.synchronize();
82
+
83
+ assert.equal(sessionsDb.getSessionById('app-1')?.custom_name, 'Fix the login redirect bug');
84
+ });
85
+ } finally {
86
+ restoreHomeDir();
87
+ await rm(tempRoot, { recursive: true, force: true });
88
+ }
89
+ });
90
+
91
+ test('Codex synchronizer leaves indexed sessions untitled when no name is available', { concurrency: false }, async () => {
92
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'codex-session-sync-indexed-'));
93
+ const workspacePath = path.join(tempRoot, 'workspace');
94
+ await mkdir(workspacePath, { recursive: true });
95
+ const restoreHomeDir = patchHomeDir(tempRoot);
96
+
97
+ try {
98
+ // A CLI-created session has no app row; its first user message must NOT be
99
+ // used as the title, preserving the existing indexing behavior.
100
+ await writeCodexTranscript(tempRoot, 'codex-indexed-1', workspacePath, 'This prompt should be ignored');
101
+ await withIsolatedDatabase(async () => {
102
+ const synchronizer = new CodexSessionSynchronizer();
103
+ await synchronizer.synchronize();
104
+
105
+ assert.equal(sessionsDb.getSessionById('codex-indexed-1')?.custom_name, 'Untitled Codex Session');
106
+ });
107
+ } finally {
108
+ restoreHomeDir();
109
+ await rm(tempRoot, { recursive: true, force: true });
110
+ }
111
+ });
@@ -257,34 +257,15 @@ test('providerMcpService handles opencode MCP config and capability validation',
257
257
  });
258
258
 
259
259
  /**
260
- * This test covers Gemini/Cursor MCP JSON formats and user/project scope persistence.
260
+ * This test covers Cursor MCP JSON format and user/project scope persistence.
261
261
  */
262
- test('providerMcpService handles gemini and cursor MCP JSON config formats', { concurrency: false }, async () => {
262
+ test('providerMcpService handles cursor MCP JSON config formats', { concurrency: false }, async () => {
263
263
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-gc-'));
264
264
  const workspacePath = path.join(tempRoot, 'workspace');
265
265
  await fs.mkdir(workspacePath, { recursive: true });
266
266
 
267
267
  const restoreHomeDir = patchHomeDir(tempRoot);
268
268
  try {
269
- await providerMcpService.upsertProviderMcpServer('gemini', {
270
- name: 'gemini-stdio',
271
- scope: 'user',
272
- transport: 'stdio',
273
- command: 'node',
274
- args: ['server.js'],
275
- env: { TOKEN: '$TOKEN' },
276
- cwd: './server',
277
- });
278
-
279
- await providerMcpService.upsertProviderMcpServer('gemini', {
280
- name: 'gemini-http',
281
- scope: 'project',
282
- transport: 'http',
283
- url: 'https://gemini.example.com/mcp',
284
- headers: { Authorization: 'Bearer token' },
285
- workspacePath,
286
- });
287
-
288
269
  await providerMcpService.upsertProviderMcpServer('cursor', {
289
270
  name: 'cursor-stdio',
290
271
  scope: 'project',
@@ -303,15 +284,6 @@ test('providerMcpService handles gemini and cursor MCP JSON config formats', { c
303
284
  headers: { API_KEY: 'value' },
304
285
  });
305
286
 
306
- const geminiUserConfig = await readJson(path.join(tempRoot, '.gemini', 'settings.json'));
307
- const geminiUserServer = (geminiUserConfig.mcpServers as Record<string, unknown>)['gemini-stdio'] as Record<string, unknown>;
308
- assert.equal(geminiUserServer.command, 'node');
309
- assert.equal(geminiUserServer.type, undefined);
310
-
311
- const geminiProjectConfig = await readJson(path.join(workspacePath, '.gemini', 'settings.json'));
312
- const geminiProjectServer = (geminiProjectConfig.mcpServers as Record<string, unknown>)['gemini-http'] as Record<string, unknown>;
313
- assert.equal(geminiProjectServer.type, 'http');
314
-
315
287
  const cursorUserConfig = await readJson(path.join(tempRoot, '.cursor', 'mcp.json'));
316
288
  const cursorHttpServer = (cursorUserConfig.mcpServers as Record<string, unknown>)['cursor-http'] as Record<string, unknown>;
317
289
  assert.equal(cursorHttpServer.url, 'http://localhost:3333/mcp');
@@ -341,7 +313,7 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
341
313
  workspacePath,
342
314
  });
343
315
 
344
- assert.equal(globalResult.length, 5);
316
+ assert.equal(globalResult.length, 4);
345
317
  assert.ok(globalResult.every((entry) => entry.created === true));
346
318
 
347
319
  const claudeProject = await readJson(path.join(workspacePath, '.mcp.json'));
@@ -350,9 +322,6 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
350
322
  const codexProject = TOML.parse(await fs.readFile(path.join(workspacePath, '.codex', 'config.toml'), 'utf8')) as Record<string, unknown>;
351
323
  assert.ok((codexProject.mcp_servers as Record<string, unknown>)['global-http']);
352
324
 
353
- const geminiProject = await readJson(path.join(workspacePath, '.gemini', 'settings.json'));
354
- assert.ok((geminiProject.mcpServers as Record<string, unknown>)['global-http']);
355
-
356
325
  const opencodeProject = await readJson(path.join(workspacePath, 'opencode.json'));
357
326
  assert.ok((opencodeProject.mcp as Record<string, unknown>)['global-http']);
358
327
 
@@ -2,8 +2,10 @@ import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
 
4
4
  import {
5
+ buildOpenCodeDefinitionFromVerboseModels,
5
6
  buildOpenCodeDefinitionFromIds,
6
7
  parseOpenCodeModelsStdout,
8
+ parseOpenCodeVerboseModelsStdout,
7
9
  } from '@/modules/providers/list/opencode/opencode-models.provider.js';
8
10
 
9
11
  test('OpenCode models provider parses plain CLI output and removes duplicates', () => {
@@ -28,6 +30,7 @@ test('OpenCode models provider formats frontend labels from provider-prefixed id
28
30
  'opencode/nemotron-3-super-free',
29
31
  'anthropic/claude-3-5-sonnet-20241022',
30
32
  'anthropic/claude-opus-4-7-fast',
33
+ 'google/model-alpha',
31
34
  'openai/gpt-5.4-mini-fast',
32
35
  'openai/gpt-5.5-pro',
33
36
  'newprovider/alpha-v12-special-20261231',
@@ -71,3 +74,69 @@ test('OpenCode models provider formats frontend labels from provider-prefixed id
71
74
  },
72
75
  ]);
73
76
  });
77
+
78
+ test('OpenCode models provider maps verbose model variants to effort options', () => {
79
+ const models = parseOpenCodeVerboseModelsStdout(`
80
+ opencode/deepseek-v4-flash-free
81
+ {
82
+ "id": "deepseek-v4-flash-free",
83
+ "providerID": "opencode",
84
+ "name": "DeepSeek V4 Flash Free",
85
+ "variants": {
86
+ "low": {
87
+ "reasoningEffort": "low"
88
+ },
89
+ "high": {
90
+ "reasoningEffort": "high"
91
+ }
92
+ }
93
+ }
94
+ anthropic/claude-sonnet-5
95
+ {
96
+ "id": "claude-sonnet-5",
97
+ "providerID": "anthropic",
98
+ "name": "Claude Sonnet 5",
99
+ "variants": {
100
+ "low": {
101
+ "effort": "low"
102
+ },
103
+ "max": {
104
+ "effort": "max"
105
+ }
106
+ }
107
+ }
108
+ google/model-alpha
109
+ {
110
+ "id": "model-alpha",
111
+ "providerID": "google",
112
+ "name": "Model Alpha"
113
+ }
114
+ `);
115
+
116
+ const definition = buildOpenCodeDefinitionFromVerboseModels(models);
117
+
118
+ assert.deepEqual(definition.OPTIONS, [
119
+ {
120
+ value: 'opencode/deepseek-v4-flash-free',
121
+ label: 'DeepSeek V4 Flash Free',
122
+ description: 'opencode - opencode/deepseek-v4-flash-free',
123
+ effort: {
124
+ values: [
125
+ { value: 'low' },
126
+ { value: 'high' },
127
+ ],
128
+ },
129
+ },
130
+ {
131
+ value: 'anthropic/claude-sonnet-5',
132
+ label: 'Claude Sonnet 5',
133
+ description: 'anthropic - anthropic/claude-sonnet-5',
134
+ effort: {
135
+ values: [
136
+ { value: 'low' },
137
+ { value: 'max' },
138
+ ],
139
+ },
140
+ },
141
+ ]);
142
+ });
@@ -9,6 +9,7 @@ import Database from 'better-sqlite3';
9
9
  import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
10
10
  import { OpenCodeSessionSynchronizer } from '@/modules/providers/list/opencode/opencode-session-synchronizer.provider.js';
11
11
  import { OpenCodeSessionsProvider } from '@/modules/providers/list/opencode/opencode-sessions.provider.js';
12
+ import { appendImagesInputTag } from '@/shared/image-attachments.js';
12
13
 
13
14
  const patchHomeDir = (nextHomeDir: string) => {
14
15
  const original = os.homedir;
@@ -321,6 +322,41 @@ test('OpenCode session synchronizer adopts the pending app session before watche
321
322
  }
322
323
  });
323
324
 
325
+ test('OpenCode sessions provider strips <images_input> from user turns and exposes attachments', { concurrency: false }, async () => {
326
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-images-'));
327
+ const workspacePath = path.join(tempRoot, 'workspace');
328
+ await mkdir(workspacePath, { recursive: true });
329
+ const restoreHomeDir = patchHomeDir(tempRoot);
330
+
331
+ try {
332
+ await createOpenCodeDatabase(tempRoot, workspacePath);
333
+
334
+ // Rewrite the user text part with the tagged prompt the runtime sends.
335
+ const taggedPrompt = appendImagesInputTag('Look at this screenshot.', [
336
+ { path: 'C:/Users/x/.cloudcli/assets/shot.png' },
337
+ ]);
338
+ const db = new Database(path.join(tempRoot, '.local', 'share', 'opencode', 'opencode.db'));
339
+ try {
340
+ db.prepare('UPDATE part SET data = ? WHERE id = ?').run(
341
+ JSON.stringify({ type: 'text', text: taggedPrompt }),
342
+ 'part-user-text',
343
+ );
344
+ } finally {
345
+ db.close();
346
+ }
347
+
348
+ const provider = new OpenCodeSessionsProvider();
349
+ const history = await provider.fetchHistory('open-session-1');
350
+ const userMessage = history.messages.find((message) => message.kind === 'text' && message.role === 'user');
351
+
352
+ assert.equal(userMessage?.content, 'Look at this screenshot.');
353
+ assert.deepEqual(userMessage?.images, [{ path: 'C:/Users/x/.cloudcli/assets/shot.png' }]);
354
+ } finally {
355
+ restoreHomeDir();
356
+ await rm(tempRoot, { recursive: true, force: true });
357
+ }
358
+ });
359
+
324
360
  test('OpenCode sessions provider normalizes quoted live text and skips user echoes', () => {
325
361
  const provider = new OpenCodeSessionsProvider();
326
362
  const normalized = provider.normalizeMessage({
@@ -381,3 +417,106 @@ test('OpenCode sessions provider reads sqlite history and token usage', { concur
381
417
  await rm(tempRoot, { recursive: true, force: true });
382
418
  }
383
419
  });
420
+
421
+ /**
422
+ * Seeds a single OpenCode session with a controllable stored title and first
423
+ * user message. Uses a minimal schema (only the columns the synchronizer reads)
424
+ * with a plain-text user part so the derived name is unambiguous.
425
+ */
426
+ const seedOpenCodeSession = async (
427
+ homeDir: string,
428
+ workspacePath: string,
429
+ options: { sessionId: string; title: string | null; firstUserText: string },
430
+ ): Promise<void> => {
431
+ const dataDir = path.join(homeDir, '.local', 'share', 'opencode');
432
+ await mkdir(dataDir, { recursive: true });
433
+
434
+ const db = new Database(path.join(dataDir, 'opencode.db'));
435
+ try {
436
+ db.exec(`
437
+ CREATE TABLE project (id TEXT PRIMARY KEY, worktree TEXT);
438
+ CREATE TABLE session (
439
+ id TEXT PRIMARY KEY,
440
+ project_id TEXT,
441
+ directory TEXT,
442
+ title TEXT,
443
+ time_created INTEGER,
444
+ time_updated INTEGER,
445
+ time_archived INTEGER
446
+ );
447
+ CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, time_created INTEGER, data TEXT);
448
+ CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT, time_created INTEGER, data TEXT);
449
+ `);
450
+
451
+ db.prepare('INSERT INTO project (id, worktree) VALUES (?, ?)').run('project-1', workspacePath);
452
+ db.prepare(`
453
+ INSERT INTO session (id, project_id, directory, title, time_created, time_updated, time_archived)
454
+ VALUES (?, ?, ?, ?, ?, ?, NULL)
455
+ `).run(options.sessionId, 'project-1', workspacePath, options.title, 1_700_000_000_000, 1_700_000_001_000);
456
+ db.prepare('INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)')
457
+ .run('message-user', options.sessionId, 1_700_000_001_000, JSON.stringify({ role: 'user' }));
458
+ db.prepare('INSERT INTO part (id, message_id, session_id, time_created, data) VALUES (?, ?, ?, ?, ?)')
459
+ .run(
460
+ 'part-user',
461
+ 'message-user',
462
+ options.sessionId,
463
+ 1_700_000_001_000,
464
+ // OpenCode persists the prompt as a JSON string literal inside the text
465
+ // field, so double-encode it here to exercise the unwrap on read.
466
+ JSON.stringify({ type: 'text', text: JSON.stringify(options.firstUserText) }),
467
+ );
468
+ } finally {
469
+ db.close();
470
+ }
471
+ };
472
+
473
+ test('OpenCode synchronizer titles app-created sessions from the first user message', { concurrency: false }, async () => {
474
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-sync-app-'));
475
+ const workspacePath = path.join(tempRoot, 'workspace');
476
+ await mkdir(workspacePath, { recursive: true });
477
+ const restoreHomeDir = patchHomeDir(tempRoot);
478
+
479
+ try {
480
+ // Stored title differs from the first message so we can prove the first
481
+ // message wins for sessions started from cloudcli.
482
+ await seedOpenCodeSession(tempRoot, workspacePath, {
483
+ sessionId: 'oc-app-1',
484
+ title: 'OpenCode generated title',
485
+ firstUserText: 'Fix the checkout crash',
486
+ });
487
+ await withIsolatedDatabase(async () => {
488
+ sessionsDb.createAppSession('app-1', 'opencode', workspacePath);
489
+ sessionsDb.assignProviderSessionId('app-1', 'oc-app-1');
490
+
491
+ await new OpenCodeSessionSynchronizer().synchronize();
492
+
493
+ assert.equal(sessionsDb.getSessionById('app-1')?.custom_name, 'Fix the checkout crash');
494
+ });
495
+ } finally {
496
+ restoreHomeDir();
497
+ await rm(tempRoot, { recursive: true, force: true });
498
+ }
499
+ });
500
+
501
+ test('OpenCode synchronizer keeps the stored title for indexed sessions', { concurrency: false }, async () => {
502
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-sync-indexed-'));
503
+ const workspacePath = path.join(tempRoot, 'workspace');
504
+ await mkdir(workspacePath, { recursive: true });
505
+ const restoreHomeDir = patchHomeDir(tempRoot);
506
+
507
+ try {
508
+ await seedOpenCodeSession(tempRoot, workspacePath, {
509
+ sessionId: 'oc-indexed-1',
510
+ title: 'OpenCode generated title',
511
+ firstUserText: 'This prompt should be ignored',
512
+ });
513
+ await withIsolatedDatabase(async () => {
514
+ await new OpenCodeSessionSynchronizer().synchronize();
515
+
516
+ assert.equal(sessionsDb.getSessionById('oc-indexed-1')?.custom_name, 'OpenCode generated title');
517
+ });
518
+ } finally {
519
+ restoreHomeDir();
520
+ await rm(tempRoot, { recursive: true, force: true });
521
+ }
522
+ });
@@ -0,0 +1,180 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { ClaudeSessionsProvider } from '@/modules/providers/list/claude/claude-sessions.provider.js';
5
+ import { CodexSessionsProvider, extractCodexUserImages } from '@/modules/providers/list/codex/codex-sessions.provider.js';
6
+ import { CursorSessionsProvider } from '@/modules/providers/list/cursor/cursor-sessions.provider.js';
7
+ import { appendImagesInputTag } from '@/shared/image-attachments.js';
8
+
9
+ const SESSION_ID = 'session-1';
10
+
11
+ // ---------------------------------------------------------------- Claude
12
+
13
+ test('claude history: base64 image blocks surface as user message images', () => {
14
+ const provider = new ClaudeSessionsProvider();
15
+ const entry = {
16
+ uuid: 'u1',
17
+ timestamp: '2026-07-03T10:00:00.000Z',
18
+ message: {
19
+ role: 'user',
20
+ content: [
21
+ { type: 'text', text: 'What is in this screenshot?' },
22
+ { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'QUJD' } },
23
+ { type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: 'REVG' } },
24
+ ],
25
+ },
26
+ };
27
+
28
+ const messages = provider.normalizeMessage(entry, SESSION_ID);
29
+
30
+ assert.equal(messages.length, 1);
31
+ assert.equal(messages[0].kind, 'text');
32
+ assert.equal(messages[0].role, 'user');
33
+ assert.equal(messages[0].content, 'What is in this screenshot?');
34
+ assert.deepEqual(messages[0].images, [
35
+ { data: 'data:image/png;base64,QUJD' },
36
+ { data: 'data:image/jpeg;base64,REVG' },
37
+ ]);
38
+ });
39
+
40
+ test('claude history: image-only user turns still produce a bubble', () => {
41
+ const provider = new ClaudeSessionsProvider();
42
+ const entry = {
43
+ uuid: 'u2',
44
+ timestamp: '2026-07-03T10:00:00.000Z',
45
+ message: {
46
+ role: 'user',
47
+ content: [
48
+ { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'QUJD' } },
49
+ ],
50
+ },
51
+ };
52
+
53
+ const messages = provider.normalizeMessage(entry, SESSION_ID);
54
+
55
+ assert.equal(messages.length, 1);
56
+ assert.equal(messages[0].role, 'user');
57
+ assert.equal(messages[0].content, '');
58
+ assert.deepEqual(messages[0].images, [{ data: 'data:image/png;base64,QUJD' }]);
59
+ });
60
+
61
+ test('claude history: plain text user turns carry no images field', () => {
62
+ const provider = new ClaudeSessionsProvider();
63
+ const entry = {
64
+ uuid: 'u3',
65
+ timestamp: '2026-07-03T10:00:00.000Z',
66
+ message: { role: 'user', content: [{ type: 'text', text: 'hello' }] },
67
+ };
68
+
69
+ const messages = provider.normalizeMessage(entry, SESSION_ID);
70
+ assert.equal(messages.length, 1);
71
+ assert.equal(messages[0].images, undefined);
72
+ });
73
+
74
+ // ---------------------------------------------------------------- Codex
75
+
76
+ test('codex history: user_message payload images become path attachments', () => {
77
+ // Real rollout shape: local_image input items land in `local_images`,
78
+ // while `images` stays an empty array.
79
+ assert.deepEqual(
80
+ extractCodexUserImages({
81
+ type: 'user_message',
82
+ message: 'can u see attached image?',
83
+ images: [],
84
+ local_images: ['C:\\proj\\.cloudcli\\assets\\a.png'],
85
+ }),
86
+ [{ path: 'C:/proj/.cloudcli/assets/a.png' }],
87
+ );
88
+ assert.deepEqual(
89
+ extractCodexUserImages({ type: 'user_message', message: 'hi', images: ['/proj/b.jpg'] }),
90
+ [{ path: '/proj/b.jpg' }],
91
+ );
92
+ assert.equal(extractCodexUserImages({ type: 'user_message', message: 'hi' }), undefined);
93
+ assert.equal(extractCodexUserImages({ type: 'user_message', message: 'hi', images: [], local_images: [] }), undefined);
94
+ });
95
+
96
+ test('codex history: base64 data URLs pass through as inline data attachments', () => {
97
+ const dataUrl = 'data:image/png;base64,QUJD';
98
+ assert.deepEqual(
99
+ extractCodexUserImages({
100
+ type: 'user_message',
101
+ message: 'look',
102
+ images: [dataUrl],
103
+ local_images: ['C:\\proj\\a.png'],
104
+ }),
105
+ [{ path: 'C:/proj/a.png' }, { data: dataUrl }],
106
+ );
107
+ });
108
+
109
+ test('codex history: normalized user entries keep their images', () => {
110
+ const provider = new CodexSessionsProvider();
111
+ const messages = provider.normalizeMessage(
112
+ {
113
+ timestamp: '2026-07-03T10:00:00.000Z',
114
+ message: { role: 'user', content: 'Look at this' },
115
+ images: [{ path: '.cloudcli/assets/a.png' }],
116
+ },
117
+ SESSION_ID,
118
+ );
119
+
120
+ assert.equal(messages.length, 1);
121
+ assert.equal(messages[0].role, 'user');
122
+ assert.equal(messages[0].content, 'Look at this');
123
+ assert.deepEqual(messages[0].images, [{ path: '.cloudcli/assets/a.png' }]);
124
+ });
125
+
126
+ // ---------------------------------------------------------------- Cursor
127
+
128
+ test('cursor history: <images_input> inside user_query is stripped and attached', () => {
129
+ const provider = new CursorSessionsProvider();
130
+ const taggedPrompt = appendImagesInputTag('Fix the layout bug', [{ path: '.cloudcli/assets/shot.png' }]);
131
+ const blobs = [
132
+ {
133
+ id: 'blob1',
134
+ sequence: 1,
135
+ rowid: 1,
136
+ content: {
137
+ role: 'user',
138
+ content: `<timestamp>2026-07-03</timestamp>\n<user_query>${taggedPrompt}</user_query>`,
139
+ },
140
+ },
141
+ {
142
+ id: 'blob2',
143
+ sequence: 2,
144
+ rowid: 2,
145
+ content: {
146
+ role: 'assistant',
147
+ content: [{ type: 'text', text: 'Done — the flex container was wrong.' }],
148
+ },
149
+ },
150
+ ];
151
+
152
+ const messages = provider.normalizeCursorBlobs(blobs, SESSION_ID);
153
+
154
+ assert.equal(messages.length, 2);
155
+ assert.equal(messages[0].role, 'user');
156
+ assert.equal(messages[0].content, 'Fix the layout bug');
157
+ assert.deepEqual(messages[0].images, [{ path: '.cloudcli/assets/shot.png' }]);
158
+ assert.equal(messages[1].role, 'assistant');
159
+ assert.equal(messages[1].images, undefined);
160
+ });
161
+
162
+ test('cursor history: user text without a tag keeps existing behavior', () => {
163
+ const provider = new CursorSessionsProvider();
164
+ const blobs = [
165
+ {
166
+ id: 'blob1',
167
+ sequence: 1,
168
+ rowid: 1,
169
+ content: {
170
+ role: 'user',
171
+ content: '<timestamp>2026-07-03</timestamp>\n<user_query>plain question</user_query>',
172
+ },
173
+ },
174
+ ];
175
+
176
+ const messages = provider.normalizeCursorBlobs(blobs, SESSION_ID);
177
+ assert.equal(messages.length, 1);
178
+ assert.equal(messages[0].content, 'plain question');
179
+ assert.equal(messages[0].images, undefined);
180
+ });
@@ -170,13 +170,13 @@ test('provider model cache is persisted across service instances', async () => {
170
170
  cachePath,
171
171
  resolveProvider: () => ({
172
172
  models: {
173
- getSupportedModels: async () => createModels('gemini-cached'),
174
- getCurrentActiveModel: async () => createCurrentActiveModel('gemini-active'),
175
- changeActiveModel: async (input) => createSessionActiveModelChange('gemini', input),
173
+ getSupportedModels: async () => createModels('cursor-cached'),
174
+ getCurrentActiveModel: async () => createCurrentActiveModel('cursor-active'),
175
+ changeActiveModel: async (input) => createSessionActiveModelChange('cursor', input),
176
176
  },
177
177
  }),
178
178
  });
179
- await writer.getProviderModels('gemini');
179
+ await writer.getProviderModels('cursor');
180
180
 
181
181
  const reader = createProviderModelsService({
182
182
  cachePath,
@@ -185,13 +185,13 @@ test('provider model cache is persisted across service instances', async () => {
185
185
  getSupportedModels: async () => {
186
186
  throw new Error('loader should not be called for persisted cache hits');
187
187
  },
188
- getCurrentActiveModel: async () => createCurrentActiveModel('gemini-active'),
189
- changeActiveModel: async (input) => createSessionActiveModelChange('gemini', input),
188
+ getCurrentActiveModel: async () => createCurrentActiveModel('cursor-active'),
189
+ changeActiveModel: async (input) => createSessionActiveModelChange('cursor', input),
190
190
  },
191
191
  }),
192
192
  });
193
- const models = await reader.getProviderModels('gemini');
194
- assert.equal(models.models.DEFAULT, 'gemini-cached');
193
+ const models = await reader.getProviderModels('cursor');
194
+ assert.equal(models.models.DEFAULT, 'cursor-cached');
195
195
  assert.equal(models.cache.source, 'disk');
196
196
  } finally {
197
197
  await rm(tempRoot, { recursive: true, force: true });