toyon 0.10.0 → 0.11.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/dist/cli.js +23 -23
- package/dist/daemon.js +1481 -440
- package/dist/shell/assets/{Editor-DL0q1Bu5.js → Editor-Cs_OeWSx.js} +3 -3
- package/dist/shell/assets/XTerm-Dc6Bk52B.js +58 -0
- package/dist/shell/assets/{cssMode-Et22hXpa.js → cssMode-D8uDYrHJ.js} +1 -1
- package/dist/shell/assets/{freemarker2-Clz20PcM.js → freemarker2-DlU9uDzS.js} +1 -1
- package/dist/shell/assets/{grammar-CPvdB3az.js → grammar-BSzyJU-q.js} +1 -1
- package/dist/shell/assets/{handlebars-B4nvXAx8.js → handlebars-B66kGi_7.js} +1 -1
- package/dist/shell/assets/{html-DPIGgBLK.js → html-D6juOeyd.js} +1 -1
- package/dist/shell/assets/{htmlMode-C2LaVuEX.js → htmlMode-CbTT-4ZV.js} +1 -1
- package/dist/shell/assets/index-B4KJ--q9.css +1 -0
- package/dist/shell/assets/index-Cj-6Xixu.js +268 -0
- package/dist/shell/assets/{javascript-CnwJfTbN.js → javascript-CHwG4FvH.js} +1 -1
- package/dist/shell/assets/{jsonMode-8znCTiqR.js → jsonMode-DzTEiaKX.js} +1 -1
- package/dist/shell/assets/{liquid-Bdm-1CUh.js → liquid-wddZX4zw.js} +1 -1
- package/dist/shell/assets/{lspLanguageFeatures-B1DMU7yH.js → lspLanguageFeatures-BlqDZ6dO.js} +1 -1
- package/dist/shell/assets/{mdx-Ktcqo9Jf.js → mdx-Dlz2LIv9.js} +1 -1
- package/dist/shell/assets/{python-QX--d8LR.js → python-DvkArqZY.js} +1 -1
- package/dist/shell/assets/{razor-C0LrXteG.js → razor-bDLQ0jQf.js} +1 -1
- package/dist/shell/assets/{tsMode-BtYCp6db.js → tsMode-B_NomV_y.js} +1 -1
- package/dist/shell/assets/{xml-BXbaQ9MG.js → xml-CBYJrQiV.js} +1 -1
- package/dist/shell/assets/{yaml-C6FhaU_C.js → yaml-DnmVK8S2.js} +1 -1
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/XTerm-C9YH4Lh2.js +0 -58
- package/dist/shell/assets/index-2QusTgw-.js +0 -262
- package/dist/shell/assets/index-TaJLoLSd.css +0 -1
package/dist/daemon.js
CHANGED
|
@@ -4,6 +4,14 @@ import { rmSync as rmSync8, writeFileSync as writeFileSync10 } from "fs";
|
|
|
4
4
|
import { dirname as dirname12 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
|
|
7
|
+
// ../shared/src/protocol/events.ts
|
|
8
|
+
var SHELL_TOOL = "shell";
|
|
9
|
+
var CHECK_TOOL = "check";
|
|
10
|
+
var TOOL_SEARCH = "ToolSearch";
|
|
11
|
+
var ASK_TOOL = "AskUserQuestion";
|
|
12
|
+
var TOYON_MCP_SERVER = "toyon";
|
|
13
|
+
var HANDOFF_TOOL = "handoff";
|
|
14
|
+
|
|
7
15
|
// ../shared/src/agent-tools.ts
|
|
8
16
|
var EDIT_KINDS = new Set(["edit", "delete", "move", "execute"]);
|
|
9
17
|
var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "Bash"]);
|
|
@@ -16,6 +24,9 @@ var WRITTEN_KINDS = ["read", "edit", "delete", "move", "search", "execute", "fet
|
|
|
16
24
|
function isWrittenKind(kind) {
|
|
17
25
|
return WRITTEN_KINDS.includes(kind ?? "");
|
|
18
26
|
}
|
|
27
|
+
function writingCall(call, input) {
|
|
28
|
+
return (isWrittenKind(call.kind) || call.name === ASK_TOOL) && emptyInput(input);
|
|
29
|
+
}
|
|
19
30
|
function emptyInput(input) {
|
|
20
31
|
if (!input || typeof input !== "object")
|
|
21
32
|
return true;
|
|
@@ -271,6 +282,11 @@ function configSibling(rel, kind) {
|
|
|
271
282
|
return place[kind];
|
|
272
283
|
}
|
|
273
284
|
|
|
285
|
+
// ../shared/src/pair.ts
|
|
286
|
+
var PAIR_TTL_MS = 120000;
|
|
287
|
+
var pairLink = (host, code) => `https://${host}/#pair=${code}`;
|
|
288
|
+
var isTailnetName = (host) => host.endsWith(".ts.net");
|
|
289
|
+
|
|
274
290
|
// ../shared/src/daemon.ts
|
|
275
291
|
var DAEMON_FILES = {
|
|
276
292
|
token: "token",
|
|
@@ -280,8 +296,18 @@ var DAEMON_FILES = {
|
|
|
280
296
|
log: "daemon.log",
|
|
281
297
|
remote: "remote.json"
|
|
282
298
|
};
|
|
299
|
+
function machineLabel(hostname, remote) {
|
|
300
|
+
const of = (name) => name.split(".")[0]?.toLowerCase() ?? "";
|
|
301
|
+
if (remote && isTailnetName(remote.host))
|
|
302
|
+
return of(remote.host) || of(hostname);
|
|
303
|
+
return of(hostname) || "toyon";
|
|
304
|
+
}
|
|
305
|
+
var PREVIEW_GRANT_PARAM = "toyon_grant";
|
|
283
306
|
var PREVIEW_PORTS = { from: 10001, to: 10008 };
|
|
284
307
|
var DNS_NAME = /^(?=.{1,253}$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
308
|
+
function isLoopbackHost(host) {
|
|
309
|
+
return host === "127.0.0.1" || host === "::1" || host === "[::1]" || host === "localhost" || host.endsWith(".localhost");
|
|
310
|
+
}
|
|
285
311
|
function isRemoteHost(name) {
|
|
286
312
|
return DNS_NAME.test(name) && !/\.\d+$/.test(name) && name !== "localhost" && !name.endsWith(".localhost");
|
|
287
313
|
}
|
|
@@ -6486,13 +6512,8 @@ function clipOutput(text) {
|
|
|
6486
6512
|
${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
|
|
6487
6513
|
}
|
|
6488
6514
|
|
|
6489
|
-
// ../shared/src/pair.ts
|
|
6490
|
-
var PAIR_TTL_MS = 120000;
|
|
6491
|
-
var pairLink = (host, code) => `https://${host}/#pair=${code}`;
|
|
6492
|
-
var isTailnetName = (host) => host.endsWith(".ts.net");
|
|
6493
|
-
|
|
6494
6515
|
// ../shared/src/protocol/limits.ts
|
|
6495
|
-
var PROTOCOL_VERSION =
|
|
6516
|
+
var PROTOCOL_VERSION = 93;
|
|
6496
6517
|
var FILE_MAX_CHARS = 5000000;
|
|
6497
6518
|
var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
6498
6519
|
var IMAGE_MAX_EDGE = 2576;
|
|
@@ -6565,11 +6586,6 @@ var elementTraitsSchema = object({
|
|
|
6565
6586
|
attrs: array(tuple([string2().max(40), string2().max(200)])).max(12)
|
|
6566
6587
|
});
|
|
6567
6588
|
|
|
6568
|
-
// ../shared/src/protocol/events.ts
|
|
6569
|
-
var SHELL_TOOL = "shell";
|
|
6570
|
-
var CHECK_TOOL = "check";
|
|
6571
|
-
var TOOL_SEARCH = "ToolSearch";
|
|
6572
|
-
|
|
6573
6589
|
// ../shared/src/protocol/ws.ts
|
|
6574
6590
|
var id = string2().min(1).max(200);
|
|
6575
6591
|
var permissionModeSchema = _enum(["auto", "ask", "plan"]);
|
|
@@ -6922,6 +6938,14 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6922
6938
|
answers: array(askAnswerSchema).max(8).optional()
|
|
6923
6939
|
}),
|
|
6924
6940
|
object({ t: literal("agent-decide"), worktreeId: id, askId: string2().max(64), choiceId: string2().max(200) }),
|
|
6941
|
+
object({
|
|
6942
|
+
t: literal("handoff-answer"),
|
|
6943
|
+
worktreeId: id,
|
|
6944
|
+
id: string2().max(64),
|
|
6945
|
+
go: boolean2(),
|
|
6946
|
+
note: string2().max(5000).optional()
|
|
6947
|
+
}),
|
|
6948
|
+
object({ t: literal("handoff-ask"), worktreeId: id, repoId: id, text: prompt.optional() }),
|
|
6925
6949
|
object({ t: literal("agent-logout"), agent: id }),
|
|
6926
6950
|
object({ t: literal("agent-config"), agent: id, repoId: id.optional() }),
|
|
6927
6951
|
object({ t: literal("reveal-agent-file"), agent: id, file: string2().max(64), repoId: id.optional() }),
|
|
@@ -8220,7 +8244,7 @@ function safeUser() {
|
|
|
8220
8244
|
// ../daemon/package.json
|
|
8221
8245
|
var package_default = {
|
|
8222
8246
|
name: "@toyon/daemon",
|
|
8223
|
-
version: "0.
|
|
8247
|
+
version: "0.11.0",
|
|
8224
8248
|
private: true,
|
|
8225
8249
|
type: "module",
|
|
8226
8250
|
main: "src/index.ts",
|
|
@@ -12415,12 +12439,24 @@ class AgentAccounts {
|
|
|
12415
12439
|
if (before.canLogout !== entry.canLogout || !same(before.status, entry.status))
|
|
12416
12440
|
this.onChange?.();
|
|
12417
12441
|
}
|
|
12442
|
+
observeLimits(agentId, limits) {
|
|
12443
|
+
const before = this.known.get(agentId) ?? { canLogout: false, status: undefined };
|
|
12444
|
+
const entry = { ...before, limits };
|
|
12445
|
+
this.known.set(agentId, entry);
|
|
12446
|
+
if (!sameLimits(before.limits, limits))
|
|
12447
|
+
this.onChange?.();
|
|
12448
|
+
}
|
|
12418
12449
|
describe(infos) {
|
|
12419
12450
|
return infos.map((info) => {
|
|
12420
12451
|
const k = this.known.get(info.id);
|
|
12421
12452
|
if (!k)
|
|
12422
12453
|
return info;
|
|
12423
|
-
return {
|
|
12454
|
+
return {
|
|
12455
|
+
...info,
|
|
12456
|
+
...k.status ? { auth: k.status } : {},
|
|
12457
|
+
...k.canLogout ? { canLogout: true } : {},
|
|
12458
|
+
...k.limits ? { limits: k.limits } : {}
|
|
12459
|
+
};
|
|
12424
12460
|
});
|
|
12425
12461
|
}
|
|
12426
12462
|
logout(agentId) {
|
|
@@ -12464,6 +12500,12 @@ class AgentAccounts {
|
|
|
12464
12500
|
}
|
|
12465
12501
|
}
|
|
12466
12502
|
}
|
|
12503
|
+
function sameLimits(a, b) {
|
|
12504
|
+
if (!a || !b)
|
|
12505
|
+
return a === b;
|
|
12506
|
+
const win = (x) => x ? `${x.used}@${x.resetsAt}` : "";
|
|
12507
|
+
return a.status === b.status && a.binding === b.binding && win(a.windows.five_hour) === win(b.windows.five_hour) && win(a.windows.seven_day) === win(b.windows.seven_day);
|
|
12508
|
+
}
|
|
12467
12509
|
function same(a, b) {
|
|
12468
12510
|
if (!a || !b)
|
|
12469
12511
|
return a === b;
|
|
@@ -12477,13 +12519,17 @@ import { Readable, Writable } from "stream";
|
|
|
12477
12519
|
// ../daemon/src/runtime/kill.ts
|
|
12478
12520
|
var KILL_GRACE_MS = 3000;
|
|
12479
12521
|
var EXIT_GRACE_MS = 200;
|
|
12480
|
-
async function killGroup(pid, exited) {
|
|
12522
|
+
async function killGroup(pid, exited, graceMs = KILL_GRACE_MS) {
|
|
12481
12523
|
if (!signalGroup(pid, "SIGTERM"))
|
|
12482
12524
|
return;
|
|
12483
|
-
|
|
12484
|
-
|
|
12525
|
+
const deadline = Date.now() + graceMs;
|
|
12526
|
+
if (await within(exited, graceMs)) {
|
|
12527
|
+
await groupGone(pid, deadline);
|
|
12528
|
+
if (!groupAlive(pid))
|
|
12529
|
+
return "SIGTERM";
|
|
12530
|
+
}
|
|
12485
12531
|
signalGroup(pid, "SIGKILL");
|
|
12486
|
-
await within(exited, EXIT_GRACE_MS);
|
|
12532
|
+
await Promise.all([within(exited, EXIT_GRACE_MS), groupGone(pid, Date.now() + EXIT_GRACE_MS)]);
|
|
12487
12533
|
return "SIGKILL";
|
|
12488
12534
|
}
|
|
12489
12535
|
async function killProcessGroup(child) {
|
|
@@ -12518,8 +12564,7 @@ function groupAlive(pgid) {
|
|
|
12518
12564
|
return e.code !== "ESRCH";
|
|
12519
12565
|
}
|
|
12520
12566
|
}
|
|
12521
|
-
function groupGone(pgid) {
|
|
12522
|
-
const deadline = Date.now() + KILL_GRACE_MS + EXIT_GRACE_MS + 500;
|
|
12567
|
+
function groupGone(pgid, deadline = Date.now() + KILL_GRACE_MS + EXIT_GRACE_MS + 500) {
|
|
12523
12568
|
return new Promise((resolve) => {
|
|
12524
12569
|
const poll = () => {
|
|
12525
12570
|
if (!groupAlive(pgid) || Date.now() >= deadline)
|
|
@@ -12949,6 +12994,303 @@ class LinuxSandbox {
|
|
|
12949
12994
|
}
|
|
12950
12995
|
}
|
|
12951
12996
|
|
|
12997
|
+
// ../daemon/src/agent/mcp.ts
|
|
12998
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
12999
|
+
|
|
13000
|
+
// ../daemon/src/core/remote.ts
|
|
13001
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
13002
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
13003
|
+
function loadRemote(file, env = process.env, managed = MANAGED_DEFAULTS) {
|
|
13004
|
+
if (env.TOYON_CLOUD === "1") {
|
|
13005
|
+
const host = (env.TOYON_PUBLIC_HOST ?? "").toLowerCase();
|
|
13006
|
+
if (!isRemoteHost(host)) {
|
|
13007
|
+
throw new Error(`TOYON_CLOUD=1 needs TOYON_PUBLIC_HOST, the name the edge answers for; got "${host}"`);
|
|
13008
|
+
}
|
|
13009
|
+
const previews = env.TOYON_PREVIEWS ?? portPreviews(host);
|
|
13010
|
+
const why = checkPreviews(previews, host);
|
|
13011
|
+
if (why)
|
|
13012
|
+
throw new Error(`TOYON_PREVIEWS: ${why}`);
|
|
13013
|
+
return { host, previews, front: "edge" };
|
|
13014
|
+
}
|
|
13015
|
+
if (!existsSync(file))
|
|
13016
|
+
return null;
|
|
13017
|
+
if (managed.remote === "off") {
|
|
13018
|
+
log.warn("remote", `${file} is ignored: remote access is turned off by your organization's policy`);
|
|
13019
|
+
return null;
|
|
13020
|
+
}
|
|
13021
|
+
const view = parseRemote(readFileSync2(file, "utf8"));
|
|
13022
|
+
if (!view) {
|
|
13023
|
+
log.warn("remote", `${file} names no valid host or previews; remote access is off`);
|
|
13024
|
+
return null;
|
|
13025
|
+
}
|
|
13026
|
+
if (managed.remote === "tailscale" && !view.host.endsWith(".ts.net")) {
|
|
13027
|
+
log.warn("remote", `${file} names ${view.host}; your organization's policy allows a tailnet name only, so remote access is off`);
|
|
13028
|
+
return null;
|
|
13029
|
+
}
|
|
13030
|
+
return { ...view, front: "local" };
|
|
13031
|
+
}
|
|
13032
|
+
function sameSecret(given, want) {
|
|
13033
|
+
if (given === null)
|
|
13034
|
+
return false;
|
|
13035
|
+
const a = Buffer.from(given);
|
|
13036
|
+
const b = Buffer.from(want);
|
|
13037
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
13038
|
+
}
|
|
13039
|
+
var isLoopbackPeer = (ip) => ip === "127.0.0.1" || ip === "::1" || ip.startsWith("::ffff:127.");
|
|
13040
|
+
function requestedAuthority(req) {
|
|
13041
|
+
const host = (req.headers.get("host") ?? "").toLowerCase();
|
|
13042
|
+
if (host.includes(":"))
|
|
13043
|
+
return host;
|
|
13044
|
+
const port = req.headers.get("x-forwarded-port");
|
|
13045
|
+
return port && /^\d{1,5}$/.test(port) && port !== "443" ? `${host}:${port}` : host;
|
|
13046
|
+
}
|
|
13047
|
+
var forbidden = (body = "forbidden") => ({ kind: "refused", response: new Response(body, { status: 403 }) });
|
|
13048
|
+
function toHttps(req, host) {
|
|
13049
|
+
const url = new URL(req.url);
|
|
13050
|
+
const location = `https://${host}${url.pathname}${url.search}`;
|
|
13051
|
+
return {
|
|
13052
|
+
kind: "refused",
|
|
13053
|
+
response: new Response(`open ${location} instead`, {
|
|
13054
|
+
status: 302,
|
|
13055
|
+
headers: { location, "cache-control": "no-store" }
|
|
13056
|
+
})
|
|
13057
|
+
};
|
|
13058
|
+
}
|
|
13059
|
+
function door(req, peer, remote, listener) {
|
|
13060
|
+
if (remote === null && listener === "preview")
|
|
13061
|
+
return { kind: "local" };
|
|
13062
|
+
if (remote?.front !== "edge" && !isLoopbackPeer(peer)) {
|
|
13063
|
+
const typed = requestedAuthority(req).replace(/:80$/, "");
|
|
13064
|
+
const browsing = req.method === "GET" || req.method === "HEAD";
|
|
13065
|
+
return remote && listener === "daemon" && browsing && typed === remote.host ? toHttps(req, remote.host) : forbidden();
|
|
13066
|
+
}
|
|
13067
|
+
const authority = requestedAuthority(req);
|
|
13068
|
+
const host = authority.split(":")[0] ?? "";
|
|
13069
|
+
if (isLoopbackHost(host)) {
|
|
13070
|
+
return remote?.front === "edge" ? forbidden() : { kind: "local" };
|
|
13071
|
+
}
|
|
13072
|
+
if (remote === null)
|
|
13073
|
+
return forbidden();
|
|
13074
|
+
const preview = matchPreview(remote.previews, authority);
|
|
13075
|
+
let worktreeId = null;
|
|
13076
|
+
if (listener === "daemon") {
|
|
13077
|
+
if (preview !== null && "id" in preview)
|
|
13078
|
+
worktreeId = preview.id;
|
|
13079
|
+
else if (preview !== null || host !== remote.host)
|
|
13080
|
+
return forbidden();
|
|
13081
|
+
} else if (preview === null || "id" in preview) {
|
|
13082
|
+
return forbidden();
|
|
13083
|
+
}
|
|
13084
|
+
if (req.headers.get("x-forwarded-proto") !== "https") {
|
|
13085
|
+
return forbidden(`${host} reaches Toyon over https only; the front must terminate TLS`);
|
|
13086
|
+
}
|
|
13087
|
+
if (listener === "preview")
|
|
13088
|
+
return { kind: "preview", worktreeId: null };
|
|
13089
|
+
return worktreeId === null ? { kind: "shell" } : { kind: "preview", worktreeId };
|
|
13090
|
+
}
|
|
13091
|
+
var PREVIEW_COOKIE = "toyon_preview";
|
|
13092
|
+
function previewGrant(token) {
|
|
13093
|
+
return createHmac("sha256", token).update("toyon preview grant").digest("hex");
|
|
13094
|
+
}
|
|
13095
|
+
function grantCookie(grant, name, opts = {}) {
|
|
13096
|
+
const site = opts.partitioned ? "SameSite=None; Partitioned" : "SameSite=Strict";
|
|
13097
|
+
return `${PREVIEW_COOKIE}=${grant}; Domain=${name}; Path=/; Max-Age=34560000; HttpOnly; Secure; ${site}`;
|
|
13098
|
+
}
|
|
13099
|
+
function takeGrant(cookie, grant) {
|
|
13100
|
+
let ok = false;
|
|
13101
|
+
const kept = [];
|
|
13102
|
+
for (const part of (cookie ?? "").split(";")) {
|
|
13103
|
+
const pair = part.trim();
|
|
13104
|
+
if (pair === "")
|
|
13105
|
+
continue;
|
|
13106
|
+
const eq = pair.indexOf("=");
|
|
13107
|
+
if (eq > 0 && pair.slice(0, eq) === PREVIEW_COOKIE) {
|
|
13108
|
+
if (sameSecret(pair.slice(eq + 1), grant))
|
|
13109
|
+
ok = true;
|
|
13110
|
+
continue;
|
|
13111
|
+
}
|
|
13112
|
+
kept.push(pair);
|
|
13113
|
+
}
|
|
13114
|
+
return { ok, rest: kept.length ? kept.join("; ") : null };
|
|
13115
|
+
}
|
|
13116
|
+
function passPreview(req, gate) {
|
|
13117
|
+
const pass = takeGrant(req.headers.get("cookie"), gate.grant);
|
|
13118
|
+
const url = new URL(req.url);
|
|
13119
|
+
const code = url.searchParams.get(PREVIEW_GRANT_PARAM);
|
|
13120
|
+
if (code !== null) {
|
|
13121
|
+
url.searchParams.delete(PREVIEW_GRANT_PARAM);
|
|
13122
|
+
const location = `${url.pathname}${url.search}`;
|
|
13123
|
+
if (!pass.ok && !gate.redeem(code))
|
|
13124
|
+
return { ok: false, response: refusedPreview() };
|
|
13125
|
+
const headers = { location, "cache-control": "no-store" };
|
|
13126
|
+
if (!pass.ok) {
|
|
13127
|
+
const partitioned = req.headers.get("sec-fetch-site") === "cross-site";
|
|
13128
|
+
headers["set-cookie"] = grantCookie(gate.grant, gate.host, { partitioned });
|
|
13129
|
+
}
|
|
13130
|
+
return { ok: false, response: new Response(null, { status: 302, headers }) };
|
|
13131
|
+
}
|
|
13132
|
+
if (!pass.ok)
|
|
13133
|
+
return { ok: false, response: refusedPreview() };
|
|
13134
|
+
const headers = new Headers(req.headers);
|
|
13135
|
+
if (pass.rest === null)
|
|
13136
|
+
headers.delete("cookie");
|
|
13137
|
+
else
|
|
13138
|
+
headers.set("cookie", pass.rest);
|
|
13139
|
+
return { ok: true, req: new Request(req, { headers }) };
|
|
13140
|
+
}
|
|
13141
|
+
function refusedPreview() {
|
|
13142
|
+
return new Response(`<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="3"></head>` + `<body>This preview opens from Toyon; open your Toyon link first.</body></html>`, { status: 403, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } });
|
|
13143
|
+
}
|
|
13144
|
+
function setsGrant(line) {
|
|
13145
|
+
return line.slice(0, line.indexOf("=")).trim() === PREVIEW_COOKIE;
|
|
13146
|
+
}
|
|
13147
|
+
|
|
13148
|
+
// ../daemon/src/agent/mcp.ts
|
|
13149
|
+
class ToolSet {
|
|
13150
|
+
providers = [];
|
|
13151
|
+
add(provider) {
|
|
13152
|
+
this.providers.push(provider);
|
|
13153
|
+
}
|
|
13154
|
+
list(worktreeId) {
|
|
13155
|
+
return this.providers.map((p) => p.tool(worktreeId));
|
|
13156
|
+
}
|
|
13157
|
+
async call(worktreeId, name, args) {
|
|
13158
|
+
const provider = this.providers.find((p) => p.tool(worktreeId).name === name);
|
|
13159
|
+
if (!provider)
|
|
13160
|
+
return { text: `unknown tool ${name}`, isError: true };
|
|
13161
|
+
return provider.call(worktreeId, args);
|
|
13162
|
+
}
|
|
13163
|
+
}
|
|
13164
|
+
var PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26"];
|
|
13165
|
+
var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
|
|
13166
|
+
var BODY_MAX_BYTES = 64 * 1024;
|
|
13167
|
+
var PARSE_ERROR = -32700;
|
|
13168
|
+
var INVALID_PARAMS = -32602;
|
|
13169
|
+
var METHOD_NOT_FOUND = -32601;
|
|
13170
|
+
var INTERNAL_ERROR = -32603;
|
|
13171
|
+
var rpcError = (id, code, message) => ({
|
|
13172
|
+
jsonrpc: "2.0",
|
|
13173
|
+
id,
|
|
13174
|
+
error: { code, message }
|
|
13175
|
+
});
|
|
13176
|
+
var asRecord = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
13177
|
+
|
|
13178
|
+
class ToyonMcp {
|
|
13179
|
+
d;
|
|
13180
|
+
bearers = new Map;
|
|
13181
|
+
constructor(d) {
|
|
13182
|
+
this.d = d;
|
|
13183
|
+
}
|
|
13184
|
+
open(worktreeId) {
|
|
13185
|
+
const bearer = randomBytes2(32).toString("base64url");
|
|
13186
|
+
this.bearers.set(worktreeId, bearer);
|
|
13187
|
+
return {
|
|
13188
|
+
type: "http",
|
|
13189
|
+
name: TOYON_MCP_SERVER,
|
|
13190
|
+
url: this.d.url(worktreeId),
|
|
13191
|
+
headers: [{ name: "Authorization", value: `Bearer ${bearer}` }]
|
|
13192
|
+
};
|
|
13193
|
+
}
|
|
13194
|
+
close(worktreeId) {
|
|
13195
|
+
this.bearers.delete(worktreeId);
|
|
13196
|
+
}
|
|
13197
|
+
async fetch(req, worktreeId) {
|
|
13198
|
+
if (req.headers.get("origin") !== null)
|
|
13199
|
+
return new Response("forbidden", { status: 403 });
|
|
13200
|
+
if (req.method !== "POST")
|
|
13201
|
+
return new Response("method not allowed", { status: 405, headers: { allow: "POST" } });
|
|
13202
|
+
const bearer = this.bearers.get(worktreeId);
|
|
13203
|
+
if (!bearer || !sameSecret(req.headers.get("authorization"), `Bearer ${bearer}`)) {
|
|
13204
|
+
return new Response("unauthorized", { status: 401 });
|
|
13205
|
+
}
|
|
13206
|
+
const body = await readCapped(req, BODY_MAX_BYTES);
|
|
13207
|
+
if (body === null)
|
|
13208
|
+
return new Response("request body too large", { status: 413 });
|
|
13209
|
+
let parsed;
|
|
13210
|
+
try {
|
|
13211
|
+
parsed = JSON.parse(body);
|
|
13212
|
+
} catch {
|
|
13213
|
+
return Response.json(rpcError(null, PARSE_ERROR, "parse error"), { status: 400 });
|
|
13214
|
+
}
|
|
13215
|
+
const batch = Array.isArray(parsed);
|
|
13216
|
+
const messages = batch ? parsed : [parsed];
|
|
13217
|
+
const replies = [];
|
|
13218
|
+
for (const m of messages) {
|
|
13219
|
+
const reply = await this.handle(worktreeId, m);
|
|
13220
|
+
if (reply !== undefined)
|
|
13221
|
+
replies.push(reply);
|
|
13222
|
+
}
|
|
13223
|
+
if (replies.length === 0)
|
|
13224
|
+
return new Response(null, { status: 202 });
|
|
13225
|
+
return Response.json(batch ? replies : replies[0], {
|
|
13226
|
+
status: 200,
|
|
13227
|
+
headers: { "cache-control": "no-store" }
|
|
13228
|
+
});
|
|
13229
|
+
}
|
|
13230
|
+
async handle(worktreeId, raw) {
|
|
13231
|
+
const m = asRecord(raw);
|
|
13232
|
+
if (!m || typeof m.method !== "string")
|
|
13233
|
+
return rpcError(m?.id ?? null, PARSE_ERROR, "parse error");
|
|
13234
|
+
const method = m.method;
|
|
13235
|
+
const id = m.id;
|
|
13236
|
+
const params = asRecord(m.params) ?? {};
|
|
13237
|
+
if (method.startsWith("notifications/"))
|
|
13238
|
+
return;
|
|
13239
|
+
if (id === undefined)
|
|
13240
|
+
return;
|
|
13241
|
+
const ok = (result) => ({ jsonrpc: "2.0", id, result });
|
|
13242
|
+
switch (method) {
|
|
13243
|
+
case "initialize": {
|
|
13244
|
+
const asked = params.protocolVersion;
|
|
13245
|
+
const protocolVersion = typeof asked === "string" && PROTOCOL_VERSIONS.includes(asked) ? asked : DEFAULT_PROTOCOL_VERSION;
|
|
13246
|
+
return ok({
|
|
13247
|
+
protocolVersion,
|
|
13248
|
+
capabilities: { tools: { listChanged: false } },
|
|
13249
|
+
serverInfo: { name: TOYON_MCP_SERVER, version: this.d.version }
|
|
13250
|
+
});
|
|
13251
|
+
}
|
|
13252
|
+
case "ping":
|
|
13253
|
+
return ok({});
|
|
13254
|
+
case "tools/list": {
|
|
13255
|
+
const tools = this.d.tools.list(worktreeId);
|
|
13256
|
+
log.debug(worktreeId, `mcp: tools/list answered with ${tools.map((t) => t.name).join(", ") || "nothing"}`);
|
|
13257
|
+
return ok({ tools });
|
|
13258
|
+
}
|
|
13259
|
+
case "tools/call": {
|
|
13260
|
+
const name = params.name;
|
|
13261
|
+
if (typeof name !== "string" || !this.d.tools.list(worktreeId).some((t) => t.name === name)) {
|
|
13262
|
+
return rpcError(id, INVALID_PARAMS, `unknown tool ${typeof name === "string" ? name : ""}`.trim());
|
|
13263
|
+
}
|
|
13264
|
+
try {
|
|
13265
|
+
const r = await this.d.tools.call(worktreeId, name, params.arguments ?? {});
|
|
13266
|
+
return ok({ content: [{ type: "text", text: r.text }], ...r.isError ? { isError: true } : {} });
|
|
13267
|
+
} catch (e) {
|
|
13268
|
+
if (e instanceof UserError)
|
|
13269
|
+
return ok({ content: [{ type: "text", text: e.message }], isError: true });
|
|
13270
|
+
log.error(worktreeId, `mcp: ${name} failed`, e);
|
|
13271
|
+
return rpcError(id, INTERNAL_ERROR, "internal error");
|
|
13272
|
+
}
|
|
13273
|
+
}
|
|
13274
|
+
default:
|
|
13275
|
+
return rpcError(id, METHOD_NOT_FOUND, `method not found: ${method}`);
|
|
13276
|
+
}
|
|
13277
|
+
}
|
|
13278
|
+
}
|
|
13279
|
+
async function readCapped(req, max) {
|
|
13280
|
+
const body = req.body;
|
|
13281
|
+
if (!body)
|
|
13282
|
+
return "";
|
|
13283
|
+
const chunks = [];
|
|
13284
|
+
let total = 0;
|
|
13285
|
+
for await (const chunk of body) {
|
|
13286
|
+
total += chunk.byteLength;
|
|
13287
|
+
if (total > max)
|
|
13288
|
+
return null;
|
|
13289
|
+
chunks.push(chunk);
|
|
13290
|
+
}
|
|
13291
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
13292
|
+
}
|
|
13293
|
+
|
|
12952
13294
|
// ../daemon/src/agent/acp/options.ts
|
|
12953
13295
|
var OPTION_FIELDS = { model: "model", thought_level: "effort" };
|
|
12954
13296
|
var OPTION_CATEGORIES = Object.keys(OPTION_FIELDS);
|
|
@@ -13050,7 +13392,7 @@ class OptionProbe {
|
|
|
13050
13392
|
}
|
|
13051
13393
|
|
|
13052
13394
|
// ../daemon/src/agent/registry.ts
|
|
13053
|
-
import { existsSync as
|
|
13395
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
13054
13396
|
import { homedir as homedir3 } from "os";
|
|
13055
13397
|
import { dirname as dirname3, isAbsolute, join as join6, resolve as resolve3 } from "path";
|
|
13056
13398
|
|
|
@@ -13078,7 +13420,7 @@ var cloud = {
|
|
|
13078
13420
|
// ../daemon/src/git/exec.ts
|
|
13079
13421
|
import { spawn as spawn2 } from "child_process";
|
|
13080
13422
|
import { createHash as createHash2 } from "crypto";
|
|
13081
|
-
import { existsSync, readFileSync as
|
|
13423
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
13082
13424
|
import { join as join3 } from "path";
|
|
13083
13425
|
|
|
13084
13426
|
// ../daemon/src/core/metrics.ts
|
|
@@ -13121,7 +13463,7 @@ function lockfileHash(dir) {
|
|
|
13121
13463
|
const h = createHash2("sha1");
|
|
13122
13464
|
for (const f of LOCKFILES) {
|
|
13123
13465
|
try {
|
|
13124
|
-
h.update(
|
|
13466
|
+
h.update(readFileSync3(join3(dir, f)));
|
|
13125
13467
|
} catch {}
|
|
13126
13468
|
}
|
|
13127
13469
|
return h.digest("hex");
|
|
@@ -13138,7 +13480,7 @@ function resolveGit() {
|
|
|
13138
13480
|
"/opt/homebrew/bin/git",
|
|
13139
13481
|
"/usr/local/bin/git"
|
|
13140
13482
|
]) {
|
|
13141
|
-
if (
|
|
13483
|
+
if (existsSync2(p))
|
|
13142
13484
|
return p;
|
|
13143
13485
|
}
|
|
13144
13486
|
return "git";
|
|
@@ -13441,7 +13783,7 @@ function confine(launch, b, extraWrite = [], platform = process.platform, which
|
|
|
13441
13783
|
}
|
|
13442
13784
|
|
|
13443
13785
|
// ../daemon/src/agent/native.ts
|
|
13444
|
-
import { existsSync as
|
|
13786
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
13445
13787
|
var OS = { darwin: "darwin", linux: "linux", win32: "windows" };
|
|
13446
13788
|
function nativePackage(pkg, host) {
|
|
13447
13789
|
const os = OS[host.platform];
|
|
@@ -13464,14 +13806,14 @@ function hostTraits() {
|
|
|
13464
13806
|
}
|
|
13465
13807
|
function muslLoader() {
|
|
13466
13808
|
try {
|
|
13467
|
-
return
|
|
13809
|
+
return existsSync3("/lib") && readdirSync2("/lib").some((f) => f.startsWith("ld-musl-"));
|
|
13468
13810
|
} catch {
|
|
13469
13811
|
return false;
|
|
13470
13812
|
}
|
|
13471
13813
|
}
|
|
13472
13814
|
function cpuFlags() {
|
|
13473
13815
|
try {
|
|
13474
|
-
return
|
|
13816
|
+
return readFileSync4("/proc/cpuinfo", "utf8").match(/^flags\s*:(.*)$/m)?.[1]?.trim().split(/\s+/) ?? [];
|
|
13475
13817
|
} catch {
|
|
13476
13818
|
return [];
|
|
13477
13819
|
}
|
|
@@ -13534,7 +13876,7 @@ function opencodeEnv() {
|
|
|
13534
13876
|
}
|
|
13535
13877
|
|
|
13536
13878
|
// ../daemon/src/agent/sandbox.ts
|
|
13537
|
-
import { existsSync as
|
|
13879
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
13538
13880
|
import { homedir as homedir2, tmpdir } from "os";
|
|
13539
13881
|
import { join as join5, resolve as resolve2 } from "path";
|
|
13540
13882
|
|
|
@@ -13579,7 +13921,7 @@ function ensureDirs(p) {
|
|
|
13579
13921
|
}
|
|
13580
13922
|
|
|
13581
13923
|
// ../daemon/src/git/exclude.ts
|
|
13582
|
-
import { existsSync as
|
|
13924
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync } from "fs";
|
|
13583
13925
|
import { dirname } from "path";
|
|
13584
13926
|
async function excludeFile(cwd) {
|
|
13585
13927
|
const r = await git(cwd, "rev-parse", "--path-format=absolute", "--git-path", "info/exclude");
|
|
@@ -13592,7 +13934,7 @@ async function excludeFromGit(cwd, pattern) {
|
|
|
13592
13934
|
const file = await excludeFile(cwd);
|
|
13593
13935
|
if (!file)
|
|
13594
13936
|
return;
|
|
13595
|
-
const current =
|
|
13937
|
+
const current = existsSync4(file) ? readFileSync5(file, "utf8") : "";
|
|
13596
13938
|
if (current.split(`
|
|
13597
13939
|
`).some((l) => l.trim() === pattern))
|
|
13598
13940
|
return;
|
|
@@ -13608,7 +13950,7 @@ async function excludeBlock(cwd, name, patterns) {
|
|
|
13608
13950
|
return;
|
|
13609
13951
|
const open = `# toyon ${name}`;
|
|
13610
13952
|
const close = `# toyon end ${name}`;
|
|
13611
|
-
const current =
|
|
13953
|
+
const current = existsSync4(file) ? readFileSync5(file, "utf8") : "";
|
|
13612
13954
|
const lines = current.split(`
|
|
13613
13955
|
`);
|
|
13614
13956
|
const from = lines.indexOf(open);
|
|
@@ -13627,9 +13969,9 @@ async function excludeBlock(cwd, name, patterns) {
|
|
|
13627
13969
|
}
|
|
13628
13970
|
async function unexcludeFromGit(cwd, pattern) {
|
|
13629
13971
|
const file = await excludeFile(cwd);
|
|
13630
|
-
if (!file || !
|
|
13972
|
+
if (!file || !existsSync4(file))
|
|
13631
13973
|
return;
|
|
13632
|
-
const lines =
|
|
13974
|
+
const lines = readFileSync5(file, "utf8").split(`
|
|
13633
13975
|
`);
|
|
13634
13976
|
const kept = lines.filter((l) => l.trim() !== pattern);
|
|
13635
13977
|
if (kept.length === lines.length)
|
|
@@ -13639,12 +13981,12 @@ async function unexcludeFromGit(cwd, pattern) {
|
|
|
13639
13981
|
}
|
|
13640
13982
|
|
|
13641
13983
|
// ../daemon/src/agent/bounds.ts
|
|
13642
|
-
import { existsSync as
|
|
13984
|
+
import { existsSync as existsSync5, realpathSync } from "fs";
|
|
13643
13985
|
import { basename, dirname as dirname2, resolve, sep } from "path";
|
|
13644
13986
|
function canonical(p) {
|
|
13645
13987
|
let probe = p;
|
|
13646
13988
|
const tail = [];
|
|
13647
|
-
while (!
|
|
13989
|
+
while (!existsSync5(probe)) {
|
|
13648
13990
|
const parent = dirname2(probe);
|
|
13649
13991
|
if (parent === probe)
|
|
13650
13992
|
return p;
|
|
@@ -13706,9 +14048,9 @@ async function writeClaudeLocalSettings(cwd, b) {
|
|
|
13706
14048
|
const file = join5(cwd, SETTINGS_REL);
|
|
13707
14049
|
mkdirSync4(join5(cwd, ".claude"), { recursive: true });
|
|
13708
14050
|
let existing = {};
|
|
13709
|
-
if (
|
|
14051
|
+
if (existsSync6(file)) {
|
|
13710
14052
|
try {
|
|
13711
|
-
const parsed = JSON.parse(
|
|
14053
|
+
const parsed = JSON.parse(readFileSync6(file, "utf8"));
|
|
13712
14054
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
13713
14055
|
existing = parsed;
|
|
13714
14056
|
} catch (e) {
|
|
@@ -13726,7 +14068,7 @@ async function writeClaudeLocalSettings(cwd, b) {
|
|
|
13726
14068
|
writeFileSync2(tmp, `${JSON.stringify(next, null, 2)}
|
|
13727
14069
|
`);
|
|
13728
14070
|
renameSync(tmp, file);
|
|
13729
|
-
if (
|
|
14071
|
+
if (existsSync6(join5(cwd, ".git")))
|
|
13730
14072
|
await excludeFromGit(cwd, SETTINGS_REL);
|
|
13731
14073
|
}
|
|
13732
14074
|
|
|
@@ -13740,7 +14082,10 @@ var BUILTIN_AGENTS = [
|
|
|
13740
14082
|
short: "Claude",
|
|
13741
14083
|
builtin: true,
|
|
13742
14084
|
run: { kind: "npm-bin", pkg: "@agentclientprotocol/claude-agent-acp", version: "0.84.0", bin: "claude-agent-acp" },
|
|
13743
|
-
|
|
14085
|
+
env: {
|
|
14086
|
+
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS: "1",
|
|
14087
|
+
...cloud.enabled ? { NO_BROWSER: "1" } : {}
|
|
14088
|
+
},
|
|
13744
14089
|
terminalLogins: {
|
|
13745
14090
|
"claude-login": { args: ["--cli", "auth", "login", "--claudeai"], env: { NO_BROWSER: "1" }, plan: true }
|
|
13746
14091
|
},
|
|
@@ -13854,18 +14199,18 @@ class AgentRegistry {
|
|
|
13854
14199
|
const pkgDir = join6(this.dir, spec.id, "node_modules", name);
|
|
13855
14200
|
if (spec.run.kind === "npm-native") {
|
|
13856
14201
|
const bin = join6(pkgDir, spec.run.bin);
|
|
13857
|
-
return
|
|
14202
|
+
return existsSync7(bin) ? bin : null;
|
|
13858
14203
|
}
|
|
13859
14204
|
const pkgJson = join6(pkgDir, "package.json");
|
|
13860
|
-
if (!
|
|
14205
|
+
if (!existsSync7(pkgJson))
|
|
13861
14206
|
return null;
|
|
13862
14207
|
try {
|
|
13863
|
-
const meta = JSON.parse(
|
|
14208
|
+
const meta = JSON.parse(readFileSync7(pkgJson, "utf8"));
|
|
13864
14209
|
const rel = typeof meta.bin === "string" ? meta.bin : meta.bin?.[spec.run.bin];
|
|
13865
14210
|
if (!rel)
|
|
13866
14211
|
return null;
|
|
13867
14212
|
const script = join6(dirname3(pkgJson), rel);
|
|
13868
|
-
return
|
|
14213
|
+
return existsSync7(script) ? script : null;
|
|
13869
14214
|
} catch (e) {
|
|
13870
14215
|
log.warn("agents", `cannot read ${pkgJson}`, e);
|
|
13871
14216
|
return null;
|
|
@@ -13885,7 +14230,7 @@ class AgentRegistry {
|
|
|
13885
14230
|
return this.installErrors.get(spec.id) ?? (spec.onDemand ? "not installed" : "not installed yet");
|
|
13886
14231
|
}
|
|
13887
14232
|
const cmd = spec.run.command;
|
|
13888
|
-
if (isAbsolute(cmd) ?
|
|
14233
|
+
if (isAbsolute(cmd) ? existsSync7(cmd) : Bun.which(cmd))
|
|
13889
14234
|
return null;
|
|
13890
14235
|
return `${cmd} not found on PATH`;
|
|
13891
14236
|
}
|
|
@@ -13947,7 +14292,7 @@ class AgentRegistry {
|
|
|
13947
14292
|
return null;
|
|
13948
14293
|
const pkgJson = join6(this.dir, spec.id, "node_modules", name, "package.json");
|
|
13949
14294
|
try {
|
|
13950
|
-
return JSON.parse(
|
|
14295
|
+
return JSON.parse(readFileSync7(pkgJson, "utf8")).version ?? null;
|
|
13951
14296
|
} catch {
|
|
13952
14297
|
return null;
|
|
13953
14298
|
}
|
|
@@ -13990,8 +14335,9 @@ class AgentRegistry {
|
|
|
13990
14335
|
}
|
|
13991
14336
|
}
|
|
13992
14337
|
var ID_RE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
13993
|
-
|
|
13994
|
-
|
|
14338
|
+
var isRecord2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
14339
|
+
function parseAgentsFile(raw, builtins) {
|
|
14340
|
+
const out = [...builtins];
|
|
13995
14341
|
let parsed;
|
|
13996
14342
|
try {
|
|
13997
14343
|
parsed = JSON.parse(raw);
|
|
@@ -13999,7 +14345,7 @@ function parseCustomAgents(raw) {
|
|
|
13999
14345
|
log.warn("agents", "agents.json is not valid JSON; ignoring it", e);
|
|
14000
14346
|
return out;
|
|
14001
14347
|
}
|
|
14002
|
-
if (!
|
|
14348
|
+
if (!isRecord2(parsed)) {
|
|
14003
14349
|
log.warn("agents", "agents.json must be an object keyed by agent id; ignoring it");
|
|
14004
14350
|
return out;
|
|
14005
14351
|
}
|
|
@@ -14007,24 +14353,42 @@ function parseCustomAgents(raw) {
|
|
|
14007
14353
|
const e = v ?? {};
|
|
14008
14354
|
const str = (k) => typeof e[k] === "string" && e[k].trim() ? e[k] : undefined;
|
|
14009
14355
|
const command = str("command");
|
|
14010
|
-
const
|
|
14356
|
+
const at = out.findIndex((s) => s.id === id);
|
|
14357
|
+
const tunes = !command && at >= 0 && out[at].builtin;
|
|
14358
|
+
const why = !ID_RE.test(id) ? "id must be lowercase letters, digits and dashes" : !command && !tunes ? 'needs a non-empty "command" (only a builtin is tuned without one)' : e.args !== undefined && !(Array.isArray(e.args) && e.args.every((a) => typeof a === "string")) ? '"args" must be an array of strings' : e.env !== undefined && !(isRecord2(e.env) && Object.values(e.env).every((x) => typeof x === "string")) ? '"env" must be an object of strings' : e.meta !== undefined && !isRecord2(e.meta) ? '"meta" must be an object' : e.confinement !== undefined && (e.confinement === "claude-settings" || !CONFINEMENTS.includes(e.confinement)) ? '"confinement" must be "none", "adapter-sandbox" or "toyon-sandbox"' : null;
|
|
14011
14359
|
if (why) {
|
|
14012
14360
|
log.warn("agents", `agents.json: skipping "${id}": ${why}`);
|
|
14013
14361
|
continue;
|
|
14014
14362
|
}
|
|
14015
|
-
|
|
14363
|
+
const env = e.env;
|
|
14364
|
+
const meta = e.meta;
|
|
14365
|
+
if (tunes) {
|
|
14366
|
+
const base = out[at];
|
|
14367
|
+
out[at] = {
|
|
14368
|
+
...base,
|
|
14369
|
+
...env ? { env: { ...base.env, ...env } } : {},
|
|
14370
|
+
...meta ? { meta } : {}
|
|
14371
|
+
};
|
|
14372
|
+
continue;
|
|
14373
|
+
}
|
|
14374
|
+
const spec = {
|
|
14016
14375
|
id,
|
|
14017
14376
|
name: str("name") ?? id,
|
|
14018
14377
|
builtin: false,
|
|
14019
14378
|
run: { kind: "command", command, ...e.args ? { args: e.args } : {} },
|
|
14020
|
-
...
|
|
14379
|
+
...env ? { env } : {},
|
|
14380
|
+
...meta ? { meta } : {},
|
|
14021
14381
|
confinement: e.confinement ?? "none",
|
|
14022
14382
|
systemPrompt: "prompt-prefix",
|
|
14023
14383
|
...str("mode") ? { mode: str("mode") } : {},
|
|
14024
14384
|
...str("planMode") ? { modes: { plan: str("planMode"), build: str("mode") } } : {},
|
|
14025
14385
|
...str("quickModel") ? { quickModel: str("quickModel") } : {},
|
|
14026
14386
|
loginHint: str("loginHint") ?? `${str("name") ?? id} is not logged in`
|
|
14027
|
-
}
|
|
14387
|
+
};
|
|
14388
|
+
if (at >= 0)
|
|
14389
|
+
out[at] = spec;
|
|
14390
|
+
else
|
|
14391
|
+
out.push(spec);
|
|
14028
14392
|
}
|
|
14029
14393
|
return out;
|
|
14030
14394
|
}
|
|
@@ -14033,8 +14397,8 @@ function managedSpecs(specs, managed) {
|
|
|
14033
14397
|
return managed.agents === null ? kept : kept.filter((s) => managed.agents?.includes(s.id));
|
|
14034
14398
|
}
|
|
14035
14399
|
function loadAgentRegistry(agentsFile, agentsDir, sandbox, managed = MANAGED_DEFAULTS) {
|
|
14036
|
-
const
|
|
14037
|
-
return new AgentRegistry(managedSpecs(
|
|
14400
|
+
const specs = managed.customAgents && existsSync7(agentsFile) ? parseAgentsFile(readFileSync7(agentsFile, "utf8"), BUILTIN_AGENTS) : BUILTIN_AGENTS;
|
|
14401
|
+
return new AgentRegistry(managedSpecs(specs, managed), agentsDir, undefined, undefined, sandbox);
|
|
14038
14402
|
}
|
|
14039
14403
|
|
|
14040
14404
|
// ../daemon/src/agent/acp/session.ts
|
|
@@ -14184,6 +14548,7 @@ function pickOption(options, verdict) {
|
|
|
14184
14548
|
var SYSTEM_APPEND = [
|
|
14185
14549
|
"You are working inside a dedicated git worktree managed by Toyon.",
|
|
14186
14550
|
"Make every change inside the current working directory and never modify files outside it. Reading files outside it is fine when the user points you there.",
|
|
14551
|
+
"When a change belongs in another project Toyon has open rather than in this worktree, do not edit outside it and do not stop at saying so: propose it with the `handoff` tool when you have one, naming the project and writing the message its agent will start from, with what is needed and why, how to reproduce it, and the paths here that depend on it; the user decides on a card, and once it is proposed you finish your turn.",
|
|
14187
14552
|
"A write Toyon refuses (outside the worktree, or to an agent's own settings such as .claude/ or opencode.json) was refused by Toyon and not by the user, who was never asked. Report it as the boundary that stopped it, never as the user declining.",
|
|
14188
14553
|
"Never run `git push`, delete branches, or create pull requests, and never offer to; shipping is handled by the Toyon UI.",
|
|
14189
14554
|
"Never run `git commit` unless the user explicitly asks you to, and never offer to, through the question tool or otherwise; leave changes uncommitted for the user to review and commit themselves.",
|
|
@@ -14304,6 +14669,22 @@ function procDiagnosis(p) {
|
|
|
14304
14669
|
return p.exitCode != null ? `It exited with code ${p.exitCode}.` : "It crashed.";
|
|
14305
14670
|
return "It never answered on that port.";
|
|
14306
14671
|
}
|
|
14672
|
+
function handoffPrompt(message, note, origin) {
|
|
14673
|
+
return [
|
|
14674
|
+
message,
|
|
14675
|
+
note ? `The person added, when they approved this: ${note}` : undefined,
|
|
14676
|
+
`This task was handed off from the worktree at ${origin.path} of the project ${origin.project}, on its branch ${origin.branch}. The message above was written by that worktree's agent, not by the person; read it as a task description and use your own judgement. Its files are readable there when needed; never write to it.`
|
|
14677
|
+
].filter((p) => !!p).join(`
|
|
14678
|
+
|
|
14679
|
+
`);
|
|
14680
|
+
}
|
|
14681
|
+
function handoffAskPrompt(repo, text) {
|
|
14682
|
+
const own = text?.trim();
|
|
14683
|
+
return `The user wants to continue this work in ${repo.name} (${repo.path}), another project open in Toyon. Propose it with the handoff tool: project "${repo.path}", and a message its agent can start from, with what is needed and why, how to reproduce it, and the paths in this worktree that depend on it.` + (own ? ` The user's own words for what ${repo.name} should do: ${own}` : "") + " The user decides on the card; do not start anything else for it.";
|
|
14684
|
+
}
|
|
14685
|
+
function handoffLandedContext(title, project, url) {
|
|
14686
|
+
return `The work this worktree handed off to ${project}, "${title}", has landed there${url ? ` (${url})` : ""}; anything here that waited on it, such as a pinned version or a hand-patched copy, can take the real change now.`;
|
|
14687
|
+
}
|
|
14307
14688
|
function ambientBlock(paragraphs) {
|
|
14308
14689
|
const said = paragraphs.filter((p) => !!p);
|
|
14309
14690
|
if (said.length === 0)
|
|
@@ -14411,7 +14792,7 @@ ${a.text}
|
|
|
14411
14792
|
}
|
|
14412
14793
|
|
|
14413
14794
|
// ../daemon/src/agent/transcript.ts
|
|
14414
|
-
import { existsSync as
|
|
14795
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
14415
14796
|
import { appendFile } from "fs/promises";
|
|
14416
14797
|
import { join as join7 } from "path";
|
|
14417
14798
|
function transcriptPathFor(transcriptsDir, worktreeId) {
|
|
@@ -14469,7 +14850,7 @@ class Transcript {
|
|
|
14469
14850
|
this.seq = (this.entries.at(-1)?.seq ?? -1) + 1;
|
|
14470
14851
|
}
|
|
14471
14852
|
read() {
|
|
14472
|
-
return
|
|
14853
|
+
return existsSync8(this.path) ? parseTranscript(readFileSync8(this.path, "utf8"), this.tag) : [];
|
|
14473
14854
|
}
|
|
14474
14855
|
append(event) {
|
|
14475
14856
|
const entry = { seq: this.seq++, event };
|
|
@@ -14567,7 +14948,7 @@ async function cleanUp(ctx, a, sessionId) {
|
|
|
14567
14948
|
}
|
|
14568
14949
|
|
|
14569
14950
|
// ../daemon/src/agent/acp/background.ts
|
|
14570
|
-
import { existsSync as
|
|
14951
|
+
import { existsSync as existsSync9, statSync as statSync3 } from "fs";
|
|
14571
14952
|
import { open as open2 } from "fs/promises";
|
|
14572
14953
|
import { homedir as homedir4 } from "os";
|
|
14573
14954
|
import { basename as basename2, dirname as dirname4, join as join8 } from "path";
|
|
@@ -14591,7 +14972,7 @@ function sessionLogFor(file, configDir = claudeConfigDir()) {
|
|
|
14591
14972
|
return null;
|
|
14592
14973
|
const session = dirname4(tasks);
|
|
14593
14974
|
const path = join8(configDir, "projects", basename2(dirname4(session)), `${basename2(session)}.jsonl`);
|
|
14594
|
-
return
|
|
14975
|
+
return existsSync9(path) ? path : null;
|
|
14595
14976
|
}
|
|
14596
14977
|
function taskEnd(text, toolId) {
|
|
14597
14978
|
const at = text.indexOf(`<tool-use-id>${toolId}</tool-use-id>`);
|
|
@@ -14900,25 +15281,25 @@ function unifiedDiff(oldText, newText, context = 3) {
|
|
|
14900
15281
|
|
|
14901
15282
|
// ../daemon/src/agent/acp/map.ts
|
|
14902
15283
|
function spawnOf(meta) {
|
|
14903
|
-
const claude =
|
|
15284
|
+
const claude = asRecord2(asRecord2(meta).claudeCode);
|
|
14904
15285
|
const parentToolId = typeof claude.parentToolUseId === "string" ? claude.parentToolUseId : undefined;
|
|
14905
|
-
const codex =
|
|
15286
|
+
const codex = asRecord2(asRecord2(meta).codex).subagent;
|
|
14906
15287
|
const spawns = claude.toolName === "Agent" || claude.toolName === "Task";
|
|
14907
|
-
const subagent = spawns || claude.subagent === true || Object.keys(
|
|
15288
|
+
const subagent = spawns || claude.subagent === true || Object.keys(asRecord2(codex)).length > 0;
|
|
14908
15289
|
return { ...parentToolId ? { parentToolId } : {}, ...subagent ? { subagent: true } : {} };
|
|
14909
15290
|
}
|
|
14910
|
-
function
|
|
15291
|
+
function asRecord2(v) {
|
|
14911
15292
|
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
14912
15293
|
}
|
|
14913
15294
|
var NETWORK_ASK = "SandboxNetworkAccess";
|
|
14914
15295
|
function isNetworkAsk(update) {
|
|
14915
|
-
return update.name === NETWORK_ASK ||
|
|
15296
|
+
return update.name === NETWORK_ASK || asRecord2(asRecord2(update._meta).claudeCode).toolName === NETWORK_ASK;
|
|
14916
15297
|
}
|
|
14917
15298
|
function isToolSearch(update) {
|
|
14918
|
-
return update.name === TOOL_SEARCH ||
|
|
15299
|
+
return update.name === TOOL_SEARCH || asRecord2(asRecord2(update._meta).claudeCode).toolName === TOOL_SEARCH;
|
|
14919
15300
|
}
|
|
14920
15301
|
function toolSearchQuery(rawInput) {
|
|
14921
|
-
const query =
|
|
15302
|
+
const query = asRecord2(rawInput).query;
|
|
14922
15303
|
if (typeof query !== "string")
|
|
14923
15304
|
return "";
|
|
14924
15305
|
const names = /^select:(.*)$/.exec(query.trim());
|
|
@@ -14926,14 +15307,33 @@ function toolSearchQuery(rawInput) {
|
|
|
14926
15307
|
return query.trim();
|
|
14927
15308
|
return (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).join(", ");
|
|
14928
15309
|
}
|
|
15310
|
+
var TOYON_TOOL = new RegExp(`^(?:mcp__${TOYON_MCP_SERVER}__|${TOYON_MCP_SERVER}/)([A-Za-z0-9_-]+)$`);
|
|
15311
|
+
function toyonTool(update) {
|
|
15312
|
+
const meta = asRecord2(asRecord2(update._meta).claudeCode).toolName;
|
|
15313
|
+
for (const spelled of [update.name ?? "", typeof meta === "string" ? meta : ""]) {
|
|
15314
|
+
const m = TOYON_TOOL.exec(spelled);
|
|
15315
|
+
if (m)
|
|
15316
|
+
return m[1];
|
|
15317
|
+
}
|
|
15318
|
+
return null;
|
|
15319
|
+
}
|
|
15320
|
+
function toyonTitle(tool, rawInput, title) {
|
|
15321
|
+
if (tool !== HANDOFF_TOOL)
|
|
15322
|
+
return title && toyonTool({ name: title }) === null ? title : tool;
|
|
15323
|
+
const project = asRecord2(rawInput).project;
|
|
15324
|
+
return typeof project === "string" && project.trim() ? `Continue in ${project.trim()}` : "Continue in another project";
|
|
15325
|
+
}
|
|
14929
15326
|
function heading(update) {
|
|
14930
15327
|
const name = typeof update.name === "string" ? update.name : "";
|
|
14931
|
-
const host =
|
|
15328
|
+
const host = asRecord2(update.rawInput).host;
|
|
14932
15329
|
if (isNetworkAsk(update) && typeof host === "string")
|
|
14933
15330
|
return { name: "", title: host, kind: "fetch" };
|
|
14934
15331
|
if (isToolSearch(update)) {
|
|
14935
15332
|
return { name: TOOL_SEARCH, title: toolSearchQuery(update.rawInput) || TOOL_SEARCH, kind: "search" };
|
|
14936
15333
|
}
|
|
15334
|
+
const own = toyonTool(update);
|
|
15335
|
+
if (own)
|
|
15336
|
+
return { name: own, title: toyonTitle(own, update.rawInput, update.title), kind: "other" };
|
|
14937
15337
|
return { name, title: update.title, ...update.kind ? { kind: update.kind } : {} };
|
|
14938
15338
|
}
|
|
14939
15339
|
function endOfAsk(req, res, memos) {
|
|
@@ -15014,7 +15414,7 @@ function mapUpdate(update, memos, tag, sink, background) {
|
|
|
15014
15414
|
content: update.content ?? [],
|
|
15015
15415
|
rawOutput: update.rawOutput,
|
|
15016
15416
|
ended: false,
|
|
15017
|
-
writing:
|
|
15417
|
+
writing: writingCall(head, input),
|
|
15018
15418
|
...spawn.parentToolId ? { parent: spawn.parentToolId } : {},
|
|
15019
15419
|
...spawn.subagent ? { spawns: true } : {}
|
|
15020
15420
|
};
|
|
@@ -15045,7 +15445,7 @@ function mapUpdate(update, memos, tag, sink, background) {
|
|
|
15045
15445
|
input,
|
|
15046
15446
|
content: [],
|
|
15047
15447
|
ended: false,
|
|
15048
|
-
writing:
|
|
15448
|
+
writing: writingCall({ name: update.name ?? "", kind: update.kind ?? undefined }, input),
|
|
15049
15449
|
...spawn.parentToolId ? { parent: spawn.parentToolId } : {},
|
|
15050
15450
|
...spawn.subagent ? { spawns: true } : {}
|
|
15051
15451
|
};
|
|
@@ -15061,10 +15461,10 @@ function mapUpdate(update, memos, tag, sink, background) {
|
|
|
15061
15461
|
});
|
|
15062
15462
|
}
|
|
15063
15463
|
const refined = { ...memo.held };
|
|
15064
|
-
const title = memo.name === TOOL_SEARCH ? toolSearchQuery(update.rawInput) : update.title;
|
|
15464
|
+
const title = memo.name === TOOL_SEARCH ? toolSearchQuery(update.rawInput) : memo.name === HANDOFF_TOOL ? update.rawInput !== undefined ? toyonTitle(HANDOFF_TOOL, update.rawInput, update.title) : undefined : update.title;
|
|
15065
15465
|
if (title && title !== memo.title)
|
|
15066
15466
|
refined.title = memo.title = title;
|
|
15067
|
-
if (update.name && update.name !== memo.name)
|
|
15467
|
+
if (update.name && update.name !== memo.name && toyonTool(update) !== memo.name)
|
|
15068
15468
|
refined.name = memo.name = update.name;
|
|
15069
15469
|
if (update.kind && update.kind !== memo.kind)
|
|
15070
15470
|
refined.kind = memo.kind = update.kind;
|
|
@@ -15214,6 +15614,46 @@ function mapStopReason(r) {
|
|
|
15214
15614
|
return r === "cancelled" ? "interrupted" : r;
|
|
15215
15615
|
}
|
|
15216
15616
|
|
|
15617
|
+
// ../daemon/src/agent/acp/ratelimit.ts
|
|
15618
|
+
var RATE_LIMIT_META = "_claude/rateLimit";
|
|
15619
|
+
var STATUSES = new Set(["allowed", "allowed_warning", "rejected"]);
|
|
15620
|
+
var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
15621
|
+
function window(v) {
|
|
15622
|
+
const w = v;
|
|
15623
|
+
if (!w || typeof w !== "object")
|
|
15624
|
+
return;
|
|
15625
|
+
const used = num(w.utilization);
|
|
15626
|
+
const resetsAt = num(w.resetsAt);
|
|
15627
|
+
if (used === undefined || resetsAt === undefined)
|
|
15628
|
+
return;
|
|
15629
|
+
return { used: Math.max(0, used), resetsAt: resetsAt * 1000 };
|
|
15630
|
+
}
|
|
15631
|
+
function parseRateLimit(meta, now) {
|
|
15632
|
+
const info = meta?.[RATE_LIMIT_META];
|
|
15633
|
+
if (!info || typeof info !== "object")
|
|
15634
|
+
return null;
|
|
15635
|
+
const status = info.status;
|
|
15636
|
+
if (!STATUSES.has(status))
|
|
15637
|
+
return null;
|
|
15638
|
+
const binding = typeof info.rateLimitType === "string" ? info.rateLimitType : undefined;
|
|
15639
|
+
const unified = info.unifiedWindows ?? undefined;
|
|
15640
|
+
const windows = {};
|
|
15641
|
+
const fiveHour = window(unified?.five_hour);
|
|
15642
|
+
const sevenDay = window(unified?.seven_day);
|
|
15643
|
+
if (fiveHour)
|
|
15644
|
+
windows.five_hour = fiveHour;
|
|
15645
|
+
if (sevenDay)
|
|
15646
|
+
windows.seven_day = sevenDay;
|
|
15647
|
+
if (!fiveHour && !sevenDay && (binding === "five_hour" || binding === "seven_day")) {
|
|
15648
|
+
const top = window({ utilization: info.utilization, resetsAt: info.resetsAt });
|
|
15649
|
+
if (top)
|
|
15650
|
+
windows[binding] = top;
|
|
15651
|
+
}
|
|
15652
|
+
if (!windows.five_hour && !windows.seven_day)
|
|
15653
|
+
return null;
|
|
15654
|
+
return { status, ...binding ? { binding } : {}, windows, at: now };
|
|
15655
|
+
}
|
|
15656
|
+
|
|
15217
15657
|
// ../daemon/src/agent/acp/steering.ts
|
|
15218
15658
|
var STEER_METHOD = "_session/steering";
|
|
15219
15659
|
function supportsSteering(init) {
|
|
@@ -15258,6 +15698,7 @@ class AcpSession {
|
|
|
15258
15698
|
tasks;
|
|
15259
15699
|
asking = 0;
|
|
15260
15700
|
asks = new Map;
|
|
15701
|
+
openHandoffs = new Set;
|
|
15261
15702
|
log;
|
|
15262
15703
|
seq;
|
|
15263
15704
|
commandList = [];
|
|
@@ -15295,6 +15736,17 @@ class AcpSession {
|
|
|
15295
15736
|
this.emit({ type: "tool-end", toolId, output: output.join(`
|
|
15296
15737
|
`), isError: true });
|
|
15297
15738
|
}
|
|
15739
|
+
for (const { event } of this.log.entries)
|
|
15740
|
+
this.trackHandoff(event);
|
|
15741
|
+
if (this.openHandoffs.size > 0)
|
|
15742
|
+
this.syncStatus();
|
|
15743
|
+
}
|
|
15744
|
+
trackHandoff(event) {
|
|
15745
|
+
if (event.type === "handoff-proposed")
|
|
15746
|
+
return this.openHandoffs.add(event.id).size > 0;
|
|
15747
|
+
if (event.type === "handoff" || event.type === "handoff-declined")
|
|
15748
|
+
return this.openHandoffs.delete(event.id);
|
|
15749
|
+
return false;
|
|
15298
15750
|
}
|
|
15299
15751
|
get commands() {
|
|
15300
15752
|
return this.commandList;
|
|
@@ -15305,6 +15757,12 @@ class AcpSession {
|
|
|
15305
15757
|
return;
|
|
15306
15758
|
await this.warm();
|
|
15307
15759
|
}
|
|
15760
|
+
modeChanged() {
|
|
15761
|
+
const live = this.live;
|
|
15762
|
+
if (!live || this.stopped)
|
|
15763
|
+
return;
|
|
15764
|
+
fireAndForget(this.d.worktreeId, this.applyMode(live), "mode change mid-session");
|
|
15765
|
+
}
|
|
15308
15766
|
async warm() {
|
|
15309
15767
|
if (this.stopped)
|
|
15310
15768
|
return;
|
|
@@ -15318,6 +15776,9 @@ class AcpSession {
|
|
|
15318
15776
|
get runningAgent() {
|
|
15319
15777
|
return this.conn?.spec.id ?? null;
|
|
15320
15778
|
}
|
|
15779
|
+
get mcpTools() {
|
|
15780
|
+
return this.conn ? this.conn.mcpServers.length > 0 : null;
|
|
15781
|
+
}
|
|
15321
15782
|
pgid_ = null;
|
|
15322
15783
|
get pgid() {
|
|
15323
15784
|
return this.pgid_;
|
|
@@ -15376,6 +15837,8 @@ class AcpSession {
|
|
|
15376
15837
|
}
|
|
15377
15838
|
note(event) {
|
|
15378
15839
|
this.emit(event);
|
|
15840
|
+
if (this.trackHandoff(event))
|
|
15841
|
+
this.syncStatus();
|
|
15379
15842
|
}
|
|
15380
15843
|
emit(event) {
|
|
15381
15844
|
const entry = this.log.append(event);
|
|
@@ -15519,7 +15982,7 @@ class AcpSession {
|
|
|
15519
15982
|
this.interrupted = false;
|
|
15520
15983
|
this.steered = [];
|
|
15521
15984
|
}
|
|
15522
|
-
this.setStatus("idle");
|
|
15985
|
+
this.setStatus(this.openHandoffs.size > 0 ? "waiting" : "idle");
|
|
15523
15986
|
} catch (e) {
|
|
15524
15987
|
if (this.conn && (isAuthRequired(e) || this.rejectedCredential(e))) {
|
|
15525
15988
|
const rejected = !isAuthRequired(e);
|
|
@@ -15690,7 +16153,23 @@ class AcpSession {
|
|
|
15690
16153
|
this.emit({ type: "turn-end", stopReason: mapStopReason(res.stopReason), ts: Date.now() });
|
|
15691
16154
|
}
|
|
15692
16155
|
contextFor(item) {
|
|
15693
|
-
return ambientBlock([...item.context ?? [], this.d.preview?.()]);
|
|
16156
|
+
return ambientBlock([...item.context ?? [], this.d.preview?.(), ...this.landedSince()]);
|
|
16157
|
+
}
|
|
16158
|
+
landedSince() {
|
|
16159
|
+
const entries = this.log.entries;
|
|
16160
|
+
const isMessage = (e) => e.type === "user-message" || e.type === "fix-asked";
|
|
16161
|
+
let i = entries.length - 1;
|
|
16162
|
+
while (i >= 0 && !isMessage(entries[i].event))
|
|
16163
|
+
i--;
|
|
16164
|
+
const out = [];
|
|
16165
|
+
for (i--;i >= 0; i--) {
|
|
16166
|
+
const e = entries[i].event;
|
|
16167
|
+
if (isMessage(e))
|
|
16168
|
+
break;
|
|
16169
|
+
if (e.type === "handoff-landed")
|
|
16170
|
+
out.unshift(handoffLandedContext(e.title, e.repoName, e.url));
|
|
16171
|
+
}
|
|
16172
|
+
return out;
|
|
15694
16173
|
}
|
|
15695
16174
|
ensureConn(why) {
|
|
15696
16175
|
if (this.conn)
|
|
@@ -15723,6 +16202,7 @@ class AcpSession {
|
|
|
15723
16202
|
log.warn(this.d.worktreeId, "agent process exited while idle");
|
|
15724
16203
|
this.conn = null;
|
|
15725
16204
|
this.live = null;
|
|
16205
|
+
this.d.mcp?.close();
|
|
15726
16206
|
this.cancelAsks();
|
|
15727
16207
|
this.tasks.endAll("the agent stopped, and the command with it");
|
|
15728
16208
|
this.endOwn("interrupted");
|
|
@@ -15749,6 +16229,7 @@ class AcpSession {
|
|
|
15749
16229
|
deleteSupported: !!init.agentCapabilities?.sessionCapabilities?.delete,
|
|
15750
16230
|
acceptsImages: init.agentCapabilities?.promptCapabilities?.image === true,
|
|
15751
16231
|
steering: supportsSteering(init),
|
|
16232
|
+
mcpServers: takesHttpMcp(init) && this.d.mcp ? [this.d.mcp.open()] : [],
|
|
15752
16233
|
side,
|
|
15753
16234
|
commands
|
|
15754
16235
|
};
|
|
@@ -15775,6 +16256,7 @@ class AcpSession {
|
|
|
15775
16256
|
}
|
|
15776
16257
|
async openLive(conn) {
|
|
15777
16258
|
const additionalDirectories = conn.bounds.gitDir ? [conn.bounds.gitDir] : [];
|
|
16259
|
+
const meta = sessionMeta(conn.spec);
|
|
15778
16260
|
let sessionId = this.d.getSessionId();
|
|
15779
16261
|
let modes;
|
|
15780
16262
|
let configOptions;
|
|
@@ -15784,9 +16266,9 @@ class AcpSession {
|
|
|
15784
16266
|
const r = await conn.ctx.request(methods.agent.session.resume, {
|
|
15785
16267
|
sessionId,
|
|
15786
16268
|
cwd: this.d.cwd,
|
|
15787
|
-
mcpServers:
|
|
16269
|
+
mcpServers: conn.mcpServers,
|
|
15788
16270
|
additionalDirectories,
|
|
15789
|
-
...
|
|
16271
|
+
...meta ? { _meta: meta } : {}
|
|
15790
16272
|
});
|
|
15791
16273
|
modes = r.modes;
|
|
15792
16274
|
configOptions = r.configOptions;
|
|
@@ -15800,9 +16282,9 @@ class AcpSession {
|
|
|
15800
16282
|
if (!resumed) {
|
|
15801
16283
|
const r = await conn.ctx.request(methods.agent.session.new, {
|
|
15802
16284
|
cwd: this.d.cwd,
|
|
15803
|
-
mcpServers:
|
|
16285
|
+
mcpServers: conn.mcpServers,
|
|
15804
16286
|
additionalDirectories,
|
|
15805
|
-
...
|
|
16287
|
+
...meta ? { _meta: meta } : {}
|
|
15806
16288
|
});
|
|
15807
16289
|
sessionId = r.sessionId;
|
|
15808
16290
|
modes = r.modes;
|
|
@@ -15903,12 +16385,23 @@ class AcpSession {
|
|
|
15903
16385
|
if (!live || params.sessionId !== live.sessionId) {
|
|
15904
16386
|
return log.debug(this.d.worktreeId, `acp: update for another session ${params.sessionId} dropped`);
|
|
15905
16387
|
}
|
|
16388
|
+
if (params.update.sessionUpdate === "usage_update" && this.conn) {
|
|
16389
|
+
const limits = parseRateLimit(params.update._meta, Date.now());
|
|
16390
|
+
if (limits)
|
|
16391
|
+
this.d.onLimits?.(this.conn.spec.id, limits);
|
|
16392
|
+
}
|
|
15906
16393
|
if (params.update.sessionUpdate === "current_mode_update") {
|
|
15907
16394
|
live.modeId = params.update.currentModeId;
|
|
15908
16395
|
return;
|
|
15909
16396
|
}
|
|
15910
|
-
if (params.update.sessionUpdate === "config_option_update")
|
|
16397
|
+
if (params.update.sessionUpdate === "config_option_update") {
|
|
16398
|
+
const before = live.options;
|
|
15911
16399
|
this.absorb(live, params.update.configOptions);
|
|
16400
|
+
for (const [category, opt] of live.options) {
|
|
16401
|
+
if (before.get(category)?.current !== opt.current)
|
|
16402
|
+
this.d.onOptionChanged?.(category, opt.current);
|
|
16403
|
+
}
|
|
16404
|
+
}
|
|
15912
16405
|
const events = mapUpdate(params.update, live.tools, this.d.worktreeId, (file, bytes) => fireAndForget(this.d.worktreeId, this.d.attachments.putToolImage(this.d.worktreeId, file, bytes), "tool image"), (toolId, start, input) => this.tasks.start(toolId, start, input));
|
|
15913
16406
|
this.forward(live, events);
|
|
15914
16407
|
this.holdInput(live, params.update);
|
|
@@ -15968,14 +16461,16 @@ class AcpSession {
|
|
|
15968
16461
|
}
|
|
15969
16462
|
async askPermission(params) {
|
|
15970
16463
|
const plan = params.toolCall.kind === "switch_mode";
|
|
15971
|
-
const
|
|
16464
|
+
const parts = permissionParts(params);
|
|
16465
|
+
const detail = parts.detail ?? "";
|
|
15972
16466
|
const planPath = plan ? await this.d.onPlan?.(detail) ?? null : null;
|
|
15973
16467
|
const offered = plan ? params.options : params.options.filter((o) => o.kind !== "allow_always");
|
|
15974
16468
|
const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
|
|
15975
16469
|
return this.openAsk((id) => ({
|
|
15976
16470
|
type: "agent-permission",
|
|
15977
16471
|
id,
|
|
15978
|
-
title:
|
|
16472
|
+
title: parts.title,
|
|
16473
|
+
...parts.command ? { command: parts.command } : {},
|
|
15979
16474
|
...detail ? { detail } : {},
|
|
15980
16475
|
...planPath ? { plan: planPath } : {},
|
|
15981
16476
|
choices,
|
|
@@ -16056,7 +16551,8 @@ class AcpSession {
|
|
|
16056
16551
|
syncStatus() {
|
|
16057
16552
|
if (this.status === "error")
|
|
16058
16553
|
return;
|
|
16059
|
-
this.
|
|
16554
|
+
const waiting = this.asks.size > 0 || this.openHandoffs.size > 0;
|
|
16555
|
+
this.setStatus(waiting ? "waiting" : this.running || this.own ? "working" : "idle");
|
|
16060
16556
|
}
|
|
16061
16557
|
trackOwn(events) {
|
|
16062
16558
|
if (!this.own) {
|
|
@@ -16123,6 +16619,7 @@ class AcpSession {
|
|
|
16123
16619
|
const conn = this.conn;
|
|
16124
16620
|
this.conn = null;
|
|
16125
16621
|
this.live = null;
|
|
16622
|
+
this.d.mcp?.close();
|
|
16126
16623
|
if (!conn)
|
|
16127
16624
|
return;
|
|
16128
16625
|
log.info(this.d.worktreeId, `agent ${conn.spec.id} stopped: ${why} (pid ${conn.link.pid ?? "unknown"})`);
|
|
@@ -16134,12 +16631,21 @@ class AcpSession {
|
|
|
16134
16631
|
function isAuthRequired(e) {
|
|
16135
16632
|
return e instanceof RequestError && e.code === -32000;
|
|
16136
16633
|
}
|
|
16634
|
+
function takesHttpMcp(init) {
|
|
16635
|
+
const http = init.agentCapabilities?.mcpCapabilities?.http;
|
|
16636
|
+
return http === true || typeof http === "object" && http !== null;
|
|
16637
|
+
}
|
|
16137
16638
|
var DETAIL_LINES = 80;
|
|
16138
|
-
function
|
|
16639
|
+
function stringField(raw, key) {
|
|
16640
|
+
const v = raw && typeof raw === "object" ? raw[key] : undefined;
|
|
16641
|
+
return typeof v === "string" && v.trim() ? v : undefined;
|
|
16642
|
+
}
|
|
16643
|
+
function permissionParts(params) {
|
|
16644
|
+
const { toolCall } = params;
|
|
16139
16645
|
const parts = [];
|
|
16140
|
-
for (const c of
|
|
16646
|
+
for (const c of toolCall.content ?? []) {
|
|
16141
16647
|
if (c.type === "content" && c.content.type === "text")
|
|
16142
|
-
parts.push(c.content.text);
|
|
16648
|
+
parts.push({ text: true, md: c.content.text });
|
|
16143
16649
|
else if (c.type === "diff") {
|
|
16144
16650
|
const lines = c.newText.split(`
|
|
16145
16651
|
`);
|
|
@@ -16147,27 +16653,37 @@ function permissionDetail(params) {
|
|
|
16147
16653
|
`);
|
|
16148
16654
|
const more = lines.length > DETAIL_LINES ? `
|
|
16149
16655
|
\u2026 ${lines.length - DETAIL_LINES} more lines` : "";
|
|
16150
|
-
parts.push(`\`${c.path}\`
|
|
16656
|
+
parts.push({ text: false, md: `\`${c.path}\`
|
|
16151
16657
|
|
|
16152
16658
|
\`\`\`
|
|
16153
16659
|
${shown}${more}
|
|
16154
|
-
\`\`\``);
|
|
16660
|
+
\`\`\`` });
|
|
16155
16661
|
}
|
|
16156
16662
|
}
|
|
16157
|
-
|
|
16158
|
-
|
|
16663
|
+
const raw = toolCall.rawInput;
|
|
16664
|
+
const command = stringField(raw, "command");
|
|
16665
|
+
if (!command) {
|
|
16666
|
+
const detail = parts.map((p) => p.md).join(`
|
|
16667
|
+
|
|
16668
|
+
`) || stringField(raw, "plan") || "";
|
|
16669
|
+
return { title: toolCall.title ?? toolCall.name ?? "the agent needs a decision", ...detail ? { detail } : {} };
|
|
16670
|
+
}
|
|
16671
|
+
const same = (a, b) => a.trim() === b.trim();
|
|
16672
|
+
const prose = (s) => s && !same(s, command) && s !== toolCall.name ? s : undefined;
|
|
16673
|
+
const title = stringField(raw, "description") ?? parts.find((p) => p.text && prose(p.md))?.md ?? prose(toolCall.title ?? undefined) ?? "run this command?";
|
|
16674
|
+
const detail = parts.filter((p) => !(p.text && (same(p.md, title) || same(p.md, command)))).map((p) => p.md).join(`
|
|
16159
16675
|
|
|
16160
16676
|
`);
|
|
16161
|
-
|
|
16162
|
-
if (typeof raw?.plan === "string")
|
|
16163
|
-
return raw.plan;
|
|
16164
|
-
if (typeof raw?.command === "string")
|
|
16165
|
-
return `\`\`\`sh
|
|
16166
|
-
${raw.command}
|
|
16167
|
-
\`\`\``;
|
|
16168
|
-
return "";
|
|
16677
|
+
return { title, command, ...detail ? { detail } : {} };
|
|
16169
16678
|
}
|
|
16170
16679
|
var REJECTED_CREDENTIAL_RE = /\b401\b|unauthorized|invalid[\s_-]?api[\s_-]?key|authentication[\s_-]?(error|failed)|api key (is )?(invalid|expired|incorrect)|incorrect api key|(token|credential)s? (have |has )?expired|expired (token|credential)|not (logged in|authenticated)/i;
|
|
16680
|
+
function sessionMeta(spec) {
|
|
16681
|
+
const meta = {
|
|
16682
|
+
...spec.meta,
|
|
16683
|
+
...spec.systemPrompt === "meta-append" ? { systemPrompt: { append: SYSTEM_APPEND } } : {}
|
|
16684
|
+
};
|
|
16685
|
+
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
16686
|
+
}
|
|
16171
16687
|
function terminalLogin(m) {
|
|
16172
16688
|
if ("type" in m && m.type === "terminal")
|
|
16173
16689
|
return { own: false, args: m.args ?? [] };
|
|
@@ -16192,27 +16708,41 @@ function sameCommands(a, b) {
|
|
|
16192
16708
|
}
|
|
16193
16709
|
|
|
16194
16710
|
// ../daemon/src/agent/planDoc.ts
|
|
16195
|
-
import { existsSync as
|
|
16711
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
16196
16712
|
import { join as join9 } from "path";
|
|
16197
16713
|
var PLANS_DIR = join9(".toyon", "plans");
|
|
16198
16714
|
var PLAN_NAME = /^(\d+)\.md$/;
|
|
16199
16715
|
function isPlanPath(rel) {
|
|
16200
16716
|
return rel.startsWith(`${PLANS_DIR}/`) && PLAN_NAME.test(rel.slice(PLANS_DIR.length + 1));
|
|
16201
16717
|
}
|
|
16202
|
-
function
|
|
16203
|
-
if (!
|
|
16204
|
-
return
|
|
16718
|
+
function lastNumber(dir) {
|
|
16719
|
+
if (!existsSync10(dir))
|
|
16720
|
+
return 0;
|
|
16205
16721
|
let max = 0;
|
|
16206
16722
|
for (const name of readdirSync3(dir)) {
|
|
16207
16723
|
const n = Number(PLAN_NAME.exec(name)?.[1]);
|
|
16208
16724
|
if (n > max)
|
|
16209
16725
|
max = n;
|
|
16210
16726
|
}
|
|
16211
|
-
return max
|
|
16727
|
+
return max;
|
|
16728
|
+
}
|
|
16729
|
+
function sameAsLast(cwd, rel, markdown) {
|
|
16730
|
+
try {
|
|
16731
|
+
return readFileSync9(join9(cwd, rel), "utf8").trim() === markdown.trim();
|
|
16732
|
+
} catch (e) {
|
|
16733
|
+
log.warn(cwd, `could not read ${rel}`, e);
|
|
16734
|
+
return false;
|
|
16735
|
+
}
|
|
16212
16736
|
}
|
|
16213
16737
|
async function writePlanDoc(cwd, markdown) {
|
|
16214
16738
|
const dir = join9(cwd, PLANS_DIR);
|
|
16215
|
-
const
|
|
16739
|
+
const last = lastNumber(dir);
|
|
16740
|
+
if (last > 0) {
|
|
16741
|
+
const newest = join9(PLANS_DIR, `${last}.md`);
|
|
16742
|
+
if (sameAsLast(cwd, newest, markdown))
|
|
16743
|
+
return newest;
|
|
16744
|
+
}
|
|
16745
|
+
const rel = join9(PLANS_DIR, `${last + 1}.md`);
|
|
16216
16746
|
try {
|
|
16217
16747
|
mkdirSync6(dir, { recursive: true });
|
|
16218
16748
|
writeFileSync4(join9(cwd, rel), markdown.endsWith(`
|
|
@@ -16227,10 +16757,10 @@ async function writePlanDoc(cwd, markdown) {
|
|
|
16227
16757
|
}
|
|
16228
16758
|
function planEdited(cwd, rel, proposed) {
|
|
16229
16759
|
const file = join9(cwd, rel);
|
|
16230
|
-
if (!
|
|
16760
|
+
if (!existsSync10(file))
|
|
16231
16761
|
return false;
|
|
16232
16762
|
try {
|
|
16233
|
-
return
|
|
16763
|
+
return readFileSync9(file, "utf8").trim() !== proposed.trim();
|
|
16234
16764
|
} catch (e) {
|
|
16235
16765
|
log.warn(cwd, `could not read ${rel}`, e);
|
|
16236
16766
|
return false;
|
|
@@ -16548,6 +17078,26 @@ function orphansIn(ledger, rows, bootId, ledgerBootId, now) {
|
|
|
16548
17078
|
}
|
|
16549
17079
|
return out;
|
|
16550
17080
|
}
|
|
17081
|
+
function strayProcs(rows, worktreesDir, livePaths) {
|
|
17082
|
+
const root = worktreesDir.endsWith("/") ? worktreesDir : `${worktreesDir}/`;
|
|
17083
|
+
const seen = new Set;
|
|
17084
|
+
const out = [];
|
|
17085
|
+
for (const r of rows) {
|
|
17086
|
+
if (r.ppid !== 1 || seen.has(r.pgid) || r.pgid === process.pid)
|
|
17087
|
+
continue;
|
|
17088
|
+
const at = r.command.indexOf(root);
|
|
17089
|
+
if (at === -1)
|
|
17090
|
+
continue;
|
|
17091
|
+
const [repoDir, wtDir] = r.command.slice(at + root.length).split(/\s/, 1)[0].split("/");
|
|
17092
|
+
if (!repoDir || !wtDir)
|
|
17093
|
+
continue;
|
|
17094
|
+
if (livePaths.has(`${root}${repoDir}/${wtDir}`))
|
|
17095
|
+
continue;
|
|
17096
|
+
seen.add(r.pgid);
|
|
17097
|
+
out.push({ worktreeId: "daemon", pgid: r.pgid, name: wtDir });
|
|
17098
|
+
}
|
|
17099
|
+
return out;
|
|
17100
|
+
}
|
|
16551
17101
|
function strayAdapters(rows, agentsDir) {
|
|
16552
17102
|
return rows.filter((r) => r.pid === r.pgid && r.ppid === 1 && r.command.includes(agentsDir));
|
|
16553
17103
|
}
|
|
@@ -16683,131 +17233,17 @@ function expandEnv(env, vars) {
|
|
|
16683
17233
|
return out;
|
|
16684
17234
|
}
|
|
16685
17235
|
|
|
16686
|
-
// ../daemon/src/core/remote.ts
|
|
16687
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
16688
|
-
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
16689
|
-
function loadRemote(file, env = process.env, managed = MANAGED_DEFAULTS) {
|
|
16690
|
-
if (env.TOYON_CLOUD === "1") {
|
|
16691
|
-
const host = (env.TOYON_PUBLIC_HOST ?? "").toLowerCase();
|
|
16692
|
-
if (!isRemoteHost(host)) {
|
|
16693
|
-
throw new Error(`TOYON_CLOUD=1 needs TOYON_PUBLIC_HOST, the name the edge answers for; got "${host}"`);
|
|
16694
|
-
}
|
|
16695
|
-
const previews = env.TOYON_PREVIEWS ?? portPreviews(host);
|
|
16696
|
-
const why = checkPreviews(previews, host);
|
|
16697
|
-
if (why)
|
|
16698
|
-
throw new Error(`TOYON_PREVIEWS: ${why}`);
|
|
16699
|
-
return { host, previews, front: "edge" };
|
|
16700
|
-
}
|
|
16701
|
-
if (!existsSync10(file))
|
|
16702
|
-
return null;
|
|
16703
|
-
if (managed.remote === "off") {
|
|
16704
|
-
log.warn("remote", `${file} is ignored: remote access is turned off by your organization's policy`);
|
|
16705
|
-
return null;
|
|
16706
|
-
}
|
|
16707
|
-
const view = parseRemote(readFileSync9(file, "utf8"));
|
|
16708
|
-
if (!view) {
|
|
16709
|
-
log.warn("remote", `${file} names no valid host or previews; remote access is off`);
|
|
16710
|
-
return null;
|
|
16711
|
-
}
|
|
16712
|
-
if (managed.remote === "tailscale" && !view.host.endsWith(".ts.net")) {
|
|
16713
|
-
log.warn("remote", `${file} names ${view.host}; your organization's policy allows a tailnet name only, so remote access is off`);
|
|
16714
|
-
return null;
|
|
16715
|
-
}
|
|
16716
|
-
return { ...view, front: "local" };
|
|
16717
|
-
}
|
|
16718
|
-
function sameSecret(given, want) {
|
|
16719
|
-
if (given === null)
|
|
16720
|
-
return false;
|
|
16721
|
-
const a = Buffer.from(given);
|
|
16722
|
-
const b = Buffer.from(want);
|
|
16723
|
-
return a.length === b.length && timingSafeEqual(a, b);
|
|
16724
|
-
}
|
|
16725
|
-
var isLoopbackPeer = (ip) => ip === "127.0.0.1" || ip === "::1" || ip.startsWith("::ffff:127.");
|
|
16726
|
-
var isLoopbackHost = (h) => h === "127.0.0.1" || h === "localhost" || h.endsWith(".localhost");
|
|
16727
|
-
function requestedAuthority(req) {
|
|
16728
|
-
const host = (req.headers.get("host") ?? "").toLowerCase();
|
|
16729
|
-
if (host.includes(":"))
|
|
16730
|
-
return host;
|
|
16731
|
-
const port = req.headers.get("x-forwarded-port");
|
|
16732
|
-
return port && /^\d{1,5}$/.test(port) && port !== "443" ? `${host}:${port}` : host;
|
|
16733
|
-
}
|
|
16734
|
-
var forbidden = (body = "forbidden") => ({ kind: "refused", response: new Response(body, { status: 403 }) });
|
|
16735
|
-
function door(req, peer, remote, listener) {
|
|
16736
|
-
if (remote === null && listener === "preview")
|
|
16737
|
-
return { kind: "local" };
|
|
16738
|
-
if (remote?.front !== "edge" && !isLoopbackPeer(peer))
|
|
16739
|
-
return forbidden();
|
|
16740
|
-
const authority = requestedAuthority(req);
|
|
16741
|
-
const host = authority.split(":")[0] ?? "";
|
|
16742
|
-
if (isLoopbackHost(host)) {
|
|
16743
|
-
return remote?.front === "edge" ? forbidden() : { kind: "local" };
|
|
16744
|
-
}
|
|
16745
|
-
if (remote === null)
|
|
16746
|
-
return forbidden();
|
|
16747
|
-
const preview = matchPreview(remote.previews, authority);
|
|
16748
|
-
let worktreeId = null;
|
|
16749
|
-
if (listener === "daemon") {
|
|
16750
|
-
if (preview !== null && "id" in preview)
|
|
16751
|
-
worktreeId = preview.id;
|
|
16752
|
-
else if (preview !== null || host !== remote.host)
|
|
16753
|
-
return forbidden();
|
|
16754
|
-
} else if (preview === null || "id" in preview) {
|
|
16755
|
-
return forbidden();
|
|
16756
|
-
}
|
|
16757
|
-
if (req.headers.get("x-forwarded-proto") !== "https") {
|
|
16758
|
-
return forbidden(`${host} reaches Toyon over https only; the front must terminate TLS`);
|
|
16759
|
-
}
|
|
16760
|
-
if (listener === "preview")
|
|
16761
|
-
return { kind: "preview", worktreeId: null };
|
|
16762
|
-
return worktreeId === null ? { kind: "shell" } : { kind: "preview", worktreeId };
|
|
16763
|
-
}
|
|
16764
|
-
var PREVIEW_COOKIE = "toyon_preview";
|
|
16765
|
-
function previewGrant(token) {
|
|
16766
|
-
return createHmac("sha256", token).update("toyon preview grant").digest("hex");
|
|
16767
|
-
}
|
|
16768
|
-
function grantCookie(grant, name) {
|
|
16769
|
-
return `${PREVIEW_COOKIE}=${grant}; Domain=${name}; Path=/; Max-Age=34560000; HttpOnly; Secure; SameSite=Strict`;
|
|
16770
|
-
}
|
|
16771
|
-
function takeGrant(cookie, grant) {
|
|
16772
|
-
let ok = false;
|
|
16773
|
-
const kept = [];
|
|
16774
|
-
for (const part of (cookie ?? "").split(";")) {
|
|
16775
|
-
const pair = part.trim();
|
|
16776
|
-
if (pair === "")
|
|
16777
|
-
continue;
|
|
16778
|
-
const eq = pair.indexOf("=");
|
|
16779
|
-
if (eq > 0 && pair.slice(0, eq) === PREVIEW_COOKIE) {
|
|
16780
|
-
if (sameSecret(pair.slice(eq + 1), grant))
|
|
16781
|
-
ok = true;
|
|
16782
|
-
continue;
|
|
16783
|
-
}
|
|
16784
|
-
kept.push(pair);
|
|
16785
|
-
}
|
|
16786
|
-
return { ok, rest: kept.length ? kept.join("; ") : null };
|
|
16787
|
-
}
|
|
16788
|
-
function passPreview(req, grant) {
|
|
16789
|
-
const pass = takeGrant(req.headers.get("cookie"), grant);
|
|
16790
|
-
if (!pass.ok) {
|
|
16791
|
-
return {
|
|
16792
|
-
ok: false,
|
|
16793
|
-
response: new Response(`<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="3"></head>` + `<body>This preview opens from Toyon; open your Toyon link first.</body></html>`, { status: 403, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } })
|
|
16794
|
-
};
|
|
16795
|
-
}
|
|
16796
|
-
const headers = new Headers(req.headers);
|
|
16797
|
-
if (pass.rest === null)
|
|
16798
|
-
headers.delete("cookie");
|
|
16799
|
-
else
|
|
16800
|
-
headers.set("cookie", pass.rest);
|
|
16801
|
-
return { ok: true, req: new Request(req, { headers }) };
|
|
16802
|
-
}
|
|
16803
|
-
function setsGrant(line) {
|
|
16804
|
-
return line.slice(0, line.indexOf("=")).trim() === PREVIEW_COOKIE;
|
|
16805
|
-
}
|
|
16806
|
-
|
|
16807
17236
|
// ../daemon/src/runtime/proxy.ts
|
|
16808
17237
|
var DIAL_TIMEOUT_MS = 5000;
|
|
16809
17238
|
var frame = (ev) => typeof ev.data === "string" ? ev.data : new Uint8Array(ev.data);
|
|
16810
17239
|
var VITE_PING = "text/x-vite-ping";
|
|
17240
|
+
var ORIGIN_KEYED = "origin-agent-cluster";
|
|
17241
|
+
function placeholder(status) {
|
|
17242
|
+
return new Response(waitingPage(), {
|
|
17243
|
+
status,
|
|
17244
|
+
headers: { "content-type": "text/html; charset=utf-8", "retry-after": "2", [ORIGIN_KEYED]: "?1" }
|
|
17245
|
+
});
|
|
17246
|
+
}
|
|
16811
17247
|
function previewHandler(opts) {
|
|
16812
17248
|
const hostPart = (t) => t.host.includes(":") ? `[${t.host}]` : t.host;
|
|
16813
17249
|
return {
|
|
@@ -16825,12 +17261,8 @@ function previewHandler(opts) {
|
|
|
16825
17261
|
let target = opts.getTarget();
|
|
16826
17262
|
if (target == null && !machine && opts.ready)
|
|
16827
17263
|
target = await opts.ready();
|
|
16828
|
-
if (target == null)
|
|
16829
|
-
return
|
|
16830
|
-
status: 503,
|
|
16831
|
-
headers: { "content-type": "text/html; charset=utf-8", "retry-after": "2" }
|
|
16832
|
-
});
|
|
16833
|
-
}
|
|
17264
|
+
if (target == null)
|
|
17265
|
+
return placeholder(503);
|
|
16834
17266
|
if (isUpgrade) {
|
|
16835
17267
|
const proto = req.headers.get("sec-websocket-protocol") ?? undefined;
|
|
16836
17268
|
const upstreamUrl = `ws://${hostPart(target)}:${target.port}${url.pathname}${url.search}`;
|
|
@@ -16856,20 +17288,18 @@ function previewHandler(opts) {
|
|
|
16856
17288
|
redirect: "manual"
|
|
16857
17289
|
});
|
|
16858
17290
|
} catch {
|
|
16859
|
-
return
|
|
16860
|
-
status: 502,
|
|
16861
|
-
headers: { "content-type": "text/html; charset=utf-8", "retry-after": "2" }
|
|
16862
|
-
});
|
|
17291
|
+
return placeholder(502);
|
|
16863
17292
|
}
|
|
16864
17293
|
const html = (res.headers.get("content-type") ?? "").includes("text/html");
|
|
16865
17294
|
const encoded = res.headers.has("content-encoding");
|
|
16866
17295
|
const cookies = res.headers.getSetCookie();
|
|
16867
17296
|
const grantSet = cookies.some(setsGrant);
|
|
16868
|
-
if (!html && !encoded && !grantSet)
|
|
16869
|
-
return res;
|
|
16870
17297
|
const h = new Headers(res.headers);
|
|
16871
|
-
h.
|
|
16872
|
-
|
|
17298
|
+
h.set(ORIGIN_KEYED, "?1");
|
|
17299
|
+
if (html || encoded) {
|
|
17300
|
+
h.delete("content-length");
|
|
17301
|
+
h.delete("content-encoding");
|
|
17302
|
+
}
|
|
16873
17303
|
if (grantSet) {
|
|
16874
17304
|
h.delete("set-cookie");
|
|
16875
17305
|
for (const c of cookies)
|
|
@@ -16923,7 +17353,9 @@ function startProxy(opts) {
|
|
|
16923
17353
|
return d.response;
|
|
16924
17354
|
let admitted = req;
|
|
16925
17355
|
if (d.kind === "preview") {
|
|
16926
|
-
|
|
17356
|
+
if (!opts.gate)
|
|
17357
|
+
return new Response("forbidden", { status: 403 });
|
|
17358
|
+
const pass = passPreview(req, opts.gate);
|
|
16927
17359
|
if (!pass.ok)
|
|
16928
17360
|
return pass.response;
|
|
16929
17361
|
admitted = pass.req;
|
|
@@ -17513,6 +17945,7 @@ var PREVIEW_WAKE_MS = 8000;
|
|
|
17513
17945
|
var PREVIEW_POLL_MS = 100;
|
|
17514
17946
|
var PROC_UP_MS = 60000;
|
|
17515
17947
|
function defaultAgent(wt, d, preview, onProcess) {
|
|
17948
|
+
const mcp = d.mcp;
|
|
17516
17949
|
const agent = new AcpSession({
|
|
17517
17950
|
worktreeId: wt.id,
|
|
17518
17951
|
cwd: wt.path,
|
|
@@ -17538,16 +17971,13 @@ function defaultAgent(wt, d, preview, onProcess) {
|
|
|
17538
17971
|
onStatus: (status) => d.hub.emit("agentStatus", wt.id, status),
|
|
17539
17972
|
beforeTurn: async () => d.turnStarting?.(wt.id),
|
|
17540
17973
|
onAuth: (agentId, o) => d.accounts?.observe(agentId, o),
|
|
17974
|
+
onLimits: (agentId, limits) => d.accounts?.observeLimits(agentId, limits),
|
|
17541
17975
|
seedCommands: () => d.state.cachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId),
|
|
17542
17976
|
onCommandsLearned: (commands) => d.state.setCachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId, commands),
|
|
17543
17977
|
mode: () => d.state.requireWorktree(wt.id).mode ?? DEFAULT_PERMISSION_MODE,
|
|
17544
17978
|
setMode: (mode) => {
|
|
17545
|
-
|
|
17546
|
-
|
|
17547
|
-
return;
|
|
17548
|
-
w.mode = mode;
|
|
17549
|
-
d.state.save();
|
|
17550
|
-
d.hub.emit("worktreesChanged");
|
|
17979
|
+
if (d.state.setMode(wt.id, mode))
|
|
17980
|
+
d.hub.emit("worktreesChanged");
|
|
17551
17981
|
},
|
|
17552
17982
|
onPlan: async (markdown) => {
|
|
17553
17983
|
const w = d.state.worktree(wt.id);
|
|
@@ -17570,7 +18000,17 @@ function defaultAgent(wt, d, preview, onProcess) {
|
|
|
17570
18000
|
const agentId = d.state.requireWorktree(wt.id).agent ?? "";
|
|
17571
18001
|
if (d.state.setCachedOptions(agentId, category, choices))
|
|
17572
18002
|
d.hub.emit("agentsChanged");
|
|
17573
|
-
}
|
|
18003
|
+
},
|
|
18004
|
+
onOptionChanged: (category, value) => {
|
|
18005
|
+
const w = d.state.requireWorktree(wt.id);
|
|
18006
|
+
const field = OPTION_FIELDS[category];
|
|
18007
|
+
if (w[field] === value)
|
|
18008
|
+
return;
|
|
18009
|
+
w[field] = value;
|
|
18010
|
+
d.state.save();
|
|
18011
|
+
d.hub.emit("worktreesChanged");
|
|
18012
|
+
},
|
|
18013
|
+
...mcp ? { mcp: { open: () => mcp.open(wt.id), close: () => mcp.close(wt.id) } } : {}
|
|
17574
18014
|
});
|
|
17575
18015
|
agent.onQueueChange = () => d.hub.emit("queue", wt.id, agent.queueItems);
|
|
17576
18016
|
agent.onCommandsChange = (commands) => d.hub.emit("agentCommands", wt.id, commands);
|
|
@@ -17587,7 +18027,7 @@ function defaultProxy(wt, previewName, procs, d, wake) {
|
|
|
17587
18027
|
port: wt.proxyPort,
|
|
17588
18028
|
hostname: cloud.bindHost,
|
|
17589
18029
|
remote: d.remote ?? null,
|
|
17590
|
-
|
|
18030
|
+
gate: d.gate ?? null,
|
|
17591
18031
|
bridgeScript: d.bridgeScript,
|
|
17592
18032
|
getTarget: () => previewTargetOf(procs, previewName),
|
|
17593
18033
|
onRequest: wake.onRequest,
|
|
@@ -18296,6 +18736,11 @@ class RuntimeRegistry {
|
|
|
18296
18736
|
const before = new Map(this.deps.state.groups(id).map((g) => [g.pgid, g]));
|
|
18297
18737
|
const now = Date.now();
|
|
18298
18738
|
const next = live.map((g) => ({ ...g, startedAt: before.get(g.pgid)?.startedAt ?? now }));
|
|
18739
|
+
const alive = this.deps.groupAlive ?? groupAlive;
|
|
18740
|
+
for (const [pgid, g] of before) {
|
|
18741
|
+
if (!live.some((l) => l.pgid === pgid) && alive(pgid))
|
|
18742
|
+
next.push(g);
|
|
18743
|
+
}
|
|
18299
18744
|
this.deps.state.setGroups(id, next);
|
|
18300
18745
|
}
|
|
18301
18746
|
async reclaimOrphans() {
|
|
@@ -18310,6 +18755,13 @@ class RuntimeRegistry {
|
|
|
18310
18755
|
seen.add(row.pgid);
|
|
18311
18756
|
found.push({ worktreeId: "daemon", pgid: row.pgid, name: "adapter" });
|
|
18312
18757
|
}
|
|
18758
|
+
const livePaths = new Set(state.worktrees.map((w) => w.path));
|
|
18759
|
+
for (const o of strayProcs(rows, this.deps.paths.worktreesDir, livePaths)) {
|
|
18760
|
+
if (seen.has(o.pgid))
|
|
18761
|
+
continue;
|
|
18762
|
+
seen.add(o.pgid);
|
|
18763
|
+
found.push(o);
|
|
18764
|
+
}
|
|
18313
18765
|
const reclaim = this.deps.reclaim ?? reclaimGroup;
|
|
18314
18766
|
await Promise.all(found.map(async (o) => {
|
|
18315
18767
|
await reclaim(o.pgid);
|
|
@@ -18325,11 +18777,11 @@ class RuntimeRegistry {
|
|
|
18325
18777
|
}
|
|
18326
18778
|
|
|
18327
18779
|
// ../daemon/src/worktrees/naming.ts
|
|
18328
|
-
import { randomBytes as
|
|
18780
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
18329
18781
|
import { existsSync as existsSync11 } from "fs";
|
|
18330
18782
|
import { join as join10 } from "path";
|
|
18331
18783
|
function shortId() {
|
|
18332
|
-
return
|
|
18784
|
+
return randomBytes3(5).toString("hex");
|
|
18333
18785
|
}
|
|
18334
18786
|
function freeSlot(parent) {
|
|
18335
18787
|
for (;; ) {
|
|
@@ -18475,6 +18927,10 @@ function openAskOf(entries) {
|
|
|
18475
18927
|
open.set(e.id, e.title);
|
|
18476
18928
|
else if (e.type === "agent-ask-end")
|
|
18477
18929
|
open.delete(e.id);
|
|
18930
|
+
else if (e.type === "handoff-proposed")
|
|
18931
|
+
open.set(e.id, `Continue in ${e.repo.name}?`);
|
|
18932
|
+
else if (e.type === "handoff" || e.type === "handoff-declined")
|
|
18933
|
+
open.delete(e.id);
|
|
18478
18934
|
}
|
|
18479
18935
|
const last = [...open.values()].at(-1)?.trim();
|
|
18480
18936
|
return last ? clip(last) : undefined;
|
|
@@ -18519,6 +18975,7 @@ var LAND_SYSTEM = "You judge whether a coding task is finished, recap where it s
|
|
|
18519
18975
|
var LAND_ASK = [
|
|
18520
18976
|
"A coding agent just stopped in a git worktree. Decide whether the work is finished and ready to merge, or still in progress: a question the agent is waiting on, a step it said it would do next, or a part of the request it did not get to.",
|
|
18521
18977
|
"The agent never commits: every change is left uncommitted for the user, and the agent saying so is normal, not a sign the work is unfinished.",
|
|
18978
|
+
"The replies are oldest first and the agent's last word is where the work stands now; it supersedes the earlier ones. A check, test or job an earlier reply said was still running is done if the last word says so, and is a step still to do only when the last word itself is waiting on it.",
|
|
18522
18979
|
"Reply with exactly this shape and nothing else:",
|
|
18523
18980
|
"Line 1: READY, or NOT READY: <what is still left to do, under 8 words, about the change itself and not the conversation>",
|
|
18524
18981
|
"Line 2: blank",
|
|
@@ -18543,7 +19000,7 @@ ${i.recentSubjects.map((s) => ` ${s}`).join(`
|
|
|
18543
19000
|
head.push(`Note from the user for the commit message, to be reflected in its wording: ${clip(i.note.trim(), 500)}`);
|
|
18544
19001
|
const diff = `Diff summary:
|
|
18545
19002
|
${i.diffStat.trim() || "(none)"}`;
|
|
18546
|
-
let blocks = i.turns
|
|
19003
|
+
let blocks = turnBlocks(i.turns);
|
|
18547
19004
|
const size = () => [...head, "", ...blocks, "", diff].join(`
|
|
18548
19005
|
|
|
18549
19006
|
`).length;
|
|
@@ -18556,14 +19013,18 @@ ${i.diffStat.trim() || "(none)"}`;
|
|
|
18556
19013
|
|
|
18557
19014
|
`);
|
|
18558
19015
|
}
|
|
18559
|
-
function
|
|
19016
|
+
function turnBlocks(turns) {
|
|
19017
|
+
const newest = turns.slice(-LAND_TURNS);
|
|
19018
|
+
return newest.map((t, n) => turnBlock(t, n === newest.length - 1));
|
|
19019
|
+
}
|
|
19020
|
+
function turnBlock(t, last) {
|
|
18560
19021
|
const lines = [];
|
|
18561
19022
|
if (t.asks.length)
|
|
18562
19023
|
lines.push(`User asked: ${clip(t.asks.join(" / "), 400)}`);
|
|
18563
19024
|
if (t.fixes.length)
|
|
18564
19025
|
lines.push(`Toyon, not the user, asked for a fix: ${clip(t.fixes.join(" / "), 400)}`);
|
|
18565
19026
|
if (t.reply.trim())
|
|
18566
|
-
lines.push(
|
|
19027
|
+
lines.push(`${last ? "Agent's last word, where the work stands now" : "Agent ended with"}: ${clip(t.reply, 1000)}`);
|
|
18567
19028
|
return lines.join(`
|
|
18568
19029
|
`);
|
|
18569
19030
|
}
|
|
@@ -18577,7 +19038,7 @@ function answerPrompt(i) {
|
|
|
18577
19038
|
const head = [ANSWER_ASK, "", `Task: ${clip(i.title, 200)}`];
|
|
18578
19039
|
if (i.firstAsk)
|
|
18579
19040
|
head.push(`First request: ${clip(i.firstAsk, 300)}`);
|
|
18580
|
-
let blocks = i.turns
|
|
19041
|
+
let blocks = turnBlocks(i.turns);
|
|
18581
19042
|
const size = () => [...head, "", ...blocks].join(`
|
|
18582
19043
|
|
|
18583
19044
|
`).length;
|
|
@@ -19284,6 +19745,40 @@ function folderDialog() {
|
|
|
19284
19745
|
};
|
|
19285
19746
|
}
|
|
19286
19747
|
|
|
19748
|
+
// ../daemon/src/core/grants.ts
|
|
19749
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
19750
|
+
class GrantCodes {
|
|
19751
|
+
live = [];
|
|
19752
|
+
ttlMs;
|
|
19753
|
+
max;
|
|
19754
|
+
now;
|
|
19755
|
+
constructor(opts = {}) {
|
|
19756
|
+
this.ttlMs = opts.ttlMs ?? 60000;
|
|
19757
|
+
this.max = opts.max ?? 32;
|
|
19758
|
+
this.now = opts.now ?? Date.now;
|
|
19759
|
+
}
|
|
19760
|
+
mint() {
|
|
19761
|
+
this.sweep();
|
|
19762
|
+
const code = randomBytes4(9).toString("base64url");
|
|
19763
|
+
this.live.push({ code, until: this.now() + this.ttlMs });
|
|
19764
|
+
if (this.live.length > this.max)
|
|
19765
|
+
this.live.splice(0, this.live.length - this.max);
|
|
19766
|
+
return { code, ms: this.ttlMs };
|
|
19767
|
+
}
|
|
19768
|
+
redeem(code) {
|
|
19769
|
+
this.sweep();
|
|
19770
|
+
const i = this.live.findIndex((g) => sameSecret(code, g.code));
|
|
19771
|
+
if (i < 0)
|
|
19772
|
+
return false;
|
|
19773
|
+
this.live.splice(i, 1);
|
|
19774
|
+
return true;
|
|
19775
|
+
}
|
|
19776
|
+
sweep() {
|
|
19777
|
+
const t = this.now();
|
|
19778
|
+
this.live = this.live.filter((g) => t < g.until);
|
|
19779
|
+
}
|
|
19780
|
+
}
|
|
19781
|
+
|
|
19287
19782
|
// ../daemon/src/core/hub.ts
|
|
19288
19783
|
class Hub {
|
|
19289
19784
|
listeners = new Map;
|
|
@@ -19563,8 +20058,21 @@ class SelfWatch {
|
|
|
19563
20058
|
}
|
|
19564
20059
|
|
|
19565
20060
|
// ../daemon/src/core/state.ts
|
|
19566
|
-
import { randomBytes as
|
|
20061
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
19567
20062
|
import { existsSync as existsSync14, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
20063
|
+
var TRUSTED_MAX = 16;
|
|
20064
|
+
function isShellOrigin(origin) {
|
|
20065
|
+
let url;
|
|
20066
|
+
try {
|
|
20067
|
+
url = new URL(origin);
|
|
20068
|
+
} catch {
|
|
20069
|
+
return false;
|
|
20070
|
+
}
|
|
20071
|
+
if (url.origin !== origin)
|
|
20072
|
+
return false;
|
|
20073
|
+
return url.protocol === "https:" || url.protocol === "http:" && isLoopbackHost(url.hostname);
|
|
20074
|
+
}
|
|
20075
|
+
var DAEMON_GROUPS = "daemon";
|
|
19568
20076
|
var GROUPS_SAVE_MS = 250;
|
|
19569
20077
|
var SEEN_WORKTREES = 50;
|
|
19570
20078
|
var empty = { repos: [], worktrees: [], sessions: {} };
|
|
@@ -19602,7 +20110,7 @@ function loadState(paths) {
|
|
|
19602
20110
|
delete state.seen?.[id];
|
|
19603
20111
|
}
|
|
19604
20112
|
for (const id of Object.keys(state.groups ?? {})) {
|
|
19605
|
-
if (!ids.has(id) && !id.startsWith("disc-"))
|
|
20113
|
+
if (!ids.has(id) && !id.startsWith("disc-") && id !== DAEMON_GROUPS)
|
|
19606
20114
|
delete state.groups?.[id];
|
|
19607
20115
|
}
|
|
19608
20116
|
if (state.modelCache) {
|
|
@@ -19625,7 +20133,7 @@ function loadOrCreateToken(paths) {
|
|
|
19625
20133
|
const TOKEN_FILE = paths.tokenFile;
|
|
19626
20134
|
if (existsSync14(TOKEN_FILE))
|
|
19627
20135
|
return readFileSync10(TOKEN_FILE, "utf8").trim();
|
|
19628
|
-
const token =
|
|
20136
|
+
const token = randomBytes5(32).toString("hex");
|
|
19629
20137
|
writeFileSync5(TOKEN_FILE, token, { mode: 384 });
|
|
19630
20138
|
return token;
|
|
19631
20139
|
}
|
|
@@ -19716,8 +20224,12 @@ class StateStore {
|
|
|
19716
20224
|
delete this.state.sessions[id];
|
|
19717
20225
|
if (this.state.seen)
|
|
19718
20226
|
delete this.state.seen[id];
|
|
19719
|
-
if (this.state.groups)
|
|
20227
|
+
if (this.state.groups) {
|
|
20228
|
+
const left = this.state.groups[id] ?? [];
|
|
19720
20229
|
delete this.state.groups[id];
|
|
20230
|
+
if (left.length > 0)
|
|
20231
|
+
this.state.groups[DAEMON_GROUPS] = [...this.state.groups[DAEMON_GROUPS] ?? [], ...left];
|
|
20232
|
+
}
|
|
19721
20233
|
this.save();
|
|
19722
20234
|
}
|
|
19723
20235
|
groups(worktreeId) {
|
|
@@ -19767,6 +20279,16 @@ class StateStore {
|
|
|
19767
20279
|
this.state.bootAt = id;
|
|
19768
20280
|
this.save();
|
|
19769
20281
|
}
|
|
20282
|
+
setMode(worktreeId, mode) {
|
|
20283
|
+
const wt = this.requireWorktree(worktreeId);
|
|
20284
|
+
if (wt.kind === "spare")
|
|
20285
|
+
throw new UserError("no mode for a spare worktree");
|
|
20286
|
+
if (wt.mode === mode)
|
|
20287
|
+
return false;
|
|
20288
|
+
wt.mode = mode;
|
|
20289
|
+
this.save();
|
|
20290
|
+
return true;
|
|
20291
|
+
}
|
|
19770
20292
|
session(worktreeId) {
|
|
19771
20293
|
return this.state.sessions[worktreeId];
|
|
19772
20294
|
}
|
|
@@ -19840,6 +20362,17 @@ class StateStore {
|
|
|
19840
20362
|
this.state.pairedAt = Date.now();
|
|
19841
20363
|
this.save();
|
|
19842
20364
|
}
|
|
20365
|
+
get trustedOrigins() {
|
|
20366
|
+
return (this.state.trustedOrigins ?? []).map((t) => t.origin);
|
|
20367
|
+
}
|
|
20368
|
+
trustOrigin(origin) {
|
|
20369
|
+
if (!isShellOrigin(origin))
|
|
20370
|
+
return;
|
|
20371
|
+
const list = (this.state.trustedOrigins ?? []).filter((t) => t.origin !== origin);
|
|
20372
|
+
list.push({ origin, at: Date.now() });
|
|
20373
|
+
this.state.trustedOrigins = list.slice(-TRUSTED_MAX);
|
|
20374
|
+
this.save();
|
|
20375
|
+
}
|
|
19843
20376
|
save() {
|
|
19844
20377
|
if (this.groupsTimer)
|
|
19845
20378
|
clearTimeout(this.groupsTimer);
|
|
@@ -26048,10 +26581,10 @@ function servedLocally(origin) {
|
|
|
26048
26581
|
}
|
|
26049
26582
|
|
|
26050
26583
|
// ../daemon/src/server/ws.ts
|
|
26051
|
-
import { homedir as homedir11 } from "os";
|
|
26584
|
+
import { homedir as homedir11, hostname as hostname2 } from "os";
|
|
26052
26585
|
|
|
26053
26586
|
// ../daemon/src/core/pair.ts
|
|
26054
|
-
import { randomBytes as
|
|
26587
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
26055
26588
|
class PairCodes {
|
|
26056
26589
|
now;
|
|
26057
26590
|
live = null;
|
|
@@ -26059,7 +26592,7 @@ class PairCodes {
|
|
|
26059
26592
|
this.now = now;
|
|
26060
26593
|
}
|
|
26061
26594
|
mint() {
|
|
26062
|
-
const code =
|
|
26595
|
+
const code = randomBytes6(9).toString("base64url");
|
|
26063
26596
|
this.live = { code, until: this.now() + PAIR_TTL_MS };
|
|
26064
26597
|
return { code, ms: PAIR_TTL_MS };
|
|
26065
26598
|
}
|
|
@@ -26114,7 +26647,7 @@ async function readTailnetPhones(run = ((bin) => bin ? cli(bin) : null)(Bun.whic
|
|
|
26114
26647
|
}
|
|
26115
26648
|
|
|
26116
26649
|
// ../daemon/src/server/handlers.ts
|
|
26117
|
-
import { randomBytes as
|
|
26650
|
+
import { randomBytes as randomBytes7 } from "crypto";
|
|
26118
26651
|
|
|
26119
26652
|
// ../daemon/src/agent/config.ts
|
|
26120
26653
|
import { existsSync as existsSync22, readFileSync as readFileSync14 } from "fs";
|
|
@@ -26831,7 +27364,7 @@ var handlers = {
|
|
|
26831
27364
|
await s.worktrees.create(msg.repoId, msg.prompt, { ...opts, worktreeId: msg.worktreeId });
|
|
26832
27365
|
return;
|
|
26833
27366
|
}
|
|
26834
|
-
const group =
|
|
27367
|
+
const group = randomBytes7(4).toString("hex");
|
|
26835
27368
|
const made = await Promise.allSettled(Array.from({ length: of }, (_, i) => s.worktrees.create(msg.repoId, msg.prompt, {
|
|
26836
27369
|
...opts,
|
|
26837
27370
|
...i === 0 ? { worktreeId: msg.worktreeId } : {},
|
|
@@ -27180,6 +27713,14 @@ var handlers = {
|
|
|
27180
27713
|
requireRun(s, msg.worktreeId);
|
|
27181
27714
|
s.runtime.agentFor(msg.worktreeId)?.answer(msg.askId, { kind: "choice", choiceId: msg.choiceId });
|
|
27182
27715
|
},
|
|
27716
|
+
async "handoff-answer"(msg, _ctx, s) {
|
|
27717
|
+
requireRun(s, msg.worktreeId);
|
|
27718
|
+
await s.handoff.answer(msg.worktreeId, msg.id, msg.go, msg.note);
|
|
27719
|
+
},
|
|
27720
|
+
async "handoff-ask"(msg, _ctx, s) {
|
|
27721
|
+
requireRun(s, msg.worktreeId);
|
|
27722
|
+
await s.handoff.ask(msg.worktreeId, msg.repoId, msg.text);
|
|
27723
|
+
},
|
|
27183
27724
|
"agent-config"(msg, ctx, s) {
|
|
27184
27725
|
const spec = s.agents.require(msg.agent);
|
|
27185
27726
|
const repoPath = msg.repoId ? s.state.repo(msg.repoId)?.path ?? null : null;
|
|
@@ -27320,7 +27861,37 @@ async function dispatch(msg, ctx, s) {
|
|
|
27320
27861
|
|
|
27321
27862
|
// ../daemon/src/server/http.ts
|
|
27322
27863
|
import { existsSync as existsSync23 } from "fs";
|
|
27864
|
+
import { hostname } from "os";
|
|
27323
27865
|
import { join as join21 } from "path";
|
|
27866
|
+
|
|
27867
|
+
// ../daemon/src/server/cors.ts
|
|
27868
|
+
var ALLOW_HEADERS = "authorization, content-type";
|
|
27869
|
+
var ALLOW_METHODS = "GET, POST";
|
|
27870
|
+
var MAX_AGE = "86400";
|
|
27871
|
+
function preflight(allowOrigin) {
|
|
27872
|
+
const res = new Response(null, { status: 204 });
|
|
27873
|
+
return withCors(res, allowOrigin);
|
|
27874
|
+
}
|
|
27875
|
+
function withCors(res, origin) {
|
|
27876
|
+
res.headers.set("access-control-allow-origin", origin);
|
|
27877
|
+
res.headers.set("access-control-allow-headers", ALLOW_HEADERS);
|
|
27878
|
+
res.headers.set("access-control-allow-methods", ALLOW_METHODS);
|
|
27879
|
+
res.headers.set("access-control-max-age", MAX_AGE);
|
|
27880
|
+
res.headers.append("vary", "Origin");
|
|
27881
|
+
return res;
|
|
27882
|
+
}
|
|
27883
|
+
function isLoopbackOrigin(origin) {
|
|
27884
|
+
try {
|
|
27885
|
+
const u = new URL(origin);
|
|
27886
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
27887
|
+
return false;
|
|
27888
|
+
return isLoopbackHost(u.hostname);
|
|
27889
|
+
} catch {
|
|
27890
|
+
return false;
|
|
27891
|
+
}
|
|
27892
|
+
}
|
|
27893
|
+
|
|
27894
|
+
// ../daemon/src/server/http.ts
|
|
27324
27895
|
var IMMUTABLE = "private, max-age=31536000, immutable";
|
|
27325
27896
|
var NO_STORE = "no-store";
|
|
27326
27897
|
function attachedHeaders(imageType) {
|
|
@@ -27333,25 +27904,31 @@ function attachedHeaders(imageType) {
|
|
|
27333
27904
|
function createFetch(opts) {
|
|
27334
27905
|
const grant = previewGrant(opts.token);
|
|
27335
27906
|
const remote = opts.remote;
|
|
27907
|
+
const gate = remote && { grant, host: remote.host, redeem: (code) => opts.grants.redeem(code) };
|
|
27336
27908
|
const health = () => Response.json({
|
|
27337
27909
|
ok: true,
|
|
27338
27910
|
version: opts.version,
|
|
27339
27911
|
pid: process.pid,
|
|
27340
27912
|
branded: opts.branded(),
|
|
27341
27913
|
remote: remote && { host: remote.host, previews: remote.previews },
|
|
27914
|
+
machine: machineLabel(hostname(), remote),
|
|
27342
27915
|
managed: opts.managed,
|
|
27343
27916
|
...opts.metrics()
|
|
27344
27917
|
});
|
|
27345
|
-
|
|
27346
|
-
const
|
|
27347
|
-
if (
|
|
27348
|
-
return
|
|
27349
|
-
|
|
27350
|
-
|
|
27351
|
-
|
|
27918
|
+
const corsAllow = (req, url, d) => {
|
|
27919
|
+
const origin = req.headers.get("origin");
|
|
27920
|
+
if (!origin || d.kind !== "local" && d.kind !== "shell")
|
|
27921
|
+
return null;
|
|
27922
|
+
if (url.pathname === "/pair/redeem")
|
|
27923
|
+
return origin;
|
|
27924
|
+
if (opts.trusted().includes(origin))
|
|
27925
|
+
return origin;
|
|
27926
|
+
return d.kind === "local" && isLoopbackOrigin(origin) ? origin : null;
|
|
27927
|
+
};
|
|
27928
|
+
async function route(req, srv, url, d) {
|
|
27352
27929
|
const remoteShell = d.kind === "shell";
|
|
27353
|
-
if (d.kind === "preview" && d.worktreeId !== null) {
|
|
27354
|
-
const pass = passPreview(req,
|
|
27930
|
+
if (d.kind === "preview" && d.worktreeId !== null && gate) {
|
|
27931
|
+
const pass = passPreview(req, gate);
|
|
27355
27932
|
if (!pass.ok)
|
|
27356
27933
|
return pass.response;
|
|
27357
27934
|
const handler = opts.preview(d.worktreeId);
|
|
@@ -27425,6 +28002,15 @@ function createFetch(opts) {
|
|
|
27425
28002
|
const phones = remote && isTailnetName(remote.host) ? await opts.phones() : null;
|
|
27426
28003
|
return Response.json(phones, { headers: { "cache-control": NO_STORE } });
|
|
27427
28004
|
}
|
|
28005
|
+
if (url.pathname === "/preview-grant" && req.method === "POST") {
|
|
28006
|
+
if (!sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`)) {
|
|
28007
|
+
return new Response("unauthorized", { status: 401 });
|
|
28008
|
+
}
|
|
28009
|
+
if (!remote)
|
|
28010
|
+
return new Response("previews here are not reached by name", { status: 400 });
|
|
28011
|
+
const body = opts.grants.mint();
|
|
28012
|
+
return Response.json(body, { headers: { "cache-control": NO_STORE } });
|
|
28013
|
+
}
|
|
27428
28014
|
if (url.pathname === "/pair/redeem" && req.method === "POST") {
|
|
27429
28015
|
if (!remote || !remoteShell)
|
|
27430
28016
|
return new Response("not found", { status: 404 });
|
|
@@ -27432,7 +28018,7 @@ function createFetch(opts) {
|
|
|
27432
28018
|
if (typeof body.code !== "string" || !opts.pair.redeem(body.code)) {
|
|
27433
28019
|
return new Response("code expired", { status: 401 });
|
|
27434
28020
|
}
|
|
27435
|
-
opts.onPaired();
|
|
28021
|
+
opts.onPaired(req.headers.get("origin"));
|
|
27436
28022
|
const reply = { token: opts.token };
|
|
27437
28023
|
return Response.json(reply, {
|
|
27438
28024
|
headers: { "cache-control": NO_STORE, "set-cookie": grantCookie(grant, remote.host) }
|
|
@@ -27560,6 +28146,28 @@ function createFetch(opts) {
|
|
|
27560
28146
|
if (existsSync23(index))
|
|
27561
28147
|
return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE } });
|
|
27562
28148
|
return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
|
|
28149
|
+
}
|
|
28150
|
+
return async function fetch2(req, srv) {
|
|
28151
|
+
const url = new URL(req.url);
|
|
28152
|
+
if (remote?.front === "edge" && url.pathname === "/health")
|
|
28153
|
+
return health();
|
|
28154
|
+
if (url.pathname.startsWith("/mcp/")) {
|
|
28155
|
+
const peer = srv.requestIP(req)?.address ?? "";
|
|
28156
|
+
if (!isLoopbackPeer(peer))
|
|
28157
|
+
return new Response("forbidden", { status: 403 });
|
|
28158
|
+
const worktreeId = url.pathname.slice("/mcp/".length);
|
|
28159
|
+
if (!worktreeId || worktreeId.includes("/"))
|
|
28160
|
+
return new Response("not found", { status: 404 });
|
|
28161
|
+
return opts.mcp(req, worktreeId);
|
|
28162
|
+
}
|
|
28163
|
+
const d = door(req, srv.requestIP(req)?.address ?? "", remote, "daemon");
|
|
28164
|
+
if (d.kind === "refused")
|
|
28165
|
+
return d.response;
|
|
28166
|
+
const allow = corsAllow(req, url, d);
|
|
28167
|
+
if (allow && req.method === "OPTIONS")
|
|
28168
|
+
return preflight(allow);
|
|
28169
|
+
const res = await route(req, srv, url, d);
|
|
28170
|
+
return res && allow ? withCors(res, allow) : res;
|
|
27563
28171
|
};
|
|
27564
28172
|
}
|
|
27565
28173
|
|
|
@@ -27688,22 +28296,20 @@ function startServer(opts) {
|
|
|
27688
28296
|
agents: s.agents.adapters(),
|
|
27689
28297
|
sockets: [...sockets].map((ws) => ({ subs: [...ws.data.subs], sent: ws.data.sent, bytes: ws.data.bytes }))
|
|
27690
28298
|
});
|
|
27691
|
-
let
|
|
27692
|
-
let statusesDirty = false;
|
|
28299
|
+
let statusesQueued = false;
|
|
27693
28300
|
const worktreesChanged = () => {
|
|
27694
|
-
if (
|
|
27695
|
-
statusesDirty = true;
|
|
28301
|
+
if (statusesQueued)
|
|
27696
28302
|
return;
|
|
27697
|
-
|
|
27698
|
-
|
|
27699
|
-
|
|
27700
|
-
|
|
27701
|
-
|
|
27702
|
-
broadcast({ t: "worktrees", rows
|
|
27703
|
-
}
|
|
27704
|
-
|
|
27705
|
-
|
|
27706
|
-
}
|
|
28303
|
+
statusesQueued = true;
|
|
28304
|
+
setTimeout(() => {
|
|
28305
|
+
statusesQueued = false;
|
|
28306
|
+
try {
|
|
28307
|
+
const { rows, trunks } = s.worktrees.snapshot();
|
|
28308
|
+
broadcast({ t: "worktrees", rows, trunks });
|
|
28309
|
+
} catch (e) {
|
|
28310
|
+
log.warn("ws", "worktrees broadcast failed", e);
|
|
28311
|
+
}
|
|
28312
|
+
}, 0);
|
|
27707
28313
|
};
|
|
27708
28314
|
s.hub.on("worktreesChanged", worktreesChanged);
|
|
27709
28315
|
s.hub.on("agentStatus", worktreesChanged);
|
|
@@ -27817,6 +28423,7 @@ function startServer(opts) {
|
|
|
27817
28423
|
s.hub.on("attachmentsChanged", (boxId, items, clientId) => broadcast({ t: "attachments", boxId, items, ...clientId ? { clientId } : {} }));
|
|
27818
28424
|
const helloFrame = async () => {
|
|
27819
28425
|
await s.update.refresh();
|
|
28426
|
+
const { rows, trunks } = s.worktrees.snapshot();
|
|
27820
28427
|
return {
|
|
27821
28428
|
t: "hello",
|
|
27822
28429
|
version,
|
|
@@ -27824,8 +28431,8 @@ function startServer(opts) {
|
|
|
27824
28431
|
registry: await s.update.registry(),
|
|
27825
28432
|
protocol: PROTOCOL_VERSION,
|
|
27826
28433
|
repos: s.state.repos,
|
|
27827
|
-
rows
|
|
27828
|
-
trunks
|
|
28434
|
+
rows,
|
|
28435
|
+
trunks,
|
|
27829
28436
|
themes: s.themes.themes,
|
|
27830
28437
|
themePrefs: s.themes.prefs,
|
|
27831
28438
|
agents: agentInfos(),
|
|
@@ -27834,7 +28441,8 @@ function startServer(opts) {
|
|
|
27834
28441
|
home: homedir11(),
|
|
27835
28442
|
folderDialog: process.platform === "darwin" && !cloud.enabled,
|
|
27836
28443
|
keepAwake: s.keepAwake.setting(),
|
|
27837
|
-
remote: opts.remote
|
|
28444
|
+
remote: opts.remote,
|
|
28445
|
+
machine: machineLabel(hostname2(), opts.remote),
|
|
27838
28446
|
paired: s.state.paired,
|
|
27839
28447
|
gitIdentity: await s.repos.gitIdentity(),
|
|
27840
28448
|
pending: s.repos.pending,
|
|
@@ -27874,11 +28482,21 @@ function startServer(opts) {
|
|
|
27874
28482
|
restart: (now) => s.restarter.request({ now }),
|
|
27875
28483
|
restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
|
|
27876
28484
|
pair: new PairCodes,
|
|
27877
|
-
|
|
27878
|
-
|
|
28485
|
+
grants: opts.grants,
|
|
28486
|
+
onPaired: (origin) => {
|
|
28487
|
+
const own = origin === null || origin === `https://${opts.remote?.host}`;
|
|
28488
|
+
if (own)
|
|
28489
|
+
s.state.notePaired();
|
|
28490
|
+
else {
|
|
28491
|
+
s.state.trustOrigin(origin);
|
|
28492
|
+
if (s.state.trustedOrigins.includes(origin))
|
|
28493
|
+
opts.onTrusted(origin);
|
|
28494
|
+
}
|
|
27879
28495
|
broadcast({ t: "paired" });
|
|
27880
28496
|
},
|
|
27881
|
-
|
|
28497
|
+
trusted: () => s.state.trustedOrigins,
|
|
28498
|
+
phones: () => readTailnetPhones(),
|
|
28499
|
+
mcp: (req, id) => s.mcp.fetch(req, id)
|
|
27882
28500
|
}),
|
|
27883
28501
|
websocket: {
|
|
27884
28502
|
async open(ws) {
|
|
@@ -29060,6 +29678,245 @@ class FixService {
|
|
|
29060
29678
|
}
|
|
29061
29679
|
}
|
|
29062
29680
|
|
|
29681
|
+
// ../daemon/src/worktrees/handoff.ts
|
|
29682
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
29683
|
+
var MESSAGE_MAX = 20000;
|
|
29684
|
+
var TITLE_MAX2 = 200;
|
|
29685
|
+
var PROJECT_MAX = 200;
|
|
29686
|
+
var PER_TURN_MAX = 3;
|
|
29687
|
+
var refused = (text) => ({ text, isError: true });
|
|
29688
|
+
function openProposals(entries) {
|
|
29689
|
+
const open = new Map;
|
|
29690
|
+
for (const { event } of entries) {
|
|
29691
|
+
if (event.type === "handoff-proposed")
|
|
29692
|
+
open.set(event.id, event);
|
|
29693
|
+
else if (event.type === "handoff" || event.type === "handoff-declined")
|
|
29694
|
+
open.delete(event.id);
|
|
29695
|
+
}
|
|
29696
|
+
return [...open.values()];
|
|
29697
|
+
}
|
|
29698
|
+
function proposedThisTurn(entries) {
|
|
29699
|
+
let n = 0;
|
|
29700
|
+
for (let i = entries.length - 1;i >= 0; i--) {
|
|
29701
|
+
const e = entries[i].event;
|
|
29702
|
+
if (e.type === "turn-start")
|
|
29703
|
+
break;
|
|
29704
|
+
if (e.type === "handoff-proposed")
|
|
29705
|
+
n++;
|
|
29706
|
+
}
|
|
29707
|
+
return n;
|
|
29708
|
+
}
|
|
29709
|
+
|
|
29710
|
+
class HandoffService {
|
|
29711
|
+
d;
|
|
29712
|
+
settling = new Set;
|
|
29713
|
+
constructor(d) {
|
|
29714
|
+
this.d = d;
|
|
29715
|
+
d.hub.on("landed", (worktreeId) => this.landed(worktreeId));
|
|
29716
|
+
}
|
|
29717
|
+
targets(worktreeId) {
|
|
29718
|
+
const own = this.d.state.worktree(worktreeId)?.repoId;
|
|
29719
|
+
return this.d.state.repos.filter((r) => r.id !== own);
|
|
29720
|
+
}
|
|
29721
|
+
resolve(worktreeId, project) {
|
|
29722
|
+
const q = project.trim();
|
|
29723
|
+
const others = this.targets(worktreeId);
|
|
29724
|
+
if (others.length === 0)
|
|
29725
|
+
return { refused: "No other project is open in Toyon, so there is nothing to hand off to." };
|
|
29726
|
+
const names = others.map((r) => r.name).join(", ");
|
|
29727
|
+
if (!q)
|
|
29728
|
+
return { refused: `Name the project to continue in. Open projects: ${names}.` };
|
|
29729
|
+
const found = match(others, q);
|
|
29730
|
+
if (found.length === 1)
|
|
29731
|
+
return { repo: found[0] };
|
|
29732
|
+
if (found.length > 1) {
|
|
29733
|
+
return {
|
|
29734
|
+
refused: `"${q}" names more than one open project: ${found.map((r) => r.path).join(", ")}. Say which by its path.`
|
|
29735
|
+
};
|
|
29736
|
+
}
|
|
29737
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
29738
|
+
const own = wt ? this.d.state.repo(wt.repoId) : undefined;
|
|
29739
|
+
if (own && match([own], q).length === 1) {
|
|
29740
|
+
return {
|
|
29741
|
+
refused: `${own.name} is this worktree's own project; a handoff goes to another one. Open projects: ${names}.`
|
|
29742
|
+
};
|
|
29743
|
+
}
|
|
29744
|
+
return { refused: `No open project is called "${q}". Open projects: ${names}.` };
|
|
29745
|
+
}
|
|
29746
|
+
tool(worktreeId) {
|
|
29747
|
+
const names = this.targets(worktreeId).map((r) => r.name);
|
|
29748
|
+
const open = names.length ? `Open projects you can name: ${names.join(", ")}.` : "No other project is open right now.";
|
|
29749
|
+
return {
|
|
29750
|
+
name: HANDOFF_TOOL,
|
|
29751
|
+
description: `Propose continuing the current task in another project Toyon has open, when a change belongs there and not in this worktree. Nothing starts: a card with your message appears in this chat and the person decides. ${open}`,
|
|
29752
|
+
inputSchema: {
|
|
29753
|
+
type: "object",
|
|
29754
|
+
properties: {
|
|
29755
|
+
project: { type: "string", description: "The other project's name as listed, or its path." },
|
|
29756
|
+
message: {
|
|
29757
|
+
type: "string",
|
|
29758
|
+
description: "What that project's agent starts from: what is needed and why, how to reproduce it, and the paths in this worktree that depend on it. It is sent as that agent's first message."
|
|
29759
|
+
},
|
|
29760
|
+
title: { type: "string", description: "A short name for the new worktree, a few words." }
|
|
29761
|
+
},
|
|
29762
|
+
required: ["project", "message"],
|
|
29763
|
+
additionalProperties: false
|
|
29764
|
+
}
|
|
29765
|
+
};
|
|
29766
|
+
}
|
|
29767
|
+
async call(worktreeId, args) {
|
|
29768
|
+
const a = args && typeof args === "object" && !Array.isArray(args) ? args : null;
|
|
29769
|
+
const shaped = a && typeof a.project === "string" && typeof a.message === "string" && (a.title === undefined || typeof a.title === "string");
|
|
29770
|
+
if (!shaped)
|
|
29771
|
+
return refused("handoff takes { project: string, message: string, title?: string }.");
|
|
29772
|
+
return this.propose(worktreeId, { project: a.project, message: a.message, ...a.title ? { title: a.title } : {} }, "agent");
|
|
29773
|
+
}
|
|
29774
|
+
propose(worktreeId, p, by) {
|
|
29775
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
29776
|
+
if (!wt)
|
|
29777
|
+
throw new UserError("that chat is gone");
|
|
29778
|
+
if (p.project.length > PROJECT_MAX)
|
|
29779
|
+
return refused(`The project name is over ${PROJECT_MAX} characters.`);
|
|
29780
|
+
if ((p.title ?? "").length > TITLE_MAX2)
|
|
29781
|
+
return refused(`The title is over ${TITLE_MAX2} characters; a few words.`);
|
|
29782
|
+
if (p.message.length > MESSAGE_MAX) {
|
|
29783
|
+
return refused(`The message is over ${MESSAGE_MAX} characters; cut it to what the other agent needs.`);
|
|
29784
|
+
}
|
|
29785
|
+
const target = this.resolve(worktreeId, p.project);
|
|
29786
|
+
if ("refused" in target)
|
|
29787
|
+
return refused(target.refused);
|
|
29788
|
+
const message = p.message.trim();
|
|
29789
|
+
if (!message)
|
|
29790
|
+
return refused("Write the message the other project's agent should start from.");
|
|
29791
|
+
const agent = this.d.runtime.ensureAgent(wt).agent;
|
|
29792
|
+
const entries = agent.transcript();
|
|
29793
|
+
if (openProposals(entries).length > 0) {
|
|
29794
|
+
return refused("A handoff is already waiting for the person on this chat; finish your turn.");
|
|
29795
|
+
}
|
|
29796
|
+
if (by === "agent" && proposedThisTurn(entries) >= PER_TURN_MAX) {
|
|
29797
|
+
return refused("Three handoffs were proposed this turn already; finish your turn and let the person answer.");
|
|
29798
|
+
}
|
|
29799
|
+
const title = p.title?.trim();
|
|
29800
|
+
const { repo } = target;
|
|
29801
|
+
agent.note({
|
|
29802
|
+
type: "handoff-proposed",
|
|
29803
|
+
id: randomUUID2(),
|
|
29804
|
+
repo: { id: repo.id, name: repo.name, path: repo.path },
|
|
29805
|
+
message,
|
|
29806
|
+
...title ? { title } : {},
|
|
29807
|
+
by,
|
|
29808
|
+
mode: wt.mode ?? DEFAULT_PERMISSION_MODE,
|
|
29809
|
+
ts: Date.now()
|
|
29810
|
+
});
|
|
29811
|
+
return { text: "Proposed to the person as a card; they decide. Finish your turn." };
|
|
29812
|
+
}
|
|
29813
|
+
async answer(worktreeId, id, go, note) {
|
|
29814
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
29815
|
+
if (!wt)
|
|
29816
|
+
throw new UserError("that chat is gone");
|
|
29817
|
+
const agent = this.d.runtime.ensureAgent(wt).agent;
|
|
29818
|
+
const entries = agent.transcript();
|
|
29819
|
+
const proposed = entries.find(({ event }) => event.type === "handoff-proposed" && event.id === id)?.event;
|
|
29820
|
+
if (!proposed)
|
|
29821
|
+
throw new UserError("That card is not in this chat");
|
|
29822
|
+
if (!openProposals(entries).some((p) => p.id === id)) {
|
|
29823
|
+
return log.debug(worktreeId, `answer for a handoff that already closed (${id})`);
|
|
29824
|
+
}
|
|
29825
|
+
if (this.settling.has(id))
|
|
29826
|
+
return;
|
|
29827
|
+
if (!go) {
|
|
29828
|
+
agent.note({ type: "handoff-declined", id, ts: Date.now() });
|
|
29829
|
+
return;
|
|
29830
|
+
}
|
|
29831
|
+
this.settling.add(id);
|
|
29832
|
+
try {
|
|
29833
|
+
const repo = this.d.state.repo(proposed.repo.id);
|
|
29834
|
+
if (!repo)
|
|
29835
|
+
throw new UserError(`${proposed.repo.name} is no longer open in Toyon`);
|
|
29836
|
+
const origin = this.d.state.requireRepo(wt.repoId);
|
|
29837
|
+
const added = note?.trim() || undefined;
|
|
29838
|
+
const prompt = handoffPrompt(proposed.message, added, { path: wt.path, project: origin.name, branch: wt.branch });
|
|
29839
|
+
const made = await this.d.create(repo.id, prompt, {
|
|
29840
|
+
...proposed.title ? { title: proposed.title } : {},
|
|
29841
|
+
mode: proposed.mode,
|
|
29842
|
+
from: { kind: "worktree", ref: wt.branch, origin: { id: wt.id, repoId: wt.repoId } }
|
|
29843
|
+
});
|
|
29844
|
+
agent.note({
|
|
29845
|
+
type: "handoff",
|
|
29846
|
+
id,
|
|
29847
|
+
worktreeId: made.id,
|
|
29848
|
+
repoId: repo.id,
|
|
29849
|
+
repoName: repo.name,
|
|
29850
|
+
title: made.title,
|
|
29851
|
+
ts: Date.now()
|
|
29852
|
+
});
|
|
29853
|
+
} catch (e) {
|
|
29854
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
29855
|
+
agent.note({ type: "handoff-declined", id, outcome: "failed", reason, ts: Date.now() });
|
|
29856
|
+
throw e;
|
|
29857
|
+
} finally {
|
|
29858
|
+
this.settling.delete(id);
|
|
29859
|
+
}
|
|
29860
|
+
}
|
|
29861
|
+
async ask(worktreeId, repoId, text) {
|
|
29862
|
+
const wt = this.d.state.requireWorktree(worktreeId);
|
|
29863
|
+
const repo = this.d.state.repo(repoId);
|
|
29864
|
+
if (!repo)
|
|
29865
|
+
throw new UserError("no such project");
|
|
29866
|
+
if (repo.id === wt.repoId)
|
|
29867
|
+
throw new UserError(`${repo.name} is this worktree's own project`);
|
|
29868
|
+
const agent = this.d.runtime.agentFor(worktreeId);
|
|
29869
|
+
if (!agent)
|
|
29870
|
+
throw new UserError("worktree still starting; try again in a moment");
|
|
29871
|
+
if (agent.mcpTools === null)
|
|
29872
|
+
await agent.warm();
|
|
29873
|
+
if (agent.mcpTools === true) {
|
|
29874
|
+
agent.send(handoffAskPrompt(repo, text), { asked: { kind: "handoff", why: `continue in ${repo.name}` } });
|
|
29875
|
+
return;
|
|
29876
|
+
}
|
|
29877
|
+
const own = text?.trim();
|
|
29878
|
+
if (!own)
|
|
29879
|
+
throw new UserError(`Write what ${repo.name} should do first`);
|
|
29880
|
+
const r = this.propose(worktreeId, { project: repo.path, message: own }, "person");
|
|
29881
|
+
if (r.isError)
|
|
29882
|
+
throw new UserError(r.text);
|
|
29883
|
+
}
|
|
29884
|
+
landed(worktreeId) {
|
|
29885
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
29886
|
+
if (wt?.from?.kind !== "worktree")
|
|
29887
|
+
return;
|
|
29888
|
+
const origin = this.d.state.worktree(wt.from.origin.id);
|
|
29889
|
+
if (origin?.kind !== "worktree")
|
|
29890
|
+
return;
|
|
29891
|
+
const repo = this.d.state.repo(wt.repoId);
|
|
29892
|
+
if (!repo)
|
|
29893
|
+
return;
|
|
29894
|
+
this.d.runtime.ensureAgent(origin).agent.note({
|
|
29895
|
+
type: "handoff-landed",
|
|
29896
|
+
worktreeId: wt.id,
|
|
29897
|
+
repoName: repo.name,
|
|
29898
|
+
title: wt.title,
|
|
29899
|
+
...wt.pr?.url ? { url: wt.pr.url } : {},
|
|
29900
|
+
ts: Date.now()
|
|
29901
|
+
});
|
|
29902
|
+
}
|
|
29903
|
+
}
|
|
29904
|
+
function match(repos, q) {
|
|
29905
|
+
const lower = q.toLowerCase();
|
|
29906
|
+
const ladder = [
|
|
29907
|
+
(r) => r.path === q,
|
|
29908
|
+
(r) => r.name === q,
|
|
29909
|
+
(r) => r.name.toLowerCase() === lower,
|
|
29910
|
+
(r) => r.name.toLowerCase().startsWith(lower)
|
|
29911
|
+
];
|
|
29912
|
+
for (const rung of ladder) {
|
|
29913
|
+
const found = repos.filter(rung);
|
|
29914
|
+
if (found.length > 0)
|
|
29915
|
+
return found;
|
|
29916
|
+
}
|
|
29917
|
+
return [];
|
|
29918
|
+
}
|
|
29919
|
+
|
|
29063
29920
|
// ../daemon/src/worktrees/landing.ts
|
|
29064
29921
|
var TAIL_CHARS = 400;
|
|
29065
29922
|
var DIFF_CHARS = 2000;
|
|
@@ -29866,7 +30723,7 @@ var UNWATCHED = {
|
|
|
29866
30723
|
step() {},
|
|
29867
30724
|
git: (cwd, args) => stepRun(cwd, args)
|
|
29868
30725
|
};
|
|
29869
|
-
function
|
|
30726
|
+
function refused2(what, r, n = 200) {
|
|
29870
30727
|
if (r.exit === "timeout") {
|
|
29871
30728
|
const after = describeDuration(r.ceilingMs ?? STEP_TIMEOUT_MS);
|
|
29872
30729
|
return `${what} gave up after ${after}${r.shown ? "; what it printed is on the chat" : ""}`;
|
|
@@ -29877,7 +30734,7 @@ function stepRefused(what, r, n = 200) {
|
|
|
29877
30734
|
const row = typeof r.exit === "number" && r.hook ? rowOf(r) : undefined;
|
|
29878
30735
|
return {
|
|
29879
30736
|
ok: false,
|
|
29880
|
-
message:
|
|
30737
|
+
message: refused2(what, r, n),
|
|
29881
30738
|
...row && r.hook ? { hook: { ...row, name: r.hook } } : {}
|
|
29882
30739
|
};
|
|
29883
30740
|
}
|
|
@@ -30217,7 +31074,7 @@ async function fetchBase(worktreePath, repo, w = UNWATCHED) {
|
|
|
30217
31074
|
w.step(`fetching ${base}`);
|
|
30218
31075
|
const f = await w.git(worktreePath, ["fetch", "--quiet", tracked.remote, tracked.merge]);
|
|
30219
31076
|
if (!f.ok)
|
|
30220
|
-
return { ok: false, message:
|
|
31077
|
+
return { ok: false, message: refused2("fetch failed", f) };
|
|
30221
31078
|
return { ok: true, message: `fetched ${base}` };
|
|
30222
31079
|
}
|
|
30223
31080
|
async function pushBranch(worktreePath, branch, w = UNWATCHED) {
|
|
@@ -30621,6 +31478,95 @@ class ArchivedGit {
|
|
|
30621
31478
|
}
|
|
30622
31479
|
}
|
|
30623
31480
|
|
|
31481
|
+
// ../daemon/src/worktrees/counts.ts
|
|
31482
|
+
var COUNTS_FLOOR_MS = 1e4;
|
|
31483
|
+
var same2 = (a, b) => a.ahead === b.ahead && a.behind === b.behind && a.unpushed === b.unpushed && a.dirty === b.dirty;
|
|
31484
|
+
|
|
31485
|
+
class Counts {
|
|
31486
|
+
d;
|
|
31487
|
+
cache = new Map;
|
|
31488
|
+
reading = new Map;
|
|
31489
|
+
marks = new Map;
|
|
31490
|
+
markAll = 0;
|
|
31491
|
+
constructor(d) {
|
|
31492
|
+
this.d = d;
|
|
31493
|
+
}
|
|
31494
|
+
get(id) {
|
|
31495
|
+
return this.cache.get(id)?.value;
|
|
31496
|
+
}
|
|
31497
|
+
stale(id) {
|
|
31498
|
+
if (id === undefined) {
|
|
31499
|
+
this.markAll++;
|
|
31500
|
+
for (const c of this.cache.values())
|
|
31501
|
+
c.at = 0;
|
|
31502
|
+
return;
|
|
31503
|
+
}
|
|
31504
|
+
this.marks.set(id, (this.marks.get(id) ?? 0) + 1);
|
|
31505
|
+
const c = this.cache.get(id);
|
|
31506
|
+
if (c)
|
|
31507
|
+
c.at = 0;
|
|
31508
|
+
}
|
|
31509
|
+
put(id, value) {
|
|
31510
|
+
return this.store(id, value);
|
|
31511
|
+
}
|
|
31512
|
+
refresh(ids) {
|
|
31513
|
+
const reads = [];
|
|
31514
|
+
for (const id of new Set(ids)) {
|
|
31515
|
+
const running = this.reading.get(id);
|
|
31516
|
+
if (running) {
|
|
31517
|
+
reads.push(running);
|
|
31518
|
+
continue;
|
|
31519
|
+
}
|
|
31520
|
+
const cached = this.cache.get(id);
|
|
31521
|
+
if (cached && Date.now() - cached.at < COUNTS_FLOOR_MS)
|
|
31522
|
+
continue;
|
|
31523
|
+
if (this.d.going(id))
|
|
31524
|
+
continue;
|
|
31525
|
+
reads.push(this.readOne(id));
|
|
31526
|
+
}
|
|
31527
|
+
if (!reads.length)
|
|
31528
|
+
return null;
|
|
31529
|
+
return Promise.all(reads).then((moved) => moved.some(Boolean));
|
|
31530
|
+
}
|
|
31531
|
+
async read(id) {
|
|
31532
|
+
this.stale(id);
|
|
31533
|
+
await this.refresh([id]);
|
|
31534
|
+
const c = this.cache.get(id);
|
|
31535
|
+
return c?.failed || !c?.value ? null : c.value;
|
|
31536
|
+
}
|
|
31537
|
+
markOf(id) {
|
|
31538
|
+
return (this.marks.get(id) ?? 0) + this.markAll;
|
|
31539
|
+
}
|
|
31540
|
+
readOne(id) {
|
|
31541
|
+
const read = (async () => {
|
|
31542
|
+
for (;; ) {
|
|
31543
|
+
const mark = this.markOf(id);
|
|
31544
|
+
let value = null;
|
|
31545
|
+
try {
|
|
31546
|
+
value = await this.d.read(id);
|
|
31547
|
+
} catch {}
|
|
31548
|
+
if (this.d.going(id))
|
|
31549
|
+
return false;
|
|
31550
|
+
if (this.markOf(id) !== mark)
|
|
31551
|
+
continue;
|
|
31552
|
+
if (value)
|
|
31553
|
+
return this.store(id, value);
|
|
31554
|
+
this.cache.set(id, { value: this.get(id), at: Date.now(), failed: true });
|
|
31555
|
+
return false;
|
|
31556
|
+
}
|
|
31557
|
+
})().finally(() => {
|
|
31558
|
+
this.reading.delete(id);
|
|
31559
|
+
});
|
|
31560
|
+
this.reading.set(id, read);
|
|
31561
|
+
return read;
|
|
31562
|
+
}
|
|
31563
|
+
store(id, value) {
|
|
31564
|
+
const prev = this.cache.get(id)?.value;
|
|
31565
|
+
this.cache.set(id, { value, at: Date.now() });
|
|
31566
|
+
return !prev || !same2(prev, value);
|
|
31567
|
+
}
|
|
31568
|
+
}
|
|
31569
|
+
|
|
30624
31570
|
// ../daemon/src/worktrees/spare.ts
|
|
30625
31571
|
import { existsSync as existsSync29 } from "fs";
|
|
30626
31572
|
import { basename as basename9, join as join26 } from "path";
|
|
@@ -30909,7 +31855,7 @@ class Trunk {
|
|
|
30909
31855
|
const f = await withFetchLock(repo.path, () => w.git(repo.path, ["fetch", "--quiet"]));
|
|
30910
31856
|
this.noteFetch(repo.id, f);
|
|
30911
31857
|
if (!f.ok)
|
|
30912
|
-
return { ok: false, message:
|
|
31858
|
+
return { ok: false, message: refused2("fetch failed", f) };
|
|
30913
31859
|
this.d.invalidateCounts();
|
|
30914
31860
|
return this.follow(main.id);
|
|
30915
31861
|
}
|
|
@@ -30937,14 +31883,13 @@ class Trunk {
|
|
|
30937
31883
|
this.d.hub.emit("worktreesChanged");
|
|
30938
31884
|
return ff;
|
|
30939
31885
|
}
|
|
30940
|
-
|
|
30941
|
-
const quick = opts.quick ? { missed: false } : null;
|
|
31886
|
+
all() {
|
|
30942
31887
|
const out = {};
|
|
30943
|
-
|
|
31888
|
+
for (const repo of this.d.state.repos) {
|
|
30944
31889
|
const main = mainOf(this.d.state, repo.id);
|
|
30945
31890
|
if (!main)
|
|
30946
|
-
|
|
30947
|
-
const { behind, dirty } =
|
|
31891
|
+
continue;
|
|
31892
|
+
const { behind, dirty } = this.d.counts(main) ?? {};
|
|
30948
31893
|
const stale = this.stale.get(repo.id);
|
|
30949
31894
|
const fetched = this.fetches.get(repo.id);
|
|
30950
31895
|
out[repo.id] = {
|
|
@@ -30956,9 +31901,7 @@ class Trunk {
|
|
|
30956
31901
|
...fetched?.at ? { fetchedAt: fetched.at } : {},
|
|
30957
31902
|
...fetched?.failed ? { fetchFailed: fetched.failed } : {}
|
|
30958
31903
|
};
|
|
30959
|
-
}
|
|
30960
|
-
if (quick?.missed)
|
|
30961
|
-
setTimeout(() => this.d.hub.emit("worktreesChanged"), 0);
|
|
31904
|
+
}
|
|
30962
31905
|
return out;
|
|
30963
31906
|
}
|
|
30964
31907
|
}
|
|
@@ -31071,6 +32014,10 @@ function withCarry(context, c) {
|
|
|
31071
32014
|
const line = c.moved ? `This worktree started with ${files} the person moved here from ${c.branch} by hand. They are part of the task, not something to clean up.` : `${c.branch} has ${files} the person chose to leave there. This worktree does not have them.`;
|
|
31072
32015
|
return [...context ?? [], line];
|
|
31073
32016
|
}
|
|
32017
|
+
var idsOf = (rows, trunks) => [
|
|
32018
|
+
...rows.map((r) => r.id),
|
|
32019
|
+
...Object.values(trunks).map((t) => t.id)
|
|
32020
|
+
];
|
|
31074
32021
|
var commandLine = (args) => args.map((a) => /^[\w./:=@,+-]+$/.test(a) ? a : JSON.stringify(a.split(`
|
|
31075
32022
|
`, 1)[0])).join(" ");
|
|
31076
32023
|
|
|
@@ -31082,21 +32029,29 @@ class WorktreeService {
|
|
|
31082
32029
|
trunk;
|
|
31083
32030
|
archive;
|
|
31084
32031
|
archiving = new Map;
|
|
32032
|
+
restoring = new Map;
|
|
31085
32033
|
going = new Set;
|
|
31086
|
-
|
|
32034
|
+
counts;
|
|
31087
32035
|
handChecked = new Map;
|
|
31088
32036
|
naming = new Set;
|
|
31089
32037
|
discoverCache = new Map;
|
|
32038
|
+
relisting = new Map;
|
|
32039
|
+
listMark = 0;
|
|
32040
|
+
pending = new Set;
|
|
31090
32041
|
usage = new Map;
|
|
31091
32042
|
constructor(d) {
|
|
31092
32043
|
this.d = d;
|
|
31093
32044
|
this.runs = d.runs ?? new RunService({ state: d.state, hub: d.hub });
|
|
31094
32045
|
this.archive = new WorktreeArchive(d.paths.archiveDir);
|
|
32046
|
+
this.counts = new Counts({
|
|
32047
|
+
read: (id) => this.readCounts(id),
|
|
32048
|
+
going: (id) => this.going.has(id)
|
|
32049
|
+
});
|
|
31095
32050
|
this.trunk = new Trunk({
|
|
31096
32051
|
state: d.state,
|
|
31097
32052
|
hub: d.hub,
|
|
31098
|
-
counts: (main
|
|
31099
|
-
invalidateCounts: () => this.
|
|
32053
|
+
counts: (main) => this.counts.get(main.id),
|
|
32054
|
+
invalidateCounts: () => this.counts.stale()
|
|
31100
32055
|
});
|
|
31101
32056
|
this.spare = new SparePool({
|
|
31102
32057
|
state: d.state,
|
|
@@ -31135,7 +32090,7 @@ class WorktreeService {
|
|
|
31135
32090
|
this.usage.set(worktreeId, { used, size, ...cost !== undefined ? { cost } : {} });
|
|
31136
32091
|
});
|
|
31137
32092
|
d.hub.on("turnSettled", (worktreeId, turn) => {
|
|
31138
|
-
this.
|
|
32093
|
+
this.counts.stale(worktreeId);
|
|
31139
32094
|
if (turn.end === "done") {
|
|
31140
32095
|
const wt = d.state.worktree(worktreeId);
|
|
31141
32096
|
const prompt = wt?.unnamed ? firstAskOf(d.runtime.agentFor(worktreeId)?.transcript() ?? []) : undefined;
|
|
@@ -31158,7 +32113,8 @@ class WorktreeService {
|
|
|
31158
32113
|
const agent = this.d.agents.require(opts.agent ?? this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id;
|
|
31159
32114
|
const profile = this.checkProfile(repo, opts.profile);
|
|
31160
32115
|
const task = taskText(prompt, attachments);
|
|
31161
|
-
const
|
|
32116
|
+
const given = cleanTitle2(opts.title ?? "") || undefined;
|
|
32117
|
+
const base = given ?? titleFrom(task);
|
|
31162
32118
|
const title = variant ? `${base} v${variant.index}` : base;
|
|
31163
32119
|
if (opts.carry && variant && variant.of > 1) {
|
|
31164
32120
|
throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
|
|
@@ -31172,6 +32128,10 @@ class WorktreeService {
|
|
|
31172
32128
|
claimed.variant = variant;
|
|
31173
32129
|
if (opts.createdBy)
|
|
31174
32130
|
claimed.createdBy = opts.createdBy;
|
|
32131
|
+
if (given)
|
|
32132
|
+
delete claimed.unnamed;
|
|
32133
|
+
if (opts.from)
|
|
32134
|
+
claimed.from = opts.from;
|
|
31175
32135
|
claimed.promptedAt = claimed.createdAt;
|
|
31176
32136
|
claimed.agent = agent;
|
|
31177
32137
|
if (opts.mode)
|
|
@@ -31191,7 +32151,7 @@ class WorktreeService {
|
|
|
31191
32151
|
if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
|
|
31192
32152
|
await rt.agent.restart();
|
|
31193
32153
|
rt.agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
|
|
31194
|
-
this.
|
|
32154
|
+
this.nameOrAsk(claimed, task, given, variant);
|
|
31195
32155
|
if (carried.unmoved)
|
|
31196
32156
|
throw new UserError(carried.unmoved, { delivered: true });
|
|
31197
32157
|
return claimed;
|
|
@@ -31211,11 +32171,12 @@ class WorktreeService {
|
|
|
31211
32171
|
kind: "worktree",
|
|
31212
32172
|
proxyPort: await allocateProxyPort(),
|
|
31213
32173
|
title,
|
|
31214
|
-
unnamed: true,
|
|
32174
|
+
...given ? {} : { unnamed: true },
|
|
31215
32175
|
createdAt: Date.now(),
|
|
31216
32176
|
promptedAt: Date.now(),
|
|
31217
32177
|
agent,
|
|
31218
32178
|
...variant ? { variant } : {},
|
|
32179
|
+
...opts.from ? { from: opts.from } : {},
|
|
31219
32180
|
...opts.createdBy ? { createdBy: opts.createdBy } : {},
|
|
31220
32181
|
...profile !== undefined ? { profile } : {},
|
|
31221
32182
|
...opts.mode ? { mode: opts.mode } : {},
|
|
@@ -31225,11 +32186,18 @@ class WorktreeService {
|
|
|
31225
32186
|
const carried = await this.carryMain(repo, wt, !!opts.carry);
|
|
31226
32187
|
this.launch(wt, repo, repo.path);
|
|
31227
32188
|
this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
|
|
31228
|
-
this.
|
|
32189
|
+
this.nameOrAsk(wt, task, given, variant);
|
|
31229
32190
|
if (carried.unmoved)
|
|
31230
32191
|
throw new UserError(carried.unmoved, { delivered: true });
|
|
31231
32192
|
return wt;
|
|
31232
32193
|
}
|
|
32194
|
+
nameOrAsk(wt, task, given, variant) {
|
|
32195
|
+
if (given === undefined) {
|
|
32196
|
+
this.scheduleNaming(wt, task, variant);
|
|
32197
|
+
return;
|
|
32198
|
+
}
|
|
32199
|
+
fireAndForget(wt.id, this.rename(wt.id, given), "branch for the given title");
|
|
32200
|
+
}
|
|
31233
32201
|
async carryMain(repo, wt, move) {
|
|
31234
32202
|
try {
|
|
31235
32203
|
return await this.carryMainOrThrow(repo, wt, move);
|
|
@@ -31242,12 +32210,14 @@ class WorktreeService {
|
|
|
31242
32210
|
async carryMainOrThrow(repo, wt, move) {
|
|
31243
32211
|
const main = repo.defaultBranch;
|
|
31244
32212
|
if (!move) {
|
|
31245
|
-
const count = this.
|
|
32213
|
+
const count = this.counts.get(this.mainOf(repo.id)?.id ?? "")?.dirty ?? 0;
|
|
31246
32214
|
return { branch: main, moved: false, count };
|
|
31247
32215
|
}
|
|
31248
32216
|
let count = 0;
|
|
32217
|
+
let left;
|
|
31249
32218
|
const unmoved = await withRepoLock(repo.path, async () => {
|
|
31250
32219
|
count = (await statusFiles(repo.path)).length;
|
|
32220
|
+
left = count;
|
|
31251
32221
|
if (count === 0)
|
|
31252
32222
|
return;
|
|
31253
32223
|
const before = await git(repo.path, "rev-parse", "-q", "--verify", "refs/stash");
|
|
@@ -31258,6 +32228,7 @@ class WorktreeService {
|
|
|
31258
32228
|
const sha = after.out.trim();
|
|
31259
32229
|
if (!after.ok || before.ok && before.out.trim() === sha)
|
|
31260
32230
|
return;
|
|
32231
|
+
left = 0;
|
|
31261
32232
|
const entry = async () => {
|
|
31262
32233
|
const list = await git(repo.path, "stash", "list", "--format=%H");
|
|
31263
32234
|
const i = list.out.split(`
|
|
@@ -31281,11 +32252,15 @@ class WorktreeService {
|
|
|
31281
32252
|
}
|
|
31282
32253
|
const ref = await entry();
|
|
31283
32254
|
const back = ref ? await git(repo.path, "stash", "pop", ref) : null;
|
|
32255
|
+
left = back?.ok ? count : undefined;
|
|
31284
32256
|
return back?.ok ? `the changes on ${main} did not apply in ${wt.title}, so they stayed on ${main}` : `the changes on ${main} did not apply in ${wt.title}; they are kept in git stash as "toyon: into ${wt.title}"`;
|
|
31285
32257
|
});
|
|
31286
32258
|
const mainWt = this.mainOf(repo.id);
|
|
31287
|
-
if (mainWt)
|
|
31288
|
-
|
|
32259
|
+
if (mainWt) {
|
|
32260
|
+
if (left !== undefined)
|
|
32261
|
+
this.counts.put(mainWt.id, { ...this.counts.get(mainWt.id), dirty: left });
|
|
32262
|
+
this.counts.stale(mainWt.id);
|
|
32263
|
+
}
|
|
31289
32264
|
return { branch: main, moved: count > 0 && !unmoved, count, unmoved };
|
|
31290
32265
|
}
|
|
31291
32266
|
launch(wt, repo, depsSource, opts = {}) {
|
|
@@ -31435,14 +32410,10 @@ class WorktreeService {
|
|
|
31435
32410
|
this.restartProcs(wt, repo);
|
|
31436
32411
|
}
|
|
31437
32412
|
setMode(worktreeId, mode) {
|
|
31438
|
-
|
|
31439
|
-
if (wt.kind === "spare")
|
|
31440
|
-
throw new UserError("no mode for a spare worktree");
|
|
31441
|
-
if (wt.mode === mode)
|
|
32413
|
+
if (!this.d.state.setMode(worktreeId, mode))
|
|
31442
32414
|
return;
|
|
31443
|
-
wt.mode = mode;
|
|
31444
|
-
this.d.state.save();
|
|
31445
32415
|
this.d.hub.emit("worktreesChanged");
|
|
32416
|
+
this.d.runtime.agentFor(worktreeId)?.modeChanged();
|
|
31446
32417
|
}
|
|
31447
32418
|
setModel(worktreeId, model) {
|
|
31448
32419
|
this.setOption(worktreeId, "model", model);
|
|
@@ -31726,6 +32697,18 @@ class WorktreeService {
|
|
|
31726
32697
|
return this.d.state.repo(rec.worktree.repoId) ?? this.d.state.repos.find((r) => r.path === rec.repoPath);
|
|
31727
32698
|
}
|
|
31728
32699
|
async restore(archiveId, createdBy, message) {
|
|
32700
|
+
const running = this.restoring.get(archiveId);
|
|
32701
|
+
const live = running ? await running : this.d.state.worktree(archiveId);
|
|
32702
|
+
if (live) {
|
|
32703
|
+
if (message)
|
|
32704
|
+
await this.send(live.id, { text: message.text, clientId: createdBy, attachments: message.attachments });
|
|
32705
|
+
return live;
|
|
32706
|
+
}
|
|
32707
|
+
const done = this.bringBack(archiveId, createdBy, message).finally(() => this.restoring.delete(archiveId));
|
|
32708
|
+
this.restoring.set(archiveId, done);
|
|
32709
|
+
return done;
|
|
32710
|
+
}
|
|
32711
|
+
async bringBack(archiveId, createdBy, message) {
|
|
31729
32712
|
const rec = this.archive.get(archiveId);
|
|
31730
32713
|
if (!rec)
|
|
31731
32714
|
throw new UserError("that archived worktree is gone");
|
|
@@ -31785,7 +32768,7 @@ class WorktreeService {
|
|
|
31785
32768
|
const dropped = await git(repo.path, "update-ref", "-d", archiveRef(old.id));
|
|
31786
32769
|
if (!dropped.ok)
|
|
31787
32770
|
log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
|
|
31788
|
-
this.
|
|
32771
|
+
this.counts.stale(wt.id);
|
|
31789
32772
|
this.launch(wt, repo, repo.path);
|
|
31790
32773
|
this.d.runtime.ensureAgent(wt).agent.note({
|
|
31791
32774
|
type: "restored",
|
|
@@ -31897,8 +32880,7 @@ class WorktreeService {
|
|
|
31897
32880
|
for (const w of sources)
|
|
31898
32881
|
await this.discardWorktree(w.id);
|
|
31899
32882
|
await this.refreshLanded(target);
|
|
31900
|
-
this.
|
|
31901
|
-
this.d.hub.emit("worktreesChanged");
|
|
32883
|
+
await this.headMoved(target.id);
|
|
31902
32884
|
return { target, grafted: sources.map((w) => w.title) };
|
|
31903
32885
|
}
|
|
31904
32886
|
startPreview(worktreeId) {
|
|
@@ -32063,7 +33045,7 @@ ${wt.landing.body}` : s;
|
|
|
32063
33045
|
const result = await commitWorktree(wt.path, message, this.landWatch(wt));
|
|
32064
33046
|
if (result.ok) {
|
|
32065
33047
|
await this.verdictSurvives(wt, { dropMessage: true });
|
|
32066
|
-
this.headMoved(wt.id);
|
|
33048
|
+
await this.headMoved(wt.id);
|
|
32067
33049
|
}
|
|
32068
33050
|
return result;
|
|
32069
33051
|
}
|
|
@@ -32165,6 +33147,7 @@ ${wt.landing.body}` : s;
|
|
|
32165
33147
|
}
|
|
32166
33148
|
noteLanded(wt, message, archiveIds) {
|
|
32167
33149
|
this.d.runtime.ensureAgent(wt).agent.note({ type: "landed", message, archiveIds, ts: Date.now() });
|
|
33150
|
+
this.d.hub.emit("landed", wt.id);
|
|
32168
33151
|
}
|
|
32169
33152
|
siblingIds(wt) {
|
|
32170
33153
|
return siblingsOf(wt, this.d.state.worktrees).map((s) => s.id);
|
|
@@ -32232,11 +33215,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32232
33215
|
const taken = await takeMainIn(wt.path, base, own, w);
|
|
32233
33216
|
if (!taken.ok)
|
|
32234
33217
|
return { result: taken };
|
|
32235
|
-
this.headMoved(wt.id);
|
|
33218
|
+
await this.headMoved(wt.id);
|
|
32236
33219
|
const pushed = await pushBranch(wt.path, wt.branch, w);
|
|
32237
33220
|
if (!pushed.ok)
|
|
32238
33221
|
return { result: pushed };
|
|
32239
|
-
this.headMoved(wt.id);
|
|
33222
|
+
await this.headMoved(wt.id);
|
|
32240
33223
|
this.setLanding(wt.id, undefined);
|
|
32241
33224
|
const did = committed ? "committed and pushed" : "pushed";
|
|
32242
33225
|
return { result: { ok: true, url: wt.pr.url, message: `${did}; PR #${wt.pr.number} has the new commits` } };
|
|
@@ -32248,7 +33231,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32248
33231
|
const taken = await takeMainIn(wt.path, base, own, w);
|
|
32249
33232
|
if (!taken.ok)
|
|
32250
33233
|
return { result: taken };
|
|
32251
|
-
this.headMoved(wt.id);
|
|
33234
|
+
await this.headMoved(wt.id);
|
|
32252
33235
|
if ((await aheadBehind(wt.path, base)).ahead === 0) {
|
|
32253
33236
|
const r = await this.prMergedOp(wt.id, mark, w);
|
|
32254
33237
|
const on = `origin's ${repo.defaultBranch} has this work already`;
|
|
@@ -32329,7 +33312,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32329
33312
|
const taken = await takeMainIn(wt.path, base, own, w);
|
|
32330
33313
|
if (!taken.ok)
|
|
32331
33314
|
return { result: taken };
|
|
32332
|
-
this.headMoved(wt.id);
|
|
33315
|
+
await this.headMoved(wt.id);
|
|
32333
33316
|
if ((await aheadBehind(wt.path, base)).ahead === 0) {
|
|
32334
33317
|
already = true;
|
|
32335
33318
|
sha = await commitOf(wt.path, base) ?? "";
|
|
@@ -32337,7 +33320,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32337
33320
|
}
|
|
32338
33321
|
const squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
|
|
32339
33322
|
const built = await landingCommit(wt.path, wt.branch, base, method, squash, own, w);
|
|
32340
|
-
this.headMoved(wt.id);
|
|
33323
|
+
await this.headMoved(wt.id);
|
|
32341
33324
|
if (!built.ok || !built.sha)
|
|
32342
33325
|
return { result: built };
|
|
32343
33326
|
sha = built.sha;
|
|
@@ -32386,7 +33369,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32386
33369
|
const r = await git(wt.path, "reset", "--keep", base);
|
|
32387
33370
|
if (!r.ok)
|
|
32388
33371
|
log.warn(wt.id, `could not restart ${wt.branch} from ${base}: ${r.err}`);
|
|
32389
|
-
this.headMoved(wt.id);
|
|
33372
|
+
await this.headMoved(wt.id);
|
|
32390
33373
|
}
|
|
32391
33374
|
async fetchBase(wt, repo, w) {
|
|
32392
33375
|
if (!baseIsRemote(repo))
|
|
@@ -32453,7 +33436,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32453
33436
|
}
|
|
32454
33437
|
const taken = await withRepoLock(repo.path, () => takeMainIn(r.path, r.base, own, w, carry));
|
|
32455
33438
|
if (taken.ok) {
|
|
32456
|
-
this.headMoved(worktreeId);
|
|
33439
|
+
await this.headMoved(worktreeId);
|
|
32457
33440
|
if (r.wt)
|
|
32458
33441
|
await this.verdictSurvives(r.wt, { dropMessage: false });
|
|
32459
33442
|
}
|
|
@@ -32469,10 +33452,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32469
33452
|
const w = { step: (name) => this.step(worktreeId, name), git: UNWATCHED.git };
|
|
32470
33453
|
return this.ship(worktreeId, "pull-main", async () => {
|
|
32471
33454
|
const result = await this.trunk.pull(repo.id, w);
|
|
32472
|
-
if (result.ok)
|
|
32473
|
-
this.
|
|
32474
|
-
this.headMoved(worktreeId);
|
|
32475
|
-
}
|
|
33455
|
+
if (result.ok && result.moved)
|
|
33456
|
+
await this.recountAll();
|
|
32476
33457
|
return result;
|
|
32477
33458
|
});
|
|
32478
33459
|
}
|
|
@@ -32494,8 +33475,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32494
33475
|
syncTrunk(repoId) {
|
|
32495
33476
|
return this.trunk.sync(repoId);
|
|
32496
33477
|
}
|
|
32497
|
-
|
|
32498
|
-
const all =
|
|
33478
|
+
trunksNow() {
|
|
33479
|
+
const all = this.trunk.all();
|
|
32499
33480
|
for (const t of Object.values(all)) {
|
|
32500
33481
|
const shipping = this.ops.get(t.id);
|
|
32501
33482
|
if (shipping)
|
|
@@ -32511,79 +33492,95 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32511
33492
|
this.usage.set(worktreeId, found);
|
|
32512
33493
|
return found ?? undefined;
|
|
32513
33494
|
}
|
|
32514
|
-
headMoved(worktreeId) {
|
|
32515
|
-
this.
|
|
33495
|
+
async headMoved(worktreeId) {
|
|
33496
|
+
this.counts.stale(worktreeId);
|
|
33497
|
+
await this.counts.refresh([worktreeId]);
|
|
32516
33498
|
this.d.hub.emit("worktreesChanged");
|
|
32517
33499
|
}
|
|
33500
|
+
async recountAll() {
|
|
33501
|
+
this.counts.stale();
|
|
33502
|
+
await this.counts.refresh(this.countedIds());
|
|
33503
|
+
}
|
|
32518
33504
|
invalidateCounts(worktreeId) {
|
|
32519
|
-
|
|
32520
|
-
this.countsCache.delete(worktreeId);
|
|
32521
|
-
else
|
|
32522
|
-
this.countsCache.clear();
|
|
33505
|
+
this.counts.stale(worktreeId);
|
|
32523
33506
|
}
|
|
32524
33507
|
recount(repoId) {
|
|
32525
|
-
this.
|
|
33508
|
+
this.counts.stale();
|
|
32526
33509
|
this.d.hub.emit("repoTick", repoId);
|
|
32527
33510
|
const repo = this.d.state.repo(repoId);
|
|
32528
33511
|
if (repo && baseIsRemote(repo))
|
|
32529
33512
|
fireAndForget(repoId, this.trunk.sync(repoId), "trunk sync");
|
|
32530
33513
|
}
|
|
32531
33514
|
invalidateDiscovered() {
|
|
32532
|
-
this.
|
|
33515
|
+
this.listMark++;
|
|
33516
|
+
for (const cached of this.discoverCache.values())
|
|
33517
|
+
cached.at = 0;
|
|
32533
33518
|
}
|
|
32534
33519
|
discoveredById(id) {
|
|
32535
|
-
|
|
32536
|
-
const found = rows.find((r) => r.id === id);
|
|
32537
|
-
if (found)
|
|
32538
|
-
return found;
|
|
32539
|
-
}
|
|
32540
|
-
return null;
|
|
32541
|
-
}
|
|
32542
|
-
discoveredQuick(quick) {
|
|
32543
|
-
return this.d.state.repos.flatMap((repo) => {
|
|
32544
|
-
const cached = this.discoverCache.get(repo.id);
|
|
32545
|
-
if (cached)
|
|
32546
|
-
return cached.rows;
|
|
32547
|
-
quick.missed = true;
|
|
32548
|
-
return [];
|
|
32549
|
-
});
|
|
33520
|
+
return this.foundCached().find((r) => r.id === id) ?? null;
|
|
32550
33521
|
}
|
|
32551
33522
|
async discovered() {
|
|
32552
|
-
|
|
32553
|
-
|
|
32554
|
-
|
|
32555
|
-
|
|
32556
|
-
|
|
32557
|
-
|
|
32558
|
-
|
|
32559
|
-
|
|
32560
|
-
|
|
32561
|
-
|
|
32562
|
-
|
|
32563
|
-
|
|
33523
|
+
await this.relist(this.d.state.repos.filter((repo) => this.listDue(repo.id)));
|
|
33524
|
+
return this.foundCached();
|
|
33525
|
+
}
|
|
33526
|
+
foundNow() {
|
|
33527
|
+
const due = this.d.state.repos.filter((repo) => this.listDue(repo.id));
|
|
33528
|
+
if (due.length)
|
|
33529
|
+
this.behind("discover", this.relist(due), "worktree list");
|
|
33530
|
+
return this.foundCached();
|
|
33531
|
+
}
|
|
33532
|
+
foundCached() {
|
|
33533
|
+
const owned = new Set(this.d.state.worktrees.flatMap((w) => [w.id, w.path]));
|
|
33534
|
+
return this.d.state.repos.flatMap((repo) => (this.discoverCache.get(repo.id)?.rows ?? []).filter((r) => !owned.has(r.id) && !owned.has(r.path)));
|
|
33535
|
+
}
|
|
33536
|
+
listDue(repoId) {
|
|
33537
|
+
const cached = this.discoverCache.get(repoId);
|
|
33538
|
+
return !cached || Date.now() - cached.at >= COUNTS_FLOOR_MS;
|
|
33539
|
+
}
|
|
33540
|
+
async relist(repos) {
|
|
33541
|
+
const moved = await Promise.all(repos.map((repo) => {
|
|
33542
|
+
const running = this.relisting.get(repo.id);
|
|
33543
|
+
if (running)
|
|
33544
|
+
return running;
|
|
33545
|
+
const listing = (async () => {
|
|
33546
|
+
const cached = this.discoverCache.get(repo.id);
|
|
33547
|
+
try {
|
|
33548
|
+
for (;; ) {
|
|
33549
|
+
const mark = this.listMark;
|
|
33550
|
+
const rows = await discoverIn(repo.id, repo.path, this.d.state.worktrees);
|
|
33551
|
+
if (mark !== this.listMark)
|
|
33552
|
+
continue;
|
|
33553
|
+
this.discoverCache.set(repo.id, { rows, at: Date.now() });
|
|
33554
|
+
return JSON.stringify(cached?.rows ?? []) !== JSON.stringify(rows);
|
|
33555
|
+
}
|
|
33556
|
+
} catch (e) {
|
|
33557
|
+
log.warn(repo.id, "could not list this repo's worktrees", e);
|
|
33558
|
+
return false;
|
|
33559
|
+
} finally {
|
|
33560
|
+
this.relisting.delete(repo.id);
|
|
33561
|
+
}
|
|
33562
|
+
})();
|
|
33563
|
+
this.relisting.set(repo.id, listing);
|
|
33564
|
+
return listing;
|
|
32564
33565
|
}));
|
|
32565
|
-
|
|
32566
|
-
|
|
32567
|
-
|
|
32568
|
-
|
|
32569
|
-
|
|
32570
|
-
|
|
32571
|
-
|
|
32572
|
-
|
|
32573
|
-
|
|
32574
|
-
|
|
32575
|
-
|
|
32576
|
-
|
|
32577
|
-
|
|
32578
|
-
|
|
32579
|
-
|
|
32580
|
-
|
|
32581
|
-
|
|
32582
|
-
|
|
32583
|
-
return fresh;
|
|
32584
|
-
} catch {
|
|
32585
|
-
return cached ?? {};
|
|
32586
|
-
}
|
|
33566
|
+
this.d.runtime.pruneLooseShells(new Set(this.foundCached().map((r) => r.id)));
|
|
33567
|
+
const any = moved.some(Boolean);
|
|
33568
|
+
if (any)
|
|
33569
|
+
this.d.hub.emit("worktreesChanged");
|
|
33570
|
+
return any;
|
|
33571
|
+
}
|
|
33572
|
+
async readCounts(id) {
|
|
33573
|
+
const wt = this.d.state.worktree(id);
|
|
33574
|
+
const found = wt ? null : this.discoveredById(id);
|
|
33575
|
+
const row = wt ?? found;
|
|
33576
|
+
const repo = row && this.d.state.repo(row.repoId);
|
|
33577
|
+
if (!row || !repo)
|
|
33578
|
+
return null;
|
|
33579
|
+
const countable = wt ? !isMain(wt) : !!found?.branch;
|
|
33580
|
+
const ab = countable ? await aheadBehind(row.path, baseOf(repo)) : await this.trunk.behind(id, row.path);
|
|
33581
|
+
const dirty = (await statusFiles(row.path)).length;
|
|
33582
|
+
const unpushed = await this.unpushed(id, row.path);
|
|
33583
|
+
return { ...ab, ...unpushed === undefined ? {} : { unpushed }, dirty };
|
|
32587
33584
|
}
|
|
32588
33585
|
async unpushed(id, path) {
|
|
32589
33586
|
if (this.d.state.worktree(id)?.pr?.state !== "open")
|
|
@@ -32591,12 +33588,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32591
33588
|
return await aheadUpstream(path) ?? undefined;
|
|
32592
33589
|
}
|
|
32593
33590
|
async freshCounts(worktreeId) {
|
|
32594
|
-
|
|
32595
|
-
const repo = wt && this.d.state.repo(wt.repoId);
|
|
32596
|
-
if (!wt || !repo)
|
|
33591
|
+
if (this.going.has(worktreeId))
|
|
32597
33592
|
return {};
|
|
32598
|
-
this.
|
|
32599
|
-
return this.counts(wt.id, wt.path, baseOf(repo), true);
|
|
33593
|
+
return await this.counts.read(worktreeId) ?? {};
|
|
32600
33594
|
}
|
|
32601
33595
|
readable(id) {
|
|
32602
33596
|
const wt = this.d.state.worktree(id);
|
|
@@ -32687,14 +33681,13 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32687
33681
|
}
|
|
32688
33682
|
}
|
|
32689
33683
|
noteCounts(id, main, dirty, ab) {
|
|
32690
|
-
const prev = this.
|
|
33684
|
+
const prev = this.counts.get(id);
|
|
32691
33685
|
if (main && !prev)
|
|
32692
33686
|
return;
|
|
32693
33687
|
const ahead = main ? prev?.ahead : ab.ahead;
|
|
32694
33688
|
const behind = main ? prev?.behind : ab.behind;
|
|
32695
33689
|
const unpushed = main ? prev?.unpushed : ab.unpushed;
|
|
32696
|
-
this.
|
|
32697
|
-
if (prev?.dirty !== dirty || prev?.ahead !== ahead || prev?.behind !== behind || prev?.unpushed !== unpushed)
|
|
33690
|
+
if (this.counts.put(id, { ahead, behind, unpushed, dirty }))
|
|
32698
33691
|
this.d.hub.emit("worktreesChanged");
|
|
32699
33692
|
}
|
|
32700
33693
|
async gitLog(worktreeId) {
|
|
@@ -32738,12 +33731,40 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32738
33731
|
this.d.state.save();
|
|
32739
33732
|
this.d.hub.emit("worktreesChanged");
|
|
32740
33733
|
}
|
|
32741
|
-
|
|
32742
|
-
const
|
|
32743
|
-
const
|
|
32744
|
-
|
|
32745
|
-
|
|
32746
|
-
|
|
33734
|
+
snapshot() {
|
|
33735
|
+
const rows = [...this.ownedRows(), ...this.foundRows()];
|
|
33736
|
+
const trunks = this.trunksNow();
|
|
33737
|
+
const reading = this.counts.refresh(idsOf(rows, trunks));
|
|
33738
|
+
if (reading) {
|
|
33739
|
+
this.behind("counts", reading.then((moved) => {
|
|
33740
|
+
if (moved)
|
|
33741
|
+
this.d.hub.emit("worktreesChanged");
|
|
33742
|
+
}), "badge counts");
|
|
33743
|
+
}
|
|
33744
|
+
return { rows, trunks };
|
|
33745
|
+
}
|
|
33746
|
+
rows() {
|
|
33747
|
+
return this.snapshot().rows;
|
|
33748
|
+
}
|
|
33749
|
+
trunks() {
|
|
33750
|
+
return this.snapshot().trunks;
|
|
33751
|
+
}
|
|
33752
|
+
countedIds() {
|
|
33753
|
+
return idsOf([...this.ownedRows(), ...this.foundCached()], this.trunksNow());
|
|
33754
|
+
}
|
|
33755
|
+
behind(tag, read, what) {
|
|
33756
|
+
this.pending.add(read);
|
|
33757
|
+
const done = () => this.pending.delete(read);
|
|
33758
|
+
read.then(done, done);
|
|
33759
|
+
fireAndForget(tag, read, what);
|
|
33760
|
+
}
|
|
33761
|
+
async settled() {
|
|
33762
|
+
let waited = false;
|
|
33763
|
+
while (this.pending.size) {
|
|
33764
|
+
waited = true;
|
|
33765
|
+
await Promise.all(this.pending);
|
|
33766
|
+
}
|
|
33767
|
+
return waited;
|
|
32747
33768
|
}
|
|
32748
33769
|
mainOf(repoId) {
|
|
32749
33770
|
return mainOf(this.d.state, repoId);
|
|
@@ -32751,24 +33772,16 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32751
33772
|
leadSpareOf(repoId) {
|
|
32752
33773
|
return this.spare.current(repoId)?.worktreeId ?? null;
|
|
32753
33774
|
}
|
|
32754
|
-
countsQuick(id, quick) {
|
|
32755
|
-
const c = this.countsCache.get(id);
|
|
32756
|
-
if (c)
|
|
32757
|
-
return c;
|
|
32758
|
-
quick.missed = true;
|
|
32759
|
-
return {};
|
|
32760
|
-
}
|
|
32761
33775
|
isRow(wt) {
|
|
32762
33776
|
if (wt.kind === "worktree")
|
|
32763
33777
|
return true;
|
|
32764
33778
|
const lead = this.leadSpareOf(wt.repoId);
|
|
32765
33779
|
return wt.kind === "spare" ? lead === wt.id : lead === null;
|
|
32766
33780
|
}
|
|
32767
|
-
|
|
32768
|
-
return
|
|
33781
|
+
ownedRows() {
|
|
33782
|
+
return this.d.state.worktrees.filter((wt) => this.isRow(wt)).map((wt) => {
|
|
32769
33783
|
const rt = this.d.runtime.get(wt.id);
|
|
32770
|
-
const
|
|
32771
|
-
const { ahead, behind, unpushed, dirty } = quick ? this.countsQuick(wt.id, quick) : await this.counts(wt.id, wt.path, baseOf(repo), !isMain(wt));
|
|
33784
|
+
const { ahead, behind, unpushed, dirty } = this.counts.get(wt.id) ?? {};
|
|
32772
33785
|
return {
|
|
32773
33786
|
id: wt.id,
|
|
32774
33787
|
repoId: wt.repoId,
|
|
@@ -32791,13 +33804,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32791
33804
|
sessionId: this.d.state.session(wt.id),
|
|
32792
33805
|
shipping: this.ops.get(wt.id)
|
|
32793
33806
|
};
|
|
32794
|
-
})
|
|
33807
|
+
});
|
|
32795
33808
|
}
|
|
32796
|
-
|
|
32797
|
-
|
|
32798
|
-
|
|
32799
|
-
const repo = this.d.state.repo(f.repoId);
|
|
32800
|
-
const { ahead, behind, dirty } = !repo ? {} : quick ? this.countsQuick(f.id, quick) : await this.counts(f.id, f.path, baseOf(repo), !!f.branch);
|
|
33809
|
+
foundRows() {
|
|
33810
|
+
return this.foundNow().map((f) => {
|
|
33811
|
+
const { ahead, behind, dirty } = this.counts.get(f.id) ?? {};
|
|
32801
33812
|
return {
|
|
32802
33813
|
...f,
|
|
32803
33814
|
procs: [],
|
|
@@ -32808,7 +33819,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
32808
33819
|
dirty,
|
|
32809
33820
|
shipping: this.ops.get(f.id)
|
|
32810
33821
|
};
|
|
32811
|
-
})
|
|
33822
|
+
});
|
|
32812
33823
|
}
|
|
32813
33824
|
}
|
|
32814
33825
|
|
|
@@ -32919,6 +33930,12 @@ ensureDirs(paths);
|
|
|
32919
33930
|
var token = loadOrCreateToken(paths);
|
|
32920
33931
|
var port = Number(process.env.TOYON_PORT ?? DAEMON_DEFAULT_PORT);
|
|
32921
33932
|
var remote = loadRemote(paths.remoteFile, process.env, managed.policy);
|
|
33933
|
+
var grants = new GrantCodes;
|
|
33934
|
+
var previewGate = remote && {
|
|
33935
|
+
grant: previewGrant(token),
|
|
33936
|
+
host: remote.host,
|
|
33937
|
+
redeem: (code) => grants.redeem(code)
|
|
33938
|
+
};
|
|
32922
33939
|
if (remote && addressedByPort(remote.previews))
|
|
32923
33940
|
pinProxyPorts(PREVIEW_PORTS);
|
|
32924
33941
|
var state = new StateStore(paths);
|
|
@@ -32951,6 +33968,9 @@ var accounts = new AgentAccounts({
|
|
|
32951
33968
|
accounts.onChange = () => hub.emit("agentsChanged");
|
|
32952
33969
|
var uploads = new UploadStore(paths.uploadsDir);
|
|
32953
33970
|
var attachments2 = new AttachmentStore(paths.attachmentsDir, uploads);
|
|
33971
|
+
var mcpPort = port;
|
|
33972
|
+
var tools = new ToolSet;
|
|
33973
|
+
var mcp = new ToyonMcp({ url: (id) => `http://127.0.0.1:${mcpPort}/mcp/${id}`, version: package_default.version, tools });
|
|
32954
33974
|
var runtime = new RuntimeRegistry({
|
|
32955
33975
|
hub,
|
|
32956
33976
|
state,
|
|
@@ -32962,10 +33982,11 @@ var runtime = new RuntimeRegistry({
|
|
|
32962
33982
|
bridgeScript: () => bridge.get(),
|
|
32963
33983
|
turnStarting: (id) => worktrees.turnStarted(id),
|
|
32964
33984
|
remote,
|
|
32965
|
-
|
|
33985
|
+
gate: previewGate,
|
|
32966
33986
|
viewed: (id) => idle.isViewed(id),
|
|
32967
33987
|
shown: (id) => idle.isShown(id),
|
|
32968
|
-
mainLeads: (repoId) => worktrees.spare.current(repoId) === null
|
|
33988
|
+
mainLeads: (repoId) => worktrees.spare.current(repoId) === null,
|
|
33989
|
+
mcp
|
|
32969
33990
|
});
|
|
32970
33991
|
var drafts = new DraftStore({
|
|
32971
33992
|
file: paths.draftsFile,
|
|
@@ -32990,6 +34011,13 @@ var worktrees = new WorktreeService({
|
|
|
32990
34011
|
fix
|
|
32991
34012
|
});
|
|
32992
34013
|
new BackendShare({ state, hub, runtime, own: (id, names) => worktrees.ownProcs(id, names) });
|
|
34014
|
+
var handoff2 = new HandoffService({
|
|
34015
|
+
state,
|
|
34016
|
+
hub,
|
|
34017
|
+
runtime,
|
|
34018
|
+
create: (repoId, prompt, opts) => worktrees.create(repoId, prompt, opts)
|
|
34019
|
+
});
|
|
34020
|
+
tools.add(handoff2);
|
|
32993
34021
|
var turns = new TurnService({
|
|
32994
34022
|
state,
|
|
32995
34023
|
hub,
|
|
@@ -33094,12 +34122,18 @@ var keepAwake = new KeepAwake({
|
|
|
33094
34122
|
mode: () => state.keepAwake,
|
|
33095
34123
|
answerable: remote !== null
|
|
33096
34124
|
});
|
|
33097
|
-
var {
|
|
34125
|
+
var {
|
|
34126
|
+
server,
|
|
34127
|
+
branded,
|
|
34128
|
+
stop: stopServer
|
|
34129
|
+
} = startServer({
|
|
33098
34130
|
port,
|
|
33099
34131
|
token,
|
|
33100
34132
|
shellDist: SHELL_DIST,
|
|
33101
34133
|
version: package_default.version,
|
|
33102
34134
|
noteShellOrigin: (origin) => bridge.learnShellOrigin(origin),
|
|
34135
|
+
onTrusted: () => refreshShellOrigins(),
|
|
34136
|
+
grants,
|
|
33103
34137
|
remote,
|
|
33104
34138
|
managed,
|
|
33105
34139
|
services: {
|
|
@@ -33124,6 +34158,8 @@ var { branded, stop: stopServer } = startServer({
|
|
|
33124
34158
|
prs,
|
|
33125
34159
|
landing,
|
|
33126
34160
|
fix,
|
|
34161
|
+
handoff: handoff2,
|
|
34162
|
+
mcp,
|
|
33127
34163
|
themes,
|
|
33128
34164
|
agents,
|
|
33129
34165
|
accounts,
|
|
@@ -33136,17 +34172,22 @@ var { branded, stop: stopServer } = startServer({
|
|
|
33136
34172
|
folderDialog: folderDialog()
|
|
33137
34173
|
}
|
|
33138
34174
|
});
|
|
33139
|
-
|
|
33140
|
-
|
|
33141
|
-
|
|
33142
|
-
|
|
33143
|
-
|
|
33144
|
-
|
|
33145
|
-
|
|
33146
|
-
|
|
33147
|
-
|
|
33148
|
-
|
|
33149
|
-
]
|
|
34175
|
+
mcpPort = server.port ?? port;
|
|
34176
|
+
function refreshShellOrigins() {
|
|
34177
|
+
bridge.setShellOrigins([
|
|
34178
|
+
...remote?.front === "edge" ? [] : [
|
|
34179
|
+
`http://127.0.0.1:${port}`,
|
|
34180
|
+
`http://localhost:${port}`,
|
|
34181
|
+
`http://toyon.localhost:${port}`,
|
|
34182
|
+
...branded ? ["http://toyon.localhost"] : [],
|
|
34183
|
+
`http://127.0.0.1:${SHELL_DEV_PORT}`,
|
|
34184
|
+
`http://localhost:${SHELL_DEV_PORT}`
|
|
34185
|
+
],
|
|
34186
|
+
...remote ? [`https://${remote.host}`] : [],
|
|
34187
|
+
...state.trustedOrigins
|
|
34188
|
+
]);
|
|
34189
|
+
}
|
|
34190
|
+
refreshShellOrigins();
|
|
33150
34191
|
writeFileSync10(paths.pidFile, `${process.pid}
|
|
33151
34192
|
`);
|
|
33152
34193
|
var stopLagSampler = startLagSampler();
|