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,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShadowClaw — Server-Side WebRTC Peer & A2A Bridge
|
|
3
|
+
*
|
|
4
|
+
* Enables the ShadowClaw server to act as a first-class WebRTC peer using
|
|
5
|
+
* node-datachannel. Can connect to local or remote PeerJS signaling servers,
|
|
6
|
+
* communicate directly with browser clients, other ShadowClaw servers,
|
|
7
|
+
* and CLI tools over RTCDataChannels.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getPeerClass } from "./webrtc.js";
|
|
11
|
+
import { ulid } from "../utils/ulid.js";
|
|
12
|
+
import type {
|
|
13
|
+
ControlMessage,
|
|
14
|
+
CommandExecutePayload,
|
|
15
|
+
CommandResultPayload,
|
|
16
|
+
} from "./control-plane-types.js";
|
|
17
|
+
|
|
18
|
+
export interface ServerPeerOptions {
|
|
19
|
+
peerId?: string;
|
|
20
|
+
host?: string;
|
|
21
|
+
port?: number;
|
|
22
|
+
path?: string;
|
|
23
|
+
secure?: boolean;
|
|
24
|
+
verbose?: boolean;
|
|
25
|
+
handlers?: Record<string, (args: any) => Promise<any> | any>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ServerPeer {
|
|
29
|
+
private _peerId: string;
|
|
30
|
+
private _host: string;
|
|
31
|
+
private _port: number;
|
|
32
|
+
private _path: string;
|
|
33
|
+
private _secure: boolean;
|
|
34
|
+
private _verbose: boolean;
|
|
35
|
+
private _peer: any = null;
|
|
36
|
+
private _connections: Map<string, any> = new Map();
|
|
37
|
+
private _pendingCommands: Map<
|
|
38
|
+
string,
|
|
39
|
+
{
|
|
40
|
+
resolve: (val: any) => void;
|
|
41
|
+
reject: (err: any) => void;
|
|
42
|
+
timeout: NodeJS.Timeout;
|
|
43
|
+
}
|
|
44
|
+
> = new Map();
|
|
45
|
+
private _handlers: Map<string, (args: any) => Promise<any> | any> = new Map();
|
|
46
|
+
private _isOpen = false;
|
|
47
|
+
|
|
48
|
+
constructor(options: ServerPeerOptions = {}) {
|
|
49
|
+
this._peerId = options.peerId || `server-${ulid().toLowerCase()}`;
|
|
50
|
+
this._host = options.host || "127.0.0.1";
|
|
51
|
+
this._port = options.port || 8888;
|
|
52
|
+
this._path = options.path || "/";
|
|
53
|
+
this._secure = options.secure ?? false;
|
|
54
|
+
this._verbose = options.verbose ?? false;
|
|
55
|
+
|
|
56
|
+
if (options.handlers) {
|
|
57
|
+
for (const [action, fn] of Object.entries(options.handlers)) {
|
|
58
|
+
this._handlers.set(action, fn);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public get peerId(): string {
|
|
64
|
+
return this._peerId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public get isOpen(): boolean {
|
|
68
|
+
return this._isOpen;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private log(msg: string): void {
|
|
72
|
+
if (this._verbose) {
|
|
73
|
+
console.log(`[server-peer] ${msg}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async start(): Promise<string> {
|
|
78
|
+
const Peer = await getPeerClass();
|
|
79
|
+
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
try {
|
|
82
|
+
const peer = new Peer(this._peerId, {
|
|
83
|
+
host: this._host,
|
|
84
|
+
port: this._port,
|
|
85
|
+
path: this._path,
|
|
86
|
+
secure: this._secure,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this._peer = peer;
|
|
90
|
+
|
|
91
|
+
peer.on("open", (id: string) => {
|
|
92
|
+
this._peerId = id;
|
|
93
|
+
this._isOpen = true;
|
|
94
|
+
this.log(`Server peer opened with ID: ${id}`);
|
|
95
|
+
resolve(id);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
peer.on("connection", (conn: any) => {
|
|
99
|
+
this._setupConnection(conn);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
peer.on("error", (err: any) => {
|
|
103
|
+
this.log(`Server peer error: ${err.message || String(err)}`);
|
|
104
|
+
if (!this._isOpen) {
|
|
105
|
+
reject(err);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
peer.on("close", () => {
|
|
110
|
+
this._isOpen = false;
|
|
111
|
+
this._connections.clear();
|
|
112
|
+
this.log("Server peer closed");
|
|
113
|
+
});
|
|
114
|
+
} catch (err) {
|
|
115
|
+
reject(err);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private _setupConnection(conn: any): void {
|
|
121
|
+
const remotePeerId = conn.peer;
|
|
122
|
+
|
|
123
|
+
conn.on("open", () => {
|
|
124
|
+
this._connections.set(remotePeerId, conn);
|
|
125
|
+
this.log(`DataChannel connected with peer: ${remotePeerId}`);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
conn.on("data", (data: any) => {
|
|
129
|
+
this._handleData(remotePeerId, conn, data);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
conn.on("close", () => {
|
|
133
|
+
this._connections.delete(remotePeerId);
|
|
134
|
+
this.log(`DataChannel closed for peer: ${remotePeerId}`);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
conn.on("error", (err: any) => {
|
|
138
|
+
this.log(`DataChannel error with ${remotePeerId}: ${err.message}`);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async _handleData(
|
|
143
|
+
_remotePeerId: string,
|
|
144
|
+
conn: any,
|
|
145
|
+
rawData: any,
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
let msg: any = rawData;
|
|
148
|
+
if (typeof rawData === "string") {
|
|
149
|
+
try {
|
|
150
|
+
msg = JSON.parse(rawData);
|
|
151
|
+
} catch {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!msg || typeof msg !== "object") {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle Control Plane execute commands
|
|
161
|
+
if (msg.type === "command:execute") {
|
|
162
|
+
const payload = msg.payload as CommandExecutePayload;
|
|
163
|
+
const { commandId, action, args } = payload || {};
|
|
164
|
+
|
|
165
|
+
const handler = this._handlers.get(action);
|
|
166
|
+
if (!handler) {
|
|
167
|
+
const result: ControlMessage = {
|
|
168
|
+
id: ulid(),
|
|
169
|
+
type: "command:result",
|
|
170
|
+
replyTo: msg.id,
|
|
171
|
+
payload: {
|
|
172
|
+
commandId,
|
|
173
|
+
success: false,
|
|
174
|
+
error: `Unknown action: ${action}`,
|
|
175
|
+
} as CommandResultPayload,
|
|
176
|
+
};
|
|
177
|
+
conn.send(result);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const data = await handler(args);
|
|
183
|
+
const result: ControlMessage = {
|
|
184
|
+
id: ulid(),
|
|
185
|
+
type: "command:result",
|
|
186
|
+
replyTo: msg.id,
|
|
187
|
+
payload: {
|
|
188
|
+
commandId,
|
|
189
|
+
success: true,
|
|
190
|
+
data,
|
|
191
|
+
} as CommandResultPayload,
|
|
192
|
+
};
|
|
193
|
+
conn.send(result);
|
|
194
|
+
} catch (err: any) {
|
|
195
|
+
const result: ControlMessage = {
|
|
196
|
+
id: ulid(),
|
|
197
|
+
type: "command:result",
|
|
198
|
+
replyTo: msg.id,
|
|
199
|
+
payload: {
|
|
200
|
+
commandId,
|
|
201
|
+
success: false,
|
|
202
|
+
error: err.message || String(err),
|
|
203
|
+
} as CommandResultPayload,
|
|
204
|
+
};
|
|
205
|
+
conn.send(result);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Handle Control Plane command result responses
|
|
211
|
+
if (msg.type === "command:result") {
|
|
212
|
+
const res = msg.payload as CommandResultPayload;
|
|
213
|
+
if (res?.commandId && this._pendingCommands.has(res.commandId)) {
|
|
214
|
+
const pending = this._pendingCommands.get(res.commandId)!;
|
|
215
|
+
clearTimeout(pending.timeout);
|
|
216
|
+
this._pendingCommands.delete(res.commandId);
|
|
217
|
+
pending.resolve(res);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
public isPeerConnected(remotePeerId: string): boolean {
|
|
224
|
+
const conn = this._connections.get(remotePeerId);
|
|
225
|
+
return !!conn && (conn.open || conn._open);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public getConnectedPeers(): string[] {
|
|
229
|
+
return Array.from(this._connections.keys());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public async sendCommand(
|
|
233
|
+
remotePeerId: string,
|
|
234
|
+
action: string,
|
|
235
|
+
args: Record<string, unknown> = {},
|
|
236
|
+
timeoutMs: number = 30_000,
|
|
237
|
+
): Promise<CommandResultPayload> {
|
|
238
|
+
if (!this._peer || !this._isOpen) {
|
|
239
|
+
throw new Error("Server peer is not running");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let conn = this._connections.get(remotePeerId);
|
|
243
|
+
if (!conn || !conn.open) {
|
|
244
|
+
conn = this._peer.connect(remotePeerId, {
|
|
245
|
+
reliable: true,
|
|
246
|
+
serialization: "binary",
|
|
247
|
+
});
|
|
248
|
+
this._setupConnection(conn);
|
|
249
|
+
|
|
250
|
+
await new Promise<void>((resolve, reject) => {
|
|
251
|
+
const t = setTimeout(
|
|
252
|
+
() => {
|
|
253
|
+
reject(
|
|
254
|
+
new Error(
|
|
255
|
+
`Connection to peer ${remotePeerId} timed out after ${timeoutMs}ms`,
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
},
|
|
259
|
+
Math.min(10_000, timeoutMs),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
conn.on("open", () => {
|
|
263
|
+
clearTimeout(t);
|
|
264
|
+
resolve();
|
|
265
|
+
});
|
|
266
|
+
conn.on("error", (err: any) => {
|
|
267
|
+
clearTimeout(t);
|
|
268
|
+
reject(err);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const commandId = ulid();
|
|
274
|
+
const commandPayload: CommandExecutePayload = {
|
|
275
|
+
commandId,
|
|
276
|
+
action: action as any,
|
|
277
|
+
args,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const message: ControlMessage = {
|
|
281
|
+
id: ulid(),
|
|
282
|
+
type: "command:execute",
|
|
283
|
+
payload: commandPayload,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return new Promise<CommandResultPayload>((resolve, reject) => {
|
|
287
|
+
const timeout = setTimeout(() => {
|
|
288
|
+
this._pendingCommands.delete(commandId);
|
|
289
|
+
reject(
|
|
290
|
+
new Error(
|
|
291
|
+
`Command ${action} to peer ${remotePeerId} timed out after ${timeoutMs}ms`,
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
}, timeoutMs);
|
|
295
|
+
|
|
296
|
+
this._pendingCommands.set(commandId, { resolve, reject, timeout });
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
conn.send(message);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
clearTimeout(timeout);
|
|
302
|
+
this._pendingCommands.delete(commandId);
|
|
303
|
+
reject(err);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
public close(): void {
|
|
309
|
+
this._isOpen = false;
|
|
310
|
+
for (const [, pending] of this._pendingCommands) {
|
|
311
|
+
clearTimeout(pending.timeout);
|
|
312
|
+
pending.reject(new Error("Server peer closed"));
|
|
313
|
+
}
|
|
314
|
+
this._pendingCommands.clear();
|
|
315
|
+
|
|
316
|
+
for (const conn of this._connections.values()) {
|
|
317
|
+
try {
|
|
318
|
+
conn.close();
|
|
319
|
+
} catch (_) {}
|
|
320
|
+
}
|
|
321
|
+
this._connections.clear();
|
|
322
|
+
|
|
323
|
+
if (this._peer) {
|
|
324
|
+
try {
|
|
325
|
+
this._peer.destroy();
|
|
326
|
+
} catch (_) {}
|
|
327
|
+
this._peer = null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
package/src/server/server.ts
CHANGED
|
@@ -7,6 +7,8 @@ import tcpPortUsed from "tcp-port-used";
|
|
|
7
7
|
import { DEFAULT_DEV_IP } from "../config/config.js";
|
|
8
8
|
import { createApp } from "./app.js";
|
|
9
9
|
import { attachPeerServer } from "./peer.js";
|
|
10
|
+
import { createControlPlane } from "./control-plane.js";
|
|
11
|
+
import { ServerPeer } from "./server-peer.js";
|
|
10
12
|
import { parseConfig, ServerConfig } from "./config.js";
|
|
11
13
|
|
|
12
14
|
export async function startServer(
|
|
@@ -27,8 +29,22 @@ export async function startServer(
|
|
|
27
29
|
|
|
28
30
|
const httpServer = http.createServer(app);
|
|
29
31
|
|
|
32
|
+
const controlPlane = createControlPlane({
|
|
33
|
+
httpServer,
|
|
34
|
+
app,
|
|
35
|
+
token: config.controlToken,
|
|
36
|
+
verbose: config.verbose,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
let serverPeer: ServerPeer | null = null;
|
|
30
40
|
if (config.peerjs) {
|
|
31
41
|
attachPeerServer(httpServer, app);
|
|
42
|
+
serverPeer = new ServerPeer({
|
|
43
|
+
host: config.bindHost,
|
|
44
|
+
port: config.port,
|
|
45
|
+
path: "/peerjs",
|
|
46
|
+
verbose: config.verbose,
|
|
47
|
+
});
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
return new Promise<http.Server>((resolve) => {
|
|
@@ -53,8 +69,27 @@ export async function startServer(
|
|
|
53
69
|
);
|
|
54
70
|
}
|
|
55
71
|
|
|
72
|
+
console.log(
|
|
73
|
+
`Control plane active at http://${config.bindHost}:${config.port}/api/control/events (SSE) and ws://${config.bindHost}:${config.port}/ws/control (WebSocket)`,
|
|
74
|
+
);
|
|
75
|
+
console.log(`Control token: ${controlPlane.getToken()}`);
|
|
76
|
+
|
|
56
77
|
if (config.peerjs) {
|
|
57
78
|
console.log("PeerJS signaling server enabled (routes at /peerjs/*)");
|
|
79
|
+
if (serverPeer) {
|
|
80
|
+
serverPeer
|
|
81
|
+
.start()
|
|
82
|
+
.then((id) => {
|
|
83
|
+
console.log(`Server WebRTC peer active with ID: ${id}`);
|
|
84
|
+
})
|
|
85
|
+
.catch((err) => {
|
|
86
|
+
if (config.verbose) {
|
|
87
|
+
console.warn(
|
|
88
|
+
`[server] ServerPeer start notice: ${err.message || String(err)}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
58
93
|
}
|
|
59
94
|
|
|
60
95
|
if (config.verbose) {
|
|
@@ -33,7 +33,7 @@ describe("LlamafileManagerService", () => {
|
|
|
33
33
|
return mod.createLlamafileManagerService();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
describe("listBinaries", () => {
|
|
36
|
+
describe("listBinaries & resolveBinary", () => {
|
|
37
37
|
it("lists .llamafile files in the cache directory", async () => {
|
|
38
38
|
statMock.mockResolvedValue({ isDirectory: () => true });
|
|
39
39
|
readdirMock.mockResolvedValue([
|
|
@@ -48,6 +48,21 @@ describe("LlamafileManagerService", () => {
|
|
|
48
48
|
expect(binaries).toHaveLength(2);
|
|
49
49
|
expect(binaries[0].id).toBe("model1");
|
|
50
50
|
expect(binaries[1].id).toBe("model2");
|
|
51
|
+
|
|
52
|
+
const resolved = await service.resolveBinary("model1");
|
|
53
|
+
expect(resolved.fileName).toBe("model1.llamafile");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("throws error if model is not found", async () => {
|
|
57
|
+
statMock.mockResolvedValue({ isDirectory: () => true });
|
|
58
|
+
readdirMock.mockResolvedValue([
|
|
59
|
+
{ name: "model1.llamafile", isFile: () => true },
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const service = await getService();
|
|
63
|
+
await expect(service.resolveBinary("missing")).rejects.toThrow(
|
|
64
|
+
/not found under assets\/cache\/llamafile/,
|
|
65
|
+
);
|
|
51
66
|
});
|
|
52
67
|
|
|
53
68
|
it("throws error if no cache directory is found", async () => {
|
|
@@ -60,7 +75,7 @@ describe("LlamafileManagerService", () => {
|
|
|
60
75
|
});
|
|
61
76
|
});
|
|
62
77
|
|
|
63
|
-
describe("getLlamafileRuntimeOptions", () => {
|
|
78
|
+
describe("getLlamafileRuntimeOptions & Host Validation", () => {
|
|
64
79
|
it("parses options from headers", async () => {
|
|
65
80
|
const service = await getService();
|
|
66
81
|
const req = {
|
|
@@ -100,11 +115,54 @@ describe("LlamafileManagerService", () => {
|
|
|
100
115
|
expect(opts.port).toBe(9090);
|
|
101
116
|
expect(opts.offline).toBe(false);
|
|
102
117
|
});
|
|
118
|
+
|
|
119
|
+
it("rejects non-loopback hosts by default", async () => {
|
|
120
|
+
const service = await getService();
|
|
121
|
+
const req = {
|
|
122
|
+
headers: {
|
|
123
|
+
"x-llamafile-host": "192.168.1.100",
|
|
124
|
+
},
|
|
125
|
+
body: {},
|
|
126
|
+
} as any;
|
|
127
|
+
|
|
128
|
+
expect(() => service.getLlamafileRuntimeOptions(req)).toThrow(
|
|
129
|
+
/Unsafe host '192.168.1.100'/,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
103
132
|
});
|
|
104
133
|
|
|
105
|
-
describe("
|
|
134
|
+
describe("cancelRequest and request ID", () => {
|
|
135
|
+
it("extracts request ID from header and body", async () => {
|
|
136
|
+
const service = await getService();
|
|
137
|
+
const reqWithHeader = {
|
|
138
|
+
headers: { "x-shadowclaw-request-id": "req-123" },
|
|
139
|
+
} as any;
|
|
140
|
+
expect(service.getLlamafileRequestId(reqWithHeader)).toBe("req-123");
|
|
141
|
+
|
|
142
|
+
const reqWithBody = {
|
|
143
|
+
headers: {},
|
|
144
|
+
body: { requestId: "req-456" },
|
|
145
|
+
} as any;
|
|
146
|
+
expect(service.getLlamafileRequestId(reqWithBody, reqWithBody.body)).toBe(
|
|
147
|
+
"req-456",
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("cancels active requests", async () => {
|
|
152
|
+
const service = await getService();
|
|
153
|
+
expect(service.cancelRequest("non-existent")).toBe(false);
|
|
154
|
+
|
|
155
|
+
const cancelFn = jest.fn();
|
|
156
|
+
service.__getActiveRequests().set("req-cancel", cancelFn);
|
|
157
|
+
|
|
158
|
+
expect(service.cancelRequest("req-cancel")).toBe(true);
|
|
159
|
+
expect(cancelFn).toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("createLlamafilePromptEchoFilter & stripLlamafilePromptEcho", () => {
|
|
106
164
|
it("strips echoed prompt from output", async () => {
|
|
107
|
-
const { createLlamafilePromptEchoFilter } =
|
|
165
|
+
const { createLlamafilePromptEchoFilter, stripLlamafilePromptEcho } =
|
|
108
166
|
await import("./llamafile-manager.js");
|
|
109
167
|
const prompt = "USER: hello\n\nASSISTANT:";
|
|
110
168
|
const filter = createLlamafilePromptEchoFilter(prompt);
|
|
@@ -112,6 +170,12 @@ describe("LlamafileManagerService", () => {
|
|
|
112
170
|
expect(filter.push(prompt)).toBe("");
|
|
113
171
|
expect(filter.push(" Hello there!")).toBe(" Hello there!");
|
|
114
172
|
expect(filter.flush()).toBe("");
|
|
173
|
+
|
|
174
|
+
const stripped = stripLlamafilePromptEcho(
|
|
175
|
+
"USER: hello\n\nASSISTANT: Hello there!",
|
|
176
|
+
prompt,
|
|
177
|
+
);
|
|
178
|
+
expect(stripped).toBe("Hello there!");
|
|
115
179
|
});
|
|
116
180
|
|
|
117
181
|
it("handles partial echoes", async () => {
|
|
@@ -129,7 +193,6 @@ describe("LlamafileManagerService", () => {
|
|
|
129
193
|
it("spawns llamafile process and streams output", async () => {
|
|
130
194
|
const service = await getService();
|
|
131
195
|
|
|
132
|
-
// Mock listBinaries/resolveBinary
|
|
133
196
|
statMock.mockResolvedValue({ isDirectory: () => true });
|
|
134
197
|
readdirMock.mockResolvedValue([
|
|
135
198
|
{ name: "test.llamafile", isFile: () => true },
|
|
@@ -161,6 +224,7 @@ describe("LlamafileManagerService", () => {
|
|
|
161
224
|
setHeader: jest.fn(),
|
|
162
225
|
flushHeaders: jest.fn(),
|
|
163
226
|
status: jest.fn().mockReturnThis(),
|
|
227
|
+
json: jest.fn(),
|
|
164
228
|
}) as any;
|
|
165
229
|
|
|
166
230
|
const body = {
|
|
@@ -177,7 +241,7 @@ describe("LlamafileManagerService", () => {
|
|
|
177
241
|
false,
|
|
178
242
|
);
|
|
179
243
|
|
|
180
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
244
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
181
245
|
|
|
182
246
|
child.stdout.emit(
|
|
183
247
|
"data",
|
|
@@ -192,5 +256,165 @@ describe("LlamafileManagerService", () => {
|
|
|
192
256
|
);
|
|
193
257
|
expect(res.end).toHaveBeenCalled();
|
|
194
258
|
});
|
|
259
|
+
|
|
260
|
+
it("handles non-streaming responses and parses tool calls", async () => {
|
|
261
|
+
const service = await getService();
|
|
262
|
+
|
|
263
|
+
statMock.mockResolvedValue({ isDirectory: () => true });
|
|
264
|
+
readdirMock.mockResolvedValue([
|
|
265
|
+
{ name: "test.llamafile", isFile: () => true },
|
|
266
|
+
]);
|
|
267
|
+
accessMock.mockResolvedValue(undefined);
|
|
268
|
+
|
|
269
|
+
const child: any = new EventEmitter();
|
|
270
|
+
child.stdout = new EventEmitter();
|
|
271
|
+
child.stderr = new EventEmitter();
|
|
272
|
+
child.pid = 1234;
|
|
273
|
+
child.kill = jest.fn();
|
|
274
|
+
child.exitCode = null;
|
|
275
|
+
child.signalCode = null;
|
|
276
|
+
child.killed = false;
|
|
277
|
+
|
|
278
|
+
spawnMock.mockImplementation(() => {
|
|
279
|
+
queueMicrotask(() => child.emit("spawn"));
|
|
280
|
+
|
|
281
|
+
return child;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const req: any = new EventEmitter();
|
|
285
|
+
req.headers = {};
|
|
286
|
+
req.socket = new EventEmitter();
|
|
287
|
+
|
|
288
|
+
const res = Object.assign(new EventEmitter(), {
|
|
289
|
+
write: jest.fn(),
|
|
290
|
+
end: jest.fn(),
|
|
291
|
+
setHeader: jest.fn(),
|
|
292
|
+
flushHeaders: jest.fn(),
|
|
293
|
+
status: jest.fn().mockReturnThis(),
|
|
294
|
+
json: jest.fn(),
|
|
295
|
+
}) as any;
|
|
296
|
+
|
|
297
|
+
const body = {
|
|
298
|
+
model: "test",
|
|
299
|
+
messages: [{ role: "user", content: "hi" }],
|
|
300
|
+
stream: false,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const promise = service.invokeCli(
|
|
304
|
+
req,
|
|
305
|
+
res,
|
|
306
|
+
body,
|
|
307
|
+
{ model: "test", offline: false },
|
|
308
|
+
false,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
312
|
+
|
|
313
|
+
child.stdout.emit(
|
|
314
|
+
"data",
|
|
315
|
+
Buffer.from(
|
|
316
|
+
'USER: hi\n\nASSISTANT: call:web_search{"query":"test"}',
|
|
317
|
+
"utf8",
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
child.emit("close", 0);
|
|
321
|
+
|
|
322
|
+
await promise;
|
|
323
|
+
|
|
324
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
325
|
+
expect.objectContaining({
|
|
326
|
+
choices: expect.arrayContaining([
|
|
327
|
+
expect.objectContaining({
|
|
328
|
+
message: expect.objectContaining({
|
|
329
|
+
tool_calls: expect.arrayContaining([
|
|
330
|
+
expect.objectContaining({
|
|
331
|
+
function: expect.objectContaining({ name: "web_search" }),
|
|
332
|
+
}),
|
|
333
|
+
]),
|
|
334
|
+
}),
|
|
335
|
+
}),
|
|
336
|
+
]),
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("throws error when binary is not executable", async () => {
|
|
342
|
+
const service = await getService();
|
|
343
|
+
statMock.mockResolvedValue({ isDirectory: () => true });
|
|
344
|
+
readdirMock.mockResolvedValue([
|
|
345
|
+
{ name: "test.llamafile", isFile: () => true },
|
|
346
|
+
]);
|
|
347
|
+
accessMock.mockRejectedValue(new Error("EACCES: permission denied"));
|
|
348
|
+
|
|
349
|
+
const req: any = { headers: {} };
|
|
350
|
+
const res: any = {};
|
|
351
|
+
|
|
352
|
+
await expect(
|
|
353
|
+
service.invokeCli(
|
|
354
|
+
req,
|
|
355
|
+
res,
|
|
356
|
+
{ model: "test", messages: [{ role: "user", content: "hi" }] },
|
|
357
|
+
{ model: "test", offline: false },
|
|
358
|
+
false,
|
|
359
|
+
),
|
|
360
|
+
).rejects.toThrow(/Binary not executable or not found/);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("cancels active requests and extracts request IDs", async () => {
|
|
364
|
+
const service = await getService();
|
|
365
|
+
|
|
366
|
+
// 1. cancelRequest
|
|
367
|
+
expect(service.cancelRequest("non-existent")).toBe(false);
|
|
368
|
+
|
|
369
|
+
const cancelFn = jest.fn(() => {
|
|
370
|
+
service.__getActiveRequests().delete("req-123");
|
|
371
|
+
});
|
|
372
|
+
service.__getActiveRequests().set("req-123", cancelFn);
|
|
373
|
+
expect(service.cancelRequest("req-123")).toBe(true);
|
|
374
|
+
expect(cancelFn).toHaveBeenCalled();
|
|
375
|
+
expect(service.__getActiveRequests().has("req-123")).toBe(false);
|
|
376
|
+
|
|
377
|
+
// 2. getLlamafileRequestId
|
|
378
|
+
const reqWithHeader = {
|
|
379
|
+
headers: { "x-shadowclaw-request-id": "header-id" },
|
|
380
|
+
} as any;
|
|
381
|
+
expect(service.getLlamafileRequestId(reqWithHeader)).toBe("header-id");
|
|
382
|
+
|
|
383
|
+
const reqWithBody = { headers: {} } as any;
|
|
384
|
+
const bodyWithId = { requestId: "body-id" };
|
|
385
|
+
expect(service.getLlamafileRequestId(reqWithBody, bodyWithId)).toBe(
|
|
386
|
+
"body-id",
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("handles invokeLlamafileServer forwarding", async () => {
|
|
391
|
+
const { invokeLlamafileServer } = await import("./llamafile-manager.js");
|
|
392
|
+
|
|
393
|
+
const req: any = { headers: {} };
|
|
394
|
+
const res: any = {
|
|
395
|
+
status: jest.fn().mockReturnThis(),
|
|
396
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
397
|
+
send: jest.fn().mockReturnThis(),
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// Mock fetch response for server error
|
|
401
|
+
(global.fetch as any) = jest.fn<any>().mockResolvedValue({
|
|
402
|
+
ok: false,
|
|
403
|
+
status: 502,
|
|
404
|
+
headers: new Headers({ "x-custom": "test" }),
|
|
405
|
+
text: async () => "Bad Gateway",
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
await invokeLlamafileServer(
|
|
409
|
+
req,
|
|
410
|
+
res,
|
|
411
|
+
{ model: "custom-model", messages: [] },
|
|
412
|
+
{ host: "127.0.0.1", port: 8080 },
|
|
413
|
+
false,
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
expect(res.status).toHaveBeenCalledWith(502);
|
|
417
|
+
expect(res.send).toHaveBeenCalledWith("Bad Gateway");
|
|
418
|
+
});
|
|
195
419
|
});
|
|
196
420
|
});
|