@agentrific/server 0.5.2 → 0.7.0

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