clawmini 0.0.8 → 0.0.9

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 (367) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +14 -0
  3. package/.github/workflows/release.yml +49 -0
  4. package/CHANGELOG.md +36 -0
  5. package/README.md +5 -4
  6. package/dist/adapter-discord/index.d.mts.map +1 -1
  7. package/dist/adapter-discord/index.mjs +465 -282
  8. package/dist/adapter-discord/index.mjs.map +1 -1
  9. package/dist/adapter-google-chat/index.mjs +367 -243
  10. package/dist/adapter-google-chat/index.mjs.map +1 -1
  11. package/dist/cli/index.mjs +684 -24
  12. package/dist/cli/index.mjs.map +1 -1
  13. package/dist/cli/lite.mjs +43 -13
  14. package/dist/cli/lite.mjs.map +1 -1
  15. package/dist/cli/{propose-policy.mjs → manage-policies.mjs} +270 -47
  16. package/dist/cli/manage-policies.mjs.map +1 -0
  17. package/dist/cli/run-host.d.mts +1 -0
  18. package/dist/cli/run-host.mjs +3090 -0
  19. package/dist/cli/run-host.mjs.map +1 -0
  20. package/dist/config-CPFQIGdG.mjs +57 -0
  21. package/dist/config-CPFQIGdG.mjs.map +1 -0
  22. package/dist/config-Dvl-Pov4.mjs +76 -0
  23. package/dist/config-Dvl-Pov4.mjs.map +1 -0
  24. package/dist/daemon/index.d.mts.map +1 -1
  25. package/dist/daemon/index.mjs +970 -332
  26. package/dist/daemon/index.mjs.map +1 -1
  27. package/dist/supervisor-actions-CiW56eLi.mjs +843 -0
  28. package/dist/supervisor-actions-CiW56eLi.mjs.map +1 -0
  29. package/dist/turn-log-buffer-DRgW53gl.mjs +767 -0
  30. package/dist/turn-log-buffer-DRgW53gl.mjs.map +1 -0
  31. package/dist/web/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
  32. package/dist/web/_app/immutable/chunks/BhRSsUCh.js +2 -0
  33. package/dist/web/_app/immutable/chunks/BiLeM2i1.js +1 -0
  34. package/{web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js → dist/web/_app/immutable/chunks/BmBj85Ll.js} +1 -1
  35. package/dist/web/_app/immutable/chunks/BrERcKAH.js +1 -0
  36. package/dist/web/_app/immutable/chunks/Bv9252RM.js +1 -0
  37. package/dist/web/_app/immutable/chunks/CIXNBPKi.js +1 -0
  38. package/dist/web/_app/immutable/chunks/DISKL3GN.js +2 -0
  39. package/dist/web/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
  40. package/dist/web/_app/immutable/chunks/DnQ3vS13.js +1 -0
  41. package/dist/web/_app/immutable/chunks/KsloHTKS.js +1 -0
  42. package/{web/.svelte-kit/output/client/_app/immutable/chunks/Ck-be5J2.js → dist/web/_app/immutable/chunks/RsHsUj-8.js} +2 -2
  43. package/dist/web/_app/immutable/chunks/{vDehDcuJ.js → wpfV79dV.js} +1 -1
  44. package/dist/web/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
  45. package/dist/web/_app/immutable/entry/start.Di0-Jhte.js +1 -0
  46. package/dist/web/_app/immutable/nodes/{0.CUGC2p-K.js → 0.DYyUA1au.js} +1 -1
  47. package/dist/web/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
  48. package/dist/web/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
  49. package/{web/.svelte-kit/output/client/_app/immutable/nodes/3.0arZe_Uf.js → dist/web/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
  50. package/dist/web/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
  51. package/dist/web/_app/immutable/nodes/{5.Bq2JzCEj.js → 5.DLbPVJY2.js} +1 -1
  52. package/dist/web/_app/version.json +1 -1
  53. package/dist/web/index.html +12 -12
  54. package/dist/workspace-oWmVh5mi.mjs +1001 -0
  55. package/dist/workspace-oWmVh5mi.mjs.map +1 -0
  56. package/docs/23_adapter_slash_autocomplete/development_log.md +19 -0
  57. package/docs/23_adapter_slash_autocomplete/notes.md +18 -0
  58. package/docs/23_adapter_slash_autocomplete/prd.md +46 -0
  59. package/docs/23_adapter_slash_autocomplete/questions.md +6 -0
  60. package/docs/23_adapter_slash_autocomplete/tickets.md +21 -0
  61. package/docs/24_subagent_job_policy_fixes/development_log.md +22 -0
  62. package/docs/24_subagent_job_policy_fixes/notes.md +28 -0
  63. package/docs/24_subagent_job_policy_fixes/prd.md +59 -0
  64. package/docs/24_subagent_job_policy_fixes/questions.md +3 -0
  65. package/docs/24_subagent_job_policy_fixes/tickets.md +49 -0
  66. package/docs/25_e2e_test_improvements/development_log.md +30 -0
  67. package/docs/25_e2e_test_improvements/notes.md +29 -0
  68. package/docs/25_e2e_test_improvements/prd.md +43 -0
  69. package/docs/25_e2e_test_improvements/questions.md +12 -0
  70. package/docs/25_e2e_test_improvements/tickets-2.md +22 -0
  71. package/docs/25_e2e_test_improvements/tickets.md +22 -0
  72. package/docs/25_policy_cwd/development_log.md +30 -0
  73. package/docs/25_policy_cwd/notes.md +28 -0
  74. package/docs/25_policy_cwd/prd.md +77 -0
  75. package/docs/25_policy_cwd/questions.md +6 -0
  76. package/docs/25_policy_cwd/tickets.md +77 -0
  77. package/docs/CLI_REFERENCE.md +3 -1
  78. package/docs/PHILOSOPHY.md +35 -0
  79. package/docs/adapter-visibility/SPEC.md +461 -0
  80. package/docs/adapter-visibility/SPEC_v2.md +202 -0
  81. package/docs/auto-update/SPEC.md +344 -0
  82. package/docs/backups/SPEC.md +296 -0
  83. package/docs/backups/clawmini.gitignore +69 -0
  84. package/docs/guides/assets/clawmini-avatar.png +0 -0
  85. package/docs/guides/backups.md +332 -0
  86. package/docs/guides/discord_adapter_setup.md +1 -1
  87. package/docs/guides/google_chat_adapter_setup.md +81 -0
  88. package/docs/unified-startup/SPEC.md +203 -0
  89. package/e2e/_helpers/test-environment.test.ts +49 -0
  90. package/e2e/_helpers/test-environment.ts +548 -0
  91. package/e2e/adapters/_google-chat-fixtures.ts +340 -0
  92. package/{src/cli/e2e → e2e/adapters}/adapter-discord.test.ts +22 -23
  93. package/e2e/adapters/adapter-google-chat-downtime.test.ts +157 -0
  94. package/e2e/adapters/adapter-google-chat-inbound.test.ts +697 -0
  95. package/e2e/adapters/adapter-google-chat-outbound.test.ts +297 -0
  96. package/e2e/adapters/adapter-google-chat-roundtrip.test.ts +56 -0
  97. package/e2e/adapters/adapter-google-chat-threads.test.ts +1078 -0
  98. package/e2e/agents/custom-api-env.test.ts +80 -0
  99. package/e2e/agents/export-lite-func.test.ts +104 -0
  100. package/e2e/agents/fallbacks.test.ts +124 -0
  101. package/e2e/agents/interrupt.test.ts +50 -0
  102. package/e2e/agents/no-reply-necessary.test.ts +57 -0
  103. package/e2e/agents/session-timeout-subagents.test.ts +76 -0
  104. package/e2e/agents/subagent-authorization.test.ts +246 -0
  105. package/e2e/agents/subagent-env.test.ts +49 -0
  106. package/e2e/agents/subagent-lifecycle.test.ts +782 -0
  107. package/e2e/agents/subagents-depth.test.ts +47 -0
  108. package/e2e/cli/agents.test.ts +176 -0
  109. package/e2e/cli/auto-update.test.ts +741 -0
  110. package/e2e/cli/basic.test.ts +44 -0
  111. package/{src/cli/e2e → e2e/cli}/export-lite.test.ts +16 -12
  112. package/e2e/cli/init-gitignore.test.ts +86 -0
  113. package/e2e/cli/init.test.ts +76 -0
  114. package/e2e/cli/messages.test.ts +363 -0
  115. package/e2e/cli/serve.test.ts +76 -0
  116. package/{src/cli/e2e → e2e/cli}/skills.test.ts +11 -10
  117. package/{src/cli/e2e → e2e/daemon}/daemon.test.ts +57 -195
  118. package/e2e/jobs/agent-jobs.test.ts +216 -0
  119. package/e2e/jobs/cron.test.ts +64 -0
  120. package/e2e/jobs/restart.test.ts +108 -0
  121. package/e2e/policies/approval-session.test.ts +69 -0
  122. package/e2e/policies/auto-create-policies-file.test.ts +35 -0
  123. package/e2e/policies/builtin-manage-policies.test.ts +184 -0
  124. package/e2e/policies/builtin-run-host.test.ts +180 -0
  125. package/e2e/policies/environment-policies.test.ts +177 -0
  126. package/e2e/policies/manage-policies.test.ts +566 -0
  127. package/e2e/policies/output-size.test.ts +98 -0
  128. package/e2e/policies/policies-context-cwd.test.ts +160 -0
  129. package/e2e/policies/relative-script-path.test.ts +60 -0
  130. package/e2e/policies/requests-show.test.ts +135 -0
  131. package/e2e/policies/requests.test.ts +208 -0
  132. package/e2e/policies/slash-policies.test.ts +308 -0
  133. package/e2e/policies/startup-cleanup.test.ts +48 -0
  134. package/e2e/routers/session-timeout.test.ts +106 -0
  135. package/e2e/routers/slash-model.test.ts +152 -0
  136. package/e2e/routers/slash-new.test.ts +50 -0
  137. package/e2e/routers/slash-restart-adapter.test.ts +96 -0
  138. package/e2e/routers/slash-restart.test.ts +114 -0
  139. package/e2e/routers/slash-shutdown.test.ts +55 -0
  140. package/e2e/routers/slash-stop.test.ts +232 -0
  141. package/e2e/routers/slash-upgrade.test.ts +88 -0
  142. package/{src/cli/e2e → e2e/sandbox}/environments.test.ts +14 -13
  143. package/eslint.config.js +6 -0
  144. package/napkin.md +1 -1
  145. package/package.json +8 -3
  146. package/src/adapter-discord/commands.test.ts +42 -0
  147. package/src/adapter-discord/commands.ts +33 -0
  148. package/src/adapter-discord/config.ts +12 -0
  149. package/src/adapter-discord/forwarder.test.ts +499 -21
  150. package/src/adapter-discord/forwarder.ts +343 -124
  151. package/src/adapter-discord/inbound-cache.test.ts +47 -0
  152. package/src/adapter-discord/inbound-cache.ts +37 -0
  153. package/src/adapter-discord/index.test.ts +67 -2
  154. package/src/adapter-discord/index.ts +84 -216
  155. package/src/adapter-discord/interactions.test.ts +54 -3
  156. package/src/adapter-discord/interactions.ts +97 -53
  157. package/src/adapter-discord/processMessage.ts +239 -0
  158. package/src/adapter-discord/state.ts +1 -0
  159. package/src/adapter-google-chat/auth.test.ts +9 -5
  160. package/src/adapter-google-chat/auth.ts +29 -23
  161. package/src/adapter-google-chat/cards.ts +7 -2
  162. package/src/adapter-google-chat/client.test.ts +37 -2
  163. package/src/adapter-google-chat/client.ts +138 -38
  164. package/src/adapter-google-chat/config.ts +19 -0
  165. package/src/adapter-google-chat/forwarder.test.ts +81 -56
  166. package/src/adapter-google-chat/forwarder.ts +394 -185
  167. package/src/adapter-google-chat/inbound-cache.test.ts +61 -0
  168. package/src/adapter-google-chat/inbound-cache.ts +36 -0
  169. package/src/adapter-google-chat/state.test.ts +1 -0
  170. package/src/adapter-google-chat/state.ts +9 -1
  171. package/src/adapter-google-chat/subscriptions.ts +8 -6
  172. package/src/cli/builtin-policies.ts +44 -0
  173. package/src/cli/commands/agents.ts +59 -5
  174. package/src/cli/commands/down.ts +54 -2
  175. package/src/cli/commands/environments.ts +8 -2
  176. package/src/cli/commands/init.ts +31 -0
  177. package/src/cli/commands/logs.ts +116 -0
  178. package/src/cli/commands/policies.ts +6 -4
  179. package/src/cli/commands/serve.test.ts +67 -0
  180. package/src/cli/commands/serve.ts +284 -0
  181. package/src/cli/commands/up.ts +122 -2
  182. package/src/cli/commands/web-api/agents.ts +3 -2
  183. package/src/cli/index.ts +4 -0
  184. package/src/cli/install-detection.test.ts +72 -0
  185. package/src/cli/install-detection.ts +48 -0
  186. package/src/cli/lite.ts +54 -22
  187. package/src/cli/manage-policies-utils.ts +104 -0
  188. package/src/cli/manage-policies.ts +291 -0
  189. package/src/cli/run-host.ts +45 -0
  190. package/src/cli/supervisor-actions.ts +267 -0
  191. package/src/cli/supervisor-control.test.ts +129 -0
  192. package/src/cli/supervisor-control.ts +155 -0
  193. package/src/cli/supervisor-pid.ts +68 -0
  194. package/src/cli/supervisor.ts +277 -0
  195. package/src/daemon/agent/agent-context.ts +11 -11
  196. package/src/daemon/agent/agent-session.ts +8 -1
  197. package/src/daemon/agent/chat-logger.test.ts +78 -9
  198. package/src/daemon/agent/chat-logger.ts +25 -5
  199. package/src/daemon/agent/turn-registry.test.ts +89 -0
  200. package/src/daemon/agent/turn-registry.ts +94 -0
  201. package/src/daemon/agent/types.ts +2 -0
  202. package/src/daemon/api/agent-policy-endpoints.ts +263 -0
  203. package/src/daemon/api/agent-router.ts +47 -126
  204. package/src/daemon/api/index.test.ts +1 -0
  205. package/src/daemon/api/policy-request.test.ts +7 -5
  206. package/src/daemon/api/router-utils.ts +6 -5
  207. package/src/daemon/api/subagent-router.ts +110 -74
  208. package/src/daemon/api/subagent-utils.test.ts +60 -0
  209. package/src/daemon/api/subagent-utils.ts +113 -87
  210. package/src/daemon/api/user-router.ts +34 -8
  211. package/src/daemon/auth.ts +1 -0
  212. package/src/daemon/cron.test.ts +62 -4
  213. package/src/daemon/cron.ts +42 -16
  214. package/src/daemon/events.ts +65 -0
  215. package/src/daemon/index.ts +24 -1
  216. package/src/daemon/message-interruption.test.ts +1 -0
  217. package/src/daemon/message-jobs.test.ts +1 -0
  218. package/src/daemon/message.ts +78 -14
  219. package/src/daemon/observation.test.ts +26 -18
  220. package/src/daemon/pending-replies.test.ts +112 -0
  221. package/src/daemon/pending-replies.ts +162 -0
  222. package/src/daemon/policy-request-service.ts +3 -1
  223. package/src/daemon/policy-utils.test.ts +66 -1
  224. package/src/daemon/policy-utils.ts +126 -1
  225. package/src/daemon/request-store.ts +31 -0
  226. package/src/daemon/routers/session-timeout.ts +4 -0
  227. package/src/daemon/routers/slash-model.test.ts +344 -0
  228. package/src/daemon/routers/slash-model.ts +207 -0
  229. package/src/daemon/routers/slash-policies.test.ts +38 -32
  230. package/src/daemon/routers/slash-policies.ts +84 -33
  231. package/src/daemon/routers/slash-restart.test.ts +69 -0
  232. package/src/daemon/routers/slash-restart.ts +36 -0
  233. package/src/daemon/routers/slash-shutdown.test.ts +50 -0
  234. package/src/daemon/routers/slash-shutdown.ts +28 -0
  235. package/src/daemon/routers/slash-upgrade.test.ts +116 -0
  236. package/src/daemon/routers/slash-upgrade.ts +76 -0
  237. package/src/daemon/routers/types.ts +7 -0
  238. package/src/daemon/routers.ts +16 -0
  239. package/src/shared/adapters/blockquote.test.ts +28 -0
  240. package/src/shared/adapters/blockquote.ts +20 -0
  241. package/src/shared/adapters/filtering.test.ts +224 -10
  242. package/src/shared/adapters/filtering.ts +95 -7
  243. package/src/shared/adapters/inbound-cache.test.ts +48 -0
  244. package/src/shared/adapters/inbound-cache.ts +54 -0
  245. package/src/shared/adapters/turn-log-buffer.ts +266 -0
  246. package/src/shared/adapters/turn-log.test.ts +389 -0
  247. package/src/shared/adapters/turn-log.ts +357 -0
  248. package/src/shared/agent-utils.ts +12 -5
  249. package/src/shared/chats.test.ts +4 -0
  250. package/src/shared/chats.ts +9 -0
  251. package/src/shared/config.ts +16 -1
  252. package/src/shared/lite.ts +76 -2
  253. package/src/shared/policies.ts +26 -0
  254. package/src/shared/template-manifest.ts +267 -0
  255. package/src/shared/utils/shell.ts +61 -0
  256. package/src/shared/version.ts +34 -0
  257. package/src/shared/workspace.test.ts +217 -0
  258. package/src/shared/workspace.ts +626 -48
  259. package/templates/environments/cladding/allowlist-domain.mjs +125 -0
  260. package/templates/environments/cladding/env.json +21 -1
  261. package/templates/environments/cladding/run-with-network.mjs +54 -0
  262. package/templates/environments/macos-proxy/allowlist-domain.mjs +95 -0
  263. package/templates/environments/macos-proxy/env.json +8 -1
  264. package/templates/environments/macos-proxy/proxy.mjs +0 -1
  265. package/templates/gemini/template.json +5 -0
  266. package/templates/gemini-claw/template.json +13 -0
  267. package/templates/skills/clawmini-requests/SKILL.md +69 -10
  268. package/templates/skills/run-host/SKILL.md +51 -0
  269. package/templates/skills/skill-creator/SKILL.md +4 -3
  270. package/templates/skills/skill-creator/scripts/validate.sh +52 -0
  271. package/tsdown.config.ts +10 -1
  272. package/vitest.config.ts +2 -2
  273. package/web/.svelte-kit/ambient.d.ts +292 -118
  274. package/web/.svelte-kit/generated/server/internal.js +1 -1
  275. package/web/.svelte-kit/output/client/.vite/manifest.json +126 -136
  276. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
  277. package/web/.svelte-kit/output/client/_app/immutable/chunks/BhRSsUCh.js +2 -0
  278. package/web/.svelte-kit/output/client/_app/immutable/chunks/BiLeM2i1.js +1 -0
  279. package/{dist/web/_app/immutable/chunks/CME08kGM.js → web/.svelte-kit/output/client/_app/immutable/chunks/BmBj85Ll.js} +1 -1
  280. package/web/.svelte-kit/output/client/_app/immutable/chunks/BrERcKAH.js +1 -0
  281. package/web/.svelte-kit/output/client/_app/immutable/chunks/Bv9252RM.js +1 -0
  282. package/web/.svelte-kit/output/client/_app/immutable/chunks/CIXNBPKi.js +1 -0
  283. package/web/.svelte-kit/output/client/_app/immutable/chunks/DISKL3GN.js +2 -0
  284. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
  285. package/web/.svelte-kit/output/client/_app/immutable/chunks/DnQ3vS13.js +1 -0
  286. package/web/.svelte-kit/output/client/_app/immutable/chunks/KsloHTKS.js +1 -0
  287. package/{dist/web/_app/immutable/chunks/Ck-be5J2.js → web/.svelte-kit/output/client/_app/immutable/chunks/RsHsUj-8.js} +2 -2
  288. package/web/.svelte-kit/output/client/_app/immutable/chunks/{vDehDcuJ.js → wpfV79dV.js} +1 -1
  289. package/web/.svelte-kit/output/client/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
  290. package/web/.svelte-kit/output/client/_app/immutable/entry/start.Di0-Jhte.js +1 -0
  291. package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.CUGC2p-K.js → 0.DYyUA1au.js} +1 -1
  292. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
  293. package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
  294. package/{dist/web/_app/immutable/nodes/3.0arZe_Uf.js → web/.svelte-kit/output/client/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
  295. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
  296. package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.Bq2JzCEj.js → 5.DLbPVJY2.js} +1 -1
  297. package/web/.svelte-kit/output/client/_app/version.json +1 -1
  298. package/web/.svelte-kit/output/server/.vite/manifest.json +12 -10
  299. package/web/.svelte-kit/output/server/chunks/Icon.js +1 -1
  300. package/web/.svelte-kit/output/server/chunks/client.js +1 -1
  301. package/web/.svelte-kit/output/server/chunks/exports.js +1 -1
  302. package/web/.svelte-kit/output/server/chunks/index-server.js +2 -1
  303. package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
  304. package/web/.svelte-kit/output/server/chunks/render-context.js +77 -0
  305. package/web/.svelte-kit/output/server/chunks/root.js +739 -788
  306. package/web/.svelte-kit/output/server/chunks/shared.js +234 -21
  307. package/web/.svelte-kit/output/server/index.js +126 -90
  308. package/web/.svelte-kit/output/server/manifest-full.js +1 -1
  309. package/web/.svelte-kit/output/server/manifest.js +1 -1
  310. package/web/.svelte-kit/output/server/nodes/0.js +1 -1
  311. package/web/.svelte-kit/output/server/nodes/1.js +1 -1
  312. package/web/.svelte-kit/output/server/nodes/2.js +1 -1
  313. package/web/.svelte-kit/output/server/nodes/3.js +1 -1
  314. package/web/.svelte-kit/output/server/nodes/4.js +1 -1
  315. package/web/.svelte-kit/output/server/nodes/5.js +1 -1
  316. package/web/.svelte-kit/output/server/remote-entry.js +245 -81
  317. package/web/.svelte-kit/tsconfig.json +4 -1
  318. package/dist/cli/propose-policy.mjs.map +0 -1
  319. package/dist/lite-CBxOT1y5.mjs +0 -241
  320. package/dist/lite-CBxOT1y5.mjs.map +0 -1
  321. package/dist/routing-D8rTxtaV.mjs +0 -245
  322. package/dist/routing-D8rTxtaV.mjs.map +0 -1
  323. package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +0 -1
  324. package/dist/web/_app/immutable/chunks/BmRlVmv6.js +0 -1
  325. package/dist/web/_app/immutable/chunks/CK9JZLaG.js +0 -2
  326. package/dist/web/_app/immutable/chunks/Ck3rYNON.js +0 -1
  327. package/dist/web/_app/immutable/chunks/D5iV40bG.js +0 -1
  328. package/dist/web/_app/immutable/chunks/DMtIqaiV.js +0 -2
  329. package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
  330. package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
  331. package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
  332. package/dist/web/_app/immutable/entry/app.BCSV3nrG.js +0 -2
  333. package/dist/web/_app/immutable/entry/start.D4eLEZUM.js +0 -1
  334. package/dist/web/_app/immutable/nodes/1.CGC_42IQ.js +0 -1
  335. package/dist/web/_app/immutable/nodes/4.ClM1bXLE.js +0 -60
  336. package/dist/workspace-BJmJBfKi.mjs +0 -456
  337. package/dist/workspace-BJmJBfKi.mjs.map +0 -1
  338. package/src/cli/e2e/agents.test.ts +0 -140
  339. package/src/cli/e2e/basic.test.ts +0 -43
  340. package/src/cli/e2e/cron.test.ts +0 -132
  341. package/src/cli/e2e/export-lite-func.test.ts +0 -206
  342. package/src/cli/e2e/fallbacks.test.ts +0 -175
  343. package/src/cli/e2e/init.test.ts +0 -77
  344. package/src/cli/e2e/messages.test.ts +0 -332
  345. package/src/cli/e2e/propose-policy.test.ts +0 -203
  346. package/src/cli/e2e/requests.test.ts +0 -180
  347. package/src/cli/e2e/session-timeout.test.ts +0 -192
  348. package/src/cli/e2e/slash-new.test.ts +0 -93
  349. package/src/cli/e2e/subagents.test.ts +0 -106
  350. package/src/cli/e2e/utils.ts +0 -66
  351. package/src/cli/propose-policy.ts +0 -91
  352. package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +0 -1
  353. package/web/.svelte-kit/output/client/_app/immutable/chunks/BmRlVmv6.js +0 -1
  354. package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +0 -2
  355. package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +0 -1
  356. package/web/.svelte-kit/output/client/_app/immutable/chunks/D5iV40bG.js +0 -1
  357. package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +0 -2
  358. package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
  359. package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
  360. package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
  361. package/web/.svelte-kit/output/client/_app/immutable/entry/app.BCSV3nrG.js +0 -2
  362. package/web/.svelte-kit/output/client/_app/immutable/entry/start.D4eLEZUM.js +0 -1
  363. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.CGC_42IQ.js +0 -1
  364. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.ClM1bXLE.js +0 -60
  365. package/web/.svelte-kit/output/server/chunks/false.js +0 -4
  366. /package/dist/cli/{propose-policy.d.mts → manage-policies.d.mts} +0 -0
  367. /package/{src/cli/e2e → e2e/_helpers}/global-setup.ts +0 -0
