@huynle/hub 0.0.0-stage → 0.6.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/CHANGELOG.md +269 -0
- package/Dockerfile +51 -0
- package/LICENSE +21 -0
- package/README.md +1029 -2
- package/bin/hub.ts +20 -0
- package/docker/hub-deps.ts +42 -0
- package/package.json +85 -4
- package/rpc.ts +2 -0
- package/scripts/hub-local.sh +28 -0
- package/scripts/io.github.huynle.hub.plist +42 -0
- package/server.ts +3 -0
- package/src/central/assets.ts +74 -0
- package/src/central/auth.ts +271 -0
- package/src/central/config.ts +310 -0
- package/src/central/http.ts +1247 -0
- package/src/central/main.ts +136 -0
- package/src/central/oidc.ts +295 -0
- package/src/central/push-target.ts +97 -0
- package/src/central/push.ts +285 -0
- package/src/central/registry.ts +1153 -0
- package/src/central/speech/models.ts +89 -0
- package/src/central/speech/openai.ts +66 -0
- package/src/central/speech/openrouter.ts +71 -0
- package/src/central/speech/provider.ts +74 -0
- package/src/central/speech/service.ts +168 -0
- package/src/central/speech/settings.ts +375 -0
- package/src/central/spoke-ws.ts +300 -0
- package/src/central/store.ts +653 -0
- package/src/config.ts +170 -0
- package/src/env.ts +63 -0
- package/src/index.ts +236 -0
- package/src/key.ts +17 -0
- package/src/loopback.ts +41 -0
- package/src/migrate.ts +105 -0
- package/src/openrouter.ts +3 -0
- package/src/options.ts +33 -0
- package/src/protocol.ts +650 -0
- package/src/reply.ts +27 -0
- package/src/rpc.ts +98 -0
- package/src/setup.ts +124 -0
- package/src/speakable.ts +35 -0
- package/src/spoke/client.ts +272 -0
- package/src/spoke/conversation.ts +93 -0
- package/src/spoke/enroll.ts +79 -0
- package/src/spoke/herdr.ts +346 -0
- package/src/spoke/listing.ts +108 -0
- package/src/spoke/localhub.ts +287 -0
- package/src/spoke/locations.ts +34 -0
- package/src/spoke/own-server.ts +32 -0
- package/src/spoke/presence.ts +45 -0
- package/src/spoke/singleton.ts +103 -0
- package/src/spoke/spoke.ts +1212 -0
- package/src/spoke/summary.ts +117 -0
- package/src/spoke/timeline.ts +93 -0
- package/src/spoke/tracker.ts +345 -0
- package/src/spoke/watch.ts +78 -0
- package/src/status.ts +232 -0
- package/src/tui-commands.ts +53 -0
- package/src/tui-forms.ts +77 -0
- package/src/tui-presence.ts +75 -0
- package/src/tui.ts +479 -0
- package/src/turns.ts +94 -0
- package/src/voice-command.ts +65 -0
- package/tui.ts +2 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/index-DClBvfLL.css +1 -0
- package/web/dist/assets/index-Djo_yF0a.js +12 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/icon-192.png +0 -0
- package/web/dist/icon-512.png +0 -0
- package/web/dist/icon-maskable-512.png +0 -0
- package/web/dist/icon.svg +18 -0
- package/web/dist/index.html +25 -0
- package/web/dist/manifest.webmanifest +18 -0
- package/web/dist/sw.js +151 -0
- package/web/dist/vad-worklet.js +27 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
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.
|