@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/CHANGELOG.md +12 -0
- package/README.md +2 -0
- package/cli.js +517 -359
- package/cli.js.map +3 -3
- package/index.js +219 -180
- package/index.js.map +2 -2
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ var init_package = __esm({
|
|
|
30
30
|
"package.json"() {
|
|
31
31
|
package_default = {
|
|
32
32
|
name: "@agentrific/server",
|
|
33
|
-
version: "0.1.
|
|
33
|
+
version: "0.1.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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
const
|
|
3305
|
-
if (!
|
|
3306
|
-
return {
|
|
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 ===
|
|
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
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
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 && (
|
|
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
|
|
3360
|
-
|
|
3361
|
-
const
|
|
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
|
|
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
|
|
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.
|
|
4174
|
-
console.log(" 2.
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
4560
|
-
if (
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4656
|
-
|
|
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
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
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
|
-
|
|
4701
|
-
process.env.
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
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
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
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
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
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
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
console.log(
|
|
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
|
-
|
|
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)) {
|