@agentrific/server 0.5.1 → 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 +14 -0
- package/README.md +3 -2
- package/cli.js +859 -525
- package/cli.js.map +4 -4
- package/index.js +719 -173
- 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
|
|
@@ -956,7 +956,7 @@ function attachEventServer(app2, security2, origins, snapshot) {
|
|
|
956
956
|
}
|
|
957
957
|
|
|
958
958
|
// src/security.ts
|
|
959
|
-
import { createHash, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
959
|
+
import { createHash as createHash2, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
960
960
|
import { Effect as Effect4 } from "effect";
|
|
961
961
|
import { z as z6 } from "zod";
|
|
962
962
|
|
|
@@ -966,10 +966,24 @@ import qrcode from "qrcode-terminal";
|
|
|
966
966
|
|
|
967
967
|
// src/control.ts
|
|
968
968
|
import { createServer, createConnection } from "node:net";
|
|
969
|
-
import { chmod, mkdir as mkdir2, readFile as readFile2, unlink, writeFile } from "node:fs/promises";
|
|
969
|
+
import { chmod, mkdir as mkdir2, lstat, readFile as readFile2, unlink, writeFile } from "node:fs/promises";
|
|
970
970
|
import path3 from "node:path";
|
|
971
971
|
import { Effect as Effect3 } from "effect";
|
|
972
972
|
import { z as z4 } from "zod";
|
|
973
|
+
|
|
974
|
+
// ../../packages/contracts/src/control-socket.ts
|
|
975
|
+
import { createHash } from "node:crypto";
|
|
976
|
+
import { join, resolve } from "node:path";
|
|
977
|
+
function controlSocketPath(directory, uid = process.getuid?.()) {
|
|
978
|
+
const direct = join(resolve(directory), "control.sock");
|
|
979
|
+
if (Buffer.byteLength(direct) < 100) return direct;
|
|
980
|
+
if (uid === void 0) throw new Error("Local control sockets require macOS or Linux.");
|
|
981
|
+
const digest2 = createHash("sha256").update(resolve(directory)).digest("hex").slice(0, 32);
|
|
982
|
+
return join("/tmp", `agentrific-${uid}`, `${digest2}.sock`);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/control.ts
|
|
986
|
+
var ProviderSchema = z4.enum(["opentunnel", "tailscale", "cloudflared"]);
|
|
973
987
|
var MaintenanceSchema = z4.object({
|
|
974
988
|
active: z4.boolean(),
|
|
975
989
|
drained: z4.boolean(),
|
|
@@ -978,6 +992,8 @@ var MaintenanceSchema = z4.object({
|
|
|
978
992
|
var ControlStatusSchema = z4.object({
|
|
979
993
|
serverId: z4.string().uuid(),
|
|
980
994
|
publicUrl: z4.string().optional(),
|
|
995
|
+
phoneProvider: ProviderSchema.nullable().optional(),
|
|
996
|
+
localUrl: z4.string().optional(),
|
|
981
997
|
serverVersion: z4.string().optional(),
|
|
982
998
|
connectedDevices: z4.number().optional(),
|
|
983
999
|
connectedSockets: z4.number().optional(),
|
|
@@ -986,6 +1002,8 @@ var ControlStatusSchema = z4.object({
|
|
|
986
1002
|
maintenance: MaintenanceSchema.optional()
|
|
987
1003
|
});
|
|
988
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() })),
|
|
989
1007
|
z4.object({
|
|
990
1008
|
pairingId: z4.string(),
|
|
991
1009
|
pin: z4.string(),
|
|
@@ -995,12 +1013,17 @@ var ControlResultSchema = z4.union([
|
|
|
995
1013
|
DeviceSchema.array(),
|
|
996
1014
|
ControlStatusSchema,
|
|
997
1015
|
z4.object({ revoked: z4.boolean() }),
|
|
1016
|
+
z4.object({ shutdown: z4.literal(true) }),
|
|
998
1017
|
MaintenanceSchema
|
|
999
1018
|
]);
|
|
1000
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") }),
|
|
1001
1023
|
z4.object({ command: z4.literal("pair") }),
|
|
1002
1024
|
z4.object({ command: z4.literal("devices") }),
|
|
1003
1025
|
z4.object({ command: z4.literal("status") }),
|
|
1026
|
+
z4.object({ command: z4.literal("shutdown") }),
|
|
1004
1027
|
z4.object({ command: z4.literal("revoke"), id: z4.string().uuid() }),
|
|
1005
1028
|
z4.object({
|
|
1006
1029
|
command: z4.literal("maintenance"),
|
|
@@ -1049,8 +1072,14 @@ async function acquireServerLock(directory) {
|
|
|
1049
1072
|
await unlink(filename);
|
|
1050
1073
|
};
|
|
1051
1074
|
}
|
|
1052
|
-
async function startControl(directory, security2, status, maintenance2) {
|
|
1053
|
-
const filename =
|
|
1075
|
+
async function startControl(directory, security2, status, maintenance2, shutdown, phone2) {
|
|
1076
|
+
const filename = controlSocketPath(directory);
|
|
1077
|
+
const socketDirectory = path3.dirname(filename);
|
|
1078
|
+
await mkdir2(socketDirectory, { recursive: true, mode: 448 });
|
|
1079
|
+
const metadata = await lstat(socketDirectory);
|
|
1080
|
+
if (!metadata.isDirectory() || metadata.uid !== process.getuid?.())
|
|
1081
|
+
throw new Error("Local control directory must be private and owned by this user.");
|
|
1082
|
+
await chmod(socketDirectory, 448);
|
|
1054
1083
|
await unlink(filename).catch((error) => {
|
|
1055
1084
|
if (!z4.object({ code: z4.literal("ENOENT") }).safeParse(error).success) throw error;
|
|
1056
1085
|
});
|
|
@@ -1070,6 +1099,36 @@ async function startControl(directory, security2, status, maintenance2) {
|
|
|
1070
1099
|
const request = ControlRequest.parse(JSON.parse(buffer.split("\n")[0]));
|
|
1071
1100
|
let result;
|
|
1072
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;
|
|
1125
|
+
case "shutdown":
|
|
1126
|
+
if (!shutdown || !maintenance2?.status().drained)
|
|
1127
|
+
throw new Error("Drain the server before shutdown");
|
|
1128
|
+
socket.end(JSON.stringify({ result: { shutdown: true } }) + "\n", () => {
|
|
1129
|
+
setImmediate(() => void shutdown());
|
|
1130
|
+
});
|
|
1131
|
+
return;
|
|
1073
1132
|
case "pair":
|
|
1074
1133
|
result = security2.createPairing();
|
|
1075
1134
|
break;
|
|
@@ -1096,22 +1155,25 @@ async function startControl(directory, security2, status, maintenance2) {
|
|
|
1096
1155
|
});
|
|
1097
1156
|
socket.on("error", () => socket.destroy());
|
|
1098
1157
|
});
|
|
1099
|
-
await new Promise((
|
|
1158
|
+
await new Promise((resolve2, reject) => {
|
|
1100
1159
|
server.once("error", reject);
|
|
1101
|
-
server.listen(filename,
|
|
1160
|
+
server.listen(filename, resolve2);
|
|
1102
1161
|
});
|
|
1103
1162
|
await chmod(filename, 384);
|
|
1104
1163
|
return async () => {
|
|
1105
1164
|
await new Promise(
|
|
1106
|
-
(
|
|
1165
|
+
(resolve2, reject) => server.close((error) => error ? reject(error) : resolve2())
|
|
1107
1166
|
);
|
|
1108
1167
|
};
|
|
1109
1168
|
}
|
|
1110
1169
|
|
|
1170
|
+
// src/cli-output.ts
|
|
1171
|
+
import { styleText } from "node:util";
|
|
1172
|
+
|
|
1111
1173
|
// package.json
|
|
1112
1174
|
var package_default = {
|
|
1113
1175
|
name: "@agentrific/server",
|
|
1114
|
-
version: "0.
|
|
1176
|
+
version: "0.6.0",
|
|
1115
1177
|
description: "Self-hosted control plane for long-running coding agents",
|
|
1116
1178
|
keywords: [
|
|
1117
1179
|
"agentrific",
|
|
@@ -1161,6 +1223,7 @@ var package_default = {
|
|
|
1161
1223
|
},
|
|
1162
1224
|
devDependencies: {
|
|
1163
1225
|
"@agentrific/contracts": "workspace:*",
|
|
1226
|
+
"@agentrific/opentunnel-runtime": "workspace:*",
|
|
1164
1227
|
"@jest/globals": "30.5.2",
|
|
1165
1228
|
"@types/node": "^22.19.1",
|
|
1166
1229
|
"@types/qrcode-terminal": "^0.12.2",
|
|
@@ -1175,6 +1238,13 @@ var package_default = {
|
|
|
1175
1238
|
// src/version.ts
|
|
1176
1239
|
var serverVersion = package_default.version;
|
|
1177
1240
|
|
|
1241
|
+
// src/cli-output.ts
|
|
1242
|
+
function printError(message) {
|
|
1243
|
+
console.error(`
|
|
1244
|
+
${styleText(["bold", "red"], "Error")} ${message}
|
|
1245
|
+
`);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1178
1248
|
// src/pairing.ts
|
|
1179
1249
|
function pairingOrigin(url) {
|
|
1180
1250
|
const endpoint = new URL(url);
|
|
@@ -1187,7 +1257,7 @@ function pairingOrigin(url) {
|
|
|
1187
1257
|
|
|
1188
1258
|
// src/security.ts
|
|
1189
1259
|
var secret = () => randomBytes(32).toString("hex");
|
|
1190
|
-
var digest = (value) =>
|
|
1260
|
+
var digest = (value) => createHash2("sha256").update(value).digest("hex");
|
|
1191
1261
|
var ServerSecurity = class {
|
|
1192
1262
|
constructor(store2, serverId) {
|
|
1193
1263
|
this.store = store2;
|
|
@@ -1249,17 +1319,17 @@ var ServerSecurity = class {
|
|
|
1249
1319
|
if (!/^[a-f0-9]{64}$/.test(token)) return void 0;
|
|
1250
1320
|
return Effect4.runSync(this.store.deviceForToken(digest(token)));
|
|
1251
1321
|
}
|
|
1252
|
-
ticket(token,
|
|
1322
|
+
ticket(token, path12) {
|
|
1253
1323
|
this.prune();
|
|
1254
1324
|
if (!this.authenticate(token) || this.tickets.size >= 1e4) return void 0;
|
|
1255
1325
|
const value = secret();
|
|
1256
|
-
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 });
|
|
1257
1327
|
return value;
|
|
1258
1328
|
}
|
|
1259
|
-
redeemTicket(value,
|
|
1329
|
+
redeemTicket(value, path12, consume = true) {
|
|
1260
1330
|
this.prune();
|
|
1261
1331
|
const ticket = this.tickets.get(value);
|
|
1262
|
-
if (!ticket || ticket.path !==
|
|
1332
|
+
if (!ticket || ticket.path !== path12) return void 0;
|
|
1263
1333
|
if (consume) this.tickets.delete(value);
|
|
1264
1334
|
return Effect4.runSync(this.store.deviceForToken(ticket.tokenHash));
|
|
1265
1335
|
}
|
|
@@ -1272,19 +1342,19 @@ var ServerSecurity = class {
|
|
|
1272
1342
|
};
|
|
1273
1343
|
async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
1274
1344
|
app2.addHook("onRequest", async (request, reply) => {
|
|
1275
|
-
const
|
|
1345
|
+
const path12 = new URL(request.url, "http://localhost");
|
|
1276
1346
|
reply.header("Cache-Control", "no-store");
|
|
1277
1347
|
if (request.method === "OPTIONS") return;
|
|
1278
1348
|
if (request.headers.origin && !allowedOrigins2.includes(request.headers.origin))
|
|
1279
1349
|
return reply.code(403).send({ error: "Browser origin is not allowed." });
|
|
1280
|
-
if (
|
|
1350
|
+
if (path12.pathname === "/health" || path12.pathname === "/api/auth/pair" && request.method === "POST")
|
|
1281
1351
|
return;
|
|
1282
1352
|
const authorization = request.headers.authorization ?? "";
|
|
1283
1353
|
const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
|
|
1284
|
-
const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(
|
|
1285
|
-
const ticket =
|
|
1286
|
-
|
|
1287
|
-
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)))
|
|
1288
1358
|
return reply.code(401).send({ error: "Pair this device with your server.", code: "UNAUTHORIZED" });
|
|
1289
1359
|
if (!asset && request.headers["x-agentrific-protocol"] !== String(PROTOCOL_VERSION))
|
|
1290
1360
|
return reply.code(409).send({
|
|
@@ -1349,6 +1419,451 @@ async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
|
1349
1419
|
});
|
|
1350
1420
|
}
|
|
1351
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
|
+
|
|
1352
1867
|
// src/maintenance.ts
|
|
1353
1868
|
var MaintenanceError = class extends Error {
|
|
1354
1869
|
statusCode = 503;
|
|
@@ -1385,14 +1900,14 @@ var Maintenance = class {
|
|
|
1385
1900
|
this.enter();
|
|
1386
1901
|
const deadline = Date.now() + timeoutMs;
|
|
1387
1902
|
while (!this.status().drained && Date.now() < deadline)
|
|
1388
|
-
await new Promise((
|
|
1903
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
1389
1904
|
return this.status();
|
|
1390
1905
|
}
|
|
1391
1906
|
attach(app2) {
|
|
1392
1907
|
app2.addHook("onRequest", async (request, reply) => {
|
|
1393
|
-
const
|
|
1908
|
+
const path12 = request.url.split("?")[0];
|
|
1394
1909
|
if (["GET", "HEAD", "OPTIONS"].includes(request.method)) return;
|
|
1395
|
-
const allowed =
|
|
1910
|
+
const allowed = path12.startsWith("/api/auth/") || /^\/api\/sessions\/[^/]+\/stop$/.test(path12);
|
|
1396
1911
|
if (this.paused && !allowed)
|
|
1397
1912
|
return reply.code(503).send({ code: "MAINTENANCE", error: new MaintenanceError().message });
|
|
1398
1913
|
this.requests.add(request.id);
|
|
@@ -1405,10 +1920,10 @@ var Maintenance = class {
|
|
|
1405
1920
|
|
|
1406
1921
|
// src/updates.ts
|
|
1407
1922
|
import semver from "semver";
|
|
1408
|
-
import { z as
|
|
1923
|
+
import { z as z9 } from "zod";
|
|
1409
1924
|
import { Effect as Effect5 } from "effect";
|
|
1410
|
-
var RegistryRelease =
|
|
1411
|
-
version:
|
|
1925
|
+
var RegistryRelease = z9.object({
|
|
1926
|
+
version: z9.string().refine((value) => semver.valid(value) !== null)
|
|
1412
1927
|
});
|
|
1413
1928
|
async function checkForUpdates() {
|
|
1414
1929
|
const response = await fetch("https://registry.npmjs.org/@agentrific%2Fserver/latest", {
|
|
@@ -1498,7 +2013,7 @@ var UpdateDiscovery = class {
|
|
|
1498
2013
|
|
|
1499
2014
|
// src/index.ts
|
|
1500
2015
|
import { Effect as Effect17, ManagedRuntime } from "effect";
|
|
1501
|
-
import { z as
|
|
2016
|
+
import { z as z19 } from "zod";
|
|
1502
2017
|
|
|
1503
2018
|
// src/automation.ts
|
|
1504
2019
|
import { Effect as Effect7 } from "effect";
|
|
@@ -1733,7 +2248,7 @@ var AutomationCoordinator = class {
|
|
|
1733
2248
|
};
|
|
1734
2249
|
|
|
1735
2250
|
// src/runtime.ts
|
|
1736
|
-
import
|
|
2251
|
+
import path6 from "node:path";
|
|
1737
2252
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
1738
2253
|
|
|
1739
2254
|
// src/git-flow.ts
|
|
@@ -1798,8 +2313,8 @@ var AgentController = class {
|
|
|
1798
2313
|
const agent = yield* Effect8.tryPromise({
|
|
1799
2314
|
try: () => this.factory.open(
|
|
1800
2315
|
session.id,
|
|
1801
|
-
session.workspacePath ??
|
|
1802
|
-
|
|
2316
|
+
session.workspacePath ?? path6.join(this.worktreesRoot, session.id),
|
|
2317
|
+
path6.join(this.sessionsRoot, session.id),
|
|
1803
2318
|
session.model,
|
|
1804
2319
|
session
|
|
1805
2320
|
),
|
|
@@ -2034,7 +2549,7 @@ var AgentSessions = class extends Context2.Service()(
|
|
|
2034
2549
|
|
|
2035
2550
|
// src/delivery.ts
|
|
2036
2551
|
import { Effect as Effect9 } from "effect";
|
|
2037
|
-
import { z as
|
|
2552
|
+
import { z as z11 } from "zod";
|
|
2038
2553
|
|
|
2039
2554
|
// src/workflow-command.ts
|
|
2040
2555
|
import { execFile } from "node:child_process";
|
|
@@ -2082,12 +2597,12 @@ function workflowError(error) {
|
|
|
2082
2597
|
}
|
|
2083
2598
|
|
|
2084
2599
|
// src/workflow-schema.ts
|
|
2085
|
-
import { z as
|
|
2086
|
-
var DeliveryJobSchema =
|
|
2087
|
-
kind:
|
|
2088
|
-
sessionId:
|
|
2600
|
+
import { z as z10 } from "zod";
|
|
2601
|
+
var DeliveryJobSchema = z10.object({
|
|
2602
|
+
kind: z10.literal("delivery"),
|
|
2603
|
+
sessionId: z10.string(),
|
|
2089
2604
|
request: DeliveryRequestSchema,
|
|
2090
|
-
phase:
|
|
2605
|
+
phase: z10.enum([
|
|
2091
2606
|
"validating",
|
|
2092
2607
|
"drafting",
|
|
2093
2608
|
"committing",
|
|
@@ -2097,25 +2612,25 @@ var DeliveryJobSchema = z8.object({
|
|
|
2097
2612
|
"opening_pr",
|
|
2098
2613
|
"delivered"
|
|
2099
2614
|
]),
|
|
2100
|
-
attempt:
|
|
2101
|
-
commitSha:
|
|
2102
|
-
title:
|
|
2103
|
-
body:
|
|
2104
|
-
logs:
|
|
2105
|
-
failed:
|
|
2106
|
-
result:
|
|
2107
|
-
url:
|
|
2108
|
-
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()
|
|
2109
2624
|
});
|
|
2110
|
-
var PreparationJobSchema =
|
|
2111
|
-
kind:
|
|
2112
|
-
sessionId:
|
|
2113
|
-
projectPath:
|
|
2625
|
+
var PreparationJobSchema = z10.object({
|
|
2626
|
+
kind: z10.literal("preparation"),
|
|
2627
|
+
sessionId: z10.string(),
|
|
2628
|
+
projectPath: z10.string(),
|
|
2114
2629
|
input: CreateSessionSchema,
|
|
2115
|
-
stage:
|
|
2116
|
-
logs:
|
|
2630
|
+
stage: z10.enum(["creating", "installing", "ready", "failed"]),
|
|
2631
|
+
logs: z10.string().default("")
|
|
2117
2632
|
});
|
|
2118
|
-
var WorkflowJobSchema =
|
|
2633
|
+
var WorkflowJobSchema = z10.discriminatedUnion("kind", [
|
|
2119
2634
|
DeliveryJobSchema,
|
|
2120
2635
|
PreparationJobSchema
|
|
2121
2636
|
]);
|
|
@@ -2411,11 +2926,11 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2411
2926
|
"--json",
|
|
2412
2927
|
"url,state,headRefOid"
|
|
2413
2928
|
]);
|
|
2414
|
-
const prs =
|
|
2415
|
-
|
|
2416
|
-
url:
|
|
2417
|
-
state:
|
|
2418
|
-
headRefOid:
|
|
2929
|
+
const prs = z11.array(
|
|
2930
|
+
z11.object({
|
|
2931
|
+
url: z11.string().url(),
|
|
2932
|
+
state: z11.string(),
|
|
2933
|
+
headRefOid: z11.string().optional()
|
|
2419
2934
|
})
|
|
2420
2935
|
).parse(JSON.parse(existing));
|
|
2421
2936
|
const open = prs.find(
|
|
@@ -2456,10 +2971,10 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2456
2971
|
};
|
|
2457
2972
|
|
|
2458
2973
|
// src/preparation.ts
|
|
2459
|
-
import { mkdir as
|
|
2460
|
-
import
|
|
2974
|
+
import { mkdir as mkdir4, readFile as readFile4 } from "node:fs/promises";
|
|
2975
|
+
import path7 from "node:path";
|
|
2461
2976
|
import { Effect as Effect10 } from "effect";
|
|
2462
|
-
import { z as
|
|
2977
|
+
import { z as z12 } from "zod";
|
|
2463
2978
|
var PreparationCoordinator = class {
|
|
2464
2979
|
constructor(store2, options) {
|
|
2465
2980
|
this.store = store2;
|
|
@@ -2553,7 +3068,7 @@ ${logs}` : ""}`);
|
|
|
2553
3068
|
const workspace = session.workspacePath;
|
|
2554
3069
|
if (branchAtWorkspace !== void 0)
|
|
2555
3070
|
throw new Error("The run workspace already exists on a different branch.");
|
|
2556
|
-
await
|
|
3071
|
+
await mkdir4(path7.dirname(workspace), { recursive: true });
|
|
2557
3072
|
try {
|
|
2558
3073
|
await command(
|
|
2559
3074
|
projectPath,
|
|
@@ -2573,12 +3088,12 @@ ${logs}` : ""}`);
|
|
|
2573
3088
|
}
|
|
2574
3089
|
}
|
|
2575
3090
|
async install(workspace, command) {
|
|
2576
|
-
const packageFile = await
|
|
3091
|
+
const packageFile = await readFile4(path7.join(workspace, "package.json"), "utf8").catch(
|
|
2577
3092
|
() => void 0
|
|
2578
3093
|
);
|
|
2579
3094
|
if (!packageFile) return;
|
|
2580
|
-
const metadata =
|
|
2581
|
-
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(
|
|
2582
3097
|
() => true,
|
|
2583
3098
|
() => false
|
|
2584
3099
|
);
|
|
@@ -2659,8 +3174,8 @@ ${logs}` : ""}`);
|
|
|
2659
3174
|
|
|
2660
3175
|
// src/delivery-writer.ts
|
|
2661
3176
|
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2662
|
-
import { z as
|
|
2663
|
-
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();
|
|
2664
3179
|
function deliveryWriter(models2, createSession = createAgentSession) {
|
|
2665
3180
|
return async (run, input, diff, signal) => {
|
|
2666
3181
|
signal?.throwIfAborted();
|
|
@@ -2815,7 +3330,7 @@ import {
|
|
|
2815
3330
|
createAgentSession as createAgentSession2,
|
|
2816
3331
|
SessionManager as SessionManager2
|
|
2817
3332
|
} from "@earendil-works/pi-coding-agent";
|
|
2818
|
-
import { z as
|
|
3333
|
+
import { z as z15 } from "zod";
|
|
2819
3334
|
|
|
2820
3335
|
// src/delivery-tool.ts
|
|
2821
3336
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
@@ -2852,23 +3367,23 @@ function deliveryTool(sessionId, delivery2) {
|
|
|
2852
3367
|
// src/file-change.ts
|
|
2853
3368
|
import { createWriteToolDefinition, defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
|
|
2854
3369
|
import { createTwoFilesPatch } from "diff";
|
|
2855
|
-
import { mkdir as
|
|
2856
|
-
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";
|
|
2857
3372
|
var previewLimit = 256e3;
|
|
2858
3373
|
function boundFileChange(change) {
|
|
2859
3374
|
if ((change.patch?.length ?? 0) + (change.content?.length ?? 0) > previewLimit)
|
|
2860
3375
|
return { path: change.path, omitted: true };
|
|
2861
3376
|
return change;
|
|
2862
3377
|
}
|
|
2863
|
-
var EditResultSchema =
|
|
2864
|
-
details:
|
|
3378
|
+
var EditResultSchema = z14.object({
|
|
3379
|
+
details: z14.object({ patch: z14.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
|
|
2865
3380
|
});
|
|
2866
|
-
function completedFileChange(result,
|
|
3381
|
+
function completedFileChange(result, path12) {
|
|
2867
3382
|
const parsed = EditResultSchema.safeParse(result);
|
|
2868
3383
|
if (!parsed.success) return;
|
|
2869
3384
|
const details = parsed.data.details;
|
|
2870
3385
|
if (details?.fileChange) return boundFileChange(details.fileChange);
|
|
2871
|
-
if (
|
|
3386
|
+
if (path12 && details?.patch) return boundFileChange({ path: path12, patch: details.patch });
|
|
2872
3387
|
}
|
|
2873
3388
|
function fileWriteTool(cwd) {
|
|
2874
3389
|
const base = createWriteToolDefinition(cwd);
|
|
@@ -2885,16 +3400,16 @@ function fileWriteTool(cwd) {
|
|
|
2885
3400
|
const tool = createWriteToolDefinition(cwd, {
|
|
2886
3401
|
operations: {
|
|
2887
3402
|
mkdir: async (dir) => {
|
|
2888
|
-
await
|
|
3403
|
+
await mkdir5(dir, { recursive: true });
|
|
2889
3404
|
},
|
|
2890
3405
|
// Pi invokes this inside its per-file mutation queue, so the snapshot
|
|
2891
3406
|
// and write cannot race another built-in edit/write on the same file.
|
|
2892
|
-
writeFile: async (
|
|
2893
|
-
const previous = await
|
|
3407
|
+
writeFile: async (path12, content) => {
|
|
3408
|
+
const previous = await readFile5(path12, "utf8").catch((error) => {
|
|
2894
3409
|
if (error.code === "ENOENT") return void 0;
|
|
2895
3410
|
throw error;
|
|
2896
3411
|
});
|
|
2897
|
-
await
|
|
3412
|
+
await writeFile3(path12, content, "utf8");
|
|
2898
3413
|
fileChange = boundFileChange(
|
|
2899
3414
|
previous === void 0 ? { path: args.path, content } : {
|
|
2900
3415
|
path: args.path,
|
|
@@ -2911,8 +3426,8 @@ function fileWriteTool(cwd) {
|
|
|
2911
3426
|
}
|
|
2912
3427
|
|
|
2913
3428
|
// src/pi.ts
|
|
2914
|
-
var ToolResultSchema =
|
|
2915
|
-
content:
|
|
3429
|
+
var ToolResultSchema = z15.object({
|
|
3430
|
+
content: z15.array(z15.object({ type: z15.string(), text: z15.string().optional() })).optional()
|
|
2916
3431
|
}).passthrough();
|
|
2917
3432
|
var boundedJson = (value, limit) => {
|
|
2918
3433
|
let text;
|
|
@@ -3036,7 +3551,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
3036
3551
|
return false;
|
|
3037
3552
|
}
|
|
3038
3553
|
const filePaths = /* @__PURE__ */ new Map();
|
|
3039
|
-
const FileArgsSchema =
|
|
3554
|
+
const FileArgsSchema = z15.object({ path: z15.string() });
|
|
3040
3555
|
function handleTool(event) {
|
|
3041
3556
|
if (event.type === "tool_execution_start") {
|
|
3042
3557
|
flushText();
|
|
@@ -3096,24 +3611,24 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
3096
3611
|
}
|
|
3097
3612
|
|
|
3098
3613
|
// src/projects.ts
|
|
3099
|
-
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";
|
|
3100
3615
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
3101
|
-
import { z as
|
|
3102
|
-
import { constants } from "node:fs";
|
|
3103
|
-
import { homedir } from "node:os";
|
|
3104
|
-
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";
|
|
3105
3620
|
import { Effect as Effect13, Semaphore as Semaphore3 } from "effect";
|
|
3106
3621
|
function resolveProjectsRoot(input) {
|
|
3107
3622
|
return Effect13.tryPromise({
|
|
3108
3623
|
try: async () => {
|
|
3109
3624
|
const trimmed = input.trim();
|
|
3110
|
-
const expanded = trimmed === "~" ?
|
|
3111
|
-
if (!
|
|
3625
|
+
const expanded = trimmed === "~" ? homedir2() : trimmed.startsWith("~/") ? path8.join(homedir2(), trimmed.slice(2)) : trimmed;
|
|
3626
|
+
if (!path8.isAbsolute(expanded))
|
|
3112
3627
|
throw new Error("Use an absolute server path or a path beginning with ~/.");
|
|
3113
3628
|
const resolved = await realpath(expanded);
|
|
3114
|
-
if (!(await
|
|
3629
|
+
if (!(await stat3(resolved)).isDirectory())
|
|
3115
3630
|
throw new Error("The projects folder must be a directory.");
|
|
3116
|
-
await
|
|
3631
|
+
await access2(resolved, constants2.R_OK | constants2.X_OK);
|
|
3117
3632
|
return resolved;
|
|
3118
3633
|
},
|
|
3119
3634
|
catch: () => new Error(
|
|
@@ -3130,36 +3645,36 @@ function projectLock(store2) {
|
|
|
3130
3645
|
}
|
|
3131
3646
|
return lock;
|
|
3132
3647
|
}
|
|
3133
|
-
var ProjectId =
|
|
3648
|
+
var ProjectId = z16.string().trim().min(1).max(255);
|
|
3134
3649
|
async function gitDirectory(directory) {
|
|
3135
|
-
const location =
|
|
3136
|
-
const info = await
|
|
3650
|
+
const location = path8.join(directory, ".git");
|
|
3651
|
+
const info = await stat3(location);
|
|
3137
3652
|
if (info.isDirectory()) return location;
|
|
3138
|
-
const contents = await
|
|
3653
|
+
const contents = await readFile6(location, "utf8");
|
|
3139
3654
|
const match = /^gitdir: (.+)\s*$/m.exec(contents);
|
|
3140
3655
|
if (!match) throw new Error("Invalid Git worktree metadata.");
|
|
3141
|
-
const resolved = await realpath(
|
|
3142
|
-
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.");
|
|
3143
3658
|
return resolved;
|
|
3144
3659
|
}
|
|
3145
3660
|
async function identity(directory, preferred) {
|
|
3146
|
-
const filename =
|
|
3661
|
+
const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
|
|
3147
3662
|
try {
|
|
3148
|
-
return ProjectId.parse(await
|
|
3663
|
+
return ProjectId.parse(await readFile6(filename, "utf8"));
|
|
3149
3664
|
} catch (error) {
|
|
3150
|
-
if (!
|
|
3151
|
-
const legacyFilename =
|
|
3152
|
-
const legacy = await
|
|
3153
|
-
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;
|
|
3154
3669
|
return null;
|
|
3155
3670
|
});
|
|
3156
3671
|
const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
|
|
3157
3672
|
try {
|
|
3158
|
-
await
|
|
3673
|
+
await writeFile4(filename, id + "\n", { flag: "wx" });
|
|
3159
3674
|
return id;
|
|
3160
3675
|
} catch (writeError) {
|
|
3161
|
-
if (!
|
|
3162
|
-
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"));
|
|
3163
3678
|
}
|
|
3164
3679
|
}
|
|
3165
3680
|
}
|
|
@@ -3172,15 +3687,15 @@ function scanProjects(root, store2) {
|
|
|
3172
3687
|
const entries = await readdir2(root, { withFileTypes: true });
|
|
3173
3688
|
for (const entry of entries) {
|
|
3174
3689
|
if (!entry.isDirectory()) continue;
|
|
3175
|
-
const directory =
|
|
3176
|
-
if (!await
|
|
3690
|
+
const directory = path8.join(root, entry.name);
|
|
3691
|
+
if (!await stat3(path8.join(directory, ".git")).catch(() => null)) continue;
|
|
3177
3692
|
const previous = registered.find((project2) => project2.path === directory);
|
|
3178
3693
|
let id = await identity(directory, previous?.available ? void 0 : previous?.id);
|
|
3179
3694
|
let existing = registered.find((project2) => project2.id === id);
|
|
3180
|
-
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)) {
|
|
3181
3696
|
id = randomUUID6();
|
|
3182
|
-
await
|
|
3183
|
-
|
|
3697
|
+
await writeFile4(
|
|
3698
|
+
path8.join(await gitDirectory(directory), "agentrific-project-id"),
|
|
3184
3699
|
id + "\n"
|
|
3185
3700
|
);
|
|
3186
3701
|
existing = void 0;
|
|
@@ -3213,16 +3728,16 @@ function reconnectProject(root, id, input, store2) {
|
|
|
3213
3728
|
))
|
|
3214
3729
|
throw new Error("Stop this project's running sessions before reconnecting its folder.");
|
|
3215
3730
|
const directory = await Effect13.runPromise(resolveProjectsRoot(input));
|
|
3216
|
-
if (
|
|
3731
|
+
if (path8.dirname(directory) !== await realpath(root))
|
|
3217
3732
|
throw new Error("Choose a Git repository directly inside the configured projects folder.");
|
|
3218
|
-
const filename =
|
|
3733
|
+
const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
|
|
3219
3734
|
const replacedId = await identity(directory);
|
|
3220
|
-
const project = { id, name:
|
|
3221
|
-
await
|
|
3735
|
+
const project = { id, name: path8.basename(directory), path: directory, available: true };
|
|
3736
|
+
await writeFile4(filename, id + "\n");
|
|
3222
3737
|
try {
|
|
3223
3738
|
await Effect13.runPromise(store2.relocateProject(project, previous.path, replacedId));
|
|
3224
3739
|
} catch (error) {
|
|
3225
|
-
await
|
|
3740
|
+
await writeFile4(filename, replacedId + "\n");
|
|
3226
3741
|
throw error;
|
|
3227
3742
|
}
|
|
3228
3743
|
return project;
|
|
@@ -3234,7 +3749,7 @@ function reconnectProject(root, id, input, store2) {
|
|
|
3234
3749
|
// src/project-git.ts
|
|
3235
3750
|
import { execFile as execFile2 } from "node:child_process";
|
|
3236
3751
|
import { promisify as promisify2 } from "node:util";
|
|
3237
|
-
var
|
|
3752
|
+
var execute2 = promisify2(execFile2);
|
|
3238
3753
|
function repositoryIdentity(remote) {
|
|
3239
3754
|
const input = remote.trim();
|
|
3240
3755
|
if (!input || input.startsWith("/") || input.startsWith(".")) return null;
|
|
@@ -3248,14 +3763,14 @@ function repositoryIdentity(remote) {
|
|
|
3248
3763
|
if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return null;
|
|
3249
3764
|
if (url.search || url.hash || url.port) return null;
|
|
3250
3765
|
const host2 = url.hostname.toLowerCase();
|
|
3251
|
-
let
|
|
3252
|
-
if (!host2 || !
|
|
3253
|
-
if (["github.com", "bitbucket.org"].includes(host2))
|
|
3254
|
-
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}`;
|
|
3255
3770
|
return { key, display: key };
|
|
3256
3771
|
}
|
|
3257
3772
|
async function git(directory, args) {
|
|
3258
|
-
const { stdout } = await
|
|
3773
|
+
const { stdout } = await execute2("git", ["-C", directory, ...args], {
|
|
3259
3774
|
timeout: 3e3,
|
|
3260
3775
|
maxBuffer: 1024 * 1024,
|
|
3261
3776
|
env: { ...workflowEnvironment(), GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
|
|
@@ -3281,7 +3796,7 @@ async function inspect(directory) {
|
|
|
3281
3796
|
git(directory, ["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => null),
|
|
3282
3797
|
// NUL-separated records avoid ambiguous filenames/newlines. --no-renames
|
|
3283
3798
|
// keeps each entry one record, so we only need its two status characters.
|
|
3284
|
-
|
|
3799
|
+
execute2(
|
|
3285
3800
|
"git",
|
|
3286
3801
|
[
|
|
3287
3802
|
"-C",
|
|
@@ -3333,35 +3848,35 @@ function projectGitStatus(directory) {
|
|
|
3333
3848
|
|
|
3334
3849
|
// src/store.ts
|
|
3335
3850
|
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
3336
|
-
import
|
|
3851
|
+
import path9 from "node:path";
|
|
3337
3852
|
|
|
3338
3853
|
// src/push-schema.ts
|
|
3339
|
-
import { z as
|
|
3340
|
-
var PushBindingSchema =
|
|
3341
|
-
bindingId:
|
|
3342
|
-
installationId:
|
|
3343
|
-
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()
|
|
3344
3859
|
});
|
|
3345
|
-
var PushStateSchema =
|
|
3346
|
-
bindings:
|
|
3347
|
-
error:
|
|
3860
|
+
var PushStateSchema = z17.object({
|
|
3861
|
+
bindings: z17.array(PushBindingSchema).max(10),
|
|
3862
|
+
error: z17.string().nullable()
|
|
3348
3863
|
});
|
|
3349
3864
|
var PendingPushSchema = PushEventSchema.extend({
|
|
3350
|
-
remaining:
|
|
3351
|
-
createdAt:
|
|
3352
|
-
attempts:
|
|
3353
|
-
nextAt:
|
|
3865
|
+
remaining: z17.array(PushBindingSchema),
|
|
3866
|
+
createdAt: z17.number(),
|
|
3867
|
+
attempts: z17.number().int().nonnegative(),
|
|
3868
|
+
nextAt: z17.number()
|
|
3354
3869
|
});
|
|
3355
3870
|
|
|
3356
3871
|
// src/store.ts
|
|
3357
3872
|
import { DatabaseSync } from "node:sqlite";
|
|
3358
3873
|
import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
|
|
3359
|
-
import { z as
|
|
3874
|
+
import { z as z18 } from "zod";
|
|
3360
3875
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
3361
3876
|
var StorageError = class extends Data3.TaggedError("StorageError") {
|
|
3362
3877
|
};
|
|
3363
|
-
var PayloadRow =
|
|
3364
|
-
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() });
|
|
3365
3880
|
var SqliteSessionStore = class {
|
|
3366
3881
|
constructor(db) {
|
|
3367
3882
|
this.db = db;
|
|
@@ -3412,7 +3927,7 @@ var SqliteSessionStore = class {
|
|
|
3412
3927
|
const row = PayloadRow.parse(
|
|
3413
3928
|
this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
|
|
3414
3929
|
);
|
|
3415
|
-
return
|
|
3930
|
+
return z18.string().uuid().parse(JSON.parse(row.payload));
|
|
3416
3931
|
});
|
|
3417
3932
|
}
|
|
3418
3933
|
saveDevice(device, tokenHash) {
|
|
@@ -3500,10 +4015,10 @@ var SqliteSessionStore = class {
|
|
|
3500
4015
|
`).all();
|
|
3501
4016
|
const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
|
|
3502
4017
|
for (const row of legacy) {
|
|
3503
|
-
const id =
|
|
4018
|
+
const id = z18.object({ id: z18.string() }).parse(row).id;
|
|
3504
4019
|
insert.run(
|
|
3505
4020
|
id,
|
|
3506
|
-
JSON.stringify({ id, name: id, path:
|
|
4021
|
+
JSON.stringify({ id, name: id, path: path9.join(root, id), available: false })
|
|
3507
4022
|
);
|
|
3508
4023
|
}
|
|
3509
4024
|
return this.db.prepare("SELECT payload FROM projects").all().map((row) => ProjectSchema.parse(JSON.parse(PayloadRow.parse(row).payload)));
|
|
@@ -3540,7 +4055,7 @@ var SqliteSessionStore = class {
|
|
|
3540
4055
|
this.db.prepare(
|
|
3541
4056
|
"UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
|
|
3542
4057
|
).run(project.path, previousPath);
|
|
3543
|
-
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;
|
|
3544
4059
|
for (const row of this.db.prepare("SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?").all(project.id)) {
|
|
3545
4060
|
const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
|
|
3546
4061
|
if (session.workspacePath) {
|
|
@@ -3710,7 +4225,7 @@ var SqliteSessionStore = class {
|
|
|
3710
4225
|
if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
|
|
3711
4226
|
const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
|
|
3712
4227
|
if (notification) {
|
|
3713
|
-
const count =
|
|
4228
|
+
const count = z18.object({ total: z18.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
|
|
3714
4229
|
if (count.total >= 1e3)
|
|
3715
4230
|
this.db.prepare(
|
|
3716
4231
|
"DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
|
|
@@ -3756,13 +4271,13 @@ var SessionStore = class extends Context3.Service()(
|
|
|
3756
4271
|
Effect14.acquireRelease(
|
|
3757
4272
|
Effect14.try({
|
|
3758
4273
|
try: () => {
|
|
3759
|
-
mkdirSync2(
|
|
4274
|
+
mkdirSync2(path9.dirname(filename), { recursive: true });
|
|
3760
4275
|
const db = new DatabaseSync(filename);
|
|
3761
4276
|
chmodSync(filename, 384);
|
|
3762
4277
|
for (const suffix of ["-wal", "-shm"])
|
|
3763
4278
|
if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
|
|
3764
4279
|
try {
|
|
3765
|
-
const schema =
|
|
4280
|
+
const schema = z18.object({ user_version: z18.number() }).parse(db.prepare("PRAGMA user_version").get());
|
|
3766
4281
|
if (schema.user_version > 1)
|
|
3767
4282
|
throw new Error("This database requires a newer Agentrific server.");
|
|
3768
4283
|
db.exec(`
|
|
@@ -3796,8 +4311,8 @@ var SessionStore = class extends Context3.Service()(
|
|
|
3796
4311
|
};
|
|
3797
4312
|
|
|
3798
4313
|
// src/worktree-cleanup.ts
|
|
3799
|
-
import { lstat, rm as rm2 } from "node:fs/promises";
|
|
3800
|
-
import
|
|
4314
|
+
import { lstat as lstat2, rm as rm2 } from "node:fs/promises";
|
|
4315
|
+
import path10 from "node:path";
|
|
3801
4316
|
import { Effect as Effect15 } from "effect";
|
|
3802
4317
|
var WorktreeCleanup = class {
|
|
3803
4318
|
constructor(store2, root, projectsRoot) {
|
|
@@ -3810,12 +4325,12 @@ var WorktreeCleanup = class {
|
|
|
3810
4325
|
projectsRoot;
|
|
3811
4326
|
async remove(session, discardChanges = true) {
|
|
3812
4327
|
if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
|
|
3813
|
-
const workspace =
|
|
3814
|
-
const info = await
|
|
4328
|
+
const workspace = path10.resolve(session.workspacePath);
|
|
4329
|
+
const info = await lstat2(workspace).catch((error) => {
|
|
3815
4330
|
if (error.code === "ENOENT") return void 0;
|
|
3816
4331
|
throw error;
|
|
3817
4332
|
});
|
|
3818
|
-
if (workspace !==
|
|
4333
|
+
if (workspace !== path10.join(this.root, session.id) || path10.basename(session.id) !== session.id) {
|
|
3819
4334
|
if (!info) return;
|
|
3820
4335
|
throw new Error("Refusing to remove a worktree outside the managed workspaces directory.");
|
|
3821
4336
|
}
|
|
@@ -3853,9 +4368,9 @@ var WorktreeCleanup = class {
|
|
|
3853
4368
|
}
|
|
3854
4369
|
};
|
|
3855
4370
|
async function removeRunFiles(dataRoot2, sessionId) {
|
|
3856
|
-
if (
|
|
4371
|
+
if (path10.basename(sessionId) !== sessionId || sessionId === "." || sessionId === "..")
|
|
3857
4372
|
throw new Error("Invalid session directory.");
|
|
3858
|
-
await rm2(
|
|
4373
|
+
await rm2(path10.join(dataRoot2, "pi", sessionId), { recursive: true, force: true });
|
|
3859
4374
|
}
|
|
3860
4375
|
|
|
3861
4376
|
// src/repo-chat.ts
|
|
@@ -3893,9 +4408,9 @@ function openRepoChat(store2, projectId, workspacePath, defaultModel, reset = fa
|
|
|
3893
4408
|
// src/index.ts
|
|
3894
4409
|
var host = process.env.AGENTRIFIC_HOST ?? "127.0.0.1";
|
|
3895
4410
|
var port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
|
|
3896
|
-
var defaultProjectsRoot =
|
|
3897
|
-
var worktreesRoot =
|
|
3898
|
-
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")
|
|
3899
4414
|
);
|
|
3900
4415
|
var execFileAsync = promisify3(execFile3);
|
|
3901
4416
|
if (host !== "127.0.0.1" && host !== "::1")
|
|
@@ -3903,7 +4418,7 @@ if (host !== "127.0.0.1" && host !== "::1")
|
|
|
3903
4418
|
var app = Fastify({
|
|
3904
4419
|
logger: {
|
|
3905
4420
|
stream: process.env.AGENTRIFIC_INTERACTIVE === "1" && process.stdout.isTTY ? pretty({
|
|
3906
|
-
colorize:
|
|
4421
|
+
colorize: styleText2("green", "ready") !== "ready",
|
|
3907
4422
|
translateTime: "SYS:HH:MM:ss",
|
|
3908
4423
|
ignore: "pid,hostname,reqId",
|
|
3909
4424
|
singleLine: true,
|
|
@@ -3918,9 +4433,9 @@ var app = Fastify({
|
|
|
3918
4433
|
}
|
|
3919
4434
|
}
|
|
3920
4435
|
});
|
|
3921
|
-
var dataRoot =
|
|
4436
|
+
var dataRoot = path11.resolve(process.env.AGENTRIFIC_DATA_DIR ?? "./data");
|
|
3922
4437
|
var releaseLock = await acquireServerLock(dataRoot);
|
|
3923
|
-
var effects = ManagedRuntime.make(SessionStore.layer(
|
|
4438
|
+
var effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
|
|
3924
4439
|
var store = await effects.runPromise(SessionStore);
|
|
3925
4440
|
var security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
|
|
3926
4441
|
var sandboxPushUrl = process.env.AGENTRIFIC_PUSH_SANDBOX === "1" ? PUSH_SANDBOX_API_URL : void 0;
|
|
@@ -3939,6 +4454,7 @@ var push = new ServerPush(
|
|
|
3939
4454
|
var allowedOrigins = [
|
|
3940
4455
|
.../* @__PURE__ */ new Set([
|
|
3941
4456
|
"agentrific-desktop://app",
|
|
4457
|
+
"https://app.agentrific.com",
|
|
3942
4458
|
...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
|
|
3943
4459
|
`http://${host === "::1" ? "[::1]" : host}:${port}`,
|
|
3944
4460
|
...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
|
|
@@ -3951,7 +4467,7 @@ await app.register(cors, {
|
|
|
3951
4467
|
await registerSecurity(app, security, serverVersion, allowedOrigins);
|
|
3952
4468
|
registerPushRoutes(app, push, security);
|
|
3953
4469
|
var attachmentEffects = ManagedRuntime.make(
|
|
3954
|
-
Attachments.layer(
|
|
4470
|
+
Attachments.layer(path11.join(dataRoot, "attachments"), store)
|
|
3955
4471
|
);
|
|
3956
4472
|
var attachments = await attachmentEffects.runPromise(Attachments);
|
|
3957
4473
|
await Effect17.runPromise(attachments.cleanup());
|
|
@@ -4035,7 +4551,7 @@ var agentEffects = ManagedRuntime.make(
|
|
|
4035
4551
|
createPiFactory(attachments, models, delivery),
|
|
4036
4552
|
publish,
|
|
4037
4553
|
worktreesRoot,
|
|
4038
|
-
|
|
4554
|
+
path11.join(dataRoot, "pi"),
|
|
4039
4555
|
(sessionId, messageId) => Effect17.runSync(store.hasMessage(sessionId, messageId)),
|
|
4040
4556
|
(session) => delivery.settled(session.id)
|
|
4041
4557
|
)
|
|
@@ -4210,7 +4726,7 @@ app.get("/api/projects/:id", async (request, reply) => {
|
|
|
4210
4726
|
return project ?? reply.code(404).send({ error: "Project not found on this server." });
|
|
4211
4727
|
});
|
|
4212
4728
|
app.post("/api/projects/:id/reconnect", async (request, reply) => {
|
|
4213
|
-
const parsed =
|
|
4729
|
+
const parsed = z19.object({ path: z19.string().trim().min(1).max(4096) }).safeParse(request.body);
|
|
4214
4730
|
if (!parsed.success)
|
|
4215
4731
|
return reply.code(400).send({ error: "Enter the project's new folder path." });
|
|
4216
4732
|
const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
@@ -4296,7 +4812,7 @@ app.patch("/api/tasks/:id", async (request, reply) => {
|
|
|
4296
4812
|
}
|
|
4297
4813
|
});
|
|
4298
4814
|
app.post("/api/tasks/:id/discard-unrun", async (request, reply) => {
|
|
4299
|
-
const parsed =
|
|
4815
|
+
const parsed = z19.object({ updatedAt: z19.string().datetime() }).safeParse(request.body);
|
|
4300
4816
|
if (!parsed.success) return reply.code(400).send({ error: "The task revision is required." });
|
|
4301
4817
|
try {
|
|
4302
4818
|
await deleteTask(store, request.params.id, void 0, parsed.data.updatedAt);
|
|
@@ -4366,9 +4882,9 @@ app.post(
|
|
|
4366
4882
|
}
|
|
4367
4883
|
})
|
|
4368
4884
|
);
|
|
4369
|
-
var HistoryQuery =
|
|
4370
|
-
after:
|
|
4371
|
-
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)
|
|
4372
4888
|
});
|
|
4373
4889
|
app.get("/api/sessions/:id/events", async (request, reply) => {
|
|
4374
4890
|
const query = HistoryQuery.safeParse(request.query);
|
|
@@ -4403,18 +4919,18 @@ async function resolveSessionProject(projectId) {
|
|
|
4403
4919
|
return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
|
|
4404
4920
|
const projectPath = await realpath2(project.path);
|
|
4405
4921
|
const resolvedRoot = await realpath2(projectsRoot);
|
|
4406
|
-
if (
|
|
4922
|
+
if (path11.dirname(projectPath) !== resolvedRoot)
|
|
4407
4923
|
return { error: "Project path is outside projects root.", status: 400 };
|
|
4408
4924
|
return { projectPath };
|
|
4409
4925
|
}
|
|
4410
4926
|
async function resolveSessionWorkspace(id, gitFlow, projectPath) {
|
|
4411
|
-
const worktreePath =
|
|
4927
|
+
const worktreePath = path11.join(worktreesRoot, id);
|
|
4412
4928
|
const branch = gitFlow ? `agentrific/${id.slice(0, 8)}` : null;
|
|
4413
4929
|
const useProjectCheckout = gitFlow?.workspace === "checkout";
|
|
4414
4930
|
const workspacePath = !gitFlow || useProjectCheckout ? projectPath : worktreePath;
|
|
4415
4931
|
if (useProjectCheckout) {
|
|
4416
4932
|
const serverPath = await realpath2(process.cwd());
|
|
4417
|
-
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${
|
|
4933
|
+
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path11.sep}`))
|
|
4418
4934
|
return {
|
|
4419
4935
|
error: "The server runs from this project. Choose a worktree for this task.",
|
|
4420
4936
|
status: 409
|
|
@@ -4506,7 +5022,7 @@ app.post("/api/projects/:id/chat", async (request, reply) => {
|
|
|
4506
5022
|
const location = await resolveSessionProject(request.params.id);
|
|
4507
5023
|
if (location.error !== void 0)
|
|
4508
5024
|
return reply.code(location.status).send({ error: location.error });
|
|
4509
|
-
const parsed =
|
|
5025
|
+
const parsed = z19.object({ reset: z19.boolean().optional() }).safeParse(request.body ?? {});
|
|
4510
5026
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
4511
5027
|
try {
|
|
4512
5028
|
return await openRepoChat(
|
|
@@ -4530,7 +5046,7 @@ app.post("/api/sessions", async (request, reply) => {
|
|
|
4530
5046
|
if (!result.session) return reply.code(result.status).send({ error: result.error });
|
|
4531
5047
|
return reply.code(201).send(result.session);
|
|
4532
5048
|
});
|
|
4533
|
-
var StartTaskBody =
|
|
5049
|
+
var StartTaskBody = z19.object({ prompt: z19.string().trim().max(2e4).optional() }).default({});
|
|
4534
5050
|
function startTask(id, instructionsOverride, automated = false) {
|
|
4535
5051
|
return withTaskLock(
|
|
4536
5052
|
"agent-dispatch",
|
|
@@ -4608,7 +5124,7 @@ app.patch("/api/automation", async (request, reply) => {
|
|
|
4608
5124
|
return automation.configure(parsed.data);
|
|
4609
5125
|
});
|
|
4610
5126
|
app.patch("/api/automation/queue", async (request, reply) => {
|
|
4611
|
-
const parsed =
|
|
5127
|
+
const parsed = z19.object({ taskId: z19.string().min(1), beforeId: z19.string().min(1).nullable() }).safeParse(request.body);
|
|
4612
5128
|
if (!parsed.success)
|
|
4613
5129
|
return reply.code(400).send({ error: "Choose a task and a queue position." });
|
|
4614
5130
|
try {
|
|
@@ -4670,12 +5186,36 @@ attachEventServer(app, security, allowedOrigins, () => ({
|
|
|
4670
5186
|
type: "snapshot",
|
|
4671
5187
|
sessions: Effect17.runSync(store.list())
|
|
4672
5188
|
}));
|
|
5189
|
+
function localServerUrl() {
|
|
5190
|
+
const address = z19.object({ port: z19.number() }).safeParse(app.server.address());
|
|
5191
|
+
return address.success ? `http://${host}:${address.data.port}` : void 0;
|
|
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;
|
|
4673
5211
|
var closeControl = await startControl(
|
|
4674
5212
|
dataRoot,
|
|
4675
5213
|
security,
|
|
4676
5214
|
() => ({
|
|
4677
5215
|
serverId: security.serverId,
|
|
4678
5216
|
publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
|
|
5217
|
+
phoneProvider: phone?.snapshot().provider,
|
|
5218
|
+
localUrl: localServerUrl(),
|
|
4679
5219
|
serverVersion,
|
|
4680
5220
|
connectedDevices: new Set(security.sockets.values()).size,
|
|
4681
5221
|
connectedSockets: security.sockets.size,
|
|
@@ -4683,10 +5223,16 @@ var closeControl = await startControl(
|
|
|
4683
5223
|
activeRuns: activeAgents(Effect17.runSync(store.list())),
|
|
4684
5224
|
maintenance: maintenance.status()
|
|
4685
5225
|
}),
|
|
4686
|
-
maintenance
|
|
5226
|
+
maintenance,
|
|
5227
|
+
async () => {
|
|
5228
|
+
await app.close();
|
|
5229
|
+
process.exit(0);
|
|
5230
|
+
},
|
|
5231
|
+
phone
|
|
4687
5232
|
);
|
|
4688
5233
|
push.start();
|
|
4689
5234
|
app.addHook("onClose", async () => {
|
|
5235
|
+
await phone?.close();
|
|
4690
5236
|
await push.close();
|
|
4691
5237
|
maintenance.enter();
|
|
4692
5238
|
stopUpdateDiscovery();
|