harnery 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/README.md +3 -3
  2. package/dist/commander.d.ts +16 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +18 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +95 -52
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +38 -11
  9. package/dist/commands/diagnostics.d.ts.map +1 -1
  10. package/dist/commands/diagnostics.js +10 -0
  11. package/dist/commands/doctor.d.ts +12 -0
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +22 -7
  14. package/dist/commands/governor.js +22 -10
  15. package/dist/commands/inbox.d.ts +1 -1
  16. package/dist/commands/inbox.d.ts.map +1 -1
  17. package/dist/commands/inbox.js +55 -2
  18. package/dist/commands/rm.d.ts.map +1 -1
  19. package/dist/commands/rm.js +28 -4
  20. package/dist/commands/servers.d.ts.map +1 -1
  21. package/dist/commands/servers.js +16 -1
  22. package/dist/commands/supervisor.d.ts.map +1 -1
  23. package/dist/commands/supervisor.js +7 -2
  24. package/dist/commands/toc.d.ts +14 -0
  25. package/dist/commands/toc.d.ts.map +1 -1
  26. package/dist/commands/toc.js +7 -4
  27. package/dist/commands/tunnel.d.ts +65 -0
  28. package/dist/commands/tunnel.d.ts.map +1 -1
  29. package/dist/commands/tunnel.js +290 -101
  30. package/dist/commands/web-fetch.d.ts +3 -2
  31. package/dist/commands/web-fetch.d.ts.map +1 -1
  32. package/dist/commands/web-fetch.js +25 -19
  33. package/dist/commands/web.d.ts +7 -1
  34. package/dist/commands/web.d.ts.map +1 -1
  35. package/dist/commands/web.js +9 -3
  36. package/dist/commands/workflow.js +24 -30
  37. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  38. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  39. package/dist/core/agents/live-authority-v3.js +11 -0
  40. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  41. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  42. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  43. package/dist/core/agents/mailbox.d.ts +9 -8
  44. package/dist/core/agents/mailbox.d.ts.map +1 -1
  45. package/dist/core/agents/mailbox.js +24 -17
  46. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  47. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  48. package/dist/core/agents/render/prompt-context.js +19 -17
  49. package/dist/core/agents/render/session-context.d.ts +2 -8
  50. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  51. package/dist/core/agents/render/session-context.js +64 -43
  52. package/dist/core/agents/rules/stop-hook.js +3 -3
  53. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  54. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  55. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  57. package/dist/core/agents/state/names.d.ts +43 -3
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +87 -3
  60. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  61. package/dist/core/agents/state/pidmap.js +5 -0
  62. package/dist/core/agents/state/proc-start.d.ts +19 -0
  63. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  64. package/dist/core/agents/state/proc-start.js +41 -0
  65. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  66. package/dist/core/agents/state/stale-sweep.js +180 -70
  67. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  68. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  69. package/dist/core/agents/state/unchanged-file.js +390 -0
  70. package/dist/core/context/index.d.ts +22 -10
  71. package/dist/core/context/index.d.ts.map +1 -1
  72. package/dist/core/context/index.js +381 -119
  73. package/dist/core/context/observation.d.ts +26 -0
  74. package/dist/core/context/observation.d.ts.map +1 -0
  75. package/dist/core/context/observation.js +11 -0
  76. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  77. package/dist/core/diagnostics/bundle.js +25 -14
  78. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  79. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  80. package/dist/core/diagnostics/path-identity.js +12 -0
  81. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  82. package/dist/core/diagnostics/sanitize.js +4 -3
  83. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  84. package/dist/core/events/v3/authority-outbox.js +29 -0
  85. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  86. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  87. package/dist/core/events/v3/generation-custody.js +50 -0
  88. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  89. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  90. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  91. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  92. package/dist/core/events/v3/producers/recorder.js +77 -64
  93. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  94. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  95. package/dist/core/governor/planning.d.ts.map +1 -1
  96. package/dist/core/governor/planning.js +14 -1
  97. package/dist/core/governor/service-read.d.ts +10 -2
  98. package/dist/core/governor/service-read.d.ts.map +1 -1
  99. package/dist/core/governor/service-read.js +105 -22
  100. package/dist/core/governor/service.d.ts +20 -2
  101. package/dist/core/governor/service.d.ts.map +1 -1
  102. package/dist/core/governor/service.js +72 -47
  103. package/dist/core/hooks/cli.js +120 -23
  104. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  105. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  106. package/dist/core/hooks/fork-lineage.js +62 -0
  107. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  108. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  109. package/dist/core/hooks/resolve/transcript.js +84 -0
  110. package/dist/core/hooks/shell-rm.d.ts +2 -0
  111. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  112. package/dist/core/hooks/shell-rm.js +105 -0
  113. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  114. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  115. package/dist/core/hooks/shell-tokens.js +97 -0
  116. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  117. package/dist/core/hooks/shell-waiter.js +3 -93
  118. package/dist/core/inbox/contract.d.ts +2 -1
  119. package/dist/core/inbox/contract.d.ts.map +1 -1
  120. package/dist/core/inbox/index.d.ts +2 -0
  121. package/dist/core/inbox/index.d.ts.map +1 -1
  122. package/dist/core/inbox/index.js +2 -0
  123. package/dist/core/inbox/peer-messages.d.ts +37 -0
  124. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  125. package/dist/core/inbox/peer-messages.js +72 -0
  126. package/dist/core/inbox/service.d.ts +8 -1
  127. package/dist/core/inbox/service.d.ts.map +1 -1
  128. package/dist/core/inbox/service.js +67 -21
  129. package/dist/core/inbox/targets.d.ts +22 -0
  130. package/dist/core/inbox/targets.d.ts.map +1 -0
  131. package/dist/core/inbox/targets.js +108 -0
  132. package/dist/core/inbox/watcher.js +7 -3
  133. package/dist/core/journal/index.d.ts.map +1 -1
  134. package/dist/core/journal/index.js +17 -2
  135. package/dist/core/resources/contract.d.ts +3 -0
  136. package/dist/core/resources/contract.d.ts.map +1 -1
  137. package/dist/core/resources/service-status.d.ts +14 -1
  138. package/dist/core/resources/service-status.d.ts.map +1 -1
  139. package/dist/core/resources/service-status.js +23 -13
  140. package/dist/core/resources/service.d.ts +9 -1
  141. package/dist/core/resources/service.d.ts.map +1 -1
  142. package/dist/core/resources/service.js +47 -27
  143. package/dist/core/resources/status.d.ts +2 -5
  144. package/dist/core/resources/status.d.ts.map +1 -1
  145. package/dist/core/resources/status.js +36 -7
  146. package/dist/core/semantic/contract.d.ts +5 -5
  147. package/dist/core/servers/index.d.ts +24 -13
  148. package/dist/core/servers/index.d.ts.map +1 -1
  149. package/dist/core/servers/index.js +132 -92
  150. package/dist/core/servers/tunnels.d.ts +3 -5
  151. package/dist/core/servers/tunnels.d.ts.map +1 -1
  152. package/dist/core/servers/tunnels.js +3 -9
  153. package/dist/core/storage/process-log.d.ts +12 -0
  154. package/dist/core/storage/process-log.d.ts.map +1 -1
  155. package/dist/core/storage/process-log.js +10 -2
  156. package/dist/core/supervisor/contract.d.ts +12 -0
  157. package/dist/core/supervisor/contract.d.ts.map +1 -1
  158. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  159. package/dist/core/supervisor/explanations.js +2 -1
  160. package/dist/core/supervisor/findings.d.ts.map +1 -1
  161. package/dist/core/supervisor/findings.js +54 -9
  162. package/dist/core/supervisor/service.d.ts +9 -1
  163. package/dist/core/supervisor/service.d.ts.map +1 -1
  164. package/dist/core/supervisor/service.js +59 -29
  165. package/dist/core/supervisor/services.d.ts.map +1 -1
  166. package/dist/core/supervisor/services.js +22 -1
  167. package/dist/core/supervisor/status.d.ts +17 -2
  168. package/dist/core/supervisor/status.d.ts.map +1 -1
  169. package/dist/core/supervisor/status.js +70 -12
  170. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  171. package/dist/core/supervisor/timeline.js +2 -1
  172. package/dist/core/work/state.js +7 -7
  173. package/dist/core/workflow/engine.d.ts.map +1 -1
  174. package/dist/core/workflow/engine.js +105 -32
  175. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  176. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  177. package/dist/core/workflow/failure-evidence.js +114 -0
  178. package/dist/core/workflow/index.d.ts +1 -1
  179. package/dist/core/workflow/index.d.ts.map +1 -1
  180. package/dist/core/workflow/index.js +1 -1
  181. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  182. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  183. package/dist/core/workflow/live-session-v3.js +126 -5
  184. package/dist/core/workflow/proof.d.ts +5 -5
  185. package/dist/core/workflow/proof.d.ts.map +1 -1
  186. package/dist/core/workflow/proof.js +115 -20
  187. package/dist/core/workflow/run-state.d.ts +3 -1
  188. package/dist/core/workflow/run-state.d.ts.map +1 -1
  189. package/dist/core/workflow/run-state.js +31 -27
  190. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  191. package/dist/core/workflow/spawn-claude.js +19 -0
  192. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  193. package/dist/core/workflow/spawn-codex.js +13 -0
  194. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  195. package/dist/core/workflow/spawn-cursor.js +19 -0
  196. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  197. package/dist/core/workflow/spawn-opencode.js +14 -0
  198. package/dist/core/workflow/types.d.ts +64 -17
  199. package/dist/core/workflow/types.d.ts.map +1 -1
  200. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  201. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  202. package/dist/core/workflow/workspaces/index.js +0 -1
  203. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  204. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  205. package/dist/core/workflow/workspaces/leases.js +30 -16
  206. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  207. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  208. package/dist/core/workflow/workspaces/local-git.js +270 -6
  209. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  210. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  211. package/dist/core/workflow/workspaces/ownership.js +194 -0
  212. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  213. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  214. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  215. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  216. package/dist/core/workflow/workspaces/recovery.js +21 -13
  217. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  218. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  219. package/dist/core/workflow/workspaces/state.js +21 -1
  220. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  221. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  222. package/dist/lib/browser/windows-chrome.js +262 -73
  223. package/dist/lib/docs-links.d.ts.map +1 -1
  224. package/dist/lib/docs-links.js +45 -2
  225. package/dist/lib/guarded-remove.d.ts +7 -0
  226. package/dist/lib/guarded-remove.d.ts.map +1 -1
  227. package/dist/lib/guarded-remove.js +106 -12
  228. package/dist/lib/identities/assume.d.ts.map +1 -1
  229. package/dist/lib/identities/assume.js +4 -0
  230. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  231. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  232. package/dist/lib/tunnel/allowlist.js +204 -0
  233. package/dist/lib/tunnel/current-address.d.ts +41 -0
  234. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  235. package/dist/lib/tunnel/current-address.js +123 -0
  236. package/dist/lib/tunnel/gate.js +13 -6
  237. package/dist/lib/tunnel/processes.d.ts +39 -0
  238. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  239. package/dist/lib/tunnel/processes.js +121 -0
  240. package/dist/lib/tunnel/state.d.ts +28 -3
  241. package/dist/lib/tunnel/state.d.ts.map +1 -1
  242. package/dist/lib/tunnel/state.js +98 -27
  243. package/dist/lib/zip/index.d.ts +41 -0
  244. package/dist/lib/zip/index.d.ts.map +1 -0
  245. package/dist/lib/zip/index.js +398 -0
  246. package/package.json +8 -1
  247. package/src/commander.ts +32 -0
  248. package/src/commands/agents.ts +151 -74
  249. package/src/commands/browse.ts +50 -14
  250. package/src/commands/diagnostics.ts +10 -0
  251. package/src/commands/doctor.ts +26 -7
  252. package/src/commands/governor.ts +23 -10
  253. package/src/commands/inbox.ts +52 -2
  254. package/src/commands/rm.ts +43 -4
  255. package/src/commands/servers.ts +16 -4
  256. package/src/commands/supervisor.ts +7 -2
  257. package/src/commands/toc.ts +14 -6
  258. package/src/commands/tunnel.ts +394 -116
  259. package/src/commands/web-fetch.ts +30 -19
  260. package/src/commands/web.ts +9 -3
  261. package/src/commands/workflow.ts +30 -30
  262. package/src/core/agents/live-authority-v3.ts +17 -0
  263. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  264. package/src/core/agents/mailbox.ts +26 -17
  265. package/src/core/agents/render/prompt-context.ts +35 -20
  266. package/src/core/agents/render/session-context.ts +92 -48
  267. package/src/core/agents/rules/stop-hook.ts +3 -3
  268. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  269. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  270. package/src/core/agents/state/names.ts +117 -6
  271. package/src/core/agents/state/pidmap.ts +13 -0
  272. package/src/core/agents/state/proc-start.ts +57 -0
  273. package/src/core/agents/state/stale-sweep.ts +247 -82
  274. package/src/core/agents/state/unchanged-file.ts +456 -0
  275. package/src/core/context/index.ts +446 -136
  276. package/src/core/context/observation.ts +29 -0
  277. package/src/core/diagnostics/bundle.ts +23 -18
  278. package/src/core/diagnostics/path-identity.ts +15 -0
  279. package/src/core/diagnostics/sanitize.ts +4 -3
  280. package/src/core/events/v3/authority-outbox.ts +56 -0
  281. package/src/core/events/v3/generation-custody.ts +59 -0
  282. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  283. package/src/core/events/v3/producers/recorder.ts +94 -68
  284. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  285. package/src/core/governor/planning.ts +20 -8
  286. package/src/core/governor/service-read.ts +128 -20
  287. package/src/core/governor/service.ts +119 -42
  288. package/src/core/hooks/cli.ts +139 -22
  289. package/src/core/hooks/fork-lineage.ts +82 -0
  290. package/src/core/hooks/resolve/transcript.ts +88 -0
  291. package/src/core/hooks/shell-rm.ts +103 -0
  292. package/src/core/hooks/shell-tokens.ts +96 -0
  293. package/src/core/hooks/shell-waiter.ts +4 -91
  294. package/src/core/inbox/contract.ts +2 -1
  295. package/src/core/inbox/index.ts +2 -0
  296. package/src/core/inbox/peer-messages.ts +94 -0
  297. package/src/core/inbox/service.ts +88 -22
  298. package/src/core/inbox/targets.ts +148 -0
  299. package/src/core/inbox/watcher.ts +8 -9
  300. package/src/core/journal/index.ts +19 -2
  301. package/src/core/resources/contract.ts +4 -0
  302. package/src/core/resources/service-status.ts +35 -12
  303. package/src/core/resources/service.ts +71 -24
  304. package/src/core/resources/status.ts +57 -8
  305. package/src/core/servers/index.ts +177 -91
  306. package/src/core/servers/tunnels.ts +3 -11
  307. package/src/core/storage/process-log.ts +17 -2
  308. package/src/core/supervisor/contract.ts +13 -0
  309. package/src/core/supervisor/explanations.ts +4 -1
  310. package/src/core/supervisor/findings.ts +68 -10
  311. package/src/core/supervisor/service.ts +88 -26
  312. package/src/core/supervisor/services.ts +26 -8
  313. package/src/core/supervisor/status.ts +89 -12
  314. package/src/core/supervisor/timeline.ts +2 -1
  315. package/src/core/work/state.ts +7 -7
  316. package/src/core/workflow/engine.ts +131 -49
  317. package/src/core/workflow/failure-evidence.ts +141 -0
  318. package/src/core/workflow/index.ts +0 -1
  319. package/src/core/workflow/live-session-v3.ts +146 -5
  320. package/src/core/workflow/proof.ts +134 -26
  321. package/src/core/workflow/run-state.ts +31 -29
  322. package/src/core/workflow/spawn-claude.ts +18 -0
  323. package/src/core/workflow/spawn-codex.ts +13 -0
  324. package/src/core/workflow/spawn-cursor.ts +18 -0
  325. package/src/core/workflow/spawn-opencode.ts +14 -0
  326. package/src/core/workflow/types.ts +51 -17
  327. package/src/core/workflow/workspaces/index.ts +0 -1
  328. package/src/core/workflow/workspaces/leases.ts +48 -17
  329. package/src/core/workflow/workspaces/local-git.ts +315 -5
  330. package/src/core/workflow/workspaces/ownership.ts +232 -0
  331. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  332. package/src/core/workflow/workspaces/recovery.ts +25 -13
  333. package/src/core/workflow/workspaces/state.ts +20 -0
  334. package/src/lib/browser/windows-chrome.ts +346 -86
  335. package/src/lib/docs-links.ts +49 -2
  336. package/src/lib/guarded-remove.ts +132 -14
  337. package/src/lib/identities/assume.ts +4 -0
  338. package/src/lib/tunnel/allowlist.ts +208 -0
  339. package/src/lib/tunnel/current-address.ts +158 -0
  340. package/src/lib/tunnel/gate.ts +16 -8
  341. package/src/lib/tunnel/processes.ts +149 -0
  342. package/src/lib/tunnel/state.ts +123 -27
  343. package/src/lib/zip/index.ts +483 -0
