@huynle/hub 0.9.2 → 0.10.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 +296 -0
- package/Dockerfile +5 -1
- package/README.md +152 -77
- package/package.json +7 -4
- package/packages/claude-code/package.json +1 -1
- package/packages/cli/dist/hub-cli.js +532 -261
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/cli.ts +36 -1
- package/packages/cli/src/subscriptions.ts +49 -0
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/sessions.ts +10 -6
- package/packages/codex/src/spoke.ts +2 -1
- package/packages/node/package.json +1 -1
- package/packages/preset/package.json +1 -1
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/schema/app-delivery/v1.schema.json +7 -5
- package/packages/protocol/schema/app-front/v1.schema.json +410 -0
- package/packages/protocol/src/app-front.ts +293 -0
- package/packages/protocol/src/concierge.ts +10 -1
- package/packages/protocol/src/context-cards.ts +59 -0
- package/packages/protocol/src/credentials.ts +39 -2
- package/packages/protocol/src/index.ts +69 -11
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/app-front.ts +524 -0
- package/packages/sdk/src/client.ts +37 -4
- package/packages/sdk/src/envelope.ts +9 -2
- package/packages/sdk/src/errors.ts +1 -0
- package/packages/sdk/src/index.ts +1 -0
- package/packages/sdk/src/types.ts +39 -1
- package/packages/spoke-sdk/package.json +1 -1
- package/packages/spoke-sdk/src/delivery.ts +17 -1
- package/packages/spoke-sdk/src/harness-spoke.ts +4 -3
- package/packages/voice-client/README.md +146 -0
- package/packages/voice-client/src/vad/LICENSE +21 -0
- package/src/central/agent/acting-routes.ts +17 -3
- package/src/central/agent/acting.ts +12 -3
- package/src/central/agent/actor.ts +6 -1
- package/src/central/agent/app-decisions.ts +146 -0
- package/src/central/agent/app-tokens.ts +9 -3
- package/src/central/agent/audit.ts +3 -0
- package/src/central/agent/connect-routes.ts +64 -0
- package/src/central/agent/connect.ts +868 -0
- package/src/central/agent/conversation.ts +625 -0
- package/src/central/agent/decisions.ts +71 -6
- package/src/central/agent/delivery.ts +3 -0
- package/src/central/agent/directory.ts +5 -0
- package/src/central/agent/errors.ts +2 -0
- package/src/central/agent/grant-requests.ts +46 -9
- package/src/central/agent/grants.ts +13 -2
- package/src/central/agent/message-limits.ts +2 -2
- package/src/central/agent/message-routes.ts +13 -4
- package/src/central/agent/read-routes.ts +16 -2
- package/src/central/agent/routes.ts +108 -6
- package/src/central/agent/speech-routes.ts +316 -0
- package/src/central/agent/subscription-routes.ts +174 -0
- package/src/central/agent/subscriptions.ts +980 -0
- package/src/central/agent-data.ts +43 -1
- package/src/central/backend/memory-counters.ts +2 -2
- package/src/central/backend/types.ts +5 -2
- package/src/central/backend/valkey-counters.ts +5 -4
- package/src/central/client-address.ts +16 -0
- package/src/central/concierge/board-render.ts +52 -1
- package/src/central/concierge/board-routes.ts +25 -2
- package/src/central/concierge/board.ts +11 -1
- package/src/central/concierge/context.ts +29 -8
- package/src/central/concierge/grant-request.ts +216 -0
- package/src/central/concierge/prompt.ts +49 -2
- package/src/central/concierge/session-routes.ts +3 -0
- package/src/central/config.ts +64 -3
- package/src/central/context-cards/budget.ts +121 -0
- package/src/central/context-cards/changes.ts +29 -0
- package/src/central/context-cards/config.ts +145 -0
- package/src/central/context-cards/engine.ts +379 -0
- package/src/central/context-cards/inputs.ts +111 -0
- package/src/central/context-cards/pickup.ts +134 -0
- package/src/central/context-cards/registry.ts +89 -0
- package/src/central/context-cards/routes.ts +37 -0
- package/src/central/context-cards/service.ts +367 -0
- package/src/central/context-cards/store.ts +67 -0
- package/src/central/context-cards/types.ts +88 -0
- package/src/central/fetcher.ts +10 -2
- package/src/central/front-relays.ts +423 -0
- package/src/central/http.ts +516 -56
- package/src/central/human-activity.ts +453 -0
- package/src/central/llm/chat.ts +133 -0
- package/src/central/registry.ts +162 -9
- package/src/central/speech/service.ts +9 -0
- package/src/central/spoke-ws.ts +13 -0
- package/src/central/store.ts +9 -12
- package/src/config.ts +30 -13
- package/src/index.ts +45 -43
- package/src/rpc.ts +33 -4
- package/src/spoke/concierge-agent.ts +1 -1
- package/src/spoke/conversation.ts +6 -75
- package/src/spoke/deliver.ts +18 -8
- package/src/spoke/hub-rpc.ts +7 -2
- package/src/spoke/hub-tool-subscriptions.ts +82 -0
- package/src/spoke/hub-tool-title.ts +89 -0
- package/src/spoke/hub-tool.ts +56 -18
- package/src/spoke/session-context.ts +10 -2
- package/src/spoke/spoke.ts +209 -238
- package/src/spoke/summary.ts +36 -1
- package/src/spoke/taskboard.ts +5 -6
- package/src/spoke/voice-front.ts +323 -30
- package/src/spoke/who-teams.ts +51 -2
- package/src/tui-messaging.ts +24 -10
- package/src/tui.ts +6 -6
- package/web/dist/assets/{ArtifactPage-BKISN-Qj.js → ArtifactPage-Bz992ZX6.js} +1 -1
- package/web/dist/assets/{ArtifactPane-CKOiAUNH.js → ArtifactPane-BJZNixJC.js} +1 -1
- package/web/dist/assets/ArtifactWorkbench-Dti3VP07.js +6 -0
- package/web/dist/assets/{Diagram-vdpmp3xu.css → Diagram-7y8kg3og.css} +1 -1
- package/web/dist/assets/Diagram-B4HdUxw0.js +1 -0
- package/web/dist/assets/{Library-CcjZXsfF.js → Library-CpzD2HDW.js} +2 -2
- package/web/dist/assets/Lightbox-BlbivCkd.js +1 -0
- package/web/dist/assets/MediaImage-Ch3ryIdR.js +4 -0
- package/web/dist/assets/ProfilesDestination-DlQeogea.js +1 -0
- package/web/dist/assets/{TeamsPage-CDyjrK-I.js → TeamsPage-4xdWCaI-.js} +1 -1
- package/web/dist/assets/{Watchtower-DYwoUWME.js → Watchtower-CpQdIRns.js} +2 -2
- package/web/dist/assets/{artifacts-DU9uRgs1.js → artifacts-But87Jfo.js} +1 -1
- package/web/dist/assets/index-BC0O1gfR.css +1 -0
- package/web/dist/assets/index-BS6lA6fN.js +33 -0
- package/web/dist/assets/{live-ycZ_aQZO.js → live-CVUFIdUX.js} +2 -2
- package/web/dist/assets/{pane-layout-0RrLHGDr.js → pane-layout-BW0RdX5a.js} +1 -1
- package/web/dist/assets/{renderer2d-BIwbgRoX.js → renderer2d-IaJiloPi.js} +1 -1
- package/web/dist/assets/{renderer3d-C1a7eYGQ.js → renderer3d-ZiD1uKGW.js} +1 -1
- package/web/dist/assets/{safe-area-DHhPy61Q.js → safe-area-C7IAlkgY.js} +1 -1
- package/web/dist/assets/{source-BWlvaODa.js → source-C-De8L3s.js} +1 -1
- package/web/dist/assets/toast-CXTwTvDw.js +9 -0
- package/web/dist/assets/vad-worklet-BfxJhrXy.js +27 -0
- package/web/dist/index.html +6 -6
- package/web/dist/assets/ArtifactWorkbench-d7R1o-rs.js +0 -6
- package/web/dist/assets/Diagram-azw2B_QH.js +0 -1
- package/web/dist/assets/Lightbox-Du56MXGX.js +0 -1
- package/web/dist/assets/MediaImage-Ds_JUx3a.js +0 -4
- package/web/dist/assets/ProfilesDestination-DdzbzmeK.js +0 -1
- package/web/dist/assets/index-C3ci5UtY.css +0 -1
- package/web/dist/assets/index-LNMZCqgE.js +0 -36
- package/web/dist/assets/toast-Cuu2K0hx.js +0 -1
- /package/{web/src/voice → packages/voice-client/src}/turn/LICENSE +0 -0
package/README.md
CHANGED
|
@@ -41,8 +41,10 @@ Formerly **opencode-voice** and, briefly, **opencode-hub**
|
|
|
41
41
|
- **Running detection**: a session counts as working while OpenCode reports it busy or retrying, or while a
|
|
42
42
|
shell or terminal it started still runs (e.g. a background `sleep 60 &` or build script), also when it was
|
|
43
43
|
already running before the plugin started.
|
|
44
|
-
- **Voice** (optional feature, `/voice`): dictation and a hands-free conversation mode
|
|
45
|
-
|
|
44
|
+
- **Voice** (optional feature, `/voice`): dictation and a hands-free conversation mode. Voice mode always talks
|
|
45
|
+
through a **voice front**: a hidden, fast voice agent that says what it understood and relays your request to
|
|
46
|
+
the session, which keeps its own agent, model and tools (Settings → Voice → Voice front; on by default, off
|
|
47
|
+
talks to the session directly).
|
|
46
48
|
Bring your own speech: OpenRouter or any OpenAI-compatible STT/TTS service (including self-hosted).
|
|
47
49
|
A developer **recording mode** (Settings → Voice, off by default) keeps your voice segments, transcripts,
|
|
48
50
|
replies and timing on the hub for 7 days, private to you, with Export and Delete
|
|
@@ -58,6 +60,9 @@ Formerly **opencode-voice** and, briefly, **opencode-hub**
|
|
|
58
60
|
- **Agent messaging**: sessions, scripts and you message each other through the hub (`hub msg @reviewer …`,
|
|
59
61
|
`hub ask`, the OpenCode `hub` tool, `/msg`, `hub mcp`); asks to `@me` become Decisions you answer from the
|
|
60
62
|
console ([Agent messaging](#agent-messaging)).
|
|
63
|
+
- **Session subscriptions**: a session follows others (`hub subscribe @builder --type state:idle`, a role, a
|
|
64
|
+
`#team` or its subagents) and gets one batched hub message per window to react to, waking it when idle, with
|
|
65
|
+
loop protection and rate limits ([docs/subscriptions.md](docs/subscriptions.md)).
|
|
61
66
|
- **Launches**: you, a script or an app start a session on one of your machines (`hub launch --branch feat/x
|
|
62
67
|
--prompt "…"`): a `hub node` daemon prepares a git worktree, the session gets its name, roles, labels and first
|
|
63
68
|
prompt, and ends by itself when it is done, with signed webhooks for apps ([docs/launches.md](docs/launches.md)).
|
|
@@ -148,7 +153,7 @@ Then, in OpenCode:
|
|
|
148
153
|
|---|---|
|
|
149
154
|
| **`/hub`** | Configures this harness's hub connection: the server URL, connect and approve (enroll) this machine, which sessions to share (mode), pair a phone, and open the hub in the browser. The first time it offers **Set up Hub** with two paths (below). |
|
|
150
155
|
| **`/voice`** | The Voice feature: talks to the session on screen. It connects (running the setup first when needed), shares the session for Voice and opens it in the browser with Voice mode starting. |
|
|
151
|
-
| **`/who [project \| @@role]`** | Lists the hub
|
|
156
|
+
| **`/who [project \| #team \| @@role]`** | Lists the sessions the hub shows the session on screen: every online one (named or not; an unnamed one by its ID and title) and the named or role-holding ones while offline, with project, state, online and roles. A `You: @name · project` line heads the dialog (`You: ses_… (unnamed…)` until it has a name: ask the agent to name itself, or run `hub name @name --session ses_…` in a logged-in terminal; a `!` command gets no session token), its own row is marked `(you)` and listed first, then online before offline. |
|
|
152
157
|
| **`/msg @name text`** | Sends the text word for word to another session (`@name`, `@@role`, `@name.project`, `ses_…`, or `@me` for you) as the session on screen. Needs a shared, connected session. |
|
|
153
158
|
|
|
154
159
|
### Local: everything on this computer
|
|
@@ -181,9 +186,12 @@ Pre-configured machines: with `"server": "https://hub.example.com"` in
|
|
|
181
186
|
`~/.config/hub/config.json`, `/hub` leads with **Connect to hub.example.com**.
|
|
182
187
|
`/hub` → **Server** → **Set up again…** switches between local and a hosted hub later.
|
|
183
188
|
|
|
184
|
-
|
|
185
|
-
`"voiceModel": "anthropic/claude-haiku-4-5"` (any `provider/model` you have
|
|
186
|
-
|
|
189
|
+
Recommended for Voice mode: put the voice front on a fast, paid model in `~/.config/hub/config.json`:
|
|
190
|
+
`"voiceModel": "anthropic/claude-haiku-4-5"` (any `provider/model` you have; a session's Voice model in the
|
|
191
|
+
console wins). Claude Haiku 4.5 measured fastest (0.7–0.8 s to the first token through OmniRoute), with Haiku 5.5
|
|
192
|
+
as the alternative. The front never thinks: the hub turns its model's thinking off, whatever the model or variant.
|
|
193
|
+
Avoid free models: when one runs out of quota the front fails and Voice mode talks to the session directly. The
|
|
194
|
+
session itself keeps its own agent and model.
|
|
187
195
|
|
|
188
196
|
## Set up a machine (one line)
|
|
189
197
|
|
|
@@ -360,7 +368,8 @@ foreground with `hub.json`; `scripts/io.github.huynle.hub.plist` is a launchd te
|
|
|
360
368
|
**Security:** the hub can drive your OpenCode sessions, which can run tools. Every API call needs a
|
|
361
369
|
paired device or a signed-in user; spokes need machine tokens; nothing trusts the client IP. For a public
|
|
362
370
|
hostname, consider [sign-in with Google](#multi-user-sign-in-with-google) or an authenticating proxy in
|
|
363
|
-
front of the web app, and leave `/spoke/*`, `/auth/*`, `/healthz`, `/.well-known/hub`, `/
|
|
371
|
+
front of the web app, and leave `/spoke/*`, `/auth/*`, `/healthz`, `/.well-known/hub`, `/connect/*` (connected apps' token
|
|
372
|
+
endpoints), `/.well-known/oauth-authorization-server`, `/install.sh`,
|
|
364
373
|
`/install.ps1`, the manifest and icons reachable for spokes, sign-in, app discovery, [machine
|
|
365
374
|
setup](#set-up-a-machine-one-line) and the PWA (and `/mcp`, bearer-authenticated, if MCP clients
|
|
366
375
|
or gateways use [MCP over HTTP](#over-http-mcp)).
|
|
@@ -410,9 +419,14 @@ conversation mode and utterances (403 `not_exposed` for sessions not shared with
|
|
|
410
419
|
| `POST /api/sessions/:id/rename` | `{title}` → `{title}`: renames the session in OpenCode (`session.update`), through the process that owns it, else any other process that reports it. The title is trimmed and must be 1–200 characters (400 otherwise). Works with Voice off; the new title shows in every console at once (the spoke reports it before answering) |
|
|
411
420
|
| `POST /api/sessions/:id/fork {messageID}` · `GET …/revert/preview?messageID=` · `POST …/revert {messageID, files?}` · `POST …/revert/restore` · `POST …/revert/commit` | fork before one of the session's user messages (201 `{sessionID, text, attachments}`), or stage, undo and commit a revert from it (409 `busy` while it runs); console only, audited; needs a spoke announcing `fork`/`revert` (409 `unsupported` otherwise). Contract: [docs/session-view.md](docs/session-view.md#fork-and-revert-a-message) |
|
|
412
421
|
| `POST /api/sessions/:id/stop` | interrupts running descendants deepest first, then the root; idempotent. Also releases the hub's turn lock for the tree: a turn request still waiting answers `{outcome: "interrupted"}` and the next turn can start (`/interrupt` does the same) |
|
|
413
|
-
| `GET /api/sessions/:id/summary` | context usage, todos, MCP status (at most 100 servers), branch and working-tree change stats (each part optional). `changes` lists at most 500 files, most-changed first, with `totalFiles` and `truncated`; its `additions`/`deletions` cover every file |
|
|
422
|
+
| `GET /api/sessions/:id/summary` | context usage, the model the session runs on (`model`: chosen, else its latest reply's, else the default; with `source`), todos, MCP status (at most 100 servers), branch and working-tree change stats (each part optional). `changes` lists at most 500 files, most-changed first, with `totalFiles` and `truncated`; its `additions`/`deletions` cover every file |
|
|
423
|
+
| `GET /api/sessions/:id/context-cards` · `POST …/context-cards/:provider/refresh` | the session's [context cards](docs/context-cards.md) (`{cards, providers, enabled, configured}`), and running one provider now (`{force?}` → `{outcome: "generated", card}` · `{outcome: "skipped", reason}` · `{outcome: "failed"}`; 404 unknown provider; 429 with `Retry-After`: one per card per 30 s, 10 per user per minute). Live: SSE `{type: "contextCard", sessionID, provider, primary?, generatedAt}` (never the text) |
|
|
424
|
+
| `GET /api/context-cards/primary?sessions=a,b` | the primary context cards of up to 200 sessions (`{cards: {sessionID: card}}`): the session list's second line |
|
|
425
|
+
| `GET /api/settings/context-cards` · `PUT {enabled}` | your context card setting, whether the hub allows them and has a model (`{enabled, available, configured, model?, usage}`, never the key); off until you opt in |
|
|
414
426
|
| `GET /api/sessions/:id/diff?file=` | one file's patch, on demand (capped at 400 KB as JSON, then `truncated: true`); never stored in the hub |
|
|
415
|
-
| `POST /api/sessions/:id/read` · `GET /api/reads` | per-user read marks, synced to your other devices |
|
|
427
|
+
| `POST /api/sessions/:id/read {open?}` · `GET /api/reads` | per-user read marks, synced to your other devices; `open: true` (the console sends it when you open the session, not for its automatic marks) also counts as touching it |
|
|
428
|
+
| `GET /api/activity/mine` | per-user "touched by me" and the activity meter: `{activity: {sessionID: {at?, kind?, last?, buckets}}}`. `at` (ms) and `kind` (`prompt-tui`, `prompt-web`, `voice`, `answer`, `view`) say what the root session shows (your last action, else your last open), `last` your latest touch of any kind; `buckets` are `[start, yourActions, agentTurns, agentToolCalls]` per 30 minutes of the last 24 h. Changes stream as SSE `{type: "activity", sessionID, activity}`. Kept 30 days, at most 2000 sessions per user. See [docs/session-view.md](docs/session-view.md#finding-your-sessions) |
|
|
429
|
+
| `GET /api/intents/mine` | the stored intents of your sessions, `{intents: {sessionID: {text, source}}}` (never the title fallback): the second line under titles in the session list |
|
|
416
430
|
| `GET /api/events?lite=1&session=<id>&last=<id>` | the live stream (SSE). Every message has an `id:`; reconnecting with `Last-Event-ID` (EventSource does it) or `?last=` replays what was missed when the hub still has it (last 2000 events or 5 minutes per user; one fresh `catalog` stands for any catalog changes), else sends the catalog then `{type: "resync"}` (the page refetches what it shows). Ids are `<epoch>.<n>`; a hub restart starts a new epoch. `lite=1` leaves out `delta`/`reasoning` except of `session`'s tree (status, activity, catalog, turns and the rest still come). Split panes add `sessions=<id>,<id>` (at most 4 with `session`, else 400): each listed tree streams, each event once, and the stream starts with `{type: "scope", sessions}` naming the ones accepted (your own only); without `sessions` nothing changes. Heartbeat: `event: ping` every 20 s |
|
|
417
431
|
| `GET /api/push/prefs?endpoint=` | `{prefs, quietHours, activityMinutes, muted, subscriptions, subscribed?}`: this device's toggles (`handoff, question, permission, done, failed, turn, stopped, retry, subagentDone, subagentFailed, jobDone, jobFailed, longTool, say, activity`; `PUT` also accepts the legacy `finished` → `done` and `blocked` → question + permission), activity update interval and quiet hours, the user's muted sessions, how many subscriptions the hub holds for this device, and whether it holds `endpoint` |
|
|
418
432
|
| `PUT /api/push/prefs {toggles…, quietHours?, activityMinutes?}` | boolean toggles (a 0.5.0 `blocked` sets question and permission); `quietHours` `{start: "HH:MM", end: "HH:MM", timeZone}` or `null`; `activityMinutes` a whole number 1–60 (400 when invalid) |
|
|
@@ -451,9 +465,20 @@ the console shows it in the session's Details rail, with Edit (live: the console
|
|
|
451
465
|
Retention: every 6 h one hub drops the intents of deleted sessions and of sessions neither seen nor written for 30
|
|
452
466
|
days; an online session's intent is always kept. A dropped intent's revision starts over at 0.
|
|
453
467
|
|
|
468
|
+
**Context cards.** Beside the intent, the hub keeps short, automatically updated [context cards](docs/context-cards.md)
|
|
469
|
+
per session, from a registry of providers: `pickup` (primary; a light model the hub calls writes "pick up where you
|
|
470
|
+
left off": a ≤ 90-character headline and a short summary) and `changes` (the working tree, no model). They are **opt-in per
|
|
471
|
+
user** (off until you turn them on in Settings › Concierge; nothing is sent to a model before). Then they run on turn
|
|
472
|
+
end, when a session gets blocked, when you come back to it, on a 30-minute sweep and on Refresh, only for sessions you
|
|
473
|
+
touched in the last 24 h, throttled, skipped when nothing changed and capped per user per day.
|
|
474
|
+
`GET /api/agent/sessions/:addr/context-cards` answers `{sessionID, cards, providers}` (users, `sessions:read`,
|
|
475
|
+
session tokens, like the intent read). New cards are `session.contextCard {provider, version, primary}` events,
|
|
476
|
+
never the text; the Concierge board shows each root's primary headline (`pick up «…»`).
|
|
477
|
+
|
|
454
478
|
**Events.** `GET /api/agent/events?cursor=&wait=&limit=&type=&session=` reads a bounded per-user journal (24 h or
|
|
455
479
|
10,000 events) of compact events: `session.added/removed/changed`, `blocker.opened/closed`, `machine.changed`,
|
|
456
|
-
`message.changed`, `launch.changed`, `session.archived`, `session.named`, `session.intent`, `role.claimed`, `role.released
|
|
480
|
+
`message.changed`, `launch.changed`, `session.archived`, `session.named`, `session.intent`, `session.contextCard`, `role.claimed`, `role.released`,
|
|
481
|
+
`subscription.created/removed`. Each is `{id, at, type, subject, data}` with IDs and states
|
|
457
482
|
only: never prompts, message bodies, titles, blocker summaries, launch reasons or labels. Without a cursor the answer
|
|
458
483
|
is empty with a cursor at the latest event; pass the returned `cursor` back to get what came after. `wait` (0–60 s)
|
|
459
484
|
holds the request while nothing matches; `limit` is 1–200 (default 50); `type` (exact, or `launch.*`) and `session`
|
|
@@ -518,21 +543,28 @@ cookie and a hub bearer token (400).
|
|
|
518
543
|
| `GET /api/agent/status` | ✓ | ✓ | ✓ | machines → projects → sessions, each with name, roles, state, harness, activity and blockers, plus `attention` |
|
|
519
544
|
| `GET /api/agent/sessions?project=&state=&machine=&label=k=v&q=` | ✓ | ✓ | ✓ | the same sessions as a flat list; `q` (≤ 200 characters): every word must appear in the title or @name (case, `@` and punctuation ignored) |
|
|
520
545
|
| `GET /api/agent/sessions/:addr` | ✓ | ✓ | ✓ | one session with full blockers and the spoke's summary (`unsupported` when its harness lacks it) |
|
|
521
|
-
| `GET /api/agent/sessions/:addr/log?n=20` | ✓ | ✓ | ✓ | newest timeline rows (1–200); `422 unsupported {feature, harness}` when the spoke has no timeline, `503 offline` |
|
|
546
|
+
| `GET /api/agent/sessions/:addr/log?n=20` | ✓ | ✓ | ✓ (or `conversation:read` for [the app's sessions](docs/app-front.md#conversation-for-apps)) | newest timeline rows (1–200); `422 unsupported {feature, harness}` when the spoke has no timeline, `503 offline` |
|
|
522
547
|
| `GET /api/agent/watch?blocked=1&session=<addr>&cursor=` | ✓ | ✓ | ✓ (+ each family's scope) | SSE of the same compact events as `/api/agent/events` (see **Events**), after `ready`; each frame's `id:` is a cursor to resume with (`?cursor=` or `Last-Event-ID`); `reset` when it is no longer covered |
|
|
523
548
|
| `GET /api/agent/events?cursor=&wait=&limit=&type=&session=` | ✓ | ✓ | `events:subscribe` (+ each family's scope) | compact events after a resumable cursor (see **Events** above) |
|
|
524
549
|
| `GET /api/agent/who?project=&role=` | ✓ | ✓ | ✓ | named, role-holding and online sessions |
|
|
550
|
+
| `POST /api/agent/subscriptions {subscriber?, to, types?, delivery?, batch?, maxPerHour?, expires?, note?}` · `GET …/subscriptions?session=` · `GET`/`DELETE …/subscriptions/:id` | ✓ | itself only | `events:subscribe` + `sessions:read` (+ `messages:send` unless `delivery: "silent"`, + family scopes); its own | a session follows other sessions' events as batched hub messages ([docs/subscriptions.md](docs/subscriptions.md)) |
|
|
525
551
|
| `POST /api/agent/name {name, session?}` | ✓ | own session | `sessions:manage` | names a session (`409 name_taken`) |
|
|
526
552
|
| `POST /api/agent/roles/claim {role, exclusive?, project?, session?}` · `…/roles/release` | ✓ | own session | `sessions:manage` + `roles` grant | claims (with a new `generation`) or releases a role; never a reserved role for an app |
|
|
527
553
|
| `POST /api/agent/msg {to, text, wake?, urgent?, all?}`; `Idempotency-Key` | ✓ | ✓ | `messages:send` | tells a session (`all`: every holder of a role); never yourself |
|
|
528
554
|
| `POST /api/agent/ask {to, text, expiresIn?, wait?}`; `Idempotency-Key` | ✓ | ✓ | `messages:send` | an ask, open until answered, declined or expired; `wait` (≤ 60 s) returns the `answer` |
|
|
529
555
|
| `POST /api/agent/ask {to: "@me", text, options?, recommendation?, urgency?, tier?, deadline?, onTimeout?, about?, wait?}` | – | ✓ | `messages:send` | opens a **Decision** for the user (below): `{message, decision, answer?}` |
|
|
530
556
|
| `GET /api/agent/decisions?state=` · `…/decisions/:id` | ✓ (all) | own | `messages:read` (own) | the Decisions the caller asked |
|
|
557
|
+
| `GET /api/agent/decisions?answerable=1[&state=]` · `POST /api/agent/decisions/:id/answer {option?, text?}` | – | – | `decisions:answer` | [Decisions from apps](docs/app-front.md#decisions-from-apps): confirm-tier Decisions without an action of a session the app launched or holds a grant allowing `answer` for (others `404`); `410 gone` when not open or another answer won, `403` tap tier / any action / Concierge-asked / a lasting option, `400` with `via`, `423 paused` |
|
|
531
558
|
| `POST /api/agent/reply {replyTo, text}` · `…/decline {replyTo, why}` | ✓ | ✓ | `messages:send` | answers a message you received (to its real sender); not a Decision's ask |
|
|
532
559
|
| `GET /api/agent/inbox` · `GET /api/agent/messages/:id?wait=` | ✓ | ✓ | `messages:read` | open asks, your waiting asks, recent messages, mutes · one message and its answer |
|
|
533
560
|
| `POST /api/agent/mute {session}` · `…/unmute` | ✓ | ✓ | `messages:send` | refuses (or allows again) messages from a session |
|
|
534
561
|
| `GET` · `PUT /api/agent/messaging {enabled}` | ✓ | – | – | the per-user messaging switch (off: `423 paused`) |
|
|
535
562
|
| `POST /api/agent/app-tokens {label?, scopes?}` · `GET` · `DELETE …/:id` | ✓ | – | – | creates (token shown once), lists, revokes app tokens |
|
|
563
|
+
| `GET` · `POST /api/agent/app-clients {name, redirectURIs, scopes, audience?}` · `PATCH`/`DELETE …/:id` · `POST …/:id/secret` | ✓ | – | – | registered app clients ([connect an app](docs/app-front.md#connect-an-app)): admins register hub-wide ones, anyone their own (`audience: "user"`); the secret is shown at create and rotate only |
|
|
564
|
+
| `GET /api/agent/connections` · `DELETE …/:id` | ✓ | – | – | your connected apps (scopes, created, last refresh); delete revokes one (its tokens, grants and webhooks) |
|
|
565
|
+
| `POST /api/agent/speech/transcribe` (raw audio) · `POST /api/agent/speech/speak {text}` | – (console: `/api/transcribe`, `/api/speak`) | – | `speech:use` | [speech for apps](docs/app-front.md#speech-for-apps): `{text}` · `audio/mpeg`, with the user's own speech provider and key; per-credential quotas (`429 rate_limited`), `422 unsupported` without a provider, `502 upstream_failed`; nothing recorded |
|
|
566
|
+
| `GET /api/agent/sessions/:addr/stream` (SSE; `Last-Event-ID`) | ✓ | – | `conversation:read` | [conversation for apps](docs/app-front.md#conversation-for-apps): `ready`, `delta`, `say`, `heard`, `status`, `conversation`, `reset`, `end` of a session the app launched or holds a grant allowing `prompt` for (others `404`); 4 streams per app, 20 per user (`429`) |
|
|
567
|
+
| `POST /api/agent/sessions/:addr/conversation {on, front?}` · `…/utterance {text}` · `…/interrupt` | ✓ | – | `conversation:write` + a grant allowing `prompt` | conversation mode (the voice front with `front: true`) · `202 {inboxID}` (counts against `maxActionsPerHour`, marked as the app's prompt) · `204`; kill switch `423 paused` |
|
|
536
568
|
|
|
537
569
|
Sends to a session (`msg`, `ask`, `reply`, `decline`) answer `{message, delivery, woke, wakeSkipped?}`: `woke` says
|
|
538
570
|
whether the hub sent it to wake the recipient, and when not, `wakeSkipped` says why (`wake_false`, `subagent`,
|
|
@@ -567,7 +599,26 @@ decision}` on every open, answer, timeout, escalation and expiry); a `decision`
|
|
|
567
599
|
Reads come from the hub's catalog; only `show` (summary) and `log` (timeline) ask the session's spoke, so they
|
|
568
600
|
depend on what its harness supports. A session token reads all of its user's sessions but names and claims roles
|
|
569
601
|
only for itself; an app token reads, and messages only with the `messages:*` scopes. `GET /.well-known/hub` (public) answers `{publicURL, version,
|
|
570
|
-
minClientVersion, auth, features}` for apps and CLIs
|
|
602
|
+
minClientVersion, auth, features}` for apps and CLIs; `features` lists `app-connect` (apps connect with consent:
|
|
603
|
+
[docs/app-front.md](docs/app-front.md#connect-an-app)), `app-speech` (speech for apps:
|
|
604
|
+
[docs/app-front.md](docs/app-front.md#speech-for-apps)), `app-conversation`
|
|
605
|
+
([conversation for apps](docs/app-front.md#conversation-for-apps)) and `app-decisions`
|
|
606
|
+
([Decisions from apps](docs/app-front.md#decisions-from-apps)).
|
|
607
|
+
|
|
608
|
+
**Connected apps.** A web app's server connects to a user's account with an OAuth 2.1-style consent flow instead of a
|
|
609
|
+
hand-made token: `GET /connect/authorize` (PKCE S256) → the hub's consent page, where the user can narrow the scopes →
|
|
610
|
+
`POST /connect/token` → a 1-hour `hca_` access token and a rotating `hcr_` refresh token; `POST /connect/revoke` ends
|
|
611
|
+
it. Clients are registered in Settings → Agents & access. See [docs/app-front.md](docs/app-front.md#connect-an-app).
|
|
612
|
+
|
|
613
|
+
**Hub as an app's AI layer (app front).** A web app can use Hub as its AI layer for its users: connect (above), launch
|
|
614
|
+
helper sessions, follow and talk to them (a conversation stream, voice turns through the voice front), speak and
|
|
615
|
+
transcribe with the user's own speech provider, and answer the everyday confirm-tier Decisions its own sessions ask.
|
|
616
|
+
Always through the app's **own server** (a BFF): hub tokens never reach a browser, and the hub sends no CORS headers.
|
|
617
|
+
The guide: [docs/app-front.md](docs/app-front.md) (the BFF pattern, scopes, the grants each feature needs, limits,
|
|
618
|
+
the refresh rule, security rules). The SDK (`packages/sdk`: `HubConnect`, `ConnectionTokens`, `hub.speech`,
|
|
619
|
+
`hub.conversation`, `hub.appDecisions`) and the voice loop's `appTransport` (`packages/voice-client`) do the work; a
|
|
620
|
+
reference BFF lives in [examples/app-front-bff](examples/app-front-bff/README.md), and the
|
|
621
|
+
[app fixture](docs/app-integration-testing.md) runs a real hub to test an app against.
|
|
571
622
|
|
|
572
623
|
**Apps.** A native app links with a code from Settings → Devices → **Add app** (`POST /api/devices/link`, then
|
|
573
624
|
`POST /api/devices/redeem` from the app; see [Multi-user](#multi-user-sign-in-with-google)) and uses the device
|
|
@@ -592,7 +643,7 @@ you. Addresses resolve among your own sessions only; another user's sessions do
|
|
|
592
643
|
| `reply <msgID> "text"` · `decline <msgID> --why "…"` | answers a message you received; it goes to the message's real sender, never through the role |
|
|
593
644
|
| `inbox` | asks waiting for your answer, your own open asks, recent messages, mutes |
|
|
594
645
|
| `mute <addr>` · `unmute <addr>` | refuses (or accepts again) messages from a session (`forbidden`, exit 5) |
|
|
595
|
-
| `who` |
|
|
646
|
+
| `who` | online sessions, and named or role-holding ones while offline |
|
|
596
647
|
|
|
597
648
|
`--no-wake` admits a message without starting a turn and `--urgent` steers it into a running turn. Texts are at
|
|
598
649
|
most 10,000 characters (a decline's reason 1,000).
|
|
@@ -643,7 +694,8 @@ sessions or apps; see [Decisions and `@me`](#agent-identity-agent-api) for the r
|
|
|
643
694
|
**Where agents send from.**
|
|
644
695
|
- In a shell of an OpenCode session: `hub msg @reviewer "…"` acts as that session ([hub CLI](#hub-cli)).
|
|
645
696
|
- The model itself: the plugin's `hub` tool (verbs `msg`, `ask`, `reply`, `decline`, `inbox`, `who`, `status`,
|
|
646
|
-
`whoami`, the [team intake](docs/teams-intake.md) verbs `intake` and `intake_update`,
|
|
697
|
+
`whoami`, the [team intake](docs/teams-intake.md) verbs `intake` and `intake_update`, the
|
|
698
|
+
[subscription](docs/subscriptions.md) verbs `subscribe`, `unsubscribe` and `subscriptions`, `name`, `role_claim` and
|
|
647
699
|
`role_release` for its own @name and roles, and the [act verbs](#agent-actions-and-grants)), as the calling session ([Spoke](#spoke-opencode-plugin)).
|
|
648
700
|
- You, in the OpenCode TUI: `/msg @name text` (as the session on screen) and `/who`.
|
|
649
701
|
- Other agents and tools: [`hub mcp`](#hub-mcp-mcp-server), as the app token you logged in with, or the hub's
|
|
@@ -792,7 +844,7 @@ and the SDK. Only you manage grants: the console (Settings → Agents) or the CL
|
|
|
792
844
|
| `POST /api/agent/sessions/:addr/approve {requestID, always?, why?}` | ✓ | under a grant | under a grant |
|
|
793
845
|
| `POST /api/agent/sessions/:addr/deny {requestID, why?}` | ✓ | under a grant | under a grant |
|
|
794
846
|
| `POST /api/agent/sessions/:addr/answer {formID, answer \| cancel: true, why?}` | ✓ | under a grant | under a grant |
|
|
795
|
-
| `POST /api/agent/sessions/:addr/prompt {text, delivery?, why?}` (users: `images?`) | ✓ | under a grant | under a grant |
|
|
847
|
+
| `POST /api/agent/sessions/:addr/prompt {text, delivery?, why?}` (users: `images?`) | ✓ | under a grant | under a grant (or `conversation:write`, the app's own sessions) |
|
|
796
848
|
| `POST /api/agent/sessions/:addr/stop {why?}` | ✓ | under a grant | under a grant |
|
|
797
849
|
| `POST /api/agent/grants` · `GET` · `GET`/`PUT`/`DELETE …/grants/:id` | ✓ | – | – |
|
|
798
850
|
| `POST /api/agent/grants/requests {grant, why}` | – | ✓ (any token) | `messages:send` |
|
|
@@ -867,7 +919,7 @@ hub show <addr> state, blockers, activity, summary
|
|
|
867
919
|
hub log <addr> [-n 20] newest timeline rows (1–200), printed oldest first
|
|
868
920
|
hub watch [--blocked] [--session <addr>] events as JSON lines until Ctrl-C (pings skipped)
|
|
869
921
|
hub whoami who this credential is
|
|
870
|
-
hub who [--project P] [--role R | @@role]
|
|
922
|
+
hub who [--project P] [--role R | @@role] online sessions, and named or role-holding ones while offline
|
|
871
923
|
hub name <@name> [--session <addr>]
|
|
872
924
|
hub intent <addr> [text… | --clear] what the session is for (one line, ≤ 280 characters)
|
|
873
925
|
hub role claim <role[.project]> [--exclusive] [--session <addr>]
|
|
@@ -881,6 +933,9 @@ hub ask @me "question" [--option id=label[:safe]]... [--recommend id [--why "…
|
|
|
881
933
|
hub decisions list [--state open|answered|expired|closed] the Decisions you asked
|
|
882
934
|
hub inbox asks to answer, your open asks, recent messages, mutes
|
|
883
935
|
hub mute <addr> · hub unmute <addr> refuse (or accept again) messages from a session
|
|
936
|
+
hub subscribe <addr|@@role|#team|children>... [--type T]... [--delivery queue|steer|silent] [--batch 30s]
|
|
937
|
+
[--expires 1d] [--max-per-hour N] [--subscriber <addr>] batched hub messages about their events
|
|
938
|
+
hub subscriptions [--session <addr>] · hub unsubscribe <subID>
|
|
884
939
|
hub approve <addr> <requestID> [--always] [--why "…"] answer a permission request (once, or always)
|
|
885
940
|
hub deny <addr> <requestID> [--why "…"] reject it; the reason reaches the harness
|
|
886
941
|
hub answer <addr> <formID> key=value… | --json '{…}' | --json - | --cancel [--why "…"]
|
|
@@ -1113,7 +1168,7 @@ event stream comes back) it asks its own server what runs now (`session.active`
|
|
|
1113
1168
|
|
|
1114
1169
|
| Entry | File | Role |
|
|
1115
1170
|
|---|---|---|
|
|
1116
|
-
| server | `server.ts` → `src/index.ts` | Starts the process-wide spoke (`src/spoke/`), `hub.*` RPC (also under the legacy id `voice.*`), the `voice`
|
|
1171
|
+
| server | `server.ts` → `src/index.ts` | Starts the process-wide spoke (`src/spoke/`), `hub.*` RPC (also under the legacy id `voice.*`), the hidden `voice-front` and `concierge` agents, and the `context` hook (short spoken replies, the voice front's note and tools) |
|
|
1117
1172
|
| tui | `tui.ts` → `src/tui.ts` | `/hub` dialog and the feature commands (`/voice`), registered through `src/tui-commands.ts` (ids `hub.<id>`, palette group "Hub"); reports the sessions it shows (`hub.presence`) |
|
|
1118
1173
|
| rpc | `rpc.ts` → `src/rpc.ts` | `HubRpc` (id `hub`; `VoiceRpc` is the deprecated id `voice`): `status / setServer / checkServer / enroll / startLocalHub / setMode / setSession / pairCode / handoff / openLink / conversation / presence` |
|
|
1119
1174
|
|
|
@@ -1122,10 +1177,12 @@ The plugin ids are `hub` (server) and `hub.tui`. OpenCode resolves a local plugi
|
|
|
1122
1177
|
|
|
1123
1178
|
- **Config**: `~/.config/hub/config.json` (or `$HUB_SPOKE_CONFIG`), shared by all
|
|
1124
1179
|
processes on the machine, mode 0600, watched live. Holds the server URL, mode, machine token,
|
|
1125
|
-
`
|
|
1180
|
+
`voiceModel` (the voice front's model, `provider/model[#variant]`; pick a fast paid one),
|
|
1126
1181
|
`autoStartLocalHub` (default `true`; see [Local only](#local-only)), `herdr` (default `true`; read open
|
|
1127
1182
|
sessions and workspaces from [Herdr](#open-sessions-herdr-and-tuis)), and `list` (the history window,
|
|
1128
1183
|
default `{"days": 30, "max": 100}`; days 1–365, max 1–500; out-of-range values fall back per field).
|
|
1184
|
+
`subagent` and `workModel` (the removed voice agent's delegate agent and worker model) are obsolete: read
|
|
1185
|
+
without error, ignored, logged once per process, and dropped the next time the plugin saves the file.
|
|
1129
1186
|
- **Modes**: `off` (no connection), `always` (every session this process knows, Voice on for all),
|
|
1130
1187
|
`session` (Voice only for sessions switched on in `/hub` or from the phone, plus their subagent
|
|
1131
1188
|
sessions). Every reported session can be inspected and controlled in both modes; in `session` mode the
|
|
@@ -1194,20 +1251,34 @@ Sessions open in a terminal are reported with `open: true` and `terminal` (`src/
|
|
|
1194
1251
|
|
|
1195
1252
|
## Conversation mode (OpenCode side)
|
|
1196
1253
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1254
|
+
Voice mode (conversation mode) always uses the **voice front** when it can: a hidden, hub-managed OpenCode session
|
|
1255
|
+
on the fast `voiceModel`, one per conversation (design: `docs/plans/2026-11-06-voice-front-design.md`, contract:
|
|
1256
|
+
`docs/plans/2026-11-06-voice-front-p1-contract.md`). Every reply starts with a short echo of what it understood;
|
|
1257
|
+
it relays your request to the session (a new turn when the session is idle, steering when it is busy, queued for
|
|
1258
|
+
an unrelated request) or stops it, and speaks the session's results, questions and failures. It has no shell, no
|
|
1259
|
+
edits and no subagents. The session you talk to keeps its own agent, model, tools and subagents.
|
|
1260
|
+
|
|
1261
|
+
The front's model requests are kept short so its echo starts at once (`src/spoke/voice-front.ts`, applied by the
|
|
1262
|
+
`context` hook to linked fronts only):
|
|
1263
|
+
- **No thinking:** an Anthropic-API model (also through a gateway such as OmniRoute) gets `thinking: {type:
|
|
1264
|
+
"disabled"}`, overriding a model's own thinking setting or a `#variant`. An OpenAI-style model gets the lowest
|
|
1265
|
+
reasoning effort it lists (`none`, else `minimal`); one that lists neither gets nothing, because a non-reasoning
|
|
1266
|
+
model may reject `reasoning_effort`. Other providers keep their default.
|
|
1267
|
+
- **Lean system prompt:** only the front's own prompt. OpenCode's instruction files (`AGENTS.md`, `CLAUDE.md`,
|
|
1268
|
+
`instructions`), model, date, environment and worktree blocks are left out; the front gets its two tools, the
|
|
1269
|
+
per-turn note and its conversation.
|
|
1270
|
+
|
|
1271
|
+
Without a front the hub talks to the session directly and the session answers in short spoken replies (the
|
|
1272
|
+
`context` hook adds a voice style to its turns): when the device turned **Voice front** off (Settings → Voice), when
|
|
1273
|
+
the machine's plugin is older or cannot create the front, after the front failed (`front_failed`, e.g. a free model
|
|
1274
|
+
out of quota), and when an older hub sends `conversation {on}` without `front`. In no case is the session's agent or
|
|
1275
|
+
model switched. Earlier versions switched the session to a `voice` agent on the fast model that delegated work to
|
|
1276
|
+
background subagents; that agent, its orchestrator prompt, the `hub_task_stop` tool and the `subagent` /
|
|
1277
|
+
`workModel` settings are gone.
|
|
1278
|
+
|
|
1279
|
+
The phone composer has one **Voice** model control per session: the voice front's model. The picker lists the
|
|
1280
|
+
connected OpenCode process's models and variants; the choice is saved per user and session, applies the next
|
|
1281
|
+
time Voice mode starts, and falls back to the machine's `voiceModel` when unset. Pick a fast, paid model.
|
|
1211
1282
|
|
|
1212
1283
|
The conversation is an inline execution timeline: streamed thinking, tools and nested subagents appear
|
|
1213
1284
|
between messages as compact expandable rows. Rows keep the session's order, never wall-clock order: history
|
|
@@ -1220,26 +1291,19 @@ not persist those potentially sensitive details.
|
|
|
1220
1291
|
|
|
1221
1292
|
**Spoke side** (`src/spoke/conversation.ts`, `src/spoke/spoke.ts`):
|
|
1222
1293
|
|
|
1223
|
-
- The plugin registers
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
the deploy going?" needs no tool call. A session without workers gets no board.
|
|
1237
|
-
- **Stopping one task by voice**: the plugin adds a tool `hub_task_stop {sessionID} | {all: true}`. The voice
|
|
1238
|
-
agent calls it for "cancel the deploy one" (that worker and whatever runs below it are interrupted, deepest
|
|
1239
|
-
first) or "stop all the workers" (every running worker of the session). It only reaches sessions below the
|
|
1240
|
-
calling session (never the caller itself, its siblings or another session's workers), so the voice agent
|
|
1241
|
-
keeps listening. The phone's stop words ("stop", "cancel", "never mind", "stop that", "hold on") still
|
|
1242
|
-
interrupt the root turn at once.
|
|
1294
|
+
- The plugin registers the hidden primary agent **`voice-front`** through `ctx.agent.transform` (everything denied
|
|
1295
|
+
but its two relay tools, `voice_main_send` and `voice_main_stop`, which the `context` hook offers only to a linked
|
|
1296
|
+
front). It registers no `voice` agent: a session an older plugin left on one (a crash mid-conversation) is moved
|
|
1297
|
+
to the default agent before the hub's next prompt, or after a turn fails with "Agent not found", because OpenCode
|
|
1298
|
+
2.2.3 fails every turn on a missing agent. A `voice` agent you define yourself is left alone.
|
|
1299
|
+
- `conversation on` with `front` creates (or re-links) the front; `on` without it, or when no front can be made,
|
|
1300
|
+
marks the session's turns as voice turns. `off` deletes the front. Both answer with the session's current agent
|
|
1301
|
+
and never switch it.
|
|
1302
|
+
- **Task board**: the front's per-turn note about its session lists the session's workers (`src/spoke/taskboard.ts`,
|
|
1303
|
+
from this process's events): title, state (working, blocked with what it waits for, done with its latest text,
|
|
1304
|
+
failed with the reason, stopped) and for how long, at most 8 (live ones first) of at most 200 characters each.
|
|
1305
|
+
So "what's running?" or "how's the deploy going?" needs no tool call. The phone's stop words ("stop", "cancel",
|
|
1306
|
+
"never mind", "stop that", "hold on") still interrupt the turn at once.
|
|
1243
1307
|
- **Messaging other sessions**: the plugin adds a tool `hub {verb, …}` with the verbs `msg`, `ask`, `reply`,
|
|
1244
1308
|
`decline`, `inbox`, `who`, `status`, `whoami`, `intake` and `intake_update` (`src/spoke/hub-tool.ts`). It acts as the calling session
|
|
1245
1309
|
with that session's own hub token (the token never appears in the tool's output). `name {name}` sets the
|
|
@@ -1251,8 +1315,9 @@ not persist those potentially sensitive details.
|
|
|
1251
1315
|
- Child text and execution events are paired by the hub. The latest assistant message of a successful
|
|
1252
1316
|
child is emitted as root-session speech; interruptions and stale partial results are not. **Announcements**
|
|
1253
1317
|
(hub, `src/central/registry.ts`): while another worker of the session runs or waits, or one was announced in
|
|
1254
|
-
the last minute, a result starts with the task's title ("Deploy check finished: …"); a result the
|
|
1255
|
-
already said in the last two minutes is not read again
|
|
1318
|
+
the last minute, a result starts with the task's title ("Deploy check finished: …"); a result the session
|
|
1319
|
+
already said in the last two minutes is not read again (with a voice front, worker results are the front's to
|
|
1320
|
+
report and the hub does not read them). A failed worker is announced once with its reason
|
|
1256
1321
|
("Lint failed: rate limit exceeded."), a worker's permission request or question once per request ("Cleanup
|
|
1257
1322
|
needs your permission.", "… has a question."); a stopped worker is not. Announcements are ordinary `say`s
|
|
1258
1323
|
without `seq` (`messageID` `announce:failed:<session>`, `announce:permission:<id>`, `announce:form:<id>`),
|
|
@@ -1260,13 +1325,13 @@ not persist those potentially sensitive details.
|
|
|
1260
1325
|
- Server ops: `steer` sends the text with `delivery: "steer"` (metadata `{source: "hub", mode:
|
|
1261
1326
|
"conversation"}`, or `mode: "remote"` for a session not shared with Voice; 0.5 wrote `source:
|
|
1262
1327
|
"opencode-hub"` and 0.4 and earlier `source: "opencode-voice"`, and readers accept all three) and returns the inbox ID at once; `interrupt`
|
|
1263
|
-
interrupts the session; `conversation` turns the mode on/off and returns the agent
|
|
1264
|
-
is refused for sessions not shared with Voice. Replies reach the hub as forwarded
|
|
1328
|
+
interrupts the session; `conversation` turns the mode on/off and returns the session's agent (never
|
|
1329
|
+
switched), and is refused for sessions not shared with Voice. Replies reach the hub as forwarded
|
|
1265
1330
|
`session.text.ended`, `session.inbox.delivered` and `session.agent.selected` events.
|
|
1266
1331
|
- State is per process and in memory; the hub re-sends `conversation on` after a reconnect.
|
|
1267
|
-
- From the terminal: `/voice` opens the current session in the browser with Voice mode starting
|
|
1268
|
-
|
|
1269
|
-
session to your phone.
|
|
1332
|
+
- From the terminal: `/voice` opens the current session in the browser with Voice mode starting (with the voice
|
|
1333
|
+
front, unless that browser turned it off); `/hub → Conversation mode` only toggles short spoken replies for the
|
|
1334
|
+
session in this process (no front), then offers to send the session to your phone.
|
|
1270
1335
|
|
|
1271
1336
|
## Central server
|
|
1272
1337
|
|
|
@@ -1293,8 +1358,10 @@ holds the startup (`bin/hub.ts` serves `web/` from disk; the compiled binaries e
|
|
|
1293
1358
|
| `HUB_RELEASE_BASE` | `https://github.com/huynle/hub-releases/releases/download/v<version>` | where `/install.sh` and `/install.ps1` download hubctl ([Set up a machine](#set-up-a-machine-one-line)): https, or plain http on loopback or a private-network IP address; the hub refuses to start with any other value. Without it a development version (not a plain `x.y.z`) answers 503 |
|
|
1294
1359
|
| `HUB_CSP` | `enforce` | `report-only` sends the console's Content-Security-Policy as `Content-Security-Policy-Report-Only` (violations are only logged in the browser console); see [Content-Security-Policy](#content-security-policy) |
|
|
1295
1360
|
| `HUB_REMOTE_FETCH` | `on`; `off` when `HTTP_PROXY`/`HTTPS_PROXY` (either case) is set and `NO_PROXY` is not `*` | `on` or `off`. A proxy could re-resolve hostnames and undo the fetcher's address pinning, so with a proxy in the environment remote fetch stays off (with a startup warning) unless you set `NO_PROXY=*` for the hub process (direct fetches) or `HUB_REMOTE_FETCH=on` (accept proxying knowingly). `off` turns off the hub fetching web images and link previews for the console (`GET /api/media/remote` and `GET /api/media/unfurl` answer `403 disabled`). Signed-in users only (session and app tokens are refused); http/https on default ports, private and reserved addresses refused, 8 MiB, never decompressed (compressed responses are refused), PNG/JPEG/WebP/GIF only, 30 a minute and 4 at once per user (images and link previews together); the audit log keeps the host, never the URL. Any other value stops the hub at start. Threat model and residual risks: [docs/media-security.md](docs/media-security.md) |
|
|
1361
|
+
| `HUB_TRUSTED_PROXIES` | | comma list of reverse-proxy addresses or CIDRs whose `X-Forwarded-For` names the client for per-address rate limits ([connect an app](docs/app-front.md#connect-an-app)); unset: the TCP peer is the client and `X-Forwarded-For` is never read; an invalid entry stops the hub at start |
|
|
1296
1362
|
| `HUB_FETCH_DENY_CIDRS` | | comma list of extra IPv4 or IPv6 CIDRs the hub never fetches from (`GET /api/media/remote`, `GET /api/media/unfurl`), on top of the built-in loopback, private, link-local (cloud metadata), CGNAT, multicast, reserved, ULA and mapped ranges (e.g. your Kubernetes pod and service CIDRs: `10.42.0.0/16,10.43.0.0/16`); an invalid entry stops the hub at start |
|
|
1297
|
-
| `OPENROUTER_API_KEY` | | default key for the `openrouter` speech provider (never sent to other providers) |
|
|
1363
|
+
| `OPENROUTER_API_KEY` | | default key for the `openrouter` speech provider (never sent to other providers); [context cards](docs/context-cards.md) on OpenRouter reuse it |
|
|
1364
|
+
| `HUB_CONTEXT_CARDS` | on | `off` (or `false`, `0`, `no`) turns [context cards](docs/context-cards.md) off for every user (the kill switch over `hub.json` `contextCards.enabled`); `on` (`true`, `1`, `yes`) overrides it. Any other value stops startup |
|
|
1298
1365
|
| `HUB_STT_MODEL`, `HUB_TTS_MODEL`, `HUB_VOICE`, `HUB_MAX_*` | see `src/central/config.ts` | speech defaults; the phone's speech settings override them |
|
|
1299
1366
|
|
|
1300
1367
|
The Docker image installs only the hub's runtime packages (web-push and its dependencies, pinned by
|
|
@@ -1344,8 +1411,10 @@ recent activity.
|
|
|
1344
1411
|
`web/` is the Hub console, a Solid + Vite + TypeScript PWA built to `web/dist` (`npm run build:web`;
|
|
1345
1412
|
the Docker image, npm package and binaries ship the build). One store fed by the hub's SSE stream drives:
|
|
1346
1413
|
|
|
1347
|
-
- **Sidebar** (⌘B collapses; a drawer on phones): search, New session, Needs you,
|
|
1348
|
-
|
|
1414
|
+
- **Sidebar** (⌘B collapses; a drawer on phones): search, New session, Needs you, **Working set** (your pinned
|
|
1415
|
+
sessions and the 7 you touched most recently within 7 days; Pinned when you touched none), Running, sessions open
|
|
1416
|
+
in a terminal grouped by project (or server) with `+`, Recents. Rows show the session's intent as a muted second
|
|
1417
|
+
line, and Needs you rows how long they have waited. Each section collapses from its heading (remembered on
|
|
1349
1418
|
the device, `hub.sidebar.sections.collapsed`), showing its count; Needs you always shows its count. Rows show status, a pulsing green dot left of the
|
|
1350
1419
|
title for the session live in Voice (in Voice mode here, or in a conversation), an unread dot and a `⋯` menu
|
|
1351
1420
|
(rename, pin, archive, stop, Allow/Disallow Voice).
|
|
@@ -1400,7 +1469,7 @@ the Docker image, npm package and binaries ship the build). One store fed by the
|
|
|
1400
1469
|
recording cancels it; each session keeps its own unsent draft), Voice mode (hands-free, see below; a toggle that
|
|
1401
1470
|
turns Voice on first when needed; one session at a time, so turning it on elsewhere turns the first one off; it
|
|
1402
1471
|
keeps running while you look at other sessions, which show a "Voice: <session>" pill with End) with a
|
|
1403
|
-
microphone picker, and the Voice
|
|
1472
|
+
microphone picker, and the Voice model menu (the voice front's model). The right rail (a bottom sheet on phones) has
|
|
1404
1473
|
cards that collapse from their header (remembered per device): **Environment** (machine with
|
|
1405
1474
|
online/offline/Connecting, project folder with the full path as tooltip, branch, and where it is open:
|
|
1406
1475
|
TUI, Herdr or background), Context, **Connectors · N connected** (MCP servers) and Changes with a diff
|
|
@@ -1685,7 +1754,7 @@ public URL, `localhost`, `127.0.0.1` and `[::1]` with the same scheme and port c
|
|
|
1685
1754
|
|---|---|---|
|
|
1686
1755
|
| `POST /api/sessions/:id/utterance {text}` | `steer` | `202 {inboxID}`; allowed while the session is busy (no turn lock). 400 empty or over 4000 chars, 404 no machine of yours reports the session, 502 spoke error, 504 no answer in 15 s |
|
|
1687
1756
|
| `POST /api/sessions/:id/interrupt` | `interrupt` | `204` |
|
|
1688
|
-
| `POST /api/sessions/:id/conversation {on}` | `conversation` | `200 {on, agent}`. 404 when no machine of yours reports the session |
|
|
1757
|
+
| `POST /api/sessions/:id/conversation {on, front?}` | `conversation` | `200 {on, agent, front?}`. `on: true` asks for the voice front unless `front: false` (the device opted out); the front is used when the session's machine serves it (`voiceFront`), and `front` in the answer says whether one answers. `agent` is the session's own agent (never switched). 404 when no machine of yours reports the session |
|
|
1689
1758
|
| `GET /api/sessions/:id` | | `{id, title, directory, parentID?, project?, projectName?, updated, exposed, state, blockers, now?, busy, conversation, status, activity}`; catalog sessions also carry `conversation` |
|
|
1690
1759
|
| `POST /api/sessions/:id/presence {visible}` | | `204`; the phone posts on `visibilitychange` and every 30 s while visible. A root session is *watched* while any device reported it (or one of its subagent sessions) visible within 45 s |
|
|
1691
1760
|
| `POST /api/sessions/:id/permission {requestID, decision}` | `permission` | `204`. `decision` is `once`, `always` or `reject`; `:id` is the session or any ancestor. Tried on every process that forwarded the request (OpenCode keeps pending permissions per process and per location; the spoke replies through the plugin instance of the location that holds it). `409 {code: "gone"}` "This request was already answered or expired" when none holds it (its card is dropped). 400 bad decision |
|
|
@@ -1779,10 +1848,13 @@ around the mic with the current state: Listening, Hearing you, Transcribing, Sen
|
|
|
1779
1848
|
it again, say "pause listening", or leave the page to turn it off. Leaving the page calls
|
|
1780
1849
|
`{on:false}` with `keepalive`. While conversation mode is off, tap-to-talk works as before.
|
|
1781
1850
|
|
|
1782
|
-
|
|
1851
|
+
The loop is the package `packages/voice-client` (a `VoiceTransport` over the console's routes and SSE,
|
|
1852
|
+
`web/src/voice/console-transport.ts`); other web apps can embed it ([docs/app-front.md](docs/app-front.md#voice-in-an-app-packagesvoice-client)).
|
|
1853
|
+
|
|
1854
|
+
- **Speech detection** (`packages/voice-client/src/core/speech-vad.ts`; ~20 ms frames from `web/public/vad-worklet.js`, with a
|
|
1783
1855
|
ScriptProcessor fallback): on-device [Silero VAD](https://github.com/snakers4/silero-vad) v5 (MIT) through
|
|
1784
1856
|
onnxruntime-web (MIT, wasm, single-threaded, so no COOP/COEP headers are needed) in a Web Worker
|
|
1785
|
-
(`
|
|
1857
|
+
(`packages/voice-client/src/silero-worker.ts`). The mic audio is resampled to 16 kHz and cut into 32 ms chunks; each
|
|
1786
1858
|
chunk's speech probability starts speech at ≥ 0.6 sustained for 250 ms (most of the onset voiced, so
|
|
1787
1859
|
isolated clicks never add up) and ends it after 350 ms below 0.35. Typing, a fan, clatter, beeps and hum
|
|
1788
1860
|
score low; speech scores high at any loudness. An energy detector stays as a cheap pre-gate (chunks within
|
|
@@ -1798,7 +1870,7 @@ it again, say "pause listening", or leave the page to turn it off. Leaving the p
|
|
|
1798
1870
|
- **Echo guard**: while a reply or a progress cue plays, and 400 ms after, a barge-in needs more: the model's
|
|
1799
1871
|
probability ≥ 0.85 (Low 0.92, High 0.75) and 8 dB more energy. The browser's echo cancellation removes most
|
|
1800
1872
|
of the reply from the mic first.
|
|
1801
|
-
- **Noise transcripts** (`
|
|
1873
|
+
- **Noise transcripts** (`packages/voice-client/src/core/junk-transcript.ts`): what speech-to-text makes of noise is dropped
|
|
1802
1874
|
before it reaches the agent and shown dim in the Voice bar for 3 s ("ignored: “Thank you.”"): empty or
|
|
1803
1875
|
punctuation-only text, sound tags ("[Music]", "(applause)", "♪"), Whisper's stock phrases ("Thank you.",
|
|
1804
1876
|
"you", "Bye.", "Thanks for watching!", "Subtitles by …", "Amara.org" …), one word repeated, and one or two
|
|
@@ -1814,10 +1886,10 @@ it again, say "pause listening", or leave the page to turn it off. Leaving the p
|
|
|
1814
1886
|
- **Turn detection (experimental, Settings → Voice → Turn detection, per device)**: "Timers (current)" is the
|
|
1815
1887
|
default and works as above. "Smart Turn (experimental)" decides the end of a turn from the audio: on-device
|
|
1816
1888
|
[Pipecat Smart Turn v3](https://huggingface.co/pipecat-ai/smart-turn-v3) (v3.2 int8, BSD-2-Clause, 8.7 MB, loaded on
|
|
1817
|
-
first use as a hashed asset; license in `
|
|
1818
|
-
(`
|
|
1889
|
+
first use as a hashed asset; license in `packages/voice-client/src/turn/LICENSE`) in its own Web Worker
|
|
1890
|
+
(`packages/voice-client/src/smart-turn-worker.ts`, onnxruntime-web as for Silero, so the console policy needs nothing new). The
|
|
1819
1891
|
console keeps the last 8 s of the turn's 16 kHz mic audio, pauses included (from the turn's first speech start),
|
|
1820
|
-
computes Whisper's log-mel features in TS (`
|
|
1892
|
+
computes Whisper's log-mel features in TS (`packages/voice-client/src/core/smart-turn-features.ts`, matched to transformers'
|
|
1821
1893
|
`WhisperFeatureExtractor` within 1e-5) and scores it at each segment end: a score ≥ 0.5 sends 150 ms after the
|
|
1822
1894
|
score once the transcripts are in; a lower one holds and is re-scored every 400 ms of quiet, with a fallback after
|
|
1823
1895
|
3 s. Speech resuming cancels the send as with the timers; commands, the 60 s cap and noise transcripts are
|
|
@@ -1837,7 +1909,7 @@ it again, say "pause listening", or leave the page to turn it off. Leaving the p
|
|
|
1837
1909
|
list items are handled, and fenced code becomes "(code omitted)". Each sentence goes to `/api/speak`,
|
|
1838
1910
|
up to two ahead of playback, and plays through one `<audio>` element while its MP3 is still downloading:
|
|
1839
1911
|
through MediaSource (`audio/mpeg` SourceBuffer; Chrome, Android, Firefox), appending bytes as they arrive
|
|
1840
|
-
(`
|
|
1912
|
+
(`packages/voice-client/src/core/audio-stream.ts`). Browsers without MP3 in MediaSource (some iOS Safari versions) play each
|
|
1841
1913
|
sentence once it has fully downloaded, as before. Prefetched sentences keep downloading while they wait. Speech while a reply plays
|
|
1842
1914
|
(barge-in, past the echo guard) pauses playback, clears the queue and mutes the rest of the replies that were speaking, also a reply still
|
|
1843
1915
|
being written whose next sentence has not arrived yet (their later sentences still show in the transcript); a
|
|
@@ -1876,7 +1948,7 @@ it again, say "pause listening", or leave the page to turn it off. Leaving the p
|
|
|
1876
1948
|
|
|
1877
1949
|
### Voice feedback (phone)
|
|
1878
1950
|
|
|
1879
|
-
Pure logic is in `
|
|
1951
|
+
Pure logic is in `packages/voice-client/src/core/feedback-core.ts` (tested with bun); `packages/voice-client/src/core/cues.ts` plays cues.
|
|
1880
1952
|
|
|
1881
1953
|
- **Voice speed**: 0.75×–2.0× in 0.05 steps, next to "Read replies aloud" and in Settings → Voice
|
|
1882
1954
|
(default 1.0×, stored per device in `localStorage`). It is applied on the phone as the audio
|
|
@@ -1896,7 +1968,8 @@ Pure logic is in `web/src/core/feedback-core.ts` (tested with bun); `web/src/cor
|
|
|
1896
1968
|
newest are at the bottom, and it keeps about 30. The agent's reply streams into its bubble (one bubble per
|
|
1897
1969
|
assistant message); in conversation mode the spoken `say` text is merged into the bubble of the same message.
|
|
1898
1970
|
The home list shows the same "now" text with a spinner for working sessions.
|
|
1899
|
-
- **Progress cues** (conversation mode; Settings → Voice → "Progress cues", on by default
|
|
1971
|
+
- **Progress cues** (conversation mode; Settings → Voice → "Progress cues", on by default; with the voice front
|
|
1972
|
+
only the tones play, its echo replaces the spoken phrases below): a soft
|
|
1900
1973
|
tone as soon as you stop speaking (the moment the segment goes to transcription; typed text gets it when
|
|
1901
1974
|
the agent receives it), then a short acknowledgement ("On it.", "One moment.", "Let me check." …) if the
|
|
1902
1975
|
session is working and the agent has not spoken within 2.5 s of the end of your speech. A hand-off phrase
|
|
@@ -1905,7 +1978,7 @@ Pure logic is in `web/src/core/feedback-core.ts` (tested with bun); `web/src/cor
|
|
|
1905
1978
|
speech while work continues, and a double tone plays when a subagent finishes. Failed tools are not spoken (they show in the feed). Cues never
|
|
1906
1979
|
play while you are talking or the agent's speech is playing; they wait, and are dropped once the agent
|
|
1907
1980
|
speaks. Cues are event-driven: one timer for the next deadline (no polling), re-armed on speech, status and
|
|
1908
|
-
playback changes. Each cue has several variants (`CUE_VARIANTS` in `
|
|
1981
|
+
playback changes. Each cue has several variants (`CUE_VARIANTS` in `packages/voice-client/src/core/feedback-core.ts`), chosen
|
|
1909
1982
|
from a shuffle-bag: every variant once before any repeats, never the same phrase twice in a row, and a variant
|
|
1910
1983
|
whose audio is already loaded wins. All variants are synthesized, 2 at a time, when Voice mode starts (ending Voice mode
|
|
1911
1984
|
cancels what is left) and kept in memory and in the browser's Cache Storage (`hub-cues-v2`, keyed by TTS provider, model, voice, speed and server, so a later
|
|
@@ -1921,7 +1994,7 @@ Pure logic is in `web/src/core/feedback-core.ts` (tested with bun); `web/src/cor
|
|
|
1921
1994
|
(`localStorage` `voice.replyLoudness`, clamped to −26…−14 LUFS) becomes the target. Audio that cannot be decoded
|
|
1922
1995
|
plays as synthesized. Cues and replies keep their plain `<audio>` elements (no Web Audio playback graph, which
|
|
1923
1996
|
would break background and iOS playback), both at volume 1 and the chosen speed. Pure parts:
|
|
1924
|
-
`
|
|
1997
|
+
`packages/voice-client/src/core/loudness.ts`; browser decoding: `packages/voice-client/src/cue-loudness.ts`.
|
|
1925
1998
|
- **Presence**: while a session view is open, the phone posts `POST /api/sessions/:id/presence
|
|
1926
1999
|
{visible}` on visibility changes and every 30 s while visible, and `{visible:false}` on `pagehide` and
|
|
1927
2000
|
when leaving the view. The hub uses this to decide when to push.
|
|
@@ -1946,7 +2019,7 @@ Pure logic is in `web/src/core/feedback-core.ts` (tested with bun); `web/src/cor
|
|
|
1946
2019
|
device cookie. It closes when Voice mode ends or the page goes away. It needs notification permission:
|
|
1947
2020
|
without it the Voice bar shows **Allow notifications**. The Media Session shows "Voice mode on" with the
|
|
1948
2021
|
session title on the lock screen while Voice audio plays; its pause and stop end Voice mode (logic in
|
|
1949
|
-
`
|
|
2022
|
+
`packages/voice-client/src/core/voice-notifier.ts`).
|
|
1950
2023
|
- **Settings → Speech** (your own settings; admins also edit the server default): cards for Speech-to-text and Text-to-speech, with provider
|
|
1951
2024
|
(OpenRouter / OpenAI-compatible), base URL (OpenAI-compatible only), model, voice (TTS), language (STT,
|
|
1952
2025
|
optional), server-side speed (TTS, optional, 0.25–4) and API key. Model and voice are buttons that open a
|
|
@@ -1995,6 +2068,8 @@ in the file).
|
|
|
1995
2068
|
| `agents.never` | permission patterns (`action[:glob]`) no agent may ever approve, for every user, checked before any grant; see [Agent actions and grants](#agent-actions-and-grants) |
|
|
1996
2069
|
| `agents.launches` | per-user launch caps `{maxQueued?, maxActive?, perHour?}` (defaults 50, 4, 60) and `lostAfterMinutes?` (0–1440 or false, default 5: a running launch the same node instance stops reporting, with its session gone, ends lost; 0/false: never); see [docs/launches.md](docs/launches.md#placement-and-caps) |
|
|
1997
2070
|
| `agents.webhooks.allowPrivate` | `true` lets webhooks target `http://`, loopback and private hosts (self-hosted receivers); see [docs/launches.md](docs/launches.md#where-webhooks-may-go) |
|
|
2071
|
+
| `contextCards` | `{enabled?, model?: {provider?, model?, baseURL?, apiKey?}, budget?: {calls?, tokens?}}`: the light model the hub calls for [context cards](docs/context-cards.md#configuration) (default OpenRouter `anthropic/claude-haiku-4.5` with the speech OpenRouter key; 200 calls and 400,000 tokens per user per day) |
|
|
2072
|
+
| `agents.speechQuotas` | per-credential quotas of [speech for apps](docs/app-front.md#speech-for-apps), any subset of `{transcribe: {perMinute, perHour}, speak: {perMinute, perHour, charsPerHour}}` (defaults 20, 300; 60, 1000, 100000), whole numbers from 1 |
|
|
1998
2073
|
| `agents.webhooks.allowPrivateHosts` | exact hostnames (at most 50) whose `https://` webhook URLs may resolve to private addresses, e.g. a LAN-hosted app behind split DNS; nothing else is opened; see [docs/launches.md](docs/launches.md#where-webhooks-may-go) |
|
|
1999
2074
|
|
|
2000
2075
|
Server fields: env vars (`HUB_HOST`, `HUB_PORT`, `HUB_PUBLIC_URL`, `HUB_STATE_DIR`) > hub.json >
|
|
@@ -2283,7 +2358,7 @@ apart:
|
|
|
2283
2358
|
- Stop on a subagent session's page stops its whole tree (as on the root's page).
|
|
2284
2359
|
- Listening with the phone screen off depends on the browser (best-effort on Android Chrome; not
|
|
2285
2360
|
possible on iOS). Replies and alerts still arrive as push notifications.
|
|
2286
|
-
- The voice
|
|
2361
|
+
- The voice front registers as a hidden primary agent (`voice-front`); OpenCode lists it with its hidden agents.
|
|
2287
2362
|
- A catalog change still sends every machine's full catalog to each stream (about 1 KB per few sessions); the
|
|
2288
2363
|
replay buffer is in memory (a hub restart means a full catalog + `resync`).
|
|
2289
2364
|
- Running shells are seeded from the spoke's own server (`shell.list` for the 20 most recent session
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huynle/hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Hub: a harness-agnostic central hub your AI agent harnesses connect to (OpenCode first). Follow, steer and stop sessions from your phone or browser, with notifications, optional hands-free Voice and a self-hostable server.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Huy Le",
|
|
@@ -71,7 +71,8 @@
|
|
|
71
71
|
"packages/codex/package.json",
|
|
72
72
|
"bin",
|
|
73
73
|
"web/dist",
|
|
74
|
-
"
|
|
74
|
+
"packages/voice-client/src/vad/LICENSE",
|
|
75
|
+
"packages/voice-client/src/turn/LICENSE",
|
|
75
76
|
"docker",
|
|
76
77
|
"Dockerfile",
|
|
77
78
|
"scripts/hub-local.sh",
|
|
@@ -98,7 +99,8 @@
|
|
|
98
99
|
"packages/claude-code",
|
|
99
100
|
"packages/conformance",
|
|
100
101
|
"packages/preset",
|
|
101
|
-
"packages/codex"
|
|
102
|
+
"packages/codex",
|
|
103
|
+
"packages/voice-client"
|
|
102
104
|
],
|
|
103
105
|
"engines": {
|
|
104
106
|
"bun": ">=1.4.4"
|
|
@@ -108,8 +110,9 @@
|
|
|
108
110
|
},
|
|
109
111
|
"scripts": {
|
|
110
112
|
"test": "bun test",
|
|
111
|
-
"typecheck": "tsc --noEmit && tsc --noEmit -p web",
|
|
113
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p web && tsc --noEmit -p packages/voice-client",
|
|
112
114
|
"build:web": "vite build --config web/vite.config.ts",
|
|
115
|
+
"build:voice-client": "vite build --config packages/voice-client/vite.config.ts",
|
|
113
116
|
"build:cli": "bun build packages/cli/src/main.ts --target=node --outfile=packages/cli/dist/hub-cli.js",
|
|
114
117
|
"dev:web": "vite --config web/vite.config.ts",
|
|
115
118
|
"test:e2e": "bash scripts/test-e2e.sh",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huynle/hub-claude-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "The Claude Code hooks spoke: per-user daemon, hook bridge, plugin dir, `hub claude` wrapper and daemon-backed MCP server (internal; shipped inside @huynle/hub, not published on its own).",
|
|
6
6
|
"license": "MIT",
|