@bd7pil/ocrc 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/LICENSE +34 -0
  2. package/README.md +330 -0
  3. package/dist/cli/host-backends.js +216 -0
  4. package/dist/cli/host-backends.js.map +1 -0
  5. package/dist/cli/host.js +185 -0
  6. package/dist/cli/host.js.map +1 -0
  7. package/dist/cli/index.js +67 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli/init.js +68 -0
  10. package/dist/cli/init.js.map +1 -0
  11. package/dist/cli/install.js +220 -0
  12. package/dist/cli/install.js.map +1 -0
  13. package/dist/cli/pair.js +11 -0
  14. package/dist/cli/pair.js.map +1 -0
  15. package/dist/cli/uninstall.js +75 -0
  16. package/dist/cli/uninstall.js.map +1 -0
  17. package/dist/connectivity/auth/cf-access.js +20 -0
  18. package/dist/connectivity/auth/cf-access.js.map +1 -0
  19. package/dist/connectivity/auth/index.js +2 -0
  20. package/dist/connectivity/auth/index.js.map +1 -0
  21. package/dist/connectivity/auth/select.js +19 -0
  22. package/dist/connectivity/auth/select.js.map +1 -0
  23. package/dist/connectivity/auth/token.js +157 -0
  24. package/dist/connectivity/auth/token.js.map +1 -0
  25. package/dist/connectivity/exposure/cloudflared.js +77 -0
  26. package/dist/connectivity/exposure/cloudflared.js.map +1 -0
  27. package/dist/connectivity/exposure/index.js +2 -0
  28. package/dist/connectivity/exposure/index.js.map +1 -0
  29. package/dist/connectivity/exposure/providers.js +49 -0
  30. package/dist/connectivity/exposure/providers.js.map +1 -0
  31. package/dist/connectivity/pairing.js +29 -0
  32. package/dist/connectivity/pairing.js.map +1 -0
  33. package/dist/core/agent/acp-backend.js +539 -0
  34. package/dist/core/agent/acp-backend.js.map +1 -0
  35. package/dist/core/agent/acp-connect.js +92 -0
  36. package/dist/core/agent/acp-connect.js.map +1 -0
  37. package/dist/core/agent/acp-normalizer.js +81 -0
  38. package/dist/core/agent/acp-normalizer.js.map +1 -0
  39. package/dist/core/agent/acp-replay.js +89 -0
  40. package/dist/core/agent/acp-replay.js.map +1 -0
  41. package/dist/core/agent/acp-store.js +129 -0
  42. package/dist/core/agent/acp-store.js.map +1 -0
  43. package/dist/core/agent/backend.js +2 -0
  44. package/dist/core/agent/backend.js.map +1 -0
  45. package/dist/core/agent/diff-util.js +70 -0
  46. package/dist/core/agent/diff-util.js.map +1 -0
  47. package/dist/core/agent/event.js +12 -0
  48. package/dist/core/agent/event.js.map +1 -0
  49. package/dist/core/agent/opencode-backend.js +598 -0
  50. package/dist/core/agent/opencode-backend.js.map +1 -0
  51. package/dist/core/agent/opencode-normalizer.js +69 -0
  52. package/dist/core/agent/opencode-normalizer.js.map +1 -0
  53. package/dist/core/agent/registry.js +55 -0
  54. package/dist/core/agent/registry.js.map +1 -0
  55. package/dist/core/agent/v2-backend.js +176 -0
  56. package/dist/core/agent/v2-backend.js.map +1 -0
  57. package/dist/core/card-bus.js +74 -0
  58. package/dist/core/card-bus.js.map +1 -0
  59. package/dist/core/channels.js +95 -0
  60. package/dist/core/channels.js.map +1 -0
  61. package/dist/core/history.js +91 -0
  62. package/dist/core/history.js.map +1 -0
  63. package/dist/core/list-files.js +50 -0
  64. package/dist/core/list-files.js.map +1 -0
  65. package/dist/core/opencode-events.js +29 -0
  66. package/dist/core/opencode-events.js.map +1 -0
  67. package/dist/core/primary-election.js +91 -0
  68. package/dist/core/primary-election.js.map +1 -0
  69. package/dist/core/push.js +152 -0
  70. package/dist/core/push.js.map +1 -0
  71. package/dist/core/relay.js +400 -0
  72. package/dist/core/relay.js.map +1 -0
  73. package/dist/core/scheduler.js +150 -0
  74. package/dist/core/scheduler.js.map +1 -0
  75. package/dist/core/state.js +222 -0
  76. package/dist/core/state.js.map +1 -0
  77. package/dist/core/stream-accumulator.js +76 -0
  78. package/dist/core/stream-accumulator.js.map +1 -0
  79. package/dist/core/structured-card.js +2 -0
  80. package/dist/core/structured-card.js.map +1 -0
  81. package/dist/core/types.js +2 -0
  82. package/dist/core/types.js.map +1 -0
  83. package/dist/index.js +3 -0
  84. package/dist/index.js.map +1 -0
  85. package/dist/opencode/global-events.js +57 -0
  86. package/dist/opencode/global-events.js.map +1 -0
  87. package/dist/opencode/list-sessions.js +122 -0
  88. package/dist/opencode/list-sessions.js.map +1 -0
  89. package/dist/opencode/submit.js +39 -0
  90. package/dist/opencode/submit.js.map +1 -0
  91. package/dist/opencode/workspaces.js +51 -0
  92. package/dist/opencode/workspaces.js.map +1 -0
  93. package/dist/plugin/config.js +128 -0
  94. package/dist/plugin/config.js.map +1 -0
  95. package/dist/plugin/control-plane.js +133 -0
  96. package/dist/plugin/control-plane.js.map +1 -0
  97. package/dist/plugin/entry.js +409 -0
  98. package/dist/plugin/entry.js.map +1 -0
  99. package/dist/plugin/v2/event-map.js +193 -0
  100. package/dist/plugin/v2/event-map.js.map +1 -0
  101. package/dist/plugin/v2/types.js +15 -0
  102. package/dist/plugin/v2/types.js.map +1 -0
  103. package/dist/transport/interface.js +2 -0
  104. package/dist/transport/interface.js.map +1 -0
  105. package/dist/transport/telegram/esc.js +9 -0
  106. package/dist/transport/telegram/esc.js.map +1 -0
  107. package/dist/transport/telegram/handlers/info-commands.js +101 -0
  108. package/dist/transport/telegram/handlers/info-commands.js.map +1 -0
  109. package/dist/transport/telegram/handlers.js +1348 -0
  110. package/dist/transport/telegram/handlers.js.map +1 -0
  111. package/dist/transport/telegram/i18n/en.js +538 -0
  112. package/dist/transport/telegram/i18n/en.js.map +1 -0
  113. package/dist/transport/telegram/i18n/index.js +83 -0
  114. package/dist/transport/telegram/i18n/index.js.map +1 -0
  115. package/dist/transport/telegram/i18n/zh.js +538 -0
  116. package/dist/transport/telegram/i18n/zh.js.map +1 -0
  117. package/dist/transport/telegram/index.js +792 -0
  118. package/dist/transport/telegram/index.js.map +1 -0
  119. package/dist/transport/telegram/main-keyboard.js +36 -0
  120. package/dist/transport/telegram/main-keyboard.js.map +1 -0
  121. package/dist/transport/telegram/managers/abort-suppression-manager.js +33 -0
  122. package/dist/transport/telegram/managers/abort-suppression-manager.js.map +1 -0
  123. package/dist/transport/telegram/managers/external-input-suppression-manager.js +51 -0
  124. package/dist/transport/telegram/managers/external-input-suppression-manager.js.map +1 -0
  125. package/dist/transport/telegram/managers/interaction-manager.js +254 -0
  126. package/dist/transport/telegram/managers/interaction-manager.js.map +1 -0
  127. package/dist/transport/telegram/managers/permission-manager.js +242 -0
  128. package/dist/transport/telegram/managers/permission-manager.js.map +1 -0
  129. package/dist/transport/telegram/managers/question-manager.js +191 -0
  130. package/dist/transport/telegram/managers/question-manager.js.map +1 -0
  131. package/dist/transport/telegram/managers/rename-manager.js +56 -0
  132. package/dist/transport/telegram/managers/rename-manager.js.map +1 -0
  133. package/dist/transport/telegram/menus.js +77 -0
  134. package/dist/transport/telegram/menus.js.map +1 -0
  135. package/dist/transport/telegram/permission-flow.js +156 -0
  136. package/dist/transport/telegram/permission-flow.js.map +1 -0
  137. package/dist/transport/telegram/question-flow.js +224 -0
  138. package/dist/transport/telegram/question-flow.js.map +1 -0
  139. package/dist/transport/telegram/render/block-parser.js +365 -0
  140. package/dist/transport/telegram/render/block-parser.js.map +1 -0
  141. package/dist/transport/telegram/render/block-plain-text.js +87 -0
  142. package/dist/transport/telegram/render/block-plain-text.js.map +1 -0
  143. package/dist/transport/telegram/render/block-splitter.js +152 -0
  144. package/dist/transport/telegram/render/block-splitter.js.map +1 -0
  145. package/dist/transport/telegram/render/chunker.js +71 -0
  146. package/dist/transport/telegram/render/chunker.js.map +1 -0
  147. package/dist/transport/telegram/render/limits.js +13 -0
  148. package/dist/transport/telegram/render/limits.js.map +1 -0
  149. package/dist/transport/telegram/render/markdown-normalizer.js +102 -0
  150. package/dist/transport/telegram/render/markdown-normalizer.js.map +1 -0
  151. package/dist/transport/telegram/render/markdown-to-telegram-v2.js +116 -0
  152. package/dist/transport/telegram/render/markdown-to-telegram-v2.js.map +1 -0
  153. package/dist/transport/telegram/render/part-signature.js +14 -0
  154. package/dist/transport/telegram/render/part-signature.js.map +1 -0
  155. package/dist/transport/telegram/render/pipeline.js +23 -0
  156. package/dist/transport/telegram/render/pipeline.js.map +1 -0
  157. package/dist/transport/telegram/render/rich-blocks.js +311 -0
  158. package/dist/transport/telegram/render/rich-blocks.js.map +1 -0
  159. package/dist/transport/telegram/render/text-splitter.js +64 -0
  160. package/dist/transport/telegram/render/text-splitter.js.map +1 -0
  161. package/dist/transport/telegram/render/types.js +2 -0
  162. package/dist/transport/telegram/render/types.js.map +1 -0
  163. package/dist/transport/telegram/renderer.js +305 -0
  164. package/dist/transport/telegram/renderer.js.map +1 -0
  165. package/dist/transport/telegram/streaming/duration-formatter.js +68 -0
  166. package/dist/transport/telegram/streaming/duration-formatter.js.map +1 -0
  167. package/dist/transport/telegram/streaming/finalize-assistant-response.js +37 -0
  168. package/dist/transport/telegram/streaming/finalize-assistant-response.js.map +1 -0
  169. package/dist/transport/telegram/streaming/response-streamer.js +400 -0
  170. package/dist/transport/telegram/streaming/response-streamer.js.map +1 -0
  171. package/dist/transport/telegram/streaming/running-tool-tracker.js +150 -0
  172. package/dist/transport/telegram/streaming/running-tool-tracker.js.map +1 -0
  173. package/dist/transport/telegram/streaming/stream-throttle.js +57 -0
  174. package/dist/transport/telegram/streaming/stream-throttle.js.map +1 -0
  175. package/dist/transport/telegram/streaming/tool-call-streamer.js +390 -0
  176. package/dist/transport/telegram/streaming/tool-call-streamer.js.map +1 -0
  177. package/dist/transport/telegram/streaming-render.js +182 -0
  178. package/dist/transport/telegram/streaming-render.js.map +1 -0
  179. package/dist/transport/telegram/types/interaction.js +2 -0
  180. package/dist/transport/telegram/types/interaction.js.map +1 -0
  181. package/dist/transport/telegram/types/model.js +25 -0
  182. package/dist/transport/telegram/types/model.js.map +1 -0
  183. package/dist/transport/telegram/types/permission.js +2 -0
  184. package/dist/transport/telegram/types/permission.js.map +1 -0
  185. package/dist/transport/telegram/types/question.js +2 -0
  186. package/dist/transport/telegram/types/question.js.map +1 -0
  187. package/dist/transport/telegram/types/rename.js +2 -0
  188. package/dist/transport/telegram/types/rename.js.map +1 -0
  189. package/dist/transport/telegram/types/scheduled-task.js +23 -0
  190. package/dist/transport/telegram/types/scheduled-task.js.map +1 -0
  191. package/dist/transport/telegram/types/session.js +2 -0
  192. package/dist/transport/telegram/types/session.js.map +1 -0
  193. package/dist/transport/telegram/ui.js +28 -0
  194. package/dist/transport/telegram/ui.js.map +1 -0
  195. package/dist/transport/web/index.js +148 -0
  196. package/dist/transport/web/index.js.map +1 -0
  197. package/dist/transport/web/middleware/cf-access.js +104 -0
  198. package/dist/transport/web/middleware/cf-access.js.map +1 -0
  199. package/dist/transport/web/opencode-config.js +15 -0
  200. package/dist/transport/web/opencode-config.js.map +1 -0
  201. package/dist/transport/web/routes/abort.js +12 -0
  202. package/dist/transport/web/routes/abort.js.map +1 -0
  203. package/dist/transport/web/routes/approval.js +16 -0
  204. package/dist/transport/web/routes/approval.js.map +1 -0
  205. package/dist/transport/web/routes/catalog.js +11 -0
  206. package/dist/transport/web/routes/catalog.js.map +1 -0
  207. package/dist/transport/web/routes/channels.js +53 -0
  208. package/dist/transport/web/routes/channels.js.map +1 -0
  209. package/dist/transport/web/routes/commands.js +15 -0
  210. package/dist/transport/web/routes/commands.js.map +1 -0
  211. package/dist/transport/web/routes/context.js +17 -0
  212. package/dist/transport/web/routes/context.js.map +1 -0
  213. package/dist/transport/web/routes/controls.js +34 -0
  214. package/dist/transport/web/routes/controls.js.map +1 -0
  215. package/dist/transport/web/routes/create-session.js +21 -0
  216. package/dist/transport/web/routes/create-session.js.map +1 -0
  217. package/dist/transport/web/routes/diff.js +7 -0
  218. package/dist/transport/web/routes/diff.js.map +1 -0
  219. package/dist/transport/web/routes/files.js +22 -0
  220. package/dist/transport/web/routes/files.js.map +1 -0
  221. package/dist/transport/web/routes/logs.js +9 -0
  222. package/dist/transport/web/routes/logs.js.map +1 -0
  223. package/dist/transport/web/routes/m8.js +69 -0
  224. package/dist/transport/web/routes/m8.js.map +1 -0
  225. package/dist/transport/web/routes/mcp.js +7 -0
  226. package/dist/transport/web/routes/mcp.js.map +1 -0
  227. package/dist/transport/web/routes/message.js +32 -0
  228. package/dist/transport/web/routes/message.js.map +1 -0
  229. package/dist/transport/web/routes/overrides.js +20 -0
  230. package/dist/transport/web/routes/overrides.js.map +1 -0
  231. package/dist/transport/web/routes/question.js +32 -0
  232. package/dist/transport/web/routes/question.js.map +1 -0
  233. package/dist/transport/web/routes/rename.js +12 -0
  234. package/dist/transport/web/routes/rename.js.map +1 -0
  235. package/dist/transport/web/routes/schedules.js +28 -0
  236. package/dist/transport/web/routes/schedules.js.map +1 -0
  237. package/dist/transport/web/routes/session.js +20 -0
  238. package/dist/transport/web/routes/session.js.map +1 -0
  239. package/dist/transport/web/routes/sessions.js +25 -0
  240. package/dist/transport/web/routes/sessions.js.map +1 -0
  241. package/dist/transport/web/routes/subagents.js +11 -0
  242. package/dist/transport/web/routes/subagents.js.map +1 -0
  243. package/dist/transport/web/routes/suggestions.js +7 -0
  244. package/dist/transport/web/routes/suggestions.js.map +1 -0
  245. package/dist/transport/web/routes/todo.js +7 -0
  246. package/dist/transport/web/routes/todo.js.map +1 -0
  247. package/dist/transport/web/routes/version.js +5 -0
  248. package/dist/transport/web/routes/version.js.map +1 -0
  249. package/dist/transport/web/routes/workspaces.js +7 -0
  250. package/dist/transport/web/routes/workspaces.js.map +1 -0
  251. package/dist/transport/web/server.js +111 -0
  252. package/dist/transport/web/server.js.map +1 -0
  253. package/dist/transport/web/session-summary.js +46 -0
  254. package/dist/transport/web/session-summary.js.map +1 -0
  255. package/dist/transport/web/ws-hub.js +83 -0
  256. package/dist/transport/web/ws-hub.js.map +1 -0
  257. package/dist/utils/logger.js +105 -0
  258. package/dist/utils/logger.js.map +1 -0
  259. package/dist/utils/markdown.js +129 -0
  260. package/dist/utils/markdown.js.map +1 -0
  261. package/dist/utils/paths.js +18 -0
  262. package/dist/utils/paths.js.map +1 -0
  263. package/dist/utils/version.js +44 -0
  264. package/dist/utils/version.js.map +1 -0
  265. package/package.json +91 -0
  266. package/web/dist/_app/env.js +1 -0
  267. package/web/dist/_app/immutable/assets/0.DxN3RsuG.css +1 -0
  268. package/web/dist/_app/immutable/assets/2.BNuGfmZP.css +1 -0
  269. package/web/dist/_app/immutable/assets/3.CTDprntu.css +1 -0
  270. package/web/dist/_app/immutable/chunks/BUpwHpMJ.js +1 -0
  271. package/web/dist/_app/immutable/chunks/BjY4dxcB.js +2 -0
  272. package/web/dist/_app/immutable/chunks/C2FuqD3s.js +1 -0
  273. package/web/dist/_app/immutable/chunks/CUHGq08X.js +2 -0
  274. package/web/dist/_app/immutable/chunks/CWj6FrbW.js +1 -0
  275. package/web/dist/_app/immutable/chunks/ClvoqtGj.js +1 -0
  276. package/web/dist/_app/immutable/chunks/DIEDQ_hk.js +1 -0
  277. package/web/dist/_app/immutable/entry/app.BoRpcVMj.js +2 -0
  278. package/web/dist/_app/immutable/entry/start.Cj-PQNTq.js +1 -0
  279. package/web/dist/_app/immutable/nodes/0.y9YEms5p.js +3 -0
  280. package/web/dist/_app/immutable/nodes/1.CPhhiRM2.js +1 -0
  281. package/web/dist/_app/immutable/nodes/2.B_c84Mc9.js +1 -0
  282. package/web/dist/_app/immutable/nodes/3.ClTd0J3D.js +64 -0
  283. package/web/dist/_app/version.json +1 -0
  284. package/web/dist/apple-touch-icon.png +0 -0
  285. package/web/dist/favicon.png +0 -0
  286. package/web/dist/favicon.svg +17 -0
  287. package/web/dist/icon-192.png +0 -0
  288. package/web/dist/icon-512-maskable.png +0 -0
  289. package/web/dist/icon-512.png +0 -0
  290. package/web/dist/index.html +73 -0
  291. package/web/dist/manifest.webmanifest +28 -0
  292. package/web/dist/service-worker.js +1 -0
