@agentrific/server 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +3 -2
- package/cli.js +804 -512
- package/cli.js.map +4 -4
- package/index.js +671 -164
- package/index.js.map +4 -4
- package/opentunnel-sdk.js +34715 -0
- package/opentunnel-sdk.js.map +7 -0
- package/package.json +1 -1
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.
|
|
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",
|
|
@@ -650,7 +651,7 @@ async function acquireServerLock(directory) {
|
|
|
650
651
|
await unlink2(filename);
|
|
651
652
|
};
|
|
652
653
|
}
|
|
653
|
-
async function startControl(directory, security2, status, maintenance2, shutdown) {
|
|
654
|
+
async function startControl(directory, security2, status, maintenance2, shutdown, phone2) {
|
|
654
655
|
const filename = controlSocketPath(directory);
|
|
655
656
|
const socketDirectory = path2.dirname(filename);
|
|
656
657
|
await mkdir2(socketDirectory, { recursive: true, mode: 448 });
|
|
@@ -677,6 +678,29 @@ async function startControl(directory, security2, status, maintenance2, shutdown
|
|
|
677
678
|
const request = ControlRequest.parse(JSON.parse(buffer.split("\n")[0]));
|
|
678
679
|
let result;
|
|
679
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;
|
|
680
704
|
case "shutdown":
|
|
681
705
|
if (!shutdown || !maintenance2?.status().drained)
|
|
682
706
|
throw new Error("Drain the server before shutdown");
|
|
@@ -746,12 +770,13 @@ function controlRequest(directory, command) {
|
|
|
746
770
|
});
|
|
747
771
|
});
|
|
748
772
|
}
|
|
749
|
-
var MaintenanceSchema, ControlStatusSchema, ControlResultSchema, ControlRequest;
|
|
773
|
+
var ProviderSchema, MaintenanceSchema, ControlStatusSchema, ControlResultSchema, ControlRequest;
|
|
750
774
|
var init_control = __esm({
|
|
751
775
|
"src/control.ts"() {
|
|
752
776
|
"use strict";
|
|
753
777
|
init_control_socket();
|
|
754
778
|
init_src();
|
|
779
|
+
ProviderSchema = z3.enum(["opentunnel", "tailscale", "cloudflared"]);
|
|
755
780
|
MaintenanceSchema = z3.object({
|
|
756
781
|
active: z3.boolean(),
|
|
757
782
|
drained: z3.boolean(),
|
|
@@ -760,6 +785,7 @@ var init_control = __esm({
|
|
|
760
785
|
ControlStatusSchema = z3.object({
|
|
761
786
|
serverId: z3.string().uuid(),
|
|
762
787
|
publicUrl: z3.string().optional(),
|
|
788
|
+
phoneProvider: ProviderSchema.nullable().optional(),
|
|
763
789
|
localUrl: z3.string().optional(),
|
|
764
790
|
serverVersion: z3.string().optional(),
|
|
765
791
|
connectedDevices: z3.number().optional(),
|
|
@@ -769,6 +795,8 @@ var init_control = __esm({
|
|
|
769
795
|
maintenance: MaintenanceSchema.optional()
|
|
770
796
|
});
|
|
771
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() })),
|
|
772
800
|
z3.object({
|
|
773
801
|
pairingId: z3.string(),
|
|
774
802
|
pin: z3.string(),
|
|
@@ -782,6 +810,9 @@ var init_control = __esm({
|
|
|
782
810
|
MaintenanceSchema
|
|
783
811
|
]);
|
|
784
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") }),
|
|
785
816
|
z3.object({ command: z3.literal("pair") }),
|
|
786
817
|
z3.object({ command: z3.literal("devices") }),
|
|
787
818
|
z3.object({ command: z3.literal("status") }),
|
|
@@ -855,6 +886,7 @@ function printHelp() {
|
|
|
855
886
|
for (const [option, description] of [
|
|
856
887
|
["--data-dir <path>", "Use a specific server data directory"],
|
|
857
888
|
["--public-url <https-url>", "Use your tunnel URL without prompting"],
|
|
889
|
+
["--connection <method>", "opentunnel (default), tailscale, cloudflared, existing, or local"],
|
|
858
890
|
["--no-interactive", "Disable prompts and keyboard shortcuts"],
|
|
859
891
|
["--show-payload", "Print manual JSON with the pairing QR code"],
|
|
860
892
|
["--timeout-ms <0-60000>", "Set the maintenance drain timeout"],
|
|
@@ -862,7 +894,9 @@ function printHelp() {
|
|
|
862
894
|
["--help", "Show this help"]
|
|
863
895
|
])
|
|
864
896
|
console.log(` ${styleText("cyan", option.padEnd(34))}${description}`);
|
|
865
|
-
console.log(
|
|
897
|
+
console.log(
|
|
898
|
+
"\n OpenTunnel is built in. Tailscale, cloudflared and existing HTTPS tunnels are also supported.\n"
|
|
899
|
+
);
|
|
866
900
|
}
|
|
867
901
|
var init_cli_output = __esm({
|
|
868
902
|
"src/cli-output.ts"() {
|
|
@@ -992,8 +1026,379 @@ var init_pairing = __esm({
|
|
|
992
1026
|
}
|
|
993
1027
|
});
|
|
994
1028
|
|
|
995
|
-
//
|
|
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";
|
|
996
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";
|
|
997
1402
|
function pushSigningText(installationId, operation, request) {
|
|
998
1403
|
return [
|
|
999
1404
|
"agentrific-push-v1",
|
|
@@ -1011,7 +1416,7 @@ var init_push = __esm({
|
|
|
1011
1416
|
"use strict";
|
|
1012
1417
|
PUSH_API_URL = "https://api.agentrific.com";
|
|
1013
1418
|
PUSH_SANDBOX_API_URL = "https://sandbox-api.agentrific.com";
|
|
1014
|
-
PushKindSchema =
|
|
1419
|
+
PushKindSchema = z7.enum([
|
|
1015
1420
|
"started",
|
|
1016
1421
|
"completed",
|
|
1017
1422
|
"needs_input",
|
|
@@ -1020,47 +1425,47 @@ var init_push = __esm({
|
|
|
1020
1425
|
"pr_created",
|
|
1021
1426
|
"failed"
|
|
1022
1427
|
]);
|
|
1023
|
-
PushPreferencesSchema =
|
|
1024
|
-
PushPublicKeySchema =
|
|
1025
|
-
PushInstallationSchema =
|
|
1026
|
-
PushRegistrationSchema =
|
|
1027
|
-
expoToken:
|
|
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_-]+\]$/),
|
|
1028
1433
|
preferences: PushPreferencesSchema
|
|
1029
1434
|
}).strict();
|
|
1030
|
-
PushEnrollmentSchema =
|
|
1435
|
+
PushEnrollmentSchema = z7.object({ serverId: z7.string().uuid(), publicKey: PushPublicKeySchema }).strict();
|
|
1031
1436
|
PushGrantSchema = PushEnrollmentSchema.extend({
|
|
1032
|
-
installationId:
|
|
1033
|
-
bindingId:
|
|
1034
|
-
grant:
|
|
1035
|
-
expiresAt:
|
|
1437
|
+
installationId: z7.string().uuid(),
|
|
1438
|
+
bindingId: z7.string().uuid(),
|
|
1439
|
+
grant: z7.string().regex(/^[a-f0-9]{64}$/),
|
|
1440
|
+
expiresAt: z7.number()
|
|
1036
1441
|
}).strict();
|
|
1037
|
-
PushPullRequestUrlSchema =
|
|
1038
|
-
PushEventSchema =
|
|
1039
|
-
eventId:
|
|
1040
|
-
sessionId:
|
|
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(),
|
|
1041
1446
|
kind: PushKindSchema,
|
|
1042
|
-
taskTitle:
|
|
1447
|
+
taskTitle: z7.string().trim().min(1).max(160).optional(),
|
|
1043
1448
|
prUrl: PushPullRequestUrlSchema.optional()
|
|
1044
1449
|
}).strict();
|
|
1045
|
-
PushSignedRequestSchema =
|
|
1046
|
-
bindingId:
|
|
1047
|
-
timestamp:
|
|
1048
|
-
nonce:
|
|
1049
|
-
payload:
|
|
1050
|
-
signature:
|
|
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}$/)
|
|
1051
1456
|
}).strict();
|
|
1052
|
-
PushEntitlementSchema =
|
|
1053
|
-
PushServerStatusSchema =
|
|
1457
|
+
PushEntitlementSchema = z7.object({ active: z7.boolean(), expiresAt: z7.number().nullable() }).strict();
|
|
1458
|
+
PushServerStatusSchema = z7.object({
|
|
1054
1459
|
publicKey: PushPublicKeySchema,
|
|
1055
|
-
bindings:
|
|
1056
|
-
|
|
1057
|
-
bindingId:
|
|
1058
|
-
installationId:
|
|
1059
|
-
deviceId:
|
|
1460
|
+
bindings: z7.array(
|
|
1461
|
+
z7.object({
|
|
1462
|
+
bindingId: z7.string().uuid(),
|
|
1463
|
+
installationId: z7.string().uuid(),
|
|
1464
|
+
deviceId: z7.string().uuid()
|
|
1060
1465
|
})
|
|
1061
1466
|
),
|
|
1062
|
-
pending:
|
|
1063
|
-
error:
|
|
1467
|
+
pending: z7.number().int().nonnegative(),
|
|
1468
|
+
error: z7.string().nullable()
|
|
1064
1469
|
});
|
|
1065
1470
|
}
|
|
1066
1471
|
});
|
|
@@ -1074,7 +1479,7 @@ import {
|
|
|
1074
1479
|
randomUUID
|
|
1075
1480
|
} from "node:crypto";
|
|
1076
1481
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1077
|
-
import
|
|
1482
|
+
import path6 from "node:path";
|
|
1078
1483
|
import { Effect as Effect3 } from "effect";
|
|
1079
1484
|
function pushKind(session, previous) {
|
|
1080
1485
|
if (session.gitDelivery?.status === "pr_opened" && previous?.gitDelivery?.status !== "pr_opened")
|
|
@@ -1134,7 +1539,7 @@ var init_push2 = __esm({
|
|
|
1134
1539
|
this.transport = transport;
|
|
1135
1540
|
this.apiUrl = apiUrl;
|
|
1136
1541
|
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
1137
|
-
const filename =
|
|
1542
|
+
const filename = path6.join(directory, "push-signing-key.pem");
|
|
1138
1543
|
if (!existsSync(filename))
|
|
1139
1544
|
writeFileSync(
|
|
1140
1545
|
filename,
|
|
@@ -1304,12 +1709,12 @@ var init_push2 = __esm({
|
|
|
1304
1709
|
// src/attachments.ts
|
|
1305
1710
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1306
1711
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
1307
|
-
import { mkdir as
|
|
1308
|
-
import
|
|
1712
|
+
import { mkdir as mkdir4, readFile as readFile3, rm, stat as stat2, readdir } from "node:fs/promises";
|
|
1713
|
+
import path7 from "node:path";
|
|
1309
1714
|
import { pipeline } from "node:stream/promises";
|
|
1310
1715
|
import sharp from "sharp";
|
|
1311
1716
|
import { Context, Data, Effect as Effect4, Layer } from "effect";
|
|
1312
|
-
import { z as
|
|
1717
|
+
import { z as z8 } from "zod";
|
|
1313
1718
|
var AttachmentError, textExtensions, imageMimes, AttachmentFiles, Attachments;
|
|
1314
1719
|
var init_attachments = __esm({
|
|
1315
1720
|
"src/attachments.ts"() {
|
|
@@ -1364,7 +1769,7 @@ var init_attachments = __esm({
|
|
|
1364
1769
|
root;
|
|
1365
1770
|
store;
|
|
1366
1771
|
directory(id) {
|
|
1367
|
-
return
|
|
1772
|
+
return path7.join(this.root, z8.string().uuid().parse(id));
|
|
1368
1773
|
}
|
|
1369
1774
|
io(operation) {
|
|
1370
1775
|
return Effect4.tryPromise({
|
|
@@ -1378,11 +1783,11 @@ var init_attachments = __esm({
|
|
|
1378
1783
|
return this.io(async () => {
|
|
1379
1784
|
const id = randomUUID2();
|
|
1380
1785
|
const directory = this.directory(id);
|
|
1381
|
-
const source =
|
|
1382
|
-
const nameField =
|
|
1786
|
+
const source = path7.join(directory, "source");
|
|
1787
|
+
const nameField = z8.object({ type: z8.literal("field"), value: z8.string() }).safeParse(part.fields.name);
|
|
1383
1788
|
const originalName = nameField.success ? nameField.data.value : part.filename;
|
|
1384
|
-
const name =
|
|
1385
|
-
await
|
|
1789
|
+
const name = path7.basename(originalName.replaceAll("\\", "/")).replace(new RegExp("\\p{Cc}", "gu"), "").slice(0, 180) || "attachment";
|
|
1790
|
+
await mkdir4(directory, { recursive: true });
|
|
1386
1791
|
try {
|
|
1387
1792
|
await pipeline(part.file, createWriteStream(source, { flags: "wx" }));
|
|
1388
1793
|
if (part.file.truncated) throw new Error("Files must be 20 MB or smaller.");
|
|
@@ -1409,14 +1814,14 @@ var init_attachments = __esm({
|
|
|
1409
1814
|
const metadata = await sharp(source, { limitInputPixels: 4e7 }).metadata().catch(() => null);
|
|
1410
1815
|
const mimeType = metadata?.format && imageMimes.get(metadata.format);
|
|
1411
1816
|
if (mimeType) {
|
|
1412
|
-
await sharp(source, { limitInputPixels: 4e7 }).rotate().resize({ width: 2048, height: 2048, fit: "inside", withoutEnlargement: true }).png().toFile(
|
|
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"));
|
|
1413
1818
|
return { kind: "image", mimeType };
|
|
1414
1819
|
}
|
|
1415
|
-
if (!textExtensions.has(
|
|
1820
|
+
if (!textExtensions.has(path7.extname(name).toLowerCase()))
|
|
1416
1821
|
throw new Error(
|
|
1417
1822
|
"Choose a PNG, JPEG, WebP, GIF, or text/code file. PDF and Office documents aren't supported yet."
|
|
1418
1823
|
);
|
|
1419
|
-
const bytes = await
|
|
1824
|
+
const bytes = await readFile3(source);
|
|
1420
1825
|
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1421
1826
|
if (new RegExp("\\p{Cc}", "u").test(text.replaceAll("\n", "").replaceAll("\r", "").replaceAll(" ", "")))
|
|
1422
1827
|
throw new Error("This file isn't readable UTF-8 text.");
|
|
@@ -1433,7 +1838,7 @@ var init_attachments = __esm({
|
|
|
1433
1838
|
throw new Error(
|
|
1434
1839
|
"An attachment is unavailable for this project. Remove it and upload it again."
|
|
1435
1840
|
);
|
|
1436
|
-
await stat2(
|
|
1841
|
+
await stat2(path7.join(this.directory(id), "source"));
|
|
1437
1842
|
attachments2.push(attachment);
|
|
1438
1843
|
}
|
|
1439
1844
|
return attachments2;
|
|
@@ -1441,7 +1846,7 @@ var init_attachments = __esm({
|
|
|
1441
1846
|
}
|
|
1442
1847
|
content(attachment, preview) {
|
|
1443
1848
|
return createReadStream(
|
|
1444
|
-
|
|
1849
|
+
path7.join(
|
|
1445
1850
|
this.directory(attachment.id),
|
|
1446
1851
|
preview && attachment.kind === "image" ? "preview.png" : "source"
|
|
1447
1852
|
)
|
|
@@ -1452,13 +1857,13 @@ var init_attachments = __esm({
|
|
|
1452
1857
|
const images = [];
|
|
1453
1858
|
const sections = [];
|
|
1454
1859
|
for (const attachment of attachments2) {
|
|
1455
|
-
const source =
|
|
1860
|
+
const source = path7.join(this.directory(attachment.id), "source");
|
|
1456
1861
|
if (attachment.kind === "image") {
|
|
1457
|
-
const bytes = await
|
|
1862
|
+
const bytes = await readFile3(path7.join(this.directory(attachment.id), "preview.png"));
|
|
1458
1863
|
images.push({ type: "image", data: bytes.toString("base64"), mimeType: "image/png" });
|
|
1459
1864
|
sections.push(`Image attachment: ${JSON.stringify(attachment.name)}`);
|
|
1460
1865
|
} else {
|
|
1461
|
-
const text = await
|
|
1866
|
+
const text = await readFile3(source, "utf8");
|
|
1462
1867
|
sections.push(
|
|
1463
1868
|
`File attachment: ${JSON.stringify(attachment.name)}
|
|
1464
1869
|
Full file available to the read tool at: ${JSON.stringify(source)}
|
|
@@ -1484,9 +1889,9 @@ ${text.slice(0, 12e3)}${text.length > 12e3 ? "\n[Preview truncated; use the read
|
|
|
1484
1889
|
if (await Effect4.runPromise(this.store.deleteAttachment(attachment.id)))
|
|
1485
1890
|
await rm(this.directory(attachment.id), { recursive: true, force: true });
|
|
1486
1891
|
}
|
|
1487
|
-
await
|
|
1892
|
+
await mkdir4(this.root, { recursive: true });
|
|
1488
1893
|
for (const entry of await readdir(this.root, { withFileTypes: true })) {
|
|
1489
|
-
if (!entry.isDirectory() || !
|
|
1894
|
+
if (!entry.isDirectory() || !z8.string().uuid().safeParse(entry.name).success) continue;
|
|
1490
1895
|
const directory = this.directory(entry.name);
|
|
1491
1896
|
if ((await stat2(directory)).mtimeMs < cutoff && !await Effect4.runPromise(this.store.getAttachment(entry.name)))
|
|
1492
1897
|
await rm(directory, { recursive: true, force: true });
|
|
@@ -1574,24 +1979,24 @@ var init_event_server = __esm({
|
|
|
1574
1979
|
});
|
|
1575
1980
|
|
|
1576
1981
|
// src/security.ts
|
|
1577
|
-
import { createHash as createHash2, 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";
|
|
1578
1983
|
import { Effect as Effect5 } from "effect";
|
|
1579
|
-
import { z as
|
|
1984
|
+
import { z as z9 } from "zod";
|
|
1580
1985
|
async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
1581
1986
|
app2.addHook("onRequest", async (request, reply) => {
|
|
1582
|
-
const
|
|
1987
|
+
const path15 = new URL(request.url, "http://localhost");
|
|
1583
1988
|
reply.header("Cache-Control", "no-store");
|
|
1584
1989
|
if (request.method === "OPTIONS") return;
|
|
1585
1990
|
if (request.headers.origin && !allowedOrigins2.includes(request.headers.origin))
|
|
1586
1991
|
return reply.code(403).send({ error: "Browser origin is not allowed." });
|
|
1587
|
-
if (
|
|
1992
|
+
if (path15.pathname === "/health" || path15.pathname === "/api/auth/pair" && request.method === "POST")
|
|
1588
1993
|
return;
|
|
1589
1994
|
const authorization = request.headers.authorization ?? "";
|
|
1590
1995
|
const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
|
|
1591
|
-
const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(
|
|
1592
|
-
const ticket =
|
|
1593
|
-
|
|
1594
|
-
if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket,
|
|
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)))
|
|
1595
2000
|
return reply.code(401).send({ error: "Pair this device with your server.", code: "UNAUTHORIZED" });
|
|
1596
2001
|
if (!asset && request.headers["x-agentrific-protocol"] !== String(PROTOCOL_VERSION))
|
|
1597
2002
|
return reply.code(409).send({
|
|
@@ -1646,8 +2051,8 @@ async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
|
1646
2051
|
ticket: security2.ticket((request.headers.authorization ?? "").slice(7), "/api/events")
|
|
1647
2052
|
}));
|
|
1648
2053
|
app2.post("/api/auth/asset-ticket", (request, reply) => {
|
|
1649
|
-
const input =
|
|
1650
|
-
path:
|
|
2054
|
+
const input = z9.object({
|
|
2055
|
+
path: z9.string().regex(/^\/api\/projects\/[^/?]+\/attachments\/[^/?]+\/content(?:\?preview=1)?$/)
|
|
1651
2056
|
}).safeParse(request.body);
|
|
1652
2057
|
if (!input.success) return reply.code(400).send({ error: "Choose a valid attachment." });
|
|
1653
2058
|
return {
|
|
@@ -1661,7 +2066,7 @@ var init_security = __esm({
|
|
|
1661
2066
|
"use strict";
|
|
1662
2067
|
init_src();
|
|
1663
2068
|
init_pairing();
|
|
1664
|
-
secret = () =>
|
|
2069
|
+
secret = () => randomBytes2(32).toString("hex");
|
|
1665
2070
|
digest = (value) => createHash2("sha256").update(value).digest("hex");
|
|
1666
2071
|
ServerSecurity = class {
|
|
1667
2072
|
constructor(store2, serverId) {
|
|
@@ -1724,17 +2129,17 @@ var init_security = __esm({
|
|
|
1724
2129
|
if (!/^[a-f0-9]{64}$/.test(token)) return void 0;
|
|
1725
2130
|
return Effect5.runSync(this.store.deviceForToken(digest(token)));
|
|
1726
2131
|
}
|
|
1727
|
-
ticket(token,
|
|
2132
|
+
ticket(token, path15) {
|
|
1728
2133
|
this.prune();
|
|
1729
2134
|
if (!this.authenticate(token) || this.tickets.size >= 1e4) return void 0;
|
|
1730
2135
|
const value = secret();
|
|
1731
|
-
this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path:
|
|
2136
|
+
this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path15 });
|
|
1732
2137
|
return value;
|
|
1733
2138
|
}
|
|
1734
|
-
redeemTicket(value,
|
|
2139
|
+
redeemTicket(value, path15, consume = true) {
|
|
1735
2140
|
this.prune();
|
|
1736
2141
|
const ticket = this.tickets.get(value);
|
|
1737
|
-
if (!ticket || ticket.path !==
|
|
2142
|
+
if (!ticket || ticket.path !== path15) return void 0;
|
|
1738
2143
|
if (consume) this.tickets.delete(value);
|
|
1739
2144
|
return Effect5.runSync(this.store.deviceForToken(ticket.tokenHash));
|
|
1740
2145
|
}
|
|
@@ -1748,6 +2153,101 @@ var init_security = __esm({
|
|
|
1748
2153
|
}
|
|
1749
2154
|
});
|
|
1750
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
|
+
|
|
1751
2251
|
// src/maintenance.ts
|
|
1752
2252
|
var MaintenanceError, Maintenance;
|
|
1753
2253
|
var init_maintenance = __esm({
|
|
@@ -1793,9 +2293,9 @@ var init_maintenance = __esm({
|
|
|
1793
2293
|
}
|
|
1794
2294
|
attach(app2) {
|
|
1795
2295
|
app2.addHook("onRequest", async (request, reply) => {
|
|
1796
|
-
const
|
|
2296
|
+
const path15 = request.url.split("?")[0];
|
|
1797
2297
|
if (["GET", "HEAD", "OPTIONS"].includes(request.method)) return;
|
|
1798
|
-
const allowed =
|
|
2298
|
+
const allowed = path15.startsWith("/api/auth/") || /^\/api\/sessions\/[^/]+\/stop$/.test(path15);
|
|
1799
2299
|
if (this.paused && !allowed)
|
|
1800
2300
|
return reply.code(503).send({ code: "MAINTENANCE", error: new MaintenanceError().message });
|
|
1801
2301
|
this.requests.add(request.id);
|
|
@@ -2069,7 +2569,7 @@ var init_git_flow = __esm({
|
|
|
2069
2569
|
});
|
|
2070
2570
|
|
|
2071
2571
|
// src/runtime.ts
|
|
2072
|
-
import
|
|
2572
|
+
import path8 from "node:path";
|
|
2073
2573
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
2074
2574
|
import { Context as Context2, Data as Data2, Effect as Effect8, Fiber, Layer as Layer2, Semaphore as Semaphore2 } from "effect";
|
|
2075
2575
|
var AgentError, AgentController, AgentSessions;
|
|
@@ -2128,8 +2628,8 @@ var init_runtime = __esm({
|
|
|
2128
2628
|
const agent = yield* Effect8.tryPromise({
|
|
2129
2629
|
try: () => this.factory.open(
|
|
2130
2630
|
session.id,
|
|
2131
|
-
session.workspacePath ??
|
|
2132
|
-
|
|
2631
|
+
session.workspacePath ?? path8.join(this.worktreesRoot, session.id),
|
|
2632
|
+
path8.join(this.sessionsRoot, session.id),
|
|
2133
2633
|
session.model,
|
|
2134
2634
|
session
|
|
2135
2635
|
),
|
|
@@ -2416,17 +2916,17 @@ var init_workflow_command = __esm({
|
|
|
2416
2916
|
});
|
|
2417
2917
|
|
|
2418
2918
|
// src/workflow-schema.ts
|
|
2419
|
-
import { z as
|
|
2919
|
+
import { z as z10 } from "zod";
|
|
2420
2920
|
var DeliveryJobSchema, PreparationJobSchema, WorkflowJobSchema;
|
|
2421
2921
|
var init_workflow_schema = __esm({
|
|
2422
2922
|
"src/workflow-schema.ts"() {
|
|
2423
2923
|
"use strict";
|
|
2424
2924
|
init_src();
|
|
2425
|
-
DeliveryJobSchema =
|
|
2426
|
-
kind:
|
|
2427
|
-
sessionId:
|
|
2925
|
+
DeliveryJobSchema = z10.object({
|
|
2926
|
+
kind: z10.literal("delivery"),
|
|
2927
|
+
sessionId: z10.string(),
|
|
2428
2928
|
request: DeliveryRequestSchema,
|
|
2429
|
-
phase:
|
|
2929
|
+
phase: z10.enum([
|
|
2430
2930
|
"validating",
|
|
2431
2931
|
"drafting",
|
|
2432
2932
|
"committing",
|
|
@@ -2436,25 +2936,25 @@ var init_workflow_schema = __esm({
|
|
|
2436
2936
|
"opening_pr",
|
|
2437
2937
|
"delivered"
|
|
2438
2938
|
]),
|
|
2439
|
-
attempt:
|
|
2440
|
-
commitSha:
|
|
2441
|
-
title:
|
|
2442
|
-
body:
|
|
2443
|
-
logs:
|
|
2444
|
-
failed:
|
|
2445
|
-
result:
|
|
2446
|
-
url:
|
|
2447
|
-
message:
|
|
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()
|
|
2448
2948
|
});
|
|
2449
|
-
PreparationJobSchema =
|
|
2450
|
-
kind:
|
|
2451
|
-
sessionId:
|
|
2452
|
-
projectPath:
|
|
2949
|
+
PreparationJobSchema = z10.object({
|
|
2950
|
+
kind: z10.literal("preparation"),
|
|
2951
|
+
sessionId: z10.string(),
|
|
2952
|
+
projectPath: z10.string(),
|
|
2453
2953
|
input: CreateSessionSchema,
|
|
2454
|
-
stage:
|
|
2455
|
-
logs:
|
|
2954
|
+
stage: z10.enum(["creating", "installing", "ready", "failed"]),
|
|
2955
|
+
logs: z10.string().default("")
|
|
2456
2956
|
});
|
|
2457
|
-
WorkflowJobSchema =
|
|
2957
|
+
WorkflowJobSchema = z10.discriminatedUnion("kind", [
|
|
2458
2958
|
DeliveryJobSchema,
|
|
2459
2959
|
PreparationJobSchema
|
|
2460
2960
|
]);
|
|
@@ -2463,7 +2963,7 @@ var init_workflow_schema = __esm({
|
|
|
2463
2963
|
|
|
2464
2964
|
// src/delivery.ts
|
|
2465
2965
|
import { Effect as Effect9 } from "effect";
|
|
2466
|
-
import { z as
|
|
2966
|
+
import { z as z11 } from "zod";
|
|
2467
2967
|
var DeliveryCoordinator;
|
|
2468
2968
|
var init_delivery = __esm({
|
|
2469
2969
|
"src/delivery.ts"() {
|
|
@@ -2762,11 +3262,11 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2762
3262
|
"--json",
|
|
2763
3263
|
"url,state,headRefOid"
|
|
2764
3264
|
]);
|
|
2765
|
-
const prs =
|
|
2766
|
-
|
|
2767
|
-
url:
|
|
2768
|
-
state:
|
|
2769
|
-
headRefOid:
|
|
3265
|
+
const prs = z11.array(
|
|
3266
|
+
z11.object({
|
|
3267
|
+
url: z11.string().url(),
|
|
3268
|
+
state: z11.string(),
|
|
3269
|
+
headRefOid: z11.string().optional()
|
|
2770
3270
|
})
|
|
2771
3271
|
).parse(JSON.parse(existing));
|
|
2772
3272
|
const open = prs.find(
|
|
@@ -2809,10 +3309,10 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2809
3309
|
});
|
|
2810
3310
|
|
|
2811
3311
|
// src/preparation.ts
|
|
2812
|
-
import { mkdir as
|
|
2813
|
-
import
|
|
3312
|
+
import { mkdir as mkdir5, readFile as readFile4 } from "node:fs/promises";
|
|
3313
|
+
import path9 from "node:path";
|
|
2814
3314
|
import { Effect as Effect10 } from "effect";
|
|
2815
|
-
import { z as
|
|
3315
|
+
import { z as z12 } from "zod";
|
|
2816
3316
|
var PreparationCoordinator;
|
|
2817
3317
|
var init_preparation = __esm({
|
|
2818
3318
|
"src/preparation.ts"() {
|
|
@@ -2913,7 +3413,7 @@ ${logs}` : ""}`);
|
|
|
2913
3413
|
const workspace = session.workspacePath;
|
|
2914
3414
|
if (branchAtWorkspace !== void 0)
|
|
2915
3415
|
throw new Error("The run workspace already exists on a different branch.");
|
|
2916
|
-
await
|
|
3416
|
+
await mkdir5(path9.dirname(workspace), { recursive: true });
|
|
2917
3417
|
try {
|
|
2918
3418
|
await command(
|
|
2919
3419
|
projectPath,
|
|
@@ -2933,12 +3433,12 @@ ${logs}` : ""}`);
|
|
|
2933
3433
|
}
|
|
2934
3434
|
}
|
|
2935
3435
|
async install(workspace, command) {
|
|
2936
|
-
const packageFile = await
|
|
3436
|
+
const packageFile = await readFile4(path9.join(workspace, "package.json"), "utf8").catch(
|
|
2937
3437
|
() => void 0
|
|
2938
3438
|
);
|
|
2939
3439
|
if (!packageFile) return;
|
|
2940
|
-
const metadata =
|
|
2941
|
-
const has = async (name) =>
|
|
3440
|
+
const metadata = z12.object({ packageManager: z12.string().optional() }).parse(JSON.parse(packageFile));
|
|
3441
|
+
const has = async (name) => readFile4(path9.join(workspace, name)).then(
|
|
2942
3442
|
() => true,
|
|
2943
3443
|
() => false
|
|
2944
3444
|
);
|
|
@@ -3021,7 +3521,7 @@ ${logs}` : ""}`);
|
|
|
3021
3521
|
|
|
3022
3522
|
// src/delivery-writer.ts
|
|
3023
3523
|
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
3024
|
-
import { z as
|
|
3524
|
+
import { z as z13 } from "zod";
|
|
3025
3525
|
function deliveryWriter(models2, createSession = createAgentSession) {
|
|
3026
3526
|
return async (run, input, diff, signal) => {
|
|
3027
3527
|
signal?.throwIfAborted();
|
|
@@ -3062,7 +3562,7 @@ var DraftSchema;
|
|
|
3062
3562
|
var init_delivery_writer = __esm({
|
|
3063
3563
|
"src/delivery-writer.ts"() {
|
|
3064
3564
|
"use strict";
|
|
3065
|
-
DraftSchema =
|
|
3565
|
+
DraftSchema = z13.object({ title: z13.string().trim().min(1).max(120), body: z13.string().trim().min(1).max(1e4) }).strict();
|
|
3066
3566
|
}
|
|
3067
3567
|
});
|
|
3068
3568
|
|
|
@@ -3219,19 +3719,19 @@ var init_delivery_tool = __esm({
|
|
|
3219
3719
|
// src/file-change.ts
|
|
3220
3720
|
import { createWriteToolDefinition, defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
|
|
3221
3721
|
import { createTwoFilesPatch } from "diff";
|
|
3222
|
-
import { mkdir as
|
|
3223
|
-
import { z as
|
|
3722
|
+
import { mkdir as mkdir6, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
|
|
3723
|
+
import { z as z14 } from "zod";
|
|
3224
3724
|
function boundFileChange(change) {
|
|
3225
3725
|
if ((change.patch?.length ?? 0) + (change.content?.length ?? 0) > previewLimit)
|
|
3226
3726
|
return { path: change.path, omitted: true };
|
|
3227
3727
|
return change;
|
|
3228
3728
|
}
|
|
3229
|
-
function completedFileChange(result,
|
|
3729
|
+
function completedFileChange(result, path15) {
|
|
3230
3730
|
const parsed = EditResultSchema.safeParse(result);
|
|
3231
3731
|
if (!parsed.success) return;
|
|
3232
3732
|
const details = parsed.data.details;
|
|
3233
3733
|
if (details?.fileChange) return boundFileChange(details.fileChange);
|
|
3234
|
-
if (
|
|
3734
|
+
if (path15 && details?.patch) return boundFileChange({ path: path15, patch: details.patch });
|
|
3235
3735
|
}
|
|
3236
3736
|
function fileWriteTool(cwd) {
|
|
3237
3737
|
const base = createWriteToolDefinition(cwd);
|
|
@@ -3248,16 +3748,16 @@ function fileWriteTool(cwd) {
|
|
|
3248
3748
|
const tool = createWriteToolDefinition(cwd, {
|
|
3249
3749
|
operations: {
|
|
3250
3750
|
mkdir: async (dir) => {
|
|
3251
|
-
await
|
|
3751
|
+
await mkdir6(dir, { recursive: true });
|
|
3252
3752
|
},
|
|
3253
3753
|
// Pi invokes this inside its per-file mutation queue, so the snapshot
|
|
3254
3754
|
// and write cannot race another built-in edit/write on the same file.
|
|
3255
|
-
writeFile: async (
|
|
3256
|
-
const previous = await
|
|
3755
|
+
writeFile: async (path15, content) => {
|
|
3756
|
+
const previous = await readFile5(path15, "utf8").catch((error) => {
|
|
3257
3757
|
if (error.code === "ENOENT") return void 0;
|
|
3258
3758
|
throw error;
|
|
3259
3759
|
});
|
|
3260
|
-
await
|
|
3760
|
+
await writeFile4(path15, content, "utf8");
|
|
3261
3761
|
fileChange = boundFileChange(
|
|
3262
3762
|
previous === void 0 ? { path: args.path, content } : {
|
|
3263
3763
|
path: args.path,
|
|
@@ -3278,8 +3778,8 @@ var init_file_change = __esm({
|
|
|
3278
3778
|
"use strict";
|
|
3279
3779
|
init_src();
|
|
3280
3780
|
previewLimit = 256e3;
|
|
3281
|
-
EditResultSchema =
|
|
3282
|
-
details:
|
|
3781
|
+
EditResultSchema = z14.object({
|
|
3782
|
+
details: z14.object({ patch: z14.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
|
|
3283
3783
|
});
|
|
3284
3784
|
}
|
|
3285
3785
|
});
|
|
@@ -3291,7 +3791,7 @@ import {
|
|
|
3291
3791
|
createAgentSession as createAgentSession2,
|
|
3292
3792
|
SessionManager as SessionManager2
|
|
3293
3793
|
} from "@earendil-works/pi-coding-agent";
|
|
3294
|
-
import { z as
|
|
3794
|
+
import { z as z15 } from "zod";
|
|
3295
3795
|
function toolOutput(value) {
|
|
3296
3796
|
const parsed = ToolResultSchema.safeParse(value);
|
|
3297
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);
|
|
@@ -3405,7 +3905,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
3405
3905
|
return false;
|
|
3406
3906
|
}
|
|
3407
3907
|
const filePaths = /* @__PURE__ */ new Map();
|
|
3408
|
-
const FileArgsSchema =
|
|
3908
|
+
const FileArgsSchema = z15.object({ path: z15.string() });
|
|
3409
3909
|
function handleTool(event) {
|
|
3410
3910
|
if (event.type === "tool_execution_start") {
|
|
3411
3911
|
flushText();
|
|
@@ -3470,8 +3970,8 @@ var init_pi = __esm({
|
|
|
3470
3970
|
init_models();
|
|
3471
3971
|
init_delivery_tool();
|
|
3472
3972
|
init_file_change();
|
|
3473
|
-
ToolResultSchema =
|
|
3474
|
-
content:
|
|
3973
|
+
ToolResultSchema = z15.object({
|
|
3974
|
+
content: z15.array(z15.object({ type: z15.string(), text: z15.string().optional() })).optional()
|
|
3475
3975
|
}).passthrough();
|
|
3476
3976
|
boundedJson = (value, limit) => {
|
|
3477
3977
|
let text;
|
|
@@ -3486,19 +3986,19 @@ var init_pi = __esm({
|
|
|
3486
3986
|
});
|
|
3487
3987
|
|
|
3488
3988
|
// src/projects.ts
|
|
3489
|
-
import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as
|
|
3989
|
+
import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
|
|
3490
3990
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
3491
|
-
import { z as
|
|
3991
|
+
import { z as z16 } from "zod";
|
|
3492
3992
|
import { constants as constants2 } from "node:fs";
|
|
3493
3993
|
import { homedir as homedir3 } from "node:os";
|
|
3494
|
-
import
|
|
3994
|
+
import path10 from "node:path";
|
|
3495
3995
|
import { Effect as Effect13, Semaphore as Semaphore3 } from "effect";
|
|
3496
3996
|
function resolveProjectsRoot(input) {
|
|
3497
3997
|
return Effect13.tryPromise({
|
|
3498
3998
|
try: async () => {
|
|
3499
3999
|
const trimmed = input.trim();
|
|
3500
|
-
const expanded = trimmed === "~" ? homedir3() : trimmed.startsWith("~/") ?
|
|
3501
|
-
if (!
|
|
4000
|
+
const expanded = trimmed === "~" ? homedir3() : trimmed.startsWith("~/") ? path10.join(homedir3(), trimmed.slice(2)) : trimmed;
|
|
4001
|
+
if (!path10.isAbsolute(expanded))
|
|
3502
4002
|
throw new Error("Use an absolute server path or a path beginning with ~/.");
|
|
3503
4003
|
const resolved = await realpath(expanded);
|
|
3504
4004
|
if (!(await stat3(resolved)).isDirectory())
|
|
@@ -3520,34 +4020,34 @@ function projectLock(store2) {
|
|
|
3520
4020
|
return lock;
|
|
3521
4021
|
}
|
|
3522
4022
|
async function gitDirectory(directory) {
|
|
3523
|
-
const location =
|
|
4023
|
+
const location = path10.join(directory, ".git");
|
|
3524
4024
|
const info = await stat3(location);
|
|
3525
4025
|
if (info.isDirectory()) return location;
|
|
3526
|
-
const contents = await
|
|
4026
|
+
const contents = await readFile6(location, "utf8");
|
|
3527
4027
|
const match = /^gitdir: (.+)\s*$/m.exec(contents);
|
|
3528
4028
|
if (!match) throw new Error("Invalid Git worktree metadata.");
|
|
3529
|
-
const resolved = await realpath(
|
|
4029
|
+
const resolved = await realpath(path10.resolve(directory, match[1].trim()));
|
|
3530
4030
|
if (!(await stat3(resolved)).isDirectory()) throw new Error("Git directory is missing.");
|
|
3531
4031
|
return resolved;
|
|
3532
4032
|
}
|
|
3533
4033
|
async function identity(directory, preferred) {
|
|
3534
|
-
const filename =
|
|
4034
|
+
const filename = path10.join(await gitDirectory(directory), "agentrific-project-id");
|
|
3535
4035
|
try {
|
|
3536
|
-
return ProjectId.parse(await
|
|
4036
|
+
return ProjectId.parse(await readFile6(filename, "utf8"));
|
|
3537
4037
|
} catch (error) {
|
|
3538
|
-
if (!
|
|
3539
|
-
const legacyFilename =
|
|
3540
|
-
const legacy = await
|
|
3541
|
-
if (!
|
|
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;
|
|
3542
4042
|
return null;
|
|
3543
4043
|
});
|
|
3544
4044
|
const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
|
|
3545
4045
|
try {
|
|
3546
|
-
await
|
|
4046
|
+
await writeFile5(filename, id + "\n", { flag: "wx" });
|
|
3547
4047
|
return id;
|
|
3548
4048
|
} catch (writeError) {
|
|
3549
|
-
if (!
|
|
3550
|
-
return ProjectId.parse(await
|
|
4049
|
+
if (!z16.object({ code: z16.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
|
|
4050
|
+
return ProjectId.parse(await readFile6(filename, "utf8"));
|
|
3551
4051
|
}
|
|
3552
4052
|
}
|
|
3553
4053
|
}
|
|
@@ -3560,15 +4060,15 @@ function scanProjects(root, store2) {
|
|
|
3560
4060
|
const entries = await readdir2(root, { withFileTypes: true });
|
|
3561
4061
|
for (const entry of entries) {
|
|
3562
4062
|
if (!entry.isDirectory()) continue;
|
|
3563
|
-
const directory =
|
|
3564
|
-
if (!await stat3(
|
|
4063
|
+
const directory = path10.join(root, entry.name);
|
|
4064
|
+
if (!await stat3(path10.join(directory, ".git")).catch(() => null)) continue;
|
|
3565
4065
|
const previous = registered.find((project2) => project2.path === directory);
|
|
3566
4066
|
let id = await identity(directory, previous?.available ? void 0 : previous?.id);
|
|
3567
4067
|
let existing = registered.find((project2) => project2.id === id);
|
|
3568
|
-
if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat3(
|
|
4068
|
+
if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat3(path10.join(existing.path, ".git")).catch(() => null)) {
|
|
3569
4069
|
id = randomUUID6();
|
|
3570
|
-
await
|
|
3571
|
-
|
|
4070
|
+
await writeFile5(
|
|
4071
|
+
path10.join(await gitDirectory(directory), "agentrific-project-id"),
|
|
3572
4072
|
id + "\n"
|
|
3573
4073
|
);
|
|
3574
4074
|
existing = void 0;
|
|
@@ -3601,16 +4101,16 @@ function reconnectProject(root, id, input, store2) {
|
|
|
3601
4101
|
))
|
|
3602
4102
|
throw new Error("Stop this project's running sessions before reconnecting its folder.");
|
|
3603
4103
|
const directory = await Effect13.runPromise(resolveProjectsRoot(input));
|
|
3604
|
-
if (
|
|
4104
|
+
if (path10.dirname(directory) !== await realpath(root))
|
|
3605
4105
|
throw new Error("Choose a Git repository directly inside the configured projects folder.");
|
|
3606
|
-
const filename =
|
|
4106
|
+
const filename = path10.join(await gitDirectory(directory), "agentrific-project-id");
|
|
3607
4107
|
const replacedId = await identity(directory);
|
|
3608
|
-
const project = { id, name:
|
|
3609
|
-
await
|
|
4108
|
+
const project = { id, name: path10.basename(directory), path: directory, available: true };
|
|
4109
|
+
await writeFile5(filename, id + "\n");
|
|
3610
4110
|
try {
|
|
3611
4111
|
await Effect13.runPromise(store2.relocateProject(project, previous.path, replacedId));
|
|
3612
4112
|
} catch (error) {
|
|
3613
|
-
await
|
|
4113
|
+
await writeFile5(filename, replacedId + "\n");
|
|
3614
4114
|
throw error;
|
|
3615
4115
|
}
|
|
3616
4116
|
return project;
|
|
@@ -3623,7 +4123,7 @@ var init_projects = __esm({
|
|
|
3623
4123
|
"src/projects.ts"() {
|
|
3624
4124
|
"use strict";
|
|
3625
4125
|
locks2 = /* @__PURE__ */ new WeakMap();
|
|
3626
|
-
ProjectId =
|
|
4126
|
+
ProjectId = z16.string().trim().min(1).max(255);
|
|
3627
4127
|
}
|
|
3628
4128
|
});
|
|
3629
4129
|
|
|
@@ -3643,10 +4143,10 @@ function repositoryIdentity(remote) {
|
|
|
3643
4143
|
if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return null;
|
|
3644
4144
|
if (url.search || url.hash || url.port) return null;
|
|
3645
4145
|
const host2 = url.hostname.toLowerCase();
|
|
3646
|
-
let
|
|
3647
|
-
if (!host2 || !
|
|
3648
|
-
if (["github.com", "bitbucket.org"].includes(host2))
|
|
3649
|
-
const key = `${host2}/${
|
|
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}`;
|
|
3650
4150
|
return { key, display: key };
|
|
3651
4151
|
}
|
|
3652
4152
|
async function git(directory, args) {
|
|
@@ -3735,36 +4235,36 @@ var init_project_git = __esm({
|
|
|
3735
4235
|
});
|
|
3736
4236
|
|
|
3737
4237
|
// src/push-schema.ts
|
|
3738
|
-
import { z as
|
|
4238
|
+
import { z as z17 } from "zod";
|
|
3739
4239
|
var PushBindingSchema, PushStateSchema, PendingPushSchema;
|
|
3740
4240
|
var init_push_schema = __esm({
|
|
3741
4241
|
"src/push-schema.ts"() {
|
|
3742
4242
|
"use strict";
|
|
3743
4243
|
init_push();
|
|
3744
|
-
PushBindingSchema =
|
|
3745
|
-
bindingId:
|
|
3746
|
-
installationId:
|
|
3747
|
-
deviceId:
|
|
4244
|
+
PushBindingSchema = z17.object({
|
|
4245
|
+
bindingId: z17.string().uuid(),
|
|
4246
|
+
installationId: z17.string().uuid(),
|
|
4247
|
+
deviceId: z17.string().uuid()
|
|
3748
4248
|
});
|
|
3749
|
-
PushStateSchema =
|
|
3750
|
-
bindings:
|
|
3751
|
-
error:
|
|
4249
|
+
PushStateSchema = z17.object({
|
|
4250
|
+
bindings: z17.array(PushBindingSchema).max(10),
|
|
4251
|
+
error: z17.string().nullable()
|
|
3752
4252
|
});
|
|
3753
4253
|
PendingPushSchema = PushEventSchema.extend({
|
|
3754
|
-
remaining:
|
|
3755
|
-
createdAt:
|
|
3756
|
-
attempts:
|
|
3757
|
-
nextAt:
|
|
4254
|
+
remaining: z17.array(PushBindingSchema),
|
|
4255
|
+
createdAt: z17.number(),
|
|
4256
|
+
attempts: z17.number().int().nonnegative(),
|
|
4257
|
+
nextAt: z17.number()
|
|
3758
4258
|
});
|
|
3759
4259
|
}
|
|
3760
4260
|
});
|
|
3761
4261
|
|
|
3762
4262
|
// src/store.ts
|
|
3763
4263
|
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
3764
|
-
import
|
|
4264
|
+
import path11 from "node:path";
|
|
3765
4265
|
import { DatabaseSync } from "node:sqlite";
|
|
3766
4266
|
import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
|
|
3767
|
-
import { z as
|
|
4267
|
+
import { z as z18 } from "zod";
|
|
3768
4268
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
3769
4269
|
var StorageError, PayloadRow, EventRow, SqliteSessionStore, SessionStore;
|
|
3770
4270
|
var init_store = __esm({
|
|
@@ -3777,8 +4277,8 @@ var init_store = __esm({
|
|
|
3777
4277
|
init_src();
|
|
3778
4278
|
StorageError = class extends Data3.TaggedError("StorageError") {
|
|
3779
4279
|
};
|
|
3780
|
-
PayloadRow =
|
|
3781
|
-
EventRow =
|
|
4280
|
+
PayloadRow = z18.object({ payload: z18.string() });
|
|
4281
|
+
EventRow = z18.object({ sequence: z18.number(), recorded_at: z18.string(), payload: z18.string() });
|
|
3782
4282
|
SqliteSessionStore = class {
|
|
3783
4283
|
constructor(db) {
|
|
3784
4284
|
this.db = db;
|
|
@@ -3829,7 +4329,7 @@ var init_store = __esm({
|
|
|
3829
4329
|
const row = PayloadRow.parse(
|
|
3830
4330
|
this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
|
|
3831
4331
|
);
|
|
3832
|
-
return
|
|
4332
|
+
return z18.string().uuid().parse(JSON.parse(row.payload));
|
|
3833
4333
|
});
|
|
3834
4334
|
}
|
|
3835
4335
|
saveDevice(device, tokenHash) {
|
|
@@ -3917,10 +4417,10 @@ var init_store = __esm({
|
|
|
3917
4417
|
`).all();
|
|
3918
4418
|
const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
|
|
3919
4419
|
for (const row of legacy) {
|
|
3920
|
-
const id =
|
|
4420
|
+
const id = z18.object({ id: z18.string() }).parse(row).id;
|
|
3921
4421
|
insert.run(
|
|
3922
4422
|
id,
|
|
3923
|
-
JSON.stringify({ id, name: id, path:
|
|
4423
|
+
JSON.stringify({ id, name: id, path: path11.join(root, id), available: false })
|
|
3924
4424
|
);
|
|
3925
4425
|
}
|
|
3926
4426
|
return this.db.prepare("SELECT payload FROM projects").all().map((row) => ProjectSchema.parse(JSON.parse(PayloadRow.parse(row).payload)));
|
|
@@ -3957,7 +4457,7 @@ var init_store = __esm({
|
|
|
3957
4457
|
this.db.prepare(
|
|
3958
4458
|
"UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
|
|
3959
4459
|
).run(project.path, previousPath);
|
|
3960
|
-
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${
|
|
4460
|
+
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path11.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
|
|
3961
4461
|
for (const row of this.db.prepare("SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?").all(project.id)) {
|
|
3962
4462
|
const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
|
|
3963
4463
|
if (session.workspacePath) {
|
|
@@ -4127,7 +4627,7 @@ var init_store = __esm({
|
|
|
4127
4627
|
if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
|
|
4128
4628
|
const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
|
|
4129
4629
|
if (notification) {
|
|
4130
|
-
const count =
|
|
4630
|
+
const count = z18.object({ total: z18.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
|
|
4131
4631
|
if (count.total >= 1e3)
|
|
4132
4632
|
this.db.prepare(
|
|
4133
4633
|
"DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
|
|
@@ -4173,13 +4673,13 @@ var init_store = __esm({
|
|
|
4173
4673
|
Effect14.acquireRelease(
|
|
4174
4674
|
Effect14.try({
|
|
4175
4675
|
try: () => {
|
|
4176
|
-
mkdirSync2(
|
|
4676
|
+
mkdirSync2(path11.dirname(filename), { recursive: true });
|
|
4177
4677
|
const db = new DatabaseSync(filename);
|
|
4178
4678
|
chmodSync(filename, 384);
|
|
4179
4679
|
for (const suffix of ["-wal", "-shm"])
|
|
4180
4680
|
if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
|
|
4181
4681
|
try {
|
|
4182
|
-
const schema =
|
|
4682
|
+
const schema = z18.object({ user_version: z18.number() }).parse(db.prepare("PRAGMA user_version").get());
|
|
4183
4683
|
if (schema.user_version > 1)
|
|
4184
4684
|
throw new Error("This database requires a newer Agentrific server.");
|
|
4185
4685
|
db.exec(`
|
|
@@ -4216,12 +4716,12 @@ var init_store = __esm({
|
|
|
4216
4716
|
|
|
4217
4717
|
// src/worktree-cleanup.ts
|
|
4218
4718
|
import { lstat as lstat2, rm as rm2 } from "node:fs/promises";
|
|
4219
|
-
import
|
|
4719
|
+
import path12 from "node:path";
|
|
4220
4720
|
import { Effect as Effect15 } from "effect";
|
|
4221
4721
|
async function removeRunFiles(dataRoot2, sessionId) {
|
|
4222
|
-
if (
|
|
4722
|
+
if (path12.basename(sessionId) !== sessionId || sessionId === "." || sessionId === "..")
|
|
4223
4723
|
throw new Error("Invalid session directory.");
|
|
4224
|
-
await rm2(
|
|
4724
|
+
await rm2(path12.join(dataRoot2, "pi", sessionId), { recursive: true, force: true });
|
|
4225
4725
|
}
|
|
4226
4726
|
var WorktreeCleanup;
|
|
4227
4727
|
var init_worktree_cleanup = __esm({
|
|
@@ -4240,12 +4740,12 @@ var init_worktree_cleanup = __esm({
|
|
|
4240
4740
|
projectsRoot;
|
|
4241
4741
|
async remove(session, discardChanges = true) {
|
|
4242
4742
|
if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
|
|
4243
|
-
const workspace =
|
|
4743
|
+
const workspace = path12.resolve(session.workspacePath);
|
|
4244
4744
|
const info = await lstat2(workspace).catch((error) => {
|
|
4245
4745
|
if (error.code === "ENOENT") return void 0;
|
|
4246
4746
|
throw error;
|
|
4247
4747
|
});
|
|
4248
|
-
if (workspace !==
|
|
4748
|
+
if (workspace !== path12.join(this.root, session.id) || path12.basename(session.id) !== session.id) {
|
|
4249
4749
|
if (!info) return;
|
|
4250
4750
|
throw new Error("Refusing to remove a worktree outside the managed workspaces directory.");
|
|
4251
4751
|
}
|
|
@@ -4335,10 +4835,10 @@ import { execFile as execFile4 } from "node:child_process";
|
|
|
4335
4835
|
import { promisify as promisify4 } from "node:util";
|
|
4336
4836
|
import { realpath as realpath2 } from "node:fs/promises";
|
|
4337
4837
|
import { homedir as homedir4 } from "node:os";
|
|
4338
|
-
import
|
|
4838
|
+
import path13 from "node:path";
|
|
4339
4839
|
import { WebSocket } from "ws";
|
|
4340
4840
|
import { Effect as Effect17, ManagedRuntime } from "effect";
|
|
4341
|
-
import { z as
|
|
4841
|
+
import { z as z19 } from "zod";
|
|
4342
4842
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
4343
4843
|
function publish(event, workflowUpdate = false) {
|
|
4344
4844
|
const previous = event.type === "session.updated" ? Effect17.runSync(store.get(event.session.id)) : void 0;
|
|
@@ -4399,18 +4899,18 @@ async function resolveSessionProject(projectId) {
|
|
|
4399
4899
|
return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
|
|
4400
4900
|
const projectPath = await realpath2(project.path);
|
|
4401
4901
|
const resolvedRoot = await realpath2(projectsRoot);
|
|
4402
|
-
if (
|
|
4902
|
+
if (path13.dirname(projectPath) !== resolvedRoot)
|
|
4403
4903
|
return { error: "Project path is outside projects root.", status: 400 };
|
|
4404
4904
|
return { projectPath };
|
|
4405
4905
|
}
|
|
4406
4906
|
async function resolveSessionWorkspace(id, gitFlow, projectPath) {
|
|
4407
|
-
const worktreePath =
|
|
4907
|
+
const worktreePath = path13.join(worktreesRoot, id);
|
|
4408
4908
|
const branch = gitFlow ? `agentrific/${id.slice(0, 8)}` : null;
|
|
4409
4909
|
const useProjectCheckout = gitFlow?.workspace === "checkout";
|
|
4410
4910
|
const workspacePath = !gitFlow || useProjectCheckout ? projectPath : worktreePath;
|
|
4411
4911
|
if (useProjectCheckout) {
|
|
4412
4912
|
const serverPath = await realpath2(process.cwd());
|
|
4413
|
-
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${
|
|
4913
|
+
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path13.sep}`))
|
|
4414
4914
|
return {
|
|
4415
4915
|
error: "The server runs from this project. Choose a worktree for this task.",
|
|
4416
4916
|
status: 409
|
|
@@ -4546,10 +5046,10 @@ async function withSessionTaskLock(id, operation) {
|
|
|
4546
5046
|
return withTaskLock(lockId, operation);
|
|
4547
5047
|
}
|
|
4548
5048
|
function localServerUrl() {
|
|
4549
|
-
const address =
|
|
5049
|
+
const address = z19.object({ port: z19.number() }).safeParse(app.server.address());
|
|
4550
5050
|
return address.success ? `http://${host}:${address.data.port}` : void 0;
|
|
4551
5051
|
}
|
|
4552
|
-
var host, port, defaultProjectsRoot, worktreesRoot, execFileAsync, app, dataRoot, releaseLock, effects, store, security, sandboxPushUrl, testPushUrl, push, allowedOrigins, attachmentEffects, attachments, attachmentCleanup, models, delivery, preparation, agentEffects, runtime, worktreeCleanup, maxWorktreeAgeDays, pruneWorktrees, worktreeCleanupTimer, updates, maintenance, stopUpdateDiscovery, HistoryQuery, StartTaskBody, automation, automationTimer, closeControl;
|
|
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;
|
|
4553
5053
|
var init_index = __esm({
|
|
4554
5054
|
async "src/index.ts"() {
|
|
4555
5055
|
"use strict";
|
|
@@ -4560,6 +5060,7 @@ var init_index = __esm({
|
|
|
4560
5060
|
init_event_server();
|
|
4561
5061
|
init_security();
|
|
4562
5062
|
init_control();
|
|
5063
|
+
init_desktop_phone();
|
|
4563
5064
|
init_version();
|
|
4564
5065
|
init_maintenance();
|
|
4565
5066
|
init_updates();
|
|
@@ -4579,9 +5080,9 @@ var init_index = __esm({
|
|
|
4579
5080
|
init_repo_chat();
|
|
4580
5081
|
host = process.env.AGENTRIFIC_HOST ?? "127.0.0.1";
|
|
4581
5082
|
port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
|
|
4582
|
-
defaultProjectsRoot =
|
|
4583
|
-
worktreesRoot =
|
|
4584
|
-
process.env.AGENTRIFIC_WORKTREES_DIR ??
|
|
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")
|
|
4585
5086
|
);
|
|
4586
5087
|
execFileAsync = promisify4(execFile4);
|
|
4587
5088
|
if (host !== "127.0.0.1" && host !== "::1")
|
|
@@ -4604,9 +5105,9 @@ var init_index = __esm({
|
|
|
4604
5105
|
}
|
|
4605
5106
|
}
|
|
4606
5107
|
});
|
|
4607
|
-
dataRoot =
|
|
5108
|
+
dataRoot = path13.resolve(process.env.AGENTRIFIC_DATA_DIR ?? "./data");
|
|
4608
5109
|
releaseLock = await acquireServerLock(dataRoot);
|
|
4609
|
-
effects = ManagedRuntime.make(SessionStore.layer(
|
|
5110
|
+
effects = ManagedRuntime.make(SessionStore.layer(path13.join(dataRoot, "sessions.sqlite")));
|
|
4610
5111
|
store = await effects.runPromise(SessionStore);
|
|
4611
5112
|
security = new ServerSecurity(store, await Effect17.runPromise(store.serverIdentity()));
|
|
4612
5113
|
sandboxPushUrl = process.env.AGENTRIFIC_PUSH_SANDBOX === "1" ? PUSH_SANDBOX_API_URL : void 0;
|
|
@@ -4638,7 +5139,7 @@ var init_index = __esm({
|
|
|
4638
5139
|
await registerSecurity(app, security, serverVersion, allowedOrigins);
|
|
4639
5140
|
registerPushRoutes(app, push, security);
|
|
4640
5141
|
attachmentEffects = ManagedRuntime.make(
|
|
4641
|
-
Attachments.layer(
|
|
5142
|
+
Attachments.layer(path13.join(dataRoot, "attachments"), store)
|
|
4642
5143
|
);
|
|
4643
5144
|
attachments = await attachmentEffects.runPromise(Attachments);
|
|
4644
5145
|
await Effect17.runPromise(attachments.cleanup());
|
|
@@ -4703,7 +5204,7 @@ var init_index = __esm({
|
|
|
4703
5204
|
createPiFactory(attachments, models, delivery),
|
|
4704
5205
|
publish,
|
|
4705
5206
|
worktreesRoot,
|
|
4706
|
-
|
|
5207
|
+
path13.join(dataRoot, "pi"),
|
|
4707
5208
|
(sessionId, messageId) => Effect17.runSync(store.hasMessage(sessionId, messageId)),
|
|
4708
5209
|
(session) => delivery.settled(session.id)
|
|
4709
5210
|
)
|
|
@@ -4864,7 +5365,7 @@ var init_index = __esm({
|
|
|
4864
5365
|
return project ?? reply.code(404).send({ error: "Project not found on this server." });
|
|
4865
5366
|
});
|
|
4866
5367
|
app.post("/api/projects/:id/reconnect", async (request, reply) => {
|
|
4867
|
-
const parsed =
|
|
5368
|
+
const parsed = z19.object({ path: z19.string().trim().min(1).max(4096) }).safeParse(request.body);
|
|
4868
5369
|
if (!parsed.success)
|
|
4869
5370
|
return reply.code(400).send({ error: "Enter the project's new folder path." });
|
|
4870
5371
|
const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
@@ -4950,7 +5451,7 @@ var init_index = __esm({
|
|
|
4950
5451
|
}
|
|
4951
5452
|
});
|
|
4952
5453
|
app.post("/api/tasks/:id/discard-unrun", async (request, reply) => {
|
|
4953
|
-
const parsed =
|
|
5454
|
+
const parsed = z19.object({ updatedAt: z19.string().datetime() }).safeParse(request.body);
|
|
4954
5455
|
if (!parsed.success) return reply.code(400).send({ error: "The task revision is required." });
|
|
4955
5456
|
try {
|
|
4956
5457
|
await deleteTask(store, request.params.id, void 0, parsed.data.updatedAt);
|
|
@@ -5020,9 +5521,9 @@ var init_index = __esm({
|
|
|
5020
5521
|
}
|
|
5021
5522
|
})
|
|
5022
5523
|
);
|
|
5023
|
-
HistoryQuery =
|
|
5024
|
-
after:
|
|
5025
|
-
limit:
|
|
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)
|
|
5026
5527
|
});
|
|
5027
5528
|
app.get("/api/sessions/:id/events", async (request, reply) => {
|
|
5028
5529
|
const query = HistoryQuery.safeParse(request.query);
|
|
@@ -5036,7 +5537,7 @@ var init_index = __esm({
|
|
|
5036
5537
|
const location = await resolveSessionProject(request.params.id);
|
|
5037
5538
|
if (location.error !== void 0)
|
|
5038
5539
|
return reply.code(location.status).send({ error: location.error });
|
|
5039
|
-
const parsed =
|
|
5540
|
+
const parsed = z19.object({ reset: z19.boolean().optional() }).safeParse(request.body ?? {});
|
|
5040
5541
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
5041
5542
|
try {
|
|
5042
5543
|
return await openRepoChat(
|
|
@@ -5060,7 +5561,7 @@ var init_index = __esm({
|
|
|
5060
5561
|
if (!result.session) return reply.code(result.status).send({ error: result.error });
|
|
5061
5562
|
return reply.code(201).send(result.session);
|
|
5062
5563
|
});
|
|
5063
|
-
StartTaskBody =
|
|
5564
|
+
StartTaskBody = z19.object({ prompt: z19.string().trim().max(2e4).optional() }).default({});
|
|
5064
5565
|
app.post("/api/tasks/:id/start", async (request, reply) => {
|
|
5065
5566
|
const body = StartTaskBody.safeParse(request.body ?? {});
|
|
5066
5567
|
if (!body.success) return reply.code(400).send({ error: "The task prompt is too long." });
|
|
@@ -5095,7 +5596,7 @@ var init_index = __esm({
|
|
|
5095
5596
|
return automation.configure(parsed.data);
|
|
5096
5597
|
});
|
|
5097
5598
|
app.patch("/api/automation/queue", async (request, reply) => {
|
|
5098
|
-
const parsed =
|
|
5599
|
+
const parsed = z19.object({ taskId: z19.string().min(1), beforeId: z19.string().min(1).nullable() }).safeParse(request.body);
|
|
5099
5600
|
if (!parsed.success)
|
|
5100
5601
|
return reply.code(400).send({ error: "Choose a task and a queue position." });
|
|
5101
5602
|
try {
|
|
@@ -5152,12 +5653,30 @@ var init_index = __esm({
|
|
|
5152
5653
|
type: "snapshot",
|
|
5153
5654
|
sessions: Effect17.runSync(store.list())
|
|
5154
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;
|
|
5155
5673
|
closeControl = await startControl(
|
|
5156
5674
|
dataRoot,
|
|
5157
5675
|
security,
|
|
5158
5676
|
() => ({
|
|
5159
5677
|
serverId: security.serverId,
|
|
5160
5678
|
publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
|
|
5679
|
+
phoneProvider: phone?.snapshot().provider,
|
|
5161
5680
|
localUrl: localServerUrl(),
|
|
5162
5681
|
serverVersion,
|
|
5163
5682
|
connectedDevices: new Set(security.sockets.values()).size,
|
|
@@ -5170,10 +5689,12 @@ var init_index = __esm({
|
|
|
5170
5689
|
async () => {
|
|
5171
5690
|
await app.close();
|
|
5172
5691
|
process.exit(0);
|
|
5173
|
-
}
|
|
5692
|
+
},
|
|
5693
|
+
phone
|
|
5174
5694
|
);
|
|
5175
5695
|
push.start();
|
|
5176
5696
|
app.addHook("onClose", async () => {
|
|
5697
|
+
await phone?.close();
|
|
5177
5698
|
await push.close();
|
|
5178
5699
|
maintenance.enter();
|
|
5179
5700
|
stopUpdateDiscovery();
|
|
@@ -5206,9 +5727,9 @@ var init_index = __esm({
|
|
|
5206
5727
|
|
|
5207
5728
|
// src/cli.ts
|
|
5208
5729
|
init_warnings();
|
|
5209
|
-
import { mkdir as
|
|
5730
|
+
import { mkdir as mkdir7, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
|
|
5210
5731
|
import { homedir as homedir5 } from "node:os";
|
|
5211
|
-
import
|
|
5732
|
+
import path14 from "node:path";
|
|
5212
5733
|
import { fileURLToPath } from "node:url";
|
|
5213
5734
|
|
|
5214
5735
|
// src/service.ts
|
|
@@ -5307,284 +5828,18 @@ init_version();
|
|
|
5307
5828
|
init_pairing();
|
|
5308
5829
|
import { spawn as spawn2 } from "node:child_process";
|
|
5309
5830
|
import { parseArgs } from "node:util";
|
|
5310
|
-
import { z as
|
|
5831
|
+
import { z as z20 } from "zod";
|
|
5311
5832
|
|
|
5312
5833
|
// src/terminal.ts
|
|
5313
5834
|
init_pairing();
|
|
5314
5835
|
init_cli_output();
|
|
5836
|
+
init_tunnels();
|
|
5837
|
+
init_opentunnel();
|
|
5315
5838
|
import { createInterface } from "node:readline/promises";
|
|
5316
5839
|
import select from "@inquirer/select";
|
|
5317
5840
|
import { emitKeypressEvents as emitKeypressEvents2 } from "node:readline";
|
|
5318
5841
|
import { styleText as styleText3 } from "node:util";
|
|
5319
|
-
|
|
5320
|
-
// src/tunnels.ts
|
|
5321
|
-
init_cli_output();
|
|
5322
|
-
import { spawn } from "node:child_process";
|
|
5323
|
-
import { access, stat } from "node:fs/promises";
|
|
5324
|
-
import { constants } from "node:fs";
|
|
5325
|
-
import { homedir as homedir2 } from "node:os";
|
|
5326
|
-
import path3 from "node:path";
|
|
5327
|
-
import { z as z5 } from "zod";
|
|
5328
|
-
function signalChild(child, signal) {
|
|
5329
|
-
if (!child.pid) return;
|
|
5330
|
-
if (process.platform === "win32") {
|
|
5331
|
-
child.kill(signal);
|
|
5332
|
-
return;
|
|
5333
|
-
}
|
|
5334
|
-
try {
|
|
5335
|
-
process.kill(-child.pid, signal);
|
|
5336
|
-
} catch (error) {
|
|
5337
|
-
if (!z5.object({ code: z5.literal("ESRCH") }).safeParse(error).success) child.kill(signal);
|
|
5338
|
-
}
|
|
5339
|
-
}
|
|
5340
|
-
function execute2(file, args, timeoutMs, env = process.env) {
|
|
5341
|
-
return new Promise((resolve2, reject) => {
|
|
5342
|
-
const child = spawn(file, args, {
|
|
5343
|
-
env,
|
|
5344
|
-
detached: process.platform !== "win32",
|
|
5345
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5346
|
-
});
|
|
5347
|
-
let stdout = "";
|
|
5348
|
-
let stderr = "";
|
|
5349
|
-
const timeout = setTimeout(() => {
|
|
5350
|
-
signalChild(child, "SIGKILL");
|
|
5351
|
-
reject(new Error("Command probe timed out."));
|
|
5352
|
-
}, timeoutMs);
|
|
5353
|
-
child.stdout?.on("data", (data) => {
|
|
5354
|
-
stdout += data.toString();
|
|
5355
|
-
checkOutput();
|
|
5356
|
-
});
|
|
5357
|
-
child.stderr?.on("data", (data) => {
|
|
5358
|
-
stderr += data.toString();
|
|
5359
|
-
checkOutput();
|
|
5360
|
-
});
|
|
5361
|
-
child.once("error", (error) => {
|
|
5362
|
-
clearTimeout(timeout);
|
|
5363
|
-
reject(error);
|
|
5364
|
-
});
|
|
5365
|
-
child.once("exit", () => signalChild(child, "SIGKILL"));
|
|
5366
|
-
child.once("close", (code) => {
|
|
5367
|
-
clearTimeout(timeout);
|
|
5368
|
-
if (code !== 0) reject(new Error("Command probe failed."));
|
|
5369
|
-
else resolve2({ stdout, stderr });
|
|
5370
|
-
});
|
|
5371
|
-
function checkOutput() {
|
|
5372
|
-
if (stdout.length + stderr.length <= 1048576) return;
|
|
5373
|
-
signalChild(child, "SIGKILL");
|
|
5374
|
-
reject(new Error("Command probe output exceeded its limit."));
|
|
5375
|
-
}
|
|
5376
|
-
});
|
|
5377
|
-
}
|
|
5378
|
-
async function findExecutable(name) {
|
|
5379
|
-
const candidates = (process.env.PATH ?? "").split(path3.delimiter).map((directory) => path3.resolve(directory, name));
|
|
5380
|
-
if (process.platform === "darwin") {
|
|
5381
|
-
candidates.push(`/opt/homebrew/bin/${name}`, `/usr/local/bin/${name}`);
|
|
5382
|
-
if (name === "tailscale")
|
|
5383
|
-
candidates.push("/Applications/Tailscale.app/Contents/MacOS/Tailscale");
|
|
5384
|
-
}
|
|
5385
|
-
for (const candidate of candidates) {
|
|
5386
|
-
try {
|
|
5387
|
-
await access(candidate, constants.X_OK);
|
|
5388
|
-
if ((await stat(candidate)).isFile()) return candidate;
|
|
5389
|
-
} catch {
|
|
5390
|
-
continue;
|
|
5391
|
-
}
|
|
5392
|
-
}
|
|
5393
|
-
return void 0;
|
|
5394
|
-
}
|
|
5395
|
-
var TcpConfig = z5.object({ TCP: z5.record(z5.string(), z5.unknown()).optional() });
|
|
5396
|
-
var ServeConfig = TcpConfig.extend({
|
|
5397
|
-
Foreground: z5.record(z5.string(), TcpConfig).optional()
|
|
5398
|
-
});
|
|
5399
|
-
async function probeTailscale() {
|
|
5400
|
-
const option = { provider: "tailscale", available: false, reason: "Not installed" };
|
|
5401
|
-
const executable = await findExecutable("tailscale");
|
|
5402
|
-
if (!executable) return option;
|
|
5403
|
-
option.executable = executable;
|
|
5404
|
-
const env = { ...process.env, TAILSCALE_BE_CLI: "1" };
|
|
5405
|
-
try {
|
|
5406
|
-
const response = await execute2(executable, ["status", "--json"], 3e3, env);
|
|
5407
|
-
const status = z5.object({
|
|
5408
|
-
BackendState: z5.string(),
|
|
5409
|
-
CertDomains: z5.array(z5.string()).optional(),
|
|
5410
|
-
Self: z5.object({ DNSName: z5.string() }).nullable().optional()
|
|
5411
|
-
}).parse(JSON.parse(response.stdout));
|
|
5412
|
-
if (status.BackendState !== "Running")
|
|
5413
|
-
return { ...option, reason: "Installed \xB7 connect Tailscale first" };
|
|
5414
|
-
const dns = status.Self?.DNSName.replace(/\.$/, "");
|
|
5415
|
-
if (!dns || !status.CertDomains?.includes(dns))
|
|
5416
|
-
return { ...option, reason: "Enable HTTPS certificates in your tailnet first" };
|
|
5417
|
-
const help = await execute2(executable, ["serve", "--help"], 2e3, env);
|
|
5418
|
-
if (!`${help.stdout}${help.stderr}`.includes("--https"))
|
|
5419
|
-
return { ...option, reason: "Update Tailscale to use foreground HTTPS Serve" };
|
|
5420
|
-
const responseConfig = await execute2(executable, ["serve", "status", "--json"], 3e3, env);
|
|
5421
|
-
const config = ServeConfig.parse(JSON.parse(responseConfig.stdout) ?? {});
|
|
5422
|
-
const occupied = new Set(Object.keys(config.TCP ?? {}));
|
|
5423
|
-
for (const foreground of Object.values(config.Foreground ?? {}))
|
|
5424
|
-
for (const port3 of Object.keys(foreground.TCP ?? {})) occupied.add(port3);
|
|
5425
|
-
const port2 = [443, ...Array.from({ length: 10 }, (_, index) => 8443 + index)].find(
|
|
5426
|
-
(candidate) => !occupied.has(String(candidate))
|
|
5427
|
-
);
|
|
5428
|
-
if (!port2) return { ...option, reason: "HTTPS Serve ports are in use \xB7 paste an existing URL" };
|
|
5429
|
-
return {
|
|
5430
|
-
...option,
|
|
5431
|
-
available: true,
|
|
5432
|
-
reason: "Private to your tailnet \xB7 Tailscale required on your phone",
|
|
5433
|
-
publicUrl: new URL(`https://${dns}${port2 === 443 ? "" : `:${port2}`}`).origin,
|
|
5434
|
-
httpsPort: port2
|
|
5435
|
-
};
|
|
5436
|
-
} catch {
|
|
5437
|
-
return { ...option, reason: "Installed \xB7 could not verify daemon or Serve access" };
|
|
5438
|
-
}
|
|
5439
|
-
}
|
|
5440
|
-
async function probeCloudflared(home) {
|
|
5441
|
-
const option = {
|
|
5442
|
-
provider: "cloudflared",
|
|
5443
|
-
available: false,
|
|
5444
|
-
reason: "Not installed"
|
|
5445
|
-
};
|
|
5446
|
-
const executable = await findExecutable("cloudflared");
|
|
5447
|
-
if (!executable) return option;
|
|
5448
|
-
option.executable = executable;
|
|
5449
|
-
try {
|
|
5450
|
-
await execute2(executable, ["--version"], 2e3);
|
|
5451
|
-
const help = await execute2(executable, ["tunnel", "--help"], 2e3);
|
|
5452
|
-
if (!`${help.stdout}${help.stderr}`.includes("--url"))
|
|
5453
|
-
return { ...option, reason: "Installed \xB7 this version does not support Quick Tunnels" };
|
|
5454
|
-
} catch {
|
|
5455
|
-
return { ...option, reason: "Installed \xB7 command probe failed or timed out" };
|
|
5456
|
-
}
|
|
5457
|
-
const directories = [
|
|
5458
|
-
path3.join(home, ".cloudflared"),
|
|
5459
|
-
path3.join(home, ".cloudflare-warp"),
|
|
5460
|
-
path3.join(home, "cloudflare-warp"),
|
|
5461
|
-
"/etc/cloudflared",
|
|
5462
|
-
"/usr/local/etc/cloudflared"
|
|
5463
|
-
];
|
|
5464
|
-
const configurations = await Promise.all(
|
|
5465
|
-
directories.flatMap(
|
|
5466
|
-
(directory) => ["config.yml", "config.yaml"].map(
|
|
5467
|
-
async (name) => access(path3.join(directory, name), constants.F_OK).then(
|
|
5468
|
-
() => true,
|
|
5469
|
-
() => false
|
|
5470
|
-
)
|
|
5471
|
-
)
|
|
5472
|
-
)
|
|
5473
|
-
);
|
|
5474
|
-
if (configurations.some(Boolean))
|
|
5475
|
-
return { ...option, reason: "Existing tunnel configuration \xB7 paste its HTTPS URL" };
|
|
5476
|
-
return { ...option, available: true, reason: "Temporary public HTTPS URL \xB7 changes each start" };
|
|
5477
|
-
}
|
|
5478
|
-
async function detectTunnels(home = homedir2()) {
|
|
5479
|
-
return Promise.all([probeTailscale(), probeCloudflared(home)]);
|
|
5480
|
-
}
|
|
5481
|
-
var ManagedTunnel = class {
|
|
5482
|
-
constructor(option) {
|
|
5483
|
-
this.option = option;
|
|
5484
|
-
}
|
|
5485
|
-
option;
|
|
5486
|
-
child;
|
|
5487
|
-
stopping = false;
|
|
5488
|
-
connected = false;
|
|
5489
|
-
closing;
|
|
5490
|
-
running() {
|
|
5491
|
-
return this.connected && !this.stopping;
|
|
5492
|
-
}
|
|
5493
|
-
stopOnSignal = () => {
|
|
5494
|
-
void this.close();
|
|
5495
|
-
};
|
|
5496
|
-
stopOnExit = () => {
|
|
5497
|
-
if (this.child) signalChild(this.child, "SIGKILL");
|
|
5498
|
-
};
|
|
5499
|
-
async start(origin, signal) {
|
|
5500
|
-
signal?.throwIfAborted();
|
|
5501
|
-
if (!this.option.available) throw new Error(this.option.reason);
|
|
5502
|
-
const tailscale = this.option.provider === "tailscale";
|
|
5503
|
-
const child = spawn(
|
|
5504
|
-
this.option.executable ?? this.option.provider,
|
|
5505
|
-
tailscale ? ["serve", "--bg=false", `--https=${this.option.httpsPort}`, origin] : ["tunnel", "--no-autoupdate", "--url", origin],
|
|
5506
|
-
{
|
|
5507
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
5508
|
-
detached: process.platform !== "win32",
|
|
5509
|
-
env: tailscale ? { ...process.env, TAILSCALE_BE_CLI: "1" } : process.env
|
|
5510
|
-
}
|
|
5511
|
-
);
|
|
5512
|
-
this.child = child;
|
|
5513
|
-
process.once("SIGINT", this.stopOnSignal);
|
|
5514
|
-
process.once("SIGTERM", this.stopOnSignal);
|
|
5515
|
-
process.once("exit", this.stopOnExit);
|
|
5516
|
-
return new Promise((resolve2, reject) => {
|
|
5517
|
-
let output = "";
|
|
5518
|
-
let address;
|
|
5519
|
-
let settled = false;
|
|
5520
|
-
const abort = () => fail("Tunnel startup canceled.");
|
|
5521
|
-
const timeout = setTimeout(
|
|
5522
|
-
() => fail("Tunnel startup timed out. Check your connection or paste an existing HTTPS URL."),
|
|
5523
|
-
45e3
|
|
5524
|
-
);
|
|
5525
|
-
const fail = (message) => {
|
|
5526
|
-
if (settled) return;
|
|
5527
|
-
settled = true;
|
|
5528
|
-
clearTimeout(timeout);
|
|
5529
|
-
signal?.removeEventListener("abort", abort);
|
|
5530
|
-
void this.close();
|
|
5531
|
-
reject(new Error(message));
|
|
5532
|
-
};
|
|
5533
|
-
const read = (data) => {
|
|
5534
|
-
output = `${output}${data.toString()}`.slice(-65536);
|
|
5535
|
-
address = tailscale ? this.option.publicUrl : output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com\b/)?.[0];
|
|
5536
|
-
const ready = tailscale ? Boolean(
|
|
5537
|
-
address && output.includes(address) && output.includes("Available within your tailnet")
|
|
5538
|
-
) : output.includes("Registered tunnel connection");
|
|
5539
|
-
if (settled || !address || !ready) return;
|
|
5540
|
-
settled = true;
|
|
5541
|
-
this.connected = true;
|
|
5542
|
-
clearTimeout(timeout);
|
|
5543
|
-
signal?.removeEventListener("abort", abort);
|
|
5544
|
-
resolve2(address);
|
|
5545
|
-
};
|
|
5546
|
-
child.stdout?.on("data", read);
|
|
5547
|
-
child.stderr?.on("data", read);
|
|
5548
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
5549
|
-
child.once(
|
|
5550
|
-
"error",
|
|
5551
|
-
() => fail(`${this.option.provider} could not start. Check the installation and permissions.`)
|
|
5552
|
-
);
|
|
5553
|
-
child.once("exit", () => signalChild(child, "SIGTERM"));
|
|
5554
|
-
child.once("close", (code) => {
|
|
5555
|
-
const unexpected = this.connected && !this.stopping;
|
|
5556
|
-
this.connected = false;
|
|
5557
|
-
process.removeListener("SIGINT", this.stopOnSignal);
|
|
5558
|
-
process.removeListener("SIGTERM", this.stopOnSignal);
|
|
5559
|
-
process.removeListener("exit", this.stopOnExit);
|
|
5560
|
-
fail(
|
|
5561
|
-
`${this.option.provider} exited (${code ?? "signal"}). Check tunnel access or paste an existing HTTPS URL.`
|
|
5562
|
-
);
|
|
5563
|
-
if (unexpected)
|
|
5564
|
-
printError(
|
|
5565
|
-
"The managed tunnel stopped. Restart the server to reconnect your phone; local work can continue."
|
|
5566
|
-
);
|
|
5567
|
-
});
|
|
5568
|
-
});
|
|
5569
|
-
}
|
|
5570
|
-
close() {
|
|
5571
|
-
this.stopping = true;
|
|
5572
|
-
const child = this.child;
|
|
5573
|
-
if (!child?.pid || child.exitCode !== null || child.signalCode !== null)
|
|
5574
|
-
return Promise.resolve();
|
|
5575
|
-
this.closing ??= new Promise((resolve2) => {
|
|
5576
|
-
const timeout = setTimeout(() => signalChild(child, "SIGKILL"), 2e3);
|
|
5577
|
-
child.once("close", () => {
|
|
5578
|
-
clearTimeout(timeout);
|
|
5579
|
-
resolve2();
|
|
5580
|
-
});
|
|
5581
|
-
signalChild(child, "SIGTERM");
|
|
5582
|
-
});
|
|
5583
|
-
return this.closing;
|
|
5584
|
-
}
|
|
5585
|
-
};
|
|
5586
|
-
|
|
5587
|
-
// src/terminal.ts
|
|
5842
|
+
import path5 from "node:path";
|
|
5588
5843
|
async function promptPublicUrl(signal, saved) {
|
|
5589
5844
|
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
5590
5845
|
const cancel = () => controller.abort();
|
|
@@ -5628,9 +5883,14 @@ async function selectConnectionMethod(options, signal, saved) {
|
|
|
5628
5883
|
return select(
|
|
5629
5884
|
{
|
|
5630
5885
|
message: "Connection method",
|
|
5631
|
-
default: saved ? "existing" :
|
|
5886
|
+
default: saved ? "existing" : "opentunnel",
|
|
5632
5887
|
loop: false,
|
|
5633
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
|
+
},
|
|
5634
5894
|
...options.filter((option) => option.available).map((option) => ({
|
|
5635
5895
|
value: option.provider,
|
|
5636
5896
|
name: option.provider === "tailscale" ? "Tailscale" : "Cloudflared",
|
|
@@ -5661,13 +5921,14 @@ async function selectConnectionMethod(options, signal, saved) {
|
|
|
5661
5921
|
{ signal }
|
|
5662
5922
|
);
|
|
5663
5923
|
}
|
|
5664
|
-
async function startSelectedTunnel(options, choice, origin, signal) {
|
|
5924
|
+
async function startSelectedTunnel(options, choice, origin, signal, dataDir) {
|
|
5665
5925
|
const option = options.find((candidate) => candidate.provider === choice);
|
|
5666
|
-
if (
|
|
5667
|
-
|
|
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);
|
|
5668
5929
|
try {
|
|
5669
5930
|
console.log(styleText3("dim", `
|
|
5670
|
-
Starting ${
|
|
5931
|
+
Starting ${choice}\u2026
|
|
5671
5932
|
`));
|
|
5672
5933
|
const url = await tunnel.start(origin, signal);
|
|
5673
5934
|
return { url, tunnel, remember: false };
|
|
@@ -5678,7 +5939,7 @@ async function startSelectedTunnel(options, choice, origin, signal) {
|
|
|
5678
5939
|
return void 0;
|
|
5679
5940
|
}
|
|
5680
5941
|
}
|
|
5681
|
-
async function selectPublicConnection(origin, saved) {
|
|
5942
|
+
async function selectPublicConnection(origin, saved, dataDir = process.env.AGENTRIFIC_DATA_DIR || ".agentrific") {
|
|
5682
5943
|
const controller = new AbortController();
|
|
5683
5944
|
const cancel = () => controller.abort();
|
|
5684
5945
|
process.once("SIGINT", cancel);
|
|
@@ -5699,7 +5960,13 @@ async function selectPublicConnection(origin, saved) {
|
|
|
5699
5960
|
console.log();
|
|
5700
5961
|
return { url: await promptPublicUrl(controller.signal, saved), remember: true };
|
|
5701
5962
|
}
|
|
5702
|
-
const connection = await startSelectedTunnel(
|
|
5963
|
+
const connection = await startSelectedTunnel(
|
|
5964
|
+
options,
|
|
5965
|
+
choice,
|
|
5966
|
+
origin,
|
|
5967
|
+
controller.signal,
|
|
5968
|
+
dataDir
|
|
5969
|
+
);
|
|
5703
5970
|
if (connection) return connection;
|
|
5704
5971
|
}
|
|
5705
5972
|
} finally {
|
|
@@ -5787,11 +6054,14 @@ function startTerminal(dataDir, publicUrl, tunnel) {
|
|
|
5787
6054
|
}
|
|
5788
6055
|
|
|
5789
6056
|
// src/cli.ts
|
|
6057
|
+
init_opentunnel();
|
|
6058
|
+
init_tunnels();
|
|
5790
6059
|
init_cli_output();
|
|
5791
|
-
var ConfigSchema =
|
|
5792
|
-
dataDir:
|
|
5793
|
-
projectsDir:
|
|
5794
|
-
publicUrl:
|
|
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()
|
|
5795
6065
|
});
|
|
5796
6066
|
function parseCommandArgs() {
|
|
5797
6067
|
return parseArgs({
|
|
@@ -5799,6 +6069,7 @@ function parseCommandArgs() {
|
|
|
5799
6069
|
options: {
|
|
5800
6070
|
"data-dir": { type: "string" },
|
|
5801
6071
|
"public-url": { type: "string" },
|
|
6072
|
+
connection: { type: "string" },
|
|
5802
6073
|
version: { type: "boolean" },
|
|
5803
6074
|
help: { type: "boolean" },
|
|
5804
6075
|
check: { type: "boolean" },
|
|
@@ -5819,15 +6090,15 @@ async function main() {
|
|
|
5819
6090
|
printHelp();
|
|
5820
6091
|
return;
|
|
5821
6092
|
}
|
|
5822
|
-
const dataDir =
|
|
5823
|
-
values["data-dir"] ?? process.env.AGENTRIFIC_DATA_DIR ??
|
|
6093
|
+
const dataDir = path14.resolve(
|
|
6094
|
+
values["data-dir"] ?? process.env.AGENTRIFIC_DATA_DIR ?? path14.join(homedir5(), ".agentrific")
|
|
5824
6095
|
);
|
|
5825
|
-
const filename =
|
|
6096
|
+
const filename = path14.join(dataDir, "config.json");
|
|
5826
6097
|
const config = ConfigSchema.parse(
|
|
5827
6098
|
JSON.parse(
|
|
5828
|
-
await
|
|
5829
|
-
if (!
|
|
5830
|
-
return JSON.stringify({ dataDir, projectsDir:
|
|
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") });
|
|
5831
6102
|
})
|
|
5832
6103
|
)
|
|
5833
6104
|
);
|
|
@@ -5844,11 +6115,12 @@ async function main() {
|
|
|
5844
6115
|
throw new Error("Unknown command. Run agentrific --help.");
|
|
5845
6116
|
}
|
|
5846
6117
|
async function initCommand({ values, dataDir, filename, config }) {
|
|
5847
|
-
await
|
|
6118
|
+
await mkdir7(dataDir, { recursive: true, mode: 448 });
|
|
5848
6119
|
const publicUrl = values["public-url"] ?? config.publicUrl;
|
|
6120
|
+
const connection = values.connection ? ConfigSchema.shape.connection.unwrap().parse(values.connection) : config.connection;
|
|
5849
6121
|
if (publicUrl && new URL(publicUrl).protocol !== "https:")
|
|
5850
6122
|
throw new Error("Configure an HTTPS URL from Tailscale Serve or Cloudflare Tunnel.");
|
|
5851
|
-
await
|
|
6123
|
+
await writeFile6(filename, JSON.stringify({ ...config, publicUrl, connection }, null, 2) + "\n", {
|
|
5852
6124
|
mode: 384
|
|
5853
6125
|
});
|
|
5854
6126
|
console.log(`Configuration saved to ${filename}. Start with agentrific start.`);
|
|
@@ -5861,28 +6133,48 @@ async function startCommand({ values, dataDir, filename, config }) {
|
|
|
5861
6133
|
process.stdin.isTTY && process.stdout.isTTY && !values["no-interactive"]
|
|
5862
6134
|
);
|
|
5863
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"}`;
|
|
5864
6138
|
if (interactive) {
|
|
5865
6139
|
process.env.AGENTRIFIC_INTERACTIVE = "1";
|
|
5866
6140
|
printHeading("server");
|
|
5867
6141
|
}
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
config.publicUrl
|
|
5871
|
-
) : {
|
|
5872
|
-
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,
|
|
5873
6144
|
remember: false,
|
|
5874
6145
|
tunnel: void 0
|
|
5875
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.");
|
|
5876
6162
|
const publicUrl = connection.url;
|
|
5877
6163
|
try {
|
|
5878
6164
|
if (connection.remember && publicUrl) {
|
|
5879
|
-
await
|
|
5880
|
-
await
|
|
6165
|
+
await mkdir7(dataDir, { recursive: true, mode: 448 });
|
|
6166
|
+
await writeFile6(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
|
|
5881
6167
|
mode: 384
|
|
5882
6168
|
});
|
|
5883
6169
|
}
|
|
5884
6170
|
if (publicUrl) process.env.AGENTRIFIC_PUBLIC_URL = pairingOrigin(publicUrl);
|
|
6171
|
+
else delete process.env.AGENTRIFIC_PUBLIC_URL;
|
|
5885
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
|
+
}
|
|
5886
6178
|
if (interactive) startTerminal(dataDir, publicUrl, connection.tunnel);
|
|
5887
6179
|
} catch (error) {
|
|
5888
6180
|
await connection.tunnel?.close();
|
|
@@ -5899,7 +6191,7 @@ async function serviceCommand({ positionals, dataDir }) {
|
|
|
5899
6191
|
return;
|
|
5900
6192
|
}
|
|
5901
6193
|
async function logsCommand({ dataDir }) {
|
|
5902
|
-
const child = process.platform === "linux" ? spawn2("journalctl", ["--user", "-u", "agentrific.service", "-f"], { stdio: "inherit" }) : spawn2("tail", ["-F",
|
|
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" });
|
|
5903
6195
|
child.on("error", (error) => {
|
|
5904
6196
|
console.error(error.message);
|
|
5905
6197
|
process.exitCode = 1;
|
|
@@ -5933,11 +6225,11 @@ async function pairCommand({ values, dataDir, config }) {
|
|
|
5933
6225
|
return;
|
|
5934
6226
|
}
|
|
5935
6227
|
async function maintenanceCommand({ values, positionals, dataDir }) {
|
|
5936
|
-
const action =
|
|
6228
|
+
const action = z20.enum(["enter", "resume", "drain"]).parse(positionals[1]);
|
|
5937
6229
|
const result = await controlRequest(dataDir, {
|
|
5938
6230
|
command: "maintenance",
|
|
5939
6231
|
action,
|
|
5940
|
-
timeoutMs:
|
|
6232
|
+
timeoutMs: z20.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
|
|
5941
6233
|
});
|
|
5942
6234
|
console.log(JSON.stringify(result, null, 2));
|
|
5943
6235
|
if (action === "drain" && "drained" in result && !result.drained) process.exitCode = 1;
|
|
@@ -5946,7 +6238,7 @@ async function maintenanceCommand({ values, positionals, dataDir }) {
|
|
|
5946
6238
|
async function devicesCommand({ dataDir, command }) {
|
|
5947
6239
|
console.log(
|
|
5948
6240
|
JSON.stringify(
|
|
5949
|
-
await controlRequest(dataDir, { command:
|
|
6241
|
+
await controlRequest(dataDir, { command: z20.enum(["devices", "status"]).parse(command) }),
|
|
5950
6242
|
null,
|
|
5951
6243
|
2
|
|
5952
6244
|
)
|
|
@@ -5954,7 +6246,7 @@ async function devicesCommand({ dataDir, command }) {
|
|
|
5954
6246
|
return;
|
|
5955
6247
|
}
|
|
5956
6248
|
async function revokeCommand({ positionals, dataDir }) {
|
|
5957
|
-
const id =
|
|
6249
|
+
const id = z20.string().uuid().parse(positionals[1]);
|
|
5958
6250
|
console.log(JSON.stringify(await controlRequest(dataDir, { command: "revoke", id })));
|
|
5959
6251
|
return;
|
|
5960
6252
|
}
|