@pixelbyte-software/pixcode 1.51.2 → 1.51.4

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 (331) hide show
  1. package/CODE_OF_CONDUCT.md +41 -41
  2. package/CONTRIBUTING.md +155 -155
  3. package/LICENSE +718 -718
  4. package/README.de.md +169 -169
  5. package/README.ja.md +167 -167
  6. package/README.ko.md +167 -167
  7. package/README.md +419 -419
  8. package/README.ru.md +169 -169
  9. package/README.tr.md +298 -298
  10. package/README.zh-CN.md +167 -167
  11. package/SECURITY.md +46 -46
  12. package/dist/api-automation.html +110 -110
  13. package/dist/api-docs.html +548 -548
  14. package/dist/assets/index-B9N-gfOQ.css +32 -0
  15. package/dist/assets/{index-EN9ngyxf.js → index-HfGHXhD6.js} +175 -175
  16. package/dist/clear-cache.html +85 -85
  17. package/dist/convert-icons.md +52 -52
  18. package/dist/docs.html +308 -308
  19. package/dist/favicon.svg +8 -8
  20. package/dist/features.html +133 -133
  21. package/dist/generate-icons.js +48 -48
  22. package/dist/humans.txt +15 -15
  23. package/dist/icons/codex-white.svg +3 -3
  24. package/dist/icons/codex.svg +3 -3
  25. package/dist/icons/cursor-white.svg +11 -11
  26. package/dist/icons/icon-128x128.svg +9 -9
  27. package/dist/icons/icon-144x144.svg +9 -9
  28. package/dist/icons/icon-152x152.svg +9 -9
  29. package/dist/icons/icon-192x192.svg +9 -9
  30. package/dist/icons/icon-384x384.svg +9 -9
  31. package/dist/icons/icon-512x512.svg +9 -9
  32. package/dist/icons/icon-72x72.svg +9 -9
  33. package/dist/icons/icon-96x96.svg +9 -9
  34. package/dist/icons/icon-template.svg +9 -9
  35. package/dist/icons/qwen-logo.svg +14 -14
  36. package/dist/index.html +59 -59
  37. package/dist/landing.html +268 -268
  38. package/dist/llms-full.txt +119 -119
  39. package/dist/llms.txt +53 -53
  40. package/dist/logo.svg +12 -12
  41. package/dist/manifest.json +60 -60
  42. package/dist/openapi.yaml +1696 -1696
  43. package/dist/orchestration.html +125 -125
  44. package/dist/robots.txt +4 -4
  45. package/dist/site.css +692 -692
  46. package/dist/sitemap.xml +51 -51
  47. package/dist/sw.js +132 -132
  48. package/dist-server/server/cli.js +96 -96
  49. package/dist-server/server/daemon/manager.js +33 -33
  50. package/dist-server/server/daemon-manager.js +64 -64
  51. package/dist-server/server/database/db.js +14 -2
  52. package/dist-server/server/database/db.js.map +1 -1
  53. package/dist-server/server/index.js +191 -31
  54. package/dist-server/server/index.js.map +1 -1
  55. package/dist-server/server/middleware/auth.js +16 -5
  56. package/dist-server/server/middleware/auth.js.map +1 -1
  57. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.js +84 -0
  58. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.js.map +1 -0
  59. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.test.js +43 -0
  60. package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.test.js.map +1 -0
  61. package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +55 -1
  62. package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
  63. package/dist-server/server/modules/orchestration/index.js +1 -0
  64. package/dist-server/server/modules/orchestration/index.js.map +1 -1
  65. package/dist-server/server/routes/auth.js +12 -5
  66. package/dist-server/server/routes/auth.js.map +1 -1
  67. package/dist-server/server/routes/commands.js +25 -25
  68. package/dist-server/server/routes/git.js +29 -17
  69. package/dist-server/server/routes/git.js.map +1 -1
  70. package/dist-server/server/routes/live-view.js +46 -46
  71. package/dist-server/server/routes/platformization.js +7 -6
  72. package/dist-server/server/routes/platformization.js.map +1 -1
  73. package/dist-server/server/services/hermes-gateway.js +310 -0
  74. package/dist-server/server/services/hermes-gateway.js.map +1 -1
  75. package/dist-server/server/services/platformization.js +58 -2
  76. package/dist-server/server/services/platformization.js.map +1 -1
  77. package/dist-server/server/services/public-api-manifest.js +59 -51
  78. package/dist-server/server/services/public-api-manifest.js.map +1 -1
  79. package/package.json +222 -222
  80. package/scripts/fix-node-pty.js +67 -67
  81. package/scripts/github/create-v1.38-issues.mjs +351 -351
  82. package/scripts/github/create-vscode-workbench-issues.mjs +121 -121
  83. package/scripts/hermes/configure-pixcode-mcp.mjs +165 -163
  84. package/scripts/hermes/pixcode-mcp-server.mjs +1009 -958
  85. package/scripts/smoke/changes-panel-layout.mjs +48 -48
  86. package/scripts/smoke/chat-composer-fixed-layout.mjs +55 -55
  87. package/scripts/smoke/chat-message-timeline-order.mjs +41 -41
  88. package/scripts/smoke/chat-realtime-hydration.mjs +44 -44
  89. package/scripts/smoke/chat-session-provider-pools.mjs +35 -35
  90. package/scripts/smoke/chat-session-state.mjs +19 -19
  91. package/scripts/smoke/code-editor-theme.mjs +55 -55
  92. package/scripts/smoke/code-editor-vscode-engine.mjs +91 -91
  93. package/scripts/smoke/command-center-agent-writes.mjs +79 -79
  94. package/scripts/smoke/command-center-non-git.mjs +46 -46
  95. package/scripts/smoke/context-packet.mjs +43 -43
  96. package/scripts/smoke/control-room-ux-redesign.mjs +91 -91
  97. package/scripts/smoke/daemon-entrypoint.mjs +20 -20
  98. package/scripts/smoke/default-landing-routing.mjs +33 -33
  99. package/scripts/smoke/desktop-native-notifications.mjs +30 -30
  100. package/scripts/smoke/desktop-tray-icon.mjs +33 -33
  101. package/scripts/smoke/discord-release-workflow.mjs +24 -24
  102. package/scripts/smoke/git-install-update.mjs +255 -255
  103. package/scripts/smoke/handoff-artifact-protocol.mjs +50 -50
  104. package/scripts/smoke/hermes-api-install.mjs +56 -56
  105. package/scripts/smoke/hermes-gateway-persistence.mjs +104 -104
  106. package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +426 -367
  107. package/scripts/smoke/hermes-rest-chat-api.mjs +162 -162
  108. package/scripts/smoke/hermes-rest-chat-live.mjs +45 -45
  109. package/scripts/smoke/hermes-rest-codex-launch.mjs +209 -209
  110. package/scripts/smoke/hermes-rest-gateway.mjs +79 -70
  111. package/scripts/smoke/hermes-rest-live.mjs +42 -42
  112. package/scripts/smoke/hermes-roundtrip.mjs +167 -167
  113. package/scripts/smoke/hermes-settings-commands.mjs +349 -346
  114. package/scripts/smoke/hermes-smoke-launcher-guard.mjs +34 -34
  115. package/scripts/smoke/live-view-diagnostics.mjs +53 -53
  116. package/scripts/smoke/live-view-environment.mjs +92 -92
  117. package/scripts/smoke/live-view-integration.mjs +450 -450
  118. package/scripts/smoke/mac-desktop-runtime.mjs +37 -37
  119. package/scripts/smoke/mobile-tunnel-guidance.mjs +29 -29
  120. package/scripts/smoke/model-registry.mjs +36 -36
  121. package/scripts/smoke/multi-project-ui.mjs +45 -45
  122. package/scripts/smoke/multi-worker-slots.mjs +42 -42
  123. package/scripts/smoke/notification-center.mjs +87 -87
  124. package/scripts/smoke/notification-inapp-preference.mjs +23 -23
  125. package/scripts/smoke/notification-taxonomy.mjs +58 -58
  126. package/scripts/smoke/orchestration-api.mjs +172 -172
  127. package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -33
  128. package/scripts/smoke/orchestration-live-run.mjs +176 -176
  129. package/scripts/smoke/orchestration-mobile-scroll.mjs +29 -29
  130. package/scripts/smoke/orchestration-model-sync.mjs +30 -30
  131. package/scripts/smoke/orchestration-permission-fallback.mjs +34 -34
  132. package/scripts/smoke/orchestration-runtime-guards.mjs +48 -48
  133. package/scripts/smoke/orchestration-user-facing-output.mjs +25 -25
  134. package/scripts/smoke/permission-policy.mjs +50 -50
  135. package/scripts/smoke/pixcode-workbench-1-48.mjs +167 -167
  136. package/scripts/smoke/provider-models-opencode-live.mjs +66 -66
  137. package/scripts/smoke/provider-rest-api.mjs +124 -124
  138. package/scripts/smoke/provider-selection-status.mjs +52 -52
  139. package/scripts/smoke/run-state-refresh.mjs +52 -52
  140. package/scripts/smoke/runtime-manager.mjs +99 -99
  141. package/scripts/smoke/shell-manual-disconnect.mjs +30 -30
  142. package/scripts/smoke/side-panel-editor-layout.mjs +34 -34
  143. package/scripts/smoke/static-root-routing.mjs +21 -21
  144. package/scripts/smoke/strict-handoff-compact.mjs +60 -60
  145. package/scripts/smoke/taskmaster-config.mjs +24 -24
  146. package/scripts/smoke/taskmaster-execution-telegram.mjs +3 -3
  147. package/scripts/smoke/taskmaster-onboarding.mjs +3 -3
  148. package/scripts/smoke/taskmaster-run-graph.mjs +3 -3
  149. package/scripts/smoke/telegram-control.mjs +242 -242
  150. package/scripts/smoke/tunnel-persistence.mjs +56 -56
  151. package/scripts/smoke/update-issue-progress.mjs +69 -69
  152. package/scripts/smoke/update-ux.mjs +55 -55
  153. package/scripts/smoke/v138-completion.mjs +132 -132
  154. package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -69
  155. package/scripts/smoke/v138-diagnostics.mjs +63 -63
  156. package/scripts/smoke/v138-issue-planner.mjs +33 -33
  157. package/scripts/smoke/v143-remote-control.mjs +76 -76
  158. package/scripts/smoke/v144-production-loop.mjs +47 -47
  159. package/scripts/smoke/v145-platformization.mjs +46 -46
  160. package/scripts/smoke/v146-control-room-ui.mjs +150 -150
  161. package/scripts/smoke/version-modal-autoshow.mjs +29 -29
  162. package/scripts/smoke/vscode-workbench-layout.mjs +63 -63
  163. package/scripts/smoke/vscode-workbench-polish.mjs +461 -436
  164. package/scripts/smoke/workflow-fallback-replay.mjs +56 -56
  165. package/scripts/smoke/workflow-templates.mjs +43 -43
  166. package/scripts/smoke/workflow-trace-timeline.mjs +46 -46
  167. package/scripts/update-git-install.mjs +293 -293
  168. package/server/claude-sdk.js +920 -920
  169. package/server/cli.js +1039 -1039
  170. package/server/constants/config.js +4 -4
  171. package/server/cursor-cli.js +344 -344
  172. package/server/daemon/manager.js +563 -563
  173. package/server/daemon-manager.js +964 -964
  174. package/server/database/db.js +908 -895
  175. package/server/database/json-store.js +197 -197
  176. package/server/gemini-cli.js +550 -550
  177. package/server/gemini-response-handler.js +79 -79
  178. package/server/index.js +201 -30
  179. package/server/load-env.js +35 -35
  180. package/server/middleware/auth.js +171 -156
  181. package/server/modules/orchestration/a2a/adapter-registry.ts +108 -108
  182. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +63 -63
  183. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +286 -286
  184. package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -244
  185. package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -249
  186. package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -248
  187. package/server/modules/orchestration/a2a/adapters/json-event.adapter.test.ts +60 -0
  188. package/server/modules/orchestration/a2a/adapters/json-event.adapter.ts +101 -0
  189. package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -248
  190. package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -248
  191. package/server/modules/orchestration/a2a/agent-card.ts +55 -55
  192. package/server/modules/orchestration/a2a/routes.ts +590 -590
  193. package/server/modules/orchestration/a2a/task-store.ts +178 -178
  194. package/server/modules/orchestration/a2a/types.ts +126 -126
  195. package/server/modules/orchestration/a2a/validator.ts +113 -113
  196. package/server/modules/orchestration/hermes/hermes.routes.ts +642 -583
  197. package/server/modules/orchestration/index.ts +101 -100
  198. package/server/modules/orchestration/preview/port-watcher.ts +112 -112
  199. package/server/modules/orchestration/preview/preview-proxy.ts +60 -60
  200. package/server/modules/orchestration/preview/types.ts +19 -19
  201. package/server/modules/orchestration/security/permission-policy.ts +401 -401
  202. package/server/modules/orchestration/tasks/orchestration-task-store.ts +41 -41
  203. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +64 -64
  204. package/server/modules/orchestration/tasks/orchestration-task.service.ts +209 -209
  205. package/server/modules/orchestration/tasks/orchestration-task.types.ts +40 -40
  206. package/server/modules/orchestration/tasks/task-run-graph.ts +155 -155
  207. package/server/modules/orchestration/workflows/approval-queue.ts +106 -106
  208. package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -127
  209. package/server/modules/orchestration/workflows/context-packet.ts +186 -186
  210. package/server/modules/orchestration/workflows/handoff-artifact.ts +175 -175
  211. package/server/modules/orchestration/workflows/workflow-fallback-policy.ts +161 -161
  212. package/server/modules/orchestration/workflows/workflow-replay.ts +254 -254
  213. package/server/modules/orchestration/workflows/workflow-runner.ts +2070 -2070
  214. package/server/modules/orchestration/workflows/workflow-store.ts +97 -97
  215. package/server/modules/orchestration/workflows/workflow-templates.ts +272 -272
  216. package/server/modules/orchestration/workflows/workflow-trace.ts +424 -424
  217. package/server/modules/orchestration/workflows/workflow.routes.ts +586 -586
  218. package/server/modules/orchestration/workflows/workflow.types.ts +111 -111
  219. package/server/modules/orchestration/workflows/workspace-target.ts +122 -122
  220. package/server/modules/orchestration/workspace/docker-workspace.ts +136 -136
  221. package/server/modules/orchestration/workspace/path-safety.ts +55 -55
  222. package/server/modules/orchestration/workspace/types.ts +52 -52
  223. package/server/modules/orchestration/workspace/workspace-manager.ts +102 -102
  224. package/server/modules/orchestration/workspace/worktree-workspace.ts +126 -126
  225. package/server/modules/providers/index.ts +2 -2
  226. package/server/modules/providers/list/claude/claude-auth.provider.ts +146 -146
  227. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  228. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
  229. package/server/modules/providers/list/claude/claude.provider.ts +15 -15
  230. package/server/modules/providers/list/codex/codex-auth.provider.ts +117 -117
  231. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  232. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
  233. package/server/modules/providers/list/codex/codex.provider.ts +15 -15
  234. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +147 -147
  235. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  236. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
  237. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
  238. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +173 -173
  239. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
  240. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
  241. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
  242. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +131 -131
  243. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
  244. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +286 -286
  245. package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
  246. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +146 -146
  247. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
  248. package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +265 -265
  249. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
  250. package/server/modules/providers/provider.registry.ts +40 -40
  251. package/server/modules/providers/provider.routes.ts +944 -944
  252. package/server/modules/providers/services/mcp.service.ts +86 -86
  253. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  254. package/server/modules/providers/services/sessions.service.ts +45 -45
  255. package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
  256. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  257. package/server/modules/providers/shared/provider-configs.ts +142 -142
  258. package/server/modules/providers/tests/mcp.test.ts +293 -293
  259. package/server/openai-codex.js +462 -462
  260. package/server/opencode-cli.js +491 -491
  261. package/server/opencode-response-handler.js +111 -111
  262. package/server/projects.js +3008 -3008
  263. package/server/qwen-code-cli.js +410 -410
  264. package/server/qwen-response-handler.js +73 -73
  265. package/server/routes/agent.js +1435 -1435
  266. package/server/routes/auth.js +154 -146
  267. package/server/routes/codex.js +20 -20
  268. package/server/routes/commands.js +570 -570
  269. package/server/routes/cursor.js +61 -61
  270. package/server/routes/diagnostics.js +41 -41
  271. package/server/routes/gemini.js +25 -25
  272. package/server/routes/git.js +1650 -1635
  273. package/server/routes/live-view.js +411 -411
  274. package/server/routes/mcp-utils.js +13 -13
  275. package/server/routes/messages.js +62 -62
  276. package/server/routes/network.js +125 -125
  277. package/server/routes/platformization.js +198 -197
  278. package/server/routes/plugins.js +320 -320
  279. package/server/routes/production-agent-loop.js +90 -90
  280. package/server/routes/projects.js +917 -917
  281. package/server/routes/public-api.js +34 -34
  282. package/server/routes/qwen.js +27 -27
  283. package/server/routes/remote.js +55 -55
  284. package/server/routes/settings.js +321 -321
  285. package/server/routes/telegram.js +140 -140
  286. package/server/routes/user.js +125 -125
  287. package/server/routes/webhooks.js +63 -63
  288. package/server/services/control-room.js +102 -102
  289. package/server/services/diagnostics.js +165 -165
  290. package/server/services/external-access.js +375 -375
  291. package/server/services/hermes-gateway.js +1562 -1247
  292. package/server/services/hermes-install-jobs.js +729 -729
  293. package/server/services/install-jobs.js +715 -715
  294. package/server/services/live-view.js +956 -956
  295. package/server/services/managed-runtimes.js +493 -493
  296. package/server/services/model-registry.js +144 -144
  297. package/server/services/notification-orchestrator.js +365 -365
  298. package/server/services/notification-taxonomy.js +204 -204
  299. package/server/services/platformization.js +844 -779
  300. package/server/services/production-agent-loop.js +248 -248
  301. package/server/services/provider-cli-versions.js +149 -149
  302. package/server/services/provider-credentials.js +189 -189
  303. package/server/services/provider-models.js +396 -396
  304. package/server/services/public-api-manifest.js +190 -182
  305. package/server/services/remote-connection.js +127 -127
  306. package/server/services/runtime-manager.js +323 -323
  307. package/server/services/startup-update.js +234 -234
  308. package/server/services/telegram/bot.js +331 -331
  309. package/server/services/telegram/control-center.js +979 -979
  310. package/server/services/telegram/telegram-http-client.js +151 -151
  311. package/server/services/telegram/translations.js +340 -340
  312. package/server/services/vapid-keys.js +36 -36
  313. package/server/services/webhooks.js +216 -216
  314. package/server/sessionManager.js +225 -225
  315. package/server/shared/interfaces.ts +54 -54
  316. package/server/shared/types.ts +172 -172
  317. package/server/shared/utils.ts +193 -193
  318. package/server/tsconfig.json +36 -36
  319. package/server/utils/colors.js +21 -21
  320. package/server/utils/commandParser.js +305 -305
  321. package/server/utils/frontmatter.js +18 -18
  322. package/server/utils/gitConfig.js +34 -34
  323. package/server/utils/plugin-loader.js +457 -457
  324. package/server/utils/plugin-process-manager.js +185 -185
  325. package/server/utils/port-access.js +209 -209
  326. package/server/utils/runtime-paths.js +37 -37
  327. package/server/utils/url-detection.js +71 -71
  328. package/server/vite-daemon.js +79 -79
  329. package/shared/modelConstants.js +161 -161
  330. package/shared/networkHosts.js +22 -22
  331. package/dist/assets/index-DMz0zv6T.css +0 -32
