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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!doctype html><html lang="en"><head><title>ShadowClaw</title><meta charset="utf-8"><meta name="revision" content="
|
|
1
|
+
<!doctype html><html lang="en"><head><title>ShadowClaw</title><meta charset="utf-8"><meta name="revision" content="3859d7ea031a1daeefa92fcdec55ca9b3862bd3f" /><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"><meta name="description" content="ShadowClaw - Browser-native AI agent." /><meta name="theme-color" content="#fff" /><meta http-equiv="origin-trial" content="A88uTgESYWcVgREKVReKB6jom41uP8TzW6ei3jNdidf+Xl6ONqKRjKNBphsxhrZmdkakLK3oXDgkGq53rtZYSAMAAAB2eyJvcmlnaW4iOiJodHRwczovL3h0LW1sLmdpdGh1Yi5pbzo0NDMiLCJmZWF0dXJlIjoiV2ViTUNQIiwiZXhwaXJ5IjoxNzk0ODczNjAwLCJpc1N1YmRvbWFpbiI6dHJ1ZSwiaXNUaGlyZFBhcnR5Ijp0cnVlfQ=="><base href="/shadow-claw/"><script id="shadow-claw-site-config" type="application/json">{"$schema":"https:\u002f\u002fjson-schema.org\u002fdraft\u002f2020-12\u002fschema","site":{"title":"ShadowClaw","description":"ShadowClaw - Browser-native AI agent.","themeColor":"#fff"},"branding":{"faviconPath":"assets\u002ficons\u002ffavicon.ico","appleTouchIconPath":"assets\u002ficons\u002f180.png","notFoundPath":"404.html"},"pwa":{"manifestPath":"manifest.json","themeColor":"#000000"},"sitemapPath":"sitemap.xml","assets":["assets"],"sidebar":{"pagesHidden":false,"chatHidden":false,"tasksHidden":false,"filesHidden":false,"defaultPage":"pages"},"settings":{"defaultToolsProfile":"__builtin_default"}}</script>
|
|
2
2
|
<script>var ShadowClawThemeInit=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(){if(globalThis.trustedTypes)return;let e=new Map;globalThis.trustedTypes={createPolicy:(t,n)=>{if(e.has(t))throw TypeError(`Policy with name "${t}" already exists.`);let r={createHTML:n.createHTML?e=>n.createHTML(e):void 0,createScriptURL:n.createScriptURL?e=>n.createScriptURL(e):void 0,createScript:n.createScript?e=>n.createScript(e):void 0};return e.set(t,r),r},getPolicy:t=>e.get(t)??null,isHTML:e=>typeof e==`string`||e instanceof Object&&`toString`in e,isScriptURL:e=>typeof e==`string`||e instanceof Object&&`toString`in e,isScript:e=>typeof e==`string`||e instanceof Object&&`toString`in e}}let n=`__shadowClawDefaultTrustedTypesPolicyState`,r=`default`,i=`shadowclaw-sandbox`;function a(){let e=globalThis;return e[n]||(e[n]={initialized:!1,policy:null}),e[n]}function o(){let e=a();if(e.initialized)return;let t=Reflect.get(globalThis,`trustedTypes`);if(!(!t||typeof t.createPolicy!=`function`)){e.initialized=!0;try{if(typeof t.getPolicy==`function`){let n=t.getPolicy(r);if(n){e.policy=n;return}}let n=!1;if(typeof t.getPolicyNames==`function`){let e=t.getPolicyNames();Array.isArray(e)&&e.includes(r)&&(n=!0)}if(n){if(typeof t.getPolicy==`function`){let n=t.getPolicy(i);if(n){e.policy=n;return}}e.policy=t.createPolicy(i,{createHTML:e=>e,createScriptURL:e=>e});return}e.policy=t.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch{typeof t.getPolicy==`function`&&(e.policy=t.getPolicy(r)??t.getPolicy(i)??null)}}}new class{loading=!1;models=new Map;getModelInfo(e){return this.models.get(e.toLowerCase())??null}registerModelInfo(e,t){this.models.set(e.toLowerCase(),t)}clear(){this.models.clear()}async fetchModelInfo(e,t,n){if(e.modelsUrl){this.loading=!0;try{let r=new Headers;if(r.set(`Content-Type`,`application/json`),e.headers)for(let[t,n]of Object.entries(e.headers))r.set(t,n);if(n)for(let[e,t]of Object.entries(n))r.set(e,t);if(t&&e.apiKeyHeader){let n=e.apiKeyHeaderFormat;r.set(e.apiKeyHeader,n?n.replace(`{key}`,t):t)}let i=await fetch(e.modelsUrl,{headers:r});if(!i.ok)throw Error(`HTTP ${i.status} ${i.statusText}`);let a=await i.json(),o=Array.isArray(a.data)?a.data:Array.isArray(a.models)?a.models:[];for(let e of o){if(!e.id)continue;let t=e.context_length??e.context_window??0,n=e.max_completion_tokens||e.per_request_limits?.completion_tokens||e.top_provider?.max_completion_tokens||null,r=typeof e.supports_tools==`boolean`?e.supports_tools:typeof e.supportsTools==`boolean`?e.supportsTools:void 0,i=this.extractModalities(e,`input`),a=this.extractModalities(e,`output`),o=this.modalitiesInclude(i,`image`,`vision`),s=this.modalitiesInclude(i,`audio`,`voice`,`hearing`),c=this.modalitiesInclude(i,`video`),l=e.id===`openrouter/free`||this.supportedParametersInclude(e,`image`,`vision`,`audio`,`tool`,`structured`),u=this.extractReasoning(e);!t&&Array.isArray(e.providers)&&e.providers.length>0&&(t=e.providers[0]?.context_length||0),this.registerModelInfo(e.id,{contextWindow:t,maxOutput:n,...r!==void 0&&{supportsTools:r},...i.length>0&&{inputModalities:i},...a.length>0&&{outputModalities:a},...o!==void 0&&{supportsImageInput:o},...s!==void 0&&{supportsAudioInput:s},...c!==void 0&&{supportsVideoInput:c},...u&&{reasoning:u},...l&&{routesByRequestFeatures:l}})}o.length>0&&console.log(`[ModelRegistry] Registered ${o.length} models for provider "${e.id}"`)}catch(t){console.error(`[ModelRegistry] Error fetching models for ${e.id}:`,t)}finally{this.loading=!1}}}extractModalities(e,t){let n=t===`input`?e.input_modalities||e.inputModalities:e.output_modalities||e.outputModalities,r=e.architecture||{},i=t===`input`?r.input_modalities||r.inputModalities:r.output_modalities||r.outputModalities,a=typeof r.modality==`string`?r.modality:``,o=this.parseArchitectureModality(a,t),s=[...this.normalizeStringArray(n),...this.normalizeStringArray(i),...o];return Array.from(new Set(s))}extractReasoning(e){let t=e?.reasoning;if(!t||typeof t!=`object`)return;let n=this.normalizeStringArray(t.supported_efforts),r=typeof t.default_effort==`string`?t.default_effort.toLowerCase():void 0,i=typeof t.default_enabled==`boolean`?t.default_enabled:void 0,a=typeof t.supports_max_tokens==`boolean`?t.supports_max_tokens:void 0,o=typeof t.mandatory==`boolean`?t.mandatory:void 0;if(!(n.length===0&&r===void 0&&i===void 0&&a===void 0&&o===void 0))return{...n.length>0&&{supportedEfforts:n},...r!==void 0&&{defaultEffort:r},...i!==void 0&&{defaultEnabled:i},...a!==void 0&&{supportsMaxTokens:a},...o!==void 0&&{mandatory:o}}}modalitiesInclude(e,...t){if(e.length!==0)return t.some(t=>e.some(e=>e.includes(t)))}normalizeStringArray(e){return Array.isArray(e)?e.filter(e=>typeof e==`string`).map(e=>e.toLowerCase()):[]}parseArchitectureModality(e,t){if(!e)return[];let[n=``,r=``]=e.toLowerCase().split(`->`).map(e=>e.trim());return(t===`input`?n:r).split(/[+,/\s]+/).map(e=>e.trim()).filter(Boolean)}supportedParametersInclude(e,...t){let n=this.normalizeStringArray(e.supported_parameters||e.supportedParameters);return t.some(e=>n.some(t=>t.includes(e)))}};function s(e){return e.replace(/^\/+|\/+$/g,``)}function c(e){let t=s(e);return t.endsWith(`/index.html`)?t=t.slice(0,-11):t.endsWith(`index.html`)&&(t=t.slice(0,-10)),s(t)}function l(e){let t=s(e),n=t.split(`/`).filter(Boolean);if(n.length===0)return null;let r=n[0].toLowerCase();if(r===`pages`){if(n.length>=3){let e=n[1];return{page:`pages`,groupId:e===`main`?`br:main`:e.startsWith(`br-`)?`br:${e.slice(3)}`:e,path:n.slice(2).join(`/`)}}return{page:`pages`,groupId:`br:main`,path:n.slice(1).join(`/`)}}if(r===`files`){let e=n[1]||`main`;return{page:`files`,groupId:e===`main`?`br:main`:e.startsWith(`br-`)?`br:${e.slice(3)}`:e,path:n.slice(2).join(`/`)||void 0}}if(r===`chat`){let e=n[1]||`main`;return{page:`chat`,groupId:e===`main`?`br:main`:e.startsWith(`br-`)?`br:${e.slice(3)}`:e}}if(r===`tasks`){let e=n[1]||`main`;return{page:`tasks`,groupId:e===`main`?`br:main`:e.startsWith(`br-`)?`br:${e.slice(3)}`:e}}return r===`main`?{page:`pages`,groupId:`br:main`,path:n.slice(1).join(`/`)}:{page:`pages`,groupId:`br:main`,path:t}}function u(){if(typeof document<`u`){let e=document.getElementById(`shadow-claw-static-routing`);if(e&&e.textContent)try{let t=JSON.parse(e.textContent);if(t&&typeof t==`object`&&t.routes)return t}catch(e){console.warn(`Failed to parse embedded static routing manifest:`,e)}}return null}function d(e,t){let n=t||u();if(!n||!n.routes)return null;let r=c(e);if(!r)return null;for(let[e,t]of Object.entries(n.routes))if(!(!t||!t.prettyPath)&&c(t.prettyPath)===r)return l(e);return null}let f=null,ee=new Set([`chat`,`files`,`tasks`,`pages`,`settings`,`tools`,`channels`]);function te(){f=null}typeof globalThis<`u`&&(globalThis.__applyBasePathCacheReset=te);function p(){let e=typeof window<`u`?window:typeof self<`u`?self:globalThis;if(e&&e.__SHADOWCLAW_DEPLOY_ID__){let t=String(e.__SHADOWCLAW_DEPLOY_ID__).trim();if(t)return t.replace(/[^a-zA-Z0-9_-]/g,`-`)}let t=globalThis.process;if(t&&t.env&&t.env.SHADOWCLAW_DEPLOY_ID){let e=String(t.env.SHADOWCLAW_DEPLOY_ID).trim();if(e)return e.replace(/[^a-zA-Z0-9_-]/g,`-`)}let n=m();return!n||n===`/`?``:n.replace(/^\/+|\/+$/g,``).replace(/[^a-zA-Z0-9_-]/g,`-`)}function m(){if(f!==null)return f;if((typeof window>`u`||globalThis.WorkerGlobalScope!==void 0&&typeof self<`u`&&self instanceof globalThis.WorkerGlobalScope)&&typeof self<`u`&&self.location){let e=(self.location.pathname||`/`).split(`/`).filter(Boolean);if(e.length<=1)return f=`/`,f;let t=e[0].toLowerCase(),n=t===`service-worker.js`||t===`sw.js`||t===`manifest.json`||t===`sitemap.xml`||t===`favicon.ico`||t===`index.html`||t===`service-worker`||t===`assets`||t===`dist`||t===`public`;if(n&&e.length===2)return f=`/`,f;if(!n)return f=`/`+e[0]+`/`,f}let e=typeof window<`u`&&window.location?window.location:typeof self<`u`&&self.location?self.location:null;if(!e)return`/`;if(typeof document<`u`&&typeof document.querySelector==`function`){let t=document.querySelector(`base[href]`);if(t){let n=t.getAttribute(`href`);if(n)try{let t=new URL(n,e.origin).pathname;return t.endsWith(`/`)||(t+=`/`),f=t,f}catch{}}}let t=e.pathname||`/`;if(t===`/`)return f=`/`,f;let n=t.split(`/`).filter(Boolean),r=n.findIndex(e=>ee.has(e.toLowerCase()));return r>=0?r===0?(f=`/`,f):(f=`/`+n.slice(0,r).join(`/`)+`/`,f):d(t)?(f=`/`,f):n.length===1&&!t.includes(`.`)?(f=`/`+n[0]+`/`,f):(f=`/`,f)}function ne(e){let t=e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`);return RegExp(`(^|\\s)@${t}\\b`,`i`)}ne(`ShadowClaw`);function re(){let e=p();return e?`shadowclaw-${e}`:`shadowclaw`}re();let h={github:{providerId:`github`,name:`GitHub`,aliases:[`github`,`api.github.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`,`git_remote`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`token `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://github.com/login/oauth/authorize`,tokenUrl:`https://github.com/login/oauth/access_token`,defaultScopes:[`repo`,`read:user`],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},gitlab:{providerId:`gitlab`,name:`GitLab`,aliases:[`gitlab`,`gitlab.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`,`git_remote`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`PRIVATE-TOKEN`,headerPrefix:``},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},tokenAuthByServiceType:{git_remote:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}}},oauth:{authorizeUrl:`https://gitlab.com/oauth/authorize`,tokenUrl:`https://gitlab.com/oauth/token`,defaultScopes:[`read_api`,`read_user`],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},figma:{providerId:`figma`,name:`Figma`,aliases:[`figma`,`api.figma.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`X-Figma-Token`,headerPrefix:``},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://www.figma.com/oauth`,tokenUrl:`https://api.figma.com/v1/oauth/token`,refreshUrl:`https://api.figma.com/v1/oauth/refresh`,defaultScopes:[],usePkce:!0,clientAuthMethod:`basic_header`,scopeSeparator:`space`}},notion:{providerId:`notion`,name:`Notion`,aliases:[`notion`,`api.notion.com`,`notion.so`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://api.notion.com/v1/oauth/authorize`,tokenUrl:`https://api.notion.com/v1/oauth/token`,defaultScopes:[],usePkce:!0,clientAuthMethod:`basic_header`,scopeSeparator:`space`}},microsoft_graph:{providerId:`microsoft_graph`,name:`Microsoft Graph`,aliases:[`microsoft`,`graph.microsoft.com`,`microsoft_graph`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://login.microsoftonline.com/common/oauth2/v2.0/authorize`,tokenUrl:`https://login.microsoftonline.com/common/oauth2/v2.0/token`,defaultScopes:[`openid`,`profile`,`offline_access`,`User.Read`],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},yahoo_mail:{providerId:`yahoo_mail`,name:`Yahoo Mail`,aliases:[`yahoo`,`yahoo_mail`,`mail.yahoo.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://api.login.yahoo.com/oauth2/request_auth`,tokenUrl:`https://api.login.yahoo.com/oauth2/get_token`,defaultScopes:[`mail-r`,`mail-w`],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},google:{providerId:`google`,name:`Google`,aliases:[`google`,`googleapis.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://accounts.google.com/o/oauth2/v2/auth`,tokenUrl:`https://oauth2.googleapis.com/token`,defaultScopes:[`openid`,`profile`,`email`],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},atlassian:{providerId:`atlassian`,name:`Atlassian`,aliases:[`atlassian`,`api.atlassian.com`],modes:[`oauth`,`token`,`basic`],defaultMode:`basic`,serviceTypes:[`http_api`,`git_remote`],authTypes:[`token`,`oauth`,`basic_userpass`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `},basic:{headerName:`Authorization`,headerPrefix:`Basic `}},oauth:{authorizeUrl:`https://auth.atlassian.com/authorize`,tokenUrl:`https://auth.atlassian.com/oauth/token`,defaultScopes:[],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},slack:{providerId:`slack`,name:`Slack`,aliases:[`slack`,`slack.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://slack.com/oauth/v2/authorize`,tokenUrl:`https://slack.com/api/oauth.v2.access`,defaultScopes:[],usePkce:!1,clientAuthMethod:`request_body`,scopeSeparator:`comma`}},linear:{providerId:`linear`,name:`Linear`,aliases:[`linear`,`api.linear.app`,`linear.app`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://linear.app/oauth/authorize`,tokenUrl:`https://api.linear.app/oauth/token`,defaultScopes:[],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},azure_devops:{providerId:`azure_devops`,name:`Azure DevOps`,aliases:[`azure_devops`,`dev.azure.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`,`git_remote`],authTypes:[`token`,`oauth`,`basic_userpass`,`ssh_key`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},tokenAuthByServiceType:{git_remote:{token:{headerName:`Authorization`,headerPrefix:`Basic `},oauth:{headerName:`Authorization`,headerPrefix:`Basic `}}},oauth:{authorizeUrl:`https://app.vssps.visualstudio.com/oauth2/authorize`,tokenUrl:`https://app.vssps.visualstudio.com/oauth2/token`,defaultScopes:[`vso.code`,`vso.profile`,`offline_access`],usePkce:!1,clientAuthMethod:`request_body`,scopeSeparator:`space`}},custom_mcp:{providerId:`custom_mcp`,name:`Custom MCP`,aliases:[`custom_mcp`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`mcp_remote`,`webmcp_local`],authTypes:[`none`,`token`,`oauth`,`custom_header`,`ssh_key`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://example.com/oauth/authorize`,tokenUrl:`https://example.com/oauth/token`,defaultScopes:[],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}}};Object.fromEntries(Object.values(h).filter(e=>!!e.oauth).map(e=>{let t=e.oauth;return[e.providerId,{id:e.providerId,name:e.name,authorizeUrl:t.authorizeUrl,tokenUrl:t.tokenUrl,redirectUri:t.redirectUri,defaultScopes:t.defaultScopes,usePkce:t.usePkce,clientAuthMethod:t.clientAuthMethod,scopeSeparator:t.scopeSeparator}]})),Object.fromEntries(Object.values(h).map(e=>[e.providerId,{providerId:e.providerId,modes:e.modes,defaultMode:e.defaultMode}])),new Promise(e=>{});
|
|
3
3
|
/*! @license DOMPurify 3.4.13 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.13/LICENSE */
|
|
4
4
|
function g(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function ie(e){if(Array.isArray(e))return e}function _(e,t){var n=e==null?null:typeof Symbol<`u`&&e[Symbol.iterator]||e[`@@iterator`];if(n!=null){var r,i,a,o,s=[],c=!0,l=!1;try{if(a=(n=n.call(e)).next,t!==0)for(;!(c=(r=a.call(n)).done)&&(s.push(r.value),s.length!==t);c=!0);}catch(e){l=!0,i=e}finally{try{if(!c&&n.return!=null&&(o=n.return(),Object(o)!==o))return}finally{if(l)throw i}}return s}}function v(){throw TypeError(`Invalid attempt to destructure non-iterable instance.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!doctype html><html lang="en"><head><title>ShadowClaw</title><meta charset="utf-8"><meta name="revision" content="
|
|
1
|
+
<!doctype html><html lang="en"><head><title>ShadowClaw</title><meta charset="utf-8"><meta name="revision" content="3859d7ea031a1daeefa92fcdec55ca9b3862bd3f" /><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"><meta name="description" content="ShadowClaw - Browser-native AI agent." /><meta name="theme-color" content="#fff" /><meta http-equiv="origin-trial" content="A88uTgESYWcVgREKVReKB6jom41uP8TzW6ei3jNdidf+Xl6ONqKRjKNBphsxhrZmdkakLK3oXDgkGq53rtZYSAMAAAB2eyJvcmlnaW4iOiJodHRwczovL3h0LW1sLmdpdGh1Yi5pbzo0NDMiLCJmZWF0dXJlIjoiV2ViTUNQIiwiZXhwaXJ5IjoxNzk0ODczNjAwLCJpc1N1YmRvbWFpbiI6dHJ1ZSwiaXNUaGlyZFBhcnR5Ijp0cnVlfQ=="><base href="/shadow-claw/"><script id="shadow-claw-site-config" type="application/json">{"$schema":"https:\u002f\u002fjson-schema.org\u002fdraft\u002f2020-12\u002fschema","site":{"title":"ShadowClaw","description":"ShadowClaw - Browser-native AI agent.","themeColor":"#fff"},"branding":{"faviconPath":"assets\u002ficons\u002ffavicon.ico","appleTouchIconPath":"assets\u002ficons\u002f180.png","notFoundPath":"404.html"},"pwa":{"manifestPath":"manifest.json","themeColor":"#000000"},"sitemapPath":"sitemap.xml","assets":["assets"],"sidebar":{"pagesHidden":false,"chatHidden":false,"tasksHidden":false,"filesHidden":false,"defaultPage":"pages"},"settings":{"defaultToolsProfile":"__builtin_default"}}</script>
|
|
2
2
|
<script>var ShadowClawThemeInit=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(){if(globalThis.trustedTypes)return;let e=new Map;globalThis.trustedTypes={createPolicy:(t,n)=>{if(e.has(t))throw TypeError(`Policy with name "${t}" already exists.`);let r={createHTML:n.createHTML?e=>n.createHTML(e):void 0,createScriptURL:n.createScriptURL?e=>n.createScriptURL(e):void 0,createScript:n.createScript?e=>n.createScript(e):void 0};return e.set(t,r),r},getPolicy:t=>e.get(t)??null,isHTML:e=>typeof e==`string`||e instanceof Object&&`toString`in e,isScriptURL:e=>typeof e==`string`||e instanceof Object&&`toString`in e,isScript:e=>typeof e==`string`||e instanceof Object&&`toString`in e}}let n=`__shadowClawDefaultTrustedTypesPolicyState`,r=`default`,i=`shadowclaw-sandbox`;function a(){let e=globalThis;return e[n]||(e[n]={initialized:!1,policy:null}),e[n]}function o(){let e=a();if(e.initialized)return;let t=Reflect.get(globalThis,`trustedTypes`);if(!(!t||typeof t.createPolicy!=`function`)){e.initialized=!0;try{if(typeof t.getPolicy==`function`){let n=t.getPolicy(r);if(n){e.policy=n;return}}let n=!1;if(typeof t.getPolicyNames==`function`){let e=t.getPolicyNames();Array.isArray(e)&&e.includes(r)&&(n=!0)}if(n){if(typeof t.getPolicy==`function`){let n=t.getPolicy(i);if(n){e.policy=n;return}}e.policy=t.createPolicy(i,{createHTML:e=>e,createScriptURL:e=>e});return}e.policy=t.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch{typeof t.getPolicy==`function`&&(e.policy=t.getPolicy(r)??t.getPolicy(i)??null)}}}new class{loading=!1;models=new Map;getModelInfo(e){return this.models.get(e.toLowerCase())??null}registerModelInfo(e,t){this.models.set(e.toLowerCase(),t)}clear(){this.models.clear()}async fetchModelInfo(e,t,n){if(e.modelsUrl){this.loading=!0;try{let r=new Headers;if(r.set(`Content-Type`,`application/json`),e.headers)for(let[t,n]of Object.entries(e.headers))r.set(t,n);if(n)for(let[e,t]of Object.entries(n))r.set(e,t);if(t&&e.apiKeyHeader){let n=e.apiKeyHeaderFormat;r.set(e.apiKeyHeader,n?n.replace(`{key}`,t):t)}let i=await fetch(e.modelsUrl,{headers:r});if(!i.ok)throw Error(`HTTP ${i.status} ${i.statusText}`);let a=await i.json(),o=Array.isArray(a.data)?a.data:Array.isArray(a.models)?a.models:[];for(let e of o){if(!e.id)continue;let t=e.context_length??e.context_window??0,n=e.max_completion_tokens||e.per_request_limits?.completion_tokens||e.top_provider?.max_completion_tokens||null,r=typeof e.supports_tools==`boolean`?e.supports_tools:typeof e.supportsTools==`boolean`?e.supportsTools:void 0,i=this.extractModalities(e,`input`),a=this.extractModalities(e,`output`),o=this.modalitiesInclude(i,`image`,`vision`),s=this.modalitiesInclude(i,`audio`,`voice`,`hearing`),c=this.modalitiesInclude(i,`video`),l=e.id===`openrouter/free`||this.supportedParametersInclude(e,`image`,`vision`,`audio`,`tool`,`structured`),u=this.extractReasoning(e);!t&&Array.isArray(e.providers)&&e.providers.length>0&&(t=e.providers[0]?.context_length||0),this.registerModelInfo(e.id,{contextWindow:t,maxOutput:n,...r!==void 0&&{supportsTools:r},...i.length>0&&{inputModalities:i},...a.length>0&&{outputModalities:a},...o!==void 0&&{supportsImageInput:o},...s!==void 0&&{supportsAudioInput:s},...c!==void 0&&{supportsVideoInput:c},...u&&{reasoning:u},...l&&{routesByRequestFeatures:l}})}o.length>0&&console.log(`[ModelRegistry] Registered ${o.length} models for provider "${e.id}"`)}catch(t){console.error(`[ModelRegistry] Error fetching models for ${e.id}:`,t)}finally{this.loading=!1}}}extractModalities(e,t){let n=t===`input`?e.input_modalities||e.inputModalities:e.output_modalities||e.outputModalities,r=e.architecture||{},i=t===`input`?r.input_modalities||r.inputModalities:r.output_modalities||r.outputModalities,a=typeof r.modality==`string`?r.modality:``,o=this.parseArchitectureModality(a,t),s=[...this.normalizeStringArray(n),...this.normalizeStringArray(i),...o];return Array.from(new Set(s))}extractReasoning(e){let t=e?.reasoning;if(!t||typeof t!=`object`)return;let n=this.normalizeStringArray(t.supported_efforts),r=typeof t.default_effort==`string`?t.default_effort.toLowerCase():void 0,i=typeof t.default_enabled==`boolean`?t.default_enabled:void 0,a=typeof t.supports_max_tokens==`boolean`?t.supports_max_tokens:void 0,o=typeof t.mandatory==`boolean`?t.mandatory:void 0;if(!(n.length===0&&r===void 0&&i===void 0&&a===void 0&&o===void 0))return{...n.length>0&&{supportedEfforts:n},...r!==void 0&&{defaultEffort:r},...i!==void 0&&{defaultEnabled:i},...a!==void 0&&{supportsMaxTokens:a},...o!==void 0&&{mandatory:o}}}modalitiesInclude(e,...t){if(e.length!==0)return t.some(t=>e.some(e=>e.includes(t)))}normalizeStringArray(e){return Array.isArray(e)?e.filter(e=>typeof e==`string`).map(e=>e.toLowerCase()):[]}parseArchitectureModality(e,t){if(!e)return[];let[n=``,r=``]=e.toLowerCase().split(`->`).map(e=>e.trim());return(t===`input`?n:r).split(/[+,/\s]+/).map(e=>e.trim()).filter(Boolean)}supportedParametersInclude(e,...t){let n=this.normalizeStringArray(e.supported_parameters||e.supportedParameters);return t.some(e=>n.some(t=>t.includes(e)))}};function s(e){return e.replace(/^\/+|\/+$/g,``)}function c(e){let t=s(e);return t.endsWith(`/index.html`)?t=t.slice(0,-11):t.endsWith(`index.html`)&&(t=t.slice(0,-10)),s(t)}function l(e){let t=s(e),n=t.split(`/`).filter(Boolean);if(n.length===0)return null;let r=n[0].toLowerCase();if(r===`pages`){if(n.length>=3){let e=n[1];return{page:`pages`,groupId:e===`main`?`br:main`:e.startsWith(`br-`)?`br:${e.slice(3)}`:e,path:n.slice(2).join(`/`)}}return{page:`pages`,groupId:`br:main`,path:n.slice(1).join(`/`)}}if(r===`files`){let e=n[1]||`main`;return{page:`files`,groupId:e===`main`?`br:main`:e.startsWith(`br-`)?`br:${e.slice(3)}`:e,path:n.slice(2).join(`/`)||void 0}}if(r===`chat`){let e=n[1]||`main`;return{page:`chat`,groupId:e===`main`?`br:main`:e.startsWith(`br-`)?`br:${e.slice(3)}`:e}}if(r===`tasks`){let e=n[1]||`main`;return{page:`tasks`,groupId:e===`main`?`br:main`:e.startsWith(`br-`)?`br:${e.slice(3)}`:e}}return r===`main`?{page:`pages`,groupId:`br:main`,path:n.slice(1).join(`/`)}:{page:`pages`,groupId:`br:main`,path:t}}function u(){if(typeof document<`u`){let e=document.getElementById(`shadow-claw-static-routing`);if(e&&e.textContent)try{let t=JSON.parse(e.textContent);if(t&&typeof t==`object`&&t.routes)return t}catch(e){console.warn(`Failed to parse embedded static routing manifest:`,e)}}return null}function d(e,t){let n=t||u();if(!n||!n.routes)return null;let r=c(e);if(!r)return null;for(let[e,t]of Object.entries(n.routes))if(!(!t||!t.prettyPath)&&c(t.prettyPath)===r)return l(e);return null}let f=null,ee=new Set([`chat`,`files`,`tasks`,`pages`,`settings`,`tools`,`channels`]);function te(){f=null}typeof globalThis<`u`&&(globalThis.__applyBasePathCacheReset=te);function p(){let e=typeof window<`u`?window:typeof self<`u`?self:globalThis;if(e&&e.__SHADOWCLAW_DEPLOY_ID__){let t=String(e.__SHADOWCLAW_DEPLOY_ID__).trim();if(t)return t.replace(/[^a-zA-Z0-9_-]/g,`-`)}let t=globalThis.process;if(t&&t.env&&t.env.SHADOWCLAW_DEPLOY_ID){let e=String(t.env.SHADOWCLAW_DEPLOY_ID).trim();if(e)return e.replace(/[^a-zA-Z0-9_-]/g,`-`)}let n=m();return!n||n===`/`?``:n.replace(/^\/+|\/+$/g,``).replace(/[^a-zA-Z0-9_-]/g,`-`)}function m(){if(f!==null)return f;if((typeof window>`u`||globalThis.WorkerGlobalScope!==void 0&&typeof self<`u`&&self instanceof globalThis.WorkerGlobalScope)&&typeof self<`u`&&self.location){let e=(self.location.pathname||`/`).split(`/`).filter(Boolean);if(e.length<=1)return f=`/`,f;let t=e[0].toLowerCase(),n=t===`service-worker.js`||t===`sw.js`||t===`manifest.json`||t===`sitemap.xml`||t===`favicon.ico`||t===`index.html`||t===`service-worker`||t===`assets`||t===`dist`||t===`public`;if(n&&e.length===2)return f=`/`,f;if(!n)return f=`/`+e[0]+`/`,f}let e=typeof window<`u`&&window.location?window.location:typeof self<`u`&&self.location?self.location:null;if(!e)return`/`;if(typeof document<`u`&&typeof document.querySelector==`function`){let t=document.querySelector(`base[href]`);if(t){let n=t.getAttribute(`href`);if(n)try{let t=new URL(n,e.origin).pathname;return t.endsWith(`/`)||(t+=`/`),f=t,f}catch{}}}let t=e.pathname||`/`;if(t===`/`)return f=`/`,f;let n=t.split(`/`).filter(Boolean),r=n.findIndex(e=>ee.has(e.toLowerCase()));return r>=0?r===0?(f=`/`,f):(f=`/`+n.slice(0,r).join(`/`)+`/`,f):d(t)?(f=`/`,f):n.length===1&&!t.includes(`.`)?(f=`/`+n[0]+`/`,f):(f=`/`,f)}function ne(e){let t=e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`);return RegExp(`(^|\\s)@${t}\\b`,`i`)}ne(`ShadowClaw`);function re(){let e=p();return e?`shadowclaw-${e}`:`shadowclaw`}re();let h={github:{providerId:`github`,name:`GitHub`,aliases:[`github`,`api.github.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`,`git_remote`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`token `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://github.com/login/oauth/authorize`,tokenUrl:`https://github.com/login/oauth/access_token`,defaultScopes:[`repo`,`read:user`],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},gitlab:{providerId:`gitlab`,name:`GitLab`,aliases:[`gitlab`,`gitlab.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`,`git_remote`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`PRIVATE-TOKEN`,headerPrefix:``},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},tokenAuthByServiceType:{git_remote:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}}},oauth:{authorizeUrl:`https://gitlab.com/oauth/authorize`,tokenUrl:`https://gitlab.com/oauth/token`,defaultScopes:[`read_api`,`read_user`],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},figma:{providerId:`figma`,name:`Figma`,aliases:[`figma`,`api.figma.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`X-Figma-Token`,headerPrefix:``},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://www.figma.com/oauth`,tokenUrl:`https://api.figma.com/v1/oauth/token`,refreshUrl:`https://api.figma.com/v1/oauth/refresh`,defaultScopes:[],usePkce:!0,clientAuthMethod:`basic_header`,scopeSeparator:`space`}},notion:{providerId:`notion`,name:`Notion`,aliases:[`notion`,`api.notion.com`,`notion.so`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://api.notion.com/v1/oauth/authorize`,tokenUrl:`https://api.notion.com/v1/oauth/token`,defaultScopes:[],usePkce:!0,clientAuthMethod:`basic_header`,scopeSeparator:`space`}},microsoft_graph:{providerId:`microsoft_graph`,name:`Microsoft Graph`,aliases:[`microsoft`,`graph.microsoft.com`,`microsoft_graph`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://login.microsoftonline.com/common/oauth2/v2.0/authorize`,tokenUrl:`https://login.microsoftonline.com/common/oauth2/v2.0/token`,defaultScopes:[`openid`,`profile`,`offline_access`,`User.Read`],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},yahoo_mail:{providerId:`yahoo_mail`,name:`Yahoo Mail`,aliases:[`yahoo`,`yahoo_mail`,`mail.yahoo.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://api.login.yahoo.com/oauth2/request_auth`,tokenUrl:`https://api.login.yahoo.com/oauth2/get_token`,defaultScopes:[`mail-r`,`mail-w`],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},google:{providerId:`google`,name:`Google`,aliases:[`google`,`googleapis.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://accounts.google.com/o/oauth2/v2/auth`,tokenUrl:`https://oauth2.googleapis.com/token`,defaultScopes:[`openid`,`profile`,`email`],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},atlassian:{providerId:`atlassian`,name:`Atlassian`,aliases:[`atlassian`,`api.atlassian.com`],modes:[`oauth`,`token`,`basic`],defaultMode:`basic`,serviceTypes:[`http_api`,`git_remote`],authTypes:[`token`,`oauth`,`basic_userpass`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `},basic:{headerName:`Authorization`,headerPrefix:`Basic `}},oauth:{authorizeUrl:`https://auth.atlassian.com/authorize`,tokenUrl:`https://auth.atlassian.com/oauth/token`,defaultScopes:[],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},slack:{providerId:`slack`,name:`Slack`,aliases:[`slack`,`slack.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://slack.com/oauth/v2/authorize`,tokenUrl:`https://slack.com/api/oauth.v2.access`,defaultScopes:[],usePkce:!1,clientAuthMethod:`request_body`,scopeSeparator:`comma`}},linear:{providerId:`linear`,name:`Linear`,aliases:[`linear`,`api.linear.app`,`linear.app`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`],authTypes:[`token`,`oauth`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://linear.app/oauth/authorize`,tokenUrl:`https://api.linear.app/oauth/token`,defaultScopes:[],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}},azure_devops:{providerId:`azure_devops`,name:`Azure DevOps`,aliases:[`azure_devops`,`dev.azure.com`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`http_api`,`git_remote`],authTypes:[`token`,`oauth`,`basic_userpass`,`ssh_key`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},tokenAuthByServiceType:{git_remote:{token:{headerName:`Authorization`,headerPrefix:`Basic `},oauth:{headerName:`Authorization`,headerPrefix:`Basic `}}},oauth:{authorizeUrl:`https://app.vssps.visualstudio.com/oauth2/authorize`,tokenUrl:`https://app.vssps.visualstudio.com/oauth2/token`,defaultScopes:[`vso.code`,`vso.profile`,`offline_access`],usePkce:!1,clientAuthMethod:`request_body`,scopeSeparator:`space`}},custom_mcp:{providerId:`custom_mcp`,name:`Custom MCP`,aliases:[`custom_mcp`],modes:[`oauth`,`token`],defaultMode:`oauth`,serviceTypes:[`mcp_remote`,`webmcp_local`],authTypes:[`none`,`token`,`oauth`,`custom_header`,`ssh_key`],tokenAuth:{token:{headerName:`Authorization`,headerPrefix:`Bearer `},oauth:{headerName:`Authorization`,headerPrefix:`Bearer `}},oauth:{authorizeUrl:`https://example.com/oauth/authorize`,tokenUrl:`https://example.com/oauth/token`,defaultScopes:[],usePkce:!0,clientAuthMethod:`request_body`,scopeSeparator:`space`}}};Object.fromEntries(Object.values(h).filter(e=>!!e.oauth).map(e=>{let t=e.oauth;return[e.providerId,{id:e.providerId,name:e.name,authorizeUrl:t.authorizeUrl,tokenUrl:t.tokenUrl,redirectUri:t.redirectUri,defaultScopes:t.defaultScopes,usePkce:t.usePkce,clientAuthMethod:t.clientAuthMethod,scopeSeparator:t.scopeSeparator}]})),Object.fromEntries(Object.values(h).map(e=>[e.providerId,{providerId:e.providerId,modes:e.modes,defaultMode:e.defaultMode}])),new Promise(e=>{});
|
|
3
3
|
/*! @license DOMPurify 3.4.13 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.13/LICENSE */
|
|
4
4
|
function g(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function ie(e){if(Array.isArray(e))return e}function _(e,t){var n=e==null?null:typeof Symbol<`u`&&e[Symbol.iterator]||e[`@@iterator`];if(n!=null){var r,i,a,o,s=[],c=!0,l=!1;try{if(a=(n=n.call(e)).next,t!==0)for(;!(c=(r=a.call(n)).done)&&(s.push(r.value),s.length!==t);c=!0);}catch(e){l=!0,i=e}finally{try{if(!c&&n.return!=null&&(o=n.return(),Object(o)!==o))return}finally{if(l)throw i}}return s}}function v(){throw TypeError(`Invalid attempt to destructure non-iterable instance.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# File Backup Subsystem
|
|
2
|
+
|
|
3
|
+
> Automated and remote workspace backups from connected browser/Electron clients to server filesystem storage.
|
|
4
|
+
|
|
5
|
+
**Source:** `src/server/routes/backup.ts` · `src/server/client-registry.ts` · `src/core/backup-controller.ts` · `bin/commands/backup.mjs`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
ShadowClaw enables connected clients (especially mobile Safari and iPad browsers) to back up their OPFS/local workspace files directly to the server filesystem without requiring manual file export or zip downloads.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Architecture & Data Flow
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
sequenceDiagram
|
|
19
|
+
participant CLI as CLI / Controller
|
|
20
|
+
participant Server as Express Server
|
|
21
|
+
participant DB as SQLite (clients.db)
|
|
22
|
+
participant Client as Browser Client
|
|
23
|
+
participant FS as Server Filesystem
|
|
24
|
+
|
|
25
|
+
CLI->>Server: POST /api/control/command {action: "trigger-backup"}
|
|
26
|
+
Server->>Client: command:execute {action: "trigger-backup"}
|
|
27
|
+
Client->>Client: Enumerate OPFS files
|
|
28
|
+
loop For each file
|
|
29
|
+
Client->>Server: POST /api/backup/upload {clientId, backupId, path, content}
|
|
30
|
+
Server->>FS: Write to .cache/backups/{clientId}/{backupId}/...
|
|
31
|
+
end
|
|
32
|
+
Client->>Server: POST /api/backup/complete {clientId, backupId, fileCount, totalBytes}
|
|
33
|
+
Server->>DB: Record backup record in SQLite
|
|
34
|
+
Server->>Client: Complete ack
|
|
35
|
+
Client->>Server: command:result {success: true, ...}
|
|
36
|
+
Server->>CLI: command result response
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## REST Endpoints
|
|
42
|
+
|
|
43
|
+
All endpoints require control token authentication via `x-control-token` header, query parameter `?token=...`, or Bearer auth.
|
|
44
|
+
|
|
45
|
+
| Endpoint | Method | Description |
|
|
46
|
+
| :--------------------- | :----- | :-------------------------------------------------------------- |
|
|
47
|
+
| `/api/backup/upload` | POST | Upload a file chunk or content into a backup snapshot directory |
|
|
48
|
+
| `/api/backup/complete` | POST | Finalize backup, update SQLite metadata, and return summary |
|
|
49
|
+
| `/api/backup/list` | GET | List available backups, optionally filtered by `?clientId=...` |
|
|
50
|
+
| `/api/backup/:id` | DELETE | Delete backup snapshot files from disk and remove SQLite record |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Directory Traversal Guard
|
|
55
|
+
|
|
56
|
+
All incoming upload paths are normalized and validated against path traversal attacks. Any paths attempting to escape the client snapshot directory are rejected with HTTP 400.
|
|
@@ -99,6 +99,130 @@ Scaffolds a new ShadowClaw content template in `[dir]` (or `process.cwd()`) with
|
|
|
99
99
|
- `pages/main/index.html` (welcome home page)
|
|
100
100
|
- `.gitignore` (`dist/`, `.cache/`, `node_modules/`)
|
|
101
101
|
|
|
102
|
+
### `shadow-claw clients [options]`
|
|
103
|
+
|
|
104
|
+
Lists connected / registered browser and Electron clients and their capabilities (e.g. OPFS, WebMCP, push, WebRTC).
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# List clients registered via HTTP / WebSocket control plane
|
|
108
|
+
npx shadow-claw clients
|
|
109
|
+
|
|
110
|
+
# List peers connected via WebRTC DataChannel (requires webrtc listen)
|
|
111
|
+
npx shadow-claw clients --transport webrtc
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `shadow-claw send <message> [options]`
|
|
115
|
+
|
|
116
|
+
Sends a message/prompt directly to a connected client or active orchestrator conversation.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Send to the default/active conversation on a client via HTTP control plane
|
|
120
|
+
npx shadow-claw send "Hello from the CLI" --client <client-id>
|
|
121
|
+
|
|
122
|
+
# Send to a specific conversation group (e.g. main AI assistant or PeerJS channel)
|
|
123
|
+
npx shadow-claw send "Hello from the CLI" --client <client-id> --group "br:main"
|
|
124
|
+
npx shadow-claw send "Hello to peer" --client <browser-peer-id> --group "peer:<cli-peer-id>"
|
|
125
|
+
|
|
126
|
+
# Send over WebRTC DataChannel transport (routes via webrtc listen IPC or direct connection)
|
|
127
|
+
npx shadow-claw send "Hello from CLI" --transport webrtc --client <browser-peer-id>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
| Option | Type | Description | Default |
|
|
131
|
+
| :------------------------ | :----- | :------------------------------------------------------------------------------------------ | :------------ |
|
|
132
|
+
| `--client <id>` | string | Target client ID or PeerJS peer ID (defaults to first available connected client) | `""` |
|
|
133
|
+
| `--group <groupId>` | string | Target conversation group ID (`br:main`, `peer:<peerId>`, etc.). Defaults to active group. | Active group |
|
|
134
|
+
| `--transport <transport>` | string | Transport mechanism: `http` (Control Plane REST/WebSocket) or `webrtc` (WebRTC DataChannel) | `"http"` |
|
|
135
|
+
| `--host <host>` | string | Control plane / signaling server host | `"127.0.0.1"` |
|
|
136
|
+
| `--port <port>` | number | Control plane / signaling server port | `8888` |
|
|
137
|
+
| `--token <token>` | string | Control token (auto-resolved from `.cache/control-token.json` or `clients.db` if omitted) | `""` |
|
|
138
|
+
| `--peer-id <id>` | string | Custom WebRTC CLI peer ID override | `""` |
|
|
139
|
+
| `--cache-dir <dir>` | string | Custom cache directory for token and peer ID storage | `".cache"` |
|
|
140
|
+
|
|
141
|
+
### `shadow-claw backup [trigger|list|delete] [options]`
|
|
142
|
+
|
|
143
|
+
Triggers a remote OPFS workspace backup from a connected client, lists available snapshots, or deletes backups.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Trigger backup via HTTP control plane
|
|
147
|
+
npx shadow-claw backup
|
|
148
|
+
npx shadow-claw backup --client <client-id>
|
|
149
|
+
|
|
150
|
+
# Trigger backup via WebRTC DataChannel
|
|
151
|
+
npx shadow-claw backup --transport webrtc --client <browser-peer-id>
|
|
152
|
+
|
|
153
|
+
# List or delete stored snapshots on server
|
|
154
|
+
npx shadow-claw backup list
|
|
155
|
+
npx shadow-claw backup delete --backup-id <id>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `shadow-claw tasks [options]`
|
|
159
|
+
|
|
160
|
+
Lists scheduled tasks configured on a connected client (with optional group filtering).
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# List all tasks on default connected client
|
|
164
|
+
npx shadow-claw tasks
|
|
165
|
+
|
|
166
|
+
# List all tasks via HTTP control plane
|
|
167
|
+
npx shadow-claw tasks --client <client-id>
|
|
168
|
+
|
|
169
|
+
# Filter tasks by conversation group (e.g. main AI chat or peer channel)
|
|
170
|
+
npx shadow-claw tasks --client <client-id> --group "br:main"
|
|
171
|
+
npx shadow-claw tasks --transport webrtc --client <browser-peer-id> --group "peer:<cli-peer-id>"
|
|
172
|
+
|
|
173
|
+
# List tasks via WebRTC DataChannel
|
|
174
|
+
npx shadow-claw tasks --transport webrtc --client <browser-peer-id>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
| Option | Type | Description | Default |
|
|
178
|
+
| :------------------------ | :----- | :---------------------------------------------------------------------- | :------------ |
|
|
179
|
+
| `--client <id>` | string | Target client ID or PeerJS peer ID (defaults to first connected client) | `""` |
|
|
180
|
+
| `--group <groupId>` | string | Filter tasks by conversation group ID (`br:main`, `peer:...`, etc.) | All groups |
|
|
181
|
+
| `--transport <transport>` | string | Transport mechanism: `http` or `webrtc` | `"http"` |
|
|
182
|
+
| `--host <host>` | string | Control plane / signaling server host | `"127.0.0.1"` |
|
|
183
|
+
| `--port <port>` | number | Control plane / signaling server port | `8888` |
|
|
184
|
+
| `--token <token>` | string | Control token | `""` |
|
|
185
|
+
| `--peer-id <id>` | string | Custom WebRTC CLI peer ID override | `""` |
|
|
186
|
+
| `--cache-dir <dir>` | string | Custom cache directory | `".cache"` |
|
|
187
|
+
|
|
188
|
+
### `shadow-claw webrtc [action] [options]` / `shadow-claw peer-id [action] [options]`
|
|
189
|
+
|
|
190
|
+
Manages WebRTC CLI peer identity (`.cache/cli-peer-id`) and provides the `webrtc listen` daemon for headless DataChannel communication with browser tabs.
|
|
191
|
+
|
|
192
|
+
#### 1. Managing CLI Peer ID
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Get or create peer ID
|
|
196
|
+
npx shadow-claw peer-id
|
|
197
|
+
|
|
198
|
+
# Force renewal / generation of a new peer ID
|
|
199
|
+
npx shadow-claw peer-id --renew
|
|
200
|
+
|
|
201
|
+
# Set custom peer ID
|
|
202
|
+
npx shadow-claw peer-id --set my-custom-peer-id
|
|
203
|
+
|
|
204
|
+
# Print only raw ID string (for shell scripting)
|
|
205
|
+
npx shadow-claw peer-id -q
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### 2. Running WebRTC Listener (`webrtc listen`)
|
|
209
|
+
|
|
210
|
+
Registers the CLI as a live PeerJS peer on the signaling server so browser tabs can initiate direct P2P connections without requiring a control plane connection. It also launches a local Unix socket IPC bridge (`.cache/webrtc-ipc.sock`) to coordinate concurrent `send` commands without peer ID conflicts.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Start WebRTC listener
|
|
214
|
+
npx shadow-claw webrtc listen
|
|
215
|
+
|
|
216
|
+
# Restrict incoming connections to specific browser peer IDs
|
|
217
|
+
npx shadow-claw webrtc listen --trusted-peer <browser-peer-id>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Browser Setup Workflow:**
|
|
221
|
+
|
|
222
|
+
1. Run `npx shadow-claw webrtc listen` to display the CLI peer ID (e.g. `cli-01m1...`).
|
|
223
|
+
2. In the browser, navigate to **Settings → WebRTC/PeerJS → Trusted Peer IDs** and add the CLI's peer ID.
|
|
224
|
+
3. Once connected, dispatch commands with `npx shadow-claw send --transport webrtc --client <browser-peer-id> [--group <groupId>] "message"`.
|
|
225
|
+
|
|
102
226
|
---
|
|
103
227
|
|
|
104
228
|
## NPM Packaging & Prepack Lifecycle
|
|
@@ -0,0 +1,169 @@
|
|
|
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`.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Private Network Access (PNA) & Static Site Integration
|
|
163
|
+
|
|
164
|
+
When ShadowClaw is accessed from a public static host (e.g. GitHub Pages `https://xt-ml.github.io` or Cloudflare Pages `https://*.pages.dev`) and connected to a local Task Server / Control Plane (`http://127.0.0.1:8888` or `http://localhost:8888`):
|
|
165
|
+
|
|
166
|
+
1. **Service Worker Loopback Bypass:** The Service Worker fetch proxy (`src/service-worker/fetch-proxy-rules.ts`) unconditionally bypasses all loopback (`localhost`, `127.0.0.1`, `::1`) and private LAN IP ranges so cross-origin requests are handled natively by the browser rather than failing within the Service Worker context.
|
|
167
|
+
2. **PNA Headers:** The server (`src/server/middleware/pna.ts`) responds with `Access-Control-Allow-Private-Network: true` on both preflight (`OPTIONS`) and cross-origin requests.
|
|
168
|
+
3. **Browser Fetch Options:** Client requests targeting loopback or private network endpoints supply `targetAddressSpace: 'loopback'` (or `'private'`) to satisfy Chromium Private Network Access permissions.
|
|
169
|
+
4. **Origin Trust:** The server automatically trusts GitHub Pages (`.github.io`), Cloudflare Pages (`.pages.dev`), and origins configured via `allowedOrigins` or `corsMode: 'all'`.
|
|
@@ -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-tHWrqi_F.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-tHWrqi_F.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-tHWrqi_F.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};
|