@huynle/hub 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +20 -4
  3. package/package.json +1 -1
  4. package/packages/claude-code/package.json +1 -1
  5. package/packages/cli/dist/hub-cli.js +625 -395
  6. package/packages/cli/package.json +1 -1
  7. package/packages/cli/src/grants-mcp.ts +65 -0
  8. package/packages/cli/src/mcp-operations.ts +49 -0
  9. package/packages/cli/src/mcp.ts +6 -0
  10. package/packages/cli/src/subscriptions-mcp.ts +76 -0
  11. package/packages/cli/src/teams-mcp.ts +5 -3
  12. package/packages/codex/package.json +1 -1
  13. package/packages/node/package.json +1 -1
  14. package/packages/preset/package.json +1 -1
  15. package/packages/protocol/package.json +1 -1
  16. package/packages/protocol/src/index.ts +13 -1
  17. package/packages/protocol/src/teams.ts +4 -0
  18. package/packages/protocol/src/voice-recording.ts +39 -2
  19. package/packages/runtime-openshell/image/entry.sh +3 -1
  20. package/packages/runtime-openshell/image/start.sh +3 -1
  21. package/packages/runtime-openshell/package.json +1 -1
  22. package/packages/runtime-sdk/package.json +1 -1
  23. package/packages/sdk/package.json +1 -1
  24. package/packages/sdk/src/client.ts +13 -0
  25. package/packages/sdk/src/teams.ts +2 -0
  26. package/packages/spoke-sdk/package.json +1 -1
  27. package/src/central/agent/decisions.ts +7 -1
  28. package/src/central/agent/delivery.ts +39 -9
  29. package/src/central/agent/grant-routes.ts +14 -1
  30. package/src/central/agent/grants.ts +23 -0
  31. package/src/central/agent/launch-engine.ts +3 -3
  32. package/src/central/agent/message-limits.ts +3 -0
  33. package/src/central/agent/routes.ts +11 -0
  34. package/src/central/agent/subscriptions.ts +3 -1
  35. package/src/central/config.ts +9 -2
  36. package/src/central/http.ts +135 -24
  37. package/src/central/recording/export.ts +57 -3
  38. package/src/central/recording/index.ts +234 -16
  39. package/src/central/recording/ingest.ts +11 -2
  40. package/src/central/recording/routes.ts +61 -7
  41. package/src/central/recording/schema.ts +66 -2
  42. package/src/central/recording/settings.ts +19 -5
  43. package/src/central/recording/store.ts +19 -0
  44. package/src/central/recording/taps.ts +45 -1
  45. package/src/central/speech/openai.ts +4 -4
  46. package/src/central/speech/openrouter.ts +4 -4
  47. package/src/central/speech/provider.ts +32 -1
  48. package/src/central/speech/service.ts +6 -5
  49. package/src/central/teams/hop-stalls.ts +72 -0
  50. package/src/central/teams/policy.ts +10 -0
  51. package/src/central/teams/routes.ts +2 -0
  52. package/src/central/teams/seats.ts +3 -3
  53. package/src/central/teams/staffing.ts +5 -0
  54. package/src/central/teams/store.ts +5 -2
  55. package/src/central/teams/validation.ts +8 -5
  56. package/src/spoke/spoke.ts +19 -0
  57. package/web/dist/assets/{ArtifactPage-Bz992ZX6.js → ArtifactPage-C5MMqXLN.js} +1 -1
  58. package/web/dist/assets/{ArtifactPane-BJZNixJC.js → ArtifactPane-CXSCmhyJ.js} +1 -1
  59. package/web/dist/assets/{ArtifactWorkbench-Dti3VP07.js → ArtifactWorkbench-S0RxNGWy.js} +1 -1
  60. package/web/dist/assets/{Diagram-B4HdUxw0.js → Diagram-BGmyRb-R.js} +1 -1
  61. package/web/dist/assets/{Library-CpzD2HDW.js → Library-CFmMbXEa.js} +2 -2
  62. package/web/dist/assets/{ProfilesDestination-DlQeogea.js → ProfilesDestination-7jpOc2HM.js} +1 -1
  63. package/web/dist/assets/{TeamsPage-4xdWCaI-.js → TeamsPage-C1NGrUR-.js} +1 -1
  64. package/web/dist/assets/{Watchtower-CpQdIRns.js → Watchtower-DnvlJaU0.js} +2 -2
  65. package/web/dist/assets/{artifacts-But87Jfo.js → artifacts-DlQEYmDl.js} +1 -1
  66. package/web/dist/assets/index-Da8SKXMw.js +33 -0
  67. package/web/dist/assets/{index-BC0O1gfR.css → index-Dyq-jBeE.css} +1 -1
  68. package/web/dist/assets/live-BmqLASSo.js +5 -0
  69. package/web/dist/assets/{pane-layout-BW0RdX5a.js → pane-layout-BAuu_pA3.js} +1 -1
  70. package/web/dist/assets/{renderer2d-IaJiloPi.js → renderer2d-B2Nh39UN.js} +1 -1
  71. package/web/dist/assets/{renderer3d-ZiD1uKGW.js → renderer3d-CTYbljHH.js} +1 -1
  72. package/web/dist/assets/{safe-area-C7IAlkgY.js → safe-area-CkhEclGo.js} +1 -1
  73. package/web/dist/assets/{silero-worker-e802lB5T.js → silero-worker-CC2etKS3.js} +1 -1
  74. package/web/dist/assets/{source-C-De8L3s.js → source-GYayxUxS.js} +1 -1
  75. package/web/dist/assets/{toast-CXTwTvDw.js → toast-q818mjUF.js} +1 -1
  76. package/web/dist/index.html +5 -5
  77. package/web/dist/assets/index-BS6lA6fN.js +0 -33
  78. package/web/dist/assets/live-CVUFIdUX.js +0 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,83 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ Voice recording's Developer mode and the richer `capture.v2` recording events (below), plus the fixes and team
