cicy-desktop 1.0.9 → 2.1.30

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 (195) hide show
  1. package/.cicy-code-ref +1 -0
  2. package/.env.dev +7 -0
  3. package/.github/workflows/linux-app-release.yml +78 -0
  4. package/.github/workflows/mac-app-release.yml +161 -0
  5. package/.github/workflows/windows-exe-release.yml +105 -0
  6. package/.kiro/steering/dev-workflow.md +6 -6
  7. package/AGENTS.md +30 -2
  8. package/CLAUDE.md +609 -162
  9. package/CLAUDE_HANDOFF.md +168 -0
  10. package/DESIGN.md +66 -0
  11. package/DOCKER.md +12 -12
  12. package/Dockerfile +2 -2
  13. package/README.md +331 -720
  14. package/bin/cicy-desktop +862 -0
  15. package/bin/cicy-rpc +13 -0
  16. package/build/icon.icns +0 -0
  17. package/build/icon.ico +0 -0
  18. package/build/icon.png +0 -0
  19. package/build/icon.svg +22 -0
  20. package/build/icons/icon-1024.png +0 -0
  21. package/build/icons/icon-128.png +0 -0
  22. package/build/icons/icon-16.png +0 -0
  23. package/build/icons/icon-24.png +0 -0
  24. package/build/icons/icon-256.png +0 -0
  25. package/build/icons/icon-32.png +0 -0
  26. package/build/icons/icon-48.png +0 -0
  27. package/build/icons/icon-512.png +0 -0
  28. package/build/icons/icon-64.png +0 -0
  29. package/build/icons/icon-96.png +0 -0
  30. package/build/icons/trayTemplate-16.png +0 -0
  31. package/build/icons/trayTemplate-16@2x.png +0 -0
  32. package/build/icons/trayTemplate-22.png +0 -0
  33. package/build/icons/trayTemplate-22@2x.png +0 -0
  34. package/build/icons/trayTemplate-32.png +0 -0
  35. package/build/icons/trayTemplate-32@2x.png +0 -0
  36. package/build/trayTemplate.png +0 -0
  37. package/build/trayTemplate.svg +13 -0
  38. package/build/trayTemplate@2x.png +0 -0
  39. package/cicy-dektop.command +51 -0
  40. package/copy-to-desktop.sh +4 -4
  41. package/dev-app-update.yml +3 -0
  42. package/docs/AUTOMATION-API.md +2 -2
  43. package/docs/REQUEST_MONITORING.md +1 -1
  44. package/docs/REST-API-FEATURE.md +2 -2
  45. package/docs/REST-API.md +276 -319
  46. package/docs/backend-selector-design.md +204 -0
  47. package/docs/chrome-proxy.md +142 -0
  48. package/docs/cicy-desktop-current-execution-bridge.md +509 -0
  49. package/docs/feature-distributed-multi-agent.md +11 -11
  50. package/docs/worklog-2026-03-27.md +108 -0
  51. package/docs/yaml.md +1 -1
  52. package/generate-openapi.js +159 -158
  53. package/package.json +80 -12
  54. package/scripts/prepare-cicy-code-sidecar.js +116 -0
  55. package/service.sh +5 -5
  56. package/src/app-updater.js +131 -0
  57. package/src/backends/auth-loopback.js +141 -0
  58. package/src/backends/homepage-preload.js +283 -0
  59. package/src/backends/homepage-react/assets/index-B8FrtpTX.js +49 -0
  60. package/src/backends/homepage-react/assets/index-CNVsvsZX.css +1 -0
  61. package/src/backends/homepage-react/index.html +13 -0
  62. package/src/backends/homepage-window.js +104 -0
  63. package/src/backends/homepage.html +1127 -0
  64. package/src/backends/ipc.js +220 -0
  65. package/src/backends/local-teams.js +621 -0
  66. package/src/backends/poller.js +63 -0
  67. package/src/backends/registry.js +141 -0
  68. package/src/backends/sidecar-ipc.js +186 -0
  69. package/src/backends/updater.js +71 -0
  70. package/src/backends/webview-preload.js +60 -0
  71. package/src/backends/window-manager.js +172 -0
  72. package/src/backends/window-tracker.js +50 -0
  73. package/src/chrome/chrome-cdp-client.js +80 -0
  74. package/src/chrome/chrome-launcher.js +229 -0
  75. package/src/chrome/debugger-port-resolver.js +44 -0
  76. package/src/chrome/runtime-registry.js +83 -0
  77. package/src/cli/rpc.js +356 -0
  78. package/src/cluster/artifact-registry.js +61 -0
  79. package/src/cluster/local-agent-registry.js +40 -0
  80. package/src/cluster/remote-executor.js +20 -0
  81. package/src/cluster/types.js +31 -0
  82. package/src/cluster/worker-client.js +74 -0
  83. package/src/cluster/worker-identity.js +25 -0
  84. package/src/i18n/index.js +56 -0
  85. package/src/i18n/locales/en.json +81 -0
  86. package/src/i18n/locales/fr.json +81 -0
  87. package/src/i18n/locales/ja.json +81 -0
  88. package/src/i18n/locales/zh-CN.json +81 -0
  89. package/src/main-old.js +7 -7
  90. package/src/main.js +840 -272
  91. package/src/master/agent-index.js +24 -0
  92. package/src/master/chrome-config.js +97 -0
  93. package/src/master/master-admin-routes.js +68 -0
  94. package/src/master/master-admin.html +359 -0
  95. package/src/master/master-main.js +103 -0
  96. package/src/master/master-metrics.js +145 -0
  97. package/src/master/master-routes.js +241 -0
  98. package/src/master/master-token-manager.js +46 -0
  99. package/src/master/session-affinity-store.js +36 -0
  100. package/src/master/task-scheduler.js +150 -0
  101. package/src/master/task-store.js +53 -0
  102. package/src/master/worker-inventory.js +267 -0
  103. package/src/master/worker-registry.js +74 -0
  104. package/src/server/args-parser.js +57 -2
  105. package/src/server/chrome-management-routes.js +145 -0
  106. package/src/server/chrome-proxy-routes.js +121 -0
  107. package/src/server/express-app.js +24 -5
  108. package/src/server/logging.js +1 -1
  109. package/src/server/mcp-server.js +1 -1
  110. package/src/server/tool-catalog.js +46 -0
  111. package/src/server/tool-executor.js +22 -0
  112. package/src/server/tool-registry.js +81 -77
  113. package/src/server/worker-observability-routes.js +54 -0
  114. package/src/sidecar/cicy-code.js +144 -0
  115. package/src/sidecar/installer.js +672 -0
  116. package/src/sidecar/mirrors.js +67 -0
  117. package/src/sidecar/net-detect.js +50 -0
  118. package/src/sidecar/wsl.js +585 -0
  119. package/src/swagger-ui.html +1 -1
  120. package/src/tools/account-tools.js +86 -59
  121. package/src/tools/chrome-tools.js +770 -0
  122. package/src/tools/exec-tools.js +27 -6
  123. package/src/tools/file-tools.js +3 -3
  124. package/src/tools/index.js +2 -1
  125. package/src/tools/ping.js +63 -60
  126. package/src/tools/system-tools.js +6 -6
  127. package/src/tools/window-tools.js +29 -5
  128. package/src/tray.js +93 -0
  129. package/src/ui-react-dist/assets/index-IWkApOSk.css +1 -0
  130. package/src/ui-react-dist/assets/index-jGZoL6K1.js +154 -0
  131. package/src/ui-react-dist/index.html +13 -0
  132. package/src/utils/auth.js +48 -12
  133. package/src/utils/global-json.js +85 -0
  134. package/src/utils/snapshot-utils.js +1 -1
  135. package/src/utils/window-monitor.js +17 -7
  136. package/src/utils/window-utils.js +94 -30
  137. package/update-desktop.sh +3 -3
  138. package/workers/render/index.html +12 -0
  139. package/workers/render/package-lock.json +2438 -0
  140. package/workers/render/package.json +20 -0
  141. package/workers/render/src/App.css +631 -0
  142. package/workers/render/src/App.jsx +864 -0
  143. package/workers/render/src/main.jsx +20 -0
  144. package/workers/render/vite.config.js +10 -0
  145. package/workers/render/wrangler.toml +15 -0
  146. package/workers/render.bak.20260528-2338/DESIGN_v2.md +254 -0
  147. package/workers/render.bak.20260528-2338/index.html +12 -0
  148. package/workers/render.bak.20260528-2338/package-lock.json +827 -0
  149. package/workers/render.bak.20260528-2338/package.json +19 -0
  150. package/workers/render.bak.20260528-2338/public/_headers +5 -0
  151. package/workers/render.bak.20260528-2338/public/manifest.json +6 -0
  152. package/workers/render.bak.20260528-2338/src/App.css +224 -0
  153. package/workers/render.bak.20260528-2338/src/App.jsx +1028 -0
  154. package/workers/render.bak.20260528-2338/src/api.js +285 -0
  155. package/workers/render.bak.20260528-2338/src/cicycode-ops.js +222 -0
  156. package/workers/render.bak.20260528-2338/src/components/BackendCard.css +299 -0
  157. package/workers/render.bak.20260528-2338/src/components/BackendCard.jsx +133 -0
  158. package/workers/render.bak.20260528-2338/src/components/BackendModal.css +161 -0
  159. package/workers/render.bak.20260528-2338/src/components/BackendModal.jsx +199 -0
  160. package/workers/render.bak.20260528-2338/src/components/Button.css +72 -0
  161. package/workers/render.bak.20260528-2338/src/components/Button.jsx +37 -0
  162. package/workers/render.bak.20260528-2338/src/components/Card.css +42 -0
  163. package/workers/render.bak.20260528-2338/src/components/Card.jsx +21 -0
  164. package/workers/render.bak.20260528-2338/src/components/Icon.jsx +30 -0
  165. package/workers/render.bak.20260528-2338/src/components/Menu.css +55 -0
  166. package/workers/render.bak.20260528-2338/src/components/Menu.jsx +91 -0
  167. package/workers/render.bak.20260528-2338/src/components/SidecarBanner.css +79 -0
  168. package/workers/render.bak.20260528-2338/src/components/SidecarBanner.jsx +84 -0
  169. package/workers/render.bak.20260528-2338/src/components/StatusChip.css +19 -0
  170. package/workers/render.bak.20260528-2338/src/components/StatusChip.jsx +31 -0
  171. package/workers/render.bak.20260528-2338/src/components/Toast.css +31 -0
  172. package/workers/render.bak.20260528-2338/src/components/Toast.jsx +23 -0
  173. package/workers/render.bak.20260528-2338/src/components/WslSetupBanner.css +464 -0
  174. package/workers/render.bak.20260528-2338/src/components/WslSetupBanner.jsx +716 -0
  175. package/workers/render.bak.20260528-2338/src/dockerInstaller.js +0 -0
  176. package/workers/render.bak.20260528-2338/src/i18n/en.json +116 -0
  177. package/workers/render.bak.20260528-2338/src/i18n/fr.json +116 -0
  178. package/workers/render.bak.20260528-2338/src/i18n/index.js +69 -0
  179. package/workers/render.bak.20260528-2338/src/i18n/ja.json +116 -0
  180. package/workers/render.bak.20260528-2338/src/i18n/zh-CN.json +121 -0
  181. package/workers/render.bak.20260528-2338/src/main.js +475 -0
  182. package/workers/render.bak.20260528-2338/src/main.jsx +18 -0
  183. package/workers/render.bak.20260528-2338/src/style.css +275 -0
  184. package/workers/render.bak.20260528-2338/src/styles/base.css +98 -0
  185. package/workers/render.bak.20260528-2338/src/styles/tokens.css +90 -0
  186. package/workers/render.bak.20260528-2338/src/tos.js +72 -0
  187. package/workers/render.bak.20260528-2338/src/worker.js +40 -0
  188. package/workers/render.bak.20260528-2338/src/wslInstaller.js +1563 -0
  189. package/workers/render.bak.20260528-2338/vite.config.js +36 -0
  190. package/workers/render.bak.20260528-2338/wrangler.toml +17 -0
  191. package/.github/workflows/build.yml +0 -85
  192. package/bin/cicy +0 -176
  193. package/electron-mcp-fixed.command +0 -134
  194. package/electron-mcp-simple.command +0 -135
  195. package/electron-mcp.command +0 -92
