@agentrific/server 0.1.5 → 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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @agentrific/server
2
2
 
3
+ ## 0.1.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 70ffb32: Add an Electron desktop client with encrypted pairing storage and task notifications, macOS preview and signed release builds, and a marketing downloads page backed by public Cloudflare R2 artifacts. Allow the desktop app origin in the server's default origin policy.
8
+ - 5e2d3a2: Show task titles and event-specific notification details, open validated GitHub pull requests from PR alerts, and provide transparent Android notification and adaptive/themed launcher icons. Native icon configuration requires a rebuilt client and a new release runtime. Add an explicitly isolated, expiring local push test mode without weakening production billing checks.
9
+ - 991dd7e: Add a Pair device modal to web Settings with a locally generated QR code, separate six-digit PIN, expiry countdown and retry handling. Authenticated clients can request a single-use invitation from the connected server using its configured public HTTPS URL.
10
+
3
11
  ## 0.1.5
4
12
 
5
13
  ### Patch Changes
package/README.md CHANGED
@@ -32,6 +32,8 @@ The server defaults to port 4318 and stores its configuration and state in `~/.a
32
32
 
33
33
  Open the Agentrific app on iOS or Android. In the server terminal, choose Tailscale Serve, Cloudflare Tunnel, or an existing HTTPS endpoint, then press **p** to display a pairing QR code. Scan it in the app and enter the separately printed six-digit PIN.
34
34
 
35
+ Already paired in the web app? Open **Settings → Pair device** to generate a QR code and separate PIN for another device. Invitations expire after five minutes and can be used once. The server must have a public HTTPS URL configured (`AGENTRIFIC_PUBLIC_URL`); restart after changing it. Only share invitations with trusted devices: pairing grants access to run agents as the server’s host user.
36
+
35
37
  Once paired, select a project, create a task and start an agent. The server runs the work and streams progress back to your device. Use HTTPS for remote connections; your phone must be able to reach the configured server address.
36
38
 
37
39
  See the [server setup and pairing guide](https://agentrific.com/docs/secure-server-setup) for connection options and configuration.
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.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,6 +732,202 @@ 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
+
730
931
  // ../../packages/contracts/src/push.ts
731
932
  import { z as z6 } from "zod";
732
933
  function pushSigningText(installationId, operation, request) {
@@ -740,7 +941,7 @@ function pushSigningText(installationId, operation, request) {
740
941
  request.payload
741
942
  ].join("\n");
742
943
  }
743
- var PUSH_API_URL, PushKindSchema, PushPreferencesSchema, PushPublicKeySchema, PushInstallationSchema, PushRegistrationSchema, PushEnrollmentSchema, PushGrantSchema, PushEventSchema, PushSignedRequestSchema, PushEntitlementSchema, PushServerStatusSchema;
944
+ var PUSH_API_URL, PushKindSchema, PushPreferencesSchema, PushPublicKeySchema, PushInstallationSchema, PushRegistrationSchema, PushEnrollmentSchema, PushGrantSchema, PushPullRequestUrlSchema, PushEventSchema, PushSignedRequestSchema, PushEntitlementSchema, PushServerStatusSchema;
744
945
  var init_push = __esm({
745
946
  "../../packages/contracts/src/push.ts"() {
746
947
  "use strict";
@@ -768,7 +969,14 @@ var init_push = __esm({
768
969
  grant: z6.string().regex(/^[a-f0-9]{64}$/),
769
970
  expiresAt: z6.number()
770
971
  }).strict();
771
- PushEventSchema = z6.object({ eventId: z6.string().uuid(), sessionId: z6.string().uuid(), kind: PushKindSchema }).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();
772
980
  PushSignedRequestSchema = z6.object({
773
981
  bindingId: z6.string().uuid(),
774
982
  timestamp: z6.number().int(),
@@ -855,10 +1063,11 @@ var init_push2 = __esm({
855
1063
  "use strict";
856
1064
  init_push();
857
1065
  ServerPush = class {
858
- constructor(store2, serverId, directory, transport = fetch) {
1066
+ constructor(store2, serverId, directory, transport = fetch, apiUrl = PUSH_API_URL) {
859
1067
  this.store = store2;
860
1068
  this.serverId = serverId;
861
1069
  this.transport = transport;
1070
+ this.apiUrl = apiUrl;
862
1071
  mkdirSync(directory, { recursive: true, mode: 448 });
863
1072
  const filename = path4.join(directory, "push-signing-key.pem");
864
1073
  if (!existsSync(filename))
@@ -873,6 +1082,7 @@ var init_push2 = __esm({
873
1082
  store;
874
1083
  serverId;
875
1084
  transport;
1085
+ apiUrl;
876
1086
  key;
877
1087
  publicKey;
878
1088
  draining;
@@ -886,6 +1096,8 @@ var init_push2 = __esm({
886
1096
  eventId: randomUUID(),
887
1097
  sessionId: session.id,
888
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,
889
1101
  remaining: state.bindings,
890
1102
  createdAt: Date.now(),
891
1103
  attempts: 0,
@@ -908,7 +1120,7 @@ var init_push2 = __esm({
908
1120
  Buffer.from(pushSigningText(installationId, operation, request)),
909
1121
  this.key
910
1122
  ).toString("hex");
911
- return this.transport(`${PUSH_API_URL}/v1/installations/${installationId}/${operation}`, {
1123
+ return this.transport(`${this.apiUrl}/v1/installations/${installationId}/${operation}`, {
912
1124
  method: "POST",
913
1125
  headers: { "Content-Type": "application/json" },
914
1126
  body: JSON.stringify({ ...request, signature }),
@@ -982,7 +1194,13 @@ var init_push2 = __esm({
982
1194
  binding.installationId,
983
1195
  binding.bindingId,
984
1196
  "events",
985
- JSON.stringify({ eventId: job.eventId, sessionId: job.sessionId, kind: job.kind })
1197
+ JSON.stringify({
1198
+ eventId: job.eventId,
1199
+ sessionId: job.sessionId,
1200
+ kind: job.kind,
1201
+ taskTitle: job.taskTitle,
1202
+ prUrl: job.prUrl
1203
+ })
986
1204
  );
987
1205
  if (response.ok) continue;
988
1206
  if ([401, 403, 404].includes(response.status)) {
@@ -1332,6 +1550,29 @@ async function registerSecurity(app2, security2, version, allowedOrigins2) {
1332
1550
  const result = await security2.pair(input.data, request.ip);
1333
1551
  return result ?? reply.code(401).send({ error: "Pairing expired, locked, or invalid. Run agentrific pair again." });
1334
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
+ });
1335
1576
  app2.get("/api/auth/session", (request) => ({
1336
1577
  serverId: security2.serverId,
1337
1578
  device: security2.authenticate((request.headers.authorization ?? "").slice(7))
@@ -1354,6 +1595,7 @@ var init_security = __esm({
1354
1595
  "src/security.ts"() {
1355
1596
  "use strict";
1356
1597
  init_src();
1598
+ init_pairing();
1357
1599
  secret = () => randomBytes(32).toString("hex");
1358
1600
  digest = (value) => createHash("sha256").update(value).digest("hex");
1359
1601
  ServerSecurity = class {
@@ -3968,7 +4210,7 @@ async function withSessionTaskLock(id, operation) {
3968
4210
  const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
3969
4211
  return withTaskLock(lockId, operation);
3970
4212
  }
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;
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;
3972
4214
  var init_index = __esm({
3973
4215
  async "src/index.ts"() {
3974
4216
  "use strict";
@@ -4025,9 +4267,13 @@ var init_index = __esm({
4025
4267
  effects = ManagedRuntime.make(SessionStore.layer(path11.join(dataRoot, "sessions.sqlite")));
4026
4268
  store = await effects.runPromise(SessionStore);
4027
4269
  security = new ServerSecurity(store, await Effect16.runPromise(store.serverIdentity()));
4028
- push = new ServerPush(store, security.serverId, dataRoot);
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);
4029
4274
  allowedOrigins = [
4030
4275
  .../* @__PURE__ */ new Set([
4276
+ "agentrific-desktop://app",
4031
4277
  ...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
4032
4278
  `http://${host === "::1" ? "[::1]" : host}:${port}`,
