@ashx-j/lunr 0.2.24 → 0.2.26

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 (254) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/dist/builtin-extensions/index.d.ts.map +1 -1
  3. package/dist/builtin-extensions/index.js +1 -0
  4. package/dist/builtin-extensions/index.js.map +1 -1
  5. package/dist/builtin-extensions/lunr-computer-use.d.ts +3 -0
  6. package/dist/builtin-extensions/lunr-computer-use.d.ts.map +1 -0
  7. package/dist/builtin-extensions/lunr-computer-use.js +179 -0
  8. package/dist/builtin-extensions/lunr-computer-use.js.map +1 -0
  9. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.d.ts.map +1 -1
  10. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js +1 -1
  11. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js.map +1 -1
  12. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts +4 -4
  13. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts.map +1 -1
  14. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js +8 -5
  15. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js.map +1 -1
  16. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.d.ts +1 -0
  17. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.d.ts.map +1 -1
  18. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.js +11 -2
  19. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.js.map +1 -1
  20. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.d.ts.map +1 -1
  21. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.js +3 -3
  22. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.js.map +1 -1
  23. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.d.ts.map +1 -1
  24. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js +15 -1
  25. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js.map +1 -1
  26. package/dist/builtin-extensions/pi-subagents/src/runs/shared/chain-outputs.d.ts +1 -0
  27. package/dist/builtin-extensions/pi-subagents/src/runs/shared/chain-outputs.d.ts.map +1 -1
  28. package/dist/builtin-extensions/pi-subagents/src/runs/shared/chain-outputs.js +5 -0
  29. package/dist/builtin-extensions/pi-subagents/src/runs/shared/chain-outputs.js.map +1 -1
  30. package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.d.ts +1 -1
  31. package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.d.ts.map +1 -1
  32. package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.js +2 -0
  33. package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.js.map +1 -1
  34. package/dist/builtin-extensions/pi-subagents/src/tui/communication.d.ts.map +1 -1
  35. package/dist/builtin-extensions/pi-subagents/src/tui/communication.js +1 -1
  36. package/dist/builtin-extensions/pi-subagents/src/tui/communication.js.map +1 -1
  37. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts +1 -0
  38. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
  39. package/dist/builtin-extensions/pi-subagents/src/tui/render.js +5 -3
  40. package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
  41. package/dist/core/computer-use/adapter.d.ts +28 -0
  42. package/dist/core/computer-use/adapter.d.ts.map +1 -0
  43. package/dist/core/computer-use/adapter.js +133 -0
  44. package/dist/core/computer-use/adapter.js.map +1 -0
  45. package/dist/core/computer-use/image.d.ts +27 -0
  46. package/dist/core/computer-use/image.d.ts.map +1 -0
  47. package/dist/core/computer-use/image.js +72 -0
  48. package/dist/core/computer-use/image.js.map +1 -0
  49. package/dist/core/computer-use/lease.d.ts +18 -0
  50. package/dist/core/computer-use/lease.d.ts.map +1 -0
  51. package/dist/core/computer-use/lease.js +129 -0
  52. package/dist/core/computer-use/lease.js.map +1 -0
  53. package/dist/core/computer-use/macos-runtime.d.ts +14 -0
  54. package/dist/core/computer-use/macos-runtime.d.ts.map +1 -0
  55. package/dist/core/computer-use/macos-runtime.js +148 -0
  56. package/dist/core/computer-use/macos-runtime.js.map +1 -0
  57. package/dist/core/computer-use/policy.d.ts +19 -0
  58. package/dist/core/computer-use/policy.d.ts.map +1 -0
  59. package/dist/core/computer-use/policy.js +67 -0
  60. package/dist/core/computer-use/policy.js.map +1 -0
  61. package/dist/core/computer-use/release.generated.d.ts +31 -0
  62. package/dist/core/computer-use/release.generated.d.ts.map +1 -0
  63. package/dist/core/computer-use/release.generated.js +36 -0
  64. package/dist/core/computer-use/release.generated.js.map +1 -0
  65. package/dist/core/computer-use/runtime.d.ts +13 -0
  66. package/dist/core/computer-use/runtime.d.ts.map +1 -0
  67. package/dist/core/computer-use/runtime.js +158 -0
  68. package/dist/core/computer-use/runtime.js.map +1 -0
  69. package/dist/core/computer-use/schemas.d.ts +100 -0
  70. package/dist/core/computer-use/schemas.d.ts.map +1 -0
  71. package/dist/core/computer-use/schemas.js +79 -0
  72. package/dist/core/computer-use/schemas.js.map +1 -0
  73. package/dist/core/computer-use/windows-desktop.d.ts +3 -0
  74. package/dist/core/computer-use/windows-desktop.d.ts.map +1 -0
  75. package/dist/core/computer-use/windows-desktop.js +48 -0
  76. package/dist/core/computer-use/windows-desktop.js.map +1 -0
  77. package/dist/core/computer-use/workflow.d.ts +38 -0
  78. package/dist/core/computer-use/workflow.d.ts.map +1 -0
  79. package/dist/core/computer-use/workflow.js +560 -0
  80. package/dist/core/computer-use/workflow.js.map +1 -0
  81. package/dist/core/extensions/loader.d.ts.map +1 -1
  82. package/dist/core/extensions/loader.js +4 -0
  83. package/dist/core/extensions/loader.js.map +1 -1
  84. package/dist/core/permissions.d.ts +1 -1
  85. package/dist/core/permissions.d.ts.map +1 -1
  86. package/dist/core/permissions.js +14 -3
  87. package/dist/core/permissions.js.map +1 -1
  88. package/dist/core/plan-mode.d.ts.map +1 -1
  89. package/dist/core/plan-mode.js +9 -0
  90. package/dist/core/plan-mode.js.map +1 -1
  91. package/dist/core/settings-manager.d.ts +6 -0
  92. package/dist/core/settings-manager.d.ts.map +1 -1
  93. package/dist/core/settings-manager.js +19 -0
  94. package/dist/core/settings-manager.js.map +1 -1
  95. package/dist/core/subagent-event-transport.d.ts +3 -0
  96. package/dist/core/subagent-event-transport.d.ts.map +1 -0
  97. package/dist/core/subagent-event-transport.js +61 -0
  98. package/dist/core/subagent-event-transport.js.map +1 -0
  99. package/dist/gateway/adapters/discord.d.ts.map +1 -1
  100. package/dist/gateway/adapters/discord.js +4 -9
  101. package/dist/gateway/adapters/discord.js.map +1 -1
  102. package/dist/gateway/adapters/telegram.d.ts.map +1 -1
  103. package/dist/gateway/adapters/telegram.js +7 -0
  104. package/dist/gateway/adapters/telegram.js.map +1 -1
  105. package/dist/gateway/agent-bridge.d.ts +4 -1
  106. package/dist/gateway/agent-bridge.d.ts.map +1 -1
  107. package/dist/gateway/agent-bridge.js +57 -17
  108. package/dist/gateway/agent-bridge.js.map +1 -1
  109. package/dist/gateway/approval.d.ts.map +1 -1
  110. package/dist/gateway/approval.js +3 -3
  111. package/dist/gateway/approval.js.map +1 -1
  112. package/dist/gateway/authz.d.ts +4 -4
  113. package/dist/gateway/authz.d.ts.map +1 -1
  114. package/dist/gateway/authz.js +8 -12
  115. package/dist/gateway/authz.js.map +1 -1
  116. package/dist/gateway/buttons.d.ts.map +1 -1
  117. package/dist/gateway/buttons.js +29 -19
  118. package/dist/gateway/buttons.js.map +1 -1
  119. package/dist/gateway/commands.d.ts +2 -0
  120. package/dist/gateway/commands.d.ts.map +1 -1
  121. package/dist/gateway/commands.js +35 -18
  122. package/dist/gateway/commands.js.map +1 -1
  123. package/dist/gateway/config.d.ts +1 -0
  124. package/dist/gateway/config.d.ts.map +1 -1
  125. package/dist/gateway/config.js +2 -0
  126. package/dist/gateway/config.js.map +1 -1
  127. package/dist/gateway/cron.d.ts +1 -1
  128. package/dist/gateway/cron.d.ts.map +1 -1
  129. package/dist/gateway/cron.js +22 -8
  130. package/dist/gateway/cron.js.map +1 -1
  131. package/dist/gateway/index.d.ts.map +1 -1
  132. package/dist/gateway/index.js +26 -26
  133. package/dist/gateway/index.js.map +1 -1
  134. package/dist/gateway/mobile-commands.d.ts.map +1 -1
  135. package/dist/gateway/mobile-commands.js +11 -11
  136. package/dist/gateway/mobile-commands.js.map +1 -1
  137. package/dist/gateway/presenter.d.ts +12 -1
  138. package/dist/gateway/presenter.d.ts.map +1 -1
  139. package/dist/gateway/presenter.js +148 -50
  140. package/dist/gateway/presenter.js.map +1 -1
  141. package/dist/gateway/router.d.ts +5 -5
  142. package/dist/gateway/router.d.ts.map +1 -1
  143. package/dist/gateway/router.js +95 -43
  144. package/dist/gateway/router.js.map +1 -1
  145. package/dist/gateway/service.d.ts.map +1 -1
  146. package/dist/gateway/service.js +13 -4
  147. package/dist/gateway/service.js.map +1 -1
  148. package/dist/gateway/setup.d.ts +5 -0
  149. package/dist/gateway/setup.d.ts.map +1 -1
  150. package/dist/gateway/setup.js +101 -43
  151. package/dist/gateway/setup.js.map +1 -1
  152. package/dist/gateway/stream.d.ts +6 -5
  153. package/dist/gateway/stream.d.ts.map +1 -1
  154. package/dist/gateway/stream.js +24 -11
  155. package/dist/gateway/stream.js.map +1 -1
  156. package/dist/modes/interactive/components/settings-selector.d.ts +4 -0
  157. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  158. package/dist/modes/interactive/components/settings-selector.js +20 -0
  159. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  160. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  161. package/dist/modes/interactive/interactive-mode.js +8 -0
  162. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  163. package/dist/modes/print-mode.d.ts.map +1 -1
  164. package/dist/modes/print-mode.js +9 -1
  165. package/dist/modes/print-mode.js.map +1 -1
  166. package/dist/node-runtime/adapter-VFG7WNPX.js +17 -0
  167. package/dist/node-runtime/{agent-session-services-IEF7LQWD.js → agent-session-services-5YXS2PLF.js} +13 -10
  168. package/dist/node-runtime/{builtin-extensions-EBAWTH4M.js → builtin-extensions-I2HIFEMY.js} +3 -2
  169. package/dist/node-runtime/{chunk-K5AGHH57.js → chunk-2CFDTH5B.js} +7 -3
  170. package/dist/node-runtime/{chunk-VWCE2HSZ.js → chunk-2HMIRK7W.js} +4 -4
  171. package/dist/node-runtime/chunk-2IDTZAG5.js +1415 -0
  172. package/dist/node-runtime/{chunk-IGV54Q5V.js → chunk-3MISGG4S.js} +137 -76
  173. package/dist/node-runtime/chunk-4SBQAI4T.js +2185 -0
  174. package/dist/node-runtime/{chunk-WHLRAGUQ.js → chunk-5B5QN3ZQ.js} +16 -16
  175. package/dist/node-runtime/chunk-5B6VKFEV.js +180 -0
  176. package/dist/node-runtime/{chunk-AD6VXAO7.js → chunk-5Q4A37RB.js} +15 -104
  177. package/dist/node-runtime/{chunk-4TLO4PO2.js → chunk-7IQGBKBJ.js} +7 -1401
  178. package/dist/node-runtime/{chunk-R3K2F5GI.js → chunk-B4HP6ZWM.js} +2 -0
  179. package/dist/node-runtime/{chunk-OCVIXVTZ.js → chunk-D5UCKPBQ.js} +12 -1
  180. package/dist/node-runtime/chunk-DVQSHCVP.js +97 -0
  181. package/dist/node-runtime/{chunk-HFVZUPFJ.js → chunk-E75WFZ33.js} +2 -2
  182. package/dist/node-runtime/chunk-FY4A63RK.js +73 -0
  183. package/dist/node-runtime/{chunk-RORY2HMJ.js → chunk-I74LN5JG.js} +16 -1
  184. package/dist/node-runtime/chunk-IKU6Y7DL.js +287 -0
  185. package/dist/node-runtime/{chunk-QM43FEAE.js → chunk-L24TPBS4.js} +6 -5
  186. package/dist/node-runtime/{chunk-DE2JB6J5.js → chunk-LJTIVM7I.js} +7 -7
  187. package/dist/node-runtime/{chunk-DSIKTUPW.js → chunk-LWJXTA6Z.js} +274 -2433
  188. package/dist/node-runtime/{chunk-YVI6L6LU.js → chunk-P7XBVXGT.js} +36 -21
  189. package/dist/node-runtime/{chunk-OVX6X377.js → chunk-PTFBPO3C.js} +1 -1
  190. package/dist/node-runtime/{chunk-F4FLTQKV.js → chunk-RAK2QA2B.js} +186 -1529
  191. package/dist/node-runtime/{chunk-L3LX4SNR.js → chunk-RCR3XAQY.js} +36 -8
  192. package/dist/node-runtime/chunk-RGQAEOM5.js +81 -0
  193. package/dist/node-runtime/{chunk-UWPJL22I.js → chunk-SLVOAQOY.js} +15 -4
  194. package/dist/node-runtime/chunk-SN2KSMPQ.js +1364 -0
  195. package/dist/node-runtime/{chunk-FU6OODRZ.js → chunk-SPVDBUOS.js} +1 -1
  196. package/dist/node-runtime/{chunk-5Q3EHOHI.js → chunk-T4DA4XE7.js} +19 -17
  197. package/dist/node-runtime/chunk-U5F4IC7R.js +53 -0
  198. package/dist/node-runtime/{chunk-UP2XBVH5.js → chunk-UOEDI7RP.js} +1 -1
  199. package/dist/node-runtime/{chunk-Y73WPE6S.js → chunk-UVWCDQYF.js} +79 -44
  200. package/dist/node-runtime/{chunk-ZXSGTGJ6.js → chunk-WIF24I7G.js} +21 -5
  201. package/dist/node-runtime/{chunk-OF7PWMG4.js → chunk-X3PGBLQ7.js} +3 -87
  202. package/dist/node-runtime/chunk-XHIRAZ26.js +108 -0
  203. package/dist/node-runtime/{chunk-YDO2F2IA.js → chunk-YWOJIGFY.js} +1 -1
  204. package/dist/node-runtime/{command-7BG7VVTS.js → command-FSBU2EVZ.js} +5 -5
  205. package/dist/node-runtime/{commands-NZLSOHPE.js → commands-EMBQQVVT.js} +4 -2
  206. package/dist/node-runtime/{commands-QFDKNQTL.js → commands-NAAL7KNT.js} +7 -6
  207. package/dist/node-runtime/{config-RORQUQFG.js → config-D4QUPAHX.js} +1 -1
  208. package/dist/node-runtime/{direct-tool-executor-77GW4F5P.js → direct-tool-executor-4D7UXYU4.js} +7 -4
  209. package/dist/node-runtime/{gateway-TWR3N65G.js → gateway-DQM333YV.js} +140 -85
  210. package/dist/node-runtime/index.js +28 -21
  211. package/dist/node-runtime/{init-HS6NQXSK.js → init-P5AUV77I.js} +4 -2
  212. package/dist/node-runtime/{install-cli-VAP6KBFN.js → install-cli-NN4YAU44.js} +3 -3
  213. package/dist/node-runtime/{interactive-mode-6J5VZ2TT.js → interactive-mode-YOHZVMKX.js} +14 -11
  214. package/dist/node-runtime/{lunr-browser-J5ZZYVGM.js → lunr-browser-IR7TL3J7.js} +3 -3
  215. package/dist/node-runtime/lunr-computer-use-Y23CCDIJ.js +158 -0
  216. package/dist/node-runtime/{lunr-cron-CXOL37AL.js → lunr-cron-USHC4LXH.js} +9 -7
  217. package/dist/node-runtime/main.js +19 -15
  218. package/dist/node-runtime/{mcp-auth-flow-QIXHW6QN.js → mcp-auth-flow-QVLFVLEO.js} +2 -1
  219. package/dist/node-runtime/{mobile-commands-VTBM5DSZ.js → mobile-commands-O5LCHL32.js} +6 -5
  220. package/dist/node-runtime/{package-manager-cli-ZLW5DZXF.js → package-manager-cli-Q7BL6KZO.js} +12 -8
  221. package/dist/node-runtime/{pi-mcp-adapter-5UKBZP44.js → pi-mcp-adapter-3PHF4KZ4.js} +5 -5
  222. package/dist/node-runtime/{pi-subagents-XPCLLKIM.js → pi-subagents-7IBSSGEW.js} +27 -21
  223. package/dist/node-runtime/{print-mode-XSFM26EL.js → print-mode-XQ76CYMT.js} +2 -1
  224. package/dist/node-runtime/{proxy-modes-LGEUYD7R.js → proxy-modes-5FAPSZEZ.js} +7 -4
  225. package/dist/node-runtime/rpc-entry.js +19 -15
  226. package/dist/node-runtime/runtime-EFPPD2UW.js +21 -0
  227. package/dist/node-runtime/{runtime-bridges-QYXUX2TG.js → runtime-bridges-7WVCLI76.js} +6 -5
  228. package/dist/node-runtime/{service-ISSP6VHE.js → service-MDQIPQ26.js} +1 -1
  229. package/dist/node-runtime/{settings-manager-WCQDUVQB.js → settings-manager-DJDM5AHM.js} +5 -4
  230. package/dist/node-runtime/setup-PFBWRNUI.js +215 -0
  231. package/dist/node-runtime/{subagent-executor-OTNISDQU.js → subagent-executor-LPZ4BHWE.js} +32 -14
  232. package/dist/node-runtime/{update-cli-VBR2777K.js → update-cli-HNMOTNGX.js} +5 -4
  233. package/dist/node-runtime/workflow-OHPBEFO4.js +637 -0
  234. package/dist/utils/image-resize-core.d.ts +2 -0
  235. package/dist/utils/image-resize-core.d.ts.map +1 -1
  236. package/dist/utils/image-resize-core.js +10 -0
  237. package/dist/utils/image-resize-core.js.map +1 -1
  238. package/docs/computer-use.md +362 -0
  239. package/docs/features.md +9 -7
  240. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  241. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  242. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  243. package/examples/extensions/gondolin/package-lock.json +2 -2
  244. package/examples/extensions/gondolin/package.json +1 -1
  245. package/examples/extensions/sandbox/package-lock.json +2 -2
  246. package/examples/extensions/sandbox/package.json +1 -1
  247. package/examples/extensions/with-deps/package-lock.json +2 -2
  248. package/examples/extensions/with-deps/package.json +1 -1
  249. package/install-lock/package-lock.json +3841 -0
  250. package/install-lock/package.json +18 -0
  251. package/native/computer-use/LICENSE.md +21 -0
  252. package/npm-shrinkwrap.json +3822 -0
  253. package/package.json +15 -9
  254. package/dist/node-runtime/setup-YRBKH2LD.js +0 -155
