borgmcp 1.1.15 → 2.0.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 (459) hide show
  1. package/CONTRIBUTING.md +15 -0
  2. package/LICENSE +201 -33
  3. package/NOTICE +2 -0
  4. package/README.md +85 -36
  5. package/SECURITY.md +7 -0
  6. package/dist/agent-runtime.d.ts.map +1 -0
  7. package/dist/agent-runtime.js +63 -1
  8. package/dist/agent-runtime.js.map +1 -0
  9. package/dist/assimilate-cmd.d.ts +96 -17
  10. package/dist/assimilate-cmd.d.ts.map +1 -0
  11. package/dist/assimilate-cmd.js +1396 -48
  12. package/dist/assimilate-cmd.js.map +1 -0
  13. package/dist/assimilate-deps.d.ts.map +1 -0
  14. package/dist/assimilate-deps.js +384 -3
  15. package/dist/assimilate-deps.js.map +1 -0
  16. package/dist/assimilate-guard.d.ts +5 -7
  17. package/dist/assimilate-guard.d.ts.map +1 -0
  18. package/dist/assimilate-guard.js +60 -2
  19. package/dist/assimilate-guard.js.map +1 -0
  20. package/dist/assimilate-welcome.d.ts +1 -1
  21. package/dist/assimilate-welcome.d.ts.map +1 -0
  22. package/dist/assimilate-welcome.js +32 -2
  23. package/dist/assimilate-welcome.js.map +1 -0
  24. package/dist/auth-env.d.ts +7 -50
  25. package/dist/auth-env.d.ts.map +1 -0
  26. package/dist/auth-env.js +22 -1
  27. package/dist/auth-env.js.map +1 -0
  28. package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
  29. package/dist/backends/launch-all-pastelist.js +11 -4
  30. package/dist/backends/launch-all-pastelist.js.map +1 -0
  31. package/dist/backends/launch-all-tmux.d.ts.map +1 -0
  32. package/dist/backends/launch-all-tmux.js +49 -1
  33. package/dist/backends/launch-all-tmux.js.map +1 -0
  34. package/dist/backends/launch-all-windows.d.ts.map +1 -0
  35. package/dist/backends/launch-all-windows.js +86 -12
  36. package/dist/backends/launch-all-windows.js.map +1 -0
  37. package/dist/bare-launch-menu.d.ts.map +1 -0
  38. package/dist/bare-launch-menu.js +83 -6
  39. package/dist/bare-launch-menu.js.map +1 -0
  40. package/dist/claude-launch-args.d.ts.map +1 -0
  41. package/dist/claude-launch-args.js +34 -1
  42. package/dist/claude-launch-args.js.map +1 -0
  43. package/dist/claude.d.ts.map +1 -0
  44. package/dist/claude.js +486 -14
  45. package/dist/claude.js.map +1 -0
  46. package/dist/cleanup-cmd.d.ts +5 -5
  47. package/dist/cleanup-cmd.d.ts.map +1 -0
  48. package/dist/cleanup-cmd.js +453 -14
  49. package/dist/cleanup-cmd.js.map +1 -0
  50. package/dist/clear-rewake-core.d.ts.map +1 -0
  51. package/dist/clear-rewake-core.js +23 -2
  52. package/dist/clear-rewake-core.js.map +1 -0
  53. package/dist/clear-rewake.d.ts.map +1 -0
  54. package/dist/clear-rewake.js +19 -1
  55. package/dist/clear-rewake.js.map +1 -0
  56. package/dist/cli-help.d.ts +5 -0
  57. package/dist/cli-help.d.ts.map +1 -0
  58. package/dist/cli-help.js +104 -56
  59. package/dist/cli-help.js.map +1 -0
  60. package/dist/cli-platform.d.ts.map +1 -0
  61. package/dist/cli-platform.js +106 -4
  62. package/dist/cli-platform.js.map +1 -0
  63. package/dist/cli-tool-approval.d.ts +84 -0
  64. package/dist/cli-tool-approval.d.ts.map +1 -0
  65. package/dist/cli-tool-approval.js +583 -0
  66. package/dist/cli-tool-approval.js.map +1 -0
  67. package/dist/codex-app-server.d.ts.map +1 -0
  68. package/dist/codex-app-server.js +228 -4
  69. package/dist/codex-app-server.js.map +1 -0
  70. package/dist/codex-app-wake.d.ts.map +1 -0
  71. package/dist/codex-app-wake.js +432 -2
  72. package/dist/codex-app-wake.js.map +1 -0
  73. package/dist/codex-launch.d.ts.map +1 -0
  74. package/dist/codex-launch.js +102 -2
  75. package/dist/codex-launch.js.map +1 -0
  76. package/dist/codex-remote.d.ts +6 -1
  77. package/dist/codex-remote.d.ts.map +1 -0
  78. package/dist/codex-remote.js +394 -1
  79. package/dist/codex-remote.js.map +1 -0
  80. package/dist/codex-wake-resolve.d.ts.map +1 -0
  81. package/dist/codex-wake-resolve.js +127 -1
  82. package/dist/codex-wake-resolve.js.map +1 -0
  83. package/dist/config-utils.d.ts +2 -2
  84. package/dist/config-utils.d.ts.map +1 -0
  85. package/dist/config-utils.js +766 -3
  86. package/dist/config-utils.js.map +1 -0
  87. package/dist/config.d.ts +98 -61
  88. package/dist/config.d.ts.map +1 -0
  89. package/dist/config.js +426 -1
  90. package/dist/config.js.map +1 -0
  91. package/dist/console-prefix.d.ts.map +1 -0
  92. package/dist/console-prefix.js +96 -1
  93. package/dist/console-prefix.js.map +1 -0
  94. package/dist/cube-name.d.ts.map +1 -0
  95. package/dist/cube-name.js +67 -1
  96. package/dist/cube-name.js.map +1 -0
  97. package/dist/cubes.d.ts +126 -18
  98. package/dist/cubes.d.ts.map +1 -0
  99. package/dist/cubes.js +412 -4
  100. package/dist/cubes.js.map +1 -0
  101. package/dist/debug.d.ts.map +1 -0
  102. package/dist/debug.js +71 -1
  103. package/dist/debug.js.map +1 -0
  104. package/dist/direct-log.d.ts.map +1 -0
  105. package/dist/direct-log.js +11 -1
  106. package/dist/direct-log.js.map +1 -0
  107. package/dist/docs-sections.d.ts +11 -10
  108. package/dist/docs-sections.d.ts.map +1 -0
  109. package/dist/docs-sections.js +113 -3
  110. package/dist/docs-sections.js.map +1 -0
  111. package/dist/drone-lifecycle.d.ts +5 -21
  112. package/dist/drone-lifecycle.d.ts.map +1 -0
  113. package/dist/drone-lifecycle.js +57 -8
  114. package/dist/drone-lifecycle.js.map +1 -0
  115. package/dist/drone-management.d.ts +29 -0
  116. package/dist/drone-management.d.ts.map +1 -0
  117. package/dist/drone-management.js +109 -0
  118. package/dist/drone-management.js.map +1 -0
  119. package/dist/ensure-mcp-config.d.ts.map +1 -0
  120. package/dist/ensure-mcp-config.js +36 -1
  121. package/dist/ensure-mcp-config.js.map +1 -0
  122. package/dist/evict-drone.d.ts +10 -2
  123. package/dist/evict-drone.d.ts.map +1 -0
  124. package/dist/evict-drone.js +60 -1
  125. package/dist/evict-drone.js.map +1 -0
  126. package/dist/gc-orphan-inboxes.d.ts.map +1 -0
  127. package/dist/gc-orphan-inboxes.js +177 -1
  128. package/dist/gc-orphan-inboxes.js.map +1 -0
  129. package/dist/inbox-monitor.d.ts +1 -1
  130. package/dist/inbox-monitor.d.ts.map +1 -0
  131. package/dist/inbox-monitor.js +856 -3
  132. package/dist/inbox-monitor.js.map +1 -0
  133. package/dist/index.d.ts +6 -4
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +1034 -37
  136. package/dist/index.js.map +1 -0
  137. package/dist/launch-all-cmd.d.ts.map +1 -0
  138. package/dist/launch-all-cmd.js +377 -37
  139. package/dist/launch-all-cmd.js.map +1 -0
  140. package/dist/launch-all-command.d.ts.map +1 -0
  141. package/dist/launch-all-command.js +24 -1
  142. package/dist/launch-all-command.js.map +1 -0
  143. package/dist/launch-all-deps.d.ts +3 -8
  144. package/dist/launch-all-deps.d.ts.map +1 -0
  145. package/dist/launch-all-deps.js +99 -1
  146. package/dist/launch-all-deps.js.map +1 -0
  147. package/dist/launch-all-discovery.d.ts +1 -0
  148. package/dist/launch-all-discovery.d.ts.map +1 -0
  149. package/dist/launch-all-discovery.js +90 -4
  150. package/dist/launch-all-discovery.js.map +1 -0
  151. package/dist/launch-all-locks.d.ts.map +1 -0
  152. package/dist/launch-all-locks.js +54 -1
  153. package/dist/launch-all-locks.js.map +1 -0
  154. package/dist/launch-gate.d.ts +2 -2
  155. package/dist/launch-gate.d.ts.map +1 -0
  156. package/dist/launch-gate.js +73 -1
  157. package/dist/launch-gate.js.map +1 -0
  158. package/dist/lifecycle-log-guard.d.ts.map +1 -0
  159. package/dist/lifecycle-log-guard.js +93 -2
  160. package/dist/lifecycle-log-guard.js.map +1 -0
  161. package/dist/list-roles-render.d.ts.map +1 -0
  162. package/dist/list-roles-render.js +40 -6
  163. package/dist/list-roles-render.js.map +1 -0
  164. package/dist/local-log-routing.d.ts +19 -0
  165. package/dist/local-log-routing.d.ts.map +1 -0
  166. package/dist/local-log-routing.js +67 -0
  167. package/dist/local-log-routing.js.map +1 -0
  168. package/dist/local-manage-tool-result.d.ts +8 -0
  169. package/dist/local-manage-tool-result.d.ts.map +1 -0
  170. package/dist/local-manage-tool-result.js +10 -0
  171. package/dist/local-manage-tool-result.js.map +1 -0
  172. package/dist/local-server-cursor.d.ts +34 -0
  173. package/dist/local-server-cursor.d.ts.map +1 -0
  174. package/dist/local-server-cursor.js +151 -0
  175. package/dist/local-server-cursor.js.map +1 -0
  176. package/dist/log-audit.d.ts.map +1 -0
  177. package/dist/log-audit.js +194 -3
  178. package/dist/log-audit.js.map +1 -0
  179. package/dist/log-stream.d.ts +31 -18
  180. package/dist/log-stream.d.ts.map +1 -0
  181. package/dist/log-stream.js +1214 -12
  182. package/dist/log-stream.js.map +1 -0
  183. package/dist/model-presets.d.ts.map +1 -0
  184. package/dist/model-presets.js +19 -1
  185. package/dist/model-presets.js.map +1 -0
  186. package/dist/name-validator.d.ts.map +1 -0
  187. package/dist/name-validator.js +22 -1
  188. package/dist/name-validator.js.map +1 -0
  189. package/dist/opencode-drone.d.ts.map +1 -0
  190. package/dist/opencode-drone.js +438 -4
  191. package/dist/opencode-drone.js.map +1 -0
  192. package/dist/opencode-plugin.d.ts +1 -1
  193. package/dist/opencode-plugin.d.ts.map +1 -0
  194. package/dist/opencode-plugin.js +29 -4
  195. package/dist/opencode-plugin.js.map +1 -0
  196. package/dist/parse-assimilate-args.d.ts +1 -1
  197. package/dist/parse-assimilate-args.d.ts.map +1 -0
  198. package/dist/parse-assimilate-args.js +125 -1
  199. package/dist/parse-assimilate-args.js.map +1 -0
  200. package/dist/parse-launch-all-args.d.ts.map +1 -0
  201. package/dist/parse-launch-all-args.js +70 -1
  202. package/dist/parse-launch-all-args.js.map +1 -0
  203. package/dist/postinstall-banner.d.ts.map +1 -0
  204. package/dist/postinstall-banner.js +27 -2
  205. package/dist/postinstall-banner.js.map +1 -0
  206. package/dist/postinstall.d.ts.map +1 -0
  207. package/dist/postinstall.js +32 -5
  208. package/dist/postinstall.js.map +1 -0
  209. package/dist/readiness-probe.d.ts.map +1 -0
  210. package/dist/readiness-probe.js +9 -1
  211. package/dist/readiness-probe.js.map +1 -0
  212. package/dist/regen-format.d.ts.map +1 -0
  213. package/dist/regen-format.js +545 -13
  214. package/dist/regen-format.js.map +1 -0
  215. package/dist/regen.d.ts.map +1 -0
  216. package/dist/regen.js +141 -5
  217. package/dist/regen.js.map +1 -0
  218. package/dist/remote-client.d.ts +65 -175
  219. package/dist/remote-client.d.ts.map +1 -0
  220. package/dist/remote-client.js +942 -1
  221. package/dist/remote-client.js.map +1 -0
  222. package/dist/reset-local-seat-cmd.d.ts +50 -0
  223. package/dist/reset-local-seat-cmd.d.ts.map +1 -0
  224. package/dist/reset-local-seat-cmd.js +169 -0
  225. package/dist/reset-local-seat-cmd.js.map +1 -0
  226. package/dist/role-match.d.ts.map +1 -0
  227. package/dist/role-match.js +23 -1
  228. package/dist/role-match.js.map +1 -0
  229. package/dist/role-resolver.d.ts.map +1 -0
  230. package/dist/role-resolver.js +60 -1
  231. package/dist/role-resolver.js.map +1 -0
  232. package/dist/roster-render.d.ts.map +1 -0
  233. package/dist/roster-render.js +121 -3
  234. package/dist/roster-render.js.map +1 -0
  235. package/dist/seat-probe.d.ts +32 -13
  236. package/dist/seat-probe.d.ts.map +1 -0
  237. package/dist/seat-probe.js +78 -1
  238. package/dist/seat-probe.js.map +1 -0
  239. package/dist/seat-store.d.ts +76 -0
  240. package/dist/seat-store.d.ts.map +1 -0
  241. package/dist/seat-store.js +309 -0
  242. package/dist/seat-store.js.map +1 -0
  243. package/dist/seats.d.ts +303 -0
  244. package/dist/seats.d.ts.map +1 -0
  245. package/dist/seats.js +551 -0
  246. package/dist/seats.js.map +1 -0
  247. package/dist/self-path.d.ts +27 -0
  248. package/dist/self-path.d.ts.map +1 -0
  249. package/dist/self-path.js +43 -0
  250. package/dist/self-path.js.map +1 -0
  251. package/dist/server-endpoint.d.ts +9 -0
  252. package/dist/server-endpoint.d.ts.map +1 -0
  253. package/dist/server-endpoint.js +38 -0
  254. package/dist/server-endpoint.js.map +1 -0
  255. package/dist/server-errors.d.ts +50 -0
  256. package/dist/server-errors.d.ts.map +1 -0
  257. package/dist/server-errors.js +79 -0
  258. package/dist/server-errors.js.map +1 -0
  259. package/dist/server-handshake.d.ts +197 -0
  260. package/dist/server-handshake.d.ts.map +1 -0
  261. package/dist/server-handshake.js +505 -0
  262. package/dist/server-handshake.js.map +1 -0
  263. package/dist/server-response.d.ts +3 -0
  264. package/dist/server-response.d.ts.map +1 -0
  265. package/dist/server-response.js +67 -0
  266. package/dist/server-response.js.map +1 -0
  267. package/dist/server-trust.d.ts +14 -0
  268. package/dist/server-trust.d.ts.map +1 -0
  269. package/dist/server-trust.js +225 -0
  270. package/dist/server-trust.js.map +1 -0
  271. package/dist/setup-confirm.d.ts +3 -3
  272. package/dist/setup-confirm.d.ts.map +1 -0
  273. package/dist/setup-confirm.js +126 -2
  274. package/dist/setup-confirm.js.map +1 -0
  275. package/dist/setup.d.ts +5 -3
  276. package/dist/setup.d.ts.map +1 -0
  277. package/dist/setup.js +169 -46
  278. package/dist/setup.js.map +1 -0
  279. package/dist/shell-escape.d.ts.map +1 -0
  280. package/dist/shell-escape.js +22 -1
  281. package/dist/shell-escape.js.map +1 -0
  282. package/dist/spawn.d.ts +2 -2
  283. package/dist/spawn.d.ts.map +1 -0
  284. package/dist/spawn.js +29 -10
  285. package/dist/spawn.js.map +1 -0
  286. package/dist/startup-services.d.ts +0 -1
  287. package/dist/startup-services.d.ts.map +1 -0
  288. package/dist/startup-services.js +26 -1
  289. package/dist/startup-services.js.map +1 -0
  290. package/dist/stream-owner.d.ts +6 -2
  291. package/dist/stream-owner.d.ts.map +1 -0
  292. package/dist/stream-owner.js +547 -2
  293. package/dist/stream-owner.js.map +1 -0
  294. package/dist/stream-status.d.ts +1 -1
  295. package/dist/stream-status.d.ts.map +1 -0
  296. package/dist/stream-status.js +268 -3
  297. package/dist/stream-status.js.map +1 -0
  298. package/dist/sync-roles-render.d.ts.map +1 -0
  299. package/dist/sync-roles-render.js +118 -3
  300. package/dist/sync-roles-render.js.map +1 -0
  301. package/dist/sync.d.ts +1 -1
  302. package/dist/sync.d.ts.map +1 -0
  303. package/dist/sync.js +286 -22
  304. package/dist/sync.js.map +1 -0
  305. package/dist/terminal-title.d.ts.map +1 -0
  306. package/dist/terminal-title.js +68 -1
  307. package/dist/terminal-title.js.map +1 -0
  308. package/dist/token-store.d.ts +17 -111
  309. package/dist/token-store.d.ts.map +1 -0
  310. package/dist/token-store.js +81 -1
  311. package/dist/token-store.js.map +1 -0
  312. package/dist/tool-manifest.d.ts +4 -8
  313. package/dist/tool-manifest.d.ts.map +1 -0
  314. package/dist/tool-manifest.js +554 -1
  315. package/dist/tool-manifest.js.map +1 -0
  316. package/dist/tool-scope.d.ts +8 -12
  317. package/dist/tool-scope.d.ts.map +1 -0
  318. package/dist/tool-scope.js +97 -1
  319. package/dist/tool-scope.js.map +1 -0
  320. package/dist/types.d.ts +1 -5
  321. package/dist/types.d.ts.map +1 -0
  322. package/dist/types.js +5 -0
  323. package/dist/types.js.map +1 -0
  324. package/dist/unknown-subcommand.d.ts +1 -1
  325. package/dist/unknown-subcommand.d.ts.map +1 -0
  326. package/dist/unknown-subcommand.js +37 -1
  327. package/dist/unknown-subcommand.js.map +1 -0
  328. package/dist/version.d.ts +1 -1
  329. package/dist/version.d.ts.map +1 -0
  330. package/dist/version.js +78 -2
  331. package/dist/version.js.map +1 -0
  332. package/dist/working-repo.d.ts.map +1 -0
  333. package/dist/working-repo.js +81 -1
  334. package/dist/working-repo.js.map +1 -0
  335. package/dist/worktree-lifecycle.d.ts.map +1 -0
  336. package/dist/worktree-lifecycle.js +205 -2
  337. package/dist/worktree-lifecycle.js.map +1 -0
  338. package/docs/EXTRACTION_PROVENANCE.md +40 -0
  339. package/docs/LOCAL_SERVER.md +103 -0
  340. package/docs/RELEASING.md +120 -0
  341. package/package.json +52 -34
  342. package/src/agent-runtime.ts +69 -0
  343. package/src/assimilate-cmd.ts +1864 -0
  344. package/src/assimilate-deps.ts +444 -0
  345. package/src/assimilate-guard.ts +84 -0
  346. package/src/assimilate-welcome.ts +39 -0
  347. package/src/auth-env.ts +21 -0
  348. package/src/backends/launch-all-pastelist.ts +18 -0
  349. package/src/backends/launch-all-tmux.ts +75 -0
  350. package/src/backends/launch-all-windows.ts +110 -0
  351. package/src/bare-launch-menu.ts +148 -0
  352. package/src/claude-launch-args.ts +35 -0
  353. package/src/claude.ts +561 -0
  354. package/src/cleanup-cmd.ts +581 -0
  355. package/src/clear-rewake-core.ts +34 -0
  356. package/src/clear-rewake.ts +18 -0
  357. package/src/cli-help.ts +116 -0
  358. package/src/cli-platform.ts +127 -0
  359. package/src/cli-tool-approval.ts +716 -0
  360. package/src/codex-app-server.ts +238 -0
  361. package/src/codex-app-wake.ts +512 -0
  362. package/src/codex-launch.ts +140 -0
  363. package/src/codex-remote.ts +500 -0
  364. package/src/codex-wake-resolve.ts +155 -0
  365. package/src/config-utils.ts +811 -0
  366. package/src/config.ts +595 -0
  367. package/src/console-prefix.ts +99 -0
  368. package/src/cube-name.ts +69 -0
  369. package/src/cubes.ts +583 -0
  370. package/src/debug.ts +73 -0
  371. package/src/direct-log.ts +9 -0
  372. package/src/docs-sections.ts +129 -0
  373. package/src/drone-lifecycle.ts +62 -0
  374. package/src/drone-management.ts +162 -0
  375. package/src/ensure-mcp-config.ts +54 -0
  376. package/src/evict-drone.ts +82 -0
  377. package/src/gc-orphan-inboxes.ts +229 -0
  378. package/src/inbox-monitor.ts +1005 -0
  379. package/src/index.ts +1201 -0
  380. package/src/launch-all-cmd.ts +450 -0
  381. package/src/launch-all-command.ts +30 -0
  382. package/src/launch-all-deps.ts +188 -0
  383. package/src/launch-all-discovery.ts +118 -0
  384. package/src/launch-all-locks.ts +74 -0
  385. package/src/launch-gate.ts +80 -0
  386. package/src/lifecycle-log-guard.ts +144 -0
  387. package/src/list-roles-render.ts +52 -0
  388. package/src/local-log-routing.ts +106 -0
  389. package/src/local-manage-tool-result.ts +10 -0
  390. package/src/local-server-cursor.ts +188 -0
  391. package/src/log-audit.ts +207 -0
  392. package/src/log-stream.ts +1505 -0
  393. package/src/model-presets.ts +22 -0
  394. package/src/name-validator.ts +23 -0
  395. package/src/opencode-drone.ts +528 -0
  396. package/src/opencode-plugin.ts +38 -0
  397. package/src/parse-assimilate-args.ts +125 -0
  398. package/src/parse-launch-all-args.ts +100 -0
  399. package/src/postinstall-banner.ts +37 -0
  400. package/src/postinstall.ts +36 -0
  401. package/src/readiness-probe.ts +12 -0
  402. package/src/regen-format.ts +651 -0
  403. package/src/regen.ts +150 -0
  404. package/src/remote-client.ts +1379 -0
  405. package/src/reset-local-seat-cmd.ts +227 -0
  406. package/src/role-match.ts +29 -0
  407. package/src/role-resolver.ts +93 -0
  408. package/src/roster-render.ts +196 -0
  409. package/src/seat-probe.ts +115 -0
  410. package/src/seat-store.ts +333 -0
  411. package/src/seats.ts +741 -0
  412. package/src/self-path.ts +51 -0
  413. package/src/server-endpoint.ts +38 -0
  414. package/src/server-errors.ts +93 -0
  415. package/src/server-handshake.ts +783 -0
  416. package/src/server-response.ts +76 -0
  417. package/src/server-trust.ts +245 -0
  418. package/src/setup-confirm.ts +171 -0
  419. package/src/setup.ts +186 -0
  420. package/src/shell-escape.ts +22 -0
  421. package/src/spawn.ts +31 -0
  422. package/src/startup-services.ts +36 -0
  423. package/src/stream-owner.ts +686 -0
  424. package/src/stream-status.ts +337 -0
  425. package/src/sync-roles-render.ts +163 -0
  426. package/src/sync.ts +363 -0
  427. package/src/terminal-title.ts +76 -0
  428. package/src/token-store.ts +102 -0
  429. package/src/tool-manifest.ts +610 -0
  430. package/src/tool-scope.ts +110 -0
  431. package/src/types.ts +5 -0
  432. package/src/unknown-subcommand.ts +35 -0
  433. package/src/version.ts +82 -0
  434. package/src/working-repo.ts +99 -0
  435. package/src/worktree-lifecycle.ts +254 -0
  436. package/dist/auth-recovery.d.ts +0 -33
  437. package/dist/auth-recovery.js +0 -1
  438. package/dist/auth.d.ts +0 -103
  439. package/dist/auth.js +0 -39
  440. package/dist/device-auth.d.ts +0 -75
  441. package/dist/device-auth.js +0 -1
  442. package/dist/drone-address.d.ts +0 -26
  443. package/dist/drone-address.js +0 -1
  444. package/dist/get-started.d.ts +0 -20
  445. package/dist/get-started.js +0 -2
  446. package/dist/health-beat.d.ts +0 -134
  447. package/dist/health-beat.js +0 -1
  448. package/dist/role-section.d.ts +0 -110
  449. package/dist/role-section.js +0 -8
  450. package/dist/setup-action.d.ts +0 -20
  451. package/dist/setup-action.js +0 -1
  452. package/dist/stale-version-check.d.ts +0 -40
  453. package/dist/stale-version-check.js +0 -1
  454. package/dist/subscription-retry.d.ts +0 -40
  455. package/dist/subscription-retry.js +0 -1
  456. package/dist/templates.d.ts +0 -107
  457. package/dist/templates.js +0 -359
  458. package/dist/token-crypto.d.ts +0 -50
  459. package/dist/token-crypto.js +0 -1
