zooid 0.13.0 → 0.14.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
@@ -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,440 @@ 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_SOURCE_BY_PROVIDER = {
875
+ github: { header: "x-github-event" },
876
+ stripe: { path: ["type"] },
877
+ standard: { path: ["type"] },
878
+ slack: { path: ["event", "type"] }
879
+ };
880
+ function stringAt(body, path) {
881
+ let cursor = body;
882
+ for (const key of path) {
883
+ if (typeof cursor !== "object" || cursor === null) return void 0;
884
+ cursor = cursor[key];
885
+ }
886
+ return typeof cursor === "string" ? cursor : void 0;
887
+ }
888
+ function eventNameFor(provider, headers, body) {
889
+ const source = EVENT_SOURCE_BY_PROVIDER[provider];
890
+ if (!source) return void 0;
891
+ return "header" in source ? headers[source.header] : stringAt(body, source.path);
892
+ }
893
+ var DELIVERY_ID_HEADER_BY_PROVIDER = {
894
+ github: "x-github-delivery",
895
+ standard: "webhook-id"
896
+ };
897
+ var RELEVANT_HEADERS = [
898
+ "x-hub-signature-256",
899
+ "x-github-event",
900
+ "x-github-delivery",
901
+ "stripe-signature",
902
+ "x-slack-signature",
903
+ "x-slack-request-timestamp",
904
+ "webhook-signature",
905
+ "webhook-id",
906
+ "webhook-timestamp"
907
+ ];
908
+ function headersOf(c, all) {
909
+ const out = {};
910
+ if (all) {
911
+ c.req.raw.headers.forEach((value, key) => {
912
+ out[key.toLowerCase()] = value;
913
+ });
914
+ return out;
915
+ }
916
+ for (const h of RELEVANT_HEADERS) out[h] = c.req.header(h);
917
+ return out;
918
+ }
919
+ function definedHeaders(headers) {
920
+ const out = {};
921
+ for (const [k, v] of Object.entries(headers)) {
922
+ if (v !== void 0) out[k] = v;
923
+ }
924
+ return out;
925
+ }
926
+ function renderPayload(raw) {
927
+ let pretty;
928
+ try {
929
+ pretty = JSON.stringify(JSON.parse(raw), null, 2);
930
+ } catch {
931
+ pretty = raw;
932
+ }
933
+ if (pretty.length <= MAX_OUTPUT_CHARS) return pretty;
934
+ return pretty.slice(0, MAX_OUTPUT_CHARS) + TRUNCATION_MARKER;
935
+ }
936
+ async function handleDelivery(deps, name, trigger, raw, headers, cache, customDeliveryId) {
937
+ try {
938
+ const webhook = trigger.webhook;
939
+ if (!webhook) return;
940
+ const idHeader = DELIVERY_ID_HEADER_BY_PROVIDER[webhook.provider];
941
+ const deliveryId = customDeliveryId ?? (idHeader ? headers[idHeader] : void 0);
942
+ if (deliveryId !== void 0 && cache.seen(`${name}:${deliveryId}`)) return;
943
+ let body;
944
+ try {
945
+ body = JSON.parse(raw);
946
+ } catch {
947
+ body = void 0;
948
+ }
949
+ const event = eventNameFor(webhook.provider, headers, body);
950
+ const ctx = {
951
+ event,
952
+ body,
953
+ headers: definedHeaders(headers),
954
+ output: renderPayload(raw)
955
+ };
956
+ for (const message of trigger.messages) {
957
+ if (message.match !== void 0 && !evaluateMatch(message.match, ctx)) continue;
958
+ const agentUserId = deps.agentUserIds[message.mention];
959
+ if (!agentUserId) {
960
+ console.warn(`[webhook:${name}] unknown agent "${message.mention}" \u2014 skipping`);
961
+ continue;
962
+ }
963
+ await fireTrigger({
964
+ name,
965
+ as: trigger.as,
966
+ message: { ...message, text: renderTemplate(message.text, ctx) },
967
+ agentUserId,
968
+ resolveRoom: deps.resolveRoom,
969
+ ensureBot: deps.ensureBot,
970
+ sendMessage: deps.sendMessage
971
+ });
972
+ }
973
+ } catch (err) {
974
+ console.warn(`[webhook:${name}] failed:`, err.message);
975
+ }
976
+ }
977
+ var WEBHOOK_ROUTE_PREFIX = "/_zooid/webhooks";
978
+ function mountWebhookRoutes(app, deps) {
979
+ const cache = new DeliveryCache(DELIVERY_CACHE_TTL_MS);
980
+ const receive = async (c) => {
981
+ const name = c.req.param("name");
982
+ const trigger = deps.triggers[name];
983
+ const raw = await c.req.text();
984
+ if (raw.length > MAX_BODY2) return c.text("too large", 413);
985
+ if (!trigger?.webhook) return c.text("unauthorized", 401);
986
+ const webhook = trigger.webhook;
987
+ const headers = headersOf(c, webhook.provider === "custom");
988
+ let customDeliveryId;
989
+ if (webhook.provider === "custom") {
990
+ const v = await verifyCustomSignature(deps.customVerifiers?.[name], {
991
+ rawBody: raw,
992
+ headers: definedHeaders(headers),
993
+ secret: webhook.secret
994
+ });
995
+ if (!v.ok) return c.text("unauthorized", 401);
996
+ customDeliveryId = v.deliveryId;
997
+ } else {
998
+ const v = verifySignature(webhook.provider, {
999
+ rawBody: raw,
1000
+ headers,
1001
+ secret: webhook.secret
1002
+ });
1003
+ if (!v.ok) return c.text("unauthorized", 401);
1004
+ }
1005
+ void handleDelivery(deps, name, trigger, raw, headers, cache, customDeliveryId);
1006
+ return c.text("accepted", 202);
1007
+ };
1008
+ app.post(`${WEBHOOK_ROUTE_PREFIX}/:name`, receive);
1009
+ }
1010
+
1011
+ // src/daemon/load-custom-verifiers.ts
1012
+ import { pathToFileURL } from "url";
1013
+ async function loadCustomVerifiers(triggers) {
1014
+ const out = {};
1015
+ for (const [name, trigger] of Object.entries(triggers)) {
1016
+ const path = trigger.webhook?.provider === "custom" ? trigger.webhook.verify : void 0;
1017
+ if (!path) continue;
1018
+ let mod;
1019
+ try {
1020
+ mod = await import(pathToFileURL(path).href);
1021
+ } catch (err) {
1022
+ throw new Error(
1023
+ `triggers.${name}.webhook.verify: cannot load ${path} \u2014 ${err.message}`
1024
+ );
1025
+ }
1026
+ const fn = mod.default ?? mod.verify;
1027
+ if (typeof fn !== "function") {
1028
+ throw new Error(
1029
+ `triggers.${name}.webhook.verify: ${path} must export a function as \`default\` (or as \`verify\`), got ${typeof fn}`
1030
+ );
1031
+ }
1032
+ out[name] = fn;
1033
+ }
1034
+ return out;
1035
+ }
1036
+
1037
+ // src/daemon/trigger-rooms.ts
1038
+ async function joinTriggerRooms(deps) {
1039
+ const { triggers, resolveRoom, ensureBot } = deps;
1040
+ const seen = /* @__PURE__ */ new Set();
1041
+ for (const trigger of Object.values(triggers)) {
1042
+ for (const message of trigger.messages) {
1043
+ const key = `${trigger.as}\0${message.room}`;
1044
+ if (seen.has(key)) continue;
1045
+ seen.add(key);
1046
+ const roomId = await resolveRoom(message.room);
1047
+ if (!roomId) {
1048
+ console.warn(`[trigger] cannot resolve room ${message.room} \u2014 skipping join`);
1049
+ continue;
1050
+ }
1051
+ await ensureBot(trigger.as, roomId);
1052
+ }
1053
+ }
1054
+ }
1055
+
630
1056
  // src/daemon/start-daemon.ts
