harnery 0.0.1 → 0.2.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 (445) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -2
  3. package/bin/agent-coord +42 -0
  4. package/bin/agent-hook +44 -0
  5. package/bin/harn +40 -0
  6. package/dist/cli.d.ts +9 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +18 -0
  9. package/dist/commander.d.ts +128 -0
  10. package/dist/commander.d.ts.map +1 -0
  11. package/dist/commander.js +126 -0
  12. package/dist/commands/agents.d.ts +18 -0
  13. package/dist/commands/agents.d.ts.map +1 -0
  14. package/dist/commands/agents.js +3946 -0
  15. package/dist/commands/backup.d.ts +22 -0
  16. package/dist/commands/backup.d.ts.map +1 -0
  17. package/dist/commands/backup.js +262 -0
  18. package/dist/commands/browse-ai.d.ts +4 -0
  19. package/dist/commands/browse-ai.d.ts.map +1 -0
  20. package/dist/commands/browse-ai.js +156 -0
  21. package/dist/commands/browse.d.ts +4 -0
  22. package/dist/commands/browse.d.ts.map +1 -0
  23. package/dist/commands/browse.js +590 -0
  24. package/dist/commands/callers.d.ts +4 -0
  25. package/dist/commands/callers.d.ts.map +1 -0
  26. package/dist/commands/callers.js +276 -0
  27. package/dist/commands/completion.d.ts +17 -0
  28. package/dist/commands/completion.d.ts.map +1 -0
  29. package/dist/commands/completion.js +158 -0
  30. package/dist/commands/config-get.d.ts +4 -0
  31. package/dist/commands/config-get.d.ts.map +1 -0
  32. package/dist/commands/config-get.js +131 -0
  33. package/dist/commands/context.d.ts +11 -0
  34. package/dist/commands/context.d.ts.map +1 -0
  35. package/dist/commands/context.js +185 -0
  36. package/dist/commands/cookies.d.ts +4 -0
  37. package/dist/commands/cookies.d.ts.map +1 -0
  38. package/dist/commands/cookies.js +140 -0
  39. package/dist/commands/docs.d.ts +4 -0
  40. package/dist/commands/docs.d.ts.map +1 -0
  41. package/dist/commands/docs.js +137 -0
  42. package/dist/commands/doctor.d.ts +25 -0
  43. package/dist/commands/doctor.d.ts.map +1 -0
  44. package/dist/commands/doctor.js +200 -0
  45. package/dist/commands/edit-batch.d.ts +18 -0
  46. package/dist/commands/edit-batch.d.ts.map +1 -0
  47. package/dist/commands/edit-batch.js +172 -0
  48. package/dist/commands/eml.d.ts +4 -0
  49. package/dist/commands/eml.d.ts.map +1 -0
  50. package/dist/commands/eml.js +428 -0
  51. package/dist/commands/env.d.ts +4 -0
  52. package/dist/commands/env.d.ts.map +1 -0
  53. package/dist/commands/env.js +201 -0
  54. package/dist/commands/fetch.d.ts +4 -0
  55. package/dist/commands/fetch.d.ts.map +1 -0
  56. package/dist/commands/fetch.js +99 -0
  57. package/dist/commands/file-history.d.ts +4 -0
  58. package/dist/commands/file-history.d.ts.map +1 -0
  59. package/dist/commands/file-history.js +152 -0
  60. package/dist/commands/grep.d.ts +4 -0
  61. package/dist/commands/grep.d.ts.map +1 -0
  62. package/dist/commands/grep.js +317 -0
  63. package/dist/commands/init.d.ts +82 -0
  64. package/dist/commands/init.d.ts.map +1 -0
  65. package/dist/commands/init.js +288 -0
  66. package/dist/commands/outline.d.ts +4 -0
  67. package/dist/commands/outline.d.ts.map +1 -0
  68. package/dist/commands/outline.js +509 -0
  69. package/dist/commands/presence.d.ts +12 -0
  70. package/dist/commands/presence.d.ts.map +1 -0
  71. package/dist/commands/presence.js +123 -0
  72. package/dist/commands/read.d.ts +7 -0
  73. package/dist/commands/read.d.ts.map +1 -0
  74. package/dist/commands/read.js +46 -0
  75. package/dist/commands/scratch.d.ts +4 -0
  76. package/dist/commands/scratch.d.ts.map +1 -0
  77. package/dist/commands/scratch.js +426 -0
  78. package/dist/commands/session.d.ts +4 -0
  79. package/dist/commands/session.d.ts.map +1 -0
  80. package/dist/commands/session.js +162 -0
  81. package/dist/commands/sync.d.ts +24 -0
  82. package/dist/commands/sync.d.ts.map +1 -0
  83. package/dist/commands/sync.js +275 -0
  84. package/dist/commands/toc.d.ts +5 -0
  85. package/dist/commands/toc.d.ts.map +1 -0
  86. package/dist/commands/toc.js +153 -0
  87. package/dist/commands/tokens.d.ts +4 -0
  88. package/dist/commands/tokens.d.ts.map +1 -0
  89. package/dist/commands/tokens.js +48 -0
  90. package/dist/commands/tunnel.d.ts +4 -0
  91. package/dist/commands/tunnel.d.ts.map +1 -0
  92. package/dist/commands/tunnel.js +513 -0
  93. package/dist/commands/uninstall.d.ts +22 -0
  94. package/dist/commands/uninstall.d.ts.map +1 -0
  95. package/dist/commands/uninstall.js +126 -0
  96. package/dist/commands/web.d.ts +4 -0
  97. package/dist/commands/web.d.ts.map +1 -0
  98. package/dist/commands/web.js +165 -0
  99. package/dist/core/agents/canonical-emit.d.ts +27 -0
  100. package/dist/core/agents/canonical-emit.d.ts.map +1 -0
  101. package/dist/core/agents/canonical-emit.js +72 -0
  102. package/dist/core/agents/cli-emit.d.ts +27 -0
  103. package/dist/core/agents/cli-emit.d.ts.map +1 -0
  104. package/dist/core/agents/cli-emit.js +57 -0
  105. package/dist/core/agents/cli.d.ts +10 -0
  106. package/dist/core/agents/cli.d.ts.map +1 -0
  107. package/dist/core/agents/cli.js +757 -0
  108. package/dist/core/agents/codex-replay.d.ts +29 -0
  109. package/dist/core/agents/codex-replay.d.ts.map +1 -0
  110. package/dist/core/agents/codex-replay.js +138 -0
  111. package/dist/core/agents/coord-client.d.ts +98 -0
  112. package/dist/core/agents/coord-client.d.ts.map +1 -0
  113. package/dist/core/agents/coord-client.js +212 -0
  114. package/dist/core/agents/events/consume.d.ts +59 -0
  115. package/dist/core/agents/events/consume.d.ts.map +1 -0
  116. package/dist/core/agents/events/consume.js +147 -0
  117. package/dist/core/agents/events/emit.d.ts +42 -0
  118. package/dist/core/agents/events/emit.d.ts.map +1 -0
  119. package/dist/core/agents/events/emit.js +70 -0
  120. package/dist/core/agents/events/ulid.d.ts +11 -0
  121. package/dist/core/agents/events/ulid.d.ts.map +1 -0
  122. package/dist/core/agents/events/ulid.js +47 -0
  123. package/dist/core/agents/index.d.ts +14 -0
  124. package/dist/core/agents/index.d.ts.map +1 -0
  125. package/dist/core/agents/index.js +13 -0
  126. package/dist/core/agents/paths.d.ts +6 -0
  127. package/dist/core/agents/paths.d.ts.map +1 -0
  128. package/dist/core/agents/paths.js +17 -0
  129. package/dist/core/agents/render/prompt-context.d.ts +43 -0
  130. package/dist/core/agents/render/prompt-context.d.ts.map +1 -0
  131. package/dist/core/agents/render/prompt-context.js +335 -0
  132. package/dist/core/agents/render/session-context.d.ts +39 -0
  133. package/dist/core/agents/render/session-context.d.ts.map +1 -0
  134. package/dist/core/agents/render/session-context.js +283 -0
  135. package/dist/core/agents/rules/claim-conflict.d.ts +35 -0
  136. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -0
  137. package/dist/core/agents/rules/claim-conflict.js +244 -0
  138. package/dist/core/agents/rules/commit-conflict.d.ts +59 -0
  139. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -0
  140. package/dist/core/agents/rules/commit-conflict.js +244 -0
  141. package/dist/core/agents/rules/stop-hook.d.ts +44 -0
  142. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -0
  143. package/dist/core/agents/rules/stop-hook.js +161 -0
  144. package/dist/core/agents/session-events.d.ts +41 -0
  145. package/dist/core/agents/session-events.d.ts.map +1 -0
  146. package/dist/core/agents/session-events.js +205 -0
  147. package/dist/core/agents/state/activity-log.d.ts +18 -0
  148. package/dist/core/agents/state/activity-log.d.ts.map +1 -0
  149. package/dist/core/agents/state/activity-log.js +34 -0
  150. package/dist/core/agents/state/council.d.ts +39 -0
  151. package/dist/core/agents/state/council.d.ts.map +1 -0
  152. package/dist/core/agents/state/council.js +216 -0
  153. package/dist/core/agents/state/heartbeat-projector.d.ts +59 -0
  154. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -0
  155. package/dist/core/agents/state/heartbeat-projector.js +436 -0
  156. package/dist/core/agents/state/heartbeat-writer.d.ts +64 -0
  157. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -0
  158. package/dist/core/agents/state/heartbeat-writer.js +271 -0
  159. package/dist/core/agents/state/names.d.ts +35 -0
  160. package/dist/core/agents/state/names.d.ts.map +1 -0
  161. package/dist/core/agents/state/names.js +376 -0
  162. package/dist/core/agents/state/pidmap.d.ts +11 -0
  163. package/dist/core/agents/state/pidmap.d.ts.map +1 -0
  164. package/dist/core/agents/state/pidmap.js +32 -0
  165. package/dist/core/agents/state/scratch.d.ts +27 -0
  166. package/dist/core/agents/state/scratch.d.ts.map +1 -0
  167. package/dist/core/agents/state/scratch.js +90 -0
  168. package/dist/core/agents/state/shell-mutation.d.ts +17 -0
  169. package/dist/core/agents/state/shell-mutation.d.ts.map +1 -0
  170. package/dist/core/agents/state/shell-mutation.js +41 -0
  171. package/dist/core/agents/state/stale-sweep.d.ts +16 -0
  172. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -0
  173. package/dist/core/agents/state/stale-sweep.js +166 -0
  174. package/dist/core/config.d.ts +29 -0
  175. package/dist/core/config.d.ts.map +1 -0
  176. package/dist/core/config.js +108 -0
  177. package/dist/core/hooks/cli.d.ts +21 -0
  178. package/dist/core/hooks/cli.d.ts.map +1 -0
  179. package/dist/core/hooks/cli.js +1123 -0
  180. package/dist/core/hooks/effects/image-capture.d.ts +43 -0
  181. package/dist/core/hooks/effects/image-capture.d.ts.map +1 -0
  182. package/dist/core/hooks/effects/image-capture.js +288 -0
  183. package/dist/core/hooks/effects/index.d.ts +64 -0
  184. package/dist/core/hooks/effects/index.d.ts.map +1 -0
  185. package/dist/core/hooks/effects/index.js +197 -0
  186. package/dist/core/hooks/events/emit.d.ts +31 -0
  187. package/dist/core/hooks/events/emit.d.ts.map +1 -0
  188. package/dist/core/hooks/events/emit.js +89 -0
  189. package/dist/core/hooks/events/schema.d.ts +235 -0
  190. package/dist/core/hooks/events/schema.d.ts.map +1 -0
  191. package/dist/core/hooks/events/schema.js +12 -0
  192. package/dist/core/hooks/events/ulid.d.ts +10 -0
  193. package/dist/core/hooks/events/ulid.d.ts.map +1 -0
  194. package/dist/core/hooks/events/ulid.js +47 -0
  195. package/dist/core/hooks/harness/detect.d.ts +9 -0
  196. package/dist/core/hooks/harness/detect.d.ts.map +1 -0
  197. package/dist/core/hooks/harness/detect.js +29 -0
  198. package/dist/core/hooks/harness/events.d.ts +45 -0
  199. package/dist/core/hooks/harness/events.d.ts.map +1 -0
  200. package/dist/core/hooks/harness/events.js +71 -0
  201. package/dist/core/hooks/harness/output.d.ts +46 -0
  202. package/dist/core/hooks/harness/output.d.ts.map +1 -0
  203. package/dist/core/hooks/harness/output.js +87 -0
  204. package/dist/core/hooks/harness/parse.d.ts +67 -0
  205. package/dist/core/hooks/harness/parse.d.ts.map +1 -0
  206. package/dist/core/hooks/harness/parse.js +132 -0
  207. package/dist/core/hooks/index.d.ts +8 -0
  208. package/dist/core/hooks/index.d.ts.map +1 -0
  209. package/dist/core/hooks/index.js +7 -0
  210. package/dist/core/hooks/resolve/anchor.d.ts +37 -0
  211. package/dist/core/hooks/resolve/anchor.d.ts.map +1 -0
  212. package/dist/core/hooks/resolve/anchor.js +48 -0
  213. package/dist/core/hooks/resolve/coord-root.d.ts +6 -0
  214. package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -0
  215. package/dist/core/hooks/resolve/coord-root.js +27 -0
  216. package/dist/core/hooks/resolve/intent.d.ts +33 -0
  217. package/dist/core/hooks/resolve/intent.d.ts.map +1 -0
  218. package/dist/core/hooks/resolve/intent.js +79 -0
  219. package/dist/core/hooks/resolve/owner.d.ts +42 -0
  220. package/dist/core/hooks/resolve/owner.d.ts.map +1 -0
  221. package/dist/core/hooks/resolve/owner.js +140 -0
  222. package/dist/core/hooks/resolve/transcript.d.ts +26 -0
  223. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -0
  224. package/dist/core/hooks/resolve/transcript.js +73 -0
  225. package/dist/index.d.ts +15 -0
  226. package/dist/index.d.ts.map +1 -0
  227. package/dist/index.js +13 -0
  228. package/dist/lib/agent-browser/client.d.ts +99 -0
  229. package/dist/lib/agent-browser/client.d.ts.map +1 -0
  230. package/dist/lib/agent-browser/client.js +177 -0
  231. package/dist/lib/agent-browser/index.d.ts +2 -0
  232. package/dist/lib/agent-browser/index.d.ts.map +1 -0
  233. package/dist/lib/agent-browser/index.js +1 -0
  234. package/dist/lib/browser/client.d.ts +193 -0
  235. package/dist/lib/browser/client.d.ts.map +1 -0
  236. package/dist/lib/browser/client.js +325 -0
  237. package/dist/lib/browser/dev-overlay.d.ts +23 -0
  238. package/dist/lib/browser/dev-overlay.d.ts.map +1 -0
  239. package/dist/lib/browser/dev-overlay.js +153 -0
  240. package/dist/lib/browser/index.d.ts +5 -0
  241. package/dist/lib/browser/index.d.ts.map +1 -0
  242. package/dist/lib/browser/index.js +2 -0
  243. package/dist/lib/browser/layout.d.ts +79 -0
  244. package/dist/lib/browser/layout.d.ts.map +1 -0
  245. package/dist/lib/browser/layout.js +220 -0
  246. package/dist/lib/browser/visibility.d.ts +86 -0
  247. package/dist/lib/browser/visibility.d.ts.map +1 -0
  248. package/dist/lib/browser/visibility.js +333 -0
  249. package/dist/lib/browser/visual-diff.d.ts +38 -0
  250. package/dist/lib/browser/visual-diff.d.ts.map +1 -0
  251. package/dist/lib/browser/visual-diff.js +107 -0
  252. package/dist/lib/completion/bash.d.ts +25 -0
  253. package/dist/lib/completion/bash.d.ts.map +1 -0
  254. package/dist/lib/completion/bash.js +284 -0
  255. package/dist/lib/completion/fish.d.ts +16 -0
  256. package/dist/lib/completion/fish.d.ts.map +1 -0
  257. package/dist/lib/completion/fish.js +118 -0
  258. package/dist/lib/completion/index.d.ts +5 -0
  259. package/dist/lib/completion/index.d.ts.map +1 -0
  260. package/dist/lib/completion/index.js +4 -0
  261. package/dist/lib/completion/walk.d.ts +68 -0
  262. package/dist/lib/completion/walk.d.ts.map +1 -0
  263. package/dist/lib/completion/walk.js +102 -0
  264. package/dist/lib/completion/zsh.d.ts +13 -0
  265. package/dist/lib/completion/zsh.d.ts.map +1 -0
  266. package/dist/lib/completion/zsh.js +249 -0
  267. package/dist/lib/context/index.d.ts +107 -0
  268. package/dist/lib/context/index.d.ts.map +1 -0
  269. package/dist/lib/context/index.js +275 -0
  270. package/dist/lib/cookies/client.d.ts +131 -0
  271. package/dist/lib/cookies/client.d.ts.map +1 -0
  272. package/dist/lib/cookies/client.js +239 -0
  273. package/dist/lib/cookies/index.d.ts +2 -0
  274. package/dist/lib/cookies/index.d.ts.map +1 -0
  275. package/dist/lib/cookies/index.js +1 -0
  276. package/dist/lib/council/index.d.ts +266 -0
  277. package/dist/lib/council/index.d.ts.map +1 -0
  278. package/dist/lib/council/index.js +674 -0
  279. package/dist/lib/docs-index.d.ts +28 -0
  280. package/dist/lib/docs-index.d.ts.map +1 -0
  281. package/dist/lib/docs-index.js +169 -0
  282. package/dist/lib/docs-lint.d.ts +26 -0
  283. package/dist/lib/docs-lint.d.ts.map +1 -0
  284. package/dist/lib/docs-lint.js +378 -0
  285. package/dist/lib/docs-sweep.d.ts +34 -0
  286. package/dist/lib/docs-sweep.d.ts.map +1 -0
  287. package/dist/lib/docs-sweep.js +304 -0
  288. package/dist/lib/docs.d.ts +27 -0
  289. package/dist/lib/docs.d.ts.map +1 -0
  290. package/dist/lib/docs.js +142 -0
  291. package/dist/lib/env.d.ts +11 -0
  292. package/dist/lib/env.d.ts.map +1 -0
  293. package/dist/lib/env.js +12 -0
  294. package/dist/lib/exec.d.ts +32 -0
  295. package/dist/lib/exec.d.ts.map +1 -0
  296. package/dist/lib/exec.js +54 -0
  297. package/dist/lib/format.d.ts +29 -0
  298. package/dist/lib/format.d.ts.map +1 -0
  299. package/dist/lib/format.js +139 -0
  300. package/dist/lib/http/client.d.ts +56 -0
  301. package/dist/lib/http/client.d.ts.map +1 -0
  302. package/dist/lib/http/client.js +160 -0
  303. package/dist/lib/http/index.d.ts +2 -0
  304. package/dist/lib/http/index.d.ts.map +1 -0
  305. package/dist/lib/http/index.js +1 -0
  306. package/dist/lib/identities/index.d.ts +77 -0
  307. package/dist/lib/identities/index.d.ts.map +1 -0
  308. package/dist/lib/identities/index.js +190 -0
  309. package/dist/lib/machine.d.ts +19 -0
  310. package/dist/lib/machine.d.ts.map +1 -0
  311. package/dist/lib/machine.js +61 -0
  312. package/dist/lib/presence.d.ts +48 -0
  313. package/dist/lib/presence.d.ts.map +1 -0
  314. package/dist/lib/presence.js +123 -0
  315. package/dist/lib/readability/client.d.ts +39 -0
  316. package/dist/lib/readability/client.d.ts.map +1 -0
  317. package/dist/lib/readability/client.js +121 -0
  318. package/dist/lib/readability/index.d.ts +2 -0
  319. package/dist/lib/readability/index.d.ts.map +1 -0
  320. package/dist/lib/readability/index.js +1 -0
  321. package/dist/lib/scratch/index.d.ts +74 -0
  322. package/dist/lib/scratch/index.d.ts.map +1 -0
  323. package/dist/lib/scratch/index.js +393 -0
  324. package/dist/lib/tunnel/gate.d.ts +12 -0
  325. package/dist/lib/tunnel/gate.d.ts.map +1 -0
  326. package/dist/lib/tunnel/gate.js +101 -0
  327. package/dist/lib/tunnel/state.d.ts +34 -0
  328. package/dist/lib/tunnel/state.d.ts.map +1 -0
  329. package/dist/lib/tunnel/state.js +132 -0
  330. package/package.json +160 -8
  331. package/schemas/.gitkeep +0 -0
  332. package/schemas/config.schema.json +109 -0
  333. package/src/cli.ts +22 -0
  334. package/src/commander.ts +242 -0
  335. package/src/commands/.gitkeep +0 -0
  336. package/src/commands/agents.ts +4567 -0
  337. package/src/commands/backup.ts +305 -0
  338. package/src/commands/browse-ai.ts +198 -0
  339. package/src/commands/browse.ts +849 -0
  340. package/src/commands/callers.ts +363 -0
  341. package/src/commands/completion.ts +193 -0
  342. package/src/commands/config-get.ts +161 -0
  343. package/src/commands/context.ts +209 -0
  344. package/src/commands/cookies.ts +198 -0
  345. package/src/commands/docs.ts +174 -0
  346. package/src/commands/doctor.ts +231 -0
  347. package/src/commands/edit-batch.ts +233 -0
  348. package/src/commands/eml.ts +519 -0
  349. package/src/commands/env.ts +254 -0
  350. package/src/commands/fetch.ts +136 -0
  351. package/src/commands/file-history.ts +202 -0
  352. package/src/commands/grep.ts +371 -0
  353. package/src/commands/init.ts +335 -0
  354. package/src/commands/outline.ts +583 -0
  355. package/src/commands/presence.ts +152 -0
  356. package/src/commands/read.ts +64 -0
  357. package/src/commands/scratch.ts +445 -0
  358. package/src/commands/session.ts +187 -0
  359. package/src/commands/sync.ts +306 -0
  360. package/src/commands/toc.ts +218 -0
  361. package/src/commands/tokens.ts +79 -0
  362. package/src/commands/tunnel.ts +633 -0
  363. package/src/commands/uninstall.ts +144 -0
  364. package/src/commands/web.ts +193 -0
  365. package/src/core/agents/canonical-emit.ts +77 -0
  366. package/src/core/agents/cli-emit.ts +64 -0
  367. package/src/core/agents/cli.ts +838 -0
  368. package/src/core/agents/codex-replay.ts +163 -0
  369. package/src/core/agents/coord-client.ts +249 -0
  370. package/src/core/agents/events/consume.ts +196 -0
  371. package/src/core/agents/events/emit.ts +108 -0
  372. package/src/core/agents/events/ulid.ts +51 -0
  373. package/src/core/agents/index.ts +14 -0
  374. package/src/core/agents/paths.ts +16 -0
  375. package/src/core/agents/render/prompt-context.ts +401 -0
  376. package/src/core/agents/render/session-context.ts +341 -0
  377. package/src/core/agents/rules/claim-conflict.ts +282 -0
  378. package/src/core/agents/rules/commit-conflict.ts +303 -0
  379. package/src/core/agents/rules/stop-hook.ts +229 -0
  380. package/src/core/agents/session-events.ts +228 -0
  381. package/src/core/agents/state/activity-log.ts +33 -0
  382. package/src/core/agents/state/council.ts +265 -0
  383. package/src/core/agents/state/heartbeat-projector.ts +488 -0
  384. package/src/core/agents/state/heartbeat-writer.ts +333 -0
  385. package/src/core/agents/state/names.ts +399 -0
  386. package/src/core/agents/state/pidmap.ts +38 -0
  387. package/src/core/agents/state/scratch.ts +121 -0
  388. package/src/core/agents/state/shell-mutation.ts +44 -0
  389. package/src/core/agents/state/stale-sweep.ts +190 -0
  390. package/src/core/config.ts +111 -0
  391. package/src/core/hooks/cli.ts +1247 -0
  392. package/src/core/hooks/effects/image-capture.ts +330 -0
  393. package/src/core/hooks/effects/index.ts +210 -0
  394. package/src/core/hooks/events/emit.ts +120 -0
  395. package/src/core/hooks/events/schema.ts +430 -0
  396. package/src/core/hooks/events/ulid.ts +51 -0
  397. package/src/core/hooks/harness/detect.ts +30 -0
  398. package/src/core/hooks/harness/events.ts +102 -0
  399. package/src/core/hooks/harness/output.ts +100 -0
  400. package/src/core/hooks/harness/parse.ts +180 -0
  401. package/src/core/hooks/index.ts +16 -0
  402. package/src/core/hooks/resolve/anchor.ts +51 -0
  403. package/src/core/hooks/resolve/coord-root.ts +25 -0
  404. package/src/core/hooks/resolve/intent.ts +89 -0
  405. package/src/core/hooks/resolve/owner.ts +140 -0
  406. package/src/core/hooks/resolve/transcript.ts +72 -0
  407. package/src/hooks/.gitkeep +0 -0
  408. package/src/index.ts +15 -0
  409. package/src/lib/agent-browser/client.ts +239 -0
  410. package/src/lib/agent-browser/index.ts +1 -0
  411. package/src/lib/browser/client.ts +449 -0
  412. package/src/lib/browser/dev-overlay.ts +207 -0
  413. package/src/lib/browser/index.ts +24 -0
  414. package/src/lib/browser/layout.ts +288 -0
  415. package/src/lib/browser/visibility.ts +419 -0
  416. package/src/lib/browser/visual-diff.ts +150 -0
  417. package/src/lib/completion/bash.ts +291 -0
  418. package/src/lib/completion/fish.ts +134 -0
  419. package/src/lib/completion/index.ts +10 -0
  420. package/src/lib/completion/walk.ts +184 -0
  421. package/src/lib/completion/zsh.ts +262 -0
  422. package/src/lib/context/index.ts +386 -0
  423. package/src/lib/cookies/client.ts +301 -0
  424. package/src/lib/cookies/index.ts +13 -0
  425. package/src/lib/council/index.ts +803 -0
  426. package/src/lib/docs-index.ts +216 -0
  427. package/src/lib/docs-lint.ts +413 -0
  428. package/src/lib/docs-sweep.ts +348 -0
  429. package/src/lib/docs.ts +199 -0
  430. package/src/lib/env.ts +12 -0
  431. package/src/lib/exec.ts +74 -0
  432. package/src/lib/format.ts +147 -0
  433. package/src/lib/http/client.ts +211 -0
  434. package/src/lib/http/index.ts +1 -0
  435. package/src/lib/identities/index.ts +210 -0
  436. package/src/lib/machine.ts +61 -0
  437. package/src/lib/presence.ts +154 -0
  438. package/src/lib/readability/client.ts +169 -0
  439. package/src/lib/readability/index.ts +5 -0
  440. package/src/lib/readability/turndown-plugin-gfm.d.ts +10 -0
  441. package/src/lib/scratch/index.ts +470 -0
  442. package/src/lib/tunnel/gate.ts +113 -0
  443. package/src/lib/tunnel/state.ts +167 -0
  444. package/src/web/.gitkeep +0 -0
  445. package/index.js +0 -1
