pi-sessions 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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/extensions/session-ask.ts +297 -0
  4. package/extensions/session-auto-title/command.ts +109 -0
  5. package/extensions/session-auto-title/context.ts +41 -0
  6. package/extensions/session-auto-title/controller.ts +298 -0
  7. package/extensions/session-auto-title/model.ts +61 -0
  8. package/extensions/session-auto-title/prompt.ts +54 -0
  9. package/extensions/session-auto-title/retitle.ts +641 -0
  10. package/extensions/session-auto-title/state.ts +69 -0
  11. package/extensions/session-auto-title/wizard.ts +583 -0
  12. package/extensions/session-auto-title.ts +209 -0
  13. package/extensions/session-handoff/extract.ts +278 -0
  14. package/extensions/session-handoff/metadata.ts +96 -0
  15. package/extensions/session-handoff/picker.ts +394 -0
  16. package/extensions/session-handoff/query.ts +318 -0
  17. package/extensions/session-handoff/refs.ts +151 -0
  18. package/extensions/session-handoff/review.ts +268 -0
  19. package/extensions/session-handoff.ts +203 -0
  20. package/extensions/session-hooks.ts +55 -0
  21. package/extensions/session-index.ts +159 -0
  22. package/extensions/session-search/extract.ts +997 -0
  23. package/extensions/session-search/hooks.ts +350 -0
  24. package/extensions/session-search/normalize.ts +170 -0
  25. package/extensions/session-search/reindex.ts +93 -0
  26. package/extensions/session-search.ts +390 -0
  27. package/extensions/shared/search-snippet.ts +40 -0
  28. package/extensions/shared/session-index/common.ts +222 -0
  29. package/extensions/shared/session-index/index.ts +5 -0
  30. package/extensions/shared/session-index/lineage.ts +417 -0
  31. package/extensions/shared/session-index/schema.ts +178 -0
  32. package/extensions/shared/session-index/search.ts +688 -0
  33. package/extensions/shared/session-index/store.ts +173 -0
  34. package/extensions/shared/session-ui.ts +15 -0
  35. package/extensions/shared/settings.ts +141 -0
  36. package/extensions/shared/time.ts +38 -0
  37. package/extensions/shared/typebox.ts +61 -0
  38. package/images/handoff.png +0 -0
  39. package/images/session-title.png +0 -0
  40. package/images/session_ask.png +0 -0
  41. package/images/session_picker.png +0 -0
  42. package/images/session_search.png +0 -0
  43. package/package.json +64 -0
