@agentrific/server 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +2 -0
- package/cli.js +1121 -536
- package/cli.js.map +4 -4
- package/index.js +1211 -652
- package/index.js.map +4 -4
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ var init_package = __esm({
|
|
|
30
30
|
"package.json"() {
|
|
31
31
|
package_default = {
|
|
32
32
|
name: "@agentrific/server",
|
|
33
|
-
version: "0.1.
|
|
33
|
+
version: "0.1.6",
|
|
34
34
|
description: "Self-hosted control plane for long-running coding agents",
|
|
35
35
|
keywords: [
|
|
36
36
|
"agentrific",
|
|
@@ -245,7 +245,7 @@ function createPairingLink(payload) {
|
|
|
245
245
|
link.searchParams.set("data", JSON.stringify(PairingPayloadSchema.parse(payload)));
|
|
246
246
|
return link.toString();
|
|
247
247
|
}
|
|
248
|
-
var cronBounds, AutomationConfigSchema, AutomationStateSchema, SessionStatusSchema, AgentKindSchema, ModelReferenceSchema, ModelIdentitySchema, ModelOptionSchema, ModelCatalogSchema, GitFlowSchema, GitDeliverySchema, DeliveryRequestSchema, RunPreparationSchema, SessionSchema, MAX_ATTACHMENTS, MAX_ATTACHMENT_BYTES, AttachmentSchema, AttachmentIdsSchema, CreateSessionSchema, SendMessageSchema, SessionEventSchema, RecordedEventSchema, EventHistorySchema, ServerEventSchema, ServerSettingsSchema, ConnectionsStatusSchema, UpdateSettingsSchema, ProjectSchema, TaskStatusSchema, TaskSchema, CreateTaskSchema, UpdateTaskSchema, PROTOCOL_VERSION, ServerUpdateStatusSchema, ServerHealthSchema, PairingPayloadSchema, PairingRequestSchema, DeviceSchema, PairingResultSchema, HandshakeSchema, HandshakeAckSchema;
|
|
248
|
+
var cronBounds, AutomationConfigSchema, AutomationStateSchema, SessionStatusSchema, AgentKindSchema, ModelReferenceSchema, ModelIdentitySchema, ModelOptionSchema, ModelCatalogSchema, GitFlowSchema, GitDeliverySchema, DeliveryRequestSchema, RunPreparationSchema, SessionSchema, MAX_ATTACHMENTS, MAX_ATTACHMENT_BYTES, AttachmentSchema, AttachmentIdsSchema, CreateSessionSchema, SendMessageSchema, SessionEventSchema, RecordedEventSchema, EventHistorySchema, ServerEventSchema, ServerSettingsSchema, ConnectionsStatusSchema, UpdateSettingsSchema, ProjectSchema, TaskStatusSchema, TaskSchema, CreateTaskSchema, UpdateTaskSchema, PROTOCOL_VERSION, ServerUpdateStatusSchema, ServerHealthSchema, PairingPayloadSchema, PairingInvitationSchema, PairingRequestSchema, DeviceSchema, PairingResultSchema, HandshakeSchema, HandshakeAckSchema;
|
|
249
249
|
var init_src = __esm({
|
|
250
250
|
"../../packages/contracts/src/index.ts"() {
|
|
251
251
|
cronBounds = [
|
|
@@ -519,6 +519,11 @@ var init_src = __esm({
|
|
|
519
519
|
serverId: z2.string().uuid(),
|
|
520
520
|
pairingId: z2.string().regex(/^[a-f0-9]{64}$/)
|
|
521
521
|
});
|
|
522
|
+
PairingInvitationSchema = z2.object({
|
|
523
|
+
payload: PairingPayloadSchema,
|
|
524
|
+
pin: z2.string().regex(/^\d{6}$/),
|
|
525
|
+
expiresAt: z2.number()
|
|
526
|
+
});
|
|
522
527
|
PairingRequestSchema = z2.object({
|
|
523
528
|
pairingId: z2.string().regex(/^[a-f0-9]{64}$/),
|
|
524
529
|
pin: z2.string().regex(/^\d{6}$/),
|
|
@@ -727,15 +732,519 @@ var init_control = __esm({
|
|
|
727
732
|
}
|
|
728
733
|
});
|
|
729
734
|
|
|
735
|
+
// src/cli-output.ts
|
|
736
|
+
import { styleText } from "node:util";
|
|
737
|
+
function printHeading(title) {
|
|
738
|
+
console.log();
|
|
739
|
+
console.log(styleText("bold", " ####### ##"));
|
|
740
|
+
console.log(styleText("bold", " ### #####"));
|
|
741
|
+
console.log(` ${styleText("bold", "## ####")} ${styleText("bold", "AGENTRIFIC")}`);
|
|
742
|
+
console.log(
|
|
743
|
+
` ${styleText("bold", "## ")}${styleText(["bold", "yellow"], "o")}${styleText("bold", " ####")} ${styleText("dim", `${title} \xB7 v${serverVersion}`)}`
|
|
744
|
+
);
|
|
745
|
+
console.log(styleText("bold", " ## ####"));
|
|
746
|
+
console.log(styleText("bold", " ### #####"));
|
|
747
|
+
console.log(styleText("bold", " ####### ##"));
|
|
748
|
+
console.log();
|
|
749
|
+
}
|
|
750
|
+
function printDetail(label2, value) {
|
|
751
|
+
console.log(` ${styleText("dim", "\u203A")} ${styleText("dim", label2.padEnd(10))} ${value}`);
|
|
752
|
+
}
|
|
753
|
+
function printShortcuts(pairingAvailable = false) {
|
|
754
|
+
console.log();
|
|
755
|
+
console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "p")} \u2502 pair a device`);
|
|
756
|
+
if (pairingAvailable)
|
|
757
|
+
console.log(
|
|
758
|
+
` ${styleText("dim", "\u203A")} Press ${styleText("bold", "j")} \u2502 show pairing JSON`
|
|
759
|
+
);
|
|
760
|
+
console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "?")} \u2502 show shortcuts`);
|
|
761
|
+
console.log(
|
|
762
|
+
` ${styleText("dim", "\u203A")} Press ${styleText("bold", "Ctrl+C")} \u2502 stop the server
|
|
763
|
+
`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
function printError(message) {
|
|
767
|
+
console.error(`
|
|
768
|
+
${styleText(["bold", "red"], "Error")} ${message}
|
|
769
|
+
`);
|
|
770
|
+
}
|
|
771
|
+
function printHelp() {
|
|
772
|
+
printHeading("server");
|
|
773
|
+
console.log(` ${styleText("bold", "Usage")} agentrific <command> [options]
|
|
774
|
+
`);
|
|
775
|
+
for (const [command, description] of [
|
|
776
|
+
["init", "Configure the server"],
|
|
777
|
+
["start", "Start the server \xB7 press p to pair"],
|
|
778
|
+
["pair", "Print a fresh pairing QR code and PIN"],
|
|
779
|
+
["devices", "List enrolled devices"],
|
|
780
|
+
["revoke <id>", "Revoke a device's access"],
|
|
781
|
+
["status", "Show server status"],
|
|
782
|
+
["maintenance enter/resume/drain", "Pause new work, resume, or wait for work to finish"],
|
|
783
|
+
["service install/uninstall/restart", "Manage the background service"],
|
|
784
|
+
["logs", "Follow server logs"],
|
|
785
|
+
["update --check", "Check for a newer release"]
|
|
786
|
+
])
|
|
787
|
+
console.log(` ${styleText("cyan", command.padEnd(34))}${description}`);
|
|
788
|
+
console.log(`
|
|
789
|
+
${styleText("bold", "Options")}
|
|
790
|
+
`);
|
|
791
|
+
for (const [option, description] of [
|
|
792
|
+
["--data-dir <path>", "Use a specific server data directory"],
|
|
793
|
+
["--public-url <https-url>", "Use your tunnel URL without prompting"],
|
|
794
|
+
["--no-interactive", "Disable prompts and keyboard shortcuts"],
|
|
795
|
+
["--show-payload", "Print manual JSON with the pairing QR code"],
|
|
796
|
+
["--timeout-ms <0-60000>", "Set the maintenance drain timeout"],
|
|
797
|
+
["--version", "Print the server version"],
|
|
798
|
+
["--help", "Show this help"]
|
|
799
|
+
])
|
|
800
|
+
console.log(` ${styleText("cyan", option.padEnd(34))}${description}`);
|
|
801
|
+
console.log("\n Connect using Tailscale Serve, cloudflared, or another HTTPS tunnel.\n");
|
|
802
|
+
}
|
|
803
|
+
var init_cli_output = __esm({
|
|
804
|
+
"src/cli-output.ts"() {
|
|
805
|
+
"use strict";
|
|
806
|
+
init_version();
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// src/pairing.ts
|
|
811
|
+
import { z as z4 } from "zod";
|
|
812
|
+
import { styleText as styleText2 } from "node:util";
|
|
813
|
+
import { emitKeypressEvents } from "node:readline";
|
|
814
|
+
import qrcode from "qrcode-terminal";
|
|
815
|
+
function printManualPairing(invitation) {
|
|
816
|
+
if (Date.now() >= invitation.expiresAt) {
|
|
817
|
+
printError("This invitation expired. Generate a fresh pairing QR code.");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
console.log(styleText2("dim", "\n Manual entry \xB7 paste this JSON into Pairing payload:\n"));
|
|
821
|
+
console.log(` ${invitation.payload}
|
|
822
|
+
`);
|
|
823
|
+
}
|
|
824
|
+
function waitForManualPairing(invitation) {
|
|
825
|
+
console.log(styleText2("dim", " Press j to show manual entry \xB7 Enter or q to finish.\n"));
|
|
826
|
+
return new Promise((resolve) => {
|
|
827
|
+
const input = process.stdin;
|
|
828
|
+
const previousRaw = input.isRaw;
|
|
829
|
+
const timeout = setTimeout(
|
|
830
|
+
() => {
|
|
831
|
+
console.log(
|
|
832
|
+
styleText2("dim", "\n Pairing invitation expired. Run agentrific pair again.\n")
|
|
833
|
+
);
|
|
834
|
+
close();
|
|
835
|
+
},
|
|
836
|
+
Math.max(0, invitation.expiresAt - Date.now())
|
|
837
|
+
);
|
|
838
|
+
function close() {
|
|
839
|
+
clearTimeout(timeout);
|
|
840
|
+
input.removeListener("keypress", pressed);
|
|
841
|
+
input.removeListener("end", close);
|
|
842
|
+
process.removeListener("SIGINT", interrupted);
|
|
843
|
+
process.removeListener("SIGTERM", terminated);
|
|
844
|
+
input.setRawMode(previousRaw);
|
|
845
|
+
input.pause();
|
|
846
|
+
resolve();
|
|
847
|
+
}
|
|
848
|
+
function interrupted() {
|
|
849
|
+
process.exitCode = 130;
|
|
850
|
+
close();
|
|
851
|
+
}
|
|
852
|
+
function terminated() {
|
|
853
|
+
process.exitCode = 143;
|
|
854
|
+
close();
|
|
855
|
+
}
|
|
856
|
+
function pressed(text, key) {
|
|
857
|
+
if (key.ctrl && key.name === "c") {
|
|
858
|
+
interrupted();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (key.ctrl || key.meta) return;
|
|
862
|
+
if (text?.toLowerCase() === "j") {
|
|
863
|
+
printManualPairing(invitation);
|
|
864
|
+
close();
|
|
865
|
+
} else if (text?.toLowerCase() === "q" || key.name === "return" || key.name === "escape") {
|
|
866
|
+
close();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
emitKeypressEvents(input);
|
|
870
|
+
input.setRawMode(true);
|
|
871
|
+
input.on("keypress", pressed);
|
|
872
|
+
input.once("end", close);
|
|
873
|
+
process.once("SIGINT", interrupted);
|
|
874
|
+
process.once("SIGTERM", terminated);
|
|
875
|
+
input.resume();
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
function pairingOrigin(url) {
|
|
879
|
+
const endpoint = new URL(url);
|
|
880
|
+
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== "/")
|
|
881
|
+
throw new Error("Use the HTTPS server origin without a path or credentials.");
|
|
882
|
+
if (endpoint.protocol !== "https:" && !(process.env.NODE_ENV !== "production" && endpoint.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname)))
|
|
883
|
+
throw new Error("Use HTTPS. Localhost HTTP is for development builds only.");
|
|
884
|
+
return endpoint.origin;
|
|
885
|
+
}
|
|
886
|
+
async function printPairing(dataDir, url) {
|
|
887
|
+
if (!url)
|
|
888
|
+
throw new Error(
|
|
889
|
+
"Specify --public-url with your Tailscale Serve or Cloudflare Tunnel HTTPS address."
|
|
890
|
+
);
|
|
891
|
+
const origin = pairingOrigin(url);
|
|
892
|
+
const grant = z4.object({
|
|
893
|
+
pairingId: z4.string(),
|
|
894
|
+
pin: z4.string(),
|
|
895
|
+
expiresAt: z4.number(),
|
|
896
|
+
serverId: z4.string()
|
|
897
|
+
}).parse(await controlRequest(dataDir, { command: "pair" }));
|
|
898
|
+
const invitation = {
|
|
899
|
+
v: 1,
|
|
900
|
+
url: origin,
|
|
901
|
+
serverId: grant.serverId,
|
|
902
|
+
pairingId: grant.pairingId
|
|
903
|
+
};
|
|
904
|
+
const payload = JSON.stringify(invitation);
|
|
905
|
+
const link = createPairingLink(invitation);
|
|
906
|
+
printHeading("pairing");
|
|
907
|
+
console.log(" 1. Scan below with your phone camera and tap Open Agentrific if offered.");
|
|
908
|
+
console.log(" 2. Or open Agentrific and choose Scan pairing QR code.");
|
|
909
|
+
console.log(" 3. Enter the PIN, then confirm pairing.\n");
|
|
910
|
+
qrcode.generate(link, { small: true }, (code) => {
|
|
911
|
+
console.log(
|
|
912
|
+
code.split("\n").map((line) => ` ${line}`).join("\n")
|
|
913
|
+
);
|
|
914
|
+
});
|
|
915
|
+
console.log();
|
|
916
|
+
printDetail("PIN:", styleText2(["bold", "yellow"], grant.pin));
|
|
917
|
+
printDetail("Server", styleText2("cyan", origin));
|
|
918
|
+
printDetail("Expires", "In five minutes \xB7 one device per code");
|
|
919
|
+
console.log("\n Pairing grants this device access to run agents as your host user.\n");
|
|
920
|
+
return { payload, expiresAt: grant.expiresAt };
|
|
921
|
+
}
|
|
922
|
+
var init_pairing = __esm({
|
|
923
|
+
"src/pairing.ts"() {
|
|
924
|
+
"use strict";
|
|
925
|
+
init_src();
|
|
926
|
+
init_control();
|
|
927
|
+
init_cli_output();
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// ../../packages/contracts/src/push.ts
|
|
932
|
+
import { z as z6 } from "zod";
|
|
933
|
+
function pushSigningText(installationId, operation, request) {
|
|
934
|
+
return [
|
|
935
|
+
"agentrific-push-v1",
|
|
936
|
+
installationId,
|
|
937
|
+
operation,
|
|
938
|
+
request.bindingId,
|
|
939
|
+
request.timestamp,
|
|
940
|
+
request.nonce,
|
|
941
|
+
request.payload
|
|
942
|
+
].join("\n");
|
|
943
|
+
}
|
|
944
|
+
var PUSH_API_URL, PushKindSchema, PushPreferencesSchema, PushPublicKeySchema, PushInstallationSchema, PushRegistrationSchema, PushEnrollmentSchema, PushGrantSchema, PushPullRequestUrlSchema, PushEventSchema, PushSignedRequestSchema, PushEntitlementSchema, PushServerStatusSchema;
|
|
945
|
+
var init_push = __esm({
|
|
946
|
+
"../../packages/contracts/src/push.ts"() {
|
|
947
|
+
"use strict";
|
|
948
|
+
PUSH_API_URL = "https://api.agentrific.com";
|
|
949
|
+
PushKindSchema = z6.enum([
|
|
950
|
+
"started",
|
|
951
|
+
"completed",
|
|
952
|
+
"needs_input",
|
|
953
|
+
"prepared",
|
|
954
|
+
"drafting_pr",
|
|
955
|
+
"pr_created",
|
|
956
|
+
"failed"
|
|
957
|
+
]);
|
|
958
|
+
PushPreferencesSchema = z6.array(PushKindSchema).max(7);
|
|
959
|
+
PushPublicKeySchema = z6.string().regex(/^[a-f0-9]{64}$/);
|
|
960
|
+
PushInstallationSchema = z6.object({ installationId: z6.string().uuid(), token: z6.string().regex(/^[a-f0-9]{64}$/) }).strict();
|
|
961
|
+
PushRegistrationSchema = z6.object({
|
|
962
|
+
expoToken: z6.string().regex(/^(Expo|Exponent)PushToken\[[A-Za-z0-9_-]+\]$/),
|
|
963
|
+
preferences: PushPreferencesSchema
|
|
964
|
+
}).strict();
|
|
965
|
+
PushEnrollmentSchema = z6.object({ serverId: z6.string().uuid(), publicKey: PushPublicKeySchema }).strict();
|
|
966
|
+
PushGrantSchema = PushEnrollmentSchema.extend({
|
|
967
|
+
installationId: z6.string().uuid(),
|
|
968
|
+
bindingId: z6.string().uuid(),
|
|
969
|
+
grant: z6.string().regex(/^[a-f0-9]{64}$/),
|
|
970
|
+
expiresAt: z6.number()
|
|
971
|
+
}).strict();
|
|
972
|
+
PushPullRequestUrlSchema = z6.string().max(512).regex(/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9][0-9]*$/);
|
|
973
|
+
PushEventSchema = z6.object({
|
|
974
|
+
eventId: z6.string().uuid(),
|
|
975
|
+
sessionId: z6.string().uuid(),
|
|
976
|
+
kind: PushKindSchema,
|
|
977
|
+
taskTitle: z6.string().trim().min(1).max(160).optional(),
|
|
978
|
+
prUrl: PushPullRequestUrlSchema.optional()
|
|
979
|
+
}).strict();
|
|
980
|
+
PushSignedRequestSchema = z6.object({
|
|
981
|
+
bindingId: z6.string().uuid(),
|
|
982
|
+
timestamp: z6.number().int(),
|
|
983
|
+
nonce: z6.string().uuid(),
|
|
984
|
+
payload: z6.string().max(2048),
|
|
985
|
+
signature: z6.string().regex(/^[a-f0-9]{128}$/)
|
|
986
|
+
}).strict();
|
|
987
|
+
PushEntitlementSchema = z6.object({ active: z6.boolean(), expiresAt: z6.number().nullable() }).strict();
|
|
988
|
+
PushServerStatusSchema = z6.object({
|
|
989
|
+
publicKey: PushPublicKeySchema,
|
|
990
|
+
bindings: z6.array(
|
|
991
|
+
z6.object({
|
|
992
|
+
bindingId: z6.string().uuid(),
|
|
993
|
+
installationId: z6.string().uuid(),
|
|
994
|
+
deviceId: z6.string().uuid()
|
|
995
|
+
})
|
|
996
|
+
),
|
|
997
|
+
pending: z6.number().int().nonnegative(),
|
|
998
|
+
error: z6.string().nullable()
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
// src/push.ts
|
|
1004
|
+
import {
|
|
1005
|
+
generateKeyPairSync,
|
|
1006
|
+
createPrivateKey,
|
|
1007
|
+
createPublicKey,
|
|
1008
|
+
sign,
|
|
1009
|
+
randomUUID
|
|
1010
|
+
} from "node:crypto";
|
|
1011
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1012
|
+
import path4 from "node:path";
|
|
1013
|
+
import { Effect as Effect3 } from "effect";
|
|
1014
|
+
function pushKind(session, previous) {
|
|
1015
|
+
if (session.gitDelivery?.status === "pr_opened" && previous?.gitDelivery?.status !== "pr_opened")
|
|
1016
|
+
return "pr_created";
|
|
1017
|
+
if (["merged", "no_changes"].includes(session.gitDelivery?.status ?? "") && session.gitDelivery?.status !== previous?.gitDelivery?.status)
|
|
1018
|
+
return "completed";
|
|
1019
|
+
if (session.gitDelivery?.status === "failed" && previous?.gitDelivery?.status !== "failed")
|
|
1020
|
+
return "failed";
|
|
1021
|
+
if (session.gitDelivery?.stage === "opening_pr" && previous?.gitDelivery?.stage !== "opening_pr")
|
|
1022
|
+
return "drafting_pr";
|
|
1023
|
+
if (session.preparation?.status === "ready" && previous?.preparation?.status !== "ready")
|
|
1024
|
+
return "prepared";
|
|
1025
|
+
if (session.status === previous?.status) return void 0;
|
|
1026
|
+
switch (session.status) {
|
|
1027
|
+
case "running":
|
|
1028
|
+
return "started";
|
|
1029
|
+
case "completed":
|
|
1030
|
+
return session.gitFlow ? void 0 : "completed";
|
|
1031
|
+
case "waiting_for_input":
|
|
1032
|
+
return "needs_input";
|
|
1033
|
+
case "failed":
|
|
1034
|
+
return "failed";
|
|
1035
|
+
default:
|
|
1036
|
+
return void 0;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function registerPushRoutes(app2, push2, security2) {
|
|
1040
|
+
function deviceId(authorization) {
|
|
1041
|
+
const device = security2.authenticate((authorization ?? "").slice(7));
|
|
1042
|
+
if (!device) throw new Error("Pair this device");
|
|
1043
|
+
return device.id;
|
|
1044
|
+
}
|
|
1045
|
+
app2.get("/api/push", (request) => push2.status(deviceId(request.headers.authorization)));
|
|
1046
|
+
app2.post("/api/push/bind", async (request, reply) => {
|
|
1047
|
+
const grant = PushGrantSchema.safeParse(request.body);
|
|
1048
|
+
if (!grant.success) return reply.code(400).send({ error: "Invalid notification enrolment" });
|
|
1049
|
+
try {
|
|
1050
|
+
return await push2.bind(grant.data, deviceId(request.headers.authorization));
|
|
1051
|
+
} catch {
|
|
1052
|
+
return reply.code(400).send({ error: "Could not enable notifications. Check your subscription and try again." });
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
app2.delete("/api/push/bind", (request) => {
|
|
1056
|
+
push2.unbind(deviceId(request.headers.authorization));
|
|
1057
|
+
return { disabled: true };
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
var ServerPush;
|
|
1061
|
+
var init_push2 = __esm({
|
|
1062
|
+
"src/push.ts"() {
|
|
1063
|
+
"use strict";
|
|
1064
|
+
init_push();
|
|
1065
|
+
ServerPush = class {
|
|
1066
|
+
constructor(store2, serverId, directory, transport = fetch, apiUrl = PUSH_API_URL) {
|
|
1067
|
+
this.store = store2;
|
|
1068
|
+
this.serverId = serverId;
|
|
1069
|
+
this.transport = transport;
|
|
1070
|
+
this.apiUrl = apiUrl;
|
|
1071
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
1072
|
+
const filename = path4.join(directory, "push-signing-key.pem");
|
|
1073
|
+
if (!existsSync(filename))
|
|
1074
|
+
writeFileSync(
|
|
1075
|
+
filename,
|
|
1076
|
+
generateKeyPairSync("ed25519").privateKey.export({ format: "pem", type: "pkcs8" }),
|
|
1077
|
+
{ mode: 384, flag: "wx" }
|
|
1078
|
+
);
|
|
1079
|
+
this.key = createPrivateKey(readFileSync(filename));
|
|
1080
|
+
this.publicKey = createPublicKey(this.key).export({ format: "der", type: "spki" }).subarray(-32).toString("hex");
|
|
1081
|
+
}
|
|
1082
|
+
store;
|
|
1083
|
+
serverId;
|
|
1084
|
+
transport;
|
|
1085
|
+
apiUrl;
|
|
1086
|
+
key;
|
|
1087
|
+
publicKey;
|
|
1088
|
+
draining;
|
|
1089
|
+
timer;
|
|
1090
|
+
stopped = false;
|
|
1091
|
+
pending(session, previous) {
|
|
1092
|
+
const kind = pushKind(session, previous);
|
|
1093
|
+
const state = Effect3.runSync(this.store.pushState());
|
|
1094
|
+
if (!kind || !state.bindings.length) return void 0;
|
|
1095
|
+
return {
|
|
1096
|
+
eventId: randomUUID(),
|
|
1097
|
+
sessionId: session.id,
|
|
1098
|
+
kind,
|
|
1099
|
+
taskTitle: session.title.replace(/[\r\n\t]+/g, " ").trim().slice(0, 160) || void 0,
|
|
1100
|
+
prUrl: kind === "pr_created" ? PushPullRequestUrlSchema.safeParse(session.gitDelivery?.url).data : void 0,
|
|
1101
|
+
remaining: state.bindings,
|
|
1102
|
+
createdAt: Date.now(),
|
|
1103
|
+
attempts: 0,
|
|
1104
|
+
nextAt: Date.now()
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
status(deviceId) {
|
|
1108
|
+
const state = Effect3.runSync(this.store.pushState());
|
|
1109
|
+
return {
|
|
1110
|
+
publicKey: this.publicKey,
|
|
1111
|
+
bindings: state.bindings.filter((binding) => binding.deviceId === deviceId),
|
|
1112
|
+
pending: Effect3.runSync(this.store.pendingPush()).length,
|
|
1113
|
+
error: state.error
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
async send(installationId, bindingId, operation, payload) {
|
|
1117
|
+
const request = { bindingId, timestamp: Date.now(), nonce: randomUUID(), payload };
|
|
1118
|
+
const signature = sign(
|
|
1119
|
+
null,
|
|
1120
|
+
Buffer.from(pushSigningText(installationId, operation, request)),
|
|
1121
|
+
this.key
|
|
1122
|
+
).toString("hex");
|
|
1123
|
+
return this.transport(`${this.apiUrl}/v1/installations/${installationId}/${operation}`, {
|
|
1124
|
+
method: "POST",
|
|
1125
|
+
headers: { "Content-Type": "application/json" },
|
|
1126
|
+
body: JSON.stringify({ ...request, signature }),
|
|
1127
|
+
signal: AbortSignal.timeout(1e4),
|
|
1128
|
+
redirect: "error"
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
async bind(grant, deviceId) {
|
|
1132
|
+
if (grant.serverId !== this.serverId || grant.publicKey !== this.publicKey || grant.expiresAt <= Date.now())
|
|
1133
|
+
throw new Error("Grant does not match this server or has expired");
|
|
1134
|
+
const existing = Effect3.runSync(this.store.pushState());
|
|
1135
|
+
if (existing.bindings.length >= 10 && !existing.bindings.some((binding) => binding.deviceId === deviceId))
|
|
1136
|
+
throw new Error("Server notification binding limit reached");
|
|
1137
|
+
const response = await this.send(
|
|
1138
|
+
grant.installationId,
|
|
1139
|
+
grant.bindingId,
|
|
1140
|
+
"redeem",
|
|
1141
|
+
JSON.stringify({ grant: grant.grant })
|
|
1142
|
+
);
|
|
1143
|
+
if (!response.ok)
|
|
1144
|
+
throw new Error("Notification enrolment failed; enable push again to obtain a fresh grant");
|
|
1145
|
+
const state = Effect3.runSync(this.store.pushState());
|
|
1146
|
+
state.bindings = state.bindings.filter((binding) => binding.deviceId !== deviceId);
|
|
1147
|
+
state.bindings.push({
|
|
1148
|
+
installationId: grant.installationId,
|
|
1149
|
+
bindingId: grant.bindingId,
|
|
1150
|
+
deviceId
|
|
1151
|
+
});
|
|
1152
|
+
state.error = null;
|
|
1153
|
+
await Effect3.runPromise(this.store.savePushState(state));
|
|
1154
|
+
return this.status(deviceId);
|
|
1155
|
+
}
|
|
1156
|
+
unbind(deviceId) {
|
|
1157
|
+
const state = Effect3.runSync(this.store.pushState());
|
|
1158
|
+
state.bindings = state.bindings.filter((binding) => binding.deviceId !== deviceId);
|
|
1159
|
+
Effect3.runSync(this.store.savePushState(state));
|
|
1160
|
+
}
|
|
1161
|
+
start() {
|
|
1162
|
+
this.timer = setInterval(() => void this.flush(), 5e3);
|
|
1163
|
+
this.timer.unref();
|
|
1164
|
+
void this.flush();
|
|
1165
|
+
}
|
|
1166
|
+
flush() {
|
|
1167
|
+
if (this.stopped) return Promise.resolve();
|
|
1168
|
+
this.draining ??= this.drain().catch(() => {
|
|
1169
|
+
const state = Effect3.runSync(this.store.pushState());
|
|
1170
|
+
state.error = "Notification delivery temporarily unavailable";
|
|
1171
|
+
Effect3.runSync(this.store.savePushState(state));
|
|
1172
|
+
}).finally(() => {
|
|
1173
|
+
this.draining = void 0;
|
|
1174
|
+
});
|
|
1175
|
+
return this.draining;
|
|
1176
|
+
}
|
|
1177
|
+
async drain() {
|
|
1178
|
+
for (const job of Effect3.runSync(this.store.pendingPush())) {
|
|
1179
|
+
if (job.createdAt + 36e5 < Date.now() || job.attempts >= 12) {
|
|
1180
|
+
Effect3.runSync(this.store.removePush(job.eventId));
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (job.nextAt > Date.now()) continue;
|
|
1184
|
+
const remaining = [];
|
|
1185
|
+
for (const binding of job.remaining) {
|
|
1186
|
+
const state2 = Effect3.runSync(this.store.pushState());
|
|
1187
|
+
const device = Effect3.runSync(this.store.devices()).find(
|
|
1188
|
+
(device2) => device2.id === binding.deviceId
|
|
1189
|
+
);
|
|
1190
|
+
if (!device || device.revokedAt !== null || !state2.bindings.some((current) => current.bindingId === binding.bindingId))
|
|
1191
|
+
continue;
|
|
1192
|
+
try {
|
|
1193
|
+
const response = await this.send(
|
|
1194
|
+
binding.installationId,
|
|
1195
|
+
binding.bindingId,
|
|
1196
|
+
"events",
|
|
1197
|
+
JSON.stringify({
|
|
1198
|
+
eventId: job.eventId,
|
|
1199
|
+
sessionId: job.sessionId,
|
|
1200
|
+
kind: job.kind,
|
|
1201
|
+
taskTitle: job.taskTitle,
|
|
1202
|
+
prUrl: job.prUrl
|
|
1203
|
+
})
|
|
1204
|
+
);
|
|
1205
|
+
if (response.ok) continue;
|
|
1206
|
+
if ([401, 403, 404].includes(response.status)) {
|
|
1207
|
+
this.unbind(binding.deviceId);
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
remaining.push(binding);
|
|
1211
|
+
} catch {
|
|
1212
|
+
remaining.push(binding);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (!remaining.length) Effect3.runSync(this.store.removePush(job.eventId));
|
|
1216
|
+
else
|
|
1217
|
+
Effect3.runSync(
|
|
1218
|
+
this.store.savePendingPush({
|
|
1219
|
+
...job,
|
|
1220
|
+
remaining,
|
|
1221
|
+
attempts: job.attempts + 1,
|
|
1222
|
+
nextAt: Date.now() + Math.min(3e5, 5e3 * 2 ** job.attempts)
|
|
1223
|
+
})
|
|
1224
|
+
);
|
|
1225
|
+
const state = Effect3.runSync(this.store.pushState());
|
|
1226
|
+
state.error = remaining.length ? "Notification delivery pending; check subscription and connectivity" : null;
|
|
1227
|
+
Effect3.runSync(this.store.savePushState(state));
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async close() {
|
|
1231
|
+
this.stopped = true;
|
|
1232
|
+
clearInterval(this.timer);
|
|
1233
|
+
await this.draining;
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
|
|
730
1239
|
// src/attachments.ts
|
|
731
|
-
import { randomUUID } from "node:crypto";
|
|
1240
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
732
1241
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
733
1242
|
import { mkdir as mkdir3, readFile as readFile2, rm, stat as stat2, readdir } from "node:fs/promises";
|
|
734
|
-
import
|
|
1243
|
+
import path5 from "node:path";
|
|
735
1244
|
import { pipeline } from "node:stream/promises";
|
|
736
1245
|
import sharp from "sharp";
|
|
737
|
-
import { Context, Data, Effect as
|
|
738
|
-
import { z as
|
|
1246
|
+
import { Context, Data, Effect as Effect4, Layer } from "effect";
|
|
1247
|
+
import { z as z7 } from "zod";
|
|
739
1248
|
var AttachmentError, textExtensions, imageMimes, AttachmentFiles, Attachments;
|
|
740
1249
|
var init_attachments = __esm({
|
|
741
1250
|
"src/attachments.ts"() {
|
|
@@ -790,10 +1299,10 @@ var init_attachments = __esm({
|
|
|
790
1299
|
root;
|
|
791
1300
|
store;
|
|
792
1301
|
directory(id) {
|
|
793
|
-
return
|
|
1302
|
+
return path5.join(this.root, z7.string().uuid().parse(id));
|
|
794
1303
|
}
|
|
795
1304
|
io(operation) {
|
|
796
|
-
return
|
|
1305
|
+
return Effect4.tryPromise({
|
|
797
1306
|
try: operation,
|
|
798
1307
|
catch: (cause) => new AttachmentError({
|
|
799
1308
|
message: cause instanceof Error ? cause.message : "Attachment operation failed."
|
|
@@ -802,12 +1311,12 @@ var init_attachments = __esm({
|
|
|
802
1311
|
}
|
|
803
1312
|
upload(projectId, part) {
|
|
804
1313
|
return this.io(async () => {
|
|
805
|
-
const id =
|
|
1314
|
+
const id = randomUUID2();
|
|
806
1315
|
const directory = this.directory(id);
|
|
807
|
-
const source =
|
|
808
|
-
const nameField =
|
|
1316
|
+
const source = path5.join(directory, "source");
|
|
1317
|
+
const nameField = z7.object({ type: z7.literal("field"), value: z7.string() }).safeParse(part.fields.name);
|
|
809
1318
|
const originalName = nameField.success ? nameField.data.value : part.filename;
|
|
810
|
-
const name =
|
|
1319
|
+
const name = path5.basename(originalName.replaceAll("\\", "/")).replace(new RegExp("\\p{Cc}", "gu"), "").slice(0, 180) || "attachment";
|
|
811
1320
|
await mkdir3(directory, { recursive: true });
|
|
812
1321
|
try {
|
|
813
1322
|
await pipeline(part.file, createWriteStream(source, { flags: "wx" }));
|
|
@@ -823,7 +1332,7 @@ var init_attachments = __esm({
|
|
|
823
1332
|
...metadata,
|
|
824
1333
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
825
1334
|
});
|
|
826
|
-
await
|
|
1335
|
+
await Effect4.runPromise(this.store.saveAttachment(attachment));
|
|
827
1336
|
return attachment;
|
|
828
1337
|
} catch (error) {
|
|
829
1338
|
await rm(directory, { recursive: true, force: true });
|
|
@@ -835,10 +1344,10 @@ var init_attachments = __esm({
|
|
|
835
1344
|
const metadata = await sharp(source, { limitInputPixels: 4e7 }).metadata().catch(() => null);
|
|
836
1345
|
const mimeType = metadata?.format && imageMimes.get(metadata.format);
|
|
837
1346
|
if (mimeType) {
|
|
838
|
-
await sharp(source, { limitInputPixels: 4e7 }).rotate().resize({ width: 2048, height: 2048, fit: "inside", withoutEnlargement: true }).png().toFile(
|
|
1347
|
+
await sharp(source, { limitInputPixels: 4e7 }).rotate().resize({ width: 2048, height: 2048, fit: "inside", withoutEnlargement: true }).png().toFile(path5.join(path5.dirname(source), "preview.png"));
|
|
839
1348
|
return { kind: "image", mimeType };
|
|
840
1349
|
}
|
|
841
|
-
if (!textExtensions.has(
|
|
1350
|
+
if (!textExtensions.has(path5.extname(name).toLowerCase()))
|
|
842
1351
|
throw new Error(
|
|
843
1352
|
"Choose a PNG, JPEG, WebP, GIF, or text/code file. PDF and Office documents aren't supported yet."
|
|
844
1353
|
);
|
|
@@ -854,12 +1363,12 @@ var init_attachments = __esm({
|
|
|
854
1363
|
throw new Error("Choose up to five different files.");
|
|
855
1364
|
const attachments2 = [];
|
|
856
1365
|
for (const id of ids) {
|
|
857
|
-
const attachment = await
|
|
1366
|
+
const attachment = await Effect4.runPromise(this.store.getAttachment(id));
|
|
858
1367
|
if (!attachment || attachment.projectId !== projectId)
|
|
859
1368
|
throw new Error(
|
|
860
1369
|
"An attachment is unavailable for this project. Remove it and upload it again."
|
|
861
1370
|
);
|
|
862
|
-
await stat2(
|
|
1371
|
+
await stat2(path5.join(this.directory(id), "source"));
|
|
863
1372
|
attachments2.push(attachment);
|
|
864
1373
|
}
|
|
865
1374
|
return attachments2;
|
|
@@ -867,7 +1376,7 @@ var init_attachments = __esm({
|
|
|
867
1376
|
}
|
|
868
1377
|
content(attachment, preview) {
|
|
869
1378
|
return createReadStream(
|
|
870
|
-
|
|
1379
|
+
path5.join(
|
|
871
1380
|
this.directory(attachment.id),
|
|
872
1381
|
preview && attachment.kind === "image" ? "preview.png" : "source"
|
|
873
1382
|
)
|
|
@@ -878,9 +1387,9 @@ var init_attachments = __esm({
|
|
|
878
1387
|
const images = [];
|
|
879
1388
|
const sections = [];
|
|
880
1389
|
for (const attachment of attachments2) {
|
|
881
|
-
const source =
|
|
1390
|
+
const source = path5.join(this.directory(attachment.id), "source");
|
|
882
1391
|
if (attachment.kind === "image") {
|
|
883
|
-
const bytes = await readFile2(
|
|
1392
|
+
const bytes = await readFile2(path5.join(this.directory(attachment.id), "preview.png"));
|
|
884
1393
|
images.push({ type: "image", data: bytes.toString("base64"), mimeType: "image/png" });
|
|
885
1394
|
sections.push(`Image attachment: ${JSON.stringify(attachment.name)}`);
|
|
886
1395
|
} else {
|
|
@@ -903,18 +1412,18 @@ ${text.slice(0, 12e3)}${text.length > 12e3 ? "\n[Preview truncated; use the read
|
|
|
903
1412
|
cleanup() {
|
|
904
1413
|
return this.io(async () => {
|
|
905
1414
|
const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
|
|
906
|
-
const expired = await
|
|
1415
|
+
const expired = await Effect4.runPromise(
|
|
907
1416
|
this.store.expiredAttachments(new Date(cutoff).toISOString())
|
|
908
1417
|
);
|
|
909
1418
|
for (const attachment of expired) {
|
|
910
|
-
if (await
|
|
1419
|
+
if (await Effect4.runPromise(this.store.deleteAttachment(attachment.id)))
|
|
911
1420
|
await rm(this.directory(attachment.id), { recursive: true, force: true });
|
|
912
1421
|
}
|
|
913
1422
|
await mkdir3(this.root, { recursive: true });
|
|
914
1423
|
for (const entry of await readdir(this.root, { withFileTypes: true })) {
|
|
915
|
-
if (!entry.isDirectory() || !
|
|
1424
|
+
if (!entry.isDirectory() || !z7.string().uuid().safeParse(entry.name).success) continue;
|
|
916
1425
|
const directory = this.directory(entry.name);
|
|
917
|
-
if ((await stat2(directory)).mtimeMs < cutoff && !await
|
|
1426
|
+
if ((await stat2(directory)).mtimeMs < cutoff && !await Effect4.runPromise(this.store.getAttachment(entry.name)))
|
|
918
1427
|
await rm(directory, { recursive: true, force: true });
|
|
919
1428
|
}
|
|
920
1429
|
});
|
|
@@ -1000,24 +1509,24 @@ var init_event_server = __esm({
|
|
|
1000
1509
|
});
|
|
1001
1510
|
|
|
1002
1511
|
// src/security.ts
|
|
1003
|
-
import { createHash, randomBytes, randomInt, randomUUID as
|
|
1004
|
-
import { Effect as
|
|
1005
|
-
import { z as
|
|
1512
|
+
import { createHash, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
1513
|
+
import { Effect as Effect5 } from "effect";
|
|
1514
|
+
import { z as z8 } from "zod";
|
|
1006
1515
|
async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
1007
1516
|
app2.addHook("onRequest", async (request, reply) => {
|
|
1008
|
-
const
|
|
1517
|
+
const path13 = new URL(request.url, "http://localhost");
|
|
1009
1518
|
reply.header("Cache-Control", "no-store");
|
|
1010
1519
|
if (request.method === "OPTIONS") return;
|
|
1011
1520
|
if (request.headers.origin && !allowedOrigins2.includes(request.headers.origin))
|
|
1012
1521
|
return reply.code(403).send({ error: "Browser origin is not allowed." });
|
|
1013
|
-
if (
|
|
1522
|
+
if (path13.pathname === "/health" || path13.pathname === "/api/auth/pair" && request.method === "POST")
|
|
1014
1523
|
return;
|
|
1015
1524
|
const authorization = request.headers.authorization ?? "";
|
|
1016
1525
|
const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
|
|
1017
|
-
const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(
|
|
1018
|
-
const ticket =
|
|
1019
|
-
|
|
1020
|
-
if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket,
|
|
1526
|
+
const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(path13.pathname);
|
|
1527
|
+
const ticket = path13.searchParams.get("ticket");
|
|
1528
|
+
path13.searchParams.delete("ticket");
|
|
1529
|
+
if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path13.pathname + path13.search, false)))
|
|
1021
1530
|
return reply.code(401).send({ error: "Pair this device with your server.", code: "UNAUTHORIZED" });
|
|
1022
1531
|
if (!asset && request.headers["x-agentrific-protocol"] !== String(PROTOCOL_VERSION))
|
|
1023
1532
|
return reply.code(409).send({
|
|
@@ -1041,6 +1550,29 @@ async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
|
1041
1550
|
const result = await security2.pair(input.data, request.ip);
|
|
1042
1551
|
return result ?? reply.code(401).send({ error: "Pairing expired, locked, or invalid. Run agentrific pair again." });
|
|
1043
1552
|
});
|
|
1553
|
+
app2.post("/api/auth/pairing-invitation", (_request, reply) => {
|
|
1554
|
+
const publicUrl = process.env.AGENTRIFIC_PUBLIC_URL;
|
|
1555
|
+
if (!publicUrl)
|
|
1556
|
+
return reply.code(503).send({
|
|
1557
|
+
error: "Configure the server\u2019s public HTTPS URL and restart the server before pairing a device."
|
|
1558
|
+
});
|
|
1559
|
+
let url;
|
|
1560
|
+
try {
|
|
1561
|
+
url = pairingOrigin(publicUrl);
|
|
1562
|
+
} catch {
|
|
1563
|
+
return reply.code(503).send({ error: "The server\u2019s public URL is invalid. Configure a public HTTPS origin." });
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
const grant = security2.createPairing();
|
|
1567
|
+
return {
|
|
1568
|
+
payload: { v: 1, url, serverId: grant.serverId, pairingId: grant.pairingId },
|
|
1569
|
+
pin: grant.pin,
|
|
1570
|
+
expiresAt: grant.expiresAt
|
|
1571
|
+
};
|
|
1572
|
+
} catch {
|
|
1573
|
+
return reply.code(429).send({ error: "Too many pending pairing codes. Wait five minutes and try again." });
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1044
1576
|
app2.get("/api/auth/session", (request) => ({
|
|
1045
1577
|
serverId: security2.serverId,
|
|
1046
1578
|
device: security2.authenticate((request.headers.authorization ?? "").slice(7))
|
|
@@ -1049,8 +1581,8 @@ async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
|
1049
1581
|
ticket: security2.ticket((request.headers.authorization ?? "").slice(7), "/api/events")
|
|
1050
1582
|
}));
|
|
1051
1583
|
app2.post("/api/auth/asset-ticket", (request, reply) => {
|
|
1052
|
-
const input =
|
|
1053
|
-
path:
|
|
1584
|
+
const input = z8.object({
|
|
1585
|
+
path: z8.string().regex(/^\/api\/projects\/[^/?]+\/attachments\/[^/?]+\/content(?:\?preview=1)?$/)
|
|
1054
1586
|
}).safeParse(request.body);
|
|
1055
1587
|
if (!input.success) return reply.code(400).send({ error: "Choose a valid attachment." });
|
|
1056
1588
|
return {
|
|
@@ -1063,6 +1595,7 @@ var init_security = __esm({
|
|
|
1063
1595
|
"src/security.ts"() {
|
|
1064
1596
|
"use strict";
|
|
1065
1597
|
init_src();
|
|
1598
|
+
init_pairing();
|
|
1066
1599
|
secret = () => randomBytes(32).toString("hex");
|
|
1067
1600
|
digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
1068
1601
|
ServerSecurity = class {
|
|
@@ -1112,36 +1645,36 @@ var init_security = __esm({
|
|
|
1112
1645
|
const token = secret();
|
|
1113
1646
|
const now = Date.now();
|
|
1114
1647
|
const device = {
|
|
1115
|
-
id:
|
|
1648
|
+
id: randomUUID3(),
|
|
1116
1649
|
name: input.deviceName,
|
|
1117
1650
|
platform: input.platform,
|
|
1118
1651
|
createdAt: now,
|
|
1119
1652
|
lastSeenAt: now,
|
|
1120
1653
|
revokedAt: null
|
|
1121
1654
|
};
|
|
1122
|
-
await
|
|
1655
|
+
await Effect5.runPromise(this.store.saveDevice(device, digest(token)));
|
|
1123
1656
|
return { deviceId: device.id, deviceToken: token, serverId: this.serverId };
|
|
1124
1657
|
}
|
|
1125
1658
|
authenticate(token) {
|
|
1126
1659
|
if (!/^[a-f0-9]{64}$/.test(token)) return void 0;
|
|
1127
|
-
return
|
|
1660
|
+
return Effect5.runSync(this.store.deviceForToken(digest(token)));
|
|
1128
1661
|
}
|
|
1129
|
-
ticket(token,
|
|
1662
|
+
ticket(token, path13) {
|
|
1130
1663
|
this.prune();
|
|
1131
1664
|
if (!this.authenticate(token) || this.tickets.size >= 1e4) return void 0;
|
|
1132
1665
|
const value = secret();
|
|
1133
|
-
this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path:
|
|
1666
|
+
this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path13 });
|
|
1134
1667
|
return value;
|
|
1135
1668
|
}
|
|
1136
|
-
redeemTicket(value,
|
|
1669
|
+
redeemTicket(value, path13, consume = true) {
|
|
1137
1670
|
this.prune();
|
|
1138
1671
|
const ticket = this.tickets.get(value);
|
|
1139
|
-
if (!ticket || ticket.path !==
|
|
1672
|
+
if (!ticket || ticket.path !== path13) return void 0;
|
|
1140
1673
|
if (consume) this.tickets.delete(value);
|
|
1141
|
-
return
|
|
1674
|
+
return Effect5.runSync(this.store.deviceForToken(ticket.tokenHash));
|
|
1142
1675
|
}
|
|
1143
1676
|
async revoke(id) {
|
|
1144
|
-
const revoked = await
|
|
1677
|
+
const revoked = await Effect5.runPromise(this.store.revokeDevice(id));
|
|
1145
1678
|
for (const [socket, owner] of this.sockets)
|
|
1146
1679
|
if (owner === id) socket.close(4401, "Device revoked");
|
|
1147
1680
|
return revoked;
|
|
@@ -1195,9 +1728,9 @@ var init_maintenance = __esm({
|
|
|
1195
1728
|
}
|
|
1196
1729
|
attach(app2) {
|
|
1197
1730
|
app2.addHook("onRequest", async (request, reply) => {
|
|
1198
|
-
const
|
|
1731
|
+
const path13 = request.url.split("?")[0];
|
|
1199
1732
|
if (["GET", "HEAD", "OPTIONS"].includes(request.method)) return;
|
|
1200
|
-
const allowed =
|
|
1733
|
+
const allowed = path13.startsWith("/api/auth/") || /^\/api\/sessions\/[^/]+\/stop$/.test(path13);
|
|
1201
1734
|
if (this.paused && !allowed)
|
|
1202
1735
|
return reply.code(503).send({ code: "MAINTENANCE", error: new MaintenanceError().message });
|
|
1203
1736
|
this.requests.add(request.id);
|
|
@@ -1211,22 +1744,27 @@ var init_maintenance = __esm({
|
|
|
1211
1744
|
});
|
|
1212
1745
|
|
|
1213
1746
|
// src/tasks.ts
|
|
1214
|
-
import { Effect as
|
|
1747
|
+
import { Effect as Effect6, Semaphore } from "effect";
|
|
1215
1748
|
function withTaskLock(id, operation) {
|
|
1216
1749
|
let lock = locks.get(id);
|
|
1217
1750
|
if (!lock) {
|
|
1218
1751
|
lock = Semaphore.makeUnsafe(1);
|
|
1219
1752
|
locks.set(id, lock);
|
|
1220
1753
|
}
|
|
1221
|
-
return
|
|
1754
|
+
return Effect6.runPromise(
|
|
1222
1755
|
lock.withPermit(
|
|
1223
|
-
|
|
1756
|
+
Effect6.tryPromise({
|
|
1224
1757
|
try: operation,
|
|
1225
1758
|
catch: (cause) => cause
|
|
1226
1759
|
})
|
|
1227
1760
|
)
|
|
1228
1761
|
);
|
|
1229
1762
|
}
|
|
1763
|
+
async function sessionTaskLockId(store2, session) {
|
|
1764
|
+
if (session.taskId) return session.taskId;
|
|
1765
|
+
const tasks = await Effect6.runPromise(store2.listTasks(session.projectId, true));
|
|
1766
|
+
return tasks.find((task) => task.sessionId === session.id)?.id ?? `session-${session.id}`;
|
|
1767
|
+
}
|
|
1230
1768
|
function taskStatusForSession(status, session) {
|
|
1231
1769
|
if (session?.gitDelivery?.status === "pending") return "running";
|
|
1232
1770
|
if (status === "completed" && session?.gitFlow && !["merged", "no_changes", "pr_opened"].includes(session.gitDelivery?.status ?? ""))
|
|
@@ -1263,8 +1801,8 @@ function sessionTask(session) {
|
|
|
1263
1801
|
});
|
|
1264
1802
|
}
|
|
1265
1803
|
async function boardTasks(store2, projectId) {
|
|
1266
|
-
const tasks = await
|
|
1267
|
-
const sessions = await
|
|
1804
|
+
const tasks = await Effect6.runPromise(store2.listTasks(projectId, true));
|
|
1805
|
+
const sessions = await Effect6.runPromise(store2.list());
|
|
1268
1806
|
const linkedSessionIds = new Set(
|
|
1269
1807
|
tasks.flatMap((task) => task.sessionId ? [task.sessionId] : [])
|
|
1270
1808
|
);
|
|
@@ -1283,10 +1821,10 @@ async function boardTasks(store2, projectId) {
|
|
|
1283
1821
|
return [...linkedTasks, ...legacyTasks].map((task) => TaskSchema.parse(task)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
1284
1822
|
}
|
|
1285
1823
|
async function findTask(store2, id) {
|
|
1286
|
-
const saved = await
|
|
1824
|
+
const saved = await Effect6.runPromise(store2.getTask(id));
|
|
1287
1825
|
if (saved?.deletedAt) throw new TaskActionError("Task not found.", 404);
|
|
1288
1826
|
if (saved) return saved;
|
|
1289
|
-
const session = id.startsWith("session-") ? await
|
|
1827
|
+
const session = id.startsWith("session-") ? await Effect6.runPromise(store2.get(id.slice("session-".length))) : void 0;
|
|
1290
1828
|
if (!session || session.taskId) throw new TaskActionError("Task not found.", 404);
|
|
1291
1829
|
return sessionTask(session);
|
|
1292
1830
|
}
|
|
@@ -1302,7 +1840,7 @@ function editTask(store2, id, input, resolveAttachments) {
|
|
|
1302
1840
|
throw new TaskActionError("Attachment validation is unavailable.", 400);
|
|
1303
1841
|
files = await resolveAttachments(task.projectId, attachmentIds);
|
|
1304
1842
|
}
|
|
1305
|
-
return
|
|
1843
|
+
return Effect6.runPromise(
|
|
1306
1844
|
store2.saveTask({
|
|
1307
1845
|
...task,
|
|
1308
1846
|
...updates2,
|
|
@@ -1313,16 +1851,25 @@ function editTask(store2, id, input, resolveAttachments) {
|
|
|
1313
1851
|
);
|
|
1314
1852
|
});
|
|
1315
1853
|
}
|
|
1316
|
-
function deleteTask(store2, id) {
|
|
1854
|
+
function deleteTask(store2, id, cleanup = async () => {
|
|
1855
|
+
}) {
|
|
1317
1856
|
return withTaskLock(id, async () => {
|
|
1318
1857
|
const task = await findTask(store2, id);
|
|
1319
|
-
const sessions = await
|
|
1858
|
+
const sessions = await Effect6.runPromise(store2.list());
|
|
1320
1859
|
if (sessions.some(
|
|
1321
1860
|
(session) => (session.taskId === task.id || session.id === task.sessionId) && (["starting", "running", "queued"].includes(session.status) || session.gitDelivery?.status === "pending")
|
|
1322
1861
|
))
|
|
1323
1862
|
throw new TaskActionError("Stop the task's running agent before deleting it.", 409);
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1863
|
+
const associated = sessions.filter(
|
|
1864
|
+
(session) => session.taskId === task.id || session.id === task.sessionId
|
|
1865
|
+
);
|
|
1866
|
+
for (const session of associated) await cleanup(session);
|
|
1867
|
+
await Effect6.runPromise(
|
|
1868
|
+
store2.purgeTask(
|
|
1869
|
+
task.id,
|
|
1870
|
+
associated.map((session) => session.id)
|
|
1871
|
+
)
|
|
1872
|
+
);
|
|
1326
1873
|
});
|
|
1327
1874
|
}
|
|
1328
1875
|
var TaskActionError, locks;
|
|
@@ -1342,7 +1889,7 @@ var init_tasks = __esm({
|
|
|
1342
1889
|
});
|
|
1343
1890
|
|
|
1344
1891
|
// src/automation.ts
|
|
1345
|
-
import { Effect as
|
|
1892
|
+
import { Effect as Effect7 } from "effect";
|
|
1346
1893
|
function activeAgents(sessions) {
|
|
1347
1894
|
return sessions.filter(
|
|
1348
1895
|
(session) => ["starting", "running", "queued"].includes(session.status) || session.gitDelivery?.status === "pending"
|
|
@@ -1371,18 +1918,18 @@ var init_automation = __esm({
|
|
|
1371
1918
|
start;
|
|
1372
1919
|
canStart;
|
|
1373
1920
|
async snapshot() {
|
|
1374
|
-
const state = await
|
|
1921
|
+
const state = await Effect7.runPromise(this.store.automation());
|
|
1375
1922
|
return {
|
|
1376
1923
|
...state,
|
|
1377
1924
|
queue: automationQueue(await boardTasks(this.store), state),
|
|
1378
|
-
active: activeAgents(await
|
|
1925
|
+
active: activeAgents(await Effect7.runPromise(this.store.list()))
|
|
1379
1926
|
};
|
|
1380
1927
|
}
|
|
1381
1928
|
// ponytail: one server process owns this SQLite database; use a durable lease before adding replicas.
|
|
1382
1929
|
configure(config) {
|
|
1383
1930
|
return withTaskLock("automation", async () => {
|
|
1384
|
-
const state = await
|
|
1385
|
-
await
|
|
1931
|
+
const state = await Effect7.runPromise(this.store.automation());
|
|
1932
|
+
await Effect7.runPromise(this.store.saveAutomation({ ...state, config, error: null }));
|
|
1386
1933
|
return this.snapshot();
|
|
1387
1934
|
});
|
|
1388
1935
|
}
|
|
@@ -1394,7 +1941,7 @@ var init_automation = __esm({
|
|
|
1394
1941
|
throw new TaskActionError("Queue changed. Refresh and try again.", 409);
|
|
1395
1942
|
const order = ids.filter((id) => id !== taskId);
|
|
1396
1943
|
order.splice(beforeId === null ? order.length : order.indexOf(beforeId), 0, taskId);
|
|
1397
|
-
await
|
|
1944
|
+
await Effect7.runPromise(
|
|
1398
1945
|
this.store.saveAutomation({
|
|
1399
1946
|
config: snapshot.config,
|
|
1400
1947
|
lastMinute: snapshot.lastMinute,
|
|
@@ -1407,22 +1954,22 @@ var init_automation = __esm({
|
|
|
1407
1954
|
}
|
|
1408
1955
|
tick(now = /* @__PURE__ */ new Date()) {
|
|
1409
1956
|
return withTaskLock("automation", async () => {
|
|
1410
|
-
let state = await
|
|
1957
|
+
let state = await Effect7.runPromise(this.store.automation());
|
|
1411
1958
|
const minute = now.toISOString().slice(0, 16);
|
|
1412
1959
|
if (!this.canStart() || !state.config.enabled || state.lastMinute !== null && state.lastMinute >= minute || !cronMatches(state.config.cron, now))
|
|
1413
1960
|
return;
|
|
1414
1961
|
state = { ...state, lastMinute: minute, error: null };
|
|
1415
|
-
await
|
|
1962
|
+
await Effect7.runPromise(this.store.saveAutomation(state));
|
|
1416
1963
|
const queue = automationQueue(await boardTasks(this.store), state);
|
|
1417
1964
|
for (const task of queue) {
|
|
1418
1965
|
if (!this.canStart()) break;
|
|
1419
|
-
if (activeAgents(await
|
|
1966
|
+
if (activeAgents(await Effect7.runPromise(this.store.list())) >= state.config.parallelism)
|
|
1420
1967
|
break;
|
|
1421
1968
|
try {
|
|
1422
1969
|
await this.start(task.id);
|
|
1423
1970
|
} catch (error) {
|
|
1424
1971
|
if (error instanceof MaintenanceError) break;
|
|
1425
|
-
await
|
|
1972
|
+
await Effect7.runPromise(
|
|
1426
1973
|
this.store.saveAutomation({
|
|
1427
1974
|
...state,
|
|
1428
1975
|
config: { ...state.config, enabled: false },
|
|
@@ -1453,9 +2000,9 @@ var init_git_flow = __esm({
|
|
|
1453
2000
|
});
|
|
1454
2001
|
|
|
1455
2002
|
// src/runtime.ts
|
|
1456
|
-
import
|
|
1457
|
-
import { randomUUID as
|
|
1458
|
-
import { Context as Context2, Data as Data2, Effect as
|
|
2003
|
+
import path6 from "node:path";
|
|
2004
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
2005
|
+
import { Context as Context2, Data as Data2, Effect as Effect8, Fiber, Layer as Layer2, Semaphore as Semaphore2 } from "effect";
|
|
1459
2006
|
var AgentError, AgentController, AgentSessions;
|
|
1460
2007
|
var init_runtime = __esm({
|
|
1461
2008
|
"src/runtime.ts"() {
|
|
@@ -1500,20 +2047,20 @@ var init_runtime = __esm({
|
|
|
1500
2047
|
this.emit({ type: "session.updated", session: entry.session });
|
|
1501
2048
|
}
|
|
1502
2049
|
operation(action) {
|
|
1503
|
-
return
|
|
2050
|
+
return Effect8.tryPromise({
|
|
1504
2051
|
try: action,
|
|
1505
2052
|
catch: (cause) => new AgentError({ message: String(cause), cause })
|
|
1506
2053
|
});
|
|
1507
2054
|
}
|
|
1508
2055
|
open(session) {
|
|
1509
|
-
return
|
|
2056
|
+
return Effect8.gen({ self: this }, function* () {
|
|
1510
2057
|
const existing = this.active.get(session.id);
|
|
1511
2058
|
if (existing) return existing;
|
|
1512
|
-
const agent = yield*
|
|
2059
|
+
const agent = yield* Effect8.tryPromise({
|
|
1513
2060
|
try: () => this.factory.open(
|
|
1514
2061
|
session.id,
|
|
1515
|
-
session.workspacePath ??
|
|
1516
|
-
|
|
2062
|
+
session.workspacePath ?? path6.join(this.worktreesRoot, session.id),
|
|
2063
|
+
path6.join(this.sessionsRoot, session.id),
|
|
1517
2064
|
session.model,
|
|
1518
2065
|
session
|
|
1519
2066
|
),
|
|
@@ -1536,14 +2083,14 @@ var init_runtime = __esm({
|
|
|
1536
2083
|
});
|
|
1537
2084
|
}
|
|
1538
2085
|
run(entry, message) {
|
|
1539
|
-
return
|
|
2086
|
+
return Effect8.gen({ self: this }, function* () {
|
|
1540
2087
|
let next = message;
|
|
1541
2088
|
while (next !== void 0) {
|
|
1542
2089
|
const current = next;
|
|
1543
2090
|
yield* this.operation(
|
|
1544
2091
|
() => entry.agent.prompt(gitFlowPrompt(entry.session, current.text), current.attachments)
|
|
1545
2092
|
);
|
|
1546
|
-
next = yield*
|
|
2093
|
+
next = yield* Effect8.sync(() => {
|
|
1547
2094
|
const queued = entry.stopping ? void 0 : entry.pending.shift();
|
|
1548
2095
|
if (queued === void 0) {
|
|
1549
2096
|
entry.running = false;
|
|
@@ -1555,19 +2102,19 @@ var init_runtime = __esm({
|
|
|
1555
2102
|
yield* this.operation(() => this.onSettled(entry.session));
|
|
1556
2103
|
}
|
|
1557
2104
|
}).pipe(
|
|
1558
|
-
|
|
1559
|
-
(error) =>
|
|
2105
|
+
Effect8.catch(
|
|
2106
|
+
(error) => Effect8.sync(() => {
|
|
1560
2107
|
entry.pending.length = 0;
|
|
1561
2108
|
entry.running = false;
|
|
1562
2109
|
this.emit({ type: "error", sessionId: entry.session.id, message: error.message });
|
|
1563
2110
|
this.status(entry, entry.stopping ? "stopped" : "failed");
|
|
1564
2111
|
})
|
|
1565
2112
|
),
|
|
1566
|
-
|
|
2113
|
+
Effect8.onInterrupt(
|
|
1567
2114
|
() => this.operation(() => entry.agent.abort()).pipe(
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
2115
|
+
Effect8.orDie,
|
|
2116
|
+
Effect8.ensuring(
|
|
2117
|
+
Effect8.sync(() => {
|
|
1571
2118
|
entry.pending.length = 0;
|
|
1572
2119
|
entry.running = false;
|
|
1573
2120
|
this.status(entry, "stopped");
|
|
@@ -1575,16 +2122,16 @@ var init_runtime = __esm({
|
|
|
1575
2122
|
)
|
|
1576
2123
|
)
|
|
1577
2124
|
),
|
|
1578
|
-
|
|
2125
|
+
Effect8.ensuring(Effect8.sync(() => this.working.delete(entry.session.id)))
|
|
1579
2126
|
);
|
|
1580
2127
|
}
|
|
1581
|
-
send(session, message, attachments2 = [], messageId =
|
|
2128
|
+
send(session, message, attachments2 = [], messageId = randomUUID4()) {
|
|
1582
2129
|
return this.lock(session.id).withPermit(
|
|
1583
|
-
|
|
2130
|
+
Effect8.gen({ self: this }, function* () {
|
|
1584
2131
|
if (this.closed)
|
|
1585
|
-
return yield*
|
|
2132
|
+
return yield* Effect8.fail(new AgentError({ message: "Server is shutting down" }));
|
|
1586
2133
|
if (!session.branch && (session.gitFlow || !session.workspacePath))
|
|
1587
|
-
return yield*
|
|
2134
|
+
return yield* Effect8.fail(new AgentError({ message: "Session checkout is not ready" }));
|
|
1588
2135
|
if (this.hasMessage(session.id, messageId)) return;
|
|
1589
2136
|
const entry = yield* this.open(session);
|
|
1590
2137
|
yield* this.operation(async () => entry.agent.validateAttachments?.(attachments2));
|
|
@@ -1603,7 +2150,7 @@ var init_runtime = __esm({
|
|
|
1603
2150
|
entry.stopping = false;
|
|
1604
2151
|
this.status(entry, "running");
|
|
1605
2152
|
this.working.add(session.id);
|
|
1606
|
-
entry.fiber = yield*
|
|
2153
|
+
entry.fiber = yield* Effect8.forkIn(
|
|
1607
2154
|
this.run(entry, { text: message, attachments: attachments2 }),
|
|
1608
2155
|
this.scope,
|
|
1609
2156
|
{
|
|
@@ -1611,22 +2158,22 @@ var init_runtime = __esm({
|
|
|
1611
2158
|
}
|
|
1612
2159
|
);
|
|
1613
2160
|
})
|
|
1614
|
-
).pipe(
|
|
2161
|
+
).pipe(Effect8.uninterruptible);
|
|
1615
2162
|
}
|
|
1616
2163
|
setModel(session, model) {
|
|
1617
2164
|
return this.lock(session.id).withPermit(
|
|
1618
|
-
|
|
2165
|
+
Effect8.gen({ self: this }, function* () {
|
|
1619
2166
|
if (this.closed)
|
|
1620
|
-
return yield*
|
|
2167
|
+
return yield* Effect8.fail(new AgentError({ message: "Server is shutting down" }));
|
|
1621
2168
|
const entry = yield* this.open(session);
|
|
1622
2169
|
if (entry.running || entry.pending.length)
|
|
1623
|
-
return yield*
|
|
2170
|
+
return yield* Effect8.fail(
|
|
1624
2171
|
new AgentError({
|
|
1625
2172
|
message: "Wait for this reply to finish before changing the model."
|
|
1626
2173
|
})
|
|
1627
2174
|
);
|
|
1628
2175
|
if (!entry.agent.setModel)
|
|
1629
|
-
return yield*
|
|
2176
|
+
return yield* Effect8.fail(
|
|
1630
2177
|
new AgentError({ message: "This agent does not support model selection." })
|
|
1631
2178
|
);
|
|
1632
2179
|
yield* this.operation(() => entry.agent.setModel(model));
|
|
@@ -1638,10 +2185,10 @@ var init_runtime = __esm({
|
|
|
1638
2185
|
this.emit({ type: "session.updated", session: entry.session });
|
|
1639
2186
|
return entry.session;
|
|
1640
2187
|
})
|
|
1641
|
-
).pipe(
|
|
2188
|
+
).pipe(Effect8.uninterruptible);
|
|
1642
2189
|
}
|
|
1643
2190
|
stopEntry(entry) {
|
|
1644
|
-
return
|
|
2191
|
+
return Effect8.gen({ self: this }, function* () {
|
|
1645
2192
|
entry.stopping = true;
|
|
1646
2193
|
entry.pending.length = 0;
|
|
1647
2194
|
yield* this.operation(() => entry.agent.abort());
|
|
@@ -1651,7 +2198,7 @@ var init_runtime = __esm({
|
|
|
1651
2198
|
}
|
|
1652
2199
|
stop(session) {
|
|
1653
2200
|
return this.lock(session.id).withPermit(
|
|
1654
|
-
|
|
2201
|
+
Effect8.gen({ self: this }, function* () {
|
|
1655
2202
|
const entry = this.active.get(session.id);
|
|
1656
2203
|
if (entry) {
|
|
1657
2204
|
yield* this.stopEntry(entry);
|
|
@@ -1662,14 +2209,26 @@ var init_runtime = __esm({
|
|
|
1662
2209
|
});
|
|
1663
2210
|
}
|
|
1664
2211
|
})
|
|
1665
|
-
).pipe(
|
|
2212
|
+
).pipe(Effect8.uninterruptible);
|
|
2213
|
+
}
|
|
2214
|
+
forget(id) {
|
|
2215
|
+
return this.lock(id).withPermit(
|
|
2216
|
+
Effect8.sync(() => {
|
|
2217
|
+
const entry = this.active.get(id);
|
|
2218
|
+
if (!entry) return;
|
|
2219
|
+
if (entry.running) throw new Error("Cannot delete an active run.");
|
|
2220
|
+
entry.unsubscribe();
|
|
2221
|
+
entry.agent.dispose();
|
|
2222
|
+
this.active.delete(id);
|
|
2223
|
+
})
|
|
2224
|
+
);
|
|
1666
2225
|
}
|
|
1667
2226
|
close() {
|
|
1668
|
-
return
|
|
2227
|
+
return Effect8.gen({ self: this }, function* () {
|
|
1669
2228
|
this.closed = true;
|
|
1670
2229
|
for (const [id, lock] of this.locks) {
|
|
1671
2230
|
yield* lock.withPermit(
|
|
1672
|
-
|
|
2231
|
+
Effect8.gen({ self: this }, function* () {
|
|
1673
2232
|
const entry = this.active.get(id);
|
|
1674
2233
|
if (!entry) return;
|
|
1675
2234
|
if (entry.running) yield* this.stopEntry(entry);
|
|
@@ -1689,8 +2248,8 @@ var init_runtime = __esm({
|
|
|
1689
2248
|
}) {
|
|
1690
2249
|
return Layer2.effect(
|
|
1691
2250
|
this,
|
|
1692
|
-
|
|
1693
|
-
const scope = yield*
|
|
2251
|
+
Effect8.gen(function* () {
|
|
2252
|
+
const scope = yield* Effect8.scope;
|
|
1694
2253
|
const controller = new AgentController(
|
|
1695
2254
|
factory,
|
|
1696
2255
|
emit,
|
|
@@ -1700,7 +2259,7 @@ var init_runtime = __esm({
|
|
|
1700
2259
|
hasMessage,
|
|
1701
2260
|
onSettled
|
|
1702
2261
|
);
|
|
1703
|
-
yield*
|
|
2262
|
+
yield* Effect8.addFinalizer(() => controller.close().pipe(Effect8.orDie));
|
|
1704
2263
|
return controller;
|
|
1705
2264
|
})
|
|
1706
2265
|
);
|
|
@@ -1757,17 +2316,17 @@ var init_workflow_command = __esm({
|
|
|
1757
2316
|
});
|
|
1758
2317
|
|
|
1759
2318
|
// src/workflow-schema.ts
|
|
1760
|
-
import { z as
|
|
2319
|
+
import { z as z9 } from "zod";
|
|
1761
2320
|
var DeliveryJobSchema, PreparationJobSchema, WorkflowJobSchema;
|
|
1762
2321
|
var init_workflow_schema = __esm({
|
|
1763
2322
|
"src/workflow-schema.ts"() {
|
|
1764
2323
|
"use strict";
|
|
1765
2324
|
init_src();
|
|
1766
|
-
DeliveryJobSchema =
|
|
1767
|
-
kind:
|
|
1768
|
-
sessionId:
|
|
2325
|
+
DeliveryJobSchema = z9.object({
|
|
2326
|
+
kind: z9.literal("delivery"),
|
|
2327
|
+
sessionId: z9.string(),
|
|
1769
2328
|
request: DeliveryRequestSchema,
|
|
1770
|
-
phase:
|
|
2329
|
+
phase: z9.enum([
|
|
1771
2330
|
"validating",
|
|
1772
2331
|
"drafting",
|
|
1773
2332
|
"committing",
|
|
@@ -1777,25 +2336,25 @@ var init_workflow_schema = __esm({
|
|
|
1777
2336
|
"opening_pr",
|
|
1778
2337
|
"delivered"
|
|
1779
2338
|
]),
|
|
1780
|
-
attempt:
|
|
1781
|
-
commitSha:
|
|
1782
|
-
title:
|
|
1783
|
-
body:
|
|
1784
|
-
logs:
|
|
1785
|
-
failed:
|
|
1786
|
-
result:
|
|
1787
|
-
url:
|
|
1788
|
-
message:
|
|
2339
|
+
attempt: z9.number().int().positive(),
|
|
2340
|
+
commitSha: z9.string().optional(),
|
|
2341
|
+
title: z9.string(),
|
|
2342
|
+
body: z9.string(),
|
|
2343
|
+
logs: z9.string().default(""),
|
|
2344
|
+
failed: z9.boolean().default(false),
|
|
2345
|
+
result: z9.enum(["merged", "pr_opened", "no_changes"]).optional(),
|
|
2346
|
+
url: z9.string().optional(),
|
|
2347
|
+
message: z9.string().optional()
|
|
1789
2348
|
});
|
|
1790
|
-
PreparationJobSchema =
|
|
1791
|
-
kind:
|
|
1792
|
-
sessionId:
|
|
1793
|
-
projectPath:
|
|
2349
|
+
PreparationJobSchema = z9.object({
|
|
2350
|
+
kind: z9.literal("preparation"),
|
|
2351
|
+
sessionId: z9.string(),
|
|
2352
|
+
projectPath: z9.string(),
|
|
1794
2353
|
input: CreateSessionSchema,
|
|
1795
|
-
stage:
|
|
1796
|
-
logs:
|
|
2354
|
+
stage: z9.enum(["creating", "installing", "ready", "failed"]),
|
|
2355
|
+
logs: z9.string().default("")
|
|
1797
2356
|
});
|
|
1798
|
-
WorkflowJobSchema =
|
|
2357
|
+
WorkflowJobSchema = z9.discriminatedUnion("kind", [
|
|
1799
2358
|
DeliveryJobSchema,
|
|
1800
2359
|
PreparationJobSchema
|
|
1801
2360
|
]);
|
|
@@ -1803,8 +2362,8 @@ var init_workflow_schema = __esm({
|
|
|
1803
2362
|
});
|
|
1804
2363
|
|
|
1805
2364
|
// src/delivery.ts
|
|
1806
|
-
import { Effect as
|
|
1807
|
-
import { z as
|
|
2365
|
+
import { Effect as Effect9 } from "effect";
|
|
2366
|
+
import { z as z10 } from "zod";
|
|
1808
2367
|
var DeliveryCoordinator;
|
|
1809
2368
|
var init_delivery = __esm({
|
|
1810
2369
|
"src/delivery.ts"() {
|
|
@@ -1827,20 +2386,20 @@ var init_delivery = __esm({
|
|
|
1827
2386
|
return [...this.workers.keys()].map((id) => `delivery:${id}`);
|
|
1828
2387
|
}
|
|
1829
2388
|
async session(id) {
|
|
1830
|
-
const session = await
|
|
2389
|
+
const session = await Effect9.runPromise(this.store.get(id));
|
|
1831
2390
|
if (!session?.gitFlow || !session.branch || !session.workspacePath)
|
|
1832
2391
|
throw new Error("This run does not have a ready Git flow workspace.");
|
|
1833
2392
|
return session;
|
|
1834
2393
|
}
|
|
1835
2394
|
async job(id) {
|
|
1836
|
-
const raw = await
|
|
2395
|
+
const raw = await Effect9.runPromise(this.store.workflow(`delivery:${id}`));
|
|
1837
2396
|
return raw === void 0 ? void 0 : DeliveryJobSchema.parse(raw);
|
|
1838
2397
|
}
|
|
1839
2398
|
git(session, args, signal) {
|
|
1840
2399
|
return this.command(session.workspacePath, "git", args, signal);
|
|
1841
2400
|
}
|
|
1842
2401
|
async record(job) {
|
|
1843
|
-
await
|
|
2402
|
+
await Effect9.runPromise(this.store.saveWorkflow(`delivery:${job.sessionId}`, job));
|
|
1844
2403
|
const session = await this.session(job.sessionId);
|
|
1845
2404
|
const delivery2 = {
|
|
1846
2405
|
status: job.failed ? "failed" : job.result ?? "pending",
|
|
@@ -2029,7 +2588,7 @@ Agentrific run ${id}.`;
|
|
|
2029
2588
|
return (await this.session(id)).gitDelivery;
|
|
2030
2589
|
}
|
|
2031
2590
|
async recover() {
|
|
2032
|
-
const jobs = await
|
|
2591
|
+
const jobs = await Effect9.runPromise(this.store.workflows());
|
|
2033
2592
|
for (const raw of jobs) {
|
|
2034
2593
|
const parsed = DeliveryJobSchema.safeParse(raw);
|
|
2035
2594
|
if (!parsed.success) continue;
|
|
@@ -2103,11 +2662,11 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2103
2662
|
"--json",
|
|
2104
2663
|
"url,state,headRefOid"
|
|
2105
2664
|
]);
|
|
2106
|
-
const prs =
|
|
2107
|
-
|
|
2108
|
-
url:
|
|
2109
|
-
state:
|
|
2110
|
-
headRefOid:
|
|
2665
|
+
const prs = z10.array(
|
|
2666
|
+
z10.object({
|
|
2667
|
+
url: z10.string().url(),
|
|
2668
|
+
state: z10.string(),
|
|
2669
|
+
headRefOid: z10.string().optional()
|
|
2111
2670
|
})
|
|
2112
2671
|
).parse(JSON.parse(existing));
|
|
2113
2672
|
const open = prs.find(
|
|
@@ -2151,9 +2710,9 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2151
2710
|
|
|
2152
2711
|
// src/preparation.ts
|
|
2153
2712
|
import { mkdir as mkdir4, readFile as readFile3 } from "node:fs/promises";
|
|
2154
|
-
import
|
|
2155
|
-
import { Effect as
|
|
2156
|
-
import { z as
|
|
2713
|
+
import path7 from "node:path";
|
|
2714
|
+
import { Effect as Effect10 } from "effect";
|
|
2715
|
+
import { z as z11 } from "zod";
|
|
2157
2716
|
var PreparationCoordinator;
|
|
2158
2717
|
var init_preparation = __esm({
|
|
2159
2718
|
"src/preparation.ts"() {
|
|
@@ -2190,8 +2749,8 @@ var init_preparation = __esm({
|
|
|
2190
2749
|
job.logs = workflowLog(`${job.logs}
|
|
2191
2750
|
${message}${logs ? `
|
|
2192
2751
|
${logs}` : ""}`);
|
|
2193
|
-
await
|
|
2194
|
-
const current = await
|
|
2752
|
+
await Effect10.runPromise(this.store.saveWorkflow(`preparation:${session.id}`, job));
|
|
2753
|
+
const current = await Effect10.runPromise(this.store.get(session.id));
|
|
2195
2754
|
this.options.publish({
|
|
2196
2755
|
type: "session.updated",
|
|
2197
2756
|
session: {
|
|
@@ -2210,7 +2769,7 @@ ${logs}` : ""}`);
|
|
|
2210
2769
|
await record("installing", "Installing run dependencies\u2026");
|
|
2211
2770
|
await this.install(workspace, command);
|
|
2212
2771
|
await record("ready", "Workspace ready.");
|
|
2213
|
-
return await
|
|
2772
|
+
return await Effect10.runPromise(this.store.get(session.id));
|
|
2214
2773
|
} catch (error) {
|
|
2215
2774
|
const step = job.stage === "installing" ? "Dependency installation failed" : "Workspace preparation failed";
|
|
2216
2775
|
await record(
|
|
@@ -2254,7 +2813,7 @@ ${logs}` : ""}`);
|
|
|
2254
2813
|
const workspace = session.workspacePath;
|
|
2255
2814
|
if (branchAtWorkspace !== void 0)
|
|
2256
2815
|
throw new Error("The run workspace already exists on a different branch.");
|
|
2257
|
-
await mkdir4(
|
|
2816
|
+
await mkdir4(path7.dirname(workspace), { recursive: true });
|
|
2258
2817
|
try {
|
|
2259
2818
|
await command(
|
|
2260
2819
|
projectPath,
|
|
@@ -2274,12 +2833,12 @@ ${logs}` : ""}`);
|
|
|
2274
2833
|
}
|
|
2275
2834
|
}
|
|
2276
2835
|
async install(workspace, command) {
|
|
2277
|
-
const packageFile = await readFile3(
|
|
2836
|
+
const packageFile = await readFile3(path7.join(workspace, "package.json"), "utf8").catch(
|
|
2278
2837
|
() => void 0
|
|
2279
2838
|
);
|
|
2280
2839
|
if (!packageFile) return;
|
|
2281
|
-
const metadata =
|
|
2282
|
-
const has = async (name) => readFile3(
|
|
2840
|
+
const metadata = z11.object({ packageManager: z11.string().optional() }).parse(JSON.parse(packageFile));
|
|
2841
|
+
const has = async (name) => readFile3(path7.join(workspace, name)).then(
|
|
2283
2842
|
() => true,
|
|
2284
2843
|
() => false
|
|
2285
2844
|
);
|
|
@@ -2292,10 +2851,10 @@ ${logs}` : ""}`);
|
|
|
2292
2851
|
else if (await has("package-lock.json")) await command(workspace, "npm", ["ci"]);
|
|
2293
2852
|
}
|
|
2294
2853
|
async retry(id) {
|
|
2295
|
-
const raw = await
|
|
2854
|
+
const raw = await Effect10.runPromise(this.store.workflow(`preparation:${id}`));
|
|
2296
2855
|
const job = PreparationJobSchema.parse(raw);
|
|
2297
2856
|
return withTaskLock(`repository:${job.projectPath}`, async () => {
|
|
2298
|
-
const session = await
|
|
2857
|
+
const session = await Effect10.runPromise(this.store.get(id));
|
|
2299
2858
|
if (!session || session.preparation?.status !== "failed")
|
|
2300
2859
|
throw new Error("This run does not need preparation retried.");
|
|
2301
2860
|
const prepared = await this.prepare(session, job.projectPath, job.input);
|
|
@@ -2303,11 +2862,11 @@ ${logs}` : ""}`);
|
|
|
2303
2862
|
});
|
|
2304
2863
|
}
|
|
2305
2864
|
async recover() {
|
|
2306
|
-
for (const raw of await
|
|
2865
|
+
for (const raw of await Effect10.runPromise(this.store.workflows())) {
|
|
2307
2866
|
const parsed = PreparationJobSchema.safeParse(raw);
|
|
2308
2867
|
if (!parsed.success) continue;
|
|
2309
2868
|
const job = parsed.data;
|
|
2310
|
-
const session = await
|
|
2869
|
+
const session = await Effect10.runPromise(this.store.get(job.sessionId));
|
|
2311
2870
|
if (!session) continue;
|
|
2312
2871
|
if (job.stage === "failed") {
|
|
2313
2872
|
if (session.preparation?.status === "failed") continue;
|
|
@@ -2338,7 +2897,7 @@ ${logs}` : ""}`);
|
|
|
2338
2897
|
});
|
|
2339
2898
|
continue;
|
|
2340
2899
|
}
|
|
2341
|
-
await
|
|
2900
|
+
await Effect10.runPromise(
|
|
2342
2901
|
this.store.saveWorkflow(`preparation:${session.id}`, { ...job, stage: "failed" })
|
|
2343
2902
|
);
|
|
2344
2903
|
this.options.publish({
|
|
@@ -2362,7 +2921,7 @@ ${logs}` : ""}`);
|
|
|
2362
2921
|
|
|
2363
2922
|
// src/delivery-writer.ts
|
|
2364
2923
|
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2365
|
-
import { z as
|
|
2924
|
+
import { z as z12 } from "zod";
|
|
2366
2925
|
function deliveryWriter(models2, createSession = createAgentSession) {
|
|
2367
2926
|
return async (run, input, diff, signal) => {
|
|
2368
2927
|
signal?.throwIfAborted();
|
|
@@ -2403,12 +2962,12 @@ var DraftSchema;
|
|
|
2403
2962
|
var init_delivery_writer = __esm({
|
|
2404
2963
|
"src/delivery-writer.ts"() {
|
|
2405
2964
|
"use strict";
|
|
2406
|
-
DraftSchema =
|
|
2965
|
+
DraftSchema = z12.object({ title: z12.string().trim().min(1).max(120), body: z12.string().trim().min(1).max(1e4) }).strict();
|
|
2407
2966
|
}
|
|
2408
2967
|
});
|
|
2409
2968
|
|
|
2410
2969
|
// src/models.ts
|
|
2411
|
-
import { Effect as
|
|
2970
|
+
import { Effect as Effect11 } from "effect";
|
|
2412
2971
|
var legacyModel, makers, ServerModels;
|
|
2413
2972
|
var init_models = __esm({
|
|
2414
2973
|
"src/models.ts"() {
|
|
@@ -2477,7 +3036,7 @@ var init_models = __esm({
|
|
|
2477
3036
|
await this.refreshing;
|
|
2478
3037
|
}
|
|
2479
3038
|
async defaultReference() {
|
|
2480
|
-
return await
|
|
3039
|
+
return await Effect11.runPromise(this.store.defaultModel()) ?? legacyModel;
|
|
2481
3040
|
}
|
|
2482
3041
|
async defaultIdentity() {
|
|
2483
3042
|
const selected = await this.defaultReference();
|
|
@@ -2513,7 +3072,7 @@ var init_models = __esm({
|
|
|
2513
3072
|
}
|
|
2514
3073
|
async setDefault(reference) {
|
|
2515
3074
|
await this.resolve(reference);
|
|
2516
|
-
await
|
|
3075
|
+
await Effect11.runPromise(this.store.setDefaultModel(reference));
|
|
2517
3076
|
return this.identity(reference.platformId, reference.modelId);
|
|
2518
3077
|
}
|
|
2519
3078
|
};
|
|
@@ -2558,13 +3117,13 @@ var init_delivery_tool = __esm({
|
|
|
2558
3117
|
});
|
|
2559
3118
|
|
|
2560
3119
|
// src/pi.ts
|
|
2561
|
-
import { Effect as
|
|
2562
|
-
import { randomUUID as
|
|
3120
|
+
import { Effect as Effect12 } from "effect";
|
|
3121
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
2563
3122
|
import {
|
|
2564
3123
|
createAgentSession as createAgentSession2,
|
|
2565
3124
|
SessionManager as SessionManager2
|
|
2566
3125
|
} from "@earendil-works/pi-coding-agent";
|
|
2567
|
-
import { z as
|
|
3126
|
+
import { z as z13 } from "zod";
|
|
2568
3127
|
function toolOutput(value) {
|
|
2569
3128
|
const parsed = ToolResultSchema.safeParse(value);
|
|
2570
3129
|
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);
|
|
@@ -2596,7 +3155,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2596
3155
|
);
|
|
2597
3156
|
},
|
|
2598
3157
|
async prompt(message, attachments2 = []) {
|
|
2599
|
-
const context = await
|
|
3158
|
+
const context = await Effect12.runPromise(files.prepare(attachments2));
|
|
2600
3159
|
await session.prompt(message + context.context, { images: context.images });
|
|
2601
3160
|
const last = session.messages.at(-1);
|
|
2602
3161
|
if (last?.role === "assistant" && last.stopReason === "error") {
|
|
@@ -2633,7 +3192,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2633
3192
|
function handleMessage(event) {
|
|
2634
3193
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
2635
3194
|
flushText();
|
|
2636
|
-
assistantMessageId =
|
|
3195
|
+
assistantMessageId = randomUUID5();
|
|
2637
3196
|
emit({
|
|
2638
3197
|
type: "message.started",
|
|
2639
3198
|
sessionId,
|
|
@@ -2644,7 +3203,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2644
3203
|
return true;
|
|
2645
3204
|
}
|
|
2646
3205
|
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
2647
|
-
if (!assistantMessageId) assistantMessageId =
|
|
3206
|
+
if (!assistantMessageId) assistantMessageId = randomUUID5();
|
|
2648
3207
|
pendingText += event.assistantMessageEvent.delta;
|
|
2649
3208
|
setActivity("responding");
|
|
2650
3209
|
if (!flushTimer) flushTimer = setTimeout(flushText, 50);
|
|
@@ -2722,8 +3281,8 @@ var init_pi = __esm({
|
|
|
2722
3281
|
"use strict";
|
|
2723
3282
|
init_models();
|
|
2724
3283
|
init_delivery_tool();
|
|
2725
|
-
ToolResultSchema =
|
|
2726
|
-
content:
|
|
3284
|
+
ToolResultSchema = z13.object({
|
|
3285
|
+
content: z13.array(z13.object({ type: z13.string(), text: z13.string().optional() })).optional()
|
|
2727
3286
|
}).passthrough();
|
|
2728
3287
|
boundedJson = (value, limit) => {
|
|
2729
3288
|
let text;
|
|
@@ -2739,18 +3298,18 @@ var init_pi = __esm({
|
|
|
2739
3298
|
|
|
2740
3299
|
// src/projects.ts
|
|
2741
3300
|
import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
2742
|
-
import { randomUUID as
|
|
2743
|
-
import { z as
|
|
3301
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
3302
|
+
import { z as z14 } from "zod";
|
|
2744
3303
|
import { constants as constants2 } from "node:fs";
|
|
2745
3304
|
import { homedir as homedir3 } from "node:os";
|
|
2746
|
-
import
|
|
2747
|
-
import { Effect as
|
|
3305
|
+
import path8 from "node:path";
|
|
3306
|
+
import { Effect as Effect13, Semaphore as Semaphore3 } from "effect";
|
|
2748
3307
|
function resolveProjectsRoot(input) {
|
|
2749
|
-
return
|
|
3308
|
+
return Effect13.tryPromise({
|
|
2750
3309
|
try: async () => {
|
|
2751
3310
|
const trimmed = input.trim();
|
|
2752
|
-
const expanded = trimmed === "~" ? homedir3() : trimmed.startsWith("~/") ?
|
|
2753
|
-
if (!
|
|
3311
|
+
const expanded = trimmed === "~" ? homedir3() : trimmed.startsWith("~/") ? path8.join(homedir3(), trimmed.slice(2)) : trimmed;
|
|
3312
|
+
if (!path8.isAbsolute(expanded))
|
|
2754
3313
|
throw new Error("Use an absolute server path or a path beginning with ~/.");
|
|
2755
3314
|
const resolved = await realpath(expanded);
|
|
2756
3315
|
if (!(await stat3(resolved)).isDirectory())
|
|
@@ -2772,61 +3331,61 @@ function projectLock(store2) {
|
|
|
2772
3331
|
return lock;
|
|
2773
3332
|
}
|
|
2774
3333
|
async function gitDirectory(directory) {
|
|
2775
|
-
const location =
|
|
3334
|
+
const location = path8.join(directory, ".git");
|
|
2776
3335
|
const info = await stat3(location);
|
|
2777
3336
|
if (info.isDirectory()) return location;
|
|
2778
3337
|
const contents = await readFile4(location, "utf8");
|
|
2779
3338
|
const match = /^gitdir: (.+)\s*$/m.exec(contents);
|
|
2780
3339
|
if (!match) throw new Error("Invalid Git worktree metadata.");
|
|
2781
|
-
const resolved = await realpath(
|
|
3340
|
+
const resolved = await realpath(path8.resolve(directory, match[1].trim()));
|
|
2782
3341
|
if (!(await stat3(resolved)).isDirectory()) throw new Error("Git directory is missing.");
|
|
2783
3342
|
return resolved;
|
|
2784
3343
|
}
|
|
2785
3344
|
async function identity(directory, preferred) {
|
|
2786
|
-
const filename =
|
|
3345
|
+
const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
|
|
2787
3346
|
try {
|
|
2788
3347
|
return ProjectId.parse(await readFile4(filename, "utf8"));
|
|
2789
3348
|
} catch (error) {
|
|
2790
|
-
if (!
|
|
2791
|
-
const legacyFilename =
|
|
3349
|
+
if (!z14.object({ code: z14.literal("ENOENT") }).safeParse(error).success) throw error;
|
|
3350
|
+
const legacyFilename = path8.join(await gitDirectory(directory), "chatplans-project-id");
|
|
2792
3351
|
const legacy = await readFile4(legacyFilename, "utf8").catch((cause) => {
|
|
2793
|
-
if (!
|
|
3352
|
+
if (!z14.object({ code: z14.literal("ENOENT") }).safeParse(cause).success) throw cause;
|
|
2794
3353
|
return null;
|
|
2795
3354
|
});
|
|
2796
|
-
const id = legacy === null ? preferred ??
|
|
3355
|
+
const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
|
|
2797
3356
|
try {
|
|
2798
3357
|
await writeFile3(filename, id + "\n", { flag: "wx" });
|
|
2799
3358
|
return id;
|
|
2800
3359
|
} catch (writeError) {
|
|
2801
|
-
if (!
|
|
3360
|
+
if (!z14.object({ code: z14.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
|
|
2802
3361
|
return ProjectId.parse(await readFile4(filename, "utf8"));
|
|
2803
3362
|
}
|
|
2804
3363
|
}
|
|
2805
3364
|
}
|
|
2806
3365
|
function scanProjects(root, store2) {
|
|
2807
|
-
return
|
|
3366
|
+
return Effect13.tryPromise({
|
|
2808
3367
|
try: async () => {
|
|
2809
3368
|
root = await realpath(root);
|
|
2810
|
-
const registered = await
|
|
3369
|
+
const registered = await Effect13.runPromise(store2.registeredProjects(root));
|
|
2811
3370
|
const found = [];
|
|
2812
3371
|
const entries = await readdir2(root, { withFileTypes: true });
|
|
2813
3372
|
for (const entry of entries) {
|
|
2814
3373
|
if (!entry.isDirectory()) continue;
|
|
2815
|
-
const directory =
|
|
2816
|
-
if (!await stat3(
|
|
3374
|
+
const directory = path8.join(root, entry.name);
|
|
3375
|
+
if (!await stat3(path8.join(directory, ".git")).catch(() => null)) continue;
|
|
2817
3376
|
const previous = registered.find((project2) => project2.path === directory);
|
|
2818
3377
|
let id = await identity(directory, previous?.available ? void 0 : previous?.id);
|
|
2819
3378
|
let existing = registered.find((project2) => project2.id === id);
|
|
2820
|
-
if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat3(
|
|
2821
|
-
id =
|
|
3379
|
+
if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat3(path8.join(existing.path, ".git")).catch(() => null)) {
|
|
3380
|
+
id = randomUUID6();
|
|
2822
3381
|
await writeFile3(
|
|
2823
|
-
|
|
3382
|
+
path8.join(await gitDirectory(directory), "agentrific-project-id"),
|
|
2824
3383
|
id + "\n"
|
|
2825
3384
|
);
|
|
2826
3385
|
existing = void 0;
|
|
2827
3386
|
}
|
|
2828
3387
|
const project = { id, name: entry.name, path: directory, available: true };
|
|
2829
|
-
await
|
|
3388
|
+
await Effect13.runPromise(store2.relocateProject(project, existing?.path));
|
|
2830
3389
|
found.push(project);
|
|
2831
3390
|
}
|
|
2832
3391
|
return [
|
|
@@ -2841,26 +3400,26 @@ function discoverProjects(root, store2) {
|
|
|
2841
3400
|
return projectLock(store2).withPermit(scanProjects(root, store2));
|
|
2842
3401
|
}
|
|
2843
3402
|
function reconnectProject(root, id, input, store2) {
|
|
2844
|
-
return
|
|
3403
|
+
return Effect13.tryPromise({
|
|
2845
3404
|
try: async () => {
|
|
2846
|
-
const projects = await
|
|
3405
|
+
const projects = await Effect13.runPromise(scanProjects(root, store2));
|
|
2847
3406
|
const previous = projects.find((project2) => project2.id === id);
|
|
2848
3407
|
if (!previous || previous.available)
|
|
2849
3408
|
throw new Error("Only a missing project can be reconnected.");
|
|
2850
|
-
const sessions = await
|
|
3409
|
+
const sessions = await Effect13.runPromise(store2.list());
|
|
2851
3410
|
if (sessions.some(
|
|
2852
3411
|
(session) => session.projectId === id && ["starting", "running", "queued"].includes(session.status)
|
|
2853
3412
|
))
|
|
2854
3413
|
throw new Error("Stop this project's running sessions before reconnecting its folder.");
|
|
2855
|
-
const directory = await
|
|
2856
|
-
if (
|
|
3414
|
+
const directory = await Effect13.runPromise(resolveProjectsRoot(input));
|
|
3415
|
+
if (path8.dirname(directory) !== await realpath(root))
|
|
2857
3416
|
throw new Error("Choose a Git repository directly inside the configured projects folder.");
|
|
2858
|
-
const filename =
|
|
3417
|
+
const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
|
|
2859
3418
|
const replacedId = await identity(directory);
|
|
2860
|
-
const project = { id, name:
|
|
3419
|
+
const project = { id, name: path8.basename(directory), path: directory, available: true };
|
|
2861
3420
|
await writeFile3(filename, id + "\n");
|
|
2862
3421
|
try {
|
|
2863
|
-
await
|
|
3422
|
+
await Effect13.runPromise(store2.relocateProject(project, previous.path, replacedId));
|
|
2864
3423
|
} catch (error) {
|
|
2865
3424
|
await writeFile3(filename, replacedId + "\n");
|
|
2866
3425
|
throw error;
|
|
@@ -2875,34 +3434,86 @@ var init_projects = __esm({
|
|
|
2875
3434
|
"src/projects.ts"() {
|
|
2876
3435
|
"use strict";
|
|
2877
3436
|
locks2 = /* @__PURE__ */ new WeakMap();
|
|
2878
|
-
ProjectId =
|
|
3437
|
+
ProjectId = z14.string().trim().min(1).max(255);
|
|
3438
|
+
}
|
|
3439
|
+
});
|
|
3440
|
+
|
|
3441
|
+
// src/push-schema.ts
|
|
3442
|
+
import { z as z15 } from "zod";
|
|
3443
|
+
var PushBindingSchema, PushStateSchema, PendingPushSchema;
|
|
3444
|
+
var init_push_schema = __esm({
|
|
3445
|
+
"src/push-schema.ts"() {
|
|
3446
|
+
"use strict";
|
|
3447
|
+
init_push();
|
|
3448
|
+
PushBindingSchema = z15.object({
|
|
3449
|
+
bindingId: z15.string().uuid(),
|
|
3450
|
+
installationId: z15.string().uuid(),
|
|
3451
|
+
deviceId: z15.string().uuid()
|
|
3452
|
+
});
|
|
3453
|
+
PushStateSchema = z15.object({
|
|
3454
|
+
bindings: z15.array(PushBindingSchema).max(10),
|
|
3455
|
+
error: z15.string().nullable()
|
|
3456
|
+
});
|
|
3457
|
+
PendingPushSchema = PushEventSchema.extend({
|
|
3458
|
+
remaining: z15.array(PushBindingSchema),
|
|
3459
|
+
createdAt: z15.number(),
|
|
3460
|
+
attempts: z15.number().int().nonnegative(),
|
|
3461
|
+
nextAt: z15.number()
|
|
3462
|
+
});
|
|
2879
3463
|
}
|
|
2880
3464
|
});
|
|
2881
3465
|
|
|
2882
3466
|
// src/store.ts
|
|
2883
|
-
import { mkdirSync } from "node:fs";
|
|
2884
|
-
import
|
|
3467
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
3468
|
+
import path9 from "node:path";
|
|
2885
3469
|
import { DatabaseSync } from "node:sqlite";
|
|
2886
|
-
import { Context as Context3, Data as Data3, Effect as
|
|
2887
|
-
import { z as
|
|
2888
|
-
import { randomUUID as
|
|
3470
|
+
import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
|
|
3471
|
+
import { z as z16 } from "zod";
|
|
3472
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
2889
3473
|
var StorageError, PayloadRow, EventRow, SqliteSessionStore, SessionStore;
|
|
2890
3474
|
var init_store = __esm({
|
|
2891
3475
|
"src/store.ts"() {
|
|
2892
3476
|
"use strict";
|
|
3477
|
+
init_push_schema();
|
|
2893
3478
|
init_src();
|
|
2894
3479
|
init_src();
|
|
2895
3480
|
init_workflow_schema();
|
|
2896
3481
|
init_src();
|
|
2897
3482
|
StorageError = class extends Data3.TaggedError("StorageError") {
|
|
2898
3483
|
};
|
|
2899
|
-
PayloadRow =
|
|
2900
|
-
EventRow =
|
|
3484
|
+
PayloadRow = z16.object({ payload: z16.string() });
|
|
3485
|
+
EventRow = z16.object({ sequence: z16.number(), recorded_at: z16.string(), payload: z16.string() });
|
|
2901
3486
|
SqliteSessionStore = class {
|
|
2902
3487
|
constructor(db) {
|
|
2903
3488
|
this.db = db;
|
|
2904
3489
|
}
|
|
2905
3490
|
db;
|
|
3491
|
+
pushState() {
|
|
3492
|
+
return this.query(() => {
|
|
3493
|
+
const row = this.db.prepare("SELECT payload FROM settings WHERE id = 'push-state'").get();
|
|
3494
|
+
return row ? PushStateSchema.parse(JSON.parse(PayloadRow.parse(row).payload)) : { bindings: [], error: null };
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
savePushState(state) {
|
|
3498
|
+
return this.query(
|
|
3499
|
+
() => this.db.prepare(
|
|
3500
|
+
"INSERT INTO settings(id, payload) VALUES ('push-state', ?) ON CONFLICT(id) DO UPDATE SET payload = excluded.payload"
|
|
3501
|
+
).run(JSON.stringify(PushStateSchema.parse(state)))
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3504
|
+
pendingPush() {
|
|
3505
|
+
return this.query(
|
|
3506
|
+
() => this.db.prepare("SELECT payload FROM push_outbox ORDER BY rowid LIMIT 100").all().map((row) => PendingPushSchema.parse(JSON.parse(PayloadRow.parse(row).payload)))
|
|
3507
|
+
);
|
|
3508
|
+
}
|
|
3509
|
+
savePendingPush(job) {
|
|
3510
|
+
return this.query(
|
|
3511
|
+
() => this.db.prepare("UPDATE push_outbox SET payload = ? WHERE id = ?").run(JSON.stringify(PendingPushSchema.parse(job)), job.eventId)
|
|
3512
|
+
);
|
|
3513
|
+
}
|
|
3514
|
+
removePush(eventId) {
|
|
3515
|
+
return this.query(() => this.db.prepare("DELETE FROM push_outbox WHERE id = ?").run(eventId));
|
|
3516
|
+
}
|
|
2906
3517
|
serverUpdateStatus() {
|
|
2907
3518
|
return this.query(() => {
|
|
2908
3519
|
const row = this.db.prepare("SELECT payload FROM settings WHERE id = 'server-update-status'").get();
|
|
@@ -2918,11 +3529,11 @@ var init_store = __esm({
|
|
|
2918
3529
|
}
|
|
2919
3530
|
serverIdentity() {
|
|
2920
3531
|
return this.query(() => {
|
|
2921
|
-
this.db.prepare("INSERT OR IGNORE INTO settings(id, payload) VALUES ('server-id', ?)").run(JSON.stringify(
|
|
3532
|
+
this.db.prepare("INSERT OR IGNORE INTO settings(id, payload) VALUES ('server-id', ?)").run(JSON.stringify(randomUUID7()));
|
|
2922
3533
|
const row = PayloadRow.parse(
|
|
2923
3534
|
this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
|
|
2924
3535
|
);
|
|
2925
|
-
return
|
|
3536
|
+
return z16.string().uuid().parse(JSON.parse(row.payload));
|
|
2926
3537
|
});
|
|
2927
3538
|
}
|
|
2928
3539
|
saveDevice(device, tokenHash) {
|
|
@@ -2959,7 +3570,7 @@ var init_store = __esm({
|
|
|
2959
3570
|
});
|
|
2960
3571
|
}
|
|
2961
3572
|
query(operation) {
|
|
2962
|
-
return
|
|
3573
|
+
return Effect14.try({
|
|
2963
3574
|
try: operation,
|
|
2964
3575
|
catch: (cause) => new StorageError({
|
|
2965
3576
|
message: cause instanceof Error ? cause.message : "Session storage operation failed",
|
|
@@ -3010,10 +3621,10 @@ var init_store = __esm({
|
|
|
3010
3621
|
`).all();
|
|
3011
3622
|
const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
|
|
3012
3623
|
for (const row of legacy) {
|
|
3013
|
-
const id =
|
|
3624
|
+
const id = z16.object({ id: z16.string() }).parse(row).id;
|
|
3014
3625
|
insert.run(
|
|
3015
3626
|
id,
|
|
3016
|
-
JSON.stringify({ id, name: id, path:
|
|
3627
|
+
JSON.stringify({ id, name: id, path: path9.join(root, id), available: false })
|
|
3017
3628
|
);
|
|
3018
3629
|
}
|
|
3019
3630
|
return this.db.prepare("SELECT payload FROM projects").all().map((row) => ProjectSchema.parse(JSON.parse(PayloadRow.parse(row).payload)));
|
|
@@ -3050,7 +3661,7 @@ var init_store = __esm({
|
|
|
3050
3661
|
this.db.prepare(
|
|
3051
3662
|
"UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
|
|
3052
3663
|
).run(project.path, previousPath);
|
|
3053
|
-
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${
|
|
3664
|
+
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path9.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
|
|
3054
3665
|
for (const row of this.db.prepare("SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?").all(project.id)) {
|
|
3055
3666
|
const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
|
|
3056
3667
|
if (session.workspacePath) {
|
|
@@ -3125,6 +3736,31 @@ var init_store = __esm({
|
|
|
3125
3736
|
return row ? TaskSchema.parse(JSON.parse(PayloadRow.parse(row).payload)) : void 0;
|
|
3126
3737
|
});
|
|
3127
3738
|
}
|
|
3739
|
+
purgeTask(id, sessionIds) {
|
|
3740
|
+
return this.query(() => {
|
|
3741
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
3742
|
+
try {
|
|
3743
|
+
for (const sessionId of sessionIds) {
|
|
3744
|
+
this.db.prepare("DELETE FROM events WHERE session_id = ?").run(sessionId);
|
|
3745
|
+
this.db.prepare("DELETE FROM push_outbox WHERE json_extract(payload, '$.sessionId') = ?").run(sessionId);
|
|
3746
|
+
this.db.prepare("DELETE FROM workflows WHERE json_extract(payload, '$.sessionId') = ?").run(sessionId);
|
|
3747
|
+
this.db.prepare("DELETE FROM sessions WHERE id = ?").run(sessionId);
|
|
3748
|
+
}
|
|
3749
|
+
this.db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
|
3750
|
+
this.db.exec(`UPDATE attachments SET claimed = 0 WHERE NOT EXISTS (
|
|
3751
|
+
SELECT 1 FROM tasks, json_each(tasks.payload, '$.attachments') AS file
|
|
3752
|
+
WHERE json_extract(file.value, '$.id') = attachments.id
|
|
3753
|
+
) AND NOT EXISTS (
|
|
3754
|
+
SELECT 1 FROM events, json_each(events.payload, '$.attachments') AS file
|
|
3755
|
+
WHERE json_extract(file.value, '$.id') = attachments.id
|
|
3756
|
+
)`);
|
|
3757
|
+
this.db.exec("COMMIT");
|
|
3758
|
+
} catch (error) {
|
|
3759
|
+
this.db.exec("ROLLBACK");
|
|
3760
|
+
throw error;
|
|
3761
|
+
}
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3128
3764
|
saveAttachment(attachment) {
|
|
3129
3765
|
return this.query(() => {
|
|
3130
3766
|
this.db.prepare("INSERT INTO attachments(id, payload) VALUES (?, ?)").run(attachment.id, JSON.stringify(AttachmentSchema.parse(attachment)));
|
|
@@ -3181,7 +3817,7 @@ var init_store = __esm({
|
|
|
3181
3817
|
}
|
|
3182
3818
|
});
|
|
3183
3819
|
}
|
|
3184
|
-
append(event) {
|
|
3820
|
+
append(event, notification) {
|
|
3185
3821
|
return this.query(() => {
|
|
3186
3822
|
const sessionId = event.type === "session.updated" ? event.session.id : event.sessionId;
|
|
3187
3823
|
const recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3194,6 +3830,14 @@ var init_store = __esm({
|
|
|
3194
3830
|
}
|
|
3195
3831
|
if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
|
|
3196
3832
|
const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
|
|
3833
|
+
if (notification) {
|
|
3834
|
+
const count = z16.object({ total: z16.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
|
|
3835
|
+
if (count.total >= 1e3)
|
|
3836
|
+
this.db.prepare(
|
|
3837
|
+
"DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
|
|
3838
|
+
).run();
|
|
3839
|
+
this.db.prepare("INSERT INTO push_outbox(id, payload) VALUES (?, ?)").run(notification.eventId, JSON.stringify(PendingPushSchema.parse(notification)));
|
|
3840
|
+
}
|
|
3197
3841
|
const record = RecordedEventSchema.parse({
|
|
3198
3842
|
sequence: Number(result.lastInsertRowid),
|
|
3199
3843
|
recordedAt,
|
|
@@ -3230,13 +3874,16 @@ var init_store = __esm({
|
|
|
3230
3874
|
static layer(filename) {
|
|
3231
3875
|
return Layer3.effect(
|
|
3232
3876
|
this,
|
|
3233
|
-
|
|
3234
|
-
|
|
3877
|
+
Effect14.acquireRelease(
|
|
3878
|
+
Effect14.try({
|
|
3235
3879
|
try: () => {
|
|
3236
|
-
|
|
3880
|
+
mkdirSync2(path9.dirname(filename), { recursive: true });
|
|
3237
3881
|
const db = new DatabaseSync(filename);
|
|
3882
|
+
chmodSync(filename, 384);
|
|
3883
|
+
for (const suffix of ["-wal", "-shm"])
|
|
3884
|
+
if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
|
|
3238
3885
|
try {
|
|
3239
|
-
const schema =
|
|
3886
|
+
const schema = z16.object({ user_version: z16.number() }).parse(db.prepare("PRAGMA user_version").get());
|
|
3240
3887
|
if (schema.user_version > 1)
|
|
3241
3888
|
throw new Error("This database requires a newer Agentrific server.");
|
|
3242
3889
|
db.exec(`
|
|
@@ -3247,6 +3894,7 @@ var init_store = __esm({
|
|
|
3247
3894
|
CREATE TABLE IF NOT EXISTS settings(id TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
3248
3895
|
CREATE TABLE IF NOT EXISTS sessions(id TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
3249
3896
|
CREATE TABLE IF NOT EXISTS tasks(id TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
3897
|
+
CREATE TABLE IF NOT EXISTS push_outbox(id TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
3250
3898
|
CREATE TABLE IF NOT EXISTS workflows(id TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
3251
3899
|
CREATE TABLE IF NOT EXISTS attachments(id TEXT PRIMARY KEY, payload TEXT NOT NULL, claimed INTEGER NOT NULL DEFAULT 0);
|
|
3252
3900
|
CREATE TABLE IF NOT EXISTS events(sequence INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL REFERENCES sessions(id), recorded_at TEXT NOT NULL, payload TEXT NOT NULL);
|
|
@@ -3262,14 +3910,85 @@ var init_store = __esm({
|
|
|
3262
3910
|
},
|
|
3263
3911
|
catch: (cause) => new StorageError({ message: "Could not open session database", cause })
|
|
3264
3912
|
}),
|
|
3265
|
-
(db) =>
|
|
3266
|
-
).pipe(
|
|
3913
|
+
(db) => Effect14.sync(() => db.close())
|
|
3914
|
+
).pipe(Effect14.map((db) => new SqliteSessionStore(db)))
|
|
3267
3915
|
);
|
|
3268
3916
|
}
|
|
3269
3917
|
};
|
|
3270
3918
|
}
|
|
3271
3919
|
});
|
|
3272
3920
|
|
|
3921
|
+
// src/worktree-cleanup.ts
|
|
3922
|
+
import { lstat, rm as rm2 } from "node:fs/promises";
|
|
3923
|
+
import path10 from "node:path";
|
|
3924
|
+
import { Effect as Effect15 } from "effect";
|
|
3925
|
+
async function removeRunFiles(dataRoot2, sessionId) {
|
|
3926
|
+
if (path10.basename(sessionId) !== sessionId || sessionId === "." || sessionId === "..")
|
|
3927
|
+
throw new Error("Invalid session directory.");
|
|
3928
|
+
await rm2(path10.join(dataRoot2, "pi", sessionId), { recursive: true, force: true });
|
|
3929
|
+
}
|
|
3930
|
+
var WorktreeCleanup;
|
|
3931
|
+
var init_worktree_cleanup = __esm({
|
|
3932
|
+
"src/worktree-cleanup.ts"() {
|
|
3933
|
+
"use strict";
|
|
3934
|
+
init_tasks();
|
|
3935
|
+
init_workflow_command();
|
|
3936
|
+
WorktreeCleanup = class {
|
|
3937
|
+
constructor(store2, root, projectsRoot) {
|
|
3938
|
+
this.store = store2;
|
|
3939
|
+
this.root = root;
|
|
3940
|
+
this.projectsRoot = projectsRoot;
|
|
3941
|
+
}
|
|
3942
|
+
store;
|
|
3943
|
+
root;
|
|
3944
|
+
projectsRoot;
|
|
3945
|
+
async remove(session, discardChanges = true) {
|
|
3946
|
+
if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
|
|
3947
|
+
const workspace = path10.resolve(session.workspacePath);
|
|
3948
|
+
const info = await lstat(workspace).catch((error) => {
|
|
3949
|
+
if (error.code === "ENOENT") return void 0;
|
|
3950
|
+
throw error;
|
|
3951
|
+
});
|
|
3952
|
+
if (workspace !== path10.join(this.root, session.id) || path10.basename(session.id) !== session.id) {
|
|
3953
|
+
if (!info) return;
|
|
3954
|
+
throw new Error("Refusing to remove a worktree outside the managed workspaces directory.");
|
|
3955
|
+
}
|
|
3956
|
+
if (info?.isSymbolicLink()) throw new Error("Refusing to remove a symlink worktree.");
|
|
3957
|
+
const job = await Effect15.runPromise(this.store.workflow(`preparation:${session.id}`));
|
|
3958
|
+
const projects = await Effect15.runPromise(this.store.registeredProjects(this.projectsRoot));
|
|
3959
|
+
const project = job?.kind === "preparation" ? job.projectPath : projects.find((item) => item.id === session.projectId)?.path;
|
|
3960
|
+
if (!project) throw new Error("Reconnect the project before removing its worktree.");
|
|
3961
|
+
if (info) {
|
|
3962
|
+
await workflowCommand(project, "git", [
|
|
3963
|
+
"worktree",
|
|
3964
|
+
"remove",
|
|
3965
|
+
...discardChanges ? ["--force"] : [],
|
|
3966
|
+
workspace
|
|
3967
|
+
]);
|
|
3968
|
+
} else {
|
|
3969
|
+
await workflowCommand(project, "git", ["worktree", "prune"]);
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
async prune(maxAgeDays, now = Date.now()) {
|
|
3973
|
+
if (!Number.isFinite(maxAgeDays) || maxAgeDays <= 0) return;
|
|
3974
|
+
const sessions = await Effect15.runPromise(this.store.list());
|
|
3975
|
+
const failures = [];
|
|
3976
|
+
for (const session of sessions) {
|
|
3977
|
+
const lockId = await sessionTaskLockId(this.store, session);
|
|
3978
|
+
await withTaskLock(lockId, async () => {
|
|
3979
|
+
const current = await Effect15.runPromise(this.store.get(session.id));
|
|
3980
|
+
if (!current || taskStatusForSession(current.status, current) !== "done" || now - Date.parse(current.updatedAt) < maxAgeDays * 864e5)
|
|
3981
|
+
return;
|
|
3982
|
+
await this.remove(current, false);
|
|
3983
|
+
}).catch((error) => failures.push(error));
|
|
3984
|
+
}
|
|
3985
|
+
if (failures.length)
|
|
3986
|
+
throw new AggregateError(failures, "Some completed worktrees could not be pruned.");
|
|
3987
|
+
}
|
|
3988
|
+
};
|
|
3989
|
+
}
|
|
3990
|
+
});
|
|
3991
|
+
|
|
3273
3992
|
// src/index.ts
|
|
3274
3993
|
var index_exports = {};
|
|
3275
3994
|
import Fastify from "fastify";
|
|
@@ -3277,18 +3996,18 @@ import pretty from "pino-pretty";
|
|
|
3277
3996
|
import { styleText as styleText4 } from "node:util";
|
|
3278
3997
|
import cors from "@fastify/cors";
|
|
3279
3998
|
import multipart from "@fastify/multipart";
|
|
3280
|
-
import { randomUUID as
|
|
3999
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
3281
4000
|
import { execFile as execFile3 } from "node:child_process";
|
|
3282
4001
|
import { promisify as promisify3 } from "node:util";
|
|
3283
4002
|
import { realpath as realpath2 } from "node:fs/promises";
|
|
3284
4003
|
import { homedir as homedir4 } from "node:os";
|
|
3285
|
-
import
|
|
4004
|
+
import path11 from "node:path";
|
|
3286
4005
|
import { WebSocket } from "ws";
|
|
3287
|
-
import { Effect as
|
|
3288
|
-
import { z as
|
|
4006
|
+
import { Effect as Effect16, ManagedRuntime } from "effect";
|
|
4007
|
+
import { z as z17 } from "zod";
|
|
3289
4008
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
3290
4009
|
function publish(event, workflowUpdate = false) {
|
|
3291
|
-
const previous = event.type === "session.updated" ?
|
|
4010
|
+
const previous = event.type === "session.updated" ? Effect16.runSync(store.get(event.session.id)) : void 0;
|
|
3292
4011
|
const emittedEvent = event.type === "session.updated" && previous && !workflowUpdate ? {
|
|
3293
4012
|
type: "session.updated",
|
|
3294
4013
|
session: {
|
|
@@ -3297,7 +4016,8 @@ function publish(event, workflowUpdate = false) {
|
|
|
3297
4016
|
preparation: previous.preparation
|
|
3298
4017
|
}
|
|
3299
4018
|
} : event;
|
|
3300
|
-
const
|
|
4019
|
+
const notification = emittedEvent.type === "session.updated" ? push.pending(emittedEvent.session, previous) : void 0;
|
|
4020
|
+
const record = Effect16.runSync(store.append(emittedEvent, notification));
|
|
3301
4021
|
const message = JSON.stringify({ type: "event", record });
|
|
3302
4022
|
for (const client of security.sockets.keys()) {
|
|
3303
4023
|
if (!(client instanceof WebSocket) || client.readyState !== WebSocket.OPEN) continue;
|
|
@@ -3337,26 +4057,26 @@ async function selectSessionModel(input, hasImages) {
|
|
|
3337
4057
|
}
|
|
3338
4058
|
}
|
|
3339
4059
|
async function resolveSessionProject(projectId) {
|
|
3340
|
-
const projectsRoot = await
|
|
3341
|
-
const project = (await
|
|
4060
|
+
const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4061
|
+
const project = (await Effect16.runPromise(discoverProjects(projectsRoot, store))).find(
|
|
3342
4062
|
(item) => item.id === projectId && item.available
|
|
3343
4063
|
);
|
|
3344
4064
|
if (!project)
|
|
3345
4065
|
return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
|
|
3346
4066
|
const projectPath = await realpath2(project.path);
|
|
3347
4067
|
const resolvedRoot = await realpath2(projectsRoot);
|
|
3348
|
-
if (
|
|
4068
|
+
if (path11.dirname(projectPath) !== resolvedRoot)
|
|
3349
4069
|
return { error: "Project path is outside projects root.", status: 400 };
|
|
3350
4070
|
return { projectPath };
|
|
3351
4071
|
}
|
|
3352
4072
|
async function resolveSessionWorkspace(id, gitFlow, projectPath) {
|
|
3353
|
-
const worktreePath =
|
|
4073
|
+
const worktreePath = path11.join(worktreesRoot, id);
|
|
3354
4074
|
const branch = gitFlow ? `agentrific/${id.slice(0, 8)}` : null;
|
|
3355
4075
|
const useProjectCheckout = gitFlow?.workspace === "checkout";
|
|
3356
4076
|
const workspacePath = !gitFlow || useProjectCheckout ? projectPath : worktreePath;
|
|
3357
4077
|
if (useProjectCheckout) {
|
|
3358
4078
|
const serverPath = await realpath2(process.cwd());
|
|
3359
|
-
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${
|
|
4079
|
+
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path11.sep}`))
|
|
3360
4080
|
return {
|
|
3361
4081
|
error: "The server runs from this project. Choose a worktree for this task.",
|
|
3362
4082
|
status: 409
|
|
@@ -3380,7 +4100,7 @@ async function createAndStartSession(input) {
|
|
|
3380
4100
|
const projectPath = location.projectPath;
|
|
3381
4101
|
let files;
|
|
3382
4102
|
try {
|
|
3383
|
-
files = await
|
|
4103
|
+
files = await Effect16.runPromise(
|
|
3384
4104
|
attachments.resolve(input.projectId, input.attachmentIds ?? [])
|
|
3385
4105
|
);
|
|
3386
4106
|
} catch (error) {
|
|
@@ -3393,11 +4113,11 @@ async function createAndStartSession(input) {
|
|
|
3393
4113
|
if (selection.error !== void 0) return { error: selection.error, status: selection.status };
|
|
3394
4114
|
const selectedModel = selection.model;
|
|
3395
4115
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3396
|
-
const task = input.taskId ? await
|
|
4116
|
+
const task = input.taskId ? await Effect16.runPromise(store.getTask(input.taskId)) : void 0;
|
|
3397
4117
|
if (input.taskId && sessionTaskUnavailable(task, input.projectId))
|
|
3398
4118
|
return { error: "Task not found.", status: 404 };
|
|
3399
4119
|
const session = SessionSchema.parse({
|
|
3400
|
-
id:
|
|
4120
|
+
id: randomUUID8(),
|
|
3401
4121
|
taskId: input.taskId ?? null,
|
|
3402
4122
|
projectId: input.projectId,
|
|
3403
4123
|
title: task?.title ?? input.prompt.slice(0, 80),
|
|
@@ -3422,7 +4142,7 @@ async function createAndStartSession(input) {
|
|
|
3422
4142
|
);
|
|
3423
4143
|
} catch (error) {
|
|
3424
4144
|
if (task)
|
|
3425
|
-
await
|
|
4145
|
+
await Effect16.runPromise(
|
|
3426
4146
|
store.saveTask({ ...task, sessionId: session.id, updatedAt: (/* @__PURE__ */ new Date()).toISOString() })
|
|
3427
4147
|
);
|
|
3428
4148
|
return {
|
|
@@ -3431,7 +4151,7 @@ async function createAndStartSession(input) {
|
|
|
3431
4151
|
};
|
|
3432
4152
|
}
|
|
3433
4153
|
try {
|
|
3434
|
-
await
|
|
4154
|
+
await Effect16.runPromise(runtime.send(running, input.prompt, files));
|
|
3435
4155
|
} catch (error) {
|
|
3436
4156
|
publish({
|
|
3437
4157
|
type: "session.updated",
|
|
@@ -3440,18 +4160,18 @@ async function createAndStartSession(input) {
|
|
|
3440
4160
|
publish({ type: "error", sessionId: session.id, message: String(error) });
|
|
3441
4161
|
return { error: "Could not start the agent session.", status: 500 };
|
|
3442
4162
|
}
|
|
3443
|
-
return { session: await
|
|
4163
|
+
return { session: await Effect16.runPromise(store.get(session.id)), status: 201 };
|
|
3444
4164
|
}
|
|
3445
4165
|
function startTask(id, instructionsOverride, automated = false) {
|
|
3446
4166
|
return withTaskLock(
|
|
3447
4167
|
"agent-dispatch",
|
|
3448
4168
|
() => withTaskLock(id, async () => {
|
|
3449
4169
|
maintenance.assertReady();
|
|
3450
|
-
const task = await
|
|
4170
|
+
const task = await Effect16.runPromise(store.getTask(id));
|
|
3451
4171
|
if (!task || task.deletedAt) throw new TaskActionError("Task not found.", 404);
|
|
3452
4172
|
if (automated) {
|
|
3453
|
-
const state = await
|
|
3454
|
-
if (activeAgents(await
|
|
4173
|
+
const state = await Effect16.runPromise(store.automation());
|
|
4174
|
+
if (activeAgents(await Effect16.runPromise(store.list())) >= state.config.parallelism) return;
|
|
3455
4175
|
const backlog = await boardTasks(store);
|
|
3456
4176
|
if (!backlog.some((item) => item.id === id && item.status === "backlog")) return;
|
|
3457
4177
|
}
|
|
@@ -3459,7 +4179,7 @@ function startTask(id, instructionsOverride, automated = false) {
|
|
|
3459
4179
|
const prompt = instructions ? `${task.title}
|
|
3460
4180
|
|
|
3461
4181
|
${instructions}` : task.title;
|
|
3462
|
-
const linkedSession = task.sessionId ? await
|
|
4182
|
+
const linkedSession = task.sessionId ? await Effect16.runPromise(store.get(task.sessionId)) : void 0;
|
|
3463
4183
|
if (linkedSession && (["starting", "running", "queued"].includes(linkedSession.status) || linkedSession.gitDelivery?.status === "pending"))
|
|
3464
4184
|
throw new TaskActionError("This task already has a running agent.", 409);
|
|
3465
4185
|
const result = await createAndStartSession({
|
|
@@ -3473,7 +4193,7 @@ ${instructions}` : task.title;
|
|
|
3473
4193
|
});
|
|
3474
4194
|
if (!result.session)
|
|
3475
4195
|
throw new TaskActionError(result.error ?? "Could not start task.", result.status);
|
|
3476
|
-
const updatedTask = await
|
|
4196
|
+
const updatedTask = await Effect16.runPromise(
|
|
3477
4197
|
store.saveTask({
|
|
3478
4198
|
...task,
|
|
3479
4199
|
status: taskStatusForSession(result.session.status, result.session),
|
|
@@ -3485,11 +4205,17 @@ ${instructions}` : task.title;
|
|
|
3485
4205
|
})
|
|
3486
4206
|
);
|
|
3487
4207
|
}
|
|
3488
|
-
|
|
4208
|
+
async function withSessionTaskLock(id, operation) {
|
|
4209
|
+
const session = await Effect16.runPromise(store.get(id));
|
|
4210
|
+
const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
|
|
4211
|
+
return withTaskLock(lockId, operation);
|
|
4212
|
+
}
|
|
4213
|
+
var host, port, defaultProjectsRoot, worktreesRoot, execFileAsync, app, dataRoot, releaseLock, effects, store, security, testPushUrl, push, allowedOrigins, attachmentEffects, attachments, attachmentCleanup, models, delivery, preparation, agentEffects, runtime, worktreeCleanup, maxWorktreeAgeDays, pruneWorktrees, worktreeCleanupTimer, updates, maintenance, stopUpdateDiscovery, HistoryQuery, StartTaskBody, automation, automationTimer, closeControl;
|
|
3489
4214
|
var init_index = __esm({
|
|
3490
4215
|
async "src/index.ts"() {
|
|
3491
4216
|
"use strict";
|
|
3492
4217
|
init_warnings();
|
|
4218
|
+
init_push2();
|
|
3493
4219
|
init_attachments();
|
|
3494
4220
|
init_event_server();
|
|
3495
4221
|
init_security();
|
|
@@ -3508,11 +4234,12 @@ var init_index = __esm({
|
|
|
3508
4234
|
init_projects();
|
|
3509
4235
|
init_store();
|
|
3510
4236
|
init_tasks();
|
|
4237
|
+
init_worktree_cleanup();
|
|
3511
4238
|
host = process.env.AGENTRIFIC_HOST ?? "127.0.0.1";
|
|
3512
4239
|
port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
|
|
3513
|
-
defaultProjectsRoot =
|
|
3514
|
-
worktreesRoot =
|
|
3515
|
-
process.env.AGENTRIFIC_WORKTREES_DIR ??
|
|
4240
|
+
defaultProjectsRoot = path11.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
|
|
4241
|
+
worktreesRoot = path11.resolve(
|
|
4242
|
+
process.env.AGENTRIFIC_WORKTREES_DIR ?? path11.join(homedir4(), ".agentrific", "workspaces")
|
|
3516
4243
|
);
|
|
3517
4244
|
execFileAsync = promisify3(execFile3);
|
|
3518
4245
|
if (host !== "127.0.0.1" && host !== "::1")
|
|
@@ -3535,13 +4262,18 @@ var init_index = __esm({
|
|
|
3535
4262
|
}
|
|
3536
4263
|
}
|
|
3537
4264
|
});
|
|
3538
|
-
dataRoot =
|
|
4265
|
+
dataRoot = path11.resolve(process.env.AGENTRIFIC_DATA_DIR ?? "./data");
|
|
3539
4266
|
releaseLock = await acquireServerLock(dataRoot);
|
|
3540
|
-
effects = ManagedRuntime.make(SessionStore.layer(
|
|
4267
|
+
effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
|
|
3541
4268
|
store = await effects.runPromise(SessionStore);
|
|
3542
|
-
security = new ServerSecurity(store, await
|
|
4269
|
+
security = new ServerSecurity(store, await Effect16.runPromise(store.serverIdentity()));
|
|
4270
|
+
testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
|
|
4271
|
+
if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
|
|
4272
|
+
throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
|
|
4273
|
+
push = new ServerPush(store, security.serverId, dataRoot, fetch, testPushUrl);
|
|
3543
4274
|
allowedOrigins = [
|
|
3544
4275
|
.../* @__PURE__ */ new Set([
|
|
4276
|
+
"agentrific-desktop://app",
|
|
3545
4277
|
...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
|
|
3546
4278
|
`http://${host === "::1" ? "[::1]" : host}:${port}`,
|
|
3547
4279
|
...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
|
|
@@ -3552,14 +4284,15 @@ var init_index = __esm({
|
|
|
3552
4284
|
methods: ["GET", "POST", "PATCH", "DELETE"]
|
|
3553
4285
|
});
|
|
3554
4286
|
await registerSecurity(app, security, serverVersion, allowedOrigins);
|
|
4287
|
+
registerPushRoutes(app, push, security);
|
|
3555
4288
|
attachmentEffects = ManagedRuntime.make(
|
|
3556
|
-
Attachments.layer(
|
|
4289
|
+
Attachments.layer(path11.join(dataRoot, "attachments"), store)
|
|
3557
4290
|
);
|
|
3558
4291
|
attachments = await attachmentEffects.runPromise(Attachments);
|
|
3559
|
-
await
|
|
4292
|
+
await Effect16.runPromise(attachments.cleanup());
|
|
3560
4293
|
attachmentCleanup = setInterval(
|
|
3561
4294
|
() => {
|
|
3562
|
-
void
|
|
4295
|
+
void Effect16.runPromise(attachments.cleanup()).catch((error) => app.log.error(error));
|
|
3563
4296
|
},
|
|
3564
4297
|
60 * 60 * 1e3
|
|
3565
4298
|
);
|
|
@@ -3570,14 +4303,14 @@ var init_index = __esm({
|
|
|
3570
4303
|
app.post(
|
|
3571
4304
|
"/api/projects/:projectId/attachments",
|
|
3572
4305
|
async (request, reply) => {
|
|
3573
|
-
const root = await
|
|
3574
|
-
const projects = await
|
|
4306
|
+
const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4307
|
+
const projects = await Effect16.runPromise(discoverProjects(root, store));
|
|
3575
4308
|
if (!projects.some((project) => project.id === request.params.projectId && project.available))
|
|
3576
4309
|
return reply.code(404).send({ error: "Project not found." });
|
|
3577
4310
|
try {
|
|
3578
4311
|
const file = await request.file();
|
|
3579
4312
|
if (!file) return reply.code(400).send({ error: "Choose a file to upload." });
|
|
3580
|
-
return reply.code(201).send(await
|
|
4313
|
+
return reply.code(201).send(await Effect16.runPromise(attachments.upload(request.params.projectId, file)));
|
|
3581
4314
|
} catch (error) {
|
|
3582
4315
|
return reply.code(400).send({ error: error instanceof Error ? error.message : "Upload failed." });
|
|
3583
4316
|
}
|
|
@@ -3586,7 +4319,7 @@ var init_index = __esm({
|
|
|
3586
4319
|
app.get(
|
|
3587
4320
|
"/api/projects/:projectId/attachments/:id/content",
|
|
3588
4321
|
async (request, reply) => {
|
|
3589
|
-
const attachment = await
|
|
4322
|
+
const attachment = await Effect16.runPromise(store.getAttachment(request.params.id));
|
|
3590
4323
|
if (!attachment || attachment.projectId !== request.params.projectId)
|
|
3591
4324
|
return reply.code(404).send({ error: "Attachment not found." });
|
|
3592
4325
|
const preview = request.query.preview === "1" && attachment.kind === "image";
|
|
@@ -3604,8 +4337,8 @@ var init_index = __esm({
|
|
|
3604
4337
|
publish: (event) => publish(event, true),
|
|
3605
4338
|
draft: deliveryWriter(models),
|
|
3606
4339
|
projectPath: async (session) => {
|
|
3607
|
-
const projectsRoot = await
|
|
3608
|
-
const project = (await
|
|
4340
|
+
const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4341
|
+
const project = (await Effect16.runPromise(discoverProjects(projectsRoot, store))).find(
|
|
3609
4342
|
(item) => item.id === session.projectId && item.available
|
|
3610
4343
|
);
|
|
3611
4344
|
if (!project) throw new Error("Reconnect the project folder before delivering changes.");
|
|
@@ -3618,12 +4351,27 @@ var init_index = __esm({
|
|
|
3618
4351
|
createPiFactory(attachments, models, delivery),
|
|
3619
4352
|
publish,
|
|
3620
4353
|
worktreesRoot,
|
|
3621
|
-
|
|
3622
|
-
(sessionId, messageId) =>
|
|
4354
|
+
path11.join(dataRoot, "pi"),
|
|
4355
|
+
(sessionId, messageId) => Effect16.runSync(store.hasMessage(sessionId, messageId)),
|
|
3623
4356
|
(session) => delivery.settled(session.id)
|
|
3624
4357
|
)
|
|
3625
4358
|
);
|
|
3626
4359
|
runtime = await agentEffects.runPromise(AgentSessions);
|
|
4360
|
+
worktreeCleanup = new WorktreeCleanup(store, worktreesRoot, defaultProjectsRoot);
|
|
4361
|
+
maxWorktreeAgeDays = Number(process.env.AGENTRIFIC_DONE_WORKTREE_MAX_AGE_DAYS ?? 0);
|
|
4362
|
+
if (!Number.isInteger(maxWorktreeAgeDays) || maxWorktreeAgeDays < 0)
|
|
4363
|
+
throw new Error("AGENTRIFIC_DONE_WORKTREE_MAX_AGE_DAYS must be a nonnegative integer.");
|
|
4364
|
+
pruneWorktrees = () => withTaskLock("worktree-pruning", async () => {
|
|
4365
|
+
await worktreeCleanup.prune(maxWorktreeAgeDays);
|
|
4366
|
+
});
|
|
4367
|
+
worktreeCleanupTimer = setInterval(
|
|
4368
|
+
() => {
|
|
4369
|
+
void pruneWorktrees().catch((error) => app.log.error(error));
|
|
4370
|
+
},
|
|
4371
|
+
60 * 60 * 1e3
|
|
4372
|
+
);
|
|
4373
|
+
worktreeCleanupTimer.unref();
|
|
4374
|
+
void pruneWorktrees().catch((error) => app.log.error(error));
|
|
3627
4375
|
updates = new UpdateDiscovery(store);
|
|
3628
4376
|
maintenance = new Maintenance(() => [
|
|
3629
4377
|
...runtime.workloads(),
|
|
@@ -3639,7 +4387,7 @@ var init_index = __esm({
|
|
|
3639
4387
|
() => maintenance.ready(),
|
|
3640
4388
|
(error) => app.log.error(error)
|
|
3641
4389
|
);
|
|
3642
|
-
for (const session of await
|
|
4390
|
+
for (const session of await Effect16.runPromise(store.list())) {
|
|
3643
4391
|
if (session.gitDelivery?.status === "pending") {
|
|
3644
4392
|
publish(
|
|
3645
4393
|
{
|
|
@@ -3669,7 +4417,7 @@ var init_index = __esm({
|
|
|
3669
4417
|
await delivery.recover();
|
|
3670
4418
|
app.get("/api/connections", async () => githubCliStatus());
|
|
3671
4419
|
app.get("/api/settings", async () => ({
|
|
3672
|
-
projectsRoot: await
|
|
4420
|
+
projectsRoot: await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot)),
|
|
3673
4421
|
defaultModel: await models.defaultIdentity()
|
|
3674
4422
|
}));
|
|
3675
4423
|
app.get("/api/models", async (_request, reply) => {
|
|
@@ -3697,7 +4445,7 @@ var init_index = __esm({
|
|
|
3697
4445
|
try {
|
|
3698
4446
|
const defaultModel = await models.setDefault(parsed.data);
|
|
3699
4447
|
return {
|
|
3700
|
-
projectsRoot: await
|
|
4448
|
+
projectsRoot: await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot)),
|
|
3701
4449
|
defaultModel
|
|
3702
4450
|
};
|
|
3703
4451
|
} catch {
|
|
@@ -3706,54 +4454,57 @@ var init_index = __esm({
|
|
|
3706
4454
|
});
|
|
3707
4455
|
}
|
|
3708
4456
|
});
|
|
3709
|
-
app.post(
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
return reply.code(
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
4457
|
+
app.post(
|
|
4458
|
+
"/api/sessions/:id/model",
|
|
4459
|
+
async (request, reply) => withSessionTaskLock(request.params.id, async () => {
|
|
4460
|
+
const parsed = ModelReferenceSchema.safeParse(request.body);
|
|
4461
|
+
const session = await Effect16.runPromise(store.get(request.params.id));
|
|
4462
|
+
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
4463
|
+
if (!parsed.success)
|
|
4464
|
+
return reply.code(400).send({ error: "Choose a model from the server catalogue." });
|
|
4465
|
+
try {
|
|
4466
|
+
await models.resolve(parsed.data);
|
|
4467
|
+
return await Effect16.runPromise(runtime.setModel(session, parsed.data));
|
|
4468
|
+
} catch {
|
|
4469
|
+
return reply.code(409).send({
|
|
4470
|
+
error: "Could not change the model. Wait for the current reply to finish and choose an available model."
|
|
4471
|
+
});
|
|
4472
|
+
}
|
|
4473
|
+
})
|
|
4474
|
+
);
|
|
3724
4475
|
app.post("/api/settings", async (request, reply) => {
|
|
3725
4476
|
const parsed = UpdateSettingsSchema.safeParse(request.body);
|
|
3726
4477
|
if (!parsed.success) return reply.code(400).send({ error: "Enter a projects folder." });
|
|
3727
|
-
const result = await
|
|
3728
|
-
resolveProjectsRoot(parsed.data.projectsRoot).pipe(
|
|
4478
|
+
const result = await Effect16.runPromise(
|
|
4479
|
+
resolveProjectsRoot(parsed.data.projectsRoot).pipe(Effect16.result)
|
|
3729
4480
|
);
|
|
3730
4481
|
if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
|
|
3731
|
-
return
|
|
4482
|
+
return Effect16.runPromise(store.setProjectsRoot(result.success));
|
|
3732
4483
|
});
|
|
3733
4484
|
app.get("/api/projects", async (_request, reply) => {
|
|
3734
|
-
const root = await
|
|
3735
|
-
const result = await
|
|
4485
|
+
const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4486
|
+
const result = await Effect16.runPromise(discoverProjects(root, store).pipe(Effect16.result));
|
|
3736
4487
|
if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
|
|
3737
4488
|
return result.success;
|
|
3738
4489
|
});
|
|
3739
4490
|
app.get("/api/projects/:id", async (request, reply) => {
|
|
3740
|
-
const root = await
|
|
3741
|
-
const result = await
|
|
4491
|
+
const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4492
|
+
const result = await Effect16.runPromise(discoverProjects(root, store).pipe(Effect16.result));
|
|
3742
4493
|
if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
|
|
3743
4494
|
const project = result.success.find((item) => item.id === request.params.id);
|
|
3744
4495
|
return project ?? reply.code(404).send({ error: "Project not found on this server." });
|
|
3745
4496
|
});
|
|
3746
4497
|
app.post("/api/projects/:id/reconnect", async (request, reply) => {
|
|
3747
|
-
const parsed =
|
|
4498
|
+
const parsed = z17.object({ path: z17.string().trim().min(1).max(4096) }).safeParse(request.body);
|
|
3748
4499
|
if (!parsed.success)
|
|
3749
4500
|
return reply.code(400).send({ error: "Enter the project's new folder path." });
|
|
3750
|
-
const root = await
|
|
3751
|
-
const result = await
|
|
3752
|
-
reconnectProject(root, request.params.id, parsed.data.path, store).pipe(
|
|
4501
|
+
const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4502
|
+
const result = await Effect16.runPromise(
|
|
4503
|
+
reconnectProject(root, request.params.id, parsed.data.path, store).pipe(Effect16.result)
|
|
3753
4504
|
);
|
|
3754
4505
|
return result._tag === "Failure" ? reply.code(400).send({ error: result.failure.message }) : result.success;
|
|
3755
4506
|
});
|
|
3756
|
-
app.get("/api/sessions", () =>
|
|
4507
|
+
app.get("/api/sessions", () => Effect16.runPromise(store.list()));
|
|
3757
4508
|
app.get(
|
|
3758
4509
|
"/api/tasks",
|
|
3759
4510
|
(request) => boardTasks(store, request.query.projectId)
|
|
@@ -3766,15 +4517,15 @@ var init_index = __esm({
|
|
|
3766
4517
|
if (!github.githubCli.authenticated)
|
|
3767
4518
|
return reply.code(400).send({ error: "GitHub CLI must be authenticated on the server to open a PR." });
|
|
3768
4519
|
}
|
|
3769
|
-
const projectsRoot = await
|
|
3770
|
-
const projects = await
|
|
3771
|
-
discoverProjects(projectsRoot, store).pipe(
|
|
4520
|
+
const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4521
|
+
const projects = await Effect16.runPromise(
|
|
4522
|
+
discoverProjects(projectsRoot, store).pipe(Effect16.result)
|
|
3772
4523
|
);
|
|
3773
4524
|
if (projects._tag === "Failure" || !projects.success.some((project) => project.id === parsed.data.projectId && project.available))
|
|
3774
4525
|
return reply.code(404).send({ error: "Project not found in the server's projects folder." });
|
|
3775
4526
|
let files;
|
|
3776
4527
|
try {
|
|
3777
|
-
files = await
|
|
4528
|
+
files = await Effect16.runPromise(
|
|
3778
4529
|
attachments.resolve(parsed.data.projectId, parsed.data.attachmentIds ?? [])
|
|
3779
4530
|
);
|
|
3780
4531
|
} catch (error) {
|
|
@@ -3782,7 +4533,7 @@ var init_index = __esm({
|
|
|
3782
4533
|
}
|
|
3783
4534
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3784
4535
|
const task = TaskSchema.parse({
|
|
3785
|
-
id:
|
|
4536
|
+
id: randomUUID8(),
|
|
3786
4537
|
...parsed.data,
|
|
3787
4538
|
attachments: files,
|
|
3788
4539
|
status: "backlog",
|
|
@@ -3790,7 +4541,7 @@ var init_index = __esm({
|
|
|
3790
4541
|
createdAt: now,
|
|
3791
4542
|
updatedAt: now
|
|
3792
4543
|
});
|
|
3793
|
-
return reply.code(201).send(await
|
|
4544
|
+
return reply.code(201).send(await Effect16.runPromise(store.saveTask(task)));
|
|
3794
4545
|
});
|
|
3795
4546
|
app.patch("/api/tasks/:id", async (request, reply) => {
|
|
3796
4547
|
const parsed = UpdateTaskSchema.safeParse(request.body);
|
|
@@ -3805,8 +4556,8 @@ var init_index = __esm({
|
|
|
3805
4556
|
return reply.code(400).send({ error: "GitHub CLI must be authenticated on the server to open a PR." });
|
|
3806
4557
|
}
|
|
3807
4558
|
return await editTask(store, request.params.id, parsed.data, async (projectId, ids) => {
|
|
3808
|
-
const result = await
|
|
3809
|
-
attachments.resolve(projectId, ids).pipe(
|
|
4559
|
+
const result = await Effect16.runPromise(
|
|
4560
|
+
attachments.resolve(projectId, ids).pipe(Effect16.result)
|
|
3810
4561
|
);
|
|
3811
4562
|
if (result._tag === "Failure") throw new TaskActionError(result.failure.message, 400);
|
|
3812
4563
|
return result.success;
|
|
@@ -3819,7 +4570,12 @@ var init_index = __esm({
|
|
|
3819
4570
|
});
|
|
3820
4571
|
app.delete("/api/tasks/:id", async (request, reply) => {
|
|
3821
4572
|
try {
|
|
3822
|
-
await deleteTask(store, request.params.id)
|
|
4573
|
+
await deleteTask(store, request.params.id, async (session) => {
|
|
4574
|
+
await Effect16.runPromise(runtime.forget(session.id));
|
|
4575
|
+
await worktreeCleanup.remove(session);
|
|
4576
|
+
await removeRunFiles(dataRoot, session.id);
|
|
4577
|
+
});
|
|
4578
|
+
await Effect16.runPromise(attachments.cleanup());
|
|
3823
4579
|
return reply.code(204).send();
|
|
3824
4580
|
} catch (error) {
|
|
3825
4581
|
if (error instanceof TaskActionError)
|
|
@@ -3828,31 +4584,34 @@ var init_index = __esm({
|
|
|
3828
4584
|
}
|
|
3829
4585
|
});
|
|
3830
4586
|
app.get("/api/sessions/:id", async (request, reply) => {
|
|
3831
|
-
const session = await
|
|
4587
|
+
const session = await Effect16.runPromise(store.get(request.params.id));
|
|
3832
4588
|
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
3833
4589
|
return session;
|
|
3834
4590
|
});
|
|
3835
|
-
app.post(
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
4591
|
+
app.post(
|
|
4592
|
+
"/api/sessions/:id/delivery",
|
|
4593
|
+
async (request, reply) => withSessionTaskLock(request.params.id, async () => {
|
|
4594
|
+
const session = await Effect16.runPromise(store.get(request.params.id));
|
|
4595
|
+
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
4596
|
+
try {
|
|
4597
|
+
const result = await delivery.retry(session.id);
|
|
4598
|
+
return reply.code(202).send(result);
|
|
4599
|
+
} catch (error) {
|
|
4600
|
+
return reply.code(409).send({ error: error instanceof Error ? error.message : "Delivery could not be retried." });
|
|
4601
|
+
}
|
|
4602
|
+
})
|
|
4603
|
+
);
|
|
3845
4604
|
app.post(
|
|
3846
4605
|
"/api/sessions/:id/preparation/retry",
|
|
3847
|
-
async (request, reply) => {
|
|
4606
|
+
async (request, reply) => withSessionTaskLock(request.params.id, async () => {
|
|
3848
4607
|
let preparedSession;
|
|
3849
4608
|
try {
|
|
3850
4609
|
const prepared = await preparation.retry(request.params.id);
|
|
3851
4610
|
preparedSession = prepared.session;
|
|
3852
|
-
const files = await
|
|
4611
|
+
const files = await Effect16.runPromise(
|
|
3853
4612
|
attachments.resolve(prepared.session.projectId, prepared.input.attachmentIds ?? [])
|
|
3854
4613
|
);
|
|
3855
|
-
await
|
|
4614
|
+
await Effect16.runPromise(runtime.send(prepared.session, prepared.input.prompt, files));
|
|
3856
4615
|
return reply.code(202).send(prepared.session);
|
|
3857
4616
|
} catch (error) {
|
|
3858
4617
|
if (preparedSession) {
|
|
@@ -3866,18 +4625,18 @@ var init_index = __esm({
|
|
|
3866
4625
|
error: error instanceof Error ? error.message : "Preparation could not be retried."
|
|
3867
4626
|
});
|
|
3868
4627
|
}
|
|
3869
|
-
}
|
|
4628
|
+
})
|
|
3870
4629
|
);
|
|
3871
|
-
HistoryQuery =
|
|
3872
|
-
after:
|
|
3873
|
-
limit:
|
|
4630
|
+
HistoryQuery = z17.object({
|
|
4631
|
+
after: z17.coerce.number().int().nonnegative().default(0),
|
|
4632
|
+
limit: z17.coerce.number().int().min(1).max(1e3).default(500)
|
|
3874
4633
|
});
|
|
3875
4634
|
app.get("/api/sessions/:id/events", async (request, reply) => {
|
|
3876
4635
|
const query = HistoryQuery.safeParse(request.query);
|
|
3877
4636
|
if (!query.success) return reply.code(400).send({ error: "Invalid history cursor or limit." });
|
|
3878
|
-
const session = await
|
|
4637
|
+
const session = await Effect16.runPromise(store.get(request.params.id));
|
|
3879
4638
|
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
3880
|
-
return
|
|
4639
|
+
return Effect16.runPromise(store.history(session.id, query.data.after, query.data.limit));
|
|
3881
4640
|
});
|
|
3882
4641
|
app.post("/api/sessions", async (request, reply) => {
|
|
3883
4642
|
const parsed = CreateSessionSchema.safeParse(request.body);
|
|
@@ -3889,7 +4648,7 @@ var init_index = __esm({
|
|
|
3889
4648
|
if (!result.session) return reply.code(result.status).send({ error: result.error });
|
|
3890
4649
|
return reply.code(201).send(result.session);
|
|
3891
4650
|
});
|
|
3892
|
-
StartTaskBody =
|
|
4651
|
+
StartTaskBody = z17.object({ prompt: z17.string().trim().max(2e4).optional() }).default({});
|
|
3893
4652
|
app.post("/api/tasks/:id/start", async (request, reply) => {
|
|
3894
4653
|
const body = StartTaskBody.safeParse(request.body ?? {});
|
|
3895
4654
|
if (!body.success) return reply.code(400).send({ error: "The task prompt is too long." });
|
|
@@ -3913,8 +4672,8 @@ var init_index = __esm({
|
|
|
3913
4672
|
const parsed = AutomationConfigSchema.safeParse(request.body);
|
|
3914
4673
|
if (!parsed.success)
|
|
3915
4674
|
return reply.code(400).send({ error: parsed.error.issues.map((issue) => issue.message).join(" ") });
|
|
3916
|
-
const root = await
|
|
3917
|
-
const projects = await
|
|
4675
|
+
const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4676
|
+
const projects = await Effect16.runPromise(discoverProjects(root, store));
|
|
3918
4677
|
if (parsed.data.projectIds.some(
|
|
3919
4678
|
(id) => !projects.some((project) => project.id === id && project.available)
|
|
3920
4679
|
))
|
|
@@ -3924,7 +4683,7 @@ var init_index = __esm({
|
|
|
3924
4683
|
return automation.configure(parsed.data);
|
|
3925
4684
|
});
|
|
3926
4685
|
app.patch("/api/automation/queue", async (request, reply) => {
|
|
3927
|
-
const parsed =
|
|
4686
|
+
const parsed = z17.object({ taskId: z17.string().min(1), beforeId: z17.string().min(1).nullable() }).safeParse(request.body);
|
|
3928
4687
|
if (!parsed.success)
|
|
3929
4688
|
return reply.code(400).send({ error: "Choose a task and a queue position." });
|
|
3930
4689
|
try {
|
|
@@ -3941,8 +4700,8 @@ var init_index = __esm({
|
|
|
3941
4700
|
automationTimer.unref();
|
|
3942
4701
|
app.post(
|
|
3943
4702
|
"/api/sessions/:id/messages",
|
|
3944
|
-
async (request, reply) => {
|
|
3945
|
-
const session = await
|
|
4703
|
+
async (request, reply) => withSessionTaskLock(request.params.id, async () => {
|
|
4704
|
+
const session = await Effect16.runPromise(store.get(request.params.id));
|
|
3946
4705
|
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
3947
4706
|
const parsed = SendMessageSchema.safeParse(request.body);
|
|
3948
4707
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
@@ -3950,27 +4709,30 @@ var init_index = __esm({
|
|
|
3950
4709
|
await delivery.assertCanWork(session.id);
|
|
3951
4710
|
if (session.preparation && session.preparation.status !== "ready")
|
|
3952
4711
|
throw new Error("Retry workspace preparation before sending a message.");
|
|
3953
|
-
const files = await
|
|
4712
|
+
const files = await Effect16.runPromise(
|
|
3954
4713
|
attachments.resolve(session.projectId, parsed.data.attachmentIds ?? [])
|
|
3955
4714
|
);
|
|
3956
|
-
await
|
|
4715
|
+
await Effect16.runPromise(
|
|
3957
4716
|
runtime.send(session, parsed.data.message, files, parsed.data.clientMessageId)
|
|
3958
4717
|
);
|
|
3959
4718
|
return reply.code(202).send({ accepted: true });
|
|
3960
4719
|
} catch (error) {
|
|
3961
4720
|
return reply.code(409).send({ error: String(error) });
|
|
3962
4721
|
}
|
|
3963
|
-
}
|
|
4722
|
+
})
|
|
4723
|
+
);
|
|
4724
|
+
app.post(
|
|
4725
|
+
"/api/sessions/:id/stop",
|
|
4726
|
+
async (request, reply) => withSessionTaskLock(request.params.id, async () => {
|
|
4727
|
+
const session = await Effect16.runPromise(store.get(request.params.id));
|
|
4728
|
+
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
4729
|
+
await Effect16.runPromise(runtime.stop(session));
|
|
4730
|
+
return reply.code(202).send({ accepted: true });
|
|
4731
|
+
})
|
|
3964
4732
|
);
|
|
3965
|
-
app.post("/api/sessions/:id/stop", async (request, reply) => {
|
|
3966
|
-
const session = await Effect14.runPromise(store.get(request.params.id));
|
|
3967
|
-
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
3968
|
-
await Effect14.runPromise(runtime.stop(session));
|
|
3969
|
-
return reply.code(202).send({ accepted: true });
|
|
3970
|
-
});
|
|
3971
4733
|
attachEventServer(app, security, allowedOrigins, () => ({
|
|
3972
4734
|
type: "snapshot",
|
|
3973
|
-
sessions:
|
|
4735
|
+
sessions: Effect16.runSync(store.list())
|
|
3974
4736
|
}));
|
|
3975
4737
|
closeControl = await startControl(
|
|
3976
4738
|
dataRoot,
|
|
@@ -3982,20 +4744,25 @@ var init_index = __esm({
|
|
|
3982
4744
|
connectedDevices: new Set(security.sockets.values()).size,
|
|
3983
4745
|
connectedSockets: security.sockets.size,
|
|
3984
4746
|
memory: process.memoryUsage().rss,
|
|
3985
|
-
activeRuns: activeAgents(
|
|
4747
|
+
activeRuns: activeAgents(Effect16.runSync(store.list())),
|
|
3986
4748
|
maintenance: maintenance.status()
|
|
3987
4749
|
}),
|
|
3988
4750
|
maintenance
|
|
3989
4751
|
);
|
|
4752
|
+
push.start();
|
|
3990
4753
|
app.addHook("onClose", async () => {
|
|
4754
|
+
await push.close();
|
|
3991
4755
|
maintenance.enter();
|
|
3992
4756
|
stopUpdateDiscovery();
|
|
3993
4757
|
await updates.settle();
|
|
3994
4758
|
clearInterval(attachmentCleanup);
|
|
4759
|
+
clearInterval(worktreeCleanupTimer);
|
|
4760
|
+
await withTaskLock("worktree-pruning", async () => {
|
|
4761
|
+
});
|
|
3995
4762
|
clearInterval(automationTimer);
|
|
3996
4763
|
await withTaskLock("automation", async () => {
|
|
3997
4764
|
});
|
|
3998
|
-
await
|
|
4765
|
+
await Effect16.runPromise(runtime.close());
|
|
3999
4766
|
await agentEffects.dispose();
|
|
4000
4767
|
await attachmentEffects.dispose();
|
|
4001
4768
|
await effects.dispose();
|
|
@@ -4018,7 +4785,7 @@ var init_index = __esm({
|
|
|
4018
4785
|
init_warnings();
|
|
4019
4786
|
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
|
|
4020
4787
|
import { homedir as homedir5 } from "node:os";
|
|
4021
|
-
import
|
|
4788
|
+
import path12 from "node:path";
|
|
4022
4789
|
import { fileURLToPath } from "node:url";
|
|
4023
4790
|
|
|
4024
4791
|
// src/service.ts
|
|
@@ -4114,204 +4881,21 @@ async function installService(platform, directory, filename, domain, dataDir, cl
|
|
|
4114
4881
|
init_updates();
|
|
4115
4882
|
init_control();
|
|
4116
4883
|
init_version();
|
|
4884
|
+
init_pairing();
|
|
4117
4885
|
import { spawn as spawn2 } from "node:child_process";
|
|
4118
4886
|
import { parseArgs } from "node:util";
|
|
4119
|
-
import { z as
|
|
4120
|
-
|
|
4121
|
-
// src/pairing.ts
|
|
4122
|
-
init_src();
|
|
4123
|
-
init_control();
|
|
4124
|
-
import { z as z4 } from "zod";
|
|
4125
|
-
import { styleText as styleText2 } from "node:util";
|
|
4126
|
-
import { emitKeypressEvents } from "node:readline";
|
|
4127
|
-
import qrcode from "qrcode-terminal";
|
|
4128
|
-
|
|
4129
|
-
// src/cli-output.ts
|
|
4130
|
-
init_version();
|
|
4131
|
-
import { styleText } from "node:util";
|
|
4132
|
-
function printHeading(title) {
|
|
4133
|
-
console.log();
|
|
4134
|
-
console.log(styleText("bold", " ####### ##"));
|
|
4135
|
-
console.log(styleText("bold", " ### #####"));
|
|
4136
|
-
console.log(` ${styleText("bold", "## ####")} ${styleText("bold", "AGENTRIFIC")}`);
|
|
4137
|
-
console.log(
|
|
4138
|
-
` ${styleText("bold", "## ")}${styleText(["bold", "yellow"], "o")}${styleText("bold", " ####")} ${styleText("dim", `${title} \xB7 v${serverVersion}`)}`
|
|
4139
|
-
);
|
|
4140
|
-
console.log(styleText("bold", " ## ####"));
|
|
4141
|
-
console.log(styleText("bold", " ### #####"));
|
|
4142
|
-
console.log(styleText("bold", " ####### ##"));
|
|
4143
|
-
console.log();
|
|
4144
|
-
}
|
|
4145
|
-
function printDetail(label2, value) {
|
|
4146
|
-
console.log(` ${styleText("dim", "\u203A")} ${styleText("dim", label2.padEnd(10))} ${value}`);
|
|
4147
|
-
}
|
|
4148
|
-
function printShortcuts(pairingAvailable = false) {
|
|
4149
|
-
console.log();
|
|
4150
|
-
console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "p")} \u2502 pair a device`);
|
|
4151
|
-
if (pairingAvailable)
|
|
4152
|
-
console.log(
|
|
4153
|
-
` ${styleText("dim", "\u203A")} Press ${styleText("bold", "j")} \u2502 show pairing JSON`
|
|
4154
|
-
);
|
|
4155
|
-
console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "?")} \u2502 show shortcuts`);
|
|
4156
|
-
console.log(
|
|
4157
|
-
` ${styleText("dim", "\u203A")} Press ${styleText("bold", "Ctrl+C")} \u2502 stop the server
|
|
4158
|
-
`
|
|
4159
|
-
);
|
|
4160
|
-
}
|
|
4161
|
-
function printError(message) {
|
|
4162
|
-
console.error(`
|
|
4163
|
-
${styleText(["bold", "red"], "Error")} ${message}
|
|
4164
|
-
`);
|
|
4165
|
-
}
|
|
4166
|
-
function printHelp() {
|
|
4167
|
-
printHeading("server");
|
|
4168
|
-
console.log(` ${styleText("bold", "Usage")} agentrific <command> [options]
|
|
4169
|
-
`);
|
|
4170
|
-
for (const [command, description] of [
|
|
4171
|
-
["init", "Configure the server"],
|
|
4172
|
-
["start", "Start the server \xB7 press p to pair"],
|
|
4173
|
-
["pair", "Print a fresh pairing QR code and PIN"],
|
|
4174
|
-
["devices", "List enrolled devices"],
|
|
4175
|
-
["revoke <id>", "Revoke a device's access"],
|
|
4176
|
-
["status", "Show server status"],
|
|
4177
|
-
["maintenance enter/resume/drain", "Pause new work, resume, or wait for work to finish"],
|
|
4178
|
-
["service install/uninstall/restart", "Manage the background service"],
|
|
4179
|
-
["logs", "Follow server logs"],
|
|
4180
|
-
["update --check", "Check for a newer release"]
|
|
4181
|
-
])
|
|
4182
|
-
console.log(` ${styleText("cyan", command.padEnd(34))}${description}`);
|
|
4183
|
-
console.log(`
|
|
4184
|
-
${styleText("bold", "Options")}
|
|
4185
|
-
`);
|
|
4186
|
-
for (const [option, description] of [
|
|
4187
|
-
["--data-dir <path>", "Use a specific server data directory"],
|
|
4188
|
-
["--public-url <https-url>", "Use your tunnel URL without prompting"],
|
|
4189
|
-
["--no-interactive", "Disable prompts and keyboard shortcuts"],
|
|
4190
|
-
["--show-payload", "Print manual JSON with the pairing QR code"],
|
|
4191
|
-
["--timeout-ms <0-60000>", "Set the maintenance drain timeout"],
|
|
4192
|
-
["--version", "Print the server version"],
|
|
4193
|
-
["--help", "Show this help"]
|
|
4194
|
-
])
|
|
4195
|
-
console.log(` ${styleText("cyan", option.padEnd(34))}${description}`);
|
|
4196
|
-
console.log("\n Connect using Tailscale Serve, cloudflared, or another HTTPS tunnel.\n");
|
|
4197
|
-
}
|
|
4198
|
-
|
|
4199
|
-
// src/pairing.ts
|
|
4200
|
-
function printManualPairing(invitation) {
|
|
4201
|
-
if (Date.now() >= invitation.expiresAt) {
|
|
4202
|
-
printError("This invitation expired. Generate a fresh pairing QR code.");
|
|
4203
|
-
return;
|
|
4204
|
-
}
|
|
4205
|
-
console.log(styleText2("dim", "\n Manual entry \xB7 paste this JSON into Pairing payload:\n"));
|
|
4206
|
-
console.log(` ${invitation.payload}
|
|
4207
|
-
`);
|
|
4208
|
-
}
|
|
4209
|
-
function waitForManualPairing(invitation) {
|
|
4210
|
-
console.log(styleText2("dim", " Press j to show manual entry \xB7 Enter or q to finish.\n"));
|
|
4211
|
-
return new Promise((resolve) => {
|
|
4212
|
-
const input = process.stdin;
|
|
4213
|
-
const previousRaw = input.isRaw;
|
|
4214
|
-
const timeout = setTimeout(
|
|
4215
|
-
() => {
|
|
4216
|
-
console.log(
|
|
4217
|
-
styleText2("dim", "\n Pairing invitation expired. Run agentrific pair again.\n")
|
|
4218
|
-
);
|
|
4219
|
-
close();
|
|
4220
|
-
},
|
|
4221
|
-
Math.max(0, invitation.expiresAt - Date.now())
|
|
4222
|
-
);
|
|
4223
|
-
function close() {
|
|
4224
|
-
clearTimeout(timeout);
|
|
4225
|
-
input.removeListener("keypress", pressed);
|
|
4226
|
-
input.removeListener("end", close);
|
|
4227
|
-
process.removeListener("SIGINT", interrupted);
|
|
4228
|
-
process.removeListener("SIGTERM", terminated);
|
|
4229
|
-
input.setRawMode(previousRaw);
|
|
4230
|
-
input.pause();
|
|
4231
|
-
resolve();
|
|
4232
|
-
}
|
|
4233
|
-
function interrupted() {
|
|
4234
|
-
process.exitCode = 130;
|
|
4235
|
-
close();
|
|
4236
|
-
}
|
|
4237
|
-
function terminated() {
|
|
4238
|
-
process.exitCode = 143;
|
|
4239
|
-
close();
|
|
4240
|
-
}
|
|
4241
|
-
function pressed(text, key) {
|
|
4242
|
-
if (key.ctrl && key.name === "c") {
|
|
4243
|
-
interrupted();
|
|
4244
|
-
return;
|
|
4245
|
-
}
|
|
4246
|
-
if (key.ctrl || key.meta) return;
|
|
4247
|
-
if (text?.toLowerCase() === "j") {
|
|
4248
|
-
printManualPairing(invitation);
|
|
4249
|
-
close();
|
|
4250
|
-
} else if (text?.toLowerCase() === "q" || key.name === "return" || key.name === "escape") {
|
|
4251
|
-
close();
|
|
4252
|
-
}
|
|
4253
|
-
}
|
|
4254
|
-
emitKeypressEvents(input);
|
|
4255
|
-
input.setRawMode(true);
|
|
4256
|
-
input.on("keypress", pressed);
|
|
4257
|
-
input.once("end", close);
|
|
4258
|
-
process.once("SIGINT", interrupted);
|
|
4259
|
-
process.once("SIGTERM", terminated);
|
|
4260
|
-
input.resume();
|
|
4261
|
-
});
|
|
4262
|
-
}
|
|
4263
|
-
function pairingOrigin(url) {
|
|
4264
|
-
const endpoint = new URL(url);
|
|
4265
|
-
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== "/")
|
|
4266
|
-
throw new Error("Use the HTTPS server origin without a path or credentials.");
|
|
4267
|
-
if (endpoint.protocol !== "https:" && !(process.env.NODE_ENV !== "production" && endpoint.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname)))
|
|
4268
|
-
throw new Error("Use HTTPS. Localhost HTTP is for development builds only.");
|
|
4269
|
-
return endpoint.origin;
|
|
4270
|
-
}
|
|
4271
|
-
async function printPairing(dataDir, url) {
|
|
4272
|
-
if (!url)
|
|
4273
|
-
throw new Error(
|
|
4274
|
-
"Specify --public-url with your Tailscale Serve or Cloudflare Tunnel HTTPS address."
|
|
4275
|
-
);
|
|
4276
|
-
const origin = pairingOrigin(url);
|
|
4277
|
-
const grant = z4.object({
|
|
4278
|
-
pairingId: z4.string(),
|
|
4279
|
-
pin: z4.string(),
|
|
4280
|
-
expiresAt: z4.number(),
|
|
4281
|
-
serverId: z4.string()
|
|
4282
|
-
}).parse(await controlRequest(dataDir, { command: "pair" }));
|
|
4283
|
-
const invitation = {
|
|
4284
|
-
v: 1,
|
|
4285
|
-
url: origin,
|
|
4286
|
-
serverId: grant.serverId,
|
|
4287
|
-
pairingId: grant.pairingId
|
|
4288
|
-
};
|
|
4289
|
-
const payload = JSON.stringify(invitation);
|
|
4290
|
-
const link = createPairingLink(invitation);
|
|
4291
|
-
printHeading("pairing");
|
|
4292
|
-
console.log(" 1. Scan below with your phone camera and tap Open Agentrific if offered.");
|
|
4293
|
-
console.log(" 2. Or open Agentrific and choose Scan pairing QR code.");
|
|
4294
|
-
console.log(" 3. Enter the PIN, then confirm pairing.\n");
|
|
4295
|
-
qrcode.generate(link, { small: true }, (code) => {
|
|
4296
|
-
console.log(
|
|
4297
|
-
code.split("\n").map((line) => ` ${line}`).join("\n")
|
|
4298
|
-
);
|
|
4299
|
-
});
|
|
4300
|
-
console.log();
|
|
4301
|
-
printDetail("PIN:", styleText2(["bold", "yellow"], grant.pin));
|
|
4302
|
-
printDetail("Server", styleText2("cyan", origin));
|
|
4303
|
-
printDetail("Expires", "In five minutes \xB7 one device per code");
|
|
4304
|
-
console.log("\n Pairing grants this device access to run agents as your host user.\n");
|
|
4305
|
-
return { payload, expiresAt: grant.expiresAt };
|
|
4306
|
-
}
|
|
4887
|
+
import { z as z18 } from "zod";
|
|
4307
4888
|
|
|
4308
4889
|
// src/terminal.ts
|
|
4890
|
+
init_pairing();
|
|
4891
|
+
init_cli_output();
|
|
4309
4892
|
import { createInterface } from "node:readline/promises";
|
|
4310
4893
|
import select from "@inquirer/select";
|
|
4311
4894
|
import { emitKeypressEvents as emitKeypressEvents2 } from "node:readline";
|
|
4312
4895
|
import { styleText as styleText3 } from "node:util";
|
|
4313
4896
|
|
|
4314
4897
|
// src/tunnels.ts
|
|
4898
|
+
init_cli_output();
|
|
4315
4899
|
import { spawn } from "node:child_process";
|
|
4316
4900
|
import { access, stat } from "node:fs/promises";
|
|
4317
4901
|
import { constants } from "node:fs";
|
|
@@ -4780,10 +5364,11 @@ function startTerminal(dataDir, publicUrl, tunnel) {
|
|
|
4780
5364
|
}
|
|
4781
5365
|
|
|
4782
5366
|
// src/cli.ts
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
5367
|
+
init_cli_output();
|
|
5368
|
+
var ConfigSchema = z18.object({
|
|
5369
|
+
dataDir: z18.string(),
|
|
5370
|
+
projectsDir: z18.string(),
|
|
5371
|
+
publicUrl: z18.string().url().optional()
|
|
4787
5372
|
});
|
|
4788
5373
|
function parseCommandArgs() {
|
|
4789
5374
|
return parseArgs({
|
|
@@ -4811,15 +5396,15 @@ async function main() {
|
|
|
4811
5396
|
printHelp();
|
|
4812
5397
|
return;
|
|
4813
5398
|
}
|
|
4814
|
-
const dataDir =
|
|
4815
|
-
values["data-dir"] ?? process.env.AGENTRIFIC_DATA_DIR ??
|
|
5399
|
+
const dataDir = path12.resolve(
|
|
5400
|
+
values["data-dir"] ?? process.env.AGENTRIFIC_DATA_DIR ?? path12.join(homedir5(), ".agentrific")
|
|
4816
5401
|
);
|
|
4817
|
-
const filename =
|
|
5402
|
+
const filename = path12.join(dataDir, "config.json");
|
|
4818
5403
|
const config = ConfigSchema.parse(
|
|
4819
5404
|
JSON.parse(
|
|
4820
5405
|
await readFile5(filename, "utf8").catch((error) => {
|
|
4821
|
-
if (!
|
|
4822
|
-
return JSON.stringify({ dataDir, projectsDir:
|
|
5406
|
+
if (!z18.object({ code: z18.literal("ENOENT") }).safeParse(error).success) throw error;
|
|
5407
|
+
return JSON.stringify({ dataDir, projectsDir: path12.join(homedir5(), "projects") });
|
|
4823
5408
|
})
|
|
4824
5409
|
)
|
|
4825
5410
|
);
|
|
@@ -4891,7 +5476,7 @@ async function serviceCommand({ positionals, dataDir }) {
|
|
|
4891
5476
|
return;
|
|
4892
5477
|
}
|
|
4893
5478
|
async function logsCommand({ dataDir }) {
|
|
4894
|
-
const child = process.platform === "linux" ? spawn2("journalctl", ["--user", "-u", "agentrific.service", "-f"], { stdio: "inherit" }) : spawn2("tail", ["-F",
|
|
5479
|
+
const child = process.platform === "linux" ? spawn2("journalctl", ["--user", "-u", "agentrific.service", "-f"], { stdio: "inherit" }) : spawn2("tail", ["-F", path12.join(dataDir, "logs", "server.log")], { stdio: "inherit" });
|
|
4895
5480
|
child.on("error", (error) => {
|
|
4896
5481
|
console.error(error.message);
|
|
4897
5482
|
process.exitCode = 1;
|
|
@@ -4925,11 +5510,11 @@ async function pairCommand({ values, dataDir, config }) {
|
|
|
4925
5510
|
return;
|
|
4926
5511
|
}
|
|
4927
5512
|
async function maintenanceCommand({ values, positionals, dataDir }) {
|
|
4928
|
-
const action =
|
|
5513
|
+
const action = z18.enum(["enter", "resume", "drain"]).parse(positionals[1]);
|
|
4929
5514
|
const result = await controlRequest(dataDir, {
|
|
4930
5515
|
command: "maintenance",
|
|
4931
5516
|
action,
|
|
4932
|
-
timeoutMs:
|
|
5517
|
+
timeoutMs: z18.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
|
|
4933
5518
|
});
|
|
4934
5519
|
console.log(JSON.stringify(result, null, 2));
|
|
4935
5520
|
if (action === "drain" && "drained" in result && !result.drained) process.exitCode = 1;
|
|
@@ -4938,7 +5523,7 @@ async function maintenanceCommand({ values, positionals, dataDir }) {
|
|
|
4938
5523
|
async function devicesCommand({ dataDir, command }) {
|
|
4939
5524
|
console.log(
|
|
4940
5525
|
JSON.stringify(
|
|
4941
|
-
await controlRequest(dataDir, { command:
|
|
5526
|
+
await controlRequest(dataDir, { command: z18.enum(["devices", "status"]).parse(command) }),
|
|
4942
5527
|
null,
|
|
4943
5528
|
2
|
|
4944
5529
|
)
|
|
@@ -4946,7 +5531,7 @@ async function devicesCommand({ dataDir, command }) {
|
|
|
4946
5531
|
return;
|
|
4947
5532
|
}
|
|
4948
5533
|
async function revokeCommand({ positionals, dataDir }) {
|
|
4949
|
-
const id =
|
|
5534
|
+
const id = z18.string().uuid().parse(positionals[1]);
|
|
4950
5535
|
console.log(JSON.stringify(await controlRequest(dataDir, { command: "revoke", id })));
|
|
4951
5536
|
return;
|
|
4952
5537
|
}
|