@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/cli.js CHANGED
@@ -30,7 +30,7 @@ var init_package = __esm({
30
30
  "package.json"() {
31
31
  package_default = {
32
32
  name: "@agentrific/server",
33
- version: "0.5.2",
33
+ version: "0.7.0",
34
34
  description: "Self-hosted control plane for long-running coding agents",
35
35
  keywords: [
36
36
  "agentrific",
@@ -56,20 +56,28 @@ var init_package = __esm({
56
56
  registry: "https://registry.npmjs.org"
57
57
  },
58
58
  scripts: {
59
- dev: "tsx watch src/index.ts",
59
+ dev: "node scripts/build-claude-bridge.mjs && tsx watch src/index.ts",
60
60
  start: "node dist/index.js",
61
61
  typecheck: "tsc --noEmit",
62
62
  build: "node scripts/build.mjs",
63
- cli: "AGENTRIFIC_DATA_DIR=./data tsx src/cli.ts"
63
+ cli: "node scripts/build-claude-bridge.mjs && AGENTRIFIC_DATA_DIR=./data tsx src/cli.ts",
64
+ "build:claude": "node scripts/build-claude-bridge.mjs"
64
65
  },
65
66
  dependencies: {
67
+ "@anthropic-ai/claude-agent-sdk": "0.3.296",
68
+ "@anthropic-ai/sdk": "^0.124.0",
69
+ "@earendil-works/pi-ai": "1.0.0",
66
70
  "@earendil-works/pi-coding-agent": "1.0.0",
67
71
  "@fastify/cors": "^11.3.0",
68
72
  "@fastify/multipart": "^10.1.2",
69
73
  "@inquirer/select": "^5.2.5",
74
+ "@modelcontextprotocol/sdk": "^1.29.0",
75
+ "cc-session-io": "^0.4.0",
76
+ "change-case": "^5.4.4",
70
77
  diff: "^9.0.0",
71
78
  effect: "4.0.0-rc.117",
72
79
  fastify: "^5.6.2",
80
+ "pi-claude-agent-sdk": "0.9.2",
73
81
  "pino-pretty": "^13.1.3",
74
82
  "qrcode-terminal": "^0.12.0",
75
83
  semver: "^7.8.5",
@@ -80,6 +88,7 @@ var init_package = __esm({
80
88
  },
81
89
  devDependencies: {
82
90
  "@agentrific/contracts": "workspace:*",
91
+ "@agentrific/opentunnel-runtime": "workspace:*",
83
92
  "@jest/globals": "30.5.2",
84
93
  "@types/node": "^22.19.1",
85
94
  "@types/qrcode-terminal": "^0.12.2",
@@ -650,7 +659,7 @@ async function acquireServerLock(directory) {
650
659
  await unlink2(filename);
651
660
  };
652
661
  }
653
- async function startControl(directory, security2, status, maintenance2, shutdown) {
662
+ async function startControl(directory, security2, status, maintenance2, shutdown, phone2) {
654
663
  const filename = controlSocketPath(directory);
655
664
  const socketDirectory = path2.dirname(filename);
656
665
  await mkdir2(socketDirectory, { recursive: true, mode: 448 });
@@ -677,6 +686,29 @@ async function startControl(directory, security2, status, maintenance2, shutdown
677
686
  const request = ControlRequest.parse(JSON.parse(buffer.split("\n")[0]));
678
687
  let result;
679
688
  switch (request.command) {
689
+ case "phone-options":
690
+ if (!phone2) throw new Error("Phone connections are managed by the desktop app.");
691
+ socket.setTimeout(18e4);
692
+ result = await phone2.options();
693
+ break;
694
+ case "phone-start": {
695
+ if (!phone2) throw new Error("Phone connections are managed by the desktop app.");
696
+ maintenance2?.assertReady();
697
+ socket.setTimeout(18e4);
698
+ const abort = new AbortController();
699
+ const disconnected = () => abort.abort();
700
+ socket.once("close", disconnected);
701
+ try {
702
+ result = await phone2.start(request.provider, abort.signal);
703
+ } finally {
704
+ socket.removeListener("close", disconnected);
705
+ }
706
+ break;
707
+ }
708
+ case "phone-stop":
709
+ if (!phone2) throw new Error("Phone connections are managed by the desktop app.");
710
+ result = await phone2.close();
711
+ break;
680
712
  case "shutdown":
681
713
  if (!shutdown || !maintenance2?.status().drained)
682
714
  throw new Error("Drain the server before shutdown");
@@ -746,12 +778,13 @@ function controlRequest(directory, command) {
746
778
  });
747
779
  });
748
780
  }
749
- var MaintenanceSchema, ControlStatusSchema, ControlResultSchema, ControlRequest;
781
+ var ProviderSchema, MaintenanceSchema, ControlStatusSchema, ControlResultSchema, ControlRequest;
750
782
  var init_control = __esm({
751
783
  "src/control.ts"() {
752
784
  "use strict";
753
785
  init_control_socket();
754
786
  init_src();
787
+ ProviderSchema = z3.enum(["opentunnel", "tailscale", "cloudflared"]);
755
788
  MaintenanceSchema = z3.object({
756
789
  active: z3.boolean(),
757
790
  drained: z3.boolean(),
@@ -760,6 +793,8 @@ var init_control = __esm({
760
793
  ControlStatusSchema = z3.object({
761
794
  serverId: z3.string().uuid(),
762
795
  publicUrl: z3.string().optional(),
796
+ phoneProvider: ProviderSchema.nullable().optional(),
797
+ phoneProgress: z3.string().nullable().optional(),
763
798
  localUrl: z3.string().optional(),
764
799
  serverVersion: z3.string().optional(),
765
800
  connectedDevices: z3.number().optional(),
@@ -769,6 +804,12 @@ var init_control = __esm({
769
804
  maintenance: MaintenanceSchema.optional()
770
805
  });
771
806
  ControlResultSchema = z3.union([
807
+ z3.object({
808
+ provider: ProviderSchema.nullable(),
809
+ publicUrl: z3.string().url().nullable(),
810
+ progress: z3.string().nullable().optional()
811
+ }),
812
+ z3.array(z3.object({ provider: ProviderSchema, available: z3.boolean(), reason: z3.string() })),
772
813
  z3.object({
773
814
  pairingId: z3.string(),
774
815
  pin: z3.string(),
@@ -782,6 +823,9 @@ var init_control = __esm({
782
823
  MaintenanceSchema
783
824
  ]);
784
825
  ControlRequest = z3.discriminatedUnion("command", [
826
+ z3.object({ command: z3.literal("phone-options") }),
827
+ z3.object({ command: z3.literal("phone-start"), provider: ProviderSchema }),
828
+ z3.object({ command: z3.literal("phone-stop") }),
785
829
  z3.object({ command: z3.literal("pair") }),
786
830
  z3.object({ command: z3.literal("devices") }),
787
831
  z3.object({ command: z3.literal("status") }),
@@ -855,6 +899,7 @@ function printHelp() {
855
899
  for (const [option, description] of [
856
900
  ["--data-dir <path>", "Use a specific server data directory"],
857
901
  ["--public-url <https-url>", "Use your tunnel URL without prompting"],
902
+ ["--connection <method>", "opentunnel (default), tailscale, cloudflared, existing, or local"],
858
903
  ["--no-interactive", "Disable prompts and keyboard shortcuts"],
859
904
  ["--show-payload", "Print manual JSON with the pairing QR code"],
860
905
  ["--timeout-ms <0-60000>", "Set the maintenance drain timeout"],
@@ -862,7 +907,9 @@ function printHelp() {
862
907
  ["--help", "Show this help"]
863
908
  ])
864
909
  console.log(` ${styleText("cyan", option.padEnd(34))}${description}`);
865
- console.log("\n Connect using Tailscale Serve, cloudflared, or another HTTPS tunnel.\n");
910
+ console.log(
911
+ "\n OpenTunnel is built in. Tailscale, cloudflared and existing HTTPS tunnels are also supported.\n"
912
+ );
866
913
  }
867
914
  var init_cli_output = __esm({
868
915
  "src/cli-output.ts"() {
@@ -992,8 +1039,402 @@ var init_pairing = __esm({
992
1039
  }
993
1040
  });
994
1041
 
995
- // ../../packages/contracts/src/push.ts
1042
+ // src/tunnels.ts
1043
+ import { spawn } from "node:child_process";
1044
+ import { access, stat } from "node:fs/promises";
1045
+ import { constants } from "node:fs";
1046
+ import { homedir as homedir2 } from "node:os";
1047
+ import path3 from "node:path";
1048
+ import { z as z5 } from "zod";
1049
+ function signalChild(child, signal) {
1050
+ if (!child.pid) return;
1051
+ if (process.platform === "win32") {
1052
+ child.kill(signal);
1053
+ return;
1054
+ }
1055
+ try {
1056
+ process.kill(-child.pid, signal);
1057
+ } catch (error) {
1058
+ if (!z5.object({ code: z5.literal("ESRCH") }).safeParse(error).success) child.kill(signal);
1059
+ }
1060
+ }
1061
+ function execute2(file, args, timeoutMs, env = process.env) {
1062
+ return new Promise((resolve2, reject) => {
1063
+ const child = spawn(file, args, {
1064
+ env,
1065
+ detached: process.platform !== "win32",
1066
+ stdio: ["ignore", "pipe", "pipe"]
1067
+ });
1068
+ let stdout = "";
1069
+ let stderr = "";
1070
+ const timeout = setTimeout(() => {
1071
+ signalChild(child, "SIGKILL");
1072
+ reject(new Error("Command probe timed out."));
1073
+ }, timeoutMs);
1074
+ child.stdout?.on("data", (data) => {
1075
+ stdout += data.toString();
1076
+ checkOutput();
1077
+ });
1078
+ child.stderr?.on("data", (data) => {
1079
+ stderr += data.toString();
1080
+ checkOutput();
1081
+ });
1082
+ child.once("error", (error) => {
1083
+ clearTimeout(timeout);
1084
+ reject(error);
1085
+ });
1086
+ child.once("exit", () => signalChild(child, "SIGKILL"));
1087
+ child.once("close", (code) => {
1088
+ clearTimeout(timeout);
1089
+ if (code !== 0) reject(new Error("Command probe failed."));
1090
+ else resolve2({ stdout, stderr });
1091
+ });
1092
+ function checkOutput() {
1093
+ if (stdout.length + stderr.length <= 1048576) return;
1094
+ signalChild(child, "SIGKILL");
1095
+ reject(new Error("Command probe output exceeded its limit."));
1096
+ }
1097
+ });
1098
+ }
1099
+ async function findExecutable(name) {
1100
+ const candidates = (process.env.PATH ?? "").split(path3.delimiter).map((directory) => path3.resolve(directory, name));
1101
+ if (process.platform === "darwin") {
1102
+ candidates.push(`/opt/homebrew/bin/${name}`, `/usr/local/bin/${name}`);
1103
+ if (name === "tailscale")
1104
+ candidates.push("/Applications/Tailscale.app/Contents/MacOS/Tailscale");
1105
+ }
1106
+ for (const candidate of candidates) {
1107
+ try {
1108
+ await access(candidate, constants.X_OK);
1109
+ if ((await stat(candidate)).isFile()) return candidate;
1110
+ } catch {
1111
+ continue;
1112
+ }
1113
+ }
1114
+ return void 0;
1115
+ }
1116
+ async function probeTailscale() {
1117
+ const option = { provider: "tailscale", available: false, reason: "Not installed" };
1118
+ const executable = await findExecutable("tailscale");
1119
+ if (!executable) return option;
1120
+ option.executable = executable;
1121
+ const env = { ...process.env, TAILSCALE_BE_CLI: "1" };
1122
+ try {
1123
+ const response = await execute2(executable, ["status", "--json"], 3e3, env);
1124
+ const status = z5.object({
1125
+ BackendState: z5.string(),
1126
+ CertDomains: z5.array(z5.string()).optional(),
1127
+ Self: z5.object({ DNSName: z5.string() }).nullable().optional()
1128
+ }).parse(JSON.parse(response.stdout));
1129
+ if (status.BackendState !== "Running")
1130
+ return { ...option, reason: "Installed \xB7 connect Tailscale first" };
1131
+ const dns = status.Self?.DNSName.replace(/\.$/, "");
1132
+ if (!dns || !status.CertDomains?.includes(dns))
1133
+ return { ...option, reason: "Enable HTTPS certificates in your tailnet first" };
1134
+ const help = await execute2(executable, ["serve", "--help"], 2e3, env);
1135
+ if (!`${help.stdout}${help.stderr}`.includes("--https"))
1136
+ return { ...option, reason: "Update Tailscale to use foreground HTTPS Serve" };
1137
+ const responseConfig = await execute2(executable, ["serve", "status", "--json"], 3e3, env);
1138
+ const config = ServeConfig.parse(JSON.parse(responseConfig.stdout) ?? {});
1139
+ const occupied = new Set(Object.keys(config.TCP ?? {}));
1140
+ for (const foreground of Object.values(config.Foreground ?? {}))
1141
+ for (const port3 of Object.keys(foreground.TCP ?? {})) occupied.add(port3);
1142
+ const port2 = [443, ...Array.from({ length: 10 }, (_, index) => 8443 + index)].find(
1143
+ (candidate) => !occupied.has(String(candidate))
1144
+ );
1145
+ if (!port2) return { ...option, reason: "HTTPS Serve ports are in use \xB7 paste an existing URL" };
1146
+ return {
1147
+ ...option,
1148
+ available: true,
1149
+ reason: "Private to your tailnet \xB7 Tailscale required on your phone",
1150
+ publicUrl: new URL(`https://${dns}${port2 === 443 ? "" : `:${port2}`}`).origin,
1151
+ httpsPort: port2
1152
+ };
1153
+ } catch {
1154
+ return { ...option, reason: "Installed \xB7 could not verify daemon or Serve access" };
1155
+ }
1156
+ }
1157
+ async function probeCloudflared(home) {
1158
+ const option = {
1159
+ provider: "cloudflared",
1160
+ available: false,
1161
+ reason: "Not installed"
1162
+ };
1163
+ const executable = await findExecutable("cloudflared");
1164
+ if (!executable) return option;
1165
+ option.executable = executable;
1166
+ try {
1167
+ await execute2(executable, ["--version"], 2e3);
1168
+ const help = await execute2(executable, ["tunnel", "--help"], 2e3);
1169
+ if (!`${help.stdout}${help.stderr}`.includes("--url"))
1170
+ return { ...option, reason: "Installed \xB7 this version does not support Quick Tunnels" };
1171
+ } catch {
1172
+ return { ...option, reason: "Installed \xB7 command probe failed or timed out" };
1173
+ }
1174
+ const directories = [
1175
+ path3.join(home, ".cloudflared"),
1176
+ path3.join(home, ".cloudflare-warp"),
1177
+ path3.join(home, "cloudflare-warp"),
1178
+ "/etc/cloudflared",
1179
+ "/usr/local/etc/cloudflared"
1180
+ ];
1181
+ const configurations = await Promise.all(
1182
+ directories.flatMap(
1183
+ (directory) => ["config.yml", "config.yaml"].map(
1184
+ async (name) => access(path3.join(directory, name), constants.F_OK).then(
1185
+ () => true,
1186
+ () => false
1187
+ )
1188
+ )
1189
+ )
1190
+ );
1191
+ if (configurations.some(Boolean))
1192
+ return { ...option, reason: "Existing tunnel configuration \xB7 paste its HTTPS URL" };
1193
+ return { ...option, available: true, reason: "Temporary public HTTPS URL \xB7 changes each start" };
1194
+ }
1195
+ async function detectTunnels(home = homedir2()) {
1196
+ return Promise.all([probeTailscale(), probeCloudflared(home)]);
1197
+ }
1198
+ var TcpConfig, ServeConfig, ManagedTunnel;
1199
+ var init_tunnels = __esm({
1200
+ "src/tunnels.ts"() {
1201
+ "use strict";
1202
+ init_cli_output();
1203
+ TcpConfig = z5.object({ TCP: z5.record(z5.string(), z5.unknown()).optional() });
1204
+ ServeConfig = TcpConfig.extend({
1205
+ Foreground: z5.record(z5.string(), TcpConfig).optional()
1206
+ });
1207
+ ManagedTunnel = class {
1208
+ constructor(option, disconnected = () => {
1209
+ }) {
1210
+ this.option = option;
1211
+ this.disconnected = disconnected;
1212
+ }
1213
+ option;
1214
+ disconnected;
1215
+ child;
1216
+ stopping = false;
1217
+ connected = false;
1218
+ closing;
1219
+ running() {
1220
+ return this.connected && !this.stopping;
1221
+ }
1222
+ stopOnSignal = () => {
1223
+ void this.close();
1224
+ };
1225
+ stopOnExit = () => {
1226
+ if (this.child) signalChild(this.child, "SIGKILL");
1227
+ };
1228
+ async start(origin, signal) {
1229
+ signal?.throwIfAborted();
1230
+ if (!this.option.available) throw new Error(this.option.reason);
1231
+ const tailscale = this.option.provider === "tailscale";
1232
+ const child = spawn(
1233
+ this.option.executable ?? this.option.provider,
1234
+ tailscale ? ["serve", "--bg=false", `--https=${this.option.httpsPort}`, origin] : ["tunnel", "--no-autoupdate", "--url", origin],
1235
+ {
1236
+ stdio: ["ignore", "pipe", "pipe"],
1237
+ detached: process.platform !== "win32",
1238
+ env: tailscale ? { ...process.env, TAILSCALE_BE_CLI: "1" } : process.env
1239
+ }
1240
+ );
1241
+ this.child = child;
1242
+ process.once("SIGINT", this.stopOnSignal);
1243
+ process.once("SIGTERM", this.stopOnSignal);
1244
+ process.once("exit", this.stopOnExit);
1245
+ return new Promise((resolve2, reject) => {
1246
+ let output = "";
1247
+ let address;
1248
+ let settled = false;
1249
+ const abort = () => fail("Tunnel startup canceled.");
1250
+ const timeout = setTimeout(
1251
+ () => fail("Tunnel startup timed out. Check your connection or paste an existing HTTPS URL."),
1252
+ 45e3
1253
+ );
1254
+ const fail = (message) => {
1255
+ if (settled) return;
1256
+ settled = true;
1257
+ clearTimeout(timeout);
1258
+ signal?.removeEventListener("abort", abort);
1259
+ void this.close();
1260
+ reject(new Error(message));
1261
+ };
1262
+ const read = (data) => {
1263
+ output = `${output}${data.toString()}`.slice(-65536);
1264
+ address = tailscale ? this.option.publicUrl : output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com\b/)?.[0];
1265
+ const ready = tailscale ? Boolean(
1266
+ address && output.includes(address) && output.includes("Available within your tailnet")
1267
+ ) : output.includes("Registered tunnel connection");
1268
+ if (settled || !address || !ready) return;
1269
+ settled = true;
1270
+ this.connected = true;
1271
+ clearTimeout(timeout);
1272
+ signal?.removeEventListener("abort", abort);
1273
+ resolve2(address);
1274
+ };
1275
+ child.stdout?.on("data", read);
1276
+ child.stderr?.on("data", read);
1277
+ signal?.addEventListener("abort", abort, { once: true });
1278
+ child.once(
1279
+ "error",
1280
+ () => fail(`${this.option.provider} could not start. Check the installation and permissions.`)
1281
+ );
1282
+ child.once("exit", () => signalChild(child, "SIGTERM"));
1283
+ child.once("close", (code) => {
1284
+ const unexpected = this.connected && !this.stopping;
1285
+ this.connected = false;
1286
+ process.removeListener("SIGINT", this.stopOnSignal);
1287
+ process.removeListener("SIGTERM", this.stopOnSignal);
1288
+ process.removeListener("exit", this.stopOnExit);
1289
+ fail(
1290
+ `${this.option.provider} exited (${code ?? "signal"}). Check tunnel access or paste an existing HTTPS URL.`
1291
+ );
1292
+ if (unexpected) {
1293
+ this.disconnected();
1294
+ printError(
1295
+ "The managed tunnel stopped. Restart the server to reconnect your phone; local work can continue."
1296
+ );
1297
+ }
1298
+ });
1299
+ });
1300
+ }
1301
+ close() {
1302
+ this.stopping = true;
1303
+ const child = this.child;
1304
+ if (!child?.pid || child.exitCode !== null || child.signalCode !== null)
1305
+ return Promise.resolve();
1306
+ this.closing ??= new Promise((resolve2) => {
1307
+ const timeout = setTimeout(() => signalChild(child, "SIGKILL"), 2e3);
1308
+ child.once("close", () => {
1309
+ clearTimeout(timeout);
1310
+ resolve2();
1311
+ });
1312
+ signalChild(child, "SIGTERM");
1313
+ });
1314
+ return this.closing;
1315
+ }
1316
+ };
1317
+ }
1318
+ });
1319
+
1320
+ // src/opentunnel.ts
1321
+ import { randomBytes } from "node:crypto";
1322
+ import { chmod as chmod2, mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
1323
+ import path4 from "node:path";
996
1324
  import { z as z6 } from "zod";
1325
+ import {
1326
+ create,
1327
+ OpenTunnelStorage
1328
+ } from "./opentunnel-sdk.js";
1329
+ var OpenTunnel;
1330
+ var init_opentunnel = __esm({
1331
+ "src/opentunnel.ts"() {
1332
+ "use strict";
1333
+ OpenTunnel = class {
1334
+ constructor(directory, disconnected = () => {
1335
+ }, makeClient = create) {
1336
+ this.directory = directory;
1337
+ this.disconnected = disconnected;
1338
+ this.makeClient = makeClient;
1339
+ }
1340
+ directory;
1341
+ disconnected;
1342
+ makeClient;
1343
+ client;
1344
+ connection;
1345
+ stopping = false;
1346
+ stage = "reading-identity";
1347
+ progress() {
1348
+ return this.connection?.status().state ?? this.stage;
1349
+ }
1350
+ running() {
1351
+ return this.connection?.status().state === "connected" && !this.stopping;
1352
+ }
1353
+ async start(origin, signal) {
1354
+ const endpoint = new URL(origin);
1355
+ if (!["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname))
1356
+ throw new Error("OpenTunnel must forward to the local loopback server.");
1357
+ await mkdir3(this.directory, { recursive: true, mode: 448 });
1358
+ await chmod2(this.directory, 448);
1359
+ const routeFile = path4.join(this.directory, "route.json");
1360
+ let route;
1361
+ try {
1362
+ route = z6.string().regex(/^[a-f0-9]{16}$/).parse(JSON.parse(await readFile2(routeFile, "utf8")));
1363
+ } catch (error) {
1364
+ if (!z6.object({ code: z6.literal("ENOENT") }).safeParse(error).success) throw error;
1365
+ route = randomBytes(8).toString("hex");
1366
+ await writeFile3(routeFile, JSON.stringify(route), { flag: "wx", mode: 384 });
1367
+ }
1368
+ this.stopping = false;
1369
+ this.client = this.makeClient({
1370
+ store: OpenTunnelStorage.xdg({ env: { XDG_DATA_HOME: this.directory } })
1371
+ });
1372
+ const abort = AbortSignal.any([
1373
+ signal ?? new AbortController().signal,
1374
+ AbortSignal.timeout(12e4)
1375
+ ]);
1376
+ let canceled = () => {
1377
+ };
1378
+ try {
1379
+ const client = this.client;
1380
+ this.stage = "reading-identity";
1381
+ const connecting = (async () => {
1382
+ const options = {
1383
+ profile: "agentrific",
1384
+ onProgress: (stage) => {
1385
+ this.stage = stage;
1386
+ }
1387
+ };
1388
+ const identity2 = await client.tunnel.get(options);
1389
+ abort.throwIfAborted();
1390
+ if (!identity2) {
1391
+ const resumed = await client.tunnel.resume(options);
1392
+ abort.throwIfAborted();
1393
+ if (!resumed) await client.tunnel.create(options);
1394
+ }
1395
+ abort.throwIfAborted();
1396
+ this.stage = "connecting";
1397
+ return client.tunnel.connect({
1398
+ profile: "agentrific",
1399
+ routes: {
1400
+ [route]: `${endpoint.hostname === "[::1]" ? "[::1]" : "127.0.0.1"}:${endpoint.port || "80"}`
1401
+ },
1402
+ signal
1403
+ });
1404
+ })();
1405
+ const cancellation = new Promise((_resolve, reject) => {
1406
+ canceled = () => reject(new Error("OpenTunnel connection canceled or timed out."));
1407
+ abort.addEventListener("abort", canceled, { once: true });
1408
+ if (abort.aborted) canceled();
1409
+ });
1410
+ const connection = await Promise.race([connecting, cancellation]);
1411
+ this.connection = connection;
1412
+ void connection.closed.then(() => {
1413
+ if (!this.stopping) this.disconnected();
1414
+ }).catch(() => {
1415
+ if (!this.stopping) this.disconnected();
1416
+ });
1417
+ return `https://${route}.${connection.tunnel.hostname}`;
1418
+ } catch (error) {
1419
+ await this.close();
1420
+ throw error;
1421
+ } finally {
1422
+ abort.removeEventListener("abort", canceled);
1423
+ }
1424
+ }
1425
+ async close() {
1426
+ this.stopping = true;
1427
+ await this.connection?.close();
1428
+ await this.client?.dispose();
1429
+ this.connection = void 0;
1430
+ this.client = void 0;
1431
+ }
1432
+ };
1433
+ }
1434
+ });
1435
+
1436
+ // ../../packages/contracts/src/push.ts
1437
+ import { z as z7 } from "zod";
997
1438
  function pushSigningText(installationId, operation, request) {
998
1439
  return [
999
1440
  "agentrific-push-v1",
@@ -1011,7 +1452,7 @@ var init_push = __esm({
1011
1452
  "use strict";
1012
1453
  PUSH_API_URL = "https://api.agentrific.com";
1013
1454
  PUSH_SANDBOX_API_URL = "https://sandbox-api.agentrific.com";
1014
- PushKindSchema = z6.enum([
1455
+ PushKindSchema = z7.enum([
1015
1456
  "started",
1016
1457
  "completed",
1017
1458
  "needs_input",
@@ -1020,47 +1461,47 @@ var init_push = __esm({
1020
1461
  "pr_created",
1021
1462
  "failed"
1022
1463
  ]);
1023
- PushPreferencesSchema = z6.array(PushKindSchema).max(7);
1024
- PushPublicKeySchema = z6.string().regex(/^[a-f0-9]{64}$/);
1025
- PushInstallationSchema = z6.object({ installationId: z6.string().uuid(), token: z6.string().regex(/^[a-f0-9]{64}$/) }).strict();
1026
- PushRegistrationSchema = z6.object({
1027
- expoToken: z6.string().regex(/^(Expo|Exponent)PushToken\[[A-Za-z0-9_-]+\]$/),
1464
+ PushPreferencesSchema = z7.array(PushKindSchema).max(7);
1465
+ PushPublicKeySchema = z7.string().regex(/^[a-f0-9]{64}$/);
1466
+ PushInstallationSchema = z7.object({ installationId: z7.string().uuid(), token: z7.string().regex(/^[a-f0-9]{64}$/) }).strict();
1467
+ PushRegistrationSchema = z7.object({
1468
+ expoToken: z7.string().regex(/^(Expo|Exponent)PushToken\[[A-Za-z0-9_-]+\]$/),
1028
1469
  preferences: PushPreferencesSchema
1029
1470
  }).strict();
1030
- PushEnrollmentSchema = z6.object({ serverId: z6.string().uuid(), publicKey: PushPublicKeySchema }).strict();
1471
+ PushEnrollmentSchema = z7.object({ serverId: z7.string().uuid(), publicKey: PushPublicKeySchema }).strict();
1031
1472
  PushGrantSchema = PushEnrollmentSchema.extend({
1032
- installationId: z6.string().uuid(),
1033
- bindingId: z6.string().uuid(),
1034
- grant: z6.string().regex(/^[a-f0-9]{64}$/),
1035
- expiresAt: z6.number()
1473
+ installationId: z7.string().uuid(),
1474
+ bindingId: z7.string().uuid(),
1475
+ grant: z7.string().regex(/^[a-f0-9]{64}$/),
1476
+ expiresAt: z7.number()
1036
1477
  }).strict();
1037
- PushPullRequestUrlSchema = z6.string().max(512).regex(/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9][0-9]*$/);
1038
- PushEventSchema = z6.object({
1039
- eventId: z6.string().uuid(),
1040
- sessionId: z6.string().uuid(),
1478
+ PushPullRequestUrlSchema = z7.string().max(512).regex(/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9][0-9]*$/);
1479
+ PushEventSchema = z7.object({
1480
+ eventId: z7.string().uuid(),
1481
+ sessionId: z7.string().uuid(),
1041
1482
  kind: PushKindSchema,
1042
- taskTitle: z6.string().trim().min(1).max(160).optional(),
1483
+ taskTitle: z7.string().trim().min(1).max(160).optional(),
1043
1484
  prUrl: PushPullRequestUrlSchema.optional()
1044
1485
  }).strict();
1045
- PushSignedRequestSchema = z6.object({
1046
- bindingId: z6.string().uuid(),
1047
- timestamp: z6.number().int(),
1048
- nonce: z6.string().uuid(),
1049
- payload: z6.string().max(2048),
1050
- signature: z6.string().regex(/^[a-f0-9]{128}$/)
1486
+ PushSignedRequestSchema = z7.object({
1487
+ bindingId: z7.string().uuid(),
1488
+ timestamp: z7.number().int(),
1489
+ nonce: z7.string().uuid(),
1490
+ payload: z7.string().max(2048),
1491
+ signature: z7.string().regex(/^[a-f0-9]{128}$/)
1051
1492
  }).strict();
1052
- PushEntitlementSchema = z6.object({ active: z6.boolean(), expiresAt: z6.number().nullable() }).strict();
1053
- PushServerStatusSchema = z6.object({
1493
+ PushEntitlementSchema = z7.object({ active: z7.boolean(), expiresAt: z7.number().nullable() }).strict();
1494
+ PushServerStatusSchema = z7.object({
1054
1495
  publicKey: PushPublicKeySchema,
1055
- bindings: z6.array(
1056
- z6.object({
1057
- bindingId: z6.string().uuid(),
1058
- installationId: z6.string().uuid(),
1059
- deviceId: z6.string().uuid()
1496
+ bindings: z7.array(
1497
+ z7.object({
1498
+ bindingId: z7.string().uuid(),
1499
+ installationId: z7.string().uuid(),
1500
+ deviceId: z7.string().uuid()
1060
1501
  })
1061
1502
  ),
1062
- pending: z6.number().int().nonnegative(),
1063
- error: z6.string().nullable()
1503
+ pending: z7.number().int().nonnegative(),
1504
+ error: z7.string().nullable()
1064
1505
  });
1065
1506
  }
1066
1507
  });
@@ -1074,7 +1515,7 @@ import {
1074
1515
  randomUUID
1075
1516
  } from "node:crypto";
1076
1517
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1077
- import path4 from "node:path";
1518
+ import path6 from "node:path";
1078
1519
  import { Effect as Effect3 } from "effect";
1079
1520
  function pushKind(session, previous) {
1080
1521
  if (session.gitDelivery?.status === "pr_opened" && previous?.gitDelivery?.status !== "pr_opened")
@@ -1134,7 +1575,7 @@ var init_push2 = __esm({
1134
1575
  this.transport = transport;
1135
1576
  this.apiUrl = apiUrl;
1136
1577
  mkdirSync(directory, { recursive: true, mode: 448 });
1137
- const filename = path4.join(directory, "push-signing-key.pem");
1578
+ const filename = path6.join(directory, "push-signing-key.pem");
1138
1579
  if (!existsSync(filename))
1139
1580
  writeFileSync(
1140
1581
  filename,
@@ -1304,12 +1745,12 @@ var init_push2 = __esm({
1304
1745
  // src/attachments.ts
1305
1746
  import { randomUUID as randomUUID2 } from "node:crypto";
1306
1747
  import { createReadStream, createWriteStream } from "node:fs";
1307
- import { mkdir as mkdir3, readFile as readFile2, rm, stat as stat2, readdir } from "node:fs/promises";
1308
- import path5 from "node:path";
1748
+ import { mkdir as mkdir4, readFile as readFile3, rm, stat as stat2, readdir } from "node:fs/promises";
1749
+ import path7 from "node:path";
1309
1750
  import { pipeline } from "node:stream/promises";
1310
1751
  import sharp from "sharp";
1311
1752
  import { Context, Data, Effect as Effect4, Layer } from "effect";
1312
- import { z as z7 } from "zod";
1753
+ import { z as z8 } from "zod";
1313
1754
  var AttachmentError, textExtensions, imageMimes, AttachmentFiles, Attachments;
1314
1755
  var init_attachments = __esm({
1315
1756
  "src/attachments.ts"() {
@@ -1364,7 +1805,7 @@ var init_attachments = __esm({
1364
1805
  root;
1365
1806
  store;
1366
1807
  directory(id) {
1367
- return path5.join(this.root, z7.string().uuid().parse(id));
1808
+ return path7.join(this.root, z8.string().uuid().parse(id));
1368
1809
  }
1369
1810
  io(operation) {
1370
1811
  return Effect4.tryPromise({
@@ -1378,11 +1819,11 @@ var init_attachments = __esm({
1378
1819
  return this.io(async () => {
1379
1820
  const id = randomUUID2();
1380
1821
  const directory = this.directory(id);
1381
- const source = path5.join(directory, "source");
1382
- const nameField = z7.object({ type: z7.literal("field"), value: z7.string() }).safeParse(part.fields.name);
1822
+ const source = path7.join(directory, "source");
1823
+ const nameField = z8.object({ type: z8.literal("field"), value: z8.string() }).safeParse(part.fields.name);
1383
1824
  const originalName = nameField.success ? nameField.data.value : part.filename;
1384
- const name = path5.basename(originalName.replaceAll("\\", "/")).replace(new RegExp("\\p{Cc}", "gu"), "").slice(0, 180) || "attachment";
1385
- await mkdir3(directory, { recursive: true });
1825
+ const name = path7.basename(originalName.replaceAll("\\", "/")).replace(new RegExp("\\p{Cc}", "gu"), "").slice(0, 180) || "attachment";
1826
+ await mkdir4(directory, { recursive: true });
1386
1827
  try {
1387
1828
  await pipeline(part.file, createWriteStream(source, { flags: "wx" }));
1388
1829
  if (part.file.truncated) throw new Error("Files must be 20 MB or smaller.");
@@ -1409,14 +1850,14 @@ var init_attachments = __esm({
1409
1850
  const metadata = await sharp(source, { limitInputPixels: 4e7 }).metadata().catch(() => null);
1410
1851
  const mimeType = metadata?.format && imageMimes.get(metadata.format);
1411
1852
  if (mimeType) {
1412
- await sharp(source, { limitInputPixels: 4e7 }).rotate().resize({ width: 2048, height: 2048, fit: "inside", withoutEnlargement: true }).png().toFile(path5.join(path5.dirname(source), "preview.png"));
1853
+ await sharp(source, { limitInputPixels: 4e7 }).rotate().resize({ width: 2048, height: 2048, fit: "inside", withoutEnlargement: true }).png().toFile(path7.join(path7.dirname(source), "preview.png"));
1413
1854
  return { kind: "image", mimeType };
1414
1855
  }
1415
- if (!textExtensions.has(path5.extname(name).toLowerCase()))
1856
+ if (!textExtensions.has(path7.extname(name).toLowerCase()))
1416
1857
  throw new Error(
1417
1858
  "Choose a PNG, JPEG, WebP, GIF, or text/code file. PDF and Office documents aren't supported yet."
1418
1859
  );
1419
- const bytes = await readFile2(source);
1860
+ const bytes = await readFile3(source);
1420
1861
  const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1421
1862
  if (new RegExp("\\p{Cc}", "u").test(text.replaceAll("\n", "").replaceAll("\r", "").replaceAll(" ", "")))
1422
1863
  throw new Error("This file isn't readable UTF-8 text.");
@@ -1433,7 +1874,7 @@ var init_attachments = __esm({
1433
1874
  throw new Error(
1434
1875
  "An attachment is unavailable for this project. Remove it and upload it again."
1435
1876
  );
1436
- await stat2(path5.join(this.directory(id), "source"));
1877
+ await stat2(path7.join(this.directory(id), "source"));
1437
1878
  attachments2.push(attachment);
1438
1879
  }
1439
1880
  return attachments2;
@@ -1441,7 +1882,7 @@ var init_attachments = __esm({
1441
1882
  }
1442
1883
  content(attachment, preview) {
1443
1884
  return createReadStream(
1444
- path5.join(
1885
+ path7.join(
1445
1886
  this.directory(attachment.id),
1446
1887
  preview && attachment.kind === "image" ? "preview.png" : "source"
1447
1888
  )
@@ -1452,13 +1893,13 @@ var init_attachments = __esm({
1452
1893
  const images = [];
1453
1894
  const sections = [];
1454
1895
  for (const attachment of attachments2) {
1455
- const source = path5.join(this.directory(attachment.id), "source");
1896
+ const source = path7.join(this.directory(attachment.id), "source");
1456
1897
  if (attachment.kind === "image") {
1457
- const bytes = await readFile2(path5.join(this.directory(attachment.id), "preview.png"));
1898
+ const bytes = await readFile3(path7.join(this.directory(attachment.id), "preview.png"));
1458
1899
  images.push({ type: "image", data: bytes.toString("base64"), mimeType: "image/png" });
1459
1900
  sections.push(`Image attachment: ${JSON.stringify(attachment.name)}`);
1460
1901
  } else {
1461
- const text = await readFile2(source, "utf8");
1902
+ const text = await readFile3(source, "utf8");
1462
1903
  sections.push(
1463
1904
  `File attachment: ${JSON.stringify(attachment.name)}
1464
1905
  Full file available to the read tool at: ${JSON.stringify(source)}
@@ -1484,9 +1925,9 @@ ${text.slice(0, 12e3)}${text.length > 12e3 ? "\n[Preview truncated; use the read
1484
1925
  if (await Effect4.runPromise(this.store.deleteAttachment(attachment.id)))
1485
1926
  await rm(this.directory(attachment.id), { recursive: true, force: true });
1486
1927
  }
1487
- await mkdir3(this.root, { recursive: true });
1928
+ await mkdir4(this.root, { recursive: true });
1488
1929
  for (const entry of await readdir(this.root, { withFileTypes: true })) {
1489
- if (!entry.isDirectory() || !z7.string().uuid().safeParse(entry.name).success) continue;
1930
+ if (!entry.isDirectory() || !z8.string().uuid().safeParse(entry.name).success) continue;
1490
1931
  const directory = this.directory(entry.name);
1491
1932
  if ((await stat2(directory)).mtimeMs < cutoff && !await Effect4.runPromise(this.store.getAttachment(entry.name)))
1492
1933
  await rm(directory, { recursive: true, force: true });
@@ -1574,24 +2015,24 @@ var init_event_server = __esm({
1574
2015
  });
1575
2016
 
1576
2017
  // src/security.ts
1577
- import { createHash as createHash2, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
2018
+ import { createHash as createHash2, randomBytes as randomBytes2, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
1578
2019
  import { Effect as Effect5 } from "effect";
1579
- import { z as z8 } from "zod";
2020
+ import { z as z9 } from "zod";
1580
2021
  async function registerSecurity(app2, security2, version, allowedOrigins2) {
1581
2022
  app2.addHook("onRequest", async (request, reply) => {
1582
- const path13 = new URL(request.url, "http://localhost");
2023
+ const path15 = new URL(request.url, "http://localhost");
1583
2024
  reply.header("Cache-Control", "no-store");
1584
2025
  if (request.method === "OPTIONS") return;
1585
2026
  if (request.headers.origin && !allowedOrigins2.includes(request.headers.origin))
1586
2027
  return reply.code(403).send({ error: "Browser origin is not allowed." });
1587
- if (path13.pathname === "/health" || path13.pathname === "/api/auth/pair" && request.method === "POST")
2028
+ if (path15.pathname === "/health" || path15.pathname === "/api/auth/pair" && request.method === "POST")
1588
2029
  return;
1589
2030
  const authorization = request.headers.authorization ?? "";
1590
2031
  const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
1591
- const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(path13.pathname);
1592
- const ticket = path13.searchParams.get("ticket");
1593
- path13.searchParams.delete("ticket");
1594
- if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path13.pathname + path13.search, false)))
2032
+ const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(path15.pathname);
2033
+ const ticket = path15.searchParams.get("ticket");
2034
+ path15.searchParams.delete("ticket");
2035
+ if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path15.pathname + path15.search, false)))
1595
2036
  return reply.code(401).send({ error: "Pair this device with your server.", code: "UNAUTHORIZED" });
1596
2037
  if (!asset && request.headers["x-agentrific-protocol"] !== String(PROTOCOL_VERSION))
1597
2038
  return reply.code(409).send({
@@ -1646,8 +2087,8 @@ async function registerSecurity(app2, security2, version, allowedOrigins2) {
1646
2087
  ticket: security2.ticket((request.headers.authorization ?? "").slice(7), "/api/events")
1647
2088
  }));
1648
2089
  app2.post("/api/auth/asset-ticket", (request, reply) => {
1649
- const input = z8.object({
1650
- path: z8.string().regex(/^\/api\/projects\/[^/?]+\/attachments\/[^/?]+\/content(?:\?preview=1)?$/)
2090
+ const input = z9.object({
2091
+ path: z9.string().regex(/^\/api\/projects\/[^/?]+\/attachments\/[^/?]+\/content(?:\?preview=1)?$/)
1651
2092
  }).safeParse(request.body);
1652
2093
  if (!input.success) return reply.code(400).send({ error: "Choose a valid attachment." });
1653
2094
  return {
@@ -1661,7 +2102,7 @@ var init_security = __esm({
1661
2102
  "use strict";
1662
2103
  init_src();
1663
2104
  init_pairing();
1664
- secret = () => randomBytes(32).toString("hex");
2105
+ secret = () => randomBytes2(32).toString("hex");
1665
2106
  digest = (value) => createHash2("sha256").update(value).digest("hex");
1666
2107
  ServerSecurity = class {
1667
2108
  constructor(store2, serverId) {
@@ -1724,17 +2165,17 @@ var init_security = __esm({
1724
2165
  if (!/^[a-f0-9]{64}$/.test(token)) return void 0;
1725
2166
  return Effect5.runSync(this.store.deviceForToken(digest(token)));
1726
2167
  }
1727
- ticket(token, path13) {
2168
+ ticket(token, path15) {
1728
2169
  this.prune();
1729
2170
  if (!this.authenticate(token) || this.tickets.size >= 1e4) return void 0;
1730
2171
  const value = secret();
1731
- this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path13 });
2172
+ this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path15 });
1732
2173
  return value;
1733
2174
  }
1734
- redeemTicket(value, path13, consume = true) {
2175
+ redeemTicket(value, path15, consume = true) {
1735
2176
  this.prune();
1736
2177
  const ticket = this.tickets.get(value);
1737
- if (!ticket || ticket.path !== path13) return void 0;
2178
+ if (!ticket || ticket.path !== path15) return void 0;
1738
2179
  if (consume) this.tickets.delete(value);
1739
2180
  return Effect5.runSync(this.store.deviceForToken(ticket.tokenHash));
1740
2181
  }
@@ -1748,6 +2189,105 @@ var init_security = __esm({
1748
2189
  }
1749
2190
  });
1750
2191
 
2192
+ // src/desktop-phone.ts
2193
+ var DesktopPhoneConnection;
2194
+ var init_desktop_phone = __esm({
2195
+ "src/desktop-phone.ts"() {
2196
+ "use strict";
2197
+ init_tunnels();
2198
+ init_opentunnel();
2199
+ DesktopPhoneConnection = class {
2200
+ constructor(origin, changed, directory, detect = async () => [
2201
+ { provider: "opentunnel", available: true, reason: "Built in \xB7 stable public HTTPS address" },
2202
+ ...await detectTunnels()
2203
+ ], create2 = (option, disconnected) => option.provider === "opentunnel" ? new OpenTunnel(directory, disconnected) : new ManagedTunnel({ ...option, provider: option.provider }, disconnected)) {
2204
+ this.origin = origin;
2205
+ this.changed = changed;
2206
+ this.directory = directory;
2207
+ this.detect = detect;
2208
+ this.create = create2;
2209
+ }
2210
+ origin;
2211
+ changed;
2212
+ directory;
2213
+ detect;
2214
+ create;
2215
+ tunnel;
2216
+ provider = null;
2217
+ publicUrl = null;
2218
+ pending = Promise.resolve();
2219
+ snapshot() {
2220
+ return {
2221
+ provider: this.provider,
2222
+ publicUrl: this.publicUrl,
2223
+ progress: this.tunnel?.progress?.() ?? null
2224
+ };
2225
+ }
2226
+ async options() {
2227
+ return (await this.detect()).map(({ provider, available, reason }) => ({
2228
+ provider,
2229
+ available,
2230
+ reason
2231
+ }));
2232
+ }
2233
+ serialize(operation) {
2234
+ const result = this.pending.then(operation);
2235
+ this.pending = result.then(
2236
+ () => {
2237
+ },
2238
+ () => {
2239
+ }
2240
+ );
2241
+ return result;
2242
+ }
2243
+ start(provider, signal) {
2244
+ return this.serialize(async () => {
2245
+ signal?.throwIfAborted();
2246
+ if (this.tunnel && this.publicUrl && this.provider === provider) return this.snapshot();
2247
+ if (this.tunnel)
2248
+ throw new Error("Disconnect the current phone connection before changing providers.");
2249
+ const option = (await this.detect()).find((candidate) => candidate.provider === provider);
2250
+ if (!option?.available) throw new Error(option?.reason || "Tunnel provider unavailable.");
2251
+ let tunnel;
2252
+ tunnel = this.create(option, () => {
2253
+ if (this.tunnel !== tunnel) return;
2254
+ void tunnel.close().catch(() => {
2255
+ });
2256
+ this.tunnel = void 0;
2257
+ this.provider = null;
2258
+ this.publicUrl = null;
2259
+ this.changed(null);
2260
+ });
2261
+ this.tunnel = tunnel;
2262
+ try {
2263
+ const url = await tunnel.start(this.origin(), signal);
2264
+ signal?.throwIfAborted();
2265
+ this.provider = provider;
2266
+ this.publicUrl = url;
2267
+ this.changed(url);
2268
+ return this.snapshot();
2269
+ } catch (error) {
2270
+ await tunnel.close();
2271
+ this.tunnel = void 0;
2272
+ this.changed(null);
2273
+ throw error;
2274
+ }
2275
+ });
2276
+ }
2277
+ close() {
2278
+ return this.serialize(async () => {
2279
+ await this.tunnel?.close();
2280
+ this.tunnel = void 0;
2281
+ this.provider = null;
2282
+ this.publicUrl = null;
2283
+ this.changed(null);
2284
+ return this.snapshot();
2285
+ });
2286
+ }
2287
+ };
2288
+ }
2289
+ });
2290
+
1751
2291
  // src/maintenance.ts
1752
2292
  var MaintenanceError, Maintenance;
1753
2293
  var init_maintenance = __esm({
@@ -1793,9 +2333,9 @@ var init_maintenance = __esm({
1793
2333
  }
1794
2334
  attach(app2) {
1795
2335
  app2.addHook("onRequest", async (request, reply) => {
1796
- const path13 = request.url.split("?")[0];
2336
+ const path15 = request.url.split("?")[0];
1797
2337
  if (["GET", "HEAD", "OPTIONS"].includes(request.method)) return;
1798
- const allowed = path13.startsWith("/api/auth/") || /^\/api\/sessions\/[^/]+\/stop$/.test(path13);
2338
+ const allowed = path15.startsWith("/api/auth/") || /^\/api\/sessions\/[^/]+\/stop$/.test(path15);
1799
2339
  if (this.paused && !allowed)
1800
2340
  return reply.code(503).send({ code: "MAINTENANCE", error: new MaintenanceError().message });
1801
2341
  this.requests.add(request.id);
@@ -2069,7 +2609,7 @@ var init_git_flow = __esm({
2069
2609
  });
2070
2610
 
2071
2611
  // src/runtime.ts
2072
- import path6 from "node:path";
2612
+ import path8 from "node:path";
2073
2613
  import { randomUUID as randomUUID4 } from "node:crypto";
2074
2614
  import { Context as Context2, Data as Data2, Effect as Effect8, Fiber, Layer as Layer2, Semaphore as Semaphore2 } from "effect";
2075
2615
  var AgentError, AgentController, AgentSessions;
@@ -2128,8 +2668,8 @@ var init_runtime = __esm({
2128
2668
  const agent = yield* Effect8.tryPromise({
2129
2669
  try: () => this.factory.open(
2130
2670
  session.id,
2131
- session.workspacePath ?? path6.join(this.worktreesRoot, session.id),
2132
- path6.join(this.sessionsRoot, session.id),
2671
+ session.workspacePath ?? path8.join(this.worktreesRoot, session.id),
2672
+ path8.join(this.sessionsRoot, session.id),
2133
2673
  session.model,
2134
2674
  session
2135
2675
  ),
@@ -2416,17 +2956,17 @@ var init_workflow_command = __esm({
2416
2956
  });
2417
2957
 
2418
2958
  // src/workflow-schema.ts
2419
- import { z as z9 } from "zod";
2959
+ import { z as z10 } from "zod";
2420
2960
  var DeliveryJobSchema, PreparationJobSchema, WorkflowJobSchema;
2421
2961
  var init_workflow_schema = __esm({
2422
2962
  "src/workflow-schema.ts"() {
2423
2963
  "use strict";
2424
2964
  init_src();
2425
- DeliveryJobSchema = z9.object({
2426
- kind: z9.literal("delivery"),
2427
- sessionId: z9.string(),
2965
+ DeliveryJobSchema = z10.object({
2966
+ kind: z10.literal("delivery"),
2967
+ sessionId: z10.string(),
2428
2968
  request: DeliveryRequestSchema,
2429
- phase: z9.enum([
2969
+ phase: z10.enum([
2430
2970
  "validating",
2431
2971
  "drafting",
2432
2972
  "committing",
@@ -2436,25 +2976,25 @@ var init_workflow_schema = __esm({
2436
2976
  "opening_pr",
2437
2977
  "delivered"
2438
2978
  ]),
2439
- attempt: z9.number().int().positive(),
2440
- commitSha: z9.string().optional(),
2441
- title: z9.string(),
2442
- body: z9.string(),
2443
- logs: z9.string().default(""),
2444
- failed: z9.boolean().default(false),
2445
- result: z9.enum(["merged", "pr_opened", "no_changes"]).optional(),
2446
- url: z9.string().optional(),
2447
- message: z9.string().optional()
2979
+ attempt: z10.number().int().positive(),
2980
+ commitSha: z10.string().optional(),
2981
+ title: z10.string(),
2982
+ body: z10.string(),
2983
+ logs: z10.string().default(""),
2984
+ failed: z10.boolean().default(false),
2985
+ result: z10.enum(["merged", "pr_opened", "no_changes"]).optional(),
2986
+ url: z10.string().optional(),
2987
+ message: z10.string().optional()
2448
2988
  });
2449
- PreparationJobSchema = z9.object({
2450
- kind: z9.literal("preparation"),
2451
- sessionId: z9.string(),
2452
- projectPath: z9.string(),
2989
+ PreparationJobSchema = z10.object({
2990
+ kind: z10.literal("preparation"),
2991
+ sessionId: z10.string(),
2992
+ projectPath: z10.string(),
2453
2993
  input: CreateSessionSchema,
2454
- stage: z9.enum(["creating", "installing", "ready", "failed"]),
2455
- logs: z9.string().default("")
2994
+ stage: z10.enum(["creating", "installing", "ready", "failed"]),
2995
+ logs: z10.string().default("")
2456
2996
  });
2457
- WorkflowJobSchema = z9.discriminatedUnion("kind", [
2997
+ WorkflowJobSchema = z10.discriminatedUnion("kind", [
2458
2998
  DeliveryJobSchema,
2459
2999
  PreparationJobSchema
2460
3000
  ]);
@@ -2463,7 +3003,7 @@ var init_workflow_schema = __esm({
2463
3003
 
2464
3004
  // src/delivery.ts
2465
3005
  import { Effect as Effect9 } from "effect";
2466
- import { z as z10 } from "zod";
3006
+ import { z as z11 } from "zod";
2467
3007
  var DeliveryCoordinator;
2468
3008
  var init_delivery = __esm({
2469
3009
  "src/delivery.ts"() {
@@ -2762,11 +3302,11 @@ ${workflowError(error instanceof Error ? error : String(error))}`
2762
3302
  "--json",
2763
3303
  "url,state,headRefOid"
2764
3304
  ]);
2765
- const prs = z10.array(
2766
- z10.object({
2767
- url: z10.string().url(),
2768
- state: z10.string(),
2769
- headRefOid: z10.string().optional()
3305
+ const prs = z11.array(
3306
+ z11.object({
3307
+ url: z11.string().url(),
3308
+ state: z11.string(),
3309
+ headRefOid: z11.string().optional()
2770
3310
  })
2771
3311
  ).parse(JSON.parse(existing));
2772
3312
  const open = prs.find(
@@ -2809,10 +3349,10 @@ ${workflowError(error instanceof Error ? error : String(error))}`
2809
3349
  });
2810
3350
 
2811
3351
  // src/preparation.ts
2812
- import { mkdir as mkdir4, readFile as readFile3 } from "node:fs/promises";
2813
- import path7 from "node:path";
3352
+ import { mkdir as mkdir5, readFile as readFile4 } from "node:fs/promises";
3353
+ import path9 from "node:path";
2814
3354
  import { Effect as Effect10 } from "effect";
2815
- import { z as z11 } from "zod";
3355
+ import { z as z12 } from "zod";
2816
3356
  var PreparationCoordinator;
2817
3357
  var init_preparation = __esm({
2818
3358
  "src/preparation.ts"() {
@@ -2913,7 +3453,7 @@ ${logs}` : ""}`);
2913
3453
  const workspace = session.workspacePath;
2914
3454
  if (branchAtWorkspace !== void 0)
2915
3455
  throw new Error("The run workspace already exists on a different branch.");
2916
- await mkdir4(path7.dirname(workspace), { recursive: true });
3456
+ await mkdir5(path9.dirname(workspace), { recursive: true });
2917
3457
  try {
2918
3458
  await command(
2919
3459
  projectPath,
@@ -2933,12 +3473,12 @@ ${logs}` : ""}`);
2933
3473
  }
2934
3474
  }
2935
3475
  async install(workspace, command) {
2936
- const packageFile = await readFile3(path7.join(workspace, "package.json"), "utf8").catch(
3476
+ const packageFile = await readFile4(path9.join(workspace, "package.json"), "utf8").catch(
2937
3477
  () => void 0
2938
3478
  );
2939
3479
  if (!packageFile) return;
2940
- const metadata = z11.object({ packageManager: z11.string().optional() }).parse(JSON.parse(packageFile));
2941
- const has = async (name) => readFile3(path7.join(workspace, name)).then(
3480
+ const metadata = z12.object({ packageManager: z12.string().optional() }).parse(JSON.parse(packageFile));
3481
+ const has = async (name) => readFile4(path9.join(workspace, name)).then(
2942
3482
  () => true,
2943
3483
  () => false
2944
3484
  );
@@ -3021,18 +3561,21 @@ ${logs}` : ""}`);
3021
3561
 
3022
3562
  // src/delivery-writer.ts
3023
3563
  import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
3024
- import { z as z12 } from "zod";
3564
+ import { z as z13 } from "zod";
3025
3565
  function deliveryWriter(models2, createSession = createAgentSession) {
3026
3566
  return async (run, input, diff, signal) => {
3027
3567
  signal?.throwIfAborted();
3568
+ const selected = run.model ?? await models2.defaultReference();
3569
+ const sessionOptions = await models2.sessionOptions(run.workspacePath);
3028
3570
  const { session } = await createSession({
3029
3571
  cwd: run.workspacePath,
3030
- model: await models2.resolve(run.model ?? await models2.defaultReference()),
3031
- modelRuntime: models2.runtime,
3572
+ model: await models2.resolve(selected),
3573
+ ...sessionOptions,
3032
3574
  sessionManager: SessionManager.inMemory(),
3033
3575
  tools: [],
3034
3576
  noTools: "all"
3035
3577
  });
3578
+ if (sessionOptions.resourceLoader) await session.bindExtensions({});
3036
3579
  const timer = setTimeout(() => {
3037
3580
  void session.abort();
3038
3581
  }, 6e4);
@@ -3056,13 +3599,116 @@ ${diff}`
3056
3599
  signal?.removeEventListener("abort", abort);
3057
3600
  session.dispose();
3058
3601
  }
3059
- };
3602
+ };
3603
+ }
3604
+ var DraftSchema;
3605
+ var init_delivery_writer = __esm({
3606
+ "src/delivery-writer.ts"() {
3607
+ "use strict";
3608
+ DraftSchema = z13.object({ title: z13.string().trim().min(1).max(120), body: z13.string().trim().min(1).max(1e4) }).strict();
3609
+ }
3610
+ });
3611
+
3612
+ // src/claude-code.ts
3613
+ import { execFile as execFile3 } from "node:child_process";
3614
+ import { homedir as homedir3 } from "node:os";
3615
+ import { join as join2 } from "node:path";
3616
+ import { promisify as promisify3 } from "node:util";
3617
+ import { z as z14 } from "zod";
3618
+ import {
3619
+ createAgentSessionServices
3620
+ } from "@earendil-works/pi-coding-agent";
3621
+ async function discoverClaudeCode() {
3622
+ if (process.env.AGENTRIFIC_CLAUDE_CODE === "off") return;
3623
+ const candidates = [
3624
+ "claude",
3625
+ join2(homedir3(), ".local/bin/claude"),
3626
+ join2(homedir3(), ".claude/local/claude")
3627
+ ];
3628
+ for (const executable of candidates) {
3629
+ try {
3630
+ await execute3(executable, ["--version"], { timeout: 5e3, maxBuffer: 64 * 1024 });
3631
+ } catch {
3632
+ continue;
3633
+ }
3634
+ try {
3635
+ const { stdout } = await execute3(executable, ["auth", "status", "--json"], {
3636
+ timeout: 5e3,
3637
+ maxBuffer: 64 * 1024
3638
+ });
3639
+ const status = AuthStatusSchema.parse(JSON.parse(stdout));
3640
+ return { executable, ready: status.loggedIn };
3641
+ } catch {
3642
+ return { executable, ready: false };
3643
+ }
3644
+ }
3060
3645
  }
3061
- var DraftSchema;
3062
- var init_delivery_writer = __esm({
3063
- "src/delivery-writer.ts"() {
3646
+ var claudePlatform, execute3, AuthStatusSchema, ClaudeCode;
3647
+ var init_claude_code = __esm({
3648
+ "src/claude-code.ts"() {
3064
3649
  "use strict";
3065
- DraftSchema = z12.object({ title: z12.string().trim().min(1).max(120), body: z12.string().trim().min(1).max(1e4) }).strict();
3650
+ claudePlatform = "claude-bridge";
3651
+ execute3 = promisify3(execFile3);
3652
+ AuthStatusSchema = z14.object({ loggedIn: z14.boolean() });
3653
+ ClaudeCode = class {
3654
+ constructor(probe = discoverClaudeCode, query) {
3655
+ this.probe = probe;
3656
+ this.query = query;
3657
+ }
3658
+ probe;
3659
+ query;
3660
+ installation;
3661
+ registered = false;
3662
+ get ready() {
3663
+ return this.installation?.ready === true;
3664
+ }
3665
+ get warning() {
3666
+ 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;
3667
+ }
3668
+ async refresh(runtime2) {
3669
+ this.installation = await this.probe();
3670
+ if (this.installation && !this.registered) {
3671
+ await this.services(process.cwd(), runtime2);
3672
+ this.registered = true;
3673
+ }
3674
+ }
3675
+ async services(cwd, modelRuntime2) {
3676
+ const module = await import(new URL("./claude-bridge.js", import.meta.url).href);
3677
+ const extension = module.createClaudeExtension(this.query);
3678
+ const services = await createAgentSessionServices({
3679
+ cwd,
3680
+ modelRuntime: modelRuntime2,
3681
+ resourceLoaderOptions: {
3682
+ // Match Pipeline: do not execute unrelated global Pi extensions.
3683
+ noExtensions: true,
3684
+ extensionFactories: [
3685
+ {
3686
+ name: "agentrific-claude-code",
3687
+ factory: (pi) => extension({
3688
+ ...pi,
3689
+ // SAFETY: The pinned bridge uses only the name/config registration overload.
3690
+ registerProvider: ((id, config) => pi.registerProvider(id, {
3691
+ ...config,
3692
+ // Bind the working directory to this provider,
3693
+ // without changing the process cwd or global Claude configuration.
3694
+ streamSimple: config.streamSimple && ((model, context, options) => {
3695
+ const request = { ...options, cwd };
3696
+ return config.streamSimple(model, context, request);
3697
+ })
3698
+ }))
3699
+ })
3700
+ }
3701
+ ]
3702
+ }
3703
+ });
3704
+ const errors = [
3705
+ ...services.diagnostics.filter((item) => item.type === "error").map((item) => item.message),
3706
+ ...services.resourceLoader.getExtensions().errors.map((item) => item.error)
3707
+ ];
3708
+ if (errors.length) throw new Error(errors.join("\n"));
3709
+ return services;
3710
+ }
3711
+ };
3066
3712
  }
3067
3713
  });
3068
3714
 
@@ -3072,6 +3718,7 @@ var legacyModel, makers, ServerModels;
3072
3718
  var init_models = __esm({
3073
3719
  "src/models.ts"() {
3074
3720
  "use strict";
3721
+ init_claude_code();
3075
3722
  legacyModel = { platformId: "openai-codex", modelId: "gpt-6-luna" };
3076
3723
  makers = [
3077
3724
  { id: "openai", name: "OpenAI", pattern: /(?:^|[/.:\s-])(?:gpt-|o[134](?:-|$))/i },
@@ -3088,12 +3735,14 @@ var init_models = __esm({
3088
3735
  { id: "amazon", name: "Amazon", pattern: /amazon\.nova|nova-/i }
3089
3736
  ];
3090
3737
  ServerModels = class {
3091
- constructor(runtime2, store2) {
3738
+ constructor(runtime2, store2, claude) {
3092
3739
  this.runtime = runtime2;
3093
3740
  this.store = store2;
3741
+ this.claude = claude;
3094
3742
  }
3095
3743
  runtime;
3096
3744
  store;
3745
+ claude;
3097
3746
  refreshing;
3098
3747
  refreshedAt = (/* @__PURE__ */ new Date()).toISOString();
3099
3748
  warning = null;
@@ -3115,18 +3764,20 @@ var init_models = __esm({
3115
3764
  platformId,
3116
3765
  modelId,
3117
3766
  name: model?.name ?? modelId,
3118
- platformName: this.runtime.getProvider(platformId)?.name ?? platformId,
3767
+ platformName: platformId === claudePlatform ? "Claude Code" : this.runtime.getProvider(platformId)?.name ?? platformId,
3119
3768
  providerId: namespace === "openrouter" ? "unspecified" : maker?.id ?? namespace ?? "unspecified",
3120
3769
  providerName: namespace === "openrouter" ? "Other / custom" : maker?.name ?? publisherName ?? "Other / custom"
3121
3770
  };
3122
3771
  }
3123
3772
  async refresh(network) {
3124
3773
  if (!this.refreshing) {
3125
- this.refreshing = this.runtime.refresh({
3126
- allowNetwork: network,
3127
- force: network,
3128
- signal: AbortSignal.timeout(15e3)
3129
- }).then((result) => {
3774
+ this.refreshing = Promise.resolve(this.claude?.refresh(this.runtime)).then(
3775
+ () => this.runtime.refresh({
3776
+ allowNetwork: network,
3777
+ force: network,
3778
+ signal: AbortSignal.timeout(15e3)
3779
+ })
3780
+ ).then((result) => {
3130
3781
  this.refreshedAt = (/* @__PURE__ */ new Date()).toISOString();
3131
3782
  this.warning = result.aborted || result.errors.size || this.runtime.getError() ? "Some providers could not refresh. Showing the catalogue currently available on this server." : null;
3132
3783
  }).finally(() => {
@@ -3136,7 +3787,10 @@ var init_models = __esm({
3136
3787
  await this.refreshing;
3137
3788
  }
3138
3789
  async defaultReference() {
3139
- return await Effect11.runPromise(this.store.defaultModel()) ?? legacyModel;
3790
+ const saved = await Effect11.runPromise(this.store.defaultModel());
3791
+ if (saved) return saved;
3792
+ const preferred = this.claude?.ready ? this.runtime.getModels().find((model) => model.provider === claudePlatform && /sonnet/i.test(model.id)) : void 0;
3793
+ return preferred ? { platformId: preferred.provider, modelId: preferred.id } : legacyModel;
3140
3794
  }
3141
3795
  async defaultIdentity() {
3142
3796
  const selected = await this.defaultReference();
@@ -3149,17 +3803,24 @@ var init_models = __esm({
3149
3803
  return {
3150
3804
  models: this.runtime.getModels().map((model) => ({
3151
3805
  ...this.identity(model.provider, model.id),
3152
- available: available.has(`${model.provider}\0${model.id}`),
3806
+ available: available.has(`${model.provider}\0${model.id}`) && (model.provider !== claudePlatform || this.claude?.ready === true),
3153
3807
  reasoning: model.reasoning,
3154
3808
  input: model.input,
3155
3809
  contextWindow: model.contextWindow
3156
3810
  })),
3157
3811
  defaultModel: await this.defaultIdentity(),
3158
3812
  refreshedAt: this.refreshedAt,
3159
- warning: this.warning
3813
+ warning: this.claude?.warning ?? this.warning
3160
3814
  };
3161
3815
  }
3162
3816
  async resolve(reference) {
3817
+ if (reference.platformId === claudePlatform && this.claude) {
3818
+ await this.claude.refresh(this.runtime);
3819
+ if (!this.claude.ready)
3820
+ throw new Error(
3821
+ this.claude.warning ?? "Claude Code is unavailable on this server. Install it and run claude auth login as the server user."
3822
+ );
3823
+ }
3163
3824
  const available = await this.runtime.getAvailable(reference.platformId, {
3164
3825
  signal: AbortSignal.timeout(1e4)
3165
3826
  });
@@ -3170,6 +3831,17 @@ var init_models = __esm({
3170
3831
  );
3171
3832
  return model;
3172
3833
  }
3834
+ async sessionOptions(cwd = process.cwd()) {
3835
+ if (this.claude && this.runtime.getProvider(claudePlatform)) {
3836
+ const services = await this.claude.services(cwd);
3837
+ return {
3838
+ modelRuntime: services.modelRuntime,
3839
+ resourceLoader: services.resourceLoader,
3840
+ settingsManager: services.settingsManager
3841
+ };
3842
+ }
3843
+ return { modelRuntime: this.runtime };
3844
+ }
3173
3845
  async setDefault(reference) {
3174
3846
  await this.resolve(reference);
3175
3847
  await Effect11.runPromise(this.store.setDefaultModel(reference));
@@ -3219,19 +3891,19 @@ var init_delivery_tool = __esm({
3219
3891
  // src/file-change.ts
3220
3892
  import { createWriteToolDefinition, defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
3221
3893
  import { createTwoFilesPatch } from "diff";
3222
- import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
3223
- import { z as z13 } from "zod";
3894
+ import { mkdir as mkdir6, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
3895
+ import { z as z15 } from "zod";
3224
3896
  function boundFileChange(change) {
3225
3897
  if ((change.patch?.length ?? 0) + (change.content?.length ?? 0) > previewLimit)
3226
3898
  return { path: change.path, omitted: true };
3227
3899
  return change;
3228
3900
  }
3229
- function completedFileChange(result, path13) {
3901
+ function completedFileChange(result, path15) {
3230
3902
  const parsed = EditResultSchema.safeParse(result);
3231
3903
  if (!parsed.success) return;
3232
3904
  const details = parsed.data.details;
3233
3905
  if (details?.fileChange) return boundFileChange(details.fileChange);
3234
- if (path13 && details?.patch) return boundFileChange({ path: path13, patch: details.patch });
3906
+ if (path15 && details?.patch) return boundFileChange({ path: path15, patch: details.patch });
3235
3907
  }
3236
3908
  function fileWriteTool(cwd) {
3237
3909
  const base = createWriteToolDefinition(cwd);
@@ -3248,16 +3920,16 @@ function fileWriteTool(cwd) {
3248
3920
  const tool = createWriteToolDefinition(cwd, {
3249
3921
  operations: {
3250
3922
  mkdir: async (dir) => {
3251
- await mkdir5(dir, { recursive: true });
3923
+ await mkdir6(dir, { recursive: true });
3252
3924
  },
3253
3925
  // Pi invokes this inside its per-file mutation queue, so the snapshot
3254
3926
  // and write cannot race another built-in edit/write on the same file.
3255
- writeFile: async (path13, content) => {
3256
- const previous = await readFile4(path13, "utf8").catch((error) => {
3927
+ writeFile: async (path15, content) => {
3928
+ const previous = await readFile5(path15, "utf8").catch((error) => {
3257
3929
  if (error.code === "ENOENT") return void 0;
3258
3930
  throw error;
3259
3931
  });
3260
- await writeFile3(path13, content, "utf8");
3932
+ await writeFile4(path15, content, "utf8");
3261
3933
  fileChange = boundFileChange(
3262
3934
  previous === void 0 ? { path: args.path, content } : {
3263
3935
  path: args.path,
@@ -3278,8 +3950,8 @@ var init_file_change = __esm({
3278
3950
  "use strict";
3279
3951
  init_src();
3280
3952
  previewLimit = 256e3;
3281
- EditResultSchema = z13.object({
3282
- details: z13.object({ patch: z13.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
3953
+ EditResultSchema = z15.object({
3954
+ details: z15.object({ patch: z15.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
3283
3955
  });
3284
3956
  }
3285
3957
  });
@@ -3291,7 +3963,7 @@ import {
3291
3963
  createAgentSession as createAgentSession2,
3292
3964
  SessionManager as SessionManager2
3293
3965
  } from "@earendil-works/pi-coding-agent";
3294
- import { z as z14 } from "zod";
3966
+ import { z as z16 } from "zod";
3295
3967
  function toolOutput(value) {
3296
3968
  const parsed = ToolResultSchema.safeParse(value);
3297
3969
  const text = parsed.success ? parsed.data.content?.filter((part) => part.type === "text").map((part) => part.text || "").join("\n") || "Completed without text output." : boundedJson(value, 4e3);
@@ -3301,16 +3973,18 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
3301
3973
  return {
3302
3974
  async open(sessionId, cwd, sessionDirectory, selected = legacyModel, run) {
3303
3975
  const model = await models2.resolve(selected);
3976
+ const sessionOptions = await models2.sessionOptions(cwd);
3304
3977
  const { session } = await createSession({
3305
3978
  cwd,
3306
3979
  model,
3307
- modelRuntime: models2.runtime,
3980
+ ...sessionOptions,
3308
3981
  sessionManager: SessionManager2.continueRecent(cwd, sessionDirectory),
3309
3982
  customTools: [
3310
3983
  fileWriteTool(cwd),
3311
3984
  ...run?.gitFlow && delivery2 ? [deliveryTool(sessionId, delivery2)] : []
3312
3985
  ]
3313
3986
  });
3987
+ if (sessionOptions.resourceLoader) await session.bindExtensions({});
3314
3988
  return {
3315
3989
  get model() {
3316
3990
  const current = session.model;
@@ -3405,7 +4079,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
3405
4079
  return false;
3406
4080
  }
3407
4081
  const filePaths = /* @__PURE__ */ new Map();
3408
- const FileArgsSchema = z14.object({ path: z14.string() });
4082
+ const FileArgsSchema = z16.object({ path: z16.string() });
3409
4083
  function handleTool(event) {
3410
4084
  if (event.type === "tool_execution_start") {
3411
4085
  flushText();
@@ -3470,8 +4144,8 @@ var init_pi = __esm({
3470
4144
  init_models();
3471
4145
  init_delivery_tool();
3472
4146
  init_file_change();
3473
- ToolResultSchema = z14.object({
3474
- content: z14.array(z14.object({ type: z14.string(), text: z14.string().optional() })).optional()
4147
+ ToolResultSchema = z16.object({
4148
+ content: z16.array(z16.object({ type: z16.string(), text: z16.string().optional() })).optional()
3475
4149
  }).passthrough();
3476
4150
  boundedJson = (value, limit) => {
3477
4151
  let text;
@@ -3486,19 +4160,19 @@ var init_pi = __esm({
3486
4160
  });
3487
4161
 
3488
4162
  // src/projects.ts
3489
- import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
4163
+ import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
3490
4164
  import { randomUUID as randomUUID6 } from "node:crypto";
3491
- import { z as z15 } from "zod";
4165
+ import { z as z17 } from "zod";
3492
4166
  import { constants as constants2 } from "node:fs";
3493
- import { homedir as homedir3 } from "node:os";
3494
- import path8 from "node:path";
4167
+ import { homedir as homedir4 } from "node:os";
4168
+ import path10 from "node:path";
3495
4169
  import { Effect as Effect13, Semaphore as Semaphore3 } from "effect";
3496
4170
  function resolveProjectsRoot(input) {
3497
4171
  return Effect13.tryPromise({
3498
4172
  try: async () => {
3499
4173
  const trimmed = input.trim();
3500
- const expanded = trimmed === "~" ? homedir3() : trimmed.startsWith("~/") ? path8.join(homedir3(), trimmed.slice(2)) : trimmed;
3501
- if (!path8.isAbsolute(expanded))
4174
+ const expanded = trimmed === "~" ? homedir4() : trimmed.startsWith("~/") ? path10.join(homedir4(), trimmed.slice(2)) : trimmed;
4175
+ if (!path10.isAbsolute(expanded))
3502
4176
  throw new Error("Use an absolute server path or a path beginning with ~/.");
3503
4177
  const resolved = await realpath(expanded);
3504
4178
  if (!(await stat3(resolved)).isDirectory())
@@ -3520,34 +4194,34 @@ function projectLock(store2) {
3520
4194
  return lock;
3521
4195
  }
3522
4196
  async function gitDirectory(directory) {
3523
- const location = path8.join(directory, ".git");
4197
+ const location = path10.join(directory, ".git");
3524
4198
  const info = await stat3(location);
3525
4199
  if (info.isDirectory()) return location;
3526
- const contents = await readFile5(location, "utf8");
4200
+ const contents = await readFile6(location, "utf8");
3527
4201
  const match = /^gitdir: (.+)\s*$/m.exec(contents);
3528
4202
  if (!match) throw new Error("Invalid Git worktree metadata.");
3529
- const resolved = await realpath(path8.resolve(directory, match[1].trim()));
4203
+ const resolved = await realpath(path10.resolve(directory, match[1].trim()));
3530
4204
  if (!(await stat3(resolved)).isDirectory()) throw new Error("Git directory is missing.");
3531
4205
  return resolved;
3532
4206
  }
3533
4207
  async function identity(directory, preferred) {
3534
- const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
4208
+ const filename = path10.join(await gitDirectory(directory), "agentrific-project-id");
3535
4209
  try {
3536
- return ProjectId.parse(await readFile5(filename, "utf8"));
4210
+ return ProjectId.parse(await readFile6(filename, "utf8"));
3537
4211
  } catch (error) {
3538
- if (!z15.object({ code: z15.literal("ENOENT") }).safeParse(error).success) throw error;
3539
- const legacyFilename = path8.join(await gitDirectory(directory), "chatplans-project-id");
3540
- const legacy = await readFile5(legacyFilename, "utf8").catch((cause) => {
3541
- if (!z15.object({ code: z15.literal("ENOENT") }).safeParse(cause).success) throw cause;
4212
+ if (!z17.object({ code: z17.literal("ENOENT") }).safeParse(error).success) throw error;
4213
+ const legacyFilename = path10.join(await gitDirectory(directory), "chatplans-project-id");
4214
+ const legacy = await readFile6(legacyFilename, "utf8").catch((cause) => {
4215
+ if (!z17.object({ code: z17.literal("ENOENT") }).safeParse(cause).success) throw cause;
3542
4216
  return null;
3543
4217
  });
3544
4218
  const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
3545
4219
  try {
3546
- await writeFile4(filename, id + "\n", { flag: "wx" });
4220
+ await writeFile5(filename, id + "\n", { flag: "wx" });
3547
4221
  return id;
3548
4222
  } catch (writeError) {
3549
- if (!z15.object({ code: z15.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
3550
- return ProjectId.parse(await readFile5(filename, "utf8"));
4223
+ if (!z17.object({ code: z17.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
4224
+ return ProjectId.parse(await readFile6(filename, "utf8"));
3551
4225
  }
3552
4226
  }
3553
4227
  }
@@ -3560,15 +4234,15 @@ function scanProjects(root, store2) {
3560
4234
  const entries = await readdir2(root, { withFileTypes: true });
3561
4235
  for (const entry of entries) {
3562
4236
  if (!entry.isDirectory()) continue;
3563
- const directory = path8.join(root, entry.name);
3564
- if (!await stat3(path8.join(directory, ".git")).catch(() => null)) continue;
4237
+ const directory = path10.join(root, entry.name);
4238
+ if (!await stat3(path10.join(directory, ".git")).catch(() => null)) continue;
3565
4239
  const previous = registered.find((project2) => project2.path === directory);
3566
4240
  let id = await identity(directory, previous?.available ? void 0 : previous?.id);
3567
4241
  let existing = registered.find((project2) => project2.id === id);
3568
- if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat3(path8.join(existing.path, ".git")).catch(() => null)) {
4242
+ if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat3(path10.join(existing.path, ".git")).catch(() => null)) {
3569
4243
  id = randomUUID6();
3570
- await writeFile4(
3571
- path8.join(await gitDirectory(directory), "agentrific-project-id"),
4244
+ await writeFile5(
4245
+ path10.join(await gitDirectory(directory), "agentrific-project-id"),
3572
4246
  id + "\n"
3573
4247
  );
3574
4248
  existing = void 0;
@@ -3601,16 +4275,16 @@ function reconnectProject(root, id, input, store2) {
3601
4275
  ))
3602
4276
  throw new Error("Stop this project's running sessions before reconnecting its folder.");
3603
4277
  const directory = await Effect13.runPromise(resolveProjectsRoot(input));
3604
- if (path8.dirname(directory) !== await realpath(root))
4278
+ if (path10.dirname(directory) !== await realpath(root))
3605
4279
  throw new Error("Choose a Git repository directly inside the configured projects folder.");
3606
- const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
4280
+ const filename = path10.join(await gitDirectory(directory), "agentrific-project-id");
3607
4281
  const replacedId = await identity(directory);
3608
- const project = { id, name: path8.basename(directory), path: directory, available: true };
3609
- await writeFile4(filename, id + "\n");
4282
+ const project = { id, name: path10.basename(directory), path: directory, available: true };
4283
+ await writeFile5(filename, id + "\n");
3610
4284
  try {
3611
4285
  await Effect13.runPromise(store2.relocateProject(project, previous.path, replacedId));
3612
4286
  } catch (error) {
3613
- await writeFile4(filename, replacedId + "\n");
4287
+ await writeFile5(filename, replacedId + "\n");
3614
4288
  throw error;
3615
4289
  }
3616
4290
  return project;
@@ -3623,13 +4297,13 @@ var init_projects = __esm({
3623
4297
  "src/projects.ts"() {
3624
4298
  "use strict";
3625
4299
  locks2 = /* @__PURE__ */ new WeakMap();
3626
- ProjectId = z15.string().trim().min(1).max(255);
4300
+ ProjectId = z17.string().trim().min(1).max(255);
3627
4301
  }
3628
4302
  });
3629
4303
 
3630
4304
  // src/project-git.ts
3631
- import { execFile as execFile3 } from "node:child_process";
3632
- import { promisify as promisify3 } from "node:util";
4305
+ import { execFile as execFile4 } from "node:child_process";
4306
+ import { promisify as promisify4 } from "node:util";
3633
4307
  function repositoryIdentity(remote) {
3634
4308
  const input = remote.trim();
3635
4309
  if (!input || input.startsWith("/") || input.startsWith(".")) return null;
@@ -3643,14 +4317,14 @@ function repositoryIdentity(remote) {
3643
4317
  if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return null;
3644
4318
  if (url.search || url.hash || url.port) return null;
3645
4319
  const host2 = url.hostname.toLowerCase();
3646
- let path13 = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
3647
- if (!host2 || !path13 || path13.includes("%") || path13.includes("//")) return null;
3648
- if (["github.com", "bitbucket.org"].includes(host2)) path13 = path13.toLowerCase();
3649
- const key = `${host2}/${path13}`;
4320
+ let path15 = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
4321
+ if (!host2 || !path15 || path15.includes("%") || path15.includes("//")) return null;
4322
+ if (["github.com", "bitbucket.org"].includes(host2)) path15 = path15.toLowerCase();
4323
+ const key = `${host2}/${path15}`;
3650
4324
  return { key, display: key };
3651
4325
  }
3652
4326
  async function git(directory, args) {
3653
- const { stdout } = await execute3("git", ["-C", directory, ...args], {
4327
+ const { stdout } = await execute4("git", ["-C", directory, ...args], {
3654
4328
  timeout: 3e3,
3655
4329
  maxBuffer: 1024 * 1024,
3656
4330
  env: { ...workflowEnvironment(), GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
@@ -3676,7 +4350,7 @@ async function inspect(directory) {
3676
4350
  git(directory, ["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => null),
3677
4351
  // NUL-separated records avoid ambiguous filenames/newlines. --no-renames
3678
4352
  // keeps each entry one record, so we only need its two status characters.
3679
- execute3(
4353
+ execute4(
3680
4354
  "git",
3681
4355
  [
3682
4356
  "-C",
@@ -3724,47 +4398,47 @@ function projectGitStatus(directory) {
3724
4398
  observations.set(directory, { expires: Date.now() + 15e3, value });
3725
4399
  return value;
3726
4400
  }
3727
- var execute3, observations;
4401
+ var execute4, observations;
3728
4402
  var init_project_git = __esm({
3729
4403
  "src/project-git.ts"() {
3730
4404
  "use strict";
3731
4405
  init_workflow_command();
3732
- execute3 = promisify3(execFile3);
4406
+ execute4 = promisify4(execFile4);
3733
4407
  observations = /* @__PURE__ */ new Map();
3734
4408
  }
3735
4409
  });
3736
4410
 
3737
4411
  // src/push-schema.ts
3738
- import { z as z16 } from "zod";
4412
+ import { z as z18 } from "zod";
3739
4413
  var PushBindingSchema, PushStateSchema, PendingPushSchema;
3740
4414
  var init_push_schema = __esm({
3741
4415
  "src/push-schema.ts"() {
3742
4416
  "use strict";
3743
4417
  init_push();
3744
- PushBindingSchema = z16.object({
3745
- bindingId: z16.string().uuid(),
3746
- installationId: z16.string().uuid(),
3747
- deviceId: z16.string().uuid()
4418
+ PushBindingSchema = z18.object({
4419
+ bindingId: z18.string().uuid(),
4420
+ installationId: z18.string().uuid(),
4421
+ deviceId: z18.string().uuid()
3748
4422
  });
3749
- PushStateSchema = z16.object({
3750
- bindings: z16.array(PushBindingSchema).max(10),
3751
- error: z16.string().nullable()
4423
+ PushStateSchema = z18.object({
4424
+ bindings: z18.array(PushBindingSchema).max(10),
4425
+ error: z18.string().nullable()
3752
4426
  });
3753
4427
  PendingPushSchema = PushEventSchema.extend({
3754
- remaining: z16.array(PushBindingSchema),
3755
- createdAt: z16.number(),
3756
- attempts: z16.number().int().nonnegative(),
3757
- nextAt: z16.number()
4428
+ remaining: z18.array(PushBindingSchema),
4429
+ createdAt: z18.number(),
4430
+ attempts: z18.number().int().nonnegative(),
4431
+ nextAt: z18.number()
3758
4432
  });
3759
4433
  }
3760
4434
  });
3761
4435
 
3762
4436
  // src/store.ts
3763
4437
  import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
3764
- import path9 from "node:path";
4438
+ import path11 from "node:path";
3765
4439
  import { DatabaseSync } from "node:sqlite";
3766
4440
  import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
3767
- import { z as z17 } from "zod";
4441
+ import { z as z19 } from "zod";
3768
4442
  import { randomUUID as randomUUID7 } from "node:crypto";
3769
4443
  var StorageError, PayloadRow, EventRow, SqliteSessionStore, SessionStore;
3770
4444
  var init_store = __esm({
@@ -3777,8 +4451,8 @@ var init_store = __esm({
3777
4451
  init_src();
3778
4452
  StorageError = class extends Data3.TaggedError("StorageError") {
3779
4453
  };
3780
- PayloadRow = z17.object({ payload: z17.string() });
3781
- EventRow = z17.object({ sequence: z17.number(), recorded_at: z17.string(), payload: z17.string() });
4454
+ PayloadRow = z19.object({ payload: z19.string() });
4455
+ EventRow = z19.object({ sequence: z19.number(), recorded_at: z19.string(), payload: z19.string() });
3782
4456
  SqliteSessionStore = class {
3783
4457
  constructor(db) {
3784
4458
  this.db = db;
@@ -3829,7 +4503,7 @@ var init_store = __esm({
3829
4503
  const row = PayloadRow.parse(
3830
4504
  this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
3831
4505
  );
3832
- return z17.string().uuid().parse(JSON.parse(row.payload));
4506
+ return z19.string().uuid().parse(JSON.parse(row.payload));
3833
4507
  });
3834
4508
  }
3835
4509
  saveDevice(device, tokenHash) {
@@ -3917,10 +4591,10 @@ var init_store = __esm({
3917
4591
  `).all();
3918
4592
  const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
3919
4593
  for (const row of legacy) {
3920
- const id = z17.object({ id: z17.string() }).parse(row).id;
4594
+ const id = z19.object({ id: z19.string() }).parse(row).id;
3921
4595
  insert.run(
3922
4596
  id,
3923
- JSON.stringify({ id, name: id, path: path9.join(root, id), available: false })
4597
+ JSON.stringify({ id, name: id, path: path11.join(root, id), available: false })
3924
4598
  );
3925
4599
  }
3926
4600
  return this.db.prepare("SELECT payload FROM projects").all().map((row) => ProjectSchema.parse(JSON.parse(PayloadRow.parse(row).payload)));
@@ -3957,7 +4631,7 @@ var init_store = __esm({
3957
4631
  this.db.prepare(
3958
4632
  "UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
3959
4633
  ).run(project.path, previousPath);
3960
- const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path9.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
4634
+ const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path11.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
3961
4635
  for (const row of this.db.prepare("SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?").all(project.id)) {
3962
4636
  const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
3963
4637
  if (session.workspacePath) {
@@ -4127,7 +4801,7 @@ var init_store = __esm({
4127
4801
  if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
4128
4802
  const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
4129
4803
  if (notification) {
4130
- const count = z17.object({ total: z17.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
4804
+ const count = z19.object({ total: z19.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
4131
4805
  if (count.total >= 1e3)
4132
4806
  this.db.prepare(
4133
4807
  "DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
@@ -4173,13 +4847,13 @@ var init_store = __esm({
4173
4847
  Effect14.acquireRelease(
4174
4848
  Effect14.try({
4175
4849
  try: () => {
4176
- mkdirSync2(path9.dirname(filename), { recursive: true });
4850
+ mkdirSync2(path11.dirname(filename), { recursive: true });
4177
4851
  const db = new DatabaseSync(filename);
4178
4852
  chmodSync(filename, 384);
4179
4853
  for (const suffix of ["-wal", "-shm"])
4180
4854
  if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
4181
4855
  try {
4182
- const schema = z17.object({ user_version: z17.number() }).parse(db.prepare("PRAGMA user_version").get());
4856
+ const schema = z19.object({ user_version: z19.number() }).parse(db.prepare("PRAGMA user_version").get());
4183
4857
  if (schema.user_version > 1)
4184
4858
  throw new Error("This database requires a newer Agentrific server.");
4185
4859
  db.exec(`
@@ -4216,12 +4890,12 @@ var init_store = __esm({
4216
4890
 
4217
4891
  // src/worktree-cleanup.ts
4218
4892
  import { lstat as lstat2, rm as rm2 } from "node:fs/promises";
4219
- import path10 from "node:path";
4893
+ import path12 from "node:path";
4220
4894
  import { Effect as Effect15 } from "effect";
4221
4895
  async function removeRunFiles(dataRoot2, sessionId) {
4222
- if (path10.basename(sessionId) !== sessionId || sessionId === "." || sessionId === "..")
4896
+ if (path12.basename(sessionId) !== sessionId || sessionId === "." || sessionId === "..")
4223
4897
  throw new Error("Invalid session directory.");
4224
- await rm2(path10.join(dataRoot2, "pi", sessionId), { recursive: true, force: true });
4898
+ await rm2(path12.join(dataRoot2, "pi", sessionId), { recursive: true, force: true });
4225
4899
  }
4226
4900
  var WorktreeCleanup;
4227
4901
  var init_worktree_cleanup = __esm({
@@ -4240,12 +4914,12 @@ var init_worktree_cleanup = __esm({
4240
4914
  projectsRoot;
4241
4915
  async remove(session, discardChanges = true) {
4242
4916
  if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
4243
- const workspace = path10.resolve(session.workspacePath);
4917
+ const workspace = path12.resolve(session.workspacePath);
4244
4918
  const info = await lstat2(workspace).catch((error) => {
4245
4919
  if (error.code === "ENOENT") return void 0;
4246
4920
  throw error;
4247
4921
  });
4248
- if (workspace !== path10.join(this.root, session.id) || path10.basename(session.id) !== session.id) {
4922
+ if (workspace !== path12.join(this.root, session.id) || path12.basename(session.id) !== session.id) {
4249
4923
  if (!info) return;
4250
4924
  throw new Error("Refusing to remove a worktree outside the managed workspaces directory.");
4251
4925
  }
@@ -4331,14 +5005,14 @@ import { styleText as styleText4 } from "node:util";
4331
5005
  import cors from "@fastify/cors";
4332
5006
  import multipart from "@fastify/multipart";
4333
5007
  import { randomUUID as randomUUID9 } from "node:crypto";
4334
- import { execFile as execFile4 } from "node:child_process";
4335
- import { promisify as promisify4 } from "node:util";
5008
+ import { execFile as execFile5 } from "node:child_process";
5009
+ import { promisify as promisify5 } from "node:util";
4336
5010
  import { realpath as realpath2 } from "node:fs/promises";
4337
- import { homedir as homedir4 } from "node:os";
4338
- import path11 from "node:path";
5011
+ import { homedir as homedir5 } from "node:os";
5012
+ import path13 from "node:path";
4339
5013
  import { WebSocket } from "ws";
4340
5014
  import { Effect as Effect17, ManagedRuntime } from "effect";
4341
- import { z as z18 } from "zod";
5015
+ import { z as z20 } from "zod";
4342
5016
  import { ModelRuntime } from "@earendil-works/pi-coding-agent";
4343
5017
  function publish(event, workflowUpdate = false) {
4344
5018
  const previous = event.type === "session.updated" ? Effect17.runSync(store.get(event.session.id)) : void 0;
@@ -4399,18 +5073,18 @@ async function resolveSessionProject(projectId) {
4399
5073
  return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
4400
5074
  const projectPath = await realpath2(project.path);
4401
5075
  const resolvedRoot = await realpath2(projectsRoot);
4402
- if (path11.dirname(projectPath) !== resolvedRoot)
5076
+ if (path13.dirname(projectPath) !== resolvedRoot)
4403
5077
  return { error: "Project path is outside projects root.", status: 400 };
4404
5078
  return { projectPath };
4405
5079
  }
4406
5080
  async function resolveSessionWorkspace(id, gitFlow, projectPath) {
4407
- const worktreePath = path11.join(worktreesRoot, id);
5081
+ const worktreePath = path13.join(worktreesRoot, id);
4408
5082
  const branch = gitFlow ? `agentrific/${id.slice(0, 8)}` : null;
4409
5083
  const useProjectCheckout = gitFlow?.workspace === "checkout";
4410
5084
  const workspacePath = !gitFlow || useProjectCheckout ? projectPath : worktreePath;
4411
5085
  if (useProjectCheckout) {
4412
5086
  const serverPath = await realpath2(process.cwd());
4413
- if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path11.sep}`))
5087
+ if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path13.sep}`))
4414
5088
  return {
4415
5089
  error: "The server runs from this project. Choose a worktree for this task.",
4416
5090
  status: 409
@@ -4546,10 +5220,10 @@ async function withSessionTaskLock(id, operation) {
4546
5220
  return withTaskLock(lockId, operation);
4547
5221
  }
4548
5222
  function localServerUrl() {
4549
- const address = z18.object({ port: z18.number() }).safeParse(app.server.address());
5223
+ const address = z20.object({ port: z20.number() }).safeParse(app.server.address());
4550
5224
  return address.success ? `http://${host}:${address.data.port}` : void 0;
4551
5225
  }
4552
- var host, port, defaultProjectsRoot, worktreesRoot, execFileAsync, app, dataRoot, releaseLock, effects, store, security, sandboxPushUrl, testPushUrl, push, allowedOrigins, attachmentEffects, attachments, attachmentCleanup, models, delivery, preparation, agentEffects, runtime, worktreeCleanup, maxWorktreeAgeDays, pruneWorktrees, worktreeCleanupTimer, updates, maintenance, stopUpdateDiscovery, HistoryQuery, StartTaskBody, automation, automationTimer, closeControl;
5226
+ var host, port, defaultProjectsRoot, worktreesRoot, execFileAsync, app, dataRoot, releaseLock, effects, store, security, sandboxPushUrl, testPushUrl, push, allowedOrigins, attachmentEffects, attachments, attachmentCleanup, claudeCode, modelRuntime, models, delivery, preparation, agentEffects, runtime, worktreeCleanup, maxWorktreeAgeDays, pruneWorktrees, worktreeCleanupTimer, updates, maintenance, stopUpdateDiscovery, HistoryQuery, StartTaskBody, automation, automationTimer, phoneOrigin, phone, closeControl;
4553
5227
  var init_index = __esm({
4554
5228
  async "src/index.ts"() {
4555
5229
  "use strict";
@@ -4560,6 +5234,7 @@ var init_index = __esm({
4560
5234
  init_event_server();
4561
5235
  init_security();
4562
5236
  init_control();
5237
+ init_desktop_phone();
4563
5238
  init_version();
4564
5239
  init_maintenance();
4565
5240
  init_updates();
@@ -4569,6 +5244,7 @@ var init_index = __esm({
4569
5244
  init_delivery();
4570
5245
  init_preparation();
4571
5246
  init_delivery_writer();
5247
+ init_claude_code();
4572
5248
  init_models();
4573
5249
  init_pi();
4574
5250
  init_projects();
@@ -4579,11 +5255,11 @@ var init_index = __esm({
4579
5255
  init_repo_chat();
4580
5256
  host = process.env.AGENTRIFIC_HOST ?? "127.0.0.1";
4581
5257
  port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
4582
- defaultProjectsRoot = path11.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
4583
- worktreesRoot = path11.resolve(
4584
- process.env.AGENTRIFIC_WORKTREES_DIR ?? path11.join(homedir4(), ".agentrific", "workspaces")
5258
+ defaultProjectsRoot = path13.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
5259
+ worktreesRoot = path13.resolve(
5260
+ process.env.AGENTRIFIC_WORKTREES_DIR ?? path13.join(homedir5(), ".agentrific", "workspaces")
4585
5261
  );
4586
- execFileAsync = promisify4(execFile4);
5262
+ execFileAsync = promisify5(execFile5);
4587
5263
  if (host !== "127.0.0.1" && host !== "::1")
4588
5264
  throw new Error("Bind Agentrific to loopback and expose it with Tailscale Serve or cloudflared.");
4589
5265
  app = Fastify({
@@ -4604,9 +5280,9 @@ var init_index = __esm({
4604
5280
  }
4605
5281
  }
4606
5282
  });
4607
- dataRoot = path11.resolve(process.env.AGENTRIFIC_DATA_DIR ?? "./data");
5283
+ dataRoot = path13.resolve(process.env.AGENTRIFIC_DATA_DIR ?? "./data");
4608
5284
  releaseLock = await acquireServerLock(dataRoot);
4609
- effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
5285
+ effects = ManagedRuntime.make(SessionStore.layer(path13.join(dataRoot, "sessions.sqlite")));
4610
5286
  store = await effects.runPromise(SessionStore);
4611
5287
  security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
4612
5288
  sandboxPushUrl = process.env.AGENTRIFIC_PUSH_SANDBOX === "1" ? PUSH_SANDBOX_API_URL : void 0;
@@ -4638,7 +5314,7 @@ var init_index = __esm({
4638
5314
  await registerSecurity(app, security, serverVersion, allowedOrigins);
4639
5315
  registerPushRoutes(app, push, security);
4640
5316
  attachmentEffects = ManagedRuntime.make(
4641
- Attachments.layer(path11.join(dataRoot, "attachments"), store)
5317
+ Attachments.layer(path13.join(dataRoot, "attachments"), store)
4642
5318
  );
4643
5319
  attachments = await attachmentEffects.runPromise(Attachments);
4644
5320
  await Effect17.runPromise(attachments.cleanup());
@@ -4684,7 +5360,10 @@ var init_index = __esm({
4684
5360
  return reply.type(preview ? "image/png" : attachment.mimeType).send(attachments.content(attachment, preview));
4685
5361
  }
4686
5362
  );
4687
- models = new ServerModels(await ModelRuntime.create(), store);
5363
+ claudeCode = new ClaudeCode();
5364
+ modelRuntime = await ModelRuntime.create();
5365
+ await claudeCode.refresh(modelRuntime);
5366
+ models = new ServerModels(modelRuntime, store, claudeCode);
4688
5367
  delivery = new DeliveryCoordinator(store, {
4689
5368
  publish: (event) => publish(event, true),
4690
5369
  draft: deliveryWriter(models),
@@ -4703,7 +5382,7 @@ var init_index = __esm({
4703
5382
  createPiFactory(attachments, models, delivery),
4704
5383
  publish,
4705
5384
  worktreesRoot,
4706
- path11.join(dataRoot, "pi"),
5385
+ path13.join(dataRoot, "pi"),
4707
5386
  (sessionId, messageId) => Effect17.runSync(store.hasMessage(sessionId, messageId)),
4708
5387
  (session) => delivery.settled(session.id)
4709
5388
  )
@@ -4864,7 +5543,7 @@ var init_index = __esm({
4864
5543
  return project ?? reply.code(404).send({ error: "Project not found on this server." });
4865
5544
  });
4866
5545
  app.post("/api/projects/:id/reconnect", async (request, reply) => {
4867
- const parsed = z18.object({ path: z18.string().trim().min(1).max(4096) }).safeParse(request.body);
5546
+ const parsed = z20.object({ path: z20.string().trim().min(1).max(4096) }).safeParse(request.body);
4868
5547
  if (!parsed.success)
4869
5548
  return reply.code(400).send({ error: "Enter the project's new folder path." });
4870
5549
  const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
@@ -4950,7 +5629,7 @@ var init_index = __esm({
4950
5629
  }
4951
5630
  });
4952
5631
  app.post("/api/tasks/:id/discard-unrun", async (request, reply) => {
4953
- const parsed = z18.object({ updatedAt: z18.string().datetime() }).safeParse(request.body);
5632
+ const parsed = z20.object({ updatedAt: z20.string().datetime() }).safeParse(request.body);
4954
5633
  if (!parsed.success) return reply.code(400).send({ error: "The task revision is required." });
4955
5634
  try {
4956
5635
  await deleteTask(store, request.params.id, void 0, parsed.data.updatedAt);
@@ -5020,9 +5699,9 @@ var init_index = __esm({
5020
5699
  }
5021
5700
  })
5022
5701
  );
5023
- HistoryQuery = z18.object({
5024
- after: z18.coerce.number().int().nonnegative().default(0),
5025
- limit: z18.coerce.number().int().min(1).max(1e3).default(500)
5702
+ HistoryQuery = z20.object({
5703
+ after: z20.coerce.number().int().nonnegative().default(0),
5704
+ limit: z20.coerce.number().int().min(1).max(1e3).default(500)
5026
5705
  });
5027
5706
  app.get("/api/sessions/:id/events", async (request, reply) => {
5028
5707
  const query = HistoryQuery.safeParse(request.query);
@@ -5036,7 +5715,7 @@ var init_index = __esm({
5036
5715
  const location = await resolveSessionProject(request.params.id);
5037
5716
  if (location.error !== void 0)
5038
5717
  return reply.code(location.status).send({ error: location.error });
5039
- const parsed = z18.object({ reset: z18.boolean().optional() }).safeParse(request.body ?? {});
5718
+ const parsed = z20.object({ reset: z20.boolean().optional() }).safeParse(request.body ?? {});
5040
5719
  if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
5041
5720
  try {
5042
5721
  return await openRepoChat(
@@ -5060,7 +5739,7 @@ var init_index = __esm({
5060
5739
  if (!result.session) return reply.code(result.status).send({ error: result.error });
5061
5740
  return reply.code(201).send(result.session);
5062
5741
  });
5063
- StartTaskBody = z18.object({ prompt: z18.string().trim().max(2e4).optional() }).default({});
5742
+ StartTaskBody = z20.object({ prompt: z20.string().trim().max(2e4).optional() }).default({});
5064
5743
  app.post("/api/tasks/:id/start", async (request, reply) => {
5065
5744
  const body = StartTaskBody.safeParse(request.body ?? {});
5066
5745
  if (!body.success) return reply.code(400).send({ error: "The task prompt is too long." });
@@ -5095,7 +5774,7 @@ var init_index = __esm({
5095
5774
  return automation.configure(parsed.data);
5096
5775
  });
5097
5776
  app.patch("/api/automation/queue", async (request, reply) => {
5098
- const parsed = z18.object({ taskId: z18.string().min(1), beforeId: z18.string().min(1).nullable() }).safeParse(request.body);
5777
+ const parsed = z20.object({ taskId: z20.string().min(1), beforeId: z20.string().min(1).nullable() }).safeParse(request.body);
5099
5778
  if (!parsed.success)
5100
5779
  return reply.code(400).send({ error: "Choose a task and a queue position." });
5101
5780
  try {
@@ -5152,12 +5831,31 @@ var init_index = __esm({
5152
5831
  type: "snapshot",
5153
5832
  sessions: Effect17.runSync(store.list())
5154
5833
  }));
5834
+ phone = process.env.AGENTRIFIC_DESKTOP_SERVER === "1" ? new DesktopPhoneConnection(
5835
+ () => {
5836
+ const origin = localServerUrl();
5837
+ if (!origin) throw new Error("Start the local server before connecting your phone.");
5838
+ return origin;
5839
+ },
5840
+ (url) => {
5841
+ if (phoneOrigin) {
5842
+ const index = allowedOrigins.indexOf(phoneOrigin);
5843
+ if (index >= 0) allowedOrigins.splice(index, 1);
5844
+ }
5845
+ phoneOrigin = url || void 0;
5846
+ process.env.AGENTRIFIC_PUBLIC_URL = url || "";
5847
+ if (url && !allowedOrigins.includes(url)) allowedOrigins.push(url);
5848
+ },
5849
+ path13.join(dataRoot, "phone-connection")
5850
+ ) : void 0;
5155
5851
  closeControl = await startControl(
5156
5852
  dataRoot,
5157
5853
  security,
5158
5854
  () => ({
5159
5855
  serverId: security.serverId,
5160
5856
  publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
5857
+ phoneProvider: phone?.snapshot().provider,
5858
+ phoneProgress: phone?.snapshot().progress,
5161
5859
  localUrl: localServerUrl(),
5162
5860
  serverVersion,
5163
5861
  connectedDevices: new Set(security.sockets.values()).size,
@@ -5170,10 +5868,12 @@ var init_index = __esm({
5170
5868
  async () => {
5171
5869
  await app.close();
5172
5870
  process.exit(0);
5173
- }
5871
+ },
5872
+ phone
5174
5873
  );
5175
5874
  push.start();
5176
5875
  app.addHook("onClose", async () => {
5876
+ await phone?.close();
5177
5877
  await push.close();
5178
5878
  maintenance.enter();
5179
5879
  stopUpdateDiscovery();
@@ -5206,9 +5906,9 @@ var init_index = __esm({
5206
5906
 
5207
5907
  // src/cli.ts
5208
5908
  init_warnings();
5209
- import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
5210
- import { homedir as homedir5 } from "node:os";
5211
- import path12 from "node:path";
5909
+ import { mkdir as mkdir7, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
5910
+ import { homedir as homedir6 } from "node:os";
5911
+ import path14 from "node:path";
5212
5912
  import { fileURLToPath } from "node:url";
5213
5913
 
5214
5914
  // src/service.ts
@@ -5307,284 +6007,18 @@ init_version();
5307
6007
  init_pairing();
5308
6008
  import { spawn as spawn2 } from "node:child_process";
5309
6009
  import { parseArgs } from "node:util";
5310
- import { z as z19 } from "zod";
6010
+ import { z as z21 } from "zod";
5311
6011
 
5312
6012
  // src/terminal.ts
5313
6013
  init_pairing();
5314
6014
  init_cli_output();
6015
+ init_tunnels();
6016
+ init_opentunnel();
5315
6017
  import { createInterface } from "node:readline/promises";
5316
6018
  import select from "@inquirer/select";
5317
6019
  import { emitKeypressEvents as emitKeypressEvents2 } from "node:readline";
5318
6020
  import { styleText as styleText3 } from "node:util";
5319
-
5320
- // src/tunnels.ts
5321
- init_cli_output();
5322
- import { spawn } from "node:child_process";
5323
- import { access, stat } from "node:fs/promises";
5324
- import { constants } from "node:fs";
5325
- import { homedir as homedir2 } from "node:os";
5326
- import path3 from "node:path";
5327
- import { z as z5 } from "zod";
5328
- function signalChild(child, signal) {
5329
- if (!child.pid) return;
5330
- if (process.platform === "win32") {
5331
- child.kill(signal);
5332
- return;
5333
- }
5334
- try {
5335
- process.kill(-child.pid, signal);
5336
- } catch (error) {
5337
- if (!z5.object({ code: z5.literal("ESRCH") }).safeParse(error).success) child.kill(signal);
5338
- }
5339
- }
5340
- function execute2(file, args, timeoutMs, env = process.env) {
5341
- return new Promise((resolve2, reject) => {
5342
- const child = spawn(file, args, {
5343
- env,
5344
- detached: process.platform !== "win32",
5345
- stdio: ["ignore", "pipe", "pipe"]
5346
- });
5347
- let stdout = "";
5348
- let stderr = "";
5349
- const timeout = setTimeout(() => {
5350
- signalChild(child, "SIGKILL");
5351
- reject(new Error("Command probe timed out."));
5352
- }, timeoutMs);
5353
- child.stdout?.on("data", (data) => {
5354
- stdout += data.toString();
5355
- checkOutput();
5356
- });
5357
- child.stderr?.on("data", (data) => {
5358
- stderr += data.toString();
5359
- checkOutput();
5360
- });
5361
- child.once("error", (error) => {
5362
- clearTimeout(timeout);
5363
- reject(error);
5364
- });
5365
- child.once("exit", () => signalChild(child, "SIGKILL"));
5366
- child.once("close", (code) => {
5367
- clearTimeout(timeout);
5368
- if (code !== 0) reject(new Error("Command probe failed."));
5369
- else resolve2({ stdout, stderr });
5370
- });
5371
- function checkOutput() {
5372
- if (stdout.length + stderr.length <= 1048576) return;
5373
- signalChild(child, "SIGKILL");
5374
- reject(new Error("Command probe output exceeded its limit."));
5375
- }
5376
- });
5377
- }
5378
- async function findExecutable(name) {
5379
- const candidates = (process.env.PATH ?? "").split(path3.delimiter).map((directory) => path3.resolve(directory, name));
5380
- if (process.platform === "darwin") {
5381
- candidates.push(`/opt/homebrew/bin/${name}`, `/usr/local/bin/${name}`);
5382
- if (name === "tailscale")
5383
- candidates.push("/Applications/Tailscale.app/Contents/MacOS/Tailscale");
5384
- }
5385
- for (const candidate of candidates) {
5386
- try {
5387
- await access(candidate, constants.X_OK);
5388
- if ((await stat(candidate)).isFile()) return candidate;
5389
- } catch {
5390
- continue;
5391
- }
5392
- }
5393
- return void 0;
5394
- }
5395
- var TcpConfig = z5.object({ TCP: z5.record(z5.string(), z5.unknown()).optional() });
5396
- var ServeConfig = TcpConfig.extend({
5397
- Foreground: z5.record(z5.string(), TcpConfig).optional()
5398
- });
5399
- async function probeTailscale() {
5400
- const option = { provider: "tailscale", available: false, reason: "Not installed" };
5401
- const executable = await findExecutable("tailscale");
5402
- if (!executable) return option;
5403
- option.executable = executable;
5404
- const env = { ...process.env, TAILSCALE_BE_CLI: "1" };
5405
- try {
5406
- const response = await execute2(executable, ["status", "--json"], 3e3, env);
5407
- const status = z5.object({
5408
- BackendState: z5.string(),
5409
- CertDomains: z5.array(z5.string()).optional(),
5410
- Self: z5.object({ DNSName: z5.string() }).nullable().optional()
5411
- }).parse(JSON.parse(response.stdout));
5412
- if (status.BackendState !== "Running")
5413
- return { ...option, reason: "Installed \xB7 connect Tailscale first" };
5414
- const dns = status.Self?.DNSName.replace(/\.$/, "");
5415
- if (!dns || !status.CertDomains?.includes(dns))
5416
- return { ...option, reason: "Enable HTTPS certificates in your tailnet first" };
5417
- const help = await execute2(executable, ["serve", "--help"], 2e3, env);
5418
- if (!`${help.stdout}${help.stderr}`.includes("--https"))
5419
- return { ...option, reason: "Update Tailscale to use foreground HTTPS Serve" };
5420
- const responseConfig = await execute2(executable, ["serve", "status", "--json"], 3e3, env);
5421
- const config = ServeConfig.parse(JSON.parse(responseConfig.stdout) ?? {});
5422
- const occupied = new Set(Object.keys(config.TCP ?? {}));
5423
- for (const foreground of Object.values(config.Foreground ?? {}))
5424
- for (const port3 of Object.keys(foreground.TCP ?? {})) occupied.add(port3);
5425
- const port2 = [443, ...Array.from({ length: 10 }, (_, index) => 8443 + index)].find(
5426
- (candidate) => !occupied.has(String(candidate))
5427
- );
5428
- if (!port2) return { ...option, reason: "HTTPS Serve ports are in use \xB7 paste an existing URL" };
5429
- return {
5430
- ...option,
5431
- available: true,
5432
- reason: "Private to your tailnet \xB7 Tailscale required on your phone",
5433
- publicUrl: new URL(`https://${dns}${port2 === 443 ? "" : `:${port2}`}`).origin,
5434
- httpsPort: port2
5435
- };
5436
- } catch {
5437
- return { ...option, reason: "Installed \xB7 could not verify daemon or Serve access" };
5438
- }
5439
- }
5440
- async function probeCloudflared(home) {
5441
- const option = {
5442
- provider: "cloudflared",
5443
- available: false,
5444
- reason: "Not installed"
5445
- };
5446
- const executable = await findExecutable("cloudflared");
5447
- if (!executable) return option;
5448
- option.executable = executable;
5449
- try {
5450
- await execute2(executable, ["--version"], 2e3);
5451
- const help = await execute2(executable, ["tunnel", "--help"], 2e3);
5452
- if (!`${help.stdout}${help.stderr}`.includes("--url"))
5453
- return { ...option, reason: "Installed \xB7 this version does not support Quick Tunnels" };
5454
- } catch {
5455
- return { ...option, reason: "Installed \xB7 command probe failed or timed out" };
5456
- }
5457
- const directories = [
5458
- path3.join(home, ".cloudflared"),
5459
- path3.join(home, ".cloudflare-warp"),
5460
- path3.join(home, "cloudflare-warp"),
5461
- "/etc/cloudflared",
5462
- "/usr/local/etc/cloudflared"
5463
- ];
5464
- const configurations = await Promise.all(
5465
- directories.flatMap(
5466
- (directory) => ["config.yml", "config.yaml"].map(
5467
- async (name) => access(path3.join(directory, name), constants.F_OK).then(
5468
- () => true,
5469
- () => false
5470
- )
5471
- )
5472
- )
5473
- );
5474
- if (configurations.some(Boolean))
5475
- return { ...option, reason: "Existing tunnel configuration \xB7 paste its HTTPS URL" };
5476
- return { ...option, available: true, reason: "Temporary public HTTPS URL \xB7 changes each start" };
5477
- }
5478
- async function detectTunnels(home = homedir2()) {
5479
- return Promise.all([probeTailscale(), probeCloudflared(home)]);
5480
- }
5481
- var ManagedTunnel = class {
5482
- constructor(option) {
5483
- this.option = option;
5484
- }
5485
- option;
5486
- child;
5487
- stopping = false;
5488
- connected = false;
5489
- closing;
5490
- running() {
5491
- return this.connected && !this.stopping;
5492
- }
5493
- stopOnSignal = () => {
5494
- void this.close();
5495
- };
5496
- stopOnExit = () => {
5497
- if (this.child) signalChild(this.child, "SIGKILL");
5498
- };
5499
- async start(origin, signal) {
5500
- signal?.throwIfAborted();
5501
- if (!this.option.available) throw new Error(this.option.reason);
5502
- const tailscale = this.option.provider === "tailscale";
5503
- const child = spawn(
5504
- this.option.executable ?? this.option.provider,
5505
- tailscale ? ["serve", "--bg=false", `--https=${this.option.httpsPort}`, origin] : ["tunnel", "--no-autoupdate", "--url", origin],
5506
- {
5507
- stdio: ["ignore", "pipe", "pipe"],
5508
- detached: process.platform !== "win32",
5509
- env: tailscale ? { ...process.env, TAILSCALE_BE_CLI: "1" } : process.env
5510
- }
5511
- );
5512
- this.child = child;
5513
- process.once("SIGINT", this.stopOnSignal);
5514
- process.once("SIGTERM", this.stopOnSignal);
5515
- process.once("exit", this.stopOnExit);
5516
- return new Promise((resolve2, reject) => {
5517
- let output = "";
5518
- let address;
5519
- let settled = false;
5520
- const abort = () => fail("Tunnel startup canceled.");
5521
- const timeout = setTimeout(
5522
- () => fail("Tunnel startup timed out. Check your connection or paste an existing HTTPS URL."),
5523
- 45e3
5524
- );
5525
- const fail = (message) => {
5526
- if (settled) return;
5527
- settled = true;
5528
- clearTimeout(timeout);
5529
- signal?.removeEventListener("abort", abort);
5530
- void this.close();
5531
- reject(new Error(message));
5532
- };
5533
- const read = (data) => {
5534
- output = `${output}${data.toString()}`.slice(-65536);
5535
- address = tailscale ? this.option.publicUrl : output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com\b/)?.[0];
5536
- const ready = tailscale ? Boolean(
5537
- address && output.includes(address) && output.includes("Available within your tailnet")
5538
- ) : output.includes("Registered tunnel connection");
5539
- if (settled || !address || !ready) return;
5540
- settled = true;
5541
- this.connected = true;
5542
- clearTimeout(timeout);
5543
- signal?.removeEventListener("abort", abort);
5544
- resolve2(address);
5545
- };
5546
- child.stdout?.on("data", read);
5547
- child.stderr?.on("data", read);
5548
- signal?.addEventListener("abort", abort, { once: true });
5549
- child.once(
5550
- "error",
5551
- () => fail(`${this.option.provider} could not start. Check the installation and permissions.`)
5552
- );
5553
- child.once("exit", () => signalChild(child, "SIGTERM"));
5554
- child.once("close", (code) => {
5555
- const unexpected = this.connected && !this.stopping;
5556
- this.connected = false;
5557
- process.removeListener("SIGINT", this.stopOnSignal);
5558
- process.removeListener("SIGTERM", this.stopOnSignal);
5559
- process.removeListener("exit", this.stopOnExit);
5560
- fail(
5561
- `${this.option.provider} exited (${code ?? "signal"}). Check tunnel access or paste an existing HTTPS URL.`
5562
- );
5563
- if (unexpected)
5564
- printError(
5565
- "The managed tunnel stopped. Restart the server to reconnect your phone; local work can continue."
5566
- );
5567
- });
5568
- });
5569
- }
5570
- close() {
5571
- this.stopping = true;
5572
- const child = this.child;
5573
- if (!child?.pid || child.exitCode !== null || child.signalCode !== null)
5574
- return Promise.resolve();
5575
- this.closing ??= new Promise((resolve2) => {
5576
- const timeout = setTimeout(() => signalChild(child, "SIGKILL"), 2e3);
5577
- child.once("close", () => {
5578
- clearTimeout(timeout);
5579
- resolve2();
5580
- });
5581
- signalChild(child, "SIGTERM");
5582
- });
5583
- return this.closing;
5584
- }
5585
- };
5586
-
5587
- // src/terminal.ts
6021
+ import path5 from "node:path";
5588
6022
  async function promptPublicUrl(signal, saved) {
5589
6023
  const prompt = createInterface({ input: process.stdin, output: process.stdout });
5590
6024
  const cancel = () => controller.abort();
@@ -5628,9 +6062,14 @@ async function selectConnectionMethod(options, signal, saved) {
5628
6062
  return select(
5629
6063
  {
5630
6064
  message: "Connection method",
5631
- default: saved ? "existing" : void 0,
6065
+ default: saved ? "existing" : "opentunnel",
5632
6066
  loop: false,
5633
6067
  choices: [
6068
+ {
6069
+ value: "opentunnel",
6070
+ name: "OpenTunnel (default)",
6071
+ description: "Built-in HTTPS connection with a persistent address. No separate CLI needed."
6072
+ },
5634
6073
  ...options.filter((option) => option.available).map((option) => ({
5635
6074
  value: option.provider,
5636
6075
  name: option.provider === "tailscale" ? "Tailscale" : "Cloudflared",
@@ -5661,13 +6100,14 @@ async function selectConnectionMethod(options, signal, saved) {
5661
6100
  { signal }
5662
6101
  );
5663
6102
  }
5664
- async function startSelectedTunnel(options, choice, origin, signal) {
6103
+ async function startSelectedTunnel(options, choice, origin, signal, dataDir) {
5665
6104
  const option = options.find((candidate) => candidate.provider === choice);
5666
- if (!option?.available) throw new Error("The selected tunnel is unavailable.");
5667
- const tunnel = new ManagedTunnel(option);
6105
+ if (choice !== "opentunnel" && !option?.available)
6106
+ throw new Error("The selected tunnel is unavailable.");
6107
+ const tunnel = choice === "opentunnel" ? new OpenTunnel(path5.join(dataDir, "phone-connection")) : new ManagedTunnel(option);
5668
6108
  try {
5669
6109
  console.log(styleText3("dim", `
5670
- Starting ${option.provider}\u2026
6110
+ Starting ${choice}\u2026
5671
6111
  `));
5672
6112
  const url = await tunnel.start(origin, signal);
5673
6113
  return { url, tunnel, remember: false };
@@ -5678,7 +6118,7 @@ async function startSelectedTunnel(options, choice, origin, signal) {
5678
6118
  return void 0;
5679
6119
  }
5680
6120
  }
5681
- async function selectPublicConnection(origin, saved) {
6121
+ async function selectPublicConnection(origin, saved, dataDir = process.env.AGENTRIFIC_DATA_DIR || ".agentrific") {
5682
6122
  const controller = new AbortController();
5683
6123
  const cancel = () => controller.abort();
5684
6124
  process.once("SIGINT", cancel);
@@ -5699,7 +6139,13 @@ async function selectPublicConnection(origin, saved) {
5699
6139
  console.log();
5700
6140
  return { url: await promptPublicUrl(controller.signal, saved), remember: true };
5701
6141
  }
5702
- const connection = await startSelectedTunnel(options, choice, origin, controller.signal);
6142
+ const connection = await startSelectedTunnel(
6143
+ options,
6144
+ choice,
6145
+ origin,
6146
+ controller.signal,
6147
+ dataDir
6148
+ );
5703
6149
  if (connection) return connection;
5704
6150
  }
5705
6151
  } finally {
@@ -5787,11 +6233,14 @@ function startTerminal(dataDir, publicUrl, tunnel) {
5787
6233
  }
5788
6234
 
5789
6235
  // src/cli.ts
6236
+ init_opentunnel();
6237
+ init_tunnels();
5790
6238
  init_cli_output();
5791
- var ConfigSchema = z19.object({
5792
- dataDir: z19.string(),
5793
- projectsDir: z19.string(),
5794
- publicUrl: z19.string().url().optional()
6239
+ var ConfigSchema = z21.object({
6240
+ dataDir: z21.string(),
6241
+ projectsDir: z21.string(),
6242
+ publicUrl: z21.string().url().optional(),
6243
+ connection: z21.enum(["opentunnel", "tailscale", "cloudflared", "existing", "local"]).optional()
5795
6244
  });
5796
6245
  function parseCommandArgs() {
5797
6246
  return parseArgs({
@@ -5799,6 +6248,7 @@ function parseCommandArgs() {
5799
6248
  options: {
5800
6249
  "data-dir": { type: "string" },
5801
6250
  "public-url": { type: "string" },
6251
+ connection: { type: "string" },
5802
6252
  version: { type: "boolean" },
5803
6253
  help: { type: "boolean" },
5804
6254
  check: { type: "boolean" },
@@ -5819,15 +6269,15 @@ async function main() {
5819
6269
  printHelp();
5820
6270
  return;
5821
6271
  }
5822
- const dataDir = path12.resolve(
5823
- values["data-dir"] ?? process.env.AGENTRIFIC_DATA_DIR ?? path12.join(homedir5(), ".agentrific")
6272
+ const dataDir = path14.resolve(
6273
+ values["data-dir"] ?? process.env.AGENTRIFIC_DATA_DIR ?? path14.join(homedir6(), ".agentrific")
5824
6274
  );
5825
- const filename = path12.join(dataDir, "config.json");
6275
+ const filename = path14.join(dataDir, "config.json");
5826
6276
  const config = ConfigSchema.parse(
5827
6277
  JSON.parse(
5828
- await readFile6(filename, "utf8").catch((error) => {
5829
- if (!z19.object({ code: z19.literal("ENOENT") }).safeParse(error).success) throw error;
5830
- return JSON.stringify({ dataDir, projectsDir: path12.join(homedir5(), "projects") });
6278
+ await readFile7(filename, "utf8").catch((error) => {
6279
+ if (!z21.object({ code: z21.literal("ENOENT") }).safeParse(error).success) throw error;
6280
+ return JSON.stringify({ dataDir, projectsDir: path14.join(homedir6(), "projects") });
5831
6281
  })
5832
6282
  )
5833
6283
  );
@@ -5844,11 +6294,12 @@ async function main() {
5844
6294
  throw new Error("Unknown command. Run agentrific --help.");
5845
6295
  }
5846
6296
  async function initCommand({ values, dataDir, filename, config }) {
5847
- await mkdir6(dataDir, { recursive: true, mode: 448 });
6297
+ await mkdir7(dataDir, { recursive: true, mode: 448 });
5848
6298
  const publicUrl = values["public-url"] ?? config.publicUrl;
6299
+ const connection = values.connection ? ConfigSchema.shape.connection.unwrap().parse(values.connection) : config.connection;
5849
6300
  if (publicUrl && new URL(publicUrl).protocol !== "https:")
5850
6301
  throw new Error("Configure an HTTPS URL from Tailscale Serve or Cloudflare Tunnel.");
5851
- await writeFile5(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
6302
+ await writeFile6(filename, JSON.stringify({ ...config, publicUrl, connection }, null, 2) + "\n", {
5852
6303
  mode: 384
5853
6304
  });
5854
6305
  console.log(`Configuration saved to ${filename}. Start with agentrific start.`);
@@ -5861,28 +6312,48 @@ async function startCommand({ values, dataDir, filename, config }) {
5861
6312
  process.stdin.isTTY && process.stdout.isTTY && !values["no-interactive"]
5862
6313
  );
5863
6314
  const explicitUrl = values["public-url"] ?? process.env.AGENTRIFIC_PUBLIC_URL;
6315
+ const method = values.connection ? z21.enum(["opentunnel", "tailscale", "cloudflared", "existing", "local"]).parse(values.connection) : explicitUrl ? "existing" : config.connection;
6316
+ const origin = `http://${process.env.AGENTRIFIC_HOST ?? "127.0.0.1"}:${process.env.AGENTRIFIC_PORT ?? "4318"}`;
5864
6317
  if (interactive) {
5865
6318
  process.env.AGENTRIFIC_INTERACTIVE = "1";
5866
6319
  printHeading("server");
5867
6320
  }
5868
- const connection = interactive && !explicitUrl ? await selectPublicConnection(
5869
- `http://${process.env.AGENTRIFIC_HOST ?? "127.0.0.1"}:${process.env.AGENTRIFIC_PORT ?? "4318"}`,
5870
- config.publicUrl
5871
- ) : {
5872
- url: explicitUrl ? pairingOrigin(explicitUrl) : config.publicUrl,
6321
+ let connection = interactive && !explicitUrl && !method ? await selectPublicConnection(origin, config.publicUrl, dataDir) : {
6322
+ url: method === "local" ? void 0 : explicitUrl ? pairingOrigin(explicitUrl) : config.publicUrl,
5873
6323
  remember: false,
5874
6324
  tunnel: void 0
5875
6325
  };
6326
+ const provider = method || (!interactive && !connection.url ? "opentunnel" : void 0);
6327
+ if (provider && !["existing", "local"].includes(provider)) {
6328
+ const option = provider === "opentunnel" ? void 0 : (await detectTunnels()).find((candidate) => candidate.provider === provider);
6329
+ if (provider !== "opentunnel" && !option?.available)
6330
+ throw new Error(option?.reason || "The selected tunnel is unavailable.");
6331
+ const tunnel = provider === "opentunnel" ? new OpenTunnel(path14.join(dataDir, "phone-connection")) : new ManagedTunnel(option);
6332
+ try {
6333
+ connection = { url: await tunnel.start(origin), remember: false, tunnel };
6334
+ } catch (error) {
6335
+ await tunnel.close();
6336
+ throw error;
6337
+ }
6338
+ }
6339
+ if (method === "existing" && !connection.url)
6340
+ throw new Error("Pass --public-url with the HTTPS address from your existing tunnel.");
5876
6341
  const publicUrl = connection.url;
5877
6342
  try {
5878
6343
  if (connection.remember && publicUrl) {
5879
- await mkdir6(dataDir, { recursive: true, mode: 448 });
5880
- await writeFile5(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
6344
+ await mkdir7(dataDir, { recursive: true, mode: 448 });
6345
+ await writeFile6(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
5881
6346
  mode: 384
5882
6347
  });
5883
6348
  }
5884
6349
  if (publicUrl) process.env.AGENTRIFIC_PUBLIC_URL = pairingOrigin(publicUrl);
6350
+ else delete process.env.AGENTRIFIC_PUBLIC_URL;
5885
6351
  await init_index().then(() => index_exports);
6352
+ if (connection.tunnel) {
6353
+ const close = () => void connection.tunnel?.close();
6354
+ process.once("SIGINT", close);
6355
+ process.once("SIGTERM", close);
6356
+ }
5886
6357
  if (interactive) startTerminal(dataDir, publicUrl, connection.tunnel);
5887
6358
  } catch (error) {
5888
6359
  await connection.tunnel?.close();
@@ -5899,7 +6370,7 @@ async function serviceCommand({ positionals, dataDir }) {
5899
6370
  return;
5900
6371
  }
5901
6372
  async function logsCommand({ dataDir }) {
5902
- const child = process.platform === "linux" ? spawn2("journalctl", ["--user", "-u", "agentrific.service", "-f"], { stdio: "inherit" }) : spawn2("tail", ["-F", path12.join(dataDir, "logs", "server.log")], { stdio: "inherit" });
6373
+ const child = process.platform === "linux" ? spawn2("journalctl", ["--user", "-u", "agentrific.service", "-f"], { stdio: "inherit" }) : spawn2("tail", ["-F", path14.join(dataDir, "logs", "server.log")], { stdio: "inherit" });
5903
6374
  child.on("error", (error) => {
5904
6375
  console.error(error.message);
5905
6376
  process.exitCode = 1;
@@ -5933,11 +6404,11 @@ async function pairCommand({ values, dataDir, config }) {
5933
6404
  return;
5934
6405
  }
5935
6406
  async function maintenanceCommand({ values, positionals, dataDir }) {
5936
- const action = z19.enum(["enter", "resume", "drain"]).parse(positionals[1]);
6407
+ const action = z21.enum(["enter", "resume", "drain"]).parse(positionals[1]);
5937
6408
  const result = await controlRequest(dataDir, {
5938
6409
  command: "maintenance",
5939
6410
  action,
5940
- timeoutMs: z19.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
6411
+ timeoutMs: z21.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
5941
6412
  });
5942
6413
  console.log(JSON.stringify(result, null, 2));
5943
6414
  if (action === "drain" && "drained" in result && !result.drained) process.exitCode = 1;
@@ -5946,7 +6417,7 @@ async function maintenanceCommand({ values, positionals, dataDir }) {
5946
6417
  async function devicesCommand({ dataDir, command }) {
5947
6418
  console.log(
5948
6419
  JSON.stringify(
5949
- await controlRequest(dataDir, { command: z19.enum(["devices", "status"]).parse(command) }),
6420
+ await controlRequest(dataDir, { command: z21.enum(["devices", "status"]).parse(command) }),
5950
6421
  null,
5951
6422
  2
5952
6423
  )
@@ -5954,7 +6425,7 @@ async function devicesCommand({ dataDir, command }) {
5954
6425
  return;
5955
6426
  }
5956
6427
  async function revokeCommand({ positionals, dataDir }) {
5957
- const id = z19.string().uuid().parse(positionals[1]);
6428
+ const id = z21.string().uuid().parse(positionals[1]);
5958
6429
  console.log(JSON.stringify(await controlRequest(dataDir, { command: "revoke", id })));
5959
6430
  return;
5960
6431
  }