@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.
- package/LICENSE +34 -0
- package/README.md +330 -0
- package/dist/cli/host-backends.js +216 -0
- package/dist/cli/host-backends.js.map +1 -0
- package/dist/cli/host.js +185 -0
- package/dist/cli/host.js.map +1 -0
- package/dist/cli/index.js +67 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.js +68 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/install.js +220 -0
- package/dist/cli/install.js.map +1 -0
- package/dist/cli/pair.js +11 -0
- package/dist/cli/pair.js.map +1 -0
- package/dist/cli/uninstall.js +75 -0
- package/dist/cli/uninstall.js.map +1 -0
- package/dist/connectivity/auth/cf-access.js +20 -0
- package/dist/connectivity/auth/cf-access.js.map +1 -0
- package/dist/connectivity/auth/index.js +2 -0
- package/dist/connectivity/auth/index.js.map +1 -0
- package/dist/connectivity/auth/select.js +19 -0
- package/dist/connectivity/auth/select.js.map +1 -0
- package/dist/connectivity/auth/token.js +157 -0
- package/dist/connectivity/auth/token.js.map +1 -0
- package/dist/connectivity/exposure/cloudflared.js +77 -0
- package/dist/connectivity/exposure/cloudflared.js.map +1 -0
- package/dist/connectivity/exposure/index.js +2 -0
- package/dist/connectivity/exposure/index.js.map +1 -0
- package/dist/connectivity/exposure/providers.js +49 -0
- package/dist/connectivity/exposure/providers.js.map +1 -0
- package/dist/connectivity/pairing.js +29 -0
- package/dist/connectivity/pairing.js.map +1 -0
- package/dist/core/agent/acp-backend.js +539 -0
- package/dist/core/agent/acp-backend.js.map +1 -0
- package/dist/core/agent/acp-connect.js +92 -0
- package/dist/core/agent/acp-connect.js.map +1 -0
- package/dist/core/agent/acp-normalizer.js +81 -0
- package/dist/core/agent/acp-normalizer.js.map +1 -0
- package/dist/core/agent/acp-replay.js +89 -0
- package/dist/core/agent/acp-replay.js.map +1 -0
- package/dist/core/agent/acp-store.js +129 -0
- package/dist/core/agent/acp-store.js.map +1 -0
- package/dist/core/agent/backend.js +2 -0
- package/dist/core/agent/backend.js.map +1 -0
- package/dist/core/agent/diff-util.js +70 -0
- package/dist/core/agent/diff-util.js.map +1 -0
- package/dist/core/agent/event.js +12 -0
- package/dist/core/agent/event.js.map +1 -0
- package/dist/core/agent/opencode-backend.js +598 -0
- package/dist/core/agent/opencode-backend.js.map +1 -0
- package/dist/core/agent/opencode-normalizer.js +69 -0
- package/dist/core/agent/opencode-normalizer.js.map +1 -0
- package/dist/core/agent/registry.js +55 -0
- package/dist/core/agent/registry.js.map +1 -0
- package/dist/core/agent/v2-backend.js +176 -0
- package/dist/core/agent/v2-backend.js.map +1 -0
- package/dist/core/card-bus.js +74 -0
- package/dist/core/card-bus.js.map +1 -0
- package/dist/core/channels.js +95 -0
- package/dist/core/channels.js.map +1 -0
- package/dist/core/history.js +91 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/list-files.js +50 -0
- package/dist/core/list-files.js.map +1 -0
- package/dist/core/opencode-events.js +29 -0
- package/dist/core/opencode-events.js.map +1 -0
- package/dist/core/primary-election.js +91 -0
- package/dist/core/primary-election.js.map +1 -0
- package/dist/core/push.js +152 -0
- package/dist/core/push.js.map +1 -0
- package/dist/core/relay.js +400 -0
- package/dist/core/relay.js.map +1 -0
- package/dist/core/scheduler.js +150 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/state.js +222 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/stream-accumulator.js +76 -0
- package/dist/core/stream-accumulator.js.map +1 -0
- package/dist/core/structured-card.js +2 -0
- package/dist/core/structured-card.js.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/opencode/global-events.js +57 -0
- package/dist/opencode/global-events.js.map +1 -0
- package/dist/opencode/list-sessions.js +122 -0
- package/dist/opencode/list-sessions.js.map +1 -0
- package/dist/opencode/submit.js +39 -0
- package/dist/opencode/submit.js.map +1 -0
- package/dist/opencode/workspaces.js +51 -0
- package/dist/opencode/workspaces.js.map +1 -0
- package/dist/plugin/config.js +128 -0
- package/dist/plugin/config.js.map +1 -0
- package/dist/plugin/control-plane.js +133 -0
- package/dist/plugin/control-plane.js.map +1 -0
- package/dist/plugin/entry.js +409 -0
- package/dist/plugin/entry.js.map +1 -0
- package/dist/plugin/v2/event-map.js +193 -0
- package/dist/plugin/v2/event-map.js.map +1 -0
- package/dist/plugin/v2/types.js +15 -0
- package/dist/plugin/v2/types.js.map +1 -0
- package/dist/transport/interface.js +2 -0
- package/dist/transport/interface.js.map +1 -0
- package/dist/transport/telegram/esc.js +9 -0
- package/dist/transport/telegram/esc.js.map +1 -0
- package/dist/transport/telegram/handlers/info-commands.js +101 -0
- package/dist/transport/telegram/handlers/info-commands.js.map +1 -0
- package/dist/transport/telegram/handlers.js +1348 -0
- package/dist/transport/telegram/handlers.js.map +1 -0
- package/dist/transport/telegram/i18n/en.js +538 -0
- package/dist/transport/telegram/i18n/en.js.map +1 -0
- package/dist/transport/telegram/i18n/index.js +83 -0
- package/dist/transport/telegram/i18n/index.js.map +1 -0
- package/dist/transport/telegram/i18n/zh.js +538 -0
- package/dist/transport/telegram/i18n/zh.js.map +1 -0
- package/dist/transport/telegram/index.js +792 -0
- package/dist/transport/telegram/index.js.map +1 -0
- package/dist/transport/telegram/main-keyboard.js +36 -0
- package/dist/transport/telegram/main-keyboard.js.map +1 -0
- package/dist/transport/telegram/managers/abort-suppression-manager.js +33 -0
- package/dist/transport/telegram/managers/abort-suppression-manager.js.map +1 -0
- package/dist/transport/telegram/managers/external-input-suppression-manager.js +51 -0
- package/dist/transport/telegram/managers/external-input-suppression-manager.js.map +1 -0
- package/dist/transport/telegram/managers/interaction-manager.js +254 -0
- package/dist/transport/telegram/managers/interaction-manager.js.map +1 -0
- package/dist/transport/telegram/managers/permission-manager.js +242 -0
- package/dist/transport/telegram/managers/permission-manager.js.map +1 -0
- package/dist/transport/telegram/managers/question-manager.js +191 -0
- package/dist/transport/telegram/managers/question-manager.js.map +1 -0
- package/dist/transport/telegram/managers/rename-manager.js +56 -0
- package/dist/transport/telegram/managers/rename-manager.js.map +1 -0
- package/dist/transport/telegram/menus.js +77 -0
- package/dist/transport/telegram/menus.js.map +1 -0
- package/dist/transport/telegram/permission-flow.js +156 -0
- package/dist/transport/telegram/permission-flow.js.map +1 -0
- package/dist/transport/telegram/question-flow.js +224 -0
- package/dist/transport/telegram/question-flow.js.map +1 -0
- package/dist/transport/telegram/render/block-parser.js +365 -0
- package/dist/transport/telegram/render/block-parser.js.map +1 -0
- package/dist/transport/telegram/render/block-plain-text.js +87 -0
- package/dist/transport/telegram/render/block-plain-text.js.map +1 -0
- package/dist/transport/telegram/render/block-splitter.js +152 -0
- package/dist/transport/telegram/render/block-splitter.js.map +1 -0
- package/dist/transport/telegram/render/chunker.js +71 -0
- package/dist/transport/telegram/render/chunker.js.map +1 -0
- package/dist/transport/telegram/render/limits.js +13 -0
- package/dist/transport/telegram/render/limits.js.map +1 -0
- package/dist/transport/telegram/render/markdown-normalizer.js +102 -0
- package/dist/transport/telegram/render/markdown-normalizer.js.map +1 -0
- package/dist/transport/telegram/render/markdown-to-telegram-v2.js +116 -0
- package/dist/transport/telegram/render/markdown-to-telegram-v2.js.map +1 -0
- package/dist/transport/telegram/render/part-signature.js +14 -0
- package/dist/transport/telegram/render/part-signature.js.map +1 -0
- package/dist/transport/telegram/render/pipeline.js +23 -0
- package/dist/transport/telegram/render/pipeline.js.map +1 -0
- package/dist/transport/telegram/render/rich-blocks.js +311 -0
- package/dist/transport/telegram/render/rich-blocks.js.map +1 -0
- package/dist/transport/telegram/render/text-splitter.js +64 -0
- package/dist/transport/telegram/render/text-splitter.js.map +1 -0
- package/dist/transport/telegram/render/types.js +2 -0
- package/dist/transport/telegram/render/types.js.map +1 -0
- package/dist/transport/telegram/renderer.js +305 -0
- package/dist/transport/telegram/renderer.js.map +1 -0
- package/dist/transport/telegram/streaming/duration-formatter.js +68 -0
- package/dist/transport/telegram/streaming/duration-formatter.js.map +1 -0
- package/dist/transport/telegram/streaming/finalize-assistant-response.js +37 -0
- package/dist/transport/telegram/streaming/finalize-assistant-response.js.map +1 -0
- package/dist/transport/telegram/streaming/response-streamer.js +400 -0
- package/dist/transport/telegram/streaming/response-streamer.js.map +1 -0
- package/dist/transport/telegram/streaming/running-tool-tracker.js +150 -0
- package/dist/transport/telegram/streaming/running-tool-tracker.js.map +1 -0
- package/dist/transport/telegram/streaming/stream-throttle.js +57 -0
- package/dist/transport/telegram/streaming/stream-throttle.js.map +1 -0
- package/dist/transport/telegram/streaming/tool-call-streamer.js +390 -0
- package/dist/transport/telegram/streaming/tool-call-streamer.js.map +1 -0
- package/dist/transport/telegram/streaming-render.js +182 -0
- package/dist/transport/telegram/streaming-render.js.map +1 -0
- package/dist/transport/telegram/types/interaction.js +2 -0
- package/dist/transport/telegram/types/interaction.js.map +1 -0
- package/dist/transport/telegram/types/model.js +25 -0
- package/dist/transport/telegram/types/model.js.map +1 -0
- package/dist/transport/telegram/types/permission.js +2 -0
- package/dist/transport/telegram/types/permission.js.map +1 -0
- package/dist/transport/telegram/types/question.js +2 -0
- package/dist/transport/telegram/types/question.js.map +1 -0
- package/dist/transport/telegram/types/rename.js +2 -0
- package/dist/transport/telegram/types/rename.js.map +1 -0
- package/dist/transport/telegram/types/scheduled-task.js +23 -0
- package/dist/transport/telegram/types/scheduled-task.js.map +1 -0
- package/dist/transport/telegram/types/session.js +2 -0
- package/dist/transport/telegram/types/session.js.map +1 -0
- package/dist/transport/telegram/ui.js +28 -0
- package/dist/transport/telegram/ui.js.map +1 -0
- package/dist/transport/web/index.js +148 -0
- package/dist/transport/web/index.js.map +1 -0
- package/dist/transport/web/middleware/cf-access.js +104 -0
- package/dist/transport/web/middleware/cf-access.js.map +1 -0
- package/dist/transport/web/opencode-config.js +15 -0
- package/dist/transport/web/opencode-config.js.map +1 -0
- package/dist/transport/web/routes/abort.js +12 -0
- package/dist/transport/web/routes/abort.js.map +1 -0
- package/dist/transport/web/routes/approval.js +16 -0
- package/dist/transport/web/routes/approval.js.map +1 -0
- package/dist/transport/web/routes/catalog.js +11 -0
- package/dist/transport/web/routes/catalog.js.map +1 -0
- package/dist/transport/web/routes/channels.js +53 -0
- package/dist/transport/web/routes/channels.js.map +1 -0
- package/dist/transport/web/routes/commands.js +15 -0
- package/dist/transport/web/routes/commands.js.map +1 -0
- package/dist/transport/web/routes/context.js +17 -0
- package/dist/transport/web/routes/context.js.map +1 -0
- package/dist/transport/web/routes/controls.js +34 -0
- package/dist/transport/web/routes/controls.js.map +1 -0
- package/dist/transport/web/routes/create-session.js +21 -0
- package/dist/transport/web/routes/create-session.js.map +1 -0
- package/dist/transport/web/routes/diff.js +7 -0
- package/dist/transport/web/routes/diff.js.map +1 -0
- package/dist/transport/web/routes/files.js +22 -0
- package/dist/transport/web/routes/files.js.map +1 -0
- package/dist/transport/web/routes/logs.js +9 -0
- package/dist/transport/web/routes/logs.js.map +1 -0
- package/dist/transport/web/routes/m8.js +69 -0
- package/dist/transport/web/routes/m8.js.map +1 -0
- package/dist/transport/web/routes/mcp.js +7 -0
- package/dist/transport/web/routes/mcp.js.map +1 -0
- package/dist/transport/web/routes/message.js +32 -0
- package/dist/transport/web/routes/message.js.map +1 -0
- package/dist/transport/web/routes/overrides.js +20 -0
- package/dist/transport/web/routes/overrides.js.map +1 -0
- package/dist/transport/web/routes/question.js +32 -0
- package/dist/transport/web/routes/question.js.map +1 -0
- package/dist/transport/web/routes/rename.js +12 -0
- package/dist/transport/web/routes/rename.js.map +1 -0
- package/dist/transport/web/routes/schedules.js +28 -0
- package/dist/transport/web/routes/schedules.js.map +1 -0
- package/dist/transport/web/routes/session.js +20 -0
- package/dist/transport/web/routes/session.js.map +1 -0
- package/dist/transport/web/routes/sessions.js +25 -0
- package/dist/transport/web/routes/sessions.js.map +1 -0
- package/dist/transport/web/routes/subagents.js +11 -0
- package/dist/transport/web/routes/subagents.js.map +1 -0
- package/dist/transport/web/routes/suggestions.js +7 -0
- package/dist/transport/web/routes/suggestions.js.map +1 -0
- package/dist/transport/web/routes/todo.js +7 -0
- package/dist/transport/web/routes/todo.js.map +1 -0
- package/dist/transport/web/routes/version.js +5 -0
- package/dist/transport/web/routes/version.js.map +1 -0
- package/dist/transport/web/routes/workspaces.js +7 -0
- package/dist/transport/web/routes/workspaces.js.map +1 -0
- package/dist/transport/web/server.js +111 -0
- package/dist/transport/web/server.js.map +1 -0
- package/dist/transport/web/session-summary.js +46 -0
- package/dist/transport/web/session-summary.js.map +1 -0
- package/dist/transport/web/ws-hub.js +83 -0
- package/dist/transport/web/ws-hub.js.map +1 -0
- package/dist/utils/logger.js +105 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/markdown.js +129 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/paths.js +18 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/version.js +44 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +91 -0
- package/web/dist/_app/env.js +1 -0
- package/web/dist/_app/immutable/assets/0.DxN3RsuG.css +1 -0
- package/web/dist/_app/immutable/assets/2.BNuGfmZP.css +1 -0
- package/web/dist/_app/immutable/assets/3.CTDprntu.css +1 -0
- package/web/dist/_app/immutable/chunks/BUpwHpMJ.js +1 -0
- package/web/dist/_app/immutable/chunks/BjY4dxcB.js +2 -0
- package/web/dist/_app/immutable/chunks/C2FuqD3s.js +1 -0
- package/web/dist/_app/immutable/chunks/CUHGq08X.js +2 -0
- package/web/dist/_app/immutable/chunks/CWj6FrbW.js +1 -0
- package/web/dist/_app/immutable/chunks/ClvoqtGj.js +1 -0
- package/web/dist/_app/immutable/chunks/DIEDQ_hk.js +1 -0
- package/web/dist/_app/immutable/entry/app.BoRpcVMj.js +2 -0
- package/web/dist/_app/immutable/entry/start.Cj-PQNTq.js +1 -0
- package/web/dist/_app/immutable/nodes/0.y9YEms5p.js +3 -0
- package/web/dist/_app/immutable/nodes/1.CPhhiRM2.js +1 -0
- package/web/dist/_app/immutable/nodes/2.B_c84Mc9.js +1 -0
- package/web/dist/_app/immutable/nodes/3.ClTd0J3D.js +64 -0
- package/web/dist/_app/version.json +1 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/favicon.png +0 -0
- package/web/dist/favicon.svg +17 -0
- package/web/dist/icon-192.png +0 -0
- package/web/dist/icon-512-maskable.png +0 -0
- package/web/dist/icon-512.png +0 -0
- package/web/dist/index.html +73 -0
- package/web/dist/manifest.webmanifest +28 -0
- 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
|
+
[](https://github.com/BD7PIL/ocrc/releases)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](https://github.com/BD7PIL/ocrc/actions)
|
|
34
|
+
[](https://opencode.ai)
|
|
35
|
+
[](./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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"}
|