shadow-claw 1.29.0 → 1.30.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.
Files changed (277) hide show
  1. package/.agents/skills/main/skill-creator/SKILL.md +3 -2
  2. package/.agents/skills/main/skill-creator/references/frontmatter-spec.md +5 -3
  3. package/.well-known/agent-skills/index.json +1 -1
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/.well-known/mcp.json +1 -1
  6. package/dist/lib/{app-routes-CqBOeA5q.js → app-routes-BAEMLu9g.js} +3 -2
  7. package/dist/lib/app-routes-BAEMLu9g.js.map +1 -0
  8. package/dist/lib/{bundler-8JNIKwOs.js → bundler-CsL7hHMq.js} +4 -9
  9. package/dist/lib/{bundler-8JNIKwOs.js.map → bundler-CsL7hHMq.js.map} +1 -1
  10. package/dist/lib/components/index.js +26 -26
  11. package/dist/lib/components/settings/shadow-claw-accounts.js +1 -1
  12. package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
  13. package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
  14. package/dist/lib/components/settings/shadow-claw-git.js +1 -1
  15. package/dist/lib/components/settings/shadow-claw-integrations.js +1 -1
  16. package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
  17. package/dist/lib/components/settings/shadow-claw-mcp-remote.js +1 -1
  18. package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
  19. package/dist/lib/components/settings/shadow-claw-notifications.js +1 -1
  20. package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
  21. package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
  22. package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
  23. package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
  24. package/dist/lib/components/shadow-claw-a2ui.js +1 -1
  25. package/dist/lib/components/shadow-claw-accounts.js +1 -1
  26. package/dist/lib/components/shadow-claw-channel-config.js +1 -1
  27. package/dist/lib/components/shadow-claw-channels.js +1 -1
  28. package/dist/lib/components/shadow-claw-chat.js +1 -1
  29. package/dist/lib/components/shadow-claw-control-plane.js +1 -1
  30. package/dist/lib/components/shadow-claw-conversations.js +1 -1
  31. package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
  32. package/dist/lib/components/shadow-claw-files.js +1 -1
  33. package/dist/lib/components/shadow-claw-git.js +1 -1
  34. package/dist/lib/components/shadow-claw-integrations.js +1 -1
  35. package/dist/lib/components/shadow-claw-llm.js +1 -1
  36. package/dist/lib/components/shadow-claw-mcp-remote.js +1 -1
  37. package/dist/lib/components/shadow-claw-networking.js +1 -1
  38. package/dist/lib/components/shadow-claw-notifications.js +1 -1
  39. package/dist/lib/components/shadow-claw-page-header.js +1 -1
  40. package/dist/lib/components/shadow-claw-pages.js +1 -1
  41. package/dist/lib/components/shadow-claw-peerjs.js +1 -1
  42. package/dist/lib/components/shadow-claw-settings.js +1 -1
  43. package/dist/lib/components/shadow-claw-storage.js +1 -1
  44. package/dist/lib/components/shadow-claw-task-server.js +1 -1
  45. package/dist/lib/components/shadow-claw-tasks.js +1 -1
  46. package/dist/lib/components/shadow-claw-terminal.js +1 -1
  47. package/dist/lib/components/shadow-claw-tools.js +1 -1
  48. package/dist/lib/components/shadow-claw-webvm.js +1 -1
  49. package/dist/lib/components/shadow-claw.js +1 -1
  50. package/dist/lib/{config--CFrw3J3.js → config-DjZ9QWc4.js} +2 -2
  51. package/dist/lib/{config--CFrw3J3.js.map → config-DjZ9QWc4.js.map} +1 -1
  52. package/dist/lib/{configurePeerJs-DG4hliKp.js → configurePeerJs-H0WaoT0w.js} +3 -3
  53. package/dist/lib/{configurePeerJs-DG4hliKp.js.map → configurePeerJs-H0WaoT0w.js.map} +1 -1
  54. package/dist/lib/{connections-DwjzwVz0.js → connections-BURqMIUv.js} +2 -2
  55. package/dist/lib/{connections-DwjzwVz0.js.map → connections-BURqMIUv.js.map} +1 -1
  56. package/dist/lib/content/markdown-links.d.ts +19 -0
  57. package/dist/lib/core/app-routes.d.ts +1 -0
  58. package/dist/lib/{downloadGroupFile-DmcBQU1n.js → downloadGroupFile-B8J4up2a.js} +2 -2
  59. package/dist/lib/{downloadGroupFile-DmcBQU1n.js.map → downloadGroupFile-B8J4up2a.js.map} +1 -1
  60. package/dist/lib/{file-viewer-C3YKnq_-.js → file-viewer-CwvhxAAG.js} +5 -5
  61. package/dist/lib/{file-viewer-C3YKnq_-.js.map → file-viewer-CwvhxAAG.js.map} +1 -1
  62. package/dist/lib/{getGroupDir-m0f-ekgX.js → getGroupDir-QrN2aTo8.js} +4 -4
  63. package/dist/lib/{getGroupDir-m0f-ekgX.js.map → getGroupDir-QrN2aTo8.js.map} +1 -1
  64. package/dist/lib/{git-C-Q6-IVe.js → git-DhZoe1SE.js} +3 -3
  65. package/dist/lib/{git-C-Q6-IVe.js.map → git-DhZoe1SE.js.map} +1 -1
  66. package/dist/lib/{iframe-sanitizer-CUqHuWQ-.js → iframe-sanitizer-Bujgq14T.js} +2 -2
  67. package/dist/lib/{iframe-sanitizer-CUqHuWQ-.js.map → iframe-sanitizer-Bujgq14T.js.map} +1 -1
  68. package/dist/lib/index.js +27 -27
  69. package/dist/lib/{initChatSplitResize-yyBTU6h9.js → initChatSplitResize-BjiWoqC_.js} +2 -2
  70. package/dist/lib/{initChatSplitResize-yyBTU6h9.js.map → initChatSplitResize-BjiWoqC_.js.map} +1 -1
  71. package/dist/lib/{initControlPlane-DnpAzphg.js → initControlPlane-C9JxQCDh.js} +16 -16
  72. package/dist/lib/{initControlPlane-DnpAzphg.js.map → initControlPlane-C9JxQCDh.js.map} +1 -1
  73. package/dist/lib/{markdown-DgdSt2GD.js → markdown-DYHmNjC4.js} +126 -5
  74. package/dist/lib/markdown-DYHmNjC4.js.map +1 -0
  75. package/dist/lib/{mcp-reconnect-C86MR3z8.js → mcp-reconnect-BxJ77WSb.js} +2 -2
  76. package/dist/lib/{mcp-reconnect-C86MR3z8.js.map → mcp-reconnect-BxJ77WSb.js.map} +1 -1
  77. package/dist/lib/{memoryStorage-UR8E8K-H.js → memoryStorage-D3FBwO9T.js} +2 -2
  78. package/dist/lib/{memoryStorage-UR8E8K-H.js.map → memoryStorage-D3FBwO9T.js.map} +1 -1
  79. package/dist/lib/{orchestrator-ClXbJ4Mb.js → orchestrator-C6dc-2OQ.js} +130 -27
  80. package/dist/lib/{orchestrator-ClXbJ4Mb.js.map → orchestrator-C6dc-2OQ.js.map} +1 -1
  81. package/dist/lib/{orchestrator-CjEau_RG.js → orchestrator-DVkyd8K9.js} +27 -17
  82. package/dist/lib/orchestrator-DVkyd8K9.js.map +1 -0
  83. package/dist/lib/{peerjs-BdfpltIe.js → peerjs-BUS9O04I.js} +274 -60
  84. package/dist/lib/peerjs-BUS9O04I.js.map +1 -0
  85. package/dist/lib/{push-client-DjwKknNg.js → push-client-4JsQGmQa.js} +3 -3
  86. package/dist/lib/{push-client-DjwKknNg.js.map → push-client-4JsQGmQa.js.map} +1 -1
  87. package/dist/lib/{readGroupFileBytes-C8oOe8Iu.js → readGroupFileBytes-CSA7u-nJ.js} +2 -2
  88. package/dist/lib/{readGroupFileBytes-C8oOe8Iu.js.map → readGroupFileBytes-CSA7u-nJ.js.map} +1 -1
  89. package/dist/lib/{shadow-claw-DcPGKenn.js → shadow-claw-BMoF8Auf.js} +11 -11
  90. package/dist/lib/{shadow-claw-DcPGKenn.js.map → shadow-claw-BMoF8Auf.js.map} +1 -1
  91. package/dist/lib/{shadow-claw-a2ui-CpZUTzlh.js → shadow-claw-a2ui-2I5nffIn.js} +2 -2
  92. package/dist/lib/{shadow-claw-a2ui-CpZUTzlh.js.map → shadow-claw-a2ui-2I5nffIn.js.map} +1 -1
  93. package/dist/lib/{shadow-claw-accounts-03oJ__XC.js → shadow-claw-accounts-BCjlgvzy.js} +3 -3
  94. package/dist/lib/{shadow-claw-accounts-03oJ__XC.js.map → shadow-claw-accounts-BCjlgvzy.js.map} +1 -1
  95. package/dist/lib/{shadow-claw-channel-config-DEL-aZ7N.js → shadow-claw-channel-config-CxQAmlDE.js} +3 -3
  96. package/dist/lib/{shadow-claw-channel-config-DEL-aZ7N.js.map → shadow-claw-channel-config-CxQAmlDE.js.map} +1 -1
  97. package/dist/lib/{shadow-claw-channels-BgaJiPdE.js → shadow-claw-channels-CuYMUVAG.js} +3 -3
  98. package/dist/lib/{shadow-claw-channels-BgaJiPdE.js.map → shadow-claw-channels-CuYMUVAG.js.map} +1 -1
  99. package/dist/lib/{shadow-claw-chat-mXPQZbwT.js → shadow-claw-chat-RFtPmmyl.js} +13 -12
  100. package/dist/lib/shadow-claw-chat-RFtPmmyl.js.map +1 -0
  101. package/dist/lib/{shadow-claw-control-plane-DO6foOLV.js → shadow-claw-control-plane-siqmRzR1.js} +7 -7
  102. package/dist/lib/{shadow-claw-control-plane-DO6foOLV.js.map → shadow-claw-control-plane-siqmRzR1.js.map} +1 -1
  103. package/dist/lib/{shadow-claw-conversations-BLwHTwdW.js → shadow-claw-conversations-DygK5y1e.js} +7 -7
  104. package/dist/lib/{shadow-claw-conversations-BLwHTwdW.js.map → shadow-claw-conversations-DygK5y1e.js.map} +1 -1
  105. package/dist/lib/{shadow-claw-file-viewer-BSbZvSl-.js → shadow-claw-file-viewer-DGMA7d7H.js} +11 -11
  106. package/dist/lib/shadow-claw-file-viewer-DGMA7d7H.js.map +1 -0
  107. package/dist/lib/{shadow-claw-files-BLVr9Pua.js → shadow-claw-files-CgD1B9J7.js} +8 -8
  108. package/dist/lib/{shadow-claw-files-BLVr9Pua.js.map → shadow-claw-files-CgD1B9J7.js.map} +1 -1
  109. package/dist/lib/{shadow-claw-git-CHC1H86J.js → shadow-claw-git-Bw1jGRrf.js} +6 -6
  110. package/dist/lib/{shadow-claw-git-CHC1H86J.js.map → shadow-claw-git-Bw1jGRrf.js.map} +1 -1
  111. package/dist/lib/{shadow-claw-integrations-Ds-CLPhj.js → shadow-claw-integrations-ChaXtvsg.js} +4 -4
  112. package/dist/lib/{shadow-claw-integrations-Ds-CLPhj.js.map → shadow-claw-integrations-ChaXtvsg.js.map} +1 -1
  113. package/dist/lib/{shadow-claw-llm-CKXfxd3D.js → shadow-claw-llm-CDM4Akjc.js} +6 -6
  114. package/dist/lib/{shadow-claw-llm-CKXfxd3D.js.map → shadow-claw-llm-CDM4Akjc.js.map} +1 -1
  115. package/dist/lib/{shadow-claw-mcp-remote-CT5Q-54E.js → shadow-claw-mcp-remote-BLxs_Z4I.js} +4 -4
  116. package/dist/lib/{shadow-claw-mcp-remote-CT5Q-54E.js.map → shadow-claw-mcp-remote-BLxs_Z4I.js.map} +1 -1
  117. package/dist/lib/{shadow-claw-networking-dpYvfchh.js → shadow-claw-networking-BYt-kMSI.js} +2 -2
  118. package/dist/lib/{shadow-claw-networking-dpYvfchh.js.map → shadow-claw-networking-BYt-kMSI.js.map} +1 -1
  119. package/dist/lib/{shadow-claw-notifications-_YeAVPga.js → shadow-claw-notifications-Dpe4EtZI.js} +3 -3
  120. package/dist/lib/{shadow-claw-notifications-_YeAVPga.js.map → shadow-claw-notifications-Dpe4EtZI.js.map} +1 -1
  121. package/dist/lib/{shadow-claw-page-header-Cti2fzf-.js → shadow-claw-page-header-P4EvWyUr.js} +2 -2
  122. package/dist/lib/{shadow-claw-page-header-Cti2fzf-.js.map → shadow-claw-page-header-P4EvWyUr.js.map} +1 -1
  123. package/dist/lib/{shadow-claw-pages-DVzZriZ7.js → shadow-claw-pages-DWd_c4d6.js} +10 -10
  124. package/dist/lib/{shadow-claw-pages-DVzZriZ7.js.map → shadow-claw-pages-DWd_c4d6.js.map} +1 -1
  125. package/dist/lib/{shadow-claw-peerjs-BvixjeBz.js → shadow-claw-peerjs-D928817m.js} +3 -3
  126. package/dist/lib/{shadow-claw-peerjs-BvixjeBz.js.map → shadow-claw-peerjs-D928817m.js.map} +1 -1
  127. package/dist/lib/{shadow-claw-settings-DV9-gD4Q.js → shadow-claw-settings-C8OPrSqs.js} +8 -8
  128. package/dist/lib/{shadow-claw-settings-DV9-gD4Q.js.map → shadow-claw-settings-C8OPrSqs.js.map} +1 -1
  129. package/dist/lib/{shadow-claw-storage-Bv__kXG6.js → shadow-claw-storage-CZ2uZJNw.js} +5 -5
  130. package/dist/lib/{shadow-claw-storage-Bv__kXG6.js.map → shadow-claw-storage-CZ2uZJNw.js.map} +1 -1
  131. package/dist/lib/{shadow-claw-task-server-DNOe8PWK.js → shadow-claw-task-server-CLT018BX.js} +2 -2
  132. package/dist/lib/{shadow-claw-task-server-DNOe8PWK.js.map → shadow-claw-task-server-CLT018BX.js.map} +1 -1
  133. package/dist/lib/{shadow-claw-tasks-qmcDT4PU.js → shadow-claw-tasks-D5h-lGmq.js} +9 -8
  134. package/dist/lib/shadow-claw-tasks-D5h-lGmq.js.map +1 -0
  135. package/dist/lib/{shadow-claw-terminal-0QUWP8of.js → shadow-claw-terminal-Cy1O6KVM.js} +2 -2
  136. package/dist/lib/{shadow-claw-terminal-0QUWP8of.js.map → shadow-claw-terminal-Cy1O6KVM.js.map} +1 -1
  137. package/dist/lib/{shadow-claw-tools-Bs_bmI6-.js → shadow-claw-tools-CVD06bCR.js} +9 -9
  138. package/dist/lib/{shadow-claw-tools-Bs_bmI6-.js.map → shadow-claw-tools-CVD06bCR.js.map} +1 -1
  139. package/dist/lib/{shadow-claw-webvm-DYaoL7QY.js → shadow-claw-webvm-DoGl5ZSJ.js} +3 -3
  140. package/dist/lib/{shadow-claw-webvm-DYaoL7QY.js.map → shadow-claw-webvm-DoGl5ZSJ.js.map} +1 -1
  141. package/dist/lib/subsystems/channels/peerjs.d.ts +37 -4
  142. package/dist/lib/subsystems/channels/room.d.ts +4 -0
  143. package/dist/lib/{syncWebMcpRegistration-BhbqI6R2.js → syncWebMcpRegistration-BeYkQEvj.js} +5 -5
  144. package/dist/lib/{syncWebMcpRegistration-BhbqI6R2.js.map → syncWebMcpRegistration-BeYkQEvj.js.map} +1 -1
  145. package/dist/lib/tools-C3Kft940.js +1 -0
  146. package/dist/lib/{tools-DBaMRiEu.js → tools-DKzPWc0q.js} +2 -2
  147. package/dist/lib/{tools-DBaMRiEu.js.map → tools-DKzPWc0q.js.map} +1 -1
  148. package/dist/lib/{tools-DwwB9RBa.js → tools-DqcJe8LS.js} +5 -5
  149. package/dist/lib/{tools-DwwB9RBa.js.map → tools-DqcJe8LS.js.map} +1 -1
  150. package/dist/lib/{trusted-types-Dx0RZlrd.js → trusted-types-Oz7xXJAY.js} +4 -4
  151. package/dist/lib/{trusted-types-Dx0RZlrd.js.map → trusted-types-Oz7xXJAY.js.map} +1 -1
  152. package/dist/lib/utils/index.js +1 -1
  153. package/dist/lib/utils/namespacedStorage.js +1 -1
  154. package/dist/lib/utils/utils.js +1 -1
  155. package/dist/lib/{webmcp-j_1_T3wf.js → webmcp-BC8_WafM.js} +4 -4
  156. package/dist/lib/{webmcp-j_1_T3wf.js.map → webmcp-BC8_WafM.js.map} +1 -1
  157. package/dist/lib/worker/utils/system-prompt.d.ts +4 -1
  158. package/dist/public/.agents/skills/main/skill-creator/SKILL.md +3 -2
  159. package/dist/public/.agents/skills/main/skill-creator/references/frontmatter-spec.md +5 -3
  160. package/dist/public/.well-known/agent-skills/index.json +1 -1
  161. package/dist/public/.well-known/mcp/server-card.json +1 -1
  162. package/dist/public/.well-known/mcp.json +1 -1
  163. package/dist/public/agent.worker.js +18 -17
  164. package/dist/public/app-routes-BMQ4HFaM.js +1 -0
  165. package/dist/public/{bundler-Ca-Xwln8.js → bundler-0if-QelV.js} +4 -4
  166. package/dist/public/{config-CrHchneq.js → config-Bnwx4EfS.js} +1 -1
  167. package/dist/public/{configurePeerJs-B470pdeU.js → configurePeerJs-DkyGvwk_.js} +1 -1
  168. package/dist/public/{connections-DvbgdkaB.js → connections-6575KDGE.js} +1 -1
  169. package/dist/public/{custom-element-security-FU04Cq05.js → custom-element-security-BQJQ_oKz.js} +1 -1
  170. package/dist/public/docs/example/article/index.html +2 -2
  171. package/dist/public/docs/publishing/index.html +2 -2
  172. package/dist/public/docs/skill-creator/index.html +2 -2
  173. package/dist/public/docs/subsystems/skills.md +3 -1
  174. package/dist/public/{downloadGroupFile-D8KRJGGL.js → downloadGroupFile-Wo1Sxdhr.js} +1 -1
  175. package/dist/public/{e2e-bridge-CSW0S5Yz.js → e2e-bridge-CvLCUU2O.js} +1 -1
  176. package/dist/public/{file-viewer-Cs-riNvy.js → file-viewer-CIjqAvNF.js} +1 -1
  177. package/dist/public/{getGroupDir-P1h9wl6S.js → getGroupDir-BwprrdrQ.js} +1 -1
  178. package/dist/public/{git-7Vq_SJSp.js → git-BSnY2D-A.js} +2 -2
  179. package/dist/public/{iframe-sanitizer-y2Z1pPwY.js → iframe-sanitizer-viROQkRn.js} +1 -1
  180. package/dist/public/index.html +2 -2
  181. package/dist/public/index.js +1 -1
  182. package/dist/public/{initChatSplitResize-CzV2E7Ul.js → initChatSplitResize-BA18wLQQ.js} +1 -1
  183. package/dist/public/{initControlPlane-BqCY0NQy.js → initControlPlane-L96uJkRA.js} +1 -1
  184. package/dist/public/main/index.html +2 -2
  185. package/dist/public/main/memory/index.html +2 -2
  186. package/dist/public/{markdown-Cw7vCtTg.js → markdown-DGjZAm_w.js} +17 -14
  187. package/dist/public/{mcp-reconnect-9emsqGRe.js → mcp-reconnect-CBHuaYdF.js} +1 -1
  188. package/dist/public/{memoryStorage-C0KvLNUp.js → memoryStorage-C1llaYus.js} +1 -1
  189. package/dist/public/orchestrator-C0VOoTiK.js +15 -0
  190. package/dist/public/{orchestrator-DpkkeD6T.js → orchestrator-DMWi--Dc.js} +20 -19
  191. package/dist/public/peerjs-Bf6ZxfbA.js +1 -0
  192. package/dist/public/{push-client-CHjSbrWs.js → push-client-BD4YalBa.js} +1 -1
  193. package/dist/public/service-worker.js +1 -1
  194. package/dist/public/{shadow-claw-BBPV_9X8.js → shadow-claw-DRy-D4Qi.js} +2 -2
  195. package/dist/public/{shadow-claw-accounts-xUOLBsQZ.js → shadow-claw-accounts-DnfmA8cb.js} +1 -1
  196. package/dist/public/{shadow-claw-channels-BJvrKQjg.js → shadow-claw-channels-8UkycZAa.js} +1 -1
  197. package/dist/public/{shadow-claw-chat-BG2EaRaw.js → shadow-claw-chat-Cf5LUiul.js} +9 -9
  198. package/dist/public/{shadow-claw-control-plane-BBa7WbTg.js → shadow-claw-control-plane-UsBF-Nvd.js} +2 -2
  199. package/dist/public/{shadow-claw-conversations-CFaWFmkm.js → shadow-claw-conversations-Aoe61Y21.js} +1 -1
  200. package/dist/public/{shadow-claw-file-viewer-Z-GIKaE9.js → shadow-claw-file-viewer-D28dSSEn.js} +6 -6
  201. package/dist/public/{shadow-claw-files-BBx4iMsk.js → shadow-claw-files-CaDUlPEU.js} +1 -1
  202. package/dist/public/{shadow-claw-git-vTOKatwd.js → shadow-claw-git-DYNcZMWA.js} +2 -2
  203. package/dist/public/{shadow-claw-integrations-BBVnO0Fc.js → shadow-claw-integrations-BJf_hQVv.js} +1 -1
  204. package/dist/public/{shadow-claw-llm-Ce1Ax6-x.js → shadow-claw-llm-BqK2SIRf.js} +2 -2
  205. package/dist/public/{shadow-claw-mcp-remote-Bkx0gsmF.js → shadow-claw-mcp-remote-B_dDG990.js} +1 -1
  206. package/dist/public/{shadow-claw-networking-q4XIzdB5.js → shadow-claw-networking-D8LxOofv.js} +1 -1
  207. package/dist/public/{shadow-claw-notifications-D2cz0gmm.js → shadow-claw-notifications-DpVyiNG_.js} +1 -1
  208. package/dist/public/{shadow-claw-page-header-BccSHItx.js → shadow-claw-page-header-DdC7bJuW.js} +1 -1
  209. package/dist/public/{shadow-claw-pages-3335lmSC.js → shadow-claw-pages-CYMl4J-6.js} +3 -3
  210. package/dist/public/{shadow-claw-settings-4ptYuJwO.js → shadow-claw-settings-Da_8Oy3O.js} +3 -3
  211. package/dist/public/{shadow-claw-storage-bIrPKFhF.js → shadow-claw-storage-DXIm86_-.js} +1 -1
  212. package/dist/public/{shadow-claw-task-server-5yqVBxy1.js → shadow-claw-task-server-BCceBD0G.js} +1 -1
  213. package/dist/public/{shadow-claw-tasks-DcqYHPLZ.js → shadow-claw-tasks-BW1QZ5L3.js} +19 -19
  214. package/dist/public/{shadow-claw-terminal-fARaDONr.js → shadow-claw-terminal-vqfsxhcK.js} +1 -1
  215. package/dist/public/{shadow-claw-tools-paEAhcJr.js → shadow-claw-tools-l0Jsa7UJ.js} +1 -1
  216. package/dist/public/{shadow-claw-webvm-CbzH_vyX.js → shadow-claw-webvm-CDPOWZm7.js} +1 -1
  217. package/dist/public/skills/main/skill-creator/SKILL.md +3 -2
  218. package/dist/public/skills/main/skill-creator/references/frontmatter-spec.md +5 -3
  219. package/dist/public/static-main-manifest.json +2 -2
  220. package/dist/public/{syncWebMcpRegistration-CWSYaHex.js → syncWebMcpRegistration-5b5FkP9_.js} +1 -1
  221. package/dist/public/tools-B-PzsLc2.js +1 -0
  222. package/dist/public/{tools-C8x3nmo4.js → tools-CcgG5XuA.js} +1 -1
  223. package/dist/public/{tools-DvckJZG2.js → tools-VSOY66vs.js} +1 -1
  224. package/dist/public/{webmcp-DAjmliCx.js → webmcp-DBONjQWd.js} +1 -1
  225. package/dist/server.js +2 -2
  226. package/package.json +2 -2
  227. package/src/__mocks__/marked.cjs +14 -0
  228. package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +17 -0
  229. package/src/components/shadow-claw-chat/shadow-claw-chat.ts +5 -1
  230. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +78 -0
  231. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +6 -2
  232. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.test.ts +56 -0
  233. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +22 -0
  234. package/src/components/shadow-claw-tasks/shadow-claw-tasks.ts +5 -1
  235. package/src/content/markdown-links.test.ts +109 -0
  236. package/src/content/markdown-links.ts +187 -0
  237. package/src/content/markdown.test.ts +33 -0
  238. package/src/content/markdown.ts +6 -1
  239. package/src/core/app-routes.test.ts +65 -0
  240. package/src/core/app-routes.ts +1 -1
  241. package/src/core/orchestrator/orchestrator.ts +3 -0
  242. package/src/core/orchestrator/utils/compactContext.ts +2 -0
  243. package/src/core/orchestrator/utils/invokeAgent.ts +1 -0
  244. package/src/subsystems/accounts/service-accounts.test.ts +0 -1
  245. package/src/subsystems/channels/peerjs.test.ts +479 -1
  246. package/src/subsystems/channels/peerjs.ts +420 -75
  247. package/src/subsystems/channels/room.test.ts +35 -0
  248. package/src/subsystems/channels/room.ts +15 -0
  249. package/src/subsystems/skills/activateSkill.test.ts +38 -5
  250. package/src/subsystems/skills/activateSkill.ts +25 -3
  251. package/src/subsystems/skills/parseSkill.test.ts +71 -0
  252. package/src/subsystems/skills/parseSkill.ts +185 -25
  253. package/src/subsystems/tools/files.ts +4 -4
  254. package/src/worker/tools/spawn-subagent/spawn-subagent.test.ts +22 -23
  255. package/src/worker/tools/workspace/send-file.test.ts +24 -1
  256. package/src/worker/tools/workspace/send-file.ts +6 -3
  257. package/src/worker/utils/handleCompact.test.ts +3 -4
  258. package/src/worker/utils/handleInvoke.test.ts +13 -11
  259. package/src/worker/utils/handleInvoke.ts +3 -0
  260. package/src/worker/utils/handleMessage.test.ts +0 -1
  261. package/src/worker/utils/post.test.ts +3 -4
  262. package/src/worker/utils/sandboxedEval.test.ts +1 -2
  263. package/src/worker/utils/system-prompt.test.ts +59 -0
  264. package/src/worker/utils/system-prompt.ts +21 -1
  265. package/src/worker/utils/withRetry.test.ts +3 -16
  266. package/dist/lib/app-routes-CqBOeA5q.js.map +0 -1
  267. package/dist/lib/markdown-DgdSt2GD.js.map +0 -1
  268. package/dist/lib/orchestrator-CjEau_RG.js.map +0 -1
  269. package/dist/lib/peerjs-BdfpltIe.js.map +0 -1
  270. package/dist/lib/shadow-claw-chat-mXPQZbwT.js.map +0 -1
  271. package/dist/lib/shadow-claw-file-viewer-BSbZvSl-.js.map +0 -1
  272. package/dist/lib/shadow-claw-tasks-qmcDT4PU.js.map +0 -1
  273. package/dist/lib/tools-D4Pp-nUw.js +0 -1
  274. package/dist/public/app-routes-Bs-riIvr.js +0 -1
  275. package/dist/public/orchestrator-BWkHHO9Y.js +0 -15
  276. package/dist/public/peerjs-tf2EmWjB.js +0 -1
  277. package/dist/public/tools-COduyYJ0.js +0 -1
