@omercnet/paseo-context-mode 3.1.3 → 3.1.4-next.434.1

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.
@@ -1,481 +0,0 @@
1
- import { lstatSync, type Stats } from "node:fs";
2
- import { basename } from "node:path";
3
- import { DatabaseSync } from "node:sqlite";
4
-
5
- const BUSY_TIMEOUT_MS = 150;
6
- const MAX_DATABASE_BYTES = 512 * 1024 * 1024;
7
- const MAX_DISTINCT_IDENTITIES = 5_000;
8
- const MAX_GROUP_ROWS = 4_096;
9
- const MAX_SOURCE_ROWS = 2_000;
10
-
11
- const REQUIRED_SESSION_SCHEMA: Record<string, readonly string[]> = {
12
- session_events: [
13
- "session_id",
14
- "category",
15
- "data",
16
- "project_dir",
17
- "bytes_avoided",
18
- "bytes_returned",
19
- "created_at",
20
- ],
21
- session_meta: ["session_id", "project_dir"],
22
- session_resume: ["session_id", "snapshot", "created_at"],
23
- tool_calls: ["session_id", "tool", "calls", "bytes_returned"],
24
- };
25
-
26
- const REQUIRED_CONTENT_SCHEMA: Record<string, readonly string[]> = {
27
- sources: ["id", "label", "chunk_count", "code_chunk_count", "indexed_at"],
28
- chunks: ["title", "content", "source_id", "content_type"],
29
- };
30
-
31
- type DatabaseRow = Record<string, unknown>;
32
-
33
- export type ContextModeDatabaseErrorCode =
34
- | "not-regular-database"
35
- | "database-open-failed"
36
- | "unsupported-schema"
37
- | "database-query-failed";
38
-
39
- export class ContextModeDatabaseError extends Error {
40
- readonly code: ContextModeDatabaseErrorCode;
41
- readonly database: string;
42
-
43
- constructor(code: ContextModeDatabaseErrorCode, path: string, message: string, cause?: unknown) {
44
- super(message, { cause });
45
- this.name = "ContextModeDatabaseError";
46
- this.code = code;
47
- this.database = basename(path);
48
- }
49
- }
50
-
51
- export interface DatabaseCategorySummary {
52
- category: string;
53
- count: number;
54
- savedBytes: number;
55
- }
56
-
57
- export interface DatabaseDaySummary {
58
- date: string;
59
- calls: number;
60
- savedBytes: number;
61
- }
62
-
63
- export interface SessionDatabaseSnapshot {
64
- kind: "session";
65
- sessions: string[];
66
- projects: string[];
67
- events: number;
68
- toolCalls: number;
69
- eventDataBytes: number;
70
- avoidedBytes: number;
71
- returnedBytes: number;
72
- snapshotBytes: number;
73
- byCategory: DatabaseCategorySummary[];
74
- byDay: DatabaseDaySummary[];
75
- }
76
-
77
- export interface ContentSourceSnapshot {
78
- source: string;
79
- chunks: number;
80
- codeChunks: number;
81
- indexedBytes: number;
82
- indexedAt: string | null;
83
- detail?: string;
84
- }
85
-
86
- export interface ContentDatabaseSnapshot {
87
- kind: "content";
88
- indexedBytes: number;
89
- chunks: number;
90
- codeChunks: number;
91
- sources: ContentSourceSnapshot[];
92
- byDay: DatabaseDaySummary[];
93
- inconsistencies: string[];
94
- }
95
-
96
- export type ContextModeDatabaseSnapshot = SessionDatabaseSnapshot | ContentDatabaseSnapshot;
97
-
98
- function nonNegativeInteger(value: unknown): number {
99
- const numeric = typeof value === "bigint" ? Number(value) : Number(value ?? 0);
100
- if (!Number.isFinite(numeric) || numeric <= 0) return 0;
101
- return Math.min(Number.MAX_SAFE_INTEGER, Math.floor(numeric));
102
- }
103
-
104
- function boundedAdd(left: number, right: number): number {
105
- return Math.min(Number.MAX_SAFE_INTEGER, left + right);
106
- }
107
-
108
- function boundedText(value: unknown, fallback: string, maximum: number): string {
109
- if (typeof value !== "string") return fallback;
110
- const trimmed = value.trim();
111
- if (!trimmed) return fallback;
112
- return trimmed.length <= maximum ? trimmed : trimmed.slice(0, maximum);
113
- }
114
-
115
- function normalizedTimestamp(value: unknown): string | null {
116
- if (typeof value !== "string" || value.trim().length === 0) return null;
117
- const input = value.trim().replace(" ", "T");
118
- const zoned = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(input) ? input : `${input}Z`;
119
- const milliseconds = Date.parse(zoned);
120
- return Number.isFinite(milliseconds) ? new Date(milliseconds).toISOString() : null;
121
- }
122
-
123
- function columnsFor(database: DatabaseSync, table: string): Set<string> {
124
- const rows = database.prepare(`PRAGMA table_xinfo('${table}')`).all() as DatabaseRow[];
125
- return new Set(
126
- rows.flatMap((row) => (typeof row.name === "string" && row.name ? [row.name] : [])),
127
- );
128
- }
129
-
130
- function assertRequiredSchema(
131
- database: DatabaseSync,
132
- path: string,
133
- expected: Record<string, readonly string[]>,
134
- ): void {
135
- for (const [table, requiredColumns] of Object.entries(expected)) {
136
- const columns = columnsFor(database, table);
137
- if (columns.size === 0) {
138
- throw new ContextModeDatabaseError(
139
- "unsupported-schema",
140
- path,
141
- `Unsupported Context Mode schema: missing ${table}.`,
142
- );
143
- }
144
- const missing = requiredColumns.filter((column) => !columns.has(column));
145
- if (missing.length > 0) {
146
- throw new ContextModeDatabaseError(
147
- "unsupported-schema",
148
- path,
149
- `Unsupported Context Mode schema: ${table} is missing ${missing.join(", ")}.`,
150
- );
151
- }
152
- }
153
- }
154
-
155
- function incrementSummary(
156
- summaries: Map<string, { count: number; savedBytes: number }>,
157
- key: string,
158
- count: number,
159
- savedBytes: number,
160
- ): void {
161
- const current = summaries.get(key) ?? { count: 0, savedBytes: 0 };
162
- current.count = boundedAdd(current.count, count);
163
- current.savedBytes = boundedAdd(current.savedBytes, savedBytes);
164
- summaries.set(key, current);
165
- }
166
-
167
- function readSessionDatabase(database: DatabaseSync, path: string): SessionDatabaseSnapshot {
168
- assertRequiredSchema(database, path, REQUIRED_SESSION_SCHEMA);
169
- const sessions = new Set<string>();
170
- const projects = new Set<string>();
171
-
172
- for (const row of database
173
- .prepare("SELECT DISTINCT session_id, project_dir FROM session_events LIMIT ?")
174
- .all(MAX_DISTINCT_IDENTITIES) as DatabaseRow[]) {
175
- const session = boundedText(row.session_id, "", 4_096);
176
- const project = boundedText(row.project_dir, "", 4_096);
177
- if (session) sessions.add(session);
178
- if (project) projects.add(project);
179
- }
180
- for (const row of database
181
- .prepare("SELECT session_id, project_dir FROM session_meta LIMIT ?")
182
- .all(MAX_DISTINCT_IDENTITIES) as DatabaseRow[]) {
183
- const session = boundedText(row.session_id, "", 4_096);
184
- const project = boundedText(row.project_dir, "", 4_096);
185
- if (session) sessions.add(session);
186
- if (project) projects.add(project);
187
- }
188
-
189
- const totals = database
190
- .prepare(`SELECT
191
- COUNT(*) AS events,
192
- COALESCE(SUM(length(CAST(data AS BLOB))), 0) AS data_bytes,
193
- COALESCE(SUM(bytes_avoided), 0) AS avoided_bytes,
194
- COALESCE(SUM(bytes_returned), 0) AS returned_bytes
195
- FROM session_events`)
196
- .get() as DatabaseRow;
197
- const resumeTotals = database
198
- .prepare(
199
- "SELECT COALESCE(SUM(length(CAST(snapshot AS BLOB))), 0) AS snapshot_bytes FROM session_resume",
200
- )
201
- .get() as DatabaseRow;
202
- const toolTotals = database
203
- .prepare("SELECT COALESCE(SUM(calls), 0) AS calls FROM tool_calls")
204
- .get() as DatabaseRow;
205
- const searchReturns = database
206
- .prepare(
207
- "SELECT COALESCE(SUM(bytes_returned), 0) AS bytes_returned FROM tool_calls WHERE tool IN ('ctx_search', 'ctx_fetch_and_index')",
208
- )
209
- .get() as DatabaseRow;
210
-
211
- const byCategory = (
212
- database
213
- .prepare(`SELECT
214
- category,
215
- COUNT(*) AS count,
216
- COALESCE(SUM(length(CAST(data AS BLOB)) + bytes_avoided), 0) AS saved_bytes
217
- FROM session_events
218
- GROUP BY category
219
- ORDER BY count DESC
220
- LIMIT ?`)
221
- .all(MAX_GROUP_ROWS) as DatabaseRow[]
222
- ).map((row) => ({
223
- category: boundedText(row.category, "unknown", 256),
224
- count: nonNegativeInteger(row.count),
225
- savedBytes: nonNegativeInteger(row.saved_bytes),
226
- }));
227
-
228
- const daySummaries = new Map<string, { count: number; savedBytes: number }>();
229
- for (const row of database
230
- .prepare(`SELECT
231
- date(created_at) AS day,
232
- COUNT(*) AS calls,
233
- COALESCE(SUM(length(CAST(data AS BLOB)) + bytes_avoided), 0) AS saved_bytes
234
- FROM session_events
235
- WHERE created_at IS NOT NULL
236
- GROUP BY date(created_at)
237
- ORDER BY day DESC
238
- LIMIT ?`)
239
- .all(MAX_GROUP_ROWS) as DatabaseRow[]) {
240
- const day = boundedText(row.day, "", 10);
241
- if (day)
242
- incrementSummary(
243
- daySummaries,
244
- day,
245
- nonNegativeInteger(row.calls),
246
- nonNegativeInteger(row.saved_bytes),
247
- );
248
- }
249
- for (const row of database
250
- .prepare(`SELECT
251
- date(created_at) AS day,
252
- COALESCE(SUM(length(CAST(snapshot AS BLOB))), 0) AS saved_bytes
253
- FROM session_resume
254
- WHERE created_at IS NOT NULL
255
- GROUP BY date(created_at)
256
- ORDER BY day DESC
257
- LIMIT ?`)
258
- .all(MAX_GROUP_ROWS) as DatabaseRow[]) {
259
- const day = boundedText(row.day, "", 10);
260
- if (day) incrementSummary(daySummaries, day, 0, nonNegativeInteger(row.saved_bytes));
261
- }
262
-
263
- return {
264
- kind: "session",
265
- sessions: [...sessions],
266
- projects: [...projects],
267
- events: nonNegativeInteger(totals.events),
268
- toolCalls: nonNegativeInteger(toolTotals.calls),
269
- eventDataBytes: nonNegativeInteger(totals.data_bytes),
270
- avoidedBytes: nonNegativeInteger(totals.avoided_bytes),
271
- returnedBytes: boundedAdd(
272
- nonNegativeInteger(totals.returned_bytes),
273
- nonNegativeInteger(searchReturns.bytes_returned),
274
- ),
275
- snapshotBytes: nonNegativeInteger(resumeTotals.snapshot_bytes),
276
- byCategory,
277
- byDay: [...daySummaries].map(([date, summary]) => ({
278
- date,
279
- calls: summary.count,
280
- savedBytes: summary.savedBytes,
281
- })),
282
- };
283
- }
284
-
285
- function readContentDatabase(database: DatabaseSync, path: string): ContentDatabaseSnapshot {
286
- assertRequiredSchema(database, path, REQUIRED_CONTENT_SCHEMA);
287
- const totals = database
288
- .prepare(`SELECT
289
- COUNT(*) AS chunks,
290
- COALESCE(SUM(CASE WHEN content_type = 'code' THEN 1 ELSE 0 END), 0) AS code_chunks,
291
- COALESCE(SUM(length(CAST(title AS BLOB)) + length(CAST(content AS BLOB))), 0) AS indexed_bytes
292
- FROM chunks`)
293
- .get() as DatabaseRow;
294
- const chunksBySource = new Map<
295
- string,
296
- { chunks: number; codeChunks: number; indexedBytes: number }
297
- >();
298
- for (const row of database
299
- .prepare(`SELECT
300
- source_id,
301
- COUNT(*) AS chunks,
302
- COALESCE(SUM(CASE WHEN content_type = 'code' THEN 1 ELSE 0 END), 0) AS code_chunks,
303
- COALESCE(SUM(length(CAST(title AS BLOB)) + length(CAST(content AS BLOB))), 0) AS indexed_bytes
304
- FROM chunks
305
- GROUP BY source_id
306
- LIMIT ?`)
307
- .all(MAX_SOURCE_ROWS + 1) as DatabaseRow[]) {
308
- const sourceId = String(row.source_id ?? "");
309
- chunksBySource.set(sourceId, {
310
- chunks: nonNegativeInteger(row.chunks),
311
- codeChunks: nonNegativeInteger(row.code_chunks),
312
- indexedBytes: nonNegativeInteger(row.indexed_bytes),
313
- });
314
- }
315
-
316
- const sources: ContentSourceSnapshot[] = [];
317
- const inconsistencies: string[] = [];
318
- const days = new Map<string, { count: number; savedBytes: number }>();
319
- const sourceRows = database
320
- .prepare(
321
- "SELECT id, label, chunk_count, code_chunk_count, indexed_at FROM sources ORDER BY id LIMIT ?",
322
- )
323
- .all(MAX_SOURCE_ROWS + 1) as DatabaseRow[];
324
- if (sourceRows.length > MAX_SOURCE_ROWS || chunksBySource.size > MAX_SOURCE_ROWS) {
325
- inconsistencies.push(`Source analytics were limited to ${MAX_SOURCE_ROWS} rows.`);
326
- }
327
- for (const row of sourceRows.slice(0, MAX_SOURCE_ROWS)) {
328
- const sourceId = String(row.id ?? "");
329
- const actual = chunksBySource.get(sourceId) ?? {
330
- chunks: 0,
331
- codeChunks: 0,
332
- indexedBytes: 0,
333
- };
334
- chunksBySource.delete(sourceId);
335
- const storedChunks = nonNegativeInteger(row.chunk_count);
336
- const storedCodeChunks = nonNegativeInteger(row.code_chunk_count);
337
- const source = boundedText(row.label, "(unnamed source)", 4_096);
338
- const indexedAt = normalizedTimestamp(row.indexed_at);
339
- let detail: string | undefined;
340
- if (storedChunks !== actual.chunks || storedCodeChunks !== actual.codeChunks) {
341
- detail = `Stored counts ${storedChunks}/${storedCodeChunks} differ from readable chunks ${actual.chunks}/${actual.codeChunks}.`;
342
- if (inconsistencies.length < 10) inconsistencies.push(`${source}: ${detail}`);
343
- }
344
- sources.push({
345
- source,
346
- chunks: actual.chunks,
347
- codeChunks: actual.codeChunks,
348
- indexedBytes: actual.indexedBytes,
349
- indexedAt,
350
- ...(detail ? { detail } : {}),
351
- });
352
- const day = indexedAt?.slice(0, 10);
353
- if (day) incrementSummary(days, day, 0, actual.indexedBytes);
354
- }
355
-
356
- if (chunksBySource.size > 0 && inconsistencies.length < 10) {
357
- const orphanChunks = [...chunksBySource.values()].reduce(
358
- (total, value) => boundedAdd(total, value.chunks),
359
- 0,
360
- );
361
- inconsistencies.push(`${orphanChunks} chunks reference a missing or unscanned source row.`);
362
- }
363
-
364
- return {
365
- kind: "content",
366
- indexedBytes: nonNegativeInteger(totals.indexed_bytes),
367
- chunks: nonNegativeInteger(totals.chunks),
368
- codeChunks: nonNegativeInteger(totals.code_chunks),
369
- sources,
370
- byDay: [...days].map(([date, summary]) => ({
371
- date,
372
- calls: summary.count,
373
- savedBytes: summary.savedBytes,
374
- })),
375
- inconsistencies,
376
- };
377
- }
378
-
379
- function classifyDatabaseError(error: unknown): ContextModeDatabaseErrorCode {
380
- const message =
381
- error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
382
- if (
383
- message.includes("not a database") ||
384
- message.includes("database disk image is malformed") ||
385
- message.includes("database is locked") ||
386
- message.includes("database is busy") ||
387
- message.includes("unable to open database")
388
- ) {
389
- return "database-open-failed";
390
- }
391
- return "database-query-failed";
392
- }
393
-
394
- export function readContextModeDatabase(path: string): ContextModeDatabaseSnapshot {
395
- let info: Stats;
396
- try {
397
- info = lstatSync(path);
398
- } catch (error) {
399
- throw new ContextModeDatabaseError(
400
- "not-regular-database",
401
- path,
402
- "The database file could not be inspected.",
403
- error,
404
- );
405
- }
406
- if (!path.endsWith(".db") || !info.isFile()) {
407
- throw new ContextModeDatabaseError(
408
- "not-regular-database",
409
- path,
410
- "Only regular .db files can be inspected.",
411
- );
412
- }
413
- if (info.size > MAX_DATABASE_BYTES) {
414
- throw new ContextModeDatabaseError(
415
- "database-open-failed",
416
- path,
417
- `The database exceeds the ${MAX_DATABASE_BYTES}-byte analytics limit.`,
418
- );
419
- }
420
-
421
- let database: DatabaseSync;
422
- try {
423
- database = new DatabaseSync(path, {
424
- readOnly: true,
425
- allowExtension: false,
426
- timeout: BUSY_TIMEOUT_MS,
427
- });
428
- } catch (error) {
429
- throw new ContextModeDatabaseError(
430
- "database-open-failed",
431
- path,
432
- "The database could not be opened read-only.",
433
- error,
434
- );
435
- }
436
-
437
- try {
438
- database.exec("PRAGMA query_only = ON");
439
- const versionRow = database.prepare("PRAGMA user_version").get() as DatabaseRow | undefined;
440
- const userVersion = nonNegativeInteger(versionRow?.user_version);
441
- if (userVersion !== 0) {
442
- throw new ContextModeDatabaseError(
443
- "unsupported-schema",
444
- path,
445
- `Unsupported Context Mode schema version ${userVersion}; expected the 1.0.169 layout.`,
446
- );
447
- }
448
- const tableRows = database
449
- .prepare("SELECT name FROM sqlite_schema WHERE type = 'table'")
450
- .all() as DatabaseRow[];
451
- const tables = new Set(
452
- tableRows.flatMap((row) => (typeof row.name === "string" ? [row.name] : [])),
453
- );
454
- const hasSessionSchema = tables.has("session_events");
455
- const hasContentSchema = tables.has("sources") || tables.has("chunks");
456
- if (hasSessionSchema && hasContentSchema) {
457
- throw new ContextModeDatabaseError(
458
- "unsupported-schema",
459
- path,
460
- "Unsupported Context Mode schema: session and content tables are mixed.",
461
- );
462
- }
463
- if (hasSessionSchema) return readSessionDatabase(database, path);
464
- if (hasContentSchema) return readContentDatabase(database, path);
465
- throw new ContextModeDatabaseError(
466
- "unsupported-schema",
467
- path,
468
- "Unsupported Context Mode schema: no recognized Context Mode tables.",
469
- );
470
- } catch (error) {
471
- if (error instanceof ContextModeDatabaseError) throw error;
472
- throw new ContextModeDatabaseError(
473
- classifyDatabaseError(error),
474
- path,
475
- "The database could not be queried safely.",
476
- error,
477
- );
478
- } finally {
479
- database.close();
480
- }
481
- }
package/shared/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from "./actions";
2
- export * from "./analytics";
3
- export * from "./contracts";
4
- export * from "./knowledge";
5
- export * from "./settings";