pinomad 0.5.0 → 0.7.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 +2 -2
- package/dist/{chunk-f0v3sht0.js → chunk-07769pqx.js} +10 -8
- package/dist/{chunk-ebfaf3k2.js → chunk-0xh6cy9v.js} +1 -1
- package/dist/{chunk-aqj4wg33.js → chunk-12kaxggg.js} +29 -12
- package/dist/{chunk-d4wdt5qs.js → chunk-1nyzr0mg.js} +2282 -1027
- package/dist/chunk-1t58fh0k.js +16 -0
- package/dist/{chunk-4by4x5c4.js → chunk-6ysezfy5.js} +44 -14
- package/dist/{chunk-kmbh44dt.js → chunk-7qb9tq54.js} +42 -6
- package/dist/{chunk-gqr7ahv4.js → chunk-8yme2wep.js} +2 -2
- package/dist/chunk-b1h374jr.js +159 -0
- package/dist/{chunk-hsp12cjt.js → chunk-e4ahjbmz.js} +212 -50
- package/dist/{chunk-9d59jjmt.js → chunk-fbkd73zr.js} +36 -10
- package/dist/chunk-kzbzacz9.js +12 -0
- package/dist/{chunk-zttf1hvh.js → chunk-m4c5gvce.js} +4 -3
- package/dist/{chunk-cjfmfyjb.js → chunk-ngf1zatm.js} +7 -6
- package/dist/{chunk-1d9sppj0.js → chunk-rtwxjb90.js} +1 -1
- package/dist/{chunk-m4q9wafw.js → chunk-sj7rnvh4.js} +118 -6
- package/dist/chunk-sz3rzk55.js +79 -0
- package/dist/{chunk-26d04z02.js → chunk-ywmm63ks.js} +7 -3
- package/dist/chunk-znhn8af7.js +33 -0
- package/dist/pinomad.js +36 -12
- package/package.json +1 -1
- package/web/assets/index-C0rwAo0H.css +1 -0
- package/web/assets/index-DGee2kNU.js +394 -0
- package/web/index.html +2 -2
- package/dist/chunk-5ng86fmy.js +0 -14
- package/dist/chunk-crv3dqzp.js +0 -63
- package/web/assets/index-BkF0-im5.css +0 -1
- package/web/assets/index-D4lgm3Y4.js +0 -383
- package/web/assets/montserrat-latin-ext-wght-normal-BsZE-iaG.woff2 +0 -0
- package/web/assets/montserrat-latin-wght-normal-l_AIctKy.woff2 +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
main,
|
|
4
|
+
restartService
|
|
5
|
+
} from "./chunk-12kaxggg.js";
|
|
6
|
+
import"./chunk-kzbzacz9.js";
|
|
7
|
+
import"./chunk-rtwxjb90.js";
|
|
8
|
+
import"./chunk-sj7rnvh4.js";
|
|
9
|
+
import"./chunk-e4ahjbmz.js";
|
|
10
|
+
import"./chunk-znhn8af7.js";
|
|
11
|
+
import"./chunk-ngf1zatm.js";
|
|
12
|
+
import"./chunk-df32avw6.js";
|
|
13
|
+
export {
|
|
14
|
+
restartService,
|
|
15
|
+
main
|
|
16
|
+
};
|
|
@@ -9,11 +9,14 @@ import {
|
|
|
9
9
|
relayWsBase,
|
|
10
10
|
verifyRelayChallenge,
|
|
11
11
|
verifyRelayChallengeV2
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-7qb9tq54.js";
|
|
13
13
|
import {
|
|
14
14
|
commandHint,
|
|
15
15
|
webRoot
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-rtwxjb90.js";
|
|
17
|
+
import {
|
|
18
|
+
normalizeHostName
|
|
19
|
+
} from "./chunk-znhn8af7.js";
|
|
17
20
|
import"./chunk-df32avw6.js";
|
|
18
21
|
|
|
19
22
|
// apps/server/src/main.ts
|
|
@@ -325,7 +328,7 @@ async function openAccounts(options) {
|
|
|
325
328
|
db.exec("ROLLBACK");
|
|
326
329
|
return { status: "refused", reason: failure };
|
|
327
330
|
}
|
|
328
|
-
const hostName = name === undefined
|
|
331
|
+
const hostName = name === undefined ? "Unknown host" : normalizeHostName(name);
|
|
329
332
|
db.prepare(`INSERT INTO hosts (host_id, user_id, noise_public_key, name, joined_at, last_seen_at)
|
|
330
333
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
331
334
|
ON CONFLICT(host_id) DO UPDATE SET
|
|
@@ -358,6 +361,9 @@ async function openAccounts(options) {
|
|
|
358
361
|
markHostSeen(hostId) {
|
|
359
362
|
db.prepare("UPDATE hosts SET last_seen_at = ? WHERE host_id = ?").run(now(), hostId);
|
|
360
363
|
},
|
|
364
|
+
renameHost(hostId, name) {
|
|
365
|
+
db.prepare("UPDATE hosts SET name = ? WHERE host_id = ?").run(normalizeHostName(name), hostId);
|
|
366
|
+
},
|
|
361
367
|
async close() {
|
|
362
368
|
db.close();
|
|
363
369
|
await release();
|
|
@@ -396,7 +402,7 @@ async function changePassword(options) {
|
|
|
396
402
|
|
|
397
403
|
// apps/server/src/apns.ts
|
|
398
404
|
import { createPrivateKey, sign } from "node:crypto";
|
|
399
|
-
import { connect } from "node:http2";
|
|
405
|
+
import { connect, constants } from "node:http2";
|
|
400
406
|
var AUTHORITIES = {
|
|
401
407
|
production: "https://api.push.apple.com",
|
|
402
408
|
development: "https://api.sandbox.push.apple.com"
|
|
@@ -465,19 +471,25 @@ function openApns(config) {
|
|
|
465
471
|
state.token = { jwt, issuedAt: now };
|
|
466
472
|
return jwt;
|
|
467
473
|
};
|
|
474
|
+
const dropSession = (state, session) => {
|
|
475
|
+
if (state.session === session)
|
|
476
|
+
state.session = undefined;
|
|
477
|
+
if (!session.destroyed)
|
|
478
|
+
session.destroy();
|
|
479
|
+
};
|
|
480
|
+
const retireSession = (state, session) => {
|
|
481
|
+
if (state.session === session)
|
|
482
|
+
state.session = undefined;
|
|
483
|
+
if (!session.closed && !session.destroyed)
|
|
484
|
+
session.close();
|
|
485
|
+
};
|
|
468
486
|
const sessionFor = (environment, state) => {
|
|
469
487
|
const existing = state.session;
|
|
470
488
|
if (existing !== undefined && !existing.destroyed && !existing.closed)
|
|
471
489
|
return existing;
|
|
472
490
|
const session = connect(state.authority);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
state.session = undefined;
|
|
476
|
-
if (!session.destroyed)
|
|
477
|
-
session.destroy();
|
|
478
|
-
};
|
|
479
|
-
session.once("goaway", drop);
|
|
480
|
-
session.once("error", drop);
|
|
491
|
+
session.once("goaway", () => dropSession(state, session));
|
|
492
|
+
session.once("error", () => dropSession(state, session));
|
|
481
493
|
session.once("close", () => {
|
|
482
494
|
if (state.session === session)
|
|
483
495
|
state.session = undefined;
|
|
@@ -560,6 +572,7 @@ function openApns(config) {
|
|
|
560
572
|
resolve({ result: "retry", reason: "session unavailable" });
|
|
561
573
|
return;
|
|
562
574
|
}
|
|
575
|
+
let responded = false;
|
|
563
576
|
let status = 0;
|
|
564
577
|
let responseHeaders = {};
|
|
565
578
|
const chunks = [];
|
|
@@ -574,11 +587,16 @@ function openApns(config) {
|
|
|
574
587
|
};
|
|
575
588
|
const timer = setTimeout(() => finish({ result: "retry", reason: "apns timeout" }), REQUEST_TIMEOUT_MS);
|
|
576
589
|
stream.on("response", (headers) => {
|
|
590
|
+
responded = true;
|
|
577
591
|
status = Number(headers[":status"] ?? 0);
|
|
578
592
|
responseHeaders = headers;
|
|
579
593
|
});
|
|
580
594
|
stream.on("data", (chunk) => chunks.push(chunk));
|
|
581
595
|
stream.on("end", () => {
|
|
596
|
+
if (!responded) {
|
|
597
|
+
finish({ result: "retry", reason: "connection closed" });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
582
600
|
let body2;
|
|
583
601
|
try {
|
|
584
602
|
body2 = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
@@ -587,7 +605,12 @@ function openApns(config) {
|
|
|
587
605
|
}
|
|
588
606
|
finish(classify(request.environment, status, responseHeaders, body2, state));
|
|
589
607
|
});
|
|
590
|
-
stream.on("error", () =>
|
|
608
|
+
stream.on("error", () => {
|
|
609
|
+
const refused = stream.rstCode === constants.NGHTTP2_REFUSED_STREAM;
|
|
610
|
+
finish({ result: "retry", reason: "connection error" });
|
|
611
|
+
if (refused)
|
|
612
|
+
retireSession(state, session);
|
|
613
|
+
});
|
|
591
614
|
stream.on("aborted", () => finish({ result: "retry", reason: "connection aborted" }));
|
|
592
615
|
stream.end(body);
|
|
593
616
|
});
|
|
@@ -1041,6 +1064,11 @@ async function startRelay(options) {
|
|
|
1041
1064
|
handlePush(socket, hostId, answer);
|
|
1042
1065
|
return;
|
|
1043
1066
|
}
|
|
1067
|
+
if (answer.t === "name") {
|
|
1068
|
+
if (controls.get(hostId)?.socket === socket)
|
|
1069
|
+
accounts?.renameHost(hostId, answer.name);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1044
1072
|
if (answer.t === "pairing" || answer.t === "pairing-refused") {
|
|
1045
1073
|
const request = pairingRequests.get(answer.requestId);
|
|
1046
1074
|
if (request === undefined || request.control !== socket)
|
|
@@ -1097,7 +1125,9 @@ async function startRelay(options) {
|
|
|
1097
1125
|
...frame.features === undefined ? {} : { features: frame.features }
|
|
1098
1126
|
});
|
|
1099
1127
|
accounts?.markHostSeen(hostId);
|
|
1100
|
-
|
|
1128
|
+
if (frame.name !== undefined)
|
|
1129
|
+
accounts?.renameHost(hostId, frame.name);
|
|
1130
|
+
socket.send(encodeRelayFrame({ t: "registered", push: pushEnvironments, features: ["name"] }));
|
|
1101
1131
|
});
|
|
1102
1132
|
socket.once("close", () => {
|
|
1103
1133
|
clearTimeout(deadline);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HOST_NAME_MAX_UNITS
|
|
3
|
+
} from "./chunk-znhn8af7.js";
|
|
4
|
+
|
|
1
5
|
// packages/protocol/src/relay.ts
|
|
2
6
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
3
7
|
|
|
@@ -332,14 +336,18 @@ function decodeRelayToHost(text) {
|
|
|
332
336
|
throw new Error("unknown relay frame");
|
|
333
337
|
}
|
|
334
338
|
}
|
|
339
|
+
var isFeatureList = (value) => Array.isArray(value) && value.length <= 16 && value.every((entry) => isString2(entry) && entry.length <= 64);
|
|
335
340
|
function decodeRegistered(value) {
|
|
341
|
+
if (value.features !== undefined && !isFeatureList(value.features))
|
|
342
|
+
throw new Error("malformed registered");
|
|
343
|
+
const declared = value.features === undefined ? {} : { features: value.features };
|
|
336
344
|
if (value.push === undefined)
|
|
337
|
-
return { t: "registered" };
|
|
345
|
+
return { t: "registered", ...declared };
|
|
338
346
|
if (!Array.isArray(value.push) || value.push.length > 8 || !value.push.every(isString2)) {
|
|
339
347
|
throw new Error("malformed registered");
|
|
340
348
|
}
|
|
341
349
|
const push = value.push.filter((entry) => entry === "production" || entry === "development");
|
|
342
|
-
return { t: "registered", push };
|
|
350
|
+
return { t: "registered", push, ...declared };
|
|
343
351
|
}
|
|
344
352
|
function decodePushResult(value) {
|
|
345
353
|
if (!isBoundedString(value.requestId) || !isBoundedString(value.reason, 256) || value.result !== "accepted" && value.result !== "unregistered" && value.result !== "retry" && value.result !== "rejected" && value.result !== "unavailable") {
|
|
@@ -388,6 +396,10 @@ function decodeHostToRelay(text) {
|
|
|
388
396
|
throw new Error("malformed pairing-refused");
|
|
389
397
|
}
|
|
390
398
|
return { t: "pairing-refused", requestId: value.requestId, reason: value.reason };
|
|
399
|
+
case "name":
|
|
400
|
+
if (!isBoundedString(value.name, HOST_NAME_MAX_UNITS))
|
|
401
|
+
throw new Error("malformed name");
|
|
402
|
+
return { t: "name", name: value.name };
|
|
391
403
|
default:
|
|
392
404
|
throw new Error("unknown host frame");
|
|
393
405
|
}
|
|
@@ -425,9 +437,8 @@ function decodeRegister(value) {
|
|
|
425
437
|
throw new Error("malformed register");
|
|
426
438
|
if (value.name !== undefined && !isString2(value.name))
|
|
427
439
|
throw new Error("malformed register");
|
|
428
|
-
if (value.features !== undefined &&
|
|
440
|
+
if (value.features !== undefined && !isFeatureList(value.features))
|
|
429
441
|
throw new Error("malformed register");
|
|
430
|
-
}
|
|
431
442
|
return {
|
|
432
443
|
t: "register",
|
|
433
444
|
hostId: value.hostId,
|
|
@@ -511,6 +522,8 @@ function startRelayLink(options) {
|
|
|
511
522
|
let retryTimer;
|
|
512
523
|
let pendingAccepts = 0;
|
|
513
524
|
let pushEnvironments;
|
|
525
|
+
let reportedName;
|
|
526
|
+
let nameDeclared = false;
|
|
514
527
|
const pushes = new Map;
|
|
515
528
|
function setState(next) {
|
|
516
529
|
const changed = state.status !== next.status || state.status === "rejected" && next.status === "rejected" && state.reason !== next.reason;
|
|
@@ -546,6 +559,8 @@ function startRelayLink(options) {
|
|
|
546
559
|
const socket = new WebSocket2(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
|
|
547
560
|
control = socket;
|
|
548
561
|
pushEnvironments = undefined;
|
|
562
|
+
nameDeclared = false;
|
|
563
|
+
reportedName = undefined;
|
|
549
564
|
watch(socket);
|
|
550
565
|
socket.on("message", (data, isBinary) => onControlMessage(socket, data, isBinary));
|
|
551
566
|
socket.once("close", (code) => onControlClosed(socket, code));
|
|
@@ -572,21 +587,30 @@ function startRelayLink(options) {
|
|
|
572
587
|
case "challenge": {
|
|
573
588
|
const noisePublicKey = toBase64Url(options.noisePublicKey);
|
|
574
589
|
const challenge = { relayOrigin: origin, hostId, nonce: frame.nonce };
|
|
590
|
+
reportedName = options.hostName?.value;
|
|
575
591
|
socket.send(encodeRelayFrame({
|
|
576
592
|
t: "register",
|
|
577
593
|
hostId,
|
|
578
594
|
signature: signRelayChallenge(options.signingKey.secretKey, challenge),
|
|
579
595
|
signatureV2: signRelayChallengeV2(options.signingKey.secretKey, { ...challenge, noisePublicKey }),
|
|
580
596
|
noisePublicKey,
|
|
581
|
-
...options.accountOffer === undefined ? {} : { features: ["pairing"] }
|
|
597
|
+
...options.accountOffer === undefined ? {} : { features: ["pairing"] },
|
|
598
|
+
...reportedName === undefined ? {} : { name: reportedName }
|
|
582
599
|
}));
|
|
583
600
|
break;
|
|
584
601
|
}
|
|
585
|
-
case "registered":
|
|
602
|
+
case "registered": {
|
|
586
603
|
delay = minDelay;
|
|
587
604
|
pushEnvironments = frame.push;
|
|
605
|
+
nameDeclared = frame.features?.includes("name") === true;
|
|
606
|
+
const current = options.hostName?.value;
|
|
607
|
+
if (nameDeclared && current !== undefined && current !== reportedName) {
|
|
608
|
+
reportedName = current;
|
|
609
|
+
socket.send(encodeRelayFrame({ t: "name", name: current }));
|
|
610
|
+
}
|
|
588
611
|
setState({ status: "registered", ...frame.push === undefined ? {} : { push: frame.push } });
|
|
589
612
|
break;
|
|
613
|
+
}
|
|
590
614
|
case "push-result": {
|
|
591
615
|
const request = pushes.get(frame.requestId);
|
|
592
616
|
if (request === undefined || request.socket !== socket)
|
|
@@ -636,6 +660,8 @@ function startRelayLink(options) {
|
|
|
636
660
|
function onControlClosed(socket, code) {
|
|
637
661
|
control = undefined;
|
|
638
662
|
pushEnvironments = undefined;
|
|
663
|
+
nameDeclared = false;
|
|
664
|
+
reportedName = undefined;
|
|
639
665
|
for (const [requestId, request] of pushes) {
|
|
640
666
|
if (request.socket === socket) {
|
|
641
667
|
pushes.delete(requestId);
|
|
@@ -656,6 +682,15 @@ function startRelayLink(options) {
|
|
|
656
682
|
reconnect(delay);
|
|
657
683
|
delay = Math.min(maxDelay, delay * 2);
|
|
658
684
|
}
|
|
685
|
+
const detachName = options.hostName?.subscribe((name) => {
|
|
686
|
+
if (name === undefined || name === reportedName)
|
|
687
|
+
return;
|
|
688
|
+
const socket = control;
|
|
689
|
+
if (socket === undefined || socket.readyState !== WebSocket2.OPEN || !nameDeclared)
|
|
690
|
+
return;
|
|
691
|
+
reportedName = name;
|
|
692
|
+
socket.send(encodeRelayFrame({ t: "name", name }));
|
|
693
|
+
});
|
|
659
694
|
connectControl();
|
|
660
695
|
return {
|
|
661
696
|
hostId,
|
|
@@ -694,6 +729,7 @@ function startRelayLink(options) {
|
|
|
694
729
|
if (retryTimer !== undefined)
|
|
695
730
|
clearTimeout(retryTimer);
|
|
696
731
|
clearInterval(pingTimer);
|
|
732
|
+
detachName?.();
|
|
697
733
|
for (const socket of watched)
|
|
698
734
|
socket.terminate();
|
|
699
735
|
return Promise.resolve();
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HOST_SETTING_KEYS,
|
|
3
|
+
cleanHostName,
|
|
4
|
+
isQueueMode,
|
|
5
|
+
isThinkingLevel,
|
|
6
|
+
normalizeHostName
|
|
7
|
+
} from "./chunk-znhn8af7.js";
|
|
8
|
+
|
|
9
|
+
// apps/host/src/host-value.ts
|
|
10
|
+
function constantValue(value) {
|
|
11
|
+
return {
|
|
12
|
+
value,
|
|
13
|
+
subscribe: () => () => {}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class Published {
|
|
18
|
+
#listeners = new Set;
|
|
19
|
+
#equal;
|
|
20
|
+
#value;
|
|
21
|
+
constructor(value, equal) {
|
|
22
|
+
this.#value = value;
|
|
23
|
+
this.#equal = equal;
|
|
24
|
+
}
|
|
25
|
+
get value() {
|
|
26
|
+
return this.#value;
|
|
27
|
+
}
|
|
28
|
+
subscribe(listener) {
|
|
29
|
+
this.#listeners.add(listener);
|
|
30
|
+
return () => this.#listeners.delete(listener);
|
|
31
|
+
}
|
|
32
|
+
set(next) {
|
|
33
|
+
if (this.#equal(this.#value, next))
|
|
34
|
+
return;
|
|
35
|
+
this.#value = next;
|
|
36
|
+
for (const listener of this.#listeners)
|
|
37
|
+
listener(next);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
var sameList = (a, b, same) => a.length === b.length && a.every((entry, index) => same(entry, b[index]));
|
|
41
|
+
|
|
42
|
+
// apps/host/src/host-settings.ts
|
|
43
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
44
|
+
import { hostname } from "node:os";
|
|
45
|
+
import { dirname } from "node:path";
|
|
46
|
+
function settingsDefaults(settings) {
|
|
47
|
+
return {
|
|
48
|
+
...settings.defaultModel === null ? {} : { model: settings.defaultModel },
|
|
49
|
+
...settings.defaultThinkingLevel === null ? {} : { thinkingLevel: settings.defaultThinkingLevel }
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
var isModelRef = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.provider === "string" && value.provider !== "" && typeof value.modelId === "string" && value.modelId !== "";
|
|
53
|
+
var fileValueError = (key, value, models) => {
|
|
54
|
+
switch (key) {
|
|
55
|
+
case "defaultModel":
|
|
56
|
+
if (!isModelRef(value))
|
|
57
|
+
return "malformed";
|
|
58
|
+
return models.some((each) => each.provider === value.provider && each.modelId === value.modelId) ? undefined : "unavailable";
|
|
59
|
+
case "defaultThinkingLevel":
|
|
60
|
+
return isThinkingLevel(value) ? undefined : "malformed";
|
|
61
|
+
case "steeringMode":
|
|
62
|
+
case "followUpMode":
|
|
63
|
+
return isQueueMode(value) ? undefined : "malformed";
|
|
64
|
+
case "acceptAccountDevices":
|
|
65
|
+
return typeof value === "boolean" ? undefined : "malformed";
|
|
66
|
+
case "hostName":
|
|
67
|
+
return typeof value === "string" ? undefined : "malformed";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var sameModel = (a, b) => a === b || a !== null && b !== null && a.provider === b.provider && a.modelId === b.modelId;
|
|
71
|
+
var sameSettings = (a, b) => a.steeringMode === b.steeringMode && a.followUpMode === b.followUpMode && a.acceptAccountDevices === b.acceptAccountDevices && a.defaultThinkingLevel === b.defaultThinkingLevel && sameModel(a.defaultModel, b.defaultModel) && a.hostName === b.hostName && sameList(a.custom, b.custom, (x, y) => x === y);
|
|
72
|
+
var parse = (path, text) => {
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(text);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error(`Invalid settings file ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
78
|
+
}
|
|
79
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
80
|
+
throw new Error(`Invalid settings file ${path}: expected a JSON object`);
|
|
81
|
+
}
|
|
82
|
+
return parsed;
|
|
83
|
+
};
|
|
84
|
+
async function openHostSettings(options) {
|
|
85
|
+
const read = () => readFile(options.path, "utf8").then((text) => parse(options.path, text), (error) => {
|
|
86
|
+
if (error.code === "ENOENT")
|
|
87
|
+
return {};
|
|
88
|
+
throw new Error(`Cannot read settings file ${options.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
});
|
|
90
|
+
let file = await read();
|
|
91
|
+
const warned = new Set;
|
|
92
|
+
const compute = () => {
|
|
93
|
+
const fileValues = {};
|
|
94
|
+
const custom = [];
|
|
95
|
+
for (const key of HOST_SETTING_KEYS) {
|
|
96
|
+
if (!Object.hasOwn(file, key))
|
|
97
|
+
continue;
|
|
98
|
+
const value = file[key];
|
|
99
|
+
const invalid = fileValueError(key, value, options.models.value);
|
|
100
|
+
if (invalid === "malformed" && !warned.has(key)) {
|
|
101
|
+
warned.add(key);
|
|
102
|
+
console.error(`${options.path}: ignoring invalid value for settings key "${key}": ${JSON.stringify(value)}`);
|
|
103
|
+
}
|
|
104
|
+
if (invalid !== undefined)
|
|
105
|
+
continue;
|
|
106
|
+
fileValues[key] = value;
|
|
107
|
+
custom.push(key);
|
|
108
|
+
}
|
|
109
|
+
const { fallback } = options;
|
|
110
|
+
return {
|
|
111
|
+
defaultModel: fileValues.defaultModel ?? fallback.defaultModel(),
|
|
112
|
+
defaultThinkingLevel: fileValues.defaultThinkingLevel ?? fallback.defaultThinkingLevel(),
|
|
113
|
+
steeringMode: fileValues.steeringMode ?? fallback.steeringMode(),
|
|
114
|
+
followUpMode: fileValues.followUpMode ?? fallback.followUpMode(),
|
|
115
|
+
acceptAccountDevices: fileValues.acceptAccountDevices ?? true,
|
|
116
|
+
hostName: normalizeHostName(fileValues.hostName ?? hostname()),
|
|
117
|
+
custom
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
const published = new Published(compute(), sameSettings);
|
|
121
|
+
options.models.subscribe(() => published.set(compute()));
|
|
122
|
+
let tail = Promise.resolve();
|
|
123
|
+
const write = async (patch) => {
|
|
124
|
+
const model = patch.defaultModel;
|
|
125
|
+
if (model !== undefined && model !== null && !options.models.value.some((each) => each.provider === model.provider && each.modelId === model.modelId)) {
|
|
126
|
+
throw new Error(`Unknown model: ${model.provider}/${model.modelId}`);
|
|
127
|
+
}
|
|
128
|
+
const name = patch.hostName;
|
|
129
|
+
if (name !== undefined && name !== null && cleanHostName(name) === "") {
|
|
130
|
+
throw new Error(`Invalid host name: ${JSON.stringify(name)}`);
|
|
131
|
+
}
|
|
132
|
+
const next = { ...await read() };
|
|
133
|
+
for (const key of HOST_SETTING_KEYS) {
|
|
134
|
+
const value = patch[key];
|
|
135
|
+
if (value === undefined)
|
|
136
|
+
continue;
|
|
137
|
+
if (value === null)
|
|
138
|
+
delete next[key];
|
|
139
|
+
else
|
|
140
|
+
next[key] = key === "hostName" ? cleanHostName(value) : value;
|
|
141
|
+
}
|
|
142
|
+
const tmp = `${options.path}.tmp`;
|
|
143
|
+
await mkdir(dirname(options.path), { recursive: true });
|
|
144
|
+
await writeFile(tmp, `${JSON.stringify(next, null, 2)}
|
|
145
|
+
`, { mode: 384 });
|
|
146
|
+
await rename(tmp, options.path);
|
|
147
|
+
file = next;
|
|
148
|
+
published.set(compute());
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
get value() {
|
|
152
|
+
return published.value;
|
|
153
|
+
},
|
|
154
|
+
subscribe: (listener) => published.subscribe(listener),
|
|
155
|
+
update: (patch) => tail = tail.then(() => write(patch), () => write(patch))
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { constantValue, Published, sameList, settingsDefaults, openHostSettings };
|