zooid 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +36 -444
  3. package/dist/bin.js +1891 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/chunk-SUPTPSN3.js +31434 -0
  6. package/dist/chunk-SUPTPSN3.js.map +1 -0
  7. package/dist/index.d.ts +64 -0
  8. package/dist/index.js +4 -3790
  9. package/dist/index.js.map +1 -0
  10. package/dist/web/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  11. package/dist/web/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  12. package/dist/web/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  13. package/dist/web/assets/index-C2MAIew3.css +1 -0
  14. package/dist/web/assets/index-CqfWXqvO.js +118066 -0
  15. package/dist/web/assets/index-Cz5fEZBc.js +295 -0
  16. package/dist/web/assets/reaction-picker-emoji-cME0vOsX.js +716 -0
  17. package/dist/web/favicon.svg +1 -0
  18. package/dist/web/index.html +14 -0
  19. package/package.json +43 -22
  20. package/src/bin.ts +115 -0
  21. package/src/bootstrap/admin.test.ts +69 -0
  22. package/src/bootstrap/admin.ts +37 -0
  23. package/src/bootstrap/configs.test.ts +24 -0
  24. package/src/bootstrap/configs.ts +64 -0
  25. package/src/bootstrap/data-layout.test.ts +25 -0
  26. package/src/bootstrap/data-layout.ts +20 -0
  27. package/src/bootstrap/derive.test.ts +74 -0
  28. package/src/bootstrap/derive.ts +40 -0
  29. package/src/bootstrap/paths.test.ts +22 -0
  30. package/src/bootstrap/paths.ts +27 -0
  31. package/src/bootstrap/tokens.test.ts +44 -0
  32. package/src/bootstrap/tokens.ts +43 -0
  33. package/src/build-registry.context.test.ts +61 -0
  34. package/src/build-registry.test.ts +59 -0
  35. package/src/build-registry.ts +343 -0
  36. package/src/build-registry.zod043.test.ts +64 -0
  37. package/src/build-registry.zod044.test.ts +261 -0
  38. package/src/commands/dev-cascade.test.ts +38 -0
  39. package/src/commands/dev-cascade.ts +35 -0
  40. package/src/commands/dev-data-layout.test.ts +55 -0
  41. package/src/commands/dev.ts +316 -0
  42. package/src/commands/init/generators.test.ts +97 -0
  43. package/src/commands/init/generators.ts +124 -0
  44. package/src/commands/init/prompts.ts +123 -0
  45. package/src/commands/init/registry.test.ts +33 -0
  46. package/src/commands/init/registry.ts +75 -0
  47. package/src/commands/init/sniff.test.ts +41 -0
  48. package/src/commands/init/sniff.ts +30 -0
  49. package/src/commands/init.test.ts +131 -0
  50. package/src/commands/init.ts +158 -0
  51. package/src/commands/logs.test.ts +105 -0
  52. package/src/commands/logs.ts +108 -0
  53. package/src/commands/start.ts +28 -0
  54. package/src/commands/status.test.ts +73 -0
  55. package/src/commands/status.ts +105 -0
  56. package/src/daemon/start-daemon.test.ts +67 -0
  57. package/src/daemon/start-daemon.ts +270 -0
  58. package/src/index.ts +2 -0
  59. package/src/observability/capture-agent.ts +58 -0
  60. package/src/observability/capture-tuwunel.test.ts +57 -0
  61. package/src/observability/capture-tuwunel.ts +24 -0
  62. package/src/observability/file-sink.test.ts +68 -0
  63. package/src/observability/file-sink.ts +78 -0
  64. package/src/observability/observability.integration.test.ts +162 -0
  65. package/src/observability/paths.test.ts +104 -0
  66. package/src/observability/paths.ts +94 -0
  67. package/src/prepull-images.test.ts +149 -0
  68. package/src/prepull-images.ts +100 -0
  69. package/src/services/tuwunel.test.ts +64 -0
  70. package/src/services/tuwunel.ts +91 -0
  71. package/src/web/resolve.test.ts +50 -0
  72. package/src/web/resolve.ts +31 -0
  73. package/src/web/static.test.ts +51 -0
  74. package/src/web/static.ts +61 -0
  75. package/src/web/watch.ts +103 -0
  76. package/dist/chunk-67ZRMVHO.js +0 -174
  77. package/dist/chunk-AR456MHY.js +0 -29
  78. package/dist/chunk-VBGU2NST.js +0 -139
  79. package/dist/client-4VMFEFDX.js +0 -22
  80. package/dist/config-2KK5GX42.js +0 -27
  81. package/dist/template-T5IB4YWC.js +0 -92
