zooid 0.9.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.
package/dist/bin.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  renderRegistration,
17
17
  startDaemonSocketServer,
18
18
  startWorkforcePublisher
19
- } from "./chunk-LOILUENL.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,
@@ -561,6 +607,7 @@ async function startDaemon(opts = {}) {
561
607
  }
562
608
  const serverName = matrix.transport.user_namespace.split(":").slice(1).join(":").replace(/\\?\)?$/, "") || new URL(matrix.transport.homeserver).hostname;
563
609
  const asUserId = `@${matrix.transport.sender_localpart}:${serverName}`;
610
+ const nameByUserId = new Map(bindings.map((b) => [b.userId, b.name]));
564
611
  const transport = createMatrixTransport({
565
612
  agents: registry,
566
613
  approvals,
@@ -569,11 +616,24 @@ async function startDaemon(opts = {}) {
569
616
  hsToken: matrix.transport.hs_token,
570
617
  adminUserId: opts.adminUserId,
571
618
  botUserId: asUserId,
572
- media: mediaClient
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
+ }
573
629
  });
574
- const requestedPort = matrix.transport.port ?? 9e3;
575
- server = serve({ fetch: transport.app.fetch, port: requestedPort, hostname: "0.0.0.0" });
576
- port = await listenAsync(server);
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
+ }
577
637
  const spaceLocalpart = matrix.transport.space ?? "dev";
578
638
  const adminUserIds = opts.adminUserId ? [opts.adminUserId] : [];
579
639
  let spaceRoomId;
@@ -592,6 +652,11 @@ async function startDaemon(opts = {}) {
592
652
  console.warn("[matrix] workforce space provisioning failed:", err);
593
653
  }