@@ -1,79 +1,79 @@
1
- // Gemini Response Handler - JSON Stream processing
2
- import { sessionsService } from './modules/providers/services/sessions.service.js';
3
-
4
- class GeminiResponseHandler {
5
- constructor(ws, options = {}) {
6
- this.ws = ws;
7
- this.buffer = '';
8
- this.onContentFragment = options.onContentFragment || null;
9
- this.onInit = options.onInit || null;
10
- this.onToolUse = options.onToolUse || null;
11
- this.onToolResult = options.onToolResult || null;
12
- }
13
-
14
- // Process incoming raw data from Gemini stream-json
15
- processData(data) {
16
- this.buffer += data;
17
-
18
- // Split by newline
19
- const lines = this.buffer.split('\n');
20
-
21
- // Keep the last incomplete line in the buffer
22
- this.buffer = lines.pop() || '';
23
-
24
- for (const line of lines) {
25
- if (!line.trim()) continue;
26
-
27
- try {
28
- const event = JSON.parse(line);
29
- this.handleEvent(event);
30
- } catch (err) {
31
- // Not a JSON line, probably debug output or CLI warnings
32
- }
33
- }
34
- }
35
-
36
- handleEvent(event) {
37
- const sid = typeof this.ws.getSessionId === 'function' ? this.ws.getSessionId() : null;
38
-
39
- if (event.type === 'init') {
40
- if (this.onInit) {
41
- this.onInit(event);
42
- }
43
- return;
44
- }
45
-
46
- // Invoke per-type callbacks for session tracking
47
- if (event.type === 'message' && event.role === 'assistant') {
48
- const content = event.content || '';
49
- if (this.onContentFragment && content) {
50
- this.onContentFragment(content);
51
- }
52
- } else if (event.type === 'tool_use' && this.onToolUse) {
53
- this.onToolUse(event);
54
- } else if (event.type === 'tool_result' && this.onToolResult) {
55
- this.onToolResult(event);
56
- }
57
-
58
- // Normalize via adapter and send all resulting messages
59
- const normalized = sessionsService.normalizeMessage('gemini', event, sid);
60
- for (const msg of normalized) {
61
- this.ws.send(msg);
62
- }
63
- }
64
-
65
- forceFlush() {
66
- if (this.buffer.trim()) {
67
- try {
68
- const event = JSON.parse(this.buffer);
69
- this.handleEvent(event);
70
- } catch (err) { }
71
- }
72
- }
73
-
74
- destroy() {
75
- this.buffer = '';
76
- }
77
- }
78
-
79
- export default GeminiResponseHandler;
1
+ // Gemini Response Handler - JSON Stream processing
2
+ import { sessionsService } from './modules/providers/services/sessions.service.js';
3
+
4
+ class GeminiResponseHandler {
5
+ constructor(ws, options = {}) {
6
+ this.ws = ws;
7
+ this.buffer = '';
8
+ this.onContentFragment = options.onContentFragment || null;
9
+ this.onInit = options.onInit || null;
10
+ this.onToolUse = options.onToolUse || null;
11
+ this.onToolResult = options.onToolResult || null;
12
+ }
13
+
14
+ // Process incoming raw data from Gemini stream-json
15
+ processData(data) {
16
+ this.buffer += data;
17
+
18
+ // Split by newline
19
+ const lines = this.buffer.split('\n');
20
+
21
+ // Keep the last incomplete line in the buffer
22
+ this.buffer = lines.pop() || '';
23
+
24
+ for (const line of lines) {
25
+ if (!line.trim()) continue;
26
+
27
+ try {
28
+ const event = JSON.parse(line);
29
+ this.handleEvent(event);
30
+ } catch (err) {
31
+ // Not a JSON line, probably debug output or CLI warnings
32
+ }
33
+ }
34
+ }
35
+
36
+ handleEvent(event) {
37
+ const sid = typeof this.ws.getSessionId === 'function' ? this.ws.getSessionId() : null;
38
+
39
+ if (event.type === 'init') {
40
+ if (this.onInit) {
41
+ this.onInit(event);
42
+ }
43
+ return;
44
+ }
45
+
46
+ // Invoke per-type callbacks for session tracking
47
+ if (event.type === 'message' && event.role === 'assistant') {
48
+ const content = event.content || '';
49
+ if (this.onContentFragment && content) {
50
+ this.onContentFragment(content);
51
+ }
52
+ } else if (event.type === 'tool_use' && this.onToolUse) {
53
+ this.onToolUse(event);
54
+ } else if (event.type === 'tool_result' && this.onToolResult) {
55
+ this.onToolResult(event);
56
+ }
57
+
58
+ // Normalize via adapter and send all resulting messages
59
+ const normalized = sessionsService.normalizeMessage('gemini', event, sid);
60
+ for (const msg of normalized) {
61
+ this.ws.send(msg);
62
+ }
63
+ }
64
+
65
+ forceFlush() {
66
+ if (this.buffer.trim()) {
67
+ try {
68
+ const event = JSON.parse(this.buffer);
69
+ this.handleEvent(event);
70
+ } catch (err) { }
71
+ }
72
+ }
73
+
74
+ destroy() {
75
+ this.buffer = '';
76
+ }
77
+ }
78
+
79
+ export default GeminiResponseHandler;
package/server/index.js CHANGED
@@ -111,6 +111,7 @@ import {
111
111
  GeminiA2AAdapter,
112
112
  OpenCodeA2AAdapter,
113
113
  QwenA2AAdapter,
114
+ JsonEventA2AAdapter,
114
115
  createPreviewProxyRouter,
115
116
  createOrchestrationTaskRouter,
116
117
  createHermesRouter,
@@ -130,7 +131,8 @@ import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './util
130
131
  import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, apiKeysDb } from './database/db.js';
