@agentrific/server 0.5.2 → 0.7.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 +18 -0
- package/CLAUDE-BRIDGE-LICENSE.txt +21 -0
- package/README.md +13 -2
- package/claude-bridge.js +2682 -0
- package/cli.js +1015 -544
- package/cli.js.map +4 -4
- package/index.js +863 -185
- package/index.js.map +4 -4
- package/opentunnel-sdk.js +34715 -0
- package/opentunnel-sdk.js.map +7 -0
- package/package.json +10 -2
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
|
|
|
@@ -885,11 +885,11 @@ var Attachments = class extends Context.Service()(
|
|
|
885
885
|
|
|
886
886
|
// src/index.ts
|
|
887
887
|
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
888
|
-
import { execFile as
|
|
889
|
-
import { promisify as
|
|
888
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
889
|
+
import { promisify as promisify4 } from "node:util";
|
|
890
890
|
import { realpath as realpath2 } from "node:fs/promises";
|
|
891
|
-
import { homedir as
|
|
892
|
-
import
|
|
891
|
+
import { homedir as homedir4 } 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,8 @@ 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(),
|
|
996
|
+
phoneProgress: z4.string().nullable().optional(),
|
|
994
997
|
localUrl: z4.string().optional(),
|
|
995
998
|
serverVersion: z4.string().optional(),
|
|
996
999
|
connectedDevices: z4.number().optional(),
|
|
@@ -1000,6 +1003,12 @@ var ControlStatusSchema = z4.object({
|
|
|
1000
1003
|
maintenance: MaintenanceSchema.optional()
|
|
1001
1004
|
});
|
|
1002
1005
|
var ControlResultSchema = z4.union([
|
|
1006
|
+
z4.object({
|
|
1007
|
+
provider: ProviderSchema.nullable(),
|
|
1008
|
+
publicUrl: z4.string().url().nullable(),
|
|
1009
|
+
progress: z4.string().nullable().optional()
|
|
1010
|
+
}),
|
|
1011
|
+
z4.array(z4.object({ provider: ProviderSchema, available: z4.boolean(), reason: z4.string() })),
|
|
1003
1012
|
z4.object({
|
|
1004
1013
|
pairingId: z4.string(),
|
|
1005
1014
|
pin: z4.string(),
|
|
@@ -1013,6 +1022,9 @@ var ControlResultSchema = z4.union([
|
|
|
1013
1022
|
MaintenanceSchema
|
|
1014
1023
|
]);
|
|
1015
1024
|
var ControlRequest = z4.discriminatedUnion("command", [
|
|
1025
|
+
z4.object({ command: z4.literal("phone-options") }),
|
|
1026
|
+
z4.object({ command: z4.literal("phone-start"), provider: ProviderSchema }),
|
|
1027
|
+
z4.object({ command: z4.literal("phone-stop") }),
|
|
1016
1028
|
z4.object({ command: z4.literal("pair") }),
|
|
1017
1029
|
z4.object({ command: z4.literal("devices") }),
|
|
1018
1030
|
z4.object({ command: z4.literal("status") }),
|
|
@@ -1065,7 +1077,7 @@ async function acquireServerLock(directory) {
|
|
|
1065
1077
|
await unlink(filename);
|
|
1066
1078
|
};
|
|
1067
1079
|
}
|
|
1068
|
-
async function startControl(directory, security2, status, maintenance2, shutdown) {
|
|
1080
|
+
async function startControl(directory, security2, status, maintenance2, shutdown, phone2) {
|
|
1069
1081
|
const filename = controlSocketPath(directory);
|
|
1070
1082
|
const socketDirectory = path3.dirname(filename);
|
|
1071
1083
|
await mkdir2(socketDirectory, { recursive: true, mode: 448 });
|
|
@@ -1092,6 +1104,29 @@ async function startControl(directory, security2, status, maintenance2, shutdown
|
|
|
1092
1104
|
const request = ControlRequest.parse(JSON.parse(buffer.split("\n")[0]));
|
|
1093
1105
|
let result;
|
|
1094
1106
|
switch (request.command) {
|
|
1107
|
+
case "phone-options":
|
|
1108
|
+
if (!phone2) throw new Error("Phone connections are managed by the desktop app.");
|
|
1109
|
+
socket.setTimeout(18e4);
|
|
1110
|
+
result = await phone2.options();
|
|
1111
|
+
break;
|
|
1112
|
+
case "phone-start": {
|
|
1113
|
+
if (!phone2) throw new Error("Phone connections are managed by the desktop app.");
|
|
1114
|
+
maintenance2?.assertReady();
|
|
1115
|
+
socket.setTimeout(18e4);
|
|
1116
|
+
const abort = new AbortController();
|
|
1117
|
+
const disconnected = () => abort.abort();
|
|
1118
|
+
socket.once("close", disconnected);
|
|
1119
|
+
try {
|
|
1120
|
+
result = await phone2.start(request.provider, abort.signal);
|
|
1121
|
+
} finally {
|
|
1122
|
+
socket.removeListener("close", disconnected);
|
|
1123
|
+
}
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
case "phone-stop":
|
|
1127
|
+
if (!phone2) throw new Error("Phone connections are managed by the desktop app.");
|
|
1128
|
+
result = await phone2.close();
|
|
1129
|
+
break;
|
|
1095
1130
|
case "shutdown":
|
|
1096
1131
|
if (!shutdown || !maintenance2?.status().drained)
|
|
1097
1132
|
throw new Error("Drain the server before shutdown");
|
|
@@ -1137,10 +1172,13 @@ async function startControl(directory, security2, status, maintenance2, shutdown
|
|
|
1137
1172
|
};
|
|
1138
1173
|
}
|
|
1139
1174
|
|
|
1175
|
+
// src/cli-output.ts
|
|
1176
|
+
import { styleText } from "node:util";
|
|
1177
|
+
|
|
1140
1178
|
// package.json
|
|
1141
1179
|
var package_default = {
|
|
1142
1180
|
name: "@agentrific/server",
|
|
1143
|
-
version: "0.
|
|
1181
|
+
version: "0.7.0",
|
|
1144
1182
|
description: "Self-hosted control plane for long-running coding agents",
|
|
1145
1183
|
keywords: [
|
|
1146
1184
|
"agentrific",
|
|
@@ -1166,20 +1204,28 @@ var package_default = {
|
|
|
1166
1204
|
registry: "https://registry.npmjs.org"
|
|
1167
1205
|
},
|
|
1168
1206
|
scripts: {
|
|
1169
|
-
dev: "tsx watch src/index.ts",
|
|
1207
|
+
dev: "node scripts/build-claude-bridge.mjs && tsx watch src/index.ts",
|
|
1170
1208
|
start: "node dist/index.js",
|
|
1171
1209
|
typecheck: "tsc --noEmit",
|
|
1172
1210
|
build: "node scripts/build.mjs",
|
|
1173
|
-
cli: "AGENTRIFIC_DATA_DIR=./data tsx src/cli.ts"
|
|
1211
|
+
cli: "node scripts/build-claude-bridge.mjs && AGENTRIFIC_DATA_DIR=./data tsx src/cli.ts",
|
|
1212
|
+
"build:claude": "node scripts/build-claude-bridge.mjs"
|
|
1174
1213
|
},
|
|
1175
1214
|
dependencies: {
|
|
1215
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.296",
|
|
1216
|
+
"@anthropic-ai/sdk": "^0.124.0",
|
|
1217
|
+
"@earendil-works/pi-ai": "1.0.0",
|
|
1176
1218
|
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
1177
1219
|
"@fastify/cors": "^11.3.0",
|
|
1178
1220
|
"@fastify/multipart": "^10.1.2",
|
|
1179
1221
|
"@inquirer/select": "^5.2.5",
|
|
1222
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
1223
|
+
"cc-session-io": "^0.4.0",
|
|
1224
|
+
"change-case": "^5.4.4",
|
|
1180
1225
|
diff: "^9.0.0",
|
|
1181
1226
|
effect: "4.0.0-rc.117",
|
|
1182
1227
|
fastify: "^5.6.2",
|
|
1228
|
+
"pi-claude-agent-sdk": "0.9.2",
|
|
1183
1229
|
"pino-pretty": "^13.1.3",
|
|
1184
1230
|
"qrcode-terminal": "^0.12.0",
|
|
1185
1231
|
semver: "^7.8.5",
|
|
@@ -1190,6 +1236,7 @@ var package_default = {
|
|
|
1190
1236
|
},
|
|
1191
1237
|
devDependencies: {
|
|
1192
1238
|
"@agentrific/contracts": "workspace:*",
|
|
1239
|
+
"@agentrific/opentunnel-runtime": "workspace:*",
|
|
1193
1240
|
"@jest/globals": "30.5.2",
|
|
1194
1241
|
"@types/node": "^22.19.1",
|
|
1195
1242
|
"@types/qrcode-terminal": "^0.12.2",
|
|
@@ -1204,6 +1251,13 @@ var package_default = {
|
|
|
1204
1251
|
// src/version.ts
|
|
1205
1252
|
var serverVersion = package_default.version;
|
|
1206
1253
|
|
|
1254
|
+
// src/cli-output.ts
|
|
1255
|
+
function printError(message) {
|
|
1256
|
+
console.error(`
|
|
1257
|
+
${styleText(["bold", "red"], "Error")} ${message}
|
|
1258
|
+
`);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1207
1261
|
// src/pairing.ts
|
|
1208
1262
|
function pairingOrigin(url) {
|
|
1209
1263
|
const endpoint = new URL(url);
|
|
@@ -1278,17 +1332,17 @@ var ServerSecurity = class {
|
|
|
1278
1332
|
if (!/^[a-f0-9]{64}$/.test(token)) return void 0;
|
|
1279
1333
|
return Effect4.runSync(this.store.deviceForToken(digest(token)));
|
|
1280
1334
|
}
|
|
1281
|
-
ticket(token,
|
|
1335
|
+
ticket(token, path12) {
|
|
1282
1336
|
this.prune();
|
|
1283
1337
|
if (!this.authenticate(token) || this.tickets.size >= 1e4) return void 0;
|
|
1284
1338
|
const value = secret();
|
|
1285
|
-
this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path:
|
|
1339
|
+
this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path12 });
|
|
1286
1340
|
return value;
|
|
1287
1341
|
}
|
|
1288
|
-
redeemTicket(value,
|
|
1342
|
+
redeemTicket(value, path12, consume = true) {
|
|
1289
1343
|
this.prune();
|
|
1290
1344
|
const ticket = this.tickets.get(value);
|
|
1291
|
-
if (!ticket || ticket.path !==
|
|
1345
|
+
if (!ticket || ticket.path !== path12) return void 0;
|
|
1292
1346
|
if (consume) this.tickets.delete(value);
|
|
1293
1347
|
return Effect4.runSync(this.store.deviceForToken(ticket.tokenHash));
|
|
1294
1348
|
}
|
|
@@ -1301,19 +1355,19 @@ var ServerSecurity = class {
|
|
|
1301
1355
|
};
|
|
1302
1356
|
async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
1303
1357
|
app2.addHook("onRequest", async (request, reply) => {
|
|
1304
|
-
const
|
|
1358
|
+
const path12 = new URL(request.url, "http://localhost");
|
|
1305
1359
|
reply.header("Cache-Control", "no-store");
|
|
1306
1360
|
if (request.method === "OPTIONS") return;
|
|
1307
1361
|
if (request.headers.origin && !allowedOrigins2.includes(request.headers.origin))
|
|
1308
1362
|
return reply.code(403).send({ error: "Browser origin is not allowed." });
|
|
1309
|
-
if (
|
|
1363
|
+
if (path12.pathname === "/health" || path12.pathname === "/api/auth/pair" && request.method === "POST")
|
|
1310
1364
|
return;
|
|
1311
1365
|
const authorization = request.headers.authorization ?? "";
|
|
1312
1366
|
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,
|
|
1367
|
+
const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(path12.pathname);
|
|
1368
|
+
const ticket = path12.searchParams.get("ticket");
|
|
1369
|
+
path12.searchParams.delete("ticket");
|
|
1370
|
+
if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path12.pathname + path12.search, false)))
|
|
1317
1371
|
return reply.code(401).send({ error: "Pair this device with your server.", code: "UNAUTHORIZED" });
|
|
1318
1372
|
if (!asset && request.headers["x-agentrific-protocol"] !== String(PROTOCOL_VERSION))
|
|
1319
1373
|
return reply.code(409).send({
|
|
@@ -1378,6 +1432,478 @@ async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
|
1378
1432
|
});
|
|
1379
1433
|
}
|
|
1380
1434
|
|
|
1435
|
+
// src/tunnels.ts
|
|
1436
|
+
import { spawn } from "node:child_process";
|
|
1437
|
+
import { access, stat as stat2 } from "node:fs/promises";
|
|
1438
|
+
import { constants } from "node:fs";
|
|
1439
|
+
import { homedir } from "node:os";
|
|
1440
|
+
import path4 from "node:path";
|
|
1441
|
+
import { z as z7 } from "zod";
|
|
1442
|
+
function signalChild(child, signal) {
|
|
1443
|
+
if (!child.pid) return;
|
|
1444
|
+
if (process.platform === "win32") {
|
|
1445
|
+
child.kill(signal);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
try {
|
|
1449
|
+
process.kill(-child.pid, signal);
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
if (!z7.object({ code: z7.literal("ESRCH") }).safeParse(error).success) child.kill(signal);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
function execute(file, args, timeoutMs, env = process.env) {
|
|
1455
|
+
return new Promise((resolve2, reject) => {
|
|
1456
|
+
const child = spawn(file, args, {
|
|
1457
|
+
env,
|
|
1458
|
+
detached: process.platform !== "win32",
|
|
1459
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1460
|
+
});
|
|
1461
|
+
let stdout = "";
|
|
1462
|
+
let stderr = "";
|
|
1463
|
+
const timeout = setTimeout(() => {
|
|
1464
|
+
signalChild(child, "SIGKILL");
|
|
1465
|
+
reject(new Error("Command probe timed out."));
|
|
1466
|
+
}, timeoutMs);
|
|
1467
|
+
child.stdout?.on("data", (data) => {
|
|
1468
|
+
stdout += data.toString();
|
|
1469
|
+
checkOutput();
|
|
1470
|
+
});
|
|
1471
|
+
child.stderr?.on("data", (data) => {
|
|
1472
|
+
stderr += data.toString();
|
|
1473
|
+
checkOutput();
|
|
1474
|
+
});
|
|
1475
|
+
child.once("error", (error) => {
|
|
1476
|
+
clearTimeout(timeout);
|
|
1477
|
+
reject(error);
|
|
1478
|
+
});
|
|
1479
|
+
child.once("exit", () => signalChild(child, "SIGKILL"));
|
|
1480
|
+
child.once("close", (code) => {
|
|
1481
|
+
clearTimeout(timeout);
|
|
1482
|
+
if (code !== 0) reject(new Error("Command probe failed."));
|
|
1483
|
+
else resolve2({ stdout, stderr });
|
|
1484
|
+
});
|
|
1485
|
+
function checkOutput() {
|
|
1486
|
+
if (stdout.length + stderr.length <= 1048576) return;
|
|
1487
|
+
signalChild(child, "SIGKILL");
|
|
1488
|
+
reject(new Error("Command probe output exceeded its limit."));
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
async function findExecutable(name) {
|
|
1493
|
+
const candidates = (process.env.PATH ?? "").split(path4.delimiter).map((directory) => path4.resolve(directory, name));
|
|
1494
|
+
if (process.platform === "darwin") {
|
|
1495
|
+
candidates.push(`/opt/homebrew/bin/${name}`, `/usr/local/bin/${name}`);
|
|
1496
|
+
if (name === "tailscale")
|
|
1497
|
+
candidates.push("/Applications/Tailscale.app/Contents/MacOS/Tailscale");
|
|
1498
|
+
}
|
|
1499
|
+
for (const candidate of candidates) {
|
|
1500
|
+
try {
|
|
1501
|
+
await access(candidate, constants.X_OK);
|
|
1502
|
+
if ((await stat2(candidate)).isFile()) return candidate;
|
|
1503
|
+
} catch {
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return void 0;
|
|
1508
|
+
}
|
|
1509
|
+
var TcpConfig = z7.object({ TCP: z7.record(z7.string(), z7.unknown()).optional() });
|
|
1510
|
+
var ServeConfig = TcpConfig.extend({
|
|
1511
|
+
Foreground: z7.record(z7.string(), TcpConfig).optional()
|
|
1512
|
+
});
|
|
1513
|
+
async function probeTailscale() {
|
|
1514
|
+
const option = { provider: "tailscale", available: false, reason: "Not installed" };
|
|
1515
|
+
const executable = await findExecutable("tailscale");
|
|
1516
|
+
if (!executable) return option;
|
|
1517
|
+
option.executable = executable;
|
|
1518
|
+
const env = { ...process.env, TAILSCALE_BE_CLI: "1" };
|
|
1519
|
+
try {
|
|
1520
|
+
const response = await execute(executable, ["status", "--json"], 3e3, env);
|
|
1521
|
+
const status = z7.object({
|
|
1522
|
+
BackendState: z7.string(),
|
|
1523
|
+
CertDomains: z7.array(z7.string()).optional(),
|
|
1524
|
+
Self: z7.object({ DNSName: z7.string() }).nullable().optional()
|
|
1525
|
+
}).parse(JSON.parse(response.stdout));
|
|
1526
|
+
if (status.BackendState !== "Running")
|
|
1527
|
+
return { ...option, reason: "Installed \xB7 connect Tailscale first" };
|
|
1528
|
+
const dns = status.Self?.DNSName.replace(/\.$/, "");
|
|
1529
|
+
if (!dns || !status.CertDomains?.includes(dns))
|
|
1530
|
+
return { ...option, reason: "Enable HTTPS certificates in your tailnet first" };
|
|
1531
|
+
const help = await execute(executable, ["serve", "--help"], 2e3, env);
|
|
1532
|
+
if (!`${help.stdout}${help.stderr}`.includes("--https"))
|
|
1533
|
+
return { ...option, reason: "Update Tailscale to use foreground HTTPS Serve" };
|
|
1534
|
+
const responseConfig = await execute(executable, ["serve", "status", "--json"], 3e3, env);
|
|
1535
|
+
const config = ServeConfig.parse(JSON.parse(responseConfig.stdout) ?? {});
|
|
1536
|
+
const occupied = new Set(Object.keys(config.TCP ?? {}));
|
|
1537
|
+
for (const foreground of Object.values(config.Foreground ?? {}))
|
|
1538
|
+
for (const port3 of Object.keys(foreground.TCP ?? {})) occupied.add(port3);
|
|
1539
|
+
const port2 = [443, ...Array.from({ length: 10 }, (_, index) => 8443 + index)].find(
|
|
1540
|
+
(candidate) => !occupied.has(String(candidate))
|
|
1541
|
+
);
|
|
1542
|
+
if (!port2) return { ...option, reason: "HTTPS Serve ports are in use \xB7 paste an existing URL" };
|
|
1543
|
+
return {
|
|
1544
|
+
...option,
|
|
1545
|
+
available: true,
|
|
1546
|
+
reason: "Private to your tailnet \xB7 Tailscale required on your phone",
|
|
1547
|
+
publicUrl: new URL(`https://${dns}${port2 === 443 ? "" : `:${port2}`}`).origin,
|
|
1548
|
+
httpsPort: port2
|
|
1549
|
+
};
|
|
1550
|
+
} catch {
|
|
1551
|
+
return { ...option, reason: "Installed \xB7 could not verify daemon or Serve access" };
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
async function probeCloudflared(home) {
|
|
1555
|
+
const option = {
|
|
1556
|
+
provider: "cloudflared",
|
|
1557
|
+
available: false,
|
|
1558
|
+
reason: "Not installed"
|
|
1559
|
+
};
|
|
1560
|
+
const executable = await findExecutable("cloudflared");
|
|
1561
|
+
if (!executable) return option;
|
|
1562
|
+
option.executable = executable;
|
|
1563
|
+
try {
|
|
1564
|
+
await execute(executable, ["--version"], 2e3);
|
|
1565
|
+
const help = await execute(executable, ["tunnel", "--help"], 2e3);
|
|
1566
|
+
if (!`${help.stdout}${help.stderr}`.includes("--url"))
|
|
1567
|
+
return { ...option, reason: "Installed \xB7 this version does not support Quick Tunnels" };
|
|
1568
|
+
} catch {
|
|
1569
|
+
return { ...option, reason: "Installed \xB7 command probe failed or timed out" };
|
|
1570
|
+
}
|
|
1571
|
+
const directories = [
|
|
1572
|
+
path4.join(home, ".cloudflared"),
|
|
1573
|
+
path4.join(home, ".cloudflare-warp"),
|
|
1574
|
+
path4.join(home, "cloudflare-warp"),
|
|
1575
|
+
"/etc/cloudflared",
|
|
1576
|
+
"/usr/local/etc/cloudflared"
|
|
1577
|
+
];
|
|
1578
|
+
const configurations = await Promise.all(
|
|
1579
|
+
directories.flatMap(
|
|
1580
|
+
(directory) => ["config.yml", "config.yaml"].map(
|
|
1581
|
+
async (name) => access(path4.join(directory, name), constants.F_OK).then(
|
|
1582
|
+
() => true,
|
|
1583
|
+
() => false
|
|
1584
|
+
)
|
|
1585
|
+
)
|
|
1586
|
+
)
|
|
1587
|
+
);
|
|
1588
|
+
if (configurations.some(Boolean))
|
|
1589
|
+
return { ...option, reason: "Existing tunnel configuration \xB7 paste its HTTPS URL" };
|
|
1590
|
+
return { ...option, available: true, reason: "Temporary public HTTPS URL \xB7 changes each start" };
|
|
1591
|
+
}
|
|
1592
|
+
async function detectTunnels(home = homedir()) {
|
|
1593
|
+
return Promise.all([probeTailscale(), probeCloudflared(home)]);
|
|
1594
|
+
}
|
|
1595
|
+
var ManagedTunnel = class {
|
|
1596
|
+
constructor(option, disconnected = () => {
|
|
1597
|
+
}) {
|
|
1598
|
+
this.option = option;
|
|
1599
|
+
this.disconnected = disconnected;
|
|
1600
|
+
}
|
|
1601
|
+
option;
|
|
1602
|
+
disconnected;
|
|
1603
|
+
child;
|
|
1604
|
+
stopping = false;
|
|
1605
|
+
connected = false;
|
|
1606
|
+
closing;
|
|
1607
|
+
running() {
|
|
1608
|
+
return this.connected && !this.stopping;
|
|
1609
|
+
}
|
|
1610
|
+
stopOnSignal = () => {
|
|
1611
|
+
void this.close();
|
|
1612
|
+
};
|
|
1613
|
+
stopOnExit = () => {
|
|
1614
|
+
if (this.child) signalChild(this.child, "SIGKILL");
|
|
1615
|
+
};
|
|
1616
|
+
async start(origin, signal) {
|
|
1617
|
+
signal?.throwIfAborted();
|
|
1618
|
+
if (!this.option.available) throw new Error(this.option.reason);
|
|
1619
|
+
const tailscale = this.option.provider === "tailscale";
|
|
1620
|
+
const child = spawn(
|
|
1621
|
+
this.option.executable ?? this.option.provider,
|
|
1622
|
+
tailscale ? ["serve", "--bg=false", `--https=${this.option.httpsPort}`, origin] : ["tunnel", "--no-autoupdate", "--url", origin],
|
|
1623
|
+
{
|
|
1624
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1625
|
+
detached: process.platform !== "win32",
|
|
1626
|
+
env: tailscale ? { ...process.env, TAILSCALE_BE_CLI: "1" } : process.env
|
|
1627
|
+
}
|
|
1628
|
+
);
|
|
1629
|
+
this.child = child;
|
|
1630
|
+
process.once("SIGINT", this.stopOnSignal);
|
|
1631
|
+
process.once("SIGTERM", this.stopOnSignal);
|
|
1632
|
+
process.once("exit", this.stopOnExit);
|
|
1633
|
+
return new Promise((resolve2, reject) => {
|
|
1634
|
+
let output = "";
|
|
1635
|
+
let address;
|
|
1636
|
+
let settled = false;
|
|
1637
|
+
const abort = () => fail("Tunnel startup canceled.");
|
|
1638
|
+
const timeout = setTimeout(
|
|
1639
|
+
() => fail("Tunnel startup timed out. Check your connection or paste an existing HTTPS URL."),
|
|
1640
|
+
45e3
|
|
1641
|
+
);
|
|
1642
|
+
const fail = (message) => {
|
|
1643
|
+
if (settled) return;
|
|
1644
|
+
settled = true;
|
|
1645
|
+
clearTimeout(timeout);
|
|
1646
|
+
signal?.removeEventListener("abort", abort);
|
|
1647
|
+
void this.close();
|
|
1648
|
+
reject(new Error(message));
|
|
1649
|
+
};
|
|
1650
|
+
const read = (data) => {
|
|
1651
|
+
output = `${output}${data.toString()}`.slice(-65536);
|
|
1652
|
+
address = tailscale ? this.option.publicUrl : output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com\b/)?.[0];
|
|
1653
|
+
const ready = tailscale ? Boolean(
|
|
1654
|
+
address && output.includes(address) && output.includes("Available within your tailnet")
|
|
1655
|
+
) : output.includes("Registered tunnel connection");
|
|
1656
|
+
if (settled || !address || !ready) return;
|
|
1657
|
+
settled = true;
|
|
1658
|
+
this.connected = true;
|
|
1659
|
+
clearTimeout(timeout);
|
|
1660
|
+
signal?.removeEventListener("abort", abort);
|
|
1661
|
+
resolve2(address);
|
|
1662
|
+
};
|
|
1663
|
+
child.stdout?.on("data", read);
|
|
1664
|
+
child.stderr?.on("data", read);
|
|
1665
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1666
|
+
child.once(
|
|
1667
|
+
"error",
|
|
1668
|
+
() => fail(`${this.option.provider} could not start. Check the installation and permissions.`)
|
|
1669
|
+
);
|
|
1670
|
+
child.once("exit", () => signalChild(child, "SIGTERM"));
|
|
1671
|
+
child.once("close", (code) => {
|
|
1672
|
+
const unexpected = this.connected && !this.stopping;
|
|
1673
|
+
this.connected = false;
|
|
1674
|
+
process.removeListener("SIGINT", this.stopOnSignal);
|
|
1675
|
+
process.removeListener("SIGTERM", this.stopOnSignal);
|
|
1676
|
+
process.removeListener("exit", this.stopOnExit);
|
|
1677
|
+
fail(
|
|
1678
|
+
`${this.option.provider} exited (${code ?? "signal"}). Check tunnel access or paste an existing HTTPS URL.`
|
|
1679
|
+
);
|
|
1680
|
+
if (unexpected) {
|
|
1681
|
+
this.disconnected();
|
|
1682
|
+
printError(
|
|
1683
|
+
"The managed tunnel stopped. Restart the server to reconnect your phone; local work can continue."
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
close() {
|
|
1690
|
+
this.stopping = true;
|
|
1691
|
+
const child = this.child;
|
|
1692
|
+
if (!child?.pid || child.exitCode !== null || child.signalCode !== null)
|
|
1693
|
+
return Promise.resolve();
|
|
1694
|
+
this.closing ??= new Promise((resolve2) => {
|
|
1695
|
+
const timeout = setTimeout(() => signalChild(child, "SIGKILL"), 2e3);
|
|
1696
|
+
child.once("close", () => {
|
|
1697
|
+
clearTimeout(timeout);
|
|
1698
|
+
resolve2();
|
|
1699
|
+
});
|
|
1700
|
+
signalChild(child, "SIGTERM");
|
|
1701
|
+
});
|
|
1702
|
+
return this.closing;
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
// src/opentunnel.ts
|
|
1707
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1708
|
+
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
1709
|
+
import path5 from "node:path";
|
|
1710
|
+
import { z as z8 } from "zod";
|
|
1711
|
+
import {
|
|
1712
|
+
create,
|
|
1713
|
+
OpenTunnelStorage
|
|
1714
|
+
} from "./opentunnel-sdk.js";
|
|
1715
|
+
var OpenTunnel = class {
|
|
1716
|
+
constructor(directory, disconnected = () => {
|
|
1717
|
+
}, makeClient = create) {
|
|
1718
|
+
this.directory = directory;
|
|
1719
|
+
this.disconnected = disconnected;
|
|
1720
|
+
this.makeClient = makeClient;
|
|
1721
|
+
}
|
|
1722
|
+
directory;
|
|
1723
|
+
disconnected;
|
|
1724
|
+
makeClient;
|
|
1725
|
+
client;
|
|
1726
|
+
connection;
|
|
1727
|
+
stopping = false;
|
|
1728
|
+
stage = "reading-identity";
|
|
1729
|
+
progress() {
|
|
1730
|
+
return this.connection?.status().state ?? this.stage;
|
|
1731
|
+
}
|
|
1732
|
+
running() {
|
|
1733
|
+
return this.connection?.status().state === "connected" && !this.stopping;
|
|
1734
|
+
}
|
|
1735
|
+
async start(origin, signal) {
|
|
1736
|
+
const endpoint = new URL(origin);
|
|
1737
|
+
if (!["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname))
|
|
1738
|
+
throw new Error("OpenTunnel must forward to the local loopback server.");
|
|
1739
|
+
await mkdir3(this.directory, { recursive: true, mode: 448 });
|
|
1740
|
+
await chmod2(this.directory, 448);
|
|
1741
|
+
const routeFile = path5.join(this.directory, "route.json");
|
|
1742
|
+
let route;
|
|
1743
|
+
try {
|
|
1744
|
+
route = z8.string().regex(/^[a-f0-9]{16}$/).parse(JSON.parse(await readFile3(routeFile, "utf8")));
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
if (!z8.object({ code: z8.literal("ENOENT") }).safeParse(error).success) throw error;
|
|
1747
|
+
route = randomBytes2(8).toString("hex");
|
|
1748
|
+
await writeFile2(routeFile, JSON.stringify(route), { flag: "wx", mode: 384 });
|
|
1749
|
+
}
|
|
1750
|
+
this.stopping = false;
|
|
1751
|
+
this.client = this.makeClient({
|
|
1752
|
+
store: OpenTunnelStorage.xdg({ env: { XDG_DATA_HOME: this.directory } })
|
|
1753
|
+
});
|
|
1754
|
+
const abort = AbortSignal.any([
|
|
1755
|
+
signal ?? new AbortController().signal,
|
|
1756
|
+
AbortSignal.timeout(12e4)
|
|
1757
|
+
]);
|
|
1758
|
+
let canceled = () => {
|
|
1759
|
+
};
|
|
1760
|
+
try {
|
|
1761
|
+
const client = this.client;
|
|
1762
|
+
this.stage = "reading-identity";
|
|
1763
|
+
const connecting = (async () => {
|
|
1764
|
+
const options = {
|
|
1765
|
+
profile: "agentrific",
|
|
1766
|
+
onProgress: (stage) => {
|
|
1767
|
+
this.stage = stage;
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
const identity2 = await client.tunnel.get(options);
|
|
1771
|
+
abort.throwIfAborted();
|
|
1772
|
+
if (!identity2) {
|
|
1773
|
+
const resumed = await client.tunnel.resume(options);
|
|
1774
|
+
abort.throwIfAborted();
|
|
1775
|
+
if (!resumed) await client.tunnel.create(options);
|
|
1776
|
+
}
|
|
1777
|
+
abort.throwIfAborted();
|
|
1778
|
+
this.stage = "connecting";
|
|
1779
|
+
return client.tunnel.connect({
|
|
1780
|
+
profile: "agentrific",
|
|
1781
|
+
routes: {
|
|
1782
|
+
[route]: `${endpoint.hostname === "[::1]" ? "[::1]" : "127.0.0.1"}:${endpoint.port || "80"}`
|
|
1783
|
+
},
|
|
1784
|
+
signal
|
|
1785
|
+
});
|
|
1786
|
+
})();
|
|
1787
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
1788
|
+
canceled = () => reject(new Error("OpenTunnel connection canceled or timed out."));
|
|
1789
|
+
abort.addEventListener("abort", canceled, { once: true });
|
|
1790
|
+
if (abort.aborted) canceled();
|
|
1791
|
+
});
|
|
1792
|
+
const connection = await Promise.race([connecting, cancellation]);
|
|
1793
|
+
this.connection = connection;
|
|
1794
|
+
void connection.closed.then(() => {
|
|
1795
|
+
if (!this.stopping) this.disconnected();
|
|
1796
|
+
}).catch(() => {
|
|
1797
|
+
if (!this.stopping) this.disconnected();
|
|
1798
|
+
});
|
|
1799
|
+
return `https://${route}.${connection.tunnel.hostname}`;
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
await this.close();
|
|
1802
|
+
throw error;
|
|
1803
|
+
} finally {
|
|
1804
|
+
abort.removeEventListener("abort", canceled);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
async close() {
|
|
1808
|
+
this.stopping = true;
|
|
1809
|
+
await this.connection?.close();
|
|
1810
|
+
await this.client?.dispose();
|
|
1811
|
+
this.connection = void 0;
|
|
1812
|
+
this.client = void 0;
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
|
|
1816
|
+
// src/desktop-phone.ts
|
|
1817
|
+
var DesktopPhoneConnection = class {
|
|
1818
|
+
constructor(origin, changed, directory, detect = async () => [
|
|
1819
|
+
{ provider: "opentunnel", available: true, reason: "Built in \xB7 stable public HTTPS address" },
|
|
1820
|
+
...await detectTunnels()
|
|
1821
|
+
], create2 = (option, disconnected) => option.provider === "opentunnel" ? new OpenTunnel(directory, disconnected) : new ManagedTunnel({ ...option, provider: option.provider }, disconnected)) {
|
|
1822
|
+
this.origin = origin;
|
|
1823
|
+
this.changed = changed;
|
|
1824
|
+
this.directory = directory;
|
|
1825
|
+
this.detect = detect;
|
|
1826
|
+
this.create = create2;
|
|
1827
|
+
}
|
|
1828
|
+
origin;
|
|
1829
|
+
changed;
|
|
1830
|
+
directory;
|
|
1831
|
+
detect;
|
|
1832
|
+
create;
|
|
1833
|
+
tunnel;
|
|
1834
|
+
provider = null;
|
|
1835
|
+
publicUrl = null;
|
|
1836
|
+
pending = Promise.resolve();
|
|
1837
|
+
snapshot() {
|
|
1838
|
+
return {
|
|
1839
|
+
provider: this.provider,
|
|
1840
|
+
publicUrl: this.publicUrl,
|
|
1841
|
+
progress: this.tunnel?.progress?.() ?? null
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
async options() {
|
|
1845
|
+
return (await this.detect()).map(({ provider, available, reason }) => ({
|
|
1846
|
+
provider,
|
|
1847
|
+
available,
|
|
1848
|
+
reason
|
|
1849
|
+
}));
|
|
1850
|
+
}
|
|
1851
|
+
serialize(operation) {
|
|
1852
|
+
const result = this.pending.then(operation);
|
|
1853
|
+
this.pending = result.then(
|
|
1854
|
+
() => {
|
|
1855
|
+
},
|
|
1856
|
+
() => {
|
|
1857
|
+
}
|
|
1858
|
+
);
|
|
1859
|
+
return result;
|
|
1860
|
+
}
|
|
1861
|
+
start(provider, signal) {
|
|
1862
|
+
return this.serialize(async () => {
|
|
1863
|
+
signal?.throwIfAborted();
|
|
1864
|
+
if (this.tunnel && this.publicUrl && this.provider === provider) return this.snapshot();
|
|
1865
|
+
if (this.tunnel)
|
|
1866
|
+
throw new Error("Disconnect the current phone connection before changing providers.");
|
|
1867
|
+
const option = (await this.detect()).find((candidate) => candidate.provider === provider);
|
|
1868
|
+
if (!option?.available) throw new Error(option?.reason || "Tunnel provider unavailable.");
|
|
1869
|
+
let tunnel;
|
|
1870
|
+
tunnel = this.create(option, () => {
|
|
1871
|
+
if (this.tunnel !== tunnel) return;
|
|
1872
|
+
void tunnel.close().catch(() => {
|
|
1873
|
+
});
|
|
1874
|
+
this.tunnel = void 0;
|
|
1875
|
+
this.provider = null;
|
|
1876
|
+
this.publicUrl = null;
|
|
1877
|
+
this.changed(null);
|
|
1878
|
+
});
|
|
1879
|
+
this.tunnel = tunnel;
|
|
1880
|
+
try {
|
|
1881
|
+
const url = await tunnel.start(this.origin(), signal);
|
|
1882
|
+
signal?.throwIfAborted();
|
|
1883
|
+
this.provider = provider;
|
|
1884
|
+
this.publicUrl = url;
|
|
1885
|
+
this.changed(url);
|
|
1886
|
+
return this.snapshot();
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
await tunnel.close();
|
|
1889
|
+
this.tunnel = void 0;
|
|
1890
|
+
this.changed(null);
|
|
1891
|
+
throw error;
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
close() {
|
|
1896
|
+
return this.serialize(async () => {
|
|
1897
|
+
await this.tunnel?.close();
|
|
1898
|
+
this.tunnel = void 0;
|
|
1899
|
+
this.provider = null;
|
|
1900
|
+
this.publicUrl = null;
|
|
1901
|
+
this.changed(null);
|
|
1902
|
+
return this.snapshot();
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1381
1907
|
// src/maintenance.ts
|
|
1382
1908
|
var MaintenanceError = class extends Error {
|
|
1383
1909
|
statusCode = 503;
|
|
@@ -1419,9 +1945,9 @@ var Maintenance = class {
|
|
|
1419
1945
|
}
|
|
1420
1946
|
attach(app2) {
|
|
1421
1947
|
app2.addHook("onRequest", async (request, reply) => {
|
|
1422
|
-
const
|
|
1948
|
+
const path12 = request.url.split("?")[0];
|
|
1423
1949
|
if (["GET", "HEAD", "OPTIONS"].includes(request.method)) return;
|
|
1424
|
-
const allowed =
|
|
1950
|
+
const allowed = path12.startsWith("/api/auth/") || /^\/api\/sessions\/[^/]+\/stop$/.test(path12);
|
|
1425
1951
|
if (this.paused && !allowed)
|
|
1426
1952
|
return reply.code(503).send({ code: "MAINTENANCE", error: new MaintenanceError().message });
|
|
1427
1953
|
this.requests.add(request.id);
|
|
@@ -1434,10 +1960,10 @@ var Maintenance = class {
|
|
|
1434
1960
|
|
|
1435
1961
|
// src/updates.ts
|
|
1436
1962
|
import semver from "semver";
|
|
1437
|
-
import { z as
|
|
1963
|
+
import { z as z9 } from "zod";
|
|
1438
1964
|
import { Effect as Effect5 } from "effect";
|
|
1439
|
-
var RegistryRelease =
|
|
1440
|
-
version:
|
|
1965
|
+
var RegistryRelease = z9.object({
|
|
1966
|
+
version: z9.string().refine((value) => semver.valid(value) !== null)
|
|
1441
1967
|
});
|
|
1442
1968
|
async function checkForUpdates() {
|
|
1443
1969
|
const response = await fetch("https://registry.npmjs.org/@agentrific%2Fserver/latest", {
|
|
@@ -1527,7 +2053,7 @@ var UpdateDiscovery = class {
|
|
|
1527
2053
|
|
|
1528
2054
|
// src/index.ts
|
|
1529
2055
|
import { Effect as Effect17, ManagedRuntime } from "effect";
|
|
1530
|
-
import { z as
|
|
2056
|
+
import { z as z20 } from "zod";
|
|
1531
2057
|
|
|
1532
2058
|
// src/automation.ts
|
|
1533
2059
|
import { Effect as Effect7 } from "effect";
|
|
@@ -1762,7 +2288,7 @@ var AutomationCoordinator = class {
|
|
|
1762
2288
|
};
|
|
1763
2289
|
|
|
1764
2290
|
// src/runtime.ts
|
|
1765
|
-
import
|
|
2291
|
+
import path6 from "node:path";
|
|
1766
2292
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
1767
2293
|
|
|
1768
2294
|
// src/git-flow.ts
|
|
@@ -1827,8 +2353,8 @@ var AgentController = class {
|
|
|
1827
2353
|
const agent = yield* Effect8.tryPromise({
|
|
1828
2354
|
try: () => this.factory.open(
|
|
1829
2355
|
session.id,
|
|
1830
|
-
session.workspacePath ??
|
|
1831
|
-
|
|
2356
|
+
session.workspacePath ?? path6.join(this.worktreesRoot, session.id),
|
|
2357
|
+
path6.join(this.sessionsRoot, session.id),
|
|
1832
2358
|
session.model,
|
|
1833
2359
|
session
|
|
1834
2360
|
),
|
|
@@ -2063,7 +2589,7 @@ var AgentSessions = class extends Context2.Service()(
|
|
|
2063
2589
|
|
|
2064
2590
|
// src/delivery.ts
|
|
2065
2591
|
import { Effect as Effect9 } from "effect";
|
|
2066
|
-
import { z as
|
|
2592
|
+
import { z as z11 } from "zod";
|
|
2067
2593
|
|
|
2068
2594
|
// src/workflow-command.ts
|
|
2069
2595
|
import { execFile } from "node:child_process";
|
|
@@ -2111,12 +2637,12 @@ function workflowError(error) {
|
|
|
2111
2637
|
}
|
|
2112
2638
|
|
|
2113
2639
|
// src/workflow-schema.ts
|
|
2114
|
-
import { z as
|
|
2115
|
-
var DeliveryJobSchema =
|
|
2116
|
-
kind:
|
|
2117
|
-
sessionId:
|
|
2640
|
+
import { z as z10 } from "zod";
|
|
2641
|
+
var DeliveryJobSchema = z10.object({
|
|
2642
|
+
kind: z10.literal("delivery"),
|
|
2643
|
+
sessionId: z10.string(),
|
|
2118
2644
|
request: DeliveryRequestSchema,
|
|
2119
|
-
phase:
|
|
2645
|
+
phase: z10.enum([
|
|
2120
2646
|
"validating",
|
|
2121
2647
|
"drafting",
|
|
2122
2648
|
"committing",
|
|
@@ -2126,25 +2652,25 @@ var DeliveryJobSchema = z8.object({
|
|
|
2126
2652
|
"opening_pr",
|
|
2127
2653
|
"delivered"
|
|
2128
2654
|
]),
|
|
2129
|
-
attempt:
|
|
2130
|
-
commitSha:
|
|
2131
|
-
title:
|
|
2132
|
-
body:
|
|
2133
|
-
logs:
|
|
2134
|
-
failed:
|
|
2135
|
-
result:
|
|
2136
|
-
url:
|
|
2137
|
-
message:
|
|
2655
|
+
attempt: z10.number().int().positive(),
|
|
2656
|
+
commitSha: z10.string().optional(),
|
|
2657
|
+
title: z10.string(),
|
|
2658
|
+
body: z10.string(),
|
|
2659
|
+
logs: z10.string().default(""),
|
|
2660
|
+
failed: z10.boolean().default(false),
|
|
2661
|
+
result: z10.enum(["merged", "pr_opened", "no_changes"]).optional(),
|
|
2662
|
+
url: z10.string().optional(),
|
|
2663
|
+
message: z10.string().optional()
|
|
2138
2664
|
});
|
|
2139
|
-
var PreparationJobSchema =
|
|
2140
|
-
kind:
|
|
2141
|
-
sessionId:
|
|
2142
|
-
projectPath:
|
|
2665
|
+
var PreparationJobSchema = z10.object({
|
|
2666
|
+
kind: z10.literal("preparation"),
|
|
2667
|
+
sessionId: z10.string(),
|
|
2668
|
+
projectPath: z10.string(),
|
|
2143
2669
|
input: CreateSessionSchema,
|
|
2144
|
-
stage:
|
|
2145
|
-
logs:
|
|
2670
|
+
stage: z10.enum(["creating", "installing", "ready", "failed"]),
|
|
2671
|
+
logs: z10.string().default("")
|
|
2146
2672
|
});
|
|
2147
|
-
var WorkflowJobSchema =
|
|
2673
|
+
var WorkflowJobSchema = z10.discriminatedUnion("kind", [
|
|
2148
2674
|
DeliveryJobSchema,
|
|
2149
2675
|
PreparationJobSchema
|
|
2150
2676
|
]);
|
|
@@ -2440,11 +2966,11 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2440
2966
|
"--json",
|
|
2441
2967
|
"url,state,headRefOid"
|
|
2442
2968
|
]);
|
|
2443
|
-
const prs =
|
|
2444
|
-
|
|
2445
|
-
url:
|
|
2446
|
-
state:
|
|
2447
|
-
headRefOid:
|
|
2969
|
+
const prs = z11.array(
|
|
2970
|
+
z11.object({
|
|
2971
|
+
url: z11.string().url(),
|
|
2972
|
+
state: z11.string(),
|
|
2973
|
+
headRefOid: z11.string().optional()
|
|
2448
2974
|
})
|
|
2449
2975
|
).parse(JSON.parse(existing));
|
|
2450
2976
|
const open = prs.find(
|
|
@@ -2485,10 +3011,10 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2485
3011
|
};
|
|
2486
3012
|
|
|
2487
3013
|
// src/preparation.ts
|
|
2488
|
-
import { mkdir as
|
|
2489
|
-
import
|
|
3014
|
+
import { mkdir as mkdir4, readFile as readFile4 } from "node:fs/promises";
|
|
3015
|
+
import path7 from "node:path";
|
|
2490
3016
|
import { Effect as Effect10 } from "effect";
|
|
2491
|
-
import { z as
|
|
3017
|
+
import { z as z12 } from "zod";
|
|
2492
3018
|
var PreparationCoordinator = class {
|
|
2493
3019
|
constructor(store2, options) {
|
|
2494
3020
|
this.store = store2;
|
|
@@ -2582,7 +3108,7 @@ ${logs}` : ""}`);
|
|
|
2582
3108
|
const workspace = session.workspacePath;
|
|
2583
3109
|
if (branchAtWorkspace !== void 0)
|
|
2584
3110
|
throw new Error("The run workspace already exists on a different branch.");
|
|
2585
|
-
await
|
|
3111
|
+
await mkdir4(path7.dirname(workspace), { recursive: true });
|
|
2586
3112
|
try {
|
|
2587
3113
|
await command(
|
|
2588
3114
|
projectPath,
|
|
@@ -2602,12 +3128,12 @@ ${logs}` : ""}`);
|
|
|
2602
3128
|
}
|
|
2603
3129
|
}
|
|
2604
3130
|
async install(workspace, command) {
|
|
2605
|
-
const packageFile = await
|
|
3131
|
+
const packageFile = await readFile4(path7.join(workspace, "package.json"), "utf8").catch(
|
|
2606
3132
|
() => void 0
|
|
2607
3133
|
);
|
|
2608
3134
|
if (!packageFile) return;
|
|
2609
|
-
const metadata =
|
|
2610
|
-
const has = async (name) =>
|
|
3135
|
+
const metadata = z12.object({ packageManager: z12.string().optional() }).parse(JSON.parse(packageFile));
|
|
3136
|
+
const has = async (name) => readFile4(path7.join(workspace, name)).then(
|
|
2611
3137
|
() => true,
|
|
2612
3138
|
() => false
|
|
2613
3139
|
);
|
|
@@ -2688,19 +3214,22 @@ ${logs}` : ""}`);
|
|
|
2688
3214
|
|
|
2689
3215
|
// src/delivery-writer.ts
|
|
2690
3216
|
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2691
|
-
import { z as
|
|
2692
|
-
var DraftSchema =
|
|
3217
|
+
import { z as z13 } from "zod";
|
|
3218
|
+
var DraftSchema = z13.object({ title: z13.string().trim().min(1).max(120), body: z13.string().trim().min(1).max(1e4) }).strict();
|
|
2693
3219
|
function deliveryWriter(models2, createSession = createAgentSession) {
|
|
2694
3220
|
return async (run, input, diff, signal) => {
|
|
2695
3221
|
signal?.throwIfAborted();
|
|
3222
|
+
const selected = run.model ?? await models2.defaultReference();
|
|
3223
|
+
const sessionOptions = await models2.sessionOptions(run.workspacePath);
|
|
2696
3224
|
const { session } = await createSession({
|
|
2697
3225
|
cwd: run.workspacePath,
|
|
2698
|
-
model: await models2.resolve(
|
|
2699
|
-
|
|
3226
|
+
model: await models2.resolve(selected),
|
|
3227
|
+
...sessionOptions,
|
|
2700
3228
|
sessionManager: SessionManager.inMemory(),
|
|
2701
3229
|
tools: [],
|
|
2702
3230
|
noTools: "all"
|
|
2703
3231
|
});
|
|
3232
|
+
if (sessionOptions.resourceLoader) await session.bindExtensions({});
|
|
2704
3233
|
const timer = setTimeout(() => {
|
|
2705
3234
|
void session.abort();
|
|
2706
3235
|
}, 6e4);
|
|
@@ -2730,6 +3259,103 @@ ${diff}`
|
|
|
2730
3259
|
// src/index.ts
|
|
2731
3260
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2732
3261
|
|
|
3262
|
+
// src/claude-code.ts
|
|
3263
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
3264
|
+
import { homedir as homedir2 } from "node:os";
|
|
3265
|
+
import { join as join2 } from "node:path";
|
|
3266
|
+
import { promisify as promisify2 } from "node:util";
|
|
3267
|
+
import { z as z14 } from "zod";
|
|
3268
|
+
import {
|
|
3269
|
+
createAgentSessionServices
|
|
3270
|
+
} from "@earendil-works/pi-coding-agent";
|
|
3271
|
+
var claudePlatform = "claude-bridge";
|
|
3272
|
+
var execute2 = promisify2(execFile2);
|
|
3273
|
+
var AuthStatusSchema = z14.object({ loggedIn: z14.boolean() });
|
|
3274
|
+
async function discoverClaudeCode() {
|
|
3275
|
+
if (process.env.AGENTRIFIC_CLAUDE_CODE === "off") return;
|
|
3276
|
+
const candidates = [
|
|
3277
|
+
"claude",
|
|
3278
|
+
join2(homedir2(), ".local/bin/claude"),
|
|
3279
|
+
join2(homedir2(), ".claude/local/claude")
|
|
3280
|
+
];
|
|
3281
|
+
for (const executable of candidates) {
|
|
3282
|
+
try {
|
|
3283
|
+
await execute2(executable, ["--version"], { timeout: 5e3, maxBuffer: 64 * 1024 });
|
|
3284
|
+
} catch {
|
|
3285
|
+
continue;
|
|
3286
|
+
}
|
|
3287
|
+
try {
|
|
3288
|
+
const { stdout } = await execute2(executable, ["auth", "status", "--json"], {
|
|
3289
|
+
timeout: 5e3,
|
|
3290
|
+
maxBuffer: 64 * 1024
|
|
3291
|
+
});
|
|
3292
|
+
const status = AuthStatusSchema.parse(JSON.parse(stdout));
|
|
3293
|
+
return { executable, ready: status.loggedIn };
|
|
3294
|
+
} catch {
|
|
3295
|
+
return { executable, ready: false };
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
var ClaudeCode = class {
|
|
3300
|
+
constructor(probe = discoverClaudeCode, query) {
|
|
3301
|
+
this.probe = probe;
|
|
3302
|
+
this.query = query;
|
|
3303
|
+
}
|
|
3304
|
+
probe;
|
|
3305
|
+
query;
|
|
3306
|
+
installation;
|
|
3307
|
+
registered = false;
|
|
3308
|
+
get ready() {
|
|
3309
|
+
return this.installation?.ready === true;
|
|
3310
|
+
}
|
|
3311
|
+
get warning() {
|
|
3312
|
+
return this.installation && !this.ready ? "Claude Code is installed but its login could not be verified. Run claude auth login as the server user, then refresh the model list." : null;
|
|
3313
|
+
}
|
|
3314
|
+
async refresh(runtime2) {
|
|
3315
|
+
this.installation = await this.probe();
|
|
3316
|
+
if (this.installation && !this.registered) {
|
|
3317
|
+
await this.services(process.cwd(), runtime2);
|
|
3318
|
+
this.registered = true;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
async services(cwd, modelRuntime2) {
|
|
3322
|
+
const module = await import(new URL("./claude-bridge.js", import.meta.url).href);
|
|
3323
|
+
const extension = module.createClaudeExtension(this.query);
|
|
3324
|
+
const services = await createAgentSessionServices({
|
|
3325
|
+
cwd,
|
|
3326
|
+
modelRuntime: modelRuntime2,
|
|
3327
|
+
resourceLoaderOptions: {
|
|
3328
|
+
// Match Pipeline: do not execute unrelated global Pi extensions.
|
|
3329
|
+
noExtensions: true,
|
|
3330
|
+
extensionFactories: [
|
|
3331
|
+
{
|
|
3332
|
+
name: "agentrific-claude-code",
|
|
3333
|
+
factory: (pi) => extension({
|
|
3334
|
+
...pi,
|
|
3335
|
+
// SAFETY: The pinned bridge uses only the name/config registration overload.
|
|
3336
|
+
registerProvider: ((id, config) => pi.registerProvider(id, {
|
|
3337
|
+
...config,
|
|
3338
|
+
// Bind the working directory to this provider,
|
|
3339
|
+
// without changing the process cwd or global Claude configuration.
|
|
3340
|
+
streamSimple: config.streamSimple && ((model, context, options) => {
|
|
3341
|
+
const request = { ...options, cwd };
|
|
3342
|
+
return config.streamSimple(model, context, request);
|
|
3343
|
+
})
|
|
3344
|
+
}))
|
|
3345
|
+
})
|
|
3346
|
+
}
|
|
3347
|
+
]
|
|
3348
|
+
}
|
|
3349
|
+
});
|
|
3350
|
+
const errors = [
|
|
3351
|
+
...services.diagnostics.filter((item) => item.type === "error").map((item) => item.message),
|
|
3352
|
+
...services.resourceLoader.getExtensions().errors.map((item) => item.error)
|
|
3353
|
+
];
|
|
3354
|
+
if (errors.length) throw new Error(errors.join("\n"));
|
|
3355
|
+
return services;
|
|
3356
|
+
}
|
|
3357
|
+
};
|
|
3358
|
+
|
|
2733
3359
|
// src/models.ts
|
|
2734
3360
|
import { Effect as Effect11 } from "effect";
|
|
2735
3361
|
var legacyModel = { platformId: "openai-codex", modelId: "gpt-6-luna" };
|
|
@@ -2748,12 +3374,14 @@ var makers = [
|
|
|
2748
3374
|
{ id: "amazon", name: "Amazon", pattern: /amazon\.nova|nova-/i }
|
|
2749
3375
|
];
|
|
2750
3376
|
var ServerModels = class {
|
|
2751
|
-
constructor(runtime2, store2) {
|
|
3377
|
+
constructor(runtime2, store2, claude) {
|
|
2752
3378
|
this.runtime = runtime2;
|
|
2753
3379
|
this.store = store2;
|
|
3380
|
+
this.claude = claude;
|
|
2754
3381
|
}
|
|
2755
3382
|
runtime;
|
|
2756
3383
|
store;
|
|
3384
|
+
claude;
|
|
2757
3385
|
refreshing;
|
|
2758
3386
|
refreshedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2759
3387
|
warning = null;
|
|
@@ -2775,18 +3403,20 @@ var ServerModels = class {
|
|
|
2775
3403
|
platformId,
|
|
2776
3404
|
modelId,
|
|
2777
3405
|
name: model?.name ?? modelId,
|
|
2778
|
-
platformName: this.runtime.getProvider(platformId)?.name ?? platformId,
|
|
3406
|
+
platformName: platformId === claudePlatform ? "Claude Code" : this.runtime.getProvider(platformId)?.name ?? platformId,
|
|
2779
3407
|
providerId: namespace === "openrouter" ? "unspecified" : maker?.id ?? namespace ?? "unspecified",
|
|
2780
3408
|
providerName: namespace === "openrouter" ? "Other / custom" : maker?.name ?? publisherName ?? "Other / custom"
|
|
2781
3409
|
};
|
|
2782
3410
|
}
|
|
2783
3411
|
async refresh(network) {
|
|
2784
3412
|
if (!this.refreshing) {
|
|
2785
|
-
this.refreshing = this.runtime.
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
3413
|
+
this.refreshing = Promise.resolve(this.claude?.refresh(this.runtime)).then(
|
|
3414
|
+
() => this.runtime.refresh({
|
|
3415
|
+
allowNetwork: network,
|
|
3416
|
+
force: network,
|
|
3417
|
+
signal: AbortSignal.timeout(15e3)
|
|
3418
|
+
})
|
|
3419
|
+
).then((result) => {
|
|
2790
3420
|
this.refreshedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2791
3421
|
this.warning = result.aborted || result.errors.size || this.runtime.getError() ? "Some providers could not refresh. Showing the catalogue currently available on this server." : null;
|
|
2792
3422
|
}).finally(() => {
|
|
@@ -2796,7 +3426,10 @@ var ServerModels = class {
|
|
|
2796
3426
|
await this.refreshing;
|
|
2797
3427
|
}
|
|
2798
3428
|
async defaultReference() {
|
|
2799
|
-
|
|
3429
|
+
const saved = await Effect11.runPromise(this.store.defaultModel());
|
|
3430
|
+
if (saved) return saved;
|
|
3431
|
+
const preferred = this.claude?.ready ? this.runtime.getModels().find((model) => model.provider === claudePlatform && /sonnet/i.test(model.id)) : void 0;
|
|
3432
|
+
return preferred ? { platformId: preferred.provider, modelId: preferred.id } : legacyModel;
|
|
2800
3433
|
}
|
|
2801
3434
|
async defaultIdentity() {
|
|
2802
3435
|
const selected = await this.defaultReference();
|
|
@@ -2809,17 +3442,24 @@ var ServerModels = class {
|
|
|
2809
3442
|
return {
|
|
2810
3443
|
models: this.runtime.getModels().map((model) => ({
|
|
2811
3444
|
...this.identity(model.provider, model.id),
|
|
2812
|
-
available: available.has(`${model.provider}\0${model.id}`),
|
|
3445
|
+
available: available.has(`${model.provider}\0${model.id}`) && (model.provider !== claudePlatform || this.claude?.ready === true),
|
|
2813
3446
|
reasoning: model.reasoning,
|
|
2814
3447
|
input: model.input,
|
|
2815
3448
|
contextWindow: model.contextWindow
|
|
2816
3449
|
})),
|
|
2817
3450
|
defaultModel: await this.defaultIdentity(),
|
|
2818
3451
|
refreshedAt: this.refreshedAt,
|
|
2819
|
-
warning: this.warning
|
|
3452
|
+
warning: this.claude?.warning ?? this.warning
|
|
2820
3453
|
};
|
|
2821
3454
|
}
|
|
2822
3455
|
async resolve(reference) {
|
|
3456
|
+
if (reference.platformId === claudePlatform && this.claude) {
|
|
3457
|
+
await this.claude.refresh(this.runtime);
|
|
3458
|
+
if (!this.claude.ready)
|
|
3459
|
+
throw new Error(
|
|
3460
|
+
this.claude.warning ?? "Claude Code is unavailable on this server. Install it and run claude auth login as the server user."
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
2823
3463
|
const available = await this.runtime.getAvailable(reference.platformId, {
|
|
2824
3464
|
signal: AbortSignal.timeout(1e4)
|
|
2825
3465
|
});
|
|
@@ -2830,6 +3470,17 @@ var ServerModels = class {
|
|
|
2830
3470
|
);
|
|
2831
3471
|
return model;
|
|
2832
3472
|
}
|
|
3473
|
+
async sessionOptions(cwd = process.cwd()) {
|
|
3474
|
+
if (this.claude && this.runtime.getProvider(claudePlatform)) {
|
|
3475
|
+
const services = await this.claude.services(cwd);
|
|
3476
|
+
return {
|
|
3477
|
+
modelRuntime: services.modelRuntime,
|
|
3478
|
+
resourceLoader: services.resourceLoader,
|
|
3479
|
+
settingsManager: services.settingsManager
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
return { modelRuntime: this.runtime };
|
|
3483
|
+
}
|
|
2833
3484
|
async setDefault(reference) {
|
|
2834
3485
|
await this.resolve(reference);
|
|
2835
3486
|
await Effect11.runPromise(this.store.setDefaultModel(reference));
|
|
@@ -2844,7 +3495,7 @@ import {
|
|
|
2844
3495
|
createAgentSession as createAgentSession2,
|
|
2845
3496
|
SessionManager as SessionManager2
|
|
2846
3497
|
} from "@earendil-works/pi-coding-agent";
|
|
2847
|
-
import { z as
|
|
3498
|
+
import { z as z16 } from "zod";
|
|
2848
3499
|
|
|
2849
3500
|
// src/delivery-tool.ts
|
|
2850
3501
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
@@ -2881,23 +3532,23 @@ function deliveryTool(sessionId, delivery2) {
|
|
|
2881
3532
|
// src/file-change.ts
|
|
2882
3533
|
import { createWriteToolDefinition, defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
|
|
2883
3534
|
import { createTwoFilesPatch } from "diff";
|
|
2884
|
-
import { mkdir as
|
|
2885
|
-
import { z as
|
|
3535
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
3536
|
+
import { z as z15 } from "zod";
|
|
2886
3537
|
var previewLimit = 256e3;
|
|
2887
3538
|
function boundFileChange(change) {
|
|
2888
3539
|
if ((change.patch?.length ?? 0) + (change.content?.length ?? 0) > previewLimit)
|
|
2889
3540
|
return { path: change.path, omitted: true };
|
|
2890
3541
|
return change;
|
|
2891
3542
|
}
|
|
2892
|
-
var EditResultSchema =
|
|
2893
|
-
details:
|
|
3543
|
+
var EditResultSchema = z15.object({
|
|
3544
|
+
details: z15.object({ patch: z15.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
|
|
2894
3545
|
});
|
|
2895
|
-
function completedFileChange(result,
|
|
3546
|
+
function completedFileChange(result, path12) {
|
|
2896
3547
|
const parsed = EditResultSchema.safeParse(result);
|
|
2897
3548
|
if (!parsed.success) return;
|
|
2898
3549
|
const details = parsed.data.details;
|
|
2899
3550
|
if (details?.fileChange) return boundFileChange(details.fileChange);
|
|
2900
|
-
if (
|
|
3551
|
+
if (path12 && details?.patch) return boundFileChange({ path: path12, patch: details.patch });
|
|
2901
3552
|
}
|
|
2902
3553
|
function fileWriteTool(cwd) {
|
|
2903
3554
|
const base = createWriteToolDefinition(cwd);
|
|
@@ -2914,16 +3565,16 @@ function fileWriteTool(cwd) {
|
|
|
2914
3565
|
const tool = createWriteToolDefinition(cwd, {
|
|
2915
3566
|
operations: {
|
|
2916
3567
|
mkdir: async (dir) => {
|
|
2917
|
-
await
|
|
3568
|
+
await mkdir5(dir, { recursive: true });
|
|
2918
3569
|
},
|
|
2919
3570
|
// Pi invokes this inside its per-file mutation queue, so the snapshot
|
|
2920
3571
|
// and write cannot race another built-in edit/write on the same file.
|
|
2921
|
-
writeFile: async (
|
|
2922
|
-
const previous = await
|
|
3572
|
+
writeFile: async (path12, content) => {
|
|
3573
|
+
const previous = await readFile5(path12, "utf8").catch((error) => {
|
|
2923
3574
|
if (error.code === "ENOENT") return void 0;
|
|
2924
3575
|
throw error;
|
|
2925
3576
|
});
|
|
2926
|
-
await
|
|
3577
|
+
await writeFile3(path12, content, "utf8");
|
|
2927
3578
|
fileChange = boundFileChange(
|
|
2928
3579
|
previous === void 0 ? { path: args.path, content } : {
|
|
2929
3580
|
path: args.path,
|
|
@@ -2940,8 +3591,8 @@ function fileWriteTool(cwd) {
|
|
|
2940
3591
|
}
|
|
2941
3592
|
|
|
2942
3593
|
// src/pi.ts
|
|
2943
|
-
var ToolResultSchema =
|
|
2944
|
-
content:
|
|
3594
|
+
var ToolResultSchema = z16.object({
|
|
3595
|
+
content: z16.array(z16.object({ type: z16.string(), text: z16.string().optional() })).optional()
|
|
2945
3596
|
}).passthrough();
|
|
2946
3597
|
var boundedJson = (value, limit) => {
|
|
2947
3598
|
let text;
|
|
@@ -2961,16 +3612,18 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2961
3612
|
return {
|
|
2962
3613
|
async open(sessionId, cwd, sessionDirectory, selected = legacyModel, run) {
|
|
2963
3614
|
const model = await models2.resolve(selected);
|
|
3615
|
+
const sessionOptions = await models2.sessionOptions(cwd);
|
|
2964
3616
|
const { session } = await createSession({
|
|
2965
3617
|
cwd,
|
|
2966
3618
|
model,
|
|
2967
|
-
|
|
3619
|
+
...sessionOptions,
|
|
2968
3620
|
sessionManager: SessionManager2.continueRecent(cwd, sessionDirectory),
|
|
2969
3621
|
customTools: [
|
|
2970
3622
|
fileWriteTool(cwd),
|
|
2971
3623
|
...run?.gitFlow && delivery2 ? [deliveryTool(sessionId, delivery2)] : []
|
|
2972
3624
|
]
|
|
2973
3625
|
});
|
|
3626
|
+
if (sessionOptions.resourceLoader) await session.bindExtensions({});
|
|
2974
3627
|
return {
|
|
2975
3628
|
get model() {
|
|
2976
3629
|
const current = session.model;
|
|
@@ -3065,7 +3718,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
3065
3718
|
return false;
|
|
3066
3719
|
}
|
|
3067
3720
|
const filePaths = /* @__PURE__ */ new Map();
|
|
3068
|
-
const FileArgsSchema =
|
|
3721
|
+
const FileArgsSchema = z16.object({ path: z16.string() });
|
|
3069
3722
|
function handleTool(event) {
|
|
3070
3723
|
if (event.type === "tool_execution_start") {
|
|
3071
3724
|
flushText();
|
|
@@ -3125,24 +3778,24 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
3125
3778
|
}
|
|
3126
3779
|
|
|
3127
3780
|
// src/projects.ts
|
|
3128
|
-
import { access, readdir as readdir2, realpath, stat as
|
|
3781
|
+
import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
3129
3782
|
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
|
|
3783
|
+
import { z as z17 } from "zod";
|
|
3784
|
+
import { constants as constants2 } from "node:fs";
|
|
3785
|
+
import { homedir as homedir3 } from "node:os";
|
|
3786
|
+
import path8 from "node:path";
|
|
3134
3787
|
import { Effect as Effect13, Semaphore as Semaphore3 } from "effect";
|
|
3135
3788
|
function resolveProjectsRoot(input) {
|
|
3136
3789
|
return Effect13.tryPromise({
|
|
3137
3790
|
try: async () => {
|
|
3138
3791
|
const trimmed = input.trim();
|
|
3139
|
-
const expanded = trimmed === "~" ?
|
|
3140
|
-
if (!
|
|
3792
|
+
const expanded = trimmed === "~" ? homedir3() : trimmed.startsWith("~/") ? path8.join(homedir3(), trimmed.slice(2)) : trimmed;
|
|
3793
|
+
if (!path8.isAbsolute(expanded))
|
|
3141
3794
|
throw new Error("Use an absolute server path or a path beginning with ~/.");
|
|
3142
3795
|
const resolved = await realpath(expanded);
|
|
3143
|
-
if (!(await
|
|
3796
|
+
if (!(await stat3(resolved)).isDirectory())
|
|
3144
3797
|
throw new Error("The projects folder must be a directory.");
|
|
3145
|
-
await
|
|
3798
|
+
await access2(resolved, constants2.R_OK | constants2.X_OK);
|
|
3146
3799
|
return resolved;
|
|
3147
3800
|
},
|
|
3148
3801
|
catch: () => new Error(
|
|
@@ -3159,36 +3812,36 @@ function projectLock(store2) {
|
|
|
3159
3812
|
}
|
|
3160
3813
|
return lock;
|
|
3161
3814
|
}
|
|
3162
|
-
var ProjectId =
|
|
3815
|
+
var ProjectId = z17.string().trim().min(1).max(255);
|
|
3163
3816
|
async function gitDirectory(directory) {
|
|
3164
|
-
const location =
|
|
3165
|
-
const info = await
|
|
3817
|
+
const location = path8.join(directory, ".git");
|
|
3818
|
+
const info = await stat3(location);
|
|
3166
3819
|
if (info.isDirectory()) return location;
|
|
3167
|
-
const contents = await
|
|
3820
|
+
const contents = await readFile6(location, "utf8");
|
|
3168
3821
|
const match = /^gitdir: (.+)\s*$/m.exec(contents);
|
|
3169
3822
|
if (!match) throw new Error("Invalid Git worktree metadata.");
|
|
3170
|
-
const resolved = await realpath(
|
|
3171
|
-
if (!(await
|
|
3823
|
+
const resolved = await realpath(path8.resolve(directory, match[1].trim()));
|
|
3824
|
+
if (!(await stat3(resolved)).isDirectory()) throw new Error("Git directory is missing.");
|
|
3172
3825
|
return resolved;
|
|
3173
3826
|
}
|
|
3174
3827
|
async function identity(directory, preferred) {
|
|
3175
|
-
const filename =
|
|
3828
|
+
const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
|
|
3176
3829
|
try {
|
|
3177
|
-
return ProjectId.parse(await
|
|
3830
|
+
return ProjectId.parse(await readFile6(filename, "utf8"));
|
|
3178
3831
|
} catch (error) {
|
|
3179
|
-
if (!
|
|
3180
|
-
const legacyFilename =
|
|
3181
|
-
const legacy = await
|
|
3182
|
-
if (!
|
|
3832
|
+
if (!z17.object({ code: z17.literal("ENOENT") }).safeParse(error).success) throw error;
|
|
3833
|
+
const legacyFilename = path8.join(await gitDirectory(directory), "chatplans-project-id");
|
|
3834
|
+
const legacy = await readFile6(legacyFilename, "utf8").catch((cause) => {
|
|
3835
|
+
if (!z17.object({ code: z17.literal("ENOENT") }).safeParse(cause).success) throw cause;
|
|
3183
3836
|
return null;
|
|
3184
3837
|
});
|
|
3185
3838
|
const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
|
|
3186
3839
|
try {
|
|
3187
|
-
await
|
|
3840
|
+
await writeFile4(filename, id + "\n", { flag: "wx" });
|
|
3188
3841
|
return id;
|
|
3189
3842
|
} catch (writeError) {
|
|
3190
|
-
if (!
|
|
3191
|
-
return ProjectId.parse(await
|
|
3843
|
+
if (!z17.object({ code: z17.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
|
|
3844
|
+
return ProjectId.parse(await readFile6(filename, "utf8"));
|
|
3192
3845
|
}
|
|
3193
3846
|
}
|
|
3194
3847
|
}
|
|
@@ -3201,15 +3854,15 @@ function scanProjects(root, store2) {
|
|
|
3201
3854
|
const entries = await readdir2(root, { withFileTypes: true });
|
|
3202
3855
|
for (const entry of entries) {
|
|
3203
3856
|
if (!entry.isDirectory()) continue;
|
|
3204
|
-
const directory =
|
|
3205
|
-
if (!await
|
|
3857
|
+
const directory = path8.join(root, entry.name);
|
|
3858
|
+
if (!await stat3(path8.join(directory, ".git")).catch(() => null)) continue;
|
|
3206
3859
|
const previous = registered.find((project2) => project2.path === directory);
|
|
3207
3860
|
let id = await identity(directory, previous?.available ? void 0 : previous?.id);
|
|
3208
3861
|
let existing = registered.find((project2) => project2.id === id);
|
|
3209
|
-
if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await
|
|
3862
|
+
if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat3(path8.join(existing.path, ".git")).catch(() => null)) {
|
|
3210
3863
|
id = randomUUID6();
|
|
3211
|
-
await
|
|
3212
|
-
|
|
3864
|
+
await writeFile4(
|
|
3865
|
+
path8.join(await gitDirectory(directory), "agentrific-project-id"),
|
|
3213
3866
|
id + "\n"
|
|
3214
3867
|
);
|
|
3215
3868
|
existing = void 0;
|
|
@@ -3242,16 +3895,16 @@ function reconnectProject(root, id, input, store2) {
|
|
|
3242
3895
|
))
|
|
3243
3896
|
throw new Error("Stop this project's running sessions before reconnecting its folder.");
|
|
3244
3897
|
const directory = await Effect13.runPromise(resolveProjectsRoot(input));
|
|
3245
|
-
if (
|
|
3898
|
+
if (path8.dirname(directory) !== await realpath(root))
|
|
3246
3899
|
throw new Error("Choose a Git repository directly inside the configured projects folder.");
|
|
3247
|
-
const filename =
|
|
3900
|
+
const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
|
|
3248
3901
|
const replacedId = await identity(directory);
|
|
3249
|
-
const project = { id, name:
|
|
3250
|
-
await
|
|
3902
|
+
const project = { id, name: path8.basename(directory), path: directory, available: true };
|
|
3903
|
+
await writeFile4(filename, id + "\n");
|
|
3251
3904
|
try {
|
|
3252
3905
|
await Effect13.runPromise(store2.relocateProject(project, previous.path, replacedId));
|
|
3253
3906
|
} catch (error) {
|
|
3254
|
-
await
|
|
3907
|
+
await writeFile4(filename, replacedId + "\n");
|
|
3255
3908
|
throw error;
|
|
3256
3909
|
}
|
|
3257
3910
|
return project;
|
|
@@ -3261,9 +3914,9 @@ function reconnectProject(root, id, input, store2) {
|
|
|
3261
3914
|
}
|
|
3262
3915
|
|
|
3263
3916
|
// src/project-git.ts
|
|
3264
|
-
import { execFile as
|
|
3265
|
-
import { promisify as
|
|
3266
|
-
var
|
|
3917
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
3918
|
+
import { promisify as promisify3 } from "node:util";
|
|
3919
|
+
var execute3 = promisify3(execFile3);
|
|
3267
3920
|
function repositoryIdentity(remote) {
|
|
3268
3921
|
const input = remote.trim();
|
|
3269
3922
|
if (!input || input.startsWith("/") || input.startsWith(".")) return null;
|
|
@@ -3277,14 +3930,14 @@ function repositoryIdentity(remote) {
|
|
|
3277
3930
|
if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return null;
|
|
3278
3931
|
if (url.search || url.hash || url.port) return null;
|
|
3279
3932
|
const host2 = url.hostname.toLowerCase();
|
|
3280
|
-
let
|
|
3281
|
-
if (!host2 || !
|
|
3282
|
-
if (["github.com", "bitbucket.org"].includes(host2))
|
|
3283
|
-
const key = `${host2}/${
|
|
3933
|
+
let path12 = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
3934
|
+
if (!host2 || !path12 || path12.includes("%") || path12.includes("//")) return null;
|
|
3935
|
+
if (["github.com", "bitbucket.org"].includes(host2)) path12 = path12.toLowerCase();
|
|
3936
|
+
const key = `${host2}/${path12}`;
|
|
3284
3937
|
return { key, display: key };
|
|
3285
3938
|
}
|
|
3286
3939
|
async function git(directory, args) {
|
|
3287
|
-
const { stdout } = await
|
|
3940
|
+
const { stdout } = await execute3("git", ["-C", directory, ...args], {
|
|
3288
3941
|
timeout: 3e3,
|
|
3289
3942
|
maxBuffer: 1024 * 1024,
|
|
3290
3943
|
env: { ...workflowEnvironment(), GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
|
|
@@ -3310,7 +3963,7 @@ async function inspect(directory) {
|
|
|
3310
3963
|
git(directory, ["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => null),
|
|
3311
3964
|
// NUL-separated records avoid ambiguous filenames/newlines. --no-renames
|
|
3312
3965
|
// keeps each entry one record, so we only need its two status characters.
|
|
3313
|
-
|
|
3966
|
+
execute3(
|
|
3314
3967
|
"git",
|
|
3315
3968
|
[
|
|
3316
3969
|
"-C",
|
|
@@ -3362,35 +4015,35 @@ function projectGitStatus(directory) {
|
|
|
3362
4015
|
|
|
3363
4016
|
// src/store.ts
|
|
3364
4017
|
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
3365
|
-
import
|
|
4018
|
+
import path9 from "node:path";
|
|
3366
4019
|
|
|
3367
4020
|
// src/push-schema.ts
|
|
3368
|
-
import { z as
|
|
3369
|
-
var PushBindingSchema =
|
|
3370
|
-
bindingId:
|
|
3371
|
-
installationId:
|
|
3372
|
-
deviceId:
|
|
4021
|
+
import { z as z18 } from "zod";
|
|
4022
|
+
var PushBindingSchema = z18.object({
|
|
4023
|
+
bindingId: z18.string().uuid(),
|
|
4024
|
+
installationId: z18.string().uuid(),
|
|
4025
|
+
deviceId: z18.string().uuid()
|
|
3373
4026
|
});
|
|
3374
|
-
var PushStateSchema =
|
|
3375
|
-
bindings:
|
|
3376
|
-
error:
|
|
4027
|
+
var PushStateSchema = z18.object({
|
|
4028
|
+
bindings: z18.array(PushBindingSchema).max(10),
|
|
4029
|
+
error: z18.string().nullable()
|
|
3377
4030
|
});
|
|
3378
4031
|
var PendingPushSchema = PushEventSchema.extend({
|
|
3379
|
-
remaining:
|
|
3380
|
-
createdAt:
|
|
3381
|
-
attempts:
|
|
3382
|
-
nextAt:
|
|
4032
|
+
remaining: z18.array(PushBindingSchema),
|
|
4033
|
+
createdAt: z18.number(),
|
|
4034
|
+
attempts: z18.number().int().nonnegative(),
|
|
4035
|
+
nextAt: z18.number()
|
|
3383
4036
|
});
|
|
3384
4037
|
|
|
3385
4038
|
// src/store.ts
|
|
3386
4039
|
import { DatabaseSync } from "node:sqlite";
|
|
3387
4040
|
import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
|
|
3388
|
-
import { z as
|
|
4041
|
+
import { z as z19 } from "zod";
|
|
3389
4042
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
3390
4043
|
var StorageError = class extends Data3.TaggedError("StorageError") {
|
|
3391
4044
|
};
|
|
3392
|
-
var PayloadRow =
|
|
3393
|
-
var EventRow =
|
|
4045
|
+
var PayloadRow = z19.object({ payload: z19.string() });
|
|
4046
|
+
var EventRow = z19.object({ sequence: z19.number(), recorded_at: z19.string(), payload: z19.string() });
|
|
3394
4047
|
var SqliteSessionStore = class {
|
|
3395
4048
|
constructor(db) {
|
|
3396
4049
|
this.db = db;
|
|
@@ -3441,7 +4094,7 @@ var SqliteSessionStore = class {
|
|
|
3441
4094
|
const row = PayloadRow.parse(
|
|
3442
4095
|
this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
|
|
3443
4096
|
);
|
|
3444
|
-
return
|
|
4097
|
+
return z19.string().uuid().parse(JSON.parse(row.payload));
|
|
3445
4098
|
});
|
|
3446
4099
|
}
|
|
3447
4100
|
saveDevice(device, tokenHash) {
|
|
@@ -3529,10 +4182,10 @@ var SqliteSessionStore = class {
|
|
|
3529
4182
|
`).all();
|
|
3530
4183
|
const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
|
|
3531
4184
|
for (const row of legacy) {
|
|
3532
|
-
const id =
|
|
4185
|
+
const id = z19.object({ id: z19.string() }).parse(row).id;
|
|
3533
4186
|
insert.run(
|
|
3534
4187
|
id,
|
|
3535
|
-
JSON.stringify({ id, name: id, path:
|
|
4188
|
+
JSON.stringify({ id, name: id, path: path9.join(root, id), available: false })
|
|
3536
4189
|
);
|
|
3537
4190
|
}
|
|
3538
4191
|
return this.db.prepare("SELECT payload FROM projects").all().map((row) => ProjectSchema.parse(JSON.parse(PayloadRow.parse(row).payload)));
|
|
@@ -3569,7 +4222,7 @@ var SqliteSessionStore = class {
|
|
|
3569
4222
|
this.db.prepare(
|
|
3570
4223
|
"UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
|
|
3571
4224
|
).run(project.path, previousPath);
|
|
3572
|
-
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${
|
|
4225
|
+
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path9.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
|
|
3573
4226
|
for (const row of this.db.prepare("SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?").all(project.id)) {
|
|
3574
4227
|
const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
|
|
3575
4228
|
if (session.workspacePath) {
|
|
@@ -3739,7 +4392,7 @@ var SqliteSessionStore = class {
|
|
|
3739
4392
|
if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
|
|
3740
4393
|
const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
|
|
3741
4394
|
if (notification) {
|
|
3742
|
-
const count =
|
|
4395
|
+
const count = z19.object({ total: z19.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
|
|
3743
4396
|
if (count.total >= 1e3)
|
|
3744
4397
|
this.db.prepare(
|
|
3745
4398
|
"DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
|
|
@@ -3785,13 +4438,13 @@ var SessionStore = class extends Context3.Service()(
|
|
|
3785
4438
|
Effect14.acquireRelease(
|
|
3786
4439
|
Effect14.try({
|
|
3787
4440
|
try: () => {
|
|
3788
|
-
mkdirSync2(
|
|
4441
|
+
mkdirSync2(path9.dirname(filename), { recursive: true });
|
|
3789
4442
|
const db = new DatabaseSync(filename);
|
|
3790
4443
|
chmodSync(filename, 384);
|
|
3791
4444
|
for (const suffix of ["-wal", "-shm"])
|
|
3792
4445
|
if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
|
|
3793
4446
|
try {
|
|
3794
|
-
const schema =
|
|
4447
|
+
const schema = z19.object({ user_version: z19.number() }).parse(db.prepare("PRAGMA user_version").get());
|
|
3795
4448
|
if (schema.user_version > 1)
|
|
3796
4449
|
throw new Error("This database requires a newer Agentrific server.");
|
|
3797
4450
|
db.exec(`
|
|
@@ -3826,7 +4479,7 @@ var SessionStore = class extends Context3.Service()(
|
|
|
3826
4479
|
|
|
3827
4480
|
// src/worktree-cleanup.ts
|
|
3828
4481
|
import { lstat as lstat2, rm as rm2 } from "node:fs/promises";
|
|
3829
|
-
import
|
|
4482
|
+
import path10 from "node:path";
|
|
3830
4483
|
import { Effect as Effect15 } from "effect";
|
|
3831
4484
|
var WorktreeCleanup = class {
|
|
3832
4485
|
constructor(store2, root, projectsRoot) {
|
|
@@ -3839,12 +4492,12 @@ var WorktreeCleanup = class {
|
|
|
3839
4492
|
projectsRoot;
|
|
3840
4493
|
async remove(session, discardChanges = true) {
|
|
3841
4494
|
if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
|
|
3842
|
-
const workspace =
|
|
4495
|
+
const workspace = path10.resolve(session.workspacePath);
|
|
3843
4496
|
const info = await lstat2(workspace).catch((error) => {
|
|
3844
4497
|
if (error.code === "ENOENT") return void 0;
|
|
3845
4498
|
throw error;
|
|
3846
4499
|
});
|
|
3847
|
-
if (workspace !==
|
|
4500
|
+
if (workspace !== path10.join(this.root, session.id) || path10.basename(session.id) !== session.id) {
|
|
3848
4501
|
if (!info) return;
|
|
3849
4502
|
throw new Error("Refusing to remove a worktree outside the managed workspaces directory.");
|
|
3850
4503
|
}
|
|
@@ -3882,9 +4535,9 @@ var WorktreeCleanup = class {
|
|
|
3882
4535
|
}
|
|
3883
4536
|
};
|
|
3884
4537
|
async function removeRunFiles(dataRoot2, sessionId) {
|
|
3885
|
-
if (
|
|
4538
|
+
if (path10.basename(sessionId) !== sessionId || sessionId === "." || sessionId === "..")
|
|
3886
4539
|
throw new Error("Invalid session directory.");
|
|
3887
|
-
await rm2(
|
|
4540
|
+
await rm2(path10.join(dataRoot2, "pi", sessionId), { recursive: true, force: true });
|
|
3888
4541
|
}
|
|
3889
4542
|
|
|
3890
4543
|
// src/repo-chat.ts
|
|
@@ -3922,17 +4575,17 @@ function openRepoChat(store2, projectId, workspacePath, defaultModel, reset = fa
|
|
|
3922
4575
|
// src/index.ts
|
|
3923
4576
|
var host = process.env.AGENTRIFIC_HOST ?? "127.0.0.1";
|
|
3924
4577
|
var port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
|
|
3925
|
-
var defaultProjectsRoot =
|
|
3926
|
-
var worktreesRoot =
|
|
3927
|
-
process.env.AGENTRIFIC_WORKTREES_DIR ??
|
|
4578
|
+
var defaultProjectsRoot = path11.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
|
|
4579
|
+
var worktreesRoot = path11.resolve(
|
|
4580
|
+
process.env.AGENTRIFIC_WORKTREES_DIR ?? path11.join(homedir4(), ".agentrific", "workspaces")
|
|
3928
4581
|
);
|
|
3929
|
-
var execFileAsync =
|
|
4582
|
+
var execFileAsync = promisify4(execFile4);
|
|
3930
4583
|
if (host !== "127.0.0.1" && host !== "::1")
|
|
3931
4584
|
throw new Error("Bind Agentrific to loopback and expose it with Tailscale Serve or cloudflared.");
|
|
3932
4585
|
var app = Fastify({
|
|
3933
4586
|
logger: {
|
|
3934
4587
|
stream: process.env.AGENTRIFIC_INTERACTIVE === "1" && process.stdout.isTTY ? pretty({
|
|
3935
|
-
colorize:
|
|
4588
|
+
colorize: styleText2("green", "ready") !== "ready",
|
|
3936
4589
|
translateTime: "SYS:HH:MM:ss",
|
|
3937
4590
|
ignore: "pid,hostname,reqId",
|
|
3938
4591
|
singleLine: true,
|
|
@@ -3947,9 +4600,9 @@ var app = Fastify({
|
|
|
3947
4600
|
}
|
|
3948
4601
|
}
|
|
3949
4602
|
});
|
|
3950
|
-
var dataRoot =
|
|
4603
|
+
var dataRoot = path11.resolve(process.env.AGENTRIFIC_DATA_DIR ?? "./data");
|
|
3951
4604
|
var releaseLock = await acquireServerLock(dataRoot);
|
|
3952
|
-
var effects = ManagedRuntime.make(SessionStore.layer(
|
|
4605
|
+
var effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
|
|
3953
4606
|
var store = await effects.runPromise(SessionStore);
|
|
3954
4607
|
var security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
|
|
3955
4608
|
var sandboxPushUrl = process.env.AGENTRIFIC_PUSH_SANDBOX === "1" ? PUSH_SANDBOX_API_URL : void 0;
|
|
@@ -3981,7 +4634,7 @@ await app.register(cors, {
|
|
|
3981
4634
|
await registerSecurity(app, security, serverVersion, allowedOrigins);
|
|
3982
4635
|
registerPushRoutes(app, push, security);
|
|
3983
4636
|
var attachmentEffects = ManagedRuntime.make(
|
|
3984
|
-
Attachments.layer(
|
|
4637
|
+
Attachments.layer(path11.join(dataRoot, "attachments"), store)
|
|
3985
4638
|
);
|
|
3986
4639
|
var attachments = await attachmentEffects.runPromise(Attachments);
|
|
3987
4640
|
await Effect17.runPromise(attachments.cleanup());
|
|
@@ -4046,7 +4699,10 @@ function publish(event, workflowUpdate = false) {
|
|
|
4046
4699
|
else client.send(message);
|
|
4047
4700
|
}
|
|
4048
4701
|
}
|
|
4049
|
-
var
|
|
4702
|
+
var claudeCode = new ClaudeCode();
|
|
4703
|
+
var modelRuntime = await ModelRuntime.create();
|
|
4704
|
+
await claudeCode.refresh(modelRuntime);
|
|
4705
|
+
var models = new ServerModels(modelRuntime, store, claudeCode);
|
|
4050
4706
|
var delivery = new DeliveryCoordinator(store, {
|
|
4051
4707
|
publish: (event) => publish(event, true),
|
|
4052
4708
|
draft: deliveryWriter(models),
|
|
@@ -4065,7 +4721,7 @@ var agentEffects = ManagedRuntime.make(
|
|
|
4065
4721
|
createPiFactory(attachments, models, delivery),
|
|
4066
4722
|
publish,
|
|
4067
4723
|
worktreesRoot,
|
|
4068
|
-
|
|
4724
|
+
path11.join(dataRoot, "pi"),
|
|
4069
4725
|
(sessionId, messageId) => Effect17.runSync(store.hasMessage(sessionId, messageId)),
|
|
4070
4726
|
(session) => delivery.settled(session.id)
|
|
4071
4727
|
)
|
|
@@ -4240,7 +4896,7 @@ app.get("/api/projects/:id", async (request, reply) => {
|
|
|
4240
4896
|
return project ?? reply.code(404).send({ error: "Project not found on this server." });
|
|
4241
4897
|
});
|
|
4242
4898
|
app.post("/api/projects/:id/reconnect", async (request, reply) => {
|
|
4243
|
-
const parsed =
|
|
4899
|
+
const parsed = z20.object({ path: z20.string().trim().min(1).max(4096) }).safeParse(request.body);
|
|
4244
4900
|
if (!parsed.success)
|
|
4245
4901
|
return reply.code(400).send({ error: "Enter the project's new folder path." });
|
|
4246
4902
|
const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
@@ -4326,7 +4982,7 @@ app.patch("/api/tasks/:id", async (request, reply) => {
|
|
|
4326
4982
|
}
|
|
4327
4983
|
});
|
|
4328
4984
|
app.post("/api/tasks/:id/discard-unrun", async (request, reply) => {
|
|
4329
|
-
const parsed =
|
|
4985
|
+
const parsed = z20.object({ updatedAt: z20.string().datetime() }).safeParse(request.body);
|
|
4330
4986
|
if (!parsed.success) return reply.code(400).send({ error: "The task revision is required." });
|
|
4331
4987
|
try {
|
|
4332
4988
|
await deleteTask(store, request.params.id, void 0, parsed.data.updatedAt);
|
|
@@ -4396,9 +5052,9 @@ app.post(
|
|
|
4396
5052
|
}
|
|
4397
5053
|
})
|
|
4398
5054
|
);
|
|
4399
|
-
var HistoryQuery =
|
|
4400
|
-
after:
|
|
4401
|
-
limit:
|
|
5055
|
+
var HistoryQuery = z20.object({
|
|
5056
|
+
after: z20.coerce.number().int().nonnegative().default(0),
|
|
5057
|
+
limit: z20.coerce.number().int().min(1).max(1e3).default(500)
|
|
4402
5058
|
});
|
|
4403
5059
|
app.get("/api/sessions/:id/events", async (request, reply) => {
|
|
4404
5060
|
const query = HistoryQuery.safeParse(request.query);
|
|
@@ -4433,18 +5089,18 @@ async function resolveSessionProject(projectId) {
|
|
|
4433
5089
|
return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
|
|
4434
5090
|
const projectPath = await realpath2(project.path);
|
|
4435
5091
|
const resolvedRoot = await realpath2(projectsRoot);
|
|
4436
|
-
if (
|
|
5092
|
+
if (path11.dirname(projectPath) !== resolvedRoot)
|
|
4437
5093
|
return { error: "Project path is outside projects root.", status: 400 };
|
|
4438
5094
|
return { projectPath };
|
|
4439
5095
|
}
|
|
4440
5096
|
async function resolveSessionWorkspace(id, gitFlow, projectPath) {
|
|
4441
|
-
const worktreePath =
|
|
5097
|
+
const worktreePath = path11.join(worktreesRoot, id);
|
|
4442
5098
|
const branch = gitFlow ? `agentrific/${id.slice(0, 8)}` : null;
|
|
4443
5099
|
const useProjectCheckout = gitFlow?.workspace === "checkout";
|
|
4444
5100
|
const workspacePath = !gitFlow || useProjectCheckout ? projectPath : worktreePath;
|
|
4445
5101
|
if (useProjectCheckout) {
|
|
4446
5102
|
const serverPath = await realpath2(process.cwd());
|
|
4447
|
-
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${
|
|
5103
|
+
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path11.sep}`))
|
|
4448
5104
|
return {
|
|
4449
5105
|
error: "The server runs from this project. Choose a worktree for this task.",
|
|
4450
5106
|
status: 409
|
|
@@ -4536,7 +5192,7 @@ app.post("/api/projects/:id/chat", async (request, reply) => {
|
|
|
4536
5192
|
const location = await resolveSessionProject(request.params.id);
|
|
4537
5193
|
if (location.error !== void 0)
|
|
4538
5194
|
return reply.code(location.status).send({ error: location.error });
|
|
4539
|
-
const parsed =
|
|
5195
|
+
const parsed = z20.object({ reset: z20.boolean().optional() }).safeParse(request.body ?? {});
|
|
4540
5196
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
4541
5197
|
try {
|
|
4542
5198
|
return await openRepoChat(
|
|
@@ -4560,7 +5216,7 @@ app.post("/api/sessions", async (request, reply) => {
|
|
|
4560
5216
|
if (!result.session) return reply.code(result.status).send({ error: result.error });
|
|
4561
5217
|
return reply.code(201).send(result.session);
|
|
4562
5218
|
});
|
|
4563
|
-
var StartTaskBody =
|
|
5219
|
+
var StartTaskBody = z20.object({ prompt: z20.string().trim().max(2e4).optional() }).default({});
|
|
4564
5220
|
function startTask(id, instructionsOverride, automated = false) {
|
|
4565
5221
|
return withTaskLock(
|
|
4566
5222
|
"agent-dispatch",
|
|
@@ -4638,7 +5294,7 @@ app.patch("/api/automation", async (request, reply) => {
|
|
|
4638
5294
|
return automation.configure(parsed.data);
|
|
4639
5295
|
});
|
|
4640
5296
|
app.patch("/api/automation/queue", async (request, reply) => {
|
|
4641
|
-
const parsed =
|
|
5297
|
+
const parsed = z20.object({ taskId: z20.string().min(1), beforeId: z20.string().min(1).nullable() }).safeParse(request.body);
|
|
4642
5298
|
if (!parsed.success)
|
|
4643
5299
|
return reply.code(400).send({ error: "Choose a task and a queue position." });
|
|
4644
5300
|
try {
|
|
@@ -4701,15 +5357,35 @@ attachEventServer(app, security, allowedOrigins, () => ({
|
|
|
4701
5357
|
sessions: Effect17.runSync(store.list())
|
|
4702
5358
|
}));
|
|
4703
5359
|
function localServerUrl() {
|
|
4704
|
-
const address =
|
|
5360
|
+
const address = z20.object({ port: z20.number() }).safeParse(app.server.address());
|
|
4705
5361
|
return address.success ? `http://${host}:${address.data.port}` : void 0;
|
|
4706
5362
|
}
|
|
5363
|
+
var phoneOrigin;
|
|
5364
|
+
var phone = process.env.AGENTRIFIC_DESKTOP_SERVER === "1" ? new DesktopPhoneConnection(
|
|
5365
|
+
() => {
|
|
5366
|
+
const origin = localServerUrl();
|
|
5367
|
+
if (!origin) throw new Error("Start the local server before connecting your phone.");
|
|
5368
|
+
return origin;
|
|
5369
|
+
},
|
|
5370
|
+
(url) => {
|
|
5371
|
+
if (phoneOrigin) {
|
|
5372
|
+
const index = allowedOrigins.indexOf(phoneOrigin);
|
|
5373
|
+
if (index >= 0) allowedOrigins.splice(index, 1);
|
|
5374
|
+
}
|
|
5375
|
+
phoneOrigin = url || void 0;
|
|
5376
|
+
process.env.AGENTRIFIC_PUBLIC_URL = url || "";
|
|
5377
|
+
if (url && !allowedOrigins.includes(url)) allowedOrigins.push(url);
|
|
5378
|
+
},
|
|
5379
|
+
path11.join(dataRoot, "phone-connection")
|
|
5380
|
+
) : void 0;
|
|
4707
5381
|
var closeControl = await startControl(
|
|
4708
5382
|
dataRoot,
|
|
4709
5383
|
security,
|
|
4710
5384
|
() => ({
|
|
4711
5385
|
serverId: security.serverId,
|
|
4712
5386
|
publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
|
|
5387
|
+
phoneProvider: phone?.snapshot().provider,
|
|
5388
|
+
phoneProgress: phone?.snapshot().progress,
|
|
4713
5389
|
localUrl: localServerUrl(),
|
|
4714
5390
|
serverVersion,
|
|
4715
5391
|
connectedDevices: new Set(security.sockets.values()).size,
|
|
@@ -4722,10 +5398,12 @@ var closeControl = await startControl(
|
|
|
4722
5398
|
async () => {
|
|
4723
5399
|
await app.close();
|
|
4724
5400
|
process.exit(0);
|
|
4725
|
-
}
|
|
5401
|
+
},
|
|
5402
|
+
phone
|
|
4726
5403
|
);
|
|
4727
5404
|
push.start();
|
|
4728
5405
|
app.addHook("onClose", async () => {
|
|
5406
|
+
await phone?.close();
|
|
4729
5407
|
await push.close();
|
|
4730
5408
|
maintenance.enter();
|
|
4731
5409
|
stopUpdateDiscovery();
|