shadow-claw 1.26.0 → 1.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/main/skill-creator/SKILL.md +39 -0
- package/.agents/skills/main/skill-creator/references/declarative-pipelines.md +44 -0
- package/.agents/skills/main/skill-creator/references/declarative-tools.md +35 -0
- package/.agents/skills/main/skill-creator/references/frontmatter-spec.md +50 -0
- package/.well-known/agent-skills/index.json +14 -0
- package/README.md +5 -3
- package/bin/build/build.mjs +148 -15
- package/bin/build/build.test.mjs +128 -0
- package/bin/cli.mjs +35 -0
- package/bin/commands/mcp.mjs +277 -55
- package/bin/commands/mcp.test.mjs +275 -1
- package/bin/commands/skills-index.mjs +447 -0
- package/bin/commands/skills-index.test.mjs +243 -0
- package/bin/site-config/utils/custom-elements/get-approved-custom-element-scripts.test.mjs +2 -0
- package/bin/site-config/utils/custom-elements/sanitize-embedded-custom-element-scripts.test.mjs +10 -1
- package/dist/lib/components/index.js +25 -25
- package/dist/lib/components/settings/shadow-claw-accounts.js +1 -1
- 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-integrations.js +1 -1
- package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
- package/dist/lib/components/settings/shadow-claw-mcp-remote.js +1 -1
- package/dist/lib/components/settings/shadow-claw-networking.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-a2ui.js +1 -1
- package/dist/lib/components/shadow-claw-accounts.js +1 -1
- 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-integrations.js +1 -1
- package/dist/lib/components/shadow-claw-llm.js +1 -1
- package/dist/lib/components/shadow-claw-mcp-remote.js +1 -1
- package/dist/lib/components/shadow-claw-networking.js +1 -1
- package/dist/lib/components/shadow-claw-page-header.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-CrlGan0m.js → configurePeerJs-oODzltaC.js} +2 -2
- package/dist/lib/{configurePeerJs-CrlGan0m.js.map → configurePeerJs-oODzltaC.js.map} +1 -1
- package/dist/lib/core/control-plane-client.d.ts +1 -0
- package/dist/lib/{downloadGroupFile-DboUjmp2.js → downloadGroupFile-DmcBQU1n.js} +2 -2
- package/dist/lib/{downloadGroupFile-DboUjmp2.js.map → downloadGroupFile-DmcBQU1n.js.map} +1 -1
- package/dist/lib/{file-viewer-BrK2Wxgh.js → file-viewer-C3YKnq_-.js} +4 -3
- package/dist/lib/{file-viewer-BrK2Wxgh.js.map → file-viewer-C3YKnq_-.js.map} +1 -1
- package/dist/lib/index.js +26 -26
- package/dist/lib/{initControlPlane-CsO4UR6_.js → initControlPlane-C2zURVIS.js} +86 -13
- package/dist/lib/initControlPlane-C2zURVIS.js.map +1 -0
- package/dist/lib/{markdown-eeSTFnMA.js → markdown-C6EHl9zs.js} +3 -3
- package/dist/lib/{markdown-eeSTFnMA.js.map → markdown-C6EHl9zs.js.map} +1 -1
- package/dist/lib/{orchestrator-CJvoC2Uu.js → orchestrator-CACLa_Ro.js} +7 -6
- package/dist/lib/{orchestrator-CJvoC2Uu.js.map → orchestrator-CACLa_Ro.js.map} +1 -1
- package/dist/lib/{orchestrator-DCfomsn9.js → orchestrator-KRjx0eba.js} +6 -534
- package/dist/lib/{orchestrator-DCfomsn9.js.map → orchestrator-KRjx0eba.js.map} +1 -1
- package/dist/lib/parsePath-DwC_YZjw.js +16 -0
- package/dist/lib/parsePath-DwC_YZjw.js.map +1 -0
- package/dist/lib/{peerjs-Dp02bXcG.js → peerjs-DFWOBcXf.js} +4 -4
- package/dist/lib/{peerjs-Dp02bXcG.js.map → peerjs-DFWOBcXf.js.map} +1 -1
- package/dist/lib/{readGroupFileBytes-Bve-e5e_.js → readGroupFileBytes-C8oOe8Iu.js} +3 -16
- package/dist/lib/readGroupFileBytes-C8oOe8Iu.js.map +1 -0
- package/dist/lib/security/custom-element-security.d.ts +8 -0
- package/dist/lib/server/control-plane-types.d.ts +1 -1
- package/dist/lib/{shadow-claw-D8xINM3c.js → shadow-claw-6UHdiy4a.js} +5 -5
- package/dist/lib/{shadow-claw-D8xINM3c.js.map → shadow-claw-6UHdiy4a.js.map} +1 -1
- package/dist/lib/{shadow-claw-a2ui-7rJqG4Ke.js → shadow-claw-a2ui-CpZUTzlh.js} +5 -4
- package/dist/lib/{shadow-claw-a2ui-7rJqG4Ke.js.map → shadow-claw-a2ui-CpZUTzlh.js.map} +1 -1
- package/dist/lib/{shadow-claw-accounts-CdruwN8A.js → shadow-claw-accounts-03oJ__XC.js} +2 -2
- package/dist/lib/{shadow-claw-accounts-CdruwN8A.js.map → shadow-claw-accounts-03oJ__XC.js.map} +1 -1
- package/dist/lib/{shadow-claw-channel-config-Bj0-Nk3t.js → shadow-claw-channel-config-DmfwJc6G.js} +3 -3
- package/dist/lib/{shadow-claw-channel-config-Bj0-Nk3t.js.map → shadow-claw-channel-config-DmfwJc6G.js.map} +1 -1
- package/dist/lib/{shadow-claw-channels-C_jI4KhP.js → shadow-claw-channels-1N6Jl-By.js} +3 -3
- package/dist/lib/{shadow-claw-channels-C_jI4KhP.js.map → shadow-claw-channels-1N6Jl-By.js.map} +1 -1
- package/dist/lib/{shadow-claw-chat-B16MspoP.js → shadow-claw-chat-BMoqQJRK.js} +10 -10
- package/dist/lib/{shadow-claw-chat-B16MspoP.js.map → shadow-claw-chat-BMoqQJRK.js.map} +1 -1
- package/dist/lib/{shadow-claw-control-plane-x8Ldid_h.js → shadow-claw-control-plane-UG0o7pCz.js} +6 -6
- package/dist/lib/{shadow-claw-control-plane-x8Ldid_h.js.map → shadow-claw-control-plane-UG0o7pCz.js.map} +1 -1
- package/dist/lib/{shadow-claw-conversations-Ct1kAYdB.js → shadow-claw-conversations-BwXep3iu.js} +4 -3
- package/dist/lib/{shadow-claw-conversations-Ct1kAYdB.js.map → shadow-claw-conversations-BwXep3iu.js.map} +1 -1
- package/dist/lib/{shadow-claw-file-viewer-BfDHHgpA.js → shadow-claw-file-viewer-DUxzVxDT.js} +14 -10
- package/dist/lib/shadow-claw-file-viewer-DUxzVxDT.js.map +1 -0
- package/dist/lib/{shadow-claw-files-EYhOPYIH.js → shadow-claw-files-DIREEnT2.js} +7 -7
- package/dist/lib/{shadow-claw-files-EYhOPYIH.js.map → shadow-claw-files-DIREEnT2.js.map} +1 -1
- package/dist/lib/{shadow-claw-git-tcSpT-P0.js → shadow-claw-git-Bg1SWgsF.js} +4 -4
- package/dist/lib/{shadow-claw-git-tcSpT-P0.js.map → shadow-claw-git-Bg1SWgsF.js.map} +1 -1
- package/dist/lib/{shadow-claw-integrations-NbcCx_ZI.js → shadow-claw-integrations-Ds-CLPhj.js} +2 -2
- package/dist/lib/{shadow-claw-integrations-NbcCx_ZI.js.map → shadow-claw-integrations-Ds-CLPhj.js.map} +1 -1
- package/dist/lib/{shadow-claw-llm-BazJdD5f.js → shadow-claw-llm-CazBrVpM.js} +3 -3
- package/dist/lib/{shadow-claw-llm-BazJdD5f.js.map → shadow-claw-llm-CazBrVpM.js.map} +1 -1
- package/dist/lib/{shadow-claw-mcp-remote-CYmhpg7a.js → shadow-claw-mcp-remote-CT5Q-54E.js} +2 -2
- package/dist/lib/{shadow-claw-mcp-remote-CYmhpg7a.js.map → shadow-claw-mcp-remote-CT5Q-54E.js.map} +1 -1
- package/dist/lib/{shadow-claw-networking-DImPxe0H.js → shadow-claw-networking-Df85tCbO.js} +2 -2
- package/dist/lib/{shadow-claw-networking-DImPxe0H.js.map → shadow-claw-networking-Df85tCbO.js.map} +1 -1
- package/dist/lib/{shadow-claw-page-header-CcHUfFUx.js → shadow-claw-page-header-Cti2fzf-.js} +2 -2
- package/dist/lib/{shadow-claw-page-header-CcHUfFUx.js.map → shadow-claw-page-header-Cti2fzf-.js.map} +1 -1
- package/dist/lib/{shadow-claw-pages-D54jF1Qq.js → shadow-claw-pages-BgPyIEZO.js} +15 -11
- package/dist/lib/shadow-claw-pages-BgPyIEZO.js.map +1 -0
- package/dist/lib/{shadow-claw-peerjs-B9-T8Nlx.js → shadow-claw-peerjs-xkBXILed.js} +3 -3
- package/dist/lib/{shadow-claw-peerjs-B9-T8Nlx.js.map → shadow-claw-peerjs-xkBXILed.js.map} +1 -1
- package/dist/lib/{shadow-claw-settings-Bt1VAe-1.js → shadow-claw-settings-CN89xRVU.js} +4 -4
- package/dist/lib/{shadow-claw-settings-Bt1VAe-1.js.map → shadow-claw-settings-CN89xRVU.js.map} +1 -1
- package/dist/lib/{shadow-claw-storage-CFOjGf1d.js → shadow-claw-storage-VnhcObCe.js} +3 -3
- package/dist/lib/{shadow-claw-storage-CFOjGf1d.js.map → shadow-claw-storage-VnhcObCe.js.map} +1 -1
- package/dist/lib/{shadow-claw-task-server-BiKa2GOm.js → shadow-claw-task-server-jgEC2hHB.js} +2 -2
- package/dist/lib/{shadow-claw-task-server-BiKa2GOm.js.map → shadow-claw-task-server-jgEC2hHB.js.map} +1 -1
- package/dist/lib/{shadow-claw-tasks-DDHd8W4d.js → shadow-claw-tasks-i_evIufy.js} +6 -6
- package/dist/lib/{shadow-claw-tasks-DDHd8W4d.js.map → shadow-claw-tasks-i_evIufy.js.map} +1 -1
- package/dist/lib/{shadow-claw-terminal-kB6_tWKz.js → shadow-claw-terminal-CwScROlu.js} +2 -2
- package/dist/lib/{shadow-claw-terminal-kB6_tWKz.js.map → shadow-claw-terminal-CwScROlu.js.map} +1 -1
- package/dist/lib/{shadow-claw-tools-DxXIm0ku.js → shadow-claw-tools-DD3fyXP4.js} +5 -4
- package/dist/lib/{shadow-claw-tools-DxXIm0ku.js.map → shadow-claw-tools-DD3fyXP4.js.map} +1 -1
- package/dist/lib/{shadow-claw-webvm-Dc8oo7ao.js → shadow-claw-webvm-DbJq4I3-.js} +2 -2
- package/dist/lib/{shadow-claw-webvm-Dc8oo7ao.js.map → shadow-claw-webvm-DbJq4I3-.js.map} +1 -1
- package/dist/lib/{syncWebMcpRegistration-jyRGbC9w.js → syncWebMcpRegistration-DNLnQ-jz.js} +3 -2
- package/dist/lib/{syncWebMcpRegistration-jyRGbC9w.js.map → syncWebMcpRegistration-DNLnQ-jz.js.map} +1 -1
- package/dist/lib/tools-DwwB9RBa.js +546 -0
- package/dist/lib/tools-DwwB9RBa.js.map +1 -0
- package/dist/lib/{trusted-types-CkLDpLiZ.js → trusted-types-Dx0RZlrd.js} +19 -8
- package/dist/lib/trusted-types-Dx0RZlrd.js.map +1 -0
- package/dist/lib/utils/index.js +1 -1
- package/dist/lib/utils/utils.js +1 -1
- package/dist/public/.well-known/agent-skills/index.json +14 -0
- package/dist/public/AGENTS.md +5 -3
- package/dist/public/README.md +5 -3
- package/dist/public/agent.worker.js +1 -1
- package/dist/public/{configurePeerJs-COcwE0E7.js → configurePeerJs-BqgeBjP7.js} +1 -1
- package/dist/public/custom-element-security-FU04Cq05.js +1 -0
- package/dist/public/dist-B9m1QM9v.js +58 -0
- package/dist/public/docs/README.md +31 -31
- package/dist/public/docs/example/article/index.html +1 -1
- package/dist/public/docs/publishing/index.html +1 -1
- package/dist/public/docs/skill-creator/index.html +1 -1
- package/dist/public/docs/subsystems/a2ui.md +11 -1
- package/dist/public/docs/subsystems/cli.md +32 -0
- package/dist/public/docs/subsystems/control-plane.md +8 -2
- package/dist/public/docs/subsystems/custom-element-security.md +19 -9
- package/dist/public/docs/subsystems/mcp-server.md +39 -15
- package/dist/public/docs/subsystems/skills.md +30 -0
- package/dist/public/docs/subsystems/webmcp.md +9 -0
- package/dist/public/downloadGroupFile-D8KRJGGL.js +1 -0
- package/dist/public/e2e/README.md +7 -4
- package/dist/public/{e2e-bridge-CHARpbpW.js → e2e-bridge-Ceqr5kGc.js} +1 -1
- package/dist/public/effect-BJCrpFdp.js +1 -0
- package/dist/public/file-viewer-C-6Jc_tb.js +1 -0
- package/dist/public/index.html +1 -1
- package/dist/public/index.js +1 -1
- package/dist/public/initControlPlane-BN9cWyO0.js +1 -0
- package/dist/public/main/index.html +1 -1
- package/dist/public/main/memory/index.html +1 -1
- package/dist/public/{markdown-1I3O6Mmd.js → markdown-C3M4OW_A.js} +1 -1
- package/dist/public/{orchestrator-Cl95w2vb.js → orchestrator-CMbCTjBf.js} +7 -7
- package/dist/public/{orchestrator-D-xlXG92.js → orchestrator-DHjdLtqa.js} +663 -663
- package/dist/public/peerjs-DUCKH-m9.js +1 -0
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-CaoKmIIB.js → shadow-claw-EAt2ml5Y.js} +3 -3
- package/dist/public/{shadow-claw-accounts-CJvYC4li.js → shadow-claw-accounts-xUOLBsQZ.js} +1 -1
- package/dist/public/{shadow-claw-channels-3K62sU5f.js → shadow-claw-channels-XsfuWFF8.js} +4 -4
- package/dist/public/{shadow-claw-chat-Cu2keU2I.js → shadow-claw-chat-CbSaOZ4q.js} +7 -7
- package/dist/public/{shadow-claw-control-plane-D1OwlQI_.js → shadow-claw-control-plane-DHvN3Ukg.js} +2 -2
- package/dist/public/{shadow-claw-conversations-DDtg3BEZ.js → shadow-claw-conversations-BWs5VVfs.js} +2 -2
- package/dist/public/{shadow-claw-file-viewer-BR__1z9S.js → shadow-claw-file-viewer-BWsoW5QS.js} +5 -4
- package/dist/public/{shadow-claw-files-BJSyZ-Bl.js → shadow-claw-files-CG7MqWiV.js} +8 -8
- package/dist/public/{shadow-claw-git-MAjhXRLc.js → shadow-claw-git-DBRPZNav.js} +6 -6
- package/dist/public/{shadow-claw-integrations-82UoFuDR.js → shadow-claw-integrations-BBVnO0Fc.js} +8 -8
- package/dist/public/{shadow-claw-llm-R8j0lwPP.js → shadow-claw-llm-DMvLgKXo.js} +2 -2
- package/dist/public/{shadow-claw-mcp-remote-BzZoz4LX.js → shadow-claw-mcp-remote-Bkx0gsmF.js} +2 -2
- package/dist/public/{shadow-claw-networking-DLEd9cNm.js → shadow-claw-networking-Dd8Rw31D.js} +2 -2
- package/dist/public/{shadow-claw-notifications-1vwR7TbT.js → shadow-claw-notifications-Dbevj-lq.js} +2 -2
- package/dist/public/{shadow-claw-page-header-DMKbkv3s.js → shadow-claw-page-header-BccSHItx.js} +1 -1
- package/dist/public/{shadow-claw-pages-CjyeIV__.js → shadow-claw-pages-3_lQAv0t.js} +4 -3
- package/dist/public/{shadow-claw-settings-BfbT4dBU.js → shadow-claw-settings-t8TIlgrM.js} +5 -5
- package/dist/public/{shadow-claw-storage-C-mWQQML.js → shadow-claw-storage-CfEXoSIE.js} +2 -2
- package/dist/public/{shadow-claw-task-server-D3lHp0Nc.js → shadow-claw-task-server-DxQSsgRZ.js} +2 -2
- package/dist/public/{shadow-claw-tasks-DVVWEm-c.js → shadow-claw-tasks-DNpicQwz.js} +14 -14
- package/dist/public/{shadow-claw-terminal-BwuTRp-O.js → shadow-claw-terminal-DpY6zU_r.js} +1 -1
- package/dist/public/{shadow-claw-toast-ClCwgzFG.js → shadow-claw-toast-BkrQIP-3.js} +1 -1
- package/dist/public/{shadow-claw-tools-zFLk2nbV.js → shadow-claw-tools-WSwWohnk.js} +2 -2
- package/dist/public/{shadow-claw-webvm-qR9X7fg7.js → shadow-claw-webvm-CP6oFjg5.js} +2 -2
- package/dist/public/syncWebMcpRegistration-DOn9gLnD.js +1 -0
- package/dist/public/toast-BXgUfbyh.js +1 -0
- package/dist/public/tools-DvckJZG2.js +1 -0
- package/dist/server.js +188 -188
- package/package.json +3 -1
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.stories.ts +1380 -93
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.ts +11 -6
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +8 -1
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +13 -5
- package/src/components/shadow-claw-pages/utils/buildHtmlPageSrcdoc.ts +13 -5
- package/src/core/control-plane-client.ts +15 -0
- package/src/core/utils/initControlPlane.test.ts +187 -0
- package/src/core/utils/initControlPlane.ts +194 -20
- package/src/security/custom-element-security.test.ts +51 -0
- package/src/security/custom-element-security.ts +33 -15
- package/src/server/control-plane-types.ts +1 -0
- package/src/server/control-plane.test.ts +69 -0
- package/src/server/control-plane.ts +45 -3
- package/src/server/mcp/tools/built-in-tools.ts +80 -5
- package/src/server/mcp/tools/client-tool-relay.ts +183 -26
- package/src/server/mcp/tools/mcp-tools.test.ts +140 -0
- package/src/server/middleware/static-files.test.ts +142 -0
- package/src/server/middleware/static-files.ts +66 -8
- package/src/testing/jest-setup.ts +14 -0
- package/src/worker/utils/handleMessage.ts +4 -1
- package/dist/lib/initControlPlane-CsO4UR6_.js.map +0 -1
- package/dist/lib/readGroupFileBytes-Bve-e5e_.js.map +0 -1
- package/dist/lib/shadow-claw-file-viewer-BfDHHgpA.js.map +0 -1
- package/dist/lib/shadow-claw-pages-D54jF1Qq.js.map +0 -1
- package/dist/lib/trusted-types-CkLDpLiZ.js.map +0 -1
- package/dist/public/custom-element-security-NCqQ5lmC.js +0 -1
- package/dist/public/downloadGroupFile-G0BZQgQg.js +0 -1
- package/dist/public/effect-rHYr4nfu.js +0 -1
- package/dist/public/file-viewer-8PdCURDD.js +0 -1
- package/dist/public/initControlPlane-CcGXax0A.js +0 -1
- package/dist/public/peerjs-BZdxWQFv.js +0 -1
- package/dist/public/syncWebMcpRegistration-DD42YsYV.js +0 -1
- package/dist/public/toast-HAq8XWFj.js +0 -58
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShadowClaw CLI — `skills:index` command
|
|
3
|
+
* Generates and updates .well-known/agent-skills/index.json per the
|
|
4
|
+
* Cloudflare Agent Skills Discovery RFC (v0.2.0).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import process from "node:process";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import matter from "gray-matter";
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Computes a standardized sha256:{hex} digest from Buffer or string.
|
|
19
|
+
*/
|
|
20
|
+
export function computeSha256(content) {
|
|
21
|
+
const hash = crypto.createHash("sha256").update(content).digest("hex");
|
|
22
|
+
return `sha256:${hash.toLowerCase()}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function pathExists(p) {
|
|
26
|
+
try {
|
|
27
|
+
await stat(p);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recursively find all files matching a filter under a root directory.
|
|
36
|
+
*/
|
|
37
|
+
async function findFilesRecursively(dir, filterFn) {
|
|
38
|
+
const results = [];
|
|
39
|
+
if (!(await pathExists(dir))) return results;
|
|
40
|
+
|
|
41
|
+
async function walk(currentDir) {
|
|
42
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
await walk(fullPath);
|
|
47
|
+
} else if (
|
|
48
|
+
entry.isFile() &&
|
|
49
|
+
(!filterFn || filterFn(entry.name, fullPath))
|
|
50
|
+
) {
|
|
51
|
+
results.push(fullPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await walk(dir);
|
|
57
|
+
return results.sort();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generates the Agent Skills Discovery index object.
|
|
62
|
+
*/
|
|
63
|
+
export async function generateSkillsIndex(
|
|
64
|
+
contentRoot = process.cwd(),
|
|
65
|
+
options = {},
|
|
66
|
+
) {
|
|
67
|
+
const resolvedRoot = path.resolve(contentRoot);
|
|
68
|
+
const wellKnownDirRel = options.outDir || ".well-known/agent-skills";
|
|
69
|
+
|
|
70
|
+
// 1. Read metadata from shadow-claw.config.json or package.json
|
|
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)) {
|
|
97
|
+
try {
|
|
98
|
+
const configStr = await readFile(candidate, "utf8");
|
|
99
|
+
loadedConfig = JSON.parse(configStr);
|
|
100
|
+
break;
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
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
|
+
|
|
151
|
+
// 2. Discover Tools (.agents/tools/**/*.json)
|
|
152
|
+
const toolsDir = path.join(resolvedRoot, ".agents", "tools");
|
|
153
|
+
const toolFiles = await findFilesRecursively(toolsDir, (name) =>
|
|
154
|
+
name.endsWith(".json"),
|
|
155
|
+
);
|
|
156
|
+
const discoveredTools = [];
|
|
157
|
+
|
|
158
|
+
for (const fullToolPath of toolFiles) {
|
|
159
|
+
const relFromRoot = path.relative(resolvedRoot, fullToolPath);
|
|
160
|
+
const relUrl = path.posix.normalize(
|
|
161
|
+
path.posix.relative(
|
|
162
|
+
wellKnownDirRel,
|
|
163
|
+
relFromRoot.split(path.sep).join(path.posix.sep),
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
const content = await readFile(fullToolPath);
|
|
167
|
+
const digest = computeSha256(content);
|
|
168
|
+
|
|
169
|
+
let name = path.basename(fullToolPath, ".json");
|
|
170
|
+
let description = "";
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(content.toString("utf8"));
|
|
174
|
+
if (parsed.name) name = parsed.name;
|
|
175
|
+
if (parsed.description) description = parsed.description;
|
|
176
|
+
} catch {}
|
|
177
|
+
|
|
178
|
+
discoveredTools.push({
|
|
179
|
+
name,
|
|
180
|
+
description,
|
|
181
|
+
url: relUrl,
|
|
182
|
+
digest,
|
|
183
|
+
relFromRoot,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 3. Discover Scripts (.agents/scripts/**/*.{js,mjs})
|
|
188
|
+
const scriptsDir = path.join(resolvedRoot, ".agents", "scripts");
|
|
189
|
+
const scriptFiles = await findFilesRecursively(
|
|
190
|
+
scriptsDir,
|
|
191
|
+
(name) => name.endsWith(".js") || name.endsWith(".mjs"),
|
|
192
|
+
);
|
|
193
|
+
const discoveredScripts = [];
|
|
194
|
+
|
|
195
|
+
for (const fullScriptPath of scriptFiles) {
|
|
196
|
+
const relFromRoot = path.relative(resolvedRoot, fullScriptPath);
|
|
197
|
+
const relUrl = path.posix.normalize(
|
|
198
|
+
path.posix.relative(
|
|
199
|
+
wellKnownDirRel,
|
|
200
|
+
relFromRoot.split(path.sep).join(path.posix.sep),
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
const content = await readFile(fullScriptPath);
|
|
204
|
+
const digest = computeSha256(content);
|
|
205
|
+
const baseName = path.basename(
|
|
206
|
+
fullScriptPath,
|
|
207
|
+
path.extname(fullScriptPath),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
discoveredScripts.push({
|
|
211
|
+
name: baseName,
|
|
212
|
+
description: `Portable ESM engine logic and tool execution handler for ${baseName}.`,
|
|
213
|
+
url: relUrl,
|
|
214
|
+
digest,
|
|
215
|
+
relFromRoot,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 4. Discover Skills (.agents/skills/**/SKILL.md)
|
|
220
|
+
const skillsDir = path.join(resolvedRoot, ".agents", "skills");
|
|
221
|
+
const skillFiles = await findFilesRecursively(
|
|
222
|
+
skillsDir,
|
|
223
|
+
(name) => name === "SKILL.md",
|
|
224
|
+
);
|
|
225
|
+
const discoveredSkills = [];
|
|
226
|
+
|
|
227
|
+
for (const fullSkillPath of skillFiles) {
|
|
228
|
+
const relFromRoot = path.relative(resolvedRoot, fullSkillPath);
|
|
229
|
+
const relUrl = path.posix.normalize(
|
|
230
|
+
path.posix.relative(
|
|
231
|
+
wellKnownDirRel,
|
|
232
|
+
relFromRoot.split(path.sep).join(path.posix.sep),
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
const rawContent = await readFile(fullSkillPath);
|
|
236
|
+
const digest = computeSha256(rawContent);
|
|
237
|
+
|
|
238
|
+
let frontmatter = {};
|
|
239
|
+
try {
|
|
240
|
+
const parsed = matter(rawContent.toString("utf8"));
|
|
241
|
+
frontmatter = parsed.data || {};
|
|
242
|
+
} catch {}
|
|
243
|
+
|
|
244
|
+
const parentDirName = path.basename(path.dirname(fullSkillPath));
|
|
245
|
+
const skillName = frontmatter.name || parentDirName;
|
|
246
|
+
const skillDesc = frontmatter.description || "";
|
|
247
|
+
|
|
248
|
+
// Identify associated tools
|
|
249
|
+
const associatedToolNames = new Set();
|
|
250
|
+
let hasExplicitTools = false;
|
|
251
|
+
if (Array.isArray(frontmatter.metadata?.execution?.tools)) {
|
|
252
|
+
for (const t of frontmatter.metadata.execution.tools) {
|
|
253
|
+
if (t?.name) {
|
|
254
|
+
associatedToolNames.add(t.name);
|
|
255
|
+
hasExplicitTools = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const allowedToolsRaw =
|
|
260
|
+
frontmatter.metadata?.["allowed-tools"] ||
|
|
261
|
+
frontmatter.metadata?.allowedTools ||
|
|
262
|
+
frontmatter["allowed-tools"];
|
|
263
|
+
if (typeof allowedToolsRaw === "string") {
|
|
264
|
+
allowedToolsRaw
|
|
265
|
+
.split(/[,\s]+/)
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.forEach((t) => {
|
|
268
|
+
associatedToolNames.add(t);
|
|
269
|
+
hasExplicitTools = true;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
// Only fallback if no explicit tools were declared in frontmatter
|
|
273
|
+
if (!hasExplicitTools) {
|
|
274
|
+
for (const tool of discoveredTools) {
|
|
275
|
+
const normTool = tool.name.replace(/[-_]/g, "");
|
|
276
|
+
const normSkill = skillName.replace(/[-_]/g, "");
|
|
277
|
+
if (normTool === normSkill) {
|
|
278
|
+
associatedToolNames.add(tool.name);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const matchingTools = discoveredTools
|
|
284
|
+
.filter((t) => associatedToolNames.has(t.name))
|
|
285
|
+
.map(({ name, url, digest }) => ({ name, url, digest }));
|
|
286
|
+
|
|
287
|
+
// Identify associated scripts
|
|
288
|
+
// Match by base name or if script base is a prefix/contains skill name base
|
|
289
|
+
const matchingScripts = discoveredScripts
|
|
290
|
+
.filter((s) => {
|
|
291
|
+
const normScript = s.name.toLowerCase().replace(/[-_]/g, "");
|
|
292
|
+
const normSkill = skillName.toLowerCase().replace(/[-_]/g, "");
|
|
293
|
+
return (
|
|
294
|
+
normSkill.startsWith(normScript) ||
|
|
295
|
+
normScript.startsWith(normSkill) ||
|
|
296
|
+
associatedToolNames.has(s.name)
|
|
297
|
+
);
|
|
298
|
+
})
|
|
299
|
+
.map(({ name, url, digest }) => ({ name, url, digest }));
|
|
300
|
+
|
|
301
|
+
discoveredSkills.push({
|
|
302
|
+
name: skillName,
|
|
303
|
+
type: "skill-md",
|
|
304
|
+
description: skillDesc,
|
|
305
|
+
url: relUrl,
|
|
306
|
+
digest,
|
|
307
|
+
...(matchingTools.length ? { tools: matchingTools } : {}),
|
|
308
|
+
...(matchingScripts.length ? { scripts: matchingScripts } : {}),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
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
|
+
|
|
367
|
+
// 5. Construct Index Document
|
|
368
|
+
const indexDoc = {
|
|
369
|
+
$schema: "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
|
370
|
+
name: siteName,
|
|
371
|
+
description: siteDescription,
|
|
372
|
+
skills: discoveredSkills,
|
|
373
|
+
...(discoveredTools.length
|
|
374
|
+
? {
|
|
375
|
+
tools: discoveredTools.map(({ name, description, url, digest }) => ({
|
|
376
|
+
name,
|
|
377
|
+
...(description ? { description } : {}),
|
|
378
|
+
url,
|
|
379
|
+
digest,
|
|
380
|
+
})),
|
|
381
|
+
}
|
|
382
|
+
: {}),
|
|
383
|
+
...(discoveredScripts.length
|
|
384
|
+
? {
|
|
385
|
+
scripts: discoveredScripts.map(
|
|
386
|
+
({ name, description, url, digest }) => ({
|
|
387
|
+
name,
|
|
388
|
+
...(description ? { description } : {}),
|
|
389
|
+
url,
|
|
390
|
+
digest,
|
|
391
|
+
}),
|
|
392
|
+
),
|
|
393
|
+
}
|
|
394
|
+
: {}),
|
|
395
|
+
...(discoveredTools.length || discoveredScripts.length
|
|
396
|
+
? {
|
|
397
|
+
dependencies: {
|
|
398
|
+
...(discoveredTools.length
|
|
399
|
+
? { tools: discoveredTools.map((t) => t.url) }
|
|
400
|
+
: {}),
|
|
401
|
+
...(discoveredScripts.length
|
|
402
|
+
? { scripts: discoveredScripts.map((s) => s.url) }
|
|
403
|
+
: {}),
|
|
404
|
+
},
|
|
405
|
+
}
|
|
406
|
+
: {}),
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// 6. Output Handling
|
|
410
|
+
if (options.stdout) {
|
|
411
|
+
console.log(JSON.stringify(indexDoc, null, 2));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (options.write !== false) {
|
|
415
|
+
const targetFile = options.outFile
|
|
416
|
+
? path.resolve(options.outFile)
|
|
417
|
+
: path.join(resolvedRoot, wellKnownDirRel, "index.json");
|
|
418
|
+
|
|
419
|
+
await mkdir(path.dirname(targetFile), { recursive: true });
|
|
420
|
+
await writeFile(
|
|
421
|
+
targetFile,
|
|
422
|
+
JSON.stringify(indexDoc, null, 2) + "\n",
|
|
423
|
+
"utf8",
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return indexDoc;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* CLI command runner for `shadow-claw skills:index [dir]`.
|
|
432
|
+
*/
|
|
433
|
+
export async function runSkillsIndexCommand(dir, options = {}) {
|
|
434
|
+
const targetDir = dir ? path.resolve(dir) : process.cwd();
|
|
435
|
+
console.log(`Generating Agent Skills Discovery index for ${targetDir}...`);
|
|
436
|
+
|
|
437
|
+
const result = await generateSkillsIndex(targetDir, {
|
|
438
|
+
includeBundled: options.bundled !== false,
|
|
439
|
+
...options,
|
|
440
|
+
configPath: options.config || options.configPath,
|
|
441
|
+
});
|
|
442
|
+
const count = result.skills ? result.skills.length : 0;
|
|
443
|
+
console.log(
|
|
444
|
+
`Indexed ${count} skill(s) into .well-known/agent-skills/index.json`,
|
|
445
|
+
);
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
jest,
|
|
7
|
+
describe,
|
|
8
|
+
it,
|
|
9
|
+
expect,
|
|
10
|
+
beforeEach,
|
|
11
|
+
afterEach,
|
|
12
|
+
} from "@jest/globals";
|
|
13
|
+
import {
|
|
14
|
+
computeSha256,
|
|
15
|
+
generateSkillsIndex,
|
|
16
|
+
runSkillsIndexCommand,
|
|
17
|
+
} from "./skills-index.mjs";
|
|
18
|
+
|
|
19
|
+
describe("skills-index command and generator", () => {
|
|
20
|
+
let tempDir;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
tempDir = await mkdtemp(path.join(os.tmpdir(), "sc-skills-index-test-"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
28
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("computes standard sha256 format", () => {
|
|
33
|
+
const hash = computeSha256("hello world");
|
|
34
|
+
expect(hash).toMatch(/^sha256:[a-f0-9]{64}$/);
|
|
35
|
+
expect(hash).toBe(
|
|
36
|
+
"sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("indexes skills, tools, and scripts into .well-known/agent-skills/index.json", async () => {
|
|
41
|
+
// 1. Setup mock repo layout
|
|
42
|
+
const skillDir = path.join(
|
|
43
|
+
tempDir,
|
|
44
|
+
".agents",
|
|
45
|
+
"skills",
|
|
46
|
+
"main",
|
|
47
|
+
"test-skill",
|
|
48
|
+
);
|
|
49
|
+
const toolDir = path.join(tempDir, ".agents", "tools", "main");
|
|
50
|
+
const scriptDir = path.join(tempDir, ".agents", "scripts", "main");
|
|
51
|
+
|
|
52
|
+
await mkdir(skillDir, { recursive: true });
|
|
53
|
+
await mkdir(toolDir, { recursive: true });
|
|
54
|
+
await mkdir(scriptDir, { recursive: true });
|
|
55
|
+
|
|
56
|
+
const skillContent = `---
|
|
57
|
+
name: test-skill
|
|
58
|
+
description: A test skill for discovery index.
|
|
59
|
+
metadata:
|
|
60
|
+
allowed-tools: test_tool
|
|
61
|
+
execution:
|
|
62
|
+
type: tools
|
|
63
|
+
tools:
|
|
64
|
+
- name: test_tool
|
|
65
|
+
---
|
|
66
|
+
# Test Skill Body
|
|
67
|
+
`;
|
|
68
|
+
await writeFile(path.join(skillDir, "SKILL.md"), skillContent, "utf8");
|
|
69
|
+
|
|
70
|
+
const toolContent = JSON.stringify({
|
|
71
|
+
name: "test_tool",
|
|
72
|
+
description: "A test tool description.",
|
|
73
|
+
input_schema: { type: "object" },
|
|
74
|
+
});
|
|
75
|
+
await writeFile(path.join(toolDir, "test_tool.json"), toolContent, "utf8");
|
|
76
|
+
|
|
77
|
+
const scriptContent = `export function run() { return 42; }`;
|
|
78
|
+
await writeFile(
|
|
79
|
+
path.join(scriptDir, "test-skill.js"),
|
|
80
|
+
scriptContent,
|
|
81
|
+
"utf8",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// 2. Run generator
|
|
85
|
+
const indexDoc = await generateSkillsIndex(tempDir);
|
|
86
|
+
|
|
87
|
+
expect(indexDoc.$schema).toBe(
|
|
88
|
+
"https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
|
89
|
+
);
|
|
90
|
+
expect(indexDoc.skills).toHaveLength(1);
|
|
91
|
+
|
|
92
|
+
const skill = indexDoc.skills[0];
|
|
93
|
+
expect(skill.name).toBe("test-skill");
|
|
94
|
+
expect(skill.type).toBe("skill-md");
|
|
95
|
+
expect(skill.description).toBe("A test skill for discovery index.");
|
|
96
|
+
expect(skill.url).toBe("../../.agents/skills/main/test-skill/SKILL.md");
|
|
97
|
+
expect(skill.digest).toBe(computeSha256(skillContent));
|
|
98
|
+
|
|
99
|
+
// Check associated tools & scripts on skill
|
|
100
|
+
expect(skill.tools).toHaveLength(1);
|
|
101
|
+
expect(skill.tools[0].name).toBe("test_tool");
|
|
102
|
+
expect(skill.tools[0].url).toBe("../../.agents/tools/main/test_tool.json");
|
|
103
|
+
expect(skill.tools[0].digest).toBe(computeSha256(toolContent));
|
|
104
|
+
|
|
105
|
+
expect(skill.scripts).toHaveLength(1);
|
|
106
|
+
expect(skill.scripts[0].name).toBe("test-skill");
|
|
107
|
+
expect(skill.scripts[0].url).toBe(
|
|
108
|
+
"../../.agents/scripts/main/test-skill.js",
|
|
109
|
+
);
|
|
110
|
+
expect(skill.scripts[0].digest).toBe(computeSha256(scriptContent));
|
|
111
|
+
|
|
112
|
+
// Check top-level arrays
|
|
113
|
+
expect(indexDoc.tools).toHaveLength(1);
|
|
114
|
+
expect(indexDoc.scripts).toHaveLength(1);
|
|
115
|
+
expect(indexDoc.dependencies.tools).toContain(
|
|
116
|
+
"../../.agents/tools/main/test_tool.json",
|
|
117
|
+
);
|
|
118
|
+
expect(indexDoc.dependencies.scripts).toContain(
|
|
119
|
+
"../../.agents/scripts/main/test-skill.js",
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Verify written file on disk
|
|
123
|
+
const targetFile = path.join(
|
|
124
|
+
tempDir,
|
|
125
|
+
".well-known",
|
|
126
|
+
"agent-skills",
|
|
127
|
+
"index.json",
|
|
128
|
+
);
|
|
129
|
+
expect(fs.existsSync(targetFile)).toBe(true);
|
|
130
|
+
|
|
131
|
+
const writtenJson = JSON.parse(
|
|
132
|
+
await fs.promises.readFile(targetFile, "utf8"),
|
|
133
|
+
);
|
|
134
|
+
expect(writtenJson.skills[0].name).toBe("test-skill");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("respects write: false and custom output file options", async () => {
|
|
138
|
+
const skillDir = path.join(tempDir, ".agents", "skills", "main", "minimal");
|
|
139
|
+
await mkdir(skillDir, { recursive: true });
|
|
140
|
+
await writeFile(
|
|
141
|
+
path.join(skillDir, "SKILL.md"),
|
|
142
|
+
"---\nname: minimal\ndescription: Minimal.\n---\n",
|
|
143
|
+
"utf8",
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const res = await generateSkillsIndex(tempDir, { write: false });
|
|
147
|
+
expect(res.skills).toHaveLength(1);
|
|
148
|
+
|
|
149
|
+
const standardPath = path.join(
|
|
150
|
+
tempDir,
|
|
151
|
+
".well-known",
|
|
152
|
+
"agent-skills",
|
|
153
|
+
"index.json",
|
|
154
|
+
);
|
|
155
|
+
expect(fs.existsSync(standardPath)).toBe(false);
|
|
156
|
+
|
|
157
|
+
const customOut = path.join(tempDir, "custom", "index.json");
|
|
158
|
+
await generateSkillsIndex(tempDir, { outFile: customOut });
|
|
159
|
+
expect(fs.existsSync(customOut)).toBe(true);
|
|
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
|
+
});
|
|
243
|
+
});
|
|
@@ -9,6 +9,7 @@ describe("getApprovedCustomElementScripts", () => {
|
|
|
9
9
|
"https://bad.example.com/x.mjs",
|
|
10
10
|
"https://allowed.example.com/x.mjs",
|
|
11
11
|
{ src: "local.mjs" },
|
|
12
|
+
{ src: "adapter.mjs", hasInit: true },
|
|
12
13
|
],
|
|
13
14
|
["allowed.example.com"],
|
|
14
15
|
(message) => warns.push(message),
|
|
@@ -17,6 +18,7 @@ describe("getApprovedCustomElementScripts", () => {
|
|
|
17
18
|
expect(approved).toEqual([
|
|
18
19
|
"https://allowed.example.com/x.mjs",
|
|
19
20
|
{ src: "local.mjs" },
|
|
21
|
+
{ src: "adapter.mjs", hasInit: true },
|
|
20
22
|
]);
|
|
21
23
|
expect(warns).toEqual([
|
|
22
24
|
"[Security] Skipping script from unapproved domain during build: https://bad.example.com/x.mjs",
|