zooid 0.12.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 +8 -1
- package/dist/bin.js +923 -184
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-3Q4BPAZD.js → chunk-R5S26T7B.js} +20576 -514
- package/dist/chunk-R5S26T7B.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/package.json +13 -9
- package/src/bin.test.ts +63 -0
- package/src/bin.ts +52 -3
- package/src/bootstrap/configs.test.ts +7 -0
- package/src/bootstrap/configs.ts +14 -0
- package/src/build-registry.context.test.ts +3 -3
- package/src/build-registry.ts +67 -24
- package/src/commands/dev.ts +37 -7
- package/src/commands/status.test.ts +15 -0
- package/src/commands/status.ts +27 -2
- 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 +177 -39
- 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/src/push-gateway/gateway.test.ts +150 -0
- package/src/push-gateway/gateway.ts +78 -0
- package/src/push-gateway/index.ts +17 -0
- package/src/push-gateway/payload.test.ts +88 -0
- package/src/push-gateway/payload.ts +39 -0
- package/src/push-gateway/types.ts +37 -0
- package/src/push-gateway/vapid.test.ts +45 -0
- package/src/push-gateway/vapid.ts +34 -0
- package/src/services/tuwunel.ts +21 -2
- package/src/version.test.ts +67 -0
- package/src/version.ts +30 -0
- package/src/web/static.test.ts +22 -0
- package/src/web/static.ts +9 -1
- package/dist/chunk-3Q4BPAZD.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
|
|
|
@@ -83,6 +86,20 @@ function renderTuwunelToml(opts) {
|
|
|
83
86
|
"allow_local_presence = true",
|
|
84
87
|
'address = ["0.0.0.0"]',
|
|
85
88
|
`port = [${TUWUNEL_INTERNAL_PORT}]`,
|
|
89
|
+
// NOT `suppress_push_when_active` ([[ZNC025]]). It reads Matrix presence,
|
|
90
|
+
// which is both too coarse and too slow for this: coarse because it is
|
|
91
|
+
// per-user, so reading room A kills the push for room B; slow because
|
|
92
|
+
// `currently_active` lingers for minutes after the last sync, so closing
|
|
93
|
+
// the tab and waiting for an agent to finish still delivers nothing —
|
|
94
|
+
// exactly the case this feature exists for. `public/sw.js` already does
|
|
95
|
+
// the suppression we actually want, precisely: it drops a push only when
|
|
96
|
+
// a *visible* window is on *that* room.
|
|
97
|
+
// DEV ONLY — disables an SSRF guard. Tuwunel is in Podman and the push
|
|
98
|
+
// gateway runs on the host, so the default ip_range_denylist (127/8,
|
|
99
|
+
// 10/8, 172.16/12, 192.168/16, ::1) silently drops every pusher delivery.
|
|
100
|
+
// Never set on a box: there the gateway is reached at the public
|
|
101
|
+
// hostname through Caddy.
|
|
102
|
+
"ip_range_denylist = []",
|
|
86
103
|
""
|
|
87
104
|
].join("\n");
|
|
88
105
|
}
|
|
@@ -201,10 +218,10 @@ MATRIX_HS_TOKEN=${tokens.hsToken}
|
|
|
201
218
|
}
|
|
202
219
|
|
|
203
220
|
// src/daemon/start-daemon.ts
|
|
204
|
-
import { readFileSync as
|
|
205
|
-
import { mkdir
|
|
221
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
222
|
+
import { mkdir } from "fs/promises";
|
|
206
223
|
import { tmpdir } from "os";
|
|
207
|
-
import { dirname as
|
|
224
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join8, resolve as resolve2 } from "path";
|
|
208
225
|
import { serve } from "@hono/node-server";
|
|
209
226
|
|
|
210
227
|
// ../transport-http/src/server.ts
|
|
@@ -417,6 +434,37 @@ function createApp({
|
|
|
417
434
|
return app;
|
|
418
435
|
}
|
|
419
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
|
+
|
|
420
468
|
// src/prepull-images.ts
|
|
421
469
|
import { execFile } from "child_process";
|
|
422
470
|
import { promisify } from "util";
|
|
@@ -479,22 +527,163 @@ var defaultExec = async (cmd, args) => {
|
|
|
479
527
|
}
|
|
480
528
|
};
|
|
481
529
|
|
|
530
|
+
// src/push-gateway/gateway.ts
|
|
531
|
+
import { Hono as Hono2 } from "hono";
|
|
532
|
+
import webpush from "web-push";
|
|
533
|
+
|
|
534
|
+
// src/push-gateway/payload.ts
|
|
535
|
+
var MAX_BODY = 140;
|
|
536
|
+
var ZOOID_APP_ID = "dev.zooid.web";
|
|
537
|
+
function truncate(s, max) {
|
|
538
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
539
|
+
}
|
|
540
|
+
function buildPushPayload(n, device) {
|
|
541
|
+
const body = typeof n.content?.body === "string" ? truncate(n.content.body, MAX_BODY) : void 0;
|
|
542
|
+
const preview = typeof n.content?.last_message === "string" ? truncate(n.content.last_message, MAX_BODY) : void 0;
|
|
543
|
+
return {
|
|
544
|
+
event_id: n.event_id,
|
|
545
|
+
room_id: n.room_id,
|
|
546
|
+
room_name: n.room_name ?? n.room_id,
|
|
547
|
+
...n.sender_display_name !== void 0 ? { sender_display_name: n.sender_display_name } : {},
|
|
548
|
+
type: n.type,
|
|
549
|
+
...body !== void 0 ? { body } : {},
|
|
550
|
+
...preview !== void 0 ? { preview } : {},
|
|
551
|
+
unread: n.counts?.unread ?? 0,
|
|
552
|
+
// Whether this makes a noise is a push-rule property the server evaluated,
|
|
553
|
+
// not a second decision made here (spec §12).
|
|
554
|
+
sound: device?.tweaks?.sound !== void 0
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/push-gateway/gateway.ts
|
|
559
|
+
function parseNotifyBody(raw) {
|
|
560
|
+
if (!raw || typeof raw !== "object") return null;
|
|
561
|
+
const notification = raw.notification;
|
|
562
|
+
if (!notification || typeof notification !== "object") return null;
|
|
563
|
+
const n = notification;
|
|
564
|
+
if (typeof n.event_id !== "string" || typeof n.room_id !== "string" || typeof n.type !== "string")
|
|
565
|
+
return null;
|
|
566
|
+
if (!Array.isArray(n.devices)) return null;
|
|
567
|
+
return n;
|
|
568
|
+
}
|
|
569
|
+
function pushGateway(opts) {
|
|
570
|
+
const app = new Hono2();
|
|
571
|
+
app.post("/_matrix/push/v1/notify", async (c) => {
|
|
572
|
+
const parsed = parseNotifyBody(await c.req.json().catch(() => null));
|
|
573
|
+
if (!parsed) {
|
|
574
|
+
console.warn("[push] notify: malformed body");
|
|
575
|
+
return c.json({ error: "malformed notification" }, 400);
|
|
576
|
+
}
|
|
577
|
+
console.log(
|
|
578
|
+
`[push] notify room=${parsed.room_id} type=${parsed.type} devices=${parsed.devices.length}`
|
|
579
|
+
);
|
|
580
|
+
let delivered = 0;
|
|
581
|
+
const rejected = [];
|
|
582
|
+
await Promise.all(
|
|
583
|
+
parsed.devices.map(async (device) => {
|
|
584
|
+
if (device.app_id !== ZOOID_APP_ID) return;
|
|
585
|
+
const endpoint = device.data?.endpoint;
|
|
586
|
+
const auth = device.data?.auth;
|
|
587
|
+
if (typeof endpoint !== "string" || typeof auth !== "string") return;
|
|
588
|
+
try {
|
|
589
|
+
await webpush.sendNotification(
|
|
590
|
+
{ endpoint, keys: { p256dh: device.pushkey, auth } },
|
|
591
|
+
JSON.stringify(buildPushPayload(parsed, device)),
|
|
592
|
+
{
|
|
593
|
+
vapidDetails: { subject: opts.subject, ...opts.keys },
|
|
594
|
+
TTL: 60 * 60 * 12,
|
|
595
|
+
urgency: device.tweaks?.sound !== void 0 ? "high" : "normal"
|
|
596
|
+
}
|
|
597
|
+
);
|
|
598
|
+
delivered++;
|
|
599
|
+
} catch (err) {
|
|
600
|
+
const status = err.statusCode;
|
|
601
|
+
if (status === 404 || status === 410) rejected.push(device.pushkey);
|
|
602
|
+
else console.warn(`[push] delivery to ${device.pushkey} failed (${status ?? "?"}):`, err);
|
|
603
|
+
}
|
|
604
|
+
})
|
|
605
|
+
);
|
|
606
|
+
console.log(`[push] notify done: delivered=${delivered} rejected=${rejected.length}`);
|
|
607
|
+
return c.json({ rejected });
|
|
608
|
+
});
|
|
609
|
+
return app;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/push-gateway/vapid.ts
|
|
613
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
614
|
+
import { join as join5 } from "path";
|
|
615
|
+
import webpush2 from "web-push";
|
|
616
|
+
var VAPID_FILENAME = "vapid.json";
|
|
617
|
+
function loadOrCreateVapidKeys(dataDir) {
|
|
618
|
+
const path = join5(dataDir, VAPID_FILENAME);
|
|
619
|
+
try {
|
|
620
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
621
|
+
if (typeof parsed.publicKey === "string" && typeof parsed.privateKey === "string")
|
|
622
|
+
return { publicKey: parsed.publicKey, privateKey: parsed.privateKey };
|
|
623
|
+
console.warn(`[push] ${path} is malformed; generating a new VAPID keypair.`);
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
const keys = webpush2.generateVAPIDKeys();
|
|
627
|
+
mkdirSync4(dataDir, { recursive: true });
|
|
628
|
+
writeFileSync4(path, JSON.stringify(keys, null, 2), { mode: 384 });
|
|
629
|
+
return keys;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/push-gateway/index.ts
|
|
633
|
+
function mountPushGateway(app, opts) {
|
|
634
|
+
const keys = loadOrCreateVapidKeys(opts.dataDir);
|
|
635
|
+
app.route("/", pushGateway({ keys, subject: opts.subject }));
|
|
636
|
+
return { publicKey: keys.publicKey };
|
|
637
|
+
}
|
|
638
|
+
|
|
482
639
|
// src/daemon/sync-cursors.ts
|
|
483
|
-
import { mkdirSync as
|
|
484
|
-
import { join as
|
|
640
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
641
|
+
import { join as join6 } from "path";
|
|
485
642
|
function makeSyncCursorStore(agentsDir) {
|
|
486
|
-
const fileFor = (agentName) =>
|
|
643
|
+
const fileFor = (agentName) => join6(agentsDir, agentName, "sync-since");
|
|
487
644
|
return {
|
|
488
645
|
loadSince(agentName) {
|
|
489
646
|
try {
|
|
490
|
-
return
|
|
647
|
+
return readFileSync4(fileFor(agentName), "utf8").trim() || null;
|
|
491
648
|
} catch {
|
|
492
649
|
return null;
|
|
493
650
|
}
|
|
494
651
|
},
|
|
495
652
|
saveSince(agentName, since) {
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
}
|
|
498
687
|
}
|
|
499
688
|
};
|
|
500
689
|
}
|
|
@@ -504,55 +693,425 @@ function shouldBindHttpListener(mode) {
|
|
|
504
693
|
return mode !== "client";
|
|
505
694
|
}
|
|
506
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
|
+
|
|
507
1041
|
// src/daemon/start-daemon.ts
|
|
508
1042
|
function listenAsync(server) {
|
|
509
|
-
return new Promise((
|
|
1043
|
+
return new Promise((resolve9) => {
|
|
510
1044
|
const check = () => {
|
|
511
1045
|
const addr = server.address();
|
|
512
|
-
if (addr && typeof addr === "object")
|
|
1046
|
+
if (addr && typeof addr === "object") resolve9(addr.port);
|
|
513
1047
|
else setImmediate(check);
|
|
514
1048
|
};
|
|
515
1049
|
check();
|
|
516
1050
|
});
|
|
517
1051
|
}
|
|
518
1052
|
function closeAsync(server) {
|
|
519
|
-
return new Promise((
|
|
520
|
-
server.close(() =>
|
|
1053
|
+
return new Promise((resolve9) => {
|
|
1054
|
+
server.close(() => resolve9());
|
|
521
1055
|
});
|
|
522
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
|
+
}
|
|
523
1062
|
async function startDaemon(opts = {}) {
|
|
524
1063
|
const cwd = opts.cwd ?? process.cwd();
|
|
525
1064
|
const found = opts.configPath ? { path: opts.configPath } : findConfigFile(cwd);
|
|
526
1065
|
if (!found) throw new Error("zooid.yaml is required");
|
|
527
|
-
const configDir =
|
|
528
|
-
const base = loadZooidConfig(
|
|
1066
|
+
const configDir = dirname3(found.path);
|
|
1067
|
+
const base = loadZooidConfig(readFileSync6(found.path, "utf8"), { configDir, validateCron });
|
|
529
1068
|
const config = mergeCliFlags(base, opts.cliFlags ?? {});
|
|
530
1069
|
const approvals = new ApprovalCorrelator();
|
|
531
|
-
const
|
|
532
|
-
await mkdir(
|
|
1070
|
+
const runDir = opts.agentsDir ? join8(opts.agentsDir, "..", "run") : join8(tmpdir(), `zooid-context-${process.pid}`);
|
|
1071
|
+
await mkdir(runDir, { recursive: true }).catch(() => {
|
|
533
1072
|
});
|
|
534
1073
|
const contextSpawnRegistry = new SpawnRegistry();
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
} catch (err) {
|
|
542
|
-
console.warn("[context] daemon socket startup failed; zooid-context MCP disabled:", err);
|
|
543
|
-
}
|
|
544
|
-
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;
|
|
545
1080
|
const registry = buildAcpRegistry(config, {
|
|
546
1081
|
approvals,
|
|
547
1082
|
onTap: opts.onTap,
|
|
548
1083
|
agentsDir: opts.agentsDir,
|
|
549
|
-
contextSpawnRegistry
|
|
550
|
-
|
|
1084
|
+
contextSpawnRegistry,
|
|
1085
|
+
daemonSockPaths: contextSockets.paths,
|
|
551
1086
|
configDir,
|
|
552
1087
|
dataDir,
|
|
553
1088
|
daemonHome: process.env.HOME
|
|
554
1089
|
});
|
|
555
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
|
+
}
|
|
556
1115
|
if (config.runtime !== "local") {
|
|
557
1116
|
await prepullImages(registry, {
|
|
558
1117
|
engine: config.runtime === "podman" ? "podman" : "docker",
|
|
@@ -562,11 +1121,13 @@ async function startDaemon(opts = {}) {
|
|
|
562
1121
|
log: opts.prepullLog
|
|
563
1122
|
});
|
|
564
1123
|
}
|
|
565
|
-
console.log(
|
|
566
|
-
|
|
567
|
-
|
|
1124
|
+
console.log(`[context] runDir=${runDir}`);
|
|
1125
|
+
for (const name of agentNames) {
|
|
1126
|
+
console.log(`[context] agent=${name} socket=${contextSockets.paths[name] ?? "(disabled)"}`);
|
|
1127
|
+
}
|
|
568
1128
|
let server = null;
|
|
569
1129
|
let syncLoops;
|
|
1130
|
+
let triggers;
|
|
570
1131
|
let stopped = false;
|
|
571
1132
|
let resolveStopped;
|
|
572
1133
|
const whenStopped = new Promise((r) => {
|
|
@@ -574,6 +1135,7 @@ async function startDaemon(opts = {}) {
|
|
|
574
1135
|
});
|
|
575
1136
|
const matrix = findMatrixTransport(config);
|
|
576
1137
|
let port;
|
|
1138
|
+
let vapidPublicKey;
|
|
577
1139
|
if (matrix) {
|
|
578
1140
|
const mode = matrix.transport.mode ?? "appservice";
|
|
579
1141
|
if (mode === "client" && !opts.agentsDir) {
|
|
@@ -600,7 +1162,7 @@ async function startDaemon(opts = {}) {
|
|
|
600
1162
|
trigger: a.matrix.trigger
|
|
601
1163
|
};
|
|
602
1164
|
if (a.matrix.display_name !== void 0) binding.displayName = a.matrix.display_name;
|
|
603
|
-
const workspaceDir =
|
|
1165
|
+
const workspaceDir = isAbsolute2(a.workdir) ? a.workdir : resolve2(configDir, a.workdir);
|
|
604
1166
|
binding.workspaceDir = workspaceDir;
|
|
605
1167
|
binding.agentWorkspacePath = isContainerRuntime ? "/workspace" : workspaceDir;
|
|
606
1168
|
bindings.push(binding);
|
|
@@ -617,6 +1179,7 @@ async function startDaemon(opts = {}) {
|
|
|
617
1179
|
adminUserId: opts.adminUserId,
|
|
618
1180
|
botUserId: asUserId,
|
|
619
1181
|
media: mediaClient,
|
|
1182
|
+
taskJournal: dataDir ? makeTaskJournal(dataDir) : void 0,
|
|
620
1183
|
mode,
|
|
621
1184
|
loadSince: (uid) => {
|
|
622
1185
|
const name = nameByUserId.get(uid);
|
|
@@ -627,16 +1190,55 @@ async function startDaemon(opts = {}) {
|
|
|
627
1190
|
if (name && cursors) cursors.saveSince(name, since);
|
|
628
1191
|
}
|
|
629
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
|
+
};
|
|
630
1208
|
if (shouldBindHttpListener(mode)) {
|
|
631
1209
|
const requestedPort = matrix.transport.port ?? 9e3;
|
|
632
|
-
|
|
1210
|
+
if (dataDir) {
|
|
1211
|
+
vapidPublicKey = mountPushGateway(transport.app, {
|
|
1212
|
+
dataDir,
|
|
1213
|
+
subject: `https://${serverName}`
|
|
1214
|
+
}).publicKey;
|
|
1215
|
+
}
|
|
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
|
+
});
|
|
633
1236
|
port = await listenAsync(server);
|
|
634
1237
|
} else {
|
|
635
1238
|
port = 0;
|
|
636
1239
|
}
|
|
637
1240
|
const spaceLocalpart = matrix.transport.space ?? "dev";
|
|
638
1241
|
const adminUserIds = opts.adminUserId ? [opts.adminUserId] : [];
|
|
639
|
-
let spaceRoomId;
|
|
640
1242
|
try {
|
|
641
1243
|
spaceRoomId = await ensureWorkforceSpace({
|
|
642
1244
|
client,
|
|
@@ -647,11 +1249,36 @@ async function startDaemon(opts = {}) {
|
|
|
647
1249
|
admins: adminUserIds,
|
|
648
1250
|
joinRule: opts.publicWorkforceSpace ? "public" : "invite"
|
|
649
1251
|
});
|
|
650
|
-
console.log(
|
|
1252
|
+
console.log(
|
|
1253
|
+
`[matrix] ensured workforce space #${spaceLocalpart}:${serverName} \u2192 ${spaceRoomId}`
|
|
1254
|
+
);
|
|
651
1255
|
} catch (err) {
|
|
652
1256
|
console.warn("[matrix] workforce space provisioning failed:", err);
|
|
653
1257
|
}
|
|
654
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
|
+
}
|
|
655
1282
|
syncLoops = transport.syncLoops;
|
|
656
1283
|
if (syncLoops?.length) {
|
|
657
1284
|
for (const loop of syncLoops) void loop.run();
|
|
@@ -689,12 +1316,20 @@ async function startDaemon(opts = {}) {
|
|
|
689
1316
|
const token = process.env.ZOOID_TOKEN;
|
|
690
1317
|
if (!token) throw new Error("ZOOID_TOKEN is required for http transport");
|
|
691
1318
|
const app = createApp({ agents: registry, approvals, token });
|
|
692
|
-
server = serve({
|
|
1319
|
+
server = serve({
|
|
1320
|
+
fetch: app.fetch,
|
|
1321
|
+
port: http.transport.port,
|
|
1322
|
+
hostname: "0.0.0.0"
|
|
1323
|
+
});
|
|
693
1324
|
port = await listenAsync(server);
|
|
694
1325
|
}
|
|
695
1326
|
const stop = async () => {
|
|
696
1327
|
if (stopped) return whenStopped;
|
|
697
1328
|
stopped = true;
|
|
1329
|
+
try {
|
|
1330
|
+
await triggers?.stop();
|
|
1331
|
+
} catch {
|
|
1332
|
+
}
|
|
698
1333
|
try {
|
|
699
1334
|
if (syncLoops) for (const loop of syncLoops) loop.stop();
|
|
700
1335
|
} catch {
|
|
@@ -709,9 +1344,7 @@ async function startDaemon(opts = {}) {
|
|
|
709
1344
|
console.error("stopAll:", err);
|
|
710
1345
|
}
|
|
711
1346
|
try {
|
|
712
|
-
|
|
713
|
-
await unlink(daemonSockPath).catch(() => {
|
|
714
|
-
});
|
|
1347
|
+
await contextSockets.close();
|
|
715
1348
|
} catch {
|
|
716
1349
|
}
|
|
717
1350
|
resolveStopped();
|
|
@@ -724,7 +1357,7 @@ async function startDaemon(opts = {}) {
|
|
|
724
1357
|
process.on("SIGINT", () => handler("SIGINT"));
|
|
725
1358
|
process.on("SIGTERM", () => handler("SIGTERM"));
|
|
726
1359
|
}
|
|
727
|
-
return { port, agentNames, stop, whenStopped };
|
|
1360
|
+
return { port, agentNames, vapidPublicKey, stop, whenStopped };
|
|
728
1361
|
}
|
|
729
1362
|
|
|
730
1363
|
// src/services/tuwunel.ts
|
|
@@ -769,9 +1402,24 @@ var TuwunelService = class {
|
|
|
769
1402
|
}
|
|
770
1403
|
async stop() {
|
|
771
1404
|
if (this.child && this.child.exitCode === null) {
|
|
772
|
-
this.child
|
|
773
|
-
|
|
774
|
-
|
|
1405
|
+
const child = this.child;
|
|
1406
|
+
child.kill("SIGTERM");
|
|
1407
|
+
await new Promise((resolve9) => {
|
|
1408
|
+
let done = false;
|
|
1409
|
+
const finish = () => {
|
|
1410
|
+
if (done) return;
|
|
1411
|
+
done = true;
|
|
1412
|
+
clearTimeout(timer);
|
|
1413
|
+
resolve9();
|
|
1414
|
+
};
|
|
1415
|
+
const timer = setTimeout(() => {
|
|
1416
|
+
try {
|
|
1417
|
+
child.kill("SIGKILL");
|
|
1418
|
+
} catch {
|
|
1419
|
+
}
|
|
1420
|
+
finish();
|
|
1421
|
+
}, 5e3);
|
|
1422
|
+
child.once("exit", finish);
|
|
775
1423
|
});
|
|
776
1424
|
}
|
|
777
1425
|
this.child = null;
|
|
@@ -788,7 +1436,7 @@ var TuwunelService = class {
|
|
|
788
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}\`).`
|
|
789
1437
|
);
|
|
790
1438
|
}
|
|
791
|
-
await new Promise((
|
|
1439
|
+
await new Promise((resolve9) => setTimeout(resolve9, 500));
|
|
792
1440
|
}
|
|
793
1441
|
while (Date.now() < deadline) {
|
|
794
1442
|
try {
|
|
@@ -796,7 +1444,7 @@ var TuwunelService = class {
|
|
|
796
1444
|
if (r.ok) return;
|
|
797
1445
|
} catch {
|
|
798
1446
|
}
|
|
799
|
-
await new Promise((
|
|
1447
|
+
await new Promise((resolve9) => setTimeout(resolve9, 500));
|
|
800
1448
|
}
|
|
801
1449
|
const finalState = await this.inspectState().catch(() => null);
|
|
802
1450
|
const detail = finalState ? ` (container status=${finalState.status}${finalState.exitCode !== void 0 ? `, exit=${finalState.exitCode}` : ""})` : "";
|
|
@@ -829,12 +1477,12 @@ var TuwunelService = class {
|
|
|
829
1477
|
}
|
|
830
1478
|
};
|
|
831
1479
|
function execEngine(engine, args) {
|
|
832
|
-
return new Promise((
|
|
1480
|
+
return new Promise((resolve9, reject) => {
|
|
833
1481
|
const child = spawn(engine, args, { stdio: "pipe" });
|
|
834
1482
|
let stderr = "";
|
|
835
1483
|
child.stderr.on("data", (b) => stderr += String(b));
|
|
836
1484
|
child.on("exit", (code) => {
|
|
837
|
-
if (code === 0)
|
|
1485
|
+
if (code === 0) resolve9();
|
|
838
1486
|
else reject(new Error(`${engine} ${args.join(" ")} failed: ${stderr.trim()}`));
|
|
839
1487
|
});
|
|
840
1488
|
child.on("error", reject);
|
|
@@ -842,20 +1490,20 @@ function execEngine(engine, args) {
|
|
|
842
1490
|
}
|
|
843
1491
|
|
|
844
1492
|
// src/web/resolve.ts
|
|
845
|
-
import { existsSync as
|
|
846
|
-
import { dirname as
|
|
1493
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1494
|
+
import { dirname as dirname4, join as join10, resolve as resolve3 } from "path";
|
|
847
1495
|
|
|
848
1496
|
// src/web/fetch.ts
|
|
849
|
-
import { mkdirSync as
|
|
850
|
-
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";
|
|
851
1499
|
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
852
1500
|
import * as tar from "tar";
|
|
853
1501
|
var PKG = "@zooid/web";
|
|
854
1502
|
var DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
855
1503
|
async function fetchWebBundle(opts) {
|
|
856
|
-
const target =
|
|
857
|
-
if (
|
|
858
|
-
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 });
|
|
859
1507
|
const f = opts.fetch ?? globalThis.fetch;
|
|
860
1508
|
const registry = (opts.registryUrl ?? DEFAULT_REGISTRY).replace(/\/$/, "");
|
|
861
1509
|
let tgz;
|
|
@@ -882,11 +1530,11 @@ async function fetchWebBundle(opts) {
|
|
|
882
1530
|
Cause: ${err instanceof Error ? err.message : String(err)}`
|
|
883
1531
|
);
|
|
884
1532
|
}
|
|
885
|
-
const tmp =
|
|
886
|
-
|
|
887
|
-
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");
|
|
888
1536
|
try {
|
|
889
|
-
|
|
1537
|
+
writeFileSync7(tgzPath, tgz);
|
|
890
1538
|
await tar.extract({
|
|
891
1539
|
file: tgzPath,
|
|
892
1540
|
cwd: tmp,
|
|
@@ -895,17 +1543,17 @@ async function fetchWebBundle(opts) {
|
|
|
895
1543
|
filter: (p) => p === "package/dist" || p.startsWith("package/dist/")
|
|
896
1544
|
});
|
|
897
1545
|
rmSync(tgzPath);
|
|
898
|
-
if (!
|
|
1546
|
+
if (!existsSync3(join9(tmp, "index.html"))) {
|
|
899
1547
|
throw new Error(`${PKG}@${opts.version} tarball has no dist/index.html`);
|
|
900
1548
|
}
|
|
901
|
-
|
|
1549
|
+
renameSync2(tmp, target);
|
|
902
1550
|
} catch (err) {
|
|
903
1551
|
rmSync(tmp, { recursive: true, force: true });
|
|
904
1552
|
throw err;
|
|
905
1553
|
}
|
|
906
1554
|
for (const entry of readdirSync(opts.cacheDir)) {
|
|
907
1555
|
if (entry !== opts.version) {
|
|
908
|
-
rmSync(
|
|
1556
|
+
rmSync(join9(opts.cacheDir, entry), { recursive: true, force: true });
|
|
909
1557
|
}
|
|
910
1558
|
}
|
|
911
1559
|
return target;
|
|
@@ -915,10 +1563,10 @@ async function fetchWebBundle(opts) {
|
|
|
915
1563
|
var ENV_OVERRIDE = "ZOOID_DEV_WEB_ROOT_OVERRIDE";
|
|
916
1564
|
async function ensureWebRoot(opts) {
|
|
917
1565
|
const override = process.env[ENV_OVERRIDE];
|
|
918
|
-
if (override &&
|
|
1566
|
+
if (override && existsSync4(join10(override, "index.html"))) return resolve3(override);
|
|
919
1567
|
const fromSource = webSourcePackage(opts.cliRoot);
|
|
920
|
-
if (fromSource &&
|
|
921
|
-
return
|
|
1568
|
+
if (fromSource && existsSync4(join10(fromSource, "dist", "index.html"))) {
|
|
1569
|
+
return join10(fromSource, "dist");
|
|
922
1570
|
}
|
|
923
1571
|
if (!opts.version) {
|
|
924
1572
|
throw new Error(
|
|
@@ -931,17 +1579,17 @@ Set ${ENV_OVERRIDE} to a built dist, or run from the monorepo.`
|
|
|
931
1579
|
return fetchBundleFn({ version: opts.version, cacheDir: opts.cacheDir });
|
|
932
1580
|
}
|
|
933
1581
|
function webSourcePackage(cliRoot) {
|
|
934
|
-
const workspaceRoot =
|
|
935
|
-
const candidate =
|
|
936
|
-
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;
|
|
937
1585
|
}
|
|
938
1586
|
|
|
939
1587
|
// src/web/pin.ts
|
|
940
|
-
import { readFileSync as
|
|
941
|
-
import { join as
|
|
1588
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1589
|
+
import { join as join11 } from "path";
|
|
942
1590
|
function readZoonWebPin(cliRoot) {
|
|
943
1591
|
try {
|
|
944
|
-
const pkg = JSON.parse(
|
|
1592
|
+
const pkg = JSON.parse(readFileSync7(join11(cliRoot, "package.json"), "utf8"));
|
|
945
1593
|
return pkg.zooid?.webVersion;
|
|
946
1594
|
} catch {
|
|
947
1595
|
return void 0;
|
|
@@ -949,9 +1597,9 @@ function readZoonWebPin(cliRoot) {
|
|
|
949
1597
|
}
|
|
950
1598
|
|
|
951
1599
|
// src/web/static.ts
|
|
952
|
-
import { readFileSync as
|
|
953
|
-
import { extname, join as
|
|
954
|
-
import { Hono as
|
|
1600
|
+
import { readFileSync as readFileSync8, statSync } from "fs";
|
|
1601
|
+
import { extname, join as join12, normalize } from "path";
|
|
1602
|
+
import { Hono as Hono3 } from "hono";
|
|
955
1603
|
var MIME = {
|
|
956
1604
|
".html": "text/html; charset=utf-8",
|
|
957
1605
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -969,26 +1617,33 @@ function isAssetPath(p) {
|
|
|
969
1617
|
return p.startsWith("/assets/") || /\.[a-z0-9]+$/i.test(p);
|
|
970
1618
|
}
|
|
971
1619
|
function webStatic(opts) {
|
|
972
|
-
const app = new
|
|
973
|
-
app.get(
|
|
1620
|
+
const app = new Hono3();
|
|
1621
|
+
app.get(
|
|
1622
|
+
"/config.json",
|
|
1623
|
+
(c) => c.json({
|
|
1624
|
+
homeserver_url: opts.homeserverUrl,
|
|
1625
|
+
...opts.pushGatewayUrl ? { push_gateway_url: opts.pushGatewayUrl } : {},
|
|
1626
|
+
...opts.vapidPublicKey ? { vapid_public_key: opts.vapidPublicKey } : {}
|
|
1627
|
+
})
|
|
1628
|
+
);
|
|
974
1629
|
app.get("*", (c) => {
|
|
975
1630
|
const url = new URL(c.req.url);
|
|
976
1631
|
const requested = decodeURIComponent(url.pathname);
|
|
977
1632
|
const wantFile = requested === "/" ? "/index.html" : requested;
|
|
978
1633
|
const safe = normalize(wantFile).replace(/^(\.\.[/\\])+/g, "");
|
|
979
|
-
const filePath =
|
|
1634
|
+
const filePath = join12(opts.webRoot, safe);
|
|
980
1635
|
if (!filePath.startsWith(opts.webRoot)) return c.notFound();
|
|
981
1636
|
try {
|
|
982
1637
|
const stat = statSync(filePath);
|
|
983
1638
|
if (stat.isFile()) {
|
|
984
|
-
const body =
|
|
1639
|
+
const body = readFileSync8(filePath);
|
|
985
1640
|
const ct = MIME[extname(filePath)] ?? "application/octet-stream";
|
|
986
1641
|
return c.body(body, 200, { "content-type": ct });
|
|
987
1642
|
}
|
|
988
1643
|
} catch {
|
|
989
1644
|
}
|
|
990
1645
|
if (isAssetPath(requested)) return c.notFound();
|
|
991
|
-
const indexBytes =
|
|
1646
|
+
const indexBytes = readFileSync8(join12(opts.webRoot, "index.html"));
|
|
992
1647
|
return c.body(indexBytes, 200, {
|
|
993
1648
|
"content-type": MIME[".html"]
|
|
994
1649
|
});
|
|
@@ -998,11 +1653,11 @@ function webStatic(opts) {
|
|
|
998
1653
|
|
|
999
1654
|
// src/web/watch.ts
|
|
1000
1655
|
import { spawn as spawn2 } from "child_process";
|
|
1001
|
-
import { existsSync as
|
|
1002
|
-
import { join as
|
|
1656
|
+
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
1657
|
+
import { join as join13 } from "path";
|
|
1003
1658
|
async function startWebWatch(opts) {
|
|
1004
|
-
const distPath =
|
|
1005
|
-
const indexPath =
|
|
1659
|
+
const distPath = join13(opts.webPackageDir, "dist");
|
|
1660
|
+
const indexPath = join13(distPath, "index.html");
|
|
1006
1661
|
const timeoutMs = opts.firstBuildTimeoutMs ?? 6e4;
|
|
1007
1662
|
const spawnTime = Date.now();
|
|
1008
1663
|
const child = spawn2(
|
|
@@ -1036,7 +1691,7 @@ async function startWebWatch(opts) {
|
|
|
1036
1691
|
attachStdio();
|
|
1037
1692
|
throw new Error(`vite build --watch exited before first build (code ${child.exitCode})`);
|
|
1038
1693
|
}
|
|
1039
|
-
if (
|
|
1694
|
+
if (existsSync5(indexPath)) {
|
|
1040
1695
|
const m = statSync2(indexPath).mtimeMs;
|
|
1041
1696
|
if (m >= spawnTime - 500) return makeHandle();
|
|
1042
1697
|
}
|
|
@@ -1056,12 +1711,12 @@ async function startWebWatch(opts) {
|
|
|
1056
1711
|
async function stopChild(child) {
|
|
1057
1712
|
if (child.exitCode !== null) return;
|
|
1058
1713
|
child.kill("SIGTERM");
|
|
1059
|
-
await new Promise((
|
|
1714
|
+
await new Promise((resolve9) => {
|
|
1060
1715
|
let done = false;
|
|
1061
1716
|
const finish = () => {
|
|
1062
1717
|
if (done) return;
|
|
1063
1718
|
done = true;
|
|
1064
|
-
|
|
1719
|
+
resolve9();
|
|
1065
1720
|
};
|
|
1066
1721
|
child.once("exit", finish);
|
|
1067
1722
|
setTimeout(() => {
|
|
@@ -1075,8 +1730,8 @@ async function stopChild(child) {
|
|
|
1075
1730
|
}
|
|
1076
1731
|
|
|
1077
1732
|
// src/observability/paths.ts
|
|
1078
|
-
import { mkdir as mkdir2, readdir, rm, symlink, unlink
|
|
1079
|
-
import { join as
|
|
1733
|
+
import { mkdir as mkdir2, readdir, rm, symlink, unlink } from "fs/promises";
|
|
1734
|
+
import { join as join14 } from "path";
|
|
1080
1735
|
function localDateSlug(d) {
|
|
1081
1736
|
const y = d.getFullYear();
|
|
1082
1737
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
@@ -1086,32 +1741,32 @@ function localDateSlug(d) {
|
|
|
1086
1741
|
var DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1087
1742
|
function resolveLogPaths({ dataDir, now }) {
|
|
1088
1743
|
const slug = localDateSlug(now ?? /* @__PURE__ */ new Date());
|
|
1089
|
-
const logsDir =
|
|
1090
|
-
const dayDir =
|
|
1744
|
+
const logsDir = join14(dataDir, "logs");
|
|
1745
|
+
const dayDir = join14(logsDir, slug);
|
|
1091
1746
|
return {
|
|
1092
1747
|
dataDir,
|
|
1093
1748
|
logsDir,
|
|
1094
1749
|
dayDir,
|
|
1095
1750
|
daySlug: slug,
|
|
1096
|
-
todayLink:
|
|
1097
|
-
tuwunelLog:
|
|
1098
|
-
daemonLog:
|
|
1099
|
-
devLog:
|
|
1100
|
-
agentLog: (n) =>
|
|
1101
|
-
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`)
|
|
1102
1757
|
};
|
|
1103
1758
|
}
|
|
1104
1759
|
async function ensureDayFolder(p) {
|
|
1105
1760
|
await mkdir2(p.dayDir, { recursive: true });
|
|
1106
1761
|
try {
|
|
1107
|
-
await
|
|
1762
|
+
await unlink(p.todayLink);
|
|
1108
1763
|
} catch {
|
|
1109
1764
|
}
|
|
1110
1765
|
await symlink(p.daySlug, p.todayLink);
|
|
1111
1766
|
}
|
|
1112
1767
|
async function pruneOldDays(opts) {
|
|
1113
1768
|
if (opts.retainDays <= 0) return [];
|
|
1114
|
-
const logsDir =
|
|
1769
|
+
const logsDir = join14(opts.dataDir, "logs");
|
|
1115
1770
|
let entries;
|
|
1116
1771
|
try {
|
|
1117
1772
|
entries = await readdir(logsDir);
|
|
@@ -1130,7 +1785,7 @@ async function pruneOldDays(opts) {
|
|
|
1130
1785
|
const [y, m, d] = name.split("-").map(Number);
|
|
1131
1786
|
const t = new Date(y, m - 1, d).getTime();
|
|
1132
1787
|
if (t < cutoff) {
|
|
1133
|
-
await rm(
|
|
1788
|
+
await rm(join14(logsDir, name), { recursive: true, force: true });
|
|
1134
1789
|
removed.push(name);
|
|
1135
1790
|
}
|
|
1136
1791
|
}
|
|
@@ -1140,14 +1795,14 @@ async function pruneOldDays(opts) {
|
|
|
1140
1795
|
// src/observability/file-sink.ts
|
|
1141
1796
|
import { createWriteStream } from "fs";
|
|
1142
1797
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
1143
|
-
import { dirname as
|
|
1798
|
+
import { dirname as dirname5 } from "path";
|
|
1144
1799
|
var TRUNC_MARKER = "\u2026[truncated]";
|
|
1145
1800
|
var JsonlSink = class {
|
|
1146
1801
|
constructor(path, opts = {}) {
|
|
1147
1802
|
this.path = path;
|
|
1148
1803
|
this.maxStringLen = opts.maxStringLen ?? 4096;
|
|
1149
1804
|
this.readyPromise = (async () => {
|
|
1150
|
-
await mkdir3(
|
|
1805
|
+
await mkdir3(dirname5(this.path), { recursive: true });
|
|
1151
1806
|
this.stream = createWriteStream(this.path, { flags: "a" });
|
|
1152
1807
|
})();
|
|
1153
1808
|
}
|
|
@@ -1159,14 +1814,14 @@ var JsonlSink = class {
|
|
|
1159
1814
|
await this.readyPromise;
|
|
1160
1815
|
const capped = capStrings(obj, this.maxStringLen);
|
|
1161
1816
|
const line = JSON.stringify(capped) + "\n";
|
|
1162
|
-
return new Promise((
|
|
1163
|
-
this.stream.write(line, (err) => err ? reject(err) :
|
|
1817
|
+
return new Promise((resolve9, reject) => {
|
|
1818
|
+
this.stream.write(line, (err) => err ? reject(err) : resolve9());
|
|
1164
1819
|
});
|
|
1165
1820
|
}
|
|
1166
1821
|
async close() {
|
|
1167
1822
|
await this.readyPromise;
|
|
1168
|
-
return new Promise((
|
|
1169
|
-
this.stream.end(() =>
|
|
1823
|
+
return new Promise((resolve9) => {
|
|
1824
|
+
this.stream.end(() => resolve9());
|
|
1170
1825
|
});
|
|
1171
1826
|
}
|
|
1172
1827
|
};
|
|
@@ -1234,15 +1889,15 @@ function wireAgentCapture(opts) {
|
|
|
1234
1889
|
// src/observability/capture-tuwunel.ts
|
|
1235
1890
|
import { createWriteStream as createWriteStream2 } from "fs";
|
|
1236
1891
|
import { mkdir as mkdir4 } from "fs/promises";
|
|
1237
|
-
import { dirname as
|
|
1892
|
+
import { dirname as dirname6 } from "path";
|
|
1238
1893
|
function captureChildToFile(child, path) {
|
|
1239
1894
|
return (async () => {
|
|
1240
|
-
await mkdir4(
|
|
1895
|
+
await mkdir4(dirname6(path), { recursive: true });
|
|
1241
1896
|
const stream = createWriteStream2(path, { flags: "a" });
|
|
1242
1897
|
if (child.stdout) child.stdout.on("data", (b) => stream.write(b));
|
|
1243
1898
|
if (child.stderr) child.stderr.on("data", (b) => stream.write(b));
|
|
1244
|
-
await new Promise((
|
|
1245
|
-
child.on("exit", () => stream.end(() =>
|
|
1899
|
+
await new Promise((resolve9) => {
|
|
1900
|
+
child.on("exit", () => stream.end(() => resolve9()));
|
|
1246
1901
|
});
|
|
1247
1902
|
})();
|
|
1248
1903
|
}
|
|
@@ -1275,17 +1930,17 @@ function buildShutdown(layers) {
|
|
|
1275
1930
|
|
|
1276
1931
|
// src/commands/dev.ts
|
|
1277
1932
|
var CLI_ROOT = (() => {
|
|
1278
|
-
let dir =
|
|
1933
|
+
let dir = dirname7(fileURLToPath(import.meta.url));
|
|
1279
1934
|
for (let i = 0; i < 8; i++) {
|
|
1280
|
-
const pkgPath =
|
|
1281
|
-
if (
|
|
1935
|
+
const pkgPath = resolve4(dir, "package.json");
|
|
1936
|
+
if (existsSync6(pkgPath)) {
|
|
1282
1937
|
try {
|
|
1283
|
-
const pkg = JSON.parse(
|
|
1938
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
1284
1939
|
if (pkg.name === "zooid" || pkg.name === "@zooid/cli") return dir;
|
|
1285
1940
|
} catch {
|
|
1286
1941
|
}
|
|
1287
1942
|
}
|
|
1288
|
-
const parent =
|
|
1943
|
+
const parent = dirname7(dir);
|
|
1289
1944
|
if (parent === dir) break;
|
|
1290
1945
|
dir = parent;
|
|
1291
1946
|
}
|
|
@@ -1296,15 +1951,15 @@ async function runDev(flags) {
|
|
|
1296
1951
|
const found = findConfigFile(cwd);
|
|
1297
1952
|
if (!found) throw new Error(`zooid.yaml not found in ${cwd}`);
|
|
1298
1953
|
loadEnvFiles(cwd);
|
|
1299
|
-
const dataRoot =
|
|
1954
|
+
const dataRoot = resolve4(cwd, flags.dataDir);
|
|
1300
1955
|
const layout = resolveDataLayout(dataRoot);
|
|
1301
1956
|
const paths = resolvePaths(layout.matrixDir);
|
|
1302
1957
|
const logPaths = resolveLogPaths({ dataDir: layout.dataRoot });
|
|
1303
1958
|
const tokens = ensureTokens(paths.envPath);
|
|
1304
1959
|
process.env.MATRIX_AS_TOKEN = tokens.asToken;
|
|
1305
1960
|
process.env.MATRIX_HS_TOKEN = tokens.hsToken;
|
|
1306
|
-
const rawYaml =
|
|
1307
|
-
const preview = loadZooidConfig(rawYaml, { configDir:
|
|
1961
|
+
const rawYaml = readFileSync9(found.path, "utf8");
|
|
1962
|
+
const preview = loadZooidConfig(rawYaml, { configDir: dirname7(found.path) });
|
|
1308
1963
|
const matrix = findMatrixTransport(preview);
|
|
1309
1964
|
if (!matrix) {
|
|
1310
1965
|
throw new Error("zooid.yaml: zooid dev requires at least one matrix transport");
|
|
@@ -1399,9 +2054,9 @@ async function runDev(flags) {
|
|
|
1399
2054
|
{
|
|
1400
2055
|
title: "Start @zooid/web watcher (vite build --watch)",
|
|
1401
2056
|
task: async () => {
|
|
1402
|
-
const pkgDir = typeof flags.watchWeb === "string" ?
|
|
2057
|
+
const pkgDir = typeof flags.watchWeb === "string" ? resolve4(flags.watchWeb) : webSourcePackage(CLI_ROOT);
|
|
1403
2058
|
if (!pkgDir) {
|
|
1404
|
-
const defaultPath =
|
|
2059
|
+
const defaultPath = dirname7(dirname7(dirname7(CLI_ROOT))) + "/zooid-clients/packages/web";
|
|
1405
2060
|
throw new Error(
|
|
1406
2061
|
`--watch-web: @zooid/web not found at ${defaultPath}.
|
|
1407
2062
|
Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
|
|
@@ -1416,13 +2071,26 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
|
|
|
1416
2071
|
task: async (_, t) => {
|
|
1417
2072
|
const webRoot = ctx.webWatch?.distPath ?? await ensureWebRoot({
|
|
1418
2073
|
cliRoot: CLI_ROOT,
|
|
1419
|
-
cacheDir:
|
|
2074
|
+
cacheDir: join15(layout.dataRoot, "web"),
|
|
1420
2075
|
version: readZoonWebPin(CLI_ROOT),
|
|
1421
2076
|
onProgress: (msg) => {
|
|
1422
2077
|
t.output = msg;
|
|
1423
2078
|
}
|
|
1424
2079
|
});
|
|
1425
|
-
const app = webStatic({
|
|
2080
|
+
const app = webStatic({
|
|
2081
|
+
webRoot,
|
|
2082
|
+
homeserverUrl: homeserver,
|
|
2083
|
+
...ctx.daemon?.vapidPublicKey ? {
|
|
2084
|
+
// Tuwunel runs in a container; `localhost` here would
|
|
2085
|
+
// resolve to the container, not the daemon. Same shorthand
|
|
2086
|
+
// as the AS registration url
|
|
2087
|
+
// (bootstrap/registration-url.ts). This URL is stored in
|
|
2088
|
+
// the pusher and fetched by the homeserver — the browser
|
|
2089
|
+
// never requests it.
|
|
2090
|
+
pushGatewayUrl: `http://host.docker.internal:${ctx.daemon.port}/_matrix/push/v1/notify`,
|
|
2091
|
+
vapidPublicKey: ctx.daemon.vapidPublicKey
|
|
2092
|
+
} : {}
|
|
2093
|
+
});
|
|
1426
2094
|
ctx.uiServer = serve2({ fetch: app.fetch, port: flags.uiPort });
|
|
1427
2095
|
}
|
|
1428
2096
|
}
|
|
@@ -1455,13 +2123,25 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
|
|
|
1455
2123
|
}
|
|
1456
2124
|
});
|
|
1457
2125
|
if (flags.installSignalHandlers !== false) {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
2126
|
+
let interrupts = 0;
|
|
2127
|
+
const onSignal = () => {
|
|
2128
|
+
interrupts += 1;
|
|
2129
|
+
if (interrupts === 1) {
|
|
2130
|
+
process.stdout.write(chalk.dim("\nStopping\u2026\n"));
|
|
2131
|
+
void shutdown().then(() => process.exit(0));
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
if (interrupts === 2) {
|
|
2135
|
+
process.stdout.write(
|
|
2136
|
+
chalk.dim("Still stopping \u2014 press Ctrl-C again to force quit.\n")
|
|
2137
|
+
);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
process.stdout.write(chalk.dim("Forced.\n"));
|
|
2141
|
+
process.exit(130);
|
|
1462
2142
|
};
|
|
1463
|
-
process.on("SIGINT",
|
|
1464
|
-
process.on("SIGTERM",
|
|
2143
|
+
process.on("SIGINT", onSignal);
|
|
2144
|
+
process.on("SIGTERM", onSignal);
|
|
1465
2145
|
}
|
|
1466
2146
|
process.stdout.write(
|
|
1467
2147
|
[
|
|
@@ -1483,8 +2163,8 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
|
|
|
1483
2163
|
}
|
|
1484
2164
|
function loadEnvFiles(cwd) {
|
|
1485
2165
|
for (const name of [".env.local", ".env"]) {
|
|
1486
|
-
const path =
|
|
1487
|
-
if (!
|
|
2166
|
+
const path = resolve4(cwd, name);
|
|
2167
|
+
if (!existsSync6(path)) continue;
|
|
1488
2168
|
try {
|
|
1489
2169
|
process.loadEnvFile(path);
|
|
1490
2170
|
} catch (err) {
|
|
@@ -1494,9 +2174,9 @@ function loadEnvFiles(cwd) {
|
|
|
1494
2174
|
}
|
|
1495
2175
|
|
|
1496
2176
|
// src/commands/init.ts
|
|
1497
|
-
import { existsSync as
|
|
2177
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync2, symlinkSync, writeFileSync as writeFileSync8 } from "fs";
|
|
1498
2178
|
import { homedir as homedir2 } from "os";
|
|
1499
|
-
import { dirname as
|
|
2179
|
+
import { dirname as dirname8, join as join17, resolve as resolve5 } from "path";
|
|
1500
2180
|
|
|
1501
2181
|
// src/commands/init/generators.ts
|
|
1502
2182
|
function generateZooidYaml(opts) {
|
|
@@ -1677,20 +2357,20 @@ function findPiProvider(id) {
|
|
|
1677
2357
|
}
|
|
1678
2358
|
|
|
1679
2359
|
// src/commands/init/sniff.ts
|
|
1680
|
-
import { existsSync as
|
|
2360
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
|
|
1681
2361
|
import { homedir } from "os";
|
|
1682
|
-
import { join as
|
|
2362
|
+
import { join as join16 } from "path";
|
|
1683
2363
|
function sniffCredentials(preset, home = homedir()) {
|
|
1684
2364
|
const rel = preset === "pi" ? PI_AUTH_FILE : findSimplePreset(preset)?.credentialDir;
|
|
1685
2365
|
if (!rel) return { found: false };
|
|
1686
|
-
const full =
|
|
1687
|
-
if (
|
|
2366
|
+
const full = join16(home, rel);
|
|
2367
|
+
if (existsSync7(full)) return { found: true, path: full };
|
|
1688
2368
|
return { found: false };
|
|
1689
2369
|
}
|
|
1690
2370
|
function sniffPiDefaults(home = homedir()) {
|
|
1691
|
-
const source =
|
|
2371
|
+
const source = join16(home, PI_SETTINGS_FILE);
|
|
1692
2372
|
try {
|
|
1693
|
-
const raw = JSON.parse(
|
|
2373
|
+
const raw = JSON.parse(readFileSync10(source, "utf8"));
|
|
1694
2374
|
const provider = raw.defaultProvider;
|
|
1695
2375
|
const model = raw.defaultModel;
|
|
1696
2376
|
if (typeof provider !== "string" || !provider) return void 0;
|
|
@@ -1714,8 +2394,8 @@ var IGNORED_PREEXISTING = /* @__PURE__ */ new Set([
|
|
|
1714
2394
|
"yarn.lock"
|
|
1715
2395
|
]);
|
|
1716
2396
|
async function runInit(opts) {
|
|
1717
|
-
const dir =
|
|
1718
|
-
|
|
2397
|
+
const dir = resolve5(opts.dir);
|
|
2398
|
+
mkdirSync8(dir, { recursive: true });
|
|
1719
2399
|
if (!opts.force) {
|
|
1720
2400
|
const blocking = readdirSync2(dir).filter((n) => !IGNORED_PREEXISTING.has(n));
|
|
1721
2401
|
if (blocking.length > 0) {
|
|
@@ -1811,14 +2491,14 @@ async function runInit(opts) {
|
|
|
1811
2491
|
}
|
|
1812
2492
|
writes.push({ path: ".gitignore", content: generateGitignore() });
|
|
1813
2493
|
for (const w of writes) {
|
|
1814
|
-
const full =
|
|
1815
|
-
const exists =
|
|
2494
|
+
const full = join17(dir, w.path);
|
|
2495
|
+
const exists = existsSync8(full);
|
|
1816
2496
|
if (exists && !opts.overwrite) {
|
|
1817
2497
|
console.warn(`\u26A0 ${w.path} exists; left as-is (use --force --overwrite to replace)`);
|
|
1818
2498
|
continue;
|
|
1819
2499
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
2500
|
+
mkdirSync8(dirname8(full), { recursive: true });
|
|
2501
|
+
writeFileSync8(full, w.content);
|
|
1822
2502
|
console.log(`\u2713 Created ${w.path}`);
|
|
1823
2503
|
}
|
|
1824
2504
|
if ((opts.preset === "claude" || opts.preset === "codex") && opts.auth === "subscription") {
|
|
@@ -1843,10 +2523,10 @@ async function runInit(opts) {
|
|
|
1843
2523
|
}
|
|
1844
2524
|
const s = sniffCredentials("pi", opts.home);
|
|
1845
2525
|
if (opts.auth === "subscription" && s.found) {
|
|
1846
|
-
const authSource =
|
|
1847
|
-
const linkPath =
|
|
1848
|
-
if (!
|
|
1849
|
-
|
|
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 });
|
|
1850
2530
|
symlinkSync(authSource, linkPath);
|
|
1851
2531
|
}
|
|
1852
2532
|
console.log(
|
|
@@ -1978,8 +2658,8 @@ async function resolveOptions(flags) {
|
|
|
1978
2658
|
|
|
1979
2659
|
// src/commands/logs.ts
|
|
1980
2660
|
import { readFile, readdir as readdir2, readlink } from "fs/promises";
|
|
1981
|
-
import { existsSync as
|
|
1982
|
-
import { join as
|
|
2661
|
+
import { existsSync as existsSync9 } from "fs";
|
|
2662
|
+
import { join as join18 } from "path";
|
|
1983
2663
|
var KNOWN_SOURCES = ["tuwunel", "daemon", "dev"];
|
|
1984
2664
|
async function runLogs(flags) {
|
|
1985
2665
|
const writer = flags.writer ?? ((s) => process.stdout.write(s));
|
|
@@ -1998,8 +2678,8 @@ async function runLogs(flags) {
|
|
|
1998
2678
|
writer("no logs yet\n");
|
|
1999
2679
|
return;
|
|
2000
2680
|
}
|
|
2001
|
-
const dayDir =
|
|
2002
|
-
if (!
|
|
2681
|
+
const dayDir = join18(flags.dataDir, "logs", day);
|
|
2682
|
+
if (!existsSync9(dayDir)) {
|
|
2003
2683
|
writer(`no logs for ${day}
|
|
2004
2684
|
`);
|
|
2005
2685
|
return;
|
|
@@ -2019,7 +2699,7 @@ async function runLogs(flags) {
|
|
|
2019
2699
|
return;
|
|
2020
2700
|
}
|
|
2021
2701
|
const path = resolveSourcePath(dayDir, flags.source);
|
|
2022
|
-
if (!
|
|
2702
|
+
if (!existsSync9(path)) {
|
|
2023
2703
|
writer(`no such source: ${flags.source}
|
|
2024
2704
|
`);
|
|
2025
2705
|
return;
|
|
@@ -2027,7 +2707,7 @@ async function runLogs(flags) {
|
|
|
2027
2707
|
writer(await readFile(path, "utf8"));
|
|
2028
2708
|
}
|
|
2029
2709
|
async function resolveTodaySlug(dataDir) {
|
|
2030
|
-
const link =
|
|
2710
|
+
const link = join18(dataDir, "logs", "today");
|
|
2031
2711
|
try {
|
|
2032
2712
|
return await readlink(link);
|
|
2033
2713
|
} catch {
|
|
@@ -2036,18 +2716,18 @@ async function resolveTodaySlug(dataDir) {
|
|
|
2036
2716
|
}
|
|
2037
2717
|
function resolveSourcePath(dayDir, source) {
|
|
2038
2718
|
if (source.startsWith("agent-")) {
|
|
2039
|
-
if (source.endsWith(".acp")) return
|
|
2040
|
-
return
|
|
2719
|
+
if (source.endsWith(".acp")) return join18(dayDir, `${source.slice(0, -4)}.acp.jsonl`);
|
|
2720
|
+
return join18(dayDir, `${source}.log`);
|
|
2041
2721
|
}
|
|
2042
2722
|
if (KNOWN_SOURCES.includes(source))
|
|
2043
|
-
return
|
|
2044
|
-
return
|
|
2723
|
+
return join18(dayDir, `${source}.log`);
|
|
2724
|
+
return join18(dayDir, source);
|
|
2045
2725
|
}
|
|
2046
2726
|
async function dumpByTurn(dayDir, turnId, writer) {
|
|
2047
2727
|
const entries = await readdir2(dayDir);
|
|
2048
2728
|
const taps = entries.filter((e) => e.endsWith(".acp.jsonl")).sort();
|
|
2049
2729
|
for (const f of taps) {
|
|
2050
|
-
const text = await readFile(
|
|
2730
|
+
const text = await readFile(join18(dayDir, f), "utf8");
|
|
2051
2731
|
for (const line of text.split("\n")) {
|
|
2052
2732
|
if (!line) continue;
|
|
2053
2733
|
try {
|
|
@@ -2061,14 +2741,14 @@ async function dumpByTurn(dayDir, turnId, writer) {
|
|
|
2061
2741
|
|
|
2062
2742
|
// src/commands/start.ts
|
|
2063
2743
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2064
|
-
import { resolve as
|
|
2744
|
+
import { resolve as resolve6 } from "path";
|
|
2065
2745
|
async function runStart(flags) {
|
|
2066
2746
|
if (flags.printToken) {
|
|
2067
2747
|
process.stdout.write(`${randomBytes2(32).toString("hex")}
|
|
2068
2748
|
`);
|
|
2069
2749
|
return;
|
|
2070
2750
|
}
|
|
2071
|
-
const dataRoot =
|
|
2751
|
+
const dataRoot = resolve6(process.cwd(), flags.dataDir ?? "./data");
|
|
2072
2752
|
const layout = resolveDataLayout(dataRoot);
|
|
2073
2753
|
const handle = await startDaemon({
|
|
2074
2754
|
cliFlags: flags,
|
|
@@ -2081,9 +2761,17 @@ async function runStart(flags) {
|
|
|
2081
2761
|
}
|
|
2082
2762
|
|
|
2083
2763
|
// src/commands/status.ts
|
|
2084
|
-
import { readFileSync as
|
|
2085
|
-
import { dirname as
|
|
2764
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
2765
|
+
import { dirname as dirname9, join as join19, resolve as resolve7 } from "path";
|
|
2086
2766
|
import chalk2 from "chalk";
|
|
2767
|
+
function readVapidPublicKey(dataDir) {
|
|
2768
|
+
try {
|
|
2769
|
+
const parsed = JSON.parse(readFileSync11(join19(dataDir, VAPID_FILENAME), "utf8"));
|
|
2770
|
+
return typeof parsed.publicKey === "string" ? parsed.publicKey : void 0;
|
|
2771
|
+
} catch {
|
|
2772
|
+
return void 0;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2087
2775
|
async function probe(url, timeoutMs = 2e3) {
|
|
2088
2776
|
try {
|
|
2089
2777
|
const r = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
@@ -2098,16 +2786,18 @@ async function collectStatus(opts) {
|
|
|
2098
2786
|
status: tuwunelUp ? "up" : "down",
|
|
2099
2787
|
url: opts.tuwunelUrl
|
|
2100
2788
|
};
|
|
2789
|
+
const vapidPublicKey = opts.dataDir ? readVapidPublicKey(opts.dataDir) : void 0;
|
|
2101
2790
|
const found = findConfigFile(opts.cwd);
|
|
2102
2791
|
if (!found) {
|
|
2103
2792
|
return {
|
|
2104
2793
|
tuwunel,
|
|
2105
2794
|
daemon: { status: "unknown", reason: "no zooid.yaml" },
|
|
2106
|
-
agents: []
|
|
2795
|
+
agents: [],
|
|
2796
|
+
...vapidPublicKey ? { vapidPublicKey } : {}
|
|
2107
2797
|
};
|
|
2108
2798
|
}
|
|
2109
|
-
const cfg = loadZooidConfig(
|
|
2110
|
-
configDir:
|
|
2799
|
+
const cfg = loadZooidConfig(readFileSync11(found.path, "utf8"), {
|
|
2800
|
+
configDir: dirname9(found.path)
|
|
2111
2801
|
});
|
|
2112
2802
|
const matrixEntry = Object.entries(cfg.transports).find(
|
|
2113
2803
|
([, t]) => t.type === "matrix"
|
|
@@ -2128,7 +2818,8 @@ async function collectStatus(opts) {
|
|
|
2128
2818
|
return {
|
|
2129
2819
|
tuwunel,
|
|
2130
2820
|
daemon: { status: daemonUp ? "up" : "down", url: daemonUrl },
|
|
2131
|
-
agents
|
|
2821
|
+
agents,
|
|
2822
|
+
...vapidPublicKey ? { vapidPublicKey } : {}
|
|
2132
2823
|
};
|
|
2133
2824
|
}
|
|
2134
2825
|
async function runStatus(flags) {
|
|
@@ -2137,8 +2828,8 @@ async function runStatus(flags) {
|
|
|
2137
2828
|
if (port === void 0) {
|
|
2138
2829
|
const found = findConfigFile(cwd);
|
|
2139
2830
|
if (found) {
|
|
2140
|
-
const cfg = loadZooidConfig(
|
|
2141
|
-
configDir:
|
|
2831
|
+
const cfg = loadZooidConfig(readFileSync11(found.path, "utf8"), {
|
|
2832
|
+
configDir: dirname9(found.path)
|
|
2142
2833
|
});
|
|
2143
2834
|
const matrix = findMatrixTransport(cfg);
|
|
2144
2835
|
if (matrix) {
|
|
@@ -2148,7 +2839,8 @@ async function runStatus(flags) {
|
|
|
2148
2839
|
}
|
|
2149
2840
|
}
|
|
2150
2841
|
const tuwunelUrl = `http://localhost:${port ?? 8448}`;
|
|
2151
|
-
const
|
|
2842
|
+
const dataDir = resolve7(cwd, flags.dataDir ?? "./data");
|
|
2843
|
+
const s = await collectStatus({ cwd, tuwunelUrl, dataDir });
|
|
2152
2844
|
const fmt = (st) => st === "up" ? chalk2.green("up") : st === "down" ? chalk2.red("down") : chalk2.yellow("unknown");
|
|
2153
2845
|
process.stdout.write(
|
|
2154
2846
|
[
|
|
@@ -2157,14 +2849,29 @@ async function runStatus(flags) {
|
|
|
2157
2849
|
...s.agents.map(
|
|
2158
2850
|
(a) => ` agent: ${a.name} (${a.userId}, trigger: ${a.trigger})`
|
|
2159
2851
|
),
|
|
2852
|
+
...s.vapidPublicKey ? [`vapid public key: ${s.vapidPublicKey}`] : [],
|
|
2160
2853
|
""
|
|
2161
2854
|
].join("\n")
|
|
2162
2855
|
);
|
|
2163
2856
|
}
|
|
2164
2857
|
|
|
2858
|
+
// src/version.ts
|
|
2859
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
2860
|
+
function readCliVersion(url = import.meta.url) {
|
|
2861
|
+
try {
|
|
2862
|
+
const manifest = new URL("../package.json", url);
|
|
2863
|
+
const raw = JSON.parse(readFileSync12(manifest, "utf8"));
|
|
2864
|
+
if (typeof raw.version === "string" && raw.version) return raw.version;
|
|
2865
|
+
return "unknown";
|
|
2866
|
+
} catch {
|
|
2867
|
+
return "unknown";
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
var CLI_VERSION = readCliVersion();
|
|
2871
|
+
|
|
2165
2872
|
// src/bin.ts
|
|
2166
2873
|
var cli = cac("zooid");
|
|
2167
|
-
cli.command("start", "Run the daemon (production entry-point)").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--runtime <local|docker|podman>", "Agent runtime").option("--image <ref>", "Agent container image").option("--print-token", "Print a 32-byte hex token and exit").action(async (flags) => {
|
|
2874
|
+
cli.command("start", "Run the daemon (production entry-point)").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--runtime <local|docker|podman>", "Agent runtime").option("--image <ref>", "Agent container image").option("--print-token", "Print a 32-byte hex token and exit").example("$ zooid start --data ./data").example("$ zooid start --runtime docker --image ghcr.io/zooid-ai/agent:latest").action(async (flags) => {
|
|
2168
2875
|
await runStart({
|
|
2169
2876
|
dataDir: flags.data,
|
|
2170
2877
|
runtime: flags.runtime,
|
|
@@ -2175,7 +2882,7 @@ cli.command("start", "Run the daemon (production entry-point)").option("--data <
|
|
|
2175
2882
|
cli.command("dev", "Tuwunel + daemon + UI for local development").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--engine <docker|podman>", "Container engine", { default: "docker" }).option("--ui-port <n>", "UI HTTP port", { default: 5173 }).option("--admin-user <name>", "Admin username", { default: "admin" }).option("--admin-password <pw>", "Admin password", { default: "admin" }).option(
|
|
2176
2883
|
"--watch-web [path]",
|
|
2177
2884
|
"Run vite build --watch on @zooid/web. Path defaults to sibling ../zooid-clients/packages/web."
|
|
2178
|
-
).action(async (flags) => {
|
|
2885
|
+
).example("$ zooid dev").example("$ zooid dev --engine podman --ui-port 5174").action(async (flags) => {
|
|
2179
2886
|
await runDev({
|
|
2180
2887
|
dataDir: flags.data,
|
|
2181
2888
|
engine: flags.engine,
|
|
@@ -2188,30 +2895,30 @@ cli.command("dev", "Tuwunel + daemon + UI for local development").option("--data
|
|
|
2188
2895
|
cli.command(
|
|
2189
2896
|
"logs [source]",
|
|
2190
2897
|
'Read captured logs. source=tuwunel|daemon|dev|agent-<name>[.acp], or "prune" to delete old days'
|
|
2191
|
-
).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 }).action(async (source, flags) => {
|
|
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) => {
|
|
2192
2899
|
if (source === "prune") {
|
|
2193
2900
|
await runLogs({
|
|
2194
|
-
dataDir:
|
|
2901
|
+
dataDir: resolve8(process.cwd(), flags.data),
|
|
2195
2902
|
subcommand: "prune",
|
|
2196
2903
|
keep: Number(flags.keep)
|
|
2197
2904
|
});
|
|
2198
2905
|
return;
|
|
2199
2906
|
}
|
|
2200
2907
|
await runLogs({
|
|
2201
|
-
dataDir:
|
|
2908
|
+
dataDir: resolve8(process.cwd(), flags.data),
|
|
2202
2909
|
source,
|
|
2203
2910
|
day: flags.day,
|
|
2204
2911
|
turn: flags.turn,
|
|
2205
2912
|
follow: Boolean(flags.follow)
|
|
2206
2913
|
});
|
|
2207
2914
|
});
|
|
2208
|
-
cli.command("status", "Print Tuwunel + daemon health").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--port <n>", "Tuwunel host port (defaults to zooid.yaml)").action(async (flags) => {
|
|
2915
|
+
cli.command("status", "Print Tuwunel + daemon health").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--port <n>", "Tuwunel host port (defaults to zooid.yaml)").example("$ zooid status").action(async (flags) => {
|
|
2209
2916
|
await runStatus({
|
|
2210
2917
|
dataDir: flags.data,
|
|
2211
2918
|
port: flags.port !== void 0 ? Number(flags.port) : void 0
|
|
2212
2919
|
});
|
|
2213
2920
|
});
|
|
2214
|
-
cli.command("init [dir]", "Scaffold a new zooid workforce in the current (or named) directory").option("--preset <name>", "claude | codex | opencode | pi").option("--auth <mode>", "subscription | api-key (claude/codex/pi only)").option("--model <id>", "Model identifier").option("--provider <id>", "opencode provider: opencode-go | opencode | anthropic | openrouter | custom; pi provider: openrouter | anthropic | openai").option("--api-key <value>", "API key (api-key path; opencode always)").option("--force", "Allow scaffolding into a non-empty directory").option("--overwrite", "With --force, overwrite existing files").option("--no-interactive", "Disable prompts; require all flags up front").action(async (dir, flags) => {
|
|
2921
|
+
cli.command("init [dir]", "Scaffold a new zooid workforce in the current (or named) directory").option("--preset <name>", "claude | codex | opencode | pi").option("--auth <mode>", "subscription | api-key (claude/codex/pi only)").option("--model <id>", "Model identifier").option("--provider <id>", "opencode provider: opencode-go | opencode | anthropic | openrouter | custom; pi provider: openrouter | anthropic | openai").option("--api-key <value>", "API key (api-key path; opencode always)").option("--force", "Allow scaffolding into a non-empty directory").option("--overwrite", "With --force, overwrite existing files").option("--no-interactive", "Disable prompts; require all flags up front").example("$ zooid init").example("$ zooid init my-workforce --preset opencode --provider anthropic").example("$ zooid init --preset claude --auth api-key --api-key sk-... --no-interactive").action(async (dir, flags) => {
|
|
2215
2922
|
const resolved = await resolveOptions({
|
|
2216
2923
|
dir: dir ?? process.cwd(),
|
|
2217
2924
|
preset: flags.preset,
|
|
@@ -2225,7 +2932,39 @@ cli.command("init [dir]", "Scaffold a new zooid workforce in the current (or nam
|
|
|
2225
2932
|
});
|
|
2226
2933
|
await runInit(resolved);
|
|
2227
2934
|
});
|
|
2228
|
-
cli.help()
|
|
2229
|
-
|
|
2230
|
-
cli.
|
|
2935
|
+
cli.command("help [command]", "Display help for zooid, or for a specific command").example("$ zooid help").example("$ zooid help init").action((commandName) => {
|
|
2936
|
+
if (!commandName) {
|
|
2937
|
+
cli.globalCommand.outputHelp();
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
const target = cli.commands.find((c) => c.isMatched(commandName));
|
|
2941
|
+
if (!target) {
|
|
2942
|
+
console.error(`Unknown command: ${commandName}
|
|
2943
|
+
`);
|
|
2944
|
+
cli.globalCommand.outputHelp();
|
|
2945
|
+
process.exitCode = 1;
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
target.outputHelp();
|
|
2949
|
+
});
|
|
2950
|
+
cli.help((sections) => {
|
|
2951
|
+
sections.push({
|
|
2952
|
+
title: "Docs",
|
|
2953
|
+
body: " https://zooid.dev/docs"
|
|
2954
|
+
});
|
|
2955
|
+
return sections;
|
|
2956
|
+
});
|
|
2957
|
+
cli.version(CLI_VERSION);
|
|
2958
|
+
cli.on("command:*", () => {
|
|
2959
|
+
console.error(`Unknown command: ${cli.args.join(" ")}
|
|
2960
|
+
`);
|
|
2961
|
+
cli.outputHelp();
|
|
2962
|
+
process.exitCode = 1;
|
|
2963
|
+
});
|
|
2964
|
+
if (process.argv.slice(2).length === 0) {
|
|
2965
|
+
cli.outputHelp();
|
|
2966
|
+
process.exitCode = 1;
|
|
2967
|
+
} else {
|
|
2968
|
+
cli.parse();
|
|
2969
|
+
}
|
|
2231
2970
|
//# sourceMappingURL=bin.js.map
|