pinomad 0.7.0 → 0.9.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 +7 -58
- package/dist/{chunk-8yme2wep.js → chunk-014b7ne7.js} +1 -1
- package/dist/{chunk-07769pqx.js → chunk-0tesv2sr.js} +8 -8
- package/dist/{chunk-ywmm63ks.js → chunk-3p1eysm9.js} +4 -2
- package/dist/chunk-4sepcfpy.js +2994 -0
- package/dist/{chunk-0xh6cy9v.js → chunk-5gp9svzs.js} +5 -3
- package/dist/{chunk-12kaxggg.js → chunk-76pvsg2d.js} +2 -2
- package/dist/{chunk-1nyzr0mg.js → chunk-ctzgjctt.js} +2395 -839
- package/dist/{chunk-e4ahjbmz.js → chunk-jgd38rna.js} +210 -7
- package/dist/{chunk-7qb9tq54.js → chunk-khcpv54k.js} +64 -310
- package/dist/{chunk-1t58fh0k.js → chunk-m824ds0q.js} +5 -5
- package/dist/{chunk-sz3rzk55.js → chunk-qgexy01f.js} +5 -5
- package/dist/chunk-sygeb9qb.js +430 -0
- package/dist/{chunk-b1h374jr.js → chunk-v414b5pp.js} +8 -2
- package/dist/{chunk-m4c5gvce.js → chunk-vax4zspw.js} +22 -5
- package/dist/{chunk-6ysezfy5.js → chunk-vxmj0200.js} +305 -23
- package/dist/{chunk-rtwxjb90.js → chunk-vyvyaavr.js} +1 -1
- package/dist/{chunk-r6w5aqkw.js → chunk-wb10efyq.js} +17 -0
- package/dist/{chunk-fbkd73zr.js → chunk-yhf7bj5q.js} +9 -9
- package/dist/pinomad.js +10 -10
- package/package.json +12 -8
- package/web/assets/index-1g60bhv2.js +395 -0
- package/web/assets/{index-C0rwAo0H.css → index-DoyHMPwf.css} +1 -1
- package/web/index.html +2 -2
- package/dist/chunk-sj7rnvh4.js +0 -719
- package/dist/chunk-znhn8af7.js +0 -33
- package/web/assets/index-DGee2kNU.js +0 -394
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
serveWebClient
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-wb10efyq.js";
|
|
5
5
|
import {
|
|
6
6
|
Published,
|
|
7
7
|
constantValue,
|
|
8
8
|
openHostSettings,
|
|
9
9
|
sameList,
|
|
10
10
|
settingsDefaults
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-v414b5pp.js";
|
|
12
12
|
import {
|
|
13
13
|
DevicesDoc,
|
|
14
14
|
RevocationsDoc,
|
|
@@ -21,21 +21,16 @@ import {
|
|
|
21
21
|
readServerBinding,
|
|
22
22
|
readServerConfig,
|
|
23
23
|
resolveRelayOrigin
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-5gp9svzs.js";
|
|
25
25
|
import {
|
|
26
|
-
|
|
27
|
-
SecureSession,
|
|
28
|
-
decodeClientHello,
|
|
29
|
-
encodeHostHello,
|
|
26
|
+
relayHostId,
|
|
30
27
|
relayWsBase,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
toBase64Url
|
|
34
|
-
} from "./chunk-7qb9tq54.js";
|
|
28
|
+
startRelayLink
|
|
29
|
+
} from "./chunk-khcpv54k.js";
|
|
35
30
|
import {
|
|
36
31
|
packagedVersion,
|
|
37
32
|
webRoot
|
|
38
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-vyvyaavr.js";
|
|
39
34
|
import {
|
|
40
35
|
ATTACHMENT_TTL_MS,
|
|
41
36
|
INVALID_FRAME_CLOSE_CODE,
|
|
@@ -44,32 +39,56 @@ import {
|
|
|
44
39
|
MAX_PENDING_ATTACHMENTS,
|
|
45
40
|
MAX_PENDING_ATTACHMENT_BYTES,
|
|
46
41
|
PROTOCOL,
|
|
42
|
+
RTC_DATA_CHANNEL_LABEL,
|
|
43
|
+
RTC_DEFAULT_STUN,
|
|
44
|
+
RTC_MAX_RECORD_BYTES,
|
|
47
45
|
UNAUTHORIZED_CLOSE_CODE,
|
|
48
46
|
attachmentBeginProblem,
|
|
49
47
|
attachmentBlock,
|
|
50
48
|
attachmentChunkProblem,
|
|
51
49
|
base64DecodedLength,
|
|
52
50
|
isAttachmentId,
|
|
51
|
+
isCustomProviderApi,
|
|
53
52
|
organize,
|
|
54
53
|
parseAttachmentBlock,
|
|
55
|
-
parseClientFrame
|
|
56
|
-
|
|
54
|
+
parseClientFrame,
|
|
55
|
+
parseRtcMaxMessageSize,
|
|
56
|
+
rtcCandidateType,
|
|
57
|
+
sdpCandidates,
|
|
58
|
+
withRtcCandidates
|
|
59
|
+
} from "./chunk-jgd38rna.js";
|
|
57
60
|
import {
|
|
61
|
+
ENDPOINT_SET_SCHEMA_VERSION,
|
|
62
|
+
MAX_HOST_ENDPOINT_URL_LENGTH,
|
|
63
|
+
SECURE_PROLOGUE,
|
|
64
|
+
SIGNAL_PROLOGUE,
|
|
65
|
+
SecureSession,
|
|
58
66
|
THINKING_LEVELS,
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
decodeClientHello,
|
|
68
|
+
encodeEndpointSetParam,
|
|
69
|
+
encodeHostHello,
|
|
70
|
+
isLoopbackUrl,
|
|
71
|
+
isThinkingLevel,
|
|
72
|
+
parseEndpointSet,
|
|
73
|
+
respondIK,
|
|
74
|
+
toBase64Url
|
|
75
|
+
} from "./chunk-sygeb9qb.js";
|
|
61
76
|
import {
|
|
62
77
|
DEFAULT_DATA_DIR
|
|
63
78
|
} from "./chunk-ngf1zatm.js";
|
|
64
|
-
import
|
|
79
|
+
import {
|
|
80
|
+
__require
|
|
81
|
+
} from "./chunk-df32avw6.js";
|
|
65
82
|
|
|
66
83
|
// apps/host/src/main.ts
|
|
84
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
85
|
+
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
67
86
|
import { homedir as homedir3 } from "node:os";
|
|
68
87
|
import { join as join12, resolve as resolve7 } from "node:path";
|
|
69
88
|
import { parseArgs } from "node:util";
|
|
70
89
|
import { createModels } from "@earendil-works/pi-ai/models";
|
|
71
90
|
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
|
72
|
-
import {
|
|
91
|
+
import { ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
73
92
|
import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
|
|
74
93
|
|
|
75
94
|
// apps/host/src/extensions/question.ts
|
|
@@ -134,7 +153,8 @@ var QuestionDoc = defineDoc({
|
|
|
134
153
|
scope: "conversation",
|
|
135
154
|
history: "latest",
|
|
136
155
|
fork: "initial",
|
|
137
|
-
initial: () => ({ requests: [] })
|
|
156
|
+
initial: () => ({ requests: [] }),
|
|
157
|
+
checkpointWhen: () => false
|
|
138
158
|
});
|
|
139
159
|
var resolutionOf = (value, requestId) => value?.requests.find((request) => request.id === requestId)?.resolution;
|
|
140
160
|
var createWait = (tx, owner, requestId) => tx.createTask(QuestionWait, { requestId }, { ownership: { kind: "task", taskId: owner } });
|
|
@@ -453,7 +473,8 @@ var RoleDoc = defineDoc2({
|
|
|
453
473
|
scope: "conversation",
|
|
454
474
|
history: "rewindable",
|
|
455
475
|
fork: "asOf",
|
|
456
|
-
initial: () => ({})
|
|
476
|
+
initial: () => ({}),
|
|
477
|
+
checkpointWhen: () => false
|
|
457
478
|
});
|
|
458
479
|
|
|
459
480
|
// apps/host/src/organization.ts
|
|
@@ -461,7 +482,8 @@ var IndexDoc = defineDoc3({
|
|
|
461
482
|
kind: "pinomad.index",
|
|
462
483
|
version: 1,
|
|
463
484
|
scope: "session",
|
|
464
|
-
initial: () => ({ projects: [], conversations: [] })
|
|
485
|
+
initial: () => ({ projects: [], conversations: [] }),
|
|
486
|
+
checkpointWhen: () => false
|
|
465
487
|
});
|
|
466
488
|
async function ensureIndex(harness, context) {
|
|
467
489
|
await harness.commit(async (tx) => {
|
|
@@ -1254,7 +1276,8 @@ var SubagentDoc = defineDoc4({
|
|
|
1254
1276
|
scope: "conversation",
|
|
1255
1277
|
history: "latest",
|
|
1256
1278
|
fork: "initial",
|
|
1257
|
-
initial: () => ({})
|
|
1279
|
+
initial: () => ({}),
|
|
1280
|
+
checkpointWhen: () => false
|
|
1258
1281
|
});
|
|
1259
1282
|
|
|
1260
1283
|
// apps/host/src/extensions/subagent.ts
|
|
@@ -1404,7 +1427,8 @@ var TodoDoc = defineDoc5({
|
|
|
1404
1427
|
scope: "conversation",
|
|
1405
1428
|
history: "rewindable",
|
|
1406
1429
|
fork: "asOf",
|
|
1407
|
-
initial: () => ({ items: [] })
|
|
1430
|
+
initial: () => ({ items: [] }),
|
|
1431
|
+
checkpointWhen: () => false
|
|
1408
1432
|
});
|
|
1409
1433
|
var todoWrite = defineTool3({
|
|
1410
1434
|
name: "todo_write",
|
|
@@ -1424,568 +1448,655 @@ var todo = {
|
|
|
1424
1448
|
docs: [{ token: TodoDoc, presentation: "pinomad.todo" }]
|
|
1425
1449
|
};
|
|
1426
1450
|
|
|
1427
|
-
// apps/host/src/
|
|
1428
|
-
import {
|
|
1429
|
-
import {
|
|
1430
|
-
import {
|
|
1431
|
-
import { BACKGROUND_CONTEXT as
|
|
1432
|
-
import {
|
|
1433
|
-
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
1434
|
-
|
|
1435
|
-
// apps/host/src/mcp.ts
|
|
1436
|
-
import { readFile } from "node:fs/promises";
|
|
1437
|
-
import { homedir } from "node:os";
|
|
1438
|
-
import { isAbsolute, resolve as resolve4 } from "node:path";
|
|
1451
|
+
// apps/host/src/gateway.ts
|
|
1452
|
+
import { createServer } from "node:http";
|
|
1453
|
+
import { homedir, networkInterfaces } from "node:os";
|
|
1454
|
+
import { join as join7, sep as sep5 } from "node:path";
|
|
1455
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
1456
|
+
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1439
1457
|
import {
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
} from "
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1458
|
+
AgentDoc as AgentDoc5,
|
|
1459
|
+
configure as configure3,
|
|
1460
|
+
InboxDoc,
|
|
1461
|
+
LiveDoc
|
|
1462
|
+
} from "@earendil-works/pi-durable";
|
|
1463
|
+
import { WebSocket as WebSocket2, WebSocketServer } from "ws";
|
|
1464
|
+
import { Value as Value2 } from "typebox/value";
|
|
1465
|
+
|
|
1466
|
+
// apps/host/src/secure-carrier.ts
|
|
1467
|
+
import { WebSocket } from "ws";
|
|
1468
|
+
function webSocketCarrier(socket) {
|
|
1469
|
+
socket.on("error", () => socket.terminate());
|
|
1470
|
+
return {
|
|
1471
|
+
get open() {
|
|
1472
|
+
return socket.readyState === WebSocket.OPEN;
|
|
1473
|
+
},
|
|
1474
|
+
send: (bytes) => socket.readyState === WebSocket.OPEN ? new Promise((resolve4) => socket.send(bytes, () => resolve4())) : Promise.resolve(),
|
|
1475
|
+
close: (code, reason) => {
|
|
1476
|
+
try {
|
|
1477
|
+
socket.close(code, reason);
|
|
1478
|
+
} catch {
|
|
1479
|
+
socket.terminate();
|
|
1480
|
+
}
|
|
1481
|
+
},
|
|
1482
|
+
handle(handlers) {
|
|
1483
|
+
socket.on("message", (data, isBinary) => {
|
|
1484
|
+
if (!isBinary) {
|
|
1485
|
+
handlers.text();
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
if (Array.isArray(data)) {
|
|
1489
|
+
handlers.binary(new Uint8Array(Buffer.concat(data)));
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
handlers.binary(data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
1493
|
+
});
|
|
1494
|
+
socket.once("close", () => handlers.closed());
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1460
1497
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1498
|
+
|
|
1499
|
+
// apps/host/src/rtc.ts
|
|
1500
|
+
import { randomUUID } from "node:crypto";
|
|
1501
|
+
var BUFFER_HIGH_WATER = 256 * 1024;
|
|
1502
|
+
var BUFFER_LOW_WATER = 64 * 1024;
|
|
1503
|
+
var PUMP_STALL_MS = 1e4;
|
|
1504
|
+
var FALLBACK_POLL_MS = 50;
|
|
1505
|
+
var CANDIDATE_BUDGET_MS = 2000;
|
|
1506
|
+
var PEER_TIMEOUT_MS = 20000;
|
|
1507
|
+
var PER_DEVICE_PEERS = 2;
|
|
1508
|
+
var HOST_PEERS = 8;
|
|
1509
|
+
var systemRtcTimers = {
|
|
1510
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
1511
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
1512
|
+
now: () => Date.now()
|
|
1513
|
+
};
|
|
1514
|
+
function platformPackageName(platform = process.platform, arch = process.arch) {
|
|
1515
|
+
if (platform === "linux" && arch === "x64")
|
|
1516
|
+
return "@node-datachannel/linux-x64-gnu";
|
|
1517
|
+
if (platform === "linux" && arch === "arm64")
|
|
1518
|
+
return "@node-datachannel/linux-arm64-gnu";
|
|
1519
|
+
if (platform === "darwin" && arch === "arm64")
|
|
1520
|
+
return "@node-datachannel/darwin-arm64";
|
|
1521
|
+
if (platform === "darwin" && arch === "x64")
|
|
1522
|
+
return "@node-datachannel/darwin-x64";
|
|
1523
|
+
if (platform === "win32" && arch === "x64")
|
|
1524
|
+
return "@node-datachannel/win32-x64-msvc";
|
|
1525
|
+
if (platform === "win32" && arch === "arm64")
|
|
1526
|
+
return "@node-datachannel/win32-arm64-msvc";
|
|
1527
|
+
if (platform === "android" && arch === "arm64")
|
|
1528
|
+
return "@node-datachannel/android-arm64";
|
|
1529
|
+
return `@node-datachannel/${platform}-${arch}`;
|
|
1530
|
+
}
|
|
1531
|
+
function rtcUnavailableDiagnostic(error, platform = process.platform, arch = process.arch) {
|
|
1532
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1533
|
+
return `RTC unavailable: cannot load node-datachannel on ${platform} (${arch}); ` + `missing ${platformPackageName(platform, arch)}. ` + "Install without --omit=optional (or build node-datachannel locally); " + "the host continues without hello.rtc. " + `(${detail})`;
|
|
1534
|
+
}
|
|
1535
|
+
async function startRtcHost(options) {
|
|
1536
|
+
let factory = options.factory;
|
|
1537
|
+
if (factory === undefined) {
|
|
1538
|
+
let loaded;
|
|
1539
|
+
try {
|
|
1540
|
+
loaded = options.load === undefined ? await import("node-datachannel") : await options.load();
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
(options.diagnostic ?? console.error)(rtcUnavailableDiagnostic(error));
|
|
1472
1543
|
return;
|
|
1473
1544
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1545
|
+
try {
|
|
1546
|
+
factory = nodeRtcFactory(loaded);
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
(options.diagnostic ?? console.error)(rtcUnavailableDiagnostic(error));
|
|
1477
1549
|
return;
|
|
1478
1550
|
}
|
|
1479
|
-
expanded[key] = result2;
|
|
1480
1551
|
}
|
|
1481
|
-
return
|
|
1552
|
+
return new PeerHost(factory, options);
|
|
1482
1553
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1554
|
+
|
|
1555
|
+
class PeerHost {
|
|
1556
|
+
#factory;
|
|
1557
|
+
#options;
|
|
1558
|
+
#timers;
|
|
1559
|
+
#peers = new Set;
|
|
1560
|
+
#byAttempt = new Map;
|
|
1561
|
+
#log;
|
|
1562
|
+
constructor(factory, options) {
|
|
1563
|
+
this.#factory = factory;
|
|
1564
|
+
this.#options = options;
|
|
1565
|
+
this.#timers = options.timers ?? systemRtcTimers;
|
|
1566
|
+
this.#log = options.log ?? (() => {});
|
|
1567
|
+
}
|
|
1568
|
+
hello() {
|
|
1569
|
+
return { stun: [...this.#options.stun()], signalOnly: true };
|
|
1570
|
+
}
|
|
1571
|
+
async offer(input) {
|
|
1572
|
+
if (input.attempt === "")
|
|
1573
|
+
throw new Error("rtcOffer requires a non-empty attempt id");
|
|
1574
|
+
const stun = this.#options.stun();
|
|
1575
|
+
if (stun.length === 0)
|
|
1576
|
+
this.#log("rtc-no-stun", { attempt: input.attempt });
|
|
1577
|
+
const key = `${input.deviceKey ?? ""}\x00${input.attempt}`;
|
|
1578
|
+
const existing = this.#byAttempt.get(key);
|
|
1579
|
+
if (existing !== undefined) {
|
|
1580
|
+
if (existing.answer !== undefined)
|
|
1581
|
+
return { sdp: existing.answer };
|
|
1582
|
+
if (existing.failure === undefined) {
|
|
1583
|
+
return { sdp: await Promise.race([existing.answerPromise, existing.closedSignal]) };
|
|
1584
|
+
}
|
|
1585
|
+
existing.close();
|
|
1586
|
+
}
|
|
1587
|
+
const perDevice = this.#options.perDeviceLimit ?? PER_DEVICE_PEERS;
|
|
1588
|
+
const hostLimit = this.#options.hostLimit ?? HOST_PEERS;
|
|
1589
|
+
if (input.deviceKey !== undefined) {
|
|
1590
|
+
const count = [...this.#peers].filter((peer2) => peer2.deviceKey === input.deviceKey).length;
|
|
1591
|
+
if (count >= perDevice) {
|
|
1592
|
+
throw new Error(`Too many WebRTC peers for this device (limit ${perDevice})`);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (this.#peers.size >= hostLimit) {
|
|
1596
|
+
throw new Error(`Too many WebRTC peers on this host (limit ${hostLimit})`);
|
|
1597
|
+
}
|
|
1598
|
+
const peer = new ManagedPeer(this, {
|
|
1599
|
+
attempt: input.attempt,
|
|
1600
|
+
owner: input.owner,
|
|
1601
|
+
...input.deviceKey === undefined ? {} : { deviceKey: input.deviceKey },
|
|
1602
|
+
timers: this.#timers
|
|
1603
|
+
});
|
|
1604
|
+
this.#peers.add(peer);
|
|
1605
|
+
this.#byAttempt.set(key, peer);
|
|
1606
|
+
let connection;
|
|
1607
|
+
try {
|
|
1608
|
+
connection = this.#factory.connect({
|
|
1609
|
+
offerSdp: input.sdp,
|
|
1610
|
+
stun,
|
|
1611
|
+
timers: this.#timers,
|
|
1612
|
+
onChannel: (channel) => this.#onChannel(peer, channel),
|
|
1613
|
+
onClosed: () => this.#remove(peer),
|
|
1614
|
+
log: this.#log
|
|
1615
|
+
});
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
this.#remove(peer);
|
|
1618
|
+
throw error;
|
|
1619
|
+
}
|
|
1620
|
+
peer.attach(connection);
|
|
1621
|
+
peer.answerPromise = connection.answer.then((sdp) => {
|
|
1622
|
+
if (peer.closed)
|
|
1623
|
+
throw new Error("WebRTC peer closed before its answer was ready");
|
|
1624
|
+
peer.answer = sdp;
|
|
1625
|
+
return sdp;
|
|
1626
|
+
}, (error) => {
|
|
1627
|
+
peer.failure = error instanceof Error ? error.message : String(error);
|
|
1628
|
+
this.#remove(peer);
|
|
1629
|
+
throw error;
|
|
1630
|
+
});
|
|
1631
|
+
peer.answerPromise.catch(() => {});
|
|
1632
|
+
peer.arm(this.#options.peerTimeoutMs ?? PEER_TIMEOUT_MS);
|
|
1633
|
+
return { sdp: await Promise.race([peer.answerPromise, peer.closedSignal]) };
|
|
1497
1634
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
if (raw.command !== undefined || raw.type === "stdio") {
|
|
1503
|
-
if (typeof raw.command !== "string" || raw.command === "")
|
|
1504
|
-
return fail(`stdio server needs a "command"`);
|
|
1505
|
-
if (raw.args !== undefined && (!Array.isArray(raw.args) || !raw.args.every((arg) => typeof arg === "string"))) {
|
|
1506
|
-
return fail(`"args" must be an array of strings`);
|
|
1635
|
+
#onChannel(peer, channel) {
|
|
1636
|
+
if (peer.closed) {
|
|
1637
|
+
channel.carrier.close(1008, "peer closed");
|
|
1638
|
+
return;
|
|
1507
1639
|
}
|
|
1508
|
-
if (
|
|
1509
|
-
|
|
1510
|
-
const env = expandAll(raw.env, "env", (message) => error(`${name}: ${message}`));
|
|
1511
|
-
if (raw.env !== undefined && env === undefined)
|
|
1640
|
+
if (peer.channel !== undefined) {
|
|
1641
|
+
channel.carrier.close(1008, "duplicate DataChannel");
|
|
1512
1642
|
return;
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
spec: { kind: "stdio", command: raw.command, args: raw.args, env, cwd },
|
|
1517
|
-
timeoutMs: timeout,
|
|
1518
|
-
exposure,
|
|
1519
|
-
...description === undefined ? {} : { description }
|
|
1520
|
-
};
|
|
1643
|
+
}
|
|
1644
|
+
peer.channel = channel;
|
|
1645
|
+
this.#options.onChannel(peer, channel);
|
|
1521
1646
|
}
|
|
1522
|
-
|
|
1523
|
-
if (
|
|
1524
|
-
return fail(`HTTP server needs a "url"`);
|
|
1525
|
-
const headers = expandAll(raw.headers, "headers", (message) => error(`${name}: ${message}`));
|
|
1526
|
-
if (raw.headers !== undefined && headers === undefined)
|
|
1647
|
+
#remove(peer) {
|
|
1648
|
+
if (!this.#peers.delete(peer))
|
|
1527
1649
|
return;
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
...description === undefined ? {} : { description }
|
|
1534
|
-
};
|
|
1650
|
+
for (const [key, candidate] of this.#byAttempt) {
|
|
1651
|
+
if (candidate === peer)
|
|
1652
|
+
this.#byAttempt.delete(key);
|
|
1653
|
+
}
|
|
1654
|
+
peer.teardown();
|
|
1535
1655
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
1542
|
-
} catch (error) {
|
|
1543
|
-
if (error.code === "ENOENT")
|
|
1544
|
-
return { errors: [], entries: [] };
|
|
1545
|
-
return { errors: [`${configPath}: ${error instanceof Error ? error.message : String(error)}`], entries: [] };
|
|
1656
|
+
closeOwner(owner) {
|
|
1657
|
+
for (const peer of [...this.#peers]) {
|
|
1658
|
+
if (peer.owner === owner && !peer.handedOff)
|
|
1659
|
+
peer.close();
|
|
1660
|
+
}
|
|
1546
1661
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1662
|
+
closeDevice(deviceKey) {
|
|
1663
|
+
for (const peer of [...this.#peers]) {
|
|
1664
|
+
if (peer.deviceKey === deviceKey && !peer.handedOff)
|
|
1665
|
+
peer.close();
|
|
1666
|
+
}
|
|
1549
1667
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1668
|
+
async close() {
|
|
1669
|
+
for (const peer of [...this.#peers])
|
|
1670
|
+
peer.close();
|
|
1671
|
+
}
|
|
1672
|
+
remove(peer) {
|
|
1673
|
+
this.#remove(peer);
|
|
1556
1674
|
}
|
|
1557
|
-
return { errors, entries };
|
|
1558
1675
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1676
|
+
|
|
1677
|
+
class ManagedPeer {
|
|
1678
|
+
attempt;
|
|
1679
|
+
deviceKey;
|
|
1680
|
+
owner;
|
|
1681
|
+
timers;
|
|
1682
|
+
answer;
|
|
1683
|
+
answerPromise = Promise.reject(new Error("peer has no connection"));
|
|
1684
|
+
closedSignal;
|
|
1685
|
+
failure;
|
|
1686
|
+
channel;
|
|
1687
|
+
handedOff = false;
|
|
1688
|
+
closed = false;
|
|
1689
|
+
#connection;
|
|
1690
|
+
#timer;
|
|
1691
|
+
#rejectClosed;
|
|
1692
|
+
#host;
|
|
1693
|
+
constructor(host, input) {
|
|
1694
|
+
this.#host = host;
|
|
1695
|
+
this.attempt = input.attempt;
|
|
1696
|
+
this.owner = input.owner;
|
|
1697
|
+
this.deviceKey = input.deviceKey;
|
|
1698
|
+
this.timers = input.timers;
|
|
1699
|
+
this.answerPromise.catch(() => {});
|
|
1700
|
+
this.closedSignal = new Promise((_, reject) => {
|
|
1701
|
+
this.#rejectClosed = reject;
|
|
1702
|
+
});
|
|
1703
|
+
this.closedSignal.catch(() => {});
|
|
1704
|
+
}
|
|
1705
|
+
attach(connection) {
|
|
1706
|
+
this.#connection = connection;
|
|
1707
|
+
}
|
|
1708
|
+
arm(timeoutMs) {
|
|
1709
|
+
if (this.closed)
|
|
1710
|
+
return;
|
|
1711
|
+
this.#timer = this.timers.setTimeout(() => {
|
|
1712
|
+
this.#timer = undefined;
|
|
1713
|
+
if (this.closed || this.handedOff)
|
|
1714
|
+
return;
|
|
1715
|
+
this.close();
|
|
1716
|
+
}, timeoutMs);
|
|
1717
|
+
}
|
|
1718
|
+
handOff() {
|
|
1719
|
+
if (this.closed)
|
|
1720
|
+
return;
|
|
1721
|
+
this.handedOff = true;
|
|
1722
|
+
if (this.#timer !== undefined) {
|
|
1723
|
+
this.timers.clearTimeout(this.#timer);
|
|
1724
|
+
this.#timer = undefined;
|
|
1587
1725
|
}
|
|
1588
|
-
}
|
|
1726
|
+
}
|
|
1727
|
+
close() {
|
|
1728
|
+
this.#host.remove(this);
|
|
1729
|
+
}
|
|
1730
|
+
teardown() {
|
|
1731
|
+
if (this.closed)
|
|
1732
|
+
return;
|
|
1733
|
+
this.closed = true;
|
|
1734
|
+
if (this.#timer !== undefined) {
|
|
1735
|
+
this.timers.clearTimeout(this.#timer);
|
|
1736
|
+
this.#timer = undefined;
|
|
1737
|
+
}
|
|
1738
|
+
this.#rejectClosed(new Error(this.failure ?? "WebRTC peer closed"));
|
|
1739
|
+
try {
|
|
1740
|
+
this.#connection?.close();
|
|
1741
|
+
} catch {}
|
|
1742
|
+
this.channel?.carrier.close(1008, "peer closed");
|
|
1743
|
+
}
|
|
1589
1744
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1745
|
+
function nativeModule(loaded) {
|
|
1746
|
+
const candidate = loaded.default ?? loaded;
|
|
1747
|
+
if (typeof candidate.PeerConnection !== "function") {
|
|
1748
|
+
throw new Error("node-datachannel did not export PeerConnection");
|
|
1749
|
+
}
|
|
1750
|
+
return candidate;
|
|
1751
|
+
}
|
|
1752
|
+
function nodeRtcFactory(loaded) {
|
|
1753
|
+
const ndc = nativeModule(loaded);
|
|
1754
|
+
return {
|
|
1755
|
+
connect(input) {
|
|
1756
|
+
const max = parseRtcMaxMessageSize(input.offerSdp);
|
|
1757
|
+
if (max.kind === "invalid")
|
|
1758
|
+
throw new Error(max.reason);
|
|
1759
|
+
const pc = new ndc.PeerConnection(`pinomad-${randomUUID()}`, { iceServers: [...input.stun] });
|
|
1760
|
+
const timers = input.timers;
|
|
1761
|
+
const candidates = new Set;
|
|
1762
|
+
let localSdp;
|
|
1763
|
+
let gatheringDone = false;
|
|
1764
|
+
let deadlineReached = false;
|
|
1765
|
+
let settled = false;
|
|
1766
|
+
let deadline;
|
|
1767
|
+
let pollTimer;
|
|
1768
|
+
let commitTimer;
|
|
1769
|
+
let resolveAnswer;
|
|
1770
|
+
let rejectAnswer;
|
|
1771
|
+
const answer = new Promise((resolve4, reject) => {
|
|
1772
|
+
resolveAnswer = resolve4;
|
|
1773
|
+
rejectAnswer = reject;
|
|
1774
|
+
});
|
|
1775
|
+
answer.catch(() => {});
|
|
1776
|
+
const addCandidate = (raw) => {
|
|
1777
|
+
const type = rtcCandidateType(raw);
|
|
1778
|
+
if (type !== "host" && type !== "srflx" && type !== "prflx")
|
|
1779
|
+
return;
|
|
1780
|
+
candidates.add(raw.startsWith("a=") ? raw.slice(2) : raw);
|
|
1781
|
+
};
|
|
1782
|
+
const stopTimers = () => {
|
|
1783
|
+
if (deadline !== undefined) {
|
|
1784
|
+
timers.clearTimeout(deadline);
|
|
1785
|
+
deadline = undefined;
|
|
1786
|
+
}
|
|
1787
|
+
if (pollTimer !== undefined) {
|
|
1788
|
+
timers.clearTimeout(pollTimer);
|
|
1789
|
+
pollTimer = undefined;
|
|
1790
|
+
}
|
|
1791
|
+
if (commitTimer !== undefined) {
|
|
1792
|
+
timers.clearTimeout(commitTimer);
|
|
1793
|
+
commitTimer = undefined;
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
const fail = (error) => {
|
|
1797
|
+
if (settled)
|
|
1798
|
+
return;
|
|
1799
|
+
settled = true;
|
|
1800
|
+
stopTimers();
|
|
1801
|
+
try {
|
|
1802
|
+
pc.close();
|
|
1803
|
+
} catch {}
|
|
1804
|
+
rejectAnswer(error);
|
|
1805
|
+
};
|
|
1806
|
+
const commit = () => {
|
|
1807
|
+
if (settled)
|
|
1808
|
+
return;
|
|
1809
|
+
const local = pc.localDescription();
|
|
1810
|
+
if (local !== null)
|
|
1811
|
+
localSdp = local.sdp;
|
|
1812
|
+
if (localSdp === undefined)
|
|
1813
|
+
return;
|
|
1814
|
+
for (const candidate of sdpCandidates(localSdp))
|
|
1815
|
+
addCandidate(candidate);
|
|
1816
|
+
if (candidates.size === 0) {
|
|
1817
|
+
if (deadlineReached)
|
|
1818
|
+
fail(new Error("no usable local ICE candidates"));
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
settled = true;
|
|
1822
|
+
stopTimers();
|
|
1823
|
+
resolveAnswer(withRtcCandidates(localSdp, [...candidates]));
|
|
1824
|
+
};
|
|
1825
|
+
const maybeCommit = () => {
|
|
1826
|
+
if (commitTimer !== undefined && !deadlineReached)
|
|
1827
|
+
return;
|
|
1828
|
+
if (localSdp !== undefined && (gatheringDone || deadlineReached))
|
|
1829
|
+
commit();
|
|
1830
|
+
};
|
|
1831
|
+
const scheduleCommit = () => {
|
|
1832
|
+
if (settled)
|
|
1833
|
+
return;
|
|
1834
|
+
if (commitTimer !== undefined)
|
|
1835
|
+
timers.clearTimeout(commitTimer);
|
|
1836
|
+
commitTimer = timers.setTimeout(() => {
|
|
1837
|
+
commitTimer = undefined;
|
|
1838
|
+
maybeCommit();
|
|
1839
|
+
}, 0);
|
|
1840
|
+
};
|
|
1841
|
+
pc.onLocalDescription((sdp) => {
|
|
1842
|
+
localSdp = sdp;
|
|
1843
|
+
if (gatheringDone)
|
|
1844
|
+
scheduleCommit();
|
|
1845
|
+
else
|
|
1846
|
+
maybeCommit();
|
|
1847
|
+
});
|
|
1848
|
+
pc.onLocalCandidate((candidate) => {
|
|
1849
|
+
addCandidate(candidate);
|
|
1850
|
+
if (gatheringDone)
|
|
1851
|
+
scheduleCommit();
|
|
1852
|
+
});
|
|
1853
|
+
pc.onGatheringStateChange((state) => {
|
|
1854
|
+
if (state === "complete") {
|
|
1855
|
+
gatheringDone = true;
|
|
1856
|
+
scheduleCommit();
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
pc.onStateChange((state) => {
|
|
1860
|
+
if (state === "closed" || state === "failed")
|
|
1861
|
+
input.onClosed();
|
|
1862
|
+
});
|
|
1863
|
+
pc.onDataChannel((channel) => {
|
|
1864
|
+
if (channel.getLabel() !== RTC_DATA_CHANNEL_LABEL) {
|
|
1865
|
+
channel.close();
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
let delivered = false;
|
|
1869
|
+
const ready = () => {
|
|
1870
|
+
if (delivered || settled === false)
|
|
1871
|
+
return;
|
|
1872
|
+
delivered = true;
|
|
1873
|
+
const limit = channel.maxMessageSize();
|
|
1874
|
+
if (limit !== 0 && limit < RTC_MAX_RECORD_BYTES) {
|
|
1875
|
+
input.log("rtc-datachannel-too-small", { maxMessageSize: limit });
|
|
1876
|
+
channel.close();
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
input.onChannel({
|
|
1880
|
+
carrier: dataChannelCarrier(channel, timers, input.log, () => input.onClosed())
|
|
1881
|
+
});
|
|
1882
|
+
};
|
|
1883
|
+
channel.onOpen(ready);
|
|
1884
|
+
if (channel.isOpen())
|
|
1885
|
+
ready();
|
|
1886
|
+
});
|
|
1887
|
+
const poll = () => {
|
|
1888
|
+
if (settled)
|
|
1889
|
+
return;
|
|
1890
|
+
const local = pc.localDescription();
|
|
1891
|
+
if (local !== null) {
|
|
1892
|
+
localSdp = local.sdp;
|
|
1893
|
+
if (gatheringDone)
|
|
1894
|
+
scheduleCommit();
|
|
1895
|
+
else
|
|
1896
|
+
maybeCommit();
|
|
1897
|
+
}
|
|
1898
|
+
if (!settled)
|
|
1899
|
+
pollTimer = timers.setTimeout(poll, 25);
|
|
1900
|
+
};
|
|
1901
|
+
pollTimer = timers.setTimeout(poll, 25);
|
|
1902
|
+
deadline = timers.setTimeout(() => {
|
|
1903
|
+
deadlineReached = true;
|
|
1904
|
+
const local = pc.localDescription();
|
|
1905
|
+
if (local !== null)
|
|
1906
|
+
localSdp = local.sdp;
|
|
1907
|
+
if (localSdp === undefined)
|
|
1908
|
+
fail(new Error(`no local description within ${CANDIDATE_BUDGET_MS} ms`));
|
|
1909
|
+
else
|
|
1910
|
+
commit();
|
|
1911
|
+
}, CANDIDATE_BUDGET_MS);
|
|
1912
|
+
try {
|
|
1913
|
+
pc.setRemoteDescription(input.offerSdp, "offer");
|
|
1914
|
+
} catch (error) {
|
|
1915
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
1622
1916
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1917
|
+
return {
|
|
1918
|
+
answer,
|
|
1919
|
+
close: () => {
|
|
1920
|
+
stopTimers();
|
|
1921
|
+
try {
|
|
1922
|
+
pc.close();
|
|
1923
|
+
} catch {}
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
function dataChannelCarrier(channel, timers, log, onClosed) {
|
|
1930
|
+
channel.setBufferedAmountLowThreshold(BUFFER_LOW_WATER);
|
|
1931
|
+
let handlers;
|
|
1932
|
+
let closed = false;
|
|
1933
|
+
let pumping = false;
|
|
1934
|
+
let falseSends = 0;
|
|
1935
|
+
let lastProgress = timers.now();
|
|
1936
|
+
let pollTimer;
|
|
1937
|
+
let stallTimer;
|
|
1938
|
+
const queue = [];
|
|
1939
|
+
const clearTimers = () => {
|
|
1940
|
+
if (pollTimer !== undefined) {
|
|
1941
|
+
timers.clearTimeout(pollTimer);
|
|
1942
|
+
pollTimer = undefined;
|
|
1943
|
+
}
|
|
1944
|
+
if (stallTimer !== undefined) {
|
|
1945
|
+
timers.clearTimeout(stallTimer);
|
|
1946
|
+
stallTimer = undefined;
|
|
1625
1947
|
}
|
|
1948
|
+
};
|
|
1949
|
+
const finish = () => {
|
|
1626
1950
|
if (closed)
|
|
1627
1951
|
return;
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
});
|
|
1635
|
-
set(name, { state: "connected", tools: tools.length, error: collisions.length === 0 ? undefined : collisions.join("; ") });
|
|
1636
|
-
install();
|
|
1952
|
+
closed = true;
|
|
1953
|
+
clearTimers();
|
|
1954
|
+
for (const item of queue.splice(0))
|
|
1955
|
+
item.resolve();
|
|
1956
|
+
handlers?.closed();
|
|
1957
|
+
onClosed?.();
|
|
1637
1958
|
};
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
if (error instanceof McpAuthRequiredError || error instanceof McpHttpError && error.status === 401) {
|
|
1646
|
-
return "requires sign-in (OAuth), not supported yet";
|
|
1959
|
+
const stall = () => {
|
|
1960
|
+
stallTimer = undefined;
|
|
1961
|
+
if (closed || queue.length === 0)
|
|
1962
|
+
return;
|
|
1963
|
+
if (timers.now() - lastProgress < PUMP_STALL_MS) {
|
|
1964
|
+
stallTimer = timers.setTimeout(stall, 1000);
|
|
1965
|
+
return;
|
|
1647
1966
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
`).slice(-5).join(`
|
|
1651
|
-
`);
|
|
1652
|
-
return stderr === undefined || stderr === "" ? base : `${base}
|
|
1653
|
-
${stderr}`;
|
|
1654
|
-
};
|
|
1655
|
-
const connect = async (name, spec, timeoutMs, description) => {
|
|
1656
|
-
const client = new McpClient({ name: "pinomad", version: "0.0.0" });
|
|
1657
|
-
live.add(client);
|
|
1658
|
-
let stdio;
|
|
1967
|
+
log("rtc-datachannel-stalled", { queued: queue.length, falseSends, buffered: channel.bufferedAmount() });
|
|
1968
|
+
finish();
|
|
1659
1969
|
try {
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
if (closed)
|
|
1663
|
-
return;
|
|
1664
|
-
let lastError;
|
|
1665
|
-
client.onError((error) => {
|
|
1666
|
-
lastError = error.message;
|
|
1667
|
-
});
|
|
1668
|
-
client.onClose(() => {
|
|
1669
|
-
if (!closed)
|
|
1670
|
-
fail(name, `disconnected${lastError === undefined ? "" : `: ${lastError}`}`);
|
|
1671
|
-
});
|
|
1672
|
-
client.onNotification("notifications/tools/list_changed", () => {
|
|
1673
|
-
relist(name, client, timeoutMs, description).catch((error) => {
|
|
1674
|
-
fail(name, error instanceof Error ? error.message : String(error));
|
|
1675
|
-
});
|
|
1676
|
-
});
|
|
1677
|
-
await relist(name, client, timeoutMs, description);
|
|
1678
|
-
} catch (error) {
|
|
1679
|
-
live.delete(client);
|
|
1680
|
-
if (!closed)
|
|
1681
|
-
fail(name, describeConnectError(error, stdio));
|
|
1682
|
-
await client.close().catch(() => {});
|
|
1683
|
-
}
|
|
1970
|
+
channel.close();
|
|
1971
|
+
} catch {}
|
|
1684
1972
|
};
|
|
1685
|
-
const
|
|
1686
|
-
|
|
1687
|
-
return
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1973
|
+
const pump = () => {
|
|
1974
|
+
if (closed || pumping)
|
|
1975
|
+
return;
|
|
1976
|
+
pumping = true;
|
|
1977
|
+
try {
|
|
1978
|
+
while (queue.length > 0 && !closed) {
|
|
1979
|
+
if (!channel.isOpen())
|
|
1980
|
+
break;
|
|
1981
|
+
if (channel.bufferedAmount() >= BUFFER_HIGH_WATER)
|
|
1982
|
+
break;
|
|
1983
|
+
const item = queue[0];
|
|
1984
|
+
const max = channel.maxMessageSize();
|
|
1985
|
+
if (max !== 0 && item.bytes.length > max) {
|
|
1986
|
+
log("rtc-datachannel-record-too-large", { bytes: item.bytes.length, maxMessageSize: max });
|
|
1987
|
+
finish();
|
|
1988
|
+
try {
|
|
1989
|
+
channel.close();
|
|
1990
|
+
} catch {}
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
if (channel.sendMessageBinary(item.bytes) === false)
|
|
1994
|
+
falseSends += 1;
|
|
1995
|
+
queue.shift();
|
|
1996
|
+
item.resolve();
|
|
1997
|
+
lastProgress = timers.now();
|
|
1998
|
+
}
|
|
1999
|
+
} finally {
|
|
2000
|
+
pumping = false;
|
|
1692
2001
|
}
|
|
1693
|
-
};
|
|
1694
|
-
loadConfig(options.configPath).then((loaded) => {
|
|
1695
2002
|
if (closed)
|
|
1696
2003
|
return;
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
servers.set(entry.name, { name: entry.name, state: "disabled", tools: 0 });
|
|
1701
|
-
else
|
|
1702
|
-
servers.set(entry.name, { name: entry.name, state: "connecting", tools: 0 });
|
|
2004
|
+
if (queue.length === 0) {
|
|
2005
|
+
clearTimers();
|
|
2006
|
+
return;
|
|
1703
2007
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
exposures.set(entry.name, entry.exposure);
|
|
1710
|
-
connect(entry.name, entry.spec, entry.timeoutMs, entry.description);
|
|
2008
|
+
if (pollTimer === undefined) {
|
|
2009
|
+
pollTimer = timers.setTimeout(() => {
|
|
2010
|
+
pollTimer = undefined;
|
|
2011
|
+
pump();
|
|
2012
|
+
}, FALLBACK_POLL_MS);
|
|
1711
2013
|
}
|
|
2014
|
+
if (stallTimer === undefined)
|
|
2015
|
+
stallTimer = timers.setTimeout(stall, 1000);
|
|
2016
|
+
};
|
|
2017
|
+
channel.onBufferedAmountLow(() => pump());
|
|
2018
|
+
channel.onMessage((message) => {
|
|
2019
|
+
if (typeof message === "string")
|
|
2020
|
+
handlers?.text();
|
|
2021
|
+
else if (message instanceof ArrayBuffer)
|
|
2022
|
+
handlers?.binary(new Uint8Array(message));
|
|
2023
|
+
else
|
|
2024
|
+
handlers?.binary(message);
|
|
2025
|
+
});
|
|
2026
|
+
channel.onClosed(finish);
|
|
2027
|
+
channel.onError((message) => {
|
|
2028
|
+
log("rtc-datachannel-error", { message });
|
|
2029
|
+
finish();
|
|
1712
2030
|
});
|
|
1713
2031
|
return {
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
2032
|
+
get open() {
|
|
2033
|
+
return !closed && channel.isOpen();
|
|
2034
|
+
},
|
|
2035
|
+
send(bytes) {
|
|
2036
|
+
if (closed || !channel.isOpen())
|
|
2037
|
+
return Promise.resolve();
|
|
2038
|
+
return new Promise((resolve4) => {
|
|
2039
|
+
queue.push({ bytes, resolve: resolve4 });
|
|
2040
|
+
pump();
|
|
2041
|
+
});
|
|
2042
|
+
},
|
|
2043
|
+
close(_code, _reason) {
|
|
2044
|
+
if (closed)
|
|
2045
|
+
return;
|
|
2046
|
+
finish();
|
|
2047
|
+
try {
|
|
2048
|
+
channel.close();
|
|
2049
|
+
} catch {}
|
|
2050
|
+
},
|
|
2051
|
+
handle(next) {
|
|
2052
|
+
handlers = next;
|
|
1718
2053
|
}
|
|
1719
2054
|
};
|
|
1720
2055
|
}
|
|
1721
2056
|
|
|
1722
|
-
// apps/host/src/
|
|
1723
|
-
import {
|
|
1724
|
-
import {
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
if (
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
if (slash <= 0 || slash === role.model.length - 1)
|
|
1748
|
-
return "model must be provider/modelId";
|
|
1749
|
-
}
|
|
1750
|
-
if (role.thinkingLevel !== undefined && !isThinkingLevel(role.thinkingLevel)) {
|
|
1751
|
-
return `thinkingLevel must be one of ${THINKING_LEVELS.join(", ")}`;
|
|
2057
|
+
// apps/host/src/folders.ts
|
|
2058
|
+
import { readdir, realpath as realpath5, stat as stat5 } from "node:fs/promises";
|
|
2059
|
+
import { isAbsolute, join as join6 } from "node:path";
|
|
2060
|
+
var denied = (error) => {
|
|
2061
|
+
const code = error?.code;
|
|
2062
|
+
return code === "EACCES" || code === "EPERM";
|
|
2063
|
+
};
|
|
2064
|
+
async function listFolders(path, home) {
|
|
2065
|
+
if (path !== undefined && !isAbsolute(path))
|
|
2066
|
+
throw new Error(`Not an absolute path: ${path}`);
|
|
2067
|
+
const given = path ?? home;
|
|
2068
|
+
const dir = await realpath5(given).catch((error) => {
|
|
2069
|
+
if (denied(error))
|
|
2070
|
+
throw new Error(`Permission denied: ${given}`);
|
|
2071
|
+
throw new Error(`No such directory: ${given}`);
|
|
2072
|
+
});
|
|
2073
|
+
if (!(await stat5(dir)).isDirectory())
|
|
2074
|
+
throw new Error(`Not a directory: ${given}`);
|
|
2075
|
+
let entries;
|
|
2076
|
+
try {
|
|
2077
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
if (denied(error))
|
|
2080
|
+
throw new Error(`Permission denied: ${given}`);
|
|
2081
|
+
throw error;
|
|
1752
2082
|
}
|
|
1753
|
-
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
// apps/host/src/agent-roles.ts
|
|
1757
|
-
var KNOWN_FIELDS = new Set(["description", "model", "thinkingLevel"]);
|
|
1758
|
-
async function load(dir) {
|
|
1759
|
-
const roles = [];
|
|
1760
|
-
const problems = [];
|
|
1761
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2083
|
+
const folders = [];
|
|
1762
2084
|
for (const entry of entries) {
|
|
1763
|
-
if (
|
|
1764
|
-
continue;
|
|
1765
|
-
const file = entry.name;
|
|
1766
|
-
const name = file.slice(0, -".md".length);
|
|
1767
|
-
const problem = (reason2) => void problems.push({ file, reason: reason2 });
|
|
1768
|
-
let parsed;
|
|
1769
|
-
try {
|
|
1770
|
-
parsed = parseFrontmatter(await readFile2(join6(dir, file), "utf8"));
|
|
1771
|
-
} catch (error) {
|
|
1772
|
-
problem(`Couldn't parse the file: ${error instanceof Error ? error.message : String(error)}`);
|
|
1773
|
-
continue;
|
|
1774
|
-
}
|
|
1775
|
-
if (typeof parsed.frontmatter !== "object" || parsed.frontmatter === null || Array.isArray(parsed.frontmatter)) {
|
|
1776
|
-
problem("Frontmatter must be a record of fields");
|
|
1777
|
-
continue;
|
|
1778
|
-
}
|
|
1779
|
-
const unknown = Object.keys(parsed.frontmatter).find((key) => !KNOWN_FIELDS.has(key));
|
|
1780
|
-
if (unknown !== undefined) {
|
|
1781
|
-
problem(`Unknown field "${unknown}"`);
|
|
1782
|
-
continue;
|
|
1783
|
-
}
|
|
1784
|
-
const bad = Object.entries(parsed.frontmatter).find(([, value]) => typeof value !== "string");
|
|
1785
|
-
if (bad !== undefined) {
|
|
1786
|
-
problem(`Field "${bad[0]}" must be a string`);
|
|
1787
|
-
continue;
|
|
1788
|
-
}
|
|
1789
|
-
const frontmatter = parsed.frontmatter;
|
|
1790
|
-
const role = {
|
|
1791
|
-
name,
|
|
1792
|
-
description: frontmatter.description ?? "",
|
|
1793
|
-
...frontmatter.model === undefined ? {} : { model: frontmatter.model },
|
|
1794
|
-
...frontmatter.thinkingLevel === undefined ? {} : { thinkingLevel: frontmatter.thinkingLevel },
|
|
1795
|
-
prompt: parsed.body.trim()
|
|
1796
|
-
};
|
|
1797
|
-
const reason = agentRoleProblem(role);
|
|
1798
|
-
if (reason !== undefined) {
|
|
1799
|
-
problem(reason);
|
|
2085
|
+
if (entry.name.startsWith("."))
|
|
1800
2086
|
continue;
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
roles.sort((a, b) => a.name.localeCompare(b.name));
|
|
1805
|
-
problems.sort((a, b) => a.file.localeCompare(b.file));
|
|
1806
|
-
return { roles, problems };
|
|
1807
|
-
}
|
|
1808
|
-
function serialize(role) {
|
|
1809
|
-
const fields = [
|
|
1810
|
-
`description: ${JSON.stringify(role.description)}`,
|
|
1811
|
-
...role.model === undefined ? [] : [`model: ${JSON.stringify(role.model)}`],
|
|
1812
|
-
...role.thinkingLevel === undefined ? [] : [`thinkingLevel: ${JSON.stringify(role.thinkingLevel)}`]
|
|
1813
|
-
];
|
|
1814
|
-
return `---
|
|
1815
|
-
${fields.join(`
|
|
1816
|
-
`)}
|
|
1817
|
-
---
|
|
1818
|
-
|
|
1819
|
-
${role.prompt}
|
|
1820
|
-
`;
|
|
1821
|
-
}
|
|
1822
|
-
var sameRole = (a, b) => a.name === b.name && a.description === b.description && a.model === b.model && a.thinkingLevel === b.thinkingLevel && a.prompt === b.prompt;
|
|
1823
|
-
var sameProblem = (a, b) => a.file === b.file && a.reason === b.reason;
|
|
1824
|
-
async function openAgentRoles(options) {
|
|
1825
|
-
const dir = options.dir;
|
|
1826
|
-
await mkdir2(dir, { recursive: true });
|
|
1827
|
-
const debounceMs = options.debounceMs ?? 150;
|
|
1828
|
-
const rescanMs = options.rescanMs ?? 1000;
|
|
1829
|
-
const published = new Published({ roles: [], problems: [] }, (a, b) => sameList(a.roles, b.roles, sameRole) && sameList(a.problems, b.problems, sameProblem));
|
|
1830
|
-
let closed = false;
|
|
1831
|
-
let reported = false;
|
|
1832
|
-
let timer;
|
|
1833
|
-
let queued = false;
|
|
1834
|
-
let chain = Promise.resolve();
|
|
1835
|
-
const loadOnce = async () => {
|
|
1836
|
-
try {
|
|
1837
|
-
published.set(await load(dir));
|
|
1838
|
-
reported = false;
|
|
1839
|
-
} catch (error) {
|
|
1840
|
-
if (!reported)
|
|
1841
|
-
console.error(`agent-roles reload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1842
|
-
reported = true;
|
|
1843
|
-
}
|
|
1844
|
-
};
|
|
1845
|
-
const reload = () => {
|
|
1846
|
-
if (closed)
|
|
1847
|
-
return Promise.resolve();
|
|
1848
|
-
queued = true;
|
|
1849
|
-
chain = chain.then(async () => {
|
|
1850
|
-
if (!queued || closed)
|
|
1851
|
-
return;
|
|
1852
|
-
queued = false;
|
|
1853
|
-
await loadOnce();
|
|
1854
|
-
});
|
|
1855
|
-
return chain;
|
|
1856
|
-
};
|
|
1857
|
-
const watcher = watch(dir, () => {
|
|
1858
|
-
if (closed)
|
|
1859
|
-
return;
|
|
1860
|
-
clearTimeout(timer);
|
|
1861
|
-
timer = setTimeout(() => void reload(), debounceMs);
|
|
1862
|
-
});
|
|
1863
|
-
watcher.on("error", (error) => {
|
|
1864
|
-
if (!reported)
|
|
1865
|
-
console.error(`agent-roles watch failed: ${error.message}`);
|
|
1866
|
-
reported = true;
|
|
1867
|
-
reload();
|
|
1868
|
-
});
|
|
1869
|
-
await reload();
|
|
1870
|
-
const rescan = setInterval(() => void reload(), rescanMs);
|
|
1871
|
-
let pending = Promise.resolve();
|
|
1872
|
-
const enqueue = (operation) => {
|
|
1873
|
-
const run2 = pending.then(operation, operation);
|
|
1874
|
-
pending = run2.then(() => {}, () => {});
|
|
1875
|
-
return run2;
|
|
1876
|
-
};
|
|
1877
|
-
const write = async (role, mustExist) => {
|
|
1878
|
-
const problem = agentRoleProblem(role);
|
|
1879
|
-
if (problem !== undefined)
|
|
1880
|
-
throw new Error(problem);
|
|
1881
|
-
await enqueue(async () => {
|
|
1882
|
-
const target = join6(dir, `${role.name}.md`);
|
|
1883
|
-
const exists = existsSync2(target);
|
|
1884
|
-
if (mustExist && !exists)
|
|
1885
|
-
throw new Error(`No such agent: "${role.name}"`);
|
|
1886
|
-
if (!mustExist && exists)
|
|
1887
|
-
throw new Error(`Agent "${role.name}" already exists`);
|
|
1888
|
-
const temp = join6(dir, `.${role.name}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
1889
|
-
try {
|
|
1890
|
-
await writeFile(temp, serialize(role));
|
|
1891
|
-
await rename(temp, target);
|
|
1892
|
-
} catch (error) {
|
|
1893
|
-
await rm2(temp, { force: true }).catch(() => {});
|
|
1894
|
-
throw error;
|
|
1895
|
-
}
|
|
1896
|
-
await reload();
|
|
1897
|
-
});
|
|
1898
|
-
};
|
|
1899
|
-
return {
|
|
1900
|
-
get value() {
|
|
1901
|
-
return published.value;
|
|
1902
|
-
},
|
|
1903
|
-
subscribe: (listener) => published.subscribe(listener),
|
|
1904
|
-
create: (role) => write(role, false),
|
|
1905
|
-
update: (role) => write(role, true),
|
|
1906
|
-
async remove(name) {
|
|
1907
|
-
const problem = agentNameProblem(name);
|
|
1908
|
-
if (problem !== undefined)
|
|
1909
|
-
throw new Error(problem);
|
|
1910
|
-
await enqueue(async () => {
|
|
1911
|
-
const target = join6(dir, `${name}.md`);
|
|
1912
|
-
if (!existsSync2(target))
|
|
1913
|
-
throw new Error(`No such agent: "${name}"`);
|
|
1914
|
-
await rm2(target);
|
|
1915
|
-
await reload();
|
|
1916
|
-
});
|
|
1917
|
-
},
|
|
1918
|
-
close() {
|
|
1919
|
-
closed = true;
|
|
1920
|
-
clearTimeout(timer);
|
|
1921
|
-
clearInterval(rescan);
|
|
1922
|
-
watcher.close();
|
|
1923
|
-
}
|
|
1924
|
-
};
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
// apps/host/src/host.ts
|
|
1928
|
-
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
1929
|
-
import lockfile from "proper-lockfile";
|
|
1930
|
-
|
|
1931
|
-
// apps/host/src/gateway.ts
|
|
1932
|
-
import { createServer } from "node:http";
|
|
1933
|
-
import { homedir as homedir2, networkInterfaces } from "node:os";
|
|
1934
|
-
import { join as join8, sep as sep5 } from "node:path";
|
|
1935
|
-
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
1936
|
-
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1937
|
-
import {
|
|
1938
|
-
AgentDoc as AgentDoc5,
|
|
1939
|
-
configure as configure3,
|
|
1940
|
-
InboxDoc,
|
|
1941
|
-
LiveDoc
|
|
1942
|
-
} from "@earendil-works/pi-durable";
|
|
1943
|
-
import { WebSocket, WebSocketServer } from "ws";
|
|
1944
|
-
import { Value as Value2 } from "typebox/value";
|
|
1945
|
-
|
|
1946
|
-
// apps/host/src/folders.ts
|
|
1947
|
-
import { readdir as readdir2, realpath as realpath5, stat as stat5 } from "node:fs/promises";
|
|
1948
|
-
import { isAbsolute as isAbsolute2, join as join7 } from "node:path";
|
|
1949
|
-
var denied = (error) => {
|
|
1950
|
-
const code = error?.code;
|
|
1951
|
-
return code === "EACCES" || code === "EPERM";
|
|
1952
|
-
};
|
|
1953
|
-
async function listFolders(path, home) {
|
|
1954
|
-
if (path !== undefined && !isAbsolute2(path))
|
|
1955
|
-
throw new Error(`Not an absolute path: ${path}`);
|
|
1956
|
-
const given = path ?? home;
|
|
1957
|
-
const dir = await realpath5(given).catch((error) => {
|
|
1958
|
-
if (denied(error))
|
|
1959
|
-
throw new Error(`Permission denied: ${given}`);
|
|
1960
|
-
throw new Error(`No such directory: ${given}`);
|
|
1961
|
-
});
|
|
1962
|
-
if (!(await stat5(dir)).isDirectory())
|
|
1963
|
-
throw new Error(`Not a directory: ${given}`);
|
|
1964
|
-
let entries;
|
|
1965
|
-
try {
|
|
1966
|
-
entries = await readdir2(dir, { withFileTypes: true });
|
|
1967
|
-
} catch (error) {
|
|
1968
|
-
if (denied(error))
|
|
1969
|
-
throw new Error(`Permission denied: ${given}`);
|
|
1970
|
-
throw error;
|
|
1971
|
-
}
|
|
1972
|
-
const folders = [];
|
|
1973
|
-
for (const entry of entries) {
|
|
1974
|
-
if (entry.name.startsWith("."))
|
|
1975
|
-
continue;
|
|
1976
|
-
let isDirectory = entry.isDirectory();
|
|
1977
|
-
if (!isDirectory && entry.isSymbolicLink()) {
|
|
1978
|
-
isDirectory = await stat5(join7(dir, entry.name)).then((target) => target.isDirectory(), () => false);
|
|
2087
|
+
let isDirectory = entry.isDirectory();
|
|
2088
|
+
if (!isDirectory && entry.isSymbolicLink()) {
|
|
2089
|
+
isDirectory = await stat5(join6(dir, entry.name)).then((target) => target.isDirectory(), () => false);
|
|
1979
2090
|
}
|
|
1980
2091
|
if (isDirectory)
|
|
1981
|
-
folders.push({ name: entry.name, path:
|
|
2092
|
+
folders.push({ name: entry.name, path: join6(dir, entry.name) });
|
|
1982
2093
|
}
|
|
1983
2094
|
folders.sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true, sensitivity: "base" }));
|
|
1984
2095
|
return { path: dir, home: await realpath5(home).catch(() => home), folders };
|
|
1985
2096
|
}
|
|
1986
2097
|
|
|
1987
2098
|
// apps/host/src/skill-expansion.ts
|
|
1988
|
-
import { readFile
|
|
2099
|
+
import { readFile } from "node:fs/promises";
|
|
1989
2100
|
import { stripFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
1990
2101
|
|
|
1991
2102
|
// packages/client/src/skill-blocks.ts
|
|
@@ -2104,7 +2215,7 @@ async function expandSkills(text, names, skills) {
|
|
|
2104
2215
|
continue;
|
|
2105
2216
|
}
|
|
2106
2217
|
try {
|
|
2107
|
-
const body = stripFrontmatter(await
|
|
2218
|
+
const body = stripFrontmatter(await readFile(skill.filePath, "utf8")).trim();
|
|
2108
2219
|
blocks.push({ name, location: skill.filePath, baseDir: skill.baseDir, body });
|
|
2109
2220
|
} catch (error) {
|
|
2110
2221
|
failures.push({ name, filePath: skill.filePath, message: error instanceof Error ? error.message : String(error) });
|
|
@@ -2164,7 +2275,7 @@ function imageProblem(images) {
|
|
|
2164
2275
|
// apps/host/src/image-refs.ts
|
|
2165
2276
|
import { createHash } from "node:crypto";
|
|
2166
2277
|
import { applyImmutable } from "@earendil-works/chord/delta";
|
|
2167
|
-
var
|
|
2278
|
+
var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2168
2279
|
var isImageBlock = (value) => value.type === "image" && typeof value.data === "string" && typeof value.mimeType === "string";
|
|
2169
2280
|
var hashed = new WeakMap;
|
|
2170
2281
|
function refOf(block) {
|
|
@@ -2230,7 +2341,7 @@ function withRef(block) {
|
|
|
2230
2341
|
return { ...rest, ref };
|
|
2231
2342
|
}
|
|
2232
2343
|
var HASH_YIELD_BYTES = 8 * 1024 * 1024;
|
|
2233
|
-
var yieldToEventLoop = () => new Promise((
|
|
2344
|
+
var yieldToEventLoop = () => new Promise((resolve4) => setImmediate(resolve4));
|
|
2234
2345
|
async function hashBlocks(blocks) {
|
|
2235
2346
|
let bytes = 0;
|
|
2236
2347
|
for (const block of blocks) {
|
|
@@ -2256,7 +2367,7 @@ function rewriteEntries(value) {
|
|
|
2256
2367
|
return out ?? value;
|
|
2257
2368
|
}
|
|
2258
2369
|
function rewriteEntry(value) {
|
|
2259
|
-
if (!
|
|
2370
|
+
if (!isObject(value) || value.model === undefined)
|
|
2260
2371
|
return value;
|
|
2261
2372
|
const model = rewriteMessages(value.model);
|
|
2262
2373
|
return model === value.model ? value : { ...value, model };
|
|
@@ -2273,7 +2384,7 @@ function rewriteMessages(value) {
|
|
|
2273
2384
|
return out ?? value;
|
|
2274
2385
|
}
|
|
2275
2386
|
function rewriteMessage(value) {
|
|
2276
|
-
if (!
|
|
2387
|
+
if (!isObject(value) || value.content === undefined)
|
|
2277
2388
|
return value;
|
|
2278
2389
|
const content = rewriteContent(value.content);
|
|
2279
2390
|
return content === value.content ? value : { ...value, content };
|
|
@@ -2284,7 +2395,7 @@ function rewriteContent(value) {
|
|
|
2284
2395
|
let out;
|
|
2285
2396
|
for (let i = 0;i < value.length; i++) {
|
|
2286
2397
|
const block = value[i];
|
|
2287
|
-
const next =
|
|
2398
|
+
const next = isObject(block) && isImageBlock(block) ? withRef(block) : block;
|
|
2288
2399
|
if (next !== block)
|
|
2289
2400
|
(out ??= value.slice())[i] = next;
|
|
2290
2401
|
}
|
|
@@ -2302,31 +2413,31 @@ function rewriteItems(value) {
|
|
|
2302
2413
|
return out ?? value;
|
|
2303
2414
|
}
|
|
2304
2415
|
function rewriteItem(value) {
|
|
2305
|
-
if (!
|
|
2416
|
+
if (!isObject(value) || value.content === undefined)
|
|
2306
2417
|
return value;
|
|
2307
2418
|
const content = rewriteContent(value.content);
|
|
2308
2419
|
return content === value.content ? value : { ...value, content };
|
|
2309
2420
|
}
|
|
2310
2421
|
function rewriteInbox(value) {
|
|
2311
|
-
if (!
|
|
2422
|
+
if (!isObject(value) || value.items === undefined)
|
|
2312
2423
|
return value;
|
|
2313
2424
|
const items = rewriteItems(value.items);
|
|
2314
2425
|
return items === value.items ? value : { ...value, items };
|
|
2315
2426
|
}
|
|
2316
2427
|
function rewriteGeneration(value) {
|
|
2317
|
-
if (!
|
|
2428
|
+
if (!isObject(value) || value.message === undefined)
|
|
2318
2429
|
return value;
|
|
2319
2430
|
const message = rewriteMessage(value.message);
|
|
2320
2431
|
return message === value.message ? value : { ...value, message };
|
|
2321
2432
|
}
|
|
2322
2433
|
function rewriteLive(value) {
|
|
2323
|
-
if (!
|
|
2434
|
+
if (!isObject(value) || value.generation === undefined)
|
|
2324
2435
|
return value;
|
|
2325
2436
|
const generation = rewriteGeneration(value.generation);
|
|
2326
2437
|
return generation === value.generation ? value : { ...value, generation };
|
|
2327
2438
|
}
|
|
2328
2439
|
function rewriteDocs(value) {
|
|
2329
|
-
if (!
|
|
2440
|
+
if (!isObject(value))
|
|
2330
2441
|
return value;
|
|
2331
2442
|
let out;
|
|
2332
2443
|
for (const key of ["pi.inbox", "pi.live"]) {
|
|
@@ -2340,7 +2451,7 @@ function rewriteDocs(value) {
|
|
|
2340
2451
|
return out ?? value;
|
|
2341
2452
|
}
|
|
2342
2453
|
function rewriteView(value) {
|
|
2343
|
-
if (!
|
|
2454
|
+
if (!isObject(value))
|
|
2344
2455
|
return value;
|
|
2345
2456
|
let out;
|
|
2346
2457
|
if (value.entries !== undefined) {
|
|
@@ -2368,7 +2479,7 @@ function rewriteSlot(slot, value) {
|
|
|
2368
2479
|
case "content":
|
|
2369
2480
|
return rewriteContent(value);
|
|
2370
2481
|
case "block":
|
|
2371
|
-
return
|
|
2482
|
+
return isObject(value) && isImageBlock(value) ? withRef(value) : value;
|
|
2372
2483
|
case "docs":
|
|
2373
2484
|
return rewriteDocs(value);
|
|
2374
2485
|
case "inbox":
|
|
@@ -2415,7 +2526,7 @@ function rewriteOp(op) {
|
|
|
2415
2526
|
return op;
|
|
2416
2527
|
}
|
|
2417
2528
|
}
|
|
2418
|
-
function
|
|
2529
|
+
function resolve4(root, path, length) {
|
|
2419
2530
|
let node = root;
|
|
2420
2531
|
for (let i = 0;i < length; i++) {
|
|
2421
2532
|
if (node === null || typeof node !== "object")
|
|
@@ -2432,36 +2543,36 @@ function reachesIntoImage(op, root) {
|
|
|
2432
2543
|
for (let length = 1;length <= depth; length++) {
|
|
2433
2544
|
if (slotAt(path.slice(0, length)) !== "block")
|
|
2434
2545
|
continue;
|
|
2435
|
-
const node =
|
|
2436
|
-
if (
|
|
2546
|
+
const node = resolve4(root, path, length);
|
|
2547
|
+
if (isObject(node) && isImageBlock(node))
|
|
2437
2548
|
return true;
|
|
2438
2549
|
}
|
|
2439
2550
|
return false;
|
|
2440
2551
|
}
|
|
2441
2552
|
function* contentArrays(view) {
|
|
2442
|
-
if (!
|
|
2553
|
+
if (!isObject(view))
|
|
2443
2554
|
return;
|
|
2444
2555
|
if (Array.isArray(view.entries)) {
|
|
2445
2556
|
for (const entry of view.entries) {
|
|
2446
|
-
if (!
|
|
2557
|
+
if (!isObject(entry) || !Array.isArray(entry.model))
|
|
2447
2558
|
continue;
|
|
2448
2559
|
for (const message of entry.model) {
|
|
2449
|
-
if (
|
|
2560
|
+
if (isObject(message) && Array.isArray(message.content))
|
|
2450
2561
|
yield message.content;
|
|
2451
2562
|
}
|
|
2452
2563
|
}
|
|
2453
2564
|
}
|
|
2454
|
-
if (!
|
|
2565
|
+
if (!isObject(view.docs))
|
|
2455
2566
|
return;
|
|
2456
2567
|
const inbox = view.docs["pi.inbox"];
|
|
2457
|
-
if (
|
|
2568
|
+
if (isObject(inbox) && Array.isArray(inbox.items)) {
|
|
2458
2569
|
for (const item of inbox.items) {
|
|
2459
|
-
if (
|
|
2570
|
+
if (isObject(item) && Array.isArray(item.content))
|
|
2460
2571
|
yield item.content;
|
|
2461
2572
|
}
|
|
2462
2573
|
}
|
|
2463
2574
|
const live = view.docs["pi.live"];
|
|
2464
|
-
if (
|
|
2575
|
+
if (isObject(live) && isObject(live.generation) && isObject(live.generation.message) && Array.isArray(live.generation.message.content)) {
|
|
2465
2576
|
yield live.generation.message.content;
|
|
2466
2577
|
}
|
|
2467
2578
|
}
|
|
@@ -2469,7 +2580,7 @@ function imageBlocks(view) {
|
|
|
2469
2580
|
const blocks = [];
|
|
2470
2581
|
for (const content of contentArrays(view)) {
|
|
2471
2582
|
for (const block of content) {
|
|
2472
|
-
if (
|
|
2583
|
+
if (isObject(block) && isImageBlock(block))
|
|
2473
2584
|
blocks.push(block);
|
|
2474
2585
|
}
|
|
2475
2586
|
}
|
|
@@ -2512,6 +2623,109 @@ class ImageRefs {
|
|
|
2512
2623
|
}
|
|
2513
2624
|
}
|
|
2514
2625
|
|
|
2626
|
+
// apps/host/src/image-variants.ts
|
|
2627
|
+
var PROFILES = {
|
|
2628
|
+
thumbnail: { maxWidth: 768, maxHeight: 768, maxBytes: 128 * 1024 },
|
|
2629
|
+
preview: { maxWidth: 1536, maxHeight: 1536, maxBytes: 384 * 1024 }
|
|
2630
|
+
};
|
|
2631
|
+
var MAX_IN_FLIGHT = 2;
|
|
2632
|
+
|
|
2633
|
+
class ImageVariants {
|
|
2634
|
+
#processor;
|
|
2635
|
+
#maxCacheBytes;
|
|
2636
|
+
#cache = new Map;
|
|
2637
|
+
#pending = new Map;
|
|
2638
|
+
#queue = [];
|
|
2639
|
+
#cacheBytes = 0;
|
|
2640
|
+
#active = 0;
|
|
2641
|
+
constructor(processor, options = {}) {
|
|
2642
|
+
this.#processor = processor;
|
|
2643
|
+
this.#maxCacheBytes = options.maxCacheBytes ?? 32 * 1024 * 1024;
|
|
2644
|
+
}
|
|
2645
|
+
async get(ref, variant, original) {
|
|
2646
|
+
const key = `${variant}:${ref}`;
|
|
2647
|
+
const cached = this.#read(key);
|
|
2648
|
+
if (cached !== undefined)
|
|
2649
|
+
return cached;
|
|
2650
|
+
let pending = this.#pending.get(key);
|
|
2651
|
+
if (pending === undefined) {
|
|
2652
|
+
pending = new Promise((resolve5, reject) => {
|
|
2653
|
+
this.#queue.push({ key, variant, original, resolve: resolve5, reject });
|
|
2654
|
+
});
|
|
2655
|
+
this.#pending.set(key, pending);
|
|
2656
|
+
this.#pump();
|
|
2657
|
+
}
|
|
2658
|
+
try {
|
|
2659
|
+
return await pending;
|
|
2660
|
+
} finally {
|
|
2661
|
+
if (this.#pending.get(key) === pending)
|
|
2662
|
+
this.#pending.delete(key);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
#pump() {
|
|
2666
|
+
while (this.#active < MAX_IN_FLIGHT) {
|
|
2667
|
+
const job = this.#queue.shift();
|
|
2668
|
+
if (job === undefined)
|
|
2669
|
+
return;
|
|
2670
|
+
this.#active += 1;
|
|
2671
|
+
this.#derive(job.key, job.variant, job.original).then((result2) => {
|
|
2672
|
+
job.resolve(result2);
|
|
2673
|
+
this.#finish();
|
|
2674
|
+
}, (error) => {
|
|
2675
|
+
job.reject(error instanceof Error ? error : new Error(String(error)));
|
|
2676
|
+
this.#finish();
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
#finish() {
|
|
2681
|
+
this.#active -= 1;
|
|
2682
|
+
this.#pump();
|
|
2683
|
+
}
|
|
2684
|
+
async#derive(key, variant, original) {
|
|
2685
|
+
if (this.#processor === undefined)
|
|
2686
|
+
return { ...original, variant: "original" };
|
|
2687
|
+
try {
|
|
2688
|
+
const bytes = new Uint8Array(Buffer.from(original.data, "base64"));
|
|
2689
|
+
const prepared = await this.#processor.prepare(bytes, original.mimeType, PROFILES[variant]);
|
|
2690
|
+
if (prepared === undefined || prepared.data === original.data && prepared.mimeType === original.mimeType) {
|
|
2691
|
+
return { ...original, variant: "original" };
|
|
2692
|
+
}
|
|
2693
|
+
const result2 = {
|
|
2694
|
+
data: prepared.data,
|
|
2695
|
+
mimeType: prepared.mimeType,
|
|
2696
|
+
variant
|
|
2697
|
+
};
|
|
2698
|
+
this.#write(key, result2);
|
|
2699
|
+
return result2;
|
|
2700
|
+
} catch {
|
|
2701
|
+
return { ...original, variant: "original" };
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
#read(key) {
|
|
2705
|
+
const cached = this.#cache.get(key);
|
|
2706
|
+
if (cached === undefined)
|
|
2707
|
+
return;
|
|
2708
|
+
this.#cache.delete(key);
|
|
2709
|
+
this.#cache.set(key, cached);
|
|
2710
|
+
return cached;
|
|
2711
|
+
}
|
|
2712
|
+
#write(key, result2) {
|
|
2713
|
+
const bytes = result2.data.length;
|
|
2714
|
+
if (bytes > this.#maxCacheBytes)
|
|
2715
|
+
return;
|
|
2716
|
+
while (this.#cacheBytes + bytes > this.#maxCacheBytes) {
|
|
2717
|
+
const oldest = this.#cache.keys().next().value;
|
|
2718
|
+
if (oldest === undefined)
|
|
2719
|
+
break;
|
|
2720
|
+
const previous = this.#cache.get(oldest);
|
|
2721
|
+
this.#cache.delete(oldest);
|
|
2722
|
+
this.#cacheBytes -= previous.data.length;
|
|
2723
|
+
}
|
|
2724
|
+
this.#cache.set(key, result2);
|
|
2725
|
+
this.#cacheBytes += bytes;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2515
2729
|
// apps/host/src/auto-title.ts
|
|
2516
2730
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2517
2731
|
|
|
@@ -2523,7 +2737,8 @@ var TitleDoc = defineDoc6({
|
|
|
2523
2737
|
scope: "conversation",
|
|
2524
2738
|
history: "latest",
|
|
2525
2739
|
fork: "initial",
|
|
2526
|
-
initial: () => ({})
|
|
2740
|
+
initial: () => ({}),
|
|
2741
|
+
checkpointWhen: () => false
|
|
2527
2742
|
});
|
|
2528
2743
|
|
|
2529
2744
|
// apps/host/src/auto-title.ts
|
|
@@ -2699,7 +2914,8 @@ var PushDoc = defineDoc7({
|
|
|
2699
2914
|
version: 2,
|
|
2700
2915
|
scope: "session",
|
|
2701
2916
|
initial: () => ({ registrations: [] }),
|
|
2702
|
-
migrate: () => ({ registrations: [] })
|
|
2917
|
+
migrate: () => ({ registrations: [] }),
|
|
2918
|
+
checkpointWhen: () => false
|
|
2703
2919
|
});
|
|
2704
2920
|
var APNS_TOKEN_PATTERN = /^(?:[0-9a-f]{2})+$/;
|
|
2705
2921
|
var APNS_TOKEN_MAX_CHARS = 256;
|
|
@@ -2827,7 +3043,7 @@ async function attachPushSender(options) {
|
|
|
2827
3043
|
return named;
|
|
2828
3044
|
if (notice.body !== "Finished" || disposed)
|
|
2829
3045
|
return FALLBACK_TITLE;
|
|
2830
|
-
return await new Promise((
|
|
3046
|
+
return await new Promise((resolve5) => {
|
|
2831
3047
|
const waiters = titleWaiters.get(notice.id) ?? new Set;
|
|
2832
3048
|
titleWaiters.set(notice.id, waiters);
|
|
2833
3049
|
const settle = (title) => {
|
|
@@ -2835,7 +3051,7 @@ async function attachPushSender(options) {
|
|
|
2835
3051
|
if (waiters.size === 0)
|
|
2836
3052
|
titleWaiters.delete(notice.id);
|
|
2837
3053
|
timers.clearTimeout(timer);
|
|
2838
|
-
|
|
3054
|
+
resolve5(title);
|
|
2839
3055
|
};
|
|
2840
3056
|
const timer = timers.setTimeout(() => settle(FALLBACK_TITLE), TITLE_WAIT_MS);
|
|
2841
3057
|
waiters.add(settle);
|
|
@@ -3067,11 +3283,48 @@ function serializedFork(harness, work) {
|
|
|
3067
3283
|
});
|
|
3068
3284
|
return next;
|
|
3069
3285
|
}
|
|
3286
|
+
var submissionTails = new WeakMap;
|
|
3287
|
+
function serializedSubmission(harness, conversationId, requestId, work) {
|
|
3288
|
+
let tails = submissionTails.get(harness);
|
|
3289
|
+
if (tails === undefined) {
|
|
3290
|
+
tails = new Map;
|
|
3291
|
+
submissionTails.set(harness, tails);
|
|
3292
|
+
}
|
|
3293
|
+
const lanes = tails;
|
|
3294
|
+
const key = `${conversationId}\x00${requestId}`;
|
|
3295
|
+
const next = (lanes.get(key) ?? Promise.resolve()).then(work, work);
|
|
3296
|
+
const settled = next.then(() => {}, () => {});
|
|
3297
|
+
lanes.set(key, settled);
|
|
3298
|
+
settled.then(() => {
|
|
3299
|
+
if (lanes.get(key) === settled)
|
|
3300
|
+
lanes.delete(key);
|
|
3301
|
+
});
|
|
3302
|
+
return next;
|
|
3303
|
+
}
|
|
3304
|
+
function submissionResult(record) {
|
|
3305
|
+
return {
|
|
3306
|
+
submissionId: record.id,
|
|
3307
|
+
status: record.status,
|
|
3308
|
+
...record.entry === undefined ? {} : { entry: record.entry }
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
function submitResult(record) {
|
|
3312
|
+
return {
|
|
3313
|
+
submissionId: record.id,
|
|
3314
|
+
...record.entry === undefined ? {} : { entry: record.entry }
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3070
3317
|
var LOOPBACK_SECURE_PATH = "/secure";
|
|
3071
3318
|
async function startGateway(options) {
|
|
3319
|
+
if (options.secure?.lan?.publicUrl !== undefined)
|
|
3320
|
+
validatePublicUrl(options.secure.lan.publicUrl);
|
|
3321
|
+
if (options.secure?.relay !== undefined) {
|
|
3322
|
+
validateRelayTarget(options.secure.relay.origin, options.secure.relay.signingKey.publicKey);
|
|
3323
|
+
}
|
|
3072
3324
|
const conversations = await ConversationList.open(options.harness, options.docs);
|
|
3073
3325
|
const fileLister = createCandidateLister(enumerateRoot);
|
|
3074
3326
|
const skillLister = createSkillLister((root, mode) => enumerateSkills(root, mode, options.agentsHome));
|
|
3327
|
+
const imageVariants = new ImageVariants(options.images);
|
|
3075
3328
|
const detachAutoTitle = options.autoTitle === false ? undefined : attachAutoTitle(conversations, {
|
|
3076
3329
|
harness: options.harness,
|
|
3077
3330
|
models: options.models,
|
|
@@ -3084,57 +3337,125 @@ async function startGateway(options) {
|
|
|
3084
3337
|
response.end();
|
|
3085
3338
|
});
|
|
3086
3339
|
});
|
|
3087
|
-
await new Promise((
|
|
3088
|
-
http.once("listening",
|
|
3340
|
+
await new Promise((resolve5, reject) => {
|
|
3341
|
+
http.once("listening", resolve5);
|
|
3089
3342
|
http.once("error", reject);
|
|
3090
3343
|
http.listen(options.port, "127.0.0.1");
|
|
3091
3344
|
});
|
|
3092
3345
|
const server = new WebSocketServer({ server: http });
|
|
3093
3346
|
const { port } = http.address();
|
|
3094
3347
|
const browserOrigins = [...options.browserOrigins ?? [], `http://127.0.0.1:${port}`];
|
|
3348
|
+
const connections = new Set;
|
|
3095
3349
|
const clients = new Set;
|
|
3350
|
+
let rtc;
|
|
3351
|
+
const rtcHello = () => rtc?.hello();
|
|
3096
3352
|
const broadcast = (level, message) => {
|
|
3097
3353
|
for (const client of clients)
|
|
3098
3354
|
client.send({ type: "notice", level, message });
|
|
3099
3355
|
};
|
|
3100
3356
|
let remote;
|
|
3101
3357
|
let relay;
|
|
3102
|
-
const
|
|
3103
|
-
|
|
3104
|
-
|
|
3358
|
+
const currentAdvertisement = () => {
|
|
3359
|
+
const advertiseUrl = remote?.advertiseUrl;
|
|
3360
|
+
const endpoints = remoteEndpointCandidates({
|
|
3361
|
+
...relay === undefined ? {} : { relay: { origin: relay.origin, hostId: relay.hostId } },
|
|
3362
|
+
...advertiseUrl === undefined ? {} : { direct: { advertiseUrl } }
|
|
3363
|
+
});
|
|
3364
|
+
return { endpoints, ...advertiseUrl === undefined ? {} : { advertiseUrl } };
|
|
3365
|
+
};
|
|
3366
|
+
const checkedEndpointSet = (endpoints) => {
|
|
3367
|
+
const parsed = parseEndpointSet({ schemaVersion: ENDPOINT_SET_SCHEMA_VERSION, endpoints });
|
|
3368
|
+
if (parsed.kind !== "valid" || parsed.endpointSet.endpoints.length !== endpoints.length) {
|
|
3369
|
+
throw new Error("The host generated an invalid remote endpoint set");
|
|
3105
3370
|
}
|
|
3106
|
-
|
|
3371
|
+
return parsed.endpointSet;
|
|
3372
|
+
};
|
|
3373
|
+
const currentEndpointSet = () => checkedEndpointSet(currentAdvertisement().endpoints);
|
|
3374
|
+
const register = (connection, devicePublicKey) => {
|
|
3375
|
+
const client = new GatewayClient(connection, options, conversations, fileLister, skillLister, imageVariants, {
|
|
3107
3376
|
devicePublicKey,
|
|
3377
|
+
rtcHello,
|
|
3108
3378
|
closed: (self) => {
|
|
3109
3379
|
clients.delete(self);
|
|
3380
|
+
connections.delete(self);
|
|
3381
|
+
rtc?.closeOwner(self);
|
|
3110
3382
|
self.dispose();
|
|
3111
3383
|
},
|
|
3112
3384
|
closeDevice: (publicKey) => {
|
|
3113
|
-
for (const
|
|
3114
|
-
if (
|
|
3115
|
-
|
|
3385
|
+
for (const connection2 of [...connections]) {
|
|
3386
|
+
if (connection2.devicePublicKey === publicKey) {
|
|
3387
|
+
connection2.closeConnection(UNAUTHORIZED_CLOSE_CODE, "device revoked");
|
|
3388
|
+
}
|
|
3116
3389
|
}
|
|
3390
|
+
rtc?.closeDevice(publicKey);
|
|
3391
|
+
},
|
|
3392
|
+
rtcOffer: (owner, key, attempt, sdp) => {
|
|
3393
|
+
if (rtc === undefined)
|
|
3394
|
+
throw new Error("WebRTC is not available on this host");
|
|
3395
|
+
return rtc.offer({ owner, ...key === undefined ? {} : { deviceKey: key }, attempt, sdp });
|
|
3117
3396
|
},
|
|
3118
3397
|
createPairing: () => {
|
|
3119
3398
|
const spec = options.secure;
|
|
3120
3399
|
if (spec === undefined)
|
|
3121
3400
|
return;
|
|
3122
3401
|
const pair = (secret2) => `pair=${toBase64Url(spec.hostKey.publicKey)}.${secret2}`;
|
|
3402
|
+
const fragment = (secret2, target2, endpointSet2) => `${pair(secret2)}&url=${encodeURIComponent(target2)}${endpointSet2 === undefined ? "" : `&endpointSet=${encodeEndpointSetParam(endpointSet2)}`}`;
|
|
3403
|
+
const advertisement = currentAdvertisement();
|
|
3404
|
+
const endpointSet = checkedEndpointSet(advertisement.endpoints);
|
|
3405
|
+
const relayReachable = endpointSet.endpoints.some((endpoint) => endpoint.role === "relay");
|
|
3406
|
+
if (relayReachable && relay !== undefined) {
|
|
3407
|
+
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3408
|
+
return {
|
|
3409
|
+
url: `${relay.origin}/#${fragment(secret2, relaySocketTarget(relay.origin, relay.hostId), endpointSet)}`,
|
|
3410
|
+
expiresAt: expiresAt2
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
if (endpointSet.endpoints.length > 0) {
|
|
3414
|
+
const direct = endpointSet.endpoints.find((endpoint) => endpoint.role === "direct");
|
|
3415
|
+
const advertise = advertisement.advertiseUrl;
|
|
3416
|
+
if (direct === undefined || advertise === undefined) {
|
|
3417
|
+
throw new Error("The host generated an invalid remote endpoint set");
|
|
3418
|
+
}
|
|
3419
|
+
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3420
|
+
return { url: `${advertise}/#${fragment(secret2, direct.url, endpointSet)}`, expiresAt: expiresAt2 };
|
|
3421
|
+
}
|
|
3123
3422
|
if (relay !== undefined) {
|
|
3124
3423
|
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3125
|
-
|
|
3126
|
-
|
|
3424
|
+
return {
|
|
3425
|
+
url: `${relay.origin}/#${fragment(secret2, relaySocketTarget(relay.origin, relay.hostId))}`,
|
|
3426
|
+
expiresAt: expiresAt2
|
|
3427
|
+
};
|
|
3127
3428
|
}
|
|
3128
3429
|
if (remote !== undefined) {
|
|
3430
|
+
const advertise = advertisement.advertiseUrl;
|
|
3431
|
+
if (advertise === undefined)
|
|
3432
|
+
throw new Error("The host generated an invalid remote endpoint set");
|
|
3129
3433
|
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3130
|
-
return { url: `${
|
|
3434
|
+
return { url: `${advertise}/#${fragment(secret2, advertiseEndpointUrl(advertise))}`, expiresAt: expiresAt2 };
|
|
3131
3435
|
}
|
|
3132
3436
|
const { secret, expiresAt } = spec.offers.create("local");
|
|
3133
3437
|
const target = `ws://127.0.0.1:${port}${LOOPBACK_SECURE_PATH}`;
|
|
3134
|
-
return { url: `http://127.0.0.1:${port}/#${
|
|
3135
|
-
}
|
|
3438
|
+
return { url: `http://127.0.0.1:${port}/#${fragment(secret, target)}`, expiresAt };
|
|
3439
|
+
},
|
|
3440
|
+
endpointSet: currentEndpointSet
|
|
3136
3441
|
});
|
|
3137
3442
|
clients.add(client);
|
|
3443
|
+
connections.add(client);
|
|
3444
|
+
};
|
|
3445
|
+
const registerSignal = (connection, devicePublicKey) => {
|
|
3446
|
+
const client = new SignalClient(connection, devicePublicKey, {
|
|
3447
|
+
rtcHello,
|
|
3448
|
+
rtcOffer: (owner, key, attempt, sdp) => {
|
|
3449
|
+
if (rtc === undefined)
|
|
3450
|
+
throw new Error("WebRTC is not available on this host");
|
|
3451
|
+
return rtc.offer({ owner, ...key === undefined ? {} : { deviceKey: key }, attempt, sdp });
|
|
3452
|
+
},
|
|
3453
|
+
closed: (self) => {
|
|
3454
|
+
connections.delete(self);
|
|
3455
|
+
rtc?.closeOwner(self);
|
|
3456
|
+
}
|
|
3457
|
+
});
|
|
3458
|
+
connections.add(client);
|
|
3138
3459
|
};
|
|
3139
3460
|
const admission = {
|
|
3140
3461
|
harness: options.harness,
|
|
@@ -3143,6 +3464,26 @@ async function startGateway(options) {
|
|
|
3143
3464
|
return;
|
|
3144
3465
|
})
|
|
3145
3466
|
};
|
|
3467
|
+
const secure = options.secure;
|
|
3468
|
+
const handshakeHandlers = {
|
|
3469
|
+
client: register,
|
|
3470
|
+
signal: registerSignal,
|
|
3471
|
+
prologues: "both"
|
|
3472
|
+
};
|
|
3473
|
+
if (secure !== undefined) {
|
|
3474
|
+
rtc = await startRtcHost({
|
|
3475
|
+
...options.rtc,
|
|
3476
|
+
stun: () => options.settings.value.rtcStun ?? RTC_DEFAULT_STUN,
|
|
3477
|
+
onChannel: (peer, channel) => {
|
|
3478
|
+
secureHandshake(channel.carrier, secure, admission, {
|
|
3479
|
+
client: register,
|
|
3480
|
+
prologues: "business",
|
|
3481
|
+
...peer.deviceKey === undefined ? {} : { expectedDeviceKey: peer.deviceKey },
|
|
3482
|
+
onRegistered: () => peer.handOff()
|
|
3483
|
+
});
|
|
3484
|
+
}
|
|
3485
|
+
});
|
|
3486
|
+
}
|
|
3146
3487
|
server.on("connection", (socket, request) => {
|
|
3147
3488
|
socket.on("error", () => socket.terminate());
|
|
3148
3489
|
const url = new URL(request.url ?? "/", "ws://127.0.0.1");
|
|
@@ -3152,7 +3493,7 @@ async function startGateway(options) {
|
|
|
3152
3493
|
socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
3153
3494
|
return;
|
|
3154
3495
|
}
|
|
3155
|
-
secureHandshake(socket, secure2, admission,
|
|
3496
|
+
secureHandshake(webSocketCarrier(socket), secure2, admission, handshakeHandlers);
|
|
3156
3497
|
return;
|
|
3157
3498
|
}
|
|
3158
3499
|
const token = url.searchParams.get("token");
|
|
@@ -3163,12 +3504,11 @@ async function startGateway(options) {
|
|
|
3163
3504
|
}
|
|
3164
3505
|
register(tokenConnection(socket));
|
|
3165
3506
|
});
|
|
3166
|
-
remote = await startRemote(options, admission,
|
|
3167
|
-
await new Promise((
|
|
3168
|
-
await new Promise((
|
|
3507
|
+
remote = await startRemote(options, admission, handshakeHandlers).catch(async (error) => {
|
|
3508
|
+
await new Promise((resolve5) => server.close(() => resolve5()));
|
|
3509
|
+
await new Promise((resolve5) => http.close(() => resolve5()));
|
|
3169
3510
|
throw error;
|
|
3170
3511
|
});
|
|
3171
|
-
const secure = options.secure;
|
|
3172
3512
|
const detachSettingsWatch = options.settings.subscribe((settings) => {
|
|
3173
3513
|
if (!settings.acceptAccountDevices)
|
|
3174
3514
|
secure?.offers.revokeAccount();
|
|
@@ -3180,7 +3520,7 @@ async function startGateway(options) {
|
|
|
3180
3520
|
origin: relayOrigin,
|
|
3181
3521
|
signingKey: secure.relay.signingKey,
|
|
3182
3522
|
noisePublicKey: secure.hostKey.publicKey,
|
|
3183
|
-
accept: (socket) => secureHandshake(socket, secure, admission,
|
|
3523
|
+
accept: (socket) => secureHandshake(webSocketCarrier(socket), secure, admission, handshakeHandlers),
|
|
3184
3524
|
hostName: {
|
|
3185
3525
|
get value() {
|
|
3186
3526
|
return options.settings.value.hostName;
|
|
@@ -3198,8 +3538,15 @@ async function startGateway(options) {
|
|
|
3198
3538
|
if (normalizeOrigin(config.origin) !== normalizeOrigin(relayOrigin)) {
|
|
3199
3539
|
return { ok: false, reason: "rebinding" };
|
|
3200
3540
|
}
|
|
3541
|
+
let endpointSet;
|
|
3542
|
+
try {
|
|
3543
|
+
const current = checkedEndpointSet(currentAdvertisement().endpoints);
|
|
3544
|
+
endpointSet = current.endpoints.length === 0 ? undefined : current;
|
|
3545
|
+
} catch {
|
|
3546
|
+
return { ok: false, reason: "invalid_endpoint" };
|
|
3547
|
+
}
|
|
3201
3548
|
const { secret, expiresAt } = secure.offers.createAccount(config.binding);
|
|
3202
|
-
return { ok: true, secret, expiresAt };
|
|
3549
|
+
return { ok: true, secret, expiresAt, ...endpointSet === undefined ? {} : { endpointSet } };
|
|
3203
3550
|
},
|
|
3204
3551
|
onStateChange: (state) => {
|
|
3205
3552
|
if (state.status === "registered") {
|
|
@@ -3224,9 +3571,15 @@ async function startGateway(options) {
|
|
|
3224
3571
|
...options.push.transport === undefined ? relay === undefined ? {} : { transport: { send: (request) => relay.sendPush(request) } } : { transport: options.push.transport },
|
|
3225
3572
|
...options.push.timers === undefined ? {} : { timers: options.push.timers }
|
|
3226
3573
|
});
|
|
3574
|
+
const remoteInfo = remote === undefined ? undefined : {
|
|
3575
|
+
url: remote.url,
|
|
3576
|
+
get advertiseUrl() {
|
|
3577
|
+
return remote.advertiseUrl;
|
|
3578
|
+
}
|
|
3579
|
+
};
|
|
3227
3580
|
return {
|
|
3228
3581
|
url: `ws://127.0.0.1:${port}`,
|
|
3229
|
-
...
|
|
3582
|
+
...remoteInfo === undefined ? {} : { remote: remoteInfo },
|
|
3230
3583
|
...relay === undefined ? {} : { relay },
|
|
3231
3584
|
broadcast,
|
|
3232
3585
|
async close() {
|
|
@@ -3235,21 +3588,22 @@ async function startGateway(options) {
|
|
|
3235
3588
|
detachSettingsWatch();
|
|
3236
3589
|
await detachPush?.close();
|
|
3237
3590
|
conversations.dispose();
|
|
3238
|
-
await
|
|
3591
|
+
await rtc?.close();
|
|
3592
|
+
await Promise.all([...connections].map((connection) => connection.dispose()));
|
|
3239
3593
|
for (const socket of server.clients)
|
|
3240
3594
|
socket.terminate();
|
|
3241
3595
|
await remote?.close();
|
|
3242
|
-
await new Promise((
|
|
3243
|
-
await new Promise((
|
|
3596
|
+
await new Promise((resolve5) => server.close(() => resolve5()));
|
|
3597
|
+
await new Promise((resolve5) => http.close(() => resolve5()));
|
|
3244
3598
|
}
|
|
3245
3599
|
};
|
|
3246
3600
|
}
|
|
3247
3601
|
function tokenConnection(socket) {
|
|
3248
3602
|
return {
|
|
3249
3603
|
get open() {
|
|
3250
|
-
return socket.readyState ===
|
|
3604
|
+
return socket.readyState === WebSocket2.OPEN;
|
|
3251
3605
|
},
|
|
3252
|
-
send: (text) => socket.readyState ===
|
|
3606
|
+
send: (text) => socket.readyState === WebSocket2.OPEN ? new Promise((resolve5) => socket.send(text, () => resolve5())) : Promise.resolve(),
|
|
3253
3607
|
close: (code, reason) => socket.close(code, reason),
|
|
3254
3608
|
handle(handlers) {
|
|
3255
3609
|
socket.on("message", (data) => handlers.message(String(data)));
|
|
@@ -3257,30 +3611,6 @@ function tokenConnection(socket) {
|
|
|
3257
3611
|
}
|
|
3258
3612
|
};
|
|
3259
3613
|
}
|
|
3260
|
-
function secureConnection(socket, session) {
|
|
3261
|
-
return {
|
|
3262
|
-
get open() {
|
|
3263
|
-
return socket.readyState === WebSocket.OPEN;
|
|
3264
|
-
},
|
|
3265
|
-
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve6) => {
|
|
3266
|
-
const chunks = session.seal(text);
|
|
3267
|
-
chunks.forEach((chunk, index) => socket.send(chunk, index === chunks.length - 1 ? () => resolve6() : undefined));
|
|
3268
|
-
}) : Promise.resolve(),
|
|
3269
|
-
close: (code, reason) => socket.close(code, reason),
|
|
3270
|
-
handle(handlers) {
|
|
3271
|
-
socket.on("message", (data) => {
|
|
3272
|
-
try {
|
|
3273
|
-
const text = session.open(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
3274
|
-
if (text !== undefined)
|
|
3275
|
-
handlers.message(text);
|
|
3276
|
-
} catch {
|
|
3277
|
-
socket.close(1008, "corrupt secure record");
|
|
3278
|
-
}
|
|
3279
|
-
});
|
|
3280
|
-
socket.once("close", () => handlers.closed());
|
|
3281
|
-
}
|
|
3282
|
-
};
|
|
3283
|
-
}
|
|
3284
3614
|
var RFC1918 = /^(?:10\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)/;
|
|
3285
3615
|
var PREFERRED_NAME = /^(?:en|eth|wlan|wl)/;
|
|
3286
3616
|
var VIRTUAL_NAME = /^(?:utun|tun|tap|wg|ppp|ipsec|bridge|docker|veth|vmnet|llw|awdl)/;
|
|
@@ -3297,65 +3627,219 @@ function pickLanAddress(interfaces) {
|
|
|
3297
3627
|
}
|
|
3298
3628
|
return fallback;
|
|
3299
3629
|
}
|
|
3300
|
-
function
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3630
|
+
function advertiseEndpointUrl(advertiseUrl) {
|
|
3631
|
+
const url = new URL(advertiseUrl);
|
|
3632
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
3633
|
+
if (!url.pathname.endsWith("/"))
|
|
3634
|
+
url.pathname = `${url.pathname}/`;
|
|
3635
|
+
return url.href;
|
|
3636
|
+
}
|
|
3637
|
+
var normalizeAdvertiseUrl = (url) => url.replace(/\/+$/, "");
|
|
3638
|
+
function relaySocketTarget(origin, hostId) {
|
|
3639
|
+
return `${relayWsBase(origin)}/c/${hostId}`;
|
|
3640
|
+
}
|
|
3641
|
+
function remoteEndpointCandidates(input) {
|
|
3642
|
+
const endpoints = [];
|
|
3643
|
+
if (input.relay !== undefined) {
|
|
3644
|
+
const endpoint = {
|
|
3645
|
+
transport: "webSocket",
|
|
3646
|
+
role: "relay",
|
|
3647
|
+
url: relaySocketTarget(input.relay.origin, input.relay.hostId)
|
|
3648
|
+
};
|
|
3649
|
+
if (!isLoopbackUrl(endpoint.url))
|
|
3650
|
+
endpoints.push(endpoint);
|
|
3651
|
+
}
|
|
3652
|
+
if (input.direct !== undefined) {
|
|
3653
|
+
const endpoint = {
|
|
3654
|
+
transport: "webSocket",
|
|
3655
|
+
role: "direct",
|
|
3656
|
+
url: advertiseEndpointUrl(input.direct.advertiseUrl)
|
|
3657
|
+
};
|
|
3658
|
+
if (!isLoopbackUrl(endpoint.url))
|
|
3659
|
+
endpoints.push(endpoint);
|
|
3660
|
+
}
|
|
3661
|
+
return endpoints;
|
|
3662
|
+
}
|
|
3663
|
+
function endpointProblem(endpoint) {
|
|
3664
|
+
if (endpoint.url.length > MAX_HOST_ENDPOINT_URL_LENGTH) {
|
|
3665
|
+
return `is longer than ${MAX_HOST_ENDPOINT_URL_LENGTH} UTF-16 units`;
|
|
3666
|
+
}
|
|
3667
|
+
const parsed = parseEndpointSet({ schemaVersion: ENDPOINT_SET_SCHEMA_VERSION, endpoints: [endpoint] });
|
|
3668
|
+
return parsed.kind === "valid" && parsed.endpointSet.endpoints.length === 1 ? undefined : "is not a ws:// or wss:// URL";
|
|
3669
|
+
}
|
|
3670
|
+
function validatePublicUrl(publicUrl) {
|
|
3671
|
+
if (publicUrl.includes("#")) {
|
|
3672
|
+
throw new Error(`--public-url must not include a fragment: ${publicUrl}`);
|
|
3673
|
+
}
|
|
3674
|
+
let url;
|
|
3675
|
+
try {
|
|
3676
|
+
url = new URL(publicUrl);
|
|
3677
|
+
} catch {
|
|
3678
|
+
throw new Error(`--public-url must be an absolute http(s) URL: ${publicUrl}`);
|
|
3679
|
+
}
|
|
3680
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
3681
|
+
throw new Error(`--public-url must be an absolute http(s) URL: ${publicUrl}`);
|
|
3682
|
+
}
|
|
3683
|
+
const problem = endpointProblem({
|
|
3684
|
+
transport: "webSocket",
|
|
3685
|
+
role: "direct",
|
|
3686
|
+
url: advertiseEndpointUrl(normalizeAdvertiseUrl(publicUrl))
|
|
3687
|
+
});
|
|
3688
|
+
if (problem !== undefined)
|
|
3689
|
+
throw new Error(`--public-url ${problem}: ${publicUrl}`);
|
|
3690
|
+
}
|
|
3691
|
+
function validateRelayOrigin(origin) {
|
|
3692
|
+
if (origin.includes("#")) {
|
|
3693
|
+
throw new Error(`--relay must not include a fragment: ${origin}`);
|
|
3694
|
+
}
|
|
3695
|
+
try {
|
|
3696
|
+
relayWsBase(origin);
|
|
3697
|
+
} catch {
|
|
3698
|
+
throw new Error(`--relay must be an absolute http(s) URL: ${origin}`);
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
function validateRelayTarget(origin, signingKey) {
|
|
3702
|
+
validateRelayOrigin(origin);
|
|
3703
|
+
const hostId = relayHostId(signingKey);
|
|
3704
|
+
const target = relaySocketTarget(origin, hostId);
|
|
3705
|
+
const problem = endpointProblem({ transport: "webSocket", role: "relay", url: target });
|
|
3706
|
+
if (problem !== undefined)
|
|
3707
|
+
throw new Error(`--relay ${problem}: ${origin}`);
|
|
3708
|
+
}
|
|
3709
|
+
function secureHandshake(carrier, secure, policy, handlers) {
|
|
3710
|
+
let stage = "new";
|
|
3711
|
+
let session;
|
|
3712
|
+
let pending;
|
|
3713
|
+
let closed = false;
|
|
3714
|
+
let sendTail = Promise.resolve();
|
|
3715
|
+
const timeout = setTimeout(() => carrier.close(1008, "handshake timeout"), secure.handshakeTimeoutMs ?? 1e4);
|
|
3716
|
+
const connection = {
|
|
3717
|
+
get open() {
|
|
3718
|
+
return !closed && carrier.open;
|
|
3719
|
+
},
|
|
3720
|
+
send(text) {
|
|
3721
|
+
if (closed || session === undefined)
|
|
3722
|
+
return Promise.resolve();
|
|
3723
|
+
const next = sendTail.then(async () => {
|
|
3724
|
+
if (closed || session === undefined)
|
|
3725
|
+
return;
|
|
3726
|
+
for (const chunk of session.seal(text))
|
|
3727
|
+
await carrier.send(chunk);
|
|
3728
|
+
}).catch(() => carrier.close(1011, "send failed"));
|
|
3729
|
+
sendTail = next;
|
|
3730
|
+
return next;
|
|
3731
|
+
},
|
|
3732
|
+
close: (code, reason) => carrier.close(code, reason ?? ""),
|
|
3733
|
+
handle: (next) => {
|
|
3734
|
+
pending = next;
|
|
3313
3735
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
3326
|
-
let hello;
|
|
3327
|
-
let remoteStatic;
|
|
3328
|
-
try {
|
|
3329
|
-
const opened = responder.readMessage1(bytes);
|
|
3330
|
-
remoteStatic = opened.remoteStatic;
|
|
3331
|
-
hello = decodeClientHello(opened.payload);
|
|
3332
|
-
} catch {
|
|
3333
|
-
socket.close(1008, "bad handshake");
|
|
3736
|
+
};
|
|
3737
|
+
carrier.handle({
|
|
3738
|
+
binary(bytes) {
|
|
3739
|
+
if (stage === "settled") {
|
|
3740
|
+
try {
|
|
3741
|
+
const text = session.open(bytes);
|
|
3742
|
+
if (text !== undefined)
|
|
3743
|
+
pending?.message(text);
|
|
3744
|
+
} catch {
|
|
3745
|
+
carrier.close(1008, "corrupt secure record");
|
|
3746
|
+
}
|
|
3334
3747
|
return;
|
|
3335
3748
|
}
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
publicKey: deviceKey,
|
|
3339
|
-
...hello.pair === undefined ? {} : { pair: hello.pair },
|
|
3340
|
-
offers: secure.offers,
|
|
3341
|
-
acceptAccountDevices: policy.acceptAccountDevices,
|
|
3342
|
-
serverBinding: policy.serverBinding
|
|
3343
|
-
}, context3);
|
|
3344
|
-
if (decision.outcome === "refused") {
|
|
3345
|
-
socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
3749
|
+
if (stage === "deciding") {
|
|
3750
|
+
carrier.close(1008, "expected handshake");
|
|
3346
3751
|
return;
|
|
3347
3752
|
}
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3753
|
+
stage = "deciding";
|
|
3754
|
+
admit(bytes).then(async (admitted) => {
|
|
3755
|
+
if (admitted === undefined)
|
|
3756
|
+
return;
|
|
3757
|
+
if (closed)
|
|
3758
|
+
return;
|
|
3759
|
+
clearTimeout(timeout);
|
|
3760
|
+
session = new SecureSession(admitted.result);
|
|
3761
|
+
stage = "settled";
|
|
3762
|
+
await carrier.send(admitted.message);
|
|
3763
|
+
if (admitted.purpose === "signal")
|
|
3764
|
+
handlers.signal?.(connection, admitted.deviceKey);
|
|
3765
|
+
else
|
|
3766
|
+
handlers.client(connection, admitted.deviceKey, admitted.joined);
|
|
3767
|
+
handlers.onRegistered?.();
|
|
3768
|
+
}).catch(() => carrier.close(1011, "handshake failed"));
|
|
3769
|
+
async function admit(bytes2) {
|
|
3770
|
+
let purpose = "business";
|
|
3771
|
+
let opened;
|
|
3772
|
+
let responder = respondIK({ prologue: SECURE_PROLOGUE, static: secure.hostKey });
|
|
3773
|
+
try {
|
|
3774
|
+
opened = responder.readMessage1(bytes2);
|
|
3775
|
+
} catch {
|
|
3776
|
+
if (handlers.prologues === "business") {
|
|
3777
|
+
carrier.close(1008, "bad handshake");
|
|
3778
|
+
return;
|
|
3779
|
+
}
|
|
3780
|
+
responder = respondIK({ prologue: SIGNAL_PROLOGUE, static: secure.hostKey });
|
|
3781
|
+
try {
|
|
3782
|
+
opened = responder.readMessage1(bytes2);
|
|
3783
|
+
purpose = "signal";
|
|
3784
|
+
} catch {
|
|
3785
|
+
carrier.close(1008, "bad handshake");
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
let hello;
|
|
3790
|
+
try {
|
|
3791
|
+
hello = decodeClientHello(opened.payload);
|
|
3792
|
+
} catch {
|
|
3793
|
+
carrier.close(1008, "bad handshake");
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
if (purpose === "signal" && hello.pair !== undefined) {
|
|
3797
|
+
carrier.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
const deviceKey = toBase64Url(opened.remoteStatic);
|
|
3801
|
+
if (handlers.expectedDeviceKey !== undefined && deviceKey !== handlers.expectedDeviceKey) {
|
|
3802
|
+
carrier.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
const decision = await admitDevice(policy.harness, {
|
|
3806
|
+
publicKey: deviceKey,
|
|
3807
|
+
...hello.pair === undefined ? {} : { pair: hello.pair },
|
|
3808
|
+
offers: secure.offers,
|
|
3809
|
+
acceptAccountDevices: policy.acceptAccountDevices,
|
|
3810
|
+
serverBinding: policy.serverBinding
|
|
3811
|
+
}, context3);
|
|
3812
|
+
if (decision.outcome === "refused") {
|
|
3813
|
+
carrier.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
if (purpose === "signal" && decision.outcome !== "registered") {
|
|
3817
|
+
carrier.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3820
|
+
const { message, result: result2 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
|
|
3821
|
+
return {
|
|
3822
|
+
result: result2,
|
|
3823
|
+
message,
|
|
3824
|
+
deviceKey,
|
|
3825
|
+
purpose,
|
|
3826
|
+
...decision.outcome === "admitted" ? { joined: decision.entry } : {}
|
|
3827
|
+
};
|
|
3828
|
+
}
|
|
3829
|
+
},
|
|
3830
|
+
text() {
|
|
3831
|
+
carrier.close(1008, stage === "settled" ? "unexpected text record" : "expected handshake");
|
|
3832
|
+
},
|
|
3833
|
+
closed() {
|
|
3834
|
+
if (closed)
|
|
3835
|
+
return;
|
|
3836
|
+
closed = true;
|
|
3837
|
+
clearTimeout(timeout);
|
|
3838
|
+
pending?.closed();
|
|
3355
3839
|
}
|
|
3356
3840
|
});
|
|
3357
3841
|
}
|
|
3358
|
-
async function startRemote(options, admission,
|
|
3842
|
+
async function startRemote(options, admission, handlers) {
|
|
3359
3843
|
const spec = options.secure;
|
|
3360
3844
|
const remote = spec?.lan;
|
|
3361
3845
|
if (spec === undefined || remote === undefined)
|
|
@@ -3373,18 +3857,20 @@ async function startRemote(options, admission, register) {
|
|
|
3373
3857
|
http.listen(remote.port, "0.0.0.0");
|
|
3374
3858
|
});
|
|
3375
3859
|
const secure = new WebSocketServer({ server: http });
|
|
3376
|
-
secure.on("connection", (socket) => secureHandshake(socket, spec, admission,
|
|
3860
|
+
secure.on("connection", (socket) => secureHandshake(webSocketCarrier(socket), spec, admission, handlers));
|
|
3377
3861
|
const { port } = http.address();
|
|
3378
3862
|
const url = `ws://127.0.0.1:${port}`;
|
|
3379
|
-
const
|
|
3863
|
+
const currentAdvertiseUrl = () => remote.publicUrl === undefined ? `http://${pickLanAddress(networkInterfaces()) ?? "127.0.0.1"}:${port}` : normalizeAdvertiseUrl(remote.publicUrl);
|
|
3380
3864
|
return {
|
|
3381
3865
|
url,
|
|
3382
|
-
advertiseUrl
|
|
3866
|
+
get advertiseUrl() {
|
|
3867
|
+
return currentAdvertiseUrl();
|
|
3868
|
+
},
|
|
3383
3869
|
async close() {
|
|
3384
3870
|
for (const socket of secure.clients)
|
|
3385
3871
|
socket.terminate();
|
|
3386
|
-
await new Promise((
|
|
3387
|
-
await new Promise((
|
|
3872
|
+
await new Promise((resolve5) => secure.close(() => resolve5()));
|
|
3873
|
+
await new Promise((resolve5) => http.close(() => resolve5()));
|
|
3388
3874
|
}
|
|
3389
3875
|
};
|
|
3390
3876
|
}
|
|
@@ -3688,6 +4174,88 @@ function callsOf(message) {
|
|
|
3688
4174
|
return calls.size === 0 ? undefined : calls;
|
|
3689
4175
|
}
|
|
3690
4176
|
|
|
4177
|
+
class SignalClient {
|
|
4178
|
+
devicePublicKey;
|
|
4179
|
+
#connection;
|
|
4180
|
+
#hooks;
|
|
4181
|
+
#disposed = false;
|
|
4182
|
+
constructor(connection, devicePublicKey, hooks) {
|
|
4183
|
+
this.#connection = connection;
|
|
4184
|
+
this.devicePublicKey = devicePublicKey;
|
|
4185
|
+
this.#hooks = hooks;
|
|
4186
|
+
connection.handle({
|
|
4187
|
+
message: (text) => this.#receive(text),
|
|
4188
|
+
closed: () => hooks.closed(this)
|
|
4189
|
+
});
|
|
4190
|
+
const rtc = hooks.rtcHello();
|
|
4191
|
+
this.send({
|
|
4192
|
+
type: "signalHello",
|
|
4193
|
+
protocol: PROTOCOL,
|
|
4194
|
+
...rtc === undefined ? {} : { rtc }
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
4197
|
+
send(frame) {
|
|
4198
|
+
if (this.#disposed || !this.#connection.open)
|
|
4199
|
+
return Promise.resolve();
|
|
4200
|
+
return this.#connection.send(JSON.stringify(frame));
|
|
4201
|
+
}
|
|
4202
|
+
closeConnection(code, reason) {
|
|
4203
|
+
this.#connection.close(code, reason);
|
|
4204
|
+
}
|
|
4205
|
+
dispose() {
|
|
4206
|
+
this.#disposed = true;
|
|
4207
|
+
}
|
|
4208
|
+
#receive(text) {
|
|
4209
|
+
let parsed;
|
|
4210
|
+
try {
|
|
4211
|
+
parsed = parseClientFrame(JSON.parse(text));
|
|
4212
|
+
} catch {
|
|
4213
|
+
this.#connection.close(INVALID_FRAME_CLOSE_CODE, "Invalid client frame");
|
|
4214
|
+
return;
|
|
4215
|
+
}
|
|
4216
|
+
switch (parsed.kind) {
|
|
4217
|
+
case "frame":
|
|
4218
|
+
if (parsed.frame.type === "subscribe" || parsed.frame.type === "unsubscribe") {
|
|
4219
|
+
this.send({ type: "ended", stream: parsed.frame.stream, reason: "Unsupported stream" });
|
|
4220
|
+
return;
|
|
4221
|
+
}
|
|
4222
|
+
if (parsed.frame.method === "rtcOffer" || parsed.frame.method === "ping") {
|
|
4223
|
+
this.#answer(parsed.frame.id, parsed.frame.method, parsed.frame.args);
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
this.send({ type: "result", id: parsed.frame.id, ok: false, error: `Unsupported method: ${parsed.frame.method}` });
|
|
4227
|
+
return;
|
|
4228
|
+
case "unsupportedCall":
|
|
4229
|
+
this.send({ type: "result", id: parsed.id, ok: false, error: `Unsupported method: ${parsed.method}` });
|
|
4230
|
+
return;
|
|
4231
|
+
case "unsupportedStream":
|
|
4232
|
+
if (parsed.type === "subscribe") {
|
|
4233
|
+
this.send({ type: "ended", stream: parsed.stream, reason: "Unsupported stream" });
|
|
4234
|
+
}
|
|
4235
|
+
return;
|
|
4236
|
+
case "unknownType":
|
|
4237
|
+
return;
|
|
4238
|
+
case "malformed":
|
|
4239
|
+
this.#connection.close(INVALID_FRAME_CLOSE_CODE, "Invalid client frame");
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
async#answer(id, method, args) {
|
|
4244
|
+
try {
|
|
4245
|
+
let value;
|
|
4246
|
+
if (method === "ping") {
|
|
4247
|
+
value = { id: args.id };
|
|
4248
|
+
} else {
|
|
4249
|
+
const offer = args;
|
|
4250
|
+
value = await this.#hooks.rtcOffer(this, this.devicePublicKey, offer.attempt, offer.sdp);
|
|
4251
|
+
}
|
|
4252
|
+
await this.send({ type: "result", id, ok: true, value });
|
|
4253
|
+
} catch (error) {
|
|
4254
|
+
await this.send({ type: "result", id, ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
|
|
3691
4259
|
class GatewayClient {
|
|
3692
4260
|
devicePublicKey;
|
|
3693
4261
|
#connection;
|
|
@@ -3695,19 +4263,28 @@ class GatewayClient {
|
|
|
3695
4263
|
#options;
|
|
3696
4264
|
#files;
|
|
3697
4265
|
#skills;
|
|
4266
|
+
#imageVariants;
|
|
4267
|
+
#providerCalls() {
|
|
4268
|
+
const modelAuth = this.#options.modelAuth;
|
|
4269
|
+
if (modelAuth === undefined)
|
|
4270
|
+
throw new Error("Provider setup is not available on this host");
|
|
4271
|
+
return modelAuth;
|
|
4272
|
+
}
|
|
3698
4273
|
#hostValues;
|
|
3699
4274
|
#subscriptions = new Map;
|
|
3700
4275
|
#disposed = false;
|
|
3701
|
-
constructor(connection, options, conversations, files, skills, hooks) {
|
|
4276
|
+
constructor(connection, options, conversations, files, skills, imageVariants, hooks) {
|
|
3702
4277
|
this.#connection = connection;
|
|
3703
4278
|
this.#options = options;
|
|
3704
4279
|
this.#files = files;
|
|
3705
4280
|
this.#skills = skills;
|
|
4281
|
+
this.#imageVariants = imageVariants;
|
|
3706
4282
|
this.#hostValues = {
|
|
3707
4283
|
conversations,
|
|
3708
4284
|
mcp: options.mcp ?? constantValue(null),
|
|
3709
4285
|
models: options.modelAuth?.models ?? constantValue(options.modelSummaries()),
|
|
3710
4286
|
auth: options.modelAuth?.auth ?? constantValue({ logins: [] }),
|
|
4287
|
+
providers: options.modelAuth?.providers ?? constantValue({ providers: [], logins: [] }),
|
|
3711
4288
|
settings: options.settings,
|
|
3712
4289
|
agents: options.agents ?? constantValue({ roles: [], problems: [] })
|
|
3713
4290
|
};
|
|
@@ -3743,6 +4320,7 @@ class GatewayClient {
|
|
|
3743
4320
|
},
|
|
3744
4321
|
closed: () => hooks.closed(this)
|
|
3745
4322
|
});
|
|
4323
|
+
const rtc = this.#hooks.rtcHello();
|
|
3746
4324
|
this.send({
|
|
3747
4325
|
type: "hello",
|
|
3748
4326
|
protocol: PROTOCOL,
|
|
@@ -3754,7 +4332,9 @@ class GatewayClient {
|
|
|
3754
4332
|
kind: doc.token.definition.kind,
|
|
3755
4333
|
...doc.presentation === undefined ? {} : { presentation: doc.presentation }
|
|
3756
4334
|
})),
|
|
3757
|
-
toolPresentations: options.toolPresentations ?? {}
|
|
4335
|
+
toolPresentations: options.toolPresentations ?? {},
|
|
4336
|
+
endpointSet: this.#hooks.endpointSet(),
|
|
4337
|
+
...rtc === undefined ? {} : { rtc }
|
|
3758
4338
|
});
|
|
3759
4339
|
}
|
|
3760
4340
|
send(frame) {
|
|
@@ -3806,16 +4386,16 @@ class GatewayClient {
|
|
|
3806
4386
|
return { stop: source.subscribe((value) => void send({ type: "snapshot", stream, value })) };
|
|
3807
4387
|
}
|
|
3808
4388
|
if (stream === "index") {
|
|
3809
|
-
const
|
|
3810
|
-
if (
|
|
4389
|
+
const watch = await this.#options.harness.watchDoc(IndexDoc, context3);
|
|
4390
|
+
if (watch === undefined)
|
|
3811
4391
|
throw new Error("Index document is missing");
|
|
3812
|
-
return this.#forward(stream,
|
|
4392
|
+
return this.#forward(stream, watch, send);
|
|
3813
4393
|
}
|
|
3814
4394
|
if (stream === "devices") {
|
|
3815
|
-
const
|
|
3816
|
-
if (
|
|
4395
|
+
const watch = await this.#options.harness.watchDoc(DevicesDoc, context3);
|
|
4396
|
+
if (watch === undefined)
|
|
3817
4397
|
throw new Error("Devices document is missing");
|
|
3818
|
-
return this.#forward(stream,
|
|
4398
|
+
return this.#forward(stream, watch, send);
|
|
3819
4399
|
}
|
|
3820
4400
|
if (stream === "tasks")
|
|
3821
4401
|
return this.#forward(stream, await this.#options.harness.watchTaskGraph(context3), send);
|
|
@@ -3837,8 +4417,8 @@ class GatewayClient {
|
|
|
3837
4417
|
await this.#conversation(id);
|
|
3838
4418
|
const { harness } = this.#options;
|
|
3839
4419
|
let signalCreation;
|
|
3840
|
-
const created = new Promise((
|
|
3841
|
-
signalCreation =
|
|
4420
|
+
const created = new Promise((resolve5) => {
|
|
4421
|
+
signalCreation = resolve5;
|
|
3842
4422
|
});
|
|
3843
4423
|
let inner;
|
|
3844
4424
|
let stopped = false;
|
|
@@ -3863,13 +4443,13 @@ class GatewayClient {
|
|
|
3863
4443
|
created.then(async () => {
|
|
3864
4444
|
if (stopped)
|
|
3865
4445
|
return;
|
|
3866
|
-
const
|
|
3867
|
-
if (
|
|
4446
|
+
const watch = await harness.watchDoc(token, id, context3);
|
|
4447
|
+
if (watch === undefined)
|
|
3868
4448
|
return;
|
|
3869
4449
|
if (stopped)
|
|
3870
|
-
await
|
|
4450
|
+
await watch.stop();
|
|
3871
4451
|
else {
|
|
3872
|
-
inner = await this.#forward(stream,
|
|
4452
|
+
inner = await this.#forward(stream, watch, send);
|
|
3873
4453
|
if (stopped)
|
|
3874
4454
|
await inner.stop();
|
|
3875
4455
|
}
|
|
@@ -3886,23 +4466,23 @@ class GatewayClient {
|
|
|
3886
4466
|
}
|
|
3887
4467
|
};
|
|
3888
4468
|
}
|
|
3889
|
-
async#forward(stream,
|
|
4469
|
+
async#forward(stream, watch, send, state) {
|
|
3890
4470
|
if (state?.images === undefined) {
|
|
3891
|
-
await send({ type: "snapshot", stream, value:
|
|
3892
|
-
|
|
3893
|
-
return
|
|
4471
|
+
await send({ type: "snapshot", stream, value: watch.value });
|
|
4472
|
+
watch.start((_value, ops) => send({ type: "ops", stream, ops }));
|
|
4473
|
+
return watch;
|
|
3894
4474
|
}
|
|
3895
4475
|
const images = state.images;
|
|
3896
|
-
let before =
|
|
4476
|
+
let before = watch.value;
|
|
3897
4477
|
state.view = before;
|
|
3898
4478
|
await send({ type: "snapshot", stream, value: await images.value(before) });
|
|
3899
|
-
|
|
4479
|
+
watch.start(async (value, ops) => {
|
|
3900
4480
|
const sent = await images.ops(ops, before);
|
|
3901
4481
|
before = value;
|
|
3902
4482
|
state.view = before;
|
|
3903
4483
|
return send({ type: "ops", stream, ops: sent });
|
|
3904
4484
|
});
|
|
3905
|
-
return
|
|
4485
|
+
return watch;
|
|
3906
4486
|
}
|
|
3907
4487
|
#shownImage(ref) {
|
|
3908
4488
|
for (const state of this.#subscriptions.values()) {
|
|
@@ -3926,19 +4506,22 @@ class GatewayClient {
|
|
|
3926
4506
|
const files = await this.#attachmentStore().use(ids);
|
|
3927
4507
|
return files === undefined ? undefined : attachmentBlock(files);
|
|
3928
4508
|
}
|
|
3929
|
-
#
|
|
3930
|
-
const
|
|
3931
|
-
|
|
4509
|
+
#captureImages(images) {
|
|
4510
|
+
const captured = images?.map((image) => ("data" in image) ? image : this.#shownImage(image.ref));
|
|
4511
|
+
return () => {
|
|
4512
|
+
const resolved = captured?.map((image, index) => {
|
|
4513
|
+
if (image === undefined)
|
|
4514
|
+
throw new Error(`Image ${index + 1} is no longer available`);
|
|
3932
4515
|
return image;
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
return
|
|
4516
|
+
});
|
|
4517
|
+
const problem = imageProblem(resolved ?? []);
|
|
4518
|
+
if (problem !== undefined)
|
|
4519
|
+
throw new Error(problem);
|
|
4520
|
+
return resolved;
|
|
4521
|
+
};
|
|
4522
|
+
}
|
|
4523
|
+
#attached(images) {
|
|
4524
|
+
return this.#captureImages(images)();
|
|
3942
4525
|
}
|
|
3943
4526
|
async#unsubscribe(stream) {
|
|
3944
4527
|
const subscription = this.#subscriptions.get(stream);
|
|
@@ -3951,6 +4534,9 @@ class GatewayClient {
|
|
|
3951
4534
|
throw new Error(`Conversation ${id} does not exist`);
|
|
3952
4535
|
return conversation;
|
|
3953
4536
|
}
|
|
4537
|
+
#submissionByRequest(conversationId, requestId) {
|
|
4538
|
+
return this.#options.harness.commit((tx) => tx.submissionByRequest(conversationId, requestId), context3);
|
|
4539
|
+
}
|
|
3954
4540
|
async#compact(conversation, instructions) {
|
|
3955
4541
|
const taskId = await conversation.compact(instructions, context3);
|
|
3956
4542
|
this.#options.harness.waitForTask(taskId, context3).then((receipt) => void this.send({ type: "notice", level: "info", message: `Compaction ${receipt.state.outcome.status}.` }), () => {});
|
|
@@ -3973,23 +4559,39 @@ class GatewayClient {
|
|
|
3973
4559
|
return null;
|
|
3974
4560
|
case "submit": {
|
|
3975
4561
|
const input = args;
|
|
3976
|
-
const images = this.#
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
4562
|
+
const images = this.#captureImages(input.images);
|
|
4563
|
+
return await serializedSubmission(this.#options.harness, input.conversationId, input.requestId, async () => {
|
|
4564
|
+
const existing = await this.#submissionByRequest(input.conversationId, input.requestId);
|
|
4565
|
+
if (existing !== undefined)
|
|
4566
|
+
return submitResult(existing);
|
|
4567
|
+
const resolved = images();
|
|
4568
|
+
const conversation2 = await this.#conversation(input.conversationId);
|
|
4569
|
+
const attachments = await this.#attachmentText(input.attachments);
|
|
4570
|
+
const text = await this.#expandSkills(input.text, { kind: "conversation", conversationId: conversation2.id }, input.skills);
|
|
4571
|
+
const dismissed = await this.#dismissPendingQuestions(conversation2.id, context3);
|
|
4572
|
+
const submission = await conversation2.submit({
|
|
4573
|
+
type: "input",
|
|
4574
|
+
content: messageContent(text, resolved, attachments),
|
|
4575
|
+
whenBusy: dismissed ? "steer" : input.whenBusy,
|
|
4576
|
+
requestId: input.requestId
|
|
4577
|
+
}, context3);
|
|
4578
|
+
let entry;
|
|
4579
|
+
try {
|
|
4580
|
+
const admitted = await this.#options.harness.submission(submission.id, context3);
|
|
4581
|
+
entry = (await admitted?.status(context3))?.entry;
|
|
4582
|
+
} catch {}
|
|
4583
|
+
submission.wait(context3).then((settled) => {
|
|
4584
|
+
if (settled.status === "unanswered" && settled.reason !== "aborted") {
|
|
4585
|
+
this.send({ type: "notice", level: "error", message: `No answer: ${settled.reason}` });
|
|
4586
|
+
}
|
|
4587
|
+
}, () => {});
|
|
4588
|
+
return { submissionId: submission.id, ...entry === undefined ? {} : { entry } };
|
|
4589
|
+
});
|
|
4590
|
+
}
|
|
4591
|
+
case "submission": {
|
|
4592
|
+
const input = args;
|
|
4593
|
+
const record = await this.#submissionByRequest(input.conversationId, input.requestId);
|
|
4594
|
+
return record === undefined ? null : submissionResult(record);
|
|
3993
4595
|
}
|
|
3994
4596
|
case "createConversation": {
|
|
3995
4597
|
const create = args;
|
|
@@ -4032,6 +4634,12 @@ class GatewayClient {
|
|
|
4032
4634
|
this.#hooks.closeDevice(publicKey);
|
|
4033
4635
|
return null;
|
|
4034
4636
|
}
|
|
4637
|
+
case "rtcOffer": {
|
|
4638
|
+
const { attempt, sdp } = args;
|
|
4639
|
+
return this.#hooks.rtcOffer(this, this.devicePublicKey, attempt, sdp);
|
|
4640
|
+
}
|
|
4641
|
+
case "ping":
|
|
4642
|
+
return { id: args.id };
|
|
4035
4643
|
case "setPushToken": {
|
|
4036
4644
|
const device = this.devicePublicKey;
|
|
4037
4645
|
if (device === undefined)
|
|
@@ -4049,6 +4657,31 @@ class GatewayClient {
|
|
|
4049
4657
|
await modelAuth.logout(args.providerId);
|
|
4050
4658
|
return null;
|
|
4051
4659
|
}
|
|
4660
|
+
case "beginLogin": {
|
|
4661
|
+
const { providerId, method: method2 } = args;
|
|
4662
|
+
return { loginId: this.#providerCalls().beginLogin(providerId, method2) };
|
|
4663
|
+
}
|
|
4664
|
+
case "answerLogin": {
|
|
4665
|
+
const { loginId, promptId, value } = args;
|
|
4666
|
+
this.#providerCalls().answerLogin(loginId, promptId, value);
|
|
4667
|
+
return null;
|
|
4668
|
+
}
|
|
4669
|
+
case "cancelLogin": {
|
|
4670
|
+
this.#providerCalls().cancelLogin(args.loginId);
|
|
4671
|
+
return null;
|
|
4672
|
+
}
|
|
4673
|
+
case "saveCustomProvider": {
|
|
4674
|
+
const { provider, apiKey } = args;
|
|
4675
|
+
await this.#providerCalls().saveCustomProvider(provider, apiKey);
|
|
4676
|
+
return null;
|
|
4677
|
+
}
|
|
4678
|
+
case "removeCustomProvider": {
|
|
4679
|
+
await this.#providerCalls().removeCustomProvider(args.providerId);
|
|
4680
|
+
return null;
|
|
4681
|
+
}
|
|
4682
|
+
case "listEndpointModels": {
|
|
4683
|
+
return { models: await this.#providerCalls().listEndpointModels(args) };
|
|
4684
|
+
}
|
|
4052
4685
|
case "updateSettings": {
|
|
4053
4686
|
await this.#options.settings.update(args.patch);
|
|
4054
4687
|
return null;
|
|
@@ -4075,7 +4708,7 @@ class GatewayClient {
|
|
|
4075
4708
|
return null;
|
|
4076
4709
|
}
|
|
4077
4710
|
case "listFolders":
|
|
4078
|
-
return listFolders(args.path,
|
|
4711
|
+
return listFolders(args.path, homedir());
|
|
4079
4712
|
case "listFiles": {
|
|
4080
4713
|
const input = args;
|
|
4081
4714
|
return listFiles(this.#options.harness, this.#options.dataDir, this.#files, input.scope, input.query, context3);
|
|
@@ -4085,10 +4718,16 @@ class GatewayClient {
|
|
|
4085
4718
|
return listSkills(this.#options.harness, this.#skills, input.scope, context3);
|
|
4086
4719
|
}
|
|
4087
4720
|
case "image": {
|
|
4088
|
-
const
|
|
4721
|
+
const input = args;
|
|
4722
|
+
const image = this.#shownImage(input.ref);
|
|
4089
4723
|
if (image === undefined)
|
|
4090
4724
|
throw new Error("Unknown image");
|
|
4091
|
-
|
|
4725
|
+
if (input.variant === undefined)
|
|
4726
|
+
return image;
|
|
4727
|
+
const derived = await this.#imageVariants.get(input.ref, input.variant, image);
|
|
4728
|
+
if (this.#shownImage(input.ref) === undefined)
|
|
4729
|
+
throw new Error("Unknown image");
|
|
4730
|
+
return derived;
|
|
4092
4731
|
}
|
|
4093
4732
|
case "beginAttachment": {
|
|
4094
4733
|
const input = args;
|
|
@@ -4282,73 +4921,577 @@ class GatewayClient {
|
|
|
4282
4921
|
return text;
|
|
4283
4922
|
let skills;
|
|
4284
4923
|
try {
|
|
4285
|
-
skills = await scopeSkills(this.#options.harness, this.#skills, scope, context3);
|
|
4924
|
+
skills = await scopeSkills(this.#options.harness, this.#skills, scope, context3);
|
|
4925
|
+
} catch (error) {
|
|
4926
|
+
this.send({
|
|
4927
|
+
type: "notice",
|
|
4928
|
+
level: "warning",
|
|
4929
|
+
message: `Skills were not expanded: ${error instanceof Error ? error.message : String(error)}`
|
|
4930
|
+
});
|
|
4931
|
+
return text;
|
|
4932
|
+
}
|
|
4933
|
+
const names = bound ?? mentionedSkillNames(text, skills.map((skill) => skill.name));
|
|
4934
|
+
const expanded = await expandSkills(text, names, skills);
|
|
4935
|
+
for (const name of expanded.missing) {
|
|
4936
|
+
this.send({ type: "notice", level: "warning", message: `Skill "${name}" is not available here; sent without it` });
|
|
4937
|
+
}
|
|
4938
|
+
for (const failure of expanded.failures) {
|
|
4939
|
+
this.send({
|
|
4940
|
+
type: "notice",
|
|
4941
|
+
level: "error",
|
|
4942
|
+
message: `Could not read skill "${failure.name}" (${failure.filePath}): ${failure.message}`
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4945
|
+
return expanded.text;
|
|
4946
|
+
}
|
|
4947
|
+
async#dismissPendingQuestions(conversationId, context4) {
|
|
4948
|
+
const questionDocs = this.#questionDocs();
|
|
4949
|
+
const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId, context4)));
|
|
4950
|
+
const pending = snapshots.some((value) => Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
|
|
4951
|
+
if (!pending)
|
|
4952
|
+
return false;
|
|
4953
|
+
return await this.#options.harness.commit((tx) => dismissPendingQuestions(tx, questionDocs, conversationId), context4);
|
|
4954
|
+
}
|
|
4955
|
+
#questionDocs() {
|
|
4956
|
+
return (this.#options.docs ?? []).filter((doc) => doc.presentation === "pinomad.question");
|
|
4957
|
+
}
|
|
4958
|
+
get #chatsPrefix() {
|
|
4959
|
+
return join7(this.#options.dataDir, "chats") + sep5;
|
|
4960
|
+
}
|
|
4961
|
+
async#changes(conversation, context4) {
|
|
4962
|
+
const cwd = (await conversation.agent(context4)).cwd;
|
|
4963
|
+
if (cwd === undefined)
|
|
4964
|
+
return { available: false, reason: "No working directory" };
|
|
4965
|
+
if (cwd.startsWith(this.#chatsPrefix))
|
|
4966
|
+
return { available: false, reason: "Chat conversations have no repository" };
|
|
4967
|
+
const index = await this.#options.harness.snapshot(IndexDoc, context4);
|
|
4968
|
+
const record = checkoutAt(index?.checkouts, cwd);
|
|
4969
|
+
if (record !== undefined) {
|
|
4970
|
+
if (!await worktreeExists(record.path))
|
|
4971
|
+
return { available: false, reason: "No changes yet" };
|
|
4972
|
+
const [changes, branch2] = await Promise.all([changesOf(record.path, record.base), branchOf(record.path)]);
|
|
4973
|
+
return { available: true, ...changes, branch: branch2 };
|
|
4974
|
+
}
|
|
4975
|
+
const [base, branch] = await Promise.all([gitBase(cwd), branchOf(cwd)]);
|
|
4976
|
+
if (base === undefined)
|
|
4977
|
+
return { available: false, reason: "Not in a git repository", branch };
|
|
4978
|
+
return { available: true, ...await changesOf(base.repo, base.base), branch };
|
|
4979
|
+
}
|
|
4980
|
+
async dispose() {
|
|
4981
|
+
if (this.#disposed)
|
|
4982
|
+
return;
|
|
4983
|
+
this.#disposed = true;
|
|
4984
|
+
const subscriptions = [...this.#subscriptions.values()];
|
|
4985
|
+
this.#subscriptions.clear();
|
|
4986
|
+
for (const state of subscriptions)
|
|
4987
|
+
await state.subscription?.stop();
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
|
|
4991
|
+
// apps/host/src/host.ts
|
|
4992
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
4993
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
4994
|
+
import { basename as basename3, join as join10, sep as sep6 } from "node:path";
|
|
4995
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
|
|
4996
|
+
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
4997
|
+
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
4998
|
+
|
|
4999
|
+
// apps/host/src/mcp.ts
|
|
5000
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
5001
|
+
import { homedir as homedir2 } from "node:os";
|
|
5002
|
+
import { isAbsolute as isAbsolute2, resolve as resolve5 } from "node:path";
|
|
5003
|
+
import {
|
|
5004
|
+
McpAuthRequiredError,
|
|
5005
|
+
McpClient,
|
|
5006
|
+
McpHttpError,
|
|
5007
|
+
StdioTransport,
|
|
5008
|
+
StreamableHttpTransport,
|
|
5009
|
+
toLlmContent
|
|
5010
|
+
} from "@earendil-works/pi-mcp";
|
|
5011
|
+
import { defineExtension as defineExtension5, defineTool as defineTool4 } from "@earendil-works/pi-durable";
|
|
5012
|
+
import { Type as Type5 } from "typebox";
|
|
5013
|
+
var ENV_VAR = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
5014
|
+
var isObject2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5015
|
+
function expand(value) {
|
|
5016
|
+
let missing;
|
|
5017
|
+
const expanded = value.replace(ENV_VAR, (_all, name) => {
|
|
5018
|
+
const found = process.env[name];
|
|
5019
|
+
if (found === undefined)
|
|
5020
|
+
missing = name;
|
|
5021
|
+
return found ?? "";
|
|
5022
|
+
});
|
|
5023
|
+
return missing === undefined ? expanded : { missing };
|
|
5024
|
+
}
|
|
5025
|
+
function expandAll(values, field, error) {
|
|
5026
|
+
if (values === undefined)
|
|
5027
|
+
return;
|
|
5028
|
+
if (!isObject2(values)) {
|
|
5029
|
+
error(`"${field}" must be an object of strings`);
|
|
5030
|
+
return;
|
|
5031
|
+
}
|
|
5032
|
+
const expanded = {};
|
|
5033
|
+
for (const [key, value] of Object.entries(values)) {
|
|
5034
|
+
if (typeof value !== "string") {
|
|
5035
|
+
error(`"${field}.${key}" must be a string`);
|
|
5036
|
+
return;
|
|
5037
|
+
}
|
|
5038
|
+
const result2 = expand(value);
|
|
5039
|
+
if (typeof result2 !== "string") {
|
|
5040
|
+
error(`"${field}.${key}" references unset environment variable ${result2.missing}`);
|
|
5041
|
+
return;
|
|
5042
|
+
}
|
|
5043
|
+
expanded[key] = result2;
|
|
5044
|
+
}
|
|
5045
|
+
return expanded;
|
|
5046
|
+
}
|
|
5047
|
+
function parseServer(name, raw, error) {
|
|
5048
|
+
const fail = (message) => {
|
|
5049
|
+
error(`${name}: ${message}`);
|
|
5050
|
+
return;
|
|
5051
|
+
};
|
|
5052
|
+
if (!isObject2(raw))
|
|
5053
|
+
return fail("server entry must be an object");
|
|
5054
|
+
if (raw.enabled === false)
|
|
5055
|
+
return { name, disabled: true };
|
|
5056
|
+
const timeout = raw.timeout === undefined ? 60000 : typeof raw.timeout === "number" && raw.timeout > 0 ? raw.timeout * 1000 : undefined;
|
|
5057
|
+
if (timeout === undefined)
|
|
5058
|
+
return fail(`"timeout" must be a positive number of seconds`);
|
|
5059
|
+
if (raw.exposure !== undefined && raw.exposure !== "codemode" && raw.exposure !== "direct") {
|
|
5060
|
+
return fail(`"exposure" must be "codemode" or "direct"`);
|
|
5061
|
+
}
|
|
5062
|
+
if (raw.description !== undefined && typeof raw.description !== "string")
|
|
5063
|
+
return fail(`"description" must be a string`);
|
|
5064
|
+
const exposure = raw.exposure ?? "codemode";
|
|
5065
|
+
const description = raw.description;
|
|
5066
|
+
if (raw.command !== undefined || raw.type === "stdio") {
|
|
5067
|
+
if (typeof raw.command !== "string" || raw.command === "")
|
|
5068
|
+
return fail(`stdio server needs a "command"`);
|
|
5069
|
+
if (raw.args !== undefined && (!Array.isArray(raw.args) || !raw.args.every((arg) => typeof arg === "string"))) {
|
|
5070
|
+
return fail(`"args" must be an array of strings`);
|
|
5071
|
+
}
|
|
5072
|
+
if (raw.cwd !== undefined && typeof raw.cwd !== "string")
|
|
5073
|
+
return fail(`"cwd" must be a string`);
|
|
5074
|
+
const env = expandAll(raw.env, "env", (message) => error(`${name}: ${message}`));
|
|
5075
|
+
if (raw.env !== undefined && env === undefined)
|
|
5076
|
+
return;
|
|
5077
|
+
const cwd = raw.cwd === undefined ? homedir2() : isAbsolute2(raw.cwd) ? raw.cwd : resolve5(homedir2(), raw.cwd);
|
|
5078
|
+
return {
|
|
5079
|
+
name,
|
|
5080
|
+
spec: { kind: "stdio", command: raw.command, args: raw.args, env, cwd },
|
|
5081
|
+
timeoutMs: timeout,
|
|
5082
|
+
exposure,
|
|
5083
|
+
...description === undefined ? {} : { description }
|
|
5084
|
+
};
|
|
5085
|
+
}
|
|
5086
|
+
if (raw.url !== undefined || raw.type === "http" || raw.type === "sse") {
|
|
5087
|
+
if (typeof raw.url !== "string" || raw.url === "")
|
|
5088
|
+
return fail(`HTTP server needs a "url"`);
|
|
5089
|
+
const headers = expandAll(raw.headers, "headers", (message) => error(`${name}: ${message}`));
|
|
5090
|
+
if (raw.headers !== undefined && headers === undefined)
|
|
5091
|
+
return;
|
|
5092
|
+
return {
|
|
5093
|
+
name,
|
|
5094
|
+
spec: { kind: "http", url: raw.url, headers },
|
|
5095
|
+
timeoutMs: timeout,
|
|
5096
|
+
exposure,
|
|
5097
|
+
...description === undefined ? {} : { description }
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
5100
|
+
return fail(`needs "command" (stdio) or "url" (HTTP)`);
|
|
5101
|
+
}
|
|
5102
|
+
async function loadConfig(configPath) {
|
|
5103
|
+
let parsed;
|
|
5104
|
+
try {
|
|
5105
|
+
parsed = JSON.parse(await readFile2(configPath, "utf8"));
|
|
5106
|
+
} catch (error) {
|
|
5107
|
+
if (error.code === "ENOENT")
|
|
5108
|
+
return { errors: [], entries: [] };
|
|
5109
|
+
return { errors: [`${configPath}: ${error instanceof Error ? error.message : String(error)}`], entries: [] };
|
|
5110
|
+
}
|
|
5111
|
+
if (!isObject2(parsed) || parsed.mcpServers !== undefined && !isObject2(parsed.mcpServers)) {
|
|
5112
|
+
return { errors: [`${configPath}: "mcpServers" must be an object`], entries: [] };
|
|
5113
|
+
}
|
|
5114
|
+
const errors = [];
|
|
5115
|
+
const entries = [];
|
|
5116
|
+
for (const [name, raw] of Object.entries(parsed.mcpServers ?? {})) {
|
|
5117
|
+
const entry = parseServer(name, raw, (message) => errors.push(message));
|
|
5118
|
+
if (entry !== undefined)
|
|
5119
|
+
entries.push(entry);
|
|
5120
|
+
}
|
|
5121
|
+
return { errors, entries };
|
|
5122
|
+
}
|
|
5123
|
+
var callName = (server, tool) => `mcp__${server}__${tool}`.replace(/[^A-Za-z0-9_]/g, "_").slice(0, 64);
|
|
5124
|
+
var callToolResultSchema = (tool) => ({
|
|
5125
|
+
type: "object",
|
|
5126
|
+
properties: {
|
|
5127
|
+
content: { type: "array", items: { type: "object" } },
|
|
5128
|
+
isError: { type: "boolean" },
|
|
5129
|
+
_meta: { type: "object" },
|
|
5130
|
+
structuredContent: tool.outputSchema ?? { type: "object" }
|
|
5131
|
+
},
|
|
5132
|
+
required: ["content"]
|
|
5133
|
+
});
|
|
5134
|
+
function mcpTool(client, timeoutMs, exposure, name, tool) {
|
|
5135
|
+
return defineTool4({
|
|
5136
|
+
name,
|
|
5137
|
+
description: tool.description ?? tool.title ?? tool.name,
|
|
5138
|
+
parameters: Type5.Unsafe({
|
|
5139
|
+
...tool.inputSchema,
|
|
5140
|
+
type: "object",
|
|
5141
|
+
properties: tool.inputSchema.properties ?? {}
|
|
5142
|
+
}),
|
|
5143
|
+
...exposure === "direct" ? {} : { callers: ["tools"] },
|
|
5144
|
+
structuredOutputSchema: Type5.Unsafe(callToolResultSchema(tool)),
|
|
5145
|
+
execute: async (args, _api, context4) => {
|
|
5146
|
+
const result2 = await client.callTool(tool.name, args, {
|
|
5147
|
+
signal: context4.abortSignal ?? NEVER_ABORT,
|
|
5148
|
+
timeoutMs
|
|
5149
|
+
});
|
|
5150
|
+
return { output: toLlmContent(result2), structuredOutput: result2, isError: result2.isError === true };
|
|
5151
|
+
}
|
|
5152
|
+
});
|
|
5153
|
+
}
|
|
5154
|
+
var NEVER_ABORT = new AbortController().signal;
|
|
5155
|
+
function startMcp(options) {
|
|
5156
|
+
const listeners = new Set;
|
|
5157
|
+
const servers = new Map;
|
|
5158
|
+
const toolsets = new Map;
|
|
5159
|
+
const namespaces = new Map;
|
|
5160
|
+
const exposures = new Map;
|
|
5161
|
+
const live = new Set;
|
|
5162
|
+
let errors = [];
|
|
5163
|
+
let closed = false;
|
|
5164
|
+
const publish = () => {
|
|
5165
|
+
const value = { configPath: options.configPath, errors, servers: [...servers.values()] };
|
|
5166
|
+
for (const listener of listeners)
|
|
5167
|
+
listener(value);
|
|
5168
|
+
};
|
|
5169
|
+
const install = () => {
|
|
5170
|
+
options.registry.install(defineExtension5({ name: "mcp", tools: [...toolsets.values()].flat() }));
|
|
5171
|
+
options.onNamespaces?.([...namespaces.values()]);
|
|
5172
|
+
};
|
|
5173
|
+
const set = (name, patch) => {
|
|
5174
|
+
servers.set(name, { name, tools: 0, ...servers.get(name), ...patch });
|
|
5175
|
+
publish();
|
|
5176
|
+
};
|
|
5177
|
+
const relist = async (name, client, timeoutMs, description) => {
|
|
5178
|
+
const taken = new Set([...toolsets.entries()].flatMap(([other, tools2]) => other === name ? [] : tools2.map((tool) => tool.name)));
|
|
5179
|
+
const tools = [];
|
|
5180
|
+
const collisions = [];
|
|
5181
|
+
for (const tool of await client.listTools()) {
|
|
5182
|
+
const wire = callName(name, tool.name);
|
|
5183
|
+
if (taken.has(wire)) {
|
|
5184
|
+
collisions.push(`tool "${tool.name}" collides as ${wire}; skipped`);
|
|
5185
|
+
continue;
|
|
5186
|
+
}
|
|
5187
|
+
taken.add(wire);
|
|
5188
|
+
tools.push(mcpTool(client, timeoutMs, exposures.get(name) ?? "codemode", wire, tool));
|
|
5189
|
+
}
|
|
5190
|
+
if (closed)
|
|
5191
|
+
return;
|
|
5192
|
+
toolsets.set(name, tools);
|
|
5193
|
+
namespaces.set(name, {
|
|
5194
|
+
name,
|
|
5195
|
+
...description === undefined ? {} : { description },
|
|
5196
|
+
...client.instructions === undefined ? {} : { instructions: client.instructions },
|
|
5197
|
+
prefix: callName(name, "")
|
|
5198
|
+
});
|
|
5199
|
+
set(name, { state: "connected", tools: tools.length, error: collisions.length === 0 ? undefined : collisions.join("; ") });
|
|
5200
|
+
install();
|
|
5201
|
+
};
|
|
5202
|
+
const fail = (name, error) => {
|
|
5203
|
+
toolsets.delete(name);
|
|
5204
|
+
namespaces.delete(name);
|
|
5205
|
+
set(name, { state: "failed", error, tools: 0 });
|
|
5206
|
+
install();
|
|
5207
|
+
};
|
|
5208
|
+
const describeConnectError = (error, transport) => {
|
|
5209
|
+
if (error instanceof McpAuthRequiredError || error instanceof McpHttpError && error.status === 401) {
|
|
5210
|
+
return "requires sign-in (OAuth), not supported yet";
|
|
5211
|
+
}
|
|
5212
|
+
const base = error instanceof Error ? error.message : String(error);
|
|
5213
|
+
const stderr = transport?.stderr.trim().split(`
|
|
5214
|
+
`).slice(-5).join(`
|
|
5215
|
+
`);
|
|
5216
|
+
return stderr === undefined || stderr === "" ? base : `${base}
|
|
5217
|
+
${stderr}`;
|
|
5218
|
+
};
|
|
5219
|
+
const connect = async (name, spec, timeoutMs, description) => {
|
|
5220
|
+
const client = new McpClient({ name: "pinomad", version: "0.0.0" });
|
|
5221
|
+
live.add(client);
|
|
5222
|
+
let stdio;
|
|
5223
|
+
try {
|
|
5224
|
+
const transport = spec.kind === "stdio" ? stdio = new StdioTransport({ command: spec.command, args: spec.args, cwd: spec.cwd, env: spec.env, stderr: "pipe" }) : new StreamableHttpTransport({ url: spec.url, headers: spec.headers });
|
|
5225
|
+
await client.connect(transport);
|
|
5226
|
+
if (closed)
|
|
5227
|
+
return;
|
|
5228
|
+
let lastError;
|
|
5229
|
+
client.onError((error) => {
|
|
5230
|
+
lastError = error.message;
|
|
5231
|
+
});
|
|
5232
|
+
client.onClose(() => {
|
|
5233
|
+
if (!closed)
|
|
5234
|
+
fail(name, `disconnected${lastError === undefined ? "" : `: ${lastError}`}`);
|
|
5235
|
+
});
|
|
5236
|
+
client.onNotification("notifications/tools/list_changed", () => {
|
|
5237
|
+
relist(name, client, timeoutMs, description).catch((error) => {
|
|
5238
|
+
fail(name, error instanceof Error ? error.message : String(error));
|
|
5239
|
+
});
|
|
5240
|
+
});
|
|
5241
|
+
await relist(name, client, timeoutMs, description);
|
|
5242
|
+
} catch (error) {
|
|
5243
|
+
live.delete(client);
|
|
5244
|
+
if (!closed)
|
|
5245
|
+
fail(name, describeConnectError(error, stdio));
|
|
5246
|
+
await client.close().catch(() => {});
|
|
5247
|
+
}
|
|
5248
|
+
};
|
|
5249
|
+
const status = {
|
|
5250
|
+
get value() {
|
|
5251
|
+
return { configPath: options.configPath, errors, servers: [...servers.values()] };
|
|
5252
|
+
},
|
|
5253
|
+
subscribe(listener) {
|
|
5254
|
+
listeners.add(listener);
|
|
5255
|
+
return () => listeners.delete(listener);
|
|
5256
|
+
}
|
|
5257
|
+
};
|
|
5258
|
+
loadConfig(options.configPath).then((loaded) => {
|
|
5259
|
+
if (closed)
|
|
5260
|
+
return;
|
|
5261
|
+
errors = loaded.errors;
|
|
5262
|
+
for (const entry of loaded.entries) {
|
|
5263
|
+
if (entry.disabled === true)
|
|
5264
|
+
servers.set(entry.name, { name: entry.name, state: "disabled", tools: 0 });
|
|
5265
|
+
else
|
|
5266
|
+
servers.set(entry.name, { name: entry.name, state: "connecting", tools: 0 });
|
|
5267
|
+
}
|
|
5268
|
+
publish();
|
|
5269
|
+
install();
|
|
5270
|
+
for (const entry of loaded.entries) {
|
|
5271
|
+
if (entry.disabled === true)
|
|
5272
|
+
continue;
|
|
5273
|
+
exposures.set(entry.name, entry.exposure);
|
|
5274
|
+
connect(entry.name, entry.spec, entry.timeoutMs, entry.description);
|
|
5275
|
+
}
|
|
5276
|
+
});
|
|
5277
|
+
return {
|
|
5278
|
+
status,
|
|
5279
|
+
async close() {
|
|
5280
|
+
closed = true;
|
|
5281
|
+
await Promise.all([...live].map((client) => client.close().catch(() => {})));
|
|
5282
|
+
}
|
|
5283
|
+
};
|
|
5284
|
+
}
|
|
5285
|
+
|
|
5286
|
+
// apps/host/src/agent-roles.ts
|
|
5287
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
5288
|
+
import { existsSync as existsSync2, watch } from "node:fs";
|
|
5289
|
+
import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rename, rm as rm2, writeFile } from "node:fs/promises";
|
|
5290
|
+
import { join as join8 } from "node:path";
|
|
5291
|
+
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
5292
|
+
|
|
5293
|
+
// packages/protocol/src/agents.ts
|
|
5294
|
+
var MAX_AGENT_NAME = 32;
|
|
5295
|
+
var MAX_AGENT_DESCRIPTION = 1024;
|
|
5296
|
+
var NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
5297
|
+
function agentNameProblem(name) {
|
|
5298
|
+
return name.length > MAX_AGENT_NAME || !NAME_PATTERN.test(name) ? `Name must be lowercase letters, digits and hyphens (max ${MAX_AGENT_NAME})` : undefined;
|
|
5299
|
+
}
|
|
5300
|
+
function agentRoleProblem(role) {
|
|
5301
|
+
const name = agentNameProblem(role.name);
|
|
5302
|
+
if (name !== undefined)
|
|
5303
|
+
return name;
|
|
5304
|
+
const description = role.description.trim();
|
|
5305
|
+
if (description === "")
|
|
5306
|
+
return "description is required";
|
|
5307
|
+
if (description.length > MAX_AGENT_DESCRIPTION)
|
|
5308
|
+
return `description is too long (max ${MAX_AGENT_DESCRIPTION})`;
|
|
5309
|
+
if (role.model !== undefined) {
|
|
5310
|
+
const slash = role.model.indexOf("/");
|
|
5311
|
+
if (slash <= 0 || slash === role.model.length - 1)
|
|
5312
|
+
return "model must be provider/modelId";
|
|
5313
|
+
}
|
|
5314
|
+
if (role.thinkingLevel !== undefined && !isThinkingLevel(role.thinkingLevel)) {
|
|
5315
|
+
return `thinkingLevel must be one of ${THINKING_LEVELS.join(", ")}`;
|
|
5316
|
+
}
|
|
5317
|
+
return;
|
|
5318
|
+
}
|
|
5319
|
+
|
|
5320
|
+
// apps/host/src/agent-roles.ts
|
|
5321
|
+
var KNOWN_FIELDS = new Set(["description", "model", "thinkingLevel"]);
|
|
5322
|
+
async function load(dir) {
|
|
5323
|
+
const roles = [];
|
|
5324
|
+
const problems = [];
|
|
5325
|
+
const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
|
|
5326
|
+
for (const entry of entries) {
|
|
5327
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
5328
|
+
continue;
|
|
5329
|
+
const file = entry.name;
|
|
5330
|
+
const name = file.slice(0, -".md".length);
|
|
5331
|
+
const problem = (reason2) => void problems.push({ file, reason: reason2 });
|
|
5332
|
+
let parsed;
|
|
5333
|
+
try {
|
|
5334
|
+
parsed = parseFrontmatter(await readFile3(join8(dir, file), "utf8"));
|
|
5335
|
+
} catch (error) {
|
|
5336
|
+
problem(`Couldn't parse the file: ${error instanceof Error ? error.message : String(error)}`);
|
|
5337
|
+
continue;
|
|
5338
|
+
}
|
|
5339
|
+
if (typeof parsed.frontmatter !== "object" || parsed.frontmatter === null || Array.isArray(parsed.frontmatter)) {
|
|
5340
|
+
problem("Frontmatter must be a record of fields");
|
|
5341
|
+
continue;
|
|
5342
|
+
}
|
|
5343
|
+
const unknown = Object.keys(parsed.frontmatter).find((key) => !KNOWN_FIELDS.has(key));
|
|
5344
|
+
if (unknown !== undefined) {
|
|
5345
|
+
problem(`Unknown field "${unknown}"`);
|
|
5346
|
+
continue;
|
|
5347
|
+
}
|
|
5348
|
+
const bad = Object.entries(parsed.frontmatter).find(([, value]) => typeof value !== "string");
|
|
5349
|
+
if (bad !== undefined) {
|
|
5350
|
+
problem(`Field "${bad[0]}" must be a string`);
|
|
5351
|
+
continue;
|
|
5352
|
+
}
|
|
5353
|
+
const frontmatter = parsed.frontmatter;
|
|
5354
|
+
const role = {
|
|
5355
|
+
name,
|
|
5356
|
+
description: frontmatter.description ?? "",
|
|
5357
|
+
...frontmatter.model === undefined ? {} : { model: frontmatter.model },
|
|
5358
|
+
...frontmatter.thinkingLevel === undefined ? {} : { thinkingLevel: frontmatter.thinkingLevel },
|
|
5359
|
+
prompt: parsed.body.trim()
|
|
5360
|
+
};
|
|
5361
|
+
const reason = agentRoleProblem(role);
|
|
5362
|
+
if (reason !== undefined) {
|
|
5363
|
+
problem(reason);
|
|
5364
|
+
continue;
|
|
5365
|
+
}
|
|
5366
|
+
roles.push(role);
|
|
5367
|
+
}
|
|
5368
|
+
roles.sort((a, b) => a.name.localeCompare(b.name));
|
|
5369
|
+
problems.sort((a, b) => a.file.localeCompare(b.file));
|
|
5370
|
+
return { roles, problems };
|
|
5371
|
+
}
|
|
5372
|
+
function serialize(role) {
|
|
5373
|
+
const fields = [
|
|
5374
|
+
`description: ${JSON.stringify(role.description)}`,
|
|
5375
|
+
...role.model === undefined ? [] : [`model: ${JSON.stringify(role.model)}`],
|
|
5376
|
+
...role.thinkingLevel === undefined ? [] : [`thinkingLevel: ${JSON.stringify(role.thinkingLevel)}`]
|
|
5377
|
+
];
|
|
5378
|
+
return `---
|
|
5379
|
+
${fields.join(`
|
|
5380
|
+
`)}
|
|
5381
|
+
---
|
|
5382
|
+
|
|
5383
|
+
${role.prompt}
|
|
5384
|
+
`;
|
|
5385
|
+
}
|
|
5386
|
+
var sameRole = (a, b) => a.name === b.name && a.description === b.description && a.model === b.model && a.thinkingLevel === b.thinkingLevel && a.prompt === b.prompt;
|
|
5387
|
+
var sameProblem = (a, b) => a.file === b.file && a.reason === b.reason;
|
|
5388
|
+
async function openAgentRoles(options) {
|
|
5389
|
+
const dir = options.dir;
|
|
5390
|
+
await mkdir2(dir, { recursive: true });
|
|
5391
|
+
const debounceMs = options.debounceMs ?? 150;
|
|
5392
|
+
const rescanMs = options.rescanMs ?? 1000;
|
|
5393
|
+
const published = new Published({ roles: [], problems: [] }, (a, b) => sameList(a.roles, b.roles, sameRole) && sameList(a.problems, b.problems, sameProblem));
|
|
5394
|
+
let closed = false;
|
|
5395
|
+
let reported = false;
|
|
5396
|
+
let timer;
|
|
5397
|
+
let queued = false;
|
|
5398
|
+
let chain = Promise.resolve();
|
|
5399
|
+
const loadOnce = async () => {
|
|
5400
|
+
try {
|
|
5401
|
+
published.set(await load(dir));
|
|
5402
|
+
reported = false;
|
|
4286
5403
|
} catch (error) {
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
message: `Skills were not expanded: ${error instanceof Error ? error.message : String(error)}`
|
|
4291
|
-
});
|
|
4292
|
-
return text;
|
|
4293
|
-
}
|
|
4294
|
-
const names = bound ?? mentionedSkillNames(text, skills.map((skill) => skill.name));
|
|
4295
|
-
const expanded = await expandSkills(text, names, skills);
|
|
4296
|
-
for (const name of expanded.missing) {
|
|
4297
|
-
this.send({ type: "notice", level: "warning", message: `Skill "${name}" is not available here; sent without it` });
|
|
5404
|
+
if (!reported)
|
|
5405
|
+
console.error(`agent-roles reload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5406
|
+
reported = true;
|
|
4298
5407
|
}
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
5408
|
+
};
|
|
5409
|
+
const reload = () => {
|
|
5410
|
+
if (closed)
|
|
5411
|
+
return Promise.resolve();
|
|
5412
|
+
queued = true;
|
|
5413
|
+
chain = chain.then(async () => {
|
|
5414
|
+
if (!queued || closed)
|
|
5415
|
+
return;
|
|
5416
|
+
queued = false;
|
|
5417
|
+
await loadOnce();
|
|
5418
|
+
});
|
|
5419
|
+
return chain;
|
|
5420
|
+
};
|
|
5421
|
+
const watcher = watch(dir, () => {
|
|
5422
|
+
if (closed)
|
|
5423
|
+
return;
|
|
5424
|
+
clearTimeout(timer);
|
|
5425
|
+
timer = setTimeout(() => void reload(), debounceMs);
|
|
5426
|
+
});
|
|
5427
|
+
watcher.on("error", (error) => {
|
|
5428
|
+
if (!reported)
|
|
5429
|
+
console.error(`agent-roles watch failed: ${error.message}`);
|
|
5430
|
+
reported = true;
|
|
5431
|
+
reload();
|
|
5432
|
+
});
|
|
5433
|
+
await reload();
|
|
5434
|
+
const rescan = setInterval(() => void reload(), rescanMs);
|
|
5435
|
+
let pending = Promise.resolve();
|
|
5436
|
+
const enqueue = (operation) => {
|
|
5437
|
+
const run2 = pending.then(operation, operation);
|
|
5438
|
+
pending = run2.then(() => {}, () => {});
|
|
5439
|
+
return run2;
|
|
5440
|
+
};
|
|
5441
|
+
const write = async (role, mustExist) => {
|
|
5442
|
+
const problem = agentRoleProblem(role);
|
|
5443
|
+
if (problem !== undefined)
|
|
5444
|
+
throw new Error(problem);
|
|
5445
|
+
await enqueue(async () => {
|
|
5446
|
+
const target = join8(dir, `${role.name}.md`);
|
|
5447
|
+
const exists = existsSync2(target);
|
|
5448
|
+
if (mustExist && !exists)
|
|
5449
|
+
throw new Error(`No such agent: "${role.name}"`);
|
|
5450
|
+
if (!mustExist && exists)
|
|
5451
|
+
throw new Error(`Agent "${role.name}" already exists`);
|
|
5452
|
+
const temp = join8(dir, `.${role.name}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
5453
|
+
try {
|
|
5454
|
+
await writeFile(temp, serialize(role));
|
|
5455
|
+
await rename(temp, target);
|
|
5456
|
+
} catch (error) {
|
|
5457
|
+
await rm2(temp, { force: true }).catch(() => {});
|
|
5458
|
+
throw error;
|
|
5459
|
+
}
|
|
5460
|
+
await reload();
|
|
5461
|
+
});
|
|
5462
|
+
};
|
|
5463
|
+
return {
|
|
5464
|
+
get value() {
|
|
5465
|
+
return published.value;
|
|
5466
|
+
},
|
|
5467
|
+
subscribe: (listener) => published.subscribe(listener),
|
|
5468
|
+
create: (role) => write(role, false),
|
|
5469
|
+
update: (role) => write(role, true),
|
|
5470
|
+
async remove(name) {
|
|
5471
|
+
const problem = agentNameProblem(name);
|
|
5472
|
+
if (problem !== undefined)
|
|
5473
|
+
throw new Error(problem);
|
|
5474
|
+
await enqueue(async () => {
|
|
5475
|
+
const target = join8(dir, `${name}.md`);
|
|
5476
|
+
if (!existsSync2(target))
|
|
5477
|
+
throw new Error(`No such agent: "${name}"`);
|
|
5478
|
+
await rm2(target);
|
|
5479
|
+
await reload();
|
|
4304
5480
|
});
|
|
5481
|
+
},
|
|
5482
|
+
close() {
|
|
5483
|
+
closed = true;
|
|
5484
|
+
clearTimeout(timer);
|
|
5485
|
+
clearInterval(rescan);
|
|
5486
|
+
watcher.close();
|
|
4305
5487
|
}
|
|
4306
|
-
|
|
4307
|
-
}
|
|
4308
|
-
async#dismissPendingQuestions(conversationId, context4) {
|
|
4309
|
-
const questionDocs = this.#questionDocs();
|
|
4310
|
-
const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId, context4)));
|
|
4311
|
-
const pending = snapshots.some((value) => Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
|
|
4312
|
-
if (!pending)
|
|
4313
|
-
return false;
|
|
4314
|
-
return await this.#options.harness.commit((tx) => dismissPendingQuestions(tx, questionDocs, conversationId), context4);
|
|
4315
|
-
}
|
|
4316
|
-
#questionDocs() {
|
|
4317
|
-
return (this.#options.docs ?? []).filter((doc) => doc.presentation === "pinomad.question");
|
|
4318
|
-
}
|
|
4319
|
-
get #chatsPrefix() {
|
|
4320
|
-
return join8(this.#options.dataDir, "chats") + sep5;
|
|
4321
|
-
}
|
|
4322
|
-
async#changes(conversation, context4) {
|
|
4323
|
-
const cwd = (await conversation.agent(context4)).cwd;
|
|
4324
|
-
if (cwd === undefined)
|
|
4325
|
-
return { available: false, reason: "No working directory" };
|
|
4326
|
-
if (cwd.startsWith(this.#chatsPrefix))
|
|
4327
|
-
return { available: false, reason: "Chat conversations have no repository" };
|
|
4328
|
-
const index = await this.#options.harness.snapshot(IndexDoc, context4);
|
|
4329
|
-
const record = checkoutAt(index?.checkouts, cwd);
|
|
4330
|
-
if (record !== undefined) {
|
|
4331
|
-
if (!await worktreeExists(record.path))
|
|
4332
|
-
return { available: false, reason: "No changes yet" };
|
|
4333
|
-
const [changes, branch2] = await Promise.all([changesOf(record.path, record.base), branchOf(record.path)]);
|
|
4334
|
-
return { available: true, ...changes, branch: branch2 };
|
|
4335
|
-
}
|
|
4336
|
-
const [base, branch] = await Promise.all([gitBase(cwd), branchOf(cwd)]);
|
|
4337
|
-
if (base === undefined)
|
|
4338
|
-
return { available: false, reason: "Not in a git repository", branch };
|
|
4339
|
-
return { available: true, ...await changesOf(base.repo, base.base), branch };
|
|
4340
|
-
}
|
|
4341
|
-
async dispose() {
|
|
4342
|
-
if (this.#disposed)
|
|
4343
|
-
return;
|
|
4344
|
-
this.#disposed = true;
|
|
4345
|
-
const subscriptions = [...this.#subscriptions.values()];
|
|
4346
|
-
this.#subscriptions.clear();
|
|
4347
|
-
for (const state of subscriptions)
|
|
4348
|
-
await state.subscription?.stop();
|
|
4349
|
-
}
|
|
5488
|
+
};
|
|
4350
5489
|
}
|
|
4351
5490
|
|
|
5491
|
+
// apps/host/src/host.ts
|
|
5492
|
+
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
5493
|
+
import lockfile from "proper-lockfile";
|
|
5494
|
+
|
|
4352
5495
|
// apps/host/src/attachments.ts
|
|
4353
5496
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
4354
5497
|
import {
|
|
@@ -4658,6 +5801,56 @@ class AttachmentStore {
|
|
|
4658
5801
|
}
|
|
4659
5802
|
}
|
|
4660
5803
|
|
|
5804
|
+
// apps/host/src/answer-timing.ts
|
|
5805
|
+
function withAnswerStart(message, answerStartedAt) {
|
|
5806
|
+
const pinomad = message.pinomad;
|
|
5807
|
+
return { ...message, pinomad: { ...pinomad, answerStartedAt } };
|
|
5808
|
+
}
|
|
5809
|
+
function stripPinomad(context4) {
|
|
5810
|
+
let changed = false;
|
|
5811
|
+
const messages = context4.messages.map((message) => {
|
|
5812
|
+
if (message.role !== "assistant" || !("pinomad" in message))
|
|
5813
|
+
return message;
|
|
5814
|
+
const { pinomad: _pinomad, ...rest } = message;
|
|
5815
|
+
changed = true;
|
|
5816
|
+
return rest;
|
|
5817
|
+
});
|
|
5818
|
+
return changed ? { ...context4, messages } : context4;
|
|
5819
|
+
}
|
|
5820
|
+
function trackAnswerStart(stream) {
|
|
5821
|
+
let answerStartedAt;
|
|
5822
|
+
const annotate = (event) => {
|
|
5823
|
+
if (event.type === "text_delta" && event.delta !== "") {
|
|
5824
|
+
answerStartedAt ??= Date.now();
|
|
5825
|
+
} else if (event.type === "text_end" && event.content !== "" && answerStartedAt === undefined) {
|
|
5826
|
+
answerStartedAt = Date.now();
|
|
5827
|
+
}
|
|
5828
|
+
if (answerStartedAt === undefined)
|
|
5829
|
+
return event;
|
|
5830
|
+
if (event.type === "done")
|
|
5831
|
+
return { ...event, message: withAnswerStart(event.message, answerStartedAt) };
|
|
5832
|
+
if (event.type === "error")
|
|
5833
|
+
return { ...event, error: withAnswerStart(event.error, answerStartedAt) };
|
|
5834
|
+
return { ...event, partial: withAnswerStart(event.partial, answerStartedAt) };
|
|
5835
|
+
};
|
|
5836
|
+
const push = stream.push.bind(stream);
|
|
5837
|
+
stream.push = (event) => push(annotate(event));
|
|
5838
|
+
const end = stream.end.bind(stream);
|
|
5839
|
+
stream.end = (result2) => end(result2 === undefined || answerStartedAt === undefined ? result2 : withAnswerStart(result2, answerStartedAt));
|
|
5840
|
+
return stream;
|
|
5841
|
+
}
|
|
5842
|
+
function withAnswerTiming(models) {
|
|
5843
|
+
const streamSimple = (model, context4, options) => trackAnswerStart(models.streamSimple(model, stripPinomad(context4), options));
|
|
5844
|
+
return new Proxy(models, {
|
|
5845
|
+
get(target, property) {
|
|
5846
|
+
if (property === "streamSimple")
|
|
5847
|
+
return streamSimple;
|
|
5848
|
+
const value = Reflect.get(target, property, target);
|
|
5849
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
5850
|
+
}
|
|
5851
|
+
});
|
|
5852
|
+
}
|
|
5853
|
+
|
|
4661
5854
|
// apps/host/src/host.ts
|
|
4662
5855
|
var context4 = BACKGROUND_CONTEXT4;
|
|
4663
5856
|
async function hostToken(dataDir) {
|
|
@@ -4672,6 +5865,8 @@ async function hostToken(dataDir) {
|
|
|
4672
5865
|
}
|
|
4673
5866
|
}
|
|
4674
5867
|
async function openHost(options) {
|
|
5868
|
+
if (options.remote?.publicUrl !== undefined)
|
|
5869
|
+
validatePublicUrl(options.remote.publicUrl);
|
|
4675
5870
|
await mkdir4(options.dataDir, { recursive: true });
|
|
4676
5871
|
const stale = Math.max(2000, options.lockStaleMs ?? 1e4);
|
|
4677
5872
|
const release = await lockfile.lock(options.dataDir, {
|
|
@@ -4687,6 +5882,7 @@ async function openHost(options) {
|
|
|
4687
5882
|
let attachments;
|
|
4688
5883
|
let extensions = [];
|
|
4689
5884
|
try {
|
|
5885
|
+
const models = withAnswerTiming(options.models);
|
|
4690
5886
|
const token = await hostToken(options.dataDir);
|
|
4691
5887
|
attachments = new AttachmentStore({ dir: join10(options.dataDir, "attachments") });
|
|
4692
5888
|
await attachments.open();
|
|
@@ -4723,7 +5919,7 @@ async function openHost(options) {
|
|
|
4723
5919
|
const environments = new Map;
|
|
4724
5920
|
const chatsDir = join10(options.dataDir, "chats");
|
|
4725
5921
|
harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(join10(options.dataDir, "session.sqlite")), {
|
|
4726
|
-
models
|
|
5922
|
+
models,
|
|
4727
5923
|
registry,
|
|
4728
5924
|
env: async (target) => {
|
|
4729
5925
|
const cwd = target.cwd;
|
|
@@ -4761,8 +5957,9 @@ async function openHost(options) {
|
|
|
4761
5957
|
const relayKey = options.relay === undefined ? undefined : await loadRelayKey(options.dataDir);
|
|
4762
5958
|
const gateway = await startGateway({
|
|
4763
5959
|
harness,
|
|
4764
|
-
models
|
|
5960
|
+
models,
|
|
4765
5961
|
modelSummaries: options.modelSummaries,
|
|
5962
|
+
...options.images === undefined ? {} : { images: options.images },
|
|
4766
5963
|
session: { id: basename3(options.dataDir), directory: options.dataDir },
|
|
4767
5964
|
dataDir: options.dataDir,
|
|
4768
5965
|
attachments,
|
|
@@ -4785,6 +5982,7 @@ async function openHost(options) {
|
|
|
4785
5982
|
...options.push.timers === undefined ? {} : { timers: options.push.timers }
|
|
4786
5983
|
}
|
|
4787
5984
|
},
|
|
5985
|
+
...options.rtc === undefined ? {} : { rtc: options.rtc },
|
|
4788
5986
|
secure: {
|
|
4789
5987
|
hostKey,
|
|
4790
5988
|
offers,
|
|
@@ -4823,7 +6021,15 @@ async function openHost(options) {
|
|
|
4823
6021
|
});
|
|
4824
6022
|
return binding === undefined ? undefined : offers.createAccount(binding);
|
|
4825
6023
|
},
|
|
4826
|
-
...gateway.remote === undefined || hostKey === undefined ? {} : {
|
|
6024
|
+
...gateway.remote === undefined || hostKey === undefined ? {} : {
|
|
6025
|
+
remote: {
|
|
6026
|
+
url: gateway.remote.url,
|
|
6027
|
+
get advertiseUrl() {
|
|
6028
|
+
return gateway.remote.advertiseUrl;
|
|
6029
|
+
},
|
|
6030
|
+
hostKey: hostKey.publicKey
|
|
6031
|
+
}
|
|
6032
|
+
},
|
|
4827
6033
|
...gateway.relay === undefined ? {} : { relay: { origin: gateway.relay.origin, hostId: gateway.relay.hostId, state: () => gateway.relay.state() } },
|
|
4828
6034
|
close() {
|
|
4829
6035
|
closing ??= (async () => {
|
|
@@ -4920,8 +6126,10 @@ function startHostLogRotation(dataDir) {
|
|
|
4920
6126
|
}
|
|
4921
6127
|
|
|
4922
6128
|
// apps/host/src/model-auth.ts
|
|
4923
|
-
import {
|
|
4924
|
-
import {
|
|
6129
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
6130
|
+
import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
|
|
6131
|
+
import { dirname as dirname3 } from "node:path";
|
|
6132
|
+
import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
4925
6133
|
|
|
4926
6134
|
// apps/host/src/pi-setup.ts
|
|
4927
6135
|
import { getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai";
|
|
@@ -5036,72 +6244,386 @@ function modelSummaries(modelRuntime) {
|
|
|
5036
6244
|
}
|
|
5037
6245
|
|
|
5038
6246
|
// apps/host/src/model-auth.ts
|
|
5039
|
-
var
|
|
6247
|
+
var BUILTIN = new Set(getBuiltinProviders());
|
|
6248
|
+
var CUSTOM_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
6249
|
+
var LOGIN_TIMEOUT_MS = 15 * 60000;
|
|
6250
|
+
var FINISHED_LOGIN_TTL_MS = 60000;
|
|
6251
|
+
var ENDPOINT_TIMEOUT_MS = 1e4;
|
|
6252
|
+
var sameModel = (a, b) => a.provider === b.provider && a.modelId === b.modelId && a.name === b.name && a.contextWindow === b.contextWindow && a.images === b.images && sameList(a.thinkingLevels, b.thinkingLevels, (x, y) => x === y);
|
|
5040
6253
|
var sameLogin = (a, b) => a.providerId === b.providerId && a.name === b.name && a.kind === b.kind && a.subscription === b.subscription;
|
|
5041
|
-
async function
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
6254
|
+
async function readModelsFile(path) {
|
|
6255
|
+
let text;
|
|
6256
|
+
try {
|
|
6257
|
+
text = await readFile6(path, "utf8");
|
|
6258
|
+
} catch (error) {
|
|
6259
|
+
if (error.code === "ENOENT")
|
|
6260
|
+
return { providers: {} };
|
|
6261
|
+
throw error;
|
|
6262
|
+
}
|
|
6263
|
+
const parsed = JSON.parse(text);
|
|
6264
|
+
return { ...parsed, providers: parsed.providers ?? {} };
|
|
5051
6265
|
}
|
|
5052
|
-
|
|
5053
|
-
const
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
6266
|
+
function customProviders(file) {
|
|
6267
|
+
const custom = new Map;
|
|
6268
|
+
for (const [id, entry] of Object.entries(file.providers)) {
|
|
6269
|
+
if (BUILTIN.has(id) || !isCustomProviderApi(entry.api) || typeof entry.baseUrl !== "string")
|
|
6270
|
+
continue;
|
|
6271
|
+
const models = Array.isArray(entry.models) ? entry.models.flatMap((model) => typeof model?.id === "string" ? [model.id] : []) : [];
|
|
6272
|
+
custom.set(id, {
|
|
6273
|
+
name: typeof entry.name === "string" ? entry.name : id,
|
|
6274
|
+
api: entry.api,
|
|
6275
|
+
baseUrl: entry.baseUrl,
|
|
6276
|
+
models
|
|
6277
|
+
});
|
|
6278
|
+
}
|
|
6279
|
+
return custom;
|
|
6280
|
+
}
|
|
6281
|
+
var toLoginEvent = (event) => {
|
|
6282
|
+
switch (event.type) {
|
|
6283
|
+
case "device_code":
|
|
6284
|
+
return {
|
|
6285
|
+
type: "device_code",
|
|
6286
|
+
userCode: event.userCode,
|
|
6287
|
+
verificationUri: event.verificationUri,
|
|
6288
|
+
...event.expiresInSeconds === undefined ? {} : { expiresAt: Date.now() + event.expiresInSeconds * 1000 }
|
|
6289
|
+
};
|
|
6290
|
+
case "info":
|
|
6291
|
+
return { type: "info", message: event.message, ...event.links === undefined ? {} : { links: event.links } };
|
|
6292
|
+
case "auth_url":
|
|
6293
|
+
return { type: "auth_url", url: event.url, ...event.instructions === undefined ? {} : { instructions: event.instructions } };
|
|
6294
|
+
case "progress":
|
|
6295
|
+
return { type: "progress", message: event.message };
|
|
6296
|
+
}
|
|
6297
|
+
};
|
|
6298
|
+
var toLoginPrompt = (id, prompt) => {
|
|
6299
|
+
const placeholder = "placeholder" in prompt && prompt.placeholder !== undefined ? { placeholder: prompt.placeholder } : {};
|
|
6300
|
+
if (prompt.type === "select") {
|
|
6301
|
+
return {
|
|
6302
|
+
id,
|
|
6303
|
+
type: "select",
|
|
6304
|
+
message: prompt.message,
|
|
6305
|
+
options: prompt.options.map((option) => ({
|
|
6306
|
+
id: option.id,
|
|
6307
|
+
label: option.label,
|
|
6308
|
+
...option.description === undefined ? {} : { description: option.description }
|
|
6309
|
+
}))
|
|
6310
|
+
};
|
|
6311
|
+
}
|
|
6312
|
+
return { id, type: prompt.type, message: prompt.message, ...placeholder };
|
|
6313
|
+
};
|
|
6314
|
+
async function openModelAuth(runtime, options) {
|
|
6315
|
+
const loginTimeoutMs = options.loginTimeoutMs ?? LOGIN_TIMEOUT_MS;
|
|
6316
|
+
const finishedLoginTtlMs = options.finishedLoginTtlMs ?? FINISHED_LOGIN_TTL_MS;
|
|
6317
|
+
let custom = customProviders(await readModelsFile(options.modelsPath));
|
|
6318
|
+
const logins = new Map;
|
|
6319
|
+
const dropTimers = new Set;
|
|
5060
6320
|
let closed = false;
|
|
5061
|
-
const
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
}
|
|
6321
|
+
const storedLogins = async () => {
|
|
6322
|
+
const credentials = await runtime.listCredentials();
|
|
6323
|
+
const list = credentials.map((credential) => ({
|
|
6324
|
+
providerId: credential.providerId,
|
|
6325
|
+
name: runtime.getProvider(credential.providerId)?.name ?? credential.providerId,
|
|
6326
|
+
kind: credential.type,
|
|
6327
|
+
subscription: runtime.isUsingSubscription(credential.providerId)
|
|
6328
|
+
}));
|
|
6329
|
+
list.sort((a, b) => a.providerId.localeCompare(b.providerId));
|
|
6330
|
+
return { logins: list };
|
|
5072
6331
|
};
|
|
5073
|
-
const
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
6332
|
+
const catalog = (stored) => {
|
|
6333
|
+
const byId = new Map(stored.logins.map((login) => [login.providerId, login]));
|
|
6334
|
+
const infos = runtime.getProviders().map((provider) => {
|
|
6335
|
+
const login = byId.get(provider.id);
|
|
6336
|
+
const status = runtime.getProviderAuthStatus(provider.id);
|
|
6337
|
+
const oauth = provider.auth.oauth;
|
|
6338
|
+
const apiKey = provider.auth.apiKey?.login === undefined ? undefined : provider.auth.apiKey;
|
|
6339
|
+
const endpoint = custom.get(provider.id);
|
|
6340
|
+
return {
|
|
6341
|
+
id: provider.id,
|
|
6342
|
+
name: endpoint?.name ?? provider.name,
|
|
6343
|
+
...oauth === undefined ? {} : {
|
|
6344
|
+
oauth: {
|
|
6345
|
+
name: oauth.name,
|
|
6346
|
+
subscription: oauth.isSubscription === true,
|
|
6347
|
+
...oauth.loginLabel === undefined ? {} : { loginLabel: oauth.loginLabel }
|
|
6348
|
+
}
|
|
6349
|
+
},
|
|
6350
|
+
...apiKey === undefined ? {} : { apiKey: { name: apiKey.name } },
|
|
6351
|
+
...login === undefined ? {} : { stored: { kind: login.kind, subscription: login.subscription } },
|
|
6352
|
+
...login === undefined && status.configured && status.source === "environment" && status.label !== undefined ? { ambient: status.label } : {},
|
|
6353
|
+
...endpoint === undefined ? {} : { custom: { api: endpoint.api, baseUrl: endpoint.baseUrl, models: endpoint.models } }
|
|
6354
|
+
};
|
|
5080
6355
|
});
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
6356
|
+
return infos.sort((a, b) => Number(a.custom !== undefined) - Number(b.custom !== undefined) || a.name.localeCompare(b.name));
|
|
6357
|
+
};
|
|
6358
|
+
const flows = () => [...logins.values()].map((login) => login.flow);
|
|
6359
|
+
const initialAuth = await storedLogins();
|
|
6360
|
+
const models = new Published(modelSummaries(runtime), (a, b) => sameList(a, b, sameModel));
|
|
6361
|
+
const auth = new Published(initialAuth, (a, b) => sameList(a.logins, b.logins, sameLogin));
|
|
6362
|
+
const providers = new Published({ providers: catalog(initialAuth), logins: [] }, () => false);
|
|
6363
|
+
const publishFlows = () => {
|
|
6364
|
+
if (!closed)
|
|
6365
|
+
providers.set({ providers: providers.value.providers, logins: flows() });
|
|
6366
|
+
};
|
|
6367
|
+
const republish = async () => {
|
|
6368
|
+
const stored = await storedLogins();
|
|
6369
|
+
models.set(modelSummaries(runtime));
|
|
6370
|
+
auth.set(stored);
|
|
6371
|
+
if (!closed)
|
|
6372
|
+
providers.set({ providers: catalog(stored), logins: flows() });
|
|
6373
|
+
};
|
|
6374
|
+
const end = (login, state, error) => {
|
|
6375
|
+
if (login.flow.state !== "running")
|
|
6376
|
+
return;
|
|
6377
|
+
clearTimeout(login.timer);
|
|
6378
|
+
login.pending?.reject(new Error("Login ended"));
|
|
6379
|
+
login.pending = undefined;
|
|
6380
|
+
const { prompt: _, ...rest } = login.flow;
|
|
6381
|
+
login.flow = { ...rest, state, ...error === undefined ? {} : { error } };
|
|
6382
|
+
const drop = setTimeout(() => {
|
|
6383
|
+
dropTimers.delete(drop);
|
|
6384
|
+
logins.delete(login.flow.id);
|
|
6385
|
+
publishFlows();
|
|
6386
|
+
}, finishedLoginTtlMs);
|
|
6387
|
+
dropTimers.add(drop);
|
|
6388
|
+
publishFlows();
|
|
6389
|
+
};
|
|
6390
|
+
const updateModelsFile = async (change) => {
|
|
6391
|
+
const file = await readModelsFile(options.modelsPath);
|
|
6392
|
+
change(file);
|
|
6393
|
+
const tmp = `${options.modelsPath}.tmp`;
|
|
6394
|
+
await mkdir5(dirname3(options.modelsPath), { recursive: true });
|
|
6395
|
+
await writeFile4(tmp, `${JSON.stringify(file, null, 2)}
|
|
6396
|
+
`, { mode: 384 });
|
|
6397
|
+
await rename4(tmp, options.modelsPath);
|
|
6398
|
+
custom = customProviders(file);
|
|
6399
|
+
};
|
|
6400
|
+
const storeKey = async (providerId, key) => {
|
|
6401
|
+
await runtime.login(providerId, "api_key", {
|
|
6402
|
+
prompt: async () => key,
|
|
6403
|
+
notify: () => {}
|
|
5085
6404
|
});
|
|
5086
|
-
}
|
|
6405
|
+
};
|
|
6406
|
+
let tail = Promise.resolve();
|
|
6407
|
+
const serial = (operation) => {
|
|
6408
|
+
const next = tail.then(operation, operation);
|
|
6409
|
+
tail = next.catch(() => {});
|
|
6410
|
+
return next;
|
|
6411
|
+
};
|
|
5087
6412
|
return {
|
|
5088
6413
|
models,
|
|
5089
6414
|
auth,
|
|
6415
|
+
providers,
|
|
5090
6416
|
async logout(providerId) {
|
|
5091
6417
|
if (!auth.value.logins.some((login) => login.providerId === providerId)) {
|
|
5092
6418
|
throw new Error(`No stored credential for provider "${providerId}"`);
|
|
5093
6419
|
}
|
|
5094
|
-
await
|
|
5095
|
-
|
|
5096
|
-
|
|
6420
|
+
await runtime.logout(providerId);
|
|
6421
|
+
await republish();
|
|
6422
|
+
},
|
|
6423
|
+
beginLogin(providerId, method) {
|
|
6424
|
+
const provider = runtime.getProvider(providerId);
|
|
6425
|
+
if (provider === undefined)
|
|
6426
|
+
throw new Error(`Unknown provider "${providerId}"`);
|
|
6427
|
+
const offered = method === "oauth" ? provider.auth.oauth !== undefined : provider.auth.apiKey?.login !== undefined;
|
|
6428
|
+
if (!offered)
|
|
6429
|
+
throw new Error(`${provider.name} has no ${method === "oauth" ? "sign-in" : "API key"} login`);
|
|
6430
|
+
if ([...logins.values()].some((login2) => login2.flow.providerId === providerId && login2.flow.state === "running")) {
|
|
6431
|
+
throw new Error(`A login for ${provider.name} is already in progress`);
|
|
6432
|
+
}
|
|
6433
|
+
const id = randomUUID2();
|
|
6434
|
+
const login = {
|
|
6435
|
+
flow: { id, providerId, method, events: [], state: "running" },
|
|
6436
|
+
controller: new AbortController,
|
|
6437
|
+
nextPromptId: 1
|
|
6438
|
+
};
|
|
6439
|
+
logins.set(id, login);
|
|
6440
|
+
login.timer = setTimeout(() => {
|
|
6441
|
+
login.controller.abort();
|
|
6442
|
+
end(login, "failed", "The login timed out");
|
|
6443
|
+
}, loginTimeoutMs);
|
|
6444
|
+
runtime.login(providerId, method, {
|
|
6445
|
+
signal: login.controller.signal,
|
|
6446
|
+
notify(event) {
|
|
6447
|
+
if (login.flow.state !== "running")
|
|
6448
|
+
return;
|
|
6449
|
+
login.flow = { ...login.flow, events: [...login.flow.events, toLoginEvent(event)] };
|
|
6450
|
+
publishFlows();
|
|
6451
|
+
},
|
|
6452
|
+
prompt(prompt) {
|
|
6453
|
+
return new Promise((resolve7, reject) => {
|
|
6454
|
+
if (login.flow.state !== "running") {
|
|
6455
|
+
reject(new Error("Login ended"));
|
|
6456
|
+
return;
|
|
6457
|
+
}
|
|
6458
|
+
const promptId = login.nextPromptId++;
|
|
6459
|
+
login.pending = { id: promptId, resolve: resolve7, reject };
|
|
6460
|
+
login.flow = { ...login.flow, prompt: toLoginPrompt(promptId, prompt) };
|
|
6461
|
+
publishFlows();
|
|
6462
|
+
prompt.signal?.addEventListener("abort", () => {
|
|
6463
|
+
if (login.pending?.id !== promptId)
|
|
6464
|
+
return;
|
|
6465
|
+
login.pending = undefined;
|
|
6466
|
+
const { prompt: _, ...rest } = login.flow;
|
|
6467
|
+
login.flow = rest;
|
|
6468
|
+
reject(new Error("Prompt withdrawn"));
|
|
6469
|
+
publishFlows();
|
|
6470
|
+
}, { once: true });
|
|
6471
|
+
});
|
|
6472
|
+
}
|
|
6473
|
+
}, { agentName: "PiNomad", getDeviceId: options.installId }).then(async () => {
|
|
6474
|
+
await republish();
|
|
6475
|
+
end(login, "succeeded");
|
|
6476
|
+
}, (error) => {
|
|
6477
|
+
if (login.controller.signal.aborted)
|
|
6478
|
+
return;
|
|
6479
|
+
end(login, "failed", error instanceof Error ? error.message : String(error));
|
|
6480
|
+
});
|
|
6481
|
+
publishFlows();
|
|
6482
|
+
return id;
|
|
6483
|
+
},
|
|
6484
|
+
answerLogin(loginId, promptId, value) {
|
|
6485
|
+
const login = logins.get(loginId);
|
|
6486
|
+
if (login === undefined || login.flow.state !== "running")
|
|
6487
|
+
throw new Error("This login has ended");
|
|
6488
|
+
const pending = login.pending;
|
|
6489
|
+
if (pending === undefined || pending.id !== promptId)
|
|
6490
|
+
throw new Error("This prompt has moved on");
|
|
6491
|
+
login.pending = undefined;
|
|
6492
|
+
const { prompt: _, ...rest } = login.flow;
|
|
6493
|
+
login.flow = rest;
|
|
6494
|
+
publishFlows();
|
|
6495
|
+
pending.resolve(value);
|
|
6496
|
+
},
|
|
6497
|
+
cancelLogin(loginId) {
|
|
6498
|
+
const login = logins.get(loginId);
|
|
6499
|
+
if (login === undefined || login.flow.state !== "running")
|
|
6500
|
+
return;
|
|
6501
|
+
login.controller.abort();
|
|
6502
|
+
end(login, "cancelled");
|
|
6503
|
+
},
|
|
6504
|
+
saveCustomProvider: (input, apiKey) => serial(async () => {
|
|
6505
|
+
if (!CUSTOM_ID.test(input.id))
|
|
6506
|
+
throw new Error("Use lowercase letters, digits and - for the endpoint id");
|
|
6507
|
+
if (BUILTIN.has(input.id))
|
|
6508
|
+
throw new Error(`"${input.id}" is a built-in provider; pick another id`);
|
|
6509
|
+
const name = input.name.trim();
|
|
6510
|
+
if (name === "")
|
|
6511
|
+
throw new Error("Name the endpoint");
|
|
6512
|
+
let url;
|
|
6513
|
+
try {
|
|
6514
|
+
url = new URL(input.baseUrl);
|
|
6515
|
+
} catch {
|
|
6516
|
+
throw new Error("Enter a full base URL, like http://localhost:11434/v1");
|
|
6517
|
+
}
|
|
6518
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
6519
|
+
throw new Error("Use an http:// or https:// base URL");
|
|
6520
|
+
const ids = [...new Set(input.models.map((model) => model.trim()).filter((model) => model !== ""))];
|
|
6521
|
+
if (ids.length === 0)
|
|
6522
|
+
throw new Error("Add at least one model");
|
|
6523
|
+
const key = typeof apiKey === "string" && apiKey !== "" ? apiKey : apiKey !== null && custom.has(input.id) ? (await runtime.getAuth(input.id))?.auth.apiKey : undefined;
|
|
6524
|
+
const reported = new Map((await fetchEndpointModels(input.api, input.baseUrl, key).catch(() => [])).map((model) => [model.id, model]));
|
|
6525
|
+
await updateModelsFile((file) => {
|
|
6526
|
+
const previous = file.providers[input.id] ?? {};
|
|
6527
|
+
const previousModels = new Map((Array.isArray(previous.models) ? previous.models : []).map((model) => [model.id, model]));
|
|
6528
|
+
file.providers[input.id] = {
|
|
6529
|
+
...previous,
|
|
6530
|
+
name,
|
|
6531
|
+
api: input.api,
|
|
6532
|
+
baseUrl: input.baseUrl,
|
|
6533
|
+
models: ids.map((id) => ({ ...reported.get(id), ...previousModels.get(id) ?? { id } }))
|
|
6534
|
+
};
|
|
6535
|
+
});
|
|
6536
|
+
await runtime.refresh({ providers: [input.id], allowNetwork: false });
|
|
6537
|
+
if (typeof apiKey === "string" && apiKey !== "")
|
|
6538
|
+
await storeKey(input.id, apiKey);
|
|
6539
|
+
else if (apiKey === null && (await runtime.listCredentials()).some((c) => c.providerId === input.id)) {
|
|
6540
|
+
await runtime.logout(input.id);
|
|
6541
|
+
}
|
|
6542
|
+
await republish();
|
|
6543
|
+
}),
|
|
6544
|
+
removeCustomProvider: (providerId) => serial(async () => {
|
|
6545
|
+
if (!custom.has(providerId))
|
|
6546
|
+
throw new Error(`No custom endpoint "${providerId}"`);
|
|
6547
|
+
if ((await runtime.listCredentials()).some((c) => c.providerId === providerId))
|
|
6548
|
+
await runtime.logout(providerId);
|
|
6549
|
+
await updateModelsFile((file) => {
|
|
6550
|
+
delete file.providers[providerId];
|
|
6551
|
+
});
|
|
6552
|
+
await runtime.refresh({ providers: [providerId], allowNetwork: false });
|
|
6553
|
+
await republish();
|
|
6554
|
+
}),
|
|
6555
|
+
async listEndpointModels(input) {
|
|
6556
|
+
let key = input.apiKey;
|
|
6557
|
+
if (key === undefined && input.providerId !== undefined && custom.has(input.providerId)) {
|
|
6558
|
+
key = (await runtime.getAuth(input.providerId))?.auth.apiKey;
|
|
6559
|
+
}
|
|
6560
|
+
return (await fetchEndpointModels(input.api, input.baseUrl, key)).map((model) => model.id);
|
|
5097
6561
|
},
|
|
5098
6562
|
close() {
|
|
5099
6563
|
closed = true;
|
|
5100
|
-
|
|
5101
|
-
|
|
6564
|
+
for (const login of logins.values()) {
|
|
6565
|
+
clearTimeout(login.timer);
|
|
6566
|
+
if (login.flow.state === "running")
|
|
6567
|
+
login.controller.abort();
|
|
6568
|
+
}
|
|
6569
|
+
for (const timer of dropTimers)
|
|
6570
|
+
clearTimeout(timer);
|
|
5102
6571
|
}
|
|
5103
6572
|
};
|
|
5104
6573
|
}
|
|
6574
|
+
async function fetchEndpointModels(api, baseUrl, key) {
|
|
6575
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
6576
|
+
const url = new URL(api === "anthropic-messages" ? `${base}/v1/models` : `${base}/models`);
|
|
6577
|
+
const headers = { accept: "application/json" };
|
|
6578
|
+
if (api === "anthropic-messages") {
|
|
6579
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
6580
|
+
if (key !== undefined)
|
|
6581
|
+
headers["x-api-key"] = key;
|
|
6582
|
+
} else if (api === "google-generative-ai") {
|
|
6583
|
+
if (key !== undefined)
|
|
6584
|
+
url.searchParams.set("key", key);
|
|
6585
|
+
} else if (key !== undefined) {
|
|
6586
|
+
headers.authorization = `Bearer ${key}`;
|
|
6587
|
+
}
|
|
6588
|
+
let response;
|
|
6589
|
+
try {
|
|
6590
|
+
response = await fetch(url, { headers, signal: AbortSignal.timeout(ENDPOINT_TIMEOUT_MS) });
|
|
6591
|
+
} catch (error) {
|
|
6592
|
+
const reason = error instanceof Error ? error.cause?.message ?? error.message : String(error);
|
|
6593
|
+
throw new Error(`Couldn't reach ${url.host} from the host: ${reason}`);
|
|
6594
|
+
}
|
|
6595
|
+
if (!response.ok)
|
|
6596
|
+
throw new Error(`${url.host} answered ${response.status} for /models`);
|
|
6597
|
+
const body = await response.json();
|
|
6598
|
+
const models = api === "google-generative-ai" ? (body.models ?? []).flatMap((model) => typeof model.name === "string" ? [
|
|
6599
|
+
endpointModel(model.name.replace(/^models\//, ""), {
|
|
6600
|
+
contextWindow: positive(model.inputTokenLimit),
|
|
6601
|
+
maxTokens: positive(model.outputTokenLimit),
|
|
6602
|
+
reasoning: model.thinking === true,
|
|
6603
|
+
image: false
|
|
6604
|
+
})
|
|
6605
|
+
] : []) : (body.data ?? []).flatMap((model) => typeof model.id === "string" ? [
|
|
6606
|
+
endpointModel(model.id, {
|
|
6607
|
+
contextWindow: positive(model.context_window) ?? positive(model.context_length) ?? positive(model.max_input_tokens),
|
|
6608
|
+
maxTokens: positive(model.max_output_tokens),
|
|
6609
|
+
reasoning: model.reasoning === true,
|
|
6610
|
+
image: Array.isArray(model.modalities?.input) && model.modalities.input.includes("image")
|
|
6611
|
+
})
|
|
6612
|
+
] : []);
|
|
6613
|
+
if (models.length === 0)
|
|
6614
|
+
throw new Error(`${url.host} listed no models`);
|
|
6615
|
+
return models;
|
|
6616
|
+
}
|
|
6617
|
+
var positive = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
6618
|
+
function endpointModel(id, reported) {
|
|
6619
|
+
return {
|
|
6620
|
+
id,
|
|
6621
|
+
...reported.contextWindow === undefined ? {} : { contextWindow: reported.contextWindow },
|
|
6622
|
+
...reported.maxTokens === undefined ? {} : { maxTokens: reported.maxTokens },
|
|
6623
|
+
...reported.reasoning ? { reasoning: true } : {},
|
|
6624
|
+
...reported.image ? { input: ["text", "image"] } : {}
|
|
6625
|
+
};
|
|
6626
|
+
}
|
|
5105
6627
|
|
|
5106
6628
|
// apps/host/src/main.ts
|
|
5107
6629
|
import { getSupportedThinkingLevels as getSupportedThinkingLevels3 } from "@earendil-works/pi-ai";
|
|
@@ -5121,6 +6643,8 @@ async function main(argv) {
|
|
|
5121
6643
|
relay: { type: "string" }
|
|
5122
6644
|
}
|
|
5123
6645
|
});
|
|
6646
|
+
if (values["public-url"] !== undefined)
|
|
6647
|
+
validatePublicUrl(values["public-url"]);
|
|
5124
6648
|
const dataDir = resolve7(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
5125
6649
|
let relayOrigin;
|
|
5126
6650
|
try {
|
|
@@ -5129,6 +6653,8 @@ async function main(argv) {
|
|
|
5129
6653
|
console.error(error instanceof Error ? error.message : String(error));
|
|
5130
6654
|
process.exit(1);
|
|
5131
6655
|
}
|
|
6656
|
+
if (relayOrigin !== undefined)
|
|
6657
|
+
validateRelayOrigin(relayOrigin);
|
|
5132
6658
|
const agentsHome = join12(homedir3(), ".agents");
|
|
5133
6659
|
const extensionsFor = (models, summaries, options2) => (host2) => [
|
|
5134
6660
|
createContext({
|
|
@@ -5194,9 +6720,10 @@ async function main(argv) {
|
|
|
5194
6720
|
};
|
|
5195
6721
|
}
|
|
5196
6722
|
async function piOptions() {
|
|
5197
|
-
const
|
|
5198
|
-
const
|
|
5199
|
-
const
|
|
6723
|
+
const modelsPath = join12(dataDir, "models.json");
|
|
6724
|
+
const modelRuntime = await ModelRuntime.create({ authPath: join12(dataDir, "auth.json"), modelsPath });
|
|
6725
|
+
const modelAuth = await openModelAuth(modelRuntime, { modelsPath, installId: await installId(dataDir) });
|
|
6726
|
+
const settingsManager = SettingsManager.inMemory(await piSettings(join12(dataDir, "settings.json")));
|
|
5200
6727
|
configureHarnessHttp(settingsManager);
|
|
5201
6728
|
const summaries = () => modelAuth.models.value;
|
|
5202
6729
|
const images = await createNodePhotonImages();
|
|
@@ -5205,6 +6732,7 @@ async function main(argv) {
|
|
|
5205
6732
|
models: modelRuntime,
|
|
5206
6733
|
modelSummaries: summaries,
|
|
5207
6734
|
modelAuth,
|
|
6735
|
+
images,
|
|
5208
6736
|
extensions: extensionsFor(modelRuntime, summaries, { images }),
|
|
5209
6737
|
settings: (store) => createHarnessSettings(settingsManager, store),
|
|
5210
6738
|
push: {},
|
|
@@ -5256,6 +6784,34 @@ async function main(argv) {
|
|
|
5256
6784
|
host.close().finally(() => process.exit(1));
|
|
5257
6785
|
});
|
|
5258
6786
|
}
|
|
6787
|
+
async function piSettings(path) {
|
|
6788
|
+
let text;
|
|
6789
|
+
try {
|
|
6790
|
+
text = await readFile7(path, "utf8");
|
|
6791
|
+
} catch (error) {
|
|
6792
|
+
if (error.code === "ENOENT")
|
|
6793
|
+
return {};
|
|
6794
|
+
throw error;
|
|
6795
|
+
}
|
|
6796
|
+
let block;
|
|
6797
|
+
try {
|
|
6798
|
+
block = JSON.parse(text).pi;
|
|
6799
|
+
} catch {
|
|
6800
|
+
return {};
|
|
6801
|
+
}
|
|
6802
|
+
return typeof block === "object" && block !== null && !Array.isArray(block) ? block : {};
|
|
6803
|
+
}
|
|
6804
|
+
async function installId(dataDir) {
|
|
6805
|
+
const path = join12(dataDir, "install-id");
|
|
6806
|
+
let id = (await readFile7(path, "utf8").catch(() => "")).trim();
|
|
6807
|
+
if (id === "") {
|
|
6808
|
+
id = randomUUID3();
|
|
6809
|
+
await mkdir6(dataDir, { recursive: true });
|
|
6810
|
+
await writeFile5(path, `${id}
|
|
6811
|
+
`, { mode: 384 });
|
|
6812
|
+
}
|
|
6813
|
+
return () => id;
|
|
6814
|
+
}
|
|
5259
6815
|
if (false)
|
|
5260
6816
|
;
|
|
5261
6817
|
export {
|