@kevin5251984/guild 0.2.12
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/LICENSE +21 -0
- package/bin/guildd.mjs +20 -0
- package/cordis.yml +24 -0
- package/package.json +52 -0
- package/src/agent-file.ts +125 -0
- package/src/browser.ts +668 -0
- package/src/catalog/default-bots.ts +263 -0
- package/src/catalog/skills.ts +128 -0
- package/src/catalog/subagents.ts +70 -0
- package/src/chat-parts.ts +71 -0
- package/src/cli-args.ts +75 -0
- package/src/cli.ts +60 -0
- package/src/compact.ts +355 -0
- package/src/cordis.d.ts +40 -0
- package/src/db.ts +653 -0
- package/src/generate.ts +673 -0
- package/src/handlers.ts +1623 -0
- package/src/harness.ts +326 -0
- package/src/host-agents.ts +137 -0
- package/src/host-browse.ts +199 -0
- package/src/host-skills.ts +150 -0
- package/src/image-gen.ts +270 -0
- package/src/index.ts +12 -0
- package/src/llm.ts +993 -0
- package/src/mcp.ts +563 -0
- package/src/memory.ts +159 -0
- package/src/mention.ts +176 -0
- package/src/oauth.ts +1474 -0
- package/src/plugins/api.ts +8 -0
- package/src/plugins/chat.ts +31 -0
- package/src/plugins/harness.ts +77 -0
- package/src/plugins/llm.ts +50 -0
- package/src/plugins/mcp.ts +58 -0
- package/src/plugins/memory.ts +42 -0
- package/src/plugins/oauth.ts +47 -0
- package/src/plugins/server.ts +126 -0
- package/src/plugins/store.ts +29 -0
- package/src/plugins/tools.ts +79 -0
- package/src/public/buddy.js +432 -0
- package/src/public/chat.css +3045 -0
- package/src/public/chat.html +5834 -0
- package/src/public/favicon-16.png +0 -0
- package/src/public/favicon-16.svg +10 -0
- package/src/public/favicon-32.png +0 -0
- package/src/public/favicon.ico +0 -0
- package/src/public/favicon.svg +13 -0
- package/src/public/i18n.js +663 -0
- package/src/public/index.html +143 -0
- package/src/public/library.html +678 -0
- package/src/public/mcp-add.html +126 -0
- package/src/public/md.js +332 -0
- package/src/public/rpg/inn-street.jpg +0 -0
- package/src/public/settings.html +795 -0
- package/src/public/skills-add.html +212 -0
- package/src/public/studio.html +1181 -0
- package/src/public/style.css +1678 -0
- package/src/public/subagents-add.html +152 -0
- package/src/router.ts +978 -0
- package/src/send-budget.ts +52 -0
- package/src/server.ts +1 -0
- package/src/skill-import.ts +250 -0
- package/src/slash.ts +15 -0
- package/src/start.ts +103 -0
- package/src/store.ts +1208 -0
- package/src/subagent.ts +355 -0
- package/src/tools.ts +818 -0
- package/src/trajectory.ts +339 -0
- package/src/usage.ts +111 -0
- package/vendor/protocol/package.json +19 -0
- package/vendor/protocol/src/index.ts +159 -0
package/src/db.ts
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { DatabaseSync } from "node:sqlite";
|
|
10
|
+
import type {
|
|
11
|
+
ChatAttachment,
|
|
12
|
+
ChatMessage,
|
|
13
|
+
ChatPart,
|
|
14
|
+
ChatUsage,
|
|
15
|
+
Room,
|
|
16
|
+
} from "@guild/protocol";
|
|
17
|
+
import type { TrajectoryDraft, TrajectoryEvent } from "./trajectory.ts";
|
|
18
|
+
|
|
19
|
+
export const GUILD_DB_FILE = "guild.sqlite";
|
|
20
|
+
const SCHEMA_VERSION = "1";
|
|
21
|
+
|
|
22
|
+
const SCHEMA = `
|
|
23
|
+
PRAGMA journal_mode = WAL;
|
|
24
|
+
PRAGMA foreign_keys = ON;
|
|
25
|
+
PRAGMA busy_timeout = 5000;
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
28
|
+
key TEXT PRIMARY KEY,
|
|
29
|
+
value TEXT NOT NULL
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS rooms (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
kind TEXT NOT NULL CHECK (kind IN ('channel', 'dm')),
|
|
35
|
+
name TEXT NOT NULL,
|
|
36
|
+
member_ids TEXT NOT NULL DEFAULT '[]',
|
|
37
|
+
created_at TEXT NOT NULL
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
43
|
+
seq INTEGER NOT NULL,
|
|
44
|
+
author TEXT NOT NULL,
|
|
45
|
+
body TEXT NOT NULL,
|
|
46
|
+
parts TEXT,
|
|
47
|
+
reply_to TEXT,
|
|
48
|
+
attachments TEXT,
|
|
49
|
+
usage TEXT,
|
|
50
|
+
created_at TEXT NOT NULL,
|
|
51
|
+
finished_at TEXT,
|
|
52
|
+
steer INTEGER NOT NULL DEFAULT 0,
|
|
53
|
+
steer_bot_id TEXT,
|
|
54
|
+
UNIQUE (room_id, seq)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE INDEX IF NOT EXISTS messages_room_seq ON messages(room_id, seq);
|
|
58
|
+
|
|
59
|
+
CREATE TABLE IF NOT EXISTS trajectory (
|
|
60
|
+
room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
|
61
|
+
seq INTEGER NOT NULL,
|
|
62
|
+
ts TEXT NOT NULL,
|
|
63
|
+
turn_id TEXT NOT NULL,
|
|
64
|
+
bot_id TEXT,
|
|
65
|
+
kind TEXT NOT NULL,
|
|
66
|
+
summary TEXT NOT NULL,
|
|
67
|
+
payload TEXT,
|
|
68
|
+
result TEXT,
|
|
69
|
+
duration_ms INTEGER,
|
|
70
|
+
is_error INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
PRIMARY KEY (room_id, seq)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
CREATE TABLE IF NOT EXISTS compact (
|
|
75
|
+
room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
|
|
76
|
+
through_id TEXT NOT NULL,
|
|
77
|
+
summary TEXT NOT NULL,
|
|
78
|
+
updated_at TEXT NOT NULL,
|
|
79
|
+
message_count INTEGER NOT NULL DEFAULT 0
|
|
80
|
+
);
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
type CompactRow = {
|
|
84
|
+
throughId: string;
|
|
85
|
+
summary: string;
|
|
86
|
+
updatedAt: string;
|
|
87
|
+
messageCount: number;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function asString(value: unknown, fallback = ""): string {
|
|
91
|
+
return typeof value === "string" ? value : fallback;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function asNumber(value: unknown, fallback = 0): number {
|
|
95
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseJson<T>(raw: unknown, fallback: T): T {
|
|
99
|
+
if (typeof raw !== "string" || !raw) return fallback;
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(raw) as T;
|
|
102
|
+
} catch {
|
|
103
|
+
return fallback;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isChatMessage(value: unknown): value is ChatMessage {
|
|
108
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
109
|
+
const rec = value as Record<string, unknown>;
|
|
110
|
+
return (
|
|
111
|
+
typeof rec.id === "string" &&
|
|
112
|
+
typeof rec.roomId === "string" &&
|
|
113
|
+
typeof rec.author === "string" &&
|
|
114
|
+
typeof rec.body === "string" &&
|
|
115
|
+
typeof rec.createdAt === "string"
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseJsonlMessages(raw: string): ChatMessage[] {
|
|
120
|
+
const out: ChatMessage[] = [];
|
|
121
|
+
for (const line of raw.split("\n")) {
|
|
122
|
+
const trimmed = line.trim();
|
|
123
|
+
if (!trimmed) continue;
|
|
124
|
+
try {
|
|
125
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
126
|
+
if (isChatMessage(parsed)) out.push(parsed);
|
|
127
|
+
} catch {
|
|
128
|
+
/* skip */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseJsonlTrajectory(raw: string): TrajectoryEvent[] {
|
|
135
|
+
const out: TrajectoryEvent[] = [];
|
|
136
|
+
for (const line of raw.split("\n")) {
|
|
137
|
+
const trimmed = line.trim();
|
|
138
|
+
if (!trimmed) continue;
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(trimmed) as TrajectoryEvent;
|
|
141
|
+
if (parsed && typeof parsed.kind === "string") out.push(parsed);
|
|
142
|
+
} catch {
|
|
143
|
+
/* skip */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function messageFromRow(row: Record<string, unknown>): ChatMessage {
|
|
150
|
+
const parts = parseJson<ChatPart[] | null>(row.parts, null);
|
|
151
|
+
const attachments = parseJson<ChatAttachment[] | null>(row.attachments, null);
|
|
152
|
+
const usage = parseJson<ChatUsage | null>(row.usage, null);
|
|
153
|
+
const message: ChatMessage = {
|
|
154
|
+
id: asString(row.id),
|
|
155
|
+
roomId: asString(row.room_id),
|
|
156
|
+
author: asString(row.author),
|
|
157
|
+
body: asString(row.body),
|
|
158
|
+
createdAt: asString(row.created_at),
|
|
159
|
+
};
|
|
160
|
+
if (parts?.length) message.parts = parts;
|
|
161
|
+
const replyTo = asString(row.reply_to);
|
|
162
|
+
if (replyTo) message.replyTo = replyTo;
|
|
163
|
+
if (attachments?.length) message.attachments = attachments;
|
|
164
|
+
if (usage) message.usage = usage;
|
|
165
|
+
const finishedAt = asString(row.finished_at);
|
|
166
|
+
if (finishedAt) message.finishedAt = finishedAt;
|
|
167
|
+
if (asNumber(row.steer) === 1) message.steer = true;
|
|
168
|
+
const steerBotId = asString(row.steer_bot_id);
|
|
169
|
+
if (steerBotId) message.steerBotId = steerBotId;
|
|
170
|
+
return message;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function roomFromRow(row: Record<string, unknown>): Room {
|
|
174
|
+
const memberIds = parseJson<string[]>(row.member_ids, []);
|
|
175
|
+
return {
|
|
176
|
+
id: asString(row.id),
|
|
177
|
+
kind: asString(row.kind) === "dm" ? "dm" : "channel",
|
|
178
|
+
name: asString(row.name),
|
|
179
|
+
memberIds: Array.isArray(memberIds)
|
|
180
|
+
? memberIds.filter((id) => typeof id === "string")
|
|
181
|
+
: [],
|
|
182
|
+
createdAt: asString(row.created_at),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function trajectoryFromRow(row: Record<string, unknown>): TrajectoryEvent {
|
|
187
|
+
const event: TrajectoryEvent = {
|
|
188
|
+
seq: asNumber(row.seq),
|
|
189
|
+
ts: asString(row.ts),
|
|
190
|
+
turnId: asString(row.turn_id),
|
|
191
|
+
kind: asString(row.kind) as TrajectoryEvent["kind"],
|
|
192
|
+
summary: asString(row.summary),
|
|
193
|
+
};
|
|
194
|
+
const botId = asString(row.bot_id);
|
|
195
|
+
if (botId) event.botId = botId;
|
|
196
|
+
if (row.payload != null && row.payload !== "") {
|
|
197
|
+
event.payload = parseJson<unknown>(row.payload, undefined);
|
|
198
|
+
}
|
|
199
|
+
const result = asString(row.result);
|
|
200
|
+
if (result) event.result = result;
|
|
201
|
+
if (row.duration_ms != null) event.durationMs = asNumber(row.duration_ms);
|
|
202
|
+
if (asNumber(row.is_error) === 1) event.isError = true;
|
|
203
|
+
return event;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export class GuildDb {
|
|
207
|
+
readonly sqlite: DatabaseSync;
|
|
208
|
+
|
|
209
|
+
constructor(readonly path: string) {
|
|
210
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
211
|
+
this.sqlite = new DatabaseSync(path, { timeout: 5000 });
|
|
212
|
+
this.sqlite.exec(SCHEMA);
|
|
213
|
+
try {
|
|
214
|
+
this.sqlite.exec("ALTER TABLE messages ADD COLUMN steer_bot_id TEXT");
|
|
215
|
+
} catch {
|
|
216
|
+
/* column already exists on fresh schema */
|
|
217
|
+
}
|
|
218
|
+
this.sqlite.prepare(
|
|
219
|
+
"INSERT OR IGNORE INTO meta (key, value) VALUES ('schema', ?)",
|
|
220
|
+
).run(SCHEMA_VERSION);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
close(): void {
|
|
224
|
+
this.sqlite.close();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
importLegacyFiles(dataDir: string): void {
|
|
228
|
+
const root = join(dataDir, "rooms");
|
|
229
|
+
if (!existsSync(root)) return;
|
|
230
|
+
const ids = readdirSync(root, { withFileTypes: true })
|
|
231
|
+
.filter((entry) => entry.isDirectory())
|
|
232
|
+
.map((entry) => entry.name);
|
|
233
|
+
for (const id of ids) this.importRoomDir(join(root, id), id);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
upsertRoom(room: Room): void {
|
|
237
|
+
this.sqlite
|
|
238
|
+
.prepare(
|
|
239
|
+
`INSERT INTO rooms (id, kind, name, member_ids, created_at)
|
|
240
|
+
VALUES (?, ?, ?, ?, ?)
|
|
241
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
242
|
+
kind = excluded.kind,
|
|
243
|
+
name = excluded.name,
|
|
244
|
+
member_ids = excluded.member_ids,
|
|
245
|
+
created_at = excluded.created_at`,
|
|
246
|
+
)
|
|
247
|
+
.run(
|
|
248
|
+
room.id,
|
|
249
|
+
room.kind,
|
|
250
|
+
room.name,
|
|
251
|
+
JSON.stringify(room.memberIds),
|
|
252
|
+
room.createdAt,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
getRoom(id: string): Room | null {
|
|
257
|
+
const row = this.sqlite
|
|
258
|
+
.prepare("SELECT * FROM rooms WHERE id = ?")
|
|
259
|
+
.get(id) as Record<string, unknown> | undefined;
|
|
260
|
+
return row ? roomFromRow(row) : null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
listRooms(): Room[] {
|
|
264
|
+
const rows = this.sqlite
|
|
265
|
+
.prepare("SELECT * FROM rooms ORDER BY created_at ASC, id ASC")
|
|
266
|
+
.all() as Record<string, unknown>[];
|
|
267
|
+
return rows.map(roomFromRow);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
deleteRoom(id: string): void {
|
|
271
|
+
this.sqlite.prepare("DELETE FROM rooms WHERE id = ?").run(id);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
listMessages(roomId: string): ChatMessage[] {
|
|
275
|
+
const rows = this.sqlite
|
|
276
|
+
.prepare("SELECT * FROM messages WHERE room_id = ? ORDER BY seq ASC")
|
|
277
|
+
.all(roomId) as Record<string, unknown>[];
|
|
278
|
+
return rows.map(messageFromRow);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
peekLastMessage(roomId: string): ChatMessage | undefined {
|
|
282
|
+
const row = this.sqlite
|
|
283
|
+
.prepare(
|
|
284
|
+
"SELECT * FROM messages WHERE room_id = ? ORDER BY seq DESC LIMIT 1",
|
|
285
|
+
)
|
|
286
|
+
.get(roomId) as Record<string, unknown> | undefined;
|
|
287
|
+
return row ? messageFromRow(row) : undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
appendMessage(message: ChatMessage): void {
|
|
291
|
+
const seq = this.nextMessageSeq(message.roomId);
|
|
292
|
+
this.insertMessage(message, seq);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
replaceMessages(roomId: string, messages: ChatMessage[]): void {
|
|
296
|
+
this.sqlite.exec("BEGIN");
|
|
297
|
+
try {
|
|
298
|
+
this.sqlite.prepare("DELETE FROM messages WHERE room_id = ?").run(roomId);
|
|
299
|
+
messages.forEach((message, index) => this.insertMessage(message, index));
|
|
300
|
+
this.sqlite.exec("COMMIT");
|
|
301
|
+
} catch (error) {
|
|
302
|
+
this.sqlite.exec("ROLLBACK");
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
updateMessageBody(roomId: string, messageId: string, body: string): ChatMessage | null {
|
|
308
|
+
const result = this.sqlite
|
|
309
|
+
.prepare("UPDATE messages SET body = ? WHERE room_id = ? AND id = ?")
|
|
310
|
+
.run(body, roomId, messageId);
|
|
311
|
+
if (!result.changes) return null;
|
|
312
|
+
return this.getMessage(roomId, messageId);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
replaceMessage(
|
|
316
|
+
roomId: string,
|
|
317
|
+
messageId: string,
|
|
318
|
+
patch: {
|
|
319
|
+
body: string;
|
|
320
|
+
parts?: ChatPart[];
|
|
321
|
+
usage?: ChatUsage;
|
|
322
|
+
createdAt: string;
|
|
323
|
+
finishedAt: string;
|
|
324
|
+
},
|
|
325
|
+
): ChatMessage | null {
|
|
326
|
+
const result = this.sqlite
|
|
327
|
+
.prepare(
|
|
328
|
+
`UPDATE messages
|
|
329
|
+
SET body = ?, parts = ?, usage = ?, created_at = ?, finished_at = ?
|
|
330
|
+
WHERE room_id = ? AND id = ?`,
|
|
331
|
+
)
|
|
332
|
+
.run(
|
|
333
|
+
patch.body,
|
|
334
|
+
patch.parts?.length ? JSON.stringify(patch.parts) : null,
|
|
335
|
+
patch.usage ? JSON.stringify(patch.usage) : null,
|
|
336
|
+
patch.createdAt,
|
|
337
|
+
patch.finishedAt,
|
|
338
|
+
roomId,
|
|
339
|
+
messageId,
|
|
340
|
+
);
|
|
341
|
+
if (!result.changes) return null;
|
|
342
|
+
return this.getMessage(roomId, messageId);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
deleteMessage(roomId: string, messageId: string): ChatMessage | null {
|
|
346
|
+
const existing = this.getMessage(roomId, messageId);
|
|
347
|
+
if (!existing) return null;
|
|
348
|
+
this.sqlite
|
|
349
|
+
.prepare("DELETE FROM messages WHERE room_id = ? AND id = ?")
|
|
350
|
+
.run(roomId, messageId);
|
|
351
|
+
this.sqlite
|
|
352
|
+
.prepare("DELETE FROM trajectory WHERE room_id = ? AND turn_id = ?")
|
|
353
|
+
.run(roomId, messageId);
|
|
354
|
+
return existing;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
truncateAfter(roomId: string, messageId: string): ChatMessage[] | null {
|
|
358
|
+
const row = this.sqlite
|
|
359
|
+
.prepare("SELECT seq FROM messages WHERE room_id = ? AND id = ?")
|
|
360
|
+
.get(roomId, messageId) as { seq?: number } | undefined;
|
|
361
|
+
if (!row || typeof row.seq !== "number") return null;
|
|
362
|
+
this.sqlite
|
|
363
|
+
.prepare("DELETE FROM messages WHERE room_id = ? AND seq > ?")
|
|
364
|
+
.run(roomId, row.seq);
|
|
365
|
+
return this.listMessages(roomId);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
listTrajectory(roomId: string): TrajectoryEvent[] {
|
|
369
|
+
const rows = this.sqlite
|
|
370
|
+
.prepare("SELECT * FROM trajectory WHERE room_id = ? ORDER BY seq ASC")
|
|
371
|
+
.all(roomId) as Record<string, unknown>[];
|
|
372
|
+
return rows.map(trajectoryFromRow);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
appendTrajectory(roomId: string, drafts: TrajectoryDraft[]): TrajectoryEvent[] {
|
|
376
|
+
let seq = this.lastTrajectorySeq(roomId);
|
|
377
|
+
const written: TrajectoryEvent[] = [];
|
|
378
|
+
this.sqlite.exec("BEGIN");
|
|
379
|
+
try {
|
|
380
|
+
const insert = this.sqlite.prepare(
|
|
381
|
+
`INSERT INTO trajectory (
|
|
382
|
+
room_id, seq, ts, turn_id, bot_id, kind, summary, payload, result, duration_ms, is_error
|
|
383
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
384
|
+
);
|
|
385
|
+
for (const draft of drafts) {
|
|
386
|
+
seq += 1;
|
|
387
|
+
const event: TrajectoryEvent = { ...draft, seq };
|
|
388
|
+
insert.run(
|
|
389
|
+
roomId,
|
|
390
|
+
event.seq,
|
|
391
|
+
event.ts,
|
|
392
|
+
event.turnId,
|
|
393
|
+
event.botId ?? null,
|
|
394
|
+
event.kind,
|
|
395
|
+
event.summary,
|
|
396
|
+
event.payload === undefined ? null : JSON.stringify(event.payload),
|
|
397
|
+
event.result ?? null,
|
|
398
|
+
event.durationMs ?? null,
|
|
399
|
+
event.isError ? 1 : 0,
|
|
400
|
+
);
|
|
401
|
+
written.push(event);
|
|
402
|
+
}
|
|
403
|
+
this.sqlite.exec("COMMIT");
|
|
404
|
+
} catch (error) {
|
|
405
|
+
this.sqlite.exec("ROLLBACK");
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
return written;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
readCompact(roomId: string): CompactRow | null {
|
|
412
|
+
const row = this.sqlite
|
|
413
|
+
.prepare("SELECT * FROM compact WHERE room_id = ?")
|
|
414
|
+
.get(roomId) as Record<string, unknown> | undefined;
|
|
415
|
+
if (!row) return null;
|
|
416
|
+
const throughId = asString(row.through_id);
|
|
417
|
+
const summary = asString(row.summary);
|
|
418
|
+
if (!throughId || !summary) return null;
|
|
419
|
+
return {
|
|
420
|
+
throughId,
|
|
421
|
+
summary,
|
|
422
|
+
updatedAt: asString(row.updated_at),
|
|
423
|
+
messageCount: asNumber(row.message_count),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
writeCompact(roomId: string, compact: CompactRow): void {
|
|
428
|
+
this.sqlite
|
|
429
|
+
.prepare(
|
|
430
|
+
`INSERT INTO compact (room_id, through_id, summary, updated_at, message_count)
|
|
431
|
+
VALUES (?, ?, ?, ?, ?)
|
|
432
|
+
ON CONFLICT(room_id) DO UPDATE SET
|
|
433
|
+
through_id = excluded.through_id,
|
|
434
|
+
summary = excluded.summary,
|
|
435
|
+
updated_at = excluded.updated_at,
|
|
436
|
+
message_count = excluded.message_count`,
|
|
437
|
+
)
|
|
438
|
+
.run(
|
|
439
|
+
roomId,
|
|
440
|
+
compact.throughId,
|
|
441
|
+
compact.summary,
|
|
442
|
+
compact.updatedAt,
|
|
443
|
+
compact.messageCount,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private getMessage(roomId: string, messageId: string): ChatMessage | null {
|
|
448
|
+
const row = this.sqlite
|
|
449
|
+
.prepare("SELECT * FROM messages WHERE room_id = ? AND id = ?")
|
|
450
|
+
.get(roomId, messageId) as Record<string, unknown> | undefined;
|
|
451
|
+
return row ? messageFromRow(row) : null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private nextMessageSeq(roomId: string): number {
|
|
455
|
+
const row = this.sqlite
|
|
456
|
+
.prepare("SELECT COALESCE(MAX(seq), -1) AS seq FROM messages WHERE room_id = ?")
|
|
457
|
+
.get(roomId) as { seq?: number } | undefined;
|
|
458
|
+
return asNumber(row?.seq, -1) + 1;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private lastTrajectorySeq(roomId: string): number {
|
|
462
|
+
const row = this.sqlite
|
|
463
|
+
.prepare("SELECT COALESCE(MAX(seq), -1) AS seq FROM trajectory WHERE room_id = ?")
|
|
464
|
+
.get(roomId) as { seq?: number } | undefined;
|
|
465
|
+
return asNumber(row?.seq, -1);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private insertMessage(message: ChatMessage, seq: number): void {
|
|
469
|
+
this.sqlite
|
|
470
|
+
.prepare(
|
|
471
|
+
`INSERT INTO messages (
|
|
472
|
+
id, room_id, seq, author, body, parts, reply_to, attachments, usage,
|
|
473
|
+
created_at, finished_at, steer, steer_bot_id
|
|
474
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
475
|
+
)
|
|
476
|
+
.run(
|
|
477
|
+
message.id,
|
|
478
|
+
message.roomId,
|
|
479
|
+
seq,
|
|
480
|
+
message.author,
|
|
481
|
+
message.body,
|
|
482
|
+
message.parts?.length ? JSON.stringify(message.parts) : null,
|
|
483
|
+
message.replyTo ?? null,
|
|
484
|
+
message.attachments?.length ? JSON.stringify(message.attachments) : null,
|
|
485
|
+
message.usage ? JSON.stringify(message.usage) : null,
|
|
486
|
+
message.createdAt,
|
|
487
|
+
message.finishedAt ?? null,
|
|
488
|
+
message.steer ? 1 : 0,
|
|
489
|
+
message.steerBotId ?? null,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private importRoomDir(dir: string, id: string): void {
|
|
494
|
+
const roomFile = join(dir, "room.json");
|
|
495
|
+
if (existsSync(roomFile) && !this.getRoom(id)) {
|
|
496
|
+
try {
|
|
497
|
+
const parsed = JSON.parse(readFileSync(roomFile, "utf8")) as Room;
|
|
498
|
+
if (parsed && parsed.id === id && (parsed.kind === "channel" || parsed.kind === "dm")) {
|
|
499
|
+
this.upsertRoom({
|
|
500
|
+
id,
|
|
501
|
+
kind: parsed.kind,
|
|
502
|
+
name: typeof parsed.name === "string" ? parsed.name : id,
|
|
503
|
+
memberIds: Array.isArray(parsed.memberIds)
|
|
504
|
+
? parsed.memberIds.filter((item): item is string => typeof item === "string")
|
|
505
|
+
: [],
|
|
506
|
+
createdAt:
|
|
507
|
+
typeof parsed.createdAt === "string"
|
|
508
|
+
? parsed.createdAt
|
|
509
|
+
: "2026-01-01T00:00:00.000Z",
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
} catch {
|
|
513
|
+
/* skip bad room.json */
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (!this.getRoom(id)) return;
|
|
517
|
+
|
|
518
|
+
if (this.messageCount(id) === 0) {
|
|
519
|
+
const jsonl = join(dir, "messages.jsonl");
|
|
520
|
+
const json = join(dir, "messages.json");
|
|
521
|
+
let messages: ChatMessage[] = [];
|
|
522
|
+
if (existsSync(jsonl)) {
|
|
523
|
+
messages = parseJsonlMessages(readFileSync(jsonl, "utf8"));
|
|
524
|
+
} else if (existsSync(json)) {
|
|
525
|
+
try {
|
|
526
|
+
const parsed = JSON.parse(readFileSync(json, "utf8")) as unknown;
|
|
527
|
+
messages = Array.isArray(parsed) ? parsed.filter(isChatMessage) : [];
|
|
528
|
+
} catch {
|
|
529
|
+
messages = [];
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (messages.length) this.replaceMessages(id, messages);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (this.trajectoryCount(id) === 0) {
|
|
536
|
+
const path = join(dir, "trajectory.jsonl");
|
|
537
|
+
if (existsSync(path)) {
|
|
538
|
+
const events = parseJsonlTrajectory(readFileSync(path, "utf8"));
|
|
539
|
+
if (events.length) {
|
|
540
|
+
this.sqlite.exec("BEGIN");
|
|
541
|
+
try {
|
|
542
|
+
const insert = this.sqlite.prepare(
|
|
543
|
+
`INSERT INTO trajectory (
|
|
544
|
+
room_id, seq, ts, turn_id, bot_id, kind, summary, payload, result, duration_ms, is_error
|
|
545
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
546
|
+
);
|
|
547
|
+
for (const event of events) {
|
|
548
|
+
insert.run(
|
|
549
|
+
id,
|
|
550
|
+
event.seq,
|
|
551
|
+
event.ts,
|
|
552
|
+
event.turnId,
|
|
553
|
+
event.botId ?? null,
|
|
554
|
+
event.kind,
|
|
555
|
+
event.summary,
|
|
556
|
+
event.payload === undefined ? null : JSON.stringify(event.payload),
|
|
557
|
+
event.result ?? null,
|
|
558
|
+
event.durationMs ?? null,
|
|
559
|
+
event.isError ? 1 : 0,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
this.sqlite.exec("COMMIT");
|
|
563
|
+
} catch (error) {
|
|
564
|
+
this.sqlite.exec("ROLLBACK");
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (!this.readCompact(id)) {
|
|
572
|
+
const path = join(dir, "compact.json");
|
|
573
|
+
if (existsSync(path)) {
|
|
574
|
+
try {
|
|
575
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as {
|
|
576
|
+
throughId?: string;
|
|
577
|
+
summary?: string;
|
|
578
|
+
updatedAt?: string;
|
|
579
|
+
messageCount?: number;
|
|
580
|
+
};
|
|
581
|
+
if (parsed.throughId && parsed.summary) {
|
|
582
|
+
this.writeCompact(id, {
|
|
583
|
+
throughId: parsed.throughId,
|
|
584
|
+
summary: parsed.summary,
|
|
585
|
+
updatedAt: parsed.updatedAt || "",
|
|
586
|
+
messageCount: Number(parsed.messageCount) || 0,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
} catch {
|
|
590
|
+
/* skip */
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (this.getRoom(id)) rmIfExists(join(dir, "room.json"));
|
|
596
|
+
if (this.messageCount(id) > 0) {
|
|
597
|
+
rmIfExists(join(dir, "messages.json"));
|
|
598
|
+
rmIfExists(join(dir, "messages.jsonl"));
|
|
599
|
+
}
|
|
600
|
+
if (this.trajectoryCount(id) > 0) {
|
|
601
|
+
rmIfExists(join(dir, "trajectory.jsonl"));
|
|
602
|
+
}
|
|
603
|
+
if (this.readCompact(id)) rmIfExists(join(dir, "compact.json"));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private messageCount(roomId: string): number {
|
|
607
|
+
const row = this.sqlite
|
|
608
|
+
.prepare("SELECT COUNT(*) AS n FROM messages WHERE room_id = ?")
|
|
609
|
+
.get(roomId) as { n?: number } | undefined;
|
|
610
|
+
return asNumber(row?.n);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private trajectoryCount(roomId: string): number {
|
|
614
|
+
const row = this.sqlite
|
|
615
|
+
.prepare("SELECT COUNT(*) AS n FROM trajectory WHERE room_id = ?")
|
|
616
|
+
.get(roomId) as { n?: number } | undefined;
|
|
617
|
+
return asNumber(row?.n);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const OPEN = new Map<string, { db: GuildDb; refs: number }>();
|
|
622
|
+
|
|
623
|
+
export function openGuildDb(dataDir: string): GuildDb {
|
|
624
|
+
const path = resolve(join(dataDir, GUILD_DB_FILE));
|
|
625
|
+
const hit = OPEN.get(path);
|
|
626
|
+
if (hit) {
|
|
627
|
+
hit.refs += 1;
|
|
628
|
+
return hit.db;
|
|
629
|
+
}
|
|
630
|
+
const db = new GuildDb(path);
|
|
631
|
+
OPEN.set(path, { db, refs: 1 });
|
|
632
|
+
return db;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export function closeGuildDb(db: GuildDb): void {
|
|
636
|
+
const hit = OPEN.get(db.path);
|
|
637
|
+
if (!hit || hit.db !== db) {
|
|
638
|
+
try {
|
|
639
|
+
db.sqlite.close();
|
|
640
|
+
} catch {
|
|
641
|
+
/* already closed */
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
hit.refs -= 1;
|
|
646
|
+
if (hit.refs > 0) return;
|
|
647
|
+
OPEN.delete(db.path);
|
|
648
|
+
db.sqlite.close();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function rmIfExists(path: string): void {
|
|
652
|
+
if (existsSync(path)) rmSync(path, { force: true });
|
|
653
|
+
}
|