@pellux/goodvibes-daemon 1.28.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 (113) hide show
  1. package/CHANGELOG.md +383 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/bin/goodvibes-daemon +100 -0
  5. package/bin/launcher-support.js +226 -0
  6. package/package.json +96 -0
  7. package/scripts/check-bun.sh +20 -0
  8. package/scripts/postinstall.js +244 -0
  9. package/src/cli/command-catalog.ts +828 -0
  10. package/src/cli/completion.ts +299 -0
  11. package/src/cli/help.ts +167 -0
  12. package/src/cli/index.ts +21 -0
  13. package/src/cli/parser.ts +55 -0
  14. package/src/cli/surface-catalog.ts +26 -0
  15. package/src/cli/types.ts +63 -0
  16. package/src/cluster/daemon-ws-call.ts +235 -0
  17. package/src/cluster/raw-reply-route.ts +111 -0
  18. package/src/config/checkpoint-settings.ts +113 -0
  19. package/src/config/run-daemon-config-migration.ts +47 -0
  20. package/src/config/secret-config.ts +175 -0
  21. package/src/config/secrets.ts +71 -0
  22. package/src/config/surface.ts +24 -0
  23. package/src/core/pairing-banner.ts +82 -0
  24. package/src/daemon/cli.ts +878 -0
  25. package/src/daemon/config-command.ts +281 -0
  26. package/src/daemon/handlers/context.ts +29 -0
  27. package/src/daemon/handlers/contracts.ts +43 -0
  28. package/src/daemon/handlers/credentials.ts +139 -0
  29. package/src/daemon/handlers/drafts/draft-store.ts +427 -0
  30. package/src/daemon/handlers/drafts/index.ts +17 -0
  31. package/src/daemon/handlers/drafts/register.ts +331 -0
  32. package/src/daemon/handlers/errors.ts +18 -0
  33. package/src/daemon/handlers/inbox/aggregator.ts +375 -0
  34. package/src/daemon/handlers/inbox/cursor-store.ts +512 -0
  35. package/src/daemon/handlers/inbox/index.ts +221 -0
  36. package/src/daemon/handlers/inbox/mapping.ts +192 -0
  37. package/src/daemon/handlers/inbox/poller.ts +239 -0
  38. package/src/daemon/handlers/inbox/provider-adapter.ts +171 -0
  39. package/src/daemon/handlers/inbox/providers/discord.ts +276 -0
  40. package/src/daemon/handlers/inbox/providers/email.ts +176 -0
  41. package/src/daemon/handlers/inbox/providers/imap-client.ts +300 -0
  42. package/src/daemon/handlers/inbox/providers/route-util.ts +24 -0
  43. package/src/daemon/handlers/inbox/providers/slack.ts +287 -0
  44. package/src/daemon/handlers/index.ts +117 -0
  45. package/src/daemon/handlers/register.ts +180 -0
  46. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +143 -0
  47. package/src/daemon/handlers/remote/backends/docker.ts +79 -0
  48. package/src/daemon/handlers/remote/backends/index.ts +40 -0
  49. package/src/daemon/handlers/remote/backends/local-process.ts +113 -0
  50. package/src/daemon/handlers/remote/backends/process-runner.ts +127 -0
  51. package/src/daemon/handlers/remote/backends/ssh.ts +126 -0
  52. package/src/daemon/handlers/remote/backends/types.ts +97 -0
  53. package/src/daemon/handlers/remote/dispatcher.ts +181 -0
  54. package/src/daemon/handlers/remote/index.ts +120 -0
  55. package/src/daemon/handlers/remote/peer-registry.ts +357 -0
  56. package/src/daemon/handlers/remote/service.ts +191 -0
  57. package/src/daemon/handlers/routing/inbox-bridge.ts +71 -0
  58. package/src/daemon/handlers/routing/index.ts +261 -0
  59. package/src/daemon/handlers/routing/route-store.ts +319 -0
  60. package/src/daemon/handlers/routing/routing-resolver.ts +75 -0
  61. package/src/daemon/handlers/sqlite-store.ts +303 -0
  62. package/src/daemon/handlers/triage/index.ts +57 -0
  63. package/src/daemon/handlers/triage/integration.ts +213 -0
  64. package/src/daemon/handlers/triage/pipeline.ts +274 -0
  65. package/src/daemon/handlers/triage/scorer.ts +287 -0
  66. package/src/daemon/handlers/triage/tagger/discord.ts +187 -0
  67. package/src/daemon/handlers/triage/tagger/imap.ts +384 -0
  68. package/src/daemon/handlers/triage/tagger/index.ts +184 -0
  69. package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
  70. package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
  71. package/src/daemon/handlers/triage/types.ts +50 -0
  72. package/src/daemon/lifecycle.ts +41 -0
  73. package/src/daemon/local-daemon-state.ts +233 -0
  74. package/src/daemon/pair-command.ts +301 -0
  75. package/src/daemon/provision-wake-model.ts +81 -0
  76. package/src/daemon/send/channels.ts +200 -0
  77. package/src/daemon/send/command.ts +333 -0
  78. package/src/daemon/send/composition.ts +100 -0
  79. package/src/daemon/send/failure-text.ts +93 -0
  80. package/src/daemon/send/inert-text.ts +225 -0
  81. package/src/daemon/send/stdin.ts +24 -0
  82. package/src/daemon/service-commands.ts +530 -0
  83. package/src/daemon/sessions-command.ts +209 -0
  84. package/src/daemon/status-command.ts +481 -0
  85. package/src/daemon/webui-command.ts +339 -0
  86. package/src/runtime/boot-tasks.ts +110 -0
  87. package/src/runtime/cluster-composition.ts +124 -0
  88. package/src/runtime/cluster-group-composition.ts +284 -0
  89. package/src/runtime/conversation-rewind-port.ts +171 -0
  90. package/src/runtime/credential-composition.ts +54 -0
  91. package/src/runtime/daemon-handler-composition.ts +76 -0
  92. package/src/runtime/device-posture-composition.ts +115 -0
  93. package/src/runtime/disposal-wiring.ts +101 -0
  94. package/src/runtime/fleet-needs-input-push.ts +61 -0
  95. package/src/runtime/fleet-services.ts +41 -0
  96. package/src/runtime/hosted-session-composition.ts +128 -0
  97. package/src/runtime/index.ts +100 -0
  98. package/src/runtime/knowledge-services.ts +101 -0
  99. package/src/runtime/legacy-daemon-migration.ts +605 -0
  100. package/src/runtime/legacy-daemon-reconcile.ts +448 -0
  101. package/src/runtime/mail-composition.ts +65 -0
  102. package/src/runtime/notification-dispatch.ts +86 -0
  103. package/src/runtime/plugin-composition.ts +111 -0
  104. package/src/runtime/runtime-services-types.ts +268 -0
  105. package/src/runtime/services.ts +756 -0
  106. package/src/runtime/trigger-services.ts +62 -0
  107. package/src/runtime/trust/checkpoint-eligibility.ts +138 -0
  108. package/src/runtime/trust/trust-gated-approvals.ts +169 -0
  109. package/src/runtime/update-check.ts +61 -0
  110. package/src/runtime/workspace-checkpointing.ts +116 -0
  111. package/src/testing/daemon-fixture.ts +276 -0
  112. package/src/testing/hosted-session-failures.ts +92 -0
  113. package/src/version.ts +26 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,383 @@
