@agentrific/server 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.1",
33
+ version: "0.6.0",
34
34
  description: "Self-hosted control plane for long-running coding agents",
35
35
  keywords: [
36
36
  "agentrific",
@@ -80,6 +80,7 @@ var init_package = __esm({
80
80
  },
81
81
  devDependencies: {
82
82
  "@agentrific/contracts": "workspace:*",
83
+ "@agentrific/opentunnel-runtime": "workspace:*",
83
84
  "@jest/globals": "30.5.2",
84
85
  "@types/node": "^22.19.1",
85
86
  "@types/qrcode-terminal": "^0.12.2",
@@ -203,6 +204,21 @@ var init_updates = __esm({
203
204
  }
204
205
  });
205
206
 
207
+ // ../../packages/contracts/src/control-socket.ts
208
+ import { createHash } from "node:crypto";
209
+ import { join, resolve } from "node:path";
210
+ function controlSocketPath(directory, uid = process.getuid?.()) {
211
+ const direct = join(resolve(directory), "control.sock");
212
+ if (Buffer.byteLength(direct) < 100) return direct;
213
+ if (uid === void 0) throw new Error("Local control sockets require macOS or Linux.");
214
+ const digest2 = createHash("sha256").update(resolve(directory)).digest("hex").slice(0, 32);
215
+ return join("/tmp", `agentrific-${uid}`, `${digest2}.sock`);
216
+ }
217
+ var init_control_socket = __esm({
218
+ "../../packages/contracts/src/control-socket.ts"() {
219
+ }
220
+ });
221
+
206
222
  // ../../packages/contracts/src/index.ts
207
223
  import { z as z2 } from "zod";
208
224
  function cronRange(base, min, max, stepped) {
@@ -590,7 +606,7 @@ var init_src = __esm({
590
606
 
591
607
  // src/control.ts
592
608
  import { createServer, createConnection } from "node:net";
593
- import { chmod, mkdir as mkdir2, readFile, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
609
+ import { chmod, mkdir as mkdir2, lstat, readFile, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
594
610
  import path2 from "node:path";
595
611
  import { Effect as Effect2 } from "effect";
596
612
  import { z as z3 } from "zod";
@@ -635,8 +651,14 @@ async function acquireServerLock(directory) {
635
651
  await unlink2(filename);
636
652
  };
637
653
  }
638
- async function startControl(directory, security2, status, maintenance2) {
639
- const filename = path2.join(directory, "control.sock");
654
+ async function startControl(directory, security2, status, maintenance2, shutdown, phone2) {
655
+ const filename = controlSocketPath(directory);
656
+ const socketDirectory = path2.dirname(filename);
657
+ await mkdir2(socketDirectory, { recursive: true, mode: 448 });
658
+ const metadata = await lstat(socketDirectory);
659
+ if (!metadata.isDirectory() || metadata.uid !== process.getuid?.())
660
+ throw new Error("Local control directory must be private and owned by this user.");
661
+ await chmod(socketDirectory, 448);
640
662
  await unlink2(filename).catch((error) => {
641
663
  if (!z3.object({ code: z3.literal("ENOENT") }).safeParse(error).success) throw error;
642
664
  });
@@ -656,6 +678,36 @@ async function startControl(directory, security2, status, maintenance2) {
656
678
  const request = ControlRequest.parse(JSON.parse(buffer.split("\n")[0]));
657
679
  let result;
658
680
  switch (request.command) {
681
+ case "phone-options":
682
+ if (!phone2) throw new Error("Phone connections are managed by the desktop app.");
683
+ socket.setTimeout(18e4);
684
+ result = await phone2.options();
685
+ break;
686
+ case "phone-start": {
687
+ if (!phone2) throw new Error("Phone connections are managed by the desktop app.");
688
+ maintenance2?.assertReady();
689
+ socket.setTimeout(18e4);
690
+ const abort = new AbortController();
691
+ const disconnected = () => abort.abort();
692
+ socket.once("close", disconnected);
693
+ try {
694
+ result = await phone2.start(request.provider, abort.signal);
695
+ } finally {
696
+ socket.removeListener("close", disconnected);
697
+ }
698
+ break;
699
+ }
700
+ case "phone-stop":
701
+ if (!phone2) throw new Error("Phone connections are managed by the desktop app.");
702
+ result = await phone2.close();
703
+ break;
704
+ case "shutdown":
705
+ if (!shutdown || !maintenance2?.status().drained)
706
+ throw new Error("Drain the server before shutdown");
707
+ socket.end(JSON.stringify({ result: { shutdown: true } }) + "\n", () => {
708
+ setImmediate(() => void shutdown());
709
+ });
710
+ return;
659
711
  case "pair":
660
712
  result = security2.createPairing();
661
713
  break;
@@ -682,20 +734,20 @@ async function startControl(directory, security2, status, maintenance2) {
682
734
  });
683
735
  socket.on("error", () => socket.destroy());
684
736
  });
685
- await new Promise((resolve, reject) => {
737
+ await new Promise((resolve2, reject) => {
686
738
  server.once("error", reject);
687
- server.listen(filename, resolve);
739
+ server.listen(filename, resolve2);
688
740
  });
689
741
  await chmod(filename, 384);
690
742
  return async () => {
691
743
  await new Promise(
692
- (resolve, reject) => server.close((error) => error ? reject(error) : resolve())
744
+ (resolve2, reject) => server.close((error) => error ? reject(error) : resolve2())
693
745
  );
694
746
  };
695
747
  }
696
748
  function controlRequest(directory, command) {
697
- return new Promise((resolve, reject) => {
698
- const socket = createConnection(path2.join(directory, "control.sock"));
749
+ return new Promise((resolve2, reject) => {
750
+ const socket = createConnection(controlSocketPath(directory));
699
751
  socket.setTimeout(command.command === "maintenance" ? 65e3 : 5e3, () => {
700
752
  socket.destroy();
701
753
  reject(new Error("Server did not respond."));
@@ -711,18 +763,20 @@ function controlRequest(directory, command) {
711
763
  const result = z3.object({ result: ControlResultSchema.optional(), error: z3.string().optional() }).parse(JSON.parse(response));
712
764
  if (result.error) throw new Error(result.error);
713
765
  if (!result.result) throw new Error("Server returned an empty response.");
714
- resolve(result.result);
766
+ resolve2(result.result);
715
767
  } catch (error) {
716
768
  reject(error);
717
769
  }
718
770
  });
719
771
  });
720
772
  }
721
- var MaintenanceSchema, ControlStatusSchema, ControlResultSchema, ControlRequest;
773
+ var ProviderSchema, MaintenanceSchema, ControlStatusSchema, ControlResultSchema, ControlRequest;
722
774
  var init_control = __esm({
723
775
  "src/control.ts"() {
724
776
  "use strict";
777
+ init_control_socket();
725
778
  init_src();
779
+ ProviderSchema = z3.enum(["opentunnel", "tailscale", "cloudflared"]);
726
780
  MaintenanceSchema = z3.object({
727
781
  active: z3.boolean(),
728
782
  drained: z3.boolean(),
@@ -731,6 +785,8 @@ var init_control = __esm({
731
785
  ControlStatusSchema = z3.object({
732
786
  serverId: z3.string().uuid(),
733
787
  publicUrl: z3.string().optional(),
788
+ phoneProvider: ProviderSchema.nullable().optional(),
789
+ localUrl: z3.string().optional(),
734
790
  serverVersion: z3.string().optional(),
735
791
  connectedDevices: z3.number().optional(),
736
792
  connectedSockets: z3.number().optional(),
@@ -739,6 +795,8 @@ var init_control = __esm({
739
795
  maintenance: MaintenanceSchema.optional()
740
796
  });
741
797
  ControlResultSchema = z3.union([
798
+ z3.object({ provider: ProviderSchema.nullable(), publicUrl: z3.string().url().nullable() }),
799
+ z3.array(z3.object({ provider: ProviderSchema, available: z3.boolean(), reason: z3.string() })),
742
800
  z3.object({
743
801
  pairingId: z3.string(),
744
802
  pin: z3.string(),
@@ -748,12 +806,17 @@ var init_control = __esm({
748
806
  DeviceSchema.array(),
749
807
  ControlStatusSchema,
750
808
  z3.object({ revoked: z3.boolean() }),
809
+ z3.object({ shutdown: z3.literal(true) }),
751
810
  MaintenanceSchema
752
811
  ]);
753
812
  ControlRequest = z3.discriminatedUnion("command", [
813
+ z3.object({ command: z3.literal("phone-options") }),
814
+ z3.object({ command: z3.literal("phone-start"), provider: ProviderSchema }),
815
+ z3.object({ command: z3.literal("phone-stop") }),
754
816
  z3.object({ command: z3.literal("pair") }),
755
817
  z3.object({ command: z3.literal("devices") }),
756
818
  z3.object({ command: z3.literal("status") }),
819
+ z3.object({ command: z3.literal("shutdown") }),
757
820
  z3.object({ command: z3.literal("revoke"), id: z3.string().uuid() }),
758
821
  z3.object({
759
822
  command: z3.literal("maintenance"),
@@ -823,6 +886,7 @@ function printHelp() {
823
886
  for (const [option, description] of [
824
887
  ["--data-dir <path>", "Use a specific server data directory"],
825
888
  ["--public-url <https-url>", "Use your tunnel URL without prompting"],
889
+ ["--connection <method>", "opentunnel (default), tailscale, cloudflared, existing, or local"],
826
890
  ["--no-interactive", "Disable prompts and keyboard shortcuts"],
827
891
  ["--show-payload", "Print manual JSON with the pairing QR code"],
828
892
  ["--timeout-ms <0-60000>", "Set the maintenance drain timeout"],
@@ -830,7 +894,9 @@ function printHelp() {
830
894
  ["--help", "Show this help"]
831
895
  ])
832
896
  console.log(` ${styleText("cyan", option.padEnd(34))}${description}`);
833
- console.log("\n Connect using Tailscale Serve, cloudflared, or another HTTPS tunnel.\n");
897
+ console.log(
898
+ "\n OpenTunnel is built in. Tailscale, cloudflared and existing HTTPS tunnels are also supported.\n"
899
+ );
834
900
  }
835
901
  var init_cli_output = __esm({
836
902
  "src/cli-output.ts"() {
@@ -855,7 +921,7 @@ function printManualPairing(invitation) {
855
921
  }
856
922
  function waitForManualPairing(invitation) {
857
923
  console.log(styleText2("dim", " Press j to show manual entry \xB7 Enter or q to finish.\n"));
858
- return new Promise((resolve) => {
924
+ return new Promise((resolve2) => {
859
925
  const input = process.stdin;
860
926
  const previousRaw = input.isRaw;
861
927
  const timeout = setTimeout(
@@ -875,7 +941,7 @@ function waitForManualPairing(invitation) {
875
941
  process.removeListener("SIGTERM", terminated);
876
942
  input.setRawMode(previousRaw);
877
943
  input.pause();
878
- resolve();
944
+ resolve2();
879
945
  }
880
946
  function interrupted() {
881
947
  process.exitCode = 130;
@@ -960,8 +1026,379 @@ var init_pairing = __esm({
960
1026
  }
961
1027
  });
962
1028
 
963
- // ../../packages/contracts/src/push.ts
1029
+ // src/tunnels.ts
1030
+ import { spawn } from "node:child_process";
1031
+ import { access, stat } from "node:fs/promises";
1032
+ import { constants } from "node:fs";
1033
+ import { homedir as homedir2 } from "node:os";
1034
+ import path3 from "node:path";
1035
+ import { z as z5 } from "zod";
1036
+ function signalChild(child, signal) {
1037
+ if (!child.pid) return;
1038
+ if (process.platform === "win32") {
1039
+ child.kill(signal);
1040
+ return;
1041
+ }
1042
+ try {
1043
+ process.kill(-child.pid, signal);
1044
+ } catch (error) {
1045
+ if (!z5.object({ code: z5.literal("ESRCH") }).safeParse(error).success) child.kill(signal);
1046
+ }
1047
+ }
1048
+ function execute2(file, args, timeoutMs, env = process.env) {
1049
+ return new Promise((resolve2, reject) => {
1050
+ const child = spawn(file, args, {
1051
+ env,
1052
+ detached: process.platform !== "win32",
1053
+ stdio: ["ignore", "pipe", "pipe"]
1054
+ });
1055
+ let stdout = "";
1056
+ let stderr = "";
1057
+ const timeout = setTimeout(() => {
1058
+ signalChild(child, "SIGKILL");
1059
+ reject(new Error("Command probe timed out."));
1060
+ }, timeoutMs);
1061
+ child.stdout?.on("data", (data) => {
1062
+ stdout += data.toString();
1063
+ checkOutput();
1064
+ });
1065
+ child.stderr?.on("data", (data) => {
1066
+ stderr += data.toString();
1067
+ checkOutput();
1068
+ });
1069
+ child.once("error", (error) => {
1070
+ clearTimeout(timeout);
1071
+ reject(error);
1072
+ });
1073
+ child.once("exit", () => signalChild(child, "SIGKILL"));
1074
+ child.once("close", (code) => {
1075
+ clearTimeout(timeout);
1076
+ if (code !== 0) reject(new Error("Command probe failed."));
1077
+ else resolve2({ stdout, stderr });
1078
+ });
1079
+ function checkOutput() {
1080
+ if (stdout.length + stderr.length <= 1048576) return;
1081
+ signalChild(child, "SIGKILL");
1082
+ reject(new Error("Command probe output exceeded its limit."));
1083
+ }
1084
+ });
1085
+ }
1086
+ async function findExecutable(name) {
1087
+ const candidates = (process.env.PATH ?? "").split(path3.delimiter).map((directory) => path3.resolve(directory, name));
1088
+ if (process.platform === "darwin") {
1089
+ candidates.push(`/opt/homebrew/bin/${name}`, `/usr/local/bin/${name}`);
1090
+ if (name === "tailscale")
1091
+ candidates.push("/Applications/Tailscale.app/Contents/MacOS/Tailscale");
1092
+ }
1093
+ for (const candidate of candidates) {
1094
+ try {
1095
+ await access(candidate, constants.X_OK);
1096
+ if ((await stat(candidate)).isFile()) return candidate;
1097
+ } catch {
1098
+ continue;
1099
+ }
1100
+ }
1101
+ return void 0;
1102
+ }
1103
+ async function probeTailscale() {
1104
+ const option = { provider: "tailscale", available: false, reason: "Not installed" };
1105
+ const executable = await findExecutable("tailscale");
1106
+ if (!executable) return option;
1107
+ option.executable = executable;
1108
+ const env = { ...process.env, TAILSCALE_BE_CLI: "1" };
1109
+ try {
1110
+ const response = await execute2(executable, ["status", "--json"], 3e3, env);
1111
+ const status = z5.object({
1112
+ BackendState: z5.string(),
1113
+ CertDomains: z5.array(z5.string()).optional(),
1114
+ Self: z5.object({ DNSName: z5.string() }).nullable().optional()
1115
+ }).parse(JSON.parse(response.stdout));
1116
+ if (status.BackendState !== "Running")
1117
+ return { ...option, reason: "Installed \xB7 connect Tailscale first" };
1118
+ const dns = status.Self?.DNSName.replace(/\.$/, "");
1119
+ if (!dns || !status.CertDomains?.includes(dns))
1120
+ return { ...option, reason: "Enable HTTPS certificates in your tailnet first" };
1121
+ const help = await execute2(executable, ["serve", "--help"], 2e3, env);
1122
+ if (!`${help.stdout}${help.stderr}`.includes("--https"))
1123
+ return { ...option, reason: "Update Tailscale to use foreground HTTPS Serve" };
1124
+ const responseConfig = await execute2(executable, ["serve", "status", "--json"], 3e3, env);
1125
+ const config = ServeConfig.parse(JSON.parse(responseConfig.stdout) ?? {});
1126
+ const occupied = new Set(Object.keys(config.TCP ?? {}));
1127
+ for (const foreground of Object.values(config.Foreground ?? {}))
1128
+ for (const port3 of Object.keys(foreground.TCP ?? {})) occupied.add(port3);
1129
+ const port2 = [443, ...Array.from({ length: 10 }, (_, index) => 8443 + index)].find(
1130
+ (candidate) => !occupied.has(String(candidate))
1131
+ );
1132
+ if (!port2) return { ...option, reason: "HTTPS Serve ports are in use \xB7 paste an existing URL" };
1133
+ return {
1134
+ ...option,
1135
+ available: true,
1136
+ reason: "Private to your tailnet \xB7 Tailscale required on your phone",
1137
+ publicUrl: new URL(`https://${dns}${port2 === 443 ? "" : `:${port2}`}`).origin,
1138
+ httpsPort: port2
1139
+ };
1140
+ } catch {
1141
+ return { ...option, reason: "Installed \xB7 could not verify daemon or Serve access" };
1142
+ }
1143
+ }
1144
+ async function probeCloudflared(home) {
1145
+ const option = {
1146
+ provider: "cloudflared",
1147
+ available: false,
1148
+ reason: "Not installed"
1149
+ };
1150
+ const executable = await findExecutable("cloudflared");
1151
+ if (!executable) return option;
1152
+ option.executable = executable;
1153
+ try {
1154
+ await execute2(executable, ["--version"], 2e3);
1155
+ const help = await execute2(executable, ["tunnel", "--help"], 2e3);
1156
+ if (!`${help.stdout}${help.stderr}`.includes("--url"))
1157
+ return { ...option, reason: "Installed \xB7 this version does not support Quick Tunnels" };
1158
+ } catch {
1159
+ return { ...option, reason: "Installed \xB7 command probe failed or timed out" };
1160
+ }
1161
+ const directories = [
1162
+ path3.join(home, ".cloudflared"),
1163
+ path3.join(home, ".cloudflare-warp"),
1164
+ path3.join(home, "cloudflare-warp"),
1165
+ "/etc/cloudflared",
1166
+ "/usr/local/etc/cloudflared"
1167
+ ];
1168
+ const configurations = await Promise.all(
1169
+ directories.flatMap(
1170
+ (directory) => ["config.yml", "config.yaml"].map(
1171
+ async (name) => access(path3.join(directory, name), constants.F_OK).then(
1172
+ () => true,
1173
+ () => false
1174
+ )
1175
+ )
1176
+ )
1177
+ );
1178
+ if (configurations.some(Boolean))
1179
+ return { ...option, reason: "Existing tunnel configuration \xB7 paste its HTTPS URL" };
1180
+ return { ...option, available: true, reason: "Temporary public HTTPS URL \xB7 changes each start" };
1181
+ }
1182
+ async function detectTunnels(home = homedir2()) {
1183
+ return Promise.all([probeTailscale(), probeCloudflared(home)]);
1184
+ }
1185
+ var TcpConfig, ServeConfig, ManagedTunnel;
1186
+ var init_tunnels = __esm({
1187
+ "src/tunnels.ts"() {
1188
+ "use strict";
1189
+ init_cli_output();
1190
+ TcpConfig = z5.object({ TCP: z5.record(z5.string(), z5.unknown()).optional() });
1191
+ ServeConfig = TcpConfig.extend({
1192
+ Foreground: z5.record(z5.string(), TcpConfig).optional()
1193
+ });
1194
+ ManagedTunnel = class {
1195
+ constructor(option, disconnected = () => {
1196
+ }) {
1197
+ this.option = option;
1198
+ this.disconnected = disconnected;
1199
+ }
1200
+ option;
1201
+ disconnected;
1202
+ child;
1203
+ stopping = false;
1204
+ connected = false;
1205
+ closing;
1206
+ running() {
1207
+ return this.connected && !this.stopping;
1208
+ }
1209
+ stopOnSignal = () => {
1210
+ void this.close();
1211
+ };
1212
+ stopOnExit = () => {
1213
+ if (this.child) signalChild(this.child, "SIGKILL");
1214
+ };
1215
+ async start(origin, signal) {
1216
+ signal?.throwIfAborted();
1217
+ if (!this.option.available) throw new Error(this.option.reason);
1218
+ const tailscale = this.option.provider === "tailscale";
1219
+ const child = spawn(
1220
+ this.option.executable ?? this.option.provider,
1221
+ tailscale ? ["serve", "--bg=false", `--https=${this.option.httpsPort}`, origin] : ["tunnel", "--no-autoupdate", "--url", origin],
1222
+ {
1223
+ stdio: ["ignore", "pipe", "pipe"],
1224
+ detached: process.platform !== "win32",
1225
+ env: tailscale ? { ...process.env, TAILSCALE_BE_CLI: "1" } : process.env
1226
+ }
1227
+ );
1228
+ this.child = child;
1229
+ process.once("SIGINT", this.stopOnSignal);
1230
+ process.once("SIGTERM", this.stopOnSignal);
1231
+ process.once("exit", this.stopOnExit);
1232
+ return new Promise((resolve2, reject) => {
1233
+ let output = "";
1234
+ let address;
1235
+ let settled = false;
1236
+ const abort = () => fail("Tunnel startup canceled.");
1237
+ const timeout = setTimeout(
1238
+ () => fail("Tunnel startup timed out. Check your connection or paste an existing HTTPS URL."),
1239
+ 45e3
1240
+ );
1241
+ const fail = (message) => {
1242
+ if (settled) return;
1243
+ settled = true;
1244
+ clearTimeout(timeout);
1245
+ signal?.removeEventListener("abort", abort);
1246
+ void this.close();
1247
+ reject(new Error(message));
1248
+ };
1249
+ const read = (data) => {
1250
+ output = `${output}${data.toString()}`.slice(-65536);
1251
+ address = tailscale ? this.option.publicUrl : output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com\b/)?.[0];
1252
+ const ready = tailscale ? Boolean(
1253
+ address && output.includes(address) && output.includes("Available within your tailnet")
1254
+ ) : output.includes("Registered tunnel connection");
1255
+ if (settled || !address || !ready) return;
1256
+ settled = true;
1257
+ this.connected = true;
1258
+ clearTimeout(timeout);
1259
+ signal?.removeEventListener("abort", abort);
1260
+ resolve2(address);
1261
+ };
1262
+ child.stdout?.on("data", read);
1263
+ child.stderr?.on("data", read);
1264
+ signal?.addEventListener("abort", abort, { once: true });
1265
+ child.once(
1266
+ "error",
1267
+ () => fail(`${this.option.provider} could not start. Check the installation and permissions.`)
1268
+ );
1269
+ child.once("exit", () => signalChild(child, "SIGTERM"));
1270
+ child.once("close", (code) => {
1271
+ const unexpected = this.connected && !this.stopping;
1272
+ this.connected = false;
1273
+ process.removeListener("SIGINT", this.stopOnSignal);
1274
+ process.removeListener("SIGTERM", this.stopOnSignal);
1275
+ process.removeListener("exit", this.stopOnExit);
1276
+ fail(
1277
+ `${this.option.provider} exited (${code ?? "signal"}). Check tunnel access or paste an existing HTTPS URL.`
1278
+ );
1279
+ if (unexpected) {
1280
+ this.disconnected();
1281
+ printError(
1282
+ "The managed tunnel stopped. Restart the server to reconnect your phone; local work can continue."
1283
+ );
1284
+ }
1285
+ });
1286
+ });
1287
+ }
1288
+ close() {
1289
+ this.stopping = true;
1290
+ const child = this.child;
1291
+ if (!child?.pid || child.exitCode !== null || child.signalCode !== null)
1292
+ return Promise.resolve();
1293
+ this.closing ??= new Promise((resolve2) => {
1294
+ const timeout = setTimeout(() => signalChild(child, "SIGKILL"), 2e3);
1295
+ child.once("close", () => {
1296
+ clearTimeout(timeout);
1297
+ resolve2();
1298
+ });
1299
+ signalChild(child, "SIGTERM");
1300
+ });
1301
+ return this.closing;
1302
+ }
1303
+ };
1304
+ }
1305
+ });
1306
+
1307
+ // src/opentunnel.ts
1308
+ import { randomBytes } from "node:crypto";
1309
+ import { chmod as chmod2, mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
1310
+ import path4 from "node:path";
964
1311
  import { z as z6 } from "zod";
1312
+ import {
1313
+ create,
1314
+ OpenTunnelStorage
1315
+ } from "./opentunnel-sdk.js";
1316
+ var OpenTunnel;
1317
+ var init_opentunnel = __esm({
1318
+ "src/opentunnel.ts"() {
1319
+ "use strict";
1320
+ OpenTunnel = class {
1321
+ constructor(directory, disconnected = () => {
1322
+ }, makeClient = create) {
1323
+ this.directory = directory;
1324
+ this.disconnected = disconnected;
1325
+ this.makeClient = makeClient;
1326
+ }
1327
+ directory;
1328
+ disconnected;
1329
+ makeClient;
1330
+ client;
1331
+ connection;
1332
+ stopping = false;
1333
+ running() {
1334
+ return this.connection?.status().state === "connected" && !this.stopping;
1335
+ }
1336
+ async start(origin, signal) {
1337
+ const endpoint = new URL(origin);
1338
+ if (!["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname))
1339
+ throw new Error("OpenTunnel must forward to the local loopback server.");
1340
+ await mkdir3(this.directory, { recursive: true, mode: 448 });
1341
+ await chmod2(this.directory, 448);
1342
+ const routeFile = path4.join(this.directory, "route.json");
1343
+ let route;
1344
+ try {
1345
+ route = z6.string().regex(/^[a-f0-9]{16}$/).parse(JSON.parse(await readFile2(routeFile, "utf8")));
1346
+ } catch (error) {
1347
+ if (!z6.object({ code: z6.literal("ENOENT") }).safeParse(error).success) throw error;
1348
+ route = randomBytes(8).toString("hex");
1349
+ await writeFile3(routeFile, JSON.stringify(route), { flag: "wx", mode: 384 });
1350
+ }
1351
+ this.stopping = false;
1352
+ this.client = this.makeClient({
1353
+ store: OpenTunnelStorage.xdg({ env: { XDG_DATA_HOME: this.directory } })
1354
+ });
1355
+ const abort = AbortSignal.any([
1356
+ signal ?? new AbortController().signal,
1357
+ AbortSignal.timeout(12e4)
1358
+ ]);
1359
+ let canceled = () => {
1360
+ };
1361
+ try {
1362
+ const connecting = this.client.tunnel.connect({
1363
+ profile: "agentrific",
1364
+ routes: {
1365
+ [route]: `${endpoint.hostname === "[::1]" ? "[::1]" : "127.0.0.1"}:${endpoint.port || "80"}`
1366
+ },
1367
+ signal
1368
+ });
1369
+ const cancellation = new Promise((_resolve, reject) => {
1370
+ canceled = () => reject(new Error("OpenTunnel connection canceled or timed out."));
1371
+ abort.addEventListener("abort", canceled, { once: true });
1372
+ if (abort.aborted) canceled();
1373
+ });
1374
+ const connection = await Promise.race([connecting, cancellation]);
1375
+ this.connection = connection;
1376
+ void connection.closed.then(() => {
1377
+ if (!this.stopping) this.disconnected();
1378
+ }).catch(() => {
1379
+ if (!this.stopping) this.disconnected();
1380
+ });
1381
+ return `https://${route}.${connection.tunnel.hostname}`;
1382
+ } catch (error) {
1383
+ await this.close();
1384
+ throw error;
1385
+ } finally {
1386
+ abort.removeEventListener("abort", canceled);
1387
+ }
1388
+ }
1389
+ async close() {
1390
+ this.stopping = true;
1391
+ await this.connection?.close();
1392
+ await this.client?.dispose();
1393
+ this.connection = void 0;
1394
+ this.client = void 0;
1395
+ }
1396
+ };
1397
+ }
1398
+ });
1399
+
1400
+ // ../../packages/contracts/src/push.ts
1401
+ import { z as z7 } from "zod";
965
1402
  function pushSigningText(installationId, operation, request) {
966
1403
  return [
967
1404
  "agentrific-push-v1",
@@ -979,7 +1416,7 @@ var init_push = __esm({
979
1416
  "use strict";
980
1417
  PUSH_API_URL = "https://api.agentrific.com";
981
1418
  PUSH_SANDBOX_API_URL = "https://sandbox-api.agentrific.com";
982
- PushKindSchema = z6.enum([
1419
+ PushKindSchema = z7.enum([
983
1420
  "started",
984
1421
  "completed",
985
1422
  "needs_input",
@@ -988,47 +1425,47 @@ var init_push = __esm({
988
1425
  "pr_created",
989
1426
  "failed"
990
1427
  ]);
991
- PushPreferencesSchema = z6.array(PushKindSchema).max(7);
992
- PushPublicKeySchema = z6.string().regex(/^[a-f0-9]{64}$/);
993
- PushInstallationSchema = z6.object({ installationId: z6.string().uuid(), token: z6.string().regex(/^[a-f0-9]{64}$/) }).strict();
994
- PushRegistrationSchema = z6.object({
995
- expoToken: z6.string().regex(/^(Expo|Exponent)PushToken\[[A-Za-z0-9_-]+\]$/),
1428
+ PushPreferencesSchema = z7.array(PushKindSchema).max(7);
1429
+ PushPublicKeySchema = z7.string().regex(/^[a-f0-9]{64}$/);
1430
+ PushInstallationSchema = z7.object({ installationId: z7.string().uuid(), token: z7.string().regex(/^[a-f0-9]{64}$/) }).strict();
1431
+ PushRegistrationSchema = z7.object({
1432
+ expoToken: z7.string().regex(/^(Expo|Exponent)PushToken\[[A-Za-z0-9_-]+\]$/),
996
1433
  preferences: PushPreferencesSchema
997
1434
  }).strict();
998
- PushEnrollmentSchema = z6.object({ serverId: z6.string().uuid(), publicKey: PushPublicKeySchema }).strict();
1435
+ PushEnrollmentSchema = z7.object({ serverId: z7.string().uuid(), publicKey: PushPublicKeySchema }).strict();
999
1436
  PushGrantSchema = PushEnrollmentSchema.extend({
1000
- installationId: z6.string().uuid(),
1001
- bindingId: z6.string().uuid(),
1002
- grant: z6.string().regex(/^[a-f0-9]{64}$/),
1003
- expiresAt: z6.number()
1437
+ installationId: z7.string().uuid(),
1438
+ bindingId: z7.string().uuid(),
1439
+ grant: z7.string().regex(/^[a-f0-9]{64}$/),
1440
+ expiresAt: z7.number()
1004
1441
  }).strict();
1005
- PushPullRequestUrlSchema = z6.string().max(512).regex(/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9][0-9]*$/);
1006
- PushEventSchema = z6.object({
1007
- eventId: z6.string().uuid(),
1008
- sessionId: z6.string().uuid(),
1442
+ PushPullRequestUrlSchema = z7.string().max(512).regex(/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9][0-9]*$/);
1443
+ PushEventSchema = z7.object({
1444
+ eventId: z7.string().uuid(),
1445
+ sessionId: z7.string().uuid(),
1009
1446
  kind: PushKindSchema,
1010
- taskTitle: z6.string().trim().min(1).max(160).optional(),
1447
+ taskTitle: z7.string().trim().min(1).max(160).optional(),
1011
1448
  prUrl: PushPullRequestUrlSchema.optional()
1012
1449
  }).strict();
1013
- PushSignedRequestSchema = z6.object({
1014
- bindingId: z6.string().uuid(),
1015
- timestamp: z6.number().int(),
1016
- nonce: z6.string().uuid(),
1017
- payload: z6.string().max(2048),
1018
- signature: z6.string().regex(/^[a-f0-9]{128}$/)
1450
+ PushSignedRequestSchema = z7.object({
1451
+ bindingId: z7.string().uuid(),
1452
+ timestamp: z7.number().int(),
1453
+ nonce: z7.string().uuid(),
1454
+ payload: z7.string().max(2048),
1455
+ signature: z7.string().regex(/^[a-f0-9]{128}$/)
1019
1456
  }).strict();
1020
- PushEntitlementSchema = z6.object({ active: z6.boolean(), expiresAt: z6.number().nullable() }).strict();
1021
- PushServerStatusSchema = z6.object({
1457
+ PushEntitlementSchema = z7.object({ active: z7.boolean(), expiresAt: z7.number().nullable() }).strict();
1458
+ PushServerStatusSchema = z7.object({
1022
1459
  publicKey: PushPublicKeySchema,
1023
- bindings: z6.array(
1024
- z6.object({
1025
- bindingId: z6.string().uuid(),
1026
- installationId: z6.string().uuid(),
1027
- deviceId: z6.string().uuid()
1460
+ bindings: z7.array(
1461
+ z7.object({
1462
+ bindingId: z7.string().uuid(),
1463
+ installationId: z7.string().uuid(),
1464
+ deviceId: z7.string().uuid()
1028
1465
  })
1029
1466
  ),
1030
- pending: z6.number().int().nonnegative(),
1031
- error: z6.string().nullable()
1467
+ pending: z7.number().int().nonnegative(),
1468
+ error: z7.string().nullable()
1032
1469
  });
1033
1470
  }
1034
1471
  });
@@ -1042,7 +1479,7 @@ import {
1042
1479
  randomUUID
1043
1480
  } from "node:crypto";
1044
1481
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1045
- import path4 from "node:path";
1482
+ import path6 from "node:path";
1046
1483
  import { Effect as Effect3 } from "effect";
1047
1484
  function pushKind(session, previous) {
1048
1485
  if (session.gitDelivery?.status === "pr_opened" && previous?.gitDelivery?.status !== "pr_opened")
@@ -1102,7 +1539,7 @@ var init_push2 = __esm({
1102
1539
  this.transport = transport;
1103
1540
  this.apiUrl = apiUrl;
1104
1541
  mkdirSync(directory, { recursive: true, mode: 448 });
1105
- const filename = path4.join(directory, "push-signing-key.pem");
1542
+ const filename = path6.join(directory, "push-signing-key.pem");
1106
1543
  if (!existsSync(filename))
1107
1544
  writeFileSync(
1108
1545
  filename,
@@ -1272,12 +1709,12 @@ var init_push2 = __esm({
1272
1709
  // src/attachments.ts
1273
1710
  import { randomUUID as randomUUID2 } from "node:crypto";
1274
1711
  import { createReadStream, createWriteStream } from "node:fs";
1275
- import { mkdir as mkdir3, readFile as readFile2, rm, stat as stat2, readdir } from "node:fs/promises";
1276
- import path5 from "node:path";
1712
+ import { mkdir as mkdir4, readFile as readFile3, rm, stat as stat2, readdir } from "node:fs/promises";
1713
+ import path7 from "node:path";
1277
1714
  import { pipeline } from "node:stream/promises";
1278
1715
  import sharp from "sharp";
1279
1716
  import { Context, Data, Effect as Effect4, Layer } from "effect";
1280
- import { z as z7 } from "zod";
1717
+ import { z as z8 } from "zod";
1281
1718
  var AttachmentError, textExtensions, imageMimes, AttachmentFiles, Attachments;
1282
1719
  var init_attachments = __esm({
1283
1720
  "src/attachments.ts"() {
@@ -1332,7 +1769,7 @@ var init_attachments = __esm({
1332
1769
  root;
1333
1770
  store;
1334
1771
  directory(id) {
1335
- return path5.join(this.root, z7.string().uuid().parse(id));
1772
+ return path7.join(this.root, z8.string().uuid().parse(id));
1336
1773
  }
1337
1774
  io(operation) {
1338
1775
  return Effect4.tryPromise({
@@ -1346,11 +1783,11 @@ var init_attachments = __esm({
1346
1783
  return this.io(async () => {
1347
1784
  const id = randomUUID2();
1348
1785
  const directory = this.directory(id);
1349
- const source = path5.join(directory, "source");
1350
- const nameField = z7.object({ type: z7.literal("field"), value: z7.string() }).safeParse(part.fields.name);
1786
+ const source = path7.join(directory, "source");
1787
+ const nameField = z8.object({ type: z8.literal("field"), value: z8.string() }).safeParse(part.fields.name);
1351
1788
  const originalName = nameField.success ? nameField.data.value : part.filename;
1352
- const name = path5.basename(originalName.replaceAll("\\", "/")).replace(new RegExp("\\p{Cc}", "gu"), "").slice(0, 180) || "attachment";
1353
- await mkdir3(directory, { recursive: true });
1789
+ const name = path7.basename(originalName.replaceAll("\\", "/")).replace(new RegExp("\\p{Cc}", "gu"), "").slice(0, 180) || "attachment";
1790
+ await mkdir4(directory, { recursive: true });
1354
1791
  try {
1355
1792
  await pipeline(part.file, createWriteStream(source, { flags: "wx" }));
1356
1793
  if (part.file.truncated) throw new Error("Files must be 20 MB or smaller.");
@@ -1377,14 +1814,14 @@ var init_attachments = __esm({
1377
1814
  const metadata = await sharp(source, { limitInputPixels: 4e7 }).metadata().catch(() => null);
1378
1815
  const mimeType = metadata?.format && imageMimes.get(metadata.format);
1379
1816
  if (mimeType) {
1380
- await sharp(source, { limitInputPixels: 4e7 }).rotate().resize({ width: 2048, height: 2048, fit: "inside", withoutEnlargement: true }).png().toFile(path5.join(path5.dirname(source), "preview.png"));
1817
+ await sharp(source, { limitInputPixels: 4e7 }).rotate().resize({ width: 2048, height: 2048, fit: "inside", withoutEnlargement: true }).png().toFile(path7.join(path7.dirname(source), "preview.png"));
1381
1818
  return { kind: "image", mimeType };
1382
1819
  }
1383
- if (!textExtensions.has(path5.extname(name).toLowerCase()))
1820
+ if (!textExtensions.has(path7.extname(name).toLowerCase()))
1384
1821
  throw new Error(
1385
1822
  "Choose a PNG, JPEG, WebP, GIF, or text/code file. PDF and Office documents aren't supported yet."
1386
1823
  );
1387
- const bytes = await readFile2(source);
1824
+ const bytes = await readFile3(source);
1388
1825
  const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1389
1826
  if (new RegExp("\\p{Cc}", "u").test(text.replaceAll("\n", "").replaceAll("\r", "").replaceAll(" ", "")))
1390
1827
  throw new Error("This file isn't readable UTF-8 text.");
@@ -1401,7 +1838,7 @@ var init_attachments = __esm({
1401
1838
  throw new Error(
1402
1839
  "An attachment is unavailable for this project. Remove it and upload it again."
1403
1840
  );
1404
- await stat2(path5.join(this.directory(id), "source"));
1841
+ await stat2(path7.join(this.directory(id), "source"));
1405
1842
  attachments2.push(attachment);
1406
1843
  }
1407
1844
  return attachments2;
@@ -1409,7 +1846,7 @@ var init_attachments = __esm({
1409
1846
  }
1410
1847
  content(attachment, preview) {
1411
1848
  return createReadStream(
1412
- path5.join(
1849
+ path7.join(
1413
1850
  this.directory(attachment.id),
1414
1851
  preview && attachment.kind === "image" ? "preview.png" : "source"
1415
1852
  )
@@ -1420,13 +1857,13 @@ var init_attachments = __esm({
1420
1857
  const images = [];
1421
1858
  const sections = [];
1422
1859
  for (const attachment of attachments2) {
1423
- const source = path5.join(this.directory(attachment.id), "source");
1860
+ const source = path7.join(this.directory(attachment.id), "source");
1424
1861
  if (attachment.kind === "image") {
1425
- const bytes = await readFile2(path5.join(this.directory(attachment.id), "preview.png"));
1862
+ const bytes = await readFile3(path7.join(this.directory(attachment.id), "preview.png"));
1426
1863
  images.push({ type: "image", data: bytes.toString("base64"), mimeType: "image/png" });
1427
1864
  sections.push(`Image attachment: ${JSON.stringify(attachment.name)}`);
1428
1865
  } else {
1429
- const text = await readFile2(source, "utf8");
1866
+ const text = await readFile3(source, "utf8");
1430
1867
  sections.push(
1431
1868
  `File attachment: ${JSON.stringify(attachment.name)}
1432
1869
  Full file available to the read tool at: ${JSON.stringify(source)}
@@ -1452,9 +1889,9 @@ ${text.slice(0, 12e3)}${text.length > 12e3 ? "\n[Preview truncated; use the read
1452
1889
  if (await Effect4.runPromise(this.store.deleteAttachment(attachment.id)))
1453
1890
  await rm(this.directory(attachment.id), { recursive: true, force: true });
1454
1891
  }
1455
- await mkdir3(this.root, { recursive: true });
1892
+ await mkdir4(this.root, { recursive: true });
1456
1893
  for (const entry of await readdir(this.root, { withFileTypes: true })) {
1457
- if (!entry.isDirectory() || !z7.string().uuid().safeParse(entry.name).success) continue;
1894
+ if (!entry.isDirectory() || !z8.string().uuid().safeParse(entry.name).success) continue;
1458
1895
  const directory = this.directory(entry.name);
1459
1896
  if ((await stat2(directory)).mtimeMs < cutoff && !await Effect4.runPromise(this.store.getAttachment(entry.name)))
1460
1897
  await rm(directory, { recursive: true, force: true });
@@ -1542,24 +1979,24 @@ var init_event_server = __esm({
1542
1979
  });
1543
1980
 
1544
1981
  // src/security.ts
1545
- import { createHash, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
1982
+ import { createHash as createHash2, randomBytes as randomBytes2, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
1546
1983
  import { Effect as Effect5 } from "effect";
1547
- import { z as z8 } from "zod";
1984
+ import { z as z9 } from "zod";
1548
1985
  async function registerSecurity(app2, security2, version, allowedOrigins2) {
1549
1986
  app2.addHook("onRequest", async (request, reply) => {
1550
- const path13 = new URL(request.url, "http://localhost");
1987
+ const path15 = new URL(request.url, "http://localhost");
1551
1988
  reply.header("Cache-Control", "no-store");
1552
1989
  if (request.method === "OPTIONS") return;
1553
1990
  if (request.headers.origin && !allowedOrigins2.includes(request.headers.origin))
1554
1991
  return reply.code(403).send({ error: "Browser origin is not allowed." });
1555
- if (path13.pathname === "/health" || path13.pathname === "/api/auth/pair" && request.method === "POST")
1992
+ if (path15.pathname === "/health" || path15.pathname === "/api/auth/pair" && request.method === "POST")
1556
1993
  return;
1557
1994
  const authorization = request.headers.authorization ?? "";
1558
1995
  const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
1559
- const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(path13.pathname);
1560
- const ticket = path13.searchParams.get("ticket");
1561
- path13.searchParams.delete("ticket");
1562
- if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path13.pathname + path13.search, false)))
1996
+ const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(path15.pathname);
1997
+ const ticket = path15.searchParams.get("ticket");
1998
+ path15.searchParams.delete("ticket");
1999
+ if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path15.pathname + path15.search, false)))
1563
2000
  return reply.code(401).send({ error: "Pair this device with your server.", code: "UNAUTHORIZED" });
1564
2001
  if (!asset && request.headers["x-agentrific-protocol"] !== String(PROTOCOL_VERSION))
1565
2002
  return reply.code(409).send({
@@ -1614,8 +2051,8 @@ async function registerSecurity(app2, security2, version, allowedOrigins2) {
1614
2051
  ticket: security2.ticket((request.headers.authorization ?? "").slice(7), "/api/events")
1615
2052
  }));
1616
2053
  app2.post("/api/auth/asset-ticket", (request, reply) => {
1617
- const input = z8.object({
1618
- path: z8.string().regex(/^\/api\/projects\/[^/?]+\/attachments\/[^/?]+\/content(?:\?preview=1)?$/)
2054
+ const input = z9.object({
2055
+ path: z9.string().regex(/^\/api\/projects\/[^/?]+\/attachments\/[^/?]+\/content(?:\?preview=1)?$/)
1619
2056
  }).safeParse(request.body);
1620
2057
  if (!input.success) return reply.code(400).send({ error: "Choose a valid attachment." });
1621
2058
  return {
@@ -1629,8 +2066,8 @@ var init_security = __esm({
1629
2066
  "use strict";
1630
2067
  init_src();
1631
2068
  init_pairing();
1632
- secret = () => randomBytes(32).toString("hex");
1633
- digest = (value) => createHash("sha256").update(value).digest("hex");
2069
+ secret = () => randomBytes2(32).toString("hex");
2070
+ digest = (value) => createHash2("sha256").update(value).digest("hex");
1634
2071
  ServerSecurity = class {
1635
2072
  constructor(store2, serverId) {
1636
2073
  this.store = store2;
@@ -1692,17 +2129,17 @@ var init_security = __esm({
1692
2129
  if (!/^[a-f0-9]{64}$/.test(token)) return void 0;
1693
2130
  return Effect5.runSync(this.store.deviceForToken(digest(token)));
1694
2131
  }
1695
- ticket(token, path13) {
2132
+ ticket(token, path15) {
1696
2133
  this.prune();
1697
2134
  if (!this.authenticate(token) || this.tickets.size >= 1e4) return void 0;
1698
2135
  const value = secret();
1699
- this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path13 });
2136
+ this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path15 });
1700
2137
  return value;
1701
2138
  }
1702
- redeemTicket(value, path13, consume = true) {
2139
+ redeemTicket(value, path15, consume = true) {
1703
2140
  this.prune();
1704
2141
  const ticket = this.tickets.get(value);
1705
- if (!ticket || ticket.path !== path13) return void 0;
2142
+ if (!ticket || ticket.path !== path15) return void 0;
1706
2143
  if (consume) this.tickets.delete(value);
1707
2144
  return Effect5.runSync(this.store.deviceForToken(ticket.tokenHash));
1708
2145
  }
@@ -1716,6 +2153,101 @@ var init_security = __esm({
1716
2153
  }
1717
2154
  });
1718
2155
 
2156
+ // src/desktop-phone.ts
2157
+ var DesktopPhoneConnection;
2158
+ var init_desktop_phone = __esm({
2159
+ "src/desktop-phone.ts"() {
2160
+ "use strict";
2161
+ init_tunnels();
2162
+ init_opentunnel();
2163
+ DesktopPhoneConnection = class {
2164
+ constructor(origin, changed, directory, detect = async () => [
2165
+ { provider: "opentunnel", available: true, reason: "Built in \xB7 stable public HTTPS address" },
2166
+ ...await detectTunnels()
2167
+ ], create2 = (option, disconnected) => option.provider === "opentunnel" ? new OpenTunnel(directory, disconnected) : new ManagedTunnel({ ...option, provider: option.provider }, disconnected)) {
2168
+ this.origin = origin;
2169
+ this.changed = changed;
2170
+ this.directory = directory;
2171
+ this.detect = detect;
2172
+ this.create = create2;
2173
+ }
2174
+ origin;
2175
+ changed;
2176
+ directory;
2177
+ detect;
2178
+ create;
2179
+ tunnel;
2180
+ provider = null;
2181
+ publicUrl = null;
2182
+ pending = Promise.resolve();
2183
+ snapshot() {
2184
+ return { provider: this.provider, publicUrl: this.publicUrl };
2185
+ }
2186
+ async options() {
2187
+ return (await this.detect()).map(({ provider, available, reason }) => ({
2188
+ provider,
2189
+ available,
2190
+ reason
2191
+ }));
2192
+ }
2193
+ serialize(operation) {
2194
+ const result = this.pending.then(operation);
2195
+ this.pending = result.then(
2196
+ () => {
2197
+ },
2198
+ () => {
2199
+ }
2200
+ );
2201
+ return result;
2202
+ }
2203
+ start(provider, signal) {
2204
+ return this.serialize(async () => {
2205
+ signal?.throwIfAborted();
2206
+ if (this.tunnel && this.publicUrl && this.provider === provider) return this.snapshot();
2207
+ if (this.tunnel)
2208
+ throw new Error("Disconnect the current phone connection before changing providers.");
2209
+ const option = (await this.detect()).find((candidate) => candidate.provider === provider);
2210
+ if (!option?.available) throw new Error(option?.reason || "Tunnel provider unavailable.");
2211
+ let tunnel;
2212
+ tunnel = this.create(option, () => {
2213
+ if (this.tunnel !== tunnel) return;
2214
+ void tunnel.close().catch(() => {
2215
+ });
2216
+ this.tunnel = void 0;
2217
+ this.provider = null;
2218
+ this.publicUrl = null;
2219
+ this.changed(null);
2220
+ });
2221
+ this.tunnel = tunnel;
2222
+ try {
2223
+ const url = await tunnel.start(this.origin(), signal);
2224
+ signal?.throwIfAborted();
2225
+ this.provider = provider;
2226
+ this.publicUrl = url;
2227
+ this.changed(url);
2228
+ return this.snapshot();
2229
+ } catch (error) {
2230
+ await tunnel.close();
2231
+ this.tunnel = void 0;
2232
+ this.changed(null);
2233
+ throw error;
2234
+ }
2235
+ });
2236
+ }
2237
+ close() {
2238
+ return this.serialize(async () => {
2239
+ await this.tunnel?.close();
2240
+ this.tunnel = void 0;
2241
+ this.provider = null;
2242
+ this.publicUrl = null;
2243
+ this.changed(null);
2244
+ return this.snapshot();
2245
+ });
2246
+ }
2247
+ };
2248
+ }
2249
+ });
2250
+
1719
2251
  // src/maintenance.ts
1720
2252
  var MaintenanceError, Maintenance;
1721
2253
  var init_maintenance = __esm({
@@ -1756,14 +2288,14 @@ var init_maintenance = __esm({
1756
2288
  this.enter();
1757
2289
  const deadline = Date.now() + timeoutMs;
1758
2290
  while (!this.status().drained && Date.now() < deadline)
1759
- await new Promise((resolve) => setTimeout(resolve, 50));
2291
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
1760
2292
  return this.status();
1761
2293
  }
1762
2294
  attach(app2) {
1763
2295
  app2.addHook("onRequest", async (request, reply) => {
1764
- const path13 = request.url.split("?")[0];
2296
+ const path15 = request.url.split("?")[0];
1765
2297
  if (["GET", "HEAD", "OPTIONS"].includes(request.method)) return;
1766
- const allowed = path13.startsWith("/api/auth/") || /^\/api\/sessions\/[^/]+\/stop$/.test(path13);
2298
+ const allowed = path15.startsWith("/api/auth/") || /^\/api\/sessions\/[^/]+\/stop$/.test(path15);
1767
2299
  if (this.paused && !allowed)
1768
2300
  return reply.code(503).send({ code: "MAINTENANCE", error: new MaintenanceError().message });
1769
2301
  this.requests.add(request.id);
@@ -2037,7 +2569,7 @@ var init_git_flow = __esm({
2037
2569
  });
2038
2570
 
2039
2571
  // src/runtime.ts
2040
- import path6 from "node:path";
2572
+ import path8 from "node:path";
2041
2573
  import { randomUUID as randomUUID4 } from "node:crypto";
2042
2574
  import { Context as Context2, Data as Data2, Effect as Effect8, Fiber, Layer as Layer2, Semaphore as Semaphore2 } from "effect";
2043
2575
  var AgentError, AgentController, AgentSessions;
@@ -2096,8 +2628,8 @@ var init_runtime = __esm({
2096
2628
  const agent = yield* Effect8.tryPromise({
2097
2629
  try: () => this.factory.open(
2098
2630
  session.id,
2099
- session.workspacePath ?? path6.join(this.worktreesRoot, session.id),
2100
- path6.join(this.sessionsRoot, session.id),
2631
+ session.workspacePath ?? path8.join(this.worktreesRoot, session.id),
2632
+ path8.join(this.sessionsRoot, session.id),
2101
2633
  session.model,
2102
2634
  session
2103
2635
  ),
@@ -2384,17 +2916,17 @@ var init_workflow_command = __esm({
2384
2916
  });
2385
2917
 
2386
2918
  // src/workflow-schema.ts
2387
- import { z as z9 } from "zod";
2919
+ import { z as z10 } from "zod";
2388
2920
  var DeliveryJobSchema, PreparationJobSchema, WorkflowJobSchema;
2389
2921
  var init_workflow_schema = __esm({
2390
2922
  "src/workflow-schema.ts"() {
2391
2923
  "use strict";
2392
2924
  init_src();
2393
- DeliveryJobSchema = z9.object({
2394
- kind: z9.literal("delivery"),
2395
- sessionId: z9.string(),
2925
+ DeliveryJobSchema = z10.object({
2926
+ kind: z10.literal("delivery"),
2927
+ sessionId: z10.string(),
2396
2928
  request: DeliveryRequestSchema,
2397
- phase: z9.enum([
2929
+ phase: z10.enum([
2398
2930
  "validating",
2399
2931
  "drafting",
2400
2932
  "committing",
@@ -2404,25 +2936,25 @@ var init_workflow_schema = __esm({
2404
2936
  "opening_pr",
2405
2937
  "delivered"
2406
2938
  ]),
2407
- attempt: z9.number().int().positive(),
2408
- commitSha: z9.string().optional(),
2409
- title: z9.string(),
2410
- body: z9.string(),
2411
- logs: z9.string().default(""),
2412
- failed: z9.boolean().default(false),
2413
- result: z9.enum(["merged", "pr_opened", "no_changes"]).optional(),
2414
- url: z9.string().optional(),
2415
- message: z9.string().optional()
2939
+ attempt: z10.number().int().positive(),
2940
+ commitSha: z10.string().optional(),
2941
+ title: z10.string(),
2942
+ body: z10.string(),
2943
+ logs: z10.string().default(""),
2944
+ failed: z10.boolean().default(false),
2945
+ result: z10.enum(["merged", "pr_opened", "no_changes"]).optional(),
2946
+ url: z10.string().optional(),
2947
+ message: z10.string().optional()
2416
2948
  });
2417
- PreparationJobSchema = z9.object({
2418
- kind: z9.literal("preparation"),
2419
- sessionId: z9.string(),
2420
- projectPath: z9.string(),
2949
+ PreparationJobSchema = z10.object({
2950
+ kind: z10.literal("preparation"),
2951
+ sessionId: z10.string(),
2952
+ projectPath: z10.string(),
2421
2953
  input: CreateSessionSchema,
2422
- stage: z9.enum(["creating", "installing", "ready", "failed"]),
2423
- logs: z9.string().default("")
2954
+ stage: z10.enum(["creating", "installing", "ready", "failed"]),
2955
+ logs: z10.string().default("")
2424
2956
  });
2425
- WorkflowJobSchema = z9.discriminatedUnion("kind", [
2957
+ WorkflowJobSchema = z10.discriminatedUnion("kind", [
2426
2958
  DeliveryJobSchema,
2427
2959
  PreparationJobSchema
2428
2960
  ]);
@@ -2431,7 +2963,7 @@ var init_workflow_schema = __esm({
2431
2963
 
2432
2964
  // src/delivery.ts
2433
2965
  import { Effect as Effect9 } from "effect";
2434
- import { z as z10 } from "zod";
2966
+ import { z as z11 } from "zod";
2435
2967
  var DeliveryCoordinator;
2436
2968
  var init_delivery = __esm({
2437
2969
  "src/delivery.ts"() {
@@ -2730,11 +3262,11 @@ ${workflowError(error instanceof Error ? error : String(error))}`
2730
3262
  "--json",
2731
3263
  "url,state,headRefOid"
2732
3264
  ]);
2733
- const prs = z10.array(
2734
- z10.object({
2735
- url: z10.string().url(),
2736
- state: z10.string(),
2737
- headRefOid: z10.string().optional()
3265
+ const prs = z11.array(
3266
+ z11.object({
3267
+ url: z11.string().url(),
3268
+ state: z11.string(),
3269
+ headRefOid: z11.string().optional()
2738
3270
  })
2739
3271
  ).parse(JSON.parse(existing));
2740
3272
  const open = prs.find(
@@ -2777,10 +3309,10 @@ ${workflowError(error instanceof Error ? error : String(error))}`
2777
3309
  });
2778
3310
 
2779
3311
  // src/preparation.ts
2780
- import { mkdir as mkdir4, readFile as readFile3 } from "node:fs/promises";
2781
- import path7 from "node:path";
3312
+ import { mkdir as mkdir5, readFile as readFile4 } from "node:fs/promises";
3313
+ import path9 from "node:path";
2782
3314
  import { Effect as Effect10 } from "effect";
2783
- import { z as z11 } from "zod";
3315
+ import { z as z12 } from "zod";
2784
3316
  var PreparationCoordinator;
2785
3317
  var init_preparation = __esm({
2786
3318
  "src/preparation.ts"() {
@@ -2881,7 +3413,7 @@ ${logs}` : ""}`);
2881
3413
  const workspace = session.workspacePath;
2882
3414
  if (branchAtWorkspace !== void 0)
2883
3415
  throw new Error("The run workspace already exists on a different branch.");
2884
- await mkdir4(path7.dirname(workspace), { recursive: true });
3416
+ await mkdir5(path9.dirname(workspace), { recursive: true });
2885
3417
  try {
2886
3418
  await command(
2887
3419
  projectPath,
@@ -2901,12 +3433,12 @@ ${logs}` : ""}`);
2901
3433
  }
2902
3434
  }
2903
3435
  async install(workspace, command) {
2904
- const packageFile = await readFile3(path7.join(workspace, "package.json"), "utf8").catch(
3436
+ const packageFile = await readFile4(path9.join(workspace, "package.json"), "utf8").catch(
2905
3437
  () => void 0
2906
3438
  );
2907
3439
  if (!packageFile) return;
2908
- const metadata = z11.object({ packageManager: z11.string().optional() }).parse(JSON.parse(packageFile));
2909
- const has = async (name) => readFile3(path7.join(workspace, name)).then(
3440
+ const metadata = z12.object({ packageManager: z12.string().optional() }).parse(JSON.parse(packageFile));
3441
+ const has = async (name) => readFile4(path9.join(workspace, name)).then(
2910
3442
  () => true,
2911
3443
  () => false
2912
3444
  );
@@ -2989,7 +3521,7 @@ ${logs}` : ""}`);
2989
3521
 
2990
3522
  // src/delivery-writer.ts
2991
3523
  import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
2992
- import { z as z12 } from "zod";
3524
+ import { z as z13 } from "zod";
2993
3525
  function deliveryWriter(models2, createSession = createAgentSession) {
2994
3526
  return async (run, input, diff, signal) => {
2995
3527
  signal?.throwIfAborted();
@@ -3030,7 +3562,7 @@ var DraftSchema;
3030
3562
  var init_delivery_writer = __esm({
3031
3563
  "src/delivery-writer.ts"() {
3032
3564
  "use strict";
3033
- DraftSchema = z12.object({ title: z12.string().trim().min(1).max(120), body: z12.string().trim().min(1).max(1e4) }).strict();
3565
+ DraftSchema = z13.object({ title: z13.string().trim().min(1).max(120), body: z13.string().trim().min(1).max(1e4) }).strict();
3034
3566
  }
3035
3567
  });
3036
3568
 
@@ -3187,19 +3719,19 @@ var init_delivery_tool = __esm({
3187
3719
  // src/file-change.ts
3188
3720
  import { createWriteToolDefinition, defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
3189
3721
  import { createTwoFilesPatch } from "diff";
3190
- import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
3191
- import { z as z13 } from "zod";
3722
+ import { mkdir as mkdir6, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
3723
+ import { z as z14 } from "zod";
3192
3724
  function boundFileChange(change) {
3193
3725
  if ((change.patch?.length ?? 0) + (change.content?.length ?? 0) > previewLimit)
3194
3726
  return { path: change.path, omitted: true };
3195
3727
  return change;
3196
3728
  }
3197
- function completedFileChange(result, path13) {
3729
+ function completedFileChange(result, path15) {
3198
3730
  const parsed = EditResultSchema.safeParse(result);
3199
3731
  if (!parsed.success) return;
3200
3732
  const details = parsed.data.details;
3201
3733
  if (details?.fileChange) return boundFileChange(details.fileChange);
3202
- if (path13 && details?.patch) return boundFileChange({ path: path13, patch: details.patch });
3734
+ if (path15 && details?.patch) return boundFileChange({ path: path15, patch: details.patch });
3203
3735
  }
3204
3736
  function fileWriteTool(cwd) {
3205
3737
  const base = createWriteToolDefinition(cwd);
@@ -3216,16 +3748,16 @@ function fileWriteTool(cwd) {
3216
3748
  const tool = createWriteToolDefinition(cwd, {
3217
3749
  operations: {
3218
3750
  mkdir: async (dir) => {
3219
- await mkdir5(dir, { recursive: true });
3751
+ await mkdir6(dir, { recursive: true });
3220
3752
  },
3221
3753
  // Pi invokes this inside its per-file mutation queue, so the snapshot
3222
3754
  // and write cannot race another built-in edit/write on the same file.
3223
- writeFile: async (path13, content) => {
3224
- const previous = await readFile4(path13, "utf8").catch((error) => {
3755
+ writeFile: async (path15, content) => {
3756
+ const previous = await readFile5(path15, "utf8").catch((error) => {
3225
3757
  if (error.code === "ENOENT") return void 0;
3226
3758
  throw error;
3227
3759
  });
3228
- await writeFile3(path13, content, "utf8");
3760
+ await writeFile4(path15, content, "utf8");
3229
3761
  fileChange = boundFileChange(
3230
3762
  previous === void 0 ? { path: args.path, content } : {
3231
3763
  path: args.path,
@@ -3246,8 +3778,8 @@ var init_file_change = __esm({
3246
3778
  "use strict";
3247
3779
  init_src();
3248
3780
  previewLimit = 256e3;
3249
- EditResultSchema = z13.object({
3250
- details: z13.object({ patch: z13.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
3781
+ EditResultSchema = z14.object({
3782
+ details: z14.object({ patch: z14.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
3251
3783
  });
3252
3784
  }
3253
3785
  });
@@ -3259,7 +3791,7 @@ import {
3259
3791
  createAgentSession as createAgentSession2,
3260
3792
  SessionManager as SessionManager2
3261
3793
  } from "@earendil-works/pi-coding-agent";
3262
- import { z as z14 } from "zod";
3794
+ import { z as z15 } from "zod";
3263
3795
  function toolOutput(value) {
3264
3796
  const parsed = ToolResultSchema.safeParse(value);
3265
3797
  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);
@@ -3373,7 +3905,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
3373
3905
  return false;
3374
3906
  }
3375
3907
  const filePaths = /* @__PURE__ */ new Map();
3376
- const FileArgsSchema = z14.object({ path: z14.string() });
3908
+ const FileArgsSchema = z15.object({ path: z15.string() });
3377
3909
  function handleTool(event) {
3378
3910
  if (event.type === "tool_execution_start") {
3379
3911
  flushText();
@@ -3438,8 +3970,8 @@ var init_pi = __esm({
3438
3970
  init_models();
3439
3971
  init_delivery_tool();
3440
3972
  init_file_change();
3441
- ToolResultSchema = z14.object({
3442
- content: z14.array(z14.object({ type: z14.string(), text: z14.string().optional() })).optional()
3973
+ ToolResultSchema = z15.object({
3974
+ content: z15.array(z15.object({ type: z15.string(), text: z15.string().optional() })).optional()
3443
3975
  }).passthrough();
3444
3976
  boundedJson = (value, limit) => {
3445
3977
  let text;
@@ -3454,19 +3986,19 @@ var init_pi = __esm({
3454
3986
  });
3455
3987
 
3456
3988
  // src/projects.ts
3457
- import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
3989
+ import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
3458
3990
  import { randomUUID as randomUUID6 } from "node:crypto";
3459
- import { z as z15 } from "zod";
3991
+ import { z as z16 } from "zod";
3460
3992
  import { constants as constants2 } from "node:fs";
3461
3993
  import { homedir as homedir3 } from "node:os";
3462
- import path8 from "node:path";
3994
+ import path10 from "node:path";
3463
3995
  import { Effect as Effect13, Semaphore as Semaphore3 } from "effect";
3464
3996
  function resolveProjectsRoot(input) {
3465
3997
  return Effect13.tryPromise({
3466
3998
  try: async () => {
3467
3999
  const trimmed = input.trim();
3468
- const expanded = trimmed === "~" ? homedir3() : trimmed.startsWith("~/") ? path8.join(homedir3(), trimmed.slice(2)) : trimmed;
3469
- if (!path8.isAbsolute(expanded))
4000
+ const expanded = trimmed === "~" ? homedir3() : trimmed.startsWith("~/") ? path10.join(homedir3(), trimmed.slice(2)) : trimmed;
4001
+ if (!path10.isAbsolute(expanded))
3470
4002
  throw new Error("Use an absolute server path or a path beginning with ~/.");
3471
4003
  const resolved = await realpath(expanded);
3472
4004
  if (!(await stat3(resolved)).isDirectory())
@@ -3488,34 +4020,34 @@ function projectLock(store2) {
3488
4020
  return lock;
3489
4021
  }
3490
4022
  async function gitDirectory(directory) {
3491
- const location = path8.join(directory, ".git");
4023
+ const location = path10.join(directory, ".git");
3492
4024
  const info = await stat3(location);
3493
4025
  if (info.isDirectory()) return location;
3494
- const contents = await readFile5(location, "utf8");
4026
+ const contents = await readFile6(location, "utf8");
3495
4027
  const match = /^gitdir: (.+)\s*$/m.exec(contents);
3496
4028
  if (!match) throw new Error("Invalid Git worktree metadata.");
3497
- const resolved = await realpath(path8.resolve(directory, match[1].trim()));
4029
+ const resolved = await realpath(path10.resolve(directory, match[1].trim()));
3498
4030
  if (!(await stat3(resolved)).isDirectory()) throw new Error("Git directory is missing.");
3499
4031
  return resolved;
3500
4032
  }
3501
4033
  async function identity(directory, preferred) {
3502
- const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
4034
+ const filename = path10.join(await gitDirectory(directory), "agentrific-project-id");
3503
4035
  try {
3504
- return ProjectId.parse(await readFile5(filename, "utf8"));
4036
+ return ProjectId.parse(await readFile6(filename, "utf8"));
3505
4037
  } catch (error) {
3506
- if (!z15.object({ code: z15.literal("ENOENT") }).safeParse(error).success) throw error;
3507
- const legacyFilename = path8.join(await gitDirectory(directory), "chatplans-project-id");
3508
- const legacy = await readFile5(legacyFilename, "utf8").catch((cause) => {
3509
- if (!z15.object({ code: z15.literal("ENOENT") }).safeParse(cause).success) throw cause;
4038
+ if (!z16.object({ code: z16.literal("ENOENT") }).safeParse(error).success) throw error;
4039
+ const legacyFilename = path10.join(await gitDirectory(directory), "chatplans-project-id");
4040
+ const legacy = await readFile6(legacyFilename, "utf8").catch((cause) => {
4041
+ if (!z16.object({ code: z16.literal("ENOENT") }).safeParse(cause).success) throw cause;
3510
4042
  return null;
3511
4043
  });
3512
4044
  const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
3513
4045
  try {
3514
- await writeFile4(filename, id + "\n", { flag: "wx" });
4046
+ await writeFile5(filename, id + "\n", { flag: "wx" });
3515
4047
  return id;
3516
4048
  } catch (writeError) {
3517
- if (!z15.object({ code: z15.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
3518
- return ProjectId.parse(await readFile5(filename, "utf8"));
4049
+ if (!z16.object({ code: z16.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
4050
+ return ProjectId.parse(await readFile6(filename, "utf8"));
3519
4051
  }
3520
4052
  }
3521
4053
  }
@@ -3528,15 +4060,15 @@ function scanProjects(root, store2) {
3528
4060
  const entries = await readdir2(root, { withFileTypes: true });
3529
4061
  for (const entry of entries) {
3530
4062
  if (!entry.isDirectory()) continue;
3531
- const directory = path8.join(root, entry.name);
3532
- if (!await stat3(path8.join(directory, ".git")).catch(() => null)) continue;
4063
+ const directory = path10.join(root, entry.name);
4064
+ if (!await stat3(path10.join(directory, ".git")).catch(() => null)) continue;
3533
4065
  const previous = registered.find((project2) => project2.path === directory);
3534
4066
  let id = await identity(directory, previous?.available ? void 0 : previous?.id);
3535
4067
  let existing = registered.find((project2) => project2.id === id);
3536
- if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat3(path8.join(existing.path, ".git")).catch(() => null)) {
4068
+ if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat3(path10.join(existing.path, ".git")).catch(() => null)) {
3537
4069
  id = randomUUID6();
3538
- await writeFile4(
3539
- path8.join(await gitDirectory(directory), "agentrific-project-id"),
4070
+ await writeFile5(
4071
+ path10.join(await gitDirectory(directory), "agentrific-project-id"),
3540
4072
  id + "\n"
3541
4073
  );
3542
4074
  existing = void 0;
@@ -3569,16 +4101,16 @@ function reconnectProject(root, id, input, store2) {
3569
4101
  ))
3570
4102
  throw new Error("Stop this project's running sessions before reconnecting its folder.");
3571
4103
  const directory = await Effect13.runPromise(resolveProjectsRoot(input));
3572
- if (path8.dirname(directory) !== await realpath(root))
4104
+ if (path10.dirname(directory) !== await realpath(root))
3573
4105
  throw new Error("Choose a Git repository directly inside the configured projects folder.");
3574
- const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
4106
+ const filename = path10.join(await gitDirectory(directory), "agentrific-project-id");
3575
4107
  const replacedId = await identity(directory);
3576
- const project = { id, name: path8.basename(directory), path: directory, available: true };
3577
- await writeFile4(filename, id + "\n");
4108
+ const project = { id, name: path10.basename(directory), path: directory, available: true };
4109
+ await writeFile5(filename, id + "\n");
3578
4110
  try {
3579
4111
  await Effect13.runPromise(store2.relocateProject(project, previous.path, replacedId));
3580
4112
  } catch (error) {
3581
- await writeFile4(filename, replacedId + "\n");
4113
+ await writeFile5(filename, replacedId + "\n");
3582
4114
  throw error;
3583
4115
  }
3584
4116
  return project;
@@ -3591,7 +4123,7 @@ var init_projects = __esm({
3591
4123
  "src/projects.ts"() {
3592
4124
  "use strict";
3593
4125
  locks2 = /* @__PURE__ */ new WeakMap();
3594
- ProjectId = z15.string().trim().min(1).max(255);
4126
+ ProjectId = z16.string().trim().min(1).max(255);
3595
4127
  }
3596
4128
  });
3597
4129
 
@@ -3611,10 +4143,10 @@ function repositoryIdentity(remote) {
3611
4143
  if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return null;
3612
4144
  if (url.search || url.hash || url.port) return null;
3613
4145
  const host2 = url.hostname.toLowerCase();
3614
- let path13 = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
3615
- if (!host2 || !path13 || path13.includes("%") || path13.includes("//")) return null;
3616
- if (["github.com", "bitbucket.org"].includes(host2)) path13 = path13.toLowerCase();
3617
- const key = `${host2}/${path13}`;
4146
+ let path15 = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
4147
+ if (!host2 || !path15 || path15.includes("%") || path15.includes("//")) return null;
4148
+ if (["github.com", "bitbucket.org"].includes(host2)) path15 = path15.toLowerCase();
4149
+ const key = `${host2}/${path15}`;
3618
4150
  return { key, display: key };
3619
4151
  }
3620
4152
  async function git(directory, args) {
@@ -3703,36 +4235,36 @@ var init_project_git = __esm({
3703
4235
  });
3704
4236
 
3705
4237
  // src/push-schema.ts
3706
- import { z as z16 } from "zod";
4238
+ import { z as z17 } from "zod";
3707
4239
  var PushBindingSchema, PushStateSchema, PendingPushSchema;
3708
4240
  var init_push_schema = __esm({
3709
4241
  "src/push-schema.ts"() {
3710
4242
  "use strict";
3711
4243
  init_push();
3712
- PushBindingSchema = z16.object({
3713
- bindingId: z16.string().uuid(),
3714
- installationId: z16.string().uuid(),
3715
- deviceId: z16.string().uuid()
4244
+ PushBindingSchema = z17.object({
4245
+ bindingId: z17.string().uuid(),
4246
+ installationId: z17.string().uuid(),
4247
+ deviceId: z17.string().uuid()
3716
4248
  });
3717
- PushStateSchema = z16.object({
3718
- bindings: z16.array(PushBindingSchema).max(10),
3719
- error: z16.string().nullable()
4249
+ PushStateSchema = z17.object({
4250
+ bindings: z17.array(PushBindingSchema).max(10),
4251
+ error: z17.string().nullable()
3720
4252
  });
3721
4253
  PendingPushSchema = PushEventSchema.extend({
3722
- remaining: z16.array(PushBindingSchema),
3723
- createdAt: z16.number(),
3724
- attempts: z16.number().int().nonnegative(),
3725
- nextAt: z16.number()
4254
+ remaining: z17.array(PushBindingSchema),
4255
+ createdAt: z17.number(),
4256
+ attempts: z17.number().int().nonnegative(),
4257
+ nextAt: z17.number()
3726
4258
  });
3727
4259
  }
3728
4260
  });
3729
4261
 
3730
4262
  // src/store.ts
3731
4263
  import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
3732
- import path9 from "node:path";
4264
+ import path11 from "node:path";
3733
4265
  import { DatabaseSync } from "node:sqlite";
3734
4266
  import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
3735
- import { z as z17 } from "zod";
4267
+ import { z as z18 } from "zod";
3736
4268
  import { randomUUID as randomUUID7 } from "node:crypto";
3737
4269
  var StorageError, PayloadRow, EventRow, SqliteSessionStore, SessionStore;
3738
4270
  var init_store = __esm({
@@ -3745,8 +4277,8 @@ var init_store = __esm({
3745
4277
  init_src();
3746
4278
  StorageError = class extends Data3.TaggedError("StorageError") {
3747
4279
  };
3748
- PayloadRow = z17.object({ payload: z17.string() });
3749
- EventRow = z17.object({ sequence: z17.number(), recorded_at: z17.string(), payload: z17.string() });
4280
+ PayloadRow = z18.object({ payload: z18.string() });
4281
+ EventRow = z18.object({ sequence: z18.number(), recorded_at: z18.string(), payload: z18.string() });
3750
4282
  SqliteSessionStore = class {
3751
4283
  constructor(db) {
3752
4284
  this.db = db;
@@ -3797,7 +4329,7 @@ var init_store = __esm({
3797
4329
  const row = PayloadRow.parse(
3798
4330
  this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
3799
4331
  );
3800
- return z17.string().uuid().parse(JSON.parse(row.payload));
4332
+ return z18.string().uuid().parse(JSON.parse(row.payload));
3801
4333
  });
3802
4334
  }
3803
4335
  saveDevice(device, tokenHash) {
@@ -3885,10 +4417,10 @@ var init_store = __esm({
3885
4417
  `).all();
3886
4418
  const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
3887
4419
  for (const row of legacy) {
3888
- const id = z17.object({ id: z17.string() }).parse(row).id;
4420
+ const id = z18.object({ id: z18.string() }).parse(row).id;
3889
4421
  insert.run(
3890
4422
  id,
3891
- JSON.stringify({ id, name: id, path: path9.join(root, id), available: false })
4423
+ JSON.stringify({ id, name: id, path: path11.join(root, id), available: false })
3892
4424
  );
3893
4425
  }
3894
4426
  return this.db.prepare("SELECT payload FROM projects").all().map((row) => ProjectSchema.parse(JSON.parse(PayloadRow.parse(row).payload)));
@@ -3925,7 +4457,7 @@ var init_store = __esm({
3925
4457
  this.db.prepare(
3926
4458
  "UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
3927
4459
  ).run(project.path, previousPath);
3928
- const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path9.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
4460
+ const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path11.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
3929
4461
  for (const row of this.db.prepare("SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?").all(project.id)) {
3930
4462
  const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
3931
4463
  if (session.workspacePath) {
@@ -4095,7 +4627,7 @@ var init_store = __esm({
4095
4627
  if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
4096
4628
  const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
4097
4629
  if (notification) {
4098
- const count = z17.object({ total: z17.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
4630
+ const count = z18.object({ total: z18.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
4099
4631
  if (count.total >= 1e3)
4100
4632
  this.db.prepare(
4101
4633
  "DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
@@ -4141,13 +4673,13 @@ var init_store = __esm({
4141
4673
  Effect14.acquireRelease(
4142
4674
  Effect14.try({
4143
4675
  try: () => {
4144
- mkdirSync2(path9.dirname(filename), { recursive: true });
4676
+ mkdirSync2(path11.dirname(filename), { recursive: true });
4145
4677
  const db = new DatabaseSync(filename);
4146
4678
  chmodSync(filename, 384);
4147
4679
  for (const suffix of ["-wal", "-shm"])
4148
4680
  if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
4149
4681
  try {
4150
- const schema = z17.object({ user_version: z17.number() }).parse(db.prepare("PRAGMA user_version").get());
4682
+ const schema = z18.object({ user_version: z18.number() }).parse(db.prepare("PRAGMA user_version").get());
4151
4683
  if (schema.user_version > 1)
4152
4684
  throw new Error("This database requires a newer Agentrific server.");
4153
4685
  db.exec(`
@@ -4183,13 +4715,13 @@ var init_store = __esm({
4183
4715
  });
4184
4716
 
4185
4717
  // src/worktree-cleanup.ts
4186
- import { lstat, rm as rm2 } from "node:fs/promises";
4187
- import path10 from "node:path";
4718
+ import { lstat as lstat2, rm as rm2 } from "node:fs/promises";
4719
+ import path12 from "node:path";
4188
4720
  import { Effect as Effect15 } from "effect";
4189
4721
  async function removeRunFiles(dataRoot2, sessionId) {
4190
- if (path10.basename(sessionId) !== sessionId || sessionId === "." || sessionId === "..")
4722
+ if (path12.basename(sessionId) !== sessionId || sessionId === "." || sessionId === "..")
4191
4723
  throw new Error("Invalid session directory.");
4192
- await rm2(path10.join(dataRoot2, "pi", sessionId), { recursive: true, force: true });
4724
+ await rm2(path12.join(dataRoot2, "pi", sessionId), { recursive: true, force: true });
4193
4725
  }
4194
4726
  var WorktreeCleanup;
4195
4727
  var init_worktree_cleanup = __esm({
@@ -4208,12 +4740,12 @@ var init_worktree_cleanup = __esm({
4208
4740
  projectsRoot;
4209
4741
  async remove(session, discardChanges = true) {
4210
4742
  if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
4211
- const workspace = path10.resolve(session.workspacePath);
4212
- const info = await lstat(workspace).catch((error) => {
4743
+ const workspace = path12.resolve(session.workspacePath);
4744
+ const info = await lstat2(workspace).catch((error) => {
4213
4745
  if (error.code === "ENOENT") return void 0;
4214
4746
  throw error;
4215
4747
  });
4216
- if (workspace !== path10.join(this.root, session.id) || path10.basename(session.id) !== session.id) {
4748
+ if (workspace !== path12.join(this.root, session.id) || path12.basename(session.id) !== session.id) {
4217
4749
  if (!info) return;
4218
4750
  throw new Error("Refusing to remove a worktree outside the managed workspaces directory.");
4219
4751
  }
@@ -4303,10 +4835,10 @@ import { execFile as execFile4 } from "node:child_process";
4303
4835
  import { promisify as promisify4 } from "node:util";
4304
4836
  import { realpath as realpath2 } from "node:fs/promises";
4305
4837
  import { homedir as homedir4 } from "node:os";
4306
- import path11 from "node:path";
4838
+ import path13 from "node:path";
4307
4839
  import { WebSocket } from "ws";
4308
4840
  import { Effect as Effect17, ManagedRuntime } from "effect";
4309
- import { z as z18 } from "zod";
4841
+ import { z as z19 } from "zod";
4310
4842
  import { ModelRuntime } from "@earendil-works/pi-coding-agent";
4311
4843
  function publish(event, workflowUpdate = false) {
4312
4844
  const previous = event.type === "session.updated" ? Effect17.runSync(store.get(event.session.id)) : void 0;
@@ -4367,18 +4899,18 @@ async function resolveSessionProject(projectId) {
4367
4899
  return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
4368
4900
  const projectPath = await realpath2(project.path);
4369
4901
  const resolvedRoot = await realpath2(projectsRoot);
4370
- if (path11.dirname(projectPath) !== resolvedRoot)
4902
+ if (path13.dirname(projectPath) !== resolvedRoot)
4371
4903
  return { error: "Project path is outside projects root.", status: 400 };
4372
4904
  return { projectPath };
4373
4905
  }
4374
4906
  async function resolveSessionWorkspace(id, gitFlow, projectPath) {
4375
- const worktreePath = path11.join(worktreesRoot, id);
4907
+ const worktreePath = path13.join(worktreesRoot, id);
4376
4908
  const branch = gitFlow ? `agentrific/${id.slice(0, 8)}` : null;
4377
4909
  const useProjectCheckout = gitFlow?.workspace === "checkout";
4378
4910
  const workspacePath = !gitFlow || useProjectCheckout ? projectPath : worktreePath;
4379
4911
  if (useProjectCheckout) {
4380
4912
  const serverPath = await realpath2(process.cwd());
4381
- if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path11.sep}`))
4913
+ if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path13.sep}`))
4382
4914
  return {
4383
4915
  error: "The server runs from this project. Choose a worktree for this task.",
4384
4916
  status: 409
@@ -4513,7 +5045,11 @@ async function withSessionTaskLock(id, operation) {
4513
5045
  const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
4514
5046
  return withTaskLock(lockId, operation);
4515
5047
  }
4516
- 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;
5048
+ function localServerUrl() {
5049
+ const address = z19.object({ port: z19.number() }).safeParse(app.server.address());
5050
+ return address.success ? `http://${host}:${address.data.port}` : void 0;
5051
+ }
5052
+ 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, phoneOrigin, phone, closeControl;
4517
5053
  var init_index = __esm({
4518
5054
  async "src/index.ts"() {
4519
5055
  "use strict";
@@ -4524,6 +5060,7 @@ var init_index = __esm({
4524
5060
  init_event_server();
4525
5061
  init_security();
4526
5062
  init_control();
5063
+ init_desktop_phone();
4527
5064
  init_version();
4528
5065
  init_maintenance();
4529
5066
  init_updates();
@@ -4543,9 +5080,9 @@ var init_index = __esm({
4543
5080
  init_repo_chat();
4544
5081
  host = process.env.AGENTRIFIC_HOST ?? "127.0.0.1";
4545
5082
  port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
4546
- defaultProjectsRoot = path11.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
4547
- worktreesRoot = path11.resolve(
4548
- process.env.AGENTRIFIC_WORKTREES_DIR ?? path11.join(homedir4(), ".agentrific", "workspaces")
5083
+ defaultProjectsRoot = path13.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
5084
+ worktreesRoot = path13.resolve(
5085
+ process.env.AGENTRIFIC_WORKTREES_DIR ?? path13.join(homedir4(), ".agentrific", "workspaces")
4549
5086
  );
4550
5087
  execFileAsync = promisify4(execFile4);
4551
5088
  if (host !== "127.0.0.1" && host !== "::1")
@@ -4568,9 +5105,9 @@ var init_index = __esm({
4568
5105
  }
4569
5106
  }
4570
5107
  });
4571
- dataRoot = path11.resolve(process.env.AGENTRIFIC_DATA_DIR ?? "./data");
5108
+ dataRoot = path13.resolve(process.env.AGENTRIFIC_DATA_DIR ?? "./data");
4572
5109
  releaseLock = await acquireServerLock(dataRoot);
4573
- effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
5110
+ effects = ManagedRuntime.make(SessionStore.layer(path13.join(dataRoot, "sessions.sqlite")));
4574
5111
  store = await effects.runPromise(SessionStore);
4575
5112
  security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
4576
5113
  sandboxPushUrl = process.env.AGENTRIFIC_PUSH_SANDBOX === "1" ? PUSH_SANDBOX_API_URL : void 0;
@@ -4589,6 +5126,7 @@ var init_index = __esm({
4589
5126
  allowedOrigins = [
4590
5127
  .../* @__PURE__ */ new Set([
4591
5128
  "agentrific-desktop://app",
5129
+ "https://app.agentrific.com",
4592
5130
  ...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
4593
5131
  `http://${host === "::1" ? "[::1]" : host}:${port}`,
4594
5132
  ...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
@@ -4601,7 +5139,7 @@ var init_index = __esm({
4601
5139
  await registerSecurity(app, security, serverVersion, allowedOrigins);
4602
5140
  registerPushRoutes(app, push, security);
4603
5141
  attachmentEffects = ManagedRuntime.make(
4604
- Attachments.layer(path11.join(dataRoot, "attachments"), store)
5142
+ Attachments.layer(path13.join(dataRoot, "attachments"), store)
4605
5143
  );
4606
5144
  attachments = await attachmentEffects.runPromise(Attachments);
4607
5145
  await Effect17.runPromise(attachments.cleanup());
@@ -4666,7 +5204,7 @@ var init_index = __esm({
4666
5204
  createPiFactory(attachments, models, delivery),
4667
5205
  publish,
4668
5206
  worktreesRoot,
4669
- path11.join(dataRoot, "pi"),
5207
+ path13.join(dataRoot, "pi"),
4670
5208
  (sessionId, messageId) => Effect17.runSync(store.hasMessage(sessionId, messageId)),
4671
5209
  (session) => delivery.settled(session.id)
4672
5210
  )
@@ -4827,7 +5365,7 @@ var init_index = __esm({
4827
5365
  return project ?? reply.code(404).send({ error: "Project not found on this server." });
4828
5366
  });
4829
5367
  app.post("/api/projects/:id/reconnect", async (request, reply) => {
4830
- const parsed = z18.object({ path: z18.string().trim().min(1).max(4096) }).safeParse(request.body);
5368
+ const parsed = z19.object({ path: z19.string().trim().min(1).max(4096) }).safeParse(request.body);
4831
5369
  if (!parsed.success)
4832
5370
  return reply.code(400).send({ error: "Enter the project's new folder path." });
4833
5371
  const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
@@ -4913,7 +5451,7 @@ var init_index = __esm({
4913
5451
  }
4914
5452
  });
4915
5453
  app.post("/api/tasks/:id/discard-unrun", async (request, reply) => {
4916
- const parsed = z18.object({ updatedAt: z18.string().datetime() }).safeParse(request.body);
5454
+ const parsed = z19.object({ updatedAt: z19.string().datetime() }).safeParse(request.body);
4917
5455
  if (!parsed.success) return reply.code(400).send({ error: "The task revision is required." });
4918
5456
  try {
4919
5457
  await deleteTask(store, request.params.id, void 0, parsed.data.updatedAt);
@@ -4983,9 +5521,9 @@ var init_index = __esm({
4983
5521
  }
4984
5522
  })
4985
5523
  );
4986
- HistoryQuery = z18.object({
4987
- after: z18.coerce.number().int().nonnegative().default(0),
4988
- limit: z18.coerce.number().int().min(1).max(1e3).default(500)
5524
+ HistoryQuery = z19.object({
5525
+ after: z19.coerce.number().int().nonnegative().default(0),
5526
+ limit: z19.coerce.number().int().min(1).max(1e3).default(500)
4989
5527
  });
4990
5528
  app.get("/api/sessions/:id/events", async (request, reply) => {
4991
5529
  const query = HistoryQuery.safeParse(request.query);
@@ -4999,7 +5537,7 @@ var init_index = __esm({
4999
5537
  const location = await resolveSessionProject(request.params.id);
5000
5538
  if (location.error !== void 0)
5001
5539
  return reply.code(location.status).send({ error: location.error });
5002
- const parsed = z18.object({ reset: z18.boolean().optional() }).safeParse(request.body ?? {});
5540
+ const parsed = z19.object({ reset: z19.boolean().optional() }).safeParse(request.body ?? {});
5003
5541
  if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
5004
5542
  try {
5005
5543
  return await openRepoChat(
@@ -5023,7 +5561,7 @@ var init_index = __esm({
5023
5561
  if (!result.session) return reply.code(result.status).send({ error: result.error });
5024
5562
  return reply.code(201).send(result.session);
5025
5563
  });
5026
- StartTaskBody = z18.object({ prompt: z18.string().trim().max(2e4).optional() }).default({});
5564
+ StartTaskBody = z19.object({ prompt: z19.string().trim().max(2e4).optional() }).default({});
5027
5565
  app.post("/api/tasks/:id/start", async (request, reply) => {
5028
5566
  const body = StartTaskBody.safeParse(request.body ?? {});
5029
5567
  if (!body.success) return reply.code(400).send({ error: "The task prompt is too long." });
@@ -5058,7 +5596,7 @@ var init_index = __esm({
5058
5596
  return automation.configure(parsed.data);
5059
5597
  });
5060
5598
  app.patch("/api/automation/queue", async (request, reply) => {
5061
- const parsed = z18.object({ taskId: z18.string().min(1), beforeId: z18.string().min(1).nullable() }).safeParse(request.body);
5599
+ const parsed = z19.object({ taskId: z19.string().min(1), beforeId: z19.string().min(1).nullable() }).safeParse(request.body);
5062
5600
  if (!parsed.success)
5063
5601
  return reply.code(400).send({ error: "Choose a task and a queue position." });
5064
5602
  try {
@@ -5115,12 +5653,31 @@ var init_index = __esm({
5115
5653
  type: "snapshot",
5116
5654
  sessions: Effect17.runSync(store.list())
5117
5655
  }));
5656
+ phone = process.env.AGENTRIFIC_DESKTOP_SERVER === "1" ? new DesktopPhoneConnection(
5657
+ () => {
5658
+ const origin = localServerUrl();
5659
+ if (!origin) throw new Error("Start the local server before connecting your phone.");
5660
+ return origin;
5661
+ },
5662
+ (url) => {
5663
+ if (phoneOrigin) {
5664
+ const index = allowedOrigins.indexOf(phoneOrigin);
5665
+ if (index >= 0) allowedOrigins.splice(index, 1);
5666
+ }
5667
+ phoneOrigin = url || void 0;
5668
+ process.env.AGENTRIFIC_PUBLIC_URL = url || "";
5669
+ if (url && !allowedOrigins.includes(url)) allowedOrigins.push(url);
5670
+ },
5671
+ path13.join(dataRoot, "phone-connection")
5672
+ ) : void 0;
5118
5673
  closeControl = await startControl(
5119
5674
  dataRoot,
5120
5675
  security,
5121
5676
  () => ({
5122
5677
  serverId: security.serverId,
5123
5678
  publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
5679
+ phoneProvider: phone?.snapshot().provider,
5680
+ localUrl: localServerUrl(),
5124
5681
  serverVersion,
5125
5682
  connectedDevices: new Set(security.sockets.values()).size,
5126
5683
  connectedSockets: security.sockets.size,
@@ -5128,10 +5685,16 @@ var init_index = __esm({
5128
5685
  activeRuns: activeAgents(Effect17.runSync(store.list())),
5129
5686
  maintenance: maintenance.status()
5130
5687
  }),
5131
- maintenance
5688
+ maintenance,
5689
+ async () => {
5690
+ await app.close();
5691
+ process.exit(0);
5692
+ },
5693
+ phone
5132
5694
  );
5133
5695
  push.start();
5134
5696
  app.addHook("onClose", async () => {
5697
+ await phone?.close();
5135
5698
  await push.close();
5136
5699
  maintenance.enter();
5137
5700
  stopUpdateDiscovery();
@@ -5164,9 +5727,9 @@ var init_index = __esm({
5164
5727
 
5165
5728
  // src/cli.ts
5166
5729
  init_warnings();
5167
- import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
5730
+ import { mkdir as mkdir7, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
5168
5731
  import { homedir as homedir5 } from "node:os";
5169
- import path12 from "node:path";
5732
+ import path14 from "node:path";
5170
5733
  import { fileURLToPath } from "node:url";
5171
5734
 
5172
5735
  // src/service.ts
@@ -5265,284 +5828,18 @@ init_version();
5265
5828
  init_pairing();
5266
5829
  import { spawn as spawn2 } from "node:child_process";
5267
5830
  import { parseArgs } from "node:util";
5268
- import { z as z19 } from "zod";
5831
+ import { z as z20 } from "zod";
5269
5832
 
5270
5833
  // src/terminal.ts
5271
5834
  init_pairing();
5272
5835
  init_cli_output();
5836
+ init_tunnels();
5837
+ init_opentunnel();
5273
5838
  import { createInterface } from "node:readline/promises";
5274
5839
  import select from "@inquirer/select";
5275
5840
  import { emitKeypressEvents as emitKeypressEvents2 } from "node:readline";
5276
5841
  import { styleText as styleText3 } from "node:util";
5277
-
5278
- // src/tunnels.ts
5279
- init_cli_output();
5280
- import { spawn } from "node:child_process";
5281
- import { access, stat } from "node:fs/promises";
5282
- import { constants } from "node:fs";
5283
- import { homedir as homedir2 } from "node:os";
5284
- import path3 from "node:path";
5285
- import { z as z5 } from "zod";
5286
- function signalChild(child, signal) {
5287
- if (!child.pid) return;
5288
- if (process.platform === "win32") {
5289
- child.kill(signal);
5290
- return;
5291
- }
5292
- try {
5293
- process.kill(-child.pid, signal);
5294
- } catch (error) {
5295
- if (!z5.object({ code: z5.literal("ESRCH") }).safeParse(error).success) child.kill(signal);
5296
- }
5297
- }
5298
- function execute2(file, args, timeoutMs, env = process.env) {
5299
- return new Promise((resolve, reject) => {
5300
- const child = spawn(file, args, {
5301
- env,
5302
- detached: process.platform !== "win32",
5303
- stdio: ["ignore", "pipe", "pipe"]
5304
- });
5305
- let stdout = "";
5306
- let stderr = "";
5307
- const timeout = setTimeout(() => {
5308
- signalChild(child, "SIGKILL");
5309
- reject(new Error("Command probe timed out."));
5310
- }, timeoutMs);
5311
- child.stdout?.on("data", (data) => {
5312
- stdout += data.toString();
5313
- checkOutput();
5314
- });
5315
- child.stderr?.on("data", (data) => {
5316
- stderr += data.toString();
5317
- checkOutput();
5318
- });
5319
- child.once("error", (error) => {
5320
- clearTimeout(timeout);
5321
- reject(error);
5322
- });
5323
- child.once("exit", () => signalChild(child, "SIGKILL"));
5324
- child.once("close", (code) => {
5325
- clearTimeout(timeout);
5326
- if (code !== 0) reject(new Error("Command probe failed."));
5327
- else resolve({ stdout, stderr });
5328
- });
5329
- function checkOutput() {
5330
- if (stdout.length + stderr.length <= 1048576) return;
5331
- signalChild(child, "SIGKILL");
5332
- reject(new Error("Command probe output exceeded its limit."));
5333
- }
5334
- });
5335
- }
5336
- async function findExecutable(name) {
5337
- const candidates = (process.env.PATH ?? "").split(path3.delimiter).map((directory) => path3.resolve(directory, name));
5338
- if (process.platform === "darwin") {
5339
- candidates.push(`/opt/homebrew/bin/${name}`, `/usr/local/bin/${name}`);
5340
- if (name === "tailscale")
5341
- candidates.push("/Applications/Tailscale.app/Contents/MacOS/Tailscale");
5342
- }
5343
- for (const candidate of candidates) {
5344
- try {
5345
- await access(candidate, constants.X_OK);
5346
- if ((await stat(candidate)).isFile()) return candidate;
5347
- } catch {
5348
- continue;
5349
- }
5350
- }
5351
- return void 0;
5352
- }
5353
- var TcpConfig = z5.object({ TCP: z5.record(z5.string(), z5.unknown()).optional() });
5354
- var ServeConfig = TcpConfig.extend({
5355
- Foreground: z5.record(z5.string(), TcpConfig).optional()
5356
- });
5357
- async function probeTailscale() {
5358
- const option = { provider: "tailscale", available: false, reason: "Not installed" };
5359
- const executable = await findExecutable("tailscale");
5360
- if (!executable) return option;
5361
- option.executable = executable;
5362
- const env = { ...process.env, TAILSCALE_BE_CLI: "1" };
5363
- try {
5364
- const response = await execute2(executable, ["status", "--json"], 3e3, env);
5365
- const status = z5.object({
5366
- BackendState: z5.string(),
5367
- CertDomains: z5.array(z5.string()).optional(),
5368
- Self: z5.object({ DNSName: z5.string() }).nullable().optional()
5369
- }).parse(JSON.parse(response.stdout));
5370
- if (status.BackendState !== "Running")
5371
- return { ...option, reason: "Installed \xB7 connect Tailscale first" };
5372
- const dns = status.Self?.DNSName.replace(/\.$/, "");
5373
- if (!dns || !status.CertDomains?.includes(dns))
5374
- return { ...option, reason: "Enable HTTPS certificates in your tailnet first" };
5375
- const help = await execute2(executable, ["serve", "--help"], 2e3, env);
5376
- if (!`${help.stdout}${help.stderr}`.includes("--https"))
5377
- return { ...option, reason: "Update Tailscale to use foreground HTTPS Serve" };
5378
- const responseConfig = await execute2(executable, ["serve", "status", "--json"], 3e3, env);
5379
- const config = ServeConfig.parse(JSON.parse(responseConfig.stdout) ?? {});
5380
- const occupied = new Set(Object.keys(config.TCP ?? {}));
5381
- for (const foreground of Object.values(config.Foreground ?? {}))
5382
- for (const port3 of Object.keys(foreground.TCP ?? {})) occupied.add(port3);
5383
- const port2 = [443, ...Array.from({ length: 10 }, (_, index) => 8443 + index)].find(
5384
- (candidate) => !occupied.has(String(candidate))
5385
- );
5386
- if (!port2) return { ...option, reason: "HTTPS Serve ports are in use \xB7 paste an existing URL" };
5387
- return {
5388
- ...option,
5389
- available: true,
5390
- reason: "Private to your tailnet \xB7 Tailscale required on your phone",
5391
- publicUrl: new URL(`https://${dns}${port2 === 443 ? "" : `:${port2}`}`).origin,
5392
- httpsPort: port2
5393
- };
5394
- } catch {
5395
- return { ...option, reason: "Installed \xB7 could not verify daemon or Serve access" };
5396
- }
5397
- }
5398
- async function probeCloudflared(home) {
5399
- const option = {
5400
- provider: "cloudflared",
5401
- available: false,
5402
- reason: "Not installed"
5403
- };
5404
- const executable = await findExecutable("cloudflared");
5405
- if (!executable) return option;
5406
- option.executable = executable;
5407
- try {
5408
- await execute2(executable, ["--version"], 2e3);
5409
- const help = await execute2(executable, ["tunnel", "--help"], 2e3);
5410
- if (!`${help.stdout}${help.stderr}`.includes("--url"))
5411
- return { ...option, reason: "Installed \xB7 this version does not support Quick Tunnels" };
5412
- } catch {
5413
- return { ...option, reason: "Installed \xB7 command probe failed or timed out" };
5414
- }
5415
- const directories = [
5416
- path3.join(home, ".cloudflared"),
5417
- path3.join(home, ".cloudflare-warp"),
5418
- path3.join(home, "cloudflare-warp"),
5419
- "/etc/cloudflared",
5420
- "/usr/local/etc/cloudflared"
5421
- ];
5422
- const configurations = await Promise.all(
5423
- directories.flatMap(
5424
- (directory) => ["config.yml", "config.yaml"].map(
5425
- async (name) => access(path3.join(directory, name), constants.F_OK).then(
5426
- () => true,
5427
- () => false
5428
- )
5429
- )
5430
- )
5431
- );
5432
- if (configurations.some(Boolean))
5433
- return { ...option, reason: "Existing tunnel configuration \xB7 paste its HTTPS URL" };
5434
- return { ...option, available: true, reason: "Temporary public HTTPS URL \xB7 changes each start" };
5435
- }
5436
- async function detectTunnels(home = homedir2()) {
5437
- return Promise.all([probeTailscale(), probeCloudflared(home)]);
5438
- }
5439
- var ManagedTunnel = class {
5440
- constructor(option) {
5441
- this.option = option;
5442
- }
5443
- option;
5444
- child;
5445
- stopping = false;
5446
- connected = false;
5447
- closing;
5448
- running() {
5449
- return this.connected && !this.stopping;
5450
- }
5451
- stopOnSignal = () => {
5452
- void this.close();
5453
- };
5454
- stopOnExit = () => {
5455
- if (this.child) signalChild(this.child, "SIGKILL");
5456
- };
5457
- async start(origin, signal) {
5458
- signal?.throwIfAborted();
5459
- if (!this.option.available) throw new Error(this.option.reason);
5460
- const tailscale = this.option.provider === "tailscale";
5461
- const child = spawn(
5462
- this.option.executable ?? this.option.provider,
5463
- tailscale ? ["serve", "--bg=false", `--https=${this.option.httpsPort}`, origin] : ["tunnel", "--no-autoupdate", "--url", origin],
5464
- {
5465
- stdio: ["ignore", "pipe", "pipe"],
5466
- detached: process.platform !== "win32",
5467
- env: tailscale ? { ...process.env, TAILSCALE_BE_CLI: "1" } : process.env
5468
- }
5469
- );
5470
- this.child = child;
5471
- process.once("SIGINT", this.stopOnSignal);
5472
- process.once("SIGTERM", this.stopOnSignal);
5473
- process.once("exit", this.stopOnExit);
5474
- return new Promise((resolve, reject) => {
5475
- let output = "";
5476
- let address;
5477
- let settled = false;
5478
- const abort = () => fail("Tunnel startup canceled.");
5479
- const timeout = setTimeout(
5480
- () => fail("Tunnel startup timed out. Check your connection or paste an existing HTTPS URL."),
5481
- 45e3
5482
- );
5483
- const fail = (message) => {
5484
- if (settled) return;
5485
- settled = true;
5486
- clearTimeout(timeout);
5487
- signal?.removeEventListener("abort", abort);
5488
- void this.close();
5489
- reject(new Error(message));
5490
- };
5491
- const read = (data) => {
5492
- output = `${output}${data.toString()}`.slice(-65536);
5493
- address = tailscale ? this.option.publicUrl : output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com\b/)?.[0];
5494
- const ready = tailscale ? Boolean(
5495
- address && output.includes(address) && output.includes("Available within your tailnet")
5496
- ) : output.includes("Registered tunnel connection");
5497
- if (settled || !address || !ready) return;
5498
- settled = true;
5499
- this.connected = true;
5500
- clearTimeout(timeout);
5501
- signal?.removeEventListener("abort", abort);
5502
- resolve(address);
5503
- };
5504
- child.stdout?.on("data", read);
5505
- child.stderr?.on("data", read);
5506
- signal?.addEventListener("abort", abort, { once: true });
5507
- child.once(
5508
- "error",
5509
- () => fail(`${this.option.provider} could not start. Check the installation and permissions.`)
5510
- );
5511
- child.once("exit", () => signalChild(child, "SIGTERM"));
5512
- child.once("close", (code) => {
5513
- const unexpected = this.connected && !this.stopping;
5514
- this.connected = false;
5515
- process.removeListener("SIGINT", this.stopOnSignal);
5516
- process.removeListener("SIGTERM", this.stopOnSignal);
5517
- process.removeListener("exit", this.stopOnExit);
5518
- fail(
5519
- `${this.option.provider} exited (${code ?? "signal"}). Check tunnel access or paste an existing HTTPS URL.`
5520
- );
5521
- if (unexpected)
5522
- printError(
5523
- "The managed tunnel stopped. Restart the server to reconnect your phone; local work can continue."
5524
- );
5525
- });
5526
- });
5527
- }
5528
- close() {
5529
- this.stopping = true;
5530
- const child = this.child;
5531
- if (!child?.pid || child.exitCode !== null || child.signalCode !== null)
5532
- return Promise.resolve();
5533
- this.closing ??= new Promise((resolve) => {
5534
- const timeout = setTimeout(() => signalChild(child, "SIGKILL"), 2e3);
5535
- child.once("close", () => {
5536
- clearTimeout(timeout);
5537
- resolve();
5538
- });
5539
- signalChild(child, "SIGTERM");
5540
- });
5541
- return this.closing;
5542
- }
5543
- };
5544
-
5545
- // src/terminal.ts
5842
+ import path5 from "node:path";
5546
5843
  async function promptPublicUrl(signal, saved) {
5547
5844
  const prompt = createInterface({ input: process.stdin, output: process.stdout });
5548
5845
  const cancel = () => controller.abort();
@@ -5586,9 +5883,14 @@ async function selectConnectionMethod(options, signal, saved) {
5586
5883
  return select(
5587
5884
  {
5588
5885
  message: "Connection method",
5589
- default: saved ? "existing" : void 0,
5886
+ default: saved ? "existing" : "opentunnel",
5590
5887
  loop: false,
5591
5888
  choices: [
5889
+ {
5890
+ value: "opentunnel",
5891
+ name: "OpenTunnel (default)",
5892
+ description: "Built-in HTTPS connection with a persistent address. No separate CLI needed."
5893
+ },
5592
5894
  ...options.filter((option) => option.available).map((option) => ({
5593
5895
  value: option.provider,
5594
5896
  name: option.provider === "tailscale" ? "Tailscale" : "Cloudflared",
@@ -5619,13 +5921,14 @@ async function selectConnectionMethod(options, signal, saved) {
5619
5921
  { signal }
5620
5922
  );
5621
5923
  }
5622
- async function startSelectedTunnel(options, choice, origin, signal) {
5924
+ async function startSelectedTunnel(options, choice, origin, signal, dataDir) {
5623
5925
  const option = options.find((candidate) => candidate.provider === choice);
5624
- if (!option?.available) throw new Error("The selected tunnel is unavailable.");
5625
- const tunnel = new ManagedTunnel(option);
5926
+ if (choice !== "opentunnel" && !option?.available)
5927
+ throw new Error("The selected tunnel is unavailable.");
5928
+ const tunnel = choice === "opentunnel" ? new OpenTunnel(path5.join(dataDir, "phone-connection")) : new ManagedTunnel(option);
5626
5929
  try {
5627
5930
  console.log(styleText3("dim", `
5628
- Starting ${option.provider}\u2026
5931
+ Starting ${choice}\u2026
5629
5932
  `));
5630
5933
  const url = await tunnel.start(origin, signal);
5631
5934
  return { url, tunnel, remember: false };
@@ -5636,7 +5939,7 @@ async function startSelectedTunnel(options, choice, origin, signal) {
5636
5939
  return void 0;
5637
5940
  }
5638
5941
  }
5639
- async function selectPublicConnection(origin, saved) {
5942
+ async function selectPublicConnection(origin, saved, dataDir = process.env.AGENTRIFIC_DATA_DIR || ".agentrific") {
5640
5943
  const controller = new AbortController();
5641
5944
  const cancel = () => controller.abort();
5642
5945
  process.once("SIGINT", cancel);
@@ -5657,7 +5960,13 @@ async function selectPublicConnection(origin, saved) {
5657
5960
  console.log();
5658
5961
  return { url: await promptPublicUrl(controller.signal, saved), remember: true };
5659
5962
  }
5660
- const connection = await startSelectedTunnel(options, choice, origin, controller.signal);
5963
+ const connection = await startSelectedTunnel(
5964
+ options,
5965
+ choice,
5966
+ origin,
5967
+ controller.signal,
5968
+ dataDir
5969
+ );
5661
5970
  if (connection) return connection;
5662
5971
  }
5663
5972
  } finally {
@@ -5745,11 +6054,14 @@ function startTerminal(dataDir, publicUrl, tunnel) {
5745
6054
  }
5746
6055
 
5747
6056
  // src/cli.ts
6057
+ init_opentunnel();
6058
+ init_tunnels();
5748
6059
  init_cli_output();
5749
- var ConfigSchema = z19.object({
5750
- dataDir: z19.string(),
5751
- projectsDir: z19.string(),
5752
- publicUrl: z19.string().url().optional()
6060
+ var ConfigSchema = z20.object({
6061
+ dataDir: z20.string(),
6062
+ projectsDir: z20.string(),
6063
+ publicUrl: z20.string().url().optional(),
6064
+ connection: z20.enum(["opentunnel", "tailscale", "cloudflared", "existing", "local"]).optional()
5753
6065
  });
5754
6066
  function parseCommandArgs() {
5755
6067
  return parseArgs({
@@ -5757,6 +6069,7 @@ function parseCommandArgs() {
5757
6069
  options: {
5758
6070
  "data-dir": { type: "string" },
5759
6071
  "public-url": { type: "string" },
6072
+ connection: { type: "string" },
5760
6073
  version: { type: "boolean" },
5761
6074
  help: { type: "boolean" },
5762
6075
  check: { type: "boolean" },
@@ -5777,15 +6090,15 @@ async function main() {
5777
6090
  printHelp();
5778
6091
  return;
5779
6092
  }
5780
- const dataDir = path12.resolve(
5781
- values["data-dir"] ?? process.env.AGENTRIFIC_DATA_DIR ?? path12.join(homedir5(), ".agentrific")
6093
+ const dataDir = path14.resolve(
6094
+ values["data-dir"] ?? process.env.AGENTRIFIC_DATA_DIR ?? path14.join(homedir5(), ".agentrific")
5782
6095
  );
5783
- const filename = path12.join(dataDir, "config.json");
6096
+ const filename = path14.join(dataDir, "config.json");
5784
6097
  const config = ConfigSchema.parse(
5785
6098
  JSON.parse(
5786
- await readFile6(filename, "utf8").catch((error) => {
5787
- if (!z19.object({ code: z19.literal("ENOENT") }).safeParse(error).success) throw error;
5788
- return JSON.stringify({ dataDir, projectsDir: path12.join(homedir5(), "projects") });
6099
+ await readFile7(filename, "utf8").catch((error) => {
6100
+ if (!z20.object({ code: z20.literal("ENOENT") }).safeParse(error).success) throw error;
6101
+ return JSON.stringify({ dataDir, projectsDir: path14.join(homedir5(), "projects") });
5789
6102
  })
5790
6103
  )
5791
6104
  );
@@ -5802,11 +6115,12 @@ async function main() {
5802
6115
  throw new Error("Unknown command. Run agentrific --help.");
5803
6116
  }
5804
6117
  async function initCommand({ values, dataDir, filename, config }) {
5805
- await mkdir6(dataDir, { recursive: true, mode: 448 });
6118
+ await mkdir7(dataDir, { recursive: true, mode: 448 });
5806
6119
  const publicUrl = values["public-url"] ?? config.publicUrl;
6120
+ const connection = values.connection ? ConfigSchema.shape.connection.unwrap().parse(values.connection) : config.connection;
5807
6121
  if (publicUrl && new URL(publicUrl).protocol !== "https:")
5808
6122
  throw new Error("Configure an HTTPS URL from Tailscale Serve or Cloudflare Tunnel.");
5809
- await writeFile5(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
6123
+ await writeFile6(filename, JSON.stringify({ ...config, publicUrl, connection }, null, 2) + "\n", {
5810
6124
  mode: 384
5811
6125
  });
5812
6126
  console.log(`Configuration saved to ${filename}. Start with agentrific start.`);
@@ -5819,28 +6133,48 @@ async function startCommand({ values, dataDir, filename, config }) {
5819
6133
  process.stdin.isTTY && process.stdout.isTTY && !values["no-interactive"]
5820
6134
  );
5821
6135
  const explicitUrl = values["public-url"] ?? process.env.AGENTRIFIC_PUBLIC_URL;
6136
+ const method = values.connection ? z20.enum(["opentunnel", "tailscale", "cloudflared", "existing", "local"]).parse(values.connection) : explicitUrl ? "existing" : config.connection;
6137
+ const origin = `http://${process.env.AGENTRIFIC_HOST ?? "127.0.0.1"}:${process.env.AGENTRIFIC_PORT ?? "4318"}`;
5822
6138
  if (interactive) {
5823
6139
  process.env.AGENTRIFIC_INTERACTIVE = "1";
5824
6140
  printHeading("server");
5825
6141
  }
5826
- const connection = interactive && !explicitUrl ? await selectPublicConnection(
5827
- `http://${process.env.AGENTRIFIC_HOST ?? "127.0.0.1"}:${process.env.AGENTRIFIC_PORT ?? "4318"}`,
5828
- config.publicUrl
5829
- ) : {
5830
- url: explicitUrl ? pairingOrigin(explicitUrl) : config.publicUrl,
6142
+ let connection = interactive && !explicitUrl && !method ? await selectPublicConnection(origin, config.publicUrl, dataDir) : {
6143
+ url: method === "local" ? void 0 : explicitUrl ? pairingOrigin(explicitUrl) : config.publicUrl,
5831
6144
  remember: false,
5832
6145
  tunnel: void 0
5833
6146
  };
6147
+ const provider = method || (!interactive && !connection.url ? "opentunnel" : void 0);
6148
+ if (provider && !["existing", "local"].includes(provider)) {
6149
+ const option = provider === "opentunnel" ? void 0 : (await detectTunnels()).find((candidate) => candidate.provider === provider);
6150
+ if (provider !== "opentunnel" && !option?.available)
6151
+ throw new Error(option?.reason || "The selected tunnel is unavailable.");
6152
+ const tunnel = provider === "opentunnel" ? new OpenTunnel(path14.join(dataDir, "phone-connection")) : new ManagedTunnel(option);
6153
+ try {
6154
+ connection = { url: await tunnel.start(origin), remember: false, tunnel };
6155
+ } catch (error) {
6156
+ await tunnel.close();
6157
+ throw error;
6158
+ }
6159
+ }
6160
+ if (method === "existing" && !connection.url)
6161
+ throw new Error("Pass --public-url with the HTTPS address from your existing tunnel.");
5834
6162
  const publicUrl = connection.url;
5835
6163
  try {
5836
6164
  if (connection.remember && publicUrl) {
5837
- await mkdir6(dataDir, { recursive: true, mode: 448 });
5838
- await writeFile5(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
6165
+ await mkdir7(dataDir, { recursive: true, mode: 448 });
6166
+ await writeFile6(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
5839
6167
  mode: 384
5840
6168
  });
5841
6169
  }
5842
6170
  if (publicUrl) process.env.AGENTRIFIC_PUBLIC_URL = pairingOrigin(publicUrl);
6171
+ else delete process.env.AGENTRIFIC_PUBLIC_URL;
5843
6172
  await init_index().then(() => index_exports);
6173
+ if (connection.tunnel) {
6174
+ const close = () => void connection.tunnel?.close();
6175
+ process.once("SIGINT", close);
6176
+ process.once("SIGTERM", close);
6177
+ }
5844
6178
  if (interactive) startTerminal(dataDir, publicUrl, connection.tunnel);
5845
6179
  } catch (error) {
5846
6180
  await connection.tunnel?.close();
@@ -5857,7 +6191,7 @@ async function serviceCommand({ positionals, dataDir }) {
5857
6191
  return;
5858
6192
  }
5859
6193
  async function logsCommand({ dataDir }) {
5860
- 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" });
6194
+ 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" });
5861
6195
  child.on("error", (error) => {
5862
6196
  console.error(error.message);
5863
6197
  process.exitCode = 1;
@@ -5891,11 +6225,11 @@ async function pairCommand({ values, dataDir, config }) {
5891
6225
  return;
5892
6226
  }
5893
6227
  async function maintenanceCommand({ values, positionals, dataDir }) {
5894
- const action = z19.enum(["enter", "resume", "drain"]).parse(positionals[1]);
6228
+ const action = z20.enum(["enter", "resume", "drain"]).parse(positionals[1]);
5895
6229
  const result = await controlRequest(dataDir, {
5896
6230
  command: "maintenance",
5897
6231
  action,
5898
- timeoutMs: z19.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
6232
+ timeoutMs: z20.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
5899
6233
  });
5900
6234
  console.log(JSON.stringify(result, null, 2));
5901
6235
  if (action === "drain" && "drained" in result && !result.drained) process.exitCode = 1;
@@ -5904,7 +6238,7 @@ async function maintenanceCommand({ values, positionals, dataDir }) {
5904
6238
  async function devicesCommand({ dataDir, command }) {
5905
6239
  console.log(
5906
6240
  JSON.stringify(
5907
- await controlRequest(dataDir, { command: z19.enum(["devices", "status"]).parse(command) }),
6241
+ await controlRequest(dataDir, { command: z20.enum(["devices", "status"]).parse(command) }),
5908
6242
  null,
5909
6243
  2
5910
6244
  )
@@ -5912,7 +6246,7 @@ async function devicesCommand({ dataDir, command }) {
5912
6246
  return;
5913
6247
  }
5914
6248
  async function revokeCommand({ positionals, dataDir }) {
5915
- const id = z19.string().uuid().parse(positionals[1]);
6249
+ const id = z20.string().uuid().parse(positionals[1]);
5916
6250
  console.log(JSON.stringify(await controlRequest(dataDir, { command: "revoke", id })));
5917
6251
  return;
5918
6252
  }