@agentrific/server 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +3 -2
- package/cli.js +804 -512
- package/cli.js.map +4 -4
- package/index.js +671 -164
- package/index.js.map +4 -4
- package/opentunnel-sdk.js +34715 -0
- package/opentunnel-sdk.js.map +7 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -306,7 +306,7 @@ function registerPushRoutes(app2, push2, security2) {
|
|
|
306
306
|
// src/index.ts
|
|
307
307
|
import Fastify from "fastify";
|
|
308
308
|
import pretty from "pino-pretty";
|
|
309
|
-
import { styleText } from "node:util";
|
|
309
|
+
import { styleText as styleText2 } from "node:util";
|
|
310
310
|
import cors from "@fastify/cors";
|
|
311
311
|
import multipart from "@fastify/multipart";
|
|
312
312
|
|
|
@@ -888,8 +888,8 @@ import { randomUUID as randomUUID9 } from "node:crypto";
|
|
|
888
888
|
import { execFile as execFile3 } from "node:child_process";
|
|
889
889
|
import { promisify as promisify3 } from "node:util";
|
|
890
890
|
import { realpath as realpath2 } from "node:fs/promises";
|
|
891
|
-
import { homedir as
|
|
892
|
-
import
|
|
891
|
+
import { homedir as homedir3 } from "node:os";
|
|
892
|
+
import path11 from "node:path";
|
|
893
893
|
import { WebSocket } from "ws";
|
|
894
894
|
|
|
895
895
|
// src/event-server.ts
|
|
@@ -983,6 +983,7 @@ function controlSocketPath(directory, uid = process.getuid?.()) {
|
|
|
983
983
|
}
|
|
984
984
|
|
|
985
985
|
// src/control.ts
|
|
986
|
+
var ProviderSchema = z4.enum(["opentunnel", "tailscale", "cloudflared"]);
|
|
986
987
|
var MaintenanceSchema = z4.object({
|
|
987
988
|
active: z4.boolean(),
|
|
988
989
|
drained: z4.boolean(),
|
|
@@ -991,6 +992,7 @@ var MaintenanceSchema = z4.object({
|
|
|
991
992
|
var ControlStatusSchema = z4.object({
|
|
992
993
|
serverId: z4.string().uuid(),
|
|
993
994
|
publicUrl: z4.string().optional(),
|
|
995
|
+
phoneProvider: ProviderSchema.nullable().optional(),
|
|
994
996
|
localUrl: z4.string().optional(),
|
|
995
997
|
serverVersion: z4.string().optional(),
|
|
996
998
|
connectedDevices: z4.number().optional(),
|
|
@@ -1000,6 +1002,8 @@ var ControlStatusSchema = z4.object({
|
|
|
1000
1002
|
maintenance: MaintenanceSchema.optional()
|
|
1001
1003
|
});
|
|
1002
1004
|
var ControlResultSchema = z4.union([
|
|
1005
|
+
z4.object({ provider: ProviderSchema.nullable(), publicUrl: z4.string().url().nullable() }),
|
|
1006
|
+
z4.array(z4.object({ provider: ProviderSchema, available: z4.boolean(), reason: z4.string() })),
|
|
1003
1007
|
z4.object({
|
|
1004
1008
|
pairingId: z4.string(),
|
|
1005
1009
|
pin: z4.string(),
|
|
@@ -1013,6 +1017,9 @@ var ControlResultSchema = z4.union([
|
|
|
1013
1017
|
MaintenanceSchema
|
|
1014
1018
|
]);
|
|
1015
1019
|
var ControlRequest = z4.discriminatedUnion("command", [
|
|
1020
|
+
z4.object({ command: z4.literal("phone-options") }),
|
|
1021
|
+
z4.object({ command: z4.literal("phone-start"), provider: ProviderSchema }),
|
|
1022
|
+
z4.object({ command: z4.literal("phone-stop") }),
|
|
1016
1023
|
z4.object({ command: z4.literal("pair") }),
|
|
1017
1024
|
z4.object({ command: z4.literal("devices") }),
|
|
1018
1025
|
z4.object({ command: z4.literal("status") }),
|
|
@@ -1065,7 +1072,7 @@ async function acquireServerLock(directory) {
|
|
|
1065
1072
|
await unlink(filename);
|
|
1066
1073
|
};
|
|
1067
1074
|
}
|
|
1068
|
-
async function startControl(directory, security2, status, maintenance2, shutdown) {
|
|
1075
|
+
async function startControl(directory, security2, status, maintenance2, shutdown, phone2) {
|
|
1069
1076
|
const filename = controlSocketPath(directory);
|
|
1070
1077
|
const socketDirectory = path3.dirname(filename);
|
|
1071
1078
|
await mkdir2(socketDirectory, { recursive: true, mode: 448 });
|
|
@@ -1092,6 +1099,29 @@ async function startControl(directory, security2, status, maintenance2, shutdown
|
|
|
1092
1099
|
const request = ControlRequest.parse(JSON.parse(buffer.split("\n")[0]));
|
|
1093
1100
|
let result;
|
|
1094
1101
|
switch (request.command) {
|
|
1102
|
+
case "phone-options":
|
|
1103
|
+
if (!phone2) throw new Error("Phone connections are managed by the desktop app.");
|
|
1104
|
+
socket.setTimeout(18e4);
|
|
1105
|
+
result = await phone2.options();
|
|
1106
|
+
break;
|
|
1107
|
+
case "phone-start": {
|
|
1108
|
+
if (!phone2) throw new Error("Phone connections are managed by the desktop app.");
|
|
1109
|
+
maintenance2?.assertReady();
|
|
1110
|
+
socket.setTimeout(18e4);
|
|
1111
|
+
const abort = new AbortController();
|
|
1112
|
+
const disconnected = () => abort.abort();
|
|
1113
|
+
socket.once("close", disconnected);
|
|
1114
|
+
try {
|
|
1115
|
+
result = await phone2.start(request.provider, abort.signal);
|
|
1116
|
+
} finally {
|
|
1117
|
+
socket.removeListener("close", disconnected);
|
|
1118
|
+
}
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
case "phone-stop":
|
|
1122
|
+
if (!phone2) throw new Error("Phone connections are managed by the desktop app.");
|
|
1123
|
+
result = await phone2.close();
|
|
1124
|
+
break;
|
|
1095
1125
|
case "shutdown":
|
|
1096
1126
|
if (!shutdown || !maintenance2?.status().drained)
|
|
1097
1127
|
throw new Error("Drain the server before shutdown");
|
|
@@ -1137,10 +1167,13 @@ async function startControl(directory, security2, status, maintenance2, shutdown
|
|
|
1137
1167
|
};
|
|
1138
1168
|
}
|
|
1139
1169
|
|
|
1170
|
+
// src/cli-output.ts
|
|
1171
|
+
import { styleText } from "node:util";
|
|
1172
|
+
|
|
1140
1173
|
// package.json
|
|
1141
1174
|
var package_default = {
|
|
1142
1175
|
name: "@agentrific/server",
|
|
1143
|
-
version: "0.
|
|
1176
|
+
version: "0.6.0",
|
|
1144
1177
|
description: "Self-hosted control plane for long-running coding agents",
|
|
1145
1178
|
keywords: [
|
|
1146
1179
|
"agentrific",
|
|
@@ -1190,6 +1223,7 @@ var package_default = {
|
|
|
1190
1223
|
},
|
|
1191
1224
|
devDependencies: {
|
|
1192
1225
|
"@agentrific/contracts": "workspace:*",
|
|
1226
|
+
"@agentrific/opentunnel-runtime": "workspace:*",
|
|
1193
1227
|
"@jest/globals": "30.5.2",
|
|
1194
1228
|
"@types/node": "^22.19.1",
|
|
1195
1229
|
"@types/qrcode-terminal": "^0.12.2",
|
|
@@ -1204,6 +1238,13 @@ var package_default = {
|
|
|
1204
1238
|
// src/version.ts
|
|
1205
1239
|
var serverVersion = package_default.version;
|
|
1206
1240
|
|
|
1241
|
+
// src/cli-output.ts
|
|
1242
|
+
function printError(message) {
|
|
1243
|
+
console.error(`
|
|
1244
|
+
${styleText(["bold", "red"], "Error")} ${message}
|
|
1245
|
+
`);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1207
1248
|
// src/pairing.ts
|
|
1208
1249
|
function pairingOrigin(url) {
|
|
1209
1250
|
const endpoint = new URL(url);
|
|
@@ -1278,17 +1319,17 @@ var ServerSecurity = class {
|
|
|
1278
1319
|
if (!/^[a-f0-9]{64}$/.test(token)) return void 0;
|
|
1279
1320
|
return Effect4.runSync(this.store.deviceForToken(digest(token)));
|
|
1280
1321
|
}
|
|
1281
|
-
ticket(token,
|
|
1322
|
+
ticket(token, path12) {
|
|
1282
1323
|
this.prune();
|
|
1283
1324
|
if (!this.authenticate(token) || this.tickets.size >= 1e4) return void 0;
|
|
1284
1325
|
const value = secret();
|
|
1285
|
-
this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path:
|
|
1326
|
+
this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path12 });
|
|
1286
1327
|
return value;
|
|
1287
1328
|
}
|
|
1288
|
-
redeemTicket(value,
|
|
1329
|
+
redeemTicket(value, path12, consume = true) {
|
|
1289
1330
|
this.prune();
|
|
1290
1331
|
const ticket = this.tickets.get(value);
|
|
1291
|
-
if (!ticket || ticket.path !==
|
|
1332
|
+
if (!ticket || ticket.path !== path12) return void 0;
|
|
1292
1333
|
if (consume) this.tickets.delete(value);
|
|
1293
1334
|
return Effect4.runSync(this.store.deviceForToken(ticket.tokenHash));
|
|
1294
1335
|
}
|
|
@@ -1301,19 +1342,19 @@ var ServerSecurity = class {
|
|
|
1301
1342
|
};
|
|
1302
1343
|
async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
1303
1344
|
app2.addHook("onRequest", async (request, reply) => {
|
|
1304
|
-
const
|
|
1345
|
+
const path12 = new URL(request.url, "http://localhost");
|
|
1305
1346
|
reply.header("Cache-Control", "no-store");
|
|
1306
1347
|
if (request.method === "OPTIONS") return;
|
|
1307
1348
|
if (request.headers.origin && !allowedOrigins2.includes(request.headers.origin))
|
|
1308
1349
|
return reply.code(403).send({ error: "Browser origin is not allowed." });
|
|
1309
|
-
if (
|
|
1350
|
+
if (path12.pathname === "/health" || path12.pathname === "/api/auth/pair" && request.method === "POST")
|
|
1310
1351
|
return;
|
|
1311
1352
|
const authorization = request.headers.authorization ?? "";
|
|
1312
1353
|
const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
|
|
1313
|
-
const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(
|
|
1314
|
-
const ticket =
|
|
1315
|
-
|
|
1316
|
-
if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket,
|
|
1354
|
+
const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(path12.pathname);
|
|
1355
|
+
const ticket = path12.searchParams.get("ticket");
|
|
1356
|
+
path12.searchParams.delete("ticket");
|
|
1357
|
+
if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path12.pathname + path12.search, false)))
|
|
1317
1358
|
return reply.code(401).send({ error: "Pair this device with your server.", code: "UNAUTHORIZED" });
|
|
1318
1359
|
if (!asset && request.headers["x-agentrific-protocol"] !== String(PROTOCOL_VERSION))
|
|
1319
1360
|
return reply.code(409).send({
|
|
@@ -1378,6 +1419,451 @@ async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
|
1378
1419
|
});
|
|
1379
1420
|
}
|
|
1380
1421
|
|
|
1422
|
+
// src/tunnels.ts
|
|
1423
|
+
import { spawn } from "node:child_process";
|
|
1424
|
+
import { access, stat as stat2 } from "node:fs/promises";
|
|
1425
|
+
import { constants } from "node:fs";
|
|
1426
|
+
import { homedir } from "node:os";
|
|
1427
|
+
import path4 from "node:path";
|
|
1428
|
+
import { z as z7 } from "zod";
|
|
1429
|
+
function signalChild(child, signal) {
|
|
1430
|
+
if (!child.pid) return;
|
|
1431
|
+
if (process.platform === "win32") {
|
|
1432
|
+
child.kill(signal);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
try {
|
|
1436
|
+
process.kill(-child.pid, signal);
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
if (!z7.object({ code: z7.literal("ESRCH") }).safeParse(error).success) child.kill(signal);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
function execute(file, args, timeoutMs, env = process.env) {
|
|
1442
|
+
return new Promise((resolve2, reject) => {
|
|
1443
|
+
const child = spawn(file, args, {
|
|
1444
|
+
env,
|
|
1445
|
+
detached: process.platform !== "win32",
|
|
1446
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1447
|
+
});
|
|
1448
|
+
let stdout = "";
|
|
1449
|
+
let stderr = "";
|
|
1450
|
+
const timeout = setTimeout(() => {
|
|
1451
|
+
signalChild(child, "SIGKILL");
|
|
1452
|
+
reject(new Error("Command probe timed out."));
|
|
1453
|
+
}, timeoutMs);
|
|
1454
|
+
child.stdout?.on("data", (data) => {
|
|
1455
|
+
stdout += data.toString();
|
|
1456
|
+
checkOutput();
|
|
1457
|
+
});
|
|
1458
|
+
child.stderr?.on("data", (data) => {
|
|
1459
|
+
stderr += data.toString();
|
|
1460
|
+
checkOutput();
|
|
1461
|
+
});
|
|
1462
|
+
child.once("error", (error) => {
|
|
1463
|
+
clearTimeout(timeout);
|
|
1464
|
+
reject(error);
|
|
1465
|
+
});
|
|
1466
|
+
child.once("exit", () => signalChild(child, "SIGKILL"));
|
|
1467
|
+
child.once("close", (code) => {
|
|
1468
|
+
clearTimeout(timeout);
|
|
1469
|
+
if (code !== 0) reject(new Error("Command probe failed."));
|
|
1470
|
+
else resolve2({ stdout, stderr });
|
|
1471
|
+
});
|
|
1472
|
+
function checkOutput() {
|
|
1473
|
+
if (stdout.length + stderr.length <= 1048576) return;
|
|
1474
|
+
signalChild(child, "SIGKILL");
|
|
1475
|
+
reject(new Error("Command probe output exceeded its limit."));
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
async function findExecutable(name) {
|
|
1480
|
+
const candidates = (process.env.PATH ?? "").split(path4.delimiter).map((directory) => path4.resolve(directory, name));
|
|
1481
|
+
if (process.platform === "darwin") {
|
|
1482
|
+
candidates.push(`/opt/homebrew/bin/${name}`, `/usr/local/bin/${name}`);
|
|
1483
|
+
if (name === "tailscale")
|
|
1484
|
+
candidates.push("/Applications/Tailscale.app/Contents/MacOS/Tailscale");
|
|
1485
|
+
}
|
|
1486
|
+
for (const candidate of candidates) {
|
|
1487
|
+
try {
|
|
1488
|
+
await access(candidate, constants.X_OK);
|
|
1489
|
+
if ((await stat2(candidate)).isFile()) return candidate;
|
|
1490
|
+
} catch {
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return void 0;
|
|
1495
|
+
}
|
|
1496
|
+
var TcpConfig = z7.object({ TCP: z7.record(z7.string(), z7.unknown()).optional() });
|
|
1497
|
+
var ServeConfig = TcpConfig.extend({
|
|
1498
|
+
Foreground: z7.record(z7.string(), TcpConfig).optional()
|
|
1499
|
+
});
|
|
1500
|
+
async function probeTailscale() {
|
|
1501
|
+
const option = { provider: "tailscale", available: false, reason: "Not installed" };
|
|
1502
|
+
const executable = await findExecutable("tailscale");
|
|
1503
|
+
if (!executable) return option;
|
|
1504
|
+
option.executable = executable;
|
|
1505
|
+
const env = { ...process.env, TAILSCALE_BE_CLI: "1" };
|
|
1506
|
+
try {
|
|
1507
|
+
const response = await execute(executable, ["status", "--json"], 3e3, env);
|
|
1508
|
+
const status = z7.object({
|
|
1509
|
+
BackendState: z7.string(),
|
|
1510
|
+
CertDomains: z7.array(z7.string()).optional(),
|
|
1511
|
+
Self: z7.object({ DNSName: z7.string() }).nullable().optional()
|
|
1512
|
+
}).parse(JSON.parse(response.stdout));
|
|
1513
|
+
if (status.BackendState !== "Running")
|
|
1514
|
+
return { ...option, reason: "Installed \xB7 connect Tailscale first" };
|
|
1515
|
+
const dns = status.Self?.DNSName.replace(/\.$/, "");
|
|
1516
|
+
if (!dns || !status.CertDomains?.includes(dns))
|
|
1517
|
+
return { ...option, reason: "Enable HTTPS certificates in your tailnet first" };
|
|
1518
|
+
const help = await execute(executable, ["serve", "--help"], 2e3, env);
|
|
1519
|
+
if (!`${help.stdout}${help.stderr}`.includes("--https"))
|
|
1520
|
+
return { ...option, reason: "Update Tailscale to use foreground HTTPS Serve" };
|
|
1521
|
+
const responseConfig = await execute(executable, ["serve", "status", "--json"], 3e3, env);
|
|
1522
|
+
const config = ServeConfig.parse(JSON.parse(responseConfig.stdout) ?? {});
|
|
1523
|
+
const occupied = new Set(Object.keys(config.TCP ?? {}));
|
|
1524
|
+
for (const foreground of Object.values(config.Foreground ?? {}))
|
|
1525
|
+
for (const port3 of Object.keys(foreground.TCP ?? {})) occupied.add(port3);
|
|
1526
|
+
const port2 = [443, ...Array.from({ length: 10 }, (_, index) => 8443 + index)].find(
|
|
1527
|
+
(candidate) => !occupied.has(String(candidate))
|
|
1528
|
+
);
|
|
1529
|
+
if (!port2) return { ...option, reason: "HTTPS Serve ports are in use \xB7 paste an existing URL" };
|
|
1530
|
+
return {
|
|
1531
|
+
...option,
|
|
1532
|
+
available: true,
|
|
1533
|
+
reason: "Private to your tailnet \xB7 Tailscale required on your phone",
|
|
1534
|
+
publicUrl: new URL(`https://${dns}${port2 === 443 ? "" : `:${port2}`}`).origin,
|
|
1535
|
+
httpsPort: port2
|
|
1536
|
+
};
|
|
1537
|
+
} catch {
|
|
1538
|
+
return { ...option, reason: "Installed \xB7 could not verify daemon or Serve access" };
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
async function probeCloudflared(home) {
|
|
1542
|
+
const option = {
|
|
1543
|
+
provider: "cloudflared",
|
|
1544
|
+
available: false,
|
|
1545
|
+
reason: "Not installed"
|
|
1546
|
+
};
|
|
1547
|
+
const executable = await findExecutable("cloudflared");
|
|
1548
|
+
if (!executable) return option;
|
|
1549
|
+
option.executable = executable;
|
|
1550
|
+
try {
|
|
1551
|
+
await execute(executable, ["--version"], 2e3);
|
|
1552
|
+
const help = await execute(executable, ["tunnel", "--help"], 2e3);
|
|
1553
|
+
if (!`${help.stdout}${help.stderr}`.includes("--url"))
|
|
1554
|
+
return { ...option, reason: "Installed \xB7 this version does not support Quick Tunnels" };
|
|
1555
|
+
} catch {
|
|
1556
|
+
return { ...option, reason: "Installed \xB7 command probe failed or timed out" };
|
|
1557
|
+
}
|
|
1558
|
+
const directories = [
|
|
1559
|
+
path4.join(home, ".cloudflared"),
|
|
1560
|
+
path4.join(home, ".cloudflare-warp"),
|
|
1561
|
+
path4.join(home, "cloudflare-warp"),
|
|
1562
|
+
"/etc/cloudflared",
|
|
1563
|
+
"/usr/local/etc/cloudflared"
|
|
1564
|
+
];
|
|
1565
|
+
const configurations = await Promise.all(
|
|
1566
|
+
directories.flatMap(
|
|
1567
|
+
(directory) => ["config.yml", "config.yaml"].map(
|
|
1568
|
+
async (name) => access(path4.join(directory, name), constants.F_OK).then(
|
|
1569
|
+
() => true,
|
|
1570
|
+
() => false
|
|
1571
|
+
)
|
|
1572
|
+
)
|
|
1573
|
+
)
|
|
1574
|
+
);
|
|
1575
|
+
if (configurations.some(Boolean))
|
|
1576
|
+
return { ...option, reason: "Existing tunnel configuration \xB7 paste its HTTPS URL" };
|
|
1577
|
+
return { ...option, available: true, reason: "Temporary public HTTPS URL \xB7 changes each start" };
|
|
1578
|
+
}
|
|
1579
|
+
async function detectTunnels(home = homedir()) {
|
|
1580
|
+
return Promise.all([probeTailscale(), probeCloudflared(home)]);
|
|
1581
|
+
}
|
|
1582
|
+
var ManagedTunnel = class {
|
|
1583
|
+
constructor(option, disconnected = () => {
|
|
1584
|
+
}) {
|
|
1585
|
+
this.option = option;
|
|
1586
|
+
this.disconnected = disconnected;
|
|
1587
|
+
}
|
|
1588
|
+
option;
|
|
1589
|
+
disconnected;
|
|
1590
|
+
child;
|
|
1591
|
+
stopping = false;
|
|
1592
|
+
connected = false;
|
|
1593
|
+
closing;
|
|
1594
|
+
running() {
|
|
1595
|
+
return this.connected && !this.stopping;
|
|
1596
|
+
}
|
|
1597
|
+
stopOnSignal = () => {
|
|
1598
|
+
void this.close();
|
|
1599
|
+
};
|
|
1600
|
+
stopOnExit = () => {
|
|
1601
|
+
if (this.child) signalChild(this.child, "SIGKILL");
|
|
1602
|
+
};
|
|
1603
|
+
async start(origin, signal) {
|
|
1604
|
+
signal?.throwIfAborted();
|
|
1605
|
+
if (!this.option.available) throw new Error(this.option.reason);
|
|
1606
|
+
const tailscale = this.option.provider === "tailscale";
|
|
1607
|
+
const child = spawn(
|
|
1608
|
+
this.option.executable ?? this.option.provider,
|
|
1609
|
+
tailscale ? ["serve", "--bg=false", `--https=${this.option.httpsPort}`, origin] : ["tunnel", "--no-autoupdate", "--url", origin],
|
|
1610
|
+
{
|
|
1611
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1612
|
+
detached: process.platform !== "win32",
|
|
1613
|
+
env: tailscale ? { ...process.env, TAILSCALE_BE_CLI: "1" } : process.env
|
|
1614
|
+
}
|
|
1615
|
+
);
|
|
1616
|
+
this.child = child;
|
|
1617
|
+
process.once("SIGINT", this.stopOnSignal);
|
|
1618
|
+
process.once("SIGTERM", this.stopOnSignal);
|
|
1619
|
+
process.once("exit", this.stopOnExit);
|
|
1620
|
+
return new Promise((resolve2, reject) => {
|
|
1621
|
+
let output = "";
|
|
1622
|
+
let address;
|
|
1623
|
+
let settled = false;
|
|
1624
|
+
const abort = () => fail("Tunnel startup canceled.");
|
|
1625
|
+
const timeout = setTimeout(
|
|
1626
|
+
() => fail("Tunnel startup timed out. Check your connection or paste an existing HTTPS URL."),
|
|
1627
|
+
45e3
|
|
1628
|
+
);
|
|
1629
|
+
const fail = (message) => {
|
|
1630
|
+
if (settled) return;
|
|
1631
|
+
settled = true;
|
|
1632
|
+
clearTimeout(timeout);
|
|
1633
|
+
signal?.removeEventListener("abort", abort);
|
|
1634
|
+
void this.close();
|
|
1635
|
+
reject(new Error(message));
|
|
1636
|
+
};
|
|
1637
|
+
const read = (data) => {
|
|
1638
|
+
output = `${output}${data.toString()}`.slice(-65536);
|
|
1639
|
+
address = tailscale ? this.option.publicUrl : output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com\b/)?.[0];
|
|
1640
|
+
const ready = tailscale ? Boolean(
|
|
1641
|
+
address && output.includes(address) && output.includes("Available within your tailnet")
|
|
1642
|
+
) : output.includes("Registered tunnel connection");
|
|
1643
|
+
if (settled || !address || !ready) return;
|
|
1644
|
+
settled = true;
|
|
1645
|
+
this.connected = true;
|
|
1646
|
+
clearTimeout(timeout);
|
|
1647
|
+
signal?.removeEventListener("abort", abort);
|
|
1648
|
+
resolve2(address);
|
|
1649
|
+
};
|
|
1650
|
+
child.stdout?.on("data", read);
|
|
1651
|
+
child.stderr?.on("data", read);
|
|
1652
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1653
|
+
child.once(
|
|
1654
|
+
"error",
|
|
1655
|
+
() => fail(`${this.option.provider} could not start. Check the installation and permissions.`)
|
|
1656
|
+
);
|
|
1657
|
+
child.once("exit", () => signalChild(child, "SIGTERM"));
|
|
1658
|
+
child.once("close", (code) => {
|
|
1659
|
+
const unexpected = this.connected && !this.stopping;
|
|
1660
|
+
this.connected = false;
|
|
1661
|
+
process.removeListener("SIGINT", this.stopOnSignal);
|
|
1662
|
+
process.removeListener("SIGTERM", this.stopOnSignal);
|
|
1663
|
+
process.removeListener("exit", this.stopOnExit);
|
|
1664
|
+
fail(
|
|
1665
|
+
`${this.option.provider} exited (${code ?? "signal"}). Check tunnel access or paste an existing HTTPS URL.`
|
|
1666
|
+
);
|
|
1667
|
+
if (unexpected) {
|
|
1668
|
+
this.disconnected();
|
|
1669
|
+
printError(
|
|
1670
|
+
"The managed tunnel stopped. Restart the server to reconnect your phone; local work can continue."
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
close() {
|
|
1677
|
+
this.stopping = true;
|
|
1678
|
+
const child = this.child;
|
|
1679
|
+
if (!child?.pid || child.exitCode !== null || child.signalCode !== null)
|
|
1680
|
+
return Promise.resolve();
|
|
1681
|
+
this.closing ??= new Promise((resolve2) => {
|
|
1682
|
+
const timeout = setTimeout(() => signalChild(child, "SIGKILL"), 2e3);
|
|
1683
|
+
child.once("close", () => {
|
|
1684
|
+
clearTimeout(timeout);
|
|
1685
|
+
resolve2();
|
|
1686
|
+
});
|
|
1687
|
+
signalChild(child, "SIGTERM");
|
|
1688
|
+
});
|
|
1689
|
+
return this.closing;
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
|
|
1693
|
+
// src/opentunnel.ts
|
|
1694
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1695
|
+
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
1696
|
+
import path5 from "node:path";
|
|
1697
|
+
import { z as z8 } from "zod";
|
|
1698
|
+
import {
|
|
1699
|
+
create,
|
|
1700
|
+
OpenTunnelStorage
|
|
1701
|
+
} from "./opentunnel-sdk.js";
|
|
1702
|
+
var OpenTunnel = class {
|
|
1703
|
+
constructor(directory, disconnected = () => {
|
|
1704
|
+
}, makeClient = create) {
|
|
1705
|
+
this.directory = directory;
|
|
1706
|
+
this.disconnected = disconnected;
|
|
1707
|
+
this.makeClient = makeClient;
|
|
1708
|
+
}
|
|
1709
|
+
directory;
|
|
1710
|
+
disconnected;
|
|
1711
|
+
makeClient;
|
|
1712
|
+
client;
|
|
1713
|
+
connection;
|
|
1714
|
+
stopping = false;
|
|
1715
|
+
running() {
|
|
1716
|
+
return this.connection?.status().state === "connected" && !this.stopping;
|
|
1717
|
+
}
|
|
1718
|
+
async start(origin, signal) {
|
|
1719
|
+
const endpoint = new URL(origin);
|
|
1720
|
+
if (!["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname))
|
|
1721
|
+
throw new Error("OpenTunnel must forward to the local loopback server.");
|
|
1722
|
+
await mkdir3(this.directory, { recursive: true, mode: 448 });
|
|
1723
|
+
await chmod2(this.directory, 448);
|
|
1724
|
+
const routeFile = path5.join(this.directory, "route.json");
|
|
1725
|
+
let route;
|
|
1726
|
+
try {
|
|
1727
|
+
route = z8.string().regex(/^[a-f0-9]{16}$/).parse(JSON.parse(await readFile3(routeFile, "utf8")));
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
if (!z8.object({ code: z8.literal("ENOENT") }).safeParse(error).success) throw error;
|
|
1730
|
+
route = randomBytes2(8).toString("hex");
|
|
1731
|
+
await writeFile2(routeFile, JSON.stringify(route), { flag: "wx", mode: 384 });
|
|
1732
|
+
}
|
|
1733
|
+
this.stopping = false;
|
|
1734
|
+
this.client = this.makeClient({
|
|
1735
|
+
store: OpenTunnelStorage.xdg({ env: { XDG_DATA_HOME: this.directory } })
|
|
1736
|
+
});
|
|
1737
|
+
const abort = AbortSignal.any([
|
|
1738
|
+
signal ?? new AbortController().signal,
|
|
1739
|
+
AbortSignal.timeout(12e4)
|
|
1740
|
+
]);
|
|
1741
|
+
let canceled = () => {
|
|
1742
|
+
};
|
|
1743
|
+
try {
|
|
1744
|
+
const connecting = this.client.tunnel.connect({
|
|
1745
|
+
profile: "agentrific",
|
|
1746
|
+
routes: {
|
|
1747
|
+
[route]: `${endpoint.hostname === "[::1]" ? "[::1]" : "127.0.0.1"}:${endpoint.port || "80"}`
|
|
1748
|
+
},
|
|
1749
|
+
signal
|
|
1750
|
+
});
|
|
1751
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
1752
|
+
canceled = () => reject(new Error("OpenTunnel connection canceled or timed out."));
|
|
1753
|
+
abort.addEventListener("abort", canceled, { once: true });
|
|
1754
|
+
if (abort.aborted) canceled();
|
|
1755
|
+
});
|
|
1756
|
+
const connection = await Promise.race([connecting, cancellation]);
|
|
1757
|
+
this.connection = connection;
|
|
1758
|
+
void connection.closed.then(() => {
|
|
1759
|
+
if (!this.stopping) this.disconnected();
|
|
1760
|
+
}).catch(() => {
|
|
1761
|
+
if (!this.stopping) this.disconnected();
|
|
1762
|
+
});
|
|
1763
|
+
return `https://${route}.${connection.tunnel.hostname}`;
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
await this.close();
|
|
1766
|
+
throw error;
|
|
1767
|
+
} finally {
|
|
1768
|
+
abort.removeEventListener("abort", canceled);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
async close() {
|
|
1772
|
+
this.stopping = true;
|
|
1773
|
+
await this.connection?.close();
|
|
1774
|
+
await this.client?.dispose();
|
|
1775
|
+
this.connection = void 0;
|
|
1776
|
+
this.client = void 0;
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1780
|
+
// src/desktop-phone.ts
|
|
1781
|
+
var DesktopPhoneConnection = class {
|
|
1782
|
+
constructor(origin, changed, directory, detect = async () => [
|
|
1783
|
+
{ provider: "opentunnel", available: true, reason: "Built in \xB7 stable public HTTPS address" },
|
|
1784
|
+
...await detectTunnels()
|
|
1785
|
+
], create2 = (option, disconnected) => option.provider === "opentunnel" ? new OpenTunnel(directory, disconnected) : new ManagedTunnel({ ...option, provider: option.provider }, disconnected)) {
|
|
1786
|
+
this.origin = origin;
|
|
1787
|
+
this.changed = changed;
|
|
1788
|
+
this.directory = directory;
|
|
1789
|
+
this.detect = detect;
|
|
1790
|
+
this.create = create2;
|
|
1791
|
+
}
|
|
1792
|
+
origin;
|
|
1793
|
+
changed;
|
|
1794
|
+
directory;
|
|
1795
|
+
detect;
|
|
1796
|
+
create;
|
|
1797
|
+
tunnel;
|
|
1798
|
+
provider = null;
|
|
1799
|
+
publicUrl = null;
|
|
1800
|
+
pending = Promise.resolve();
|
|
1801
|
+
snapshot() {
|
|
1802
|
+
return { provider: this.provider, publicUrl: this.publicUrl };
|
|
1803
|
+
}
|
|
1804
|
+
async options() {
|
|
1805
|
+
return (await this.detect()).map(({ provider, available, reason }) => ({
|
|
1806
|
+
provider,
|
|
1807
|
+
available,
|
|
1808
|
+
reason
|
|
1809
|
+
}));
|
|
1810
|
+
}
|
|
1811
|
+
serialize(operation) {
|
|
1812
|
+
const result = this.pending.then(operation);
|
|
1813
|
+
this.pending = result.then(
|
|
1814
|
+
() => {
|
|
1815
|
+
},
|
|
1816
|
+
() => {
|
|
1817
|
+
}
|
|
1818
|
+
);
|
|
1819
|
+
return result;
|
|
1820
|
+
}
|
|
1821
|
+
start(provider, signal) {
|
|
1822
|
+
return this.serialize(async () => {
|
|
1823
|
+
signal?.throwIfAborted();
|
|
1824
|
+
if (this.tunnel && this.publicUrl && this.provider === provider) return this.snapshot();
|
|
1825
|
+
if (this.tunnel)
|
|
1826
|
+
throw new Error("Disconnect the current phone connection before changing providers.");
|
|
1827
|
+
const option = (await this.detect()).find((candidate) => candidate.provider === provider);
|
|
1828
|
+
if (!option?.available) throw new Error(option?.reason || "Tunnel provider unavailable.");
|
|
1829
|
+
let tunnel;
|
|
1830
|
+
tunnel = this.create(option, () => {
|
|
1831
|
+
if (this.tunnel !== tunnel) return;
|
|
1832
|
+
void tunnel.close().catch(() => {
|
|
1833
|
+
});
|
|
1834
|
+
this.tunnel = void 0;
|
|
1835
|
+
this.provider = null;
|
|
1836
|
+
this.publicUrl = null;
|
|
1837
|
+
this.changed(null);
|
|
1838
|
+
});
|
|
1839
|
+
this.tunnel = tunnel;
|
|
1840
|
+
try {
|
|
1841
|
+
const url = await tunnel.start(this.origin(), signal);
|
|
1842
|
+
signal?.throwIfAborted();
|
|
1843
|
+
this.provider = provider;
|
|
1844
|
+
this.publicUrl = url;
|
|
1845
|
+
this.changed(url);
|
|
1846
|
+
return this.snapshot();
|
|
1847
|
+
} catch (error) {
|
|
1848
|
+
await tunnel.close();
|
|
1849
|
+
this.tunnel = void 0;
|
|
1850
|
+
this.changed(null);
|
|
1851
|
+
throw error;
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
close() {
|
|
1856
|
+
return this.serialize(async () => {
|
|
1857
|
+
await this.tunnel?.close();
|
|
1858
|
+
this.tunnel = void 0;
|
|
1859
|
+
this.provider = null;
|
|
1860
|
+
this.publicUrl = null;
|
|
1861
|
+
this.changed(null);
|
|
1862
|
+
return this.snapshot();
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
|
|
1381
1867
|
// src/maintenance.ts
|
|
1382
1868
|
var MaintenanceError = class extends Error {
|
|
1383
1869
|
statusCode = 503;
|
|
@@ -1419,9 +1905,9 @@ var Maintenance = class {
|
|
|
1419
1905
|
}
|
|
1420
1906
|
attach(app2) {
|
|
1421
1907
|
app2.addHook("onRequest", async (request, reply) => {
|
|
1422
|
-
const
|
|
1908
|
+
const path12 = request.url.split("?")[0];
|
|
1423
1909
|
if (["GET", "HEAD", "OPTIONS"].includes(request.method)) return;
|
|
1424
|
-
const allowed =
|
|
1910
|
+
const allowed = path12.startsWith("/api/auth/") || /^\/api\/sessions\/[^/]+\/stop$/.test(path12);
|
|
1425
1911
|
if (this.paused && !allowed)
|
|
1426
1912
|
return reply.code(503).send({ code: "MAINTENANCE", error: new MaintenanceError().message });
|
|
1427
1913
|
this.requests.add(request.id);
|
|
@@ -1434,10 +1920,10 @@ var Maintenance = class {
|
|
|
1434
1920
|
|
|
1435
1921
|
// src/updates.ts
|
|
1436
1922
|
import semver from "semver";
|
|
1437
|
-
import { z as
|
|
1923
|
+
import { z as z9 } from "zod";
|
|
1438
1924
|
import { Effect as Effect5 } from "effect";
|
|
1439
|
-
var RegistryRelease =
|
|
1440
|
-
version:
|
|
1925
|
+
var RegistryRelease = z9.object({
|
|
1926
|
+
version: z9.string().refine((value) => semver.valid(value) !== null)
|
|
1441
1927
|
});
|
|
1442
1928
|
async function checkForUpdates() {
|
|
1443
1929
|
const response = await fetch("https://registry.npmjs.org/@agentrific%2Fserver/latest", {
|
|
@@ -1527,7 +2013,7 @@ var UpdateDiscovery = class {
|
|
|
1527
2013
|
|
|
1528
2014
|
// src/index.ts
|
|
1529
2015
|
import { Effect as Effect17, ManagedRuntime } from "effect";
|
|
1530
|
-
import { z as
|
|
2016
|
+
import { z as z19 } from "zod";
|
|
1531
2017
|
|
|
1532
2018
|
// src/automation.ts
|
|
1533
2019
|
import { Effect as Effect7 } from "effect";
|
|
@@ -1762,7 +2248,7 @@ var AutomationCoordinator = class {
|
|
|
1762
2248
|
};
|
|
1763
2249
|
|
|
1764
2250
|
// src/runtime.ts
|
|
1765
|
-
import
|
|
2251
|
+
import path6 from "node:path";
|
|
1766
2252
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
1767
2253
|
|
|
1768
2254
|
// src/git-flow.ts
|
|
@@ -1827,8 +2313,8 @@ var AgentController = class {
|
|
|
1827
2313
|
const agent = yield* Effect8.tryPromise({
|
|
1828
2314
|
try: () => this.factory.open(
|
|
1829
2315
|
session.id,
|
|
1830
|
-
session.workspacePath ??
|
|
1831
|
-
|
|
2316
|
+
session.workspacePath ?? path6.join(this.worktreesRoot, session.id),
|
|
2317
|
+
path6.join(this.sessionsRoot, session.id),
|
|
1832
2318
|
session.model,
|
|
1833
2319
|
session
|
|
1834
2320
|
),
|
|
@@ -2063,7 +2549,7 @@ var AgentSessions = class extends Context2.Service()(
|
|
|
2063
2549
|
|
|
2064
2550
|
// src/delivery.ts
|
|
2065
2551
|
import { Effect as Effect9 } from "effect";
|
|
2066
|
-
import { z as
|
|
2552
|
+
import { z as z11 } from "zod";
|
|
2067
2553
|
|
|
2068
2554
|
// src/workflow-command.ts
|
|
2069
2555
|
import { execFile } from "node:child_process";
|
|
@@ -2111,12 +2597,12 @@ function workflowError(error) {
|
|
|
2111
2597
|
}
|
|
2112
2598
|
|
|
2113
2599
|
// src/workflow-schema.ts
|
|
2114
|
-
import { z as
|
|
2115
|
-
var DeliveryJobSchema =
|
|
2116
|
-
kind:
|
|
2117
|
-
sessionId:
|
|
2600
|
+
import { z as z10 } from "zod";
|
|
2601
|
+
var DeliveryJobSchema = z10.object({
|
|
2602
|
+
kind: z10.literal("delivery"),
|
|
2603
|
+
sessionId: z10.string(),
|
|
2118
2604
|
request: DeliveryRequestSchema,
|
|
2119
|
-
phase:
|
|
2605
|
+
phase: z10.enum([
|
|
2120
2606
|
"validating",
|
|
2121
2607
|
"drafting",
|
|
2122
2608
|
"committing",
|
|
@@ -2126,25 +2612,25 @@ var DeliveryJobSchema = z8.object({
|
|
|
2126
2612
|
"opening_pr",
|
|
2127
2613
|
"delivered"
|
|
2128
2614
|
]),
|
|
2129
|
-
attempt:
|
|
2130
|
-
commitSha:
|
|
2131
|
-
title:
|
|
2132
|
-
body:
|
|
2133
|
-
logs:
|
|
2134
|
-
failed:
|
|
2135
|
-
result:
|
|
2136
|
-
url:
|
|
2137
|
-
message:
|
|
2615
|
+
attempt: z10.number().int().positive(),
|
|
2616
|
+
commitSha: z10.string().optional(),
|
|
2617
|
+
title: z10.string(),
|
|
2618
|
+
body: z10.string(),
|
|
2619
|
+
logs: z10.string().default(""),
|
|
2620
|
+
failed: z10.boolean().default(false),
|
|
2621
|
+
result: z10.enum(["merged", "pr_opened", "no_changes"]).optional(),
|
|
2622
|
+
url: z10.string().optional(),
|
|
2623
|
+
message: z10.string().optional()
|
|
2138
2624
|
});
|
|
2139
|
-
var PreparationJobSchema =
|
|
2140
|
-
kind:
|
|
2141
|
-
sessionId:
|
|
2142
|
-
projectPath:
|
|
2625
|
+
var PreparationJobSchema = z10.object({
|
|
2626
|
+
kind: z10.literal("preparation"),
|
|
2627
|
+
sessionId: z10.string(),
|
|
2628
|
+
projectPath: z10.string(),
|
|
2143
2629
|
input: CreateSessionSchema,
|
|
2144
|
-
stage:
|
|
2145
|
-
logs:
|
|
2630
|
+
stage: z10.enum(["creating", "installing", "ready", "failed"]),
|
|
2631
|
+
logs: z10.string().default("")
|
|
2146
2632
|
});
|
|
2147
|
-
var WorkflowJobSchema =
|
|
2633
|
+
var WorkflowJobSchema = z10.discriminatedUnion("kind", [
|
|
2148
2634
|
DeliveryJobSchema,
|
|
2149
2635
|
PreparationJobSchema
|
|
2150
2636
|
]);
|
|
@@ -2440,11 +2926,11 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2440
2926
|
"--json",
|
|
2441
2927
|
"url,state,headRefOid"
|
|
2442
2928
|
]);
|
|
2443
|
-
const prs =
|
|
2444
|
-
|
|
2445
|
-
url:
|
|
2446
|
-
state:
|
|
2447
|
-
headRefOid:
|
|
2929
|
+
const prs = z11.array(
|
|
2930
|
+
z11.object({
|
|
2931
|
+
url: z11.string().url(),
|
|
2932
|
+
state: z11.string(),
|
|
2933
|
+
headRefOid: z11.string().optional()
|
|
2448
2934
|
})
|
|
2449
2935
|
).parse(JSON.parse(existing));
|
|
2450
2936
|
const open = prs.find(
|
|
@@ -2485,10 +2971,10 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2485
2971
|
};
|
|
2486
2972
|
|
|
2487
2973
|
// src/preparation.ts
|
|
2488
|
-
import { mkdir as
|
|
2489
|
-
import
|
|
2974
|
+
import { mkdir as mkdir4, readFile as readFile4 } from "node:fs/promises";
|
|
2975
|
+
import path7 from "node:path";
|
|
2490
2976
|
import { Effect as Effect10 } from "effect";
|
|
2491
|
-
import { z as
|
|
2977
|
+
import { z as z12 } from "zod";
|
|
2492
2978
|
var PreparationCoordinator = class {
|
|
2493
2979
|
constructor(store2, options) {
|
|
2494
2980
|
this.store = store2;
|
|
@@ -2582,7 +3068,7 @@ ${logs}` : ""}`);
|
|
|
2582
3068
|
const workspace = session.workspacePath;
|
|
2583
3069
|
if (branchAtWorkspace !== void 0)
|
|
2584
3070
|
throw new Error("The run workspace already exists on a different branch.");
|
|
2585
|
-
await
|
|
3071
|
+
await mkdir4(path7.dirname(workspace), { recursive: true });
|
|
2586
3072
|
try {
|
|
2587
3073
|
await command(
|
|
2588
3074
|
projectPath,
|
|
@@ -2602,12 +3088,12 @@ ${logs}` : ""}`);
|
|
|
2602
3088
|
}
|
|
2603
3089
|
}
|
|
2604
3090
|
async install(workspace, command) {
|
|
2605
|
-
const packageFile = await
|
|
3091
|
+
const packageFile = await readFile4(path7.join(workspace, "package.json"), "utf8").catch(
|
|
2606
3092
|
() => void 0
|
|
2607
3093
|
);
|
|
2608
3094
|
if (!packageFile) return;
|
|
2609
|
-
const metadata =
|
|
2610
|
-
const has = async (name) =>
|
|
3095
|
+
const metadata = z12.object({ packageManager: z12.string().optional() }).parse(JSON.parse(packageFile));
|
|
3096
|
+
const has = async (name) => readFile4(path7.join(workspace, name)).then(
|
|
2611
3097
|
() => true,
|
|
2612
3098
|
() => false
|
|
2613
3099
|
);
|
|
@@ -2688,8 +3174,8 @@ ${logs}` : ""}`);
|
|
|
2688
3174
|
|
|
2689
3175
|
// src/delivery-writer.ts
|
|
2690
3176
|
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2691
|
-
import { z as
|
|
2692
|
-
var DraftSchema =
|
|
3177
|
+
import { z as z13 } from "zod";
|
|
3178
|
+
var DraftSchema = z13.object({ title: z13.string().trim().min(1).max(120), body: z13.string().trim().min(1).max(1e4) }).strict();
|
|
2693
3179
|
function deliveryWriter(models2, createSession = createAgentSession) {
|
|
2694
3180
|
return async (run, input, diff, signal) => {
|
|
2695
3181
|
signal?.throwIfAborted();
|
|
@@ -2844,7 +3330,7 @@ import {
|
|
|
2844
3330
|
createAgentSession as createAgentSession2,
|
|
2845
3331
|
SessionManager as SessionManager2
|
|
2846
3332
|
} from "@earendil-works/pi-coding-agent";
|
|
2847
|
-
import { z as
|
|
3333
|
+
import { z as z15 } from "zod";
|
|
2848
3334
|
|
|
2849
3335
|
// src/delivery-tool.ts
|
|
2850
3336
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
@@ -2881,23 +3367,23 @@ function deliveryTool(sessionId, delivery2) {
|
|
|
2881
3367
|
// src/file-change.ts
|
|
2882
3368
|
import { createWriteToolDefinition, defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
|
|
2883
3369
|
import { createTwoFilesPatch } from "diff";
|
|
2884
|
-
import { mkdir as
|
|
2885
|
-
import { z as
|
|
3370
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
3371
|
+
import { z as z14 } from "zod";
|
|
2886
3372
|
var previewLimit = 256e3;
|
|
2887
3373
|
function boundFileChange(change) {
|
|
2888
3374
|
if ((change.patch?.length ?? 0) + (change.content?.length ?? 0) > previewLimit)
|
|
2889
3375
|
return { path: change.path, omitted: true };
|
|
2890
3376
|
return change;
|
|
2891
3377
|
}
|
|
2892
|
-
var EditResultSchema =
|
|
2893
|
-
details:
|
|
3378
|
+
var EditResultSchema = z14.object({
|
|
3379
|
+
details: z14.object({ patch: z14.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
|
|
2894
3380
|
});
|
|
2895
|
-
function completedFileChange(result,
|
|
3381
|
+
function completedFileChange(result, path12) {
|
|
2896
3382
|
const parsed = EditResultSchema.safeParse(result);
|
|
2897
3383
|
if (!parsed.success) return;
|
|
2898
3384
|
const details = parsed.data.details;
|
|
2899
3385
|
if (details?.fileChange) return boundFileChange(details.fileChange);
|
|
2900
|
-
if (
|
|
3386
|
+
if (path12 && details?.patch) return boundFileChange({ path: path12, patch: details.patch });
|
|
2901
3387
|
}
|
|
2902
3388
|
function fileWriteTool(cwd) {
|
|
2903
3389
|
const base = createWriteToolDefinition(cwd);
|
|
@@ -2914,16 +3400,16 @@ function fileWriteTool(cwd) {
|
|
|
2914
3400
|
const tool = createWriteToolDefinition(cwd, {
|
|
2915
3401
|
operations: {
|
|
2916
3402
|
mkdir: async (dir) => {
|
|
2917
|
-
await
|
|
3403
|
+
await mkdir5(dir, { recursive: true });
|
|
2918
3404
|
},
|
|
2919
3405
|
// Pi invokes this inside its per-file mutation queue, so the snapshot
|
|
2920
3406
|
// and write cannot race another built-in edit/write on the same file.
|
|
2921
|
-
writeFile: async (
|
|
2922
|
-
const previous = await
|
|
3407
|
+
writeFile: async (path12, content) => {
|
|
3408
|
+
const previous = await readFile5(path12, "utf8").catch((error) => {
|
|
2923
3409
|
if (error.code === "ENOENT") return void 0;
|
|
2924
3410
|
throw error;
|
|
2925
3411
|
});
|
|
2926
|
-
await
|
|
3412
|
+
await writeFile3(path12, content, "utf8");
|
|
2927
3413
|
fileChange = boundFileChange(
|
|
2928
3414
|
previous === void 0 ? { path: args.path, content } : {
|
|
2929
3415
|
path: args.path,
|
|
@@ -2940,8 +3426,8 @@ function fileWriteTool(cwd) {
|
|
|
2940
3426
|
}
|
|
2941
3427
|
|
|
2942
3428
|
// src/pi.ts
|
|
2943
|
-
var ToolResultSchema =
|
|
2944
|
-
content:
|
|
3429
|
+
var ToolResultSchema = z15.object({
|
|
3430
|
+
content: z15.array(z15.object({ type: z15.string(), text: z15.string().optional() })).optional()
|
|
2945
3431
|
}).passthrough();
|
|
2946
3432
|
var boundedJson = (value, limit) => {
|
|
2947
3433
|
let text;
|
|
@@ -3065,7 +3551,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
3065
3551
|
return false;
|
|
3066
3552
|
}
|
|
3067
3553
|
const filePaths = /* @__PURE__ */ new Map();
|
|
3068
|
-
const FileArgsSchema =
|
|
3554
|
+
const FileArgsSchema = z15.object({ path: z15.string() });
|
|
3069
3555
|
function handleTool(event) {
|
|
3070
3556
|
if (event.type === "tool_execution_start") {
|
|
3071
3557
|
flushText();
|
|
@@ -3125,24 +3611,24 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
3125
3611
|
}
|
|
3126
3612
|
|
|
3127
3613
|
// src/projects.ts
|
|
3128
|
-
import { access, readdir as readdir2, realpath, stat as
|
|
3614
|
+
import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
3129
3615
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
3130
|
-
import { z as
|
|
3131
|
-
import { constants } from "node:fs";
|
|
3132
|
-
import { homedir } from "node:os";
|
|
3133
|
-
import
|
|
3616
|
+
import { z as z16 } from "zod";
|
|
3617
|
+
import { constants as constants2 } from "node:fs";
|
|
3618
|
+
import { homedir as homedir2 } from "node:os";
|
|
3619
|
+
import path8 from "node:path";
|
|
3134
3620
|
import { Effect as Effect13, Semaphore as Semaphore3 } from "effect";
|
|
3135
3621
|
function resolveProjectsRoot(input) {
|
|
3136
3622
|
return Effect13.tryPromise({
|
|
3137
3623
|
try: async () => {
|
|
3138
3624
|
const trimmed = input.trim();
|
|
3139
|
-
const expanded = trimmed === "~" ?
|
|
3140
|
-
if (!
|
|
3625
|
+
const expanded = trimmed === "~" ? homedir2() : trimmed.startsWith("~/") ? path8.join(homedir2(), trimmed.slice(2)) : trimmed;
|
|
3626
|
+
if (!path8.isAbsolute(expanded))
|
|
3141
3627
|
throw new Error("Use an absolute server path or a path beginning with ~/.");
|
|
3142
3628
|
const resolved = await realpath(expanded);
|
|
3143
|
-
if (!(await
|
|
3629
|
+
if (!(await stat3(resolved)).isDirectory())
|
|
3144
3630
|
throw new Error("The projects folder must be a directory.");
|
|
3145
|
-
await
|
|
3631
|
+
await access2(resolved, constants2.R_OK | constants2.X_OK);
|
|
3146
3632
|
return resolved;
|
|
3147
3633
|
},
|
|
3148
3634
|
catch: () => new Error(
|
|
@@ -3159,36 +3645,36 @@ function projectLock(store2) {
|
|
|
3159
3645
|
}
|
|
3160
3646
|
return lock;
|
|
3161
3647
|
}
|
|
3162
|
-
var ProjectId =
|
|
3648
|
+
var ProjectId = z16.string().trim().min(1).max(255);
|
|
3163
3649
|
async function gitDirectory(directory) {
|
|
3164
|
-
const location =
|
|
3165
|
-
const info = await
|
|
3650
|
+
const location = path8.join(directory, ".git");
|
|
3651
|
+
const info = await stat3(location);
|
|
3166
3652
|
if (info.isDirectory()) return location;
|
|
3167
|
-
const contents = await
|
|
3653
|
+
const contents = await readFile6(location, "utf8");
|
|
3168
3654
|
const match = /^gitdir: (.+)\s*$/m.exec(contents);
|
|
3169
3655
|
if (!match) throw new Error("Invalid Git worktree metadata.");
|
|
3170
|
-
const resolved = await realpath(
|
|
3171
|
-
if (!(await
|
|
3656
|
+
const resolved = await realpath(path8.resolve(directory, match[1].trim()));
|
|
3657
|
+
if (!(await stat3(resolved)).isDirectory()) throw new Error("Git directory is missing.");
|
|
3172
3658
|
return resolved;
|
|
3173
3659
|
}
|
|
3174
3660
|
async function identity(directory, preferred) {
|
|
3175
|
-
const filename =
|
|
3661
|
+
const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
|
|
3176
3662
|
try {
|
|
3177
|
-
return ProjectId.parse(await
|
|
3663
|
+
return ProjectId.parse(await readFile6(filename, "utf8"));
|
|
3178
3664
|
} catch (error) {
|
|
3179
|
-
if (!
|
|
3180
|
-
const legacyFilename =
|
|
3181
|
-
const legacy = await
|
|
3182
|
-
if (!
|
|
3665
|
+
if (!z16.object({ code: z16.literal("ENOENT") }).safeParse(error).success) throw error;
|
|
3666
|
+
const legacyFilename = path8.join(await gitDirectory(directory), "chatplans-project-id");
|
|
3667
|
+
const legacy = await readFile6(legacyFilename, "utf8").catch((cause) => {
|
|
3668
|
+
if (!z16.object({ code: z16.literal("ENOENT") }).safeParse(cause).success) throw cause;
|
|
3183
3669
|
return null;
|
|
3184
3670
|
});
|
|
3185
3671
|
const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
|
|
3186
3672
|
try {
|
|
3187
|
-
await
|
|
3673
|
+
await writeFile4(filename, id + "\n", { flag: "wx" });
|
|
3188
3674
|
return id;
|
|
3189
3675
|
} catch (writeError) {
|
|
3190
|
-
if (!
|
|
3191
|
-
return ProjectId.parse(await
|
|
3676
|
+
if (!z16.object({ code: z16.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
|
|
3677
|
+
return ProjectId.parse(await readFile6(filename, "utf8"));
|
|
3192
3678
|
}
|
|
3193
3679
|
}
|
|
3194
3680
|
}
|
|
@@ -3201,15 +3687,15 @@ function scanProjects(root, store2) {
|
|
|
3201
3687
|
const entries = await readdir2(root, { withFileTypes: true });
|
|
3202
3688
|
for (const entry of entries) {
|
|
3203
3689
|
if (!entry.isDirectory()) continue;
|
|
3204
|
-
const directory =
|
|
3205
|
-
if (!await
|
|
3690
|
+
const directory = path8.join(root, entry.name);
|
|
3691
|
+
if (!await stat3(path8.join(directory, ".git")).catch(() => null)) continue;
|
|
3206
3692
|
const previous = registered.find((project2) => project2.path === directory);
|
|
3207
3693
|
let id = await identity(directory, previous?.available ? void 0 : previous?.id);
|
|
3208
3694
|
let existing = registered.find((project2) => project2.id === id);
|
|
3209
|
-
if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await
|
|
3695
|
+
if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat3(path8.join(existing.path, ".git")).catch(() => null)) {
|
|
3210
3696
|
id = randomUUID6();
|
|
3211
|
-
await
|
|
3212
|
-
|
|
3697
|
+
await writeFile4(
|
|
3698
|
+
path8.join(await gitDirectory(directory), "agentrific-project-id"),
|
|
3213
3699
|
id + "\n"
|
|
3214
3700
|
);
|
|
3215
3701
|
existing = void 0;
|
|
@@ -3242,16 +3728,16 @@ function reconnectProject(root, id, input, store2) {
|
|
|
3242
3728
|
))
|
|
3243
3729
|
throw new Error("Stop this project's running sessions before reconnecting its folder.");
|
|
3244
3730
|
const directory = await Effect13.runPromise(resolveProjectsRoot(input));
|
|
3245
|
-
if (
|
|
3731
|
+
if (path8.dirname(directory) !== await realpath(root))
|
|
3246
3732
|
throw new Error("Choose a Git repository directly inside the configured projects folder.");
|
|
3247
|
-
const filename =
|
|
3733
|
+
const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
|
|
3248
3734
|
const replacedId = await identity(directory);
|
|
3249
|
-
const project = { id, name:
|
|
3250
|
-
await
|
|
3735
|
+
const project = { id, name: path8.basename(directory), path: directory, available: true };
|
|
3736
|
+
await writeFile4(filename, id + "\n");
|
|
3251
3737
|
try {
|
|
3252
3738
|
await Effect13.runPromise(store2.relocateProject(project, previous.path, replacedId));
|
|
3253
3739
|
} catch (error) {
|
|
3254
|
-
await
|
|
3740
|
+
await writeFile4(filename, replacedId + "\n");
|
|
3255
3741
|
throw error;
|
|
3256
3742
|
}
|
|
3257
3743
|
return project;
|
|
@@ -3263,7 +3749,7 @@ function reconnectProject(root, id, input, store2) {
|
|
|
3263
3749
|
// src/project-git.ts
|
|
3264
3750
|
import { execFile as execFile2 } from "node:child_process";
|
|
3265
3751
|
import { promisify as promisify2 } from "node:util";
|
|
3266
|
-
var
|
|
3752
|
+
var execute2 = promisify2(execFile2);
|
|
3267
3753
|
function repositoryIdentity(remote) {
|
|
3268
3754
|
const input = remote.trim();
|
|
3269
3755
|
if (!input || input.startsWith("/") || input.startsWith(".")) return null;
|
|
@@ -3277,14 +3763,14 @@ function repositoryIdentity(remote) {
|
|
|
3277
3763
|
if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return null;
|
|
3278
3764
|
if (url.search || url.hash || url.port) return null;
|
|
3279
3765
|
const host2 = url.hostname.toLowerCase();
|
|
3280
|
-
let
|
|
3281
|
-
if (!host2 || !
|
|
3282
|
-
if (["github.com", "bitbucket.org"].includes(host2))
|
|
3283
|
-
const key = `${host2}/${
|
|
3766
|
+
let path12 = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
3767
|
+
if (!host2 || !path12 || path12.includes("%") || path12.includes("//")) return null;
|
|
3768
|
+
if (["github.com", "bitbucket.org"].includes(host2)) path12 = path12.toLowerCase();
|
|
3769
|
+
const key = `${host2}/${path12}`;
|
|
3284
3770
|
return { key, display: key };
|
|
3285
3771
|
}
|
|
3286
3772
|
async function git(directory, args) {
|
|
3287
|
-
const { stdout } = await
|
|
3773
|
+
const { stdout } = await execute2("git", ["-C", directory, ...args], {
|
|
3288
3774
|
timeout: 3e3,
|
|
3289
3775
|
maxBuffer: 1024 * 1024,
|
|
3290
3776
|
env: { ...workflowEnvironment(), GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
|
|
@@ -3310,7 +3796,7 @@ async function inspect(directory) {
|
|
|
3310
3796
|
git(directory, ["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => null),
|
|
3311
3797
|
// NUL-separated records avoid ambiguous filenames/newlines. --no-renames
|
|
3312
3798
|
// keeps each entry one record, so we only need its two status characters.
|
|
3313
|
-
|
|
3799
|
+
execute2(
|
|
3314
3800
|
"git",
|
|
3315
3801
|
[
|
|
3316
3802
|
"-C",
|
|
@@ -3362,35 +3848,35 @@ function projectGitStatus(directory) {
|
|
|
3362
3848
|
|
|
3363
3849
|
// src/store.ts
|
|
3364
3850
|
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
3365
|
-
import
|
|
3851
|
+
import path9 from "node:path";
|
|
3366
3852
|
|
|
3367
3853
|
// src/push-schema.ts
|
|
3368
|
-
import { z as
|
|
3369
|
-
var PushBindingSchema =
|
|
3370
|
-
bindingId:
|
|
3371
|
-
installationId:
|
|
3372
|
-
deviceId:
|
|
3854
|
+
import { z as z17 } from "zod";
|
|
3855
|
+
var PushBindingSchema = z17.object({
|
|
3856
|
+
bindingId: z17.string().uuid(),
|
|
3857
|
+
installationId: z17.string().uuid(),
|
|
3858
|
+
deviceId: z17.string().uuid()
|
|
3373
3859
|
});
|
|
3374
|
-
var PushStateSchema =
|
|
3375
|
-
bindings:
|
|
3376
|
-
error:
|
|
3860
|
+
var PushStateSchema = z17.object({
|
|
3861
|
+
bindings: z17.array(PushBindingSchema).max(10),
|
|
3862
|
+
error: z17.string().nullable()
|
|
3377
3863
|
});
|
|
3378
3864
|
var PendingPushSchema = PushEventSchema.extend({
|
|
3379
|
-
remaining:
|
|
3380
|
-
createdAt:
|
|
3381
|
-
attempts:
|
|
3382
|
-
nextAt:
|
|
3865
|
+
remaining: z17.array(PushBindingSchema),
|
|
3866
|
+
createdAt: z17.number(),
|
|
3867
|
+
attempts: z17.number().int().nonnegative(),
|
|
3868
|
+
nextAt: z17.number()
|
|
3383
3869
|
});
|
|
3384
3870
|
|
|
3385
3871
|
// src/store.ts
|
|
3386
3872
|
import { DatabaseSync } from "node:sqlite";
|
|
3387
3873
|
import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
|
|
3388
|
-
import { z as
|
|
3874
|
+
import { z as z18 } from "zod";
|
|
3389
3875
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
3390
3876
|
var StorageError = class extends Data3.TaggedError("StorageError") {
|
|
3391
3877
|
};
|
|
3392
|
-
var PayloadRow =
|
|
3393
|
-
var EventRow =
|
|
3878
|
+
var PayloadRow = z18.object({ payload: z18.string() });
|
|
3879
|
+
var EventRow = z18.object({ sequence: z18.number(), recorded_at: z18.string(), payload: z18.string() });
|
|
3394
3880
|
var SqliteSessionStore = class {
|
|
3395
3881
|
constructor(db) {
|
|
3396
3882
|
this.db = db;
|
|
@@ -3441,7 +3927,7 @@ var SqliteSessionStore = class {
|
|
|
3441
3927
|
const row = PayloadRow.parse(
|
|
3442
3928
|
this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
|
|
3443
3929
|
);
|
|
3444
|
-
return
|
|
3930
|
+
return z18.string().uuid().parse(JSON.parse(row.payload));
|
|
3445
3931
|
});
|
|
3446
3932
|
}
|
|
3447
3933
|
saveDevice(device, tokenHash) {
|
|
@@ -3529,10 +4015,10 @@ var SqliteSessionStore = class {
|
|
|
3529
4015
|
`).all();
|
|
3530
4016
|
const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
|
|
3531
4017
|
for (const row of legacy) {
|
|
3532
|
-
const id =
|
|
4018
|
+
const id = z18.object({ id: z18.string() }).parse(row).id;
|
|
3533
4019
|
insert.run(
|
|
3534
4020
|
id,
|
|
3535
|
-
JSON.stringify({ id, name: id, path:
|
|
4021
|
+
JSON.stringify({ id, name: id, path: path9.join(root, id), available: false })
|
|
3536
4022
|
);
|
|
3537
4023
|
}
|
|
3538
4024
|
return this.db.prepare("SELECT payload FROM projects").all().map((row) => ProjectSchema.parse(JSON.parse(PayloadRow.parse(row).payload)));
|
|
@@ -3569,7 +4055,7 @@ var SqliteSessionStore = class {
|
|
|
3569
4055
|
this.db.prepare(
|
|
3570
4056
|
"UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
|
|
3571
4057
|
).run(project.path, previousPath);
|
|
3572
|
-
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${
|
|
4058
|
+
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path9.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
|
|
3573
4059
|
for (const row of this.db.prepare("SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?").all(project.id)) {
|
|
3574
4060
|
const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
|
|
3575
4061
|
if (session.workspacePath) {
|
|
@@ -3739,7 +4225,7 @@ var SqliteSessionStore = class {
|
|
|
3739
4225
|
if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
|
|
3740
4226
|
const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
|
|
3741
4227
|
if (notification) {
|
|
3742
|
-
const count =
|
|
4228
|
+
const count = z18.object({ total: z18.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
|
|
3743
4229
|
if (count.total >= 1e3)
|
|
3744
4230
|
this.db.prepare(
|
|
3745
4231
|
"DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
|
|
@@ -3785,13 +4271,13 @@ var SessionStore = class extends Context3.Service()(
|
|
|
3785
4271
|
Effect14.acquireRelease(
|
|
3786
4272
|
Effect14.try({
|
|
3787
4273
|
try: () => {
|
|
3788
|
-
mkdirSync2(
|
|
4274
|
+
mkdirSync2(path9.dirname(filename), { recursive: true });
|
|
3789
4275
|
const db = new DatabaseSync(filename);
|
|
3790
4276
|
chmodSync(filename, 384);
|
|
3791
4277
|
for (const suffix of ["-wal", "-shm"])
|
|
3792
4278
|
if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
|
|
3793
4279
|
try {
|
|
3794
|
-
const schema =
|
|
4280
|
+
const schema = z18.object({ user_version: z18.number() }).parse(db.prepare("PRAGMA user_version").get());
|
|
3795
4281
|
if (schema.user_version > 1)
|
|
3796
4282
|
throw new Error("This database requires a newer Agentrific server.");
|
|
3797
4283
|
db.exec(`
|
|
@@ -3826,7 +4312,7 @@ var SessionStore = class extends Context3.Service()(
|
|
|
3826
4312
|
|
|
3827
4313
|
// src/worktree-cleanup.ts
|
|
3828
4314
|
import { lstat as lstat2, rm as rm2 } from "node:fs/promises";
|
|
3829
|
-
import
|
|
4315
|
+
import path10 from "node:path";
|
|
3830
4316
|
import { Effect as Effect15 } from "effect";
|
|
3831
4317
|
var WorktreeCleanup = class {
|
|
3832
4318
|
constructor(store2, root, projectsRoot) {
|
|
@@ -3839,12 +4325,12 @@ var WorktreeCleanup = class {
|
|
|
3839
4325
|
projectsRoot;
|
|
3840
4326
|
async remove(session, discardChanges = true) {
|
|
3841
4327
|
if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
|
|
3842
|
-
const workspace =
|
|
4328
|
+
const workspace = path10.resolve(session.workspacePath);
|
|
3843
4329
|
const info = await lstat2(workspace).catch((error) => {
|
|
3844
4330
|
if (error.code === "ENOENT") return void 0;
|
|
3845
4331
|
throw error;
|
|
3846
4332
|
});
|
|
3847
|
-
if (workspace !==
|
|
4333
|
+
if (workspace !== path10.join(this.root, session.id) || path10.basename(session.id) !== session.id) {
|
|
3848
4334
|
if (!info) return;
|
|
3849
4335
|
throw new Error("Refusing to remove a worktree outside the managed workspaces directory.");
|
|
3850
4336
|
}
|
|
@@ -3882,9 +4368,9 @@ var WorktreeCleanup = class {
|
|
|
3882
4368
|
}
|
|
3883
4369
|
};
|
|
3884
4370
|
async function removeRunFiles(dataRoot2, sessionId) {
|
|
3885
|
-
if (
|
|
4371
|
+
if (path10.basename(sessionId) !== sessionId || sessionId === "." || sessionId === "..")
|
|
3886
4372
|
throw new Error("Invalid session directory.");
|
|
3887
|
-
await rm2(
|
|
4373
|
+
await rm2(path10.join(dataRoot2, "pi", sessionId), { recursive: true, force: true });
|
|
3888
4374
|
}
|
|
3889
4375
|
|
|
3890
4376
|
// src/repo-chat.ts
|
|
@@ -3922,9 +4408,9 @@ function openRepoChat(store2, projectId, workspacePath, defaultModel, reset = fa
|
|
|
3922
4408
|
// src/index.ts
|
|
3923
4409
|
var host = process.env.AGENTRIFIC_HOST ?? "127.0.0.1";
|
|
3924
4410
|
var port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
|
|
3925
|
-
var defaultProjectsRoot =
|
|
3926
|
-
var worktreesRoot =
|
|
3927
|
-
process.env.AGENTRIFIC_WORKTREES_DIR ??
|
|
4411
|
+
var defaultProjectsRoot = path11.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
|
|
4412
|
+
var worktreesRoot = path11.resolve(
|
|
4413
|
+
process.env.AGENTRIFIC_WORKTREES_DIR ?? path11.join(homedir3(), ".agentrific", "workspaces")
|
|
3928
4414
|
);
|
|
3929
4415
|
var execFileAsync = promisify3(execFile3);
|
|
3930
4416
|
if (host !== "127.0.0.1" && host !== "::1")
|
|
@@ -3932,7 +4418,7 @@ if (host !== "127.0.0.1" && host !== "::1")
|
|
|
3932
4418
|
var app = Fastify({
|
|
3933
4419
|
logger: {
|
|
3934
4420
|
stream: process.env.AGENTRIFIC_INTERACTIVE === "1" && process.stdout.isTTY ? pretty({
|
|
3935
|
-
colorize:
|
|
4421
|
+
colorize: styleText2("green", "ready") !== "ready",
|
|
3936
4422
|
translateTime: "SYS:HH:MM:ss",
|
|
3937
4423
|
ignore: "pid,hostname,reqId",
|
|
3938
4424
|
singleLine: true,
|
|
@@ -3947,9 +4433,9 @@ var app = Fastify({
|
|
|
3947
4433
|
}
|
|
3948
4434
|
}
|
|
3949
4435
|
});
|
|
3950
|
-
var dataRoot =
|
|
4436
|
+
var dataRoot = path11.resolve(process.env.AGENTRIFIC_DATA_DIR ?? "./data");
|
|
3951
4437
|
var releaseLock = await acquireServerLock(dataRoot);
|
|
3952
|
-
var effects = ManagedRuntime.make(SessionStore.layer(
|
|
4438
|
+
var effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
|
|
3953
4439
|
var store = await effects.runPromise(SessionStore);
|
|
3954
4440
|
var security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
|
|
3955
4441
|
var sandboxPushUrl = process.env.AGENTRIFIC_PUSH_SANDBOX === "1" ? PUSH_SANDBOX_API_URL : void 0;
|
|
@@ -3981,7 +4467,7 @@ await app.register(cors, {
|
|
|
3981
4467
|
await registerSecurity(app, security, serverVersion, allowedOrigins);
|
|
3982
4468
|
registerPushRoutes(app, push, security);
|
|
3983
4469
|
var attachmentEffects = ManagedRuntime.make(
|
|
3984
|
-
Attachments.layer(
|
|
4470
|
+
Attachments.layer(path11.join(dataRoot, "attachments"), store)
|
|
3985
4471
|
);
|
|
3986
4472
|
var attachments = await attachmentEffects.runPromise(Attachments);
|
|
3987
4473
|
await Effect17.runPromise(attachments.cleanup());
|
|
@@ -4065,7 +4551,7 @@ var agentEffects = ManagedRuntime.make(
|
|
|
4065
4551
|
createPiFactory(attachments, models, delivery),
|
|
4066
4552
|
publish,
|
|
4067
4553
|
worktreesRoot,
|
|
4068
|
-
|
|
4554
|
+
path11.join(dataRoot, "pi"),
|
|
4069
4555
|
(sessionId, messageId) => Effect17.runSync(store.hasMessage(sessionId, messageId)),
|
|
4070
4556
|
(session) => delivery.settled(session.id)
|
|
4071
4557
|
)
|
|
@@ -4240,7 +4726,7 @@ app.get("/api/projects/:id", async (request, reply) => {
|
|
|
4240
4726
|
return project ?? reply.code(404).send({ error: "Project not found on this server." });
|
|
4241
4727
|
});
|
|
4242
4728
|
app.post("/api/projects/:id/reconnect", async (request, reply) => {
|
|
4243
|
-
const parsed =
|
|
4729
|
+
const parsed = z19.object({ path: z19.string().trim().min(1).max(4096) }).safeParse(request.body);
|
|
4244
4730
|
if (!parsed.success)
|
|
4245
4731
|
return reply.code(400).send({ error: "Enter the project's new folder path." });
|
|
4246
4732
|
const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
@@ -4326,7 +4812,7 @@ app.patch("/api/tasks/:id", async (request, reply) => {
|
|
|
4326
4812
|
}
|
|
4327
4813
|
});
|
|
4328
4814
|
app.post("/api/tasks/:id/discard-unrun", async (request, reply) => {
|
|
4329
|
-
const parsed =
|
|
4815
|
+
const parsed = z19.object({ updatedAt: z19.string().datetime() }).safeParse(request.body);
|
|
4330
4816
|
if (!parsed.success) return reply.code(400).send({ error: "The task revision is required." });
|
|
4331
4817
|
try {
|
|
4332
4818
|
await deleteTask(store, request.params.id, void 0, parsed.data.updatedAt);
|
|
@@ -4396,9 +4882,9 @@ app.post(
|
|
|
4396
4882
|
}
|
|
4397
4883
|
})
|
|
4398
4884
|
);
|
|
4399
|
-
var HistoryQuery =
|
|
4400
|
-
after:
|
|
4401
|
-
limit:
|
|
4885
|
+
var HistoryQuery = z19.object({
|
|
4886
|
+
after: z19.coerce.number().int().nonnegative().default(0),
|
|
4887
|
+
limit: z19.coerce.number().int().min(1).max(1e3).default(500)
|
|
4402
4888
|
});
|
|
4403
4889
|
app.get("/api/sessions/:id/events", async (request, reply) => {
|
|
4404
4890
|
const query = HistoryQuery.safeParse(request.query);
|
|
@@ -4433,18 +4919,18 @@ async function resolveSessionProject(projectId) {
|
|
|
4433
4919
|
return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
|
|
4434
4920
|
const projectPath = await realpath2(project.path);
|
|
4435
4921
|
const resolvedRoot = await realpath2(projectsRoot);
|
|
4436
|
-
if (
|
|
4922
|
+
if (path11.dirname(projectPath) !== resolvedRoot)
|
|
4437
4923
|
return { error: "Project path is outside projects root.", status: 400 };
|
|
4438
4924
|
return { projectPath };
|
|
4439
4925
|
}
|
|
4440
4926
|
async function resolveSessionWorkspace(id, gitFlow, projectPath) {
|
|
4441
|
-
const worktreePath =
|
|
4927
|
+
const worktreePath = path11.join(worktreesRoot, id);
|
|
4442
4928
|
const branch = gitFlow ? `agentrific/${id.slice(0, 8)}` : null;
|
|
4443
4929
|
const useProjectCheckout = gitFlow?.workspace === "checkout";
|
|
4444
4930
|
const workspacePath = !gitFlow || useProjectCheckout ? projectPath : worktreePath;
|
|
4445
4931
|
if (useProjectCheckout) {
|
|
4446
4932
|
const serverPath = await realpath2(process.cwd());
|
|
4447
|
-
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${
|
|
4933
|
+
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path11.sep}`))
|
|
4448
4934
|
return {
|
|
4449
4935
|
error: "The server runs from this project. Choose a worktree for this task.",
|
|
4450
4936
|
status: 409
|
|
@@ -4536,7 +5022,7 @@ app.post("/api/projects/:id/chat", async (request, reply) => {
|
|
|
4536
5022
|
const location = await resolveSessionProject(request.params.id);
|
|
4537
5023
|
if (location.error !== void 0)
|
|
4538
5024
|
return reply.code(location.status).send({ error: location.error });
|
|
4539
|
-
const parsed =
|
|
5025
|
+
const parsed = z19.object({ reset: z19.boolean().optional() }).safeParse(request.body ?? {});
|
|
4540
5026
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
4541
5027
|
try {
|
|
4542
5028
|
return await openRepoChat(
|
|
@@ -4560,7 +5046,7 @@ app.post("/api/sessions", async (request, reply) => {
|
|
|
4560
5046
|
if (!result.session) return reply.code(result.status).send({ error: result.error });
|
|
4561
5047
|
return reply.code(201).send(result.session);
|
|
4562
5048
|
});
|
|
4563
|
-
var StartTaskBody =
|
|
5049
|
+
var StartTaskBody = z19.object({ prompt: z19.string().trim().max(2e4).optional() }).default({});
|
|
4564
5050
|
function startTask(id, instructionsOverride, automated = false) {
|
|
4565
5051
|
return withTaskLock(
|
|
4566
5052
|
"agent-dispatch",
|
|
@@ -4638,7 +5124,7 @@ app.patch("/api/automation", async (request, reply) => {
|
|
|
4638
5124
|
return automation.configure(parsed.data);
|
|
4639
5125
|
});
|
|
4640
5126
|
app.patch("/api/automation/queue", async (request, reply) => {
|
|
4641
|
-
const parsed =
|
|
5127
|
+
const parsed = z19.object({ taskId: z19.string().min(1), beforeId: z19.string().min(1).nullable() }).safeParse(request.body);
|
|
4642
5128
|
if (!parsed.success)
|
|
4643
5129
|
return reply.code(400).send({ error: "Choose a task and a queue position." });
|
|
4644
5130
|
try {
|
|
@@ -4701,15 +5187,34 @@ attachEventServer(app, security, allowedOrigins, () => ({
|
|
|
4701
5187
|
sessions: Effect17.runSync(store.list())
|
|
4702
5188
|
}));
|
|
4703
5189
|
function localServerUrl() {
|
|
4704
|
-
const address =
|
|
5190
|
+
const address = z19.object({ port: z19.number() }).safeParse(app.server.address());
|
|
4705
5191
|
return address.success ? `http://${host}:${address.data.port}` : void 0;
|
|
4706
5192
|
}
|
|
5193
|
+
var phoneOrigin;
|
|
5194
|
+
var phone = process.env.AGENTRIFIC_DESKTOP_SERVER === "1" ? new DesktopPhoneConnection(
|
|
5195
|
+
() => {
|
|
5196
|
+
const origin = localServerUrl();
|
|
5197
|
+
if (!origin) throw new Error("Start the local server before connecting your phone.");
|
|
5198
|
+
return origin;
|
|
5199
|
+
},
|
|
5200
|
+
(url) => {
|
|
5201
|
+
if (phoneOrigin) {
|
|
5202
|
+
const index = allowedOrigins.indexOf(phoneOrigin);
|
|
5203
|
+
if (index >= 0) allowedOrigins.splice(index, 1);
|
|
5204
|
+
}
|
|
5205
|
+
phoneOrigin = url || void 0;
|
|
5206
|
+
process.env.AGENTRIFIC_PUBLIC_URL = url || "";
|
|
5207
|
+
if (url && !allowedOrigins.includes(url)) allowedOrigins.push(url);
|
|
5208
|
+
},
|
|
5209
|
+
path11.join(dataRoot, "phone-connection")
|
|
5210
|
+
) : void 0;
|
|
4707
5211
|
var closeControl = await startControl(
|
|
4708
5212
|
dataRoot,
|
|
4709
5213
|
security,
|
|
4710
5214
|
() => ({
|
|
4711
5215
|
serverId: security.serverId,
|
|
4712
5216
|
publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
|
|
5217
|
+
phoneProvider: phone?.snapshot().provider,
|
|
4713
5218
|
localUrl: localServerUrl(),
|
|
4714
5219
|
serverVersion,
|
|
4715
5220
|
connectedDevices: new Set(security.sockets.values()).size,
|
|
@@ -4722,10 +5227,12 @@ var closeControl = await startControl(
|
|
|
4722
5227
|
async () => {
|
|
4723
5228
|
await app.close();
|
|
4724
5229
|
process.exit(0);
|
|
4725
|
-
}
|
|
5230
|
+
},
|
|
5231
|
+
phone
|
|
4726
5232
|
);
|
|
4727
5233
|
push.start();
|
|
4728
5234
|
app.addHook("onClose", async () => {
|
|
5235
|
+
await phone?.close();
|
|
4729
5236
|
await push.close();
|
|
4730
5237
|
maintenance.enter();
|
|
4731
5238
|
stopUpdateDiscovery();
|