131
132
  import { setNotificationWebSocketServer } from './services/notification-orchestrator.js';
132
133
  import { configureWebPush } from './services/vapid-keys.js';
133
- import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
134
+ import { validateApiKey, authenticateToken, authenticateWebSocket, requireAdmin } from './middleware/auth.js';
135
+ import { filterProjectsForUser, userHasProjectAccess } from './services/platformization.js';
134
136
  import { IS_PLATFORM } from './constants/config.js';
135
137
 
136
138
  import { getConnectableHost } from '../shared/networkHosts.js';
@@ -139,6 +141,37 @@ import { buildDaemonCliCommand, handleDaemonCommand } from './daemon-manager.js'
139
141
 
140
142
  const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode'];
141
143
 
144
+ function requireProjectAccess(capability = 'viewFiles') {
145
+ return (req, res, next) => {
146
+ const projectName = req.params.projectName || req.query.project || req.body?.project;
147
+ if (!projectName) {
148
+ return next();
149
+ }
150
+
151
+ if (!userHasProjectAccess(req.user, { name: String(projectName), projectName: String(projectName) }, capability)) {
152
+ return res.status(403).json({ error: 'Project access denied.' });
153
+ }
154
+
155
+ next();
156
+ };
157
+ }
158
+
159
+ function requireProjectPathAccess(capability = 'viewFiles') {
160
+ return (req, res, next) => {
161
+ const projectPath = req.body?.projectPath || req.query.projectPath || os.homedir();
162
+ const resolvedProjectPath = path.resolve(String(projectPath));
163
+ if (!userHasProjectAccess(req.user, {
164
+ fullPath: resolvedProjectPath,
165
+ path: resolvedProjectPath,
166
+ projectPath: resolvedProjectPath,
167
+ }, capability)) {
168
+ return res.status(403).json({ error: 'Project access denied.' });
169
+ }
170
+
171
+ next();
172
+ };
173
+ }
174
+
142
175
  // File system watchers for provider project/session folders
