cicy-desktop 2.1.236 → 2.1.237

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.
package/CLAUDE.md DELETED
@@ -1,615 +0,0 @@
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 ~100+ system tools (Chrome control, CDP, clipboard, screenshot, shell/node/python exec, system info, file ops, ...) 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). It does **not** bundle a `cicy-code` binary — the sidecar daemon is acquired at runtime (`npx cicy-code` on mac/linux, Docker on Windows; see [Sidecar](#sidecar-cicy-code-daemon)).
6
-
7
- **Distribution (2026-06):** end users run via npm, not a packaged installer — `npx cicy-desktop` (mac/linux) / `npm i -g cicy-desktop && cicy-desktop` (Windows). First launch drops a desktop shortcut and auto-acquires the sidecar. CN needs `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/` so electron's binary postinstall doesn't hit GitHub. The electron-builder packaged build (dmg/NSIS) still exists but is secondary. To discover tools at runtime, call the `list_tools` meta-tool (`electronRPC("list_tools")` / `agent-desktop rpc list_tools`).
8
-
9
- ## Development workflow rules (read first)
10
-
11
- **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.
12
-
13
- ### Platform routing
14
-
15
- | Role | Where the SPA comes from | When to use |
16
- |---|---|---|
17
- | **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 |
18
- | **Mac packaged** | bundled `file://src/backends/homepage-react/` inside the .app | Loop B — release-shaped validation (main-process / preload / IPC changes) |
19
- | **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 |
20
-
21
- 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.
22
-
23
- ### Loop A — fast (Mac-native Vite + source-mode Electron)
24
-
25
- 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.
26
-
27
- ```bash
28
- # 1. Linux dev: sync source to Mac
29
- rsync -avz --delete \
30
- --exclude=node_modules --exclude=dist --exclude=.git \
31
- ~/projects/cicy-desktop/ mac:~/projects/cicy-desktop/
32
-
33
- # 2. Mac: start the Vite dev server (first time also: `npm install` in workers/render)
34
- ssh mac
35
- cd ~/projects/cicy-desktop/workers/render && nohup npm run dev > /tmp/vite-dev.log 2>&1 &
36
- # Vite listens on localhost:8173
37
-
38
- # 3. Mac: kill any installed .app then run Electron from source
39
- # .env.dev already sets CICY_HOMEPAGE_URL=http://localhost:8173, so
40
- # source-mode Electron loads the live Vite bundle (HMR enabled) instead
41
- # of the bundled file:// one.
42
- pkill -f "MacOS/CiCy Desktop" 2>/dev/null
43
- cd ~/projects/cicy-desktop && npm install # first time only
44
- nohup bash -c 'set -a; . ./.env.dev; set +a; npm start' > /tmp/cicy-desktop-dev.log 2>&1 &
45
- ```
46
-
47
- 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.
48
-
49
- 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.
50
-
51
- ### Loop B — packaged build (only for release validation)
52
-
53
- ```bash
54
- # Linux: full sync (same as Loop A step 1)
55
- rsync -avz --delete \
56
- --exclude=node_modules --exclude=dist --exclude=.git \
57
- ~/projects/cicy-desktop/ mac:~/projects/cicy-desktop/
58
-
59
- # Mac: build the .app (electron-builder won't overwrite a running one)
60
- ssh mac
61
- cd ~/projects/cicy-desktop
62
- pkill -f "MacOS/CiCy Desktop" 2>/dev/null
63
- CICY_CODE_BIN_PATH=<path-to-cicy-code-binary> npm run build:mac
64
- open "dist/mac/CiCy Desktop.app"
65
- ```
66
-
67
- ### Windows side — CF Worker is the SPA delivery path
68
-
69
- 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:
70
-
71
- ```bash
72
- # Linux dev: build + deploy SPA
73
- cd ~/projects/cicy-desktop/workers/render
74
- npm run build
75
- CLOUDFLARE_ACCOUNT_ID=$(jq -r .cf.prod.account_id ~/cicy-ai/global.json) \
76
- CLOUDFLARE_API_TOKEN=$(jq -r .cf.prod.api_token ~/cicy-ai/global.json) \
77
- npx wrangler deploy
78
- # Also mirror into the file:// folder for Mac packaged builds
79
- rsync -av --delete dist/ ../../src/backends/homepage-react/
80
- ```
81
-
82
- 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`).
83
-
84
- ### Loop A failure modes
85
-
86
- The Mac-native loop has no SSH tunnel to drop, but it has three other failure shapes:
87
-
88
- 1. **Vite died** (terminal closed, OOM, port conflict)
89
- ```bash
90
- ssh mac "curl -sI http://localhost:8173/ -m 4 | head -1"
91
- # Connection refused → Vite down. Restart with step 2 of Loop A.
92
- ```
93
-
94
- 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)
95
- ```bash
96
- ssh mac 'ps -ef | grep "MacOS/CiCy Desktop" | grep -v grep'
97
- # If you see /Applications/CiCy Desktop.app/... — that's the packaged one.
98
- # pkill -f "MacOS/CiCy Desktop" then re-run Loop A step 3.
99
- ```
100
-
101
- 3. **Electron loaded a stale URL** (`.env.dev` not sourced, fallback to `file://` or `desktop.cicy-ai.com`). Inspect the live URL over CDP:
102
- ```bash
103
- ssh mac 'curl -s http://127.0.0.1:9221/json | python3 -c "
104
- import sys, json
105
- for t in json.load(sys.stdin):
106
- print(t.get(\"type\"), t.get(\"url\",\"\")[:80])"'
107
- # If url is anything other than http://localhost:8173/, source-mode wasn't
108
- # picked up cleanly. Re-export CICY_HOMEPAGE_URL and restart Electron.
109
- ```
110
-
111
- ### The three reload classes (the trap that wastes hours)
112
-
113
- 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.
114
-
115
- | Class | Files | Reload trigger |
116
- |---|---|---|
117
- | **Vite render** | `workers/render/src/**` — App.jsx, App.css, any imported JS | ✅ **HMR**. Save → instant. No Electron restart. |
118
- | **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. |
119
- | **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. |
120
-
121
- **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)).
122
-
123
- ### Where edits go
124
-
125
- - **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).
126
- - **Commits / pushes from Linux.** Mac is a working-tree mirror; nothing committed there should be the source of truth.
127
- - **Windows builds: GitHub Actions only** (see `.github/workflows/build-windows*.yml`). Don't try local Windows builds.
128
-
129
- ## Common commands
130
-
131
- ### Install and run
132
-
133
- ```bash
134
- npm install
135
- npm start # local Electron worker via bin/cicy-desktop
136
- npm run start:master # control-plane master on port 8100
137
- ```
138
-
139
- ### Formatting
140
-
141
- ```bash
142
- npm run format
143
- npm run format:check
144
- ```
145
-
146
- ### Tests
147
-
148
- ```bash
149
- npm test # full suite
150
- npx jest --runInBand tests/rpc/master-routes.test.js # single file
151
- npx jest --runInBand --testNamePattern="Master routes" tests/rpc/master-routes.test.js
152
- ```
153
-
154
- `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.
155
-
156
- ### Build & distribute
157
-
158
- ```bash
159
- npm install
160
- npm run build # multi-platform via electron-builder
161
- npm run build:mac # dist/CiCy Desktop-<ver>.dmg + .zip + dist/mac/CiCy Desktop.app
162
- npm run build:win # NSIS installer
163
- npm run build:linux # deb + AppImage
164
- ```
165
-
166
- Dev iteration loop on macOS:
167
-
168
- 1. edit `src/...`
169
- 2. `pkill -f "MacOS/CiCy Desktop"` (electron-builder won't overwrite a running app)
170
- 3. `CICY_CODE_BIN_PATH=<path-to-cicy-code-binary> npm run build:mac`
171
- 4. `open "dist/mac/CiCy Desktop.app"`
172
- 5. inspect the packaged renderer via `npx --yes asar extract-file "dist/mac/CiCy Desktop.app/Contents/Resources/app.asar" <path>`
173
-
174
- ### RPC CLI workflows
175
-
176
- ```bash
177
- ./bin/cicy-rpc init
178
- ./bin/cicy-rpc tools
179
- ./bin/cicy-rpc ping
180
- CICY_NODE=windows ./bin/cicy-rpc ping
181
- CICY_NODE=windows ./bin/cicy-rpc chrome_launch_profile accountIdx=1 url=https://example.com/
182
- ```
183
-
184
- Distinctions to internalize:
185
-
186
- - `cicy` / `cicy-desktop` — local worker lifecycle (start/stop/status). **Not** for tool calls.
187
- - `cicy-rpc` — tool invocation. Reads `~/global.json`. Choose remote node via `CICY_NODE=<name>`.
188
-
189
- ## Architecture
190
-
191
- Two runtime roles:
192
-
193
- 1. **Worker** — an Electron process exposing tools over MCP and REST/RPC
194
- 2. **Master** — a thin control plane that tracks workers/agents/tasks and forwards `/api/rpc/:toolName` calls to a selected worker
195
-
196
- A single host can run either or both. The bundled `.app` is normally a worker; running `npm run start:master` adds the master role.
197
-
198
- ### Worker runtime
199
-
200
- Entrypoint: `src/main.js`.
201
-
202
- 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.
203
-
204
- Supporting modules:
205
-
206
- - `src/server/express-app.js` — base Express app, CORS, `/ping`, `/docs`, `/openapi.json`, UI shell routes
207
- - `src/server/mcp-server.js` — MCP transport setup
208
- - `src/server/tool-registry.js` — tool registration bridge
209
- - `src/server/tool-executor.js` — central execution path for REST/MCP tool calls
210
- - `src/cluster/worker-client.js` — worker registration + heartbeat to the master
211
- - `src/cluster/worker-identity.js` — identity payload advertised to the master
212
-
213
- Worker tool surface (how the ~100 tools are reached):
214
-
215
- - **IPC** — `ipcMain.handle("rpc", (e, toolName, args) => executeTool(...))` (`src/main.js`). This is what `window.electronRPC(tool, args)` and the cicy-code `desktop_event` bridge ride. No HTTP, no port.
216
- - **MCP** — every registered tool is exposed via the MCP server (`src/server/mcp-server.js`, `/mcp` + `/messages`).
217
- - **HTTP index** — `GET /openapi.json` (dynamic, every tool + inputSchema) and the Swagger UI at `/docs`.
218
- - **`list_tools`** — a meta-tool (`src/tools/list-tools.js`) that returns the live catalog over the IPC/RPC side, so callers that can't reach `/openapi.json` (renderer, `<webview>`, `agent-desktop rpc list_tools`) can still enumerate.
219
- - **REST `POST /rpc/:toolName`** — served by the **master** (`src/master/master-routes.js`), which forwards to a worker. The worker process itself does not mount `/rpc/*` execution routes.
220
-
221
- ### Tool system
222
-
223
- Tool implementations live in `src/tools/*.js`, listed in `src/tools/index.js`, loaded via `require("../tools")` from `src/server/tool-catalog.js`. Each module exports a function receiving `registerTool(name, description, schema, handler, options)`. The catalog (grouped by `tag`) is reused for MCP registration, `list_tools`, and OpenAPI generation in `/openapi.json` — **a tool definition change affects all of them at once.**
224
-
225
- Tools use **CommonJS** and **Zod schemas**.
226
-
227
- ### Master runtime
228
-
229
- Entrypoint: `src/master/master-main.js`.
230
-
231
- In-memory state:
232
-
233
- - `WorkerRegistry` — live registered workers
234
- - `WorkerInventory` — merged view of configured nodes from `~/global.json` plus registered workers
235
- - `AgentIndex` — worker agent metadata
236
- - `TaskStore` — forwarded task records
237
- - `SessionAffinityStore` — control-session routing affinity
238
-
239
- Master routes split into:
240
-
241
- - `src/master/master-routes.js` — public API under `/api`
242
- - `src/master/master-admin-routes.js` — admin-only routes under `/admin`
243
-
244
- The hot path is `POST /api/rpc/:toolName`:
245
-
246
- 1. build request context from `workerId`, `agentId`, runtime session, control session, `accountIdx`
247
- 2. choose an execution target with `src/master/task-scheduler.js`
248
- 3. create a task record in `TaskStore`
249
- 4. inject worker-specific fields (`win_id`, `agentId`, `runtimeSessionId`, `effectiveChromeProfile`)
250
- 5. forward to the selected worker via `src/cluster/remote-executor`
251
- 6. store completion/failure state
252
-
253
- ### Chrome profile dispatch
254
-
255
- 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.
256
-
257
- Master-side:
258
-
259
- - `src/master/chrome-config.js` reads master-local `~/cicy-ai/db/chrome.json`
260
- - `src/master/master-routes.js` injects `effectiveChromeProfile` for forwarded chrome tool calls when `accountIdx` is present
261
- - injection covers `chrome_launch_profile`, `chrome_get_profile`, `chrome_get_targets`, `chrome_cdp_call`
262
-
263
- Worker-side launch (`src/tools/chrome-tools.js::chrome_launch_profile`):
264
-
265
- - prefers injected `effectiveChromeProfile`
266
- - falls back to local `~/cicy-ai/db/chrome.json` for backward compat
267
- - if neither exists → clear error
268
- - if target user-data-dir doesn't exist → initialize from `~/chrome/_tmp` (or just `mkdir`)
269
- - `orgPath -> Default` copy is best-effort if the path exists
270
-
271
- Chrome internals separated:
272
-
273
- - `src/chrome/chrome-launcher.js` — binary resolution, args, spawn, debugger readiness, per-profile proxy via `--proxy-server=<url>`
274
- - `src/chrome/chrome-cdp-client.js` — `/json/version`, `/json/list`, activation, generic CDP calls
275
- - `src/chrome/runtime-registry.js` — local runtime state per account
276
- - `src/chrome/debugger-port-resolver.js` — port assignment
277
-
278
- Cross-platform Chrome discovery (`chrome-launcher.js::getBinaryCandidates`):
279
-
280
- - macOS: `/Applications/Google Chrome.app`, `~/Applications/Google Chrome.app`, `/Applications/Chromium.app`
281
- - Windows: `%LOCALAPPDATA%` / `%PROGRAMFILES%` / `%PROGRAMFILES(X86)%` under `Google\Chrome\Application\chrome.exe` (+ Chromium variants)
282
- - Linux: `google-chrome` / `chromium` / `chromium-browser` from PATH
283
-
284
- When none are present, launch errors with `"Chrome/Chromium binary not found"` — user must install Chrome first.
285
-
286
- ### Homepage UI (Vite + React subproject)
287
-
288
- 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.
289
-
290
- - entry: `workers/render/src/App.jsx` + `App.css`
291
- - dev server: `workers/render/vite.config.js` → `0.0.0.0:8173`
292
- - 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`)
293
- - BrowserWindow config (`src/backends/homepage-window.js`):
294
- - `preload: src/backends/homepage-preload.js`
295
- - `webviewTag: true` + `allowRunningInsecureContent: true` (the right-side Team Helper drawer is a `<webview>` loading a remote http:// SPA)
296
- - `sandbox: false` + `contextIsolation: true`
297
-
298
- 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)).
299
-
300
- ### Preload bridges — `homepage-preload.js` vs `webview-preload.js`
301
-
302
- Two distinct preload files because they run in different webContents with different security needs:
303
-
304
- `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:
305
-
306
- - `window.electronRPC(tool, args)` — generic dispatch into the worker tool registry (any tool from `src/tools/*.js`)
307
- - `window.cicy.localTeams.{list, open, add, remove, update, upgrade, onWebviewRelay, replyWebviewRelay}`
308
- - `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)
309
- - `window.cicy.auth.{loginStart, loginCancel, onComplete}` — browser-loopback login flow (`src/backends/auth-loopback.js`)
310
- - `window.cicy.app.*`, `window.cicy.windows.*`, `window.cicy.shell.openExternal`, etc.
311
- - `window.cicy.preloadPath` (legacy) and `window.cicy.webviewPreloadPath` — absolute paths the React code reads to wire the right-drawer `<webview preload={...}>`
312
-
313
- `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:
314
-
315
- - `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)
316
- - `window.cicy.localTeams.{list, add, remove, update, upgrade}` — all five go through `webview:relay` (next section), not directly to main
317
-
318
- 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.
319
-
320
- ### `webview:relay` — webview ↔ host renderer authority pattern
321
-
322
- 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.
323
-
324
- ```
325
- webview main process host renderer (App.jsx)
326
- │ │ │
327
- ipcRenderer.invoke │ │
328
- ("webview:relay", msg) │ │
329
- ──────────────────────────────▶ ipcMain.handle │
330
- │ │
331
- host.send │
332
- ("webview:relay", │
333
- {reqId, msg}) │
334
- ─────────────────────────▶ onWebviewRelay handler
335
-
336
- await window.cicy.localTeams.add(spec)
337
- fetchLocalTeams() ← UI refresh
338
-
339
- ipcRenderer.send
340
- ◀────────────────────────── ("webview:relay-reply",
341
- {reqId, result})
342
- resolve(result) │
343
- ◀────────────────────────────── │ │
344
- promise resolves │ │
345
- ```
346
-
347
- 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.
348
-
349
- ### Team Helper drawer (cloud SPA in `<webview>`)
350
-
351
- 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:
352
-
353
- - `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).
354
- - `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.
355
- - `HELPER_PANE_ID = "w-6002:main.0"` — the `Team Helper` opencode pane the cloud `cicy-code --helper=1` mode pins.
356
-
357
- 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).
358
-
359
- 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".
360
-
361
- #### Helper token rotation workflow
362
-
363
- 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):
364
-
365
- ```bash
366
- # 1. Grab the new token straight out of the helper container's global.json
367
- docker exec cicy-helper grep api_token /home/cicy/cicy-ai/global.json
368
- # "api_token": "cicy_XXXXXXXX…",
369
-
370
- # 2. Paste it into workers/render/src/App.jsx HELPER_SHARED_TOKEN
371
- # Vite HMR's the constant change into the running renderer immediately,
372
- # no Electron restart needed for THIS step (the webview keys off helperUrl
373
- # so it remounts with the new token).
374
-
375
- # 3. rsync to Mac so its source matches Linux (vite-dev tunnel still works,
376
- # but explicit sync prevents drift if you later switch loops).
377
- rsync -avz --delete \
378
- --exclude=node_modules --exclude=dist --exclude=.git \
379
- --exclude=workers/render/node_modules --exclude=workers/render/dist \
380
- ~/projects/cicy-desktop/ mac:~/projects/cicy-desktop/
381
- ```
382
-
383
- 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.
384
-
385
- `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.
386
-
387
- ### Deep links (`cicy://`) and local-team add/rename
388
-
389
- Protocol `cicy://` is registered (`package.json` build `protocols` + `setAsDefaultProtocolClient("cicy")` in `src/main.js`). The handler is `src/main.js::handleDeepLink(url)`, fed by `app.on("open-url")` (mac), `second-instance`, and cold-start `argv` (win/linux).
390
-
391
- `cicy://addTeam?title=<t>&url=<base_url>&token=<api_token>`:
392
-
393
- - **handleDeepLink adds the team directly in the main process** via `local-teams.addTeam({base_url, api_token, name})` — it does NOT rely on the renderer. (An earlier version only broadcast `deeplink:addTeam` to the renderer, but App.jsx never wired `window.cicy.deeplink.onAddTeam`, so the team was silently dropped.) The homepage polls `localTeams:list` every few seconds, so the new team shows up on its own.
394
- - **Upsert is keyed by `base_url`** (`local-teams.js::addTeam`): re-adding a known URL refreshes `api_token` + install meta but **keeps the existing (possibly user-renamed) name** — only a brand-new team takes the provided title. No-name → i18n default `localTeams.unnamed` (`Unnamed`/`未命名`/…).
395
- - The URL value must be **single** percent-encoded; raw CJK in `title` makes macOS `open` re-encode the whole thing (double-encoding → `bad base_url`).
396
-
397
- **Rename**: every local team is renamable. `LocalTeamCard` (App.jsx) has inline edit (double-click name / ✎) → `window.cicy.localTeams.update(id, {name})` → `local-teams.js::updateTeam` (whitelists `name`). Labels go through `window.cicyI18n.t` (preload-exposed i18n; keys in `src/i18n/locales/*.json` under `localTeams`).
398
-
399
- ### Backends launcher (legacy `src/backends/`)
400
-
401
- `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`.
402
-
403
- ### Trust gate (`isTrustedUrl`)
404
-
405
- `src/utils/window-utils.js::isTrustedUrl(url)` decides whether a `BrowserWindow` gets:
406
-
407
- - `nodeIntegration: true`
408
- - `contextIsolation: false`
409
- - the `dom-ready` `electronRPC` auto-injection
410
-
411
- Trusted hosts:
412
-
413
- 1. `localhost` / `127.0.0.1`
414
- 2. `*.de5.net`
415
- 3. **any hostname in `backends.json`** — anything the user added via the Add form (v2.0.2 widening)
416
-
417
- 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.
418
-
419
- ### Bridge to cicy-code (`desktop_event` / `rpc_call`)
420
-
421
- 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.
422
-
423
- Flow:
424
-
425
- 1. cicy-code server posts `POST /api/chat/push` with `{ type: "desktop_event", data: { type: "rpc_call", tool, args, requestId } }`
426
- 2. cicy-code relays to the connected client over WebSocket
427
- 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
428
- 4. result dispatched as `rpc-result` CustomEvent → relayed back through the WS by `Workspace.tsx`
429
- 5. server-side skill (`hosttools.go::desktopRPC`) matches by `requestId` and returns
430
-
431
- 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.
432
-
433
- 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.
434
-
435
- ### On-disk layout — `~/.local/bin/cicy-code` symlink → versioned binary
436
-
437
- The cloud Team Helper agent writes the daemon into the user's `~/.local/bin/` with this shape (the npx sidecar path uses the npm cache instead, but a Helper-installed `~/.local/bin/cicy-code` on `:8008` is still reused first via `probeExisting`, and `upgradeNative` manages it):
438
-
439
- ```
440
- ~/.local/bin/cicy-code-2.1.8 (actual binary, +x)
441
- ~/.local/bin/cicy-code-2.1.9 (next version after upgrade)
442
- ~/.local/bin/cicy-code (symlink → cicy-code-2.1.9, atomic-swapped on upgrade)
443
- ```
444
-
445
- Rationale:
446
-
447
- - **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.
448
- - **Rollback**: old versioned binaries stay on disk. Rolling back is one symlink swap.
449
- - **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.
450
-
451
- Upgrade flow inside `src/backends/local-teams.js::upgradeNative`:
452
-
453
- 1. `fetchManifestVersion()` learns the upcoming version (so the download filename is `cicy-code-<ver>` from the start).
454
- 2. `downloadFile(directURL → mirrorURL, ~/.local/bin/cicy-code-<ver>)`.
455
- 3. `chmod 0o755`.
456
- 4. `--version` round-trip verifies the bytes; if mirror served stale, rename the file onto the real version.
457
- 5. `pkill -f ~/.local/bin/cicy-code` (and the previously stored `install_path` if it differs) kills the old daemon.
458
- 6. `ln -sfn cicy-code-<ver> ~/.local/bin/cicy-code` (written at a tmp name + renamed = atomic).
459
- 7. Re-spawn via the symlink (`spawn(linkPath, [], { detached: true })`).
460
- 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.
461
-
462
- 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.
463
-
464
- ### Sidecar cicy-code daemon
465
-
466
- `cicy-desktop` never bundles a `cicy-code` binary. `src/sidecar/cicy-code.js::start()` acquires the daemon at runtime:
467
-
468
- 1. **Already running on `:8008`** — left over from a previous session, started by the user, or installed by the cloud Team Helper. `probeExisting()` detects it and `start()` reuses without re-spawning. This wins on every platform.
469
- 2. **mac / linux → `npx cicy-code`** — `start()` spawns `npx -y cicy-code` (default registry npmmirror for CN; override with `CICY_NPM_REGISTRY`, pin with `CICY_CODE_VERSION`). The launcher fetches the per-version binary from npm and does its own `:8008` port hygiene. No download/installer code in cicy-desktop.
470
- 3. **Windows → Docker** — `start()` delegates to `src/sidecar/docker.js` (`docker run` of the cicy-code image whose entrypoint `npx`-installs cicy-code). The image is loaded from R2 when absent.
471
- 4. **Cloud Team Helper** — the trial helper container walks the user through installing cicy-code on their machine, then registers the team via `window.cicy.localTeams.add({...})`.
472
-
473
- **Retired (do not look for these):** the old in-app downloader `src/sidecar/installer.js` and the WSL path `src/sidecar/wsl.js` are **deleted**, along with their IPC handlers and the in-app install UI. There is no `bundledBinaryPath()`/`userBinary()`/`extraResources`/`vendor/cicy-code/` anymore. If no daemon is on `:8008` and the npx/Docker spawn can't run, `start()` returns `null` and the homepage's Team Helper card is the path from "no daemon" to "daemon running".
474
-
475
- #### What broke that motivated the principle (2026-05-29)
476
-
477
- 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.
478
-
479
- ### CLI/config split
480
-
481
- Two CLIs, different jobs:
482
-
483
- - `bin/cicy-desktop` / `cicy` — local worker lifecycle (start/stop/status)
484
- - `bin/cicy-rpc` — tool invocation
485
-
486
- `src/cli/rpc.js` (`cicy-rpc`):
487
-
488
- - reads `~/global.json`
489
- - resolves `cicyDesktopNodes[<name>]`
490
- - uses `CICY_NODE` to choose the target node
491
- - POSTs directly to `/<rpc-path>` on that node with bearer auth
492
-
493
- `cicy-rpc init` only initializes `~/global.json` if missing. It is not a general node-management command.
494
-
495
- ## Config and auth
496
-
497
- ### `~/global.json`
498
-
499
- ```json
500
- {
501
- "api_token": "cicy_…",
502
- "cicyDesktopNodes": {
503
- "mac": { "base_url": "http://127.0.0.1:8101", "api_token": "…" },
504
- "windows": { "base_url": "http://1.2.3.4:8101", "api_token": "…" }
505
- }
506
- }
507
- ```
508
-
509
- `cicy-rpc` picks the token in this order:
510
-
511
- 1. `cicyDesktopNodes.<name>.api_token`
512
- 2. top-level `api_token`
513
-
514
- ### Worker registration to master
515
-
516
- ```bash
517
- MASTER_TOKEN=$(jq -r '.api_token' ~/global.json)
518
- PORT=8101 CICY_MASTER_URL="http://127.0.0.1:8100" CICY_MASTER_TOKEN="$MASTER_TOKEN" npm start
519
- ```
520
-
521
- The master uses `CICY_MASTER_TOKEN` directly or falls back to `MasterTokenManager`.
522
-
523
- ## File map
524
-
525
- | What | Where |
526
- |---|---|
527
- | worker startup/runtime | `src/main.js` |
528
- | master startup/runtime | `src/master/master-main.js` |
529
- | master forwarding | `src/master/master-routes.js` |
530
- | `~/global.json` inventory | `src/master/worker-inventory.js` |
531
- | RPC CLI | `src/cli/rpc.js` |
532
- | worker tool catalog | `src/server/tool-catalog.js` |
533
- | tool execution plumbing | `src/server/tool-executor.js` |
534
- | Chrome tools | `src/tools/chrome-tools.js` |
535
- | Chrome launcher | `src/chrome/chrome-launcher.js` |
536
- | Chrome CDP helpers | `src/chrome/chrome-cdp-client.js` |
537
- | cluster registration | `src/cluster/worker-client.js` |
538
- | backends registry | `src/backends/registry.js` |
539
- | backends launcher UI | `src/backends/homepage.html` |
540
- | backends preload bridge | `src/backends/homepage-preload.js` |
541
- | BrowserWindow trust + auto-inject | `src/utils/window-utils.js` |
542
- | sidecar acquisition (npx / docker) | `src/sidecar/cicy-code.js`, `src/sidecar/docker.js` |
543
- | desktop shortcut + deep links (`cicy://`) | `bin/cicy-desktop` (shortcut gen), `src/main.js` (handleDeepLink) |
544
- | local teams (add/upsert/rename) + i18n | `src/backends/local-teams.js`, `src/i18n/locales/*.json` |
545
- | tool list module | `src/tools/index.js`, `src/tools/list-tools.js` |
546
- | RPC test files | `tests/rpc/master-routes.test.js`, `tests/rpc/cicy-rpc.test.js` |
547
-
548
- ## Debugging via remote-debugging-port 9221
549
-
550
- Electron renderers in dev are launched with `--remote-debugging-port=9221`. Use it instead of guessing.
551
-
552
- ```bash
553
- # Open the tunnel once per session
554
- ssh -fNR 9221:127.0.0.1:9221 mac # if your dev → Mac
555
- # or
556
- ssh -fNL 9221:127.0.0.1:9221 mac # if you're on Linux looking at Mac
557
-
558
- # Enumerate targets (homepage + every <webview>)
559
- curl -s http://127.0.0.1:9221/json/list | jq '.[] | {type, url, webSocketDebuggerUrl}'
560
- ```
561
-
562
- Each target has a `webSocketDebuggerUrl`. Connect and send any `Runtime.evaluate` to inspect window state from outside Electron:
563
-
564
- ```js
565
- // /tmp/cdp-probe.mjs
566
- import http from 'node:http';
567
- const targets = await new Promise(r =>
568
- http.get('http://127.0.0.1:9221/json/list', res => {
569
- let b=''; res.on('data',c=>b+=c); res.on('end',()=>r(JSON.parse(b)));
570
- }));
571
- const target = targets.find(t => t.type === 'webview'); // or 'page' for homepage
572
- const ws = new WebSocket(target.webSocketDebuggerUrl);
573
- await new Promise((ok, fail) => { ws.onopen = ok; ws.onerror = fail; });
574
-
575
- let id = 0;
576
- function call(method, params={}) {
577
- const reqId = ++id;
578
- return new Promise(res => {
579
- ws.addEventListener('message', function h(e) {
580
- const m = JSON.parse(e.data);
581
- if (m.id === reqId) { ws.removeEventListener('message', h); res(m); }
582
- });
583
- ws.send(JSON.stringify({ id: reqId, method, params }));
584
- });
585
- }
586
-
587
- for (const expr of [
588
- 'typeof window.electronRPC',
589
- 'typeof window.cicy',
590
- 'window.cicy && Object.keys(window.cicy.localTeams || {})',
591
- '(window.cicy && window.cicy.webviewPreloadPath) || null',
592
- ]) {
593
- const r = await call('Runtime.evaluate', { expression: expr, returnByValue: true });
594
- console.log(expr, '→', JSON.stringify(r.result?.result?.value));
595
- }
596
- ws.close();
597
- ```
598
-
599
- 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.
600
-
601
- 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`.
602
-
603
- ## Mental model checklist
604
-
605
- When touching any of these, expect ripple effects:
606
-
607
- - adding a tool → touches MCP server, REST/RPC, OpenAPI all at once via the catalog
608
- - changing trust criteria → changes `nodeIntegration` for whole classes of windows; verify with the cicy-code bridge still works
609
- - changing the homepage entry flow → check that cold-launch and "back to launcher" paths both behave (history.length / sessionStorage gates)
610
- - changing the sidecar acquisition → there is no bundled binary; verify `npx cicy-code` (mac/linux) / Docker (win) actually spawns and binds `:8008`, and that `probeExisting` reuse still works
611
- - changing `chrome_*` tools → both master injection (`effectiveChromeProfile`) and worker fallback (`~/cicy-ai/db/chrome.json`) paths still need to work
612
- - changing **any preload file** → `⌘+Q` + reopen Electron is mandatory; HMR / `⌘+R` won't reload it. Confirm via CDP `Runtime.evaluate` on the target window
613
- - 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)
614
- - 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`
615
- - 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
package/bin/cicy-rpc DELETED
@@ -1,13 +0,0 @@
1
- #!/usr/bin/env node
2
- const { version } = require("../package.json");
3
- const { runRpcCli } = require("../src/cli/rpc");
4
-
5
- runRpcCli({
6
- argv: process.argv.slice(2),
7
- version,
8
- workerPort: Number(process.env.PORT || 8101),
9
- programName: "cicy-rpc",
10
- }).catch((error) => {
11
- console.error(`❌ ${error.message}`);
12
- process.exit(1);
13
- });
@@ -1,3 +0,0 @@
1
- owner: cicy-ai
2
- repo: cicy-desktop
3
- provider: github