@@ -0,0 +1,267 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+
5
+ import { detectInstall } from './install-detection.js';
6
+ import {
7
+ startControlServer,
8
+ type ControlRequest,
9
+ type ControlResponse,
10
+ } from './supervisor-control.js';
11
+ import { removeSupervisorPid } from './supervisor-pid.js';
12
+ import { enqueuePendingReply, dequeuePendingReply } from '../daemon/pending-replies.js';
13
+ import type { Supervisor } from './supervisor.js';
14
+
15
+ // Grace period before a destructive action fires. The daemon-side router
16
+ // awaits sendControlRequest but the daemon still needs to flush the user
17
+ // message + ack reply through the chat log before the daemon process is
18
+ // killed. SIGTERM arriving mid-flush is non-fatal (the daemon's own SIGTERM
19
+ // handler still runs) but the grace makes the ordering deterministic.
20
+ const ACTION_GRACE_MS = 1000;
21
+
22
+ /**
23
+ * Wire up the supervisor control socket so the daemon can request
24
+ * /restart, /shutdown, and /upgrade out-of-band.
25
+ */
26
+ export function startSupervisorControl(supervisor: Supervisor): void {
27
+ startControlServer({
28
+ restart: async (req: ControlRequest): Promise<ControlResponse> => {
29
+ // Enqueue BEFORE scheduling the kill, so a crash between enqueue and
30
+ // kill leaves the post-restart message ready to drain.
31
+ if (req.chatId) {
32
+ enqueuePendingReply({
33
+ chatId: req.chatId,
34
+ kind: 'restart-complete',
35
+ ...(req.messageId ? { messageId: req.messageId } : {}),
36
+ });
37
+ }
38
+ setTimeout(() => {
39
+ // restartAll instead of restartService('daemon'): adapters hold
40
+ // tRPC subscriptions to the daemon and won't reconnect on their
41
+ // own when the daemon process dies, so outbound replies wouldn't
42
+ // make it back to the chat. Bouncing the adapters forces them to
43
+ // re-subscribe to the new daemon.
44
+ void supervisor.restartAll().catch((err) => {
45
+ process.stderr.write(
46
+ `[supervisor] /restart failed: ${err instanceof Error ? err.message : String(err)}\n`
47
+ );
48
+ // Restart failed — back out the queued reply so the next successful
49
+ // start doesn't surface a phantom "restarted" message.
50
+ if (req.chatId && req.messageId) {
51
+ dequeuePendingReply(
52
+ (e) => e.kind === 'restart-complete' && e.messageId === req.messageId
53
+ );
54
+ }
55
+ });
56
+ }, ACTION_GRACE_MS);
57
+ return { ok: true };
58
+ },
59
+ shutdown: async (): Promise<ControlResponse> => {
60
+ setTimeout(() => void supervisor.shutdown(0), ACTION_GRACE_MS);
61
+ return { ok: true };
62
+ },
63
+ upgrade: async (req: ControlRequest): Promise<ControlResponse> => {
64
+ const info = detectInstall();
65
+ if (!info.isNpmGlobal) {
66
+ return {
67
+ ok: false,
68
+ error: `not installed via npm install -g (running from ${info.entryRealPath})`,
69
+ };
70
+ }
71
+ const version = req.version?.trim();
72
+ if (!version) {
73
+ return { ok: false, error: 'missing target version' };
74
+ }
75
+ if (!isAcceptableVersion(version)) {
76
+ return { ok: false, error: `invalid version: ${version}` };
77
+ }
78
+ setTimeout(() => {
79
+ void runUpgrade(supervisor, version, req.chatId, req.messageId).catch((err) => {
80
+ process.stderr.write(
81
+ `[supervisor] /upgrade encountered an unexpected error: ${err instanceof Error ? err.message : String(err)}\n`
82
+ );
83
+ });
84
+ }, ACTION_GRACE_MS);
85
+ return { ok: true };
86
+ },
87
+ });
88
+ }
89
+
90
+ // Accept semver-ish versions (1.2.3, 1.2.3-beta.1, etc.), the literal
91
+ // "latest", or a dist-tag (alphanumeric + dashes). Reject anything else so a
92
+ // malicious client can't smuggle shell metacharacters or `--registry=...`
93
+ // into the npm command line.
94
+ export function isAcceptableVersion(version: string): boolean {
95
+ return /^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,63}$/.test(version);
96
+ }
97
+
98
+ /**
99
+ * In-place upgrade. Order:
100
+ * 1. `npm install -g clawmini@<version>` (services kept running so the
101
+ * user isn't left in the dark if the install fails).
102
+ * 2. Resolve the freshly-installed binary by absolute path. Bail out with
103
+ * an upgrade-failed reply if it's missing.
104
+ * 3. Enqueue the upgrade-complete reply.
105
+ * 4. Stop all children, spawn the replacement supervisor, exit.
106
+ *
107
+ * Any failure path enqueues an upgrade-failed reply (so the user gets visible
108
+ * feedback) and restarts the daemon to drain it, rather than silently exiting.
109
+ */
110
+ export async function runUpgrade(
111
+ supervisor: Supervisor,
112
+ version: string,
113
+ chatId?: string,
114
+ messageId?: string
115
+ ): Promise<void> {
116
+ const info = detectInstall();
117
+ if (!info.isNpmGlobal || !info.npmRootRealPath) {
118
+ process.stderr.write(
119
+ `[supervisor] /upgrade aborted: clawmini is not installed via npm install -g (running from ${info.entryRealPath}).\n`
120
+ );
121
+ return;
122
+ }
123
+
124
+ const installErr = await runNpmInstall(version);
125
+ if (installErr) {
126
+ process.stderr.write(`[supervisor] /upgrade failed: ${installErr}\n`);
127
+ if (chatId) {
128
+ enqueuePendingReply({
129
+ chatId,
130
+ kind: 'upgrade-failed',
131
+ ...(messageId ? { messageId } : {}),
132
+ requestedVersion: version,
133
+ reason: installErr,
134
+ });
135
+ // The daemon is still running (we never stopped it). Bounce the whole
136
+ // stack so the new daemon drains the failure message AND adapters
137
+ // re-subscribe to it.
138
+ await supervisor.restartAll().catch((err) => {
139
+ process.stderr.write(
140
+ `[supervisor] additionally failed to restart services to surface the failure: ${err instanceof Error ? err.message : String(err)}\n`
141
+ );
142
+ });
143
+ }
144
+ return;
145
+ }
146
+
147
+ // npm install reported success — confirm the binary exists where we expect
148
+ // before we start tearing down services.
149
+ const newCli = path.join(info.npmRootRealPath, 'clawmini', 'dist', 'cli', 'index.mjs');
150
+ if (!fs.existsSync(newCli)) {
151
+ const reason = `npm install reported success but ${newCli} is missing`;
152
+ process.stderr.write(`[supervisor] /upgrade aborted: ${reason}\n`);
153
+ if (chatId) {
154
+ enqueuePendingReply({
155
+ chatId,
156
+ kind: 'upgrade-failed',
157
+ ...(messageId ? { messageId } : {}),
158
+ requestedVersion: version,
159
+ reason,
160
+ });
161
+ await supervisor.restartAll().catch(() => {});
162
+ }
163
+ return;
164
+ }
165
+
166
+ if (chatId) {
167
+ enqueuePendingReply({
168
+ chatId,
169
+ kind: 'upgrade-complete',
170
+ ...(messageId ? { messageId } : {}),
171
+ requestedVersion: version,
172
+ });
173
+ }
174
+
175
+ process.stderr.write('[supervisor] /upgrade: stopping all services...\n');
176
+ await supervisor.stopAllChildren();
177
+
178
+ process.stderr.write(`[supervisor] /upgrade: relaunching ${newCli} serve --detach...\n`);
179
+ // Drop our pid file so the new supervisor doesn't see us as already
180
+ // running; it will write its own pid on startup.
181
+ removeSupervisorPid();
182
+
183
+ // Run the new entry point directly via the current Node binary so the
184
+ // spawn doesn't depend on the shell's PATH being refreshed (which it isn't
185
+ // for an already-running process).
186
+ const replacement = spawn(process.execPath, [newCli, 'serve', '--detach'], {
187
+ detached: true,
188
+ stdio: 'ignore',
189
+ cwd: process.cwd(),
190
+ env: process.env,
191
+ });
192
+
193
+ const spawnOk = await waitForSpawn(replacement);
194
+ if (!spawnOk) {
195
+ process.stderr.write(
196
+ `[supervisor] /upgrade: replacement supervisor failed to spawn — restarting in place\n`
197
+ );
198
+ if (chatId) {
199
+ // Roll back the optimistic complete reply and replace it with a
200
+ // failure entry so the user knows the upgrade landed on disk but
201
+ // never came back up.
202
+ dequeuePendingReply(
203
+ (e) =>
204
+ e.kind === 'upgrade-complete' &&
205
+ e.messageId === messageId &&
206
+ e.requestedVersion === version
207
+ );
208
+ enqueuePendingReply({
209
+ chatId,
210
+ kind: 'upgrade-failed',
211
+ ...(messageId ? { messageId } : {}),
212
+ requestedVersion: version,
213
+ reason: 'replacement supervisor failed to spawn',
214
+ });
215
+ }
216
+ // Bring the whole stack back (daemon + adapters + web) so the failure
217
+ // message is drained AND adapters can deliver it to the chat.
218
+ try {
219
+ await supervisor.restartAll();
220
+ } catch (err) {
221
+ process.stderr.write(
222
+ `[supervisor] additionally failed to restart services after spawn failure: ${err instanceof Error ? err.message : String(err)}\n`
223
+ );
224
+ }
225
+ return;
226
+ }
227
+
228
+ replacement.unref();
229
+ process.exit(0);
230
+ }
231
+
232
+ function runNpmInstall(version: string): Promise<string | null> {
233
+ return new Promise((resolve) => {
234
+ // execFile-style argv (no shell), so the version argument can't be
235
+ // interpreted by a shell even if it slipped past isAcceptableVersion.
236
+ const child = spawn('npm', ['install', '-g', `clawmini@${version}`], {
237
+ stdio: 'inherit',
238
+ env: process.env,
239
+ });
240
+ child.on('exit', (code) => {
241
+ if (code === 0) resolve(null);
242
+ else resolve(`npm install -g exited with code ${code}`);
243
+ });
244
+ child.on('error', (err) => {
245
+ resolve(err instanceof Error ? err.message : String(err));
246
+ });
247
+ });
248
+ }
249
+
250
+ function waitForSpawn(child: ReturnType<typeof spawn>, timeoutMs = 5000): Promise<boolean> {
251
+ return new Promise((resolve) => {
252
+ let settled = false;
253
+ const settle = (ok: boolean): void => {
254
+ if (settled) return;
255
+ settled = true;
256
+ resolve(ok);
257
+ };
258
+ child.once('spawn', () => settle(true));
259
+ child.once('error', (err) => {
260
+ process.stderr.write(`[supervisor] replacement spawn error: ${err.message}\n`);
261
+ settle(false);
262
+ });
263
+ // 'spawn' fires almost immediately on success. Cap the wait so we never
264
+ // hang if neither event fires.
265
+ setTimeout(() => settle(true), timeoutMs);
266
+ });
267
+ }
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import {
7
+ startControlServer,
8
+ sendControlRequest,
9
+ type ControlAction,
10
+ type ControlHandler,
11
+ type ControlRequest,
12
+ } from './supervisor-control.js';
13
+
14
+ describe('supervisor control socket', () => {
15
+ const cleanup: Array<() => void> = [];
16
+
17
+ afterEach(() => {
18
+ while (cleanup.length) {
19
+ try {
20
+ cleanup.pop()!();
21
+ } catch {
22
+ // best-effort
23
+ }
24
+ }
25
+ });
26
+
27
+ function makeSocketPath(): string {
28
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'clawmini-ctl-'));
29
+ cleanup.push(() => fs.rmSync(tmp, { recursive: true, force: true }));
30
+ return path.join(tmp, 'supervisor.sock');
31
+ }
32
+
33
+ it('round-trips a request to the matching handler and forwards extra fields', async () => {
34
+ const sockPath = makeSocketPath();
35
+ const seen: ControlRequest[] = [];
36
+ const handlers: Record<ControlAction, ControlHandler> = {
37
+ restart: async (req) => {
38
+ seen.push(req);
39
+ return { ok: true };
40
+ },
41
+ shutdown: async () => ({ ok: true }),
42
+ upgrade: async () => ({ ok: true }),
43
+ };
44
+ const server = startControlServer(handlers, sockPath);
45
+ cleanup.push(() => server.close());
46
+
47
+ const res = await sendControlRequest(
48
+ { action: 'restart', chatId: 'c1', messageId: 'm1' },
49
+ sockPath
50
+ );
51
+ expect(res).toEqual({ ok: true });
52
+ expect(seen).toEqual([{ action: 'restart', chatId: 'c1', messageId: 'm1' }]);
53
+ });
54
+
55
+ it('returns the handler error when the handler rejects', async () => {
56
+ const sockPath = makeSocketPath();
57
+ const server = startControlServer(
58
+ {
59
+ restart: async () => {
60
+ throw new Error('boom');
61
+ },
62
+ shutdown: async () => ({ ok: true }),
63
+ upgrade: async () => ({ ok: true }),
64
+ },
65
+ sockPath
66
+ );
67
+ cleanup.push(() => server.close());
68
+
69
+ const res = await sendControlRequest({ action: 'restart' }, sockPath);
70
+ expect(res.ok).toBe(false);
71
+ expect(res.error).toBe('boom');
72
+ });
73
+
74
+ it('rejects unknown actions', async () => {
75
+ const sockPath = makeSocketPath();
76
+ const server = startControlServer(
77
+ {
78
+ restart: async () => ({ ok: true }),
79
+ shutdown: async () => ({ ok: true }),
80
+ upgrade: async () => ({ ok: true }),
81
+ },
82
+ sockPath
83
+ );
84
+ cleanup.push(() => server.close());
85
+
86
+ const res = await sendControlRequest({ action: 'unknown' as ControlAction }, sockPath);
87
+ expect(res.ok).toBe(false);
88
+ expect(res.error).toContain('unknown action');
89
+ });
90
+
91
+ it('overwrites a stale socket file at startup', async () => {
92
+ const sockPath = makeSocketPath();
93
+ fs.writeFileSync(sockPath, ''); // pretend a stale file is left over
94
+ const server = startControlServer(
95
+ {
96
+ restart: async () => ({ ok: true }),
97
+ shutdown: async () => ({ ok: true }),
98
+ upgrade: async () => ({ ok: true }),
99
+ },
100
+ sockPath
101
+ );
102
+ cleanup.push(() => server.close());
103
+
104
+ const res = await sendControlRequest({ action: 'shutdown' }, sockPath);
105
+ expect(res).toEqual({ ok: true });
106
+ });
107
+
108
+ it('chmods the socket to 0600 so other users on the host cannot connect', async () => {
109
+ const sockPath = makeSocketPath();
110
+ const server = startControlServer(
111
+ {
112
+ restart: async () => ({ ok: true }),
113
+ shutdown: async () => ({ ok: true }),
114
+ upgrade: async () => ({ ok: true }),
115
+ },
116
+ sockPath
117
+ );
118
+ cleanup.push(() => server.close());
119
+
120
+ // Round-trip a request to make sure the server is fully up — once we get
121
+ // a response, the listen() callback that does the chmod has fired.
122
+ const res = await sendControlRequest({ action: 'restart' }, sockPath);
123
+ expect(res.ok).toBe(true);
124
+
125
+ const stat = fs.statSync(sockPath);
126
+ // Compare mode bits, not the full mode (which includes the file type).
127
+ expect(stat.mode & 0o777).toBe(0o600);
128
+ });
129
+ });
@@ -0,0 +1,155 @@
1
+ import fs from 'node:fs';
2
+ import net from 'node:net';
3
+ import path from 'node:path';
4
+
5
+ import { getClawminiDir } from '../shared/workspace.js';
6
+
7
+ export type ControlAction = 'restart' | 'shutdown' | 'upgrade';
8
+
9
+ export interface ControlRequest {
10
+ action: ControlAction;
11
+ /** Target version for `upgrade` (e.g. "1.2.3" or "latest"). */
12
+ version?: string;
13
+ /** Chat to deliver the post-action SystemMessage to. */
14
+ chatId?: string;
15
+ /** User message that triggered the action, threaded onto the reply. */
16
+ messageId?: string;
17
+ }
18
+
19
+ export interface ControlResponse {
20
+ ok: boolean;
21
+ error?: string;
22
+ }
23
+
24
+ // Kept short on purpose: AF_UNIX sun_path is 104 bytes on macOS, 108 on
25
+ // Linux. A workspace nested a few directories deep + ".clawmini/super.sock"
26
+ // is already close to the limit; "supervisor.sock" pushed common paths over
27
+ // it (EINVAL from listen()).
28
+ export function getControlSocketPath(startDir = process.cwd()): string {
29
+ return path.join(getClawminiDir(startDir), 'super.sock');
30
+ }
31
+
32
+ export type ControlHandler = (req: ControlRequest) => Promise<ControlResponse> | ControlResponse;
33
+
34
+ export function startControlServer(
35
+ handlers: Record<ControlAction, ControlHandler>,
36
+ socketPath = getControlSocketPath()
37
+ ): net.Server {
38
+ if (fs.existsSync(socketPath)) {
39
+ try {
40
+ fs.unlinkSync(socketPath);
41
+ } catch {
42
+ // best-effort
43
+ }
44
+ }
45
+
46
+ const server = net.createServer((socket) => {
47
+ let buf = '';
48
+ let handled = false;
49
+
50
+ const respond = (res: ControlResponse): void => {
51
+ if (handled) return;
52
+ handled = true;
53
+ socket.end(JSON.stringify(res) + '\n');
54
+ };
55
+
56
+ socket.on('data', (chunk) => {
57
+ if (handled) return;
58
+ buf += chunk.toString();
59
+ const idx = buf.indexOf('\n');
60
+ if (idx === -1) return;
61
+ const line = buf.slice(0, idx);
62
+ let req: ControlRequest;
63
+ try {
64
+ req = JSON.parse(line) as ControlRequest;
65
+ } catch {
66
+ respond({ ok: false, error: 'invalid request' });
67
+ return;
68
+ }
69
+ const handler = handlers[req.action];
70
+ if (!handler) {
71
+ respond({ ok: false, error: `unknown action: ${req.action}` });
72
+ return;
73
+ }
74
+ Promise.resolve()
75
+ .then(() => handler(req))
76
+ .then(
77
+ (res) => respond(res),
78
+ (err: unknown) =>
79
+ respond({ ok: false, error: err instanceof Error ? err.message : String(err) })
80
+ );
81
+ });
82
+
83
+ socket.on('error', () => {
84
+ // Ignore; the client likely went away mid-write.
85
+ });
86
+ });
87
+
88
+ server.listen(socketPath, () => {
89
+ // Restrict to the owning user. The control channel can shut down or
90
+ // upgrade clawmini, so a process running as another local user must not
91
+ // be able to connect just because they can reach the .clawmini directory.
92
+ try {
93
+ fs.chmodSync(socketPath, 0o600);
94
+ } catch {
95
+ // best-effort
96
+ }
97
+ });
98
+ return server;
99
+ }
100
+
101
+ export function sendControlRequest(
102
+ req: ControlRequest,
103
+ socketPath = getControlSocketPath(),
104
+ timeoutMs = 5000
105
+ ): Promise<ControlResponse> {
106
+ return new Promise((resolve, reject) => {
107
+ const socket = net.createConnection({ path: socketPath });
108
+ let buf = '';
109
+ let settled = false;
110
+
111
+ const finish = (fn: () => void): void => {
112
+ if (settled) return;
113
+ settled = true;
114
+ try {
115
+ socket.destroy();
116
+ } catch {
117
+ // best-effort
118
+ }
119
+ fn();
120
+ };
121
+
122
+ const timer = setTimeout(() => {
123
+ finish(() => reject(new Error(`Control request timed out after ${timeoutMs}ms`)));
124
+ }, timeoutMs);
125
+
126
+ socket.on('connect', () => {
127
+ socket.write(JSON.stringify(req) + '\n');
128
+ });
129
+ socket.on('data', (chunk) => {
130
+ buf += chunk.toString();
131
+ const idx = buf.indexOf('\n');
132
+ if (idx !== -1) {
133
+ const line = buf.slice(0, idx);
134
+ clearTimeout(timer);
135
+ try {
136
+ const res = JSON.parse(line) as ControlResponse;
137
+ finish(() => resolve(res));
138
+ } catch (err) {
139
+ finish(() => reject(err instanceof Error ? err : new Error(String(err))));
140
+ }
141
+ }
142
+ });
143
+ socket.on('error', (err) => {
144
+ clearTimeout(timer);
145
+ finish(() => reject(err));
146
+ });
147
+ socket.on('end', () => {
148
+ // If the server closed before sending us a full line, surface it.
149
+ if (!settled) {
150
+ clearTimeout(timer);
151
+ finish(() => reject(new Error('Control server closed connection without responding')));
152
+ }
153
+ });
154
+ });
155
+ }
@@ -0,0 +1,68 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { getClawminiDir } from '../shared/workspace.js';
5
+
6
+ export function getSupervisorPidPath(startDir = process.cwd()): string {
7
+ return path.join(getClawminiDir(startDir), 'supervisor.pid');
8
+ }
9
+
10
+ // Returns the kernel-reported start time of `pid` as an opaque string, or
11
+ // null if the process doesn't exist. The exact format is platform-defined
12
+ // but stable per-host, which is all we need: we only ever compare the
13
+ // stored value against a fresh read on the same machine.
14
+ function getProcessStartTime(pid: number): string | null {
15
+ try {
16
+ const out = execFileSync('ps', ['-p', String(pid), '-o', 'lstart='], {
17
+ encoding: 'utf-8',
18
+ stdio: ['ignore', 'pipe', 'ignore'],
19
+ });
20
+ const trimmed = out.trim();
21
+ return trimmed.length > 0 ? trimmed : null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ // The pid file stores `<pid>:<start-time>` so we can detect pid reuse:
28
+ // `kill(pid, 0)` only confirms *something* with that pid is alive. After
29
+ // the supervisor exits the OS may hand the same pid to an unrelated
30
+ // process (browser tab, ssh, etc.) — without the start-time check we'd
31
+ // happily SIGTERM that.
32
+ export function readSupervisorPid(startDir = process.cwd()): number | null {
33
+ const p = getSupervisorPidPath(startDir);
34
+ if (!fs.existsSync(p)) return null;
35
+ const content = fs.readFileSync(p, 'utf-8').trim();
36
+ const sep = content.indexOf(':');
37
+ if (sep <= 0) return null;
38
+ const pid = parseInt(content.slice(0, sep), 10);
39
+ const storedStart = content.slice(sep + 1).trim();
40
+ if (!Number.isFinite(pid) || pid <= 0 || storedStart.length === 0) return null;
41
+ try {
42
+ process.kill(pid, 0);
43
+ } catch {
44
+ return null;
45
+ }
46
+ const currentStart = getProcessStartTime(pid);
47
+ if (currentStart === null || currentStart !== storedStart) return null;
48
+ return pid;
49
+ }
50
+
51
+ export function writeSupervisorPid(pid: number, startDir = process.cwd()): void {
52
+ const startTime = getProcessStartTime(pid);
53
+ if (!startTime) {
54
+ throw new Error(`Cannot read start time for pid ${pid}; refusing to write supervisor.pid`);
55
+ }
56
+ fs.writeFileSync(getSupervisorPidPath(startDir), `${pid}:${startTime}`);
57
+ }
58
+
59
+ export function removeSupervisorPid(startDir = process.cwd()): void {
60
+ const p = getSupervisorPidPath(startDir);
61
+ if (fs.existsSync(p)) {
62
+ try {
63
+ fs.unlinkSync(p);
64
+ } catch {
65
+ // best-effort cleanup
66
+ }
67
+ }
68
+ }