zooid 0.7.0 → 0.7.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/dist/bin.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  SpawnRegistry,
6
6
  buildAcpRegistry,
7
7
  createMatrixTransport,
8
+ ensureDefaultChannel,
8
9
  ensureWorkforceSpace,
9
10
  findConfigFile,
10
11
  findHttpTransport,
@@ -14,7 +15,7 @@ import {
14
15
  renderRegistration,
15
16
  startDaemonSocketServer,
16
17
  startWorkforcePublisher
17
- } from "./chunk-N5POSZX5.js";
18
+ } from "./chunk-O6E4CDTV.js";
18
19
 
19
20
  // src/bin.ts
20
21
  import { resolve as resolve5 } from "path";
@@ -401,6 +402,68 @@ function createApp({
401
402
  return app;
402
403
  }
403
404
 
405
+ // src/prepull-images.ts
406
+ import { execFile } from "child_process";
407
+ import { promisify } from "util";
408
+ var pExecFile = promisify(execFile);
409
+ async function prepullImages(registry, opts) {
410
+ if (opts.skip || opts.runtime === "local") return;
411
+ const exec = opts.exec ?? defaultExec;
412
+ const log = opts.log ?? ((l) => console.log(l));
413
+ const images = /* @__PURE__ */ new Set();
414
+ for (const name of registry.agentNames()) {
415
+ const img = registry.resolveSpawnImage(name);
416
+ if (img) images.add(img);
417
+ }
418
+ const toPull = [];
419
+ let cached = 0;
420
+ if (opts.refresh) {
421
+ toPull.push(...images);
422
+ } else {
423
+ await Promise.all(
424
+ [...images].map(async (img) => {
425
+ const r = await exec(opts.engine, ["image", "inspect", img]);
426
+ if (r.code === 0) cached++;
427
+ else toPull.push(img);
428
+ })
429
+ );
430
+ }
431
+ log(
432
+ `[zooid] image prepull: ${images.size} unique images (${cached} cached, ${toPull.length} to pull)`
433
+ );
434
+ if (toPull.length === 0) return;
435
+ await Promise.all(
436
+ toPull.map(async (img) => {
437
+ log(`[zooid] ${img} pulling\u2026`);
438
+ const start = Date.now();
439
+ const r = await exec(opts.engine, ["pull", img]);
440
+ const secs = ((Date.now() - start) / 1e3).toFixed(1);
441
+ if (r.code !== 0) {
442
+ throw new Error(
443
+ `image prepull failed for ${img}:
444
+ ${r.stderr.trim() || "(no stderr)"}`
445
+ );
446
+ }
447
+ log(`[zooid] ${img} \u2713 ${secs}s`);
448
+ })
449
+ );
450
+ }
451
+ var defaultExec = async (cmd, args) => {
452
+ try {
453
+ const { stdout, stderr } = await pExecFile(cmd, args, {
454
+ maxBuffer: 16 * 1024 * 1024
455
+ });
456
+ return { code: 0, stdout, stderr };
457
+ } catch (err) {
458
+ const e = err;
459
+ return {
460
+ code: typeof e.code === "number" ? e.code : 1,
461
+ stdout: e.stdout ?? "",
462
+ stderr: e.stderr ?? String(e)
463
+ };
464
+ }
465
+ };
466
+
404
467
  // src/daemon/start-daemon.ts
405
468
  function listenAsync(server) {
406
469
  return new Promise((resolve6) => {
@@ -421,7 +484,8 @@ async function startDaemon(opts = {}) {
421
484
  const cwd = opts.cwd ?? process.cwd();
422
485
  const found = opts.configPath ? { path: opts.configPath } : findConfigFile(cwd);
423
486
  if (!found) throw new Error("zooid.yaml is required");
424
- const base = loadZooidConfig(readFileSync2(found.path, "utf8"));
487
+ const configDir = dirname2(found.path);
488
+ const base = loadZooidConfig(readFileSync2(found.path, "utf8"), { configDir });
425
489
  const config = mergeCliFlags(base, opts.cliFlags ?? {});
426
490
  const approvals = new ApprovalCorrelator();
427
491
  const daemonSockPath = opts.agentsDir ? join3(opts.agentsDir, "..", "run", "context.sock") : join3(tmpdir(), `zooid-context-${process.pid}.sock`);
@@ -437,14 +501,27 @@ async function startDaemon(opts = {}) {
437
501
  } catch (err) {
438
502
  console.warn("[context] daemon socket startup failed; zooid-context MCP disabled:", err);
439
503
  }
504
+ const dataDir = opts.agentsDir ? dirname2(opts.agentsDir) : void 0;
440
505
  const registry = buildAcpRegistry(config, {
441
506
  approvals,
442
507
  onTap: opts.onTap,
443
508
  agentsDir: opts.agentsDir,
444
509
  contextSpawnRegistry: contextSocket ? contextSpawnRegistry : void 0,
445
- daemonSockPath: contextSocket ? daemonSockPath : void 0
510
+ daemonSockPath: contextSocket ? daemonSockPath : void 0,
511
+ configDir,
512
+ dataDir,
513
+ daemonHome: process.env.HOME
446
514
  });
447
515
  const agentNames = Object.keys(config.agents);
516
+ if (config.runtime !== "local") {
517
+ await prepullImages(registry, {
518
+ engine: config.runtime === "podman" ? "podman" : "docker",
519
+ runtime: config.runtime,
520
+ skip: opts.noPrepull ?? process.env.ZOOID_NO_PREPULL === "1",
521
+ refresh: opts.refreshImages ?? process.env.ZOOID_REFRESH_IMAGES === "1",
522
+ log: opts.prepullLog
523
+ });
524
+ }
448
525
  console.log(
449
526
  `[context] socket=${daemonSockPath} status=${contextSocket ? "listening" : "disabled"} agents={${agentNames.map((n) => `${n}:${registry.hasContextSpawn(n) ? "yes" : "no"}`).join(", ")}}`
450
527
  );
@@ -481,12 +558,13 @@ async function startDaemon(opts = {}) {
481
558
  hsToken: matrix.transport.hs_token,
482
559
  adminUserId: opts.adminUserId
483
560
  });
484
- const requestedPort = matrix.transport.port ?? 8080;
561
+ const requestedPort = matrix.transport.port ?? 9e3;
485
562
  server = serve({ fetch: transport.app.fetch, port: requestedPort, hostname: "0.0.0.0" });
486
563
  port = await listenAsync(server);
487
564
  const serverName = matrix.transport.user_namespace.split(":").slice(1).join(":").replace(/\\?\)?$/, "") || new URL(matrix.transport.homeserver).hostname;
488
565
  const asUserId = `@${matrix.transport.sender_localpart}:${serverName}`;
489
566
  const spaceLocalpart = matrix.transport.space ?? "dev";
567
+ const adminUserIds = opts.adminUserId ? [opts.adminUserId] : [];
490
568
  let spaceRoomId;
491
569
  try {
492
570
  spaceRoomId = await ensureWorkforceSpace({
@@ -494,14 +572,27 @@ async function startDaemon(opts = {}) {
494
572
  asUserId,
495
573
  serverName,
496
574
  spaceLocalpart,
497
- preset: "public_chat"
575
+ preset: "public_chat",
576
+ admins: adminUserIds
498
577
  });
499
578
  console.log(`[matrix] ensured workforce space #${spaceLocalpart}:${serverName} \u2192 ${spaceRoomId}`);
500
579
  } catch (err) {
501
580
  console.warn("[matrix] workforce space provisioning failed:", err);
502
581
  }
503
- await transport.bootstrap({ spaceRoomId, asUserId });
582
+ await transport.bootstrap({ spaceRoomId, asUserId, adminUserIds });
504
583
  if (spaceRoomId) {
584
+ try {
585
+ const generalRoomId = await ensureDefaultChannel({
586
+ client,
587
+ asUserId,
588
+ serverName,
589
+ spaceId: spaceRoomId,
590
+ admins: adminUserIds
591
+ });
592
+ console.log(`[matrix] ensured default channel #general:${serverName} \u2192 ${generalRoomId}`);
593
+ } catch (err) {
594
+ console.warn("[matrix] default channel provisioning failed:", err);
595
+ }
505
596
  try {
506
597
  const publisher = await startWorkforcePublisher({
507
598
  client,
@@ -1011,7 +1102,7 @@ async function runDev(flags) {
1011
1102
  process.env.MATRIX_AS_TOKEN = tokens.asToken;
1012
1103
  process.env.MATRIX_HS_TOKEN = tokens.hsToken;
1013
1104
  const rawYaml = readFileSync4(found.path, "utf8");
1014
- const preview = loadZooidConfig(rawYaml);
1105
+ const preview = loadZooidConfig(rawYaml, { configDir: dirname6(found.path) });
1015
1106
  const matrix = findMatrixTransport(preview);
1016
1107
  if (!matrix) {
1017
1108
  throw new Error("zooid.yaml: zooid dev requires at least one matrix transport");
@@ -1072,7 +1163,7 @@ async function runDev(flags) {
1072
1163
  },
1073
1164
  {
1074
1165
  title: "Start daemon",
1075
- task: async () => {
1166
+ task: async (_ctx, t) => {
1076
1167
  const captures = {};
1077
1168
  for (const name of Object.keys(preview.agents)) {
1078
1169
  captures[name] = wireAgentCapture({
@@ -1091,7 +1182,10 @@ async function runDev(flags) {
1091
1182
  installSignalHandlers: false,
1092
1183
  adminUserId: `@${flags.adminUser}:${shape.serverName}`,
1093
1184
  agentsDir: layout.agentsDir,
1094
- onTap: (agentName, event) => captures[agentName]?.onTap(event)
1185
+ onTap: (agentName, event) => captures[agentName]?.onTap(event),
1186
+ prepullLog: (line) => {
1187
+ t.output = line.replace(/^\[zooid\]\s+/, "").trim();
1188
+ }
1095
1189
  });
1096
1190
  }
1097
1191
  },
@@ -1099,10 +1193,12 @@ async function runDev(flags) {
1099
1193
  {
1100
1194
  title: "Start @zoon/web watcher (vite build --watch)",
1101
1195
  task: async () => {
1102
- const pkgDir = webSourcePackage(CLI_ROOT);
1196
+ const pkgDir = typeof flags.watchWeb === "string" ? resolve2(flags.watchWeb) : webSourcePackage(CLI_ROOT);
1103
1197
  if (!pkgDir) {
1198
+ const defaultPath = dirname6(dirname6(dirname6(CLI_ROOT))) + "/zoon/packages/web";
1104
1199
  throw new Error(
1105
- "--watch-web requires running from the monorepo source (no zoon/packages/web found)."
1200
+ `--watch-web: @zoon/web not found at ${defaultPath}.
1201
+ Pass an explicit path: --watch-web=/path/to/zoon/packages/web`
1106
1202
  );
1107
1203
  }
1108
1204
  ctx.webWatch = await startWebWatch({ webPackageDir: pkgDir });
@@ -1218,6 +1314,11 @@ You are the zooid setup assistant. Your job is to help the operator get a workfo
1218
1314
  - Zooid documentation: https://zooid.dev/docs/
1219
1315
  - Use your web fetch tool to read documentation pages when answering questions.
1220
1316
 
1317
+ ## Stack facts
1318
+
1319
+ - **Tuwunel** is a lightweight Rust Matrix homeserver (Conduit fork) backed by RocksDB. Single binary, low resource floor \u2014 not "heavy" for typical workforces.
1320
+ - **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.
1321
+
1221
1322
  ## Behavior
1222
1323
 
1223
1324
  - Be terse. Give the answer; link to the doc page; stop.
@@ -1656,6 +1757,7 @@ async function runStart(flags) {
1656
1757
 
1657
1758
  // src/commands/status.ts
1658
1759
  import { readFileSync as readFileSync5 } from "fs";
1760
+ import { dirname as dirname8 } from "path";
1659
1761
  import chalk2 from "chalk";
1660
1762
  async function probe(url, timeoutMs = 2e3) {
1661
1763
  try {
@@ -1679,7 +1781,9 @@ async function collectStatus(opts) {
1679
1781
  agents: []
1680
1782
  };
1681
1783
  }
1682
- const cfg = loadZooidConfig(readFileSync5(found.path, "utf8"));
1784
+ const cfg = loadZooidConfig(readFileSync5(found.path, "utf8"), {
1785
+ configDir: dirname8(found.path)
1786
+ });
1683
1787
  const matrixEntry = Object.entries(cfg.transports).find(
1684
1788
  ([, t]) => t.type === "matrix"
1685
1789
  );
@@ -1708,7 +1812,9 @@ async function runStatus(flags) {
1708
1812
  if (port === void 0) {
1709
1813
  const found = findConfigFile(cwd);
1710
1814
  if (found) {
1711
- const cfg = loadZooidConfig(readFileSync5(found.path, "utf8"));
1815
+ const cfg = loadZooidConfig(readFileSync5(found.path, "utf8"), {
1816
+ configDir: dirname8(found.path)
1817
+ });
1712
1818
  const matrix = findMatrixTransport(cfg);
1713
1819
  if (matrix) {
1714
1820
  const userIds = Object.values(cfg.agents).filter((a) => a.matrix?.transport === matrix.name).map((a) => a.matrix.user_id);
@@ -1741,14 +1847,17 @@ cli.command("start", "Run the daemon (production entry-point)").option("--data <
1741
1847
  printToken: flags.printToken
1742
1848
  });
1743
1849
  });
1744
- 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("--watch-web", "Run vite build --watch on @zoon/web (monorepo source only)").action(async (flags) => {
1850
+ 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(
1851
+ "--watch-web [path]",
1852
+ "Run vite build --watch on @zoon/web. Path defaults to sibling ../zoon/packages/web."
1853
+ ).action(async (flags) => {
1745
1854
  await runDev({
1746
1855
  dataDir: flags.data,
1747
1856
  engine: flags.engine,
1748
1857
  uiPort: Number(flags.uiPort),
1749
1858
  adminUser: flags.adminUser,
1750
1859
  adminPassword: flags.adminPassword,
1751
- watchWeb: Boolean(flags.watchWeb)
1860
+ watchWeb: flags.watchWeb
1752
1861
  });
1753
1862
  });
1754
1863
  cli.command(