@huynle/hub 0.0.0-stage → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/Dockerfile +51 -0
  3. package/LICENSE +21 -0
  4. package/README.md +1029 -2
  5. package/bin/hub.ts +20 -0
  6. package/docker/hub-deps.ts +42 -0
  7. package/package.json +85 -4
  8. package/rpc.ts +2 -0
  9. package/scripts/hub-local.sh +28 -0
  10. package/scripts/io.github.huynle.hub.plist +42 -0
  11. package/server.ts +3 -0
  12. package/src/central/assets.ts +74 -0
  13. package/src/central/auth.ts +271 -0
  14. package/src/central/config.ts +310 -0
  15. package/src/central/http.ts +1247 -0
  16. package/src/central/main.ts +136 -0
  17. package/src/central/oidc.ts +295 -0
  18. package/src/central/push-target.ts +97 -0
  19. package/src/central/push.ts +285 -0
  20. package/src/central/registry.ts +1153 -0
  21. package/src/central/speech/models.ts +89 -0
  22. package/src/central/speech/openai.ts +66 -0
  23. package/src/central/speech/openrouter.ts +71 -0
  24. package/src/central/speech/provider.ts +74 -0
  25. package/src/central/speech/service.ts +168 -0
  26. package/src/central/speech/settings.ts +375 -0
  27. package/src/central/spoke-ws.ts +300 -0
  28. package/src/central/store.ts +653 -0
  29. package/src/config.ts +170 -0
  30. package/src/env.ts +63 -0
  31. package/src/index.ts +236 -0
  32. package/src/key.ts +17 -0
  33. package/src/loopback.ts +41 -0
  34. package/src/migrate.ts +105 -0
  35. package/src/openrouter.ts +3 -0
  36. package/src/options.ts +33 -0
  37. package/src/protocol.ts +650 -0
  38. package/src/reply.ts +27 -0
  39. package/src/rpc.ts +98 -0
  40. package/src/setup.ts +124 -0
  41. package/src/speakable.ts +35 -0
  42. package/src/spoke/client.ts +272 -0
  43. package/src/spoke/conversation.ts +93 -0
  44. package/src/spoke/enroll.ts +79 -0
  45. package/src/spoke/herdr.ts +346 -0
  46. package/src/spoke/listing.ts +108 -0
  47. package/src/spoke/localhub.ts +287 -0
  48. package/src/spoke/locations.ts +34 -0
  49. package/src/spoke/own-server.ts +32 -0
  50. package/src/spoke/presence.ts +45 -0
  51. package/src/spoke/singleton.ts +103 -0
  52. package/src/spoke/spoke.ts +1212 -0
  53. package/src/spoke/summary.ts +117 -0
  54. package/src/spoke/timeline.ts +93 -0
  55. package/src/spoke/tracker.ts +345 -0
  56. package/src/spoke/watch.ts +78 -0
  57. package/src/status.ts +232 -0
  58. package/src/tui-commands.ts +53 -0
  59. package/src/tui-forms.ts +77 -0
  60. package/src/tui-presence.ts +75 -0
  61. package/src/tui.ts +479 -0
  62. package/src/turns.ts +94 -0
  63. package/src/voice-command.ts +65 -0
  64. package/tui.ts +2 -0
  65. package/web/dist/apple-touch-icon.png +0 -0
  66. package/web/dist/assets/index-DClBvfLL.css +1 -0
  67. package/web/dist/assets/index-Djo_yF0a.js +12 -0
  68. package/web/dist/favicon.ico +0 -0
  69. package/web/dist/icon-192.png +0 -0
  70. package/web/dist/icon-512.png +0 -0
  71. package/web/dist/icon-maskable-512.png +0 -0
  72. package/web/dist/icon.svg +18 -0
  73. package/web/dist/index.html +25 -0
  74. package/web/dist/manifest.webmanifest +18 -0
  75. package/web/dist/sw.js +151 -0
  76. package/web/dist/vad-worklet.js +27 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,269 @@
