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