pinomad 0.4.0 → 0.6.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 +4 -2
- package/dist/{chunk-837gh8nr.js → chunk-0xh6cy9v.js} +69 -17
- package/dist/{chunk-n73t5xzw.js → chunk-367n9je8.js} +2 -2
- package/dist/{chunk-xhwkx9bd.js → chunk-3dnnaz4j.js} +14 -15
- package/dist/{chunk-z5pmcscx.js → chunk-4btj08we.js} +676 -377
- package/dist/{chunk-7gmkgyaz.js → chunk-7qb9tq54.js} +324 -4
- package/dist/{chunk-d4y99pfy.js → chunk-7s1f4z4g.js} +4 -3
- package/dist/chunk-b1h374jr.js +159 -0
- package/dist/{chunk-fgx2sh7d.js → chunk-c8nxym2z.js} +1 -1
- package/dist/{chunk-akgxwg8f.js → chunk-d5ypmmr7.js} +29 -12
- package/dist/{chunk-374zqx5h.js → chunk-d93kvxvc.js} +18 -2
- package/dist/chunk-ggb1taqy.js +16 -0
- package/dist/chunk-jn7y815x.js +79 -0
- package/dist/{chunk-b3f0ced1.js → chunk-k2s1hfq5.js} +39 -16
- package/dist/chunk-kzbzacz9.js +12 -0
- package/dist/{chunk-myjp910s.js → chunk-n4tjhcb7.js} +25 -21
- package/dist/{chunk-cjfmfyjb.js → chunk-ngf1zatm.js} +7 -6
- package/dist/{chunk-vw5yb5pd.js → chunk-wqvp1fwh.js} +304 -38
- package/dist/{chunk-5785kwej.js → chunk-ywmm63ks.js} +9 -7
- package/dist/chunk-znhn8af7.js +33 -0
- package/dist/pinomad.js +40 -16
- package/package.json +1 -1
- package/web/apple-touch-icon.png +0 -0
- package/web/assets/index-C_aU_ZiV.js +383 -0
- package/web/assets/index-DaPcXZPM.css +1 -0
- package/web/favicon.svg +1 -0
- package/web/index.html +4 -2
- package/dist/chunk-17p6bfvy.js +0 -63
- package/dist/chunk-302y6by9.js +0 -50
- package/dist/chunk-knaw1a9g.js +0 -182
- package/dist/chunk-prhvksh3.js +0 -14
- package/dist/chunk-xrvmnymc.js +0 -33
- package/web/assets/index-8mNNPXPT.js +0 -383
- package/web/assets/index-BFJJgGGo.css +0 -1
|
@@ -1,32 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
normalizeOrigin,
|
|
4
|
-
readServerBinding,
|
|
5
|
-
readServerConfig,
|
|
6
|
-
resolveRelayOrigin
|
|
7
|
-
} from "./chunk-302y6by9.js";
|
|
8
2
|
import {
|
|
9
3
|
serveWebClient
|
|
10
4
|
} from "./chunk-r6w5aqkw.js";
|
|
11
5
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from "./chunk-fgx2sh7d.js";
|
|
19
|
-
import {
|
|
20
|
-
HOST_SETTING_KEYS,
|
|
21
|
-
INVALID_FRAME_CLOSE_CODE,
|
|
22
|
-
PROTOCOL,
|
|
23
|
-
THINKING_LEVELS,
|
|
24
|
-
UNAUTHORIZED_CLOSE_CODE,
|
|
25
|
-
isQueueMode,
|
|
26
|
-
isThinkingLevel,
|
|
27
|
-
organize,
|
|
28
|
-
parseClientFrame
|
|
29
|
-
} from "./chunk-myjp910s.js";
|
|
6
|
+
Published,
|
|
7
|
+
constantValue,
|
|
8
|
+
openHostSettings,
|
|
9
|
+
sameList,
|
|
10
|
+
settingsDefaults
|
|
11
|
+
} from "./chunk-b1h374jr.js";
|
|
30
12
|
import {
|
|
31
13
|
DevicesDoc,
|
|
32
14
|
RevocationsDoc,
|
|
@@ -34,24 +16,45 @@ import {
|
|
|
34
16
|
ensureDevices,
|
|
35
17
|
loadHostKey,
|
|
36
18
|
loadRelayKey,
|
|
37
|
-
|
|
38
|
-
|
|
19
|
+
normalizeOrigin,
|
|
20
|
+
pairingOffers,
|
|
21
|
+
readServerBinding,
|
|
22
|
+
readServerConfig,
|
|
23
|
+
resolveRelayOrigin
|
|
24
|
+
} from "./chunk-0xh6cy9v.js";
|
|
39
25
|
import {
|
|
40
26
|
SECURE_PROLOGUE,
|
|
41
27
|
SecureSession,
|
|
42
28
|
decodeClientHello,
|
|
43
29
|
encodeHostHello,
|
|
30
|
+
relayWsBase,
|
|
44
31
|
respondIK,
|
|
32
|
+
startRelayLink,
|
|
45
33
|
toBase64Url
|
|
46
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-7qb9tq54.js";
|
|
35
|
+
import {
|
|
36
|
+
packagedVersion,
|
|
37
|
+
webRoot
|
|
38
|
+
} from "./chunk-c8nxym2z.js";
|
|
39
|
+
import {
|
|
40
|
+
INVALID_FRAME_CLOSE_CODE,
|
|
41
|
+
PROTOCOL,
|
|
42
|
+
UNAUTHORIZED_CLOSE_CODE,
|
|
43
|
+
organize,
|
|
44
|
+
parseClientFrame
|
|
45
|
+
} from "./chunk-n4tjhcb7.js";
|
|
46
|
+
import {
|
|
47
|
+
THINKING_LEVELS,
|
|
48
|
+
isThinkingLevel
|
|
49
|
+
} from "./chunk-znhn8af7.js";
|
|
47
50
|
import {
|
|
48
51
|
DEFAULT_DATA_DIR
|
|
49
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-ngf1zatm.js";
|
|
50
53
|
import"./chunk-df32avw6.js";
|
|
51
54
|
|
|
52
55
|
// apps/host/src/main.ts
|
|
53
56
|
import { homedir as homedir3 } from "node:os";
|
|
54
|
-
import { join as
|
|
57
|
+
import { join as join9, resolve as resolve4 } from "node:path";
|
|
55
58
|
import { parseArgs } from "node:util";
|
|
56
59
|
import { createModels } from "@earendil-works/pi-ai/models";
|
|
57
60
|
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
|
@@ -185,7 +188,7 @@ function result(questions, resolution) {
|
|
|
185
188
|
var askUserQuestion = defineTool({
|
|
186
189
|
name: "ask_user_question",
|
|
187
190
|
callers: ["model"],
|
|
188
|
-
description: "Ask the user 1–4 multiple-choice questions when their decision is needed to proceed (ambiguous requirements, choosing between approaches, scope). Blocks until answered. The user can always type a custom answer, so never add an 'Other' option. Keep headers
|
|
191
|
+
description: "Ask the user 1–4 multiple-choice questions when their decision is needed to proceed (ambiguous requirements, choosing between approaches, scope). Blocks until answered. The user can always type a custom answer, so never add an 'Other' option. Keep everything short: headers a word or two, option labels a few words. Usually leave option descriptions out; add one only when the label alone is ambiguous, as one line of at most ~60 characters.",
|
|
189
192
|
parameters: Type2.Object({
|
|
190
193
|
questions: Type2.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 })
|
|
191
194
|
}),
|
|
@@ -289,6 +292,7 @@ function createContext(options) {
|
|
|
289
292
|
].join(`
|
|
290
293
|
`);
|
|
291
294
|
}),
|
|
295
|
+
section("file_references", () => "When the user writes @./path in their message, it is a project file the user references; the path is relative to your current working directory. Its contents are not expanded for you — read the file yourself if the task needs them.", { tag: false }),
|
|
292
296
|
section("project_context", async (input, context) => {
|
|
293
297
|
const cwd = input.agent.cwd;
|
|
294
298
|
if (cwd === undefined)
|
|
@@ -525,8 +529,8 @@ var todo = {
|
|
|
525
529
|
|
|
526
530
|
// apps/host/src/host.ts
|
|
527
531
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
528
|
-
import { mkdir as
|
|
529
|
-
import { basename as basename2, join as
|
|
532
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
533
|
+
import { basename as basename2, join as join8, sep as sep5 } from "node:path";
|
|
530
534
|
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
|
|
531
535
|
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
532
536
|
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
@@ -541,7 +545,7 @@ import {
|
|
|
541
545
|
} from "@earendil-works/pi-durable";
|
|
542
546
|
|
|
543
547
|
// apps/host/src/checkout.ts
|
|
544
|
-
import { execFile } from "node:child_process";
|
|
548
|
+
import { execFile, spawn } from "node:child_process";
|
|
545
549
|
import { randomBytes } from "node:crypto";
|
|
546
550
|
import { realpath, rm, stat } from "node:fs/promises";
|
|
547
551
|
import { tmpdir } from "node:os";
|
|
@@ -549,6 +553,42 @@ import { join as join2, relative, sep as sep2 } from "node:path";
|
|
|
549
553
|
import { promisify } from "node:util";
|
|
550
554
|
var run = promisify(execFile);
|
|
551
555
|
var git = (dir, args, env) => run("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
556
|
+
function gitCapped(dir, args, maxBytes, env) {
|
|
557
|
+
return new Promise((resolve, reject) => {
|
|
558
|
+
const child = spawn("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
559
|
+
const chunks = [];
|
|
560
|
+
let size = 0;
|
|
561
|
+
let killed = false;
|
|
562
|
+
let stderr = "";
|
|
563
|
+
child.stdout.on("data", (chunk) => {
|
|
564
|
+
if (killed)
|
|
565
|
+
return;
|
|
566
|
+
chunks.push(chunk);
|
|
567
|
+
size += chunk.length;
|
|
568
|
+
if (size > maxBytes) {
|
|
569
|
+
killed = true;
|
|
570
|
+
child.kill();
|
|
571
|
+
child.stdout.destroy();
|
|
572
|
+
child.stderr.destroy();
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
child.stderr.on("data", (chunk) => {
|
|
576
|
+
if (stderr.length < 8192)
|
|
577
|
+
stderr += chunk.toString("utf8");
|
|
578
|
+
});
|
|
579
|
+
child.on("error", reject);
|
|
580
|
+
child.on("close", (code) => {
|
|
581
|
+
const stdout = Buffer.concat(chunks).toString("utf8");
|
|
582
|
+
if (killed)
|
|
583
|
+
resolve({ stdout, truncated: true });
|
|
584
|
+
else if (code === 0)
|
|
585
|
+
resolve({ stdout, truncated: false });
|
|
586
|
+
else
|
|
587
|
+
reject(Object.assign(new Error(`Command failed: git ${args.join(" ")}
|
|
588
|
+
${stderr}`), { code, stderr }));
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
}
|
|
552
592
|
var worktreeRoot = (dataDir) => join2(dataDir, "worktrees");
|
|
553
593
|
var worktreePath = (dataDir, id) => join2(worktreeRoot(dataDir), String(id));
|
|
554
594
|
var worktreeBranch = (id, suffix) => `pinomad/${id}-${suffix}`;
|
|
@@ -568,6 +608,15 @@ async function gitBase(projectPath) {
|
|
|
568
608
|
return;
|
|
569
609
|
return { repo, subdir: relative(repo, projectPath), base };
|
|
570
610
|
}
|
|
611
|
+
async function branchOf(dir) {
|
|
612
|
+
const ref = await git(dir, ["symbolic-ref", "--quiet", "HEAD"]).then(({ stdout }) => stdout.trim(), () => {
|
|
613
|
+
return;
|
|
614
|
+
});
|
|
615
|
+
const prefix = "refs/heads/";
|
|
616
|
+
if (ref === undefined || !ref.startsWith(prefix))
|
|
617
|
+
return;
|
|
618
|
+
return ref.slice(prefix.length) || undefined;
|
|
619
|
+
}
|
|
571
620
|
var repoLocks = new Map;
|
|
572
621
|
function locked(repo, work) {
|
|
573
622
|
const next = (repoLocks.get(repo) ?? Promise.resolve()).then(work, work);
|
|
@@ -618,12 +667,11 @@ async function changesOf(dir, base, maxBytes = 1024 * 1024) {
|
|
|
618
667
|
try {
|
|
619
668
|
await git(dir, ["read-tree", "HEAD"], env);
|
|
620
669
|
await git(dir, ["add", "-A"], env);
|
|
621
|
-
const { stdout } = await
|
|
622
|
-
if (Buffer.byteLength(stdout) <= maxBytes)
|
|
670
|
+
const { stdout, truncated } = await gitCapped(dir, ["diff", "--cached", "--no-color", "--find-renames", "--src-prefix=a/", "--dst-prefix=b/", base], maxBytes, env);
|
|
671
|
+
if (!truncated && Buffer.byteLength(stdout) <= maxBytes)
|
|
623
672
|
return { base, patch: stdout, truncated: false };
|
|
624
|
-
const cut = stdout.
|
|
625
|
-
const patch = cut.
|
|
626
|
-
`) + 1);
|
|
673
|
+
const cut = Buffer.from(stdout, "utf8").subarray(0, maxBytes);
|
|
674
|
+
const patch = cut.subarray(0, cut.lastIndexOf(10) + 1).toString("utf8");
|
|
627
675
|
return { base, patch, truncated: true };
|
|
628
676
|
} finally {
|
|
629
677
|
await rm(indexFile, { force: true });
|
|
@@ -686,7 +734,23 @@ class DuplicateRequest extends Error {
|
|
|
686
734
|
this.conversationId = conversationId;
|
|
687
735
|
}
|
|
688
736
|
}
|
|
689
|
-
|
|
737
|
+
function requireRole(agents, name) {
|
|
738
|
+
const found = agents.roles.find((role) => role.name === name);
|
|
739
|
+
if (found !== undefined)
|
|
740
|
+
return found;
|
|
741
|
+
const broken = agents.problems.find((problem) => problem.file === `${name}.md`);
|
|
742
|
+
if (broken !== undefined)
|
|
743
|
+
throw new Error(`Agent "${name}" failed to load: ${broken.reason}`);
|
|
744
|
+
const defined = agents.roles.map((role) => role.name).join(", ");
|
|
745
|
+
throw new Error(defined === "" ? `Unknown agent "${name}"` : `Unknown agent "${name}". Available agents: ${defined}`);
|
|
746
|
+
}
|
|
747
|
+
function roleModelRef(role) {
|
|
748
|
+
if (role.model === undefined)
|
|
749
|
+
return;
|
|
750
|
+
const slash = role.model.indexOf("/");
|
|
751
|
+
return { provider: role.model.slice(0, slash), modelId: role.model.slice(slash + 1) };
|
|
752
|
+
}
|
|
753
|
+
async function createConversation(harness, dataDir, input, defaults, models, modelSummaries, agents, context) {
|
|
690
754
|
const index = await harness.snapshot(IndexDoc, context);
|
|
691
755
|
const prior = index?.conversations.find((entry) => entry.requestId === input.requestId);
|
|
692
756
|
if (prior !== undefined) {
|
|
@@ -696,12 +760,24 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
696
760
|
}
|
|
697
761
|
return prior.id;
|
|
698
762
|
}
|
|
699
|
-
const
|
|
763
|
+
const role = input.agent === undefined ? undefined : requireRole(agents, input.agent);
|
|
764
|
+
const roleModel = role === undefined ? undefined : roleModelRef(role);
|
|
765
|
+
const modelRef = input.model ?? roleModel ?? defaults.model;
|
|
700
766
|
const model = modelRef === undefined ? undefined : models.getModel(modelRef.provider, modelRef.modelId);
|
|
701
767
|
if (input.model !== undefined && model === undefined) {
|
|
702
768
|
throw new Error(`Unknown model: ${input.model.provider}/${input.model.modelId}`);
|
|
703
769
|
}
|
|
704
|
-
|
|
770
|
+
if (role !== undefined) {
|
|
771
|
+
const listed = modelSummaries().map((m) => `${m.provider}/${m.modelId}`).join(", ");
|
|
772
|
+
const suffix2 = listed === "" ? "" : `. Available models: ${listed}`;
|
|
773
|
+
if (modelRef === undefined) {
|
|
774
|
+
throw new Error(`Agent "${role.name}" has no model: the role pins none and the host has no default${suffix2}`);
|
|
775
|
+
}
|
|
776
|
+
if (model === undefined || !modelSummaries().some((m) => m.provider === modelRef.provider && m.modelId === modelRef.modelId)) {
|
|
777
|
+
throw new Error(`Model ${modelRef.provider}/${modelRef.modelId} is not available${suffix2}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const thinkingLevel = model === undefined ? input.thinkingLevel ?? role?.thinkingLevel ?? defaults.thinkingLevel : clampThinkingLevel2(model, input.thinkingLevel ?? role?.thinkingLevel ?? defaults.thinkingLevel ?? "off");
|
|
705
781
|
const home = input.home;
|
|
706
782
|
const projectPath = home.kind === "project" ? await normalizePath(home.path).catch(() => home.path) : undefined;
|
|
707
783
|
const repo = home.kind === "project" && input.checkout !== "project" && projectPath !== undefined ? await gitBase(projectPath) : undefined;
|
|
@@ -712,7 +788,8 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
712
788
|
agent: {
|
|
713
789
|
...projectPath === undefined || repo !== undefined ? {} : { cwd: projectPath },
|
|
714
790
|
...modelRef === undefined ? {} : { model: modelRef },
|
|
715
|
-
...thinkingLevel === undefined ? {} : { thinkingLevel }
|
|
791
|
+
...thinkingLevel === undefined ? {} : { thinkingLevel },
|
|
792
|
+
...role === undefined || role.prompt === "" ? {} : { instructions: role.prompt }
|
|
716
793
|
},
|
|
717
794
|
init: async (tx, id) => {
|
|
718
795
|
const doc = await tx.doc(IndexDoc);
|
|
@@ -723,7 +800,13 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
723
800
|
throw new Error(`Unknown project: ${home.path}`);
|
|
724
801
|
}
|
|
725
802
|
const stored = home.kind === "chat" ? { kind: "chat" } : { kind: "project", path: projectPath };
|
|
726
|
-
doc.conversations.push({
|
|
803
|
+
doc.conversations.push({
|
|
804
|
+
id,
|
|
805
|
+
home: stored,
|
|
806
|
+
createdAt: Date.now(),
|
|
807
|
+
requestId: input.requestId,
|
|
808
|
+
...role === undefined ? {} : { role: role.name }
|
|
809
|
+
});
|
|
727
810
|
if (repo !== undefined && suffix !== undefined) {
|
|
728
811
|
const path = worktreePath(dataDir, id);
|
|
729
812
|
if (doc.checkouts === undefined)
|
|
@@ -1164,39 +1247,6 @@ function agentRoleProblem(role) {
|
|
|
1164
1247
|
return;
|
|
1165
1248
|
}
|
|
1166
1249
|
|
|
1167
|
-
// apps/host/src/host-value.ts
|
|
1168
|
-
function constantValue(value) {
|
|
1169
|
-
return {
|
|
1170
|
-
value,
|
|
1171
|
-
subscribe: () => () => {}
|
|
1172
|
-
};
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
class Published {
|
|
1176
|
-
#listeners = new Set;
|
|
1177
|
-
#equal;
|
|
1178
|
-
#value;
|
|
1179
|
-
constructor(value, equal) {
|
|
1180
|
-
this.#value = value;
|
|
1181
|
-
this.#equal = equal;
|
|
1182
|
-
}
|
|
1183
|
-
get value() {
|
|
1184
|
-
return this.#value;
|
|
1185
|
-
}
|
|
1186
|
-
subscribe(listener) {
|
|
1187
|
-
this.#listeners.add(listener);
|
|
1188
|
-
return () => this.#listeners.delete(listener);
|
|
1189
|
-
}
|
|
1190
|
-
set(next) {
|
|
1191
|
-
if (this.#equal(this.#value, next))
|
|
1192
|
-
return;
|
|
1193
|
-
this.#value = next;
|
|
1194
|
-
for (const listener of this.#listeners)
|
|
1195
|
-
listener(next);
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
var sameList = (a, b, same) => a.length === b.length && a.every((entry, index) => same(entry, b[index]));
|
|
1199
|
-
|
|
1200
1250
|
// apps/host/src/agent-roles.ts
|
|
1201
1251
|
var KNOWN_FIELDS = new Set(["description", "model", "thinkingLevel"]);
|
|
1202
1252
|
async function load(dir) {
|
|
@@ -1350,114 +1400,6 @@ async function openAgentRoles(options) {
|
|
|
1350
1400
|
};
|
|
1351
1401
|
}
|
|
1352
1402
|
|
|
1353
|
-
// apps/host/src/host-settings.ts
|
|
1354
|
-
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1355
|
-
import { dirname as dirname2 } from "node:path";
|
|
1356
|
-
function settingsDefaults(settings) {
|
|
1357
|
-
return {
|
|
1358
|
-
...settings.defaultModel === null ? {} : { model: settings.defaultModel },
|
|
1359
|
-
...settings.defaultThinkingLevel === null ? {} : { thinkingLevel: settings.defaultThinkingLevel }
|
|
1360
|
-
};
|
|
1361
|
-
}
|
|
1362
|
-
var isModelRef = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.provider === "string" && value.provider !== "" && typeof value.modelId === "string" && value.modelId !== "";
|
|
1363
|
-
var fileValueError = (key, value, models) => {
|
|
1364
|
-
switch (key) {
|
|
1365
|
-
case "defaultModel":
|
|
1366
|
-
if (!isModelRef(value))
|
|
1367
|
-
return "malformed";
|
|
1368
|
-
return models.some((each) => each.provider === value.provider && each.modelId === value.modelId) ? undefined : "unavailable";
|
|
1369
|
-
case "defaultThinkingLevel":
|
|
1370
|
-
return isThinkingLevel(value) ? undefined : "malformed";
|
|
1371
|
-
case "steeringMode":
|
|
1372
|
-
case "followUpMode":
|
|
1373
|
-
return isQueueMode(value) ? undefined : "malformed";
|
|
1374
|
-
case "acceptAccountDevices":
|
|
1375
|
-
return typeof value === "boolean" ? undefined : "malformed";
|
|
1376
|
-
}
|
|
1377
|
-
};
|
|
1378
|
-
var sameModel = (a, b) => a === b || a !== null && b !== null && a.provider === b.provider && a.modelId === b.modelId;
|
|
1379
|
-
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) && sameList(a.custom, b.custom, (x, y) => x === y);
|
|
1380
|
-
var parse = (path, text) => {
|
|
1381
|
-
let parsed;
|
|
1382
|
-
try {
|
|
1383
|
-
parsed = JSON.parse(text);
|
|
1384
|
-
} catch (error) {
|
|
1385
|
-
throw new Error(`Invalid settings file ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1386
|
-
}
|
|
1387
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1388
|
-
throw new Error(`Invalid settings file ${path}: expected a JSON object`);
|
|
1389
|
-
}
|
|
1390
|
-
return parsed;
|
|
1391
|
-
};
|
|
1392
|
-
async function openHostSettings(options) {
|
|
1393
|
-
let file = await readFile3(options.path, "utf8").then((text) => parse(options.path, text), (error) => {
|
|
1394
|
-
if (error.code === "ENOENT")
|
|
1395
|
-
return {};
|
|
1396
|
-
throw new Error(`Cannot read settings file ${options.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1397
|
-
});
|
|
1398
|
-
const warned = new Set;
|
|
1399
|
-
const compute = () => {
|
|
1400
|
-
const fileValues = {};
|
|
1401
|
-
const custom = [];
|
|
1402
|
-
for (const key of HOST_SETTING_KEYS) {
|
|
1403
|
-
if (!Object.hasOwn(file, key))
|
|
1404
|
-
continue;
|
|
1405
|
-
const value = file[key];
|
|
1406
|
-
const invalid = fileValueError(key, value, options.models.value);
|
|
1407
|
-
if (invalid === "malformed" && !warned.has(key)) {
|
|
1408
|
-
warned.add(key);
|
|
1409
|
-
console.error(`${options.path}: ignoring invalid value for settings key "${key}": ${JSON.stringify(value)}`);
|
|
1410
|
-
}
|
|
1411
|
-
if (invalid !== undefined)
|
|
1412
|
-
continue;
|
|
1413
|
-
fileValues[key] = value;
|
|
1414
|
-
custom.push(key);
|
|
1415
|
-
}
|
|
1416
|
-
const { fallback } = options;
|
|
1417
|
-
return {
|
|
1418
|
-
defaultModel: fileValues.defaultModel ?? fallback.defaultModel(),
|
|
1419
|
-
defaultThinkingLevel: fileValues.defaultThinkingLevel ?? fallback.defaultThinkingLevel(),
|
|
1420
|
-
steeringMode: fileValues.steeringMode ?? fallback.steeringMode(),
|
|
1421
|
-
followUpMode: fileValues.followUpMode ?? fallback.followUpMode(),
|
|
1422
|
-
acceptAccountDevices: fileValues.acceptAccountDevices ?? true,
|
|
1423
|
-
custom
|
|
1424
|
-
};
|
|
1425
|
-
};
|
|
1426
|
-
const published = new Published(compute(), sameSettings);
|
|
1427
|
-
options.models.subscribe(() => published.set(compute()));
|
|
1428
|
-
let tail = Promise.resolve();
|
|
1429
|
-
const write = async (patch) => {
|
|
1430
|
-
const model = patch.defaultModel;
|
|
1431
|
-
if (model !== undefined && model !== null && !options.models.value.some((each) => each.provider === model.provider && each.modelId === model.modelId)) {
|
|
1432
|
-
throw new Error(`Unknown model: ${model.provider}/${model.modelId}`);
|
|
1433
|
-
}
|
|
1434
|
-
const next = { ...file };
|
|
1435
|
-
for (const key of HOST_SETTING_KEYS) {
|
|
1436
|
-
const value = patch[key];
|
|
1437
|
-
if (value === undefined)
|
|
1438
|
-
continue;
|
|
1439
|
-
if (value === null)
|
|
1440
|
-
delete next[key];
|
|
1441
|
-
else
|
|
1442
|
-
next[key] = value;
|
|
1443
|
-
}
|
|
1444
|
-
const tmp = `${options.path}.tmp`;
|
|
1445
|
-
await mkdir3(dirname2(options.path), { recursive: true });
|
|
1446
|
-
await writeFile2(tmp, `${JSON.stringify(next, null, 2)}
|
|
1447
|
-
`, { mode: 384 });
|
|
1448
|
-
await rename2(tmp, options.path);
|
|
1449
|
-
file = next;
|
|
1450
|
-
published.set(compute());
|
|
1451
|
-
};
|
|
1452
|
-
return {
|
|
1453
|
-
get value() {
|
|
1454
|
-
return published.value;
|
|
1455
|
-
},
|
|
1456
|
-
subscribe: (listener) => published.subscribe(listener),
|
|
1457
|
-
update: (patch) => tail = tail.then(() => write(patch), () => write(patch))
|
|
1458
|
-
};
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
1403
|
// apps/host/src/host.ts
|
|
1462
1404
|
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
1463
1405
|
import lockfile from "proper-lockfile";
|
|
@@ -1465,11 +1407,11 @@ import lockfile from "proper-lockfile";
|
|
|
1465
1407
|
// apps/host/src/gateway.ts
|
|
1466
1408
|
import { createServer } from "node:http";
|
|
1467
1409
|
import { homedir as homedir2, networkInterfaces } from "node:os";
|
|
1468
|
-
import { join as
|
|
1410
|
+
import { join as join7, sep as sep4 } from "node:path";
|
|
1469
1411
|
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
1470
1412
|
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1471
1413
|
import {
|
|
1472
|
-
AgentDoc as
|
|
1414
|
+
AgentDoc as AgentDoc4,
|
|
1473
1415
|
LiveDoc
|
|
1474
1416
|
} from "@earendil-works/pi-durable";
|
|
1475
1417
|
import { WebSocket, WebSocketServer } from "ws";
|
|
@@ -1516,6 +1458,273 @@ async function listFolders(path, home) {
|
|
|
1516
1458
|
return { path: dir, home: await realpath3(home).catch(() => home), folders };
|
|
1517
1459
|
}
|
|
1518
1460
|
|
|
1461
|
+
// apps/host/src/files.ts
|
|
1462
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1463
|
+
import { opendir, realpath as realpath4, stat as stat4 } from "node:fs/promises";
|
|
1464
|
+
import { join as join6, resolve as resolve3, sep as sep3 } from "node:path";
|
|
1465
|
+
import { AgentDoc as AgentDoc3 } from "@earendil-works/pi-durable";
|
|
1466
|
+
var CACHE_TTL_MS = 3000;
|
|
1467
|
+
var MAX_CACHED_ROOTS = 64;
|
|
1468
|
+
var MAX_CONCURRENT_SCANS = 4;
|
|
1469
|
+
var SCAN_BUDGET_MS = 5000;
|
|
1470
|
+
var MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
1471
|
+
var MAX_WALK_ENTRIES = 20000;
|
|
1472
|
+
var MAX_RESULTS = 50;
|
|
1473
|
+
var UNUSABLE_PATH = /[\s`\x00-\x1f\x7f-\x9f]/;
|
|
1474
|
+
function createCandidateLister(enumerate, options = {}) {
|
|
1475
|
+
const now = options.now ?? Date.now;
|
|
1476
|
+
const ttlMs = options.ttlMs ?? CACHE_TTL_MS;
|
|
1477
|
+
const maxConcurrent = options.maxConcurrent ?? MAX_CONCURRENT_SCANS;
|
|
1478
|
+
const maxCached = options.maxCached ?? MAX_CACHED_ROOTS;
|
|
1479
|
+
const cache = new Map;
|
|
1480
|
+
const inflight = new Map;
|
|
1481
|
+
let running = 0;
|
|
1482
|
+
const waiting = [];
|
|
1483
|
+
return (root, mode) => {
|
|
1484
|
+
const key = `${mode}${root}`;
|
|
1485
|
+
const hit = cache.get(key);
|
|
1486
|
+
if (hit !== undefined) {
|
|
1487
|
+
if (hit.expiresAt > now())
|
|
1488
|
+
return Promise.resolve(hit.value);
|
|
1489
|
+
cache.delete(key);
|
|
1490
|
+
}
|
|
1491
|
+
const pending = inflight.get(key);
|
|
1492
|
+
if (pending !== undefined)
|
|
1493
|
+
return pending;
|
|
1494
|
+
const scan = (async () => {
|
|
1495
|
+
if (running >= maxConcurrent)
|
|
1496
|
+
await new Promise((release) => waiting.push(release));
|
|
1497
|
+
running += 1;
|
|
1498
|
+
try {
|
|
1499
|
+
return await enumerate(root, mode);
|
|
1500
|
+
} finally {
|
|
1501
|
+
running -= 1;
|
|
1502
|
+
waiting.shift()?.();
|
|
1503
|
+
}
|
|
1504
|
+
})();
|
|
1505
|
+
inflight.set(key, scan);
|
|
1506
|
+
scan.then((value) => {
|
|
1507
|
+
const at = now();
|
|
1508
|
+
for (const [cached, entry] of cache) {
|
|
1509
|
+
if (entry.expiresAt <= at)
|
|
1510
|
+
cache.delete(cached);
|
|
1511
|
+
}
|
|
1512
|
+
while (cache.size >= maxCached)
|
|
1513
|
+
cache.delete(cache.keys().next().value);
|
|
1514
|
+
cache.set(key, { value, expiresAt: at + ttlMs });
|
|
1515
|
+
}, () => {});
|
|
1516
|
+
return scan.finally(() => inflight.delete(key));
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
function gitLines(cwd, args, limits) {
|
|
1520
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
1521
|
+
const child = spawn2("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
1522
|
+
const chunks = [];
|
|
1523
|
+
let bytes = 0;
|
|
1524
|
+
let truncated = false;
|
|
1525
|
+
let settled = false;
|
|
1526
|
+
const kill = (markTruncated) => {
|
|
1527
|
+
truncated ||= markTruncated;
|
|
1528
|
+
child.kill("SIGKILL");
|
|
1529
|
+
};
|
|
1530
|
+
const timer = setTimeout(() => kill(true), Math.max(0, limits.deadline - limits.now()));
|
|
1531
|
+
const finish = (error) => {
|
|
1532
|
+
if (settled)
|
|
1533
|
+
return;
|
|
1534
|
+
settled = true;
|
|
1535
|
+
clearTimeout(timer);
|
|
1536
|
+
if (error !== undefined) {
|
|
1537
|
+
rejectPromise(error);
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
const output = Buffer.concat(chunks).toString("utf8");
|
|
1541
|
+
const parts = output.split("\x00");
|
|
1542
|
+
if (truncated)
|
|
1543
|
+
parts.pop();
|
|
1544
|
+
resolvePromise({ lines: parts.filter((part) => part !== ""), truncated });
|
|
1545
|
+
};
|
|
1546
|
+
child.stdout.on("data", (chunk) => {
|
|
1547
|
+
bytes += chunk.length;
|
|
1548
|
+
if (bytes > limits.maxBytes) {
|
|
1549
|
+
kill(true);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
chunks.push(chunk);
|
|
1553
|
+
if (limits.now() > limits.deadline)
|
|
1554
|
+
kill(true);
|
|
1555
|
+
});
|
|
1556
|
+
child.on("error", (error) => finish(error));
|
|
1557
|
+
child.stderr.resume();
|
|
1558
|
+
child.on("close", (code) => {
|
|
1559
|
+
if (code === 0 || truncated) {
|
|
1560
|
+
finish(undefined);
|
|
1561
|
+
} else {
|
|
1562
|
+
finish(new Error(`git ${args.join(" ")} exited with code ${code}`));
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
async function inWorkTree(root) {
|
|
1568
|
+
return await new Promise((resolvePromise) => {
|
|
1569
|
+
const child = spawn2("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
1570
|
+
let out = "";
|
|
1571
|
+
child.stdout.on("data", (chunk) => out += chunk.toString("utf8"));
|
|
1572
|
+
child.on("error", () => resolvePromise(false));
|
|
1573
|
+
child.on("close", (code) => resolvePromise(code === 0 && out.trim() === "true"));
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
async function hasHead(root) {
|
|
1577
|
+
return await new Promise((resolvePromise) => {
|
|
1578
|
+
const child = spawn2("git", ["-C", root, "rev-parse", "--verify", "-q", "HEAD"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
1579
|
+
child.on("error", () => resolvePromise(false));
|
|
1580
|
+
child.on("close", (code) => resolvePromise(code === 0));
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
var STAGED_RECORD = /^([0-7]{6}) [0-9a-f]{40,64} [0-7]\t/;
|
|
1584
|
+
async function gitWorkingFiles(root, limits) {
|
|
1585
|
+
const listing = await gitLines(root, ["ls-files", "-z", "-s", "--cached", "--others", "--exclude-standard"], limits);
|
|
1586
|
+
const deleted = await gitLines(root, ["ls-files", "-z", "--deleted"], limits);
|
|
1587
|
+
const gone = new Set(deleted.lines);
|
|
1588
|
+
const files = new Set;
|
|
1589
|
+
for (const line of listing.lines) {
|
|
1590
|
+
const staged = STAGED_RECORD.exec(line);
|
|
1591
|
+
const path = staged === null ? line : line.slice(staged[0].length);
|
|
1592
|
+
if (staged !== null && staged[1] === "160000")
|
|
1593
|
+
continue;
|
|
1594
|
+
if (path === "" || path.endsWith("/") || gone.has(path))
|
|
1595
|
+
continue;
|
|
1596
|
+
files.add(path);
|
|
1597
|
+
}
|
|
1598
|
+
return { files: [...files], truncated: listing.truncated || deleted.truncated };
|
|
1599
|
+
}
|
|
1600
|
+
async function gitHeadFiles(root, limits) {
|
|
1601
|
+
const listing = await gitLines(root, ["ls-tree", "-r", "-z", "HEAD"], limits);
|
|
1602
|
+
const files = [];
|
|
1603
|
+
for (const line of listing.lines) {
|
|
1604
|
+
const tab = line.indexOf("\t");
|
|
1605
|
+
if (tab === -1)
|
|
1606
|
+
continue;
|
|
1607
|
+
const kind = line.slice(0, tab).split(" ")[1];
|
|
1608
|
+
if (kind === "blob")
|
|
1609
|
+
files.push(line.slice(tab + 1));
|
|
1610
|
+
}
|
|
1611
|
+
return { files, truncated: listing.truncated };
|
|
1612
|
+
}
|
|
1613
|
+
async function walkFiles(root, limits) {
|
|
1614
|
+
const files = [];
|
|
1615
|
+
let entries = 0;
|
|
1616
|
+
let truncated = false;
|
|
1617
|
+
const dirs = [{ dir: root, prefix: "" }];
|
|
1618
|
+
while (dirs.length > 0) {
|
|
1619
|
+
if (limits.now() > limits.deadline) {
|
|
1620
|
+
truncated = true;
|
|
1621
|
+
break;
|
|
1622
|
+
}
|
|
1623
|
+
const { dir, prefix } = dirs.pop();
|
|
1624
|
+
let handle;
|
|
1625
|
+
try {
|
|
1626
|
+
handle = await opendir(dir);
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
if (prefix === "")
|
|
1629
|
+
throw error;
|
|
1630
|
+
truncated = true;
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
for await (const entry of handle) {
|
|
1634
|
+
if (entries++ >= limits.maxEntries || limits.now() > limits.deadline) {
|
|
1635
|
+
truncated = true;
|
|
1636
|
+
break;
|
|
1637
|
+
}
|
|
1638
|
+
if (entry.isDirectory()) {
|
|
1639
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
1640
|
+
continue;
|
|
1641
|
+
dirs.push({ dir: join6(dir, entry.name), prefix: `${prefix}${entry.name}/` });
|
|
1642
|
+
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
1643
|
+
files.push(`${prefix}${entry.name}`);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
if (truncated)
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
return { files, truncated };
|
|
1650
|
+
}
|
|
1651
|
+
async function enumerateRoot(root, mode, limits = {}) {
|
|
1652
|
+
const now = limits.now ?? Date.now;
|
|
1653
|
+
const bounded = {
|
|
1654
|
+
now,
|
|
1655
|
+
deadline: now() + (limits.budgetMs ?? SCAN_BUDGET_MS),
|
|
1656
|
+
maxBytes: limits.maxBytes ?? MAX_OUTPUT_BYTES,
|
|
1657
|
+
maxEntries: limits.maxEntries ?? MAX_WALK_ENTRIES
|
|
1658
|
+
};
|
|
1659
|
+
if (!await inWorkTree(root)) {
|
|
1660
|
+
return walkFiles(root, bounded);
|
|
1661
|
+
}
|
|
1662
|
+
if (mode === "head" && !await hasHead(root)) {
|
|
1663
|
+
return walkFiles(root, bounded);
|
|
1664
|
+
}
|
|
1665
|
+
return mode === "head" ? gitHeadFiles(root, bounded) : gitWorkingFiles(root, bounded);
|
|
1666
|
+
}
|
|
1667
|
+
function matchFiles(candidates, query, max = MAX_RESULTS) {
|
|
1668
|
+
const usable = candidates.filter((path) => !UNUSABLE_PATH.test(path));
|
|
1669
|
+
if (query === "") {
|
|
1670
|
+
const sorted = [...usable].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
1671
|
+
return { files: sorted.slice(0, max), truncated: sorted.length > max };
|
|
1672
|
+
}
|
|
1673
|
+
const needle = query.toLowerCase();
|
|
1674
|
+
const tiers = [[], [], []];
|
|
1675
|
+
for (const path of usable) {
|
|
1676
|
+
const base = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
|
|
1677
|
+
const lower = path.toLowerCase();
|
|
1678
|
+
if (base.startsWith(needle))
|
|
1679
|
+
tiers[0].push(path);
|
|
1680
|
+
else if (base.includes(needle))
|
|
1681
|
+
tiers[1].push(path);
|
|
1682
|
+
else if (lower.includes(needle))
|
|
1683
|
+
tiers[2].push(path);
|
|
1684
|
+
}
|
|
1685
|
+
const shorter = (a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0);
|
|
1686
|
+
const matched = [...tiers[0].sort(shorter), ...tiers[1].sort(shorter), ...tiers[2].sort(shorter)];
|
|
1687
|
+
return { files: matched.slice(0, max), truncated: matched.length > max };
|
|
1688
|
+
}
|
|
1689
|
+
async function listFiles(harness, dataDir, enumerate, scope, query, context) {
|
|
1690
|
+
let root;
|
|
1691
|
+
let mode = "live";
|
|
1692
|
+
if (scope.kind === "conversation") {
|
|
1693
|
+
const agent = await harness.snapshot(AgentDoc3, scope.conversationId, context);
|
|
1694
|
+
if (agent === undefined)
|
|
1695
|
+
throw new Error(`Conversation ${scope.conversationId} does not exist`);
|
|
1696
|
+
const cwd = agent.cwd;
|
|
1697
|
+
if (cwd === undefined)
|
|
1698
|
+
throw new Error(`Conversation ${scope.conversationId} has no checkout`);
|
|
1699
|
+
if (cwd.startsWith(join6(dataDir, "chats") + sep3))
|
|
1700
|
+
return { files: [], truncated: false };
|
|
1701
|
+
const info = await stat4(cwd).catch(() => {
|
|
1702
|
+
return;
|
|
1703
|
+
});
|
|
1704
|
+
if (info === undefined || !info.isDirectory()) {
|
|
1705
|
+
throw new Error(`Conversation ${scope.conversationId}'s checkout is unavailable: ${cwd}`);
|
|
1706
|
+
}
|
|
1707
|
+
root = cwd;
|
|
1708
|
+
} else {
|
|
1709
|
+
const normalized = await realpath4(resolve3(scope.path)).catch(() => resolve3(scope.path));
|
|
1710
|
+
const index = await harness.snapshot(IndexDoc, context);
|
|
1711
|
+
if (!index?.projects.some((project) => project.path === normalized)) {
|
|
1712
|
+
throw new Error(`Unknown project: ${scope.path}`);
|
|
1713
|
+
}
|
|
1714
|
+
const info = await stat4(normalized).catch(() => {
|
|
1715
|
+
return;
|
|
1716
|
+
});
|
|
1717
|
+
if (info === undefined || !info.isDirectory()) {
|
|
1718
|
+
throw new Error(`Not a directory: ${normalized}`);
|
|
1719
|
+
}
|
|
1720
|
+
root = normalized;
|
|
1721
|
+
mode = scope.checkout === "worktree" ? "head" : "live";
|
|
1722
|
+
}
|
|
1723
|
+
const candidates = await enumerate(root, mode);
|
|
1724
|
+
const matched = matchFiles(candidates.files, query);
|
|
1725
|
+
return { files: matched.files, truncated: matched.truncated || candidates.truncated };
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1519
1728
|
// packages/protocol/src/images.ts
|
|
1520
1729
|
var IMAGE_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
1521
1730
|
var MAX_IMAGES_PER_MESSAGE = 4;
|
|
@@ -1660,6 +1869,7 @@ ${answer}`}`,
|
|
|
1660
1869
|
}
|
|
1661
1870
|
|
|
1662
1871
|
// apps/host/src/push.ts
|
|
1872
|
+
import { createHash } from "node:crypto";
|
|
1663
1873
|
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
|
|
1664
1874
|
import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
|
|
1665
1875
|
|
|
@@ -1721,26 +1931,38 @@ function diffRunNotices(previous, organized) {
|
|
|
1721
1931
|
var context2 = BACKGROUND_CONTEXT2;
|
|
1722
1932
|
var PushDoc = defineDoc6({
|
|
1723
1933
|
kind: "pinomad.push",
|
|
1724
|
-
version:
|
|
1934
|
+
version: 2,
|
|
1725
1935
|
scope: "session",
|
|
1726
|
-
initial: () => ({ registrations: [] })
|
|
1936
|
+
initial: () => ({ registrations: [] }),
|
|
1937
|
+
migrate: () => ({ registrations: [] })
|
|
1727
1938
|
});
|
|
1728
|
-
var
|
|
1729
|
-
var
|
|
1939
|
+
var APNS_TOKEN_PATTERN = /^(?:[0-9a-f]{2})+$/;
|
|
1940
|
+
var APNS_TOKEN_MAX_CHARS = 256;
|
|
1941
|
+
var isApnsEnvironment = (value) => value === "production" || value === "development";
|
|
1730
1942
|
function pushTokenProblem(token) {
|
|
1731
1943
|
if (token === null)
|
|
1732
1944
|
return;
|
|
1733
|
-
if (token
|
|
1734
|
-
return
|
|
1945
|
+
if (typeof token === "string") {
|
|
1946
|
+
return "Push notifications no longer go through Expo — register the device's APNs token instead";
|
|
1947
|
+
}
|
|
1948
|
+
if (!isApnsEnvironment(token.environment)) {
|
|
1949
|
+
return 'environment must be "production" or "development"';
|
|
1735
1950
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1951
|
+
const apns = token.apns.toLowerCase();
|
|
1952
|
+
if (apns.length > APNS_TOKEN_MAX_CHARS) {
|
|
1953
|
+
return `APNs token is too long (${APNS_TOKEN_MAX_CHARS} characters at most)`;
|
|
1954
|
+
}
|
|
1955
|
+
if (!APNS_TOKEN_PATTERN.test(apns))
|
|
1956
|
+
return "Not an APNs device token (expected hex)";
|
|
1738
1957
|
return;
|
|
1739
1958
|
}
|
|
1740
|
-
async function setPushToken(harness, publicKey, token, context3) {
|
|
1959
|
+
async function setPushToken(harness, publicKey, token, context3, joined) {
|
|
1741
1960
|
const problem = pushTokenProblem(token);
|
|
1742
1961
|
if (problem !== undefined)
|
|
1743
1962
|
throw new Error(problem);
|
|
1963
|
+
if (token !== null && typeof token !== "string" && !await joined()) {
|
|
1964
|
+
throw new Error("Push notifications need the host joined to an account (`pinomad join`)");
|
|
1965
|
+
}
|
|
1744
1966
|
await harness.commit(async (tx) => {
|
|
1745
1967
|
const devices = await tx.doc(DevicesDoc);
|
|
1746
1968
|
if (!devices.devices.some((entry) => entry.publicKey === publicKey)) {
|
|
@@ -1748,8 +1970,14 @@ async function setPushToken(harness, publicKey, token, context3) {
|
|
|
1748
1970
|
}
|
|
1749
1971
|
const push = await tx.doc(PushDoc);
|
|
1750
1972
|
push.registrations = push.registrations.filter((entry) => entry.publicKey !== publicKey);
|
|
1751
|
-
if (token !== null)
|
|
1752
|
-
push.registrations.push({
|
|
1973
|
+
if (token !== null && typeof token !== "string") {
|
|
1974
|
+
push.registrations.push({
|
|
1975
|
+
publicKey,
|
|
1976
|
+
token: token.apns.toLowerCase(),
|
|
1977
|
+
environment: token.environment,
|
|
1978
|
+
updatedAt: Date.now()
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1753
1981
|
}, context3);
|
|
1754
1982
|
}
|
|
1755
1983
|
async function revokeDevice(harness, publicKey, context3) {
|
|
@@ -1763,38 +1991,43 @@ async function revokeDevice(harness, publicKey, context3) {
|
|
|
1763
1991
|
revocations.revoked.push(publicKey);
|
|
1764
1992
|
}, context3);
|
|
1765
1993
|
}
|
|
1766
|
-
async function
|
|
1994
|
+
async function dropStaleRegistration(harness, token, environment, unregisteredAt, context3) {
|
|
1767
1995
|
await harness.commit(async (tx) => {
|
|
1768
1996
|
const push = await tx.doc(PushDoc);
|
|
1769
|
-
push.registrations = push.registrations.filter((entry) => entry.token !== token);
|
|
1997
|
+
push.registrations = push.registrations.filter((entry) => entry.token !== token || entry.environment !== environment || entry.updatedAt >= unregisteredAt);
|
|
1770
1998
|
}, context3);
|
|
1771
1999
|
}
|
|
1772
|
-
var EXPO_SEND_URL = "https://exp.host/--/api/v2/push/send";
|
|
1773
|
-
var EXPO_RECEIPTS_URL = "https://exp.host/--/api/v2/push/getReceipts";
|
|
1774
2000
|
var FALLBACK_TITLE = "PiNomad conversation";
|
|
1775
2001
|
var TITLE_WAIT_MS = 15000;
|
|
1776
|
-
var
|
|
1777
|
-
var REQUEST_TIMEOUT_MS = 1e4;
|
|
2002
|
+
var TITLE_MAX_CHARS = 160;
|
|
1778
2003
|
var MAX_ATTEMPTS = 4;
|
|
1779
2004
|
var RETRY_BASE_MS = 5000;
|
|
1780
|
-
var
|
|
2005
|
+
var MAX_INFLIGHT = 8;
|
|
1781
2006
|
var MAX_PENDING = 128;
|
|
2007
|
+
var UNAVAILABLE_TRANSPORT = {
|
|
2008
|
+
send: () => Promise.resolve({ result: "unavailable", reason: "no server connection" })
|
|
2009
|
+
};
|
|
1782
2010
|
var realTimers = {
|
|
1783
2011
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
1784
2012
|
clearTimeout: (handle) => clearTimeout(handle)
|
|
1785
2013
|
};
|
|
1786
2014
|
var tokenHint = (token) => `…${token.slice(-6)}`;
|
|
2015
|
+
var collapseId = (hostKey, id) => createHash("sha256").update(`${hostKey}:${id}`).digest("hex").slice(0, 32);
|
|
1787
2016
|
async function attachPushSender(options) {
|
|
1788
2017
|
const { harness, conversations } = options;
|
|
1789
|
-
const
|
|
2018
|
+
const transport = options.transport ?? UNAVAILABLE_TRANSPORT;
|
|
1790
2019
|
const timers = options.timers ?? realTimers;
|
|
2020
|
+
const maxPending = options.limits?.maxPending ?? MAX_PENDING;
|
|
2021
|
+
const maxInflight = options.limits?.maxInflight ?? MAX_INFLIGHT;
|
|
1791
2022
|
const indexWatch = await harness.watchDoc(IndexDoc, context2);
|
|
1792
2023
|
let index = indexWatch?.value ?? undefined;
|
|
1793
2024
|
let baseline;
|
|
1794
2025
|
let disposed = false;
|
|
1795
2026
|
const pending = new Set;
|
|
1796
2027
|
const titleWaiters = new Map;
|
|
1797
|
-
|
|
2028
|
+
let inflight = 0;
|
|
2029
|
+
const readyQueue = [];
|
|
2030
|
+
let unavailableLogged = false;
|
|
1798
2031
|
const generation = new Map;
|
|
1799
2032
|
const update = () => {
|
|
1800
2033
|
if (index === undefined)
|
|
@@ -1816,32 +2049,9 @@ async function attachPushSender(options) {
|
|
|
1816
2049
|
const fresh = (item) => baseline?.has(item.notice.id) === true && generation.get(item.notice.id) === item.generation;
|
|
1817
2050
|
const registrations = async () => {
|
|
1818
2051
|
const doc = await harness.snapshot(PushDoc, context2);
|
|
1819
|
-
return
|
|
1820
|
-
};
|
|
1821
|
-
const post = async (url, payload) => {
|
|
1822
|
-
const response = await fetcher(url, {
|
|
1823
|
-
method: "POST",
|
|
1824
|
-
headers: { accept: "application/json", "content-type": "application/json" },
|
|
1825
|
-
body: JSON.stringify(payload),
|
|
1826
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1827
|
-
});
|
|
1828
|
-
return { status: response.status, body: await response.json() };
|
|
1829
|
-
};
|
|
1830
|
-
const ticketOf = (body) => {
|
|
1831
|
-
const data = body?.data;
|
|
1832
|
-
const ticket = Array.isArray(data) ? data[0] : data;
|
|
1833
|
-
return ticket !== null && typeof ticket === "object" ? ticket : undefined;
|
|
2052
|
+
return doc?.registrations ?? [];
|
|
1834
2053
|
};
|
|
1835
|
-
const
|
|
1836
|
-
to: item.token,
|
|
1837
|
-
title,
|
|
1838
|
-
body: item.notice.body,
|
|
1839
|
-
sound: "default",
|
|
1840
|
-
ttl: TTL_SECONDS,
|
|
1841
|
-
data: { hostKey: options.hostKey, conversationId: item.notice.id },
|
|
1842
|
-
threadId: `${options.hostKey}:${item.notice.id}`,
|
|
1843
|
-
collapseId: `${options.hostKey}:${item.notice.id}`
|
|
1844
|
-
});
|
|
2054
|
+
const findRegistration = (entries, item) => entries.find((entry) => entry.token === item.token && entry.environment === item.environment);
|
|
1845
2055
|
const namedTitle = async (id) => {
|
|
1846
2056
|
const doc = await harness.snapshot(TitleDoc, id, context2);
|
|
1847
2057
|
return typeof doc?.title === "string" && doc.title !== "" ? doc.title : undefined;
|
|
@@ -1852,7 +2062,7 @@ async function attachPushSender(options) {
|
|
|
1852
2062
|
return named;
|
|
1853
2063
|
if (notice.body !== "Finished" || disposed)
|
|
1854
2064
|
return FALLBACK_TITLE;
|
|
1855
|
-
return await new Promise((
|
|
2065
|
+
return await new Promise((resolve4) => {
|
|
1856
2066
|
const waiters = titleWaiters.get(notice.id) ?? new Set;
|
|
1857
2067
|
titleWaiters.set(notice.id, waiters);
|
|
1858
2068
|
const settle = (title) => {
|
|
@@ -1860,7 +2070,7 @@ async function attachPushSender(options) {
|
|
|
1860
2070
|
if (waiters.size === 0)
|
|
1861
2071
|
titleWaiters.delete(notice.id);
|
|
1862
2072
|
timers.clearTimeout(timer);
|
|
1863
|
-
|
|
2073
|
+
resolve4(title);
|
|
1864
2074
|
};
|
|
1865
2075
|
const timer = timers.setTimeout(() => settle(FALLBACK_TITLE), TITLE_WAIT_MS);
|
|
1866
2076
|
waiters.add(settle);
|
|
@@ -1874,78 +2084,96 @@ async function attachPushSender(options) {
|
|
|
1874
2084
|
pending.delete(item);
|
|
1875
2085
|
if (item.timer !== undefined)
|
|
1876
2086
|
timers.clearTimeout(item.timer);
|
|
2087
|
+
const queuedAt = readyQueue.indexOf(item);
|
|
2088
|
+
if (queuedAt >= 0)
|
|
2089
|
+
readyQueue.splice(queuedAt, 1);
|
|
2090
|
+
};
|
|
2091
|
+
const pump = () => {
|
|
2092
|
+
while (inflight < maxInflight) {
|
|
2093
|
+
const next = readyQueue.shift();
|
|
2094
|
+
if (next === undefined)
|
|
2095
|
+
return;
|
|
2096
|
+
next.queued = false;
|
|
2097
|
+
attempt(next);
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
const scheduleRetry = (item, retryAfterMs) => {
|
|
2101
|
+
if (item.attempts + 1 >= MAX_ATTEMPTS) {
|
|
2102
|
+
console.error(`push: send to ${tokenHint(item.token)} gave up`);
|
|
2103
|
+
drop(item);
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
const delayMs = retryAfterMs ?? RETRY_BASE_MS * 2 ** item.attempts;
|
|
2107
|
+
item.attempts += 1;
|
|
2108
|
+
item.timer = timers.setTimeout(() => void attempt(item), delayMs);
|
|
1877
2109
|
};
|
|
1878
2110
|
const attempt = async (item) => {
|
|
1879
2111
|
if (disposed || !pending.has(item))
|
|
1880
2112
|
return;
|
|
1881
2113
|
try {
|
|
1882
|
-
if (
|
|
2114
|
+
if (findRegistration(await registrations(), item) === undefined || !fresh(item))
|
|
1883
2115
|
return drop(item);
|
|
1884
2116
|
if (item.title === undefined) {
|
|
1885
2117
|
item.title = await titleFor(item.notice);
|
|
1886
|
-
if (disposed || !pending.has(item) ||
|
|
2118
|
+
if (disposed || !pending.has(item) || findRegistration(await registrations(), item) === undefined || !fresh(item)) {
|
|
1887
2119
|
return drop(item);
|
|
2120
|
+
}
|
|
1888
2121
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
await dropTokenIfCurrent(harness, item.token, context2);
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
|
-
if (status >= 200 && status < 300 && ticket?.status === "ok") {
|
|
1898
|
-
drop(item);
|
|
1899
|
-
if (typeof ticket.id === "string")
|
|
1900
|
-
scheduleReceiptCheck(item.token, ticket.id);
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
if ((status === 429 || status >= 500) && item.attempts + 1 < MAX_ATTEMPTS) {
|
|
1904
|
-
item.attempts += 1;
|
|
1905
|
-
item.timer = timers.setTimeout(() => void attempt(item), RETRY_BASE_MS * 2 ** (item.attempts - 1));
|
|
2122
|
+
if (inflight >= maxInflight) {
|
|
2123
|
+
if (!item.queued) {
|
|
2124
|
+
item.queued = true;
|
|
2125
|
+
readyQueue.push(item);
|
|
2126
|
+
}
|
|
1906
2127
|
return;
|
|
1907
2128
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2129
|
+
inflight += 1;
|
|
2130
|
+
let verdict;
|
|
2131
|
+
try {
|
|
2132
|
+
verdict = await transport.send({
|
|
2133
|
+
token: item.token,
|
|
2134
|
+
environment: item.environment,
|
|
2135
|
+
notification: {
|
|
2136
|
+
title: item.title.slice(0, TITLE_MAX_CHARS),
|
|
2137
|
+
body: item.notice.body,
|
|
2138
|
+
collapseId: collapseId(options.hostKey, item.notice.id),
|
|
2139
|
+
hostKey: options.hostKey,
|
|
2140
|
+
conversationId: item.notice.id
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
} catch {
|
|
2144
|
+
verdict = { result: "retry", reason: "transport error" };
|
|
2145
|
+
} finally {
|
|
2146
|
+
inflight -= 1;
|
|
2147
|
+
pump();
|
|
1915
2148
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
if (receipt.status === "error" && receipt.details?.error === "DeviceNotRegistered") {
|
|
1941
|
-
console.error(`push: token ${tokenHint(token)} is not registered (receipt)`);
|
|
1942
|
-
await dropTokenIfCurrent(harness, token, context2);
|
|
1943
|
-
return;
|
|
2149
|
+
if (disposed || !pending.has(item))
|
|
2150
|
+
return drop(item);
|
|
2151
|
+
switch (verdict.result) {
|
|
2152
|
+
case "accepted":
|
|
2153
|
+
drop(item);
|
|
2154
|
+
return;
|
|
2155
|
+
case "unregistered":
|
|
2156
|
+
drop(item);
|
|
2157
|
+
await dropStaleRegistration(harness, item.token, item.environment, verdict.unregisteredAt, context2);
|
|
2158
|
+
return;
|
|
2159
|
+
case "retry":
|
|
2160
|
+
scheduleRetry(item, verdict.retryAfterMs);
|
|
2161
|
+
return;
|
|
2162
|
+
case "rejected":
|
|
2163
|
+
console.error(`push: send to ${tokenHint(item.token)} rejected: ${verdict.reason}`);
|
|
2164
|
+
drop(item);
|
|
2165
|
+
return;
|
|
2166
|
+
case "unavailable":
|
|
2167
|
+
if (!unavailableLogged) {
|
|
2168
|
+
unavailableLogged = true;
|
|
2169
|
+
console.error(`push: ${verdict.reason} — notifications are not being delivered`);
|
|
2170
|
+
}
|
|
2171
|
+
drop(item);
|
|
2172
|
+
return;
|
|
1944
2173
|
}
|
|
1945
|
-
if (receipt.status === "error")
|
|
1946
|
-
console.error(`push: receipt ${ticketId} reports ${receipt.details?.error ?? "error"}`);
|
|
1947
2174
|
} catch (error) {
|
|
1948
|
-
console.error(`push:
|
|
2175
|
+
console.error(`push: send to ${tokenHint(item.token)} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2176
|
+
scheduleRetry(item);
|
|
1949
2177
|
}
|
|
1950
2178
|
};
|
|
1951
2179
|
const enqueue = async (notice) => {
|
|
@@ -1953,15 +2181,20 @@ async function attachPushSender(options) {
|
|
|
1953
2181
|
return;
|
|
1954
2182
|
const itemGeneration = generation.get(notice.id) ?? 0;
|
|
1955
2183
|
try {
|
|
1956
|
-
const
|
|
1957
|
-
|
|
2184
|
+
const entries = await registrations();
|
|
2185
|
+
const seen = new Set;
|
|
2186
|
+
for (const entry of entries) {
|
|
1958
2187
|
if (disposed)
|
|
1959
2188
|
return;
|
|
1960
|
-
|
|
2189
|
+
const identity = `${entry.environment}:${entry.token}`;
|
|
2190
|
+
if (seen.has(identity))
|
|
2191
|
+
continue;
|
|
2192
|
+
seen.add(identity);
|
|
2193
|
+
if (pending.size >= maxPending) {
|
|
1961
2194
|
const [oldest] = pending;
|
|
1962
2195
|
drop(oldest);
|
|
1963
2196
|
}
|
|
1964
|
-
const item = { token, notice, generation: itemGeneration, attempts: 0 };
|
|
2197
|
+
const item = { token: entry.token, environment: entry.environment, notice, generation: itemGeneration, attempts: 0, queued: false };
|
|
1965
2198
|
pending.add(item);
|
|
1966
2199
|
attempt(item);
|
|
1967
2200
|
}
|
|
@@ -1991,18 +2224,19 @@ async function attachPushSender(options) {
|
|
|
1991
2224
|
update();
|
|
1992
2225
|
});
|
|
1993
2226
|
update();
|
|
1994
|
-
return
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2227
|
+
return {
|
|
2228
|
+
stats: () => ({ pending: pending.size, queued: readyQueue.length }),
|
|
2229
|
+
async close() {
|
|
2230
|
+
disposed = true;
|
|
2231
|
+
unsubscribeConversations();
|
|
2232
|
+
unsubscribeCommits();
|
|
2233
|
+
await indexWatch?.stop();
|
|
2234
|
+
for (const item of pending)
|
|
2235
|
+
drop(item);
|
|
2236
|
+
pending.clear();
|
|
2237
|
+
readyQueue.length = 0;
|
|
2238
|
+
titleWaiters.clear();
|
|
2239
|
+
}
|
|
2006
2240
|
};
|
|
2007
2241
|
}
|
|
2008
2242
|
|
|
@@ -2049,18 +2283,12 @@ function serializedFork(harness, work) {
|
|
|
2049
2283
|
var LOOPBACK_SECURE_PATH = "/secure";
|
|
2050
2284
|
async function startGateway(options) {
|
|
2051
2285
|
const conversations = await ConversationList.open(options.harness, options.docs);
|
|
2286
|
+
const fileLister = createCandidateLister(enumerateRoot);
|
|
2052
2287
|
const detachAutoTitle = options.autoTitle === false ? undefined : attachAutoTitle(conversations, {
|
|
2053
2288
|
harness: options.harness,
|
|
2054
2289
|
models: options.models,
|
|
2055
2290
|
settings: options.settings
|
|
2056
2291
|
});
|
|
2057
|
-
const detachPush = options.push === undefined ? undefined : await attachPushSender({
|
|
2058
|
-
harness: options.harness,
|
|
2059
|
-
conversations,
|
|
2060
|
-
hostKey: options.push.hostKey,
|
|
2061
|
-
...options.push.fetch === undefined ? {} : { fetch: options.push.fetch },
|
|
2062
|
-
...options.push.timers === undefined ? {} : { timers: options.push.timers }
|
|
2063
|
-
});
|
|
2064
2292
|
const http = createServer((request, response) => {
|
|
2065
2293
|
serveWebClient(request, response, options.webRoot).catch(() => {
|
|
2066
2294
|
if (!response.headersSent)
|
|
@@ -2069,8 +2297,8 @@ async function startGateway(options) {
|
|
|
2069
2297
|
});
|
|
2070
2298
|
});
|
|
2071
2299
|
const server = new WebSocketServer({ server: http });
|
|
2072
|
-
await new Promise((
|
|
2073
|
-
http.once("listening",
|
|
2300
|
+
await new Promise((resolve4, reject) => {
|
|
2301
|
+
http.once("listening", resolve4);
|
|
2074
2302
|
http.once("error", reject);
|
|
2075
2303
|
http.listen(options.port, "127.0.0.1");
|
|
2076
2304
|
});
|
|
@@ -2087,7 +2315,7 @@ async function startGateway(options) {
|
|
|
2087
2315
|
if (joined?.source === "account") {
|
|
2088
2316
|
broadcast("info", `New device "${joined.name}" joined through your account`);
|
|
2089
2317
|
}
|
|
2090
|
-
const client = new GatewayClient(connection, options, conversations, {
|
|
2318
|
+
const client = new GatewayClient(connection, options, conversations, fileLister, {
|
|
2091
2319
|
devicePublicKey,
|
|
2092
2320
|
closed: (self) => {
|
|
2093
2321
|
clients.delete(self);
|
|
@@ -2161,6 +2389,12 @@ async function startGateway(options) {
|
|
|
2161
2389
|
signingKey: secure.relay.signingKey,
|
|
2162
2390
|
noisePublicKey: secure.hostKey.publicKey,
|
|
2163
2391
|
accept: (socket) => secureHandshake(socket, secure, admission, register),
|
|
2392
|
+
hostName: {
|
|
2393
|
+
get value() {
|
|
2394
|
+
return options.settings.value.hostName;
|
|
2395
|
+
},
|
|
2396
|
+
subscribe: (listener) => options.settings.subscribe((settings) => listener(settings.hostName))
|
|
2397
|
+
},
|
|
2164
2398
|
accountOffer: async () => {
|
|
2165
2399
|
if (!options.settings.value.acceptAccountDevices)
|
|
2166
2400
|
return { ok: false, reason: "disabled" };
|
|
@@ -2177,7 +2411,8 @@ async function startGateway(options) {
|
|
|
2177
2411
|
},
|
|
2178
2412
|
onStateChange: (state) => {
|
|
2179
2413
|
if (state.status === "registered") {
|
|
2180
|
-
|
|
2414
|
+
const push = state.push === undefined ? "undeclared" : state.push.length === 0 ? "none" : state.push.join(",");
|
|
2415
|
+
console.log(JSON.stringify({ event: "relay", status: "registered", origin: link.origin, hostId: link.hostId, push }));
|
|
2181
2416
|
} else if (state.status === "connecting") {
|
|
2182
2417
|
console.log(JSON.stringify({ event: "relay", status: "connecting", origin: link.origin }));
|
|
2183
2418
|
} else {
|
|
@@ -2190,6 +2425,13 @@ async function startGateway(options) {
|
|
|
2190
2425
|
});
|
|
2191
2426
|
relay = link;
|
|
2192
2427
|
}
|
|
2428
|
+
const detachPush = options.push === undefined ? undefined : await attachPushSender({
|
|
2429
|
+
harness: options.harness,
|
|
2430
|
+
conversations,
|
|
2431
|
+
hostKey: options.push.hostKey,
|
|
2432
|
+
...options.push.transport === undefined ? relay === undefined ? {} : { transport: { send: (request) => relay.sendPush(request) } } : { transport: options.push.transport },
|
|
2433
|
+
...options.push.timers === undefined ? {} : { timers: options.push.timers }
|
|
2434
|
+
});
|
|
2193
2435
|
return {
|
|
2194
2436
|
url: `ws://127.0.0.1:${port}`,
|
|
2195
2437
|
...remote === undefined ? {} : { remote: { url: remote.url, advertiseUrl: remote.advertiseUrl } },
|
|
@@ -2199,14 +2441,14 @@ async function startGateway(options) {
|
|
|
2199
2441
|
await relay?.close();
|
|
2200
2442
|
detachAutoTitle?.();
|
|
2201
2443
|
detachSettingsWatch();
|
|
2202
|
-
await detachPush?.();
|
|
2444
|
+
await detachPush?.close();
|
|
2203
2445
|
conversations.dispose();
|
|
2204
2446
|
await Promise.all([...clients].map((client) => client.dispose()));
|
|
2205
2447
|
for (const socket of server.clients)
|
|
2206
2448
|
socket.terminate();
|
|
2207
2449
|
await remote?.close();
|
|
2208
|
-
await new Promise((
|
|
2209
|
-
await new Promise((
|
|
2450
|
+
await new Promise((resolve4) => server.close(() => resolve4()));
|
|
2451
|
+
await new Promise((resolve4) => http.close(() => resolve4()));
|
|
2210
2452
|
}
|
|
2211
2453
|
};
|
|
2212
2454
|
}
|
|
@@ -2215,7 +2457,7 @@ function tokenConnection(socket) {
|
|
|
2215
2457
|
get open() {
|
|
2216
2458
|
return socket.readyState === WebSocket.OPEN;
|
|
2217
2459
|
},
|
|
2218
|
-
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((
|
|
2460
|
+
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve4) => socket.send(text, () => resolve4())) : Promise.resolve(),
|
|
2219
2461
|
close: (code, reason) => socket.close(code, reason),
|
|
2220
2462
|
handle(handlers) {
|
|
2221
2463
|
socket.on("message", (data) => handlers.message(String(data)));
|
|
@@ -2228,9 +2470,9 @@ function secureConnection(socket, session) {
|
|
|
2228
2470
|
get open() {
|
|
2229
2471
|
return socket.readyState === WebSocket.OPEN;
|
|
2230
2472
|
},
|
|
2231
|
-
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((
|
|
2473
|
+
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve4) => {
|
|
2232
2474
|
const chunks = session.seal(text);
|
|
2233
|
-
chunks.forEach((chunk, index) => socket.send(chunk, index === chunks.length - 1 ? () =>
|
|
2475
|
+
chunks.forEach((chunk, index) => socket.send(chunk, index === chunks.length - 1 ? () => resolve4() : undefined));
|
|
2234
2476
|
}) : Promise.resolve(),
|
|
2235
2477
|
close: (code, reason) => socket.close(code, reason),
|
|
2236
2478
|
handle(handlers) {
|
|
@@ -2349,8 +2591,8 @@ async function startRemote(options, admission, register) {
|
|
|
2349
2591
|
async close() {
|
|
2350
2592
|
for (const socket of secure.clients)
|
|
2351
2593
|
socket.terminate();
|
|
2352
|
-
await new Promise((
|
|
2353
|
-
await new Promise((
|
|
2594
|
+
await new Promise((resolve4) => secure.close(() => resolve4()));
|
|
2595
|
+
await new Promise((resolve4) => http.close(() => resolve4()));
|
|
2354
2596
|
}
|
|
2355
2597
|
};
|
|
2356
2598
|
}
|
|
@@ -2361,6 +2603,7 @@ class ConversationList {
|
|
|
2361
2603
|
#questionKinds;
|
|
2362
2604
|
#records = new Map;
|
|
2363
2605
|
#facts = new Map;
|
|
2606
|
+
#roles = new Map;
|
|
2364
2607
|
#index = new Map;
|
|
2365
2608
|
#runEnd = new Set;
|
|
2366
2609
|
#list = [];
|
|
@@ -2374,19 +2617,26 @@ class ConversationList {
|
|
|
2374
2617
|
const list = new ConversationList(docs);
|
|
2375
2618
|
const summaries = [];
|
|
2376
2619
|
let cursor;
|
|
2620
|
+
const records = [];
|
|
2377
2621
|
do {
|
|
2378
2622
|
const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor), context3);
|
|
2379
2623
|
for (const record of page.items) {
|
|
2380
2624
|
list.#records.set(record.id, record);
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
list.#index.set(record.id, summaries.length);
|
|
2384
|
-
summaries.push(summaryOf(record, facts));
|
|
2625
|
+
list.#facts.set(record.id, await list.#load(harness, record));
|
|
2626
|
+
records.push(record);
|
|
2385
2627
|
}
|
|
2386
2628
|
cursor = page.next;
|
|
2387
2629
|
} while (cursor !== undefined);
|
|
2630
|
+
list.#roles = rolesOf(await harness.snapshot(IndexDoc, context3));
|
|
2631
|
+
for (const record of records) {
|
|
2632
|
+
list.#index.set(record.id, summaries.length);
|
|
2633
|
+
summaries.push(list.#summaryOf(record));
|
|
2634
|
+
}
|
|
2388
2635
|
list.#list = summaries;
|
|
2389
2636
|
list.#unsubscribe = harness.subscribeCommits((publication) => list.#observe(publication));
|
|
2637
|
+
const touched = new Set;
|
|
2638
|
+
list.#reindex(await harness.snapshot(IndexDoc, context3), touched);
|
|
2639
|
+
list.#refreshSummaries(touched);
|
|
2390
2640
|
return list;
|
|
2391
2641
|
}
|
|
2392
2642
|
async#load(harness, record) {
|
|
@@ -2440,8 +2690,11 @@ class ConversationList {
|
|
|
2440
2690
|
facts.calls = entry.kind === "pi.assistant" ? callsOf(message) : undefined;
|
|
2441
2691
|
} else if (change.type === "document") {
|
|
2442
2692
|
const id = change.conversationId;
|
|
2443
|
-
if (id === undefined)
|
|
2693
|
+
if (id === undefined) {
|
|
2694
|
+
if (change.record.kind === IndexDoc.definition.kind)
|
|
2695
|
+
this.#reindex(change.value, touched);
|
|
2444
2696
|
continue;
|
|
2697
|
+
}
|
|
2445
2698
|
const facts = this.#fact(id);
|
|
2446
2699
|
if (change.record.kind === "pi.live") {
|
|
2447
2700
|
const live = change.value;
|
|
@@ -2454,12 +2707,12 @@ class ConversationList {
|
|
|
2454
2707
|
} else if (this.#questionKinds.has(change.record.kind)) {
|
|
2455
2708
|
const value = change.value;
|
|
2456
2709
|
const pending = Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
|
|
2457
|
-
const
|
|
2710
|
+
const next = new Set(facts.pending);
|
|
2458
2711
|
if (pending)
|
|
2459
|
-
|
|
2712
|
+
next.add(change.record.kind);
|
|
2460
2713
|
else
|
|
2461
|
-
|
|
2462
|
-
facts.pending =
|
|
2714
|
+
next.delete(change.record.kind);
|
|
2715
|
+
facts.pending = next;
|
|
2463
2716
|
touched.add(id);
|
|
2464
2717
|
} else if (change.record.kind === SubagentDoc.definition.kind) {
|
|
2465
2718
|
const value = change.value;
|
|
@@ -2473,12 +2726,35 @@ class ConversationList {
|
|
|
2473
2726
|
}
|
|
2474
2727
|
}
|
|
2475
2728
|
}
|
|
2729
|
+
this.#refreshSummaries(touched);
|
|
2730
|
+
for (const id of ended)
|
|
2731
|
+
for (const listener of this.#runEnd)
|
|
2732
|
+
listener(id);
|
|
2733
|
+
}
|
|
2734
|
+
onRunEnd(listener) {
|
|
2735
|
+
this.#runEnd.add(listener);
|
|
2736
|
+
return () => this.#runEnd.delete(listener);
|
|
2737
|
+
}
|
|
2738
|
+
#reindex(index, touched) {
|
|
2739
|
+
const next = rolesOf(index);
|
|
2740
|
+
for (const record of this.#records.values()) {
|
|
2741
|
+
if (record.owner === undefined && topLevelRole(record, this.#records, this.#roles) !== topLevelRole(record, this.#records, next)) {
|
|
2742
|
+
touched.add(record.id);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
this.#roles = next;
|
|
2746
|
+
}
|
|
2747
|
+
#summaryOf(record) {
|
|
2748
|
+
const facts = this.#fact(record.id);
|
|
2749
|
+
return summaryOf(record, facts, record.owner === undefined ? topLevelRole(record, this.#records, this.#roles) : facts.role);
|
|
2750
|
+
}
|
|
2751
|
+
#refreshSummaries(touched) {
|
|
2476
2752
|
let next;
|
|
2477
2753
|
for (const id of touched) {
|
|
2478
2754
|
const record = this.#records.get(id);
|
|
2479
2755
|
if (record === undefined)
|
|
2480
2756
|
continue;
|
|
2481
|
-
const summary = summaryOf(record
|
|
2757
|
+
const summary = this.#summaryOf(record);
|
|
2482
2758
|
const index = this.#index.get(id);
|
|
2483
2759
|
if (index === undefined) {
|
|
2484
2760
|
next ??= this.#list.slice();
|
|
@@ -2491,13 +2767,6 @@ class ConversationList {
|
|
|
2491
2767
|
}
|
|
2492
2768
|
if (next !== undefined)
|
|
2493
2769
|
this.#set(next);
|
|
2494
|
-
for (const id of ended)
|
|
2495
|
-
for (const listener of this.#runEnd)
|
|
2496
|
-
listener(id);
|
|
2497
|
-
}
|
|
2498
|
-
onRunEnd(listener) {
|
|
2499
|
-
this.#runEnd.add(listener);
|
|
2500
|
-
return () => this.#runEnd.delete(listener);
|
|
2501
2770
|
}
|
|
2502
2771
|
#fact(id) {
|
|
2503
2772
|
let facts = this.#facts.get(id);
|
|
@@ -2528,7 +2797,26 @@ class ConversationList {
|
|
|
2528
2797
|
this.#listeners.clear();
|
|
2529
2798
|
}
|
|
2530
2799
|
}
|
|
2531
|
-
function
|
|
2800
|
+
function rolesOf(index) {
|
|
2801
|
+
const roles = new Map;
|
|
2802
|
+
for (const entry of index?.conversations ?? []) {
|
|
2803
|
+
if (typeof entry.role === "string" && entry.role !== "")
|
|
2804
|
+
roles.set(entry.id, entry.role);
|
|
2805
|
+
}
|
|
2806
|
+
return roles;
|
|
2807
|
+
}
|
|
2808
|
+
function topLevelRole(record, records, roles) {
|
|
2809
|
+
let current = record;
|
|
2810
|
+
const seen = new Set;
|
|
2811
|
+
while (current !== undefined && !seen.has(current.id)) {
|
|
2812
|
+
seen.add(current.id);
|
|
2813
|
+
if (current.parent === undefined)
|
|
2814
|
+
return roles.get(current.id);
|
|
2815
|
+
current = records.get(current.parent.conversationId);
|
|
2816
|
+
}
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
function summaryOf(record, facts, role) {
|
|
2532
2820
|
const parent = record.parent?.conversationId ?? record.owner?.conversationId;
|
|
2533
2821
|
const status = facts.pending.size > 0 ? "needs-answer" : facts.running === true ? "running" : facts.failed === true ? "failed" : undefined;
|
|
2534
2822
|
const runningSlot = facts.running === true ? facts.tools?.find((slot) => slot.status === "running") : undefined;
|
|
@@ -2545,7 +2833,7 @@ function summaryOf(record, facts) {
|
|
|
2545
2833
|
...status === undefined ? {} : { status },
|
|
2546
2834
|
...facts.updatedAt === undefined ? {} : { updatedAt: facts.updatedAt },
|
|
2547
2835
|
...facts.label === undefined ? {} : { label: facts.label },
|
|
2548
|
-
...
|
|
2836
|
+
...role === undefined ? {} : { role },
|
|
2549
2837
|
...activity === undefined ? {} : { activity }
|
|
2550
2838
|
};
|
|
2551
2839
|
}
|
|
@@ -2601,12 +2889,14 @@ class GatewayClient {
|
|
|
2601
2889
|
#connection;
|
|
2602
2890
|
#hooks;
|
|
2603
2891
|
#options;
|
|
2892
|
+
#files;
|
|
2604
2893
|
#hostValues;
|
|
2605
2894
|
#subscriptions = new Map;
|
|
2606
2895
|
#disposed = false;
|
|
2607
|
-
constructor(connection, options, conversations, hooks) {
|
|
2896
|
+
constructor(connection, options, conversations, files, hooks) {
|
|
2608
2897
|
this.#connection = connection;
|
|
2609
2898
|
this.#options = options;
|
|
2899
|
+
this.#files = files;
|
|
2610
2900
|
this.#hostValues = {
|
|
2611
2901
|
conversations,
|
|
2612
2902
|
mcp: options.mcp ?? constantValue(null),
|
|
@@ -2741,8 +3031,8 @@ class GatewayClient {
|
|
|
2741
3031
|
await this.#conversation(id);
|
|
2742
3032
|
const { harness } = this.#options;
|
|
2743
3033
|
let signalCreation;
|
|
2744
|
-
const created = new Promise((
|
|
2745
|
-
signalCreation =
|
|
3034
|
+
const created = new Promise((resolve4) => {
|
|
3035
|
+
signalCreation = resolve4;
|
|
2746
3036
|
});
|
|
2747
3037
|
let inner;
|
|
2748
3038
|
let stopped = false;
|
|
@@ -2833,8 +3123,9 @@ class GatewayClient {
|
|
|
2833
3123
|
...create.images === undefined ? {} : { images: create.images },
|
|
2834
3124
|
...create.checkout === undefined ? {} : { checkout: create.checkout },
|
|
2835
3125
|
...create.model === undefined ? {} : { model: create.model },
|
|
2836
|
-
...create.thinkingLevel === undefined ? {} : { thinkingLevel: create.thinkingLevel }
|
|
2837
|
-
|
|
3126
|
+
...create.thinkingLevel === undefined ? {} : { thinkingLevel: create.thinkingLevel },
|
|
3127
|
+
...create.agent === undefined ? {} : { agent: create.agent }
|
|
3128
|
+
}, settingsDefaults(this.#options.settings.value), this.#options.models, this.#options.modelSummaries, this.#options.agents?.value ?? { roles: [], problems: [] }, context3);
|
|
2838
3129
|
return { conversationId };
|
|
2839
3130
|
}
|
|
2840
3131
|
case "archive": {
|
|
@@ -2864,7 +3155,10 @@ class GatewayClient {
|
|
|
2864
3155
|
const device = this.devicePublicKey;
|
|
2865
3156
|
if (device === undefined)
|
|
2866
3157
|
throw new Error("Push tokens register over the secure channel only");
|
|
2867
|
-
await
|
|
3158
|
+
const joined = async () => await readServerConfig(this.#options.dataDir).catch(() => {
|
|
3159
|
+
return;
|
|
3160
|
+
}) !== undefined;
|
|
3161
|
+
await setPushToken(this.#options.harness, device, args.token, context3, joined);
|
|
2868
3162
|
return null;
|
|
2869
3163
|
}
|
|
2870
3164
|
case "logout": {
|
|
@@ -2901,6 +3195,10 @@ class GatewayClient {
|
|
|
2901
3195
|
}
|
|
2902
3196
|
case "listFolders":
|
|
2903
3197
|
return listFolders(args.path, homedir2());
|
|
3198
|
+
case "listFiles": {
|
|
3199
|
+
const input = args;
|
|
3200
|
+
return listFiles(this.#options.harness, this.#options.dataDir, this.#files, input.scope, input.query, context3);
|
|
3201
|
+
}
|
|
2904
3202
|
}
|
|
2905
3203
|
const conversation = await this.#conversation(args.conversationId);
|
|
2906
3204
|
switch (method) {
|
|
@@ -3061,7 +3359,7 @@ class GatewayClient {
|
|
|
3061
3359
|
}, context4);
|
|
3062
3360
|
}
|
|
3063
3361
|
get #chatsPrefix() {
|
|
3064
|
-
return
|
|
3362
|
+
return join7(this.#options.dataDir, "chats") + sep4;
|
|
3065
3363
|
}
|
|
3066
3364
|
async#changes(conversation, context4) {
|
|
3067
3365
|
const cwd = (await conversation.agent(context4)).cwd;
|
|
@@ -3074,15 +3372,16 @@ class GatewayClient {
|
|
|
3074
3372
|
if (record !== undefined) {
|
|
3075
3373
|
if (!await worktreeExists(record.path))
|
|
3076
3374
|
return { available: false, reason: "No changes yet" };
|
|
3077
|
-
|
|
3375
|
+
const [changes, branch2] = await Promise.all([changesOf(record.path, record.base), branchOf(record.path)]);
|
|
3376
|
+
return { available: true, ...changes, branch: branch2 };
|
|
3078
3377
|
}
|
|
3079
|
-
const base = await gitBase(cwd);
|
|
3378
|
+
const [base, branch] = await Promise.all([gitBase(cwd), branchOf(cwd)]);
|
|
3080
3379
|
if (base === undefined)
|
|
3081
|
-
return { available: false, reason: "Not in a git repository" };
|
|
3082
|
-
return { available: true, ...await changesOf(base.repo, base.base) };
|
|
3380
|
+
return { available: false, reason: "Not in a git repository", branch };
|
|
3381
|
+
return { available: true, ...await changesOf(base.repo, base.base), branch };
|
|
3083
3382
|
}
|
|
3084
3383
|
async#agent(id) {
|
|
3085
|
-
return await this.#options.harness.snapshot(
|
|
3384
|
+
return await this.#options.harness.snapshot(AgentDoc4, id, context3) ?? {};
|
|
3086
3385
|
}
|
|
3087
3386
|
async dispose() {
|
|
3088
3387
|
if (this.#disposed)
|
|
@@ -3098,18 +3397,18 @@ class GatewayClient {
|
|
|
3098
3397
|
// apps/host/src/host.ts
|
|
3099
3398
|
var context4 = BACKGROUND_CONTEXT4;
|
|
3100
3399
|
async function hostToken(dataDir) {
|
|
3101
|
-
const path =
|
|
3400
|
+
const path = join8(dataDir, "token");
|
|
3102
3401
|
try {
|
|
3103
|
-
return (await
|
|
3402
|
+
return (await readFile3(path, "utf8")).trim();
|
|
3104
3403
|
} catch {
|
|
3105
3404
|
const token = randomBytes3(24).toString("base64url");
|
|
3106
|
-
await
|
|
3405
|
+
await writeFile2(path, `${token}
|
|
3107
3406
|
`, { mode: 384 });
|
|
3108
3407
|
return token;
|
|
3109
3408
|
}
|
|
3110
3409
|
}
|
|
3111
3410
|
async function openHost(options) {
|
|
3112
|
-
await
|
|
3411
|
+
await mkdir3(options.dataDir, { recursive: true });
|
|
3113
3412
|
const stale = Math.max(2000, options.lockStaleMs ?? 1e4);
|
|
3114
3413
|
const release = await lockfile.lock(options.dataDir, {
|
|
3115
3414
|
realpath: false,
|
|
@@ -3127,7 +3426,7 @@ async function openHost(options) {
|
|
|
3127
3426
|
const reports = [];
|
|
3128
3427
|
let report = (error) => void reports.push(error);
|
|
3129
3428
|
const registry = createRegistry();
|
|
3130
|
-
agentRoles = await openAgentRoles({ dir:
|
|
3429
|
+
agentRoles = await openAgentRoles({ dir: join8(options.dataDir, "agents") });
|
|
3131
3430
|
extensions = typeof options.extensions === "function" ? options.extensions({
|
|
3132
3431
|
harness: () => {
|
|
3133
3432
|
if (harness === undefined)
|
|
@@ -3150,13 +3449,13 @@ async function openHost(options) {
|
|
|
3150
3449
|
}
|
|
3151
3450
|
});
|
|
3152
3451
|
const hostSettings = await openHostSettings({
|
|
3153
|
-
path:
|
|
3452
|
+
path: join8(options.dataDir, "settings.json"),
|
|
3154
3453
|
fallback: options.settingsFallback,
|
|
3155
3454
|
models: options.modelAuth?.models ?? constantValue(options.modelSummaries())
|
|
3156
3455
|
});
|
|
3157
3456
|
const environments = new Map;
|
|
3158
|
-
const chatsDir =
|
|
3159
|
-
harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(
|
|
3457
|
+
const chatsDir = join8(options.dataDir, "chats");
|
|
3458
|
+
harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(join8(options.dataDir, "session.sqlite")), {
|
|
3160
3459
|
models: options.models,
|
|
3161
3460
|
registry,
|
|
3162
3461
|
env: async (target) => {
|
|
@@ -3164,16 +3463,16 @@ async function openHost(options) {
|
|
|
3164
3463
|
if (cwd === undefined)
|
|
3165
3464
|
return;
|
|
3166
3465
|
const worktrees = worktreeRoot(options.dataDir);
|
|
3167
|
-
if (cwd === worktrees || cwd.startsWith(worktrees +
|
|
3466
|
+
if (cwd === worktrees || cwd.startsWith(worktrees + sep5)) {
|
|
3168
3467
|
const index = await target.read.snapshot(IndexDoc, context4);
|
|
3169
3468
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
3170
3469
|
if (record !== undefined) {
|
|
3171
3470
|
await ensureWorktree(record);
|
|
3172
|
-
await
|
|
3471
|
+
await mkdir3(cwd, { recursive: true });
|
|
3173
3472
|
}
|
|
3174
3473
|
}
|
|
3175
3474
|
if (cwd.startsWith(`${chatsDir}/`))
|
|
3176
|
-
await
|
|
3475
|
+
await mkdir3(cwd, { recursive: true });
|
|
3177
3476
|
let env = environments.get(cwd);
|
|
3178
3477
|
if (env === undefined) {
|
|
3179
3478
|
env = new NodeExecutionEnv({ cwd });
|
|
@@ -3213,7 +3512,7 @@ async function openHost(options) {
|
|
|
3213
3512
|
...options.push === undefined ? {} : {
|
|
3214
3513
|
push: {
|
|
3215
3514
|
hostKey: toBase64Url(hostKey.publicKey),
|
|
3216
|
-
...options.push.
|
|
3515
|
+
...options.push.transport === undefined ? {} : { transport: options.push.transport },
|
|
3217
3516
|
...options.push.timers === undefined ? {} : { timers: options.push.timers }
|
|
3218
3517
|
}
|
|
3219
3518
|
},
|
|
@@ -3289,7 +3588,7 @@ async function openHost(options) {
|
|
|
3289
3588
|
|
|
3290
3589
|
// apps/host/src/model-auth.ts
|
|
3291
3590
|
import { existsSync as existsSync2, watch as watch2 } from "node:fs";
|
|
3292
|
-
import { basename as basename3, dirname as
|
|
3591
|
+
import { basename as basename3, dirname as dirname2 } from "node:path";
|
|
3293
3592
|
|
|
3294
3593
|
// apps/host/src/pi-setup.ts
|
|
3295
3594
|
import { getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai";
|
|
@@ -3404,7 +3703,7 @@ function modelSummaries(modelRuntime) {
|
|
|
3404
3703
|
}
|
|
3405
3704
|
|
|
3406
3705
|
// apps/host/src/model-auth.ts
|
|
3407
|
-
var
|
|
3706
|
+
var sameModel = (a, b) => a.provider === b.provider && a.modelId === b.modelId && a.name === b.name && a.contextWindow === b.contextWindow && sameList(a.thinkingLevels, b.thinkingLevels, (x, y) => x === y);
|
|
3408
3707
|
var sameLogin = (a, b) => a.providerId === b.providerId && a.name === b.name && a.kind === b.kind && a.subscription === b.subscription;
|
|
3409
3708
|
async function storedLogins(modelRuntime) {
|
|
3410
3709
|
const credentials = await modelRuntime.listCredentials();
|
|
@@ -3419,7 +3718,7 @@ async function storedLogins(modelRuntime) {
|
|
|
3419
3718
|
}
|
|
3420
3719
|
async function watchModelAuth(modelRuntime, options) {
|
|
3421
3720
|
const debounceMs = options.debounceMs ?? 150;
|
|
3422
|
-
const models = new Published(modelSummaries(modelRuntime), (a, b) => sameList(a, b,
|
|
3721
|
+
const models = new Published(modelSummaries(modelRuntime), (a, b) => sameList(a, b, sameModel));
|
|
3423
3722
|
const auth = new Published(await storedLogins(modelRuntime), (a, b) => sameList(a.logins, b.logins, sameLogin));
|
|
3424
3723
|
const fileName = basename3(options.authPath);
|
|
3425
3724
|
let watcher;
|
|
@@ -3438,7 +3737,7 @@ async function watchModelAuth(modelRuntime, options) {
|
|
|
3438
3737
|
reported = true;
|
|
3439
3738
|
}
|
|
3440
3739
|
};
|
|
3441
|
-
const directory =
|
|
3740
|
+
const directory = dirname2(options.authPath);
|
|
3442
3741
|
if (existsSync2(directory)) {
|
|
3443
3742
|
watcher = watch2(directory, (_event, changed) => {
|
|
3444
3743
|
if (closed || changed !== null && changed !== fileName)
|
|
@@ -3489,7 +3788,7 @@ async function main(argv) {
|
|
|
3489
3788
|
relay: { type: "string" }
|
|
3490
3789
|
}
|
|
3491
3790
|
});
|
|
3492
|
-
const dataDir =
|
|
3791
|
+
const dataDir = resolve4(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
3493
3792
|
let relayOrigin;
|
|
3494
3793
|
try {
|
|
3495
3794
|
relayOrigin = resolveRelayOrigin(values.relay, await readServerConfig(dataDir));
|
|
@@ -3499,7 +3798,7 @@ async function main(argv) {
|
|
|
3499
3798
|
}
|
|
3500
3799
|
const extensionsFor = (models, summaries, options2) => (host2) => [
|
|
3501
3800
|
createContext({
|
|
3502
|
-
agentsHome:
|
|
3801
|
+
agentsHome: join9(homedir3(), ".agents"),
|
|
3503
3802
|
checkout: (input, context5) => checkoutInfo(host2.harness(), input, context5)
|
|
3504
3803
|
}),
|
|
3505
3804
|
createCoding(options2),
|
|
@@ -3517,7 +3816,7 @@ async function main(argv) {
|
|
|
3517
3816
|
dataDir,
|
|
3518
3817
|
projects: values.project ?? [],
|
|
3519
3818
|
port: Number(values.port),
|
|
3520
|
-
mcpConfig:
|
|
3819
|
+
mcpConfig: join9(homedir3(), ".agents", "mcp.json"),
|
|
3521
3820
|
browserOrigins: values["browser-origin"] ?? ["http://127.0.0.1:5199"],
|
|
3522
3821
|
webRoot,
|
|
3523
3822
|
...values["lock-stale-ms"] === undefined ? {} : { lockStaleMs: Number(values["lock-stale-ms"]) },
|
|
@@ -3561,7 +3860,7 @@ async function main(argv) {
|
|
|
3561
3860
|
}
|
|
3562
3861
|
async function piOptions() {
|
|
3563
3862
|
const modelRuntime = await ModelRuntime.create();
|
|
3564
|
-
const modelAuth = await watchModelAuth(modelRuntime, { authPath:
|
|
3863
|
+
const modelAuth = await watchModelAuth(modelRuntime, { authPath: join9(getAgentDir(), "auth.json") });
|
|
3565
3864
|
const settingsManager = SettingsManager.create(process.cwd());
|
|
3566
3865
|
configureHarnessHttp(settingsManager);
|
|
3567
3866
|
const summaries = () => modelAuth.models.value;
|
|
@@ -3593,7 +3892,7 @@ async function main(argv) {
|
|
|
3593
3892
|
console.log(JSON.stringify({
|
|
3594
3893
|
event: "ready",
|
|
3595
3894
|
url: host.url,
|
|
3596
|
-
tokenFile:
|
|
3895
|
+
tokenFile: join9(dataDir, "token"),
|
|
3597
3896
|
dataDir,
|
|
3598
3897
|
model,
|
|
3599
3898
|
web,
|