@agentrific/server 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -30,7 +30,7 @@ var init_package = __esm({
30
30
  "package.json"() {
31
31
  package_default = {
32
32
  name: "@agentrific/server",
33
- version: "0.1.5",
33
+ version: "0.2.0",
34
34
  description: "Self-hosted control plane for long-running coding agents",
35
35
  keywords: [
36
36
  "agentrific",
@@ -67,6 +67,7 @@ var init_package = __esm({
67
67
  "@fastify/cors": "^11.3.0",
68
68
  "@fastify/multipart": "^10.1.2",
69
69
  "@inquirer/select": "^5.2.5",
70
+ diff: "^9.0.0",
70
71
  effect: "4.0.0-rc.117",
71
72
  fastify: "^5.6.2",
72
73
  "pino-pretty": "^13.1.3",
@@ -245,7 +246,7 @@ function createPairingLink(payload) {
245
246
  link.searchParams.set("data", JSON.stringify(PairingPayloadSchema.parse(payload)));
246
247
  return link.toString();
247
248
  }
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;
249
+ var cronBounds, AutomationConfigSchema, AutomationStateSchema, SessionStatusSchema, AgentKindSchema, ModelReferenceSchema, ModelIdentitySchema, ModelOptionSchema, ModelCatalogSchema, GitFlowSchema, GitDeliverySchema, DeliveryRequestSchema, RunPreparationSchema, SessionSchema, MAX_ATTACHMENTS, MAX_ATTACHMENT_BYTES, AttachmentSchema, AttachmentIdsSchema, CreateSessionSchema, SendMessageSchema, FileChangeSchema, SessionEventSchema, RecordedEventSchema, EventHistorySchema, ServerEventSchema, ServerSettingsSchema, ConnectionsStatusSchema, UpdateSettingsSchema, ProjectSchema, TaskStatusSchema, TaskSchema, CreateTaskSchema, UpdateTaskSchema, PROTOCOL_VERSION, ServerUpdateStatusSchema, ServerHealthSchema, PairingPayloadSchema, PairingInvitationSchema, PairingRequestSchema, DeviceSchema, PairingResultSchema, HandshakeSchema, HandshakeAckSchema;
249
250
  var init_src = __esm({
250
251
  "../../packages/contracts/src/index.ts"() {
251
252
  cronBounds = [
@@ -381,11 +382,18 @@ var init_src = __esm({
381
382
  SendMessageSchema = z2.object({
382
383
  message: z2.string().trim().max(2e4),
383
384
  attachmentIds: AttachmentIdsSchema.optional(),
384
- clientMessageId: z2.string().min(1).max(120).optional()
385
+ clientMessageId: z2.string().min(1).max(120).optional(),
386
+ mode: z2.enum(["followUp", "steer"]).optional()
385
387
  }).refine(
386
388
  (input) => input.message.length > 0 || (input.attachmentIds?.length ?? 0) > 0,
387
389
  "Write a message or attach a file."
388
390
  );
391
+ FileChangeSchema = z2.object({
392
+ path: z2.string(),
393
+ patch: z2.string().optional(),
394
+ content: z2.string().optional(),
395
+ omitted: z2.boolean().optional()
396
+ });
389
397
  SessionEventSchema = z2.discriminatedUnion("type", [
390
398
  z2.object({ type: z2.literal("session.updated"), session: SessionSchema }),
391
399
  z2.object({
@@ -420,7 +428,8 @@ var init_src = __esm({
420
428
  sessionId: z2.string(),
421
429
  toolCallId: z2.string().optional(),
422
430
  tool: z2.string(),
423
- input: z2.string()
431
+ input: z2.string(),
432
+ filePath: z2.string().optional()
424
433
  }),
425
434
  z2.object({
426
435
  type: z2.literal("tool.completed"),
@@ -428,6 +437,7 @@ var init_src = __esm({
428
437
  toolCallId: z2.string().optional(),
429
438
  tool: z2.string(),
430
439
  output: z2.string(),
440
+ fileChange: FileChangeSchema.optional(),
431
441
  isError: z2.boolean().default(false)
432
442
  }),
433
443
  z2.object({
@@ -519,6 +529,11 @@ var init_src = __esm({
519
529
  serverId: z2.string().uuid(),
520
530
  pairingId: z2.string().regex(/^[a-f0-9]{64}$/)
521
531
  });
532
+ PairingInvitationSchema = z2.object({
533
+ payload: PairingPayloadSchema,
534
+ pin: z2.string().regex(/^\d{6}$/),
535
+ expiresAt: z2.number()
536
+ });
522
537
  PairingRequestSchema = z2.object({
523
538
  pairingId: z2.string().regex(/^[a-f0-9]{64}$/),
524
539
  pin: z2.string().regex(/^\d{6}$/),
@@ -727,6 +742,202 @@ var init_control = __esm({
727
742
  }
728
743
  });
729
744
 
745
+ // src/cli-output.ts
746
+ import { styleText } from "node:util";
747
+ function printHeading(title) {
748
+ console.log();
749
+ console.log(styleText("bold", " ####### ##"));
750
+ console.log(styleText("bold", " ### #####"));
751
+ console.log(` ${styleText("bold", "## ####")} ${styleText("bold", "AGENTRIFIC")}`);
752
+ console.log(
753
+ ` ${styleText("bold", "## ")}${styleText(["bold", "yellow"], "o")}${styleText("bold", " ####")} ${styleText("dim", `${title} \xB7 v${serverVersion}`)}`
754
+ );
755
+ console.log(styleText("bold", " ## ####"));
756
+ console.log(styleText("bold", " ### #####"));
757
+ console.log(styleText("bold", " ####### ##"));
758
+ console.log();
759
+ }
760
+ function printDetail(label2, value) {
761
+ console.log(` ${styleText("dim", "\u203A")} ${styleText("dim", label2.padEnd(10))} ${value}`);
762
+ }
763
+ function printShortcuts(pairingAvailable = false) {
764
+ console.log();
765
+ console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "p")} \u2502 pair a device`);
766
+ if (pairingAvailable)
767
+ console.log(
768
+ ` ${styleText("dim", "\u203A")} Press ${styleText("bold", "j")} \u2502 show pairing JSON`
769
+ );
770
+ console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "?")} \u2502 show shortcuts`);
771
+ console.log(
772
+ ` ${styleText("dim", "\u203A")} Press ${styleText("bold", "Ctrl+C")} \u2502 stop the server
773
+ `
774
+ );
775
+ }
776
+ function printError(message) {
777
+ console.error(`
778
+ ${styleText(["bold", "red"], "Error")} ${message}
779
+ `);
780
+ }
781
+ function printHelp() {
782
+ printHeading("server");
783
+ console.log(` ${styleText("bold", "Usage")} agentrific <command> [options]
784
+ `);
785
+ for (const [command, description] of [
786
+ ["init", "Configure the server"],
787
+ ["start", "Start the server \xB7 press p to pair"],
788
+ ["pair", "Print a fresh pairing QR code and PIN"],
789
+ ["devices", "List enrolled devices"],
790
+ ["revoke <id>", "Revoke a device's access"],
791
+ ["status", "Show server status"],
792
+ ["maintenance enter/resume/drain", "Pause new work, resume, or wait for work to finish"],
793
+ ["service install/uninstall/restart", "Manage the background service"],
794
+ ["logs", "Follow server logs"],
795
+ ["update --check", "Check for a newer release"]
796
+ ])
797
+ console.log(` ${styleText("cyan", command.padEnd(34))}${description}`);
798
+ console.log(`
799
+ ${styleText("bold", "Options")}
800
+ `);
801
+ for (const [option, description] of [
802
+ ["--data-dir <path>", "Use a specific server data directory"],
803
+ ["--public-url <https-url>", "Use your tunnel URL without prompting"],
804
+ ["--no-interactive", "Disable prompts and keyboard shortcuts"],
805
+ ["--show-payload", "Print manual JSON with the pairing QR code"],
806
+ ["--timeout-ms <0-60000>", "Set the maintenance drain timeout"],
807
+ ["--version", "Print the server version"],
808
+ ["--help", "Show this help"]
809
+ ])
810
+ console.log(` ${styleText("cyan", option.padEnd(34))}${description}`);
811
+ console.log("\n Connect using Tailscale Serve, cloudflared, or another HTTPS tunnel.\n");
812
+ }
813
+ var init_cli_output = __esm({
814
+ "src/cli-output.ts"() {
815
+ "use strict";
816
+ init_version();
817
+ }
818
+ });
819
+
820
+ // src/pairing.ts
821
+ import { z as z4 } from "zod";
822
+ import { styleText as styleText2 } from "node:util";
823
+ import { emitKeypressEvents } from "node:readline";
824
+ import qrcode from "qrcode-terminal";
825
+ function printManualPairing(invitation) {
826
+ if (Date.now() >= invitation.expiresAt) {
827
+ printError("This invitation expired. Generate a fresh pairing QR code.");
828
+ return;
829
+ }
830
+ console.log(styleText2("dim", "\n Manual entry \xB7 paste this JSON into Pairing payload:\n"));
831
+ console.log(` ${invitation.payload}
832
+ `);
833
+ }
834
+ function waitForManualPairing(invitation) {
835
+ console.log(styleText2("dim", " Press j to show manual entry \xB7 Enter or q to finish.\n"));
836
+ return new Promise((resolve) => {
837
+ const input = process.stdin;
838
+ const previousRaw = input.isRaw;
839
+ const timeout = setTimeout(
840
+ () => {
841
+ console.log(
842
+ styleText2("dim", "\n Pairing invitation expired. Run agentrific pair again.\n")
843
+ );
844
+ close();
845
+ },
846
+ Math.max(0, invitation.expiresAt - Date.now())
847
+ );
848
+ function close() {
849
+ clearTimeout(timeout);
850
+ input.removeListener("keypress", pressed);
851
+ input.removeListener("end", close);
852
+ process.removeListener("SIGINT", interrupted);
853
+ process.removeListener("SIGTERM", terminated);
854
+ input.setRawMode(previousRaw);
855
+ input.pause();
856
+ resolve();
857
+ }
858
+ function interrupted() {
859
+ process.exitCode = 130;
860
+ close();
861
+ }
862
+ function terminated() {
863
+ process.exitCode = 143;
864
+ close();
865
+ }
866
+ function pressed(text, key) {
867
+ if (key.ctrl && key.name === "c") {
868
+ interrupted();
869
+ return;
870
+ }
871
+ if (key.ctrl || key.meta) return;
872
+ if (text?.toLowerCase() === "j") {
873
+ printManualPairing(invitation);
874
+ close();
875
+ } else if (text?.toLowerCase() === "q" || key.name === "return" || key.name === "escape") {
876
+ close();
877
+ }
878
+ }
879
+ emitKeypressEvents(input);
880
+ input.setRawMode(true);
881
+ input.on("keypress", pressed);
882
+ input.once("end", close);
883
+ process.once("SIGINT", interrupted);
884
+ process.once("SIGTERM", terminated);
885
+ input.resume();
886
+ });
887
+ }
888
+ function pairingOrigin(url) {
889
+ const endpoint = new URL(url);
890
+ if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== "/")
891
+ throw new Error("Use the HTTPS server origin without a path or credentials.");
892
+ if (endpoint.protocol !== "https:" && !(process.env.NODE_ENV !== "production" && endpoint.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname)))
893
+ throw new Error("Use HTTPS. Localhost HTTP is for development builds only.");
894
+ return endpoint.origin;
895
+ }
896
+ async function printPairing(dataDir, url) {
897
+ if (!url)
898
+ throw new Error(
899
+ "Specify --public-url with your Tailscale Serve or Cloudflare Tunnel HTTPS address."
900
+ );
901
+ const origin = pairingOrigin(url);
902
+ const grant = z4.object({
903
+ pairingId: z4.string(),
904
+ pin: z4.string(),
905
+ expiresAt: z4.number(),
906
+ serverId: z4.string()
907
+ }).parse(await controlRequest(dataDir, { command: "pair" }));
908
+ const invitation = {
909
+ v: 1,
910
+ url: origin,
911
+ serverId: grant.serverId,
912
+ pairingId: grant.pairingId
913
+ };
914
+ const payload = JSON.stringify(invitation);
915
+ const link = createPairingLink(invitation);
916
+ printHeading("pairing");
917
+ console.log(" 1. Scan below with your phone camera and tap Open Agentrific if offered.");
918
+ console.log(" 2. Or open Agentrific and choose Scan pairing QR code.");
919
+ console.log(" 3. Enter the PIN, then confirm pairing.\n");
920
+ qrcode.generate(link, { small: true }, (code) => {
921
+ console.log(
922
+ code.split("\n").map((line) => ` ${line}`).join("\n")
923
+ );
924
+ });
925
+ console.log();
926
+ printDetail("PIN:", styleText2(["bold", "yellow"], grant.pin));
927
+ printDetail("Server", styleText2("cyan", origin));
928
+ printDetail("Expires", "In five minutes \xB7 one device per code");
929
+ console.log("\n Pairing grants this device access to run agents as your host user.\n");
930
+ return { payload, expiresAt: grant.expiresAt };
931
+ }
932
+ var init_pairing = __esm({
933
+ "src/pairing.ts"() {
934
+ "use strict";
935
+ init_src();
936
+ init_control();
937
+ init_cli_output();
938
+ }
939
+ });
940
+
730
941
  // ../../packages/contracts/src/push.ts
731
942
  import { z as z6 } from "zod";
732
943
  function pushSigningText(installationId, operation, request) {
@@ -740,7 +951,7 @@ function pushSigningText(installationId, operation, request) {
740
951
  request.payload
741
952
  ].join("\n");
742
953
  }
743
- var PUSH_API_URL, PushKindSchema, PushPreferencesSchema, PushPublicKeySchema, PushInstallationSchema, PushRegistrationSchema, PushEnrollmentSchema, PushGrantSchema, PushEventSchema, PushSignedRequestSchema, PushEntitlementSchema, PushServerStatusSchema;
954
+ var PUSH_API_URL, PushKindSchema, PushPreferencesSchema, PushPublicKeySchema, PushInstallationSchema, PushRegistrationSchema, PushEnrollmentSchema, PushGrantSchema, PushPullRequestUrlSchema, PushEventSchema, PushSignedRequestSchema, PushEntitlementSchema, PushServerStatusSchema;
744
955
  var init_push = __esm({
745
956
  "../../packages/contracts/src/push.ts"() {
746
957
  "use strict";
@@ -768,7 +979,14 @@ var init_push = __esm({
768
979
  grant: z6.string().regex(/^[a-f0-9]{64}$/),
769
980
  expiresAt: z6.number()
770
981
  }).strict();
771
- PushEventSchema = z6.object({ eventId: z6.string().uuid(), sessionId: z6.string().uuid(), kind: PushKindSchema }).strict();
982
+ PushPullRequestUrlSchema = z6.string().max(512).regex(/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9][0-9]*$/);
983
+ PushEventSchema = z6.object({
984
+ eventId: z6.string().uuid(),
985
+ sessionId: z6.string().uuid(),
986
+ kind: PushKindSchema,
987
+ taskTitle: z6.string().trim().min(1).max(160).optional(),
988
+ prUrl: PushPullRequestUrlSchema.optional()
989
+ }).strict();
772
990
  PushSignedRequestSchema = z6.object({
773
991
  bindingId: z6.string().uuid(),
774
992
  timestamp: z6.number().int(),
@@ -855,10 +1073,11 @@ var init_push2 = __esm({
855
1073
  "use strict";
856
1074
  init_push();
857
1075
  ServerPush = class {
858
- constructor(store2, serverId, directory, transport = fetch) {
1076
+ constructor(store2, serverId, directory, transport = fetch, apiUrl = PUSH_API_URL) {
859
1077
  this.store = store2;
860
1078
  this.serverId = serverId;
861
1079
  this.transport = transport;
1080
+ this.apiUrl = apiUrl;
862
1081
  mkdirSync(directory, { recursive: true, mode: 448 });
863
1082
  const filename = path4.join(directory, "push-signing-key.pem");
864
1083
  if (!existsSync(filename))
@@ -873,6 +1092,7 @@ var init_push2 = __esm({
873
1092
  store;
874
1093
  serverId;
875
1094
  transport;
1095
+ apiUrl;
876
1096
  key;
877
1097
  publicKey;
878
1098
  draining;
@@ -886,6 +1106,8 @@ var init_push2 = __esm({
886
1106
  eventId: randomUUID(),
887
1107
  sessionId: session.id,
888
1108
  kind,
1109
+ taskTitle: session.title.replace(/[\r\n\t]+/g, " ").trim().slice(0, 160) || void 0,
1110
+ prUrl: kind === "pr_created" ? PushPullRequestUrlSchema.safeParse(session.gitDelivery?.url).data : void 0,
889
1111
  remaining: state.bindings,
890
1112
  createdAt: Date.now(),
891
1113
  attempts: 0,
@@ -908,7 +1130,7 @@ var init_push2 = __esm({
908
1130
  Buffer.from(pushSigningText(installationId, operation, request)),
909
1131
  this.key
910
1132
  ).toString("hex");
911
- return this.transport(`${PUSH_API_URL}/v1/installations/${installationId}/${operation}`, {
1133
+ return this.transport(`${this.apiUrl}/v1/installations/${installationId}/${operation}`, {
912
1134
  method: "POST",
913
1135
  headers: { "Content-Type": "application/json" },
914
1136
  body: JSON.stringify({ ...request, signature }),
@@ -982,7 +1204,13 @@ var init_push2 = __esm({
982
1204
  binding.installationId,
983
1205
  binding.bindingId,
984
1206
  "events",
985
- JSON.stringify({ eventId: job.eventId, sessionId: job.sessionId, kind: job.kind })
1207
+ JSON.stringify({
1208
+ eventId: job.eventId,
1209
+ sessionId: job.sessionId,
1210
+ kind: job.kind,
1211
+ taskTitle: job.taskTitle,
1212
+ prUrl: job.prUrl
1213
+ })
986
1214
  );
987
1215
  if (response.ok) continue;
988
1216
  if ([401, 403, 404].includes(response.status)) {
@@ -1332,6 +1560,29 @@ async function registerSecurity(app2, security2, version, allowedOrigins2) {
1332
1560
  const result = await security2.pair(input.data, request.ip);
1333
1561
  return result ?? reply.code(401).send({ error: "Pairing expired, locked, or invalid. Run agentrific pair again." });
1334
1562
  });
1563
+ app2.post("/api/auth/pairing-invitation", (_request, reply) => {
1564
+ const publicUrl = process.env.AGENTRIFIC_PUBLIC_URL;
1565
+ if (!publicUrl)
1566
+ return reply.code(503).send({
1567
+ error: "Configure the server\u2019s public HTTPS URL and restart the server before pairing a device."
1568
+ });
1569
+ let url;
1570
+ try {
1571
+ url = pairingOrigin(publicUrl);
1572
+ } catch {
1573
+ return reply.code(503).send({ error: "The server\u2019s public URL is invalid. Configure a public HTTPS origin." });
1574
+ }
1575
+ try {
1576
+ const grant = security2.createPairing();
1577
+ return {
1578
+ payload: { v: 1, url, serverId: grant.serverId, pairingId: grant.pairingId },
1579
+ pin: grant.pin,
1580
+ expiresAt: grant.expiresAt
1581
+ };
1582
+ } catch {
1583
+ return reply.code(429).send({ error: "Too many pending pairing codes. Wait five minutes and try again." });
1584
+ }
1585
+ });
1335
1586
  app2.get("/api/auth/session", (request) => ({
1336
1587
  serverId: security2.serverId,
1337
1588
  device: security2.authenticate((request.headers.authorization ?? "").slice(7))
@@ -1354,6 +1605,7 @@ var init_security = __esm({
1354
1605
  "src/security.ts"() {
1355
1606
  "use strict";
1356
1607
  init_src();
1608
+ init_pairing();
1357
1609
  secret = () => randomBytes(32).toString("hex");
1358
1610
  digest = (value) => createHash("sha256").update(value).digest("hex");
1359
1611
  ServerSecurity = class {
@@ -1883,7 +2135,7 @@ var init_runtime = __esm({
1883
2135
  Effect8.ensuring(Effect8.sync(() => this.working.delete(entry.session.id)))
1884
2136
  );
1885
2137
  }
1886
- send(session, message, attachments2 = [], messageId = randomUUID4()) {
2138
+ send(session, message, attachments2 = [], messageId = randomUUID4(), mode = "followUp") {
1887
2139
  return this.lock(session.id).withPermit(
1888
2140
  Effect8.gen({ self: this }, function* () {
1889
2141
  if (this.closed)
@@ -1893,6 +2145,16 @@ var init_runtime = __esm({
1893
2145
  if (this.hasMessage(session.id, messageId)) return;
1894
2146
  const entry = yield* this.open(session);
1895
2147
  yield* this.operation(async () => entry.agent.validateAttachments?.(attachments2));
2148
+ const steering = entry.running && mode === "steer";
2149
+ if (steering) {
2150
+ if (!entry.agent.steer)
2151
+ return yield* Effect8.fail(
2152
+ new AgentError({ message: "This agent does not support steering." })
2153
+ );
2154
+ yield* this.operation(
2155
+ () => entry.agent.steer(gitFlowPrompt(entry.session, message), attachments2)
2156
+ );
2157
+ }
1896
2158
  this.emit({
1897
2159
  type: "message.user",
1898
2160
  sessionId: session.id,
@@ -1900,6 +2162,7 @@ var init_runtime = __esm({
1900
2162
  text: message,
1901
2163
  attachments: attachments2
1902
2164
  });
2165
+ if (steering) return;
1903
2166
  if (entry.running) {
1904
2167
  entry.pending.push({ text: message, attachments: attachments2 });
1905
2168
  return;
@@ -2874,6 +3137,74 @@ var init_delivery_tool = __esm({
2874
3137
  }
2875
3138
  });
2876
3139
 
3140
+ // src/file-change.ts
3141
+ import { createWriteToolDefinition, defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
3142
+ import { createTwoFilesPatch } from "diff";
3143
+ import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
3144
+ import { z as z13 } from "zod";
3145
+ function boundFileChange(change) {
3146
+ if ((change.patch?.length ?? 0) + (change.content?.length ?? 0) > previewLimit)
3147
+ return { path: change.path, omitted: true };
3148
+ return change;
3149
+ }
3150
+ function completedFileChange(result, path13) {
3151
+ const parsed = EditResultSchema.safeParse(result);
3152
+ if (!parsed.success) return;
3153
+ const details = parsed.data.details;
3154
+ if (details?.fileChange) return boundFileChange(details.fileChange);
3155
+ if (path13 && details?.patch) return boundFileChange({ path: path13, patch: details.patch });
3156
+ }
3157
+ function fileWriteTool(cwd) {
3158
+ const base = createWriteToolDefinition(cwd);
3159
+ return defineTool2({
3160
+ name: base.name,
3161
+ label: base.label,
3162
+ description: base.description,
3163
+ promptSnippet: base.promptSnippet,
3164
+ promptGuidelines: base.promptGuidelines,
3165
+ parameters: base.parameters,
3166
+ constrainedSampling: base.constrainedSampling,
3167
+ async execute(toolCallId, args, signal, _onUpdate, ctx) {
3168
+ let fileChange;
3169
+ const tool = createWriteToolDefinition(cwd, {
3170
+ operations: {
3171
+ mkdir: async (dir) => {
3172
+ await mkdir5(dir, { recursive: true });
3173
+ },
3174
+ // Pi invokes this inside its per-file mutation queue, so the snapshot
3175
+ // and write cannot race another built-in edit/write on the same file.
3176
+ writeFile: async (path13, content) => {
3177
+ const previous = await readFile4(path13, "utf8").catch((error) => {
3178
+ if (error.code === "ENOENT") return void 0;
3179
+ throw error;
3180
+ });
3181
+ await writeFile3(path13, content, "utf8");
3182
+ fileChange = boundFileChange(
3183
+ previous === void 0 ? { path: args.path, content } : {
3184
+ path: args.path,
3185
+ patch: createTwoFilesPatch(args.path, args.path, previous, content)
3186
+ }
3187
+ );
3188
+ }
3189
+ }
3190
+ });
3191
+ const result = await tool.execute(toolCallId, args, signal, void 0, ctx);
3192
+ return { ...result, details: { fileChange } };
3193
+ }
3194
+ });
3195
+ }
3196
+ var previewLimit, EditResultSchema;
3197
+ var init_file_change = __esm({
3198
+ "src/file-change.ts"() {
3199
+ "use strict";
3200
+ init_src();
3201
+ previewLimit = 256e3;
3202
+ EditResultSchema = z13.object({
3203
+ details: z13.object({ patch: z13.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
3204
+ });
3205
+ }
3206
+ });
3207
+
2877
3208
  // src/pi.ts
2878
3209
  import { Effect as Effect12 } from "effect";
2879
3210
  import { randomUUID as randomUUID5 } from "node:crypto";
@@ -2881,7 +3212,7 @@ import {
2881
3212
  createAgentSession as createAgentSession2,
2882
3213
  SessionManager as SessionManager2
2883
3214
  } from "@earendil-works/pi-coding-agent";
2884
- import { z as z13 } from "zod";
3215
+ import { z as z14 } from "zod";
2885
3216
  function toolOutput(value) {
2886
3217
  const parsed = ToolResultSchema.safeParse(value);
2887
3218
  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);
@@ -2896,7 +3227,10 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
2896
3227
  model,
2897
3228
  modelRuntime: models2.runtime,
2898
3229
  sessionManager: SessionManager2.continueRecent(cwd, sessionDirectory),
2899
- customTools: run?.gitFlow && delivery2 ? [deliveryTool(sessionId, delivery2)] : []
3230
+ customTools: [
3231
+ fileWriteTool(cwd),
3232
+ ...run?.gitFlow && delivery2 ? [deliveryTool(sessionId, delivery2)] : []
3233
+ ]
2900
3234
  });
2901
3235
  return {
2902
3236
  get model() {
@@ -2920,6 +3254,12 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
2920
3254
  throw new Error(last.errorMessage ?? "The model request failed");
2921
3255
  }
2922
3256
  },
3257
+ async steer(message, attachments2 = []) {
3258
+ const context = await Effect12.runPromise(files.prepare(attachments2));
3259
+ if (!session.isStreaming)
3260
+ throw new Error("The response has finished. Send this message as a follow-up instead.");
3261
+ await session.steer(message + context.context, context.images);
3262
+ },
2923
3263
  async abort() {
2924
3264
  session.clearQueue();
2925
3265
  await session.abort();
@@ -2984,16 +3324,23 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
2984
3324
  }
2985
3325
  return false;
2986
3326
  }
3327
+ const filePaths = /* @__PURE__ */ new Map();
3328
+ const FileArgsSchema = z14.object({ path: z14.string() });
2987
3329
  function handleTool(event) {
2988
3330
  if (event.type === "tool_execution_start") {
2989
3331
  flushText();
2990
3332
  setActivity("thinking");
3333
+ if (event.toolName === "edit" || event.toolName === "write") {
3334
+ const args = FileArgsSchema.safeParse(event.args);
3335
+ if (args.success) filePaths.set(event.toolCallId, args.data.path);
3336
+ }
2991
3337
  emit({
2992
3338
  type: "tool.started",
2993
3339
  sessionId,
2994
3340
  toolCallId: event.toolCallId,
2995
3341
  tool: event.toolName,
2996
- input: boundedJson(event.args, 1200)
3342
+ input: boundedJson(event.args, 1200),
3343
+ filePath: filePaths.get(event.toolCallId)
2997
3344
  });
2998
3345
  return true;
2999
3346
  }
@@ -3005,8 +3352,10 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
3005
3352
  toolCallId: event.toolCallId,
3006
3353
  tool: event.toolName,
3007
3354
  output: toolOutput(event.result),
3355
+ fileChange: event.isError ? void 0 : completedFileChange(event.result, filePaths.get(event.toolCallId)),
3008
3356
  isError: event.isError
3009
3357
  });
3358
+ filePaths.delete(event.toolCallId);
3010
3359
  setActivity("thinking");
3011
3360
  return true;
3012
3361
  }
@@ -3039,8 +3388,9 @@ var init_pi = __esm({
3039
3388
  "use strict";
3040
3389
  init_models();
3041
3390
  init_delivery_tool();
3042
- ToolResultSchema = z13.object({
3043
- content: z13.array(z13.object({ type: z13.string(), text: z13.string().optional() })).optional()
3391
+ init_file_change();
3392
+ ToolResultSchema = z14.object({
3393
+ content: z14.array(z14.object({ type: z14.string(), text: z14.string().optional() })).optional()
3044
3394
  }).passthrough();
3045
3395
  boundedJson = (value, limit) => {
3046
3396
  let text;
@@ -3055,9 +3405,9 @@ var init_pi = __esm({
3055
3405
  });
3056
3406
 
3057
3407
  // src/projects.ts
3058
- import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
3408
+ import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
3059
3409
  import { randomUUID as randomUUID6 } from "node:crypto";
3060
- import { z as z14 } from "zod";
3410
+ import { z as z15 } from "zod";
3061
3411
  import { constants as constants2 } from "node:fs";
3062
3412
  import { homedir as homedir3 } from "node:os";
3063
3413
  import path8 from "node:path";
@@ -3092,7 +3442,7 @@ async function gitDirectory(directory) {
3092
3442
  const location = path8.join(directory, ".git");
3093
3443
  const info = await stat3(location);
3094
3444
  if (info.isDirectory()) return location;
3095
- const contents = await readFile4(location, "utf8");
3445
+ const contents = await readFile5(location, "utf8");
3096
3446
  const match = /^gitdir: (.+)\s*$/m.exec(contents);
3097
3447
  if (!match) throw new Error("Invalid Git worktree metadata.");
3098
3448
  const resolved = await realpath(path8.resolve(directory, match[1].trim()));
@@ -3102,21 +3452,21 @@ async function gitDirectory(directory) {
3102
3452
  async function identity(directory, preferred) {
3103
3453
  const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
3104
3454
  try {
3105
- return ProjectId.parse(await readFile4(filename, "utf8"));
3455
+ return ProjectId.parse(await readFile5(filename, "utf8"));
3106
3456
  } catch (error) {
3107
- if (!z14.object({ code: z14.literal("ENOENT") }).safeParse(error).success) throw error;
3457
+ if (!z15.object({ code: z15.literal("ENOENT") }).safeParse(error).success) throw error;
3108
3458
  const legacyFilename = path8.join(await gitDirectory(directory), "chatplans-project-id");
3109
- const legacy = await readFile4(legacyFilename, "utf8").catch((cause) => {
3110
- if (!z14.object({ code: z14.literal("ENOENT") }).safeParse(cause).success) throw cause;
3459
+ const legacy = await readFile5(legacyFilename, "utf8").catch((cause) => {
3460
+ if (!z15.object({ code: z15.literal("ENOENT") }).safeParse(cause).success) throw cause;
3111
3461
  return null;
3112
3462
  });
3113
3463
  const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
3114
3464
  try {
3115
- await writeFile3(filename, id + "\n", { flag: "wx" });
3465
+ await writeFile4(filename, id + "\n", { flag: "wx" });
3116
3466
  return id;
3117
3467
  } catch (writeError) {
3118
- if (!z14.object({ code: z14.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
3119
- return ProjectId.parse(await readFile4(filename, "utf8"));
3468
+ if (!z15.object({ code: z15.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
3469
+ return ProjectId.parse(await readFile5(filename, "utf8"));
3120
3470
  }
3121
3471
  }
3122
3472
  }
@@ -3136,7 +3486,7 @@ function scanProjects(root, store2) {
3136
3486
  let existing = registered.find((project2) => project2.id === id);
3137
3487
  if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat3(path8.join(existing.path, ".git")).catch(() => null)) {
3138
3488
  id = randomUUID6();
3139
- await writeFile3(
3489
+ await writeFile4(
3140
3490
  path8.join(await gitDirectory(directory), "agentrific-project-id"),
3141
3491
  id + "\n"
3142
3492
  );
@@ -3175,11 +3525,11 @@ function reconnectProject(root, id, input, store2) {
3175
3525
  const filename = path8.join(await gitDirectory(directory), "agentrific-project-id");
3176
3526
  const replacedId = await identity(directory);
3177
3527
  const project = { id, name: path8.basename(directory), path: directory, available: true };
3178
- await writeFile3(filename, id + "\n");
3528
+ await writeFile4(filename, id + "\n");
3179
3529
  try {
3180
3530
  await Effect13.runPromise(store2.relocateProject(project, previous.path, replacedId));
3181
3531
  } catch (error) {
3182
- await writeFile3(filename, replacedId + "\n");
3532
+ await writeFile4(filename, replacedId + "\n");
3183
3533
  throw error;
3184
3534
  }
3185
3535
  return project;
@@ -3192,31 +3542,31 @@ var init_projects = __esm({
3192
3542
  "src/projects.ts"() {
3193
3543
  "use strict";
3194
3544
  locks2 = /* @__PURE__ */ new WeakMap();
3195
- ProjectId = z14.string().trim().min(1).max(255);
3545
+ ProjectId = z15.string().trim().min(1).max(255);
3196
3546
  }
3197
3547
  });
3198
3548
 
3199
3549
  // src/push-schema.ts
3200
- import { z as z15 } from "zod";
3550
+ import { z as z16 } from "zod";
3201
3551
  var PushBindingSchema, PushStateSchema, PendingPushSchema;
3202
3552
  var init_push_schema = __esm({
3203
3553
  "src/push-schema.ts"() {
3204
3554
  "use strict";
3205
3555
  init_push();
3206
- PushBindingSchema = z15.object({
3207
- bindingId: z15.string().uuid(),
3208
- installationId: z15.string().uuid(),
3209
- deviceId: z15.string().uuid()
3556
+ PushBindingSchema = z16.object({
3557
+ bindingId: z16.string().uuid(),
3558
+ installationId: z16.string().uuid(),
3559
+ deviceId: z16.string().uuid()
3210
3560
  });
3211
- PushStateSchema = z15.object({
3212
- bindings: z15.array(PushBindingSchema).max(10),
3213
- error: z15.string().nullable()
3561
+ PushStateSchema = z16.object({
3562
+ bindings: z16.array(PushBindingSchema).max(10),
3563
+ error: z16.string().nullable()
3214
3564
  });
3215
3565
  PendingPushSchema = PushEventSchema.extend({
3216
- remaining: z15.array(PushBindingSchema),
3217
- createdAt: z15.number(),
3218
- attempts: z15.number().int().nonnegative(),
3219
- nextAt: z15.number()
3566
+ remaining: z16.array(PushBindingSchema),
3567
+ createdAt: z16.number(),
3568
+ attempts: z16.number().int().nonnegative(),
3569
+ nextAt: z16.number()
3220
3570
  });
3221
3571
  }
3222
3572
  });
@@ -3226,7 +3576,7 @@ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "n
3226
3576
  import path9 from "node:path";
3227
3577
  import { DatabaseSync } from "node:sqlite";
3228
3578
  import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
3229
- import { z as z16 } from "zod";
3579
+ import { z as z17 } from "zod";
3230
3580
  import { randomUUID as randomUUID7 } from "node:crypto";
3231
3581
  var StorageError, PayloadRow, EventRow, SqliteSessionStore, SessionStore;
3232
3582
  var init_store = __esm({
@@ -3239,8 +3589,8 @@ var init_store = __esm({
3239
3589
  init_src();
3240
3590
  StorageError = class extends Data3.TaggedError("StorageError") {
3241
3591
  };
3242
- PayloadRow = z16.object({ payload: z16.string() });
3243
- EventRow = z16.object({ sequence: z16.number(), recorded_at: z16.string(), payload: z16.string() });
3592
+ PayloadRow = z17.object({ payload: z17.string() });
3593
+ EventRow = z17.object({ sequence: z17.number(), recorded_at: z17.string(), payload: z17.string() });
3244
3594
  SqliteSessionStore = class {
3245
3595
  constructor(db) {
3246
3596
  this.db = db;
@@ -3291,7 +3641,7 @@ var init_store = __esm({
3291
3641
  const row = PayloadRow.parse(
3292
3642
  this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
3293
3643
  );
3294
- return z16.string().uuid().parse(JSON.parse(row.payload));
3644
+ return z17.string().uuid().parse(JSON.parse(row.payload));
3295
3645
  });
3296
3646
  }
3297
3647
  saveDevice(device, tokenHash) {
@@ -3379,7 +3729,7 @@ var init_store = __esm({
3379
3729
  `).all();
3380
3730
  const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
3381
3731
  for (const row of legacy) {
3382
- const id = z16.object({ id: z16.string() }).parse(row).id;
3732
+ const id = z17.object({ id: z17.string() }).parse(row).id;
3383
3733
  insert.run(
3384
3734
  id,
3385
3735
  JSON.stringify({ id, name: id, path: path9.join(root, id), available: false })
@@ -3589,7 +3939,7 @@ var init_store = __esm({
3589
3939
  if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
3590
3940
  const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
3591
3941
  if (notification) {
3592
- const count = z16.object({ total: z16.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
3942
+ const count = z17.object({ total: z17.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
3593
3943
  if (count.total >= 1e3)
3594
3944
  this.db.prepare(
3595
3945
  "DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
@@ -3641,7 +3991,7 @@ var init_store = __esm({
3641
3991
  for (const suffix of ["-wal", "-shm"])
3642
3992
  if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
3643
3993
  try {
3644
- const schema = z16.object({ user_version: z16.number() }).parse(db.prepare("PRAGMA user_version").get());
3994
+ const schema = z17.object({ user_version: z17.number() }).parse(db.prepare("PRAGMA user_version").get());
3645
3995
  if (schema.user_version > 1)
3646
3996
  throw new Error("This database requires a newer Agentrific server.");
3647
3997
  db.exec(`
@@ -3762,7 +4112,7 @@ import { homedir as homedir4 } from "node:os";
3762
4112
  import path11 from "node:path";
3763
4113
  import { WebSocket } from "ws";
3764
4114
  import { Effect as Effect16, ManagedRuntime } from "effect";
3765
- import { z as z17 } from "zod";
4115
+ import { z as z18 } from "zod";
3766
4116
  import { ModelRuntime } from "@earendil-works/pi-coding-agent";
3767
4117
  function publish(event, workflowUpdate = false) {
3768
4118
  const previous = event.type === "session.updated" ? Effect16.runSync(store.get(event.session.id)) : void 0;
@@ -3968,7 +4318,7 @@ async function withSessionTaskLock(id, operation) {
3968
4318
  const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
3969
4319
  return withTaskLock(lockId, operation);
3970
4320
  }
3971
- var host, port, defaultProjectsRoot, worktreesRoot, execFileAsync, app, dataRoot, releaseLock, effects, store, security, push, allowedOrigins, attachmentEffects, attachments, attachmentCleanup, models, delivery, preparation, agentEffects, runtime, worktreeCleanup, maxWorktreeAgeDays, pruneWorktrees, worktreeCleanupTimer, updates, maintenance, stopUpdateDiscovery, HistoryQuery, StartTaskBody, automation, automationTimer, closeControl;
4321
+ 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;
3972
4322
  var init_index = __esm({
3973
4323
  async "src/index.ts"() {
3974
4324
  "use strict";
@@ -4025,9 +4375,13 @@ var init_index = __esm({
4025
4375
  effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
4026
4376
  store = await effects.runPromise(SessionStore);
4027
4377
  security = new ServerSecurity(store, await Effect16.runPromise(store.serverIdentity()));
4028
- push = new ServerPush(store, security.serverId, dataRoot);
4378
+ testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
4379
+ if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
4380
+ throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
4381
+ push = new ServerPush(store, security.serverId, dataRoot, fetch, testPushUrl);
4029
4382
  allowedOrigins = [
4030
4383
  .../* @__PURE__ */ new Set([
4384
+ "agentrific-desktop://app",
4031
4385
  ...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
4032
4386
  `http://${host === "::1" ? "[::1]" : host}:${port}`,
4033
4387
  ...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
@@ -4249,7 +4603,7 @@ var init_index = __esm({
4249
4603
  return project ?? reply.code(404).send({ error: "Project not found on this server." });
4250
4604
  });
4251
4605
  app.post("/api/projects/:id/reconnect", async (request, reply) => {
4252
- const parsed = z17.object({ path: z17.string().trim().min(1).max(4096) }).safeParse(request.body);
4606
+ const parsed = z18.object({ path: z18.string().trim().min(1).max(4096) }).safeParse(request.body);
4253
4607
  if (!parsed.success)
4254
4608
  return reply.code(400).send({ error: "Enter the project's new folder path." });
4255
4609
  const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
@@ -4381,9 +4735,9 @@ var init_index = __esm({
4381
4735
  }
4382
4736
  })
4383
4737
  );
4384
- HistoryQuery = z17.object({
4385
- after: z17.coerce.number().int().nonnegative().default(0),
4386
- limit: z17.coerce.number().int().min(1).max(1e3).default(500)
4738
+ HistoryQuery = z18.object({
4739
+ after: z18.coerce.number().int().nonnegative().default(0),
4740
+ limit: z18.coerce.number().int().min(1).max(1e3).default(500)
4387
4741
  });
4388
4742
  app.get("/api/sessions/:id/events", async (request, reply) => {
4389
4743
  const query = HistoryQuery.safeParse(request.query);
@@ -4402,7 +4756,7 @@ var init_index = __esm({
4402
4756
  if (!result.session) return reply.code(result.status).send({ error: result.error });
4403
4757
  return reply.code(201).send(result.session);
4404
4758
  });
4405
- StartTaskBody = z17.object({ prompt: z17.string().trim().max(2e4).optional() }).default({});
4759
+ StartTaskBody = z18.object({ prompt: z18.string().trim().max(2e4).optional() }).default({});
4406
4760
  app.post("/api/tasks/:id/start", async (request, reply) => {
4407
4761
  const body = StartTaskBody.safeParse(request.body ?? {});
4408
4762
  if (!body.success) return reply.code(400).send({ error: "The task prompt is too long." });
@@ -4437,7 +4791,7 @@ var init_index = __esm({
4437
4791
  return automation.configure(parsed.data);
4438
4792
  });
4439
4793
  app.patch("/api/automation/queue", async (request, reply) => {
4440
- const parsed = z17.object({ taskId: z17.string().min(1), beforeId: z17.string().min(1).nullable() }).safeParse(request.body);
4794
+ const parsed = z18.object({ taskId: z18.string().min(1), beforeId: z18.string().min(1).nullable() }).safeParse(request.body);
4441
4795
  if (!parsed.success)
4442
4796
  return reply.code(400).send({ error: "Choose a task and a queue position." });
4443
4797
  try {
@@ -4467,7 +4821,13 @@ var init_index = __esm({
4467
4821
  attachments.resolve(session.projectId, parsed.data.attachmentIds ?? [])
4468
4822
  );
4469
4823
  await Effect16.runPromise(
4470
- runtime.send(session, parsed.data.message, files, parsed.data.clientMessageId)
4824
+ runtime.send(
4825
+ session,
4826
+ parsed.data.message,
4827
+ files,
4828
+ parsed.data.clientMessageId,
4829
+ parsed.data.mode
4830
+ )
4471
4831
  );
4472
4832
  return reply.code(202).send({ accepted: true });
4473
4833
  } catch (error) {
@@ -4537,7 +4897,7 @@ var init_index = __esm({
4537
4897
 
4538
4898
  // src/cli.ts
4539
4899
  init_warnings();
4540
- import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
4900
+ import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
4541
4901
  import { homedir as homedir5 } from "node:os";
4542
4902
  import path12 from "node:path";
4543
4903
  import { fileURLToPath } from "node:url";
@@ -4635,204 +4995,21 @@ async function installService(platform, directory, filename, domain, dataDir, cl
4635
4995
  init_updates();
4636
4996
  init_control();
4637
4997
  init_version();
4998
+ init_pairing();
4638
4999
  import { spawn as spawn2 } from "node:child_process";
4639
5000
  import { parseArgs } from "node:util";
4640
- import { z as z18 } from "zod";
4641
-
4642
- // src/pairing.ts
4643
- init_src();
4644
- init_control();
4645
- import { z as z4 } from "zod";
4646
- import { styleText as styleText2 } from "node:util";
4647
- import { emitKeypressEvents } from "node:readline";
4648
- import qrcode from "qrcode-terminal";
4649
-
4650
- // src/cli-output.ts
4651
- init_version();
4652
- import { styleText } from "node:util";
4653
- function printHeading(title) {
4654
- console.log();
4655
- console.log(styleText("bold", " ####### ##"));
4656
- console.log(styleText("bold", " ### #####"));
4657
- console.log(` ${styleText("bold", "## ####")} ${styleText("bold", "AGENTRIFIC")}`);
4658
- console.log(
4659
- ` ${styleText("bold", "## ")}${styleText(["bold", "yellow"], "o")}${styleText("bold", " ####")} ${styleText("dim", `${title} \xB7 v${serverVersion}`)}`
4660
- );
4661
- console.log(styleText("bold", " ## ####"));
4662
- console.log(styleText("bold", " ### #####"));
4663
- console.log(styleText("bold", " ####### ##"));
4664
- console.log();
4665
- }
4666
- function printDetail(label2, value) {
4667
- console.log(` ${styleText("dim", "\u203A")} ${styleText("dim", label2.padEnd(10))} ${value}`);
4668
- }
4669
- function printShortcuts(pairingAvailable = false) {
4670
- console.log();
4671
- console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "p")} \u2502 pair a device`);
4672
- if (pairingAvailable)
4673
- console.log(
4674
- ` ${styleText("dim", "\u203A")} Press ${styleText("bold", "j")} \u2502 show pairing JSON`
4675
- );
4676
- console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "?")} \u2502 show shortcuts`);
4677
- console.log(
4678
- ` ${styleText("dim", "\u203A")} Press ${styleText("bold", "Ctrl+C")} \u2502 stop the server
4679
- `
4680
- );
4681
- }
4682
- function printError(message) {
4683
- console.error(`
4684
- ${styleText(["bold", "red"], "Error")} ${message}
4685
- `);
4686
- }
4687
- function printHelp() {
4688
- printHeading("server");
4689
- console.log(` ${styleText("bold", "Usage")} agentrific <command> [options]
4690
- `);
4691
- for (const [command, description] of [
4692
- ["init", "Configure the server"],
4693
- ["start", "Start the server \xB7 press p to pair"],
4694
- ["pair", "Print a fresh pairing QR code and PIN"],
4695
- ["devices", "List enrolled devices"],
4696
- ["revoke <id>", "Revoke a device's access"],
4697
- ["status", "Show server status"],
4698
- ["maintenance enter/resume/drain", "Pause new work, resume, or wait for work to finish"],
4699
- ["service install/uninstall/restart", "Manage the background service"],
4700
- ["logs", "Follow server logs"],
4701
- ["update --check", "Check for a newer release"]
4702
- ])
4703
- console.log(` ${styleText("cyan", command.padEnd(34))}${description}`);
4704
- console.log(`
4705
- ${styleText("bold", "Options")}
4706
- `);
4707
- for (const [option, description] of [
4708
- ["--data-dir <path>", "Use a specific server data directory"],
4709
- ["--public-url <https-url>", "Use your tunnel URL without prompting"],
4710
- ["--no-interactive", "Disable prompts and keyboard shortcuts"],
4711
- ["--show-payload", "Print manual JSON with the pairing QR code"],
4712
- ["--timeout-ms <0-60000>", "Set the maintenance drain timeout"],
4713
- ["--version", "Print the server version"],
4714
- ["--help", "Show this help"]
4715
- ])
4716
- console.log(` ${styleText("cyan", option.padEnd(34))}${description}`);
4717
- console.log("\n Connect using Tailscale Serve, cloudflared, or another HTTPS tunnel.\n");
4718
- }
4719
-
4720
- // src/pairing.ts
4721
- function printManualPairing(invitation) {
4722
- if (Date.now() >= invitation.expiresAt) {
4723
- printError("This invitation expired. Generate a fresh pairing QR code.");
4724
- return;
4725
- }
4726
- console.log(styleText2("dim", "\n Manual entry \xB7 paste this JSON into Pairing payload:\n"));
4727
- console.log(` ${invitation.payload}
4728
- `);
4729
- }
4730
- function waitForManualPairing(invitation) {
4731
- console.log(styleText2("dim", " Press j to show manual entry \xB7 Enter or q to finish.\n"));
4732
- return new Promise((resolve) => {
4733
- const input = process.stdin;
4734
- const previousRaw = input.isRaw;
4735
- const timeout = setTimeout(
4736
- () => {
4737
- console.log(
4738
- styleText2("dim", "\n Pairing invitation expired. Run agentrific pair again.\n")
4739
- );
4740
- close();
4741
- },
4742
- Math.max(0, invitation.expiresAt - Date.now())
4743
- );
4744
- function close() {
4745
- clearTimeout(timeout);
4746
- input.removeListener("keypress", pressed);
4747
- input.removeListener("end", close);
4748
- process.removeListener("SIGINT", interrupted);
4749
- process.removeListener("SIGTERM", terminated);
4750
- input.setRawMode(previousRaw);
4751
- input.pause();
4752
- resolve();
4753
- }
4754
- function interrupted() {
4755
- process.exitCode = 130;
4756
- close();
4757
- }
4758
- function terminated() {
4759
- process.exitCode = 143;
4760
- close();
4761
- }
4762
- function pressed(text, key) {
4763
- if (key.ctrl && key.name === "c") {
4764
- interrupted();
4765
- return;
4766
- }
4767
- if (key.ctrl || key.meta) return;
4768
- if (text?.toLowerCase() === "j") {
4769
- printManualPairing(invitation);
4770
- close();
4771
- } else if (text?.toLowerCase() === "q" || key.name === "return" || key.name === "escape") {
4772
- close();
4773
- }
4774
- }
4775
- emitKeypressEvents(input);
4776
- input.setRawMode(true);
4777
- input.on("keypress", pressed);
4778
- input.once("end", close);
4779
- process.once("SIGINT", interrupted);
4780
- process.once("SIGTERM", terminated);
4781
- input.resume();
4782
- });
4783
- }
4784
- function pairingOrigin(url) {
4785
- const endpoint = new URL(url);
4786
- if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== "/")
4787
- throw new Error("Use the HTTPS server origin without a path or credentials.");
4788
- if (endpoint.protocol !== "https:" && !(process.env.NODE_ENV !== "production" && endpoint.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname)))
4789
- throw new Error("Use HTTPS. Localhost HTTP is for development builds only.");
4790
- return endpoint.origin;
4791
- }
4792
- async function printPairing(dataDir, url) {
4793
- if (!url)
4794
- throw new Error(
4795
- "Specify --public-url with your Tailscale Serve or Cloudflare Tunnel HTTPS address."
4796
- );
4797
- const origin = pairingOrigin(url);
4798
- const grant = z4.object({
4799
- pairingId: z4.string(),
4800
- pin: z4.string(),
4801
- expiresAt: z4.number(),
4802
- serverId: z4.string()
4803
- }).parse(await controlRequest(dataDir, { command: "pair" }));
4804
- const invitation = {
4805
- v: 1,
4806
- url: origin,
4807
- serverId: grant.serverId,
4808
- pairingId: grant.pairingId
4809
- };
4810
- const payload = JSON.stringify(invitation);
4811
- const link = createPairingLink(invitation);
4812
- printHeading("pairing");
4813
- console.log(" 1. Scan below with your phone camera and tap Open Agentrific if offered.");
4814
- console.log(" 2. Or open Agentrific and choose Scan pairing QR code.");
4815
- console.log(" 3. Enter the PIN, then confirm pairing.\n");
4816
- qrcode.generate(link, { small: true }, (code) => {
4817
- console.log(
4818
- code.split("\n").map((line) => ` ${line}`).join("\n")
4819
- );
4820
- });
4821
- console.log();
4822
- printDetail("PIN:", styleText2(["bold", "yellow"], grant.pin));
4823
- printDetail("Server", styleText2("cyan", origin));
4824
- printDetail("Expires", "In five minutes \xB7 one device per code");
4825
- console.log("\n Pairing grants this device access to run agents as your host user.\n");
4826
- return { payload, expiresAt: grant.expiresAt };
4827
- }
5001
+ import { z as z19 } from "zod";
4828
5002
 
4829
5003
  // src/terminal.ts
5004
+ init_pairing();
5005
+ init_cli_output();
4830
5006
  import { createInterface } from "node:readline/promises";
4831
5007
  import select from "@inquirer/select";
4832
5008
  import { emitKeypressEvents as emitKeypressEvents2 } from "node:readline";
4833
5009
  import { styleText as styleText3 } from "node:util";
4834
5010
 
4835
5011
  // src/tunnels.ts
5012
+ init_cli_output();
4836
5013
  import { spawn } from "node:child_process";
4837
5014
  import { access, stat } from "node:fs/promises";
4838
5015
  import { constants } from "node:fs";
@@ -5301,10 +5478,11 @@ function startTerminal(dataDir, publicUrl, tunnel) {
5301
5478
  }
5302
5479
 
5303
5480
  // src/cli.ts
5304
- var ConfigSchema = z18.object({
5305
- dataDir: z18.string(),
5306
- projectsDir: z18.string(),
5307
- publicUrl: z18.string().url().optional()
5481
+ init_cli_output();
5482
+ var ConfigSchema = z19.object({
5483
+ dataDir: z19.string(),
5484
+ projectsDir: z19.string(),
5485
+ publicUrl: z19.string().url().optional()
5308
5486
  });
5309
5487
  function parseCommandArgs() {
5310
5488
  return parseArgs({
@@ -5338,8 +5516,8 @@ async function main() {
5338
5516
  const filename = path12.join(dataDir, "config.json");
5339
5517
  const config = ConfigSchema.parse(
5340
5518
  JSON.parse(
5341
- await readFile5(filename, "utf8").catch((error) => {
5342
- if (!z18.object({ code: z18.literal("ENOENT") }).safeParse(error).success) throw error;
5519
+ await readFile6(filename, "utf8").catch((error) => {
5520
+ if (!z19.object({ code: z19.literal("ENOENT") }).safeParse(error).success) throw error;
5343
5521
  return JSON.stringify({ dataDir, projectsDir: path12.join(homedir5(), "projects") });
5344
5522
  })
5345
5523
  )
@@ -5357,11 +5535,11 @@ async function main() {
5357
5535
  throw new Error("Unknown command. Run agentrific --help.");
5358
5536
  }
5359
5537
  async function initCommand({ values, dataDir, filename, config }) {
5360
- await mkdir5(dataDir, { recursive: true, mode: 448 });
5538
+ await mkdir6(dataDir, { recursive: true, mode: 448 });
5361
5539
  const publicUrl = values["public-url"] ?? config.publicUrl;
5362
5540
  if (publicUrl && new URL(publicUrl).protocol !== "https:")
5363
5541
  throw new Error("Configure an HTTPS URL from Tailscale Serve or Cloudflare Tunnel.");
5364
- await writeFile4(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
5542
+ await writeFile5(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
5365
5543
  mode: 384
5366
5544
  });
5367
5545
  console.log(`Configuration saved to ${filename}. Start with agentrific start.`);
@@ -5389,8 +5567,8 @@ async function startCommand({ values, dataDir, filename, config }) {
5389
5567
  const publicUrl = connection.url;
5390
5568
  try {
5391
5569
  if (connection.remember && publicUrl) {
5392
- await mkdir5(dataDir, { recursive: true, mode: 448 });
5393
- await writeFile4(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
5570
+ await mkdir6(dataDir, { recursive: true, mode: 448 });
5571
+ await writeFile5(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
5394
5572
  mode: 384
5395
5573
  });
5396
5574
  }
@@ -5446,11 +5624,11 @@ async function pairCommand({ values, dataDir, config }) {
5446
5624
  return;
5447
5625
  }
5448
5626
  async function maintenanceCommand({ values, positionals, dataDir }) {
5449
- const action = z18.enum(["enter", "resume", "drain"]).parse(positionals[1]);
5627
+ const action = z19.enum(["enter", "resume", "drain"]).parse(positionals[1]);
5450
5628
  const result = await controlRequest(dataDir, {
5451
5629
  command: "maintenance",
5452
5630
  action,
5453
- timeoutMs: z18.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
5631
+ timeoutMs: z19.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
5454
5632
  });
5455
5633
  console.log(JSON.stringify(result, null, 2));
5456
5634
  if (action === "drain" && "drained" in result && !result.drained) process.exitCode = 1;
@@ -5459,7 +5637,7 @@ async function maintenanceCommand({ values, positionals, dataDir }) {
5459
5637
  async function devicesCommand({ dataDir, command }) {
5460
5638
  console.log(
5461
5639
  JSON.stringify(
5462
- await controlRequest(dataDir, { command: z18.enum(["devices", "status"]).parse(command) }),
5640
+ await controlRequest(dataDir, { command: z19.enum(["devices", "status"]).parse(command) }),
5463
5641
  null,
5464
5642
  2
5465
5643
  )
@@ -5467,7 +5645,7 @@ async function devicesCommand({ dataDir, command }) {
5467
5645
  return;
5468
5646
  }
5469
5647
  async function revokeCommand({ positionals, dataDir }) {
5470
- const id = z18.string().uuid().parse(positionals[1]);
5648
+ const id = z19.string().uuid().parse(positionals[1]);
5471
5649
  console.log(JSON.stringify(await controlRequest(dataDir, { command: "revoke", id })));
5472
5650
  return;
5473
5651
  }