ocwatch 0.4.0 → 0.6.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/README.md +22 -3
- package/package.json +4 -4
- package/src/client/dist/assets/GraphView-BZV40eAE.css +1 -0
- package/src/client/dist/assets/GraphView-KWCCGYb2.js +9 -0
- package/src/client/dist/assets/graph-Cw_XSlvx.js +7 -0
- package/src/client/dist/assets/index-CbgYG3pJ.js +23 -0
- package/src/client/dist/assets/index-CgDCc8Mm.css +1 -0
- package/src/client/dist/assets/motion-CGUGF2CN.js +9 -0
- package/src/client/dist/index.html +4 -2
- package/src/server/__tests__/helpers/testDb.ts +220 -0
- package/src/server/index.ts +27 -27
- package/src/server/logic/activityLogic.ts +260 -0
- package/src/server/logic/index.ts +2 -0
- package/src/server/logic/sessionLogic.ts +107 -0
- package/src/server/routes/parts.ts +9 -7
- package/src/server/routes/poll.ts +32 -46
- package/src/server/routes/projects.ts +10 -27
- package/src/server/routes/sessions.ts +159 -68
- package/src/server/routes/sse.ts +10 -4
- package/src/server/services/parsing.ts +211 -0
- package/src/server/services/pollService.ts +400 -116
- package/src/server/services/recentSessions.ts +14 -0
- package/src/server/services/sessionContext.ts +97 -0
- package/src/server/services/sessionService.ts +97 -193
- package/src/server/services/sessionTree.ts +92 -0
- package/src/server/storage/db.ts +63 -0
- package/src/server/storage/index.ts +28 -0
- package/src/server/storage/queries.ts +528 -0
- package/src/server/utils/projectResolver.ts +9 -3
- package/src/server/utils/sessionStatus.ts +5 -89
- package/src/server/validation.ts +2 -4
- package/src/server/watcher.ts +225 -82
- package/src/shared/constants.ts +8 -3
- package/src/shared/index.ts +3 -0
- package/src/shared/types/index.ts +48 -53
- package/src/shared/utils/activityUtils.ts +3 -2
- package/src/client/dist/assets/index-BIu7r5_5.css +0 -1
- package/src/client/dist/assets/index-BYMVif3u.js +0 -50
- package/src/server/storage/messageParser.ts +0 -169
- package/src/server/storage/partParser.ts +0 -532
- package/src/server/storage/sessionParser.ts +0 -180
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const SQLITE_BUSY_TIMEOUT_MS = 5000;
|
|
7
|
+
const SQLITE_CACHE_SIZE = -20000;
|
|
8
|
+
|
|
9
|
+
let dbSingleton: Database | null | undefined;
|
|
10
|
+
|
|
11
|
+
function getStorageRootPath(): string {
|
|
12
|
+
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
13
|
+
if (xdgDataHome) {
|
|
14
|
+
return xdgDataHome;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return join(homedir(), ".local", "share");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getDbPath(): string {
|
|
21
|
+
return join(getStorageRootPath(), "opencode", "opencode.db");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function configureConnectionPragmas(db: Database): void {
|
|
25
|
+
db.query(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS};`).run();
|
|
26
|
+
db.query(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE};`).run();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function checkDbExists(): boolean {
|
|
30
|
+
return existsSync(getDbPath());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getDb(): Database | null {
|
|
34
|
+
if (dbSingleton !== undefined) {
|
|
35
|
+
return dbSingleton;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!checkDbExists()) {
|
|
39
|
+
dbSingleton = null;
|
|
40
|
+
return dbSingleton;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const db = new Database(getDbPath(), { readonly: true });
|
|
45
|
+
configureConnectionPragmas(db);
|
|
46
|
+
dbSingleton = db;
|
|
47
|
+
return dbSingleton;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
50
|
+
console.warn(`[storage/db] Failed to open SQLite database: ${reason}`);
|
|
51
|
+
dbSingleton = null;
|
|
52
|
+
return dbSingleton;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function closeDb(): void {
|
|
57
|
+
if (!dbSingleton) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
dbSingleton.close();
|
|
62
|
+
dbSingleton = undefined;
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export { checkDbExists, closeDb, getDb } from "./db";
|
|
2
|
+
export {
|
|
3
|
+
queryMaxTimestamp,
|
|
4
|
+
queryMessages,
|
|
5
|
+
queryMessagesForSessions,
|
|
6
|
+
queryPart,
|
|
7
|
+
queryParts,
|
|
8
|
+
queryPartsForSessions,
|
|
9
|
+
queryProjects,
|
|
10
|
+
queryProjectByWorktree,
|
|
11
|
+
queryProjectSummaries,
|
|
12
|
+
querySession,
|
|
13
|
+
querySessionChildren,
|
|
14
|
+
querySessionSubtree,
|
|
15
|
+
querySessionSubtreeRevision,
|
|
16
|
+
querySessions,
|
|
17
|
+
queryTodos,
|
|
18
|
+
listProjects,
|
|
19
|
+
} from "./queries";
|
|
20
|
+
export type {
|
|
21
|
+
DbMessageRow,
|
|
22
|
+
DbPartRow,
|
|
23
|
+
DbProjectRow,
|
|
24
|
+
DbProjectSummaryRow,
|
|
25
|
+
DbSessionRow,
|
|
26
|
+
DbTodoRow,
|
|
27
|
+
} from "./queries";
|
|
28
|
+
export { parseBoulder, calculatePlanProgress } from "./boulderParser";
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import type { Database, Statement } from "bun:sqlite";
|
|
2
|
+
import { getDb } from "./db";
|
|
3
|
+
|
|
4
|
+
export interface DbProjectRow {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string | null;
|
|
7
|
+
worktree: string;
|
|
8
|
+
vcs: string | null;
|
|
9
|
+
commands: string | null;
|
|
10
|
+
sandboxes: string | null;
|
|
11
|
+
timeCreated: number;
|
|
12
|
+
timeUpdated: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DbSessionRow {
|
|
16
|
+
id: string;
|
|
17
|
+
projectID: string;
|
|
18
|
+
parentID: string | null;
|
|
19
|
+
slug: string | null;
|
|
20
|
+
directory: string;
|
|
21
|
+
title: string;
|
|
22
|
+
version: string | null;
|
|
23
|
+
timeCreated: number;
|
|
24
|
+
timeUpdated: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DbMessageRow {
|
|
28
|
+
id: string;
|
|
29
|
+
sessionID: string;
|
|
30
|
+
timeCreated: number;
|
|
31
|
+
timeUpdated: number;
|
|
32
|
+
role: string | null;
|
|
33
|
+
agent: string | null;
|
|
34
|
+
data: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DbPartRow {
|
|
38
|
+
id: string;
|
|
39
|
+
messageID: string;
|
|
40
|
+
sessionID: string;
|
|
41
|
+
timeCreated: number;
|
|
42
|
+
timeUpdated: number;
|
|
43
|
+
type: string | null;
|
|
44
|
+
tool: string | null;
|
|
45
|
+
state: string | null;
|
|
46
|
+
data: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DbTodoRow {
|
|
50
|
+
sessionID: string;
|
|
51
|
+
content: string;
|
|
52
|
+
status: string;
|
|
53
|
+
priority: string;
|
|
54
|
+
position: number;
|
|
55
|
+
timeCreated: number;
|
|
56
|
+
timeUpdated: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DbProjectSummaryRow {
|
|
60
|
+
id: string;
|
|
61
|
+
worktree: string;
|
|
62
|
+
sessionCount: number;
|
|
63
|
+
lastActivityAt: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let cachedDb: Database | null | undefined;
|
|
67
|
+
|
|
68
|
+
let queryProjectsStmt: Statement<DbProjectRow, []> | null = null;
|
|
69
|
+
let querySessionsStmt: Statement<DbSessionRow, [string | null, number | null, number]> | null = null;
|
|
70
|
+
let querySessionStmt: Statement<DbSessionRow, [string]> | null = null;
|
|
71
|
+
let querySessionChildrenStmt: Statement<DbSessionRow, [string]> | null = null;
|
|
72
|
+
let querySessionSubtreeStmt: Statement<DbSessionRow, [string]> | null = null;
|
|
73
|
+
let queryMessagesStmt: Statement<DbMessageRow, [string, number]> | null = null;
|
|
74
|
+
let queryPartsStmt: Statement<DbPartRow, [string]> | null = null;
|
|
75
|
+
let queryPartStmt: Statement<DbPartRow, [string]> | null = null;
|
|
76
|
+
let queryTodosStmt: Statement<DbTodoRow, [string]> | null = null;
|
|
77
|
+
let queryMaxTimestampStmt: Statement<{ maxTimestamp: number | null }, []> | null = null;
|
|
78
|
+
let querySessionSubtreeRevisionStmt: Statement<{ maxTimestamp: number | null }, [string]> | null = null;
|
|
79
|
+
let queryProjectByWorktreeStmt: Statement<DbProjectRow, [string]> | null = null;
|
|
80
|
+
let queryProjectSummariesStmt: Statement<DbProjectSummaryRow, []> | null = null;
|
|
81
|
+
|
|
82
|
+
function getReadyDb(): Database | null {
|
|
83
|
+
const db = getDb();
|
|
84
|
+
if (!db) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (cachedDb === db) {
|
|
89
|
+
return db;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
cachedDb = db;
|
|
93
|
+
queryProjectsStmt = db.query<DbProjectRow, []>(`
|
|
94
|
+
SELECT
|
|
95
|
+
id,
|
|
96
|
+
name,
|
|
97
|
+
worktree,
|
|
98
|
+
vcs,
|
|
99
|
+
commands,
|
|
100
|
+
sandboxes,
|
|
101
|
+
time_created AS timeCreated,
|
|
102
|
+
time_updated AS timeUpdated
|
|
103
|
+
FROM project
|
|
104
|
+
ORDER BY time_updated DESC
|
|
105
|
+
`);
|
|
106
|
+
|
|
107
|
+
querySessionsStmt = db.query<DbSessionRow, [string | null, number | null, number]>(`
|
|
108
|
+
SELECT
|
|
109
|
+
id,
|
|
110
|
+
project_id AS projectID,
|
|
111
|
+
parent_id AS parentID,
|
|
112
|
+
slug,
|
|
113
|
+
directory,
|
|
114
|
+
title,
|
|
115
|
+
version,
|
|
116
|
+
time_created AS timeCreated,
|
|
117
|
+
time_updated AS timeUpdated
|
|
118
|
+
FROM session
|
|
119
|
+
WHERE (?1 IS NULL OR project_id = ?1)
|
|
120
|
+
AND (?2 IS NULL OR time_updated > ?2)
|
|
121
|
+
ORDER BY time_updated DESC
|
|
122
|
+
LIMIT ?3
|
|
123
|
+
`);
|
|
124
|
+
|
|
125
|
+
querySessionStmt = db.query<DbSessionRow, [string]>(`
|
|
126
|
+
SELECT
|
|
127
|
+
id,
|
|
128
|
+
project_id AS projectID,
|
|
129
|
+
parent_id AS parentID,
|
|
130
|
+
slug,
|
|
131
|
+
directory,
|
|
132
|
+
title,
|
|
133
|
+
version,
|
|
134
|
+
time_created AS timeCreated,
|
|
135
|
+
time_updated AS timeUpdated
|
|
136
|
+
FROM session
|
|
137
|
+
WHERE id = ?1
|
|
138
|
+
LIMIT 1
|
|
139
|
+
`);
|
|
140
|
+
|
|
141
|
+
querySessionChildrenStmt = db.query<DbSessionRow, [string]>(`
|
|
142
|
+
SELECT
|
|
143
|
+
id,
|
|
144
|
+
project_id AS projectID,
|
|
145
|
+
parent_id AS parentID,
|
|
146
|
+
slug,
|
|
147
|
+
directory,
|
|
148
|
+
title,
|
|
149
|
+
version,
|
|
150
|
+
time_created AS timeCreated,
|
|
151
|
+
time_updated AS timeUpdated
|
|
152
|
+
FROM session
|
|
153
|
+
WHERE parent_id = ?1
|
|
154
|
+
ORDER BY time_created ASC
|
|
155
|
+
`);
|
|
156
|
+
|
|
157
|
+
querySessionSubtreeStmt = db.query<DbSessionRow, [string]>(`
|
|
158
|
+
WITH RECURSIVE subtree AS (
|
|
159
|
+
SELECT
|
|
160
|
+
id,
|
|
161
|
+
project_id AS projectID,
|
|
162
|
+
parent_id AS parentID,
|
|
163
|
+
slug,
|
|
164
|
+
directory,
|
|
165
|
+
title,
|
|
166
|
+
version,
|
|
167
|
+
time_created AS timeCreated,
|
|
168
|
+
time_updated AS timeUpdated
|
|
169
|
+
FROM session
|
|
170
|
+
WHERE id = ?1
|
|
171
|
+
|
|
172
|
+
UNION ALL
|
|
173
|
+
|
|
174
|
+
SELECT
|
|
175
|
+
s.id,
|
|
176
|
+
s.project_id AS projectID,
|
|
177
|
+
s.parent_id AS parentID,
|
|
178
|
+
s.slug,
|
|
179
|
+
s.directory,
|
|
180
|
+
s.title,
|
|
181
|
+
s.version,
|
|
182
|
+
s.time_created AS timeCreated,
|
|
183
|
+
s.time_updated AS timeUpdated
|
|
184
|
+
FROM session s
|
|
185
|
+
INNER JOIN subtree st ON s.parent_id = st.id
|
|
186
|
+
)
|
|
187
|
+
SELECT
|
|
188
|
+
id,
|
|
189
|
+
projectID,
|
|
190
|
+
parentID,
|
|
191
|
+
slug,
|
|
192
|
+
directory,
|
|
193
|
+
title,
|
|
194
|
+
version,
|
|
195
|
+
timeCreated,
|
|
196
|
+
timeUpdated
|
|
197
|
+
FROM subtree
|
|
198
|
+
ORDER BY timeCreated ASC, id ASC
|
|
199
|
+
`);
|
|
200
|
+
|
|
201
|
+
queryMessagesStmt = db.query<DbMessageRow, [string, number]>(`
|
|
202
|
+
SELECT
|
|
203
|
+
id,
|
|
204
|
+
session_id AS sessionID,
|
|
205
|
+
time_created AS timeCreated,
|
|
206
|
+
time_updated AS timeUpdated,
|
|
207
|
+
json_extract(data, '$.role') AS role,
|
|
208
|
+
json_extract(data, '$.agent') AS agent,
|
|
209
|
+
data
|
|
210
|
+
FROM message
|
|
211
|
+
WHERE session_id = ?1
|
|
212
|
+
ORDER BY time_created DESC
|
|
213
|
+
LIMIT ?2
|
|
214
|
+
`);
|
|
215
|
+
|
|
216
|
+
queryPartsStmt = db.query<DbPartRow, [string]>(`
|
|
217
|
+
SELECT
|
|
218
|
+
id,
|
|
219
|
+
message_id AS messageID,
|
|
220
|
+
session_id AS sessionID,
|
|
221
|
+
time_created AS timeCreated,
|
|
222
|
+
time_updated AS timeUpdated,
|
|
223
|
+
json_extract(data, '$.type') AS type,
|
|
224
|
+
json_extract(data, '$.tool') AS tool,
|
|
225
|
+
CASE
|
|
226
|
+
WHEN json_type(data, '$.state') = 'text' THEN json_extract(data, '$.state')
|
|
227
|
+
WHEN json_type(data, '$.state.type') = 'text' THEN json_extract(data, '$.state.type')
|
|
228
|
+
ELSE NULL
|
|
229
|
+
END AS state,
|
|
230
|
+
data
|
|
231
|
+
FROM part
|
|
232
|
+
WHERE session_id = ?1
|
|
233
|
+
ORDER BY time_created DESC
|
|
234
|
+
`);
|
|
235
|
+
|
|
236
|
+
queryPartStmt = db.query<DbPartRow, [string]>(`
|
|
237
|
+
SELECT
|
|
238
|
+
id,
|
|
239
|
+
message_id AS messageID,
|
|
240
|
+
session_id AS sessionID,
|
|
241
|
+
time_created AS timeCreated,
|
|
242
|
+
time_updated AS timeUpdated,
|
|
243
|
+
json_extract(data, '$.type') AS type,
|
|
244
|
+
json_extract(data, '$.tool') AS tool,
|
|
245
|
+
CASE
|
|
246
|
+
WHEN json_type(data, '$.state') = 'text' THEN json_extract(data, '$.state')
|
|
247
|
+
WHEN json_type(data, '$.state.type') = 'text' THEN json_extract(data, '$.state.type')
|
|
248
|
+
ELSE NULL
|
|
249
|
+
END AS state,
|
|
250
|
+
data
|
|
251
|
+
FROM part
|
|
252
|
+
WHERE id = ?1
|
|
253
|
+
LIMIT 1
|
|
254
|
+
`);
|
|
255
|
+
|
|
256
|
+
queryTodosStmt = db.query<DbTodoRow, [string]>(`
|
|
257
|
+
SELECT
|
|
258
|
+
session_id AS sessionID,
|
|
259
|
+
content,
|
|
260
|
+
status,
|
|
261
|
+
priority,
|
|
262
|
+
position,
|
|
263
|
+
time_created AS timeCreated,
|
|
264
|
+
time_updated AS timeUpdated
|
|
265
|
+
FROM todo
|
|
266
|
+
WHERE session_id = ?1
|
|
267
|
+
ORDER BY position ASC, time_created ASC
|
|
268
|
+
`);
|
|
269
|
+
|
|
270
|
+
queryMaxTimestampStmt = db.query<{ maxTimestamp: number | null }, []>(`
|
|
271
|
+
SELECT MAX(ts) AS maxTimestamp
|
|
272
|
+
FROM (
|
|
273
|
+
SELECT MAX(time_updated) AS ts FROM session
|
|
274
|
+
UNION ALL
|
|
275
|
+
SELECT MAX(time_updated) AS ts FROM message
|
|
276
|
+
UNION ALL
|
|
277
|
+
SELECT MAX(time_updated) AS ts FROM part
|
|
278
|
+
)
|
|
279
|
+
`);
|
|
280
|
+
|
|
281
|
+
querySessionSubtreeRevisionStmt = db.query<{ maxTimestamp: number | null }, [string]>(`
|
|
282
|
+
WITH RECURSIVE subtree AS (
|
|
283
|
+
SELECT id
|
|
284
|
+
FROM session
|
|
285
|
+
WHERE id = ?1
|
|
286
|
+
|
|
287
|
+
UNION ALL
|
|
288
|
+
|
|
289
|
+
SELECT s.id
|
|
290
|
+
FROM session s
|
|
291
|
+
INNER JOIN subtree st ON s.parent_id = st.id
|
|
292
|
+
)
|
|
293
|
+
SELECT MAX(ts) AS maxTimestamp
|
|
294
|
+
FROM (
|
|
295
|
+
SELECT MAX(time_updated) AS ts FROM session WHERE id IN (SELECT id FROM subtree)
|
|
296
|
+
UNION ALL
|
|
297
|
+
SELECT MAX(time_updated) AS ts FROM message WHERE session_id IN (SELECT id FROM subtree)
|
|
298
|
+
UNION ALL
|
|
299
|
+
SELECT MAX(time_updated) AS ts FROM part WHERE session_id IN (SELECT id FROM subtree)
|
|
300
|
+
)
|
|
301
|
+
`);
|
|
302
|
+
|
|
303
|
+
queryProjectByWorktreeStmt = db.query<DbProjectRow, [string]>(`
|
|
304
|
+
SELECT
|
|
305
|
+
id,
|
|
306
|
+
name,
|
|
307
|
+
worktree,
|
|
308
|
+
vcs,
|
|
309
|
+
commands,
|
|
310
|
+
sandboxes,
|
|
311
|
+
time_created AS timeCreated,
|
|
312
|
+
time_updated AS timeUpdated
|
|
313
|
+
FROM project
|
|
314
|
+
WHERE worktree = ?1
|
|
315
|
+
LIMIT 1
|
|
316
|
+
`);
|
|
317
|
+
|
|
318
|
+
queryProjectSummariesStmt = db.query<DbProjectSummaryRow, []>(`
|
|
319
|
+
SELECT
|
|
320
|
+
p.id,
|
|
321
|
+
p.worktree,
|
|
322
|
+
COUNT(s.id) AS sessionCount,
|
|
323
|
+
COALESCE(MAX(s.time_updated), p.time_updated) AS lastActivityAt
|
|
324
|
+
FROM project p
|
|
325
|
+
LEFT JOIN session s ON s.project_id = p.id
|
|
326
|
+
GROUP BY p.id
|
|
327
|
+
ORDER BY lastActivityAt DESC
|
|
328
|
+
`);
|
|
329
|
+
|
|
330
|
+
return db;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function queryProjects(): DbProjectRow[] {
|
|
334
|
+
const db = getReadyDb();
|
|
335
|
+
if (!db || !queryProjectsStmt) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return queryProjectsStmt.all();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function querySessions(
|
|
343
|
+
projectId?: string,
|
|
344
|
+
since?: number,
|
|
345
|
+
limit = 20,
|
|
346
|
+
): DbSessionRow[] {
|
|
347
|
+
const db = getReadyDb();
|
|
348
|
+
if (!db || !querySessionsStmt) {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return querySessionsStmt.all(projectId ?? null, since ?? null, limit);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function querySession(sessionId: string): DbSessionRow | null {
|
|
356
|
+
const db = getReadyDb();
|
|
357
|
+
if (!db || !querySessionStmt) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return querySessionStmt.get(sessionId);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function querySessionChildren(sessionId: string): DbSessionRow[] {
|
|
365
|
+
const db = getReadyDb();
|
|
366
|
+
if (!db || !querySessionChildrenStmt) {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return querySessionChildrenStmt.all(sessionId);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function querySessionSubtree(sessionId: string): DbSessionRow[] {
|
|
374
|
+
const db = getReadyDb();
|
|
375
|
+
if (!db || !querySessionSubtreeStmt) {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return querySessionSubtreeStmt.all(sessionId);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function queryMessages(sessionId: string, limit = 100): DbMessageRow[] {
|
|
383
|
+
const db = getReadyDb();
|
|
384
|
+
if (!db || !queryMessagesStmt) {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return queryMessagesStmt.all(sessionId, limit);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function queryParts(sessionId: string): DbPartRow[] {
|
|
392
|
+
const db = getReadyDb();
|
|
393
|
+
if (!db || !queryPartsStmt) {
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return queryPartsStmt.all(sessionId);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function queryMessagesForSessions(sessionIds: string[], limitPerSession = 100): DbMessageRow[] {
|
|
401
|
+
const db = getReadyDb();
|
|
402
|
+
if (!db || sessionIds.length === 0) {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const placeholders = sessionIds.map((_, index) => `?${index + 1}`).join(", ");
|
|
407
|
+
const limitParam = `?${sessionIds.length + 1}`;
|
|
408
|
+
const statement = db.query<DbMessageRow, (string | number)[]>(`
|
|
409
|
+
SELECT
|
|
410
|
+
id,
|
|
411
|
+
sessionID,
|
|
412
|
+
timeCreated,
|
|
413
|
+
timeUpdated,
|
|
414
|
+
role,
|
|
415
|
+
agent,
|
|
416
|
+
data
|
|
417
|
+
FROM (
|
|
418
|
+
SELECT
|
|
419
|
+
id,
|
|
420
|
+
session_id AS sessionID,
|
|
421
|
+
time_created AS timeCreated,
|
|
422
|
+
time_updated AS timeUpdated,
|
|
423
|
+
json_extract(data, '$.role') AS role,
|
|
424
|
+
json_extract(data, '$.agent') AS agent,
|
|
425
|
+
data,
|
|
426
|
+
ROW_NUMBER() OVER (
|
|
427
|
+
PARTITION BY session_id
|
|
428
|
+
ORDER BY time_created DESC, id DESC
|
|
429
|
+
) AS rowNumber
|
|
430
|
+
FROM message
|
|
431
|
+
WHERE session_id IN (${placeholders})
|
|
432
|
+
)
|
|
433
|
+
WHERE rowNumber <= ${limitParam}
|
|
434
|
+
ORDER BY sessionID ASC, timeCreated DESC, id DESC
|
|
435
|
+
`);
|
|
436
|
+
|
|
437
|
+
return statement.all(...sessionIds, limitPerSession);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function queryPartsForSessions(sessionIds: string[]): DbPartRow[] {
|
|
441
|
+
const db = getReadyDb();
|
|
442
|
+
if (!db || sessionIds.length === 0) {
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const placeholders = sessionIds.map((_, index) => `?${index + 1}`).join(", ");
|
|
447
|
+
const statement = db.query<DbPartRow, string[]>(`
|
|
448
|
+
SELECT
|
|
449
|
+
id,
|
|
450
|
+
message_id AS messageID,
|
|
451
|
+
session_id AS sessionID,
|
|
452
|
+
time_created AS timeCreated,
|
|
453
|
+
time_updated AS timeUpdated,
|
|
454
|
+
json_extract(data, '$.type') AS type,
|
|
455
|
+
json_extract(data, '$.tool') AS tool,
|
|
456
|
+
CASE
|
|
457
|
+
WHEN json_type(data, '$.state') = 'text' THEN json_extract(data, '$.state')
|
|
458
|
+
WHEN json_type(data, '$.state.type') = 'text' THEN json_extract(data, '$.state.type')
|
|
459
|
+
ELSE NULL
|
|
460
|
+
END AS state,
|
|
461
|
+
data
|
|
462
|
+
FROM part
|
|
463
|
+
WHERE session_id IN (${placeholders})
|
|
464
|
+
ORDER BY sessionID ASC, timeCreated DESC, id DESC
|
|
465
|
+
`);
|
|
466
|
+
|
|
467
|
+
return statement.all(...sessionIds);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function queryPart(partId: string): DbPartRow | null {
|
|
471
|
+
const db = getReadyDb();
|
|
472
|
+
if (!db || !queryPartStmt) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return queryPartStmt.get(partId);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function queryTodos(sessionId: string): DbTodoRow[] {
|
|
480
|
+
const db = getReadyDb();
|
|
481
|
+
if (!db || !queryTodosStmt) {
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return queryTodosStmt.all(sessionId);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function queryMaxTimestamp(): number {
|
|
489
|
+
const db = getReadyDb();
|
|
490
|
+
if (!db || !queryMaxTimestampStmt) {
|
|
491
|
+
return 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return Number(queryMaxTimestampStmt.get()?.maxTimestamp ?? 0);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function querySessionSubtreeRevision(sessionId: string): number {
|
|
498
|
+
const db = getReadyDb();
|
|
499
|
+
if (!db || !querySessionSubtreeRevisionStmt) {
|
|
500
|
+
return 0;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return Number(querySessionSubtreeRevisionStmt.get(sessionId)?.maxTimestamp ?? 0);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
export function listProjects(): string[] {
|
|
508
|
+
const projects = queryProjects();
|
|
509
|
+
return projects.map((p) => p.id);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function queryProjectByWorktree(directory: string): DbProjectRow | null {
|
|
513
|
+
const db = getReadyDb();
|
|
514
|
+
if (!db || !queryProjectByWorktreeStmt) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return queryProjectByWorktreeStmt.get(directory);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export function queryProjectSummaries(): DbProjectSummaryRow[] {
|
|
522
|
+
const db = getReadyDb();
|
|
523
|
+
if (!db || !queryProjectSummariesStmt) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return queryProjectSummariesStmt.all();
|
|
528
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { queryProjects } from "../storage";
|
|
3
3
|
import type { SessionMetadata } from "../../shared/types";
|
|
4
4
|
|
|
5
5
|
async function directoryExists(directory: string): Promise<boolean> {
|
|
@@ -15,8 +15,14 @@ export async function resolveProjectDirectory(
|
|
|
15
15
|
projectId: string,
|
|
16
16
|
preloadedSessions?: SessionMetadata[],
|
|
17
17
|
): Promise<string | null> {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
let directory: string | undefined;
|
|
19
|
+
|
|
20
|
+
if (preloadedSessions) {
|
|
21
|
+
directory = preloadedSessions.find((session) => session.projectID === projectId)?.directory;
|
|
22
|
+
} else {
|
|
23
|
+
const projects = queryProjects();
|
|
24
|
+
directory = projects.find((p) => p.id === projectId)?.worktree;
|
|
25
|
+
}
|
|
20
26
|
|
|
21
27
|
if (!directory) {
|
|
22
28
|
return null;
|