@cecwxf/dsh-room 0.3.2 → 0.3.4
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 +31 -0
- package/dist/client.js +72 -48
- package/dist/index.js +490 -184
- package/dist/types/adapter.d.ts +1 -0
- package/dist/types/adapter.d.ts.map +1 -1
- package/dist/types/approval-queue.d.ts +1 -0
- package/dist/types/approval-queue.d.ts.map +1 -1
- package/dist/types/http.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -9
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/mcp-workspace.d.ts.map +1 -1
- package/dist/types/models.d.ts +14 -0
- package/dist/types/models.d.ts.map +1 -0
- package/dist/types/orchestrator.d.ts +4 -1
- package/dist/types/orchestrator.d.ts.map +1 -1
- package/dist/types/plugin.d.ts +20 -1
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/state.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -416,8 +416,8 @@ __export(mcp_workspace_exports, {
|
|
|
416
416
|
MCP_TOOL_DEFS: () => MCP_TOOL_DEFS,
|
|
417
417
|
WorkspaceMcp: () => WorkspaceMcp
|
|
418
418
|
});
|
|
419
|
-
import { mkdir as mkdir3, readFile as readFile3, readdir, rm, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
420
|
-
import { dirname as dirname2, join as join2, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
419
|
+
import { lstat, mkdir as mkdir3, readFile as readFile3, readdir, realpath, rm, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
420
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
421
421
|
import { spawn } from "node:child_process";
|
|
422
422
|
function actionFor(tool) {
|
|
423
423
|
if (tool === "workspace_write" || tool === "workspace_mkdir") return "fs.write";
|
|
@@ -482,16 +482,27 @@ var init_mcp_workspace = __esm({
|
|
|
482
482
|
const root = resolve2(this.cfg.workspacePath);
|
|
483
483
|
const target = resolve2(root, rel || ".");
|
|
484
484
|
const relToRoot = relative2(root, target);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
485
|
+
const outside = (p) => isAbsolute2(p) || p === ".." || p.startsWith(`..${sep}`);
|
|
486
|
+
if (outside(relToRoot)) throw new Error("path escapes workspace");
|
|
487
|
+
const realRoot = await realpath(root);
|
|
488
|
+
let ancestor = resolve2(realRoot, relToRoot);
|
|
489
|
+
const suffix = [];
|
|
490
|
+
while (true) {
|
|
491
|
+
let real;
|
|
492
|
+
try {
|
|
493
|
+
real = await realpath(ancestor);
|
|
494
|
+
} catch (e) {
|
|
495
|
+
if (e.code !== "ENOENT") throw e;
|
|
496
|
+
if ((await lstat(ancestor).catch(() => null))?.isSymbolicLink()) throw new Error("path escapes workspace (dangling symlink)");
|
|
497
|
+
const parent = dirname2(ancestor);
|
|
498
|
+
if (parent === ancestor) throw e;
|
|
499
|
+
suffix.unshift(relative2(parent, ancestor));
|
|
500
|
+
ancestor = parent;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (outside(relative2(realRoot, real))) throw new Error("path escapes workspace (symlink)");
|
|
504
|
+
return resolve2(real, ...suffix);
|
|
493
505
|
}
|
|
494
|
-
return target;
|
|
495
506
|
}
|
|
496
507
|
async maybeApprove(tool, summary, detail, from) {
|
|
497
508
|
const action = actionFor(tool);
|
|
@@ -503,7 +514,7 @@ var init_mcp_workspace = __esm({
|
|
|
503
514
|
if (cap) {
|
|
504
515
|
const grant = resolvePeerGrant(this.cfg, from, cap);
|
|
505
516
|
if (grant === "deny") throw new Error(`peer grant deny: ${cap} for ${from}`);
|
|
506
|
-
if (grant === "auto") return;
|
|
517
|
+
if (grant === "auto" && (cap !== "read" || this.cfg.permissions.autoRead)) return;
|
|
507
518
|
}
|
|
508
519
|
if (!action && cap === "read") {
|
|
509
520
|
const decision2 = await this.approvals.request({
|
|
@@ -541,6 +552,7 @@ var init_mcp_workspace = __esm({
|
|
|
541
552
|
await mkdir3(this.cfg.workspacePath, { recursive: true });
|
|
542
553
|
switch (tool) {
|
|
543
554
|
case "workspace_list": {
|
|
555
|
+
await this.maybeApprove(tool, `list ${args.path ?? "."}`, "", from);
|
|
544
556
|
const dir = await this.resolveSafe(String(args.path ?? "."));
|
|
545
557
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
546
558
|
return {
|
|
@@ -549,18 +561,20 @@ var init_mcp_workspace = __esm({
|
|
|
549
561
|
};
|
|
550
562
|
}
|
|
551
563
|
case "workspace_read": {
|
|
552
|
-
|
|
564
|
+
await this.maybeApprove(tool, `read ${args.path}`, "", from);
|
|
553
565
|
const file = await this.resolveSafe(String(args.path ?? ""));
|
|
554
566
|
const buf = await readFile3(file);
|
|
555
567
|
if (buf.length > 256 * 1024) throw new Error("file too large (>256KiB)");
|
|
556
568
|
return { path: String(args.path), content: buf.toString("utf8") };
|
|
557
569
|
}
|
|
558
570
|
case "workspace_stat": {
|
|
571
|
+
await this.maybeApprove(tool, `stat ${args.path}`, "", from);
|
|
559
572
|
const file = await this.resolveSafe(String(args.path ?? ""));
|
|
560
573
|
const st = await stat2(file);
|
|
561
574
|
return { path: String(args.path), size: st.size, mtimeMs: st.mtimeMs, isDirectory: st.isDirectory(), isFile: st.isFile() };
|
|
562
575
|
}
|
|
563
576
|
case "workspace_search": {
|
|
577
|
+
await this.maybeApprove(tool, `search ${args.path ?? "."}`, String(args.query ?? ""), from);
|
|
564
578
|
const q = String(args.query ?? "");
|
|
565
579
|
if (!q) throw new Error("query required");
|
|
566
580
|
const start = await this.resolveSafe(String(args.path ?? "."));
|
|
@@ -630,9 +644,22 @@ var init_mcp_workspace = __esm({
|
|
|
630
644
|
}
|
|
631
645
|
});
|
|
632
646
|
|
|
647
|
+
// src/models.ts
|
|
648
|
+
function resolveSelection(defaults, room = {}, agent = {}) {
|
|
649
|
+
return {
|
|
650
|
+
provider: agent.provider || room.provider || defaults.provider,
|
|
651
|
+
model: agent.model || room.model || defaults.model,
|
|
652
|
+
reasoningEffort: agent.reasoningEffort || room.reasoningEffort || defaults.reasoningEffort
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function describeModel(selection, catalog) {
|
|
656
|
+
if (!selection.provider || !selection.model) return null;
|
|
657
|
+
return catalog.find((m) => m.provider === selection.provider && m.model === selection.model) ?? { provider: selection.provider, model: selection.model, name: selection.model };
|
|
658
|
+
}
|
|
659
|
+
|
|
633
660
|
// src/plugin.ts
|
|
634
|
-
import { randomUUID as
|
|
635
|
-
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile4, appendFile as appendFile2 } from "node:fs/promises";
|
|
661
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
662
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile4, appendFile as appendFile2, rename as rename2, rm as rm3 } from "node:fs/promises";
|
|
636
663
|
import { dirname as dirname4, join as join3 } from "node:path";
|
|
637
664
|
|
|
638
665
|
// ../protocol/dist/types.js
|
|
@@ -1229,7 +1256,7 @@ var WsTransport = class {
|
|
|
1229
1256
|
return;
|
|
1230
1257
|
this.teardownSocket();
|
|
1231
1258
|
const gen = ++this.generation;
|
|
1232
|
-
await new Promise((
|
|
1259
|
+
await new Promise((resolve4, reject) => {
|
|
1233
1260
|
const ws = new WebSocket(this.url);
|
|
1234
1261
|
this.ws = ws;
|
|
1235
1262
|
let settled = false;
|
|
@@ -1250,7 +1277,7 @@ var WsTransport = class {
|
|
|
1250
1277
|
reject(err);
|
|
1251
1278
|
} else {
|
|
1252
1279
|
this._connected = true;
|
|
1253
|
-
|
|
1280
|
+
resolve4();
|
|
1254
1281
|
}
|
|
1255
1282
|
};
|
|
1256
1283
|
const timer = setTimeout(() => finish(new Error(`WebSocket connect timeout (${timeoutMs}ms) to ${this.url}`)), timeoutMs);
|
|
@@ -1340,6 +1367,8 @@ var CollabClient = class _CollabClient {
|
|
|
1340
1367
|
statusHandlers = [];
|
|
1341
1368
|
waiters = [];
|
|
1342
1369
|
joined = false;
|
|
1370
|
+
hasJoined = false;
|
|
1371
|
+
reconnecting;
|
|
1343
1372
|
disposed = false;
|
|
1344
1373
|
capabilities;
|
|
1345
1374
|
reconnect;
|
|
@@ -1361,9 +1390,11 @@ var CollabClient = class _CollabClient {
|
|
|
1361
1390
|
this.transport.onClose(() => {
|
|
1362
1391
|
this.joined = false;
|
|
1363
1392
|
this.setConnStatus("disconnected", "ws_closed");
|
|
1364
|
-
if (this.reconnect && !this.disposed) {
|
|
1393
|
+
if (this.reconnect && !this.disposed && !this.reconnecting) {
|
|
1365
1394
|
this.setConnStatus("connecting", "reconnect");
|
|
1366
|
-
|
|
1395
|
+
this.reconnecting = this.reconnectLoop().finally(() => {
|
|
1396
|
+
this.reconnecting = void 0;
|
|
1397
|
+
});
|
|
1367
1398
|
}
|
|
1368
1399
|
});
|
|
1369
1400
|
}
|
|
@@ -1441,7 +1472,15 @@ var CollabClient = class _CollabClient {
|
|
|
1441
1472
|
await this.transport.connect();
|
|
1442
1473
|
}
|
|
1443
1474
|
async join(timeoutMs = 1e4) {
|
|
1475
|
+
if (this.disposed)
|
|
1476
|
+
throw new Error("disposed");
|
|
1477
|
+
if (this.joined)
|
|
1478
|
+
return;
|
|
1479
|
+
const catchUp = this.hasJoined || !!this.cursor;
|
|
1480
|
+
const since = this.cursor?.get() ?? this.seq;
|
|
1444
1481
|
await this.connect();
|
|
1482
|
+
if (this.disposed)
|
|
1483
|
+
throw new Error("disposed");
|
|
1445
1484
|
const challengeP = this.waitFor((f) => f.type === "ctrl" && (f.method === "challenge" || f.method === "error"), timeoutMs);
|
|
1446
1485
|
this.transport.send({
|
|
1447
1486
|
v: 1,
|
|
@@ -1481,14 +1520,16 @@ var CollabClient = class _CollabClient {
|
|
|
1481
1520
|
throw new Error("expected joined");
|
|
1482
1521
|
}
|
|
1483
1522
|
this.joined = true;
|
|
1523
|
+
this.hasJoined = true;
|
|
1484
1524
|
this.peers = joined.peers;
|
|
1485
1525
|
this.seq = joined.seq;
|
|
1486
1526
|
this.setConnStatus("connected");
|
|
1487
1527
|
for (const h of this.peerHandlers)
|
|
1488
1528
|
h(this.peers);
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1529
|
+
if (catchUp && since < joined.seq) {
|
|
1530
|
+
const missed = await this.pollFrames(since);
|
|
1531
|
+
for (const frame of missed.frames)
|
|
1532
|
+
await this.handleFwd(frame);
|
|
1492
1533
|
}
|
|
1493
1534
|
this.startPing();
|
|
1494
1535
|
}
|
|
@@ -1583,6 +1624,18 @@ var CollabClient = class _CollabClient {
|
|
|
1583
1624
|
}
|
|
1584
1625
|
async poll(sinceSeq) {
|
|
1585
1626
|
const since = sinceSeq ?? this.cursor?.get() ?? 0;
|
|
1627
|
+
const res = await this.pollFrames(since);
|
|
1628
|
+
const out = [];
|
|
1629
|
+
for (const fwd of res.frames) {
|
|
1630
|
+
const p = await this.decryptFwd(fwd);
|
|
1631
|
+
if (p)
|
|
1632
|
+
out.push(p);
|
|
1633
|
+
}
|
|
1634
|
+
this.seq = Math.max(this.seq, res.seq);
|
|
1635
|
+
this.cursor?.set(this.seq);
|
|
1636
|
+
return out;
|
|
1637
|
+
}
|
|
1638
|
+
async pollFrames(since) {
|
|
1586
1639
|
const resP = this.waitFor((f) => f.type === "ctrl" && f.method === "poll_result", 5e3);
|
|
1587
1640
|
this.transport.send({
|
|
1588
1641
|
v: 1,
|
|
@@ -1594,16 +1647,8 @@ var CollabClient = class _CollabClient {
|
|
|
1594
1647
|
});
|
|
1595
1648
|
const res = await resP;
|
|
1596
1649
|
if (res.type !== "ctrl" || res.method !== "poll_result")
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
for (const fwd of res.frames) {
|
|
1600
|
-
const p = await this.decryptFwd(fwd);
|
|
1601
|
-
if (p)
|
|
1602
|
-
out.push(p);
|
|
1603
|
-
}
|
|
1604
|
-
this.seq = Math.max(this.seq, res.seq);
|
|
1605
|
-
this.cursor?.set(this.seq);
|
|
1606
|
-
return out;
|
|
1650
|
+
throw new Error("expected poll_result");
|
|
1651
|
+
return res;
|
|
1607
1652
|
}
|
|
1608
1653
|
async history(sinceSeq = 0) {
|
|
1609
1654
|
return this.poll(sinceSeq);
|
|
@@ -1651,20 +1696,23 @@ var CollabClient = class _CollabClient {
|
|
|
1651
1696
|
}
|
|
1652
1697
|
async decryptFwd(frame) {
|
|
1653
1698
|
try {
|
|
1699
|
+
if (frame.roomId !== this.roomId)
|
|
1700
|
+
return null;
|
|
1654
1701
|
const aad = new TextEncoder().encode(`${frame.roomId}:${frame.from}`);
|
|
1655
1702
|
const plain = await decrypt(this.roomKey, frame.nonce, frame.ciphertext, aad);
|
|
1656
|
-
|
|
1703
|
+
const payload = decodePayload(plain);
|
|
1704
|
+
return payload.sender === frame.from ? payload : null;
|
|
1657
1705
|
} catch {
|
|
1658
1706
|
return null;
|
|
1659
1707
|
}
|
|
1660
1708
|
}
|
|
1661
1709
|
waitFor(pred, timeoutMs) {
|
|
1662
|
-
const p = new Promise((
|
|
1710
|
+
const p = new Promise((resolve4, reject) => {
|
|
1663
1711
|
const timer = setTimeout(() => {
|
|
1664
1712
|
this.waiters = this.waiters.filter((w) => w.timer !== timer);
|
|
1665
1713
|
reject(new Error("timeout waiting for frame"));
|
|
1666
1714
|
}, timeoutMs);
|
|
1667
|
-
this.waiters.push({ pred, resolve:
|
|
1715
|
+
this.waiters.push({ pred, resolve: resolve4, reject, timer });
|
|
1668
1716
|
});
|
|
1669
1717
|
p.catch(() => {
|
|
1670
1718
|
});
|
|
@@ -1818,6 +1866,7 @@ var RunScheduler = class {
|
|
|
1818
1866
|
runs = [];
|
|
1819
1867
|
reqs = /* @__PURE__ */ new Map();
|
|
1820
1868
|
aborts = /* @__PURE__ */ new Map();
|
|
1869
|
+
gates = /* @__PURE__ */ new Map();
|
|
1821
1870
|
idleWaiters = [];
|
|
1822
1871
|
enqueue(req) {
|
|
1823
1872
|
const run = {
|
|
@@ -1835,7 +1884,10 @@ var RunScheduler = class {
|
|
|
1835
1884
|
this.reqs.set(run.id, req);
|
|
1836
1885
|
this.changed();
|
|
1837
1886
|
if (req.gate) {
|
|
1838
|
-
req.gate
|
|
1887
|
+
const gate = req.gate;
|
|
1888
|
+
const ac = new AbortController();
|
|
1889
|
+
this.gates.set(run.id, ac);
|
|
1890
|
+
Promise.resolve().then(() => ac.signal.aborted ? false : gate(ac.signal)).then(
|
|
1839
1891
|
(ok2) => {
|
|
1840
1892
|
if (run.state !== "approval") return;
|
|
1841
1893
|
if (ok2) run.state = "queued";
|
|
@@ -1844,8 +1896,12 @@ var RunScheduler = class {
|
|
|
1844
1896
|
},
|
|
1845
1897
|
(e) => {
|
|
1846
1898
|
if (run.state === "approval") this.finish(run, "failed", e.message);
|
|
1899
|
+
this.pump();
|
|
1847
1900
|
}
|
|
1848
|
-
)
|
|
1901
|
+
).finally(() => {
|
|
1902
|
+
this.gates.delete(run.id);
|
|
1903
|
+
this.pump();
|
|
1904
|
+
});
|
|
1849
1905
|
} else this.pump();
|
|
1850
1906
|
return run;
|
|
1851
1907
|
}
|
|
@@ -1853,11 +1909,14 @@ var RunScheduler = class {
|
|
|
1853
1909
|
list(roomId, finished = 8) {
|
|
1854
1910
|
const mine = this.runs.filter((r) => !roomId || r.roomId === roomId);
|
|
1855
1911
|
const active = mine.filter((r) => r.state === "approval" || r.state === "queued" || r.state === "running");
|
|
1856
|
-
const done = mine.filter((r) => !active.includes(r)).slice(-finished);
|
|
1912
|
+
const done = finished > 0 ? mine.filter((r) => !active.includes(r)).slice(-finished) : [];
|
|
1857
1913
|
return [...active, ...done].map((r) => ({ ...r }));
|
|
1858
1914
|
}
|
|
1859
1915
|
running(roomId) {
|
|
1860
|
-
return this.runs.filter((r) => r.roomId === roomId && r.
|
|
1916
|
+
return this.runs.filter((r) => r.roomId === roomId && this.aborts.has(r.id)).length;
|
|
1917
|
+
}
|
|
1918
|
+
isExecuting(roomId, agentId) {
|
|
1919
|
+
return this.runs.some((r) => r.roomId === roomId && r.agentId === agentId && this.aborts.has(r.id));
|
|
1861
1920
|
}
|
|
1862
1921
|
/** Stop every queued/running run in a room (optionally one agent). Returns how many were stopped. */
|
|
1863
1922
|
stop(roomId, agentId) {
|
|
@@ -1865,6 +1924,7 @@ var RunScheduler = class {
|
|
|
1865
1924
|
for (const r of this.runs) {
|
|
1866
1925
|
if (r.roomId !== roomId || agentId && r.agentId !== agentId) continue;
|
|
1867
1926
|
if (r.state === "approval" || r.state === "queued") {
|
|
1927
|
+
this.gates.get(r.id)?.abort();
|
|
1868
1928
|
this.finish(r, "stopped", "stopped by user");
|
|
1869
1929
|
n++;
|
|
1870
1930
|
} else if (r.state === "running") {
|
|
@@ -1878,18 +1938,18 @@ var RunScheduler = class {
|
|
|
1878
1938
|
}
|
|
1879
1939
|
/** Resolves when no run is approval/queued/running (tests, e2e). */
|
|
1880
1940
|
whenIdle() {
|
|
1881
|
-
if (!this.
|
|
1941
|
+
if (!this.busy()) return Promise.resolve();
|
|
1882
1942
|
return new Promise((r) => this.idleWaiters.push(r));
|
|
1883
1943
|
}
|
|
1884
1944
|
pump() {
|
|
1885
1945
|
for (const run of this.runs) {
|
|
1886
1946
|
if (run.state !== "queued") continue;
|
|
1887
1947
|
if (this.running(run.roomId) >= Math.max(1, this.limitFor(run.roomId))) continue;
|
|
1888
|
-
if (this.
|
|
1948
|
+
if (this.isExecuting(run.roomId, run.agentId)) continue;
|
|
1889
1949
|
this.start(run);
|
|
1890
1950
|
}
|
|
1891
1951
|
this.changed();
|
|
1892
|
-
if (!this.
|
|
1952
|
+
if (!this.busy()) {
|
|
1893
1953
|
const w = this.idleWaiters;
|
|
1894
1954
|
this.idleWaiters = [];
|
|
1895
1955
|
for (const fn of w) fn();
|
|
@@ -1901,7 +1961,9 @@ var RunScheduler = class {
|
|
|
1901
1961
|
run.state = "running";
|
|
1902
1962
|
run.startedAt = Date.now();
|
|
1903
1963
|
const req = this.reqs.get(run.id);
|
|
1904
|
-
Promise.resolve().then(() =>
|
|
1964
|
+
Promise.resolve().then(() => {
|
|
1965
|
+
if (!ac.signal.aborted) return this.exec(run, req, ac.signal);
|
|
1966
|
+
}).then(
|
|
1905
1967
|
() => run.state === "running" && this.finish(run, "done"),
|
|
1906
1968
|
(e) => run.state === "running" && this.finish(run, ac.signal.aborted ? "stopped" : "failed", e?.message ?? String(e))
|
|
1907
1969
|
).finally(() => {
|
|
@@ -1914,9 +1976,17 @@ var RunScheduler = class {
|
|
|
1914
1976
|
run.endedAt = Date.now();
|
|
1915
1977
|
if (error) run.error = error;
|
|
1916
1978
|
this.reqs.delete(run.id);
|
|
1917
|
-
if (this.runs.length > 400)
|
|
1979
|
+
if (this.runs.length > 400) {
|
|
1980
|
+
const active = this.runs.filter((r) => r.state === "approval" || r.state === "queued" || this.aborts.has(r.id) || this.gates.has(r.id));
|
|
1981
|
+
const finished = this.runs.filter((r) => !active.includes(r)).slice(-300);
|
|
1982
|
+
const keep = /* @__PURE__ */ new Set([...active, ...finished]);
|
|
1983
|
+
this.runs = this.runs.filter((r) => keep.has(r));
|
|
1984
|
+
}
|
|
1918
1985
|
this.changed();
|
|
1919
1986
|
}
|
|
1987
|
+
busy() {
|
|
1988
|
+
return this.aborts.size > 0 || this.gates.size > 0 || this.runs.some((r) => r.state === "approval" || r.state === "queued");
|
|
1989
|
+
}
|
|
1920
1990
|
changed() {
|
|
1921
1991
|
try {
|
|
1922
1992
|
this.onChange();
|
|
@@ -1936,19 +2006,23 @@ import { WebSocketServer } from "ws";
|
|
|
1936
2006
|
import { WebSocket as WebSocket2 } from "ws";
|
|
1937
2007
|
var RING = 1e3;
|
|
1938
2008
|
var RATE = 40;
|
|
2009
|
+
var MAX_CIPHERTEXT = 256 * 1024;
|
|
1939
2010
|
var LanRoom = class {
|
|
1940
2011
|
roomId;
|
|
1941
2012
|
socks = /* @__PURE__ */ new Set();
|
|
1942
2013
|
seq = 0;
|
|
1943
2014
|
ring = [];
|
|
1944
2015
|
roomRate = [];
|
|
2016
|
+
identities = /* @__PURE__ */ new Map();
|
|
1945
2017
|
constructor(roomId) {
|
|
1946
2018
|
this.roomId = roomId;
|
|
1947
2019
|
}
|
|
1948
2020
|
attach(ws) {
|
|
1949
|
-
const s = { ws, joined: false, stamps: [] };
|
|
2021
|
+
const s = { ws, joined: false, stamps: [], processing: Promise.resolve() };
|
|
1950
2022
|
this.socks.add(s);
|
|
1951
|
-
ws.on("message", (data) =>
|
|
2023
|
+
ws.on("message", (data) => {
|
|
2024
|
+
s.processing = s.processing.then(() => this.onMessage(s, data.toString())).catch(() => this.error(s, "bad_frame", "invalid frame"));
|
|
2025
|
+
});
|
|
1952
2026
|
ws.on("close", () => this.onClose(s));
|
|
1953
2027
|
ws.on("error", () => this.onClose(s));
|
|
1954
2028
|
}
|
|
@@ -1965,6 +2039,9 @@ var LanRoom = class {
|
|
|
1965
2039
|
this.send(s.ws, frame);
|
|
1966
2040
|
}
|
|
1967
2041
|
}
|
|
2042
|
+
error(s, code, message) {
|
|
2043
|
+
this.send(s.ws, { v: 1, type: "ctrl", method: "error", code, message, roomId: this.roomId });
|
|
2044
|
+
}
|
|
1968
2045
|
rateOk(stamps, now, limit) {
|
|
1969
2046
|
const recent = stamps.filter((t) => now - t < 1e3);
|
|
1970
2047
|
if (recent.length >= limit)
|
|
@@ -1973,22 +2050,26 @@ var LanRoom = class {
|
|
|
1973
2050
|
return { ok: true, next: recent };
|
|
1974
2051
|
}
|
|
1975
2052
|
async onMessage(s, text2) {
|
|
1976
|
-
const now = Date.now();
|
|
1977
|
-
const rr = this.rateOk(this.roomRate, now, 200);
|
|
1978
|
-
this.roomRate = rr.next;
|
|
1979
|
-
if (!rr.ok)
|
|
1980
|
-
return;
|
|
1981
|
-
const sr = this.rateOk(s.stamps, now, RATE);
|
|
1982
|
-
s.stamps = sr.next;
|
|
1983
|
-
if (!sr.ok)
|
|
1984
|
-
return;
|
|
1985
2053
|
for (const line of text2.split("\n")) {
|
|
2054
|
+
if (!this.socks.has(s))
|
|
2055
|
+
return;
|
|
1986
2056
|
if (!line.trim())
|
|
1987
2057
|
continue;
|
|
2058
|
+
const now = Date.now();
|
|
2059
|
+
const rr = this.rateOk(this.roomRate, now, 200);
|
|
2060
|
+
this.roomRate = rr.next;
|
|
2061
|
+
const sr = this.rateOk(s.stamps, now, RATE);
|
|
2062
|
+
s.stamps = sr.next;
|
|
2063
|
+
if (!rr.ok || !sr.ok)
|
|
2064
|
+
return this.error(s, "rate_limit", "too many frames");
|
|
1988
2065
|
let frame;
|
|
1989
2066
|
try {
|
|
1990
|
-
|
|
2067
|
+
const parsed = JSON.parse(line);
|
|
2068
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
2069
|
+
throw new Error("not an object");
|
|
2070
|
+
frame = parsed;
|
|
1991
2071
|
} catch {
|
|
2072
|
+
this.error(s, "bad_json", "frame must be a JSON object");
|
|
1992
2073
|
continue;
|
|
1993
2074
|
}
|
|
1994
2075
|
if (frame.v !== 1)
|
|
@@ -2009,11 +2090,19 @@ var LanRoom = class {
|
|
|
2009
2090
|
}
|
|
2010
2091
|
if (frame.type === "ctrl" && frame.method === "poll") {
|
|
2011
2092
|
const since = frame.sinceSeq ?? 0;
|
|
2093
|
+
if (!Number.isSafeInteger(since) || since < 0) {
|
|
2094
|
+
this.error(s, "bad_poll", "invalid sinceSeq");
|
|
2095
|
+
continue;
|
|
2096
|
+
}
|
|
2012
2097
|
const frames = this.ring.filter((f) => f.seq > since);
|
|
2013
2098
|
this.send(s.ws, { v: 1, type: "ctrl", method: "poll_result", roomId: this.roomId, frames, seq: this.seq });
|
|
2014
2099
|
continue;
|
|
2015
2100
|
}
|
|
2016
2101
|
if (frame.type === "fwd") {
|
|
2102
|
+
if (frame.roomId !== this.roomId || frame.from !== s.agentId || typeof frame.ciphertext !== "string" || !frame.ciphertext || frame.ciphertext.length > MAX_CIPHERTEXT || typeof frame.nonce !== "string" || !/^[A-Za-z0-9_-]{16}$/.test(frame.nonce)) {
|
|
2103
|
+
this.error(s, "bad_frame", "invalid encrypted frame");
|
|
2104
|
+
continue;
|
|
2105
|
+
}
|
|
2017
2106
|
this.seq += 1;
|
|
2018
2107
|
const fwd = { ...frame, seq: this.seq, roomId: this.roomId, from: s.agentId };
|
|
2019
2108
|
this.ring.push(fwd);
|
|
@@ -2028,23 +2117,44 @@ var LanRoom = class {
|
|
|
2028
2117
|
this.send(s.ws, { v: 1, type: "ctrl", method: "error", code: "bad_room", message: "room mismatch", roomId: this.roomId });
|
|
2029
2118
|
return;
|
|
2030
2119
|
}
|
|
2031
|
-
if (!
|
|
2032
|
-
s
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
s
|
|
2120
|
+
if (typeof frame.agentId !== "string" || !frame.agentId || frame.agentId.length > 128 || typeof frame.publicKey !== "string" || !frame.publicKey || frame.publicKey.length > 256 || typeof frame.challengeResp !== "string" || frame.challengeResp.length > 256 || frame.capabilities !== void 0 && (!Array.isArray(frame.capabilities) || frame.capabilities.length > 32 || frame.capabilities.some((c) => typeof c !== "string" || c.length > 128))) {
|
|
2121
|
+
return this.error(s, "bad_join", "invalid identity or capabilities");
|
|
2122
|
+
}
|
|
2123
|
+
const knownKey = this.identities.get(frame.agentId);
|
|
2124
|
+
if (knownKey && knownKey !== frame.publicKey)
|
|
2125
|
+
return this.error(s, "identity_conflict", "agentId belongs to another public key in this room");
|
|
2126
|
+
if (s.joined) {
|
|
2127
|
+
if (frame.agentId !== s.agentId || frame.publicKey !== s.publicKey)
|
|
2128
|
+
return this.error(s, "identity_locked", "joined socket identity cannot change");
|
|
2129
|
+
this.send(s.ws, { v: 1, type: "ctrl", method: "joined", roomId: this.roomId, agentId: s.agentId, peers: this.members(), seq: this.seq });
|
|
2037
2130
|
return;
|
|
2038
2131
|
}
|
|
2039
|
-
|
|
2132
|
+
if (!s.pending || !frame.challengeResp || s.pending.agentId !== frame.agentId || s.pending.publicKey !== frame.publicKey) {
|
|
2133
|
+
s.pending = { challenge: b64uEncode(randomBytes(32)), agentId: frame.agentId, publicKey: frame.publicKey };
|
|
2134
|
+
this.send(s.ws, { v: 1, type: "ctrl", method: "challenge", roomId: this.roomId, challenge: s.pending.challenge });
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
const pending = s.pending;
|
|
2138
|
+
s.pending = void 0;
|
|
2139
|
+
const ok2 = await verifyChallenge(frame.publicKey, pending.challenge, frame.challengeResp).catch(() => false);
|
|
2140
|
+
if (!this.socks.has(s))
|
|
2141
|
+
return;
|
|
2040
2142
|
if (!ok2) {
|
|
2041
2143
|
this.send(s.ws, { v: 1, type: "ctrl", method: "error", code: "bad_sig", message: "challenge failed", roomId: this.roomId });
|
|
2042
2144
|
return;
|
|
2043
2145
|
}
|
|
2146
|
+
const verifiedKey = this.identities.get(frame.agentId);
|
|
2147
|
+
if (verifiedKey && verifiedKey !== frame.publicKey)
|
|
2148
|
+
return this.error(s, "identity_conflict", "agentId belongs to another public key in this room");
|
|
2149
|
+
this.identities.set(frame.agentId, frame.publicKey);
|
|
2044
2150
|
s.agentId = frame.agentId;
|
|
2045
2151
|
s.publicKey = frame.publicKey;
|
|
2046
2152
|
s.capabilities = frame.capabilities;
|
|
2047
2153
|
s.joined = true;
|
|
2154
|
+
for (const old of [...this.socks]) {
|
|
2155
|
+
if (old !== s && old.joined && old.agentId === s.agentId)
|
|
2156
|
+
this.onClose(old);
|
|
2157
|
+
}
|
|
2048
2158
|
const peers = this.members();
|
|
2049
2159
|
this.send(s.ws, { v: 1, type: "ctrl", method: "joined", roomId: this.roomId, agentId: s.agentId, peers, seq: this.seq });
|
|
2050
2160
|
this.broadcast({ v: 1, type: "ctrl", method: "peer_update", roomId: this.roomId, peers, event: "joined", agentId: s.agentId }, s.ws);
|
|
@@ -2158,10 +2268,10 @@ function browseLanRelays(opts) {
|
|
|
2158
2268
|
}
|
|
2159
2269
|
});
|
|
2160
2270
|
dns.query({ questions: [{ name: MDNS_SERVICE, type: "PTR" }] });
|
|
2161
|
-
return new Promise((
|
|
2271
|
+
return new Promise((resolve4) => {
|
|
2162
2272
|
setTimeout(() => {
|
|
2163
2273
|
dns.destroy();
|
|
2164
|
-
|
|
2274
|
+
resolve4([...found.values()]);
|
|
2165
2275
|
}, timeoutMs);
|
|
2166
2276
|
});
|
|
2167
2277
|
}
|
|
@@ -2472,19 +2582,33 @@ var LanRelayServer = class {
|
|
|
2472
2582
|
res.writeHead(200, { "content-type": "text/plain" });
|
|
2473
2583
|
res.end("Collab LAN / VPS Relay\nWS: /ws/:roomId\nDirectory: GET|POST /directory/rooms, DELETE /directory/rooms/:roomId\nHealth: /health\nmDNS: _collab._tcp.local\n");
|
|
2474
2584
|
});
|
|
2475
|
-
this.wss = new WebSocketServer({ noServer: true });
|
|
2585
|
+
this.wss = new WebSocketServer({ noServer: true, maxPayload: 1024 * 1024 });
|
|
2476
2586
|
this.http.on("upgrade", (req, socket, head) => {
|
|
2477
|
-
const m = /^\/ws\/([^/?#]+)
|
|
2587
|
+
const m = /^\/ws\/([^/?#]+)\/?(?:\?[^#]*)?$/.exec(req.url ?? "");
|
|
2478
2588
|
if (!m) {
|
|
2479
2589
|
socket.destroy();
|
|
2480
2590
|
return;
|
|
2481
2591
|
}
|
|
2482
|
-
|
|
2592
|
+
let roomId;
|
|
2593
|
+
try {
|
|
2594
|
+
roomId = decodeURIComponent(m[1]);
|
|
2595
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(roomId))
|
|
2596
|
+
throw new Error("invalid roomId");
|
|
2597
|
+
} catch {
|
|
2598
|
+
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2483
2601
|
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
2484
2602
|
this.room(roomId).attach(ws);
|
|
2485
2603
|
});
|
|
2486
2604
|
});
|
|
2487
|
-
await new Promise((
|
|
2605
|
+
await new Promise((resolve4, reject) => {
|
|
2606
|
+
this.http.once("error", reject);
|
|
2607
|
+
this.http.listen(port, host, () => {
|
|
2608
|
+
this.http.off("error", reject);
|
|
2609
|
+
resolve4();
|
|
2610
|
+
});
|
|
2611
|
+
});
|
|
2488
2612
|
const addr = this.http.address();
|
|
2489
2613
|
this.port = typeof addr === "object" && addr ? addr.port : port;
|
|
2490
2614
|
if (this.opts.mdns !== false) {
|
|
@@ -2494,8 +2618,12 @@ var LanRelayServer = class {
|
|
|
2494
2618
|
}
|
|
2495
2619
|
async close() {
|
|
2496
2620
|
this.stopMdns?.();
|
|
2497
|
-
|
|
2498
|
-
|
|
2621
|
+
for (const ws of this.wss?.clients ?? [])
|
|
2622
|
+
ws.terminate();
|
|
2623
|
+
if (this.wss)
|
|
2624
|
+
await new Promise((resolve4) => this.wss.close(() => resolve4()));
|
|
2625
|
+
if (this.http)
|
|
2626
|
+
await new Promise((resolve4) => this.http.close(() => resolve4()));
|
|
2499
2627
|
}
|
|
2500
2628
|
};
|
|
2501
2629
|
|
|
@@ -2521,8 +2649,9 @@ var ApprovalQueue = class {
|
|
|
2521
2649
|
}
|
|
2522
2650
|
/** Ask for approval; resolves with allow|deny. Auto-denies after timeoutMs. */
|
|
2523
2651
|
request(opts) {
|
|
2652
|
+
if (opts.signal?.aborted) return Promise.resolve("deny");
|
|
2524
2653
|
const requestId = randomUUID();
|
|
2525
|
-
return new Promise((
|
|
2654
|
+
return new Promise((resolve4) => {
|
|
2526
2655
|
const entry = {
|
|
2527
2656
|
requestId,
|
|
2528
2657
|
roomId: opts.roomId,
|
|
@@ -2532,22 +2661,22 @@ var ApprovalQueue = class {
|
|
|
2532
2661
|
from: opts.from,
|
|
2533
2662
|
createdAt: Date.now(),
|
|
2534
2663
|
resolve: (d) => {
|
|
2664
|
+
if (!this.pending.has(requestId)) return;
|
|
2535
2665
|
clearTimeout(timer);
|
|
2666
|
+
opts.signal?.removeEventListener("abort", abort);
|
|
2536
2667
|
this.pending.delete(requestId);
|
|
2537
2668
|
this.emit();
|
|
2538
|
-
|
|
2669
|
+
resolve4(d);
|
|
2539
2670
|
}
|
|
2540
2671
|
};
|
|
2541
|
-
|
|
2542
|
-
this.emit();
|
|
2672
|
+
const abort = () => entry.resolve("deny");
|
|
2543
2673
|
const timer = setTimeout(() => {
|
|
2544
|
-
|
|
2545
|
-
this.pending.delete(requestId);
|
|
2546
|
-
this.emit();
|
|
2547
|
-
resolve3("deny");
|
|
2548
|
-
}
|
|
2674
|
+
entry.resolve("deny");
|
|
2549
2675
|
}, opts.timeoutMs ?? 12e4);
|
|
2550
2676
|
timer.unref?.();
|
|
2677
|
+
this.pending.set(requestId, entry);
|
|
2678
|
+
opts.signal?.addEventListener("abort", abort, { once: true });
|
|
2679
|
+
this.emit();
|
|
2551
2680
|
});
|
|
2552
2681
|
}
|
|
2553
2682
|
decide(requestId, decision) {
|
|
@@ -2564,7 +2693,8 @@ init_member_config();
|
|
|
2564
2693
|
|
|
2565
2694
|
// src/state.ts
|
|
2566
2695
|
import { chmod, mkdir as mkdir4, readFile as readFile4, rename, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
|
|
2567
|
-
import { dirname as dirname3 } from "node:path";
|
|
2696
|
+
import { dirname as dirname3, resolve as resolve3 } from "node:path";
|
|
2697
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2568
2698
|
function newId(prefix) {
|
|
2569
2699
|
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2570
2700
|
}
|
|
@@ -2608,12 +2738,25 @@ async function loadState(path) {
|
|
|
2608
2738
|
return null;
|
|
2609
2739
|
}
|
|
2610
2740
|
}
|
|
2741
|
+
var pendingWrites = /* @__PURE__ */ new Map();
|
|
2611
2742
|
async function saveState(path, state) {
|
|
2743
|
+
path = resolve3(path);
|
|
2744
|
+
const snapshot = structuredClone(state);
|
|
2745
|
+
const pending = (pendingWrites.get(path) ?? Promise.resolve()).catch(() => {
|
|
2746
|
+
}).then(() => writeState(path, snapshot));
|
|
2747
|
+
pendingWrites.set(path, pending);
|
|
2748
|
+
try {
|
|
2749
|
+
await pending;
|
|
2750
|
+
} finally {
|
|
2751
|
+
if (pendingWrites.get(path) === pending) pendingWrites.delete(path);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
async function writeState(path, state) {
|
|
2612
2755
|
const dir = dirname3(path);
|
|
2613
2756
|
await mkdir4(dir, { recursive: true, mode: 448 });
|
|
2614
2757
|
await chmod(dir, 448).catch(() => {
|
|
2615
2758
|
});
|
|
2616
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
2759
|
+
const tmp = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
2617
2760
|
const body = {
|
|
2618
2761
|
version: 3,
|
|
2619
2762
|
activeWorkspaceId: state.activeWorkspaceId,
|
|
@@ -2622,9 +2765,13 @@ async function saveState(path, state) {
|
|
|
2622
2765
|
rooms: state.rooms,
|
|
2623
2766
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2624
2767
|
};
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2768
|
+
try {
|
|
2769
|
+
await writeFile3(tmp, JSON.stringify(body, null, 2), { mode: 384, flag: "wx" });
|
|
2770
|
+
await chmod(tmp, 384);
|
|
2771
|
+
await rename(tmp, path);
|
|
2772
|
+
} finally {
|
|
2773
|
+
await rm2(tmp, { force: true });
|
|
2774
|
+
}
|
|
2628
2775
|
}
|
|
2629
2776
|
|
|
2630
2777
|
// src/commands.ts
|
|
@@ -2768,19 +2915,35 @@ ${HELP}` };
|
|
|
2768
2915
|
}
|
|
2769
2916
|
|
|
2770
2917
|
// src/plugin.ts
|
|
2918
|
+
import { createRequire } from "node:module";
|
|
2919
|
+
var requirePkg = createRequire(import.meta.url);
|
|
2920
|
+
var PLUGIN_VERSION = (() => {
|
|
2921
|
+
try {
|
|
2922
|
+
return requirePkg("../package.json").version;
|
|
2923
|
+
} catch {
|
|
2924
|
+
return "0.3.4";
|
|
2925
|
+
}
|
|
2926
|
+
})();
|
|
2771
2927
|
var SUMMARY_MAX = 120;
|
|
2772
2928
|
var MESSAGES_MAX = 300;
|
|
2773
2929
|
var AGENT_SERVICES = ["agents", "agentDefaultModel", "agentPresets", "permissionPresets", "sessionTitle", "workspaceRegistry"];
|
|
2774
2930
|
var text = (s) => [{ type: "text", text: s }];
|
|
2775
2931
|
var jsonRender = (_args, value) => text(JSON.stringify(value));
|
|
2776
|
-
function withTimeout(p, ms, label) {
|
|
2932
|
+
function withTimeout(p, ms, label, signal) {
|
|
2777
2933
|
let t;
|
|
2934
|
+
let onAbort;
|
|
2778
2935
|
const timeout = new Promise((_, rej) => {
|
|
2779
2936
|
t = setTimeout(() => rej(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
2780
2937
|
t.unref?.();
|
|
2781
2938
|
});
|
|
2782
|
-
|
|
2939
|
+
const aborted = new Promise((_, reject) => {
|
|
2940
|
+
onAbort = () => reject(signal?.reason ?? new Error("stopped by user"));
|
|
2941
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2942
|
+
if (signal?.aborted) onAbort();
|
|
2943
|
+
});
|
|
2944
|
+
return Promise.race([p, timeout, aborted]).finally(() => {
|
|
2783
2945
|
if (t) clearTimeout(t);
|
|
2946
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
2784
2947
|
});
|
|
2785
2948
|
}
|
|
2786
2949
|
async function loadIdentity(path, agentName) {
|
|
@@ -2850,6 +3013,7 @@ var RoomConn = class {
|
|
|
2850
3013
|
locks;
|
|
2851
3014
|
git;
|
|
2852
3015
|
historyTimer;
|
|
3016
|
+
historySave;
|
|
2853
3017
|
announceTimer;
|
|
2854
3018
|
normalized;
|
|
2855
3019
|
/** Normalized once; the same object afterwards so `conn.local.x = …` always sticks. */
|
|
@@ -2911,6 +3075,36 @@ var CollabBridge = class {
|
|
|
2911
3075
|
sessionAgents = /* @__PURE__ */ new Map();
|
|
2912
3076
|
/** Override how a local agent run executes (tests / mock provider). */
|
|
2913
3077
|
localRunner = null;
|
|
3078
|
+
models = [];
|
|
3079
|
+
async refreshModels() {
|
|
3080
|
+
const llm = this.ctx.get("llm", false);
|
|
3081
|
+
if (!llm?.listProviders || !llm?.listModels) return;
|
|
3082
|
+
const results = await Promise.allSettled(llm.listProviders().map(async (p) => {
|
|
3083
|
+
const entries = await withTimeout(llm.listModels(p.id), 5e3, `models ${p.id}`);
|
|
3084
|
+
return entries.map((m) => ({ provider: p.id, model: m.id, name: m.name || m.id }));
|
|
3085
|
+
}));
|
|
3086
|
+
this.models = results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
|
|
3087
|
+
this.emit({ type: "rooms" });
|
|
3088
|
+
}
|
|
3089
|
+
selectedModel(conn, agent) {
|
|
3090
|
+
const defaults = this.ctx.get("agentDefaultModel", false)?.currentSelection?.() ?? {};
|
|
3091
|
+
return resolveSelection(defaults, conn?.meta.memberConfig, agent);
|
|
3092
|
+
}
|
|
3093
|
+
resolvedModel(conn, agent) {
|
|
3094
|
+
return describeModel(this.selectedModel(conn, agent), this.models);
|
|
3095
|
+
}
|
|
3096
|
+
async resetAgentSession(conn, agent) {
|
|
3097
|
+
const key = `${conn.meta.roomId}:${agent.agentId}`;
|
|
3098
|
+
const pending = this.agentHandles.get(key);
|
|
3099
|
+
this.agentHandles.delete(key);
|
|
3100
|
+
const sid = conn.meta.agentSessions?.[agent.agentId];
|
|
3101
|
+
if (sid) this.sessionAgents.delete(sid);
|
|
3102
|
+
if (conn.meta.agentSessions) delete conn.meta.agentSessions[agent.agentId];
|
|
3103
|
+
conn.meta.agentNoEffort = conn.meta.agentNoEffort?.filter((id) => id !== agent.agentId);
|
|
3104
|
+
this.noEffort.delete(key);
|
|
3105
|
+
if (pending) await pending.then((h) => h.dispose()).catch(() => {
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
2914
3108
|
log(level, ...a) {
|
|
2915
3109
|
this.ctx.logger[level]("[collab]", ...a);
|
|
2916
3110
|
const dbg = process.env.DSH_ROOM_DEBUG_LOG;
|
|
@@ -2941,6 +3135,7 @@ var CollabBridge = class {
|
|
|
2941
3135
|
start() {
|
|
2942
3136
|
this.ready ??= (async () => {
|
|
2943
3137
|
await this.self();
|
|
3138
|
+
await this.refreshModels();
|
|
2944
3139
|
const st = await loadState(this.config.statePath);
|
|
2945
3140
|
for (const w of st?.workspaces ?? []) this.workspaces.set(w.id, w);
|
|
2946
3141
|
if (!this.workspaces.size) {
|
|
@@ -2949,14 +3144,26 @@ var CollabBridge = class {
|
|
|
2949
3144
|
}
|
|
2950
3145
|
this.activeWorkspaceId = st?.activeWorkspaceId ?? [...this.workspaces.keys()][0] ?? null;
|
|
2951
3146
|
const metas = st?.rooms ?? [];
|
|
2952
|
-
if (this.config.invite && !metas.some((m) =>
|
|
3147
|
+
if (this.config.invite && !metas.some((m) => m.roomId === CollabClient.parseInvite(this.config.invite).roomId)) {
|
|
2953
3148
|
const { roomId } = CollabClient.parseInvite(this.config.invite);
|
|
2954
3149
|
const wsId = this.activeWorkspaceId;
|
|
2955
3150
|
metas.push({ roomId, invite: this.config.invite, name: roomId, description: "", tags: [], public: false, owner: false, joinedAt: Date.now(), workspaceId: wsId });
|
|
2956
3151
|
}
|
|
2957
3152
|
this.activeRoomId = st?.activeRoomId ?? metas[0]?.roomId ?? null;
|
|
2958
|
-
|
|
3153
|
+
for (const meta of metas) {
|
|
3154
|
+
const conn = new RoomConn(meta);
|
|
3155
|
+
conn.onAdd = () => this.scheduleHistorySave(conn);
|
|
3156
|
+
this.rooms.set(meta.roomId, conn);
|
|
3157
|
+
}
|
|
2959
3158
|
await this.persist();
|
|
3159
|
+
this.emit({ type: "rooms" });
|
|
3160
|
+
void Promise.all(
|
|
3161
|
+
metas.map(
|
|
3162
|
+
(m) => withTimeout(this.open(m), 2e4, `rejoin ${m.roomId}`).catch(
|
|
3163
|
+
(e) => this.log("error", `rejoin ${m.roomId} failed: ${e.message}`)
|
|
3164
|
+
)
|
|
3165
|
+
)
|
|
3166
|
+
).then(() => this.emit({ type: "rooms" }));
|
|
2960
3167
|
})();
|
|
2961
3168
|
return this.ready;
|
|
2962
3169
|
}
|
|
@@ -2966,11 +3173,13 @@ var CollabBridge = class {
|
|
|
2966
3173
|
// ── room lifecycle ───────────────────────────────────────────────────────
|
|
2967
3174
|
async open(meta) {
|
|
2968
3175
|
const existing = this.rooms.get(meta.roomId);
|
|
2969
|
-
if (existing) return existing;
|
|
2970
|
-
const conn = new RoomConn(meta);
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
3176
|
+
if (existing?.client?.isJoined()) return existing;
|
|
3177
|
+
const conn = existing ?? new RoomConn(meta);
|
|
3178
|
+
if (!existing) {
|
|
3179
|
+
conn.onAdd = () => this.scheduleHistorySave(conn);
|
|
3180
|
+
this.rooms.set(meta.roomId, conn);
|
|
3181
|
+
this.emit({ type: "rooms" });
|
|
3182
|
+
}
|
|
2974
3183
|
try {
|
|
2975
3184
|
await this.connect(conn);
|
|
2976
3185
|
} catch (e) {
|
|
@@ -2988,7 +3197,7 @@ var CollabBridge = class {
|
|
|
2988
3197
|
const roomId = parsed.roomId;
|
|
2989
3198
|
const roomKeyB64 = parsed.roomKeyB64;
|
|
2990
3199
|
conn.meta.transport = parsed.mode;
|
|
2991
|
-
const wsUrl =
|
|
3200
|
+
const wsUrl = this.inviteUrl(parsed);
|
|
2992
3201
|
if (!conn.meta.memberConfig) {
|
|
2993
3202
|
conn.meta.memberConfig = defaultMemberConfig(this.config.workspacePath);
|
|
2994
3203
|
}
|
|
@@ -3055,18 +3264,25 @@ var CollabBridge = class {
|
|
|
3055
3264
|
conn.presenceTimer.unref?.();
|
|
3056
3265
|
}
|
|
3057
3266
|
this.log("info", `joined room ${roomId} as ${identity.agentId} via ${loopback ? "in-process loopback" : wsUrl} (${loopback ? "local" : parsed.mode})`);
|
|
3058
|
-
if (loopback || conn.local.agents.length)
|
|
3267
|
+
if (loopback || conn.local.agents.length) {
|
|
3268
|
+
await withTimeout(this.loadLocalHistory(conn), 3e3, `local-history ${roomId}`).catch(
|
|
3269
|
+
(e) => this.log("warn", `local history ${roomId}: ${e.message}`)
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3059
3272
|
try {
|
|
3060
|
-
|
|
3273
|
+
const hist = await withTimeout(client.history(0), 5e3, `history ${roomId}`);
|
|
3274
|
+
for (const p of hist) {
|
|
3061
3275
|
const m = toMessage(p, identity.agentId);
|
|
3062
3276
|
if (m) conn.add(m);
|
|
3063
3277
|
}
|
|
3064
3278
|
} catch (e) {
|
|
3065
3279
|
this.log("warn", `history for ${roomId} failed: ${e.message}`);
|
|
3066
3280
|
}
|
|
3067
|
-
await this.connectAgents(conn)
|
|
3281
|
+
await withTimeout(this.connectAgents(conn), loopback ? 8e3 : 2e4, `connectAgents ${roomId}`).catch(
|
|
3282
|
+
(e) => this.log("warn", `local agents for ${roomId} failed: ${e.message}`)
|
|
3283
|
+
);
|
|
3068
3284
|
try {
|
|
3069
|
-
await this.announceMemberConfig(conn);
|
|
3285
|
+
await withTimeout(this.announceMemberConfig(conn), 5e3, `announce ${roomId}`);
|
|
3070
3286
|
} catch (e) {
|
|
3071
3287
|
this.log("warn", `announce member.config failed: ${e.message}`);
|
|
3072
3288
|
}
|
|
@@ -3204,7 +3420,13 @@ var CollabBridge = class {
|
|
|
3204
3420
|
}
|
|
3205
3421
|
};
|
|
3206
3422
|
meta.local = normalizeLocalSettings(meta.local);
|
|
3207
|
-
|
|
3423
|
+
this.log("info", `createRoom begin name=${meta.name} localOnly=${!!opts.localOnly} agents=${(opts.agents ?? []).length}`);
|
|
3424
|
+
const conn = new RoomConn(meta);
|
|
3425
|
+
conn.onAdd = () => this.scheduleHistorySave(conn);
|
|
3426
|
+
this.rooms.set(meta.roomId, conn);
|
|
3427
|
+
conn.status = "connecting";
|
|
3428
|
+
this.activeRoomId = meta.roomId;
|
|
3429
|
+
this.emit({ type: "rooms" });
|
|
3208
3430
|
for (const entry of opts.agents ?? []) {
|
|
3209
3431
|
if (typeof entry === "string") {
|
|
3210
3432
|
if (AGENT_PRESETS.some((p) => p.key === entry)) await this.addLocalAgent(meta.roomId, { preset: entry }, { connect: false });
|
|
@@ -3212,24 +3434,35 @@ var CollabBridge = class {
|
|
|
3212
3434
|
await this.addLocalAgent(meta.roomId, entry, { connect: false });
|
|
3213
3435
|
}
|
|
3214
3436
|
}
|
|
3215
|
-
this.activeRoomId = meta.roomId;
|
|
3216
3437
|
if (bindTo) this.bind(conn, bindTo);
|
|
3217
3438
|
await this.persist();
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3439
|
+
this.log("info", `createRoom persisted ${meta.roomId} with ${conn.local.agents.length} agent(s); connecting in background`);
|
|
3440
|
+
void this.finishCreateConnect(conn, meta.public).catch(
|
|
3441
|
+
(e) => this.log("error", `createRoom background connect ${meta.roomId}: ${e.message}`)
|
|
3442
|
+
);
|
|
3443
|
+
return { ...this.view(conn), invite: meta.invite, listed: false };
|
|
3444
|
+
}
|
|
3445
|
+
/** Background: host join + local agent joins + optional directory publish. Never awaited by HTTP create. */
|
|
3446
|
+
async finishCreateConnect(conn, isPublic) {
|
|
3447
|
+
try {
|
|
3448
|
+
await withTimeout(this.connect(conn), conn.local.loopback ? 8e3 : 2e4, `connect ${conn.meta.roomId}`);
|
|
3449
|
+
if (conn.local.agents.length) {
|
|
3450
|
+
await withTimeout(this.connectAgents(conn), conn.local.loopback ? 8e3 : 25e3, `agents ${conn.meta.roomId}`).catch(
|
|
3451
|
+
(e) => this.log("warn", `local agents for ${conn.meta.roomId}: ${e.message}`)
|
|
3222
3452
|
);
|
|
3223
|
-
} else {
|
|
3224
|
-
void this.connectAgents(conn).catch((e) => this.log("warn", `background local agents for ${meta.roomId}: ${e.message}`));
|
|
3225
3453
|
}
|
|
3454
|
+
if (isPublic && conn.meta.owner) {
|
|
3455
|
+
this.startHeartbeat(conn);
|
|
3456
|
+
await this.publish(conn).catch((e) => this.log("warn", `directory publish failed: ${e.message}`));
|
|
3457
|
+
}
|
|
3458
|
+
this.log("info", `createRoom connected ${conn.meta.roomId} status=${conn.status}`);
|
|
3459
|
+
} catch (e) {
|
|
3460
|
+
conn.status = "offline";
|
|
3461
|
+
conn.error = e.message;
|
|
3462
|
+
this.emit({ type: "rooms" });
|
|
3463
|
+
throw e;
|
|
3226
3464
|
}
|
|
3227
|
-
|
|
3228
|
-
if (meta.public) listing = await this.publish(conn).catch((e) => {
|
|
3229
|
-
this.log("warn", `directory publish failed: ${e.message}`);
|
|
3230
|
-
return void 0;
|
|
3231
|
-
});
|
|
3232
|
-
return { ...this.view(conn), invite: meta.invite, listed: !!listing };
|
|
3465
|
+
this.emit({ type: "rooms" });
|
|
3233
3466
|
}
|
|
3234
3467
|
/** Join by invite. `info` carries directory metadata when joining from search results. */
|
|
3235
3468
|
async joinInvite(invite, info = {}, bindTo) {
|
|
@@ -3314,6 +3547,7 @@ var CollabBridge = class {
|
|
|
3314
3547
|
this.emit({ type: "message", roomId: conn.meta.roomId, message: m });
|
|
3315
3548
|
this.routeLocal(conn, full);
|
|
3316
3549
|
}
|
|
3550
|
+
if (conn.local.loopback || conn.local.agents.length) await this.saveLocalHistory(conn);
|
|
3317
3551
|
return { roomId: conn.meta.roomId, ...r };
|
|
3318
3552
|
}
|
|
3319
3553
|
send(roomId, textIn, mentions, replyTo) {
|
|
@@ -3382,7 +3616,7 @@ var CollabBridge = class {
|
|
|
3382
3616
|
error: conn.error,
|
|
3383
3617
|
members: (() => {
|
|
3384
3618
|
const localAgents = conn.local.agents;
|
|
3385
|
-
for (const a of localAgents)
|
|
3619
|
+
for (const a of localAgents) live.add(a.agentId);
|
|
3386
3620
|
const ids = [...live];
|
|
3387
3621
|
const cfgs = this.peerConfigs.get(conn.meta.roomId);
|
|
3388
3622
|
return ids.map((agentId) => {
|
|
@@ -3405,7 +3639,7 @@ var CollabBridge = class {
|
|
|
3405
3639
|
bound: !!sessionId,
|
|
3406
3640
|
sessionId,
|
|
3407
3641
|
transport: conn.local.loopback ? "local" : conn.meta.transport ?? (conn.meta.invite.startsWith("collab://lan/") ? "lan" : "relay"),
|
|
3408
|
-
memberConfig: conn.meta.memberConfig ? { ...publicMemberConfig(conn.meta.memberConfig), workspacePath: conn.meta.memberConfig.workspacePath, peerGrants: conn.meta.memberConfig.peerGrants, permissions: conn.meta.memberConfig.permissions } : null,
|
|
3642
|
+
memberConfig: conn.meta.memberConfig ? { ...publicMemberConfig(conn.meta.memberConfig), workspacePath: conn.meta.memberConfig.workspacePath, resolvedModel: this.resolvedModel(conn), peerGrants: conn.meta.memberConfig.peerGrants, permissions: conn.meta.memberConfig.permissions } : null,
|
|
3409
3643
|
peerConfigs: Object.fromEntries(this.peerConfigs.get(conn.meta.roomId) ?? []),
|
|
3410
3644
|
local: this.localView(conn),
|
|
3411
3645
|
runs: this.scheduler.list(conn.meta.roomId)
|
|
@@ -3413,6 +3647,9 @@ var CollabBridge = class {
|
|
|
3413
3647
|
}
|
|
3414
3648
|
state() {
|
|
3415
3649
|
return {
|
|
3650
|
+
version: PLUGIN_VERSION,
|
|
3651
|
+
models: this.models,
|
|
3652
|
+
defaultModel: this.resolvedModel(),
|
|
3416
3653
|
self: this.identity?.agentId ?? null,
|
|
3417
3654
|
relayUrl: this.config.relayUrl,
|
|
3418
3655
|
lanRelayPort: this.lanRelay?.port ?? null,
|
|
@@ -3443,7 +3680,20 @@ var CollabBridge = class {
|
|
|
3443
3680
|
workspacePath: patch.workspacePath ?? cur.workspacePath,
|
|
3444
3681
|
workspaceLabel: patch.workspaceLabel ?? cur.workspaceLabel
|
|
3445
3682
|
};
|
|
3683
|
+
const modelChanged = ["provider", "model", "reasoningEffort"].some((k) => cur[k] !== next[k]);
|
|
3684
|
+
if (modelChanged && conn.local.agents.some((a) => this.scheduler.isExecuting(roomId, a.agentId))) throw new Error("Stop running agents before changing the room model");
|
|
3685
|
+
const before = conn.local.agents.map((a) => this.selectedModel(conn, a));
|
|
3446
3686
|
conn.meta.memberConfig = next;
|
|
3687
|
+
if (modelChanged) {
|
|
3688
|
+
const host = this.sessions.get(roomId);
|
|
3689
|
+
this.sessions.delete(roomId);
|
|
3690
|
+
delete conn.meta.sessionId;
|
|
3691
|
+
if (host) await host.then((h) => h.dispose()).catch(() => {
|
|
3692
|
+
});
|
|
3693
|
+
for (const [i, a] of conn.local.agents.entries()) {
|
|
3694
|
+
if (JSON.stringify(before[i]) !== JSON.stringify(this.selectedModel(conn, a))) await this.resetAgentSession(conn, a);
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3447
3697
|
this.mcpByRoom.get(roomId)?.updateConfig(next);
|
|
3448
3698
|
await this.persist();
|
|
3449
3699
|
if (conn.client) await this.announceMemberConfig(conn);
|
|
@@ -3621,7 +3871,7 @@ var CollabBridge = class {
|
|
|
3621
3871
|
const summary = `collab ${kind} from ${from} in ${conn.meta.name}`.slice(0, SUMMARY_MAX);
|
|
3622
3872
|
return Object.freeze({
|
|
3623
3873
|
role: "user",
|
|
3624
|
-
id:
|
|
3874
|
+
id: randomUUID3(),
|
|
3625
3875
|
content: Object.freeze([Object.freeze({ type: "text", text: body })]),
|
|
3626
3876
|
source: Object.freeze({ kind: "collab", roomId: conn.meta.roomId, from, messageId, form: "notice", summary })
|
|
3627
3877
|
});
|
|
@@ -3685,13 +3935,13 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
3685
3935
|
const wsPath = roomPath && roomPath.trim() ? roomPath : cfg.workspacePath;
|
|
3686
3936
|
await mkdir5(wsPath, { recursive: true });
|
|
3687
3937
|
const workspace = await svc.workspaceRegistry.create(wsPath, wsPath === cfg.workspacePath ? "collab" : void 0);
|
|
3688
|
-
const selected =
|
|
3689
|
-
const sessionId = `collab-${roomId}-${
|
|
3938
|
+
const selected = this.selectedModel(conn);
|
|
3939
|
+
const sessionId = `collab-${roomId}-${randomUUID3().slice(0, 8)}`;
|
|
3690
3940
|
const handle = await svc.agents.create({
|
|
3691
3941
|
sessionId,
|
|
3692
3942
|
signal: this.abort.signal,
|
|
3693
3943
|
meta: { cwd: workspace.path, agentPreset: preset.id },
|
|
3694
|
-
agentOptions: { provider: selected.provider, model: selected.model },
|
|
3944
|
+
agentOptions: { provider: selected.provider, model: selected.model, ...selected.reasoningEffort ? { reasoningEffort: selected.reasoningEffort } : {} },
|
|
3695
3945
|
setup: async (agentCtx) => {
|
|
3696
3946
|
await svc.agentPresets.mount(agentCtx, preset.id);
|
|
3697
3947
|
}
|
|
@@ -3712,6 +3962,9 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
3712
3962
|
return pending;
|
|
3713
3963
|
}
|
|
3714
3964
|
// ── local multi-agent: members hosted by this dsh ────────────────────────
|
|
3965
|
+
inviteUrl(parsed) {
|
|
3966
|
+
return inviteWsUrl(parsed.uri.startsWith("collab://join/") ? { ...parsed, wsBase: this.config.relayUrl } : parsed);
|
|
3967
|
+
}
|
|
3715
3968
|
transportFor(conn, wsUrl) {
|
|
3716
3969
|
return conn.local.loopback ? new MockTransport(this.hub, `loopback:${conn.meta.roomId}`) : new WsTransport(wsUrl);
|
|
3717
3970
|
}
|
|
@@ -3738,7 +3991,7 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
3738
3991
|
identity,
|
|
3739
3992
|
roomId: parsed.roomId,
|
|
3740
3993
|
roomKey: parsed.roomKeyB64,
|
|
3741
|
-
transport: this.transportFor(conn,
|
|
3994
|
+
transport: this.transportFor(conn, this.inviteUrl(parsed)),
|
|
3742
3995
|
autoReply: "off",
|
|
3743
3996
|
reconnect: !conn.local.loopback,
|
|
3744
3997
|
capabilities: ["collab.v1", "dsh", "member.config", "local-agent"]
|
|
@@ -3837,13 +4090,11 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
3837
4090
|
const clean = sanitizeAgentInput(input);
|
|
3838
4091
|
const sessionKeys = ["model", "provider", "reasoningEffort", "scope"];
|
|
3839
4092
|
const resetSession = sessionKeys.some((k) => k in clean && clean[k] !== a[k]);
|
|
4093
|
+
if (resetSession && this.scheduler.isExecuting(roomId, a.agentId)) throw new Error("Stop this agent before changing its model or scope");
|
|
3840
4094
|
if (clean.moderator) for (const x of conn.local.agents) x.moderator = false;
|
|
3841
4095
|
Object.assign(a, clean, { id: a.id, agentId: a.agentId });
|
|
3842
4096
|
if (resetSession) {
|
|
3843
|
-
this.
|
|
3844
|
-
if (conn.meta.agentSessions) delete conn.meta.agentSessions[a.agentId];
|
|
3845
|
-
conn.meta.agentNoEffort = conn.meta.agentNoEffort?.filter((x) => x !== a.agentId);
|
|
3846
|
-
this.noEffort.delete(`${conn.meta.roomId}:${a.agentId}`);
|
|
4097
|
+
await this.resetAgentSession(conn, a);
|
|
3847
4098
|
}
|
|
3848
4099
|
await this.persist();
|
|
3849
4100
|
await this.announceAgentConfig(conn, a).catch(() => {
|
|
@@ -3901,6 +4152,7 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
3901
4152
|
await conn.client?.dispose().catch(() => {
|
|
3902
4153
|
});
|
|
3903
4154
|
conn.client = null;
|
|
4155
|
+
conn.lastSeenPeers.clear();
|
|
3904
4156
|
conn.local.loopback = wantLoopback;
|
|
3905
4157
|
conn.status = "connecting";
|
|
3906
4158
|
this.emit({ type: "rooms" });
|
|
@@ -3914,19 +4166,31 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
3914
4166
|
return join3(dirname4(this.config.statePath), "local-history", `${roomId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
|
|
3915
4167
|
}
|
|
3916
4168
|
scheduleHistorySave(conn) {
|
|
3917
|
-
if (!conn.local.loopback || conn.historyTimer) return;
|
|
4169
|
+
if (!conn.local.loopback && !conn.local.agents.length || conn.historyTimer) return;
|
|
3918
4170
|
conn.historyTimer = setTimeout(() => {
|
|
3919
4171
|
conn.historyTimer = void 0;
|
|
3920
|
-
void this.saveLocalHistory(conn);
|
|
4172
|
+
void this.saveLocalHistory(conn).catch((e) => this.log("error", `save history ${conn.meta.roomId}: ${e.message}`));
|
|
3921
4173
|
}, 500);
|
|
3922
4174
|
conn.historyTimer.unref?.();
|
|
3923
4175
|
}
|
|
3924
|
-
|
|
4176
|
+
saveLocalHistory(conn) {
|
|
4177
|
+
clearTimeout(conn.historyTimer);
|
|
4178
|
+
conn.historyTimer = void 0;
|
|
3925
4179
|
const f = this.historyPath(conn.meta.roomId);
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
4180
|
+
const body = JSON.stringify(conn.messages.slice(-MESSAGES_MAX));
|
|
4181
|
+
const save = (conn.historySave ?? Promise.resolve()).catch(() => {
|
|
4182
|
+
}).then(async () => {
|
|
4183
|
+
await mkdir5(dirname4(f), { recursive: true, mode: 448 });
|
|
4184
|
+
const tmp = `${f}.${randomUUID3()}.tmp`;
|
|
4185
|
+
try {
|
|
4186
|
+
await writeFile4(tmp, body, { mode: 384, flag: "wx" });
|
|
4187
|
+
await rename2(tmp, f);
|
|
4188
|
+
} finally {
|
|
4189
|
+
await rm3(tmp, { force: true });
|
|
4190
|
+
}
|
|
3929
4191
|
});
|
|
4192
|
+
conn.historySave = save;
|
|
4193
|
+
return save;
|
|
3930
4194
|
}
|
|
3931
4195
|
async loadLocalHistory(conn) {
|
|
3932
4196
|
try {
|
|
@@ -4001,7 +4265,7 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
4001
4265
|
tasks: opts.tasks
|
|
4002
4266
|
};
|
|
4003
4267
|
if (a.approvalRequired) {
|
|
4004
|
-
req.gate = async () => await this.approvals.request({ roomId: conn.meta.roomId, action: "shell.exec", summary: `Run local agent ${a.name} (${a.agentId})`, detail: summary, from: a.agentId, timeoutMs: 10 * 6e4 }) === "allow";
|
|
4268
|
+
req.gate = async (signal) => await this.approvals.request({ roomId: conn.meta.roomId, action: "shell.exec", summary: `Run local agent ${a.name} (${a.agentId})`, detail: summary, from: a.agentId, timeoutMs: 10 * 6e4, signal }) === "allow";
|
|
4005
4269
|
}
|
|
4006
4270
|
this.scheduler.enqueue(req);
|
|
4007
4271
|
}
|
|
@@ -4030,13 +4294,14 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
4030
4294
|
conn.runThread.set(a.agentId, run.threadId);
|
|
4031
4295
|
try {
|
|
4032
4296
|
const cwd = await this.agentCwd(conn, a);
|
|
4297
|
+
signal.throwIfAborted();
|
|
4033
4298
|
const prompt = this.buildLocalPrompt(conn, a, mode, req.trigger, run.threadId, cwd, req.tasks);
|
|
4034
4299
|
const ctx = { bridge: this, roomId: conn.meta.roomId, agent: a, prompt, mode, trigger: req.trigger, threadId: run.threadId, cwd, signal };
|
|
4035
4300
|
const wtBefore = conn.worktrees.get(a.agentId);
|
|
4036
4301
|
const before = wtBefore ? await mainStatus(wtBefore.repo).catch(() => null) : null;
|
|
4037
4302
|
try {
|
|
4038
4303
|
if (this.localRunner) await this.localRunner(ctx);
|
|
4039
|
-
else if (this.config.localAgentRunner === "mock"
|
|
4304
|
+
else if (this.config.localAgentRunner === "mock") await mockLocalRunner(ctx);
|
|
4040
4305
|
else await this.runInSession(conn, a, ctx);
|
|
4041
4306
|
} finally {
|
|
4042
4307
|
if (wtBefore && before) {
|
|
@@ -4080,49 +4345,59 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
4080
4345
|
return root;
|
|
4081
4346
|
}
|
|
4082
4347
|
async runInSession(conn, a, ctx, retried = false) {
|
|
4348
|
+
ctx.signal.throwIfAborted();
|
|
4083
4349
|
const handle = await this.agentSessionFor(conn, a, ctx.cwd);
|
|
4350
|
+
ctx.signal.throwIfAborted();
|
|
4084
4351
|
const agent = handle.agent;
|
|
4085
|
-
|
|
4086
|
-
const seq0 = agent.session?.seq;
|
|
4087
|
-
agent.followup(this.notice(conn, ctx.prompt, ctx.trigger?.sender ?? "room", ctx.trigger?.messageId ?? randomUUID2(), `${ctx.mode} \u2192 ${a.name}`));
|
|
4088
|
-
if (!agent.whenIdle) return;
|
|
4089
|
-
const aborted = new Promise((resolve3) => ctx.signal.addEventListener("abort", () => resolve3(), { once: true }));
|
|
4090
|
-
const timeout = new Promise((resolve3) => {
|
|
4091
|
-
const t = setTimeout(resolve3, 20 * 6e4);
|
|
4092
|
-
t.unref?.();
|
|
4093
|
-
});
|
|
4094
|
-
const settled = (async () => {
|
|
4095
|
-
for (let i = 0; i < 40; i++) {
|
|
4096
|
-
await agent.whenIdle();
|
|
4097
|
-
if (seq0 === void 0 || agent.session?.seq !== seq0) return;
|
|
4098
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
4099
|
-
}
|
|
4100
|
-
})();
|
|
4101
|
-
await Promise.race([settled, aborted, timeout]);
|
|
4102
|
-
this.log("info", `run ${a.agentId}: seq ${seq0} -> ${agent.session?.seq}`);
|
|
4103
|
-
if (process.env.DSH_ROOM_DEBUG_LOG && seq0 !== void 0) {
|
|
4104
|
-
const sess = agent.session;
|
|
4105
|
-
for (let i = seq0; i < (sess.seq ?? seq0); i++) this.log("info", ` ev ${i}: ${JSON.stringify(sess.eventAt?.(i)).slice(0, 700)}`);
|
|
4106
|
-
}
|
|
4107
|
-
if (!ctx.signal.aborted && seq0 !== void 0 && agent.session?.seq === seq0) throw new Error("agent session did not start a turn");
|
|
4108
|
-
const failure = !ctx.signal.aborted && seq0 !== void 0 ? this.turnError(agent.session, seq0) : void 0;
|
|
4109
|
-
if (failure) {
|
|
4110
|
-
const key = `${conn.meta.roomId}:${a.agentId}`;
|
|
4111
|
-
if (!retried && failure.code === "UNSUPPORTED_REASONING_EFFORT" && a.reasoningEffort) {
|
|
4112
|
-
this.log("warn", `${a.agentId}: ${failure.message}; recreating its session with the model's default effort`);
|
|
4113
|
-
this.noEffort.add(key);
|
|
4114
|
-
conn.meta.agentNoEffort = [.../* @__PURE__ */ new Set([...conn.meta.agentNoEffort ?? [], a.agentId])];
|
|
4115
|
-
this.agentHandles.delete(key);
|
|
4116
|
-
if (conn.meta.agentSessions) delete conn.meta.agentSessions[a.agentId];
|
|
4117
|
-
return this.runInSession(conn, a, ctx, true);
|
|
4118
|
-
}
|
|
4119
|
-
throw new Error(failure.message);
|
|
4120
|
-
}
|
|
4121
|
-
if (ctx.signal.aborted) {
|
|
4352
|
+
const cancel = () => {
|
|
4122
4353
|
try {
|
|
4123
4354
|
agent.cancel?.({ kind: "user" });
|
|
4124
4355
|
} catch {
|
|
4125
4356
|
}
|
|
4357
|
+
};
|
|
4358
|
+
ctx.signal.addEventListener("abort", cancel, { once: true });
|
|
4359
|
+
try {
|
|
4360
|
+
if (agent.whenIdle) await withTimeout(agent.whenIdle(), 3e4, `session idle ${a.agentId}`, ctx.signal);
|
|
4361
|
+
ctx.signal.throwIfAborted();
|
|
4362
|
+
const seq0 = agent.session?.seq;
|
|
4363
|
+
agent.followup(this.notice(conn, ctx.prompt, ctx.trigger?.sender ?? "room", ctx.trigger?.messageId ?? randomUUID3(), `${ctx.mode} \u2192 ${a.name}`));
|
|
4364
|
+
if (!agent.whenIdle) return;
|
|
4365
|
+
const settled = (async () => {
|
|
4366
|
+
for (let i = 0; i < 40; i++) {
|
|
4367
|
+
ctx.signal.throwIfAborted();
|
|
4368
|
+
await agent.whenIdle();
|
|
4369
|
+
ctx.signal.throwIfAborted();
|
|
4370
|
+
if (seq0 === void 0 || agent.session?.seq !== seq0) return;
|
|
4371
|
+
await sleep2(250, ctx.signal);
|
|
4372
|
+
}
|
|
4373
|
+
})();
|
|
4374
|
+
await withTimeout(settled, 20 * 6e4, `agent turn ${a.agentId}`, ctx.signal);
|
|
4375
|
+
this.log("info", `run ${a.agentId}: seq ${seq0} -> ${agent.session?.seq}`);
|
|
4376
|
+
if (process.env.DSH_ROOM_DEBUG_LOG && seq0 !== void 0) {
|
|
4377
|
+
const sess = agent.session;
|
|
4378
|
+
for (let i = seq0; i < (sess.seq ?? seq0); i++) this.log("info", ` ev ${i}: ${JSON.stringify(sess.eventAt?.(i)).slice(0, 700)}`);
|
|
4379
|
+
}
|
|
4380
|
+
if (!ctx.signal.aborted && seq0 !== void 0 && agent.session?.seq === seq0) throw new Error("agent session did not start a turn");
|
|
4381
|
+
const failure = !ctx.signal.aborted && seq0 !== void 0 ? this.turnError(agent.session, seq0) : void 0;
|
|
4382
|
+
if (failure) {
|
|
4383
|
+
const key = `${conn.meta.roomId}:${a.agentId}`;
|
|
4384
|
+
if (!retried && failure.code === "UNSUPPORTED_REASONING_EFFORT" && this.selectedModel(conn, a).reasoningEffort) {
|
|
4385
|
+
this.log("warn", `${a.agentId}: ${failure.message}; recreating its session with the model's default effort`);
|
|
4386
|
+
this.noEffort.add(key);
|
|
4387
|
+
conn.meta.agentNoEffort = [.../* @__PURE__ */ new Set([...conn.meta.agentNoEffort ?? [], a.agentId])];
|
|
4388
|
+
this.agentHandles.delete(key);
|
|
4389
|
+
if (conn.meta.agentSessions) delete conn.meta.agentSessions[a.agentId];
|
|
4390
|
+
return this.runInSession(conn, a, ctx, true);
|
|
4391
|
+
}
|
|
4392
|
+
throw new Error(failure.message);
|
|
4393
|
+
}
|
|
4394
|
+
} catch (e) {
|
|
4395
|
+
cancel();
|
|
4396
|
+
if (agent.whenIdle) await withTimeout(agent.whenIdle(), 3e4, `agent stop ${a.agentId}`).catch(() => {
|
|
4397
|
+
});
|
|
4398
|
+
throw e;
|
|
4399
|
+
} finally {
|
|
4400
|
+
ctx.signal.removeEventListener("abort", cancel);
|
|
4126
4401
|
}
|
|
4127
4402
|
}
|
|
4128
4403
|
/** Last turn/end error since `from`, if the agent's turn failed (provider/model errors). */
|
|
@@ -4160,15 +4435,15 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
4160
4435
|
try {
|
|
4161
4436
|
const root = conn.meta.memberConfig?.workspacePath?.trim() || this.config.workspacePath;
|
|
4162
4437
|
const workspace = await svc.workspaceRegistry.create(root, root === this.config.workspacePath ? "collab" : void 0);
|
|
4163
|
-
const
|
|
4164
|
-
const base = { provider:
|
|
4165
|
-
const withEffort =
|
|
4438
|
+
const effective = this.selectedModel(conn, a);
|
|
4439
|
+
const base = { provider: effective.provider, model: effective.model };
|
|
4440
|
+
const withEffort = effective.reasoningEffort && !this.noEffort.has(key) && !conn.meta.agentNoEffort?.includes(a.agentId);
|
|
4166
4441
|
const resume = svc.agents.resume;
|
|
4167
4442
|
if (prevSid && resume) {
|
|
4168
4443
|
try {
|
|
4169
4444
|
const h = await resume.call(svc.agents, {
|
|
4170
4445
|
resumeSessionId: prevSid,
|
|
4171
|
-
agentOptions: withEffort ? { ...base, reasoningEffort:
|
|
4446
|
+
agentOptions: withEffort ? { ...base, reasoningEffort: effective.reasoningEffort } : base,
|
|
4172
4447
|
setup: async (agentCtx) => {
|
|
4173
4448
|
await svc.agentPresets.mount(agentCtx, preset.id);
|
|
4174
4449
|
}
|
|
@@ -4186,7 +4461,7 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
4186
4461
|
this.log("warn", `resume ${prevSid} for ${a.agentId} failed (${e.message}); creating a new session`);
|
|
4187
4462
|
}
|
|
4188
4463
|
}
|
|
4189
|
-
const sessionId = `room-${conn.meta.roomId.slice(-8)}-${a.id}-${
|
|
4464
|
+
const sessionId = `room-${conn.meta.roomId.slice(-8)}-${a.id}-${randomUUID3().slice(0, 6)}`;
|
|
4190
4465
|
const create = (agentOptions, cwdPath) => svc.agents.create({
|
|
4191
4466
|
sessionId,
|
|
4192
4467
|
signal: this.abort.signal,
|
|
@@ -4198,7 +4473,7 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
4198
4473
|
});
|
|
4199
4474
|
let handle;
|
|
4200
4475
|
try {
|
|
4201
|
-
handle = await create(withEffort ? { ...base, reasoningEffort:
|
|
4476
|
+
handle = await create(withEffort ? { ...base, reasoningEffort: effective.reasoningEffort } : base, root);
|
|
4202
4477
|
} catch (e) {
|
|
4203
4478
|
this.log("warn", `session for ${a.agentId} with reasoning effort failed (${e.message}); retrying with defaults`);
|
|
4204
4479
|
handle = await create(base, root);
|
|
@@ -4317,6 +4592,7 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4317
4592
|
this.emit({ type: "message", roomId: conn.meta.roomId, message: m });
|
|
4318
4593
|
this.routeLocal(conn, full);
|
|
4319
4594
|
}
|
|
4595
|
+
if (conn.local.loopback || conn.local.agents.length) await this.saveLocalHistory(conn);
|
|
4320
4596
|
return { roomId: conn.meta.roomId, ...r };
|
|
4321
4597
|
}
|
|
4322
4598
|
// ── merge view: per-agent worktree diffs ─────────────────────────────────
|
|
@@ -4353,7 +4629,7 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4353
4629
|
if (!approved) throw new Error("merge needs explicit approval");
|
|
4354
4630
|
const conn = this.room(roomId);
|
|
4355
4631
|
const a = this.localAgent(conn, agentId);
|
|
4356
|
-
if (this.scheduler.
|
|
4632
|
+
if (this.scheduler.isExecuting(conn.meta.roomId, a.agentId)) throw new Error(`${a.name} is still running \u2014 wait for it to finish stopping`);
|
|
4357
4633
|
const wt = conn.worktrees.get(a.agentId) ?? await this.findWorktree(conn, a);
|
|
4358
4634
|
if (!wt) throw new Error(`${a.name} has no worktree yet`);
|
|
4359
4635
|
const r = await mergeWorktree(wt, a.agentId);
|
|
@@ -4367,7 +4643,7 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4367
4643
|
if (!approved) throw new Error("discard needs explicit approval");
|
|
4368
4644
|
const conn = this.room(roomId);
|
|
4369
4645
|
const a = this.localAgent(conn, agentId);
|
|
4370
|
-
if (this.scheduler.
|
|
4646
|
+
if (this.scheduler.isExecuting(conn.meta.roomId, a.agentId)) throw new Error(`${a.name} is still running \u2014 wait for it to finish stopping`);
|
|
4371
4647
|
const wt = conn.worktrees.get(a.agentId) ?? await this.findWorktree(conn, a);
|
|
4372
4648
|
if (!wt) return { ok: true, discarded: false };
|
|
4373
4649
|
await discardWorktree(wt);
|
|
@@ -4401,6 +4677,7 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4401
4677
|
const wt = conn.worktrees.get(a.agentId);
|
|
4402
4678
|
return {
|
|
4403
4679
|
...a,
|
|
4680
|
+
resolvedModel: this.resolvedModel(conn, a),
|
|
4404
4681
|
sessionId: conn.meta.agentSessions?.[a.agentId],
|
|
4405
4682
|
online: online.has(a.agentId) || !!conn.agents.get(a.agentId)?.isJoined(),
|
|
4406
4683
|
state: r?.state ?? "idle",
|
|
@@ -4426,8 +4703,10 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4426
4703
|
for (const conn of this.rooms.values()) {
|
|
4427
4704
|
if (conn.heartbeat) clearInterval(conn.heartbeat);
|
|
4428
4705
|
if (conn.presenceTimer) clearInterval(conn.presenceTimer);
|
|
4706
|
+
clearTimeout(conn.announceTimer);
|
|
4707
|
+
clearTimeout(conn.historyTimer);
|
|
4429
4708
|
this.scheduler.stop(conn.meta.roomId);
|
|
4430
|
-
if (conn.local.loopback) await this.saveLocalHistory(conn);
|
|
4709
|
+
if (conn.local.loopback || conn.local.agents.length) await this.saveLocalHistory(conn);
|
|
4431
4710
|
await this.disconnectAgents(conn);
|
|
4432
4711
|
await conn.client?.dispose().catch(() => {
|
|
4433
4712
|
});
|
|
@@ -4454,7 +4733,7 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4454
4733
|
}
|
|
4455
4734
|
const who = bridge.callerAgent(exec);
|
|
4456
4735
|
bridge.log("info", `tool ${name2} by ${who?.agent.agentId ?? "host"}`);
|
|
4457
|
-
return await run(a, who) ?? { ok: true };
|
|
4736
|
+
return JSON.parse(JSON.stringify(await run(a, who) ?? { ok: true }));
|
|
4458
4737
|
}
|
|
4459
4738
|
});
|
|
4460
4739
|
const str2 = (v) => typeof v === "string" && v ? v : void 0;
|
|
@@ -4516,8 +4795,8 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4516
4795
|
];
|
|
4517
4796
|
}
|
|
4518
4797
|
};
|
|
4519
|
-
var sleep2 = (ms, signal) => new Promise((
|
|
4520
|
-
const t = setTimeout(
|
|
4798
|
+
var sleep2 = (ms, signal) => new Promise((resolve4, reject) => {
|
|
4799
|
+
const t = setTimeout(resolve4, ms);
|
|
4521
4800
|
signal?.addEventListener("abort", () => {
|
|
4522
4801
|
clearTimeout(t);
|
|
4523
4802
|
reject(new Error("stopped"));
|
|
@@ -4565,7 +4844,7 @@ Written by ${a.agentId} (${a.name}) at ${(/* @__PURE__ */ new Date()).toISOStrin
|
|
|
4565
4844
|
|
|
4566
4845
|
// src/config.ts
|
|
4567
4846
|
import { homedir } from "node:os";
|
|
4568
|
-
import { join as join4, isAbsolute as
|
|
4847
|
+
import { join as join4, isAbsolute as isAbsolute3 } from "node:path";
|
|
4569
4848
|
var AUTO = /* @__PURE__ */ new Set(["off", "mention", "mention-or-task"]);
|
|
4570
4849
|
function expand(p) {
|
|
4571
4850
|
return p.startsWith("~/") ? join4(homedir(), p.slice(2)) : p;
|
|
@@ -4578,7 +4857,7 @@ function normalizeConfig(raw) {
|
|
|
4578
4857
|
const relayUrl = str2("relayUrl") ?? DEFAULT_RELAY_URL;
|
|
4579
4858
|
if (!/^wss?:\/\//.test(relayUrl)) throw new Error(`dsh-room: relayUrl must be ws:// or wss://, got ${relayUrl}`);
|
|
4580
4859
|
const workspacePath = expand(str2("workspacePath") ?? "~/.collab/workspace");
|
|
4581
|
-
if (!
|
|
4860
|
+
if (!isAbsolute3(workspacePath)) throw new Error("dsh-room: workspacePath must be absolute");
|
|
4582
4861
|
return {
|
|
4583
4862
|
relayUrl,
|
|
4584
4863
|
invite: str2("invite"),
|
|
@@ -4794,6 +5073,27 @@ async function readJson2(req) {
|
|
|
4794
5073
|
}
|
|
4795
5074
|
var str = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
4796
5075
|
var strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : typeof v === "string" ? v.split(/[\s,]+/).filter(Boolean) : void 0;
|
|
5076
|
+
function peerGrants(value) {
|
|
5077
|
+
const object = (v) => {
|
|
5078
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) throw new HttpError(400, "peerGrants must contain objects");
|
|
5079
|
+
return v;
|
|
5080
|
+
};
|
|
5081
|
+
const grants = (v) => {
|
|
5082
|
+
const input2 = object(v), out = {};
|
|
5083
|
+
for (const cap of ["read", "write", "exec", "delete"]) {
|
|
5084
|
+
if (!(cap in input2)) continue;
|
|
5085
|
+
const grant = input2[cap];
|
|
5086
|
+
if (grant !== "auto" && grant !== "ask" && grant !== "deny") throw new HttpError(400, `invalid peer grant for ${cap}`);
|
|
5087
|
+
out[cap] = grant;
|
|
5088
|
+
}
|
|
5089
|
+
return out;
|
|
5090
|
+
};
|
|
5091
|
+
const input = object(value);
|
|
5092
|
+
return {
|
|
5093
|
+
default: "default" in input ? grants(input.default) : {},
|
|
5094
|
+
byAgent: "byAgent" in input ? Object.fromEntries(Object.entries(object(input.byAgent)).map(([id, v]) => [id, grants(v)])) : {}
|
|
5095
|
+
};
|
|
5096
|
+
}
|
|
4797
5097
|
function createApiHandler(bridge, connection) {
|
|
4798
5098
|
return async (req, res) => {
|
|
4799
5099
|
const conn = connection();
|
|
@@ -4810,6 +5110,7 @@ function createApiHandler(bridge, connection) {
|
|
|
4810
5110
|
await bridge.start();
|
|
4811
5111
|
if (method === "GET" && path === "/stream") return stream(bridge, req, res);
|
|
4812
5112
|
if (method === "GET" && path === "/state") return sendJson2(res, 200, bridge.state());
|
|
5113
|
+
if (method === "GET" && path === "/version") return sendJson2(res, 200, { name: "@cecwxf/dsh-room", version: bridge.state().version ?? "unknown" });
|
|
4813
5114
|
if (method === "GET" && path === "/directory") return sendJson2(res, 200, { rooms: await bridge.search(str(url.searchParams.get("q")), str(url.searchParams.get("tag"))) });
|
|
4814
5115
|
if (method === "POST" && path === "/rooms") {
|
|
4815
5116
|
const b = await readJson2(req);
|
|
@@ -4916,6 +5217,7 @@ function createApiHandler(bridge, connection) {
|
|
|
4916
5217
|
if (k in b) patch[k] = b[k];
|
|
4917
5218
|
}
|
|
4918
5219
|
if (b.permissions && typeof b.permissions === "object") patch.permissions = b.permissions;
|
|
5220
|
+
if ("peerGrants" in b) patch.peerGrants = peerGrants(b.peerGrants);
|
|
4919
5221
|
return sendJson2(res, 200, await bridge.updateMemberConfig(roomId, patch));
|
|
4920
5222
|
}
|
|
4921
5223
|
if (method === "GET" && action === "mcp") return sendJson2(res, 200, { tools: (await Promise.resolve().then(() => (init_mcp_workspace(), mcp_workspace_exports))).MCP_TOOL_DEFS });
|
|
@@ -4931,7 +5233,7 @@ function createApiHandler(bridge, connection) {
|
|
|
4931
5233
|
}
|
|
4932
5234
|
throw new HttpError(404, `no route ${method} ${API}${path}`);
|
|
4933
5235
|
} catch (e) {
|
|
4934
|
-
const status = e instanceof HttpError ? e.status : /not in room|not in a collab room|no local agent/.test(e.message) ? 404 : /needs explicit approval
|
|
5236
|
+
const status = e instanceof HttpError ? e.status : /not in room|not in a collab room|no local agent/.test(e.message) ? 404 : /needs explicit approval|peer grant deny|^denied:/.test(e.message) ? 403 : 500;
|
|
4935
5237
|
if (!res.headersSent) sendJson2(res, status, { error: e.message });
|
|
4936
5238
|
}
|
|
4937
5239
|
};
|
|
@@ -4982,6 +5284,9 @@ function apply(ctx, config) {
|
|
|
4982
5284
|
ctx.inject?.(["webServer"], (c) => {
|
|
4983
5285
|
c.effect(() => registerRoutes(bridge, c.webServer, () => c.get("connection")), "dsh-room: /api/collab routes");
|
|
4984
5286
|
});
|
|
5287
|
+
ctx.inject?.(["llm", "agentDefaultModel"], () => {
|
|
5288
|
+
void bridge.refreshModels().catch((e) => bridge.log("warn", `model catalog: ${e.message}`));
|
|
5289
|
+
});
|
|
4985
5290
|
bridge.start().catch((e) => bridge.log("error", `start failed: ${e.message}`));
|
|
4986
5291
|
bridge.log("info", `loaded (relay ${bridge.config.relayUrl}, autoReply ${bridge.config.autoReply}, ${unregister.length} tools)`);
|
|
4987
5292
|
}
|
|
@@ -4989,6 +5294,7 @@ export {
|
|
|
4989
5294
|
AGENT_PRESETS,
|
|
4990
5295
|
CollabBridge,
|
|
4991
5296
|
Config,
|
|
5297
|
+
PLUGIN_VERSION,
|
|
4992
5298
|
apply,
|
|
4993
5299
|
inject,
|
|
4994
5300
|
mockLocalRunner,
|