@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.
- package/CHANGELOG.md +383 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/bin/goodvibes-daemon +100 -0
- package/bin/launcher-support.js +226 -0
- package/package.json +96 -0
- package/scripts/check-bun.sh +20 -0
- package/scripts/postinstall.js +244 -0
- package/src/cli/command-catalog.ts +828 -0
- package/src/cli/completion.ts +299 -0
- package/src/cli/help.ts +167 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/parser.ts +55 -0
- package/src/cli/surface-catalog.ts +26 -0
- package/src/cli/types.ts +63 -0
- package/src/cluster/daemon-ws-call.ts +235 -0
- package/src/cluster/raw-reply-route.ts +111 -0
- package/src/config/checkpoint-settings.ts +113 -0
- package/src/config/run-daemon-config-migration.ts +47 -0
- package/src/config/secret-config.ts +175 -0
- package/src/config/secrets.ts +71 -0
- package/src/config/surface.ts +24 -0
- package/src/core/pairing-banner.ts +82 -0
- package/src/daemon/cli.ts +878 -0
- package/src/daemon/config-command.ts +281 -0
- package/src/daemon/handlers/context.ts +29 -0
- package/src/daemon/handlers/contracts.ts +43 -0
- package/src/daemon/handlers/credentials.ts +139 -0
- package/src/daemon/handlers/drafts/draft-store.ts +427 -0
- package/src/daemon/handlers/drafts/index.ts +17 -0
- package/src/daemon/handlers/drafts/register.ts +331 -0
- package/src/daemon/handlers/errors.ts +18 -0
- package/src/daemon/handlers/inbox/aggregator.ts +375 -0
- package/src/daemon/handlers/inbox/cursor-store.ts +512 -0
- package/src/daemon/handlers/inbox/index.ts +221 -0
- package/src/daemon/handlers/inbox/mapping.ts +192 -0
- package/src/daemon/handlers/inbox/poller.ts +239 -0
- package/src/daemon/handlers/inbox/provider-adapter.ts +171 -0
- package/src/daemon/handlers/inbox/providers/discord.ts +276 -0
- package/src/daemon/handlers/inbox/providers/email.ts +176 -0
- package/src/daemon/handlers/inbox/providers/imap-client.ts +300 -0
- package/src/daemon/handlers/inbox/providers/route-util.ts +24 -0
- package/src/daemon/handlers/inbox/providers/slack.ts +287 -0
- package/src/daemon/handlers/index.ts +117 -0
- package/src/daemon/handlers/register.ts +180 -0
- package/src/daemon/handlers/remote/backends/cloud-terminal.ts +143 -0
- package/src/daemon/handlers/remote/backends/docker.ts +79 -0
- package/src/daemon/handlers/remote/backends/index.ts +40 -0
- package/src/daemon/handlers/remote/backends/local-process.ts +113 -0
- package/src/daemon/handlers/remote/backends/process-runner.ts +127 -0
- package/src/daemon/handlers/remote/backends/ssh.ts +126 -0
- package/src/daemon/handlers/remote/backends/types.ts +97 -0
- package/src/daemon/handlers/remote/dispatcher.ts +181 -0
- package/src/daemon/handlers/remote/index.ts +120 -0
- package/src/daemon/handlers/remote/peer-registry.ts +357 -0
- package/src/daemon/handlers/remote/service.ts +191 -0
- package/src/daemon/handlers/routing/inbox-bridge.ts +71 -0
- package/src/daemon/handlers/routing/index.ts +261 -0
- package/src/daemon/handlers/routing/route-store.ts +319 -0
- package/src/daemon/handlers/routing/routing-resolver.ts +75 -0
- package/src/daemon/handlers/sqlite-store.ts +303 -0
- package/src/daemon/handlers/triage/index.ts +57 -0
- package/src/daemon/handlers/triage/integration.ts +213 -0
- package/src/daemon/handlers/triage/pipeline.ts +274 -0
- package/src/daemon/handlers/triage/scorer.ts +287 -0
- package/src/daemon/handlers/triage/tagger/discord.ts +187 -0
- package/src/daemon/handlers/triage/tagger/imap.ts +384 -0
- package/src/daemon/handlers/triage/tagger/index.ts +184 -0
- package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
- package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
- package/src/daemon/handlers/triage/types.ts +50 -0
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/local-daemon-state.ts +233 -0
- package/src/daemon/pair-command.ts +301 -0
- package/src/daemon/provision-wake-model.ts +81 -0
- package/src/daemon/send/channels.ts +200 -0
- package/src/daemon/send/command.ts +333 -0
- package/src/daemon/send/composition.ts +100 -0
- package/src/daemon/send/failure-text.ts +93 -0
- package/src/daemon/send/inert-text.ts +225 -0
- package/src/daemon/send/stdin.ts +24 -0
- package/src/daemon/service-commands.ts +530 -0
- package/src/daemon/sessions-command.ts +209 -0
- package/src/daemon/status-command.ts +481 -0
- package/src/daemon/webui-command.ts +339 -0
- package/src/runtime/boot-tasks.ts +110 -0
- package/src/runtime/cluster-composition.ts +124 -0
- package/src/runtime/cluster-group-composition.ts +284 -0
- package/src/runtime/conversation-rewind-port.ts +171 -0
- package/src/runtime/credential-composition.ts +54 -0
- package/src/runtime/daemon-handler-composition.ts +76 -0
- package/src/runtime/device-posture-composition.ts +115 -0
- package/src/runtime/disposal-wiring.ts +101 -0
- package/src/runtime/fleet-needs-input-push.ts +61 -0
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/hosted-session-composition.ts +128 -0
- package/src/runtime/index.ts +100 -0
- package/src/runtime/knowledge-services.ts +101 -0
- package/src/runtime/legacy-daemon-migration.ts +605 -0
- package/src/runtime/legacy-daemon-reconcile.ts +448 -0
- package/src/runtime/mail-composition.ts +65 -0
- package/src/runtime/notification-dispatch.ts +86 -0
- package/src/runtime/plugin-composition.ts +111 -0
- package/src/runtime/runtime-services-types.ts +268 -0
- package/src/runtime/services.ts +756 -0
- package/src/runtime/trigger-services.ts +62 -0
- package/src/runtime/trust/checkpoint-eligibility.ts +138 -0
- package/src/runtime/trust/trust-gated-approvals.ts +169 -0
- package/src/runtime/update-check.ts +61 -0
- package/src/runtime/workspace-checkpointing.ts +116 -0
- package/src/testing/daemon-fixture.ts +276 -0
- package/src/testing/hosted-session-failures.ts +92 -0
- 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
|
+
[](https://github.com/mgd34msu/goodvibes-daemon/actions/workflows/ci.yml)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](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);
|