4033
4279
  ...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
@@ -4635,204 +4881,21 @@ async function installService(platform, directory, filename, domain, dataDir, cl
4635
4881
  init_updates();
4636
4882
  init_control();
4637
4883
  init_version();
4884
+ init_pairing();
4638
4885
  import { spawn as spawn2 } from "node:child_process";
4639
4886
  import { parseArgs } from "node:util";
4640
4887
  import { z as z18 } from "zod";
4641
4888
 
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
- }
4828
-
4829
4889
  // src/terminal.ts
4890
+ init_pairing();
4891
+ init_cli_output();
4830
4892
  import { createInterface } from "node:readline/promises";
4831
4893
  import select from "@inquirer/select";
4832
4894
  import { emitKeypressEvents as emitKeypressEvents2 } from "node:readline";
4833
4895
  import { styleText as styleText3 } from "node:util";
4834
4896
 
4835
4897
  // src/tunnels.ts
4898
+ init_cli_output();
4836
4899
  import { spawn } from "node:child_process";
4837
4900
  import { access, stat } from "node:fs/promises";
4838
4901
  import { constants } from "node:fs";
@@ -5301,6 +5364,7 @@ function startTerminal(dataDir, publicUrl, tunnel) {
5301
5364
  }
5302
5365
 
5303
5366
  // src/cli.ts
5367
+ init_cli_output();
5304
5368
  var ConfigSchema = z18.object({
5305
5369
  dataDir: z18.string(),
5306
5370
  projectsDir: z18.string(),