@@ -10,21 +10,22 @@
10
10
  * /context, /compact, /thinking, /stop, /status, /help, /whoami)
11
11
  * 4. normal path: bridge.runTurn with a StreamConsumer when streaming is
12
12
  * enabled and the adapter supports edit; the final text is
13
- * silence-filtered and folded INTO the streaming preview (edit, plus
14
- * continuation chunks when the preview was truncated) — no preview +
15
- * final double-send. Without streaming it is split (text.ts) and sent
16
- * sequentially, the first chunk replying to the triggering message.
13
+ * silence-filtered and folded into a successful streaming preview edit.
14
+ * Failed final edits and ordinary answers enter the durable outbox, which
15
+ * acknowledges chunks in order and retries by destination.
17
16
  * Errors surface as a compact "⚠ <one-line>" — never a stack trace.
18
17
  */
19
18
  import { existsSync } from "node:fs";
20
19
  import { QUEUED } from "./agent-bridge.js";
21
20
  import { registerGatewayApprovalHandler, runWithApprovalContext } from "./approval.js";
22
- import { isAuthorized, requireGatewayOwner } from "./authz.js";
21
+ import { isAuthorized, requireAuthorized } from "./authz.js";
23
22
  import { CHAT_COMMANDS, runChatCommand, sendCommandReply } from "./commands.js";
24
23
  import { gatewayConfigPath, loadGatewayConfig, platformConfigFor } from "./config.js";
25
24
  import { bindConversation, conversationBinding } from "./conversations.js";
26
- import { acceptGatewayInput, invalidateGatewayDialogs } from "./presenter.js";
25
+ import { resolveWithinRoots } from "./mobile-commands.js";
26
+ import { acceptGatewayInput, gatewayEpoch, invalidateGatewayDialogs, queueGatewayText, rememberGatewayRole, } from "./presenter.js";
27
27
  import { buildSessionKey } from "./session-keys.js";
28
+ import { getSession as getStoredSession } from "./store.js";
28
29
  import { applySilenceFilter, StreamConsumer } from "./stream.js";
29
30
  import { splitMessage } from "./text.js";
30
31
  function oneLine(text) {
@@ -54,10 +55,12 @@ export function createRouter(deps) {
54
55
  }
55
56
  return initialCfg;
56
57
  }
