@cloudcli-ai/cloudcli 1.34.0 → 1.35.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 (247) hide show
  1. package/README.de.md +9 -1
  2. package/README.ja.md +8 -0
  3. package/README.ko.md +9 -1
  4. package/README.md +23 -5
  5. package/README.ru.md +9 -1
  6. package/README.tr.md +8 -1
  7. package/README.zh-CN.md +9 -1
  8. package/README.zh-TW.md +9 -1
  9. package/dist/api-docs.html +36 -18
  10. package/dist/assets/index-BvX30NW5.css +32 -0
  11. package/dist/assets/index-D1IdCTT6.js +1418 -0
  12. package/dist/index.html +12 -4
  13. package/dist-server/server/browser-use-mcp.js +350 -0
  14. package/dist-server/server/browser-use-mcp.js.map +1 -0
  15. package/dist-server/server/claude-sdk.js +25 -5
  16. package/dist-server/server/claude-sdk.js.map +1 -1
  17. package/dist-server/server/cli.js +14 -6
  18. package/dist-server/server/cli.js.map +1 -1
  19. package/dist-server/server/cursor-cli.js +27 -11
  20. package/dist-server/server/cursor-cli.js.map +1 -1
  21. package/dist-server/server/gemini-cli.js +17 -2
  22. package/dist-server/server/gemini-cli.js.map +1 -1
  23. package/dist-server/server/index.js +136 -40
  24. package/dist-server/server/index.js.map +1 -1
  25. package/dist-server/server/load-env.js +1 -1
  26. package/dist-server/server/load-env.js.map +1 -1
  27. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js +107 -0
  28. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js.map +1 -0
  29. package/dist-server/server/modules/browser-use/browser-use.routes.js +93 -0
  30. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -0
  31. package/dist-server/server/modules/browser-use/browser-use.service.js +688 -0
  32. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -0
  33. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js +8 -0
  34. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js.map +1 -0
  35. package/dist-server/server/modules/database/index.js +1 -0
  36. package/dist-server/server/modules/database/index.js.map +1 -1
  37. package/dist-server/server/modules/database/migrations.js +23 -1
  38. package/dist-server/server/modules/database/migrations.js.map +1 -1
  39. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +100 -0
  40. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js.map +1 -0
  41. package/dist-server/server/modules/database/repositories/notification-preferences.js +8 -0
  42. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -1
  43. package/dist-server/server/modules/database/repositories/sessions.db.js +173 -17
  44. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  45. package/dist-server/server/modules/database/schema.js +26 -0
  46. package/dist-server/server/modules/database/schema.js.map +1 -1
  47. package/dist-server/server/modules/database/tests/projects.db.integration.test.js.map +1 -0
  48. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +78 -0
  49. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -0
  50. package/dist-server/server/modules/database/{repositories → tests}/sessions.db.integration.test.js +10 -0
  51. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -0
  52. package/dist-server/server/modules/notifications/index.js +4 -0
  53. package/dist-server/server/modules/notifications/index.js.map +1 -0
  54. package/dist-server/server/modules/notifications/notifications.routes.js +115 -0
  55. package/dist-server/server/modules/notifications/notifications.routes.js.map +1 -0
  56. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js +97 -0
  57. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js.map +1 -0
  58. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +229 -0
  59. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -0
  60. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js +79 -0
  61. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js.map +1 -0
  62. package/dist-server/server/modules/projects/services/project-management.service.js +0 -4
  63. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
  64. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +10 -38
  65. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  66. package/dist-server/server/modules/providers/index.js +1 -0
  67. package/dist-server/server/modules/providers/index.js.map +1 -1
  68. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +24 -1
  69. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  70. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +9 -12
  71. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  72. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +7 -0
  73. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  74. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +7 -3
  75. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  76. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +3 -9
  77. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  78. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -0
  79. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
  80. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +13 -22
  81. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  82. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +7 -0
  83. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -1
  84. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +5 -8
  85. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -1
  86. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +6 -6
  87. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  88. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +7 -0
  89. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -1
  90. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +17 -3
  91. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  92. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +9 -10
  93. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
  94. package/dist-server/server/modules/providers/provider.routes.js +123 -1
  95. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  96. package/dist-server/server/modules/providers/services/mcp.service.js +23 -0
  97. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  98. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +65 -0
  99. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -0
  100. package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
  101. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  102. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +62 -23
  103. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  104. package/dist-server/server/modules/providers/services/sessions.service.js +62 -4
  105. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  106. package/dist-server/server/modules/providers/services/skills.service.js +11 -0
  107. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -1
  108. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +163 -1
  109. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -1
  110. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +45 -0
  111. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  112. package/dist-server/server/modules/providers/tests/skills.test.js +160 -0
  113. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  114. package/dist-server/server/modules/websocket/index.js +1 -0
  115. package/dist-server/server/modules/websocket/index.js.map +1 -1
  116. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +252 -0
  117. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -0
  118. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +105 -0
  119. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -0
  120. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +218 -156
  121. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  122. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +83 -31
  123. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  124. package/dist-server/server/modules/websocket/services/websocket-server.service.js +5 -0
  125. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -1
  126. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +216 -0
  127. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -0
  128. package/dist-server/server/openai-codex.js +24 -14
  129. package/dist-server/server/openai-codex.js.map +1 -1
  130. package/dist-server/server/opencode-cli.js +21 -8
  131. package/dist-server/server/opencode-cli.js.map +1 -1
  132. package/dist-server/server/opencode-cli.test.js +19 -1
  133. package/dist-server/server/opencode-cli.test.js.map +1 -1
  134. package/dist-server/server/routes/settings.js +1 -1
  135. package/dist-server/server/routes/settings.js.map +1 -1
  136. package/dist-server/server/services/notification-orchestrator.js +1 -176
  137. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  138. package/dist-server/server/services/tests/notification-orchestrator.test.js +64 -0
  139. package/dist-server/server/services/tests/notification-orchestrator.test.js.map +1 -0
  140. package/dist-server/server/shared/{claude-cli-path.test.js → tests/claude-cli-path.test.js} +1 -1
  141. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -0
  142. package/dist-server/server/shared/tests/slice-tail-page.test.js +35 -0
  143. package/dist-server/server/shared/tests/slice-tail-page.test.js.map +1 -0
  144. package/dist-server/server/shared/utils.js +73 -1
  145. package/dist-server/server/shared/utils.js.map +1 -1
  146. package/dist-server/server/voice-proxy.js +226 -0
  147. package/dist-server/server/voice-proxy.js.map +1 -0
  148. package/electron/assets/logo-macos.icns +0 -0
  149. package/electron/assets/logo-macos.png +0 -0
  150. package/electron/assets/logo-windows.ico +0 -0
  151. package/electron/cloud.js +260 -0
  152. package/electron/desktopNotifications.js +378 -0
  153. package/electron/desktopWindow.js +766 -0
  154. package/electron/launcher/index.html +14 -0
  155. package/electron/launcher/launcher.css +801 -0
  156. package/electron/launcher/launcher.js +687 -0
  157. package/electron/localServer.js +550 -0
  158. package/electron/main.js +944 -0
  159. package/electron/preload.cjs +60 -0
  160. package/electron/scripts/generate-macos-icon.js +62 -0
  161. package/electron/serverInstaller.js +277 -0
  162. package/electron/tabs.js +87 -0
  163. package/electron/viewHost.js +331 -0
  164. package/package.json +79 -3
  165. package/public/api-docs.html +36 -18
  166. package/scripts/release/build-server-bundle.js +176 -0
  167. package/scripts/release/prepare-desktop-app.js +152 -0
  168. package/server/browser-use-mcp.ts +384 -0
  169. package/server/claude-sdk.js +27 -5
  170. package/server/cli.js +15 -6
  171. package/server/cursor-cli.js +27 -11
  172. package/server/gemini-cli.js +17 -2
  173. package/server/index.js +137 -47
  174. package/server/load-env.js +1 -1
  175. package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -0
  176. package/server/modules/browser-use/browser-use.routes.ts +96 -0
  177. package/server/modules/browser-use/browser-use.service.ts +836 -0
  178. package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -0
  179. package/server/modules/database/index.ts +1 -0
  180. package/server/modules/database/migrations.ts +25 -0
  181. package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -0
  182. package/server/modules/database/repositories/notification-preferences.ts +12 -1
  183. package/server/modules/database/repositories/sessions.db.ts +227 -23
  184. package/server/modules/database/schema.ts +27 -0
  185. package/server/modules/database/tests/sessions-provider-mapping.test.ts +108 -0
  186. package/server/modules/database/{repositories → tests}/sessions.db.integration.test.ts +12 -0
  187. package/server/modules/notifications/index.ts +13 -0
  188. package/server/modules/notifications/notifications.routes.ts +127 -0
  189. package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -0
  190. package/server/modules/notifications/services/notification-orchestrator.service.js +288 -0
  191. package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -0
  192. package/server/modules/projects/services/project-management.service.ts +0 -8
  193. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +12 -53
  194. package/server/modules/providers/README.md +1 -1
  195. package/server/modules/providers/index.ts +1 -0
  196. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +26 -1
  197. package/server/modules/providers/list/claude/claude-sessions.provider.ts +9 -14
  198. package/server/modules/providers/list/claude/claude-skills.provider.ts +8 -0
  199. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +7 -3
  200. package/server/modules/providers/list/codex/codex-sessions.provider.ts +3 -12
  201. package/server/modules/providers/list/codex/codex-skills.provider.ts +8 -0
  202. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +13 -22
  203. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +8 -0
  204. package/server/modules/providers/list/gemini/gemini-models.provider.ts +5 -8
  205. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +6 -6
  206. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +8 -0
  207. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +18 -4
  208. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +9 -12
  209. package/server/modules/providers/provider.routes.ts +167 -1
  210. package/server/modules/providers/services/mcp.service.ts +26 -0
  211. package/server/modules/providers/services/provider-capabilities.service.ts +91 -0
  212. package/server/modules/providers/services/provider-models.service.ts +1 -1
  213. package/server/modules/providers/services/sessions-watcher.service.ts +71 -25
  214. package/server/modules/providers/services/sessions.service.ts +79 -4
  215. package/server/modules/providers/services/skills.service.ts +25 -1
  216. package/server/modules/providers/shared/skills/skills.provider.ts +223 -0
  217. package/server/modules/providers/tests/opencode-sessions.test.ts +49 -0
  218. package/server/modules/providers/tests/skills.test.ts +212 -0
  219. package/server/modules/websocket/README.md +33 -27
  220. package/server/modules/websocket/index.ts +1 -0
  221. package/server/modules/websocket/services/chat-run-registry.service.ts +327 -0
  222. package/server/modules/websocket/services/chat-session-writer.service.ts +145 -0
  223. package/server/modules/websocket/services/chat-websocket.service.ts +292 -207
  224. package/server/modules/websocket/services/shell-websocket.service.ts +108 -32
  225. package/server/modules/websocket/services/websocket-server.service.ts +6 -0
  226. package/server/modules/websocket/tests/chat-run-registry.test.ts +244 -0
  227. package/server/openai-codex.js +24 -14
  228. package/server/opencode-cli.js +21 -8
  229. package/server/opencode-cli.test.js +20 -1
  230. package/server/routes/settings.js +6 -1
  231. package/server/services/notification-orchestrator.js +3 -224
  232. package/server/services/tests/notification-orchestrator.test.js +80 -0
  233. package/server/shared/interfaces.ts +15 -0
  234. package/server/shared/tests/slice-tail-page.test.ts +42 -0
  235. package/server/shared/types.ts +84 -1
  236. package/server/shared/utils.ts +95 -1
  237. package/server/voice-proxy.js +224 -0
  238. package/dist/assets/index-73Xw3i9v.css +0 -32
  239. package/dist/assets/index-BIHaviaA.js +0 -1361
  240. package/dist/modelConstants.js +0 -854
  241. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +0 -1
  242. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +0 -1
  243. package/dist-server/server/shared/claude-cli-path.test.js.map +0 -1
  244. package/public/modelConstants.js +0 -854
  245. /package/dist-server/server/modules/database/{repositories → tests}/projects.db.integration.test.js +0 -0
  246. /package/server/modules/database/{repositories → tests}/projects.db.integration.test.ts +0 -0
  247. /package/server/shared/{claude-cli-path.test.ts → tests/claude-cli-path.test.ts} +0 -0