@@ -0,0 +1,398 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { closeSync, constants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { inflateRawSync } from "node:zlib";
5
+ import { strFromU8, zipSync } from "fflate";
6
+ export const DEFAULT_ZIP_LIMITS = Object.freeze({
7
+ archiveBytes: 64 * 1024 * 1024,
8
+ entryBytes: 64 * 1024 * 1024,
9
+ totalBytes: 256 * 1024 * 1024,
10
+ entries: 10_000,
11
+ });
12
+ function limitsFor(options) {
13
+ const limits = { ...DEFAULT_ZIP_LIMITS, ...options.limits };
14
+ for (const [key, value] of Object.entries(limits))
15
+ if (!Number.isSafeInteger(value) || value < 1)
16
+ throw new Error(`Invalid ZIP limit: ${key}`);
17
+ return limits;
18
+ }
19
+ function readBoundedFile(path, limit, noFollow = false) {
20
+ const fd = openSync(path, constants.O_RDONLY | (noFollow ? (constants.O_NOFOLLOW ?? 0) : 0));
21
+ try {
22
+ const stat = fstatSync(fd);
23
+ if (!stat.isFile() || stat.size > limit)
24
+ throw new Error("ZIP input exceeds byte limit or is not a file");
25
+ const bytes = Buffer.alloc(stat.size + 1);
26
+ let count = 0;
27
+ while (count < bytes.length) {
28
+ const read = readSync(fd, bytes, count, bytes.length - count, null);
29
+ if (!read)
30
+ break;
31
+ count += read;
32
+ }
33
+ if (count !== stat.size)
34
+ throw new Error("ZIP input changed while reading");
35
+ return bytes.subarray(0, count);
36
+ }
37
+ finally {
38
+ closeSync(fd);
39
+ }
40
+ }
41
+ function bytesOf(source, limits) {
42
+ const bytes = typeof source === "string" ? readBoundedFile(source, limits.archiveBytes) : source;
43
+ if (bytes.byteLength > limits.archiveBytes)
44
+ throw new Error("ZIP archive exceeds compressed byte limit");
45
+ return bytes;
46
+ }
47
+ function centralEntries(bytes, limits) {
48
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
49
+ let end = bytes.length - 22;
50
+ for (; end >= Math.max(0, bytes.length - 65_557); end--) {
51
+ if (view.getUint32(end, true) === 0x06054b50 &&
52
+ end + 22 + view.getUint16(end + 20, true) === bytes.length)
53
+ break;
54
+ }
55
+ if (end < Math.max(0, bytes.length - 65_557))
56
+ throw new Error("Missing ZIP end record");
57
+ const count = view.getUint16(end + 10, true);
58
+ if (view.getUint16(end + 4, true) !== 0 ||
59
+ view.getUint16(end + 6, true) !== 0 ||
60
+ view.getUint16(end + 8, true) !== count ||
61
+ count === 0xffff)
62
+ throw new Error("Split or ZIP64 archives are unsupported");
63
+ if (count > limits.entries)
64
+ throw new Error("ZIP archive exceeds entry count limit");
65
+ const central = view.getUint32(end + 16, true);
66
+ let offset = central;
67
+ if (offset + view.getUint32(end + 12, true) !== end)
68
+ throw new Error("Invalid ZIP directory bounds");
69
+ const entries = [];
70
+ const names = new Set();
71
+ let total = 0;
72
+ for (let index = 0; index < count; index++) {
73
+ if (offset + 46 > end || view.getUint32(offset, true) !== 0x02014b50)
74
+ throw new Error("Invalid ZIP directory entry");
75
+ const flags = view.getUint16(offset + 8, true);
76
+ const method = view.getUint16(offset + 10, true);
77
+ if (flags & 1)
78
+ throw new Error("Encrypted ZIP entries are unsupported");
79
+ if (method !== 0 && method !== 8)
80
+ throw new Error("Unsupported ZIP compression method");
81
+ const nameLength = view.getUint16(offset + 28, true);
82
+ const next = offset +
83
+ 46 +
84
+ nameLength +
85
+ view.getUint16(offset + 30, true) +
86
+ view.getUint16(offset + 32, true);
87
+ if (next > end || view.getUint16(offset + 34, true) !== 0)
88
+ throw new Error("Invalid ZIP directory entry bounds");
89
+ const rawName = bytes.subarray(offset + 46, offset + 46 + nameLength);
90
+ const name = strFromU8(rawName, !(flags & 0x800));
91
+ if (names.has(name))
92
+ throw new Error(`Duplicate ZIP entry: ${name}`);
93
+ names.add(name);
94
+ const size = view.getUint32(offset + 24, true);
95
+ const compressedSize = view.getUint32(offset + 20, true);
96
+ const checksum = view.getUint32(offset + 16, true);
97
+ const header = view.getUint32(offset + 42, true);
98
+ if (size === 0xffffffff || compressedSize === 0xffffffff || header === 0xffffffff)
99
+ throw new Error("ZIP64 entries are unsupported");
100
+ total += size;
101
+ if (size > limits.entryBytes || total > limits.totalBytes)
102
+ throw new Error("ZIP archive exceeds entry or total byte limit");
103
+ if (header + 30 > central || view.getUint32(header, true) !== 0x04034b50)
104
+ throw new Error("Invalid ZIP local header");
105
+ const localNameLength = view.getUint16(header + 26, true);
106
+ const dataOffset = header + 30 + localNameLength + view.getUint16(header + 28, true);
107
+ if (dataOffset + compressedSize > central ||
108
+ localNameLength !== nameLength ||
109
+ view.getUint16(header + 6, true) !== flags ||
110
+ view.getUint16(header + 8, true) !== method ||
111
+ !rawName.every((byte, i) => bytes[header + 30 + i] === byte))
112
+ throw new Error("ZIP local and central header mismatch");
113
+ if (!(flags & 8) &&
114
+ (view.getUint32(header + 14, true) !== checksum ||
115
+ view.getUint32(header + 18, true) !== compressedSize ||
116
+ view.getUint32(header + 22, true) !== size))
117
+ throw new Error("ZIP local and central size/checksum mismatch");
118
+ if (method === 0 && compressedSize !== size)
119
+ throw new Error("Stored ZIP entry size mismatch");
120
+ if (name.endsWith("/") && size !== 0)
121
+ throw new Error("ZIP directory has a payload");
122
+ entries.push({
123
+ name,
124
+ size,
125
+ compressedSize,
126
+ directory: name.endsWith("/"),
127
+ crc32: checksum,
128
+ method,
129
+ dataOffset,
130
+ headerOffset: header,
131
+ });
132
+ offset = next;
133
+ }
134
+ if (offset !== end)
135
+ throw new Error("Invalid ZIP directory length");
136
+ const ordered = [...entries].sort((a, b) => a.headerOffset - b.headerOffset);
137
+ for (let i = 1; i < ordered.length; i++)
138
+ if (ordered[i].headerOffset < ordered[i - 1].dataOffset + ordered[i - 1].compressedSize)
139
+ throw new Error("Overlapping ZIP entry ranges");
140
+ return entries;
141
+ }
142
+ const CRC_TABLE = Uint32Array.from({ length: 256 }, (_, value) => {
143
+ for (let bit = 0; bit < 8; bit++)
144
+ value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
145
+ return value >>> 0;
146
+ });
147
+ function crc32(bytes) {
148
+ let crc = 0xffffffff;
149
+ for (const byte of bytes)
150
+ crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ byte) & 255];
151
+ return (crc ^ 0xffffffff) >>> 0;
152
+ }
153
+ function inventory(bytes, limits) {
154
+ try {
155
+ return centralEntries(bytes, limits);
156
+ }
157
+ catch (error) {
158
+ throw new Error(`Unreadable ZIP archive: ${error instanceof Error ? error.message : String(error)}`);
159
+ }
160
+ }
161
+ export function listZip(source, options = {}) {
162
+ const limits = limitsFor(options);
163
+ return inventory(bytesOf(source, limits), limits).map(({ method, dataOffset, headerOffset, ...entry }) => entry);
164
+ }
165
+ export function readZipEntries(source, keep = () => true, options = {}) {
166
+ const limits = limitsFor(options);
167
+ const bytes = bytesOf(source, limits);
168
+ const selected = inventory(bytes, limits).filter((entry) => !entry.directory && keep(entry.name));
169
+ const result = new Map();
170
+ for (const entry of selected) {
171
+ const compressed = bytes.subarray(entry.dataOffset, entry.dataOffset + entry.compressedSize);
172
+ // zlib enforces actual output length during decoding, independent of forged headers.
173
+ const data = entry.method === 0
174
+ ? compressed.slice()
175
+ : inflateRawSync(compressed, {
176
+ maxOutputLength: Math.max(1, Math.min(entry.size, limits.entryBytes)),
177
+ });
178
+ if (!(data instanceof Uint8Array) || data.byteLength !== entry.size)
179
+ throw new Error(`ZIP entry size or decode mismatch: ${entry.name}`);
180
+ if (crc32(data) !== entry.crc32)
181
+ throw new Error(`ZIP entry checksum mismatch: ${entry.name}`);
182
+ result.set(entry.name, data);
183
+ }
184
+ return result;
185
+ }
186
+ export function readZipEntry(source, name, options = {}) {
187
+ return readZipEntries(source, (entry) => entry === name, options).get(name);
188
+ }
189
+ export function readZipText(source, name, options = {}) {
190
+ const bytes = readZipEntry(source, name, options);
191
+ return bytes ? new TextDecoder().decode(bytes) : undefined;
192
+ }
193
+ /** Portable names exclude traversal, device names, streams and ambiguous aliases. */
194
+ export function safeEntryPath(root, name) {
195
+ const normalized = name.replaceAll("\\", "/");
196
+ const parts = normalized.replace(/\/$/, "").split("/");
197
+ if (!name ||
198
+ normalized.startsWith("/") ||
199
+ isAbsolute(normalized) ||
200
+ parts.some((part) => !part ||
201
+ part === "." ||
202
+ part === ".." ||
203
+ [...part].some((character) => character.charCodeAt(0) < 32) ||
204
+ /[:<>"|?*]/.test(part) ||
205
+ /[. ]$/.test(part) ||
206
+ /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) {
207
+ throw new Error(`Unsafe ZIP entry path: ${name}`);
208
+ }
209
+ const base = resolve(root);
210
+ const target = resolve(base, ...parts);
211
+ if (!target.startsWith(base.endsWith(sep) ? base : base + sep))
212
+ throw new Error(`ZIP entry escapes destination: ${name}`);
213
+ return target;
214
+ }
215
+ function statIfPresent(path) {
216
+ try {
217
+ return lstatSync(path);
218
+ }
219
+ catch (error) {
220
+ if (error.code === "ENOENT")
221
+ return undefined;
222
+ throw error;
223
+ }
224
+ }
225
+ const outside = (rel) => rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
226
+ /**
227
+ * Canonicalize the directory holding a caller-named path. The caller chose
228
+ * that location, so links among its ancestors (macOS /tmp and /var, a linked
229
+ * home directory) are resolved once here. The named path itself and everything
230
+ * below it stay subject to assertNoLinks, which is where a planted link could
231
+ * redirect a write. Missing directories are appended unresolved and checked
232
+ * again once they exist.
233
+ */
234
+ function anchored(path) {
235
+ const named = resolve(path);
236
+ const missing = [basename(named)];
237
+ let existing = dirname(named);
238
+ while (!statIfPresent(existing) && dirname(existing) !== existing) {
239
+ missing.unshift(basename(existing));
240
+ existing = dirname(existing);
241
+ }
242
+ const anchor = realpathSync(existing);
243
+ return { anchor, path: join(anchor, ...missing) };
244
+ }
245
+ /**
246
+ * Inspect every existing component from the path up to, not including, its
247
+ * anchor, including a dangling link at the final path.
248
+ */
249
+ function assertNoLinks(path, leaf, anchor) {
250
+ let current = resolve(path);
251
+ if (outside(relative(anchor, current)))
252
+ throw new Error(`ZIP path is outside its anchored directory: ${current}`);
253
+ let first = true;
254
+ while (current !== anchor) {
255
+ const stat = statIfPresent(current);
256
+ if (stat) {
257
+ if (stat.isSymbolicLink())
258
+ throw new Error(`Refusing ZIP path through a symbolic link: ${current}`);
259
+ if ((!first || leaf === "directory") && !stat.isDirectory())
260
+ throw new Error(`ZIP directory path is not a directory: ${current}`);
261
+ if (first && leaf === "file" && !stat.isFile())
262
+ throw new Error(`ZIP output is not a regular file: ${current}`);
263
+ }
264
+ current = dirname(current);
265
+ first = false;
266
+ }
267
+ }
268
+ function writeAtomic(path, bytes, overwrite, anchor) {
269
+ assertNoLinks(path, "file", anchor);
270
+ if (!overwrite && statIfPresent(path))
271
+ throw new Error(`ZIP output already exists: ${path}`);
272
+ mkdirSync(dirname(path), { recursive: true });
273
+ assertNoLinks(dirname(path), "directory", anchor);
274
+ const temp = join(dirname(path), `.zip-${randomUUID()}.tmp`);
275
+ let fd;
276
+ try {
277
+ fd = openSync(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), 0o600);
278
+ writeFileSync(fd, bytes);
279
+ closeSync(fd);
280
+ fd = undefined;
281
+ assertNoLinks(path, "file", anchor);
282
+ if (overwrite)
283
+ renameSync(temp, path);
284
+ else {
285
+ linkSync(temp, path);
286
+ unlinkSync(temp);
287
+ }
288
+ }
289
+ finally {
290
+ if (fd !== undefined)
291
+ closeSync(fd);
292
+ if (statIfPresent(temp))
293
+ unlinkSync(temp);
294
+ }
295
+ }
296
+ /** Preflight all selected names, collisions and existing filesystem paths before writing. */
297
+ export function extractZip(source, dest, options = {}) {
298
+ const limits = limitsFor(options);
299
+ const bytes = bytesOf(source, limits);
300
+ const entries = inventory(bytes, limits).filter((entry) => (options.filter ?? (() => true))(entry.name));
301
+ const { anchor, path: root } = anchored(dest);
302
+ assertNoLinks(root, "directory", anchor);
303
+ const targets = new Map();
304
+ for (const entry of entries) {
305
+ const target = safeEntryPath(root, entry.name);
306
+ const key = target.toLowerCase();
307
+ if (targets.has(key))
308
+ throw new Error(`Colliding ZIP entry: ${entry.name}`);
309
+ targets.set(key, entry.directory);
310
+ assertNoLinks(target, entry.directory ? "directory" : "file", anchor);
311
+ if (!entry.directory && !options.overwrite && statIfPresent(target))
312
+ throw new Error(`ZIP output already exists: ${target}`);
313
+ }
314
+ for (const target of targets.keys()) {
315
+ let parent = dirname(target);
316
+ while (parent !== dirname(parent)) {
317
+ if (targets.get(parent) === false)
318
+ throw new Error(`ZIP file/directory collision: ${parent}`);
319
+ parent = dirname(parent);
320
+ }
321
+ }
322
+ const selected = new Set(entries.filter((entry) => !entry.directory).map((entry) => entry.name));
323
+ const files = readZipEntries(bytes, (name) => selected.has(name), options);
324
+ mkdirSync(root, { recursive: true });
325
+ for (const entry of entries) {
326
+ if (entry.directory) {
327
+ const path = safeEntryPath(root, entry.name);
328
+ assertNoLinks(path, "directory", anchor);
329
+ mkdirSync(path, { recursive: true });
330
+ }
331
+ }
332
+ for (const [name, data] of files)
333
+ writeAtomic(safeEntryPath(root, name), data, options.overwrite ?? false, anchor);
334
+ return [...files.keys()];
335
+ }
336
+ export function buildZip(inputs, options = {}) {
337
+ const limits = limitsFor(options);
338
+ // Inputs are matched against the base as the caller spelled it; reads go
339
+ // through its anchored form.
340
+ const named = resolve(options.base ?? ".");
341
+ const { anchor, path: base } = anchored(named);
342
+ assertNoLinks(base, "directory", anchor);
343
+ const tree = Object.create(null);
344
+ const entries = [];
345
+ const names = new Set();
346
+ let total = 0;
347
+ const add = (input) => {
348
+ const rel = relative(named, resolve(named, input));
349
+ if (!rel || outside(rel))
350
+ throw new Error(`ZIP input is outside its base directory: ${input}`);
351
+ const path = join(base, rel);
352
+ const stat = lstatSync(path);
353
+ if (stat.isSymbolicLink())
354
+ throw new Error(`Refusing ZIP input symbolic link: ${input}`);
355
+ assertNoLinks(path, stat.isDirectory() ? "directory" : "file", anchor);
356
+ if (stat.isDirectory()) {
357
+ for (const name of readdirSync(path).sort())
358
+ add(join(named, rel, name));
359
+ return;
360
+ }
361
+ if (!stat.isFile())
362
+ throw new Error(`ZIP input is not a regular file: ${input}`);
363
+ const name = rel.split(sep).join("/");
364
+ safeEntryPath(base, name);
365
+ if (Object.hasOwn(tree, name))
366
+ return;
367
+ if (names.has(name.toLowerCase()))
368
+ throw new Error(`Colliding ZIP input: ${name}`);
369
+ total += stat.size;
370
+ if (stat.size > limits.entryBytes ||
371
+ total > limits.totalBytes ||
372
+ entries.length >= limits.entries)
373
+ throw new Error("ZIP inputs exceed size or entry count limit");
374
+ const bytes = readBoundedFile(path, Math.min(stat.size, limits.entryBytes), true);
375
+ if (bytes.byteLength !== stat.size)
376
+ throw new Error(`ZIP input changed while reading: ${name}`);
377
+ names.add(name.toLowerCase());
378
+ tree[name] = [bytes, { mtime: stat.mtime }];
379
+ entries.push(name);
380
+ };
381
+ for (const input of [...inputs].sort())
382
+ add(input);
383
+ if (!entries.length)
384
+ throw new Error("Nothing to ZIP: inputs contain no files");
385
+ const bytes = zipSync(tree, { level: options.level ?? 6 });
386
+ if (bytes.byteLength > limits.archiveBytes)
387
+ throw new Error("ZIP archive exceeds compressed byte limit");
388
+ return { bytes, entries };
389
+ }
390
+ export function createZip(archive, inputs, options = {}) {
391
+ const { anchor, path } = anchored(archive);
392
+ assertNoLinks(path, "file", anchor);
393
+ if (!options.overwrite && statIfPresent(path))
394
+ throw new Error(`ZIP output already exists: ${archive}`);
395
+ const result = buildZip(inputs, options);
396
+ writeAtomic(path, result.bytes, options.overwrite ?? false, anchor);
397
+ return result.entries;
398
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
@@ -182,6 +182,11 @@
182
182
  "types": "./dist/lib/format.d.ts",
183
183
  "import": "./dist/lib/format.js"
184
184
  },
185
+ "./lib/zip": {
186
+ "bun": "./src/lib/zip/index.ts",
187
+ "types": "./dist/lib/zip/index.d.ts",
188
+ "import": "./dist/lib/zip/index.js"
189
+ },
185
190
  "./lib/headless": {
186
191
  "bun": "./src/lib/headless/index.ts",
187
192
  "types": "./dist/lib/headless/index.d.ts",
@@ -266,6 +271,7 @@
266
271
  "test:web": "cd web && bun install && bun test",
267
272
  "test:integration": "bash tests/integration/run.sh",
268
273
  "smoke": "node scripts/smoke-test.mjs",
274
+ "check:local": "node scripts/check-local.mjs",
269
275
  "docs:dev": "bun run --cwd docs dev",
270
276
  "docs:build": "bun run --cwd docs build",
271
277
  "brand:showcase": "node scripts/build-brand-showcase.mjs",
@@ -283,6 +289,7 @@
283
289
  "@sinclair/typebox": "^0.34.52",
284
290
  "axe-core": "^4.12.1",
285
291
  "commander": "^14.0.3",
292
+ "fflate": "^0.8.2",
286
293
  "gpt-tokenizer": "^3.4.0",
287
294
  "js-yaml": "^4.3.0",
288
295
  "linkedom": "^0.18.0",
package/src/commander.ts CHANGED
@@ -280,6 +280,38 @@ export async function registerHarneryLogStorageCommands(
280
280
  return parent;
281
281
  }
282
282
 
283
+ export interface HarneryTunnelCommandOptions {
284
+ emit?: EmitContext;
285
+ context?: HarneryProgramContext;
286
+ }
287
+
288
+ /**
289
+ * Mount `tunnel` under `parent`, for a host whose own program does not come
290
+ * from `createHarneryProgram` (a host CLI that exposes the tunnel as one of its
291
+ * command families).
292
+ */
293
+ export async function registerHarneryTunnelCommand(
294
+ parent: Command,
295
+ options: HarneryTunnelCommandOptions = {},
296
+ ): Promise<Command> {
297
+ const { registerTunnelCommand } = await import("./commands/tunnel.ts");
298
+ registerTunnelCommand(parent, options.emit ?? defaultEmit, options.context);
299
+ return parent;
300
+ }
301
+
302
+ export type { RefreshCurrentOptions, RefreshCurrentResult } from "./commands/tunnel.ts";
303
+
304
+ /**
305
+ * `tunnel allow add --current` as a function: allow this machine's public
306
+ * addresses and optionally reload running gates, keeping their URLs.
307
+ */
308
+ export async function refreshTunnelCurrentAddress(
309
+ options: import("./commands/tunnel.ts").RefreshCurrentOptions,
310
+ ): Promise<import("./commands/tunnel.ts").RefreshCurrentResult> {
311
+ const { refreshCurrentAddress } = await import("./commands/tunnel.ts");
312
+ return refreshCurrentAddress(options);
313
+ }
314
+
283
315
  export function createHarneryProgram(opts: HarneryContextOpts = {}): Command {
284
316
  const program = new Command();
285
317
  const emit = opts.emit ?? defaultEmit;