1
+ # Changelog
2
+
3
+ ## 0.6.1
4
+
5
+ - **GitHub installs**: an install without the built web app (`web/dist`, e.g. `opencode plugin add
6
+ github:huynle/hub`) no longer starts a local hub that only answers 503: `/hub` → **Start local hub**
7
+ (and opening the hub, and the automatic start) report that the web app is missing and how to get it
8
+ (npm package, release binary, or `npm run build:web` in a checkout).
9
+ - **Push security**: `POST /api/push/subscribe` only accepts https endpoints on public hostnames. IP
10
+ literals in loopback, private, link-local, CGNAT and ULA ranges, `localhost`, `*.local` and `*.internal`
11
+ are rejected, and the host is resolved at subscribe time and rejected when it resolves to such an address
12
+ or not at all (400). Push services (FCM, Mozilla, Apple, Windows) keep working.
13
+ - **Model overrides**: `PUT /api/sessions/:id/model-prefs` only accepts models (and variants) from the
14
+ session's catalog; anything else is `400 Unknown model`, and a catalog that cannot be fetched is a 502
15
+ instead of an accepted selection. Clearing a Voice model override switches the session back to the model
16
+ it had before the first override (at the next turn boundary if it is running), unless you picked another
17
+ model meanwhile.
18
+ - **State directory**: the hub no longer crashes with `EPERM` on a fresh volume whose mount root it does
19
+ not own (a Kubernetes local-path PVC with `fsGroup`). When the state directory cannot be restricted to
20
+ 0700 but is writable, the hub logs a one-line warning and continues; state files are still written 0600.
21
+ An unwritable directory still fails.
22
+ - **Permissions**: fixed "Permission request not found" when allowing or denying from the hub. OpenCode
23
+ keeps pending permissions per location (directory), and the spoke replied through the first plugin
24
+ instance of the process; it now replies through the instance of the location that holds the request.
25
+ The hub tries every process that forwarded a request; a request nobody holds any more (answered in the
26
+ terminal, expired) answers `409 {code: "gone"}` and its card goes away.
27
+ - **Questions**: forms from the agent (the question tool, any OpenCode form) are answered in the console:
28
+ text, single and multiple choice (with your own answer), yes/no, numbers and links, with validation and
29
+ Submit / Dismiss (`POST /api/sessions/:id/form`). A standalone server started by a TUI answers through
30
+ that TUI (RPC `hub.formActions`).
31
+ - **Subagent sessions**: a subagent row opens the subagent's own page with a breadcrumb back to its
32
+ parent, its own live output, status and permission/question cards, and a composer that steers it. The
33
+ hub serves subagent sessions no process lists from their root's tree (same owner checks); a session is
34
+ never nested under another user's session.
35
+
36
+ - **Console**: the All sessions table has no Voice column and no Voice on/off filter chips (saved Voice
37
+ filters and sort are ignored). The session live in Voice (Voice mode in this browser, or a conversation)
38
+ shows a small pulsing green dot left of its title in the table, the phone list and the sidebar
39
+ (no animation with reduced motion); sessions that merely allow Voice get no badge. The row `⋯` menu
40
+ says **Allow Voice** / **Disallow Voice**. The old session browser's "Voice on" filter is gone.
41
+
42
+ ## 0.6.0
43
+
44
+ **OpenCode Hub is now Hub** (`@huynle/hub`): a harness-agnostic central hub that AI agent harnesses
45
+ connect to. OpenCode is the first supported harness; Voice is one feature (`/voice`), and `/hub`
46
+ configures the harness's hub connection. Everything from opencode-hub and opencode-voice keeps working;
47
+ see [Migrating from opencode-voice or opencode-hub](README.md#migrating-from-opencode-voice-or-opencode-hub).
48
+
49
+ - **Package and repository**: npm `@huynle/hub` 0.6.0, repository `github.com/huynle/hub`. Install with
50
+ `opencode plugin add @huynle/hub`. The interim name `@huynle/opencode-hub` was never published to npm, so
51
+ there is **no `@huynle/opencode-hub` shim**; `@huynle/opencode-voice` 0.6.0 re-exports `@huynle/hub`
52
+ with a rename notice.
53
+ - **Commands**: the hub server command is `hub-server` (a bare `hub` would collide with GitHub's `hub`
54
+ CLI); `opencode-hub` and `opencode-voice-hub` stay as aliases. Release binaries are
55
+ `hub-<os>-<arch>[.exe]`; the image is `ghcr.io/huynle/hub`.
56
+ - **Plugin ids** `hub` and `hub.tui` (were `opencode-hub` / `opencode-hub.tui`); TUI command ids
57
+ `hub.open` (`/hub`) and `hub.voice` (`/voice`) in the palette group "Hub". The RPC id stays `hub`, and the
58
+ server still registers the legacy `voice` id. The TUI's commands come from a small registry
59
+ (`src/tui-commands.ts`) so further `/<feature>` commands are one entry each.
60
+ - **Config and state**: `~/.config/hub/{config,hub}.json` and `~/.local/state/hub`. On first start, when
61
+ the new location is missing, the `opencode-hub` copy is copied there, else the `opencode-voice` one
62
+ (copy only, modes kept, the old copies never deleted, the `hub.pid` lock skipped; explicit paths skip
63
+ it).
64
+ - **Env**: `HUB_SPOKE_CONFIG` is the new name of the plugin config path; `OPENCODE_HUB_CONFIG` and
65
+ `OPENCODE_VOICE_CONFIG` still work with a one-time deprecation warning (newest legacy name wins). The
66
+ other `HUB_*` names are unchanged, with the `VOICE_*` fallbacks as before.
67
+ - **Branding**: "Hub" in the console (title, sidebar, sign-in page), the installed app (manifest name and
68
+ short name "Hub"), notifications, TUI toasts and dialogs, and the hub's logs (`[hub]` prefix). Prompts the
69
+ hub sends carry `metadata.source: "hub"`; `"opencode-hub"` and `"opencode-voice"` are still recognized.
70
+ The default VAPID subject is `mailto:hub@localhost`. Device cookies (`voice_device`, `voice_oidc`) and
71
+ the `voice` agent are unchanged, so nobody is signed out.
72
+ - **New icon**: an original Hub mark (a central node in the console's accent joined by three spokes to
73
+ satellite nodes) on the dark tile, as `icon.svg`, 192/512 px PNGs, a maskable icon, an Apple touch icon
74
+ and a `favicon.ico` (16/32/48 px), also drawn inline next to the "Hub" wordmark in the sidebar and on the
75
+ sign-in page. `scripts/gen-icons.ts` generates them all with headless Chromium.
76
+ - **launchd template** renamed to `scripts/io.github.huynle.hub.plist` (label `io.github.huynle.hub`).
77
+
78
+ Also in this release (listed as 0.5.1, unreleased, before):
79
+
80
+ - **New sessions, composer first.** `/new` is a big "Describe a task or ask a question" composer (with
81
+ images) and a row under it: **Environment** (your machines; online first with process and OpenCode
82
+ version, offline ones disabled; ✓ on the choice), **Project** (searchable Herdr workspaces and known
83
+ folders), the folder's **branch**, and **Open in Herdr tab**. Submitting creates the session on that
84
+ machine and sends the prompt as its first turn; the last environment and folder are remembered per
85
+ device. The session rail's **Server** card is now a collapsible **Environment** card (machine and
86
+ status, project, branch, where it is open), and every rail card collapses from its header (remembered),
87
+ with MCP shown as **Connectors · N connected**.
88
+ - **`/hub` and `/voice` are separate commands.** `/hub` configures the hub connection: server URL and
89
+ connection state, change the server (reachability checked) or use a hub on this computer, start the
90
+ local hub, approval status, mode, this session on/off, pair a phone, open the hub in the browser.
91
+ `/voice` has no menu: it sets up and connects first when needed, shares the session for Voice and opens
92
+ it in the browser with Voice mode starting (`/s/<id>?voice=1` through a one-time sign-in link; the hub's
93
+ `openLink` op takes `voice: true` and `/login` accepts `?voice=1`). When the browser needs a tap before
94
+ it plays or records, the Voice bar shows **Tap to start Voice mode** instead of an error. "Talk on this
95
+ device" is gone from `/hub` (use `/voice`).
96
+ - **"Voice mode on" notification.** While Voice mode is on, a silent persistent notification says which
97
+ session and what Voice is doing ("API server · listening", updated at most every 3 s) with **End** (ends
98
+ Voice mode through the page, or with no page open turns conversation mode off at the hub) and **Open**;
99
+ it closes when Voice mode ends or the page unloads. The lock screen's media controls show "Voice mode on"
100
+ and the session title, and pause/stop end Voice mode (before, pause muted listening). Without
101
+ notification permission the Voice bar offers **Allow notifications**.
102
+ - **Notifications that arrive.** Settings → Notifications shows the real state of this device (permission,
103
+ service worker, and whether this hub holds its subscription) and warns when nothing would arrive; **Enable
104
+ notifications on this device** registers the worker and subscribes; the app re-subscribes by itself when
105
+ permission is granted but the hub lost the subscription (e.g. after the hub moved to a new address), and
106
+ the service worker follows `pushsubscriptionchange`. **Send test notification** (`POST /api/push/test`,
107
+ only the caller's device, at most one per 10 s) reports where it went or the push service's status.
108
+ Preferences can't be mistaken for working notifications any more: they say they apply once subscribed.
109
+ - **Personal notifications.** Separate toggles for questions, permissions, failed runs (split from
110
+ finished), finished runs, hand-offs and Voice replies (0.5.0 prefs are migrated: blocked → question +
111
+ permission, finished → finished + failed); quiet hours per device in its time zone; **Mute
112
+ notifications** per session (⋯ menu, per user, `PUT /api/push/mute`). Questions, permissions and failures
113
+ go out with high urgency and a one-hour TTL, stay on screen, vibrate and re-alert per session and kind; a
114
+ permission shows what it is (fetched by the service worker, never in the payload) with **Allow once** /
115
+ **Reject**.
116
+ - **Image attachments**: the composer's **+** button attaches images (file picker, which offers the camera
117
+ on phones), as do paste and drag-and-drop; up to 4 per message, shown as removable thumbnails, and a
118
+ message may be images only. The browser re-encodes each image before sending (longest side at most
119
+ 1600 px, JPEG with stepped-down quality, PNG kept for small transparent images; GIFs become their first
120
+ frame; camera metadata such as location is dropped) so each stays under ~180 KB and all under ~700 KB of
121
+ base64. `POST /api/sessions/:id/turn` takes `images: [{mime, data, name?}]` (validated by the hub and
122
+ the spoke: at most 4, PNG/JPEG/WebP/GIF whose bytes match, ≤ 512 KB each, ≤ 750,000 base64 characters
123
+ together) and the spoke passes them to OpenCode as inline (data URL) file attachments, for turns and
124
+ steers alike. Utterances (Voice mode) stay text-only; a message with images sent while Voice mode is on
125
+ goes as a typed turn.
126
+ - **Images in the transcript**: user messages show their image attachments as thumbnails, loaded on
127
+ demand from the owner-scoped `GET /api/sessions/:id/timeline/messages/:message/files/:index`; other
128
+ attached files show as named chips.
129
+ - **Fix: thoughts always expand.** "Thought for Ns" opens even when the provider stored no readable
130
+ reasoning (Claude usually keeps only an encrypted signature) and then says so; live reasoning deltas go
131
+ into the thought of the same message instead of a second, empty row when a session is opened mid-run.
132
+ - **Fix: speech model lists of OpenAI-compatible gateways.** `GET /api/settings/speech/models` answers
133
+ `{models, note?}`: a gateway's full `/models` list is narrowed to speech-to-text or text-to-speech models
134
+ (never chat, image, embedding, rerank or music models), the configured model is always offered first, and
135
+ when nothing is labeled every model shows with a note. The pickers group models by provider.
136
+ - **Fix: Voice and Worker model menus.** The spoke listed models by their upstream `modelID` instead of
137
+ OpenCode's catalog `id`, so models of gateway providers with aliases (e.g. `claude-opus-5-5` sent as
138
+ `claude/claude-opus-5-5`) could not be selected or switched to. The menus now handle catalogs of hundreds
139
+ of models (at most 100 rows rendered with a count, arrows + Enter, number keys only outside the search box so
140
+ digits can be typed), have a Back button in the variant step, never hide every model when a catalog
141
+ reports them all disabled, and report a failed save.
142
+ - **Fix: dictation and Voice mode stay with their session.** Stopping Voice mode while it was still starting
143
+ (waiting for the microphone or the hub), or leaving the session meanwhile, left a hidden conversation
144
+ listening that sent what the user said, including dictation in another session, to the first session. A
145
+ start that is stopped now releases the microphone and turns conversation mode off again. Dictation is
146
+ app-level: the text goes into the draft of the session where recording started (never sent), leaving that
147
+ session while recording cancels it (a notice says so), and each session keeps its own unsent draft.
148
+ - **Voice mode is one app-level toggle.** The Voice button turns Voice mode on for the session (turning
149
+ Voice on first when needed) and off again, and shows a pressed state while on. Only one session is in Voice
150
+ mode at a time: turning it on in another session first turns the current one off (conversation mode off,
151
+ microphone released, playback stopped). Voice mode keeps running while other sessions are viewed; they show
152
+ a "Voice: <session>" pill (a link back, with End) and the sidebar marks the session. A failure to turn it on
153
+ leaves Voice mode off and shows the error in that session. Dictation mutes Voice mode while it records.
154
+
155
+ ## 0.5.0
156
+
157
+ **opencode-voice is now OpenCode Hub** (`@huynle/opencode-hub`, `github:huynle/opencode-hub`): a remote
158
+ console for your OpenCode sessions, with Voice as an optional feature instead of the access boundary.
159
+ Existing installs keep working; see [Migrating from opencode-voice](README.md#migrating-from-opencode-voice).
160
+
161
+ - **Rename**: npm `@huynle/opencode-hub`, command `opencode-hub` (alias `opencode-voice-hub`), binaries
162
+ `opencode-hub-<os>-<arch>`, image `ghcr.io/huynle/opencode-hub`, plugin ids `opencode-hub` /
163
+ `opencode-hub.tui`, TUI command **`/hub`** (`/voice` kept as an alias), RPC id `hub` (the server also
164
+ registers the legacy `voice` id), log prefix `[opencode-hub]`, prompt metadata `source: "opencode-hub"`
165
+ (readers accept `"opencode-voice"`). The `voice` agent keeps its name.
166
+ - **Environment**: `HUB_*` variables (`HUB_HOST`, `HUB_PORT`, `HUB_PUBLIC_URL`, `HUB_STATE_DIR`,
167
+ `HUB_CONFIG`, `HUB_AUTH`, `HUB_OIDC_*`, `HUB_ADMIN_EMAILS`, `HUB_STT_MODEL`, `HUB_TTS_MODEL`,
168
+ `HUB_VOICE`, `HUB_MAX_*`, `HUB_TURN_TIMEOUT_MS`, `HUB_VAPID_SUBJECT`) and `OPENCODE_HUB_CONFIG`. The
169
+ legacy `VOICE_*` / `OPENCODE_VOICE_CONFIG` names are still read as fallbacks (the new name wins) with a
170
+ one-time deprecation warning each. The Docker image now sets `HUB_HOST`/`HUB_PORT`/`HUB_STATE_DIR`:
171
+ container overrides of `VOICE_HOST`/`VOICE_PORT`/`VOICE_STATE_DIR` must be renamed.
172
+ - **Config and state**: `~/.config/opencode-hub/config.json`, `~/.config/opencode-hub/hub.json` and
173
+ `~/.local/state/opencode-hub/`. On first start the legacy `opencode-voice` files are copied there
174
+ (modes kept, legacy files never deleted, logged once); explicit paths are used as they are.
175
+ - **Remote console**: every session a connected process reports is inspectable and controllable without
176
+ Voice; Voice gates only conversation mode and utterances. `POST /api/sessions/:id/turn` steers while the
177
+ session is busy (`{mode: "turn" | "steer"}`), `POST /api/sessions/:id/stop` stops the whole tree
178
+ (descendants first), `GET …/summary` (context usage, todos, MCP, branch, change stats) and
179
+ `GET …/diff?file=` (per-file patch, on demand), and per-user read marks (`POST …/read`, `GET /api/reads`).
180
+ - **Limits**: typed prompts/steers are capped at 100,000 characters (400); diffs at 400 KB of JSON and the
181
+ summary at 500 changed files (`totalFiles`, `truncated`) and 100 MCP servers, so no reply outgrows a
182
+ protocol frame; a spoke answers an oversize result with an error instead of dropping the connection.
183
+ Stop and interrupt release the hub's turn lock (a waiting turn answers `interrupted`). The busy message
184
+ is now "A turn is already running for this session". Reply links refuse backslashes and control
185
+ characters.
186
+ - **Releases** publish to npm through trusted publishing (OIDC) instead of an `NPM_TOKEN`.
187
+ - **Legacy package**: `packages/opencode-voice-shim/` is a final `@huynle/opencode-voice` 0.5.0 that
188
+ depends on and re-exports `@huynle/opencode-hub` and prints a one-line rename notice. It is published by
189
+ hand: after `@huynle/opencode-hub@0.5.0` is on npm, `cd packages/opencode-voice-shim && npm publish
190
+ --access public`, then `npm deprecate @huynle/opencode-voice@"<0.5.0" "Renamed to @huynle/opencode-hub"`.
191
+ - The phone now has separate per-session Voice and Worker model selectors, including model variants.
192
+ Changes apply to the next turn or task; machine configuration remains the fallback.
193
+ - Thinking, tools and subagents now appear inline with the conversation in compact expandable rows inspired
194
+ by OpenCode. Reopening loads ordered history from OpenCode, and complete tool input/output is fetched only
195
+ when expanded instead of being stored in the hub.
196
+ - Conversation mode handles quick answers and up to two low-risk tools directly, uses the fast voice
197
+ model for routine workers and the configured work model for complex work, and reuses workers only for
198
+ follow-ups on the same active task.
199
+ - Successful worker results can be spoken directly from child completion events, removing the extra
200
+ voice-agent summarization turn while filtering failed, interrupted and stale results.
201
+
202
+ ## 0.4.0
203
+
204
+ Open sessions first: the phone now shows what is open in your terminals, like Herdr's sidebar, instead
205
+ of every stored session.
206
+
207
+ - **Open tab (default)**: sessions open in a terminal right now, grouped by Herdr workspace (or project
208
+ folder), with working / idle / blocked dots and "Needs you" on top. "Open" comes from Herdr (`herdr
209
+ agent list`, when installed) or from the OpenCode TUI itself (the plugin's TUI part reports the session
210
+ and tabs it shows), so it works without Herdr too.
211
+ - **Recent tab**: the history window per machine — last 30 days, at most 100 sessions (config
212
+ `list: {days, max}`); open, running, blocked, conversation and voice-enabled sessions are always
213
+ included. Search and filters as before.
214
+ - **Hide and pin** sessions and workspaces, synced across your devices (per user); a **Hidden** tab to
215
+ restore them.
216
+ - **New session from the phone** in a Herdr workspace or folder; with Herdr it opens in a new tab of that
217
+ workspace (`opencode --session <id>`), so it appears in your terminal too.
218
+ - CI runs each test file in its own process (works around an intermittent Bun 1.3.5 hang on Linux).
219
+
220
+ ## 0.3.0
221
+
222
+ One hub, several people. Each person signs in and sees only their own machines, sessions, phones and
223
+ speech settings. Without sign-in configured the hub is the single-owner hub it has always been.
224
+
225
+ - **Sign in with Google** (any OpenID Connect provider): `VOICE_AUTH=oidc` with a client ID/secret, an
226
+ allow-list of emails or `@domains`, and admin emails. Authorization code flow with PKCE, state and nonce.
227
+ Pairing codes and one-time login links keep working and belong to the machine's owner.
228
+ - **Ownership and isolation**: machines belong to whoever approves them, phones to whoever pairs them.
229
+ Every API route declares its access level in one table; other people's sessions and machines answer
230
+ 404, and SSE and push only carry your own. A dedicated tenancy test suite covers every route.
231
+ - **Admin**: manage users (roles, disable), all machines and devices, pending approvals, and an audit
232
+ log. Admins don't see other people's sessions.
233
+ - **Per-user speech settings** with a server default set by an admin; your keys are never shown to anyone
234
+ else.
235
+ - **Migration**: existing machines and phones become the first admin's when sign-in is turned on.
236
+ - Unknown session IDs now return 404 (was 409) so IDs don't leak.
237
+
238
+ ## 0.2.0
239
+
240
+ Easier to run, for people with their own voice server and for people who just want it on their computer.
241
+
242
+ - **First-run setup in `/voice`**: "On this computer" starts a local hub, connects this machine, shares
243
+ sessions and opens the voice page; "Connect to a voice server…" checks the server, opens its approve page
244
+ and asks which sessions to share once approved. A server already in the config is offered first.
245
+ `/voice` → **Open voice UI** opens the page (starting the local hub if needed); **Server → Set up again…**
246
+ switches between local and a server.
247
+ - **Zero-install local hub**: without Bun, the plugin runs the hub with OpenCode's own runtime
248
+ (`BUN_BE_BUN=1`).
249
+ - **Speech onboarding on the page**: a "Set up speech" card until a provider is usable, the mic leads to
250
+ speech settings instead of failing, and a "Test voice" step after saving. Sign-in links land on speech
251
+ setup while it is missing. `GET /api/settings/speech` reports `ready: {stt, tts, reason?}`.
252
+ - **Standalone hub binaries** for macOS, Linux and Windows (runtime and web page embedded) with
253
+ `SHA256SUMS`, built by `scripts/build-binaries.sh` and attached to each GitHub release.
254
+ - **Docker image** `ghcr.io/huynle/opencode-voice-hub` for linux/amd64 and linux/arm64.
255
+ - The hub answers `--help` and `--version`. With `VOICE_HOST=0.0.0.0` (Docker) and no `VOICE_PUBLIC_URL`,
256
+ links default to `http://127.0.0.1:<port>`.
257
+ - Clearer pairing page: where codes come from (`/voice` → Pair a phone, or the hub's log for the first
258
+ device).
259
+
260
+ ## 0.1.0
261
+
262
+ First release.
263
+
264
+ - OpenCode plugin ("spoke") that connects every OpenCode process to a voice hub, with `/voice` to connect,
265
+ share sessions, pair phones and open sessions in the browser.
266
+ - Voice hub with a dependency-free PWA: session browser, tap-to-talk and hands-free conversation mode,
267
+ live activity, push notifications, speech via OpenRouter or OpenAI-compatible providers.
268
+ - Voice agent that answers in short spoken sentences and delegates work to subagents.
269
+ - Local hub auto-start for loopback servers; Dockerfile for self-hosting.
package/Dockerfile ADDED
@@ -0,0 +1,51 @@
1
+ # Hub server. Runtime: Bun, the sources, and the hub's npm dependencies
2
+ # (web-push for notifications) at the versions pinned in package-lock.json. Published as
3
+ # ghcr.io/huynle/hub (see .github/workflows/release.yml; formerly opencode-hub and opencode-voice-hub).
4
+ # docker build -t hub .
5
+ # docker run -p 127.0.0.1:4097:4097 -v hub-data:/data -e HUB_PUBLIC_URL=https://hub.example.com \
6
+ # -e OPENROUTER_API_KEY=... hub
7
+ # Legacy VOICE_* variables still work, except that the HUB_HOST/HUB_PORT/HUB_STATE_DIR defaults below
8
+ # win over VOICE_HOST/VOICE_PORT/VOICE_STATE_DIR: rename those overrides to HUB_*.
9
+ # The console (web/, Solid + Vite) is built in its own stage; the image ships only web/dist.
10
+ FROM node:22-slim AS web
11
+ WORKDIR /build
12
+ COPY package.json package-lock.json ./
13
+ RUN npm ci --no-audit --no-fund --ignore-scripts
14
+ COPY web ./web
15
+ RUN npx vite build --config web/vite.config.ts
16
+
17
+ FROM oven/bun:1.3-slim AS deps
18
+ WORKDIR /build
19
+ COPY package.json package-lock.json ./
20
+ # Bun migrates package-lock.json; --frozen-lockfile keeps the pinned versions.
21
+ RUN bun install --production --frozen-lockfile --ignore-scripts
22
+ COPY docker/hub-deps.ts ./
23
+ # Keep only what the hub loads at runtime (the @opencode/* packages are for the plugin).
24
+ RUN bun hub-deps.ts /deps web-push
25
+
26
+ FROM oven/bun:1.3-slim
27
+ LABEL org.opencontainers.image.title="hub" \
28
+ org.opencontainers.image.description="Hub: a central hub for AI agent harnesses (OpenCode first): remote console, pairing, push and optional Voice" \
29
+ org.opencontainers.image.licenses="MIT" \
30
+ org.opencontainers.image.source="https://github.com/huynle/hub"
31
+ WORKDIR /app
32
+ COPY --from=deps /deps/node_modules ./node_modules
33
+ COPY package.json ./
34
+ COPY bin ./bin
35
+ COPY src ./src
36
+ COPY --from=web /build/web/dist ./web/dist
37
+
38
+ ENV HUB_HOST=0.0.0.0 \
39
+ HUB_PORT=4097 \
40
+ HUB_STATE_DIR=/data \
41
+ NODE_ENV=production
42
+
43
+ RUN mkdir -p /data && chown bun:bun /data
44
+ VOLUME /data
45
+ USER bun
46
+ EXPOSE 4097
47
+
48
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
49
+ CMD ["bun", "-e", "fetch('http://127.0.0.1:' + (process.env.HUB_PORT || process.env.VOICE_PORT || 4097) + '/healthz').then(r => process.exit(r.ok ? 0 : 1), () => process.exit(1))"]
50
+
51
+ CMD ["bun", "bin/hub.ts"]
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Huy Le
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.