@@ -0,0 +1,500 @@
1
+ import { mkdirSync, chmodSync, readdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { spawn } from 'node:child_process';
6
+ import { CodexAppServerClient } from './codex-app-server.js';
7
+ import { codexBorgSessionConfigArgs, BORG_SESSION_ENV } from './launch-gate.js';
8
+ import { codexAppServerSocketConfigArgs } from './codex-wake-resolve.js';
9
+ import {
10
+ BORG_AGENT_KIND_ENV,
11
+ BORG_CODEX_REMOTE_WAKE_ENV,
12
+ codexAgentKindConfigArgs,
13
+ codexRemoteWakeConfigArgs,
14
+ } from './agent-runtime.js';
15
+
16
+ /**
17
+ * gh#528 — npm-managed Codex remote-wake.
18
+ *
19
+ * borg owns a per-launch DIRECT Codex app-server (`codex app-server --listen
20
+ * unix://<socket>`) as the primary wake path, instead of the standalone
21
+ * `codex app-server daemon start` (which only exists for standalone-installer
22
+ * Codex — npm-managed Codex has no daemon, so those sessions had no push-wake
23
+ * and relied on periodic log-drain catch-up). Per-launch direct
24
+ * app-server works for both install kinds and starts fresh each launch (so it
25
+ * always loads the current borgmcp MCP — no daemon-restart version refresh
26
+ * needed). The TUI is then launched with `codex --remote unix://<socket>` and
27
+ * Borg delivers wakes by connecting to that socket (see codex-app-wake.ts).
28
+ */
29
+
30
+ export interface CodexAppServerHandle {
31
+ pid: number | undefined;
32
+ socketPath: string;
33
+ /** Kill the owned app-server + remove its socket/pidfile. Wire to TUI exit. */
34
+ cleanup: () => void;
35
+ }
36
+
37
+ export interface CodexRemoteLaunch {
38
+ args: string[];
39
+ env: Record<string, string>;
40
+ warning?: string;
41
+ /** Present when borg owns a per-launch app-server that must be cleaned up on TUI exit. */
42
+ server?: CodexAppServerHandle;
43
+ }
44
+
45
+ export interface CodexChild {
46
+ pid: number | undefined;
47
+ kill: () => void;
48
+ /** Snapshot app-server exit state + bounded stderr captured by the production spawn. */
49
+ diagnostics?: () => CodexChildDiagnostics;
50
+ }
51
+
52
+ export interface CodexChildDiagnostics {
53
+ exited: boolean;
54
+ exitCode: number | null;
55
+ signal: string | null;
56
+ error?: string | null;
57
+ stderr: string;
58
+ }
59
+
60
+ export interface PrepareCodexRemoteDeps {
61
+ /** Spawn the long-lived `codex app-server --listen unix://<socketPath>` child. */
62
+ spawnAppServer: (socketPath: string) => CodexChild;
63
+ /** Readiness probe: a real CodexAppServerClient connect + thread/loaded/list round-trip. */
64
+ probeReady: (socketPath: string) => Promise<boolean>;
65
+ /** Delay between readiness polls (injected so tests don't actually wait). */
66
+ sleep: (ms: number) => Promise<void>;
67
+ /** 0700 runtime dir (default ~/.config/borgmcp/codex-remote). */
68
+ runtimeDir?: string;
69
+ /** Unique socket id generator (default 32-hex). Injected for deterministic tests. */
70
+ socketId?: () => string;
71
+ /** Readiness timeout (default 30000ms) + poll interval (default 250ms). */
72
+ readyTimeoutMs?: number;
73
+ pollIntervalMs?: number;
74
+ /** Whether a pid is alive (default process.kill(pid, 0)). Injected for tests. */
75
+ isAlive?: (pid: number) => boolean;
76
+ }
77
+
78
+ export const DEFAULT_CODEX_REMOTE_DIR = join(homedir(), '.config', 'borgmcp', 'codex-remote');
79
+ const DEFAULT_CODEX_REMOTE_READY_TIMEOUT_MS = 30_000;
80
+ const MAX_CODEX_APP_SERVER_STDERR_CHARS = 16_384;
81
+
82
+ /**
83
+ * Keep app-server failure details useful without copying credentials or local
84
+ * filesystem locations into the parent process warning.
85
+ */
86
+ function sanitizeCodexDiagnostic(input: string): string {
87
+ let sanitized = String(input).replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
88
+ sanitized = sanitized.replace(
89
+ /\b((?:authorization\s*:\s*)?bearer)\s+[^\s,;]+/gi,
90
+ '$1 <REDACTED>'
91
+ );
92
+ sanitized = sanitized.replace(
93
+ /\b([a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^@\s/]+)@/gi,
94
+ '$1<REDACTED>@'
95
+ );
96
+ sanitized = sanitized.replace(
97
+ /([?&](?:access_token|auth_token|id_token|refresh_token|session_token|token|api_key|apikey|secret|password)=)[^&#\s]*/gi,
98
+ '$1<REDACTED>'
99
+ );
100
+ sanitized = sanitized.replace(
101
+ /\b((?:[a-z0-9_-]*(?:token|secret|password)|api[_-]?key)\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;]+)/gi,
102
+ '$1<REDACTED>'
103
+ );
104
+ sanitized = sanitized.replace(
105
+ /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g,
106
+ '<REDACTED>'
107
+ );
108
+ sanitized = sanitized.replace(
109
+ /(^|[\s("'=])(?:file:\/\/)?\/(?:Users|home|private|tmp|var|etc|opt|Volumes)\/[^\s"'`<>)}\]]+/gm,
110
+ '$1<REDACTED_PATH>'
111
+ );
112
+ sanitized = sanitized.replace(
113
+ /\b[A-Za-z]:\\(?:Users|Documents and Settings|ProgramData|Temp)\\[^\s"'`<>)}\]]+/g,
114
+ '<REDACTED_PATH>'
115
+ );
116
+ return sanitized.slice(-MAX_CODEX_APP_SERVER_STDERR_CHARS);
117
+ }
118
+
119
+ export function withCodexCwdArg(args: string[], cwd: string): string[] {
120
+ if (hasCodexCwdArg(args)) return args;
121
+ return ['--cd', cwd, ...args];
122
+ }
123
+
124
+ /** Resolve the project directory Codex itself will use for this launch.
125
+ * Relative explicit paths are interpreted from the wrapper's current working
126
+ * directory, matching Codex's CLI path resolution. The final occurrence wins,
127
+ * matching clap's override behavior for repeated options. */
128
+ export function resolveCodexLaunchCwd(args: string[], fallbackCwd: string): string {
129
+ const baseCwd = resolve(fallbackCwd);
130
+ let selected = baseCwd;
131
+ for (let index = 0; index < args.length; index += 1) {
132
+ const arg = args[index];
133
+ if (arg === '--') break;
134
+ if (arg === '--cd' || arg === '-C') {
135
+ const value = args[index + 1];
136
+ if (value !== undefined && value.length > 0) {
137
+ selected = resolve(baseCwd, value);
138
+ index += 1;
139
+ }
140
+ continue;
141
+ }
142
+ if (arg.startsWith('--cd=')) {
143
+ const value = arg.slice('--cd='.length);
144
+ if (value.length > 0) selected = resolve(baseCwd, value);
145
+ continue;
146
+ }
147
+ if (arg.startsWith('-C') && arg.length > 2) {
148
+ const value = arg.slice(2).replace(/^=/, '');
149
+ if (value.length > 0) selected = resolve(baseCwd, value);
150
+ }
151
+ }
152
+ return selected;
153
+ }
154
+
155
+ function hasCodexCwdArg(args: string[]): boolean {
156
+ for (const arg of args) {
157
+ if (arg === '--') return false;
158
+ if (arg === '--cd' || arg.startsWith('--cd=') || arg === '-C' || (arg.startsWith('-C') && arg.length > 2)) {
159
+ return true;
160
+ }
161
+ }
162
+ return false;
163
+ }
164
+
165
+ export function defaultIsAlive(pid: number): boolean {
166
+ try {
167
+ process.kill(pid, 0);
168
+ return true;
169
+ } catch (err: any) {
170
+ // EPERM ⇒ the process exists but we can't signal it (still alive).
171
+ return err?.code === 'EPERM';
172
+ }
173
+ }
174
+
175
+ /**
176
+ * gh#633: process-liveness probe for the borg-owned codex app-server — the
177
+ * transport-agnostic analogue of the claude tail-F Monitor's pgrep check
178
+ * (checkInboxMonitorHealthy / stream-status.ts:48). Codex drones wake via this
179
+ * app-server bridge, NOT a tail-F Monitor, so wake_path_client_monitor_armed is
180
+ * false-by-design for them; the HOP-2 wake-path-deaf classifier mis-read that
181
+ * as deaf (gh#633). This gives HOP-2 the codex wake path's ACTUAL health.
182
+ *
183
+ * Uses the app-server PIDFILE (written at spawn, beside the socket) +
184
+ * process.kill(pid, 0) — NOT pgrep. The codex TUI is also launched
185
+ * `codex --remote unix://<socketPath>`, so a `pgrep -f <socketPath>` would ALSO
186
+ * match the live TUI and FALSE-ARM when the app-server has crashed but the TUI
187
+ * still runs (the deaf-but-alive case). The pidfile holds the EXACT app-server
188
+ * pid, so kill(0) reflects the app-server's liveness specifically — and it's
189
+ * cheaper than pgrep (no subprocess). Mirrors pruneStaleSockets' pid check.
190
+ *
191
+ * Tri-state (mirrors checkInboxMonitorHealthy's boolean|null contract):
192
+ * - true: pidfile resolves to a LIVE pid → app-server (bridge) is up → armed.
193
+ * - false: pidfile resolves to a DEAD pid → an unclean exit (crash/kill -9)
194
+ * left a stale pidfile (cleanup never ran) → bridge down → HOP-2
195
+ * correctly flags a genuinely-deaf codex drone (no SLI-lie).
196
+ * - null: pidfile missing / unreadable / unparseable → cannot determine →
197
+ * caller maps null→armed (false-deaf-avoidance, same as the claude
198
+ * monitor branch). A CLEAN app-server exit removes the pidfile, but
199
+ * then the drone is shutting down → the silent-stall watchdog
200
+ * backstops it via a separate layer.
201
+ *
202
+ * Residual (negligible, gh#633 / Coordinator 6f28fe3f): PID reuse — if the
203
+ * crashed app-server's pid is recycled by an unrelated process before the next
204
+ * launch's pruneStaleSockets removes the stale pidfile, kill(0) reports alive →
205
+ * a brief false-arm. The window is tiny (exact-pid reuse during the crash gap)
206
+ * and self-heals on the next launch's prune; far smaller than existsSync's
207
+ * always-on stale-file masking.
208
+ */
209
+ export function checkCodexBridgeHealthy(
210
+ socketPath: string | null,
211
+ deps: {
212
+ isAlive?: (pid: number) => boolean;
213
+ readPidFile?: (pidPath: string) => string;
214
+ } = {}
215
+ ): boolean | null {
216
+ if (!socketPath) return null;
217
+ const isAlive = deps.isAlive ?? defaultIsAlive;
218
+ const readPidFile =
219
+ deps.readPidFile ?? ((pidPath: string) => readFileSync(pidPath, 'utf-8'));
220
+ const pidPath = socketPath.replace(/\.sock$/, '.pid');
221
+ try {
222
+ const pid = Number.parseInt(readPidFile(pidPath).trim(), 10);
223
+ if (Number.isNaN(pid)) return null;
224
+ return isAlive(pid);
225
+ } catch {
226
+ return null;
227
+ }
228
+ }
229
+
230
+ function safeRm(p: string): void {
231
+ try {
232
+ rmSync(p, { force: true });
233
+ } catch {
234
+ // best-effort
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Remove sockets in the owned dir whose owning app-server pid is no longer
240
+ * alive (crashed prior launches), leaving live concurrent sessions' sockets
241
+ * untouched. Operates ONLY inside the borg-owned runtime dir.
242
+ */
243
+ function pruneStaleSockets(runtimeDir: string, isAlive: (pid: number) => boolean): void {
244
+ let entries: string[];
245
+ try {
246
+ entries = readdirSync(runtimeDir);
247
+ } catch {
248
+ return;
249
+ }
250
+ for (const name of entries) {
251
+ if (!name.endsWith('.pid')) continue;
252
+ const pidPath = join(runtimeDir, name);
253
+ const sockPath = join(runtimeDir, name.replace(/\.pid$/, '.sock'));
254
+ let pid: number;
255
+ try {
256
+ pid = Number.parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
257
+ } catch {
258
+ safeRm(pidPath);
259
+ continue;
260
+ }
261
+ if (Number.isNaN(pid) || !isAlive(pid)) {
262
+ safeRm(sockPath);
263
+ safeRm(pidPath);
264
+ }
265
+ }
266
+ }
267
+
268
+ function failLoud(reason: string): CodexRemoteLaunch {
269
+ return { args: [], env: {}, warning: reason };
270
+ }
271
+
272
+ function formatExitReason(diagnostics: CodexChildDiagnostics): string {
273
+ if (diagnostics.error) return `process error: ${sanitizeCodexDiagnostic(diagnostics.error)}`;
274
+ if (diagnostics.exitCode != null) return `exit code ${diagnostics.exitCode}`;
275
+ if (diagnostics.signal) return `signal ${diagnostics.signal}`;
276
+ return 'unknown exit status';
277
+ }
278
+
279
+ function formatStderr(stderr: string): string {
280
+ const trimmed = sanitizeCodexDiagnostic(stderr).trim();
281
+ return trimmed ? ` Stderr: ${trimmed}` : '';
282
+ }
283
+
284
+ /**
285
+ * Start a borg-owned per-launch Codex app-server, probe it for readiness, and
286
+ * return the `--remote` launch args + an owned handle (or a fail-loud warning).
287
+ * Async + lifecycle-owning: the caller MUST call `result.server?.cleanup()` on
288
+ * TUI exit.
289
+ */
290
+ export async function prepareCodexRemoteLaunch(
291
+ deps: PrepareCodexRemoteDeps
292
+ ): Promise<CodexRemoteLaunch> {
293
+ const runtimeDir = deps.runtimeDir ?? DEFAULT_CODEX_REMOTE_DIR;
294
+ const isAlive = deps.isAlive ?? defaultIsAlive;
295
+ const readyTimeoutMs = deps.readyTimeoutMs ?? DEFAULT_CODEX_REMOTE_READY_TIMEOUT_MS;
296
+ const pollIntervalMs = deps.pollIntervalMs ?? 250;
297
+
298
+ // 1. 0700 owned dir + prune crashed prior sockets (concurrent-safe via pid liveness).
299
+ try {
300
+ mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
301
+ chmodSync(runtimeDir, 0o700); // enforce 0700 even if it pre-existed with looser perms
302
+ pruneStaleSockets(runtimeDir, isAlive);
303
+ } catch (err: any) {
304
+ return failLoud(
305
+ `Codex remote-wake disabled: could not prepare ${runtimeDir} (${err?.message ?? err}); run borg_regen manually.`
306
+ );
307
+ }
308
+
309
+ // 2. unique, non-predictable socket path inside the owned dir.
310
+ const id = (deps.socketId ?? (() => randomBytes(16).toString('hex')))();
311
+ const socketPath = join(runtimeDir, `${id}.sock`);
312
+ const pidPath = join(runtimeDir, `${id}.pid`);
313
+
314
+ // 3. spawn the long-lived app-server.
315
+ let child: CodexChild;
316
+ try {
317
+ child = deps.spawnAppServer(socketPath);
318
+ } catch (err: any) {
319
+ safeRm(socketPath);
320
+ return failLoud(
321
+ `Codex remote-wake disabled: could not start \`codex app-server\` (${sanitizeCodexDiagnostic(String(err?.message ?? err))}) — ` +
322
+ `confirm the Codex executable is installed and available on PATH. ` +
323
+ `Remote wake is unavailable for this session; run borg_regen manually to catch up.`
324
+ );
325
+ }
326
+ if (child.pid != null) {
327
+ try {
328
+ writeFileSync(pidPath, String(child.pid));
329
+ } catch {
330
+ // pidfile is only for stale-prune; launch still proceeds.
331
+ }
332
+ }
333
+
334
+ const cleanup = () => {
335
+ try {
336
+ child.kill();
337
+ } catch {
338
+ // best-effort
339
+ }
340
+ safeRm(socketPath);
341
+ safeRm(pidPath);
342
+ };
343
+
344
+ // 4. readiness via a real protocol round-trip — bounded attempts (no clock dep).
345
+ const attempts = Math.max(1, Math.ceil(readyTimeoutMs / pollIntervalMs));
346
+ let ready = false;
347
+ let exitDiagnostics: CodexChildDiagnostics | undefined;
348
+ for (let i = 0; i < attempts && !ready; i++) {
349
+ try {
350
+ ready = await deps.probeReady(socketPath);
351
+ } catch {
352
+ ready = false;
353
+ }
354
+ if (!ready) {
355
+ const diagnostics = child.diagnostics?.();
356
+ if (diagnostics?.exited) {
357
+ exitDiagnostics = diagnostics;
358
+ break;
359
+ }
360
+ }
361
+ if (!ready && i < attempts - 1) await deps.sleep(pollIntervalMs);
362
+ }
363
+
364
+ if (!ready) {
365
+ const diagnostics = exitDiagnostics ?? child.diagnostics?.();
366
+ cleanup();
367
+ if (diagnostics?.exited) {
368
+ return failLoud(
369
+ `Codex remote-wake disabled: \`codex app-server\` exited before becoming ready ` +
370
+ `(${formatExitReason(diagnostics)}).${formatStderr(diagnostics.stderr)} ` +
371
+ `Run borg_regen manually to catch up.`
372
+ );
373
+ }
374
+ return failLoud(
375
+ `Codex remote-wake disabled: \`codex app-server\` remained running but did not become ready ` +
376
+ `at ${sanitizeCodexDiagnostic(socketPath)} within ${readyTimeoutMs}ms.${formatStderr(diagnostics?.stderr ?? '')} ` +
377
+ `Run borg_regen manually to catch up.`
378
+ );
379
+ }
380
+
381
+ // 5. ready → owned remote launch.
382
+ return {
383
+ args: ['--remote', `unix://${socketPath}`],
384
+ env: { [BORG_CODEX_REMOTE_WAKE_ENV]: '1' },
385
+ server: { pid: child.pid, socketPath, cleanup },
386
+ };
387
+ }
388
+
389
+ /**
390
+ * Production deps for prepareCodexRemoteLaunch — spawn the real `codex
391
+ * app-server` child + probe it with the real CodexAppServerClient. Shared by
392
+ * claude.ts and assimilate-deps.ts so there's ONE wiring.
393
+ *
394
+ * The readiness probe uses Codex app-server RPCs ONLY (connect + thread/loaded/
395
+ * list) — it never calls a Borg coordination endpoint — so it can never advance
396
+ * last_seen/last_regen_at and mask a deaf Codex (the gh#46/gh#406 signal-truth
397
+ * invariant; the app-server socket is the wake-DELIVERY wire, not a liveness
398
+ * signal).
399
+ */
400
+ export function defaultCodexRemoteDeps(): Pick<
401
+ PrepareCodexRemoteDeps,
402
+ 'spawnAppServer' | 'probeReady' | 'sleep'
403
+ > {
404
+ return {
405
+ spawnAppServer: (socketPath) => {
406
+ // gh#851: this app-server — NOT the `codex --remote` TUI — spawns the
407
+ // borg-mcp MCP child, so the BORG_SESSION activation marker (gh#673) must
408
+ // ride HERE too, or the child gates dormant. Load-bearing = the `-c
409
+ // mcp_servers.borg.env.BORG_SESSION="1"` override (codex MCP children read
410
+ // only the pinned env, injected via the app-server's -c — same V2b
411
+ // mechanism the TUI launch uses, applied at the app-server boundary). The
412
+ // env BORG_SESSION=1 is a defensive belt (inherited env never reaches the
413
+ // codex MCP child by itself). Still activation-only per the launch-gate
414
+ // SR-BINDING — never a security/access signal.
415
+ //
416
+ // gh#855: pin THIS app-server's live socket into the same child env via the
417
+ // same `-c` channel, so the waking borg-mcp child is authoritative about
418
+ // its OWN socket and re-resolves the loaded thread FRESH each wake — a
419
+ // missed/stale launch probe can no longer cause permanent deafness. The
420
+ // socketPath is borg-generated (randomBytes), never user input, TOML-quoted
421
+ // exactly like the BORG_SESSION override — zero injection surface.
422
+ const child = spawn(
423
+ 'codex',
424
+ [
425
+ 'app-server',
426
+ ...codexBorgSessionConfigArgs(),
427
+ ...codexAgentKindConfigArgs(),
428
+ ...codexRemoteWakeConfigArgs(),
429
+ ...codexAppServerSocketConfigArgs(socketPath),
430
+ '--listen',
431
+ `unix://${socketPath}`,
432
+ ],
433
+ {
434
+ stdio: ['ignore', 'ignore', 'pipe'],
435
+ shell: false,
436
+ env: {
437
+ ...process.env,
438
+ [BORG_SESSION_ENV]: '1',
439
+ [BORG_AGENT_KIND_ENV]: 'codex',
440
+ [BORG_CODEX_REMOTE_WAKE_ENV]: '1',
441
+ },
442
+ }
443
+ );
444
+ let stderr = '';
445
+ let exited = false;
446
+ let exitCode: number | null = null;
447
+ let signal: string | null = null;
448
+ let error: string | null = null;
449
+ child.stderr?.setEncoding('utf8');
450
+ child.stderr?.on('data', (chunk: string | Buffer) => {
451
+ stderr = `${stderr}${String(chunk)}`.slice(-MAX_CODEX_APP_SERVER_STDERR_CHARS);
452
+ });
453
+ // `close` follows stdio drain; using it instead of `exit` keeps the final
454
+ // stderr chunk available when readiness reports the child failure.
455
+ child.once('close', (code, exitSignal) => {
456
+ exited = true;
457
+ exitCode = code;
458
+ signal = exitSignal;
459
+ });
460
+ child.once('error', (childError) => {
461
+ exited = true;
462
+ error = sanitizeCodexDiagnostic(childError.message);
463
+ });
464
+ return {
465
+ pid: child.pid,
466
+ kill: () => {
467
+ try {
468
+ child.kill();
469
+ } catch {
470
+ // best-effort
471
+ }
472
+ },
473
+ diagnostics: () => ({
474
+ exited,
475
+ exitCode,
476
+ signal,
477
+ error: error == null ? null : sanitizeCodexDiagnostic(error),
478
+ stderr: sanitizeCodexDiagnostic(stderr),
479
+ }),
480
+ };
481
+ },
482
+ probeReady: async (socketPath) => {
483
+ const probe = new CodexAppServerClient(socketPath);
484
+ try {
485
+ await probe.connect();
486
+ await probe.loadedThreadIds();
487
+ return true;
488
+ } catch {
489
+ return false;
490
+ } finally {
491
+ try {
492
+ probe.close();
493
+ } catch {
494
+ // best-effort
495
+ }
496
+ }
497
+ },
498
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
499
+ };
500
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * gh#855 — pure helpers for FRESH codex wake-target re-resolution.
3
+ *
4
+ * Root cause of codex deaf-when-idle: the wake target (socket + thread) was
5
+ * resolved once at launch and never refreshed, so a missed/stale launch probe
6
+ * left the drone permanently deaf. Phase 1 makes the waking borg-mcp child
7
+ * authoritative about its OWN live app-server socket — the socket is injected
8
+ * into the child's pinned env at spawn (codex-remote.ts, via the #851 `-c
9
+ * mcp_servers.borg.env.X` channel) — and re-resolves the loaded thread FRESH on
10
+ * every wake (loadedThreadIds is a re-runnable RPC).
11
+ *
12
+ * These are the pure pieces; the IO orchestration lives in codex-app-wake.ts.
13
+ */
14
+
15
+ /** Pinned-env var carrying THIS drone's live app-server socket (set at spawn). */
16
+ export const BORG_CODEX_APP_SERVER_SOCKET_ENV = 'BORG_CODEX_APP_SERVER_SOCKET';
17
+
18
+ /** The live app-server socket for this borg-mcp child, or null (un-upgraded launch). */
19
+ export function codexAppServerSocketFromEnv(env: NodeJS.ProcessEnv = process.env): string | null {
20
+ const v = env[BORG_CODEX_APP_SERVER_SOCKET_ENV];
21
+ return v && v.length > 0 ? v : null;
22
+ }
23
+
24
+ /**
25
+ * The per-launch codex config override that pins THIS app-server's live socket
26
+ * into the borg-mcp child's [mcp_servers.borg.env] — the same `-c` channel the
27
+ * #851 BORG_SESSION marker rides (codex MCP children read only the pinned env,
28
+ * never inherited env). The socketPath is borg-generated (randomBytes under
29
+ * ~/.config/borgmcp/codex-remote), never user input; TOML-quoted exactly like
30
+ * the BORG_SESSION override, so there is zero injection surface.
31
+ */
32
+ export function codexAppServerSocketConfigArgs(socketPath: string): string[] {
33
+ return ['-c', `mcp_servers.borg.env.${BORG_CODEX_APP_SERVER_SOCKET_ENV}="${socketPath}"`];
34
+ }
35
+
36
+ export interface CodexThreadInfo {
37
+ id: string;
38
+ cwd?: string;
39
+ updatedAt?: number;
40
+ }
41
+
42
+ /**
43
+ * Pick the loaded thread to wake on the live socket. Each borg-owned app-server
44
+ * is fresh-per-launch / single-session, so the common case is exactly one loaded
45
+ * thread. When more than one is loaded, prefer the thread whose cwd matches this
46
+ * drone's working directory (sibling worktrees have distinct cwds), then the
47
+ * newest by updatedAt — always deterministic. No loaded thread → null (no wake
48
+ * this cycle; the next wake retries, so a transient empty list never causes
49
+ * permanent deafness).
50
+ */
51
+ export function pickFreshThread(threads: CodexThreadInfo[], opts: { cwd: string }): string | null {
52
+ if (threads.length === 0) return null;
53
+ if (threads.length === 1) return threads[0].id;
54
+ const cwdMatches = threads.filter((t) => t.cwd === opts.cwd);
55
+ const pool = cwdMatches.length > 0 ? cwdMatches : threads;
56
+ let best = pool[0];
57
+ for (const t of pool) {
58
+ if ((t.updatedAt ?? 0) > (best.updatedAt ?? 0)) best = t;
59
+ }
60
+ return best.id;
61
+ }
62
+
63
+ /**
64
+ * Pure prune: drop wake-target entries whose app-server socket is positively
65
+ * dead (liveness === false), so the file self-heals. Keeps alive (true) and
66
+ * indeterminate (null) entries — false-deaf-avoidance, mirroring
67
+ * checkCodexBridgeHealthy's tri-state. Returns the surviving map + whether
68
+ * anything changed (so the caller writes only on change).
69
+ */
70
+ export function pruneDeadWakeTargets<T extends { socketPath: string }>(
71
+ targets: Record<string, T>,
72
+ socketLiveness: (socketPath: string) => boolean | null
73
+ ): { targets: Record<string, T>; changed: boolean } {
74
+ const out: Record<string, T> = {};
75
+ let changed = false;
76
+ for (const [key, t] of Object.entries(targets)) {
77
+ if (socketLiveness(t.socketPath) === false) {
78
+ changed = true;
79
+ continue;
80
+ }
81
+ out[key] = t;
82
+ }
83
+ return { targets: out, changed };
84
+ }
85
+
86
+ /**
87
+ * Whether the freshly-resolved target differs from what's already recorded —
88
+ * so the self-healing cache write happens only on change (no file thrash on a
89
+ * busy cube re-resolving the same socket+thread every wake).
90
+ */
91
+ export function wakeTargetChanged(
92
+ existing: { socketPath: string; threadId: string } | null,
93
+ fresh: { socketPath: string; threadId: string }
94
+ ): boolean {
95
+ return (
96
+ !existing || existing.socketPath !== fresh.socketPath || existing.threadId !== fresh.threadId
97
+ );
98
+ }
99
+
100
+ // ─── gh#857 Phase 2: durable retry + heartbeat (pure helpers) ───────────────
101
+
102
+ /** Base backoff for the first retry of a dropped/deferred wake. */
103
+ export const WAKE_RETRY_BASE_MS = 5_000;
104
+ /** Backoff ceiling — a wedged thread is retried at most this often. */
105
+ export const WAKE_RETRY_CAP_MS = 60_000;
106
+ /**
107
+ * Age cap: a pending wake older than this is given up (dropped from the queue).
108
+ * Generous on purpose — the WI-2 heartbeat is the backstop beyond it, and the
109
+ * server read-cursor means the next delivery (heartbeat or a fresh entry) drains
110
+ * everything anyway, so an aged-out single wake is never a permanent miss.
111
+ */
112
+ export const WAKE_RETRY_MAX_AGE_MS = 45 * 60_000;
113
+ /**
114
+ * Hard iteration ceiling for the retry-drain loop — a defensive belt ALONGSIDE
115
+ * the time-based age cap. In prod the age cap (real clock) terminates the loop in
116
+ * ~45-50 attempts; this ceiling only matters if the clock fails to advance
117
+ * (pathological / a non-advancing injected clock in tests) where a time-only
118
+ * guard would hot-spin forever. Set far above any real attempt count.
119
+ */
120
+ export const WAKE_RETRY_MAX_ATTEMPTS = 1000;
121
+
122
+ /**
123
+ * Exponential backoff (ms) for the Nth retry of a pending wake (0-based),
124
+ * doubling from WAKE_RETRY_BASE_MS, saturating at WAKE_RETRY_CAP_MS, plus
125
+ * caller-supplied jitter so co-located sibling drones don't retry in lockstep.
126
+ */
127
+ export function wakeRetryBackoffMs(attempts: number, jitter = 0): number {
128
+ const n = Math.max(0, attempts);
129
+ const exp = WAKE_RETRY_BASE_MS * 2 ** n;
130
+ return Math.min(exp, WAKE_RETRY_CAP_MS) + jitter;
131
+ }
132
+
133
+ /** True once a pending wake has outlived the age cap (give up; heartbeat backstops). */
134
+ export function wakeRetryExpired(
135
+ firstEnqueuedAt: number,
136
+ now: number,
137
+ maxAgeMs = WAKE_RETRY_MAX_AGE_MS
138
+ ): boolean {
139
+ return now - firstEnqueuedAt >= maxAgeMs;
140
+ }
141
+
142
+ /**
143
+ * WI-2 double-fire avoidance: the periodic heartbeat fires only when no wake (or
144
+ * prior heartbeat) delivery landed within the cadence window — so an active cube
145
+ * with flowing per-entry wakes doesn't get redundant heartbeat injections. A
146
+ * never-delivered seat (null) always fires.
147
+ */
148
+ export function shouldFireHeartbeat(
149
+ lastDeliveredAt: number | null,
150
+ now: number,
151
+ cadenceMs: number
152
+ ): boolean {
153
+ if (lastDeliveredAt === null) return true;
154
+ return now - lastDeliveredAt >= cadenceMs;
155
+ }