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