@@ -6,6 +6,7 @@ import {
6
6
  DEFAULT_PROMPT_API_FALLBACK_MODEL,
7
7
  DEFAULT_PROVIDER,
8
8
  } from "../../config/config.js";
9
+ import { decodePathSegment } from "../../core/app-routes.js";
9
10
 
10
11
  import { shouldInstallE2eBridge } from "../../testing/e2e-bridge.js";
11
12
 
@@ -1160,7 +1161,10 @@ export class ShadowClawChat extends ShadowClawElement {
1160
1161
  return null;
1161
1162
  }
1162
1163
 
1163
- const parts = normalized.split("/").filter(Boolean);
1164
+ const parts = normalized
1165
+ .split("/")
1166
+ .filter(Boolean)
1167
+ .map((part) => decodePathSegment(part));
1164
1168
  if (parts.some((part) => part === "..")) {
1165
1169
  return null;
1166
1170
  }
@@ -1445,6 +1445,31 @@ describe("shadow-claw-file-viewer", () => {
1445
1445
  );
1446
1446
  });
1447
1447
 
1448
+ it("rewrites relative asset hrefs with spaces in markdown preview HTML", () => {
1449
+ const component = new ShadowClawFileViewer();
1450
+ const inputHtml =
1451
+ '<p><a href="./My%20File%20With%20Spaces%20In%20The%20Name.md">file</a></p>';
1452
+ const rewritten = component.rewriteWorkspacePreviewHtml(
1453
+ inputHtml,
1454
+ "index.md",
1455
+ );
1456
+ expect(rewritten).toContain(
1457
+ 'href="/files/test-group/My%20File%20With%20Spaces%20In%20The%20Name.md"',
1458
+ );
1459
+ });
1460
+
1461
+ it("rewrites relative asset hrefs in subfolders with spaces in markdown preview HTML", () => {
1462
+ const component = new ShadowClawFileViewer();
1463
+ const inputHtml = '<p><a href="sub%20folder/My%20File.md">file</a></p>';
1464
+ const rewritten = component.rewriteWorkspacePreviewHtml(
1465
+ inputHtml,
1466
+ "index.md",
1467
+ );
1468
+ expect(rewritten).toContain(
1469
+ 'href="/files/test-group/sub%20folder/My%20File.md"',
1470
+ );
1471
+ });
1472
+
1448
1473
  it("rewrites relative asset hrefs when subpath base path is active", () => {
1449
1474
  document.querySelectorAll("base").forEach((el) => el.remove());
1450
1475
  const baseEl = document.createElement("base");
@@ -1530,6 +1555,59 @@ describe("shadow-claw-file-viewer", () => {
1530
1555
  ).toBe("weather/README.md");
1531
1556
  });
1532
1557
 
1558
+ it("resolves files and subfolders with spaces (both literal and percent-encoded)", () => {
1559
+ const component = new ShadowClawFileViewer();
1560
+
1561
+ expect(
1562
+ component.resolveWorkspaceLinkPath(
1563
+ "My File With Spaces In The Name.md",
1564
+ "README.md",
1565
+ ),
1566
+ ).toBe("My File With Spaces In The Name.md");
1567
+
1568
+ expect(
1569
+ component.resolveWorkspaceLinkPath(
1570
+ "./My File With Spaces In The Name.md",
1571
+ "README.md",
1572
+ ),
1573
+ ).toBe("My File With Spaces In The Name.md");
1574
+
1575
+ expect(
1576
+ component.resolveWorkspaceLinkPath(
1577
+ "./My%20File%20With%20Spaces%20In%20The%20Name.md",
1578
+ "README.md",
1579
+ ),
1580
+ ).toBe("My File With Spaces In The Name.md");
1581
+
1582
+ expect(
1583
+ component.resolveWorkspaceLinkPath(
1584
+ "sub folder/My File With Spaces In The Name.md",
1585
+ "README.md",
1586
+ ),
1587
+ ).toBe("sub folder/My File With Spaces In The Name.md");
1588
+
1589
+ expect(
1590
+ component.resolveWorkspaceLinkPath(
1591
+ "sub%20folder/My%20File%20With%20Spaces%20In%20The%20Name.md",
1592
+ "README.md",
1593
+ ),
1594
+ ).toBe("sub folder/My File With Spaces In The Name.md");
1595
+
1596
+ expect(
1597
+ component.resolveWorkspaceLinkPath(
1598
+ "./child note.md",
1599
+ "my sub folder/README.md",
1600
+ ),
1601
+ ).toBe("my sub folder/child note.md");
1602
+
1603
+ expect(
1604
+ component.resolveWorkspaceLinkPath(
1605
+ "../other folder/doc.md",
1606
+ "my sub folder/README.md",
1607
+ ),
1608
+ ).toBe("other folder/doc.md");
1609
+ });
1610
+
1533
1611
  it("rejects external and root-escape links", () => {
1534
1612
  const component = new ShadowClawFileViewer();
1535
1613
 
@@ -6,6 +6,7 @@ import { splitFrontmatter } from "../../common/utils/frontmatter.mjs";
6
6
  import { CONFIG_KEYS } from "../../config/config.js";
7
7
  import {
8
8
  applyBasePath,
9
+ decodePathSegment,
9
10
  getFileRouteDirPath,
10
11
  getWorkspaceRouteRequestPath,
11
12
  isPossibleAppRoute,
@@ -1068,7 +1069,10 @@ export class ShadowClawFileViewer extends ShadowClawElement {
1068
1069
 
1069
1070
  if (!isAbsolute) {
1070
1071
  const baseNormalized = basePath.replace(/\\/g, "/").replace(/^\/+/, "");
1071
- const baseParts = baseNormalized.split("/").filter(Boolean);
1072
+ const baseParts = baseNormalized
1073
+ .split("/")
1074
+ .filter(Boolean)
1075
+ .map((segment) => decodePathSegment(segment));
1072
1076
  baseParts.pop();
1073
1077
  stack.push(...baseParts);
1074
1078
  }
@@ -1088,7 +1092,7 @@ export class ShadowClawFileViewer extends ShadowClawElement {
1088
1092
  continue;
1089
1093
  }
1090
1094
 
1091
- stack.push(part);
1095
+ stack.push(decodePathSegment(part));
1092
1096
  }
1093
1097
 
1094
1098
  return stack.length > 0 ? stack.join("/") : null;
@@ -76,4 +76,60 @@ describe("resolveWorkspaceLinkPath", () => {
76
76
  );
77
77
  expect(result).toBeNull();
78
78
  });
79
+
80
+ it("resolves files and subfolders with spaces (both literal and percent-encoded)", () => {
81
+ expect(
82
+ resolveWorkspaceLinkPath(
83
+ "My File With Spaces In The Name.md",
84
+ "index.md",
85
+ groupId,
86
+ origin,
87
+ ),
88
+ ).toBe("My File With Spaces In The Name.md");
89
+
90
+ expect(
91
+ resolveWorkspaceLinkPath(
92
+ "./My File With Spaces In The Name.md",
93
+ "index.md",
94
+ groupId,
95
+ origin,
96
+ ),
97
+ ).toBe("My File With Spaces In The Name.md");
98
+
99
+ expect(
100
+ resolveWorkspaceLinkPath(
101
+ "./My%20File%20With%20Spaces%20In%20The%20Name.md",
102
+ "index.md",
103
+ groupId,
104
+ origin,
105
+ ),
106
+ ).toBe("My File With Spaces In The Name.md");
107
+
108
+ expect(
109
+ resolveWorkspaceLinkPath(
110
+ "sub folder/My File With Spaces In The Name.md",
111
+ "index.md",
112
+ groupId,
113
+ origin,
114
+ ),
115
+ ).toBe("sub folder/My File With Spaces In The Name.md");
116
+
117
+ expect(
118
+ resolveWorkspaceLinkPath(
119
+ "sub%20folder/My%20File%20With%20Spaces%20In%20The%20Name.md",
120
+ "index.md",
121
+ groupId,
122
+ origin,
123
+ ),
124
+ ).toBe("sub folder/My File With Spaces In The Name.md");
125
+
126
+ expect(
127
+ resolveWorkspaceLinkPath(
128
+ "./child.md",
129
+ "sub folder/index.md",
130
+ groupId,
131
+ origin,
132
+ ),
133
+ ).toBe("sub folder/child.md");
134
+ });
79
135
  });
@@ -137,6 +137,28 @@ describe("shadow-claw-tasks", () => {
137
137
  ).toBe("weather/archive/index.md");
138
138
  });