631
1057
  function listenAsync(server) {
632
- return new Promise((resolve8) => {
1058
+ return new Promise((resolve9) => {
633
1059
  const check = () => {
634
1060
  const addr = server.address();
635
- if (addr && typeof addr === "object") resolve8(addr.port);
1061
+ if (addr && typeof addr === "object") resolve9(addr.port);
636
1062
  else setImmediate(check);
637
1063
  };
638
1064
  check();
639
1065
  });
640
1066
  }
641
1067
  function closeAsync(server) {
642
- return new Promise((resolve8) => {
643
- server.close(() => resolve8());
1068
+ return new Promise((resolve9) => {
1069
+ server.close(() => resolve9());
644
1070
  });
645
1071
  }
1072
+ function localpart(userId) {
1073
+ const m = /^@([^:]+):/.exec(userId);
1074
+ if (!m) throw new Error(`bad user id: ${userId}`);
1075
+ return m[1];
1076
+ }
646
1077
  async function startDaemon(opts = {}) {
647
1078
  const cwd = opts.cwd ?? process.cwd();
648
1079
  const found = opts.configPath ? { path: opts.configPath } : findConfigFile(cwd);
649
1080
  if (!found) throw new Error("zooid.yaml is required");
650
- const configDir = dirname2(found.path);
651
- const base = loadZooidConfig(readFileSync4(found.path, "utf8"), { configDir });
1081
+ const configDir = dirname3(found.path);
1082
+ const base = loadZooidConfig(readFileSync6(found.path, "utf8"), { configDir, validateCron });
652
1083
  const config = mergeCliFlags(base, opts.cliFlags ?? {});
653
1084
  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(() => {
1085
+ const runDir = opts.agentsDir ? join8(opts.agentsDir, "..", "run") : join8(tmpdir(), `zooid-context-${process.pid}`);
1086
+ await mkdir(runDir, { recursive: true }).catch(() => {
656
1087
  });
657
1088
  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;
1089
+ const contextSockets = await startAgentSocketServers({
1090
+ runDir,
1091
+ registry: contextSpawnRegistry,
1092
+ agentNames: contextEligibleAgents(config)
1093
+ });
1094
+ const dataDir = opts.agentsDir ? dirname3(opts.agentsDir) : void 0;
668
1095
  const registry = buildAcpRegistry(config, {
669
1096
  approvals,
670
1097
  onTap: opts.onTap,
671
1098
  agentsDir: opts.agentsDir,
672
- contextSpawnRegistry: contextSocket ? contextSpawnRegistry : void 0,
673
- daemonSockPath: contextSocket ? daemonSockPath : void 0,
1099
+ contextSpawnRegistry,
1100
+ daemonSockPaths: contextSockets.paths,
674
1101
  configDir,
675
1102
  dataDir,
676
1103
  daemonHome: process.env.HOME
677
1104
  });
678
1105
  const agentNames = Object.keys(config.agents);
1106
+ const piAgents = agentNames.filter(
1107
+ (name) => config.agents[name].acp?.preset === "pi"
1108
+ );
1109
+ if (piAgents.length > 0) {
1110
+ const bundlePath = resolvePiExtensionBundle();
1111
+ const installed = /* @__PURE__ */ new Set();
1112
+ for (const name of piAgents) {
1113
+ const { dir, scope } = resolvePiAgentDir({
1114
+ agentWorkdir: resolve2(configDir, config.agents[name].workdir),
1115
+ daemonHome: process.env.HOME ?? "",
1116
+ env: process.env
1117
+ });
1118
+ if (installed.has(dir)) continue;
1119
+ installed.add(dir);
1120
+ const result = installPiExtension({
1121
+ agentDir: dir,
1122
+ bundlePath,
1123
+ createMissing: scope === "project"
1124
+ });
1125
+ const where = result.target ? ` extension=${result.target}` : "";
1126
+ const why = result.reason ? ` reason=${result.reason}` : "";
1127
+ console.log(`[pi] agent=${name}${where} status=${result.status}${why}`);
1128
+ }
1129
+ }
679
1130
  if (config.runtime !== "local") {
680
1131
  await prepullImages(registry, {
681
1132
  engine: config.runtime === "podman" ? "podman" : "docker",
@@ -685,11 +1136,13 @@ async function startDaemon(opts = {}) {
685
1136
  log: opts.prepullLog
686
1137
  });
687
1138
  }
688
- console.log(
689
- `[context] socket=${daemonSockPath} status=${contextSocket ? "listening" : "disabled"} agents={${agentNames.map((n) => `${n}:${registry.hasContextSpawn(n) ? "yes" : "no"}`).join(", ")}}`
690
- );
1139
+ console.log(`[context] runDir=${runDir}`);
1140
+ for (const name of agentNames) {
1141
+ console.log(`[context] agent=${name} socket=${contextSockets.paths[name] ?? "(disabled)"}`);
1142
+ }
691
1143
  let server = null;
692
1144
  let syncLoops;
1145
+ let triggers;
693
1146
  let stopped = false;
694
1147
  let resolveStopped;
695
1148
  const whenStopped = new Promise((r) => {
@@ -724,7 +1177,7 @@ async function startDaemon(opts = {}) {
724
1177
  trigger: a.matrix.trigger
725
1178
  };
726
1179
  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);
1180
+ const workspaceDir = isAbsolute2(a.workdir) ? a.workdir : resolve2(configDir, a.workdir);
728
1181
  binding.workspaceDir = workspaceDir;
729
1182
  binding.agentWorkspacePath = isContainerRuntime ? "/workspace" : workspaceDir;
730
1183
  bindings.push(binding);
@@ -741,6 +1194,7 @@ async function startDaemon(opts = {}) {
741
1194
  adminUserId: opts.adminUserId,
742
1195
  botUserId: asUserId,
743
1196
  media: mediaClient,
1197
+ taskJournal: dataDir ? makeTaskJournal(dataDir) : void 0,
744
1198
  mode,
745
1199
  loadSince: (uid) => {
746
1200
  const name = nameByUserId.get(uid);
@@ -751,6 +1205,21 @@ async function startDaemon(opts = {}) {
751
1205
  if (name && cursors) cursors.saveSince(name, since);
752
1206
  }
753
1207
  });
1208
+ contextSpawnRegistry.setTaskActions(transport.taskActions);
1209
+ let spaceRoomId;
1210
+ const agentUserIds = Object.fromEntries(bindings.map((b) => [b.name, b.userId]));
1211
+ const resolveRoom = async (r) => r.startsWith("!") ? r : await client.resolveAlias(r);
1212
+ const ensureBot = async (mxid, roomId) => {
1213
+ await client.registerBot(localpart(mxid)).catch(() => {
1214
+ });
1215
+ if (spaceRoomId) {
1216
+ await client.invite({ roomId: spaceRoomId, asUserId, targetUserId: mxid }).catch(() => {
1217
+ });
1218
+ await client.joinRoom(spaceRoomId, mxid).catch(() => {
1219
+ });
1220
+ }
1221
+ await client.joinRoom(roomId, mxid);
1222
+ };
754
1223
  if (shouldBindHttpListener(mode)) {
755
1224
  const requestedPort = matrix.transport.port ?? 9e3;
756
1225
  if (dataDir) {
@@ -759,14 +1228,32 @@ async function startDaemon(opts = {}) {
759
1228
  subject: `https://${serverName}`
760
1229
  }).publicKey;
761
1230
  }
762
- server = serve({ fetch: transport.app.fetch, port: requestedPort, hostname: "0.0.0.0" });
1231
+ const webhookTriggers = Object.entries(config.triggers).filter(([, t]) => t.webhook);
1232
+ if (webhookTriggers.length > 0) {
1233
+ const customVerifiers = await loadCustomVerifiers(config.triggers);
1234
+ mountWebhookRoutes(transport.app, {
1235
+ triggers: config.triggers,
1236
+ customVerifiers,
1237
+ agentUserIds,
1238
+ resolveRoom,
1239
+ ensureBot,
1240
+ sendMessage: (m) => client.sendMessage(m)
1241
+ });
1242
+ for (const [name] of webhookTriggers) {
1243
+ console.log(`[webhook] POST ${WEBHOOK_ROUTE_PREFIX}/${name}`);
1244
+ }
1245
+ }
1246
+ server = serve({
1247
+ fetch: transport.app.fetch,
1248
+ port: requestedPort,
1249
+ hostname: "0.0.0.0"
1250
+ });
763
1251
  port = await listenAsync(server);
764
1252
  } else {
765
1253
  port = 0;
766
1254
  }
767
1255
  const spaceLocalpart = matrix.transport.space ?? "dev";
768
1256
  const adminUserIds = opts.adminUserId ? [opts.adminUserId] : [];
769
- let spaceRoomId;
770
1257
  try {
771
1258
  spaceRoomId = await ensureWorkforceSpace({
772
1259
  client,
@@ -777,11 +1264,36 @@ async function startDaemon(opts = {}) {
777
1264
  admins: adminUserIds,
778
1265
  joinRule: opts.publicWorkforceSpace ? "public" : "invite"
779
1266
  });
780
- console.log(`[matrix] ensured workforce space #${spaceLocalpart}:${serverName} \u2192 ${spaceRoomId}`);
1267
+ console.log(
1268
+ `[matrix] ensured workforce space #${spaceLocalpart}:${serverName} \u2192 ${spaceRoomId}`
1269
+ );
781
1270
  } catch (err) {
782
1271
  console.warn("[matrix] workforce space provisioning failed:", err);
783
1272
  }
784
1273
  await transport.bootstrap({ spaceRoomId, asUserId, adminUserIds });
1274
+ if (Object.keys(config.triggers).length > 0) {
1275
+ await joinTriggerRooms({ triggers: config.triggers, resolveRoom, ensureBot });
1276
+ console.log(
1277
+ `[trigger] joined rooms for ${Object.keys(config.triggers).length} trigger(s)`
1278
+ );
1279
+ triggers = startTriggerScheduler({
1280
+ triggers: config.triggers,
1281
+ agentUserIds,
1282
+ resolveRoom,
1283
+ ensureBot,
1284
+ sendMessage: (m) => client.sendMessage(m)
1285
+ });
1286
+ const scheduled = Object.values(config.triggers).filter((t) => t.schedule).length;
1287
+ if (scheduled > 0) console.log(`[trigger] scheduled ${scheduled} trigger(s)`);
1288
+ if (!shouldBindHttpListener(mode)) {
1289
+ const webhooks = Object.values(config.triggers).filter((t) => t.webhook).length;
1290
+ if (webhooks > 0) {
1291
+ console.warn(
1292
+ `[webhook] ${webhooks} webhook trigger(s) configured, but pull mode binds no inbound listener \u2014 these will never fire.`
1293
+ );
1294
+ }
1295
+ }
1296
+ }
785
1297
  syncLoops = transport.syncLoops;
786
1298
  if (syncLoops?.length) {
787
1299
  for (const loop of syncLoops) void loop.run();
@@ -819,12 +1331,20 @@ async function startDaemon(opts = {}) {
819
1331
  const token = process.env.ZOOID_TOKEN;
820
1332
  if (!token) throw new Error("ZOOID_TOKEN is required for http transport");
821
1333
  const app = createApp({ agents: registry, approvals, token });
822
- server = serve({ fetch: app.fetch, port: http.transport.port, hostname: "0.0.0.0" });
1334
+ server = serve({
1335
+ fetch: app.fetch,
1336
+ port: http.transport.port,
1337
+ hostname: "0.0.0.0"
1338
+ });
823
1339
  port = await listenAsync(server);
824
1340
  }
825
1341
  const stop = async () => {
826
1342
  if (stopped) return whenStopped;
827
1343
  stopped = true;
1344
+ try {
1345
+ await triggers?.stop();
1346
+ } catch {
1347
+ }
828
1348
  try {
829
1349
  if (syncLoops) for (const loop of syncLoops) loop.stop();
830
1350
  } catch {
@@ -839,9 +1359,7 @@ async function startDaemon(opts = {}) {
839
1359
  console.error("stopAll:", err);
840
1360
  }
841
1361
  try {
842
- if (contextSocket) await contextSocket.close();
843
- await unlink(daemonSockPath).catch(() => {
844
- });
1362
+ await contextSockets.close();
845
1363
  } catch {
846
1364
  }
847
1365
  resolveStopped();
@@ -901,13 +1419,13 @@ var TuwunelService = class {
901
1419
  if (this.child && this.child.exitCode === null) {
902
1420
  const child = this.child;
903
1421
  child.kill("SIGTERM");
904
- await new Promise((resolve8) => {
1422
+ await new Promise((resolve9) => {
905
1423
  let done = false;
906
1424
  const finish = () => {
907
1425
  if (done) return;
908
1426
  done = true;
909
1427
  clearTimeout(timer);
910
- resolve8();
1428
+ resolve9();
911
1429
  };
912
1430
  const timer = setTimeout(() => {
913
1431
  try {
@@ -933,7 +1451,7 @@ var TuwunelService = class {
933
1451
  `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
1452
  );
935
1453
  }
936
- await new Promise((resolve8) => setTimeout(resolve8, 500));
1454
+ await new Promise((resolve9) => setTimeout(resolve9, 500));
937
1455
  }
938
1456
  while (Date.now() < deadline) {
939
1457
  try {
@@ -941,7 +1459,7 @@ var TuwunelService = class {
941
1459
  if (r.ok) return;
942
1460
  } catch {
943
1461
  }
944
- await new Promise((resolve8) => setTimeout(resolve8, 500));
1462
+ await new Promise((resolve9) => setTimeout(resolve9, 500));
945
1463
  }
946
1464
  const finalState = await this.inspectState().catch(() => null);
947
1465
  const detail = finalState ? ` (container status=${finalState.status}${finalState.exitCode !== void 0 ? `, exit=${finalState.exitCode}` : ""})` : "";
@@ -974,12 +1492,12 @@ var TuwunelService = class {
974
1492
  }
975
1493
  };
976
1494
  function execEngine(engine, args) {
977
- return new Promise((resolve8, reject) => {
1495
+ return new Promise((resolve9, reject) => {
978
1496
  const child = spawn(engine, args, { stdio: "pipe" });
979
1497
  let stderr = "";
980
1498
  child.stderr.on("data", (b) => stderr += String(b));
981
1499
  child.on("exit", (code) => {
982
- if (code === 0) resolve8();
1500
+ if (code === 0) resolve9();
983
1501
  else reject(new Error(`${engine} ${args.join(" ")} failed: ${stderr.trim()}`));
984
1502
  });
985
1503
  child.on("error", reject);
@@ -987,20 +1505,20 @@ function execEngine(engine, args) {
987
1505
  }
988
1506
 
989
1507
  // src/web/resolve.ts
990
- import { existsSync as existsSync3 } from "fs";
991
- import { dirname as dirname3, join as join8, resolve as resolve2 } from "path";
1508
+ import { existsSync as existsSync4 } from "fs";
1509
+ import { dirname as dirname4, join as join10, resolve as resolve3 } from "path";
992
1510
 
993
1511
  // 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";
1512
+ import { mkdirSync as mkdirSync7, renameSync as renameSync2, rmSync, existsSync as existsSync3, readdirSync, writeFileSync as writeFileSync7 } from "fs";
1513
+ import { join as join9 } from "path";
996
1514
  import { createHash, randomUUID as randomUUID2 } from "crypto";
997
1515
  import * as tar from "tar";
998
1516
  var PKG = "@zooid/web";
999
1517
  var DEFAULT_REGISTRY = "https://registry.npmjs.org";
1000
1518
  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 });
1519
+ const target = join9(opts.cacheDir, opts.version);
1520
+ if (existsSync3(join9(target, "index.html"))) return target;
1521
+ if (existsSync3(target)) rmSync(target, { recursive: true, force: true });
1004
1522
  const f = opts.fetch ?? globalThis.fetch;
1005
1523
  const registry = (opts.registryUrl ?? DEFAULT_REGISTRY).replace(/\/$/, "");
1006
1524
  let tgz;
@@ -1027,11 +1545,11 @@ async function fetchWebBundle(opts) {
1027
1545
  Cause: ${err instanceof Error ? err.message : String(err)}`
1028
1546
  );
1029
1547
  }
1030
- const tmp = join7(opts.cacheDir, `.tmp-${randomUUID2().slice(0, 8)}`);
1031
- mkdirSync5(tmp, { recursive: true });
1032
- const tgzPath = join7(tmp, ".bundle.tgz");
1548
+ const tmp = join9(opts.cacheDir, `.tmp-${randomUUID2().slice(0, 8)}`);
1549
+ mkdirSync7(tmp, { recursive: true });
1550
+ const tgzPath = join9(tmp, ".bundle.tgz");
1033
1551
  try {
1034
- writeFileSync5(tgzPath, tgz);
1552
+ writeFileSync7(tgzPath, tgz);
1035
1553
  await tar.extract({
1036
1554
  file: tgzPath,
1037
1555
  cwd: tmp,
@@ -1040,17 +1558,17 @@ async function fetchWebBundle(opts) {
1040
1558
  filter: (p) => p === "package/dist" || p.startsWith("package/dist/")
1041
1559
  });
1042
1560
  rmSync(tgzPath);
1043
- if (!existsSync2(join7(tmp, "index.html"))) {
1561
+ if (!existsSync3(join9(tmp, "index.html"))) {
1044
1562
  throw new Error(`${PKG}@${opts.version} tarball has no dist/index.html`);
1045
1563
  }
1046
- renameSync(tmp, target);
1564
+ renameSync2(tmp, target);
1047
1565
  } catch (err) {
1048
1566
  rmSync(tmp, { recursive: true, force: true });
1049
1567
  throw err;
1050
1568
  }
1051
1569
  for (const entry of readdirSync(opts.cacheDir)) {
1052
1570
  if (entry !== opts.version) {
1053
- rmSync(join7(opts.cacheDir, entry), { recursive: true, force: true });
1571
+ rmSync(join9(opts.cacheDir, entry), { recursive: true, force: true });
1054
1572
  }
1055
1573
  }
1056
1574
  return target;
@@ -1060,10 +1578,10 @@ async function fetchWebBundle(opts) {
1060
1578
  var ENV_OVERRIDE = "ZOOID_DEV_WEB_ROOT_OVERRIDE";
1061
1579
  async function ensureWebRoot(opts) {
1062
1580
  const override = process.env[ENV_OVERRIDE];
1063
- if (override && existsSync3(join8(override, "index.html"))) return resolve2(override);
1581
+ if (override && existsSync4(join10(override, "index.html"))) return resolve3(override);
1064
1582
  const fromSource = webSourcePackage(opts.cliRoot);
1065
- if (fromSource && existsSync3(join8(fromSource, "dist", "index.html"))) {
1066
- return join8(fromSource, "dist");
1583
+ if (fromSource && existsSync4(join10(fromSource, "dist", "index.html"))) {
1584
+ return join10(fromSource, "dist");
1067
1585
  }
1068
1586
  if (!opts.version) {
1069
1587
  throw new Error(
@@ -1076,17 +1594,17 @@ Set ${ENV_OVERRIDE} to a built dist, or run from the monorepo.`
1076
1594
  return fetchBundleFn({ version: opts.version, cacheDir: opts.cacheDir });
1077
1595
  }
1078
1596
  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;
1597
+ const workspaceRoot = dirname4(dirname4(dirname4(cliRoot)));
1598
+ const candidate = join10(workspaceRoot, "zooid-clients", "packages", "web");
1599
+ return existsSync4(join10(candidate, "package.json")) ? candidate : null;
1082
1600
  }
1083
1601
 
1084
1602
  // src/web/pin.ts
1085
- import { readFileSync as readFileSync5 } from "fs";
1086
- import { join as join9 } from "path";
1603
+ import { readFileSync as readFileSync7 } from "fs";
1604
+ import { join as join11 } from "path";
1087
1605
  function readZoonWebPin(cliRoot) {
1088
1606
  try {
1089
- const pkg = JSON.parse(readFileSync5(join9(cliRoot, "package.json"), "utf8"));
1607
+ const pkg = JSON.parse(readFileSync7(join11(cliRoot, "package.json"), "utf8"));
1090
1608
  return pkg.zooid?.webVersion;
1091
1609
  } catch {
1092
1610
  return void 0;
@@ -1094,8 +1612,8 @@ function readZoonWebPin(cliRoot) {
1094
1612
  }
1095
1613
 
1096
1614
  // src/web/static.ts
1097
- import { readFileSync as readFileSync6, statSync } from "fs";
1098
- import { extname, join as join10, normalize } from "path";
1615
+ import { readFileSync as readFileSync8, statSync } from "fs";
1616
+ import { extname, join as join12, normalize } from "path";
1099
1617
  import { Hono as Hono3 } from "hono";
1100
1618
  var MIME = {
1101
1619
  ".html": "text/html; charset=utf-8",
@@ -1128,19 +1646,19 @@ function webStatic(opts) {
1128
1646
  const requested = decodeURIComponent(url.pathname);
1129
1647
  const wantFile = requested === "/" ? "/index.html" : requested;
1130
1648
  const safe = normalize(wantFile).replace(/^(\.\.[/\\])+/g, "");
1131
- const filePath = join10(opts.webRoot, safe);
1649
+ const filePath = join12(opts.webRoot, safe);
1132
1650
  if (!filePath.startsWith(opts.webRoot)) return c.notFound();
1133
1651
  try {
1134
1652
  const stat = statSync(filePath);
1135
1653
  if (stat.isFile()) {
1136
- const body = readFileSync6(filePath);
1654
+ const body = readFileSync8(filePath);
1137
1655
  const ct = MIME[extname(filePath)] ?? "application/octet-stream";
1138
1656
  return c.body(body, 200, { "content-type": ct });
1139
1657
  }
1140
1658
  } catch {
1141
1659
  }
1142
1660
  if (isAssetPath(requested)) return c.notFound();
1143
- const indexBytes = readFileSync6(join10(opts.webRoot, "index.html"));
1661
+ const indexBytes = readFileSync8(join12(opts.webRoot, "index.html"));
1144
1662
  return c.body(indexBytes, 200, {
1145
1663
  "content-type": MIME[".html"]
1146
1664
  });
@@ -1150,11 +1668,11 @@ function webStatic(opts) {
1150
1668
 
1151
1669
  // src/web/watch.ts
1152
1670
  import { spawn as spawn2 } from "child_process";
1153
- import { existsSync as existsSync4, statSync as statSync2 } from "fs";
1154
- import { join as join11 } from "path";
1671
+ import { existsSync as existsSync5, statSync as statSync2 } from "fs";
1672
+ import { join as join13 } from "path";
1155
1673
  async function startWebWatch(opts) {
1156
- const distPath = join11(opts.webPackageDir, "dist");
1157
- const indexPath = join11(distPath, "index.html");
1674
+ const distPath = join13(opts.webPackageDir, "dist");
1675
+ const indexPath = join13(distPath, "index.html");
1158
1676
  const timeoutMs = opts.firstBuildTimeoutMs ?? 6e4;
1159
1677
  const spawnTime = Date.now();
1160
1678
  const child = spawn2(
@@ -1188,7 +1706,7 @@ async function startWebWatch(opts) {
1188
1706
  attachStdio();
1189
1707
  throw new Error(`vite build --watch exited before first build (code ${child.exitCode})`);
1190
1708
  }
1191
- if (existsSync4(indexPath)) {
1709
+ if (existsSync5(indexPath)) {
1192
1710
  const m = statSync2(indexPath).mtimeMs;
1193
1711
  if (m >= spawnTime - 500) return makeHandle();
1194
1712
  }
@@ -1208,12 +1726,12 @@ async function startWebWatch(opts) {
1208
1726
  async function stopChild(child) {
1209
1727
  if (child.exitCode !== null) return;
1210
1728
  child.kill("SIGTERM");
1211
- await new Promise((resolve8) => {
1729
+ await new Promise((resolve9) => {
1212
1730
  let done = false;
1213
1731
  const finish = () => {
1214
1732
  if (done) return;
1215
1733
  done = true;
1216
- resolve8();
1734
+ resolve9();
1217
1735
  };
1218
1736
  child.once("exit", finish);
1219
1737
  setTimeout(() => {
@@ -1227,8 +1745,8 @@ async function stopChild(child) {
1227
1745
  }
1228
1746
 
1229
1747
  // 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";
1748
+ import { mkdir as mkdir2, readdir, rm, symlink, unlink } from "fs/promises";
1749
+ import { join as join14 } from "path";
1232
1750
  function localDateSlug(d) {
1233
1751
  const y = d.getFullYear();
1234
1752
  const m = String(d.getMonth() + 1).padStart(2, "0");
@@ -1238,32 +1756,32 @@ function localDateSlug(d) {
1238
1756
  var DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
1239
1757
  function resolveLogPaths({ dataDir, now }) {
1240
1758
  const slug = localDateSlug(now ?? /* @__PURE__ */ new Date());
1241
- const logsDir = join12(dataDir, "logs");
1242
- const dayDir = join12(logsDir, slug);
1759
+ const logsDir = join14(dataDir, "logs");
1760
+ const dayDir = join14(logsDir, slug);
1243
1761
  return {
1244
1762
  dataDir,
1245
1763
  logsDir,
1246
1764
  dayDir,
1247
1765
  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`)
1766
+ todayLink: join14(logsDir, "today"),
1767
+ tuwunelLog: join14(dayDir, "tuwunel.log"),
1768
+ daemonLog: join14(dayDir, "daemon.log"),
1769
+ devLog: join14(dayDir, "dev.log"),
1770
+ agentLog: (n) => join14(dayDir, `agent-${n}.log`),
1771
+ agentTap: (n) => join14(dayDir, `agent-${n}.acp.jsonl`)
1254
1772
  };
1255
1773
  }
1256
1774
  async function ensureDayFolder(p) {
1257
1775
  await mkdir2(p.dayDir, { recursive: true });
1258
1776
  try {
1259
- await unlink2(p.todayLink);
1777
+ await unlink(p.todayLink);
1260
1778
  } catch {
1261
1779
  }
1262
1780
  await symlink(p.daySlug, p.todayLink);
1263
1781
  }
1264
1782
  async function pruneOldDays(opts) {
1265
1783
  if (opts.retainDays <= 0) return [];
1266
- const logsDir = join12(opts.dataDir, "logs");
1784
+ const logsDir = join14(opts.dataDir, "logs");
1267
1785
  let entries;
1268
1786
  try {
1269
1787
  entries = await readdir(logsDir);
@@ -1282,7 +1800,7 @@ async function pruneOldDays(opts) {
1282
1800
  const [y, m, d] = name.split("-").map(Number);
1283
1801
  const t = new Date(y, m - 1, d).getTime();
1284
1802
  if (t < cutoff) {
1285
- await rm(join12(logsDir, name), { recursive: true, force: true });
1803
+ await rm(join14(logsDir, name), { recursive: true, force: true });
1286
1804
  removed.push(name);
1287
1805
  }
1288
1806
  }
@@ -1292,14 +1810,14 @@ async function pruneOldDays(opts) {
1292
1810
  // src/observability/file-sink.ts
1293
1811
  import { createWriteStream } from "fs";
1294
1812
  import { mkdir as mkdir3 } from "fs/promises";
1295
- import { dirname as dirname4 } from "path";
1813
+ import { dirname as dirname5 } from "path";
1296
1814
  var TRUNC_MARKER = "\u2026[truncated]";
1297
1815
  var JsonlSink = class {
1298
1816
  constructor(path, opts = {}) {
1299
1817
  this.path = path;
1300
1818
  this.maxStringLen = opts.maxStringLen ?? 4096;
1301
1819
  this.readyPromise = (async () => {
1302
- await mkdir3(dirname4(this.path), { recursive: true });
1820
+ await mkdir3(dirname5(this.path), { recursive: true });
1303
1821
  this.stream = createWriteStream(this.path, { flags: "a" });
1304
1822
  })();
1305
1823
  }
@@ -1311,14 +1829,14 @@ var JsonlSink = class {
1311
1829
  await this.readyPromise;
1312
1830
  const capped = capStrings(obj, this.maxStringLen);
1313
1831
  const line = JSON.stringify(capped) + "\n";
1314
- return new Promise((resolve8, reject) => {
1315
- this.stream.write(line, (err) => err ? reject(err) : resolve8());
1832
+ return new Promise((resolve9, reject) => {
1833
+ this.stream.write(line, (err) => err ? reject(err) : resolve9());
1316
1834
  });
1317
1835
  }
1318
1836
  async close() {
1319
1837
  await this.readyPromise;
1320
- return new Promise((resolve8) => {
1321
- this.stream.end(() => resolve8());
1838
+ return new Promise((resolve9) => {
1839
+ this.stream.end(() => resolve9());
1322
1840
  });
1323
1841
  }
1324
1842
  };
@@ -1386,15 +1904,15 @@ function wireAgentCapture(opts) {
1386
1904
  // src/observability/capture-tuwunel.ts
1387
1905
  import { createWriteStream as createWriteStream2 } from "fs";
1388
1906
  import { mkdir as mkdir4 } from "fs/promises";
1389
- import { dirname as dirname5 } from "path";
1907
+ import { dirname as dirname6 } from "path";
1390
1908
  function captureChildToFile(child, path) {
1391
1909
  return (async () => {
1392
- await mkdir4(dirname5(path), { recursive: true });
1910
+ await mkdir4(dirname6(path), { recursive: true });
1393
1911
  const stream = createWriteStream2(path, { flags: "a" });
1394
1912
  if (child.stdout) child.stdout.on("data", (b) => stream.write(b));
1395
1913
  if (child.stderr) child.stderr.on("data", (b) => stream.write(b));
1396
- await new Promise((resolve8) => {
1397
- child.on("exit", () => stream.end(() => resolve8()));
1914
+ await new Promise((resolve9) => {
1915
+ child.on("exit", () => stream.end(() => resolve9()));
1398
1916
  });
1399
1917
  })();
1400
1918
  }
@@ -1427,17 +1945,17 @@ function buildShutdown(layers) {
1427
1945
 
1428
1946
  // src/commands/dev.ts
1429
1947
  var CLI_ROOT = (() => {
1430
- let dir = dirname6(fileURLToPath(import.meta.url));
1948
+ let dir = dirname7(fileURLToPath(import.meta.url));
1431
1949
  for (let i = 0; i < 8; i++) {
1432
- const pkgPath = resolve3(dir, "package.json");
1433
- if (existsSync5(pkgPath)) {
1950
+ const pkgPath = resolve4(dir, "package.json");
1951
+ if (existsSync6(pkgPath)) {
1434
1952
  try {
1435
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1953
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
1436
1954
  if (pkg.name === "zooid" || pkg.name === "@zooid/cli") return dir;
1437
1955
  } catch {
1438
1956
  }
1439
1957
  }
1440
- const parent = dirname6(dir);
1958
+ const parent = dirname7(dir);
1441
1959
  if (parent === dir) break;
1442
1960
  dir = parent;
1443
1961
  }
@@ -1448,15 +1966,15 @@ async function runDev(flags) {
1448
1966
  const found = findConfigFile(cwd);
1449
1967
  if (!found) throw new Error(`zooid.yaml not found in ${cwd}`);
1450
1968
  loadEnvFiles(cwd);
1451
- const dataRoot = resolve3(cwd, flags.dataDir);
1969
+ const dataRoot = resolve4(cwd, flags.dataDir);
1452
1970
  const layout = resolveDataLayout(dataRoot);
1453
1971
  const paths = resolvePaths(layout.matrixDir);
1454
1972
  const logPaths = resolveLogPaths({ dataDir: layout.dataRoot });
1455
1973
  const tokens = ensureTokens(paths.envPath);
1456
1974
  process.env.MATRIX_AS_TOKEN = tokens.asToken;
1457
1975
  process.env.MATRIX_HS_TOKEN = tokens.hsToken;
1458
- const rawYaml = readFileSync7(found.path, "utf8");
1459
- const preview = loadZooidConfig(rawYaml, { configDir: dirname6(found.path) });
1976
+ const rawYaml = readFileSync9(found.path, "utf8");
1977
+ const preview = loadZooidConfig(rawYaml, { configDir: dirname7(found.path) });
1460
1978
  const matrix = findMatrixTransport(preview);
1461
1979
  if (!matrix) {
1462
1980
  throw new Error("zooid.yaml: zooid dev requires at least one matrix transport");
@@ -1551,9 +2069,9 @@ async function runDev(flags) {
1551
2069
  {
1552
2070
  title: "Start @zooid/web watcher (vite build --watch)",
1553
2071
  task: async () => {
1554
- const pkgDir = typeof flags.watchWeb === "string" ? resolve3(flags.watchWeb) : webSourcePackage(CLI_ROOT);
2072
+ const pkgDir = typeof flags.watchWeb === "string" ? resolve4(flags.watchWeb) : webSourcePackage(CLI_ROOT);
1555
2073
  if (!pkgDir) {
1556
- const defaultPath = dirname6(dirname6(dirname6(CLI_ROOT))) + "/zooid-clients/packages/web";
2074
+ const defaultPath = dirname7(dirname7(dirname7(CLI_ROOT))) + "/zooid-clients/packages/web";
1557
2075
  throw new Error(
1558
2076
  `--watch-web: @zooid/web not found at ${defaultPath}.
1559
2077
  Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
@@ -1568,7 +2086,7 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
1568
2086
  task: async (_, t) => {
1569
2087
  const webRoot = ctx.webWatch?.distPath ?? await ensureWebRoot({
1570
2088
  cliRoot: CLI_ROOT,
1571
- cacheDir: join13(layout.dataRoot, "web"),
2089
+ cacheDir: join15(layout.dataRoot, "web"),
1572
2090
  version: readZoonWebPin(CLI_ROOT),
1573
2091
  onProgress: (msg) => {
1574
2092
  t.output = msg;
@@ -1660,8 +2178,8 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
1660
2178
  }
1661
2179
  function loadEnvFiles(cwd) {
1662
2180
  for (const name of [".env.local", ".env"]) {
1663
- const path = resolve3(cwd, name);
1664
- if (!existsSync5(path)) continue;
2181
+ const path = resolve4(cwd, name);
2182
+ if (!existsSync6(path)) continue;
1665
2183
  try {
1666
2184
  process.loadEnvFile(path);
1667
2185
  } catch (err) {
@@ -1671,9 +2189,9 @@ function loadEnvFiles(cwd) {
1671
2189
  }
1672
2190
 
1673
2191
  // src/commands/init.ts
1674
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync as readdirSync2, symlinkSync, writeFileSync as writeFileSync6 } from "fs";
2192
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync2, symlinkSync, writeFileSync as writeFileSync8 } from "fs";
1675
2193
  import { homedir as homedir2 } from "os";
1676
- import { dirname as dirname7, join as join15, resolve as resolve4 } from "path";
2194
+ import { dirname as dirname8, join as join17, resolve as resolve5 } from "path";
1677
2195
 
1678
2196
  // src/commands/init/generators.ts
1679
2197
  function generateZooidYaml(opts) {
@@ -1854,20 +2372,20 @@ function findPiProvider(id) {
1854
2372
  }
1855
2373
 
1856
2374
  // src/commands/init/sniff.ts
1857
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
2375
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
1858
2376
  import { homedir } from "os";
1859
- import { join as join14 } from "path";
2377
+ import { join as join16 } from "path";
1860
2378
  function sniffCredentials(preset, home = homedir()) {
1861
2379
  const rel = preset === "pi" ? PI_AUTH_FILE : findSimplePreset(preset)?.credentialDir;
1862
2380
  if (!rel) return { found: false };
1863
- const full = join14(home, rel);
1864
- if (existsSync6(full)) return { found: true, path: full };
2381
+ const full = join16(home, rel);
2382
+ if (existsSync7(full)) return { found: true, path: full };
1865
2383
  return { found: false };
1866
2384
  }
1867
2385
  function sniffPiDefaults(home = homedir()) {
1868
- const source = join14(home, PI_SETTINGS_FILE);
2386
+ const source = join16(home, PI_SETTINGS_FILE);
1869
2387
  try {
1870
- const raw = JSON.parse(readFileSync8(source, "utf8"));
2388
+ const raw = JSON.parse(readFileSync10(source, "utf8"));
1871
2389
  const provider = raw.defaultProvider;
1872
2390
  const model = raw.defaultModel;
1873
2391
  if (typeof provider !== "string" || !provider) return void 0;
@@ -1891,8 +2409,8 @@ var IGNORED_PREEXISTING = /* @__PURE__ */ new Set([
1891
2409
  "yarn.lock"
1892
2410
  ]);
1893
2411
  async function runInit(opts) {
1894
- const dir = resolve4(opts.dir);
1895
- mkdirSync6(dir, { recursive: true });
2412
+ const dir = resolve5(opts.dir);
2413
+ mkdirSync8(dir, { recursive: true });
1896
2414
  if (!opts.force) {
1897
2415
  const blocking = readdirSync2(dir).filter((n) => !IGNORED_PREEXISTING.has(n));
1898
2416
  if (blocking.length > 0) {
@@ -1988,14 +2506,14 @@ async function runInit(opts) {
1988
2506
  }
1989
2507
  writes.push({ path: ".gitignore", content: generateGitignore() });
1990
2508
  for (const w of writes) {
1991
- const full = join15(dir, w.path);
1992
- const exists = existsSync7(full);
2509
+ const full = join17(dir, w.path);
2510
+ const exists = existsSync8(full);
1993
2511
  if (exists && !opts.overwrite) {
1994
2512
  console.warn(`\u26A0 ${w.path} exists; left as-is (use --force --overwrite to replace)`);
1995
2513
  continue;
1996
2514
  }
1997
- mkdirSync6(dirname7(full), { recursive: true });
1998
- writeFileSync6(full, w.content);
2515
+ mkdirSync8(dirname8(full), { recursive: true });
2516
+ writeFileSync8(full, w.content);
1999
2517
  console.log(`\u2713 Created ${w.path}`);
2000
2518
  }
2001
2519
  if ((opts.preset === "claude" || opts.preset === "codex") && opts.auth === "subscription") {
@@ -2020,10 +2538,10 @@ async function runInit(opts) {
2020
2538
  }
2021
2539
  const s = sniffCredentials("pi", opts.home);
2022
2540
  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 });
2541
+ const authSource = join17(opts.home ?? homedir2(), PI_AUTH_FILE);
2542
+ const linkPath = join17(dir, `agents/zooid-assistant/${PI_AGENT_DIR}/auth.json`);
2543
+ if (!existsSync8(linkPath)) {
2544
+ mkdirSync8(dirname8(linkPath), { recursive: true });
2027
2545
  symlinkSync(authSource, linkPath);
2028
2546
  }
2029
2547
  console.log(
@@ -2155,8 +2673,8 @@ async function resolveOptions(flags) {
2155
2673
 
2156
2674
  // src/commands/logs.ts
2157
2675
  import { readFile, readdir as readdir2, readlink } from "fs/promises";
2158
- import { existsSync as existsSync8 } from "fs";
2159
- import { join as join16 } from "path";
2676
+ import { existsSync as existsSync9 } from "fs";
2677
+ import { join as join18 } from "path";
2160
2678
  var KNOWN_SOURCES = ["tuwunel", "daemon", "dev"];
2161
2679
  async function runLogs(flags) {
2162
2680
  const writer = flags.writer ?? ((s) => process.stdout.write(s));
@@ -2175,8 +2693,8 @@ async function runLogs(flags) {
2175
2693
  writer("no logs yet\n");
2176
2694
  return;
2177
2695
  }
2178
- const dayDir = join16(flags.dataDir, "logs", day);
2179
- if (!existsSync8(dayDir)) {
2696
+ const dayDir = join18(flags.dataDir, "logs", day);
2697
+ if (!existsSync9(dayDir)) {
2180
2698
  writer(`no logs for ${day}
2181
2699
  `);
2182
2700
  return;
@@ -2196,7 +2714,7 @@ async function runLogs(flags) {
2196
2714
  return;
2197
2715
  }
2198
2716
  const path = resolveSourcePath(dayDir, flags.source);
2199
- if (!existsSync8(path)) {
2717
+ if (!existsSync9(path)) {
2200
2718
  writer(`no such source: ${flags.source}
2201
2719
  `);
2202
2720
  return;
@@ -2204,7 +2722,7 @@ async function runLogs(flags) {
2204
2722
  writer(await readFile(path, "utf8"));
2205
2723
  }
2206
2724
  async function resolveTodaySlug(dataDir) {
2207
- const link = join16(dataDir, "logs", "today");
2725
+ const link = join18(dataDir, "logs", "today");
2208
2726
  try {
2209
2727
  return await readlink(link);
2210
2728
  } catch {
@@ -2213,18 +2731,18 @@ async function resolveTodaySlug(dataDir) {
2213
2731
  }
2214
2732
  function resolveSourcePath(dayDir, source) {
2215
2733
  if (source.startsWith("agent-")) {
2216
- if (source.endsWith(".acp")) return join16(dayDir, `${source.slice(0, -4)}.acp.jsonl`);
2217
- return join16(dayDir, `${source}.log`);
2734
+ if (source.endsWith(".acp")) return join18(dayDir, `${source.slice(0, -4)}.acp.jsonl`);
2735
+ return join18(dayDir, `${source}.log`);
2218
2736
  }
2219
2737
  if (KNOWN_SOURCES.includes(source))
2220
- return join16(dayDir, `${source}.log`);
2221
- return join16(dayDir, source);
2738
+ return join18(dayDir, `${source}.log`);
2739
+ return join18(dayDir, source);
2222
2740
  }
2223
2741
  async function dumpByTurn(dayDir, turnId, writer) {
2224
2742
  const entries = await readdir2(dayDir);
2225
2743
  const taps = entries.filter((e) => e.endsWith(".acp.jsonl")).sort();
2226
2744
  for (const f of taps) {
2227
- const text = await readFile(join16(dayDir, f), "utf8");
2745
+ const text = await readFile(join18(dayDir, f), "utf8");
2228
2746
  for (const line of text.split("\n")) {
2229
2747
  if (!line) continue;
2230
2748
  try {
@@ -2238,14 +2756,14 @@ async function dumpByTurn(dayDir, turnId, writer) {
2238
2756
 
2239
2757
  // src/commands/start.ts
2240
2758
  import { randomBytes as randomBytes2 } from "crypto";
2241
- import { resolve as resolve5 } from "path";
2759
+ import { resolve as resolve6 } from "path";
2242
2760
  async function runStart(flags) {
2243
2761
  if (flags.printToken) {
2244
2762
  process.stdout.write(`${randomBytes2(32).toString("hex")}
2245
2763
  `);
2246
2764
  return;
2247
2765
  }
2248
- const dataRoot = resolve5(process.cwd(), flags.dataDir ?? "./data");
2766
+ const dataRoot = resolve6(process.cwd(), flags.dataDir ?? "./data");
2249
2767
  const layout = resolveDataLayout(dataRoot);
2250
2768
  const handle = await startDaemon({
2251
2769
  cliFlags: flags,
@@ -2258,12 +2776,12 @@ async function runStart(flags) {
2258
2776
  }
2259
2777
 
2260
2778
  // src/commands/status.ts
2261
- import { readFileSync as readFileSync9 } from "fs";
2262
- import { dirname as dirname8, join as join17, resolve as resolve6 } from "path";
2779
+ import { readFileSync as readFileSync11 } from "fs";
2780
+ import { dirname as dirname9, join as join19, resolve as resolve7 } from "path";
2263
2781
  import chalk2 from "chalk";
2264
2782
  function readVapidPublicKey(dataDir) {
2265
2783
  try {
2266
- const parsed = JSON.parse(readFileSync9(join17(dataDir, VAPID_FILENAME), "utf8"));
2784
+ const parsed = JSON.parse(readFileSync11(join19(dataDir, VAPID_FILENAME), "utf8"));
2267
2785
  return typeof parsed.publicKey === "string" ? parsed.publicKey : void 0;
2268
2786
  } catch {
2269
2787
  return void 0;
@@ -2293,8 +2811,8 @@ async function collectStatus(opts) {
2293
2811
  ...vapidPublicKey ? { vapidPublicKey } : {}
2294
2812
  };
2295
2813
  }
2296
- const cfg = loadZooidConfig(readFileSync9(found.path, "utf8"), {
2297
- configDir: dirname8(found.path)
2814
+ const cfg = loadZooidConfig(readFileSync11(found.path, "utf8"), {
2815
+ configDir: dirname9(found.path)
2298
2816
  });
2299
2817
  const matrixEntry = Object.entries(cfg.transports).find(
2300
2818
  ([, t]) => t.type === "matrix"
@@ -2325,8 +2843,8 @@ async function runStatus(flags) {
2325
2843
  if (port === void 0) {
2326
2844
  const found = findConfigFile(cwd);
2327
2845
  if (found) {
2328
- const cfg = loadZooidConfig(readFileSync9(found.path, "utf8"), {
2329
- configDir: dirname8(found.path)
2846
+ const cfg = loadZooidConfig(readFileSync11(found.path, "utf8"), {
2847
+ configDir: dirname9(found.path)
2330
2848
  });
2331
2849
  const matrix = findMatrixTransport(cfg);
2332
2850
  if (matrix) {
@@ -2336,7 +2854,7 @@ async function runStatus(flags) {
2336
2854
  }
2337
2855
  }
2338
2856
  const tuwunelUrl = `http://localhost:${port ?? 8448}`;
2339
- const dataDir = resolve6(cwd, flags.dataDir ?? "./data");
2857
+ const dataDir = resolve7(cwd, flags.dataDir ?? "./data");
2340
2858
  const s = await collectStatus({ cwd, tuwunelUrl, dataDir });
2341
2859
  const fmt = (st) => st === "up" ? chalk2.green("up") : st === "down" ? chalk2.red("down") : chalk2.yellow("unknown");
2342
2860
  process.stdout.write(
@@ -2353,11 +2871,11 @@ async function runStatus(flags) {
2353
2871
  }
2354
2872
 
2355
2873
  // src/version.ts
2356
- import { readFileSync as readFileSync10 } from "fs";
2874
+ import { readFileSync as readFileSync12 } from "fs";
2357
2875
  function readCliVersion(url = import.meta.url) {
2358
2876
  try {
2359
2877
  const manifest = new URL("../package.json", url);
2360
- const raw = JSON.parse(readFileSync10(manifest, "utf8"));
2878
+ const raw = JSON.parse(readFileSync12(manifest, "utf8"));
2361
2879
  if (typeof raw.version === "string" && raw.version) return raw.version;
2362
2880
  return "unknown";
2363
2881
  } catch {
@@ -2395,14 +2913,14 @@ cli.command(
2395
2913
  ).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
2914
  if (source === "prune") {
2397
2915
  await runLogs({
2398
- dataDir: resolve7(process.cwd(), flags.data),
2916
+ dataDir: resolve8(process.cwd(), flags.data),
2399
2917
  subcommand: "prune",
2400
2918
  keep: Number(flags.keep)
2401
2919
  });
2402
2920
  return;
2403
2921
  }
2404
2922
  await runLogs({
2405
- dataDir: resolve7(process.cwd(), flags.data),
2923
+ dataDir: resolve8(process.cwd(), flags.data),
2406
2924
  source,
2407
2925
  day: flags.day,
2408
2926
  turn: flags.turn,