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