zooid 0.13.0 → 0.14.0

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,28 +5,31 @@ import {
5
5
  MediaClient,
6
6
  SpawnRegistry,
7
7
  buildAcpRegistry,
8
+ contextEligibleAgents,
8
9
  createMatrixTransport,
9
10
  ensureDefaultChannel,
10
11
  ensureWorkforceSpace,
12
+ evaluateMatch,
11
13
  findConfigFile,
12
14
  findHttpTransport,
13
15
  findMatrixTransport,
14
16
  loadZooidConfig,
15
17
  mergeCliFlags,
16
18
  renderRegistration,
17
- startDaemonSocketServer,
19
+ renderTemplate,
20
+ startAgentSocketServers,
18
21
  startWorkforcePublisher
19
- } from "./chunk-YZ4IO5MR.js";
22
+ } from "./chunk-R5S26T7B.js";
20
23
 
21
24
  // src/bin.ts
22
- import { resolve as resolve7 } from "path";
25
+ import { resolve as resolve8 } from "path";
23
26
  import { cac } from "cac";
24
27
 
25
28
  // src/commands/dev.ts
26
29
  import chalk from "chalk";
27
30
  import { Listr } from "listr2";
28
- import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
29
- import { dirname as dirname6, join as join13, resolve as resolve3 } from "path";
31
+ import { existsSync as existsSync6, readFileSync as readFileSync9 } from "fs";
32
+ import { dirname as dirname7, join as join15, resolve as resolve4 } from "path";
30
33
  import { fileURLToPath } from "url";
31
34
  import { serve as serve2 } from "@hono/node-server";
32
35
 
@@ -215,10 +218,10 @@ MATRIX_HS_TOKEN=${tokens.hsToken}
215
218
  }
216
219
 
217
220
  // src/daemon/start-daemon.ts
218
- import { readFileSync as readFileSync4 } from "fs";
219
- import { mkdir, unlink } from "fs/promises";
221
+ import { readFileSync as readFileSync6 } from "fs";
222
+ import { mkdir } from "fs/promises";
220
223
  import { tmpdir } from "os";
221
- import { dirname as dirname2, isAbsolute, join as join6, resolve } from "path";
224
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join8, resolve as resolve2 } from "path";
222
225
  import { serve } from "@hono/node-server";
223
226
 
224
227
  // ../transport-http/src/server.ts