@@ -28,10 +28,14 @@ import {
28
28
  } from './services/notification-orchestrator.js';
29
29
  import { sessionsService } from './modules/providers/services/sessions.service.js';
30
30
  import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
31
- import { createNormalizedMessage } from './shared/utils.js';
31
+ import { createCompleteMessage, createNormalizedMessage } from './shared/utils.js';
32
32
 
33
33
  const activeSessions = new Map();
34
34
  const pendingToolApprovals = new Map();
35
+ // Sessions cancelled via abort-session. The abort handler already sent the
36
+ // terminal `complete` (aborted: true) to the client, so the run loop must not
37
+ // emit a second one when its generator winds down.
38
+ const abortedSessionIds = new Set();
35
39
 
36
40
  const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
37
41
 
@@ -731,14 +735,18 @@ async function queryClaudeSDK(command, options = {}, ws) {
731
735
  // Clean up temporary image files
732
736
  await cleanupTempFiles(tempImagePaths, tempDir);
733
737
 
734
- // Send completion event
735
- ws.send(createNormalizedMessage({ kind: 'complete', exitCode: 0, isNewSession: !sessionId && !!command, sessionId: capturedSessionId, provider: 'claude' }));
738
+ // Send the terminal completion event — skipped for aborted runs, whose
739
+ // terminal `complete` (aborted: true) was already sent by abort-session.
740
+ const wasAborted = capturedSessionId ? abortedSessionIds.delete(capturedSessionId) : false;
741
+ if (!wasAborted) {
742
+ ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 0 }));
743
+ }
736
744
  notifyRunStopped({
737
745
  userId: ws?.userId || null,
738
746
  provider: 'claude',
739
747
  sessionId: capturedSessionId || sessionId || null,
740
748
  sessionName: sessionSummary,
741
- stopReason: 'completed'
749
+ stopReason: wasAborted ? 'aborted' : 'completed'
742
750
  });
