shadow-claw 1.23.3 → 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/assert-version-bump.mjs +60 -38
- package/bin/assert-version-bump.test.mjs +74 -11
- 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 +6 -6
- package/dist/public/{app-routes-B0ePT49v.js → app-routes-Bs-riIvr.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-storage/shadow-claw-storage.html +1 -1
- 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-files/shadow-claw-files.css +1 -1
- package/dist/public/components/shadow-claw-files/shadow-claw-files.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-B7kNU8AQ.js → configurePeerJs-CxpZeDJJ.js} +1 -1
- package/dist/public/{connections-xd_66YVU.js → connections-DvbgdkaB.js} +1 -1
- package/dist/public/{custom-element-security-CZ92anB8.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/architecture/storage.md +5 -2
- package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
- package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +6 -3
- package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
- package/dist/public/docs/example/article/index.html +2 -2
- 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 +19 -76
- package/dist/public/docs/skill-creator/index.html +2 -2
- 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-DkS5xFGX.js → e2e-bridge-IGWsRl4i.js} +1 -1
- package/dist/public/file-viewer-D0eflgt6.js +1 -0
- package/dist/public/files/main/~/docs/publishing-to-github-pages.md +17 -82
- package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
- package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
- package/dist/public/{git-BkjrsRXX.js → git-7Vq_SJSp.js} +2 -2
- package/dist/public/{iframe-sanitizer-L6Jyu__Z.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/public/index.js +1 -1
- package/dist/public/{initChatSplitResize-BslPXAXS.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 +2 -2
- package/dist/public/main/memory/index.html +2 -2
- package/dist/public/{markdown-6MBrIo-S.js → markdown-BnpKxSxl.js} +1 -1
- package/dist/public/mcp-reconnect-9emsqGRe.js +1 -0
- package/dist/public/{memoryStorage-CjGseKI1.js → memoryStorage-C0KvLNUp.js} +1 -1
- package/dist/public/{orchestrator-B9KmIj8Q.js → orchestrator-3h9xxHUm.js} +676 -676
- package/dist/public/orchestrator-nJj59RqD.js +15 -0
- package/dist/public/pages/main/~/docs/publishing-to-github-pages.md +17 -82
- package/dist/public/{peerjs-Dv8JNm7v.js → peerjs-BP4laMXj.js} +1 -1
- package/dist/public/{push-client-Dqq06wbB.js → push-client-CrjEXK3U.js} +1 -1
- 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-Bd1rfuVC.js → shadow-claw-CbwETBWf.js} +2 -2
- package/dist/public/{shadow-claw-accounts-DL2l3oM0.js → shadow-claw-accounts-B4sBTQEj.js} +3 -3
- package/dist/public/{shadow-claw-channels-BGXDsQ38.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
- package/dist/public/{shadow-claw-chat-NXg6zz_l.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-C8eqGpP9.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-Blf6yGVD.js +577 -0
- package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
- package/dist/public/{shadow-claw-git-C0ntz7aX.js → shadow-claw-git-v8S0JjuR.js} +3 -3
- package/dist/public/{shadow-claw-integrations-DCXV2r69.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
- package/dist/public/{shadow-claw-llm-CudgFQsS.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
- package/dist/public/{shadow-claw-mcp-remote-B8RkpX28.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
- package/dist/public/{shadow-claw-networking-BLiRsrsK.js → shadow-claw-networking-qHbP95z9.js} +1 -1
- package/dist/public/{shadow-claw-notifications-CQhQcjtL.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
- package/dist/public/{shadow-claw-page-header-NeRD0tKU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
- package/dist/public/{shadow-claw-pages-DCHnFh6r.js → shadow-claw-pages-DFZHhzFZ.js} +3 -3
- package/dist/public/{shadow-claw-settings-VAfjKFQg.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
- package/dist/public/shadow-claw-storage-BFynbP0j.js +219 -0
- package/dist/public/{shadow-claw-task-server-Dk33N_1G.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
- package/dist/public/{shadow-claw-tasks-xA1VfmWN.js → shadow-claw-tasks-Caxc9atL.js} +4 -4
- package/dist/public/{shadow-claw-terminal-Du2r0ciq.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
- package/dist/public/{shadow-claw-tools-CZj4rq7l.js → shadow-claw-tools-BfJx3A9W.js} +2 -2
- package/dist/public/{shadow-claw-webvm-JYUkWICG.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
- package/dist/public/static-main/~/docs/publishing-to-github-pages.md +17 -82
- package/dist/public/static-main-manifest.json +1 -1
- package/dist/public/{syncWebMcpRegistration-DfpvASnh.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.html +17 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +120 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.ts +56 -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 +378 -0
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +11 -9
- package/src/components/shadow-claw-files/shadow-claw-files.css +41 -17
- package/src/components/shadow-claw-files/shadow-claw-files.html +35 -0
- package/src/components/shadow-claw-files/shadow-claw-files.test.ts +627 -1
- package/src/components/shadow-claw-files/shadow-claw-files.ts +170 -6
- package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.test.ts +31 -0
- package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.ts +2 -1
- package/src/components/shadow-claw-pages/utils/rewriteWorkspacePreviewHtml.test.ts +15 -0
- 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.test.ts +9 -0
- package/src/config/config.ts +15 -0
- package/src/core/app-routes.test.ts +56 -0
- package/src/core/app-routes.ts +8 -1
- 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/db/openDatabase.test.ts +10 -1
- package/src/db/openDatabase.ts +3 -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/migrateLegacyOpfs.test.ts +327 -0
- package/src/storage/migrateLegacyOpfs.ts +117 -0
- 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-AL77QAx4.js +0 -1
- package/dist/public/downloadGroupFile-BABv_1mJ.js +0 -1
- package/dist/public/file-viewer-CCSABMsE.js +0 -1
- package/dist/public/mcp-reconnect-DoicazQd.js +0 -1
- package/dist/public/orchestrator-4su7iyWj.js +0 -15
- package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
- package/dist/public/shadow-claw-file-viewer-9Qkwc30T.js +0 -577
- package/dist/public/shadow-claw-files-aDyq6Hv7.js +0 -781
- package/dist/public/shadow-claw-storage-Cf31np81.js +0 -202
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShadowClaw CLI — WebRTC DataChannel Control Client
|
|
3
|
+
*
|
|
4
|
+
* Provides a stateless WebRTC transport client for CLI commands to execute
|
|
5
|
+
* commands directly against browser tabs or server peers over RTCDataChannels.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import http from "node:http";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import { ulid } from "./ulid.mjs";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// IPC socket helpers — allow `send` to talk to a running `webrtc listen`
|
|
15
|
+
// process via a Unix domain socket (.cache/webrtc-ipc.sock).
|
|
16
|
+
// No port selection, no TCP, no collision.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export function getIpcSocketPath(cacheDir) {
|
|
20
|
+
const dir = cacheDir || path.join(process.cwd(), ".cache");
|
|
21
|
+
return path.join(dir, "webrtc-ipc.sock");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Keep getIpcFilePath as an alias pointing at the socket for test compatibility
|
|
25
|
+
export function getIpcFilePath(cacheDir) {
|
|
26
|
+
return getIpcSocketPath(cacheDir);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writeIpcFile(socketPath, cacheDir) {
|
|
30
|
+
// Nothing to write — the socket file IS the IPC endpoint.
|
|
31
|
+
// This function is kept for API compatibility; callers pass the socket path.
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function clearIpcFile(cacheDir) {
|
|
35
|
+
try {
|
|
36
|
+
fs.unlinkSync(getIpcSocketPath(cacheDir));
|
|
37
|
+
} catch (_) {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns the socket path if a live listener is reachable, else null.
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveListenerIpc(cacheDir) {
|
|
44
|
+
const socketPath = getIpcSocketPath(cacheDir);
|
|
45
|
+
try {
|
|
46
|
+
if (!fs.existsSync(socketPath)) return null;
|
|
47
|
+
|
|
48
|
+
// Verify the socket actually responds
|
|
49
|
+
await new Promise((resolve, reject) => {
|
|
50
|
+
const req = http.request(
|
|
51
|
+
{ socketPath, path: "/ping", method: "GET" },
|
|
52
|
+
(res) => {
|
|
53
|
+
res.resume();
|
|
54
|
+
res.statusCode === 200
|
|
55
|
+
? resolve()
|
|
56
|
+
: reject(new Error(`status ${res.statusCode}`));
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
req.setTimeout(1000, () => {
|
|
60
|
+
req.destroy();
|
|
61
|
+
reject(new Error("timeout"));
|
|
62
|
+
});
|
|
63
|
+
req.on("error", reject);
|
|
64
|
+
req.end();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return { socketPath };
|
|
68
|
+
} catch (_) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Send a command to a running listener via its Unix socket.
|
|
75
|
+
*/
|
|
76
|
+
export function sendCommandViaIpc(ipc, targetPeerId, action, args, timeoutMs) {
|
|
77
|
+
const socketPath = ipc.socketPath || ipc.port; // backwards compat
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const body = JSON.stringify({ targetPeerId, action, args, timeoutMs });
|
|
80
|
+
const req = http.request(
|
|
81
|
+
{
|
|
82
|
+
socketPath,
|
|
83
|
+
path: "/command",
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"Content-Length": Buffer.byteLength(body),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
(res) => {
|
|
91
|
+
let data = "";
|
|
92
|
+
res.on("data", (c) => (data += c));
|
|
93
|
+
res.on("end", () => {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(data);
|
|
96
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
97
|
+
resolve(parsed);
|
|
98
|
+
} else {
|
|
99
|
+
reject(new Error(parsed?.error || `IPC error ${res.statusCode}`));
|
|
100
|
+
}
|
|
101
|
+
} catch (_) {
|
|
102
|
+
reject(new Error(`Bad IPC response: ${data}`));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
req.setTimeout(timeoutMs + 2000, () => {
|
|
108
|
+
req.destroy();
|
|
109
|
+
reject(new Error("IPC request timed out"));
|
|
110
|
+
});
|
|
111
|
+
req.on("error", (err) =>
|
|
112
|
+
reject(new Error(`IPC connection failed: ${err.message}`)),
|
|
113
|
+
);
|
|
114
|
+
req.write(body);
|
|
115
|
+
req.end();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getCliPeerIdFilePath(cacheDir) {
|
|
120
|
+
const dir = cacheDir || path.join(process.cwd(), ".cache");
|
|
121
|
+
return path.join(dir, "cli-peer-id");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function readCliPeerId(cacheDir) {
|
|
125
|
+
if (process.env.SHADOWCLAW_CLI_PEER_ID) {
|
|
126
|
+
return process.env.SHADOWCLAW_CLI_PEER_ID.trim();
|
|
127
|
+
}
|
|
128
|
+
const filePath = getCliPeerIdFilePath(cacheDir);
|
|
129
|
+
try {
|
|
130
|
+
if (fs.existsSync(filePath)) {
|
|
131
|
+
const stored = fs.readFileSync(filePath, "utf8").trim();
|
|
132
|
+
if (stored) {
|
|
133
|
+
return stored;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch (_) {}
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function renewCliPeerId(customId, cacheDir) {
|
|
141
|
+
const filePath = getCliPeerIdFilePath(cacheDir);
|
|
142
|
+
const dir = path.dirname(filePath);
|
|
143
|
+
const newId =
|
|
144
|
+
customId && typeof customId === "string" && customId.trim()
|
|
145
|
+
? customId.trim()
|
|
146
|
+
: `cli-${ulid().toLowerCase()}`;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
if (!fs.existsSync(dir)) {
|
|
150
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
fs.writeFileSync(filePath, newId + "\n", "utf8");
|
|
153
|
+
} catch (_) {}
|
|
154
|
+
|
|
155
|
+
return newId;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function getOrCreateCliPeerId(customId, cacheDir) {
|
|
159
|
+
if (customId && typeof customId === "string" && customId.trim()) {
|
|
160
|
+
return customId.trim();
|
|
161
|
+
}
|
|
162
|
+
if (process.env.SHADOWCLAW_CLI_PEER_ID) {
|
|
163
|
+
return process.env.SHADOWCLAW_CLI_PEER_ID.trim();
|
|
164
|
+
}
|
|
165
|
+
const existing = readCliPeerId(cacheDir);
|
|
166
|
+
if (existing) {
|
|
167
|
+
return existing;
|
|
168
|
+
}
|
|
169
|
+
return renewCliPeerId(undefined, cacheDir);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function ensureWebRtcPolyfill() {
|
|
173
|
+
if (
|
|
174
|
+
typeof globalThis !== "undefined" &&
|
|
175
|
+
typeof globalThis.RTCPeerConnection !== "undefined"
|
|
176
|
+
) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const wrtc = await import("node-datachannel/polyfill");
|
|
182
|
+
if (wrtc && wrtc.RTCPeerConnection) {
|
|
183
|
+
Object.assign(globalThis, {
|
|
184
|
+
RTCPeerConnection: wrtc.RTCPeerConnection,
|
|
185
|
+
RTCSessionDescription: wrtc.RTCSessionDescription,
|
|
186
|
+
RTCIceCandidate: wrtc.RTCIceCandidate,
|
|
187
|
+
});
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.warn(
|
|
192
|
+
"[webrtc-cli] Failed to load node-datachannel polyfill:",
|
|
193
|
+
err.message || String(err),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export class CliWebRtcControlClient {
|
|
200
|
+
constructor(options = {}) {
|
|
201
|
+
this.host = options.host || process.env.SHADOWCLAW_HOST || "127.0.0.1";
|
|
202
|
+
this.port =
|
|
203
|
+
options.port || parseInt(process.env.SHADOWCLAW_PORT || "8888", 10);
|
|
204
|
+
this.path = options.path || "/";
|
|
205
|
+
this.secure = options.secure ?? false;
|
|
206
|
+
this.peer = null;
|
|
207
|
+
this.cacheDir = options.cacheDir;
|
|
208
|
+
this.cliPeerId =
|
|
209
|
+
options.peerId && options.peerId.trim()
|
|
210
|
+
? options.peerId.trim()
|
|
211
|
+
: getOrCreateCliPeerId(undefined, options.cacheDir);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async _getPeer() {
|
|
215
|
+
if (this.peer && !this.peer.destroyed) {
|
|
216
|
+
return this.peer;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await ensureWebRtcPolyfill();
|
|
220
|
+
const mod = await import("peerjs");
|
|
221
|
+
const Peer = mod.default?.Peer || mod.default || mod.Peer;
|
|
222
|
+
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const peer = new Peer(this.cliPeerId, {
|
|
225
|
+
host: this.host,
|
|
226
|
+
port: this.port,
|
|
227
|
+
path: this.path,
|
|
228
|
+
secure: this.secure,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
this.peer = peer;
|
|
232
|
+
|
|
233
|
+
peer.on("open", () => {
|
|
234
|
+
resolve(peer);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
peer.on("error", (err) => {
|
|
238
|
+
reject(new Error(`PeerJS error: ${err.type || err.message || err}`));
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async sendCommand(targetPeerId, action, args = {}, timeoutMs = 30000) {
|
|
244
|
+
// If a `webrtc listen` process is running, route through its local IPC
|
|
245
|
+
// socket so we don't try to register the same peer ID on the PeerJS server.
|
|
246
|
+
const ipc = await resolveListenerIpc(this.cacheDir);
|
|
247
|
+
if (ipc) {
|
|
248
|
+
return sendCommandViaIpc(ipc, targetPeerId, action, args, timeoutMs);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const peer = await this._getPeer();
|
|
252
|
+
|
|
253
|
+
return new Promise((resolve, reject) => {
|
|
254
|
+
let timeoutTimer = null;
|
|
255
|
+
let conn = null;
|
|
256
|
+
|
|
257
|
+
const cleanup = () => {
|
|
258
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
259
|
+
if (conn) {
|
|
260
|
+
try {
|
|
261
|
+
conn.close();
|
|
262
|
+
} catch (_) {}
|
|
263
|
+
}
|
|
264
|
+
if (this.peer) {
|
|
265
|
+
try {
|
|
266
|
+
this.peer.destroy();
|
|
267
|
+
} catch (_) {}
|
|
268
|
+
this.peer = null;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
timeoutTimer = setTimeout(() => {
|
|
273
|
+
cleanup();
|
|
274
|
+
reject(
|
|
275
|
+
new Error(
|
|
276
|
+
`WebRTC command '${action}' to peer '${targetPeerId}' timed out after ${timeoutMs}ms`,
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
}, timeoutMs);
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
conn = peer.connect(targetPeerId, {
|
|
283
|
+
reliable: true,
|
|
284
|
+
serialization: "binary",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
conn.on("open", () => {
|
|
288
|
+
const commandId = ulid();
|
|
289
|
+
const message = {
|
|
290
|
+
id: ulid(),
|
|
291
|
+
type: "command:execute",
|
|
292
|
+
payload: {
|
|
293
|
+
commandId,
|
|
294
|
+
action,
|
|
295
|
+
args,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
conn.send(message);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
conn.on("data", (rawData) => {
|
|
302
|
+
let msg = rawData;
|
|
303
|
+
if (typeof rawData === "string") {
|
|
304
|
+
try {
|
|
305
|
+
msg = JSON.parse(rawData);
|
|
306
|
+
} catch (_) {}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Respond to A2A AgentCard discovery if requested
|
|
310
|
+
if (msg && msg.method === "GetAgentCard" && msg.id) {
|
|
311
|
+
try {
|
|
312
|
+
conn.send({
|
|
313
|
+
jsonrpc: "2.0",
|
|
314
|
+
id: msg.id,
|
|
315
|
+
result: {
|
|
316
|
+
name: "ShadowClaw CLI",
|
|
317
|
+
description: "ShadowClaw Command Line Interface",
|
|
318
|
+
version: "1.0.0",
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
} catch (_) {}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (msg && msg.type === "command:result") {
|
|
326
|
+
cleanup();
|
|
327
|
+
if (msg.payload?.error) {
|
|
328
|
+
reject(new Error(msg.payload.error));
|
|
329
|
+
} else {
|
|
330
|
+
resolve(msg.payload);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
conn.on("error", (err) => {
|
|
336
|
+
cleanup();
|
|
337
|
+
reject(
|
|
338
|
+
new Error(
|
|
339
|
+
`WebRTC connection error to peer '${targetPeerId}': ${err.message || String(err)}`,
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
} catch (err) {
|
|
344
|
+
cleanup();
|
|
345
|
+
reject(err);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async listClients() {
|
|
351
|
+
const ipc = await resolveListenerIpc(this.cacheDir);
|
|
352
|
+
if (!ipc) {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const socketPath = ipc.socketPath || ipc.port;
|
|
357
|
+
return new Promise((resolve) => {
|
|
358
|
+
const req = http.request(
|
|
359
|
+
{ socketPath, path: "/clients", method: "GET" },
|
|
360
|
+
(res) => {
|
|
361
|
+
let data = "";
|
|
362
|
+
res.on("data", (c) => (data += c));
|
|
363
|
+
res.on("end", () => {
|
|
364
|
+
try {
|
|
365
|
+
const parsed = JSON.parse(data);
|
|
366
|
+
resolve(parsed.clients || []);
|
|
367
|
+
} catch (_) {
|
|
368
|
+
resolve([]);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
},
|
|
372
|
+
);
|
|
373
|
+
req.setTimeout(2000, () => {
|
|
374
|
+
req.destroy();
|
|
375
|
+
resolve([]);
|
|
376
|
+
});
|
|
377
|
+
req.on("error", () => resolve([]));
|
|
378
|
+
req.end();
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
close() {
|
|
383
|
+
if (this.peer) {
|
|
384
|
+
try {
|
|
385
|
+
this.peer.destroy();
|
|
386
|
+
} catch (_) {}
|
|
387
|
+
this.peer = null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* CliWebRtcListener — Keeps the CLI registered as a live PeerJS peer on the
|
|
394
|
+
* signaling server so that browser peers can initiate connections to it without
|
|
395
|
+
* needing a control-plane (SSE/WebSocket) connection.
|
|
396
|
+
*
|
|
397
|
+
* Usage:
|
|
398
|
+
* const listener = new CliWebRtcListener({ host, port, path, secure });
|
|
399
|
+
* await listener.start(); // registers on signaling server and blocks
|
|
400
|
+
* // Ctrl-C / SIGINT cleanly calls listener.close()
|
|
401
|
+
*/
|
|
402
|
+
export class CliWebRtcListener {
|
|
403
|
+
static _RECONNECT_BASE_MS = 1000;
|
|
404
|
+
static _RECONNECT_MAX_MS = 30000;
|
|
405
|
+
|
|
406
|
+
constructor(options = {}) {
|
|
407
|
+
this.host = options.host || process.env.SHADOWCLAW_HOST || "127.0.0.1";
|
|
408
|
+
this.port =
|
|
409
|
+
options.port || parseInt(process.env.SHADOWCLAW_PORT || "8888", 10);
|
|
410
|
+
this.path = options.path || "/";
|
|
411
|
+
this.secure = options.secure ?? false;
|
|
412
|
+
this.cacheDir = options.cacheDir;
|
|
413
|
+
this.verbose = options.verbose ?? false;
|
|
414
|
+
this.handlers = options.handlers || {};
|
|
415
|
+
this.trustedPeerIds = new Set(
|
|
416
|
+
(options.trustedPeerIds || []).map((id) => id.trim()).filter(Boolean),
|
|
417
|
+
);
|
|
418
|
+
this.cliPeerId = options.renewPeerId
|
|
419
|
+
? renewCliPeerId(options.peerId, options.cacheDir)
|
|
420
|
+
: getOrCreateCliPeerId(options.peerId, options.cacheDir);
|
|
421
|
+
|
|
422
|
+
this._peer = null;
|
|
423
|
+
this._connections = new Map();
|
|
424
|
+
this._peerCards = new Map();
|
|
425
|
+
this._running = false;
|
|
426
|
+
this._reconnectAttempts = 0;
|
|
427
|
+
this._reconnectTimer = null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
_log(msg) {
|
|
431
|
+
if (this.verbose) {
|
|
432
|
+
console.log(`[webrtc-listen] ${msg}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
_isTrusted(remotePeerId) {
|
|
437
|
+
if (this.trustedPeerIds.size === 0) return true;
|
|
438
|
+
if (this.trustedPeerIds.has(remotePeerId)) return true;
|
|
439
|
+
for (const t of this.trustedPeerIds) {
|
|
440
|
+
if (t.endsWith("*") && remotePeerId.startsWith(t.slice(0, -1)))
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Schedule a reconnection attempt with exponential backoff.
|
|
448
|
+
* Delay doubles each attempt: 1s, 2s, 4s, 8s, ... up to 30s max.
|
|
449
|
+
*/
|
|
450
|
+
_scheduleReconnect() {
|
|
451
|
+
if (!this._running || this._reconnectTimer !== null) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const delay = Math.min(
|
|
456
|
+
CliWebRtcListener._RECONNECT_BASE_MS *
|
|
457
|
+
Math.pow(2, this._reconnectAttempts),
|
|
458
|
+
CliWebRtcListener._RECONNECT_MAX_MS,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
this._reconnectAttempts++;
|
|
462
|
+
|
|
463
|
+
console.log(
|
|
464
|
+
`[webrtc-listen] Disconnected from signaling server — reconnecting in ${(delay / 1000).toFixed(1)}s (attempt ${this._reconnectAttempts})…`,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
this._reconnectTimer = setTimeout(() => {
|
|
468
|
+
this._reconnectTimer = null;
|
|
469
|
+
if (!this._running || !this._peer || this._peer.destroyed) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
this._peer.reconnect();
|
|
475
|
+
} catch {
|
|
476
|
+
this._scheduleReconnect();
|
|
477
|
+
}
|
|
478
|
+
}, delay);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async _handleCommand(conn, remotePeerId, msg) {
|
|
482
|
+
const payload = msg.payload || {};
|
|
483
|
+
const { commandId, action, args } = payload;
|
|
484
|
+
|
|
485
|
+
const handler = this.handlers[action];
|
|
486
|
+
if (!handler) {
|
|
487
|
+
conn.send({
|
|
488
|
+
id: ulid(),
|
|
489
|
+
type: "command:result",
|
|
490
|
+
payload: {
|
|
491
|
+
commandId,
|
|
492
|
+
success: false,
|
|
493
|
+
error: `Unknown action: ${action}`,
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
const data = await handler(args || {});
|
|
501
|
+
conn.send({
|
|
502
|
+
id: ulid(),
|
|
503
|
+
type: "command:result",
|
|
504
|
+
payload: { commandId, success: true, data },
|
|
505
|
+
});
|
|
506
|
+
} catch (err) {
|
|
507
|
+
conn.send({
|
|
508
|
+
id: ulid(),
|
|
509
|
+
type: "command:result",
|
|
510
|
+
payload: {
|
|
511
|
+
commandId,
|
|
512
|
+
success: false,
|
|
513
|
+
error: err?.message || String(err),
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
_setupConnection(conn, remotePeerId) {
|
|
520
|
+
conn.on("open", () => {
|
|
521
|
+
this._connections.set(remotePeerId, conn);
|
|
522
|
+
console.log(
|
|
523
|
+
`[webrtc-listen] Connection opened from peer: ${remotePeerId}`,
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
// Request remote peer's agent card for client discovery
|
|
527
|
+
try {
|
|
528
|
+
conn.send({
|
|
529
|
+
jsonrpc: "2.0",
|
|
530
|
+
id: ulid(),
|
|
531
|
+
method: "GetAgentCard",
|
|
532
|
+
params: {},
|
|
533
|
+
});
|
|
534
|
+
} catch (_) {}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
conn.on("data", async (rawData) => {
|
|
538
|
+
let msg = rawData;
|
|
539
|
+
if (typeof rawData === "string") {
|
|
540
|
+
try {
|
|
541
|
+
msg = JSON.parse(rawData);
|
|
542
|
+
} catch (_) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (!msg || typeof msg !== "object") return;
|
|
547
|
+
|
|
548
|
+
// Store AgentCard response from browser peer
|
|
549
|
+
if (msg.result && typeof msg.result === "object" && msg.result.name) {
|
|
550
|
+
this._peerCards.set(remotePeerId, msg.result);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Respond to GetAgentCard request from browser peer
|
|
555
|
+
if (msg.method === "GetAgentCard" && msg.id) {
|
|
556
|
+
try {
|
|
557
|
+
conn.send({
|
|
558
|
+
jsonrpc: "2.0",
|
|
559
|
+
id: msg.id,
|
|
560
|
+
result: {
|
|
561
|
+
name: "ShadowClaw CLI",
|
|
562
|
+
description: "ShadowClaw Command Line Interface",
|
|
563
|
+
version: "1.0.0",
|
|
564
|
+
capabilities: ["cli", "webrtc", "control"],
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
} catch (_) {}
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (msg.type === "command:execute") {
|
|
572
|
+
await this._handleCommand(conn, remotePeerId, msg);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
conn.on("close", () => {
|
|
577
|
+
this._connections.delete(remotePeerId);
|
|
578
|
+
this._peerCards.delete(remotePeerId);
|
|
579
|
+
console.log(
|
|
580
|
+
`[webrtc-listen] Connection closed from peer: ${remotePeerId}`,
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
conn.on("error", (err) => {
|
|
585
|
+
this._connections.delete(remotePeerId);
|
|
586
|
+
this._peerCards.delete(remotePeerId);
|
|
587
|
+
console.error(
|
|
588
|
+
`[webrtc-listen] Connection error from ${remotePeerId}: ${err?.message || err}`,
|
|
589
|
+
);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Start listening. Resolves when registered on the signaling server.
|
|
595
|
+
* The process stays alive until close() is called or SIGINT/SIGTERM.
|
|
596
|
+
*/
|
|
597
|
+
async start() {
|
|
598
|
+
await ensureWebRtcPolyfill();
|
|
599
|
+
const mod = await import("peerjs");
|
|
600
|
+
const Peer = mod.default?.Peer || mod.default || mod.Peer;
|
|
601
|
+
|
|
602
|
+
return new Promise((resolve, reject) => {
|
|
603
|
+
let isOpened = false;
|
|
604
|
+
|
|
605
|
+
const peer = new Peer(this.cliPeerId, {
|
|
606
|
+
host: this.host,
|
|
607
|
+
port: this.port,
|
|
608
|
+
path: this.path,
|
|
609
|
+
secure: this.secure,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
this._peer = peer;
|
|
613
|
+
this._running = true;
|
|
614
|
+
|
|
615
|
+
peer.on("open", (id) => {
|
|
616
|
+
isOpened = true;
|
|
617
|
+
this._reconnectAttempts = 0;
|
|
618
|
+
if (this._reconnectTimer !== null) {
|
|
619
|
+
clearTimeout(this._reconnectTimer);
|
|
620
|
+
this._reconnectTimer = null;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
console.log(
|
|
624
|
+
`[webrtc-listen] Registered on signaling server as peer: ${id}`,
|
|
625
|
+
);
|
|
626
|
+
console.log(
|
|
627
|
+
`[webrtc-listen] Waiting for connections from browser peers…`,
|
|
628
|
+
);
|
|
629
|
+
resolve(id);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
peer.on("connection", (conn) => {
|
|
633
|
+
const remotePeerId = conn.peer;
|
|
634
|
+
|
|
635
|
+
if (!this._isTrusted(remotePeerId)) {
|
|
636
|
+
console.warn(
|
|
637
|
+
`[webrtc-listen] Rejecting untrusted peer: ${remotePeerId}`,
|
|
638
|
+
);
|
|
639
|
+
try {
|
|
640
|
+
conn.close();
|
|
641
|
+
} catch (_) {}
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
this._setupConnection(conn, remotePeerId);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
peer.on("error", (err) => {
|
|
649
|
+
if (!this._running) return;
|
|
650
|
+
const errStr = err?.type || err?.message || String(err);
|
|
651
|
+
console.error(`[webrtc-listen] PeerJS error: ${errStr}`);
|
|
652
|
+
if (!isOpened) {
|
|
653
|
+
reject(new Error(`PeerJS error: ${errStr}`));
|
|
654
|
+
} else if (!peer.open) {
|
|
655
|
+
this._scheduleReconnect();
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
peer.on("disconnected", () => {
|
|
660
|
+
if (this._running) {
|
|
661
|
+
this._scheduleReconnect();
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
peer.on("close", () => {
|
|
666
|
+
this._running = false;
|
|
667
|
+
if (this._reconnectTimer !== null) {
|
|
668
|
+
clearTimeout(this._reconnectTimer);
|
|
669
|
+
this._reconnectTimer = null;
|
|
670
|
+
}
|
|
671
|
+
console.log("[webrtc-listen] Peer closed.");
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
close() {
|
|
677
|
+
this._running = false;
|
|
678
|
+
if (this._reconnectTimer !== null) {
|
|
679
|
+
clearTimeout(this._reconnectTimer);
|
|
680
|
+
this._reconnectTimer = null;
|
|
681
|
+
}
|
|
682
|
+
this._reconnectAttempts = 0;
|
|
683
|
+
|
|
684
|
+
for (const conn of this._connections.values()) {
|
|
685
|
+
try {
|
|
686
|
+
conn.close();
|
|
687
|
+
} catch (_) {}
|
|
688
|
+
}
|
|
689
|
+
this._connections.clear();
|
|
690
|
+
this._peerCards.clear();
|
|
691
|
+
|
|
692
|
+
if (this._peer) {
|
|
693
|
+
try {
|
|
694
|
+
this._peer.destroy();
|
|
695
|
+
} catch (_) {}
|
|
696
|
+
this._peer = null;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|