6
+ tools merged since 0.10.1: a launch's `end.cleanup: keep` is honoured when an ended host launch is reconciled, and
7
+ the applied cleanup policy is stored before a launch ends, so kept, `needs_user` and lost workspaces are no longer
8
+ deleted later; a per-team agent message hop limit with a user-only Renew Decision when a team chain stalls (and
9
+ orphaned stall claims expire); team member names show without graph interactions; holder-scoped grant tools and
10
+ read-only subscription, session-model and machine/node tools over MCP; the console's Launch new member form sets
11
+ the model, end policy and timeout; the sandbox entry publishes its PID atomically, so a second start no longer
12
+ starts a duplicate harness. CI cancels superseded test runs without interrupting an image release and can reuse a
13
+ PR's passing result for the same tree.
14
+
15
+ ### Voice recording (developer)
16
+ - **Developer mode** (Settings → Voice, per user, off by default; user request, 2026-11-10). One more field of the
17
+ recording settings record (`developer`, the same revision and `PUT /api/settings/recording {developer}`): on turns
18
+ recording on, recording off turns it off, older consoles' `{enabled}` keep it. While a developer recording runs it
19
+ also keeps, behind the new hub feature `dev.v1` (start answer `developer: true`):
20
+ - **raw microphone audio clips** (3 s before to 2 s after) around near-misses and marks, from a ring buffer of the
21
+ detector's input (`audio.clip`, `PUT/GET /api/recordings/:id/clips/:n`, ≤ 200 per recording);
22
+ - the detector's **frame traces** around speech starts, ends and near-misses (`vad.trace`);
23
+ - **device and network facts** on start and on change (`device.info`: audio devices, mic track processing,
24
+ Network Information, visibility, wake lock, battery), **Resource Timing** of the voice routes
25
+ (`resource.timing`) and **console errors and warnings** (`console.log`, scrubbed, rate-limited);
26
+ - hub side: per turn the **model, provider, agent and context token counts** and each tool call's start and end
27
+ (`turn.detail`; never prompt text), **request lines** of the voice routes (`request`), and the **reply audio**
28
+ kept from `/api/speak` as it streams (`reply.audio`, `GET /api/recordings/:id/replies/:n`).
29
+ The extras count against the same caps; their audio stops first (50 MiB below the user's cap, 15 MiB below the
30
+ recording's; `cap.reached {what: "extras"}`), segments and events go on. Exports add `clips/` and `replies/`,
31
+ listed in the manifest. Turning Developer mode off stops the extras at once. The indicator says
32
+ "● Recording · developer"; `scripts/voice-eval.ts` reports models per turn, devices, routes, clips and errors.
33
+ - **More of what happens is recorded**, so "I talked and nothing happened" and lost words leave a trace. New console
34
+ events, sent only to a hub whose recording start lists the new feature `capture.v2` (an older hub refuses a whole
35
+ batch with an unknown kind, value or field, so toward it the console drops them before they take a `seq`):
36
+ - `vad.near`: what the speech detector almost took for speech: an onset abandoned before it started
37
+ (`short-onset`, `voiced-ratio`) or ≥ 150 ms just under the start threshold (`below-threshold`), with its peak, the
38
+ threshold that applied and whether the echo guard was up (the agent speaking). At most one per 500 ms of audio
39
+ and 20 per minute (the rest are counted in the next summary).
40
+ - `vad.summary`: every 10 s of listening, the detector's frames, max and p90 speech probability, loudest frame,
41
+ noise floor, frames over the start and echo-guard thresholds, time under the echo guard, mode and sensitivity
42
+ (quiet windows after a quiet summary are skipped).
43
+ - `stt.received` adds `junkReason` (why a transcript was dropped as noise), `held` (the text held at that moment)
44
+ and `speechMs`.
45
+ - `stt.slow`: a transcription still unanswered after 2 s (once per segment).
46
+ - `mark`: a bookmark. Say **"mark"**, **"mark that"** or **"bookmark"** alone (not sent to the agent; the held
47
+ text waits on), or press the Voice bar's new **Mark** button (shown only while recording on such a hub). The
48
+ Voice bar notes "Marked".
49
+ - The hub's `stt.result` adds the provider's `httpStatus`, `responseBytes` and `headersMs` (time to the response's
50
+ headers; `providerMs` stays the total).
51
+ - **`scripts/voice-eval.ts`** reads a recording export (zip or folder) and reports, across all its recordings,
52
+ per-turn latency by stage (median / p90 / max, by recording kind and device), dropped segments with their reason
53
+ and held text, early sends, STT stalls over 3 s with the provider's share, near-misses with the echo-guard share
54
+ and marks with ±15 s of context (`--json`, `--no-text`). Older exports work: what they lack is reported as not
55
+ recorded.
56
+
57
+ ## 0.10.1
58
+
59
+ Clicking the composer's Model chip changes the session's model. No migration is needed. Machines need the new
60
+ plugin (and an OpenCode restart) for the chip to become clickable; with older plugins it stays display only.
61
+
62
+ ### Console
63
+ - **The composer's Model chip changes the session's model** (user decision, 2026-11-10: "yes, it should change the
64
+ session model"). Clicking (or Enter on) "Model: …" opens the same picker as the Voice model (search over the
65
+ session machine's catalog, the variant step, arrows, Enter and 1–9); a pick sets the **session's own model**, so its
66
+ next turn and every later one run on it, whether the prompt comes from the hub console, the OpenCode TUI or a
67
+ Voice relay. The chip then shows it with source "chosen" (the picker marks it). The Voice chip and its picker are
68
+ unchanged, and a Voice pick still never changes the session's model.
69
+ - Mechanism: OpenCode 2.x's `session.switchModel`, which stores the model on the session (the same call the TUI
70
+ makes when you pick a model there); the runner uses it for every turn of that session. Last writer wins: a model
71
+ you then pick in the TUI's own model dialog replaces it at the TUI's next prompt. OpenCode has no way to go back
72
+ to "no model chosen", so the picker has no Default entry: pick the default model itself.
73
+ - New console route `PUT /api/sessions/:id/model {model: {providerID, modelID, variant?}}` → `{model}` (the
74
+ session's model now). The same access as sending a prompt: the session's owner only (foreign or unknown
75
+ sessions 404, session/app tokens and grant-holders 403, as for every console route). 400 for a malformed body or
76
+ a model or variant the session's catalog lacks, 409 `unsupported` when its spoke is older. Audited as
77
+ `session.model`.
78
+ - Protocol (additive): spoke op `setModel {sessionID, model}` → `{model: SessionModel}`, announced as the new spoke
79
+ feature `setModel`. Older spokes (and hubs that list no features) keep the chip display only.
80
+
3
81
  ## 0.10.0
4
82
 
5
83
  One release for everything since 0.9.2: the production-testing fixes, the voice update (Voice front on by default,
package/README.md CHANGED
@@ -312,7 +312,7 @@ The hub serves the web app, pairs devices, enrolls machines, and does push and (
312
312
  |---|---|---|
313
313
  | **`/hub` → On this computer** | Local use; nothing to install (plugin from npm) | started by the plugin with OpenCode's own runtime (`BUN_BE_BUN=1 opencode bin/hub.ts`), or `bun` when installed |
314
314
  | **Standalone binary** | Any server or laptop, no runtime needed | `hub-<os>-<arch>` from the [releases](https://github.com/huynle/hub-releases/releases) (the source repository is private; releases are mirrored to the public `huynle/hub-releases`) |
315
- | **Docker** | Servers | `ghcr.io/huynle/hub` (linux/amd64, linux/arm64) |
315
+ | **Docker** | Servers | `ghcr.io/huynle/hub` (linux/amd64, linux/arm64 for `v*` releases and `latest`; per-commit `git-…` tags from `main` are linux/amd64 only) |
316
316
  | **bunx / from source** | Bun users, development | `bunx --package @huynle/hub hub-server` · `bun bin/hub.ts` |
317
317
  | **Kubernetes (cluster mode)** | Several pods on Postgres + Valkey, rolling updates | [docs/cluster.md](docs/cluster.md), manifests in [`deploy/k8s/`](deploy/k8s/) |
318
318
 
@@ -419,6 +419,7 @@ conversation mode and utterances (403 `not_exposed` for sessions not shared with
419
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) |
420
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) |
421
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) |
422
+ | `PUT /api/sessions/:id/model {model: {providerID, modelID, variant?}}` | (feature `setModel`; console only, same access as a prompt: the session's owner) sets the session's own model: its next turn and every later one run on it, whoever prompts (the console, the TUI, a Voice relay). The spoke calls OpenCode's `session.switchModel`, which stores it on the session. Answers `{model}` (the summary's `model`, source `chosen`). 400 malformed or not in the session's catalog (`GET …/models`), 409 `unsupported` from an older spoke. Audited `session.model`. The Voice model (`…/model-prefs`) is separate |
422
423
  | `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
424
  | `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
425
  | `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 |
@@ -818,6 +819,13 @@ the requester and may only be the requester itself or a reserved role it holds.
818
819
  the grant; approving creates it as yours, and the requester is told the grant id in a reply. One open request per
819
820
  requester, 5 an hour.
820
821
 
822
+ `hub mcp` exposes `grant_request` (the same request route and limits), `grant_list`, `grant_show` and
823
+ `grant_revoke` (grants held by the calling session or app; apps need `messages:read` to list/show and
824
+ `messages:send` to revoke), and
825
+ `grant_create` (user device credential only; an agent must request and wait for your tap). A user credential on
826
+ the holder-scoped tools has no holder; manage all grants in the console or with `hub grant` instead. Pinning a
827
+ profile as an app also requires a `prompt` grant on the target session, not only `profiles:assign`.
828
+
821
829
  **Prompts.** An agent's prompt reaches the session as the hub's, never as yours: the spoke wraps it in a provenance
822
830
  envelope (`[hub] prompt from @lead · grant g_… · msg_…`, the body between boundary lines) and admits it as a hub
823
831
  message, so it never resets the loop protection's hop chain the way your own typing does. Agents may prompt root
@@ -1101,7 +1109,12 @@ required and absolute; `mode`, `branch`, `base`, `remote`, `key`, `name`, `roles
1101
1109
  `cleanup`), `launches` (`state`, `label`), `launch_show` and `launch_stop` (`id`); the profile tools (`profiles`,
1102
1110
  `profile_show`, `profile_create`, `profile_version_create`, `profile_skill_set`, `profile_command_set`,
1103
1111
  `profile_assign`, …; [docs/profiles.md](docs/profiles.md#sdk-and-mcp)), the workspace, artifact and team tools
1104
- ([docs/teams.md](docs/teams.md#mcp-tools); `team_staff` takes the nested launch spec), with the same arguments
1112
+ ([docs/teams.md](docs/teams.md#mcp-tools); `team_staff` takes the nested launch spec), subscription tools
1113
+ (`subscribe`, `subscriptions`, `unsubscribe`; [docs/subscriptions.md](docs/subscriptions.md#surfaces)),
1114
+ the grant tools (`grant_request`, `grant_list`, `grant_show`, `grant_revoke`, `grant_create`;
1115
+ [docs/multi-user.md](docs/multi-user.md#grants-tenancy)), `session_model_set` (user device credential only, same
1116
+ catalog checks as the console), and read-only `machines` and `launch_nodes` (user device credential only; connected
1117
+ node roots, slots, runtimes and labels; [docs/mcp-operations.md](docs/mcp-operations.md)), with the same arguments
1105
1118
  (`to`, `text`, `addr`, `id`, `requestID`, `formID`, `why`, `n`, `wake`, `urgent`, `all`, `project`, `role`, …). The act
1106
1119
  tools need an app token with scope `sessions:act` and a grant, the launch tools scope `sessions:launch` and a
1107
1120
  launch grant; `needs_user` results include the evaluation. `ask` takes `wait` of at
@@ -1469,7 +1482,8 @@ the Docker image, npm package and binaries ship the build). One store fed by the
1469
1482
  recording cancels it; each session keeps its own unsent draft), Voice mode (hands-free, see below; a toggle that
1470
1483
  turns Voice on first when needed; one session at a time, so turning it on elsewhere turns the first one off; it
1471
1484
  keeps running while you look at other sessions, which show a "Voice: <session>" pill with End) with a
1472
- microphone picker, and the Voice model menu (the voice front's model). The right rail (a bottom sheet on phones) has
1485
+ microphone picker, the Voice model menu (the voice front's model) and the Model chip (the session's own model; where
1486
+ the machine's plugin serves `setModel` it opens the same menu and sets the model every later turn runs on). The right rail (a bottom sheet on phones) has
1473
1487
  cards that collapse from their header (remembered per device): **Environment** (machine with
1474
1488
  online/offline/Connecting, project folder with the full path as tooltip, branch, and where it is open:
1475
1489
  TUI, Herdr or background), Context, **Connectors · N connected** (MCP servers) and Changes with a diff
@@ -1902,7 +1916,9 @@ The loop is the package `packages/voice-client` (a `VoiceTransport` over the con
1902
1916
  `voiceTurn.fallback`, `voiceTurn.slow` as URL query parameters (one visit) or localStorage keys. The recording
1903
1917
  mode records each score as `turn.predict` ([docs/voice-recording.md](docs/voice-recording.md)).
1904
1918
  - **Commands**: an utterance that is only "stop", "cancel", "never mind", "stop that" or "hold on"
1905
- calls `POST …/interrupt` and stops speech. "Pause listening" turns the mode off.
1919
+ calls `POST …/interrupt` and stops speech. "Pause listening" turns the mode off. While the recording mode records
1920
+ (on a hub with its `capture.v2` feature), "mark", "mark that" or "bookmark" alone bookmarks the recording instead
1921
+ of reaching the agent ([docs/voice-recording.md](docs/voice-recording.md)).
1906
1922
  - **Speech out**: root replies arrive one sentence at a time while they are written (`say` with `seq`);
1907
1923
  the phone plays them in `seq` order per text part and drops duplicates (`SayAssembler`). A `say` without
1908
1924
  `seq` (a subagent's result) is split into sentences. Abbreviations, decimals, file names and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huynle/hub",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huynle/hub-claude-code",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",