1
+ # Changelog
2
+
3
+ All notable changes to the GoodVibes daemon.
4
+
5
+ ---
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Changes
10
+
11
+ - `pair --host <name>` now reaches a DIFFERENT daemon instead of being refused.
12
+ It asks that daemon to mint a brand-new per-device pairing token over
13
+ `pairing.handoff.create` and prints the pairing block for it — a different
14
+ act than the plain `pair` reprint, which still just reprints this machine's
15
+ existing shared token and never mints. Because it changes state on a daemon
16
+ that may not be this process's own, it states the plan and asks for
17
+ confirmation first; `-y`/`--yes` answers non-interactively, same as
18
+ `migrate-service`. An unreachable daemon, a rejected token, and a daemon too
19
+ old to serve the verb are each refused by name, never a stack trace, and a
20
+ target with no web origin configured still gets its token and fragment
21
+ printed honestly rather than a fabricated link.
22
+ - The unified inbox is served. `channels.inbox.list` has had a handler in this
23
+ repository for a while and no client could reach it: the SDK descriptor
24
+ carried `invokable: false`, so the method-dispatch endpoint refused the call
25
+ before the handler was consulted, and `GET /api/channels/inbox` was in no
26
+ route table. The agent's inbox asked on every refresh and wrote down
27
+ `method_unavailable` every time. It answers now, over both the gateway invoke
28
+ and the advertised REST path.
29
+
30
+ What a client gets is one merged timeline, newest first, across every
31
+ provider — items interleave by arrival rather than being grouped, and each
32
+ carries its own `provider`, so an inbox reads like an inbox. Pages are bounded
33
+ and walked with an opaque `nextCursor`; `cursor` stays what it was, the
34
+ freshness watermark you hand back as `since`. That is a keyset, not an offset:
35
+ the feed is written to while it is read, and an offset page re-anchors on
36
+ every insert, so a caller walking pages during a poll would see items twice
37
+ and miss others.
38
+
39
+ The answer is served from this daemon's SYNCED MIRROR — the sqlite store the
40
+ Slack, Discord and IMAP adapters already write into on their own cadences —
41
+ and not from a fresh remote fetch per call. Four reasons, all of them about
42
+ what a fetch-per-call would cost: a third-party rate limit would sit behind a
43
+ read verb any client may call at any rate; the cluster hands FETCHING for each
44
+ inbox account to one elected node, and a read that fetched would make every
45
+ standby fetch too, which is the double-read the election exists to prevent;
46
+ triage scores are applied as items are persisted, so inline-fetched items
47
+ would come back unscored and the verb would answer two shapes depending on
48
+ timing; and a provider outage would turn a read into a hang instead of an
49
+ answer.
50
+
51
+ The price of serving a mirror is that its age is invisible in the items, so it
52
+ is not left implicit. Every call reports `providers`: one entry per provider
53
+ this daemon knows about, whether or not it contributed anything, with its
54
+ state, when it last synced, how much of the mirror is its, and whether this
55
+ node is the one fetching it. `ready`, `empty`, `unconfigured`, `error` and
56
+ `pending` are five different things, and a caller does something different
57
+ about each — a fresh install with no tokens is not an outage, and a node that
58
+ has not looked yet is not a node reporting an empty inbox. A provider whose
59
+ sync failed contributes no items, says why, and sets `partial`, so a short
60
+ list is never mistaken for a quiet week. Nothing configured is an empty list
61
+ with three unconfigured statuses, not an error: the verb is callable in every
62
+ state.
63
+
64
+ - Two saves of one daemon SQLite store in the same millisecond raced. The temp
65
+ filename was `<path>.<pid>.<Date.now()>.tmp`, so both writes picked the same
66
+ path, the first rename moved it away, and the second failed with ENOENT on a
67
+ file it had just written. Not hypothetical: the inbox poller flushes once per
68
+ provider and polls every provider concurrently, so an ordinary two-provider
69
+ startup hit it — and once `channels.inbox.list` began reporting per-provider
70
+ health, the failure showed up as a provider reporting a filesystem error for
71
+ its feed. The temp name now carries a per-process counter. Every store on
72
+ `HandlerSqliteStore` shared the hazard, so the fix is there.
73
+
74
+ - A gateway invocation that carries no context no longer throws a TypeError out
75
+ of the handler wrapper. `normalizeContext` read `.metadata` off the context
76
+ unconditionally, and an in-process invoke that builds the invocation by hand
77
+ can omit it; an absent context now reads as the empty one — no principal, no
78
+ scopes, not admin, nobody claiming a person asked — which can only cost a
79
+ caller an authorization it never proved, never grant one.
80
+
81
+ - Three config modules the terminal app carried a byte-identical copy of are the
82
+ SDK's: `config/goodvibes-home.ts` (tree-root and daemon-home resolution),
83
+ `config/provider-model.ts` (`provider:model` parsing) and `config/index.ts`
84
+ (a barrel over the SDK plus derivation helpers, all of which moved). Every
85
+ importer here reads them from `@pellux/goodvibes-sdk/platform/config` and
86
+ `.../platform/providers`.
87
+
88
+ - `config set <key> <value>` reads its value with the terminal shell's
89
+ `parseConfigValueText`. `src/cli/config-value.ts` held a byte-identical copy
90
+ of that function and its `cli/index.ts` re-export is gone with it. The copy
91
+ existed because the shared one was private; it is exported now, and one
92
+ implementation is the whole point — `--config x=false` and `config set x
93
+ false` must write the same thing.
94
+
95
+ - The local `sql.js` ambient declaration is gone. The SDK ships the declaration
96
+ now, and `daemon/handlers/sqlite-store.ts` picks it up with
97
+ `/// <reference types="@pellux/goodvibes-sdk/sql-js" />`.
98
+
99
+ - The test helpers this repo and the terminal app both carry now say so in a
100
+ header. They are byte-identical on purpose: each binds to its own repo's
101
+ working tree, source layout and Bun test lifecycle, so a shared home would
102
+ mean inventing a test-only published package rather than hoisting anything.
103
+
104
+ ---
105
+
106
+ ## [1.28.0] - 2026-07-30
107
+
108
+ ### Changes
109
+
110
+ - Hosted third-party coding agents (Claude Code, Codex CLI, opencode) now work
111
+ over this daemon's own gateway. `acp.agents.list` (read-only discovery of
112
+ installed agents) and `acp.sessions.create` (spawn one as a long-lived
113
+ session) were cataloged and advertised as callable on every build, and
114
+ answered nothing: the composition never constructed the ACP host they are
115
+ handlers for. `runtime/services.ts` now builds that host — permission asks
116
+ from a hosted agent route through the same shared approval broker every
117
+ other confirmation rides, and each hosted agent registers onto a shared
118
+ session so it is attachable and steerable like any native one — and threads
119
+ it into the gateway registration and the fleet registry, so a hosted agent
120
+ also shows up as a fleet row.
121
+
122
+ - The command line is the daemon's, and it is an operator surface rather than a
123
+ way to start a process. It shipped carrying the terminal app's parser: a table
124
+ of two dozen command words — `tui`, `run`, `doctor`, `models`, `providers`,
125
+ `auth`, `secrets`, `plugin` — against an entry point that dispatched on help,
126
+ version and four service verbs. Everything else fell through to "start a
127
+ daemon in the foreground", so `goodvibes-daemon status` served, and so did
128
+ `goodvibes-daemon install-servce`. The parser's own unknown-command error was
129
+ unreachable, because no word could fail to match.
130
+
131
+ The vocabulary is now exactly this binary's real commands, held in one catalog
132
+ (`src/cli/command-catalog.ts`) that the parser, the help text and the shell
133
+ completions all read. Serving happens on a bare invocation or on `serve`, and
134
+ on nothing else; any other unrecognized word exits 2 with `Unknown command: X`
135
+ and the help. The terminal app's conversation flags — `--resume`, `--continue`,
136
+ `--fork`, `--print`, `--prompt`, `-o/--output`, `--open`, `--no-alt-screen`,
137
+ `--session`, `--strict` — were accepted in silence and read by nothing; each is
138
+ now refused by name and says which surface owns it.
139
+
140
+ - New commands, all of them things a headless box's operator previously had no
141
+ way to do:
142
+
143
+ - `status [--json]` asks a RUNNING daemon what it is doing: version, uptime,
144
+ the address it actually bound, its health roll-up, its channels, its place in
145
+ the cluster, how many sessions it is hosting, and what its last update or
146
+ rollback did. `--host`/`--port`/`--token` ask a daemon on another machine,
147
+ over the same convention `cluster` uses.
148
+ - `sessions list` and `sessions kill <id>` over the daemon's hosted-session
149
+ verbs, which are ws-only and so are reached over the control-plane socket
150
+ with the same operator token.
151
+ - `config list|get|set|unset` reads and writes this machine's settings
152
+ directly, so it works whether or not a daemon is running. Every value it
153
+ PRINTS goes through the redaction rules first — a token, a password or an API
154
+ key reads as `<redacted>` — while `config set` still writes the real value.
155
+ - `pair` prints the pairing link and QR again, from the same renderer the
156
+ daemon uses at startup and carrying the same existing token, so the block is
157
+ no longer lost when the boot banner scrolls away.
158
+ - `update [--check]` reports the running version, the receipts the daemon wrote
159
+ about its own swaps and restarts, the version an automatic rollback rejected,
160
+ and whether a rollback is in force. `--check` states plainly that no verb
161
+ exists to trigger an early check and names what does work, rather than
162
+ calling a verb the daemon does not answer.
163
+ - `start-service`, `stop-service` and `restart-service`, on the same service
164
+ manager `install-service` already used. A verb aimed at a service that is not
165
+ installed says so and exits 4 instead of dispatching a doomed platform call.
166
+ - `completion bash|zsh|fish`, generated from the catalog, and `help <command>`
167
+ for any command's own arguments and flags.
168
+
169
+ - `service-status` answers with an exit code — 0 installed and running, 3
170
+ installed but not running, 4 not installed — and takes `--json`. A script no
171
+ longer has to read the prose to find out.
172
+
173
+ - The help text describes the binary that exists: every command, the flags that
174
+ work, `-y/--yes`, `--config`, `--enable`/`--disable`, `--json`, the exit codes,
175
+ and a systemd user service, a launchd agent or a Scheduled Task depending on
176
+ the platform it is printed on — it used to say systemd on every platform,
177
+ including macOS, where `install-service` writes a launchd agent.
178
+
179
+ - `status` reads the daemon's identity, health and channel routes with the
180
+ envelope they actually use. Those routes answer with the payload itself while
181
+ the cluster routes wrap theirs in `{ ok, data }`, and the wrapped reader turned
182
+ a healthy 200 into "the daemon refused the request".
183
+
184
+ - Four never-called functions the terminal app left behind
185
+ (`applyTuiRuntimeConfigDefaults`, `applyConfiguredHitlMode`,
186
+ `applyRuntimeConfigDefault`, `applyRuntimeCommandEndpointFlagOverrides`) are
187
+ gone, verified to have no importers first.
188
+
189
+ - The daemon can run a conversation, not just watch one. Stating how a workspace
190
+ floor is built (`DaemonConfig.hostedSessions`, wired in
191
+ `runtime/hosted-session-composition.ts`) turns on the SDK's hosted-session
192
+ engine and its `sessions.hosted.create/attach/detach/kill/list` verbs: a full
193
+ loop composed inside this process — the same orchestrator, the same tool
194
+ registry rooted at the named workspace, the same permission machinery a
195
+ terminal runs. Driving one uses the verbs that already existed
196
+ (`sessions.steer`, `sessions.followUp`, `sessions.toolCalls.cancel`,
197
+ `sessions.queuedMessages.*`), and its streamed output rides the `turn` and
198
+ `tools` event domains stamped with the session id, so a client watches a
199
+ hosted turn exactly as it watches a local one.
200
+
201
+ What this daemon states is where a hosted run's asks are gated: the workspace
202
+ trust decision, scoped to the SESSION's workspace rather than the daemon's own
203
+ directory. An undecided workspace raises the trust question as an approval
204
+ record any attached surface can answer, a restricted one refuses non-read
205
+ categories without asking, and a daemon hosting three sessions in three
206
+ directories asks three separate questions.
207
+
208
+ Detaching is governed by `hostedSessions.detachPolicy`, which defaults to
209
+ `kill` — closing a client has always ended its work. `survive` opts into
210
+ sessions that outlive both the client and a restart of this daemon; a single
211
+ session may override the setting when it is created. `hostedSessions.maxSessions`
212
+ caps how many loops this machine holds at once, and the transcript bound and
213
+ retention window govern what a restart can restore.
214
+
215
+ A floor is also seeded with the local models this daemon discovered, so the
216
+ machine's own Ollama or LM Studio is routable inside a hosted session rather
217
+ than only for the daemon's own agents.
218
+
219
+ - `bun run smoke:hosted` drives the whole story against the COMPILED binary on an
220
+ isolated home and a high port: create, a real turn through `sessions.steer`
221
+ against a local stub model, attach with history, detach under both toggle
222
+ positions, a per-session override, reattach, kill, and the refusal a relative
223
+ workspace path earns.
224
+
225
+ - A paired phone is now reachable from a surface that is not this process. Binding the
226
+ gateway catalog to the device posture runtime already served the grants surface;
227
+ it now also serves `devices.capability.request` and `devices.artifacts.list`/`read`,
228
+ so a client with no device runtime of its own can ask for a photo, a screen capture,
229
+ a location fix, the clipboard, or a device command, and read the capture bytes back
230
+ by id. The runtime is handed over whole, deliberately: the verbs and the `phone`
231
+ tool must reach the same service, because a second path to a phone would be a second
232
+ place the confirmation prompt and the durable grants could be decided differently.
233
+ Nothing about the gates moved — the prompt still rides this daemon's shared approval
234
+ seam and appears wherever the person is looking.
235
+
236
+ - Conversation-scope rewind stopped answering for sessions it holds nothing for.
237
+ `conversation-rewind-port.ts` resolves a session's conversation from an in-process
238
+ registry, and while the conversation loops run in the surfaces that registry is
239
+ empty here. It reported "0 messages to drop" — the same answer a conversation
240
+ already at the anchor gives, so a caller could not tell a rewind that found nothing
241
+ from one that reached nobody. It now reports the anchor as unavailable with the
242
+ reason, which `rewind.plan` surfaces as a warning and `rewind.apply` records instead
243
+ of a truncation it never performed.
244
+
245
+ The surfaces reach conversation rewind a different way now: they offer their live
246
+ conversation over the control plane (`rewind.conversation.*`, served on this
247
+ catalog), and the daemon puts its question to whichever surface is actually running
248
+ the loop. This port is what that falls through to for sessions the daemon hosts
249
+ itself.
250
+
251
+ - The daemon is its own product. It was built and shipped out of the terminal app's repository,
252
+ which meant one repository held two programs with very different jobs and every daemon change
253
+ rode a terminal-app release. It now has its own repository, its own release line and its own
254
+ binary, and the terminal app and the agent become clients of it.
255
+
256
+ - The suite installer lives here now. `scripts/install.sh` — the script behind
257
+ `curl -fsSL https://goodvibes.sh/install.sh | sh` — moved out of the terminal app's
258
+ repository into this one, because the daemon is the product everything else is
259
+ installed alongside and this repository's release lane is the one that publishes it.
260
+ There is exactly one copy: two installers in two repositories is how two installers
261
+ drift apart.
262
+
263
+ It resolves a release tag per repository and verifies every file against that
264
+ repository's own SHA256SUMS.txt, so one curl still installs the whole suite: the
265
+ daemon and the sqlite-vec addon from here, the terminal app from `goodvibes-tui`,
266
+ the agent and its browser driver from `goodvibes-agent`, and the web UI bundle from
267
+ `goodvibes-webui`. `GOODVIBES_DAEMON_VERSION` pins the daemon's tag the way
268
+ `GOODVIBES_VERSION` and `GOODVIBES_AGENT_VERSION` already pinned theirs.
269
+
270
+ The installer itself now ships as a release asset of this repository, checksummed by
271
+ the same SHA256SUMS.txt as the binaries, and the release workflow publishes it to
272
+ goodvibes.sh. It used to say in its own header that it was "published to goodvibes.sh
273
+ on release" while nothing anywhere did that.
274
+
275
+ - **The browser operator surface installs with everything else.** It is not a fourth
276
+ binary and not a fourth service: the installer unpacks a checksum-verified bundle to
277
+ `~/.local/bin/webui/<version>` and the daemon serves it on its own listener, same
278
+ origin as the API. One curl now installs all four consumption paths.
279
+
280
+ It is served to THIS MACHINE ONLY by default. The daemon's shipped binding is
281
+ loopback and installing the web UI does not change it, so nothing new is exposed to
282
+ your network by installing. Reaching it from another device is a deliberate separate
283
+ act, and the install receipt prints both the URL and the one command that does it.
284
+
285
+ - **New: `goodvibes-daemon webui enable | disable | status`.** The command that owns
286
+ serving the web UI — which directory, whether it is served at all, and the honest
287
+ answer to "what URL do I open and who can reach it". `enable --bundle-dir <dir>`
288
+ refuses a directory with no index.html rather than pointing the daemon at something it
289
+ cannot serve; `--lan` is the one act that widens exposure and `--loopback` takes it
290
+ back; `status` says whether a configured bundle is still on disk.
291
+
292
+ The URL it reports is the control-plane origin, because that is the listener serving
293
+ the bundle. `web.port` is the surface's declared endpoint and nothing binds it, so
294
+ `enable` also replaces the shipped `web.publicBaseUrl` placeholder (`http://127.0.0.1:3423`)
295
+ with the origin that actually answers — leaving any value an operator chose alone, and
296
+ saying so when the two differ.
297
+
298
+ - The daemon updates itself from this repository. The platform default for
299
+ `update.releasesUrl` now names `mgd34msu/goodvibes-daemon`, so a daemon built from
300
+ this repository resolves its own release line without being configured. Asset names
301
+ are unchanged and the service unit's ExecStart is path-stable, so replacing the
302
+ binary at its installed path is all a version change takes.
303
+
304
+ **A daemon from the old repository cannot hand ITSELF over, and an earlier draft of
305
+ this entry said it could.** Every daemon shipped at 1.27.1 or below was compiled
306
+ against SDK 1.20.0, whose baked `update.releasesUrl` default names
307
+ `mgd34msu/goodvibes-tui`. That default is compiled in rather than persisted: no
308
+ settings file carries it and no migration rewrites it. The terminal repository no
309
+ longer builds daemon binaries, so those daemons resolve a release with no
310
+ `goodvibes-daemon-<os>-<arch>` asset and fail. Pointing them at this repository
311
+ instead does not rescue them either — their shipped updater adds the terminal binary
312
+ beside them to the same all-or-nothing download whenever `goodvibes` sits in the
313
+ install directory, which `scripts/install.sh` guarantees, and this repository
314
+ deliberately publishes no terminal binary. There is also no remote write path to
315
+ reconfigure them: the control plane's `config.set` verb exists as a catalog
316
+ descriptor with no handler registered behind it.
317
+
318
+ What performs the handover is the terminal, once, at launch: it reads the version of
319
+ the `goodvibes-daemon` binary installed beside it and, when that binary predates this
320
+ split, downloads the current daemon from this repository's releases,
321
+ checksum-verifies it against `SHA256SUMS.txt`, swaps it with the outgoing build kept
322
+ at `<path>.previous`, and restarts the service. It replaces that one file and nothing
323
+ around it. The mechanism is `src/runtime/daemon-handover.ts` in `goodvibes-tui`; it
324
+ reaches hosts with that product's next release, and until then a pre-split daemon is
325
+ moved across by re-running the installer.
326
+
327
+ Named rather than implied away: the SDK's `resolveDaemonInstalledFiles` still adds
328
+ the terminal binary to the daemon's OWN update target set when one sits beside it, so
329
+ on a three-binary install a daemon from this repository cannot yet complete an
330
+ unattended self-update either — it asks for a `goodvibes-<os>-<arch>` asset this
331
+ repository does not publish and takes the 404. Making each product update strictly
332
+ its own files is an SDK change, not one this repository can make.
333
+
334
+ - The daemon stopped keeping its own copy of code the platform owns. Forty-five
335
+ modules under `src/` held the same implementation `@pellux/goodvibes-sdk` or
336
+ `@pellux/goodvibes-terminal-shell` exports, because both products grew out of
337
+ one repository and the split copied files rather than pointing at them. Each
338
+ is now deleted here and imported from where it lives: the transcript journal,
339
+ the durability sweep and its housekeeping, the session-liveness markers, the
340
+ work-plan store, the workstream engine and its draft journal, the turn
341
+ anchors, the versioned-read quarantine, the atomic write, the pairing family
342
+ (stable host, handoff mint, offer copy, web origin), the session-cost
343
+ resolver, the credential-availability read, the memory-status projection, the
344
+ alert gate, the focus tracker, the consolidation receipt, the grid types, the
345
+ cluster command family and its remote-target convention, the CLI redaction,
346
+ endpoint resolution and config overrides, and thirteen composition helpers
347
+ that construct the platform's own objects.
348
+
349
+ Nothing about where this daemon keeps its state changed. The hoisted modules
350
+ that used to spell the storage scope now take it as a parameter, and every
351
+ call site here passes the daemon's own — the work plan, the session surface,
352
+ the workspace trust file, the code-index database and the operator-token
353
+ pruning candidates all resolve to exactly the paths they resolved to before.
354
+
355
+ Two behaviours the shared modules do not carry are stated here instead of
356
+ dropped: `src/cluster/raw-reply-route.ts`, because `/status`, `/api/health`
357
+ and `/api/channels/status` answer with their payload rather than the wrapped
358
+ `{ ok, data }` every `/api/cluster/*` route uses, and reading one as the
359
+ other called a healthy daemon a refusing one; and `src/cli/config-value.ts`,
360
+ because `config set <key> <value>` needs a settings value coerced on its own,
361
+ which the shared override path only does for whole `key=value` strings.
362
+
363
+ - The command line is parsed by the shared argument engine, driven by this
364
+ binary's catalog. `src/cli/parser.ts` was a full engine — the command-word
365
+ pre-scan, arity skipping, `--`, inline `=value`, per-kind application — with
366
+ a switch over one product's flag field names. The engine is now
367
+ `parseWithCatalog`, and `src/cli/command-catalog.ts` is the vocabulary it
368
+ reads: the same commands, the same aliases, the same flags per command, the
369
+ same refusal that an unrecognized word exits 2 rather than starting a daemon.
370
+ One sentence reads differently — a conversation flag this binary does not
371
+ have is now refused as "`--resume` is not a goodvibes-daemon flag — resuming
372
+ a conversation, a terminal app concern that belongs to another surface."
373
+
374
+ - The packages a hosted session's tools need are pinned by this product rather
375
+ than inherited as optional. A session this daemon hosts parses with
376
+ tree-sitter, spawns language servers, reads sql.js, matches with fuse.js and
377
+ bundles with jszip; the platform declares all of them optional, which is
378
+ right for a surface that never opens a file and leaves a hosted turn without
379
+ its tools when an optional install quietly fails. They are declared here at
380
+ the ranges the platform states, and a dependency check makes a missing one
381
+ fail at build time instead of at the first hosted turn. `@anthropic-ai/vertex-sdk`
382
+ and `@aws/bedrock-token-generator` are removed — nothing in this repository or
383
+ the platform imports either.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mike Davis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # goodvibes-daemon
2
+
3
+ [![CI](https://github.com/mgd34msu/goodvibes-daemon/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-daemon/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Version](https://img.shields.io/badge/version-1.28.0-blue.svg)](https://github.com/mgd34msu/goodvibes-daemon)
6
+
7
+ The GoodVibes daemon: one long-running process per machine that holds the control plane every
8
+ GoodVibes client talks to. It answers the operator verb families over HTTP, reads and replies on
9
+ your channels, elects a leader among the machines you have grouped together so only one of them
10
+ answers a shared inbox, runs scheduled and triggered work, keeps the session, memory, knowledge
11
+ and code-index stores, provisions the local voice and wake-word models, and updates itself at an
12
+ idle moment with a rollback if the new binary will not start.
13
+
14
+ The terminal app (`goodvibes`), the conversational agent (`goodvibes-agent`) and the web app are
15
+ clients of this process. They render, they capture input, and they call verbs; the work happens
16
+ here.
17
+
18
+ ## What this repository is
19
+
20
+ A **product** over `@pellux/goodvibes-sdk`, exactly like the TUI and the agent are:
21
+
22
+ - the composition root that builds the daemon's service graph,
23
+ - the product handlers the SDK does not own (inbox, triage, drafts, routing, remote peers,
24
+ credentials),
25
+ - the CLI (`send`, `cluster`, `webui`, `provision-wake-model`, `install-service` and friends),
26
+ - packaging: the compiled `goodvibes-daemon-<os>-<arch>` binaries,
27
+ - `scripts/install.sh` — the suite installer behind `https://goodvibes.sh/install.sh`, which
28
+ installs all four products (this daemon, the terminal app, the agent, the browser operator
29
+ surface) from their own repositories' releases. It lives here because the daemon is the product
30
+ everything else is installed alongside, and because this repository's release lane publishes it.
31
+
32
+ Every engine — the facade, the routes, the brokers, the updater, the channel adapters, the
33
+ schedulers — lives in the SDK and is consumed from the published package. Nothing was moved out of
34
+ the SDK to build this repository, and nothing should be: a capability that both a client and the
35
+ daemon need belongs in the SDK, not here.
36
+
37
+ ## Version line
38
+
39
+ The daemon's version is **1.28.0**. Live installs already carry a settings reader-floor
40
+ (`$goodvibes.minReaderVersion`), the update handover compares versions monotonically, and the
41
+ rejected-version record is keyed by version — those three mechanics all depend on the version
42
+ line staying continuous and monotonically increasing.
43
+
44
+ ## Install
45
+
46
+ The one-line installer downloads checksum-verified binaries and needs no package
47
+ manager. It installs the whole suite — this daemon and the sqlite-vec addon from
48
+ this repository's release, the terminal app from `goodvibes-tui`, the agent and
49
+ its browser driver from `goodvibes-agent`, and the browser operator surface's
50
+ bundle from `goodvibes-webui` — resolving a tag per repository and verifying
51
+ every file against that repository's own `SHA256SUMS.txt`:
52
+
53
+ ```sh
54
+ curl -fsSL https://goodvibes.sh/install.sh | sh
55
+ ```
56
+
57
+ The installer is `scripts/install.sh` in this repository and ships as a release
58
+ asset of it, so the current published copy is always at
59
+ `https://github.com/mgd34msu/goodvibes-daemon/releases/latest/download/install.sh`.
60
+
61
+ The browser surface is not a fourth binary and not a fourth service: the bundle
62
+ unpacks to `<install dir>/webui/<version>` and this daemon serves it on its own
63
+ listener, same origin as the API. Installing it exposes nothing new to your
64
+ network — the shipped binding is loopback and the installer does not change it.
65
+ `goodvibes-daemon webui --lan` is the deliberate act that widens it, and
66
+ `goodvibes-daemon webui status` says which posture is in force.
67
+
68
+ Or install from the npm registry with [Bun](https://bun.sh):
69
+
70
+ ```sh
71
+ bun add -g goodvibes-daemon
72
+ bun pm trust -g goodvibes-daemon
73
+ goodvibes-daemon install-service
74
+ ```
75
+
76
+ Bun blocks lifecycle scripts for untrusted global packages, so the second line
77
+ lets the package's postinstall place the matching daemon binary. If you skip it,
78
+ the `goodvibes-daemon` launcher still self-heals on first run by fetching and
79
+ checksum-verifying the binary. `npm install -g goodvibes-daemon` also works when
80
+ `bun` is already on `PATH`.
81
+
82
+ The npm package carries the product source and the launcher; the daemon itself is
83
+ a compiled binary published as a GitHub release asset of this repository, and the
84
+ release always lands before the registry publish so a fresh install can never
85
+ resolve a version whose binary does not exist yet.
86
+
87
+ ## Build
88
+
89
+ ```sh
90
+ bun install
91
+ bun run typecheck
92
+ bun run test
93
+ bun run build # host target
94
+ bun run build:all # every release target
95
+ ```
96
+
97
+ The compiled artifact names (`goodvibes-daemon-linux-x64`, `goodvibes-daemon-macos-arm64`, …) are
98
+ load-bearing: the installer and the running daemon's own updater both resolve release assets by
99
+ these exact names, so changing one without the other breaks installs and self-updates.
100
+
101
+ ## Running it
102
+
103
+ ```sh
104
+ goodvibes-daemon # run in the foreground
105
+ goodvibes-daemon install-service # install and start the user service unit
106
+ goodvibes-daemon service-status
107
+ goodvibes-daemon send --channel telegram "message"
108
+ goodvibes-daemon cluster status
109
+ goodvibes-daemon webui status # is the browser surface served, from where, to whom
110
+ goodvibes-daemon provision-wake-model
111
+ ```
112
+
113
+ ## Documentation
114
+
115
+ - [Getting Started](docs/getting-started.md) — install, first boot, pairing, where state lives, health checks
116
+ - [Command Reference](docs/commands-reference.md) — every command, its flags, and its exit codes
117
+ - [Configuration](docs/configuration.md) — the settings this daemon reads, by key
118
+ - [Service and Deployment](docs/service-and-deployment.md) — the host service, migration from an older install, `--daemon-home` vs the data home
119
+ - [Updates and Rollback](docs/updates-and-rollback.md) — the hourly self-update loop, automatic crash-loop rollback, `.previous`
120
+ - [Daemon-Hosted Sessions](docs/hosted-sessions.md) — conversations that run inside the daemon and outlive any one client
121
+ - [Troubleshooting](docs/troubleshooting.md) — startup failures, log locations, port conflicts, service-status oddities
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bun
2
+ //
3
+ // ── READ THIS BEFORE DEV-TESTING DAEMON ROUTES THROUGH THIS SHIM ──────────
4
+ //
5
+ // This launcher prefers a PREBUILT binary over the source checkout. On a machine
6
+ // with `vendor/goodvibes-daemon-<platform>` staged, running this shim runs THAT
7
+ // binary — compiled against the PUBLISHED SDK — not the source in this repo and
8
+ // not whatever `sdk-dev link` overlaid into node_modules.
9
+ //
10
+ // The consequence is quiet and expensive: a route you just added returns 404 and
11
+ // every explanation you reach for is wrong, because the process answering never
12
+ // contained your code.
13
+ //
14
+ // Two further traps in the same area:
15
+ // - Killing this shim does NOT kill the vendored child. `pkill -f` on the
16
+ // shim's command line leaves the daemon listening, so the "restart" you
17
+ // think you performed changed nothing.
18
+ // - `sdk-dev link` overlays node_modules only. A vendored binary ignores it
19
+ // completely.
20
+ //
21
+ // To exercise source (and a linked SDK), bypass this launcher:
22
+ // bun src/daemon/cli.ts --port <port> --hostname 127.0.0.1
23
+ // and isolate it with GOODVIBES_HOME / GOODVIBES_DAEMON_HOME /
24
+ // GOODVIBES_WORKING_DIR so it cannot read the machine's real credentials or
25
+ // claim its service unit.
26
+ //
27
+ import { dirname, join } from 'node:path';
28
+ import { fileURLToPath } from 'node:url';
29
+ import { spawnSync } from 'node:child_process';
30
+ import {
31
+ ensureVendoredBinary,
32
+ ensureVendoredSqliteVecAddon,
33
+ isExecutable,
34
+ isSourceCheckout,
35
+ resolveArtifactName,
36
+ run,
37
+ supportedTargetsText,
38
+ } from './launcher-support.js';
39
+
40
+ const __dirname = dirname(fileURLToPath(import.meta.url));
41
+ const packageRoot = join(__dirname, '..');
42
+
43
+ if (process.platform === 'win32') {
44
+ console.error('goodvibes-daemon: native Windows is not supported.');
45
+ console.error('Use WSL so the Linux release binary path applies.');
46
+ process.exit(1);
47
+ }
48
+
49
+ const artifactName = resolveArtifactName(process.platform, process.arch);
50
+ const localPlatformBuild = artifactName ? join(packageRoot, 'dist', artifactName) : null;
51
+ const localBuild = join(packageRoot, 'dist', 'goodvibes-daemon');
52
+ const vendoredBinary = artifactName ? join(packageRoot, 'vendor', artifactName) : null;
53
+
54
+ if (localPlatformBuild && isExecutable(localPlatformBuild)) {
55
+ run(localPlatformBuild, process.argv.slice(2));
56
+ }
57
+
58
+ if (isExecutable(localBuild)) {
59
+ run(localBuild, process.argv.slice(2));
60
+ }
61
+
62
+ if (vendoredBinary && isExecutable(vendoredBinary)) {
63
+ run(vendoredBinary, process.argv.slice(2));
64
+ }
65
+
66
+ // Self-heal: an install whose postinstall was blocked (the default for an
67
+ // untrusted global package under bun) has no vendored binary yet. Fetch it now,
68
+ // checksum-verified against the release manifest, and cache it in vendor/.
69
+ if (artifactName) {
70
+ try {
71
+ const installedBinary = await ensureVendoredBinary({ packageRoot, artifactName });
72
+ // Best-effort: a failure here must never block the daemon from running on
73
+ // the binary that just installed successfully above. It only costs
74
+ // semantic vector search (the daemon logs a warning and falls back to
75
+ // lexical matching); the binary the user asked to run must still start.
76
+ try {
77
+ await ensureVendoredSqliteVecAddon({ packageRoot, platform: process.platform, arch: process.arch });
78
+ } catch (addonError) {
79
+ console.error(
80
+ `goodvibes-daemon: could not install the sqlite-vec addon (semantic search will fall back to lexical matching): ${addonError instanceof Error ? addonError.message : String(addonError)}`,
81
+ );
82
+ }
83
+ run(installedBinary, process.argv.slice(2));
84
+ } catch (error) {
85
+ console.error(`goodvibes-daemon: failed to install release binary: ${error instanceof Error ? error.message : String(error)}`);
86
+ }
87
+ }
88
+
89
+ const bunProbe = spawnSync('bun', ['--version'], { stdio: 'ignore' });
90
+ if (bunProbe.status === 0 && isSourceCheckout(packageRoot)) {
91
+ run('bun', [join(packageRoot, 'src', 'daemon', 'cli.ts'), ...process.argv.slice(2)]);
92
+ }
93
+
94
+ console.error('goodvibes-daemon: no runnable binary is available.');
95
+ console.error(`platform: ${process.platform}-${process.arch}`);
96
+ console.error(`supported prebuilt targets: ${supportedTargetsText()}`);
97
+ console.error('Either:');
98
+ console.error(' 1. check network access to the GitHub release assets, or');
99
+ console.error(' 2. build locally with `bun run build` in a source checkout.');
100
+ process.exit(1);