@agentrific/server 0.1.2 → 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,7 +30,7 @@ var init_package = __esm({
30
30
  "package.json"() {
31
31
  package_default = {
32
32
  name: "@agentrific/server",
33
- version: "0.1.2",
33
+ version: "0.1.4",
34
34
  description: "Self-hosted control plane for long-running coding agents",
35
35
  keywords: [
36
36
  "agentrific",
@@ -204,6 +204,9 @@ var init_updates = __esm({
204
204
 
205
205
  // ../../packages/contracts/src/index.ts
206
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
+ }
207
210
  function cronFields(expression) {
208
211
  const fields = expression.trim().split(/\s+/);
209
212
  if (fields.length !== 5)
@@ -216,7 +219,7 @@ function cronFields(expression) {
216
219
  if (!match) throw new Error("Use numeric cron fields, *, ranges, lists and steps.");
217
220
  const base = match[1];
218
221
  const step = Number(match[2] ?? 1);
219
- 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]));
220
223
  if (!Number.isInteger(step) || step < 1 || step > max - min + 1 || start < min || end > max || start > end)
221
224
  throw new Error("Cron field is outside its allowed range.");
222
225
  for (let value = start; value <= end; value += step) values.add(value);
@@ -237,6 +240,11 @@ function cronMatches(expression, date) {
237
240
  const day = fields[2].wildcard || fields[4].wildcard ? matches[2] && matches[4] : matches[2] || matches[4];
238
241
  return Boolean(matches[0] && matches[1] && matches[3] && day);
239
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
+ }
240
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;
241
249
  var init_src = __esm({
242
250
  "../../packages/contracts/src/index.ts"() {
@@ -468,6 +476,7 @@ var init_src = __esm({
468
476
  gitFlow: GitFlowSchema.optional(),
469
477
  status: TaskStatusSchema,
470
478
  sessionId: z2.string().nullable(),
479
+ pullRequestUrl: z2.string().url().optional(),
471
480
  createdAt: z2.string().datetime(),
472
481
  updatedAt: z2.string().datetime(),
473
482
  deletedAt: z2.string().datetime().optional()
@@ -485,7 +494,7 @@ var init_src = __esm({
485
494
  prompt: true,
486
495
  attachmentIds: true,
487
496
  gitFlow: true
488
- }).strict();
497
+ }).extend({ gitFlow: GitFlowSchema.nullable().optional() }).strict();
489
498
  PROTOCOL_VERSION = 1;
490
499
  ServerUpdateStatusSchema = z2.object({
491
500
  currentVersion: z2.string(),
@@ -1235,6 +1244,9 @@ function taskStatusForSession(status, session) {
1235
1244
  return "needs_user_input";
1236
1245
  }
1237
1246
  }
1247
+ function pullRequestUrl(session) {
1248
+ return session.gitDelivery?.status === "pr_opened" ? session.gitDelivery.url : void 0;
1249
+ }
1238
1250
  function sessionTask(session) {
1239
1251
  return TaskSchema.parse({
1240
1252
  id: `session-${session.id}`,
@@ -1245,6 +1257,7 @@ function sessionTask(session) {
1245
1257
  gitFlow: session.gitFlow,
1246
1258
  status: taskStatusForSession(session.status, session),
1247
1259
  sessionId: session.id,
1260
+ pullRequestUrl: pullRequestUrl(session),
1248
1261
  createdAt: session.createdAt,
1249
1262
  updatedAt: session.updatedAt
1250
1263
  });
@@ -1256,12 +1269,13 @@ async function boardTasks(store2, projectId) {
1256
1269
  tasks.flatMap((task) => task.sessionId ? [task.sessionId] : [])
1257
1270
  );
1258
1271
  const linkedTasks = tasks.filter((task) => !task.deletedAt).map((task) => {
1259
- const session = sessions.find((item) => item.taskId === task.id) ?? sessions.find((item) => item.id === task.sessionId);
1260
- 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 };
1261
1274
  linkedSessionIds.add(session.id);
1262
1275
  return {
1263
1276
  ...task,
1264
1277
  sessionId: session.id,
1278
+ pullRequestUrl: pullRequestUrl(session),
1265
1279
  status: taskStatusForSession(session.status, session)
1266
1280
  };
1267
1281
  });
@@ -1279,7 +1293,9 @@ async function findTask(store2, id) {
1279
1293
  function editTask(store2, id, input, resolveAttachments) {
1280
1294
  return withTaskLock(id, async () => {
1281
1295
  const task = await findTask(store2, id);
1282
- 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;
1283
1299
  let files = task.attachments;
1284
1300
  if (attachmentIds) {
1285
1301
  if (!resolveAttachments)
@@ -1290,6 +1306,7 @@ function editTask(store2, id, input, resolveAttachments) {
1290
1306
  store2.saveTask({
1291
1307
  ...task,
1292
1308
  ...updates2,
1309
+ ...configuration,
1293
1310
  attachments: files,
1294
1311
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1295
1312
  })
@@ -1566,7 +1583,7 @@ var init_runtime = __esm({
1566
1583
  Effect7.gen({ self: this }, function* () {
1567
1584
  if (this.closed)
1568
1585
  return yield* Effect7.fail(new AgentError({ message: "Server is shutting down" }));
1569
- if (!session.branch)
1586
+ if (!session.branch && (session.gitFlow || !session.workspacePath))
1570
1587
  return yield* Effect7.fail(new AgentError({ message: "Session checkout is not ready" }));
1571
1588
  if (this.hasMessage(session.id, messageId)) return;
1572
1589
  const entry = yield* this.open(session);
@@ -1894,45 +1911,7 @@ ${error ? workflowError(error instanceof Error ? error : String(error)) : messag
1894
1911
  if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
1895
1912
  throw new Error("Restore the assigned run branch before requesting delivery.");
1896
1913
  if (dirty) {
1897
- await this.git(session, ["add", "--all"], signal);
1898
- await this.stage(job, "drafting");
1899
- const diff = await this.git(session, ["diff", "--cached", "HEAD", "--"], signal);
1900
- const files = await this.git(
1901
- session,
1902
- ["ls-files", "--others", "--exclude-standard"],
1903
- signal
1904
- );
1905
- if (this.options.draft) {
1906
- try {
1907
- const draft = await this.options.draft(
1908
- session,
1909
- input,
1910
- `${diff}
1911
- New files:
1912
- ${files}`.slice(0, 4e4),
1913
- signal
1914
- );
1915
- job.title = draft.title;
1916
- job.body = draft.body;
1917
- } catch (error) {
1918
- job.logs = workflowLog(
1919
- `${job.logs}
1920
- Message helper unavailable; using task summary. ${workflowError(error instanceof Error ? error : String(error))}`
1921
- );
1922
- }
1923
- }
1924
- await this.stage(job, "committing");
1925
- if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
1926
- throw new Error("The workspace changed branches while preparing delivery.");
1927
- const output = await this.git(
1928
- session,
1929
- ["commit", "-m", `${job.title}
1930
-
1931
- Agentrific run ${id}.`],
1932
- signal
1933
- );
1934
- job.logs = workflowLog(`${job.logs}
1935
- ${output}`);
1914
+ await this.commitChanges(session, job, input, signal);
1936
1915
  } else {
1937
1916
  await this.git(session, ["hook", "run", "--ignore-missing", "pre-commit"], signal);
1938
1917
  }
@@ -1959,6 +1938,44 @@ ${workflowError(error instanceof Error ? error : String(error))}`
1959
1938
  }
1960
1939
  });
1961
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
+ }
1962
1979
  fallbackBody(id, input) {
1963
1980
  return `${input.summary}
1964
1981
 
@@ -2031,6 +2048,31 @@ Agentrific run ${id}.`;
2031
2048
  );
2032
2049
  }
2033
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
+ }
2034
2076
  async integrate(id) {
2035
2077
  const job = await this.job(id);
2036
2078
  if (!job?.commitSha) return;
@@ -2044,29 +2086,7 @@ Agentrific run ${id}.`;
2044
2086
  "The workspace changed after requesting delivery. Call deliver_changes again for the new changes."
2045
2087
  );
2046
2088
  if (session.gitFlow.delivery === "merge") {
2047
- await this.stage(job, "integrating");
2048
- if (session.gitFlow.workspace === "checkout" && await this.command(projectPath, "git", ["branch", "--show-current"]) === session.branch)
2049
- await this.command(projectPath, "git", ["switch", "main"]);
2050
- if (await this.command(projectPath, "git", ["branch", "--show-current"]) !== "main")
2051
- throw new Error("Switch the project checkout to main, then retry delivery.");
2052
- if (await this.command(projectPath, "git", ["status", "--porcelain"]))
2053
- throw new Error(
2054
- "Waiting for local changes in the main checkout. Commit or move them, then retry delivery."
2055
- );
2056
- const included = await this.command(projectPath, "git", ["merge-base", "main", commit]) === commit;
2057
- if (!included) {
2058
- try {
2059
- await this.command(projectPath, "git", ["merge", "--no-edit", commit]);
2060
- } catch (error) {
2061
- await this.command(projectPath, "git", ["merge", "--abort"]).catch(() => void 0);
2062
- throw new Error(
2063
- `Integration failed. Resolve conflicts against main in the run workspace, then call deliver_changes again.
2064
- ${workflowError(error instanceof Error ? error : String(error))}`
2065
- );
2066
- }
2067
- }
2068
- job.result = "merged";
2069
- job.message = "Committed changes are on main.";
2089
+ await this.mergeChanges(session, job, projectPath, commit);
2070
2090
  } else {
2071
2091
  await this.stage(job, "pushing");
2072
2092
  await this.git(session, ["push", "origin", `${commit}:refs/heads/${session.branch}`]);
@@ -2153,6 +2173,7 @@ var init_preparation = __esm({
2153
2173
  return [...this.preparing.values()].map((id) => `preparation:${id}`);
2154
2174
  }
2155
2175
  async prepare(session, projectPath, input) {
2176
+ if (!session.gitFlow) return { ...session, branch: null, workspacePath: projectPath };
2156
2177
  const preparation2 = Symbol(session.id);
2157
2178
  this.preparing.set(preparation2, session.id);
2158
2179
  return withTaskLock(`prepare:${session.id}`, async () => {
@@ -2185,56 +2206,7 @@ ${logs}` : ""}`);
2185
2206
  try {
2186
2207
  await record("creating", "Creating the run workspace\u2026");
2187
2208
  const workspace = session.workspacePath;
2188
- const checkout = session.gitFlow?.workspace === "checkout";
2189
- const branchAtWorkspace = await command(workspace, "git", [
2190
- "branch",
2191
- "--show-current"
2192
- ]).catch(() => void 0);
2193
- if (branchAtWorkspace !== session.branch) {
2194
- if (checkout) {
2195
- if (branchAtWorkspace !== "main" || await command(projectPath, "git", ["status", "--porcelain"]))
2196
- throw new Error(
2197
- "The project checkout must be clean and on main before starting a checkout run."
2198
- );
2199
- await command(
2200
- projectPath,
2201
- "git",
2202
- ["switch", "-c", session.branch, "main"],
2203
- void 0,
2204
- true
2205
- );
2206
- } else {
2207
- if (branchAtWorkspace !== void 0)
2208
- throw new Error("The run workspace already exists on a different branch.");
2209
- await mkdir4(path6.dirname(workspace), { recursive: true });
2210
- try {
2211
- await command(
2212
- projectPath,
2213
- "git",
2214
- [
2215
- "worktree",
2216
- "add",
2217
- "-b",
2218
- session.branch,
2219
- workspace,
2220
- session.gitFlow ? "main" : "HEAD"
2221
- ],
2222
- void 0,
2223
- true
2224
- );
2225
- } catch (error) {
2226
- if (await command(workspace, "git", ["branch", "--show-current"]).catch(
2227
- () => void 0
2228
- ) !== session.branch)
2229
- throw error;
2230
- await record(
2231
- "creating",
2232
- "Workspace created; a checkout hook failed. Preparing dependencies\u2026",
2233
- workflowError(error instanceof Error ? error : String(error))
2234
- );
2235
- }
2236
- }
2237
- }
2209
+ await this.createWorkspace(session, projectPath, command, record);
2238
2210
  await record("installing", "Installing run dependencies\u2026");
2239
2211
  await this.install(workspace, command);
2240
2212
  await record("ready", "Workspace ready.");
@@ -2250,6 +2222,57 @@ ${logs}` : ""}`);
2250
2222
  }
2251
2223
  }).finally(() => this.preparing.delete(preparation2));
2252
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
+ }
2253
2276
  async install(workspace, command) {
2254
2277
  const packageFile = await readFile3(path6.join(workspace, "package.json"), "utf8").catch(
2255
2278
  () => void 0
@@ -3010,31 +3033,7 @@ var init_store = __esm({
3010
3033
  this.db.prepare("DELETE FROM projects WHERE id = ?").run(replacedId);
3011
3034
  }
3012
3035
  if (previousPath && previousPath !== project.path) {
3013
- this.db.prepare(
3014
- "UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
3015
- ).run(project.path, previousPath);
3016
- const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path8.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
3017
- for (const row of this.db.prepare(
3018
- "SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?"
3019
- ).all(project.id)) {
3020
- const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
3021
- if (session.workspacePath) {
3022
- session.workspacePath = String(relocate(session.workspacePath));
3023
- this.db.prepare("UPDATE sessions SET payload = ? WHERE id = ?").run(JSON.stringify(session), session.id);
3024
- }
3025
- }
3026
- for (const row of this.db.prepare(
3027
- "SELECT sequence, payload FROM events WHERE json_extract(payload, '$.session.projectId') = ?"
3028
- ).all(project.id)) {
3029
- const event = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload).session);
3030
- if (event.workspacePath) {
3031
- event.workspacePath = String(relocate(event.workspacePath));
3032
- this.db.prepare("UPDATE events SET payload = ? WHERE sequence = ?").run(
3033
- JSON.stringify({ type: "session.updated", session: event }),
3034
- Number(row.sequence)
3035
- );
3036
- }
3037
- }
3036
+ this.relocateProjectHistory(project, previousPath);
3038
3037
  }
3039
3038
  this.db.prepare(
3040
3039
  "INSERT INTO projects(id, payload) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET payload = excluded.payload"
@@ -3047,6 +3046,28 @@ var init_store = __esm({
3047
3046
  }
3048
3047
  });
3049
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
+ }
3050
3071
  defaultModel() {
3051
3072
  return this.query(() => {
3052
3073
  const row = this.db.prepare("SELECT payload FROM settings WHERE id = 'default-model'").get();
@@ -3298,16 +3319,27 @@ async function githubCliStatus() {
3298
3319
  return { githubCli: { installed: true, authenticated: false } };
3299
3320
  }
3300
3321
  }
3301
- async function createAndStartSession(input) {
3302
- maintenance.assertReady();
3303
- if (input.gitFlow?.delivery === "pull_request") {
3304
- const github = await githubCliStatus();
3305
- if (!github.githubCli.authenticated)
3306
- 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
+ };
3307
3337
  }
3338
+ }
3339
+ async function resolveSessionProject(projectId) {
3308
3340
  const projectsRoot = await Effect14.runPromise(store.projectsRoot(defaultProjectsRoot));
3309
3341
  const project = (await Effect14.runPromise(discoverProjects(projectsRoot, store))).find(
3310
- (item) => item.id === input.projectId && item.available
3342
+ (item) => item.id === projectId && item.available
3311
3343
  );
3312
3344
  if (!project)
3313
3345
  return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
@@ -3315,6 +3347,37 @@ async function createAndStartSession(input) {
3315
3347
  const resolvedRoot = await realpath2(projectsRoot);
3316
3348
  if (path9.dirname(projectPath) !== resolvedRoot)
3317
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;
3318
3381
  let files;
3319
3382
  try {
3320
3383
  files = await Effect14.runPromise(
@@ -3323,25 +3386,15 @@ async function createAndStartSession(input) {
3323
3386
  } catch (error) {
3324
3387
  return { error: String(error), status: 400 };
3325
3388
  }
3326
- let selectedModel;
3327
- try {
3328
- const reference = input.model ?? await models.defaultReference();
3329
- const selected = await models.resolve(reference);
3330
- if (files.some((file) => file.kind === "image") && !selected.input.includes("image"))
3331
- return {
3332
- error: "This model cannot read images. Choose a vision model in Settings.",
3333
- status: 400
3334
- };
3335
- selectedModel = models.identity(selected.provider, selected.id);
3336
- } catch {
3337
- return {
3338
- error: "The default model is unavailable. Choose an available model in Settings.",
3339
- status: 400
3340
- };
3341
- }
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;
3342
3395
  const now = (/* @__PURE__ */ new Date()).toISOString();
3343
3396
  const task = input.taskId ? await Effect14.runPromise(store.getTask(input.taskId)) : void 0;
3344
- if (input.taskId && (!task || task.deletedAt || task.projectId !== input.projectId))
3397
+ if (input.taskId && sessionTaskUnavailable(task, input.projectId))
3345
3398
  return { error: "Task not found.", status: 404 };
3346
3399
  const session = SessionSchema.parse({
3347
3400
  id: randomUUID7(),
@@ -3356,18 +3409,9 @@ async function createAndStartSession(input) {
3356
3409
  createdAt: now,
3357
3410
  updatedAt: now
3358
3411
  });
3359
- const worktreePath = path9.join(worktreesRoot, session.id);
3360
- const branch = `agentrific/${session.id.slice(0, 8)}`;
3361
- const useProjectCheckout = input.gitFlow?.workspace === "checkout";
3362
- const workspacePath = useProjectCheckout ? projectPath : worktreePath;
3363
- if (useProjectCheckout) {
3364
- const serverPath = await realpath2(process.cwd());
3365
- if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path9.sep}`))
3366
- return {
3367
- error: "The server runs from this project. Choose a worktree for this task.",
3368
- status: 409
3369
- };
3370
- }
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;
3371
3415
  const initial = { ...session, branch, workspacePath };
3372
3416
  publish({ type: "session.updated", session: initial });
3373
3417
  let running;
@@ -4038,17 +4082,7 @@ async function manageService(action, dataDir, cli) {
4038
4082
  );
4039
4083
  const domain = `gui/${process.getuid?.()}`;
4040
4084
  if (action === "install") {
4041
- await mkdir(directory, { recursive: true, mode: 448 });
4042
- await mkdir(path.join(dataDir, "logs"), { recursive: true, mode: 448 });
4043
- await writeFile(filename, serviceDefinition(platform, process.execPath, cli, dataDir), {
4044
- mode: 384,
4045
- flag: "wx"
4046
- });
4047
- if (platform === "darwin") await execute("launchctl", ["bootstrap", domain, filename]);
4048
- else {
4049
- await execute("systemctl", ["--user", "daemon-reload"]);
4050
- await execute("systemctl", ["--user", "enable", "--now", "agentrific.service"]);
4051
- }
4085
+ await installService(platform, directory, filename, domain, dataDir, cli);
4052
4086
  return;
4053
4087
  }
4054
4088
  if (action === "restart") {
@@ -4062,6 +4096,19 @@ async function manageService(action, dataDir, cli) {
4062
4096
  await unlink(filename);
4063
4097
  if (platform === "linux") await execute("systemctl", ["--user", "daemon-reload"]);
4064
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
+ }
4065
4112
 
4066
4113
  // src/cli.ts
4067
4114
  init_updates();
@@ -4072,9 +4119,11 @@ import { parseArgs } from "node:util";
4072
4119
  import { z as z16 } from "zod";
4073
4120
 
4074
4121
  // src/pairing.ts
4122
+ init_src();
4075
4123
  init_control();
4076
4124
  import { z as z4 } from "zod";
4077
4125
  import { styleText as styleText2 } from "node:util";
4126
+ import { emitKeypressEvents } from "node:readline";
4078
4127
  import qrcode from "qrcode-terminal";
4079
4128
 
4080
4129
  // src/cli-output.ts
@@ -4096,9 +4145,13 @@ function printHeading(title) {
4096
4145
  function printDetail(label2, value) {
4097
4146
  console.log(` ${styleText("dim", "\u203A")} ${styleText("dim", label2.padEnd(10))} ${value}`);
4098
4147
  }
4099
- function printShortcuts() {
4148
+ function printShortcuts(pairingAvailable = false) {
4100
4149
  console.log();
4101
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
+ );
4102
4155
  console.log(` ${styleText("dim", "\u203A")} Press ${styleText("bold", "?")} \u2502 show shortcuts`);
4103
4156
  console.log(
4104
4157
  ` ${styleText("dim", "\u203A")} Press ${styleText("bold", "Ctrl+C")} \u2502 stop the server
@@ -4134,6 +4187,7 @@ function printHelp() {
4134
4187
  ["--data-dir <path>", "Use a specific server data directory"],
4135
4188
  ["--public-url <https-url>", "Use your tunnel URL without prompting"],
4136
4189
  ["--no-interactive", "Disable prompts and keyboard shortcuts"],
4190
+ ["--show-payload", "Print manual JSON with the pairing QR code"],
4137
4191
  ["--timeout-ms <0-60000>", "Set the maintenance drain timeout"],
4138
4192
  ["--version", "Print the server version"],
4139
4193
  ["--help", "Show this help"]
@@ -4143,6 +4197,69 @@ function printHelp() {
4143
4197
  }
4144
4198
 
4145
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
+ }
4146
4263
  function pairingOrigin(url) {
4147
4264
  const endpoint = new URL(url);
4148
4265
  if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== "/")
@@ -4163,17 +4280,19 @@ async function printPairing(dataDir, url) {
4163
4280
  expiresAt: z4.number(),
4164
4281
  serverId: z4.string()
4165
4282
  }).parse(await controlRequest(dataDir, { command: "pair" }));
4166
- const payload = JSON.stringify({
4283
+ const invitation = {
4167
4284
  v: 1,
4168
4285
  url: origin,
4169
4286
  serverId: grant.serverId,
4170
4287
  pairingId: grant.pairingId
4171
- });
4288
+ };
4289
+ const payload = JSON.stringify(invitation);
4290
+ const link = createPairingLink(invitation);
4172
4291
  printHeading("pairing");
4173
- console.log(" 1. Open Agentrific on your phone.");
4174
- 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.");
4175
4294
  console.log(" 3. Enter the PIN, then confirm pairing.\n");
4176
- qrcode.generate(payload, { small: true }, (code) => {
4295
+ qrcode.generate(link, { small: true }, (code) => {
4177
4296
  console.log(
4178
4297
  code.split("\n").map((line) => ` ${line}`).join("\n")
4179
4298
  );
@@ -4183,16 +4302,13 @@ async function printPairing(dataDir, url) {
4183
4302
  printDetail("Server", styleText2("cyan", origin));
4184
4303
  printDetail("Expires", "In five minutes \xB7 one device per code");
4185
4304
  console.log("\n Pairing grants this device access to run agents as your host user.\n");
4186
- console.log(styleText2("dim", " Manual entry \xB7 paste this payload into the app:"));
4187
- console.log(`Pairing payload:
4188
- ${payload}`);
4189
- console.log();
4305
+ return { payload, expiresAt: grant.expiresAt };
4190
4306
  }
4191
4307
 
4192
4308
  // src/terminal.ts
4193
4309
  import { createInterface } from "node:readline/promises";
4194
4310
  import select from "@inquirer/select";
4195
- import { emitKeypressEvents } from "node:readline";
4311
+ import { emitKeypressEvents as emitKeypressEvents2 } from "node:readline";
4196
4312
  import { styleText as styleText3 } from "node:util";
4197
4313
 
4198
4314
  // src/tunnels.ts
@@ -4501,6 +4617,60 @@ async function promptPublicUrl(signal, saved) {
4501
4617
  prompt.close();
4502
4618
  }
4503
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
+ }
4504
4674
  async function selectPublicConnection(origin, saved) {
4505
4675
  const controller = new AbortController();
4506
4676
  const cancel = () => controller.abort();
@@ -4516,60 +4686,14 @@ async function selectPublicConnection(origin, saved) {
4516
4686
  }
4517
4687
  console.log();
4518
4688
  while (true) {
4519
- const choice = await select(
4520
- {
4521
- message: "Connection method",
4522
- default: saved ? "existing" : void 0,
4523
- loop: false,
4524
- choices: [
4525
- ...options.filter((option2) => option2.available).map((option2) => ({
4526
- value: option2.provider,
4527
- name: option2.provider === "tailscale" ? "Tailscale" : "Cloudflared",
4528
- description: option2.reason
4529
- })),
4530
- {
4531
- value: "existing",
4532
- name: "Existing HTTPS address",
4533
- description: saved ? `Paste an address, or keep ${saved}` : "Paste the public address from your tunnel provider."
4534
- },
4535
- {
4536
- value: "local",
4537
- name: "Local only",
4538
- description: "Start without a public address."
4539
- }
4540
- ],
4541
- theme: {
4542
- prefix: " ",
4543
- style: {
4544
- message: (text) => styleText3("bold", text),
4545
- highlight: (text) => styleText3("cyan", text),
4546
- answer: (text) => styleText3("cyan", text),
4547
- description: (text) => styleText3("dim", ` ${text}`),
4548
- keysHelpTip: () => styleText3("dim", " \u2191/\u2193 to move \xB7 Enter to select")
4549
- }
4550
- }
4551
- },
4552
- { signal: controller.signal }
4553
- );
4689
+ const choice = await selectConnectionMethod(options, controller.signal, saved);
4554
4690
  if (choice === "local") return { remember: false };
4555
4691
  if (choice === "existing") {
4556
4692
  console.log();
4557
4693
  return { url: await promptPublicUrl(controller.signal, saved), remember: true };
4558
4694
  }
4559
- const option = options.find((candidate) => candidate.provider === choice);
4560
- if (!option?.available) throw new Error("The selected tunnel is unavailable.");
4561
- const tunnel = new ManagedTunnel(option);
4562
- try {
4563
- console.log(styleText3("dim", `
4564
- Starting ${option.provider}\u2026
4565
- `));
4566
- const url = await tunnel.start(origin, controller.signal);
4567
- return { url, tunnel, remember: false };
4568
- } catch (error) {
4569
- await tunnel.close();
4570
- if (controller.signal.aborted) throw error;
4571
- printError(error instanceof Error ? error.message : String(error));
4572
- }
4695
+ const connection = await startSelectedTunnel(options, choice, origin, controller.signal);
4696
+ if (connection) return connection;
4573
4697
  }
4574
4698
  } finally {
4575
4699
  process.removeListener("SIGINT", cancel);
@@ -4581,6 +4705,7 @@ function startTerminal(dataDir, publicUrl, tunnel) {
4581
4705
  const previousRaw = input.isRaw;
4582
4706
  let busy = false;
4583
4707
  let closed = false;
4708
+ let invitation;
4584
4709
  const close = () => {
4585
4710
  if (closed) return;
4586
4711
  closed = true;
@@ -4602,10 +4727,15 @@ function startTerminal(dataDir, publicUrl, tunnel) {
4602
4727
  return;
4603
4728
  }
4604
4729
  if (text === "?" && !busy) {
4605
- 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);
4606
4736
  return;
4607
4737
  }
4608
- if (text?.toLowerCase() !== "p" || busy || key.ctrl || key.meta) return;
4738
+ if (text?.toLowerCase() !== "p") return;
4609
4739
  if (tunnel && !tunnel.running()) {
4610
4740
  printError(
4611
4741
  "The managed tunnel is no longer running. Restart the server to reconnect your phone."
@@ -4613,12 +4743,15 @@ function startTerminal(dataDir, publicUrl, tunnel) {
4613
4743
  return;
4614
4744
  }
4615
4745
  busy = true;
4616
- 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(() => {
4617
4750
  busy = false;
4618
- if (!closed) printShortcuts();
4751
+ if (!closed) printShortcuts(Boolean(invitation));
4619
4752
  });
4620
4753
  }
4621
- emitKeypressEvents(input);
4754
+ emitKeypressEvents2(input);
4622
4755
  input.setRawMode(true);
4623
4756
  input.on("keypress", pressed);
4624
4757
  input.once("end", close);
@@ -4652,8 +4785,8 @@ var ConfigSchema = z16.object({
4652
4785
  projectsDir: z16.string(),
4653
4786
  publicUrl: z16.string().url().optional()
4654
4787
  });
4655
- async function main() {
4656
- const { values, positionals } = parseArgs({
4788
+ function parseCommandArgs() {
4789
+ return parseArgs({
4657
4790
  allowPositionals: true,
4658
4791
  options: {
4659
4792
  "data-dir": { type: "string" },
@@ -4662,9 +4795,13 @@ async function main() {
4662
4795
  help: { type: "boolean" },
4663
4796
  check: { type: "boolean" },
4664
4797
  "timeout-ms": { type: "string" },
4665
- "no-interactive": { type: "boolean" }
4798
+ "no-interactive": { type: "boolean" },
4799
+ "show-payload": { type: "boolean" }
4666
4800
  }
4667
4801
  });
4802
+ }
4803
+ async function main() {
4804
+ const { values, positionals } = parseCommandArgs();
4668
4805
  if (values.version) {
4669
4806
  console.log(serverVersion);
4670
4807
  return;
@@ -4686,111 +4823,132 @@ async function main() {
4686
4823
  })
4687
4824
  )
4688
4825
  );
4689
- if (command === "init") {
4690
- await mkdir5(dataDir, { recursive: true, mode: 448 });
4691
- const publicUrl = values["public-url"] ?? config.publicUrl;
4692
- if (publicUrl && new URL(publicUrl).protocol !== "https:")
4693
- throw new Error("Configure an HTTPS URL from Tailscale Serve or Cloudflare Tunnel.");
4694
- await writeFile4(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
4695
- mode: 384
4696
- });
4697
- console.log(`Configuration saved to ${filename}. Start with agentrific start.`);
4698
- 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");
4699
4859
  }
4700
- if (command === "start") {
4701
- process.env.AGENTRIFIC_DATA_DIR = dataDir;
4702
- process.env.AGENTRIFIC_PROJECTS_DIR ??= config.projectsDir;
4703
- const interactive = Boolean(
4704
- process.stdin.isTTY && process.stdout.isTTY && !values["no-interactive"]
4705
- );
4706
- const explicitUrl = values["public-url"] ?? process.env.AGENTRIFIC_PUBLIC_URL;
4707
- if (interactive) {
4708
- process.env.AGENTRIFIC_INTERACTIVE = "1";
4709
- printHeading("server");
4710
- }
4711
- const connection = interactive && !explicitUrl ? await selectPublicConnection(
4712
- `http://${process.env.AGENTRIFIC_HOST ?? "127.0.0.1"}:${process.env.AGENTRIFIC_PORT ?? "4318"}`,
4713
- config.publicUrl
4714
- ) : {
4715
- url: explicitUrl ? pairingOrigin(explicitUrl) : config.publicUrl,
4716
- remember: false,
4717
- tunnel: void 0
4718
- };
4719
- const publicUrl = connection.url;
4720
- try {
4721
- if (connection.remember && publicUrl) {
4722
- await mkdir5(dataDir, { recursive: true, mode: 448 });
4723
- await writeFile4(filename, JSON.stringify({ ...config, publicUrl }, null, 2) + "\n", {
4724
- mode: 384
4725
- });
4726
- }
4727
- if (publicUrl) process.env.AGENTRIFIC_PUBLIC_URL = pairingOrigin(publicUrl);
4728
- await init_index().then(() => index_exports);
4729
- if (interactive) startTerminal(dataDir, publicUrl, connection.tunnel);
4730
- } catch (error) {
4731
- await connection.tunnel?.close();
4732
- 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
+ });
4733
4875
  }
4734
- return;
4735
- }
4736
- if (command === "service") {
4737
- const cli = fileURLToPath(import.meta.url);
4738
- if (!cli.endsWith(".js"))
4739
- throw new Error(
4740
- "Build and install the compiled server package before registering a service."
4741
- );
4742
- await manageService(positionals[1] ?? "", dataDir, cli);
4743
- console.log(`Service ${positionals[1]} completed.`);
4744
- return;
4745
- }
4746
- if (command === "logs") {
4747
- 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" });
4748
- child.on("error", (error) => {
4749
- console.error(error.message);
4750
- process.exitCode = 1;
4751
- });
4752
- child.on("exit", (code) => {
4753
- process.exitCode = code ?? 0;
4754
- });
4755
- return;
4756
- }
4757
- if (command === "update") {
4758
- if (!values.check)
4759
- throw new Error(
4760
- "Use update --check. Automatic installation is unavailable; stop the server and install a reviewed package before restarting."
4761
- );
4762
- console.log(JSON.stringify(await checkForUpdates(), null, 2));
4763
- 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;
4764
4882
  }
4765
- if (command === "pair") {
4766
- const status = await controlRequest(dataDir, { command: "status" });
4767
- await printPairing(
4768
- dataDir,
4769
- 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."
4770
4908
  );
4771
- return;
4772
- }
4773
- if (command === "maintenance") {
4774
- const action = z16.enum(["enter", "resume", "drain"]).parse(positionals[1]);
4775
- const result = await controlRequest(dataDir, {
4776
- command,
4777
- action,
4778
- timeoutMs: z16.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
4779
- });
4780
- console.log(JSON.stringify(result, null, 2));
4781
- if (action === "drain" && "drained" in result && !result.drained) process.exitCode = 1;
4782
- return;
4783
- }
4784
- if (command === "devices" || command === "status") {
4785
- console.log(JSON.stringify(await controlRequest(dataDir, { command }), null, 2));
4786
- return;
4787
- }
4788
- if (command === "revoke") {
4789
- const id = z16.string().uuid().parse(positionals[1]);
4790
- console.log(JSON.stringify(await controlRequest(dataDir, { command, id })));
4791
- 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");
4792
4924
  }
4793
- 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;
4794
4952
  }
4795
4953
  void main().catch((error) => {
4796
4954
  if (["ExitPromptError", "AbortPromptError", "AbortError"].includes(error.name)) {