@@ -0,0 +1,417 @@
1
+ import { type Static, Type } from "@sinclair/typebox";
2
+ import { parseTypeBoxRows, parseTypeBoxValue } from "../typebox.js";
3
+ import {
4
+ NULLABLE_STRING_SCHEMA,
5
+ parseRepoRoots,
6
+ SESSION_LINEAGE_RELATION_SCHEMA,
7
+ SESSION_ORIGIN_SCHEMA,
8
+ type SessionIndexDatabase,
9
+ type SessionLineageRelation,
10
+ type SessionLineageRow,
11
+ type SessionRelatedSessionRow,
12
+ } from "./common.js";
13
+
14
+ const SESSION_GRAPH_ROW_SCHEMA = Type.Object({
15
+ sessionId: Type.String(),
16
+ sessionPath: Type.String(),
17
+ parentSessionPath: NULLABLE_STRING_SCHEMA,
18
+ parentSessionId: NULLABLE_STRING_SCHEMA,
19
+ });
20
+
21
+ const SESSION_LINEAGE_QUERY_ROW_SCHEMA = Type.Object({
22
+ sessionId: Type.String(),
23
+ sessionPath: Type.String(),
24
+ sessionName: Type.String(),
25
+ firstUserPrompt: NULLABLE_STRING_SCHEMA,
26
+ cwd: Type.String(),
27
+ repoRootsJson: Type.String(),
28
+ modifiedAt: Type.String(),
29
+ parentSessionPath: NULLABLE_STRING_SCHEMA,
30
+ parentSessionId: NULLABLE_STRING_SCHEMA,
31
+ sessionOrigin: Type.Union([SESSION_ORIGIN_SCHEMA, Type.Null()]),
32
+ handoffGoal: NULLABLE_STRING_SCHEMA,
33
+ handoffNextTask: NULLABLE_STRING_SCHEMA,
34
+ });
35
+
36
+ const SESSION_RELATED_QUERY_ROW_SCHEMA = Type.Intersect([
37
+ SESSION_LINEAGE_QUERY_ROW_SCHEMA,
38
+ Type.Object({
39
+ relation: SESSION_LINEAGE_RELATION_SCHEMA,
40
+ distance: Type.Number(),
41
+ }),
42
+ ]);
43
+
44
+ type SessionGraphNode = Static<typeof SESSION_GRAPH_ROW_SCHEMA> & {
45
+ resolvedParentSessionId?: string | undefined;
46
+ };
47
+
48
+ interface MaterializedLineageRow {
49
+ sessionId: string;
50
+ relatedSessionId: string;
51
+ relation: SessionLineageRelation;
52
+ distance: number;
53
+ }
54
+
55
+ function sessionLineageColumns(alias?: string): string {
56
+ const prefix = alias ? `${alias}.` : "";
57
+ return [
58
+ `${prefix}session_id as sessionId`,
59
+ `${prefix}session_path as sessionPath`,
60
+ `${prefix}session_name as sessionName`,
61
+ `${prefix}first_user_prompt as firstUserPrompt`,
62
+ `${prefix}cwd`,
63
+ `${prefix}repo_roots_json as repoRootsJson`,
64
+ `${prefix}modified_ts as modifiedAt`,
65
+ `${prefix}parent_session_path as parentSessionPath`,
66
+ `${prefix}parent_session_id as parentSessionId`,
67
+ `${prefix}session_origin as sessionOrigin`,
68
+ `${prefix}handoff_goal as handoffGoal`,
69
+ `${prefix}handoff_next_task as handoffNextTask`,
70
+ ].join(",\n ");
71
+ }
72
+
73
+ function buildSessionLineageRow(
74
+ row: Static<typeof SESSION_LINEAGE_QUERY_ROW_SCHEMA>,
75
+ ): SessionLineageRow {
76
+ return {
77
+ sessionId: row.sessionId,
78
+ sessionPath: row.sessionPath,
79
+ sessionName: row.sessionName,
80
+ firstUserPrompt: row.firstUserPrompt ?? undefined,
81
+ cwd: row.cwd,
82
+ repoRoots: parseRepoRoots(row.repoRootsJson),
83
+ modifiedAt: row.modifiedAt,
84
+ parentSessionPath: row.parentSessionPath ?? undefined,
85
+ parentSessionId: row.parentSessionId ?? undefined,
86
+ sessionOrigin: row.sessionOrigin ?? undefined,
87
+ handoffGoal: row.handoffGoal ?? undefined,
88
+ handoffNextTask: row.handoffNextTask ?? undefined,
89
+ };
90
+ }
91
+
92
+ function buildRelatedSessionRow(
93
+ row: Static<typeof SESSION_RELATED_QUERY_ROW_SCHEMA>,
94
+ ): SessionRelatedSessionRow {
95
+ return {
96
+ ...buildSessionLineageRow(row),
97
+ relation: row.relation,
98
+ distance: row.distance,
99
+ };
100
+ }
101
+
102
+ export function getSessionById(
103
+ db: SessionIndexDatabase,
104
+ sessionId: string,
105
+ ): SessionLineageRow | undefined {
106
+ const row = db
107
+ .prepare(
108
+ `
109
+ SELECT ${sessionLineageColumns()}
110
+ FROM sessions
111
+ WHERE session_id = ?
112
+ `,
113
+ )
114
+ .get(sessionId);
115
+
116
+ if (row === undefined) {
117
+ return undefined;
118
+ }
119
+
120
+ return buildSessionLineageRow(
121
+ parseTypeBoxValue(
122
+ SESSION_LINEAGE_QUERY_ROW_SCHEMA,
123
+ row,
124
+ `Invalid session row for ${sessionId}`,
125
+ ),
126
+ );
127
+ }
128
+
129
+ export function getSessionByPath(
130
+ db: SessionIndexDatabase,
131
+ sessionPath: string,
132
+ ): SessionLineageRow | undefined {
133
+ const row = db
134
+ .prepare(
135
+ `
136
+ SELECT ${sessionLineageColumns()}
137
+ FROM sessions
138
+ WHERE session_path = ?
139
+ `,
140
+ )
141
+ .get(sessionPath);
142
+
143
+ if (row === undefined) {
144
+ return undefined;
145
+ }
146
+
147
+ return buildSessionLineageRow(
148
+ parseTypeBoxValue(
149
+ SESSION_LINEAGE_QUERY_ROW_SCHEMA,
150
+ row,
151
+ `Invalid session row for path ${sessionPath}`,
152
+ ),
153
+ );
154
+ }
155
+
156
+ export function getLineageSessions(
157
+ db: SessionIndexDatabase,
158
+ sessionId: string,
159
+ ): SessionRelatedSessionRow[] {
160
+ return queryRelatedSessions(db, sessionId);
161
+ }
162
+
163
+ export function getParentSession(
164
+ db: SessionIndexDatabase,
165
+ sessionId: string,
166
+ ): SessionLineageRow | undefined {
167
+ return queryRelatedSessions(db, sessionId, ["parent"])[0];
168
+ }
169
+
170
+ export function getAncestorSessions(
171
+ db: SessionIndexDatabase,
172
+ sessionId: string,
173
+ ): SessionLineageRow[] {
174
+ return queryRelatedSessions(db, sessionId, ["parent", "ancestor"]);
175
+ }
176
+
177
+ export function getChildSessions(db: SessionIndexDatabase, sessionId: string): SessionLineageRow[] {
178
+ return queryRelatedSessions(db, sessionId, ["child"]);
179
+ }
180
+
181
+ export function getSiblingSessions(
182
+ db: SessionIndexDatabase,
183
+ sessionId: string,
184
+ ): SessionLineageRow[] {
185
+ return queryRelatedSessions(db, sessionId, ["sibling"]);
186
+ }
187
+
188
+ export function rebuildSessionLineageRelations(db: SessionIndexDatabase): void {
189
+ db.prepare(`DELETE FROM session_lineage_relations`).run();
190
+
191
+ const rows = parseTypeBoxRows(
192
+ SESSION_GRAPH_ROW_SCHEMA,
193
+ db
194
+ .prepare(
195
+ `
196
+ SELECT
197
+ session_id as sessionId,
198
+ session_path as sessionPath,
199
+ parent_session_path as parentSessionPath,
200
+ parent_session_id as parentSessionId
201
+ FROM sessions
202
+ `,
203
+ )
204
+ .all(),
205
+ "Invalid session graph rows",
206
+ );
207
+
208
+ const pathToId = new Map(rows.map((row) => [row.sessionPath, row.sessionId]));
209
+ const nodes = new Map<string, SessionGraphNode>(
210
+ rows.map((row) => [
211
+ row.sessionId,
212
+ {
213
+ ...row,
214
+ resolvedParentSessionId:
215
+ row.parentSessionId ??
216
+ (row.parentSessionPath ? pathToId.get(row.parentSessionPath) : undefined),
217
+ },
218
+ ]),
219
+ );
220
+ const childrenByParent = new Map<string, string[]>();
221
+
222
+ for (const node of nodes.values()) {
223
+ if (!node.resolvedParentSessionId) {
224
+ continue;
225
+ }
226
+
227
+ const children = childrenByParent.get(node.resolvedParentSessionId) ?? [];
228
+ children.push(node.sessionId);
229
+ childrenByParent.set(node.resolvedParentSessionId, children);
230
+ }
231
+
232
+ const insertRelation = db.prepare(
233
+ `
234
+ INSERT INTO session_lineage_relations(session_id, related_session_id, relation, distance)
235
+ VALUES (?, ?, ?, ?)
236
+ `,
237
+ );
238
+
239
+ for (const node of nodes.values()) {
240
+ const relations = collectMaterializedLineageRows(node.sessionId, nodes, childrenByParent);
241
+ for (const relation of relations.values()) {
242
+ insertRelation.run(
243
+ relation.sessionId,
244
+ relation.relatedSessionId,
245
+ relation.relation,
246
+ relation.distance,
247
+ );
248
+ }
249
+ }
250
+ }
251
+
252
+ function queryRelatedSessions(
253
+ db: SessionIndexDatabase,
254
+ sessionId: string,
255
+ relations?: SessionLineageRelation[],
256
+ ): SessionRelatedSessionRow[] {
257
+ const relationFilter = relations?.length
258
+ ? ` AND r.relation IN (${relations.map(() => "?").join(", ")})`
259
+ : "";
260
+ const rows = parseTypeBoxRows(
261
+ SESSION_RELATED_QUERY_ROW_SCHEMA,
262
+ db
263
+ .prepare(
264
+ `
265
+ SELECT
266
+ ${sessionLineageColumns("s")},
267
+ r.relation as relation,
268
+ r.distance as distance
269
+ FROM session_lineage_relations r
270
+ JOIN sessions s ON s.session_id = r.related_session_id
271
+ WHERE r.session_id = ?${relationFilter}
272
+ ORDER BY
273
+ CASE r.relation
274
+ WHEN 'parent' THEN 1
275
+ WHEN 'child' THEN 2
276
+ WHEN 'sibling' THEN 3
277
+ WHEN 'ancestor' THEN 4
278
+ WHEN 'descendant' THEN 5
279
+ WHEN 'ancestor_sibling' THEN 6
280
+ ELSE 7
281
+ END ASC,
282
+ r.distance ASC,
283
+ s.modified_ts DESC
284
+ `,
285
+ )
286
+ .all(sessionId, ...(relations ?? [])),
287
+ `Invalid related session rows for ${sessionId}`,
288
+ );
289
+
290
+ return rows.map(buildRelatedSessionRow);
291
+ }
292
+
293
+ function collectMaterializedLineageRows(
294
+ sessionId: string,
295
+ nodes: Map<string, SessionGraphNode>,
296
+ childrenByParent: Map<string, string[]>,
297
+ ): Map<string, MaterializedLineageRow> {
298
+ const relations = new Map<string, MaterializedLineageRow>();
299
+ const visitedAncestors = new Set<string>();
300
+ const ancestors: Array<{ sessionId: string; distance: number }> = [];
301
+
302
+ let currentId = nodes.get(sessionId)?.resolvedParentSessionId;
303
+ let distance = 1;
304
+ while (currentId && !visitedAncestors.has(currentId)) {
305
+ visitedAncestors.add(currentId);
306
+ ancestors.push({ sessionId: currentId, distance });
307
+ setMaterializedLineageRow(relations, {
308
+ sessionId,
309
+ relatedSessionId: currentId,
310
+ relation: distance === 1 ? "parent" : "ancestor",
311
+ distance,
312
+ });
313
+ currentId = nodes.get(currentId)?.resolvedParentSessionId;
314
+ distance += 1;
315
+ }
316
+
317
+ const visitedDescendants = new Set<string>();
318
+ const queue = (childrenByParent.get(sessionId) ?? []).map((childId) => ({
319
+ childId,
320
+ distance: 1,
321
+ }));
322
+ while (queue.length > 0) {
323
+ const next = queue.shift();
324
+ if (!next || visitedDescendants.has(next.childId)) {
325
+ continue;
326
+ }
327
+
328
+ visitedDescendants.add(next.childId);
329
+ setMaterializedLineageRow(relations, {
330
+ sessionId,
331
+ relatedSessionId: next.childId,
332
+ relation: next.distance === 1 ? "child" : "descendant",
333
+ distance: next.distance,
334
+ });
335
+
336
+ for (const childId of childrenByParent.get(next.childId) ?? []) {
337
+ queue.push({ childId, distance: next.distance + 1 });
338
+ }
339
+ }
340
+
341
+ const parentId = nodes.get(sessionId)?.resolvedParentSessionId;
342
+ if (parentId) {
343
+ for (const siblingId of childrenByParent.get(parentId) ?? []) {
344
+ if (siblingId === sessionId) {
345
+ continue;
346
+ }
347
+
348
+ setMaterializedLineageRow(relations, {
349
+ sessionId,
350
+ relatedSessionId: siblingId,
351
+ relation: "sibling",
352
+ distance: 1,
353
+ });
354
+ }
355
+ }
356
+
357
+ for (const ancestor of ancestors) {
358
+ const ancestorParentId = nodes.get(ancestor.sessionId)?.resolvedParentSessionId;
359
+ if (!ancestorParentId) {
360
+ continue;
361
+ }
362
+
363
+ for (const siblingId of childrenByParent.get(ancestorParentId) ?? []) {
364
+ if (siblingId === ancestor.sessionId) {
365
+ continue;
366
+ }
367
+
368
+ setMaterializedLineageRow(relations, {
369
+ sessionId,
370
+ relatedSessionId: siblingId,
371
+ relation: "ancestor_sibling",
372
+ distance: ancestor.distance + 1,
373
+ });
374
+ }
375
+ }
376
+
377
+ return relations;
378
+ }
379
+
380
+ function setMaterializedLineageRow(
381
+ rows: Map<string, MaterializedLineageRow>,
382
+ candidate: MaterializedLineageRow,
383
+ ): void {
384
+ const existing = rows.get(candidate.relatedSessionId);
385
+ if (!existing) {
386
+ rows.set(candidate.relatedSessionId, candidate);
387
+ return;
388
+ }
389
+
390
+ const existingPriority = getLineageRelationPriority(existing.relation);
391
+ const candidatePriority = getLineageRelationPriority(candidate.relation);
392
+ if (candidatePriority < existingPriority) {
393
+ rows.set(candidate.relatedSessionId, candidate);
394
+ return;
395
+ }
396
+
397
+ if (candidatePriority === existingPriority && candidate.distance < existing.distance) {
398
+ rows.set(candidate.relatedSessionId, candidate);
399
+ }
400
+ }
401
+
402
+ function getLineageRelationPriority(relation: SessionLineageRelation): number {
403
+ switch (relation) {
404
+ case "parent":
405
+ return 1;
406
+ case "child":
407
+ return 2;
408
+ case "sibling":
409
+ return 3;
410
+ case "ancestor":
411
+ return 4;
412
+ case "descendant":
413
+ return 5;
414
+ case "ancestor_sibling":
415
+ return 6;
416
+ }
417
+ }
@@ -0,0 +1,178 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ import Database from "better-sqlite3";
4
+ import { parseTypeBoxValue } from "../typebox.js";
5
+ import {
6
+ INDEX_SCHEMA_VERSION,
7
+ METADATA_ROW_SCHEMA,
8
+ ROW_COUNT_SCHEMA,
9
+ type SessionIndexDatabase,
10
+ type SessionIndexStatus,
11
+ } from "./common.js";
12
+
13
+ export function ensureIndexDir(dir: string): string {
14
+ if (!existsSync(dir)) {
15
+ mkdirSync(dir, { recursive: true });
16
+ }
17
+ return dir;
18
+ }
19
+
20
+ export function createTempIndexPath(finalPath: string): string {
21
+ const dir = path.dirname(finalPath);
22
+ const baseName = path.basename(finalPath, path.extname(finalPath));
23
+ ensureIndexDir(dir);
24
+ return path.join(dir, `${baseName}.tmp-${process.pid}-${Date.now()}.sqlite`);
25
+ }
26
+
27
+ export function openIndexDatabase(
28
+ dbPath: string,
29
+ options?: { create?: boolean },
30
+ ): SessionIndexDatabase {
31
+ const create = options?.create ?? true;
32
+ const db = new Database(dbPath, { fileMustExist: !create });
33
+ db.pragma("journal_mode = WAL");
34
+ db.pragma("synchronous = NORMAL");
35
+ db.pragma("foreign_keys = ON");
36
+ return db;
37
+ }
38
+
39
+ export function initializeSchema(db: SessionIndexDatabase): void {
40
+ db.exec(`
41
+ CREATE TABLE IF NOT EXISTS metadata (
42
+ key TEXT PRIMARY KEY,
43
+ value TEXT NOT NULL
44
+ );
45
+
46
+ CREATE TABLE IF NOT EXISTS sessions (
47
+ session_id TEXT PRIMARY KEY,
48
+ session_path TEXT NOT NULL,
49
+ session_name TEXT NOT NULL,
50
+ first_user_prompt TEXT,
51
+ cwd TEXT NOT NULL,
52
+ repo_roots_json TEXT NOT NULL,
53
+ created_ts TEXT NOT NULL,
54
+ modified_ts TEXT NOT NULL,
55
+ message_count INTEGER NOT NULL,
56
+ entry_count INTEGER NOT NULL,
57
+ parent_session_path TEXT,
58
+ parent_session_id TEXT,
59
+ session_origin TEXT,
60
+ handoff_goal TEXT,
61
+ handoff_next_task TEXT,
62
+ index_version INTEGER NOT NULL,
63
+ indexed_at_ts TEXT NOT NULL,
64
+ index_source TEXT NOT NULL
65
+ );
66
+
67
+ CREATE INDEX IF NOT EXISTS sessions_modified_idx ON sessions(modified_ts DESC);
68
+ CREATE INDEX IF NOT EXISTS sessions_cwd_idx ON sessions(cwd);
69
+ CREATE INDEX IF NOT EXISTS sessions_path_idx ON sessions(session_path);
70
+ CREATE INDEX IF NOT EXISTS sessions_parent_id_idx ON sessions(parent_session_id);
71
+ CREATE INDEX IF NOT EXISTS sessions_parent_path_idx ON sessions(parent_session_path);
72
+
73
+ CREATE TABLE IF NOT EXISTS session_lineage_relations (
74
+ session_id TEXT NOT NULL,
75
+ related_session_id TEXT NOT NULL,
76
+ relation TEXT NOT NULL,
77
+ distance INTEGER NOT NULL,
78
+ PRIMARY KEY (session_id, related_session_id),
79
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
80
+ FOREIGN KEY (related_session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
81
+ );
82
+
83
+ CREATE INDEX IF NOT EXISTS session_lineage_relations_related_idx
84
+ ON session_lineage_relations(related_session_id);
85
+ CREATE INDEX IF NOT EXISTS session_lineage_relations_relation_idx
86
+ ON session_lineage_relations(session_id, relation, distance);
87
+
88
+ CREATE TABLE IF NOT EXISTS session_text_chunks (
89
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
90
+ session_id TEXT NOT NULL,
91
+ entry_id TEXT,
92
+ entry_type TEXT NOT NULL,
93
+ role TEXT,
94
+ ts TEXT NOT NULL,
95
+ source_kind TEXT NOT NULL,
96
+ text TEXT NOT NULL,
97
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
98
+ );
99
+
100
+ CREATE INDEX IF NOT EXISTS session_text_chunks_session_idx ON session_text_chunks(session_id);
101
+ CREATE INDEX IF NOT EXISTS session_text_chunks_ts_idx ON session_text_chunks(ts DESC);
102
+
103
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_text_chunks_fts USING fts5(
104
+ chunk_id UNINDEXED,
105
+ session_id UNINDEXED,
106
+ text
107
+ );
108
+
109
+ CREATE TABLE IF NOT EXISTS session_file_touches (
110
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
111
+ session_id TEXT NOT NULL,
112
+ entry_id TEXT,
113
+ op TEXT NOT NULL,
114
+ source TEXT NOT NULL,
115
+ raw_path TEXT NOT NULL,
116
+ abs_path TEXT,
117
+ cwd_rel_path TEXT,
118
+ repo_root TEXT,
119
+ repo_rel_path TEXT,
120
+ basename TEXT NOT NULL,
121
+ path_scope TEXT NOT NULL,
122
+ ts TEXT NOT NULL,
123
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
124
+ );
125
+
126
+ CREATE INDEX IF NOT EXISTS session_file_touches_session_idx ON session_file_touches(session_id);
127
+ CREATE INDEX IF NOT EXISTS session_file_touches_op_idx ON session_file_touches(op);
128
+ CREATE INDEX IF NOT EXISTS session_file_touches_abs_idx ON session_file_touches(abs_path);
129
+ CREATE INDEX IF NOT EXISTS session_file_touches_repo_root_idx ON session_file_touches(repo_root);
130
+ CREATE INDEX IF NOT EXISTS session_file_touches_repo_rel_idx ON session_file_touches(repo_rel_path);
131
+ CREATE INDEX IF NOT EXISTS session_file_touches_cwd_rel_idx ON session_file_touches(cwd_rel_path);
132
+ CREATE INDEX IF NOT EXISTS session_file_touches_basename_idx ON session_file_touches(basename);
133
+ `);
134
+
135
+ setMetadata(db, "schema_version", String(INDEX_SCHEMA_VERSION));
136
+ }
137
+
138
+ export function setMetadata(db: SessionIndexDatabase, key: string, value: string): void {
139
+ db.prepare(
140
+ `INSERT INTO metadata(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
141
+ ).run(key, value);
142
+ }
143
+
144
+ export function getMetadata(db: SessionIndexDatabase, key: string): string | undefined {
145
+ const row = db.prepare(`SELECT value FROM metadata WHERE key = ?`).get(key);
146
+ if (row === undefined) {
147
+ return undefined;
148
+ }
149
+
150
+ return parseTypeBoxValue(METADATA_ROW_SCHEMA, row, `Invalid metadata row for key ${key}`).value;
151
+ }
152
+
153
+ export function getIndexStatus(dbPath: string): SessionIndexStatus {
154
+ if (!existsSync(dbPath)) {
155
+ return { dbPath, exists: false };
156
+ }
157
+
158
+ const db = openIndexDatabase(dbPath, { create: false });
159
+ try {
160
+ const schemaVersionRaw = getMetadata(db, "schema_version");
161
+ const lastFullReindexAt = getMetadata(db, "indexed_at");
162
+ const sessionCountRow = parseTypeBoxValue(
163
+ ROW_COUNT_SCHEMA,
164
+ db.prepare(`SELECT COUNT(*) as count FROM sessions`).get(),
165
+ "Invalid session count row",
166
+ );
167
+
168
+ return {
169
+ dbPath,
170
+ exists: true,
171
+ schemaVersion: schemaVersionRaw ? Number(schemaVersionRaw) : undefined,
172
+ sessionCount: sessionCountRow.count,
173
+ lastFullReindexAt,
174
+ };
175
+ } finally {
176
+ db.close();
177
+ }
178
+ }