@agentrific/server 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +2 -0
- package/cli.js +1121 -536
- package/cli.js.map +4 -4
- package/index.js +1211 -652
- package/index.js.map +4 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -7,6 +7,303 @@ process.on("warning", (warning) => {
|
|
|
7
7
|
for (const handler of handlers) handler.call(process, warning);
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
+
// src/push.ts
|
|
11
|
+
import {
|
|
12
|
+
generateKeyPairSync,
|
|
13
|
+
createPrivateKey,
|
|
14
|
+
createPublicKey,
|
|
15
|
+
sign,
|
|
16
|
+
randomUUID
|
|
17
|
+
} from "node:crypto";
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { Effect } from "effect";
|
|
21
|
+
|
|
22
|
+
// ../../packages/contracts/src/push.ts
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
var PUSH_API_URL = "https://api.agentrific.com";
|
|
25
|
+
var PushKindSchema = z.enum([
|
|
26
|
+
"started",
|
|
27
|
+
"completed",
|
|
28
|
+
"needs_input",
|
|
29
|
+
"prepared",
|
|
30
|
+
"drafting_pr",
|
|
31
|
+
"pr_created",
|
|
32
|
+
"failed"
|
|
33
|
+
]);
|
|
34
|
+
var PushPreferencesSchema = z.array(PushKindSchema).max(7);
|
|
35
|
+
var PushPublicKeySchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
36
|
+
var PushInstallationSchema = z.object({ installationId: z.string().uuid(), token: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
|
|
37
|
+
var PushRegistrationSchema = z.object({
|
|
38
|
+
expoToken: z.string().regex(/^(Expo|Exponent)PushToken\[[A-Za-z0-9_-]+\]$/),
|
|
39
|
+
preferences: PushPreferencesSchema
|
|
40
|
+
}).strict();
|
|
41
|
+
var PushEnrollmentSchema = z.object({ serverId: z.string().uuid(), publicKey: PushPublicKeySchema }).strict();
|
|
42
|
+
var PushGrantSchema = PushEnrollmentSchema.extend({
|
|
43
|
+
installationId: z.string().uuid(),
|
|
44
|
+
bindingId: z.string().uuid(),
|
|
45
|
+
grant: z.string().regex(/^[a-f0-9]{64}$/),
|
|
46
|
+
expiresAt: z.number()
|
|
47
|
+
}).strict();
|
|
48
|
+
var PushPullRequestUrlSchema = z.string().max(512).regex(/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9][0-9]*$/);
|
|
49
|
+
var PushEventSchema = z.object({
|
|
50
|
+
eventId: z.string().uuid(),
|
|
51
|
+
sessionId: z.string().uuid(),
|
|
52
|
+
kind: PushKindSchema,
|
|
53
|
+
taskTitle: z.string().trim().min(1).max(160).optional(),
|
|
54
|
+
prUrl: PushPullRequestUrlSchema.optional()
|
|
55
|
+
}).strict();
|
|
56
|
+
var PushSignedRequestSchema = z.object({
|
|
57
|
+
bindingId: z.string().uuid(),
|
|
58
|
+
timestamp: z.number().int(),
|
|
59
|
+
nonce: z.string().uuid(),
|
|
60
|
+
payload: z.string().max(2048),
|
|
61
|
+
signature: z.string().regex(/^[a-f0-9]{128}$/)
|
|
62
|
+
}).strict();
|
|
63
|
+
var PushEntitlementSchema = z.object({ active: z.boolean(), expiresAt: z.number().nullable() }).strict();
|
|
64
|
+
var PushServerStatusSchema = z.object({
|
|
65
|
+
publicKey: PushPublicKeySchema,
|
|
66
|
+
bindings: z.array(
|
|
67
|
+
z.object({
|
|
68
|
+
bindingId: z.string().uuid(),
|
|
69
|
+
installationId: z.string().uuid(),
|
|
70
|
+
deviceId: z.string().uuid()
|
|
71
|
+
})
|
|
72
|
+
),
|
|
73
|
+
pending: z.number().int().nonnegative(),
|
|
74
|
+
error: z.string().nullable()
|
|
75
|
+
});
|
|
76
|
+
function pushSigningText(installationId, operation, request) {
|
|
77
|
+
return [
|
|
78
|
+
"agentrific-push-v1",
|
|
79
|
+
installationId,
|
|
80
|
+
operation,
|
|
81
|
+
request.bindingId,
|
|
82
|
+
request.timestamp,
|
|
83
|
+
request.nonce,
|
|
84
|
+
request.payload
|
|
85
|
+
].join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/push.ts
|
|
89
|
+
function pushKind(session, previous) {
|
|
90
|
+
if (session.gitDelivery?.status === "pr_opened" && previous?.gitDelivery?.status !== "pr_opened")
|
|
91
|
+
return "pr_created";
|
|
92
|
+
if (["merged", "no_changes"].includes(session.gitDelivery?.status ?? "") && session.gitDelivery?.status !== previous?.gitDelivery?.status)
|
|
93
|
+
return "completed";
|
|
94
|
+
if (session.gitDelivery?.status === "failed" && previous?.gitDelivery?.status !== "failed")
|
|
95
|
+
return "failed";
|
|
96
|
+
if (session.gitDelivery?.stage === "opening_pr" && previous?.gitDelivery?.stage !== "opening_pr")
|
|
97
|
+
return "drafting_pr";
|
|
98
|
+
if (session.preparation?.status === "ready" && previous?.preparation?.status !== "ready")
|
|
99
|
+
return "prepared";
|
|
100
|
+
if (session.status === previous?.status) return void 0;
|
|
101
|
+
switch (session.status) {
|
|
102
|
+
case "running":
|
|
103
|
+
return "started";
|
|
104
|
+
case "completed":
|
|
105
|
+
return session.gitFlow ? void 0 : "completed";
|
|
106
|
+
case "waiting_for_input":
|
|
107
|
+
return "needs_input";
|
|
108
|
+
case "failed":
|
|
109
|
+
return "failed";
|
|
110
|
+
default:
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
var ServerPush = class {
|
|
115
|
+
constructor(store2, serverId, directory, transport = fetch, apiUrl = PUSH_API_URL) {
|
|
116
|
+
this.store = store2;
|
|
117
|
+
this.serverId = serverId;
|
|
118
|
+
this.transport = transport;
|
|
119
|
+
this.apiUrl = apiUrl;
|
|
120
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
121
|
+
const filename = path.join(directory, "push-signing-key.pem");
|
|
122
|
+
if (!existsSync(filename))
|
|
123
|
+
writeFileSync(
|
|
124
|
+
filename,
|
|
125
|
+
generateKeyPairSync("ed25519").privateKey.export({ format: "pem", type: "pkcs8" }),
|
|
126
|
+
{ mode: 384, flag: "wx" }
|
|
127
|
+
);
|
|
128
|
+
this.key = createPrivateKey(readFileSync(filename));
|
|
129
|
+
this.publicKey = createPublicKey(this.key).export({ format: "der", type: "spki" }).subarray(-32).toString("hex");
|
|
130
|
+
}
|
|
131
|
+
store;
|
|
132
|
+
serverId;
|
|
133
|
+
transport;
|
|
134
|
+
apiUrl;
|
|
135
|
+
key;
|
|
136
|
+
publicKey;
|
|
137
|
+
draining;
|
|
138
|
+
timer;
|
|
139
|
+
stopped = false;
|
|
140
|
+
pending(session, previous) {
|
|
141
|
+
const kind = pushKind(session, previous);
|
|
142
|
+
const state = Effect.runSync(this.store.pushState());
|
|
143
|
+
if (!kind || !state.bindings.length) return void 0;
|
|
144
|
+
return {
|
|
145
|
+
eventId: randomUUID(),
|
|
146
|
+
sessionId: session.id,
|
|
147
|
+
kind,
|
|
148
|
+
taskTitle: session.title.replace(/[\r\n\t]+/g, " ").trim().slice(0, 160) || void 0,
|
|
149
|
+
prUrl: kind === "pr_created" ? PushPullRequestUrlSchema.safeParse(session.gitDelivery?.url).data : void 0,
|
|
150
|
+
remaining: state.bindings,
|
|
151
|
+
createdAt: Date.now(),
|
|
152
|
+
attempts: 0,
|
|
153
|
+
nextAt: Date.now()
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
status(deviceId) {
|
|
157
|
+
const state = Effect.runSync(this.store.pushState());
|
|
158
|
+
return {
|
|
159
|
+
publicKey: this.publicKey,
|
|
160
|
+
bindings: state.bindings.filter((binding) => binding.deviceId === deviceId),
|
|
161
|
+
pending: Effect.runSync(this.store.pendingPush()).length,
|
|
162
|
+
error: state.error
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async send(installationId, bindingId, operation, payload) {
|
|
166
|
+
const request = { bindingId, timestamp: Date.now(), nonce: randomUUID(), payload };
|
|
167
|
+
const signature = sign(
|
|
168
|
+
null,
|
|
169
|
+
Buffer.from(pushSigningText(installationId, operation, request)),
|
|
170
|
+
this.key
|
|
171
|
+
).toString("hex");
|
|
172
|
+
return this.transport(`${this.apiUrl}/v1/installations/${installationId}/${operation}`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "Content-Type": "application/json" },
|
|
175
|
+
body: JSON.stringify({ ...request, signature }),
|
|
176
|
+
signal: AbortSignal.timeout(1e4),
|
|
177
|
+
redirect: "error"
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async bind(grant, deviceId) {
|
|
181
|
+
if (grant.serverId !== this.serverId || grant.publicKey !== this.publicKey || grant.expiresAt <= Date.now())
|
|
182
|
+
throw new Error("Grant does not match this server or has expired");
|
|
183
|
+
const existing = Effect.runSync(this.store.pushState());
|
|
184
|
+
if (existing.bindings.length >= 10 && !existing.bindings.some((binding) => binding.deviceId === deviceId))
|
|
185
|
+
throw new Error("Server notification binding limit reached");
|
|
186
|
+
const response = await this.send(
|
|
187
|
+
grant.installationId,
|
|
188
|
+
grant.bindingId,
|
|
189
|
+
"redeem",
|
|
190
|
+
JSON.stringify({ grant: grant.grant })
|
|
191
|
+
);
|
|
192
|
+
if (!response.ok)
|
|
193
|
+
throw new Error("Notification enrolment failed; enable push again to obtain a fresh grant");
|
|
194
|
+
const state = Effect.runSync(this.store.pushState());
|
|
195
|
+
state.bindings = state.bindings.filter((binding) => binding.deviceId !== deviceId);
|
|
196
|
+
state.bindings.push({
|
|
197
|
+
installationId: grant.installationId,
|
|
198
|
+
bindingId: grant.bindingId,
|
|
199
|
+
deviceId
|
|
200
|
+
});
|
|
201
|
+
state.error = null;
|
|
202
|
+
await Effect.runPromise(this.store.savePushState(state));
|
|
203
|
+
return this.status(deviceId);
|
|
204
|
+
}
|
|
205
|
+
unbind(deviceId) {
|
|
206
|
+
const state = Effect.runSync(this.store.pushState());
|
|
207
|
+
state.bindings = state.bindings.filter((binding) => binding.deviceId !== deviceId);
|
|
208
|
+
Effect.runSync(this.store.savePushState(state));
|
|
209
|
+
}
|
|
210
|
+
start() {
|
|
211
|
+
this.timer = setInterval(() => void this.flush(), 5e3);
|
|
212
|
+
this.timer.unref();
|
|
213
|
+
void this.flush();
|
|
214
|
+
}
|
|
215
|
+
flush() {
|
|
216
|
+
if (this.stopped) return Promise.resolve();
|
|
217
|
+
this.draining ??= this.drain().catch(() => {
|
|
218
|
+
const state = Effect.runSync(this.store.pushState());
|
|
219
|
+
state.error = "Notification delivery temporarily unavailable";
|
|
220
|
+
Effect.runSync(this.store.savePushState(state));
|
|
221
|
+
}).finally(() => {
|
|
222
|
+
this.draining = void 0;
|
|
223
|
+
});
|
|
224
|
+
return this.draining;
|
|
225
|
+
}
|
|
226
|
+
async drain() {
|
|
227
|
+
for (const job of Effect.runSync(this.store.pendingPush())) {
|
|
228
|
+
if (job.createdAt + 36e5 < Date.now() || job.attempts >= 12) {
|
|
229
|
+
Effect.runSync(this.store.removePush(job.eventId));
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (job.nextAt > Date.now()) continue;
|
|
233
|
+
const remaining = [];
|
|
234
|
+
for (const binding of job.remaining) {
|
|
235
|
+
const state2 = Effect.runSync(this.store.pushState());
|
|
236
|
+
const device = Effect.runSync(this.store.devices()).find(
|
|
237
|
+
(device2) => device2.id === binding.deviceId
|
|
238
|
+
);
|
|
239
|
+
if (!device || device.revokedAt !== null || !state2.bindings.some((current) => current.bindingId === binding.bindingId))
|
|
240
|
+
continue;
|
|
241
|
+
try {
|
|
242
|
+
const response = await this.send(
|
|
243
|
+
binding.installationId,
|
|
244
|
+
binding.bindingId,
|
|
245
|
+
"events",
|
|
246
|
+
JSON.stringify({
|
|
247
|
+
eventId: job.eventId,
|
|
248
|
+
sessionId: job.sessionId,
|
|
249
|
+
kind: job.kind,
|
|
250
|
+
taskTitle: job.taskTitle,
|
|
251
|
+
prUrl: job.prUrl
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
if (response.ok) continue;
|
|
255
|
+
if ([401, 403, 404].includes(response.status)) {
|
|
256
|
+
this.unbind(binding.deviceId);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
remaining.push(binding);
|
|
260
|
+
} catch {
|
|
261
|
+
remaining.push(binding);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (!remaining.length) Effect.runSync(this.store.removePush(job.eventId));
|
|
265
|
+
else
|
|
266
|
+
Effect.runSync(
|
|
267
|
+
this.store.savePendingPush({
|
|
268
|
+
...job,
|
|
269
|
+
remaining,
|
|
270
|
+
attempts: job.attempts + 1,
|
|
271
|
+
nextAt: Date.now() + Math.min(3e5, 5e3 * 2 ** job.attempts)
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
const state = Effect.runSync(this.store.pushState());
|
|
275
|
+
state.error = remaining.length ? "Notification delivery pending; check subscription and connectivity" : null;
|
|
276
|
+
Effect.runSync(this.store.savePushState(state));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async close() {
|
|
280
|
+
this.stopped = true;
|
|
281
|
+
clearInterval(this.timer);
|
|
282
|
+
await this.draining;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
function registerPushRoutes(app2, push2, security2) {
|
|
286
|
+
function deviceId(authorization) {
|
|
287
|
+
const device = security2.authenticate((authorization ?? "").slice(7));
|
|
288
|
+
if (!device) throw new Error("Pair this device");
|
|
289
|
+
return device.id;
|
|
290
|
+
}
|
|
291
|
+
app2.get("/api/push", (request) => push2.status(deviceId(request.headers.authorization)));
|
|
292
|
+
app2.post("/api/push/bind", async (request, reply) => {
|
|
293
|
+
const grant = PushGrantSchema.safeParse(request.body);
|
|
294
|
+
if (!grant.success) return reply.code(400).send({ error: "Invalid notification enrolment" });
|
|
295
|
+
try {
|
|
296
|
+
return await push2.bind(grant.data, deviceId(request.headers.authorization));
|
|
297
|
+
} catch {
|
|
298
|
+
return reply.code(400).send({ error: "Could not enable notifications. Check your subscription and try again." });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
app2.delete("/api/push/bind", (request) => {
|
|
302
|
+
push2.unbind(deviceId(request.headers.authorization));
|
|
303
|
+
return { disabled: true };
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
10
307
|
// src/index.ts
|
|
11
308
|
import Fastify from "fastify";
|
|
12
309
|
import pretty from "pino-pretty";
|
|
@@ -15,17 +312,17 @@ import cors from "@fastify/cors";
|
|
|
15
312
|
import multipart from "@fastify/multipart";
|
|
16
313
|
|
|
17
314
|
// src/attachments.ts
|
|
18
|
-
import { randomUUID } from "node:crypto";
|
|
315
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
19
316
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
20
317
|
import { mkdir, readFile, rm, stat, readdir } from "node:fs/promises";
|
|
21
|
-
import
|
|
318
|
+
import path2 from "node:path";
|
|
22
319
|
import { pipeline } from "node:stream/promises";
|
|
23
320
|
import sharp from "sharp";
|
|
24
|
-
import { Context, Data, Effect, Layer } from "effect";
|
|
25
|
-
import { z as
|
|
321
|
+
import { Context, Data, Effect as Effect2, Layer } from "effect";
|
|
322
|
+
import { z as z3 } from "zod";
|
|
26
323
|
|
|
27
324
|
// ../../packages/contracts/src/index.ts
|
|
28
|
-
import { z } from "zod";
|
|
325
|
+
import { z as z2 } from "zod";
|
|
29
326
|
var cronBounds = [
|
|
30
327
|
[0, 59],
|
|
31
328
|
[0, 23],
|
|
@@ -69,10 +366,10 @@ function cronMatches(expression, date) {
|
|
|
69
366
|
const day = fields[2].wildcard || fields[4].wildcard ? matches[2] && matches[4] : matches[2] || matches[4];
|
|
70
367
|
return Boolean(matches[0] && matches[1] && matches[3] && day);
|
|
71
368
|
}
|
|
72
|
-
var AutomationConfigSchema =
|
|
73
|
-
enabled:
|
|
74
|
-
parallelism:
|
|
75
|
-
cron:
|
|
369
|
+
var AutomationConfigSchema = z2.object({
|
|
370
|
+
enabled: z2.boolean(),
|
|
371
|
+
parallelism: z2.number().int().min(1).max(32),
|
|
372
|
+
cron: z2.string().trim().min(1).max(100).superRefine((value, context) => {
|
|
76
373
|
try {
|
|
77
374
|
cronFields(value);
|
|
78
375
|
} catch (error) {
|
|
@@ -82,15 +379,15 @@ var AutomationConfigSchema = z.object({
|
|
|
82
379
|
});
|
|
83
380
|
}
|
|
84
381
|
}),
|
|
85
|
-
projectIds:
|
|
382
|
+
projectIds: z2.array(z2.string().min(1).max(200)).max(1e3).refine((ids) => new Set(ids).size === ids.length, "Projects must be unique.")
|
|
86
383
|
});
|
|
87
|
-
var AutomationStateSchema =
|
|
384
|
+
var AutomationStateSchema = z2.object({
|
|
88
385
|
config: AutomationConfigSchema,
|
|
89
|
-
order:
|
|
90
|
-
lastMinute:
|
|
91
|
-
error:
|
|
386
|
+
order: z2.array(z2.string()),
|
|
387
|
+
lastMinute: z2.string().nullable(),
|
|
388
|
+
error: z2.string().nullable()
|
|
92
389
|
});
|
|
93
|
-
var SessionStatusSchema =
|
|
390
|
+
var SessionStatusSchema = z2.enum([
|
|
94
391
|
"queued",
|
|
95
392
|
"starting",
|
|
96
393
|
"running",
|
|
@@ -99,36 +396,36 @@ var SessionStatusSchema = z.enum([
|
|
|
99
396
|
"failed",
|
|
100
397
|
"stopped"
|
|
101
398
|
]);
|
|
102
|
-
var AgentKindSchema =
|
|
103
|
-
var ModelReferenceSchema =
|
|
104
|
-
platformId:
|
|
105
|
-
modelId:
|
|
399
|
+
var AgentKindSchema = z2.enum(["pi"]);
|
|
400
|
+
var ModelReferenceSchema = z2.object({
|
|
401
|
+
platformId: z2.string().min(1).max(200),
|
|
402
|
+
modelId: z2.string().min(1).max(500)
|
|
106
403
|
});
|
|
107
404
|
var ModelIdentitySchema = ModelReferenceSchema.extend({
|
|
108
|
-
name:
|
|
109
|
-
platformName:
|
|
110
|
-
providerId:
|
|
111
|
-
providerName:
|
|
405
|
+
name: z2.string(),
|
|
406
|
+
platformName: z2.string(),
|
|
407
|
+
providerId: z2.string(),
|
|
408
|
+
providerName: z2.string()
|
|
112
409
|
});
|
|
113
410
|
var ModelOptionSchema = ModelIdentitySchema.extend({
|
|
114
|
-
available:
|
|
115
|
-
reasoning:
|
|
116
|
-
input:
|
|
117
|
-
contextWindow:
|
|
411
|
+
available: z2.boolean(),
|
|
412
|
+
reasoning: z2.boolean(),
|
|
413
|
+
input: z2.array(z2.enum(["text", "image"])),
|
|
414
|
+
contextWindow: z2.number()
|
|
118
415
|
});
|
|
119
|
-
var ModelCatalogSchema =
|
|
120
|
-
models:
|
|
416
|
+
var ModelCatalogSchema = z2.object({
|
|
417
|
+
models: z2.array(ModelOptionSchema),
|
|
121
418
|
defaultModel: ModelIdentitySchema.nullable(),
|
|
122
|
-
refreshedAt:
|
|
123
|
-
warning:
|
|
419
|
+
refreshedAt: z2.string().datetime(),
|
|
420
|
+
warning: z2.string().nullable()
|
|
124
421
|
});
|
|
125
|
-
var GitFlowSchema =
|
|
126
|
-
workspace:
|
|
127
|
-
delivery:
|
|
422
|
+
var GitFlowSchema = z2.object({
|
|
423
|
+
workspace: z2.enum(["checkout", "worktree"]),
|
|
424
|
+
delivery: z2.enum(["merge", "pull_request"])
|
|
128
425
|
});
|
|
129
|
-
var GitDeliverySchema =
|
|
130
|
-
status:
|
|
131
|
-
stage:
|
|
426
|
+
var GitDeliverySchema = z2.object({
|
|
427
|
+
status: z2.enum(["pending", "waiting", "merged", "no_changes", "pr_opened", "failed"]),
|
|
428
|
+
stage: z2.enum([
|
|
132
429
|
"validating",
|
|
133
430
|
"drafting",
|
|
134
431
|
"committing",
|
|
@@ -140,165 +437,165 @@ var GitDeliverySchema = z.object({
|
|
|
140
437
|
"blocked",
|
|
141
438
|
"delivered"
|
|
142
439
|
]).optional(),
|
|
143
|
-
commitSha:
|
|
144
|
-
attempt:
|
|
145
|
-
logs:
|
|
146
|
-
url:
|
|
147
|
-
message:
|
|
440
|
+
commitSha: z2.string().optional(),
|
|
441
|
+
attempt: z2.number().int().positive().optional(),
|
|
442
|
+
logs: z2.string().optional(),
|
|
443
|
+
url: z2.string().url().optional(),
|
|
444
|
+
message: z2.string().optional()
|
|
148
445
|
});
|
|
149
|
-
var DeliveryRequestSchema =
|
|
150
|
-
summary:
|
|
151
|
-
verification:
|
|
446
|
+
var DeliveryRequestSchema = z2.object({
|
|
447
|
+
summary: z2.string().trim().min(1).max(1e3),
|
|
448
|
+
verification: z2.array(z2.string().max(1e3)).max(20).default([])
|
|
152
449
|
});
|
|
153
|
-
var RunPreparationSchema =
|
|
154
|
-
status:
|
|
155
|
-
message:
|
|
156
|
-
logs:
|
|
450
|
+
var RunPreparationSchema = z2.object({
|
|
451
|
+
status: z2.enum(["creating", "installing", "ready", "failed"]),
|
|
452
|
+
message: z2.string(),
|
|
453
|
+
logs: z2.string().optional()
|
|
157
454
|
});
|
|
158
|
-
var SessionSchema =
|
|
159
|
-
id:
|
|
160
|
-
taskId:
|
|
161
|
-
projectId:
|
|
162
|
-
title:
|
|
455
|
+
var SessionSchema = z2.object({
|
|
456
|
+
id: z2.string(),
|
|
457
|
+
taskId: z2.string().nullable().optional(),
|
|
458
|
+
projectId: z2.string(),
|
|
459
|
+
title: z2.string(),
|
|
163
460
|
status: SessionStatusSchema,
|
|
164
461
|
agent: AgentKindSchema,
|
|
165
462
|
model: ModelIdentitySchema.optional(),
|
|
166
|
-
branch:
|
|
167
|
-
workspacePath:
|
|
463
|
+
branch: z2.string().nullable(),
|
|
464
|
+
workspacePath: z2.string().optional(),
|
|
168
465
|
gitFlow: GitFlowSchema.optional(),
|
|
169
466
|
gitDelivery: GitDeliverySchema.optional(),
|
|
170
467
|
preparation: RunPreparationSchema.optional(),
|
|
171
|
-
createdAt:
|
|
172
|
-
updatedAt:
|
|
468
|
+
createdAt: z2.string().datetime(),
|
|
469
|
+
updatedAt: z2.string().datetime()
|
|
173
470
|
});
|
|
174
471
|
var MAX_ATTACHMENTS = 5;
|
|
175
472
|
var MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
176
|
-
var AttachmentSchema =
|
|
177
|
-
id:
|
|
178
|
-
projectId:
|
|
179
|
-
name:
|
|
180
|
-
mimeType:
|
|
181
|
-
size:
|
|
182
|
-
kind:
|
|
183
|
-
createdAt:
|
|
473
|
+
var AttachmentSchema = z2.object({
|
|
474
|
+
id: z2.string().uuid(),
|
|
475
|
+
projectId: z2.string(),
|
|
476
|
+
name: z2.string(),
|
|
477
|
+
mimeType: z2.string(),
|
|
478
|
+
size: z2.number().int().nonnegative(),
|
|
479
|
+
kind: z2.enum(["image", "text"]),
|
|
480
|
+
createdAt: z2.string().datetime()
|
|
184
481
|
});
|
|
185
|
-
var AttachmentIdsSchema =
|
|
186
|
-
var CreateSessionSchema =
|
|
187
|
-
projectId:
|
|
188
|
-
taskId:
|
|
189
|
-
prompt:
|
|
482
|
+
var AttachmentIdsSchema = z2.array(z2.string().uuid()).max(MAX_ATTACHMENTS).refine((ids) => new Set(ids).size === ids.length, "Choose each attachment once.");
|
|
483
|
+
var CreateSessionSchema = z2.object({
|
|
484
|
+
projectId: z2.string().min(1),
|
|
485
|
+
taskId: z2.string().optional(),
|
|
486
|
+
prompt: z2.string().min(1),
|
|
190
487
|
agent: AgentKindSchema.default("pi"),
|
|
191
488
|
model: ModelReferenceSchema.optional(),
|
|
192
489
|
gitFlow: GitFlowSchema.optional(),
|
|
193
490
|
attachmentIds: AttachmentIdsSchema.optional()
|
|
194
491
|
});
|
|
195
|
-
var SendMessageSchema =
|
|
196
|
-
message:
|
|
492
|
+
var SendMessageSchema = z2.object({
|
|
493
|
+
message: z2.string().trim().max(2e4),
|
|
197
494
|
attachmentIds: AttachmentIdsSchema.optional(),
|
|
198
|
-
clientMessageId:
|
|
495
|
+
clientMessageId: z2.string().min(1).max(120).optional()
|
|
199
496
|
}).refine(
|
|
200
497
|
(input) => input.message.length > 0 || (input.attachmentIds?.length ?? 0) > 0,
|
|
201
498
|
"Write a message or attach a file."
|
|
202
499
|
);
|
|
203
|
-
var SessionEventSchema =
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
type:
|
|
207
|
-
sessionId:
|
|
208
|
-
messageId:
|
|
209
|
-
text:
|
|
210
|
-
attachments:
|
|
500
|
+
var SessionEventSchema = z2.discriminatedUnion("type", [
|
|
501
|
+
z2.object({ type: z2.literal("session.updated"), session: SessionSchema }),
|
|
502
|
+
z2.object({
|
|
503
|
+
type: z2.literal("message.user"),
|
|
504
|
+
sessionId: z2.string(),
|
|
505
|
+
messageId: z2.string().optional(),
|
|
506
|
+
text: z2.string(),
|
|
507
|
+
attachments: z2.array(AttachmentSchema).optional()
|
|
211
508
|
}),
|
|
212
|
-
|
|
213
|
-
type:
|
|
509
|
+
z2.object({
|
|
510
|
+
type: z2.literal("message.started"),
|
|
214
511
|
model: ModelIdentitySchema.optional(),
|
|
215
|
-
sessionId:
|
|
216
|
-
messageId:
|
|
512
|
+
sessionId: z2.string(),
|
|
513
|
+
messageId: z2.string().optional()
|
|
217
514
|
}),
|
|
218
|
-
|
|
219
|
-
type:
|
|
515
|
+
z2.object({
|
|
516
|
+
type: z2.literal("message.delta"),
|
|
220
517
|
model: ModelIdentitySchema.optional(),
|
|
221
|
-
sessionId:
|
|
222
|
-
messageId:
|
|
223
|
-
text:
|
|
518
|
+
sessionId: z2.string(),
|
|
519
|
+
messageId: z2.string().optional(),
|
|
520
|
+
text: z2.string()
|
|
224
521
|
}),
|
|
225
|
-
|
|
226
|
-
type:
|
|
522
|
+
z2.object({
|
|
523
|
+
type: z2.literal("message.completed"),
|
|
227
524
|
model: ModelIdentitySchema.optional(),
|
|
228
|
-
sessionId:
|
|
229
|
-
messageId:
|
|
230
|
-
text:
|
|
525
|
+
sessionId: z2.string(),
|
|
526
|
+
messageId: z2.string().optional(),
|
|
527
|
+
text: z2.string()
|
|
231
528
|
}),
|
|
232
|
-
|
|
233
|
-
type:
|
|
234
|
-
sessionId:
|
|
235
|
-
toolCallId:
|
|
236
|
-
tool:
|
|
237
|
-
input:
|
|
529
|
+
z2.object({
|
|
530
|
+
type: z2.literal("tool.started"),
|
|
531
|
+
sessionId: z2.string(),
|
|
532
|
+
toolCallId: z2.string().optional(),
|
|
533
|
+
tool: z2.string(),
|
|
534
|
+
input: z2.string()
|
|
238
535
|
}),
|
|
239
|
-
|
|
240
|
-
type:
|
|
241
|
-
sessionId:
|
|
242
|
-
toolCallId:
|
|
243
|
-
tool:
|
|
244
|
-
output:
|
|
245
|
-
isError:
|
|
536
|
+
z2.object({
|
|
537
|
+
type: z2.literal("tool.completed"),
|
|
538
|
+
sessionId: z2.string(),
|
|
539
|
+
toolCallId: z2.string().optional(),
|
|
540
|
+
tool: z2.string(),
|
|
541
|
+
output: z2.string(),
|
|
542
|
+
isError: z2.boolean().default(false)
|
|
246
543
|
}),
|
|
247
|
-
|
|
248
|
-
type:
|
|
249
|
-
sessionId:
|
|
250
|
-
state:
|
|
544
|
+
z2.object({
|
|
545
|
+
type: z2.literal("activity.updated"),
|
|
546
|
+
sessionId: z2.string(),
|
|
547
|
+
state: z2.enum(["thinking", "responding", "idle"])
|
|
251
548
|
}),
|
|
252
|
-
|
|
549
|
+
z2.object({ type: z2.literal("error"), sessionId: z2.string(), message: z2.string() })
|
|
253
550
|
]);
|
|
254
|
-
var RecordedEventSchema =
|
|
255
|
-
sequence:
|
|
256
|
-
recordedAt:
|
|
551
|
+
var RecordedEventSchema = z2.object({
|
|
552
|
+
sequence: z2.number().int().positive(),
|
|
553
|
+
recordedAt: z2.string().datetime(),
|
|
257
554
|
event: SessionEventSchema
|
|
258
555
|
});
|
|
259
|
-
var EventHistorySchema =
|
|
260
|
-
events:
|
|
261
|
-
cursor:
|
|
262
|
-
hasMore:
|
|
556
|
+
var EventHistorySchema = z2.object({
|
|
557
|
+
events: z2.array(RecordedEventSchema),
|
|
558
|
+
cursor: z2.number().int().nonnegative(),
|
|
559
|
+
hasMore: z2.boolean()
|
|
263
560
|
});
|
|
264
|
-
var ServerEventSchema =
|
|
265
|
-
|
|
266
|
-
|
|
561
|
+
var ServerEventSchema = z2.union([
|
|
562
|
+
z2.object({ type: z2.literal("snapshot"), sessions: z2.array(SessionSchema) }),
|
|
563
|
+
z2.object({ type: z2.literal("event"), record: RecordedEventSchema })
|
|
267
564
|
]);
|
|
268
|
-
var ServerSettingsSchema =
|
|
269
|
-
projectsRoot:
|
|
565
|
+
var ServerSettingsSchema = z2.object({
|
|
566
|
+
projectsRoot: z2.string().min(1),
|
|
270
567
|
defaultModel: ModelIdentitySchema.nullable().optional()
|
|
271
568
|
});
|
|
272
|
-
var ConnectionsStatusSchema =
|
|
273
|
-
githubCli:
|
|
569
|
+
var ConnectionsStatusSchema = z2.object({
|
|
570
|
+
githubCli: z2.object({ installed: z2.boolean(), authenticated: z2.boolean() })
|
|
274
571
|
});
|
|
275
|
-
var UpdateSettingsSchema =
|
|
276
|
-
var ProjectSchema =
|
|
277
|
-
id:
|
|
278
|
-
name:
|
|
279
|
-
path:
|
|
280
|
-
available:
|
|
572
|
+
var UpdateSettingsSchema = z2.object({ projectsRoot: z2.string().trim().min(1).max(4096) });
|
|
573
|
+
var ProjectSchema = z2.object({
|
|
574
|
+
id: z2.string(),
|
|
575
|
+
name: z2.string(),
|
|
576
|
+
path: z2.string(),
|
|
577
|
+
available: z2.boolean().default(true)
|
|
281
578
|
});
|
|
282
|
-
var TaskStatusSchema =
|
|
283
|
-
var TaskSchema =
|
|
284
|
-
id:
|
|
285
|
-
projectId:
|
|
286
|
-
title:
|
|
287
|
-
prompt:
|
|
288
|
-
attachments:
|
|
579
|
+
var TaskStatusSchema = z2.enum(["backlog", "needs_user_input", "running", "done"]);
|
|
580
|
+
var TaskSchema = z2.object({
|
|
581
|
+
id: z2.string(),
|
|
582
|
+
projectId: z2.string(),
|
|
583
|
+
title: z2.string(),
|
|
584
|
+
prompt: z2.string(),
|
|
585
|
+
attachments: z2.array(AttachmentSchema).optional(),
|
|
289
586
|
model: ModelReferenceSchema.optional(),
|
|
290
587
|
gitFlow: GitFlowSchema.optional(),
|
|
291
588
|
status: TaskStatusSchema,
|
|
292
|
-
sessionId:
|
|
293
|
-
pullRequestUrl:
|
|
294
|
-
createdAt:
|
|
295
|
-
updatedAt:
|
|
296
|
-
deletedAt:
|
|
589
|
+
sessionId: z2.string().nullable(),
|
|
590
|
+
pullRequestUrl: z2.string().url().optional(),
|
|
591
|
+
createdAt: z2.string().datetime(),
|
|
592
|
+
updatedAt: z2.string().datetime(),
|
|
593
|
+
deletedAt: z2.string().datetime().optional()
|
|
297
594
|
});
|
|
298
|
-
var CreateTaskSchema =
|
|
299
|
-
projectId:
|
|
300
|
-
title:
|
|
301
|
-
prompt:
|
|
595
|
+
var CreateTaskSchema = z2.object({
|
|
596
|
+
projectId: z2.string().min(1),
|
|
597
|
+
title: z2.string().trim().min(1).max(120),
|
|
598
|
+
prompt: z2.string().trim().max(2e4).default(""),
|
|
302
599
|
attachmentIds: AttachmentIdsSchema.optional(),
|
|
303
600
|
model: ModelReferenceSchema.optional(),
|
|
304
601
|
gitFlow: GitFlowSchema.optional()
|
|
@@ -310,57 +607,62 @@ var UpdateTaskSchema = CreateTaskSchema.pick({
|
|
|
310
607
|
gitFlow: true
|
|
311
608
|
}).extend({ gitFlow: GitFlowSchema.nullable().optional() }).strict();
|
|
312
609
|
var PROTOCOL_VERSION = 1;
|
|
313
|
-
var ServerUpdateStatusSchema =
|
|
314
|
-
currentVersion:
|
|
315
|
-
latestVersion:
|
|
316
|
-
available:
|
|
317
|
-
published:
|
|
318
|
-
canAutoUpdate:
|
|
319
|
-
checkedAt:
|
|
320
|
-
error:
|
|
610
|
+
var ServerUpdateStatusSchema = z2.object({
|
|
611
|
+
currentVersion: z2.string(),
|
|
612
|
+
latestVersion: z2.string().optional(),
|
|
613
|
+
available: z2.boolean(),
|
|
614
|
+
published: z2.boolean().nullable(),
|
|
615
|
+
canAutoUpdate: z2.literal(false),
|
|
616
|
+
checkedAt: z2.string().datetime().nullable(),
|
|
617
|
+
error: z2.string().nullable()
|
|
321
618
|
});
|
|
322
|
-
var ServerHealthSchema =
|
|
323
|
-
status:
|
|
324
|
-
service:
|
|
325
|
-
serverId:
|
|
326
|
-
serverVersion:
|
|
327
|
-
protocolVersion:
|
|
328
|
-
authentication:
|
|
619
|
+
var ServerHealthSchema = z2.object({
|
|
620
|
+
status: z2.literal("ok"),
|
|
621
|
+
service: z2.literal("agentrific-server"),
|
|
622
|
+
serverId: z2.string().uuid(),
|
|
623
|
+
serverVersion: z2.string(),
|
|
624
|
+
protocolVersion: z2.literal(PROTOCOL_VERSION),
|
|
625
|
+
authentication: z2.literal("pairing")
|
|
329
626
|
});
|
|
330
|
-
var PairingPayloadSchema =
|
|
331
|
-
v:
|
|
332
|
-
url:
|
|
333
|
-
serverId:
|
|
334
|
-
pairingId:
|
|
627
|
+
var PairingPayloadSchema = z2.object({
|
|
628
|
+
v: z2.literal(1),
|
|
629
|
+
url: z2.string().url(),
|
|
630
|
+
serverId: z2.string().uuid(),
|
|
631
|
+
pairingId: z2.string().regex(/^[a-f0-9]{64}$/)
|
|
335
632
|
});
|
|
336
|
-
var
|
|
337
|
-
|
|
338
|
-
pin:
|
|
339
|
-
|
|
340
|
-
platform: z.enum(["ios", "android", "web"])
|
|
633
|
+
var PairingInvitationSchema = z2.object({
|
|
634
|
+
payload: PairingPayloadSchema,
|
|
635
|
+
pin: z2.string().regex(/^\d{6}$/),
|
|
636
|
+
expiresAt: z2.number()
|
|
341
637
|
});
|
|
342
|
-
var
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
lastSeenAt: z.number(),
|
|
348
|
-
revokedAt: z.number().nullable()
|
|
638
|
+
var PairingRequestSchema = z2.object({
|
|
639
|
+
pairingId: z2.string().regex(/^[a-f0-9]{64}$/),
|
|
640
|
+
pin: z2.string().regex(/^\d{6}$/),
|
|
641
|
+
deviceName: z2.string().trim().min(1).max(100),
|
|
642
|
+
platform: z2.enum(["ios", "android", "web"])
|
|
349
643
|
});
|
|
350
|
-
var
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
644
|
+
var DeviceSchema = z2.object({
|
|
645
|
+
id: z2.string().uuid(),
|
|
646
|
+
name: z2.string(),
|
|
647
|
+
platform: z2.enum(["ios", "android", "web"]),
|
|
648
|
+
createdAt: z2.number(),
|
|
649
|
+
lastSeenAt: z2.number(),
|
|
650
|
+
revokedAt: z2.number().nullable()
|
|
354
651
|
});
|
|
355
|
-
var
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
652
|
+
var PairingResultSchema = z2.object({
|
|
653
|
+
deviceId: z2.string().uuid(),
|
|
654
|
+
deviceToken: z2.string().regex(/^[a-f0-9]{64}$/),
|
|
655
|
+
serverId: z2.string().uuid()
|
|
359
656
|
});
|
|
360
|
-
var
|
|
361
|
-
type:
|
|
362
|
-
|
|
363
|
-
protocolVersion:
|
|
657
|
+
var HandshakeSchema = z2.object({
|
|
658
|
+
type: z2.literal("client.handshake"),
|
|
659
|
+
ticket: z2.string().regex(/^[a-f0-9]{64}$/),
|
|
660
|
+
protocolVersion: z2.literal(PROTOCOL_VERSION)
|
|
661
|
+
});
|
|
662
|
+
var HandshakeAckSchema = z2.object({
|
|
663
|
+
type: z2.literal("handshake.ack"),
|
|
664
|
+
serverId: z2.string().uuid(),
|
|
665
|
+
protocolVersion: z2.literal(PROTOCOL_VERSION)
|
|
364
666
|
});
|
|
365
667
|
|
|
366
668
|
// src/attachments.ts
|
|
@@ -413,10 +715,10 @@ var AttachmentFiles = class {
|
|
|
413
715
|
root;
|
|
414
716
|
store;
|
|
415
717
|
directory(id) {
|
|
416
|
-
return
|
|
718
|
+
return path2.join(this.root, z3.string().uuid().parse(id));
|
|
417
719
|
}
|
|
418
720
|
io(operation) {
|
|
419
|
-
return
|
|
721
|
+
return Effect2.tryPromise({
|
|
420
722
|
try: operation,
|
|
421
723
|
catch: (cause) => new AttachmentError({
|
|
422
724
|
message: cause instanceof Error ? cause.message : "Attachment operation failed."
|
|
@@ -425,12 +727,12 @@ var AttachmentFiles = class {
|
|
|
425
727
|
}
|
|
426
728
|
upload(projectId, part) {
|
|
427
729
|
return this.io(async () => {
|
|
428
|
-
const id =
|
|
730
|
+
const id = randomUUID2();
|
|
429
731
|
const directory = this.directory(id);
|
|
430
|
-
const source =
|
|
431
|
-
const nameField =
|
|
732
|
+
const source = path2.join(directory, "source");
|
|
733
|
+
const nameField = z3.object({ type: z3.literal("field"), value: z3.string() }).safeParse(part.fields.name);
|
|
432
734
|
const originalName = nameField.success ? nameField.data.value : part.filename;
|
|
433
|
-
const name =
|
|
735
|
+
const name = path2.basename(originalName.replaceAll("\\", "/")).replace(new RegExp("\\p{Cc}", "gu"), "").slice(0, 180) || "attachment";
|
|
434
736
|
await mkdir(directory, { recursive: true });
|
|
435
737
|
try {
|
|
436
738
|
await pipeline(part.file, createWriteStream(source, { flags: "wx" }));
|
|
@@ -446,7 +748,7 @@ var AttachmentFiles = class {
|
|
|
446
748
|
...metadata,
|
|
447
749
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
448
750
|
});
|
|
449
|
-
await
|
|
751
|
+
await Effect2.runPromise(this.store.saveAttachment(attachment));
|
|
450
752
|
return attachment;
|
|
451
753
|
} catch (error) {
|
|
452
754
|
await rm(directory, { recursive: true, force: true });
|
|
@@ -458,10 +760,10 @@ var AttachmentFiles = class {
|
|
|
458
760
|
const metadata = await sharp(source, { limitInputPixels: 4e7 }).metadata().catch(() => null);
|
|
459
761
|
const mimeType = metadata?.format && imageMimes.get(metadata.format);
|
|
460
762
|
if (mimeType) {
|
|
461
|
-
await sharp(source, { limitInputPixels: 4e7 }).rotate().resize({ width: 2048, height: 2048, fit: "inside", withoutEnlargement: true }).png().toFile(
|
|
763
|
+
await sharp(source, { limitInputPixels: 4e7 }).rotate().resize({ width: 2048, height: 2048, fit: "inside", withoutEnlargement: true }).png().toFile(path2.join(path2.dirname(source), "preview.png"));
|
|
462
764
|
return { kind: "image", mimeType };
|
|
463
765
|
}
|
|
464
|
-
if (!textExtensions.has(
|
|
766
|
+
if (!textExtensions.has(path2.extname(name).toLowerCase()))
|
|
465
767
|
throw new Error(
|
|
466
768
|
"Choose a PNG, JPEG, WebP, GIF, or text/code file. PDF and Office documents aren't supported yet."
|
|
467
769
|
);
|
|
@@ -477,12 +779,12 @@ var AttachmentFiles = class {
|
|
|
477
779
|
throw new Error("Choose up to five different files.");
|
|
478
780
|
const attachments2 = [];
|
|
479
781
|
for (const id of ids) {
|
|
480
|
-
const attachment = await
|
|
782
|
+
const attachment = await Effect2.runPromise(this.store.getAttachment(id));
|
|
481
783
|
if (!attachment || attachment.projectId !== projectId)
|
|
482
784
|
throw new Error(
|
|
483
785
|
"An attachment is unavailable for this project. Remove it and upload it again."
|
|
484
786
|
);
|
|
485
|
-
await stat(
|
|
787
|
+
await stat(path2.join(this.directory(id), "source"));
|
|
486
788
|
attachments2.push(attachment);
|
|
487
789
|
}
|
|
488
790
|
return attachments2;
|
|
@@ -490,7 +792,7 @@ var AttachmentFiles = class {
|
|
|
490
792
|
}
|
|
491
793
|
content(attachment, preview) {
|
|
492
794
|
return createReadStream(
|
|
493
|
-
|
|
795
|
+
path2.join(
|
|
494
796
|
this.directory(attachment.id),
|
|
495
797
|
preview && attachment.kind === "image" ? "preview.png" : "source"
|
|
496
798
|
)
|
|
@@ -501,9 +803,9 @@ var AttachmentFiles = class {
|
|
|
501
803
|
const images = [];
|
|
502
804
|
const sections = [];
|
|
503
805
|
for (const attachment of attachments2) {
|
|
504
|
-
const source =
|
|
806
|
+
const source = path2.join(this.directory(attachment.id), "source");
|
|
505
807
|
if (attachment.kind === "image") {
|
|
506
|
-
const bytes = await readFile(
|
|
808
|
+
const bytes = await readFile(path2.join(this.directory(attachment.id), "preview.png"));
|
|
507
809
|
images.push({ type: "image", data: bytes.toString("base64"), mimeType: "image/png" });
|
|
508
810
|
sections.push(`Image attachment: ${JSON.stringify(attachment.name)}`);
|
|
509
811
|
} else {
|
|
@@ -526,18 +828,18 @@ ${text.slice(0, 12e3)}${text.length > 12e3 ? "\n[Preview truncated; use the read
|
|
|
526
828
|
cleanup() {
|
|
527
829
|
return this.io(async () => {
|
|
528
830
|
const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
|
|
529
|
-
const expired = await
|
|
831
|
+
const expired = await Effect2.runPromise(
|
|
530
832
|
this.store.expiredAttachments(new Date(cutoff).toISOString())
|
|
531
833
|
);
|
|
532
834
|
for (const attachment of expired) {
|
|
533
|
-
if (await
|
|
835
|
+
if (await Effect2.runPromise(this.store.deleteAttachment(attachment.id)))
|
|
534
836
|
await rm(this.directory(attachment.id), { recursive: true, force: true });
|
|
535
837
|
}
|
|
536
838
|
await mkdir(this.root, { recursive: true });
|
|
537
839
|
for (const entry of await readdir(this.root, { withFileTypes: true })) {
|
|
538
|
-
if (!entry.isDirectory() || !
|
|
840
|
+
if (!entry.isDirectory() || !z3.string().uuid().safeParse(entry.name).success) continue;
|
|
539
841
|
const directory = this.directory(entry.name);
|
|
540
|
-
if ((await stat(directory)).mtimeMs < cutoff && !await
|
|
842
|
+
if ((await stat(directory)).mtimeMs < cutoff && !await Effect2.runPromise(this.store.getAttachment(entry.name)))
|
|
541
843
|
await rm(directory, { recursive: true, force: true });
|
|
542
844
|
}
|
|
543
845
|
});
|
|
@@ -552,12 +854,12 @@ var Attachments = class extends Context.Service()(
|
|
|
552
854
|
};
|
|
553
855
|
|
|
554
856
|
// src/index.ts
|
|
555
|
-
import { randomUUID as
|
|
857
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
556
858
|
import { execFile as execFile2 } from "node:child_process";
|
|
557
859
|
import { promisify as promisify2 } from "node:util";
|
|
558
860
|
import { realpath as realpath2 } from "node:fs/promises";
|
|
559
861
|
import { homedir as homedir2 } from "node:os";
|
|
560
|
-
import
|
|
862
|
+
import path9 from "node:path";
|
|
561
863
|
import { WebSocket } from "ws";
|
|
562
864
|
|
|
563
865
|
// src/event-server.ts
|
|
@@ -624,153 +926,18 @@ function attachEventServer(app2, security2, origins, snapshot) {
|
|
|
624
926
|
}
|
|
625
927
|
|
|
626
928
|
// src/security.ts
|
|
627
|
-
import { createHash, randomBytes, randomInt, randomUUID as
|
|
628
|
-
import { Effect as
|
|
629
|
-
import { z as
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
this.store = store2;
|
|
635
|
-
this.serverId = serverId;
|
|
636
|
-
}
|
|
637
|
-
store;
|
|
638
|
-
serverId;
|
|
639
|
-
grants = /* @__PURE__ */ new Map();
|
|
640
|
-
tickets = /* @__PURE__ */ new Map();
|
|
641
|
-
attempts = /* @__PURE__ */ new Map();
|
|
642
|
-
sockets = /* @__PURE__ */ new Map();
|
|
643
|
-
prune() {
|
|
644
|
-
const now = Date.now();
|
|
645
|
-
for (const [key, grant] of this.grants) if (grant.expiresAt <= now) this.grants.delete(key);
|
|
646
|
-
for (const [key, ticket] of this.tickets) if (ticket.expiresAt <= now) this.tickets.delete(key);
|
|
647
|
-
for (const [key, attempts] of this.attempts)
|
|
648
|
-
if (attempts.until <= now) this.attempts.delete(key);
|
|
649
|
-
}
|
|
650
|
-
createPairing() {
|
|
651
|
-
this.prune();
|
|
652
|
-
if (this.grants.size >= 10)
|
|
653
|
-
throw new Error("Too many pending enrollments. Wait for them to expire.");
|
|
654
|
-
const pairingId = secret();
|
|
655
|
-
const grant = {
|
|
656
|
-
pin: String(randomInt(1e6)).padStart(6, "0"),
|
|
657
|
-
expiresAt: Date.now() + 3e5,
|
|
658
|
-
attempts: 0
|
|
659
|
-
};
|
|
660
|
-
this.grants.set(pairingId, grant);
|
|
661
|
-
return { pairingId, pin: grant.pin, expiresAt: grant.expiresAt, serverId: this.serverId };
|
|
662
|
-
}
|
|
663
|
-
async pair(input, address) {
|
|
664
|
-
this.prune();
|
|
665
|
-
const attempts = this.attempts.get(address) ?? { count: 0, until: Date.now() + 6e4 };
|
|
666
|
-
if (attempts.count >= 20 || !this.attempts.has(address) && this.attempts.size >= 5e3)
|
|
667
|
-
return void 0;
|
|
668
|
-
attempts.count++;
|
|
669
|
-
this.attempts.set(address, attempts);
|
|
670
|
-
const grant = this.grants.get(input.pairingId);
|
|
671
|
-
if (!grant) return void 0;
|
|
672
|
-
grant.attempts++;
|
|
673
|
-
const match = timingSafeEqual(Buffer.from(grant.pin), Buffer.from(input.pin));
|
|
674
|
-
if (grant.attempts >= 3 || match) this.grants.delete(input.pairingId);
|
|
675
|
-
if (!match) return void 0;
|
|
676
|
-
const token = secret();
|
|
677
|
-
const now = Date.now();
|
|
678
|
-
const device = {
|
|
679
|
-
id: randomUUID2(),
|
|
680
|
-
name: input.deviceName,
|
|
681
|
-
platform: input.platform,
|
|
682
|
-
createdAt: now,
|
|
683
|
-
lastSeenAt: now,
|
|
684
|
-
revokedAt: null
|
|
685
|
-
};
|
|
686
|
-
await Effect2.runPromise(this.store.saveDevice(device, digest(token)));
|
|
687
|
-
return { deviceId: device.id, deviceToken: token, serverId: this.serverId };
|
|
688
|
-
}
|
|
689
|
-
authenticate(token) {
|
|
690
|
-
if (!/^[a-f0-9]{64}$/.test(token)) return void 0;
|
|
691
|
-
return Effect2.runSync(this.store.deviceForToken(digest(token)));
|
|
692
|
-
}
|
|
693
|
-
ticket(token, path8) {
|
|
694
|
-
this.prune();
|
|
695
|
-
if (!this.authenticate(token) || this.tickets.size >= 1e4) return void 0;
|
|
696
|
-
const value = secret();
|
|
697
|
-
this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path8 });
|
|
698
|
-
return value;
|
|
699
|
-
}
|
|
700
|
-
redeemTicket(value, path8, consume = true) {
|
|
701
|
-
this.prune();
|
|
702
|
-
const ticket = this.tickets.get(value);
|
|
703
|
-
if (!ticket || ticket.path !== path8) return void 0;
|
|
704
|
-
if (consume) this.tickets.delete(value);
|
|
705
|
-
return Effect2.runSync(this.store.deviceForToken(ticket.tokenHash));
|
|
706
|
-
}
|
|
707
|
-
async revoke(id) {
|
|
708
|
-
const revoked = await Effect2.runPromise(this.store.revokeDevice(id));
|
|
709
|
-
for (const [socket, owner] of this.sockets)
|
|
710
|
-
if (owner === id) socket.close(4401, "Device revoked");
|
|
711
|
-
return revoked;
|
|
712
|
-
}
|
|
713
|
-
};
|
|
714
|
-
async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
715
|
-
app2.addHook("onRequest", async (request, reply) => {
|
|
716
|
-
const path8 = new URL(request.url, "http://localhost");
|
|
717
|
-
reply.header("Cache-Control", "no-store");
|
|
718
|
-
if (request.method === "OPTIONS") return;
|
|
719
|
-
if (request.headers.origin && !allowedOrigins2.includes(request.headers.origin))
|
|
720
|
-
return reply.code(403).send({ error: "Browser origin is not allowed." });
|
|
721
|
-
if (path8.pathname === "/health" || path8.pathname === "/api/auth/pair" && request.method === "POST")
|
|
722
|
-
return;
|
|
723
|
-
const authorization = request.headers.authorization ?? "";
|
|
724
|
-
const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
|
|
725
|
-
const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(path8.pathname);
|
|
726
|
-
const ticket = path8.searchParams.get("ticket");
|
|
727
|
-
path8.searchParams.delete("ticket");
|
|
728
|
-
if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path8.pathname + path8.search, false)))
|
|
729
|
-
return reply.code(401).send({ error: "Pair this device with your server.", code: "UNAUTHORIZED" });
|
|
730
|
-
if (!asset && request.headers["x-agentrific-protocol"] !== String(PROTOCOL_VERSION))
|
|
731
|
-
return reply.code(409).send({
|
|
732
|
-
error: "Update your Agentrific client or server.",
|
|
733
|
-
code: "VERSION_MISMATCH",
|
|
734
|
-
protocolVersion: PROTOCOL_VERSION
|
|
735
|
-
});
|
|
736
|
-
});
|
|
737
|
-
app2.get("/health", () => ({
|
|
738
|
-
status: "ok",
|
|
739
|
-
service: "agentrific-server",
|
|
740
|
-
serverId: security2.serverId,
|
|
741
|
-
serverVersion: version,
|
|
742
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
743
|
-
authentication: "pairing"
|
|
744
|
-
}));
|
|
745
|
-
app2.post("/api/auth/pair", async (request, reply) => {
|
|
746
|
-
const input = PairingRequestSchema.safeParse(request.body);
|
|
747
|
-
if (!input.success)
|
|
748
|
-
return reply.code(400).send({ error: "Enter a valid pairing payload and six-digit PIN." });
|
|
749
|
-
const result = await security2.pair(input.data, request.ip);
|
|
750
|
-
return result ?? reply.code(401).send({ error: "Pairing expired, locked, or invalid. Run agentrific pair again." });
|
|
751
|
-
});
|
|
752
|
-
app2.get("/api/auth/session", (request) => ({
|
|
753
|
-
serverId: security2.serverId,
|
|
754
|
-
device: security2.authenticate((request.headers.authorization ?? "").slice(7))
|
|
755
|
-
}));
|
|
756
|
-
app2.post("/api/auth/event-ticket", (request) => ({
|
|
757
|
-
ticket: security2.ticket((request.headers.authorization ?? "").slice(7), "/api/events")
|
|
758
|
-
}));
|
|
759
|
-
app2.post("/api/auth/asset-ticket", (request, reply) => {
|
|
760
|
-
const input = z3.object({
|
|
761
|
-
path: z3.string().regex(/^\/api\/projects\/[^/?]+\/attachments\/[^/?]+\/content(?:\?preview=1)?$/)
|
|
762
|
-
}).safeParse(request.body);
|
|
763
|
-
if (!input.success) return reply.code(400).send({ error: "Choose a valid attachment." });
|
|
764
|
-
return {
|
|
765
|
-
ticket: security2.ticket((request.headers.authorization ?? "").slice(7), input.data.path)
|
|
766
|
-
};
|
|
767
|
-
});
|
|
768
|
-
}
|
|
929
|
+
import { createHash, randomBytes, randomInt, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
930
|
+
import { Effect as Effect4 } from "effect";
|
|
931
|
+
import { z as z6 } from "zod";
|
|
932
|
+
|
|
933
|
+
// src/pairing.ts
|
|
934
|
+
import { z as z5 } from "zod";
|
|
935
|
+
import qrcode from "qrcode-terminal";
|
|
769
936
|
|
|
770
937
|
// src/control.ts
|
|
771
938
|
import { createServer, createConnection } from "node:net";
|
|
772
939
|
import { chmod, mkdir as mkdir2, readFile as readFile2, unlink, writeFile } from "node:fs/promises";
|
|
773
|
-
import
|
|
940
|
+
import path3 from "node:path";
|
|
774
941
|
import { Effect as Effect3 } from "effect";
|
|
775
942
|
import { z as z4 } from "zod";
|
|
776
943
|
var MaintenanceSchema = z4.object({
|
|
@@ -814,7 +981,7 @@ var ControlRequest = z4.discriminatedUnion("command", [
|
|
|
814
981
|
async function acquireServerLock(directory) {
|
|
815
982
|
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
816
983
|
await chmod(directory, 448);
|
|
817
|
-
const filename =
|
|
984
|
+
const filename = path3.join(directory, "server.lock");
|
|
818
985
|
try {
|
|
819
986
|
await writeFile(filename, String(process.pid), { flag: "wx", mode: 384 });
|
|
820
987
|
} catch (cause) {
|
|
@@ -853,7 +1020,7 @@ async function acquireServerLock(directory) {
|
|
|
853
1020
|
};
|
|
854
1021
|
}
|
|
855
1022
|
async function startControl(directory, security2, status, maintenance2) {
|
|
856
|
-
const filename =
|
|
1023
|
+
const filename = path3.join(directory, "control.sock");
|
|
857
1024
|
await unlink(filename).catch((error) => {
|
|
858
1025
|
if (!z4.object({ code: z4.literal("ENOENT") }).safeParse(error).success) throw error;
|
|
859
1026
|
});
|
|
@@ -914,7 +1081,7 @@ async function startControl(directory, security2, status, maintenance2) {
|
|
|
914
1081
|
// package.json
|
|
915
1082
|
var package_default = {
|
|
916
1083
|
name: "@agentrific/server",
|
|
917
|
-
version: "0.1.
|
|
1084
|
+
version: "0.1.6",
|
|
918
1085
|
description: "Self-hosted control plane for long-running coding agents",
|
|
919
1086
|
keywords: [
|
|
920
1087
|
"agentrific",
|
|
@@ -977,6 +1144,180 @@ var package_default = {
|
|
|
977
1144
|
// src/version.ts
|
|
978
1145
|
var serverVersion = package_default.version;
|
|
979
1146
|
|
|
1147
|
+
// src/pairing.ts
|
|
1148
|
+
function pairingOrigin(url) {
|
|
1149
|
+
const endpoint = new URL(url);
|
|
1150
|
+
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.pathname !== "/")
|
|
1151
|
+
throw new Error("Use the HTTPS server origin without a path or credentials.");
|
|
1152
|
+
if (endpoint.protocol !== "https:" && !(process.env.NODE_ENV !== "production" && endpoint.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname)))
|
|
1153
|
+
throw new Error("Use HTTPS. Localhost HTTP is for development builds only.");
|
|
1154
|
+
return endpoint.origin;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/security.ts
|
|
1158
|
+
var secret = () => randomBytes(32).toString("hex");
|
|
1159
|
+
var digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
1160
|
+
var ServerSecurity = class {
|
|
1161
|
+
constructor(store2, serverId) {
|
|
1162
|
+
this.store = store2;
|
|
1163
|
+
this.serverId = serverId;
|
|
1164
|
+
}
|
|
1165
|
+
store;
|
|
1166
|
+
serverId;
|
|
1167
|
+
grants = /* @__PURE__ */ new Map();
|
|
1168
|
+
tickets = /* @__PURE__ */ new Map();
|
|
1169
|
+
attempts = /* @__PURE__ */ new Map();
|
|
1170
|
+
sockets = /* @__PURE__ */ new Map();
|
|
1171
|
+
prune() {
|
|
1172
|
+
const now = Date.now();
|
|
1173
|
+
for (const [key, grant] of this.grants) if (grant.expiresAt <= now) this.grants.delete(key);
|
|
1174
|
+
for (const [key, ticket] of this.tickets) if (ticket.expiresAt <= now) this.tickets.delete(key);
|
|
1175
|
+
for (const [key, attempts] of this.attempts)
|
|
1176
|
+
if (attempts.until <= now) this.attempts.delete(key);
|
|
1177
|
+
}
|
|
1178
|
+
createPairing() {
|
|
1179
|
+
this.prune();
|
|
1180
|
+
if (this.grants.size >= 10)
|
|
1181
|
+
throw new Error("Too many pending enrollments. Wait for them to expire.");
|
|
1182
|
+
const pairingId = secret();
|
|
1183
|
+
const grant = {
|
|
1184
|
+
pin: String(randomInt(1e6)).padStart(6, "0"),
|
|
1185
|
+
expiresAt: Date.now() + 3e5,
|
|
1186
|
+
attempts: 0
|
|
1187
|
+
};
|
|
1188
|
+
this.grants.set(pairingId, grant);
|
|
1189
|
+
return { pairingId, pin: grant.pin, expiresAt: grant.expiresAt, serverId: this.serverId };
|
|
1190
|
+
}
|
|
1191
|
+
async pair(input, address) {
|
|
1192
|
+
this.prune();
|
|
1193
|
+
const attempts = this.attempts.get(address) ?? { count: 0, until: Date.now() + 6e4 };
|
|
1194
|
+
if (attempts.count >= 20 || !this.attempts.has(address) && this.attempts.size >= 5e3)
|
|
1195
|
+
return void 0;
|
|
1196
|
+
attempts.count++;
|
|
1197
|
+
this.attempts.set(address, attempts);
|
|
1198
|
+
const grant = this.grants.get(input.pairingId);
|
|
1199
|
+
if (!grant) return void 0;
|
|
1200
|
+
grant.attempts++;
|
|
1201
|
+
const match = timingSafeEqual(Buffer.from(grant.pin), Buffer.from(input.pin));
|
|
1202
|
+
if (grant.attempts >= 3 || match) this.grants.delete(input.pairingId);
|
|
1203
|
+
if (!match) return void 0;
|
|
1204
|
+
const token = secret();
|
|
1205
|
+
const now = Date.now();
|
|
1206
|
+
const device = {
|
|
1207
|
+
id: randomUUID3(),
|
|
1208
|
+
name: input.deviceName,
|
|
1209
|
+
platform: input.platform,
|
|
1210
|
+
createdAt: now,
|
|
1211
|
+
lastSeenAt: now,
|
|
1212
|
+
revokedAt: null
|
|
1213
|
+
};
|
|
1214
|
+
await Effect4.runPromise(this.store.saveDevice(device, digest(token)));
|
|
1215
|
+
return { deviceId: device.id, deviceToken: token, serverId: this.serverId };
|
|
1216
|
+
}
|
|
1217
|
+
authenticate(token) {
|
|
1218
|
+
if (!/^[a-f0-9]{64}$/.test(token)) return void 0;
|
|
1219
|
+
return Effect4.runSync(this.store.deviceForToken(digest(token)));
|
|
1220
|
+
}
|
|
1221
|
+
ticket(token, path10) {
|
|
1222
|
+
this.prune();
|
|
1223
|
+
if (!this.authenticate(token) || this.tickets.size >= 1e4) return void 0;
|
|
1224
|
+
const value = secret();
|
|
1225
|
+
this.tickets.set(value, { tokenHash: digest(token), expiresAt: Date.now() + 6e4, path: path10 });
|
|
1226
|
+
return value;
|
|
1227
|
+
}
|
|
1228
|
+
redeemTicket(value, path10, consume = true) {
|
|
1229
|
+
this.prune();
|
|
1230
|
+
const ticket = this.tickets.get(value);
|
|
1231
|
+
if (!ticket || ticket.path !== path10) return void 0;
|
|
1232
|
+
if (consume) this.tickets.delete(value);
|
|
1233
|
+
return Effect4.runSync(this.store.deviceForToken(ticket.tokenHash));
|
|
1234
|
+
}
|
|
1235
|
+
async revoke(id) {
|
|
1236
|
+
const revoked = await Effect4.runPromise(this.store.revokeDevice(id));
|
|
1237
|
+
for (const [socket, owner] of this.sockets)
|
|
1238
|
+
if (owner === id) socket.close(4401, "Device revoked");
|
|
1239
|
+
return revoked;
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
async function registerSecurity(app2, security2, version, allowedOrigins2) {
|
|
1243
|
+
app2.addHook("onRequest", async (request, reply) => {
|
|
1244
|
+
const path10 = new URL(request.url, "http://localhost");
|
|
1245
|
+
reply.header("Cache-Control", "no-store");
|
|
1246
|
+
if (request.method === "OPTIONS") return;
|
|
1247
|
+
if (request.headers.origin && !allowedOrigins2.includes(request.headers.origin))
|
|
1248
|
+
return reply.code(403).send({ error: "Browser origin is not allowed." });
|
|
1249
|
+
if (path10.pathname === "/health" || path10.pathname === "/api/auth/pair" && request.method === "POST")
|
|
1250
|
+
return;
|
|
1251
|
+
const authorization = request.headers.authorization ?? "";
|
|
1252
|
+
const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
|
|
1253
|
+
const asset = request.method === "GET" && /^\/api\/projects\/[^/]+\/attachments\/[^/]+\/content$/.test(path10.pathname);
|
|
1254
|
+
const ticket = path10.searchParams.get("ticket");
|
|
1255
|
+
path10.searchParams.delete("ticket");
|
|
1256
|
+
if (!security2.authenticate(token) && !(asset && ticket && security2.redeemTicket(ticket, path10.pathname + path10.search, false)))
|
|
1257
|
+
return reply.code(401).send({ error: "Pair this device with your server.", code: "UNAUTHORIZED" });
|
|
1258
|
+
if (!asset && request.headers["x-agentrific-protocol"] !== String(PROTOCOL_VERSION))
|
|
1259
|
+
return reply.code(409).send({
|
|
1260
|
+
error: "Update your Agentrific client or server.",
|
|
1261
|
+
code: "VERSION_MISMATCH",
|
|
1262
|
+
protocolVersion: PROTOCOL_VERSION
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
app2.get("/health", () => ({
|
|
1266
|
+
status: "ok",
|
|
1267
|
+
service: "agentrific-server",
|
|
1268
|
+
serverId: security2.serverId,
|
|
1269
|
+
serverVersion: version,
|
|
1270
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1271
|
+
authentication: "pairing"
|
|
1272
|
+
}));
|
|
1273
|
+
app2.post("/api/auth/pair", async (request, reply) => {
|
|
1274
|
+
const input = PairingRequestSchema.safeParse(request.body);
|
|
1275
|
+
if (!input.success)
|
|
1276
|
+
return reply.code(400).send({ error: "Enter a valid pairing payload and six-digit PIN." });
|
|
1277
|
+
const result = await security2.pair(input.data, request.ip);
|
|
1278
|
+
return result ?? reply.code(401).send({ error: "Pairing expired, locked, or invalid. Run agentrific pair again." });
|
|
1279
|
+
});
|
|
1280
|
+
app2.post("/api/auth/pairing-invitation", (_request, reply) => {
|
|
1281
|
+
const publicUrl = process.env.AGENTRIFIC_PUBLIC_URL;
|
|
1282
|
+
if (!publicUrl)
|
|
1283
|
+
return reply.code(503).send({
|
|
1284
|
+
error: "Configure the server\u2019s public HTTPS URL and restart the server before pairing a device."
|
|
1285
|
+
});
|
|
1286
|
+
let url;
|
|
1287
|
+
try {
|
|
1288
|
+
url = pairingOrigin(publicUrl);
|
|
1289
|
+
} catch {
|
|
1290
|
+
return reply.code(503).send({ error: "The server\u2019s public URL is invalid. Configure a public HTTPS origin." });
|
|
1291
|
+
}
|
|
1292
|
+
try {
|
|
1293
|
+
const grant = security2.createPairing();
|
|
1294
|
+
return {
|
|
1295
|
+
payload: { v: 1, url, serverId: grant.serverId, pairingId: grant.pairingId },
|
|
1296
|
+
pin: grant.pin,
|
|
1297
|
+
expiresAt: grant.expiresAt
|
|
1298
|
+
};
|
|
1299
|
+
} catch {
|
|
1300
|
+
return reply.code(429).send({ error: "Too many pending pairing codes. Wait five minutes and try again." });
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
app2.get("/api/auth/session", (request) => ({
|
|
1304
|
+
serverId: security2.serverId,
|
|
1305
|
+
device: security2.authenticate((request.headers.authorization ?? "").slice(7))
|
|
1306
|
+
}));
|
|
1307
|
+
app2.post("/api/auth/event-ticket", (request) => ({
|
|
1308
|
+
ticket: security2.ticket((request.headers.authorization ?? "").slice(7), "/api/events")
|
|
1309
|
+
}));
|
|
1310
|
+
app2.post("/api/auth/asset-ticket", (request, reply) => {
|
|
1311
|
+
const input = z6.object({
|
|
1312
|
+
path: z6.string().regex(/^\/api\/projects\/[^/?]+\/attachments\/[^/?]+\/content(?:\?preview=1)?$/)
|
|
1313
|
+
}).safeParse(request.body);
|
|
1314
|
+
if (!input.success) return reply.code(400).send({ error: "Choose a valid attachment." });
|
|
1315
|
+
return {
|
|
1316
|
+
ticket: security2.ticket((request.headers.authorization ?? "").slice(7), input.data.path)
|
|
1317
|
+
};
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
|
|
980
1321
|
// src/maintenance.ts
|
|
981
1322
|
var MaintenanceError = class extends Error {
|
|
982
1323
|
statusCode = 503;
|
|
@@ -1018,9 +1359,9 @@ var Maintenance = class {
|
|
|
1018
1359
|
}
|
|
1019
1360
|
attach(app2) {
|
|
1020
1361
|
app2.addHook("onRequest", async (request, reply) => {
|
|
1021
|
-
const
|
|
1362
|
+
const path10 = request.url.split("?")[0];
|
|
1022
1363
|
if (["GET", "HEAD", "OPTIONS"].includes(request.method)) return;
|
|
1023
|
-
const allowed =
|
|
1364
|
+
const allowed = path10.startsWith("/api/auth/") || /^\/api\/sessions\/[^/]+\/stop$/.test(path10);
|
|
1024
1365
|
if (this.paused && !allowed)
|
|
1025
1366
|
return reply.code(503).send({ code: "MAINTENANCE", error: new MaintenanceError().message });
|
|
1026
1367
|
this.requests.add(request.id);
|
|
@@ -1033,10 +1374,10 @@ var Maintenance = class {
|
|
|
1033
1374
|
|
|
1034
1375
|
// src/updates.ts
|
|
1035
1376
|
import semver from "semver";
|
|
1036
|
-
import { z as
|
|
1037
|
-
import { Effect as
|
|
1038
|
-
var RegistryRelease =
|
|
1039
|
-
version:
|
|
1377
|
+
import { z as z7 } from "zod";
|
|
1378
|
+
import { Effect as Effect5 } from "effect";
|
|
1379
|
+
var RegistryRelease = z7.object({
|
|
1380
|
+
version: z7.string().refine((value) => semver.valid(value) !== null)
|
|
1040
1381
|
});
|
|
1041
1382
|
async function checkForUpdates() {
|
|
1042
1383
|
const response = await fetch("https://registry.npmjs.org/@agentrific%2Fserver/latest", {
|
|
@@ -1073,7 +1414,7 @@ var UpdateDiscovery = class {
|
|
|
1073
1414
|
checkedAt: null,
|
|
1074
1415
|
error: null
|
|
1075
1416
|
};
|
|
1076
|
-
const saved =
|
|
1417
|
+
const saved = Effect5.runSync(store2.serverUpdateStatus());
|
|
1077
1418
|
if (saved)
|
|
1078
1419
|
this.state = {
|
|
1079
1420
|
...saved,
|
|
@@ -1110,7 +1451,7 @@ var UpdateDiscovery = class {
|
|
|
1110
1451
|
error: "Could not check for server updates. Try again when the registry is reachable."
|
|
1111
1452
|
};
|
|
1112
1453
|
}
|
|
1113
|
-
await
|
|
1454
|
+
await Effect5.runPromise(this.store.saveServerUpdateStatus(this.state));
|
|
1114
1455
|
return this.state;
|
|
1115
1456
|
}
|
|
1116
1457
|
start(canCheck, onError) {
|
|
@@ -1125,14 +1466,14 @@ var UpdateDiscovery = class {
|
|
|
1125
1466
|
};
|
|
1126
1467
|
|
|
1127
1468
|
// src/index.ts
|
|
1128
|
-
import { Effect as
|
|
1129
|
-
import { z as
|
|
1469
|
+
import { Effect as Effect16, ManagedRuntime } from "effect";
|
|
1470
|
+
import { z as z16 } from "zod";
|
|
1130
1471
|
|
|
1131
1472
|
// src/automation.ts
|
|
1132
|
-
import { Effect as
|
|
1473
|
+
import { Effect as Effect7 } from "effect";
|
|
1133
1474
|
|
|
1134
1475
|
// src/tasks.ts
|
|
1135
|
-
import { Effect as
|
|
1476
|
+
import { Effect as Effect6, Semaphore } from "effect";
|
|
1136
1477
|
var TaskActionError = class extends Error {
|
|
1137
1478
|
constructor(message, status) {
|
|
1138
1479
|
super(message);
|
|
@@ -1147,15 +1488,20 @@ function withTaskLock(id, operation) {
|
|
|
1147
1488
|
lock = Semaphore.makeUnsafe(1);
|
|
1148
1489
|
locks.set(id, lock);
|
|
1149
1490
|
}
|
|
1150
|
-
return
|
|
1491
|
+
return Effect6.runPromise(
|
|
1151
1492
|
lock.withPermit(
|
|
1152
|
-
|
|
1493
|
+
Effect6.tryPromise({
|
|
1153
1494
|
try: operation,
|
|
1154
1495
|
catch: (cause) => cause
|
|
1155
1496
|
})
|
|
1156
1497
|
)
|
|
1157
1498
|
);
|
|
1158
1499
|
}
|
|
1500
|
+
async function sessionTaskLockId(store2, session) {
|
|
1501
|
+
if (session.taskId) return session.taskId;
|
|
1502
|
+
const tasks = await Effect6.runPromise(store2.listTasks(session.projectId, true));
|
|
1503
|
+
return tasks.find((task) => task.sessionId === session.id)?.id ?? `session-${session.id}`;
|
|
1504
|
+
}
|
|
1159
1505
|
function taskStatusForSession(status, session) {
|
|
1160
1506
|
if (session?.gitDelivery?.status === "pending") return "running";
|
|
1161
1507
|
if (status === "completed" && session?.gitFlow && !["merged", "no_changes", "pr_opened"].includes(session.gitDelivery?.status ?? ""))
|
|
@@ -1192,8 +1538,8 @@ function sessionTask(session) {
|
|
|
1192
1538
|
});
|
|
1193
1539
|
}
|
|
1194
1540
|
async function boardTasks(store2, projectId) {
|
|
1195
|
-
const tasks = await
|
|
1196
|
-
const sessions = await
|
|
1541
|
+
const tasks = await Effect6.runPromise(store2.listTasks(projectId, true));
|
|
1542
|
+
const sessions = await Effect6.runPromise(store2.list());
|
|
1197
1543
|
const linkedSessionIds = new Set(
|
|
1198
1544
|
tasks.flatMap((task) => task.sessionId ? [task.sessionId] : [])
|
|
1199
1545
|
);
|
|
@@ -1212,10 +1558,10 @@ async function boardTasks(store2, projectId) {
|
|
|
1212
1558
|
return [...linkedTasks, ...legacyTasks].map((task) => TaskSchema.parse(task)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
1213
1559
|
}
|
|
1214
1560
|
async function findTask(store2, id) {
|
|
1215
|
-
const saved = await
|
|
1561
|
+
const saved = await Effect6.runPromise(store2.getTask(id));
|
|
1216
1562
|
if (saved?.deletedAt) throw new TaskActionError("Task not found.", 404);
|
|
1217
1563
|
if (saved) return saved;
|
|
1218
|
-
const session = id.startsWith("session-") ? await
|
|
1564
|
+
const session = id.startsWith("session-") ? await Effect6.runPromise(store2.get(id.slice("session-".length))) : void 0;
|
|
1219
1565
|
if (!session || session.taskId) throw new TaskActionError("Task not found.", 404);
|
|
1220
1566
|
return sessionTask(session);
|
|
1221
1567
|
}
|
|
@@ -1231,7 +1577,7 @@ function editTask(store2, id, input, resolveAttachments) {
|
|
|
1231
1577
|
throw new TaskActionError("Attachment validation is unavailable.", 400);
|
|
1232
1578
|
files = await resolveAttachments(task.projectId, attachmentIds);
|
|
1233
1579
|
}
|
|
1234
|
-
return
|
|
1580
|
+
return Effect6.runPromise(
|
|
1235
1581
|
store2.saveTask({
|
|
1236
1582
|
...task,
|
|
1237
1583
|
...updates2,
|
|
@@ -1242,16 +1588,25 @@ function editTask(store2, id, input, resolveAttachments) {
|
|
|
1242
1588
|
);
|
|
1243
1589
|
});
|
|
1244
1590
|
}
|
|
1245
|
-
function deleteTask(store2, id) {
|
|
1591
|
+
function deleteTask(store2, id, cleanup = async () => {
|
|
1592
|
+
}) {
|
|
1246
1593
|
return withTaskLock(id, async () => {
|
|
1247
1594
|
const task = await findTask(store2, id);
|
|
1248
|
-
const sessions = await
|
|
1595
|
+
const sessions = await Effect6.runPromise(store2.list());
|
|
1249
1596
|
if (sessions.some(
|
|
1250
1597
|
(session) => (session.taskId === task.id || session.id === task.sessionId) && (["starting", "running", "queued"].includes(session.status) || session.gitDelivery?.status === "pending")
|
|
1251
1598
|
))
|
|
1252
1599
|
throw new TaskActionError("Stop the task's running agent before deleting it.", 409);
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1600
|
+
const associated = sessions.filter(
|
|
1601
|
+
(session) => session.taskId === task.id || session.id === task.sessionId
|
|
1602
|
+
);
|
|
1603
|
+
for (const session of associated) await cleanup(session);
|
|
1604
|
+
await Effect6.runPromise(
|
|
1605
|
+
store2.purgeTask(
|
|
1606
|
+
task.id,
|
|
1607
|
+
associated.map((session) => session.id)
|
|
1608
|
+
)
|
|
1609
|
+
);
|
|
1255
1610
|
});
|
|
1256
1611
|
}
|
|
1257
1612
|
|
|
@@ -1277,18 +1632,18 @@ var AutomationCoordinator = class {
|
|
|
1277
1632
|
start;
|
|
1278
1633
|
canStart;
|
|
1279
1634
|
async snapshot() {
|
|
1280
|
-
const state = await
|
|
1635
|
+
const state = await Effect7.runPromise(this.store.automation());
|
|
1281
1636
|
return {
|
|
1282
1637
|
...state,
|
|
1283
1638
|
queue: automationQueue(await boardTasks(this.store), state),
|
|
1284
|
-
active: activeAgents(await
|
|
1639
|
+
active: activeAgents(await Effect7.runPromise(this.store.list()))
|
|
1285
1640
|
};
|
|
1286
1641
|
}
|
|
1287
1642
|
// ponytail: one server process owns this SQLite database; use a durable lease before adding replicas.
|
|
1288
1643
|
configure(config) {
|
|
1289
1644
|
return withTaskLock("automation", async () => {
|
|
1290
|
-
const state = await
|
|
1291
|
-
await
|
|
1645
|
+
const state = await Effect7.runPromise(this.store.automation());
|
|
1646
|
+
await Effect7.runPromise(this.store.saveAutomation({ ...state, config, error: null }));
|
|
1292
1647
|
return this.snapshot();
|
|
1293
1648
|
});
|
|
1294
1649
|
}
|
|
@@ -1300,7 +1655,7 @@ var AutomationCoordinator = class {
|
|
|
1300
1655
|
throw new TaskActionError("Queue changed. Refresh and try again.", 409);
|
|
1301
1656
|
const order = ids.filter((id) => id !== taskId);
|
|
1302
1657
|
order.splice(beforeId === null ? order.length : order.indexOf(beforeId), 0, taskId);
|
|
1303
|
-
await
|
|
1658
|
+
await Effect7.runPromise(
|
|
1304
1659
|
this.store.saveAutomation({
|
|
1305
1660
|
config: snapshot.config,
|
|
1306
1661
|
lastMinute: snapshot.lastMinute,
|
|
@@ -1313,22 +1668,22 @@ var AutomationCoordinator = class {
|
|
|
1313
1668
|
}
|
|
1314
1669
|
tick(now = /* @__PURE__ */ new Date()) {
|
|
1315
1670
|
return withTaskLock("automation", async () => {
|
|
1316
|
-
let state = await
|
|
1671
|
+
let state = await Effect7.runPromise(this.store.automation());
|
|
1317
1672
|
const minute = now.toISOString().slice(0, 16);
|
|
1318
1673
|
if (!this.canStart() || !state.config.enabled || state.lastMinute !== null && state.lastMinute >= minute || !cronMatches(state.config.cron, now))
|
|
1319
1674
|
return;
|
|
1320
1675
|
state = { ...state, lastMinute: minute, error: null };
|
|
1321
|
-
await
|
|
1676
|
+
await Effect7.runPromise(this.store.saveAutomation(state));
|
|
1322
1677
|
const queue = automationQueue(await boardTasks(this.store), state);
|
|
1323
1678
|
for (const task of queue) {
|
|
1324
1679
|
if (!this.canStart()) break;
|
|
1325
|
-
if (activeAgents(await
|
|
1680
|
+
if (activeAgents(await Effect7.runPromise(this.store.list())) >= state.config.parallelism)
|
|
1326
1681
|
break;
|
|
1327
1682
|
try {
|
|
1328
1683
|
await this.start(task.id);
|
|
1329
1684
|
} catch (error) {
|
|
1330
1685
|
if (error instanceof MaintenanceError) break;
|
|
1331
|
-
await
|
|
1686
|
+
await Effect7.runPromise(
|
|
1332
1687
|
this.store.saveAutomation({
|
|
1333
1688
|
...state,
|
|
1334
1689
|
config: { ...state.config, enabled: false },
|
|
@@ -1343,8 +1698,8 @@ var AutomationCoordinator = class {
|
|
|
1343
1698
|
};
|
|
1344
1699
|
|
|
1345
1700
|
// src/runtime.ts
|
|
1346
|
-
import
|
|
1347
|
-
import { randomUUID as
|
|
1701
|
+
import path4 from "node:path";
|
|
1702
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
1348
1703
|
|
|
1349
1704
|
// src/git-flow.ts
|
|
1350
1705
|
function gitFlowPrompt(session, message) {
|
|
@@ -1356,7 +1711,7 @@ Work on the assigned run branch ${session.branch}. Before finishing, call delive
|
|
|
1356
1711
|
}
|
|
1357
1712
|
|
|
1358
1713
|
// src/runtime.ts
|
|
1359
|
-
import { Context as Context2, Data as Data2, Effect as
|
|
1714
|
+
import { Context as Context2, Data as Data2, Effect as Effect8, Fiber, Layer as Layer2, Semaphore as Semaphore2 } from "effect";
|
|
1360
1715
|
var AgentError = class extends Data2.TaggedError("AgentError") {
|
|
1361
1716
|
};
|
|
1362
1717
|
var AgentController = class {
|
|
@@ -1396,20 +1751,20 @@ var AgentController = class {
|
|
|
1396
1751
|
this.emit({ type: "session.updated", session: entry.session });
|
|
1397
1752
|
}
|
|
1398
1753
|
operation(action) {
|
|
1399
|
-
return
|
|
1754
|
+
return Effect8.tryPromise({
|
|
1400
1755
|
try: action,
|
|
1401
1756
|
catch: (cause) => new AgentError({ message: String(cause), cause })
|
|
1402
1757
|
});
|
|
1403
1758
|
}
|
|
1404
1759
|
open(session) {
|
|
1405
|
-
return
|
|
1760
|
+
return Effect8.gen({ self: this }, function* () {
|
|
1406
1761
|
const existing = this.active.get(session.id);
|
|
1407
1762
|
if (existing) return existing;
|
|
1408
|
-
const agent = yield*
|
|
1763
|
+
const agent = yield* Effect8.tryPromise({
|
|
1409
1764
|
try: () => this.factory.open(
|
|
1410
1765
|
session.id,
|
|
1411
|
-
session.workspacePath ??
|
|
1412
|
-
|
|
1766
|
+
session.workspacePath ?? path4.join(this.worktreesRoot, session.id),
|
|
1767
|
+
path4.join(this.sessionsRoot, session.id),
|
|
1413
1768
|
session.model,
|
|
1414
1769
|
session
|
|
1415
1770
|
),
|
|
@@ -1432,14 +1787,14 @@ var AgentController = class {
|
|
|
1432
1787
|
});
|
|
1433
1788
|
}
|
|
1434
1789
|
run(entry, message) {
|
|
1435
|
-
return
|
|
1790
|
+
return Effect8.gen({ self: this }, function* () {
|
|
1436
1791
|
let next = message;
|
|
1437
1792
|
while (next !== void 0) {
|
|
1438
1793
|
const current = next;
|
|
1439
1794
|
yield* this.operation(
|
|
1440
1795
|
() => entry.agent.prompt(gitFlowPrompt(entry.session, current.text), current.attachments)
|
|
1441
1796
|
);
|
|
1442
|
-
next = yield*
|
|
1797
|
+
next = yield* Effect8.sync(() => {
|
|
1443
1798
|
const queued = entry.stopping ? void 0 : entry.pending.shift();
|
|
1444
1799
|
if (queued === void 0) {
|
|
1445
1800
|
entry.running = false;
|
|
@@ -1451,19 +1806,19 @@ var AgentController = class {
|
|
|
1451
1806
|
yield* this.operation(() => this.onSettled(entry.session));
|
|
1452
1807
|
}
|
|
1453
1808
|
}).pipe(
|
|
1454
|
-
|
|
1455
|
-
(error) =>
|
|
1809
|
+
Effect8.catch(
|
|
1810
|
+
(error) => Effect8.sync(() => {
|
|
1456
1811
|
entry.pending.length = 0;
|
|
1457
1812
|
entry.running = false;
|
|
1458
1813
|
this.emit({ type: "error", sessionId: entry.session.id, message: error.message });
|
|
1459
1814
|
this.status(entry, entry.stopping ? "stopped" : "failed");
|
|
1460
1815
|
})
|
|
1461
1816
|
),
|
|
1462
|
-
|
|
1817
|
+
Effect8.onInterrupt(
|
|
1463
1818
|
() => this.operation(() => entry.agent.abort()).pipe(
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1819
|
+
Effect8.orDie,
|
|
1820
|
+
Effect8.ensuring(
|
|
1821
|
+
Effect8.sync(() => {
|
|
1467
1822
|
entry.pending.length = 0;
|
|
1468
1823
|
entry.running = false;
|
|
1469
1824
|
this.status(entry, "stopped");
|
|
@@ -1471,16 +1826,16 @@ var AgentController = class {
|
|
|
1471
1826
|
)
|
|
1472
1827
|
)
|
|
1473
1828
|
),
|
|
1474
|
-
|
|
1829
|
+
Effect8.ensuring(Effect8.sync(() => this.working.delete(entry.session.id)))
|
|
1475
1830
|
);
|
|
1476
1831
|
}
|
|
1477
|
-
send(session, message, attachments2 = [], messageId =
|
|
1832
|
+
send(session, message, attachments2 = [], messageId = randomUUID4()) {
|
|
1478
1833
|
return this.lock(session.id).withPermit(
|
|
1479
|
-
|
|
1834
|
+
Effect8.gen({ self: this }, function* () {
|
|
1480
1835
|
if (this.closed)
|
|
1481
|
-
return yield*
|
|
1836
|
+
return yield* Effect8.fail(new AgentError({ message: "Server is shutting down" }));
|
|
1482
1837
|
if (!session.branch && (session.gitFlow || !session.workspacePath))
|
|
1483
|
-
return yield*
|
|
1838
|
+
return yield* Effect8.fail(new AgentError({ message: "Session checkout is not ready" }));
|
|
1484
1839
|
if (this.hasMessage(session.id, messageId)) return;
|
|
1485
1840
|
const entry = yield* this.open(session);
|
|
1486
1841
|
yield* this.operation(async () => entry.agent.validateAttachments?.(attachments2));
|
|
@@ -1499,7 +1854,7 @@ var AgentController = class {
|
|
|
1499
1854
|
entry.stopping = false;
|
|
1500
1855
|
this.status(entry, "running");
|
|
1501
1856
|
this.working.add(session.id);
|
|
1502
|
-
entry.fiber = yield*
|
|
1857
|
+
entry.fiber = yield* Effect8.forkIn(
|
|
1503
1858
|
this.run(entry, { text: message, attachments: attachments2 }),
|
|
1504
1859
|
this.scope,
|
|
1505
1860
|
{
|
|
@@ -1507,22 +1862,22 @@ var AgentController = class {
|
|
|
1507
1862
|
}
|
|
1508
1863
|
);
|
|
1509
1864
|
})
|
|
1510
|
-
).pipe(
|
|
1865
|
+
).pipe(Effect8.uninterruptible);
|
|
1511
1866
|
}
|
|
1512
1867
|
setModel(session, model) {
|
|
1513
1868
|
return this.lock(session.id).withPermit(
|
|
1514
|
-
|
|
1869
|
+
Effect8.gen({ self: this }, function* () {
|
|
1515
1870
|
if (this.closed)
|
|
1516
|
-
return yield*
|
|
1871
|
+
return yield* Effect8.fail(new AgentError({ message: "Server is shutting down" }));
|
|
1517
1872
|
const entry = yield* this.open(session);
|
|
1518
1873
|
if (entry.running || entry.pending.length)
|
|
1519
|
-
return yield*
|
|
1874
|
+
return yield* Effect8.fail(
|
|
1520
1875
|
new AgentError({
|
|
1521
1876
|
message: "Wait for this reply to finish before changing the model."
|
|
1522
1877
|
})
|
|
1523
1878
|
);
|
|
1524
1879
|
if (!entry.agent.setModel)
|
|
1525
|
-
return yield*
|
|
1880
|
+
return yield* Effect8.fail(
|
|
1526
1881
|
new AgentError({ message: "This agent does not support model selection." })
|
|
1527
1882
|
);
|
|
1528
1883
|
yield* this.operation(() => entry.agent.setModel(model));
|
|
@@ -1534,10 +1889,10 @@ var AgentController = class {
|
|
|
1534
1889
|
this.emit({ type: "session.updated", session: entry.session });
|
|
1535
1890
|
return entry.session;
|
|
1536
1891
|
})
|
|
1537
|
-
).pipe(
|
|
1892
|
+
).pipe(Effect8.uninterruptible);
|
|
1538
1893
|
}
|
|
1539
1894
|
stopEntry(entry) {
|
|
1540
|
-
return
|
|
1895
|
+
return Effect8.gen({ self: this }, function* () {
|
|
1541
1896
|
entry.stopping = true;
|
|
1542
1897
|
entry.pending.length = 0;
|
|
1543
1898
|
yield* this.operation(() => entry.agent.abort());
|
|
@@ -1547,7 +1902,7 @@ var AgentController = class {
|
|
|
1547
1902
|
}
|
|
1548
1903
|
stop(session) {
|
|
1549
1904
|
return this.lock(session.id).withPermit(
|
|
1550
|
-
|
|
1905
|
+
Effect8.gen({ self: this }, function* () {
|
|
1551
1906
|
const entry = this.active.get(session.id);
|
|
1552
1907
|
if (entry) {
|
|
1553
1908
|
yield* this.stopEntry(entry);
|
|
@@ -1558,14 +1913,26 @@ var AgentController = class {
|
|
|
1558
1913
|
});
|
|
1559
1914
|
}
|
|
1560
1915
|
})
|
|
1561
|
-
).pipe(
|
|
1916
|
+
).pipe(Effect8.uninterruptible);
|
|
1917
|
+
}
|
|
1918
|
+
forget(id) {
|
|
1919
|
+
return this.lock(id).withPermit(
|
|
1920
|
+
Effect8.sync(() => {
|
|
1921
|
+
const entry = this.active.get(id);
|
|
1922
|
+
if (!entry) return;
|
|
1923
|
+
if (entry.running) throw new Error("Cannot delete an active run.");
|
|
1924
|
+
entry.unsubscribe();
|
|
1925
|
+
entry.agent.dispose();
|
|
1926
|
+
this.active.delete(id);
|
|
1927
|
+
})
|
|
1928
|
+
);
|
|
1562
1929
|
}
|
|
1563
1930
|
close() {
|
|
1564
|
-
return
|
|
1931
|
+
return Effect8.gen({ self: this }, function* () {
|
|
1565
1932
|
this.closed = true;
|
|
1566
1933
|
for (const [id, lock] of this.locks) {
|
|
1567
1934
|
yield* lock.withPermit(
|
|
1568
|
-
|
|
1935
|
+
Effect8.gen({ self: this }, function* () {
|
|
1569
1936
|
const entry = this.active.get(id);
|
|
1570
1937
|
if (!entry) return;
|
|
1571
1938
|
if (entry.running) yield* this.stopEntry(entry);
|
|
@@ -1585,8 +1952,8 @@ var AgentSessions = class extends Context2.Service()(
|
|
|
1585
1952
|
}) {
|
|
1586
1953
|
return Layer2.effect(
|
|
1587
1954
|
this,
|
|
1588
|
-
|
|
1589
|
-
const scope = yield*
|
|
1955
|
+
Effect8.gen(function* () {
|
|
1956
|
+
const scope = yield* Effect8.scope;
|
|
1590
1957
|
const controller = new AgentController(
|
|
1591
1958
|
factory,
|
|
1592
1959
|
emit,
|
|
@@ -1596,7 +1963,7 @@ var AgentSessions = class extends Context2.Service()(
|
|
|
1596
1963
|
hasMessage,
|
|
1597
1964
|
onSettled
|
|
1598
1965
|
);
|
|
1599
|
-
yield*
|
|
1966
|
+
yield* Effect8.addFinalizer(() => controller.close().pipe(Effect8.orDie));
|
|
1600
1967
|
return controller;
|
|
1601
1968
|
})
|
|
1602
1969
|
);
|
|
@@ -1604,8 +1971,8 @@ var AgentSessions = class extends Context2.Service()(
|
|
|
1604
1971
|
};
|
|
1605
1972
|
|
|
1606
1973
|
// src/delivery.ts
|
|
1607
|
-
import { Effect as
|
|
1608
|
-
import { z as
|
|
1974
|
+
import { Effect as Effect9 } from "effect";
|
|
1975
|
+
import { z as z9 } from "zod";
|
|
1609
1976
|
|
|
1610
1977
|
// src/workflow-command.ts
|
|
1611
1978
|
import { execFile } from "node:child_process";
|
|
@@ -1649,12 +2016,12 @@ function workflowError(error) {
|
|
|
1649
2016
|
}
|
|
1650
2017
|
|
|
1651
2018
|
// src/workflow-schema.ts
|
|
1652
|
-
import { z as
|
|
1653
|
-
var DeliveryJobSchema =
|
|
1654
|
-
kind:
|
|
1655
|
-
sessionId:
|
|
2019
|
+
import { z as z8 } from "zod";
|
|
2020
|
+
var DeliveryJobSchema = z8.object({
|
|
2021
|
+
kind: z8.literal("delivery"),
|
|
2022
|
+
sessionId: z8.string(),
|
|
1656
2023
|
request: DeliveryRequestSchema,
|
|
1657
|
-
phase:
|
|
2024
|
+
phase: z8.enum([
|
|
1658
2025
|
"validating",
|
|
1659
2026
|
"drafting",
|
|
1660
2027
|
"committing",
|
|
@@ -1664,25 +2031,25 @@ var DeliveryJobSchema = z6.object({
|
|
|
1664
2031
|
"opening_pr",
|
|
1665
2032
|
"delivered"
|
|
1666
2033
|
]),
|
|
1667
|
-
attempt:
|
|
1668
|
-
commitSha:
|
|
1669
|
-
title:
|
|
1670
|
-
body:
|
|
1671
|
-
logs:
|
|
1672
|
-
failed:
|
|
1673
|
-
result:
|
|
1674
|
-
url:
|
|
1675
|
-
message:
|
|
2034
|
+
attempt: z8.number().int().positive(),
|
|
2035
|
+
commitSha: z8.string().optional(),
|
|
2036
|
+
title: z8.string(),
|
|
2037
|
+
body: z8.string(),
|
|
2038
|
+
logs: z8.string().default(""),
|
|
2039
|
+
failed: z8.boolean().default(false),
|
|
2040
|
+
result: z8.enum(["merged", "pr_opened", "no_changes"]).optional(),
|
|
2041
|
+
url: z8.string().optional(),
|
|
2042
|
+
message: z8.string().optional()
|
|
1676
2043
|
});
|
|
1677
|
-
var PreparationJobSchema =
|
|
1678
|
-
kind:
|
|
1679
|
-
sessionId:
|
|
1680
|
-
projectPath:
|
|
2044
|
+
var PreparationJobSchema = z8.object({
|
|
2045
|
+
kind: z8.literal("preparation"),
|
|
2046
|
+
sessionId: z8.string(),
|
|
2047
|
+
projectPath: z8.string(),
|
|
1681
2048
|
input: CreateSessionSchema,
|
|
1682
|
-
stage:
|
|
1683
|
-
logs:
|
|
2049
|
+
stage: z8.enum(["creating", "installing", "ready", "failed"]),
|
|
2050
|
+
logs: z8.string().default("")
|
|
1684
2051
|
});
|
|
1685
|
-
var WorkflowJobSchema =
|
|
2052
|
+
var WorkflowJobSchema = z8.discriminatedUnion("kind", [
|
|
1686
2053
|
DeliveryJobSchema,
|
|
1687
2054
|
PreparationJobSchema
|
|
1688
2055
|
]);
|
|
@@ -1702,20 +2069,20 @@ var DeliveryCoordinator = class {
|
|
|
1702
2069
|
return [...this.workers.keys()].map((id) => `delivery:${id}`);
|
|
1703
2070
|
}
|
|
1704
2071
|
async session(id) {
|
|
1705
|
-
const session = await
|
|
2072
|
+
const session = await Effect9.runPromise(this.store.get(id));
|
|
1706
2073
|
if (!session?.gitFlow || !session.branch || !session.workspacePath)
|
|
1707
2074
|
throw new Error("This run does not have a ready Git flow workspace.");
|
|
1708
2075
|
return session;
|
|
1709
2076
|
}
|
|
1710
2077
|
async job(id) {
|
|
1711
|
-
const raw = await
|
|
2078
|
+
const raw = await Effect9.runPromise(this.store.workflow(`delivery:${id}`));
|
|
1712
2079
|
return raw === void 0 ? void 0 : DeliveryJobSchema.parse(raw);
|
|
1713
2080
|
}
|
|
1714
2081
|
git(session, args, signal) {
|
|
1715
2082
|
return this.command(session.workspacePath, "git", args, signal);
|
|
1716
2083
|
}
|
|
1717
2084
|
async record(job) {
|
|
1718
|
-
await
|
|
2085
|
+
await Effect9.runPromise(this.store.saveWorkflow(`delivery:${job.sessionId}`, job));
|
|
1719
2086
|
const session = await this.session(job.sessionId);
|
|
1720
2087
|
const delivery2 = {
|
|
1721
2088
|
status: job.failed ? "failed" : job.result ?? "pending",
|
|
@@ -1904,7 +2271,7 @@ Agentrific run ${id}.`;
|
|
|
1904
2271
|
return (await this.session(id)).gitDelivery;
|
|
1905
2272
|
}
|
|
1906
2273
|
async recover() {
|
|
1907
|
-
const jobs = await
|
|
2274
|
+
const jobs = await Effect9.runPromise(this.store.workflows());
|
|
1908
2275
|
for (const raw of jobs) {
|
|
1909
2276
|
const parsed = DeliveryJobSchema.safeParse(raw);
|
|
1910
2277
|
if (!parsed.success) continue;
|
|
@@ -1978,11 +2345,11 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
1978
2345
|
"--json",
|
|
1979
2346
|
"url,state,headRefOid"
|
|
1980
2347
|
]);
|
|
1981
|
-
const prs =
|
|
1982
|
-
|
|
1983
|
-
url:
|
|
1984
|
-
state:
|
|
1985
|
-
headRefOid:
|
|
2348
|
+
const prs = z9.array(
|
|
2349
|
+
z9.object({
|
|
2350
|
+
url: z9.string().url(),
|
|
2351
|
+
state: z9.string(),
|
|
2352
|
+
headRefOid: z9.string().optional()
|
|
1986
2353
|
})
|
|
1987
2354
|
).parse(JSON.parse(existing));
|
|
1988
2355
|
const open = prs.find(
|
|
@@ -2024,9 +2391,9 @@ ${workflowError(error instanceof Error ? error : String(error))}`
|
|
|
2024
2391
|
|
|
2025
2392
|
// src/preparation.ts
|
|
2026
2393
|
import { mkdir as mkdir3, readFile as readFile3 } from "node:fs/promises";
|
|
2027
|
-
import
|
|
2028
|
-
import { Effect as
|
|
2029
|
-
import { z as
|
|
2394
|
+
import path5 from "node:path";
|
|
2395
|
+
import { Effect as Effect10 } from "effect";
|
|
2396
|
+
import { z as z10 } from "zod";
|
|
2030
2397
|
var PreparationCoordinator = class {
|
|
2031
2398
|
constructor(store2, options) {
|
|
2032
2399
|
this.store = store2;
|
|
@@ -2056,8 +2423,8 @@ var PreparationCoordinator = class {
|
|
|
2056
2423
|
job.logs = workflowLog(`${job.logs}
|
|
2057
2424
|
${message}${logs ? `
|
|
2058
2425
|
${logs}` : ""}`);
|
|
2059
|
-
await
|
|
2060
|
-
const current = await
|
|
2426
|
+
await Effect10.runPromise(this.store.saveWorkflow(`preparation:${session.id}`, job));
|
|
2427
|
+
const current = await Effect10.runPromise(this.store.get(session.id));
|
|
2061
2428
|
this.options.publish({
|
|
2062
2429
|
type: "session.updated",
|
|
2063
2430
|
session: {
|
|
@@ -2076,7 +2443,7 @@ ${logs}` : ""}`);
|
|
|
2076
2443
|
await record("installing", "Installing run dependencies\u2026");
|
|
2077
2444
|
await this.install(workspace, command);
|
|
2078
2445
|
await record("ready", "Workspace ready.");
|
|
2079
|
-
return await
|
|
2446
|
+
return await Effect10.runPromise(this.store.get(session.id));
|
|
2080
2447
|
} catch (error) {
|
|
2081
2448
|
const step = job.stage === "installing" ? "Dependency installation failed" : "Workspace preparation failed";
|
|
2082
2449
|
await record(
|
|
@@ -2120,7 +2487,7 @@ ${logs}` : ""}`);
|
|
|
2120
2487
|
const workspace = session.workspacePath;
|
|
2121
2488
|
if (branchAtWorkspace !== void 0)
|
|
2122
2489
|
throw new Error("The run workspace already exists on a different branch.");
|
|
2123
|
-
await mkdir3(
|
|
2490
|
+
await mkdir3(path5.dirname(workspace), { recursive: true });
|
|
2124
2491
|
try {
|
|
2125
2492
|
await command(
|
|
2126
2493
|
projectPath,
|
|
@@ -2140,12 +2507,12 @@ ${logs}` : ""}`);
|
|
|
2140
2507
|
}
|
|
2141
2508
|
}
|
|
2142
2509
|
async install(workspace, command) {
|
|
2143
|
-
const packageFile = await readFile3(
|
|
2510
|
+
const packageFile = await readFile3(path5.join(workspace, "package.json"), "utf8").catch(
|
|
2144
2511
|
() => void 0
|
|
2145
2512
|
);
|
|
2146
2513
|
if (!packageFile) return;
|
|
2147
|
-
const metadata =
|
|
2148
|
-
const has = async (name) => readFile3(
|
|
2514
|
+
const metadata = z10.object({ packageManager: z10.string().optional() }).parse(JSON.parse(packageFile));
|
|
2515
|
+
const has = async (name) => readFile3(path5.join(workspace, name)).then(
|
|
2149
2516
|
() => true,
|
|
2150
2517
|
() => false
|
|
2151
2518
|
);
|
|
@@ -2158,10 +2525,10 @@ ${logs}` : ""}`);
|
|
|
2158
2525
|
else if (await has("package-lock.json")) await command(workspace, "npm", ["ci"]);
|
|
2159
2526
|
}
|
|
2160
2527
|
async retry(id) {
|
|
2161
|
-
const raw = await
|
|
2528
|
+
const raw = await Effect10.runPromise(this.store.workflow(`preparation:${id}`));
|
|
2162
2529
|
const job = PreparationJobSchema.parse(raw);
|
|
2163
2530
|
return withTaskLock(`repository:${job.projectPath}`, async () => {
|
|
2164
|
-
const session = await
|
|
2531
|
+
const session = await Effect10.runPromise(this.store.get(id));
|
|
2165
2532
|
if (!session || session.preparation?.status !== "failed")
|
|
2166
2533
|
throw new Error("This run does not need preparation retried.");
|
|
2167
2534
|
const prepared = await this.prepare(session, job.projectPath, job.input);
|
|
@@ -2169,11 +2536,11 @@ ${logs}` : ""}`);
|
|
|
2169
2536
|
});
|
|
2170
2537
|
}
|
|
2171
2538
|
async recover() {
|
|
2172
|
-
for (const raw of await
|
|
2539
|
+
for (const raw of await Effect10.runPromise(this.store.workflows())) {
|
|
2173
2540
|
const parsed = PreparationJobSchema.safeParse(raw);
|
|
2174
2541
|
if (!parsed.success) continue;
|
|
2175
2542
|
const job = parsed.data;
|
|
2176
|
-
const session = await
|
|
2543
|
+
const session = await Effect10.runPromise(this.store.get(job.sessionId));
|
|
2177
2544
|
if (!session) continue;
|
|
2178
2545
|
if (job.stage === "failed") {
|
|
2179
2546
|
if (session.preparation?.status === "failed") continue;
|
|
@@ -2204,7 +2571,7 @@ ${logs}` : ""}`);
|
|
|
2204
2571
|
});
|
|
2205
2572
|
continue;
|
|
2206
2573
|
}
|
|
2207
|
-
await
|
|
2574
|
+
await Effect10.runPromise(
|
|
2208
2575
|
this.store.saveWorkflow(`preparation:${session.id}`, { ...job, stage: "failed" })
|
|
2209
2576
|
);
|
|
2210
2577
|
this.options.publish({
|
|
@@ -2226,8 +2593,8 @@ ${logs}` : ""}`);
|
|
|
2226
2593
|
|
|
2227
2594
|
// src/delivery-writer.ts
|
|
2228
2595
|
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2229
|
-
import { z as
|
|
2230
|
-
var DraftSchema =
|
|
2596
|
+
import { z as z11 } from "zod";
|
|
2597
|
+
var DraftSchema = z11.object({ title: z11.string().trim().min(1).max(120), body: z11.string().trim().min(1).max(1e4) }).strict();
|
|
2231
2598
|
function deliveryWriter(models2, createSession = createAgentSession) {
|
|
2232
2599
|
return async (run, input, diff, signal) => {
|
|
2233
2600
|
signal?.throwIfAborted();
|
|
@@ -2269,7 +2636,7 @@ ${diff}`
|
|
|
2269
2636
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2270
2637
|
|
|
2271
2638
|
// src/models.ts
|
|
2272
|
-
import { Effect as
|
|
2639
|
+
import { Effect as Effect11 } from "effect";
|
|
2273
2640
|
var legacyModel = { platformId: "openai-codex", modelId: "gpt-6-luna" };
|
|
2274
2641
|
var makers = [
|
|
2275
2642
|
{ id: "openai", name: "OpenAI", pattern: /(?:^|[/.:\s-])(?:gpt-|o[134](?:-|$))/i },
|
|
@@ -2334,7 +2701,7 @@ var ServerModels = class {
|
|
|
2334
2701
|
await this.refreshing;
|
|
2335
2702
|
}
|
|
2336
2703
|
async defaultReference() {
|
|
2337
|
-
return await
|
|
2704
|
+
return await Effect11.runPromise(this.store.defaultModel()) ?? legacyModel;
|
|
2338
2705
|
}
|
|
2339
2706
|
async defaultIdentity() {
|
|
2340
2707
|
const selected = await this.defaultReference();
|
|
@@ -2370,19 +2737,19 @@ var ServerModels = class {
|
|
|
2370
2737
|
}
|
|
2371
2738
|
async setDefault(reference) {
|
|
2372
2739
|
await this.resolve(reference);
|
|
2373
|
-
await
|
|
2740
|
+
await Effect11.runPromise(this.store.setDefaultModel(reference));
|
|
2374
2741
|
return this.identity(reference.platformId, reference.modelId);
|
|
2375
2742
|
}
|
|
2376
2743
|
};
|
|
2377
2744
|
|
|
2378
2745
|
// src/pi.ts
|
|
2379
|
-
import { Effect as
|
|
2380
|
-
import { randomUUID as
|
|
2746
|
+
import { Effect as Effect12 } from "effect";
|
|
2747
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
2381
2748
|
import {
|
|
2382
2749
|
createAgentSession as createAgentSession2,
|
|
2383
2750
|
SessionManager as SessionManager2
|
|
2384
2751
|
} from "@earendil-works/pi-coding-agent";
|
|
2385
|
-
import { z as
|
|
2752
|
+
import { z as z12 } from "zod";
|
|
2386
2753
|
|
|
2387
2754
|
// src/delivery-tool.ts
|
|
2388
2755
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
@@ -2417,8 +2784,8 @@ function deliveryTool(sessionId, delivery2) {
|
|
|
2417
2784
|
}
|
|
2418
2785
|
|
|
2419
2786
|
// src/pi.ts
|
|
2420
|
-
var ToolResultSchema =
|
|
2421
|
-
content:
|
|
2787
|
+
var ToolResultSchema = z12.object({
|
|
2788
|
+
content: z12.array(z12.object({ type: z12.string(), text: z12.string().optional() })).optional()
|
|
2422
2789
|
}).passthrough();
|
|
2423
2790
|
var boundedJson = (value, limit) => {
|
|
2424
2791
|
let text;
|
|
@@ -2460,7 +2827,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2460
2827
|
);
|
|
2461
2828
|
},
|
|
2462
2829
|
async prompt(message, attachments2 = []) {
|
|
2463
|
-
const context = await
|
|
2830
|
+
const context = await Effect12.runPromise(files.prepare(attachments2));
|
|
2464
2831
|
await session.prompt(message + context.context, { images: context.images });
|
|
2465
2832
|
const last = session.messages.at(-1);
|
|
2466
2833
|
if (last?.role === "assistant" && last.stopReason === "error") {
|
|
@@ -2497,7 +2864,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2497
2864
|
function handleMessage(event) {
|
|
2498
2865
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
2499
2866
|
flushText();
|
|
2500
|
-
assistantMessageId =
|
|
2867
|
+
assistantMessageId = randomUUID5();
|
|
2501
2868
|
emit({
|
|
2502
2869
|
type: "message.started",
|
|
2503
2870
|
sessionId,
|
|
@@ -2508,7 +2875,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2508
2875
|
return true;
|
|
2509
2876
|
}
|
|
2510
2877
|
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
2511
|
-
if (!assistantMessageId) assistantMessageId =
|
|
2878
|
+
if (!assistantMessageId) assistantMessageId = randomUUID5();
|
|
2512
2879
|
pendingText += event.assistantMessageEvent.delta;
|
|
2513
2880
|
setActivity("responding");
|
|
2514
2881
|
if (!flushTimer) flushTimer = setTimeout(flushText, 50);
|
|
@@ -2583,18 +2950,18 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
|
|
|
2583
2950
|
|
|
2584
2951
|
// src/projects.ts
|
|
2585
2952
|
import { access, readdir as readdir2, realpath, stat as stat2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
|
|
2586
|
-
import { randomUUID as
|
|
2587
|
-
import { z as
|
|
2953
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
2954
|
+
import { z as z13 } from "zod";
|
|
2588
2955
|
import { constants } from "node:fs";
|
|
2589
2956
|
import { homedir } from "node:os";
|
|
2590
|
-
import
|
|
2591
|
-
import { Effect as
|
|
2957
|
+
import path6 from "node:path";
|
|
2958
|
+
import { Effect as Effect13, Semaphore as Semaphore3 } from "effect";
|
|
2592
2959
|
function resolveProjectsRoot(input) {
|
|
2593
|
-
return
|
|
2960
|
+
return Effect13.tryPromise({
|
|
2594
2961
|
try: async () => {
|
|
2595
2962
|
const trimmed = input.trim();
|
|
2596
|
-
const expanded = trimmed === "~" ? homedir() : trimmed.startsWith("~/") ?
|
|
2597
|
-
if (!
|
|
2963
|
+
const expanded = trimmed === "~" ? homedir() : trimmed.startsWith("~/") ? path6.join(homedir(), trimmed.slice(2)) : trimmed;
|
|
2964
|
+
if (!path6.isAbsolute(expanded))
|
|
2598
2965
|
throw new Error("Use an absolute server path or a path beginning with ~/.");
|
|
2599
2966
|
const resolved = await realpath(expanded);
|
|
2600
2967
|
if (!(await stat2(resolved)).isDirectory())
|
|
@@ -2616,63 +2983,63 @@ function projectLock(store2) {
|
|
|
2616
2983
|
}
|
|
2617
2984
|
return lock;
|
|
2618
2985
|
}
|
|
2619
|
-
var ProjectId =
|
|
2986
|
+
var ProjectId = z13.string().trim().min(1).max(255);
|
|
2620
2987
|
async function gitDirectory(directory) {
|
|
2621
|
-
const location =
|
|
2988
|
+
const location = path6.join(directory, ".git");
|
|
2622
2989
|
const info = await stat2(location);
|
|
2623
2990
|
if (info.isDirectory()) return location;
|
|
2624
2991
|
const contents = await readFile4(location, "utf8");
|
|
2625
2992
|
const match = /^gitdir: (.+)\s*$/m.exec(contents);
|
|
2626
2993
|
if (!match) throw new Error("Invalid Git worktree metadata.");
|
|
2627
|
-
const resolved = await realpath(
|
|
2994
|
+
const resolved = await realpath(path6.resolve(directory, match[1].trim()));
|
|
2628
2995
|
if (!(await stat2(resolved)).isDirectory()) throw new Error("Git directory is missing.");
|
|
2629
2996
|
return resolved;
|
|
2630
2997
|
}
|
|
2631
2998
|
async function identity(directory, preferred) {
|
|
2632
|
-
const filename =
|
|
2999
|
+
const filename = path6.join(await gitDirectory(directory), "agentrific-project-id");
|
|
2633
3000
|
try {
|
|
2634
3001
|
return ProjectId.parse(await readFile4(filename, "utf8"));
|
|
2635
3002
|
} catch (error) {
|
|
2636
|
-
if (!
|
|
2637
|
-
const legacyFilename =
|
|
3003
|
+
if (!z13.object({ code: z13.literal("ENOENT") }).safeParse(error).success) throw error;
|
|
3004
|
+
const legacyFilename = path6.join(await gitDirectory(directory), "chatplans-project-id");
|
|
2638
3005
|
const legacy = await readFile4(legacyFilename, "utf8").catch((cause) => {
|
|
2639
|
-
if (!
|
|
3006
|
+
if (!z13.object({ code: z13.literal("ENOENT") }).safeParse(cause).success) throw cause;
|
|
2640
3007
|
return null;
|
|
2641
3008
|
});
|
|
2642
|
-
const id = legacy === null ? preferred ??
|
|
3009
|
+
const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
|
|
2643
3010
|
try {
|
|
2644
3011
|
await writeFile2(filename, id + "\n", { flag: "wx" });
|
|
2645
3012
|
return id;
|
|
2646
3013
|
} catch (writeError) {
|
|
2647
|
-
if (!
|
|
3014
|
+
if (!z13.object({ code: z13.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
|
|
2648
3015
|
return ProjectId.parse(await readFile4(filename, "utf8"));
|
|
2649
3016
|
}
|
|
2650
3017
|
}
|
|
2651
3018
|
}
|
|
2652
3019
|
function scanProjects(root, store2) {
|
|
2653
|
-
return
|
|
3020
|
+
return Effect13.tryPromise({
|
|
2654
3021
|
try: async () => {
|
|
2655
3022
|
root = await realpath(root);
|
|
2656
|
-
const registered = await
|
|
3023
|
+
const registered = await Effect13.runPromise(store2.registeredProjects(root));
|
|
2657
3024
|
const found = [];
|
|
2658
3025
|
const entries = await readdir2(root, { withFileTypes: true });
|
|
2659
3026
|
for (const entry of entries) {
|
|
2660
3027
|
if (!entry.isDirectory()) continue;
|
|
2661
|
-
const directory =
|
|
2662
|
-
if (!await stat2(
|
|
3028
|
+
const directory = path6.join(root, entry.name);
|
|
3029
|
+
if (!await stat2(path6.join(directory, ".git")).catch(() => null)) continue;
|
|
2663
3030
|
const previous = registered.find((project2) => project2.path === directory);
|
|
2664
3031
|
let id = await identity(directory, previous?.available ? void 0 : previous?.id);
|
|
2665
3032
|
let existing = registered.find((project2) => project2.id === id);
|
|
2666
|
-
if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat2(
|
|
2667
|
-
id =
|
|
3033
|
+
if (found.some((project2) => project2.id === id) || existing && existing.path !== directory && await stat2(path6.join(existing.path, ".git")).catch(() => null)) {
|
|
3034
|
+
id = randomUUID6();
|
|
2668
3035
|
await writeFile2(
|
|
2669
|
-
|
|
3036
|
+
path6.join(await gitDirectory(directory), "agentrific-project-id"),
|
|
2670
3037
|
id + "\n"
|
|
2671
3038
|
);
|
|
2672
3039
|
existing = void 0;
|
|
2673
3040
|
}
|
|
2674
3041
|
const project = { id, name: entry.name, path: directory, available: true };
|
|
2675
|
-
await
|
|
3042
|
+
await Effect13.runPromise(store2.relocateProject(project, existing?.path));
|
|
2676
3043
|
found.push(project);
|
|
2677
3044
|
}
|
|
2678
3045
|
return [
|
|
@@ -2687,26 +3054,26 @@ function discoverProjects(root, store2) {
|
|
|
2687
3054
|
return projectLock(store2).withPermit(scanProjects(root, store2));
|
|
2688
3055
|
}
|
|
2689
3056
|
function reconnectProject(root, id, input, store2) {
|
|
2690
|
-
return
|
|
3057
|
+
return Effect13.tryPromise({
|
|
2691
3058
|
try: async () => {
|
|
2692
|
-
const projects = await
|
|
3059
|
+
const projects = await Effect13.runPromise(scanProjects(root, store2));
|
|
2693
3060
|
const previous = projects.find((project2) => project2.id === id);
|
|
2694
3061
|
if (!previous || previous.available)
|
|
2695
3062
|
throw new Error("Only a missing project can be reconnected.");
|
|
2696
|
-
const sessions = await
|
|
3063
|
+
const sessions = await Effect13.runPromise(store2.list());
|
|
2697
3064
|
if (sessions.some(
|
|
2698
3065
|
(session) => session.projectId === id && ["starting", "running", "queued"].includes(session.status)
|
|
2699
3066
|
))
|
|
2700
3067
|
throw new Error("Stop this project's running sessions before reconnecting its folder.");
|
|
2701
|
-
const directory = await
|
|
2702
|
-
if (
|
|
3068
|
+
const directory = await Effect13.runPromise(resolveProjectsRoot(input));
|
|
3069
|
+
if (path6.dirname(directory) !== await realpath(root))
|
|
2703
3070
|
throw new Error("Choose a Git repository directly inside the configured projects folder.");
|
|
2704
|
-
const filename =
|
|
3071
|
+
const filename = path6.join(await gitDirectory(directory), "agentrific-project-id");
|
|
2705
3072
|
const replacedId = await identity(directory);
|
|
2706
|
-
const project = { id, name:
|
|
3073
|
+
const project = { id, name: path6.basename(directory), path: directory, available: true };
|
|
2707
3074
|
await writeFile2(filename, id + "\n");
|
|
2708
3075
|
try {
|
|
2709
|
-
await
|
|
3076
|
+
await Effect13.runPromise(store2.relocateProject(project, previous.path, replacedId));
|
|
2710
3077
|
} catch (error) {
|
|
2711
3078
|
await writeFile2(filename, replacedId + "\n");
|
|
2712
3079
|
throw error;
|
|
@@ -2718,21 +3085,67 @@ function reconnectProject(root, id, input, store2) {
|
|
|
2718
3085
|
}
|
|
2719
3086
|
|
|
2720
3087
|
// src/store.ts
|
|
2721
|
-
import { mkdirSync } from "node:fs";
|
|
2722
|
-
import
|
|
3088
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
3089
|
+
import path7 from "node:path";
|
|
3090
|
+
|
|
3091
|
+
// src/push-schema.ts
|
|
3092
|
+
import { z as z14 } from "zod";
|
|
3093
|
+
var PushBindingSchema = z14.object({
|
|
3094
|
+
bindingId: z14.string().uuid(),
|
|
3095
|
+
installationId: z14.string().uuid(),
|
|
3096
|
+
deviceId: z14.string().uuid()
|
|
3097
|
+
});
|
|
3098
|
+
var PushStateSchema = z14.object({
|
|
3099
|
+
bindings: z14.array(PushBindingSchema).max(10),
|
|
3100
|
+
error: z14.string().nullable()
|
|
3101
|
+
});
|
|
3102
|
+
var PendingPushSchema = PushEventSchema.extend({
|
|
3103
|
+
remaining: z14.array(PushBindingSchema),
|
|
3104
|
+
createdAt: z14.number(),
|
|
3105
|
+
attempts: z14.number().int().nonnegative(),
|
|
3106
|
+
nextAt: z14.number()
|
|
3107
|
+
});
|
|
3108
|
+
|
|
3109
|
+
// src/store.ts
|
|
2723
3110
|
import { DatabaseSync } from "node:sqlite";
|
|
2724
|
-
import { Context as Context3, Data as Data3, Effect as
|
|
2725
|
-
import { z as
|
|
2726
|
-
import { randomUUID as
|
|
3111
|
+
import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
|
|
3112
|
+
import { z as z15 } from "zod";
|
|
3113
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
2727
3114
|
var StorageError = class extends Data3.TaggedError("StorageError") {
|
|
2728
3115
|
};
|
|
2729
|
-
var PayloadRow =
|
|
2730
|
-
var EventRow =
|
|
3116
|
+
var PayloadRow = z15.object({ payload: z15.string() });
|
|
3117
|
+
var EventRow = z15.object({ sequence: z15.number(), recorded_at: z15.string(), payload: z15.string() });
|
|
2731
3118
|
var SqliteSessionStore = class {
|
|
2732
3119
|
constructor(db) {
|
|
2733
3120
|
this.db = db;
|
|
2734
3121
|
}
|
|
2735
3122
|
db;
|
|
3123
|
+
pushState() {
|
|
3124
|
+
return this.query(() => {
|
|
3125
|
+
const row = this.db.prepare("SELECT payload FROM settings WHERE id = 'push-state'").get();
|
|
3126
|
+
return row ? PushStateSchema.parse(JSON.parse(PayloadRow.parse(row).payload)) : { bindings: [], error: null };
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
savePushState(state) {
|
|
3130
|
+
return this.query(
|
|
3131
|
+
() => this.db.prepare(
|
|
3132
|
+
"INSERT INTO settings(id, payload) VALUES ('push-state', ?) ON CONFLICT(id) DO UPDATE SET payload = excluded.payload"
|
|
3133
|
+
).run(JSON.stringify(PushStateSchema.parse(state)))
|
|
3134
|
+
);
|
|
3135
|
+
}
|
|
3136
|
+
pendingPush() {
|
|
3137
|
+
return this.query(
|
|
3138
|
+
() => this.db.prepare("SELECT payload FROM push_outbox ORDER BY rowid LIMIT 100").all().map((row) => PendingPushSchema.parse(JSON.parse(PayloadRow.parse(row).payload)))
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
savePendingPush(job) {
|
|
3142
|
+
return this.query(
|
|
3143
|
+
() => this.db.prepare("UPDATE push_outbox SET payload = ? WHERE id = ?").run(JSON.stringify(PendingPushSchema.parse(job)), job.eventId)
|
|
3144
|
+
);
|
|
3145
|
+
}
|
|
3146
|
+
removePush(eventId) {
|
|
3147
|
+
return this.query(() => this.db.prepare("DELETE FROM push_outbox WHERE id = ?").run(eventId));
|
|
3148
|
+
}
|
|
2736
3149
|
serverUpdateStatus() {
|
|
2737
3150
|
return this.query(() => {
|
|
2738
3151
|
const row = this.db.prepare("SELECT payload FROM settings WHERE id = 'server-update-status'").get();
|
|
@@ -2748,11 +3161,11 @@ var SqliteSessionStore = class {
|
|
|
2748
3161
|
}
|
|
2749
3162
|
serverIdentity() {
|
|
2750
3163
|
return this.query(() => {
|
|
2751
|
-
this.db.prepare("INSERT OR IGNORE INTO settings(id, payload) VALUES ('server-id', ?)").run(JSON.stringify(
|
|
3164
|
+
this.db.prepare("INSERT OR IGNORE INTO settings(id, payload) VALUES ('server-id', ?)").run(JSON.stringify(randomUUID7()));
|
|
2752
3165
|
const row = PayloadRow.parse(
|
|
2753
3166
|
this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
|
|
2754
3167
|
);
|
|
2755
|
-
return
|
|
3168
|
+
return z15.string().uuid().parse(JSON.parse(row.payload));
|
|
2756
3169
|
});
|
|
2757
3170
|
}
|
|
2758
3171
|
saveDevice(device, tokenHash) {
|
|
@@ -2789,7 +3202,7 @@ var SqliteSessionStore = class {
|
|
|
2789
3202
|
});
|
|
2790
3203
|
}
|
|
2791
3204
|
query(operation) {
|
|
2792
|
-
return
|
|
3205
|
+
return Effect14.try({
|
|
2793
3206
|
try: operation,
|
|
2794
3207
|
catch: (cause) => new StorageError({
|
|
2795
3208
|
message: cause instanceof Error ? cause.message : "Session storage operation failed",
|
|
@@ -2840,10 +3253,10 @@ var SqliteSessionStore = class {
|
|
|
2840
3253
|
`).all();
|
|
2841
3254
|
const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
|
|
2842
3255
|
for (const row of legacy) {
|
|
2843
|
-
const id =
|
|
3256
|
+
const id = z15.object({ id: z15.string() }).parse(row).id;
|
|
2844
3257
|
insert.run(
|
|
2845
3258
|
id,
|
|
2846
|
-
JSON.stringify({ id, name: id, path:
|
|
3259
|
+
JSON.stringify({ id, name: id, path: path7.join(root, id), available: false })
|
|
2847
3260
|
);
|
|
2848
3261
|
}
|
|
2849
3262
|
return this.db.prepare("SELECT payload FROM projects").all().map((row) => ProjectSchema.parse(JSON.parse(PayloadRow.parse(row).payload)));
|
|
@@ -2880,7 +3293,7 @@ var SqliteSessionStore = class {
|
|
|
2880
3293
|
this.db.prepare(
|
|
2881
3294
|
"UPDATE workflows SET payload = json_set(payload, '$.projectPath', ?) WHERE json_extract(payload, '$.kind') = 'preparation' AND json_extract(payload, '$.projectPath') = ?"
|
|
2882
3295
|
).run(project.path, previousPath);
|
|
2883
|
-
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${
|
|
3296
|
+
const relocate = (workspace) => workspace === previousPath || workspace.startsWith(`${previousPath}${path7.sep}`) ? project.path + workspace.slice(previousPath.length) : workspace;
|
|
2884
3297
|
for (const row of this.db.prepare("SELECT id, payload FROM sessions WHERE json_extract(payload, '$.projectId') = ?").all(project.id)) {
|
|
2885
3298
|
const session = SessionSchema.parse(JSON.parse(PayloadRow.parse(row).payload));
|
|
2886
3299
|
if (session.workspacePath) {
|
|
@@ -2955,6 +3368,31 @@ var SqliteSessionStore = class {
|
|
|
2955
3368
|
return row ? TaskSchema.parse(JSON.parse(PayloadRow.parse(row).payload)) : void 0;
|
|
2956
3369
|
});
|
|
2957
3370
|
}
|
|
3371
|
+
purgeTask(id, sessionIds) {
|
|
3372
|
+
return this.query(() => {
|
|
3373
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
3374
|
+
try {
|
|
3375
|
+
for (const sessionId of sessionIds) {
|
|
3376
|
+
this.db.prepare("DELETE FROM events WHERE session_id = ?").run(sessionId);
|
|
3377
|
+
this.db.prepare("DELETE FROM push_outbox WHERE json_extract(payload, '$.sessionId') = ?").run(sessionId);
|
|
3378
|
+
this.db.prepare("DELETE FROM workflows WHERE json_extract(payload, '$.sessionId') = ?").run(sessionId);
|
|
3379
|
+
this.db.prepare("DELETE FROM sessions WHERE id = ?").run(sessionId);
|
|
3380
|
+
}
|
|
3381
|
+
this.db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
|
3382
|
+
this.db.exec(`UPDATE attachments SET claimed = 0 WHERE NOT EXISTS (
|
|
3383
|
+
SELECT 1 FROM tasks, json_each(tasks.payload, '$.attachments') AS file
|
|
3384
|
+
WHERE json_extract(file.value, '$.id') = attachments.id
|
|
3385
|
+
) AND NOT EXISTS (
|
|
3386
|
+
SELECT 1 FROM events, json_each(events.payload, '$.attachments') AS file
|
|
3387
|
+
WHERE json_extract(file.value, '$.id') = attachments.id
|
|
3388
|
+
)`);
|
|
3389
|
+
this.db.exec("COMMIT");
|
|
3390
|
+
} catch (error) {
|
|
3391
|
+
this.db.exec("ROLLBACK");
|
|
3392
|
+
throw error;
|
|
3393
|
+
}
|
|
3394
|
+
});
|
|
3395
|
+
}
|
|
2958
3396
|
saveAttachment(attachment) {
|
|
2959
3397
|
return this.query(() => {
|
|
2960
3398
|
this.db.prepare("INSERT INTO attachments(id, payload) VALUES (?, ?)").run(attachment.id, JSON.stringify(AttachmentSchema.parse(attachment)));
|
|
@@ -3011,7 +3449,7 @@ var SqliteSessionStore = class {
|
|
|
3011
3449
|
}
|
|
3012
3450
|
});
|
|
3013
3451
|
}
|
|
3014
|
-
append(event) {
|
|
3452
|
+
append(event, notification) {
|
|
3015
3453
|
return this.query(() => {
|
|
3016
3454
|
const sessionId = event.type === "session.updated" ? event.session.id : event.sessionId;
|
|
3017
3455
|
const recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3024,6 +3462,14 @@ var SqliteSessionStore = class {
|
|
|
3024
3462
|
}
|
|
3025
3463
|
if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
|
|
3026
3464
|
const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
|
|
3465
|
+
if (notification) {
|
|
3466
|
+
const count = z15.object({ total: z15.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
|
|
3467
|
+
if (count.total >= 1e3)
|
|
3468
|
+
this.db.prepare(
|
|
3469
|
+
"DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
|
|
3470
|
+
).run();
|
|
3471
|
+
this.db.prepare("INSERT INTO push_outbox(id, payload) VALUES (?, ?)").run(notification.eventId, JSON.stringify(PendingPushSchema.parse(notification)));
|
|
3472
|
+
}
|
|
3027
3473
|
const record = RecordedEventSchema.parse({
|
|
3028
3474
|
sequence: Number(result.lastInsertRowid),
|
|
3029
3475
|
recordedAt,
|
|
@@ -3060,13 +3506,16 @@ var SessionStore = class extends Context3.Service()(
|
|
|
3060
3506
|
static layer(filename) {
|
|
3061
3507
|
return Layer3.effect(
|
|
3062
3508
|
this,
|
|
3063
|
-
|
|
3064
|
-
|
|
3509
|
+
Effect14.acquireRelease(
|
|
3510
|
+
Effect14.try({
|
|
3065
3511
|
try: () => {
|
|
3066
|
-
|
|
3512
|
+
mkdirSync2(path7.dirname(filename), { recursive: true });
|
|
3067
3513
|
const db = new DatabaseSync(filename);
|
|
3514
|
+
chmodSync(filename, 384);
|
|
3515
|
+
for (const suffix of ["-wal", "-shm"])
|
|
3516
|
+
if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
|
|
3068
3517
|
try {
|
|
3069
|
-
const schema =
|
|
3518
|
+
const schema = z15.object({ user_version: z15.number() }).parse(db.prepare("PRAGMA user_version").get());
|
|
3070
3519
|
if (schema.user_version > 1)
|
|
3071
3520
|
throw new Error("This database requires a newer Agentrific server.");
|
|
3072
3521
|
db.exec(`
|
|
@@ -3077,6 +3526,7 @@ var SessionStore = class extends Context3.Service()(
|
|
|
3077
3526
|
CREATE TABLE IF NOT EXISTS settings(id TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
3078
3527
|
CREATE TABLE IF NOT EXISTS sessions(id TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
3079
3528
|
CREATE TABLE IF NOT EXISTS tasks(id TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
3529
|
+
CREATE TABLE IF NOT EXISTS push_outbox(id TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
3080
3530
|
CREATE TABLE IF NOT EXISTS workflows(id TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
3081
3531
|
CREATE TABLE IF NOT EXISTS attachments(id TEXT PRIMARY KEY, payload TEXT NOT NULL, claimed INTEGER NOT NULL DEFAULT 0);
|
|
3082
3532
|
CREATE TABLE IF NOT EXISTS events(sequence INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL REFERENCES sessions(id), recorded_at TEXT NOT NULL, payload TEXT NOT NULL);
|
|
@@ -3092,18 +3542,81 @@ var SessionStore = class extends Context3.Service()(
|
|
|
3092
3542
|
},
|
|
3093
3543
|
catch: (cause) => new StorageError({ message: "Could not open session database", cause })
|
|
3094
3544
|
}),
|
|
3095
|
-
(db) =>
|
|
3096
|
-
).pipe(
|
|
3545
|
+
(db) => Effect14.sync(() => db.close())
|
|
3546
|
+
).pipe(Effect14.map((db) => new SqliteSessionStore(db)))
|
|
3097
3547
|
);
|
|
3098
3548
|
}
|
|
3099
3549
|
};
|
|
3100
3550
|
|
|
3551
|
+
// src/worktree-cleanup.ts
|
|
3552
|
+
import { lstat, rm as rm2 } from "node:fs/promises";
|
|
3553
|
+
import path8 from "node:path";
|
|
3554
|
+
import { Effect as Effect15 } from "effect";
|
|
3555
|
+
var WorktreeCleanup = class {
|
|
3556
|
+
constructor(store2, root, projectsRoot) {
|
|
3557
|
+
this.store = store2;
|
|
3558
|
+
this.root = root;
|
|
3559
|
+
this.projectsRoot = projectsRoot;
|
|
3560
|
+
}
|
|
3561
|
+
store;
|
|
3562
|
+
root;
|
|
3563
|
+
projectsRoot;
|
|
3564
|
+
async remove(session, discardChanges = true) {
|
|
3565
|
+
if (session.gitFlow?.workspace !== "worktree" || !session.workspacePath) return;
|
|
3566
|
+
const workspace = path8.resolve(session.workspacePath);
|
|
3567
|
+
const info = await lstat(workspace).catch((error) => {
|
|
3568
|
+
if (error.code === "ENOENT") return void 0;
|
|
3569
|
+
throw error;
|
|
3570
|
+
});
|
|
3571
|
+
if (workspace !== path8.join(this.root, session.id) || path8.basename(session.id) !== session.id) {
|
|
3572
|
+
if (!info) return;
|
|
3573
|
+
throw new Error("Refusing to remove a worktree outside the managed workspaces directory.");
|
|
3574
|
+
}
|
|
3575
|
+
if (info?.isSymbolicLink()) throw new Error("Refusing to remove a symlink worktree.");
|
|
3576
|
+
const job = await Effect15.runPromise(this.store.workflow(`preparation:${session.id}`));
|
|
3577
|
+
const projects = await Effect15.runPromise(this.store.registeredProjects(this.projectsRoot));
|
|
3578
|
+
const project = job?.kind === "preparation" ? job.projectPath : projects.find((item) => item.id === session.projectId)?.path;
|
|
3579
|
+
if (!project) throw new Error("Reconnect the project before removing its worktree.");
|
|
3580
|
+
if (info) {
|
|
3581
|
+
await workflowCommand(project, "git", [
|
|
3582
|
+
"worktree",
|
|
3583
|
+
"remove",
|
|
3584
|
+
...discardChanges ? ["--force"] : [],
|
|
3585
|
+
workspace
|
|
3586
|
+
]);
|
|
3587
|
+
} else {
|
|
3588
|
+
await workflowCommand(project, "git", ["worktree", "prune"]);
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
async prune(maxAgeDays, now = Date.now()) {
|
|
3592
|
+
if (!Number.isFinite(maxAgeDays) || maxAgeDays <= 0) return;
|
|
3593
|
+
const sessions = await Effect15.runPromise(this.store.list());
|
|
3594
|
+
const failures = [];
|
|
3595
|
+
for (const session of sessions) {
|
|
3596
|
+
const lockId = await sessionTaskLockId(this.store, session);
|
|
3597
|
+
await withTaskLock(lockId, async () => {
|
|
3598
|
+
const current = await Effect15.runPromise(this.store.get(session.id));
|
|
3599
|
+
if (!current || taskStatusForSession(current.status, current) !== "done" || now - Date.parse(current.updatedAt) < maxAgeDays * 864e5)
|
|
3600
|
+
return;
|
|
3601
|
+
await this.remove(current, false);
|
|
3602
|
+
}).catch((error) => failures.push(error));
|
|
3603
|
+
}
|
|
3604
|
+
if (failures.length)
|
|
3605
|
+
throw new AggregateError(failures, "Some completed worktrees could not be pruned.");
|
|
3606
|
+
}
|
|
3607
|
+
};
|
|
3608
|
+
async function removeRunFiles(dataRoot2, sessionId) {
|
|
3609
|
+
if (path8.basename(sessionId) !== sessionId || sessionId === "." || sessionId === "..")
|
|
3610
|
+
throw new Error("Invalid session directory.");
|
|
3611
|
+
await rm2(path8.join(dataRoot2, "pi", sessionId), { recursive: true, force: true });
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3101
3614
|
// src/index.ts
|
|
3102
3615
|
var host = process.env.AGENTRIFIC_HOST ?? "127.0.0.1";
|
|
3103
3616
|
var port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
|
|
3104
|
-
var defaultProjectsRoot =
|
|
3105
|
-
var worktreesRoot =
|
|
3106
|
-
process.env.AGENTRIFIC_WORKTREES_DIR ??
|
|
3617
|
+
var defaultProjectsRoot = path9.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
|
|
3618
|
+
var worktreesRoot = path9.resolve(
|
|
3619
|
+
process.env.AGENTRIFIC_WORKTREES_DIR ?? path9.join(homedir2(), ".agentrific", "workspaces")
|
|
3107
3620
|
);
|
|
3108
3621
|
var execFileAsync = promisify2(execFile2);
|
|
3109
3622
|
if (host !== "127.0.0.1" && host !== "::1")
|
|
@@ -3126,13 +3639,18 @@ var app = Fastify({
|
|
|
3126
3639
|
}
|
|
3127
3640
|
}
|
|
3128
3641
|
});
|
|
3129
|
-
var dataRoot =
|
|
3642
|
+
var dataRoot = path9.resolve(process.env.AGENTRIFIC_DATA_DIR ?? "./data");
|
|
3130
3643
|
var releaseLock = await acquireServerLock(dataRoot);
|
|
3131
|
-
var effects = ManagedRuntime.make(SessionStore.layer(
|
|
3644
|
+
var effects = ManagedRuntime.make(SessionStore.layer(path9.join(dataRoot, "sessions.sqlite")));
|
|
3132
3645
|
var store = await effects.runPromise(SessionStore);
|
|
3133
|
-
var security = new ServerSecurity(store, await
|
|
3646
|
+
var security = new ServerSecurity(store, await Effect16.runPromise(store.serverIdentity()));
|
|
3647
|
+
var testPushUrl = process.env.AGENTRIFIC_PUSH_TEST_URL;
|
|
3648
|
+
if (testPushUrl && (process.env.AGENTRIFIC_TEST_MODE !== "1" || !new URL(testPushUrl).hostname.endsWith(".ts.net") || new URL(testPushUrl).protocol !== "https:"))
|
|
3649
|
+
throw new Error("Test relay requires explicit test mode and Tailscale HTTPS.");
|
|
3650
|
+
var push = new ServerPush(store, security.serverId, dataRoot, fetch, testPushUrl);
|
|
3134
3651
|
var allowedOrigins = [
|
|
3135
3652
|
.../* @__PURE__ */ new Set([
|
|
3653
|
+
"agentrific-desktop://app",
|
|
3136
3654
|
...(process.env.AGENTRIFIC_ALLOWED_ORIGINS ?? "http://localhost:8081,http://127.0.0.1:8081").split(","),
|
|
3137
3655
|
`http://${host === "::1" ? "[::1]" : host}:${port}`,
|
|
3138
3656
|
...process.env.AGENTRIFIC_PUBLIC_URL ? [new URL(process.env.AGENTRIFIC_PUBLIC_URL).origin] : []
|
|
@@ -3143,14 +3661,15 @@ await app.register(cors, {
|
|
|
3143
3661
|
methods: ["GET", "POST", "PATCH", "DELETE"]
|
|
3144
3662
|
});
|
|
3145
3663
|
await registerSecurity(app, security, serverVersion, allowedOrigins);
|
|
3664
|
+
registerPushRoutes(app, push, security);
|
|
3146
3665
|
var attachmentEffects = ManagedRuntime.make(
|
|
3147
|
-
Attachments.layer(
|
|
3666
|
+
Attachments.layer(path9.join(dataRoot, "attachments"), store)
|
|
3148
3667
|
);
|
|
3149
3668
|
var attachments = await attachmentEffects.runPromise(Attachments);
|
|
3150
|
-
await
|
|
3669
|
+
await Effect16.runPromise(attachments.cleanup());
|
|
3151
3670
|
var attachmentCleanup = setInterval(
|
|
3152
3671
|
() => {
|
|
3153
|
-
void
|
|
3672
|
+
void Effect16.runPromise(attachments.cleanup()).catch((error) => app.log.error(error));
|
|
3154
3673
|
},
|
|
3155
3674
|
60 * 60 * 1e3
|
|
3156
3675
|
);
|
|
@@ -3161,14 +3680,14 @@ await app.register(multipart, {
|
|
|
3161
3680
|
app.post(
|
|
3162
3681
|
"/api/projects/:projectId/attachments",
|
|
3163
3682
|
async (request, reply) => {
|
|
3164
|
-
const root = await
|
|
3165
|
-
const projects = await
|
|
3683
|
+
const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
3684
|
+
const projects = await Effect16.runPromise(discoverProjects(root, store));
|
|
3166
3685
|
if (!projects.some((project) => project.id === request.params.projectId && project.available))
|
|
3167
3686
|
return reply.code(404).send({ error: "Project not found." });
|
|
3168
3687
|
try {
|
|
3169
3688
|
const file = await request.file();
|
|
3170
3689
|
if (!file) return reply.code(400).send({ error: "Choose a file to upload." });
|
|
3171
|
-
return reply.code(201).send(await
|
|
3690
|
+
return reply.code(201).send(await Effect16.runPromise(attachments.upload(request.params.projectId, file)));
|
|
3172
3691
|
} catch (error) {
|
|
3173
3692
|
return reply.code(400).send({ error: error instanceof Error ? error.message : "Upload failed." });
|
|
3174
3693
|
}
|
|
@@ -3177,7 +3696,7 @@ app.post(
|
|
|
3177
3696
|
app.get(
|
|
3178
3697
|
"/api/projects/:projectId/attachments/:id/content",
|
|
3179
3698
|
async (request, reply) => {
|
|
3180
|
-
const attachment = await
|
|
3699
|
+
const attachment = await Effect16.runPromise(store.getAttachment(request.params.id));
|
|
3181
3700
|
if (!attachment || attachment.projectId !== request.params.projectId)
|
|
3182
3701
|
return reply.code(404).send({ error: "Attachment not found." });
|
|
3183
3702
|
const preview = request.query.preview === "1" && attachment.kind === "image";
|
|
@@ -3191,7 +3710,7 @@ app.get(
|
|
|
3191
3710
|
}
|
|
3192
3711
|
);
|
|
3193
3712
|
function publish(event, workflowUpdate = false) {
|
|
3194
|
-
const previous = event.type === "session.updated" ?
|
|
3713
|
+
const previous = event.type === "session.updated" ? Effect16.runSync(store.get(event.session.id)) : void 0;
|
|
3195
3714
|
const emittedEvent = event.type === "session.updated" && previous && !workflowUpdate ? {
|
|
3196
3715
|
type: "session.updated",
|
|
3197
3716
|
session: {
|
|
@@ -3200,7 +3719,8 @@ function publish(event, workflowUpdate = false) {
|
|
|
3200
3719
|
preparation: previous.preparation
|
|
3201
3720
|
}
|
|
3202
3721
|
} : event;
|
|
3203
|
-
const
|
|
3722
|
+
const notification = emittedEvent.type === "session.updated" ? push.pending(emittedEvent.session, previous) : void 0;
|
|
3723
|
+
const record = Effect16.runSync(store.append(emittedEvent, notification));
|
|
3204
3724
|
const message = JSON.stringify({ type: "event", record });
|
|
3205
3725
|
for (const client of security.sockets.keys()) {
|
|
3206
3726
|
if (!(client instanceof WebSocket) || client.readyState !== WebSocket.OPEN) continue;
|
|
@@ -3213,8 +3733,8 @@ var delivery = new DeliveryCoordinator(store, {
|
|
|
3213
3733
|
publish: (event) => publish(event, true),
|
|
3214
3734
|
draft: deliveryWriter(models),
|
|
3215
3735
|
projectPath: async (session) => {
|
|
3216
|
-
const projectsRoot = await
|
|
3217
|
-
const project = (await
|
|
3736
|
+
const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
3737
|
+
const project = (await Effect16.runPromise(discoverProjects(projectsRoot, store))).find(
|
|
3218
3738
|
(item) => item.id === session.projectId && item.available
|
|
3219
3739
|
);
|
|
3220
3740
|
if (!project) throw new Error("Reconnect the project folder before delivering changes.");
|
|
@@ -3227,12 +3747,27 @@ var agentEffects = ManagedRuntime.make(
|
|
|
3227
3747
|
createPiFactory(attachments, models, delivery),
|
|
3228
3748
|
publish,
|
|
3229
3749
|
worktreesRoot,
|
|
3230
|
-
|
|
3231
|
-
(sessionId, messageId) =>
|
|
3750
|
+
path9.join(dataRoot, "pi"),
|
|
3751
|
+
(sessionId, messageId) => Effect16.runSync(store.hasMessage(sessionId, messageId)),
|
|
3232
3752
|
(session) => delivery.settled(session.id)
|
|
3233
3753
|
)
|
|
3234
3754
|
);
|
|
3235
3755
|
var runtime = await agentEffects.runPromise(AgentSessions);
|
|
3756
|
+
var worktreeCleanup = new WorktreeCleanup(store, worktreesRoot, defaultProjectsRoot);
|
|
3757
|
+
var maxWorktreeAgeDays = Number(process.env.AGENTRIFIC_DONE_WORKTREE_MAX_AGE_DAYS ?? 0);
|
|
3758
|
+
if (!Number.isInteger(maxWorktreeAgeDays) || maxWorktreeAgeDays < 0)
|
|
3759
|
+
throw new Error("AGENTRIFIC_DONE_WORKTREE_MAX_AGE_DAYS must be a nonnegative integer.");
|
|
3760
|
+
var pruneWorktrees = () => withTaskLock("worktree-pruning", async () => {
|
|
3761
|
+
await worktreeCleanup.prune(maxWorktreeAgeDays);
|
|
3762
|
+
});
|
|
3763
|
+
var worktreeCleanupTimer = setInterval(
|
|
3764
|
+
() => {
|
|
3765
|
+
void pruneWorktrees().catch((error) => app.log.error(error));
|
|
3766
|
+
},
|
|
3767
|
+
60 * 60 * 1e3
|
|
3768
|
+
);
|
|
3769
|
+
worktreeCleanupTimer.unref();
|
|
3770
|
+
void pruneWorktrees().catch((error) => app.log.error(error));
|
|
3236
3771
|
var updates = new UpdateDiscovery(store);
|
|
3237
3772
|
var maintenance = new Maintenance(() => [
|
|
3238
3773
|
...runtime.workloads(),
|
|
@@ -3248,7 +3783,7 @@ var stopUpdateDiscovery = updates.start(
|
|
|
3248
3783
|
() => maintenance.ready(),
|
|
3249
3784
|
(error) => app.log.error(error)
|
|
3250
3785
|
);
|
|
3251
|
-
for (const session of await
|
|
3786
|
+
for (const session of await Effect16.runPromise(store.list())) {
|
|
3252
3787
|
if (session.gitDelivery?.status === "pending") {
|
|
3253
3788
|
publish(
|
|
3254
3789
|
{
|
|
@@ -3292,7 +3827,7 @@ async function githubCliStatus() {
|
|
|
3292
3827
|
}
|
|
3293
3828
|
app.get("/api/connections", async () => githubCliStatus());
|
|
3294
3829
|
app.get("/api/settings", async () => ({
|
|
3295
|
-
projectsRoot: await
|
|
3830
|
+
projectsRoot: await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot)),
|
|
3296
3831
|
defaultModel: await models.defaultIdentity()
|
|
3297
3832
|
}));
|
|
3298
3833
|
app.get("/api/models", async (_request, reply) => {
|
|
@@ -3320,7 +3855,7 @@ app.post("/api/settings/model", async (request, reply) => {
|
|
|
3320
3855
|
try {
|
|
3321
3856
|
const defaultModel = await models.setDefault(parsed.data);
|
|
3322
3857
|
return {
|
|
3323
|
-
projectsRoot: await
|
|
3858
|
+
projectsRoot: await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot)),
|
|
3324
3859
|
defaultModel
|
|
3325
3860
|
};
|
|
3326
3861
|
} catch {
|
|
@@ -3329,54 +3864,57 @@ app.post("/api/settings/model", async (request, reply) => {
|
|
|
3329
3864
|
});
|
|
3330
3865
|
}
|
|
3331
3866
|
});
|
|
3332
|
-
app.post(
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
return reply.code(
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
});
|
|
3867
|
+
app.post(
|
|
3868
|
+
"/api/sessions/:id/model",
|
|
3869
|
+
async (request, reply) => withSessionTaskLock(request.params.id, async () => {
|
|
3870
|
+
const parsed = ModelReferenceSchema.safeParse(request.body);
|
|
3871
|
+
const session = await Effect16.runPromise(store.get(request.params.id));
|
|
3872
|
+
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
3873
|
+
if (!parsed.success)
|
|
3874
|
+
return reply.code(400).send({ error: "Choose a model from the server catalogue." });
|
|
3875
|
+
try {
|
|
3876
|
+
await models.resolve(parsed.data);
|
|
3877
|
+
return await Effect16.runPromise(runtime.setModel(session, parsed.data));
|
|
3878
|
+
} catch {
|
|
3879
|
+
return reply.code(409).send({
|
|
3880
|
+
error: "Could not change the model. Wait for the current reply to finish and choose an available model."
|
|
3881
|
+
});
|
|
3882
|
+
}
|
|
3883
|
+
})
|
|
3884
|
+
);
|
|
3347
3885
|
app.post("/api/settings", async (request, reply) => {
|
|
3348
3886
|
const parsed = UpdateSettingsSchema.safeParse(request.body);
|
|
3349
3887
|
if (!parsed.success) return reply.code(400).send({ error: "Enter a projects folder." });
|
|
3350
|
-
const result = await
|
|
3351
|
-
resolveProjectsRoot(parsed.data.projectsRoot).pipe(
|
|
3888
|
+
const result = await Effect16.runPromise(
|
|
3889
|
+
resolveProjectsRoot(parsed.data.projectsRoot).pipe(Effect16.result)
|
|
3352
3890
|
);
|
|
3353
3891
|
if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
|
|
3354
|
-
return
|
|
3892
|
+
return Effect16.runPromise(store.setProjectsRoot(result.success));
|
|
3355
3893
|
});
|
|
3356
3894
|
app.get("/api/projects", async (_request, reply) => {
|
|
3357
|
-
const root = await
|
|
3358
|
-
const result = await
|
|
3895
|
+
const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
3896
|
+
const result = await Effect16.runPromise(discoverProjects(root, store).pipe(Effect16.result));
|
|
3359
3897
|
if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
|
|
3360
3898
|
return result.success;
|
|
3361
3899
|
});
|
|
3362
3900
|
app.get("/api/projects/:id", async (request, reply) => {
|
|
3363
|
-
const root = await
|
|
3364
|
-
const result = await
|
|
3901
|
+
const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
3902
|
+
const result = await Effect16.runPromise(discoverProjects(root, store).pipe(Effect16.result));
|
|
3365
3903
|
if (result._tag === "Failure") return reply.code(400).send({ error: result.failure.message });
|
|
3366
3904
|
const project = result.success.find((item) => item.id === request.params.id);
|
|
3367
3905
|
return project ?? reply.code(404).send({ error: "Project not found on this server." });
|
|
3368
3906
|
});
|
|
3369
3907
|
app.post("/api/projects/:id/reconnect", async (request, reply) => {
|
|
3370
|
-
const parsed =
|
|
3908
|
+
const parsed = z16.object({ path: z16.string().trim().min(1).max(4096) }).safeParse(request.body);
|
|
3371
3909
|
if (!parsed.success)
|
|
3372
3910
|
return reply.code(400).send({ error: "Enter the project's new folder path." });
|
|
3373
|
-
const root = await
|
|
3374
|
-
const result = await
|
|
3375
|
-
reconnectProject(root, request.params.id, parsed.data.path, store).pipe(
|
|
3911
|
+
const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
3912
|
+
const result = await Effect16.runPromise(
|
|
3913
|
+
reconnectProject(root, request.params.id, parsed.data.path, store).pipe(Effect16.result)
|
|
3376
3914
|
);
|
|
3377
3915
|
return result._tag === "Failure" ? reply.code(400).send({ error: result.failure.message }) : result.success;
|
|
3378
3916
|
});
|
|
3379
|
-
app.get("/api/sessions", () =>
|
|
3917
|
+
app.get("/api/sessions", () => Effect16.runPromise(store.list()));
|
|
3380
3918
|
app.get(
|
|
3381
3919
|
"/api/tasks",
|
|
3382
3920
|
(request) => boardTasks(store, request.query.projectId)
|
|
@@ -3389,15 +3927,15 @@ app.post("/api/tasks", async (request, reply) => {
|
|
|
3389
3927
|
if (!github.githubCli.authenticated)
|
|
3390
3928
|
return reply.code(400).send({ error: "GitHub CLI must be authenticated on the server to open a PR." });
|
|
3391
3929
|
}
|
|
3392
|
-
const projectsRoot = await
|
|
3393
|
-
const projects = await
|
|
3394
|
-
discoverProjects(projectsRoot, store).pipe(
|
|
3930
|
+
const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
3931
|
+
const projects = await Effect16.runPromise(
|
|
3932
|
+
discoverProjects(projectsRoot, store).pipe(Effect16.result)
|
|
3395
3933
|
);
|
|
3396
3934
|
if (projects._tag === "Failure" || !projects.success.some((project) => project.id === parsed.data.projectId && project.available))
|
|
3397
3935
|
return reply.code(404).send({ error: "Project not found in the server's projects folder." });
|
|
3398
3936
|
let files;
|
|
3399
3937
|
try {
|
|
3400
|
-
files = await
|
|
3938
|
+
files = await Effect16.runPromise(
|
|
3401
3939
|
attachments.resolve(parsed.data.projectId, parsed.data.attachmentIds ?? [])
|
|
3402
3940
|
);
|
|
3403
3941
|
} catch (error) {
|
|
@@ -3405,7 +3943,7 @@ app.post("/api/tasks", async (request, reply) => {
|
|
|
3405
3943
|
}
|
|
3406
3944
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3407
3945
|
const task = TaskSchema.parse({
|
|
3408
|
-
id:
|
|
3946
|
+
id: randomUUID8(),
|
|
3409
3947
|
...parsed.data,
|
|
3410
3948
|
attachments: files,
|
|
3411
3949
|
status: "backlog",
|
|
@@ -3413,7 +3951,7 @@ app.post("/api/tasks", async (request, reply) => {
|
|
|
3413
3951
|
createdAt: now,
|
|
3414
3952
|
updatedAt: now
|
|
3415
3953
|
});
|
|
3416
|
-
return reply.code(201).send(await
|
|
3954
|
+
return reply.code(201).send(await Effect16.runPromise(store.saveTask(task)));
|
|
3417
3955
|
});
|
|
3418
3956
|
app.patch("/api/tasks/:id", async (request, reply) => {
|
|
3419
3957
|
const parsed = UpdateTaskSchema.safeParse(request.body);
|
|
@@ -3428,8 +3966,8 @@ app.patch("/api/tasks/:id", async (request, reply) => {
|
|
|
3428
3966
|
return reply.code(400).send({ error: "GitHub CLI must be authenticated on the server to open a PR." });
|
|
3429
3967
|
}
|
|
3430
3968
|
return await editTask(store, request.params.id, parsed.data, async (projectId, ids) => {
|
|
3431
|
-
const result = await
|
|
3432
|
-
attachments.resolve(projectId, ids).pipe(
|
|
3969
|
+
const result = await Effect16.runPromise(
|
|
3970
|
+
attachments.resolve(projectId, ids).pipe(Effect16.result)
|
|
3433
3971
|
);
|
|
3434
3972
|
if (result._tag === "Failure") throw new TaskActionError(result.failure.message, 400);
|
|
3435
3973
|
return result.success;
|
|
@@ -3442,7 +3980,12 @@ app.patch("/api/tasks/:id", async (request, reply) => {
|
|
|
3442
3980
|
});
|
|
3443
3981
|
app.delete("/api/tasks/:id", async (request, reply) => {
|
|
3444
3982
|
try {
|
|
3445
|
-
await deleteTask(store, request.params.id)
|
|
3983
|
+
await deleteTask(store, request.params.id, async (session) => {
|
|
3984
|
+
await Effect16.runPromise(runtime.forget(session.id));
|
|
3985
|
+
await worktreeCleanup.remove(session);
|
|
3986
|
+
await removeRunFiles(dataRoot, session.id);
|
|
3987
|
+
});
|
|
3988
|
+
await Effect16.runPromise(attachments.cleanup());
|
|
3446
3989
|
return reply.code(204).send();
|
|
3447
3990
|
} catch (error) {
|
|
3448
3991
|
if (error instanceof TaskActionError)
|
|
@@ -3451,31 +3994,34 @@ app.delete("/api/tasks/:id", async (request, reply) => {
|
|
|
3451
3994
|
}
|
|
3452
3995
|
});
|
|
3453
3996
|
app.get("/api/sessions/:id", async (request, reply) => {
|
|
3454
|
-
const session = await
|
|
3997
|
+
const session = await Effect16.runPromise(store.get(request.params.id));
|
|
3455
3998
|
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
3456
3999
|
return session;
|
|
3457
4000
|
});
|
|
3458
|
-
app.post(
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
});
|
|
4001
|
+
app.post(
|
|
4002
|
+
"/api/sessions/:id/delivery",
|
|
4003
|
+
async (request, reply) => withSessionTaskLock(request.params.id, async () => {
|
|
4004
|
+
const session = await Effect16.runPromise(store.get(request.params.id));
|
|
4005
|
+
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
4006
|
+
try {
|
|
4007
|
+
const result = await delivery.retry(session.id);
|
|
4008
|
+
return reply.code(202).send(result);
|
|
4009
|
+
} catch (error) {
|
|
4010
|
+
return reply.code(409).send({ error: error instanceof Error ? error.message : "Delivery could not be retried." });
|
|
4011
|
+
}
|
|
4012
|
+
})
|
|
4013
|
+
);
|
|
3468
4014
|
app.post(
|
|
3469
4015
|
"/api/sessions/:id/preparation/retry",
|
|
3470
|
-
async (request, reply) => {
|
|
4016
|
+
async (request, reply) => withSessionTaskLock(request.params.id, async () => {
|
|
3471
4017
|
let preparedSession;
|
|
3472
4018
|
try {
|
|
3473
4019
|
const prepared = await preparation.retry(request.params.id);
|
|
3474
4020
|
preparedSession = prepared.session;
|
|
3475
|
-
const files = await
|
|
4021
|
+
const files = await Effect16.runPromise(
|
|
3476
4022
|
attachments.resolve(prepared.session.projectId, prepared.input.attachmentIds ?? [])
|
|
3477
4023
|
);
|
|
3478
|
-
await
|
|
4024
|
+
await Effect16.runPromise(runtime.send(prepared.session, prepared.input.prompt, files));
|
|
3479
4025
|
return reply.code(202).send(prepared.session);
|
|
3480
4026
|
} catch (error) {
|
|
3481
4027
|
if (preparedSession) {
|
|
@@ -3489,18 +4035,18 @@ app.post(
|
|
|
3489
4035
|
error: error instanceof Error ? error.message : "Preparation could not be retried."
|
|
3490
4036
|
});
|
|
3491
4037
|
}
|
|
3492
|
-
}
|
|
4038
|
+
})
|
|
3493
4039
|
);
|
|
3494
|
-
var HistoryQuery =
|
|
3495
|
-
after:
|
|
3496
|
-
limit:
|
|
4040
|
+
var HistoryQuery = z16.object({
|
|
4041
|
+
after: z16.coerce.number().int().nonnegative().default(0),
|
|
4042
|
+
limit: z16.coerce.number().int().min(1).max(1e3).default(500)
|
|
3497
4043
|
});
|
|
3498
4044
|
app.get("/api/sessions/:id/events", async (request, reply) => {
|
|
3499
4045
|
const query = HistoryQuery.safeParse(request.query);
|
|
3500
4046
|
if (!query.success) return reply.code(400).send({ error: "Invalid history cursor or limit." });
|
|
3501
|
-
const session = await
|
|
4047
|
+
const session = await Effect16.runPromise(store.get(request.params.id));
|
|
3502
4048
|
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
3503
|
-
return
|
|
4049
|
+
return Effect16.runPromise(store.history(session.id, query.data.after, query.data.limit));
|
|
3504
4050
|
});
|
|
3505
4051
|
async function selectSessionModel(input, hasImages) {
|
|
3506
4052
|
try {
|
|
@@ -3520,26 +4066,26 @@ async function selectSessionModel(input, hasImages) {
|
|
|
3520
4066
|
}
|
|
3521
4067
|
}
|
|
3522
4068
|
async function resolveSessionProject(projectId) {
|
|
3523
|
-
const projectsRoot = await
|
|
3524
|
-
const project = (await
|
|
4069
|
+
const projectsRoot = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4070
|
+
const project = (await Effect16.runPromise(discoverProjects(projectsRoot, store))).find(
|
|
3525
4071
|
(item) => item.id === projectId && item.available
|
|
3526
4072
|
);
|
|
3527
4073
|
if (!project)
|
|
3528
4074
|
return { error: "Project folder is missing. Reconnect it before starting a run.", status: 404 };
|
|
3529
4075
|
const projectPath = await realpath2(project.path);
|
|
3530
4076
|
const resolvedRoot = await realpath2(projectsRoot);
|
|
3531
|
-
if (
|
|
4077
|
+
if (path9.dirname(projectPath) !== resolvedRoot)
|
|
3532
4078
|
return { error: "Project path is outside projects root.", status: 400 };
|
|
3533
4079
|
return { projectPath };
|
|
3534
4080
|
}
|
|
3535
4081
|
async function resolveSessionWorkspace(id, gitFlow, projectPath) {
|
|
3536
|
-
const worktreePath =
|
|
4082
|
+
const worktreePath = path9.join(worktreesRoot, id);
|
|
3537
4083
|
const branch = gitFlow ? `agentrific/${id.slice(0, 8)}` : null;
|
|
3538
4084
|
const useProjectCheckout = gitFlow?.workspace === "checkout";
|
|
3539
4085
|
const workspacePath = !gitFlow || useProjectCheckout ? projectPath : worktreePath;
|
|
3540
4086
|
if (useProjectCheckout) {
|
|
3541
4087
|
const serverPath = await realpath2(process.cwd());
|
|
3542
|
-
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${
|
|
4088
|
+
if (serverPath === projectPath || serverPath.startsWith(`${projectPath}${path9.sep}`))
|
|
3543
4089
|
return {
|
|
3544
4090
|
error: "The server runs from this project. Choose a worktree for this task.",
|
|
3545
4091
|
status: 409
|
|
@@ -3563,7 +4109,7 @@ async function createAndStartSession(input) {
|
|
|
3563
4109
|
const projectPath = location.projectPath;
|
|
3564
4110
|
let files;
|
|
3565
4111
|
try {
|
|
3566
|
-
files = await
|
|
4112
|
+
files = await Effect16.runPromise(
|
|
3567
4113
|
attachments.resolve(input.projectId, input.attachmentIds ?? [])
|
|
3568
4114
|
);
|
|
3569
4115
|
} catch (error) {
|
|
@@ -3576,11 +4122,11 @@ async function createAndStartSession(input) {
|
|
|
3576
4122
|
if (selection.error !== void 0) return { error: selection.error, status: selection.status };
|
|
3577
4123
|
const selectedModel = selection.model;
|
|
3578
4124
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3579
|
-
const task = input.taskId ? await
|
|
4125
|
+
const task = input.taskId ? await Effect16.runPromise(store.getTask(input.taskId)) : void 0;
|
|
3580
4126
|
if (input.taskId && sessionTaskUnavailable(task, input.projectId))
|
|
3581
4127
|
return { error: "Task not found.", status: 404 };
|
|
3582
4128
|
const session = SessionSchema.parse({
|
|
3583
|
-
id:
|
|
4129
|
+
id: randomUUID8(),
|
|
3584
4130
|
taskId: input.taskId ?? null,
|
|
3585
4131
|
projectId: input.projectId,
|
|
3586
4132
|
title: task?.title ?? input.prompt.slice(0, 80),
|
|
@@ -3605,7 +4151,7 @@ async function createAndStartSession(input) {
|
|
|
3605
4151
|
);
|
|
3606
4152
|
} catch (error) {
|
|
3607
4153
|
if (task)
|
|
3608
|
-
await
|
|
4154
|
+
await Effect16.runPromise(
|
|
3609
4155
|
store.saveTask({ ...task, sessionId: session.id, updatedAt: (/* @__PURE__ */ new Date()).toISOString() })
|
|
3610
4156
|
);
|
|
3611
4157
|
return {
|
|
@@ -3614,7 +4160,7 @@ async function createAndStartSession(input) {
|
|
|
3614
4160
|
};
|
|
3615
4161
|
}
|
|
3616
4162
|
try {
|
|
3617
|
-
await
|
|
4163
|
+
await Effect16.runPromise(runtime.send(running, input.prompt, files));
|
|
3618
4164
|
} catch (error) {
|
|
3619
4165
|
publish({
|
|
3620
4166
|
type: "session.updated",
|
|
@@ -3623,7 +4169,7 @@ async function createAndStartSession(input) {
|
|
|
3623
4169
|
publish({ type: "error", sessionId: session.id, message: String(error) });
|
|
3624
4170
|
return { error: "Could not start the agent session.", status: 500 };
|
|
3625
4171
|
}
|
|
3626
|
-
return { session: await
|
|
4172
|
+
return { session: await Effect16.runPromise(store.get(session.id)), status: 201 };
|
|
3627
4173
|
}
|
|
3628
4174
|
app.post("/api/sessions", async (request, reply) => {
|
|
3629
4175
|
const parsed = CreateSessionSchema.safeParse(request.body);
|
|
@@ -3635,17 +4181,17 @@ app.post("/api/sessions", async (request, reply) => {
|
|
|
3635
4181
|
if (!result.session) return reply.code(result.status).send({ error: result.error });
|
|
3636
4182
|
return reply.code(201).send(result.session);
|
|
3637
4183
|
});
|
|
3638
|
-
var StartTaskBody =
|
|
4184
|
+
var StartTaskBody = z16.object({ prompt: z16.string().trim().max(2e4).optional() }).default({});
|
|
3639
4185
|
function startTask(id, instructionsOverride, automated = false) {
|
|
3640
4186
|
return withTaskLock(
|
|
3641
4187
|
"agent-dispatch",
|
|
3642
4188
|
() => withTaskLock(id, async () => {
|
|
3643
4189
|
maintenance.assertReady();
|
|
3644
|
-
const task = await
|
|
4190
|
+
const task = await Effect16.runPromise(store.getTask(id));
|
|
3645
4191
|
if (!task || task.deletedAt) throw new TaskActionError("Task not found.", 404);
|
|
3646
4192
|
if (automated) {
|
|
3647
|
-
const state = await
|
|
3648
|
-
if (activeAgents(await
|
|
4193
|
+
const state = await Effect16.runPromise(store.automation());
|
|
4194
|
+
if (activeAgents(await Effect16.runPromise(store.list())) >= state.config.parallelism) return;
|
|
3649
4195
|
const backlog = await boardTasks(store);
|
|
3650
4196
|
if (!backlog.some((item) => item.id === id && item.status === "backlog")) return;
|
|
3651
4197
|
}
|
|
@@ -3653,7 +4199,7 @@ function startTask(id, instructionsOverride, automated = false) {
|
|
|
3653
4199
|
const prompt = instructions ? `${task.title}
|
|
3654
4200
|
|
|
3655
4201
|
${instructions}` : task.title;
|
|
3656
|
-
const linkedSession = task.sessionId ? await
|
|
4202
|
+
const linkedSession = task.sessionId ? await Effect16.runPromise(store.get(task.sessionId)) : void 0;
|
|
3657
4203
|
if (linkedSession && (["starting", "running", "queued"].includes(linkedSession.status) || linkedSession.gitDelivery?.status === "pending"))
|
|
3658
4204
|
throw new TaskActionError("This task already has a running agent.", 409);
|
|
3659
4205
|
const result = await createAndStartSession({
|
|
@@ -3667,7 +4213,7 @@ ${instructions}` : task.title;
|
|
|
3667
4213
|
});
|
|
3668
4214
|
if (!result.session)
|
|
3669
4215
|
throw new TaskActionError(result.error ?? "Could not start task.", result.status);
|
|
3670
|
-
const updatedTask = await
|
|
4216
|
+
const updatedTask = await Effect16.runPromise(
|
|
3671
4217
|
store.saveTask({
|
|
3672
4218
|
...task,
|
|
3673
4219
|
status: taskStatusForSession(result.session.status, result.session),
|
|
@@ -3702,8 +4248,8 @@ app.patch("/api/automation", async (request, reply) => {
|
|
|
3702
4248
|
const parsed = AutomationConfigSchema.safeParse(request.body);
|
|
3703
4249
|
if (!parsed.success)
|
|
3704
4250
|
return reply.code(400).send({ error: parsed.error.issues.map((issue) => issue.message).join(" ") });
|
|
3705
|
-
const root = await
|
|
3706
|
-
const projects = await
|
|
4251
|
+
const root = await Effect16.runPromise(store.projectsRoot(defaultProjectsRoot));
|
|
4252
|
+
const projects = await Effect16.runPromise(discoverProjects(root, store));
|
|
3707
4253
|
if (parsed.data.projectIds.some(
|
|
3708
4254
|
(id) => !projects.some((project) => project.id === id && project.available)
|
|
3709
4255
|
))
|
|
@@ -3713,7 +4259,7 @@ app.patch("/api/automation", async (request, reply) => {
|
|
|
3713
4259
|
return automation.configure(parsed.data);
|
|
3714
4260
|
});
|
|
3715
4261
|
app.patch("/api/automation/queue", async (request, reply) => {
|
|
3716
|
-
const parsed =
|
|
4262
|
+
const parsed = z16.object({ taskId: z16.string().min(1), beforeId: z16.string().min(1).nullable() }).safeParse(request.body);
|
|
3717
4263
|
if (!parsed.success)
|
|
3718
4264
|
return reply.code(400).send({ error: "Choose a task and a queue position." });
|
|
3719
4265
|
try {
|
|
@@ -3730,8 +4276,8 @@ var automationTimer = setInterval(() => {
|
|
|
3730
4276
|
automationTimer.unref();
|
|
3731
4277
|
app.post(
|
|
3732
4278
|
"/api/sessions/:id/messages",
|
|
3733
|
-
async (request, reply) => {
|
|
3734
|
-
const session = await
|
|
4279
|
+
async (request, reply) => withSessionTaskLock(request.params.id, async () => {
|
|
4280
|
+
const session = await Effect16.runPromise(store.get(request.params.id));
|
|
3735
4281
|
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
3736
4282
|
const parsed = SendMessageSchema.safeParse(request.body);
|
|
3737
4283
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
|
|
@@ -3739,27 +4285,35 @@ app.post(
|
|
|
3739
4285
|
await delivery.assertCanWork(session.id);
|
|
3740
4286
|
if (session.preparation && session.preparation.status !== "ready")
|
|
3741
4287
|
throw new Error("Retry workspace preparation before sending a message.");
|
|
3742
|
-
const files = await
|
|
4288
|
+
const files = await Effect16.runPromise(
|
|
3743
4289
|
attachments.resolve(session.projectId, parsed.data.attachmentIds ?? [])
|
|
3744
4290
|
);
|
|
3745
|
-
await
|
|
4291
|
+
await Effect16.runPromise(
|
|
3746
4292
|
runtime.send(session, parsed.data.message, files, parsed.data.clientMessageId)
|
|
3747
4293
|
);
|
|
3748
4294
|
return reply.code(202).send({ accepted: true });
|
|
3749
4295
|
} catch (error) {
|
|
3750
4296
|
return reply.code(409).send({ error: String(error) });
|
|
3751
4297
|
}
|
|
3752
|
-
}
|
|
4298
|
+
})
|
|
4299
|
+
);
|
|
4300
|
+
async function withSessionTaskLock(id, operation) {
|
|
4301
|
+
const session = await Effect16.runPromise(store.get(id));
|
|
4302
|
+
const lockId = session ? await sessionTaskLockId(store, session) : `session-${id}`;
|
|
4303
|
+
return withTaskLock(lockId, operation);
|
|
4304
|
+
}
|
|
4305
|
+
app.post(
|
|
4306
|
+
"/api/sessions/:id/stop",
|
|
4307
|
+
async (request, reply) => withSessionTaskLock(request.params.id, async () => {
|
|
4308
|
+
const session = await Effect16.runPromise(store.get(request.params.id));
|
|
4309
|
+
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
4310
|
+
await Effect16.runPromise(runtime.stop(session));
|
|
4311
|
+
return reply.code(202).send({ accepted: true });
|
|
4312
|
+
})
|
|
3753
4313
|
);
|
|
3754
|
-
app.post("/api/sessions/:id/stop", async (request, reply) => {
|
|
3755
|
-
const session = await Effect14.runPromise(store.get(request.params.id));
|
|
3756
|
-
if (!session) return reply.code(404).send({ error: "Session not found." });
|
|
3757
|
-
await Effect14.runPromise(runtime.stop(session));
|
|
3758
|
-
return reply.code(202).send({ accepted: true });
|
|
3759
|
-
});
|
|
3760
4314
|
attachEventServer(app, security, allowedOrigins, () => ({
|
|
3761
4315
|
type: "snapshot",
|
|
3762
|
-
sessions:
|
|
4316
|
+
sessions: Effect16.runSync(store.list())
|
|
3763
4317
|
}));
|
|
3764
4318
|
var closeControl = await startControl(
|
|
3765
4319
|
dataRoot,
|
|
@@ -3771,20 +4325,25 @@ var closeControl = await startControl(
|
|
|
3771
4325
|
connectedDevices: new Set(security.sockets.values()).size,
|
|
3772
4326
|
connectedSockets: security.sockets.size,
|
|
3773
4327
|
memory: process.memoryUsage().rss,
|
|
3774
|
-
activeRuns: activeAgents(
|
|
4328
|
+
activeRuns: activeAgents(Effect16.runSync(store.list())),
|
|
3775
4329
|
maintenance: maintenance.status()
|
|
3776
4330
|
}),
|
|
3777
4331
|
maintenance
|
|
3778
4332
|
);
|
|
4333
|
+
push.start();
|
|
3779
4334
|
app.addHook("onClose", async () => {
|
|
4335
|
+
await push.close();
|
|
3780
4336
|
maintenance.enter();
|
|
3781
4337
|
stopUpdateDiscovery();
|
|
3782
4338
|
await updates.settle();
|
|
3783
4339
|
clearInterval(attachmentCleanup);
|
|
4340
|
+
clearInterval(worktreeCleanupTimer);
|
|
4341
|
+
await withTaskLock("worktree-pruning", async () => {
|
|
4342
|
+
});
|
|
3784
4343
|
clearInterval(automationTimer);
|
|
3785
4344
|
await withTaskLock("automation", async () => {
|
|
3786
4345
|
});
|
|
3787
|
-
await
|
|
4346
|
+
await Effect16.runPromise(runtime.close());
|
|
3788
4347
|
await agentEffects.dispose();
|
|
3789
4348
|
await attachmentEffects.dispose();
|
|
3790
4349
|
await effects.dispose();
|