@agentrific/server 0.1.0 → 0.1.4

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,8 +30,20 @@ var init_package = __esm({
30
30
  "package.json"() {
31
31
  package_default = {
32
32
  name: "@agentrific/server",
33
- version: "0.1.0",
33
+ version: "0.1.4",
34
34
  description: "Self-hosted control plane for long-running coding agents",
35
+ keywords: [
36
+ "agentrific",
37
+ "ai",
38
+ "android",
39
+ "cli",
40
+ "coding-agents",
41
+ "ios",
42
+ "pi",
43
+ "remote-control",
44
+ "self-hosted"
45
+ ],
46
+ homepage: "https://agentrific.com",
35
47
  repository: {
36
48
  type: "git",
37
49
  url: "git+https://github.com/devular/agentrific.git",
@@ -51,7 +63,7 @@ var init_package = __esm({
51
63
  cli: "AGENTRIFIC_DATA_DIR=./data tsx src/cli.ts"
52
64
  },
53
65
  dependencies: {
54
- "@earendil-works/pi-coding-agent": "^0.87.1",
66
+ "@earendil-works/pi-coding-agent": "1.0.0",
55
67
  "@fastify/cors": "^11.3.0",
56
68
  "@fastify/multipart": "^10.1.2",
57
69
  "@inquirer/select": "^5.2.5",
@@ -192,6 +204,9 @@ var init_updates = __esm({
192
204
 
193
205
  // ../../packages/contracts/src/index.ts
194
206
  import { z as z2 } from "zod";
207
+ function cronRange(base, min, max, stepped) {
208
+ return base === "*" ? [min, max] : base.includes("-") ? base.split("-").map(Number) : [Number(base), stepped ? max : Number(base)];
209
+ }
195
210
  function cronFields(expression) {
196
211
  const fields = expression.trim().split(/\s+/);
197
212
  if (fields.length !== 5)
@@ -204,7 +219,7 @@ function cronFields(expression) {
204
219
  if (!match) throw new Error("Use numeric cron fields, *, ranges, lists and steps.");
205
220
  const base = match[1];
206
221
  const step = Number(match[2] ?? 1);
207
- const [start, end] = base === "*" ? [min, max] : base.includes("-") ? base.split("-").map(Number) : [Number(base), match[2] ? max : Number(base)];
222
+ const [start, end] = cronRange(base, min, max, Boolean(match[2]));
208
223
  if (!Number.isInteger(step) || step < 1 || step > max - min + 1 || start < min || end > max || start > end)
209
224
  throw new Error("Cron field is outside its allowed range.");
210
225
  for (let value = start; value <= end; value += step) values.add(value);
@@ -225,6 +240,11 @@ function cronMatches(expression, date) {
225
240
  const day = fields[2].wildcard || fields[4].wildcard ? matches[2] && matches[4] : matches[2] || matches[4];
226
241
  return Boolean(matches[0] && matches[1] && matches[3] && day);
227
242
  }
243
+ function createPairingLink(payload) {
244
+ const link = new URL("agentrific://pair");
245
+ link.searchParams.set("data", JSON.stringify(PairingPayloadSchema.parse(payload)));
246
+ return link.toString();
247
+ }
228
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;
229
249
  var init_src = __esm({
230
250
  "../../packages/contracts/src/index.ts"() {
@@ -456,6 +476,7 @@ var init_src = __esm({
456
476
  gitFlow: GitFlowSchema.optional(),
457
477
  status: TaskStatusSchema,
458
478
  sessionId: z2.string().nullable(),
479
+ pullRequestUrl: z2.string().url().optional(),
459
480
  createdAt: z2.string().datetime(),
460
481
  updatedAt: z2.string().datetime(),
461
482
  deletedAt: z2.string().datetime().optional()
@@ -473,7 +494,7 @@ var init_src = __esm({
473
494
  prompt: true,
474
495
  attachmentIds: true,
475
496
  gitFlow: true
476
- }).strict();
497
+ }).extend({ gitFlow: GitFlowSchema.nullable().optional() }).strict();
477
498
  PROTOCOL_VERSION = 1;
478
499
  ServerUpdateStatusSchema = z2.object({
479
500
  currentVersion: z2.string(),
@@ -1223,6 +1244,9 @@ function taskStatusForSession(status, session) {
1223
1244
  return "needs_user_input";
1224
1245
  }
1225
1246
  }
1247
+ function pullRequestUrl(session) {
1248
+ return session.gitDelivery?.status === "pr_opened" ? session.gitDelivery.url : void 0;
1249
+ }
1226
1250
  function sessionTask(session) {
1227
1251
  return TaskSchema.parse({
1228
1252
  id: `session-${session.id}`,
@@ -1233,6 +1257,7 @@ function sessionTask(session) {
1233
1257
  gitFlow: session.gitFlow,
1234
1258
  status: taskStatusForSession(session.status, session),
1235
1259
  sessionId: session.id,
1260
+ pullRequestUrl: pullRequestUrl(session),
1236
1261
  createdAt: session.createdAt,
1237
1262
  updatedAt: session.updatedAt
1238
1263
  });
@@ -1244,12 +1269,13 @@ async function boardTasks(store2, projectId) {
1244
1269
  tasks.flatMap((task) => task.sessionId ? [task.sessionId] : [])
1245
1270
  );
1246
1271
  const linkedTasks = tasks.filter((task) => !task.deletedAt).map((task) => {
1247
- const session = sessions.find((item) => item.taskId === task.id) ?? sessions.find((item) => item.id === task.sessionId);
1248
- if (!session) return { ...task, status: "backlog" };
1272
+ const session = sessions.find((item) => item.id === task.sessionId) ?? sessions.find((item) => item.taskId === task.id);
1273
+ if (!session) return { ...task, status: "backlog", pullRequestUrl: void 0 };
1249
1274
  linkedSessionIds.add(session.id);
1250
1275
  return {
1251
1276
  ...task,
1252
1277
  sessionId: session.id,
1278
+ pullRequestUrl: pullRequestUrl(session),
1253
1279
  status: taskStatusForSession(session.status, session)
1254
1280
  };
1255
1281
  });
@@ -1267,7 +1293,9 @@ async function findTask(store2, id) {
1267
1293
  function editTask(store2, id, input, resolveAttachments) {
1268
1294
  return withTaskLock(id, async () => {
1269
1295
  const task = await findTask(store2, id);
1270
- const { attachmentIds, ...updates2 } = input;
1296
+ const { attachmentIds, gitFlow, ...updates2 } = input;
1297
+ const configuration = { gitFlow: task.gitFlow };
1298
+ if (gitFlow !== void 0) configuration.gitFlow = gitFlow ?? void 0;
1271
1299
  let files = task.attachments;
1272
1300
  if (attachmentIds) {
1273
1301
  if (!resolveAttachments)
@@ -1278,6 +1306,7 @@ function editTask(store2, id, input, resolveAttachments) {
1278
1306
  store2.saveTask({
1279
1307
  ...task,
1280
1308
  ...updates2,
1309
+ ...configuration,
1281
1310
  attachments: files,
1282
1311
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1283
1312
  })
@@ -1554,7 +1583,7 @@ var init_runtime = __esm({
1554
1583
  Effect7.gen({ self: this }, function* () {
1555
1584
  if (this.closed)
1556
1585
  return yield* Effect7.fail(new AgentError({ message: "Server is shutting down" }));
1557
- if (!session.branch)
1586
+ if (!session.branch && (session.gitFlow || !session.workspacePath))
1558
1587
  return yield* Effect7.fail(new AgentError({ message: "Session checkout is not ready" }));
1559
1588
  if (this.hasMessage(session.id, messageId)) return;
1560
1589
  const entry = yield* this.open(session);
@@ -1882,45 +1911,7 @@ ${error ? workflowError(error instanceof Error ? error : String(error)) : messag
1882
1911
  if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
1883
1912
  throw new Error("Restore the assigned run branch before requesting delivery.");
1884
1913
  if (dirty) {
1885
- await this.git(session, ["add", "--all"], signal);
1886
- await this.stage(job, "drafting");
1887
- const diff = await this.git(session, ["diff", "--cached", "HEAD", "--"], signal);
1888
- const files = await this.git(
1889
- session,
1890
- ["ls-files", "--others", "--exclude-standard"],
1891
- signal
1892
- );
1893
- if (this.options.draft) {
1894
- try {
1895
- const draft = await this.options.draft(
1896
- session,
1897
- input,
1898
- `${diff}
1899
- New files:
1900
- ${files}`.slice(0, 4e4),
1901
- signal
1902
- );
1903
- job.title = draft.title;
1904
- job.body = draft.body;
1905
- } catch (error) {
1906
- job.logs = workflowLog(
1907
- `${job.logs}
1908
- Message helper unavailable; using task summary. ${workflowError(error instanceof Error ? error : String(error))}`
1909
- );
1910
- }
1911
- }
1912
- await this.stage(job, "committing");
1913
- if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
1914
- throw new Error("The workspace changed branches while preparing delivery.");
1915
- const output = await this.git(
1916
- session,
1917
- ["commit", "-m", `${job.title}
1918
-
1919
- Agentrific run ${id}.`],
1920
- signal
1921
- );
1922
- job.logs = workflowLog(`${job.logs}
1923
- ${output}`);
1914
+ await this.commitChanges(session, job, input, signal);
1924
1915
  } else {
1925
1916
  await this.git(session, ["hook", "run", "--ignore-missing", "pre-commit"], signal);
1926
1917
  }
@@ -1947,6 +1938,44 @@ ${workflowError(error instanceof Error ? error : String(error))}`
1947
1938
  }
1948
1939
  });
1949
1940
  }
1941
+ async commitChanges(session, job, input, signal) {
1942
+ const id = session.id;
1943
+ await this.git(session, ["add", "--all"], signal);
1944
+ await this.stage(job, "drafting");
1945
+ const diff = await this.git(session, ["diff", "--cached", "HEAD", "--"], signal);
1946
+ const files = await this.git(session, ["ls-files", "--others", "--exclude-standard"], signal);
1947
+ if (this.options.draft) {
1948
+ try {
1949
+ const draft = await this.options.draft(
1950
+ session,
1951
+ input,
1952
+ `${diff}
1953
+ New files:
1954
+ ${files}`.slice(0, 4e4),
1955
+ signal
1956
+ );
1957
+ job.title = draft.title;
1958
+ job.body = draft.body;
1959
+ } catch (error) {
1960
+ job.logs = workflowLog(
1961
+ `${job.logs}
1962
+ Message helper unavailable; using task summary. ${workflowError(error instanceof Error ? error : String(error))}`
1963
+ );
1964
+ }
1965
+ }
1966
+ await this.stage(job, "committing");
1967
+ if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
1968
+ throw new Error("The workspace changed branches while preparing delivery.");
1969
+ const output = await this.git(
1970
+ session,
1971
+ ["commit", "-m", `${job.title}
1972
+
1973
+ Agentrific run ${id}.`],
1974
+ signal
1975
+ );
1976
+ job.logs = workflowLog(`${job.logs}
1977
+ ${output}`);
1978
+ }
1950
1979
  fallbackBody(id, input) {
1951
1980
  return `${input.summary}
1952
1981
 
@@ -2019,6 +2048,31 @@ Agentrific run ${id}.`;
2019
2048
  );
2020
2049
  }
2021
2050
  }
2051
+ async mergeChanges(session, job, projectPath, commit) {
2052
+ await this.stage(job, "integrating");
2053
+ if (session.gitFlow.workspace === "checkout" && await this.command(projectPath, "git", ["branch", "--show-current"]) === session.branch)
2054
+ await this.command(projectPath, "git", ["switch", "main"]);
2055
+ if (await this.command(projectPath, "git", ["branch", "--show-current"]) !== "main")
2056
+ throw new Error("Switch the project checkout to main, then retry delivery.");
2057
+ if (await this.command(projectPath, "git", ["status", "--porcelain"]))
2058
+ throw new Error(
2059
+ "Waiting for local changes in the main checkout. Commit or move them, then retry delivery."
2060
+ );
2061
+ const included = await this.command(projectPath, "git", ["merge-base", "main", commit]) === commit;
2062
+ if (!included) {
2063
+ try {
2064
+ await this.command(projectPath, "git", ["merge", "--no-edit", commit]);
2065
+ } catch (error) {
2066
+ await this.command(projectPath, "git", ["merge", "--abort"]).catch(() => void 0);
2067
+ throw new Error(
2068
+ `Integration failed. Resolve conflicts against main in the run workspace, then call deliver_changes again.
2069
+ ${workflowError(error instanceof Error ? error : String(error))}`
2070
+ );
2071
+ }
2072
+ }
2073
+ job.result = "merged";
2074
+ job.message = "Committed changes are on main.";
2075
+ }
2022
2076
  async integrate(id) {
2023
2077
  const job = await this.job(id);
2024
2078
  if (!job?.commitSha) return;
@@ -2032,29 +2086,7 @@ Agentrific run ${id}.`;
2032
2086
  "The workspace changed after requesting delivery. Call deliver_changes again for the new changes."
2033
2087
  );
2034
2088
  if (session.gitFlow.delivery === "merge") {
2035
- await this.stage(job, "integrating");
2036
- if (session.gitFlow.workspace === "checkout" && await this.command(projectPath, "git", ["branch", "--show-current"]) === session.branch)
2037
- await this.command(projectPath, "git", ["switch", "main"]);
2038
- if (await this.command(projectPath, "git", ["branch", "--show-current"]) !== "main")
2039
- throw new Error("Switch the project checkout to main, then retry delivery.");
2040
- if (await this.command(projectPath, "git", ["status", "--porcelain"]))
2041
- throw new Error(
2042
- "Waiting for local changes in the main checkout. Commit or move them, then retry delivery."
2043
- );
2044
- const included = await this.command(projectPath, "git", ["merge-base", "main", commit]) === commit;
2045
- if (!included) {
2046
- try {
2047
- await this.command(projectPath, "git", ["merge", "--no-edit", commit]);
2048
- } catch (error) {
2049
- await this.command(projectPath, "git", ["merge", "--abort"]).catch(() => void 0);
2050
- throw new Error(
2051
- `Integration failed. Resolve conflicts against main in the run workspace, then call deliver_changes again.
2052
- ${workflowError(error instanceof Error ? error : String(error))}`
2053
- );
2054
- }
2055
- }
2056
- job.result = "merged";
2057
- job.message = "Committed changes are on main.";
2089
+ await this.mergeChanges(session, job, projectPath, commit);
2058
2090
  } else {
2059
2091
  await this.stage(job, "pushing");
2060
2092
  await this.git(session, ["push", "origin", `${commit}:refs/heads/${session.branch}`]);
@@ -2141,6 +2173,7 @@ var init_preparation = __esm({
2141
2173
  return [...this.preparing.values()].map((id) => `preparation:${id}`);
2142
2174
  }
2143
2175
  async prepare(session, projectPath, input) {
2176
+ if (!session.gitFlow) return { ...session, branch: null, workspacePath: projectPath };
2144
2177
  const preparation2 = Symbol(session.id);
2145
2178
  this.preparing.set(preparation2, session.id);
2146
2179
  return withTaskLock(`prepare:${session.id}`, async () => {
@@ -2173,56 +2206,7 @@ ${logs}` : ""}`);
2173
2206
  try {
2174
2207
  await record("creating", "Creating the run workspace\u2026");
2175
2208
  const workspace = session.workspacePath;
2176
- const checkout = session.gitFlow?.workspace === "checkout";
2177
- const branchAtWorkspace = await command(workspace, "git", [
2178
- "branch",
2179
- "--show-current"
2180
- ]).catch(() => void 0);
2181
- if (branchAtWorkspace !== session.branch) {
2182
- if (checkout) {
2183
- if (branchAtWorkspace !== "main" || await command(projectPath, "git", ["status", "--porcelain"]))
2184
- throw new Error(
2185
- "The project checkout must be clean and on main before starting a checkout run."
2186
- );
2187
- await command(
2188
- projectPath,
2189
- "git",
2190
- ["switch", "-c", session.branch, "main"],
2191
- void 0,
2192
- true
2193
- );
2194
- } else {
2195
- if (branchAtWorkspace !== void 0)
2196
- throw new Error("The run workspace already exists on a different branch.");
2197
- await mkdir4(path6.dirname(workspace), { recursive: true });
2198
- try {
2199
- await command(
2200
- projectPath,
2201
- "git",
2202
- [
2203
- "worktree",
2204
- "add",
2205
- "-b",
2206
- session.branch,
2207
- workspace,
2208
- session.gitFlow ? "main" : "HEAD"
2209
- ],
2210
- void 0,
2211
- true
2212
- );
2213
- } catch (error) {
2214
- if (await command(workspace, "git", ["branch", "--show-current"]).catch(
2215
- () => void 0
2216
- ) !== session.branch)
2217
- throw error;
2218
- await record(
2219
- "creating",
2220
- "Workspace created; a checkout hook failed. Preparing dependencies\u2026",
2221
- workflowError(error instanceof Error ? error : String(error))
2222
- );
2223
- }
2224
- }
2225
- }
2209
+ await this.createWorkspace(session, projectPath, command, record);
2226
2210
  await record("installing", "Installing run dependencies\u2026");
2227
2211
  await this.install(workspace, command);
2228
2212
  await record("ready", "Workspace ready.");
@@ -2238,6 +2222,57 @@ ${logs}` : ""}`);
2238
2222
  }
2239
2223
  }).finally(() => this.preparing.delete(preparation2));
2240
2224
  }
2225
+ async createWorkspace(session, projectPath, command, record) {
2226
+ const workspace = session.workspacePath;
2227
+ const checkout = session.gitFlow?.workspace === "checkout";
2228
+ if (!await command(projectPath, "git", ["rev-parse", "--verify", "HEAD"]).catch(() => void 0))
2229
+ throw new Error(
2230
+ "Git Flow needs an initial commit. Create one in the project, or turn off Git Flow for a plain chat."
2231
+ );
2232
+ const branchAtWorkspace = await command(workspace, "git", ["branch", "--show-current"]).catch(
2233
+ () => void 0
2234
+ );
2235
+ if (branchAtWorkspace !== session.branch) {
2236
+ if (checkout) {
2237
+ if (branchAtWorkspace !== "main" || await command(projectPath, "git", ["status", "--porcelain"]))
2238
+ throw new Error(
2239
+ "The project checkout must be clean and on main before starting a checkout run."
2240
+ );
2241
+ await command(
2242
+ projectPath,
2243
+ "git",
2244
+ ["switch", "-c", session.branch, "main"],
2245
+ void 0,
2246
+ true
2247
+ );
2248
+ } else {
2249
+ await this.createWorktree(session, projectPath, command, record, branchAtWorkspace);
2250
+ }
2251
+ }
2252
+ }
2253
+ async createWorktree(session, projectPath, command, record, branchAtWorkspace) {
2254
+ const workspace = session.workspacePath;
2255
+ if (branchAtWorkspace !== void 0)
2256
+ throw new Error("The run workspace already exists on a different branch.");
2257
+ await mkdir4(path6.dirname(workspace), { recursive: true });
2258
+ try {
2259
+ await command(
2260
+ projectPath,
2261
+ "git",
2262
+ ["worktree", "add", "-b", session.branch, workspace, "main"],
2263
+ void 0,
2264
+ true
2265
+ );
2266
+ } catch (error) {
2267
+ if (await command(workspace, "git", ["branch", "--show-current"]).catch(() => void 0) !== session.branch)
2268
+ throw error;
2269
+ await record(
2270
+ "creating",
2271
+ "Workspace created; a checkout hook failed. Preparing dependencies\u2026",
2272
+ workflowError(error instanceof Error ? error : String(error))
2273
+ );
2274
+ }
2275
+ }
2241
2276
  async install(workspace, command) {
2242
2277
  const packageFile = await readFile3(path6.join(workspace, "package.json"), "utf8").catch(
2243
2278
  () => void 0
@@ -2328,10 +2363,10 @@ ${logs}` : ""}`);
2328
2363
  // src/delivery-writer.ts
2329
2364
  import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
2330
2365
  import { z as z11 } from "zod";
2331
- function deliveryWriter(models2) {
2366
+ function deliveryWriter(models2, createSession = createAgentSession) {
2332
2367
  return async (run, input, diff, signal) => {
2333
2368
  signal?.throwIfAborted();
2334
- const { session } = await createAgentSession({
2369
+ const { session } = await createSession({
2335
2370
  cwd: run.workspacePath,
2336
2371
  model: await models2.resolve(run.model ?? await models2.defaultReference()),
2337
2372
  modelRuntime: models2.runtime,
@@ -2535,11 +2570,11 @@ function toolOutput(value) {
2535
2570
  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);
2536
2571
  return text.length > 4e3 ? `${text.slice(0, 4e3)}\u2026 [truncated]` : text;
2537
2572
  }
2538
- function createPiFactory(files, models2, delivery2) {
2573
+ function createPiFactory(files, models2, delivery2, createSession = createAgentSession2) {
2539
2574
  return {
2540
2575
  async open(sessionId, cwd, sessionDirectory, selected = legacyModel, run) {
2541
2576
  const model = await models2.resolve(selected);
2542
- const { session } = await createAgentSession2({
2577
+ const { session } = await createSession({
2543
2578
  cwd,
2544
2579
  model,
2545
2580
  modelRuntime: models2.runtime,
@@ -2998,31 +3033,7 @@ var init_store = __esm({
2998
3033
  this.db.prepare("DELETE FROM projects WHERE id = ?").run(replacedId);
2999
3034
  }
3000
3035
  if (previousPath && previousPath !== project.path) {
3001
- this.db.prepare(
3002
- "UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
3003
- ).run(project.path, previousPath);
3004
- const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path8.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
3005
- for (const row of this.db.prepare(
3006
- "SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?"
3007
- ).all(project.id)) {
3008
- const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
3009
- if (session.workspacePath) {
3010
- session.workspacePath = String(relocate(session.workspacePath));
3011
- this.db.prepare("UPDATE sessions SET payload = ? WHERE id = ?").run(JSON.stringify(session), session.id);
3012
- }
3013
- }
3014
- for (const row of this.db.prepare(
3015
- "SELECT sequence, payload FROM events WHERE json_extract(payload, '$.session.projectId') = ?"
3016
- ).all(project.id)) {
3017
- const event = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload).session);
3018
- if (event.workspacePath) {
3019
- event.workspacePath = String(relocate(event.workspacePath));
3020
- this.db.prepare("UPDATE events SET payload = ? WHERE sequence = ?").run(
3021
- JSON.stringify({ type: "session.updated", session: event }),
3022
- Number(row.sequence)
3023
- );
3024
- }
3025
- }
3036
+ this.relocateProjectHistory(project, previousPath);
3026
3037
  }
3027
3038
  this.db.prepare(
3028
3039
  "INSERT INTO projects(id, payload) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET payload = excluded.payload"
@@ -3035,6 +3046,28 @@ var init_store = __esm({
3035
3046
  }
3036
3047
  });
3037
3048
  }
3049
+ relocateProjectHistory(project, previousPath) {
3050
+ this.db.prepare(
3051
+ "UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
3052
+ ).run(project.path, previousPath);
3053
+ const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path8.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
3054
+ for (const row of this.db.prepare("SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?").all(project.id)) {
3055
+ const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
3056
+ if (session.workspacePath) {
3057
+ session.workspacePath = String(relocate(session.workspacePath));
3058
+ this.db.prepare("UPDATE sessions SET payload = ? WHERE id = ?").run(JSON.stringify(session), session.id);
3059
+ }
3060
+ }
3061
+ for (const row of this.db.prepare(
3062
+ "SELECT sequence, payload FROM events WHERE json_extract(payload, '$.session.projectId') = ?"
3063
+ ).all(project.id)) {
3064
+ const event = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload).session);
3065
+ if (event.workspacePath) {
3066
+ event.workspacePath = String(relocate(event.workspacePath));
3067
+ this.db.prepare("UPDATE events SET payload = ? WHERE sequence = ?").run(JSON.stringify({ type: "session.updated", session: event }), Number(row.sequence));
3068
+ }
3069
+ }
3070
+ }
3038
3071
  defaultModel() {
3039
3072
  return this.query(() => {
3040
3073
  const row = this.db.prepare("SELECT payload FROM settings WHERE id = 'default-model'").get();
@@ -3286,16 +3319,27 @@ async function githubCliStatus() {
3286
3319
  return { githubCli: { installed: true, authenticated: false } };
3287
3320
  }
3288
3321
  }
3289
- async function createAndStartSession(input) {
3290
- maintenance.assertReady();
3291
- if (input.gitFlow?.delivery === "pull_request") {
3292
- const github = await githubCliStatus();
3293
- if (!github.githubCli.authenticated)
3294
- return { error: "GitHub CLI must be authenticated on the server to open a PR.", status: 400 };
3322
+ async function selectSessionModel(input, hasImages) {
3323
+ try {
3324
+ const reference = input.model ?? await models.defaultReference();
3325
+ const selected = await models.resolve(reference);
3326
+ if (hasImages && !selected.input.includes("image"))
3327
+ return {
3328
+ error: "This model cannot read images. Choose a vision model in Settings.",
3329
+ status: 400
3330
+ };
3331
+ return { model: models.identity(selected.provider, selected.id) };
3332
+ } catch {
3333
+ return {
3334
+ error: "The default model is unavailable. Choose an available model in Settings.",
3335
+ status: 400
3336
+ };
3295
3337
  }
3338
+ }
3339
+ async function resolveSessionProject(projectId) {
3296
3340
  const projectsRoot = await Effect14.runPromise(store.projectsRoot(defaultProjectsRoot));
3297
3341
  const project = (await Effect14.runPromise(discoverProjects(projectsRoot, store))).find(
3298
- (item) => item.id === input.projectId && item.available
3342
+ (item) => item.id === projectId && item.available
3299
3343
  );
3300
3344
  if (!project)
3301
3345
  return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
@@ -3303,6 +3347,37 @@ async function createAndStartSession(input) {
3303
3347
  const resolvedRoot = await realpath2(projectsRoot);
3304
3348
  if (path9.dirname(projectPath) !== resolvedRoot)
3305
3349
  return { error: "Project path is outside projects root.", status: 400 };
3350
+ return { projectPath };
3351
+ }
3352
+ async function resolveSessionWorkspace(id, gitFlow, projectPath) {
3353
+ const worktreePath = path9.join(worktreesRoot, id);
3354
+ const branch = gitFlow ? `agentrific/${id.slice(0, 8)}` : null;
3355
+ const useProjectCheckout = gitFlow?.workspace === "checkout";
3356
+ const workspacePath = !gitFlow || useProjectCheckout ? projectPath : worktreePath;
3357
+ if (useProjectCheckout) {
3358
+ const serverPath = await realpath2(process.cwd());
3359
+ if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path9.sep}`))
3360
+ return {
3361
+ error: "The server runs from this project. Choose a worktree for this task.",
3362
+ status: 409
3363
+ };
3364
+ }
3365
+ return { branch, workspacePath };
3366
+ }
3367
+ function sessionTaskUnavailable(task, projectId) {
3368
+ return !task || Boolean(task.deletedAt) || task.projectId !== projectId;
3369
+ }
3370
+ async function sessionDeliveryAvailable(gitFlow) {
3371
+ if (gitFlow?.delivery !== "pull_request") return true;
3372
+ return (await githubCliStatus()).githubCli.authenticated;
3373
+ }
3374
+ async function createAndStartSession(input) {
3375
+ maintenance.assertReady();
3376
+ if (!await sessionDeliveryAvailable(input.gitFlow))
3377
+ return { error: "GitHub CLI must be authenticated on the server to open a PR.", status: 400 };
3378
+ const location = await resolveSessionProject(input.projectId);
3379
+ if (location.error !== void 0) return { error: location.error, status: location.status };
3380
+ const projectPath = location.projectPath;
3306
3381
  let files;
3307
3382
  try {
3308
3383
  files = await Effect14.runPromise(
@@ -3311,25 +3386,15 @@ async function createAndStartSession(input) {
3311
3386
  } catch (error) {
3312
3387
  return { error: String(error), status: 400 };
3313
3388
  }
3314
- let selectedModel;
3315
- try {
3316
- const reference = input.model ?? await models.defaultReference();
3317
- const selected = await models.resolve(reference);
3318
- if (files.some((file) => file.kind === "image") && !selected.input.includes("image"))
3319
- return {
3320
- error: "This model cannot read images. Choose a vision model in Settings.",
3321
- status: 400
3322
- };
3323
- selectedModel = models.identity(selected.provider, selected.id);
3324
- } catch {
3325
- return {
3326
- error: "The default model is unavailable. Choose an available model in Settings.",
3327
- status: 400
3328
- };
3329
- }
3389
+ const selection = await selectSessionModel(
3390
+ input,
3391
+ files.some((file) => file.kind === "image")
3392
+ );
3393
+ if (selection.error !== void 0) return { error: selection.error, status: selection.status };
3394
+ const selectedModel = selection.model;
3330
3395
  const now = (/* @__PURE__ */ new Date()).toISOString();
3331
3396
  const task = input.taskId ? await Effect14.runPromise(store.getTask(input.taskId)) : void 0;
3332
- if (input.taskId && (!task || task.deletedAt || task.projectId !== input.projectId))
3397
+ if (input.taskId && sessionTaskUnavailable(task, input.projectId))
3333
3398
  return { error: "Task not found.", status: 404 };
3334
3399
  const session = SessionSchema.parse({
3335
3400
  id: randomUUID7(),
@@ -3344,18 +3409,9 @@ async function createAndStartSession(input) {
3344
3409
  createdAt: now,
3345
3410
  updatedAt: now
3346
3411
  });
3347
- const worktreePath = path9.join(worktreesRoot, session.id);
3348
- const branch = `agentrific/${session.id.slice(0, 8)}`;
3349
- const useProjectCheckout = input.gitFlow?.workspace === "checkout";
3350
- const workspacePath = useProjectCheckout ? projectPath : worktreePath;
3351
- if (useProjectCheckout) {
3352
- const serverPath = await realpath2(process.cwd());
3353
- if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path9.sep}`))
3354
- return {
3355
- error: "The server runs from this project. Choose a worktree for this task.",
3356
- status: 409
3357
- };
3358
- }
3412
+ const workspace = await resolveSessionWorkspace(session.id, input.gitFlow, projectPath);
3413
+ if (workspace.error !== void 0) return { error: workspace.error, status: workspace.status };
3414
+ const { branch, workspacePath } = workspace;
3359
3415
  const initial = { ...session, branch, workspacePath };
3360
3416
  publish({ type: "session.updated", session: initial });
3361
3417
  let running;
@@ -4026,17 +4082,7 @@ async function manageService(action, dataDir, cli) {
4026
4082
  );
4027
4083
  const domain = `gui/${process.getuid?.()}`;
4028
4084
  if (action === "install") {
4029
- await mkdir(directory, { recursive: true, mode: 448 });
4030
- await mkdir(path.join(dataDir, "logs"), { recursive: true, mode: 448 });
4031
- await writeFile(filename, serviceDefinition(platform, process.execPath, cli, dataDir), {
4032
- mode: 384,
4033
- flag: "wx"
4034
- });
4035
- if (platform === "darwin") await execute("launchctl", ["bootstrap", domain, filename]);
4036
- else {
4037
- await execute("systemctl", ["--user", "daemon-reload"]);
4038
- await execute("systemctl", ["--user", "enable", "--now", "agentrific.service"]);
4039
- }
4085
+ await installService(platform, directory, filename, domain, dataDir, cli);
4040
4086
  return;
4041
4087
  }
4042
4088
  if (action === "restart") {
@@ -4050,6 +4096,19 @@ async function manageService(action, dataDir, cli) {
4050
4096
  await unlink(filename);
4051
4097
  if (platform === "linux") await execute("systemctl", ["--user", "daemon-reload"]);
4052
4098
  }
4099
+ async function installService(platform, directory, filename, domain, dataDir, cli) {
4100
+ await mkdir(directory, { recursive: true, mode: 448 });
4101
+ await mkdir(path.join(dataDir, "logs"), { recursive: true, mode: 448 });
4102
+ await writeFile(filename, serviceDefinition(platform, process.execPath, cli, dataDir), {
4103
+ mode: 384,
4104
+ flag: "wx"
4105
+ });
4106
+ if (platform === "darwin") await execute("launchctl", ["bootstrap", domain, filename]);
4107
+ else {
4108
+ await execute("systemctl", ["--user", "daemon-reload"]);
4109
+ await execute("systemctl", ["--user", "enable", "--now", "agentrific.service"]);
4110
+ }
4111
+ }
4053
4112
 
4054
4113
  // src/cli.ts
4055
4114
  init_updates();
@@ -4060,9 +4119,11 @@ import { parseArgs } from "node:util";
4060
4119
  import { z as z16 } from "zod";
4061
4120
 
4062
4121
  // src/pairing.ts
4122
+ init_src();
4063
4123
  init_control();
4064
4124
  import { z as z4 } from "zod";
4065
4125
  import { styleText as styleText2 } from "node:util";
4126
+ import { emitKeypressEvents } from "node:readline";
4066
4127
  import qrcode from "qrcode-terminal";
4067
4128
 
4068
4129
  // src/cli-output.ts
@@ -4084,9 +4145,13 @@ function printHeading(title) {
4084
4145
  function printDetail(label2, value) {
4085
4146
  console.log(` ${styleText("dim", "\u203A")} ${styleText("dim", label2.padEnd(10))} ${value}`);
4086
4147
  }
4087
- function printShortcuts() {
4148
+ function printShortcuts(pairingAvailable = false) {
4088
4149
  console.log();
4089
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
+ );
4090
4155
  console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "?")} \u2502 show shortcuts`);
4091
4156
  console.log(
4092
4157
  ` ${styleText("dim", "\u203A")} Press ${styleText("bold", "Ctrl+C")} \u2502 stop the server
@@ -4122,6 +4187,7 @@ function printHelp() {
4122
4187
  ["--data-dir <path>", "Use a specific server data directory"],
4123
4188
  ["--public-url <https-url>", "Use your tunnel URL without prompting"],
4124
4189
  ["--no-interactive", "Disable prompts and keyboard shortcuts"],
4190
+ ["--show-payload", "Print manual JSON with the pairing QR code"],
4125
4191
  ["--timeout-ms <0-60000>", "Set the maintenance drain timeout"],
4126
4192
  ["--version", "Print the server version"],
4127
4193
  ["--help", "Show this help"]
@@ -4131,6 +4197,69 @@ function printHelp() {
4131
4197
  }
4132
4198
 
4133
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
+ }
4134
4263
  function pairingOrigin(url) {
4135
4264
  const endpoint = new URL(url);
4136
4265
  if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== "/")
@@ -4151,17 +4280,19 @@ async function printPairing(dataDir, url) {
4151
4280
  expiresAt: z4.number(),
4152
4281
  serverId: z4.string()
4153
4282
  }).parse(await controlRequest(dataDir, { command: "pair" }));
4154
- const payload = JSON.stringify({
4283
+ const invitation = {
4155
4284
  v: 1,
4156
4285
  url: origin,
4157
4286
  serverId: grant.serverId,
4158
4287
  pairingId: grant.pairingId
4159
- });
4288
+ };
4289
+ const payload = JSON.stringify(invitation);
4290
+ const link = createPairingLink(invitation);
4160
4291
  printHeading("pairing");
4161
- console.log(" 1. Open Agentrific on your phone.");
4162
- console.log(" 2. Choose Scan pairing QR code and scan below.");
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.");
4163
4294
  console.log(" 3. Enter the PIN, then confirm pairing.\n");
4164
- qrcode.generate(payload, { small: true }, (code) => {
4295
+ qrcode.generate(link, { small: true }, (code) => {
4165
4296
  console.log(
4166
4297
  code.split("\n").map((line) => ` ${line}`).join("\n")
4167
4298
  );
@@ -4171,16 +4302,13 @@ async function printPairing(dataDir, url) {
4171
4302
  printDetail("Server", styleText2("cyan", origin));
4172
4303
  printDetail("Expires", "In five minutes \xB7 one device per code");
4173
4304
  console.log("\n Pairing grants this device access to run agents as your host user.\n");
4174
- console.log(styleText2("dim", " Manual entry \xB7 paste this payload into the app:"));
4175
- console.log(`Pairing payload:
4176
- ${payload}`);
4177
- console.log();
4305
+ return { payload, expiresAt: grant.expiresAt };
4178
4306
  }
4179
4307
 
4180
4308
  // src/terminal.ts
4181
4309
  import { createInterface } from "node:readline/promises";
4182
4310
  import select from "@inquirer/select";
4183
- import { emitKeypressEvents } from "node:readline";
4311
+ import { emitKeypressEvents as emitKeypressEvents2 } from "node:readline";
4184
4312
  import { styleText as styleText3 } from "node:util";
4185
4313
 
4186
4314
  // src/tunnels.ts
@@ -4489,6 +4617,60 @@ async function promptPublicUrl(signal, saved) {
4489
4617
  prompt.close();
4490
4618
  }
4491
4619
  }
4620
+ async function selectConnectionMethod(options, signal, saved) {
4621
+ return select(
4622
+ {
4623
+ message: "Connection method",
4624
+ default: saved ? "existing" : void 0,
4625
+ loop: false,
4626
+ choices: [
4627
+ ...options.filter((option) => option.available).map((option) => ({
4628
+ value: option.provider,
4629
+ name: option.provider === "tailscale" ? "Tailscale" : "Cloudflared",
4630
+ description: option.reason
4631
+ })),
4632
+ {
4633
+ value: "existing",
4634
+ name: "Existing HTTPS address",
4635
+ description: saved ? `Paste an address, or keep ${saved}` : "Paste the public address from your tunnel provider."
4636
+ },
4637
+ {
4638
+ value: "local",
4639
+ name: "Local only",
4640
+ description: "Start without a public address."
4641
+ }
4642
+ ],
4643
+ theme: {
4644
+ prefix: " ",
4645
+ style: {
4646
+ message: (text) => styleText3("bold", text),
4647
+ highlight: (text) => styleText3("cyan", text),
4648
+ answer: (text) => styleText3("cyan", text),
4649
+ description: (text) => styleText3("dim", ` ${text}`),
4650
+ keysHelpTip: () => styleText3("dim", " \u2191/\u2193 to move \xB7 Enter to select")
4651
+ }
4652
+ }
4653
+ },
4654
+ { signal }
4655
+ );
4656
+ }
4657
+ async function startSelectedTunnel(options, choice, origin, signal) {
4658
+ const option = options.find((candidate) => candidate.provider === choice);
4659
+ if (!option?.available) throw new Error("The selected tunnel is unavailable.");
4660
+ const tunnel = new ManagedTunnel(option);
4661
+ try {
4662
+ console.log(styleText3("dim", `
4663
+ Starting ${option.provider}\u2026
4664
+ `));
4665
+ const url = await tunnel.start(origin, signal);
4666
+ return { url, tunnel, remember: false };
4667
+ } catch (error) {
4668
+ await tunnel.close();
4669
+ if (signal.aborted) throw error;
4670
+ printError(error instanceof Error ? error.message : String(error));
4671
+ return void 0;
4672
+ }
4673
+ }
4492
4674
  async function selectPublicConnection(origin, saved) {
4493
4675
  const controller = new AbortController();
4494
4676
  const cancel = () => controller.abort();
@@ -4504,60 +4686,14 @@ async function selectPublicConnection(origin, saved) {
4504
4686
  }
4505
4687
  console.log();
4506
4688
  while (true) {
4507
- const choice = await select(
4508
- {
4509
- message: "Connection method",
4510
- default: saved ? "existing" : void 0,
4511
- loop: false,
4512
- choices: [
4513
- ...options.filter((option2) => option2.available).map((option2) => ({
4514
- value: option2.provider,
4515
- name: option2.provider === "tailscale" ? "Tailscale" : "Cloudflared",
4516
- description: option2.reason
4517
- })),
4518
- {
4519
- value: "existing",
4520
- name: "Existing HTTPS address",
4521
- description: saved ? `Paste an address, or keep ${saved}` : "Paste the public address from your tunnel provider."
4522
- },
4523
- {
4524
- value: "local",
4525
- name: "Local only",
4526
- description: "Start without a public address."
4527
- }
4528
- ],
4529
- theme: {
4530
- prefix: " ",
4531
- style: {
4532
- message: (text) => styleText3("bold", text),
4533
- highlight: (text) => styleText3("cyan", text),
4534
- answer: (text) => styleText3("cyan", text),
4535
- description: (text) => styleText3("dim", ` ${text}`),
4536
- keysHelpTip: () => styleText3("dim", " \u2191/\u2193 to move \xB7 Enter to select")
4537
- }
4538
- }
4539
- },
4540
- { signal: controller.signal }
4541
- );
4689
+ const choice = await selectConnectionMethod(options, controller.signal, saved);
4542
4690
  if (choice === "local") return { remember: false };
4543
4691
  if (choice === "existing") {
4544
4692
  console.log();
4545
4693
  return { url: await promptPublicUrl(controller.signal, saved), remember: true };
4546
4694
  }
4547
- const option = options.find((candidate) => candidate.provider === choice);
4548
- if (!option?.available) throw new Error("The selected tunnel is unavailable.");
4549
- const tunnel = new ManagedTunnel(option);
4550
- try {
4551
- console.log(styleText3("dim", `
4552
- Starting ${option.provider}\u2026
4553
- `));
4554
- const url = await tunnel.start(origin, controller.signal);
4555
- return { url, tunnel, remember: false };
4556
- } catch (error) {
4557
- await tunnel.close();
4558
- if (controller.signal.aborted) throw error;
4559
- printError(error instanceof Error ? error.message : String(error));
4560
- }
4695
+ const connection = await startSelectedTunnel(options, choice, origin, controller.signal);
4696
+ if (connection) return connection;
4561
4697
  }
4562
4698
  } finally {
4563
4699
  process.removeListener("SIGINT", cancel);
@@ -4569,6 +4705,7 @@ function startTerminal(dataDir, publicUrl, tunnel) {
4569
4705
  const previousRaw = input.isRaw;
4570
4706
  let busy = false;
4571
4707
  let closed = false;
4708
+ let invitation;
4572
4709
  const close = () => {
4573
4710
  if (closed) return;
4574
4711
  closed = true;
@@ -4590,10 +4727,15 @@ function startTerminal(dataDir, publicUrl, tunnel) {
4590
4727
  return;
4591
4728
  }
4592
4729
  if (text === "?" && !busy) {
4593
- printShortcuts();
4730
+ printShortcuts(Boolean(invitation));
4731
+ return;
4732
+ }
4733
+ if (busy || key.ctrl || key.meta) return;
4734
+ if (text?.toLowerCase() === "j" && invitation) {
4735
+ printManualPairing(invitation);
4594
4736
  return;
4595
4737
  }
4596
- if (text?.toLowerCase() !== "p" || busy || key.ctrl || key.meta) return;
4738
+ if (text?.toLowerCase() !== "p") return;
4597
4739
  if (tunnel && !tunnel.running()) {
4598
4740
  printError(
4599
4741
  "The managed tunnel is no longer running. Restart the server to reconnect your phone."
@@ -4601,12 +4743,15 @@ function startTerminal(dataDir, publicUrl, tunnel) {
4601
4743
  return;
4602
4744
  }
4603
4745
  busy = true;
4604
- void printPairing(dataDir, publicUrl).catch((error) => printError(error.message)).finally(() => {
4746
+ invitation = void 0;
4747
+ void printPairing(dataDir, publicUrl).then((result) => {
4748
+ invitation = result;
4749
+ }).catch((error) => printError(error.message)).finally(() => {
4605
4750
  busy = false;
4606
- if (!closed) printShortcuts();
4751
+ if (!closed) printShortcuts(Boolean(invitation));
4607
4752
  });
4608
4753
  }
4609
- emitKeypressEvents(input);
4754
+ emitKeypressEvents2(input);
4610
4755
  input.setRawMode(true);
4611
4756
  input.on("keypress", pressed);
4612
4757
  input.once("end", close);
@@ -4640,8 +4785,8 @@ var ConfigSchema = z16.object({
4640
4785
  projectsDir: z16.string(),
4641
4786
  publicUrl: z16.string().url().optional()
4642
4787
  });
4643
- async function main() {
4644
- const { values, positionals } = parseArgs({
4788
+ function parseCommandArgs() {
4789
+ return parseArgs({
4645
4790
  allowPositionals: true,
4646
4791
  options: {
4647
4792
  "data-dir": { type: "string" },
@@ -4650,9 +4795,13 @@ async function main() {
4650
4795
  help: { type: "boolean" },
4651
4796
  check: { type: "boolean" },
4652
4797
  "timeout-ms": { type: "string" },
4653
- "no-interactive": { type: "boolean" }
4798
+ "no-interactive": { type: "boolean" },
4799
+ "show-payload": { type: "boolean" }
4654
4800
  }
4655
4801
  });
4802
+ }
4803
+ async function main() {
4804
+ const { values, positionals } = parseCommandArgs();
4656
4805
  if (values.version) {
4657
4806
  console.log(serverVersion);
4658
4807
  return;
@@ -4674,111 +4823,132 @@ async function main() {
4674
4823
  })
4675
4824
  )
4676
4825
  );
4677
- if (command === "init") {
4678
- await mkdir5(dataDir, { recursive: true, mode: 448 });
4679
- const publicUrl = values["public-url"] ?? config.publicUrl;
4680
- if (publicUrl && new URL(publicUrl).protocol !== "https:")
4681
- throw new Error("Configure an HTTPS URL from Tailscale Serve or Cloudflare Tunnel.");
4682
- await writeFile4(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
4683
- mode: 384
4684
- });
4685
- console.log(`Configuration saved to ${filename}. Start with agentrific start.`);
4686
- return;
4826
+ const context = { values, positionals, dataDir, filename, config, command };
4827
+ if (command === "init") return initCommand(context);
4828
+ if (command === "start") return startCommand(context);
4829
+ if (command === "service") return serviceCommand(context);
4830
+ if (command === "logs") return logsCommand(context);
4831
+ if (command === "update") return updateCommand(context);
4832
+ if (command === "pair") return pairCommand(context);
4833
+ if (command === "maintenance") return maintenanceCommand(context);
4834
+ if (command === "devices" || command === "status") return devicesCommand(context);
4835
+ if (command === "revoke") return revokeCommand(context);
4836
+ throw new Error("Unknown command. Run agentrific --help.");
4837
+ }
4838
+ async function initCommand({ values, dataDir, filename, config }) {
4839
+ await mkdir5(dataDir, { recursive: true, mode: 448 });
4840
+ const publicUrl = values["public-url"] ?? config.publicUrl;
4841
+ if (publicUrl && new URL(publicUrl).protocol !== "https:")
4842
+ throw new Error("Configure an HTTPS URL from Tailscale Serve or Cloudflare Tunnel.");
4843
+ await writeFile4(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
4844
+ mode: 384
4845
+ });
4846
+ console.log(`Configuration saved to ${filename}. Start with agentrific start.`);
4847
+ return;
4848
+ }
4849
+ async function startCommand({ values, dataDir, filename, config }) {
4850
+ process.env.AGENTRIFIC_DATA_DIR = dataDir;
4851
+ process.env.AGENTRIFIC_PROJECTS_DIR ??= config.projectsDir;
4852
+ const interactive = Boolean(
4853
+ process.stdin.isTTY && process.stdout.isTTY && !values["no-interactive"]
4854
+ );
4855
+ const explicitUrl = values["public-url"] ?? process.env.AGENTRIFIC_PUBLIC_URL;
4856
+ if (interactive) {
4857
+ process.env.AGENTRIFIC_INTERACTIVE = "1";
4858
+ printHeading("server");
4687
4859
  }
4688
- if (command === "start") {
4689
- process.env.AGENTRIFIC_DATA_DIR = dataDir;
4690
- process.env.AGENTRIFIC_PROJECTS_DIR ??= config.projectsDir;
4691
- const interactive = Boolean(
4692
- process.stdin.isTTY && process.stdout.isTTY && !values["no-interactive"]
4693
- );
4694
- const explicitUrl = values["public-url"] ?? process.env.AGENTRIFIC_PUBLIC_URL;
4695
- if (interactive) {
4696
- process.env.AGENTRIFIC_INTERACTIVE = "1";
4697
- printHeading("server");
4698
- }
4699
- const connection = interactive && !explicitUrl ? await selectPublicConnection(
4700
- `http://${process.env.AGENTRIFIC_HOST ?? "127.0.0.1"}:${process.env.AGENTRIFIC_PORT ?? "4318"}`,
4701
- config.publicUrl
4702
- ) : {
4703
- url: explicitUrl ? pairingOrigin(explicitUrl) : config.publicUrl,
4704
- remember: false,
4705
- tunnel: void 0
4706
- };
4707
- const publicUrl = connection.url;
4708
- try {
4709
- if (connection.remember && publicUrl) {
4710
- await mkdir5(dataDir, { recursive: true, mode: 448 });
4711
- await writeFile4(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
4712
- mode: 384
4713
- });
4714
- }
4715
- if (publicUrl) process.env.AGENTRIFIC_PUBLIC_URL = pairingOrigin(publicUrl);
4716
- await init_index().then(() => index_exports);
4717
- if (interactive) startTerminal(dataDir, publicUrl, connection.tunnel);
4718
- } catch (error) {
4719
- await connection.tunnel?.close();
4720
- throw error;
4860
+ const connection = interactive && !explicitUrl ? await selectPublicConnection(
4861
+ `http://${process.env.AGENTRIFIC_HOST ?? "127.0.0.1"}:${process.env.AGENTRIFIC_PORT ?? "4318"}`,
4862
+ config.publicUrl
4863
+ ) : {
4864
+ url: explicitUrl ? pairingOrigin(explicitUrl) : config.publicUrl,
4865
+ remember: false,
4866
+ tunnel: void 0
4867
+ };
4868
+ const publicUrl = connection.url;
4869
+ try {
4870
+ if (connection.remember && publicUrl) {
4871
+ await mkdir5(dataDir, { recursive: true, mode: 448 });
4872
+ await writeFile4(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
4873
+ mode: 384
4874
+ });
4721
4875
  }
4722
- return;
4723
- }
4724
- if (command === "service") {
4725
- const cli = fileURLToPath(import.meta.url);
4726
- if (!cli.endsWith(".js"))
4727
- throw new Error(
4728
- "Build and install the compiled server package before registering a service."
4729
- );
4730
- await manageService(positionals[1] ?? "", dataDir, cli);
4731
- console.log(`Service ${positionals[1]} completed.`);
4732
- return;
4733
- }
4734
- if (command === "logs") {
4735
- const child = process.platform === "linux" ? spawn2("journalctl", ["--user", "-u", "agentrific.service", "-f"], { stdio: "inherit" }) : spawn2("tail", ["-F", path10.join(dataDir, "logs", "server.log")], { stdio: "inherit" });
4736
- child.on("error", (error) => {
4737
- console.error(error.message);
4738
- process.exitCode = 1;
4739
- });
4740
- child.on("exit", (code) => {
4741
- process.exitCode = code ?? 0;
4742
- });
4743
- return;
4744
- }
4745
- if (command === "update") {
4746
- if (!values.check)
4747
- throw new Error(
4748
- "Use update --check. Automatic installation is unavailable; stop the server and install a reviewed package before restarting."
4749
- );
4750
- console.log(JSON.stringify(await checkForUpdates(), null, 2));
4751
- return;
4876
+ if (publicUrl) process.env.AGENTRIFIC_PUBLIC_URL = pairingOrigin(publicUrl);
4877
+ await init_index().then(() => index_exports);
4878
+ if (interactive) startTerminal(dataDir, publicUrl, connection.tunnel);
4879
+ } catch (error) {
4880
+ await connection.tunnel?.close();
4881
+ throw error;
4752
4882
  }
4753
- if (command === "pair") {
4754
- const status = await controlRequest(dataDir, { command: "status" });
4755
- await printPairing(
4756
- dataDir,
4757
- values["public-url"] ?? process.env.AGENTRIFIC_PUBLIC_URL ?? ("publicUrl" in status ? status.publicUrl : void 0) ?? config.publicUrl
4883
+ return;
4884
+ }
4885
+ async function serviceCommand({ positionals, dataDir }) {
4886
+ const cli = fileURLToPath(import.meta.url);
4887
+ if (!cli.endsWith(".js"))
4888
+ throw new Error("Build and install the compiled server package before registering a service.");
4889
+ await manageService(positionals[1] ?? "", dataDir, cli);
4890
+ console.log(`Service ${positionals[1]} completed.`);
4891
+ return;
4892
+ }
4893
+ async function logsCommand({ dataDir }) {
4894
+ const child = process.platform === "linux" ? spawn2("journalctl", ["--user", "-u", "agentrific.service", "-f"], { stdio: "inherit" }) : spawn2("tail", ["-F", path10.join(dataDir, "logs", "server.log")], { stdio: "inherit" });
4895
+ child.on("error", (error) => {
4896
+ console.error(error.message);
4897
+ process.exitCode = 1;
4898
+ });
4899
+ child.on("exit", (code) => {
4900
+ process.exitCode = code ?? 0;
4901
+ });
4902
+ return;
4903
+ }
4904
+ async function updateCommand({ values }) {
4905
+ if (!values.check)
4906
+ throw new Error(
4907
+ "Use update --check. Automatic installation is unavailable; stop the server and install a reviewed package before restarting."
4758
4908
  );
4759
- return;
4760
- }
4761
- if (command === "maintenance") {
4762
- const action = z16.enum(["enter", "resume", "drain"]).parse(positionals[1]);
4763
- const result = await controlRequest(dataDir, {
4764
- command,
4765
- action,
4766
- timeoutMs: z16.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
4767
- });
4768
- console.log(JSON.stringify(result, null, 2));
4769
- if (action === "drain" && "drained" in result && !result.drained) process.exitCode = 1;
4770
- return;
4771
- }
4772
- if (command === "devices" || command === "status") {
4773
- console.log(JSON.stringify(await controlRequest(dataDir, { command }), null, 2));
4774
- return;
4775
- }
4776
- if (command === "revoke") {
4777
- const id = z16.string().uuid().parse(positionals[1]);
4778
- console.log(JSON.stringify(await controlRequest(dataDir, { command, id })));
4779
- return;
4909
+ console.log(JSON.stringify(await checkForUpdates(), null, 2));
4910
+ return;
4911
+ }
4912
+ async function pairCommand({ values, dataDir, config }) {
4913
+ const status = await controlRequest(dataDir, { command: "status" });
4914
+ const invitation = await printPairing(
4915
+ dataDir,
4916
+ values["public-url"] ?? process.env.AGENTRIFIC_PUBLIC_URL ?? ("publicUrl" in status ? status.publicUrl : void 0) ?? config.publicUrl
4917
+ );
4918
+ if (values["show-payload"]) {
4919
+ printManualPairing(invitation);
4920
+ } else if (process.stdin.isTTY && process.stdout.isTTY && !values["no-interactive"]) {
4921
+ await waitForManualPairing(invitation);
4922
+ } else {
4923
+ console.log(" For manual entry, run agentrific pair --show-payload.\n");
4780
4924
  }
4781
- throw new Error("Unknown command. Run agentrific --help.");
4925
+ return;
4926
+ }
4927
+ async function maintenanceCommand({ values, positionals, dataDir }) {
4928
+ const action = z16.enum(["enter", "resume", "drain"]).parse(positionals[1]);
4929
+ const result = await controlRequest(dataDir, {
4930
+ command: "maintenance",
4931
+ action,
4932
+ timeoutMs: z16.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
4933
+ });
4934
+ console.log(JSON.stringify(result, null, 2));
4935
+ if (action === "drain" && "drained" in result && !result.drained) process.exitCode = 1;
4936
+ return;
4937
+ }
4938
+ async function devicesCommand({ dataDir, command }) {
4939
+ console.log(
4940
+ JSON.stringify(
4941
+ await controlRequest(dataDir, { command: z16.enum(["devices", "status"]).parse(command) }),
4942
+ null,
4943
+ 2
4944
+ )
4945
+ );
4946
+ return;
4947
+ }
4948
+ async function revokeCommand({ positionals, dataDir }) {
4949
+ const id = z16.string().uuid().parse(positionals[1]);
4950
+ console.log(JSON.stringify(await controlRequest(dataDir, { command: "revoke", id })));
4951
+ return;
4782
4952
  }
4783
4953
  void main().catch((error) => {
4784
4954
  if (["ExitPromptError", "AbortPromptError", "AbortError"].includes(error.name)) {