@@ -431,6 +434,37 @@ function createApp({
431
434
  return app;
432
435
  }
433
436
 
437
+ // src/pi-extension-install.ts
438
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
439
+ import { dirname as dirname2, isAbsolute, join as join4, resolve } from "path";
440
+ function resolvePiAgentDir(opts) {
441
+ const override = opts.env?.PI_CODING_AGENT_DIR;
442
+ if (override) {
443
+ return {
444
+ dir: isAbsolute(override) ? override : resolve(opts.agentWorkdir, override),
445
+ scope: "project"
446
+ };
447
+ }
448
+ return { dir: join4(opts.daemonHome, ".pi", "agent"), scope: "home" };
449
+ }
450
+ function installPiExtension(opts) {
451
+ if (!opts.createMissing && !existsSync2(opts.agentDir)) {
452
+ return { status: "skipped", reason: "no Pi home" };
453
+ }
454
+ const target = join4(opts.agentDir, "extensions", "zooid-tasks.js");
455
+ mkdirSync3(dirname2(target), { recursive: true });
456
+ const source = readFileSync2(opts.bundlePath);
457
+ if (existsSync2(target) && readFileSync2(target).equals(source)) return { status: "unchanged", target };
458
+ writeFileSync3(target, source);
459
+ return { status: "installed", target };
460
+ }
461
+
462
+ // ../pi-extension/src/index.ts
463
+ import { createRequire } from "module";
464
+ function resolvePiExtensionBundle() {
465
+ return createRequire(import.meta.url).resolve("@zooid/pi-extension/bundle");
466
+ }
467
+
434
468
  // src/prepull-images.ts
435
469
  import { execFile } from "child_process";
436
470
  import { promisify } from "util";
@@ -576,22 +610,22 @@ function pushGateway(opts) {
576
610
  }
577
611
 
578
612
  // src/push-gateway/vapid.ts
579
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
580
- import { join as join4 } from "path";
613
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
614
+ import { join as join5 } from "path";
581
615
  import webpush2 from "web-push";
582
616
  var VAPID_FILENAME = "vapid.json";
583
617
  function loadOrCreateVapidKeys(dataDir) {
584
- const path = join4(dataDir, VAPID_FILENAME);
618
+ const path = join5(dataDir, VAPID_FILENAME);
585
619
  try {
586
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
620
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
587
621
  if (typeof parsed.publicKey === "string" && typeof parsed.privateKey === "string")
588
622
  return { publicKey: parsed.publicKey, privateKey: parsed.privateKey };
589
623
  console.warn(`[push] ${path} is malformed; generating a new VAPID keypair.`);
590
624
  } catch {
591
625
  }
592
626
  const keys = webpush2.generateVAPIDKeys();
593
- mkdirSync3(dataDir, { recursive: true });
594
- writeFileSync3(path, JSON.stringify(keys, null, 2), { mode: 384 });
627
+ mkdirSync4(dataDir, { recursive: true });
628
+ writeFileSync4(path, JSON.stringify(keys, null, 2), { mode: 384 });
595
629
  return keys;
596
630
  }
597
631
 
@@ -603,21 +637,53 @@ function mountPushGateway(app, opts) {
603
637
  }
604
638
 
605
639
  // src/daemon/sync-cursors.ts
606
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
607
- import { join as join5 } from "path";
640
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
641
+ import { join as join6 } from "path";
608
642
  function makeSyncCursorStore(agentsDir) {
609
- const fileFor = (agentName) => join5(agentsDir, agentName, "sync-since");
643
+ const fileFor = (agentName) => join6(agentsDir, agentName, "sync-since");
610
644
  return {
611
645
  loadSince(agentName) {
612
646
  try {
613
- return readFileSync3(fileFor(agentName), "utf8").trim() || null;
647
+ return readFileSync4(fileFor(agentName), "utf8").trim() || null;
614
648
  } catch {
615
649
  return null;
616
650
  }
617
651
  },
618
652
  saveSince(agentName, since) {
619
- mkdirSync4(join5(agentsDir, agentName), { recursive: true });
620
- writeFileSync4(fileFor(agentName), since, "utf8");
653
+ mkdirSync5(join6(agentsDir, agentName), { recursive: true });
654
+ writeFileSync5(fileFor(agentName), since, "utf8");
655
+ }
656
+ };
657
+ }
658
+
659
+ // src/daemon/task-journal.ts
660
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, renameSync, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
661
+ import { join as join7 } from "path";
662
+ function makeTaskJournal(dataDir) {
663
+ const path = join7(dataDir, "tasks.json");
664
+ return {
665
+ load() {
666
+ try {
667
+ const value = JSON.parse(readFileSync5(path, "utf8"));
668
+ return value.version === 1 && Array.isArray(value.tasks) ? value.tasks : [];
669
+ } catch (error) {
670
+ if (error.code !== "ENOENT") console.warn("[tasks] journal unavailable; starting empty:", error);
671
+ return [];
672
+ }
673
+ },
674
+ save(tasks) {
675
+ mkdirSync6(dataDir, { recursive: true });
676
+ const temp = `${path}.tmp-${process.pid}`;
677
+ try {
678
+ writeFileSync6(temp, JSON.stringify({ version: 1, tasks }, null, 2), "utf8");
679
+ renameSync(temp, path);
680
+ } catch (error) {
681
+ console.warn("[tasks] journal write failed:", error);
682
+ try {
683
+ unlinkSync(temp);
684
+ } catch {
685
+ }
686
+ }
621
687
  }
622
688
  };
623
689
  }
@@ -627,55 +693,425 @@ function shouldBindHttpListener(mode) {
627
693
  return mode !== "client";
628
694
  }
629
695
 
696
+ // src/daemon/trigger-scheduler.ts
697
+ import { Cron } from "croner";
698
+
699
+ // src/daemon/trigger-runner.ts
700
+ async function fireTrigger(deps) {
701
+ const { name, as, message, agentUserId, resolveRoom, ensureBot, sendMessage } = deps;
702
+ try {
703
+ const roomId = await resolveRoom(message.room);
704
+ if (!roomId) {
705
+ console.warn(`[trigger:${name}] cannot resolve room ${message.room} \u2014 skipping`);
706
+ return;
707
+ }
708
+ await ensureBot(as, roomId);
709
+ await sendMessage({
710
+ roomId,
711
+ asUserId: as,
712
+ content: {
713
+ msgtype: "m.text",
714
+ body: message.text,
715
+ // Structural mention: routes deterministically AND disarms the raw-body
716
+ // fallback in extractMentions, which only fires when nothing matched.
717
+ "m.mentions": { user_ids: [agentUserId] }
718
+ }
719
+ });
720
+ } catch (err) {
721
+ console.warn(`[trigger:${name}] failed:`, err.message);
722
+ }
723
+ }
724
+
725
+ // src/daemon/trigger-scheduler.ts
726
+ function validateCron(name, expr) {
727
+ try {
728
+ new Cron(expr, { paused: true }).stop();
729
+ } catch (err) {
730
+ throw new Error(`triggers.${name}.schedule: ${err.message}`);
731
+ }
732
+ }
733
+ function startTriggerScheduler(deps) {
734
+ const { triggers, agentUserIds, resolveRoom, ensureBot, sendMessage } = deps;
735
+ const jobs = [];
736
+ for (const [name, trigger] of Object.entries(triggers)) {
737
+ if (!trigger.schedule) continue;
738
+ const job = new Cron(trigger.schedule, () => {
739
+ for (const message of trigger.messages) {
740
+ const agentUserId = agentUserIds[message.mention];
741
+ if (!agentUserId) {
742
+ console.warn(`[trigger:${name}] unknown agent "${message.mention}" \u2014 skipping`);
743
+ continue;
744
+ }
745
+ void fireTrigger({ name, as: trigger.as, message, agentUserId, resolveRoom, ensureBot, sendMessage });
746
+ }
747
+ });
748
+ jobs.push(job);
749
+ }
750
+ return {
751
+ async stop() {
752
+ for (const job of jobs) job.stop();
753
+ }
754
+ };
755
+ }
756
+
757
+ // src/daemon/webhook-verify.ts
758
+ import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
759
+ var FRESHNESS_S = 300;
760
+ function safeEqual(a, b) {
761
+ const ab = Buffer.from(a, "utf8");
762
+ const bb = Buffer.from(b, "utf8");
763
+ return ab.length === bb.length && timingSafeEqual2(ab, bb);
764
+ }
765
+ function hmacHex(secret, baseString) {
766
+ return createHmac("sha256", secret).update(baseString).digest("hex");
767
+ }
768
+ function verifyGithub(input) {
769
+ const header = input.headers["x-hub-signature-256"];
770
+ if (!header) return { ok: false };
771
+ const [scheme, sig] = header.split("=");
772
+ if (scheme !== "sha256" || !sig) return { ok: false };
773
+ const expected = hmacHex(input.secret, input.rawBody);
774
+ return safeEqual(sig, expected) ? { ok: true } : { ok: false };
775
+ }
776
+ function verifyStripe(input) {
777
+ const header = input.headers["stripe-signature"];
778
+ if (!header) return { ok: false };
779
+ const parts = Object.fromEntries(
780
+ header.split(",").map((p) => p.split("=", 2)).filter(([, v]) => v !== void 0)
781
+ );
782
+ const ts = parts.t;
783
+ const sig = parts.v1;
784
+ if (!ts || !sig) return { ok: false };
785
+ if (!isFresh(ts)) return { ok: false };
786
+ const expected = hmacHex(input.secret, `${ts}.${input.rawBody}`);
787
+ return safeEqual(sig, expected) ? { ok: true } : { ok: false };
788
+ }
789
+ function verifySlack(input) {
790
+ const header = input.headers["x-slack-signature"];
791
+ const ts = input.headers["x-slack-request-timestamp"];
792
+ if (!header || !ts) return { ok: false };
793
+ if (!header.startsWith("v0=")) return { ok: false };
794
+ const sig = header.slice("v0=".length);
795
+ if (!isFresh(ts)) return { ok: false };
796
+ const expected = hmacHex(input.secret, `v0:${ts}:${input.rawBody}`);
797
+ return safeEqual(sig, expected) ? { ok: true } : { ok: false };
798
+ }
799
+ function verifyStandard(input) {
800
+ const header = input.headers["webhook-signature"];
801
+ const id = input.headers["webhook-id"];
802
+ const ts = input.headers["webhook-timestamp"];
803
+ if (!header || !id || !ts) return { ok: false };
804
+ if (!isFresh(ts)) return { ok: false };
805
+ const candidate = header.split(" ").map((p) => p.startsWith("v1,") ? p.slice("v1,".length) : void 0).find((v) => v !== void 0);
806
+ if (!candidate) return { ok: false };
807
+ const expected = createHmac("sha256", input.secret).update(`${id}.${ts}.${input.rawBody}`).digest("base64");
808
+ return safeEqual(candidate, expected) ? { ok: true } : { ok: false };
809
+ }
810
+ function isFresh(tsRaw) {
811
+ const ts = Number(tsRaw);
812
+ if (!Number.isFinite(ts)) return false;
813
+ const nowS = Date.now() / 1e3;
814
+ return Math.abs(nowS - ts) <= FRESHNESS_S;
815
+ }
816
+ var VERIFIERS = {
817
+ github: verifyGithub,
818
+ stripe: verifyStripe,
819
+ slack: verifySlack,
820
+ standard: verifyStandard
821
+ };
822
+ function verifySignature(provider, input) {
823
+ try {
824
+ return VERIFIERS[provider](input);
825
+ } catch {
826
+ return { ok: false };
827
+ }
828
+ }
829
+ async function verifyCustomSignature(verifier, input) {
830
+ if (typeof verifier !== "function") return { ok: false };
831
+ try {
832
+ const result = await verifier(input);
833
+ if (result === true) return { ok: true };
834
+ if (result === false || result === null || typeof result !== "object") return { ok: false };
835
+ if (result.ok !== true) return { ok: false };
836
+ return typeof result.deliveryId === "string" ? { ok: true, deliveryId: result.deliveryId } : { ok: true };
837
+ } catch {
838
+ return { ok: false };
839
+ }
840
+ }
841
+
842
+ // src/daemon/delivery-cache.ts
843
+ var DeliveryCache = class {
844
+ ttlMs;
845
+ expiryById = /* @__PURE__ */ new Map();
846
+ constructor(ttlMs) {
847
+ this.ttlMs = ttlMs;
848
+ }
849
+ get size() {
850
+ return this.expiryById.size;
851
+ }
852
+ /** Returns true if `id` was already seen (and still within its TTL). */
853
+ seen(id) {
854
+ this.evictExpired();
855
+ const now = Date.now();
856
+ const expiry = this.expiryById.get(id);
857
+ if (expiry !== void 0 && expiry > now) return true;
858
+ this.expiryById.set(id, now + this.ttlMs);
859
+ return false;
860
+ }
861
+ evictExpired() {
862
+ const now = Date.now();
863
+ for (const [id, expiry] of this.expiryById) {
864
+ if (expiry <= now) this.expiryById.delete(id);
865
+ }
866
+ }
867
+ };
868
+
869
+ // src/daemon/webhook-routes.ts
870
+ var MAX_BODY2 = 1e6;
871
+ var MAX_OUTPUT_CHARS = 6e4;
872
+ var TRUNCATION_MARKER = "\n\n\u2026 (truncated)";
873
+ var DELIVERY_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
874
+ var EVENT_HEADER_BY_PROVIDER = {
875
+ github: "x-github-event"
876
+ };
877
+ var DELIVERY_ID_HEADER_BY_PROVIDER = {
878
+ github: "x-github-delivery",
879
+ standard: "webhook-id"
880
+ };
881
+ var RELEVANT_HEADERS = [
882
+ "x-hub-signature-256",
883
+ "x-github-event",
884
+ "x-github-delivery",
885
+ "stripe-signature",
886
+ "x-slack-signature",
887
+ "x-slack-request-timestamp",
888
+ "webhook-signature",
889
+ "webhook-id",
890
+ "webhook-timestamp"
891
+ ];
892
+ function headersOf(c, all) {
893
+ const out = {};
894
+ if (all) {
895
+ c.req.raw.headers.forEach((value, key) => {
896
+ out[key.toLowerCase()] = value;
897
+ });
898
+ return out;
899
+ }
900
+ for (const h of RELEVANT_HEADERS) out[h] = c.req.header(h);
901
+ return out;
902
+ }
903
+ function definedHeaders(headers) {
904
+ const out = {};
905
+ for (const [k, v] of Object.entries(headers)) {
906
+ if (v !== void 0) out[k] = v;
907
+ }
908
+ return out;
909
+ }
910
+ function renderPayload(raw) {
911
+ let pretty;
912
+ try {
913
+ pretty = JSON.stringify(JSON.parse(raw), null, 2);
914
+ } catch {
915
+ pretty = raw;
916
+ }
917
+ if (pretty.length <= MAX_OUTPUT_CHARS) return pretty;
918
+ return pretty.slice(0, MAX_OUTPUT_CHARS) + TRUNCATION_MARKER;
919
+ }
920
+ async function handleDelivery(deps, name, trigger, raw, headers, cache, customDeliveryId) {
921
+ try {
922
+ const webhook = trigger.webhook;
923
+ if (!webhook) return;
924
+ const idHeader = DELIVERY_ID_HEADER_BY_PROVIDER[webhook.provider];
925
+ const deliveryId = customDeliveryId ?? (idHeader ? headers[idHeader] : void 0);
926
+ if (deliveryId !== void 0 && cache.seen(`${name}:${deliveryId}`)) return;
927
+ const eventHeader = EVENT_HEADER_BY_PROVIDER[webhook.provider];
928
+ const event = eventHeader ? headers[eventHeader] : void 0;
929
+ let body;
930
+ try {
931
+ body = JSON.parse(raw);
932
+ } catch {
933
+ body = void 0;
934
+ }
935
+ const ctx = {
936
+ event,
937
+ body,
938
+ headers: definedHeaders(headers),
939
+ output: renderPayload(raw)
940
+ };
941
+ for (const message of trigger.messages) {
942
+ if (message.match !== void 0 && !evaluateMatch(message.match, ctx)) continue;
943
+ const agentUserId = deps.agentUserIds[message.mention];
944
+ if (!agentUserId) {
945
+ console.warn(`[webhook:${name}] unknown agent "${message.mention}" \u2014 skipping`);
946
+ continue;
947
+ }
948
+ await fireTrigger({
949
+ name,
950
+ as: trigger.as,
951
+ message: { ...message, text: renderTemplate(message.text, ctx) },
952
+ agentUserId,
953
+ resolveRoom: deps.resolveRoom,
954
+ ensureBot: deps.ensureBot,
955
+ sendMessage: deps.sendMessage
956
+ });
957
+ }
958
+ } catch (err) {
959
+ console.warn(`[webhook:${name}] failed:`, err.message);
960
+ }
961
+ }
962
+ var WEBHOOK_ROUTE_PREFIX = "/_zooid/webhooks";
963
+ function mountWebhookRoutes(app, deps) {
964
+ const cache = new DeliveryCache(DELIVERY_CACHE_TTL_MS);
965
+ const receive = async (c) => {
966
+ const name = c.req.param("name");
967
+ const trigger = deps.triggers[name];
968
+ const raw = await c.req.text();
969
+ if (raw.length > MAX_BODY2) return c.text("too large", 413);
970
+ if (!trigger?.webhook) return c.text("unauthorized", 401);
971
+ const webhook = trigger.webhook;
972
+ const headers = headersOf(c, webhook.provider === "custom");
973
+ let customDeliveryId;
974
+ if (webhook.provider === "custom") {
975
+ const v = await verifyCustomSignature(deps.customVerifiers?.[name], {
976
+ rawBody: raw,
977
+ headers: definedHeaders(headers),
978
+ secret: webhook.secret
979
+ });
980
+ if (!v.ok) return c.text("unauthorized", 401);
981
+ customDeliveryId = v.deliveryId;
982
+ } else {
983
+ const v = verifySignature(webhook.provider, {
984
+ rawBody: raw,
985
+ headers,
986
+ secret: webhook.secret
987
+ });
988
+ if (!v.ok) return c.text("unauthorized", 401);
989
+ }
990
+ void handleDelivery(deps, name, trigger, raw, headers, cache, customDeliveryId);
991
+ return c.text("accepted", 202);
992
+ };
993
+ app.post(`${WEBHOOK_ROUTE_PREFIX}/:name`, receive);
994
+ }
995
+
996
+ // src/daemon/load-custom-verifiers.ts
997
+ import { pathToFileURL } from "url";
998
+ async function loadCustomVerifiers(triggers) {
999
+ const out = {};
1000
+ for (const [name, trigger] of Object.entries(triggers)) {
1001
+ const path = trigger.webhook?.provider === "custom" ? trigger.webhook.verify : void 0;
1002
+ if (!path) continue;
1003
+ let mod;
1004
+ try {
1005
+ mod = await import(pathToFileURL(path).href);
1006
+ } catch (err) {
1007
+ throw new Error(
1008
+ `triggers.${name}.webhook.verify: cannot load ${path} \u2014 ${err.message}`
1009
+ );
1010
+ }
1011
+ const fn = mod.default ?? mod.verify;
1012
+ if (typeof fn !== "function") {
1013
+ throw new Error(
1014
+ `triggers.${name}.webhook.verify: ${path} must export a function as \`default\` (or as \`verify\`), got ${typeof fn}`
1015
+ );
1016
+ }
1017
+ out[name] = fn;
1018
+ }
1019
+ return out;
1020
+ }
1021
+
1022
+ // src/daemon/trigger-rooms.ts
1023
+ async function joinTriggerRooms(deps) {
1024
+ const { triggers, resolveRoom, ensureBot } = deps;
1025
+ const seen = /* @__PURE__ */ new Set();
1026
+ for (const trigger of Object.values(triggers)) {
1027
+ for (const message of trigger.messages) {
1028
+ const key = `${trigger.as}\0${message.room}`;
1029
+ if (seen.has(key)) continue;
1030
+ seen.add(key);
1031
+ const roomId = await resolveRoom(message.room);
1032
+ if (!roomId) {
1033
+ console.warn(`[trigger] cannot resolve room ${message.room} \u2014 skipping join`);
1034
+ continue;
1035
+ }
1036
+ await ensureBot(trigger.as, roomId);
1037
+ }
1038
+ }
1039
+ }
1040
+
630
1041
  // src/daemon/start-daemon.ts
631
1042
  function listenAsync(server) {
632
- return new Promise((resolve8) => {
1043
+ return new Promise((resolve9) => {
633
1044
  const check = () => {
634
1045
  const addr = server.address();
635
- if (addr && typeof addr === "object") resolve8(addr.port);
1046
+ if (addr && typeof addr === "object") resolve9(addr.port);
636
1047
  else setImmediate(check);
637
1048
  };
638
1049
  check();
639
1050
  });
640
1051
  }
641
1052
  function closeAsync(server) {
642
- return new Promise((resolve8) => {
643
- server.close(() => resolve8());
1053
+ return new Promise((resolve9) => {
1054
+ server.close(() => resolve9());
644
1055
  });
645
1056
  }
1057
+ function localpart(userId) {
1058
+ const m = /^@([^:]+):/.exec(userId);
1059
+ if (!m) throw new Error(`bad user id: ${userId}`);
1060
+ return m[1];
1061
+ }
646
1062
  async function startDaemon(opts = {}) {
647
1063
  const cwd = opts.cwd ?? process.cwd();
648
1064
  const found = opts.configPath ? { path: opts.configPath } : findConfigFile(cwd);
649
1065
  if (!found) throw new Error("zooid.yaml is required");
650
- const configDir = dirname2(found.path);
651
- const base = loadZooidConfig(readFileSync4(found.path, "utf8"), { configDir });
1066
+ const configDir = dirname3(found.path);
1067
+ const base = loadZooidConfig(readFileSync6(found.path, "utf8"), { configDir, validateCron });
652
1068
  const config = mergeCliFlags(base, opts.cliFlags ?? {});
653
1069
  const approvals = new ApprovalCorrelator();
654
- const daemonSockPath = opts.agentsDir ? join6(opts.agentsDir, "..", "run", "context.sock") : join6(tmpdir(), `zooid-context-${process.pid}.sock`);
655
- await mkdir(dirname2(daemonSockPath), { recursive: true }).catch(() => {
1070
+ const runDir = opts.agentsDir ? join8(opts.agentsDir, "..", "run") : join8(tmpdir(), `zooid-context-${process.pid}`);
1071
+ await mkdir(runDir, { recursive: true }).catch(() => {
656
1072
  });
657
1073
  const contextSpawnRegistry = new SpawnRegistry();
658
- let contextSocket = null;
659
- try {
660
- contextSocket = await startDaemonSocketServer({
661
- sockPath: daemonSockPath,
662
- registry: contextSpawnRegistry
663
- });
664
- } catch (err) {
665
- console.warn("[context] daemon socket startup failed; zooid-context MCP disabled:", err);
666
- }
667
- const dataDir = opts.agentsDir ? dirname2(opts.agentsDir) : void 0;
1074
+ const contextSockets = await startAgentSocketServers({
1075
+ runDir,
1076
+ registry: contextSpawnRegistry,
1077
+ agentNames: contextEligibleAgents(config)
1078
+ });
1079
+ const dataDir = opts.agentsDir ? dirname3(opts.agentsDir) : void 0;
668
1080
  const registry = buildAcpRegistry(config, {
669
1081
  approvals,
670
1082
  onTap: opts.onTap,
671
1083
  agentsDir: opts.agentsDir,
672
- contextSpawnRegistry: contextSocket ? contextSpawnRegistry : void 0,
673
- daemonSockPath: contextSocket ? daemonSockPath : void 0,
1084
+ contextSpawnRegistry,
1085
+ daemonSockPaths: contextSockets.paths,
674
1086
  configDir,
675
1087
  dataDir,
676
1088
  daemonHome: process.env.HOME
677
1089
  });
678
1090
  const agentNames = Object.keys(config.agents);
1091
+ const piAgents = agentNames.filter(
1092
+ (name) => config.agents[name].acp?.preset === "pi"
1093
+ );
1094
+ if (piAgents.length > 0) {
1095
+ const bundlePath = resolvePiExtensionBundle();
1096
+ const installed = /* @__PURE__ */ new Set();
1097
+ for (const name of piAgents) {
1098
+ const { dir, scope } = resolvePiAgentDir({
1099
+ agentWorkdir: resolve2(configDir, config.agents[name].workdir),
1100
+ daemonHome: process.env.HOME ?? "",
1101
+ env: process.env
1102
+ });
1103
+ if (installed.has(dir)) continue;
1104
+ installed.add(dir);
1105
+ const result = installPiExtension({
1106
+ agentDir: dir,
1107
+ bundlePath,
1108
+ createMissing: scope === "project"
1109
+ });
1110
+ const where = result.target ? ` extension=${result.target}` : "";
1111
+ const why = result.reason ? ` reason=${result.reason}` : "";
1112
+ console.log(`[pi] agent=${name}${where} status=${result.status}${why}`);
1113
+ }
1114
+ }
679
1115
  if (config.runtime !== "local") {
680
1116
  await prepullImages(registry, {
681
1117
  engine: config.runtime === "podman" ? "podman" : "docker",
@@ -685,11 +1121,13 @@ async function startDaemon(opts = {}) {
685
1121
  log: opts.prepullLog
686
1122
  });
687
1123
  }
688
- console.log(
689
- `[context] socket=${daemonSockPath} status=${contextSocket ? "listening" : "disabled"} agents={${agentNames.map((n) => `${n}:${registry.hasContextSpawn(n) ? "yes" : "no"}`).join(", ")}}`
690
- );
1124
+ console.log(`[context] runDir=${runDir}`);
1125
+ for (const name of agentNames) {
1126
+ console.log(`[context] agent=${name} socket=${contextSockets.paths[name] ?? "(disabled)"}`);
1127
+ }
691
1128
  let server = null;
692
1129
  let syncLoops;
1130
+ let triggers;
693
1131
  let stopped = false;
694
1132
  let resolveStopped;
695
1133
  const whenStopped = new Promise((r) => {
@@ -724,7 +1162,7 @@ async function startDaemon(opts = {}) {
724
1162
  trigger: a.matrix.trigger
725
1163
  };
726
1164
  if (a.matrix.display_name !== void 0) binding.displayName = a.matrix.display_name;
727
- const workspaceDir = isAbsolute(a.workdir) ? a.workdir : resolve(configDir, a.workdir);
1165
+ const workspaceDir = isAbsolute2(a.workdir) ? a.workdir : resolve2(configDir, a.workdir);
728
1166
  binding.workspaceDir = workspaceDir;
729
1167
  binding.agentWorkspacePath = isContainerRuntime ? "/workspace" : workspaceDir;
730
1168
  bindings.push(binding);
@@ -741,6 +1179,7 @@ async function startDaemon(opts = {}) {
741
1179
  adminUserId: opts.adminUserId,
742
1180
  botUserId: asUserId,
743
1181
  media: mediaClient,
1182
+ taskJournal: dataDir ? makeTaskJournal(dataDir) : void 0,
744
1183
  mode,
745
1184
  loadSince: (uid) => {
746
1185
  const name = nameByUserId.get(uid);
@@ -751,6 +1190,21 @@ async function startDaemon(opts = {}) {
751
1190
  if (name && cursors) cursors.saveSince(name, since);
752
1191
  }
753
1192
  });
1193
+ contextSpawnRegistry.setTaskActions(transport.taskActions);
1194
+ let spaceRoomId;
1195
+ const agentUserIds = Object.fromEntries(bindings.map((b) => [b.name, b.userId]));
1196
+ const resolveRoom = async (r) => r.startsWith("!") ? r : await client.resolveAlias(r);
1197
+ const ensureBot = async (mxid, roomId) => {
1198
+ await client.registerBot(localpart(mxid)).catch(() => {
1199
+ });
1200
+ if (spaceRoomId) {
1201
+ await client.invite({ roomId: spaceRoomId, asUserId, targetUserId: mxid }).catch(() => {
1202
+ });
1203
+ await client.joinRoom(spaceRoomId, mxid).catch(() => {
1204
+ });
1205
+ }
1206
+ await client.joinRoom(roomId, mxid);
1207
+ };
754
1208
  if (shouldBindHttpListener(mode)) {
755
1209
  const requestedPort = matrix.transport.port ?? 9e3;
756
1210
  if (dataDir) {
@@ -759,14 +1213,32 @@ async function startDaemon(opts = {}) {
759
1213
  subject: `https://${serverName}`
760
1214
  }).publicKey;
761
1215
  }
762
- server = serve({ fetch: transport.app.fetch, port: requestedPort, hostname: "0.0.0.0" });
1216
+ const webhookTriggers = Object.entries(config.triggers).filter(([, t]) => t.webhook);
1217
+ if (webhookTriggers.length > 0) {
1218
+ const customVerifiers = await loadCustomVerifiers(config.triggers);
1219
+ mountWebhookRoutes(transport.app, {
1220
+ triggers: config.triggers,
1221
+ customVerifiers,
1222
+ agentUserIds,
1223
+ resolveRoom,
1224
+ ensureBot,
1225
+ sendMessage: (m) => client.sendMessage(m)
1226
+ });
1227
+ for (const [name] of webhookTriggers) {
1228
+ console.log(`[webhook] POST ${WEBHOOK_ROUTE_PREFIX}/${name}`);
1229
+ }
1230
+ }
1231
+ server = serve({
1232
+ fetch: transport.app.fetch,
1233
+ port: requestedPort,
1234
+ hostname: "0.0.0.0"
1235
+ });
763
1236
  port = await listenAsync(server);
764
1237
  } else {
765
1238
  port = 0;
766
1239
  }
767
1240
  const spaceLocalpart = matrix.transport.space ?? "dev";
768
1241
  const adminUserIds = opts.adminUserId ? [opts.adminUserId] : [];
769
- let spaceRoomId;
770
1242
  try {
771
1243
  spaceRoomId = await ensureWorkforceSpace({
772
1244
  client,
@@ -777,11 +1249,36 @@ async function startDaemon(opts = {}) {
777
1249
  admins: adminUserIds,
778
1250
  joinRule: opts.publicWorkforceSpace ? "public" : "invite"
779
1251
  });
780
- console.log(`[matrix] ensured workforce space #${spaceLocalpart}:${serverName} \u2192 ${spaceRoomId}`);
1252
+ console.log(
1253
+ `[matrix] ensured workforce space #${spaceLocalpart}:${serverName} \u2192 ${spaceRoomId}`
1254
+ );
781
1255
  } catch (err) {
782
1256
  console.warn("[matrix] workforce space provisioning failed:", err);
783
1257
  }
784
1258
  await transport.bootstrap({ spaceRoomId, asUserId, adminUserIds });
1259
+ if (Object.keys(config.triggers).length > 0) {
1260
+ await joinTriggerRooms({ triggers: config.triggers, resolveRoom, ensureBot });
1261
+ console.log(
1262
+ `[trigger] joined rooms for ${Object.keys(config.triggers).length} trigger(s)`
1263
+ );
1264
+ triggers = startTriggerScheduler({
1265
+ triggers: config.triggers,
1266
+ agentUserIds,
1267
+ resolveRoom,
1268
+ ensureBot,
1269
+ sendMessage: (m) => client.sendMessage(m)
1270
+ });
1271
+ const scheduled = Object.values(config.triggers).filter((t) => t.schedule).length;
1272
+ if (scheduled > 0) console.log(`[trigger] scheduled ${scheduled} trigger(s)`);
1273
+ if (!shouldBindHttpListener(mode)) {
1274
+ const webhooks = Object.values(config.triggers).filter((t) => t.webhook).length;
1275
+ if (webhooks > 0) {
1276
+ console.warn(
1277
+ `[webhook] ${webhooks} webhook trigger(s) configured, but pull mode binds no inbound listener \u2014 these will never fire.`
1278
+ );
1279
+ }
1280
+ }
1281
+ }
785
1282
  syncLoops = transport.syncLoops;
786
1283
  if (syncLoops?.length) {
787
1284
  for (const loop of syncLoops) void loop.run();
@@ -819,12 +1316,20 @@ async function startDaemon(opts = {}) {
819
1316
  const token = process.env.ZOOID_TOKEN;
820
1317
  if (!token) throw new Error("ZOOID_TOKEN is required for http transport");
821
1318
  const app = createApp({ agents: registry, approvals, token });
822
- server = serve({ fetch: app.fetch, port: http.transport.port, hostname: "0.0.0.0" });
1319
+ server = serve({
1320
+ fetch: app.fetch,
1321
+ port: http.transport.port,
1322
+ hostname: "0.0.0.0"
1323
+ });
823
1324
  port = await listenAsync(server);
824
1325
  }
825
1326
  const stop = async () => {
826
1327
  if (stopped) return whenStopped;
827
1328
  stopped = true;
1329
+ try {
1330
+ await triggers?.stop();
1331
+ } catch {
1332
+ }
828
1333
  try {
829
1334
  if (syncLoops) for (const loop of syncLoops) loop.stop();
830
1335
  } catch {
@@ -839,9 +1344,7 @@ async function startDaemon(opts = {}) {
839
1344
  console.error("stopAll:", err);
840
1345
  }
841
1346
  try {
842
- if (contextSocket) await contextSocket.close();
843
- await unlink(daemonSockPath).catch(() => {
844
- });
1347
+ await contextSockets.close();
845
1348
  } catch {
846
1349
  }
847
1350
  resolveStopped();
@@ -901,13 +1404,13 @@ var TuwunelService = class {
901
1404
  if (this.child && this.child.exitCode === null) {
902
1405
  const child = this.child;
903
1406
  child.kill("SIGTERM");
904
- await new Promise((resolve8) => {
1407
+ await new Promise((resolve9) => {
905
1408
  let done = false;
906
1409
  const finish = () => {
907
1410
  if (done) return;
908
1411
  done = true;
909
1412
  clearTimeout(timer);
910
- resolve8();
1413
+ resolve9();
911
1414
  };
912
1415
  const timer = setTimeout(() => {
913
1416
  try {
@@ -933,7 +1436,7 @@ var TuwunelService = class {
933
1436
  `Tuwunel container exited before serving HTTP (exit=${state.exitCode}${state.error ? `, error=${state.error}` : ""}). Check the engine logs (\`${this.opts.engine} logs ${this.opts.name}\`).`
934
1437
  );
935
1438
  }
936
- await new Promise((resolve8) => setTimeout(resolve8, 500));
1439
+ await new Promise((resolve9) => setTimeout(resolve9, 500));
937
1440
  }
938
1441
  while (Date.now() < deadline) {
939
1442
  try {
@@ -941,7 +1444,7 @@ var TuwunelService = class {
941
1444
  if (r.ok) return;
942
1445
  } catch {
943
1446
  }
944
- await new Promise((resolve8) => setTimeout(resolve8, 500));
1447
+ await new Promise((resolve9) => setTimeout(resolve9, 500));
945
1448
  }
946
1449
  const finalState = await this.inspectState().catch(() => null);
947
1450
  const detail = finalState ? ` (container status=${finalState.status}${finalState.exitCode !== void 0 ? `, exit=${finalState.exitCode}` : ""})` : "";
@@ -974,12 +1477,12 @@ var TuwunelService = class {
974
1477
  }
975
1478
  };
976
1479
  function execEngine(engine, args) {
977
- return new Promise((resolve8, reject) => {
1480
+ return new Promise((resolve9, reject) => {
978
1481
  const child = spawn(engine, args, { stdio: "pipe" });
979
1482
  let stderr = "";
980
1483
  child.stderr.on("data", (b) => stderr += String(b));
981
1484
  child.on("exit", (code) => {
982
- if (code === 0) resolve8();
1485
+ if (code === 0) resolve9();
983
1486
  else reject(new Error(`${engine} ${args.join(" ")} failed: ${stderr.trim()}`));
984
1487
  });
985
1488
  child.on("error", reject);
@@ -987,20 +1490,20 @@ function execEngine(engine, args) {
987
1490
  }
988
1491
 
989
1492
  // src/web/resolve.ts
990
- import { existsSync as existsSync3 } from "fs";
991
- import { dirname as dirname3, join as join8, resolve as resolve2 } from "path";
1493
+ import { existsSync as existsSync4 } from "fs";
1494
+ import { dirname as dirname4, join as join10, resolve as resolve3 } from "path";
992
1495
 
993
1496
  // src/web/fetch.ts
994
- import { mkdirSync as mkdirSync5, renameSync, rmSync, existsSync as existsSync2, readdirSync, writeFileSync as writeFileSync5 } from "fs";
995
- import { join as join7 } from "path";
1497
+ import { mkdirSync as mkdirSync7, renameSync as renameSync2, rmSync, existsSync as existsSync3, readdirSync, writeFileSync as writeFileSync7 } from "fs";
1498
+ import { join as join9 } from "path";
996
1499
  import { createHash, randomUUID as randomUUID2 } from "crypto";
997
1500
  import * as tar from "tar";
998
1501
  var PKG = "@zooid/web";
999
1502
  var DEFAULT_REGISTRY = "https://registry.npmjs.org";
1000
1503
  async function fetchWebBundle(opts) {
1001
- const target = join7(opts.cacheDir, opts.version);
1002
- if (existsSync2(join7(target, "index.html"))) return target;
1003
- if (existsSync2(target)) rmSync(target, { recursive: true, force: true });
1504
+ const target = join9(opts.cacheDir, opts.version);
1505
+ if (existsSync3(join9(target, "index.html"))) return target;
1506
+ if (existsSync3(target)) rmSync(target, { recursive: true, force: true });
1004
1507
  const f = opts.fetch ?? globalThis.fetch;
1005
1508
  const registry = (opts.registryUrl ?? DEFAULT_REGISTRY).replace(/\/$/, "");
1006
1509
  let tgz;
@@ -1027,11 +1530,11 @@ async function fetchWebBundle(opts) {
1027
1530
  Cause: ${err instanceof Error ? err.message : String(err)}`
1028
1531
  );
1029
1532
  }
1030
- const tmp = join7(opts.cacheDir, `.tmp-${randomUUID2().slice(0, 8)}`);
1031
- mkdirSync5(tmp, { recursive: true });
1032
- const tgzPath = join7(tmp, ".bundle.tgz");
1533
+ const tmp = join9(opts.cacheDir, `.tmp-${randomUUID2().slice(0, 8)}`);
1534
+ mkdirSync7(tmp, { recursive: true });
1535
+ const tgzPath = join9(tmp, ".bundle.tgz");
1033
1536
  try {
1034
- writeFileSync5(tgzPath, tgz);
1537
+ writeFileSync7(tgzPath, tgz);
1035
1538
  await tar.extract({
1036
1539
  file: tgzPath,
1037
1540
  cwd: tmp,
@@ -1040,17 +1543,17 @@ async function fetchWebBundle(opts) {
1040
1543
  filter: (p) => p === "package/dist" || p.startsWith("package/dist/")
1041
1544
  });
1042
1545
  rmSync(tgzPath);
1043
- if (!existsSync2(join7(tmp, "index.html"))) {
1546
+ if (!existsSync3(join9(tmp, "index.html"))) {
1044
1547
  throw new Error(`${PKG}@${opts.version} tarball has no dist/index.html`);
1045
1548
  }
1046
- renameSync(tmp, target);
1549
+ renameSync2(tmp, target);
1047
1550
  } catch (err) {
1048
1551
  rmSync(tmp, { recursive: true, force: true });
1049
1552
  throw err;
1050
1553
  }
1051
1554
  for (const entry of readdirSync(opts.cacheDir)) {
1052
1555
  if (entry !== opts.version) {
1053
- rmSync(join7(opts.cacheDir, entry), { recursive: true, force: true });
1556
+ rmSync(join9(opts.cacheDir, entry), { recursive: true, force: true });
1054
1557
  }
1055
1558
  }
1056
1559
  return target;
@@ -1060,10 +1563,10 @@ async function fetchWebBundle(opts) {
1060
1563
  var ENV_OVERRIDE = "ZOOID_DEV_WEB_ROOT_OVERRIDE";
1061
1564
  async function ensureWebRoot(opts) {
1062
1565
  const override = process.env[ENV_OVERRIDE];
1063
- if (override && existsSync3(join8(override, "index.html"))) return resolve2(override);
1566
+ if (override && existsSync4(join10(override, "index.html"))) return resolve3(override);
1064
1567
  const fromSource = webSourcePackage(opts.cliRoot);
1065
- if (fromSource && existsSync3(join8(fromSource, "dist", "index.html"))) {
1066
- return join8(fromSource, "dist");
1568
+ if (fromSource && existsSync4(join10(fromSource, "dist", "index.html"))) {
1569
+ return join10(fromSource, "dist");
1067
1570
  }
1068
1571
  if (!opts.version) {
1069
1572
  throw new Error(
@@ -1076,17 +1579,17 @@ Set ${ENV_OVERRIDE} to a built dist, or run from the monorepo.`
1076
1579
  return fetchBundleFn({ version: opts.version, cacheDir: opts.cacheDir });
1077
1580
  }
1078
1581
  function webSourcePackage(cliRoot) {
1079
- const workspaceRoot = dirname3(dirname3(dirname3(cliRoot)));
1080
- const candidate = join8(workspaceRoot, "zooid-clients", "packages", "web");
1081
- return existsSync3(join8(candidate, "package.json")) ? candidate : null;
1582
+ const workspaceRoot = dirname4(dirname4(dirname4(cliRoot)));
1583
+ const candidate = join10(workspaceRoot, "zooid-clients", "packages", "web");
1584
+ return existsSync4(join10(candidate, "package.json")) ? candidate : null;
1082
1585
  }
1083
1586
 
1084
1587
  // src/web/pin.ts
1085
- import { readFileSync as readFileSync5 } from "fs";
1086
- import { join as join9 } from "path";
1588
+ import { readFileSync as readFileSync7 } from "fs";
1589
+ import { join as join11 } from "path";
1087
1590
  function readZoonWebPin(cliRoot) {
1088
1591
  try {
1089
- const pkg = JSON.parse(readFileSync5(join9(cliRoot, "package.json"), "utf8"));
1592
+ const pkg = JSON.parse(readFileSync7(join11(cliRoot, "package.json"), "utf8"));
1090
1593
  return pkg.zooid?.webVersion;
1091
1594
  } catch {
1092
1595
  return void 0;
@@ -1094,8 +1597,8 @@ function readZoonWebPin(cliRoot) {
1094
1597
  }
1095
1598
 
1096
1599
  // src/web/static.ts
1097
- import { readFileSync as readFileSync6, statSync } from "fs";
1098
- import { extname, join as join10, normalize } from "path";
1600
+ import { readFileSync as readFileSync8, statSync } from "fs";
1601
+ import { extname, join as join12, normalize } from "path";
1099
1602
  import { Hono as Hono3 } from "hono";
1100
1603
  var MIME = {
1101
1604
  ".html": "text/html; charset=utf-8",
@@ -1128,19 +1631,19 @@ function webStatic(opts) {
1128
1631
  const requested = decodeURIComponent(url.pathname);
1129
1632
  const wantFile = requested === "/" ? "/index.html" : requested;
1130
1633
  const safe = normalize(wantFile).replace(/^(\.\.[/\\])+/g, "");
1131
- const filePath = join10(opts.webRoot, safe);
1634
+ const filePath = join12(opts.webRoot, safe);
1132
1635
  if (!filePath.startsWith(opts.webRoot)) return c.notFound();
1133
1636
  try {
1134
1637
  const stat = statSync(filePath);
1135
1638
  if (stat.isFile()) {
1136
- const body = readFileSync6(filePath);
1639
+ const body = readFileSync8(filePath);
1137
1640
  const ct = MIME[extname(filePath)] ?? "application/octet-stream";
1138
1641
  return c.body(body, 200, { "content-type": ct });
1139
1642
  }
1140
1643
  } catch {
1141
1644
  }
1142
1645
  if (isAssetPath(requested)) return c.notFound();
1143
- const indexBytes = readFileSync6(join10(opts.webRoot, "index.html"));
1646
+ const indexBytes = readFileSync8(join12(opts.webRoot, "index.html"));
1144
1647
  return c.body(indexBytes, 200, {
1145
1648
  "content-type": MIME[".html"]
1146
1649
  });
@@ -1150,11 +1653,11 @@ function webStatic(opts) {
1150
1653
 
1151
1654
  // src/web/watch.ts
1152
1655
  import { spawn as spawn2 } from "child_process";
1153
- import { existsSync as existsSync4, statSync as statSync2 } from "fs";
1154
- import { join as join11 } from "path";
1656
+ import { existsSync as existsSync5, statSync as statSync2 } from "fs";
1657
+ import { join as join13 } from "path";
1155
1658
  async function startWebWatch(opts) {
1156
- const distPath = join11(opts.webPackageDir, "dist");
1157
- const indexPath = join11(distPath, "index.html");
1659
+ const distPath = join13(opts.webPackageDir, "dist");
1660
+ const indexPath = join13(distPath, "index.html");
1158
1661
  const timeoutMs = opts.firstBuildTimeoutMs ?? 6e4;
1159
1662
  const spawnTime = Date.now();
1160
1663
  const child = spawn2(
@@ -1188,7 +1691,7 @@ async function startWebWatch(opts) {
1188
1691
  attachStdio();
1189
1692
  throw new Error(`vite build --watch exited before first build (code ${child.exitCode})`);
1190
1693
  }
1191
- if (existsSync4(indexPath)) {
1694
+ if (existsSync5(indexPath)) {
1192
1695
  const m = statSync2(indexPath).mtimeMs;
1193
1696
  if (m >= spawnTime - 500) return makeHandle();
1194
1697
  }
@@ -1208,12 +1711,12 @@ async function startWebWatch(opts) {
1208
1711
  async function stopChild(child) {
1209
1712
  if (child.exitCode !== null) return;
1210
1713
  child.kill("SIGTERM");
1211
- await new Promise((resolve8) => {
1714
+ await new Promise((resolve9) => {
1212
1715
  let done = false;
1213
1716
  const finish = () => {
1214
1717
  if (done) return;
1215
1718
  done = true;
1216
- resolve8();
1719
+ resolve9();
1217
1720
  };
1218
1721
  child.once("exit", finish);
1219
1722
  setTimeout(() => {
@@ -1227,8 +1730,8 @@ async function stopChild(child) {
1227
1730
  }
1228
1731
 
1229
1732
  // src/observability/paths.ts
1230
- import { mkdir as mkdir2, readdir, rm, symlink, unlink as unlink2 } from "fs/promises";
1231
- import { join as join12 } from "path";
1733
+ import { mkdir as mkdir2, readdir, rm, symlink, unlink } from "fs/promises";
1734
+ import { join as join14 } from "path";
1232
1735
  function localDateSlug(d) {
1233
1736
  const y = d.getFullYear();
1234
1737
  const m = String(d.getMonth() + 1).padStart(2, "0");
@@ -1238,32 +1741,32 @@ function localDateSlug(d) {
1238
1741
  var DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
1239
1742
  function resolveLogPaths({ dataDir, now }) {
1240
1743
  const slug = localDateSlug(now ?? /* @__PURE__ */ new Date());
1241
- const logsDir = join12(dataDir, "logs");
1242
- const dayDir = join12(logsDir, slug);
1744
+ const logsDir = join14(dataDir, "logs");
1745
+ const dayDir = join14(logsDir, slug);
1243
1746
  return {
1244
1747
  dataDir,
1245
1748
  logsDir,
1246
1749
  dayDir,
1247
1750
  daySlug: slug,
1248
- todayLink: join12(logsDir, "today"),
1249
- tuwunelLog: join12(dayDir, "tuwunel.log"),
1250
- daemonLog: join12(dayDir, "daemon.log"),
1251
- devLog: join12(dayDir, "dev.log"),
1252
- agentLog: (n) => join12(dayDir, `agent-${n}.log`),
1253
- agentTap: (n) => join12(dayDir, `agent-${n}.acp.jsonl`)
1751
+ todayLink: join14(logsDir, "today"),
1752
+ tuwunelLog: join14(dayDir, "tuwunel.log"),
1753
+ daemonLog: join14(dayDir, "daemon.log"),
1754
+ devLog: join14(dayDir, "dev.log"),
1755
+ agentLog: (n) => join14(dayDir, `agent-${n}.log`),
1756
+ agentTap: (n) => join14(dayDir, `agent-${n}.acp.jsonl`)
1254
1757
  };
1255
1758
  }
1256
1759
  async function ensureDayFolder(p) {
1257
1760
  await mkdir2(p.dayDir, { recursive: true });
1258
1761
  try {
1259
- await unlink2(p.todayLink);
1762
+ await unlink(p.todayLink);
1260
1763
  } catch {
1261
1764
  }
1262
1765
  await symlink(p.daySlug, p.todayLink);
1263
1766
  }
1264
1767
  async function pruneOldDays(opts) {
1265
1768
  if (opts.retainDays <= 0) return [];
1266
- const logsDir = join12(opts.dataDir, "logs");
1769
+ const logsDir = join14(opts.dataDir, "logs");
1267
1770
  let entries;
1268
1771
  try {
1269
1772
  entries = await readdir(logsDir);
@@ -1282,7 +1785,7 @@ async function pruneOldDays(opts) {
1282
1785
  const [y, m, d] = name.split("-").map(Number);
1283
1786
  const t = new Date(y, m - 1, d).getTime();
1284
1787
  if (t < cutoff) {
1285
- await rm(join12(logsDir, name), { recursive: true, force: true });
1788
+ await rm(join14(logsDir, name), { recursive: true, force: true });
1286
1789
  removed.push(name);
1287
1790
  }
1288
1791
  }
@@ -1292,14 +1795,14 @@ async function pruneOldDays(opts) {
1292
1795
  // src/observability/file-sink.ts
1293
1796
  import { createWriteStream } from "fs";
1294
1797
  import { mkdir as mkdir3 } from "fs/promises";
1295
- import { dirname as dirname4 } from "path";
1798
+ import { dirname as dirname5 } from "path";
1296
1799
  var TRUNC_MARKER = "\u2026[truncated]";
1297
1800
  var JsonlSink = class {
1298
1801
  constructor(path, opts = {}) {
1299
1802
  this.path = path;
1300
1803
  this.maxStringLen = opts.maxStringLen ?? 4096;
1301
1804
  this.readyPromise = (async () => {
1302
- await mkdir3(dirname4(this.path), { recursive: true });
1805
+ await mkdir3(dirname5(this.path), { recursive: true });
1303
1806
  this.stream = createWriteStream(this.path, { flags: "a" });
1304
1807
  })();
1305
1808
  }
@@ -1311,14 +1814,14 @@ var JsonlSink = class {
1311
1814
  await this.readyPromise;
1312
1815
  const capped = capStrings(obj, this.maxStringLen);
1313
1816
  const line = JSON.stringify(capped) + "\n";
1314
- return new Promise((resolve8, reject) => {
1315
- this.stream.write(line, (err) => err ? reject(err) : resolve8());
1817
+ return new Promise((resolve9, reject) => {
1818
+ this.stream.write(line, (err) => err ? reject(err) : resolve9());
1316
1819
  });
1317
1820
  }
1318
1821
  async close() {
1319
1822
  await this.readyPromise;
1320
- return new Promise((resolve8) => {
1321
- this.stream.end(() => resolve8());
1823
+ return new Promise((resolve9) => {
1824
+ this.stream.end(() => resolve9());
1322
1825
  });
1323
1826
  }
1324
1827
  };
@@ -1386,15 +1889,15 @@ function wireAgentCapture(opts) {
1386
1889
  // src/observability/capture-tuwunel.ts
1387
1890
  import { createWriteStream as createWriteStream2 } from "fs";
1388
1891
  import { mkdir as mkdir4 } from "fs/promises";
1389
- import { dirname as dirname5 } from "path";
1892
+ import { dirname as dirname6 } from "path";
1390
1893
  function captureChildToFile(child, path) {
1391
1894
  return (async () => {
1392
- await mkdir4(dirname5(path), { recursive: true });
1895
+ await mkdir4(dirname6(path), { recursive: true });
1393
1896
  const stream = createWriteStream2(path, { flags: "a" });
1394
1897
  if (child.stdout) child.stdout.on("data", (b) => stream.write(b));
1395
1898
  if (child.stderr) child.stderr.on("data", (b) => stream.write(b));
1396
- await new Promise((resolve8) => {
1397
- child.on("exit", () => stream.end(() => resolve8()));
1899
+ await new Promise((resolve9) => {
1900
+ child.on("exit", () => stream.end(() => resolve9()));
1398
1901
  });
1399
1902
  })();
1400
1903
  }
@@ -1427,17 +1930,17 @@ function buildShutdown(layers) {
1427
1930
 
1428
1931
  // src/commands/dev.ts
1429
1932
  var CLI_ROOT = (() => {
1430
- let dir = dirname6(fileURLToPath(import.meta.url));
1933
+ let dir = dirname7(fileURLToPath(import.meta.url));
1431
1934
  for (let i = 0; i < 8; i++) {
1432
- const pkgPath = resolve3(dir, "package.json");
1433
- if (existsSync5(pkgPath)) {
1935
+ const pkgPath = resolve4(dir, "package.json");
1936
+ if (existsSync6(pkgPath)) {
1434
1937
  try {
1435
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1938
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
1436
1939
  if (pkg.name === "zooid" || pkg.name === "@zooid/cli") return dir;
1437
1940
  } catch {
1438
1941
  }
1439
1942
  }
1440
- const parent = dirname6(dir);
1943
+ const parent = dirname7(dir);
1441
1944
  if (parent === dir) break;
1442
1945
  dir = parent;
1443
1946
  }
@@ -1448,15 +1951,15 @@ async function runDev(flags) {
1448
1951
  const found = findConfigFile(cwd);
1449
1952
  if (!found) throw new Error(`zooid.yaml not found in ${cwd}`);
1450
1953
  loadEnvFiles(cwd);
1451
- const dataRoot = resolve3(cwd, flags.dataDir);
1954
+ const dataRoot = resolve4(cwd, flags.dataDir);
1452
1955
  const layout = resolveDataLayout(dataRoot);
1453
1956
  const paths = resolvePaths(layout.matrixDir);
1454
1957
  const logPaths = resolveLogPaths({ dataDir: layout.dataRoot });
1455
1958
  const tokens = ensureTokens(paths.envPath);
1456
1959
  process.env.MATRIX_AS_TOKEN = tokens.asToken;
1457
1960
  process.env.MATRIX_HS_TOKEN = tokens.hsToken;
1458
- const rawYaml = readFileSync7(found.path, "utf8");
1459
- const preview = loadZooidConfig(rawYaml, { configDir: dirname6(found.path) });
1961
+ const rawYaml = readFileSync9(found.path, "utf8");
1962
+ const preview = loadZooidConfig(rawYaml, { configDir: dirname7(found.path) });
1460
1963
  const matrix = findMatrixTransport(preview);
1461
1964
  if (!matrix) {
1462
1965
  throw new Error("zooid.yaml: zooid dev requires at least one matrix transport");
@@ -1551,9 +2054,9 @@ async function runDev(flags) {
1551
2054
  {
1552
2055
  title: "Start @zooid/web watcher (vite build --watch)",
1553
2056
  task: async () => {
1554
- const pkgDir = typeof flags.watchWeb === "string" ? resolve3(flags.watchWeb) : webSourcePackage(CLI_ROOT);
2057
+ const pkgDir = typeof flags.watchWeb === "string" ? resolve4(flags.watchWeb) : webSourcePackage(CLI_ROOT);
1555
2058
  if (!pkgDir) {
1556
- const defaultPath = dirname6(dirname6(dirname6(CLI_ROOT))) + "/zooid-clients/packages/web";
2059
+ const defaultPath = dirname7(dirname7(dirname7(CLI_ROOT))) + "/zooid-clients/packages/web";
1557
2060
  throw new Error(
1558
2061
  `--watch-web: @zooid/web not found at ${defaultPath}.
1559
2062
  Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
@@ -1568,7 +2071,7 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
1568
2071
  task: async (_, t) => {
1569
2072
  const webRoot = ctx.webWatch?.distPath ?? await ensureWebRoot({
1570
2073
  cliRoot: CLI_ROOT,
1571
- cacheDir: join13(layout.dataRoot, "web"),
2074
+ cacheDir: join15(layout.dataRoot, "web"),
1572
2075
  version: readZoonWebPin(CLI_ROOT),
1573
2076
  onProgress: (msg) => {
1574
2077
  t.output = msg;
@@ -1660,8 +2163,8 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
1660
2163
  }
1661
2164
  function loadEnvFiles(cwd) {
1662
2165
  for (const name of [".env.local", ".env"]) {
1663
- const path = resolve3(cwd, name);
1664
- if (!existsSync5(path)) continue;
2166
+ const path = resolve4(cwd, name);
2167
+ if (!existsSync6(path)) continue;
1665
2168
  try {
1666
2169
  process.loadEnvFile(path);
1667
2170
  } catch (err) {
@@ -1671,9 +2174,9 @@ function loadEnvFiles(cwd) {
1671
2174
  }
1672
2175
 
1673
2176
  // src/commands/init.ts
1674
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync as readdirSync2, symlinkSync, writeFileSync as writeFileSync6 } from "fs";
2177
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync2, symlinkSync, writeFileSync as writeFileSync8 } from "fs";
1675
2178
  import { homedir as homedir2 } from "os";
1676
- import { dirname as dirname7, join as join15, resolve as resolve4 } from "path";
2179
+ import { dirname as dirname8, join as join17, resolve as resolve5 } from "path";
1677
2180
 
1678
2181
  // src/commands/init/generators.ts
1679
2182
  function generateZooidYaml(opts) {
@@ -1854,20 +2357,20 @@ function findPiProvider(id) {
1854
2357
  }
1855
2358
 
1856
2359
  // src/commands/init/sniff.ts
1857
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
2360
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
1858
2361
  import { homedir } from "os";
1859
- import { join as join14 } from "path";
2362
+ import { join as join16 } from "path";
1860
2363
  function sniffCredentials(preset, home = homedir()) {
1861
2364
  const rel = preset === "pi" ? PI_AUTH_FILE : findSimplePreset(preset)?.credentialDir;
1862
2365
  if (!rel) return { found: false };
1863
- const full = join14(home, rel);
1864
- if (existsSync6(full)) return { found: true, path: full };
2366
+ const full = join16(home, rel);
2367
+ if (existsSync7(full)) return { found: true, path: full };
1865
2368
  return { found: false };
1866
2369
  }
1867
2370
  function sniffPiDefaults(home = homedir()) {
1868
- const source = join14(home, PI_SETTINGS_FILE);
2371
+ const source = join16(home, PI_SETTINGS_FILE);
1869
2372
  try {
1870
- const raw = JSON.parse(readFileSync8(source, "utf8"));
2373
+ const raw = JSON.parse(readFileSync10(source, "utf8"));
1871
2374
  const provider = raw.defaultProvider;
1872
2375
  const model = raw.defaultModel;
1873
2376
  if (typeof provider !== "string" || !provider) return void 0;
@@ -1891,8 +2394,8 @@ var IGNORED_PREEXISTING = /* @__PURE__ */ new Set([
1891
2394
  "yarn.lock"
1892
2395
  ]);
1893
2396
  async function runInit(opts) {
1894
- const dir = resolve4(opts.dir);
1895
- mkdirSync6(dir, { recursive: true });
2397
+ const dir = resolve5(opts.dir);
2398
+ mkdirSync8(dir, { recursive: true });
1896
2399
  if (!opts.force) {
1897
2400
  const blocking = readdirSync2(dir).filter((n) => !IGNORED_PREEXISTING.has(n));
1898
2401
  if (blocking.length > 0) {
@@ -1988,14 +2491,14 @@ async function runInit(opts) {
1988
2491
  }
1989
2492
  writes.push({ path: ".gitignore", content: generateGitignore() });
1990
2493
  for (const w of writes) {
1991
- const full = join15(dir, w.path);
1992
- const exists = existsSync7(full);
2494
+ const full = join17(dir, w.path);
2495
+ const exists = existsSync8(full);
1993
2496
  if (exists && !opts.overwrite) {
1994
2497
  console.warn(`\u26A0 ${w.path} exists; left as-is (use --force --overwrite to replace)`);
1995
2498
  continue;
1996
2499
  }
1997
- mkdirSync6(dirname7(full), { recursive: true });
1998
- writeFileSync6(full, w.content);
2500
+ mkdirSync8(dirname8(full), { recursive: true });
2501
+ writeFileSync8(full, w.content);
1999
2502
  console.log(`\u2713 Created ${w.path}`);
2000
2503
  }
2001
2504
  if ((opts.preset === "claude" || opts.preset === "codex") && opts.auth === "subscription") {
@@ -2020,10 +2523,10 @@ async function runInit(opts) {
2020
2523
  }
2021
2524
  const s = sniffCredentials("pi", opts.home);
2022
2525
  if (opts.auth === "subscription" && s.found) {
2023
- const authSource = join15(opts.home ?? homedir2(), PI_AUTH_FILE);
2024
- const linkPath = join15(dir, `agents/zooid-assistant/${PI_AGENT_DIR}/auth.json`);
2025
- if (!existsSync7(linkPath)) {
2026
- mkdirSync6(dirname7(linkPath), { recursive: true });
2526
+ const authSource = join17(opts.home ?? homedir2(), PI_AUTH_FILE);
2527
+ const linkPath = join17(dir, `agents/zooid-assistant/${PI_AGENT_DIR}/auth.json`);
2528
+ if (!existsSync8(linkPath)) {
2529
+ mkdirSync8(dirname8(linkPath), { recursive: true });
2027
2530
  symlinkSync(authSource, linkPath);
2028
2531
  }
2029
2532
  console.log(
@@ -2155,8 +2658,8 @@ async function resolveOptions(flags) {
2155
2658
 
2156
2659
  // src/commands/logs.ts
2157
2660
  import { readFile, readdir as readdir2, readlink } from "fs/promises";
2158
- import { existsSync as existsSync8 } from "fs";
2159
- import { join as join16 } from "path";
2661
+ import { existsSync as existsSync9 } from "fs";
2662
+ import { join as join18 } from "path";
2160
2663
  var KNOWN_SOURCES = ["tuwunel", "daemon", "dev"];
2161
2664
  async function runLogs(flags) {
2162
2665
  const writer = flags.writer ?? ((s) => process.stdout.write(s));
@@ -2175,8 +2678,8 @@ async function runLogs(flags) {
2175
2678
  writer("no logs yet\n");
2176
2679
  return;
2177
2680
  }
2178
- const dayDir = join16(flags.dataDir, "logs", day);
2179
- if (!existsSync8(dayDir)) {
2681
+ const dayDir = join18(flags.dataDir, "logs", day);
2682
+ if (!existsSync9(dayDir)) {
2180
2683
  writer(`no logs for ${day}
2181
2684
  `);
2182
2685
  return;
@@ -2196,7 +2699,7 @@ async function runLogs(flags) {
2196
2699
  return;
2197
2700
  }
2198
2701
  const path = resolveSourcePath(dayDir, flags.source);
2199
- if (!existsSync8(path)) {
2702
+ if (!existsSync9(path)) {
2200
2703
  writer(`no such source: ${flags.source}
2201
2704
  `);
2202
2705
  return;
@@ -2204,7 +2707,7 @@ async function runLogs(flags) {
2204
2707
  writer(await readFile(path, "utf8"));
2205
2708
  }
2206
2709
  async function resolveTodaySlug(dataDir) {
2207
- const link = join16(dataDir, "logs", "today");
2710
+ const link = join18(dataDir, "logs", "today");
2208
2711
  try {
2209
2712
  return await readlink(link);
2210
2713
  } catch {
@@ -2213,18 +2716,18 @@ async function resolveTodaySlug(dataDir) {
2213
2716
  }
2214
2717
  function resolveSourcePath(dayDir, source) {
2215
2718
  if (source.startsWith("agent-")) {
2216
- if (source.endsWith(".acp")) return join16(dayDir, `${source.slice(0, -4)}.acp.jsonl`);
2217
- return join16(dayDir, `${source}.log`);
2719
+ if (source.endsWith(".acp")) return join18(dayDir, `${source.slice(0, -4)}.acp.jsonl`);
2720
+ return join18(dayDir, `${source}.log`);
2218
2721
  }
2219
2722
  if (KNOWN_SOURCES.includes(source))
2220
- return join16(dayDir, `${source}.log`);
2221
- return join16(dayDir, source);
2723
+ return join18(dayDir, `${source}.log`);
2724
+ return join18(dayDir, source);
2222
2725
  }
2223
2726
  async function dumpByTurn(dayDir, turnId, writer) {
2224
2727
  const entries = await readdir2(dayDir);
2225
2728
  const taps = entries.filter((e) => e.endsWith(".acp.jsonl")).sort();
2226
2729
  for (const f of taps) {
2227
- const text = await readFile(join16(dayDir, f), "utf8");
2730
+ const text = await readFile(join18(dayDir, f), "utf8");
2228
2731
  for (const line of text.split("\n")) {
2229
2732
  if (!line) continue;
2230
2733
  try {
@@ -2238,14 +2741,14 @@ async function dumpByTurn(dayDir, turnId, writer) {
2238
2741
 
2239
2742
  // src/commands/start.ts
2240
2743
  import { randomBytes as randomBytes2 } from "crypto";
2241
- import { resolve as resolve5 } from "path";
2744
+ import { resolve as resolve6 } from "path";
2242
2745
  async function runStart(flags) {
2243
2746
  if (flags.printToken) {
2244
2747
  process.stdout.write(`${randomBytes2(32).toString("hex")}
2245
2748
  `);
2246
2749
  return;
2247
2750
  }
2248
- const dataRoot = resolve5(process.cwd(), flags.dataDir ?? "./data");
2751
+ const dataRoot = resolve6(process.cwd(), flags.dataDir ?? "./data");
2249
2752
  const layout = resolveDataLayout(dataRoot);
2250
2753
  const handle = await startDaemon({
2251
2754
  cliFlags: flags,
@@ -2258,12 +2761,12 @@ async function runStart(flags) {
2258
2761
  }
2259
2762
 
2260
2763
  // src/commands/status.ts
2261
- import { readFileSync as readFileSync9 } from "fs";
2262
- import { dirname as dirname8, join as join17, resolve as resolve6 } from "path";
2764
+ import { readFileSync as readFileSync11 } from "fs";
2765
+ import { dirname as dirname9, join as join19, resolve as resolve7 } from "path";
2263
2766
  import chalk2 from "chalk";
2264
2767
  function readVapidPublicKey(dataDir) {
2265
2768
  try {
2266
- const parsed = JSON.parse(readFileSync9(join17(dataDir, VAPID_FILENAME), "utf8"));
2769
+ const parsed = JSON.parse(readFileSync11(join19(dataDir, VAPID_FILENAME), "utf8"));
2267
2770
  return typeof parsed.publicKey === "string" ? parsed.publicKey : void 0;
2268
2771
  } catch {
2269
2772
  return void 0;
@@ -2293,8 +2796,8 @@ async function collectStatus(opts) {
2293
2796
  ...vapidPublicKey ? { vapidPublicKey } : {}
2294
2797
  };
2295
2798
  }
2296
- const cfg = loadZooidConfig(readFileSync9(found.path, "utf8"), {
2297
- configDir: dirname8(found.path)
2799
+ const cfg = loadZooidConfig(readFileSync11(found.path, "utf8"), {
2800
+ configDir: dirname9(found.path)
2298
2801
  });
2299
2802
  const matrixEntry = Object.entries(cfg.transports).find(
2300
2803
  ([, t]) => t.type === "matrix"
@@ -2325,8 +2828,8 @@ async function runStatus(flags) {
2325
2828
  if (port === void 0) {
2326
2829
  const found = findConfigFile(cwd);
2327
2830
  if (found) {
2328
- const cfg = loadZooidConfig(readFileSync9(found.path, "utf8"), {
2329
- configDir: dirname8(found.path)
2831
+ const cfg = loadZooidConfig(readFileSync11(found.path, "utf8"), {
2832
+ configDir: dirname9(found.path)
2330
2833
  });
2331
2834
  const matrix = findMatrixTransport(cfg);
2332
2835
  if (matrix) {
@@ -2336,7 +2839,7 @@ async function runStatus(flags) {
2336
2839
  }
2337
2840
  }
2338
2841
  const tuwunelUrl = `http://localhost:${port ?? 8448}`;
2339
- const dataDir = resolve6(cwd, flags.dataDir ?? "./data");
2842
+ const dataDir = resolve7(cwd, flags.dataDir ?? "./data");
2340
2843
  const s = await collectStatus({ cwd, tuwunelUrl, dataDir });
2341
2844
  const fmt = (st) => st === "up" ? chalk2.green("up") : st === "down" ? chalk2.red("down") : chalk2.yellow("unknown");
2342
2845
  process.stdout.write(
@@ -2353,11 +2856,11 @@ async function runStatus(flags) {
2353
2856
  }
2354
2857
 
2355
2858
  // src/version.ts
2356
- import { readFileSync as readFileSync10 } from "fs";
2859
+ import { readFileSync as readFileSync12 } from "fs";
2357
2860
  function readCliVersion(url = import.meta.url) {
2358
2861
  try {
2359
2862
  const manifest = new URL("../package.json", url);
2360
- const raw = JSON.parse(readFileSync10(manifest, "utf8"));
2863
+ const raw = JSON.parse(readFileSync12(manifest, "utf8"));
2361
2864
  if (typeof raw.version === "string" && raw.version) return raw.version;
2362
2865
  return "unknown";
2363
2866
  } catch {
@@ -2395,14 +2898,14 @@ cli.command(
2395
2898
  ).option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--day <YYYY-MM-DD>", "Day partition (defaults to today)").option("--turn <id>", "Filter ACP taps to a single turn id").option("-f, --follow", "Tail the file (not yet implemented)").option("--keep <n>", "For `logs prune`: days to retain", { default: 14 }).example("$ zooid logs daemon").example("$ zooid logs agent-support.acp --turn 3f9c1a --day 2026-09-06").example("$ zooid logs prune --keep 7").action(async (source, flags) => {
2396
2899
  if (source === "prune") {
2397
2900
  await runLogs({
2398
- dataDir: resolve7(process.cwd(), flags.data),
2901
+ dataDir: resolve8(process.cwd(), flags.data),
2399
2902
  subcommand: "prune",
2400
2903
  keep: Number(flags.keep)
2401
2904
  });
2402
2905
  return;
2403
2906
  }
2404
2907
  await runLogs({
2405
- dataDir: resolve7(process.cwd(), flags.data),
2908
+ dataDir: resolve8(process.cwd(), flags.data),
2406
2909
  source,
2407
2910
  day: flags.day,
2408
2911
  turn: flags.turn,