shadow-claw 1.23.4 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -5
- package/bin/cli.mjs +166 -0
- package/bin/cli.test.mjs +60 -0
- package/bin/commands/backup.mjs +70 -0
- package/bin/commands/clients.mjs +41 -0
- package/bin/commands/peer-id.mjs +76 -0
- package/bin/commands/peer-id.test.mjs +104 -0
- package/bin/commands/send.mjs +51 -0
- package/bin/commands/tasks.mjs +59 -0
- package/bin/commands/webrtc-listen.mjs +254 -0
- package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
- package/bin/rolldown.config.test.mjs +401 -8
- package/bin/utils/control-client.mjs +243 -0
- package/bin/utils/control-client.test.mjs +83 -0
- package/bin/utils/ulid.mjs +45 -0
- package/bin/utils/webrtc-control-client.mjs +699 -0
- package/bin/utils/webrtc-control-client.test.mjs +318 -0
- package/dist/electron/main.cjs +1 -1
- package/dist/public/AGENTS.md +5 -3
- package/dist/public/README.md +42 -5
- package/dist/public/agent.worker.js +1 -1
- package/dist/public/backup-controller-C_s9in2t.js +1 -0
- package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
- package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
- package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
- package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
- package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
- package/dist/public/config-CrHchneq.js +1 -0
- package/dist/public/{configurePeerJs-B4Ym9VMj.js → configurePeerJs-CxpZeDJJ.js} +1 -1
- package/dist/public/{connections-BEn6JYdu.js → connections-DvbgdkaB.js} +1 -1
- package/dist/public/{custom-element-security-DdYlUozq.js → custom-element-security-NCqQ5lmC.js} +1 -1
- package/dist/public/docs/README.md +3 -1
- package/dist/public/docs/architecture/overview.md +12 -12
- package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
- package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +2 -2
- package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
- package/dist/public/docs/example/article/index.html +1 -1
- package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
- package/dist/public/docs/guides/server-development-configuration.md +24 -0
- package/dist/public/docs/publishing/index.html +1 -1
- package/dist/public/docs/skill-creator/index.html +1 -1
- package/dist/public/docs/subsystems/backup.md +56 -0
- package/dist/public/docs/subsystems/cli.md +124 -0
- package/dist/public/docs/subsystems/control-plane.md +158 -0
- package/dist/public/docs/subsystems/notifications.md +54 -2
- package/dist/public/docs/subsystems/reactive-ui.md +1 -0
- package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
- package/dist/public/e2e/README.md +5 -1
- package/dist/public/{e2e-bridge-BQ4KzUt5.js → e2e-bridge-IGWsRl4i.js} +1 -1
- package/dist/public/file-viewer-D0eflgt6.js +1 -0
- package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
- package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
- package/dist/public/{git-DEUvQTUA.js → git-7Vq_SJSp.js} +2 -2
- package/dist/public/{iframe-sanitizer-0NS4XLOd.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
- package/dist/public/index.html +1 -1
- package/dist/public/index.js +1 -1
- package/dist/public/{initChatSplitResize-llGkSJXg.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
- package/dist/public/initControlPlane-C9jpnWev.js +1 -0
- package/dist/public/llms.txt +1 -1
- package/dist/public/main/index.html +1 -1
- package/dist/public/main/memory/index.html +1 -1
- package/dist/public/{markdown-CyYR4App.js → markdown-BnpKxSxl.js} +1 -1
- package/dist/public/{mcp-reconnect-CSx4aMwy.js → mcp-reconnect-9emsqGRe.js} +1 -1
- package/dist/public/{memoryStorage-Dwg8NhAZ.js → memoryStorage-C0KvLNUp.js} +1 -1
- package/dist/public/{orchestrator-DRfEudXk.js → orchestrator-3h9xxHUm.js} +676 -676
- package/dist/public/orchestrator-nJj59RqD.js +15 -0
- package/dist/public/{peerjs-CurBuKvk.js → peerjs-BP4laMXj.js} +1 -1
- package/dist/public/{push-client-Dii-vPEh.js → push-client-CrjEXK3U.js} +1 -1
- package/dist/public/service-worker/push-handler.js +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-5IHaDDMJ.js → shadow-claw-CbwETBWf.js} +2 -2
- package/dist/public/{shadow-claw-accounts-CZvz4iQz.js → shadow-claw-accounts-B4sBTQEj.js} +1 -1
- package/dist/public/{shadow-claw-channels-BKXFyRS3.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
- package/dist/public/{shadow-claw-chat-CCLuhWdI.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
- package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
- package/dist/public/{shadow-claw-conversations-DV-87UUh.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
- package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
- package/dist/public/{shadow-claw-file-viewer-Dk2dbAUd.js → shadow-claw-file-viewer-Blf6yGVD.js} +1 -1
- package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
- package/dist/public/{shadow-claw-git-DRA7_qck.js → shadow-claw-git-v8S0JjuR.js} +2 -2
- package/dist/public/{shadow-claw-integrations-1Xu5snuV.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
- package/dist/public/{shadow-claw-llm-6Aci1BDP.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
- package/dist/public/{shadow-claw-mcp-remote-BUjAeVXC.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
- package/dist/public/{shadow-claw-networking-FQe4nCp3.js → shadow-claw-networking-qHbP95z9.js} +1 -1
- package/dist/public/{shadow-claw-notifications-DV2wibL3.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
- package/dist/public/{shadow-claw-page-header-B63S4HTU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
- package/dist/public/{shadow-claw-pages-DE3jWj5K.js → shadow-claw-pages-DFZHhzFZ.js} +1 -1
- package/dist/public/{shadow-claw-settings-S6DAKfYx.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
- package/dist/public/{shadow-claw-storage-DaacA3_K.js → shadow-claw-storage-BFynbP0j.js} +2 -2
- package/dist/public/{shadow-claw-task-server-IjKX042p.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
- package/dist/public/{shadow-claw-tasks-Cy0JYmGU.js → shadow-claw-tasks-Caxc9atL.js} +1 -1
- package/dist/public/{shadow-claw-terminal-znAt7HyP.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
- package/dist/public/{shadow-claw-tools-Cutv9Si5.js → shadow-claw-tools-BfJx3A9W.js} +1 -1
- package/dist/public/{shadow-claw-webvm-BH8mPDEu.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
- package/dist/public/{syncWebMcpRegistration-BqJxaMGR.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
- package/dist/server.js +286 -189
- package/package.json +2 -1
- package/src/__mocks__/jszip.cjs +15 -5
- package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
- package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
- package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
- package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
- package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
- package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
- package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +76 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
- package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
- package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
- package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
- package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
- package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
- package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
- package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
- package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +273 -0
- package/src/components/shadow-claw-files/shadow-claw-files.test.ts +408 -1
- package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
- package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
- package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
- package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
- package/src/config/config.ts +10 -0
- package/src/core/backup-controller.test.ts +101 -0
- package/src/core/backup-controller.ts +137 -0
- package/src/core/control-plane-client.test.ts +408 -0
- package/src/core/control-plane-client.ts +425 -0
- package/src/core/orchestrator/orchestrator.ts +4 -0
- package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
- package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
- package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
- package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
- package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
- package/src/core/utils/initControlPlane.test.ts +276 -0
- package/src/core/utils/initControlPlane.ts +437 -0
- package/src/core/utils/initializeApp.ts +18 -0
- package/src/db/getSession.test.ts +13 -1
- package/src/server/app.test.ts +12 -0
- package/src/server/app.ts +18 -8
- package/src/server/client-registry.test.ts +250 -0
- package/src/server/client-registry.ts +477 -0
- package/src/server/config.ts +15 -0
- package/src/server/control-plane-types.ts +120 -0
- package/src/server/control-plane.test.ts +472 -0
- package/src/server/control-plane.ts +581 -0
- package/src/server/routes/backup.test.ts +263 -0
- package/src/server/routes/backup.ts +262 -0
- package/src/server/routes/integrations-email.test.ts +532 -0
- package/src/server/routes/llamafile.test.ts +172 -48
- package/src/server/routes/mesh-llm.test.ts +192 -0
- package/src/server/routes/ollama.test.ts +49 -0
- package/src/server/server-peer.test.ts +86 -0
- package/src/server/server-peer.ts +330 -0
- package/src/server/server.ts +35 -0
- package/src/server/services/llamafile-manager.test.ts +230 -6
- package/src/server/services/transformers-runtime.test.ts +186 -18
- package/src/server/webrtc.test.ts +21 -0
- package/src/server/webrtc.ts +72 -0
- package/src/service-worker/fetch-proxy.test.ts +64 -1
- package/src/service-worker/push-handler.ts +39 -0
- package/src/shell/vm.test.ts +744 -12
- package/src/storage/ensureMainGroupIndex.test.ts +153 -0
- package/src/storage/getGroupFile.test.ts +66 -0
- package/src/storage/memoryStorage.test.ts +82 -2
- package/src/storage/writeGroupFileBytes.test.ts +143 -0
- package/src/stores/chat-ui.test.ts +20 -0
- package/src/stores/orchestrator.test.ts +134 -0
- package/src/stores/toast.test.ts +50 -0
- package/src/stores/tools.test.ts +147 -10
- package/src/subsystems/channels/imessage.test.ts +59 -0
- package/src/subsystems/channels/peerjs.test.ts +500 -62
- package/src/subsystems/channels/peerjs.ts +71 -4
- package/src/subsystems/channels/telegram.test.ts +177 -0
- package/src/subsystems/email/catalog.test.ts +1 -0
- package/src/subsystems/email/connections.test.ts +39 -1
- package/src/subsystems/git/credentials.test.ts +160 -825
- package/src/subsystems/git/git.test.ts +463 -0
- package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
- package/src/subsystems/notifications/push-routes.test.ts +33 -0
- package/src/subsystems/notifications/push-routes.ts +23 -0
- package/src/subsystems/notifications/push-store.ts +1 -1
- package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
- package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
- package/src/subsystems/providers/model-registry.test.ts +115 -0
- package/src/subsystems/providers/providers.test.ts +94 -1
- package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
- package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
- package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
- package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
- package/src/subsystems/skills/activateSkill.test.ts +101 -0
- package/src/subsystems/skills/discoverSkills.test.ts +251 -0
- package/src/subsystems/tools/cron.test.ts +64 -0
- package/src/subsystems/tools/declarative.test.ts +255 -89
- package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
- package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
- package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
- package/src/testing/jest-setup.ts +328 -40
- package/src/testing/mock-v86.mjs +49 -0
- package/src/ui/a2ui/types.ts +4 -3
- package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
- package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
- package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
- package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
- package/src/worker/tools/email/email.test.ts +193 -8
- package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
- package/src/worker/tools/git/git.test.ts +214 -3
- package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
- package/src/worker/tools/rooms/leave-room.test.ts +40 -0
- package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
- package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
- package/src/worker/tools/ui/ask-user.test.ts +50 -0
- package/src/worker/tools/ui/clear-chat.test.ts +19 -0
- package/src/worker/tools/ui/get-current-time.test.ts +19 -0
- package/src/worker/tools/ui/javascript.test.ts +116 -0
- package/src/worker/tools/ui/list-components.test.ts +10 -0
- package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
- package/src/worker/tools/ui/manage-tools.test.ts +34 -0
- package/src/worker/tools/ui/render-component.test.ts +346 -0
- package/src/worker/tools/ui/send-notification.test.ts +40 -0
- package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
- package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
- package/src/worker/tools/workspace/attach-file.test.ts +84 -0
- package/src/worker/tools/workspace/copy-file.test.ts +69 -0
- package/src/worker/tools/workspace/create-directory.test.ts +40 -0
- package/src/worker/tools/workspace/delete-file.test.ts +73 -0
- package/src/worker/tools/workspace/diff-files.test.ts +94 -0
- package/src/worker/tools/workspace/list-files.test.ts +36 -0
- package/src/worker/tools/workspace/move-file.test.ts +66 -0
- package/src/worker/tools/workspace/open-file.test.ts +57 -0
- package/src/worker/tools/workspace/patch-file.test.ts +81 -0
- package/src/worker/tools/workspace/read-file.test.ts +116 -0
- package/src/worker/tools/workspace/send-file.test.ts +82 -0
- package/src/worker/tools/workspace/update-memory.test.ts +29 -0
- package/src/worker/tools/workspace/write-file.test.ts +28 -0
- package/src/worker/utils/rate-limit.test.ts +199 -0
- package/src/worker/utils/tool-state.test.ts +29 -0
- package/src/worker/utils/transformers-js-utils.test.ts +59 -0
- package/dist/public/config-BYAuxx3j.js +0 -1
- package/dist/public/downloadGroupFile-Drdvg1Ev.js +0 -1
- package/dist/public/file-viewer-CcZYw-PS.js +0 -1
- package/dist/public/orchestrator-o9Fj67BN.js +0 -15
- package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
- package/dist/public/shadow-claw-files-BpfpJIuP.js +0 -840
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
jest,
|
|
7
|
+
describe,
|
|
8
|
+
it,
|
|
9
|
+
expect,
|
|
10
|
+
beforeEach,
|
|
11
|
+
afterEach,
|
|
12
|
+
} from "@jest/globals";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
ensureWebRtcPolyfill,
|
|
16
|
+
CliWebRtcControlClient,
|
|
17
|
+
getOrCreateCliPeerId,
|
|
18
|
+
renewCliPeerId,
|
|
19
|
+
readCliPeerId,
|
|
20
|
+
getIpcFilePath,
|
|
21
|
+
writeIpcFile,
|
|
22
|
+
clearIpcFile,
|
|
23
|
+
} from "./webrtc-control-client.mjs";
|
|
24
|
+
|
|
25
|
+
describe("webrtc-control-client", () => {
|
|
26
|
+
let tempDir;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
tempDir = path.join(
|
|
30
|
+
os.tmpdir(),
|
|
31
|
+
"test-webrtc-peer-" +
|
|
32
|
+
Date.now() +
|
|
33
|
+
"-" +
|
|
34
|
+
Math.random().toString(36).slice(2),
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
try {
|
|
40
|
+
if (fs.existsSync(tempDir)) {
|
|
41
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
} catch (_) {}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("initializes WebRTC polyfill in Node environment", async () => {
|
|
47
|
+
const ok = await ensureWebRtcPolyfill();
|
|
48
|
+
expect(ok).toBe(true);
|
|
49
|
+
expect(globalThis.RTCPeerConnection).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("constructs CliWebRtcControlClient with default options", () => {
|
|
53
|
+
const client = new CliWebRtcControlClient({
|
|
54
|
+
host: "127.0.0.1",
|
|
55
|
+
port: 8888,
|
|
56
|
+
peerId: "cli-test-peer",
|
|
57
|
+
cacheDir: tempDir,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(client.cliPeerId).toBe("cli-test-peer");
|
|
61
|
+
expect(client.host).toBe("127.0.0.1");
|
|
62
|
+
expect(client.port).toBe(8888);
|
|
63
|
+
expect(client.path).toBe("/");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("creates, reads, and renews CLI peer IDs via helper functions", () => {
|
|
67
|
+
expect(readCliPeerId(tempDir)).toBe("");
|
|
68
|
+
|
|
69
|
+
const id1 = getOrCreateCliPeerId(undefined, tempDir);
|
|
70
|
+
expect(id1).toMatch(/^cli-[0-9a-z]+$/);
|
|
71
|
+
expect(readCliPeerId(tempDir)).toBe(id1);
|
|
72
|
+
|
|
73
|
+
// Subsequent call returns same
|
|
74
|
+
const id2 = getOrCreateCliPeerId(undefined, tempDir);
|
|
75
|
+
expect(id2).toBe(id1);
|
|
76
|
+
|
|
77
|
+
// Renewal generates new ID
|
|
78
|
+
const id3 = renewCliPeerId(undefined, tempDir);
|
|
79
|
+
expect(id3).not.toBe(id1);
|
|
80
|
+
expect(readCliPeerId(tempDir)).toBe(id3);
|
|
81
|
+
|
|
82
|
+
// Custom renewal
|
|
83
|
+
const custom = renewCliPeerId("my-special-peer", tempDir);
|
|
84
|
+
expect(custom).toBe("my-special-peer");
|
|
85
|
+
expect(readCliPeerId(tempDir)).toBe("my-special-peer");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("CliWebRtcControlClient uses the stored persistent peer ID", () => {
|
|
89
|
+
const client = new CliWebRtcControlClient({ cacheDir: tempDir });
|
|
90
|
+
// Should auto-create and persist a cli-* ID
|
|
91
|
+
expect(client.cliPeerId).toMatch(/^cli-[0-9a-z]+$/);
|
|
92
|
+
// A second instance in the same dir reuses the same ID
|
|
93
|
+
const client2 = new CliWebRtcControlClient({ cacheDir: tempDir });
|
|
94
|
+
expect(client2.cliPeerId).toBe(client.cliPeerId);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("CliWebRtcControlClient respects an explicit --peer-id override", () => {
|
|
98
|
+
const client = new CliWebRtcControlClient({
|
|
99
|
+
cacheDir: tempDir,
|
|
100
|
+
peerId: "cli-my-custom-id",
|
|
101
|
+
});
|
|
102
|
+
expect(client.cliPeerId).toBe("cli-my-custom-id");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("CliWebRtcControlClient.listClients returns empty array when no listener is active", async () => {
|
|
106
|
+
const client = new CliWebRtcControlClient({ cacheDir: tempDir });
|
|
107
|
+
const clients = await client.listClients();
|
|
108
|
+
expect(clients).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("CliWebRtcControlClient.listClients retrieves connected clients from listener IPC socket", async () => {
|
|
112
|
+
const http = await import("node:http");
|
|
113
|
+
const { getIpcSocketPath } = await import("./webrtc-control-client.mjs");
|
|
114
|
+
const socketPath = getIpcSocketPath(tempDir);
|
|
115
|
+
|
|
116
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
117
|
+
const mockClients = [
|
|
118
|
+
{
|
|
119
|
+
clientId: "browser-peer-1",
|
|
120
|
+
peerId: "browser-peer-1",
|
|
121
|
+
deviceLabel: "Chrome Browser",
|
|
122
|
+
capabilities: ["webrtc", "peerjs"],
|
|
123
|
+
version: "1.0.0",
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const server = http.createServer((req, res) => {
|
|
128
|
+
if (req.method === "GET" && req.url === "/ping") {
|
|
129
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
130
|
+
res.end(JSON.stringify({ ok: true }));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (req.method === "GET" && req.url === "/clients") {
|
|
134
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
135
|
+
res.end(JSON.stringify({ ok: true, clients: mockClients }));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
res.writeHead(404);
|
|
139
|
+
res.end();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await new Promise((resolve) => server.listen(socketPath, resolve));
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const client = new CliWebRtcControlClient({ cacheDir: tempDir });
|
|
146
|
+
const clients = await client.listClients();
|
|
147
|
+
expect(clients).toEqual(mockClients);
|
|
148
|
+
} finally {
|
|
149
|
+
await new Promise((resolve) => server.close(resolve));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("IPC socket helpers", () => {
|
|
155
|
+
let tempDir;
|
|
156
|
+
|
|
157
|
+
beforeEach(() => {
|
|
158
|
+
tempDir = path.join(
|
|
159
|
+
os.tmpdir(),
|
|
160
|
+
"test-webrtc-ipc-" +
|
|
161
|
+
Date.now() +
|
|
162
|
+
"-" +
|
|
163
|
+
Math.random().toString(36).slice(2),
|
|
164
|
+
);
|
|
165
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
afterEach(() => {
|
|
169
|
+
try {
|
|
170
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
171
|
+
} catch (_) {}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("getIpcFilePath returns the .sock path inside cacheDir", () => {
|
|
175
|
+
const p = getIpcFilePath(tempDir);
|
|
176
|
+
expect(p).toBe(path.join(tempDir, "webrtc-ipc.sock"));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("writeIpcFile is a no-op (socket file is created by the server)", () => {
|
|
180
|
+
expect(() => writeIpcFile("/some/path.sock", tempDir)).not.toThrow();
|
|
181
|
+
// No file should be created
|
|
182
|
+
expect(fs.existsSync(getIpcFilePath(tempDir))).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("clearIpcFile removes the socket file if it exists", () => {
|
|
186
|
+
const sockPath = getIpcFilePath(tempDir);
|
|
187
|
+
// Create a dummy socket file to simulate a running listener
|
|
188
|
+
fs.writeFileSync(sockPath, "");
|
|
189
|
+
expect(fs.existsSync(sockPath)).toBe(true);
|
|
190
|
+
clearIpcFile(tempDir);
|
|
191
|
+
expect(fs.existsSync(sockPath)).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("clearIpcFile does not throw if socket file does not exist", () => {
|
|
195
|
+
expect(() => clearIpcFile(tempDir)).not.toThrow();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("CliWebRtcListener", () => {
|
|
200
|
+
let tempDir;
|
|
201
|
+
let CliWebRtcListener;
|
|
202
|
+
|
|
203
|
+
beforeEach(async () => {
|
|
204
|
+
tempDir = path.join(
|
|
205
|
+
os.tmpdir(),
|
|
206
|
+
"test-webrtc-listener-" +
|
|
207
|
+
Date.now() +
|
|
208
|
+
"-" +
|
|
209
|
+
Math.random().toString(36).slice(2),
|
|
210
|
+
);
|
|
211
|
+
// Dynamic import to pick up after initial module load
|
|
212
|
+
({ CliWebRtcListener } = await import("./webrtc-control-client.mjs"));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
afterEach(() => {
|
|
216
|
+
try {
|
|
217
|
+
if (fs.existsSync(tempDir)) {
|
|
218
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
} catch (_) {}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("constructs CliWebRtcListener with defaults and assigns a cli- peer ID", () => {
|
|
224
|
+
const listener = new CliWebRtcListener({ cacheDir: tempDir });
|
|
225
|
+
expect(listener.cliPeerId).toMatch(/^cli-[0-9a-z]+$/);
|
|
226
|
+
expect(listener.host).toBe("127.0.0.1");
|
|
227
|
+
expect(listener.port).toBe(8888);
|
|
228
|
+
expect(listener.path).toBe("/");
|
|
229
|
+
expect(listener.secure).toBe(false);
|
|
230
|
+
expect(listener.trustedPeerIds).toBeInstanceOf(Set);
|
|
231
|
+
expect(listener.trustedPeerIds.size).toBe(0);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("constructs CliWebRtcListener with a fixed peer ID", () => {
|
|
235
|
+
const listener = new CliWebRtcListener({
|
|
236
|
+
cacheDir: tempDir,
|
|
237
|
+
peerId: "cli-fixed-peer",
|
|
238
|
+
});
|
|
239
|
+
expect(listener.cliPeerId).toBe("cli-fixed-peer");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("populates trustedPeerIds from options", () => {
|
|
243
|
+
const listener = new CliWebRtcListener({
|
|
244
|
+
cacheDir: tempDir,
|
|
245
|
+
trustedPeerIds: ["browser-peer-1", "browser-peer-2"],
|
|
246
|
+
});
|
|
247
|
+
expect(listener.trustedPeerIds.has("browser-peer-1")).toBe(true);
|
|
248
|
+
expect(listener.trustedPeerIds.has("browser-peer-2")).toBe(true);
|
|
249
|
+
expect(listener.trustedPeerIds.size).toBe(2);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("_isTrusted returns true for any peer when trustedPeerIds is empty", () => {
|
|
253
|
+
const listener = new CliWebRtcListener({ cacheDir: tempDir });
|
|
254
|
+
expect(listener._isTrusted("any-random-peer")).toBe(true);
|
|
255
|
+
expect(listener._isTrusted("cli-abc")).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("_isTrusted returns true only for listed peers when trustedPeerIds is set", () => {
|
|
259
|
+
const listener = new CliWebRtcListener({
|
|
260
|
+
cacheDir: tempDir,
|
|
261
|
+
trustedPeerIds: ["browser-abc"],
|
|
262
|
+
});
|
|
263
|
+
expect(listener._isTrusted("browser-abc")).toBe(true);
|
|
264
|
+
expect(listener._isTrusted("browser-xyz")).toBe(false);
|
|
265
|
+
expect(listener._isTrusted("")).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("_isTrusted supports wildcard suffix matching", () => {
|
|
269
|
+
const listener = new CliWebRtcListener({
|
|
270
|
+
cacheDir: tempDir,
|
|
271
|
+
trustedPeerIds: ["browser-*"],
|
|
272
|
+
});
|
|
273
|
+
expect(listener._isTrusted("browser-abc")).toBe(true);
|
|
274
|
+
expect(listener._isTrusted("browser-123")).toBe(true);
|
|
275
|
+
expect(listener._isTrusted("other-peer")).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("close() before start() does not throw", () => {
|
|
279
|
+
const listener = new CliWebRtcListener({ cacheDir: tempDir });
|
|
280
|
+
expect(() => listener.close()).not.toThrow();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("renewPeerId generates a fresh peer ID on construction", () => {
|
|
284
|
+
const listener1 = new CliWebRtcListener({ cacheDir: tempDir });
|
|
285
|
+
const listener2 = new CliWebRtcListener({
|
|
286
|
+
cacheDir: tempDir,
|
|
287
|
+
renewPeerId: true,
|
|
288
|
+
});
|
|
289
|
+
expect(listener2.cliPeerId).not.toBe(listener1.cliPeerId);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("_scheduleReconnect implements exponential backoff and tracks attempts", () => {
|
|
293
|
+
const listener = new CliWebRtcListener({ cacheDir: tempDir });
|
|
294
|
+
listener._running = true;
|
|
295
|
+
const mockPeer = {
|
|
296
|
+
destroyed: false,
|
|
297
|
+
reconnect: jest.fn(),
|
|
298
|
+
};
|
|
299
|
+
listener._peer = mockPeer;
|
|
300
|
+
|
|
301
|
+
expect(listener._reconnectAttempts).toBe(0);
|
|
302
|
+
expect(listener._reconnectTimer).toBeNull();
|
|
303
|
+
|
|
304
|
+
// First schedule
|
|
305
|
+
listener._scheduleReconnect();
|
|
306
|
+
expect(listener._reconnectAttempts).toBe(1);
|
|
307
|
+
expect(listener._reconnectTimer).not.toBeNull();
|
|
308
|
+
|
|
309
|
+
// Scheduling again while timer is active is a no-op
|
|
310
|
+
listener._scheduleReconnect();
|
|
311
|
+
expect(listener._reconnectAttempts).toBe(1);
|
|
312
|
+
|
|
313
|
+
// Closing clears timer and resets attempts
|
|
314
|
+
listener.close();
|
|
315
|
+
expect(listener._reconnectAttempts).toBe(0);
|
|
316
|
+
expect(listener._reconnectTimer).toBeNull();
|
|
317
|
+
});
|
|
318
|
+
});
|
package/dist/electron/main.cjs
CHANGED
|
@@ -30,7 +30,7 @@ t.exports=a,t.exports.append=r;var n=/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;function r
|
|
|
30
30
|
keys_auth TEXT NOT NULL,
|
|
31
31
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
32
32
|
)
|
|
33
|
-
`),ze)}function Ve(e=`mailto:admin@creativeindustrial.com`){if(!ze)throw Error(`Push store not opened. Call openPushStore() first.`);let t=ze.prepare(`SELECT * FROM vapid_keys WHERE id = 1`).get();if(t)return{publicKey:`${t.public_key}`,privateKey:`${t.private_key}`,subject:`${t.subject}`};let n=E.default.generateVAPIDKeys();return ze.prepare(`INSERT INTO vapid_keys (id, public_key, private_key, subject) VALUES (1, ?, ?, ?)`).run(n.publicKey,n.privateKey,e),{publicKey:n.publicKey,privateKey:n.privateKey,subject:e}}function He(e){if(!ze)throw Error(`Push store not opened.`);ze.prepare(`INSERT OR REPLACE INTO subscriptions (endpoint, keys_p256dh, keys_auth) VALUES (?, ?, ?)`).run(e.endpoint,e.keys.p256dh,e.keys.auth)}function Ue(e){if(!ze)throw Error(`Push store not opened.`);ze.prepare(`DELETE FROM subscriptions WHERE endpoint = ?`).run(e)}function We(e){if(!ze)throw Error(`Push store not opened.`);ze.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(e)}function Ge(e){if(!ze)throw Error(`Push store not opened.`);let t=ze.prepare(`SELECT * FROM subscriptions WHERE endpoint = ?`).get(e);return t?{id:Number(t.id),endpoint:`${t.endpoint}`,keys_p256dh:`${t.keys_p256dh}`,keys_auth:`${t.keys_auth}`,created_at:`${t.created_at}`}:void 0}function Ke(){if(!ze)throw Error(`Push store not opened.`);let e=ze.prepare(`SELECT * FROM subscriptions ORDER BY created_at DESC`).all();return e?e.map(e=>({id:Number(e.id),endpoint:`${e.endpoint}`,keys_p256dh:`${e.keys_p256dh}`,keys_auth:`${e.keys_auth}`,created_at:`${e.created_at}`})):[]}async function qe(e){let t=Ke();if(t.length===0)return{sent:0,failed:0,noSubscribers:!0};let n=Ve(),r=JSON.stringify(e),i={TTL:3600,vapidDetails:{subject:n.subject,publicKey:n.publicKey,privateKey:n.privateKey}},a=0,o=0;return await Promise.all(t.map(async e=>{let t={endpoint:e.endpoint,keys:{p256dh:e.keys_p256dh,auth:e.keys_auth}};try{await E.default.sendNotification(t,r,i),a++}catch(t){o++,(t.statusCode===410||t.statusCode===404)&&Ue(e.endpoint)}})),{sent:a,failed:o}}function Je(e){e.get(`/push/vapid-public-key`,(e,t)=>{let n=Ve();t.json({publicKey:n.publicKey})}),e.post(`/push/subscribe`,(e,t)=>{let n=e.body;if(!n?.endpoint||!n?.keys)return t.status(400).json({error:`Invalid subscription object`});He(n),t.sendStatus(201)}),e.delete(`/push/subscribe`,(e,t)=>{let{endpoint:n}=e.body||{};if(!n)return t.status(400).json({error:`Missing endpoint`});Ue(n),t.sendStatus(200)}),e.get(`/push/subscriptions`,(e,t)=>{let n=Ke();t.json(n)}),e.post(`/push/send`,async(e,t)=>{let{endpoint:n,payload:r}=e.body||{};if(!n)return t.status(400).json({error:`Missing endpoint`});let i=Ge(n);if(!i)return t.status(404).json({error:`Subscription not found`});let a=Ve(),o={endpoint:i.endpoint,keys:{p256dh:i.keys_p256dh,auth:i.keys_auth}},s=JSON.stringify({title:`ShadowClaw`,body:r||`Test notification`}),c={TTL:3600,vapidDetails:{subject:a.subject,publicKey:a.publicKey,privateKey:a.privateKey}};try{await E.default.sendNotification(o,s,c),t.sendStatus(200)}catch(e){if(console.error(`Push send failed:`,e.message),e.statusCode===410||e.statusCode===404)return Ue(n),t.status(410).json({error:`Subscription expired, removed from store`});t.status(500).json({error:e.message})}}),e.delete(`/push/subscription/:id`,(e,t)=>{let n=parseInt(e.params.id,10);if(isNaN(n))return t.status(400).json({error:`Invalid subscription ID`});We(n),t.sendStatus(200)}),e.post(`/push/broadcast`,async(e,t)=>{let{title:n,body:r}=e.body||{};if(!r)return t.status(400).json({error:`Missing body`});try{let e=await qe({title:n||`ShadowClaw`,body:r});t.json(e)}catch(e){t.status(500).json({error:e.message})}})}let Ye=null;function Xe(e){return{id:`${e.id}`,group_id:`${e.group_id}`,schedule:`${e.schedule}`,type:e.type?`${e.type}`:null,prompt:`${e.prompt}`,tools:e.tools?`${e.tools}`:null,enabled:Number(e.enabled),last_run:e.last_run==null?null:Number(e.last_run),created_at:Number(e.created_at),channel:e.channel?`${e.channel}`:null,subscriber_id:e.subscriber_id?`${e.subscriber_id}`:null,name:e.name?`${e.name}`:null,task_order:e.task_order==null?null:Number(e.task_order)}}function Ze(e=`database/scheduled-tasks.db`){if(Ye)return Ye;Ye=new D.DatabaseSync(e),Ye.exec(`
|
|
33
|
+
`),ze)}function Ve(e=`mailto:admin@creativeindustrial.com`){if(!ze)throw Error(`Push store not opened. Call openPushStore() first.`);let t=ze.prepare(`SELECT * FROM vapid_keys WHERE id = 1`).get();if(t)return{publicKey:`${t.public_key}`,privateKey:`${t.private_key}`,subject:`${t.subject}`};let n=E.default.generateVAPIDKeys();return ze.prepare(`INSERT INTO vapid_keys (id, public_key, private_key, subject) VALUES (1, ?, ?, ?)`).run(n.publicKey,n.privateKey,e),{publicKey:n.publicKey,privateKey:n.privateKey,subject:e}}function He(e){if(!ze)throw Error(`Push store not opened.`);ze.prepare(`INSERT OR REPLACE INTO subscriptions (endpoint, keys_p256dh, keys_auth) VALUES (?, ?, ?)`).run(e.endpoint,e.keys.p256dh,e.keys.auth)}function Ue(e){if(!ze)throw Error(`Push store not opened.`);ze.prepare(`DELETE FROM subscriptions WHERE endpoint = ?`).run(e)}function We(e){if(!ze)throw Error(`Push store not opened.`);ze.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(e)}function Ge(e){if(!ze)throw Error(`Push store not opened.`);let t=ze.prepare(`SELECT * FROM subscriptions WHERE endpoint = ?`).get(e);return t?{id:Number(t.id),endpoint:`${t.endpoint}`,keys_p256dh:`${t.keys_p256dh}`,keys_auth:`${t.keys_auth}`,created_at:`${t.created_at}`}:void 0}function Ke(){if(!ze)throw Error(`Push store not opened.`);let e=ze.prepare(`SELECT * FROM subscriptions ORDER BY created_at DESC, id DESC`).all();return e?e.map(e=>({id:Number(e.id),endpoint:`${e.endpoint}`,keys_p256dh:`${e.keys_p256dh}`,keys_auth:`${e.keys_auth}`,created_at:`${e.created_at}`})):[]}async function qe(e){let t=Ke();if(t.length===0)return{sent:0,failed:0,noSubscribers:!0};let n=Ve(),r=JSON.stringify(e),i={TTL:3600,vapidDetails:{subject:n.subject,publicKey:n.publicKey,privateKey:n.privateKey}},a=0,o=0;return await Promise.all(t.map(async e=>{let t={endpoint:e.endpoint,keys:{p256dh:e.keys_p256dh,auth:e.keys_auth}};try{await E.default.sendNotification(t,r,i),a++}catch(t){o++,(t.statusCode===410||t.statusCode===404)&&Ue(e.endpoint)}})),{sent:a,failed:o}}function Je(e){e.get(`/push/vapid-public-key`,(e,t)=>{let n=Ve();t.json({publicKey:n.publicKey})}),e.post(`/push/subscribe`,(e,t)=>{let n=e.body;if(!n?.endpoint||!n?.keys)return t.status(400).json({error:`Invalid subscription object`});He(n),t.sendStatus(201)}),e.delete(`/push/subscribe`,(e,t)=>{let{endpoint:n}=e.body||{};if(!n)return t.status(400).json({error:`Missing endpoint`});Ue(n),t.sendStatus(200)}),e.get(`/push/subscriptions`,(e,t)=>{let n=Ke();t.json(n)}),e.post(`/push/send`,async(e,t)=>{let{endpoint:n,payload:r}=e.body||{};if(!n)return t.status(400).json({error:`Missing endpoint`});let i=Ge(n);if(!i)return t.status(404).json({error:`Subscription not found`});let a=Ve(),o={endpoint:i.endpoint,keys:{p256dh:i.keys_p256dh,auth:i.keys_auth}},s=JSON.stringify({title:`ShadowClaw`,body:r||`Test notification`}),c={TTL:3600,vapidDetails:{subject:a.subject,publicKey:a.publicKey,privateKey:a.privateKey}};try{await E.default.sendNotification(o,s,c),t.sendStatus(200)}catch(e){if(console.error(`Push send failed:`,e.message),e.statusCode===410||e.statusCode===404)return Ue(n),t.status(410).json({error:`Subscription expired, removed from store`});t.status(500).json({error:e.message})}}),e.delete(`/push/subscription/:id`,(e,t)=>{let n=parseInt(e.params.id,10);if(isNaN(n))return t.status(400).json({error:`Invalid subscription ID`});We(n),t.sendStatus(200)}),e.post(`/push/broadcast`,async(e,t)=>{let{title:n,body:r}=e.body||{};if(!r)return t.status(400).json({error:`Missing body`});try{let e=await qe({title:n||`ShadowClaw`,body:r});t.json(e)}catch(e){t.status(500).json({error:e.message})}}),e.post(`/push/command`,async(e,t)=>{let{clientId:n,action:r,args:i,prompt:a}=e.body||{};if(!r)return t.status(400).json({error:`Missing action`});try{let e=await qe({type:`remote-command`,title:`ShadowClaw Command`,body:a||`Remote command: ${r}`,clientId:n,action:r,args:i});t.json(e)}catch(e){t.status(500).json({error:e.message})}})}let Ye=null;function Xe(e){return{id:`${e.id}`,group_id:`${e.group_id}`,schedule:`${e.schedule}`,type:e.type?`${e.type}`:null,prompt:`${e.prompt}`,tools:e.tools?`${e.tools}`:null,enabled:Number(e.enabled),last_run:e.last_run==null?null:Number(e.last_run),created_at:Number(e.created_at),channel:e.channel?`${e.channel}`:null,subscriber_id:e.subscriber_id?`${e.subscriber_id}`:null,name:e.name?`${e.name}`:null,task_order:e.task_order==null?null:Number(e.task_order)}}function Ze(e=`database/scheduled-tasks.db`){if(Ye)return Ye;Ye=new D.DatabaseSync(e),Ye.exec(`
|
|
34
34
|
CREATE TABLE IF NOT EXISTS scheduled_tasks (
|
|
35
35
|
id TEXT PRIMARY KEY,
|
|
36
36
|
group_id TEXT NOT NULL,
|
package/dist/public/AGENTS.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
## Project Snapshot
|
|
7
7
|
|
|
8
|
-
ShadowClaw is a browser-native AI assistant written in **TypeScript** (`.ts`).
|
|
8
|
+
ShadowClaw is a browser-native AI assistant written in **TypeScript** (`.ts`) — deployable as a PWA, a native desktop app via Electron, a standalone Node.js server, and driven via the `shadow-claw` CLI (for builds, dev serving, and runtime agent interaction).
|
|
9
9
|
The project uses a **Rolldown build pipeline** to bundle the application.
|
|
10
10
|
|
|
11
11
|
**Stack:** HTML + TypeScript / ESM · Web Components · TC39 Signals · IndexedDB · OPFS · Web Workers · Service Worker (Workbox PWA · Web Push) · Express dev server · Electron desktop · AWS Bedrock · Jest + Playwright tests
|
|
@@ -44,6 +44,8 @@ ShadowClaw has been significantly deduplicated. Instead of a massive `AGENTS.md`
|
|
|
44
44
|
| WebMCP Integration | [docs/subsystems/webmcp.md](docs/subsystems/webmcp.md) |
|
|
45
45
|
| WebVM (v86 Alpine) | [docs/subsystems/vm.md](docs/subsystems/vm.md) |
|
|
46
46
|
| CLI & Static Site Publishing | [docs/subsystems/cli.md](docs/subsystems/cli.md) |
|
|
47
|
+
| Control Plane & Client Bridge | [docs/subsystems/control-plane.md](docs/subsystems/control-plane.md) |
|
|
48
|
+
| File Backup Subsystem | [docs/subsystems/backup.md](docs/subsystems/backup.md) |
|
|
47
49
|
|
|
48
50
|
## Conventions & Guardrails
|
|
49
51
|
|
|
@@ -150,7 +152,7 @@ Markdown and HTML preview work should preserve the Settings-backed iframe host a
|
|
|
150
152
|
### CLI & Dual-Root Build Pipeline
|
|
151
153
|
|
|
152
154
|
- **Dual-Root Path Resolution:** The build toolchain (`bin/build/build.mjs`) cleanly decouples `toolchainRoot` (the ShadowClaw package/repo root) from `contentRoot` (the consumer template project). In-repo builds (`resolve(contentRoot) === resolve(toolchainRoot)`) preserve the standalone in-tree compilation path. CLI/template consumer builds read pre-bundled web assets from `toolchainRoot/dist/public` and inject `pages/`, `site-config.json`, `assets/`, `.agents/`, and pretty routes from `contentRoot`, outputting to `<contentRoot>/dist/public`.
|
|
153
|
-
- **CLI Commands (`bin/cli.mjs`):** The `shadow-claw` / `shadowclaw` CLI provides `build`, `dev`, `run`, `serve`, and `
|
|
155
|
+
- **CLI Commands (`bin/cli.mjs`):** The `shadow-claw` / `shadowclaw` CLI provides `build`, `dev`, `run`, `serve`, `init`, `clients`, `send`, `backup`, `tasks`, `webrtc`, and `peer-id` commands. It supports running dev servers programmatically via `startServer` (`src/server/server.ts`) with custom `--root-path` and `--database-dir` arguments.
|
|
154
156
|
- **Naming Conventions:** Refer to the product/brand in prose and documentation as **ShadowClaw**. Use kebab-case **`shadow-claw`** for package name, CLI commands (`npx shadow-claw`), repositories, directory paths, and custom elements.
|
|
155
157
|
|
|
156
158
|
## What to Avoid
|
|
@@ -162,7 +164,7 @@ Markdown and HTML preview work should preserve the Settings-backed iframe host a
|
|
|
162
164
|
- **Do not** import Electron modules from browser-side `.ts` files — Electron is desktop-only.
|
|
163
165
|
- **Do not** register or inject unapproved custom elements without configuring them through `site-config.json` or `setAllowedCustomElements`.
|
|
164
166
|
- **Do not** rely on `navigator.modelContext` alone for WebMCP detection; prefer `document.modelContext` with `navigator.modelContext` fallback for compatibility. Use `parseWebMcpInputSchema` to normalize schemas across Chrome 154+ (native object) and Chrome < 154 (DOMString JSON) versions, and `getWebMcpTools` for querying registered tools with graceful degradation.
|
|
165
|
-
- **Do not** commit `dist-electron/`, `push-subscriptions.db`,
|
|
167
|
+
- **Do not** commit `dist-electron/`, `push-subscriptions.db`, `scheduled-tasks.db`, or `clients.db` — they are git-ignored.
|
|
166
168
|
- **Do not** add new docs pages without updating `docs/README.md` and verifying references in `AGENTS.md`.
|
|
167
169
|
- **Do not** allow file-browser copy/move flows to target the same folder or any descendant folder; enforce the guard in both UI and storage paths and cover it with tests. Always specify both `sourceGroupId` and `targetGroupId` when invoking storage moves/copies.
|
|
168
170
|
- **Do not** use raw `window.location.href` or traditional link anchors for internal navigation. Always dispatch a `shadow-claw-navigate` custom event (or use the `handleSpecialLinkNavigation` utility) to switch pages, open files, or scroll to anchors seamlessly.
|
package/dist/public/README.md
CHANGED
|
@@ -2,23 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://deepwiki.com/xt-ml/shadow-claw)
|
|
4
4
|
|
|
5
|
-
A browser-native, fully-featured AI assistant. TypeScript + Web Components + IndexedDB + File System API.
|
|
5
|
+
A browser-native, fully-featured AI assistant — runs as a PWA, a native desktop app, a Node.js server, and via the `shadow-claw` CLI. TypeScript + Web Components + IndexedDB + File System API.
|
|
6
6
|
|
|
7
7
|
[](https://xt-ml.github.io/shadow-claw/)
|
|
8
8
|
|
|
9
9
|
## Quick Start
|
|
10
10
|
|
|
11
|
+
### 1. Launch ShadowClaw instantly without cloning the repo
|
|
12
|
+
|
|
11
13
|
```bash
|
|
12
|
-
|
|
14
|
+
npx shadow-claw dev --open
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
Open Settings, select
|
|
17
|
+
Open Settings, select your AI provider (or use the in-browser Prompt API / local models), and start chatting.
|
|
18
|
+
|
|
19
|
+
### 2. Scaffold a Project or Knowledge Hub
|
|
20
|
+
|
|
21
|
+
Create a custom template with pages and site configuration:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx shadow-claw init my-assistant
|
|
25
|
+
cd my-assistant
|
|
26
|
+
npx shadow-claw dev --open
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 3. From Source (Contributing / Development)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/xt-ml/shadow-claw.git
|
|
33
|
+
cd shadow-claw
|
|
34
|
+
npm install && npm run dev # Dev server → http://localhost:8888
|
|
35
|
+
```
|
|
16
36
|
|
|
17
|
-
**Desktop:** `npm run electron` or `npm run electron:build` for distributable.
|
|
37
|
+
**Desktop App:** `npm run electron` or `npm run electron:build` for distributable installer.
|
|
18
38
|
|
|
19
39
|
## What is ShadowClaw?
|
|
20
40
|
|
|
21
|
-
A fully-functional agent runtime
|
|
41
|
+
A fully-functional agent runtime built on browser-native technology — the orchestration and tool-use loop run client-side, with AI inference routing to remote APIs, local servers, or in-browser models depending on your configuration. Built with TypeScript, it provides:
|
|
22
42
|
|
|
23
43
|
- **Multi-model support**: OpenRouter, Anthropic, Google Gemini, AWS Bedrock, Ollama, Llamafile, Mesh LLM, Transformers.js, and browser-native Prompt API
|
|
24
44
|
- **Web Components UI**: Native Custom Elements + TC39 Signals for reactive updates
|
|
@@ -378,6 +398,23 @@ npm run electron:build:win # Build Electron for Windows
|
|
|
378
398
|
npm run electron:build:mac # Build Electron for macOS
|
|
379
399
|
```
|
|
380
400
|
|
|
401
|
+
### CLI Runtime Commands
|
|
402
|
+
|
|
403
|
+
The `shadow-claw` CLI connects to a running server or browser clients to interact with active sessions:
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
npx shadow-claw clients # List connected browser/Electron clients
|
|
407
|
+
npx shadow-claw send "your prompt" --client <id> # Send a message to a connected client
|
|
408
|
+
npx shadow-claw backup # Trigger OPFS workspace backup
|
|
409
|
+
npx shadow-claw backup list # List available backup snapshots
|
|
410
|
+
npx shadow-claw backup delete --backup-id <id> # Delete a backup snapshot
|
|
411
|
+
npx shadow-claw tasks --client <id> # List scheduled tasks on a client
|
|
412
|
+
npx shadow-claw webrtc listen # Start headless WebRTC DataChannel daemon
|
|
413
|
+
npx shadow-claw peer-id # Get or generate persistent CLI Peer ID
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Commands support `--transport webrtc` for direct peer-to-peer DataChannel execution with connected browser clients. Control plane authentication uses `SHADOWCLAW_CONTROL_TOKEN` (env) or `--token` flag. The control plane endpoint and token are printed to the console on server start.
|
|
417
|
+
|
|
381
418
|
## License
|
|
382
419
|
|
|
383
420
|
AGPLv3. Core logic derived from [openbrowserclaw](https://github.com/sachaa/openbrowserclaw) (MIT).
|