@@ -0,0 +1,79 @@
1
+ import { readFileSync } from "node:fs";
2
+ import type { Command } from "commander";
3
+ import { countTokens } from "gpt-tokenizer/model/gpt-4o";
4
+ import type { EmitContext } from "../commander.ts";
5
+
6
+ /**
7
+ * `tokens`: count tokens in text/markdown files.
8
+ *
9
+ * Uses OpenAI's `o200k_base` BPE tokenizer (via `gpt-tokenizer`) as a proxy
10
+ * for Claude's tokenizer. Claude doesn't publish a local tokenizer, but for
11
+ * English markdown the two agree to within ~3-5%. Calls
12
+ * `emit.config()/data()/setExitCode()` via the injected EmitContext so the
13
+ * same code path serves both standalone and composed consumers.
14
+ */
15
+
16
+ interface TokenResult {
17
+ path: string;
18
+ chars: number;
19
+ tokens: number;
20
+ error: string | null;
21
+ }
22
+
23
+ interface TokensOpts {
24
+ limit?: number;
25
+ format: string;
26
+ }
27
+
28
+ export function registerTokensCommand(program: Command, emit: EmitContext): void {
29
+ program
30
+ .command("tokens")
31
+ .description("Count tokens in text/markdown files (offline, uses o200k_base as Claude proxy)")
32
+ .argument("<files...>", "One or more file paths (globs are shell-expanded)")
33
+ .option("--limit <n>", "Flag files exceeding N tokens; exit non-zero if any exceed", (v) =>
34
+ Number.parseInt(v, 10),
35
+ )
36
+ .option("--format <type>", "Output format: table, json, plain", "table")
37
+ .action((files: string[], opts: TokensOpts) => {
38
+ if (opts.format === "json") emit.config({ format: "json" });
39
+
40
+ const results = files.map(countFile);
41
+ const over =
42
+ opts.limit !== undefined
43
+ ? results.filter((r) => !r.error && r.tokens > (opts.limit as number))
44
+ : [];
45
+
46
+ const enriched = results.map((r) => ({
47
+ path: r.path,
48
+ chars: r.chars,
49
+ tokens: r.tokens,
50
+ error: r.error,
51
+ over_limit: opts.limit !== undefined && !r.error ? r.tokens > opts.limit : null,
52
+ pct_of_limit:
53
+ opts.limit !== undefined && !r.error
54
+ ? Math.round((r.tokens / (opts.limit as number)) * 100)
55
+ : null,
56
+ }));
57
+
58
+ emit.data({
59
+ ok: true,
60
+ tokenizer: "o200k_base",
61
+ limit: opts.limit ?? null,
62
+ count: enriched.length,
63
+ over_count: over.length,
64
+ results: enriched,
65
+ });
66
+
67
+ if (over.length > 0) emit.setExitCode(1);
68
+ });
69
+ }
70
+
71
+ function countFile(path: string): TokenResult {
72
+ try {
73
+ const content = readFileSync(path, "utf8");
74
+ return { path, chars: content.length, tokens: countTokens(content), error: null };
75
+ } catch (err) {
76
+ const msg = err instanceof Error ? err.message : String(err);
77
+ return { path, chars: 0, tokens: 0, error: msg };
78
+ }
79
+ }
@@ -0,0 +1,633 @@
1
+ import type { Command } from "commander";
2
+ import type { EmitContext, HarneryProgramContext } from "../commander.ts";
3
+
4
+ // Inline cachePath; see lib/tunnel/state.ts for rationale.
5
+ function cachePath(tool: string, filename: string): string {
6
+ const dir = resolve(process.cwd(), ".cache", tool);
7
+ return resolve(dir, filename);
8
+ }
9
+
10
+ import { spawn, spawnSync } from "node:child_process";
11
+ import { existsSync, openSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { resolve } from "node:path";
13
+
14
+ import {
15
+ cfdLogFile,
16
+ clearState,
17
+ DEFAULT_INSTANCE,
18
+ ensureCloudflared,
19
+ gateLogFile,
20
+ isProcessAlive,
21
+ listStates,
22
+ readConfig,
23
+ readState,
24
+ type TunnelState,
25
+ writeConfig,
26
+ writeState,
27
+ } from "../lib/tunnel/state.ts";
28
+
29
+ /**
30
+ * `tunnel`: IP-gated Cloudflare quick tunnel in front of a local upstream.
31
+ *
32
+ * Two-process design: a Bun reverse-proxy worker (lib/tunnel/gate.ts) checks
33
+ * the Cloudflare-set `CF-Connecting-IP` header against an allowlist and
34
+ * rewrites Host for the upstream; cloudflared --url then exposes the gate
35
+ * as a random `<words>.trycloudflare.com` hostname.
36
+ *
37
+ * State + config persisted under `.cache/tunnel/`. cloudflared auto-installs
38
+ * to ~/.local/bin/ on first run (Linux only; macOS users `brew install`).
39
+ */
40
+
41
+ const DEFAULT_TARGET = "127.0.0.1:8001";
42
+ const DEFAULT_VHOST = "localhost";
43
+ const DEFAULT_GATE_PORT = 9001;
44
+ const MAX_GATE_PORT = DEFAULT_GATE_PORT + 99; // auto-allocation scan ceiling
45
+
46
+ interface UpOpts {
47
+ name?: string;
48
+ target?: string;
49
+ vhost?: string;
50
+ gatePort?: string;
51
+ }
52
+
53
+ interface DownOpts {
54
+ name?: string;
55
+ all?: boolean;
56
+ }
57
+
58
+ interface StatusOpts {
59
+ name?: string;
60
+ }
61
+
62
+ interface LogsOpts {
63
+ name?: string;
64
+ follow?: boolean;
65
+ gate?: boolean;
66
+ cloudflared?: boolean;
67
+ }
68
+
69
+ /**
70
+ * Validate + normalize an instance name. Names become filename fragments
71
+ * (state-<name>.json) and pgrep patterns, so they're restricted to a safe
72
+ * charset. Throws a friendly emit.error + exits on a bad name.
73
+ */
74
+ function resolveName(raw: string | undefined): string {
75
+ const name = (raw ?? DEFAULT_INSTANCE).trim();
76
+ if (!/^[a-z0-9][a-z0-9-]*$/i.test(name)) {
77
+ emit.error({
78
+ code: "tunnel_bad_name",
79
+ message: `Invalid instance name "${name}". Use letters, digits, and dashes (must start alphanumeric).`,
80
+ });
81
+ process.exit(1);
82
+ }
83
+ return name;
84
+ }
85
+
86
+ function gateScriptPath(): string {
87
+ return resolve(import.meta.dirname, "..", "lib", "tunnel", "gate.ts");
88
+ }
89
+
90
+ /**
91
+ * `harn tunnel` is the one command that hard-requires Bun: the gate worker is a
92
+ * `Bun.serve` process (HTTP + WebSocket reverse proxy), spawned as `bun run
93
+ * gate.ts`. Everything else in harnery runs on Node, but this can't until the
94
+ * gate is ported off `Bun.serve` (node:http + `ws`). Detect Bun up front so the
95
+ * failure is a clear message rather than an opaque ENOENT from the gate spawn.
96
+ */
97
+ function bunAvailable(): boolean {
98
+ return spawnSync("bun", ["--version"], { stdio: "ignore" }).status === 0;
99
+ }
100
+
101
+ function sleep(ms: number): Promise<void> {
102
+ return new Promise((r) => setTimeout(r, ms));
103
+ }
104
+
105
+ /**
106
+ * Kill every process whose command line matches `pattern` (via `pgrep -f`),
107
+ * skipping our own PID and any already-killed. Returns the count killed. Used
108
+ * as a fallback so orphaned gate/cloudflared processes get cleaned even when
109
+ * the state file was lost (which otherwise left them squatting on the port).
110
+ */
111
+ function killByPattern(pattern: string, alreadyKilled: Set<number>): number {
112
+ const r = spawnSync("pgrep", ["-f", pattern], { encoding: "utf-8" });
113
+ if (r.status !== 0 || typeof r.stdout !== "string") return 0;
114
+ let killed = 0;
115
+ for (const line of r.stdout.split("\n")) {
116
+ const pid = Number(line.trim());
117
+ if (!pid || pid === process.pid || alreadyKilled.has(pid)) continue;
118
+ try {
119
+ process.kill(pid);
120
+ alreadyKilled.add(pid);
121
+ killed++;
122
+ } catch {
123
+ /* race: already gone */
124
+ }
125
+ }
126
+ return killed;
127
+ }
128
+
129
+ /**
130
+ * Sweep stray gate + cloudflared processes for ONE instance, identified by its
131
+ * gate port. Both signatures are port-scoped so tearing down one tunnel never
132
+ * touches another:
133
+ * - gate: `gate.ts ... --port <port>` (the port is on the gate's argv)
134
+ * - cloudflared: `--url http://localhost:<port>` (order-independent, so it
135
+ * matches regardless of the `--protocol http2` flag we also pass).
136
+ * Port boundary is guarded with `( |$)` so port 9001 doesn't match 90011.
137
+ */
138
+ function sweepStrays(gatePort: number, alreadyKilled: Set<number>): number {
139
+ return (
140
+ killByPattern(`gate\\.ts.*--port ${gatePort}( |$)`, alreadyKilled) +
141
+ killByPattern(`--url http://localhost:${gatePort}( |$)`, alreadyKilled)
142
+ );
143
+ }
144
+
145
+ /** Ports currently bound by a LISTEN socket (best-effort via `ss`). */
146
+ function listeningPorts(): Set<number> {
147
+ const ports = new Set<number>();
148
+ const r = spawnSync("ss", ["-tlnH"], { encoding: "utf-8" });
149
+ if (r.status === 0 && typeof r.stdout === "string") {
150
+ for (const m of r.stdout.matchAll(/:(\d+)\s/g)) ports.add(Number(m[1]));
151
+ }
152
+ return ports;
153
+ }
154
+
155
+ /**
156
+ * Pick a gate port for a new instance. An explicit `--gate-port` is honored
157
+ * (and rejected if it's already taken); otherwise scan upward from 9001 for the
158
+ * first port that's neither held by a live instance nor currently listening.
159
+ */
160
+ function allocateGatePort(preferred: number | undefined): number {
161
+ const used = new Set<number>(
162
+ listStates()
163
+ .filter((s) => isProcessAlive(s.gate_pid))
164
+ .map((s) => s.gate_port),
165
+ );
166
+ const listening = listeningPorts();
167
+ const taken = (p: number) => used.has(p) || listening.has(p);
168
+
169
+ if (preferred !== undefined) {
170
+ if (taken(preferred)) {
171
+ emit.error({
172
+ code: "tunnel_port_taken",
173
+ message: `Gate port ${preferred} is already in use. Omit --gate-port to auto-allocate, or pick a free one.`,
174
+ });
175
+ process.exit(1);
176
+ }
177
+ return preferred;
178
+ }
179
+ for (let p = DEFAULT_GATE_PORT; p <= MAX_GATE_PORT; p++) {
180
+ if (!taken(p)) return p;
181
+ }
182
+ emit.error({
183
+ code: "tunnel_no_free_port",
184
+ message: `No free gate port in ${DEFAULT_GATE_PORT}-${MAX_GATE_PORT}. Tear down some tunnels first.`,
185
+ });
186
+ process.exit(1);
187
+ }
188
+
189
+ function extractUrl(log: string): string | null {
190
+ const m = log.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
191
+ return m ? m[0] : null;
192
+ }
193
+
194
+ /**
195
+ * cloudflared logs "Registered tunnel connection" once the edge is live and
196
+ * routable. Match that exact line only, because earlier lines carry `connIndex=`
197
+ * too (e.g. "Tunnel connection curve preferences … connIndex=0"), which would
198
+ * false-positive readiness before the connection actually registers.
199
+ */
200
+ function isRegistered(log: string): boolean {
201
+ return /Registered tunnel connection/.test(log);
202
+ }
203
+
204
+ /**
205
+ * Wait for the tunnel to be genuinely usable. cloudflared prints the
206
+ * `*.trycloudflare.com` URL early (at precheck) but the hostname doesn't route
207
+ * until the edge connection is *registered*, a few seconds later, and
208
+ * occasionally never on a wedged QUIC start. We gate readiness on the
209
+ * registration line, not just the URL, so `up` doesn't hand back a URL that
210
+ * 404s/times out. Returns the URL (if seen at all) plus whether it registered.
211
+ */
212
+ async function waitForReady(
213
+ logPath: string,
214
+ timeoutMs: number,
215
+ ): Promise<{ url: string | null; registered: boolean }> {
216
+ const deadline = Date.now() + timeoutMs;
217
+ let url: string | null = null;
218
+ while (Date.now() < deadline) {
219
+ if (existsSync(logPath)) {
220
+ const log = readFileSync(logPath, "utf-8");
221
+ url = url ?? extractUrl(log);
222
+ if (url && isRegistered(log)) return { url, registered: true };
223
+ }
224
+ await sleep(500);
225
+ }
226
+ return { url, registered: false };
227
+ }
228
+
229
+ /** Resolve the context-supplied default vhost (literal or lazy resolver). */
230
+ function contextVhost(): string | null {
231
+ const v = context?.tunnelDefaultVhost;
232
+ const resolved = typeof v === "function" ? v() : v;
233
+ return resolved ?? null;
234
+ }
235
+
236
+ async function up(opts: UpOpts): Promise<void> {
237
+ if (!bunAvailable()) {
238
+ emit.error({
239
+ code: "tunnel_requires_bun",
240
+ message:
241
+ "harn tunnel requires Bun: the gate worker is a Bun.serve process. " +
242
+ "Install Bun (https://bun.sh) and re-run. (Every other harn command runs on Node.)",
243
+ });
244
+ process.exit(1);
245
+ }
246
+ const name = resolveName(opts.name);
247
+ const target = opts.target ?? DEFAULT_TARGET;
248
+ // Precedence: explicit --vhost > the consumer's configured default (via
249
+ // context.tunnelDefaultVhost) > "localhost".
250
+ const vhost = opts.vhost ?? contextVhost() ?? DEFAULT_VHOST;
251
+
252
+ const existing = readState(name);
253
+ if (existing && isProcessAlive(existing.gate_pid) && isProcessAlive(existing.cloudflared_pid)) {
254
+ emit.text(`Already up [${name}]: ${existing.url}\n`);
255
+ emit.text(` Forwarding: ${existing.target} (Host: ${existing.vhost})\n`);
256
+ return;
257
+ }
258
+ if (existing) clearState(name);
259
+
260
+ // Allocate the gate port (after clearing dead state so its old port frees up
261
+ // for reuse). Explicit --gate-port is validated; otherwise auto-scan.
262
+ const gatePort = allocateGatePort(opts.gatePort ? Number(opts.gatePort) : undefined);
263
+
264
+ // Self-heal: clear any orphaned gate/cloudflared on THIS instance's port from
265
+ // a prior crashed or state-cleared run so the gate port is free before we bind.
266
+ if (sweepStrays(gatePort, new Set())) await sleep(500);
267
+
268
+ const cfg = readConfig();
269
+ if (cfg.allowed_ips.length === 0) {
270
+ emit.error({
271
+ code: "tunnel_allowlist_empty",
272
+ message: "Allowlist is empty; refusing to start. Add an IP first: harn tunnel allow add <ip>",
273
+ });
274
+ process.exit(1);
275
+ }
276
+
277
+ const cloudflaredBin = ensureCloudflared();
278
+
279
+ const gateLogPath = cachePath("tunnel", gateLogFile(name));
280
+ const cfdLogPath = cachePath("tunnel", cfdLogFile(name));
281
+ writeFileSync(gateLogPath, "");
282
+ writeFileSync(cfdLogPath, "");
283
+
284
+ const gateFd = openSync(gateLogPath, "a");
285
+ // `--name`/`--port` on argv mirror the env vars; they're what makes the gate
286
+ // process distinguishable per-instance in `pgrep -f` (see sweepStrays).
287
+ const gateProc = spawn(
288
+ "bun",
289
+ ["run", gateScriptPath(), "--name", name, "--port", String(gatePort)],
290
+ {
291
+ detached: true,
292
+ stdio: ["ignore", gateFd, gateFd],
293
+ env: {
294
+ ...process.env,
295
+ HARNERY_TUNNEL_ALLOW: cfg.allowed_ips.join(","),
296
+ HARNERY_TUNNEL_TARGET: target,
297
+ HARNERY_TUNNEL_VHOST: vhost,
298
+ HARNERY_TUNNEL_PORT: String(gatePort),
299
+ },
300
+ },
301
+ );
302
+ gateProc.unref();
303
+
304
+ await sleep(800);
305
+
306
+ if (!isProcessAlive(gateProc.pid!)) {
307
+ emit.error({
308
+ code: "tunnel_gate_failed",
309
+ message: `Gate failed to start. Check log: ${gateLogPath}`,
310
+ });
311
+ process.exit(1);
312
+ }
313
+
314
+ const cfdFd = openSync(cfdLogPath, "a");
315
+ // Force HTTP/2 transport. The default QUIC transport wedges at precheck on
316
+ // constrained hosts (e.g. WSL, where UDP receive buffers can't grow and ICMP
317
+ // is restricted): the URL prints but the edge never registers. HTTP/2 is
318
+ // marginally higher-latency but registers reliably, which is what a dev
319
+ // tunnel needs.
320
+ const cfdProc = spawn(
321
+ cloudflaredBin,
322
+ ["tunnel", "--protocol", "http2", "--url", `http://localhost:${gatePort}`],
323
+ {
324
+ detached: true,
325
+ stdio: ["ignore", cfdFd, cfdFd],
326
+ },
327
+ );
328
+ cfdProc.unref();
329
+
330
+ const { url, registered } = await waitForReady(cfdLogPath, 30_000);
331
+ if (!url) {
332
+ try {
333
+ process.kill(gateProc.pid!);
334
+ } catch {
335
+ /* already dead */
336
+ }
337
+ try {
338
+ process.kill(cfdProc.pid!);
339
+ } catch {
340
+ /* already dead */
341
+ }
342
+ emit.error({
343
+ code: "tunnel_url_timeout",
344
+ message: `Failed to obtain tunnel URL within 30s. Check log: ${cfdLogPath}`,
345
+ });
346
+ process.exit(1);
347
+ }
348
+
349
+ const state: TunnelState = {
350
+ name,
351
+ url,
352
+ gate_pid: gateProc.pid!,
353
+ cloudflared_pid: cfdProc.pid!,
354
+ started_at: new Date().toISOString(),
355
+ target,
356
+ vhost,
357
+ gate_port: gatePort,
358
+ };
359
+ writeState(state);
360
+
361
+ const stopHint =
362
+ name === DEFAULT_INSTANCE ? "harn tunnel down" : `harn tunnel down --name ${name}`;
363
+ emit.text(`\n Instance: ${name}\n`);
364
+ emit.text(` URL: ${url}\n\n`);
365
+ emit.text(` Forwarding: ${target} (Host: ${vhost})\n`);
366
+ emit.text(` Gate port: ${gatePort}\n`);
367
+ emit.text(` Allowed IPs: ${cfg.allowed_ips.join(", ")}\n\n`);
368
+ if (!registered) {
369
+ emit.text(
370
+ ` ⚠ Edge connection didn't register within 30s (QUIC can wedge on a cold\n start). If the URL 404s or times out, bounce it: ${stopHint} && harn tunnel up\n\n`,
371
+ );
372
+ }
373
+ emit.text(` Stop: ${stopHint}\n`);
374
+ emit.text(" Status: harn tunnel status\n");
375
+ }
376
+
377
+ /** Tear down a single instance by name. Returns the number of processes killed. */
378
+ function downOne(name: string, killed: Set<number>): number {
379
+ const before = killed.size;
380
+ const state = readState(name);
381
+ if (state) {
382
+ for (const pid of [state.gate_pid, state.cloudflared_pid]) {
383
+ if (isProcessAlive(pid)) {
384
+ try {
385
+ process.kill(pid);
386
+ killed.add(pid);
387
+ } catch {
388
+ /* race: already gone */
389
+ }
390
+ }
391
+ }
392
+ }
393
+ // Fallback: sweep orphans on this instance's gate port, even when state was
394
+ // lost; they'd otherwise squat on the port and break the next `up`.
395
+ sweepStrays(state?.gate_port ?? DEFAULT_GATE_PORT, killed);
396
+ clearState(name);
397
+ return killed.size - before;
398
+ }
399
+
400
+ function down(opts: DownOpts): void {
401
+ const killed = new Set<number>();
402
+
403
+ if (opts.all) {
404
+ const states = listStates();
405
+ if (states.length === 0) {
406
+ emit.text("No tunnels up. Nothing to stop.\n");
407
+ return;
408
+ }
409
+ for (const s of states) downOne(s.name, killed);
410
+ emit.text(
411
+ `Stopped ${states.length} tunnel(s) [${states.map((s) => s.name).join(", ")}], ${killed.size} process(es).\n`,
412
+ );
413
+ return;
414
+ }
415
+
416
+ const name = resolveName(opts.name);
417
+ // Bare `down` targets the default instance. If it's not up but named ones
418
+ // are, don't silently no-op; point the operator at them.
419
+ if (name === DEFAULT_INSTANCE && !readState(DEFAULT_INSTANCE)) {
420
+ const others = listStates();
421
+ if (others.length > 0) {
422
+ emit.text(
423
+ `No default tunnel running. Other tunnels up: ${others.map((s) => s.name).join(", ")}.\nUse \`harn tunnel down --name <name>\` or \`harn tunnel down --all\`.\n`,
424
+ );
425
+ return;
426
+ }
427
+ }
428
+
429
+ downOne(name, killed);
430
+ emit.text(
431
+ killed.size === 0
432
+ ? `No tunnel processes found for [${name}]. Nothing to stop.\n`
433
+ : `Stopped ${killed.size} process(es). Tunnel [${name}] down.\n`,
434
+ );
435
+ }
436
+
437
+ function instanceState(state: TunnelState): "up" | "stale" {
438
+ return isProcessAlive(state.gate_pid) && isProcessAlive(state.cloudflared_pid) ? "up" : "stale";
439
+ }
440
+
441
+ function fmtUptime(startedAt: string): string {
442
+ const secs = Math.floor((Date.now() - new Date(startedAt).getTime()) / 1000);
443
+ if (!Number.isFinite(secs) || secs < 0) return "?";
444
+ if (secs < 60) return `${secs}s`;
445
+ if (secs < 3600) return `${Math.floor(secs / 60)}m`;
446
+ return `${Math.floor(secs / 3600)}h${Math.floor((secs % 3600) / 60)}m`;
447
+ }
448
+
449
+ /** Detailed single-instance block (the pre-multi-instance format). */
450
+ function statusDetail(state: TunnelState): void {
451
+ const gateAlive = isProcessAlive(state.gate_pid);
452
+ const cfdAlive = isProcessAlive(state.cloudflared_pid);
453
+ const cfg = readConfig();
454
+ emit.text(`${gateAlive && cfdAlive ? "up" : "stale"} [${state.name}]\n`);
455
+ emit.text(` URL: ${state.url}\n`);
456
+ emit.text(` Forwarding: ${state.target} (Host: ${state.vhost})\n`);
457
+ emit.text(` Gate port: ${state.gate_port}\n`);
458
+ emit.text(` Allowed IPs: ${cfg.allowed_ips.join(", ")}\n`);
459
+ emit.text(` Gate PID: ${state.gate_pid}${gateAlive ? "" : " (DEAD)"}\n`);
460
+ emit.text(` CFD PID: ${state.cloudflared_pid}${cfdAlive ? "" : " (DEAD)"}\n`);
461
+ emit.text(` Uptime: ${fmtUptime(state.started_at)}\n`);
462
+ }
463
+
464
+ function status(opts: StatusOpts): void {
465
+ // Named → detailed single block.
466
+ if (opts.name) {
467
+ const state = readState(resolveName(opts.name));
468
+ if (!state) {
469
+ emit.text(`down [${resolveName(opts.name)}]\n`);
470
+ return;
471
+ }
472
+ statusDetail(state);
473
+ return;
474
+ }
475
+
476
+ // No name → table of every instance.
477
+ const states = listStates();
478
+ if (states.length === 0) {
479
+ emit.text("down\n");
480
+ return;
481
+ }
482
+ if (states.length === 1) {
483
+ // Single tunnel: show the full detail block (backward-compatible).
484
+ statusDetail(states[0]);
485
+ return;
486
+ }
487
+
488
+ const rows = states.map((s) => ({
489
+ name: s.name,
490
+ state: instanceState(s),
491
+ url: s.url,
492
+ fwd: `${s.target} (${s.vhost})`,
493
+ port: String(s.gate_port),
494
+ up: fmtUptime(s.started_at),
495
+ }));
496
+ const w = {
497
+ name: Math.max(4, ...rows.map((r) => r.name.length)),
498
+ state: 5,
499
+ url: Math.max(3, ...rows.map((r) => r.url.length)),
500
+ fwd: Math.max(10, ...rows.map((r) => r.fwd.length)),
501
+ port: 4,
502
+ };
503
+ const pad = (s: string, n: number) => s.padEnd(n);
504
+ emit.text(
505
+ `${pad("NAME", w.name)} ${pad("STATE", w.state)} ${pad("URL", w.url)} ${pad("FORWARDING", w.fwd)} ${pad("PORT", w.port)} UPTIME\n`,
506
+ );
507
+ for (const r of rows) {
508
+ emit.text(
509
+ `${pad(r.name, w.name)} ${pad(r.state, w.state)} ${pad(r.url, w.url)} ${pad(r.fwd, w.fwd)} ${pad(r.port, w.port)} ${r.up}\n`,
510
+ );
511
+ }
512
+ }
513
+
514
+ function logs(opts: LogsOpts): void {
515
+ const name = resolveName(opts.name);
516
+ const which = opts.cloudflared ? cfdLogFile(name) : gateLogFile(name);
517
+ const path = cachePath("tunnel", which);
518
+ if (!existsSync(path)) {
519
+ emit.error({ code: "tunnel_no_log", message: `No log file at ${path}` });
520
+ process.exit(1);
521
+ }
522
+ const args = opts.follow ? ["-f", path] : [path];
523
+ const r = spawnSync("tail", args, { stdio: "inherit" });
524
+ if (r.status !== null && r.status !== 0) process.exit(r.status);
525
+ }
526
+
527
+ function allowList(): void {
528
+ const cfg = readConfig();
529
+ if (cfg.allowed_ips.length === 0) {
530
+ emit.text("(empty)\n");
531
+ return;
532
+ }
533
+ for (const ip of cfg.allowed_ips) emit.text(`${ip}\n`);
534
+ }
535
+
536
+ function allowAdd(ip: string): void {
537
+ const cfg = readConfig();
538
+ if (cfg.allowed_ips.includes(ip)) {
539
+ emit.text(`${ip} already in allowlist.\n`);
540
+ return;
541
+ }
542
+ cfg.allowed_ips.push(ip);
543
+ writeConfig(cfg);
544
+ emit.text(`Added ${ip}.\n`);
545
+ const up = listStates();
546
+ if (up.length > 0) {
547
+ emit.text(
548
+ `Allowlist is shared across all tunnels; restart each to apply (${up.map((s) => s.name).join(", ")}).\n`,
549
+ );
550
+ }
551
+ }
552
+
553
+ function allowRm(ip: string): void {
554
+ const cfg = readConfig();
555
+ const idx = cfg.allowed_ips.indexOf(ip);
556
+ if (idx === -1) {
557
+ emit.text(`${ip} not in allowlist.\n`);
558
+ return;
559
+ }
560
+ cfg.allowed_ips.splice(idx, 1);
561
+ writeConfig(cfg);
562
+ emit.text(`Removed ${ip}.\n`);
563
+ const up = listStates();
564
+ if (up.length > 0) {
565
+ emit.text(
566
+ `Allowlist is shared across all tunnels; restart each to apply (${up.map((s) => s.name).join(", ")}).\n`,
567
+ );
568
+ }
569
+ }
570
+
571
+ let emit: EmitContext;
572
+ let context: HarneryProgramContext | undefined;
573
+
574
+ export function registerTunnelCommand(
575
+ program: Command,
576
+ emitParam: EmitContext,
577
+ contextParam?: HarneryProgramContext,
578
+ ): void {
579
+ emit = emitParam;
580
+ context = contextParam;
581
+ const cmd = program
582
+ .command("tunnel")
583
+ .description(
584
+ "IP-gated Cloudflare quick tunnel(s) in front of a local upstream (default upstream: 127.0.0.1:8001). " +
585
+ "Run several at once with --name <instance>.",
586
+ );
587
+
588
+ cmd
589
+ .command("up")
590
+ .description("Start a gate + cloudflared tunnel (one per --name instance)")
591
+ .option("--name <name>", "instance name; run multiple tunnels side by side", DEFAULT_INSTANCE)
592
+ .option("--target <addr>", "upstream to forward to", DEFAULT_TARGET)
593
+ .option(
594
+ "--vhost <host>",
595
+ "Host header sent to the upstream (default: the consumer's configured " +
596
+ "default, else localhost).",
597
+ )
598
+ .option(
599
+ "--gate-port <port>",
600
+ "local port the gate binds to (default: auto-allocate the first free port from 9001)",
601
+ )
602
+ .action(up);
603
+
604
+ cmd
605
+ .command("down")
606
+ .description("Stop a tunnel (default instance, --name <instance>, or --all)")
607
+ .option("--name <name>", "instance to stop", DEFAULT_INSTANCE)
608
+ .option("--all", "stop every running tunnel")
609
+ .action(down);
610
+
611
+ cmd
612
+ .command("status")
613
+ .description("Show tunnel state: a table of all instances, or detail for one via --name")
614
+ .option("--name <name>", "show detail for a single instance")
615
+ .action(status);
616
+
617
+ cmd
618
+ .command("logs")
619
+ .description("Tail the gate log (default) or cloudflared log for an instance")
620
+ .option("--name <name>", "instance whose log to tail", DEFAULT_INSTANCE)
621
+ .option("-f, --follow", "follow the log")
622
+ .option("--gate", "tail the gate log (default)")
623
+ .option("--cloudflared", "tail the cloudflared log instead")
624
+ .action(logs);
625
+
626
+ const allow = cmd
627
+ .command("allow")
628
+ .description("Manage the CF-Connecting-IP allowlist")
629
+ .action(allowList);
630
+ allow.command("add <ip>").description("Add an IP to the allowlist").action(allowAdd);
631
+ allow.command("rm <ip>").description("Remove an IP from the allowlist").action(allowRm);
632
+ allow.command("list").description("List allowed IPs (default action)").action(allowList);
633
+ }