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.
Files changed (240) hide show
  1. package/.well-known/ai-catalog.json +11 -0
  2. package/.well-known/mcp/index.html +19 -0
  3. package/.well-known/mcp/server-card.json +52 -0
  4. package/.well-known/mcp/servers.json +15 -0
  5. package/.well-known/mcp.json +52 -0
  6. package/.well-known/oauth-protected-resource +10 -0
  7. package/.well-known/oauth-protected-resource.json +10 -0
  8. package/bin/assert-version-bump.mjs +61 -3
  9. package/bin/assert-version-bump.test.mjs +77 -1
  10. package/bin/build/build.mjs +116 -49
  11. package/bin/build/build.test.mjs +103 -0
  12. package/bin/cli.mjs +299 -237
  13. package/bin/cli.test.mjs +27 -0
  14. package/bin/commands/mcp.mjs +132 -2
  15. package/bin/commands/mcp.test.mjs +166 -0
  16. package/bin/commands/skills-index.mjs +139 -20
  17. package/bin/commands/skills-index.test.mjs +82 -0
  18. package/bin/utils/control-client.mjs +17 -0
  19. package/bin/utils/control-client.test.mjs +46 -0
  20. package/bin/version.mjs +208 -0
  21. package/bin/version.test.mjs +138 -0
  22. package/dist/electron/main.cjs +195 -185
  23. package/dist/lib/components/index.js +21 -21
  24. package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
  25. package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
  26. package/dist/lib/components/settings/shadow-claw-git.js +1 -1
  27. package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
  28. package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
  29. package/dist/lib/components/settings/shadow-claw-notifications.js +1 -1
  30. package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
  31. package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
  32. package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
  33. package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
  34. package/dist/lib/components/shadow-claw/shadow-claw.d.ts +1 -0
  35. package/dist/lib/components/shadow-claw/utils/setupAppLifecycle.d.ts +23 -0
  36. package/dist/lib/components/shadow-claw-channel-config.js +1 -1
  37. package/dist/lib/components/shadow-claw-channels.js +1 -1
  38. package/dist/lib/components/shadow-claw-chat.js +1 -1
  39. package/dist/lib/components/shadow-claw-control-plane.js +1 -1
  40. package/dist/lib/components/shadow-claw-conversations.js +1 -1
  41. package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
  42. package/dist/lib/components/shadow-claw-files.js +1 -1
  43. package/dist/lib/components/shadow-claw-git.js +1 -1
  44. package/dist/lib/components/shadow-claw-llm.js +1 -1
  45. package/dist/lib/components/shadow-claw-networking.js +1 -1
  46. package/dist/lib/components/shadow-claw-notifications.js +1 -1
  47. package/dist/lib/components/shadow-claw-pages.js +1 -1
  48. package/dist/lib/components/shadow-claw-peerjs.js +1 -1
  49. package/dist/lib/components/shadow-claw-settings.js +1 -1
  50. package/dist/lib/components/shadow-claw-storage.js +1 -1
  51. package/dist/lib/components/shadow-claw-task-server.js +1 -1
  52. package/dist/lib/components/shadow-claw-tasks.js +1 -1
  53. package/dist/lib/components/shadow-claw-terminal.js +1 -1
  54. package/dist/lib/components/shadow-claw-tools.js +1 -1
  55. package/dist/lib/components/shadow-claw-webvm.js +1 -1
  56. package/dist/lib/components/shadow-claw.js +1 -1
  57. package/dist/lib/{configurePeerJs-oODzltaC.js → configurePeerJs-D8Ph0-A9.js} +2 -2
  58. package/dist/lib/{configurePeerJs-oODzltaC.js.map → configurePeerJs-D8Ph0-A9.js.map} +1 -1
  59. package/dist/lib/core/control-plane-client.d.ts +2 -0
  60. package/dist/lib/core/orchestrator/orchestrator.d.ts +10 -0
  61. package/dist/lib/core/orchestrator/utils/operations/channel.d.ts +2 -2
  62. package/dist/lib/core/utils/initControlPlane.d.ts +6 -0
  63. package/dist/lib/index.js +21 -21
  64. package/dist/lib/{initControlPlane-C2zURVIS.js → initControlPlane-BIrWmpzc.js} +76 -9
  65. package/dist/lib/initControlPlane-BIrWmpzc.js.map +1 -0
  66. package/dist/lib/{markdown-C6EHl9zs.js → markdown-DEUoM1c6.js} +2 -2
  67. package/dist/lib/{markdown-C6EHl9zs.js.map → markdown-DEUoM1c6.js.map} +1 -1
  68. package/dist/lib/{orchestrator-KRjx0eba.js → orchestrator-BtVj9bBB.js} +11 -10
  69. package/dist/lib/{orchestrator-KRjx0eba.js.map → orchestrator-BtVj9bBB.js.map} +1 -1
  70. package/dist/lib/{orchestrator-CACLa_Ro.js → orchestrator-CFCW-l1O.js} +52 -4
  71. package/dist/lib/orchestrator-CFCW-l1O.js.map +1 -0
  72. package/dist/lib/{peerjs-DFWOBcXf.js → peerjs-BoBkx9DJ.js} +23 -3
  73. package/dist/lib/peerjs-BoBkx9DJ.js.map +1 -0
  74. package/dist/lib/{push-client-_tiWQsCT.js → push-client-D6hbzDNV.js} +10 -4
  75. package/dist/lib/push-client-D6hbzDNV.js.map +1 -0
  76. package/dist/lib/{shadow-claw-6UHdiy4a.js → shadow-claw-Cuf2O5bZ.js} +71 -4
  77. package/dist/lib/shadow-claw-Cuf2O5bZ.js.map +1 -0
  78. package/dist/lib/{shadow-claw-channel-config-DmfwJc6G.js → shadow-claw-channel-config-BDPKt6Bd.js} +3 -3
  79. package/dist/lib/{shadow-claw-channel-config-DmfwJc6G.js.map → shadow-claw-channel-config-BDPKt6Bd.js.map} +1 -1
  80. package/dist/lib/{shadow-claw-channels-1N6Jl-By.js → shadow-claw-channels-BsP5JABM.js} +2 -2
  81. package/dist/lib/{shadow-claw-channels-1N6Jl-By.js.map → shadow-claw-channels-BsP5JABM.js.map} +1 -1
  82. package/dist/lib/{shadow-claw-chat-BMoqQJRK.js → shadow-claw-chat-Cam2lX4k.js} +4 -4
  83. package/dist/lib/{shadow-claw-chat-BMoqQJRK.js.map → shadow-claw-chat-Cam2lX4k.js.map} +1 -1
  84. package/dist/lib/{shadow-claw-control-plane-UG0o7pCz.js → shadow-claw-control-plane-DB2qcCFs.js} +20 -10
  85. package/dist/lib/shadow-claw-control-plane-DB2qcCFs.js.map +1 -0
  86. package/dist/lib/{shadow-claw-conversations-BwXep3iu.js → shadow-claw-conversations-C9Vf4XGx.js} +2 -2
  87. package/dist/lib/{shadow-claw-conversations-BwXep3iu.js.map → shadow-claw-conversations-C9Vf4XGx.js.map} +1 -1
  88. package/dist/lib/{shadow-claw-file-viewer-DUxzVxDT.js → shadow-claw-file-viewer-B4HYiI0v.js} +3 -3
  89. package/dist/lib/{shadow-claw-file-viewer-DUxzVxDT.js.map → shadow-claw-file-viewer-B4HYiI0v.js.map} +1 -1
  90. package/dist/lib/{shadow-claw-files-DIREEnT2.js → shadow-claw-files-DVROUESO.js} +2 -2
  91. package/dist/lib/{shadow-claw-files-DIREEnT2.js.map → shadow-claw-files-DVROUESO.js.map} +1 -1
  92. package/dist/lib/{shadow-claw-git-Bg1SWgsF.js → shadow-claw-git-BqrO7cHJ.js} +3 -3
  93. package/dist/lib/{shadow-claw-git-Bg1SWgsF.js.map → shadow-claw-git-BqrO7cHJ.js.map} +1 -1
  94. package/dist/lib/{shadow-claw-llm-CazBrVpM.js → shadow-claw-llm-BexUQdYo.js} +2 -2
  95. package/dist/lib/{shadow-claw-llm-CazBrVpM.js.map → shadow-claw-llm-BexUQdYo.js.map} +1 -1
  96. package/dist/lib/{shadow-claw-networking-Df85tCbO.js → shadow-claw-networking-CfXRGpUD.js} +2 -2
  97. package/dist/lib/{shadow-claw-networking-Df85tCbO.js.map → shadow-claw-networking-CfXRGpUD.js.map} +1 -1
  98. package/dist/lib/{shadow-claw-notifications-v62FryTc.js → shadow-claw-notifications-C9KPMgx_.js} +2 -2
  99. package/dist/lib/{shadow-claw-notifications-v62FryTc.js.map → shadow-claw-notifications-C9KPMgx_.js.map} +1 -1
  100. package/dist/lib/{shadow-claw-pages-BgPyIEZO.js → shadow-claw-pages-CGzGXmrI.js} +3 -3
  101. package/dist/lib/{shadow-claw-pages-BgPyIEZO.js.map → shadow-claw-pages-CGzGXmrI.js.map} +1 -1
  102. package/dist/lib/{shadow-claw-peerjs-xkBXILed.js → shadow-claw-peerjs-B0ZB7yhB.js} +3 -3
  103. package/dist/lib/{shadow-claw-peerjs-xkBXILed.js.map → shadow-claw-peerjs-B0ZB7yhB.js.map} +1 -1
  104. package/dist/lib/{shadow-claw-settings-CN89xRVU.js → shadow-claw-settings-DBfMZdDS.js} +2 -2
  105. package/dist/lib/{shadow-claw-settings-CN89xRVU.js.map → shadow-claw-settings-DBfMZdDS.js.map} +1 -1
  106. package/dist/lib/{shadow-claw-storage-VnhcObCe.js → shadow-claw-storage-CM-w6mgG.js} +2 -2
  107. package/dist/lib/{shadow-claw-storage-VnhcObCe.js.map → shadow-claw-storage-CM-w6mgG.js.map} +1 -1
  108. package/dist/lib/{shadow-claw-task-server-jgEC2hHB.js → shadow-claw-task-server-DV4b1g2-.js} +4 -2
  109. package/dist/lib/{shadow-claw-task-server-jgEC2hHB.js.map → shadow-claw-task-server-DV4b1g2-.js.map} +1 -1
  110. package/dist/lib/{shadow-claw-tasks-i_evIufy.js → shadow-claw-tasks-K-foOHSE.js} +3 -3
  111. package/dist/lib/{shadow-claw-tasks-i_evIufy.js.map → shadow-claw-tasks-K-foOHSE.js.map} +1 -1
  112. package/dist/lib/{shadow-claw-terminal-CwScROlu.js → shadow-claw-terminal-CpAknKyA.js} +2 -2
  113. package/dist/lib/{shadow-claw-terminal-CwScROlu.js.map → shadow-claw-terminal-CpAknKyA.js.map} +1 -1
  114. package/dist/lib/{shadow-claw-tools-DD3fyXP4.js → shadow-claw-tools-D98XZ0Ep.js} +3 -3
  115. package/dist/lib/{shadow-claw-tools-DD3fyXP4.js.map → shadow-claw-tools-D98XZ0Ep.js.map} +1 -1
  116. package/dist/lib/{shadow-claw-webvm-DbJq4I3-.js → shadow-claw-webvm-DLOb8dMI.js} +2 -2
  117. package/dist/lib/{shadow-claw-webvm-DbJq4I3-.js.map → shadow-claw-webvm-DLOb8dMI.js.map} +1 -1
  118. package/dist/lib/subsystems/channels/imessage.d.ts +1 -0
  119. package/dist/lib/subsystems/channels/peerjs.d.ts +1 -0
  120. package/dist/lib/subsystems/channels/telegram.d.ts +1 -0
  121. package/dist/lib/subsystems/channels/types.d.ts +2 -0
  122. package/dist/lib/subsystems/notifications/push-client.d.ts +1 -1
  123. package/dist/lib/{syncWebMcpRegistration-DNLnQ-jz.js → syncWebMcpRegistration-NspKefOI.js} +2 -2
  124. package/dist/lib/{syncWebMcpRegistration-DNLnQ-jz.js.map → syncWebMcpRegistration-NspKefOI.js.map} +1 -1
  125. package/dist/public/.well-known/ai-catalog.json +11 -0
  126. package/dist/public/.well-known/mcp/index.html +19 -0
  127. package/dist/public/.well-known/mcp/server-card.json +52 -0
  128. package/dist/public/.well-known/mcp/servers.json +15 -0
  129. package/dist/public/.well-known/mcp.json +52 -0
  130. package/dist/public/.well-known/oauth-protected-resource +10 -0
  131. package/dist/public/.well-known/oauth-protected-resource.json +10 -0
  132. package/dist/public/404.html +28 -1
  133. package/dist/public/AGENTS.md +1 -1
  134. package/dist/public/agent.worker.js +1 -1
  135. package/dist/public/{configurePeerJs-BqgeBjP7.js → configurePeerJs-BhBokQlC.js} +1 -1
  136. package/dist/public/docs/example/article/index.html +3 -3
  137. package/dist/public/docs/publishing/index.html +3 -3
  138. package/dist/public/docs/skill-creator/index.html +3 -3
  139. package/dist/public/docs/subsystems/cli.md +11 -8
  140. package/dist/public/docs/subsystems/mcp-server.md +1 -0
  141. package/dist/public/docs/subsystems/notifications.md +10 -0
  142. package/dist/public/{e2e-bridge-Ceqr5kGc.js → e2e-bridge-DcBoeFvd.js} +1 -1
  143. package/dist/public/{file-viewer-C-6Jc_tb.js → file-viewer-CKtVC9Ix.js} +1 -1
  144. package/dist/public/index.css +1 -1
  145. package/dist/public/index.html +3 -3
  146. package/dist/public/index.js +1 -1
  147. package/dist/public/initControlPlane-Bi8ouFEb.js +1 -0
  148. package/dist/public/llms.txt +36 -1
  149. package/dist/public/main/index.html +3 -3
  150. package/dist/public/main/memory/index.html +3 -3
  151. package/dist/public/{markdown-C3M4OW_A.js → markdown-C-hCVJtc.js} +1 -1
  152. package/dist/public/orchestrator-8qNGfw8r.js +15 -0
  153. package/dist/public/{orchestrator-DHjdLtqa.js → orchestrator-BT3Ks5ip.js} +2 -2
  154. package/dist/public/peerjs-DfhwEiiq.js +1 -0
  155. package/dist/public/push-client-BVLrE9pK.js +1 -0
  156. package/dist/public/service-worker/fetch-proxy.js +1 -1
  157. package/dist/public/service-worker.js +1 -1
  158. package/dist/public/{shadow-claw-EAt2ml5Y.js → shadow-claw-DCjxNDCX.js} +3 -3
  159. package/dist/public/{shadow-claw-channels-XsfuWFF8.js → shadow-claw-channels-CITDrqMl.js} +1 -1
  160. package/dist/public/{shadow-claw-chat-CbSaOZ4q.js → shadow-claw-chat-BQrSgSKF.js} +1 -1
  161. package/dist/public/{shadow-claw-control-plane-DHvN3Ukg.js → shadow-claw-control-plane-Cp001OY0.js} +2 -2
  162. package/dist/public/{shadow-claw-conversations-BWs5VVfs.js → shadow-claw-conversations-EhX4RvIj.js} +1 -1
  163. package/dist/public/{shadow-claw-file-viewer-BWsoW5QS.js → shadow-claw-file-viewer-DiQXIjZD.js} +1 -1
  164. package/dist/public/{shadow-claw-files-CG7MqWiV.js → shadow-claw-files-lkTnSJsn.js} +1 -1
  165. package/dist/public/{shadow-claw-git-DBRPZNav.js → shadow-claw-git-BlcA_z8x.js} +2 -2
  166. package/dist/public/{shadow-claw-llm-DMvLgKXo.js → shadow-claw-llm-CW16X9Rg.js} +1 -1
  167. package/dist/public/{shadow-claw-networking-Dd8Rw31D.js → shadow-claw-networking-ySsMBY_i.js} +1 -1
  168. package/dist/public/{shadow-claw-notifications-Dbevj-lq.js → shadow-claw-notifications-CyV2zCQ8.js} +1 -1
  169. package/dist/public/{shadow-claw-pages-3_lQAv0t.js → shadow-claw-pages-Cx7cF9ah.js} +1 -1
  170. package/dist/public/{shadow-claw-settings-t8TIlgrM.js → shadow-claw-settings-zm4PHHGi.js} +2 -2
  171. package/dist/public/{shadow-claw-storage-CfEXoSIE.js → shadow-claw-storage-dy9JAEGd.js} +1 -1
  172. package/dist/public/{shadow-claw-task-server-DxQSsgRZ.js → shadow-claw-task-server-Bleww48s.js} +2 -2
  173. package/dist/public/{shadow-claw-tasks-DNpicQwz.js → shadow-claw-tasks-CTI5oaLN.js} +1 -1
  174. package/dist/public/{shadow-claw-terminal-DpY6zU_r.js → shadow-claw-terminal-mZ3i4HaQ.js} +1 -1
  175. package/dist/public/{shadow-claw-tools-WSwWohnk.js → shadow-claw-tools-B_nuk8Zl.js} +1 -1
  176. package/dist/public/{shadow-claw-webvm-CP6oFjg5.js → shadow-claw-webvm-Vl_jH5YL.js} +1 -1
  177. package/dist/public/{syncWebMcpRegistration-DOn9gLnD.js → syncWebMcpRegistration-n2vAMJ4w.js} +1 -1
  178. package/dist/server.js +197 -187
  179. package/package.json +2 -1
  180. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +6 -0
  181. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +19 -9
  182. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +14 -0
  183. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +12 -0
  184. package/src/components/shadow-claw/shadow-claw.ts +6 -0
  185. package/src/components/shadow-claw/utils/bindEventListeners.test.ts +57 -0
  186. package/src/components/shadow-claw/utils/bindEventListeners.ts +14 -0
  187. package/src/components/shadow-claw/utils/setupAppLifecycle.test.ts +201 -0
  188. package/src/components/shadow-claw/utils/setupAppLifecycle.ts +101 -0
  189. package/src/core/control-plane-client.test.ts +82 -1
  190. package/src/core/control-plane-client.ts +35 -1
  191. package/src/core/orchestrator/orchestrator.test.ts +49 -0
  192. package/src/core/orchestrator/orchestrator.ts +43 -0
  193. package/src/core/orchestrator/utils/operations/channel.test.ts +17 -0
  194. package/src/core/orchestrator/utils/operations/channel.ts +11 -5
  195. package/src/core/orchestrator/utils/operations/task.test.ts +26 -0
  196. package/src/core/orchestrator/utils/operations/task.ts +8 -1
  197. package/src/core/utils/initControlPlane.test.ts +138 -0
  198. package/src/core/utils/initControlPlane.ts +82 -0
  199. package/src/core/utils/initializeApp.test.ts +27 -1
  200. package/src/core/utils/initializeApp.ts +6 -13
  201. package/src/server/mcp/mcp-server.test.ts +6 -0
  202. package/src/server/mcp/mcp-server.ts +2 -1
  203. package/src/server/mcp/tools/built-in-tools.ts +88 -1
  204. package/src/server/mcp/tools/client-tool-relay.ts +6 -0
  205. package/src/server/mcp/tools/mcp-tools.test.ts +128 -0
  206. package/src/server/openapi/openapi-coverage.test.ts +2 -1
  207. package/src/server/openapi/openapi-spec.ts +2 -1
  208. package/src/server/openapi/paths/mcp.ts +104 -0
  209. package/src/server/openapi/paths/push.ts +3 -0
  210. package/src/server/routes/mcp.test.ts +92 -0
  211. package/src/server/routes/mcp.ts +138 -2
  212. package/src/server/utils/packageVersion.test.ts +10 -0
  213. package/src/server/utils/packageVersion.ts +32 -0
  214. package/src/service-worker/fetch-proxy-rules.test.ts +73 -0
  215. package/src/service-worker/fetch-proxy-rules.ts +44 -0
  216. package/src/service-worker/fetch-proxy.ts +1 -1
  217. package/src/subsystems/channels/imessage.test.ts +36 -0
  218. package/src/subsystems/channels/imessage.ts +11 -0
  219. package/src/subsystems/channels/peerjs.test.ts +56 -0
  220. package/src/subsystems/channels/peerjs.ts +29 -0
  221. package/src/subsystems/channels/telegram.test.ts +38 -0
  222. package/src/subsystems/channels/telegram.ts +13 -0
  223. package/src/subsystems/channels/types.ts +2 -0
  224. package/src/subsystems/notifications/push-client.test.ts +70 -0
  225. package/src/subsystems/notifications/push-client.ts +28 -3
  226. package/src/subsystems/notifications/push-routes.test.ts +82 -0
  227. package/src/subsystems/notifications/push-routes.ts +70 -16
  228. package/src/subsystems/notifications/push-store.test.ts +59 -0
  229. package/src/subsystems/notifications/push-store.ts +113 -19
  230. package/src/testing/jest-setup.ts +15 -0
  231. package/dist/lib/initControlPlane-C2zURVIS.js.map +0 -1
  232. package/dist/lib/orchestrator-CACLa_Ro.js.map +0 -1
  233. package/dist/lib/peerjs-DFWOBcXf.js.map +0 -1
  234. package/dist/lib/push-client-_tiWQsCT.js.map +0 -1
  235. package/dist/lib/shadow-claw-6UHdiy4a.js.map +0 -1
  236. package/dist/lib/shadow-claw-control-plane-UG0o7pCz.js.map +0 -1
  237. package/dist/public/initControlPlane-BN9cWyO0.js +0 -1
  238. package/dist/public/orchestrator-CMbCTjBf.js +0 -15
  239. package/dist/public/peerjs-DUCKH-m9.js +0 -1
  240. package/dist/public/push-client-D0lkwrK0.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadow-claw",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0",