594
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
+ }
595
660
  if (spaceRoomId) {
596
661
  try {
597
662
  const generalRoomId = await ensureDefaultChannel({
@@ -630,6 +695,10 @@ async function startDaemon(opts = {}) {
630
695
  const stop = async () => {
631
696
  if (stopped) return whenStopped;
632
697
  stopped = true;
698
+ try {
699
+ if (syncLoops) for (const loop of syncLoops) loop.stop();
700
+ } catch {
701
+ }
633
702
  try {
634
703
  if (server) await closeAsync(server);
635
704
  } catch {
@@ -690,6 +759,7 @@ var TuwunelService = class {
690
759
  constructor(opts) {
691
760
  this.opts = opts;
692
761
  }
762
+ opts;
693
763
  child = null;
694
764
  start() {
695
765
  this.child = spawn(this.opts.engine, buildRunArgs(this.opts), {
@@ -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
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,10 +915,10 @@ 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(
@@ -862,16 +932,16 @@ Set ${ENV_OVERRIDE} to a built dist, or run from the monorepo.`
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"));
944
+ const pkg = JSON.parse(readFileSync4(join8(cliRoot, "package.json"), "utf8"));
875
945
  return pkg.zooid?.webVersion;
876
946
  } catch {
877
947
  return void 0;
@@ -879,8 +949,8 @@ function readZoonWebPin(cliRoot) {
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
  }
@@ -1081,6 +1151,7 @@ var JsonlSink = class {
1081
1151
  this.stream = createWriteStream(this.path, { flags: "a" });
1082
1152
  })();
1083
1153
  }
1154
+ path;
1084
1155
  stream = null;
1085
1156
  maxStringLen;
1086
1157
  readyPromise;
@@ -1209,7 +1280,7 @@ var CLI_ROOT = (() => {
1209
1280
  const pkgPath = resolve3(dir, "package.json");
1210
1281
  if (existsSync5(pkgPath)) {
1211
1282
  try {
1212
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
1283
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
1213
1284
  if (pkg.name === "zooid" || pkg.name === "@zooid/cli") return dir;
1214
1285
  } catch {
1215
1286
  }
@@ -1232,7 +1303,7 @@ async function runDev(flags) {
1232
1303
  const tokens = ensureTokens(paths.envPath);
1233
1304
  process.env.MATRIX_AS_TOKEN = tokens.asToken;
1234
1305
  process.env.MATRIX_HS_TOKEN = tokens.hsToken;
1235
- const rawYaml = readFileSync5(found.path, "utf8");
1306
+ const rawYaml = readFileSync6(found.path, "utf8");
1236
1307
  const preview = loadZooidConfig(rawYaml, { configDir: dirname6(found.path) });
1237
1308
  const matrix = findMatrixTransport(preview);
1238
1309
  if (!matrix) {
@@ -1345,7 +1416,7 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
1345
1416
  task: async (_, t) => {
1346
1417
  const webRoot = ctx.webWatch?.distPath ?? await ensureWebRoot({
1347
1418
  cliRoot: CLI_ROOT,
1348
- cacheDir: join10(layout.dataRoot, "web"),
1419
+ cacheDir: join12(layout.dataRoot, "web"),
1349
1420
  version: readZoonWebPin(CLI_ROOT),
1350
1421
  onProgress: (msg) => {
1351
1422
  t.output = msg;
@@ -1396,7 +1467,7 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
1396
1467
  [
1397
1468
  "",
1398
1469
  chalk.bold("Tuwunel is up.") + ` ${homeserver}`,
1399
- chalk.bold("UI: ") + ` http://localhost:${flags.uiPort}`,
1470
+ chalk.bold("UI: ") + ` http://localhost:${ctx.uiServer?.address()?.port ?? flags.uiPort}`,
1400
1471
  ` ${chalk.cyan("admin user:")} ${flags.adminUser} / ${flags.adminPassword}`,
1401
1472
  ` ${chalk.cyan("data dir:")} ${layout.dataRoot}`,
1402
1473
  ...ctx.webWatch ? [` ${chalk.cyan("web watcher:")} live (vite build --watch on @zooid/web)`] : [],
@@ -1423,15 +1494,21 @@ function loadEnvFiles(cwd) {
1423
1494
  }
1424
1495
 
1425
1496
  // src/commands/init.ts
1426
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
1427
- 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";
1428
1499
 
1429
1500
  // src/commands/init/generators.ts
1430
1501
  function generateZooidYaml(opts) {
1431
- const acpBlock = opts.preset === "opencode" ? ` acp: { preset: opencode }` : ` acp:
1502
+ const acpBlock = opts.preset === "opencode" || !opts.model ? ` acp: { preset: ${opts.preset} }` : ` acp:
1432
1503
  preset: ${opts.preset}
1433
1504
  model: ${opts.model}`;
1434
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.
1435
1512
 
1436
1513
  transports:
1437
1514
  matrix:
@@ -1488,23 +1565,20 @@ function generateOpencodeJson(opts) {
1488
1565
  return JSON.stringify(
1489
1566
  {
1490
1567
  $schema: "https://opencode.ai/config.json",
1491
- model: "TODO/your-model",
1492
1568
  provider: { TODO: { options: {} } }
1493
1569
  },
1494
1570
  null,
1495
1571
  2
1496
1572
  ) + "\n";
1497
1573
  }
1498
- const cfg = {
1499
- $schema: "https://opencode.ai/config.json",
1500
- model: `${opts.provider}/${opts.model}`,
1501
- provider: {
1502
- [opts.provider]: {
1503
- options: { apiKey: `{env:${opts.apiKeyEnvVar}}` }
1504
- }
1505
- },
1506
- 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
+ }
1507
1580
  };
1581
+ cfg.permission = { webfetch: "allow" };
1508
1582
  return JSON.stringify(cfg, null, 2) + "\n";
1509
1583
  }
1510
1584
  function generateEnv(opts) {
@@ -1524,7 +1598,6 @@ provider-specific options and per-tool permissions.
1524
1598
  var SIMPLE_PRESETS = [
1525
1599
  {
1526
1600
  preset: "claude",
1527
- models: ["claude-sonnet-4-6", "claude-opus-4-7", "claude-haiku-4-5"],
1528
1601
  subscriptionLabel: "Claude subscription (Pro / Max / Team / Enterprise)",
1529
1602
  apiKeyEnvVar: "ANTHROPIC_API_KEY",
1530
1603
  apiKeyPromptLabel: "Anthropic",
@@ -1532,7 +1605,6 @@ var SIMPLE_PRESETS = [
1532
1605
  },
1533
1606
  {
1534
1607
  preset: "codex",
1535
- models: ["gpt-5.5"],
1536
1608
  subscriptionLabel: "ChatGPT subscription (Plus / Pro / Team)",
1537
1609
  apiKeyEnvVar: "OPENAI_API_KEY",
1538
1610
  apiKeyPromptLabel: "OpenAI",
@@ -1544,28 +1616,24 @@ var OPENCODE_PROVIDERS = [
1544
1616
  id: "opencode-go",
1545
1617
  label: "opencode-go",
1546
1618
  description: "$10/mo subscription \u2014 Kimi, GLM, MiniMax",
1547
- models: ["kimi-k2.6", "kimi-k2.5", "glm-5.1", "glm-5", "minimax-m2.7"],
1548
1619
  apiKeyEnvVar: "OPENCODE_API_KEY"
1549
1620
  },
1550
1621
  {
1551
1622
  id: "opencode",
1552
1623
  label: "opencode (Zen)",
1553
1624
  description: "free tier \u2014 curated models",
1554
- models: ["kimi-k2.5-free", "glm-4.7-free", "minimax-m2.1-free"],
1555
1625
  apiKeyEnvVar: "OPENCODE_API_KEY"
1556
1626
  },
1557
1627
  {
1558
1628
  id: "anthropic",
1559
1629
  label: "Anthropic",
1560
1630
  description: "Claude via direct Anthropic API",
1561
- models: ["claude-sonnet-4-6", "claude-opus-4-7", "claude-haiku-4-5"],
1562
1631
  apiKeyEnvVar: "ANTHROPIC_API_KEY"
1563
1632
  },
1564
1633
  {
1565
1634
  id: "openrouter",
1566
1635
  label: "OpenRouter",
1567
1636
  description: "many providers via OpenRouter",
1568
- models: ["anthropic/claude-sonnet-4-6", "zhipuai/glm-5", "moonshot/kimi-k2.6"],
1569
1637
  apiKeyEnvVar: "OPENROUTER_API_KEY"
1570
1638
  }
1571
1639
  ];
@@ -1579,11 +1647,11 @@ function findOpencodeProvider(id) {
1579
1647
  // src/commands/init/sniff.ts
1580
1648
  import { existsSync as existsSync6 } from "fs";
1581
1649
  import { homedir } from "os";
1582
- import { join as join11 } from "path";
1650
+ import { join as join13 } from "path";
1583
1651
  function sniffCredentials(preset, home = homedir()) {
1584
1652
  const meta = findSimplePreset(preset);
1585
1653
  if (!meta) return { found: false };
1586
- const full = join11(home, meta.credentialDir);
1654
+ const full = join13(home, meta.credentialDir);
1587
1655
  if (existsSync6(full)) return { found: true, path: full };
1588
1656
  return { found: false };
1589
1657
  }
@@ -1602,7 +1670,7 @@ var IGNORED_PREEXISTING = /* @__PURE__ */ new Set([
1602
1670
  ]);
1603
1671
  async function runInit(opts) {
1604
1672
  const dir = resolve4(opts.dir);
1605
- mkdirSync4(dir, { recursive: true });
1673
+ mkdirSync5(dir, { recursive: true });
1606
1674
  if (!opts.force) {
1607
1675
  const blocking = readdirSync2(dir).filter((n) => !IGNORED_PREEXISTING.has(n));
1608
1676
  if (blocking.length > 0) {
@@ -1613,7 +1681,6 @@ async function runInit(opts) {
1613
1681
  }
1614
1682
  const writes = [];
1615
1683
  if (opts.preset === "claude" || opts.preset === "codex") {
1616
- if (!opts.model) throw new Error(`--model is required for preset ${opts.preset}`);
1617
1684
  if (!opts.auth) throw new Error("--auth (subscription|api-key) is required for claude/codex");
1618
1685
  const meta = findSimplePreset(opts.preset);
1619
1686
  writes.push({
@@ -1643,7 +1710,6 @@ async function runInit(opts) {
1643
1710
  const isCustom = opts.provider === "custom";
1644
1711
  const providerMeta = isCustom ? void 0 : findOpencodeProvider(opts.provider);
1645
1712
  if (!isCustom && !providerMeta) throw new Error(`unknown opencode provider: ${opts.provider}`);
1646
- if (!isCustom && !opts.model) throw new Error("--model is required (paired with --provider)");
1647
1713
  if (!isCustom && !opts.apiKey) throw new Error("--api-key is required for opencode");
1648
1714
  writes.push({ path: "zooid.yaml", content: generateZooidYaml({ preset: "opencode" }) });
1649
1715
  writes.push({ path: "agents/zooid-assistant/AGENTS.md", content: generateAgentsMd() });
@@ -1671,14 +1737,14 @@ async function runInit(opts) {
1671
1737
  }
1672
1738
  writes.push({ path: ".gitignore", content: generateGitignore() });
1673
1739
  for (const w of writes) {
1674
- const full = join12(dir, w.path);
1740
+ const full = join14(dir, w.path);
1675
1741
  const exists = existsSync7(full);
1676
1742
  if (exists && !opts.overwrite) {
1677
1743
  console.warn(`\u26A0 ${w.path} exists; left as-is (use --force --overwrite to replace)`);
1678
1744
  continue;
1679
1745
  }
1680
- mkdirSync4(dirname7(full), { recursive: true });
1681
- writeFileSync4(full, w.content);
1746
+ mkdirSync5(dirname7(full), { recursive: true });
1747
+ writeFileSync5(full, w.content);
1682
1748
  console.log(`\u2713 Created ${w.path}`);
1683
1749
  }
1684
1750
  if ((opts.preset === "claude" || opts.preset === "codex") && opts.auth === "subscription") {
@@ -1714,19 +1780,7 @@ async function resolveOptions(flags) {
1714
1780
  () => "--preset is required (claude | codex | opencode)"
1715
1781
  );
1716
1782
  if (preset === "opencode") {
1717
- const provider = flags.provider ?? await ask(
1718
- () => select({
1719
- message: "Which provider?",
1720
- choices: [
1721
- ...OPENCODE_PROVIDERS.map((p) => ({
1722
- name: `${p.label} (${p.description})`,
1723
- value: p.id
1724
- })),
1725
- { name: "custom (write your own opencode.json block)", value: "custom" }
1726
- ]
1727
- }),
1728
- () => "--provider is required for opencode"
1729
- );
1783
+ const provider = flags.provider ?? "opencode-go";
1730
1784
  if (provider === "custom") {
1731
1785
  return {
1732
1786
  dir: flags.dir,
@@ -1738,13 +1792,6 @@ async function resolveOptions(flags) {
1738
1792
  }
1739
1793
  const meta2 = findOpencodeProvider(provider);
1740
1794
  if (!meta2) throw new Error(`unknown opencode provider: ${provider}`);
1741
- const model2 = flags.model ?? await ask(
1742
- () => select({
1743
- message: "Which model?",
1744
- choices: meta2.models.map((m) => ({ name: m, value: m }))
1745
- }),
1746
- () => "--model is required"
1747
- );
1748
1795
  const apiKey2 = flags.apiKey ?? await ask(
1749
1796
  () => password({ message: `${meta2.label} API key:` }),
1750
1797
  () => "--api-key is required"
@@ -1753,7 +1800,7 @@ async function resolveOptions(flags) {
1753
1800
  dir: flags.dir,
1754
1801
  preset,
1755
1802
  provider,
1756
- model: model2,
1803
+ model: flags.model,
1757
1804
  apiKey: apiKey2,
1758
1805
  force: flags.force,
1759
1806
  overwrite: flags.overwrite
@@ -1771,13 +1818,6 @@ async function resolveOptions(flags) {
1771
1818
  }),
1772
1819
  () => "--auth is required (subscription | api-key)"
1773
1820
  );
1774
- const model = flags.model ?? await ask(
1775
- () => select({
1776
- message: "Which model?",
1777
- choices: meta.models.map((m) => ({ name: m, value: m }))
1778
- }),
1779
- () => "--model is required"
1780
- );
1781
1821
  const apiKey = auth === "api-key" ? flags.apiKey ?? await ask(
1782
1822
  () => password({ message: `${meta.apiKeyPromptLabel} API key:` }),
1783
1823
  () => "--api-key is required when --auth=api-key"
@@ -1786,7 +1826,7 @@ async function resolveOptions(flags) {
1786
1826
  dir: flags.dir,
1787
1827
  preset,
1788
1828
  auth,
1789
- model,
1829
+ model: flags.model,
1790
1830
  apiKey,
1791
1831
  force: flags.force,
1792
1832
  overwrite: flags.overwrite
@@ -1796,7 +1836,7 @@ async function resolveOptions(flags) {
1796
1836
  // src/commands/logs.ts
1797
1837
  import { readFile, readdir as readdir2, readlink } from "fs/promises";
1798
1838
  import { existsSync as existsSync8 } from "fs";
1799
- import { join as join13 } from "path";
1839
+ import { join as join15 } from "path";
1800
1840
  var KNOWN_SOURCES = ["tuwunel", "daemon", "dev"];
1801
1841
  async function runLogs(flags) {
1802
1842
  const writer = flags.writer ?? ((s) => process.stdout.write(s));
@@ -1815,7 +1855,7 @@ async function runLogs(flags) {
1815
1855
  writer("no logs yet\n");
1816
1856
  return;
1817
1857
  }
1818
- const dayDir = join13(flags.dataDir, "logs", day);
1858
+ const dayDir = join15(flags.dataDir, "logs", day);
1819
1859
  if (!existsSync8(dayDir)) {
1820
1860
  writer(`no logs for ${day}
1821
1861
  `);
@@ -1844,7 +1884,7 @@ async function runLogs(flags) {
1844
1884
  writer(await readFile(path, "utf8"));
1845
1885
  }
1846
1886
  async function resolveTodaySlug(dataDir) {
1847
- const link = join13(dataDir, "logs", "today");
1887
+ const link = join15(dataDir, "logs", "today");
1848
1888
  try {
1849
1889
  return await readlink(link);
1850
1890
  } catch {
@@ -1853,18 +1893,18 @@ async function resolveTodaySlug(dataDir) {
1853
1893
  }
1854
1894
  function resolveSourcePath(dayDir, source) {
1855
1895
  if (source.startsWith("agent-")) {
1856
- if (source.endsWith(".acp")) return join13(dayDir, `${source.slice(0, -4)}.acp.jsonl`);
1857
- 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`);
1858
1898
  }
1859
1899
  if (KNOWN_SOURCES.includes(source))
1860
- return join13(dayDir, `${source}.log`);
1861
- return join13(dayDir, source);
1900
+ return join15(dayDir, `${source}.log`);
1901
+ return join15(dayDir, source);
1862
1902
  }
1863
1903
  async function dumpByTurn(dayDir, turnId, writer) {
1864
1904
  const entries = await readdir2(dayDir);
1865
1905
  const taps = entries.filter((e) => e.endsWith(".acp.jsonl")).sort();
1866
1906
  for (const f of taps) {
1867
- const text = await readFile(join13(dayDir, f), "utf8");
1907
+ const text = await readFile(join15(dayDir, f), "utf8");
1868
1908
  for (const line of text.split("\n")) {
1869
1909
  if (!line) continue;
1870
1910
  try {
@@ -1898,7 +1938,7 @@ async function runStart(flags) {
1898
1938
  }
1899
1939
 
1900
1940
  // src/commands/status.ts
1901
- import { readFileSync as readFileSync6 } from "fs";
1941
+ import { readFileSync as readFileSync7 } from "fs";
1902
1942
  import { dirname as dirname8 } from "path";
1903
1943
  import chalk2 from "chalk";
1904
1944
  async function probe(url, timeoutMs = 2e3) {
@@ -1923,7 +1963,7 @@ async function collectStatus(opts) {
1923
1963
  agents: []
1924
1964
  };
1925
1965
  }
1926
- const cfg = loadZooidConfig(readFileSync6(found.path, "utf8"), {
1966
+ const cfg = loadZooidConfig(readFileSync7(found.path, "utf8"), {
1927
1967
  configDir: dirname8(found.path)
1928
1968
  });
1929
1969
  const matrixEntry = Object.entries(cfg.transports).find(
@@ -1954,7 +1994,7 @@ async function runStatus(flags) {
1954
1994
  if (port === void 0) {
1955
1995
  const found = findConfigFile(cwd);
1956
1996
  if (found) {
1957
- const cfg = loadZooidConfig(readFileSync6(found.path, "utf8"), {
1997
+ const cfg = loadZooidConfig(readFileSync7(found.path, "utf8"), {
1958
1998
  configDir: dirname8(found.path)
1959
1999
  });
1960
2000
  const matrix = findMatrixTransport(cfg);