shadow-claw 1.27.0 → 1.28.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/.well-known/ai-catalog.json +11 -0
- package/.well-known/mcp/index.html +19 -0
- package/.well-known/mcp/server-card.json +52 -0
- package/.well-known/mcp/servers.json +15 -0
- package/.well-known/mcp.json +52 -0
- package/.well-known/oauth-protected-resource +10 -0
- package/.well-known/oauth-protected-resource.json +10 -0
- package/bin/assert-version-bump.mjs +61 -3
- package/bin/assert-version-bump.test.mjs +77 -1
- package/bin/build/build.mjs +116 -49
- package/bin/build/build.test.mjs +103 -0
- package/bin/cli.mjs +299 -237
- package/bin/cli.test.mjs +27 -0
- package/bin/commands/mcp.mjs +132 -2
- package/bin/commands/mcp.test.mjs +166 -0
- package/bin/commands/skills-index.mjs +139 -20
- package/bin/commands/skills-index.test.mjs +82 -0
- package/bin/utils/control-client.mjs +17 -0
- package/bin/utils/control-client.test.mjs +46 -0
- package/bin/version.mjs +208 -0
- package/bin/version.test.mjs +138 -0
- package/dist/electron/main.cjs +195 -185
- package/dist/lib/components/index.js +21 -21
- package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
- package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
- package/dist/lib/components/settings/shadow-claw-git.js +1 -1
- package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
- package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
- package/dist/lib/components/settings/shadow-claw-notifications.js +1 -1
- package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
- package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
- package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
- package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
- package/dist/lib/components/shadow-claw/shadow-claw.d.ts +1 -0
- package/dist/lib/components/shadow-claw/utils/setupAppLifecycle.d.ts +23 -0
- package/dist/lib/components/shadow-claw-channel-config.js +1 -1
- package/dist/lib/components/shadow-claw-channels.js +1 -1
- package/dist/lib/components/shadow-claw-chat.js +1 -1
- package/dist/lib/components/shadow-claw-control-plane.js +1 -1
- package/dist/lib/components/shadow-claw-conversations.js +1 -1
- package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
- package/dist/lib/components/shadow-claw-files.js +1 -1
- package/dist/lib/components/shadow-claw-git.js +1 -1
- package/dist/lib/components/shadow-claw-llm.js +1 -1
- package/dist/lib/components/shadow-claw-networking.js +1 -1
- package/dist/lib/components/shadow-claw-notifications.js +1 -1
- package/dist/lib/components/shadow-claw-pages.js +1 -1
- package/dist/lib/components/shadow-claw-peerjs.js +1 -1
- package/dist/lib/components/shadow-claw-settings.js +1 -1
- package/dist/lib/components/shadow-claw-storage.js +1 -1
- package/dist/lib/components/shadow-claw-task-server.js +1 -1
- package/dist/lib/components/shadow-claw-tasks.js +1 -1
- package/dist/lib/components/shadow-claw-terminal.js +1 -1
- package/dist/lib/components/shadow-claw-tools.js +1 -1
- package/dist/lib/components/shadow-claw-webvm.js +1 -1
- package/dist/lib/components/shadow-claw.js +1 -1
- package/dist/lib/{configurePeerJs-oODzltaC.js → configurePeerJs-D8Ph0-A9.js} +2 -2
- package/dist/lib/{configurePeerJs-oODzltaC.js.map → configurePeerJs-D8Ph0-A9.js.map} +1 -1
- package/dist/lib/core/control-plane-client.d.ts +2 -0
- package/dist/lib/core/orchestrator/orchestrator.d.ts +10 -0
- package/dist/lib/core/orchestrator/utils/operations/channel.d.ts +2 -2
- package/dist/lib/core/utils/initControlPlane.d.ts +6 -0
- package/dist/lib/index.js +21 -21
- package/dist/lib/{initControlPlane-C2zURVIS.js → initControlPlane-BIrWmpzc.js} +76 -9
- package/dist/lib/initControlPlane-BIrWmpzc.js.map +1 -0
- package/dist/lib/{markdown-C6EHl9zs.js → markdown-DEUoM1c6.js} +2 -2
- package/dist/lib/{markdown-C6EHl9zs.js.map → markdown-DEUoM1c6.js.map} +1 -1
- package/dist/lib/{orchestrator-KRjx0eba.js → orchestrator-BtVj9bBB.js} +11 -10
- package/dist/lib/{orchestrator-KRjx0eba.js.map → orchestrator-BtVj9bBB.js.map} +1 -1
- package/dist/lib/{orchestrator-CACLa_Ro.js → orchestrator-CFCW-l1O.js} +52 -4
- package/dist/lib/orchestrator-CFCW-l1O.js.map +1 -0
- package/dist/lib/{peerjs-DFWOBcXf.js → peerjs-BoBkx9DJ.js} +23 -3
- package/dist/lib/peerjs-BoBkx9DJ.js.map +1 -0
- package/dist/lib/{push-client-_tiWQsCT.js → push-client-D6hbzDNV.js} +10 -4
- package/dist/lib/push-client-D6hbzDNV.js.map +1 -0
- package/dist/lib/{shadow-claw-6UHdiy4a.js → shadow-claw-Cuf2O5bZ.js} +71 -4
- package/dist/lib/shadow-claw-Cuf2O5bZ.js.map +1 -0
- package/dist/lib/{shadow-claw-channel-config-DmfwJc6G.js → shadow-claw-channel-config-BDPKt6Bd.js} +3 -3
- package/dist/lib/{shadow-claw-channel-config-DmfwJc6G.js.map → shadow-claw-channel-config-BDPKt6Bd.js.map} +1 -1
- package/dist/lib/{shadow-claw-channels-1N6Jl-By.js → shadow-claw-channels-BsP5JABM.js} +2 -2
- package/dist/lib/{shadow-claw-channels-1N6Jl-By.js.map → shadow-claw-channels-BsP5JABM.js.map} +1 -1
- package/dist/lib/{shadow-claw-chat-BMoqQJRK.js → shadow-claw-chat-Cam2lX4k.js} +4 -4
- package/dist/lib/{shadow-claw-chat-BMoqQJRK.js.map → shadow-claw-chat-Cam2lX4k.js.map} +1 -1
- package/dist/lib/{shadow-claw-control-plane-UG0o7pCz.js → shadow-claw-control-plane-DB2qcCFs.js} +20 -10
- package/dist/lib/shadow-claw-control-plane-DB2qcCFs.js.map +1 -0
- package/dist/lib/{shadow-claw-conversations-BwXep3iu.js → shadow-claw-conversations-C9Vf4XGx.js} +2 -2
- package/dist/lib/{shadow-claw-conversations-BwXep3iu.js.map → shadow-claw-conversations-C9Vf4XGx.js.map} +1 -1
- package/dist/lib/{shadow-claw-file-viewer-DUxzVxDT.js → shadow-claw-file-viewer-B4HYiI0v.js} +3 -3
- package/dist/lib/{shadow-claw-file-viewer-DUxzVxDT.js.map → shadow-claw-file-viewer-B4HYiI0v.js.map} +1 -1
- package/dist/lib/{shadow-claw-files-DIREEnT2.js → shadow-claw-files-DVROUESO.js} +2 -2
- package/dist/lib/{shadow-claw-files-DIREEnT2.js.map → shadow-claw-files-DVROUESO.js.map} +1 -1
- package/dist/lib/{shadow-claw-git-Bg1SWgsF.js → shadow-claw-git-BqrO7cHJ.js} +3 -3
- package/dist/lib/{shadow-claw-git-Bg1SWgsF.js.map → shadow-claw-git-BqrO7cHJ.js.map} +1 -1
- package/dist/lib/{shadow-claw-llm-CazBrVpM.js → shadow-claw-llm-BexUQdYo.js} +2 -2
- package/dist/lib/{shadow-claw-llm-CazBrVpM.js.map → shadow-claw-llm-BexUQdYo.js.map} +1 -1
- package/dist/lib/{shadow-claw-networking-Df85tCbO.js → shadow-claw-networking-CfXRGpUD.js} +2 -2
- package/dist/lib/{shadow-claw-networking-Df85tCbO.js.map → shadow-claw-networking-CfXRGpUD.js.map} +1 -1
- package/dist/lib/{shadow-claw-notifications-v62FryTc.js → shadow-claw-notifications-C9KPMgx_.js} +2 -2
- package/dist/lib/{shadow-claw-notifications-v62FryTc.js.map → shadow-claw-notifications-C9KPMgx_.js.map} +1 -1
- package/dist/lib/{shadow-claw-pages-BgPyIEZO.js → shadow-claw-pages-CGzGXmrI.js} +3 -3
- package/dist/lib/{shadow-claw-pages-BgPyIEZO.js.map → shadow-claw-pages-CGzGXmrI.js.map} +1 -1
- package/dist/lib/{shadow-claw-peerjs-xkBXILed.js → shadow-claw-peerjs-B0ZB7yhB.js} +3 -3
- package/dist/lib/{shadow-claw-peerjs-xkBXILed.js.map → shadow-claw-peerjs-B0ZB7yhB.js.map} +1 -1
- package/dist/lib/{shadow-claw-settings-CN89xRVU.js → shadow-claw-settings-DBfMZdDS.js} +2 -2
- package/dist/lib/{shadow-claw-settings-CN89xRVU.js.map → shadow-claw-settings-DBfMZdDS.js.map} +1 -1
- package/dist/lib/{shadow-claw-storage-VnhcObCe.js → shadow-claw-storage-CM-w6mgG.js} +2 -2
- package/dist/lib/{shadow-claw-storage-VnhcObCe.js.map → shadow-claw-storage-CM-w6mgG.js.map} +1 -1
- package/dist/lib/{shadow-claw-task-server-jgEC2hHB.js → shadow-claw-task-server-DV4b1g2-.js} +4 -2
- package/dist/lib/{shadow-claw-task-server-jgEC2hHB.js.map → shadow-claw-task-server-DV4b1g2-.js.map} +1 -1
- package/dist/lib/{shadow-claw-tasks-i_evIufy.js → shadow-claw-tasks-K-foOHSE.js} +3 -3
- package/dist/lib/{shadow-claw-tasks-i_evIufy.js.map → shadow-claw-tasks-K-foOHSE.js.map} +1 -1
- package/dist/lib/{shadow-claw-terminal-CwScROlu.js → shadow-claw-terminal-CpAknKyA.js} +2 -2
- package/dist/lib/{shadow-claw-terminal-CwScROlu.js.map → shadow-claw-terminal-CpAknKyA.js.map} +1 -1
- package/dist/lib/{shadow-claw-tools-DD3fyXP4.js → shadow-claw-tools-D98XZ0Ep.js} +3 -3
- package/dist/lib/{shadow-claw-tools-DD3fyXP4.js.map → shadow-claw-tools-D98XZ0Ep.js.map} +1 -1
- package/dist/lib/{shadow-claw-webvm-DbJq4I3-.js → shadow-claw-webvm-DLOb8dMI.js} +2 -2
- package/dist/lib/{shadow-claw-webvm-DbJq4I3-.js.map → shadow-claw-webvm-DLOb8dMI.js.map} +1 -1
- package/dist/lib/subsystems/channels/imessage.d.ts +1 -0
- package/dist/lib/subsystems/channels/peerjs.d.ts +1 -0
- package/dist/lib/subsystems/channels/telegram.d.ts +1 -0
- package/dist/lib/subsystems/channels/types.d.ts +2 -0
- package/dist/lib/subsystems/notifications/push-client.d.ts +1 -1
- package/dist/lib/{syncWebMcpRegistration-DNLnQ-jz.js → syncWebMcpRegistration-NspKefOI.js} +2 -2
- package/dist/lib/{syncWebMcpRegistration-DNLnQ-jz.js.map → syncWebMcpRegistration-NspKefOI.js.map} +1 -1
- package/dist/public/.well-known/ai-catalog.json +11 -0
- package/dist/public/.well-known/mcp/index.html +19 -0
- package/dist/public/.well-known/mcp/server-card.json +52 -0
- package/dist/public/.well-known/mcp/servers.json +15 -0
- package/dist/public/.well-known/mcp.json +52 -0
- package/dist/public/.well-known/oauth-protected-resource +10 -0
- package/dist/public/.well-known/oauth-protected-resource.json +10 -0
- package/dist/public/404.html +28 -1
- package/dist/public/AGENTS.md +1 -1
- package/dist/public/agent.worker.js +1 -1
- package/dist/public/{configurePeerJs-BqgeBjP7.js → configurePeerJs-BhBokQlC.js} +1 -1
- package/dist/public/docs/example/article/index.html +3 -3
- package/dist/public/docs/publishing/index.html +3 -3
- package/dist/public/docs/skill-creator/index.html +3 -3
- package/dist/public/docs/subsystems/cli.md +11 -8
- package/dist/public/docs/subsystems/mcp-server.md +1 -0
- package/dist/public/docs/subsystems/notifications.md +10 -0
- package/dist/public/{e2e-bridge-Ceqr5kGc.js → e2e-bridge-DcBoeFvd.js} +1 -1
- package/dist/public/{file-viewer-C-6Jc_tb.js → file-viewer-CKtVC9Ix.js} +1 -1
- package/dist/public/index.css +1 -1
- package/dist/public/index.html +3 -3
- package/dist/public/index.js +1 -1
- package/dist/public/initControlPlane-Bi8ouFEb.js +1 -0
- package/dist/public/llms.txt +36 -1
- package/dist/public/main/index.html +3 -3
- package/dist/public/main/memory/index.html +3 -3
- package/dist/public/{markdown-C3M4OW_A.js → markdown-C-hCVJtc.js} +1 -1
- package/dist/public/orchestrator-8qNGfw8r.js +15 -0
- package/dist/public/{orchestrator-DHjdLtqa.js → orchestrator-BT3Ks5ip.js} +2 -2
- package/dist/public/peerjs-DfhwEiiq.js +1 -0
- package/dist/public/push-client-BVLrE9pK.js +1 -0
- package/dist/public/service-worker/fetch-proxy.js +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-EAt2ml5Y.js → shadow-claw-DCjxNDCX.js} +3 -3
- package/dist/public/{shadow-claw-channels-XsfuWFF8.js → shadow-claw-channels-CITDrqMl.js} +1 -1
- package/dist/public/{shadow-claw-chat-CbSaOZ4q.js → shadow-claw-chat-BQrSgSKF.js} +1 -1
- package/dist/public/{shadow-claw-control-plane-DHvN3Ukg.js → shadow-claw-control-plane-Cp001OY0.js} +2 -2
- package/dist/public/{shadow-claw-conversations-BWs5VVfs.js → shadow-claw-conversations-EhX4RvIj.js} +1 -1
- package/dist/public/{shadow-claw-file-viewer-BWsoW5QS.js → shadow-claw-file-viewer-DiQXIjZD.js} +1 -1
- package/dist/public/{shadow-claw-files-CG7MqWiV.js → shadow-claw-files-lkTnSJsn.js} +1 -1
- package/dist/public/{shadow-claw-git-DBRPZNav.js → shadow-claw-git-BlcA_z8x.js} +2 -2
- package/dist/public/{shadow-claw-llm-DMvLgKXo.js → shadow-claw-llm-CW16X9Rg.js} +1 -1
- package/dist/public/{shadow-claw-networking-Dd8Rw31D.js → shadow-claw-networking-ySsMBY_i.js} +1 -1
- package/dist/public/{shadow-claw-notifications-Dbevj-lq.js → shadow-claw-notifications-CyV2zCQ8.js} +1 -1
- package/dist/public/{shadow-claw-pages-3_lQAv0t.js → shadow-claw-pages-Cx7cF9ah.js} +1 -1
- package/dist/public/{shadow-claw-settings-t8TIlgrM.js → shadow-claw-settings-zm4PHHGi.js} +2 -2
- package/dist/public/{shadow-claw-storage-CfEXoSIE.js → shadow-claw-storage-dy9JAEGd.js} +1 -1
- package/dist/public/{shadow-claw-task-server-DxQSsgRZ.js → shadow-claw-task-server-Bleww48s.js} +2 -2
- package/dist/public/{shadow-claw-tasks-DNpicQwz.js → shadow-claw-tasks-CTI5oaLN.js} +1 -1
- package/dist/public/{shadow-claw-terminal-DpY6zU_r.js → shadow-claw-terminal-mZ3i4HaQ.js} +1 -1
- package/dist/public/{shadow-claw-tools-WSwWohnk.js → shadow-claw-tools-B_nuk8Zl.js} +1 -1
- package/dist/public/{shadow-claw-webvm-CP6oFjg5.js → shadow-claw-webvm-Vl_jH5YL.js} +1 -1
- package/dist/public/{syncWebMcpRegistration-DOn9gLnD.js → syncWebMcpRegistration-n2vAMJ4w.js} +1 -1
- package/dist/server.js +197 -187
- package/package.json +2 -1
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +6 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +19 -9
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +14 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +12 -0
- package/src/components/shadow-claw/shadow-claw.ts +6 -0
- package/src/components/shadow-claw/utils/bindEventListeners.test.ts +57 -0
- package/src/components/shadow-claw/utils/bindEventListeners.ts +14 -0
- package/src/components/shadow-claw/utils/setupAppLifecycle.test.ts +201 -0
- package/src/components/shadow-claw/utils/setupAppLifecycle.ts +101 -0
- package/src/core/control-plane-client.test.ts +82 -1
- package/src/core/control-plane-client.ts +35 -1
- package/src/core/orchestrator/orchestrator.test.ts +49 -0
- package/src/core/orchestrator/orchestrator.ts +43 -0
- package/src/core/orchestrator/utils/operations/channel.test.ts +17 -0
- package/src/core/orchestrator/utils/operations/channel.ts +11 -5
- package/src/core/orchestrator/utils/operations/task.test.ts +26 -0
- package/src/core/orchestrator/utils/operations/task.ts +8 -1
- package/src/core/utils/initControlPlane.test.ts +138 -0
- package/src/core/utils/initControlPlane.ts +82 -0
- package/src/core/utils/initializeApp.test.ts +27 -1
- package/src/core/utils/initializeApp.ts +6 -13
- package/src/server/mcp/mcp-server.test.ts +6 -0
- package/src/server/mcp/mcp-server.ts +2 -1
- package/src/server/mcp/tools/built-in-tools.ts +88 -1
- package/src/server/mcp/tools/client-tool-relay.ts +6 -0
- package/src/server/mcp/tools/mcp-tools.test.ts +128 -0
- package/src/server/openapi/openapi-coverage.test.ts +2 -1
- package/src/server/openapi/openapi-spec.ts +2 -1
- package/src/server/openapi/paths/mcp.ts +104 -0
- package/src/server/openapi/paths/push.ts +3 -0
- package/src/server/routes/mcp.test.ts +92 -0
- package/src/server/routes/mcp.ts +138 -2
- package/src/server/utils/packageVersion.test.ts +10 -0
- package/src/server/utils/packageVersion.ts +32 -0
- package/src/service-worker/fetch-proxy-rules.test.ts +73 -0
- package/src/service-worker/fetch-proxy-rules.ts +44 -0
- package/src/service-worker/fetch-proxy.ts +1 -1
- package/src/subsystems/channels/imessage.test.ts +36 -0
- package/src/subsystems/channels/imessage.ts +11 -0
- package/src/subsystems/channels/peerjs.test.ts +56 -0
- package/src/subsystems/channels/peerjs.ts +29 -0
- package/src/subsystems/channels/telegram.test.ts +38 -0
- package/src/subsystems/channels/telegram.ts +13 -0
- package/src/subsystems/channels/types.ts +2 -0
- package/src/subsystems/notifications/push-client.test.ts +70 -0
- package/src/subsystems/notifications/push-client.ts +28 -3
- package/src/subsystems/notifications/push-routes.test.ts +82 -0
- package/src/subsystems/notifications/push-routes.ts +70 -16
- package/src/subsystems/notifications/push-store.test.ts +59 -0
- package/src/subsystems/notifications/push-store.ts +113 -19
- package/src/testing/jest-setup.ts +15 -0
- package/dist/lib/initControlPlane-C2zURVIS.js.map +0 -1
- package/dist/lib/orchestrator-CACLa_Ro.js.map +0 -1
- package/dist/lib/peerjs-DFWOBcXf.js.map +0 -1
- package/dist/lib/push-client-_tiWQsCT.js.map +0 -1
- package/dist/lib/shadow-claw-6UHdiy4a.js.map +0 -1
- package/dist/lib/shadow-claw-control-plane-UG0o7pCz.js.map +0 -1
- package/dist/public/initControlPlane-BN9cWyO0.js +0 -1
- package/dist/public/orchestrator-CMbCTjBf.js +0 -15
- package/dist/public/peerjs-DUCKH-m9.js +0 -1
- package/dist/public/push-client-D0lkwrK0.js +0 -1
package/bin/cli.test.mjs
CHANGED
|
@@ -60,6 +60,12 @@ describe("shadow-claw CLI", () => {
|
|
|
60
60
|
"--help",
|
|
61
61
|
]);
|
|
62
62
|
expect(stdout).toContain("Usage: shadow-claw");
|
|
63
|
+
expect(stdout).toContain("Development:");
|
|
64
|
+
expect(stdout).toContain("Server:");
|
|
65
|
+
expect(stdout).toContain("Control Plane & Client Bridge:");
|
|
66
|
+
expect(stdout).toContain("WebRTC:");
|
|
67
|
+
expect(stdout).toContain("Project:");
|
|
68
|
+
expect(stdout).toContain("Help & Utility:");
|
|
63
69
|
expect(stdout).toContain("build");
|
|
64
70
|
expect(stdout).toContain("dev");
|
|
65
71
|
expect(stdout).toContain("run");
|
|
@@ -75,6 +81,27 @@ describe("shadow-claw CLI", () => {
|
|
|
75
81
|
expect(stdout).toContain("mcp");
|
|
76
82
|
});
|
|
77
83
|
|
|
84
|
+
it("groups subcommand options into logical categories in help", async () => {
|
|
85
|
+
const { stdout: devHelp } = await execFileAsync(process.execPath, [
|
|
86
|
+
cliPath,
|
|
87
|
+
"dev",
|
|
88
|
+
"--help",
|
|
89
|
+
]);
|
|
90
|
+
expect(devHelp).toContain("Server & Network Options:");
|
|
91
|
+
expect(devHelp).toContain("Site & Build Options:");
|
|
92
|
+
expect(devHelp).toContain("Security & Proxy Options:");
|
|
93
|
+
expect(devHelp).toContain("TLS / HTTPS Options:");
|
|
94
|
+
expect(devHelp).toContain("Storage & Database Options:");
|
|
95
|
+
|
|
96
|
+
const { stdout: webrtcHelp } = await execFileAsync(process.execPath, [
|
|
97
|
+
cliPath,
|
|
98
|
+
"webrtc",
|
|
99
|
+
"--help",
|
|
100
|
+
]);
|
|
101
|
+
expect(webrtcHelp).toContain("Identity Options:");
|
|
102
|
+
expect(webrtcHelp).toContain("Signaling & Listener Options");
|
|
103
|
+
});
|
|
104
|
+
|
|
78
105
|
it("outputs server command help with server --help and aliases", async () => {
|
|
79
106
|
const { stdout: outServer } = await execFileAsync(process.execPath, [
|
|
80
107
|
cliPath,
|
package/bin/commands/mcp.mjs
CHANGED
|
@@ -8,8 +8,23 @@
|
|
|
8
8
|
|
|
9
9
|
import readline from "node:readline";
|
|
10
10
|
import http from "node:http";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
11
14
|
import { CliControlClient } from "../utils/control-client.mjs";
|
|
12
15
|
|
|
16
|
+
const __mcp_dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
function getCliVersion() {
|
|
18
|
+
try {
|
|
19
|
+
const pkgPath = path.resolve(__mcp_dirname, "../../package.json");
|
|
20
|
+
if (fs.existsSync(pkgPath)) {
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
22
|
+
if (pkg.version) return pkg.version;
|
|
23
|
+
}
|
|
24
|
+
} catch {}
|
|
25
|
+
return "1.27.1";
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
export const CLI_BUILTIN_TOOLS = [
|
|
14
29
|
{
|
|
15
30
|
name: "shadowclaw_list_clients",
|
|
@@ -105,6 +120,30 @@ export const CLI_BUILTIN_TOOLS = [
|
|
|
105
120
|
},
|
|
106
121
|
},
|
|
107
122
|
},
|
|
123
|
+
{
|
|
124
|
+
name: "shadowclaw_send_notification",
|
|
125
|
+
description:
|
|
126
|
+
"Broadcast an OS-level push notification to subscribed devices via Web Push (VAPID), or send to a specific registered client. Works even when the client browser tab is closed, asleep, or running in the background.",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
title: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "Notification title (default: 'ShadowClaw').",
|
|
133
|
+
},
|
|
134
|
+
body: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "The notification body message text.",
|
|
137
|
+
},
|
|
138
|
+
clientId: {
|
|
139
|
+
type: "string",
|
|
140
|
+
description:
|
|
141
|
+
"Target client ID, prefix, or device label of a specific client that has registered in the past. If omitted, broadcasts to all subscribed devices.",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
required: ["body"],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
108
147
|
];
|
|
109
148
|
|
|
110
149
|
export function createCliMcpEngine(options = {}) {
|
|
@@ -115,7 +154,7 @@ export function createCliMcpEngine(options = {}) {
|
|
|
115
154
|
|
|
116
155
|
const serverInfo = {
|
|
117
156
|
name: "shadow-claw",
|
|
118
|
-
version:
|
|
157
|
+
version: options.version || getCliVersion(),
|
|
119
158
|
};
|
|
120
159
|
|
|
121
160
|
const capabilities = {
|
|
@@ -193,6 +232,12 @@ export function createCliMcpEngine(options = {}) {
|
|
|
193
232
|
Array.isArray(res.data.tools)
|
|
194
233
|
) {
|
|
195
234
|
for (const t of res.data.tools) {
|
|
235
|
+
if (
|
|
236
|
+
t.name === "send_notification" ||
|
|
237
|
+
t.name.startsWith("shadowclaw_")
|
|
238
|
+
) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
196
241
|
if (!toolDefMap.has(t.name)) {
|
|
197
242
|
toolDefMap.set(t.name, t);
|
|
198
243
|
toolSupportingClientsMap.set(t.name, []);
|
|
@@ -316,7 +361,9 @@ export function createCliMcpEngine(options = {}) {
|
|
|
316
361
|
combined.set(t.name, t);
|
|
317
362
|
}
|
|
318
363
|
for (const t of relayed) {
|
|
319
|
-
combined.
|
|
364
|
+
if (!combined.has(t.name)) {
|
|
365
|
+
combined.set(t.name, t);
|
|
366
|
+
}
|
|
320
367
|
}
|
|
321
368
|
|
|
322
369
|
const sortedTools = Array.from(combined.values()).sort((a, b) =>
|
|
@@ -541,6 +588,89 @@ export function createCliMcpEngine(options = {}) {
|
|
|
541
588
|
};
|
|
542
589
|
}
|
|
543
590
|
|
|
591
|
+
if (
|
|
592
|
+
toolName === "shadowclaw_send_notification" ||
|
|
593
|
+
toolName === "send_notification"
|
|
594
|
+
) {
|
|
595
|
+
const body = String(args.body || "").trim();
|
|
596
|
+
if (!body) {
|
|
597
|
+
return {
|
|
598
|
+
jsonrpc: "2.0",
|
|
599
|
+
id: reqId,
|
|
600
|
+
result: {
|
|
601
|
+
resultType: "complete",
|
|
602
|
+
isError: true,
|
|
603
|
+
content: [
|
|
604
|
+
{
|
|
605
|
+
type: "text",
|
|
606
|
+
text: "Error: Notification 'body' parameter cannot be empty.",
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
_meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const title = String(args.title || "ShadowClaw").trim();
|
|
615
|
+
const targetClientId =
|
|
616
|
+
typeof args.clientId === "string" && args.clientId.trim()
|
|
617
|
+
? args.clientId.trim()
|
|
618
|
+
: undefined;
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
const res = await client.broadcastNotification({
|
|
622
|
+
title,
|
|
623
|
+
body,
|
|
624
|
+
...(targetClientId ? { clientId: targetClientId } : {}),
|
|
625
|
+
});
|
|
626
|
+
const sentCount = res?.sent ?? 0;
|
|
627
|
+
const failedCount = res?.failed ?? 0;
|
|
628
|
+
let msg;
|
|
629
|
+
|
|
630
|
+
if (res?.notFound) {
|
|
631
|
+
msg = `Warning: No push subscriptions found for client '${targetClientId}'. Ensure push notifications were registered by this client in ShadowClaw Settings.`;
|
|
632
|
+
} else if (res?.noSubscribers) {
|
|
633
|
+
msg =
|
|
634
|
+
"Warning: Push notification broadcast completed, but no devices are currently subscribed to push notifications. Ensure push notifications are enabled in ShadowClaw Settings on the client first.";
|
|
635
|
+
} else if (targetClientId) {
|
|
636
|
+
msg = `Push notification sent to client '${targetClientId}': ${sentCount} recipient(s) delivered, ${failedCount} failed.`;
|
|
637
|
+
} else {
|
|
638
|
+
msg = `Push notification broadcast sent: ${sentCount} recipient(s) delivered, ${failedCount} failed.`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return {
|
|
642
|
+
jsonrpc: "2.0",
|
|
643
|
+
id: reqId,
|
|
644
|
+
result: {
|
|
645
|
+
resultType: "complete",
|
|
646
|
+
isError: false,
|
|
647
|
+
content: [{ type: "text", text: msg }],
|
|
648
|
+
_meta: {
|
|
649
|
+
...res,
|
|
650
|
+
...(targetClientId ? { targetClientId } : {}),
|
|
651
|
+
"io.modelcontextprotocol/serverInfo": serverInfo,
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
};
|
|
655
|
+
} catch (err) {
|
|
656
|
+
return {
|
|
657
|
+
jsonrpc: "2.0",
|
|
658
|
+
id: reqId,
|
|
659
|
+
result: {
|
|
660
|
+
resultType: "complete",
|
|
661
|
+
isError: true,
|
|
662
|
+
content: [
|
|
663
|
+
{
|
|
664
|
+
type: "text",
|
|
665
|
+
text: `Failed to send push notification: ${err.message}`,
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
_meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
544
674
|
// Relayed interactive ask_user tool with MRTR response fulfillment if provided
|
|
545
675
|
if (
|
|
546
676
|
toolName === "ask_user" &&
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
1
4
|
import {
|
|
2
5
|
describe,
|
|
3
6
|
it,
|
|
@@ -8,6 +11,11 @@ import {
|
|
|
8
11
|
} from "@jest/globals";
|
|
9
12
|
import { createCliMcpEngine } from "./mcp.mjs";
|
|
10
13
|
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const pkg = JSON.parse(
|
|
16
|
+
fs.readFileSync(path.resolve(__dirname, "../../package.json"), "utf8"),
|
|
17
|
+
);
|
|
18
|
+
|
|
11
19
|
describe("CLI MCP Engine (createCliMcpEngine)", () => {
|
|
12
20
|
let errorSpy;
|
|
13
21
|
|
|
@@ -41,6 +49,7 @@ describe("CLI MCP Engine (createCliMcpEngine)", () => {
|
|
|
41
49
|
expect(res.id).toBe(1);
|
|
42
50
|
expect(res.result.protocolVersion).toBe("2026-07-28");
|
|
43
51
|
expect(res.result.serverInfo.name).toBe("shadow-claw");
|
|
52
|
+
expect(res.result.serverInfo.version).toBe(pkg.version);
|
|
44
53
|
});
|
|
45
54
|
|
|
46
55
|
it("processes tools/list including built-in and client tools", async () => {
|
|
@@ -82,7 +91,164 @@ describe("CLI MCP Engine (createCliMcpEngine)", () => {
|
|
|
82
91
|
const toolNames = res.result.tools.map((t) => t.name);
|
|
83
92
|
expect(toolNames).toContain("shadowclaw_list_clients");
|
|
84
93
|
expect(toolNames).toContain("shadowclaw_send_message");
|
|
94
|
+
expect(toolNames).toContain("shadowclaw_send_notification");
|
|
85
95
|
expect(toolNames).toContain("read_file");
|
|
96
|
+
|
|
97
|
+
const notifTool = res.result.tools.find(
|
|
98
|
+
(t) => t.name === "shadowclaw_send_notification",
|
|
99
|
+
);
|
|
100
|
+
expect(notifTool.inputSchema.properties).toHaveProperty("clientId");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("processes tools/call for shadowclaw_send_notification directly via broadcastNotification", async () => {
|
|
104
|
+
const mockClient = {
|
|
105
|
+
listClients: jest.fn().mockResolvedValue([]),
|
|
106
|
+
broadcastNotification: jest.fn().mockResolvedValue({
|
|
107
|
+
sent: 2,
|
|
108
|
+
failed: 0,
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const engine = createCliMcpEngine({ client: mockClient });
|
|
113
|
+
const res = await engine.handleMessage({
|
|
114
|
+
jsonrpc: "2.0",
|
|
115
|
+
id: 30,
|
|
116
|
+
method: "tools/call",
|
|
117
|
+
params: {
|
|
118
|
+
name: "shadowclaw_send_notification",
|
|
119
|
+
arguments: { title: "Custom Alert", body: "Server push message" },
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(res).toBeDefined();
|
|
124
|
+
expect(res.result.resultType).toBe("complete");
|
|
125
|
+
expect(res.result.isError).toBe(false);
|
|
126
|
+
expect(res.result.content[0].text).toContain(
|
|
127
|
+
"2 recipient(s) delivered, 0 failed",
|
|
128
|
+
);
|
|
129
|
+
expect(mockClient.broadcastNotification).toHaveBeenCalledWith({
|
|
130
|
+
title: "Custom Alert",
|
|
131
|
+
body: "Server push message",
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("processes tools/call for send_notification alias directly via broadcastNotification", async () => {
|
|
136
|
+
const mockClient = {
|
|
137
|
+
listClients: jest.fn().mockResolvedValue([]),
|
|
138
|
+
broadcastNotification: jest.fn().mockResolvedValue({
|
|
139
|
+
sent: 1,
|
|
140
|
+
failed: 0,
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const engine = createCliMcpEngine({ client: mockClient });
|
|
145
|
+
const res = await engine.handleMessage({
|
|
146
|
+
jsonrpc: "2.0",
|
|
147
|
+
id: 31,
|
|
148
|
+
method: "tools/call",
|
|
149
|
+
params: {
|
|
150
|
+
name: "send_notification",
|
|
151
|
+
arguments: { body: "Fallback push" },
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(res).toBeDefined();
|
|
156
|
+
expect(res.result.resultType).toBe("complete");
|
|
157
|
+
expect(res.result.isError).toBe(false);
|
|
158
|
+
expect(mockClient.broadcastNotification).toHaveBeenCalledWith({
|
|
159
|
+
title: "ShadowClaw",
|
|
160
|
+
body: "Fallback push",
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("handles empty body validation for shadowclaw_send_notification", async () => {
|
|
165
|
+
const mockClient = {
|
|
166
|
+
listClients: jest.fn().mockResolvedValue([]),
|
|
167
|
+
broadcastNotification: jest.fn(),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const engine = createCliMcpEngine({ client: mockClient });
|
|
171
|
+
const res = await engine.handleMessage({
|
|
172
|
+
jsonrpc: "2.0",
|
|
173
|
+
id: 32,
|
|
174
|
+
method: "tools/call",
|
|
175
|
+
params: {
|
|
176
|
+
name: "shadowclaw_send_notification",
|
|
177
|
+
arguments: { body: "" },
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(res).toBeDefined();
|
|
182
|
+
expect(res.result.isError).toBe(true);
|
|
183
|
+
expect(res.result.content[0].text).toContain("cannot be empty");
|
|
184
|
+
expect(mockClient.broadcastNotification).not.toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("targets a specific client in shadowclaw_send_notification", async () => {
|
|
188
|
+
const mockClient = {
|
|
189
|
+
listClients: jest.fn().mockResolvedValue([]),
|
|
190
|
+
broadcastNotification: jest.fn().mockResolvedValue({
|
|
191
|
+
sent: 1,
|
|
192
|
+
failed: 0,
|
|
193
|
+
}),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const engine = createCliMcpEngine({ client: mockClient });
|
|
197
|
+
const res = await engine.handleMessage({
|
|
198
|
+
jsonrpc: "2.0",
|
|
199
|
+
id: 33,
|
|
200
|
+
method: "tools/call",
|
|
201
|
+
params: {
|
|
202
|
+
name: "shadowclaw_send_notification",
|
|
203
|
+
arguments: {
|
|
204
|
+
title: "Direct Alert",
|
|
205
|
+
body: "Direct push message",
|
|
206
|
+
clientId: "client-target-99",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(res).toBeDefined();
|
|
212
|
+
expect(res.result.isError).toBe(false);
|
|
213
|
+
expect(res.result.content[0].text).toContain(
|
|
214
|
+
"Push notification sent to client 'client-target-99': 1 recipient(s) delivered, 0 failed.",
|
|
215
|
+
);
|
|
216
|
+
expect(mockClient.broadcastNotification).toHaveBeenCalledWith({
|
|
217
|
+
title: "Direct Alert",
|
|
218
|
+
body: "Direct push message",
|
|
219
|
+
clientId: "client-target-99",
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("handles notFound warning when targeted client has no push subscriptions", async () => {
|
|
224
|
+
const mockClient = {
|
|
225
|
+
listClients: jest.fn().mockResolvedValue([]),
|
|
226
|
+
broadcastNotification: jest.fn().mockResolvedValue({
|
|
227
|
+
sent: 0,
|
|
228
|
+
failed: 0,
|
|
229
|
+
notFound: true,
|
|
230
|
+
}),
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const engine = createCliMcpEngine({ client: mockClient });
|
|
234
|
+
const res = await engine.handleMessage({
|
|
235
|
+
jsonrpc: "2.0",
|
|
236
|
+
id: 34,
|
|
237
|
+
method: "tools/call",
|
|
238
|
+
params: {
|
|
239
|
+
name: "shadowclaw_send_notification",
|
|
240
|
+
arguments: {
|
|
241
|
+
body: "Alert",
|
|
242
|
+
clientId: "missing-client-id",
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(res).toBeDefined();
|
|
248
|
+
expect(res.result.isError).toBe(false);
|
|
249
|
+
expect(res.result.content[0].text).toContain(
|
|
250
|
+
"No push subscriptions found for client 'missing-client-id'",
|
|
251
|
+
);
|
|
86
252
|
});
|
|
87
253
|
|
|
88
254
|
it("processes tools/call for shadowclaw_send_message", async () => {
|
|
@@ -8,8 +8,12 @@ import crypto from "node:crypto";
|
|
|
8
8
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import process from "node:process";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
11
12
|
import matter from "gray-matter";
|
|
12
13
|
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* Computes a standardized sha256:{hex} digest from Buffer or string.
|
|
15
19
|
*/
|
|
@@ -64,30 +68,86 @@ export async function generateSkillsIndex(
|
|
|
64
68
|
const wellKnownDirRel = options.outDir || ".well-known/agent-skills";
|
|
65
69
|
|
|
66
70
|
// 1. Read metadata from shadow-claw.config.json or package.json
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
const metadataRoot = options.metadataRoot
|
|
72
|
+
? path.resolve(options.metadataRoot)
|
|
73
|
+
: resolvedRoot;
|
|
74
|
+
|
|
75
|
+
let siteName = options.siteName;
|
|
76
|
+
let siteDescription = options.siteDescription;
|
|
77
|
+
|
|
78
|
+
const parent1 = path.dirname(metadataRoot);
|
|
79
|
+
const parent2 = path.dirname(parent1);
|
|
80
|
+
|
|
81
|
+
const configCandidates = [
|
|
82
|
+
options.configPath,
|
|
83
|
+
path.join(metadataRoot, "shadow-claw.config.json"),
|
|
84
|
+
path.join(metadataRoot, "shadow-claw-config.json"),
|
|
85
|
+
path.join(metadataRoot, "site-config.json"),
|
|
86
|
+
path.join(parent1, "shadow-claw.config.json"),
|
|
87
|
+
path.join(parent1, "shadow-claw-config.json"),
|
|
88
|
+
path.join(parent1, "site-config.json"),
|
|
89
|
+
path.join(parent2, "shadow-claw.config.json"),
|
|
90
|
+
path.join(parent2, "shadow-claw-config.json"),
|
|
91
|
+
path.join(parent2, "site-config.json"),
|
|
92
|
+
].filter(Boolean);
|
|
93
|
+
|
|
94
|
+
let loadedConfig = null;
|
|
95
|
+
for (const candidate of configCandidates) {
|
|
96
|
+
if (await pathExists(candidate)) {
|
|
82
97
|
try {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (pkg.description) siteDescription = pkg.description;
|
|
98
|
+
const configStr = await readFile(candidate, "utf8");
|
|
99
|
+
loadedConfig = JSON.parse(configStr);
|
|
100
|
+
break;
|
|
87
101
|
} catch {}
|
|
88
102
|
}
|
|
89
103
|
}
|
|
90
104
|
|
|
105
|
+
if (loadedConfig) {
|
|
106
|
+
if (!siteName && loadedConfig.site?.title)
|
|
107
|
+
siteName = loadedConfig.site.title;
|
|
108
|
+
if (!siteDescription && loadedConfig.site?.description) {
|
|
109
|
+
siteDescription = loadedConfig.site.description;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!siteName || !siteDescription) {
|
|
114
|
+
const pkgCandidates = [
|
|
115
|
+
path.join(metadataRoot, "package.json"),
|
|
116
|
+
path.join(parent1, "package.json"),
|
|
117
|
+
path.join(parent2, "package.json"),
|
|
118
|
+
];
|
|
119
|
+
for (const pkgPath of pkgCandidates) {
|
|
120
|
+
if (await pathExists(pkgPath)) {
|
|
121
|
+
try {
|
|
122
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
123
|
+
if (!siteName && pkg.name) siteName = pkg.name;
|
|
124
|
+
if (!siteDescription && pkg.description) {
|
|
125
|
+
siteDescription = pkg.description;
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!siteName) {
|
|
134
|
+
siteName = path.basename(metadataRoot);
|
|
135
|
+
if (siteName === "public" || siteName === "dist") {
|
|
136
|
+
siteName = path.basename(parent1);
|
|
137
|
+
if (
|
|
138
|
+
(siteName === "public" || siteName === "dist") &&
|
|
139
|
+
path.basename(parent2)
|
|
140
|
+
) {
|
|
141
|
+
siteName = path.basename(parent2);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!siteDescription) {
|
|
147
|
+
siteDescription =
|
|
148
|
+
"Agent skills and tools collection, powered by ShadowClaw.";
|
|
149
|
+
}
|
|
150
|
+
|
|
91
151
|
// 2. Discover Tools (.agents/tools/**/*.json)
|
|
92
152
|
const toolsDir = path.join(resolvedRoot, ".agents", "tools");
|
|
93
153
|
const toolFiles = await findFilesRecursively(toolsDir, (name) =>
|
|
@@ -249,6 +309,61 @@ export async function generateSkillsIndex(
|
|
|
249
309
|
});
|
|
250
310
|
}
|
|
251
311
|
|
|
312
|
+
// 4b. Discover Bundled Skills from Toolchain (if requested or enabled)
|
|
313
|
+
const defaultToolchainRoot = path.resolve(__dirname, "../..");
|
|
314
|
+
const bundledSkillsDir = options.bundledSkillsDir
|
|
315
|
+
? path.resolve(options.bundledSkillsDir)
|
|
316
|
+
: options.toolchainRoot
|
|
317
|
+
? path.join(path.resolve(options.toolchainRoot), ".agents", "skills")
|
|
318
|
+
: options.includeBundled
|
|
319
|
+
? path.join(defaultToolchainRoot, ".agents", "skills")
|
|
320
|
+
: null;
|
|
321
|
+
|
|
322
|
+
if (
|
|
323
|
+
bundledSkillsDir &&
|
|
324
|
+
(await pathExists(bundledSkillsDir)) &&
|
|
325
|
+
bundledSkillsDir !== skillsDir
|
|
326
|
+
) {
|
|
327
|
+
const bundledSkillFiles = await findFilesRecursively(
|
|
328
|
+
bundledSkillsDir,
|
|
329
|
+
(name) => name === "SKILL.md",
|
|
330
|
+
);
|
|
331
|
+
for (const fullSkillPath of bundledSkillFiles) {
|
|
332
|
+
const relFromBundled = path.relative(bundledSkillsDir, fullSkillPath);
|
|
333
|
+
const relFromRoot = path.join(".agents", "skills", relFromBundled);
|
|
334
|
+
const relUrl = path.posix.normalize(
|
|
335
|
+
path.posix.relative(
|
|
336
|
+
wellKnownDirRel,
|
|
337
|
+
relFromRoot.split(path.sep).join(path.posix.sep),
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
const rawContent = await readFile(fullSkillPath);
|
|
341
|
+
const digest = computeSha256(rawContent);
|
|
342
|
+
|
|
343
|
+
let frontmatter = {};
|
|
344
|
+
try {
|
|
345
|
+
const parsed = matter(rawContent.toString("utf8"));
|
|
346
|
+
frontmatter = parsed.data || {};
|
|
347
|
+
} catch {}
|
|
348
|
+
|
|
349
|
+
const parentDirName = path.basename(path.dirname(fullSkillPath));
|
|
350
|
+
const skillName = frontmatter.name || parentDirName;
|
|
351
|
+
const skillDesc = frontmatter.description || "";
|
|
352
|
+
|
|
353
|
+
if (discoveredSkills.some((s) => s.name === skillName)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
discoveredSkills.push({
|
|
358
|
+
name: skillName,
|
|
359
|
+
type: "skill-md",
|
|
360
|
+
description: skillDesc,
|
|
361
|
+
url: relUrl,
|
|
362
|
+
digest,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
252
367
|
// 5. Construct Index Document
|
|
253
368
|
const indexDoc = {
|
|
254
369
|
$schema: "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
|
@@ -319,7 +434,11 @@ export async function runSkillsIndexCommand(dir, options = {}) {
|
|
|
319
434
|
const targetDir = dir ? path.resolve(dir) : process.cwd();
|
|
320
435
|
console.log(`Generating Agent Skills Discovery index for ${targetDir}...`);
|
|
321
436
|
|
|
322
|
-
const result = await generateSkillsIndex(targetDir,
|
|
437
|
+
const result = await generateSkillsIndex(targetDir, {
|
|
438
|
+
includeBundled: options.bundled !== false,
|
|
439
|
+
...options,
|
|
440
|
+
configPath: options.config || options.configPath,
|
|
441
|
+
});
|
|
323
442
|
const count = result.skills ? result.skills.length : 0;
|
|
324
443
|
console.log(
|
|
325
444
|
`Indexed ${count} skill(s) into .well-known/agent-skills/index.json`,
|
|
@@ -158,4 +158,86 @@ metadata:
|
|
|
158
158
|
await generateSkillsIndex(tempDir, { outFile: customOut });
|
|
159
159
|
expect(fs.existsSync(customOut)).toBe(true);
|
|
160
160
|
});
|
|
161
|
+
|
|
162
|
+
it("resolves site metadata from parent directory when indexing a subfolder like dist/public", async () => {
|
|
163
|
+
await writeFile(
|
|
164
|
+
path.join(tempDir, "shadow-claw.config.json"),
|
|
165
|
+
JSON.stringify({
|
|
166
|
+
site: {
|
|
167
|
+
title: "My Custom Knowledge Hub",
|
|
168
|
+
description: "A custom description for testing metadata resolution.",
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
"utf8",
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const distPublicDir = path.join(tempDir, "dist", "public");
|
|
175
|
+
const skillDir = path.join(
|
|
176
|
+
distPublicDir,
|
|
177
|
+
".agents",
|
|
178
|
+
"skills",
|
|
179
|
+
"main",
|
|
180
|
+
"custom",
|
|
181
|
+
);
|
|
182
|
+
await mkdir(skillDir, { recursive: true });
|
|
183
|
+
await writeFile(
|
|
184
|
+
path.join(skillDir, "SKILL.md"),
|
|
185
|
+
"---\nname: custom\ndescription: Custom skill.\n---\n",
|
|
186
|
+
"utf8",
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const res = await generateSkillsIndex(distPublicDir, { write: false });
|
|
190
|
+
expect(res.name).toBe("My Custom Knowledge Hub");
|
|
191
|
+
expect(res.description).toBe(
|
|
192
|
+
"A custom description for testing metadata resolution.",
|
|
193
|
+
);
|
|
194
|
+
expect(res.skills).toHaveLength(1);
|
|
195
|
+
expect(res.skills[0].name).toBe("custom");
|
|
196
|
+
expect(res.skills[0].url).toBe("../../.agents/skills/main/custom/SKILL.md");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("discovers bundled skills when toolchainRoot is provided and merges them without duplicates", async () => {
|
|
200
|
+
const mockToolchain = path.join(tempDir, "toolchain");
|
|
201
|
+
const bundledSkillDir = path.join(
|
|
202
|
+
mockToolchain,
|
|
203
|
+
".agents",
|
|
204
|
+
"skills",
|
|
205
|
+
"main",
|
|
206
|
+
"skill-creator",
|
|
207
|
+
);
|
|
208
|
+
await mkdir(bundledSkillDir, { recursive: true });
|
|
209
|
+
await writeFile(
|
|
210
|
+
path.join(bundledSkillDir, "SKILL.md"),
|
|
211
|
+
"---\nname: skill-creator\ndescription: Bundled skill creator.\n---\n",
|
|
212
|
+
"utf8",
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const mockContent = path.join(tempDir, "content");
|
|
216
|
+
const contentSkillDir = path.join(
|
|
217
|
+
mockContent,
|
|
218
|
+
".agents",
|
|
219
|
+
"skills",
|
|
220
|
+
"main",
|
|
221
|
+
"my-skill",
|
|
222
|
+
);
|
|
223
|
+
await mkdir(contentSkillDir, { recursive: true });
|
|
224
|
+
await writeFile(
|
|
225
|
+
path.join(contentSkillDir, "SKILL.md"),
|
|
226
|
+
"---\nname: my-skill\ndescription: Custom skill.\n---\n",
|
|
227
|
+
"utf8",
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const res = await generateSkillsIndex(mockContent, {
|
|
231
|
+
toolchainRoot: mockToolchain,
|
|
232
|
+
write: false,
|
|
233
|
+
});
|
|
234
|
+
expect(res.skills).toHaveLength(2);
|
|
235
|
+
expect(res.skills.map((s) => s.name)).toEqual(
|
|
236
|
+
expect.arrayContaining(["my-skill", "skill-creator"]),
|
|
237
|
+
);
|
|
238
|
+
const creatorSkill = res.skills.find((s) => s.name === "skill-creator");
|
|
239
|
+
expect(creatorSkill.url).toBe(
|
|
240
|
+
"../../.agents/skills/main/skill-creator/SKILL.md",
|
|
241
|
+
);
|
|
242
|
+
});
|
|
161
243
|
});
|