@agentrific/server 0.5.2 → 0.6.0

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