@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/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 homedir2 } from "node:os";
892
- import path9 from "node:path";
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 = path3.join(directory, "control.sock");
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((resolve, reject) => {
1158
+ await new Promise((resolve2, reject) => {
1100
1159
  server.once("error", reject);
1101
- server.listen(filename, resolve);
1160
+ server.listen(filename, resolve2);
1102
1161
  });
1103
1162
  await chmod(filename, 384);
1104
1163
  return async () => {
1105
1164
  await new Promise(
1106
- (resolve, reject) => server.close((error) => error ? reject(error) : resolve())
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.5.1",
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) => createHash("sha256").update(value).digest("hex");
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, path10) {
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: path10 });
1326
+ this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path12 });
1257
1327
  return value;
1258
1328
  }
1259
- redeemTicket(value, path10, consume = true) {
1329
+ redeemTicket(value, path12, consume = true) {
1260
1330
  this.prune();
1261
1331
  const ticket = this.tickets.get(value);
1262
- if (!ticket || ticket.path !== path10) return void 0;
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 path10 = new URL(request.url, "http://localhost");
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 (path10.pathname === "/health" || path10.pathname === "/api/auth/pair" && request.method === "POST")
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(path10.pathname);
1285
- const ticket = path10.searchParams.get("ticket");
1286
- path10.searchParams.delete("ticket");
1287
- if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path10.pathname + path10.search, false)))
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((resolve) => setTimeout(resolve, 50));
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 path10 = request.url.split("?")[0];
1908
+ const path12 = request.url.split("?")[0];
1394
1909
  if (["GET", "HEAD", "OPTIONS"].includes(request.method)) return;
1395
- const allowed = path10.startsWith("/api/auth/") || /^\/api\/sessions\/[^/]+\/stop$/.test(path10);
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 z7 } from "zod";
1923
+ import { z as z9 } from "zod";
1409
1924
  import { Effect as Effect5 } from "effect";