package/LICENSE ADDED
@@ -0,0 +1,34 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agentjoey (opencode-remote-control)
4
+ Copyright (c) 2026 bd7pil (ocrc fork)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+
24
+ ---
25
+
26
+ Portions of this software (the Telegram UX layer transplanted by the P2b
27
+ workstream: managers, menus, callbacks and i18n) are derived from
28
+ @grinev/opencode-telegram-bot:
29
+
30
+ Copyright (c) 2026 Ruslan Grinev
31
+ https://github.com/grinev/opencode-telegram-bot
32
+
33
+ That portion is likewise distributed under the MIT License set out above,
34
+ with the copyright notice attributable to its author preserved here.
package/README.md ADDED
@@ -0,0 +1,330 @@
1
+ # ocrc — remote control for opencode
2
+
3
+ > **ocrc is a fork of [agentjoey/opencode-remote-control](https://github.com/agentjoey/opencode-remote-control) (OCRC, MIT).**
4
+ > Upstream's architecture — the in-process plugin, CardBus, transport
5
+ > interface, Telegram + Web PWA — is kept intact. This fork exists for four
6
+ > reasons, all additive on top of upstream's design:
7
+ >
8
+ > 1. **Dual-host** — one default export loads on opencode **V1 (server hook) and
9
+ > V2 (setup hook)**; no plugin beta dependency, local structural V2 types.
10
+ > 2. **More channels** — Feishu (official outbound WebSocket) and WeChat (official
11
+ > iLink bot API) as new transports under the same Transport interface; the
12
+ > relay core stays untouched.
13
+ > 3. **Telegram UX at grinev depth** — the interaction model of
14
+ > [@grinev/opencode-telegram-bot](https://github.com/grinev/opencode-telegram-bot)
15
+ > (MIT): interaction mutex, grouped permission requests, background-session
16
+ > notifications, Chinese i18n — transplanted onto ocrc's relay pipeline.
17
+ > 4. **LAN-first web** — binds `0.0.0.0:4099` by default (token gate mandatory,
18
+ > no cloud relay needed); dual light/dark "paper-ink" theme, auto-switched.
19
+ >
20
+ > Fork identity: npm `@bd7pil/ocrc`, config home `~/.ocrc/`, CLI `ocrc`.
21
+ > Functional behaviour otherwise follows upstream's original design — this fork
22
+ > changes identity, defaults and skins, not the interaction model.
23
+
24
+ # OCRC — opencode-remote-control (upstream README follows)
25
+
26
+ > **Drive your local opencode from your phone or browser.** An [opencode](https://opencode.ai)
27
+ > **plugin** that runs a **Telegram bot + a Web PWA** in-process — fire off a prompt from
28
+ > anywhere and watch the assistant code in real time, even when you're away from your desk.
29
+ > Now also drives any [ACP](https://agentclientprotocol.com) agent (e.g. Kimi) in standalone host mode.
30
+
31
+ [![Release](https://img.shields.io/github/v/release/agentjoey/opencode-remote-control?color=10b981)](https://github.com/BD7PIL/ocrc/releases)
32
+ [![License: MIT](https://img.shields.io/github/license/agentjoey/opencode-remote-control?color=10b981)](LICENSE)
33
+ [![CI](https://img.shields.io/github/actions/workflow/status/agentjoey/opencode-remote-control/ci.yml?branch=main&label=CI)](https://github.com/BD7PIL/ocrc/actions)
34
+ [![opencode plugin](https://img.shields.io/badge/opencode-plugin-10b981)](https://opencode.ai)
35
+ [![tests](https://img.shields.io/badge/tests-413%20backend%20%2B%2093%20web-10b981)](./CHANGELOG.md)
36
+
37
+ <p align="center">
38
+ <img src="docs/assets/ocrc-web.png" width="840" alt="OCRC — the Web PWA driving a live opencode session (sessions, live chat, task & cost inspector)">
39
+ </p>
40
+
41
+ Install once; it auto-starts in-process with opencode. Same sessions stream live to
42
+ **Telegram** and a desktop **PWA** at the same time — switch surfaces mid-task without
43
+ losing context.
44
+
45
+ ## ⚡ Quick start (5 minutes)
46
+
47
+ Requires **opencode 1.17+** and **Node 20+** (or Bun). Install from GitHub
48
+ (build from source); the npm package `@bd7pil/ocrc` is the official channel
49
+ (the `opencode-remote-control` name on npm is an unrelated package — don't
50
+ `npx` that).
51
+
52
+ There are two ways to run it — pick one:
53
+
54
+ ### Mode A — Plugin (default; controls opencode)
55
+
56
+ ```bash
57
+ # 1. Clone, build (backend + PWA), and install the plugin
58
+ git clone https://github.com/BD7PIL/ocrc
59
+ cd ocrc
60
+ npm install && npm run build:all
61
+ node dist/cli/install.js # interactive — paste your Telegram bot token + user id
62
+
63
+ # 2. Run opencode from any directory — the plugin auto-starts
64
+ opencode
65
+ ```
66
+
67
+ `install.js` writes a plugin bridge to `~/.config/opencode/plugins/` and your
68
+ config to `.env`. The plugin then loads in-process whenever opencode runs.
69
+
70
+ - **Telegram:** make a bot with [@BotFather](https://t.me/BotFather); get your numeric id from [@userinfobot](https://t.me/userinfobot) (the installer asks for both). Send "hello" → the assistant replies.
71
+ - **Web PWA:** enabled by default. Run `ocrc pair` (or send `/pair` in Telegram) → open the URL/QR it prints. Auth is a device **token** (persisted at `~/.ocrc/token`) — no Cloudflare Access needed.
72
+ - **From another device:** the web binds to `localhost`, so expose it over a tunnel or VPN — e.g. `tailscale serve 17081`. See [Remote access without a domain](#remote-access-without-a-domain).
73
+
74
+ ### Mode B — Standalone multi-backend host (opencode + ACP agents)
75
+
76
+ Run OCRC as its own process serving **multiple agents at once** (opencode + any
77
+ [ACP](https://agentclientprotocol.com) agent like Kimi) with an in-UI switcher —
78
+ no opencode plugin needed. Requires the ACP agent to be installed and logged in
79
+ (e.g. `kimi login`).
80
+
81
+ ```bash
82
+ git clone https://github.com/BD7PIL/ocrc
83
+ cd ocrc
84
+ npm install && npm run build:all
85
+
86
+ cp .env.acp.example .env.acp # set WEB_TOKEN; OCRC_BACKENDS="opencode, kimi=kimi acp"
87
+ scripts/run-acp-host.sh # run in a real terminal (needs full PATH to spawn agents)
88
+ ```
89
+
90
+ For an always-on service (auto-start + crash-restart), install the launchd unit
91
+ from `deploy/com.ocrc.host.plist`. Full runbook: [`docs/OPS.md`](docs/OPS.md).
92
+
93
+ > Note: `ocrc` isn't on your PATH by default — use `node dist/cli/index.js <cmd>`
94
+ > (e.g. `node dist/cli/index.js pair`), or `npm link` to get the `ocrc` shim.
95
+
96
+ ## How we're different
97
+
98
+ - **Runs as an opencode plugin, in-process.** One install, no extra process, no
99
+ daemon to babysit — it starts and stops with `opencode` itself.
100
+ - **Telegram + Web from a single codebase.** The same sessions stream live to
101
+ Telegram and a desktop PWA simultaneously; switch surfaces mid-task without
102
+ losing context.
103
+ - **SDK-native.** Built on `@opencode-ai/sdk` and the opencode plugin event
104
+ hook — it speaks opencode's own protocol rather than scraping a UI, so agent /
105
+ model overrides, approvals, diffs, and cost/token metadata all come through
106
+ first-class.
107
+ - **Transport-agnostic core.** A channel-neutral `CardBus` carries structured
108
+ cards; each transport renders them independently. Adding a new channel
109
+ (Discord, Slack, …) doesn't touch the relay core.
110
+ - **Local-first & single-user.** It runs on your machine against your local
111
+ opencode server, stores state in a local file, and answers to one allowlisted
112
+ user. No cloud, no shared backend.
113
+
114
+ ## Architecture
115
+
116
+ ```
117
+ ┌──────────────────────────────────────────────────────┐
118
+ │ opencode (single process) │
119
+ │ │
120
+ │ ┌──────────────────┐ ┌───────────────────────────┐ │
121
+ │ │ AI engine :4096 │ │ plugin: remote-control │ │
122
+ │ │ │ │ ├─ Telegraf (Telegram) │ │
123
+ │ │ event hook ────┼──┼─►├─ Hono + WS (Web PWA) │ │
124
+ │ │ │ │ └─ relay + CardBus │ │
125
+ │ └──────────────────┘ └──────────┬────────────────┘ │
126
+ │ ▼ │
127
+ │ Telegram / Web (PWA) │
128
+ └──────────────────────────────────────────────────────┘
129
+ ```
130
+
131
+ The plugin loads inside opencode and is driven by the plugin **event hook**: it
132
+ submits prompts via the SDK, consumes streaming events, and renders structured
133
+ cards to whichever transports are enabled. A TUI, if you run one, is just
134
+ another client of the same opencode server.
135
+
136
+ Full deep-dive: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
137
+
138
+ ## Quick Start (Telegram)
139
+
140
+ 1. **Create a bot** with [@BotFather](https://t.me/BotFather), get a token.
141
+ 2. **Find your user ID** — message [@userinfobot](https://t.me/userinfobot).
142
+ 3. **Build and install the plugin** (opencode 1.17+):
143
+ ```bash
144
+ npm install && npm run build
145
+ node dist/cli/install.js
146
+ ```
147
+ The installer writes a plugin bridge to `~/.config/opencode/plugins/`
148
+ (opencode 1.17 loads local plugins from there — directory paths in
149
+ `opencode.json` no longer work), ensures that dir's `package.json` has
150
+ `"type": "module"`, and saves `TELEGRAM_BOT_TOKEN` / `ALLOWED_USER_IDS` /
151
+ `WEB_ENABLED` / `WEB_PORT` to the repo's `.env`.
152
+ 4. **Run opencode** from any directory — the plugin loads globally:
153
+ ```bash
154
+ opencode
155
+ ```
156
+ The plugin auto-starts. For an always-on remote-control hub, run it from a
157
+ small/empty directory (e.g. `~/ocrc-hub`) so opencode's file watcher stays
158
+ fast. You can run several opencode instances — they elect one **PRIMARY**
159
+ (atomic lock at `~/.ocrc/primary.lock`) to own the web (`:4099`) and
160
+ Telegram singletons; the rest stand down PASSIVE. The web/bot can switch
161
+ between the workspaces of the running instances.
162
+ 5. **Send "hello"** in Telegram → the assistant responds.
163
+
164
+ ## Web UI (PWA)
165
+
166
+ The Web UI runs alongside Telegram and shows the same sessions in real time
167
+ (full streaming), installable as a desktop/mobile PWA.
168
+
169
+ 1. Set `WEB_ENABLED=true` (and `WEB_PORT` — default `17081`, since opencode's
170
+ own server occupies `7081`).
171
+ 2. Build the web app: `cd web && npm run build`.
172
+ 3. The plugin serves the PWA at `http://127.0.0.1:17081`.
173
+
174
+ ### Auth — pair a device (default, no Cloudflare Access needed)
175
+
176
+ Auth defaults to an app **token** (`WEB_AUTH=token`). Onboard a device with
177
+ `ocrc pair` (or Telegram `/pair`): it prints a URL + QR with the token in the
178
+ fragment (`https://<host>/#token=…`). Open it once — the app stores the token
179
+ and attaches it to every request thereafter. The token is persisted at
180
+ `~/.ocrc/token`, so it survives restarts and re-installs.
181
+
182
+ > Behind a tunnel, keep `WEB_CF_ACCESS_DEV_BYPASS=false`: `cloudflared` connects
183
+ > from loopback, so a loopback bypass would trust all tunnel traffic.
184
+
185
+ Prefer Cloudflare Access instead? Set `WEB_AUTH=cf-access` with
186
+ `WEB_CF_ACCESS_TEAM` / `WEB_CF_ACCESS_AUD` — see [`docs/OPS.md`](docs/OPS.md).
187
+
188
+ ### Remote access without a domain
189
+
190
+ A PWA install needs a **secure context** (HTTPS, or `http://localhost`). To
191
+ reach the hub from another machine without owning a domain:
192
+
193
+ | Method | Command | Notes |
194
+ |---|---|---|
195
+ | **Tailscale** (recommended) | `tailscale serve 17081` | Stable `https://<host>.ts.net`, device-level auth, survives restarts |
196
+ | **cloudflared quick tunnel** | `cloudflared tunnel --url http://localhost:17081` | Free `https://*.trycloudflare.com`; URL changes each run |
197
+ | **SSH port-forward** | `ssh -L 17081:localhost:17081 <host>` | Then open `http://127.0.0.1:17081` (localhost = secure context) |
198
+
199
+ Set `WEB_PUBLIC_URL` to the resulting HTTPS URL so `/pair` emits the right
200
+ links. (If you already run a `cloudflared` tunnel to `:4099`, `/pair`
201
+ auto-detects its hostname from `~/.cloudflared`.) Plain `http://<LAN-IP>` is
202
+ **not** a secure context — Chrome won't install it as an app.
203
+
204
+ ### Install as an app
205
+
206
+ In Chrome: the omnibox install icon, or **⋮ → Cast, save, and share → Install
207
+ page as app…**. The installed PWA reuses the browser's stored token, so it stays
208
+ signed in.
209
+
210
+ ## Commands
211
+
212
+ | Command | Description |
213
+ |---|---|
214
+ | `/start` | Handshake + health check |
215
+ | `/status` | Server health, session count, pinned session |
216
+ | `/sessions` | List all sessions with pin buttons |
217
+ | `/session <id>` | Pin a specific session |
218
+ | `/workspaces` | List known workspaces (directories) |
219
+ | `/new` | Start a new session in the active workspace |
220
+ | `/rename <title>` | Rename the pinned session |
221
+ | `/files` | Files touched in the last session |
222
+ | `/diff` | Pending git diff for the session |
223
+ | `/todo` | Session todo list |
224
+ | `/context` | Tokens + cost + model for the session |
225
+ | `/agent` | Set next agent (sticky until cleared) |
226
+ | `/model` | Set next model (sticky until cleared) |
227
+ | `/current` | Show pinned session |
228
+ | `/abort` | Stop the current generation |
229
+ | `/pair` | Pair a device (URL + QR with token) for the Web PWA |
230
+ | `/version` | Plugin version + uptime |
231
+ | `/help` | Show this list |
232
+
233
+ Send any text to relay it into opencode.
234
+
235
+ ## Push notifications
236
+
237
+ The plugin watches opencode sessions and proactively pushes summaries:
238
+
239
+ | Trigger | When | Content |
240
+ |---|---|---|
241
+ | Session finished | >60s run completes | Duration + assistant text summary (first 300 chars) |
242
+ | Test failure | Bash output contains FAIL/FAILED | Last 200 chars of output |
243
+
244
+ Rate limits: max 10 notifications/hour, 5-min cooldown per session. A session
245
+ the foreground UI just delivered is skipped (no double-ping).
246
+
247
+ ## Multi-transport
248
+
249
+ Telegram and Web run simultaneously and share the same opencode session state,
250
+ relaying output to every connected channel in real time.
251
+
252
+ | Transport | Status | Notes |
253
+ |---|---|---|
254
+ | Telegram | ✅ Stable | Final-result delivery, pagination, approvals |
255
+ | Web (PWA) | ✅ Stable | SvelteKit, full streaming, token auth (default) / Cloudflare Access |
256
+
257
+ To add another channel, see
258
+ [`docs/transports/CONTRIBUTING-NEW-TRANSPORT.md`](docs/transports/CONTRIBUTING-NEW-TRANSPORT.md).
259
+
260
+ ## Security model
261
+
262
+ - **Single-user per install.** Only the Telegram IDs in `ALLOWED_USER_IDS` can
263
+ interact with the bot.
264
+ - **Local-first.** Runs on your machine, talks to your local opencode server,
265
+ stores state in a local JSON file (`data/state.json`).
266
+ - **No secrets in repo.** `.env` is gitignored; `.env.example` documents every
267
+ variable.
268
+ - **Web auth is pluggable.** Default is an app **token** (auto-generated,
269
+ persisted `0600` at `~/.ocrc/token`), verified on HTTP and WS with a
270
+ constant-time compare; `WEB_AUTH=cf-access` switches to Cloudflare Access. The
271
+ dev bypass only trusts a real loopback peer (never the bind address) and is
272
+ **off by default**.
273
+
274
+ ## Environment variables
275
+
276
+ | Variable | Default | Description |
277
+ |---|---|---|
278
+ | `TELEGRAM_BOT_TOKEN` | — (required) | Telegram bot token from @BotFather |
279
+ | `ALLOWED_USER_IDS` | — (required) | Comma-separated allowed Telegram user IDs |
280
+ | `OPENCODE_BASE_URL` | — | opencode server URL. Unused in plugin mode (the SDK client is injected); legacy/sidecar only |
281
+ | `CHAT_TIMEOUT_MS` | `600000` | Per-message timeout (ms) |
282
+ | `TUI_VISIBLE` | `true` | Navigate the TUI to the target session; `false` = pure direct API |
283
+ | `STATE_PATH` | `./data/state.json` | Persistent state file |
284
+ | `LOG_LEVEL` | `info` | `debug` \| `info` \| `warn` \| `error` |
285
+ | `TG_CHUNK_SOFT_LIMIT` | `3500` | Telegram message pagination soft limit |
286
+ | **Web** |||
287
+ | `WEB_ENABLED` | `false` | Enable the Web transport |
288
+ | `WEB_HOST` | `127.0.0.1` | Web bind address (keep loopback; front with a tunnel) |
289
+ | `WEB_PORT` | `17081` | Web port (opencode 1.17's own server occupies `7081`) |
290
+ | `WEB_AUTH` | `token` | Auth strategy: `token` (app token) or `cf-access` |
291
+ | `WEB_TOKEN` | auto | App token; auto-generated and persisted `0600` at `~/.ocrc/token` if unset |
292
+ | `WEB_PUBLIC_URL` | — | Public URL for pairing/QR; falls back to LAN, then loopback |
293
+ | `WEB_STATIC_ROOT` | `<repo>/web/dist` | Built PWA path (resolved from the plugin dir, cwd-independent) |
294
+ | `WEB_SESSION_CACHE_SIZE` | `100` | Per-session card ring-buffer size |
295
+ | `WEB_CF_ACCESS_TEAM` | — | Cloudflare Access team name (when `WEB_AUTH=cf-access`) |
296
+ | `WEB_CF_ACCESS_AUD` | — | Cloudflare Access app AUD tag (when `WEB_AUTH=cf-access`) |
297
+ | `WEB_CF_ACCESS_DEV_BYPASS` | `false` | Bypass auth **only for a loopback socket peer** |
298
+
299
+ ## opencode 1.17+ notes
300
+
301
+ opencode 1.17 changed plugin loading; this project accounts for all of it:
302
+
303
+ - **Local plugins load from `~/.config/opencode/plugins/`**, not from directory
304
+ paths in `opencode.json`. The installer writes a bridge file there that
305
+ re-invokes the built `dist/` (the source of truth — rebuild + restart to
306
+ update). 1.17 also only calls functions *defined* in the loaded module, so the
307
+ bridge wraps the plugin in a local function rather than re-exporting it.
308
+ - **Plugins run in a worker thread.** An unhandled rejection would otherwise
309
+ crash the worker (`Worker has been terminated`), taking down the web server.
310
+ The plugin installs absorbing guards so it survives.
311
+ - **Web runs on `17081`** because opencode's own server occupies `7081`. Point
312
+ your tunnel ingress at `17081`.
313
+ - **PRIMARY election.** Web (`:4099`) and the Telegram bot are global
314
+ singletons. Multiple opencode instances elect one PRIMARY (atomic lock at
315
+ `~/.ocrc/primary.lock`) to own them; the others stand down PASSIVE.
316
+ Run the hub from a small/empty directory so the file watcher stays fast.
317
+
318
+ ## Testing
319
+
320
+ ```bash
321
+ npm test # backend unit tests
322
+ npx tsc --noEmit # backend type-check
323
+ cd web && npm run check # web type-check (svelte-check)
324
+ cd web && npm test # web unit tests
325
+ cd web && npm run build # build PWA
326
+ ```
327
+
328
+ ## License
329
+
330
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Builds the backend set for the standalone host from an OCRC_BACKENDS spec and
3
+ * wires each backend's event source to the relay. This is what lets one host
4
+ * serve multiple agents (opencode + kimi + …) with in-UI switching (Phase 3).
5
+ *
6
+ * Event sources differ by backend:
7
+ * - ACP backends own their stream (backend.onEvent → relay; busy/idle → push).
8
+ * - opencode has no onEvent: the host spawns its OWN opencode server
9
+ * (createOpencodeServer — validated reachable from a standalone process) and
10
+ * consumes the global event SSE, normalizing → relay and forwarding permission
11
+ * events to the approval bridge. opencode events feed push natively.
12
+ */
13
+ import { createOpencodeServer, createOpencodeClient } from '@opencode-ai/sdk';
14
+ import { createAcpBackend } from '../core/agent/acp-backend.js';
15
+ import { makeAcpConnect, parseAcpCommand } from '../core/agent/acp-connect.js';
16
+ import { createOpencodeBackend } from '../core/agent/opencode-backend.js';
17
+ import { normalizeOpencodeEvent } from '../core/agent/opencode-normalizer.js';
18
+ import { startGlobalEvents } from '../opencode/global-events.js';
19
+ import { createLogger } from '../utils/logger.js';
20
+ import { readFileSync } from 'node:fs';
21
+ import { homedir } from 'node:os';
22
+ import { join } from 'node:path';
23
+ const log = createLogger('host-backends');
24
+ /**
25
+ * Discover the working directories kimi has sessions in, by reading its global
26
+ * session index ($KIMI_CODE_HOME/session_index.jsonl — one {sessionId, workDir,
27
+ * sessionDir} per line). ACP `session/list` is per-cwd, so this lets OCRC surface
28
+ * sessions you created in the kimi TUI in directories OCRC never used. Best-effort
29
+ * + kimi-specific (file absent / other agents → []).
30
+ */
31
+ function kimiWorkDirs() {
32
+ try {
33
+ const home = process.env.KIMI_CODE_HOME || join(homedir(), '.kimi-code');
34
+ const dirs = new Set();
35
+ for (const line of readFileSync(join(home, 'session_index.jsonl'), 'utf8').split('\n')) {
36
+ const t = line.trim();
37
+ if (!t)
38
+ continue;
39
+ try {
40
+ const d = JSON.parse(t);
41
+ if (typeof d.workDir === 'string')
42
+ dirs.add(d.workDir);
43
+ }
44
+ catch { /* skip bad line */ }
45
+ }
46
+ return [...dirs];
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ }
52
+ /** kimi's own configured MCP servers (~/.kimi-code/mcp.json) — surfaced in the MCP panel. */
53
+ function kimiMcp() {
54
+ try {
55
+ const home = process.env.KIMI_CODE_HOME || join(homedir(), '.kimi-code');
56
+ const raw = JSON.parse(readFileSync(join(home, 'mcp.json'), 'utf8'));
57
+ return Object.entries(raw.mcpServers ?? {}).map(([name, cfg]) => ({
58
+ name,
59
+ type: cfg?.url ? (cfg.type ?? 'http') : 'stdio',
60
+ status: cfg?.disabled ? 'disabled' : 'configured',
61
+ }));
62
+ }
63
+ catch {
64
+ return [];
65
+ }
66
+ }
67
+ /**
68
+ * Parse OCRC_BACKENDS. Entries: `opencode` or `<id>=<acp command>`. When empty,
69
+ * falls back to a single ACP backend from OCRC_ACP_CMD (legacy single-backend host).
70
+ */
71
+ export function parseBackendsSpec(spec, fallbackAcpCmd) {
72
+ const entries = spec.split(',').map((s) => s.trim()).filter(Boolean);
73
+ if (entries.length === 0) {
74
+ const cmd = parseAcpCommand(fallbackAcpCmd);
75
+ return [{ id: `acp:${cmd.command}`, kind: 'acp', command: fallbackAcpCmd }];
76
+ }
77
+ return entries.map((e) => {
78
+ if (e === 'opencode')
79
+ return { id: 'opencode', kind: 'opencode' };
80
+ const eq = e.indexOf('=');
81
+ if (eq === -1) {
82
+ const cmd = parseAcpCommand(e);
83
+ return { id: `acp:${cmd.command}`, kind: 'acp', command: e };
84
+ }
85
+ const rawId = e.slice(0, eq).trim();
86
+ const command = e.slice(eq + 1).trim();
87
+ return { id: rawId.includes(':') ? rawId : `acp:${rawId}`, kind: 'acp', command };
88
+ });
89
+ }
90
+ const PERMISSION_TYPES = new Set(['permission.asked', 'permission.updated', 'permission.replied']);
91
+ /** How many consecutive ports to probe when spawning an opencode server. */
92
+ const PORT_PROBE_ATTEMPTS = 10;
93
+ /**
94
+ * Spawn an opencode server, probing forward from `startPort` when a port is
95
+ * taken (previously a busy 4096 silently dropped the whole opencode backend).
96
+ * Returns the server and the port it bound; rethrows the last error when every
97
+ * probe fails.
98
+ */
99
+ async function spawnOpencodeServer(startPort) {
100
+ let lastErr;
101
+ for (let port = startPort; port < startPort + PORT_PROBE_ATTEMPTS; port++) {
102
+ try {
103
+ const server = await createOpencodeServer({ hostname: '127.0.0.1', port, timeout: 15000 });
104
+ if (port !== startPort)
105
+ log.warn(`port ${startPort} unavailable — opencode backend bound to ${port} instead`);
106
+ return { server, port };
107
+ }
108
+ catch (err) {
109
+ lastErr = err;
110
+ log.warn(`opencode server failed on port ${port}: ${err.message}`);
111
+ }
112
+ }
113
+ throw lastErr;
114
+ }
115
+ export async function buildHostBackends(specs, deps) {
116
+ const backends = [];
117
+ const opencodeServers = [];
118
+ /** ACP connect factories — dispose() kills their spawned agent children. */
119
+ const acpConnects = [];
120
+ let nextPort = deps.opencodePort ?? 4096;
121
+ for (const spec of specs) {
122
+ if (spec.kind === 'opencode') {
123
+ try {
124
+ const { server, port } = await spawnOpencodeServer(nextPort);
125
+ nextPort = port + 1;
126
+ const client = createOpencodeClient({ baseUrl: server.url });
127
+ const backend = createOpencodeBackend({ client, baseUrl: server.url });
128
+ backends.push({ id: spec.id, backend });
129
+ opencodeServers.push({ id: spec.id, client, close: async () => { try {
130
+ await server.close();
131
+ }
132
+ catch { /* noop */ } } });
133
+ log.info(`opencode backend ready @ ${server.url}`);
134
+ }
135
+ catch (err) {
136
+ log.error(`failed to start opencode backend (skipping): ${err.message}`);
137
+ }
138
+ }
139
+ else {
140
+ const connect = makeAcpConnect(parseAcpCommand(spec.command ?? 'kimi acp'));
141
+ acpConnects.push(connect);
142
+ const backend = createAcpBackend({
143
+ id: spec.id,
144
+ cwd: deps.cwd,
145
+ connect,
146
+ onPermission: deps.onAcpPermission,
147
+ store: deps.store,
148
+ discoverDirs: kimiWorkDirs,
149
+ discoverMcp: kimiMcp,
150
+ });
151
+ backends.push({ id: spec.id, backend });
152
+ log.info(`acp backend ready: ${spec.id} (${spec.command})`);
153
+ }
154
+ }
155
+ if (backends.length === 0)
156
+ throw new Error('host: no backends could be started');
157
+ return {
158
+ backends,
159
+ wire(relay, push, onOpencodePermission) {
160
+ const disposers = [];
161
+ // push.handleEvent is async — fire-and-forget, but never let a rejection
162
+ // escape to the global guard.
163
+ const safePush = (ev) => {
164
+ void Promise.resolve(push.handleEvent(ev)).catch((err) => log.warn('push.handleEvent failed', err));
165
+ };
166
+ // ACP backends: own their stream.
167
+ for (const { backend } of backends) {
168
+ if (!backend.onEvent)
169
+ continue;
170
+ const off = backend.onEvent((e) => {
171
+ relay.handleEvent(e).catch((err) => log.error('relay.handleEvent failed', err));
172
+ if (e.kind === 'idle')
173
+ safePush({ type: 'session.idle', properties: { sessionID: e.sessionId } });
174
+ else if (e.kind === 'part' || e.kind === 'delta')
175
+ safePush({ type: 'session.status', properties: { sessionID: e.sessionId, status: { type: 'busy' } } });
176
+ });
177
+ disposers.push(off);
178
+ }
179
+ // opencode backends: pull the global event SSE.
180
+ for (const { client } of opencodeServers) {
181
+ const handle = startGlobalEvents({
182
+ client,
183
+ onEvent: (ev) => {
184
+ safePush(ev); // opencode events are already the shape push expects
185
+ if (PERMISSION_TYPES.has(ev.type ?? '')) {
186
+ onOpencodePermission(ev);
187
+ return;
188
+ }
189
+ const ae = normalizeOpencodeEvent(ev);
190
+ if (ae)
191
+ relay.handleEvent(ae).catch((err) => log.error('relay.handleEvent failed', err));
192
+ },
193
+ });
194
+ disposers.push(() => handle.stop());
195
+ }
196
+ return async () => {
197
+ for (const d of disposers) {
198
+ try {
199
+ d();
200
+ }
201
+ catch { /* noop */ }
202
+ }
203
+ // Kill spawned ACP agent children so they don't outlive the host.
204
+ for (const c of acpConnects) {
205
+ try {
206
+ c.dispose();
207
+ }
208
+ catch { /* noop */ }
209
+ }
210
+ for (const s of opencodeServers)
211
+ await s.close();
212
+ };
213
+ },
214
+ };
215
+ }
216
+ //# sourceMappingURL=host-backends.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-backends.js","sourceRoot":"","sources":["../../src/cli/host-backends.ts"],"names":[],"mappings":"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"}