@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/index.js
CHANGED
|
@@ -33,6 +33,9 @@ var cronBounds = [
|
|
|
33
33
|
[1, 12],
|
|
34
34
|
[0, 6]
|
|
35
35
|
];
|
|
36
|
+
function cronRange(base, min, max, stepped) {
|
|
37
|
+
return base === "*" ? [min, max] : base.includes("-") ? base.split("-").map(Number) : [Number(base), stepped ? max : Number(base)];
|
|
38
|
+
}
|
|
36
39
|
function cronFields(expression) {
|
|
37
40
|
const fields = expression.trim().split(/\s+/);
|
|
38
41
|
if (fields.length !== 5)
|
|
@@ -45,7 +48,7 @@ function cronFields(expression) {
|
|
|
45
48
|
if (!match) throw new Error("Use numeric cron fields, *, ranges, lists and steps.");
|
|
46
49
|
const base = match[1];
|
|
47
50
|
const step = Number(match[2] ?? 1);
|
|
48
|
-
const [start, end] = base
|
|
51
|
+
const [start, end] = cronRange(base, min, max, Boolean(match[2]));
|
|
49
52
|
if (!Number.isInteger(step) || step < 1 || step > max - min + 1 || start < min || end > max || start > end)
|
|
50
53
|
throw new Error("Cron field is outside its allowed range.");
|
|
51
54
|
for (let value = start; value <= end; value += step) values.add(value);
|
|
@@ -287,6 +290,7 @@ var TaskSchema = z.object({
|
|
|
287
290
|
gitFlow: GitFlowSchema.optional(),
|
|
288
291
|
status: TaskStatusSchema,
|
|
289
292
|
sessionId: z.string().nullable(),
|
|
293
|
+
pullRequestUrl: z.string().url().optional(),
|
|
290
294
|
createdAt: z.string().datetime(),
|
|
291
295
|
updatedAt: z.string().datetime(),
|
|
292
296
|
deletedAt: z.string().datetime().optional()
|
|
@@ -304,7 +308,7 @@ var UpdateTaskSchema = CreateTaskSchema.pick({
|
|
|
304
308
|
prompt: true,
|
|
305
309
|
attachmentIds: true,
|
|
306
310
|
gitFlow: true
|
|
307
|
-
}).strict();
|
|
311
|
+
}).extend({ gitFlow: GitFlowSchema.nullable().optional() }).strict();
|
|
308
312
|
var PROTOCOL_VERSION = 1;
|
|
309
313
|
var ServerUpdateStatusSchema = z.object({
|
|
310
314
|
currentVersion: z.string(),
|
|
@@ -910,7 +914,7 @@ async function startControl(directory, security2, status, maintenance2) {
|
|
|
910
914
|
// package.json
|
|
911
915
|
var package_default = {
|
|
912
916
|
name: "@agentrific/server",
|
|
913
|
-
version: "0.1.
|
|
917
|
+
version: "0.1.4",
|
|
914
918
|
description: "Self-hosted control plane for long-running coding agents",
|
|
915
919
|
keywords: [
|
|
916
920
|
"agentrific",
|
|
@@ -1169,6 +1173,9 @@ function taskStatusForSession(status, session) {
|
|
|
1169
1173
|
return "needs_user_input";
|
|
1170
1174
|
}
|
|
1171
1175
|
}
|
|
1176
|
+
function pullRequestUrl(session) {
|
|
1177
|
+
return session.gitDelivery?.status === "pr_opened" ? session.gitDelivery.url : void 0;
|
|
1178
|
+
}
|
|
1172
1179
|
function sessionTask(session) {
|
|
1173
1180
|
return TaskSchema.parse({
|
|
1174
1181
|
id: `session-${session.id}`,
|
|
@@ -1179,6 +1186,7 @@ function sessionTask(session) {
|
|
|
1179
1186
|
gitFlow: session.gitFlow,
|
|
1180
1187
|
status: taskStatusForSession(session.status, session),
|
|
1181
1188
|
sessionId: session.id,
|
|
1189
|
+
pullRequestUrl: pullRequestUrl(session),
|
|
1182
1190
|
createdAt: session.createdAt,
|
|
1183
1191
|
updatedAt: session.updatedAt
|
|
1184
1192
|
});
|
|
@@ -1190,12 +1198,13 @@ async function boardTasks(store2, projectId) {
|
|
|
1190
1198
|
tasks.flatMap((task) => task.sessionId ? [task.sessionId] : [])
|
|
1191
1199
|
);
|
|
1192
1200
|
const linkedTasks = tasks.filter((task) => !task.deletedAt).map((task) => {
|
|
1193
|
-
const session = sessions.find((item) => item.
|
|
1194
|
-
if (!session) return { ...task, status: "backlog" };
|
|
1201
|
+
const session = sessions.find((item) => item.id === task.sessionId) ?? sessions.find((item) => item.taskId === task.id);
|
|
1202
|
+
if (!session) return { ...task, status: "backlog", pullRequestUrl: void 0 };
|
|
1195
1203
|
linkedSessionIds.add(session.id);
|
|
1196
1204
|
return {
|
|
1197
1205
|
...task,
|
|
1198
1206
|
sessionId: session.id,
|
|
1207
|
+
pullRequestUrl: pullRequestUrl(session),
|
|
1199
1208
|
status: taskStatusForSession(session.status, session)
|
|
1200
1209
|
};
|
|
1201
1210
|
});
|
|
@@ -1213,7 +1222,9 @@ async function findTask(store2, id) {
|
|
|
1213
1222
|
function editTask(store2, id, input, resolveAttachments) {
|
|
1214
1223
|
return withTaskLock(id, async () => {
|
|
1215
1224
|
const task = await findTask(store2, id);
|
|
1216
|
-
const { attachmentIds, ...updates2 } = input;
|
|
1225
|
+
const { attachmentIds, gitFlow, ...updates2 } = input;
|
|
1226
|
+
const configuration = { gitFlow: task.gitFlow };
|
|
1227
|
+
if (gitFlow !== void 0) configuration.gitFlow = gitFlow ?? void 0;
|
|
1217
1228
|
let files = task.attachments;
|
|
1218
1229
|
if (attachmentIds) {
|
|
1219
1230
|
if (!resolveAttachments)
|
|
@@ -1224,6 +1235,7 @@ function editTask(store2, id, input, resolveAttachments) {
|
|
|
1224
1235
|
store2.saveTask({
|
|
1225
1236
|
...task,
|
|
1226
1237
|
...updates2,
|
|
1238
|
+
...configuration,
|
|
1227
1239
|
attachments: files,
|
|
1228
1240
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1229
1241
|
})
|
|
@@ -1467,7 +1479,7 @@ var AgentController = class {
|
|
|
1467
1479
|
Effect7.gen({ self: this }, function* () {
|
|
1468
1480
|
if (this.closed)
|
|
1469
1481
|
return yield* Effect7.fail(new AgentError({ message: "Server is shutting down" }));
|
|
1470
|
-
if (!session.branch)
|
|
1482
|
+
if (!session.branch && (session.gitFlow || !session.workspacePath))
|
|
1471
1483
|
return yield* Effect7.fail(new AgentError({ message: "Session checkout is not ready" }));
|
|
1472
1484
|
if (this.hasMessage(session.id, messageId)) return;
|
|
1473
1485
|
const entry = yield* this.open(session);
|
|
@@ -1774,45 +1786,7 @@ ${error ? workflowError(error instanceof Error ? error : String(error)) : messag
|
|
|
1774
1786
|
if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
|
|
1775
1787
|
throw new Error("Restore the assigned run branch before requesting delivery.");
|
|
1776
1788
|
if (dirty) {
|
|
1777
|
-
await this.
|
|
1778
|
-
await this.stage(job, "drafting");
|
|
1779
|
-
const diff = await this.git(session, ["diff", "--cached", "HEAD", "--"], signal);
|
|
1780
|
-
const files = await this.git(
|
|
1781
|
-
session,
|
|
1782
|
-
["ls-files", "--others", "--exclude-standard"],
|
|
1783
|
-
signal
|
|
1784
|
-
);
|
|
1785
|
-
if (this.options.draft) {
|
|
1786
|
-
try {
|
|
1787
|
-
const draft = await this.options.draft(
|
|
1788
|
-
session,
|
|
1789
|
-
input,
|
|
1790
|
-
`${diff}
|
|
1791
|
-
New files:
|
|
1792
|
-
${files}`.slice(0, 4e4),
|
|
1793
|
-
signal
|
|
1794
|
-
);
|
|
1795
|
-
job.title = draft.title;
|
|
1796
|
-
job.body = draft.body;
|
|
1797
|
-
} catch (error) {
|
|
1798
|
-
job.logs = workflowLog(
|
|
1799
|
-
`${job.logs}
|
|
1800
|
-
Message helper unavailable; using task summary. ${workflowError(error instanceof Error ? error : String(error))}`
|
|
1801
|
-
);
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
await this.stage(job, "committing");
|
|
1805
|
-
if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
|
|
1806
|
-
throw new Error("The workspace changed branches while preparing delivery.");
|
|
1807
|
-
const output = await this.git(
|
|
1808
|
-
session,
|
|
1809
|
-
["commit", "-m", `${job.title}
|
|
1810
|
-
|
|
1811
|
-
Agentrific run ${id}.`],
|
|
1812
|
-
signal
|
|
1813
|
-
);
|
|
1814
|
-
job.logs = workflowLog(`${job.logs}
|
|
1815
|
-
${output}`);
|
|
1789
|
+
await this.commitChanges(session, job, input, signal);
|
|
1816
1790
|
} else {
|
|
1817
1791
|
await this.git(session, ["hook", "run", "--ignore-missing", "pre-commit"], signal);
|
|
1818
1792
|
}
|
|
@@ -1839,6 +1813,44 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
1839
1813
|
}
|
|
1840
1814
|
});
|
|
1841
1815
|
}
|
|
1816
|
+
async commitChanges(session, job, input, signal) {
|
|
1817
|
+
const id = session.id;
|
|
1818
|
+
await this.git(session, ["add", "--all"], signal);
|
|
1819
|
+
await this.stage(job, "drafting");
|
|
1820
|
+
const diff = await this.git(session, ["diff", "--cached", "HEAD", "--"], signal);
|
|
1821
|
+
const files = await this.git(session, ["ls-files", "--others", "--exclude-standard"], signal);
|
|
1822
|
+
if (this.options.draft) {
|
|
1823
|
+
try {
|
|
1824
|
+
const draft = await this.options.draft(
|
|
1825
|
+
session,
|
|
1826
|
+
input,
|
|
1827
|
+
`${diff}
|
|
1828
|
+
New files:
|
|
1829
|
+
${files}`.slice(0, 4e4),
|
|
1830
|
+
signal
|
|
1831
|
+
);
|
|
1832
|
+
job.title = draft.title;
|
|
1833
|
+
job.body = draft.body;
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
job.logs = workflowLog(
|
|
1836
|
+
`${job.logs}
|
|
1837
|
+
Message helper unavailable; using task summary. ${workflowError(error instanceof Error ? error : String(error))}`
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
await this.stage(job, "committing");
|
|
1842
|
+
if (await this.git(session, ["branch", "--show-current"], signal) !== session.branch)
|
|
1843
|
+
throw new Error("The workspace changed branches while preparing delivery.");
|
|
1844
|
+
const output = await this.git(
|
|
1845
|
+
session,
|
|
1846
|
+
["commit", "-m", `${job.title}
|
|
1847
|
+
|
|
1848
|
+
Agentrific run ${id}.`],
|
|
1849
|
+
signal
|
|
1850
|
+
);
|
|
1851
|
+
job.logs = workflowLog(`${job.logs}
|
|
1852
|
+
${output}`);
|
|
1853
|
+
}
|
|
1842
1854
|
fallbackBody(id, input) {
|
|
1843
1855
|
return `${input.summary}
|
|
1844
1856
|
|
|
@@ -1911,6 +1923,31 @@ Agentrific run ${id}.`;
|
|
|
1911
1923
|
);
|
|
1912
1924
|
}
|
|
1913
1925
|
}
|
|
1926
|
+
async mergeChanges(session, job, projectPath, commit) {
|
|
1927
|
+
await this.stage(job, "integrating");
|
|
1928
|
+
if (session.gitFlow.workspace === "checkout" && await this.command(projectPath, "git", ["branch", "--show-current"]) === session.branch)
|
|
1929
|
+
await this.command(projectPath, "git", ["switch", "main"]);
|
|
1930
|
+
if (await this.command(projectPath, "git", ["branch", "--show-current"]) !== "main")
|
|
1931
|
+
throw new Error("Switch the project checkout to main, then retry delivery.");
|
|
1932
|
+
if (await this.command(projectPath, "git", ["status", "--porcelain"]))
|
|
1933
|
+
throw new Error(
|
|
1934
|
+
"Waiting for local changes in the main checkout. Commit or move them, then retry delivery."
|
|
1935
|
+
);
|
|
1936
|
+
const included = await this.command(projectPath, "git", ["merge-base", "main", commit]) === commit;
|
|
1937
|
+
if (!included) {
|
|
1938
|
+
try {
|
|
1939
|
+
await this.command(projectPath, "git", ["merge", "--no-edit", commit]);
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
await this.command(projectPath, "git", ["merge", "--abort"]).catch(() => void 0);
|
|
1942
|
+
throw new Error(
|
|
1943
|
+
`Integration failed. Resolve conflicts against main in the run workspace, then call deliver_changes again.
|
|
1944
|
+
${workflowError(error instanceof Error ? error : String(error))}`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
job.result = "merged";
|
|
1949
|
+
job.message = "Committed changes are on main.";
|
|
1950
|
+
}
|
|
1914
1951
|
async integrate(id) {
|
|
1915
1952
|
const job = await this.job(id);
|
|
1916
1953
|
if (!job?.commitSha) return;
|
|
@@ -1924,29 +1961,7 @@ Agentrific run ${id}.`;
|
|
|
1924
1961
|
"The workspace changed after requesting delivery. Call deliver_changes again for the new changes."
|
|
1925
1962
|
);
|
|
1926
1963
|
if (session.gitFlow.delivery === "merge") {
|
|
1927
|
-
await this.
|
|
1928
|
-
if (session.gitFlow.workspace === "checkout" && await this.command(projectPath, "git", ["branch", "--show-current"]) === session.branch)
|
|
1929
|
-
await this.command(projectPath, "git", ["switch", "main"]);
|
|
1930
|
-
if (await this.command(projectPath, "git", ["branch", "--show-current"]) !== "main")
|
|
1931
|
-
throw new Error("Switch the project checkout to main, then retry delivery.");
|
|
1932
|
-
if (await this.command(projectPath, "git", ["status", "--porcelain"]))
|
|
1933
|
-
throw new Error(
|
|
1934
|
-
"Waiting for local changes in the main checkout. Commit or move them, then retry delivery."
|
|
1935
|
-
);
|
|
1936
|
-
const included = await this.command(projectPath, "git", ["merge-base", "main", commit]) === commit;
|
|
1937
|
-
if (!included) {
|
|
1938
|
-
try {
|
|
1939
|
-
await this.command(projectPath, "git", ["merge", "--no-edit", commit]);
|
|
1940
|
-
} catch (error) {
|
|
1941
|
-
await this.command(projectPath, "git", ["merge", "--abort"]).catch(() => void 0);
|
|
1942
|
-
throw new Error(
|
|
1943
|
-
`Integration failed. Resolve conflicts against main in the run workspace, then call deliver_changes again.
|
|
1944
|
-
${workflowError(error instanceof Error ? error : String(error))}`
|
|
1945
|
-
);
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
job.result = "merged";
|
|
1949
|
-
job.message = "Committed changes are on main.";
|
|
1964
|
+
await this.mergeChanges(session, job, projectPath, commit);
|
|
1950
1965
|
} else {
|
|
1951
1966
|
await this.stage(job, "pushing");
|
|
1952
1967
|
await this.git(session, ["push", "origin", `${commit}:refs/heads/${session.branch}`]);
|
|
@@ -2024,6 +2039,7 @@ var PreparationCoordinator = class {
|
|
|
2024
2039
|
return [...this.preparing.values()].map((id) => `preparation:${id}`);
|
|
2025
2040
|
}
|
|
2026
2041
|
async prepare(session, projectPath, input) {
|
|
2042
|
+
if (!session.gitFlow) return { ...session, branch: null, workspacePath: projectPath };
|
|
2027
2043
|
const preparation2 = Symbol(session.id);
|
|
2028
2044
|
this.preparing.set(preparation2, session.id);
|
|
2029
2045
|
return withTaskLock(`prepare:${session.id}`, async () => {
|
|
@@ -2056,56 +2072,7 @@ ${logs}` : ""}`);
|
|
|
2056
2072
|
try {
|
|
2057
2073
|
await record("creating", "Creating the run workspace\u2026");
|
|
2058
2074
|
const workspace = session.workspacePath;
|
|
2059
|
-
|
|
2060
|
-
const branchAtWorkspace = await command(workspace, "git", [
|
|
2061
|
-
"branch",
|
|
2062
|
-
"--show-current"
|
|
2063
|
-
]).catch(() => void 0);
|
|
2064
|
-
if (branchAtWorkspace !== session.branch) {
|
|
2065
|
-
if (checkout) {
|
|
2066
|
-
if (branchAtWorkspace !== "main" || await command(projectPath, "git", ["status", "--porcelain"]))
|
|
2067
|
-
throw new Error(
|
|
2068
|
-
"The project checkout must be clean and on main before starting a checkout run."
|
|
2069
|
-
);
|
|
2070
|
-
await command(
|
|
2071
|
-
projectPath,
|
|
2072
|
-
"git",
|
|
2073
|
-
["switch", "-c", session.branch, "main"],
|
|
2074
|
-
void 0,
|
|
2075
|
-
true
|
|
2076
|
-
);
|
|
2077
|
-
} else {
|
|
2078
|
-
if (branchAtWorkspace !== void 0)
|
|
2079
|
-
throw new Error("The run workspace already exists on a different branch.");
|
|
2080
|
-
await mkdir3(path4.dirname(workspace), { recursive: true });
|
|
2081
|
-
try {
|
|
2082
|
-
await command(
|
|
2083
|
-
projectPath,
|
|
2084
|
-
"git",
|
|
2085
|
-
[
|
|
2086
|
-
"worktree",
|
|
2087
|
-
"add",
|
|
2088
|
-
"-b",
|
|
2089
|
-
session.branch,
|
|
2090
|
-
workspace,
|
|
2091
|
-
session.gitFlow ? "main" : "HEAD"
|
|
2092
|
-
],
|
|
2093
|
-
void 0,
|
|
2094
|
-
true
|
|
2095
|
-
);
|
|
2096
|
-
} catch (error) {
|
|
2097
|
-
if (await command(workspace, "git", ["branch", "--show-current"]).catch(
|
|
2098
|
-
() => void 0
|
|
2099
|
-
) !== session.branch)
|
|
2100
|
-
throw error;
|
|
2101
|
-
await record(
|
|
2102
|
-
"creating",
|
|
2103
|
-
"Workspace created; a checkout hook failed. Preparing dependencies\u2026",
|
|
2104
|
-
workflowError(error instanceof Error ? error : String(error))
|
|
2105
|
-
);
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2075
|
+
await this.createWorkspace(session, projectPath, command, record);
|
|
2109
2076
|
await record("installing", "Installing run dependencies\u2026");
|
|
2110
2077
|
await this.install(workspace, command);
|
|
2111
2078
|
await record("ready", "Workspace ready.");
|
|
@@ -2121,6 +2088,57 @@ ${logs}` : ""}`);
|
|
|
2121
2088
|
}
|
|
2122
2089
|
}).finally(() => this.preparing.delete(preparation2));
|
|
2123
2090
|
}
|
|
2091
|
+
async createWorkspace(session, projectPath, command, record) {
|
|
2092
|
+
const workspace = session.workspacePath;
|
|
2093
|
+
const checkout = session.gitFlow?.workspace === "checkout";
|
|
2094
|
+
if (!await command(projectPath, "git", ["rev-parse", "--verify", "HEAD"]).catch(() => void 0))
|
|
2095
|
+
throw new Error(
|
|
2096
|
+
"Git Flow needs an initial commit. Create one in the project, or turn off Git Flow for a plain chat."
|
|
2097
|
+
);
|
|
2098
|
+
const branchAtWorkspace = await command(workspace, "git", ["branch", "--show-current"]).catch(
|
|
2099
|
+
() => void 0
|
|
2100
|
+
);
|
|
2101
|
+
if (branchAtWorkspace !== session.branch) {
|
|
2102
|
+
if (checkout) {
|
|
2103
|
+
if (branchAtWorkspace !== "main" || await command(projectPath, "git", ["status", "--porcelain"]))
|
|
2104
|
+
throw new Error(
|
|
2105
|
+
"The project checkout must be clean and on main before starting a checkout run."
|
|
2106
|
+
);
|
|
2107
|
+
await command(
|
|
2108
|
+
projectPath,
|
|
2109
|
+
"git",
|
|
2110
|
+
["switch", "-c", session.branch, "main"],
|
|
2111
|
+
void 0,
|
|
2112
|
+
true
|
|
2113
|
+
);
|
|
2114
|
+
} else {
|
|
2115
|
+
await this.createWorktree(session, projectPath, command, record, branchAtWorkspace);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
async createWorktree(session, projectPath, command, record, branchAtWorkspace) {
|
|
2120
|
+
const workspace = session.workspacePath;
|
|
2121
|
+
if (branchAtWorkspace !== void 0)
|
|
2122
|
+
throw new Error("The run workspace already exists on a different branch.");
|
|
2123
|
+
await mkdir3(path4.dirname(workspace), { recursive: true });
|
|
2124
|
+
try {
|
|
2125
|
+
await command(
|
|
2126
|
+
projectPath,
|
|
2127
|
+
"git",
|
|
2128
|
+
["worktree", "add", "-b", session.branch, workspace, "main"],
|
|
2129
|
+
void 0,
|
|
2130
|
+
true
|
|
2131
|
+
);
|
|
2132
|
+
} catch (error) {
|
|
2133
|
+
if (await command(workspace, "git", ["branch", "--show-current"]).catch(() => void 0) !== session.branch)
|
|
2134
|
+
throw error;
|
|
2135
|
+
await record(
|
|
2136
|
+
"creating",
|
|
2137
|
+
"Workspace created; a checkout hook failed. Preparing dependencies\u2026",
|
|
2138
|
+
workflowError(error instanceof Error ? error : String(error))
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2124
2142
|
async install(workspace, command) {
|
|
2125
2143
|
const packageFile = await readFile3(path4.join(workspace, "package.json"), "utf8").catch(
|
|
2126
2144
|
() => void 0
|
|
@@ -2845,31 +2863,7 @@ var SqliteSessionStore = class {
|
|
|
2845
2863
|
this.db.prepare("DELETE FROM projects WHERE id = ?").run(replacedId);
|
|
2846
2864
|
}
|
|
2847
2865
|
if (previousPath && previousPath !== project.path) {
|
|
2848
|
-
this.
|
|
2849
|
-
"UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
|
|
2850
|
-
).run(project.path, previousPath);
|
|
2851
|
-
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path6.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
|
|
2852
|
-
for (const row of this.db.prepare(
|
|
2853
|
-
"SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?"
|
|
2854
|
-
).all(project.id)) {
|
|
2855
|
-
const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
|
|
2856
|
-
if (session.workspacePath) {
|
|
2857
|
-
session.workspacePath = String(relocate(session.workspacePath));
|
|
2858
|
-
this.db.prepare("UPDATE sessions SET payload = ? WHERE id = ?").run(JSON.stringify(session), session.id);
|
|
2859
|
-
}
|
|
2860
|
-
}
|
|
2861
|
-
for (const row of this.db.prepare(
|
|
2862
|
-
"SELECT sequence, payload FROM events WHERE json_extract(payload, '$.session.projectId') = ?"
|
|
2863
|
-
).all(project.id)) {
|
|
2864
|
-
const event = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload).session);
|
|
2865
|
-
if (event.workspacePath) {
|
|
2866
|
-
event.workspacePath = String(relocate(event.workspacePath));
|
|
2867
|
-
this.db.prepare("UPDATE events SET payload = ? WHERE sequence = ?").run(
|
|
2868
|
-
JSON.stringify({ type: "session.updated", session: event }),
|
|
2869
|
-
Number(row.sequence)
|
|
2870
|
-
);
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2866
|
+
this.relocateProjectHistory(project, previousPath);
|
|
2873
2867
|
}
|
|
2874
2868
|
this.db.prepare(
|
|
2875
2869
|
"INSERT INTO projects(id, payload) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET payload = excluded.payload"
|
|
@@ -2882,6 +2876,28 @@ var SqliteSessionStore = class {
|
|
|
2882
2876
|
}
|
|
2883
2877
|
});
|
|
2884
2878
|
}
|
|
2879
|
+
relocateProjectHistory(project, previousPath) {
|
|
2880
|
+
this.db.prepare(
|
|
2881
|
+
"UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
|
|
2882
|
+
).run(project.path, previousPath);
|
|
2883
|
+
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path6.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
|
|
2884
|
+
for (const row of this.db.prepare("SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?").all(project.id)) {
|
|
2885
|
+
const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
|
|
2886
|
+
if (session.workspacePath) {
|
|
2887
|
+
session.workspacePath = String(relocate(session.workspacePath));
|
|
2888
|
+
this.db.prepare("UPDATE sessions SET payload = ? WHERE id = ?").run(JSON.stringify(session), session.id);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
for (const row of this.db.prepare(
|
|
2892
|
+
"SELECT sequence, payload FROM events WHERE json_extract(payload, '$.session.projectId') = ?"
|
|
2893
|
+
).all(project.id)) {
|
|
2894
|
+
const event = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload).session);
|
|
2895
|
+
if (event.workspacePath) {
|
|
2896
|
+
event.workspacePath = String(relocate(event.workspacePath));
|
|
2897
|
+
this.db.prepare("UPDATE events SET payload = ? WHERE sequence = ?").run(JSON.stringify({ type: "session.updated", session: event }), Number(row.sequence));
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2885
2901
|
defaultModel() {
|
|
2886
2902
|
return this.query(() => {
|
|
2887
2903
|
const row = this.db.prepare("SELECT payload FROM settings WHERE id = 'default-model'").get();
|
|
@@ -3486,16 +3502,27 @@ app.get("/api/sessions/:id/events", async (request, reply) => {
|
|
|
3486
3502
|
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
3487
3503
|
return Effect14.runPromise(store.history(session.id, query.data.after, query.data.limit));
|
|
3488
3504
|
});
|
|
3489
|
-
async function
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
const
|
|
3493
|
-
if (!
|
|
3494
|
-
return {
|
|
3505
|
+
async function selectSessionModel(input, hasImages) {
|
|
3506
|
+
try {
|
|
3507
|
+
const reference = input.model ?? await models.defaultReference();
|
|
3508
|
+
const selected = await models.resolve(reference);
|
|
3509
|
+
if (hasImages && !selected.input.includes("image"))
|
|
3510
|
+
return {
|
|
3511
|
+
error: "This model cannot read images. Choose a vision model in Settings.",
|
|
3512
|
+
status: 400
|
|
3513
|
+
};
|
|
3514
|
+
return { model: models.identity(selected.provider, selected.id) };
|
|
3515
|
+
} catch {
|
|
3516
|
+
return {
|
|
3517
|
+
error: "The default model is unavailable. Choose an available model in Settings.",
|
|
3518
|
+
status: 400
|
|
3519
|
+
};
|
|
3495
3520
|
}
|
|
3521
|
+
}
|
|
3522
|
+
async function resolveSessionProject(projectId) {
|
|
3496
3523
|
const projectsRoot = await Effect14.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
3497
3524
|
const project = (await Effect14.runPromise(discoverProjects(projectsRoot, store))).find(
|
|
3498
|
-
(item) => item.id ===
|
|
3525
|
+
(item) => item.id === projectId && item.available
|
|
3499
3526
|
);
|
|
3500
3527
|
if (!project)
|
|
3501
3528
|
return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
|
|
@@ -3503,6 +3530,37 @@ async function createAndStartSession(input) {
|
|
|
3503
3530
|
const resolvedRoot = await realpath2(projectsRoot);
|
|
3504
3531
|
if (path7.dirname(projectPath) !== resolvedRoot)
|
|
3505
3532
|
return { error: "Project path is outside projects root.", status: 400 };
|
|
3533
|
+
return { projectPath };
|
|
3534
|
+
}
|
|
3535
|
+
async function resolveSessionWorkspace(id, gitFlow, projectPath) {
|
|
3536
|
+
const worktreePath = path7.join(worktreesRoot, id);
|
|
3537
|
+
const branch = gitFlow ? `agentrific/${id.slice(0, 8)}` : null;
|
|
3538
|
+
const useProjectCheckout = gitFlow?.workspace === "checkout";
|
|
3539
|
+
const workspacePath = !gitFlow || useProjectCheckout ? projectPath : worktreePath;
|
|
3540
|
+
if (useProjectCheckout) {
|
|
3541
|
+
const serverPath = await realpath2(process.cwd());
|
|
3542
|
+
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path7.sep}`))
|
|
3543
|
+
return {
|
|
3544
|
+
error: "The server runs from this project. Choose a worktree for this task.",
|
|
3545
|
+
status: 409
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
return { branch, workspacePath };
|
|
3549
|
+
}
|
|
3550
|
+
function sessionTaskUnavailable(task, projectId) {
|
|
3551
|
+
return !task || Boolean(task.deletedAt) || task.projectId !== projectId;
|
|
3552
|
+
}
|
|
3553
|
+
async function sessionDeliveryAvailable(gitFlow) {
|
|
3554
|
+
if (gitFlow?.delivery !== "pull_request") return true;
|
|
3555
|
+
return (await githubCliStatus()).githubCli.authenticated;
|
|
3556
|
+
}
|
|
3557
|
+
async function createAndStartSession(input) {
|
|
3558
|
+
maintenance.assertReady();
|
|
3559
|
+
if (!await sessionDeliveryAvailable(input.gitFlow))
|
|
3560
|
+
return { error: "GitHub CLI must be authenticated on the server to open a PR.", status: 400 };
|
|
3561
|
+
const location = await resolveSessionProject(input.projectId);
|
|
3562
|
+
if (location.error !== void 0) return { error: location.error, status: location.status };
|
|
3563
|
+
const projectPath = location.projectPath;
|
|
3506
3564
|
let files;
|
|
3507
3565
|
try {
|
|
3508
3566
|
files = await Effect14.runPromise(
|
|
@@ -3511,25 +3569,15 @@ async function createAndStartSession(input) {
|
|
|
3511
3569
|
} catch (error) {
|
|
3512
3570
|
return { error: String(error), status: 400 };
|
|
3513
3571
|
}
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
error: "This model cannot read images. Choose a vision model in Settings.",
|
|
3521
|
-
status: 400
|
|
3522
|
-
};
|
|
3523
|
-
selectedModel = models.identity(selected.provider, selected.id);
|
|
3524
|
-
} catch {
|
|
3525
|
-
return {
|
|
3526
|
-
error: "The default model is unavailable. Choose an available model in Settings.",
|
|
3527
|
-
status: 400
|
|
3528
|
-
};
|
|
3529
|
-
}
|
|
3572
|
+
const selection = await selectSessionModel(
|
|
3573
|
+
input,
|
|
3574
|
+
files.some((file) => file.kind === "image")
|
|
3575
|
+
);
|
|
3576
|
+
if (selection.error !== void 0) return { error: selection.error, status: selection.status };
|
|
3577
|
+
const selectedModel = selection.model;
|
|
3530
3578
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3531
3579
|
const task = input.taskId ? await Effect14.runPromise(store.getTask(input.taskId)) : void 0;
|
|
3532
|
-
if (input.taskId && (
|
|
3580
|
+
if (input.taskId && sessionTaskUnavailable(task, input.projectId))
|
|
3533
3581
|
return { error: "Task not found.", status: 404 };
|
|
3534
3582
|
const session = SessionSchema.parse({
|
|
3535
3583
|
id: randomUUID7(),
|
|
@@ -3544,18 +3592,9 @@ async function createAndStartSession(input) {
|
|
|
3544
3592
|
createdAt: now,
|
|
3545
3593
|
updatedAt: now
|
|
3546
3594
|
});
|
|
3547
|
-
const
|
|
3548
|
-
|
|
3549
|
-
const
|
|
3550
|
-
const workspacePath = useProjectCheckout ? projectPath : worktreePath;
|
|
3551
|
-
if (useProjectCheckout) {
|
|
3552
|
-
const serverPath = await realpath2(process.cwd());
|
|
3553
|
-
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path7.sep}`))
|
|
3554
|
-
return {
|
|
3555
|
-
error: "The server runs from this project. Choose a worktree for this task.",
|
|
3556
|
-
status: 409
|
|
3557
|
-
};
|
|
3558
|
-
}
|
|
3595
|
+
const workspace = await resolveSessionWorkspace(session.id, input.gitFlow, projectPath);
|
|
3596
|
+
if (workspace.error !== void 0) return { error: workspace.error, status: workspace.status };
|
|
3597
|
+
const { branch, workspacePath } = workspace;
|
|
3559
3598
|
const initial = { ...session, branch, workspacePath };
|
|
3560
3599
|
publish({ type: "session.updated", session: initial });
|
|
3561
3600
|
let running;
|