shadow-claw 1.23.4 → 1.24.0
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/README.md +42 -5
- package/bin/cli.mjs +166 -0
- package/bin/cli.test.mjs +60 -0
- package/bin/commands/backup.mjs +70 -0
- package/bin/commands/clients.mjs +41 -0
- package/bin/commands/peer-id.mjs +76 -0
- package/bin/commands/peer-id.test.mjs +104 -0
- package/bin/commands/send.mjs +51 -0
- package/bin/commands/tasks.mjs +59 -0
- package/bin/commands/webrtc-listen.mjs +254 -0
- package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
- package/bin/rolldown.config.test.mjs +401 -8
- package/bin/utils/control-client.mjs +243 -0
- package/bin/utils/control-client.test.mjs +83 -0
- package/bin/utils/ulid.mjs +45 -0
- package/bin/utils/webrtc-control-client.mjs +699 -0
- package/bin/utils/webrtc-control-client.test.mjs +318 -0
- package/dist/electron/main.cjs +1 -1
- package/dist/public/AGENTS.md +5 -3
- package/dist/public/README.md +42 -5
- package/dist/public/agent.worker.js +1 -1
- package/dist/public/backup-controller-C_s9in2t.js +1 -0
- package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
- package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
- package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
- package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
- package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
- package/dist/public/config-CrHchneq.js +1 -0
- package/dist/public/{configurePeerJs-B4Ym9VMj.js → configurePeerJs-CxpZeDJJ.js} +1 -1
- package/dist/public/{connections-BEn6JYdu.js → connections-DvbgdkaB.js} +1 -1
- package/dist/public/{custom-element-security-DdYlUozq.js → custom-element-security-NCqQ5lmC.js} +1 -1
- package/dist/public/docs/README.md +3 -1
- package/dist/public/docs/architecture/overview.md +12 -12
- package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
- package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +2 -2
- package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
- package/dist/public/docs/example/article/index.html +1 -1
- package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
- package/dist/public/docs/guides/server-development-configuration.md +24 -0
- package/dist/public/docs/publishing/index.html +1 -1
- package/dist/public/docs/skill-creator/index.html +1 -1
- package/dist/public/docs/subsystems/backup.md +56 -0
- package/dist/public/docs/subsystems/cli.md +124 -0
- package/dist/public/docs/subsystems/control-plane.md +158 -0
- package/dist/public/docs/subsystems/notifications.md +54 -2
- package/dist/public/docs/subsystems/reactive-ui.md +1 -0
- package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
- package/dist/public/e2e/README.md +5 -1
- package/dist/public/{e2e-bridge-BQ4KzUt5.js → e2e-bridge-IGWsRl4i.js} +1 -1
- package/dist/public/file-viewer-D0eflgt6.js +1 -0
- package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
- package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
- package/dist/public/{git-DEUvQTUA.js → git-7Vq_SJSp.js} +2 -2
- package/dist/public/{iframe-sanitizer-0NS4XLOd.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
- package/dist/public/index.html +1 -1
- package/dist/public/index.js +1 -1
- package/dist/public/{initChatSplitResize-llGkSJXg.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
- package/dist/public/initControlPlane-C9jpnWev.js +1 -0
- package/dist/public/llms.txt +1 -1
- package/dist/public/main/index.html +1 -1
- package/dist/public/main/memory/index.html +1 -1
- package/dist/public/{markdown-CyYR4App.js → markdown-BnpKxSxl.js} +1 -1
- package/dist/public/{mcp-reconnect-CSx4aMwy.js → mcp-reconnect-9emsqGRe.js} +1 -1
- package/dist/public/{memoryStorage-Dwg8NhAZ.js → memoryStorage-C0KvLNUp.js} +1 -1
- package/dist/public/{orchestrator-DRfEudXk.js → orchestrator-3h9xxHUm.js} +676 -676
- package/dist/public/orchestrator-nJj59RqD.js +15 -0
- package/dist/public/{peerjs-CurBuKvk.js → peerjs-BP4laMXj.js} +1 -1
- package/dist/public/{push-client-Dii-vPEh.js → push-client-CrjEXK3U.js} +1 -1
- package/dist/public/service-worker/push-handler.js +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-5IHaDDMJ.js → shadow-claw-CbwETBWf.js} +2 -2
- package/dist/public/{shadow-claw-accounts-CZvz4iQz.js → shadow-claw-accounts-B4sBTQEj.js} +1 -1
- package/dist/public/{shadow-claw-channels-BKXFyRS3.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
- package/dist/public/{shadow-claw-chat-CCLuhWdI.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
- package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
- package/dist/public/{shadow-claw-conversations-DV-87UUh.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
- package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
- package/dist/public/{shadow-claw-file-viewer-Dk2dbAUd.js → shadow-claw-file-viewer-Blf6yGVD.js} +1 -1
- package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
- package/dist/public/{shadow-claw-git-DRA7_qck.js → shadow-claw-git-v8S0JjuR.js} +2 -2
- package/dist/public/{shadow-claw-integrations-1Xu5snuV.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
- package/dist/public/{shadow-claw-llm-6Aci1BDP.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
- package/dist/public/{shadow-claw-mcp-remote-BUjAeVXC.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
- package/dist/public/{shadow-claw-networking-FQe4nCp3.js → shadow-claw-networking-qHbP95z9.js} +1 -1
- package/dist/public/{shadow-claw-notifications-DV2wibL3.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
- package/dist/public/{shadow-claw-page-header-B63S4HTU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
- package/dist/public/{shadow-claw-pages-DE3jWj5K.js → shadow-claw-pages-DFZHhzFZ.js} +1 -1
- package/dist/public/{shadow-claw-settings-S6DAKfYx.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
- package/dist/public/{shadow-claw-storage-DaacA3_K.js → shadow-claw-storage-BFynbP0j.js} +2 -2
- package/dist/public/{shadow-claw-task-server-IjKX042p.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
- package/dist/public/{shadow-claw-tasks-Cy0JYmGU.js → shadow-claw-tasks-Caxc9atL.js} +1 -1
- package/dist/public/{shadow-claw-terminal-znAt7HyP.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
- package/dist/public/{shadow-claw-tools-Cutv9Si5.js → shadow-claw-tools-BfJx3A9W.js} +1 -1
- package/dist/public/{shadow-claw-webvm-BH8mPDEu.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
- package/dist/public/{syncWebMcpRegistration-BqJxaMGR.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
- package/dist/server.js +286 -189
- package/package.json +2 -1
- package/src/__mocks__/jszip.cjs +15 -5
- package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
- package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
- package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
- package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
- package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
- package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
- package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +76 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
- package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
- package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
- package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
- package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
- package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
- package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
- package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
- package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +273 -0
- package/src/components/shadow-claw-files/shadow-claw-files.test.ts +408 -1
- package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
- package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
- package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
- package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
- package/src/config/config.ts +10 -0
- package/src/core/backup-controller.test.ts +101 -0
- package/src/core/backup-controller.ts +137 -0
- package/src/core/control-plane-client.test.ts +408 -0
- package/src/core/control-plane-client.ts +425 -0
- package/src/core/orchestrator/orchestrator.ts +4 -0
- package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
- package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
- package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
- package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
- package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
- package/src/core/utils/initControlPlane.test.ts +276 -0
- package/src/core/utils/initControlPlane.ts +437 -0
- package/src/core/utils/initializeApp.ts +18 -0
- package/src/db/getSession.test.ts +13 -1
- package/src/server/app.test.ts +12 -0
- package/src/server/app.ts +18 -8
- package/src/server/client-registry.test.ts +250 -0
- package/src/server/client-registry.ts +477 -0
- package/src/server/config.ts +15 -0
- package/src/server/control-plane-types.ts +120 -0
- package/src/server/control-plane.test.ts +472 -0
- package/src/server/control-plane.ts +581 -0
- package/src/server/routes/backup.test.ts +263 -0
- package/src/server/routes/backup.ts +262 -0
- package/src/server/routes/integrations-email.test.ts +532 -0
- package/src/server/routes/llamafile.test.ts +172 -48
- package/src/server/routes/mesh-llm.test.ts +192 -0
- package/src/server/routes/ollama.test.ts +49 -0
- package/src/server/server-peer.test.ts +86 -0
- package/src/server/server-peer.ts +330 -0
- package/src/server/server.ts +35 -0
- package/src/server/services/llamafile-manager.test.ts +230 -6
- package/src/server/services/transformers-runtime.test.ts +186 -18
- package/src/server/webrtc.test.ts +21 -0
- package/src/server/webrtc.ts +72 -0
- package/src/service-worker/fetch-proxy.test.ts +64 -1
- package/src/service-worker/push-handler.ts +39 -0
- package/src/shell/vm.test.ts +744 -12
- package/src/storage/ensureMainGroupIndex.test.ts +153 -0
- package/src/storage/getGroupFile.test.ts +66 -0
- package/src/storage/memoryStorage.test.ts +82 -2
- package/src/storage/writeGroupFileBytes.test.ts +143 -0
- package/src/stores/chat-ui.test.ts +20 -0
- package/src/stores/orchestrator.test.ts +134 -0
- package/src/stores/toast.test.ts +50 -0
- package/src/stores/tools.test.ts +147 -10
- package/src/subsystems/channels/imessage.test.ts +59 -0
- package/src/subsystems/channels/peerjs.test.ts +500 -62
- package/src/subsystems/channels/peerjs.ts +71 -4
- package/src/subsystems/channels/telegram.test.ts +177 -0
- package/src/subsystems/email/catalog.test.ts +1 -0
- package/src/subsystems/email/connections.test.ts +39 -1
- package/src/subsystems/git/credentials.test.ts +160 -825
- package/src/subsystems/git/git.test.ts +463 -0
- package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
- package/src/subsystems/notifications/push-routes.test.ts +33 -0
- package/src/subsystems/notifications/push-routes.ts +23 -0
- package/src/subsystems/notifications/push-store.ts +1 -1
- package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
- package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
- package/src/subsystems/providers/model-registry.test.ts +115 -0
- package/src/subsystems/providers/providers.test.ts +94 -1
- package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
- package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
- package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
- package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
- package/src/subsystems/skills/activateSkill.test.ts +101 -0
- package/src/subsystems/skills/discoverSkills.test.ts +251 -0
- package/src/subsystems/tools/cron.test.ts +64 -0
- package/src/subsystems/tools/declarative.test.ts +255 -89
- package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
- package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
- package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
- package/src/testing/jest-setup.ts +328 -40
- package/src/testing/mock-v86.mjs +49 -0
- package/src/ui/a2ui/types.ts +4 -3
- package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
- package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
- package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
- package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
- package/src/worker/tools/email/email.test.ts +193 -8
- package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
- package/src/worker/tools/git/git.test.ts +214 -3
- package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
- package/src/worker/tools/rooms/leave-room.test.ts +40 -0
- package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
- package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
- package/src/worker/tools/ui/ask-user.test.ts +50 -0
- package/src/worker/tools/ui/clear-chat.test.ts +19 -0
- package/src/worker/tools/ui/get-current-time.test.ts +19 -0
- package/src/worker/tools/ui/javascript.test.ts +116 -0
- package/src/worker/tools/ui/list-components.test.ts +10 -0
- package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
- package/src/worker/tools/ui/manage-tools.test.ts +34 -0
- package/src/worker/tools/ui/render-component.test.ts +346 -0
- package/src/worker/tools/ui/send-notification.test.ts +40 -0
- package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
- package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
- package/src/worker/tools/workspace/attach-file.test.ts +84 -0
- package/src/worker/tools/workspace/copy-file.test.ts +69 -0
- package/src/worker/tools/workspace/create-directory.test.ts +40 -0
- package/src/worker/tools/workspace/delete-file.test.ts +73 -0
- package/src/worker/tools/workspace/diff-files.test.ts +94 -0
- package/src/worker/tools/workspace/list-files.test.ts +36 -0
- package/src/worker/tools/workspace/move-file.test.ts +66 -0
- package/src/worker/tools/workspace/open-file.test.ts +57 -0
- package/src/worker/tools/workspace/patch-file.test.ts +81 -0
- package/src/worker/tools/workspace/read-file.test.ts +116 -0
- package/src/worker/tools/workspace/send-file.test.ts +82 -0
- package/src/worker/tools/workspace/update-memory.test.ts +29 -0
- package/src/worker/tools/workspace/write-file.test.ts +28 -0
- package/src/worker/utils/rate-limit.test.ts +199 -0
- package/src/worker/utils/tool-state.test.ts +29 -0
- package/src/worker/utils/transformers-js-utils.test.ts +59 -0
- package/dist/public/config-BYAuxx3j.js +0 -1
- package/dist/public/downloadGroupFile-Drdvg1Ev.js +0 -1
- package/dist/public/file-viewer-CcZYw-PS.js +0 -1
- package/dist/public/orchestrator-o9Fj67BN.js +0 -15
- package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
- package/dist/public/shadow-claw-files-BpfpJIuP.js +0 -840
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Control Plane & Client Bridge
|
|
2
|
+
|
|
3
|
+
> Bidirectional communication gateway connecting CLI commands, external MCP clients, and connected browser/Electron tabs.
|
|
4
|
+
|
|
5
|
+
**Source:** `src/server/control-plane.ts` · `src/server/client-registry.ts` · `src/core/control-plane-client.ts` · `bin/commands/`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
ShadowClaw provides a real-time **Control Plane** and client bridge that enables external tools and CLI commands to interact directly with connected browser or Electron clients. Connected clients act as persistent execution surfaces with access to local browser features, WebMCP tools, and IndexedDB/OPFS storage.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Architecture & Transports
|
|
16
|
+
|
|
17
|
+
The Control Plane supports three interchangeable communication transports:
|
|
18
|
+
|
|
19
|
+
1. **Server-Sent Events + HTTP POST (`sse`, Default & Recommended):**
|
|
20
|
+
- **Server $\rightarrow$ Client:** Persistent HTTP SSE stream (`GET /api/control/events?clientId=...`).
|
|
21
|
+
- **Client $\rightarrow$ Server:** Standard HTTP JSON requests (`POST /api/control/messages`).
|
|
22
|
+
- **Why Default:** Extremely resilient across corporate firewalls, reverse proxies (Nginx, Caddy, Traefik), and cloud edge/CDN setups that do not support or maintain WebSocket connection upgrades.
|
|
23
|
+
2. **WebSocket (`websocket`, Opt-in):**
|
|
24
|
+
- Full-duplex WebSocket connection to `/ws/control?token=...`.
|
|
25
|
+
- Useful for low-latency direct connections or environments where WebSockets are preferred.
|
|
26
|
+
3. **WebRTC DataChannel (`webrtc`, Peer-to-Peer):**
|
|
27
|
+
- Direct P2P browser-to-CLI communication via PeerJS DataChannels.
|
|
28
|
+
- CLI coordinates via `shadow-claw webrtc listen` and the local IPC socket bridge (`.cache/webrtc-ipc.sock`).
|
|
29
|
+
- Allows headless commands and automation without requiring an active control-plane HTTP/SSE session.
|
|
30
|
+
|
|
31
|
+
```mermaid
|
|
32
|
+
graph TB
|
|
33
|
+
subgraph CLI ["CLI & External Tools"]
|
|
34
|
+
CLI_BIN["shadow-claw CLI (clients, send, backup, tasks)"]
|
|
35
|
+
CLI_LISTEN["webrtc listen (IPC Socket: .cache/webrtc-ipc.sock)"]
|
|
36
|
+
EXT_MCP["External MCP Clients (Hermes, Claude, Cursor)"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
subgraph Server ["Express Server & Control Plane"]
|
|
40
|
+
SSE_ENDPOINT["/api/control/events (SSE Stream)"]
|
|
41
|
+
MSG_ENDPOINT["/api/control/messages (HTTP POST)"]
|
|
42
|
+
WS_ENDPOINT["/ws/control (WebSocket Server)"]
|
|
43
|
+
PEERJS_SERVER["/peerjs/* (PeerJS Signaling Server)"]
|
|
44
|
+
REST_ROUTES["/api/control/* (REST API)"]
|
|
45
|
+
REGISTRY["Client Registry (SQLite: clients.db)"]
|
|
46
|
+
TOKEN_STORE["Control Token Auth"]
|
|
47
|
+
PUSH_WAKE["Web Push Remote Command Wakeup"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
subgraph Browser ["Connected Browser Client"]
|
|
51
|
+
CP_CLIENT["src/core/control-plane-client.ts<br>(SSE Default / WS Configurable)"]
|
|
52
|
+
PEERJS_CHANNEL["src/subsystems/channels/peerjs.ts<br>(WebRTC DataChannel)"]
|
|
53
|
+
ORCH["Orchestrator"]
|
|
54
|
+
WEBMCP["WebMCP (document.modelContext)"]
|
|
55
|
+
STORAGE["OPFS Storage"]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
CLI_BIN <-->|REST & WebSocket| WS_ENDPOINT
|
|
59
|
+
CLI_BIN <-->|Local IPC Socket| CLI_LISTEN
|
|
60
|
+
CLI_LISTEN <-->|WebRTC DataChannel| PEERJS_CHANNEL
|
|
61
|
+
EXT_MCP <-->|Standard MCP| REST_ROUTES
|
|
62
|
+
CP_CLIENT -->|SSE Stream (Commands)| SSE_ENDPOINT
|
|
63
|
+
CP_CLIENT -->|HTTP POST (Results)| MSG_ENDPOINT
|
|
64
|
+
CP_CLIENT -.->|Optional WebSocket| WS_ENDPOINT
|
|
65
|
+
PEERJS_CHANNEL <-->|Signaling| PEERJS_SERVER
|
|
66
|
+
WS_ENDPOINT --> REGISTRY
|
|
67
|
+
SSE_ENDPOINT --> REGISTRY
|
|
68
|
+
REGISTRY --> TOKEN_STORE
|
|
69
|
+
WS_ENDPOINT --> PUSH_WAKE
|
|
70
|
+
|
|
71
|
+
CP_CLIENT --> ORCH
|
|
72
|
+
CP_CLIENT --> WEBMCP
|
|
73
|
+
CP_CLIENT --> STORAGE
|
|
74
|
+
PEERJS_CHANNEL --> ORCH
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Protocol Specification
|
|
80
|
+
|
|
81
|
+
All messages are JSON objects matching the `ControlMessage` envelope:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
interface ControlMessage {
|
|
85
|
+
id: string; // ULID for message/response correlation
|
|
86
|
+
type: string; // Message discriminator
|
|
87
|
+
replyTo?: string; // Request ID when responding
|
|
88
|
+
payload: unknown; // Type-specific payload
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Message Types
|
|
93
|
+
|
|
94
|
+
| Message Type | Direction | Description |
|
|
95
|
+
| :------------------ | :-------------- | :-------------------------------------------------------------------------------------------------------- |
|
|
96
|
+
| `client:register` | Client → Server | Client registration with ID, device label, version, transport, and capabilities |
|
|
97
|
+
| `server:registered` | Server → Client | Acknowledgement of successful registration |
|
|
98
|
+
| `client:heartbeat` | Client → Server | Periodic heartbeat to refresh active client timestamp |
|
|
99
|
+
| `command:execute` | Server → Client | Dispatches a command action (`send-message`, `list-tasks`, `invoke-tool`, `read-state`, `trigger-backup`) |
|
|
100
|
+
| `command:result` | Client → Server | Response containing execution status, result data, or error message |
|
|
101
|
+
|
|
102
|
+
### Command Payloads
|
|
103
|
+
|
|
104
|
+
#### `command:execute` Payload
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
interface CommandExecutePayload {
|
|
108
|
+
commandId: string;
|
|
109
|
+
action:
|
|
110
|
+
| "send-message"
|
|
111
|
+
| "list-tasks"
|
|
112
|
+
| "trigger-backup"
|
|
113
|
+
| "invoke-tool"
|
|
114
|
+
| "read-state";
|
|
115
|
+
args: Record<string, unknown>;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### `command:result` Payload
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
interface CommandResultPayload {
|
|
123
|
+
commandId: string;
|
|
124
|
+
success: boolean;
|
|
125
|
+
data?: unknown;
|
|
126
|
+
error?: string;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Authentication & Security
|
|
133
|
+
|
|
134
|
+
1. **Control Token:** Connections must supply a secret control token via query parameter (`?token=...`), header (`x-control-token`), or Bearer authorization header (`Authorization: Bearer <token>`).
|
|
135
|
+
2. **Token Generation:** On server startup, a token is read from `SHADOWCLAW_CONTROL_TOKEN` or generated and persisted in SQLite (`clients.db` metadata table) and `.cache/control-token.json`.
|
|
136
|
+
3. **WebRTC CLI Peer Identity & Trust:** CLI clients connecting over WebRTC identify using a persistent peer ID in `.cache/cli-peer-id`. This ID can be retrieved or renewed using `npx shadow-claw peer-id [--renew|--set <id>]` and must be added in the browser under **Settings → WebRTC/PeerJS → Trusted Peer IDs** when peer restrictions are enabled.
|
|
137
|
+
4. **Localhost Binding:** Binds to `127.0.0.1` by default.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Browser Integration & Settings
|
|
142
|
+
|
|
143
|
+
Browser tabs connect automatically to the Control Plane on startup using `createDefaultControlPlaneClient` when served from a server environment.
|
|
144
|
+
|
|
145
|
+
### Transport Configuration
|
|
146
|
+
|
|
147
|
+
Users can toggle between transports in **Settings $\rightarrow$ Integrations $\rightarrow$ Control Plane $\rightarrow$ Control Plane Transport**:
|
|
148
|
+
|
|
149
|
+
- `Server-Sent Events (SSE + HTTP POST)` — Default
|
|
150
|
+
- `WebSocket (Full Duplex)` — Opt-in
|
|
151
|
+
|
|
152
|
+
### Registered Command Handlers
|
|
153
|
+
|
|
154
|
+
- `send-message`: Submits message/prompt into Orchestrator queue (supports optional `groupId` e.g. `br:main` or `peer:<peerId>`).
|
|
155
|
+
- `list-tasks`: Queries scheduled tasks from IndexedDB.
|
|
156
|
+
- `read-state`: Returns current active conversation group and state.
|
|
157
|
+
- `invoke-tool`: Invokes WebMCP (`document.modelContext`) tools directly.
|
|
158
|
+
- `trigger-backup`: Initiates workspace upload via `BackupController`.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Web Push notifications delivered via VAPID, a server-side SQLite task scheduler,
|
|
4
4
|
> and a client-side cron evaluator — so tasks fire even when no browser tab is open.
|
|
5
5
|
|
|
6
|
-
**Source:** `src/subsystems/notifications/` · `src/subsystems/tools/task-scheduler.ts` · `src/subsystems/tools/cron.ts` · `src/service-worker/push-handler.ts`
|
|
6
|
+
**Source:** `src/subsystems/notifications/` · `src/subsystems/notifications/push-routes.ts` · `src/subsystems/tools/task-scheduler.ts` · `src/subsystems/tools/cron.ts` · `src/service-worker/push-handler.ts`
|
|
7
7
|
|
|
8
8
|
## System Overview
|
|
9
9
|
|
|
@@ -14,6 +14,7 @@ graph TD
|
|
|
14
14
|
SW["Service Worker<br>push-handler.ts"]
|
|
15
15
|
CS["ClientTaskScheduler<br>task-scheduler.ts"]
|
|
16
16
|
PC["push-client.ts<br>Subscribe / Unsubscribe"]
|
|
17
|
+
OW["Open Windows<br>(postMessage)"]
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
subgraph Server ["Express / Electron Server"]
|
|
@@ -22,6 +23,7 @@ graph TD
|
|
|
22
23
|
TSR["task-schedule-routes.ts<br>/schedule/tasks/* endpoints"]
|
|
23
24
|
TSS["task-schedule-store.ts<br>SQLite scheduled tasks"]
|
|
24
25
|
Sched["ServerTaskScheduler<br>60s cron ticks"]
|
|
26
|
+
CP["Control Plane<br>/push/command"]
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
subgraph Agent ["Agent"]
|
|
@@ -35,9 +37,11 @@ graph TD
|
|
|
35
37
|
TT --> TSR
|
|
36
38
|
TSR --> TSS
|
|
37
39
|
TSS --> Sched
|
|
38
|
-
Sched -->|Web Push| SW
|
|
40
|
+
Sched -->|Web Push: scheduled-task| SW
|
|
39
41
|
SW -->|relay to window| CS
|
|
40
42
|
CS --> TT
|
|
43
|
+
CP -->|Web Push: remote-command| SW
|
|
44
|
+
SW -->|postMessage: remote-command-trigger| OW
|
|
41
45
|
```
|
|
42
46
|
|
|
43
47
|
## Push Notifications
|
|
@@ -75,6 +79,54 @@ On `push` event:
|
|
|
75
79
|
|
|
76
80
|
The service worker uses a `BroadcastChannel("shadowclaw-push")` to relay push events to any open windows. The orchestrator listens on this channel and triggers the agent invocation for scheduled tasks.
|
|
77
81
|
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Remote Command Push Wakeup
|
|
85
|
+
|
|
86
|
+
**Files:** `src/service-worker/push-handler.ts` · `src/subsystems/notifications/push-routes.ts`
|
|
87
|
+
|
|
88
|
+
### Purpose
|
|
89
|
+
|
|
90
|
+
The control plane may need to dispatch a command to a specific ShadowClaw client that is currently dormant — i.e. not connected to the WebSocket. Rather than dropping the command or waiting for the client to reconnect, the server sends a Web Push notification of type `remote-command` to wake the client up and relay the payload.
|
|
91
|
+
|
|
92
|
+
### How it works
|
|
93
|
+
|
|
94
|
+
```mermaid
|
|
95
|
+
sequenceDiagram
|
|
96
|
+
participant CP as Control Plane
|
|
97
|
+
participant PR as push-routes.ts<br>POST /push/command
|
|
98
|
+
participant PS as push-store.ts<br>getAllSubscriptions()
|
|
99
|
+
participant WP as web-push
|
|
100
|
+
participant SW as Service Worker<br>push-handler.ts
|
|
101
|
+
participant OW as Open Windows
|
|
102
|
+
|
|
103
|
+
CP->>PR: POST /push/command { clientId, action, args, prompt }
|
|
104
|
+
PR->>PS: getAllSubscriptions() [ORDER BY … id DESC]
|
|
105
|
+
PS-->>PR: subscriptions[]
|
|
106
|
+
loop each subscription
|
|
107
|
+
PR->>WP: sendNotification(subscription, { type: "remote-command", clientId, action, args, prompt })
|
|
108
|
+
end
|
|
109
|
+
WP-->>SW: push event
|
|
110
|
+
SW->>OW: client.postMessage({ type: "remote-command-trigger", clientId, action, args, prompt })
|
|
111
|
+
SW->>SW: showNotification("ShadowClaw — Remote Command")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
1. **Server:** The control plane POSTs to `/push/command` with `{ clientId, action, args, prompt }`.
|
|
115
|
+
2. **`push-routes.ts`:** Calls `getAllSubscriptions()` (results are stably ordered by `id DESC`) and fans out a Web Push to every subscribed endpoint with payload `{ type: "remote-command", clientId, action, args, prompt }`.
|
|
116
|
+
3. **Service worker (`push-handler.ts`):** On receiving a push whose `data.type === 'remote-command'`, the handler:
|
|
117
|
+
- Iterates all open `WindowClient`s and calls `client.postMessage({ type: 'remote-command-trigger', clientId, action, args, prompt })`.
|
|
118
|
+
- Shows an OS notification titled **`ShadowClaw — Remote Command`** so the user is informed even if no window catches the message.
|
|
119
|
+
|
|
120
|
+
### Client-side handling
|
|
121
|
+
|
|
122
|
+
Open windows listen for `message` events from the service worker. When a message of type `'remote-command-trigger'` arrives, the orchestrator can act on `action`, `args`, and `prompt` in the same way as a locally-initiated command. The `clientId` field lets the handler route the command only to the intended client and ignore it otherwise.
|
|
123
|
+
|
|
124
|
+
### Push subscription ordering
|
|
125
|
+
|
|
126
|
+
`push-store.ts`'s `getAllSubscriptions()` query now applies a secondary `ORDER BY … id DESC` clause, ensuring that subscriptions are returned in a stable, deterministic order. This prevents non-deterministic fan-out behaviour when multiple subscriptions exist for the same browser profile.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
78
130
|
## Client-Side Task Scheduler
|
|
79
131
|
|
|
80
132
|
**File:** `src/subsystems/tools/task-scheduler.ts`
|
|
@@ -216,6 +216,7 @@ _profiles (State) ──────────┘
|
|
|
216
216
|
| Page Header | `<shadow-claw-page-header>` | Reusable mobile-first header |
|
|
217
217
|
| Settings — Accounts | `<shadow-claw-accounts>` | General account connection and OAuth flows |
|
|
218
218
|
| Settings — Channels | `<shadow-claw-channel-config>` | Third-party messaging channels configuration |
|
|
219
|
+
| Settings — Control Plane | `<shadow-claw-control-plane>` | CLI and remote control plane client configuration |
|
|
219
220
|
| Settings — Git | `<shadow-claw-git>` | Git token and proxy settings |
|
|
220
221
|
| Settings — Integrations | `<shadow-claw-integrations>` | Special API integrations and Web Search |
|
|
221
222
|
| Settings — LLM | `<shadow-claw-llm>` | Provider and model settings |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{xn as e}from"./orchestrator-3h9xxHUm.js";import{t}from"./getGroupDir-P1h9wl6S.js";async function n(n,r,i){let a=await t(n,r),{dirs:o,filename:s}=e(i),c=a;for(let e of o)c=await c.getDirectoryHandle(e);let l=await(await c.getFileHandle(s)).getFile(),u=URL.createObjectURL(l),d=document.createElement(`a`);d.href=u,d.download=s,document.body.appendChild(d),d.click(),document.body.removeChild(d),URL.revokeObjectURL(u)}export{n as t};
|
|
@@ -380,6 +380,7 @@ Settings panel controller.
|
|
|
380
380
|
- `sidebarTasksHiddenToggle()` — Toggle visibility for Tasks navigation item in sidebar
|
|
381
381
|
- `sidebarFilesHiddenToggle()` — Toggle visibility for Files navigation item in sidebar
|
|
382
382
|
- `taskServerConfig()` — Task server configuration component (`<shadow-claw-task-server>`)
|
|
383
|
+
- `controlPlaneConfig()` — Control plane configuration component (`<shadow-claw-control-plane>`)
|
|
383
384
|
|
|
384
385
|
## Shared Utilities (`shared/index.ts`)
|
|
385
386
|
|
|
@@ -454,11 +455,14 @@ The build, CLI, and prerender test suites live under `bin/` and run through the
|
|
|
454
455
|
project Jest configuration. The build integration test covers both the normal
|
|
455
456
|
`pages/` layout and a project with no `pages/` directory (which publishes the
|
|
456
457
|
built-in `index.html` and `MEMORY.md` defaults), as well as the unified `shadow-claw`
|
|
457
|
-
CLI tool commands (`build`, `dev`, `run`, `serve`, `init`, `--version`).
|
|
458
|
+
CLI tool commands (`build`, `dev`, `run`, `serve`, `init`, `clients`, `send`, `backup`, `tasks`, `webrtc`, `peer-id`, `--version`).
|
|
458
459
|
|
|
459
460
|
```bash
|
|
460
461
|
NODE_OPTIONS="--no-warnings --experimental-vm-modules" \
|
|
461
462
|
npx jest --runInBand bin/cli.test.mjs \
|
|
463
|
+
bin/commands/peer-id.test.mjs \
|
|
464
|
+
bin/utils/control-client.test.mjs \
|
|
465
|
+
bin/utils/webrtc-control-client.test.mjs \
|
|
462
466
|
bin/build/build.test.mjs \
|
|
463
467
|
bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs \
|
|
464
468
|
bin/prerender-pretty-paths/prerender-pretty-paths.test.mjs
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{W as e,it as t,tt as n,v as r}from"./orchestrator-
|
|
1
|
+
import{W as e,it as t,tt as n,v as r}from"./orchestrator-3h9xxHUm.js";function i(i,a){globalThis.__SHADOWCLAW_E2E__={isReady(){return i.ready},getDb(){return a.db},getActiveGroupId(){return i.activeGroupId},async createConversation(e){let t=a.db;if(!t)throw Error(`[E2E Bridge] DB not ready`);return i.createConversation(t,e)},async switchConversation(e){let t=a.db;if(!t)throw Error(`[E2E Bridge] DB not ready`);await i.switchConversation(t,e)},async loadTasks(){let e=a.db;if(!e)throw Error(`[E2E Bridge] DB not ready`);await i.loadTasks(e)},async configureProvider(o,s,c,l){let u=i.orchestrator,d=a.db;if(!u||!d)throw Error(`[E2E Bridge] Orchestrator or DB not ready`);await t(u,d,o,{loadApiKeyForProvider:u.loadApiKeyForProvider.bind(u),getApiKeyForHeaders:()=>e(u)}),await u.setApiKey(d,s),await n(u,d,c),await r(u,d,l)}}}function a(){return globalThis.__SHADOWCLAW_E2E_ENABLE__===!0}export{a as n,i as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as e}from"./config-CrHchneq.js";import{n as t}from"./toast-60iDlgiH.js";import{_n as n,bn as r,xn as i}from"./orchestrator-3h9xxHUm.js";import{t as a}from"./getGroupDir-P1h9wl6S.js";async function o(e,t,n){let r=await a(e,t),{dirs:o,filename:s}=i(n),c=r;for(let e of o)c=await c.getDirectoryHandle(e);return await(await c.getFileHandle(s)).getFile()}const s=new class{_file;constructor(){this._file=new t.State(null)}closeFile(){this._file.set(null)}get file(){return this._file.get()}getFile(){return this.file}getPreviewBinaryMimeType(e){return{"3gp":`video/3gpp`,"7z":`application/x-7z-compressed`,aac:`audio/aac`,apng:`image/apng`,avi:`video/x-msvideo`,avif:`image/avif`,bin:`application/octet-stream`,bmp:`image/bmp`,db:`application/vnd.sqlite3`,dll:`application/octet-stream`,dmg:`application/octet-stream`,dylib:`application/octet-stream`,exe:`application/vnd.microsoft.portable-executable`,flac:`audio/flac`,flv:`video/x-flv`,gif:`image/gif`,gz:`application/gzip`,heic:`image/heic`,heif:`image/heif`,ico:`image/x-icon`,iso:`application/octet-stream`,jpeg:`image/jpeg`,jpg:`image/jpeg`,m4a:`audio/mp4`,m4v:`video/mp4`,mkv:`video/x-matroska`,mov:`video/mp4`,mp3:`audio/mpeg`,mp4:`video/mp4`,oga:`audio/ogg`,ogg:`audio/ogg`,ogv:`video/ogg`,png:`image/png`,rar:`application/vnd.rar`,so:`application/octet-stream`,sqlite:`application/vnd.sqlite3`,sqlite3:`application/vnd.sqlite3`,tar:`application/x-tar`,tif:`image/tiff`,tiff:`image/tiff`,wasm:`application/wasm`,wav:`audio/wav`,weba:`audio/webm`,webm:`video/webm`,webp:`image/webp`,wmv:`video/x-ms-wmv`,zip:`application/zip`}[e.toLowerCase().split(`.`).pop()||``]||``}async openFile(t,i,a=e){try{let e=i.split(`/`).pop()||i,s=/\.pdf$/i.test(e),c=this.getPreviewBinaryMimeType(e);if(s){let n=await r(t,a,i);this._file.set({name:e,path:i,groupId:a,content:``,kind:`pdf`,binaryContent:n,mimeType:`application/pdf`});return}if(c){let n=await o(t,a,i);this._file.set({name:e,path:i,groupId:a,content:``,kind:`binary`,binaryContent:null,nativeFile:n,mimeType:c});return}let l=await n(t,a,i);this._file.set({name:e,path:i,groupId:a,content:l,kind:`text`,binaryContent:null,mimeType:`text/plain`})}catch(e){throw console.error(`Failed to open file:`,i,e),e}}};export{s as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{t as e}from"./txPromise-EBECky1b.js";function t(t){return e(t,`tasks`,`readonly`,e=>e.getAll()).then(e=>e.map(e=>({...e,enabled:!!e.enabled})))}export{t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{n as e}from"./rolldown-runtime-aKtaBQYM.js";import{r as t,x as n}from"./config-CrHchneq.js";import{i as r}from"./app-routes-Bs-riIvr.js";import{t as i}from"./txPromise-EBECky1b.js";import{t as a}from"./getConfig-D89uJgo5.js";import{t as o}from"./memoryStorage-C0KvLNUp.js";async function s(e,...t){let n=e;for(let e of t){let t=await n.getDirectoryHandle(e,{create:!0});if(typeof n.isSameEntry==`function`&&await n.isSameEntry(t))throw Error(`Directory navigation failed: "${e}" resolved to the same directory as its parent. The stored directory handle may be stale — try re-selecting the storage folder.`);n=t}return n}function c(e,t){return i(e,`config`,`readwrite`,e=>e.delete(t)).then(()=>void 0)}let l=null;async function u(e,t){try{return await e.entries().next(),`granted`}catch{return t}}function d(){let e=r();return e?`${n}-${e}`:n}async function f(){try{if(typeof navigator<`u`&&navigator.storage&&typeof navigator.storage.getDirectory==`function`){let e=await navigator.storage.getDirectory(),t=d();return await e.getDirectoryHandle(t,{create:!0})}}catch(e){console.warn(`OPFS is unavailable (e.g. Firefox Private Browsing). Using in-memory storage fallback:`,e)}return o()}function p(e){if(!e||typeof e!=`object`)return!1;let t=globalThis.FileSystemDirectoryHandle;return t!==void 0&&e instanceof t||typeof e.getDirectoryHandle==`function`&&typeof e.getFileHandle==`function`}async function m(e){if(l)return l;try{let n=await a(e,t.STORAGE_HANDLE);if(p(n)){let e=n;if(typeof e.queryPermission==`function`&&await e.queryPermission({mode:`readwrite`})!==`granted`&&typeof e.requestPermission==`function`)try{await e.requestPermission({mode:`readwrite`})}catch{}return e}}catch(e){console.warn(`Failed to retrieve local storage handle from DB:`,e)}return f()}function h(e){l=e}function g(){l=null}function _(e){return e instanceof DOMException&&e.name===`InvalidStateError`}async function v(e){await c(e,t.STORAGE_HANDLE),l=null}async function y(e){if(l)try{let e=await l.queryPermission({mode:`readwrite`});return{type:`local`,permission:e===`granted`?`granted`:await u(l,e),name:l.name}}catch{console.warn(`Getting storage status failed for explicit storage root.`)}try{let n=await a(e,t.STORAGE_HANDLE);if(p(n)){let e=n,t=await e.queryPermission({mode:`readwrite`});return{type:`local`,permission:t===`granted`?`granted`:await u(e,t),name:e.name}}}catch(e){console.warn(`Failed to check storage status:`,e)}return{type:`opfs`,permission:`granted`,name:`OPFS`}}var b=e({getGroupDir:()=>x});async function x(e,t){return s(await m(e),`groups`,t.replace(/:/g,`-`))}export{y as a,v as c,m as i,h as l,b as n,g as o,d as r,_ as s,x as t};
|