@beevibe/daemon 0.1.0 → 0.1.2
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/README.md +91 -301
- package/dist/main.js +257 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,354 +1,144 @@
|
|
|
1
|
-
# beevibe
|
|
1
|
+
# @beevibe/daemon
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The local worker. Runs on a user's machine, registers with the api as a `(daemon, runtime)` pair, then claims sessions whose agent's `preferred_runtime_id` matches and spawns the local CLI (`claude`, etc.) to fulfill them. Streams steps back via `/runtime/events` and finalizes with `/runtime/done`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The daemon never proxies MCP tool calls — the spawned CLI calls the api's `/mcp` endpoint directly using the `bv_a_` agent token written into its workspace's `mcp-config.json`. The daemon only writes that file and supervises the subprocess.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
For full setup, see the [root README](../../README.md).
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Run it
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Three subcommands. `setup` once; `start` to claim sessions; `update` for the brew/curl install path.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- **Docker / docker-compose** — `git clone && docker compose up` against a tagged release (see [Self-hosting](#self-hosting) below).
|
|
20
|
-
- **Manual** — `pnpm install && pnpm build && pnpm start` per service against your own Postgres.
|
|
21
|
-
|
|
22
|
-
The repo and its tagged releases are the source of truth. The Railway button is a convenience layer; you can deploy the same code to Fly, Render, Coolify, k8s, or any container host.
|
|
13
|
+
```bash
|
|
14
|
+
# 1. one-time registration with an api server
|
|
15
|
+
beevibe-daemon setup --api https://api.beevibe.io --user-token bv_u_…
|
|
16
|
+
# detects CLIs on PATH (claude, …), POSTs /runtime/register,
|
|
17
|
+
# writes ~/.beevibe/config.json (mode 0600) with the bv_d_ token
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
# 2. start claiming
|
|
20
|
+
beevibe-daemon start
|
|
21
|
+
# loads config, syncs ~/.beevibe/skills, opens WS to /runtime/ws,
|
|
22
|
+
# polls /runtime/claim every 30s, heartbeats every 15s
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
beevibe
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
│ ├── api/ MCP tool surface for agents + REST for humans + mesh broker
|
|
31
|
-
│ ├── scheduler/ fallback claimant for null-runtime sessions + orphan reaper
|
|
32
|
-
│ ├── daemon/ runs on each user's machine; claims sessions and spawns CLIs locally
|
|
33
|
-
│ └── web/ Next.js dashboard with live updates over SSE
|
|
34
|
-
│
|
|
35
|
-
├── skills/ Anthropic Agent Skills (markdown behavioral protocols)
|
|
36
|
-
├── migrations/ node-pg-migrate SQL files
|
|
37
|
-
├── scripts/ dev orchestrator + e2e smokes + provisioning helpers
|
|
38
|
-
└── docker-compose.yml Postgres 16 + pgvector
|
|
24
|
+
# 3. update self (Bun-compiled binaries only)
|
|
25
|
+
beevibe-daemon update
|
|
26
|
+
# downloads latest GitHub release, SHA-256 verifies, atomic rename.
|
|
27
|
+
# For npm / source installs, prints the right install command and bails.
|
|
39
28
|
```
|
|
40
29
|
|
|
41
|
-
|
|
30
|
+
`setup` flags:
|
|
42
31
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
32
|
+
| Flag | Default | Purpose |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `--api` / `-a` | (required) | Api base URL (http or https). |
|
|
35
|
+
| `--user-token` / `-t` | (required) | Your `bv_u_` user key — used once to mint the `bv_d_` daemon key. |
|
|
36
|
+
| `--device-name` | `<user>@<hostname>` | Human label shown in the Runtimes panel. |
|
|
37
|
+
| `--external-id` | `<hostname>` | Stable per-machine id; lets `setup` re-run idempotently. |
|
|
48
38
|
|
|
49
|
-
|
|
39
|
+
`start` takes no flags — everything comes from `~/.beevibe/config.json` + env. `update` takes `--yes` / `-y` to skip the confirmation prompt.
|
|
50
40
|
|
|
51
|
-
|
|
52
|
-
- **Person / user**. A human owner. Authenticates with a `bv_u_…` API key. Can act as their top-level agent through MCP, or use the dashboard for review.
|
|
53
|
-
- **Task**. A unit of work assigned to an agent. Moves through `pending → running → review → done` (or `failed` / `blocked` / `cancelled`). Tasks can spawn child tasks; parents auto-complete when their children settle.
|
|
54
|
-
- **Session**. One run of the `claude` CLI for one task. Has a transcript, a `cli_session_id` (for `--resume`), and usage telemetry (including cache-hit ratios).
|
|
55
|
-
- **Mesh**. Agents can `ask` peers one-shot questions or `negotiate` with team/org peers across multiple rounds. If they can't agree, either side can `escalate_to_humans` and a person resolves it from the dashboard.
|
|
56
|
-
- **Memory**. `save_memory(...)` archives a fact (`belief`/`pattern`/`gotcha`/`preference`/`decision`); after the session ends, the FactPromoter LLM may elevate it from `ic` scope to `team` or `org` based on whether it generalizes. `update_core_memory(...)` writes the durable per-agent blocks.
|
|
57
|
-
- **Skills**. Markdown procedural protocols ([Anthropic Agent Skills standard](https://agentskills.io/specification)) auto-discovered by `claude` from `<workspace>/.claude/skills/`. We ship two: `beevibe-pre-task-setup` and `beevibe-team-mesh-negotiation`.
|
|
41
|
+
## Setup flow
|
|
58
42
|
|
|
59
|
-
|
|
43
|
+
1. **`setup`** probes `PATH` for known CLIs (`KNOWN_CLIS` from `@beevibe/core`), running `<cli> --version` for each one it finds.
|
|
44
|
+
2. POSTs `/runtime/register` with `{ external_id, device_name, runtimes }` and `Authorization: Bearer <bv_u_…>`.
|
|
45
|
+
3. Server returns `{ daemon_id, daemon_token, runtimes: [{id, cli}] }`. The daemon token is shown ONCE — saved straight to `~/.beevibe/config.json` (server stores only the SHA-256).
|
|
46
|
+
4. **`start`** loads config, fetches `/runtime/skills` (version-cached), syncs into `~/.beevibe/skills/`, opens the WS, and starts the claim/heartbeat loops.
|
|
60
47
|
|
|
61
|
-
|
|
48
|
+
`~/.beevibe/config.json` (dir `0700`, file `0600`):
|
|
62
49
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
pnpm install
|
|
72
|
-
|
|
73
|
-
cp .env.example .env
|
|
74
|
-
# fill in ANTHROPIC_API_KEY and OPENAI_API_KEY
|
|
75
|
-
|
|
76
|
-
docker compose up -d # postgres + pgvector
|
|
77
|
-
pnpm migrate up # apply migrations to dev DB
|
|
78
|
-
pnpm dev # postgres + api + scheduler (+ tunnel if cloudflared is on PATH)
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"api_url": "https://api.beevibe.io",
|
|
53
|
+
"external_id": "macbook-pro.local",
|
|
54
|
+
"daemon_id": "dmn_…",
|
|
55
|
+
"daemon_token": "bv_d_…",
|
|
56
|
+
"runtimes": [{ "id": "rt_…", "cli": "claude" }]
|
|
57
|
+
}
|
|
79
58
|
```
|
|
80
59
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
- starts Postgres if it isn't already running
|
|
84
|
-
- applies migrations
|
|
85
|
-
- spawns `@beevibe/api` (port 3000) and `@beevibe/scheduler` (health on 3001) in watch mode with `[api]` / `[exec]` log prefixes
|
|
86
|
-
- if `cloudflared` is on PATH, exposes the api at a `*.trycloudflare.com` URL and prints a paste-ready `mcp.json` snippet for remote `claude` CLI access (pass `--no-tunnel` to disable)
|
|
87
|
-
|
|
88
|
-
`Ctrl+C` tears the whole tree down.
|
|
60
|
+
## Concurrency cap
|
|
89
61
|
|
|
90
|
-
|
|
62
|
+
A single global hardware cap across all sessions on this machine. Default: **10**. Override:
|
|
91
63
|
|
|
92
64
|
```bash
|
|
93
|
-
|
|
94
|
-
# then visit http://localhost:3030
|
|
65
|
+
BEEVIBE_DAEMON_MAX_CONCURRENT=4 beevibe-daemon start
|
|
95
66
|
```
|
|
96
67
|
|
|
97
|
-
The
|
|
68
|
+
The `Supervisor` tracks each session under its own `AbortController`; cancel frames received over the WS abort the named session. `SIGINT` / `SIGTERM` calls `cancelAll()` and exits.
|
|
98
69
|
|
|
99
|
-
|
|
70
|
+
Per-agent caps (`max_task_sessions=1`, mesh=3) are enforced server-side at claim time, so you don't have to set those here.
|
|
100
71
|
|
|
101
|
-
|
|
72
|
+
## Polling, heartbeat, reconnect
|
|
102
73
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
pnpm tsx scripts/provision-demo.ts
|
|
110
|
-
```
|
|
74
|
+
| Signal | Cadence | Source |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| WS push (`task_available`, `cancel`) | event-driven | api `DaemonHub.notify` |
|
|
77
|
+
| HTTP `/runtime/claim` poll | 30s | `claimer.ts` `DEFAULT_POLL_MS` |
|
|
78
|
+
| `/runtime/heartbeat` | 15s | `RUNTIME_HEARTBEAT_INTERVAL_MS` from `@beevibe/core` |
|
|
79
|
+
| WS reconnect | exponential 1s → 30s cap | `DEFAULT_WS_RECONNECT_MAX_MS` |
|
|
111
80
|
|
|
112
|
-
|
|
81
|
+
The WS push is the low-latency wakeup; the HTTP poll is the catch-up + safety net. Server stales a runtime after 60s of silence (`packages/core/src/services/orphan-reaper.ts`) — well over 2× the heartbeat interval.
|
|
113
82
|
|
|
114
|
-
|
|
115
|
-
person: demo-user (bv_u_<token>)
|
|
116
|
-
└── captain (team-tier, owned by demo-user)
|
|
117
|
-
├── ic-alice (ic-tier)
|
|
118
|
-
└── ic-bob (ic-tier)
|
|
119
|
-
```
|
|
83
|
+
When the WS opens, the daemon drains `/runtime/claim` until it returns 204 or the supervisor is full, then idles waiting for the next push.
|
|
120
84
|
|
|
121
|
-
|
|
85
|
+
## Workspaces + env
|
|
122
86
|
|
|
123
|
-
|
|
124
|
-
claude
|
|
125
|
-
# In the chat, try:
|
|
126
|
-
# "who are my subordinates?" → find_subordinates → [ic-alice, ic-bob]
|
|
127
|
-
# "create a task for ic-alice: write a hello-world bash script"
|
|
128
|
-
# wait ~30-60s, watching [exec] logs in Terminal 1
|
|
129
|
-
# "what's the status of that task?" → check_work_status → done
|
|
130
|
-
# "ask ic-alice: where is hello.sh?" → mesh-ask round-trip
|
|
131
|
-
```
|
|
87
|
+
Per-agent workspace dirs are created lazily on first claim:
|
|
132
88
|
|
|
133
|
-
|
|
89
|
+
- Default: `~/.beevibe/workspaces/<agent_id>/`
|
|
90
|
+
- Override: `WORKSPACE_ROOT=/path/to/dir`
|
|
134
91
|
|
|
135
|
-
|
|
136
|
-
pnpm tsx scripts/provision-demo.ts --print # re-print existing keys/snippet
|
|
137
|
-
pnpm tsx scripts/provision-demo.ts --clean # wipe demo rows (no reseed)
|
|
138
|
-
```
|
|
92
|
+
Each workspace contains `mcp-config.json` (mode `0600`) with `Bearer <bv_a_…>` for that agent and the api's `/mcp` URL. The CLI reads this file directly; the daemon never sees the `bv_a_` token in a request path.
|
|
139
93
|
|
|
140
|
-
|
|
94
|
+
Skills cache: `~/.beevibe/skills/`, version-gated via `.version`. Refreshed on every `start`.
|
|
141
95
|
|
|
142
|
-
##
|
|
96
|
+
## What it doesn't do
|
|
143
97
|
|
|
144
|
-
|
|
98
|
+
- **No MCP proxy.** The CLI calls `/mcp` directly using the `bv_a_` token in `mcp-config.json`. The daemon's job stops at writing the file and supervising the subprocess.
|
|
99
|
+
- **No agent semantics.** `/runtime/claim` payloads are self-contained — agent_id, intent, prior_session_id (for `--resume`), workspace dir. The daemon never queries agents or tasks.
|
|
100
|
+
- **No persistence beyond config.** No DB connection, no cache of agent state. The token is plaintext locally; the server stores only the SHA-256.
|
|
145
101
|
|
|
146
|
-
|
|
102
|
+
## Source layout
|
|
147
103
|
|
|
148
|
-
```bash
|
|
149
|
-
pnpm install-skills
|
|
150
104
|
```
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
git clone https://github.com/beevibe-ai/beevibe.git
|
|
166
|
-
cd beevibe
|
|
167
|
-
# Optional: pin to a tagged release for reproducible builds
|
|
168
|
-
# git checkout v0.1.0
|
|
169
|
-
|
|
170
|
-
# 1. Build the three service images
|
|
171
|
-
docker build -f infra/railway/Dockerfile.api -t beevibe-api .
|
|
172
|
-
docker build -f infra/railway/Dockerfile.scheduler -t beevibe-scheduler .
|
|
173
|
-
docker build -f infra/railway/Dockerfile.web \
|
|
174
|
-
--build-arg NEXT_PUBLIC_BV_API_URL=http://localhost:3000 \
|
|
175
|
-
-t beevibe-web .
|
|
176
|
-
|
|
177
|
-
# 2. Start Postgres
|
|
178
|
-
docker compose up -d postgres
|
|
179
|
-
|
|
180
|
-
# 3. Run migrations once
|
|
181
|
-
docker run --rm --network host \
|
|
182
|
-
-e DATABASE_URL=postgresql://beevibe:beevibe@localhost:5433/beevibe \
|
|
183
|
-
beevibe-api pnpm migrate:deploy up
|
|
184
|
-
|
|
185
|
-
# 4. Run the services (each in its own terminal or via your orchestrator)
|
|
186
|
-
docker run -p 3000:3000 --network host --env-file .env beevibe-api
|
|
187
|
-
docker run --network host --env-file .env beevibe-scheduler
|
|
188
|
-
docker run -p 8080:3000 --env-file .env beevibe-web
|
|
105
|
+
src/
|
|
106
|
+
├── main.ts CLI arg parser + subcommand dispatch
|
|
107
|
+
├── setup.ts runSetup: detect CLIs, POST /runtime/register, write config
|
|
108
|
+
├── start.ts runStart: load config, sync skills, build Supervisor + Claimer
|
|
109
|
+
├── update.ts runUpdate: GitHub-release self-update for compiled binaries
|
|
110
|
+
├── config.ts load/save ~/.beevibe/config.json (DaemonConfig shape)
|
|
111
|
+
├── api-client.ts thin GET/POST/claim over /runtime/* with bv_d_ auth + WS open
|
|
112
|
+
├── claimer.ts WS push + 30s HTTP poll + 15s heartbeat + WS reconnect
|
|
113
|
+
├── supervisor.ts bounded concurrency cap with per-session AbortControllers
|
|
114
|
+
├── spawner.ts runDispatch: workspace + ClaudeCodeRuntime + batched events
|
|
115
|
+
├── skills-cache.ts syncSkillsCache: pull /runtime/skills into ~/.beevibe/skills/
|
|
116
|
+
└── supervisor.test.ts vitest unit tests
|
|
189
117
|
```
|
|
190
118
|
|
|
191
|
-
|
|
119
|
+
## Build distribution
|
|
192
120
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
If you'd rather not use Docker:
|
|
121
|
+
For local dev:
|
|
196
122
|
|
|
197
123
|
```bash
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
# Optional: pin to a tagged release
|
|
201
|
-
# git checkout v0.1.0
|
|
202
|
-
|
|
203
|
-
pnpm install --frozen-lockfile
|
|
204
|
-
pnpm build
|
|
205
|
-
pnpm migrate:deploy up
|
|
206
|
-
|
|
207
|
-
# Start each service in its own process (use systemd, pm2, or your service manager)
|
|
208
|
-
node packages/api/dist/main.js # api on $PORT (default 3000)
|
|
209
|
-
node packages/scheduler/dist/main.js # scheduler (background worker)
|
|
210
|
-
pnpm --filter @beevibe/web start # web (next start, port from $PORT)
|
|
124
|
+
pnpm --filter @beevibe/daemon build # tsc → dist/
|
|
125
|
+
pnpm --filter @beevibe/daemon dev # tsx watch
|
|
211
126
|
```
|
|
212
127
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
### Notes for production self-hosting
|
|
216
|
-
|
|
217
|
-
- **Single api replica** for v1 (see [v1 single-instance API constraint](#v1-single-instance-api-constraint) below).
|
|
218
|
-
- **Postgres 16+** with `pgvector` extension. The included `docker-compose.yml` uses `pgvector/pgvector:pg16`.
|
|
219
|
-
- **Reverse proxy** in front of the api (nginx / Caddy / Cloudflare) must support WebSockets (`/runtime/ws`) and long-held HTTP responses (mesh negotiate held connections, up to 5 minutes idle).
|
|
220
|
-
- **CORS**: set `BEEVIBE_CORS_ORIGINS` to the public URL of the web service. Localhost variants are always allowed in addition.
|
|
221
|
-
- **Daemon distribution**: end users still install the [`beevibe-daemon`](./packages/daemon) on their own machines to run agent CLIs locally. The hosted api never spawns user CLIs.
|
|
222
|
-
|
|
223
|
-
## Architecture
|
|
224
|
-
|
|
225
|
-
The dependency direction across packages is one-way and ESLint-enforced:
|
|
226
|
-
|
|
227
|
-
```
|
|
228
|
-
core/domain → nothing
|
|
229
|
-
core/ports → domain
|
|
230
|
-
core/services → domain + ports (NEVER adapters)
|
|
231
|
-
core/adapters → ports it implements + domain
|
|
232
|
-
api/ → core (composition root)
|
|
233
|
-
scheduler/ → core (composition root)
|
|
234
|
-
daemon/ → core (workspace + runtime adapters, direct imports)
|
|
235
|
-
web/ → core (types only) + api (HTTP)
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
The api, scheduler, and per-user daemons are independent processes. The api never talks to the scheduler or daemons over HTTP for task dispatch — Postgres is the integration point:
|
|
239
|
-
|
|
240
|
-
- The api writes task lifecycle changes (created, approved, revised, cancelled) and inserts `session` rows with `status='pending'`.
|
|
241
|
-
- A daemon claims sessions whose `preferred_runtime_id` matches its registered runtime and spawns the CLI locally on the user's machine.
|
|
242
|
-
- The scheduler claims null-runtime sessions (server-fallback for offline-target mesh asks) and runs the daemon-orphan reaper.
|
|
243
|
-
- Cancellation: api `UPDATE`s the row + `pg_notify('cancel_task', task_id)`; the active claimant (daemon or scheduler) aborts the in-flight session in <200ms.
|
|
244
|
-
- Live updates: every state-changing INSERT/UPDATE fires a `pg_notify`; api's `/api/stream` SSE endpoint relays them to the dashboard.
|
|
245
|
-
|
|
246
|
-
That decoupling means each component scales independently — many daemons (one per user machine), one or a few schedulers, a single api replica per region (see [v1 single-instance API constraint](#v1-single-instance-api-constraint) below).
|
|
247
|
-
|
|
248
|
-
### v1 single-instance API constraint
|
|
249
|
-
|
|
250
|
-
The mesh resolver (`packages/api/src/mesh/server.ts:90`) and the
|
|
251
|
-
forthcoming chat / room resolvers are in-process `Map`s — when an agent's
|
|
252
|
-
`ask` fires, the API holds the HTTP request open and resolves it from
|
|
253
|
-
that map when the target's `respond_ask` lands. **Both halves of the
|
|
254
|
-
round-trip must hit the same API process** for v1; a multi-instance API
|
|
255
|
-
fronted by a load balancer can drop responses on the floor when the two
|
|
256
|
-
HTTP requests land on different replicas. Documented in code; tracked
|
|
257
|
-
for cross-instance federation via `pg_notify` as a follow-up. **Self-hosters
|
|
258
|
-
should run a single API replica until that ships.** The scheduler and
|
|
259
|
-
per-user daemons scale horizontally either way.
|
|
260
|
-
|
|
261
|
-
## Common commands
|
|
128
|
+
For releases (Bun-compiled standalone binaries — no Node required):
|
|
262
129
|
|
|
263
130
|
```bash
|
|
264
|
-
pnpm
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
pnpm test # vitest (unit + integration)
|
|
268
|
-
pnpm dev # full local stack (postgres + api + scheduler + tunnel)
|
|
269
|
-
pnpm migrate up # apply migrations to DATABASE_URL
|
|
270
|
-
pnpm migrate:test up # apply migrations to DATABASE_URL_TEST
|
|
271
|
-
pnpm install-skills # install beevibe skills into ~/.claude/skills/
|
|
131
|
+
pnpm --filter @beevibe/daemon build:binaries
|
|
132
|
+
# bun build --compile for darwin-{arm64,x64} + linux-{x64,arm64}
|
|
133
|
+
# outputs dist-bin/ with size + SHA-256, ready for GitHub release upload
|
|
272
134
|
```
|
|
273
135
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
- TypeScript strict (ES2022, NodeNext)
|
|
277
|
-
- pnpm workspaces + Turborepo
|
|
278
|
-
- Postgres 16 + pgvector, raw `pg` driver (no ORM)
|
|
279
|
-
- node-pg-migrate
|
|
280
|
-
- Claude Code CLI runtime (spawned per-session)
|
|
281
|
-
- `@modelcontextprotocol/sdk`
|
|
282
|
-
- Next.js 14 + TanStack Query + Tailwind (dashboard)
|
|
136
|
+
Used by `.github/workflows/release.yml`. The companion `scripts/prepare-publish.sh` bundles the workspace dep `@beevibe/core` into one ESM file via `bun build` for `npm publish`.
|
|
283
137
|
|
|
284
|
-
##
|
|
285
|
-
|
|
286
|
-
Live integration tests against real Postgres + LLM APIs. Each is gated by an env flag.
|
|
138
|
+
## Build / test
|
|
287
139
|
|
|
288
140
|
```bash
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
OPENAI_API_KEY=... ANTHROPIC_API_KEY=... \
|
|
293
|
-
pnpm tsx scripts/m6-e2e.ts
|
|
294
|
-
|
|
295
|
-
# Multi-process smoke — api + scheduler as actual `node dist/main.js`
|
|
296
|
-
# subprocesses; verifies cross-process IPC, signal propagation, no orphans
|
|
297
|
-
RUN_M7_E2E=1 \
|
|
298
|
-
DATABASE_URL_TEST=postgresql://beevibe:beevibe@localhost:5433/beevibe_test \
|
|
299
|
-
OPENAI_API_KEY=... ANTHROPIC_API_KEY=... \
|
|
300
|
-
pnpm tsx scripts/m7-e2e.ts
|
|
301
|
-
|
|
302
|
-
# Skills + memory + cache-hit ratio smoke (M9 features)
|
|
303
|
-
RUN_M9_E2E=1 \
|
|
304
|
-
DATABASE_URL_TEST=postgresql://beevibe:beevibe@localhost:5433/beevibe_test \
|
|
305
|
-
OPENAI_API_KEY=... ANTHROPIC_API_KEY=... \
|
|
306
|
-
pnpm tsx scripts/m9-e2e.ts
|
|
141
|
+
pnpm --filter @beevibe/daemon build
|
|
142
|
+
pnpm --filter @beevibe/daemon typecheck
|
|
143
|
+
pnpm --filter @beevibe/daemon test
|
|
307
144
|
```
|
|
308
|
-
|
|
309
|
-
Apply test-DB migrations first with `pnpm migrate:test up`. All scripts truncate the test DB at the start, so back-to-back runs work.
|
|
310
|
-
|
|
311
|
-
## Project status
|
|
312
|
-
|
|
313
|
-
The platform was built in milestones (M0 → M9), each with its own integration test:
|
|
314
|
-
|
|
315
|
-
| Milestone | What landed |
|
|
316
|
-
|---|---|
|
|
317
|
-
| M0 | Repo scaffold |
|
|
318
|
-
| M1 | Domain + ports + Postgres adapter + schema + migrations |
|
|
319
|
-
| M2 | Claude Code runtime adapter + workspace adapter |
|
|
320
|
-
| M3 | Memory subsystem + pgvector + LLM providers |
|
|
321
|
-
| M4 | API-key auth + agent provisioning |
|
|
322
|
-
| M5 | Executor binary (polling + dispatch) |
|
|
323
|
-
| M6 | API binary (MCP tools + REST + mesh + escalation + cancellation) |
|
|
324
|
-
| M7 | Multi-process integration test + dev orchestrator |
|
|
325
|
-
| M8 | Web dashboard (Next.js + SSE) |
|
|
326
|
-
| M9 | Skill system (auto-discovered behavioral protocols) + memory tooling refresh |
|
|
327
|
-
|
|
328
|
-
The full design discussions and PR references live in the [closed issues](https://github.com/beevibe-ai/beevibe/issues?q=is%3Aissue+is%3Aclosed) — each milestone has one tracking issue.
|
|
329
|
-
|
|
330
|
-
## Contributing
|
|
331
|
-
|
|
332
|
-
Issues and PRs welcome. For non-trivial changes, open an issue first so we can align on direction.
|
|
333
|
-
|
|
334
|
-
A few conventions worth knowing:
|
|
335
|
-
|
|
336
|
-
- **Plan first, build second.** Each milestone is scoped end-to-end (schema → types → unit tests → integration test) before any code is written. See `tasks/` (gitignored) for the workflow.
|
|
337
|
-
- **TDD where it pays.** Domain types and pure services are test-first. Adapters get integration tests against real services (Postgres, real LLM keys).
|
|
338
|
-
- **No parallel systems.** When a feature replaces an older one, the old code is deleted in the same PR — no `if (new) ... else if (old) ...` branches.
|
|
339
|
-
|
|
340
|
-
## License
|
|
341
|
-
|
|
342
|
-
The Beevibe source code is licensed under the [Apache License 2.0](./LICENSE).
|
|
343
|
-
|
|
344
|
-
The **Beevibe** name and logo are trademarks of the project — see
|
|
345
|
-
[TRADEMARK.md](./TRADEMARK.md). Apache 2.0 grants rights to the source
|
|
346
|
-
code; it does not grant rights to use the project's name or marks. Forks
|
|
347
|
-
and derivative works are welcome under any name that is not "Beevibe."
|
|
348
|
-
|
|
349
|
-
Contributing? See [CONTRIBUTING.md](./CONTRIBUTING.md). All commits must
|
|
350
|
-
include a `Signed-off-by:` trailer (the [Developer Certificate of Origin
|
|
351
|
-
v1.1](./CONTRIBUTING.md#developer-certificate-of-origin-v11)) — `git commit -s`.
|
|
352
|
-
|
|
353
|
-
Copyright (c) 2026 Zhe Pang. Rights to be assigned to Beevibe Inc. upon
|
|
354
|
-
its formation.
|
package/dist/main.js
CHANGED
|
@@ -4,6 +4,205 @@
|
|
|
4
4
|
import { hostname, userInfo } from "node:os";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
|
|
7
|
+
// ../../node_modules/.pnpm/nanoid@5.1.9/node_modules/nanoid/index.js
|
|
8
|
+
import { webcrypto as crypto } from "node:crypto";
|
|
9
|
+
var POOL_SIZE_MULTIPLIER = 128;
|
|
10
|
+
var pool;
|
|
11
|
+
var poolOffset;
|
|
12
|
+
function fillPool(bytes) {
|
|
13
|
+
if (!pool || pool.length < bytes) {
|
|
14
|
+
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
|
15
|
+
crypto.getRandomValues(pool);
|
|
16
|
+
poolOffset = 0;
|
|
17
|
+
} else if (poolOffset + bytes > pool.length) {
|
|
18
|
+
crypto.getRandomValues(pool);
|
|
19
|
+
poolOffset = 0;
|
|
20
|
+
}
|
|
21
|
+
poolOffset += bytes;
|
|
22
|
+
}
|
|
23
|
+
function random(bytes) {
|
|
24
|
+
fillPool(bytes |= 0);
|
|
25
|
+
return pool.subarray(poolOffset - bytes, poolOffset);
|
|
26
|
+
}
|
|
27
|
+
function customRandom(alphabet, defaultSize, getRandom) {
|
|
28
|
+
let safeByteCutoff = 256 - 256 % alphabet.length;
|
|
29
|
+
if (safeByteCutoff === 256) {
|
|
30
|
+
let mask = alphabet.length - 1;
|
|
31
|
+
return (size = defaultSize) => {
|
|
32
|
+
if (!size)
|
|
33
|
+
return "";
|
|
34
|
+
let id = "";
|
|
35
|
+
while (true) {
|
|
36
|
+
let bytes = getRandom(size);
|
|
37
|
+
let i = size;
|
|
38
|
+
while (i--) {
|
|
39
|
+
id += alphabet[bytes[i] & mask];
|
|
40
|
+
if (id.length >= size)
|
|
41
|
+
return id;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
let step = Math.ceil(1.6 * 256 * defaultSize / safeByteCutoff);
|
|
47
|
+
return (size = defaultSize) => {
|
|
48
|
+
if (!size)
|
|
49
|
+
return "";
|
|
50
|
+
let id = "";
|
|
51
|
+
while (true) {
|
|
52
|
+
let bytes = getRandom(step);
|
|
53
|
+
let i = step;
|
|
54
|
+
while (i--) {
|
|
55
|
+
if (bytes[i] < safeByteCutoff) {
|
|
56
|
+
id += alphabet[bytes[i] % alphabet.length];
|
|
57
|
+
if (id.length >= size)
|
|
58
|
+
return id;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function customAlphabet(alphabet, size = 21) {
|
|
65
|
+
return customRandom(alphabet, size, random);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ../core/dist/domain/ids.js
|
|
69
|
+
var alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
70
|
+
var nanoid12 = customAlphabet(alphabet, 12);
|
|
71
|
+
// ../core/dist/domain/core-memory.js
|
|
72
|
+
var DEFAULT_BLOCK_TEMPLATES = {
|
|
73
|
+
ic: [
|
|
74
|
+
{
|
|
75
|
+
block_name: "tag_line",
|
|
76
|
+
char_limit: 100,
|
|
77
|
+
is_system: true,
|
|
78
|
+
initial_content: "",
|
|
79
|
+
description: "One-line headline of my enduring specialization — shown on agent " + "cards in the UI. Describes what I'm an expert in, not what " + "project I'm currently on. Examples: 'Go backend specialist " + "(Chi/sqlc, websockets)', 'Next.js + React UI lead'. Max 100 " + "chars. Update only when my specialization itself shifts."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
block_name: "persona",
|
|
83
|
+
char_limit: 2000,
|
|
84
|
+
is_system: true,
|
|
85
|
+
initial_content: "",
|
|
86
|
+
description: "Who I am and how I work — my role and working style, persistent " + "across every project I touch. 1-3 sentences in first person. " + "Update when my self-conception genuinely shifts (acquired a " + "major capability, refined my approach). NOT my current project " + "(that's `active_context`), NOT my domain scope (that's `domain`)."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
block_name: "domain",
|
|
90
|
+
char_limit: 2000,
|
|
91
|
+
is_system: true,
|
|
92
|
+
initial_content: "",
|
|
93
|
+
description: "The areas I specialize in ACROSS all projects — my enduring " + "expertise. As I work on more projects in my domain, this can " + "deepen (becoming truly expert in narrower sub-areas). Bullet " + "format. Example: 'Go backend services: HTTP via Chi/echo, DB " + "via sqlc, websockets via gorilla, distribution via goreleaser.' " + "NOT project-specific paths (those go in `active_context`). NOT " + "the rules I follow (those go in `constraints`)."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
block_name: "active_context",
|
|
97
|
+
char_limit: 2000,
|
|
98
|
+
is_system: true,
|
|
99
|
+
initial_content: "",
|
|
100
|
+
description: "What I'm currently working on — the specific project and its " + "in-flight details. Bullet format. Example: 'Project: " + "github.com/multica-ai/multica. Local clone: /tmp/multica-repo. " + "Owned paths in this project: server/cmd/**, server/internal/**. " + "Current task: task_xfQpuEHWjbvk.' Transient — rewrite when the " + "project changes. This is where ALL project/codebase-specific " + "details live, NOT in `domain`."
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
block_name: "constraints",
|
|
104
|
+
char_limit: 2000,
|
|
105
|
+
is_system: true,
|
|
106
|
+
initial_content: "",
|
|
107
|
+
description: "Hard rules I follow — non-negotiable conventions and " + "coordination boundaries. Mix of persistent rules ('queries " + "always via sqlc — never hand-write the DB layer') and " + "project-specific rules ('read /CLAUDE.md before changes in " + "this codebase'). Bullet format. Reference docs by path, not " + "content."
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
team: [
|
|
111
|
+
{
|
|
112
|
+
block_name: "tag_line",
|
|
113
|
+
char_limit: 100,
|
|
114
|
+
is_system: true,
|
|
115
|
+
initial_content: "",
|
|
116
|
+
description: "One-line headline of my enduring role — shown on agent cards. " + "Describes the team I lead, not the project we're on. Examples: " + "'Daniel's team — orchestrates 3 backend/frontend/platform " + "specialists', 'Solo lead — driving a small team for hire'. " + "Max 100 chars."
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
block_name: "persona",
|
|
120
|
+
char_limit: 2000,
|
|
121
|
+
is_system: true,
|
|
122
|
+
initial_content: "",
|
|
123
|
+
description: "Who I am as a team lead — my orchestration style and how I " + "delegate. 1-3 sentences in first person. Persistent across " + "projects. NOT my team roster (that's `team_members`), NOT the " + "current work (that's `active_work`)."
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
block_name: "team_members",
|
|
127
|
+
char_limit: 3000,
|
|
128
|
+
is_system: true,
|
|
129
|
+
initial_content: "",
|
|
130
|
+
description: "Roster of my direct reports — for each: name, agent_id, " + "specialization (NOT project assignment — same agents stay over " + "time as we work different projects). Bullet format. Update " + "when subordinates are spawned/archived/reassigned."
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
block_name: "active_work",
|
|
134
|
+
char_limit: 2000,
|
|
135
|
+
is_system: true,
|
|
136
|
+
initial_content: "",
|
|
137
|
+
description: "What my team is currently working on — the active project + " + "high-level work in flight across specialists. Bullet format. " + "Example: 'Project: github.com/multica-ai/multica. Backend " + "specialist running CLI audit (task_xfQpuEHWjbvk); Frontend on " + "standby.' Transient — rewrite on project shifts."
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
block_name: "patterns",
|
|
141
|
+
char_limit: 2000,
|
|
142
|
+
is_system: true,
|
|
143
|
+
initial_content: "",
|
|
144
|
+
description: "Cross-project patterns I've observed in how my team operates — " + "what works, what trips them up. Persistent. Example: 'When I " + "assign an audit task, the IC tends to produce thorough work " + "products but forgets to save_memory mid-pass.' NOT specific " + "findings about a codebase (those go in archival memory via " + "save_memory)."
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
org: [
|
|
148
|
+
{
|
|
149
|
+
block_name: "tag_line",
|
|
150
|
+
char_limit: 100,
|
|
151
|
+
is_system: true,
|
|
152
|
+
initial_content: "",
|
|
153
|
+
description: "One-line headline of my org-level role — shown on agent cards. " + "Describes the scope I oversee, not the current project. " + "Examples: 'Eng org lead — 3 teams (product/platform/infra)'. " + "Max 100 chars."
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
block_name: "persona",
|
|
157
|
+
char_limit: 2000,
|
|
158
|
+
is_system: true,
|
|
159
|
+
initial_content: "",
|
|
160
|
+
description: "Who I am as an org leader — my decision style and how I balance " + "across teams. 1-3 sentences. Persistent."
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
block_name: "teams",
|
|
164
|
+
char_limit: 3000,
|
|
165
|
+
is_system: true,
|
|
166
|
+
initial_content: "",
|
|
167
|
+
description: "Teams under my oversight — for each: name, team-lead agent_id, " + "scope. Persistent identity. Bullet format."
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
block_name: "strategy",
|
|
171
|
+
char_limit: 2000,
|
|
172
|
+
is_system: true,
|
|
173
|
+
initial_content: "",
|
|
174
|
+
description: "Cross-project / cross-team direction I'm driving. Higher-level " + "than active_work. Examples: 'Q2 focus: ship Multica self-host " + "v1', 'Hiring: prioritize backend over frontend this quarter'."
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
block_name: "decisions",
|
|
178
|
+
char_limit: 2000,
|
|
179
|
+
is_system: true,
|
|
180
|
+
initial_content: "",
|
|
181
|
+
description: "Cross-team decisions I've resolved — bullet log. Each entry: " + "what was decided, when, why. Persistent record so the same " + "question doesn't come back up."
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
};
|
|
185
|
+
// ../core/dist/domain/memory.js
|
|
186
|
+
var FACT_TYPE_DESCRIPTIONS = {
|
|
187
|
+
belief: "A position you hold based on multiple sessions of evidence. A lasting view, " + "not a fleeting reaction to one session.",
|
|
188
|
+
pattern: "A recurring observation about the codebase or the domain you work in — " + "knowledge another agent could reuse. NOT a pattern about your own behavior " + "(your search habits, your memory-keeping, how you should have responded next " + "time). Save the thing you learned about the world, not a note-to-self about " + "how to behave.",
|
|
189
|
+
gotcha: "A non-obvious thing-that-bites — a footgun that's easy to step on, where the " + "surprise itself is the value. Concrete and reusable across future tasks in " + "the same area.",
|
|
190
|
+
preference: `A user's stated durable rule. Trigger words: "always", "from now on", ` + '"every time", "as a default", "going forward". Do NOT save preferences ' + "for one-off requests scoped to a specific task, session, or work-product " + '("after this task", "for this audit", "now"). When in doubt, just do the ' + "thing once without saving — the user can restate it if they want it to stick.",
|
|
191
|
+
decision: 'A chosen path with rationale. The "why" that future-you (or another agent) ' + "needs to understand why the codebase / approach looks the way it does — not " + 'the mechanical "what" (read the code for that).'
|
|
192
|
+
};
|
|
193
|
+
// ../core/dist/domain/runtime.js
|
|
194
|
+
var KNOWN_CLIS = ["claude", "codex", "opencode"];
|
|
195
|
+
var RUNTIME_HEARTBEAT_INTERVAL_MS = 15000;
|
|
196
|
+
// ../core/dist/auth/api-key.js
|
|
197
|
+
var KEY_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
198
|
+
var nanoid24 = customAlphabet(KEY_ALPHABET, 24);
|
|
199
|
+
// ../core/dist/auth/password.js
|
|
200
|
+
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
|
|
201
|
+
import { promisify } from "node:util";
|
|
202
|
+
var scryptAsync = promisify(scrypt);
|
|
203
|
+
var SCRYPT_N = 16384;
|
|
204
|
+
var SCRYPT_R = 8;
|
|
205
|
+
var SCRYPT_MAXMEM = 128 * SCRYPT_N * SCRYPT_R * 2;
|
|
7
206
|
// src/config.ts
|
|
8
207
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
208
|
import { homedir } from "node:os";
|
|
@@ -26,7 +225,6 @@ function saveConfig(cfg) {
|
|
|
26
225
|
}
|
|
27
226
|
|
|
28
227
|
// src/setup.ts
|
|
29
|
-
var KNOWN_CLIS = ["claude", "codex", "opencode"];
|
|
30
228
|
async function runSetup(options) {
|
|
31
229
|
if (!/^https?:\/\//.test(options.apiUrl)) {
|
|
32
230
|
throw new Error("--api must be an http(s) URL");
|
|
@@ -83,9 +281,9 @@ function detectClis() {
|
|
|
83
281
|
}
|
|
84
282
|
|
|
85
283
|
// ../core/dist/adapters/local-workspace/manager.js
|
|
86
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
284
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
87
285
|
import { homedir as homedir2 } from "node:os";
|
|
88
|
-
import { join as join2 } from "node:path";
|
|
286
|
+
import { isAbsolute, join as join2 } from "node:path";
|
|
89
287
|
|
|
90
288
|
// ../core/dist/services/skills/sync.js
|
|
91
289
|
import { promises as fs } from "node:fs";
|
|
@@ -216,35 +414,38 @@ class LocalWorkspaceManager {
|
|
|
216
414
|
root;
|
|
217
415
|
constructor(config) {
|
|
218
416
|
this.config = config;
|
|
219
|
-
this.root = config.workspaceRoot
|
|
417
|
+
this.root = config.workspaceRoot || join2(homedir2(), ".beevibe", "workspaces");
|
|
418
|
+
if (!isAbsolute(this.root)) {
|
|
419
|
+
throw new Error(`LocalWorkspaceManager: workspaceRoot must be absolute, got "${this.root}"`);
|
|
420
|
+
}
|
|
220
421
|
}
|
|
221
|
-
async ensureWorkspace({ agent }) {
|
|
222
|
-
const path2 = join2(this.root,
|
|
422
|
+
async ensureWorkspace({ agent: agent2 }) {
|
|
423
|
+
const path2 = join2(this.root, agent2.id);
|
|
223
424
|
mkdirSync2(path2, { recursive: true, mode: 448 });
|
|
425
|
+
if (!agent2.api_key) {
|
|
426
|
+
throw new Error(`Cannot write mcp-config.json for agent ${agent2.id}: agent.api_key is missing`);
|
|
427
|
+
}
|
|
224
428
|
const configPath = join2(path2, "mcp-config.json");
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
writeFileSync2(configPath, buildMcpConfig(agent.api_key, this.config.mcpServerUrl), {
|
|
230
|
-
mode: 384
|
|
231
|
-
});
|
|
429
|
+
const expected = buildMcpConfig(agent2.api_key, this.config.mcpServerUrl);
|
|
430
|
+
const needsWrite = !existsSync2(configPath) || readFileSync2(configPath, "utf-8") !== expected;
|
|
431
|
+
if (needsWrite) {
|
|
432
|
+
writeFileSync2(configPath, expected, { mode: 384 });
|
|
232
433
|
}
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
if (!
|
|
236
|
-
throw new Error(`No runtime registered for agent ${
|
|
434
|
+
const workspace2 = { path: path2 };
|
|
435
|
+
const runtime3 = this.config.runtimeRegistry[agent2.runtime_config.type];
|
|
436
|
+
if (!runtime3) {
|
|
437
|
+
throw new Error(`No runtime registered for agent ${agent2.id} (runtime_config.type='${agent2.runtime_config.type}')`);
|
|
237
438
|
}
|
|
238
439
|
await syncSkills({
|
|
239
440
|
sourceDir: this.config.skillsSourceDir,
|
|
240
|
-
targetDir:
|
|
241
|
-
filter: tierFilterFor(
|
|
441
|
+
targetDir: runtime3.skillsDir(workspace2),
|
|
442
|
+
filter: tierFilterFor(agent2.hierarchy_level),
|
|
242
443
|
namespacePrefix: "beevibe"
|
|
243
444
|
});
|
|
244
|
-
return
|
|
445
|
+
return workspace2;
|
|
245
446
|
}
|
|
246
|
-
async removeWorkspace(
|
|
247
|
-
rmSync(
|
|
447
|
+
async removeWorkspace(workspace2) {
|
|
448
|
+
rmSync(workspace2.path, { recursive: true, force: true });
|
|
248
449
|
}
|
|
249
450
|
}
|
|
250
451
|
function buildMcpConfig(apiKey, mcpServerUrl) {
|
|
@@ -594,12 +795,15 @@ function parseClaudeMessages(messages, exitCode) {
|
|
|
594
795
|
} : undefined;
|
|
595
796
|
return {
|
|
596
797
|
status: succeeded ? "completed" : "failed",
|
|
597
|
-
output: output || (succeeded ? "Session completed." :
|
|
798
|
+
output: output || (succeeded ? "Session completed." : bareCliExitMessage(exitCode)),
|
|
598
799
|
transcript: transcript || undefined,
|
|
599
800
|
usage,
|
|
600
801
|
cli_session_id: sessionId
|
|
601
802
|
};
|
|
602
803
|
}
|
|
804
|
+
function bareCliExitMessage(exitCode) {
|
|
805
|
+
return `CLI exited with code ${exitCode}`;
|
|
806
|
+
}
|
|
603
807
|
|
|
604
808
|
// ../core/dist/adapters/claude-code/runtime.js
|
|
605
809
|
var NESTING_GUARD_VARS = [
|
|
@@ -644,13 +848,13 @@ class ClaudeCodeRuntime {
|
|
|
644
848
|
if (context.system_prompt_append.length > 0) {
|
|
645
849
|
args.push("--append-system-prompt", context.system_prompt_append);
|
|
646
850
|
}
|
|
647
|
-
const
|
|
851
|
+
const env2 = { ...process.env };
|
|
648
852
|
for (const key of NESTING_GUARD_VARS)
|
|
649
|
-
delete
|
|
853
|
+
delete env2[key];
|
|
650
854
|
for (const key of ANTHROPIC_AUTH_VARS)
|
|
651
|
-
delete
|
|
855
|
+
delete env2[key];
|
|
652
856
|
if (context.env)
|
|
653
|
-
Object.assign(
|
|
857
|
+
Object.assign(env2, context.env);
|
|
654
858
|
const messages = [];
|
|
655
859
|
let pending = "";
|
|
656
860
|
const handleLine = (line) => {
|
|
@@ -668,7 +872,7 @@ class ClaudeCodeRuntime {
|
|
|
668
872
|
command: this.config.command ?? "claude",
|
|
669
873
|
args,
|
|
670
874
|
cwd,
|
|
671
|
-
env,
|
|
875
|
+
env: env2,
|
|
672
876
|
stdin: context.intent,
|
|
673
877
|
abortSignal: context.abort_signal,
|
|
674
878
|
onSpawn: ({ pid, process_group_id }) => {
|
|
@@ -700,10 +904,14 @@ class ClaudeCodeRuntime {
|
|
|
700
904
|
};
|
|
701
905
|
}
|
|
702
906
|
const parsed = parseClaudeMessages(messages, result.exitCode);
|
|
907
|
+
const STDERR_TAIL_BYTES = 4096;
|
|
908
|
+
const stderrTail = parsed.status === "failed" && result.stderr ? result.stderr.slice(-STDERR_TAIL_BYTES) : undefined;
|
|
703
909
|
return {
|
|
704
910
|
...parsed,
|
|
705
911
|
process_pid: result.pid ?? undefined,
|
|
706
|
-
process_group_id: result.process_group_id ?? undefined
|
|
912
|
+
process_group_id: result.process_group_id ?? undefined,
|
|
913
|
+
exit_code: result.exitCode,
|
|
914
|
+
...stderrTail ? { stderr: stderrTail } : {}
|
|
707
915
|
};
|
|
708
916
|
}
|
|
709
917
|
async healthCheck() {
|
|
@@ -724,8 +932,8 @@ class ClaudeCodeRuntime {
|
|
|
724
932
|
}
|
|
725
933
|
}
|
|
726
934
|
async shutdown() {}
|
|
727
|
-
skillsDir(
|
|
728
|
-
return join3(
|
|
935
|
+
skillsDir(workspace2) {
|
|
936
|
+
return join3(workspace2.path, ".claude", "skills");
|
|
729
937
|
}
|
|
730
938
|
}
|
|
731
939
|
|
|
@@ -803,7 +1011,8 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
803
1011
|
runtime_config: { type: "claude" }
|
|
804
1012
|
};
|
|
805
1013
|
const ws = await deps.workspaceManager.ensureWorkspace({ agent: syntheticAgent });
|
|
806
|
-
|
|
1014
|
+
console.log(`[daemon/spawn] sess=${payload.session_id} agent=${payload.agent_id} type=${payload.type} cwd=${ws.path}`);
|
|
1015
|
+
const runtime3 = deps.runtime ?? new ClaudeCodeRuntime;
|
|
807
1016
|
const buffer = [];
|
|
808
1017
|
let flushTimer;
|
|
809
1018
|
const flush = async () => {
|
|
@@ -839,7 +1048,7 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
839
1048
|
let result;
|
|
840
1049
|
let runError;
|
|
841
1050
|
try {
|
|
842
|
-
result = await
|
|
1051
|
+
result = await runtime3.execute({
|
|
843
1052
|
intent: payload.intent,
|
|
844
1053
|
workspace: ws,
|
|
845
1054
|
system_prompt_append: payload.system_prompt_append,
|
|
@@ -859,15 +1068,25 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
859
1068
|
}
|
|
860
1069
|
await flush();
|
|
861
1070
|
const status = runError ? "failed" : result?.status === "completed" ? "succeeded" : result?.status === "cancelled" ? "cancelled" : "failed";
|
|
1071
|
+
const errorDetail = runError?.message ?? result?.stderr;
|
|
862
1072
|
const done = {
|
|
863
1073
|
session_id: payload.session_id,
|
|
864
1074
|
status,
|
|
865
1075
|
cli_session_id: result?.cli_session_id,
|
|
866
1076
|
result_summary: result?.output ?? "",
|
|
867
|
-
exit_code:
|
|
868
|
-
error:
|
|
1077
|
+
exit_code: result?.exit_code ?? null,
|
|
1078
|
+
error: errorDetail,
|
|
869
1079
|
usage: result?.usage
|
|
870
1080
|
};
|
|
1081
|
+
if (status === "succeeded") {
|
|
1082
|
+
console.log(`[daemon/spawn] sess=${payload.session_id} exit=0`);
|
|
1083
|
+
} else {
|
|
1084
|
+
console.error(`[daemon/spawn] sess=${payload.session_id} status=${status} exit=${done.exit_code}` + (errorDetail ? `
|
|
1085
|
+
error:
|
|
1086
|
+
${errorDetail.split(`
|
|
1087
|
+
`).join(`
|
|
1088
|
+
`)}` : ""));
|
|
1089
|
+
}
|
|
871
1090
|
try {
|
|
872
1091
|
await deps.api.post("/runtime/done", done);
|
|
873
1092
|
} catch (err) {
|
|
@@ -877,7 +1096,6 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
877
1096
|
|
|
878
1097
|
// src/claimer.ts
|
|
879
1098
|
var DEFAULT_POLL_MS = 30000;
|
|
880
|
-
var DEFAULT_HEARTBEAT_MS = 15000;
|
|
881
1099
|
var DEFAULT_WS_RECONNECT_MAX_MS = 30000;
|
|
882
1100
|
|
|
883
1101
|
class Claimer {
|
|
@@ -894,7 +1112,7 @@ class Claimer {
|
|
|
894
1112
|
constructor(cfg) {
|
|
895
1113
|
this.cfg = cfg;
|
|
896
1114
|
this.pollIntervalMs = cfg.pollIntervalMs ?? DEFAULT_POLL_MS;
|
|
897
|
-
this.heartbeatIntervalMs = cfg.heartbeatIntervalMs ??
|
|
1115
|
+
this.heartbeatIntervalMs = cfg.heartbeatIntervalMs ?? RUNTIME_HEARTBEAT_INTERVAL_MS;
|
|
898
1116
|
this.wsReconnectMaxDelayMs = cfg.wsReconnectMaxDelayMs ?? DEFAULT_WS_RECONNECT_MAX_MS;
|
|
899
1117
|
}
|
|
900
1118
|
start() {
|
|
@@ -991,6 +1209,7 @@ class Claimer {
|
|
|
991
1209
|
const payload = await this.cfg.api.claim(runtimeId);
|
|
992
1210
|
if (!payload)
|
|
993
1211
|
return;
|
|
1212
|
+
console.log(`[daemon/claim] sess=${payload.session_id} agent=${payload.agent_id} runtime=${runtimeId}`);
|
|
994
1213
|
const ctrl = this.cfg.supervisor.start(payload.session_id);
|
|
995
1214
|
runDispatch({ api: this.cfg.api, workspaceManager: this.cfg.workspaceManager }, payload, ctrl.signal).catch((err) => console.error(`[daemon] dispatch ${payload.session_id} failed:`, err instanceof Error ? err.message : String(err))).finally(() => this.cfg.supervisor.finish(payload.session_id));
|
|
996
1215
|
}
|
|
@@ -1157,7 +1376,7 @@ var PLATFORM_ASSETS = {
|
|
|
1157
1376
|
"linux-arm64": "beevibe-daemon-linux-arm64"
|
|
1158
1377
|
};
|
|
1159
1378
|
function currentVersion() {
|
|
1160
|
-
return "0.1.
|
|
1379
|
+
return "0.1.2";
|
|
1161
1380
|
}
|
|
1162
1381
|
function isCompiledBinary() {
|
|
1163
1382
|
if (!process.versions.bun)
|
package/package.json
CHANGED