@huynle/hub 0.8.7 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/README.md +75 -16
  3. package/package.json +5 -1
  4. package/packages/claude-code/package.json +1 -1
  5. package/packages/claude-code/src/wrapper.ts +2 -2
  6. package/packages/cli/dist/hub-cli.js +1413 -523
  7. package/packages/cli/package.json +1 -1
  8. package/packages/cli/src/bootstrap/PHASE3-CONTRACT.md +112 -0
  9. package/packages/cli/src/bootstrap/adapters/node-services/control.ts +164 -21
  10. package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +24 -7
  11. package/packages/cli/src/bootstrap/adapters/node-services/render.ts +116 -9
  12. package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +8 -2
  13. package/packages/cli/src/bootstrap/contracts/actions.ts +8 -6
  14. package/packages/cli/src/bootstrap/contracts/discovery.ts +4 -3
  15. package/packages/cli/src/bootstrap/contracts/exec.ts +4 -2
  16. package/packages/cli/src/bootstrap/contracts/paths.ts +2 -1
  17. package/packages/cli/src/bootstrap/contracts/platform.ts +5 -5
  18. package/packages/cli/src/bootstrap/discovery/discover.ts +7 -2
  19. package/packages/cli/src/bootstrap/discovery/host.ts +2 -0
  20. package/packages/cli/src/bootstrap/preview/plan.ts +5 -1
  21. package/packages/cli/src/bootstrap/profiles/desired.ts +16 -1
  22. package/packages/cli/src/bootstrap/profiles/history.ts +2 -1
  23. package/packages/cli/src/bootstrap/profiles/upgrade.ts +2 -2
  24. package/packages/cli/src/bootstrap/release/assets.ts +4 -1
  25. package/packages/cli/src/cli.ts +23 -10
  26. package/packages/cli/src/http.ts +1 -0
  27. package/packages/cli/src/launches.ts +5 -1
  28. package/packages/cli/src/mcp.ts +28 -8
  29. package/packages/cli/src/messaging.ts +14 -0
  30. package/packages/cli/src/node.ts +94 -12
  31. package/packages/cli/src/setup/commands.ts +27 -7
  32. package/packages/cli/src/setup/runtime.ts +31 -4
  33. package/packages/cli/src/team-intake-mcp.ts +26 -2
  34. package/packages/codex/package.json +1 -1
  35. package/packages/codex/src/wrapper.ts +2 -2
  36. package/packages/node/package.json +1 -1
  37. package/packages/node/src/config.ts +49 -17
  38. package/packages/node/src/git.ts +16 -2
  39. package/packages/node/src/harnesses.ts +54 -9
  40. package/packages/node/src/host.ts +61 -15
  41. package/packages/node/src/opencode-service.ts +111 -0
  42. package/packages/node/src/opencode-warm.ts +11 -5
  43. package/packages/node/src/run.ts +50 -5
  44. package/packages/preset/package.json +1 -1
  45. package/packages/protocol/package.json +1 -1
  46. package/packages/protocol/schema/app-delivery/v1.schema.json +677 -0
  47. package/packages/protocol/src/artifacts.ts +43 -0
  48. package/packages/protocol/src/credentials.ts +38 -0
  49. package/packages/protocol/src/index.ts +59 -6
  50. package/packages/protocol/src/launch.ts +46 -6
  51. package/packages/protocol/src/markdown.ts +753 -0
  52. package/packages/protocol/src/media-allowlist.ts +240 -0
  53. package/packages/protocol/src/media.ts +120 -0
  54. package/packages/protocol/src/preview-bundle.ts +221 -0
  55. package/packages/protocol/src/voice-recording.ts +8 -0
  56. package/packages/runtime-openshell/package.json +1 -1
  57. package/packages/runtime-sdk/package.json +1 -1
  58. package/packages/sdk/package.json +1 -1
  59. package/packages/sdk/src/client.ts +17 -3
  60. package/packages/sdk/src/errors.ts +1 -0
  61. package/packages/sdk/src/team-intake.ts +8 -0
  62. package/packages/sdk/src/types.ts +16 -2
  63. package/packages/spoke-sdk/package.json +1 -1
  64. package/src/central/agent/delivery.ts +35 -6
  65. package/src/central/agent/errors.ts +2 -0
  66. package/src/central/agent/events.ts +11 -2
  67. package/src/central/agent/launch-engine.ts +65 -4
  68. package/src/central/agent/launch-routes.ts +8 -0
  69. package/src/central/agent/launch-spec.ts +22 -3
  70. package/src/central/agent/launch-store.ts +2 -2
  71. package/src/central/agent/message-routes.ts +49 -11
  72. package/src/central/agent/messages.ts +17 -4
  73. package/src/central/agent/not-after.ts +28 -0
  74. package/src/central/agent/placement.ts +36 -4
  75. package/src/central/agent/routes.ts +3 -1
  76. package/src/central/agent/webhook-routes.ts +21 -9
  77. package/src/central/agent/webhooks.ts +27 -9
  78. package/src/central/artifacts/host.ts +3 -1
  79. package/src/central/artifacts/maintenance.ts +2 -1
  80. package/src/central/artifacts/preview-bundle.ts +81 -0
  81. package/src/central/artifacts/previews.ts +94 -0
  82. package/src/central/artifacts/routes.ts +12 -1
  83. package/src/central/artifacts/store.ts +198 -8
  84. package/src/central/config.ts +34 -0
  85. package/src/central/fetch-budget.ts +59 -0
  86. package/src/central/fetcher.ts +372 -0
  87. package/src/central/http.ts +277 -22
  88. package/src/central/install/CONTRACT.md +4 -4
  89. package/src/central/install/install.ps1 +37 -5
  90. package/src/central/install/render.ts +13 -8
  91. package/src/central/media-remote.ts +248 -0
  92. package/src/central/media-timeline.ts +317 -0
  93. package/src/central/media-unfurl.ts +119 -0
  94. package/src/central/profiles/content.ts +3 -35
  95. package/src/central/recording/routes.ts +2 -2
  96. package/src/central/recording/schema.ts +3 -1
  97. package/src/central/registry.ts +449 -21
  98. package/src/central/spoke-ws.ts +17 -3
  99. package/src/central/store.ts +21 -0
  100. package/src/central/teams/intake-routes.ts +2 -1
  101. package/src/central/teams/intake.ts +200 -15
  102. package/src/central/unfurl-parse.ts +313 -0
  103. package/src/index.ts +68 -8
  104. package/src/speakable.ts +33 -1
  105. package/src/spoke/conversation.ts +7 -4
  106. package/src/spoke/deliver.ts +19 -0
  107. package/src/spoke/media-bytes.ts +171 -0
  108. package/src/spoke/media-policy.ts +394 -0
  109. package/src/spoke/media.ts +189 -0
  110. package/src/spoke/own-server.ts +30 -0
  111. package/src/spoke/preview-bundle.ts +538 -0
  112. package/src/spoke/spoke.ts +387 -17
  113. package/src/spoke/timeline.ts +59 -5
  114. package/src/spoke/voice-front.ts +335 -0
  115. package/web/dist/assets/24BXRY42-Zfr7jm_p.js +2 -0
  116. package/web/dist/assets/4FHPYW2L-C6Z67098.js +2 -0
  117. package/web/dist/assets/4LWW3ZNH-CUiYeygH.js +1 -0
  118. package/web/dist/assets/5THPTBWS-Ce_hS451.js +1 -0
  119. package/web/dist/assets/5XYWFMKW-CW-BCKiK.js +4 -0
  120. package/web/dist/assets/64PMEAB3-BjpCDGU9.js +8 -0
  121. package/web/dist/assets/6BEWHPQE-BpNa96Ze.js +2 -0
  122. package/web/dist/assets/6PIGVMFT-YXn9Hgnb.js +1 -0
  123. package/web/dist/assets/75JW6PN6-BFpDBuGN.js +2 -0
  124. package/web/dist/assets/7WLXJQOR-D6QVYUu9.js +1 -0
  125. package/web/dist/assets/7XWI72E6-BMuIspEy.js +2 -0
  126. package/web/dist/assets/A4UD2L5Z-BQzcD9dU.js +8 -0
  127. package/web/dist/assets/ArtifactPage-BKISN-Qj.js +1 -0
  128. package/web/dist/assets/ArtifactPane-CKOiAUNH.js +1 -0
  129. package/web/dist/assets/ArtifactWorkbench-d7R1o-rs.js +6 -0
  130. package/web/dist/assets/BGW36YJW-CJMhya8S.js +4 -0
  131. package/web/dist/assets/CGIZI67J-DYDSLME8.js +2 -0
  132. package/web/dist/assets/DKTLEL5E-DciMwFJ1.js +1 -0
  133. package/web/dist/assets/DNNCRC2E-BIeKf5t-.js +5 -0
  134. package/web/dist/assets/Diagram-azw2B_QH.js +1 -0
  135. package/web/dist/assets/Diagram-vdpmp3xu.css +1 -0
  136. package/web/dist/assets/E67EBMDD--AucIw59.js +5 -0
  137. package/web/dist/assets/EFYWJBJK-CVywe_D5.js +1 -0
  138. package/web/dist/assets/EXE5EH6O-DR1IHCTr.js +4 -0
  139. package/web/dist/assets/FTXIJQGB-BZVuiBn1.js +1 -0
  140. package/web/dist/assets/G3MDBVQH-BZmnMkFw.js +1 -0
  141. package/web/dist/assets/G6GPDDNC-DEz8F449.js +2 -0
  142. package/web/dist/assets/G76NGGFM-CrB2V34K.js +1 -0
  143. package/web/dist/assets/GH6G3TJC-D3RTYEH4.js +2 -0
  144. package/web/dist/assets/HPNUA7RO-B19NeNOJ.js +1 -0
  145. package/web/dist/assets/HQRGC5D7-Dmpzq6Rn.js +4 -0
  146. package/web/dist/assets/IX6Z2TVR-DphRHnek.js +5 -0
  147. package/web/dist/assets/J77NQRCJ-B7AGcxvw.js +3 -0
  148. package/web/dist/assets/JAD36DTZ-B9-dbsS0.js +5 -0
  149. package/web/dist/assets/LF3NJQAX-koL91UZ9.js +2 -0
  150. package/web/dist/assets/LRZCOFJZ-Cdjasfuf.js +6 -0
  151. package/web/dist/assets/Library-CcjZXsfF.js +2 -0
  152. package/web/dist/assets/Lightbox-C8KNKwOA.css +1 -0
  153. package/web/dist/assets/Lightbox-Du56MXGX.js +1 -0
  154. package/web/dist/assets/MediaImage-Ds_JUx3a.js +4 -0
  155. package/web/dist/assets/N5NF2FGY-OuapYTfr.js +2 -0
  156. package/web/dist/assets/N7OWWQHY-BNLd-Q_U.js +2 -0
  157. package/web/dist/assets/NT5JY5WP-1lJM-aun.js +1 -0
  158. package/web/dist/assets/OT7W3RNY-jDL3drzX.js +4 -0
  159. package/web/dist/assets/P67P66DB-CE-opRb8.js +3 -0
  160. package/web/dist/assets/PFLLLM7K-DDJFgzW3.js +2 -0
  161. package/web/dist/assets/PUAMC4MJ-CtBm-WuU.js +12 -0
  162. package/web/dist/assets/PVFCHRLY-CG5JXv60.js +2 -0
  163. package/web/dist/assets/ProfilesDestination-DdzbzmeK.js +1 -0
  164. package/web/dist/assets/QQW2P742-BbcEkpa4.js +1 -0
  165. package/web/dist/assets/RVARI57O-CgA1WoRv.js +1 -0
  166. package/web/dist/assets/S63OEDPG-CKlhpvl2.js +3 -0
  167. package/web/dist/assets/SG5TVYLW-BBtiwXPL.js +2 -0
  168. package/web/dist/assets/TeamsPage-CDyjrK-I.js +1 -0
  169. package/web/dist/assets/UGRSUGA2-urZ_hx83.js +5 -0
  170. package/web/dist/assets/V44WA6U7-DYfX3OlM.js +1 -0
  171. package/web/dist/assets/V67O4WWV-xYxJ00QW.js +1 -0
  172. package/web/dist/assets/VBUCM3YD-JHGjuJC_.js +1 -0
  173. package/web/dist/assets/VINGRCPA-DZuJa3SH.js +7 -0
  174. package/web/dist/assets/WL6DNKX4-BHqripdA.js +2 -0
  175. package/web/dist/assets/WQ63CJYP-D6G5frDY.js +2 -0
  176. package/web/dist/assets/Watchtower-DYwoUWME.js +2 -0
  177. package/web/dist/assets/XOYAOB3S-L-KeA1EF.js +13 -0
  178. package/web/dist/assets/Y5LQLG3M-dr2-erb8.js +2 -0
  179. package/web/dist/assets/YIK6JTN6--1t-Ud8k.js +2 -0
  180. package/web/dist/assets/artifacts-DU9uRgs1.js +1 -0
  181. package/web/dist/assets/index-C3ci5UtY.css +1 -0
  182. package/web/dist/assets/index-LNMZCqgE.js +36 -0
  183. package/web/dist/assets/lazy-f3CuUNJZ.js +43 -0
  184. package/web/dist/assets/live-ycZ_aQZO.js +5 -0
  185. package/web/dist/assets/mermaid.worker-WDVRAh32.js +1 -0
  186. package/web/dist/assets/pane-layout-0RrLHGDr.js +1 -0
  187. package/web/dist/assets/preload-helper-BLdHbfdN.js +1 -0
  188. package/web/dist/assets/{renderer2d-CYag1CnR.js → renderer2d-BIwbgRoX.js} +1 -1
  189. package/web/dist/assets/{renderer3d-BRB6rgDJ.js → renderer3d-C1a7eYGQ.js} +1 -1
  190. package/web/dist/assets/{safe-area-fOy0AOuG.js → safe-area-DHhPy61Q.js} +1 -1
  191. package/web/dist/assets/{silero-worker-DhlIHyD_.js → silero-worker-e802lB5T.js} +1 -1
  192. package/web/dist/assets/smart-turn-v3.2-cpu-WpeLRbPA.onnx +0 -0
  193. package/web/dist/assets/smart-turn-worker-DaVJPdvJ.js +11 -0
  194. package/web/dist/assets/source-BWlvaODa.js +1 -0
  195. package/web/dist/assets/toast-Cuu2K0hx.js +1 -0
  196. package/web/dist/assets/ui-CFB4wQfF.js +3 -0
  197. package/web/dist/index.html +8 -6
  198. package/web/src/voice/turn/LICENSE +28 -0
  199. package/web/dist/assets/ArtifactPage-Dq7VW-bU.js +0 -1
  200. package/web/dist/assets/ArtifactPane-DKwbMTX9.js +0 -1
  201. package/web/dist/assets/ArtifactWorkbench-vrgIfncX.js +0 -6
  202. package/web/dist/assets/Library-D6-Go6Cu.js +0 -2
  203. package/web/dist/assets/ProfilesDestination-BOML2Dgf.js +0 -1
  204. package/web/dist/assets/TeamsPage-CNBTE8gB.js +0 -1
  205. package/web/dist/assets/Watchtower-Dor1Ubjy.js +0 -2
  206. package/web/dist/assets/hub-CrbItiwP.js +0 -13
  207. package/web/dist/assets/index-82Ce0IAf.css +0 -1
  208. package/web/dist/assets/index-D9SP3_0z.js +0 -22
  209. package/web/dist/assets/live-BNJgy0vg.js +0 -5
  210. package/web/dist/assets/pane-layout-EdR1kRrG.js +0 -1
  211. package/web/dist/assets/preload-helper-DqrJTq0t.js +0 -1
  212. package/web/dist/assets/source-Wbq35qGp.js +0 -1
  213. package/web/dist/assets/ui-j5FFk90B.js +0 -3
@@ -7,7 +7,7 @@ import { createInterface } from "readline";
7
7
  // packages/cli/package.json