139
139
 
140
+ it("resolves workspace links with spaces and percent-encoded spaces", () => {
141
+ const component = new ShadowClawTasks();
142
+
143
+ expect(
144
+ component.resolveWorkspaceLinkPath(
145
+ "My File With Spaces In The Name.md",
146
+ ),
147
+ ).toBe("My File With Spaces In The Name.md");
148
+
149
+ expect(
150
+ component.resolveWorkspaceLinkPath(
151
+ "./My%20File%20With%20Spaces%20In%20The%20Name.md",
152
+ ),
153
+ ).toBe("My File With Spaces In The Name.md");
154
+
155
+ expect(
156
+ component.resolveWorkspaceLinkPath(
157
+ "sub%20folder/My%20File%20With%20Spaces.md",
158
+ ),
159
+ ).toBe("sub folder/My File With Spaces.md");
160
+ });
161
+
140
162
  it("rejects external links and parent traversal", () => {
141
163
  const component = new ShadowClawTasks();
142
164
 
@@ -1,6 +1,7 @@
1
1
  import { renderMarkdown } from "../../content/markdown.js";
2
2
  import { CONFIG_KEYS } from "../../config/config.js";
3
3
  import { effect } from "../../core/effect.js";
4
+ import { decodePathSegment } from "../../core/app-routes.js";
4
5
 
5
6
  import { getDb, ShadowClawDatabase } from "../../db/db.js";
6
7
  import { getConfig } from "../../db/getConfig.js";
@@ -629,7 +630,10 @@ export class ShadowClawTasks extends ShadowClawElement {
629
630
  return null;
630
631
  }
631
632
 
632
- const parts = normalized.split("/").filter(Boolean);
633
+ const parts = normalized
634
+ .split("/")
635
+ .filter(Boolean)
636
+ .map((part) => decodePathSegment(part));
633
637
  if (parts.some((part) => part === "..")) {
634
638
  return null;
635
639
  }
@@ -0,0 +1,109 @@
1
+ import { normalizeMarkdownLinks } from "./markdown-links.js";
2
+
3
+ describe("normalizeMarkdownLinks", () => {
4
+ it("wraps relative link destinations with spaces in angle brackets", () => {
5
+ const input =
6
+ "[My File With Spaces In The Name.md](./My File With Spaces In The Name.md)";
7
+ const expected =
8
+ "[My File With Spaces In The Name.md](<./My File With Spaces In The Name.md>)";
9
+ expect(normalizeMarkdownLinks(input)).toBe(expected);
10
+ });
11
+
12
+ it("wraps bare filename destinations with spaces in angle brackets", () => {
13
+ const input =
14
+ "[My File With Spaces In The Name.md](My File With Spaces In The Name.md)";
15
+ const expected =
16
+ "[My File With Spaces In The Name.md](<My File With Spaces In The Name.md>)";
17
+ expect(normalizeMarkdownLinks(input)).toBe(expected);
18
+ });
19
+
20
+ it("wraps subfolder paths with spaces in angle brackets", () => {
21
+ const input =
22
+ "[My File](folder with spaces/My File With Spaces In The Name.md)";
23
+ const expected =
24
+ "[My File](<folder with spaces/My File With Spaces In The Name.md>)";
25
+ expect(normalizeMarkdownLinks(input)).toBe(expected);
26
+ });
27
+
28
+ it("wraps relative parent paths with spaces in angle brackets", () => {
29
+ const input =
30
+ "[My File](../parent folder/sub folder/My File With Spaces In The Name.md)";
31
+ const expected =
32
+ "[My File](<../parent folder/sub folder/My File With Spaces In The Name.md>)";
33
+ expect(normalizeMarkdownLinks(input)).toBe(expected);
34
+ });
35
+
36
+ it("wraps HTTP/HTTPS URLs with spaces in angle brackets", () => {
37
+ const input =
38
+ "[My File With Spaces In The Name.md](https://karlherrick.com/What About This)";
39
+ const expected =
40
+ "[My File With Spaces In The Name.md](<https://karlherrick.com/What About This>)";
41
+ expect(normalizeMarkdownLinks(input)).toBe(expected);
42
+ });
43
+
44
+ it("wraps image destinations with spaces in angle brackets", () => {
45
+ const input = "![My Image](./sub folder/My Image With Spaces.png)";
46
+ const expected = "![My Image](<./sub folder/My Image With Spaces.png>)";
47
+ expect(normalizeMarkdownLinks(input)).toBe(expected);
48
+ });
49
+
50
+ it("supports link titles when destination contains spaces", () => {
51
+ const input = '[My File](./My File With Spaces.md "File Title")';
52
+ const expected = '[My File](<./My File With Spaces.md> "File Title")';
53
+ expect(normalizeMarkdownLinks(input)).toBe(expected);
54
+ });
55
+
56
+ it("supports single-quoted link titles when destination contains spaces", () => {
57
+ const input = "[My File](./My File With Spaces.md 'File Title')";
58
+ const expected = "[My File](<./My File With Spaces.md> 'File Title')";
59
+ expect(normalizeMarkdownLinks(input)).toBe(expected);
60
+ });
61
+
62
+ it("does not modify links that are already wrapped in angle brackets", () => {
63
+ const input = "[My File](<./My File With Spaces.md>)";
64
+ expect(normalizeMarkdownLinks(input)).toBe(input);
65
+ });
66
+
67
+ it("does not modify links that have no spaces", () => {
68
+ const input = "[My File](./MyFile.md)";
69
+ expect(normalizeMarkdownLinks(input)).toBe(input);
70
+ });
71
+
72
+ it("preserves fenced code blocks without modifying link-like text inside them", () => {
73
+ const input = [
74
+ "Here is some code:",
75
+ "```markdown",
76
+ "[My File With Spaces In The Name.md](./My File With Spaces In The Name.md)",
77
+ "```",
78
+ "And a real link outside:",
79
+ "[My Real File](./Real File.md)",
80
+ ].join("\n");
81
+
82
+ const expected = [
83
+ "Here is some code:",
84
+ "```markdown",
85
+ "[My File With Spaces In The Name.md](./My File With Spaces In The Name.md)",
86
+ "```",
87
+ "And a real link outside:",
88
+ "[My Real File](<./Real File.md>)",
89
+ ].join("\n");
90
+
91
+ expect(normalizeMarkdownLinks(input)).toBe(expected);
92
+ });
93
+
94
+ it("preserves inline code spans without modifying link-like text inside them", () => {
95
+ const input =
96
+ "Inline: `[My File](./My File.md)` and link: [My Link](./My Link.md)";
97
+ const expected =
98
+ "Inline: `[My File](./My File.md)` and link: [My Link](<./My Link.md>)";
99
+ expect(normalizeMarkdownLinks(input)).toBe(expected);
100
+ });
101
+
102
+ it("handles multiple links on the same line", () => {
103
+ const input =
104
+ "See [File 1](File 1.md) and [File 2](File 2.md) for details.";
105
+ const expected =
106
+ "See [File 1](<File 1.md>) and [File 2](<File 2.md>) for details.";
107
+ expect(normalizeMarkdownLinks(input)).toBe(expected);
108
+ });
109
+ });
@@ -0,0 +1,187 @@
1
+ /**
2
+ * ShadowClaw — Markdown link destination normalizer
3
+ *
4
+ * CommonMark / Marked specifications require link destinations containing spaces
5
+ * to be enclosed in angle brackets `<...>` (e.g. `[text](<path with spaces.md>)`).
6
+ *
7
+ * This utility allows users to write natural Markdown links and image tags with spaces
8
+ * in filenames and URLs without manually typing `%20` or `<...>`:
9
+ * [Doc](./My File With Spaces.md) -> [Doc](<./My File With Spaces.md>)
10
+ * [Doc](folder/My File With Spaces.md) -> [Doc](<folder/My File With Spaces.md>)
11
+ * [Link](https://example.com/My Page) -> [Link](<https://example.com/My Page>)
12
+ * ![Img](./My Image.png) -> ![Img](<./My Image.png>)
13
+ *
14
+ * Code blocks and inline code spans are strictly preserved.
15
+ */
16
+
17
+ function normalizeDestination(rawInsideParens: string): string {
18
+ const trimmed = rawInsideParens.trim();
19
+ if (!trimmed) {
20
+ return rawInsideParens;
21
+ }
22
+
23
+ // Check for an optional quoted title at the end: "title" or 'title'
24
+ const titleMatch = trimmed.match(/\s+("[^"]*"|'[^']*')\s*$/);
25
+ let dest: string;
26
+ let titlePart = "";
27
+
28
+ if (titleMatch && typeof titleMatch.index === "number") {
29
+ dest = trimmed.slice(0, titleMatch.index).trim();
30
+ titlePart = ` ${titleMatch[1]}`;
31
+ } else {
32
+ dest = trimmed;
33
+ }
34
+
35
+ // If already wrapped in angle brackets or has no spaces, do not modify
36
+ if ((dest.startsWith("<") && dest.endsWith(">")) || !/[\s]/.test(dest)) {
37
+ return rawInsideParens;
38
+ }
39
+
40
+ return `<${dest}>${titlePart}`;
41
+ }
42
+
43
+ /**
44
+ * Normalizes link and image destinations containing spaces by enclosing them in CommonMark `<...>`.
45
+ */
46
+ export function normalizeMarkdownLinks(markdown: string): string {
47
+ if (typeof markdown !== "string" || !markdown.includes("[")) {
48
+ return markdown;
49
+ }
50
+
51
+ const length = markdown.length;
52
+ let result = "";
53
+ let i = 0;
54
+
55
+ while (i < length) {
56
+ // 1. Check for fenced code blocks at start of line: ``` or ~~~
57
+ const isStartOfLine = i === 0 || markdown[i - 1] === "\n";
58
+ if (isStartOfLine && (markdown[i] === "`" || markdown[i] === "~")) {
59
+ const char = markdown[i];
60
+ let fenceLen = 1;
61
+ while (i + fenceLen < length && markdown[i + fenceLen] === char) {
62
+ fenceLen++;
63
+ }
64
+
65
+ if (fenceLen >= 3) {
66
+ const fenceStr = char.repeat(fenceLen);
67
+ const endOfOpeningLine = markdown.indexOf("\n", i + fenceLen);
68
+ const searchStart =
69
+ endOfOpeningLine === -1 ? length : endOfOpeningLine + 1;
70
+ const closeIdx = markdown.indexOf(`\n${fenceStr}`, searchStart - 1);
71
+
72
+ if (closeIdx !== -1) {
73
+ const endOfClosingLine = markdown.indexOf(
74
+ "\n",
75
+ closeIdx + 1 + fenceLen,
76
+ );
77
+ const endIdx =
78
+ endOfClosingLine === -1 ? length : endOfClosingLine + 1;
79
+ result += markdown.slice(i, endIdx);
80
+ i = endIdx;
81
+ continue;
82
+ } else {
83
+ // Unclosed fence runs to the end
84
+ result += markdown.slice(i);
85
+ break;
86
+ }
87
+ }
88
+ }
89
+
90
+ // 2. Check for inline code spans: ` or `` etc.
91
+ if (markdown[i] === "`") {
92
+ let backtickCount = 1;
93
+ while (
94
+ i + backtickCount < length &&
95
+ markdown[i + backtickCount] === "`"
96
+ ) {
97
+ backtickCount++;
98
+ }
99
+ const backticks = "`".repeat(backtickCount);
100
+ const closeIdx = markdown.indexOf(backticks, i + backtickCount);
101
+
102
+ if (closeIdx !== -1) {
103
+ const endIdx = closeIdx + backtickCount;
104
+ result += markdown.slice(i, endIdx);
105
+ i = endIdx;
106
+ continue;
107
+ }
108
+ }
109
+
110
+ // 3. Check for link or image: ![...](...) or [...](...)
111
+ const isImage =
112
+ markdown[i] === "!" && i + 1 < length && markdown[i + 1] === "[";
113
+ const isLink = markdown[i] === "[";
114
+
115
+ if (isImage || isLink) {
116
+ const labelStart = isImage ? i + 2 : i + 1;
117
+ let depth = 1;
118
+ let j = labelStart;
119
+
120
+ while (j < length && depth > 0) {
121
+ if (markdown[j] === "\\") {
122
+ j += 2; // Skip escaped character
123
+ continue;
124
+ }
125
+ if (markdown[j] === "[") {
126
+ depth++;
127
+ } else if (markdown[j] === "]") {
128
+ depth--;
129
+ }
130
+ if (depth > 0) {
131
+ j++;
132
+ }
133
+ }
134
+
135
+ // If we found the matching ']' and the next non-whitespace or immediate char is '('
136
+ if (depth === 0 && j < length && markdown[j] === "]") {
137
+ const labelEnd = j;
138
+ const nextCharIdx = j + 1;
139
+
140
+ if (nextCharIdx < length && markdown[nextCharIdx] === "(") {
141
+ // Find matching ')'
142
+ let parenDepth = 1;
143
+ let k = nextCharIdx + 1;
144
+
145
+ while (k < length && parenDepth > 0) {
146
+ if (markdown[k] === "\\") {
147
+ k += 2;
148
+ continue;
149
+ }
150
+ if (markdown[k] === "(") {
151
+ parenDepth++;
152
+ } else if (markdown[k] === ")") {
153
+ parenDepth--;
154
+ }
155
+ if (parenDepth > 0) {
156
+ k++;
157
+ }
158
+ }
159
+
160
+ if (parenDepth === 0 && k < length && markdown[k] === ")") {
161
+ const rawInsideLabel = markdown.slice(labelStart, labelEnd);
162
+ const rawInsideParens = markdown.slice(nextCharIdx + 1, k);
163
+
164
+ // Recursively normalize label in case there are nested links/images
165
+ const normalizedLabel = rawInsideLabel.includes("[")
166
+ ? normalizeMarkdownLinks(rawInsideLabel)
167
+ : rawInsideLabel;
168
+
169
+ const normalizedInsideParens =
170
+ normalizeDestination(rawInsideParens);
171
+ const prefix = isImage ? "![" : "[";
172
+
173
+ result += `${prefix}${normalizedLabel}](${normalizedInsideParens})`;
174
+ i = k + 1;
175
+ continue;
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ // Default: append current character
182
+ result += markdown[i];
183
+ i++;
184
+ }
185
+
186
+ return result;
187
+ }
@@ -115,4 +115,37 @@ describe("renderMarkdown", () => {
115
115
  expect(html).not.toContain("<iframe");
116
116
  expect(html).not.toContain("phishing-site.example.com");
117
117
  });
118
+
119
+ it("renders markdown links with spaces in relative filenames and URLs", async () => {
120
+ const markdown = [
121
+ "[My File With Spaces In The Name.md](./My File With Spaces In The Name.md)",
122
+ "[My File With Spaces In The Name.md](My File With Spaces In The Name.md)",
123
+ "[Subfolder Doc](sub folder/My File With Spaces In The Name.md)",
124
+ "[External](https://karlherrick.com/What About This)",
125
+ ].join("\n\n");
126
+
127
+ const html = await renderMarkdown(markdown);
128
+
129
+ expect(html).toContain(
130
+ '<a href="./My%20File%20With%20Spaces%20In%20The%20Name.md">My File With Spaces In The Name.md</a>',
131
+ );
132
+ expect(html).toContain(
133
+ '<a href="My%20File%20With%20Spaces%20In%20The%20Name.md">My File With Spaces In The Name.md</a>',
134
+ );
135
+ expect(html).toContain(
136
+ '<a href="sub%20folder/My%20File%20With%20Spaces%20In%20The%20Name.md">Subfolder Doc</a>',
137
+ );
138
+ expect(html).toContain(
139
+ '<a href="https://karlherrick.com/What%20About%20This">External</a>',
140
+ );
141
+ });
142
+
143
+ it("renders markdown images with spaces in relative filenames", async () => {
144
+ const markdown = "![My Image](./sub folder/My Image With Spaces.png)";
145
+ const html = await renderMarkdown(markdown);
146
+
147
+ expect(html).toContain(
148
+ '<img src="./sub%20folder/My%20Image%20With%20Spaces.png" alt="My Image">',
149
+ );
150
+ });
118
151
  });
@@ -9,6 +9,8 @@
9
9
  import hljs from "highlight.js";
10
10
  import { marked } from "marked";
11
11
 
12
+ import { normalizeMarkdownLinks } from "./markdown-links.js";
13
+
12
14
  import {
13
15
  createFrontmatterDetailsElement,
14
16
  PostFrontmatter,
@@ -164,9 +166,12 @@ export async function renderMarkdown(
164
166
  const parsed: { data: PostFrontmatter; content: string } =
165
167
  splitFrontmatter(src);
166
168
 
169
+ // Normalize links and images with spaces in URLs
170
+ const normalizedContent = normalizeMarkdownLinks(parsed.content);
171
+
167
172
  // Parse markdown to HTML
168
173
  const headingCounts = new Map<string, number>();
169
- const html = await marked.parse(parsed.content, {
174
+ const html = await marked.parse(normalizedContent, {
170
175
  gfm: true,
171
176
  breaks: options?.breaks ?? false,
172
177
  headingCounts,
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  applyBasePath,
3
3
  buildRoutePath,
4
+ decodePathSegment,
4
5
  getAppBasePath,
5
6
  getDeploymentNamespace,
6
7
  getFileRouteDirPath,
@@ -146,6 +147,70 @@ describe("app-routes", () => {
146
147
  });
147
148
  });
148
149
 
150
+ it("decodes percent-encoded segments in getWorkspaceRouteRequestPath", () => {
151
+ expect(
152
+ getWorkspaceRouteRequestPath(
153
+ "/files/main/My%20File%20With%20Spaces%20In%20The%20Name.md",
154
+ ),
155
+ ).toEqual({
156
+ groupId: "br:main",
157
+ path: "My File With Spaces In The Name.md",
158
+ });
159
+
160
+ expect(
161
+ getWorkspaceRouteRequestPath(
162
+ "/files/main/sub%20folder/My%20File%20With%20Spaces%20In%20The%20Name.md",
163
+ ),
164
+ ).toEqual({
165
+ groupId: "br:main",
166
+ path: "sub folder/My File With Spaces In The Name.md",
167
+ });
168
+ });
169
+
170
+ describe("decodePathSegment", () => {
171
+ it("decodes percent-encoded strings", () => {
172
+ expect(decodePathSegment("My%20File.md")).toBe("My File.md");
173
+ expect(decodePathSegment("folder%20name")).toBe("folder name");
174
+ });
175
+
176
+ it("leaves unencoded strings untouched", () => {
177
+ expect(decodePathSegment("simple.md")).toBe("simple.md");
178
+ expect(decodePathSegment("already has spaces.md")).toBe(
179
+ "already has spaces.md",
180
+ );
181
+ });
182
+
183
+ it("safely handles malformed percent encoding without throwing", () => {
184
+ expect(decodePathSegment("100%real")).toBe("100%real");
185
+ });
186
+ });
187
+
188
+ it("parses route URLs with spaces in filenames and subfolders", () => {
189
+ const route = parseRouteFromUrl(
190
+ new URL(
191
+ "http://localhost:8888/files/main/My%20File%20With%20Spaces%20In%20The%20Name.md",
192
+ ),
193
+ );
194
+ expect(route).toEqual({
195
+ page: "files",
196
+ groupId: "br:main",
197
+ path: "My File With Spaces In The Name.md",
198
+ anchor: undefined,
199
+ });
200
+
201
+ const nestedRoute = parseRouteFromUrl(
202
+ new URL(
203
+ "http://localhost:8888/files/main/sub%20folder/My%20File%20With%20Spaces%20In%20The%20Name.md",
204
+ ),
205
+ );
206
+ expect(nestedRoute).toEqual({
207
+ page: "files",
208
+ groupId: "br:main",
209
+ path: "sub folder/My File With Spaces In The Name.md",
210
+ anchor: undefined,
211
+ });
212
+ });
213
+
149
214
  describe("applyBasePath", () => {
150
215
  // Reset cached base path between tests
151
216
  beforeEach(() => {