57
- async function sendError(adapter, event, message) {
58
- await adapter.send(event.source.chatId, `⚠ ${oneLine(message)}`, {
58
+ async function sendError(adapter, event, key, cfg, message, epoch) {
59
+ await queueGatewayText(key, event.source, adapter, `⚠ ${oneLine(message)}`, {
60
+ kind: "notice",
59
61
  replyTo: event.messageId,
60
- threadId: event.source.threadId,
62
+ cfg,
63
+ epoch,
61
64
  });
62
65
  }
63
66
  /** Step 1: group-chat early gating. Returns true when the event must be dropped. */
@@ -92,9 +95,24 @@ export function createRouter(deps) {
92
95
  const code = pairing.issueCode(source.platform, source.userId);
93
96
  if (code === null)
94
97
  return true; // rate-limited or pending list full: stay silent
95
- await adapter.send(source.chatId, `Your lunR pairing code: ${formatPairingCode(code)} — ask the owner to run: lunr gateway pair approve ${source.platform} ${formatPairingCode(code)}`, { replyTo: event.messageId, threadId: source.threadId });
98
+ await adapter.send(source.chatId, `Your lunR pairing code: ${formatPairingCode(code)}. On the computer running lunr, approve access with: lunr gateway pair approve ${source.platform} ${formatPairingCode(code)}. Approval grants full gateway access, including local projects and saved sessions.`, { replyTo: event.messageId, threadId: source.threadId });
96
99
  return true;
97
100
  }
101
+ function workspaceReadiness(event, key, cfg) {
102
+ if (getStoredSession(key) || conversationBinding(key)?.cwd)
103
+ return undefined;
104
+ const root = cfg.defaultProject;
105
+ if (!root)
106
+ return "No default project selected. Use /project to choose an approved folder.";
107
+ try {
108
+ const cwd = resolveWithinRoots(root, cfg.projectRoots ?? []);
109
+ bindConversation(key, event.source, { cwd, owner: event.source.userId });
110
+ return undefined;
111
+ }
112
+ catch {
113
+ return "Default project is no longer an approved folder. Use /project to choose an approved folder.";
114
+ }
115
+ }
98
116
  /** Step 3: command registry. Returns true when the event was consumed. */
99
117
  async function handleSlash(adapter, event, key) {
100
118
  const text = event.text.trim();
@@ -120,6 +138,35 @@ export function createRouter(deps) {
120
138
  if (!event.text.startsWith("/"))
121
139
  return false;
122
140
  }
141
+ if (commandWord === "start") {
142
+ const issue = deps.remoteControls ? workspaceReadiness(event, key, freshCfg()) : undefined;
143
+ if (issue) {
144
+ await sendCommandReply(adapter, event, issue);
145
+ return true;
146
+ }
147
+ try {
148
+ const session = await bridge.getSession(key, true);
149
+ const models = await session?.modelRuntime.getAvailable();
150
+ if (!models?.length)
151
+ await sendCommandReply(adapter, event, "No authenticated model available. Use /login on your computer, then try /start again.");
152
+ else if (!session?.model ||
153
+ !models.some((model) => model.id === session.model?.id && model.provider === session.model.provider))
154
+ await sendCommandReply(adapter, event, "No available model selected. Use /model here to choose one.");
155
+ else
156
+ await sendCommandReply(adapter, event, "Ready. Send a message to begin, or use /model to choose a model.");
157
+ }
158
+ catch {
159
+ await sendCommandReply(adapter, event, "Session setup failed. Run lunr gateway doctor on your computer, then try /start again.");
160
+ }
161
+ return true;
162
+ }
163
+ if (deps.remoteControls && commandWord === "model") {
164
+ const issue = workspaceReadiness(event, key, freshCfg());
165
+ if (issue) {
166
+ await sendCommandReply(adapter, event, issue);
167
+ return true;
168
+ }
169
+ }
123
170
  const cmd = CHAT_COMMANDS.find((c) => c.name === commandWord || c.aliases?.includes(commandWord));
124
171
  if (!cmd) {
125
172
  return event.source.chatType !== "dm";
@@ -129,45 +176,41 @@ export function createRouter(deps) {
129
176
  key,
130
177
  adapter,
131
178
  bridge,
179
+ cfg: freshCfg(),
132
180
  args,
133
181
  reply: (message) => sendCommandReply(adapter, event, message),
134
182
  };
135
183
  return runWithApprovalContext({ key, adapter, source: event.source }, () => runChatCommand(cmd, ctx));
136
184
  }
137
- /** Step 4 delivery: silence filter → split → sequential send.
138
- *
139
- * When a streaming preview message exists (editMessageId), the final text
140
- * is folded INTO it instead of sent as a duplicate: non-truncated previews
141
- * get a final edit (covers silence-marker stripping), truncated previews
142
- * are edited to chunk 1 and the remaining chunks are sent as follow-ups. */
143
- async function deliver(adapter, event, text, opts) {
185
+ /** A successful preview is finalized by edit; failed edits fall back to a durable full reply. */
186
+ async function deliver(adapter, event, text, key, cfg, opts) {
144
187
  const filtered = applySilenceFilter(text);
145
188
  if (filtered === null)
146
189
  return;
147
- const chunks = splitMessage(filtered, adapter.maxMessageLength);
148
- let startIndex = 0;
149
190
  if (opts.editMessageId) {
150
- if (!opts.previewTruncated) {
151
- // Preview already holds the whole text: one final edit, no new message.
152
- await adapter.editMessage(event.source.chatId, opts.editMessageId, filtered);
153
- return;
154
- }
155
- // Preview was truncated: upgrade it to the first full chunk.
156
- await adapter.editMessage(event.source.chatId, opts.editMessageId, chunks[0]);
157
- startIndex = 1;
158
- }
159
- for (let index = startIndex; index < chunks.length; index++) {
160
- const result = await adapter.send(event.source.chatId, chunks[index], {
161
- replyTo: opts.reply && index === 0 ? event.messageId : undefined,
162
- threadId: event.source.threadId,
163
- });
164
- if (!result.success) {
165
- console.error(`[gateway] send failed on ${adapter.platform}: ${result.error ?? "unknown error"}`);
191
+ const chunks = splitMessage(filtered, adapter.maxMessageLength);
192
+ const edit = await adapter
193
+ .editMessage(event.source.chatId, opts.editMessageId, opts.previewTruncated ? chunks[0] : filtered)
194
+ .catch(() => ({ success: false }));
195
+ if (edit.success) {
196
+ if (!opts.previewTruncated)
197
+ return;
198
+ await queueGatewayText(key, event.source, adapter, chunks.slice(1).join(""), {
199
+ kind: "result",
200
+ cfg,
201
+ chunks: chunks.slice(1),
202
+ });
166
203
  return;
167
204
  }
168
205
  }
206
+ await queueGatewayText(key, event.source, adapter, filtered, {
207
+ kind: "result",
208
+ replyTo: opts.reply ? event.messageId : undefined,
209
+ cfg,
210
+ });
169
211
  }
170
212
  async function runTurn(adapter, event, key, cfg) {
213
+ const epoch = gatewayEpoch(key);
171
214
  const streaming = cfg.streaming.enabled && typeof adapter.editMessage === "function";
172
215
  let consumer;
173
216
  if (streaming) {
@@ -180,7 +223,9 @@ export function createRouter(deps) {
180
223
  return result.success ? (result.messageId ?? null) : null;
181
224
  },
182
225
  edit: async (messageId, text) => {
183
- await adapter.editMessage(event.source.chatId, messageId, text);
226
+ const result = await adapter.editMessage(event.source.chatId, messageId, text);
227
+ if (!result.success)
228
+ throw new Error(result.error ?? "Preview edit failed");
184
229
  },
185
230
  intervalMs: cfg.streaming.editIntervalMs,
186
231
  threshold: cfg.streaming.bufferThreshold,
@@ -190,10 +235,10 @@ export function createRouter(deps) {
190
235
  const callbacks = {
191
236
  onDelta: consumer ? (delta) => consumer.push(delta) : undefined,
192
237
  onFollowUpResult: (text) => {
193
- void deliver(adapter, event, text, { reply: false });
238
+ void deliver(adapter, event, text, key, cfg, { reply: false }).catch((error) => console.error("[gateway] follow-up delivery failed:", error));
194
239
  },
195
240
  onError: (message) => {
196
- void sendError(adapter, event, message);
241
+ void sendError(adapter, event, key, cfg, message, epoch).catch((error) => console.error("[gateway] error delivery failed:", error));
197
242
  },
198
243
  };
199
244
  const result = await runWithApprovalContext({ key, adapter, source: event.source }, () => bridge.runTurn(key, event, callbacks));
@@ -201,7 +246,7 @@ export function createRouter(deps) {
201
246
  return; // queued behind a running turn: no reply
202
247
  if (consumer)
203
248
  await consumer.finalize();
204
- await deliver(adapter, event, result, {
249
+ await deliver(adapter, event, result, key, cfg, {
205
250
  reply: !consumer,
206
251
  editMessageId: consumer?.sentMessageId ?? undefined,
207
252
  previewTruncated: consumer?.truncated ?? false,
@@ -213,18 +258,20 @@ export function createRouter(deps) {
213
258
  if (!adapter)
214
259
  return;
215
260
  const cfg = freshCfg();
261
+ const key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });
262
+ const epoch = gatewayEpoch(key);
216
263
  try {
217
264
  if (isGroupGated(event, cfg))
218
265
  return;
266
+ rememberGatewayRole(key, event.source);
219
267
  if (await isDenied(adapter, event, cfg))
220
268
  return;
221
- const key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });
222
269
  if (deps.remoteControls) {
223
270
  const binding = conversationBinding(key);
224
271
  if (binding?.owner) {
225
- requireGatewayOwner(event.source, cfg);
272
+ requireAuthorized(event.source, cfg);
226
273
  if (binding.owner !== event.source.userId)
227
- throw new Error("This session belongs to a different owner.");
274
+ throw new Error("This session belongs to a different user.");
228
275
  }
229
276
  bindConversation(key, event.source);
230
277
  if (acceptGatewayInput(key, event))
@@ -232,11 +279,16 @@ export function createRouter(deps) {
232
279
  }
233
280
  if (await handleSlash(adapter, event, key))
234
281
  return;
282
+ const issue = deps.remoteControls ? workspaceReadiness(event, key, cfg) : undefined;
283
+ if (issue) {
284
+ await sendCommandReply(adapter, event, issue);
285
+ return;
286
+ }
235
287
  await adapter.sendTyping(event.source.chatId, event.source.threadId).catch(() => { });
236
288
  await runTurn(adapter, event, key, cfg);
237
289
  }
238
290
  catch (err) {
239
- await sendError(adapter, event, err instanceof Error ? err.message : String(err)).catch(() => { });
291
+ await sendError(adapter, event, key, cfg, err instanceof Error ? err.message : String(err), epoch).catch(() => { });
240
292
  }
241
293
  },
242
294
  };
@@ -1 +1 @@
1
- {"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/gateway/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAgD,MAAM,EAAsB,MAAM,mBAAmB,CAAC;AAC7G,OAAO,EAAE,8BAA8B,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvF,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAsB,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC1G,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE3E,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AA6BzC,SAAS,OAAO,CAAC,IAAY,EAAU;IACtC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAAA,CACtD;AAED,SAAS,iBAAiB,CAAC,IAAY,EAAU;IAChD,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAAA,CACzE;AAED,SAAS,YAAY,CAAC,KAAmB,EAAW;IACnD,OAAO,KAAK,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;AAAA,CAC7C;AAED,MAAM,UAAU,YAAY,CAAC,IAAgB,EAAU;IACtD,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAE1E,mFAAmF;IACnF,8BAA8B,EAAE,CAAC;IAEjC,2GAA2G;IAC3G,SAAS,QAAQ,GAAkB;QAClC,IAAI,CAAC,YAAY;YAAE,OAAO,UAAU,CAAC;QACrC,IAAI,CAAC;YACJ,IAAI,UAAU,CAAC,iBAAiB,EAAE,CAAC,EAAE,CAAC;gBACrC,OAAO,iBAAiB,EAAE,CAAC;YAC5B,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,iCAAiC;QAClC,CAAC;QACD,OAAO,UAAU,CAAC;IAAA,CAClB;IAED,KAAK,UAAU,SAAS,CAAC,OAAwB,EAAE,KAAmB,EAAE,OAAe,EAAiB;QACvG,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAK,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE;YAChE,OAAO,EAAE,KAAK,CAAC,SAAS;YACxB,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ;SAC/B,CAAC,CAAC;IAAA,CACH;IAED,oFAAoF;IACpF,SAAS,YAAY,CAAC,KAAmB,EAAE,GAAkB,EAAW;QACvE,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;QACzB,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC3C,MAAM,WAAW,GAAG,iBAAiB,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC5D,IAAI,CAAC,WAAW;YAAE,OAAO,IAAI,CAAC;QAC9B,MAAM,YAAY,GAAG,WAAW,CAAC,iBAAiB,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3E,IACC,WAAW,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC;YACnC,CAAC,WAAW,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;YACjD,CAAC,YAAY;YACb,CAAC,YAAY,CAAC,KAAK,CAAC,EACnB,CAAC;YACF,OAAO,IAAI,CAAC;QACb,CAAC;QACD,IAAI,WAAW,CAAC,cAAc,IAAI,CAAC,YAAY,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;YACzE,OAAO,IAAI,CAAC;QACb,CAAC;QACD,OAAO,KAAK,CAAC;IAAA,CACb;IAED,2FAA2F;IAC3F,KAAK,UAAU,QAAQ,CAAC,OAAwB,EAAE,KAAmB,EAAE,GAAkB,EAAoB;QAC5G,IAAI,YAAY,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC;QAC3D,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;QACzB,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,CAAC,uBAAuB;QAClE,IAAI,GAAG,CAAC,sBAAsB,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC;QACvD,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAC/D,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,CAAC,iDAAiD;QACjF,MAAM,OAAO,CAAC,IAAI,CACjB,MAAM,CAAC,MAAM,EACb,2BAA2B,iBAAiB,CAAC,IAAI,CAAC,wDAAsD,MAAM,CAAC,QAAQ,IAAI,iBAAiB,CAAC,IAAI,CAAC,EAAE,EACpJ,EAAE,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CACvD,CAAC;QACF,OAAO,IAAI,CAAC;IAAA,CACZ;IAED,0EAA0E;IAC1E,KAAK,UAAU,WAAW,CAAC,OAAwB,EAAE,KAAmB,EAAE,GAAW,EAAoB;QACxG,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACxC,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QACzD,MAAM,WAAW,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAClD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACzB,MAAM,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,GAAG,MAAM,MAAM,CAAC,sBAAsB,CAAC,CAAC;YAC3F,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;gBAAE,oBAAoB,CAAC,GAAG,CAAC,CAAC;YAC1F,IAAI,WAAW,KAAK,QAAQ,EAAE,CAAC;gBAC9B,wBAAwB,CAAC,GAAG,CAAC,CAAC;gBAC9B,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,8CAA8C,EAAE;oBACvF,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ;iBAC/B,CAAC,CAAC;gBACH,OAAO,IAAI,CAAC;YACb,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,sBAAsB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,CAC1F,mBAAmB,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,CACxF,CAAC;YACF,IAAI,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC1B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,OAAO,KAAK,CAAC;QAC/C,CAAC;QACD,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC;QAClG,IAAI,CAAC,GAAG,EAAE,CAAC;YACV,OAAO,KAAK,CAAC,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC;QACvC,CAAC;QACD,MAAM,GAAG,GAAG;YACX,KAAK;YACL,GAAG;YACH,OAAO;YACP,MAAM;YACN,IAAI;YACJ,KAAK,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC;SACrE,CAAC;QACF,OAAO,sBAAsB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAAA,CACtG;IAED;;;;;gFAK4E;IAC5E,KAAK,UAAU,OAAO,CACrB,OAAwB,EACxB,KAAmB,EACnB,IAAY,EACZ,IAA4E,EAC5D;QAChB,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO;QAC9B,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;QAChE,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACxB,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBAC5B,wEAAwE;gBACxE,MAAM,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;gBAC7E,OAAO;YACR,CAAC;YACD,6DAA6D;YAC7D,MAAM,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9E,UAAU,GAAG,CAAC,CAAC;QAChB,CAAC;QACD,KAAK,IAAI,KAAK,GAAG,UAAU,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;YAC7D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE;gBACrE,OAAO,EAAE,IAAI,CAAC,KAAK,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;gBAChE,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ;aAC/B,CAAC,CAAC;YACH,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACrB,OAAO,CAAC,KAAK,CAAC,4BAA4B,OAAO,CAAC,QAAQ,KAAK,MAAM,CAAC,KAAK,IAAI,eAAe,EAAE,CAAC,CAAC;gBAClG,OAAO;YACR,CAAC;QACF,CAAC;IAAA,CACD;IAED,KAAK,UAAU,OAAO,CACrB,OAAwB,EACxB,KAAmB,EACnB,GAAW,EACX,GAAkB,EACF;QAChB,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC,OAAO,IAAI,OAAO,OAAO,CAAC,WAAW,KAAK,UAAU,CAAC;QACrF,IAAI,QAAoC,CAAC;QACzC,IAAI,SAAS,EAAE,CAAC;YACf,QAAQ,GAAG,IAAI,cAAc,CAAC;gBAC7B,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;oBAC5B,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE;wBAC5D,OAAO,EAAE,KAAK,CAAC,SAAS;wBACxB,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ;qBAC/B,CAAC,CAAC;oBACH,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBAAA,CAC1D;gBACD,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC;oBAChC,MAAM,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;gBAAA,CAChE;gBACD,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,cAAc;gBACxC,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,eAAe;gBACxC,UAAU,EAAE,OAAO,CAAC,gBAAgB;aACpC,CAAC,CAAC;QACJ,CAAC;QAED,MAAM,SAAS,GAAkB;YAChC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS;YAC/D,gBAAgB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;gBAC3B,KAAK,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YAAA,CACrD;YACD,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC;gBACrB,KAAK,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAAA,CACxC;SACD,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,sBAAsB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,CACxF,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,CAAC,CACrC,CAAC;QACF,IAAI,MAAM,KAAK,MAAM;YAAE,OAAO,CAAC,yCAAyC;QACxE,IAAI,QAAQ;YAAE,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACxC,MAAM,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE;YACrC,KAAK,EAAE,CAAC,QAAQ;YAChB,aAAa,EAAE,QAAQ,EAAE,aAAa,IAAI,SAAS;YACnD,gBAAgB,EAAE,QAAQ,EAAE,SAAS,IAAI,KAAK;SAC9C,CAAC,CAAC;IAAA,CACH;IAED,OAAO;QACN,KAAK,CAAC,WAAW,CAAC,KAAmB,EAAiB;YACrD,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YACpD,IAAI,CAAC,OAAO;gBAAE,OAAO;YACrB,MAAM,GAAG,GAAG,QAAQ,EAAE,CAAC;YACvB,IAAI,CAAC;gBACJ,IAAI,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC;oBAAE,OAAO;gBACrC,IAAI,MAAM,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC;oBAAE,OAAO;gBAChD,MAAM,GAAG,GAAG,eAAe,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,oBAAoB,EAAE,GAAG,CAAC,oBAAoB,EAAE,CAAC,CAAC;gBAC9F,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;oBACzB,MAAM,OAAO,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;oBACzC,IAAI,OAAO,EAAE,KAAK,EAAE,CAAC;wBACpB,mBAAmB,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;wBACvC,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,CAAC,MAAM,CAAC,MAAM;4BACxC,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;oBAChE,CAAC;oBACD,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;oBACpC,IAAI,kBAAkB,CAAC,GAAG,EAAE,KAAK,CAAC;wBAAE,OAAO;gBAC5C,CAAC;gBACD,IAAI,MAAM,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC;oBAAE,OAAO;gBACnD,MAAM,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;gBACrF,MAAM,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YACzC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACd,MAAM,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;YACnG,CAAC;QAAA,CACD;KACD,CAAC;AAAA,CACF","sourcesContent":["/**\n * lunR: gateway router.\n *\n * Every inbound MessageEvent flows through handleEvent:\n * 1. group gating (allowedChats / freeResponseChats / requireMention,\n * with adapter-supplied metadata.mentionedBot)\n * 2. authorization (authz.ts); denied DMs get a pairing code when the\n * behavior is \"pair\" and the user isn't rate-limited\n * 3. command registry (/new, /undo, /redo, /model, /sessions, /title,\n * /context, /compact, /thinking, /stop, /status, /help, /whoami)\n * 4. normal path: bridge.runTurn with a StreamConsumer when streaming is\n * enabled and the adapter supports edit; the final text is\n * silence-filtered and folded INTO the streaming preview (edit, plus\n * continuation chunks when the preview was truncated) — no preview +\n * final double-send. Without streaming it is split (text.ts) and sent\n * sequentially, the first chunk replying to the triggering message.\n * Errors surface as a compact \"⚠ <one-line>\" — never a stack trace.\n */\n\nimport { existsSync } from \"node:fs\";\n\nimport { type BridgeSession, type BridgeSessionStatus, QUEUED, type TurnCallbacks } from \"./agent-bridge.ts\";\nimport { registerGatewayApprovalHandler, runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized, requireGatewayOwner } from \"./authz.ts\";\nimport { CHAT_COMMANDS, runChatCommand, sendCommandReply } from \"./commands.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig, platformConfigFor } from \"./config.ts\";\nimport { bindConversation, conversationBinding } from \"./conversations.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport { acceptGatewayInput, invalidateGatewayDialogs } from \"./presenter.ts\";\nimport { buildSessionKey } from \"./session-keys.ts\";\nimport { applySilenceFilter, StreamConsumer } from \"./stream.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\n/** Structural bridge shape (AgentBridge satisfies it; tests fake it). */\nexport interface BridgeLike {\n\trunTurn(key: string, event: MessageEvent, callbacks: TurnCallbacks): Promise<string>;\n\tabort(key: string): Promise<void> | void;\n\treset(key: string): void | Promise<void>;\n\tgetStatus(key: string): BridgeSessionStatus;\n\tgetSession(key: string): Promise<BridgeSession | null>;\n\tswitchSession(key: string, sessionFile: string): Promise<void>;\n\tundo(key: string): Promise<{ userText: string }>;\n\tredo(key: string): Promise<void>;\n}\n\nexport interface RouterDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n\t/** Reload gateway.json on every inbound event so CLI config edits (e.g. pair approve) are picked up by the running daemon. */\n\treloadConfig?: boolean;\n\tremoteControls?: boolean;\n}\n\nexport interface Router {\n\thandleEvent(event: MessageEvent): Promise<void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction formatPairingCode(code: string): string {\n\treturn code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nfunction mentionedBot(event: MessageEvent): boolean {\n\treturn event.metadata?.mentionedBot === true;\n}\n\nexport function createRouter(deps: RouterDeps): Router {\n\tconst { adapters, cfg: initialCfg, pairing, bridge, reloadConfig } = deps;\n\n\t// Headless gateway sessions still prompt for large subagent launches in yolo mode.\n\tregisterGatewayApprovalHandler();\n\n\t/** Reload gateway.json on every inbound event so CLI edits (e.g. pair approve) take effect immediately. */\n\tfunction freshCfg(): GatewayConfig {\n\t\tif (!reloadConfig) return initialCfg;\n\t\ttry {\n\t\t\tif (existsSync(gatewayConfigPath())) {\n\t\t\t\treturn loadGatewayConfig();\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to initial config\n\t\t}\n\t\treturn initialCfg;\n\t}\n\n\tasync function sendError(adapter: PlatformAdapter, event: MessageEvent, message: string): Promise<void> {\n\t\tawait adapter.send(event.source.chatId, `⚠ ${oneLine(message)}`, {\n\t\t\treplyTo: event.messageId,\n\t\t\tthreadId: event.source.threadId,\n\t\t});\n\t}\n\n\t/** Step 1: group-chat early gating. Returns true when the event must be dropped. */\n\tfunction isGroupGated(event: MessageEvent, cfg: GatewayConfig): boolean {\n\t\tconst { source } = event;\n\t\tif (source.chatType === \"dm\") return false;\n\t\tconst platformCfg = platformConfigFor(cfg, source.platform);\n\t\tif (!platformCfg) return true;\n\t\tconst freeResponse = platformCfg.freeResponseChats.includes(source.chatId);\n\t\tif (\n\t\t\tplatformCfg.allowedChats.length > 0 &&\n\t\t\t!platformCfg.allowedChats.includes(source.chatId) &&\n\t\t\t!freeResponse &&\n\t\t\t!mentionedBot(event)\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\tif (platformCfg.requireMention && !freeResponse && !mentionedBot(event)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Step 2: authorization; handles the denied-DM pairing flow. Returns true when denied. */\n\tasync function isDenied(adapter: PlatformAdapter, event: MessageEvent, cfg: GatewayConfig): Promise<boolean> {\n\t\tif (isAuthorized(event.source, cfg, pairing)) return false;\n\t\tconst { source } = event;\n\t\tif (source.chatType !== \"dm\") return true; // denied group: silent\n\t\tif (cfg.unauthorizedDmBehavior !== \"pair\") return true;\n\t\tconst code = pairing.issueCode(source.platform, source.userId);\n\t\tif (code === null) return true; // rate-limited or pending list full: stay silent\n\t\tawait adapter.send(\n\t\t\tsource.chatId,\n\t\t\t`Your lunR pairing code: ${formatPairingCode(code)} — ask the owner to run: lunr gateway pair approve ${source.platform} ${formatPairingCode(code)}`,\n\t\t\t{ replyTo: event.messageId, threadId: source.threadId },\n\t\t);\n\t\treturn true;\n\t}\n\n\t/** Step 3: command registry. Returns true when the event was consumed. */\n\tasync function handleSlash(adapter: PlatformAdapter, event: MessageEvent, key: string): Promise<boolean> {\n\t\tconst text = event.text.trim();\n\t\tif (!text.startsWith(\"/\")) return false;\n\t\tconst firstToken = text.split(/\\s+/, 1)[0].toLowerCase();\n\t\tconst commandWord = firstToken.split(\"@\")[0].slice(1);\n\t\tconst args = text.slice(firstToken.length).trim();\n\t\tif (deps.remoteControls) {\n\t\t\tconst { handleMobileCommand, cancelMobileTransfer } = await import(\"./mobile-commands.ts\");\n\t\t\tif ([\"stop\", \"stopall\", \"new\", \"cancel\"].includes(commandWord)) cancelMobileTransfer(key);\n\t\t\tif (commandWord === \"cancel\") {\n\t\t\t\tinvalidateGatewayDialogs(key);\n\t\t\t\tawait adapter.send(event.source.chatId, \"Cancelled the pending selection or transfer.\", {\n\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t});\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst consumed = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\t\thandleMobileCommand({ key, event, adapter, bridge, cfg: freshCfg() }, commandWord, args),\n\t\t\t);\n\t\t\tif (consumed) return true;\n\t\t\tif (!event.text.startsWith(\"/\")) return false;\n\t\t}\n\t\tconst cmd = CHAT_COMMANDS.find((c) => c.name === commandWord || c.aliases?.includes(commandWord));\n\t\tif (!cmd) {\n\t\t\treturn event.source.chatType !== \"dm\";\n\t\t}\n\t\tconst ctx = {\n\t\t\tevent,\n\t\t\tkey,\n\t\t\tadapter,\n\t\t\tbridge,\n\t\t\targs,\n\t\t\treply: (message: string) => sendCommandReply(adapter, event, message),\n\t\t};\n\t\treturn runWithApprovalContext({ key, adapter, source: event.source }, () => runChatCommand(cmd, ctx));\n\t}\n\n\t/** Step 4 delivery: silence filter → split → sequential send.\n\t *\n\t * When a streaming preview message exists (editMessageId), the final text\n\t * is folded INTO it instead of sent as a duplicate: non-truncated previews\n\t * get a final edit (covers silence-marker stripping), truncated previews\n\t * are edited to chunk 1 and the remaining chunks are sent as follow-ups. */\n\tasync function deliver(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\ttext: string,\n\t\topts: { reply: boolean; editMessageId?: string; previewTruncated?: boolean },\n\t): Promise<void> {\n\t\tconst filtered = applySilenceFilter(text);\n\t\tif (filtered === null) return;\n\t\tconst chunks = splitMessage(filtered, adapter.maxMessageLength);\n\t\tlet startIndex = 0;\n\t\tif (opts.editMessageId) {\n\t\t\tif (!opts.previewTruncated) {\n\t\t\t\t// Preview already holds the whole text: one final edit, no new message.\n\t\t\t\tawait adapter.editMessage(event.source.chatId, opts.editMessageId, filtered);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Preview was truncated: upgrade it to the first full chunk.\n\t\t\tawait adapter.editMessage(event.source.chatId, opts.editMessageId, chunks[0]);\n\t\t\tstartIndex = 1;\n\t\t}\n\t\tfor (let index = startIndex; index < chunks.length; index++) {\n\t\t\tconst result = await adapter.send(event.source.chatId, chunks[index], {\n\t\t\t\treplyTo: opts.reply && index === 0 ? event.messageId : undefined,\n\t\t\t\tthreadId: event.source.threadId,\n\t\t\t});\n\t\t\tif (!result.success) {\n\t\t\t\tconsole.error(`[gateway] send failed on ${adapter.platform}: ${result.error ?? \"unknown error\"}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync function runTurn(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t): Promise<void> {\n\t\tconst streaming = cfg.streaming.enabled && typeof adapter.editMessage === \"function\";\n\t\tlet consumer: StreamConsumer | undefined;\n\t\tif (streaming) {\n\t\t\tconsumer = new StreamConsumer({\n\t\t\t\tsendInitial: async (text) => {\n\t\t\t\t\tconst result = await adapter.send(event.source.chatId, text, {\n\t\t\t\t\t\treplyTo: event.messageId,\n\t\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t\t});\n\t\t\t\t\treturn result.success ? (result.messageId ?? null) : null;\n\t\t\t\t},\n\t\t\t\tedit: async (messageId, text) => {\n\t\t\t\t\tawait adapter.editMessage(event.source.chatId, messageId, text);\n\t\t\t\t},\n\t\t\t\tintervalMs: cfg.streaming.editIntervalMs,\n\t\t\t\tthreshold: cfg.streaming.bufferThreshold,\n\t\t\t\tmaxPreview: adapter.maxMessageLength,\n\t\t\t});\n\t\t}\n\n\t\tconst callbacks: TurnCallbacks = {\n\t\t\tonDelta: consumer ? (delta) => consumer.push(delta) : undefined,\n\t\t\tonFollowUpResult: (text) => {\n\t\t\t\tvoid deliver(adapter, event, text, { reply: false });\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tvoid sendError(adapter, event, message);\n\t\t\t},\n\t\t};\n\n\t\tconst result = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\tbridge.runTurn(key, event, callbacks),\n\t\t);\n\t\tif (result === QUEUED) return; // queued behind a running turn: no reply\n\t\tif (consumer) await consumer.finalize();\n\t\tawait deliver(adapter, event, result, {\n\t\t\treply: !consumer,\n\t\t\teditMessageId: consumer?.sentMessageId ?? undefined,\n\t\t\tpreviewTruncated: consumer?.truncated ?? false,\n\t\t});\n\t}\n\n\treturn {\n\t\tasync handleEvent(event: MessageEvent): Promise<void> {\n\t\t\tconst adapter = adapters.get(event.source.platform);\n\t\t\tif (!adapter) return;\n\t\t\tconst cfg = freshCfg();\n\t\t\ttry {\n\t\t\t\tif (isGroupGated(event, cfg)) return;\n\t\t\t\tif (await isDenied(adapter, event, cfg)) return;\n\t\t\t\tconst key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });\n\t\t\t\tif (deps.remoteControls) {\n\t\t\t\t\tconst binding = conversationBinding(key);\n\t\t\t\t\tif (binding?.owner) {\n\t\t\t\t\t\trequireGatewayOwner(event.source, cfg);\n\t\t\t\t\t\tif (binding.owner !== event.source.userId)\n\t\t\t\t\t\t\tthrow new Error(\"This session belongs to a different owner.\");\n\t\t\t\t\t}\n\t\t\t\t\tbindConversation(key, event.source);\n\t\t\t\t\tif (acceptGatewayInput(key, event)) return;\n\t\t\t\t}\n\t\t\t\tif (await handleSlash(adapter, event, key)) return;\n\t\t\t\tawait adapter.sendTyping(event.source.chatId, event.source.threadId).catch(() => {});\n\t\t\t\tawait runTurn(adapter, event, key, cfg);\n\t\t\t} catch (err) {\n\t\t\t\tawait sendError(adapter, event, err instanceof Error ? err.message : String(err)).catch(() => {});\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/gateway/router.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway router.\n *\n * Every inbound MessageEvent flows through handleEvent:\n * 1. group gating (allowedChats / freeResponseChats / requireMention,\n * with adapter-supplied metadata.mentionedBot)\n * 2. authorization (authz.ts); denied DMs get a pairing code when the\n * behavior is \"pair\" and the user isn't rate-limited\n * 3. command registry (/new, /undo, /redo, /model, /sessions, /title,\n * /context, /compact, /thinking, /stop, /status, /help, /whoami)\n * 4. normal path: bridge.runTurn with a StreamConsumer when streaming is\n * enabled and the adapter supports edit; the final text is\n * silence-filtered and folded into a successful streaming preview edit.\n * Failed final edits and ordinary answers enter the durable outbox, which\n * acknowledges chunks in order and retries by destination.\n * Errors surface as a compact \"⚠ <one-line>\" — never a stack trace.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { type BridgeSession, type BridgeSessionStatus, QUEUED, type TurnCallbacks } from \"./agent-bridge.ts\";\nimport { registerGatewayApprovalHandler, runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized, requireAuthorized } from \"./authz.ts\";\nimport { CHAT_COMMANDS, runChatCommand, sendCommandReply } from \"./commands.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig, platformConfigFor } from \"./config.ts\";\nimport { bindConversation, conversationBinding } from \"./conversations.ts\";\nimport { resolveWithinRoots } from \"./mobile-commands.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport {\n\tacceptGatewayInput,\n\tgatewayEpoch,\n\tinvalidateGatewayDialogs,\n\tqueueGatewayText,\n\trememberGatewayRole,\n} from \"./presenter.ts\";\nimport { buildSessionKey } from \"./session-keys.ts\";\nimport { getSession as getStoredSession } from \"./store.ts\";\nimport { applySilenceFilter, StreamConsumer } from \"./stream.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\n/** Structural bridge shape (AgentBridge satisfies it; tests fake it). */\nexport interface BridgeLike {\n\trunTurn(key: string, event: MessageEvent, callbacks: TurnCallbacks): Promise<string>;\n\tabort(key: string): Promise<void> | void;\n\treset(key: string): void | Promise<void>;\n\tgetStatus(key: string): BridgeSessionStatus;\n\tgetSession(key: string, create?: boolean): Promise<BridgeSession | null>;\n\tpeekSession?(key: string): BridgeSession | undefined;\n\tswitchSession(key: string, sessionFile: string): Promise<void>;\n\tundo(key: string): Promise<{ userText: string }>;\n\tredo(key: string): Promise<void>;\n}\n\nexport interface RouterDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n\t/** Reload gateway.json on every inbound event so CLI config edits (e.g. pair approve) are picked up by the running daemon. */\n\treloadConfig?: boolean;\n\tremoteControls?: boolean;\n}\n\nexport interface Router {\n\thandleEvent(event: MessageEvent): Promise<void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction formatPairingCode(code: string): string {\n\treturn code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nfunction mentionedBot(event: MessageEvent): boolean {\n\treturn event.metadata?.mentionedBot === true;\n}\n\nexport function createRouter(deps: RouterDeps): Router {\n\tconst { adapters, cfg: initialCfg, pairing, bridge, reloadConfig } = deps;\n\n\t// Headless gateway sessions still prompt for large subagent launches in yolo mode.\n\tregisterGatewayApprovalHandler();\n\n\t/** Reload gateway.json on every inbound event so CLI edits (e.g. pair approve) take effect immediately. */\n\tfunction freshCfg(): GatewayConfig {\n\t\tif (!reloadConfig) return initialCfg;\n\t\ttry {\n\t\t\tif (existsSync(gatewayConfigPath())) {\n\t\t\t\treturn loadGatewayConfig();\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to initial config\n\t\t}\n\t\treturn initialCfg;\n\t}\n\n\tasync function sendError(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t\tmessage: string,\n\t\tepoch: number,\n\t): Promise<void> {\n\t\tawait queueGatewayText(key, event.source, adapter, `⚠ ${oneLine(message)}`, {\n\t\t\tkind: \"notice\",\n\t\t\treplyTo: event.messageId,\n\t\t\tcfg,\n\t\t\tepoch,\n\t\t});\n\t}\n\n\t/** Step 1: group-chat early gating. Returns true when the event must be dropped. */\n\tfunction isGroupGated(event: MessageEvent, cfg: GatewayConfig): boolean {\n\t\tconst { source } = event;\n\t\tif (source.chatType === \"dm\") return false;\n\t\tconst platformCfg = platformConfigFor(cfg, source.platform);\n\t\tif (!platformCfg) return true;\n\t\tconst freeResponse = platformCfg.freeResponseChats.includes(source.chatId);\n\t\tif (\n\t\t\tplatformCfg.allowedChats.length > 0 &&\n\t\t\t!platformCfg.allowedChats.includes(source.chatId) &&\n\t\t\t!freeResponse &&\n\t\t\t!mentionedBot(event)\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\tif (platformCfg.requireMention && !freeResponse && !mentionedBot(event)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Step 2: authorization; handles the denied-DM pairing flow. Returns true when denied. */\n\tasync function isDenied(adapter: PlatformAdapter, event: MessageEvent, cfg: GatewayConfig): Promise<boolean> {\n\t\tif (isAuthorized(event.source, cfg, pairing)) return false;\n\t\tconst { source } = event;\n\t\tif (source.chatType !== \"dm\") return true; // denied group: silent\n\t\tif (cfg.unauthorizedDmBehavior !== \"pair\") return true;\n\t\tconst code = pairing.issueCode(source.platform, source.userId);\n\t\tif (code === null) return true; // rate-limited or pending list full: stay silent\n\t\tawait adapter.send(\n\t\t\tsource.chatId,\n\t\t\t`Your lunR pairing code: ${formatPairingCode(code)}. On the computer running lunr, approve access with: lunr gateway pair approve ${source.platform} ${formatPairingCode(code)}. Approval grants full gateway access, including local projects and saved sessions.`,\n\t\t\t{ replyTo: event.messageId, threadId: source.threadId },\n\t\t);\n\t\treturn true;\n\t}\n\n\tfunction workspaceReadiness(event: MessageEvent, key: string, cfg: GatewayConfig): string | undefined {\n\t\tif (getStoredSession(key) || conversationBinding(key)?.cwd) return undefined;\n\t\tconst root = cfg.defaultProject;\n\t\tif (!root) return \"No default project selected. Use /project to choose an approved folder.\";\n\t\ttry {\n\t\t\tconst cwd = resolveWithinRoots(root, cfg.projectRoots ?? []);\n\t\t\tbindConversation(key, event.source, { cwd, owner: event.source.userId });\n\t\t\treturn undefined;\n\t\t} catch {\n\t\t\treturn \"Default project is no longer an approved folder. Use /project to choose an approved folder.\";\n\t\t}\n\t}\n\n\t/** Step 3: command registry. Returns true when the event was consumed. */\n\tasync function handleSlash(adapter: PlatformAdapter, event: MessageEvent, key: string): Promise<boolean> {\n\t\tconst text = event.text.trim();\n\t\tif (!text.startsWith(\"/\")) return false;\n\t\tconst firstToken = text.split(/\\s+/, 1)[0].toLowerCase();\n\t\tconst commandWord = firstToken.split(\"@\")[0].slice(1);\n\t\tconst args = text.slice(firstToken.length).trim();\n\t\tif (deps.remoteControls) {\n\t\t\tconst { handleMobileCommand, cancelMobileTransfer } = await import(\"./mobile-commands.ts\");\n\t\t\tif ([\"stop\", \"stopall\", \"new\", \"cancel\"].includes(commandWord)) cancelMobileTransfer(key);\n\t\t\tif (commandWord === \"cancel\") {\n\t\t\t\tinvalidateGatewayDialogs(key);\n\t\t\t\tawait adapter.send(event.source.chatId, \"Cancelled the pending selection or transfer.\", {\n\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t});\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst consumed = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\t\thandleMobileCommand({ key, event, adapter, bridge, cfg: freshCfg() }, commandWord, args),\n\t\t\t);\n\t\t\tif (consumed) return true;\n\t\t\tif (!event.text.startsWith(\"/\")) return false;\n\t\t}\n\t\tif (commandWord === \"start\") {\n\t\t\tconst issue = deps.remoteControls ? workspaceReadiness(event, key, freshCfg()) : undefined;\n\t\t\tif (issue) {\n\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst session = await bridge.getSession(key, true);\n\t\t\t\tconst models = await session?.modelRuntime.getAvailable();\n\t\t\t\tif (!models?.length)\n\t\t\t\t\tawait sendCommandReply(\n\t\t\t\t\t\tadapter,\n\t\t\t\t\t\tevent,\n\t\t\t\t\t\t\"No authenticated model available. Use /login on your computer, then try /start again.\",\n\t\t\t\t\t);\n\t\t\t\telse if (\n\t\t\t\t\t!session?.model ||\n\t\t\t\t\t!models.some((model) => model.id === session.model?.id && model.provider === session.model.provider)\n\t\t\t\t)\n\t\t\t\t\tawait sendCommandReply(adapter, event, \"No available model selected. Use /model here to choose one.\");\n\t\t\t\telse\n\t\t\t\t\tawait sendCommandReply(\n\t\t\t\t\t\tadapter,\n\t\t\t\t\t\tevent,\n\t\t\t\t\t\t\"Ready. Send a message to begin, or use /model to choose a model.\",\n\t\t\t\t\t);\n\t\t\t} catch {\n\t\t\t\tawait sendCommandReply(\n\t\t\t\t\tadapter,\n\t\t\t\t\tevent,\n\t\t\t\t\t\"Session setup failed. Run lunr gateway doctor on your computer, then try /start again.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tif (deps.remoteControls && commandWord === \"model\") {\n\t\t\tconst issue = workspaceReadiness(event, key, freshCfg());\n\t\t\tif (issue) {\n\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\tconst cmd = CHAT_COMMANDS.find((c) => c.name === commandWord || c.aliases?.includes(commandWord));\n\t\tif (!cmd) {\n\t\t\treturn event.source.chatType !== \"dm\";\n\t\t}\n\t\tconst ctx = {\n\t\t\tevent,\n\t\t\tkey,\n\t\t\tadapter,\n\t\t\tbridge,\n\t\t\tcfg: freshCfg(),\n\t\t\targs,\n\t\t\treply: (message: string) => sendCommandReply(adapter, event, message),\n\t\t};\n\t\treturn runWithApprovalContext({ key, adapter, source: event.source }, () => runChatCommand(cmd, ctx));\n\t}\n\n\t/** A successful preview is finalized by edit; failed edits fall back to a durable full reply. */\n\tasync function deliver(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\ttext: string,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t\topts: { reply: boolean; editMessageId?: string; previewTruncated?: boolean },\n\t): Promise<void> {\n\t\tconst filtered = applySilenceFilter(text);\n\t\tif (filtered === null) return;\n\t\tif (opts.editMessageId) {\n\t\t\tconst chunks = splitMessage(filtered, adapter.maxMessageLength);\n\t\t\tconst edit = await adapter\n\t\t\t\t.editMessage(event.source.chatId, opts.editMessageId, opts.previewTruncated ? chunks[0] : filtered)\n\t\t\t\t.catch(() => ({ success: false }));\n\t\t\tif (edit.success) {\n\t\t\t\tif (!opts.previewTruncated) return;\n\t\t\t\tawait queueGatewayText(key, event.source, adapter, chunks.slice(1).join(\"\"), {\n\t\t\t\t\tkind: \"result\",\n\t\t\t\t\tcfg,\n\t\t\t\t\tchunks: chunks.slice(1),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tawait queueGatewayText(key, event.source, adapter, filtered, {\n\t\t\tkind: \"result\",\n\t\t\treplyTo: opts.reply ? event.messageId : undefined,\n\t\t\tcfg,\n\t\t});\n\t}\n\n\tasync function runTurn(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t): Promise<void> {\n\t\tconst epoch = gatewayEpoch(key);\n\t\tconst streaming = cfg.streaming.enabled && typeof adapter.editMessage === \"function\";\n\t\tlet consumer: StreamConsumer | undefined;\n\t\tif (streaming) {\n\t\t\tconsumer = new StreamConsumer({\n\t\t\t\tsendInitial: async (text) => {\n\t\t\t\t\tconst result = await adapter.send(event.source.chatId, text, {\n\t\t\t\t\t\treplyTo: event.messageId,\n\t\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t\t});\n\t\t\t\t\treturn result.success ? (result.messageId ?? null) : null;\n\t\t\t\t},\n\t\t\t\tedit: async (messageId, text) => {\n\t\t\t\t\tconst result = await adapter.editMessage(event.source.chatId, messageId, text);\n\t\t\t\t\tif (!result.success) throw new Error(result.error ?? \"Preview edit failed\");\n\t\t\t\t},\n\t\t\t\tintervalMs: cfg.streaming.editIntervalMs,\n\t\t\t\tthreshold: cfg.streaming.bufferThreshold,\n\t\t\t\tmaxPreview: adapter.maxMessageLength,\n\t\t\t});\n\t\t}\n\n\t\tconst callbacks: TurnCallbacks = {\n\t\t\tonDelta: consumer ? (delta) => consumer.push(delta) : undefined,\n\t\t\tonFollowUpResult: (text) => {\n\t\t\t\tvoid deliver(adapter, event, text, key, cfg, { reply: false }).catch((error) =>\n\t\t\t\t\tconsole.error(\"[gateway] follow-up delivery failed:\", error),\n\t\t\t\t);\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tvoid sendError(adapter, event, key, cfg, message, epoch).catch((error) =>\n\t\t\t\t\tconsole.error(\"[gateway] error delivery failed:\", error),\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\n\t\tconst result = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\tbridge.runTurn(key, event, callbacks),\n\t\t);\n\t\tif (result === QUEUED) return; // queued behind a running turn: no reply\n\t\tif (consumer) await consumer.finalize();\n\t\tawait deliver(adapter, event, result, key, cfg, {\n\t\t\treply: !consumer,\n\t\t\teditMessageId: consumer?.sentMessageId ?? undefined,\n\t\t\tpreviewTruncated: consumer?.truncated ?? false,\n\t\t});\n\t}\n\n\treturn {\n\t\tasync handleEvent(event: MessageEvent): Promise<void> {\n\t\t\tconst adapter = adapters.get(event.source.platform);\n\t\t\tif (!adapter) return;\n\t\t\tconst cfg = freshCfg();\n\t\t\tconst key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });\n\t\t\tconst epoch = gatewayEpoch(key);\n\t\t\ttry {\n\t\t\t\tif (isGroupGated(event, cfg)) return;\n\t\t\t\trememberGatewayRole(key, event.source);\n\t\t\t\tif (await isDenied(adapter, event, cfg)) return;\n\t\t\t\tif (deps.remoteControls) {\n\t\t\t\t\tconst binding = conversationBinding(key);\n\t\t\t\t\tif (binding?.owner) {\n\t\t\t\t\t\trequireAuthorized(event.source, cfg);\n\t\t\t\t\t\tif (binding.owner !== event.source.userId)\n\t\t\t\t\t\t\tthrow new Error(\"This session belongs to a different user.\");\n\t\t\t\t\t}\n\t\t\t\t\tbindConversation(key, event.source);\n\t\t\t\t\tif (acceptGatewayInput(key, event)) return;\n\t\t\t\t}\n\t\t\t\tif (await handleSlash(adapter, event, key)) return;\n\t\t\t\tconst issue = deps.remoteControls ? workspaceReadiness(event, key, cfg) : undefined;\n\t\t\t\tif (issue) {\n\t\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait adapter.sendTyping(event.source.chatId, event.source.threadId).catch(() => {});\n\t\t\t\tawait runTurn(adapter, event, key, cfg);\n\t\t\t} catch (err) {\n\t\t\t\tawait sendError(adapter, event, key, cfg, err instanceof Error ? err.message : String(err), epoch).catch(\n\t\t\t\t\t() => {},\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/gateway/service.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;AACnD,MAAM,WAAW,sBAAsB;IACtC,OAAO,EAAE,WAAW,CAAC;CACrB;AACD,MAAM,WAAW,oBAAoB;IACpC,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,CAAC;IACzC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,eAAO,MAAM,gBAAgB,cAA+C,CAAC;AAC7E,eAAO,MAAM,cAAc,cAAgD,CAAC;AAK5E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAK7D;AAUD,wBAAgB,mBAAmB,IAAI,sBAAsB,CAM5D;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQlD;AAED,wBAAgB,iBAAiB,CAAC,IAAI,SAAe,GAAG,oBAAoB,GAAG,SAAS,CA4BvF;AAED,wBAAgB,gBAAgB,IAAI,OAAO,CAG1C;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG;IACjD,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,EAAE,oBAAoB,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IAC3F,OAAO,EAAE,MAAM,IAAI,CAAC;CACpB,CAwEA;AAUD,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC,CA6C3D;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAS1D;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;CAChB;AAWD,wBAAgB,WAAW,CAC1B,QAAQ,EAAE,MAAM,CAAC,QAAQ,EACzB,IAAI,EAAE,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,EACjC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACnG,WAAW,CA+Db;AASD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiDvE;AAuBD,wBAAgB,iBAAiB,IAAI,MAAM,CAY1C;AAED,wBAAgB,cAAc,IAAI,MAAM,CAUvC","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir, userInfo } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getAgentDir } from \"../config.ts\";\n\nexport type StartupMode = \"off\" | \"login\" | \"boot\";\nexport interface GatewayServiceSettings {\n\tstartup: StartupMode;\n}\nexport interface GatewayRuntimeStatus {\n\tversion: 1;\n\tinstance: string;\n\tpid: number;\n\tstartedAt: string;\n\tupdatedAt: string;\n\tstate: \"starting\" | \"ready\" | \"stopping\";\n\tplatforms: Record<string, string>;\n}\nconst servicePrefix = \"lunr-gateway\";\nexport const serviceDirectory = () => join(getAgentDir(), \"gateway-service\");\nexport const gatewayLogPath = () => join(serviceDirectory(), \"gateway.log\");\nconst statusPath = () => join(serviceDirectory(), \"status.json\");\nconst controlPath = () => join(serviceDirectory(), \"control.json\");\nconst settingsPath = () => join(serviceDirectory(), \"service.json\");\n\nexport function atomicJson(path: string, value: unknown): void {\n\tmkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n\tconst temp = `${path}.${randomUUID()}.tmp`;\n\twriteFileSync(temp, `${JSON.stringify(value, null, 2)}\\n`, { mode: 0o600 });\n\trenameSync(temp, path);\n}\n\nfunction readJson(path: string): unknown {\n\ttry {\n\t\treturn JSON.parse(readFileSync(path, \"utf8\"));\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function loadServiceSettings(): GatewayServiceSettings {\n\tconst raw = readJson(settingsPath());\n\tif (raw && typeof raw === \"object\" && \"startup\" in raw && (raw.startup === \"login\" || raw.startup === \"boot\")) {\n\t\treturn { startup: raw.startup };\n\t}\n\treturn { startup: \"off\" };\n}\n\nexport function processExists(pid: number): boolean {\n\tif (!Number.isSafeInteger(pid) || pid <= 0) return false;\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn !(error && typeof error === \"object\" && \"code\" in error && error.code === \"ESRCH\");\n\t}\n}\n\nexport function readGatewayStatus(file = statusPath()): GatewayRuntimeStatus | undefined {\n\tconst raw = readJson(file);\n\tif (\n\t\t!raw ||\n\t\ttypeof raw !== \"object\" ||\n\t\t!(\"version\" in raw) ||\n\t\traw.version !== 1 ||\n\t\t!(\"pid\" in raw) ||\n\t\ttypeof raw.pid !== \"number\"\n\t)\n\t\treturn undefined;\n\tif (\n\t\t!(\"instance\" in raw) ||\n\t\ttypeof raw.instance !== \"string\" ||\n\t\t!(\"updatedAt\" in raw) ||\n\t\ttypeof raw.updatedAt !== \"string\"\n\t)\n\t\treturn undefined;\n\tif (!(\"state\" in raw) || ![\"starting\", \"ready\", \"stopping\"].includes(String(raw.state))) return undefined;\n\tif (\n\t\t!(\"platforms\" in raw) ||\n\t\t!raw.platforms ||\n\t\ttypeof raw.platforms !== \"object\" ||\n\t\t!(\"startedAt\" in raw) ||\n\t\ttypeof raw.startedAt !== \"string\"\n\t)\n\t\treturn undefined;\n\treturn raw as GatewayRuntimeStatus;\n}\n\nexport function gatewayIsRunning(): boolean {\n\tconst status = readGatewayStatus();\n\treturn !!status && processExists(status.pid);\n}\n\nexport function claimGateway(onStop: () => void): {\n\tupdate: (platforms: Record<string, string>, state?: GatewayRuntimeStatus[\"state\"]) => void;\n\trelease: () => void;\n} {\n\tconst dir = serviceDirectory();\n\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\tconst lock = join(dir, \"lock\");\n\ttry {\n\t\tmkdirSync(lock);\n\t} catch (error) {\n\t\tif (!error || typeof error !== \"object\" || !(\"code\" in error) || error.code !== \"EEXIST\") throw error;\n\t\tconst previous = readGatewayStatus(join(lock, \"owner.json\"));\n\t\tif (!previous || processExists(previous.pid))\n\t\t\tthrow new Error(\"Gateway already running or startup lock is unresolved. Run lunr gateway doctor.\");\n\t\t// Recovery is serialized; a late recoverer must not remove a new owner's lock.\n\t\tconst recovery = join(dir, \"recovery\");\n\t\tmkdirSync(recovery);\n\t\ttry {\n\t\t\tconst current = readGatewayStatus(join(lock, \"owner.json\"));\n\t\t\tif (!current || current.instance !== previous.instance || processExists(current.pid))\n\t\t\t\tthrow new Error(\"Gateway ownership changed. Try again.\");\n\t\t\trmSync(lock, { recursive: true });\n\t\t\tmkdirSync(lock);\n\t\t} finally {\n\t\t\trmSync(recovery, { recursive: true, force: true });\n\t\t}\n\t}\n\tconst status: GatewayRuntimeStatus = {\n\t\tversion: 1,\n\t\tinstance: randomUUID(),\n\t\tpid: process.pid,\n\t\tstartedAt: new Date().toISOString(),\n\t\tupdatedAt: new Date().toISOString(),\n\t\tstate: \"starting\",\n\t\tplatforms: {},\n\t};\n\tatomicJson(join(lock, \"owner.json\"), status);\n\tconst save = () => {\n\t\tstatus.updatedAt = new Date().toISOString();\n\t\tatomicJson(statusPath(), status);\n\t};\n\tsave();\n\tlet stopping = false;\n\tconst timer = setInterval(() => {\n\t\tsave();\n\t\tconst control = readJson(controlPath());\n\t\tif (\n\t\t\t!stopping &&\n\t\t\tcontrol &&\n\t\t\ttypeof control === \"object\" &&\n\t\t\t\"instance\" in control &&\n\t\t\tcontrol.instance === status.instance &&\n\t\t\t\"action\" in control &&\n\t\t\tcontrol.action === \"stop\"\n\t\t) {\n\t\t\tstopping = true;\n\t\t\tstatus.state = \"stopping\";\n\t\t\tonStop();\n\t\t}\n\t}, 500);\n\treturn {\n\t\tupdate(platforms, state = \"ready\") {\n\t\t\tstatus.platforms = { ...platforms };\n\t\t\tstatus.state = state;\n\t\t\tsave();\n\t\t},\n\t\trelease() {\n\t\t\tclearInterval(timer);\n\t\t\tif (readGatewayStatus()?.instance === status.instance) {\n\t\t\t\trmSync(statusPath(), { force: true });\n\t\t\t\trmSync(controlPath(), { force: true });\n\t\t\t\trmSync(lock, { recursive: true, force: true });\n\t\t\t}\n\t\t},\n\t};\n}\n\nfunction cliPath(): string {\n\tconst candidate = process.argv[1];\n\tif (candidate && existsSync(candidate)) return resolve(candidate);\n\treturn fileURLToPath(new URL(\"../cli.js\", import.meta.url));\n}\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\nexport async function startGatewayService(): Promise<string> {\n\tif (gatewayIsRunning()) return \"Gateway is already running.\";\n\tmkdirSync(serviceDirectory(), { recursive: true, mode: 0o700 });\n\tconst startup = loadServiceSettings().startup;\n\tif (startup !== \"off\") {\n\t\tconst user = userInfo();\n\t\tconst spec = startupSpec(process.platform, startup, {\n\t\t\thome: homedir(),\n\t\t\tagentDir: getAgentDir(),\n\t\t\tnode: process.execPath,\n\t\t\tcli: cliPath(),\n\t\t\tusername: user.username,\n\t\t\tuid: user.uid,\n\t\t});\n\t\texecute(spec.start);\n\t\tfor (let i = 0; i < 100; i++) {\n\t\t\tif (readGatewayStatus()?.state === \"ready\" && gatewayIsRunning()) return \"Gateway started.\";\n\t\t\tawait sleep(200);\n\t\t}\n\t\tthrow new Error(`Startup service did not become ready. Run lunr gateway doctor and inspect ${gatewayLogPath()}`);\n\t}\n\tconst { openSync, closeSync } = await import(\"node:fs\");\n\tconst log = openSync(gatewayLogPath(), \"a\", 0o600);\n\tconst child = spawn(process.execPath, [cliPath(), \"gateway\", \"run\"], {\n\t\tcwd: homedir(),\n\t\tdetached: true,\n\t\twindowsHide: true,\n\t\tstdio: [\"ignore\", log, log],\n\t\tenv: { ...process.env, PI_CODING_AGENT_DIR: getAgentDir() },\n\t});\n\tcloseSync(log);\n\tlet spawnError: Error | undefined;\n\tchild.on(\"error\", (error) => {\n\t\tspawnError = error;\n\t});\n\tchild.unref();\n\tfor (let i = 0; i < 100; i++) {\n\t\tif (spawnError) throw spawnError;\n\t\tconst status = readGatewayStatus();\n\t\tif (status && status.pid === child.pid && status.state === \"ready\") return \"Gateway started.\";\n\t\tif (child.exitCode !== null)\n\t\t\tthrow new Error(`Gateway exited with code ${child.exitCode}. See ${gatewayLogPath()}`);\n\t\tawait sleep(200);\n\t}\n\tthrow new Error(`Gateway did not become ready within 20 seconds. Check lunr gateway status and ${gatewayLogPath()}`);\n}\n\nexport async function stopGatewayService(): Promise<string> {\n\tconst status = readGatewayStatus();\n\tif (!status || !processExists(status.pid)) return \"Gateway is not running.\";\n\tatomicJson(controlPath(), { instance: status.instance, action: \"stop\" });\n\tfor (let i = 0; i < 150; i++) {\n\t\tif (readGatewayStatus()?.instance !== status.instance || !processExists(status.pid)) return \"Gateway stopped.\";\n\t\tawait sleep(200);\n\t}\n\tthrow new Error(\"Gateway is still stopping after 30 seconds. Inspect the log; no unrelated process was terminated.\");\n}\n\nexport interface StartupSpec {\n\tpath: string;\n\tcontent: string;\n\tinstall: string[][];\n\tremove: string[][];\n\tverify: string[];\n\tstart: string[];\n}\nfunction xml(value: string): string {\n\treturn value.replaceAll(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\");\n}\nfunction systemdArg(value: string): string {\n\treturn `\"${value.replaceAll(\"%\", \"%%\").replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll('\"', '\\\\\"')}\"`;\n}\nfunction ps(value: string): string {\n\treturn `'${value.replaceAll(\"'\", \"''\")}'`;\n}\n\nexport function startupSpec(\n\tplatform: NodeJS.Platform,\n\tmode: Exclude<StartupMode, \"off\">,\n\toptions: { home: string; agentDir: string; node: string; cli: string; username: string; uid: number },\n): StartupSpec {\n\tconst { home, agentDir, node, cli, username, uid } = options;\n\tif ([home, agentDir, node, cli, username].some((value) => /[\\r\\n\\0]/.test(value)))\n\t\tthrow new Error(\"Startup paths and account names must not contain control characters.\");\n\tconst suffix = createHash(\"sha256\").update(resolve(agentDir)).digest(\"hex\").slice(0, 10);\n\tconst serviceName = `${servicePrefix}-${suffix}`;\n\tconst log = join(agentDir, \"gateway-service\", \"gateway.log\");\n\tif (platform === \"linux\") {\n\t\tconst path = join(home, \".config\", \"systemd\", \"user\", `${serviceName}.service`);\n\t\treturn {\n\t\t\tpath,\n\t\t\tcontent: `[Unit]\\nDescription=lunR chat gateway\\nAfter=network-online.target\\n\\n[Service]\\nType=simple\\nExecStart=${systemdArg(node)} ${systemdArg(cli)} gateway run\\nWorkingDirectory=${systemdArg(home)}\\nEnvironment=${systemdArg(`PI_CODING_AGENT_DIR=${agentDir}`)}\\nRestart=on-failure\\nRestartSec=5\\nTimeoutStopSec=30\\nStandardOutput=append:${log.replaceAll(\"%\", \"%%\")}\\nStandardError=append:${log.replaceAll(\"%\", \"%%\")}\\nUMask=0077\\n\\n[Install]\\nWantedBy=default.target\\n`,\n\t\t\tinstall: [\n\t\t\t\t...(mode === \"boot\" ? [[\"loginctl\", \"enable-linger\", username]] : []),\n\t\t\t\t[\"systemctl\", \"--user\", \"daemon-reload\"],\n\t\t\t\t[\"systemctl\", \"--user\", \"enable\", `${serviceName}.service`],\n\t\t\t],\n\t\t\tremove: [[\"systemctl\", \"--user\", \"disable\", `${serviceName}.service`]],\n\t\t\tverify: [\"systemctl\", \"--user\", \"is-enabled\", `${serviceName}.service`],\n\t\t\tstart: [\"systemctl\", \"--user\", \"start\", `${serviceName}.service`],\n\t\t};\n\t}\n\tif (platform === \"darwin\") {\n\t\tconst label = `dev.lunr.gateway.${suffix}`;\n\t\tconst path =\n\t\t\tmode === \"boot\"\n\t\t\t\t? `/Library/LaunchDaemons/${label}.plist`\n\t\t\t\t: join(home, \"Library\", \"LaunchAgents\", `${label}.plist`);\n\t\tconst domain = mode === \"boot\" ? \"system\" : `gui/${uid}`;\n\t\tconst command = (args: string[]) => (mode === \"boot\" ? [\"sudo\", \"launchctl\", ...args] : [\"launchctl\", ...args]);\n\t\treturn {\n\t\t\tpath,\n\t\t\tcontent: `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\\n<plist version=\"1.0\"><dict><key>Label</key><string>${label}</string><key>ProgramArguments</key><array>${[node, cli, \"gateway\", \"run\"].map((v) => `<string>${xml(v)}</string>`).join(\"\")}</array><key>WorkingDirectory</key><string>${xml(home)}</string>${mode === \"boot\" ? `<key>UserName</key><string>${xml(username)}</string>` : \"\"}<key>EnvironmentVariables</key><dict><key>PI_CODING_AGENT_DIR</key><string>${xml(agentDir)}</string><key>HOME</key><string>${xml(home)}</string></dict><key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict><key>StandardOutPath</key><string>${xml(log)}</string><key>StandardErrorPath</key><string>${xml(log)}</string></dict></plist>\\n`,\n\t\t\tinstall: [command([\"bootstrap\", domain, path])],\n\t\t\tremove: [command([\"bootout\", `${domain}/${label}`])],\n\t\t\tverify: command([\"print\", `${domain}/${label}`]),\n\t\t\tstart: command([\"kickstart\", `${domain}/${label}`]),\n\t\t};\n\t}\n\tif (platform === \"win32\") {\n\t\tconst path = join(agentDir, \"gateway-service\", \"startup.ps1\");\n\t\tconst content = `$env:PI_CODING_AGENT_DIR = ${ps(agentDir)}\\nSet-Location -LiteralPath ${ps(home)}\\n& ${ps(node)} ${ps(cli)} gateway run *>> ${ps(log)}\\nexit $LASTEXITCODE\\n`;\n\t\tconst action = `New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ${ps(`-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File \"${path}\"`)}`;\n\t\tconst trigger =\n\t\t\tmode === \"boot\"\n\t\t\t\t? \"New-ScheduledTaskTrigger -AtStartup\"\n\t\t\t\t: `New-ScheduledTaskTrigger -AtLogOn -User ${ps(username)}`;\n\t\tconst register = `$ErrorActionPreference='Stop'; $a=${action}; $t=${trigger}; $s=New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries; ${mode === \"boot\" ? `$c=Get-Credential -UserName ${ps(username)} -Message 'Windows needs your account credentials to run lunR before login'; if(!$c){throw 'Cancelled'}; Register-ScheduledTask -TaskName '${serviceName}' -Action $a -Trigger $t -Settings $s -User $c.UserName -Password $c.GetNetworkCredential().Password -Force | Out-Null` : `Register-ScheduledTask -TaskName '${serviceName}' -Action $a -Trigger $t -Settings $s -User ${ps(username)} -Force | Out-Null`}`;\n\t\tconst command = (script: string) => [\"powershell.exe\", \"-NoProfile\", \"-Command\", script];\n\t\treturn {\n\t\t\tpath,\n\t\t\tcontent,\n\t\t\tinstall: [command(register)],\n\t\t\tremove: [\n\t\t\t\tcommand(\n\t\t\t\t\t`$ErrorActionPreference='Stop'; Unregister-ScheduledTask -TaskName '${serviceName}' -Confirm:$false`,\n\t\t\t\t),\n\t\t\t],\n\t\t\tverify: command(`$ErrorActionPreference='Stop'; Get-ScheduledTask -TaskName '${serviceName}' | Out-Null`),\n\t\t\tstart: command(`$ErrorActionPreference='Stop'; Start-ScheduledTask -TaskName '${serviceName}'`),\n\t\t};\n\t}\n\tthrow new Error(`Automatic startup is not supported on ${platform}. Run lunr gateway start manually.`);\n}\n\nfunction execute(command: string[]): void {\n\tconst [file, ...args] = command;\n\tconst result = spawnSync(file, args, { stdio: \"inherit\", windowsHide: true, timeout: 120_000 });\n\tif (result.error) throw result.error;\n\tif (result.status !== 0) throw new Error(`${file} failed. Check OS permissions and run lunr gateway doctor.`);\n}\n\nexport async function configureStartup(mode: StartupMode): Promise<void> {\n\tconst previous = loadServiceSettings().startup;\n\tif (process.platform === \"linux\" && mode === \"login\") {\n\t\tconst linger = spawnSync(\"loginctl\", [\"show-user\", userInfo().username, \"--property=Linger\", \"--value\"], {\n\t\t\tencoding: \"utf8\",\n\t\t\ttimeout: 10_000,\n\t\t});\n\t\tif (linger.stdout?.trim() === \"yes\")\n\t\t\tthrow new Error(\n\t\t\t\t\"Linux user services already start at boot because lingering is enabled. Choose boot, or disable lingering yourself before choosing login. lunR will not change other user services.\",\n\t\t\t);\n\t}\n\tconst user = userInfo();\n\tconst options = {\n\t\thome: homedir(),\n\t\tagentDir: getAgentDir(),\n\t\tnode: process.execPath,\n\t\tcli: cliPath(),\n\t\tusername: user.username,\n\t\tuid: user.uid,\n\t};\n\tif (previous !== \"off\") {\n\t\tconst old = startupSpec(process.platform, previous, options);\n\t\tfor (const command of old.remove) execute(command);\n\t\tif (process.platform === \"darwin\" && previous === \"boot\") execute([\"sudo\", \"rm\", \"-f\", old.path]);\n\t\telse rmSync(old.path, { force: true });\n\t\tatomicJson(settingsPath(), { startup: \"off\" });\n\t}\n\tif (mode === \"off\") {\n\t\tatomicJson(settingsPath(), { startup: \"off\" });\n\t\treturn;\n\t}\n\tconst spec = startupSpec(process.platform, mode, options);\n\tmkdirSync(serviceDirectory(), { recursive: true, mode: 0o700 });\n\tif (process.platform === \"darwin\" && mode === \"boot\") {\n\t\tconst temporary = join(serviceDirectory(), \"install.plist\");\n\t\twriteFileSync(temporary, spec.content, { mode: 0o600 });\n\t\ttry {\n\t\t\texecute([\"sudo\", \"install\", \"-o\", \"root\", \"-g\", \"wheel\", \"-m\", \"644\", temporary, spec.path]);\n\t\t} finally {\n\t\t\trmSync(temporary, { force: true });\n\t\t}\n\t} else {\n\t\tmkdirSync(dirname(spec.path), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(spec.path, spec.content, { mode: 0o600 });\n\t}\n\tfor (const command of spec.install) execute(command);\n\texecute(spec.verify);\n\tatomicJson(settingsPath(), { startup: mode });\n}\n\nfunction startupVerification(): string {\n\tconst mode = loadServiceSettings().startup;\n\tif (mode === \"off\") return \"\";\n\ttry {\n\t\tconst user = userInfo();\n\t\tconst spec = startupSpec(process.platform, mode, {\n\t\t\thome: homedir(),\n\t\t\tagentDir: getAgentDir(),\n\t\t\tnode: process.execPath,\n\t\t\tcli: cliPath(),\n\t\t\tusername: user.username,\n\t\t\tuid: user.uid,\n\t\t});\n\t\tconst [command, ...args] = spec.verify[0] === \"sudo\" ? spec.verify.slice(1) : spec.verify;\n\t\tconst result = spawnSync(command, args, { encoding: \"utf8\", windowsHide: true, timeout: 5000 });\n\t\treturn result.status === 0 ? \" (native service verified)\" : \" (native service missing or unavailable)\";\n\t} catch {\n\t\treturn \" (native service could not be checked)\";\n\t}\n}\n\nexport function serviceStatusText(): string {\n\tconst status = readGatewayStatus();\n\tconst alive = !!status && processExists(status.pid);\n\tconst fresh = !!status && Date.now() - Date.parse(status.updatedAt) < 5000;\n\treturn [\n\t\t`Gateway: ${alive ? `${status.state}${fresh ? \"\" : \" (not responding)\"}` : \"stopped\"}`,\n\t\t`Automatic startup: ${loadServiceSettings().startup}${startupVerification()}`,\n\t\t...(alive\n\t\t\t? [`PID: ${status.pid}`, ...Object.entries(status.platforms).map(([name, state]) => `${name}: ${state}`)]\n\t\t\t: []),\n\t\t`Log: ${gatewayLogPath()}`,\n\t].join(\"\\n\");\n}\n\nexport function readGatewayLog(): string {\n\ttry {\n\t\treturn readFileSync(gatewayLogPath(), \"utf8\")\n\t\t\t.split(\"\\n\")\n\t\t\t.slice(-100)\n\t\t\t.join(\"\\n\")\n\t\t\t.replace(/bot\\d+:[A-Za-z0-9_-]+/g, \"bot[redacted]\");\n\t} catch {\n\t\treturn \"No gateway log yet.\";\n\t}\n}\n"]}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/gateway/service.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;AACnD,MAAM,WAAW,sBAAsB;IACtC,OAAO,EAAE,WAAW,CAAC;CACrB;AACD,MAAM,WAAW,oBAAoB;IACpC,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,CAAC;IACzC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,eAAO,MAAM,gBAAgB,cAA+C,CAAC;AAC7E,eAAO,MAAM,cAAc,cAAgD,CAAC;AAK5E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAK7D;AAUD,wBAAgB,mBAAmB,IAAI,sBAAsB,CAM5D;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQlD;AAED,wBAAgB,iBAAiB,CAAC,IAAI,SAAe,GAAG,oBAAoB,GAAG,SAAS,CA4BvF;AAED,wBAAgB,gBAAgB,IAAI,OAAO,CAG1C;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG;IACjD,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,EAAE,oBAAoB,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IAC3F,OAAO,EAAE,MAAM,IAAI,CAAC;CACpB,CA8EA;AAUD,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC,CAsD3D;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAS1D;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;CAChB;AAWD,wBAAgB,WAAW,CAC1B,QAAQ,EAAE,MAAM,CAAC,QAAQ,EACzB,IAAI,EAAE,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,EACjC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACnG,WAAW,CA+Db;AASD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiDvE;AAuBD,wBAAgB,iBAAiB,IAAI,MAAM,CAY1C;AAED,wBAAgB,cAAc,IAAI,MAAM,CAUvC","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir, userInfo } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getAgentDir } from \"../config.ts\";\n\nexport type StartupMode = \"off\" | \"login\" | \"boot\";\nexport interface GatewayServiceSettings {\n\tstartup: StartupMode;\n}\nexport interface GatewayRuntimeStatus {\n\tversion: 1;\n\tinstance: string;\n\tpid: number;\n\tstartedAt: string;\n\tupdatedAt: string;\n\tstate: \"starting\" | \"ready\" | \"stopping\";\n\tplatforms: Record<string, string>;\n}\nconst servicePrefix = \"lunr-gateway\";\nexport const serviceDirectory = () => join(getAgentDir(), \"gateway-service\");\nexport const gatewayLogPath = () => join(serviceDirectory(), \"gateway.log\");\nconst statusPath = () => join(serviceDirectory(), \"status.json\");\nconst controlPath = () => join(serviceDirectory(), \"control.json\");\nconst settingsPath = () => join(serviceDirectory(), \"service.json\");\n\nexport function atomicJson(path: string, value: unknown): void {\n\tmkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n\tconst temp = `${path}.${randomUUID()}.tmp`;\n\twriteFileSync(temp, `${JSON.stringify(value, null, 2)}\\n`, { mode: 0o600 });\n\trenameSync(temp, path);\n}\n\nfunction readJson(path: string): unknown {\n\ttry {\n\t\treturn JSON.parse(readFileSync(path, \"utf8\"));\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function loadServiceSettings(): GatewayServiceSettings {\n\tconst raw = readJson(settingsPath());\n\tif (raw && typeof raw === \"object\" && \"startup\" in raw && (raw.startup === \"login\" || raw.startup === \"boot\")) {\n\t\treturn { startup: raw.startup };\n\t}\n\treturn { startup: \"off\" };\n}\n\nexport function processExists(pid: number): boolean {\n\tif (!Number.isSafeInteger(pid) || pid <= 0) return false;\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn !(error && typeof error === \"object\" && \"code\" in error && error.code === \"ESRCH\");\n\t}\n}\n\nexport function readGatewayStatus(file = statusPath()): GatewayRuntimeStatus | undefined {\n\tconst raw = readJson(file);\n\tif (\n\t\t!raw ||\n\t\ttypeof raw !== \"object\" ||\n\t\t!(\"version\" in raw) ||\n\t\traw.version !== 1 ||\n\t\t!(\"pid\" in raw) ||\n\t\ttypeof raw.pid !== \"number\"\n\t)\n\t\treturn undefined;\n\tif (\n\t\t!(\"instance\" in raw) ||\n\t\ttypeof raw.instance !== \"string\" ||\n\t\t!(\"updatedAt\" in raw) ||\n\t\ttypeof raw.updatedAt !== \"string\"\n\t)\n\t\treturn undefined;\n\tif (!(\"state\" in raw) || ![\"starting\", \"ready\", \"stopping\"].includes(String(raw.state))) return undefined;\n\tif (\n\t\t!(\"platforms\" in raw) ||\n\t\t!raw.platforms ||\n\t\ttypeof raw.platforms !== \"object\" ||\n\t\t!(\"startedAt\" in raw) ||\n\t\ttypeof raw.startedAt !== \"string\"\n\t)\n\t\treturn undefined;\n\treturn raw as GatewayRuntimeStatus;\n}\n\nexport function gatewayIsRunning(): boolean {\n\tconst status = readGatewayStatus();\n\treturn !!status && processExists(status.pid);\n}\n\nexport function claimGateway(onStop: () => void): {\n\tupdate: (platforms: Record<string, string>, state?: GatewayRuntimeStatus[\"state\"]) => void;\n\trelease: () => void;\n} {\n\tconst dir = serviceDirectory();\n\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\tconst lock = join(dir, \"lock\");\n\ttry {\n\t\tmkdirSync(lock);\n\t} catch (error) {\n\t\tif (!error || typeof error !== \"object\" || !(\"code\" in error) || error.code !== \"EEXIST\") throw error;\n\t\tconst previous = readGatewayStatus(join(lock, \"owner.json\"));\n\t\tif (!previous || processExists(previous.pid))\n\t\t\tthrow new Error(\"Gateway already running or startup lock is unresolved. Run lunr gateway doctor.\");\n\t\t// Recovery is serialized; a late recoverer must not remove a new owner's lock.\n\t\tconst recovery = join(dir, \"recovery\");\n\t\tmkdirSync(recovery);\n\t\ttry {\n\t\t\tconst current = readGatewayStatus(join(lock, \"owner.json\"));\n\t\t\tif (!current || current.instance !== previous.instance || processExists(current.pid))\n\t\t\t\tthrow new Error(\"Gateway ownership changed. Try again.\");\n\t\t\trmSync(lock, { recursive: true });\n\t\t\tmkdirSync(lock);\n\t\t} finally {\n\t\t\trmSync(recovery, { recursive: true, force: true });\n\t\t}\n\t}\n\tconst status: GatewayRuntimeStatus = {\n\t\tversion: 1,\n\t\tinstance: randomUUID(),\n\t\tpid: process.pid,\n\t\tstartedAt: new Date().toISOString(),\n\t\tupdatedAt: new Date().toISOString(),\n\t\tstate: \"starting\",\n\t\tplatforms: {},\n\t};\n\tatomicJson(join(lock, \"owner.json\"), status);\n\tconst save = () => {\n\t\tstatus.updatedAt = new Date().toISOString();\n\t\tatomicJson(statusPath(), status);\n\t};\n\tsave();\n\tlet stopping = false;\n\tconst timer = setInterval(() => {\n\t\tsave();\n\t\tconst control = readJson(controlPath());\n\t\tif (\n\t\t\t!stopping &&\n\t\t\tcontrol &&\n\t\t\ttypeof control === \"object\" &&\n\t\t\t\"instance\" in control &&\n\t\t\tcontrol.instance === status.instance &&\n\t\t\t\"action\" in control &&\n\t\t\tcontrol.action === \"stop\"\n\t\t) {\n\t\t\tstopping = true;\n\t\t\tstatus.state = \"stopping\";\n\t\t\tonStop();\n\t\t}\n\t}, 500);\n\treturn {\n\t\tupdate(platforms, state = \"ready\") {\n\t\t\tif (stopping && state !== \"stopping\") {\n\t\t\t\tstopping = false;\n\t\t\t\tconst control = readJson(controlPath());\n\t\t\t\tif (control && typeof control === \"object\" && \"instance\" in control && control.instance === status.instance)\n\t\t\t\t\trmSync(controlPath(), { force: true });\n\t\t\t}\n\t\t\tstatus.platforms = { ...platforms };\n\t\t\tstatus.state = state;\n\t\t\tsave();\n\t\t},\n\t\trelease() {\n\t\t\tclearInterval(timer);\n\t\t\tif (readGatewayStatus()?.instance === status.instance) {\n\t\t\t\trmSync(statusPath(), { force: true });\n\t\t\t\trmSync(controlPath(), { force: true });\n\t\t\t\trmSync(lock, { recursive: true, force: true });\n\t\t\t}\n\t\t},\n\t};\n}\n\nfunction cliPath(): string {\n\tconst candidate = process.argv[1];\n\tif (candidate && existsSync(candidate)) return resolve(candidate);\n\treturn fileURLToPath(new URL(\"../cli.js\", import.meta.url));\n}\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\nexport async function startGatewayService(): Promise<string> {\n\tif (gatewayIsRunning()) {\n\t\tif (readGatewayStatus()?.state === \"ready\") return \"Gateway is already running.\";\n\t\tthrow new Error(\n\t\t\t`Gateway process is running but no bot is connected. ${serviceStatusText()}\\nInspect ${gatewayLogPath()}`,\n\t\t);\n\t}\n\tmkdirSync(serviceDirectory(), { recursive: true, mode: 0o700 });\n\tconst startup = loadServiceSettings().startup;\n\tif (startup !== \"off\") {\n\t\tconst user = userInfo();\n\t\tconst spec = startupSpec(process.platform, startup, {\n\t\t\thome: homedir(),\n\t\t\tagentDir: getAgentDir(),\n\t\t\tnode: process.execPath,\n\t\t\tcli: cliPath(),\n\t\t\tusername: user.username,\n\t\t\tuid: user.uid,\n\t\t});\n\t\texecute(spec.start);\n\t\tfor (let i = 0; i < 100; i++) {\n\t\t\tif (readGatewayStatus()?.state === \"ready\" && gatewayIsRunning()) return \"Gateway started.\";\n\t\t\tawait sleep(200);\n\t\t}\n\t\tthrow new Error(\n\t\t\t`Startup service did not connect within 20 seconds. ${serviceStatusText()}\\nRun lunr gateway doctor and inspect ${gatewayLogPath()}`,\n\t\t);\n\t}\n\tconst { openSync, closeSync } = await import(\"node:fs\");\n\tconst log = openSync(gatewayLogPath(), \"a\", 0o600);\n\tconst child = spawn(process.execPath, [cliPath(), \"gateway\", \"run\"], {\n\t\tcwd: homedir(),\n\t\tdetached: true,\n\t\twindowsHide: true,\n\t\tstdio: [\"ignore\", log, log],\n\t\tenv: { ...process.env, PI_CODING_AGENT_DIR: getAgentDir() },\n\t});\n\tcloseSync(log);\n\tlet spawnError: Error | undefined;\n\tchild.on(\"error\", (error) => {\n\t\tspawnError = error;\n\t});\n\tchild.unref();\n\tfor (let i = 0; i < 100; i++) {\n\t\tif (spawnError) throw spawnError;\n\t\tconst status = readGatewayStatus();\n\t\tif (status && status.pid === child.pid && status.state === \"ready\") return \"Gateway started.\";\n\t\tif (child.exitCode !== null)\n\t\t\tthrow new Error(`Gateway exited with code ${child.exitCode}. See ${gatewayLogPath()}`);\n\t\tawait sleep(200);\n\t}\n\tthrow new Error(\n\t\t`Gateway did not connect within 20 seconds. ${serviceStatusText()}\\nCheck lunr gateway status and ${gatewayLogPath()}`,\n\t);\n}\n\nexport async function stopGatewayService(): Promise<string> {\n\tconst status = readGatewayStatus();\n\tif (!status || !processExists(status.pid)) return \"Gateway is not running.\";\n\tatomicJson(controlPath(), { instance: status.instance, action: \"stop\" });\n\tfor (let i = 0; i < 150; i++) {\n\t\tif (readGatewayStatus()?.instance !== status.instance || !processExists(status.pid)) return \"Gateway stopped.\";\n\t\tawait sleep(200);\n\t}\n\tthrow new Error(\"Gateway is still stopping after 30 seconds. Inspect the log; no unrelated process was terminated.\");\n}\n\nexport interface StartupSpec {\n\tpath: string;\n\tcontent: string;\n\tinstall: string[][];\n\tremove: string[][];\n\tverify: string[];\n\tstart: string[];\n}\nfunction xml(value: string): string {\n\treturn value.replaceAll(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\");\n}\nfunction systemdArg(value: string): string {\n\treturn `\"${value.replaceAll(\"%\", \"%%\").replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll('\"', '\\\\\"')}\"`;\n}\nfunction ps(value: string): string {\n\treturn `'${value.replaceAll(\"'\", \"''\")}'`;\n}\n\nexport function startupSpec(\n\tplatform: NodeJS.Platform,\n\tmode: Exclude<StartupMode, \"off\">,\n\toptions: { home: string; agentDir: string; node: string; cli: string; username: string; uid: number },\n): StartupSpec {\n\tconst { home, agentDir, node, cli, username, uid } = options;\n\tif ([home, agentDir, node, cli, username].some((value) => /[\\r\\n\\0]/.test(value)))\n\t\tthrow new Error(\"Startup paths and account names must not contain control characters.\");\n\tconst suffix = createHash(\"sha256\").update(resolve(agentDir)).digest(\"hex\").slice(0, 10);\n\tconst serviceName = `${servicePrefix}-${suffix}`;\n\tconst log = join(agentDir, \"gateway-service\", \"gateway.log\");\n\tif (platform === \"linux\") {\n\t\tconst path = join(home, \".config\", \"systemd\", \"user\", `${serviceName}.service`);\n\t\treturn {\n\t\t\tpath,\n\t\t\tcontent: `[Unit]\\nDescription=lunR chat gateway\\nAfter=network-online.target\\n\\n[Service]\\nType=simple\\nExecStart=${systemdArg(node)} ${systemdArg(cli)} gateway run\\nWorkingDirectory=${systemdArg(home)}\\nEnvironment=${systemdArg(`PI_CODING_AGENT_DIR=${agentDir}`)}\\nRestart=on-failure\\nRestartSec=5\\nTimeoutStopSec=30\\nStandardOutput=append:${log.replaceAll(\"%\", \"%%\")}\\nStandardError=append:${log.replaceAll(\"%\", \"%%\")}\\nUMask=0077\\n\\n[Install]\\nWantedBy=default.target\\n`,\n\t\t\tinstall: [\n\t\t\t\t...(mode === \"boot\" ? [[\"loginctl\", \"enable-linger\", username]] : []),\n\t\t\t\t[\"systemctl\", \"--user\", \"daemon-reload\"],\n\t\t\t\t[\"systemctl\", \"--user\", \"enable\", `${serviceName}.service`],\n\t\t\t],\n\t\t\tremove: [[\"systemctl\", \"--user\", \"disable\", `${serviceName}.service`]],\n\t\t\tverify: [\"systemctl\", \"--user\", \"is-enabled\", `${serviceName}.service`],\n\t\t\tstart: [\"systemctl\", \"--user\", \"start\", `${serviceName}.service`],\n\t\t};\n\t}\n\tif (platform === \"darwin\") {\n\t\tconst label = `dev.lunr.gateway.${suffix}`;\n\t\tconst path =\n\t\t\tmode === \"boot\"\n\t\t\t\t? `/Library/LaunchDaemons/${label}.plist`\n\t\t\t\t: join(home, \"Library\", \"LaunchAgents\", `${label}.plist`);\n\t\tconst domain = mode === \"boot\" ? \"system\" : `gui/${uid}`;\n\t\tconst command = (args: string[]) => (mode === \"boot\" ? [\"sudo\", \"launchctl\", ...args] : [\"launchctl\", ...args]);\n\t\treturn {\n\t\t\tpath,\n\t\t\tcontent: `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\\n<plist version=\"1.0\"><dict><key>Label</key><string>${label}</string><key>ProgramArguments</key><array>${[node, cli, \"gateway\", \"run\"].map((v) => `<string>${xml(v)}</string>`).join(\"\")}</array><key>WorkingDirectory</key><string>${xml(home)}</string>${mode === \"boot\" ? `<key>UserName</key><string>${xml(username)}</string>` : \"\"}<key>EnvironmentVariables</key><dict><key>PI_CODING_AGENT_DIR</key><string>${xml(agentDir)}</string><key>HOME</key><string>${xml(home)}</string></dict><key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict><key>StandardOutPath</key><string>${xml(log)}</string><key>StandardErrorPath</key><string>${xml(log)}</string></dict></plist>\\n`,\n\t\t\tinstall: [command([\"bootstrap\", domain, path])],\n\t\t\tremove: [command([\"bootout\", `${domain}/${label}`])],\n\t\t\tverify: command([\"print\", `${domain}/${label}`]),\n\t\t\tstart: command([\"kickstart\", `${domain}/${label}`]),\n\t\t};\n\t}\n\tif (platform === \"win32\") {\n\t\tconst path = join(agentDir, \"gateway-service\", \"startup.ps1\");\n\t\tconst content = `$env:PI_CODING_AGENT_DIR = ${ps(agentDir)}\\nSet-Location -LiteralPath ${ps(home)}\\n& ${ps(node)} ${ps(cli)} gateway run *>> ${ps(log)}\\nexit $LASTEXITCODE\\n`;\n\t\tconst action = `New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ${ps(`-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File \"${path}\"`)}`;\n\t\tconst trigger =\n\t\t\tmode === \"boot\"\n\t\t\t\t? \"New-ScheduledTaskTrigger -AtStartup\"\n\t\t\t\t: `New-ScheduledTaskTrigger -AtLogOn -User ${ps(username)}`;\n\t\tconst register = `$ErrorActionPreference='Stop'; $a=${action}; $t=${trigger}; $s=New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries; ${mode === \"boot\" ? `$c=Get-Credential -UserName ${ps(username)} -Message 'Windows needs your account credentials to run lunR before login'; if(!$c){throw 'Cancelled'}; Register-ScheduledTask -TaskName '${serviceName}' -Action $a -Trigger $t -Settings $s -User $c.UserName -Password $c.GetNetworkCredential().Password -Force | Out-Null` : `Register-ScheduledTask -TaskName '${serviceName}' -Action $a -Trigger $t -Settings $s -User ${ps(username)} -Force | Out-Null`}`;\n\t\tconst command = (script: string) => [\"powershell.exe\", \"-NoProfile\", \"-Command\", script];\n\t\treturn {\n\t\t\tpath,\n\t\t\tcontent,\n\t\t\tinstall: [command(register)],\n\t\t\tremove: [\n\t\t\t\tcommand(\n\t\t\t\t\t`$ErrorActionPreference='Stop'; Unregister-ScheduledTask -TaskName '${serviceName}' -Confirm:$false`,\n\t\t\t\t),\n\t\t\t],\n\t\t\tverify: command(`$ErrorActionPreference='Stop'; Get-ScheduledTask -TaskName '${serviceName}' | Out-Null`),\n\t\t\tstart: command(`$ErrorActionPreference='Stop'; Start-ScheduledTask -TaskName '${serviceName}'`),\n\t\t};\n\t}\n\tthrow new Error(`Automatic startup is not supported on ${platform}. Run lunr gateway start manually.`);\n}\n\nfunction execute(command: string[]): void {\n\tconst [file, ...args] = command;\n\tconst result = spawnSync(file, args, { stdio: \"inherit\", windowsHide: true, timeout: 120_000 });\n\tif (result.error) throw result.error;\n\tif (result.status !== 0) throw new Error(`${file} failed. Check OS permissions and run lunr gateway doctor.`);\n}\n\nexport async function configureStartup(mode: StartupMode): Promise<void> {\n\tconst previous = loadServiceSettings().startup;\n\tif (process.platform === \"linux\" && mode === \"login\") {\n\t\tconst linger = spawnSync(\"loginctl\", [\"show-user\", userInfo().username, \"--property=Linger\", \"--value\"], {\n\t\t\tencoding: \"utf8\",\n\t\t\ttimeout: 10_000,\n\t\t});\n\t\tif (linger.stdout?.trim() === \"yes\")\n\t\t\tthrow new Error(\n\t\t\t\t\"Linux user services already start at boot because lingering is enabled. Choose boot, or disable lingering yourself before choosing login. lunR will not change other user services.\",\n\t\t\t);\n\t}\n\tconst user = userInfo();\n\tconst options = {\n\t\thome: homedir(),\n\t\tagentDir: getAgentDir(),\n\t\tnode: process.execPath,\n\t\tcli: cliPath(),\n\t\tusername: user.username,\n\t\tuid: user.uid,\n\t};\n\tif (previous !== \"off\") {\n\t\tconst old = startupSpec(process.platform, previous, options);\n\t\tfor (const command of old.remove) execute(command);\n\t\tif (process.platform === \"darwin\" && previous === \"boot\") execute([\"sudo\", \"rm\", \"-f\", old.path]);\n\t\telse rmSync(old.path, { force: true });\n\t\tatomicJson(settingsPath(), { startup: \"off\" });\n\t}\n\tif (mode === \"off\") {\n\t\tatomicJson(settingsPath(), { startup: \"off\" });\n\t\treturn;\n\t}\n\tconst spec = startupSpec(process.platform, mode, options);\n\tmkdirSync(serviceDirectory(), { recursive: true, mode: 0o700 });\n\tif (process.platform === \"darwin\" && mode === \"boot\") {\n\t\tconst temporary = join(serviceDirectory(), \"install.plist\");\n\t\twriteFileSync(temporary, spec.content, { mode: 0o600 });\n\t\ttry {\n\t\t\texecute([\"sudo\", \"install\", \"-o\", \"root\", \"-g\", \"wheel\", \"-m\", \"644\", temporary, spec.path]);\n\t\t} finally {\n\t\t\trmSync(temporary, { force: true });\n\t\t}\n\t} else {\n\t\tmkdirSync(dirname(spec.path), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(spec.path, spec.content, { mode: 0o600 });\n\t}\n\tfor (const command of spec.install) execute(command);\n\texecute(spec.verify);\n\tatomicJson(settingsPath(), { startup: mode });\n}\n\nfunction startupVerification(): string {\n\tconst mode = loadServiceSettings().startup;\n\tif (mode === \"off\") return \"\";\n\ttry {\n\t\tconst user = userInfo();\n\t\tconst spec = startupSpec(process.platform, mode, {\n\t\t\thome: homedir(),\n\t\t\tagentDir: getAgentDir(),\n\t\t\tnode: process.execPath,\n\t\t\tcli: cliPath(),\n\t\t\tusername: user.username,\n\t\t\tuid: user.uid,\n\t\t});\n\t\tconst [command, ...args] = spec.verify[0] === \"sudo\" ? spec.verify.slice(1) : spec.verify;\n\t\tconst result = spawnSync(command, args, { encoding: \"utf8\", windowsHide: true, timeout: 5000 });\n\t\treturn result.status === 0 ? \" (native service verified)\" : \" (native service missing or unavailable)\";\n\t} catch {\n\t\treturn \" (native service could not be checked)\";\n\t}\n}\n\nexport function serviceStatusText(): string {\n\tconst status = readGatewayStatus();\n\tconst alive = !!status && processExists(status.pid);\n\tconst fresh = !!status && Date.now() - Date.parse(status.updatedAt) < 5000;\n\treturn [\n\t\t`Gateway: ${alive ? `${status.state}${fresh ? \"\" : \" (not responding)\"}` : \"stopped\"}`,\n\t\t`Automatic startup: ${loadServiceSettings().startup}${startupVerification()}`,\n\t\t...(alive\n\t\t\t? [`PID: ${status.pid}`, ...Object.entries(status.platforms).map(([name, state]) => `${name}: ${state}`)]\n\t\t\t: []),\n\t\t`Log: ${gatewayLogPath()}`,\n\t].join(\"\\n\");\n}\n\nexport function readGatewayLog(): string {\n\ttry {\n\t\treturn readFileSync(gatewayLogPath(), \"utf8\")\n\t\t\t.split(\"\\n\")\n\t\t\t.slice(-100)\n\t\t\t.join(\"\\n\")\n\t\t\t.replace(/bot\\d+:[A-Za-z0-9_-]+/g, \"bot[redacted]\");\n\t} catch {\n\t\treturn \"No gateway log yet.\";\n\t}\n}\n"]}
@@ -131,6 +131,12 @@ export function claimGateway(onStop) {
131
131
  }, 500);
132
132
  return {
133
133
  update(platforms, state = "ready") {
134
+ if (stopping && state !== "stopping") {
135
+ stopping = false;
136
+ const control = readJson(controlPath());
137
+ if (control && typeof control === "object" && "instance" in control && control.instance === status.instance)
138
+ rmSync(controlPath(), { force: true });
139
+ }
134
140
  status.platforms = { ...platforms };
135
141
  status.state = state;
136
142
  save();
@@ -153,8 +159,11 @@ function cliPath() {
153
159
  }
154
160
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
155
161
  export async function startGatewayService() {
156
- if (gatewayIsRunning())
157
- return "Gateway is already running.";
162
+ if (gatewayIsRunning()) {
163
+ if (readGatewayStatus()?.state === "ready")
164
+ return "Gateway is already running.";
165
+ throw new Error(`Gateway process is running but no bot is connected. ${serviceStatusText()}\nInspect ${gatewayLogPath()}`);
166
+ }
158
167
  mkdirSync(serviceDirectory(), { recursive: true, mode: 0o700 });
159
168
  const startup = loadServiceSettings().startup;
160
169
  if (startup !== "off") {
@@ -173,7 +182,7 @@ export async function startGatewayService() {
173
182
  return "Gateway started.";
174
183
  await sleep(200);
175
184
  }
176
- throw new Error(`Startup service did not become ready. Run lunr gateway doctor and inspect ${gatewayLogPath()}`);
185
+ throw new Error(`Startup service did not connect within 20 seconds. ${serviceStatusText()}\nRun lunr gateway doctor and inspect ${gatewayLogPath()}`);
177
186
  }
178
187
  const { openSync, closeSync } = await import("node:fs");
179
188
  const log = openSync(gatewayLogPath(), "a", 0o600);
@@ -200,7 +209,7 @@ export async function startGatewayService() {
200
209
  throw new Error(`Gateway exited with code ${child.exitCode}. See ${gatewayLogPath()}`);
201
210
  await sleep(200);
202
211
  }
203
- throw new Error(`Gateway did not become ready within 20 seconds. Check lunr gateway status and ${gatewayLogPath()}`);
212
+ throw new Error(`Gateway did not connect within 20 seconds. ${serviceStatusText()}\nCheck lunr gateway status and ${gatewayLogPath()}`);
204
213
  }
205
214
  export async function stopGatewayService() {
206
215
  const status = readGatewayStatus();