zooid 0.8.0 → 0.9.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 (37) hide show
  1. package/README.md +5 -5
  2. package/dist/bin.js +195 -156
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-R6EDLH23.js → chunk-PDSJJWQW.js} +1928 -990
  5. package/dist/chunk-PDSJJWQW.js.map +1 -0
  6. package/dist/index.js +1 -1
  7. package/package.json +10 -10
  8. package/src/bin.ts +1 -1
  9. package/src/bootstrap/configs.ts +19 -6
  10. package/src/bootstrap/derive.ts +3 -2
  11. package/src/bootstrap/identity-bootstrap.integration.test.ts +53 -0
  12. package/src/bootstrap/registration-url.test.ts +14 -0
  13. package/src/bootstrap/registration-url.ts +7 -0
  14. package/src/commands/dev.ts +9 -9
  15. package/src/commands/init/generators.test.ts +30 -12
  16. package/src/commands/init/generators.ts +23 -12
  17. package/src/commands/init/prompts.ts +9 -34
  18. package/src/commands/init/registry.test.ts +2 -2
  19. package/src/commands/init/registry.ts +9 -8
  20. package/src/commands/init/scaffold-roundtrip.test.ts +38 -0
  21. package/src/commands/init.test.ts +17 -12
  22. package/src/commands/init.ts +2 -4
  23. package/src/daemon/pull-wiring.test.ts +14 -0
  24. package/src/daemon/pull-wiring.ts +4 -0
  25. package/src/daemon/start-daemon-pull.integration.test.ts +95 -0
  26. package/src/daemon/start-daemon.ts +62 -15
  27. package/src/daemon/sync-cursors.test.ts +38 -0
  28. package/src/daemon/sync-cursors.ts +31 -0
  29. package/src/web/fetch.integration.test.ts +4 -4
  30. package/src/web/fetch.test.ts +4 -4
  31. package/src/web/fetch.ts +2 -2
  32. package/src/web/pin.test.ts +2 -2
  33. package/src/web/pin.ts +2 -2
  34. package/src/web/resolve.test.ts +5 -5
  35. package/src/web/resolve.ts +5 -5
  36. package/src/web/test-helpers.ts +1 -1
  37. package/dist/chunk-R6EDLH23.js.map +0 -1
package/dist/bin.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  renderRegistration,
17
17
  startDaemonSocketServer,
18
18
  startWorkforcePublisher
19
- } from "./chunk-R6EDLH23.js";
19
+ } from "./chunk-PDSJJWQW.js";
20
20
 
21
21
  // src/bin.ts
22
22
  import { resolve as resolve6 } from "path";
@@ -25,8 +25,8 @@ import { cac } from "cac";
25
25
  // src/commands/dev.ts
26
26
  import chalk from "chalk";
27
27
  import { Listr } from "listr2";
28
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
29
- import { dirname as dirname6, join as join10, resolve as resolve3 } from "path";
28
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
29
+ import { dirname as dirname6, join as join12, resolve as resolve3 } from "path";
30
30
  import { fileURLToPath } from "url";
31
31
  import { serve as serve2 } from "@hono/node-server";
32
32
 
@@ -57,7 +57,19 @@ async function ensureAdminUser(opts) {
57
57
  }
58
58
 
59
59
  // src/bootstrap/configs.ts
60
+ import { join } from "path";
60
61
  import { mkdirSync, writeFileSync } from "fs";
62
+
63
+ // src/bootstrap/registration-url.ts
64
+ function deriveRegistrationUrl(opts) {
65
+ if (opts.port != null && opts.advertise_url != null)
66
+ throw new Error("registration url: 'port' and 'advertise_url' are mutually exclusive");
67
+ if (opts.advertise_url != null) return opts.advertise_url;
68
+ if (opts.port != null) return `http://host.docker.internal:${opts.port}`;
69
+ throw new Error("registration url: need port or advertise_url");
70
+ }
71
+
72
+ // src/bootstrap/configs.ts
61
73
  var TUWUNEL_INTERNAL_PORT = 8448;
