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/README.md +6 -1
- package/dist/bin.js +689 -171
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-YZ4IO5MR.js → chunk-R5S26T7B.js} +20523 -507
- package/dist/chunk-R5S26T7B.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/package.json +11 -9
- package/src/build-registry.context.test.ts +3 -3
- package/src/build-registry.ts +67 -24
- package/src/daemon/delivery-cache.test.ts +34 -0
- package/src/daemon/delivery-cache.ts +36 -0
- package/src/daemon/load-custom-verifiers.ts +43 -0
- package/src/daemon/start-daemon.ts +162 -38
- package/src/daemon/task-journal.test.ts +19 -0
- package/src/daemon/task-journal.ts +32 -0
- package/src/daemon/trigger-rooms.test.ts +60 -0
- package/src/daemon/trigger-rooms.ts +0 -0
- package/src/daemon/trigger-runner.test.ts +71 -0
- package/src/daemon/trigger-runner.ts +41 -0
- package/src/daemon/trigger-scheduler.ts +55 -0
- package/src/daemon/webhook-routes.test.ts +104 -0
- package/src/daemon/webhook-routes.ts +237 -0
- package/src/daemon/webhook-verify.test.ts +155 -0
- package/src/daemon/webhook-verify.ts +155 -0
- package/src/pi-extension-install.test.ts +65 -0
- package/src/pi-extension-install.ts +56 -0
- package/dist/chunk-YZ4IO5MR.js.map +0 -1
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
|
-
|
|
19
|
+
renderTemplate,
|
|
20
|
+
startAgentSocketServers,
|
|
18
21
|
startWorkforcePublisher
|
|
19
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-R5S26T7B.js";
|
|
20
23
|
|
|
21
24
|
// src/bin.ts
|
|
22
|
-
import { resolve as
|
|
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
|
|
29
|
-
import { dirname as
|
|
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
|
|
219
|
-
import { mkdir
|
|
221
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
222
|
+
import { mkdir } from "fs/promises";
|
|
220
223
|
import { tmpdir } from "os";
|
|
221
|
-
import { dirname as
|
|
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
|
|
580
|
-
import { join as
|
|
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 =
|
|
618
|
+
const path = join5(dataDir, VAPID_FILENAME);
|
|
585
619
|
try {
|
|
586
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
594
|
-
|
|
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
|
|
607
|
-
import { join as
|
|
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) =>
|
|
643
|
+
const fileFor = (agentName) => join6(agentsDir, agentName, "sync-since");
|
|
610
644
|
return {
|
|
611
645
|
loadSince(agentName) {
|
|
612
646
|
try {
|
|
613
|
-
return
|
|
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
|
-
|
|
620
|
-
|
|
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((
|
|
1058
|
+
return new Promise((resolve9) => {
|
|
633
1059
|
const check = () => {
|
|
634
1060
|
const addr = server.address();
|
|
635
|
-
if (addr && typeof addr === "object")
|
|
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((
|
|
643
|
-
server.close(() =>
|
|
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 =
|
|
651
|
-
const base = loadZooidConfig(
|
|
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
|
|
655
|
-
await mkdir(
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
|
673
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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({
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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((
|
|
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((
|
|
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)
|
|
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
|
|
991
|
-
import { dirname as
|
|
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
|
|
995
|
-
import { join as
|
|
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 =
|
|
1002
|
-
if (
|
|
1003
|
-
if (
|
|
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 =
|
|
1031
|
-
|
|
1032
|
-
const tgzPath =
|
|
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
|
-
|
|
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 (!
|
|
1561
|
+
if (!existsSync3(join9(tmp, "index.html"))) {
|
|
1044
1562
|
throw new Error(`${PKG}@${opts.version} tarball has no dist/index.html`);
|
|
1045
1563
|
}
|
|
1046
|
-
|
|
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(
|
|
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 &&
|
|
1581
|
+
if (override && existsSync4(join10(override, "index.html"))) return resolve3(override);
|
|
1064
1582
|
const fromSource = webSourcePackage(opts.cliRoot);
|
|
1065
|
-
if (fromSource &&
|
|
1066
|
-
return
|
|
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 =
|
|
1080
|
-
const candidate =
|
|
1081
|
-
return
|
|
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
|
|
1086
|
-
import { join as
|
|
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(
|
|
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
|
|
1098
|
-
import { extname, join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
1154
|
-
import { join as
|
|
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 =
|
|
1157
|
-
const indexPath =
|
|
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 (
|
|
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((
|
|
1729
|
+
await new Promise((resolve9) => {
|
|
1212
1730
|
let done = false;
|
|
1213
1731
|
const finish = () => {
|
|
1214
1732
|
if (done) return;
|
|
1215
1733
|
done = true;
|
|
1216
|
-
|
|
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
|
|
1231
|
-
import { join as
|
|
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 =
|
|
1242
|
-
const dayDir =
|
|
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:
|
|
1249
|
-
tuwunelLog:
|
|
1250
|
-
daemonLog:
|
|
1251
|
-
devLog:
|
|
1252
|
-
agentLog: (n) =>
|
|
1253
|
-
agentTap: (n) =>
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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(
|
|
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((
|
|
1315
|
-
this.stream.write(line, (err) => err ? reject(err) :
|
|
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((
|
|
1321
|
-
this.stream.end(() =>
|
|
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
|
|
1907
|
+
import { dirname as dirname6 } from "path";
|
|
1390
1908
|
function captureChildToFile(child, path) {
|
|
1391
1909
|
return (async () => {
|
|
1392
|
-
await mkdir4(
|
|
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((
|
|
1397
|
-
child.on("exit", () => stream.end(() =>
|
|
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 =
|
|
1948
|
+
let dir = dirname7(fileURLToPath(import.meta.url));
|
|
1431
1949
|
for (let i = 0; i < 8; i++) {
|
|
1432
|
-
const pkgPath =
|
|
1433
|
-
if (
|
|
1950
|
+
const pkgPath = resolve4(dir, "package.json");
|
|
1951
|
+
if (existsSync6(pkgPath)) {
|
|
1434
1952
|
try {
|
|
1435
|
-
const pkg = JSON.parse(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1459
|
-
const preview = loadZooidConfig(rawYaml, { configDir:
|
|
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" ?
|
|
2072
|
+
const pkgDir = typeof flags.watchWeb === "string" ? resolve4(flags.watchWeb) : webSourcePackage(CLI_ROOT);
|
|
1555
2073
|
if (!pkgDir) {
|
|
1556
|
-
const defaultPath =
|
|
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:
|
|
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 =
|
|
1664
|
-
if (!
|
|
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
|
|
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
|
|
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
|
|
2375
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
|
|
1858
2376
|
import { homedir } from "os";
|
|
1859
|
-
import { join as
|
|
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 =
|
|
1864
|
-
if (
|
|
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 =
|
|
2386
|
+
const source = join16(home, PI_SETTINGS_FILE);
|
|
1869
2387
|
try {
|
|
1870
|
-
const raw = JSON.parse(
|
|
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 =
|
|
1895
|
-
|
|
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 =
|
|
1992
|
-
const exists =
|
|
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
|
-
|
|
1998
|
-
|
|
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 =
|
|
2024
|
-
const linkPath =
|
|
2025
|
-
if (!
|
|
2026
|
-
|
|
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
|
|
2159
|
-
import { join as
|
|
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 =
|
|
2179
|
-
if (!
|
|
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 (!
|
|
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 =
|
|
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
|
|
2217
|
-
return
|
|
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
|
|
2221
|
-
return
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
2262
|
-
import { dirname as
|
|
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(
|
|
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(
|
|
2297
|
-
configDir:
|
|
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(
|
|
2329
|
-
configDir:
|
|
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 =
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
2923
|
+
dataDir: resolve8(process.cwd(), flags.data),
|
|
2406
2924
|
source,
|
|
2407
2925
|
day: flags.day,
|
|
2408
2926
|
turn: flags.turn,
|