pinomad 0.1.0 → 0.2.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 +51 -4
- package/dist/{chunk-jew71ysz.js → chunk-5712fvyp.js} +4 -4
- package/dist/{chunk-y73t2jmc.js → chunk-5h97zxag.js} +119 -41
- package/dist/{chunk-928atme2.js → chunk-d9q0dz8s.js} +2 -2
- package/dist/{chunk-hach8mdw.js → chunk-fhtkkcgt.js} +413 -168
- package/dist/{chunk-wgn3cx5q.js → chunk-h35fermn.js} +48 -12
- package/dist/{chunk-c3v7vhny.js → chunk-haycqqp1.js} +1 -1
- package/dist/{chunk-srbefzgp.js → chunk-n4bvmcy8.js} +4 -4
- package/dist/{chunk-hsdzb73r.js → chunk-q26n39mf.js} +10 -4
- package/dist/{chunk-pkxr5my9.js → chunk-qdxhqva8.js} +1 -1
- package/dist/{chunk-v7yf5752.js → chunk-rkfdsacq.js} +1 -1
- package/dist/pinomad.js +7 -7
- package/package.json +1 -1
- package/web/assets/index-4p91UVoc.js +383 -0
- package/web/assets/index-KwICnYux.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-B9yvK0AN.js +0 -383
- package/web/assets/index-t5dAiohJ.css +0 -1
|
@@ -3,13 +3,15 @@ import {
|
|
|
3
3
|
serveWebClient
|
|
4
4
|
} from "./chunk-r6w5aqkw.js";
|
|
5
5
|
import {
|
|
6
|
+
packagedVersion,
|
|
6
7
|
webRoot
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-rkfdsacq.js";
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
+
INVALID_FRAME_CLOSE_CODE,
|
|
11
|
+
PROTOCOL,
|
|
10
12
|
UNAUTHORIZED_CLOSE_CODE,
|
|
11
|
-
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
parseClientFrame
|
|
14
|
+
} from "./chunk-h35fermn.js";
|
|
13
15
|
import {
|
|
14
16
|
DevicesDoc,
|
|
15
17
|
ensureDevices,
|
|
@@ -618,6 +620,7 @@ import { dirname, join, sep } from "node:path";
|
|
|
618
620
|
import { defineExtension as defineExtension3, section as section2 } from "@earendil-works/pi-durable";
|
|
619
621
|
import { formatSkillsForPrompt, loadProjectContextFiles, loadSkills } from "@earendil-works/pi-coding-agent";
|
|
620
622
|
var PREAMBLE = "You are PiNomad, a coding agent running on the user's own machine. You work in the project directory below with the read, write, edit, and bash tools: read files before changing them, prefer edit over rewriting whole files, and run commands to check your work. Keep answers short and concrete. When it is unclear what the user wants, ask instead of guessing.";
|
|
623
|
+
var FORK_SHARED_DIR = "This conversation is a fork: it shares this working directory with the conversation it was forked from, which may be editing the same files. If your task needs isolation, create your own git worktree (for example `git worktree add <dir> -b <branch>`) and use absolute paths inside it for every read, write, edit and bash command.";
|
|
621
624
|
function createContext(options) {
|
|
622
625
|
const { agentsHome } = options;
|
|
623
626
|
const resolveCheckout = (input, context) => options.checkout?.(input, context);
|
|
@@ -630,14 +633,19 @@ function createContext(options) {
|
|
|
630
633
|
const cwd = input.agent.cwd;
|
|
631
634
|
if (cwd === undefined)
|
|
632
635
|
return;
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
+
const resolved = await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context);
|
|
637
|
+
const checkout = resolved?.checkout;
|
|
638
|
+
const forkLine = resolved?.fork === true && checkout?.own !== true ? [FORK_SHARED_DIR] : [];
|
|
639
|
+
if (checkout === undefined) {
|
|
640
|
+
return [`Working directory: ${cwd}`, ...forkLine].join(`
|
|
641
|
+
`);
|
|
642
|
+
}
|
|
636
643
|
return [
|
|
637
644
|
`Working directory: ${cwd}`,
|
|
638
|
-
`This directory is a git worktree PiNomad created for this conversation, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.`,
|
|
645
|
+
checkout.own ? `This directory is a git worktree PiNomad created for this conversation, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.` : `This directory is a git worktree PiNomad created for the conversation this one belongs to, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.`,
|
|
639
646
|
`The user's project directory is ${checkout.projectPath} and must not be modified.`,
|
|
640
|
-
"This is a fresh checkout: ignored files (dependencies such as node_modules, or .env) are absent — install dependencies per AGENTS.md when needed."
|
|
647
|
+
"This is a fresh checkout: ignored files (dependencies such as node_modules, or .env) are absent — install dependencies per AGENTS.md when needed.",
|
|
648
|
+
...forkLine
|
|
641
649
|
].join(`
|
|
642
650
|
`);
|
|
643
651
|
}),
|
|
@@ -645,7 +653,7 @@ function createContext(options) {
|
|
|
645
653
|
const cwd = input.agent.cwd;
|
|
646
654
|
if (cwd === undefined)
|
|
647
655
|
return;
|
|
648
|
-
const checkout = await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context);
|
|
656
|
+
const checkout = (await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context))?.checkout;
|
|
649
657
|
let files;
|
|
650
658
|
if (checkout === undefined) {
|
|
651
659
|
files = loadProjectContextFiles({ cwd, agentDir: agentsHome });
|
|
@@ -695,6 +703,19 @@ import {
|
|
|
695
703
|
defineTool as defineTool3
|
|
696
704
|
} from "@earendil-works/pi-durable";
|
|
697
705
|
import { Type as Type4 } from "typebox";
|
|
706
|
+
|
|
707
|
+
// apps/host/src/subagent-doc.ts
|
|
708
|
+
import { defineDoc as defineDoc2 } from "@earendil-works/pi-durable";
|
|
709
|
+
var SubagentDoc = defineDoc2({
|
|
710
|
+
kind: "pinomad.subagent",
|
|
711
|
+
version: 1,
|
|
712
|
+
scope: "conversation",
|
|
713
|
+
history: "latest",
|
|
714
|
+
fork: "initial",
|
|
715
|
+
initial: () => ({})
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// apps/host/src/extensions/subagent.ts
|
|
698
719
|
var INSTRUCTIONS = "You are a subagent working on one delegated task. You cannot ask the user questions; if you need a decision or clarification, stop and state exactly what you need in your final answer. Your final message is returned to the delegating agent as the result, so make it a complete, self-contained report.";
|
|
699
720
|
var THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
700
721
|
function createSubagent(options) {
|
|
@@ -715,8 +736,14 @@ function createSubagent(options) {
|
|
|
715
736
|
execute: async (args, api, context) => {
|
|
716
737
|
const child = await api.commit(async (tx) => {
|
|
717
738
|
const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
718
|
-
if (existing !== undefined)
|
|
719
|
-
|
|
739
|
+
if (existing !== undefined) {
|
|
740
|
+
const doc2 = await tx.doc(SubagentDoc, existing.id);
|
|
741
|
+
return {
|
|
742
|
+
id: existing.id,
|
|
743
|
+
...doc2?.model === undefined ? {} : { model: doc2.model },
|
|
744
|
+
...doc2?.startedAt === undefined ? {} : { startedAt: doc2.startedAt }
|
|
745
|
+
};
|
|
746
|
+
}
|
|
720
747
|
let model;
|
|
721
748
|
let thinkingLevel;
|
|
722
749
|
let override;
|
|
@@ -747,12 +774,22 @@ function createSubagent(options) {
|
|
|
747
774
|
};
|
|
748
775
|
const created = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
|
|
749
776
|
await configure(tx, created.id, change);
|
|
750
|
-
|
|
777
|
+
const childAgent = await tx.doc(AgentDoc, created.id);
|
|
778
|
+
const ref = childAgent.model ?? model ?? (await tx.doc(AgentDoc, api.conversationId)).model;
|
|
779
|
+
const startedAt = Date.now();
|
|
780
|
+
const doc = await tx.doc(SubagentDoc, created.id);
|
|
781
|
+
doc.startedAt = startedAt;
|
|
782
|
+
if (ref !== undefined)
|
|
783
|
+
doc.model = `${ref.provider}/${ref.modelId}`;
|
|
784
|
+
const label = args.description?.trim();
|
|
785
|
+
if (label !== undefined && label !== "")
|
|
786
|
+
doc.label = label;
|
|
787
|
+
return { id: created.id, ...ref === undefined ? {} : { model: doc.model }, startedAt };
|
|
751
788
|
}, context);
|
|
752
|
-
await api.details({ conversationId: child }, context);
|
|
753
|
-
const handle = await api.conversation(child, context);
|
|
789
|
+
await api.details({ conversationId: child.id, ...child.model === undefined ? {} : { model: child.model }, ...child.startedAt === undefined ? {} : { startedAt: child.startedAt } }, context);
|
|
790
|
+
const handle = await api.conversation(child.id, context);
|
|
754
791
|
if (handle === undefined)
|
|
755
|
-
throw new Error(`Subagent conversation ${child} does not exist`);
|
|
792
|
+
throw new Error(`Subagent conversation ${child.id} does not exist`);
|
|
756
793
|
const request = { type: "input", content: args.task, requestId: `subagent:${api.taskId}` };
|
|
757
794
|
const settled = await (await handle.submit(request, context)).wait(context);
|
|
758
795
|
if (settled.status !== "done" || settled.type !== "input")
|
|
@@ -762,7 +799,11 @@ function createSubagent(options) {
|
|
|
762
799
|
const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
|
|
763
800
|
const result2 = {
|
|
764
801
|
content: [{ type: "text", text: text === "" ? "(The subagent returned no text.)" : text }],
|
|
765
|
-
details: {
|
|
802
|
+
details: {
|
|
803
|
+
conversationId: child.id,
|
|
804
|
+
...child.model === undefined ? {} : { model: child.model },
|
|
805
|
+
...child.startedAt === undefined ? {} : { startedAt: child.startedAt }
|
|
806
|
+
}
|
|
766
807
|
};
|
|
767
808
|
return result2;
|
|
768
809
|
}
|
|
@@ -772,9 +813,9 @@ function createSubagent(options) {
|
|
|
772
813
|
}
|
|
773
814
|
|
|
774
815
|
// apps/host/src/extensions/todo.ts
|
|
775
|
-
import { defineDoc as
|
|
816
|
+
import { defineDoc as defineDoc3, defineExtension as defineExtension5, defineTool as defineTool4 } from "@earendil-works/pi-durable";
|
|
776
817
|
import { Type as Type5 } from "typebox";
|
|
777
|
-
var TodoDoc =
|
|
818
|
+
var TodoDoc = defineDoc3({
|
|
778
819
|
kind: "todo.list",
|
|
779
820
|
version: 1,
|
|
780
821
|
scope: "conversation",
|
|
@@ -817,7 +858,7 @@ import { basename, join as join3, resolve } from "node:path";
|
|
|
817
858
|
import { clampThinkingLevel as clampThinkingLevel2 } from "@earendil-works/pi-ai";
|
|
818
859
|
import {
|
|
819
860
|
AgentDoc as AgentDoc2,
|
|
820
|
-
defineDoc as
|
|
861
|
+
defineDoc as defineDoc4
|
|
821
862
|
} from "@earendil-works/pi-durable";
|
|
822
863
|
|
|
823
864
|
// apps/host/src/checkout.ts
|
|
@@ -848,27 +889,6 @@ async function gitBase(projectPath) {
|
|
|
848
889
|
return;
|
|
849
890
|
return { repo, subdir: relative(repo, projectPath), base };
|
|
850
891
|
}
|
|
851
|
-
var SNAPSHOT_IDENTITY = {
|
|
852
|
-
GIT_AUTHOR_NAME: "PiNomad",
|
|
853
|
-
GIT_AUTHOR_EMAIL: "pinomad@localhost",
|
|
854
|
-
GIT_COMMITTER_NAME: "PiNomad",
|
|
855
|
-
GIT_COMMITTER_EMAIL: "pinomad@localhost"
|
|
856
|
-
};
|
|
857
|
-
async function snapshotOf(sourceDir) {
|
|
858
|
-
const indexFile = join2(tmpdir(), `pinomad-index-${randomBytes(8).toString("hex")}`);
|
|
859
|
-
const env = { GIT_INDEX_FILE: indexFile };
|
|
860
|
-
try {
|
|
861
|
-
const { stdout: head } = await git(sourceDir, ["rev-parse", "--verify", "HEAD"], env);
|
|
862
|
-
const base = head.trim();
|
|
863
|
-
await git(sourceDir, ["read-tree", base], env);
|
|
864
|
-
await git(sourceDir, ["add", "-A"], env);
|
|
865
|
-
const { stdout: tree } = await git(sourceDir, ["write-tree"], env);
|
|
866
|
-
const { stdout: commit } = await git(sourceDir, ["commit-tree", tree.trim(), "-p", base, "-m", "pinomad fork snapshot"], { ...env, ...SNAPSHOT_IDENTITY });
|
|
867
|
-
return { base, snapshot: commit.trim() };
|
|
868
|
-
} finally {
|
|
869
|
-
await rm(indexFile, { force: true });
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
892
|
var repoLocks = new Map;
|
|
873
893
|
function locked(repo, work) {
|
|
874
894
|
const next = (repoLocks.get(repo) ?? Promise.resolve()).then(work, work);
|
|
@@ -932,7 +952,7 @@ async function changesOf(dir, base, maxBytes = 1024 * 1024) {
|
|
|
932
952
|
}
|
|
933
953
|
|
|
934
954
|
// apps/host/src/organization.ts
|
|
935
|
-
var IndexDoc =
|
|
955
|
+
var IndexDoc = defineDoc4({
|
|
936
956
|
kind: "pinomad.index",
|
|
937
957
|
version: 1,
|
|
938
958
|
scope: "session",
|
|
@@ -1056,21 +1076,42 @@ async function archive(harness, id, archived, context) {
|
|
|
1056
1076
|
entry.archived = archived;
|
|
1057
1077
|
}, context);
|
|
1058
1078
|
}
|
|
1059
|
-
|
|
1079
|
+
var forkFlags = new WeakMap;
|
|
1080
|
+
async function forkFlagOf(harness, id, context) {
|
|
1081
|
+
const cached = forkFlags.get(harness)?.get(id);
|
|
1082
|
+
if (cached !== undefined)
|
|
1083
|
+
return cached;
|
|
1084
|
+
const record = await harness.commit((tx) => tx.conversation(id), context);
|
|
1085
|
+
if (record === undefined)
|
|
1086
|
+
return false;
|
|
1087
|
+
let flags = forkFlags.get(harness);
|
|
1088
|
+
if (flags === undefined)
|
|
1089
|
+
forkFlags.set(harness, flags = new Map);
|
|
1090
|
+
flags.set(id, record.parent !== undefined);
|
|
1091
|
+
return record.parent !== undefined;
|
|
1092
|
+
}
|
|
1093
|
+
async function checkoutInfo(harness, input, context) {
|
|
1060
1094
|
const agent = await input.read.snapshot(AgentDoc2, input.conversationId, context);
|
|
1061
1095
|
const cwd = agent?.cwd;
|
|
1062
1096
|
if (cwd === undefined)
|
|
1063
1097
|
return;
|
|
1064
|
-
const index = await
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1098
|
+
const [index, fork] = await Promise.all([
|
|
1099
|
+
input.read.snapshot(IndexDoc, context),
|
|
1100
|
+
forkFlagOf(harness, input.conversationId, context)
|
|
1101
|
+
]);
|
|
1102
|
+
const checkout = checkoutAt(index?.checkouts, cwd);
|
|
1068
1103
|
return {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1104
|
+
fork,
|
|
1105
|
+
...checkout === undefined ? {} : {
|
|
1106
|
+
checkout: {
|
|
1107
|
+
branch: checkout.branch,
|
|
1108
|
+
base: checkout.base,
|
|
1109
|
+
projectPath: join3(checkout.repo, checkout.subdir),
|
|
1110
|
+
repo: checkout.repo,
|
|
1111
|
+
worktreeRoot: checkout.path,
|
|
1112
|
+
own: checkout.conversationId === input.conversationId
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1074
1115
|
};
|
|
1075
1116
|
}
|
|
1076
1117
|
async function cleanupArchivedWorktrees(harness, rootId, context) {
|
|
@@ -1409,15 +1450,43 @@ import lockfile from "proper-lockfile";
|
|
|
1409
1450
|
// apps/host/src/gateway.ts
|
|
1410
1451
|
import { createServer } from "node:http";
|
|
1411
1452
|
import { networkInterfaces } from "node:os";
|
|
1412
|
-
import { join as join4,
|
|
1453
|
+
import { join as join4, sep as sep3 } from "node:path";
|
|
1413
1454
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
1414
1455
|
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1415
1456
|
import {
|
|
1416
|
-
AgentDoc as AgentDoc3
|
|
1457
|
+
AgentDoc as AgentDoc3,
|
|
1458
|
+
LiveDoc
|
|
1417
1459
|
} from "@earendil-works/pi-durable";
|
|
1418
1460
|
import { WebSocket as WebSocket2, WebSocketServer } from "ws";
|
|
1419
1461
|
import { Value as Value2 } from "typebox/value";
|
|
1420
1462
|
|
|
1463
|
+
// packages/protocol/src/tool-target.ts
|
|
1464
|
+
var TARGET_KEYS = [
|
|
1465
|
+
"path",
|
|
1466
|
+
"file_path",
|
|
1467
|
+
"filePath",
|
|
1468
|
+
"command",
|
|
1469
|
+
"cmd",
|
|
1470
|
+
"query",
|
|
1471
|
+
"pattern",
|
|
1472
|
+
"url",
|
|
1473
|
+
"name",
|
|
1474
|
+
"description",
|
|
1475
|
+
"task"
|
|
1476
|
+
];
|
|
1477
|
+
var TARGET_MAX_LENGTH = 120;
|
|
1478
|
+
function toolTarget(args) {
|
|
1479
|
+
if (typeof args !== "object" || args === null)
|
|
1480
|
+
return;
|
|
1481
|
+
for (const key of TARGET_KEYS) {
|
|
1482
|
+
const value = args[key];
|
|
1483
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1484
|
+
return value.length > TARGET_MAX_LENGTH ? `${value.slice(0, TARGET_MAX_LENGTH)}…` : value;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1421
1490
|
// apps/host/src/relay-link.ts
|
|
1422
1491
|
import { WebSocket } from "ws";
|
|
1423
1492
|
function relayWsBase(origin) {
|
|
@@ -1446,6 +1515,12 @@ function startRelayLink(options) {
|
|
|
1446
1515
|
let delay = minDelay;
|
|
1447
1516
|
let retryTimer;
|
|
1448
1517
|
let pendingAccepts = 0;
|
|
1518
|
+
function setState(next) {
|
|
1519
|
+
const changed = state.status !== next.status || state.status === "rejected" && next.status === "rejected" && state.reason !== next.reason;
|
|
1520
|
+
state = next;
|
|
1521
|
+
if (changed)
|
|
1522
|
+
options.onStateChange?.(next);
|
|
1523
|
+
}
|
|
1449
1524
|
const watched = new Set;
|
|
1450
1525
|
const ponged = new WeakMap;
|
|
1451
1526
|
const pingIntervalMs = options.pingIntervalMs ?? 30000;
|
|
@@ -1505,7 +1580,7 @@ function startRelayLink(options) {
|
|
|
1505
1580
|
break;
|
|
1506
1581
|
case "registered":
|
|
1507
1582
|
delay = minDelay;
|
|
1508
|
-
|
|
1583
|
+
setState({ status: "registered" });
|
|
1509
1584
|
break;
|
|
1510
1585
|
case "incoming": {
|
|
1511
1586
|
if (pendingAccepts >= maxPendingAccepts)
|
|
@@ -1532,14 +1607,12 @@ function startRelayLink(options) {
|
|
|
1532
1607
|
const rejection = REJECTION_REASONS[code];
|
|
1533
1608
|
if (rejection !== undefined) {
|
|
1534
1609
|
const reason = `relay ${origin}: ${rejection(hostId)} (${code})`;
|
|
1535
|
-
|
|
1536
|
-
options.onRejected?.(reason);
|
|
1537
|
-
state = { status: "rejected", reason };
|
|
1610
|
+
setState({ status: "rejected", reason });
|
|
1538
1611
|
delay = maxDelay;
|
|
1539
1612
|
reconnect(maxDelay);
|
|
1540
1613
|
return;
|
|
1541
1614
|
}
|
|
1542
|
-
|
|
1615
|
+
setState({ status: "connecting" });
|
|
1543
1616
|
reconnect(delay);
|
|
1544
1617
|
delay = Math.min(maxDelay, delay * 2);
|
|
1545
1618
|
}
|
|
@@ -1562,8 +1635,20 @@ function startRelayLink(options) {
|
|
|
1562
1635
|
|
|
1563
1636
|
// apps/host/src/gateway.ts
|
|
1564
1637
|
var context = BACKGROUND_CONTEXT;
|
|
1638
|
+
var forkTails = new WeakMap;
|
|
1639
|
+
function serializedFork(harness, work) {
|
|
1640
|
+
const next = (forkTails.get(harness) ?? Promise.resolve()).then(work, work);
|
|
1641
|
+
const settled = next.then(() => {}, () => {});
|
|
1642
|
+
forkTails.set(harness, settled);
|
|
1643
|
+
settled.then(() => {
|
|
1644
|
+
if (forkTails.get(harness) === settled)
|
|
1645
|
+
forkTails.delete(harness);
|
|
1646
|
+
});
|
|
1647
|
+
return next;
|
|
1648
|
+
}
|
|
1649
|
+
var LOOPBACK_SECURE_PATH = "/secure";
|
|
1565
1650
|
async function startGateway(options) {
|
|
1566
|
-
const conversations = await ConversationList.open(options.harness);
|
|
1651
|
+
const conversations = await ConversationList.open(options.harness, options.docs);
|
|
1567
1652
|
const http = createServer((request, response) => {
|
|
1568
1653
|
serveWebClient(request, response, options.webRoot).catch(() => {
|
|
1569
1654
|
if (!response.headersSent)
|
|
@@ -1601,22 +1686,35 @@ async function startGateway(options) {
|
|
|
1601
1686
|
},
|
|
1602
1687
|
createPairing: () => {
|
|
1603
1688
|
const spec = options.secure;
|
|
1604
|
-
if (spec === undefined
|
|
1689
|
+
if (spec === undefined)
|
|
1605
1690
|
return;
|
|
1606
1691
|
const { secret, expiresAt } = spec.offers.create();
|
|
1607
1692
|
const pair = `pair=${toBase64Url(spec.hostKey.publicKey)}.${secret}`;
|
|
1608
1693
|
if (relay !== undefined) {
|
|
1609
|
-
const
|
|
1610
|
-
return { url: `${relay.origin}/#${pair}&url=${encodeURIComponent(
|
|
1694
|
+
const target2 = `${relayWsBase(relay.origin)}/c/${relay.hostId}`;
|
|
1695
|
+
return { url: `${relay.origin}/#${pair}&url=${encodeURIComponent(target2)}`, expiresAt };
|
|
1611
1696
|
}
|
|
1612
|
-
|
|
1697
|
+
if (remote !== undefined)
|
|
1698
|
+
return { url: `${remote.advertiseUrl}/#${pair}`, expiresAt };
|
|
1699
|
+
const target = `ws://127.0.0.1:${port}${LOOPBACK_SECURE_PATH}`;
|
|
1700
|
+
return { url: `http://127.0.0.1:${port}/#${pair}&url=${encodeURIComponent(target)}`, expiresAt };
|
|
1613
1701
|
}
|
|
1614
1702
|
});
|
|
1615
1703
|
clients.add(client);
|
|
1616
1704
|
};
|
|
1617
1705
|
server.on("connection", (socket, request) => {
|
|
1618
1706
|
socket.on("error", () => socket.terminate());
|
|
1619
|
-
const
|
|
1707
|
+
const url = new URL(request.url ?? "/", "ws://127.0.0.1");
|
|
1708
|
+
if (url.pathname === LOOPBACK_SECURE_PATH) {
|
|
1709
|
+
const secure2 = options.secure;
|
|
1710
|
+
if (secure2 === undefined) {
|
|
1711
|
+
socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
secureHandshake(socket, secure2, options.harness, register);
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
const token = url.searchParams.get("token");
|
|
1620
1718
|
const origin = request.headers.origin;
|
|
1621
1719
|
if (token !== options.token || origin !== undefined && !browserOrigins.includes(origin)) {
|
|
1622
1720
|
socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
@@ -1627,17 +1725,25 @@ async function startGateway(options) {
|
|
|
1627
1725
|
remote = await startRemote(options, register);
|
|
1628
1726
|
const secure = options.secure;
|
|
1629
1727
|
if (secure?.relay !== undefined) {
|
|
1630
|
-
|
|
1728
|
+
let link;
|
|
1729
|
+
link = startRelayLink({
|
|
1631
1730
|
origin: secure.relay.origin,
|
|
1632
1731
|
signingKey: secure.relay.signingKey,
|
|
1633
1732
|
accept: (socket) => secureHandshake(socket, secure, options.harness, register),
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1733
|
+
onStateChange: (state) => {
|
|
1734
|
+
if (state.status === "registered") {
|
|
1735
|
+
console.log(JSON.stringify({ event: "relay", status: "registered", origin: link.origin, hostId: link.hostId }));
|
|
1736
|
+
} else if (state.status === "connecting") {
|
|
1737
|
+
console.log(JSON.stringify({ event: "relay", status: "connecting", origin: link.origin }));
|
|
1738
|
+
} else {
|
|
1739
|
+
broadcast("warning", state.reason);
|
|
1740
|
+
console.error(state.reason);
|
|
1741
|
+
}
|
|
1637
1742
|
},
|
|
1638
1743
|
...secure.relay.reconnectDelayMs === undefined ? {} : { reconnectDelayMs: secure.relay.reconnectDelayMs },
|
|
1639
1744
|
...secure.relay.pingIntervalMs === undefined ? {} : { pingIntervalMs: secure.relay.pingIntervalMs }
|
|
1640
1745
|
});
|
|
1746
|
+
relay = link;
|
|
1641
1747
|
}
|
|
1642
1748
|
return {
|
|
1643
1749
|
url: `ws://127.0.0.1:${port}`,
|
|
@@ -1794,36 +1900,131 @@ async function startRemote(options, register) {
|
|
|
1794
1900
|
|
|
1795
1901
|
class ConversationList {
|
|
1796
1902
|
#listeners = new Set;
|
|
1797
|
-
#
|
|
1903
|
+
#questionTokens;
|
|
1904
|
+
#questionKinds;
|
|
1905
|
+
#records = new Map;
|
|
1906
|
+
#facts = new Map;
|
|
1907
|
+
#index = new Map;
|
|
1908
|
+
#list = [];
|
|
1798
1909
|
#unsubscribe = () => {};
|
|
1799
1910
|
#notifying = false;
|
|
1800
|
-
constructor(
|
|
1801
|
-
this.#
|
|
1911
|
+
constructor(docs) {
|
|
1912
|
+
this.#questionTokens = (docs ?? []).filter((doc) => doc.presentation === "pinomad.question").map((doc) => doc.token);
|
|
1913
|
+
this.#questionKinds = new Set(this.#questionTokens.map((token) => token.definition.kind));
|
|
1802
1914
|
}
|
|
1803
|
-
static async open(harness) {
|
|
1915
|
+
static async open(harness, docs) {
|
|
1916
|
+
const list = new ConversationList(docs);
|
|
1804
1917
|
const summaries = [];
|
|
1805
1918
|
let cursor;
|
|
1806
1919
|
do {
|
|
1807
1920
|
const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor), context);
|
|
1808
|
-
for (const record of page.items)
|
|
1809
|
-
|
|
1921
|
+
for (const record of page.items) {
|
|
1922
|
+
list.#records.set(record.id, record);
|
|
1923
|
+
const facts = await list.#load(harness, record);
|
|
1924
|
+
list.#facts.set(record.id, facts);
|
|
1925
|
+
list.#index.set(record.id, summaries.length);
|
|
1926
|
+
summaries.push(summaryOf(record, facts));
|
|
1927
|
+
}
|
|
1810
1928
|
cursor = page.next;
|
|
1811
1929
|
} while (cursor !== undefined);
|
|
1812
|
-
|
|
1813
|
-
list.#unsubscribe = harness.subscribeCommits((publication) =>
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1930
|
+
list.#list = summaries;
|
|
1931
|
+
list.#unsubscribe = harness.subscribeCommits((publication) => list.#observe(publication));
|
|
1932
|
+
return list;
|
|
1933
|
+
}
|
|
1934
|
+
async#load(harness, record) {
|
|
1935
|
+
const history = await historyFacts(harness, record.id);
|
|
1936
|
+
const facts = { pending: new Set, ...history };
|
|
1937
|
+
const live = await harness.snapshot(LiveDoc, record.id, context);
|
|
1938
|
+
facts.running = live?.run !== undefined;
|
|
1939
|
+
facts.tools = live?.tools;
|
|
1940
|
+
for (const token of this.#questionTokens) {
|
|
1941
|
+
const value = await harness.snapshot(token, record.id, context);
|
|
1942
|
+
if (Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined)) {
|
|
1943
|
+
facts.pending.add(token.definition.kind);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
if (record.owner !== undefined) {
|
|
1947
|
+
const label = (await harness.snapshot(SubagentDoc, record.id, context))?.label;
|
|
1948
|
+
if (typeof label === "string" && label !== "")
|
|
1949
|
+
facts.label = label;
|
|
1950
|
+
}
|
|
1951
|
+
return facts;
|
|
1952
|
+
}
|
|
1953
|
+
#observe(publication) {
|
|
1954
|
+
const touched = new Set;
|
|
1955
|
+
for (const change of publication.changes) {
|
|
1956
|
+
if (change.type === "conversation") {
|
|
1957
|
+
this.#records.set(change.value.id, change.value);
|
|
1958
|
+
touched.add(change.value.id);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
for (const change of publication.changes) {
|
|
1962
|
+
if (change.type === "entry") {
|
|
1963
|
+
const entry = change.value;
|
|
1964
|
+
if (entry.kind !== "pi.user" && entry.kind !== "pi.assistant")
|
|
1965
|
+
continue;
|
|
1966
|
+
const facts = this.#fact(entry.conversationId);
|
|
1967
|
+
if (entry.kind === "pi.user" && facts.title === undefined)
|
|
1968
|
+
Object.assign(facts, titleOf(entry));
|
|
1969
|
+
if (facts.newest !== undefined && entry.id <= facts.newest)
|
|
1970
|
+
continue;
|
|
1971
|
+
touched.add(entry.conversationId);
|
|
1972
|
+
facts.newest = entry.id;
|
|
1973
|
+
const message = entry.model?.[0];
|
|
1974
|
+
facts.updatedAt = typeof message?.timestamp === "number" ? message.timestamp : undefined;
|
|
1975
|
+
facts.failed = entry.kind === "pi.assistant" && message?.role === "assistant" && message.stopReason === "error";
|
|
1976
|
+
facts.calls = entry.kind === "pi.assistant" ? callsOf(message) : undefined;
|
|
1977
|
+
} else if (change.type === "document") {
|
|
1978
|
+
const id = change.conversationId;
|
|
1979
|
+
if (id === undefined)
|
|
1980
|
+
continue;
|
|
1981
|
+
const facts = this.#fact(id);
|
|
1982
|
+
if (change.record.kind === "pi.live") {
|
|
1983
|
+
const live = change.value;
|
|
1984
|
+
facts.running = live?.run !== undefined;
|
|
1985
|
+
facts.tools = live?.tools;
|
|
1986
|
+
touched.add(id);
|
|
1987
|
+
} else if (this.#questionKinds.has(change.record.kind)) {
|
|
1988
|
+
const value = change.value;
|
|
1989
|
+
const pending = Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
|
|
1990
|
+
const next2 = new Set(facts.pending);
|
|
1991
|
+
if (pending)
|
|
1992
|
+
next2.add(change.record.kind);
|
|
1993
|
+
else
|
|
1994
|
+
next2.delete(change.record.kind);
|
|
1995
|
+
facts.pending = next2;
|
|
1996
|
+
touched.add(id);
|
|
1997
|
+
} else if (change.record.kind === SubagentDoc.definition.kind) {
|
|
1998
|
+
const label = change.value?.label;
|
|
1999
|
+
facts.label = typeof label === "string" && label !== "" ? label : undefined;
|
|
2000
|
+
touched.add(id);
|
|
1821
2001
|
}
|
|
1822
2002
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
2003
|
+
}
|
|
2004
|
+
let next;
|
|
2005
|
+
for (const id of touched) {
|
|
2006
|
+
const record = this.#records.get(id);
|
|
2007
|
+
if (record === undefined)
|
|
2008
|
+
continue;
|
|
2009
|
+
const summary = summaryOf(record, this.#fact(id));
|
|
2010
|
+
const index = this.#index.get(id);
|
|
2011
|
+
if (index === undefined) {
|
|
2012
|
+
next ??= this.#list.slice();
|
|
2013
|
+
this.#index.set(id, next.length);
|
|
2014
|
+
next.push(summary);
|
|
2015
|
+
} else if (!sameSummary(this.#list[index], summary)) {
|
|
2016
|
+
next ??= this.#list.slice();
|
|
2017
|
+
next[index] = summary;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
if (next !== undefined)
|
|
2021
|
+
this.#set(next);
|
|
2022
|
+
}
|
|
2023
|
+
#fact(id) {
|
|
2024
|
+
let facts = this.#facts.get(id);
|
|
2025
|
+
if (facts === undefined)
|
|
2026
|
+
this.#facts.set(id, facts = { pending: new Set });
|
|
2027
|
+
return facts;
|
|
1827
2028
|
}
|
|
1828
2029
|
get value() {
|
|
1829
2030
|
return this.#list;
|
|
@@ -1848,14 +2049,29 @@ class ConversationList {
|
|
|
1848
2049
|
this.#listeners.clear();
|
|
1849
2050
|
}
|
|
1850
2051
|
}
|
|
1851
|
-
function summaryOf(record) {
|
|
2052
|
+
function summaryOf(record, facts) {
|
|
1852
2053
|
const parent = record.parent?.conversationId ?? record.owner?.conversationId;
|
|
2054
|
+
const status = facts.pending.size > 0 ? "needs-answer" : facts.running === true ? "running" : facts.failed === true ? "failed" : undefined;
|
|
2055
|
+
const runningSlot = facts.running === true ? facts.tools?.find((slot) => slot.status === "running") : undefined;
|
|
2056
|
+
const activity = runningSlot === undefined ? undefined : {
|
|
2057
|
+
tool: runningSlot.name,
|
|
2058
|
+
...facts.calls?.get(runningSlot.callId) === undefined ? {} : { target: facts.calls.get(runningSlot.callId) }
|
|
2059
|
+
};
|
|
1853
2060
|
return {
|
|
1854
2061
|
id: record.id,
|
|
1855
2062
|
kind: record.owner !== undefined ? "subagent" : record.parent !== undefined ? "fork" : "conversation",
|
|
1856
|
-
...parent === undefined ? {} : { parent }
|
|
2063
|
+
...parent === undefined ? {} : { parent },
|
|
2064
|
+
...record.parent === undefined ? {} : { forkedAt: String(record.parent.at) },
|
|
2065
|
+
...facts.title === undefined ? {} : { title: facts.title },
|
|
2066
|
+
...status === undefined ? {} : { status },
|
|
2067
|
+
...facts.updatedAt === undefined ? {} : { updatedAt: facts.updatedAt },
|
|
2068
|
+
...facts.label === undefined ? {} : { label: facts.label },
|
|
2069
|
+
...activity === undefined ? {} : { activity }
|
|
1857
2070
|
};
|
|
1858
2071
|
}
|
|
2072
|
+
function sameSummary(a, b) {
|
|
2073
|
+
return a.id === b.id && a.kind === b.kind && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
|
|
2074
|
+
}
|
|
1859
2075
|
function titleOf(entry) {
|
|
1860
2076
|
const message = entry?.model?.[0];
|
|
1861
2077
|
if (message?.role !== "user")
|
|
@@ -1863,16 +2079,40 @@ function titleOf(entry) {
|
|
|
1863
2079
|
const text = typeof message.content === "string" ? message.content : message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join(" ");
|
|
1864
2080
|
return { title: text.replace(/\s+/g, " ").trim() };
|
|
1865
2081
|
}
|
|
1866
|
-
async function
|
|
2082
|
+
async function historyFacts(harness, id) {
|
|
1867
2083
|
const conversation = await harness.conversation(id, context);
|
|
1868
2084
|
let first;
|
|
2085
|
+
let newest;
|
|
1869
2086
|
let cursor;
|
|
1870
2087
|
do {
|
|
1871
2088
|
const page = await conversation.entries({}, 256, cursor, context);
|
|
1872
2089
|
first = page.items.findLast((entry) => entry.kind === "pi.user" && entry.conversationId === id) ?? first;
|
|
2090
|
+
newest ??= page.items.find((entry) => entry.conversationId === id && (entry.kind === "pi.user" || entry.kind === "pi.assistant"));
|
|
1873
2091
|
cursor = page.next;
|
|
1874
2092
|
} while (cursor !== undefined);
|
|
1875
|
-
|
|
2093
|
+
const message = newest?.model?.[0];
|
|
2094
|
+
return {
|
|
2095
|
+
...titleOf(first),
|
|
2096
|
+
...newest === undefined ? {} : {
|
|
2097
|
+
newest: newest.id,
|
|
2098
|
+
...typeof message?.timestamp === "number" ? { updatedAt: message.timestamp } : {},
|
|
2099
|
+
failed: newest.kind === "pi.assistant" && message?.role === "assistant" && message.stopReason === "error",
|
|
2100
|
+
...newest.kind === "pi.assistant" ? { calls: callsOf(message) } : {}
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
function callsOf(message) {
|
|
2105
|
+
if (message?.role !== "assistant")
|
|
2106
|
+
return;
|
|
2107
|
+
const content = message.content;
|
|
2108
|
+
if (!Array.isArray(content))
|
|
2109
|
+
return;
|
|
2110
|
+
const calls = new Map;
|
|
2111
|
+
for (const block of content) {
|
|
2112
|
+
if (block?.type === "toolCall" && typeof block.id === "string")
|
|
2113
|
+
calls.set(block.id, toolTarget(block.arguments));
|
|
2114
|
+
}
|
|
2115
|
+
return calls.size === 0 ? undefined : calls;
|
|
1876
2116
|
}
|
|
1877
2117
|
|
|
1878
2118
|
class GatewayClient {
|
|
@@ -1891,24 +2131,38 @@ class GatewayClient {
|
|
|
1891
2131
|
this.devicePublicKey = hooks.devicePublicKey;
|
|
1892
2132
|
connection.handle({
|
|
1893
2133
|
message: (text) => {
|
|
1894
|
-
let
|
|
2134
|
+
let parsed;
|
|
1895
2135
|
try {
|
|
1896
|
-
|
|
2136
|
+
parsed = parseClientFrame(JSON.parse(text));
|
|
1897
2137
|
} catch {
|
|
1898
|
-
connection.close(
|
|
2138
|
+
connection.close(INVALID_FRAME_CLOSE_CODE, "Invalid client frame");
|
|
1899
2139
|
return;
|
|
1900
2140
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2141
|
+
switch (parsed.kind) {
|
|
2142
|
+
case "frame":
|
|
2143
|
+
this.#receive(parsed.frame).catch(() => connection.close(1011, "Gateway request failed"));
|
|
2144
|
+
return;
|
|
2145
|
+
case "unsupportedCall":
|
|
2146
|
+
this.send({ type: "result", id: parsed.id, ok: false, error: `Unsupported method: ${parsed.method}` });
|
|
2147
|
+
return;
|
|
2148
|
+
case "unsupportedStream":
|
|
2149
|
+
if (parsed.type === "subscribe") {
|
|
2150
|
+
this.send({ type: "ended", stream: parsed.stream, reason: "Unsupported stream" });
|
|
2151
|
+
}
|
|
2152
|
+
return;
|
|
2153
|
+
case "unknownType":
|
|
2154
|
+
return;
|
|
2155
|
+
case "malformed":
|
|
2156
|
+
connection.close(INVALID_FRAME_CLOSE_CODE, "Invalid client frame");
|
|
2157
|
+
return;
|
|
1904
2158
|
}
|
|
1905
|
-
this.#receive(frame).catch(() => connection.close(1011, "Gateway request failed"));
|
|
1906
2159
|
},
|
|
1907
2160
|
closed: () => hooks.closed(this)
|
|
1908
2161
|
});
|
|
1909
2162
|
this.send({
|
|
1910
2163
|
type: "hello",
|
|
1911
|
-
protocol:
|
|
2164
|
+
protocol: PROTOCOL,
|
|
2165
|
+
...packagedVersion === undefined ? {} : { hostVersion: packagedVersion },
|
|
1912
2166
|
session: options.session,
|
|
1913
2167
|
models: options.modelSummaries(),
|
|
1914
2168
|
defaults: options.defaults,
|
|
@@ -2162,15 +2416,34 @@ class GatewayClient {
|
|
|
2162
2416
|
}
|
|
2163
2417
|
case "fork": {
|
|
2164
2418
|
const { entryId, removeTools = [] } = args;
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2419
|
+
return await serializedFork(this.#options.harness, async () => {
|
|
2420
|
+
const at = Number(entryId);
|
|
2421
|
+
await this.#options.harness.commit(async (tx) => {
|
|
2422
|
+
const record = await tx.conversation(conversation.id);
|
|
2423
|
+
if (record === undefined)
|
|
2424
|
+
throw new Error(`Conversation ${conversation.id} does not exist`);
|
|
2425
|
+
if (record.parent !== undefined || record.owner !== undefined) {
|
|
2426
|
+
throw new Error("Only top-level conversations can be forked");
|
|
2427
|
+
}
|
|
2428
|
+
let cursor;
|
|
2429
|
+
do {
|
|
2430
|
+
const page = await tx.scanConversations({}, 256, cursor);
|
|
2431
|
+
for (const candidate of page.items) {
|
|
2432
|
+
if (candidate.parent?.conversationId === conversation.id && candidate.parent.at === at) {
|
|
2433
|
+
throw new Error("This message already has a fork");
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
cursor = page.next;
|
|
2437
|
+
} while (cursor !== undefined);
|
|
2438
|
+
}, context);
|
|
2439
|
+
const agent = await conversation.agent(context);
|
|
2440
|
+
const remove = agent.tools.filter((tool) => removeTools.includes(tool.name));
|
|
2441
|
+
const fork = await conversation.fork(at, {
|
|
2442
|
+
ownership: { kind: "ownerless" },
|
|
2443
|
+
...remove.length === 0 ? {} : { agent: { tools: { remove } } }
|
|
2444
|
+
}, context);
|
|
2445
|
+
return { conversationId: fork.id };
|
|
2446
|
+
});
|
|
2174
2447
|
}
|
|
2175
2448
|
case "answer": {
|
|
2176
2449
|
const { kind, requestId, answers } = args;
|
|
@@ -2254,41 +2527,6 @@ class GatewayClient {
|
|
|
2254
2527
|
return { available: false, reason: "Not in a git repository" };
|
|
2255
2528
|
return { available: true, ...await changesOf(base.repo, base.base) };
|
|
2256
2529
|
}
|
|
2257
|
-
async#forkCheckout(cwd) {
|
|
2258
|
-
if (cwd === undefined)
|
|
2259
|
-
return;
|
|
2260
|
-
const index = await this.#options.harness.snapshot(IndexDoc, context);
|
|
2261
|
-
const existing = checkoutAt(index?.checkouts, cwd);
|
|
2262
|
-
let source;
|
|
2263
|
-
if (existing !== undefined) {
|
|
2264
|
-
source = { repo: existing.repo, dir: existing.path, subdir: relative2(existing.path, cwd) };
|
|
2265
|
-
} else {
|
|
2266
|
-
if (cwd.startsWith(this.#chatsPrefix))
|
|
2267
|
-
return;
|
|
2268
|
-
const base2 = await gitBase(cwd);
|
|
2269
|
-
if (base2 === undefined)
|
|
2270
|
-
return;
|
|
2271
|
-
source = { repo: base2.repo, dir: base2.repo, subdir: base2.subdir };
|
|
2272
|
-
}
|
|
2273
|
-
const { base, snapshot } = await snapshotOf(source.dir);
|
|
2274
|
-
const suffix = branchSuffix();
|
|
2275
|
-
return async (tx, id) => {
|
|
2276
|
-
const path = worktreePath(this.#options.dataDir, id);
|
|
2277
|
-
const doc = await tx.doc(IndexDoc);
|
|
2278
|
-
if (doc.checkouts === undefined)
|
|
2279
|
-
doc.checkouts = [];
|
|
2280
|
-
doc.checkouts.push({
|
|
2281
|
-
conversationId: id,
|
|
2282
|
-
path,
|
|
2283
|
-
repo: source.repo,
|
|
2284
|
-
subdir: source.subdir,
|
|
2285
|
-
branch: worktreeBranch(id, suffix),
|
|
2286
|
-
base,
|
|
2287
|
-
snapshot
|
|
2288
|
-
});
|
|
2289
|
-
(await tx.doc(AgentDoc3, id)).cwd = join4(path, source.subdir);
|
|
2290
|
-
};
|
|
2291
|
-
}
|
|
2292
2530
|
async#agent(id) {
|
|
2293
2531
|
return await this.#options.harness.snapshot(AgentDoc3, id, context) ?? {};
|
|
2294
2532
|
}
|
|
@@ -2333,7 +2571,14 @@ async function openHost(options) {
|
|
|
2333
2571
|
const reports = [];
|
|
2334
2572
|
let report = (error) => void reports.push(error);
|
|
2335
2573
|
const registry = createRegistry();
|
|
2336
|
-
const extensions = typeof options.extensions === "function" ? options.extensions({
|
|
2574
|
+
const extensions = typeof options.extensions === "function" ? options.extensions({
|
|
2575
|
+
scriptTools: () => mcp?.scriptTools() ?? [],
|
|
2576
|
+
harness: () => {
|
|
2577
|
+
if (harness === undefined)
|
|
2578
|
+
throw new Error("harness is not open yet");
|
|
2579
|
+
return harness;
|
|
2580
|
+
}
|
|
2581
|
+
}) : options.extensions ?? [];
|
|
2337
2582
|
for (const { extension } of extensions)
|
|
2338
2583
|
registry.install(extension);
|
|
2339
2584
|
mcp = options.mcpConfig === undefined ? undefined : startMcp({ configPath: options.mcpConfig, registry });
|
|
@@ -2372,9 +2617,8 @@ async function openHost(options) {
|
|
|
2372
2617
|
await ensureDevices(harness, context2);
|
|
2373
2618
|
for (const path of options.projects ?? [])
|
|
2374
2619
|
await addProject(harness, path, context2);
|
|
2375
|
-
const
|
|
2376
|
-
const
|
|
2377
|
-
const offers = remoteAccess ? pairingOffers(options.remote?.pairingTtlMs) : undefined;
|
|
2620
|
+
const hostKey = await loadHostKey(options.dataDir);
|
|
2621
|
+
const offers = pairingOffers(options.remote?.pairingTtlMs);
|
|
2378
2622
|
const relayKey = options.relay === undefined ? undefined : await loadRelayKey(options.dataDir);
|
|
2379
2623
|
const defaults = initialModel === undefined ? {} : {
|
|
2380
2624
|
model: { provider: initialModel.provider, modelId: initialModel.modelId },
|
|
@@ -2394,24 +2638,22 @@ async function openHost(options) {
|
|
|
2394
2638
|
toolPresentations: Object.assign({}, ...extensions.map((extension) => extension.tools ?? {})),
|
|
2395
2639
|
...mcp === undefined ? {} : { mcp: mcp.status },
|
|
2396
2640
|
...options.webRoot === undefined ? {} : { webRoot: options.webRoot },
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
...options.relay.pingIntervalMs === undefined ? {} : { pingIntervalMs: options.relay.pingIntervalMs }
|
|
2414
|
-
}
|
|
2641
|
+
secure: {
|
|
2642
|
+
hostKey,
|
|
2643
|
+
offers,
|
|
2644
|
+
...options.remote?.handshakeTimeoutMs === undefined ? {} : { handshakeTimeoutMs: options.remote.handshakeTimeoutMs },
|
|
2645
|
+
...options.remote === undefined ? {} : {
|
|
2646
|
+
lan: {
|
|
2647
|
+
port: options.remote.port,
|
|
2648
|
+
...options.remote.publicUrl === undefined ? {} : { publicUrl: options.remote.publicUrl }
|
|
2649
|
+
}
|
|
2650
|
+
},
|
|
2651
|
+
...options.relay === undefined || relayKey === undefined ? {} : {
|
|
2652
|
+
relay: {
|
|
2653
|
+
origin: options.relay.origin,
|
|
2654
|
+
signingKey: relayKey,
|
|
2655
|
+
...options.relay.reconnectDelayMs === undefined ? {} : { reconnectDelayMs: options.relay.reconnectDelayMs },
|
|
2656
|
+
...options.relay.pingIntervalMs === undefined ? {} : { pingIntervalMs: options.relay.pingIntervalMs }
|
|
2415
2657
|
}
|
|
2416
2658
|
}
|
|
2417
2659
|
}
|
|
@@ -2584,7 +2826,10 @@ async function main(argv) {
|
|
|
2584
2826
|
const dataDir = resolve3(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
2585
2827
|
const extensionsFor = (models, summaries) => (host2) => {
|
|
2586
2828
|
const others = [
|
|
2587
|
-
createContext({
|
|
2829
|
+
createContext({
|
|
2830
|
+
agentsHome: join6(homedir2(), ".agents"),
|
|
2831
|
+
checkout: (input, context3) => checkoutInfo(host2.harness(), input, context3)
|
|
2832
|
+
}),
|
|
2588
2833
|
coding,
|
|
2589
2834
|
todo,
|
|
2590
2835
|
question,
|
|
@@ -2656,7 +2901,7 @@ async function main(argv) {
|
|
|
2656
2901
|
const options = values.faux !== undefined ? fauxOptions(() => () => fauxAssistantMessage(values.faux)) : await piOptions();
|
|
2657
2902
|
const host = await openHost(options);
|
|
2658
2903
|
const model = options.initialModel === undefined ? null : `${options.initialModel.provider}/${options.initialModel.modelId}`;
|
|
2659
|
-
const web = "http://127.0.0.1:5199/";
|
|
2904
|
+
const web = packagedVersion === undefined ? "http://127.0.0.1:5199/" : new URL(host.url.replace(/^ws/, "http")).href;
|
|
2660
2905
|
console.log(JSON.stringify({
|
|
2661
2906
|
event: "ready",
|
|
2662
2907
|
url: host.url,
|