6
6
  "description": "Browser-native personal AI assistant.",
@@ -55,6 +55,7 @@
55
55
  },
56
56
  "homepage": "https://github.com/xt-ml/shadow-claw#readme",
57
57
  "scripts": {
58
+ "version": "node bin/version.mjs",
58
59
  "prepack": "npm run build:prod",
59
60
  "assert:version": "node bin/assert-version-bump.mjs",
60
61
  "build:clean": "node bin/rimraf.mjs dist && node bin/rimraf.mjs dist-electron",
@@ -60,6 +60,12 @@ describe("shadow-claw-control-plane", () => {
60
60
  expect(getControlPlaneTargetAddressSpace("https://172.20.0.1:8888")).toBe(
61
61
  "private",
62
62
  );
63
+ expect(getControlPlaneTargetAddressSpace("https://hostname:8888")).toBe(
64
+ "private",
65
+ );
66
+ expect(
67
+ getControlPlaneTargetAddressSpace("https://hostname.local:8888"),
68
+ ).toBe("private");
63
69
  expect(
64
70
  getControlPlaneTargetAddressSpace("https://my-server.example.com"),
65
71
  ).toBeUndefined();
@@ -37,9 +37,13 @@ export function getControlPlaneTargetAddressSpace(
37
37
  if (isLoopback) {
38
38
  return "loopback";
39
39
  }
40
- const isPrivate = /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(
41
- hostname,
42
- );
40
+ const isPrivate =
41
+ /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(hostname) ||
42
+ !hostname.includes(".") ||
43
+ hostname.endsWith(".local") ||
44
+ hostname.endsWith(".lan") ||
45
+ hostname.endsWith(".home") ||
46
+ hostname.endsWith(".internal");
43
47
  if (isPrivate) {
44
48
  return "private";
45
49
  }
@@ -309,7 +313,7 @@ export class ShadowClawControlPlane extends ShadowClawElement {
309
313
 
310
314
  const {
311
315
  isControlPlaneEnabled,
312
- createDefaultControlPlaneClient,
316
+ ensureControlPlaneConnected,
313
317
  shouldConnectControlPlane,
314
318
  } = await import("../../../core/utils/initControlPlane.js");
315
319
 
@@ -317,8 +321,10 @@ export class ShadowClawControlPlane extends ShadowClawElement {
317
321
  (await isControlPlaneEnabled(this.db)) &&
318
322
  shouldConnectControlPlane()
319
323
  ) {
320
- createDefaultControlPlaneClient({
324
+ await ensureControlPlaneConnected({
321
325
  orchestrator: this.orchestrator || undefined,
326
+ db: this.db,
327
+ force: true,
322
328
  transport,
323
329
  });
324
330
  }
@@ -371,7 +377,7 @@ export class ShadowClawControlPlane extends ShadowClawElement {
371
377
 
372
378
  const {
373
379
  isControlPlaneEnabled,
374
- createDefaultControlPlaneClient,
380
+ ensureControlPlaneConnected,
375
381
  shouldConnectControlPlane,
376
382
  } = await import("../../../core/utils/initControlPlane.js");
377
383
 
@@ -379,8 +385,10 @@ export class ShadowClawControlPlane extends ShadowClawElement {
379
385
  (await isControlPlaneEnabled(this.db)) &&
380
386
  shouldConnectControlPlane()
381
387
  ) {
382
- createDefaultControlPlaneClient({
388
+ await ensureControlPlaneConnected({
383
389
  orchestrator: this.orchestrator || undefined,
390
+ db: this.db,
391
+ force: true,
384
392
  });
385
393
  }
386
394
 
@@ -424,15 +432,17 @@ export class ShadowClawControlPlane extends ShadowClawElement {
424
432
  }
425
433
 
426
434
  const {
427
- createDefaultControlPlaneClient,
435
+ ensureControlPlaneConnected,
428
436
  stopControlPlaneClient,
429
437
  shouldConnectControlPlane,
430
438
  } = await import("../../../core/utils/initControlPlane.js");
431
439
 
432
440
  if (enabled) {
433
441
  if (shouldConnectControlPlane()) {
434
- createDefaultControlPlaneClient({
442
+ await ensureControlPlaneConnected({
435
443
  orchestrator: this.orchestrator || undefined,
444
+ db: this.db,
445
+ force: true,
436
446
  });
437
447
  }
438
448
  showSuccess("Control plane client enabled and connected", 3000);
@@ -21,11 +21,15 @@ jest.unstable_mockModule(
21
21
  }),
22
22
  );
23
23
 
24
+ const mockEnsureAllConnections = jest
25
+ .fn()
26
+ .mockResolvedValue(undefined as never);
24
27
  jest.unstable_mockModule("../../../stores/orchestrator.js", () => ({
25
28
  orchestratorStore: {
26
29
  orchestrator: {
27
30
  taskServerUrl: "/test-schedule",
28
31
  taskServerEnabled: true,
32
+ ensureAllConnections: mockEnsureAllConnections,
29
33
  },
30
34
  ready: true,
31
35
  },
@@ -81,6 +85,7 @@ describe("shadow-claw-task-server", () => {
81
85
  "https://new-server.com",
82
86
  );
83
87
  expect(showSuccess).toHaveBeenCalled();
88
+ expect(mockEnsureAllConnections).toHaveBeenCalled();
84
89
  });
85
90
 
86
91
  it("should save task server enabled toggle", async () => {
@@ -95,5 +100,14 @@ describe("shadow-claw-task-server", () => {
95
100
  expect.anything(),
96
101
  false,
97
102
  );
103
+
104
+ mockEnsureAllConnections.mockClear();
105
+ await el.saveTaskServerEnabled(true);
106
+ expect(mockSetTaskServerEnabled).toHaveBeenCalledWith(
107
+ orchestratorStore.orchestrator,
108
+ expect.anything(),
109
+ true,
110
+ );
111
+ expect(mockEnsureAllConnections).toHaveBeenCalled();
98
112
  });
99
113
  });
@@ -108,6 +108,12 @@ export class ShadowClawTaskServer extends ShadowClawElement {
108
108
 
109
109
  try {
110
110
  await setTaskServerEnabled(this.orchestrator, this.db, enabled);
111
+ if (
112
+ enabled &&
113
+ typeof this.orchestrator.ensureAllConnections === "function"
114
+ ) {
115
+ void this.orchestrator.ensureAllConnections();
116
+ }
111
117
  } catch (err) {
112
118
  const errorMsg = err instanceof Error ? err.message : String(err);
113
119
  showError("Error saving Task Server setting: " + errorMsg, 6000);
@@ -135,6 +141,12 @@ export class ShadowClawTaskServer extends ShadowClawElement {
135
141
 
136
142
  try {
137
143
  await setTaskServerUrl(this.orchestrator, this.db, url || "/schedule");
144
+ if (
145
+ this.orchestrator.taskServerEnabled &&
146
+ typeof this.orchestrator.ensureAllConnections === "function"
147
+ ) {
148
+ void this.orchestrator.ensureAllConnections();
149
+ }
138
150
  showSuccess("Task Server URL saved", 3000);
139
151
  } catch (err) {
140
152
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -88,6 +88,7 @@ export class ShadowClaw extends ShadowClawElement {
88
88
  };
89
89
 
90
90
  vmStatusCleanup: (() => void) | null = null;
91
+ appLifecycleCleanup: (() => void) | null = null;
91
92
 
92
93
  constructor() {
93
94
  super();
@@ -251,6 +252,11 @@ export class ShadowClaw extends ShadowClawElement {
251
252
  this.vmStatusCleanup = null;
252
253
  }
253
254
 
255
+ if (this.appLifecycleCleanup) {
256
+ this.appLifecycleCleanup();
257
+ this.appLifecycleCleanup = null;
258
+ }
259
+
254
260
  if (this.terminalPlacementFrame !== null) {
255
261
  cancelAnimationFrame(this.terminalPlacementFrame);
256
262
  this.terminalPlacementFrame = null;
@@ -27,6 +27,8 @@ describe("bindEventListeners", () => {
27
27
  let mockUpdateHeaderMainToggle: jest.Mock<any>;
28
28
  let mockUpdateHostTheme: jest.Mock<any>;
29
29
  let mockUpdateThemeIcons: jest.Mock<any>;
30
+ let mockSetupAppLifecycle: jest.Mock<any>;
31
+ let mockLifecycleCleanup: jest.Mock<any>;
30
32
 
31
33
  beforeEach(async () => {
32
34
  jest.resetModules();
@@ -54,6 +56,11 @@ describe("bindEventListeners", () => {
54
56
  jest.unstable_mockModule("./supportsNavigationApi.js", () => ({
55
57
  supportsNavigationApi: jest.fn(),
56
58
  }));
59
+ mockLifecycleCleanup = jest.fn();
60
+ mockSetupAppLifecycle = jest.fn(() => mockLifecycleCleanup);
61
+ jest.unstable_mockModule("./setupAppLifecycle.js", () => ({
62
+ setupAppLifecycle: mockSetupAppLifecycle,
63
+ }));
57
64
  jest.unstable_mockModule(
58
65
  "./syncPageHeaderMainVisibilityOverride.js",
59
66
  () => ({
@@ -940,4 +947,54 @@ describe("bindEventListeners", () => {
940
947
  );
941
948
  });
942
949
  });
950
+
951
+ describe("app lifecycle", () => {
952
+ it("should set up app lifecycle listener when orchestrator is present", () => {
953
+ shadowClaw.orchestrator = { ensureAllConnections: jest.fn() };
954
+
955
+ bindEventListeners(
956
+ win,
957
+ doc,
958
+ shadow,
959
+ shadowClaw,
960
+ db,
961
+ oStore,
962
+ fStore,
963
+ tStore,
964
+ url,
965
+ );
966
+
967
+ expect(mockSetupAppLifecycle).toHaveBeenCalledWith(
968
+ win,
969
+ doc,
970
+ shadowClaw.orchestrator,
971
+ );
972
+ expect(shadowClaw.appLifecycleCleanup).toBe(mockLifecycleCleanup);
973
+ });
974
+
975
+ it("should clean up previous app lifecycle listener before setting up new one", () => {
976
+ const prevCleanup = jest.fn();
977
+ shadowClaw.appLifecycleCleanup = prevCleanup;
978
+ shadowClaw.orchestrator = { ensureAllConnections: jest.fn() };
979
+
980
+ bindEventListeners(
981
+ win,
982
+ doc,
983
+ shadow,
984
+ shadowClaw,
985
+ db,
986
+ oStore,
987
+ fStore,
988
+ tStore,
989
+ url,
990
+ );
991
+
992
+ expect(prevCleanup).toHaveBeenCalled();
993
+ expect(mockSetupAppLifecycle).toHaveBeenCalledWith(
994
+ win,
995
+ doc,
996
+ shadowClaw.orchestrator,
997
+ );
998
+ });
999
+ });
943
1000
  });
@@ -10,6 +10,7 @@ import {
10
10
  } from "./setPagesSidebarHidden.js";
11
11
  import { showPage } from "./showPage.js";
12
12
  import { supportsNavigationApi } from "./supportsNavigationApi.js";
13
+ import { setupAppLifecycle } from "./setupAppLifecycle.js";
13
14
  import { syncPageHeaderMainVisibilityOverride } from "./syncPageHeaderMainVisibilityOverride.js";
14
15
 
15
16
  import { toggleActivityLogVisibility } from "./toggleActivityLogVisibility.js";
@@ -298,4 +299,17 @@ export function bindEventListeners(
298
299
  const currentTheme = tStore.resolved;
299
300
  updateThemeIcons(shadow, currentTheme);
300
301
  updateHostTheme(currentTheme, shadowClaw.classList);
302
+
303
+ if (shadowClaw.appLifecycleCleanup) {
304
+ shadowClaw.appLifecycleCleanup();
305
+ shadowClaw.appLifecycleCleanup = null;
306
+ }
307
+
308
+ if (shadowClaw.orchestrator) {
309
+ shadowClaw.appLifecycleCleanup = setupAppLifecycle(
310
+ win,
311
+ doc,
312
+ shadowClaw.orchestrator,
313
+ );
314
+ }
301
315
  }
@@ -0,0 +1,201 @@
1
+ import { jest } from "@jest/globals";
2
+ import { setupAppLifecycle } from "./setupAppLifecycle.js";
3
+
4
+ describe("setupAppLifecycle", () => {
5
+ let win: any;
6
+ let doc: any;
7
+ let orchestrator: any;
8
+ let winListeners: Record<string, Function>;
9
+ let docListeners: Record<string, Function>;
10
+
11
+ beforeEach(() => {
12
+ jest.useFakeTimers();
13
+ winListeners = {};
14
+ docListeners = {};
15
+
16
+ win = {
17
+ addEventListener: jest.fn((event: string, handler: Function) => {
18
+ winListeners[event] = handler;
19
+ }),
20
+ removeEventListener: jest.fn((event: string) => {
21
+ delete winListeners[event];
22
+ }),
23
+ };
24
+
25
+ doc = {
26
+ visibilityState: "visible",
27
+ addEventListener: jest.fn((event: string, handler: Function) => {
28
+ docListeners[event] = handler;
29
+ }),
30
+ removeEventListener: jest.fn((event: string) => {
31
+ delete docListeners[event];
32
+ }),
33
+ };
34
+
35
+ orchestrator = {
36
+ ensureAllConnections: jest.fn().mockResolvedValue(undefined as never),
37
+ };
38
+ });
39
+
40
+ afterEach(() => {
41
+ jest.useRealTimers();
42
+ });
43
+
44
+ it("registers event listeners on win and doc", () => {
45
+ const cleanup = setupAppLifecycle(win, doc, orchestrator);
46
+
47
+ expect(doc.addEventListener).toHaveBeenCalledWith(
48
+ "visibilitychange",
49
+ expect.any(Function),
50
+ );
51
+ expect(win.addEventListener).toHaveBeenCalledWith(
52
+ "pageshow",
53
+ expect.any(Function),
54
+ );
55
+ expect(win.addEventListener).toHaveBeenCalledWith(
56
+ "focus",
57
+ expect.any(Function),
58
+ );
59
+ expect(win.addEventListener).toHaveBeenCalledWith(
60
+ "online",
61
+ expect.any(Function),
62
+ );
63
+
64
+ cleanup();
65
+
66
+ expect(doc.removeEventListener).toHaveBeenCalledWith(
67
+ "visibilitychange",
68
+ expect.any(Function),
69
+ );
70
+ expect(win.removeEventListener).toHaveBeenCalledWith(
71
+ "pageshow",
72
+ expect.any(Function),
73
+ );
74
+ expect(win.removeEventListener).toHaveBeenCalledWith(
75
+ "focus",
76
+ expect.any(Function),
77
+ );
78
+ expect(win.removeEventListener).toHaveBeenCalledWith(
79
+ "online",
80
+ expect.any(Function),
81
+ );
82
+ });
83
+
84
+ it("triggers ensureAllConnections({ force: true }) on wake up from sleep (> 1000ms hidden)", () => {
85
+ setupAppLifecycle(win, doc, orchestrator, {
86
+ debounceMs: 100,
87
+ minSleepDurationMs: 1000,
88
+ });
89
+
90
+ // App goes to sleep / hidden
91
+ doc.visibilityState = "hidden";
92
+ docListeners["visibilitychange"]();
93
+
94
+ // Advance time by 5 seconds while phone sleeps
95
+ jest.advanceTimersByTime(5000);
96
+
97
+ // App wakes up
98
+ doc.visibilityState = "visible";
99
+ docListeners["visibilitychange"]();
100
+
101
+ // Before debounce timer fires
102
+ expect(orchestrator.ensureAllConnections).not.toHaveBeenCalled();
103
+
104
+ // Advance past debounce
105
+ jest.advanceTimersByTime(100);
106
+
107
+ expect(orchestrator.ensureAllConnections).toHaveBeenCalledWith({
108
+ force: true,
109
+ });
110
+ });
111
+
112
+ it("triggers ensureAllConnections({ force: false }) on short tab switch (< 1000ms hidden)", () => {
113
+ setupAppLifecycle(win, doc, orchestrator, {
114
+ debounceMs: 100,
115
+ minSleepDurationMs: 1000,
116
+ });
117
+
118
+ doc.visibilityState = "hidden";
119
+ docListeners["visibilitychange"]();
120
+
121
+ // Only hidden for 200ms
122
+ jest.advanceTimersByTime(200);
123
+
124
+ doc.visibilityState = "visible";
125
+ docListeners["visibilitychange"]();
126
+
127
+ jest.advanceTimersByTime(100);
128
+
129
+ expect(orchestrator.ensureAllConnections).toHaveBeenCalledWith({
130
+ force: false,
131
+ });
132
+ });
133
+
134
+ it("coalesces rapid resume events (visibilitychange, pageshow, focus, online)", () => {
135
+ setupAppLifecycle(win, doc, orchestrator, {
136
+ debounceMs: 250,
137
+ minSleepDurationMs: 1000,
138
+ });
139
+
140
+ doc.visibilityState = "hidden";
141
+ docListeners["visibilitychange"]();
142
+ jest.advanceTimersByTime(2000);
143
+
144
+ // Device wakes up
145
+ doc.visibilityState = "visible";
146
+ docListeners["visibilitychange"]();
147
+
148
+ // Chrome fires pageshow, focus, online right after
149
+ jest.advanceTimersByTime(20);
150
+ winListeners["pageshow"]?.({ persisted: true });
151
+
152
+ jest.advanceTimersByTime(20);
153
+ winListeners["focus"]?.();
154
+
155
+ jest.advanceTimersByTime(20);
156
+ winListeners["online"]?.();
157
+
158
+ // Total 60ms elapsed, still within 250ms debounce
159
+ expect(orchestrator.ensureAllConnections).not.toHaveBeenCalled();
160
+
161
+ // Advance remaining debounce time
162
+ jest.advanceTimersByTime(250);
163
+
164
+ // Should only be called ONCE
165
+ expect(orchestrator.ensureAllConnections).toHaveBeenCalledTimes(1);
166
+ expect(orchestrator.ensureAllConnections).toHaveBeenCalledWith({
167
+ force: true,
168
+ });
169
+ });
170
+
171
+ it("cancels pending reconnect if app goes hidden again during debounce", () => {
172
+ setupAppLifecycle(win, doc, orchestrator, { debounceMs: 250 });
173
+
174
+ doc.visibilityState = "visible";
175
+ docListeners["visibilitychange"]();
176
+
177
+ // Debounce is pending
178
+ expect(orchestrator.ensureAllConnections).not.toHaveBeenCalled();
179
+
180
+ // User immediately switches away / phone locks before debounce expires
181
+ doc.visibilityState = "hidden";
182
+ docListeners["visibilitychange"]();
183
+
184
+ // Advance past debounce
185
+ jest.advanceTimersByTime(500);
186
+
187
+ expect(orchestrator.ensureAllConnections).not.toHaveBeenCalled();
188
+ });
189
+
190
+ it("cleanup cancels any pending debounce timer", () => {
191
+ const cleanup = setupAppLifecycle(win, doc, orchestrator, {
192
+ debounceMs: 250,
193
+ });
194
+
195
+ winListeners["online"]();
196
+ cleanup();
197
+
198
+ jest.advanceTimersByTime(500);
199
+ expect(orchestrator.ensureAllConnections).not.toHaveBeenCalled();
200
+ });
201
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * ShadowClaw — App Lifecycle & Connection Health Management
3
+ *
4
+ * Listens for mobile sleep / wake, tab switching, and network changes:
5
+ * - visibilitychange: detects when the app/tab is backgrounded or brought back to foreground.
6
+ * If the app was backgrounded for longer than minSleepDurationMs (default: 1000ms),
7
+ * it indicates the device went to sleep or user switched away, requiring forced reconnection.
8
+ * - pageshow: handles bfcache restoration and page reveal.
9
+ * - focus: handles window/tab focus.
10
+ * - online: handles network connectivity resumption.
11
+ *
12
+ * Coalesces rapid succession of resume events via a debounce window (default: 250ms).
13
+ */
14
+
15
+ export interface AppLifecycleOptions {
16
+ debounceMs?: number;
17
+ minSleepDurationMs?: number;
18
+ }
19
+
20
+ export interface MinimalOrchestrator {
21
+ ensureAllConnections(options?: { force?: boolean }): Promise<void>;
22
+ }
23
+
24
+ export function setupAppLifecycle(
25
+ win: Window,
26
+ doc: Document,
27
+ orchestrator: MinimalOrchestrator,
28
+ options: AppLifecycleOptions = {},
29
+ ): () => void {
30
+ const debounceMs = options.debounceMs ?? 250;
31
+ const minSleepDurationMs = options.minSleepDurationMs ?? 1000;
32
+
33
+ let lastHiddenTime = 0;
34
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
35
+ let pendingForce = false;
36
+
37
+ const triggerReconnect = (force: boolean) => {
38
+ if (force) {
39
+ pendingForce = true;
40
+ }
41
+
42
+ if (debounceTimer !== null) {
43
+ clearTimeout(debounceTimer);
44
+ }
45
+
46
+ debounceTimer = setTimeout(() => {
47
+ debounceTimer = null;
48
+ const shouldForce = pendingForce;
49
+ pendingForce = false;
50
+ void orchestrator.ensureAllConnections({ force: shouldForce });
51
+ }, debounceMs);
52
+ };
53
+
54
+ const cancelPendingReconnect = () => {
55
+ if (debounceTimer !== null) {
56
+ clearTimeout(debounceTimer);
57
+ debounceTimer = null;
58
+ }
59
+ pendingForce = false;
60
+ };
61
+
62
+ const onVisibilityChange = () => {
63
+ if (doc.visibilityState === "hidden") {
64
+ lastHiddenTime = Date.now();
65
+ cancelPendingReconnect();
66
+ } else if (doc.visibilityState === "visible") {
67
+ const now = Date.now();
68
+ const wasAsleep =
69
+ lastHiddenTime > 0 && now - lastHiddenTime >= minSleepDurationMs;
70
+ triggerReconnect(wasAsleep);
71
+ }
72
+ };
73
+
74
+ const onPageShow = (event?: PageTransitionEvent) => {
75
+ const isPersisted = Boolean(event && event.persisted);
76
+ triggerReconnect(isPersisted || doc.visibilityState === "visible");
77
+ };
78
+
79
+ const onFocus = () => {
80
+ if (doc.visibilityState === "visible") {
81
+ triggerReconnect(false);
82
+ }
83
+ };
84
+
85
+ const onOnline = () => {
86
+ triggerReconnect(true);
87
+ };
88
+
89
+ doc.addEventListener("visibilitychange", onVisibilityChange);
90
+ win.addEventListener("pageshow", onPageShow);
91
+ win.addEventListener("focus", onFocus);
92
+ win.addEventListener("online", onOnline);
93
+
94
+ return () => {
95
+ cancelPendingReconnect();
96
+ doc.removeEventListener("visibilitychange", onVisibilityChange);
97
+ win.removeEventListener("pageshow", onPageShow);
98
+ win.removeEventListener("focus", onFocus);
99
+ win.removeEventListener("online", onOnline);
100
+ };
101
+ }