743
751
  // Complete
744
752
 
@@ -753,14 +761,22 @@ async function queryClaudeSDK(command, options = {}, ws) {
753
761
  // Clean up temporary image files on error
754
762
  await cleanupTempFiles(tempImagePaths, tempDir);
755
763
 
764
+ const wasAborted = capturedSessionId ? abortedSessionIds.delete(capturedSessionId) : false;
765
+ if (wasAborted) {
766
+ // The abort already produced the terminal complete; a generator throw
767
+ // caused by interrupt() is expected noise, not a user-facing error.
768
+ return;
769
+ }
770
+
756
771
  // Check if Claude CLI is installed for a clearer error message
757
772
  const installed = await providerAuthService.isProviderInstalled('claude');
758
773
  const errorContent = !installed
759
774
  ? 'Claude Code is not installed. Please install it first: https://docs.anthropic.com/en/docs/claude-code'
760
775
  : error.message;
761
776
 
762
- // Send error to WebSocket
777
+ // Send error to WebSocket, then the terminal complete
763
778
  ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
779
+ ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 1 }));
764
780
  notifyRunFailed({
765
781
  userId: ws?.userId || null,
766
782
  provider: 'claude',
@@ -787,6 +803,10 @@ async function abortClaudeSDKSession(sessionId) {
787
803
  try {
788
804
  console.log(`Aborting SDK session: ${sessionId}`);
789
805
 
806
+ // Mark before interrupting so the run loop knows not to emit its own
807
+ // terminal complete (the abort handler sends the aborted one).
808
+ abortedSessionIds.add(sessionId);
809
+
790
810
  // Call interrupt() on the query instance
791
811
  await session.instance.interrupt();
792
812
 
@@ -802,6 +822,8 @@ async function abortClaudeSDKSession(sessionId) {
802
822
  return true;
803
823
  } catch (error) {
804
824
  console.error(`Error aborting session ${sessionId}:`, error);
825
+ // The run keeps going; let it emit its own terminal complete.
826
+ abortedSessionIds.delete(sessionId);
805
827
  return false;
806
828
  }
807
829
  }
package/server/cli.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * (no args) - Start the server (default)
9
9
  * start - Start the server
10
10
  * sandbox - Manage Docker sandbox environments
11
+ * browser-use-mcp - Run Browser MCP stdio server
11
12
  * status - Show configuration and data locations
12
13
  * help - Show help information
13
14
  * version - Show version information
@@ -154,12 +155,13 @@ Usage:
154
155
  cloudcli [command] [options]
155
156
 
156
157
  Commands:
157
- start Start the CloudCLI server (default)
158
- sandbox Manage Docker sandbox environments
159
- status Show configuration and data locations
160
- update Update to the latest version
161
- help Show this help information
162
- version Show version information
158
+ start Start the CloudCLI server (default)
159
+ sandbox Manage Docker sandbox environments
160
+ browser-use-mcp Run the Browser MCP stdio server
161
+ status Show configuration and data locations
162
+ update Update to the latest version
163
+ help Show this help information
164
+ version Show version information
163
165
 
164
166
  Options:
165
167
  -p, --port <port> Set server port (default: 3001)
@@ -605,6 +607,10 @@ async function startServer() {
605
607
  await import('./index.js');
606
608
  }
607
609
 
610
+ async function startBrowserUseMcp() {
611
+ await import('./browser-use-mcp.js');
612
+ }
613
+
608
614
  // Parse CLI arguments
609
615
  function parseArgs(args) {
610
616
  const parsed = { command: 'start', options: {} };
@@ -658,6 +664,9 @@ async function main() {
658
664
  case 'sandbox':
659
665
  await sandboxCommand(remainingArgs || []);
660
666
  break;
667
+ case 'browser-use-mcp':
668
+ await startBrowserUseMcp();
669
+ break;
661
670
  case 'status':
662
671
  case 'info':
663
672
  showStatus();
@@ -4,7 +4,7 @@ import { notifyRunFailed, notifyRunStopped } from './services/notification-orche
4
4
  import { sessionsService } from './modules/providers/services/sessions.service.js';
5
5
  import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
6
6
  import { providerModelsService } from './modules/providers/services/provider-models.service.js';
7
- import { createNormalizedMessage } from './shared/utils.js';
7
+ import { createCompleteMessage, createNormalizedMessage } from './shared/utils.js';
8
8
 
9
9
  // Use cross-spawn on Windows for better command execution
10
10
  const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
@@ -34,6 +34,10 @@ async function spawnCursor(command, options = {}, ws) {
34
34
  let sessionCreatedSent = false; // Track if we've already sent session-created event
35
35
  let hasRetriedWithTrust = false;
36
36
  let settled = false;
37
+ // The unified lifecycle contract requires exactly one terminal `complete`
38
+ // per run. Cursor surfaces completion twice (the `result` JSON line and
39
+ // the process close), so the first emission wins.
40
+ let completeSent = false;
37
41
 
38
42
  // Use tools settings passed from frontend, or defaults
39
43
  const settings = toolsSettings || {
@@ -197,15 +201,15 @@ async function spawnCursor(command, options = {}, ws) {
197
201
  break;
198
202
 
199
203
  case 'result': {
200
- // Session complete — send stream end + lifecycle complete with result payload
201
- const resultText = typeof response.result === 'string' ? response.result : '';
202
- ws.send(createNormalizedMessage({
203
- kind: 'complete',
204
- exitCode: response.subtype === 'success' ? 0 : 1,
205
- resultText,
206
- isError: response.subtype !== 'success',
207
- sessionId: capturedSessionId || sessionId, provider: 'cursor',
208
- }));
204
+ // Session complete — terminal lifecycle event for this run
205
+ if (!completeSent) {
206
+ completeSent = true;
207
+ ws.send(createCompleteMessage({
208
+ provider: 'cursor',
209
+ sessionId: capturedSessionId || sessionId || null,
210
+ exitCode: response.subtype === 'success' ? 0 : 1,
211
+ }));
212
+ }
209
213
  break;
210
214
  }
211
215
 
@@ -271,7 +275,12 @@ async function spawnCursor(command, options = {}, ws) {
271
275
  return;
272
276
  }
273
277
 
274
- ws.send(createNormalizedMessage({ kind: 'complete', exitCode: code, isNewSession: !sessionId && !!command, sessionId: finalSessionId, provider: 'cursor' }));
278
+ // Terminal complete unless the `result` line already sent it, or the
279
+ // run was aborted (abort-session sent the aborted complete).
280
+ if (!completeSent && !cursorProcess.aborted) {
281
+ completeSent = true;
282
+ ws.send(createCompleteMessage({ provider: 'cursor', sessionId: finalSessionId, exitCode: code }));
283
+ }
275
284
 
276
285
  if (code === 0) {
277
286
  notifyTerminalState({ code });
@@ -297,6 +306,10 @@ async function spawnCursor(command, options = {}, ws) {
297
306
  : error.message;
298
307
 
299
308
  ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'cursor' }));
309
+ if (!completeSent && !cursorProcess.aborted) {
310
+ completeSent = true;
311
+ ws.send(createCompleteMessage({ provider: 'cursor', sessionId: capturedSessionId || sessionId || null, exitCode: 1 }));
312
+ }
300
313
  notifyTerminalState({ error });
301
314
 
302
315
  settleOnce(() => reject(error));
@@ -314,6 +327,9 @@ function abortCursorSession(sessionId) {
314
327
  const process = activeCursorProcesses.get(sessionId);
315
328
  if (process) {
316
329
  console.log(`Aborting Cursor session: ${sessionId}`);
330
+ // The abort handler sends the terminal complete (aborted: true); flag the
331
+ // process so its close handler does not emit a second one.
332
+ process.aborted = true;
317
333
  process.kill('SIGTERM');
318
334
  activeCursorProcesses.delete(sessionId);
319
335
  return true;
@@ -10,7 +10,7 @@ import GeminiResponseHandler from './gemini-response-handler.js';
10
10
  import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
11
11
  import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
12
12
  import { providerModelsService } from './modules/providers/services/provider-models.service.js';
13
- import { createNormalizedMessage } from './shared/utils.js';
13
+ import { createCompleteMessage, createNormalizedMessage } from './shared/utils.js';
14
14
 
15
15
  // Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
16
16
  const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
@@ -129,6 +129,9 @@ async function spawnGemini(command, options = {}, ws) {
129
129
  let capturedSessionId = sessionId; // Track session ID throughout the process
130
130
  let sessionCreatedSent = false; // Track if we've already sent session-created event
131
131
  let assistantBlocks = []; // Accumulate the full response blocks including tools
132
+ // Unified lifecycle contract: exactly one terminal `complete` per run
133
+ // (close and error handlers can both fire for spawn failures).
134
+ let completeSent = false;
132
135
 
133
136
  // Use tools settings passed from frontend, or defaults
134
137
  const settings = toolsSettings || {
@@ -486,7 +489,12 @@ async function spawnGemini(command, options = {}, ws) {
486
489
  sessionManager.addMessage(finalSessionId, 'assistant', assistantBlocks);
487
490
  }
488
491
 
489
- ws.send(createNormalizedMessage({ kind: 'complete', exitCode: code, isNewSession: !sessionId && !!command, sessionId: finalSessionId, provider: 'gemini' }));
492
+ // Terminal complete skipped for aborted runs (abort-session
493
+ // already sent the aborted complete on this run's behalf).
494
+ if (!completeSent && !geminiProcess.aborted) {
495
+ completeSent = true;
496
+ ws.send(createCompleteMessage({ provider: 'gemini', sessionId: finalSessionId, exitCode: code }));
497
+ }
490
498
 
491
499
  // Clean up temporary image files if any
492
500
  if (geminiProcess.tempImagePaths && geminiProcess.tempImagePaths.length > 0) {
@@ -566,6 +574,10 @@ async function spawnGemini(command, options = {}, ws) {
566
574
 
567
575
  const errorSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
568
576
  ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: errorSessionId, provider: 'gemini' }));
577
+ if (!completeSent && !geminiProcess.aborted) {
578
+ completeSent = true;
579
+ ws.send(createCompleteMessage({ provider: 'gemini', sessionId: errorSessionId, exitCode: 1 }));
580
+ }
569
581
  notifyTerminalState({ error });
570
582
 
571
583
  reject(error);
@@ -590,6 +602,9 @@ function abortGeminiSession(sessionId) {
590
602
 
591
603
  if (geminiProc) {
592
604
  try {
605
+ // The abort handler sends the terminal complete (aborted: true);
606
+ // flag the process so its close handler does not emit a second one.
607
+ geminiProc.aborted = true;
593
608
  geminiProc.kill('SIGTERM');
594
609
  setTimeout(() => {
595
610
  if (activeGeminiProcesses.has(processKey)) {
package/server/index.js CHANGED
@@ -22,35 +22,24 @@ import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
22
22
  import {
23
23
  queryClaudeSDK,
24
24
  abortClaudeSDKSession,
25
- isClaudeSDKSessionActive,
26
- getActiveClaudeSDKSessions,
27
25
  resolveToolApproval,
28
26
  getPendingApprovalsForSession,
29
- reconnectSessionWriter,
30
27
  } from './claude-sdk.js';
31
28
  import {
32
29
  spawnCursor,
33
30
  abortCursorSession,
34
- isCursorSessionActive,
35
- getActiveCursorSessions,
36
31
  } from './cursor-cli.js';
37
32
  import {
38
33
  queryCodex,
39
34
  abortCodexSession,
40
- isCodexSessionActive,
41
- getActiveCodexSessions,
42
35
  } from './openai-codex.js';
43
36
  import {
44
37
  spawnGemini,
45
38
  abortGeminiSession,
46
- isGeminiSessionActive,
47
- getActiveGeminiSessions,
48
39
  } from './gemini-cli.js';
49
40
  import {
50
41
  spawnOpenCode,
51
42
  abortOpenCodeSession,
52
- isOpenCodeSessionActive,
53
- getActiveOpenCodeSessions,
54
43
  } from './opencode-cli.js';
55
44
  import sessionManager from './sessionManager.js';
56
45
  import {
@@ -68,10 +57,15 @@ import commandsRoutes from './routes/commands.js';
68
57
  import settingsRoutes from './routes/settings.js';
69
58
  import agentRoutes from './routes/agent.js';
70
59
  import projectModuleRoutes from './modules/projects/projects.routes.js';
60
+ import notificationRoutes from './modules/notifications/notifications.routes.js';
71
61
  import userRoutes from './routes/user.js';
72
62
  import geminiRoutes from './routes/gemini.js';
73
63
  import pluginsRoutes from './routes/plugins.js';
74
64
  import providerRoutes from './modules/providers/provider.routes.js';
65
+ import voiceRoutes from './voice-proxy.js';
66
+ import browserUseRoutes from './modules/browser-use/browser-use.routes.js';
67
+ import browserUseMcpRoutes from './modules/browser-use/browser-use-mcp.routes.js';
68
+ import { browserUseService } from './modules/browser-use/browser-use.service.js';
75
69
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
76
70
  import { initializeDatabase, projectsDb, sessionsDb } from './modules/database/index.js';
77
71
  import { configureWebPush } from './services/vapid-keys.js';
@@ -84,6 +78,19 @@ const __dirname = getModuleDir(import.meta.url);
84
78
  // Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
85
79
  const APP_ROOT = findAppRoot(__dirname);
86
80
  const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
81
+ // Version of the code that is actually running, captured once at process
82
+ // startup. This intentionally does NOT re-read package.json per request: after
83
+ // an update replaces the files on disk, package.json reflects the NEW version
84
+ // while this long-lived process still runs the OLD code. The frontend bundle is
85
+ // rebuilt on update, so a mismatch between this value and the frontend's
86
+ // build-time version means the server was updated but not restarted.
87
+ const RUNNING_VERSION = (() => {
88
+ try {
89
+ return JSON.parse(fs.readFileSync(path.join(APP_ROOT, 'package.json'), 'utf8')).version || null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ })();
87
94
  const MAX_FILE_UPLOAD_SIZE_MB = 200;
88
95
  const MAX_FILE_UPLOAD_SIZE_BYTES = MAX_FILE_UPLOAD_SIZE_MB * 1024 * 1024;
89
96
  const MAX_FILE_UPLOAD_COUNT = 20;
@@ -105,32 +112,35 @@ const wss = createWebSocketServer(server, {
105
112
  authenticateWebSocket,
106
113
  },
107
114
  chat: {
108
- queryClaudeSDK,
109
- spawnCursor,
110
- queryCodex,
111
- spawnGemini,
112
- spawnOpenCode,
113
- abortClaudeSDKSession,
114
- abortCursorSession,
115
- abortCodexSession,
116
- abortGeminiSession,
117
- abortOpenCodeSession,
115
+ spawnFns: {
116
+ claude: queryClaudeSDK,
117
+ cursor: spawnCursor,
118
+ codex: queryCodex,
119
+ gemini: spawnGemini,
120
+ opencode: spawnOpenCode,
121
+ },
122
+ abortFns: {
123
+ claude: abortClaudeSDKSession,
124
+ cursor: abortCursorSession,
125
+ codex: abortCodexSession,
126
+ gemini: abortGeminiSession,
127
+ opencode: abortOpenCodeSession,
128
+ },
118
129
  resolveToolApproval,
119
- isClaudeSDKSessionActive,
120
- isCursorSessionActive,
121
- isCodexSessionActive,
122
- isGeminiSessionActive,
123
- isOpenCodeSessionActive,
124
- reconnectSessionWriter,
125
130
  getPendingApprovalsForSession,
126
- getActiveClaudeSDKSessions,
127
- getActiveCursorSessions,
128
- getActiveCodexSessions,
129
- getActiveGeminiSessions,
130
- getActiveOpenCodeSessions,
131
131
  },
132
132
  shell: {
133
- getSessionById: (sessionId) => sessionManager.getSession(sessionId),
133
+ resolveProviderSessionId: (sessionId, provider) => {
134
+ const dbSession = sessionsDb.getSessionById(sessionId);
135
+ const legacyGeminiSession =
136
+ provider === 'gemini' ? sessionManager.getSession(sessionId) : null;
137
+
138
+ if (dbSession) {
139
+ return dbSession.provider_session_id ?? legacyGeminiSession?.cliSessionId ?? null;
140
+ }
141
+
142
+ return legacyGeminiSession?.cliSessionId;
143
+ },
134
144
  stripAnsiSequences,
135
145
  normalizeDetectedUrl,
136
146
  extractUrlsFromText,
@@ -161,7 +171,8 @@ app.get('/health', (req, res) => {
161
171
  res.json({
162
172
  status: 'ok',
163
173
  timestamp: new Date().toISOString(),
164
- installMode
174
+ installMode,
175
+ version: RUNNING_VERSION
165
176
  });
166
177
  });
167
178
 
@@ -192,6 +203,8 @@ app.use('/api/commands', authenticateToken, commandsRoutes);
192
203
  // Settings API Routes (protected)
193
204
  app.use('/api/settings', authenticateToken, settingsRoutes);
194
205
 
206
+ app.use('/api/notifications', authenticateToken, notificationRoutes);
207
+
195
208
  // User API Routes (protected)
196
209
  app.use('/api/user', authenticateToken, userRoutes);
197
210
 
@@ -201,12 +214,20 @@ app.use('/api/gemini', authenticateToken, geminiRoutes);
201
214
  // Plugins API Routes (protected)
202
215
  app.use('/api/plugins', authenticateToken, pluginsRoutes);
203
216
 
217
+ // Browser MCP bridge API (local token protected)
218
+ app.use('/api/browser-use-mcp', browserUseMcpRoutes);
219
+
220
+ // Browser API Routes (protected)
221
+ app.use('/api/browser-use', authenticateToken, browserUseRoutes);
222
+
204
223
  // Unified provider MCP routes (protected)
205
224
  app.use('/api/providers', authenticateToken, providerRoutes);
206
225
 
207
226
  // Agent API Routes (uses API key authentication)
208
227
  app.use('/api/agent', agentRoutes);
209
228
 
229
+ app.use('/api/voice', authenticateToken, voiceRoutes);
230
+
210
231
  // Serve public files (like api-docs.html)
211
232
  app.use(express.static(path.join(APP_ROOT, 'public')));
212
233
 
@@ -1143,7 +1164,6 @@ app.post('/api/projects/:projectId/upload-images', authenticateToken, async (req
1143
1164
  app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
1144
1165
  try {
1145
1166
  const { projectId, sessionId } = req.params;
1146
- const { provider = 'claude' } = req.query;
1147
1167
  const homeDir = os.homedir();
1148
1168
 
1149
1169
  // Allow only safe characters in sessionId
@@ -1152,6 +1172,18 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1152
1172
  return res.status(400).json({ error: 'Invalid sessionId' });
1153
1173
  }
1154
1174
 
1175
+ // Provider artifacts on disk (JSONL file names, OpenCode sqlite rows)
1176
+ // are keyed by the provider-native session id, while the caller sends
1177
+ // the app-facing id. Resolve provider and id mapping from the indexed
1178
+ // session row so the frontend does not choose provider-specific paths.
1179
+ const sessionRow = sessionsDb.getSessionById(safeSessionId);
1180
+ if (!sessionRow) {
1181
+ return res.status(404).json({ error: 'Session not found', sessionId: safeSessionId });
1182
+ }
1183
+
1184
+ const provider = sessionRow.provider || 'claude';
1185
+ const providerNativeSessionId = sessionRow?.provider_session_id || safeSessionId;
1186
+
1155
1187
  // Handle Cursor sessions - they use SQLite and don't have token usage info
1156
1188
  if (provider === 'cursor') {
1157
1189
  return res.json({
@@ -1252,7 +1284,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1252
1284
  tokens_cache_write AS cacheWriteTokens
1253
1285
  FROM session
1254
1286
  WHERE id = ?
1255
- `).get(safeSessionId);
1287
+ `).get(providerNativeSessionId);
1256
1288
 
1257
1289
  if (!row) {
1258
1290
  return res.status(404).json({ error: 'OpenCode session not found', sessionId: safeSessionId });
@@ -1293,7 +1325,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1293
1325
  if (entry.isDirectory()) {
1294
1326
  const found = await findSessionFile(fullPath);
1295
1327
  if (found) return found;
1296
- } else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
1328
+ } else if (entry.name.includes(providerNativeSessionId) && entry.name.endsWith('.jsonl')) {
1297
1329
  return fullPath;
1298
1330
  }
1299
1331
  }
@@ -1377,12 +1409,19 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1377
1409
  const encodedPath = projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
1378
1410
  const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
1379
1411
 
1380
- const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
1381
-
1382
- // Constrain to projectDir
1383
- const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
1384
- if (rel.startsWith('..') || path.isAbsolute(rel)) {
1385
- return res.status(400).json({ error: 'Invalid path' });
1412
+ // Prefer the indexed transcript path (already produced by the trusted
1413
+ // session synchronizer); fall back to the conventional location
1414
+ // derived from the provider-native session id.
1415
+ let jsonlPath = sessionRow?.jsonl_path;
1416
+ if (!jsonlPath) {
1417
+ jsonlPath = path.join(projectDir, `${providerNativeSessionId}.jsonl`);
1418
+
1419
+ // Constrain the constructed path to projectDir (the id is
1420
+ // caller-influenced in this fallback branch).
1421
+ const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
1422
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
1423
+ return res.status(400).json({ error: 'Invalid path' });
1424
+ }
1386
1425
  }
1387
1426
 
1388
1427
  // Read and parse the JSONL file
@@ -1646,6 +1685,40 @@ const SERVER_PORT = process.env.SERVER_PORT || 3001;
1646
1685
  const HOST = process.env.HOST || '0.0.0.0';
1647
1686
  const DISPLAY_HOST = getConnectableHost(HOST);
1648
1687
  const VITE_PORT = process.env.VITE_PORT || 5173;
1688
+ const LOCAL_SERVER_MARKER_PATH = path.join(os.homedir(), '.cloudcli', 'local-server.json');
1689
+
1690
+ async function writeLocalServerMarker() {
1691
+ const marker = {
1692
+ pid: process.pid,
1693
+ host: HOST,
1694
+ port: Number.parseInt(String(SERVER_PORT), 10),
1695
+ url: `http://${DISPLAY_HOST}:${SERVER_PORT}`,
1696
+ installMode,
1697
+ appRoot: APP_ROOT,
1698
+ updatedAt: new Date().toISOString(),
1699
+ };
1700
+
1701
+ await fsPromises.mkdir(path.dirname(LOCAL_SERVER_MARKER_PATH), { recursive: true });
1702
+ await fsPromises.writeFile(LOCAL_SERVER_MARKER_PATH, JSON.stringify(marker, null, 2), 'utf8');
1703
+ }
1704
+
1705
+ async function removeLocalServerMarker() {
1706
+ try {
1707
+ const raw = await fsPromises.readFile(LOCAL_SERVER_MARKER_PATH, 'utf8');
1708
+ const marker = JSON.parse(raw);
1709
+ if (marker.pid && marker.pid !== process.pid) return;
1710
+ } catch (error) {
1711
+ if (error.code === 'ENOENT') return;
1712
+ }
1713
+
1714
+ try {
1715
+ await fsPromises.unlink(LOCAL_SERVER_MARKER_PATH);
1716
+ } catch (error) {
1717
+ if (error.code !== 'ENOENT') {
1718
+ console.warn('[WARN] Could not remove local server marker:', error.message);
1719
+ }
1720
+ }
1721
+ }
1649
1722
 
1650
1723
  // Initialize database and start server
1651
1724
  async function startServer() {
@@ -1672,6 +1745,9 @@ async function startServer() {
1672
1745
 
1673
1746
  server.listen(SERVER_PORT, HOST, async () => {
1674
1747
  const appInstallPath = APP_ROOT;
1748
+ await writeLocalServerMarker().catch((error) => {
1749
+ console.warn('[WARN] Could not write local server marker:', error.message);
1750
+ });
1675
1751
 
1676
1752
  console.log('');
1677
1753
  console.log(c.dim('═'.repeat(63)));
@@ -1694,12 +1770,26 @@ async function startServer() {
1694
1770
 
1695
1771
  await closeSessionsWatcher();
1696
1772
  // Clean up plugin processes on shutdown
1697
- const shutdownPlugins = async () => {
1698
- await stopAllPlugins();
1773
+ const shutdownRuntimeServices = async () => {
1774
+ try {
1775
+ await browserUseService.stopAllSessions();
1776
+ } catch (err) {
1777
+ console.error('[Browser] Error stopping sessions during shutdown:', err?.message || err);
1778
+ }
1779
+ try {
1780
+ await stopAllPlugins();
1781
+ } catch (err) {
1782
+ console.error('[Plugins] Error stopping plugins during shutdown:', err?.message || err);
1783
+ }
1784
+ try {
1785
+ await removeLocalServerMarker();
1786
+ } catch (err) {
1787
+ console.error('[Local Server] Error removing server marker during shutdown:', err?.message || err);
1788
+ }
1699
1789
  process.exit(0);
1700
1790
  };
1701
- process.on('SIGTERM', () => void shutdownPlugins());
1702
- process.on('SIGINT', () => void shutdownPlugins());
1791
+ process.on('SIGTERM', () => void shutdownRuntimeServices());
1792
+ process.on('SIGINT', () => void shutdownRuntimeServices());
1703
1793
  } catch (error) {
1704
1794
  console.error('[ERROR] Failed to start server:', error);
1705
1795
  process.exit(1);
@@ -22,7 +22,7 @@ try {
22
22
  }
23
23
  });
24
24
  } catch (e) {
25
- console.log('No .env file found or error reading it:', e.message);
25
+ console.error('No .env file found or error reading it:', e.message);
26
26
  }
27
27
 
28
28
  // Keep the default database in a stable user-level location so rebuilding dist-server