62
74
  function renderTuwunelToml(opts) {
63
75
  return [
@@ -80,9 +92,13 @@ function writeBootstrapConfigs(opts) {
80
92
  mkdirSync(paths.mediaDir, { recursive: true });
81
93
  mkdirSync(paths.registrationsDir, { recursive: true });
82
94
  writeFileSync(paths.tuwunelTomlPath, renderTuwunelToml({ serverName }));
95
+ const id = opts.workstation ?? "zooid";
96
+ const url = opts.workstation ? deriveRegistrationUrl({ port: opts.port ?? 9099, advertise_url: opts.advertiseUrl }) : `http://host.docker.internal:9099`;
97
+ const exclusive = !!opts.workstation;
98
+ const registrationPath = join(paths.registrationsDir, `${id}.yaml`);
83
99
  const yaml = renderRegistration({
84
- id: "zooid",
85
- url: `http://host.docker.internal:9099`,
100
+ id,
101
+ url,
86
102
  homeserver: `http://localhost:${TUWUNEL_INTERNAL_PORT}`,
87
103
  asToken,
88
104
  hsToken,
@@ -91,28 +107,26 @@ function writeBootstrapConfigs(opts) {
91
107
  // BotPool.bootstrap creates `#alias:<server>` rooms when missing — the
92
108
  // AS needs an aliases namespace to legally claim them.
93
109
  aliasNamespace: `#.*:${serverName}`,
94
- // zooid dev shares @.*:<server_name> between bots and the predefined
95
- // admin human, so the AS cannot claim the namespace exclusively.
96
- exclusive: false
110
+ exclusive
97
111
  });
98
- writeFileSync(paths.appserviceYamlPath, yaml);
112
+ writeFileSync(registrationPath, yaml);
99
113
  }
100
114
 
101
115
  // src/bootstrap/data-layout.ts
102
- import { join } from "path";
116
+ import { join as join2 } from "path";
103
117
  function resolveDataLayout(dataRoot) {
104
- const agentsDir = join(dataRoot, "agents");
118
+ const agentsDir = join2(dataRoot, "agents");
105
119
  return {
106
120
  dataRoot,
107
- matrixDir: join(dataRoot, "matrix"),
108
- logsDir: join(dataRoot, "logs"),
121
+ matrixDir: join2(dataRoot, "matrix"),
122
+ logsDir: join2(dataRoot, "logs"),
109
123
  agentsDir,
110
- agentDir: (id) => join(agentsDir, id)
124
+ agentDir: (id) => join2(agentsDir, id)
111
125
  };
112
126
  }
113
127
 
114
128
  // src/bootstrap/derive.ts
115
- var NAMESPACE_RE = /^@\.\*:([A-Za-z0-9.-]+)$/;
129
+ var NAMESPACE_RE = /^@(?:\.\*|[a-z0-9-]+\\\.\.\*):([A-Za-z0-9.-]+)$/;
116
130
  function deriveHomeserverShape(matrix, agentUserIds) {
117
131
  const url = new URL(matrix.homeserver);
118
132
  const host = url.hostname;
@@ -120,7 +134,7 @@ function deriveHomeserverShape(matrix, agentUserIds) {
120
134
  const m = NAMESPACE_RE.exec(matrix.user_namespace);
121
135
  if (!m) {
122
136
  throw new Error(
123
- `user_namespace ${matrix.user_namespace}: expected the simple form '@.*:<server_name>'`
137
+ `user_namespace ${matrix.user_namespace}: expected '@.*:<server_name>' or '@<workstation>\\..*:<server_name>'`
124
138
  );
125
139
  }
126
140
  const serverName = m[1];
@@ -135,19 +149,19 @@ function deriveHomeserverShape(matrix, agentUserIds) {
135
149
  }
136
150
 
137
151
  // src/bootstrap/paths.ts
138
- import { join as join2 } from "path";
152
+ import { join as join3 } from "path";
139
153
  function resolvePaths(dataDir) {
140
- const configDir = join2(dataDir, "config");
141
- const registrationsDir = join2(configDir, "registrations");
154
+ const configDir = join3(dataDir, "config");
155
+ const registrationsDir = join3(configDir, "registrations");
142
156
  return {
143
157
  dataDir,
144
- dbDir: join2(dataDir, "db"),
145
- mediaDir: join2(dataDir, "media"),
158
+ dbDir: join3(dataDir, "db"),
159
+ mediaDir: join3(dataDir, "media"),
146
160
  configDir,
147
161
  registrationsDir,
148
- tuwunelTomlPath: join2(configDir, "tuwunel.toml"),
149
- appserviceYamlPath: join2(registrationsDir, "zooid.yaml"),
150
- envPath: join2(configDir, ".env")
162
+ tuwunelTomlPath: join3(configDir, "tuwunel.toml"),
163
+ appserviceYamlPath: join3(registrationsDir, "zooid.yaml"),
164
+ envPath: join3(configDir, ".env")
151
165
  };
152
166
  }
153
167
 
@@ -187,10 +201,10 @@ MATRIX_HS_TOKEN=${tokens.hsToken}
187
201
  }
188
202
 
189
203
  // src/daemon/start-daemon.ts
190
- import { readFileSync as readFileSync2 } from "fs";
204
+ import { readFileSync as readFileSync3 } from "fs";
191
205
  import { mkdir, unlink } from "fs/promises";
192
206
  import { tmpdir } from "os";
193
- import { dirname as dirname2, isAbsolute, join as join3, resolve } from "path";
207
+ import { dirname as dirname2, isAbsolute, join as join5, resolve } from "path";
194
208
  import { serve } from "@hono/node-server";
195
209
 
196
210
  // ../transport-http/src/server.ts
@@ -465,6 +479,31 @@ var defaultExec = async (cmd, args) => {
465
479
  }
466
480
  };
467
481
 
482
+ // src/daemon/sync-cursors.ts
483
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
484
+ import { join as join4 } from "path";
485
+ function makeSyncCursorStore(agentsDir) {
486
+ const fileFor = (agentName) => join4(agentsDir, agentName, "sync-since");
487
+ return {
488
+ loadSince(agentName) {
489
+ try {
490
+ return readFileSync2(fileFor(agentName), "utf8").trim() || null;
491
+ } catch {
492
+ return null;
493
+ }
494
+ },
495
+ saveSince(agentName, since) {
496
+ mkdirSync3(join4(agentsDir, agentName), { recursive: true });
497
+ writeFileSync3(fileFor(agentName), since, "utf8");
498
+ }
499
+ };
500
+ }
501
+
502
+ // src/daemon/pull-wiring.ts
503
+ function shouldBindHttpListener(mode) {
504
+ return mode !== "client";
505
+ }
506
+
468
507
  // src/daemon/start-daemon.ts
469
508
  function listenAsync(server) {
470
509
  return new Promise((resolve7) => {
@@ -486,10 +525,10 @@ async function startDaemon(opts = {}) {
486
525
  const found = opts.configPath ? { path: opts.configPath } : findConfigFile(cwd);
487
526
  if (!found) throw new Error("zooid.yaml is required");
488
527
  const configDir = dirname2(found.path);
489
- const base = loadZooidConfig(readFileSync2(found.path, "utf8"), { configDir });
528
+ const base = loadZooidConfig(readFileSync3(found.path, "utf8"), { configDir });
490
529
  const config = mergeCliFlags(base, opts.cliFlags ?? {});
491
530
  const approvals = new ApprovalCorrelator();
492
- const daemonSockPath = opts.agentsDir ? join3(opts.agentsDir, "..", "run", "context.sock") : join3(tmpdir(), `zooid-context-${process.pid}.sock`);
531
+ const daemonSockPath = opts.agentsDir ? join5(opts.agentsDir, "..", "run", "context.sock") : join5(tmpdir(), `zooid-context-${process.pid}.sock`);
493
532
  await mkdir(dirname2(daemonSockPath), { recursive: true }).catch(() => {
494
533
  });
495
534
  const contextSpawnRegistry = new SpawnRegistry();
@@ -527,6 +566,7 @@ async function startDaemon(opts = {}) {
527
566
  `[context] socket=${daemonSockPath} status=${contextSocket ? "listening" : "disabled"} agents={${agentNames.map((n) => `${n}:${registry.hasContextSpawn(n) ? "yes" : "no"}`).join(", ")}}`
528
567
  );
529
568
  let server = null;
569
+ let syncLoops;
530
570
  let stopped = false;
531
571
  let resolveStopped;
532
572
  const whenStopped = new Promise((r) => {
@@ -535,9 +575,15 @@ async function startDaemon(opts = {}) {
535
575
  const matrix = findMatrixTransport(config);
536
576
  let port;
537
577
  if (matrix) {
578
+ const mode = matrix.transport.mode ?? "appservice";
579
+ if (mode === "client" && !opts.agentsDir) {
580
+ throw new Error("matrix client (pull) mode requires a data dir for since-cursor persistence");
581
+ }
582
+ const cursors = opts.agentsDir ? makeSyncCursorStore(opts.agentsDir) : void 0;
538
583
  const client = new MatrixClient({
539
584
  homeserver: matrix.transport.homeserver,
540
- asToken: matrix.transport.as_token
585
+ asToken: matrix.transport.as_token,
586
+ ...opts.fetch ? { fetch: opts.fetch } : {}
541
587
  });
542
588
  const mediaClient = new MediaClient({
543
589
  homeserver: matrix.transport.homeserver,
@@ -559,6 +605,9 @@ async function startDaemon(opts = {}) {
559
605
  binding.agentWorkspacePath = isContainerRuntime ? "/workspace" : workspaceDir;
560
606
  bindings.push(binding);
561
607
  }
608
+ const serverName = matrix.transport.user_namespace.split(":").slice(1).join(":").replace(/\\?\)?$/, "") || new URL(matrix.transport.homeserver).hostname;
609
+ const asUserId = `@${matrix.transport.sender_localpart}:${serverName}`;
610
+ const nameByUserId = new Map(bindings.map((b) => [b.userId, b.name]));
562
611
  const transport = createMatrixTransport({
563
612
  agents: registry,
564
613
  approvals,
@@ -566,13 +615,25 @@ async function startDaemon(opts = {}) {
566
615
  bindings,
567
616
  hsToken: matrix.transport.hs_token,
568
617
  adminUserId: opts.adminUserId,
569
- media: mediaClient
618
+ botUserId: asUserId,
619
+ media: mediaClient,
620
+ mode,
621
+ loadSince: (uid) => {
622
+ const name = nameByUserId.get(uid);
623
+ return name && cursors ? cursors.loadSince(name) : null;
624
+ },
625
+ saveSince: (uid, since) => {
626
+ const name = nameByUserId.get(uid);
627
+ if (name && cursors) cursors.saveSince(name, since);
628
+ }
570
629
  });
571
- const requestedPort = matrix.transport.port ?? 9e3;
572
- server = serve({ fetch: transport.app.fetch, port: requestedPort, hostname: "0.0.0.0" });
573
- port = await listenAsync(server);
574
- const serverName = matrix.transport.user_namespace.split(":").slice(1).join(":").replace(/\\?\)?$/, "") || new URL(matrix.transport.homeserver).hostname;
575
- const asUserId = `@${matrix.transport.sender_localpart}:${serverName}`;
630
+ if (shouldBindHttpListener(mode)) {
631
+ const requestedPort = matrix.transport.port ?? 9e3;
632
+ server = serve({ fetch: transport.app.fetch, port: requestedPort, hostname: "0.0.0.0" });
633
+ port = await listenAsync(server);
634
+ } else {
635
+ port = 0;
636
+ }
576
637
  const spaceLocalpart = matrix.transport.space ?? "dev";
577
638
  const adminUserIds = opts.adminUserId ? [opts.adminUserId] : [];
578
639
  let spaceRoomId;
@@ -591,6 +652,11 @@ async function startDaemon(opts = {}) {
591
652
  console.warn("[matrix] workforce space provisioning failed:", err);
592
653
  }
593
654
  await transport.bootstrap({ spaceRoomId, asUserId, adminUserIds });
655
+ syncLoops = transport.syncLoops;
656
+ if (syncLoops?.length) {
657
+ for (const loop of syncLoops) void loop.run();
658
+ console.log(`[matrix] pull mode: ${syncLoops.length} sync loop(s) running`);
659
+ }
594
660
  if (spaceRoomId) {
595
661
  try {
596
662
  const generalRoomId = await ensureDefaultChannel({
@@ -611,7 +677,7 @@ async function startDaemon(opts = {}) {
611
677
  asUserId,
612
678
  getAgents: () => bindings
613
679
  });
614
- console.log(`[matrix] published eco.zoon.workforce (${bindings.length} agents)`);
680
+ console.log(`[matrix] published dev.zooid.workforce (${bindings.length} agents)`);
615
681
  void publisher;
616
682
  } catch (err) {
617
683
  console.warn("[matrix] workforce roster publication failed:", err);
@@ -629,6 +695,10 @@ async function startDaemon(opts = {}) {
629
695
  const stop = async () => {
630
696
  if (stopped) return whenStopped;
631
697
  stopped = true;
698
+ try {
699
+ if (syncLoops) for (const loop of syncLoops) loop.stop();
700
+ } catch {
701
+ }
632
702
  try {
633
703
  if (server) await closeAsync(server);
634
704
  } catch {
@@ -773,18 +843,18 @@ function execEngine(engine, args) {
773
843
 
774
844
  // src/web/resolve.ts
775
845
  import { existsSync as existsSync3 } from "fs";
776
- import { dirname as dirname3, join as join5, resolve as resolve2 } from "path";
846
+ import { dirname as dirname3, join as join7, resolve as resolve2 } from "path";
777
847
 
778
848
  // src/web/fetch.ts
779
- import { mkdirSync as mkdirSync3, renameSync, rmSync, existsSync as existsSync2, readdirSync, writeFileSync as writeFileSync3 } from "fs";
780
- import { join as join4 } from "path";
849
+ import { mkdirSync as mkdirSync4, renameSync, rmSync, existsSync as existsSync2, readdirSync, writeFileSync as writeFileSync4 } from "fs";
850
+ import { join as join6 } from "path";
781
851
  import { createHash, randomUUID as randomUUID2 } from "crypto";
782
852
  import * as tar from "tar";
783
- var PKG = "@zooid/zoon-web";
853
+ var PKG = "@zooid/web";
784
854
  var DEFAULT_REGISTRY = "https://registry.npmjs.org";
785
855
  async function fetchWebBundle(opts) {
786
- const target = join4(opts.cacheDir, opts.version);
787
- if (existsSync2(join4(target, "index.html"))) return target;
856
+ const target = join6(opts.cacheDir, opts.version);
857
+ if (existsSync2(join6(target, "index.html"))) return target;
788
858
  if (existsSync2(target)) rmSync(target, { recursive: true, force: true });
789
859
  const f = opts.fetch ?? globalThis.fetch;
790
860
  const registry = (opts.registryUrl ?? DEFAULT_REGISTRY).replace(/\/$/, "");
@@ -812,11 +882,11 @@ async function fetchWebBundle(opts) {
812
882
  Cause: ${err instanceof Error ? err.message : String(err)}`
813
883
  );
814
884
  }
815
- const tmp = join4(opts.cacheDir, `.tmp-${randomUUID2().slice(0, 8)}`);
816
- mkdirSync3(tmp, { recursive: true });
817
- const tgzPath = join4(tmp, ".bundle.tgz");
885
+ const tmp = join6(opts.cacheDir, `.tmp-${randomUUID2().slice(0, 8)}`);
886
+ mkdirSync4(tmp, { recursive: true });
887
+ const tgzPath = join6(tmp, ".bundle.tgz");
818
888
  try {
819
- writeFileSync3(tgzPath, tgz);
889
+ writeFileSync4(tgzPath, tgz);
820
890
  await tar.extract({
821
891
  file: tgzPath,
822
892
  cwd: tmp,
@@ -825,7 +895,7 @@ async function fetchWebBundle(opts) {
825
895
  filter: (p) => p === "package/dist" || p.startsWith("package/dist/")
826
896
  });
827
897
  rmSync(tgzPath);
828
- if (!existsSync2(join4(tmp, "index.html"))) {
898
+ if (!existsSync2(join6(tmp, "index.html"))) {
829
899
  throw new Error(`${PKG}@${opts.version} tarball has no dist/index.html`);
830
900
  }
831
901
  renameSync(tmp, target);
@@ -835,7 +905,7 @@ async function fetchWebBundle(opts) {
835
905
  }
836
906
  for (const entry of readdirSync(opts.cacheDir)) {
837
907
  if (entry !== opts.version) {
838
- rmSync(join4(opts.cacheDir, entry), { recursive: true, force: true });
908
+ rmSync(join6(opts.cacheDir, entry), { recursive: true, force: true });
839
909
  }
840
910
  }
841
911
  return target;
@@ -845,42 +915,42 @@ async function fetchWebBundle(opts) {
845
915
  var ENV_OVERRIDE = "ZOOID_DEV_WEB_ROOT_OVERRIDE";
846
916
  async function ensureWebRoot(opts) {
847
917
  const override = process.env[ENV_OVERRIDE];
848
- if (override && existsSync3(join5(override, "index.html"))) return resolve2(override);
918
+ if (override && existsSync3(join7(override, "index.html"))) return resolve2(override);
849
919
  const fromSource = webSourcePackage(opts.cliRoot);
850
- if (fromSource && existsSync3(join5(fromSource, "dist", "index.html"))) {
851
- return join5(fromSource, "dist");
920
+ if (fromSource && existsSync3(join7(fromSource, "dist", "index.html"))) {
921
+ return join7(fromSource, "dist");
852
922
  }
853
923
  if (!opts.version) {
854
924
  throw new Error(
855
- `No @zooid/zoon-web version pin (zooid.zoonWebVersion) in the cli package.json and no monorepo sibling build found.
925
+ `No @zooid/web version pin (zooid.webVersion) in the cli package.json and no monorepo sibling build found.
856
926
  Set ${ENV_OVERRIDE} to a built dist, or run from the monorepo.`
857
927
  );
858
928
  }
859
- opts.onProgress?.(`Fetching @zooid/zoon-web ${opts.version}\u2026`);
929
+ opts.onProgress?.(`Fetching @zooid/web ${opts.version}\u2026`);
860
930
  const fetchBundleFn = opts.fetchBundle ?? fetchWebBundle;
861
931
  return fetchBundleFn({ version: opts.version, cacheDir: opts.cacheDir });
862
932
  }
863
933
  function webSourcePackage(cliRoot) {
864
934
  const workspaceRoot = dirname3(dirname3(dirname3(cliRoot)));
865
- const candidate = join5(workspaceRoot, "zoon", "packages", "web");
866
- return existsSync3(join5(candidate, "package.json")) ? candidate : null;
935
+ const candidate = join7(workspaceRoot, "zooid-clients", "packages", "web");
936
+ return existsSync3(join7(candidate, "package.json")) ? candidate : null;
867
937
  }
868
938
 
869
939
  // src/web/pin.ts
870
- import { readFileSync as readFileSync3 } from "fs";
871
- import { join as join6 } from "path";
940
+ import { readFileSync as readFileSync4 } from "fs";
941
+ import { join as join8 } from "path";
872
942
  function readZoonWebPin(cliRoot) {
873
943
  try {
874
- const pkg = JSON.parse(readFileSync3(join6(cliRoot, "package.json"), "utf8"));
875
- return pkg.zooid?.zoonWebVersion;
944
+ const pkg = JSON.parse(readFileSync4(join8(cliRoot, "package.json"), "utf8"));
945
+ return pkg.zooid?.webVersion;
876
946
  } catch {
877
947
  return void 0;
878
948
  }
879
949
  }
880
950
 
881
951
  // src/web/static.ts
882
- import { readFileSync as readFileSync4, statSync } from "fs";
883
- import { extname, join as join7, normalize } from "path";
952
+ import { readFileSync as readFileSync5, statSync } from "fs";
953
+ import { extname, join as join9, normalize } from "path";
884
954
  import { Hono as Hono2 } from "hono";
885
955
  var MIME = {
886
956
  ".html": "text/html; charset=utf-8",
@@ -906,19 +976,19 @@ function webStatic(opts) {
906
976
  const requested = decodeURIComponent(url.pathname);
907
977
  const wantFile = requested === "/" ? "/index.html" : requested;
908
978
  const safe = normalize(wantFile).replace(/^(\.\.[/\\])+/g, "");
909
- const filePath = join7(opts.webRoot, safe);
979
+ const filePath = join9(opts.webRoot, safe);
910
980
  if (!filePath.startsWith(opts.webRoot)) return c.notFound();
911
981
  try {
912
982
  const stat = statSync(filePath);
913
983
  if (stat.isFile()) {
914
- const body = readFileSync4(filePath);
984
+ const body = readFileSync5(filePath);
915
985
  const ct = MIME[extname(filePath)] ?? "application/octet-stream";
916
986
  return c.body(body, 200, { "content-type": ct });
917
987
  }
918
988
  } catch {
919
989
  }
920
990
  if (isAssetPath(requested)) return c.notFound();
921
- const indexBytes = readFileSync4(join7(opts.webRoot, "index.html"));
991
+ const indexBytes = readFileSync5(join9(opts.webRoot, "index.html"));
922
992
  return c.body(indexBytes, 200, {
923
993
  "content-type": MIME[".html"]
924
994
  });
@@ -929,10 +999,10 @@ function webStatic(opts) {
929
999
  // src/web/watch.ts
930
1000
  import { spawn as spawn2 } from "child_process";
931
1001
  import { existsSync as existsSync4, statSync as statSync2 } from "fs";
932
- import { join as join8 } from "path";
1002
+ import { join as join10 } from "path";
933
1003
  async function startWebWatch(opts) {
934
- const distPath = join8(opts.webPackageDir, "dist");
935
- const indexPath = join8(distPath, "index.html");
1004
+ const distPath = join10(opts.webPackageDir, "dist");
1005
+ const indexPath = join10(distPath, "index.html");
936
1006
  const timeoutMs = opts.firstBuildTimeoutMs ?? 6e4;
937
1007
  const spawnTime = Date.now();
938
1008
  const child = spawn2(
@@ -1006,7 +1076,7 @@ async function stopChild(child) {
1006
1076
 
1007
1077
  // src/observability/paths.ts
1008
1078
  import { mkdir as mkdir2, readdir, rm, symlink, unlink as unlink2 } from "fs/promises";
1009
- import { join as join9 } from "path";
1079
+ import { join as join11 } from "path";
1010
1080
  function localDateSlug(d) {
1011
1081
  const y = d.getFullYear();
1012
1082
  const m = String(d.getMonth() + 1).padStart(2, "0");
@@ -1016,19 +1086,19 @@ function localDateSlug(d) {
1016
1086
  var DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
1017
1087
  function resolveLogPaths({ dataDir, now }) {
1018
1088
  const slug = localDateSlug(now ?? /* @__PURE__ */ new Date());
1019
- const logsDir = join9(dataDir, "logs");
1020
- const dayDir = join9(logsDir, slug);
1089
+ const logsDir = join11(dataDir, "logs");
1090
+ const dayDir = join11(logsDir, slug);
1021
1091
  return {
1022
1092
  dataDir,
1023
1093
  logsDir,
1024
1094
  dayDir,
1025
1095
  daySlug: slug,
1026
- todayLink: join9(logsDir, "today"),
1027
- tuwunelLog: join9(dayDir, "tuwunel.log"),
1028
- daemonLog: join9(dayDir, "daemon.log"),
1029
- devLog: join9(dayDir, "dev.log"),
1030
- agentLog: (n) => join9(dayDir, `agent-${n}.log`),
1031
- agentTap: (n) => join9(dayDir, `agent-${n}.acp.jsonl`)
1096
+ todayLink: join11(logsDir, "today"),
1097
+ tuwunelLog: join11(dayDir, "tuwunel.log"),
1098
+ daemonLog: join11(dayDir, "daemon.log"),
1099
+ devLog: join11(dayDir, "dev.log"),
1100
+ agentLog: (n) => join11(dayDir, `agent-${n}.log`),
1101
+ agentTap: (n) => join11(dayDir, `agent-${n}.acp.jsonl`)
1032
1102
  };
1033
1103
  }
1034
1104
  async function ensureDayFolder(p) {
@@ -1041,7 +1111,7 @@ async function ensureDayFolder(p) {
1041
1111
  }
1042
1112
  async function pruneOldDays(opts) {
1043
1113
  if (opts.retainDays <= 0) return [];
1044
- const logsDir = join9(opts.dataDir, "logs");
1114
+ const logsDir = join11(opts.dataDir, "logs");
1045
1115
  let entries;
1046
1116
  try {
1047
1117
  entries = await readdir(logsDir);
@@ -1060,7 +1130,7 @@ async function pruneOldDays(opts) {
1060
1130
  const [y, m, d] = name.split("-").map(Number);
1061
1131
  const t = new Date(y, m - 1, d).getTime();
1062
1132
  if (t < cutoff) {
1063
- await rm(join9(logsDir, name), { recursive: true, force: true });
1133
+ await rm(join11(logsDir, name), { recursive: true, force: true });
1064
1134
  removed.push(name);
1065
1135
  }
1066
1136
  }
@@ -1210,7 +1280,7 @@ var CLI_ROOT = (() => {
1210
1280
  const pkgPath = resolve3(dir, "package.json");
1211
1281
  if (existsSync5(pkgPath)) {
1212
1282
  try {
1213
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
1283
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
1214
1284
  if (pkg.name === "zooid" || pkg.name === "@zooid/cli") return dir;
1215
1285
  } catch {
1216
1286
  }
@@ -1233,7 +1303,7 @@ async function runDev(flags) {
1233
1303
  const tokens = ensureTokens(paths.envPath);
1234
1304
  process.env.MATRIX_AS_TOKEN = tokens.asToken;
1235
1305
  process.env.MATRIX_HS_TOKEN = tokens.hsToken;
1236
- const rawYaml = readFileSync5(found.path, "utf8");
1306
+ const rawYaml = readFileSync6(found.path, "utf8");
1237
1307
  const preview = loadZooidConfig(rawYaml, { configDir: dirname6(found.path) });
1238
1308
  const matrix = findMatrixTransport(preview);
1239
1309
  if (!matrix) {
@@ -1327,14 +1397,14 @@ async function runDev(flags) {
1327
1397
  },
1328
1398
  ...flags.watchWeb ? [
1329
1399
  {
1330
- title: "Start @zooid/zoon-web watcher (vite build --watch)",
1400
+ title: "Start @zooid/web watcher (vite build --watch)",
1331
1401
  task: async () => {
1332
1402
  const pkgDir = typeof flags.watchWeb === "string" ? resolve3(flags.watchWeb) : webSourcePackage(CLI_ROOT);
1333
1403
  if (!pkgDir) {
1334
- const defaultPath = dirname6(dirname6(dirname6(CLI_ROOT))) + "/zoon/packages/web";
1404
+ const defaultPath = dirname6(dirname6(dirname6(CLI_ROOT))) + "/zooid-clients/packages/web";
1335
1405
  throw new Error(
1336
- `--watch-web: @zooid/zoon-web not found at ${defaultPath}.
1337
- Pass an explicit path: --watch-web=/path/to/zoon/packages/web`
1406
+ `--watch-web: @zooid/web not found at ${defaultPath}.
1407
+ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
1338
1408
  );
1339
1409
  }
1340
1410
  ctx.webWatch = await startWebWatch({ webPackageDir: pkgDir });
@@ -1342,11 +1412,11 @@ Pass an explicit path: --watch-web=/path/to/zoon/packages/web`
1342
1412
  }
1343
1413
  ] : [],
1344
1414
  {
1345
- title: `Serve @zooid/zoon-web on http://localhost:${flags.uiPort}`,
1415
+ title: `Serve @zooid/web on http://localhost:${flags.uiPort}`,
1346
1416
  task: async (_, t) => {
1347
1417
  const webRoot = ctx.webWatch?.distPath ?? await ensureWebRoot({
1348
1418
  cliRoot: CLI_ROOT,
1349
- cacheDir: join10(layout.dataRoot, "web"),
1419
+ cacheDir: join12(layout.dataRoot, "web"),
1350
1420
  version: readZoonWebPin(CLI_ROOT),
1351
1421
  onProgress: (msg) => {
1352
1422
  t.output = msg;
@@ -1397,10 +1467,10 @@ Pass an explicit path: --watch-web=/path/to/zoon/packages/web`
1397
1467
  [
1398
1468
  "",
1399
1469
  chalk.bold("Tuwunel is up.") + ` ${homeserver}`,
1400
- chalk.bold("UI: ") + ` http://localhost:${flags.uiPort}`,
1470
+ chalk.bold("UI: ") + ` http://localhost:${ctx.uiServer?.address()?.port ?? flags.uiPort}`,
1401
1471
  ` ${chalk.cyan("admin user:")} ${flags.adminUser} / ${flags.adminPassword}`,
1402
1472
  ` ${chalk.cyan("data dir:")} ${layout.dataRoot}`,
1403
- ...ctx.webWatch ? [` ${chalk.cyan("web watcher:")} live (vite build --watch on @zooid/zoon-web)`] : [],
1473
+ ...ctx.webWatch ? [` ${chalk.cyan("web watcher:")} live (vite build --watch on @zooid/web)`] : [],
1404
1474
  "",
1405
1475
  chalk.dim("Press Ctrl-C to stop."),
1406
1476
  ""
@@ -1424,15 +1494,21 @@ function loadEnvFiles(cwd) {
1424
1494
  }
1425
1495
 
1426
1496
  // src/commands/init.ts
1427
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
1428
- import { dirname as dirname7, join as join12, resolve as resolve4 } from "path";
1497
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync2, writeFileSync as writeFileSync5 } from "fs";
1498
+ import { dirname as dirname7, join as join14, resolve as resolve4 } from "path";
1429
1499
 
1430
1500
  // src/commands/init/generators.ts
1431
1501
  function generateZooidYaml(opts) {
1432
- const acpBlock = opts.preset === "opencode" ? ` acp: { preset: opencode }` : ` acp:
1502
+ const acpBlock = opts.preset === "opencode" || !opts.model ? ` acp: { preset: ${opts.preset} }` : ` acp:
1433
1503
  preset: ${opts.preset}
1434
1504
  model: ${opts.model}`;
1435
1505
  return `runtime: local
1506
+ workstation: dev
1507
+
1508
+ # This daemon connects in push mode: the homeserver reaches the listener on
1509
+ # \`port\` below. To run it against a homeserver it can't open a port to \u2014
1510
+ # e.g. on your laptop, behind NAT \u2014 switch to pull mode: set \`mode: client\`
1511
+ # under transports.matrix and drop \`port\`. Agents are reachable either way.
1436
1512
 
1437
1513
  transports:
1438
1514
  matrix:
@@ -1489,23 +1565,20 @@ function generateOpencodeJson(opts) {
1489
1565
  return JSON.stringify(
1490
1566
  {
1491
1567
  $schema: "https://opencode.ai/config.json",
1492
- model: "TODO/your-model",
1493
1568
  provider: { TODO: { options: {} } }
1494
1569
  },
1495
1570
  null,
1496
1571
  2
1497
1572
  ) + "\n";
1498
1573
  }
1499
- const cfg = {
1500
- $schema: "https://opencode.ai/config.json",
1501
- model: `${opts.provider}/${opts.model}`,
1502
- provider: {
1503
- [opts.provider]: {
1504
- options: { apiKey: `{env:${opts.apiKeyEnvVar}}` }
1505
- }
1506
- },
1507
- permission: { webfetch: "allow" }
1574
+ const cfg = { $schema: "https://opencode.ai/config.json" };
1575
+ if (opts.model) cfg.model = `${opts.provider}/${opts.model}`;
1576
+ cfg.provider = {
1577
+ [opts.provider]: {
1578
+ options: { apiKey: `{env:${opts.apiKeyEnvVar}}` }
1579
+ }
1508
1580
  };
1581
+ cfg.permission = { webfetch: "allow" };
1509
1582
  return JSON.stringify(cfg, null, 2) + "\n";
1510
1583
  }
1511
1584
  function generateEnv(opts) {
@@ -1525,7 +1598,6 @@ provider-specific options and per-tool permissions.
1525
1598
  var SIMPLE_PRESETS = [
1526
1599
  {
1527
1600
  preset: "claude",
1528
- models: ["claude-sonnet-4-6", "claude-opus-4-7", "claude-haiku-4-5"],
1529
1601
  subscriptionLabel: "Claude subscription (Pro / Max / Team / Enterprise)",
1530
1602
  apiKeyEnvVar: "ANTHROPIC_API_KEY",
1531
1603
  apiKeyPromptLabel: "Anthropic",
@@ -1533,7 +1605,6 @@ var SIMPLE_PRESETS = [
1533
1605
  },
1534
1606
  {
1535
1607
  preset: "codex",
1536
- models: ["gpt-5.5"],
1537
1608
  subscriptionLabel: "ChatGPT subscription (Plus / Pro / Team)",
1538
1609
  apiKeyEnvVar: "OPENAI_API_KEY",
1539
1610
  apiKeyPromptLabel: "OpenAI",
@@ -1545,28 +1616,24 @@ var OPENCODE_PROVIDERS = [
1545
1616
  id: "opencode-go",
1546
1617
  label: "opencode-go",
1547
1618
  description: "$10/mo subscription \u2014 Kimi, GLM, MiniMax",
1548
- models: ["kimi-k2.6", "kimi-k2.5", "glm-5.1", "glm-5", "minimax-m2.7"],
1549
1619
  apiKeyEnvVar: "OPENCODE_API_KEY"
1550
1620
  },
1551
1621
  {
1552
1622
  id: "opencode",
1553
1623
  label: "opencode (Zen)",
1554
1624
  description: "free tier \u2014 curated models",
1555
- models: ["kimi-k2.5-free", "glm-4.7-free", "minimax-m2.1-free"],
1556
1625
  apiKeyEnvVar: "OPENCODE_API_KEY"
1557
1626
  },
1558
1627
  {
1559
1628
  id: "anthropic",
1560
1629
  label: "Anthropic",
1561
1630
  description: "Claude via direct Anthropic API",
1562
- models: ["claude-sonnet-4-6", "claude-opus-4-7", "claude-haiku-4-5"],
1563
1631
  apiKeyEnvVar: "ANTHROPIC_API_KEY"
1564
1632
  },
1565
1633
  {
1566
1634
  id: "openrouter",
1567
1635
  label: "OpenRouter",
1568
1636
  description: "many providers via OpenRouter",
1569
- models: ["anthropic/claude-sonnet-4-6", "zhipuai/glm-5", "moonshot/kimi-k2.6"],
1570
1637
  apiKeyEnvVar: "OPENROUTER_API_KEY"
1571
1638
  }
1572
1639
  ];
@@ -1580,11 +1647,11 @@ function findOpencodeProvider(id) {
1580
1647
  // src/commands/init/sniff.ts
1581
1648
  import { existsSync as existsSync6 } from "fs";
1582
1649
  import { homedir } from "os";
1583
- import { join as join11 } from "path";
1650
+ import { join as join13 } from "path";
1584
1651
  function sniffCredentials(preset, home = homedir()) {
1585
1652
  const meta = findSimplePreset(preset);
1586
1653
  if (!meta) return { found: false };
1587
- const full = join11(home, meta.credentialDir);
1654
+ const full = join13(home, meta.credentialDir);
1588
1655
  if (existsSync6(full)) return { found: true, path: full };
1589
1656
  return { found: false };
1590
1657
  }
@@ -1603,7 +1670,7 @@ var IGNORED_PREEXISTING = /* @__PURE__ */ new Set([
1603
1670
  ]);
1604
1671
  async function runInit(opts) {
1605
1672
  const dir = resolve4(opts.dir);
1606
- mkdirSync4(dir, { recursive: true });
1673
+ mkdirSync5(dir, { recursive: true });
1607
1674
  if (!opts.force) {
1608
1675
  const blocking = readdirSync2(dir).filter((n) => !IGNORED_PREEXISTING.has(n));
1609
1676
  if (blocking.length > 0) {
@@ -1614,7 +1681,6 @@ async function runInit(opts) {
1614
1681
  }
1615
1682
  const writes = [];
1616
1683
  if (opts.preset === "claude" || opts.preset === "codex") {
1617
- if (!opts.model) throw new Error(`--model is required for preset ${opts.preset}`);
1618
1684
  if (!opts.auth) throw new Error("--auth (subscription|api-key) is required for claude/codex");
1619
1685
  const meta = findSimplePreset(opts.preset);
1620
1686
  writes.push({
@@ -1644,7 +1710,6 @@ async function runInit(opts) {
1644
1710
  const isCustom = opts.provider === "custom";
1645
1711
  const providerMeta = isCustom ? void 0 : findOpencodeProvider(opts.provider);
1646
1712
  if (!isCustom && !providerMeta) throw new Error(`unknown opencode provider: ${opts.provider}`);
1647
- if (!isCustom && !opts.model) throw new Error("--model is required (paired with --provider)");
1648
1713
  if (!isCustom && !opts.apiKey) throw new Error("--api-key is required for opencode");
1649
1714
  writes.push({ path: "zooid.yaml", content: generateZooidYaml({ preset: "opencode" }) });
1650
1715
  writes.push({ path: "agents/zooid-assistant/AGENTS.md", content: generateAgentsMd() });
@@ -1672,14 +1737,14 @@ async function runInit(opts) {
1672
1737
  }
1673
1738
  writes.push({ path: ".gitignore", content: generateGitignore() });
1674
1739
  for (const w of writes) {
1675
- const full = join12(dir, w.path);
1740
+ const full = join14(dir, w.path);
1676
1741
  const exists = existsSync7(full);
1677
1742
  if (exists && !opts.overwrite) {
1678
1743
  console.warn(`\u26A0 ${w.path} exists; left as-is (use --force --overwrite to replace)`);
1679
1744
  continue;
1680
1745
  }
1681
- mkdirSync4(dirname7(full), { recursive: true });
1682
- writeFileSync4(full, w.content);
1746
+ mkdirSync5(dirname7(full), { recursive: true });
1747
+ writeFileSync5(full, w.content);
1683
1748
  console.log(`\u2713 Created ${w.path}`);
1684
1749
  }
1685
1750
  if ((opts.preset === "claude" || opts.preset === "codex") && opts.auth === "subscription") {
@@ -1715,19 +1780,7 @@ async function resolveOptions(flags) {
1715
1780
  () => "--preset is required (claude | codex | opencode)"
1716
1781
  );
1717
1782
  if (preset === "opencode") {
1718
- const provider = flags.provider ?? await ask(
1719
- () => select({
1720
- message: "Which provider?",
1721
- choices: [
1722
- ...OPENCODE_PROVIDERS.map((p) => ({
1723
- name: `${p.label} (${p.description})`,
1724
- value: p.id
1725
- })),
1726
- { name: "custom (write your own opencode.json block)", value: "custom" }
1727
- ]
1728
- }),
1729
- () => "--provider is required for opencode"
1730
- );
1783
+ const provider = flags.provider ?? "opencode-go";
1731
1784
  if (provider === "custom") {
1732
1785
  return {
1733
1786
  dir: flags.dir,
@@ -1739,13 +1792,6 @@ async function resolveOptions(flags) {
1739
1792
  }
1740
1793
  const meta2 = findOpencodeProvider(provider);
1741
1794
  if (!meta2) throw new Error(`unknown opencode provider: ${provider}`);
1742
- const model2 = flags.model ?? await ask(
1743
- () => select({
1744
- message: "Which model?",
1745
- choices: meta2.models.map((m) => ({ name: m, value: m }))
1746
- }),
1747
- () => "--model is required"
1748
- );
1749
1795
  const apiKey2 = flags.apiKey ?? await ask(
1750
1796
  () => password({ message: `${meta2.label} API key:` }),
1751
1797
  () => "--api-key is required"
@@ -1754,7 +1800,7 @@ async function resolveOptions(flags) {
1754
1800
  dir: flags.dir,
1755
1801
  preset,
1756
1802
  provider,
1757
- model: model2,
1803
+ model: flags.model,
1758
1804
  apiKey: apiKey2,
1759
1805
  force: flags.force,
1760
1806
  overwrite: flags.overwrite
@@ -1772,13 +1818,6 @@ async function resolveOptions(flags) {
1772
1818
  }),
1773
1819
  () => "--auth is required (subscription | api-key)"
1774
1820
  );
1775
- const model = flags.model ?? await ask(
1776
- () => select({
1777
- message: "Which model?",
1778
- choices: meta.models.map((m) => ({ name: m, value: m }))
1779
- }),
1780
- () => "--model is required"
1781
- );
1782
1821
  const apiKey = auth === "api-key" ? flags.apiKey ?? await ask(
1783
1822
  () => password({ message: `${meta.apiKeyPromptLabel} API key:` }),
1784
1823
  () => "--api-key is required when --auth=api-key"
@@ -1787,7 +1826,7 @@ async function resolveOptions(flags) {
1787
1826
  dir: flags.dir,
1788
1827
  preset,
1789
1828
  auth,
1790
- model,
1829
+ model: flags.model,
1791
1830
  apiKey,
1792
1831
  force: flags.force,
1793
1832
  overwrite: flags.overwrite
@@ -1797,7 +1836,7 @@ async function resolveOptions(flags) {
1797
1836
  // src/commands/logs.ts
1798
1837
  import { readFile, readdir as readdir2, readlink } from "fs/promises";
1799
1838
  import { existsSync as existsSync8 } from "fs";
1800
- import { join as join13 } from "path";
1839
+ import { join as join15 } from "path";
1801
1840
  var KNOWN_SOURCES = ["tuwunel", "daemon", "dev"];
1802
1841
  async function runLogs(flags) {
1803
1842
  const writer = flags.writer ?? ((s) => process.stdout.write(s));
@@ -1816,7 +1855,7 @@ async function runLogs(flags) {
1816
1855
  writer("no logs yet\n");
1817
1856
  return;
1818
1857
  }
1819
- const dayDir = join13(flags.dataDir, "logs", day);
1858
+ const dayDir = join15(flags.dataDir, "logs", day);
1820
1859
  if (!existsSync8(dayDir)) {
1821
1860
  writer(`no logs for ${day}
1822
1861
  `);
@@ -1845,7 +1884,7 @@ async function runLogs(flags) {
1845
1884
  writer(await readFile(path, "utf8"));
1846
1885
  }
1847
1886
  async function resolveTodaySlug(dataDir) {
1848
- const link = join13(dataDir, "logs", "today");
1887
+ const link = join15(dataDir, "logs", "today");
1849
1888
  try {
1850
1889
  return await readlink(link);
1851
1890
  } catch {
@@ -1854,18 +1893,18 @@ async function resolveTodaySlug(dataDir) {
1854
1893
  }
1855
1894
  function resolveSourcePath(dayDir, source) {
1856
1895
  if (source.startsWith("agent-")) {
1857
- if (source.endsWith(".acp")) return join13(dayDir, `${source.slice(0, -4)}.acp.jsonl`);
1858
- return join13(dayDir, `${source}.log`);
1896
+ if (source.endsWith(".acp")) return join15(dayDir, `${source.slice(0, -4)}.acp.jsonl`);
1897
+ return join15(dayDir, `${source}.log`);
1859
1898
  }
1860
1899
  if (KNOWN_SOURCES.includes(source))
1861
- return join13(dayDir, `${source}.log`);
1862
- return join13(dayDir, source);
1900
+ return join15(dayDir, `${source}.log`);
1901
+ return join15(dayDir, source);
1863
1902
  }
1864
1903
  async function dumpByTurn(dayDir, turnId, writer) {
1865
1904
  const entries = await readdir2(dayDir);
1866
1905
  const taps = entries.filter((e) => e.endsWith(".acp.jsonl")).sort();
1867
1906
  for (const f of taps) {
1868
- const text = await readFile(join13(dayDir, f), "utf8");
1907
+ const text = await readFile(join15(dayDir, f), "utf8");
1869
1908
  for (const line of text.split("\n")) {
1870
1909
  if (!line) continue;
1871
1910
  try {
@@ -1899,7 +1938,7 @@ async function runStart(flags) {
1899
1938
  }
1900
1939
 
1901
1940
  // src/commands/status.ts
1902
- import { readFileSync as readFileSync6 } from "fs";
1941
+ import { readFileSync as readFileSync7 } from "fs";
1903
1942
  import { dirname as dirname8 } from "path";
1904
1943
  import chalk2 from "chalk";
1905
1944
  async function probe(url, timeoutMs = 2e3) {
@@ -1924,7 +1963,7 @@ async function collectStatus(opts) {
1924
1963
  agents: []
1925
1964
  };
1926
1965
  }
1927
- const cfg = loadZooidConfig(readFileSync6(found.path, "utf8"), {
1966
+ const cfg = loadZooidConfig(readFileSync7(found.path, "utf8"), {
1928
1967
  configDir: dirname8(found.path)
1929
1968
  });
1930
1969
  const matrixEntry = Object.entries(cfg.transports).find(
@@ -1955,7 +1994,7 @@ async function runStatus(flags) {
1955
1994
  if (port === void 0) {
1956
1995
  const found = findConfigFile(cwd);
1957
1996
  if (found) {
1958
- const cfg = loadZooidConfig(readFileSync6(found.path, "utf8"), {
1997
+ const cfg = loadZooidConfig(readFileSync7(found.path, "utf8"), {
1959
1998
  configDir: dirname8(found.path)
1960
1999
  });
1961
2000
  const matrix = findMatrixTransport(cfg);
@@ -1992,7 +2031,7 @@ cli.command("start", "Run the daemon (production entry-point)").option("--data <
1992
2031
  });
1993
2032
  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(
1994
2033
  "--watch-web [path]",
1995
- "Run vite build --watch on @zooid/zoon-web. Path defaults to sibling ../zoon/packages/web."
2034
+ "Run vite build --watch on @zooid/web. Path defaults to sibling ../zooid-clients/packages/web."
1996
2035
  ).action(async (flags) => {
1997
2036
  await runDev({
1998
2037
  dataDir: flags.data,