1410
- var RegistryRelease = z7.object({
1411
- version: z7.string().refine((value) => semver.valid(value) !== null)
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 z17 } from "zod";
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 path4 from "node:path";
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 ?? path4.join(this.worktreesRoot, session.id),
1802
- path4.join(this.sessionsRoot, session.id),
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 z9 } from "zod";
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 z8 } from "zod";
2086
- var DeliveryJobSchema = z8.object({
2087
- kind: z8.literal("delivery"),
2088
- sessionId: z8.string(),
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: z8.enum([
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: z8.number().int().positive(),
2101
- commitSha: z8.string().optional(),
2102
- title: z8.string(),
2103
- body: z8.string(),
2104
- logs: z8.string().default(""),
2105
- failed: z8.boolean().default(false),
2106
- result: z8.enum(["merged", "pr_opened", "no_changes"]).optional(),
2107
- url: z8.string().optional(),
2108
- message: z8.string().optional()
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 = z8.object({
2111
- kind: z8.literal("preparation"),
2112
- sessionId: z8.string(),
2113
- projectPath: z8.string(),
2625
+ var PreparationJobSchema = z10.object({
2626
+ kind: z10.literal("preparation"),
2627
+ sessionId: z10.string(),
2628
+ projectPath: z10.string(),
2114
2629
  input: CreateSessionSchema,
2115
- stage: z8.enum(["creating", "installing", "ready", "failed"]),
2116
- logs: z8.string().default("")
2630
+ stage: z10.enum(["creating", "installing", "ready", "failed"]),
2631
+ logs: z10.string().default("")
2117
2632
  });
2118
- var WorkflowJobSchema = z8.discriminatedUnion("kind", [
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 = z9.array(
2415
- z9.object({
2416
- url: z9.string().url(),
2417
- state: z9.string(),
2418
- headRefOid: z9.string().optional()
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 mkdir3, readFile as readFile3 } from "node:fs/promises";
2460
- import path5 from "node:path";
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 z10 } from "zod";
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 mkdir3(path5.dirname(workspace), { recursive: true });
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 readFile3(path5.join(workspace, "package.json"), "utf8").catch(
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 = z10.object({ packageManager: z10.string().optional() }).parse(JSON.parse(packageFile));
2581
- const has = async (name) => readFile3(path5.join(workspace, name)).then(
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 z11 } from "zod";
2663
- var DraftSchema = z11.object({ title: z11.string().trim().min(1).max(120), body: z11.string().trim().min(1).max(1e4) }).strict();
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 z13 } from "zod";
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 mkdir4, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
2856
- import { z as z12 } from "zod";
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 = z12.object({
2864
- details: z12.object({ patch: z12.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
3378
+ var EditResultSchema = z14.object({
3379
+ details: z14.object({ patch: z14.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
2865
3380
  });
2866
- function completedFileChange(result, path10) {
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 (path10 && details?.patch) return boundFileChange({ path: path10, patch: details.patch });
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 mkdir4(dir, { recursive: true });
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 (path10, content) => {
2893
- const previous = await readFile4(path10, "utf8").catch((error) => {
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 writeFile2(path10, content, "utf8");
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 = z13.object({
2915
- content: z13.array(z13.object({ type: z13.string(), text: z13.string().optional() })).optional()
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 = z13.object({ path: z13.string() });
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 stat2, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
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 z14 } from "zod";
3102
- import { constants } from "node:fs";
3103
- import { homedir } from "node:os";
3104
- import path6 from "node:path";
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 === "~" ? homedir() : trimmed.startsWith("~/") ? path6.join(homedir(), trimmed.slice(2)) : trimmed;
3111
- if (!path6.isAbsolute(expanded))
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 stat2(resolved)).isDirectory())
3629
+ if (!(await stat3(resolved)).isDirectory())
3115
3630
  throw new Error("The projects folder must be a directory.");
3116
- await access(resolved, constants.R_OK | constants.X_OK);
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 = z14.string().trim().min(1).max(255);
3648
+ var ProjectId = z16.string().trim().min(1).max(255);
3134
3649
  async function gitDirectory(directory) {
3135
- const location = path6.join(directory, ".git");
3136
- const info = await stat2(location);
3650
+ const location = path8.join(directory, ".git");
3651
+ const info = await stat3(location);
3137
3652
  if (info.isDirectory()) return location;
3138
- const contents = await readFile5(location, "utf8");
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(path6.resolve(directory, match[1].trim()));
3142
- if (!(await stat2(resolved)).isDirectory()) throw new Error("Git directory is missing.");
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 = path6.join(await gitDirectory(directory), "agentrific-project-id");
3661
+ const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
3147
3662
  try {
3148
- return ProjectId.parse(await readFile5(filename, "utf8"));
3663
+ return ProjectId.parse(await readFile6(filename, "utf8"));
3149
3664
  } catch (error) {
3150
- if (!z14.object({ code: z14.literal("ENOENT") }).safeParse(error).success) throw error;
3151
- const legacyFilename = path6.join(await gitDirectory(directory), "chatplans-project-id");
3152
- const legacy = await readFile5(legacyFilename, "utf8").catch((cause) => {
3153
- if (!z14.object({ code: z14.literal("ENOENT") }).safeParse(cause).success) throw cause;
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 writeFile3(filename, id + "\n", { flag: "wx" });
3673
+ await writeFile4(filename, id + "\n", { flag: "wx" });
3159
3674
  return id;
3160
3675
  } catch (writeError) {
3161
- if (!z14.object({ code: z14.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
3162
- return ProjectId.parse(await readFile5(filename, "utf8"));
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 = path6.join(root, entry.name);
3176
- if (!await stat2(path6.join(directory, ".git")).catch(() => null)) continue;
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 stat2(path6.join(existing.path, ".git")).catch(() => null)) {
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 writeFile3(
3183
- path6.join(await gitDirectory(directory), "agentrific-project-id"),
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 (path6.dirname(directory) !== await realpath(root))
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 = path6.join(await gitDirectory(directory), "agentrific-project-id");
3733
+ const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
3219
3734
  const replacedId = await identity(directory);
3220
- const project = { id, name: path6.basename(directory), path: directory, available: true };
3221
- await writeFile3(filename, id + "\n");
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 writeFile3(filename, replacedId + "\n");
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 execute = promisify2(execFile2);
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 path10 = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
3252
- if (!host2 || !path10 || path10.includes("%") || path10.includes("//")) return null;
3253
- if (["github.com", "bitbucket.org"].includes(host2)) path10 = path10.toLowerCase();
3254
- const key = `${host2}/${path10}`;
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 execute("git", ["-C", directory, ...args], {
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
- execute(
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 path7 from "node:path";
3851
+ import path9 from "node:path";
3337
3852
 
3338
3853
  // src/push-schema.ts
3339
- import { z as z15 } from "zod";
3340
- var PushBindingSchema = z15.object({
3341
- bindingId: z15.string().uuid(),
3342
- installationId: z15.string().uuid(),
3343
- deviceId: z15.string().uuid()
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 = z15.object({
3346
- bindings: z15.array(PushBindingSchema).max(10),
3347
- error: z15.string().nullable()
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: z15.array(PushBindingSchema),
3351
- createdAt: z15.number(),
3352
- attempts: z15.number().int().nonnegative(),
3353
- nextAt: z15.number()
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 z16 } from "zod";
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 = z16.object({ payload: z16.string() });
3364
- var EventRow = z16.object({ sequence: z16.number(), recorded_at: z16.string(), payload: z16.string() });
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 z16.string().uuid().parse(JSON.parse(row.payload));
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 = z16.object({ id: z16.string() }).parse(row).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: path7.join(root, id), available: false })
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}${path7.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
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 = z16.object({ total: z16.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
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(path7.dirname(filename), { recursive: true });
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 = z16.object({ user_version: z16.number() }).parse(db.prepare("PRAGMA user_version").get());
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 path8 from "node:path";
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 = path8.resolve(session.workspacePath);
3814
- const info = await lstat(workspace).catch((error) => {
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 !== path8.join(this.root, session.id) || path8.basename(session.id) !== session.id) {
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 (path8.basename(sessionId) !== sessionId || sessionId === "." || sessionId === "..")
4371
+ if (path10.basename(sessionId) !== sessionId || sessionId === "." || sessionId === "..")
3857
4372
  throw new Error("Invalid session directory.");
3858
- await rm2(path8.join(dataRoot2, "pi", sessionId), { recursive: true, force: true });
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 = path9.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
3897
- var worktreesRoot = path9.resolve(
3898
- process.env.AGENTRIFIC_WORKTREES_DIR ?? path9.join(homedir2(), ".agentrific", "workspaces")
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: styleText("green", "ready") !== "ready",
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 = path9.resolve(process.env.AGENTRIFIC_DATA_DIR ?? "./data");
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(path9.join(dataRoot, "sessions.sqlite")));
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(path9.join(dataRoot, "attachments"), store)
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
- path9.join(dataRoot, "pi"),
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 = z17.object({ path: z17.string().trim().min(1).max(4096) }).safeParse(request.body);
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 = z17.object({ updatedAt: z17.string().datetime() }).safeParse(request.body);
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 = z17.object({
4370
- after: z17.coerce.number().int().nonnegative().default(0),
4371
- limit: z17.coerce.number().int().min(1).max(1e3).default(500)
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 (path9.dirname(projectPath) !== resolvedRoot)
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 = path9.join(worktreesRoot, id);
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}${path9.sep}`))
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 = z17.object({ reset: z17.boolean().optional() }).safeParse(request.body ?? {});
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 = z17.object({ prompt: z17.string().trim().max(2e4).optional() }).default({});
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 = z17.object({ taskId: z17.string().min(1), beforeId: z17.string().min(1).nullable() }).safeParse(request.body);
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();