143
176
  const PROVIDER_WATCH_PATHS = [
144
177
  { provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
@@ -229,19 +262,21 @@ async function setupProjectsWatcher() {
229
262
  // Get updated projects list
230
263
  const updatedProjects = await getProjects(broadcastProgress);
231
264
 
232
- // Notify all connected clients about the project changes
233
- const updateMessage = JSON.stringify({
265
+ const updatePayload = {
234
266
  type: 'projects_updated',
235
- projects: updatedProjects,
236
267
  timestamp: new Date().toISOString(),
237
268
  changeType: eventType,
238
269
  changedFile: path.relative(rootPath, filePath),
239
270
  watchProvider: provider
240
- });
271
+ };
241
272
 
273
+ // Notify all connected clients about project changes, scoped to their access.
242
274
  connectedClients.forEach(client => {
243
275
  if (client.readyState === WebSocket.OPEN) {
244
- client.send(updateMessage);
276
+ client.send(JSON.stringify({
277
+ ...updatePayload,
278
+ projects: filterProjectsForUser(updatedProjects, client.user),
279
+ }));
245
280
  }
246
281
  });
247
282
 
@@ -302,6 +337,118 @@ async function setupProjectsWatcher() {
302
337
  }
303
338
  }
304
339
 
340
+ // ── Per-project workspace watchers (file explorer live refresh) ─────────────
341
+ // setupProjectsWatcher() above only watches provider metadata folders
342
+ // (~/.claude/projects etc.), so the file explorer never learned about changes
343
+ // inside the actual project working directory. These watchers are created on
344
+ // demand when a client sends `watch-project` over /ws and broadcast debounced
345
+ // `project_files_updated` events to subscribed clients only, letting the
346
+ // explorer refresh automatically without HTTP polling.
347
+ const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
348
+ const workspaceWatchers = new Map(); // projectName -> { watcher, subscribers, debounceTimer, pendingEvent, rootPath }
349
+
350
+ async function subscribeToWorkspace(ws, projectName) {
351
+ if (!projectName || typeof projectName !== 'string') return;
352
+ if (!userHasProjectAccess(ws.user, { name: projectName, projectName }, 'viewFiles')) return;
353
+
354
+ const existing = workspaceWatchers.get(projectName);
355
+ if (existing) {
356
+ existing.subscribers.add(ws);
357
+ return;
358
+ }
359
+
360
+ let rootPath;
361
+ try {
362
+ rootPath = await extractProjectDirectory(projectName);
363
+ await fsPromises.access(rootPath);
364
+ } catch (error) {
365
+ console.warn(`[watcher] Cannot watch workspace for ${projectName}:`, error.message);
366
+ return;
367
+ }
368
+
369
+ const entry = {
370
+ watcher: null,
371
+ subscribers: new Set([ws]),
372
+ debounceTimer: null,
373
+ pendingEvent: null,
374
+ rootPath,
375
+ };
376
+ workspaceWatchers.set(projectName, entry);
377
+
378
+ const broadcastFileUpdate = (eventType, filePath) => {
379
+ entry.pendingEvent = { eventType, filePath };
380
+ if (entry.debounceTimer) {
381
+ clearTimeout(entry.debounceTimer);
382
+ }
383
+ entry.debounceTimer = setTimeout(() => {
384
+ entry.debounceTimer = null;
385
+ const pending = entry.pendingEvent || {};
386
+ entry.pendingEvent = null;
387
+ const message = JSON.stringify({
388
+ type: 'project_files_updated',
389
+ projectName,
390
+ changeType: pending.eventType || 'change',
391
+ changedFile: pending.filePath ? path.relative(rootPath, pending.filePath) : null,
392
+ timestamp: new Date().toISOString(),
393
+ });
394
+ entry.subscribers.forEach((client) => {
395
+ if (client.readyState === WebSocket.OPEN) {
396
+ client.send(message);
397
+ }
398
+ });
399
+ }, WORKSPACE_WATCHER_DEBOUNCE_MS);
400
+ };
401
+
402
+ try {
403
+ const chokidar = (await import('chokidar')).default;
404
+ const watcher = chokidar.watch(rootPath, {
405
+ ignored: WATCHER_IGNORED_PATTERNS,
406
+ persistent: true,
407
+ ignoreInitial: true,
408
+ followSymlinks: false,
409
+ depth: 10,
410
+ awaitWriteFinish: {
411
+ stabilityThreshold: 500,
412
+ pollInterval: 250
413
+ }
414
+ });
415
+
416
+ watcher
417
+ .on('add', (filePath) => broadcastFileUpdate('add', filePath))
418
+ .on('change', (filePath) => broadcastFileUpdate('change', filePath))
419
+ .on('unlink', (filePath) => broadcastFileUpdate('unlink', filePath))
420
+ .on('addDir', (dirPath) => broadcastFileUpdate('addDir', dirPath))
421
+ .on('unlinkDir', (dirPath) => broadcastFileUpdate('unlinkDir', dirPath))
422
+ .on('error', (error) => {
423
+ console.error(`[ERROR] Workspace watcher error for ${projectName}:`, error);
424
+ });
425
+
426
+ entry.watcher = watcher;
427
+ } catch (error) {
428
+ workspaceWatchers.delete(projectName);
429
+ console.error(`[ERROR] Failed to watch workspace for ${projectName}:`, error);
430
+ }
431
+ }
432
+
433
+ function unsubscribeFromWorkspace(ws, projectName = null) {
434
+ const entries = projectName
435
+ ? (workspaceWatchers.has(projectName) ? [[projectName, workspaceWatchers.get(projectName)]] : [])
436
+ : Array.from(workspaceWatchers.entries());
437
+
438
+ for (const [name, entry] of entries) {
439
+ entry.subscribers.delete(ws);
440
+ if (entry.subscribers.size === 0) {
441
+ if (entry.debounceTimer) {
442
+ clearTimeout(entry.debounceTimer);
443
+ }
444
+ workspaceWatchers.delete(name);
445
+ entry.watcher?.close().catch((error) => {
446
+ console.warn(`[watcher] Failed to close workspace watcher for ${name}:`, error?.message || error);
447
+ });
448
+ }
449
+ }
450
+ }
451
+
305
452
 
306
453
  const app = express();
307
454
  const server = http.createServer(app);
@@ -781,7 +928,7 @@ app.get('/health', (req, res) => {
781
928
  // Optional API key validation (if configured)
782
929
  app.use('/api', validateApiKey);
783
930
 
784
- app.post('/api/shell/sessions/terminate', authenticateToken, (req, res) => {
931
+ app.post('/api/shell/sessions/terminate', authenticateToken, requireProjectPathAccess('useShell'), (req, res) => {
785
932
  const provider = req.body?.provider || 'claude';
786
933
  const projectPath = req.body?.projectPath || os.homedir();
787
934
 
@@ -793,7 +940,7 @@ app.post('/api/shell/sessions/terminate', authenticateToken, (req, res) => {
793
940
  res.json({ success: true, killedSessions });
794
941
  });
795
942
 
796
- app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) => {
943
+ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProjectPathAccess('useShell'), (req, res) => {
797
944
  const provider = String(req.query.provider || 'claude');
798
945
  const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.trim()
799
946
  ? req.query.projectPath.trim()
@@ -850,7 +997,7 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) =>
850
997
  });
851
998
  });
852
999
 
853
- app.post('/api/shell/sessions/provider-input', authenticateToken, (req, res) => {
1000
+ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProjectPathAccess('useShell'), (req, res) => {
854
1001
  const provider = String(req.body?.provider || 'claude');
855
1002
  const projectPath = typeof req.body?.projectPath === 'string' && req.body.projectPath.trim()
856
1003
  ? req.body.projectPath.trim()
@@ -970,8 +1117,8 @@ app.use('/api/webhooks', authenticateToken, webhooksRoutes);
970
1117
  // Production agent loop APIs (protected)
971
1118
  app.use('/api/production-agent-loop', authenticateToken, productionAgentLoopRoutes);
972
1119
 
973
- // Platform control plane APIs (protected)
974
- app.use('/api/platformization', authenticateToken, platformizationRoutes);
1120
+ // Platform control plane APIs (admin-only)
1121
+ app.use('/api/platformization', authenticateToken, requireAdmin, platformizationRoutes);
975
1122
 
976
1123
  // Project Live View (protected control API + public share proxy)
977
1124
  app.use('/api/live-view', authenticateToken, liveViewRoutes);
@@ -986,6 +1133,7 @@ adapterRegistry.register(new CursorA2AAdapter());
986
1133
  adapterRegistry.register(new GeminiA2AAdapter());
987
1134
  adapterRegistry.register(new QwenA2AAdapter());
988
1135
  adapterRegistry.register(new OpenCodeA2AAdapter());
1136
+ adapterRegistry.register(new JsonEventA2AAdapter());
989
1137
  app.use('/hermes', createHermesTaskRouter());
990
1138
  app.use('/preview', authenticateToken, createPreviewProxyRouter());
991
1139
  app.use('/api/orchestration', authenticateToken, createOrchestrationTaskRouter());
@@ -1432,13 +1580,13 @@ app.post('/api/system/restart', authenticateToken, (req, res) => {
1432
1580
  app.get('/api/projects', authenticateToken, async (req, res) => {
1433
1581
  try {
1434
1582
  const projects = await getProjects(broadcastProgress);
1435
- res.json(projects);
1583
+ res.json(filterProjectsForUser(projects, req.user));
1436
1584
  } catch (error) {
1437
1585
  res.status(500).json({ error: error.message });
1438
1586
  }
1439
1587
  });
1440
1588
 
1441
- app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => {
1589
+ app.get('/api/projects/:projectName/sessions', authenticateToken, requireProjectAccess('viewFiles'), async (req, res) => {
1442
1590
  try {
1443
1591
  const { limit = 5, offset = 0 } = req.query;
1444
1592
  const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
@@ -1450,7 +1598,7 @@ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, re
1450
1598
  });
1451
1599
 
1452
1600
  // Rename project endpoint
1453
- app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
1601
+ app.put('/api/projects/:projectName/rename', authenticateToken, requireProjectAccess('manageProjectSettings'), async (req, res) => {
1454
1602
  try {
1455
1603
  const { displayName } = req.body;
1456
1604
  await renameProject(req.params.projectName, displayName);
@@ -1461,7 +1609,7 @@ app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res)
1461
1609
  });
1462
1610
 
1463
1611
  // Delete session endpoint
1464
- app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
1612
+ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, requireProjectAccess('editFiles'), async (req, res) => {
1465
1613
  try {
1466
1614
  const { projectName, sessionId } = req.params;
1467
1615
  console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
@@ -1504,7 +1652,7 @@ app.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) =
1504
1652
  // Delete project endpoint
1505
1653
  // force=true to allow removal even when sessions exist
1506
1654
  // deleteData=true to also delete session/memory files on disk (destructive)
1507
- app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
1655
+ app.delete('/api/projects/:projectName', authenticateToken, requireProjectAccess('manageProjectSettings'), async (req, res) => {
1508
1656
  try {
1509
1657
  const { projectName } = req.params;
1510
1658
  const force = req.query.force === 'true';
@@ -1522,7 +1670,7 @@ app.delete('/api/projects/:projectName', authenticateToken, async (req, res) =>
1522
1670
  });
1523
1671
 
1524
1672
  // Search conversations content (SSE streaming)
1525
- app.get('/api/search/conversations', authenticateToken, async (req, res) => {
1673
+ app.get('/api/search/conversations', authenticateToken, requireAdmin, async (req, res) => {
1526
1674
  const query = typeof req.query.q === 'string' ? req.query.q.trim() : '';
1527
1675
  const parsedLimit = Number.parseInt(String(req.query.limit), 10);
1528
1676
  const limit = Number.isNaN(parsedLimit) ? 50 : Math.max(1, Math.min(parsedLimit, 100));
@@ -1717,7 +1865,7 @@ app.post('/api/create-folder', authenticateToken, async (req, res) => {
1717
1865
  });
1718
1866
 
1719
1867
  // Read file content endpoint
1720
- app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
1868
+ app.get('/api/projects/:projectName/file', authenticateToken, requireProjectAccess('viewFiles'), async (req, res) => {
1721
1869
  try {
1722
1870
  const { projectName } = req.params;
1723
1871
  const { filePath } = req.query;
@@ -1757,7 +1905,7 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
1757
1905
  });
1758
1906
 
1759
1907
  // Serve raw file bytes for previews and downloads.
1760
- app.get('/api/projects/:projectName/files/content', authenticateToken, async (req, res) => {
1908
+ app.get('/api/projects/:projectName/files/content', authenticateToken, requireProjectAccess('viewFiles'), async (req, res) => {
1761
1909
  try {
1762
1910
  const { projectName } = req.params;
1763
1911
  const { path: filePath } = req.query;
@@ -1814,7 +1962,7 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
1814
1962
  });
1815
1963
 
1816
1964
  // Save file content endpoint
1817
- app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
1965
+ app.put('/api/projects/:projectName/file', authenticateToken, requireProjectAccess('editFiles'), async (req, res) => {
1818
1966
  try {
1819
1967
  const { projectName } = req.params;
1820
1968
  const { filePath, content } = req.body;
@@ -1863,7 +2011,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
1863
2011
  }
1864
2012
  });
1865
2013
 
1866
- app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
2014
+ app.get('/api/projects/:projectName/files', authenticateToken, requireProjectAccess('viewFiles'), async (req, res) => {
1867
2015
  try {
1868
2016
 
1869
2017
  // Using fsPromises from import
@@ -1874,8 +2022,12 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
1874
2022
  actualPath = await extractProjectDirectory(req.params.projectName);
1875
2023
  } catch (error) {
1876
2024
  console.error('Error extracting project directory:', error);
1877
- // Fallback to simple dash replacement
1878
- actualPath = req.params.projectName.replace(/-/g, '/');
2025
+ // Do NOT fall back to projectName.replace(/-/g, '/') here: on Windows the
2026
+ // dash-encoded name ("C--Users-...") decodes to a garbage path ("C//Users/...")
2027
+ // that can never exist, so the old fallback just produced a confusing 404
2028
+ // with a fabricated path. extractProjectDirectory already handles the
2029
+ // POSIX-style decode internally; if it throws, the project is unknown.
2030
+ return res.status(404).json({ error: `Project not found: ${req.params.projectName}` });
1879
2031
  }
1880
2032
 
1881
2033
  // Check if path exists
@@ -1941,7 +2093,7 @@ function validateFilename(name) {
1941
2093
  }
1942
2094
 
1943
2095
  // POST /api/projects/:projectName/files/create - Create new file or directory
1944
- app.post('/api/projects/:projectName/files/create', authenticateToken, async (req, res) => {
2096
+ app.post('/api/projects/:projectName/files/create', authenticateToken, requireProjectAccess('editFiles'), async (req, res) => {
1945
2097
  try {
1946
2098
  const { projectName } = req.params;
1947
2099
  const { path: parentPath, type, name } = req.body;
@@ -2018,7 +2170,7 @@ app.post('/api/projects/:projectName/files/create', authenticateToken, async (re
2018
2170
  });
2019
2171
 
2020
2172
  // PUT /api/projects/:projectName/files/rename - Rename file or directory
2021
- app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req, res) => {
2173
+ app.put('/api/projects/:projectName/files/rename', authenticateToken, requireProjectAccess('editFiles'), async (req, res) => {
2022
2174
  try {
2023
2175
  const { projectName } = req.params;
2024
2176
  const { oldPath, newName } = req.body;
@@ -2095,7 +2247,7 @@ app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req
2095
2247
  });
2096
2248
 
2097
2249
  // DELETE /api/projects/:projectName/files - Delete file or directory
2098
- app.delete('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
2250
+ app.delete('/api/projects/:projectName/files', authenticateToken, requireProjectAccess('editFiles'), async (req, res) => {
2099
2251
  try {
2100
2252
  const { projectName } = req.params;
2101
2253
  const { path: targetPath, type } = req.body;
@@ -2321,7 +2473,7 @@ const uploadFilesHandler = async (req, res) => {
2321
2473
  });
2322
2474
  };
2323
2475
 
2324
- app.post('/api/projects/:projectName/files/upload', authenticateToken, uploadFilesHandler);
2476
+ app.post('/api/projects/:projectName/files/upload', authenticateToken, requireProjectAccess('editFiles'), uploadFilesHandler);
2325
2477
 
2326
2478
  /**
2327
2479
  * Proxy an authenticated client WebSocket to a plugin's internal WS server.
@@ -2428,6 +2580,7 @@ function handleChatConnection(ws, request) {
2428
2580
 
2429
2581
  // Add to connected clients for project updates
2430
2582
  ws.userId = request?.user?.id ?? request?.user?.userId ?? null;
2583
+ ws.user = request?.user ?? null;
2431
2584
  connectedClients.add(ws);
2432
2585
 
2433
2586
  // Wrap WebSocket with writer for consistent interface with SSEStreamWriter
@@ -2554,6 +2707,13 @@ function handleChatConnection(ws, request) {
2554
2707
  data: pending
2555
2708
  });
2556
2709
  }
2710
+ } else if (data.type === 'watch-project') {
2711
+ // Subscribe this client to live file-tree updates for a project
2712
+ // workspace. The server pushes debounced `project_files_updated`
2713
+ // events so the explorer refreshes without HTTP polling.
2714
+ await subscribeToWorkspace(ws, data.projectName);
2715
+ } else if (data.type === 'unwatch-project') {
2716
+ unsubscribeFromWorkspace(ws, data.projectName || null);
2557
2717
  } else if (data.type === 'get-active-sessions') {
2558
2718
  // Get all currently active sessions
2559
2719
  const activeSessions = {
@@ -2582,6 +2742,8 @@ function handleChatConnection(ws, request) {
2582
2742
  console.log('🔌 Chat client disconnected');
2583
2743
  // Remove from connected clients
2584
2744
  connectedClients.delete(ws);
2745
+ // Drop any workspace watcher subscriptions held by this socket
2746
+ unsubscribeFromWorkspace(ws);
2585
2747
  });
2586
2748
  }
2587
2749
 
@@ -2613,6 +2775,15 @@ function handleShellConnection(ws, request) {
2613
2775
  // is writable, has a git-friendly cwd, and matches where
2614
2776
  // every provider already stores its config (~/.codex etc.).
2615
2777
  const projectPath = data.projectPath || os.homedir();
2778
+ const requestedProjectPath = path.resolve(projectPath);
2779
+ if (!userHasProjectAccess(request.user, {
2780
+ fullPath: requestedProjectPath,
2781
+ path: requestedProjectPath,
2782
+ projectPath: requestedProjectPath,
2783
+ }, 'useShell')) {
2784
+ ws.send(JSON.stringify({ type: 'error', message: 'Shell access denied for this project' }));
2785
+ return;
2786
+ }
2616
2787
  const sessionId = data.sessionId;
2617
2788
  const hasSession = data.hasSession;
2618
2789
  const provider = data.provider || 'claude';
@@ -2750,7 +2921,7 @@ function handleShellConnection(ws, request) {
2750
2921
 
2751
2922
  try {
2752
2923
  // Validate projectPath — resolve to absolute and verify it exists
2753
- const resolvedProjectPath = path.resolve(projectPath);
2924
+ const resolvedProjectPath = requestedProjectPath;
2754
2925
  try {
2755
2926
  const stats = fs.statSync(resolvedProjectPath);
2756
2927
  if (!stats.isDirectory()) {
@@ -3108,7 +3279,7 @@ function handleShellConnection(ws, request) {
3108
3279
  });
3109
3280
  }
3110
3281
  // Image upload endpoint
3111
- app.post('/api/projects/:projectName/upload-images', authenticateToken, async (req, res) => {
3282
+ app.post('/api/projects/:projectName/upload-images', authenticateToken, requireProjectAccess('editFiles'), async (req, res) => {
3112
3283
  try {
3113
3284
  const multer = (await import('multer')).default;
3114
3285
  const path = (await import('path')).default;
@@ -3193,7 +3364,7 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
3193
3364
  });
3194
3365
 
3195
3366
  // Get token usage for a specific session
3196
- app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
3367
+ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, requireProjectAccess('viewFiles'), async (req, res) => {
3197
3368
  try {
3198
3369
  const { projectName, sessionId } = req.params;
3199
3370
  const { provider = 'claude' } = req.query;