8
8
  var package_default = {
9
9
  name: "@huynle/hub-cli",
10
- version: "0.8.7",
10
+ version: "0.9.1",
11
11
  private: true,
12
12
  description: "The hub CLI (`hub` / `hubctl`) for agents and scripts (internal; bundled into @huynle/hub, not published on its own).",
13
13
  license: "MIT",
@@ -710,7 +710,11 @@ var decisionFields = (options) => {
710
710
  var whyField = (options) => options.why !== undefined ? { why: options.why } : {};
711
711
  var signalOf = (options) => options.signal ? { signal: options.signal } : {};
712
712
  var idempotent = (options) => ({ ...signalOf(options), ...options.idempotencyKey ? { headers: { "idempotency-key": options.idempotencyKey } } : {} });
713
- var sendFields = (options) => ({ ...options.wake !== undefined ? { wake: options.wake } : {}, ...options.urgent !== undefined ? { urgent: options.urgent } : {} });
713
+ var sendFields = (options) => ({
714
+ ...options.wake !== undefined ? { wake: options.wake } : {},
715
+ ...options.urgent !== undefined ? { urgent: options.urgent } : {},
716
+ ...options.notAfter !== undefined ? { notAfter: options.notAfter } : {}
717
+ });
714
718
  var roleTarget = (options) => ({ ...options.session ? { session: options.session } : {}, ...options.project !== undefined ? { project: options.project } : {} });
715
719
 
716
720
  class HubClient {
@@ -916,7 +920,7 @@ class HubClient {
916
920
  return this.call("POST", `${AGENT}/roles/release`, { body: { role: bareRole(role), ...roleTarget(options) }, ...signalOf(options) });
917
921
  }
918
922
  msg(to, text, options = {}) {
919
- return this.call("POST", `${AGENT}/msg`, { body: { to, text, ...sendFields(options), ...options.all ? { all: true } : {} }, ...signalOf(options) });
923
+ return this.call("POST", `${AGENT}/msg`, { body: { to, text, ...sendFields(options), ...options.all ? { all: true } : {} }, ...idempotent(options) });
920
924
  }
921
925
  async ask(to, text, options = {}) {
922
926
  if (options.all && options.wait !== undefined)
@@ -933,7 +937,7 @@ class HubClient {
933
937
  ...options.wait !== undefined ? { wait: first } : {},
934
938
  ...decisionFields(options)
935
939
  };
936
- const asked = await this.call("POST", `${AGENT}/ask`, { body, ...signalOf(options) });
940
+ const asked = await this.call("POST", `${AGENT}/ask`, { body, ...idempotent(options) });
937
941
  if ("sent" in asked)
938
942
  return asked;
939
943
  let current = asked;
@@ -1110,6 +1114,7 @@ var EXIT_OF = {
1110
1114
  offline: EXIT.unavailable,
1111
1115
  busy: EXIT.unavailable,
1112
1116
  gone: EXIT.gone,
1117
+ expired: EXIT.gone,
1113
1118
  unauthorized: EXIT.unauthorized,
1114
1119
  token_expired: EXIT.unauthorized
1115
1120
  };
@@ -1690,7 +1695,7 @@ var SUPPORTED_TARGETS = Object.freeze([
1690
1695
  { target: "darwin-x64", platform: "darwin", arch: "x64", libc: "none", serviceManager: "launchd", bunTarget: "bun-darwin-x64-baseline" },
1691
1696
  { target: "linux-x64", platform: "linux", arch: "x64", libc: "glibc", serviceManager: "systemd-user", bunTarget: "bun-linux-x64-baseline" },
1692
1697
  { target: "linux-arm64", platform: "linux", arch: "arm64", libc: "glibc", serviceManager: "systemd-user", bunTarget: "bun-linux-arm64" },
1693
- { target: "windows-x64", platform: "windows", arch: "x64", libc: "none", serviceManager: "none", bunTarget: "bun-windows-x64-baseline" }
1698
+ { target: "windows-x64", platform: "windows", arch: "x64", libc: "none", serviceManager: "task-scheduler", bunTarget: "bun-windows-x64-baseline" }
1694
1699
  ]);
1695
1700
  var UNSUPPORTED_TARGETS = Object.freeze([
1696
1701
  { target: "windows-arm64", reason: "Windows on arm64 has no hubctl build yet (only windows-x64 is built)" },
@@ -1711,7 +1716,7 @@ function checkTarget(target) {
1711
1716
  return fail("", "unsupported_platform", known ? `${target} is not supported: ${known.reason}` : `${target} is not a supported target (supported: ${SUPPORTED_TARGETS.map((t) => t.target).join(", ")})`);
1712
1717
  }
1713
1718
  // packages/cli/src/bootstrap/contracts/paths.ts
1714
- var ROOT_IDS = ["home", "config", "state", "data", "user-bin", "launch-agents", "systemd-user", "system-bin"];
1719
+ var ROOT_IDS = ["home", "config", "state", "data", "user-bin", "launch-agents", "systemd-user", "system-bin", "task-defs"];
1715
1720
  var ELEVATED_ROOTS = ["system-bin"];
1716
1721
  var BAD_REL = /[\u0000-\u001f\u007f\\]/;
1717
1722
  function isSafeRel(rel) {
@@ -1793,7 +1798,8 @@ var EMBEDDED_ASSETS = Object.freeze({
1793
1798
  "service/launchd/claude-code": { kind: "launchd-plist", service: "claude-code", manager: "launchd", source: "scripts/io.github.huynle.hub-claude-code.plist", installAs: "io.github.huynle.hub-claude-code.plist" },
1794
1799
  "service/systemd-user/claude-code": { kind: "systemd-unit", service: "claude-code", manager: "systemd-user", source: "scripts/hub-claude-code.service", installAs: "hub-claude-code.service" },
1795
1800
  "service/launchd/codex-spoke": { kind: "launchd-plist", service: "codex-spoke", manager: "launchd", source: "scripts/io.github.huynle.hub-codex-spoke.plist", installAs: "io.github.huynle.hub-codex-spoke.plist" },
1796
- "service/systemd-user/codex-spoke": { kind: "systemd-unit", service: "codex-spoke", manager: "systemd-user", source: "scripts/hub-codex-spoke.service", installAs: "hub-codex-spoke.service" }
1801
+ "service/systemd-user/codex-spoke": { kind: "systemd-unit", service: "codex-spoke", manager: "systemd-user", source: "scripts/hub-codex-spoke.service", installAs: "hub-codex-spoke.service" },
1802
+ "service/task-scheduler/node": { kind: "task-xml", service: "node", manager: "task-scheduler", source: "scripts/hub-node.task.xml", installAs: "hub-node.xml" }
1797
1803
  });
1798
1804
  // packages/cli/src/bootstrap/contracts/harness.ts
1799
1805
  var HARNESS_ORDER = ["opencode", "claude-code", "codex"];
@@ -1879,6 +1885,7 @@ function createAdapterRegistry() {
1879
1885
  // packages/cli/src/bootstrap/contracts/actions.ts
1880
1886
  var EXEC_KEYS = ["command", "cmd", "commands", "shell", "script", "scripts", "exec", "execute", "argv", "args", "arguments", "run", "eval", "sh", "bash", "program", "spawn", "hook", "hooks"];
1881
1887
  var CONFIG_OWNERS = ["user", "hub", ...HARNESS_ORDER.map((h) => `harness:${h}`)];
1888
+ var SERVICE_MANAGERS = ["launchd", "systemd-user", "task-scheduler"];
1882
1889
  var harnessVersionRef = (harness, version) => ({ root: "data", rel: `hub/harnesses/${harness}/${version}` });
1883
1890
  var harnessCurrentRef = (harness) => ({ root: "data", rel: `hub/harnesses/${harness}/current` });
1884
1891
  var COMMON2 = ["id", "kind", "escalation", "summary"];
@@ -1907,7 +1914,7 @@ var ASSET_ID = /^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/;
1907
1914
  var MACHINE_NAME = /^[A-Za-z0-9._-]{1,64}$/;
1908
1915
  var MCP_NAME = /^[a-z0-9][a-z0-9-]{0,31}$/;
1909
1916
  var JSON_POINTER = /^(?:\/(?:[^~/]|~[01])*)+$/;
1910
- var SERVICE_ROOT = { launchd: "launch-agents", "systemd-user": "systemd-user" };
1917
+ var SERVICE_ROOT = Object.freeze({ launchd: "launch-agents", "systemd-user": "systemd-user", "task-scheduler": "task-defs" });
1911
1918
  var EXEC = new Set(EXEC_KEYS);
1912
1919
  function findExecKeys(value, base = "") {
1913
1920
  const found = [];
@@ -2094,7 +2101,7 @@ function checkAction(c, path, value) {
2094
2101
  break;
2095
2102
  case "install-service": {
2096
2103
  oneOf(c, at("service"), r.service, SERVICE_IDS);
2097
- oneOf(c, at("manager"), r.manager, ["launchd", "systemd-user"]);
2104
+ oneOf(c, at("manager"), r.manager, SERVICE_MANAGERS);
2098
2105
  ref("path");
2099
2106
  digest(c, at("sha256"), r.sha256);
2100
2107
  const asset = typeof r.asset === "string" ? EMBEDDED_ASSETS[r.asset] : undefined;
@@ -2107,18 +2114,18 @@ function checkAction(c, path, value) {
2107
2114
  }
2108
2115
  case "deactivate-service":
2109
2116
  oneOf(c, at("service"), r.service, SERVICE_IDS);
2110
- oneOf(c, at("manager"), r.manager, ["launchd", "systemd-user"]);
2117
+ oneOf(c, at("manager"), r.manager, SERVICE_MANAGERS);
2111
2118
  break;
2112
2119
  case "remove-service":
2113
2120
  oneOf(c, at("service"), r.service, SERVICE_IDS);
2114
- oneOf(c, at("manager"), r.manager, ["launchd", "systemd-user"]);
2121
+ oneOf(c, at("manager"), r.manager, SERVICE_MANAGERS);
2115
2122
  ref("path");
2116
2123
  digest(c, at("sha256"), r.sha256);
2117
2124
  checkServiceUnitPath(c, at, r);
2118
2125
  break;
2119
2126
  case "activate-service":
2120
2127
  oneOf(c, at("service"), r.service, SERVICE_IDS);
2121
- oneOf(c, at("manager"), r.manager, ["launchd", "systemd-user"]);
2128
+ oneOf(c, at("manager"), r.manager, SERVICE_MANAGERS);
2122
2129
  if (r.confirm !== "explicit")
2123
2130
  c.add(at("confirm"), "invalid_value", "services are only activated on explicit confirmation");
2124
2131
  break;
@@ -2252,12 +2259,14 @@ function validateDiscoverySnapshot(value) {
2252
2259
  semver(c, pointer(p, "version"), rt.version);
2253
2260
  absPath(c, pointer(p, "path"), rt.path);
2254
2261
  });
2255
- oneOf(c, "/managers/service", managers.service, ["launchd", "systemd-user", "none"]);
2262
+ oneOf(c, "/managers/service", managers.service, ["launchd", "systemd-user", "task-scheduler", "none"]);
2256
2263
  const platform = host?.platform;
2257
2264
  if (managers.service === "launchd" && platform !== "darwin")
2258
2265
  c.add("/managers/service", "inconsistent", "launchd is macOS only");
2259
2266
  if (managers.service === "systemd-user" && platform !== "linux")
2260
2267
  c.add("/managers/service", "inconsistent", "systemd is Linux only");
2268
+ if (managers.service === "task-scheduler" && platform !== "win32")
2269
+ c.add("/managers/service", "inconsistent", "Task Scheduler is Windows only");
2261
2270
  }
2262
2271
  list(c, "/harnesses", r.harnesses, HARNESS_ORDER.length, (p, v) => checkHarness(c, p, v), (v) => v?.id);
2263
2272
  list(c, "/runtimes", r.runtimes, RUNTIME_IDS.length, (p, v) => {
@@ -2275,7 +2284,7 @@ function validateDiscoverySnapshot(value) {
2275
2284
  return;
2276
2285
  absPath(c, pointer(p, "path"), cf.path);
2277
2286
  oneOf(c, pointer(p, "owner"), cf.owner, [...CONFIG_OWNERS, "package-manager"]);
2278
- oneOf(c, pointer(p, "format"), cf.format, ["json", "jsonc", "toml", "plist", "unit"]);
2287
+ oneOf(c, pointer(p, "format"), cf.format, ["json", "jsonc", "toml", "plist", "unit", "xml"]);
2279
2288
  if (cf.sha256 !== null)
2280
2289
  digest(c, pointer(p, "sha256"), cf.sha256);
2281
2290
  }, (v) => v?.path);
@@ -3349,6 +3358,99 @@ function parseFrame(raw) {
3349
3358
  }
3350
3359
  }
3351
3360
 
3361
+ // packages/protocol/src/launch.ts
3362
+ var WORKSPACE_MODES = ["worktree", "checkout", "clone"];
3363
+ var RUNTIME_KINDS = ["host", "openshell"];
3364
+ var LAUNCH_END_WHEN = ["idle", "asked", "never"];
3365
+ var CLEANUP_POLICIES = ["keep", "remove"];
3366
+ var PATH_MAX = 4096;
3367
+ var REF_MAX = 200;
3368
+ var REMOTE_MAX = 2048;
3369
+ var LABELS_MAX = 32;
3370
+ var LABEL_VALUE_MAX = 256;
3371
+ var LABEL_KEY = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,62}$/;
3372
+ function isAbsolutePath2(v) {
3373
+ if (typeof v !== "string" || v.length > PATH_MAX || UNSAFE_CHARS.test(v))
3374
+ return false;
3375
+ if (WINDOWS_ROOT2.test(v))
3376
+ return !v.includes("\\") && normalSegments(v.slice(2));
3377
+ if (!v.startsWith("/"))
3378
+ return false;
3379
+ return normalSegments(v);
3380
+ }
3381
+ function normalSegments(rest) {
3382
+ if (!rest.startsWith("/"))
3383
+ return false;
3384
+ if (rest === "/")
3385
+ return true;
3386
+ return rest.slice(1).replace(/\/$/, "").split("/").every((part) => part !== "" && part !== "." && part !== "..");
3387
+ }
3388
+ var WINDOWS_ROOT2 = /^[A-Z]:(?=\/)/;
3389
+ var isWindowsAbsolutePath = (v) => typeof v === "string" && WINDOWS_ROOT2.test(v) && isAbsolutePath2(v);
3390
+ function canonicalNativePath(value) {
3391
+ if (typeof value !== "string")
3392
+ return;
3393
+ let p = value.replace(/^(\\\\\?\\|\\\?\?\\)(?=[A-Za-z]:\\)/, "");
3394
+ if (/^[A-Za-z]:[\\/]/.test(p))
3395
+ p = p[0].toUpperCase() + p.slice(1).replace(/\\/g, "/");
3396
+ else if (!p.startsWith("/") || p.startsWith("//"))
3397
+ return;
3398
+ p = p.replace(/\/{2,}/g, "/");
3399
+ if (p.endsWith("/") && p !== "/" && !/^[A-Z]:\/$/.test(p))
3400
+ p = p.slice(0, -1);
3401
+ return isAbsolutePath2(p) ? p : undefined;
3402
+ }
3403
+ function isRefName(v) {
3404
+ if (typeof v !== "string" || v.length === 0 || v.length > REF_MAX)
3405
+ return false;
3406
+ if (!/^[A-Za-z0-9._/-]+$/.test(v) || v.startsWith("-") || v.endsWith(".") || v.includes(".."))
3407
+ return false;
3408
+ return v.split("/").every((part) => part !== "" && !part.startsWith(".") && !part.endsWith(".lock"));
3409
+ }
3410
+ function isGitRemote(v) {
3411
+ if (typeof v !== "string" || v.length === 0 || v.length > REMOTE_MAX || v.startsWith("-") || /[\s\p{Cc}\p{Cf}]/u.test(v))
3412
+ return false;
3413
+ if (/^[A-Za-z0-9][A-Za-z0-9._-]*@[A-Za-z0-9][A-Za-z0-9.-]*:[^-:][^:]*$/.test(v))
3414
+ return true;
3415
+ try {
3416
+ const url = new URL(v);
3417
+ return (url.protocol === "https:" || url.protocol === "ssh:") && url.hostname.length > 0 && !url.hostname.startsWith("-") && url.pathname.length > 1;
3418
+ } catch {
3419
+ return false;
3420
+ }
3421
+ }
3422
+ function launchWorkspaceProblem(v) {
3423
+ if (!isObj(v))
3424
+ return "workspace must be an object";
3425
+ if (!isAbsolutePath2(v.repo))
3426
+ return `workspace.repo must be an absolute path of at most ${PATH_MAX} characters without . or .. segments`;
3427
+ if (!WORKSPACE_MODES.includes(v.mode))
3428
+ return `workspace.mode must be one of ${WORKSPACE_MODES.join(", ")}`;
3429
+ if (v.branch !== undefined && !isRefName(v.branch))
3430
+ return "workspace.branch must be a valid branch name (letters, digits, . _ / -; not starting with -)";
3431
+ if (v.base !== undefined && !isRefName(v.base))
3432
+ return "workspace.base must be a valid branch name (letters, digits, . _ / -; not starting with -)";
3433
+ if (v.remote !== undefined && !isGitRemote(v.remote))
3434
+ return "workspace.remote must be an https or ssh git remote";
3435
+ if (v.mode === "clone" && v.remote === undefined)
3436
+ return "workspace.remote is required for a clone";
3437
+ return;
3438
+ }
3439
+ function labelsProblem(v) {
3440
+ if (!isObj(v))
3441
+ return "labels must be an object";
3442
+ const entries = Object.entries(v);
3443
+ if (entries.length > LABELS_MAX)
3444
+ return `at most ${LABELS_MAX} labels`;
3445
+ for (const [key, value] of entries) {
3446
+ if (!LABEL_KEY.test(key))
3447
+ return `label key ${JSON.stringify(key.slice(0, 80))} must be 1–63 letters, digits, . _ / - (starting with a letter or digit)`;
3448
+ if (typeof value !== "string" || value.length > LABEL_VALUE_MAX || UNSAFE_CHARS.test(value))
3449
+ return `label ${key} must be text of at most ${LABEL_VALUE_MAX} printable characters`;
3450
+ }
3451
+ return;
3452
+ }
3453
+
3352
3454
  // packages/protocol/src/contracts.ts
3353
3455
  var SLUG = /^[a-z0-9-]{2,40}$/;
3354
3456
  var isSlug = (v) => typeof v === "string" && SLUG.test(v);
@@ -3551,6 +3653,113 @@ function sessionContextProblem(v) {
3551
3653
  return;
3552
3654
  }
3553
3655
 
3656
+ // packages/protocol/src/media.ts
3657
+ var MEDIA_LIMITS = {
3658
+ maxBytes: 8 * 1024 * 1024,
3659
+ maxPixels: 40000000,
3660
+ maxSide: 16384,
3661
+ chunkBytes: 512 * 1024,
3662
+ perRow: 8
3663
+ };
3664
+ var MEDIA_PATH_MAX = 4096;
3665
+ var MEDIA_REF_MAX = 2 + Math.ceil(MEDIA_PATH_MAX * 4 / 3) + 4;
3666
+ var FILE_REF = /^f(0|[1-9]\d{0,2})$/;
3667
+ var TOOL_REF = /^t(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,2})$/;
3668
+ var PATH_REF = /^p:([A-Za-z0-9_-]+)$/;
3669
+ function base64url(bytes) {
3670
+ let binary = "";
3671
+ for (const byte of bytes)
3672
+ binary += String.fromCharCode(byte);
3673
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
3674
+ }
3675
+ function fromBase64url(text) {
3676
+ if (text.length % 4 === 1)
3677
+ return;
3678
+ try {
3679
+ const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
3680
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
3681
+ } catch {
3682
+ return;
3683
+ }
3684
+ }
3685
+ function mediaPathRef(path) {
3686
+ return `p:${base64url(new TextEncoder().encode(path))}`;
3687
+ }
3688
+ function parseMediaRef(ref) {
3689
+ if (typeof ref !== "string" || ref.length > MEDIA_REF_MAX)
3690
+ return;
3691
+ const file = FILE_REF.exec(ref);
3692
+ if (file)
3693
+ return { kind: "file", index: Number(file[1]) };
3694
+ const tool = TOOL_REF.exec(ref);
3695
+ if (tool)
3696
+ return { kind: "tool", part: Number(tool[1]), item: Number(tool[2]) };
3697
+ const path = PATH_REF.exec(ref);
3698
+ if (!path)
3699
+ return;
3700
+ const bytes = fromBase64url(path[1]);
3701
+ if (!bytes || bytes.length === 0 || bytes.length > MEDIA_PATH_MAX)
3702
+ return;
3703
+ let text;
3704
+ try {
3705
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
3706
+ } catch {
3707
+ return;
3708
+ }
3709
+ if (text.includes("\x00") || mediaPathRef(text) !== ref)
3710
+ return;
3711
+ return { kind: "path", path: text };
3712
+ }
3713
+ var isMediaOffset = (offset) => Number.isInteger(offset) && offset >= 0 && offset <= MEDIA_LIMITS.maxBytes;
3714
+
3715
+ // packages/protocol/src/preview-bundle.ts
3716
+ var PREVIEW_BUNDLE_FEATURE = "previewBundle";
3717
+ var PREVIEW_BUNDLE_LIMITS = {
3718
+ files: 200,
3719
+ fileBytes: 5 * 1024 * 1024,
3720
+ totalBytes: 20 * 1024 * 1024,
3721
+ chunkBytes: 512 * 1024,
3722
+ inlineBytes: 384 * 1024,
3723
+ bundlePathMax: 200,
3724
+ pathMax: 4096,
3725
+ refMax: 300,
3726
+ skipped: 200
3727
+ };
3728
+ var PREVIEW_MEDIA_TYPES = {
3729
+ html: "text/html",
3730
+ htm: "text/html",
3731
+ css: "text/css",
3732
+ js: "text/javascript",
3733
+ mjs: "text/javascript",
3734
+ json: "application/json",
3735
+ txt: "text/plain",
3736
+ csv: "text/csv",
3737
+ md: "text/markdown",
3738
+ pdf: "application/pdf",
3739
+ png: "image/png",
3740
+ jpg: "image/jpeg",
3741
+ jpeg: "image/jpeg",
3742
+ gif: "image/gif",
3743
+ webp: "image/webp",
3744
+ svg: "image/svg+xml"
3745
+ };
3746
+ var MEDIA_TYPES = new Set(Object.values(PREVIEW_MEDIA_TYPES));
3747
+ var HEX64 = /^[0-9a-f]{64}$/;
3748
+ var int2 = (v, min, max) => Number.isSafeInteger(v) && v >= min && v <= max;
3749
+ var isPreviewPath = (v) => typeof v === "string" && v.length > 0 && v.length <= PREVIEW_BUNDLE_LIMITS.pathMax && !/[\u0000\r\n]/.test(v);
3750
+ function isPreviewBundleRequest(v) {
3751
+ if (typeof v.sessionID !== "string" || !v.sessionID)
3752
+ return false;
3753
+ if (typeof v.messageID !== "string" || !v.messageID || v.messageID.length > 300)
3754
+ return false;
3755
+ if (!isPreviewPath(v.path))
3756
+ return false;
3757
+ const chunk = [v.bundle, v.file, v.offset].filter((x) => x !== undefined).length;
3758
+ if (chunk === 0)
3759
+ return true;
3760
+ return chunk === 3 && typeof v.bundle === "string" && HEX64.test(v.bundle) && int2(v.file, 0, PREVIEW_BUNDLE_LIMITS.files - 1) && int2(v.offset, 0, PREVIEW_BUNDLE_LIMITS.fileBytes - 1);
3761
+ }
3762
+
3554
3763
  // packages/protocol/src/harness-events.ts
3555
3764
  var HARNESS_EVENTS = [
3556
3765
  "session.created",
@@ -3628,74 +3837,6 @@ function harnessEventProblem(event) {
3628
3837
  return "event data must be an object";
3629
3838
  return CHECKS[event.type](event.data);
3630
3839
  }
3631
- // packages/protocol/src/launch.ts
3632
- var WORKSPACE_MODES = ["worktree", "checkout", "clone"];
3633
- var RUNTIME_KINDS = ["host", "openshell"];
3634
- var LAUNCH_END_WHEN = ["idle", "asked", "never"];
3635
- var CLEANUP_POLICIES = ["keep", "remove"];
3636
- var PATH_MAX = 4096;
3637
- var REF_MAX = 200;
3638
- var REMOTE_MAX = 2048;
3639
- var LABELS_MAX = 32;
3640
- var LABEL_VALUE_MAX = 256;
3641
- var LABEL_KEY = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,62}$/;
3642
- function isAbsolutePath2(v) {
3643
- if (typeof v !== "string" || !v.startsWith("/") || v.length > PATH_MAX || UNSAFE_CHARS.test(v))
3644
- return false;
3645
- if (v === "/")
3646
- return true;
3647
- return v.slice(1).replace(/\/$/, "").split("/").every((part) => part !== "" && part !== "." && part !== "..");
3648
- }
3649
- function isRefName(v) {
3650
- if (typeof v !== "string" || v.length === 0 || v.length > REF_MAX)
3651
- return false;
3652
- if (!/^[A-Za-z0-9._/-]+$/.test(v) || v.startsWith("-") || v.endsWith(".") || v.includes(".."))
3653
- return false;
3654
- return v.split("/").every((part) => part !== "" && !part.startsWith(".") && !part.endsWith(".lock"));
3655
- }
3656
- function isGitRemote(v) {
3657
- if (typeof v !== "string" || v.length === 0 || v.length > REMOTE_MAX || v.startsWith("-") || /[\s\p{Cc}\p{Cf}]/u.test(v))
3658
- return false;
3659
- if (/^[A-Za-z0-9][A-Za-z0-9._-]*@[A-Za-z0-9][A-Za-z0-9.-]*:[^-:][^:]*$/.test(v))
3660
- return true;
3661
- try {
3662
- const url = new URL(v);
3663
- return (url.protocol === "https:" || url.protocol === "ssh:") && url.hostname.length > 0 && !url.hostname.startsWith("-") && url.pathname.length > 1;
3664
- } catch {
3665
- return false;
3666
- }
3667
- }
3668
- function launchWorkspaceProblem(v) {
3669
- if (!isObj(v))
3670
- return "workspace must be an object";
3671
- if (!isAbsolutePath2(v.repo))
3672
- return `workspace.repo must be an absolute path of at most ${PATH_MAX} characters without . or .. segments`;
3673
- if (!WORKSPACE_MODES.includes(v.mode))
3674
- return `workspace.mode must be one of ${WORKSPACE_MODES.join(", ")}`;
3675
- if (v.branch !== undefined && !isRefName(v.branch))
3676
- return "workspace.branch must be a valid branch name (letters, digits, . _ / -; not starting with -)";
3677
- if (v.base !== undefined && !isRefName(v.base))
3678
- return "workspace.base must be a valid branch name (letters, digits, . _ / -; not starting with -)";
3679
- if (v.remote !== undefined && !isGitRemote(v.remote))
3680
- return "workspace.remote must be an https or ssh git remote";
3681
- if (v.mode === "clone" && v.remote === undefined)
3682
- return "workspace.remote is required for a clone";
3683
- return;
3684
- }
3685
- function labelsProblem(v) {
3686
- if (!isObj(v))
3687
- return "labels must be an object";
3688
- const entries = Object.entries(v);
3689
- if (entries.length > LABELS_MAX)
3690
- return `at most ${LABELS_MAX} labels`;
3691
- for (const [key, value] of entries) {
3692
- if (!LABEL_KEY.test(key))
3693
- return `label key ${JSON.stringify(key.slice(0, 80))} must be 1–63 letters, digits, . _ / - (starting with a letter or digit)`;
3694
- if (typeof value !== "string" || value.length > LABEL_VALUE_MAX || UNSAFE_CHARS.test(value))
3695
- return `label ${key} must be text of at most ${LABEL_VALUE_MAX} printable characters`;
3696
- }
3697
- return;
3698
- }
3699
3840
  // packages/protocol/src/node.ts
3700
3841
  var NODE_ROLE = "node";
3701
3842
  var NODE_FEATURES = ["sandbox"];
@@ -3936,6 +4077,60 @@ var ARTIFACT_LIMITS = {
3936
4077
  aiJobsPerArtifact: 2,
3937
4078
  aiInstructionChars: 8000
3938
4079
  };
4080
+ // packages/protocol/src/media-allowlist.ts
4081
+ var PUBLIC_SUFFIXES = new Set([
4082
+ ...["co", "org", "ac", "gov", "net", "me", "ltd", "plc", "sch", "nhs"].map((s) => `${s}.uk`),
4083
+ ...["com", "net", "org", "edu", "gov", "id", "asn"].map((s) => `${s}.au`),
4084
+ ...["co", "ne", "or", "ac", "go", "gr", "ed", "ad", "lg"].map((s) => `${s}.jp`),
4085
+ ...["co", "org", "net", "ac", "govt", "school", "geek", "gen", "kiwi", "maori"].map((s) => `${s}.nz`),
4086
+ ...["com", "net", "org", "gov", "edu"].map((s) => `${s}.br`),
4087
+ ...["com", "net", "org", "gov", "edu", "ac"].map((s) => `${s}.cn`),
4088
+ ...["com", "net", "org", "gob", "edu"].map((s) => `${s}.mx`),
4089
+ ...["co", "net", "org", "firm", "gen", "ind", "ac", "edu", "gov", "res"].map((s) => `${s}.in`),
4090
+ ...["co", "org", "net", "ac", "gov", "web"].map((s) => `${s}.za`),
4091
+ ...["com", "net", "org", "gov", "edu"].map((s) => `${s}.tr`),
4092
+ ...["com", "net", "org", "gov", "edu"].map((s) => `${s}.sg`),
4093
+ ...["com", "net", "org", "gov", "edu"].map((s) => `${s}.hk`),
4094
+ ...["co", "or", "ne", "go", "ac", "re"].map((s) => `${s}.kr`),
4095
+ ...["com", "net", "org", "gov", "edu"].map((s) => `${s}.tw`),
4096
+ ...["com", "net", "org", "gov", "edu"].map((s) => `${s}.ar`),
4097
+ ...["co", "or", "ac", "go"].map((s) => `${s}.id`),
4098
+ ...["co", "or", "ac", "go"].map((s) => `${s}.th`),
4099
+ ...["com", "net", "org", "gov", "edu"].map((s) => `${s}.my`),
4100
+ ...["com", "net", "org", "gov", "edu"].map((s) => `${s}.ph`),
4101
+ ...["com", "net", "org", "gov", "edu"].map((s) => `${s}.pk`),
4102
+ ...["com", "net", "org", "gov", "edu"].map((s) => `${s}.ua`),
4103
+ ...["com", "net", "org", "gov", "edu"].map((s) => `${s}.pl`),
4104
+ ...["co", "or", "ac"].map((s) => `${s}.at`),
4105
+ ...["co", "org", "ac"].map((s) => `${s}.il`),
4106
+ "com.es",
4107
+ "org.es",
4108
+ "co.it",
4109
+ "com.ru",
4110
+ "co.ca",
4111
+ "eu.org",
4112
+ "github.io",
4113
+ "githubpreview.dev",
4114
+ "gitlab.io",
4115
+ "pages.dev",
4116
+ "workers.dev",
4117
+ "netlify.app",
4118
+ "vercel.app",
4119
+ "herokuapp.com",
4120
+ "appspot.com",
4121
+ "blogspot.com",
4122
+ "cloudfront.net",
4123
+ "azurewebsites.net",
4124
+ "web.app",
4125
+ "firebaseapp.com",
4126
+ "s3.amazonaws.com",
4127
+ "ngrok.io",
4128
+ "ngrok-free.app",
4129
+ "glitch.me",
4130
+ "repl.co",
4131
+ "fly.dev",
4132
+ "onrender.com"
4133
+ ]);
3939
4134
  // packages/protocol/src/graph.ts
3940
4135
  var GRAPH_LIMITS = {
3941
4136
  nodes: 2000,
@@ -4054,7 +4249,7 @@ var IMPLIED_OPENCODE_FEATURES = [
4054
4249
  "expose",
4055
4250
  "create"
4056
4251
  ];
4057
- var OPTIONAL_SPOKE_FEATURES = [...IMPLIED_OPENCODE_FEATURES, "autoName", "sessionTokens", "deliver", "hubPrompt", "permissionMessage", "actTokens", "launch", "fork", "revert", "dismissError"];
4252
+ var OPTIONAL_SPOKE_FEATURES = [...IMPLIED_OPENCODE_FEATURES, "autoName", "sessionTokens", "deliver", "hubPrompt", "permissionMessage", "actTokens", "launch", "fork", "revert", "dismissError", "timelineMedia", PREVIEW_BUNDLE_FEATURE, "voiceFront"];
4058
4253
  var CORE_OPS = ["turn", "steer", "interrupt", "stop", "permission", "form"];
4059
4254
  var KNOWN_SPOKE_FEATURES = [...CORE_OPS, ...OPTIONAL_SPOKE_FEATURES, "turnEvents", PROFILE_APPLY_FEATURE, SESSION_CONTEXT_FEATURE];
4060
4255
  var SPOKE_FEATURES = [...OPTIONAL_SPOKE_FEATURES, PROFILE_APPLY_FEATURE, SESSION_CONTEXT_FEATURE];
@@ -4072,6 +4267,8 @@ function featureForOp(op) {
4072
4267
  case "revertRestore":
4073
4268
  case "revertCommit":
4074
4269
  return "revert";
4270
+ case "removeFront":
4271
+ return "voiceFront";
4075
4272
  default:
4076
4273
  return op;
4077
4274
  }
@@ -4252,15 +4449,20 @@ function isServerRequest(v) {
4252
4449
  return nonEmpty(v.sessionID) && promptBody(v);
4253
4450
  case "timelineFile":
4254
4451
  return nonEmpty(v.sessionID) && nonEmpty(v.messageID) && v.messageID.length <= 300 && Number.isInteger(v.index) && Number(v.index) >= 0 && Number(v.index) < 1000;
4452
+ case "timelineMedia":
4453
+ return nonEmpty(v.sessionID) && nonEmpty(v.messageID) && v.messageID.length <= 300 && str2(v.ref) && parseMediaRef(v.ref) !== undefined && (v.offset === undefined || isMediaOffset(v.offset));
4454
+ case "previewBundle":
4455
+ return isPreviewBundleRequest(v);
4255
4456
  case "diff":
4256
4457
  return nonEmpty(v.sessionID) && nonEmpty(v.file) && v.file.length <= 4096;
4257
4458
  case "rename":
4258
4459
  return nonEmpty(v.sessionID) && str2(v.title) && v.title.length <= 1000;
4259
4460
  case "conversation":
4461
+ return nonEmpty(v.sessionID) && typeof v.on === "boolean" && (v.frontID === undefined || nonEmpty(v.frontID) && v.frontID.length <= 300);
4260
4462
  case "expose":
4261
4463
  return nonEmpty(v.sessionID) && typeof v.on === "boolean";
4262
4464
  case "create":
4263
- return nonEmpty(v.directory) && v.directory.startsWith("/") && v.directory.length <= 4096 && (v.title === undefined || str2(v.title) && v.title.length <= 200) && (v.herdrWorkspace === undefined || nonEmpty(v.herdrWorkspace) && v.herdrWorkspace.length <= 100) && (v.launchID === undefined || deliverID(v.launchID)) && (v.agent === undefined || agentName(v.agent)) && (v.model === undefined || isModelSelection(v.model));
4465
+ return nonEmpty(v.directory) && (v.directory.startsWith("/") || isWindowsAbsolutePath(v.directory)) && v.directory.length <= 4096 && (v.title === undefined || str2(v.title) && v.title.length <= 200) && (v.herdrWorkspace === undefined || nonEmpty(v.herdrWorkspace) && v.herdrWorkspace.length <= 100) && (v.launchID === undefined || deliverID(v.launchID)) && (v.agent === undefined || agentName(v.agent)) && (v.model === undefined || isModelSelection(v.model));
4264
4466
  case "sessionTokens":
4265
4467
  return Array.isArray(v.tokens) && v.tokens.length <= SESSION_TOKENS_MAX && v.tokens.every(isSessionToken) && (v.hubURL === undefined || isHubURL(v.hubURL));
4266
4468
  case "actTokens":
@@ -4279,6 +4481,7 @@ function isServerRequest(v) {
4279
4481
  case "revertRestore":
4280
4482
  case "revertCommit":
4281
4483
  case "dismissError":
4484
+ case "removeFront":
4282
4485
  return nonEmpty(v.sessionID);
4283
4486
  default:
4284
4487
  return false;
@@ -5300,18 +5503,22 @@ class OpenCodeWarmer {
5300
5503
  }
5301
5504
  }
5302
5505
  async function readServiceRegistration(file, options = {}) {
5303
- const uid = options.uid ?? (typeof process.getuid === "function" ? process.getuid() : undefined);
5506
+ const win32 = isWin32(options);
5507
+ const uid = win32 ? undefined : options.uid ?? (typeof process.getuid === "function" ? process.getuid() : undefined);
5304
5508
  let info;
5305
5509
  let fd;
5306
5510
  try {
5307
5511
  const before = lstatSync3(file);
5308
5512
  if (!before.isFile())
5309
5513
  return;
5310
- fd = openSync4(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
5514
+ fd = win32 ? openSync4(file, "r") : openSync4(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
5311
5515
  const stat = fstatSync(fd);
5312
5516
  if (!stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino)
5313
5517
  return;
5314
- if ((stat.mode & 18) !== 0)
5518
+ if (win32) {
5519
+ if (!(options.acl ?? defaultWinAcl()).check(nativeWinPath(file)).ok)
5520
+ return;
5521
+ } else if ((stat.mode & 18) !== 0)
5315
5522
  return;
5316
5523
  if (uid !== undefined && stat.uid !== uid)
5317
5524
  return;
@@ -5422,7 +5629,23 @@ function nodeConfigPath(env = process.env, home = homedir2()) {
5422
5629
  var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5423
5630
  var whole = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
5424
5631
  var show2 = (v) => JSON.stringify(v)?.slice(0, 80) ?? String(v);
5425
- function normalizeNodeConfig(raw) {
5632
+ function configPathOf(raw, options) {
5633
+ if ((options.platform ?? process.platform) !== "win32")
5634
+ return isAbsolutePath2(raw) ? raw : undefined;
5635
+ if (typeof raw !== "string")
5636
+ return;
5637
+ let native = raw;
5638
+ if (/^~[\\/]/.test(raw)) {
5639
+ const home = canonicalNativePath(options.home ?? homedir2());
5640
+ if (!home || !isWindowsAbsolutePath(home))
5641
+ return;
5642
+ native = `${home}/${raw.slice(2)}`;
5643
+ }
5644
+ const canonical = canonicalNativePath(native);
5645
+ return canonical && isWindowsAbsolutePath(canonical) ? canonical : undefined;
5646
+ }
5647
+ var isRoot = (p) => p === "/" || /^[A-Z]:\/$/.test(p);
5648
+ function normalizeNodeConfig(raw, options = {}) {
5426
5649
  const problems = [];
5427
5650
  let r = {};
5428
5651
  if (isObject3(raw))
@@ -5432,9 +5655,10 @@ function normalizeNodeConfig(raw) {
5432
5655
  const workspaceRoots = [];
5433
5656
  if (r.workspaceRoots !== undefined && !Array.isArray(r.workspaceRoots))
5434
5657
  problems.push("workspaceRoots must be a list of absolute paths");
5435
- for (const root of Array.isArray(r.workspaceRoots) ? r.workspaceRoots : []) {
5436
- if (!isAbsolutePath2(root) || root === "/") {
5437
- problems.push(`workspaceRoots: ${show2(root)} is not an absolute path (without . or .. segments, and not /)`);
5658
+ for (const entry of Array.isArray(r.workspaceRoots) ? r.workspaceRoots : []) {
5659
+ const root = configPathOf(entry, options);
5660
+ if (root === undefined || isRoot(root)) {
5661
+ problems.push(`workspaceRoots: ${show2(entry)} is not an absolute path (without . or .. segments, and not /)`);
5438
5662
  continue;
5439
5663
  }
5440
5664
  const clean = root.replace(/\/+$/, "");
@@ -5469,21 +5693,22 @@ function normalizeNodeConfig(raw) {
5469
5693
  }
5470
5694
  const openshell = r.openshell === undefined ? undefined : normalizeOpenShell(r.openshell, problems);
5471
5695
  const harnesses = r.harnesses === undefined ? undefined : normalizeHarnesses(r.harnesses, problems);
5472
- const opencode = r.opencode === undefined ? undefined : normalizeOpenCode(r.opencode, problems);
5696
+ const opencode = r.opencode === undefined ? undefined : normalizeOpenCode(r.opencode, problems, options);
5473
5697
  return {
5474
5698
  config: { workspaceRoots, labels, slots, ...memoryMB !== undefined ? { memoryMB } : {}, ...openshell ? { openshell } : {}, ...harnesses ? { harnesses } : {}, ...opencode ? { opencode } : {} },
5475
5699
  problems
5476
5700
  };
5477
5701
  }
5478
- function normalizeDirectories(raw, field, problems) {
5702
+ function normalizeDirectories(raw, field, problems, options) {
5479
5703
  if (!Array.isArray(raw)) {
5480
5704
  problems.push(`${field} must be a list of absolute paths`);
5481
5705
  return;
5482
5706
  }
5483
5707
  const out = [];
5484
- for (const dir of raw) {
5485
- if (!isAbsolutePath2(dir) || dir === "/") {
5486
- problems.push(`${field}: ${show2(dir)} is not an absolute path (without . or .. segments, and not /)`);
5708
+ for (const entry of raw) {
5709
+ const dir = configPathOf(entry, options);
5710
+ if (dir === undefined || isRoot(dir)) {
5711
+ problems.push(`${field}: ${show2(entry)} is not an absolute path (without . or .. segments, and not /)`);
5487
5712
  continue;
5488
5713
  }
5489
5714
  const clean = dir.replace(/\/+$/, "");
@@ -5514,12 +5739,12 @@ function loopbackOrigin(raw) {
5514
5739
  return;
5515
5740
  return url.origin;
5516
5741
  }
5517
- function normalizeOpenCode(raw, problems) {
5742
+ function normalizeOpenCode(raw, problems, options) {
5518
5743
  if (!isObject3(raw)) {
5519
5744
  problems.push("opencode must be an object");
5520
5745
  return;
5521
5746
  }
5522
- const directories = raw.directories === undefined ? undefined : normalizeDirectories(raw.directories, "opencode.directories", problems);
5747
+ const directories = raw.directories === undefined ? undefined : normalizeDirectories(raw.directories, "opencode.directories", problems, options);
5523
5748
  const servers = [];
5524
5749
  if (raw.servers !== undefined && !Array.isArray(raw.servers))
5525
5750
  problems.push("opencode.servers must be a list of {url, passwordFile, directories?}");
@@ -5531,12 +5756,13 @@ function normalizeOpenCode(raw, problems) {
5531
5756
  const url = loopbackOrigin(entry.url);
5532
5757
  if (!url)
5533
5758
  return void problems.push(`${field}.url must be an http(s) URL on 127.0.0.1, [::1] or localhost, without user info, path or query`);
5534
- if (!isAbsolutePath2(entry.passwordFile) || entry.passwordFile === "/")
5759
+ const passwordFile = configPathOf(entry.passwordFile, options);
5760
+ if (passwordFile === undefined || isRoot(passwordFile))
5535
5761
  return void problems.push(`${field}.passwordFile must be the absolute path of a private (chmod 600) file holding the server's password`);
5536
5762
  if (servers.some((s) => s.url === url))
5537
5763
  return void problems.push(`${field}: ${url} is listed twice; the first one is used`);
5538
- const dirs = entry.directories === undefined ? undefined : normalizeDirectories(entry.directories, `${field}.directories`, problems);
5539
- servers.push({ url, passwordFile: entry.passwordFile, ...dirs ? { directories: dirs } : {} });
5764
+ const dirs = entry.directories === undefined ? undefined : normalizeDirectories(entry.directories, `${field}.directories`, problems, options);
5765
+ servers.push({ url, passwordFile, ...dirs ? { directories: dirs } : {} });
5540
5766
  });
5541
5767
  if (servers.length > OPENCODE_SERVERS_MAX) {
5542
5768
  problems.push(`opencode.servers: at most ${OPENCODE_SERVERS_MAX} servers; the rest are ignored`);
@@ -5598,7 +5824,7 @@ function normalizeOpenShell(raw, problems) {
5598
5824
  problems.push(...profiles.problems);
5599
5825
  return { enabled, ...gateway ? { gateway } : {}, ...image ? { image } : {}, profiles };
5600
5826
  }
5601
- function readNodeConfig(file) {
5827
+ function readNodeConfig(file, options = {}) {
5602
5828
  let text;
5603
5829
  try {
5604
5830
  text = readFileSync4(file, "utf8");
@@ -5608,7 +5834,7 @@ function readNodeConfig(file) {
5608
5834
  return { config: normalizeNodeConfig(undefined).config, problems: [`${file}: cannot read (${cause.message})`] };
5609
5835
  }
5610
5836
  try {
5611
- return normalizeNodeConfig(JSON.parse(text));
5837
+ return normalizeNodeConfig(JSON.parse(text), options);
5612
5838
  } catch {
5613
5839
  return { config: normalizeNodeConfig(undefined).config, problems: [`${file}: not valid JSON`] };
5614
5840
  }
@@ -5739,6 +5965,14 @@ function durationArg(value, flag) {
5739
5965
  throw new UsageError(`${flag} takes seconds or a duration like 30s, 5m, 2h, 1d (got "${value}")`);
5740
5966
  return Number(match[1]) * UNIT_S[match[2]];
5741
5967
  }
5968
+ var ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})?$/i;
5969
+ function notAfterArg(value, what) {
5970
+ const text = value.trim();
5971
+ const ms = /^\d+$/.test(text) ? Number(text) : ISO_DATE_TIME.test(text) ? Date.parse(text) : Number.NaN;
5972
+ if (!Number.isSafeInteger(ms))
5973
+ throw new UsageError(`${what} takes epoch milliseconds or an ISO 8601 date-time like 2027-01-15T12:00:00Z (got "${value}")`);
5974
+ return ms;
5975
+ }
5742
5976
  var iso = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
5743
5977
  function askOutcome(asked, waited) {
5744
5978
  const { message, answer } = asked;
@@ -5911,7 +6145,8 @@ var LAUNCH_FLAGS = {
5911
6145
  prompt: "string",
5912
6146
  end: "string",
5913
6147
  timeout: "string",
5914
- cleanup: "string"
6148
+ cleanup: "string",
6149
+ "not-after": "string"
5915
6150
  };
5916
6151
  var choices = (options) => options.length === 2 ? options.join(" or ") : `${options.slice(0, -1).join(", ")} or ${options.at(-1)}`;
5917
6152
  function oneOf3(flags, flag, options) {
@@ -5970,10 +6205,12 @@ async function launchSpecArg(flags, context) {
5970
6205
  const runtime = kind === undefined ? undefined : { kind, ...profile !== undefined ? { profile } : {} };
5971
6206
  const placement = defined({ labels: nodeLabels, machine, mode: flags.prefer === true ? "prefer" : undefined });
5972
6207
  const end = defined({ when, timeout: str(flags, "timeout"), cleanup });
6208
+ const notAfterText = str(flags, "not-after");
6209
+ const notAfter = notAfterText === undefined ? undefined : notAfterArg(notAfterText, "--not-after");
5973
6210
  return {
5974
6211
  ...defined({ key: str(flags, "key"), name, roles, harness: str(flags, "harness"), agent: str(flags, "agent"), model: str(flags, "model") }),
5975
6212
  workspace,
5976
- ...defined({ runtime, placement, prompt, labels, end })
6213
+ ...defined({ runtime, placement, prompt, labels, end, notAfter })
5977
6214
  };
5978
6215
  }
5979
6216
  var at2 = (name) => `@${name.replace(/^@+/, "")}`;
@@ -6432,6 +6669,23 @@ function dueOf(args) {
6432
6669
  throw new ToolArgError(`"due" must be an ISO date/time or epoch milliseconds`);
6433
6670
  return ms;
6434
6671
  }
6672
+ function notAfterOf(args) {
6673
+ const value = args.notAfter;
6674
+ if (value === undefined || value === null || value === "")
6675
+ return;
6676
+ if (typeof value === "number" && Number.isSafeInteger(value))
6677
+ return value;
6678
+ if (typeof value === "string") {
6679
+ try {
6680
+ return notAfterArg(value, `"notAfter"`);
6681
+ } catch (cause) {
6682
+ if (cause instanceof UsageError)
6683
+ throw new ToolArgError(cause.message);
6684
+ throw cause;
6685
+ }
6686
+ }
6687
+ throw new ToolArgError(`"notAfter" must be epoch milliseconds or an ISO 8601 date-time like 2027-01-15T12:00:00Z`);
6688
+ }
6435
6689
  var line4 = (i) => `${i.id} ${i.status}${i.progress ? `/${i.progress}` : ""} ${i.priority} r${i.revision} ${sanitizeText(i.title).replace(/\s+/g, " ")}`;
6436
6690
  function intakeText(i) {
6437
6691
  const from = i.source.kind === "user" ? "the user" : i.source.kind === "app" ? `app ${i.source.tokenID}` : `session ${i.source.sessionID}`;
@@ -6441,6 +6695,7 @@ function intakeText(i) {
6441
6695
  `status: ${i.status}${i.progress ? ` (${i.progress})` : ""} priority: ${i.priority} from: ${from}`,
6442
6696
  ...i.assignmentID ? [`assignment: ${i.assignmentID} in workspace ${i.workspaceID ?? "?"}${i.sessionID ? `, delivered to ${i.sessionID}` : ""}`] : [],
6443
6697
  ...i.due !== undefined ? [`due: ${new Date(i.due).toISOString()}`] : [],
6698
+ ...i.notAfter !== undefined ? [`not after: ${new Date(i.notAfter).toISOString()}`] : [],
6444
6699
  ...i.labels?.length ? [`labels: ${i.labels.join(", ")}`] : [],
6445
6700
  ...i.criteria?.length ? ["done means:", ...i.criteria.map((c) => `- ${sanitizeText(c)}`)] : [],
6446
6701
  ...i.ref ? [`ref: ${sanitizeText(i.ref)}`] : [],
@@ -6452,7 +6707,7 @@ function intakeText(i) {
6452
6707
  var teamIntakeTools = [
6453
6708
  {
6454
6709
  name: "team_submit",
6455
- description: "Give work to a team: its entry role (e.g. the project manager) receives the task with the team's context and owns it as an assignment in the team's workspace; while the role is vacant it waits queued. Pass `key` to make retries idempotent. App tokens need scope teams:submit; sessions submit under the team's communication policy.",
6710
+ description: "Give work to a team: its entry role (e.g. the project manager) receives the task with the team's context and owns it as an assignment in the team's workspace; while the role is vacant it waits queued (with `notAfter`, only until then: still queued at that time, it is cancelled with reason not_after instead of delivered late). Pass `key` to make retries idempotent. App tokens need scope teams:submit; sessions submit under the team's communication policy.",
6456
6711
  properties: {
6457
6712
  team: TEAM,
6458
6713
  title: { ...STRING3, description: "One line, at most 120 characters." },
@@ -6460,6 +6715,10 @@ var teamIntakeTools = [
6460
6715
  priority: { type: "string", enum: PRIORITIES, description: "Default normal." },
6461
6716
  labels: { type: "array", items: STRING3, description: "Slugs (lowercase letters, digits, dashes)." },
6462
6717
  due: { ...STRING3, description: "When it is due: an ISO date/time." },
6718
+ notAfter: {
6719
+ type: ["number", "string"],
6720
+ description: "Never deliver after this time: epoch milliseconds or an ISO 8601 date-time (at most 30 days ahead). Already passed: nothing is submitted (expired). Still queued then: cancelled (reason not_after)."
6721
+ },
6463
6722
  criteria: { type: "array", items: STRING3, description: "What done means (at most 8 lines)." },
6464
6723
  ref: { ...STRING3, description: "An external reference, e.g. a Brain task ID." },
6465
6724
  key: { ...STRING3, description: "Idempotency-Key: the same key and body return the original submission." }
@@ -6472,10 +6731,11 @@ var teamIntakeTools = [
6472
6731
  const labels = strings2(args, "labels");
6473
6732
  const criteria = strings2(args, "criteria");
6474
6733
  const due = dueOf(args);
6734
+ const notAfter = notAfterOf(args);
6475
6735
  const brief = optional4(args, "brief");
6476
6736
  const ref = optional4(args, "ref");
6477
6737
  const key = optional4(args, "key");
6478
- const { intake, created } = await hub.teams.submit(need3(args, "team"), { title: need3(args, "title"), ...brief ? { brief } : {}, ...priority ? { priority } : {}, ...labels ? { labels } : {}, ...due !== undefined ? { due } : {}, ...criteria ? { criteria } : {}, ...ref ? { ref } : {} }, { signal, ...key ? { idempotencyKey: key } : {} });
6738
+ const { intake, created } = await hub.teams.submit(need3(args, "team"), { title: need3(args, "title"), ...brief ? { brief } : {}, ...priority ? { priority } : {}, ...labels ? { labels } : {}, ...due !== undefined ? { due } : {}, ...notAfter !== undefined ? { notAfter } : {}, ...criteria ? { criteria } : {}, ...ref ? { ref } : {} }, { signal, ...key ? { idempotencyKey: key } : {} });
6479
6739
  return `${created ? "Submitted" : "Already submitted (same key)"}: ${intake.id} is ${intake.status}${intake.status === "queued" ? " (waiting for the entry role)" : ""}
6480
6740
  ${intakeText(intake)}`;
6481
6741
  }
@@ -6641,7 +6901,7 @@ var revision = (args) => {
6641
6901
  throw new ArgError(`"expectedRevision" is required: the team's revision you read (team_show)`);
6642
6902
  return value;
6643
6903
  };
6644
- var int2 = (args, key, min, max) => {
6904
+ var int3 = (args, key, min, max) => {
6645
6905
  const value = args[key];
6646
6906
  if (value === undefined || value === null)
6647
6907
  return;
@@ -6748,7 +7008,7 @@ var teamTools = [
6748
7008
  const tags = strings3(args, "tags");
6749
7009
  const project = optional5(args, "project");
6750
7010
  const metadata = record(args, "metadata");
6751
- const limit = int2(args, "limit", 1, 200);
7011
+ const limit = int3(args, "limit", 1, 200);
6752
7012
  const cursor = optional5(args, "cursor");
6753
7013
  if (args.paused !== undefined && typeof args.paused !== "boolean")
6754
7014
  throw new ArgError(`"paused" must be true or false`);
@@ -6815,8 +7075,8 @@ var teamTools = [
6815
7075
  required: ["team"],
6816
7076
  readOnly: true,
6817
7077
  run: async (hub, args, signal) => {
6818
- const from = int2(args, "from", 1, 1e9);
6819
- const to = args.to === undefined || args.to === null || args.to === "draft" ? "draft" : int2(args, "to", 1, 1e9);
7078
+ const from = int3(args, "from", 1, 1e9);
7079
+ const to = args.to === undefined || args.to === null || args.to === "draft" ? "draft" : int3(args, "to", 1, 1e9);
6820
7080
  const { from: a, to: b, diff } = await hub.teams.diff(need4(args, "team"), { signal, ...from !== undefined ? { from } : {}, to });
6821
7081
  const part = (label, d) => ["added", "removed", "changed"].flatMap((k) => d[k]?.length ? [`${label} ${k}: ${d[k].join(", ")}`] : []);
6822
7082
  const lines = [
@@ -7387,10 +7647,28 @@ var optionalExpires = (args) => {
7387
7647
  }
7388
7648
  throw new ArgError3(`"expires" must be seconds or a duration like 30s, 5m, 2h, 1d`);
7389
7649
  };
7650
+ var optionalNotAfter = (args) => {
7651
+ const value = args.notAfter;
7652
+ if (value === undefined || value === null)
7653
+ return;
7654
+ if (typeof value === "number" && Number.isSafeInteger(value))
7655
+ return value;
7656
+ if (typeof value === "string") {
7657
+ try {
7658
+ return notAfterArg(value, '"notAfter"');
7659
+ } catch (cause) {
7660
+ if (cause instanceof UsageError)
7661
+ throw new ArgError3(cause.message);
7662
+ throw cause;
7663
+ }
7664
+ }
7665
+ throw new ArgError3(`"notAfter" must be epoch milliseconds or an ISO 8601 date-time like 2027-01-15T12:00:00Z`);
7666
+ };
7390
7667
  var sendOptions = (args) => {
7391
7668
  const wake = optionalBool(args, "wake");
7392
7669
  const urgent = optionalBool(args, "urgent");
7393
- return { ...wake !== undefined ? { wake } : {}, ...urgent !== undefined ? { urgent } : {} };
7670
+ const notAfter = optionalNotAfter(args);
7671
+ return { ...wake !== undefined ? { wake } : {}, ...urgent !== undefined ? { urgent } : {}, ...notAfter !== undefined ? { notAfter } : {} };
7394
7672
  };
7395
7673
  var whyOption = (args) => {
7396
7674
  const why = optional7(args, "why");
@@ -7464,7 +7742,8 @@ function launchSpec(args) {
7464
7742
  placement: definedFields({ labels: nodeLabels, machine, mode: prefer ? "prefer" : undefined }),
7465
7743
  prompt: optional7(args, "prompt"),
7466
7744
  labels: optionalLabels(args, "labels"),
7467
- end: definedFields({ when: optionalEnum(args, "end", LAUNCH_END_WHEN), timeout: optional7(args, "timeout"), cleanup: optionalEnum(args, "cleanup", CLEANUP_POLICIES) })
7745
+ end: definedFields({ when: optionalEnum(args, "end", LAUNCH_END_WHEN), timeout: optional7(args, "timeout"), cleanup: optionalEnum(args, "cleanup", CLEANUP_POLICIES) }),
7746
+ notAfter: optionalNotAfter(args)
7468
7747
  })
7469
7748
  };
7470
7749
  }
@@ -7514,6 +7793,7 @@ var STRING6 = { type: "string" };
7514
7793
  var SEND = {
7515
7794
  wake: { type: "boolean", description: "false: admit the message without starting a turn." },
7516
7795
  urgent: { type: "boolean", description: "Steer into the recipient's running turn." },
7796
+ notAfter: { type: ["number", "string"], description: "Never deliver after this time: epoch milliseconds or an ISO 8601 date-time (at most 30 days ahead). Already passed: nothing is sent (expired)." },
7517
7797
  all: { type: "boolean", description: "Send to every session the address names (each holder of a role)." }
7518
7798
  };
7519
7799
  var TO = { type: "string", description: "The recipient: @name, @@role, @@role#team (a role of one of your teams), @name.project, ses_…, or @me (the user)." };
@@ -7671,7 +7951,7 @@ var webhookText = (w) => `${w.id}${w.label ? ` ${w.label}` : ""} ${w.url} ${w
7671
7951
  var webhookTools = [
7672
7952
  {
7673
7953
  name: "webhook_create",
7674
- description: "Create a durable signed webhook for canonical compact events. The signing secret is returned only here. Needs events:subscribe plus every selected family's scope.",
7954
+ description: `Create a durable signed webhook for canonical compact events (e.g. launch.*, session.*, message.changed, team.intake). The signing secret is returned only here. Needs events:subscribe plus every selected family's scope; events ["team.intake"] alone needs only teams:submit (an app gets only its own submissions).`,
7675
7955
  properties: { url: STRING6, events: { type: "array", items: STRING6 }, label: STRING6 },
7676
7956
  required: ["url"],
7677
7957
  readOnly: false,
@@ -8188,13 +8468,13 @@ var TOOLS = [
8188
8468
  {
8189
8469
  name: "msg",
8190
8470
  description: "Tell a session something (fire and forget). Sent as this server's credential, not as an OpenCode session.",
8191
- properties: { to: TO, text: { ...STRING6, description: "The message." }, ...SEND },
8471
+ properties: { to: TO, text: { ...STRING6, description: "The message." }, ...SEND, idempotencyKey: IDEMPOTENCY3 },
8192
8472
  required: ["to", "text"],
8193
8473
  readOnly: false,
8194
8474
  run: async (hub, args, signal) => {
8195
8475
  const to = need7(args, "to");
8196
8476
  const text = need7(args, "text");
8197
- const options = { ...sendOptions(args), signal };
8477
+ const options = { ...sendOptions(args), ...idemOption(args), signal };
8198
8478
  if (optionalBool(args, "all"))
8199
8479
  return sentAllText(await hub.msg(to, text, { ...options, all: true }));
8200
8480
  return sentText(await hub.msg(to, text, options));
@@ -8208,7 +8488,8 @@ var TOOLS = [
8208
8488
  text: { ...STRING6, description: "The question." },
8209
8489
  wait: { type: "number", minimum: 0, maximum: WAIT_POLL_MAX_S, description: `Seconds to wait for the answer (0–${WAIT_POLL_MAX_S}).` },
8210
8490
  expires: { type: ["number", "string"], description: "When the ask expires: seconds, or 30s, 5m, 2h, 1d (1 minute to 7 days; default 24 h)." },
8211
- ...SEND
8491
+ ...SEND,
8492
+ idempotencyKey: IDEMPOTENCY3
8212
8493
  },
8213
8494
  required: ["to", "text"],
8214
8495
  readOnly: false,
@@ -8217,7 +8498,7 @@ var TOOLS = [
8217
8498
  const text = need7(args, "text");
8218
8499
  const wait = optionalWait(args);
8219
8500
  const expires = optionalExpires(args);
8220
- const options = { ...sendOptions(args), ...expires !== undefined ? { expires } : {}, signal };
8501
+ const options = { ...sendOptions(args), ...expires !== undefined ? { expires } : {}, ...idemOption(args), signal };
8221
8502
  if (optionalBool(args, "all")) {
8222
8503
  if (wait !== undefined)
8223
8504
  throw new ArgError3(`"wait" cannot be combined with "all"`);
@@ -8381,7 +8662,7 @@ ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
8381
8662
  },
8382
8663
  {
8383
8664
  name: "launch",
8384
- description: "Start a new session on a node (a hub node with the repo under its workspace roots). The same key again answers the same launch. Needs an app token with scope sessions:launch and a grant with verb launch covering the repo.",
8665
+ description: "Start a new session on a node (a hub node with the repo under its workspace roots). The same key again answers the same launch. With notAfter, a launch still waiting for a machine at that time is never started (it ends expired). Needs an app token with scope sessions:launch and a grant with verb launch covering the repo.",
8385
8666
  properties: {
8386
8667
  repo: { ...STRING6, description: "Absolute path of the repository on the node (or where a clone goes)." },
8387
8668
  mode: { type: "string", enum: [...WORKSPACE_MODES], description: "worktree (default: <repo>/.worktrees/<branch>, needs branch), checkout, or clone (needs remote)." },
@@ -8403,7 +8684,8 @@ ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
8403
8684
  prompt: { ...STRING6, description: "The session's first prompt." },
8404
8685
  end: { type: "string", enum: [...LAUNCH_END_WHEN], description: "When it ends: idle (default; idle with nothing open for 30 s), asked (with a question open: needs_user), never." },
8405
8686
  timeout: { ...STRING6, description: "Longest run, e.g. 4h or 90m." },
8406
- cleanup: { type: "string", enum: [...CLEANUP_POLICIES], description: "keep (default) or remove the workspace when it ends." }
8687
+ cleanup: { type: "string", enum: [...CLEANUP_POLICIES], description: "keep (default) or remove the workspace when it ends." },
8688
+ notAfter: { type: ["number", "string"], description: "Do not start after this time: epoch milliseconds or an ISO 8601 date-time (at most 30 days ahead). Still waiting for a machine then, the launch ends expired; once placed it runs (timeout bounds it). Already passed: nothing is launched (expired)." }
8407
8689
  },
8408
8690
  required: ["repo"],
8409
8691
  readOnly: false,
@@ -8721,7 +9003,7 @@ function grantText2(g) {
8721
9003
  var requestedText = (body) => `requested; waiting for the user (Decision ${body.decision.id})`;
8722
9004
 
8723
9005
  // packages/cli/src/node.ts
8724
- import { existsSync as existsSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync8, realpathSync as realpathSync3, rmSync as rmSync8, writeFileSync as writeFileSync4 } from "node:fs";
9006
+ import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync8, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
8725
9007
  import path16 from "node:path";
8726
9008
  // packages/node/src/connection.ts
8727
9009
  var UNSUPPORTED_RETRY_MS = 30 * 60000;
@@ -8945,7 +9227,7 @@ import { arch as osArch, platform as osPlatform } from "node:os";
8945
9227
  // packages/node/package.json
8946
9228
  var package_default2 = {
8947
9229
  name: "@huynle/hub-node",
8948
- version: "0.8.7",
9230
+ version: "0.9.1",
8949
9231
  private: true,
8950
9232
  description: "`hub node`: the per-machine daemon that prepares, collects and cleans up launch workspaces (internal; shipped inside @huynle/hub, not published on its own).",
8951
9233
  license: "MIT",
@@ -8962,46 +9244,251 @@ import path11 from "node:path";
8962
9244
 
8963
9245
  // packages/node/src/git.ts
8964
9246
  import { spawn } from "node:child_process";
8965
- var ALLOWED_PROTOCOLS = "https:ssh";
8966
- var GIT_TIMEOUT_MS = 120000;
8967
- var MAX_BUFFER = 16 * 1024 * 1024;
8968
- var STDERR_MAX = 600;
8969
- var KEPT_GIT_VARS = new Set(["GIT_SSH", "GIT_SSH_COMMAND", "GIT_SSH_VARIANT"]);
8970
9247
 
8971
- class GitError extends Error {
8972
- name = "GitError";
8973
- }
8974
- function gitEnv(base = process.env, overlay = {}) {
8975
- const env = {};
8976
- for (const [name, value] of Object.entries(base)) {
8977
- if (value === undefined)
8978
- continue;
8979
- if (name.startsWith("GIT_") && !KEPT_GIT_VARS.has(name))
8980
- continue;
8981
- env[name] = value;
8982
- }
8983
- Object.assign(env, { GIT_TERMINAL_PROMPT: "0", GIT_ALLOW_PROTOCOL: ALLOWED_PROTOCOLS, GCM_INTERACTIVE: "never", LC_ALL: "C" });
8984
- for (const [name, value] of Object.entries(overlay))
8985
- if (value !== undefined)
8986
- env[name] = value;
8987
- return env;
8988
- }
8989
- function redact(text) {
8990
- return text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1***@");
9248
+ // packages/cli/src/bootstrap/adapters/process-tree.ts
9249
+ import { execFile, spawn as nodeSpawn } from "node:child_process";
9250
+ var TOOL_TIMEOUT_MS = 15000;
9251
+ var START_SLACK_MS = 2000;
9252
+ var FILETIME_EPOCH_MS = 11644473600000n;
9253
+ var defaultExec = (file, args) => new Promise((resolve) => {
9254
+ execFile(file, [...args], { shell: false, windowsHide: true, timeout: TOOL_TIMEOUT_MS, encoding: "utf8", maxBuffer: 1 << 20 }, (error, stdout) => {
9255
+ const code = error?.code;
9256
+ resolve({ code: error ? typeof code === "number" ? code : null : 0, stdout: String(stdout ?? "") });
9257
+ });
9258
+ });
9259
+ function systemRootOf(value) {
9260
+ const v = value?.replace(/\\+$/, "");
9261
+ return v && /^[A-Za-z]:\\[A-Za-z0-9 ._\\()-]*$/.test(v) ? v : "C:\\Windows";
8991
9262
  }
8992
- function runGit(args, options) {
8993
- const argv = ["-c", "protocol.ext.allow=never", ...args];
8994
- const what = `git ${args.find((a) => !a.startsWith("-")) ?? ""}`.trim();
8995
- return new Promise((resolve, reject) => {
8996
- if (options.signal?.aborted)
8997
- return reject(new GitError(`${what} cancelled`));
8998
- let settled = false;
8999
- const finish = (error, stdout) => {
9000
- if (settled)
9001
- return;
9002
- settled = true;
9003
- clearTimeout(timer);
9004
- options.signal?.removeEventListener("abort", onAbort);
9263
+ function processTree(deps = {}) {
9264
+ const platform = deps.platform ?? process.platform;
9265
+ if (platform === "win32") {
9266
+ const exec = deps.exec ?? defaultExec;
9267
+ const root = systemRootOf(deps.systemRoot ?? process.env.SystemRoot ?? process.env.SYSTEMROOT);
9268
+ const taskkill = `${root}\\System32\\taskkill.exe`;
9269
+ const powershell = `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
9270
+ const killTree = async (pid) => {
9271
+ if (!Number.isInteger(pid) || pid <= 0)
9272
+ return;
9273
+ try {
9274
+ await exec(taskkill, ["/T", "/F", "/PID", String(pid)]);
9275
+ } catch {}
9276
+ };
9277
+ return {
9278
+ detached: false,
9279
+ killTree,
9280
+ async reap(pid, startedAt) {
9281
+ if (!Number.isInteger(pid) || pid <= 0)
9282
+ return;
9283
+ const since = (BigInt(Math.max(0, Math.floor(startedAt) - START_SLACK_MS)) + FILETIME_EPOCH_MS) * 10000n;
9284
+ const script = `$t=[DateTime]::FromFileTimeUtc(${since}); ` + `Get-CimInstance -ClassName Win32_Process -Filter 'ParentProcessId=${pid}' | ` + `Where-Object { $_.CreationDate.ToUniversalTime() -ge $t } | ForEach-Object { $_.ProcessId }`;
9285
+ let stdout = "";
9286
+ try {
9287
+ stdout = (await exec(powershell, ["-NoProfile", "-NonInteractive", "-Command", script])).stdout;
9288
+ } catch {
9289
+ return;
9290
+ }
9291
+ for (const line of stdout.split(/\r?\n/)) {
9292
+ const child = line.trim();
9293
+ if (/^[1-9]\d*$/.test(child))
9294
+ await killTree(Number(child));
9295
+ }
9296
+ }
9297
+ };
9298
+ }
9299
+ const kill = deps.kill ?? ((pid, signal) => void process.kill(pid, signal));
9300
+ const group = (pid) => {
9301
+ try {
9302
+ kill(-pid, "SIGKILL");
9303
+ return true;
9304
+ } catch {
9305
+ return false;
9306
+ }
9307
+ };
9308
+ return {
9309
+ detached: true,
9310
+ async killTree(pid) {
9311
+ if (!Number.isInteger(pid) || pid <= 0)
9312
+ return;
9313
+ if (group(pid))
9314
+ return;
9315
+ try {
9316
+ kill(pid, "SIGKILL");
9317
+ } catch {}
9318
+ },
9319
+ async reap(pid) {
9320
+ if (Number.isInteger(pid) && pid > 0)
9321
+ group(pid);
9322
+ }
9323
+ };
9324
+ }
9325
+ var DRAIN_GRACE_MS = 1000;
9326
+ function runBounded(request, options = {}) {
9327
+ const tree = options.tree ?? processTree();
9328
+ const spawn = options.spawn ?? nodeSpawn;
9329
+ const now = options.now ?? Date.now;
9330
+ const env = Object.fromEntries(Object.entries(request.env).filter((e) => typeof e[1] === "string"));
9331
+ return new Promise((resolve) => {
9332
+ let timedOut = false;
9333
+ let aborted = false;
9334
+ let overflowed = false;
9335
+ let ended = false;
9336
+ const out = [];
9337
+ const err = [];
9338
+ const size = { out: 0, err: 0 };
9339
+ const result = (exitCode, spawnError) => ({
9340
+ exitCode,
9341
+ stdout: Buffer.concat(out).toString("utf8"),
9342
+ stderr: Buffer.concat(err).toString("utf8"),
9343
+ timedOut,
9344
+ aborted,
9345
+ overflowed,
9346
+ ...spawnError ? { spawnError } : {}
9347
+ });
9348
+ if (request.signal?.aborted) {
9349
+ aborted = true;
9350
+ return resolve(result(null));
9351
+ }
9352
+ const startedAt = now();
9353
+ let child;
9354
+ try {
9355
+ child = spawn(request.file, [...request.args], {
9356
+ shell: false,
9357
+ env,
9358
+ stdio: ["ignore", "pipe", request.stderr ? "pipe" : "ignore"],
9359
+ windowsHide: true,
9360
+ detached: tree.detached
9361
+ });
9362
+ } catch (error) {
9363
+ return resolve(result(null, error));
9364
+ }
9365
+ let settled = false;
9366
+ let exited = false;
9367
+ const pid = child.pid;
9368
+ const end = () => {
9369
+ if (ended || pid === undefined)
9370
+ return;
9371
+ ended = true;
9372
+ tree.killTree(pid);
9373
+ };
9374
+ const timer = setTimeout(() => {
9375
+ timedOut = true;
9376
+ end();
9377
+ }, request.timeoutMs);
9378
+ const onAbort = () => {
9379
+ aborted = true;
9380
+ end();
9381
+ };
9382
+ request.signal?.addEventListener("abort", onAbort, { once: true });
9383
+ const finish = (r) => {
9384
+ if (settled)
9385
+ return;
9386
+ settled = true;
9387
+ clearTimeout(timer);
9388
+ request.signal?.removeEventListener("abort", onAbort);
9389
+ resolve(r);
9390
+ };
9391
+ const collect = (into, key) => (chunk) => {
9392
+ const room = request.maxOutputBytes - size[key];
9393
+ if (chunk.length > room) {
9394
+ overflowed = true;
9395
+ if (room > 0)
9396
+ into.push(chunk.subarray(0, room));
9397
+ size[key] = request.maxOutputBytes;
9398
+ if ((request.overflow ?? "kill") === "kill")
9399
+ end();
9400
+ return;
9401
+ }
9402
+ into.push(chunk);
9403
+ size[key] += chunk.length;
9404
+ };
9405
+ child.stdout?.on("data", collect(out, "out"));
9406
+ child.stderr?.on("data", collect(err, "err"));
9407
+ const streams = [child.stdout, child.stderr].filter((s) => Boolean(s));
9408
+ const drained = () => new Promise((done) => {
9409
+ let open = streams.filter((s) => !s.readableEnded && !s.destroyed).length;
9410
+ if (open === 0)
9411
+ return done();
9412
+ const grace = setTimeout(() => {
9413
+ for (const s of streams)
9414
+ s.destroy();
9415
+ done();
9416
+ }, DRAIN_GRACE_MS);
9417
+ for (const s of streams) {
9418
+ if (s.readableEnded || s.destroyed)
9419
+ continue;
9420
+ const one = () => {
9421
+ if (--open === 0) {
9422
+ clearTimeout(grace);
9423
+ done();
9424
+ }
9425
+ };
9426
+ s.once("end", one);
9427
+ s.once("close", () => s.readableEnded ? undefined : one());
9428
+ }
9429
+ });
9430
+ child.on("error", (error) => {
9431
+ if (!exited)
9432
+ finish(result(null, error));
9433
+ });
9434
+ child.on("exit", (code) => {
9435
+ if (exited)
9436
+ return;
9437
+ exited = true;
9438
+ clearTimeout(timer);
9439
+ const exitCode = typeof code === "number" && !ended ? code : null;
9440
+ (async () => {
9441
+ if (pid !== undefined)
9442
+ await tree.reap(pid, startedAt);
9443
+ await drained();
9444
+ finish(result(exitCode));
9445
+ })();
9446
+ });
9447
+ });
9448
+ }
9449
+
9450
+ // packages/node/src/git.ts
9451
+ var ALLOWED_PROTOCOLS = "https:ssh";
9452
+ var GIT_TIMEOUT_MS = 120000;
9453
+ var MAX_BUFFER = 16 * 1024 * 1024;
9454
+ var STDERR_MAX = 600;
9455
+ var KEPT_GIT_VARS = new Set(["GIT_SSH", "GIT_SSH_COMMAND", "GIT_SSH_VARIANT"]);
9456
+
9457
+ class GitError extends Error {
9458
+ name = "GitError";
9459
+ }
9460
+ function gitEnv(base = process.env, overlay = {}) {
9461
+ const env = {};
9462
+ for (const [name, value] of Object.entries(base)) {
9463
+ if (value === undefined)
9464
+ continue;
9465
+ if (name.startsWith("GIT_") && !KEPT_GIT_VARS.has(name))
9466
+ continue;
9467
+ env[name] = value;
9468
+ }
9469
+ Object.assign(env, { GIT_TERMINAL_PROMPT: "0", GIT_ALLOW_PROTOCOL: ALLOWED_PROTOCOLS, GCM_INTERACTIVE: "never", LC_ALL: "C" });
9470
+ for (const [name, value] of Object.entries(overlay))
9471
+ if (value !== undefined)
9472
+ env[name] = value;
9473
+ return env;
9474
+ }
9475
+ function redact(text) {
9476
+ return text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1***@");
9477
+ }
9478
+ function runGit(args, options) {
9479
+ const argv = ["-c", "protocol.ext.allow=never", ...args];
9480
+ const what = `git ${args.find((a) => !a.startsWith("-")) ?? ""}`.trim();
9481
+ const win32 = (options.platform ?? process.platform) === "win32";
9482
+ return new Promise((resolve, reject) => {
9483
+ if (options.signal?.aborted)
9484
+ return reject(new GitError(`${what} cancelled`));
9485
+ let settled = false;
9486
+ const finish = (error, stdout) => {
9487
+ if (settled)
9488
+ return;
9489
+ settled = true;
9490
+ clearTimeout(timer);
9491
+ options.signal?.removeEventListener("abort", onAbort);
9005
9492
  if (error)
9006
9493
  reject(error);
9007
9494
  else
@@ -9011,7 +9498,7 @@ function runGit(args, options) {
9011
9498
  cwd: options.cwd,
9012
9499
  env: gitEnv(options.baseEnv ?? process.env, options.env),
9013
9500
  stdio: ["ignore", "pipe", "pipe"],
9014
- detached: true,
9501
+ detached: !win32,
9015
9502
  windowsHide: true
9016
9503
  });
9017
9504
  const out = [];
@@ -9035,6 +9522,13 @@ function runGit(args, options) {
9035
9522
  function stop(why) {
9036
9523
  if (settled)
9037
9524
  return;
9525
+ if (win32) {
9526
+ if (child.pid)
9527
+ (options.tree ?? processTree({ platform: "win32" })).killTree(child.pid);
9528
+ else
9529
+ child.kill("SIGKILL");
9530
+ return finish(new GitError(`${what} ${why}`));
9531
+ }
9038
9532
  try {
9039
9533
  if (child.pid)
9040
9534
  process.kill(-child.pid, "SIGKILL");
@@ -9052,43 +9546,72 @@ function runGit(args, options) {
9052
9546
 
9053
9547
  // packages/node/src/host.ts
9054
9548
  var LAUNCH_MARKER = "hub-launch";
9055
- function resolvePath(p) {
9549
+ var nativeOps = {
9550
+ win32: process.platform === "win32",
9551
+ path: path11,
9552
+ realpath: (p) => realpathSync2(p),
9553
+ isSymlink: (p) => {
9554
+ try {
9555
+ return lstatSync4(p).isSymbolicLink();
9556
+ } catch {
9557
+ return false;
9558
+ }
9559
+ }
9560
+ };
9561
+ function realOf(p, ops) {
9562
+ const real = ops.realpath(p);
9563
+ if (!ops.win32)
9564
+ return real;
9565
+ const canonical = canonicalNativePath(real);
9566
+ if (canonical === undefined || !isWindowsAbsolutePath(canonical))
9567
+ throw new RuntimeError(`${p} resolves to ${real}, which is not on a local drive`);
9568
+ return canonical;
9569
+ }
9570
+ function resolvePath(p, ops = nativeOps) {
9056
9571
  const rest = [];
9057
9572
  let current = p;
9058
9573
  for (;; ) {
9574
+ let real;
9059
9575
  try {
9060
- return path11.join(realpathSync2(current), ...rest.reverse());
9576
+ real = realOf(current, ops);
9061
9577
  } catch (cause) {
9578
+ if (cause instanceof RuntimeError)
9579
+ throw cause;
9062
9580
  if (cause.code !== "ENOENT")
9063
9581
  throw new RuntimeError(`cannot resolve ${p}: ${cause.message}`);
9064
- let link = false;
9065
- try {
9066
- link = lstatSync4(current).isSymbolicLink();
9067
- } catch {}
9068
- if (link)
9582
+ if (ops.isSymlink(current))
9069
9583
  throw new RuntimeError(`${p} goes through a dangling symlink`);
9070
- const parent = path11.dirname(current);
9584
+ const parent = ops.path.dirname(current);
9071
9585
  if (parent === current)
9072
9586
  throw new RuntimeError(`cannot resolve ${p}`);
9073
- rest.push(path11.basename(current));
9587
+ rest.push(ops.path.basename(current));
9074
9588
  current = parent;
9589
+ continue;
9075
9590
  }
9591
+ return ops.win32 ? path11.posix.join(real, ...rest.reverse()) : path11.join(real, ...rest.reverse());
9076
9592
  }
9077
9593
  }
9078
- function resolveUnderRoots(p, roots) {
9594
+ function within3(real, root, ops) {
9595
+ if (!ops.win32)
9596
+ return real === root || real.startsWith(root.endsWith(path11.sep) ? root : root + path11.sep);
9597
+ const r = real.toLowerCase();
9598
+ const base = root.toLowerCase();
9599
+ return r === base || r.startsWith(base.endsWith("/") ? base : `${base}/`);
9600
+ }
9601
+ function resolveUnderRoots(p, roots, ops = nativeOps) {
9079
9602
  if (roots.length === 0)
9080
9603
  throw new RuntimeError("this node has no workspaceRoots in node.json, so it prepares no workspaces");
9081
9604
  if (!isAbsolutePath2(p))
9082
9605
  throw new RuntimeError(`${p} is not an absolute path`);
9083
- const real = resolvePath(p);
9606
+ const real = resolvePath(p, ops);
9084
9607
  for (const root of roots) {
9085
9608
  let realRoot;
9086
9609
  try {
9087
- realRoot = realpathSync2(root);
9610
+ realRoot = realOf(root, ops);
9088
9611
  } catch {
9089
9612
  continue;
9090
9613
  }
9091
- if (real === realRoot || real.startsWith(realRoot.endsWith(path11.sep) ? realRoot : realRoot + path11.sep))
9614
+ if (within3(real, realRoot, ops))
9092
9615
  return real;
9093
9616
  }
9094
9617
  throw new RuntimeError(`${p} is not under this node's workspace roots`);
@@ -9143,7 +9666,7 @@ class HostRuntime {
9143
9666
  const directory = resolveUnderRoots(existing, this.roots);
9144
9667
  return this.prepared(repo, directory, "worktree", branch, base, true, ctx);
9145
9668
  }
9146
- const directory = resolveUnderRoots(path11.join(repo, ".worktrees", ...branch.split("/")), this.roots);
9669
+ const directory = resolveUnderRoots(path11.posix.join(repo, ".worktrees", ...branch.split("/")), this.roots);
9147
9670
  if (existsSync3(directory))
9148
9671
  throw new RuntimeError(`${directory} exists and is not the worktree of ${branch}`);
9149
9672
  if (base)
@@ -9175,7 +9698,7 @@ class HostRuntime {
9175
9698
  const target = resolveUnderRoots(input.workspace.repo, this.roots);
9176
9699
  if (existsSync3(target) || isLink3(target))
9177
9700
  throw new RuntimeError(`${target} exists; a clone needs a new directory`);
9178
- const parent = path11.dirname(target);
9701
+ const parent = path11.posix.dirname(target);
9179
9702
  mkdirSync6(parent, { recursive: true });
9180
9703
  if (resolveUnderRoots(parent, this.roots) !== parent)
9181
9704
  throw new RuntimeError(`${parent} moved while preparing`);
@@ -9547,19 +10070,30 @@ import { accessSync, constants as constants2, readFileSync as readFileSync6, sta
9547
10070
  import { homedir as homedir3 } from "node:os";
9548
10071
  import path12 from "node:path";
9549
10072
  var KNOWN = ["opencode", "claude-code", "codex"];
9550
- function executable(file) {
10073
+ function executable(file, win32 = false) {
9551
10074
  try {
9552
10075
  if (!statSync3(file).isFile())
9553
10076
  return false;
9554
- accessSync(file, constants2.X_OK);
10077
+ if (!win32)
10078
+ accessSync(file, constants2.X_OK);
9555
10079
  return true;
9556
10080
  } catch {
9557
10081
  return false;
9558
10082
  }
9559
10083
  }
9560
- function findExecutable(name, env) {
10084
+ var WIN_RUNNABLE = [".exe", ".com"];
10085
+ function windowsNames(name, env) {
10086
+ if (WIN_RUNNABLE.includes(path12.win32.extname(name).toLowerCase()))
10087
+ return [name];
10088
+ const listed = (env.PATHEXT ?? "").split(";").map((e) => e.trim().toLowerCase()).filter((e) => WIN_RUNNABLE.includes(e));
10089
+ const order = listed.length ? [...new Set(listed)] : WIN_RUNNABLE;
10090
+ return order.map((ext) => `${name}${ext}`);
10091
+ }
10092
+ function findExecutable(name, env, platform = process.platform) {
9561
10093
  if (!name)
9562
10094
  return;
10095
+ if (platform === "win32")
10096
+ return findWindowsExecutable(name, env);
9563
10097
  if (path12.isAbsolute(name))
9564
10098
  return executable(name) ? name : undefined;
9565
10099
  if (name.includes("/"))
@@ -9573,13 +10107,30 @@ function findExecutable(name, env) {
9573
10107
  }
9574
10108
  return;
9575
10109
  }
10110
+ function findWindowsExecutable(name, env) {
10111
+ if (path12.isAbsolute(name) || path12.win32.isAbsolute(name))
10112
+ return windowsNames(name, env).find((file) => executable(file, true));
10113
+ if (/[\\/]/.test(name))
10114
+ return;
10115
+ for (const dir of (env.PATH ?? env.Path ?? "").split(";")) {
10116
+ if (!dir || !(path12.isAbsolute(dir) || path12.win32.isAbsolute(dir)))
10117
+ continue;
10118
+ for (const file of windowsNames(name, env)) {
10119
+ const full = path12.join(dir, file);
10120
+ if (executable(full, true))
10121
+ return full;
10122
+ }
10123
+ }
10124
+ return;
10125
+ }
9576
10126
  function detectHarnesses(options = {}) {
9577
10127
  const env = options.env ?? process.env;
9578
10128
  const found = new Set(["opencode"]);
9579
10129
  const home = options.home ?? homedir3();
9580
- if (findExecutable(claudeBinary(env, home), env))
10130
+ const platform = options.platform ?? process.platform;
10131
+ if (findExecutable(claudeBinary(env, home), env, platform))
9581
10132
  found.add("claude-code");
9582
- if (findExecutable(codexBinary(env, home), env))
10133
+ if (findExecutable(codexBinary(env, home, platform), env, platform))
9583
10134
  found.add("codex");
9584
10135
  for (const [name, on] of Object.entries(options.overrides ?? {})) {
9585
10136
  if (on)
@@ -9604,12 +10155,21 @@ function claudeBinary(env, home = homedir3()) {
9604
10155
  } catch {}
9605
10156
  return "claude";
9606
10157
  }
9607
- function codexBinary(env, home = homedir3()) {
10158
+ function codexBinary(env, home = homedir3(), platform = process.platform) {
9608
10159
  if (env.HUB_CODEX_BIN)
9609
10160
  return env.HUB_CODEX_BIN;
9610
10161
  const data = env.XDG_DATA_HOME && path12.isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : path12.join(home, ".local", "share");
9611
- const current = path12.join(data, "hub", "harnesses", "codex", "current", "bin", "codex");
9612
- return executable(current) ? current : "codex";
10162
+ const win32 = platform === "win32";
10163
+ const current = path12.join(data, "hub", "harnesses", "codex", "current", "bin", win32 ? "codex.exe" : "codex");
10164
+ return executable(current, win32) ? current : "codex";
10165
+ }
10166
+ function opencodeBinary(env, home = homedir3(), platform = process.platform) {
10167
+ if (env.HUB_OPENCODE_BIN)
10168
+ return env.HUB_OPENCODE_BIN;
10169
+ const data = env.XDG_DATA_HOME && path12.isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : path12.join(home, ".local", "share");
10170
+ const win32 = platform === "win32";
10171
+ const current = path12.join(data, "hub", "harnesses", "opencode", "current", "bin", win32 ? "opencode.exe" : "opencode");
10172
+ return executable(current, win32) ? current : "opencode";
9613
10173
  }
9614
10174
  // packages/node/src/run.ts
9615
10175
  import { homedir as homedir4 } from "node:os";
@@ -10029,6 +10589,91 @@ class OpenShellCli {
10029
10589
  }
10030
10590
  }
10031
10591
 
10592
+ // packages/node/src/opencode-service.ts
10593
+ import { spawn as spawn3 } from "node:child_process";
10594
+ var START_BACKOFF_MS = 30000;
10595
+ var START_BACKOFF_MAX_MS = 10 * 60000;
10596
+ var START_TIMEOUT_MS = 90000;
10597
+ var STDERR_MAX2 = 2000;
10598
+ function startRun(env = process.env, timeoutMs = START_TIMEOUT_MS) {
10599
+ return (file, args) => new Promise((resolve) => {
10600
+ const clean = Object.fromEntries(Object.entries(env).filter((e) => typeof e[1] === "string"));
10601
+ let child;
10602
+ try {
10603
+ child = spawn3(file, [...args], { shell: false, windowsHide: true, env: clean, stdio: ["ignore", "ignore", "pipe"] });
10604
+ } catch (cause) {
10605
+ return resolve({ code: null, stderr: "", spawnError: cause.message });
10606
+ }
10607
+ let stderr = "";
10608
+ let done = false;
10609
+ const finish = (r) => {
10610
+ if (done)
10611
+ return;
10612
+ done = true;
10613
+ clearTimeout(timer);
10614
+ child.stderr?.destroy();
10615
+ resolve(r);
10616
+ };
10617
+ child.stderr?.on("data", (chunk) => {
10618
+ if (stderr.length < STDERR_MAX2)
10619
+ stderr += chunk.toString("utf8").slice(0, STDERR_MAX2 - stderr.length);
10620
+ });
10621
+ child.on("error", (cause) => finish({ code: null, stderr, spawnError: cause.message }));
10622
+ child.on("exit", (code) => finish({ code, stderr }));
10623
+ const timer = setTimeout(() => {
10624
+ child.kill();
10625
+ finish({ code: null, stderr, spawnError: `timed out after ${Math.round(timeoutMs / 1000)} s` });
10626
+ }, timeoutMs);
10627
+ });
10628
+ }
10629
+ var oneLine2 = (text) => text.replace(/[\p{Cc}\p{Cf}\u2028\u2029]+/gu, " ").replace(/\s+/g, " ").trim().slice(0, 300);
10630
+
10631
+ class OpenCodeServiceStarter {
10632
+ options;
10633
+ nextAttempt = 0;
10634
+ delay = START_BACKOFF_MS;
10635
+ missingLogged = false;
10636
+ constructor(options) {
10637
+ this.options = options;
10638
+ }
10639
+ async start() {
10640
+ const now = (this.options.now ?? Date.now)();
10641
+ if (now < this.nextAttempt)
10642
+ return false;
10643
+ const binary = this.options.binary();
10644
+ if (!binary) {
10645
+ if (!this.missingLogged)
10646
+ this.options.log("OpenCode is not installed (no opencode.exe found), so its background service cannot be started");
10647
+ this.missingLogged = true;
10648
+ return false;
10649
+ }
10650
+ this.missingLogged = false;
10651
+ this.options.log("OpenCode's background service is not running; starting it (opencode service start)");
10652
+ const r = await (this.options.run ?? startRun(this.options.env))(binary, ["service", "start"]);
10653
+ if (r.code === 0) {
10654
+ this.delay = START_BACKOFF_MS;
10655
+ this.nextAttempt = 0;
10656
+ this.options.log("started OpenCode's background service");
10657
+ return true;
10658
+ }
10659
+ const why = r.spawnError ?? (oneLine2(r.stderr) || `exit ${r.code}`);
10660
+ this.options.log(`could not start OpenCode's background service (${why}); retrying in ${Math.round(this.delay / 1000)} s`);
10661
+ this.nextAttempt = (this.options.now ?? Date.now)() + this.delay;
10662
+ this.delay = Math.min(this.delay * 2, START_BACKOFF_MAX_MS);
10663
+ return false;
10664
+ }
10665
+ }
10666
+ function serviceDiscovery(read, starter, platform = process.platform) {
10667
+ if (platform !== "win32")
10668
+ return read;
10669
+ return async () => {
10670
+ const endpoint = await read();
10671
+ if (endpoint)
10672
+ return endpoint;
10673
+ return await starter.start() ? read() : undefined;
10674
+ };
10675
+ }
10676
+
10032
10677
  // packages/runtime-openshell/src/runtime.ts
10033
10678
  import { createHash as createHash2 } from "node:crypto";
10034
10679
  import { existsSync as existsSync4, mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync7, writeFileSync as writeFileSync3 } from "node:fs";
@@ -10037,7 +10682,7 @@ import path14 from "node:path";
10037
10682
  // packages/runtime-openshell/package.json
10038
10683
  var package_default3 = {
10039
10684
  name: "@huynle/hub-runtime-openshell",
10040
- version: "0.8.7",
10685
+ version: "0.9.1",
10041
10686
  private: true,
10042
10687
  description: "The `openshell` runtime of `hub node`: launches in NVIDIA OpenShell sandboxes, driven through the openshell CLI (internal; shipped inside @huynle/hub, not published on its own).",
10043
10688
  license: "MIT",
@@ -10597,6 +11242,16 @@ async function sweepSandboxes(client, machineID) {
10597
11242
 
10598
11243
  // packages/node/src/run.ts
10599
11244
  var EXIT_CONFIG = 78;
11245
+ var IDLE_RECHECK_MS = 60000;
11246
+ var sleep5 = (ms, signal) => new Promise((resolve) => {
11247
+ const timer = setTimeout(done, ms);
11248
+ function done() {
11249
+ clearTimeout(timer);
11250
+ signal.removeEventListener("abort", done);
11251
+ resolve();
11252
+ }
11253
+ signal.addEventListener("abort", done, { once: true });
11254
+ });
10600
11255
  var openCodeServiceFile = (env, home) => path15.join(env.XDG_STATE_HOME || path15.join(home, ".local", "state"), "opencode", "service.json");
10601
11256
  function startOpenCodeWarmers(config, service, log, signal) {
10602
11257
  const roots = () => config.workspaceRoots;
@@ -10632,6 +11287,7 @@ async function setupOpenShell(config, credentials, options, env, log) {
10632
11287
  async function runNode(options) {
10633
11288
  const env = options.env ?? process.env;
10634
11289
  const home = options.home ?? homedir4();
11290
+ const platform = options.platform ?? process.platform;
10635
11291
  const log = options.log ?? ((line) => process.stderr.write(`[hub node] ${line}
10636
11292
  `));
10637
11293
  const credentials = readSpokeCredentials(env, home);
@@ -10640,12 +11296,23 @@ async function runNode(options) {
10640
11296
  return 1;
10641
11297
  }
10642
11298
  const file = nodeConfigPath(env, home);
10643
- const { config, problems } = readNodeConfig(file);
11299
+ let { config, problems } = readNodeConfig(file, { platform, home });
10644
11300
  for (const problem of problems)
10645
11301
  log(`${file}: ${problem}`);
10646
11302
  if (config.workspaceRoots.length === 0) {
10647
11303
  log(`${file} lists no workspaceRoots, so this node can prepare no workspaces; add e.g. {"workspaceRoots": ["${home}/projects"]}`);
10648
- return EXIT_CONFIG;
11304
+ if (platform !== "win32")
11305
+ return EXIT_CONFIG;
11306
+ log(`waiting: re-reading ${file} every ${Math.round((options.idleRecheckMs ?? IDLE_RECHECK_MS) / 1000)} s until it lists workspaceRoots`);
11307
+ while (config.workspaceRoots.length === 0) {
11308
+ await sleep5(options.idleRecheckMs ?? IDLE_RECHECK_MS, options.signal);
11309
+ if (options.signal.aborted)
11310
+ return 0;
11311
+ ({ config, problems } = readNodeConfig(file, { platform, home }));
11312
+ }
11313
+ for (const problem of problems)
11314
+ log(`${file}: ${problem}`);
11315
+ log(`workspace roots now set in ${file}; starting`);
10649
11316
  }
10650
11317
  if (options.signal.aborted)
10651
11318
  return 0;
@@ -10670,8 +11337,16 @@ async function runNode(options) {
10670
11337
  });
10671
11338
  log(`hub node ${NODE_VERSION}: ${config.slots} slots, roots ${config.workspaceRoots.join(", ")}; host harnesses ${harnesses.join(", ") || "none"}`);
10672
11339
  daemon.start();
10673
- if (harnesses.includes("opencode"))
10674
- startOpenCodeWarmers(config, options.openCodeService ?? (() => readServiceRegistration(openCodeServiceFile(env, home))), log, options.signal);
11340
+ if (harnesses.includes("opencode")) {
11341
+ const read = options.openCodeService ?? (() => readServiceRegistration(openCodeServiceFile(env, home)));
11342
+ const starter = new OpenCodeServiceStarter({
11343
+ binary: options.openCodeBinary ?? (() => findExecutable(opencodeBinary(env, home, platform), env, platform)),
11344
+ log,
11345
+ env,
11346
+ ...options.startOpenCode ? { run: options.startOpenCode } : {}
11347
+ });
11348
+ startOpenCodeWarmers(config, serviceDiscovery(read, starter, platform), log, options.signal);
11349
+ }
10675
11350
  await new Promise((resolve) => {
10676
11351
  if (options.signal.aborted)
10677
11352
  return resolve();
@@ -10972,6 +11647,64 @@ NoNewPrivileges=true
10972
11647
  WantedBy=default.target
10973
11648
  `;
10974
11649
 
11650
+ // scripts/hub-node.task.xml
11651
+ var hub_node_task_default = `<?xml version="1.0" encoding="UTF-16"?>
11652
+ <!-- The hub node as a per-user logon task (Windows Task Scheduler). hubctl setup and \`hub node install\` render it
11653
+ (no visible window: the hidden console host starts hubctl; native paths; Windows argv quoting) and write it as
11654
+ UTF-16LE with a BOM, which schtasks /XML requires. -->
11655
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
11656
+ <RegistrationInfo>
11657
+ <Description>Hub node (prepares launch workspaces for the hub). Started at logon without a window; started again within a minute after it exits.</Description>
11658
+ </RegistrationInfo>
11659
+ <Triggers>
11660
+ <LogonTrigger>
11661
+ <Enabled>true</Enabled>
11662
+ <UserId>__USER__</UserId>
11663
+ <Repetition>
11664
+ <Interval>PT1M</Interval>
11665
+ <StopAtDurationEnd>false</StopAtDurationEnd>
11666
+ </Repetition>
11667
+ </LogonTrigger>
11668
+ </Triggers>
11669
+ <Principals>
11670
+ <Principal id="Author">
11671
+ <UserId>__USER__</UserId>
11672
+ <LogonType>InteractiveToken</LogonType>
11673
+ <RunLevel>LeastPrivilege</RunLevel>
11674
+ </Principal>
11675
+ </Principals>
11676
+ <Settings>
11677
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
11678
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
11679
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
11680
+ <AllowHardTerminate>true</AllowHardTerminate>
11681
+ <StartWhenAvailable>false</StartWhenAvailable>
11682
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
11683
+ <IdleSettings>
11684
+ <StopOnIdleEnd>false</StopOnIdleEnd>
11685
+ <RestartOnIdle>false</RestartOnIdle>
11686
+ </IdleSettings>
11687
+ <AllowStartOnDemand>true</AllowStartOnDemand>
11688
+ <Enabled>true</Enabled>
11689
+ <Hidden>false</Hidden>
11690
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
11691
+ <WakeToRun>false</WakeToRun>
11692
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
11693
+ <Priority>7</Priority>
11694
+ <RestartOnFailure>
11695
+ <Interval>PT1M</Interval>
11696
+ <Count>999</Count>
11697
+ </RestartOnFailure>
11698
+ </Settings>
11699
+ <Actions Context="Author">
11700
+ <Exec>
11701
+ <Command>%SystemRoot%\\System32\\conhost.exe</Command>
11702
+ <Arguments>--headless __ARGV__</Arguments>
11703
+ </Exec>
11704
+ </Actions>
11705
+ </Task>
11706
+ `;
11707
+
10975
11708
  // packages/cli/src/bootstrap/release/assets.ts
10976
11709
  var EMBEDDED_TEXT = Object.freeze({
10977
11710
  "service/launchd/node": io_github_huynle_hub_node_default,
@@ -10979,7 +11712,8 @@ var EMBEDDED_TEXT = Object.freeze({
10979
11712
  "service/launchd/claude-code": io_github_huynle_hub_claude_code_default,
10980
11713
  "service/systemd-user/claude-code": hub_claude_code_default,
10981
11714
  "service/launchd/codex-spoke": io_github_huynle_hub_codex_spoke_default,
10982
- "service/systemd-user/codex-spoke": hub_codex_spoke_default
11715
+ "service/systemd-user/codex-spoke": hub_codex_spoke_default,
11716
+ "service/task-scheduler/node": hub_node_task_default
10983
11717
  });
10984
11718
 
10985
11719
  // packages/cli/src/bootstrap/adapters/node-services/render.ts
@@ -10988,7 +11722,34 @@ var SERVICE_ENVIRONMENT = Object.freeze({
10988
11722
  "claude-code": ["HUB_SPOKE_CONFIG", "XDG_CONFIG_HOME", "HUB_STATE_DIR", "XDG_STATE_HOME", "HUB_CLAUDE_SOCKET", "HUB_CLAUDE_BIN"],
10989
11723
  "codex-spoke": ["HUB_SPOKE_CONFIG", "XDG_CONFIG_HOME", "HUB_STATE_DIR", "XDG_STATE_HOME", "CODEX_HOME", "HUB_CODEX_BIN", "HUB_CODEX_DAEMON_SOCKET"]
10990
11724
  });
10991
- var UNIT_ROOT = Object.freeze({ launchd: "launch-agents", "systemd-user": "systemd-user" });
11725
+ var UNIT_ROOT = SERVICE_ROOT;
11726
+ var TASK_NAME = /^hub-(?:node|claude-code|codex-spoke|test-[a-z0-9-]{1,32})-[A-Za-z0-9._-]{1,64}$/;
11727
+ var TEST_PREFIX = /^hub-test-[a-z0-9-]{1,32}$/;
11728
+ var SID2 = /^S-1-[0-9]{1,12}(?:-[0-9]{1,10}){1,15}$/;
11729
+ function taskNameFor(service, user, env) {
11730
+ const safe = user.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64);
11731
+ if (!safe)
11732
+ throw new Error("the Windows user name is empty; set USERNAME");
11733
+ const prefix = env.HUBCTL_TEST_TASK_PREFIX;
11734
+ if (prefix !== undefined && !TEST_PREFIX.test(prefix))
11735
+ throw new Error("HUBCTL_TEST_TASK_PREFIX must start with hub-test- (lower-case letters, digits and -)");
11736
+ return `${prefix ?? `hub-${service}`}-${safe}`;
11737
+ }
11738
+ function quoteWindowsArg(word) {
11739
+ if (word !== "" && !/[\s"]/.test(word))
11740
+ return word;
11741
+ let out = '"';
11742
+ let slashes = 0;
11743
+ for (const ch of word) {
11744
+ if (ch === "\\")
11745
+ slashes++;
11746
+ else {
11747
+ out += "\\".repeat(ch === '"' ? slashes * 2 + 1 : slashes) + ch;
11748
+ slashes = 0;
11749
+ }
11750
+ }
11751
+ return `${out}${"\\".repeat(slashes * 2)}"`;
11752
+ }
10992
11753
  var assetIDFor = (service, manager) => `service/${manager}/${service}`;
10993
11754
  function serviceName(service, manager) {
10994
11755
  const file = EMBEDDED_ASSETS[assetIDFor(service, manager)].installAs;
@@ -11048,6 +11809,40 @@ ${indent}<string>${xml(value)}</string>`).join("");
11048
11809
  return template.replace(/<!--[\s\S]*?-->/, () => `<!-- ${header2(asset, by)} -->`).replace(programArguments, (_all, open, _words, close) => `${open}${argv.map((w) => `
11049
11810
  ${indent}<string>${xml(w)}</string>`).join("")}${close}`).replaceAll("__HOME__", xml(home)).replace(/(<key>PATH<\/key>\s*<string>[^<]*<\/string>)/, (line) => `${line}${extra}`);
11050
11811
  }
11812
+ var xmlText = (text) => text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
11813
+ function renderTask(c, input, asset, template) {
11814
+ if (input.layout.kind !== "standalone")
11815
+ c.add("/layout", "unsupported_platform", "a Windows logon task starts the standalone hubctl.exe only, not a script under a runtime");
11816
+ if (Object.keys(input.environment ?? {}).length > 0)
11817
+ c.add("/environment", "invalid_value", "a Task Scheduler task carries no environment of its own");
11818
+ const task = input.task;
11819
+ if (!task)
11820
+ c.add("/task", "missing_field", "a Task Scheduler task needs its name and the user's SID");
11821
+ else {
11822
+ if (!TASK_NAME.test(task.name))
11823
+ c.add("/task/name", "invalid_value", "expected a hub task name (hub-<service>-<user>)");
11824
+ if (!SID2.test(task.userSid))
11825
+ c.add("/task/userSid", "invalid_value", "expected the user's SID (S-1-…)");
11826
+ }
11827
+ if (!isAbsolutePath(input.home) || !isWindowsPath(input.home))
11828
+ c.add("/home", "invalid_path", "a Windows logon task needs a Windows home directory (C:/…)");
11829
+ if (!/<Arguments>--headless __ARGV__<\/Arguments>/.test(template))
11830
+ c.add("/asset", "inconsistent", `${asset} does not start hubctl under the hidden console host`);
11831
+ if (c.issues.length > 0)
11832
+ return;
11833
+ const argv = serviceArgv(input.layout, input.service);
11834
+ const words = argv.map((w, i) => i === 0 ? toNativePath(w, "win32") : w);
11835
+ if (words.some((w) => w.includes('"')))
11836
+ c.add("/layout", "invalid_path", "the program path may not contain a double quote");
11837
+ return template.replace(/<!--[\s\S]*?-->/, () => `<!-- ${header2(asset, input.writtenBy)} -->`).replaceAll("__USER__", () => task.userSid).replace("__ARGV__", () => xmlText(words.map(quoteWindowsArg).join(" ")));
11838
+ }
11839
+ var utf16le = (text) => {
11840
+ const body = Buffer.from(text, "utf16le");
11841
+ const bytes = new Uint8Array(body.length + 2);
11842
+ bytes.set([255, 254]);
11843
+ bytes.set(body, 2);
11844
+ return bytes;
11845
+ };
11051
11846
  function renderServiceUnit(input) {
11052
11847
  const c = new Checker;
11053
11848
  const asset = assetIDFor(input.service, input.manager);
@@ -11057,22 +11852,44 @@ function renderServiceUnit(input) {
11057
11852
  c.add("/service", "invalid_value", `no embedded template for ${input.service} under ${input.manager}`);
11058
11853
  return c.result(undefined);
11059
11854
  }
11855
+ if (input.manager === "task-scheduler") {
11856
+ const text = renderTask(c, input, asset, template);
11857
+ if (text === undefined || c.issues.length > 0)
11858
+ return c.result(undefined);
11859
+ if (PLACEHOLDER.test(text))
11860
+ c.add("/asset", "inconsistent", `${asset} has a placeholder this hubctl does not fill`);
11861
+ if (c.issues.length > 0)
11862
+ return c.result(undefined);
11863
+ const bytes = utf16le(text);
11864
+ return c.result({
11865
+ asset,
11866
+ service: input.service,
11867
+ manager: input.manager,
11868
+ path: { root: UNIT_ROOT[input.manager], rel: entry.installAs },
11869
+ name: input.task.name,
11870
+ argv: serviceArgv(input.layout, input.service),
11871
+ text,
11872
+ bytes,
11873
+ sha256: `sha256:${sha256Hex(bytes)}`
11874
+ });
11875
+ }
11876
+ const manager = input.manager;
11060
11877
  let argv = [];
11061
11878
  try {
11062
11879
  argv = serviceArgv(input.layout, input.service);
11063
11880
  } catch (error) {
11064
11881
  c.add("/layout", "invalid_path", error.message);
11065
11882
  }
11066
- const homeOK = isAbsolutePath(input.home) && input.home !== "/" && (input.manager !== "systemd-user" || SYSTEMD_HOME.test(input.home));
11883
+ const homeOK = isAbsolutePath(input.home) && input.home !== "/" && (manager !== "systemd-user" || SYSTEMD_HOME.test(input.home));
11067
11884
  if (!homeOK)
11068
11885
  c.add("/home", "invalid_path", "the service's home must be an absolute, normalized directory other than /");
11069
11886
  const env = checkEnvironment(c, input.service, input.environment ?? {});
11070
11887
  if (c.issues.length > 0)
11071
11888
  return c.result(undefined);
11072
11889
  const command = SERVICE_COMMANDS[input.service];
11073
- const raw = input.manager === "systemd-user" ? renderSystemd(c, asset, template, argv, command, input.home, env, input.writtenBy) : renderLaunchd(c, asset, template, argv, command, input.home, env, input.writtenBy);
11890
+ const raw = manager === "systemd-user" ? renderSystemd(c, asset, template, argv, command, input.home, env, input.writtenBy) : renderLaunchd(c, asset, template, argv, command, input.home, env, input.writtenBy);
11074
11891
  const asBun = input.layout.kind === "script";
11075
- const text = asBun ? raw : raw.replace(BUN_BE_BUN[input.manager], "");
11892
+ const text = asBun ? raw : raw.replace(BUN_BE_BUN[manager], "");
11076
11893
  if (!asBun && text.includes("BUN_BE_BUN"))
11077
11894
  c.add("/asset", "inconsistent", `${asset} sets BUN_BE_BUN, which a standalone hubctl must not get`);
11078
11895
  if (PLACEHOLDER.test(text))
@@ -11087,7 +11904,7 @@ function renderServiceUnit(input) {
11087
11904
  service: input.service,
11088
11905
  manager: input.manager,
11089
11906
  path: { root: UNIT_ROOT[input.manager], rel: entry.installAs },
11090
- name: serviceName(input.service, input.manager),
11907
+ name: serviceName(input.service, manager),
11091
11908
  argv,
11092
11909
  text,
11093
11910
  bytes,
@@ -11095,6 +11912,39 @@ function renderServiceUnit(input) {
11095
11912
  });
11096
11913
  }
11097
11914
 
11915
+ // packages/cli/src/bootstrap/discovery/util.ts
11916
+ function normalizeAbs(value) {
11917
+ if (typeof value !== "string")
11918
+ return;
11919
+ let p = value;
11920
+ if (/^[A-Za-z]:[\\/]/.test(p))
11921
+ p = p[0].toUpperCase() + p.slice(1).replace(/\\/g, "/");
11922
+ else if (!p.startsWith("/") || p.startsWith("//"))
11923
+ return;
11924
+ p = p.replace(/\/{2,}/g, "/");
11925
+ if (p.endsWith("/") && p !== "/" && !/^[A-Z]:\/$/.test(p))
11926
+ p = p.slice(0, -1);
11927
+ return isAbsolutePath(p) ? p : undefined;
11928
+ }
11929
+ function canonicalWinLink(text) {
11930
+ const stripped = text.replace(/^(\\\\\?\\|\\\?\?\\)/, "");
11931
+ return normalizeAbs(stripped) ?? text;
11932
+ }
11933
+ var probePath = (platform, text) => text !== undefined && platform === "win32" ? canonicalWinLink(text) : text;
11934
+ var join = (dir, rel) => dir.endsWith("/") ? `${dir}${rel}` : `${dir}/${rel}`;
11935
+ var decode = (bytes) => bytes ? new TextDecoder("utf-8", { fatal: false }).decode(bytes) : "";
11936
+ var oneLine3 = (v, max = 64) => v && v.length <= max && !/[\u0000-\u001f\u007f]/.test(v) ? v : undefined;
11937
+ var hostAbs = (platform, value) => {
11938
+ const p = normalizeAbs(value);
11939
+ return p && (platform !== "win32" || isWindowsPath(p)) ? p : undefined;
11940
+ };
11941
+ var underDir = (p, dir) => {
11942
+ const prefix = dir.endsWith("/") ? dir : `${dir}/`;
11943
+ return isWindowsPath(p) ? p.toLowerCase().startsWith(prefix.toLowerCase()) : p.startsWith(prefix);
11944
+ };
11945
+ var WINDOWS_COMMAND_EXTENSIONS = [".com", ".exe", ".bat", ".cmd"];
11946
+ var isWindowsExecutable = (path) => WINDOWS_COMMAND_EXTENSIONS.some((e) => path.toLowerCase().endsWith(e));
11947
+
11098
11948
  // packages/cli/src/node.ts
11099
11949
  var NODE_LABEL = "io.github.huynle.hub-node";
11100
11950
  var NODE_UNIT = "hub-node.service";
@@ -11169,13 +12019,58 @@ function systemd(spec, io, extra) {
11169
12019
  check: [`systemctl --user status ${unit}`, `journalctl --user -u ${unit} -f`]
11170
12020
  };
11171
12021
  }
12022
+ function taskScheduler(spec, io, extra) {
12023
+ if (spec.service !== "node")
12024
+ throw new CliError(EXIT.unavailable, `${spec.command} install does not support Windows yet: hubctl installs no user service for it there`, spec.elsewhere);
12025
+ const home = normalizeAbs(io.home);
12026
+ if (!home)
12027
+ throw new CliError(EXIT.usage, `cannot write the ${spec.command} task: the home directory ${io.home} is not a drive path`);
12028
+ const state = normalizeAbs(resolveHubStateDir(io.env, io.home).dir);
12029
+ if (!state)
12030
+ throw new CliError(EXIT.usage, `cannot write the ${spec.command} task: the hub state directory is not a drive path`);
12031
+ const executable = normalizeAbs(io.program.execPath);
12032
+ const found = programLayout(io.program);
12033
+ const layout = found.kind === "standalone" && executable ? { kind: "standalone", executable } : found;
12034
+ const { environment, skipped } = carried(spec, io, extra);
12035
+ let name;
12036
+ try {
12037
+ name = taskNameFor(spec.service, io.env.USERNAME ?? "", io.env);
12038
+ } catch (error) {
12039
+ throw new CliError(EXIT.usage, `cannot write the ${spec.command} task: ${error.message}`);
12040
+ }
12041
+ const r = renderServiceUnit({ service: spec.service, manager: "task-scheduler", layout, home, writtenBy: spec.command, task: { name, userSid: (io.userSid ?? (() => defaultWinAcl().sid()))() } });
12042
+ if (!r.ok)
12043
+ throw new CliError(EXIT.usage, `cannot write the ${spec.command} task: ${r.issues.map((i) => `${i.path ? `${i.path}: ` : ""}${i.message}`).join("; ")}`);
12044
+ const file = toNativePath(`${state}/tasks/${r.value.path.rel}`, "win32");
12045
+ const stopNode = `Get-CimInstance Win32_Process -Filter "Name='hubctl.exe'" | Where-Object CommandLine -like '* node run' | Invoke-CimMethod -MethodName Terminate # PowerShell: /End stops only the hidden console host`;
12046
+ return {
12047
+ kind: "task-scheduler",
12048
+ file,
12049
+ content: r.value.text,
12050
+ bytes: r.value.bytes,
12051
+ skipped: [...Object.keys(environment), ...skipped].sort(),
12052
+ dirs: [],
12053
+ load: [`schtasks /Create /TN ${name} /XML "${file}" /F`, `schtasks /Run /TN ${name}`],
12054
+ unload: [`schtasks /End /TN ${name}`, stopNode, `schtasks /Delete /TN ${name} /F`],
12055
+ check: [`schtasks /Query /TN ${name} /V /FO LIST`]
12056
+ };
12057
+ }
11172
12058
  function serviceFor(spec, io, extra) {
11173
12059
  if (io.platform === "darwin")
11174
12060
  return launchd(spec, io, extra);
11175
12061
  if (io.platform === "linux")
11176
12062
  return systemd(spec, io, extra);
12063
+ if (io.platform === "win32")
12064
+ return taskScheduler(spec, io, extra);
11177
12065
  throw new CliError(EXIT.unavailable, `${spec.command} install supports macOS (launchd) and Linux (systemd), not ${io.platform}`, spec.elsewhere);
11178
12066
  }
12067
+ var readBytes = (file) => {
12068
+ try {
12069
+ return readFileSync8(file);
12070
+ } catch {
12071
+ return;
12072
+ }
12073
+ };
11179
12074
  var readText = (file) => {
11180
12075
  try {
11181
12076
  return readFileSync8(file, "utf8");
@@ -11202,21 +12097,22 @@ function installNode(io, options) {
11202
12097
  }
11203
12098
  function installService(spec, io, options, given, extra) {
11204
12099
  const service = serviceFor(spec, io, extra);
11205
- const warnings = [...given, ...service.skipped.map((key) => `${key} is not an absolute path, so the service does not carry it`)];
12100
+ const why = service.kind === "task-scheduler" ? "is set in this shell, but a logon task has no environment of its own, so the node does not see it" : "is not an absolute path, so the service does not carry it";
12101
+ const warnings = [...given, ...service.skipped.map((key) => `${key} ${why}`)];
11206
12102
  const load = commands(`Load it ${NOT_RUN}:`, service.load);
11207
12103
  if (options.dryRun)
11208
12104
  return { stdout: `would write ${service.file}:
11209
12105
 
11210
12106
  ${service.content}
11211
12107
  ${load}`, warnings };
11212
- const current = readText(service.file);
12108
+ const current = service.bytes ? readBytes(service.file) : readText(service.file);
11213
12109
  for (const dir of service.dirs)
11214
12110
  mkdirSync7(dir, { recursive: true });
11215
- if (current === service.content)
12111
+ if (service.bytes ? current !== undefined && Buffer.from(service.bytes).equals(current) : current === service.content)
11216
12112
  return { stdout: `${service.file} is up to date
11217
12113
  ${load}`, warnings };
11218
12114
  mkdirSync7(path16.dirname(service.file), { recursive: true });
11219
- writeFileSync4(service.file, service.content, { mode: 420 });
12115
+ writeFileSync4(service.file, service.bytes ?? service.content, { mode: 420 });
11220
12116
  return { stdout: `${current === undefined ? "wrote" : "updated"} ${service.file}
11221
12117
  ${load}`, warnings };
11222
12118
  }
@@ -11238,6 +12134,10 @@ ${stop}`, warnings: [] };
11238
12134
  function serviceState(spec, io, extra) {
11239
12135
  try {
11240
12136
  const s = serviceFor(spec, io, extra);
12137
+ if (s.bytes) {
12138
+ const bytes = readBytes(s.file);
12139
+ return { kind: s.kind, file: s.file, installed: bytes !== undefined, current: bytes !== undefined && Buffer.from(s.bytes).equals(bytes), check: s.check };
12140
+ }
11241
12141
  const text = readText(s.file);
11242
12142
  return { kind: s.kind, file: s.file, installed: text !== undefined, current: text === s.content, check: s.check };
11243
12143
  } catch {
@@ -11278,9 +12178,28 @@ function nodeStatusText(status) {
11278
12178
  return lines.join(`
11279
12179
  `);
11280
12180
  }
12181
+ var NODE_LOG_MAX_BYTES = 1024 * 1024;
12182
+ function nodeLogger(o) {
12183
+ const file = path16.join(o.stateDir, "node.log");
12184
+ return (line) => {
12185
+ o.stderr(`[hub node] ${line}
12186
+ `);
12187
+ if (o.platform !== "win32")
12188
+ return;
12189
+ try {
12190
+ mkdirSync7(o.stateDir, { recursive: true });
12191
+ try {
12192
+ if (statSync4(file).size > NODE_LOG_MAX_BYTES)
12193
+ renameSync5(file, `${file}.1`);
12194
+ } catch {}
12195
+ appendFileSync(file, `${(o.now ?? (() => new Date))().toISOString()} ${line}
12196
+ `);
12197
+ } catch {}
12198
+ };
12199
+ }
11281
12200
  function runNodeCommand(io) {
11282
- return runNode({ env: io.env, home: io.home, signal: io.signal ?? new AbortController().signal, log: (line) => io.stderr(`[hub node] ${line}
11283
- `) });
12201
+ const log = nodeLogger({ platform: io.platform ?? process.platform, stateDir: resolveHubStateDir(io.env, io.home).dir, stderr: (s) => io.stderr(s) });
12202
+ return runNode({ env: io.env, home: io.home, signal: io.signal ?? new AbortController().signal, log });
11284
12203
  }
11285
12204
 
11286
12205
  // packages/cli/src/openshell.ts
@@ -12515,8 +13434,8 @@ async function runMcpPipe(options) {
12515
13434
  }
12516
13435
 
12517
13436
  // packages/claude-code/src/launcher.ts
12518
- import { spawn as spawn3 } from "node:child_process";
12519
- import { realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
13437
+ import { spawn as spawn4 } from "node:child_process";
13438
+ import { realpathSync as realpathSync4, statSync as statSync5 } from "node:fs";
12520
13439
  import path19 from "node:path";
12521
13440
  var INTERRUPTED = "Interrupted from the hub";
12522
13441
  var CONTROL_WAIT_MS = 5000;
@@ -12525,7 +13444,7 @@ var KILL_GRACE_MS = 1000;
12525
13444
  var LINE_MAX = 16 * 1024 * 1024;
12526
13445
  var STDERR_KEEP = 2000;
12527
13446
  var record2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
12528
- var sleep5 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13447
+ var sleep6 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12529
13448
  var AGENT2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
12530
13449
  var MODEL = /^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,299}$/;
12531
13450
  var PROVIDERS = new Set(["anthropic", "claude-code"]);
@@ -12539,7 +13458,7 @@ function launchDirectory(directory, roots) {
12539
13458
  } catch {
12540
13459
  throw new Error(`${directory} does not exist`);
12541
13460
  }
12542
- if (!statSync4(real).isDirectory())
13461
+ if (!statSync5(real).isDirectory())
12543
13462
  throw new Error(`${directory} is not a directory`);
12544
13463
  for (const root of roots) {
12545
13464
  let base;
@@ -12613,7 +13532,7 @@ class ClaudeLauncher {
12613
13532
  delete env.BUN_BE_BUN;
12614
13533
  let child;
12615
13534
  try {
12616
- child = spawn3(this.#options.bin, launchArgs(input), { cwd: input.directory, env, stdio: ["pipe", "pipe", "pipe"] });
13535
+ child = spawn4(this.#options.bin, launchArgs(input), { cwd: input.directory, env, stdio: ["pipe", "pipe", "pipe"] });
12617
13536
  } catch (cause) {
12618
13537
  throw new Error(`cannot run ${this.#options.bin}: ${cause.message}`);
12619
13538
  }
@@ -12701,7 +13620,7 @@ class ClaudeLauncher {
12701
13620
  const answered = new Promise((resolve) => proc.controls.set(id, resolve));
12702
13621
  proc.child.stdin.write(`${JSON.stringify({ type: "control_request", request_id: id, request })}
12703
13622
  `);
12704
- const response = await Promise.race([answered, sleep5(waitMs).then(() => {
13623
+ const response = await Promise.race([answered, sleep6(waitMs).then(() => {
12705
13624
  return;
12706
13625
  })]);
12707
13626
  proc.controls.delete(id);
@@ -12712,10 +13631,10 @@ class ClaudeLauncher {
12712
13631
  for (const proc of procs)
12713
13632
  proc.child.stdin.end();
12714
13633
  await Promise.all(procs.map(async (proc) => {
12715
- if (await Promise.race([proc.exited.then(() => true), sleep5(STOP_GRACE_MS).then(() => false)]))
13634
+ if (await Promise.race([proc.exited.then(() => true), sleep6(STOP_GRACE_MS).then(() => false)]))
12716
13635
  return;
12717
13636
  proc.child.kill("SIGTERM");
12718
- if (await Promise.race([proc.exited.then(() => true), sleep5(KILL_GRACE_MS).then(() => false)]))
13637
+ if (await Promise.race([proc.exited.then(() => true), sleep6(KILL_GRACE_MS).then(() => false)]))
12719
13638
  return;
12720
13639
  proc.child.kill("SIGKILL");
12721
13640
  await proc.exited;
@@ -13714,14 +14633,14 @@ async function startDaemon(options) {
13714
14633
  }
13715
14634
 
13716
14635
  // packages/claude-code/src/wrapper.ts
13717
- import { spawn as spawn4 } from "node:child_process";
14636
+ import { spawn as spawn5 } from "node:child_process";
13718
14637
  import { randomBytes as randomBytes9 } from "node:crypto";
13719
14638
  import { closeSync as closeSync5, openSync as openSync5 } from "node:fs";
13720
14639
  import path23 from "node:path";
13721
14640
  // packages/claude-code/package.json
13722
14641
  var package_default4 = {
13723
14642
  name: "@huynle/hub-claude-code",
13724
- version: "0.8.7",
14643
+ version: "0.9.1",
13725
14644
  private: true,
13726
14645
  description: "The Claude Code hooks spoke: per-user daemon, hook bridge, plugin dir, `hub claude` wrapper and daemon-backed MCP server (internal; shipped inside @huynle/hub, not published on its own).",
13727
14646
  license: "MIT",
@@ -13734,7 +14653,7 @@ var package_default4 = {
13734
14653
 
13735
14654
  // packages/claude-code/src/plugin.ts
13736
14655
  import { createHash as createHash5 } from "node:crypto";
13737
- import { existsSync as existsSync7, mkdirSync as mkdirSync9, renameSync as renameSync5, rmSync as rmSync9, writeFileSync as writeFileSync6 } from "node:fs";
14656
+ import { existsSync as existsSync7, mkdirSync as mkdirSync9, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync6 } from "node:fs";
13738
14657
  import path22 from "node:path";
13739
14658
  var HOOK_EVENTS = [
13740
14659
  "SessionStart",
@@ -13792,7 +14711,7 @@ function writePlugin(base, options) {
13792
14711
  writeFileSync6(path22.join(tmp, file), content, { mode: 384 });
13793
14712
  }
13794
14713
  try {
13795
- renameSync5(tmp, dir);
14714
+ renameSync6(tmp, dir);
13796
14715
  } catch (cause) {
13797
14716
  rmSync9(tmp, { recursive: true, force: true });
13798
14717
  if (!existsSync7(dir))
@@ -13818,7 +14737,7 @@ function spawnDaemon(io) {
13818
14737
  ensurePrivateDir(dir);
13819
14738
  const log = openSync5(path23.join(dir, "daemon.log"), "a", 384);
13820
14739
  try {
13821
- const child = spawn4(io.program.execPath, [io.program.entry, "claude-code", "daemon"], { detached: true, stdio: ["ignore", log, log], env: { ...io.env, BUN_BE_BUN: "1" } });
14740
+ const child = spawn5(io.program.execPath, [io.program.entry, "claude-code", "daemon"], { detached: true, windowsHide: true, stdio: ["ignore", log, log], env: { ...io.env, BUN_BE_BUN: "1" } });
13822
14741
  child.on("error", () => {});
13823
14742
  child.unref();
13824
14743
  } finally {
@@ -13857,7 +14776,7 @@ async function prepare(io, socket) {
13857
14776
  }
13858
14777
  function spawnClaude(bin, args, env, stderr) {
13859
14778
  return new Promise((resolve) => {
13860
- const child = spawn4(bin, args, { stdio: "inherit", env });
14779
+ const child = spawn5(bin, args, { stdio: "inherit", windowsHide: true, env });
13861
14780
  const ignore = () => {};
13862
14781
  const forward = () => child.kill("SIGTERM");
13863
14782
  process.on("SIGINT", ignore);
@@ -14010,7 +14929,7 @@ import path26 from "node:path";
14010
14929
  // packages/preset/package.json
14011
14930
  var package_default5 = {
14012
14931
  name: "@huynle/hub-preset",
14013
- version: "0.8.7",
14932
+ version: "0.9.1",
14014
14933
  private: true,
14015
14934
  description: "The preset spoke: drives a harness without a spoke of its own through a terminal pane (tmux), described by a data-only preset; official presets in presets/ (internal; shipped inside @huynle/hub, not published on its own).",
14016
14935
  license: "MIT",
@@ -14286,12 +15205,12 @@ class PresetSpoke {
14286
15205
  }
14287
15206
 
14288
15207
  // packages/preset/src/tmux.ts
14289
- import { execFile } from "node:child_process";
15208
+ import { execFile as execFile2 } from "node:child_process";
14290
15209
  import { randomBytes as randomBytes10 } from "node:crypto";
14291
15210
  var TMUX_TIMEOUT_MS = 5000;
14292
15211
  function tmuxRunner(bin = "tmux", env = process.env, timeoutMs = TMUX_TIMEOUT_MS) {
14293
15212
  return (args) => new Promise((resolve) => {
14294
- execFile(bin, args, { env, timeout: timeoutMs, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
15213
+ execFile2(bin, args, { env, timeout: timeoutMs, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
14295
15214
  const exit = error?.code;
14296
15215
  const code = !error ? 0 : typeof exit === "number" ? exit : 1;
14297
15216
  resolve({ code, stdout: stdout ?? "", stderr: stderr || (error ? error.message : "") });
@@ -14535,7 +15454,7 @@ import { openSync as openSync6, closeSync as closeSync6, readFileSync as readFil
14535
15454
  // packages/codex/package.json
14536
15455
  var package_default6 = {
14537
15456
  name: "@huynle/hub-codex",
14538
- version: "0.8.7",
15457
+ version: "0.9.1",
14539
15458
  private: true,
14540
15459
  description: "The Codex app-server spoke: attaches to Codex's app-server (its own over stdio, and the shared daemon's socket when present), and the `hub codex` wrapper (internal; shipped inside @huynle/hub, not published on its own).",
14541
15460
  license: "MIT",
@@ -14735,7 +15654,7 @@ async function openAppServer(channel, options) {
14735
15654
 
14736
15655
  // packages/codex/src/profile.ts
14737
15656
  import { randomBytes as randomBytes11 } from "node:crypto";
14738
- import { existsSync as existsSync8, mkdirSync as mkdirSync11, readdirSync as readdirSync2, renameSync as renameSync6, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "node:fs";
15657
+ import { existsSync as existsSync8, mkdirSync as mkdirSync11, readdirSync as readdirSync2, renameSync as renameSync7, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "node:fs";
14739
15658
  import path28 from "node:path";
14740
15659
  var CODEX_PROFILE_CAPABILITIES = Object.freeze({
14741
15660
  instructions: true,
@@ -14829,7 +15748,7 @@ class CodexProfiles {
14829
15748
  mkdirSync11(path28.join(temp, "skills", skill.name), { recursive: true, mode: 448 });
14830
15749
  writeFileSync7(path28.join(temp, "skills", skill.name, "SKILL.md"), skillFile(skill), { mode: 384 });
14831
15750
  }
14832
- renameSync6(temp, versionDir);
15751
+ renameSync7(temp, versionDir);
14833
15752
  } catch (cause) {
14834
15753
  rmSync10(temp, { recursive: true, force: true });
14835
15754
  throw cause;
@@ -14863,7 +15782,7 @@ class CodexProfiles {
14863
15782
  }
14864
15783
 
14865
15784
  // packages/codex/src/sessions.ts
14866
- import { statSync as statSync5 } from "node:fs";
15785
+ import { statSync as statSync6 } from "node:fs";
14867
15786
  import path29 from "node:path";
14868
15787
  import { randomBytes as randomBytes13 } from "node:crypto";
14869
15788
 
@@ -14893,7 +15812,7 @@ var textInput = (text) => ({ type: "text", text, text_elements: [] });
14893
15812
  function gitRoot2(cwd) {
14894
15813
  for (let dir = path29.resolve(cwd);; dir = path29.dirname(dir)) {
14895
15814
  try {
14896
- statSync5(path29.join(dir, ".git"));
15815
+ statSync6(path29.join(dir, ".git"));
14897
15816
  return dir;
14898
15817
  } catch {}
14899
15818
  if (path29.dirname(dir) === dir)
@@ -15161,7 +16080,7 @@ class CodexSessions {
15161
16080
  throw new Error(`The directory must be an absolute path (got ${JSON.stringify(req.directory)})`);
15162
16081
  let isDir = false;
15163
16082
  try {
15164
- isDir = statSync5(req.directory).isDirectory();
16083
+ isDir = statSync6(req.directory).isDirectory();
15165
16084
  } catch {}
15166
16085
  if (!isDir)
15167
16086
  throw new Error(`${req.directory} does not exist or is not a directory`);
@@ -15344,7 +16263,7 @@ function input(text, images) {
15344
16263
  }
15345
16264
 
15346
16265
  // packages/codex/src/stdio.ts
15347
- import { spawn as spawn5 } from "node:child_process";
16266
+ import { spawn as spawn6 } from "node:child_process";
15348
16267
  var STDIO_MAX_LINE = 16 * 1024 * 1024;
15349
16268
  var KILL_GRACE_MS2 = 2000;
15350
16269
  var LOG_LINE_MAX = 2000;
@@ -15360,7 +16279,7 @@ function spawnStdioChannel(options) {
15360
16279
  cb(reason);
15361
16280
  };
15362
16281
  const env = Object.fromEntries(Object.entries(options.env).filter(([, v]) => v !== undefined));
15363
- const child = spawn5(options.command, [...options.args], { cwd: options.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
16282
+ const child = spawn6(options.command, [...options.args], { cwd: options.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
15364
16283
  let exitDone;
15365
16284
  const exited = new Promise((resolve) => exitDone = resolve);
15366
16285
  child.on("error", (error) => {
@@ -16021,7 +16940,7 @@ function spokeServiceStatusText(name, status) {
16021
16940
  }
16022
16941
 
16023
16942
  // packages/codex/src/wrapper.ts
16024
- import { spawn as spawn6 } from "node:child_process";
16943
+ import { spawn as spawn7 } from "node:child_process";
16025
16944
  import { closeSync as closeSync7, openSync as openSync7 } from "node:fs";
16026
16945
  import path32 from "node:path";
16027
16946
  var SERVICE_START_MS = 8000;
@@ -16031,7 +16950,7 @@ function spawnService(io) {
16031
16950
  ensurePrivateDir2(dir);
16032
16951
  const log = openSync7(path32.join(dir, "spoke.log"), "a", 384);
16033
16952
  try {
16034
- const child = spawn6(io.program.execPath, [io.program.entry, "codex-spoke", "serve"], { detached: true, stdio: ["ignore", log, log], env: { ...io.env, BUN_BE_BUN: "1" } });
16953
+ const child = spawn7(io.program.execPath, [io.program.entry, "codex-spoke", "serve"], { detached: true, windowsHide: true, stdio: ["ignore", log, log], env: { ...io.env, BUN_BE_BUN: "1" } });
16035
16954
  child.on("error", () => {});
16036
16955
  child.unref();
16037
16956
  } finally {
@@ -16060,7 +16979,7 @@ async function ensureService(io) {
16060
16979
  var SIGNALS2 = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGTERM: 15 };
16061
16980
  function spawnCodex(bin, args, env, stderr) {
16062
16981
  return new Promise((resolve) => {
16063
- const child = spawn6(bin, args, { stdio: "inherit", env });
16982
+ const child = spawn7(bin, args, { stdio: "inherit", windowsHide: true, env });
16064
16983
  const ignore = () => {};
16065
16984
  const forward = () => child.kill("SIGTERM");
16066
16985
  process.on("SIGINT", ignore);
@@ -16094,7 +17013,7 @@ async function runCodex(args, given) {
16094
17013
  }
16095
17014
 
16096
17015
  // packages/cli/src/setup/commands.ts
16097
- import { existsSync as existsSync9, readFileSync as readFileSync13, renameSync as renameSync8, writeFileSync as writeFileSync8 } from "node:fs";
17016
+ import { existsSync as existsSync9, readFileSync as readFileSync13, renameSync as renameSync9, writeFileSync as writeFileSync8 } from "node:fs";
16098
17017
  import { hostname as hostname4 } from "node:os";
16099
17018
  import path35 from "node:path";
16100
17019
 
@@ -18019,44 +18938,9 @@ function renderYAML(vars) {
18019
18938
  }
18020
18939
 
18021
18940
  // packages/cli/src/bootstrap/discovery/node-probe.ts
18022
- import childProcess from "node:child_process";
18023
- import fs from "node:fs";
18024
- import os from "node:os";
18025
-
18026
- // packages/cli/src/bootstrap/discovery/util.ts
18027
- function normalizeAbs(value) {
18028
- if (typeof value !== "string")
18029
- return;
18030
- let p = value;
18031
- if (/^[A-Za-z]:[\\/]/.test(p))
18032
- p = p[0].toUpperCase() + p.slice(1).replace(/\\/g, "/");
18033
- else if (!p.startsWith("/") || p.startsWith("//"))
18034
- return;
18035
- p = p.replace(/\/{2,}/g, "/");
18036
- if (p.endsWith("/") && p !== "/" && !/^[A-Z]:\/$/.test(p))
18037
- p = p.slice(0, -1);
18038
- return isAbsolutePath(p) ? p : undefined;
18039
- }
18040
- function canonicalWinLink(text) {
18041
- const stripped = text.replace(/^(\\\\\?\\|\\\?\?\\)/, "");
18042
- return normalizeAbs(stripped) ?? text;
18043
- }
18044
- var probePath = (platform, text) => text !== undefined && platform === "win32" ? canonicalWinLink(text) : text;
18045
- var join = (dir, rel) => dir.endsWith("/") ? `${dir}${rel}` : `${dir}/${rel}`;
18046
- var decode = (bytes) => bytes ? new TextDecoder("utf-8", { fatal: false }).decode(bytes) : "";
18047
- var oneLine2 = (v, max = 64) => v && v.length <= max && !/[\u0000-\u001f\u007f]/.test(v) ? v : undefined;
18048
- var hostAbs = (platform, value) => {
18049
- const p = normalizeAbs(value);
18050
- return p && (platform !== "win32" || isWindowsPath(p)) ? p : undefined;
18051
- };
18052
- var underDir = (p, dir) => {
18053
- const prefix = dir.endsWith("/") ? dir : `${dir}/`;
18054
- return isWindowsPath(p) ? p.toLowerCase().startsWith(prefix.toLowerCase()) : p.startsWith(prefix);
18055
- };
18056
- var WINDOWS_COMMAND_EXTENSIONS = [".com", ".exe", ".bat", ".cmd"];
18057
- var isWindowsExecutable = (path) => WINDOWS_COMMAND_EXTENSIONS.some((e) => path.toLowerCase().endsWith(e));
18058
-
18059
- // packages/cli/src/bootstrap/discovery/node-probe.ts
18941
+ import childProcess from "node:child_process";
18942
+ import fs from "node:fs";
18943
+ import os from "node:os";
18060
18944
  var MAX_READ_BYTES = 16 * 1024 * 1024;
18061
18945
  var kindOf = (st) => st.isSymbolicLink() ? "symlink" : st.isDirectory() ? "directory" : st.isFile() ? "file" : "other";
18062
18946
  function attempt(f) {
@@ -18155,6 +19039,10 @@ function createNodeProbe(options = {}) {
18155
19039
  }
18156
19040
  // packages/cli/src/bootstrap/profiles/desired.ts
18157
19041
  var NO_SERVICES_ON_TARGET = "needs user services, which hubctl does not install on Windows yet";
19042
+ var NO_SERVICE_ON_TARGET = "needs a user service hubctl does not install on Windows yet";
19043
+ function servicesWithoutTemplate(profile, manager, at = "/profile/services") {
19044
+ return profile.services.flatMap((service, i) => EMBEDDED_ASSETS[`service/${manager}/${service}`] ? [] : [{ path: `${at}/${i}`, code: "unsupported_platform", message: `${service} ${NO_SERVICE_ON_TARGET}` }]);
19045
+ }
18158
19046
 
18159
19047
  // packages/cli/src/bootstrap/preview/plan.ts
18160
19048
  var PHASE = {
@@ -18202,6 +19090,11 @@ function planSetup(input) {
18202
19090
  return failed(issuesAt("/snapshot/host/target", target.ok ? fail("", "unsupported_platform", "unsupported host") : target));
18203
19091
  if (profile.services.length > 0 && target.value.serviceManager === "none")
18204
19092
  return fail("/profile/services", "unsupported_platform", `${profile.id} ${NO_SERVICES_ON_TARGET}`);
19093
+ if (target.value.serviceManager !== "none") {
19094
+ const missing = servicesWithoutTemplate(profile, target.value.serviceManager);
19095
+ if (missing.length > 0)
19096
+ return failed(missing);
19097
+ }
18205
19098
  if (profile.services.length > 0 && snapshot.managers.service === "none")
18206
19099
  return fail("/profile/services", "no_service_manager", `the ${profile.id} profile installs services, but this host has no user service manager (launchd or systemd --user)`);
18207
19100
  const planInput = { snapshot, profile };
@@ -18499,6 +19392,8 @@ function discoverRoots(probe) {
18499
19392
  paths["launch-agents"] = join(home, "Library/LaunchAgents");
18500
19393
  if (probe.platform === "linux")
18501
19394
  paths["systemd-user"] = join(config, "systemd/user");
19395
+ if (windows)
19396
+ paths["task-defs"] = join(paths.state, "hub/tasks");
18502
19397
  return ROOT_IDS.flatMap((id) => paths[id] ? [{ id, path: paths[id] }] : []);
18503
19398
  }
18504
19399
  function hubStateRoot(probe, home) {
@@ -18550,17 +19445,17 @@ function discoverLibc(probe) {
18550
19445
  function discoverOS(probe) {
18551
19446
  if (probe.platform === "darwin") {
18552
19447
  const plist = decode(probe.readFile("/System/Library/CoreServices/SystemVersion.plist", 64 * 1024));
18553
- const value = (key) => oneLine2(new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`).exec(plist)?.[1]);
19448
+ const value = (key) => oneLine3(new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`).exec(plist)?.[1]);
18554
19449
  return { name: value("ProductName") ?? "macOS", version: value("ProductVersion") ?? "unknown" };
18555
19450
  }
18556
19451
  if (probe.platform === "linux") {
18557
19452
  const text = decode(probe.readFile("/etc/os-release", 64 * 1024) ?? probe.readFile("/usr/lib/os-release", 64 * 1024));
18558
- const value = (key) => oneLine2(new RegExp(`^${key}=(.*)$`, "m").exec(text)?.[1]?.trim().replace(/^(["'])(.*)\1$/, "$2"));
19453
+ const value = (key) => oneLine3(new RegExp(`^${key}=(.*)$`, "m").exec(text)?.[1]?.trim().replace(/^(["'])(.*)\1$/, "$2"));
18559
19454
  return { name: value("NAME") ?? "Linux", version: value("VERSION_ID") ?? "unknown" };
18560
19455
  }
18561
19456
  if (probe.platform === "win32")
18562
- return { name: oneLine2(probe.osInfo?.name) ?? "Windows", version: oneLine2(probe.osInfo?.version) ?? "unknown" };
18563
- return { name: oneLine2(probe.platform, 32) ?? "unknown", version: "unknown" };
19457
+ return { name: oneLine3(probe.osInfo?.name) ?? "Windows", version: oneLine3(probe.osInfo?.version) ?? "unknown" };
19458
+ return { name: oneLine3(probe.platform, 32) ?? "unknown", version: "unknown" };
18564
19459
  }
18565
19460
  function discoverContainer(probe) {
18566
19461
  if (probe.platform !== "linux")
@@ -18610,10 +19505,13 @@ function configCandidates(platform, roots, context = "host") {
18610
19505
  add("launch-agents", asset.installAs, "hub", "plist");
18611
19506
  if (platform === "linux" && asset.manager === "systemd-user")
18612
19507
  add("systemd-user", asset.installAs, "hub", "unit");
19508
+ if (platform === "win32" && asset.manager === "task-scheduler")
19509
+ add("task-defs", asset.installAs, "hub", "xml");
18613
19510
  }
18614
19511
  return list.filter((c) => c !== undefined);
18615
19512
  }
18616
19513
  var HARNESS_BIN_DIR = { opencode: "/bin", "claude-code": "", codex: "/bin" };
19514
+ var systemRootOf2 = (probe) => hostAbs("win32", probe.env.SystemRoot ?? probe.env.SYSTEMROOT) ?? "C:/Windows";
18617
19515
  function discover(probe, options) {
18618
19516
  let roots = discoverRoots(probe);
18619
19517
  if (!roots)
@@ -18635,6 +19533,8 @@ function discover(probe, options) {
18635
19533
  service = "launchd";
18636
19534
  if (probe.platform === "linux" && probe.lstat("/run/systemd/system") === "directory" && which(probe, [...dirs, "/usr/bin", "/bin"], "systemctl"))
18637
19535
  service = "systemd-user";
19536
+ if (probe.platform === "win32" && probe.executable(join(systemRootOf2(probe), "System32/schtasks.exe")))
19537
+ service = "task-scheduler";
18638
19538
  const shims = [];
18639
19539
  const harnesses = HARNESS_ORDER.map((id) => {
18640
19540
  const name = HARNESS_BINARIES[id];
@@ -19032,7 +19932,7 @@ function installedHarnesses(history) {
19032
19932
  }
19033
19933
  function installedServiceUnits(history, manager) {
19034
19934
  const byFile = new Map(Object.values(EMBEDDED_ASSETS).filter((a) => a.manager === manager).map((a) => [a.installAs, a.service]));
19035
- const root = manager === "launchd" ? "launch-agents" : "systemd-user";
19935
+ const root = SERVICE_ROOT[manager];
19036
19936
  const units = {};
19037
19937
  for (const tx of history) {
19038
19938
  if (tx.outcome !== "committed")
@@ -19153,7 +20053,7 @@ import {
19153
20053
  readFileSync as readFileSync12,
19154
20054
  readlinkSync as readlinkSync2,
19155
20055
  readSync as readSync2,
19156
- renameSync as renameSync7,
20056
+ renameSync as renameSync8,
19157
20057
  rmdirSync,
19158
20058
  symlinkSync as symlinkSync2,
19159
20059
  unlinkSync as unlinkSync2,
@@ -19264,7 +20164,7 @@ var posixFs = {
19264
20164
  abort: () => finish(false)
19265
20165
  };
19266
20166
  },
19267
- rename: (from, to) => renameSync7(from, to),
20167
+ rename: (from, to) => renameSync8(from, to),
19268
20168
  link: (from, to) => linkSync(from, to),
19269
20169
  unlink: (path) => unlinkSync2(path),
19270
20170
  mkdir: (path, mode) => mkdirSync12(path, { mode }),
@@ -20355,208 +21255,6 @@ import { closeSync as closeSync9, mkdtempSync as mkdtempSync3, openSync as openS
20355
21255
  import { tmpdir as tmpdir3 } from "node:os";
20356
21256
  import { join as join2 } from "node:path";
20357
21257
 
20358
- // packages/cli/src/bootstrap/adapters/process-tree.ts
20359
- import { execFile as execFile2, spawn as nodeSpawn } from "node:child_process";
20360
- var TOOL_TIMEOUT_MS = 15000;
20361
- var START_SLACK_MS = 2000;
20362
- var FILETIME_EPOCH_MS = 11644473600000n;
20363
- var defaultExec = (file, args) => new Promise((resolve) => {
20364
- execFile2(file, [...args], { shell: false, windowsHide: true, timeout: TOOL_TIMEOUT_MS, encoding: "utf8", maxBuffer: 1 << 20 }, (error, stdout) => {
20365
- const code = error?.code;
20366
- resolve({ code: error ? typeof code === "number" ? code : null : 0, stdout: String(stdout ?? "") });
20367
- });
20368
- });
20369
- function systemRootOf(value) {
20370
- const v = value?.replace(/\\+$/, "");
20371
- return v && /^[A-Za-z]:\\[A-Za-z0-9 ._\\()-]*$/.test(v) ? v : "C:\\Windows";
20372
- }
20373
- function processTree(deps = {}) {
20374
- const platform = deps.platform ?? process.platform;
20375
- if (platform === "win32") {
20376
- const exec = deps.exec ?? defaultExec;
20377
- const root = systemRootOf(deps.systemRoot ?? process.env.SystemRoot ?? process.env.SYSTEMROOT);
20378
- const taskkill = `${root}\\System32\\taskkill.exe`;
20379
- const powershell = `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
20380
- const killTree = async (pid) => {
20381
- if (!Number.isInteger(pid) || pid <= 0)
20382
- return;
20383
- try {
20384
- await exec(taskkill, ["/T", "/F", "/PID", String(pid)]);
20385
- } catch {}
20386
- };
20387
- return {
20388
- detached: false,
20389
- killTree,
20390
- async reap(pid, startedAt) {
20391
- if (!Number.isInteger(pid) || pid <= 0)
20392
- return;
20393
- const since = (BigInt(Math.max(0, Math.floor(startedAt) - START_SLACK_MS)) + FILETIME_EPOCH_MS) * 10000n;
20394
- const script = `$t=[DateTime]::FromFileTimeUtc(${since}); ` + `Get-CimInstance -ClassName Win32_Process -Filter 'ParentProcessId=${pid}' | ` + `Where-Object { $_.CreationDate.ToUniversalTime() -ge $t } | ForEach-Object { $_.ProcessId }`;
20395
- let stdout = "";
20396
- try {
20397
- stdout = (await exec(powershell, ["-NoProfile", "-NonInteractive", "-Command", script])).stdout;
20398
- } catch {
20399
- return;
20400
- }
20401
- for (const line of stdout.split(/\r?\n/)) {
20402
- const child = line.trim();
20403
- if (/^[1-9]\d*$/.test(child))
20404
- await killTree(Number(child));
20405
- }
20406
- }
20407
- };
20408
- }
20409
- const kill = deps.kill ?? ((pid, signal) => void process.kill(pid, signal));
20410
- const group = (pid) => {
20411
- try {
20412
- kill(-pid, "SIGKILL");
20413
- return true;
20414
- } catch {
20415
- return false;
20416
- }
20417
- };
20418
- return {
20419
- detached: true,
20420
- async killTree(pid) {
20421
- if (!Number.isInteger(pid) || pid <= 0)
20422
- return;
20423
- if (group(pid))
20424
- return;
20425
- try {
20426
- kill(pid, "SIGKILL");
20427
- } catch {}
20428
- },
20429
- async reap(pid) {
20430
- if (Number.isInteger(pid) && pid > 0)
20431
- group(pid);
20432
- }
20433
- };
20434
- }
20435
- var DRAIN_GRACE_MS = 1000;
20436
- function runBounded(request, options = {}) {
20437
- const tree = options.tree ?? processTree();
20438
- const spawn = options.spawn ?? nodeSpawn;
20439
- const now = options.now ?? Date.now;
20440
- const env = Object.fromEntries(Object.entries(request.env).filter((e) => typeof e[1] === "string"));
20441
- return new Promise((resolve) => {
20442
- let timedOut = false;
20443
- let aborted = false;
20444
- let overflowed = false;
20445
- let ended = false;
20446
- const out = [];
20447
- const err = [];
20448
- const size = { out: 0, err: 0 };
20449
- const result = (exitCode, spawnError) => ({
20450
- exitCode,
20451
- stdout: Buffer.concat(out).toString("utf8"),
20452
- stderr: Buffer.concat(err).toString("utf8"),
20453
- timedOut,
20454
- aborted,
20455
- overflowed,
20456
- ...spawnError ? { spawnError } : {}
20457
- });
20458
- if (request.signal?.aborted) {
20459
- aborted = true;
20460
- return resolve(result(null));
20461
- }
20462
- const startedAt = now();
20463
- let child;
20464
- try {
20465
- child = spawn(request.file, [...request.args], {
20466
- shell: false,
20467
- env,
20468
- stdio: ["ignore", "pipe", request.stderr ? "pipe" : "ignore"],
20469
- windowsHide: true,
20470
- detached: tree.detached
20471
- });
20472
- } catch (error) {
20473
- return resolve(result(null, error));
20474
- }
20475
- let settled = false;
20476
- let exited = false;
20477
- const pid = child.pid;
20478
- const end = () => {
20479
- if (ended || pid === undefined)
20480
- return;
20481
- ended = true;
20482
- tree.killTree(pid);
20483
- };
20484
- const timer = setTimeout(() => {
20485
- timedOut = true;
20486
- end();
20487
- }, request.timeoutMs);
20488
- const onAbort = () => {
20489
- aborted = true;
20490
- end();
20491
- };
20492
- request.signal?.addEventListener("abort", onAbort, { once: true });
20493
- const finish = (r) => {
20494
- if (settled)
20495
- return;
20496
- settled = true;
20497
- clearTimeout(timer);
20498
- request.signal?.removeEventListener("abort", onAbort);
20499
- resolve(r);
20500
- };
20501
- const collect = (into, key) => (chunk) => {
20502
- const room = request.maxOutputBytes - size[key];
20503
- if (chunk.length > room) {
20504
- overflowed = true;
20505
- if (room > 0)
20506
- into.push(chunk.subarray(0, room));
20507
- size[key] = request.maxOutputBytes;
20508
- if ((request.overflow ?? "kill") === "kill")
20509
- end();
20510
- return;
20511
- }
20512
- into.push(chunk);
20513
- size[key] += chunk.length;
20514
- };
20515
- child.stdout?.on("data", collect(out, "out"));
20516
- child.stderr?.on("data", collect(err, "err"));
20517
- const streams = [child.stdout, child.stderr].filter((s) => Boolean(s));
20518
- const drained = () => new Promise((done) => {
20519
- let open = streams.filter((s) => !s.readableEnded && !s.destroyed).length;
20520
- if (open === 0)
20521
- return done();
20522
- const grace = setTimeout(() => {
20523
- for (const s of streams)
20524
- s.destroy();
20525
- done();
20526
- }, DRAIN_GRACE_MS);
20527
- for (const s of streams) {
20528
- if (s.readableEnded || s.destroyed)
20529
- continue;
20530
- const one = () => {
20531
- if (--open === 0) {
20532
- clearTimeout(grace);
20533
- done();
20534
- }
20535
- };
20536
- s.once("end", one);
20537
- s.once("close", () => s.readableEnded ? undefined : one());
20538
- }
20539
- });
20540
- child.on("error", (error) => {
20541
- if (!exited)
20542
- finish(result(null, error));
20543
- });
20544
- child.on("exit", (code) => {
20545
- if (exited)
20546
- return;
20547
- exited = true;
20548
- clearTimeout(timer);
20549
- const exitCode = typeof code === "number" && !ended ? code : null;
20550
- (async () => {
20551
- if (pid !== undefined)
20552
- await tree.reap(pid, startedAt);
20553
- await drained();
20554
- finish(result(exitCode));
20555
- })();
20556
- });
20557
- });
20558
- }
20559
-
20560
21258
  // packages/cli/src/bootstrap/adapters/claude/ports.ts
20561
21259
  class DownloadTooLarge extends Error {
20562
21260
  constructor(url, maxBytes) {
@@ -21282,7 +21980,7 @@ var OIDC_ISSUER_V2 = "1.3.6.1.4.1.57264.1.8";
21282
21980
  var OIDC_ISSUER_V1 = "1.3.6.1.4.1.57264.1.1";
21283
21981
  var isObj5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
21284
21982
  var b64 = (s) => typeof s === "string" && /^[A-Za-z0-9+/]*={0,2}$/.test(s) ? Buffer.from(s, "base64") : undefined;
21285
- var within3 = (t, v) => t >= v.start && (v.end === undefined || t <= v.end);
21983
+ var within4 = (t, v) => t >= v.start && (v.end === undefined || t <= v.end);
21286
21984
  function tlv(buf, at) {
21287
21985
  const tag = buf[at];
21288
21986
  let len = buf[at + 1];
@@ -21417,7 +22115,7 @@ function verifySigstoreBundle(bundle, artifact, options) {
21417
22115
  const t = b.payload.integratedTime * 1000;
21418
22116
  if (t < Date.parse(leaf.validFrom) || t > Date.parse(leaf.validTo))
21419
22117
  return failed2("the entry was logged outside the certificate's validity");
21420
- if (!within3(t, authority.validFor))
22118
+ if (!within4(t, authority.validFor))
21421
22119
  return failed2("the entry was logged outside the CA's validity");
21422
22120
  const sans = (leaf.subjectAltName ?? "").split(/,\s*/);
21423
22121
  if (!sans.includes(`URI:${identity.san}`))
@@ -21437,7 +22135,7 @@ function verifySigstoreBundle(bundle, artifact, options) {
21437
22135
  const log = trust.logs.find((l) => l.logID === b.payload.logID.toLowerCase());
21438
22136
  if (!log)
21439
22137
  return unavailable("the entry is in a transparency log this hubctl does not trust");
21440
- if (!within3(t, log.validFor))
22138
+ if (!within4(t, log.validFor))
21441
22139
  return failed2("the entry was logged outside the log key's validity");
21442
22140
  if (!verifySET(log, b))
21443
22141
  return failed2("the transparency log's signed entry timestamp does not verify");
@@ -21574,13 +22272,50 @@ var labelOfTarget = (word) => {
21574
22272
  const at = word?.lastIndexOf("/") ?? -1;
21575
22273
  return at > 0 && DOMAIN.test(word.slice(0, at)) ? word.slice(at + 1) : undefined;
21576
22274
  };
22275
+ var TASK_FILES = new Set(Object.values(EMBEDDED_ASSETS).flatMap((a) => a.manager === "task-scheduler" ? [a.installAs] : []));
22276
+ var TASK_FILE = /^[A-Z]:\\(?:[^\\/:*?"<>|]+\\)*([^\\/:*?"<>|]+)$/;
22277
+ function taskQueryWords(name) {
22278
+ if (!TASK_NAME.test(name))
22279
+ throw new Error(`not a hub task name: ${JSON.stringify(name)}`);
22280
+ const script = "$ErrorActionPreference='Stop';$s=New-Object -ComObject Schedule.Service;$s.Connect();" + `$t=$s.GetFolder('\\').GetTask('${name}');` + "'state='+[int]$t.State;'enabled='+$t.Enabled;'result='+$t.LastTaskResult;" + "foreach($r in $t.GetInstances(0)){'pid='+$r.EnginePID}";
22281
+ return ["-NoProfile", "-NonInteractive", "-Command", script];
22282
+ }
22283
+ function checkWindowsCall(program, words) {
22284
+ if (program === "taskkill")
22285
+ return words.length === 4 && words[0] === "/T" && words[1] === "/F" && words[2] === "/PID" && /^[1-9][0-9]{0,9}$/.test(words[3]);
22286
+ if (program === "powershell") {
22287
+ const name = /GetTask\('([^']*)'\)/.exec(words[words.length - 1] ?? "")?.[1];
22288
+ if (!name || !TASK_NAME.test(name))
22289
+ return false;
22290
+ const expected = taskQueryWords(name);
22291
+ return words.length === expected.length && words.every((w, i) => w === expected[i]);
22292
+ }
22293
+ const [verb, tn, name] = words;
22294
+ if (tn !== "/TN" || !TASK_NAME.test(name ?? ""))
22295
+ return false;
22296
+ if (verb === "/Run" || verb === "/End")
22297
+ return words.length === 3;
22298
+ if (verb === "/Delete")
22299
+ return words.length === 4 && words[3] === "/F";
22300
+ if (verb === "/Query")
22301
+ return words.length === 4 && words[3] === "/XML";
22302
+ if (verb === "/Create")
22303
+ return words.length === 6 && words[3] === "/XML" && words[5] === "/F" && TASK_FILES.has(TASK_FILE.exec(words[4])?.[1] ?? "");
22304
+ return false;
22305
+ }
22306
+ var WINDOWS_PROGRAMS = new Set(["schtasks", "powershell", "taskkill"]);
21577
22307
  function checkManagerCall(call) {
21578
- if (!isRecord2(call) || !Array.isArray(call.words) || call.words.length > 4)
22308
+ if (!isRecord2(call) || !Array.isArray(call.words))
22309
+ return false;
22310
+ const windows = typeof call.program === "string" && WINDOWS_PROGRAMS.has(call.program);
22311
+ if (call.words.length > (windows ? 6 : 4))
21579
22312
  return false;
21580
22313
  const w = call.words;
21581
22314
  if (!w.every((x) => typeof x === "string" && x.length > 0 && x.length <= 4096 && !CONTROL5.test(x)))
21582
22315
  return false;
21583
22316
  const words = w;
22317
+ if (windows)
22318
+ return checkWindowsCall(call.program, words);
21584
22319
  if (call.program === "systemctl") {
21585
22320
  if (words[0] !== "--user")
21586
22321
  return false;
@@ -21614,6 +22349,24 @@ function unitPathResolver(roots, context) {
21614
22349
  return where.ok ? where.value : undefined;
21615
22350
  };
21616
22351
  }
22352
+ var taskFields = (output) => showFields(output.replaceAll("\r", ""));
22353
+ var taskPids = (output) => output.replaceAll("\r", "").split(`
22354
+ `).flatMap((line) => /^pid=([1-9][0-9]{0,9})$/.exec(line.trim()) ? [Number(line.trim().slice(4))] : []);
22355
+ function taskSchedulerHealth(output) {
22356
+ const fields = taskFields(output);
22357
+ const state = /^[0-4]$/.test(fields.state ?? "") ? Number(fields.state) : undefined;
22358
+ const result = /^-?[0-9]{1,10}$/.test(fields.result ?? "") ? Number(fields.result) >>> 0 : undefined;
22359
+ const pid = taskPids(output)[0];
22360
+ if (state === 4)
22361
+ return { state: "running", restarts: 0, detail: pid ? `running, pid ${pid}` : "running" };
22362
+ if (state === 2)
22363
+ return { state: "starting", restarts: 0, detail: "queued" };
22364
+ if (state === 1)
22365
+ return { state: "stopped", restarts: 0, detail: "disabled" };
22366
+ if (state === 3)
22367
+ return { state: result === 267011 ? "starting" : "stopped", restarts: 0, detail: `ready, last result 0x${(result ?? 0).toString(16)}` };
22368
+ return { state: "stopped", restarts: 0, detail: "unknown" };
22369
+ }
21617
22370
  var ok4 = txOk(undefined);
21618
22371
  function showFields(output) {
21619
22372
  const fields = {};
@@ -21775,20 +22528,84 @@ function createServiceControl(options) {
21775
22528
  return await call("launchctl", "print", `${domain}/${label}`) === 0 ? txFail("internal", `${label} is still loaded after launchctl bootout`) : ok4;
21776
22529
  }
21777
22530
  };
21778
- const deactivate = (service, manager) => guarded(() => manager === "launchd" ? launchd.deactivate(service) : systemd.restore(service, { loaded: false, enabled: false }));
22531
+ const taskOf = (service) => {
22532
+ const name = options.taskName?.(service);
22533
+ return name && TASK_NAME.test(name) ? name : undefined;
22534
+ };
22535
+ const noTask = (service) => txFail("invalid", `this hubctl has no Task Scheduler task name for the ${service} service`);
22536
+ const tasks = {
22537
+ async query(name) {
22538
+ const r = await options.run(checked("powershell", ...taskQueryWords(name)));
22539
+ return r.exitCode === 0 ? r.output ?? "" : undefined;
22540
+ },
22541
+ async endTrees(name) {
22542
+ for (const pid of taskPids(await tasks.query(name) ?? ""))
22543
+ await call("taskkill", "/T", "/F", "/PID", String(pid));
22544
+ },
22545
+ async state(service) {
22546
+ const name = taskOf(service);
22547
+ if (!name)
22548
+ return noTask(service);
22549
+ const r = await options.run(checked("schtasks", "/Query", "/TN", name, "/XML"));
22550
+ if (r.exitCode !== 0)
22551
+ return txOk({ loaded: false, enabled: false });
22552
+ const enabled = /<Settings>[\s\S]*?<Enabled>(true|false)<\/Enabled>/.exec(r.output ?? "")?.[1] !== "false";
22553
+ return txOk({ loaded: true, enabled });
22554
+ },
22555
+ async activate(service) {
22556
+ const name = taskOf(service);
22557
+ if (!name)
22558
+ return noTask(service);
22559
+ const file = options.unitPath(service, "task-scheduler");
22560
+ if (!file)
22561
+ return txFail("invalid", `this host has no task-defs directory for ${name}`);
22562
+ await tasks.endTrees(name);
22563
+ const created = await call("schtasks", "/Create", "/TN", name, "/XML", toNativePath(file, "win32"), "/F");
22564
+ if (created !== 0)
22565
+ return txFail("internal", `schtasks /Create of ${name} failed (exit ${created})`);
22566
+ const run = await call("schtasks", "/Run", "/TN", name);
22567
+ if (run !== 0)
22568
+ return txFail("internal", `schtasks /Run of ${name} failed (exit ${run})`);
22569
+ return settle(() => tasks.health(service), name, `schtasks /Query /TN ${name} /V /FO LIST and the Task Scheduler history`);
22570
+ },
22571
+ async health(service) {
22572
+ const name = taskOf(service);
22573
+ if (!name)
22574
+ return noTask(service);
22575
+ const output = await tasks.query(name);
22576
+ return txOk(output === undefined ? { state: "stopped", restarts: 0, detail: "not registered" } : taskSchedulerHealth(output));
22577
+ },
22578
+ async deactivate(service) {
22579
+ const name = taskOf(service);
22580
+ if (!name)
22581
+ return noTask(service);
22582
+ await tasks.endTrees(name);
22583
+ await call("schtasks", "/End", "/TN", name);
22584
+ await call("schtasks", "/Delete", "/TN", name, "/F");
22585
+ return await call("schtasks", "/Query", "/TN", name, "/XML") === 0 ? txFail("internal", `${name} is still registered after schtasks /Delete`) : ok4;
22586
+ }
22587
+ };
22588
+ const by = (manager, calls) => guarded(() => {
22589
+ const f = Object.hasOwn(calls, manager) ? calls[manager] : undefined;
22590
+ if (!f)
22591
+ throw new Error(`unknown service manager ${String(manager)}`);
22592
+ return f();
22593
+ });
22594
+ const deactivate = (service, manager) => by(manager, { launchd: () => launchd.deactivate(service), "systemd-user": () => systemd.restore(service, { loaded: false, enabled: false }), "task-scheduler": () => tasks.deactivate(service) });
21779
22595
  return {
21780
- state: (service, manager) => guarded(() => manager === "launchd" ? launchd.state(service) : systemd.state(service)),
21781
- activate: (service, manager) => guarded(() => manager === "launchd" ? launchd.activate(service) : systemd.activate(service)),
22596
+ state: (service, manager) => by(manager, { launchd: () => launchd.state(service), "systemd-user": () => systemd.state(service), "task-scheduler": () => tasks.state(service) }),
22597
+ activate: (service, manager) => by(manager, { launchd: () => launchd.activate(service), "systemd-user": () => systemd.activate(service), "task-scheduler": () => tasks.activate(service) }),
21782
22598
  deactivate,
21783
- restore: (service, manager, prior) => prior.loaded ? guarded(async () => manager === "launchd" ? ok4 : systemd.restore(service, prior)) : deactivate(service, manager),
21784
- health: (service, manager) => guarded(() => manager === "launchd" ? launchd.health(service) : systemd.health(service))
22599
+ restore: (service, manager, prior) => prior.loaded ? by(manager, { launchd: async () => ok4, "systemd-user": () => systemd.restore(service, prior), "task-scheduler": async () => ok4 }) : deactivate(service, manager),
22600
+ health: (service, manager) => by(manager, { launchd: () => launchd.health(service), "systemd-user": () => systemd.health(service), "task-scheduler": () => tasks.health(service) })
21785
22601
  };
21786
22602
  }
21787
22603
  var MAX_OUTPUT2 = 64 * 1024;
21788
- var capturesOutput = (call) => call.program === "systemctl" ? call.words[1] === "show" : call.words[0] === "print";
22604
+ var capturesOutput = (call) => call.program === "systemctl" ? call.words[1] === "show" : call.program === "launchctl" ? call.words[0] === "print" : call.program === "powershell" || call.program === "schtasks" && call.words[0] === "/Query";
21789
22605
  var BUS_ENV = ["XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS"];
22606
+ var WINDOWS_ENV = ["SystemRoot", "windir", "SystemDrive", "TEMP", "TMP", "USERPROFILE", "LOCALAPPDATA", "APPDATA", "USERNAME", "USERDOMAIN"];
21790
22607
  var defaultSpawn = (file, words, options) => new Promise((resolve) => {
21791
- const child = nodeSpawn2(file, [...words], { shell: false, env: { ...options.env }, stdio: ["ignore", options.capture ? "pipe" : "ignore", "ignore"], timeout: options.timeoutMs, killSignal: "SIGKILL" });
22608
+ const child = nodeSpawn2(file, [...words], { shell: false, windowsHide: true, env: { ...options.env }, stdio: ["ignore", options.capture ? "pipe" : "ignore", "ignore"], timeout: options.timeoutMs, killSignal: "SIGKILL" });
21792
22609
  let output = "";
21793
22610
  child.stdout?.setEncoding("utf8");
21794
22611
  child.stdout?.on("data", (chunk) => {
@@ -21800,13 +22617,16 @@ var defaultSpawn = (file, words, options) => new Promise((resolve) => {
21800
22617
  child.on("close", (code, signal) => done(code ?? (signal ? 128 : 1)));
21801
22618
  });
21802
22619
  function spawnManagerRunner(options) {
21803
- for (const program of ["systemctl", "launchctl"]) {
22620
+ const given = Object.keys(options.programs);
22621
+ const windows = given.some((p) => WINDOWS_PROGRAMS.has(p));
22622
+ for (const program of windows ? given : ["systemctl", "launchctl"]) {
21804
22623
  const file = options.programs[program];
21805
- if (!isAbsolutePath(file) || path34.basename(file) !== program)
22624
+ const base = file === undefined ? "" : path34.basename(file);
22625
+ if (!isAbsolutePath(file) || base !== program && base.toLowerCase() !== `${program}.exe`)
21806
22626
  throw new Error(`${program} must be given as an absolute path to ${program}`);
21807
22627
  }
21808
22628
  const env = {};
21809
- for (const key of BUS_ENV)
22629
+ for (const key of windows ? WINDOWS_ENV : BUS_ENV)
21810
22630
  if (options.env?.[key])
21811
22631
  env[key] = options.env[key];
21812
22632
  const spawn = options.spawn ?? defaultSpawn;
@@ -21815,7 +22635,10 @@ function spawnManagerRunner(options) {
21815
22635
  if (!checkManagerCall(call))
21816
22636
  throw new Error(`refused a service manager call outside the fixed set (${String(call.program)})`);
21817
22637
  const capture = capturesOutput(call);
21818
- const r = await spawn(options.programs[call.program], [...call.words], { shell: false, env, timeoutMs, ...capture ? { capture: true } : {} });
22638
+ const file = options.programs[call.program];
22639
+ if (!file)
22640
+ throw new Error(`this host's service manager does not use ${call.program}`);
22641
+ const r = await spawn(file, [...call.words], { shell: false, env, timeoutMs, ...capture ? { capture: true } : {} });
21819
22642
  if (typeof r === "number")
21820
22643
  return { exitCode: r };
21821
22644
  return capture ? { exitCode: r.exitCode, output: r.output.slice(0, MAX_OUTPUT2) } : { exitCode: r.exitCode };
@@ -21994,8 +22817,12 @@ function launchSessionExecutor(probe) {
21994
22817
  var DEFAULT_SLOTS2 = 4;
21995
22818
  var BOOTSTRAP_SLOTS_MAX = 64;
21996
22819
  var RESERVED_LABELS = ["os", "arch"];
21997
- var inside = (path, dir) => path === dir || path.startsWith(dir === "/" ? "/" : `${dir}/`);
21998
- var parentOf3 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
22820
+ var folded = (p) => isWindowsPath(p) ? p.toLowerCase() : p;
22821
+ var inside = (path, dir) => {
22822
+ const [p, d] = [folded(path), folded(dir)];
22823
+ return p === d || p.startsWith(d.endsWith("/") ? d : `${d}/`);
22824
+ };
22825
+ var parentOf3 = (path) => folded(path.slice(0, path.lastIndexOf("/")) || "/");
21999
22826
  function otherViews(roots, context) {
22000
22827
  return roots.flatMap((r) => r.containerPath !== undefined && r.containerPath !== r.path ? [context === "container" ? r.path : r.containerPath] : []);
22001
22828
  }
@@ -22118,6 +22945,7 @@ function serviceContextOf(input) {
22118
22945
  // packages/cli/src/bootstrap/adapters/node-services/plan.ts
22119
22946
  var NODE_CONFIG_REF = { root: "config", rel: "hub/node.json" };
22120
22947
  var CONFIG_DIR = { root: "config", rel: "hub" };
22948
+ var UNIT_KIND = { launchd: "launchd agent", "systemd-user": "systemd --user unit", "task-scheduler": "logon task" };
22121
22949
  var LABEL3 = { node: "hub node", "claude-code": "Claude Code spoke", "codex-spoke": "Codex spoke" };
22122
22950
  var SERVICE_HARNESS = Object.fromEntries(Object.entries(HARNESS_SERVICE).map(([h, s]) => [s, h]));
22123
22951
  var BIN_SETTING = { "claude-code": "HUB_CLAUDE_BIN", codex: "HUB_CODEX_BIN" };
@@ -22194,9 +23022,19 @@ function planNodeServices(input, options) {
22194
23022
  }
22195
23023
  }
22196
23024
  const units = [];
23025
+ const uncarried = new Set;
22197
23026
  for (const service of profile.services) {
22198
- const environment = environmentFor(service, servedBy(service, profile.harnesses), ctx, options.environment);
22199
- const unit = renderServiceUnit({ service, manager: ctx.manager, layout: options.layout, home: ctx.home, environment });
23027
+ let environment = environmentFor(service, servedBy(service, profile.harnesses), ctx, options.environment);
23028
+ if (ctx.manager === "task-scheduler") {
23029
+ for (const key of Object.keys(environment))
23030
+ uncarried.add(key);
23031
+ environment = {};
23032
+ if (!options.task) {
23033
+ issues.push({ path: `/services/${service}/task`, code: "missing_field", message: "a Windows logon task needs its name and your SID; hubctl could not find them (USERNAME, whoami)" });
23034
+ continue;
23035
+ }
23036
+ }
23037
+ const unit = renderServiceUnit({ service, manager: ctx.manager, layout: options.layout, home: ctx.home, environment, ...ctx.manager === "task-scheduler" ? { task: options.task } : {} });
22200
23038
  if (!unit.ok) {
22201
23039
  issues.push(...prefixed(`/services/${service}`, unit));
22202
23040
  continue;
@@ -22206,7 +23044,7 @@ function planNodeServices(input, options) {
22206
23044
  id: `install-${service}-service`,
22207
23045
  kind: "install-service",
22208
23046
  escalation: "user",
22209
- summary: `Write the ${LABEL3[service]} ${ctx.manager === "launchd" ? "launchd agent" : "systemd --user unit"} (${unit.value.name})`,
23047
+ summary: `Write the ${LABEL3[service]} ${UNIT_KIND[ctx.manager]} (${unit.value.name})`,
22210
23048
  service,
22211
23049
  manager: ctx.manager,
22212
23050
  path: unit.value.path,
@@ -22238,7 +23076,7 @@ function planNodeServices(input, options) {
22238
23076
  }
22239
23077
  if (issues.length > 0)
22240
23078
  return failed3(issues);
22241
- return { ok: true, value: { actions, units, ...settings ? { settings } : {}, context: ctx, notes: notesFor(options, ctx) } };
23079
+ return { ok: true, value: { actions, units, ...settings ? { settings } : {}, context: ctx, notes: notesFor(options, ctx, [...uncarried].sort()) } };
22242
23080
  }
22243
23081
  function checkLaunch(input, options, ctx, settings) {
22244
23082
  const launch = options.launch;
@@ -22256,11 +23094,15 @@ function checkLaunch(input, options, ctx, settings) {
22256
23094
  return [{ path: "/launch/workspace", code: "invalid_path", message: `${where.value} is outside the node's workspace roots` }];
22257
23095
  return [];
22258
23096
  }
22259
- function notesFor(options, ctx) {
23097
+ function notesFor(options, ctx, uncarried) {
22260
23098
  const notes = [];
22261
23099
  if (!options.activate)
22262
23100
  notes.push("The services are installed but not loaded: pass --confirm-activation to load them (hubctl never loads a service without it).");
22263
- if (ctx.manager === "systemd-user")
23101
+ if (ctx.manager === "task-scheduler") {
23102
+ notes.push("The hub node runs as a logon task for you (no window, no administrator rights, no stored password): Windows starts it at each logon and again within a minute after it exits; it runs while you are logged in.");
23103
+ if (uncarried.length > 0)
23104
+ notes.push(`The logon task does not see ${uncarried.join(", ")} from this shell (a task has no environment of its own); set them as user environment variables if the node needs them.`);
23105
+ } else if (ctx.manager === "systemd-user")
22264
23106
  notes.push(options.linger ? "To keep the services running while you are logged out, run `loginctl enable-linger` yourself; hubctl does not change login settings." : "systemd --user services stop when you log out; keeping them running (linger) is opt-in.");
22265
23107
  else
22266
23108
  notes.push("launchd user agents run while you are logged in.");
@@ -22566,7 +23408,6 @@ function createOpencodeHarness(options) {
22566
23408
  }
22567
23409
 
22568
23410
  // packages/cli/src/bootstrap/profiles/upgrade.ts
22569
- var UNIT_ROOT2 = { launchd: "launch-agents", "systemd-user": "systemd-user" };
22570
23411
  function removeServiceActions(service, facts) {
22571
23412
  const actions = [
22572
23413
  { id: `deactivate-${service}`, kind: "deactivate-service", escalation: "user", summary: `Unload the hub ${service} service (profile downgrade)`, service, manager: facts.manager }
@@ -22581,7 +23422,7 @@ function removeServiceActions(service, facts) {
22581
23422
  summary: `Remove the hub ${service} ${facts.manager} unit (profile downgrade)`,
22582
23423
  service,
22583
23424
  manager: facts.manager,
22584
- path: { root: UNIT_ROOT2[facts.manager], rel: asset.installAs },
23425
+ path: { root: SERVICE_ROOT[facts.manager], rel: asset.installAs },
22585
23426
  sha256
22586
23427
  });
22587
23428
  return actions;
@@ -23784,6 +24625,10 @@ function codexSpokeStatus(options) {
23784
24625
  }
23785
24626
 
23786
24627
  // packages/cli/src/setup/runtime.ts
24628
+ function windowsServicePrograms(systemRoot) {
24629
+ const dir = `${systemRoot.replace(/\/+$/, "")}/System32`;
24630
+ return { schtasks: `${dir}/schtasks.exe`, powershell: `${dir}/WindowsPowerShell/v1.0/powershell.exe`, taskkill: `${dir}/taskkill.exe` };
24631
+ }
23787
24632
  function readinessGate(consent, harness) {
23788
24633
  return { requireReady: consent.requireReady === true || (consent.strict ?? []).includes(harness), ...consent.onReadiness ? { onReadiness: consent.onReadiness } : {} };
23789
24634
  }
@@ -23952,7 +24797,8 @@ function createSetupRuntime(deps) {
23952
24797
  ...choices.node ? { node: choices.node } : {},
23953
24798
  ...choices.activate ? { activate: true } : {},
23954
24799
  environment: deps.env,
23955
- ...choices.launch ? { launch: choices.launch } : {}
24800
+ ...choices.launch ? { launch: choices.launch } : {},
24801
+ ...input.snapshot.managers.service === "task-scheduler" && input.profile.services.length > 0 ? { task: taskIdentity() } : {}
23956
24802
  });
23957
24803
  if (!planned.ok)
23958
24804
  return planned;
@@ -23973,15 +24819,28 @@ function createSetupRuntime(deps) {
23973
24819
  ];
23974
24820
  return { sources: { adapters: registry, contributors, ...choices.enrollment ? { enrollment: choices.enrollment } : {} }, node: () => nodePlan, history: records };
23975
24821
  }
24822
+ function taskIdentity() {
24823
+ return { name: taskNameFor("node", deps.env.USERNAME ?? "", deps.env), userSid: (ports.userSid ?? (() => defaultWinAcl().sid()))() };
24824
+ }
23976
24825
  function control(snap) {
23977
24826
  if (snap.managers.service === "none")
23978
24827
  return;
24828
+ const tasks = snap.managers.service === "task-scheduler";
23979
24829
  let run = ports.serviceRunner;
23980
24830
  if (!run) {
23981
- const systemctl = which(deps.probe, [...pathDirs(deps.probe), "/usr/bin", "/bin"], "systemctl") ?? "/usr/bin/systemctl";
23982
- run = spawnManagerRunner({ programs: { launchctl: "/bin/launchctl", systemctl }, env: deps.env });
24831
+ if (tasks)
24832
+ run = spawnManagerRunner({ programs: windowsServicePrograms(systemRootOf2(deps.probe)), env: deps.env });
24833
+ else {
24834
+ const systemctl = which(deps.probe, [...pathDirs(deps.probe), "/usr/bin", "/bin"], "systemctl") ?? "/usr/bin/systemctl";
24835
+ run = spawnManagerRunner({ programs: { launchctl: "/bin/launchctl", systemctl }, env: deps.env });
24836
+ }
23983
24837
  }
23984
- return createServiceControl({ run, uid: ports.uid ?? deps.probe.uid, unitPath: unitPathResolver(snap.roots, executionContextOf(snap)) });
24838
+ return createServiceControl({
24839
+ run,
24840
+ uid: ports.uid ?? deps.probe.uid,
24841
+ unitPath: unitPathResolver(snap.roots, executionContextOf(snap)),
24842
+ ...tasks ? { taskName: (service) => taskNameFor(service, deps.env.USERNAME ?? "", deps.env) } : {}
24843
+ });
23985
24844
  }
23986
24845
  function hubctlSource() {
23987
24846
  const manifest = deps.manifest;
@@ -24276,14 +25135,14 @@ class BootstrapStateStore {
24276
25135
  }
24277
25136
 
24278
25137
  // packages/cli/src/setup/self-aside.ts
24279
- import { spawn as spawn7 } from "node:child_process";
24280
- import { readdirSync as readdirSync4, statSync as statSync6, unlinkSync as unlinkSync3 } from "node:fs";
25138
+ import { spawn as spawn8 } from "node:child_process";
25139
+ import { readdirSync as readdirSync4, statSync as statSync7, unlinkSync as unlinkSync3 } from "node:fs";
24281
25140
  var nodeSelfAsideIo = (env = process.env) => ({
24282
25141
  platform: process.platform,
24283
25142
  systemRoot: env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows",
24284
25143
  stat: (path) => {
24285
25144
  try {
24286
- return { ino: statSync6(path, { bigint: true }).ino };
25145
+ return { ino: statSync7(path, { bigint: true }).ino };
24287
25146
  } catch {
24288
25147
  return;
24289
25148
  }
@@ -24291,7 +25150,7 @@ var nodeSelfAsideIo = (env = process.env) => ({
24291
25150
  readdir: (path) => readdirSync4(path),
24292
25151
  unlink: (path) => unlinkSync3(path),
24293
25152
  spawnDetached: (file, line, cwd) => {
24294
- const child = spawn7(file, [line], { cwd, detached: true, stdio: "ignore", windowsHide: true, windowsVerbatimArguments: true });
25153
+ const child = spawn8(file, [line], { cwd, detached: true, stdio: "ignore", windowsHide: true, windowsVerbatimArguments: true });
24295
25154
  child.on("error", () => {});
24296
25155
  child.unref();
24297
25156
  }
@@ -24544,20 +25403,38 @@ function refFor(abs, snapshot) {
24544
25403
  for (const id of ["home"]) {
24545
25404
  const root = snapshot.roots.find((r) => r.id === id);
24546
25405
  const base = root ? rootPathIn(root, ctx) : undefined;
24547
- if (base && abs.startsWith(`${base}/`))
24548
- return { root: id, rel: abs.slice(base.length + 1) };
25406
+ if (base && underDir(abs, base))
25407
+ return { root: id, rel: abs.slice(base.length + (base.endsWith("/") ? 0 : 1)) };
24549
25408
  }
24550
25409
  return;
24551
25410
  }
25411
+ function workspaceRootOf(raw, where) {
25412
+ if (where.platform !== "win32")
25413
+ return path35.resolve(where.cwd, raw);
25414
+ if (/^[\\/]{2}/.test(raw))
25415
+ return;
25416
+ const home = normalizeAbs(where.home);
25417
+ const tilde = /^~(?:[\\/](.*))?$/.exec(raw);
25418
+ if (tilde)
25419
+ return home ? normalizeAbs(path35.win32.join(home, tilde[1] ?? "")) : undefined;
25420
+ const resolved = path35.win32.resolve(path35.win32.normalize(where.cwd), raw);
25421
+ return /^[\\/]{2}/.test(resolved) ? undefined : normalizeAbs(resolved);
25422
+ }
24552
25423
  function choicesOf(io, flags, profile, snapshot) {
24553
25424
  const enrollment = str(flags, "enrollment");
24554
25425
  if (enrollment !== undefined && !ENROLLMENT_MODES.includes(enrollment))
24555
25426
  throw new Refusal("usage", `--enrollment is ${ENROLLMENT_MODES.join(" or ")}`);
24556
- let roots = flags["workspace-root"]?.map((r) => path35.resolve(io.cwd ?? process.cwd(), r));
25427
+ const where = { platform: snapshot.host.platform, cwd: io.cwd ?? process.cwd(), home: io.home };
25428
+ let roots = flags["workspace-root"]?.map((r) => {
25429
+ const root = workspaceRootOf(r, where);
25430
+ if (root === undefined)
25431
+ throw new Refusal("usage", `--workspace-root ${r} is not a local absolute path (a drive path such as C:\\Users\\you\\projects, or ~/projects)`);
25432
+ return root;
25433
+ });
24557
25434
  if (!roots?.length && profile.services.includes("node")) {
24558
25435
  const existing = readNodeConfig(nodeConfigPath(io.env, io.home)).config.workspaceRoots;
24559
25436
  if (existing.length > 0)
24560
- roots = [...existing];
25437
+ roots = where.platform === "win32" ? existing.map((r) => workspaceRootOf(r, where) ?? r) : [...existing];
24561
25438
  }
24562
25439
  const launchHarness = str(flags, "verify-launch");
24563
25440
  let launch;
@@ -24677,7 +25554,7 @@ async function preview3(io, flags, json) {
24677
25554
  else {
24678
25555
  const tmp = `${file}.${process.pid}.tmp`;
24679
25556
  writeFileSync8(tmp, text, { mode: 384 });
24680
- renameSync8(tmp, file);
25557
+ renameSync9(tmp, file);
24681
25558
  }
24682
25559
  }
24683
25560
  const warnings = previewWarnings(snapshot, r.value.plan, bundle.node()?.notes ?? [], io.env);
@@ -25130,10 +26007,13 @@ Identity
25130
26007
  hub role release <role[.project]> [--session <addr>]
25131
26008
 
25132
26009
  Messaging (text "-" reads it from stdin)
25133
- hub msg <addr> "text" [--all] [--no-wake] [--urgent] tell: fire and forget
25134
- hub ask <addr> "text" [--wait [secs]] [--expires 2h] [--all] [--no-wake] [--urgent]
26010
+ hub msg <addr> "text" [--all] [--no-wake] [--urgent] [--not-after <ms|ISO time>] [--key K]
26011
+ tell: fire and forget; --not-after: never delivered after that time
26012
+ hub ask <addr> "text" [--wait [secs]] [--expires 2h] [--not-after <ms|ISO time>] [--all] [--no-wake] [--urgent] [--key K]
25135
26013
  tracked until answered, declined or expired; --wait alone waits until
25136
- it expires; exit 0 answered, 1 declined or no answer yet, 8 expired
26014
+ it expires; exit 0 answered, 1 declined or no answer yet, 8 expired;
26015
+ --key (Idempotency-Key): a retry with the same key and text answers
26016
+ the first send again and sends nothing (24 h, per credential)
25137
26017
  hub reply <msgID> "text" [--no-wake] goes to the real sender
25138
26018
  hub decline <msgID> --why "reason"
25139
26019
  hub ask @me "question" [--option id=label[:safe]]... [--recommend id [--why "…"]] [--urgency low|normal|high]
@@ -25168,9 +26048,10 @@ Launching (users; agents and app tokens with sessions:launch under a grant with
25168
26048
  [--name <@name>] [--role R]... [--harness H] [--agent A] [--model provider/model]
25169
26049
  [--runtime host|openshell [--profile P]] [--machine M] [--node-label k=v]... [--prefer]
25170
26050
  [--label k=v]... [--prompt "text" | --prompt -] [--end idle|asked|never] [--timeout 4h]
25171
- [--cleanup keep|remove] [--key K]
26051
+ [--cleanup keep|remove] [--key K] [--not-after <ms|ISO time>]
25172
26052
  start a session; the same --key again answers the same launch;
25173
- --label tags it for apps, --node-label picks nodes (--prefer: not required)
26053
+ --label tags it for apps, --node-label picks nodes (--prefer: not required);
26054
+ --not-after: still waiting for a machine then, it never starts (expired)
25174
26055
  hub launches list [--state S[,S]] [--label k=v]... your launches (agents: the ones they started)
25175
26056
  hub launches show <launchID> · hub launches stop <launchID> (stop: exit 8 when it already ended)
25176
26057
 
@@ -25299,7 +26180,16 @@ var whyOf = (flags) => {
25299
26180
  const why = str(flags, "why");
25300
26181
  return why !== undefined ? { why } : {};
25301
26182
  };
25302
- var sendFlags = (flags) => ({ ...flags["no-wake"] === true ? { wake: false } : {}, ...flags.urgent === true ? { urgent: true } : {} });
26183
+ var sendFlags = (flags) => {
26184
+ const notAfter = str(flags, "not-after");
26185
+ const key = str(flags, "key");
26186
+ return {
26187
+ ...flags["no-wake"] === true ? { wake: false } : {},
26188
+ ...flags.urgent === true ? { urgent: true } : {},
26189
+ ...notAfter === undefined ? {} : { notAfter: notAfterArg(notAfter, "--not-after") },
26190
+ ...key !== undefined ? { idempotencyKey: key } : {}
26191
+ };
26192
+ };
25303
26193
  var nodeIo = (io) => ({
25304
26194
  env: io.env,
25305
26195
  home: io.home,
@@ -25421,7 +26311,7 @@ var COMMANDS = {
25421
26311
  },
25422
26312
  msg: {
25423
26313
  args: ["<addr>", "<text>"],
25424
- flags: { all: "bool", "no-wake": "bool", urgent: "bool" },
26314
+ flags: { all: "bool", "no-wake": "bool", urgent: "bool", "not-after": "string", key: "string" },
25425
26315
  async handle(context) {
25426
26316
  const { flags, args } = context.parsed;
25427
26317
  const text = await textArg(args[1], context.io.stdin);
@@ -25433,7 +26323,7 @@ var COMMANDS = {
25433
26323
  },
25434
26324
  ask: {
25435
26325
  args: ["<addr>", "<text>"],
25436
- flags: { all: "bool", "no-wake": "bool", urgent: "bool", expires: "string", wait: "optional", ...DECISION_FLAGS },
26326
+ flags: { all: "bool", "no-wake": "bool", urgent: "bool", "not-after": "string", key: "string", expires: "string", wait: "optional", ...DECISION_FLAGS },
25437
26327
  async handle(context) {
25438
26328
  const { flags, args } = context.parsed;
25439
26329
  const decision = decisionOptions(flags, args[0]);