package/dist/bin.js ADDED
@@ -0,0 +1,1891 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ApprovalCorrelator,
4
+ MatrixClient,
5
+ SpawnRegistry,
6
+ buildAcpRegistry,
7
+ createMatrixTransport,
8
+ ensureWorkforceSpace,
9
+ findConfigFile,
10
+ findHttpTransport,
11
+ findMatrixTransport,
12
+ loadZooidConfig,
13
+ mergeCliFlags,
14
+ renderRegistration,
15
+ startDaemonSocketServer,
16
+ startWorkforcePublisher
17
+ } from "./chunk-SUPTPSN3.js";
18
+
19
+ // src/bin.ts
20
+ import { resolve as resolve5 } from "path";
21
+ import { cac } from "cac";
22
+
23
+ // src/commands/dev.ts
24
+ import chalk from "chalk";
25
+ import { Listr } from "listr2";
26
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
27
+ import { dirname as dirname6, resolve as resolve2 } from "path";
28
+ import { fileURLToPath } from "url";
29
+ import { serve as serve2 } from "@hono/node-server";
30
+
31
+ // src/bootstrap/admin.ts
32
+ async function ensureAdminUser(opts) {
33
+ const r = await fetch(`${opts.homeserver}/_matrix/client/v3/register`, {
34
+ method: "POST",
35
+ headers: { "content-type": "application/json" },
36
+ body: JSON.stringify({
37
+ auth: { type: "m.login.dummy" },
38
+ username: opts.username,
39
+ password: opts.password,
40
+ inhibit_login: true
41
+ })
42
+ });
43
+ if (r.ok) {
44
+ const j2 = await r.json();
45
+ return { created: true, userId: j2.user_id };
46
+ }
47
+ const j = await r.json();
48
+ if (j.errcode === "M_USER_IN_USE") {
49
+ const host = new URL(opts.homeserver).hostname;
50
+ return { created: false, userId: `@${opts.username}:${host}` };
51
+ }
52
+ throw new Error(
53
+ `register ${opts.username}: ${r.status} ${j.errcode ?? ""} ${j.error ?? ""}`.trim()
54
+ );
55
+ }
56
+
57
+ // src/bootstrap/configs.ts
58
+ import { mkdirSync, writeFileSync } from "fs";
59
+ var TUWUNEL_INTERNAL_PORT = 8448;
60
+ function renderTuwunelToml(opts) {
61
+ return [
62
+ "[global]",
63
+ `server_name = "${opts.serverName}"`,
64
+ 'database_path = "/var/lib/tuwunel/db"',
65
+ 'media_path = "/var/lib/tuwunel/media"',
66
+ 'appservice_dir = "/var/lib/tuwunel/registrations"',
67
+ "allow_registration = true",
68
+ "yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true",
69
+ "allow_local_presence = true",
70
+ 'address = ["0.0.0.0"]',
71
+ `port = [${TUWUNEL_INTERNAL_PORT}]`,
72
+ ""
73
+ ].join("\n");
74
+ }
75
+ function writeBootstrapConfigs(opts) {
76
+ const { paths, serverName, asToken, hsToken, senderLocalpart, userNamespace } = opts;
77
+ mkdirSync(paths.dbDir, { recursive: true });
78
+ mkdirSync(paths.mediaDir, { recursive: true });
79
+ mkdirSync(paths.registrationsDir, { recursive: true });
80
+ writeFileSync(paths.tuwunelTomlPath, renderTuwunelToml({ serverName }));
81
+ const yaml = renderRegistration({
82
+ id: "zooid",
83
+ url: `http://host.docker.internal:9099`,
84
+ homeserver: `http://localhost:${TUWUNEL_INTERNAL_PORT}`,
85
+ asToken,
86
+ hsToken,
87
+ senderLocalpart,
88
+ userNamespace,
89
+ // BotPool.bootstrap creates `#alias:<server>` rooms when missing — the
90
+ // AS needs an aliases namespace to legally claim them.
91
+ aliasNamespace: `#.*:${serverName}`,
92
+ // zooid dev shares @.*:<server_name> between bots and the predefined
93
+ // admin human, so the AS cannot claim the namespace exclusively.
94
+ exclusive: false
95
+ });
96
+ writeFileSync(paths.appserviceYamlPath, yaml);
97
+ }
98
+
99
+ // src/bootstrap/data-layout.ts
100
+ import { join } from "path";
101
+ function resolveDataLayout(dataRoot) {
102
+ const agentsDir = join(dataRoot, "agents");
103
+ return {
104
+ dataRoot,
105
+ matrixDir: join(dataRoot, "matrix"),
106
+ logsDir: join(dataRoot, "logs"),
107
+ agentsDir,
108
+ agentDir: (id) => join(agentsDir, id)
109
+ };
110
+ }
111
+
112
+ // src/bootstrap/derive.ts
113
+ var NAMESPACE_RE = /^@\.\*:([A-Za-z0-9.-]+)$/;
114
+ function deriveHomeserverShape(matrix, agentUserIds) {
115
+ const url = new URL(matrix.homeserver);
116
+ const host = url.hostname;
117
+ const port = url.port ? Number(url.port) : url.protocol === "https:" ? 443 : 80;
118
+ const m = NAMESPACE_RE.exec(matrix.user_namespace);
119
+ if (!m) {
120
+ throw new Error(
121
+ `user_namespace ${matrix.user_namespace}: expected the simple form '@.*:<server_name>'`
122
+ );
123
+ }
124
+ const serverName = m[1];
125
+ for (const userId of agentUserIds) {
126
+ if (!userId.endsWith(`:${serverName}`)) {
127
+ throw new Error(
128
+ `server_name mismatch: agent ${userId} does not end with :${serverName}`
129
+ );
130
+ }
131
+ }
132
+ return { host, port, serverName };
133
+ }
134
+
135
+ // src/bootstrap/paths.ts
136
+ import { join as join2 } from "path";
137
+ function resolvePaths(dataDir) {
138
+ const configDir = join2(dataDir, "config");
139
+ const registrationsDir = join2(configDir, "registrations");
140
+ return {
141
+ dataDir,
142
+ dbDir: join2(dataDir, "db"),
143
+ mediaDir: join2(dataDir, "media"),
144
+ configDir,
145
+ registrationsDir,
146
+ tuwunelTomlPath: join2(configDir, "tuwunel.toml"),
147
+ appserviceYamlPath: join2(registrationsDir, "zooid.yaml"),
148
+ envPath: join2(configDir, ".env")
149
+ };
150
+ }
151
+
152
+ // src/bootstrap/tokens.ts
153
+ import { randomBytes } from "crypto";
154
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
155
+ import { dirname } from "path";
156
+ var AS_RE = /^MATRIX_AS_TOKEN=(.+)$/m;
157
+ var HS_RE = /^MATRIX_HS_TOKEN=(.+)$/m;
158
+ function genToken(prefix) {
159
+ return `${prefix}-${randomBytes(24).toString("hex")}`;
160
+ }
161
+ function readTokens(envPath) {
162
+ if (!existsSync(envPath)) return null;
163
+ const text = readFileSync(envPath, "utf8");
164
+ const as = text.match(AS_RE)?.[1];
165
+ const hs = text.match(HS_RE)?.[1];
166
+ if (!as && !hs) return null;
167
+ if (!as || !hs) {
168
+ throw new Error(
169
+ `${envPath}: missing MATRIX_AS_TOKEN or MATRIX_HS_TOKEN. Delete the file to regenerate, or restore the missing value.`
170
+ );
171
+ }
172
+ return { asToken: as, hsToken: hs };
173
+ }
174
+ function ensureTokens(envPath) {
175
+ const existing = readTokens(envPath);
176
+ if (existing) return existing;
177
+ const tokens = { asToken: genToken("as"), hsToken: genToken("hs") };
178
+ mkdirSync2(dirname(envPath), { recursive: true });
179
+ const previous = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
180
+ const next = previous.trimEnd() + (previous ? "\n" : "") + `MATRIX_AS_TOKEN=${tokens.asToken}
181
+ MATRIX_HS_TOKEN=${tokens.hsToken}
182
+ `;
183
+ writeFileSync2(envPath, next, { mode: 384 });
184
+ return tokens;
185
+ }
186
+
187
+ // src/daemon/start-daemon.ts
188
+ import { readFileSync as readFileSync2 } from "fs";
189
+ import { mkdir, unlink } from "fs/promises";
190
+ import { tmpdir } from "os";
191
+ import { dirname as dirname2, join as join3 } from "path";
192
+ import { serve } from "@hono/node-server";
193
+
194
+ // ../transport-http/src/server.ts
195
+ import { Hono } from "hono";
196
+ import { streamSSE } from "hono/streaming";
197
+ import { timingSafeEqual, randomUUID } from "crypto";
198
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
199
+ function isAuthorized(token, header) {
200
+ if (!header || !header.startsWith("Bearer ")) return false;
201
+ const provided = header.slice("Bearer ".length);
202
+ if (provided.length !== token.length) return false;
203
+ return timingSafeEqual(Buffer.from(provided), Buffer.from(token));
204
+ }
205
+ function parsePromptBody(raw) {
206
+ if (!raw || typeof raw !== "object") return { error: "body must be a JSON object" };
207
+ const obj = raw;
208
+ if (typeof obj.prompt !== "string" || obj.prompt.length === 0) {
209
+ return { error: "missing or empty prompt" };
210
+ }
211
+ return { prompt: obj.prompt };
212
+ }
213
+ async function readJson(c) {
214
+ const raw = await c.req.text();
215
+ try {
216
+ return JSON.parse(raw);
217
+ } catch {
218
+ return { error: "body must be valid JSON" };
219
+ }
220
+ }
221
+ function parseDecisionBody(raw) {
222
+ if (!raw || typeof raw !== "object") return { error: "body must be a JSON object" };
223
+ const obj = raw;
224
+ if (obj.decision !== "allow" && obj.decision !== "cancel") {
225
+ return { error: 'decision must be "allow" or "cancel"' };
226
+ }
227
+ if (obj.decision === "allow" && typeof obj.option_id !== "string") {
228
+ return { error: 'option_id is required when decision is "allow"' };
229
+ }
230
+ return {
231
+ decision: obj.decision,
232
+ option_id: typeof obj.option_id === "string" ? obj.option_id : void 0
233
+ };
234
+ }
235
+ function approvalRequestFrame(handle) {
236
+ return JSON.stringify({
237
+ type: "approval.request",
238
+ approval_id: handle.approvalId,
239
+ session_id: handle.sessionId,
240
+ tool_call_id: handle.toolCallId,
241
+ options: handle.options
242
+ });
243
+ }
244
+ function createApp({
245
+ agents,
246
+ approvals,
247
+ token,
248
+ keepaliveMs = 3e4
249
+ }) {
250
+ const app = new Hono();
251
+ const streams = /* @__PURE__ */ new Map();
252
+ agents.onEvent = (_name, event) => {
253
+ const stream = streams.get(event.sessionId);
254
+ if (!stream) return;
255
+ void stream.writeSSE({ data: JSON.stringify(event) });
256
+ };
257
+ agents.onApprovalRequest = async (name, req) => {
258
+ const handle = approvals.register(name, req.sessionId, req, {
259
+ timeoutMs: agents.getApprovalTimeoutMs(name)
260
+ });
261
+ return handle.decisionPromise;
262
+ };
263
+ approvals.on("registered", (handle) => {
264
+ const stream = streams.get(handle.sessionId);
265
+ if (!stream) return;
266
+ void stream.writeSSE({ data: approvalRequestFrame(handle) });
267
+ });
268
+ approvals.on("timeout", ({ approvalId, sessionId }) => {
269
+ const stream = streams.get(sessionId);
270
+ if (!stream) return;
271
+ void stream.writeSSE({
272
+ data: JSON.stringify({ type: "approval.timeout", approval_id: approvalId, session_id: sessionId })
273
+ });
274
+ });
275
+ function checkAgent(c, name) {
276
+ if (!agents.hasAgent(name)) {
277
+ return c.json({ error: "unknown agent" }, 404);
278
+ }
279
+ return null;
280
+ }
281
+ function attachKeepalive(sse) {
282
+ if (!keepaliveMs || keepaliveMs <= 0) return null;
283
+ const timer = setInterval(() => {
284
+ void sse.writeSSE({ data: "", event: "keepalive" }).catch(() => {
285
+ });
286
+ }, keepaliveMs);
287
+ timer.unref?.();
288
+ return timer;
289
+ }
290
+ app.post("/agents/:name/sessions", async (c) => {
291
+ if (!isAuthorized(token, c.req.header("authorization"))) {
292
+ return c.json({ error: "unauthorized" }, 401);
293
+ }
294
+ const name = c.req.param("name");
295
+ const reject = checkAgent(c, name);
296
+ if (reject) return reject;
297
+ const parsed = await readJson(c);
298
+ if (parsed && typeof parsed === "object" && "error" in parsed) {
299
+ return c.json({ error: parsed.error }, 400);
300
+ }
301
+ const body = parsePromptBody(parsed);
302
+ if ("error" in body) return c.json({ error: body.error }, 400);
303
+ const threadId = randomUUID();
304
+ return streamSSE(c, async (sse) => {
305
+ let sessionId = null;
306
+ let keepalive = null;
307
+ try {
308
+ sessionId = await agents.ensureSession(name, threadId);
309
+ streams.set(sessionId, sse);
310
+ keepalive = attachKeepalive(sse);
311
+ await sse.writeSSE({
312
+ data: JSON.stringify({ type: "session.start", session_id: sessionId })
313
+ });
314
+ await sse.writeSSE({ data: JSON.stringify({ type: "turn.start" }) });
315
+ const result = await agents.prompt(name, {
316
+ threadId,
317
+ content: [{ type: "text", text: body.prompt }]
318
+ });
319
+ await sse.writeSSE({
320
+ data: JSON.stringify({ type: "turn.end", stop_reason: result.stopReason })
321
+ });
322
+ } catch (err) {
323
+ await sse.writeSSE({
324
+ data: JSON.stringify({
325
+ type: "turn.end",
326
+ stop_reason: "error",
327
+ error: err.message
328
+ })
329
+ });
330
+ } finally {
331
+ if (keepalive) clearInterval(keepalive);
332
+ if (sessionId) {
333
+ approvals.cancelSession(sessionId);
334
+ if (streams.get(sessionId) === sse) {
335
+ streams.delete(sessionId);
336
+ }
337
+ }
338
+ }
339
+ });
340
+ });
341
+ app.get("/agents/:name/sessions/:id/events", async (c) => {
342
+ if (!isAuthorized(token, c.req.header("authorization"))) {
343
+ return c.json({ error: "unauthorized" }, 401);
344
+ }
345
+ const name = c.req.param("name");
346
+ const reject = checkAgent(c, name);
347
+ if (reject) return reject;
348
+ const id = c.req.param("id");
349
+ if (!UUID_RE.test(id)) {
350
+ return c.json({ error: "session id must be a UUID" }, 400);
351
+ }
352
+ if (!streams.has(id)) {
353
+ return c.json({ error: "no in-flight session" }, 404);
354
+ }
355
+ return streamSSE(c, async (sse) => {
356
+ streams.set(id, sse);
357
+ const keepalive = attachKeepalive(sse);
358
+ for (const handle of approvals.listPending(id)) {
359
+ await sse.writeSSE({ data: approvalRequestFrame(handle) });
360
+ }
361
+ try {
362
+ while (streams.get(id) === sse) {
363
+ await new Promise((r) => setTimeout(r, 250));
364
+ }
365
+ } finally {
366
+ if (keepalive) clearInterval(keepalive);
367
+ }
368
+ });
369
+ });
370
+ app.post("/agents/:name/sessions/:sid/approvals/:approval_id", async (c) => {
371
+ if (!isAuthorized(token, c.req.header("authorization"))) {
372
+ return c.json({ error: "unauthorized" }, 401);
373
+ }
374
+ const name = c.req.param("name");
375
+ const reject = checkAgent(c, name);
376
+ if (reject) return reject;
377
+ const sid = c.req.param("sid");
378
+ const approvalId = c.req.param("approval_id");
379
+ const parsed = await readJson(c);
380
+ if (parsed && typeof parsed === "object" && "error" in parsed) {
381
+ return c.json({ error: parsed.error }, 400);
382
+ }
383
+ const body = parseDecisionBody(parsed);
384
+ if ("error" in body) return c.json({ error: body.error }, 400);
385
+ const decision = body.decision === "cancel" ? { decision: "cancel" } : { decision: "allow", optionId: body.option_id };
386
+ const ok = approvals.resolve(sid, approvalId, decision);
387
+ if (!ok) return c.json({ error: "unknown approval" }, 404);
388
+ return c.json({ ok: true });
389
+ });
390
+ app.post("/agents/:name/sessions/:sid/cancel", async (c) => {
391
+ if (!isAuthorized(token, c.req.header("authorization"))) {
392
+ return c.json({ error: "unauthorized" }, 401);
393
+ }
394
+ const name = c.req.param("name");
395
+ const reject = checkAgent(c, name);
396
+ if (reject) return reject;
397
+ const sid = c.req.param("sid");
398
+ approvals.cancelSession(sid);
399
+ return c.body(null, 204);
400
+ });
401
+ return app;
402
+ }
403
+
404
+ // src/prepull-images.ts
405
+ import { execFile } from "child_process";
406
+ import { promisify } from "util";
407
+ var pExecFile = promisify(execFile);
408
+ async function prepullImages(registry, opts) {
409
+ if (opts.skip || opts.runtime === "local") return;
410
+ const exec = opts.exec ?? defaultExec;
411
+ const log = opts.log ?? ((l) => console.log(l));
412
+ const images = /* @__PURE__ */ new Set();
413
+ for (const name of registry.agentNames()) {
414
+ const img = registry.resolveSpawnImage(name);
415
+ if (img) images.add(img);
416
+ }
417
+ const toPull = [];
418
+ let cached = 0;
419
+ if (opts.refresh) {
420
+ toPull.push(...images);
421
+ } else {
422
+ await Promise.all(
423
+ [...images].map(async (img) => {
424
+ const r = await exec(opts.engine, ["image", "inspect", img]);
425
+ if (r.code === 0) cached++;
426
+ else toPull.push(img);
427
+ })
428
+ );
429
+ }
430
+ log(
431
+ `[zooid] image prepull: ${images.size} unique images (${cached} cached, ${toPull.length} to pull)`
432
+ );
433
+ if (toPull.length === 0) return;
434
+ await Promise.all(
435
+ toPull.map(async (img) => {
436
+ log(`[zooid] ${img} pulling\u2026`);
437
+ const start = Date.now();
438
+ const r = await exec(opts.engine, ["pull", img]);
439
+ const secs = ((Date.now() - start) / 1e3).toFixed(1);
440
+ if (r.code !== 0) {
441
+ throw new Error(
442
+ `image prepull failed for ${img}:
443
+ ${r.stderr.trim() || "(no stderr)"}`
444
+ );
445
+ }
446
+ log(`[zooid] ${img} \u2713 ${secs}s`);
447
+ })
448
+ );
449
+ }
450
+ var defaultExec = async (cmd, args) => {
451
+ try {
452
+ const { stdout, stderr } = await pExecFile(cmd, args, {
453
+ maxBuffer: 16 * 1024 * 1024
454
+ });
455
+ return { code: 0, stdout, stderr };
456
+ } catch (err) {
457
+ const e = err;
458
+ return {
459
+ code: typeof e.code === "number" ? e.code : 1,
460
+ stdout: e.stdout ?? "",
461
+ stderr: e.stderr ?? String(e)
462
+ };
463
+ }
464
+ };
465
+
466
+ // src/daemon/start-daemon.ts
467
+ function listenAsync(server) {
468
+ return new Promise((resolve6) => {
469
+ const check = () => {
470
+ const addr = server.address();
471
+ if (addr && typeof addr === "object") resolve6(addr.port);
472
+ else setImmediate(check);
473
+ };
474
+ check();
475
+ });
476
+ }
477
+ function closeAsync(server) {
478
+ return new Promise((resolve6) => {
479
+ server.close(() => resolve6());
480
+ });
481
+ }
482
+ async function startDaemon(opts = {}) {
483
+ const cwd = opts.cwd ?? process.cwd();
484
+ const found = opts.configPath ? { path: opts.configPath } : findConfigFile(cwd);
485
+ if (!found) throw new Error("zooid.yaml is required");
486
+ const configDir = dirname2(found.path);
487
+ const base = loadZooidConfig(readFileSync2(found.path, "utf8"), { configDir });
488
+ const config = mergeCliFlags(base, opts.cliFlags ?? {});
489
+ const approvals = new ApprovalCorrelator();
490
+ const daemonSockPath = opts.agentsDir ? join3(opts.agentsDir, "..", "run", "context.sock") : join3(tmpdir(), `zooid-context-${process.pid}.sock`);
491
+ await mkdir(dirname2(daemonSockPath), { recursive: true }).catch(() => {
492
+ });
493
+ const contextSpawnRegistry = new SpawnRegistry();
494
+ let contextSocket = null;
495
+ try {
496
+ contextSocket = await startDaemonSocketServer({
497
+ sockPath: daemonSockPath,
498
+ registry: contextSpawnRegistry
499
+ });
500
+ } catch (err) {
501
+ console.warn("[context] daemon socket startup failed; zooid-context MCP disabled:", err);
502
+ }
503
+ const dataDir = opts.agentsDir ? dirname2(opts.agentsDir) : void 0;
504
+ const registry = buildAcpRegistry(config, {
505
+ approvals,
506
+ onTap: opts.onTap,
507
+ agentsDir: opts.agentsDir,
508
+ contextSpawnRegistry: contextSocket ? contextSpawnRegistry : void 0,
509
+ daemonSockPath: contextSocket ? daemonSockPath : void 0,
510
+ configDir,
511
+ dataDir,
512
+ daemonHome: process.env.HOME
513
+ });
514
+ const agentNames = Object.keys(config.agents);
515
+ if (config.runtime !== "local") {
516
+ await prepullImages(registry, {
517
+ engine: config.runtime === "podman" ? "podman" : "docker",
518
+ runtime: config.runtime,
519
+ skip: opts.noPrepull ?? process.env.ZOOID_NO_PREPULL === "1",
520
+ refresh: opts.refreshImages ?? process.env.ZOOID_REFRESH_IMAGES === "1",
521
+ log: opts.prepullLog
522
+ });
523
+ }
524
+ console.log(
525
+ `[context] socket=${daemonSockPath} status=${contextSocket ? "listening" : "disabled"} agents={${agentNames.map((n) => `${n}:${registry.hasContextSpawn(n) ? "yes" : "no"}`).join(", ")}}`
526
+ );
527
+ let server = null;
528
+ let stopped = false;
529
+ let resolveStopped;
530
+ const whenStopped = new Promise((r) => {
531
+ resolveStopped = r;
532
+ });
533
+ const matrix = findMatrixTransport(config);
534
+ let port;
535
+ if (matrix) {
536
+ const client = new MatrixClient({
537
+ homeserver: matrix.transport.homeserver,
538
+ asToken: matrix.transport.as_token
539
+ });
540
+ const bindings = [];
541
+ for (const a of Object.values(config.agents)) {
542
+ if (a.matrix?.transport !== matrix.name) continue;
543
+ const binding = {
544
+ name: a.name,
545
+ userId: a.matrix.user_id,
546
+ rooms: a.matrix.rooms,
547
+ trigger: a.matrix.trigger
548
+ };
549
+ if (a.matrix.display_name !== void 0) binding.displayName = a.matrix.display_name;
550
+ bindings.push(binding);
551
+ }
552
+ const transport = createMatrixTransport({
553
+ agents: registry,
554
+ approvals,
555
+ client,
556
+ bindings,
557
+ hsToken: matrix.transport.hs_token,
558
+ adminUserId: opts.adminUserId
559
+ });
560
+ const requestedPort = matrix.transport.port ?? 8080;
561
+ server = serve({ fetch: transport.app.fetch, port: requestedPort, hostname: "0.0.0.0" });
562
+ port = await listenAsync(server);
563
+ const serverName = matrix.transport.user_namespace.split(":").slice(1).join(":").replace(/\\?\)?$/, "") || new URL(matrix.transport.homeserver).hostname;
564
+ const asUserId = `@${matrix.transport.sender_localpart}:${serverName}`;
565
+ const spaceLocalpart = matrix.transport.space ?? "dev";
566
+ let spaceRoomId;
567
+ try {
568
+ spaceRoomId = await ensureWorkforceSpace({
569
+ client,
570
+ asUserId,
571
+ serverName,
572
+ spaceLocalpart,
573
+ preset: "public_chat"
574
+ });
575
+ console.log(`[matrix] ensured workforce space #${spaceLocalpart}:${serverName} \u2192 ${spaceRoomId}`);
576
+ } catch (err) {
577
+ console.warn("[matrix] workforce space provisioning failed:", err);
578
+ }
579
+ await transport.bootstrap({ spaceRoomId, asUserId });
580
+ if (spaceRoomId) {
581
+ try {
582
+ const publisher = await startWorkforcePublisher({
583
+ client,
584
+ spaceRoomId,
585
+ asUserId,
586
+ getAgents: () => bindings
587
+ });
588
+ console.log(`[matrix] published eco.zoon.workforce (${bindings.length} agents)`);
589
+ void publisher;
590
+ } catch (err) {
591
+ console.warn("[matrix] workforce roster publication failed:", err);
592
+ }
593
+ }
594
+ } else {
595
+ const http = findHttpTransport(config);
596
+ if (!http) throw new Error("no transport declared in zooid.yaml");
597
+ const token = process.env.ZOOID_TOKEN;
598
+ if (!token) throw new Error("ZOOID_TOKEN is required for http transport");
599
+ const app = createApp({ agents: registry, approvals, token });
600
+ server = serve({ fetch: app.fetch, port: http.transport.port, hostname: "0.0.0.0" });
601
+ port = await listenAsync(server);
602
+ }
603
+ const stop = async () => {
604
+ if (stopped) return whenStopped;
605
+ stopped = true;
606
+ try {
607
+ if (server) await closeAsync(server);
608
+ } catch {
609
+ }
610
+ try {
611
+ await registry.stopAll();
612
+ } catch (err) {
613
+ console.error("stopAll:", err);
614
+ }
615
+ try {
616
+ if (contextSocket) await contextSocket.close();
617
+ await unlink(daemonSockPath).catch(() => {
618
+ });
619
+ } catch {
620
+ }
621
+ resolveStopped();
622
+ };
623
+ if (opts.installSignalHandlers !== false) {
624
+ const handler = (sig) => {
625
+ console.log(`received ${sig}, stopping agents...`);
626
+ void stop().then(() => process.exit(0));
627
+ };
628
+ process.on("SIGINT", () => handler("SIGINT"));
629
+ process.on("SIGTERM", () => handler("SIGTERM"));
630
+ }
631
+ return { port, agentNames, stop, whenStopped };
632
+ }
633
+
634
+ // src/services/tuwunel.ts
635
+ import { spawn } from "child_process";
636
+ var DEFAULT_IMAGE = "ghcr.io/matrix-construct/tuwunel:latest";
637
+ function buildRunArgs(opts) {
638
+ const image = opts.image ?? DEFAULT_IMAGE;
639
+ return [
640
+ "run",
641
+ "--rm",
642
+ "--name",
643
+ opts.name,
644
+ "-p",
645
+ `${opts.hostPort}:8448`,
646
+ "-v",
647
+ `${opts.paths.dbDir}:/var/lib/tuwunel/db`,
648
+ "-v",
649
+ `${opts.paths.mediaDir}:/var/lib/tuwunel/media`,
650
+ "-v",
651
+ `${opts.paths.tuwunelTomlPath}:/etc/tuwunel/tuwunel.toml:ro`,
652
+ "-v",
653
+ `${opts.paths.registrationsDir}:/var/lib/tuwunel/registrations:ro`,
654
+ "-e",
655
+ "TUWUNEL_CONFIG=/etc/tuwunel/tuwunel.toml",
656
+ image
657
+ ];
658
+ }
659
+ var TuwunelService = class {
660
+ constructor(opts) {
661
+ this.opts = opts;
662
+ }
663
+ child = null;
664
+ start() {
665
+ this.child = spawn(this.opts.engine, buildRunArgs(this.opts), {
666
+ stdio: ["ignore", "pipe", "pipe"]
667
+ });
668
+ return this.child;
669
+ }
670
+ async stop() {
671
+ if (this.child && this.child.exitCode === null) {
672
+ this.child.kill("SIGTERM");
673
+ await new Promise((resolve6) => {
674
+ this.child.on("exit", () => resolve6());
675
+ });
676
+ }
677
+ this.child = null;
678
+ await execEngine(this.opts.engine, ["stop", this.opts.name]).catch(() => {
679
+ });
680
+ }
681
+ async waitHealthy(opts) {
682
+ const deadline = Date.now() + opts.timeoutMs;
683
+ while (Date.now() < deadline) {
684
+ try {
685
+ const r = await fetch(`${opts.url}/_matrix/client/versions`);
686
+ if (r.ok) return;
687
+ } catch {
688
+ }
689
+ await new Promise((resolve6) => setTimeout(resolve6, 500));
690
+ }
691
+ throw new Error(
692
+ `Tuwunel did not become healthy at ${opts.url} in ${opts.timeoutMs}ms`
693
+ );
694
+ }
695
+ };
696
+ function execEngine(engine, args) {
697
+ return new Promise((resolve6, reject) => {
698
+ const child = spawn(engine, args, { stdio: "pipe" });
699
+ let stderr = "";
700
+ child.stderr.on("data", (b) => stderr += String(b));
701
+ child.on("exit", (code) => {
702
+ if (code === 0) resolve6();
703
+ else reject(new Error(`${engine} ${args.join(" ")} failed: ${stderr.trim()}`));
704
+ });
705
+ child.on("error", reject);
706
+ });
707
+ }
708
+
709
+ // src/web/resolve.ts
710
+ import { existsSync as existsSync2 } from "fs";
711
+ import { dirname as dirname3, join as join4, resolve } from "path";
712
+ var ENV_OVERRIDE = "ZOOID_DEV_WEB_ROOT_OVERRIDE";
713
+ function resolveWebRoot(cliRoot) {
714
+ const override = process.env[ENV_OVERRIDE];
715
+ if (override && existsSync2(join4(override, "index.html"))) return resolve(override);
716
+ const published = join4(cliRoot, "dist", "web");
717
+ if (existsSync2(join4(published, "index.html"))) return published;
718
+ const fromSource = webSourcePackage(cliRoot);
719
+ if (fromSource && existsSync2(join4(fromSource, "dist", "index.html"))) {
720
+ return join4(fromSource, "dist");
721
+ }
722
+ throw new Error(
723
+ `@zoon/web build not found.
724
+ Tried: ${published}
725
+ Tried: ${fromSource ?? "(no monorepo source)"}
726
+ Run \`pnpm -C zoon/packages/web build\` (or set ${ENV_OVERRIDE} to a built dist).`
727
+ );
728
+ }
729
+ function webSourcePackage(cliRoot) {
730
+ const workspaceRoot = dirname3(dirname3(dirname3(cliRoot)));
731
+ const candidate = join4(workspaceRoot, "zoon", "packages", "web");
732
+ return existsSync2(join4(candidate, "package.json")) ? candidate : null;
733
+ }
734
+
735
+ // src/web/static.ts
736
+ import { readFileSync as readFileSync3, statSync } from "fs";
737
+ import { extname, join as join5, normalize } from "path";
738
+ import { Hono as Hono2 } from "hono";
739
+ var MIME = {
740
+ ".html": "text/html; charset=utf-8",
741
+ ".js": "application/javascript; charset=utf-8",
742
+ ".mjs": "application/javascript; charset=utf-8",
743
+ ".css": "text/css; charset=utf-8",
744
+ ".json": "application/json; charset=utf-8",
745
+ ".svg": "image/svg+xml",
746
+ ".png": "image/png",
747
+ ".jpg": "image/jpeg",
748
+ ".jpeg": "image/jpeg",
749
+ ".woff": "font/woff",
750
+ ".woff2": "font/woff2"
751
+ };
752
+ function isAssetPath(p) {
753
+ return p.startsWith("/assets/") || /\.[a-z0-9]+$/i.test(p);
754
+ }
755
+ function webStatic(opts) {
756
+ const app = new Hono2();
757
+ app.get("/config.json", (c) => c.json({ homeserver_url: opts.homeserverUrl }));
758
+ app.get("*", (c) => {
759
+ const url = new URL(c.req.url);
760
+ const requested = decodeURIComponent(url.pathname);
761
+ const wantFile = requested === "/" ? "/index.html" : requested;
762
+ const safe = normalize(wantFile).replace(/^(\.\.[/\\])+/g, "");
763
+ const filePath = join5(opts.webRoot, safe);
764
+ if (!filePath.startsWith(opts.webRoot)) return c.notFound();
765
+ try {
766
+ const stat = statSync(filePath);
767
+ if (stat.isFile()) {
768
+ const body = readFileSync3(filePath);
769
+ const ct = MIME[extname(filePath)] ?? "application/octet-stream";
770
+ return c.body(body, 200, { "content-type": ct });
771
+ }
772
+ } catch {
773
+ }
774
+ if (isAssetPath(requested)) return c.notFound();
775
+ const indexBytes = readFileSync3(join5(opts.webRoot, "index.html"));
776
+ return c.body(indexBytes, 200, {
777
+ "content-type": MIME[".html"]
778
+ });
779
+ });
780
+ return app;
781
+ }
782
+
783
+ // src/web/watch.ts
784
+ import { spawn as spawn2 } from "child_process";
785
+ import { existsSync as existsSync3, statSync as statSync2 } from "fs";
786
+ import { join as join6 } from "path";
787
+ async function startWebWatch(opts) {
788
+ const distPath = join6(opts.webPackageDir, "dist");
789
+ const indexPath = join6(distPath, "index.html");
790
+ const timeoutMs = opts.firstBuildTimeoutMs ?? 6e4;
791
+ const spawnTime = Date.now();
792
+ const child = spawn2(
793
+ "pnpm",
794
+ ["-C", opts.webPackageDir, "exec", "vite", "build", "--watch"],
795
+ { stdio: ["ignore", "pipe", "pipe"] }
796
+ );
797
+ let buffered = "";
798
+ const onData = (chunk) => {
799
+ buffered += chunk.toString("utf8");
800
+ };
801
+ child.stdout?.on("data", onData);
802
+ child.stderr?.on("data", onData);
803
+ let attached = false;
804
+ const attachStdio = () => {
805
+ if (attached) return;
806
+ attached = true;
807
+ child.stdout?.off("data", onData);
808
+ child.stderr?.off("data", onData);
809
+ if (buffered) process.stdout.write(buffered);
810
+ buffered = "";
811
+ child.stdout?.pipe(process.stdout);
812
+ child.stderr?.pipe(process.stderr);
813
+ };
814
+ let exited = false;
815
+ child.once("exit", () => {
816
+ exited = true;
817
+ });
818
+ while (Date.now() - spawnTime < timeoutMs) {
819
+ if (exited) {
820
+ attachStdio();
821
+ throw new Error(`vite build --watch exited before first build (code ${child.exitCode})`);
822
+ }
823
+ if (existsSync3(indexPath)) {
824
+ const m = statSync2(indexPath).mtimeMs;
825
+ if (m >= spawnTime - 500) return makeHandle();
826
+ }
827
+ await new Promise((r) => setTimeout(r, 200));
828
+ }
829
+ attachStdio();
830
+ await stopChild(child);
831
+ throw new Error(`vite build --watch did not produce ${indexPath} within ${timeoutMs}ms`);
832
+ function makeHandle() {
833
+ return {
834
+ distPath,
835
+ attachStdio,
836
+ stop: () => stopChild(child)
837
+ };
838
+ }
839
+ }
840
+ async function stopChild(child) {
841
+ if (child.exitCode !== null) return;
842
+ child.kill("SIGTERM");
843
+ await new Promise((resolve6) => {
844
+ let done = false;
845
+ const finish = () => {
846
+ if (done) return;
847
+ done = true;
848
+ resolve6();
849
+ };
850
+ child.once("exit", finish);
851
+ setTimeout(() => {
852
+ try {
853
+ child.kill("SIGKILL");
854
+ } catch {
855
+ }
856
+ finish();
857
+ }, 2e3);
858
+ });
859
+ }
860
+
861
+ // src/observability/paths.ts
862
+ import { mkdir as mkdir2, readdir, rm, symlink, unlink as unlink2 } from "fs/promises";
863
+ import { join as join7 } from "path";
864
+ function localDateSlug(d) {
865
+ const y = d.getFullYear();
866
+ const m = String(d.getMonth() + 1).padStart(2, "0");
867
+ const day = String(d.getDate()).padStart(2, "0");
868
+ return `${y}-${m}-${day}`;
869
+ }
870
+ var DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
871
+ function resolveLogPaths({ dataDir, now }) {
872
+ const slug = localDateSlug(now ?? /* @__PURE__ */ new Date());
873
+ const logsDir = join7(dataDir, "logs");
874
+ const dayDir = join7(logsDir, slug);
875
+ return {
876
+ dataDir,
877
+ logsDir,
878
+ dayDir,
879
+ daySlug: slug,
880
+ todayLink: join7(logsDir, "today"),
881
+ tuwunelLog: join7(dayDir, "tuwunel.log"),
882
+ daemonLog: join7(dayDir, "daemon.log"),
883
+ devLog: join7(dayDir, "dev.log"),
884
+ agentLog: (n) => join7(dayDir, `agent-${n}.log`),
885
+ agentTap: (n) => join7(dayDir, `agent-${n}.acp.jsonl`)
886
+ };
887
+ }
888
+ async function ensureDayFolder(p) {
889
+ await mkdir2(p.dayDir, { recursive: true });
890
+ try {
891
+ await unlink2(p.todayLink);
892
+ } catch {
893
+ }
894
+ await symlink(p.daySlug, p.todayLink);
895
+ }
896
+ async function pruneOldDays(opts) {
897
+ if (opts.retainDays <= 0) return [];
898
+ const logsDir = join7(opts.dataDir, "logs");
899
+ let entries;
900
+ try {
901
+ entries = await readdir(logsDir);
902
+ } catch {
903
+ return [];
904
+ }
905
+ const today = opts.now ?? /* @__PURE__ */ new Date();
906
+ const cutoff = new Date(
907
+ today.getFullYear(),
908
+ today.getMonth(),
909
+ today.getDate() - opts.retainDays + 1
910
+ ).getTime();
911
+ const removed = [];
912
+ for (const name of entries) {
913
+ if (!DAY_RE.test(name)) continue;
914
+ const [y, m, d] = name.split("-").map(Number);
915
+ const t = new Date(y, m - 1, d).getTime();
916
+ if (t < cutoff) {
917
+ await rm(join7(logsDir, name), { recursive: true, force: true });
918
+ removed.push(name);
919
+ }
920
+ }
921
+ return removed;
922
+ }
923
+
924
+ // src/observability/file-sink.ts
925
+ import { createWriteStream } from "fs";
926
+ import { mkdir as mkdir3 } from "fs/promises";
927
+ import { dirname as dirname4 } from "path";
928
+ var TRUNC_MARKER = "\u2026[truncated]";
929
+ var JsonlSink = class {
930
+ constructor(path, opts = {}) {
931
+ this.path = path;
932
+ this.maxStringLen = opts.maxStringLen ?? 4096;
933
+ this.readyPromise = (async () => {
934
+ await mkdir3(dirname4(this.path), { recursive: true });
935
+ this.stream = createWriteStream(this.path, { flags: "a" });
936
+ })();
937
+ }
938
+ stream = null;
939
+ maxStringLen;
940
+ readyPromise;
941
+ async write(obj) {
942
+ await this.readyPromise;
943
+ const capped = capStrings(obj, this.maxStringLen);
944
+ const line = JSON.stringify(capped) + "\n";
945
+ return new Promise((resolve6, reject) => {
946
+ this.stream.write(line, (err) => err ? reject(err) : resolve6());
947
+ });
948
+ }
949
+ async close() {
950
+ await this.readyPromise;
951
+ return new Promise((resolve6) => {
952
+ this.stream.end(() => resolve6());
953
+ });
954
+ }
955
+ };
956
+ function capStrings(v, max) {
957
+ if (typeof v === "string") {
958
+ return v.length <= max ? v : v.slice(0, max) + TRUNC_MARKER;
959
+ }
960
+ if (Array.isArray(v)) return v.map((x) => capStrings(x, max));
961
+ if (v && typeof v === "object") {
962
+ const out = {};
963
+ for (const [k, val] of Object.entries(v)) out[k] = capStrings(val, max);
964
+ return out;
965
+ }
966
+ return v;
967
+ }
968
+ var DEFAULT_VARIANTS = /* @__PURE__ */ new Set([
969
+ "tool_call",
970
+ "tool_call_update",
971
+ "plan",
972
+ "available_commands_update",
973
+ "current_mode_update",
974
+ "user_message_chunk"
975
+ ]);
976
+ function shouldCaptureUpdate(verbosity, variant) {
977
+ if (verbosity === "verbose") return true;
978
+ if (verbosity === "verbose-thoughts") {
979
+ return DEFAULT_VARIANTS.has(variant) || variant === "agent_thought_chunk";
980
+ }
981
+ return DEFAULT_VARIANTS.has(variant);
982
+ }
983
+
984
+ // src/observability/capture-agent.ts
985
+ function wireAgentCapture(opts) {
986
+ const sink = new JsonlSink(opts.paths.agentTap(opts.agent));
987
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
988
+ const pending = [];
989
+ const onTap = (event) => {
990
+ if (event.kind === "session_update") {
991
+ const variant = event.update.sessionUpdate ?? "unknown";
992
+ if (!shouldCaptureUpdate(opts.verbosity, variant)) return;
993
+ }
994
+ const matrix = opts.matrixContext();
995
+ const envelope = {
996
+ ts: now().toISOString(),
997
+ agent: opts.agent,
998
+ session_id: event.sessionId,
999
+ turn_id: event.turnId,
1000
+ kind: event.kind
1001
+ };
1002
+ if (matrix) envelope.matrix = matrix;
1003
+ if (event.kind === "session_update") envelope.notification = event.update;
1004
+ else if (event.kind === "turn_started") envelope.prompt_text = event.promptText;
1005
+ else if (event.kind === "turn_completed") envelope.stop_reason = event.stopReason;
1006
+ pending.push(sink.write(envelope));
1007
+ };
1008
+ return {
1009
+ onTap,
1010
+ close: async () => {
1011
+ await Promise.all(pending);
1012
+ await sink.close();
1013
+ }
1014
+ };
1015
+ }
1016
+
1017
+ // src/observability/capture-tuwunel.ts
1018
+ import { createWriteStream as createWriteStream2 } from "fs";
1019
+ import { mkdir as mkdir4 } from "fs/promises";
1020
+ import { dirname as dirname5 } from "path";
1021
+ function captureChildToFile(child, path) {
1022
+ return (async () => {
1023
+ await mkdir4(dirname5(path), { recursive: true });
1024
+ const stream = createWriteStream2(path, { flags: "a" });
1025
+ if (child.stdout) child.stdout.on("data", (b) => stream.write(b));
1026
+ if (child.stderr) child.stderr.on("data", (b) => stream.write(b));
1027
+ await new Promise((resolve6) => {
1028
+ child.on("exit", () => stream.end(() => resolve6()));
1029
+ });
1030
+ })();
1031
+ }
1032
+
1033
+ // src/commands/dev-cascade.ts
1034
+ function buildShutdown(layers) {
1035
+ let started = null;
1036
+ return () => {
1037
+ if (started) return started;
1038
+ started = (async () => {
1039
+ const order = [
1040
+ "stopUi",
1041
+ "stopWebWatch",
1042
+ "stopCaptures",
1043
+ "stopDaemon",
1044
+ "stopTuwunel",
1045
+ "finalizeTuwunelCapture"
1046
+ ];
1047
+ for (const step of order) {
1048
+ try {
1049
+ await layers[step]?.();
1050
+ } catch (err) {
1051
+ console.error(`${step}:`, err);
1052
+ }
1053
+ }
1054
+ })();
1055
+ return started;
1056
+ };
1057
+ }
1058
+
1059
+ // src/commands/dev.ts
1060
+ var CLI_ROOT = (() => {
1061
+ let dir = dirname6(fileURLToPath(import.meta.url));
1062
+ for (let i = 0; i < 8; i++) {
1063
+ const pkgPath = resolve2(dir, "package.json");
1064
+ if (existsSync4(pkgPath)) {
1065
+ try {
1066
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
1067
+ if (pkg.name === "zooid" || pkg.name === "@zooid/cli") return dir;
1068
+ } catch {
1069
+ }
1070
+ }
1071
+ const parent = dirname6(dir);
1072
+ if (parent === dir) break;
1073
+ dir = parent;
1074
+ }
1075
+ throw new Error("cli root not found (no @zooid/cli package.json above " + import.meta.url + ")");
1076
+ })();
1077
+ async function runDev(flags) {
1078
+ const cwd = flags.cwd ?? process.cwd();
1079
+ const found = findConfigFile(cwd);
1080
+ if (!found) throw new Error(`zooid.yaml not found in ${cwd}`);
1081
+ loadEnvFiles(cwd);
1082
+ const dataRoot = resolve2(cwd, flags.dataDir);
1083
+ const layout = resolveDataLayout(dataRoot);
1084
+ const paths = resolvePaths(layout.matrixDir);
1085
+ const logPaths = resolveLogPaths({ dataDir: layout.dataRoot });
1086
+ const tokens = ensureTokens(paths.envPath);
1087
+ process.env.MATRIX_AS_TOKEN = tokens.asToken;
1088
+ process.env.MATRIX_HS_TOKEN = tokens.hsToken;
1089
+ const rawYaml = readFileSync4(found.path, "utf8");
1090
+ const preview = loadZooidConfig(rawYaml, { configDir: dirname6(found.path) });
1091
+ const matrix = findMatrixTransport(preview);
1092
+ if (!matrix) {
1093
+ throw new Error("zooid.yaml: zooid dev requires at least one matrix transport");
1094
+ }
1095
+ const agentUserIds = Object.values(preview.agents).filter((a) => a.matrix?.transport === matrix.name).map((a) => a.matrix.user_id);
1096
+ const shape = deriveHomeserverShape(matrix.transport, agentUserIds);
1097
+ const port = flags.hostPort ?? shape.port;
1098
+ const homeserver = `http://localhost:${port}`;
1099
+ await ensureDayFolder(logPaths);
1100
+ await pruneOldDays({ dataDir: layout.dataRoot, retainDays: 14 });
1101
+ const ctx = { logPaths, tokens };
1102
+ const tasks = new Listr(
1103
+ [
1104
+ {
1105
+ title: "Write tuwunel.toml + appservice.yaml",
1106
+ task: () => {
1107
+ if (!ctx.tokens) throw new Error("tokens not ready");
1108
+ writeBootstrapConfigs({
1109
+ paths,
1110
+ serverName: shape.serverName,
1111
+ asToken: ctx.tokens.asToken,
1112
+ hsToken: ctx.tokens.hsToken,
1113
+ senderLocalpart: matrix.transport.sender_localpart,
1114
+ userNamespace: matrix.transport.user_namespace
1115
+ });
1116
+ }
1117
+ },
1118
+ {
1119
+ title: `Start Tuwunel container (${flags.engine})`,
1120
+ task: () => {
1121
+ ctx.svc = new TuwunelService({
1122
+ name: "zooid-tuwunel",
1123
+ hostPort: port,
1124
+ paths,
1125
+ engine: flags.engine
1126
+ });
1127
+ const child = ctx.svc.start();
1128
+ ctx.tuwunelCaptureDone = captureChildToFile(child, logPaths.tuwunelLog);
1129
+ }
1130
+ },
1131
+ {
1132
+ title: "Wait for Tuwunel /_matrix/client/versions",
1133
+ task: async () => {
1134
+ if (!ctx.svc) throw new Error("service not started");
1135
+ await ctx.svc.waitHealthy({ url: homeserver, timeoutMs: 6e4 });
1136
+ }
1137
+ },
1138
+ {
1139
+ title: `Register admin user @${flags.adminUser}:${shape.serverName}`,
1140
+ task: async (_, t) => {
1141
+ const r = await ensureAdminUser({
1142
+ homeserver,
1143
+ username: flags.adminUser,
1144
+ password: flags.adminPassword
1145
+ });
1146
+ t.title = r.created ? `Registered admin: ${r.userId}` : `Admin already exists: ${r.userId}`;
1147
+ }
1148
+ },
1149
+ {
1150
+ title: "Start daemon",
1151
+ task: async (_ctx, t) => {
1152
+ const captures = {};
1153
+ for (const name of Object.keys(preview.agents)) {
1154
+ captures[name] = wireAgentCapture({
1155
+ agent: name,
1156
+ paths: logPaths,
1157
+ verbosity: "default",
1158
+ // Matrix cross-link is a follow-up — wire to transport-matrix's
1159
+ // most-recent (room_id, event_id) per session in a later cycle.
1160
+ matrixContext: () => null
1161
+ });
1162
+ }
1163
+ ctx.captures = captures;
1164
+ ctx.daemon = await startDaemon({
1165
+ configPath: found.path,
1166
+ cwd,
1167
+ installSignalHandlers: false,
1168
+ adminUserId: `@${flags.adminUser}:${shape.serverName}`,
1169
+ agentsDir: layout.agentsDir,
1170
+ onTap: (agentName, event) => captures[agentName]?.onTap(event),
1171
+ prepullLog: (line) => {
1172
+ t.output = line.replace(/^\[zooid\]\s+/, "").trim();
1173
+ }
1174
+ });
1175
+ }
1176
+ },
1177
+ ...flags.watchWeb ? [
1178
+ {
1179
+ title: "Start @zoon/web watcher (vite build --watch)",
1180
+ task: async () => {
1181
+ const pkgDir = typeof flags.watchWeb === "string" ? resolve2(flags.watchWeb) : webSourcePackage(CLI_ROOT);
1182
+ if (!pkgDir) {
1183
+ const defaultPath = dirname6(dirname6(dirname6(CLI_ROOT))) + "/zoon/packages/web";
1184
+ throw new Error(
1185
+ `--watch-web: @zoon/web not found at ${defaultPath}.
1186
+ Pass an explicit path: --watch-web=/path/to/zoon/packages/web`
1187
+ );
1188
+ }
1189
+ ctx.webWatch = await startWebWatch({ webPackageDir: pkgDir });
1190
+ }
1191
+ }
1192
+ ] : [],
1193
+ {
1194
+ title: `Serve @zoon/web on http://localhost:${flags.uiPort}`,
1195
+ task: () => {
1196
+ const webRoot = ctx.webWatch?.distPath ?? resolveWebRoot(CLI_ROOT);
1197
+ const app = webStatic({ webRoot, homeserverUrl: homeserver });
1198
+ ctx.uiServer = serve2({ fetch: app.fetch, port: flags.uiPort });
1199
+ }
1200
+ }
1201
+ ],
1202
+ { concurrent: false, exitOnError: true }
1203
+ );
1204
+ await tasks.run();
1205
+ ctx.webWatch?.attachStdio();
1206
+ const shutdown = buildShutdown({
1207
+ stopUi: async () => {
1208
+ const s = ctx.uiServer;
1209
+ if (!s) return;
1210
+ await new Promise((r) => s.close(() => r()));
1211
+ },
1212
+ stopDaemon: async () => {
1213
+ await ctx.daemon?.stop();
1214
+ },
1215
+ stopTuwunel: async () => {
1216
+ await ctx.svc?.stop();
1217
+ },
1218
+ stopWebWatch: async () => {
1219
+ await ctx.webWatch?.stop();
1220
+ },
1221
+ stopCaptures: async () => {
1222
+ if (!ctx.captures) return;
1223
+ await Promise.all(Object.values(ctx.captures).map((c) => c.close()));
1224
+ },
1225
+ finalizeTuwunelCapture: async () => {
1226
+ if (ctx.tuwunelCaptureDone) await ctx.tuwunelCaptureDone;
1227
+ }
1228
+ });
1229
+ if (flags.installSignalHandlers !== false) {
1230
+ const handler = async () => {
1231
+ process.stdout.write(chalk.dim("\nStopping\u2026\n"));
1232
+ await shutdown();
1233
+ process.exit(0);
1234
+ };
1235
+ process.on("SIGINT", () => void handler());
1236
+ process.on("SIGTERM", () => void handler());
1237
+ }
1238
+ process.stdout.write(
1239
+ [
1240
+ "",
1241
+ chalk.bold("Tuwunel is up.") + ` ${homeserver}`,
1242
+ chalk.bold("UI: ") + ` http://localhost:${flags.uiPort}`,
1243
+ ` ${chalk.cyan("admin user:")} ${flags.adminUser} / ${flags.adminPassword}`,
1244
+ ` ${chalk.cyan("data dir:")} ${layout.dataRoot}`,
1245
+ ...ctx.webWatch ? [` ${chalk.cyan("web watcher:")} live (vite build --watch on @zoon/web)`] : [],
1246
+ "",
1247
+ chalk.dim("Press Ctrl-C to stop."),
1248
+ ""
1249
+ ].join("\n")
1250
+ );
1251
+ if (flags.foreground !== false) {
1252
+ await ctx.daemon.whenStopped;
1253
+ }
1254
+ return { stop: shutdown };
1255
+ }
1256
+ function loadEnvFiles(cwd) {
1257
+ for (const name of [".env.local", ".env"]) {
1258
+ const path = resolve2(cwd, name);
1259
+ if (!existsSync4(path)) continue;
1260
+ try {
1261
+ process.loadEnvFile(path);
1262
+ } catch (err) {
1263
+ console.warn(`[zooid dev] failed to load ${name}: ${err.message}`);
1264
+ }
1265
+ }
1266
+ }
1267
+
1268
+ // src/commands/init.ts
1269
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync, writeFileSync as writeFileSync3 } from "fs";
1270
+ import { dirname as dirname7, join as join9, resolve as resolve3 } from "path";
1271
+
1272
+ // src/commands/init/generators.ts
1273
+ function generateZooidYaml(opts) {
1274
+ const acpBlock = opts.preset === "opencode" ? ` acp: { preset: opencode }` : ` acp:
1275
+ preset: ${opts.preset}
1276
+ model: ${opts.model}`;
1277
+ return `runtime: local
1278
+
1279
+ transports:
1280
+ matrix:
1281
+ homeserver: http://localhost:8448
1282
+ port: 9099
1283
+
1284
+ agents:
1285
+ zooid-assistant:
1286
+ ${acpBlock}
1287
+ matrix:
1288
+ display_name: 'Zooid Assistant'
1289
+ rooms: ['#zooid']
1290
+ `;
1291
+ }
1292
+ function generateAgentsMd() {
1293
+ return `# zooid-assistant
1294
+
1295
+ You are the zooid setup assistant. Your job is to help the operator get a workforce running, add agents, and configure zooid for their use case.
1296
+
1297
+ ## Resources
1298
+
1299
+ - Zooid documentation: https://zooid.dev/docs/
1300
+ - Use your web fetch tool to read documentation pages when answering questions.
1301
+
1302
+ ## Stack facts
1303
+
1304
+ - **Tuwunel** is a lightweight Rust Matrix homeserver (Conduit fork) backed by RocksDB. Single binary, low resource floor \u2014 not "heavy" for typical workforces.
1305
+ - **Logs land on disk** in \`./data/\` inside this zooid home dir: tuwunel logs, the daemon, and each agent's ACP stream. Use \`zooid logs <source>\` to tail them (sources: \`tuwunel\`, \`daemon\`, \`dev\`, \`agent-<name>\`, \`agent-<name>.acp\`). Debugging is mostly "read the log file" \u2014 don't tell the operator it's opaque.
1306
+
1307
+ ## Behavior
1308
+
1309
+ - Be terse. Give the answer; link to the doc page; stop.
1310
+ - When the operator asks how to do something, prefer to fetch the relevant docs section over guessing from training data.
1311
+ - When the operator asks you to *configure* something, propose a yaml edit and let them apply it. Don't write files directly unless they ask.
1312
+ `;
1313
+ }
1314
+ function generateClaudeMd() {
1315
+ return `@AGENTS.md
1316
+ `;
1317
+ }
1318
+ function generateClaudeSettings() {
1319
+ return JSON.stringify(
1320
+ {
1321
+ permissions: {
1322
+ allow: ["WebFetch(domain:zooid.dev)"]
1323
+ }
1324
+ },
1325
+ null,
1326
+ 2
1327
+ ) + "\n";
1328
+ }
1329
+ function generateOpencodeJson(opts) {
1330
+ if (opts.provider === "custom") {
1331
+ return JSON.stringify(
1332
+ {
1333
+ $schema: "https://opencode.ai/config.json",
1334
+ model: "TODO/your-model",
1335
+ provider: { TODO: { options: {} } }
1336
+ },
1337
+ null,
1338
+ 2
1339
+ ) + "\n";
1340
+ }
1341
+ const cfg = {
1342
+ $schema: "https://opencode.ai/config.json",
1343
+ model: `${opts.provider}/${opts.model}`,
1344
+ provider: {
1345
+ [opts.provider]: {
1346
+ options: { apiKey: `{env:${opts.apiKeyEnvVar}}` }
1347
+ }
1348
+ },
1349
+ permission: { webfetch: "allow" }
1350
+ };
1351
+ return JSON.stringify(cfg, null, 2) + "\n";
1352
+ }
1353
+ function generateEnv(opts) {
1354
+ return `${opts.envVar}=${opts.value}
1355
+ `;
1356
+ }
1357
+ function generateGitignore() {
1358
+ return [".env", "node_modules/", "data/", ""].join("\n");
1359
+ }
1360
+ function generateOpencodeReadme() {
1361
+ return `See https://opencode.ai/config for the full opencode.json schema, including
1362
+ provider-specific options and per-tool permissions.
1363
+ `;
1364
+ }
1365
+
1366
+ // src/commands/init/registry.ts
1367
+ var SIMPLE_PRESETS = [
1368
+ {
1369
+ preset: "claude",
1370
+ models: ["claude-sonnet-4-6", "claude-opus-4-7", "claude-haiku-4-5"],
1371
+ subscriptionLabel: "Claude subscription (Pro / Max / Team / Enterprise)",
1372
+ apiKeyEnvVar: "ANTHROPIC_API_KEY",
1373
+ apiKeyPromptLabel: "Anthropic",
1374
+ credentialDir: ".claude"
1375
+ },
1376
+ {
1377
+ preset: "codex",
1378
+ models: ["gpt-5.5"],
1379
+ subscriptionLabel: "ChatGPT subscription (Plus / Pro / Team)",
1380
+ apiKeyEnvVar: "OPENAI_API_KEY",
1381
+ apiKeyPromptLabel: "OpenAI",
1382
+ credentialDir: ".codex"
1383
+ }
1384
+ ];
1385
+ var OPENCODE_PROVIDERS = [
1386
+ {
1387
+ id: "opencode-go",
1388
+ label: "opencode-go",
1389
+ description: "$10/mo subscription \u2014 Kimi, GLM, MiniMax",
1390
+ models: ["kimi-k2.6", "kimi-k2.5", "glm-5.1", "glm-5", "minimax-m2.7"],
1391
+ apiKeyEnvVar: "OPENCODE_API_KEY"
1392
+ },
1393
+ {
1394
+ id: "opencode",
1395
+ label: "opencode (Zen)",
1396
+ description: "free tier \u2014 curated models",
1397
+ models: ["kimi-k2.5-free", "glm-4.7-free", "minimax-m2.1-free"],
1398
+ apiKeyEnvVar: "OPENCODE_API_KEY"
1399
+ },
1400
+ {
1401
+ id: "anthropic",
1402
+ label: "Anthropic",
1403
+ description: "Claude via direct Anthropic API",
1404
+ models: ["claude-sonnet-4-6", "claude-opus-4-7", "claude-haiku-4-5"],
1405
+ apiKeyEnvVar: "ANTHROPIC_API_KEY"
1406
+ },
1407
+ {
1408
+ id: "openrouter",
1409
+ label: "OpenRouter",
1410
+ description: "many providers via OpenRouter",
1411
+ models: ["anthropic/claude-sonnet-4-6", "zhipuai/glm-5", "moonshot/kimi-k2.6"],
1412
+ apiKeyEnvVar: "OPENROUTER_API_KEY"
1413
+ }
1414
+ ];
1415
+ function findSimplePreset(name) {
1416
+ return SIMPLE_PRESETS.find((p) => p.preset === name);
1417
+ }
1418
+ function findOpencodeProvider(id) {
1419
+ return OPENCODE_PROVIDERS.find((p) => p.id === id);
1420
+ }
1421
+
1422
+ // src/commands/init/sniff.ts
1423
+ import { existsSync as existsSync5 } from "fs";
1424
+ import { homedir } from "os";
1425
+ import { join as join8 } from "path";
1426
+ function sniffCredentials(preset, home = homedir()) {
1427
+ const meta = findSimplePreset(preset);
1428
+ if (!meta) return { found: false };
1429
+ const full = join8(home, meta.credentialDir);
1430
+ if (existsSync5(full)) return { found: true, path: full };
1431
+ return { found: false };
1432
+ }
1433
+
1434
+ // src/commands/init.ts
1435
+ var IGNORED_PREEXISTING = /* @__PURE__ */ new Set([
1436
+ ".",
1437
+ "..",
1438
+ ".git",
1439
+ ".DS_Store",
1440
+ "node_modules",
1441
+ "package.json",
1442
+ "package-lock.json",
1443
+ "pnpm-lock.yaml",
1444
+ "yarn.lock"
1445
+ ]);
1446
+ async function runInit(opts) {
1447
+ const dir = resolve3(opts.dir);
1448
+ mkdirSync3(dir, { recursive: true });
1449
+ if (!opts.force) {
1450
+ const blocking = readdirSync(dir).filter((n) => !IGNORED_PREEXISTING.has(n));
1451
+ if (blocking.length > 0) {
1452
+ throw new Error(
1453
+ `${dir} is non-empty (use --force to allow scaffolding into it). Conflicting entries: ${blocking.join(", ")}`
1454
+ );
1455
+ }
1456
+ }
1457
+ const writes = [];
1458
+ if (opts.preset === "claude" || opts.preset === "codex") {
1459
+ if (!opts.model) throw new Error(`--model is required for preset ${opts.preset}`);
1460
+ if (!opts.auth) throw new Error("--auth (subscription|api-key) is required for claude/codex");
1461
+ const meta = findSimplePreset(opts.preset);
1462
+ writes.push({
1463
+ path: "zooid.yaml",
1464
+ content: generateZooidYaml({ preset: opts.preset, model: opts.model })
1465
+ });
1466
+ writes.push({ path: "agents/zooid-assistant/AGENTS.md", content: generateAgentsMd() });
1467
+ if (opts.preset === "claude") {
1468
+ writes.push({
1469
+ path: "agents/zooid-assistant/CLAUDE.md",
1470
+ content: generateClaudeMd()
1471
+ });
1472
+ writes.push({
1473
+ path: "agents/zooid-assistant/.claude/settings.json",
1474
+ content: generateClaudeSettings()
1475
+ });
1476
+ }
1477
+ if (opts.auth === "api-key") {
1478
+ if (!opts.apiKey) throw new Error("--api-key is required when --auth=api-key");
1479
+ writes.push({
1480
+ path: ".env",
1481
+ content: generateEnv({ envVar: meta.apiKeyEnvVar, value: opts.apiKey })
1482
+ });
1483
+ }
1484
+ } else if (opts.preset === "opencode") {
1485
+ if (!opts.provider) throw new Error("--provider is required for opencode");
1486
+ const isCustom = opts.provider === "custom";
1487
+ const providerMeta = isCustom ? void 0 : findOpencodeProvider(opts.provider);
1488
+ if (!isCustom && !providerMeta) throw new Error(`unknown opencode provider: ${opts.provider}`);
1489
+ if (!isCustom && !opts.model) throw new Error("--model is required (paired with --provider)");
1490
+ if (!isCustom && !opts.apiKey) throw new Error("--api-key is required for opencode");
1491
+ writes.push({ path: "zooid.yaml", content: generateZooidYaml({ preset: "opencode" }) });
1492
+ writes.push({ path: "agents/zooid-assistant/AGENTS.md", content: generateAgentsMd() });
1493
+ writes.push({
1494
+ path: "agents/zooid-assistant/opencode.json",
1495
+ content: generateOpencodeJson({
1496
+ provider: opts.provider,
1497
+ model: opts.model,
1498
+ apiKeyEnvVar: providerMeta?.apiKeyEnvVar
1499
+ })
1500
+ });
1501
+ if (isCustom) {
1502
+ writes.push({
1503
+ path: "agents/zooid-assistant/opencode.json.README",
1504
+ content: generateOpencodeReadme()
1505
+ });
1506
+ } else {
1507
+ writes.push({
1508
+ path: ".env",
1509
+ content: generateEnv({ envVar: providerMeta.apiKeyEnvVar, value: opts.apiKey })
1510
+ });
1511
+ }
1512
+ } else {
1513
+ throw new Error(`unknown preset: ${String(opts.preset)}`);
1514
+ }
1515
+ writes.push({ path: ".gitignore", content: generateGitignore() });
1516
+ for (const w of writes) {
1517
+ const full = join9(dir, w.path);
1518
+ const exists = existsSync6(full);
1519
+ if (exists && !opts.overwrite) {
1520
+ console.warn(`\u26A0 ${w.path} exists; left as-is (use --force --overwrite to replace)`);
1521
+ continue;
1522
+ }
1523
+ mkdirSync3(dirname7(full), { recursive: true });
1524
+ writeFileSync3(full, w.content);
1525
+ console.log(`\u2713 Created ${w.path}`);
1526
+ }
1527
+ if ((opts.preset === "claude" || opts.preset === "codex") && opts.auth === "subscription") {
1528
+ const s = sniffCredentials(opts.preset);
1529
+ if (s.found) {
1530
+ console.log(`\u2713 Found ${opts.preset} config at ${s.path}`);
1531
+ } else {
1532
+ console.warn(
1533
+ `\u26A0 No ${opts.preset} config detected \u2014 run \`${opts.preset}\` to log in before \`zooid dev\``
1534
+ );
1535
+ }
1536
+ }
1537
+ console.log("\nNext: zooid dev");
1538
+ }
1539
+
1540
+ // src/commands/init/prompts.ts
1541
+ import { select, password } from "@inquirer/prompts";
1542
+ async function resolveOptions(flags) {
1543
+ const interactive = flags.interactive !== false && Boolean(process.stdin.isTTY);
1544
+ const ask = (prompt, fallback) => {
1545
+ if (!interactive) return Promise.reject(new Error(fallback()));
1546
+ return prompt();
1547
+ };
1548
+ const preset = flags.preset ?? await ask(
1549
+ () => select({
1550
+ message: "Which agent should zooid-assistant use?",
1551
+ choices: [
1552
+ { name: "claude (Claude Code)", value: "claude" },
1553
+ { name: "codex (OpenAI Codex)", value: "codex" },
1554
+ { name: "opencode", value: "opencode" }
1555
+ ]
1556
+ }),
1557
+ () => "--preset is required (claude | codex | opencode)"
1558
+ );
1559
+ if (preset === "opencode") {
1560
+ const provider = flags.provider ?? await ask(
1561
+ () => select({
1562
+ message: "Which provider?",
1563
+ choices: [
1564
+ ...OPENCODE_PROVIDERS.map((p) => ({
1565
+ name: `${p.label} (${p.description})`,
1566
+ value: p.id
1567
+ })),
1568
+ { name: "custom (write your own opencode.json block)", value: "custom" }
1569
+ ]
1570
+ }),
1571
+ () => "--provider is required for opencode"
1572
+ );
1573
+ if (provider === "custom") {
1574
+ return {
1575
+ dir: flags.dir,
1576
+ preset,
1577
+ provider,
1578
+ force: flags.force,
1579
+ overwrite: flags.overwrite
1580
+ };
1581
+ }
1582
+ const meta2 = findOpencodeProvider(provider);
1583
+ if (!meta2) throw new Error(`unknown opencode provider: ${provider}`);
1584
+ const model2 = flags.model ?? await ask(
1585
+ () => select({
1586
+ message: "Which model?",
1587
+ choices: meta2.models.map((m) => ({ name: m, value: m }))
1588
+ }),
1589
+ () => "--model is required"
1590
+ );
1591
+ const apiKey2 = flags.apiKey ?? await ask(
1592
+ () => password({ message: `${meta2.label} API key:` }),
1593
+ () => "--api-key is required"
1594
+ );
1595
+ return {
1596
+ dir: flags.dir,
1597
+ preset,
1598
+ provider,
1599
+ model: model2,
1600
+ apiKey: apiKey2,
1601
+ force: flags.force,
1602
+ overwrite: flags.overwrite
1603
+ };
1604
+ }
1605
+ const meta = findSimplePreset(preset);
1606
+ if (!meta) throw new Error(`unknown preset: ${preset}`);
1607
+ const auth = flags.auth ?? await ask(
1608
+ () => select({
1609
+ message: "How do you authenticate?",
1610
+ choices: [
1611
+ { name: `My ${meta.subscriptionLabel}`, value: "subscription" },
1612
+ { name: `I'll provide an API key`, value: "api-key" }
1613
+ ]
1614
+ }),
1615
+ () => "--auth is required (subscription | api-key)"
1616
+ );
1617
+ const model = flags.model ?? await ask(
1618
+ () => select({
1619
+ message: "Which model?",
1620
+ choices: meta.models.map((m) => ({ name: m, value: m }))
1621
+ }),
1622
+ () => "--model is required"
1623
+ );
1624
+ const apiKey = auth === "api-key" ? flags.apiKey ?? await ask(
1625
+ () => password({ message: `${meta.apiKeyPromptLabel} API key:` }),
1626
+ () => "--api-key is required when --auth=api-key"
1627
+ ) : void 0;
1628
+ return {
1629
+ dir: flags.dir,
1630
+ preset,
1631
+ auth,
1632
+ model,
1633
+ apiKey,
1634
+ force: flags.force,
1635
+ overwrite: flags.overwrite
1636
+ };
1637
+ }
1638
+
1639
+ // src/commands/logs.ts
1640
+ import { readFile, readdir as readdir2, readlink } from "fs/promises";
1641
+ import { existsSync as existsSync7 } from "fs";
1642
+ import { join as join10 } from "path";
1643
+ var KNOWN_SOURCES = ["tuwunel", "daemon", "dev"];
1644
+ async function runLogs(flags) {
1645
+ const writer = flags.writer ?? ((s) => process.stdout.write(s));
1646
+ if (flags.subcommand === "prune") {
1647
+ const removed = await pruneOldDays({
1648
+ dataDir: flags.dataDir,
1649
+ retainDays: flags.keep ?? 14,
1650
+ now: flags.now
1651
+ });
1652
+ writer(`pruned ${removed.length} day(s): ${removed.join(", ")}
1653
+ `);
1654
+ return;
1655
+ }
1656
+ const day = flags.day ?? await resolveTodaySlug(flags.dataDir);
1657
+ if (!day) {
1658
+ writer("no logs yet\n");
1659
+ return;
1660
+ }
1661
+ const dayDir = join10(flags.dataDir, "logs", day);
1662
+ if (!existsSync7(dayDir)) {
1663
+ writer(`no logs for ${day}
1664
+ `);
1665
+ return;
1666
+ }
1667
+ if (flags.turn) {
1668
+ await dumpByTurn(dayDir, flags.turn, writer);
1669
+ return;
1670
+ }
1671
+ if (!flags.source) {
1672
+ const entries = await readdir2(dayDir);
1673
+ const sources = /* @__PURE__ */ new Set();
1674
+ for (const e of entries) {
1675
+ if (e.endsWith(".log")) sources.add(e.replace(/\.log$/, ""));
1676
+ if (e.endsWith(".acp.jsonl")) sources.add(e.replace(/\.acp\.jsonl$/, ".acp"));
1677
+ }
1678
+ writer([...sources].sort().join("\n") + "\n");
1679
+ return;
1680
+ }
1681
+ const path = resolveSourcePath(dayDir, flags.source);
1682
+ if (!existsSync7(path)) {
1683
+ writer(`no such source: ${flags.source}
1684
+ `);
1685
+ return;
1686
+ }
1687
+ writer(await readFile(path, "utf8"));
1688
+ }
1689
+ async function resolveTodaySlug(dataDir) {
1690
+ const link = join10(dataDir, "logs", "today");
1691
+ try {
1692
+ return await readlink(link);
1693
+ } catch {
1694
+ return null;
1695
+ }
1696
+ }
1697
+ function resolveSourcePath(dayDir, source) {
1698
+ if (source.startsWith("agent-")) {
1699
+ if (source.endsWith(".acp")) return join10(dayDir, `${source.slice(0, -4)}.acp.jsonl`);
1700
+ return join10(dayDir, `${source}.log`);
1701
+ }
1702
+ if (KNOWN_SOURCES.includes(source))
1703
+ return join10(dayDir, `${source}.log`);
1704
+ return join10(dayDir, source);
1705
+ }
1706
+ async function dumpByTurn(dayDir, turnId, writer) {
1707
+ const entries = await readdir2(dayDir);
1708
+ const taps = entries.filter((e) => e.endsWith(".acp.jsonl")).sort();
1709
+ for (const f of taps) {
1710
+ const text = await readFile(join10(dayDir, f), "utf8");
1711
+ for (const line of text.split("\n")) {
1712
+ if (!line) continue;
1713
+ try {
1714
+ const obj = JSON.parse(line);
1715
+ if (obj.turn_id === turnId) writer(line + "\n");
1716
+ } catch {
1717
+ }
1718
+ }
1719
+ }
1720
+ }
1721
+
1722
+ // src/commands/start.ts
1723
+ import { randomBytes as randomBytes2 } from "crypto";
1724
+ import { resolve as resolve4 } from "path";
1725
+ async function runStart(flags) {
1726
+ if (flags.printToken) {
1727
+ process.stdout.write(`${randomBytes2(32).toString("hex")}
1728
+ `);
1729
+ return;
1730
+ }
1731
+ const dataRoot = resolve4(process.cwd(), flags.dataDir ?? "./data");
1732
+ const layout = resolveDataLayout(dataRoot);
1733
+ const handle = await startDaemon({
1734
+ cliFlags: flags,
1735
+ installSignalHandlers: true,
1736
+ agentsDir: layout.agentsDir
1737
+ });
1738
+ console.log(`zooid listening on http://localhost:${handle.port}`);
1739
+ for (const name of handle.agentNames) console.log(` agent: ${name}`);
1740
+ await handle.whenStopped;
1741
+ }
1742
+
1743
+ // src/commands/status.ts
1744
+ import { readFileSync as readFileSync5 } from "fs";
1745
+ import { dirname as dirname8 } from "path";
1746
+ import chalk2 from "chalk";
1747
+ async function probe(url, timeoutMs = 2e3) {
1748
+ try {
1749
+ const r = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
1750
+ return r.status > 0;
1751
+ } catch {
1752
+ return false;
1753
+ }
1754
+ }
1755
+ async function collectStatus(opts) {
1756
+ const tuwunelUp = await probe(`${opts.tuwunelUrl}/_matrix/client/versions`);
1757
+ const tuwunel = {
1758
+ status: tuwunelUp ? "up" : "down",
1759
+ url: opts.tuwunelUrl
1760
+ };
1761
+ const found = findConfigFile(opts.cwd);
1762
+ if (!found) {
1763
+ return {
1764
+ tuwunel,
1765
+ daemon: { status: "unknown", reason: "no zooid.yaml" },
1766
+ agents: []
1767
+ };
1768
+ }
1769
+ const cfg = loadZooidConfig(readFileSync5(found.path, "utf8"), {
1770
+ configDir: dirname8(found.path)
1771
+ });
1772
+ const matrixEntry = Object.entries(cfg.transports).find(
1773
+ ([, t]) => t.type === "matrix"
1774
+ );
1775
+ const daemonPort = matrixEntry?.[1]?.port ?? 8080;
1776
+ const daemonUrl = `http://localhost:${daemonPort}`;
1777
+ const daemonUp = await probe(daemonUrl);
1778
+ const agents = [];
1779
+ for (const a of Object.values(cfg.agents)) {
1780
+ if (a.matrix) {
1781
+ agents.push({
1782
+ name: a.name,
1783
+ userId: a.matrix.user_id,
1784
+ trigger: a.matrix.trigger
1785
+ });
1786
+ }
1787
+ }
1788
+ return {
1789
+ tuwunel,
1790
+ daemon: { status: daemonUp ? "up" : "down", url: daemonUrl },
1791
+ agents
1792
+ };
1793
+ }
1794
+ async function runStatus(flags) {
1795
+ const cwd = flags.cwd ?? process.cwd();
1796
+ let port = flags.port;
1797
+ if (port === void 0) {
1798
+ const found = findConfigFile(cwd);
1799
+ if (found) {
1800
+ const cfg = loadZooidConfig(readFileSync5(found.path, "utf8"), {
1801
+ configDir: dirname8(found.path)
1802
+ });
1803
+ const matrix = findMatrixTransport(cfg);
1804
+ if (matrix) {
1805
+ const userIds = Object.values(cfg.agents).filter((a) => a.matrix?.transport === matrix.name).map((a) => a.matrix.user_id);
1806
+ port = deriveHomeserverShape(matrix.transport, userIds).port;
1807
+ }
1808
+ }
1809
+ }
1810
+ const tuwunelUrl = `http://localhost:${port ?? 8448}`;
1811
+ const s = await collectStatus({ cwd, tuwunelUrl });
1812
+ const fmt = (st) => st === "up" ? chalk2.green("up") : st === "down" ? chalk2.red("down") : chalk2.yellow("unknown");
1813
+ process.stdout.write(
1814
+ [
1815
+ `tuwunel ${fmt(s.tuwunel.status)} ${s.tuwunel.url}`,
1816
+ `daemon ${fmt(s.daemon.status)} ${"url" in s.daemon ? s.daemon.url : s.daemon.reason}`,
1817
+ ...s.agents.map(
1818
+ (a) => ` agent: ${a.name} (${a.userId}, trigger: ${a.trigger})`
1819
+ ),
1820
+ ""
1821
+ ].join("\n")
1822
+ );
1823
+ }
1824
+
1825
+ // src/bin.ts
1826
+ var cli = cac("zooid");
1827
+ cli.command("start", "Run the daemon (production entry-point)").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--runtime <local|docker|podman>", "Agent runtime").option("--image <ref>", "Agent container image").option("--print-token", "Print a 32-byte hex token and exit").action(async (flags) => {
1828
+ await runStart({
1829
+ dataDir: flags.data,
1830
+ runtime: flags.runtime,
1831
+ image: flags.image,
1832
+ printToken: flags.printToken
1833
+ });
1834
+ });
1835
+ cli.command("dev", "Tuwunel + daemon + UI for local development").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--engine <docker|podman>", "Container engine", { default: "docker" }).option("--ui-port <n>", "UI HTTP port", { default: 5173 }).option("--admin-user <name>", "Admin username", { default: "admin" }).option("--admin-password <pw>", "Admin password", { default: "admin" }).option(
1836
+ "--watch-web [path]",
1837
+ "Run vite build --watch on @zoon/web. Path defaults to sibling ../zoon/packages/web."
1838
+ ).action(async (flags) => {
1839
+ await runDev({
1840
+ dataDir: flags.data,
1841
+ engine: flags.engine,
1842
+ uiPort: Number(flags.uiPort),
1843
+ adminUser: flags.adminUser,
1844
+ adminPassword: flags.adminPassword,
1845
+ watchWeb: flags.watchWeb
1846
+ });
1847
+ });
1848
+ cli.command(
1849
+ "logs [source]",
1850
+ 'Read captured logs. source=tuwunel|daemon|dev|agent-<name>[.acp], or "prune" to delete old days'
1851
+ ).option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--day <YYYY-MM-DD>", "Day partition (defaults to today)").option("--turn <id>", "Filter ACP taps to a single turn id").option("-f, --follow", "Tail the file (not yet implemented)").option("--keep <n>", "For `logs prune`: days to retain", { default: 14 }).action(async (source, flags) => {
1852
+ if (source === "prune") {
1853
+ await runLogs({
1854
+ dataDir: resolve5(process.cwd(), flags.data),
1855
+ subcommand: "prune",
1856
+ keep: Number(flags.keep)
1857
+ });
1858
+ return;
1859
+ }
1860
+ await runLogs({
1861
+ dataDir: resolve5(process.cwd(), flags.data),
1862
+ source,
1863
+ day: flags.day,
1864
+ turn: flags.turn,
1865
+ follow: Boolean(flags.follow)
1866
+ });
1867
+ });
1868
+ cli.command("status", "Print Tuwunel + daemon health").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--port <n>", "Tuwunel host port (defaults to zooid.yaml)").action(async (flags) => {
1869
+ await runStatus({
1870
+ dataDir: flags.data,
1871
+ port: flags.port !== void 0 ? Number(flags.port) : void 0
1872
+ });
1873
+ });
1874
+ cli.command("init [dir]", "Scaffold a new zooid workforce in the current (or named) directory").option("--preset <name>", "claude | codex | opencode").option("--auth <mode>", "subscription | api-key (claude/codex only)").option("--model <id>", "Model identifier").option("--provider <id>", "opencode provider: opencode-go | opencode | anthropic | openrouter | custom").option("--api-key <value>", "API key (api-key path; opencode always)").option("--force", "Allow scaffolding into a non-empty directory").option("--overwrite", "With --force, overwrite existing files").option("--no-interactive", "Disable prompts; require all flags up front").action(async (dir, flags) => {
1875
+ const resolved = await resolveOptions({
1876
+ dir: dir ?? process.cwd(),
1877
+ preset: flags.preset,
1878
+ auth: flags.auth,
1879
+ model: flags.model,
1880
+ provider: flags.provider,
1881
+ apiKey: flags.apiKey,
1882
+ force: Boolean(flags.force),
1883
+ overwrite: Boolean(flags.overwrite),
1884
+ interactive: flags.interactive
1885
+ });
1886
+ await runInit(resolved);
1887
+ });
1888
+ cli.help();
1889
+ cli.version("0.0.1");
1890
+ cli.parse();
1891
+ //# sourceMappingURL=bin.js.map