package/CLAUDE.md CHANGED
@@ -1,162 +1,609 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- This is an Electron-based MCP (Model Context Protocol) server that provides browser automation and web scraping capabilities. It exposes window management, Chrome DevTools Protocol (CDP) operations, and page interaction tools through a standardized MCP interface.
8
-
9
- ## Development Commands
10
-
11
- ### Starting the Server
12
- ```bash
13
- # Start MCP server (default port 8101)
14
- npm start
15
-
16
- # Start with custom port
17
- npm start -- --port=8102
18
-
19
- # Start in test mode with environment variables
20
- export URL=https://www.google.com
21
- export TEST=true
22
- export DISPLAY=:1
23
- npx electron main.js
24
- ```
25
-
26
- ### Testing
27
- ```bash
28
- # Run complete test suite
29
- npm test
30
-
31
- # Run specific test categories
32
- npm run test:api # Basic API tests
33
- npm run test:cdp # All CDP tools tests
34
- npm run test:cdp-mouse # CDP mouse operations
35
- npm run test:cdp-keyboard # CDP keyboard operations
36
- npm run test:cdp-page # CDP page operations
37
-
38
- # Run single test with pattern
39
- npm test -- --testNamePattern="cdp_click"
40
- ```
41
-
42
- ### Development Setup
43
- ```bash
44
- # Install dependencies
45
- npm install
46
-
47
- # Kill existing processes on port
48
- pkill electron
49
- ```
50
-
51
- ## Architecture
52
-
53
- ### Core Components
54
-
55
- **main.js** - Main application file containing:
56
- - `ElectronMcpServer` class - Core MCP server implementation
57
- - Tool registration system using `registerTool()` method
58
- - HTTP server with SSE (Server-Sent Events) transport
59
- - Authentication token management
60
- - Network monitoring and resource capture system
61
-
62
- **start-mcp.js** - Server launcher with:
63
- - Port management and conflict resolution
64
- - Background process spawning
65
- - Logging system (~/logs/electron-mcp.log)
66
- - Process lifecycle management
67
-
68
- **snapshot-utils.js** - Screenshot utilities:
69
- - Page capture with automatic macOS scaling
70
- - Clipboard integration
71
- - MCP-compatible image format conversion
72
-
73
- ### MCP Tool Categories
74
-
75
- 1. **Window Management**: `get_windows`, `open_window`, `close_window`, `load_url`, `get_title`
76
- 2. **Code Execution**: `invoke_window`, `invoke_window_webContents`, `invoke_window_webContents_debugger_cdp`
77
- 3. **CDP Mouse**: `cdp_click`, `cdp_double_click`
78
- 4. **CDP Keyboard**: `cdp_press_key`, `cdp_type_text`, `cdp_press_key_enter`, etc.
79
- 5. **CDP Page**: `cdp_scroll`, `cdp_find_element`, `cdp_execute_script`, `cdp_get_page_title`
80
- 6. **Screenshots**: `webpage_screenshot_to_clipboard`
81
- 7. **System**: `ping`
82
-
83
- ### Authentication System
84
-
85
- - Auto-generates authentication tokens stored in `~/data/electron/token.txt`
86
- - All HTTP requests require `Authorization: Bearer <token>` header
87
- - Token validation in `validateAuth()` method
88
-
89
- ### Network Monitoring
90
-
91
- The server automatically captures network resources when pages load:
92
- - Monitors Network.* CDP events
93
- - Saves resources by type: html, json, js, css, images
94
- - Organizes by domain in `~/data/captured_data/`
95
- - Prettifies JSON and code content
96
-
97
- ## Key Implementation Details
98
-
99
- ### Tool Registration Pattern
100
- ```javascript
101
- this.registerTool(
102
- "tool_name",
103
- "Description in Chinese",
104
- { param: z.string().describe("Parameter description") },
105
- async ({ param }) => {
106
- // Implementation
107
- return { content: [{ type: "text", text: "Result" }] };
108
- }
109
- );
110
- ```
111
-
112
- ### Code Execution Context
113
- - `invoke_window`: Access to `win` (BrowserWindow) and `webContents` objects
114
- - `invoke_window_webContents`: Access to `webContents` and `win` objects
115
- - `invoke_window_webContents_debugger_cdp`: Access to `debuggerObj`, `webContents`, `win`
116
-
117
- ### Error Handling
118
- - All tools return `{ isError: true }` for failures
119
- - Comprehensive error messages with stack traces
120
- - Parameter validation using Zod schemas
121
-
122
- ### Test Architecture
123
- - Uses Jest with supertest for HTTP API testing
124
- - SSE connection management for real-time communication
125
- - Comprehensive CDP tool coverage (33 test cases)
126
- - Automatic Electron process lifecycle management
127
- - Authentication token integration
128
-
129
- ## Common Patterns
130
-
131
- ### Adding New Tools
132
- 1. Use `registerTool()` in the `setupTools()` method
133
- 2. Follow the established parameter validation pattern with Zod
134
- 3. Add corresponding test cases in `tests/api.test.js`
135
- 4. Update documentation
136
-
137
- ### CDP Operations
138
- - Always check if debugger is attached: `debuggerObj.isAttached()`
139
- - Attach with version: `debuggerObj.attach('1.3')`
140
- - Use `sendCommand()` for CDP protocol calls
141
- - Handle async operations with proper await
142
-
143
- ### Window Management
144
- - Window IDs are used consistently across all window operations
145
- - Default window ID is 1 when not specified
146
- - Always validate window existence before operations
147
-
148
- ## Environment Variables
149
-
150
- - `PORT`: Server port (default: 8101)
151
- - `TEST`: Enable test mode with auto-window creation
152
- - `URL`: Default URL for test window
153
- - `DISPLAY`: X11 display for headless environments
154
- - `NODE_ENV`: Environment mode (test/development/production)
155
-
156
- ## File Structure Notes
157
-
158
- - Main server logic in `main.js` (1141 lines)
159
- - Comprehensive test suite in `tests/api.test.js` (712 lines)
160
- - Utility functions separated in `snapshot-utils.js`
161
- - Process management in `start-mcp.js`
162
- - All dependencies managed through `package.json`
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ `cicy-desktop` is an Electron app that exposes ~50 system tools (Chrome control, clipboard, screenshot, shell exec, system info, ...) over MCP and REST/RPC. The app runs in two roles — **worker** (the Electron process exposing tools) and **master** (a thin control plane that routes tool calls across workers) — and ships a bundled `cicy-code` sidecar daemon so the desktop is a fully offline-capable backend.
6
+
7
+ ## Development workflow rules (read first)
8
+
9
+ **This repo is edited in exactly one place** — the Linux dev machine. Mac is a runtime mirror for macOS-side validation; Windows is a separate runtime mirror that always rides the production CF Worker. There are **two iteration loops**: a fast one for React UI work (Mac, HMR) and a slow one for packaged-build validation. Pick whichever matches what you're touching.
10
+
11
+ ### Platform routing
12
+
13
+ | Role | Where the SPA comes from | When to use |
14
+ |---|---|---|
15
+ | **Mac dev** | Vite dev server on Mac (`localhost:8173`) loaded by source-mode Electron | Loop A — any edit to `workers/render/src/**` (React UI), HMR live |
16
+ | **Mac packaged** | bundled `file://src/backends/homepage-react/` inside the .app | Loop B — release-shaped validation (main-process / preload / IPC changes) |
17
+ | **Windows** | remote `https://desktop.cicy-ai.com/` (CF Worker `desktop-render`) | Always. Win NSIS package + electron-updater auto-pulls newer releases. SPA changes ship via `wrangler deploy`, no rebuild needed |
18
+
19
+ Linux never runs Electron. Linux never serves the SPA to Mac. Edits + commits + the CF Worker deploy all happen here; the Mac and Win machines are pure runtime mirrors.
20
+
21
+ ### Loop A — fast (Mac-native Vite + source-mode Electron)
22
+
23
+ Use this for anything in `workers/render/src/` (React UI, CSS, App.jsx). React + Vite **HMRs** without restarting Electron. No SSH tunnel involved — Vite and Electron both run on Mac, so the URL is genuinely local.
24
+
25
+ ```bash
26
+ # 1. Linux dev: sync source to Mac
27
+ rsync -avz --delete \
28
+ --exclude=node_modules --exclude=dist --exclude=.git \
29
+ ~/projects/cicy-desktop/ mac:~/projects/cicy-desktop/
30
+
31
+ # 2. Mac: start the Vite dev server (first time also: `npm install` in workers/render)
32
+ ssh mac
33
+ cd ~/projects/cicy-desktop/workers/render && nohup npm run dev > /tmp/vite-dev.log 2>&1 &
34
+ # Vite listens on localhost:8173
35
+
36
+ # 3. Mac: kill any installed .app then run Electron from source
37
+ # .env.dev already sets CICY_HOMEPAGE_URL=http://localhost:8173, so
38
+ # source-mode Electron loads the live Vite bundle (HMR enabled) instead
39
+ # of the bundled file:// one.
40
+ pkill -f "MacOS/CiCy Desktop" 2>/dev/null
41
+ cd ~/projects/cicy-desktop && npm install # first time only
42
+ nohup bash -c 'set -a; . ./.env.dev; set +a; npm start' > /tmp/cicy-desktop-dev.log 2>&1 &
43
+ ```
44
+
45
+ Now: edit `workers/render/src/App.jsx` on Linux → `rsync` to Mac → HMR picks it up instantly. **No Electron restart needed** for React/CSS changes.
46
+
47
+ For continuous syncing during a session, run a one-shot rsync after each save, or set up `fswatch` / IDE-side sync. Whatever you do, the source of truth stays on Linux.
48
+
49
+ ### Loop B — packaged build (only for release validation)
50
+
51
+ ```bash
52
+ # Linux: full sync (same as Loop A step 1)
53
+ rsync -avz --delete \
54
+ --exclude=node_modules --exclude=dist --exclude=.git \
55
+ ~/projects/cicy-desktop/ mac:~/projects/cicy-desktop/
56
+
57
+ # Mac: build the .app (electron-builder won't overwrite a running one)
58
+ ssh mac
59
+ cd ~/projects/cicy-desktop
60
+ pkill -f "MacOS/CiCy Desktop" 2>/dev/null
61
+ CICY_CODE_BIN_PATH=<path-to-cicy-code-binary> npm run build:mac
62
+ open "dist/mac/CiCy Desktop.app"
63
+ ```
64
+
65
+ ### Windows side — CF Worker is the SPA delivery path
66
+
67
+ Win never runs Vite or source-mode Electron. The Win NSIS package's main process loads `https://desktop.cicy-ai.com/`, which is the `desktop-render` Worker on Cloudflare serving `workers/render/dist/`. To ship a SPA change to Win users without releasing a new desktop package:
68
+
69
+ ```bash
70
+ # Linux dev: build + deploy SPA
71
+ cd ~/projects/cicy-desktop/workers/render
72
+ npm run build
73
+ CLOUDFLARE_ACCOUNT_ID=$(jq -r .cf.prod.account_id ~/cicy-ai/global.json) \
74
+ CLOUDFLARE_API_TOKEN=$(jq -r .cf.prod.api_token ~/cicy-ai/global.json) \
75
+ npx wrangler deploy
76
+ # Also mirror into the file:// folder for Mac packaged builds
77
+ rsync -av --delete dist/ ../../src/backends/homepage-react/
78
+ ```
79
+
80
+ Win users see the new SPA on next desktop relaunch (no auto-reload — they have to restart cicy-desktop). For main-process / preload changes Win needs an actual NSIS rebuild + electron-updater push (see `## Build & distribute`).
81
+
82
+ ### Loop A failure modes
83
+
84
+ The Mac-native loop has no SSH tunnel to drop, but it has three other failure shapes:
85
+
86
+ 1. **Vite died** (terminal closed, OOM, port conflict)
87
+ ```bash
88
+ ssh mac "curl -sI http://localhost:8173/ -m 4 | head -1"
89
+ # Connection refused → Vite down. Restart with step 2 of Loop A.
90
+ ```
91
+
92
+ 2. **The bundled .app is running instead of source-mode Electron** (it'll happily load Vite at 8173 if `.env.dev` is sourced, but more often Mac auto-launches the .app and you forget to kill it)
93
+ ```bash
94
+ ssh mac 'ps -ef | grep "MacOS/CiCy Desktop" | grep -v grep'
95
+ # If you see /Applications/CiCy Desktop.app/... — that's the packaged one.
96
+ # pkill -f "MacOS/CiCy Desktop" then re-run Loop A step 3.
97
+ ```
98
+
99
+ 3. **Electron loaded a stale URL** (`.env.dev` not sourced, fallback to `file://` or `desktop.cicy-ai.com`). Inspect the live URL over CDP:
100
+ ```bash
101
+ ssh mac 'curl -s http://127.0.0.1:9221/json | python3 -c "
102
+ import sys, json
103
+ for t in json.load(sys.stdin):
104
+ print(t.get(\"type\"), t.get(\"url\",\"\")[:80])"'
105
+ # If url is anything other than http://localhost:8173/, source-mode wasn't
106
+ # picked up cleanly. Re-export CICY_HOMEPAGE_URL and restart Electron.
107
+ ```
108
+
109
+ ### The three reload classes (the trap that wastes hours)
110
+
111
+ Electron has three independent execution contexts. **Each has its own reload rule**, and they don't share. Knowing which class your file belongs to is the difference between a 2-second iteration and a 30-second one.
112
+
113
+ | Class | Files | Reload trigger |
114
+ |---|---|---|
115
+ | **Vite render** | `workers/render/src/**` App.jsx, App.css, any imported JS | ✅ **HMR**. Save → instant. No Electron restart. |
116
+ | **Preload** | `src/backends/homepage-preload.js`, `src/backends/webview-preload.js` | ❌ **Full Electron restart**. Preloads load once at BrowserWindow creation. `⌘+R`/devtools reload does NOT re-read them. |
117
+ | **Main process** | `src/main.js`, `src/backends/*.js` required by main (e.g. `local-teams.js`), IPC handler registration, any tool module | ❌ **Full Electron restart**. Main runs in Node and is never reloaded by the renderer. |
118
+
119
+ **The silent failure pattern**: React (Vite) HMRs to a new App.jsx that calls `window.cicy.someNewField`. If the **preload** still exposes the old surface, `someNewField` is `undefined` and your code paths silently misbehave. Always check that preload changes have actually landed by inspecting `window.cicy` over CDP (see [Debugging](#debugging-via-remote-debugging-port-9221)).
120
+
121
+ ### Where edits go
122
+
123
+ - **Edit only on Linux** at `~/projects/cicy-desktop`. Never `ssh mac` to edit `src/...` — that creates two-master divergence (the kind of mess earlier rebases had to clean up).
124
+ - **Commits / pushes from Linux.** Mac is a working-tree mirror; nothing committed there should be the source of truth.
125
+ - **Windows builds: GitHub Actions only** (see `.github/workflows/build-windows*.yml`). Don't try local Windows builds.
126
+
127
+ ## Common commands
128
+
129
+ ### Install and run
130
+
131
+ ```bash
132
+ npm install
133
+ npm start # local Electron worker via bin/cicy-desktop
134
+ npm run start:master # control-plane master on port 8100
135
+ ```
136
+
137
+ ### Formatting
138
+
139
+ ```bash
140
+ npm run format
141
+ npm run format:check
142
+ ```
143
+
144
+ ### Tests
145
+
146
+ ```bash
147
+ npm test # full suite
148
+ npx jest --runInBand tests/rpc/master-routes.test.js # single file
149
+ npx jest --runInBand --testNamePattern="Master routes" tests/rpc/master-routes.test.js
150
+ ```
151
+
152
+ `jest.config.js` runs single-process (`maxWorkers: 1`) with `forceExit: true`. Tests under `tests/rpc/` spin up the real Electron worker or supertest HTTP routes, so they take real time and are not pure unit tests.
153
+
154
+ ### Build & distribute
155
+
156
+ ```bash
157
+ npm install
158
+ npm run build # multi-platform via electron-builder
159
+ npm run build:mac # dist/CiCy Desktop-<ver>.dmg + .zip + dist/mac/CiCy Desktop.app
160
+ npm run build:win # NSIS installer
161
+ npm run build:linux # deb + AppImage
162
+ ```
163
+
164
+ Dev iteration loop on macOS:
165
+
166
+ 1. edit `src/...`
167
+ 2. `pkill -f "MacOS/CiCy Desktop"` (electron-builder won't overwrite a running app)
168
+ 3. `CICY_CODE_BIN_PATH=<path-to-cicy-code-binary> npm run build:mac`
169
+ 4. `open "dist/mac/CiCy Desktop.app"`
170
+ 5. inspect the packaged renderer via `npx --yes asar extract-file "dist/mac/CiCy Desktop.app/Contents/Resources/app.asar" <path>`
171
+
172
+ ### RPC CLI workflows
173
+
174
+ ```bash
175
+ ./bin/cicy-rpc init
176
+ ./bin/cicy-rpc tools
177
+ ./bin/cicy-rpc ping
178
+ CICY_NODE=windows ./bin/cicy-rpc ping
179
+ CICY_NODE=windows ./bin/cicy-rpc chrome_launch_profile accountIdx=1 url=https://example.com/
180
+ ```
181
+
182
+ Distinctions to internalize:
183
+
184
+ - `cicy` / `cicy-desktop` — local worker lifecycle (start/stop/status). **Not** for tool calls.
185
+ - `cicy-rpc` — tool invocation. Reads `~/global.json`. Choose remote node via `CICY_NODE=<name>`.
186
+
187
+ ## Architecture
188
+
189
+ Two runtime roles:
190
+
191
+ 1. **Worker** — an Electron process exposing tools over MCP and REST/RPC
192
+ 2. **Master** — a thin control plane that tracks workers/agents/tasks and forwards `/api/rpc/:toolName` calls to a selected worker
193
+
194
+ A single host can run either or both. The bundled `.app` is normally a worker; running `npm run start:master` adds the master role.
195
+
196
+ ### Worker runtime
197
+
198
+ Entrypoint: `src/main.js`.
199
+
200
+ Initializes Electron flags, auth, logging, Express, MCP plumbing, loads tool modules through `src/server/tool-catalog.js`, and registers every tool into both the MCP server and the REST/RPC surface. Registers + heartbeats to a master when `CICY_MASTER_URL` and `CICY_MASTER_TOKEN` are present.
201
+
202
+ Supporting modules:
203
+
204
+ - `src/server/express-app.js` — base Express app, CORS, `/ping`, `/docs`, `/openapi.json`, UI shell routes
205
+ - `src/server/mcp-server.js` — MCP transport setup
206
+ - `src/server/tool-registry.js` — tool registration bridge
207
+ - `src/server/tool-executor.js` — central execution path for REST/MCP tool calls
208
+ - `src/cluster/worker-client.js` — worker registration + heartbeat to the master
209
+ - `src/cluster/worker-identity.js` — identity payload advertised to the master
210
+
211
+ Worker RPC surface:
212
+
213
+ - `GET /rpc/tools` — list registered tools
214
+ - `POST /rpc/tools/call` — `{ name, arguments }` style invocation
215
+ - `POST /rpc/:toolName` — direct REST entrypoint, what `cicy-rpc` uses after resolving the node
216
+
217
+ ### Tool system
218
+
219
+ Tool implementations live in `src/tools/*.js` and are loaded via `require("../tools")` from `src/server/tool-catalog.js`. Each module exports a function receiving `registerTool(name, description, schema, handler, options)`. The catalog is grouped by `tag` and reused for:
220
+
221
+ - MCP tool registration
222
+ - `GET /rpc/tools`
223
+ - OpenAPI generation in `/openapi.json`
224
+
225
+ A tool definition change affects all three surfaces at once.
226
+
227
+ Tools use **CommonJS** and **Zod schemas**.
228
+
229
+ ### Master runtime
230
+
231
+ Entrypoint: `src/master/master-main.js`.
232
+
233
+ In-memory state:
234
+
235
+ - `WorkerRegistry` — live registered workers
236
+ - `WorkerInventory` — merged view of configured nodes from `~/global.json` plus registered workers
237
+ - `AgentIndex` — worker agent metadata
238
+ - `TaskStore` — forwarded task records
239
+ - `SessionAffinityStore` — control-session routing affinity
240
+
241
+ Master routes split into:
242
+
243
+ - `src/master/master-routes.js` — public API under `/api`
244
+ - `src/master/master-admin-routes.js` — admin-only routes under `/admin`
245
+
246
+ The hot path is `POST /api/rpc/:toolName`:
247
+
248
+ 1. build request context from `workerId`, `agentId`, runtime session, control session, `accountIdx`
249
+ 2. choose an execution target with `src/master/task-scheduler.js`
250
+ 3. create a task record in `TaskStore`
251
+ 4. inject worker-specific fields (`win_id`, `agentId`, `runtimeSessionId`, `effectiveChromeProfile`)
252
+ 5. forward to the selected worker via `src/cluster/remote-executor`
253
+ 6. store completion/failure state
254
+
255
+ ### Chrome profile dispatch
256
+
257
+ Chrome profile handling is split between master and worker. The source-of-truth `chrome.json` lives on the **master** at `~/cicy-ai/db/chrome.json`; workers don't need a local one.
258
+
259
+ Master-side:
260
+
261
+ - `src/master/chrome-config.js` reads master-local `~/cicy-ai/db/chrome.json`
262
+ - `src/master/master-routes.js` injects `effectiveChromeProfile` for forwarded chrome tool calls when `accountIdx` is present
263
+ - injection covers `chrome_launch_profile`, `chrome_get_profile`, `chrome_get_targets`, `chrome_cdp_call`
264
+
265
+ Worker-side launch (`src/tools/chrome-tools.js::chrome_launch_profile`):
266
+
267
+ - prefers injected `effectiveChromeProfile`
268
+ - falls back to local `~/cicy-ai/db/chrome.json` for backward compat
269
+ - if neither exists → clear error
270
+ - if target user-data-dir doesn't exist → initialize from `~/chrome/_tmp` (or just `mkdir`)
271
+ - `orgPath -> Default` copy is best-effort if the path exists
272
+
273
+ Chrome internals separated:
274
+
275
+ - `src/chrome/chrome-launcher.js` — binary resolution, args, spawn, debugger readiness, per-profile proxy via `--proxy-server=<url>`
276
+ - `src/chrome/chrome-cdp-client.js` — `/json/version`, `/json/list`, activation, generic CDP calls
277
+ - `src/chrome/runtime-registry.js` — local runtime state per account
278
+ - `src/chrome/debugger-port-resolver.js` — port assignment
279
+
280
+ Cross-platform Chrome discovery (`chrome-launcher.js::getBinaryCandidates`):
281
+
282
+ - macOS: `/Applications/Google Chrome.app`, `~/Applications/Google Chrome.app`, `/Applications/Chromium.app`
283
+ - Windows: `%LOCALAPPDATA%` / `%PROGRAMFILES%` / `%PROGRAMFILES(X86)%` under `Google\Chrome\Application\chrome.exe` (+ Chromium variants)
284
+ - Linux: `google-chrome` / `chromium` / `chromium-browser` from PATH
285
+
286
+ When none are present, launch errors with `"Chrome/Chromium binary not found"` — user must install Chrome first.
287
+
288
+ ### Homepage UI (Vite + React subproject)
289
+
290
+ The first window's UI is a **Vite + React subproject** at `workers/render/`, **not** the older `src/backends/homepage.html`. Treat them as independent codebases that happen to live in the same repo.
291
+
292
+ - entry: `workers/render/src/App.jsx` + `App.css`
293
+ - dev server: `workers/render/vite.config.js` → `0.0.0.0:8173`
294
+ - prod bundle: `workers/render/dist/index.html` (Electron loads via `file://` fallback when `CICY_HOMEPAGE_URL` is unset — see `src/backends/homepage-window.js:pickHomepageURL`)
295
+ - BrowserWindow config (`src/backends/homepage-window.js`):
296
+ - `preload: src/backends/homepage-preload.js`
297
+ - `webviewTag: true` + `allowRunningInsecureContent: true` (the right-side Team Helper drawer is a `<webview>` loading a remote http:// SPA)
298
+ - `sandbox: false` + `contextIsolation: true`
299
+
300
+ Day-to-day UI work happens entirely here. The Linux dev machine runs `npm run dev` in this subproject; the Mac's Electron loads from it via `CICY_HOMEPAGE_URL=http://localhost:8173` (see [Loop A](#loop-a--fast-vite--ssh--r--electron-from-source)).
301
+
302
+ ### Preload bridges — `homepage-preload.js` vs `webview-preload.js`
303
+
304
+ Two distinct preload files because they run in different webContents with different security needs:
305
+
306
+ `src/backends/homepage-preload.js` — loaded into the **main BrowserWindow**'s renderer (the Vite/React UI). Exposes the full host surface the React app needs:
307
+
308
+ - `window.electronRPC(tool, args)` — generic dispatch into the worker tool registry (any tool from `src/tools/*.js`)
309
+ - `window.cicy.localTeams.{list, open, add, remove, update, upgrade, onWebviewRelay, replyWebviewRelay}`
310
+ - `window.cicy.cloud.fetch(url, opts)` — main-process `fetch` proxy (sidesteps CORS for `cicy-ai.com` calls; renderer's `localhost:8173` / `file://` origins aren't on the cloud's CORS allowlist)
311
+ - `window.cicy.auth.{loginStart, loginCancel, onComplete}` — browser-loopback login flow (`src/backends/auth-loopback.js`)
312
+ - `window.cicy.app.*`, `window.cicy.windows.*`, `window.cicy.shell.openExternal`, etc.
313
+ - `window.cicy.preloadPath` (legacy) and `window.cicy.webviewPreloadPath` — absolute paths the React code reads to wire the right-drawer `<webview preload={...}>`
314
+
315
+ `src/backends/webview-preload.js` — loaded into the **right-drawer `<webview>`** that hosts the cloud Team Helper SPA. Deliberately **TINY** because the webview loads a remote (third-party) SPA:
316
+
317
+ - `window.electronRPC(tool, args)` — same generic dispatch (the cloud helper's `agent-desktop` skill needs it to run shell commands on the user's machine)
318
+ - `window.cicy.localTeams.{list, add, remove, update, upgrade}` — all five go through `webview:relay` (next section), not directly to main
319
+
320
+ We don't reuse `homepage-preload.js` here because (1) it `require()`s non-electron modules (`../i18n`) that throw in the webview's sandboxed context, half-killing the preload before any contextBridge runs, and (2) exposing `cicy.backends.*` / `cicy.sidecar.*` / `cicy.auth.*` to a cloud SPA is unnecessary attack surface.
321
+
322
+ ### `webview:relay` — webview ↔ host renderer authority pattern
323
+
324
+ The Team Helper webview can't be allowed to mutate `~/cicy-ai/global.json` directly — that's the host renderer's UX decision. So `webview-preload.js`'s `cicy.localTeams.*` methods relay through main to the host renderer (App.jsx), wait for its reply, and return the result to the webview's awaited promise.
325
+
326
+ ```
327
+ webview main process host renderer (App.jsx)
328
+ │ │ │
329
+ ipcRenderer.invoke │ │
330
+ ("webview:relay", msg) │ │
331
+ ──────────────────────────────▶ ipcMain.handle │
332
+ │ │
333
+ host.send │
334
+ ("webview:relay", │
335
+ {reqId, msg}) │
336
+ ─────────────────────────▶ onWebviewRelay handler
337
+
338
+ await window.cicy.localTeams.add(spec)
339
+ fetchLocalTeams() ← UI refresh
340
+
341
+ ipcRenderer.send
342
+ ◀────────────────────────── ("webview:relay-reply",
343
+ {reqId, result})
344
+ resolve(result) │
345
+ ◀────────────────────────────── │ │
346
+ promise resolves │ │
347
+ ```
348
+
349
+ 15s timeout in main if the host renderer never replies. The host renderer is the only place that actually calls `localTeams:add/remove/update/upgrade` IPCs against `local-teams.js`. This keeps add/remove/upgrade authoritative for the UX (it can confirm/deny + refresh state) while still giving the webview real awaitable promises.
350
+
351
+ ### Team Helper drawer (cloud SPA in `<webview>`)
352
+
353
+ The right-side drawer in App.jsx hosts a cloud-trial agent that walks new users through installing a local `cicy-code` backend, then hands them off to their own local helper:
354
+
355
+ - `HELPER_URL_BASE` (App.jsx constant): URL of the cloud helper container — currently `http://43.99.56.150:8011`. The container is built from `cicy-cloud/workers/helper/` (separate repo).
356
+ - `HELPER_SHARED_TOKEN`: the cloud container's `api_token`. Regenerated on every container restart; must be re-pasted into App.jsx whenever the cloud helper is rebuilt.
357
+ - `HELPER_PANE_ID = "w-6002:main.0"` — the `Team Helper` opencode pane the cloud `cicy-code --helper=1` mode pins.
358
+
359
+ The webview `src` is `${HELPER_URL_BASE}/?token=${token}#/agent/w-6002`. Once `agent-webpage helper-init` returns the user's OS / arch / network reachability, the cloud agent downloads `cicy-code` to the user's machine and registers the new team via `await window.cicy.localTeams.add({...install_source: "helper-mac-linux"...})`. App.jsx detects `install_source` starting with `helper-` and **auto-swaps `helperUrl`** 2.5 s later to `<new team base_url>/?token=...#/agent/w-6002`. From that point the drawer is the user's own long-lived local Team Helper — no 30-min cap, same task surface (install / upgrade / token-rotate / remove / open).
360
+
361
+ The "send `start`" centered modal in the drawer is a manual fallback for when the server-side helper-kick goroutine (cicy-code's `watchHelperOpencodeReadyAndKick`) didn't fire — e.g. the user reopened the drawer too quickly. Local-storage key `helper_modal_suppressed` records "Don't show again".
362
+
363
+ #### Helper token rotation workflow
364
+
365
+ Every cloud-helper rebuild generates a fresh `api_token`. `HELPER_SHARED_TOKEN` in App.jsx must be updated to match, otherwise the drawer's `<webview src=…?token=…>` and the renderer's `cloud.fetch` calls 401 against the helper. Standard loop (already proven against both local-Docker and the remote `43.99.56.150` helper, which accepts the same token):
366
+
367
+ ```bash
368
+ # 1. Grab the new token straight out of the helper container's global.json
369
+ docker exec cicy-helper grep api_token /home/cicy/cicy-ai/global.json
370
+ # "api_token": "cicy_XXXXXXXX…",
371
+
372
+ # 2. Paste it into workers/render/src/App.jsx HELPER_SHARED_TOKEN
373
+ # Vite HMR's the constant change into the running renderer immediately,
374
+ # no Electron restart needed for THIS step (the webview keys off helperUrl
375
+ # so it remounts with the new token).
376
+
377
+ # 3. rsync to Mac so its source matches Linux (vite-dev tunnel still works,
378
+ # but explicit sync prevents drift if you later switch loops).
379
+ rsync -avz --delete \
380
+ --exclude=node_modules --exclude=dist --exclude=.git \
381
+ --exclude=workers/render/node_modules --exclude=workers/render/dist \
382
+ ~/projects/cicy-desktop/ mac:~/projects/cicy-desktop/
383
+ ```
384
+
385
+ Only the **token** HMRs cleanly. If you also rebuilt the helper image with new AGENTS.md / new preload-relevant code, the cloud SPA in the webview is fine to reload (it's served by the container), but anything on the Electron side (homepage-preload, webview-preload, main, local-teams.js) is a full `⌘+Q` + reopen as usual.
386
+
387
+ `HELPER_URL_BASE` (App.jsx) is the **other** half of the pairing. It currently points at `http://43.99.56.150:8011` (a long-running shared helper). If you want to swap to a locally rebuilt container, change it to `http://localhost:8011` and `ssh -fNR 8011:127.0.0.1:8011 mac` so the Mac can reach your dev box's helper. Same token-grab step still applies.
388
+
389
+ ### Backends launcher (legacy `src/backends/`)
390
+
391
+ `src/backends/homepage.html` + `cicy.backends.{list, add, remove, …}` is the **pre-Vite** launcher. It still ships and still works (the bundled sidecar and Add-by-URL flow live here), but it is **not** what new users see — `pickHomepageURL` prefers the Vite/React entry. Touch this only when you're working on the old launcher path. Registry file: `<userData>/backends.json` (`src/backends/registry.js`). IPC handlers: `src/backends/ipc.js`.
392
+
393
+ ### Trust gate (`isTrustedUrl`)
394
+
395
+ `src/utils/window-utils.js::isTrustedUrl(url)` decides whether a `BrowserWindow` gets:
396
+
397
+ - `nodeIntegration: true`
398
+ - `contextIsolation: false`
399
+ - the `dom-ready` `electronRPC` auto-injection
400
+
401
+ Trusted hosts:
402
+
403
+ 1. `localhost` / `127.0.0.1`
404
+ 2. `*.de5.net`
405
+ 3. **any hostname in `backends.json`** — anything the user added via the Add form (v2.0.2 widening)
406
+
407
+ Effect for renderers loading a trusted URL: `window.electronRPC(toolName, args)` is a function round-tripping through `ipcRenderer.invoke("rpc", toolName, args)` into the worker's tool registry.
408
+
409
+ ### Bridge to cicy-code (`desktop_event` / `rpc_call`)
410
+
411
+ When this app opens a cicy-code backend, the server-side `agent-desktop` and `agent-chrome` skills reach Electron-main tools through cicy-code's chat WebSocket — **not** through this app's REST/RPC surface.
412
+
413
+ Flow:
414
+
415
+ 1. cicy-code server posts `POST /api/chat/push` with `{ type: "desktop_event", data: { type: "rpc_call", tool, args, requestId } }`
416
+ 2. cicy-code relays to the connected client over WebSocket
417
+ 3. cicy-code's React app (`app/src/components/layout/useDesktopEvents.ts`) listens for `desktop_event`, sees `type === "rpc_call"`, awaits `window.electronRPC(tool, args)` — the same function the trust gate exposes
418
+ 4. result dispatched as `rpc-result` CustomEvent → relayed back through the WS by `Workspace.tsx`
419
+ 5. server-side skill (`hosttools.go::desktopRPC`) matches by `requestId` and returns
420
+
421
+ Why this exists: `agent-webpage exec-js` runs synchronously via `window.eval` and cannot await Promises, so it can't call `electronRPC` directly. `rpc_call` is the async-safe sibling.
422
+
423
+ Implication: anything that calls `window.electronRPC` from outside the cicy-code React tree must run inside a renderer where `isTrustedUrl` granted `nodeIntegration`, or where `homepage-preload.js` is the preload.
424
+
425
+ ### On-disk layout — `~/.local/bin/cicy-code` symlink → versioned binary
426
+
427
+ Both the in-app installer (`src/sidecar/installer.js`) and the cloud Team Helper agent write the daemon into the user's `~/.local/bin/` with this shape:
428
+
429
+ ```
430
+ ~/.local/bin/cicy-code-2.1.8 (actual binary, +x)
431
+ ~/.local/bin/cicy-code-2.1.9 (next version after upgrade)
432
+ ~/.local/bin/cicy-code (symlink → cicy-code-2.1.9, atomic-swapped on upgrade)
433
+ ```
434
+
435
+ Rationale:
436
+
437
+ - **Atomic upgrade**: `ln -sfn cicy-code-<new> ~/.local/bin/cicy-code` is a POSIX-atomic relink. A long-running daemon spawned via the symlink keeps its current inode open; future re-spawns pick up the new target.
438
+ - **Rollback**: old versioned binaries stay on disk. Rolling back is one symlink swap.
439
+ - **Version-from-disk**: `fs.readlinkSync(~/.local/bin/cicy-code)` and parsing the basename gives the current version — no separate `version` file to keep in sync. Legacy fallback to a `<binDir>/version` file is kept in `installer.userVersion()` for older installs.
440
+
441
+ Upgrade flow inside `src/backends/local-teams.js::upgradeNative`:
442
+
443
+ 1. `fetchManifestVersion()` learns the upcoming version (so the download filename is `cicy-code-<ver>` from the start).
444
+ 2. `downloadFile(directURL → mirrorURL, ~/.local/bin/cicy-code-<ver>)`.
445
+ 3. `chmod 0o755`.
446
+ 4. `--version` round-trip verifies the bytes; if mirror served stale, rename the file onto the real version.
447
+ 5. `pkill -f ~/.local/bin/cicy-code` (and the previously stored `install_path` if it differs) kills the old daemon.
448
+ 6. `ln -sfn cicy-code-<ver> ~/.local/bin/cicy-code` (written at a tmp name + renamed = atomic).
449
+ 7. Re-spawn via the symlink (`spawn(linkPath, [], { detached: true })`).
450
+ 8. `waitForHealth(/api/health)` up to 30 s; on success, `updateTeam(id, {install_path: linkPath})` so older team rows that stored a versioned path migrate to the symlink.
451
+
452
+ The cloud helper agent uses the **same layout** when it does the initial install (`AGENTS.md` step 1A.2/1A.3 in `cicy-cloud/workers/helper/`). It writes `cicy-code-<ver>` + `ln -sfn` + spawns via the symlink, then registers the team with `install_path=~/.local/bin/cicy-code`. This way the agent's install path and the desktop's upgrade path are identical — no special-case wiring needed.
453
+
454
+ ### Sidecar cicy-code daemon — **NOT bundled** (2026-05-29 principle)
455
+
456
+ `cicy-desktop` no longer ships a `cicy-code` binary in the `.app`. The daemon is acquired one of three ways:
457
+
458
+ 1. **Already running on `:8008`** — left over from a previous session, started by the user, or installed by the cloud Team Helper. `src/sidecar/cicy-code.js::probeExisting` detects it and `start()` reuses without re-spawning.
459
+ 2. **In-app installer** — `src/sidecar/installer.js` downloads the platform-matching binary from `cicy-ai/cicy-code` GitHub releases into `<userData>/cicy-code/<platform>-<arch>/cicy-code`. Triggered from the homepage when no daemon is running. `userBinary()` returns this path; `bundledBinaryPath()` only checks this single source — there is intentionally no `<App>/Contents/Resources/cicy-code` fallback.
460
+ 3. **Cloud Team Helper** — the trial helper container walks the user through installing cicy-code on their own machine, then registers it via `window.cicy.localTeams.add({...})`. Today the helper writes to `~/Downloads/cicy-code`; the in-app installer location is preferred (`<userData>/cicy-code/...`) so `userBinary()` discovers it on the next desktop launch with no extra wiring.
461
+
462
+ Removed in this principle change:
463
+ - `package.json` no longer has `extraResources` entries for `vendor/cicy-code/*/cicy-code`
464
+ - `package.json` no longer runs `prepare:sidecar` in `prebuild*`
465
+ - `scripts/prepare-cicy-code-sidecar.js` is dormant (kept for reference; can be deleted)
466
+ - `vendor/cicy-code/` directory is no longer touched by the build
467
+
468
+ `sidecar/cicy-code.js::start()` therefore returns `null` whenever no `userBinary()` is present AND no daemon is on `:8008`. The homepage's Team Helper card is the surface that gets the user from "no daemon" to "daemon running" — either by triggering the in-app installer (for the legacy single-click path) or by walking the cloud-helper onboarding flow.
469
+
470
+ Windows path is unchanged — `src/sidecar/wsl.js` runs the daemon inside WSL2 because cicy-code is POSIX-only.
471
+
472
+ #### What broke that motivated the principle (2026-05-29)
473
+
474
+ Bundled `cicy-code` was pinned at `v2.1.2`. The trial helper installs `releases/latest` (now `v2.1.8`). On every cicy-desktop start the bundled `v2.1.2` raced ahead and bound `:8008`; when the helper later tried to launch `~/Downloads/cicy-code` it hit "address in use" and silently exited. Worse: `localTeams.list()` saw `:8008` healthy and added a "running" team card pointing at `w-6002`, which the `v2.1.2` daemon doesn't know how to spawn (the built-in pane only exists in `v2.1.8+`). End result: drawer swap → 404, version churn, hours wasted. The principle removes the bundled copy entirely — there's exactly one acquisition path and one source of truth at any time.
475
+
476
+ ### CLI/config split
477
+
478
+ Two CLIs, different jobs:
479
+
480
+ - `bin/cicy-desktop` / `cicy` — local worker lifecycle (start/stop/status)
481
+ - `bin/cicy-rpc` — tool invocation
482
+
483
+ `src/cli/rpc.js` (`cicy-rpc`):
484
+
485
+ - reads `~/global.json`
486
+ - resolves `cicyDesktopNodes[<name>]`
487
+ - uses `CICY_NODE` to choose the target node
488
+ - POSTs directly to `/<rpc-path>` on that node with bearer auth
489
+
490
+ `cicy-rpc init` only initializes `~/global.json` if missing. It is not a general node-management command.
491
+
492
+ ## Config and auth
493
+
494
+ ### `~/global.json`
495
+
496
+ ```json
497
+ {
498
+ "api_token": "cicy_…",
499
+ "cicyDesktopNodes": {
500
+ "mac": { "base_url": "http://127.0.0.1:8101", "api_token": "…" },
501
+ "windows": { "base_url": "http://1.2.3.4:8101", "api_token": "…" }
502
+ }
503
+ }
504
+ ```
505
+
506
+ `cicy-rpc` picks the token in this order:
507
+
508
+ 1. `cicyDesktopNodes.<name>.api_token`
509
+ 2. top-level `api_token`
510
+
511
+ ### Worker registration to master
512
+
513
+ ```bash
514
+ MASTER_TOKEN=$(jq -r '.api_token' ~/global.json)
515
+ PORT=8101 CICY_MASTER_URL="http://127.0.0.1:8100" CICY_MASTER_TOKEN="$MASTER_TOKEN" npm start
516
+ ```
517
+
518
+ The master uses `CICY_MASTER_TOKEN` directly or falls back to `MasterTokenManager`.
519
+
520
+ ## File map
521
+
522
+ | What | Where |
523
+ |---|---|
524
+ | worker startup/runtime | `src/main.js` |
525
+ | master startup/runtime | `src/master/master-main.js` |
526
+ | master forwarding | `src/master/master-routes.js` |
527
+ | `~/global.json` inventory | `src/master/worker-inventory.js` |
528
+ | RPC CLI | `src/cli/rpc.js` |
529
+ | worker tool catalog | `src/server/tool-catalog.js` |
530
+ | tool execution plumbing | `src/server/tool-executor.js` |
531
+ | Chrome tools | `src/tools/chrome-tools.js` |
532
+ | Chrome launcher | `src/chrome/chrome-launcher.js` |
533
+ | Chrome CDP helpers | `src/chrome/chrome-cdp-client.js` |
534
+ | cluster registration | `src/cluster/worker-client.js` |
535
+ | backends registry | `src/backends/registry.js` |
536
+ | backends launcher UI | `src/backends/homepage.html` |
537
+ | backends preload bridge | `src/backends/homepage-preload.js` |
538
+ | BrowserWindow trust + auto-inject | `src/utils/window-utils.js` |
539
+ | sidecar packaging | `scripts/prepare-cicy-code-sidecar.js` |
540
+ | RPC test files | `tests/rpc/master-routes.test.js`, `tests/rpc/cicy-rpc.test.js` |
541
+
542
+ ## Debugging via remote-debugging-port 9221
543
+
544
+ Electron renderers in dev are launched with `--remote-debugging-port=9221`. Use it instead of guessing.
545
+
546
+ ```bash
547
+ # Open the tunnel once per session
548
+ ssh -fNR 9221:127.0.0.1:9221 mac # if your dev → Mac
549
+ # or
550
+ ssh -fNL 9221:127.0.0.1:9221 mac # if you're on Linux looking at Mac
551
+
552
+ # Enumerate targets (homepage + every <webview>)
553
+ curl -s http://127.0.0.1:9221/json/list | jq '.[] | {type, url, webSocketDebuggerUrl}'
554
+ ```
555
+
556
+ Each target has a `webSocketDebuggerUrl`. Connect and send any `Runtime.evaluate` to inspect window state from outside Electron:
557
+
558
+ ```js
559
+ // /tmp/cdp-probe.mjs
560
+ import http from 'node:http';
561
+ const targets = await new Promise(r =>
562
+ http.get('http://127.0.0.1:9221/json/list', res => {
563
+ let b=''; res.on('data',c=>b+=c); res.on('end',()=>r(JSON.parse(b)));
564
+ }));
565
+ const target = targets.find(t => t.type === 'webview'); // or 'page' for homepage
566
+ const ws = new WebSocket(target.webSocketDebuggerUrl);
567
+ await new Promise((ok, fail) => { ws.onopen = ok; ws.onerror = fail; });
568
+
569
+ let id = 0;
570
+ function call(method, params={}) {
571
+ const reqId = ++id;
572
+ return new Promise(res => {
573
+ ws.addEventListener('message', function h(e) {
574
+ const m = JSON.parse(e.data);
575
+ if (m.id === reqId) { ws.removeEventListener('message', h); res(m); }
576
+ });
577
+ ws.send(JSON.stringify({ id: reqId, method, params }));
578
+ });
579
+ }
580
+
581
+ for (const expr of [
582
+ 'typeof window.electronRPC',
583
+ 'typeof window.cicy',
584
+ 'window.cicy && Object.keys(window.cicy.localTeams || {})',
585
+ '(window.cicy && window.cicy.webviewPreloadPath) || null',
586
+ ]) {
587
+ const r = await call('Runtime.evaluate', { expression: expr, returnByValue: true });
588
+ console.log(expr, '→', JSON.stringify(r.result?.result?.value));
589
+ }
590
+ ws.close();
591
+ ```
592
+
593
+ Typical pattern after editing a preload: this probe **must** show your new field on the homepage target before you assume the change landed. If it shows the old surface, you forgot to `⌘+Q` + reopen Electron.
594
+
595
+ For the Team Helper webview specifically: its `webSocketDebuggerUrl` lives in the same `/json/list` output, typed `webview`. Probing it confirms whether the `<webview preload={file://...}>` attribute actually loaded `webview-preload.js`.
596
+
597
+ ## Mental model checklist
598
+
599
+ When touching any of these, expect ripple effects:
600
+
601
+ - adding a tool → touches MCP server, REST/RPC, OpenAPI all at once via the catalog
602
+ - changing trust criteria → changes `nodeIntegration` for whole classes of windows; verify with the cicy-code bridge still works
603
+ - changing the homepage entry flow → check that cold-launch and "back to launcher" paths both behave (history.length / sessionStorage gates)
604
+ - changing the sidecar packaging → verify `dist/mac/CiCy Desktop.app/Contents/Resources/cicy-code/cicy-code` is present and executable after build
605
+ - changing `chrome_*` tools → both master injection (`effectiveChromeProfile`) and worker fallback (`~/cicy-ai/db/chrome.json`) paths still need to work
606
+ - changing **any preload file** → `⌘+Q` + reopen Electron is mandatory; HMR / `⌘+R` won't reload it. Confirm via CDP `Runtime.evaluate` on the target window
607
+ - changing **`src/backends/local-teams.js`** → it's `require`d by main; full Electron restart needed. Don't forget to also expose any new methods through both `homepage-preload.js` (full surface) and `webview-preload.js` (relay)
608
+ - changing the `<webview>` preload surface → also update `webview:relay` handlers in main + App.jsx so the new methods route correctly. Webview can't call IPCs directly; everything funnels through `webview:relay`
609
+ - changing the cloud Team Helper container → rebuild + restart copies a NEW `api_token`. Re-paste `HELPER_SHARED_TOKEN` in `workers/render/src/App.jsx` (HMRs) but if you also changed `HELPER_URL_BASE` you've effectively repointed the drawer — verify the new URL is reachable from the user's machine, not just yours