@cospacehq/server 0.1.0
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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/dist/agent-runtime.d.ts +45 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +374 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -0
- package/dist/db.d.ts +128 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +854 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/model-client.d.ts +43 -0
- package/dist/model-client.d.ts.map +1 -0
- package/dist/model-client.js +138 -0
- package/dist/model-client.js.map +1 -0
- package/dist/provider-crypto.d.ts +4 -0
- package/dist/provider-crypto.d.ts.map +1 -0
- package/dist/provider-crypto.js +30 -0
- package/dist/provider-crypto.js.map +1 -0
- package/dist/sandbox.d.ts +3 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +22 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1296 -0
- package/dist/server.js.map +1 -0
- package/package.json +33 -0
- package/src/agent-runtime.ts +479 -0
- package/src/config.ts +21 -0
- package/src/db.ts +1197 -0
- package/src/index.ts +44 -0
- package/src/model-client.ts +187 -0
- package/src/provider-crypto.ts +39 -0
- package/src/sandbox.ts +26 -0
- package/src/server.ts +1548 -0
- package/tsconfig.json +8 -0
package/src/db.ts
ADDED
|
@@ -0,0 +1,1197 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { nanoid } from "nanoid";
|
|
5
|
+
import type {
|
|
6
|
+
Agent,
|
|
7
|
+
Message,
|
|
8
|
+
Project,
|
|
9
|
+
ProjectThread,
|
|
10
|
+
ProviderKind,
|
|
11
|
+
SeenReceipt,
|
|
12
|
+
Task,
|
|
13
|
+
TaskEvent,
|
|
14
|
+
TaskEventType,
|
|
15
|
+
TaskActionPayload,
|
|
16
|
+
TaskActionType,
|
|
17
|
+
TaskApprovalStatus,
|
|
18
|
+
TaskStatus
|
|
19
|
+
} from "@cospacehq/shared";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_PROJECT_ID = "project-general";
|
|
22
|
+
|
|
23
|
+
const DEFAULT_AGENTS: Omit<Agent, "createdAt" | "status" | "providerKey" | "systemPrompt" | "seenEnabled" | "traceEnabled">[] = [
|
|
24
|
+
{ id: "agent-pm", name: "Luna", title: "Project Manager", model: "gpt-5" },
|
|
25
|
+
{ id: "agent-ui", name: "Rafi", title: "UI Designer", model: "claude-sonnet" },
|
|
26
|
+
{ id: "agent-be", name: "Mako", title: "Backend Engineer", model: "gemini-2.0-pro" }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
type FileActionStatus = "ok" | "error";
|
|
30
|
+
|
|
31
|
+
type AgentRow = Omit<Agent, "seenEnabled" | "traceEnabled"> & {
|
|
32
|
+
seenEnabled: number;
|
|
33
|
+
traceEnabled: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type TaskRow = Omit<Task, "actionPayload"> & {
|
|
37
|
+
actionPayload: string | null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type TaskEventRow = TaskEvent;
|
|
41
|
+
|
|
42
|
+
export type ProviderRecord = {
|
|
43
|
+
id: string;
|
|
44
|
+
providerKey: string;
|
|
45
|
+
label: string;
|
|
46
|
+
kind: ProviderKind;
|
|
47
|
+
baseUrl: string;
|
|
48
|
+
defaultModel: string;
|
|
49
|
+
encryptedApiKey: string | null;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
updatedAt: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function mapAgentRow(row: AgentRow): Agent {
|
|
55
|
+
return {
|
|
56
|
+
...row,
|
|
57
|
+
seenEnabled: row.seenEnabled !== 0,
|
|
58
|
+
traceEnabled: row.traceEnabled !== 0
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseTaskActionPayload(rawValue: string | null): TaskActionPayload | null {
|
|
63
|
+
if (!rawValue) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(rawValue) as TaskActionPayload;
|
|
69
|
+
if (!parsed || typeof parsed !== "object") {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return parsed;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mapTaskRow(row: TaskRow): Task {
|
|
79
|
+
return {
|
|
80
|
+
...row,
|
|
81
|
+
actionPayload: parseTaskActionPayload(row.actionPayload)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mapTaskEventRow(row: TaskEventRow): TaskEvent {
|
|
86
|
+
return row;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class CoSpaceStore {
|
|
90
|
+
private readonly db: DatabaseSync;
|
|
91
|
+
private readonly defaultProjectName: string;
|
|
92
|
+
|
|
93
|
+
public constructor(dbPath: string, options?: { projectName?: string }) {
|
|
94
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
95
|
+
this.db = new DatabaseSync(dbPath);
|
|
96
|
+
this.defaultProjectName = options?.projectName?.trim() ? options.projectName.trim() : "General";
|
|
97
|
+
|
|
98
|
+
this.initSchema();
|
|
99
|
+
this.applyMigrations();
|
|
100
|
+
this.seedDefaults();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public close(): void {
|
|
104
|
+
this.db.close();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public getProject(): Project {
|
|
108
|
+
const row = this.db
|
|
109
|
+
.prepare("SELECT id, name, created_at AS createdAt FROM projects ORDER BY created_at ASC LIMIT 1")
|
|
110
|
+
.get() as Project | undefined;
|
|
111
|
+
|
|
112
|
+
if (!row) {
|
|
113
|
+
throw new Error("No projects found");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return row;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public getProjectById(projectId: string): Project | null {
|
|
120
|
+
const row = this.db
|
|
121
|
+
.prepare("SELECT id, name, created_at AS createdAt FROM projects WHERE id = ? LIMIT 1")
|
|
122
|
+
.get(projectId) as Project | undefined;
|
|
123
|
+
|
|
124
|
+
return row ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public listProjectThreads(): ProjectThread[] {
|
|
128
|
+
type ProjectThreadRow = {
|
|
129
|
+
id: string;
|
|
130
|
+
name: string;
|
|
131
|
+
createdAt: string;
|
|
132
|
+
lastMessageId: string | null;
|
|
133
|
+
lastSenderRole: Message["senderRole"] | null;
|
|
134
|
+
lastSenderName: string | null;
|
|
135
|
+
lastBody: string | null;
|
|
136
|
+
lastAt: string | null;
|
|
137
|
+
assignedAgentCount: number;
|
|
138
|
+
lastSeenByCount: number;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const rows = this.db
|
|
142
|
+
.prepare(
|
|
143
|
+
[
|
|
144
|
+
"SELECT",
|
|
145
|
+
" p.id AS id,",
|
|
146
|
+
" p.name AS name,",
|
|
147
|
+
" p.created_at AS createdAt,",
|
|
148
|
+
" lm.id AS lastMessageId,",
|
|
149
|
+
" lm.sender_role AS lastSenderRole,",
|
|
150
|
+
" lm.sender_name AS lastSenderName,",
|
|
151
|
+
" lm.body AS lastBody,",
|
|
152
|
+
" lm.created_at AS lastAt,",
|
|
153
|
+
" (",
|
|
154
|
+
" SELECT COUNT(*)",
|
|
155
|
+
" FROM project_agents pa",
|
|
156
|
+
" INNER JOIN agents a ON a.id = pa.agent_id",
|
|
157
|
+
" WHERE pa.project_id = p.id AND a.seen_enabled = 1",
|
|
158
|
+
" ) AS assignedAgentCount,",
|
|
159
|
+
" CASE",
|
|
160
|
+
" WHEN lm.id IS NULL THEN 0",
|
|
161
|
+
" ELSE (",
|
|
162
|
+
" SELECT COUNT(*)",
|
|
163
|
+
" FROM seen_receipts sr",
|
|
164
|
+
" INNER JOIN project_agents pa ON pa.project_id = p.id AND pa.agent_id = sr.agent_id",
|
|
165
|
+
" INNER JOIN agents a ON a.id = pa.agent_id AND a.seen_enabled = 1",
|
|
166
|
+
" WHERE sr.message_id = lm.id",
|
|
167
|
+
" )",
|
|
168
|
+
" END AS lastSeenByCount",
|
|
169
|
+
"FROM projects p",
|
|
170
|
+
"LEFT JOIN messages lm ON lm.id = (",
|
|
171
|
+
" SELECT m.id",
|
|
172
|
+
" FROM messages m",
|
|
173
|
+
" WHERE m.project_id = p.id",
|
|
174
|
+
" ORDER BY m.created_at DESC",
|
|
175
|
+
" LIMIT 1",
|
|
176
|
+
")",
|
|
177
|
+
"ORDER BY COALESCE(lm.created_at, p.created_at) DESC, p.created_at DESC"
|
|
178
|
+
].join(" ")
|
|
179
|
+
)
|
|
180
|
+
.all() as ProjectThreadRow[];
|
|
181
|
+
|
|
182
|
+
return rows.map((row) => ({
|
|
183
|
+
id: row.id,
|
|
184
|
+
name: row.name,
|
|
185
|
+
createdAt: row.createdAt,
|
|
186
|
+
assignedAgentCount: Number(row.assignedAgentCount ?? 0),
|
|
187
|
+
lastSeenByCount: Number(row.lastSeenByCount ?? 0),
|
|
188
|
+
lastMessage:
|
|
189
|
+
row.lastMessageId && row.lastSenderName && row.lastBody && row.lastAt
|
|
190
|
+
? {
|
|
191
|
+
messageId: row.lastMessageId,
|
|
192
|
+
senderRole: row.lastSenderRole ?? "human",
|
|
193
|
+
senderName: row.lastSenderName,
|
|
194
|
+
body: row.lastBody,
|
|
195
|
+
createdAt: row.lastAt
|
|
196
|
+
}
|
|
197
|
+
: null
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
public listProjectAgentIdsByProject(): Record<string, string[]> {
|
|
202
|
+
const projects = this.db
|
|
203
|
+
.prepare("SELECT id FROM projects ORDER BY created_at ASC")
|
|
204
|
+
.all() as Array<{ id: string }>;
|
|
205
|
+
|
|
206
|
+
const grouped: Record<string, string[]> = {};
|
|
207
|
+
for (const project of projects) {
|
|
208
|
+
grouped[project.id] = [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const rows = this.db
|
|
212
|
+
.prepare(
|
|
213
|
+
[
|
|
214
|
+
"SELECT pa.project_id AS projectId, pa.agent_id AS agentId",
|
|
215
|
+
"FROM project_agents pa",
|
|
216
|
+
"INNER JOIN agents a ON a.id = pa.agent_id",
|
|
217
|
+
"ORDER BY pa.project_id ASC, a.created_at ASC"
|
|
218
|
+
].join(" ")
|
|
219
|
+
)
|
|
220
|
+
.all() as Array<{ projectId: string; agentId: string }>;
|
|
221
|
+
|
|
222
|
+
for (const row of rows) {
|
|
223
|
+
if (!grouped[row.projectId]) {
|
|
224
|
+
grouped[row.projectId] = [];
|
|
225
|
+
}
|
|
226
|
+
grouped[row.projectId].push(row.agentId);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return grouped;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public createProject(input: { name: string }): Project {
|
|
233
|
+
const createdAt = new Date().toISOString();
|
|
234
|
+
const project: Project = {
|
|
235
|
+
id: `project-${nanoid(10)}`,
|
|
236
|
+
name: input.name.trim(),
|
|
237
|
+
createdAt
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
this.db
|
|
241
|
+
.prepare("INSERT INTO projects (id, name, created_at) VALUES (?, ?, ?)")
|
|
242
|
+
.run(project.id, project.name, project.createdAt);
|
|
243
|
+
|
|
244
|
+
const agentIds = this.listAgents().map((agent) => agent.id);
|
|
245
|
+
this.setProjectAgentIds(project.id, agentIds);
|
|
246
|
+
|
|
247
|
+
return project;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
public listAgents(): Agent[] {
|
|
251
|
+
const rows = this.db
|
|
252
|
+
.prepare(
|
|
253
|
+
[
|
|
254
|
+
"SELECT",
|
|
255
|
+
" a.id,",
|
|
256
|
+
" a.name,",
|
|
257
|
+
" a.title,",
|
|
258
|
+
" COALESCE(ab.model, pc.default_model, a.model) AS model,",
|
|
259
|
+
" ab.provider_key AS providerKey,",
|
|
260
|
+
" a.system_prompt AS systemPrompt,",
|
|
261
|
+
" a.seen_enabled AS seenEnabled,",
|
|
262
|
+
" a.trace_enabled AS traceEnabled,",
|
|
263
|
+
" a.status,",
|
|
264
|
+
" a.created_at AS createdAt",
|
|
265
|
+
"FROM agents a",
|
|
266
|
+
"LEFT JOIN agent_model_bindings ab ON ab.agent_id = a.id",
|
|
267
|
+
"LEFT JOIN provider_configs pc ON pc.provider_key = ab.provider_key",
|
|
268
|
+
"ORDER BY a.created_at ASC"
|
|
269
|
+
].join(" ")
|
|
270
|
+
)
|
|
271
|
+
.all() as AgentRow[];
|
|
272
|
+
|
|
273
|
+
return rows.map(mapAgentRow);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
public getAgentById(agentId: string): Agent | null {
|
|
277
|
+
const row = this.db
|
|
278
|
+
.prepare(
|
|
279
|
+
[
|
|
280
|
+
"SELECT",
|
|
281
|
+
" a.id,",
|
|
282
|
+
" a.name,",
|
|
283
|
+
" a.title,",
|
|
284
|
+
" COALESCE(ab.model, pc.default_model, a.model) AS model,",
|
|
285
|
+
" ab.provider_key AS providerKey,",
|
|
286
|
+
" a.system_prompt AS systemPrompt,",
|
|
287
|
+
" a.seen_enabled AS seenEnabled,",
|
|
288
|
+
" a.trace_enabled AS traceEnabled,",
|
|
289
|
+
" a.status,",
|
|
290
|
+
" a.created_at AS createdAt",
|
|
291
|
+
"FROM agents a",
|
|
292
|
+
"LEFT JOIN agent_model_bindings ab ON ab.agent_id = a.id",
|
|
293
|
+
"LEFT JOIN provider_configs pc ON pc.provider_key = ab.provider_key",
|
|
294
|
+
"WHERE a.id = ?",
|
|
295
|
+
"LIMIT 1"
|
|
296
|
+
].join(" ")
|
|
297
|
+
)
|
|
298
|
+
.get(agentId) as AgentRow | undefined;
|
|
299
|
+
|
|
300
|
+
return row ? mapAgentRow(row) : null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
public listProjectAgentIds(projectId: string): string[] {
|
|
304
|
+
const rows = this.db
|
|
305
|
+
.prepare(
|
|
306
|
+
[
|
|
307
|
+
"SELECT pa.agent_id AS agentId",
|
|
308
|
+
"FROM project_agents pa",
|
|
309
|
+
"INNER JOIN agents a ON a.id = pa.agent_id",
|
|
310
|
+
"WHERE pa.project_id = ?",
|
|
311
|
+
"ORDER BY a.created_at ASC"
|
|
312
|
+
].join(" ")
|
|
313
|
+
)
|
|
314
|
+
.all(projectId) as Array<{ agentId: string }>;
|
|
315
|
+
|
|
316
|
+
return rows.map((row) => row.agentId);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
public listAgentsForProject(projectId: string): Agent[] {
|
|
320
|
+
const rows = this.db
|
|
321
|
+
.prepare(
|
|
322
|
+
[
|
|
323
|
+
"SELECT",
|
|
324
|
+
" a.id,",
|
|
325
|
+
" a.name,",
|
|
326
|
+
" a.title,",
|
|
327
|
+
" COALESCE(ab.model, pc.default_model, a.model) AS model,",
|
|
328
|
+
" ab.provider_key AS providerKey,",
|
|
329
|
+
" a.system_prompt AS systemPrompt,",
|
|
330
|
+
" a.seen_enabled AS seenEnabled,",
|
|
331
|
+
" a.trace_enabled AS traceEnabled,",
|
|
332
|
+
" a.status,",
|
|
333
|
+
" a.created_at AS createdAt",
|
|
334
|
+
"FROM project_agents pa",
|
|
335
|
+
"INNER JOIN agents a ON a.id = pa.agent_id",
|
|
336
|
+
"LEFT JOIN agent_model_bindings ab ON ab.agent_id = a.id",
|
|
337
|
+
"LEFT JOIN provider_configs pc ON pc.provider_key = ab.provider_key",
|
|
338
|
+
"WHERE pa.project_id = ?",
|
|
339
|
+
"ORDER BY a.created_at ASC"
|
|
340
|
+
].join(" ")
|
|
341
|
+
)
|
|
342
|
+
.all(projectId) as AgentRow[];
|
|
343
|
+
|
|
344
|
+
return rows.map(mapAgentRow);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
public createAgent(input: {
|
|
348
|
+
name: string;
|
|
349
|
+
title: string;
|
|
350
|
+
model: string;
|
|
351
|
+
providerKey: string | null;
|
|
352
|
+
systemPrompt: string | null;
|
|
353
|
+
seenEnabled: boolean;
|
|
354
|
+
traceEnabled: boolean;
|
|
355
|
+
projectId?: string;
|
|
356
|
+
}): Agent {
|
|
357
|
+
const createdAt = new Date().toISOString();
|
|
358
|
+
const id = `agent-${nanoid(10)}`;
|
|
359
|
+
|
|
360
|
+
this.db.exec("BEGIN");
|
|
361
|
+
try {
|
|
362
|
+
this.db
|
|
363
|
+
.prepare(
|
|
364
|
+
[
|
|
365
|
+
"INSERT INTO agents",
|
|
366
|
+
"(id, name, title, model, status, system_prompt, seen_enabled, trace_enabled, created_at)",
|
|
367
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
368
|
+
].join(" ")
|
|
369
|
+
)
|
|
370
|
+
.run(
|
|
371
|
+
id,
|
|
372
|
+
input.name.trim(),
|
|
373
|
+
input.title.trim(),
|
|
374
|
+
input.model.trim(),
|
|
375
|
+
"idle",
|
|
376
|
+
input.systemPrompt,
|
|
377
|
+
input.seenEnabled ? 1 : 0,
|
|
378
|
+
input.traceEnabled ? 1 : 0,
|
|
379
|
+
createdAt
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
if (input.providerKey) {
|
|
383
|
+
this.setAgentBinding({
|
|
384
|
+
agentId: id,
|
|
385
|
+
providerKey: input.providerKey,
|
|
386
|
+
model: input.model.trim()
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (input.projectId) {
|
|
391
|
+
this.assignAgentToProject(input.projectId, id);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this.db.exec("COMMIT");
|
|
395
|
+
} catch (error) {
|
|
396
|
+
this.db.exec("ROLLBACK");
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const created = this.getAgentById(id);
|
|
401
|
+
if (!created) {
|
|
402
|
+
throw new Error(`Failed to create agent ${id}`);
|
|
403
|
+
}
|
|
404
|
+
return created;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
public assignAgentToProject(projectId: string, agentId: string): void {
|
|
408
|
+
this.db
|
|
409
|
+
.prepare("INSERT OR IGNORE INTO project_agents (project_id, agent_id, created_at) VALUES (?, ?, ?)")
|
|
410
|
+
.run(projectId, agentId, new Date().toISOString());
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
public setProjectAgentIds(projectId: string, agentIds: string[]): string[] {
|
|
414
|
+
const uniqueIds = Array.from(new Set(agentIds));
|
|
415
|
+
this.db.exec("BEGIN");
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
this.db.prepare("DELETE FROM project_agents WHERE project_id = ?").run(projectId);
|
|
419
|
+
|
|
420
|
+
if (uniqueIds.length > 0) {
|
|
421
|
+
const insert = this.db.prepare(
|
|
422
|
+
"INSERT INTO project_agents (project_id, agent_id, created_at) VALUES (?, ?, ?)"
|
|
423
|
+
);
|
|
424
|
+
const createdAt = new Date().toISOString();
|
|
425
|
+
|
|
426
|
+
for (const agentId of uniqueIds) {
|
|
427
|
+
insert.run(projectId, agentId, createdAt);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
this.db.exec("COMMIT");
|
|
432
|
+
} catch (error) {
|
|
433
|
+
this.db.exec("ROLLBACK");
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return this.listProjectAgentIds(projectId);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
public listMessages(projectId: string): Message[] {
|
|
441
|
+
return this.db
|
|
442
|
+
.prepare(
|
|
443
|
+
[
|
|
444
|
+
"SELECT",
|
|
445
|
+
" id,",
|
|
446
|
+
" project_id AS projectId,",
|
|
447
|
+
" sender_id AS senderId,",
|
|
448
|
+
" sender_name AS senderName,",
|
|
449
|
+
" sender_role AS senderRole,",
|
|
450
|
+
" body,",
|
|
451
|
+
" internal_trace AS internalTrace,",
|
|
452
|
+
" created_at AS createdAt",
|
|
453
|
+
"FROM messages",
|
|
454
|
+
"WHERE project_id = ?",
|
|
455
|
+
"ORDER BY created_at ASC"
|
|
456
|
+
].join(" ")
|
|
457
|
+
)
|
|
458
|
+
.all(projectId) as Message[];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
public listRecentMessages(projectId: string, limit: number): Message[] {
|
|
462
|
+
return this.db
|
|
463
|
+
.prepare(
|
|
464
|
+
[
|
|
465
|
+
"SELECT",
|
|
466
|
+
" id,",
|
|
467
|
+
" project_id AS projectId,",
|
|
468
|
+
" sender_id AS senderId,",
|
|
469
|
+
" sender_name AS senderName,",
|
|
470
|
+
" sender_role AS senderRole,",
|
|
471
|
+
" body,",
|
|
472
|
+
" internal_trace AS internalTrace,",
|
|
473
|
+
" created_at AS createdAt",
|
|
474
|
+
"FROM messages",
|
|
475
|
+
"WHERE project_id = ?",
|
|
476
|
+
"ORDER BY created_at DESC",
|
|
477
|
+
"LIMIT ?"
|
|
478
|
+
].join(" ")
|
|
479
|
+
)
|
|
480
|
+
.all(projectId, limit)
|
|
481
|
+
.reverse() as Message[];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
public listReceiptsByProject(projectId: string): Record<string, SeenReceipt[]> {
|
|
485
|
+
const rows = this.db
|
|
486
|
+
.prepare(
|
|
487
|
+
[
|
|
488
|
+
"SELECT",
|
|
489
|
+
" sr.id AS id,",
|
|
490
|
+
" sr.message_id AS messageId,",
|
|
491
|
+
" sr.agent_id AS agentId,",
|
|
492
|
+
" sr.agent_name AS agentName,",
|
|
493
|
+
" sr.model AS model,",
|
|
494
|
+
" sr.seen_at AS seenAt",
|
|
495
|
+
"FROM seen_receipts sr",
|
|
496
|
+
"INNER JOIN messages m ON m.id = sr.message_id",
|
|
497
|
+
"WHERE m.project_id = ?",
|
|
498
|
+
"ORDER BY sr.seen_at ASC"
|
|
499
|
+
].join(" ")
|
|
500
|
+
)
|
|
501
|
+
.all(projectId) as SeenReceipt[];
|
|
502
|
+
|
|
503
|
+
const grouped: Record<string, SeenReceipt[]> = {};
|
|
504
|
+
|
|
505
|
+
for (const row of rows) {
|
|
506
|
+
if (!grouped[row.messageId]) {
|
|
507
|
+
grouped[row.messageId] = [];
|
|
508
|
+
}
|
|
509
|
+
grouped[row.messageId].push(row);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return grouped;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
public listReceiptsForMessage(messageId: string): SeenReceipt[] {
|
|
516
|
+
return this.db
|
|
517
|
+
.prepare(
|
|
518
|
+
[
|
|
519
|
+
"SELECT id, message_id AS messageId, agent_id AS agentId, agent_name AS agentName, model, seen_at AS seenAt",
|
|
520
|
+
"FROM seen_receipts",
|
|
521
|
+
"WHERE message_id = ?",
|
|
522
|
+
"ORDER BY seen_at ASC"
|
|
523
|
+
].join(" ")
|
|
524
|
+
)
|
|
525
|
+
.all(messageId) as SeenReceipt[];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
public createHumanMessage(input: { projectId: string; senderName: string; body: string }): Message {
|
|
529
|
+
const createdAt = new Date().toISOString();
|
|
530
|
+
const message: Message = {
|
|
531
|
+
id: nanoid(12),
|
|
532
|
+
projectId: input.projectId,
|
|
533
|
+
senderId: "human-user",
|
|
534
|
+
senderName: input.senderName,
|
|
535
|
+
senderRole: "human",
|
|
536
|
+
body: input.body,
|
|
537
|
+
internalTrace: null,
|
|
538
|
+
createdAt
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
this.db
|
|
542
|
+
.prepare(
|
|
543
|
+
[
|
|
544
|
+
"INSERT INTO messages (id, project_id, sender_id, sender_name, sender_role, body, internal_trace, created_at)",
|
|
545
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
546
|
+
].join(" ")
|
|
547
|
+
)
|
|
548
|
+
.run(
|
|
549
|
+
message.id,
|
|
550
|
+
message.projectId,
|
|
551
|
+
message.senderId,
|
|
552
|
+
message.senderName,
|
|
553
|
+
message.senderRole,
|
|
554
|
+
message.body,
|
|
555
|
+
message.internalTrace,
|
|
556
|
+
message.createdAt
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
return message;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
public createAgentMessage(input: {
|
|
563
|
+
projectId: string;
|
|
564
|
+
senderId: string;
|
|
565
|
+
senderName: string;
|
|
566
|
+
body: string;
|
|
567
|
+
internalTrace: string | null;
|
|
568
|
+
}): Message {
|
|
569
|
+
const createdAt = new Date().toISOString();
|
|
570
|
+
const message: Message = {
|
|
571
|
+
id: nanoid(12),
|
|
572
|
+
projectId: input.projectId,
|
|
573
|
+
senderId: input.senderId,
|
|
574
|
+
senderName: input.senderName,
|
|
575
|
+
senderRole: "agent",
|
|
576
|
+
body: input.body,
|
|
577
|
+
internalTrace: input.internalTrace,
|
|
578
|
+
createdAt
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
this.db
|
|
582
|
+
.prepare(
|
|
583
|
+
[
|
|
584
|
+
"INSERT INTO messages (id, project_id, sender_id, sender_name, sender_role, body, internal_trace, created_at)",
|
|
585
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
586
|
+
].join(" ")
|
|
587
|
+
)
|
|
588
|
+
.run(
|
|
589
|
+
message.id,
|
|
590
|
+
message.projectId,
|
|
591
|
+
message.senderId,
|
|
592
|
+
message.senderName,
|
|
593
|
+
message.senderRole,
|
|
594
|
+
message.body,
|
|
595
|
+
message.internalTrace,
|
|
596
|
+
message.createdAt
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
return message;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
public createSeenReceipt(input: {
|
|
603
|
+
messageId: string;
|
|
604
|
+
agentId: string;
|
|
605
|
+
agentName: string;
|
|
606
|
+
model: string;
|
|
607
|
+
}): SeenReceipt {
|
|
608
|
+
const receipt: SeenReceipt = {
|
|
609
|
+
id: nanoid(12),
|
|
610
|
+
messageId: input.messageId,
|
|
611
|
+
agentId: input.agentId,
|
|
612
|
+
agentName: input.agentName,
|
|
613
|
+
model: input.model,
|
|
614
|
+
seenAt: new Date().toISOString()
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
this.db
|
|
618
|
+
.prepare(
|
|
619
|
+
"INSERT INTO seen_receipts (id, message_id, agent_id, agent_name, model, seen_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
620
|
+
)
|
|
621
|
+
.run(receipt.id, receipt.messageId, receipt.agentId, receipt.agentName, receipt.model, receipt.seenAt);
|
|
622
|
+
|
|
623
|
+
return receipt;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
public updateAgentStatus(agentId: string, status: Agent["status"]): void {
|
|
627
|
+
this.db.prepare("UPDATE agents SET status = ? WHERE id = ?").run(status, agentId);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
public listTasksByProject(projectId: string): Task[] {
|
|
631
|
+
const rows = this.db
|
|
632
|
+
.prepare(
|
|
633
|
+
[
|
|
634
|
+
"SELECT",
|
|
635
|
+
" t.id AS id,",
|
|
636
|
+
" t.project_id AS projectId,",
|
|
637
|
+
" t.title AS title,",
|
|
638
|
+
" t.description AS description,",
|
|
639
|
+
" t.status AS status,",
|
|
640
|
+
" t.approval_status AS approvalStatus,",
|
|
641
|
+
" t.action_type AS actionType,",
|
|
642
|
+
" t.action_payload AS actionPayload,",
|
|
643
|
+
" t.assignee_agent_id AS assigneeAgentId,",
|
|
644
|
+
" a.name AS assigneeAgentName,",
|
|
645
|
+
" t.delegated_by_message_id AS delegatedByMessageId,",
|
|
646
|
+
" t.created_by AS createdBy,",
|
|
647
|
+
" t.created_at AS createdAt,",
|
|
648
|
+
" t.updated_at AS updatedAt,",
|
|
649
|
+
" t.completed_at AS completedAt,",
|
|
650
|
+
" t.error_message AS errorMessage",
|
|
651
|
+
"FROM tasks t",
|
|
652
|
+
"LEFT JOIN agents a ON a.id = t.assignee_agent_id",
|
|
653
|
+
"WHERE t.project_id = ?",
|
|
654
|
+
"ORDER BY t.created_at DESC"
|
|
655
|
+
].join(" ")
|
|
656
|
+
)
|
|
657
|
+
.all(projectId) as TaskRow[];
|
|
658
|
+
|
|
659
|
+
return rows.map(mapTaskRow);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
public listTaskEventsByProject(projectId: string): Record<string, TaskEvent[]> {
|
|
663
|
+
const rows = this.db
|
|
664
|
+
.prepare(
|
|
665
|
+
[
|
|
666
|
+
"SELECT",
|
|
667
|
+
" id AS id,",
|
|
668
|
+
" task_id AS taskId,",
|
|
669
|
+
" project_id AS projectId,",
|
|
670
|
+
" event_type AS eventType,",
|
|
671
|
+
" actor_id AS actorId,",
|
|
672
|
+
" detail AS detail,",
|
|
673
|
+
" created_at AS createdAt",
|
|
674
|
+
"FROM task_events",
|
|
675
|
+
"WHERE project_id = ?",
|
|
676
|
+
"ORDER BY created_at ASC"
|
|
677
|
+
].join(" ")
|
|
678
|
+
)
|
|
679
|
+
.all(projectId) as TaskEventRow[];
|
|
680
|
+
|
|
681
|
+
const grouped: Record<string, TaskEvent[]> = {};
|
|
682
|
+
for (const row of rows.map(mapTaskEventRow)) {
|
|
683
|
+
if (!grouped[row.taskId]) {
|
|
684
|
+
grouped[row.taskId] = [];
|
|
685
|
+
}
|
|
686
|
+
grouped[row.taskId].push(row);
|
|
687
|
+
}
|
|
688
|
+
return grouped;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
public getTaskById(taskId: string): Task | null {
|
|
692
|
+
const row = this.db
|
|
693
|
+
.prepare(
|
|
694
|
+
[
|
|
695
|
+
"SELECT",
|
|
696
|
+
" t.id AS id,",
|
|
697
|
+
" t.project_id AS projectId,",
|
|
698
|
+
" t.title AS title,",
|
|
699
|
+
" t.description AS description,",
|
|
700
|
+
" t.status AS status,",
|
|
701
|
+
" t.approval_status AS approvalStatus,",
|
|
702
|
+
" t.action_type AS actionType,",
|
|
703
|
+
" t.action_payload AS actionPayload,",
|
|
704
|
+
" t.assignee_agent_id AS assigneeAgentId,",
|
|
705
|
+
" a.name AS assigneeAgentName,",
|
|
706
|
+
" t.delegated_by_message_id AS delegatedByMessageId,",
|
|
707
|
+
" t.created_by AS createdBy,",
|
|
708
|
+
" t.created_at AS createdAt,",
|
|
709
|
+
" t.updated_at AS updatedAt,",
|
|
710
|
+
" t.completed_at AS completedAt,",
|
|
711
|
+
" t.error_message AS errorMessage",
|
|
712
|
+
"FROM tasks t",
|
|
713
|
+
"LEFT JOIN agents a ON a.id = t.assignee_agent_id",
|
|
714
|
+
"WHERE t.id = ?",
|
|
715
|
+
"LIMIT 1"
|
|
716
|
+
].join(" ")
|
|
717
|
+
)
|
|
718
|
+
.get(taskId) as TaskRow | undefined;
|
|
719
|
+
|
|
720
|
+
return row ? mapTaskRow(row) : null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
public createTask(input: {
|
|
724
|
+
projectId: string;
|
|
725
|
+
title: string;
|
|
726
|
+
description: string | null;
|
|
727
|
+
status: TaskStatus;
|
|
728
|
+
approvalStatus: TaskApprovalStatus;
|
|
729
|
+
actionType: TaskActionType;
|
|
730
|
+
actionPayload: TaskActionPayload | null;
|
|
731
|
+
assigneeAgentId: string | null;
|
|
732
|
+
delegatedByMessageId: string | null;
|
|
733
|
+
createdBy: string;
|
|
734
|
+
}): Task {
|
|
735
|
+
const id = `task-${nanoid(10)}`;
|
|
736
|
+
const now = new Date().toISOString();
|
|
737
|
+
const completedAt = input.status === "done" || input.status === "cancelled" ? now : null;
|
|
738
|
+
|
|
739
|
+
this.db
|
|
740
|
+
.prepare(
|
|
741
|
+
[
|
|
742
|
+
"INSERT INTO tasks",
|
|
743
|
+
"(",
|
|
744
|
+
"id, project_id, title, description, status, approval_status, action_type, action_payload,",
|
|
745
|
+
"assignee_agent_id, delegated_by_message_id, created_by, created_at, updated_at, completed_at, error_message",
|
|
746
|
+
")",
|
|
747
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
748
|
+
].join(" ")
|
|
749
|
+
)
|
|
750
|
+
.run(
|
|
751
|
+
id,
|
|
752
|
+
input.projectId,
|
|
753
|
+
input.title,
|
|
754
|
+
input.description,
|
|
755
|
+
input.status,
|
|
756
|
+
input.approvalStatus,
|
|
757
|
+
input.actionType,
|
|
758
|
+
input.actionPayload ? JSON.stringify(input.actionPayload) : null,
|
|
759
|
+
input.assigneeAgentId,
|
|
760
|
+
input.delegatedByMessageId,
|
|
761
|
+
input.createdBy,
|
|
762
|
+
now,
|
|
763
|
+
now,
|
|
764
|
+
completedAt,
|
|
765
|
+
null
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
const created = this.getTaskById(id);
|
|
769
|
+
if (!created) {
|
|
770
|
+
throw new Error(`Failed to create task ${id}`);
|
|
771
|
+
}
|
|
772
|
+
return created;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
public updateTaskState(input: {
|
|
776
|
+
taskId: string;
|
|
777
|
+
status?: TaskStatus;
|
|
778
|
+
approvalStatus?: TaskApprovalStatus;
|
|
779
|
+
errorMessage?: string | null;
|
|
780
|
+
completedAt?: string | null;
|
|
781
|
+
}): Task {
|
|
782
|
+
const existing = this.getTaskById(input.taskId);
|
|
783
|
+
if (!existing) {
|
|
784
|
+
throw new Error(`Task ${input.taskId} not found`);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const nextStatus = input.status ?? existing.status;
|
|
788
|
+
const nextApprovalStatus = input.approvalStatus ?? existing.approvalStatus;
|
|
789
|
+
const nextCompletedAt =
|
|
790
|
+
input.completedAt !== undefined
|
|
791
|
+
? input.completedAt
|
|
792
|
+
: nextStatus === "done" || nextStatus === "cancelled"
|
|
793
|
+
? existing.completedAt ?? new Date().toISOString()
|
|
794
|
+
: null;
|
|
795
|
+
const nextErrorMessage = input.errorMessage !== undefined ? input.errorMessage : existing.errorMessage;
|
|
796
|
+
const updatedAt = new Date().toISOString();
|
|
797
|
+
|
|
798
|
+
this.db
|
|
799
|
+
.prepare(
|
|
800
|
+
[
|
|
801
|
+
"UPDATE tasks",
|
|
802
|
+
"SET status = ?,",
|
|
803
|
+
" approval_status = ?,",
|
|
804
|
+
" updated_at = ?,",
|
|
805
|
+
" completed_at = ?,",
|
|
806
|
+
" error_message = ?",
|
|
807
|
+
"WHERE id = ?"
|
|
808
|
+
].join(" ")
|
|
809
|
+
)
|
|
810
|
+
.run(nextStatus, nextApprovalStatus, updatedAt, nextCompletedAt, nextErrorMessage, input.taskId);
|
|
811
|
+
|
|
812
|
+
const updated = this.getTaskById(input.taskId);
|
|
813
|
+
if (!updated) {
|
|
814
|
+
throw new Error(`Task ${input.taskId} disappeared after update`);
|
|
815
|
+
}
|
|
816
|
+
return updated;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
public listProviderRecords(): ProviderRecord[] {
|
|
820
|
+
return this.db
|
|
821
|
+
.prepare(
|
|
822
|
+
[
|
|
823
|
+
"SELECT",
|
|
824
|
+
" id,",
|
|
825
|
+
" provider_key AS providerKey,",
|
|
826
|
+
" label,",
|
|
827
|
+
" kind,",
|
|
828
|
+
" base_url AS baseUrl,",
|
|
829
|
+
" default_model AS defaultModel,",
|
|
830
|
+
" encrypted_api_key AS encryptedApiKey,",
|
|
831
|
+
" created_at AS createdAt,",
|
|
832
|
+
" updated_at AS updatedAt",
|
|
833
|
+
"FROM provider_configs",
|
|
834
|
+
"ORDER BY created_at ASC"
|
|
835
|
+
].join(" ")
|
|
836
|
+
)
|
|
837
|
+
.all() as ProviderRecord[];
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
public getProviderRecord(providerKey: string): ProviderRecord | null {
|
|
841
|
+
const row = this.db
|
|
842
|
+
.prepare(
|
|
843
|
+
[
|
|
844
|
+
"SELECT",
|
|
845
|
+
" id,",
|
|
846
|
+
" provider_key AS providerKey,",
|
|
847
|
+
" label,",
|
|
848
|
+
" kind,",
|
|
849
|
+
" base_url AS baseUrl,",
|
|
850
|
+
" default_model AS defaultModel,",
|
|
851
|
+
" encrypted_api_key AS encryptedApiKey,",
|
|
852
|
+
" created_at AS createdAt,",
|
|
853
|
+
" updated_at AS updatedAt",
|
|
854
|
+
"FROM provider_configs",
|
|
855
|
+
"WHERE provider_key = ?",
|
|
856
|
+
"LIMIT 1"
|
|
857
|
+
].join(" ")
|
|
858
|
+
)
|
|
859
|
+
.get(providerKey) as ProviderRecord | undefined;
|
|
860
|
+
|
|
861
|
+
return row ?? null;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
public upsertProviderRecord(input: {
|
|
865
|
+
providerKey: string;
|
|
866
|
+
label: string;
|
|
867
|
+
kind: ProviderKind;
|
|
868
|
+
baseUrl: string;
|
|
869
|
+
defaultModel: string;
|
|
870
|
+
encryptedApiKey?: string | null;
|
|
871
|
+
}): ProviderRecord {
|
|
872
|
+
const now = new Date().toISOString();
|
|
873
|
+
const existing = this.getProviderRecord(input.providerKey);
|
|
874
|
+
|
|
875
|
+
if (!existing) {
|
|
876
|
+
const id = nanoid(12);
|
|
877
|
+
this.db
|
|
878
|
+
.prepare(
|
|
879
|
+
[
|
|
880
|
+
"INSERT INTO provider_configs",
|
|
881
|
+
"(id, provider_key, label, kind, base_url, default_model, encrypted_api_key, created_at, updated_at)",
|
|
882
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
883
|
+
].join(" ")
|
|
884
|
+
)
|
|
885
|
+
.run(
|
|
886
|
+
id,
|
|
887
|
+
input.providerKey,
|
|
888
|
+
input.label,
|
|
889
|
+
input.kind,
|
|
890
|
+
input.baseUrl,
|
|
891
|
+
input.defaultModel,
|
|
892
|
+
input.encryptedApiKey ?? null,
|
|
893
|
+
now,
|
|
894
|
+
now
|
|
895
|
+
);
|
|
896
|
+
} else {
|
|
897
|
+
this.db
|
|
898
|
+
.prepare(
|
|
899
|
+
[
|
|
900
|
+
"UPDATE provider_configs",
|
|
901
|
+
"SET label = ?,",
|
|
902
|
+
" kind = ?,",
|
|
903
|
+
" base_url = ?,",
|
|
904
|
+
" default_model = ?,",
|
|
905
|
+
" encrypted_api_key = ?,",
|
|
906
|
+
" updated_at = ?",
|
|
907
|
+
"WHERE provider_key = ?"
|
|
908
|
+
].join(" ")
|
|
909
|
+
)
|
|
910
|
+
.run(
|
|
911
|
+
input.label,
|
|
912
|
+
input.kind,
|
|
913
|
+
input.baseUrl,
|
|
914
|
+
input.defaultModel,
|
|
915
|
+
input.encryptedApiKey === undefined ? existing.encryptedApiKey : input.encryptedApiKey,
|
|
916
|
+
now,
|
|
917
|
+
input.providerKey
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const updated = this.getProviderRecord(input.providerKey);
|
|
922
|
+
if (!updated) {
|
|
923
|
+
throw new Error(`Failed to upsert provider ${input.providerKey}`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return updated;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
public setAgentBinding(input: { agentId: string; providerKey: string | null; model: string | null }): void {
|
|
930
|
+
if (input.providerKey === null && input.model === null) {
|
|
931
|
+
this.db.prepare("DELETE FROM agent_model_bindings WHERE agent_id = ?").run(input.agentId);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const now = new Date().toISOString();
|
|
936
|
+
this.db
|
|
937
|
+
.prepare(
|
|
938
|
+
[
|
|
939
|
+
"INSERT INTO agent_model_bindings (agent_id, provider_key, model, updated_at)",
|
|
940
|
+
"VALUES (?, ?, ?, ?)",
|
|
941
|
+
"ON CONFLICT(agent_id) DO UPDATE SET",
|
|
942
|
+
" provider_key = excluded.provider_key,",
|
|
943
|
+
" model = excluded.model,",
|
|
944
|
+
" updated_at = excluded.updated_at"
|
|
945
|
+
].join(" ")
|
|
946
|
+
)
|
|
947
|
+
.run(input.agentId, input.providerKey, input.model, now);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
public setAgentVisibility(input: { agentId: string; seenEnabled: boolean; traceEnabled: boolean }): void {
|
|
951
|
+
this.db
|
|
952
|
+
.prepare("UPDATE agents SET seen_enabled = ?, trace_enabled = ? WHERE id = ?")
|
|
953
|
+
.run(input.seenEnabled ? 1 : 0, input.traceEnabled ? 1 : 0, input.agentId);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
public recordTaskEvent(input: {
|
|
957
|
+
taskId: string;
|
|
958
|
+
projectId: string;
|
|
959
|
+
eventType: TaskEventType;
|
|
960
|
+
actorId: string;
|
|
961
|
+
detail: string | null;
|
|
962
|
+
}): TaskEvent {
|
|
963
|
+
const event: TaskEvent = {
|
|
964
|
+
id: nanoid(12),
|
|
965
|
+
taskId: input.taskId,
|
|
966
|
+
projectId: input.projectId,
|
|
967
|
+
eventType: input.eventType,
|
|
968
|
+
actorId: input.actorId,
|
|
969
|
+
detail: input.detail,
|
|
970
|
+
createdAt: new Date().toISOString()
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
this.db
|
|
974
|
+
.prepare(
|
|
975
|
+
[
|
|
976
|
+
"INSERT INTO task_events",
|
|
977
|
+
"(id, task_id, project_id, event_type, actor_id, detail, created_at)",
|
|
978
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
979
|
+
].join(" ")
|
|
980
|
+
)
|
|
981
|
+
.run(
|
|
982
|
+
event.id,
|
|
983
|
+
event.taskId,
|
|
984
|
+
event.projectId,
|
|
985
|
+
event.eventType,
|
|
986
|
+
event.actorId,
|
|
987
|
+
event.detail,
|
|
988
|
+
event.createdAt
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
return event;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
public recordFileAction(input: {
|
|
995
|
+
actorId: string;
|
|
996
|
+
action: string;
|
|
997
|
+
targetPath: string;
|
|
998
|
+
status: FileActionStatus;
|
|
999
|
+
errorMessage: string | null;
|
|
1000
|
+
}): void {
|
|
1001
|
+
this.db
|
|
1002
|
+
.prepare(
|
|
1003
|
+
[
|
|
1004
|
+
"INSERT INTO file_actions (id, actor_id, action, target_path, status, error_message, created_at)",
|
|
1005
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
1006
|
+
].join(" ")
|
|
1007
|
+
)
|
|
1008
|
+
.run(
|
|
1009
|
+
nanoid(12),
|
|
1010
|
+
input.actorId,
|
|
1011
|
+
input.action,
|
|
1012
|
+
input.targetPath,
|
|
1013
|
+
input.status,
|
|
1014
|
+
input.errorMessage,
|
|
1015
|
+
new Date().toISOString()
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
private initSchema(): void {
|
|
1020
|
+
this.db.exec(`
|
|
1021
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
1022
|
+
id TEXT PRIMARY KEY,
|
|
1023
|
+
name TEXT NOT NULL,
|
|
1024
|
+
created_at TEXT NOT NULL
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
1028
|
+
id TEXT PRIMARY KEY,
|
|
1029
|
+
name TEXT NOT NULL,
|
|
1030
|
+
title TEXT NOT NULL,
|
|
1031
|
+
model TEXT NOT NULL,
|
|
1032
|
+
status TEXT NOT NULL,
|
|
1033
|
+
system_prompt TEXT,
|
|
1034
|
+
seen_enabled INTEGER NOT NULL DEFAULT 1,
|
|
1035
|
+
trace_enabled INTEGER NOT NULL DEFAULT 1,
|
|
1036
|
+
created_at TEXT NOT NULL
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
1040
|
+
id TEXT PRIMARY KEY,
|
|
1041
|
+
project_id TEXT NOT NULL,
|
|
1042
|
+
sender_id TEXT NOT NULL,
|
|
1043
|
+
sender_name TEXT NOT NULL,
|
|
1044
|
+
sender_role TEXT NOT NULL,
|
|
1045
|
+
body TEXT NOT NULL,
|
|
1046
|
+
internal_trace TEXT,
|
|
1047
|
+
created_at TEXT NOT NULL,
|
|
1048
|
+
FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
CREATE TABLE IF NOT EXISTS seen_receipts (
|
|
1052
|
+
id TEXT PRIMARY KEY,
|
|
1053
|
+
message_id TEXT NOT NULL,
|
|
1054
|
+
agent_id TEXT NOT NULL,
|
|
1055
|
+
agent_name TEXT NOT NULL,
|
|
1056
|
+
model TEXT NOT NULL,
|
|
1057
|
+
seen_at TEXT NOT NULL,
|
|
1058
|
+
FOREIGN KEY (message_id) REFERENCES messages(id)
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
CREATE TABLE IF NOT EXISTS file_actions (
|
|
1062
|
+
id TEXT PRIMARY KEY,
|
|
1063
|
+
actor_id TEXT NOT NULL,
|
|
1064
|
+
action TEXT NOT NULL,
|
|
1065
|
+
target_path TEXT NOT NULL,
|
|
1066
|
+
status TEXT NOT NULL,
|
|
1067
|
+
error_message TEXT,
|
|
1068
|
+
created_at TEXT NOT NULL
|
|
1069
|
+
);
|
|
1070
|
+
|
|
1071
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
1072
|
+
id TEXT PRIMARY KEY,
|
|
1073
|
+
project_id TEXT NOT NULL,
|
|
1074
|
+
title TEXT NOT NULL,
|
|
1075
|
+
description TEXT,
|
|
1076
|
+
status TEXT NOT NULL,
|
|
1077
|
+
approval_status TEXT NOT NULL,
|
|
1078
|
+
action_type TEXT NOT NULL,
|
|
1079
|
+
action_payload TEXT,
|
|
1080
|
+
assignee_agent_id TEXT,
|
|
1081
|
+
delegated_by_message_id TEXT,
|
|
1082
|
+
created_by TEXT NOT NULL,
|
|
1083
|
+
created_at TEXT NOT NULL,
|
|
1084
|
+
updated_at TEXT NOT NULL,
|
|
1085
|
+
completed_at TEXT,
|
|
1086
|
+
error_message TEXT,
|
|
1087
|
+
FOREIGN KEY (project_id) REFERENCES projects(id),
|
|
1088
|
+
FOREIGN KEY (assignee_agent_id) REFERENCES agents(id),
|
|
1089
|
+
FOREIGN KEY (delegated_by_message_id) REFERENCES messages(id)
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
CREATE TABLE IF NOT EXISTS task_events (
|
|
1093
|
+
id TEXT PRIMARY KEY,
|
|
1094
|
+
task_id TEXT NOT NULL,
|
|
1095
|
+
project_id TEXT NOT NULL,
|
|
1096
|
+
event_type TEXT NOT NULL,
|
|
1097
|
+
actor_id TEXT NOT NULL,
|
|
1098
|
+
detail TEXT,
|
|
1099
|
+
created_at TEXT NOT NULL,
|
|
1100
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id),
|
|
1101
|
+
FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
CREATE TABLE IF NOT EXISTS provider_configs (
|
|
1105
|
+
id TEXT PRIMARY KEY,
|
|
1106
|
+
provider_key TEXT NOT NULL UNIQUE,
|
|
1107
|
+
label TEXT NOT NULL,
|
|
1108
|
+
kind TEXT NOT NULL,
|
|
1109
|
+
base_url TEXT NOT NULL,
|
|
1110
|
+
default_model TEXT NOT NULL,
|
|
1111
|
+
encrypted_api_key TEXT,
|
|
1112
|
+
created_at TEXT NOT NULL,
|
|
1113
|
+
updated_at TEXT NOT NULL
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
CREATE TABLE IF NOT EXISTS agent_model_bindings (
|
|
1117
|
+
agent_id TEXT PRIMARY KEY,
|
|
1118
|
+
provider_key TEXT,
|
|
1119
|
+
model TEXT,
|
|
1120
|
+
updated_at TEXT NOT NULL,
|
|
1121
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id),
|
|
1122
|
+
FOREIGN KEY (provider_key) REFERENCES provider_configs(provider_key)
|
|
1123
|
+
);
|
|
1124
|
+
|
|
1125
|
+
CREATE TABLE IF NOT EXISTS project_agents (
|
|
1126
|
+
project_id TEXT NOT NULL,
|
|
1127
|
+
agent_id TEXT NOT NULL,
|
|
1128
|
+
created_at TEXT NOT NULL,
|
|
1129
|
+
PRIMARY KEY (project_id, agent_id),
|
|
1130
|
+
FOREIGN KEY (project_id) REFERENCES projects(id),
|
|
1131
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
1132
|
+
);
|
|
1133
|
+
`);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private applyMigrations(): void {
|
|
1137
|
+
const agentColumns = this.db
|
|
1138
|
+
.prepare("PRAGMA table_info(agents)")
|
|
1139
|
+
.all() as Array<{ name: string }>;
|
|
1140
|
+
|
|
1141
|
+
const hasSystemPrompt = agentColumns.some((column) => column.name === "system_prompt");
|
|
1142
|
+
if (!hasSystemPrompt) {
|
|
1143
|
+
this.db.exec("ALTER TABLE agents ADD COLUMN system_prompt TEXT");
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const hasSeenEnabled = agentColumns.some((column) => column.name === "seen_enabled");
|
|
1147
|
+
if (!hasSeenEnabled) {
|
|
1148
|
+
this.db.exec("ALTER TABLE agents ADD COLUMN seen_enabled INTEGER NOT NULL DEFAULT 1");
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const hasTraceEnabled = agentColumns.some((column) => column.name === "trace_enabled");
|
|
1152
|
+
if (!hasTraceEnabled) {
|
|
1153
|
+
this.db.exec("ALTER TABLE agents ADD COLUMN trace_enabled INTEGER NOT NULL DEFAULT 1");
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
private seedDefaults(): void {
|
|
1158
|
+
const projectCount = this.db.prepare("SELECT COUNT(*) AS count FROM projects").get() as { count: number };
|
|
1159
|
+
if (projectCount.count === 0) {
|
|
1160
|
+
const defaultProject: Project = {
|
|
1161
|
+
id: DEFAULT_PROJECT_ID,
|
|
1162
|
+
name: this.defaultProjectName,
|
|
1163
|
+
createdAt: new Date().toISOString()
|
|
1164
|
+
};
|
|
1165
|
+
this.db
|
|
1166
|
+
.prepare("INSERT INTO projects (id, name, created_at) VALUES (?, ?, ?)")
|
|
1167
|
+
.run(defaultProject.id, defaultProject.name, defaultProject.createdAt);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const agentCount = this.db.prepare("SELECT COUNT(*) AS count FROM agents").get() as { count: number };
|
|
1171
|
+
if (agentCount.count === 0) {
|
|
1172
|
+
const stmt = this.db.prepare(
|
|
1173
|
+
"INSERT INTO agents (id, name, title, model, status, system_prompt, seen_enabled, trace_enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
1174
|
+
);
|
|
1175
|
+
const createdAt = new Date().toISOString();
|
|
1176
|
+
|
|
1177
|
+
for (const agent of DEFAULT_AGENTS) {
|
|
1178
|
+
stmt.run(agent.id, agent.name, agent.title, agent.model, "idle", null, 1, 1, createdAt);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const projectRows = this.db
|
|
1183
|
+
.prepare("SELECT id FROM projects ORDER BY created_at ASC")
|
|
1184
|
+
.all() as Array<{ id: string }>;
|
|
1185
|
+
const agentIds = this.listAgents().map((agent) => agent.id);
|
|
1186
|
+
|
|
1187
|
+
for (const project of projectRows) {
|
|
1188
|
+
const assignmentCount = this.db
|
|
1189
|
+
.prepare("SELECT COUNT(*) AS count FROM project_agents WHERE project_id = ?")
|
|
1190
|
+
.get(project.id) as { count: number };
|
|
1191
|
+
|
|
1192
|
+
if (assignmentCount.count === 0 && agentIds.length > 0) {
|
|
1193
|
+
this.setProjectAgentIds(project.id, agentIds);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|