shadow-claw 1.23.4 → 1.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +12 -3
- package/dist/public/README.md +42 -5
- package/dist/public/agent.worker.js +50 -50
- package/dist/public/backup-controller-CvzDOq4T.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-UfQ6HUlv.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 +169 -0
- package/dist/public/docs/subsystems/notifications.md +54 -2
- package/dist/public/docs/subsystems/reactive-ui.md +1 -0
- package/dist/public/downloadGroupFile-9niHBj9n.js +1 -0
- package/dist/public/e2e/README.md +5 -1
- package/dist/public/{e2e-bridge-BQ4KzUt5.js → e2e-bridge-D28tOggc.js} +1 -1
- package/dist/public/file-viewer-BVg3B_JH.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-DKNht5ph.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-SiFq0PTy.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-8w9-exfN.js +15 -0
- package/dist/public/{orchestrator-DRfEudXk.js → orchestrator-tHWrqi_F.js} +676 -676
- package/dist/public/{peerjs-CurBuKvk.js → peerjs-D8hck8Ih.js} +1 -1
- package/dist/public/push-client-D0lkwrK0.js +1 -0
- package/dist/public/service-worker/fetch-proxy.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-Cs8wbk3d.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-mY-JMvib.js} +1 -1
- package/dist/public/{shadow-claw-chat-CCLuhWdI.js → shadow-claw-chat-DGPKpmU5.js} +6 -6
- package/dist/public/shadow-claw-control-plane-B1F0jnPk.js +199 -0
- package/dist/public/{shadow-claw-conversations-DV-87UUh.js → shadow-claw-conversations-0Q-VC_7N.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-AZfG4D-u.js} +1 -1
- package/dist/public/shadow-claw-files-Bx4v0Ggp.js +840 -0
- package/dist/public/{shadow-claw-git-DRA7_qck.js → shadow-claw-git-DD7iQRxq.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-CfeDBZKB.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-e0EarDVs.js} +1 -1
- package/dist/public/{shadow-claw-notifications-DV2wibL3.js → shadow-claw-notifications-BkxuDEQI.js} +3 -3
- 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-BWSE91I1.js} +1 -1
- package/dist/public/{shadow-claw-settings-S6DAKfYx.js → shadow-claw-settings-D6lPBn6m.js} +10 -3
- package/dist/public/{shadow-claw-storage-DaacA3_K.js → shadow-claw-storage-Crs6LY15.js} +2 -2
- package/dist/public/{shadow-claw-task-server-IjKX042p.js → shadow-claw-task-server-CKm_Ennl.js} +25 -4
- package/dist/public/{shadow-claw-tasks-Cy0JYmGU.js → shadow-claw-tasks-Czx6Ucf5.js} +1 -1
- package/dist/public/{shadow-claw-terminal-znAt7HyP.js → shadow-claw-terminal-Dmtr9KY1.js} +1 -1
- package/dist/public/{shadow-claw-tools-Cutv9Si5.js → shadow-claw-tools-t3Qq4TDO.js} +1 -1
- package/dist/public/{shadow-claw-webvm-BH8mPDEu.js → shadow-claw-webvm-CIuSywD_.js} +1 -1
- package/dist/public/{syncWebMcpRegistration-BqJxaMGR.js → syncWebMcpRegistration-DzC80wVG.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-notifications/shadow-claw-notifications.test.ts +3 -0
- package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.ts +43 -9
- 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 +166 -0
- package/src/core/control-plane-client.test.ts +415 -0
- package/src/core/control-plane-client.ts +441 -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/task.ts +46 -15
- 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 +20 -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 +508 -0
- package/src/server/control-plane.ts +607 -0
- package/src/server/middleware/pna.test.ts +15 -3
- package/src/server/middleware/pna.ts +6 -2
- package/src/server/routes/backup.test.ts +277 -0
- package/src/server/routes/backup.ts +291 -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 +37 -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-rules.test.ts +41 -0
- package/src/service-worker/fetch-proxy-rules.ts +15 -8
- package/src/service-worker/fetch-proxy.test.ts +64 -1
- package/src/service-worker/fetch-proxy.ts +18 -2
- 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-client.test.ts +5 -1
- package/src/subsystems/notifications/push-client.ts +50 -12
- 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/push-client-Dii-vPEh.js +0 -1
- package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
- package/dist/public/shadow-claw-files-BpfpJIuP.js +0 -840
package/README.md
CHANGED
|
@@ -2,23 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://deepwiki.com/xt-ml/shadow-claw)
|
|
4
4
|
|
|
5
|
-
A browser-native, fully-featured AI assistant. TypeScript + Web Components + IndexedDB + File System API.
|
|
5
|
+
A browser-native, fully-featured AI assistant — runs as a PWA, a native desktop app, a Node.js server, and via the `shadow-claw` CLI. TypeScript + Web Components + IndexedDB + File System API.
|
|
6
6
|
|
|
7
7
|
[](https://xt-ml.github.io/shadow-claw/)
|
|
8
8
|
|
|
9
9
|
## Quick Start
|
|
10
10
|
|
|
11
|
+
### 1. Launch ShadowClaw instantly without cloning the repo
|
|
12
|
+
|
|
11
13
|
```bash
|
|
12
|
-
|
|
14
|
+
npx shadow-claw dev --open
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
Open Settings, select
|
|
17
|
+
Open Settings, select your AI provider (or use the in-browser Prompt API / local models), and start chatting.
|
|
18
|
+
|
|
19
|
+
### 2. Scaffold a Project or Knowledge Hub
|
|
20
|
+
|
|
21
|
+
Create a custom template with pages and site configuration:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx shadow-claw init my-assistant
|
|
25
|
+
cd my-assistant
|
|
26
|
+
npx shadow-claw dev --open
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 3. From Source (Contributing / Development)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/xt-ml/shadow-claw.git
|
|
33
|
+
cd shadow-claw
|
|
34
|
+
npm install && npm run dev # Dev server → http://localhost:8888
|
|
35
|
+
```
|
|
16
36
|
|
|
17
|
-
**Desktop:** `npm run electron` or `npm run electron:build` for distributable.
|
|
37
|
+
**Desktop App:** `npm run electron` or `npm run electron:build` for distributable installer.
|
|
18
38
|
|
|
19
39
|
## What is ShadowClaw?
|
|
20
40
|
|
|
21
|
-
A fully-functional agent runtime
|
|
41
|
+
A fully-functional agent runtime built on browser-native technology — the orchestration and tool-use loop run client-side, with AI inference routing to remote APIs, local servers, or in-browser models depending on your configuration. Built with TypeScript, it provides:
|
|
22
42
|
|
|
23
43
|
- **Multi-model support**: OpenRouter, Anthropic, Google Gemini, AWS Bedrock, Ollama, Llamafile, Mesh LLM, Transformers.js, and browser-native Prompt API
|
|
24
44
|
- **Web Components UI**: Native Custom Elements + TC39 Signals for reactive updates
|
|
@@ -378,6 +398,23 @@ npm run electron:build:win # Build Electron for Windows
|
|
|
378
398
|
npm run electron:build:mac # Build Electron for macOS
|
|
379
399
|
```
|
|
380
400
|
|
|
401
|
+
### CLI Runtime Commands
|
|
402
|
+
|
|
403
|
+
The `shadow-claw` CLI connects to a running server or browser clients to interact with active sessions:
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
npx shadow-claw clients # List connected browser/Electron clients
|
|
407
|
+
npx shadow-claw send "your prompt" --client <id> # Send a message to a connected client
|
|
408
|
+
npx shadow-claw backup # Trigger OPFS workspace backup
|
|
409
|
+
npx shadow-claw backup list # List available backup snapshots
|
|
410
|
+
npx shadow-claw backup delete --backup-id <id> # Delete a backup snapshot
|
|
411
|
+
npx shadow-claw tasks --client <id> # List scheduled tasks on a client
|
|
412
|
+
npx shadow-claw webrtc listen # Start headless WebRTC DataChannel daemon
|
|
413
|
+
npx shadow-claw peer-id # Get or generate persistent CLI Peer ID
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Commands support `--transport webrtc` for direct peer-to-peer DataChannel execution with connected browser clients. Control plane authentication uses `SHADOWCLAW_CONTROL_TOKEN` (env) or `--token` flag. The control plane endpoint and token are printed to the console on server start.
|
|
417
|
+
|
|
381
418
|
## License
|
|
382
419
|
|
|
383
420
|
AGPLv3. Core logic derived from [openbrowserclaw](https://github.com/sachaa/openbrowserclaw) (MIT).
|
package/bin/cli.mjs
CHANGED
|
@@ -8,6 +8,12 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
|
|
10
10
|
import { runBuild } from "./build/build.mjs";
|
|
11
|
+
import { runClientsCommand } from "./commands/clients.mjs";
|
|
12
|
+
import { runSendCommand } from "./commands/send.mjs";
|
|
13
|
+
import { runBackupCommand } from "./commands/backup.mjs";
|
|
14
|
+
import { runTasksCommand } from "./commands/tasks.mjs";
|
|
15
|
+
import { runPeerIdCommand } from "./commands/peer-id.mjs";
|
|
16
|
+
import { runWebRtcListenCommand } from "./commands/webrtc-listen.mjs";
|
|
11
17
|
|
|
12
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
19
|
const __dirname = path.dirname(__filename);
|
|
@@ -421,4 +427,164 @@ slug: "home"
|
|
|
421
427
|
}
|
|
422
428
|
});
|
|
423
429
|
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// PEER-ID / WEBRTC COMMAND
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
program
|
|
434
|
+
.command("peer-id [action] [customId]")
|
|
435
|
+
.alias("webrtc-peer-id")
|
|
436
|
+
.description(
|
|
437
|
+
"Get, generate, or renew the WebRTC CLI peer ID stored in .cache/cli-peer-id",
|
|
438
|
+
)
|
|
439
|
+
.option(
|
|
440
|
+
"-r, --renew",
|
|
441
|
+
"Force renewal / generation of a new WebRTC CLI peer ID",
|
|
442
|
+
)
|
|
443
|
+
.option("--set <id>", "Set a specific custom WebRTC CLI peer ID")
|
|
444
|
+
.option("--cache-dir <dir>", "Custom cache directory (defaults to .cache)")
|
|
445
|
+
.option("-q, --quiet", "Output only the peer ID string")
|
|
446
|
+
.option("--json", "Output peer ID and file path as JSON")
|
|
447
|
+
.action(async (action, customId, options) => {
|
|
448
|
+
const opts = { ...options };
|
|
449
|
+
if (customId && !opts.set) {
|
|
450
|
+
opts.set = customId;
|
|
451
|
+
}
|
|
452
|
+
await runPeerIdCommand(action, opts);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
program
|
|
456
|
+
.command("webrtc [action] [customId]")
|
|
457
|
+
.description(
|
|
458
|
+
"Manage WebRTC CLI configuration and peer identity (.cache/cli-peer-id)\n\n" +
|
|
459
|
+
" Actions:\n" +
|
|
460
|
+
" (default) Show or generate the CLI peer ID\n" +
|
|
461
|
+
" listen Register as a live PeerJS peer — browser tabs can then\n" +
|
|
462
|
+
" connect directly without a control-plane connection",
|
|
463
|
+
)
|
|
464
|
+
.option(
|
|
465
|
+
"-r, --renew",
|
|
466
|
+
"Force renewal / generation of a new WebRTC CLI peer ID",
|
|
467
|
+
)
|
|
468
|
+
.option("--set <id>", "Set a specific custom WebRTC CLI peer ID")
|
|
469
|
+
.option("--cache-dir <dir>", "Custom cache directory (defaults to .cache)")
|
|
470
|
+
.option("-q, --quiet", "Output only the peer ID string")
|
|
471
|
+
.option("--json", "Output peer ID and file path as JSON")
|
|
472
|
+
// listen-specific options
|
|
473
|
+
.option("--host <host>", "PeerJS signaling server host (for listen)")
|
|
474
|
+
.option("--port <port>", "PeerJS signaling server port (for listen)", "8888")
|
|
475
|
+
.option("--path <path>", "PeerJS signaling server path (for listen)", "/")
|
|
476
|
+
.option(
|
|
477
|
+
"--secure",
|
|
478
|
+
"Use TLS (wss://) for the signaling server (for listen)",
|
|
479
|
+
false,
|
|
480
|
+
)
|
|
481
|
+
.option(
|
|
482
|
+
"--trusted-peer <id>",
|
|
483
|
+
"Accept connections only from this peer ID (repeatable, for listen)",
|
|
484
|
+
(v, prev) => (prev ? [...prev, v] : [v]),
|
|
485
|
+
[],
|
|
486
|
+
)
|
|
487
|
+
.option("--verbose", "Verbose connection logging (for listen)", false)
|
|
488
|
+
.option("--renew-peer-id", "Renew CLI peer ID before listening", false)
|
|
489
|
+
.action(async (action, customId, options) => {
|
|
490
|
+
if (action === "listen") {
|
|
491
|
+
await runWebRtcListenCommand(options);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const opts = { ...options };
|
|
495
|
+
if (customId && !opts.set) {
|
|
496
|
+
opts.set = customId;
|
|
497
|
+
}
|
|
498
|
+
await runPeerIdCommand(action, opts);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// CLIENTS COMMAND
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
program
|
|
505
|
+
.command("clients")
|
|
506
|
+
.description("List connected / registered browser and Electron clients")
|
|
507
|
+
.option("--host <host>", "Control plane host")
|
|
508
|
+
.option("--port <port>", "Control plane port")
|
|
509
|
+
.option("--token <token>", "Control token")
|
|
510
|
+
.option("--transport <transport>", "Transport to use: http | webrtc", "http")
|
|
511
|
+
.option("--peer-id <id>", "Custom WebRTC CLI peer ID")
|
|
512
|
+
.option(
|
|
513
|
+
"--renew-peer-id",
|
|
514
|
+
"Renew WebRTC CLI peer ID before connecting",
|
|
515
|
+
false,
|
|
516
|
+
)
|
|
517
|
+
.option("--cache-dir <dir>", "Custom cache directory")
|
|
518
|
+
.action(async (options) => {
|
|
519
|
+
await runClientsCommand(options);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
// SEND COMMAND
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
program
|
|
526
|
+
.command("send <message>")
|
|
527
|
+
.description("Send a message/prompt to a connected client")
|
|
528
|
+
.option("--client <id>", "Target client ID (defaults to first available)")
|
|
529
|
+
.option("--group <groupId>", "Target conversation group ID")
|
|
530
|
+
.option("--host <host>", "Control plane host")
|
|
531
|
+
.option("--port <port>", "Control plane port")
|
|
532
|
+
.option("--token <token>", "Control token")
|
|
533
|
+
.option("--transport <transport>", "Transport to use: http | webrtc", "http")
|
|
534
|
+
.option("--peer-id <id>", "Custom WebRTC CLI peer ID")
|
|
535
|
+
.option("--renew-peer-id", "Renew WebRTC CLI peer ID before sending", false)
|
|
536
|
+
.option("--cache-dir <dir>", "Custom cache directory")
|
|
537
|
+
.action(async (message, options) => {
|
|
538
|
+
await runSendCommand(message, options);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// ---------------------------------------------------------------------------
|
|
542
|
+
// BACKUP COMMAND
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
program
|
|
545
|
+
.command("backup [action]")
|
|
546
|
+
.description(
|
|
547
|
+
"Trigger or manage client workspace backups (trigger | list | delete)",
|
|
548
|
+
)
|
|
549
|
+
.option("--client <id>", "Client ID")
|
|
550
|
+
.option("--backup-id <id>", "Backup ID (for delete)")
|
|
551
|
+
.option("--group <groupId>", "Specific workspace group ID")
|
|
552
|
+
.option("--host <host>", "Control plane host")
|
|
553
|
+
.option("--port <port>", "Control plane port")
|
|
554
|
+
.option("--token <token>", "Control token")
|
|
555
|
+
.option("--transport <transport>", "Transport to use: http | webrtc", "http")
|
|
556
|
+
.option("--peer-id <id>", "Custom WebRTC CLI peer ID")
|
|
557
|
+
.option(
|
|
558
|
+
"--renew-peer-id",
|
|
559
|
+
"Renew WebRTC CLI peer ID before connecting",
|
|
560
|
+
false,
|
|
561
|
+
)
|
|
562
|
+
.option("--cache-dir <dir>", "Custom cache directory")
|
|
563
|
+
.action(async (action, options) => {
|
|
564
|
+
await runBackupCommand(action || "trigger", options);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
// TASKS COMMAND
|
|
569
|
+
// ---------------------------------------------------------------------------
|
|
570
|
+
program
|
|
571
|
+
.command("tasks")
|
|
572
|
+
.description("List scheduled tasks on a connected client")
|
|
573
|
+
.option("--client <id>", "Target client ID")
|
|
574
|
+
.option("--group <groupId>", "Filter tasks by conversation group ID")
|
|
575
|
+
.option("--host <host>", "Control plane host")
|
|
576
|
+
.option("--port <port>", "Control plane port")
|
|
577
|
+
.option("--token <token>", "Control token")
|
|
578
|
+
.option("--transport <transport>", "Transport to use: http | webrtc", "http")
|
|
579
|
+
.option("--peer-id <id>", "Custom WebRTC CLI peer ID")
|
|
580
|
+
.option(
|
|
581
|
+
"--renew-peer-id",
|
|
582
|
+
"Renew WebRTC CLI peer ID before connecting",
|
|
583
|
+
false,
|
|
584
|
+
)
|
|
585
|
+
.option("--cache-dir <dir>", "Custom cache directory")
|
|
586
|
+
.action(async (options) => {
|
|
587
|
+
await runTasksCommand(options);
|
|
588
|
+
});
|
|
589
|
+
|
|
424
590
|
program.parse(process.argv);
|
package/bin/cli.test.mjs
CHANGED
|
@@ -49,6 +49,66 @@ describe("shadow-claw CLI", () => {
|
|
|
49
49
|
expect(stdout).toContain("run");
|
|
50
50
|
expect(stdout).toContain("serve");
|
|
51
51
|
expect(stdout).toContain("init");
|
|
52
|
+
expect(stdout).toContain("clients");
|
|
53
|
+
expect(stdout).toContain("send");
|
|
54
|
+
expect(stdout).toContain("backup");
|
|
55
|
+
expect(stdout).toContain("tasks");
|
|
56
|
+
expect(stdout).toContain("peer-id");
|
|
57
|
+
expect(stdout).toContain("webrtc");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("manages WebRTC CLI peer ID with peer-id command", async () => {
|
|
61
|
+
const cacheDir = path.join(tempDir, ".cache");
|
|
62
|
+
|
|
63
|
+
// 1. Get/generate new peer ID
|
|
64
|
+
const { stdout: out1 } = await execFileAsync(process.execPath, [
|
|
65
|
+
cliPath,
|
|
66
|
+
"peer-id",
|
|
67
|
+
"--cache-dir",
|
|
68
|
+
cacheDir,
|
|
69
|
+
"--json",
|
|
70
|
+
]);
|
|
71
|
+
const json1 = JSON.parse(out1);
|
|
72
|
+
expect(json1.peerId).toMatch(/^cli-[0-9a-z]+$/);
|
|
73
|
+
expect(json1.renewed).toBe(true);
|
|
74
|
+
|
|
75
|
+
// 2. Subsequent call returns existing peer ID
|
|
76
|
+
const { stdout: out2 } = await execFileAsync(process.execPath, [
|
|
77
|
+
cliPath,
|
|
78
|
+
"peer-id",
|
|
79
|
+
"--cache-dir",
|
|
80
|
+
cacheDir,
|
|
81
|
+
"-q",
|
|
82
|
+
]);
|
|
83
|
+
expect(out2.trim()).toBe(json1.peerId);
|
|
84
|
+
|
|
85
|
+
// 3. Renew peer ID
|
|
86
|
+
const { stdout: out3 } = await execFileAsync(process.execPath, [
|
|
87
|
+
cliPath,
|
|
88
|
+
"peer-id",
|
|
89
|
+
"--renew",
|
|
90
|
+
"--cache-dir",
|
|
91
|
+
cacheDir,
|
|
92
|
+
"--json",
|
|
93
|
+
]);
|
|
94
|
+
const json3 = JSON.parse(out3);
|
|
95
|
+
expect(json3.peerId).not.toBe(json1.peerId);
|
|
96
|
+
expect(json3.renewed).toBe(true);
|
|
97
|
+
|
|
98
|
+
// 4. Set custom peer ID
|
|
99
|
+
const { stdout: out4 } = await execFileAsync(process.execPath, [
|
|
100
|
+
cliPath,
|
|
101
|
+
"peer-id",
|
|
102
|
+
"--set",
|
|
103
|
+
"my-custom-cli-peer",
|
|
104
|
+
"--cache-dir",
|
|
105
|
+
cacheDir,
|
|
106
|
+
"-q",
|
|
107
|
+
]);
|
|
108
|
+
expect(out4.trim()).toBe("my-custom-cli-peer");
|
|
109
|
+
|
|
110
|
+
const saved = await readFile(path.join(cacheDir, "cli-peer-id"), "utf8");
|
|
111
|
+
expect(saved.trim()).toBe("my-custom-cli-peer");
|
|
52
112
|
});
|
|
53
113
|
|
|
54
114
|
it("initializes a new template with init command", async () => {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShadowClaw CLI — `backup` command
|
|
3
|
+
* Triggers or lists workspace backups.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CliControlClient } from "../utils/control-client.mjs";
|
|
7
|
+
|
|
8
|
+
export async function runBackupCommand(action = "trigger", options = {}) {
|
|
9
|
+
const client = new CliControlClient(options);
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
if (action === "list") {
|
|
13
|
+
const backups = await client.listBackups(options.client);
|
|
14
|
+
if (!backups || backups.length === 0) {
|
|
15
|
+
console.log("No backups found.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(`Available backups (${backups.length}):\n`);
|
|
20
|
+
backups.forEach((b, idx) => {
|
|
21
|
+
const dateStr = new Date(b.timestamp).toISOString();
|
|
22
|
+
const sizeMb = (b.totalBytes / (1024 * 1024)).toFixed(2);
|
|
23
|
+
console.log(` ${idx + 1}. [${b.id}] Client: ${b.clientId}`);
|
|
24
|
+
console.log(` Date: ${dateStr}`);
|
|
25
|
+
console.log(` Files: ${b.fileCount} (${sizeMb} MB)\n`);
|
|
26
|
+
});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (action === "delete") {
|
|
31
|
+
if (!options.backupId) {
|
|
32
|
+
console.error("Error: --backup-id is required for delete action.");
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
await client.deleteBackup(options.backupId, options.client);
|
|
37
|
+
console.log(`Backup ${options.backupId} deleted.`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Default: Trigger backup
|
|
42
|
+
let targetClientId = options.client;
|
|
43
|
+
if (!targetClientId) {
|
|
44
|
+
const clients = await client.listClients();
|
|
45
|
+
if (!clients || clients.length === 0) {
|
|
46
|
+
console.error("Error: No clients are connected to back up.");
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
targetClientId = clients[0].clientId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(`Requesting backup from client ${targetClientId}...`);
|
|
54
|
+
const result = await client.sendCommand(targetClientId, "trigger-backup", {
|
|
55
|
+
groupId: options.group,
|
|
56
|
+
token: client.token,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.success) {
|
|
60
|
+
console.log("Backup completed successfully.");
|
|
61
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
62
|
+
} else {
|
|
63
|
+
console.error(`Backup failed: ${result.error}`);
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(`Error executing backup command: ${err.message}`);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShadowClaw CLI — `clients` command
|
|
3
|
+
* Lists connected clients and their capabilities.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CliControlClient } from "../utils/control-client.mjs";
|
|
7
|
+
|
|
8
|
+
export async function runClientsCommand(options = {}) {
|
|
9
|
+
const client = new CliControlClient(options);
|
|
10
|
+
try {
|
|
11
|
+
const clients = await client.listClients();
|
|
12
|
+
|
|
13
|
+
if (!clients || clients.length === 0) {
|
|
14
|
+
console.log("No clients currently registered or connected.");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(`Connected / registered clients (${clients.length}):\n`);
|
|
19
|
+
clients.forEach((c, idx) => {
|
|
20
|
+
const lastSeenSec = Math.max(
|
|
21
|
+
0,
|
|
22
|
+
Math.floor((Date.now() - c.lastSeen) / 1000),
|
|
23
|
+
);
|
|
24
|
+
const lastSeenStr =
|
|
25
|
+
lastSeenSec < 60
|
|
26
|
+
? `${lastSeenSec}s ago`
|
|
27
|
+
: `${Math.floor(lastSeenSec / 60)}m ago`;
|
|
28
|
+
|
|
29
|
+
console.log(` ${idx + 1}. ${c.deviceLabel} (id: ${c.clientId})`);
|
|
30
|
+
if (c.peerId) {
|
|
31
|
+
console.log(` Peer ID: ${c.peerId}`);
|
|
32
|
+
}
|
|
33
|
+
console.log(` Capabilities: ${c.capabilities.join(", ") || "none"}`);
|
|
34
|
+
console.log(` Version: ${c.version}`);
|
|
35
|
+
console.log(` Last seen: ${lastSeenStr}\n`);
|
|
36
|
+
});
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(`Error fetching clients: ${err.message}`);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShadowClaw CLI — `peer-id` command
|
|
3
|
+
* Manages the WebRTC CLI peer ID stored in .cache/cli-peer-id.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getCliPeerIdFilePath,
|
|
8
|
+
getOrCreateCliPeerId,
|
|
9
|
+
readCliPeerId,
|
|
10
|
+
renewCliPeerId,
|
|
11
|
+
} from "../utils/webrtc-control-client.mjs";
|
|
12
|
+
|
|
13
|
+
export async function runPeerIdCommand(action = "get", options = {}) {
|
|
14
|
+
const cacheDir = options.cacheDir;
|
|
15
|
+
const filePath = getCliPeerIdFilePath(cacheDir);
|
|
16
|
+
|
|
17
|
+
const shouldRenew = Boolean(
|
|
18
|
+
options.renew ||
|
|
19
|
+
action === "renew" ||
|
|
20
|
+
action === "generate" ||
|
|
21
|
+
action === "new",
|
|
22
|
+
);
|
|
23
|
+
const customSetId =
|
|
24
|
+
options.set || (action === "set" && options.id ? options.id : null);
|
|
25
|
+
|
|
26
|
+
let peerId = "";
|
|
27
|
+
let isRenewed = false;
|
|
28
|
+
|
|
29
|
+
if (customSetId) {
|
|
30
|
+
peerId = renewCliPeerId(customSetId, cacheDir);
|
|
31
|
+
isRenewed = true;
|
|
32
|
+
} else if (shouldRenew) {
|
|
33
|
+
peerId = renewCliPeerId(undefined, cacheDir);
|
|
34
|
+
isRenewed = true;
|
|
35
|
+
} else {
|
|
36
|
+
const existing = readCliPeerId(cacheDir);
|
|
37
|
+
if (existing) {
|
|
38
|
+
peerId = existing;
|
|
39
|
+
isRenewed = false;
|
|
40
|
+
} else {
|
|
41
|
+
peerId = getOrCreateCliPeerId(undefined, cacheDir);
|
|
42
|
+
isRenewed = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options.quiet) {
|
|
47
|
+
console.log(peerId);
|
|
48
|
+
return { peerId, filePath, isRenewed };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (options.json) {
|
|
52
|
+
console.log(
|
|
53
|
+
JSON.stringify(
|
|
54
|
+
{
|
|
55
|
+
peerId,
|
|
56
|
+
filePath,
|
|
57
|
+
renewed: isRenewed,
|
|
58
|
+
},
|
|
59
|
+
null,
|
|
60
|
+
2,
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
return { peerId, filePath, isRenewed };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (customSetId) {
|
|
67
|
+
console.log(`WebRTC CLI Peer ID set to: ${peerId}`);
|
|
68
|
+
} else if (isRenewed) {
|
|
69
|
+
console.log(`WebRTC CLI Peer ID created/renewed: ${peerId}`);
|
|
70
|
+
} else {
|
|
71
|
+
console.log(`WebRTC CLI Peer ID: ${peerId}`);
|
|
72
|
+
}
|
|
73
|
+
console.log(`Stored in: ${filePath}`);
|
|
74
|
+
|
|
75
|
+
return { peerId, filePath, isRenewed };
|
|
76
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
jest,
|
|
6
|
+
describe,
|
|
7
|
+
it,
|
|
8
|
+
expect,
|
|
9
|
+
beforeEach,
|
|
10
|
+
afterEach,
|
|
11
|
+
} from "@jest/globals";
|
|
12
|
+
import { runPeerIdCommand } from "./peer-id.mjs";
|
|
13
|
+
import {
|
|
14
|
+
getCliPeerIdFilePath,
|
|
15
|
+
readCliPeerId,
|
|
16
|
+
} from "../utils/webrtc-control-client.mjs";
|
|
17
|
+
|
|
18
|
+
describe("runPeerIdCommand", () => {
|
|
19
|
+
let tempDir;
|
|
20
|
+
let logSpy;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
tempDir = path.join(
|
|
24
|
+
os.tmpdir(),
|
|
25
|
+
"test-cli-peer-id-" +
|
|
26
|
+
Date.now() +
|
|
27
|
+
"-" +
|
|
28
|
+
Math.random().toString(36).slice(2),
|
|
29
|
+
);
|
|
30
|
+
logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
if (logSpy) logSpy.mockRestore();
|
|
35
|
+
try {
|
|
36
|
+
if (fs.existsSync(tempDir)) {
|
|
37
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
} catch (_) {}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("creates and retrieves a new peer ID if none exists", async () => {
|
|
43
|
+
const result = await runPeerIdCommand("get", { cacheDir: tempDir });
|
|
44
|
+
expect(result.peerId).toMatch(/^cli-[0-9a-z]+$/);
|
|
45
|
+
expect(result.isRenewed).toBe(true);
|
|
46
|
+
|
|
47
|
+
const saved = readCliPeerId(tempDir);
|
|
48
|
+
expect(saved).toBe(result.peerId);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns existing peer ID without changing it on subsequent calls", async () => {
|
|
52
|
+
const res1 = await runPeerIdCommand("get", { cacheDir: tempDir });
|
|
53
|
+
const res2 = await runPeerIdCommand("get", { cacheDir: tempDir });
|
|
54
|
+
|
|
55
|
+
expect(res2.peerId).toBe(res1.peerId);
|
|
56
|
+
expect(res2.isRenewed).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renews peer ID when action is 'renew'", async () => {
|
|
60
|
+
const res1 = await runPeerIdCommand("get", { cacheDir: tempDir });
|
|
61
|
+
const res2 = await runPeerIdCommand("renew", { cacheDir: tempDir });
|
|
62
|
+
|
|
63
|
+
expect(res2.peerId).not.toBe(res1.peerId);
|
|
64
|
+
expect(res2.isRenewed).toBe(true);
|
|
65
|
+
expect(readCliPeerId(tempDir)).toBe(res2.peerId);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("renews peer ID when --renew flag is passed", async () => {
|
|
69
|
+
const res1 = await runPeerIdCommand("get", { cacheDir: tempDir });
|
|
70
|
+
const res2 = await runPeerIdCommand("get", {
|
|
71
|
+
cacheDir: tempDir,
|
|
72
|
+
renew: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(res2.peerId).not.toBe(res1.peerId);
|
|
76
|
+
expect(res2.isRenewed).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("sets a custom peer ID when --set is passed", async () => {
|
|
80
|
+
const res = await runPeerIdCommand("get", {
|
|
81
|
+
cacheDir: tempDir,
|
|
82
|
+
set: "custom-my-peer",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(res.peerId).toBe("custom-my-peer");
|
|
86
|
+
expect(res.isRenewed).toBe(true);
|
|
87
|
+
expect(readCliPeerId(tempDir)).toBe("custom-my-peer");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("supports quiet mode and json mode", async () => {
|
|
91
|
+
const resQuiet = await runPeerIdCommand("get", {
|
|
92
|
+
cacheDir: tempDir,
|
|
93
|
+
quiet: true,
|
|
94
|
+
});
|
|
95
|
+
expect(resQuiet.peerId).toBeDefined();
|
|
96
|
+
|
|
97
|
+
const resJson = await runPeerIdCommand("get", {
|
|
98
|
+
cacheDir: tempDir,
|
|
99
|
+
json: true,
|
|
100
|
+
});
|
|
101
|
+
expect(resJson.peerId).toBe(resQuiet.peerId);
|
|
102
|
+
expect(resJson.filePath).toBe(getCliPeerIdFilePath(tempDir));
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShadowClaw CLI — `send` command
|
|
3
|
+
* Dispatches a prompt / message to a connected client.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CliControlClient } from "../utils/control-client.mjs";
|
|
7
|
+
|
|
8
|
+
export async function runSendCommand(message, options = {}) {
|
|
9
|
+
if (!message || typeof message !== "string" || !message.trim()) {
|
|
10
|
+
console.error("Error: Message cannot be empty.");
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const client = new CliControlClient(options);
|
|
16
|
+
try {
|
|
17
|
+
let targetClientId = options.client;
|
|
18
|
+
|
|
19
|
+
if (!targetClientId) {
|
|
20
|
+
const clients = await client.listClients();
|
|
21
|
+
if (!clients || clients.length === 0) {
|
|
22
|
+
console.error("Error: No clients are connected.");
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
targetClientId = clients[0].clientId;
|
|
27
|
+
console.log(
|
|
28
|
+
`Targeting client: ${clients[0].deviceLabel} (${targetClientId})`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`Sending message to ${targetClientId}...`);
|
|
33
|
+
const result = await client.sendCommand(targetClientId, "send-message", {
|
|
34
|
+
text: message.trim(),
|
|
35
|
+
groupId: options.group,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (result.success) {
|
|
39
|
+
console.log("Message successfully dispatched.");
|
|
40
|
+
if (result.data) {
|
|
41
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
console.error(`Error from client: ${result.error}`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(`Error sending message: ${err.message}`);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
}
|
|
51
|
+
}
|