xtctx 0.3.2 → 0.11.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.
Files changed (300) hide show
  1. package/CHANGELOG.md +267 -122
  2. package/LICENSE +21 -21
  3. package/README.md +154 -121
  4. package/dist/src/cli/disconnect.d.ts +8 -0
  5. package/dist/src/cli/disconnect.d.ts.map +1 -0
  6. package/dist/src/cli/disconnect.js +42 -0
  7. package/dist/src/cli/disconnect.js.map +1 -0
  8. package/dist/src/cli/hook.d.ts +7 -0
  9. package/dist/src/cli/hook.d.ts.map +1 -0
  10. package/dist/src/cli/hook.js +21 -0
  11. package/dist/src/cli/hook.js.map +1 -0
  12. package/dist/src/cli/index.d.ts.map +1 -1
  13. package/dist/src/cli/index.js +71 -33
  14. package/dist/src/cli/index.js.map +1 -1
  15. package/dist/src/cli/setup.d.ts +8 -0
  16. package/dist/src/cli/setup.d.ts.map +1 -0
  17. package/dist/src/cli/setup.js +77 -0
  18. package/dist/src/cli/setup.js.map +1 -0
  19. package/dist/src/cli/status.d.ts +7 -0
  20. package/dist/src/cli/status.d.ts.map +1 -0
  21. package/dist/src/cli/status.js +84 -0
  22. package/dist/src/cli/status.js.map +1 -0
  23. package/dist/src/config/disconnect.d.ts +32 -0
  24. package/dist/src/config/disconnect.d.ts.map +1 -0
  25. package/dist/src/config/disconnect.js +304 -0
  26. package/dist/src/config/disconnect.js.map +1 -0
  27. package/dist/src/config/mcp-config.d.ts +91 -0
  28. package/dist/src/config/mcp-config.d.ts.map +1 -0
  29. package/dist/src/config/mcp-config.js +462 -0
  30. package/dist/src/config/mcp-config.js.map +1 -0
  31. package/dist/src/config/setup.d.ts +37 -0
  32. package/dist/src/config/setup.d.ts.map +1 -0
  33. package/dist/src/config/setup.js +338 -0
  34. package/dist/src/config/setup.js.map +1 -0
  35. package/dist/src/config/skills.d.ts +73 -0
  36. package/dist/src/config/skills.d.ts.map +1 -0
  37. package/dist/src/config/skills.js +427 -0
  38. package/dist/src/config/skills.js.map +1 -0
  39. package/dist/src/handoff/embeddings.d.ts +15 -0
  40. package/dist/src/handoff/embeddings.d.ts.map +1 -0
  41. package/dist/src/handoff/embeddings.js +63 -0
  42. package/dist/src/handoff/embeddings.js.map +1 -0
  43. package/dist/src/handoff/sqlite-index.d.ts +43 -0
  44. package/dist/src/handoff/sqlite-index.d.ts.map +1 -0
  45. package/dist/src/handoff/sqlite-index.js +718 -0
  46. package/dist/src/handoff/sqlite-index.js.map +1 -0
  47. package/dist/src/handoff/types.d.ts +57 -0
  48. package/dist/src/handoff/types.d.ts.map +1 -0
  49. package/dist/src/handoff/types.js +2 -0
  50. package/dist/src/handoff/types.js.map +1 -0
  51. package/dist/src/handoff/vector.d.ts +4 -0
  52. package/dist/src/handoff/vector.d.ts.map +1 -0
  53. package/dist/src/handoff/vector.js +31 -0
  54. package/dist/src/handoff/vector.js.map +1 -0
  55. package/dist/src/mcp/server.d.ts +2 -19
  56. package/dist/src/mcp/server.d.ts.map +1 -1
  57. package/dist/src/mcp/server.js +75 -199
  58. package/dist/src/mcp/server.js.map +1 -1
  59. package/dist/src/mcp/tools/continuity.d.ts +3 -0
  60. package/dist/src/mcp/tools/continuity.d.ts.map +1 -0
  61. package/dist/src/mcp/tools/continuity.js +30 -0
  62. package/dist/src/mcp/tools/continuity.js.map +1 -0
  63. package/dist/src/mcp/tools/manifest.d.ts +40 -0
  64. package/dist/src/mcp/tools/manifest.d.ts.map +1 -0
  65. package/dist/src/mcp/tools/manifest.js +96 -0
  66. package/dist/src/mcp/tools/manifest.js.map +1 -0
  67. package/dist/src/mcp/tools/sessions.d.ts +11 -32
  68. package/dist/src/mcp/tools/sessions.d.ts.map +1 -1
  69. package/dist/src/mcp/tools/sessions.js +47 -13
  70. package/dist/src/mcp/tools/sessions.js.map +1 -1
  71. package/dist/src/runtime/services.d.ts +11 -38
  72. package/dist/src/runtime/services.d.ts.map +1 -1
  73. package/dist/src/runtime/services.js +43 -202
  74. package/dist/src/runtime/services.js.map +1 -1
  75. package/dist/src/scrapers/antigravity.d.ts +41 -0
  76. package/dist/src/scrapers/antigravity.d.ts.map +1 -0
  77. package/dist/src/scrapers/antigravity.js +858 -0
  78. package/dist/src/scrapers/antigravity.js.map +1 -0
  79. package/dist/src/scrapers/base.d.ts +53 -1
  80. package/dist/src/scrapers/base.d.ts.map +1 -1
  81. package/dist/src/scrapers/base.js +89 -3
  82. package/dist/src/scrapers/base.js.map +1 -1
  83. package/dist/src/scrapers/claude-code.d.ts +20 -6
  84. package/dist/src/scrapers/claude-code.d.ts.map +1 -1
  85. package/dist/src/scrapers/claude-code.js +144 -26
  86. package/dist/src/scrapers/claude-code.js.map +1 -1
  87. package/dist/src/scrapers/codex.d.ts +36 -6
  88. package/dist/src/scrapers/codex.d.ts.map +1 -1
  89. package/dist/src/scrapers/codex.js +256 -46
  90. package/dist/src/scrapers/codex.js.map +1 -1
  91. package/dist/src/scrapers/copilot-cli.d.ts +33 -0
  92. package/dist/src/scrapers/copilot-cli.d.ts.map +1 -0
  93. package/dist/src/scrapers/copilot-cli.js +257 -0
  94. package/dist/src/scrapers/copilot-cli.js.map +1 -0
  95. package/dist/src/scrapers/copilot.d.ts +41 -8
  96. package/dist/src/scrapers/copilot.d.ts.map +1 -1
  97. package/dist/src/scrapers/copilot.js +256 -118
  98. package/dist/src/scrapers/copilot.js.map +1 -1
  99. package/dist/src/scrapers/cursor.d.ts +24 -9
  100. package/dist/src/scrapers/cursor.d.ts.map +1 -1
  101. package/dist/src/scrapers/cursor.js +247 -151
  102. package/dist/src/scrapers/cursor.js.map +1 -1
  103. package/dist/src/scrapers/index.d.ts +18 -0
  104. package/dist/src/scrapers/index.d.ts.map +1 -0
  105. package/dist/src/scrapers/index.js +17 -0
  106. package/dist/src/scrapers/index.js.map +1 -0
  107. package/dist/src/scrapers/opencode.d.ts +36 -0
  108. package/dist/src/scrapers/opencode.d.ts.map +1 -0
  109. package/dist/src/scrapers/opencode.js +248 -0
  110. package/dist/src/scrapers/opencode.js.map +1 -0
  111. package/dist/src/scrapers/registry.d.ts +1 -0
  112. package/dist/src/scrapers/registry.d.ts.map +1 -1
  113. package/dist/src/scrapers/registry.js +3 -0
  114. package/dist/src/scrapers/registry.js.map +1 -1
  115. package/dist/src/tools/sources.d.ts +28 -0
  116. package/dist/src/tools/sources.d.ts.map +1 -0
  117. package/dist/src/tools/sources.js +149 -0
  118. package/dist/src/tools/sources.js.map +1 -0
  119. package/dist/src/types/config.d.ts +24 -37
  120. package/dist/src/types/config.d.ts.map +1 -1
  121. package/dist/src/types/index.d.ts +0 -2
  122. package/dist/src/types/index.d.ts.map +1 -1
  123. package/dist/src/types/index.js +0 -2
  124. package/dist/src/types/index.js.map +1 -1
  125. package/dist/src/types/scraper.d.ts +34 -5
  126. package/dist/src/types/scraper.d.ts.map +1 -1
  127. package/dist/src/utils/errors.d.ts +2 -0
  128. package/dist/src/utils/errors.d.ts.map +1 -0
  129. package/dist/src/utils/errors.js +7 -0
  130. package/dist/src/utils/errors.js.map +1 -0
  131. package/dist/src/utils/package-info.d.ts +11 -0
  132. package/dist/src/utils/package-info.d.ts.map +1 -0
  133. package/dist/src/utils/package-info.js +30 -0
  134. package/dist/src/utils/package-info.js.map +1 -0
  135. package/dist/src/utils/project-scope.d.ts +3 -0
  136. package/dist/src/utils/project-scope.d.ts.map +1 -0
  137. package/dist/src/utils/project-scope.js +45 -0
  138. package/dist/src/utils/project-scope.js.map +1 -0
  139. package/package.json +70 -66
  140. package/dist/src/api/routes/config.d.ts +0 -4
  141. package/dist/src/api/routes/config.d.ts.map +0 -1
  142. package/dist/src/api/routes/config.js +0 -61
  143. package/dist/src/api/routes/config.js.map +0 -1
  144. package/dist/src/api/routes/knowledge.d.ts +0 -4
  145. package/dist/src/api/routes/knowledge.d.ts.map +0 -1
  146. package/dist/src/api/routes/knowledge.js +0 -40
  147. package/dist/src/api/routes/knowledge.js.map +0 -1
  148. package/dist/src/api/routes/search.d.ts +0 -7
  149. package/dist/src/api/routes/search.d.ts.map +0 -1
  150. package/dist/src/api/routes/search.js +0 -108
  151. package/dist/src/api/routes/search.js.map +0 -1
  152. package/dist/src/api/routes/sources.d.ts +0 -12
  153. package/dist/src/api/routes/sources.d.ts.map +0 -1
  154. package/dist/src/api/routes/sources.js +0 -70
  155. package/dist/src/api/routes/sources.js.map +0 -1
  156. package/dist/src/api/server.d.ts +0 -26
  157. package/dist/src/api/server.d.ts.map +0 -1
  158. package/dist/src/api/server.js +0 -312
  159. package/dist/src/api/server.js.map +0 -1
  160. package/dist/src/cli/ingest.d.ts +0 -6
  161. package/dist/src/cli/ingest.d.ts.map +0 -1
  162. package/dist/src/cli/ingest.js +0 -11
  163. package/dist/src/cli/ingest.js.map +0 -1
  164. package/dist/src/cli/init.d.ts +0 -6
  165. package/dist/src/cli/init.d.ts.map +0 -1
  166. package/dist/src/cli/init.js +0 -105
  167. package/dist/src/cli/init.js.map +0 -1
  168. package/dist/src/cli/serve.d.ts +0 -7
  169. package/dist/src/cli/serve.d.ts.map +0 -1
  170. package/dist/src/cli/serve.js +0 -100
  171. package/dist/src/cli/serve.js.map +0 -1
  172. package/dist/src/cli/sync.d.ts +0 -5
  173. package/dist/src/cli/sync.d.ts.map +0 -1
  174. package/dist/src/cli/sync.js +0 -13
  175. package/dist/src/cli/sync.js.map +0 -1
  176. package/dist/src/compaction/pipeline.d.ts +0 -19
  177. package/dist/src/compaction/pipeline.d.ts.map +0 -1
  178. package/dist/src/compaction/pipeline.js +0 -37
  179. package/dist/src/compaction/pipeline.js.map +0 -1
  180. package/dist/src/compaction/providers/cli.d.ts +0 -13
  181. package/dist/src/compaction/providers/cli.d.ts.map +0 -1
  182. package/dist/src/compaction/providers/cli.js +0 -60
  183. package/dist/src/compaction/providers/cli.js.map +0 -1
  184. package/dist/src/compaction/rule-based.d.ts +0 -8
  185. package/dist/src/compaction/rule-based.d.ts.map +0 -1
  186. package/dist/src/compaction/rule-based.js +0 -120
  187. package/dist/src/compaction/rule-based.js.map +0 -1
  188. package/dist/src/config/loader.d.ts +0 -12
  189. package/dist/src/config/loader.d.ts.map +0 -1
  190. package/dist/src/config/loader.js +0 -76
  191. package/dist/src/config/loader.js.map +0 -1
  192. package/dist/src/config/mappings/claude-code.d.ts +0 -3
  193. package/dist/src/config/mappings/claude-code.d.ts.map +0 -1
  194. package/dist/src/config/mappings/claude-code.js +0 -31
  195. package/dist/src/config/mappings/claude-code.js.map +0 -1
  196. package/dist/src/config/mappings/codex.d.ts +0 -3
  197. package/dist/src/config/mappings/codex.d.ts.map +0 -1
  198. package/dist/src/config/mappings/codex.js +0 -31
  199. package/dist/src/config/mappings/codex.js.map +0 -1
  200. package/dist/src/config/mappings/copilot.d.ts +0 -3
  201. package/dist/src/config/mappings/copilot.d.ts.map +0 -1
  202. package/dist/src/config/mappings/copilot.js +0 -33
  203. package/dist/src/config/mappings/copilot.js.map +0 -1
  204. package/dist/src/config/mappings/cursor.d.ts +0 -3
  205. package/dist/src/config/mappings/cursor.d.ts.map +0 -1
  206. package/dist/src/config/mappings/cursor.js +0 -29
  207. package/dist/src/config/mappings/cursor.js.map +0 -1
  208. package/dist/src/config/sync.d.ts +0 -13
  209. package/dist/src/config/sync.d.ts.map +0 -1
  210. package/dist/src/config/sync.js +0 -96
  211. package/dist/src/config/sync.js.map +0 -1
  212. package/dist/src/ingestion/coordinator.d.ts +0 -37
  213. package/dist/src/ingestion/coordinator.d.ts.map +0 -1
  214. package/dist/src/ingestion/coordinator.js +0 -127
  215. package/dist/src/ingestion/coordinator.js.map +0 -1
  216. package/dist/src/ingestion/daemon.d.ts +0 -17
  217. package/dist/src/ingestion/daemon.d.ts.map +0 -1
  218. package/dist/src/ingestion/daemon.js +0 -35
  219. package/dist/src/ingestion/daemon.js.map +0 -1
  220. package/dist/src/ingestion/watcher.d.ts +0 -19
  221. package/dist/src/ingestion/watcher.d.ts.map +0 -1
  222. package/dist/src/ingestion/watcher.js +0 -70
  223. package/dist/src/ingestion/watcher.js.map +0 -1
  224. package/dist/src/knowledge/autotag.d.ts +0 -3
  225. package/dist/src/knowledge/autotag.d.ts.map +0 -1
  226. package/dist/src/knowledge/autotag.js +0 -24
  227. package/dist/src/knowledge/autotag.js.map +0 -1
  228. package/dist/src/knowledge/dedup.d.ts +0 -7
  229. package/dist/src/knowledge/dedup.d.ts.map +0 -1
  230. package/dist/src/knowledge/dedup.js +0 -15
  231. package/dist/src/knowledge/dedup.js.map +0 -1
  232. package/dist/src/knowledge/repository.d.ts +0 -12
  233. package/dist/src/knowledge/repository.d.ts.map +0 -1
  234. package/dist/src/knowledge/repository.js +0 -74
  235. package/dist/src/knowledge/repository.js.map +0 -1
  236. package/dist/src/mcp/tools/config.d.ts +0 -36
  237. package/dist/src/mcp/tools/config.d.ts.map +0 -1
  238. package/dist/src/mcp/tools/config.js +0 -54
  239. package/dist/src/mcp/tools/config.js.map +0 -1
  240. package/dist/src/mcp/tools/knowledge.d.ts +0 -17
  241. package/dist/src/mcp/tools/knowledge.d.ts.map +0 -1
  242. package/dist/src/mcp/tools/knowledge.js +0 -46
  243. package/dist/src/mcp/tools/knowledge.js.map +0 -1
  244. package/dist/src/mcp/tools/search.d.ts +0 -22
  245. package/dist/src/mcp/tools/search.d.ts.map +0 -1
  246. package/dist/src/mcp/tools/search.js +0 -30
  247. package/dist/src/mcp/tools/search.js.map +0 -1
  248. package/dist/src/mcp/tools/write.d.ts +0 -38
  249. package/dist/src/mcp/tools/write.d.ts.map +0 -1
  250. package/dist/src/mcp/tools/write.js +0 -97
  251. package/dist/src/mcp/tools/write.js.map +0 -1
  252. package/dist/src/runtime/ingestion.d.ts +0 -17
  253. package/dist/src/runtime/ingestion.d.ts.map +0 -1
  254. package/dist/src/runtime/ingestion.js +0 -106
  255. package/dist/src/runtime/ingestion.js.map +0 -1
  256. package/dist/src/scrapers/gemini.d.ts +0 -17
  257. package/dist/src/scrapers/gemini.d.ts.map +0 -1
  258. package/dist/src/scrapers/gemini.js +0 -245
  259. package/dist/src/scrapers/gemini.js.map +0 -1
  260. package/dist/src/store/embeddings.d.ts +0 -9
  261. package/dist/src/store/embeddings.d.ts.map +0 -1
  262. package/dist/src/store/embeddings.js +0 -28
  263. package/dist/src/store/embeddings.js.map +0 -1
  264. package/dist/src/store/lance.d.ts +0 -25
  265. package/dist/src/store/lance.d.ts.map +0 -1
  266. package/dist/src/store/lance.js +0 -74
  267. package/dist/src/store/lance.js.map +0 -1
  268. package/dist/src/store/search.d.ts +0 -18
  269. package/dist/src/store/search.d.ts.map +0 -1
  270. package/dist/src/store/search.js +0 -55
  271. package/dist/src/store/search.js.map +0 -1
  272. package/dist/src/types/compaction.d.ts +0 -29
  273. package/dist/src/types/compaction.d.ts.map +0 -1
  274. package/dist/src/types/compaction.js +0 -2
  275. package/dist/src/types/compaction.js.map +0 -1
  276. package/dist/src/types/context.d.ts +0 -33
  277. package/dist/src/types/context.d.ts.map +0 -1
  278. package/dist/src/types/context.js +0 -18
  279. package/dist/src/types/context.js.map +0 -1
  280. package/dist/src/utils/retry.d.ts +0 -11
  281. package/dist/src/utils/retry.d.ts.map +0 -1
  282. package/dist/src/utils/retry.js +0 -46
  283. package/dist/src/utils/retry.js.map +0 -1
  284. package/dist/src/utils/shutdown.d.ts +0 -25
  285. package/dist/src/utils/shutdown.d.ts.map +0 -1
  286. package/dist/src/utils/shutdown.js +0 -56
  287. package/dist/src/utils/shutdown.js.map +0 -1
  288. package/dist/web/apple-touch-icon.png +0 -0
  289. package/dist/web/assets/index-Nuadr5ox.css +0 -1
  290. package/dist/web/assets/index-aA0u8eXA.js +0 -3504
  291. package/dist/web/assets/primeicons-C6QP2o4f.woff2 +0 -0
  292. package/dist/web/assets/primeicons-DMOk5skT.eot +0 -0
  293. package/dist/web/assets/primeicons-Dr5RGzOO.svg +0 -345
  294. package/dist/web/assets/primeicons-MpK4pl85.ttf +0 -0
  295. package/dist/web/assets/primeicons-WjwUDZjB.woff +0 -0
  296. package/dist/web/favicon.ico +0 -0
  297. package/dist/web/favicon.svg +0 -21
  298. package/dist/web/index.html +0 -21
  299. package/dist/web/site.webmanifest +0 -19
  300. package/dist/web/vite.svg +0 -1
@@ -0,0 +1,718 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import Database from "better-sqlite3";
5
+ import { DEFAULT_EMBEDDING_MODEL, TransformersEmbeddingProvider, } from "./embeddings.js";
6
+ import { cosineSimilarity, deserializeVector, serializeVector } from "./vector.js";
7
+ const DEFAULT_LIMIT = 5;
8
+ const MAX_LIMIT = 100;
9
+ const DEFAULT_WINDOW_SIZE = 8;
10
+ const DEFAULT_WINDOW_STRIDE = 4;
11
+ const MAX_MATCHES_PER_SESSION = 3;
12
+ const SOURCE_CURSOR_OVERLAP_MS = 1_000;
13
+ export class SqliteHandoffIndex {
14
+ dbPath;
15
+ projectRoot;
16
+ tools;
17
+ db = null;
18
+ initialized;
19
+ refreshPromise = null;
20
+ lastRefreshMs = 0;
21
+ refreshTtlMs = 1_000;
22
+ embeddingProvider;
23
+ windowSize;
24
+ windowStride;
25
+ constructor(dbPath, projectRoot, tools, options = {}) {
26
+ this.dbPath = dbPath;
27
+ this.projectRoot = projectRoot;
28
+ this.tools = tools;
29
+ this.embeddingProvider =
30
+ options.embeddingProvider ?? new TransformersEmbeddingProvider(DEFAULT_EMBEDDING_MODEL);
31
+ this.windowSize = Math.max(2, Math.floor(options.windowSize ?? DEFAULT_WINDOW_SIZE));
32
+ this.windowStride = Math.max(1, Math.floor(options.windowStride ?? DEFAULT_WINDOW_STRIDE));
33
+ this.initialized = this.initialize();
34
+ }
35
+ async listRecentSessions(limit, toolFilter) {
36
+ await this.refresh({ toolFilter });
37
+ const db = this.getDb();
38
+ const normalizedLimit = normalizeLimit(limit, DEFAULT_LIMIT);
39
+ const filters = normalizeToolFilter(toolFilter);
40
+ const where = filters.length > 0 ? `WHERE tool IN (${placeholders(filters.length)})` : "";
41
+ const rows = db
42
+ .prepare(`SELECT session_ref, tool, started_at, last_activity_at, message_count, preview, source_path
43
+ FROM sessions
44
+ ${where}
45
+ ORDER BY datetime(last_activity_at) DESC
46
+ LIMIT ?`)
47
+ .all(...filters, normalizedLimit);
48
+ return rows.map(formatSessionRow);
49
+ }
50
+ async getSessionDetail(sessionRef, offset, limit) {
51
+ await this.refresh({ sessionRef });
52
+ const db = this.getDb();
53
+ const normalizedOffset = Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 0;
54
+ const normalizedLimit = normalizeLimit(limit, 50);
55
+ const rows = db
56
+ .prepare(`SELECT id, timestamp, role, content, message_index, source_pointer
57
+ FROM messages
58
+ WHERE session_ref = ?
59
+ ORDER BY datetime(timestamp) ASC, message_index ASC, id ASC
60
+ LIMIT ? OFFSET ?`)
61
+ .all(sessionRef, normalizedLimit, normalizedOffset);
62
+ return rows.map((row) => ({
63
+ timestamp: row.timestamp,
64
+ role: row.role,
65
+ content: row.content,
66
+ source_pointer: row.source_pointer ?? undefined,
67
+ }));
68
+ }
69
+ async searchSessions(query, limit, toolFilter, mode = "hybrid") {
70
+ await this.refresh({ toolFilter });
71
+ const trimmed = query.trim();
72
+ if (!trimmed) {
73
+ return [];
74
+ }
75
+ const normalizedMode = normalizeSearchMode(mode);
76
+ if (normalizedMode === "keyword") {
77
+ return this.keywordSearch(trimmed, limit, toolFilter);
78
+ }
79
+ try {
80
+ return await this.semanticSearch(trimmed, limit, toolFilter, normalizedMode);
81
+ }
82
+ catch (error) {
83
+ if (normalizedMode === "hybrid") {
84
+ return this.keywordSearch(trimmed, limit, toolFilter);
85
+ }
86
+ throw error;
87
+ }
88
+ }
89
+ async getStatus() {
90
+ await this.refresh({ statusOnly: true });
91
+ const db = this.getDb();
92
+ const sessionCount = count(db, "sessions");
93
+ const messageCount = count(db, "messages");
94
+ const retrievalUnitCount = count(db, "retrieval_units");
95
+ const vectorizedUnitCount = count(db, "retrieval_unit_vectors");
96
+ const lastScan = getSetting(db, "last_scan_at");
97
+ const indexedByTool = new Map(db
98
+ .prepare(`SELECT s.tool,
99
+ COUNT(DISTINCT s.session_ref) AS sessions,
100
+ COUNT(m.id) AS messages,
101
+ MAX(m.indexed_at) AS last_indexed_at
102
+ FROM sessions s
103
+ LEFT JOIN messages m ON m.session_ref = s.session_ref
104
+ GROUP BY s.tool`)
105
+ .all().map((row) => [row.tool, row]));
106
+ const tools = await Promise.all(this.tools.map(async ({ tool, scraper }) => {
107
+ const detected = await safeDetect(scraper);
108
+ const indexed = indexedByTool.get(tool);
109
+ return {
110
+ tool,
111
+ detected,
112
+ store_paths: scraper.getStorePaths(),
113
+ indexed_sessions: indexed?.sessions ?? 0,
114
+ indexed_messages: indexed?.messages ?? 0,
115
+ last_indexed_at: indexed?.last_indexed_at ?? null,
116
+ };
117
+ }));
118
+ return {
119
+ project_root: this.projectRoot,
120
+ db_path: this.dbPath,
121
+ last_scan_at: lastScan,
122
+ sessions: sessionCount,
123
+ messages: messageCount,
124
+ retrieval_units: retrievalUnitCount,
125
+ vectorized_units: vectorizedUnitCount,
126
+ vector_model: this.embeddingProvider.model,
127
+ tools,
128
+ };
129
+ }
130
+ async close() {
131
+ await this.initialized;
132
+ this.db?.close();
133
+ this.db = null;
134
+ }
135
+ async refresh(reason) {
136
+ await this.initialized;
137
+ if (reason.statusOnly) {
138
+ return;
139
+ }
140
+ if (Date.now() - this.lastRefreshMs < this.refreshTtlMs) {
141
+ return;
142
+ }
143
+ if (!this.refreshPromise) {
144
+ this.refreshPromise = this.refreshNow().finally(() => {
145
+ this.refreshPromise = null;
146
+ });
147
+ }
148
+ await this.refreshPromise;
149
+ }
150
+ async refreshNow() {
151
+ const db = this.getDb();
152
+ const startedAt = new Date().toISOString();
153
+ const touchedSessions = new Set();
154
+ for (const { scraper } of this.tools) {
155
+ if (!(await safeDetect(scraper))) {
156
+ continue;
157
+ }
158
+ let latestTimestamp = null;
159
+ try {
160
+ for await (const chunk of scraper.scrape()) {
161
+ const sessionRef = this.upsertChunk(chunk);
162
+ if (sessionRef) {
163
+ touchedSessions.add(sessionRef);
164
+ }
165
+ if (!latestTimestamp || chunk.timestamp > latestTimestamp) {
166
+ latestTimestamp = chunk.timestamp;
167
+ }
168
+ }
169
+ if (latestTimestamp) {
170
+ await scraper.saveScrapedPosition({
171
+ lastTimestamp: overlapTimestamp(latestTimestamp),
172
+ });
173
+ }
174
+ }
175
+ catch (error) {
176
+ setSetting(db, `last_error:${scraper.tool}`, error instanceof Error ? error.message : String(error));
177
+ if (latestTimestamp) {
178
+ try {
179
+ await scraper.saveScrapedPosition({
180
+ lastTimestamp: overlapTimestamp(latestTimestamp),
181
+ });
182
+ }
183
+ catch {
184
+ // Ignore position save failures inside catch
185
+ }
186
+ }
187
+ }
188
+ }
189
+ for (const sessionRef of touchedSessions) {
190
+ this.rebuildRetrievalUnitsForSession(sessionRef);
191
+ }
192
+ setSetting(db, "last_scan_at", startedAt);
193
+ this.lastRefreshMs = Date.now();
194
+ }
195
+ upsertChunk(chunk) {
196
+ if (!chunk.content.trim()) {
197
+ return null;
198
+ }
199
+ const db = this.getDb();
200
+ const timestamp = chunk.timestamp.toISOString();
201
+ const sessionRef = `${chunk.tool}:${chunk.sessionId}`;
202
+ const messageIndex = chunk.metadata.messageIndex ?? 0;
203
+ const id = hashParts([
204
+ chunk.tool,
205
+ chunk.sessionId,
206
+ timestamp,
207
+ chunk.role,
208
+ String(messageIndex),
209
+ chunk.content,
210
+ ]);
211
+ const contentHash = hashParts([chunk.content]);
212
+ const now = new Date().toISOString();
213
+ const metadataJson = JSON.stringify(chunk.metadata ?? {});
214
+ const sourcePointer = sourcePathFromMetadata(chunk.metadata);
215
+ db.prepare(`INSERT INTO sessions
216
+ (session_ref, tool, source_session_id, project_root, started_at, last_activity_at,
217
+ message_count, preview, source_path, updated_at)
218
+ VALUES (?, ?, ?, ?, ?, ?, 0, NULL, ?, ?)
219
+ ON CONFLICT(session_ref) DO UPDATE SET
220
+ started_at = CASE
221
+ WHEN excluded.started_at < started_at THEN excluded.started_at
222
+ ELSE started_at
223
+ END,
224
+ last_activity_at = CASE
225
+ WHEN excluded.last_activity_at > last_activity_at THEN excluded.last_activity_at
226
+ ELSE last_activity_at
227
+ END,
228
+ source_path = COALESCE(source_path, excluded.source_path),
229
+ updated_at = excluded.updated_at`).run(sessionRef, chunk.tool, chunk.sessionId, this.projectRoot, timestamp, timestamp, sourcePointer, now);
230
+ db.prepare(`INSERT OR IGNORE INTO messages
231
+ (id, session_ref, tool, source_session_id, timestamp, role, content,
232
+ message_index, content_hash, metadata_json, source_pointer, indexed_at)
233
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, sessionRef, chunk.tool, chunk.sessionId, timestamp, chunk.role, chunk.content, messageIndex, contentHash, metadataJson, sourcePointer, now);
234
+ db.prepare(`INSERT OR IGNORE INTO messages_fts(rowid, session_ref, tool, role, timestamp, content)
235
+ SELECT rowid, session_ref, tool, role, timestamp, content
236
+ FROM messages
237
+ WHERE id = ?`).run(id);
238
+ db.prepare(`UPDATE sessions
239
+ SET message_count = (
240
+ SELECT COUNT(*) FROM messages WHERE messages.session_ref = sessions.session_ref
241
+ ),
242
+ preview = COALESCE(
243
+ (
244
+ SELECT substr(content, 1, 240)
245
+ FROM messages
246
+ WHERE messages.session_ref = sessions.session_ref
247
+ ORDER BY datetime(timestamp) ASC, message_index ASC, id ASC
248
+ LIMIT 1
249
+ ),
250
+ preview
251
+ )
252
+ WHERE session_ref = ?`).run(sessionRef);
253
+ return sessionRef;
254
+ }
255
+ rebuildRetrievalUnitsForSession(sessionRef) {
256
+ const db = this.getDb();
257
+ const messages = db
258
+ .prepare(`SELECT id, timestamp, role, content, message_index, source_pointer
259
+ FROM messages
260
+ WHERE session_ref = ?
261
+ ORDER BY datetime(timestamp) ASC, message_index ASC, id ASC`)
262
+ .all(sessionRef);
263
+ db.prepare("DELETE FROM retrieval_units_fts WHERE session_ref = ?").run(sessionRef);
264
+ db.prepare("DELETE FROM retrieval_units WHERE session_ref = ?").run(sessionRef);
265
+ if (messages.length === 0) {
266
+ return;
267
+ }
268
+ const session = db
269
+ .prepare("SELECT tool FROM sessions WHERE session_ref = ?")
270
+ .get(sessionRef);
271
+ if (!session) {
272
+ return;
273
+ }
274
+ const now = new Date().toISOString();
275
+ for (const window of buildMessageWindows(messages, this.windowSize, this.windowStride)) {
276
+ const content = formatRetrievalUnitContent(sessionRef, window.messages);
277
+ const contentHash = hashParts([content]);
278
+ const unitId = hashParts([
279
+ "retrieval-unit",
280
+ sessionRef,
281
+ String(window.start.message_index),
282
+ String(window.end.message_index),
283
+ contentHash,
284
+ ]);
285
+ db.prepare(`INSERT INTO retrieval_units
286
+ (id, session_ref, tool, message_start_index, message_end_index,
287
+ started_at, ended_at, content, content_hash, updated_at)
288
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(unitId, sessionRef, session.tool, window.start.message_index, window.end.message_index, window.start.timestamp, window.end.timestamp, content, contentHash, now);
289
+ db.prepare(`INSERT INTO retrieval_units_fts(unit_id, session_ref, tool, content)
290
+ VALUES (?, ?, ?, ?)`).run(unitId, sessionRef, session.tool, content);
291
+ }
292
+ }
293
+ async keywordSearch(query, limit, toolFilter) {
294
+ const rows = this.queryKeywordUnits(query, limit, toolFilter);
295
+ return groupUnits(rows, new Map(), "keyword", normalizeLimit(limit, DEFAULT_LIMIT));
296
+ }
297
+ async semanticSearch(query, limit, toolFilter, mode) {
298
+ const normalizedLimit = normalizeLimit(limit, DEFAULT_LIMIT);
299
+ await this.ensureVectors(toolFilter);
300
+ const db = this.getDb();
301
+ const filters = normalizeToolFilter(toolFilter);
302
+ const toolWhere = filters.length > 0 ? `AND u.tool IN (${placeholders(filters.length)})` : "";
303
+ const rows = db
304
+ .prepare(`${retrievalUnitSelect()},
305
+ v.vector,
306
+ v.dimensions
307
+ FROM retrieval_units u
308
+ JOIN retrieval_unit_vectors v ON v.unit_id = u.id
309
+ JOIN sessions s ON s.session_ref = u.session_ref
310
+ WHERE v.model = ? ${toolWhere}`)
311
+ .all(this.embeddingProvider.model, ...filters);
312
+ if (rows.length === 0) {
313
+ return [];
314
+ }
315
+ const keywordRows = mode === "hybrid" ? this.queryKeywordUnits(query, limit, toolFilter) : [];
316
+ const keywordScores = rankKeywordRows(keywordRows);
317
+ const queryVector = await this.embeddingProvider.embed(query);
318
+ const timeRange = getTimeRange(rows.map((row) => row.ended_at));
319
+ const scored = rows
320
+ .map((row) => {
321
+ const semanticScore = normalizeCosine(cosineSimilarity(queryVector, deserializeVector(row.vector, row.dimensions)));
322
+ const keywordScore = keywordScores.get(row.unit_id) ?? 0;
323
+ const recencyScore = scoreRecency(row.ended_at, timeRange);
324
+ const continuityScore = scoreContinuity(row.message_end_index, row.session_message_count);
325
+ return {
326
+ row,
327
+ score: blendScores(mode, semanticScore, keywordScore, recencyScore, continuityScore),
328
+ semanticScore,
329
+ keywordScore,
330
+ recencyScore,
331
+ continuityScore,
332
+ };
333
+ })
334
+ .sort((left, right) => right.score - left.score);
335
+ return groupScoredUnits(scored, mode, normalizedLimit);
336
+ }
337
+ queryKeywordUnits(query, limit, toolFilter) {
338
+ const ftsQuery = toFtsQuery(query);
339
+ if (!ftsQuery) {
340
+ return [];
341
+ }
342
+ const db = this.getDb();
343
+ const normalizedLimit = normalizeLimit(limit, DEFAULT_LIMIT);
344
+ const filters = normalizeToolFilter(toolFilter);
345
+ const toolWhere = filters.length > 0 ? `AND u.tool IN (${placeholders(filters.length)})` : "";
346
+ return db
347
+ .prepare(`${retrievalUnitSelect()}
348
+ FROM retrieval_units_fts f
349
+ JOIN retrieval_units u ON u.id = f.unit_id
350
+ JOIN sessions s ON s.session_ref = u.session_ref
351
+ WHERE retrieval_units_fts MATCH ? ${toolWhere}
352
+ ORDER BY bm25(retrieval_units_fts), datetime(u.ended_at) DESC
353
+ LIMIT ?`)
354
+ .all(ftsQuery, ...filters, normalizedLimit * MAX_MATCHES_PER_SESSION);
355
+ }
356
+ async ensureVectors(toolFilter) {
357
+ const db = this.getDb();
358
+ const filters = normalizeToolFilter(toolFilter);
359
+ const toolWhere = filters.length > 0 ? `AND u.tool IN (${placeholders(filters.length)})` : "";
360
+ const rows = db
361
+ .prepare(`SELECT u.id AS unit_id, u.content, u.content_hash
362
+ FROM retrieval_units u
363
+ LEFT JOIN retrieval_unit_vectors v
364
+ ON v.unit_id = u.id
365
+ AND v.model = ?
366
+ AND v.content_hash = u.content_hash
367
+ WHERE v.unit_id IS NULL ${toolWhere}
368
+ ORDER BY datetime(u.ended_at) DESC`)
369
+ .all(this.embeddingProvider.model, ...filters);
370
+ if (rows.length === 0) {
371
+ return;
372
+ }
373
+ const vectors = await this.embeddingProvider.embedBatch(rows.map((row) => row.content));
374
+ const now = new Date().toISOString();
375
+ const upsert = db.prepare(`INSERT INTO retrieval_unit_vectors
376
+ (unit_id, model, dimensions, content_hash, vector, created_at)
377
+ VALUES (?, ?, ?, ?, ?, ?)
378
+ ON CONFLICT(unit_id) DO UPDATE SET
379
+ model = excluded.model,
380
+ dimensions = excluded.dimensions,
381
+ content_hash = excluded.content_hash,
382
+ vector = excluded.vector,
383
+ created_at = excluded.created_at`);
384
+ const transaction = db.transaction(() => {
385
+ rows.forEach((row, index) => {
386
+ const vector = vectors[index];
387
+ upsert.run(row.unit_id, this.embeddingProvider.model, vector.length, row.content_hash, serializeVector(vector), now);
388
+ });
389
+ });
390
+ transaction();
391
+ }
392
+ async initialize() {
393
+ await mkdir(dirname(this.dbPath), { recursive: true });
394
+ this.db = new Database(this.dbPath);
395
+ this.db.pragma("journal_mode = WAL");
396
+ this.db.pragma("foreign_keys = ON");
397
+ createSchema(this.db);
398
+ }
399
+ getDb() {
400
+ if (!this.db) {
401
+ throw new Error("xtctx handoff index is closed");
402
+ }
403
+ return this.db;
404
+ }
405
+ }
406
+ function createSchema(db) {
407
+ db.exec(`
408
+ CREATE TABLE IF NOT EXISTS sessions (
409
+ session_ref TEXT PRIMARY KEY,
410
+ tool TEXT NOT NULL,
411
+ source_session_id TEXT NOT NULL,
412
+ project_root TEXT NOT NULL,
413
+ started_at TEXT NOT NULL,
414
+ last_activity_at TEXT NOT NULL,
415
+ message_count INTEGER NOT NULL DEFAULT 0,
416
+ preview TEXT,
417
+ source_path TEXT,
418
+ updated_at TEXT NOT NULL
419
+ );
420
+
421
+ CREATE TABLE IF NOT EXISTS messages (
422
+ id TEXT PRIMARY KEY,
423
+ session_ref TEXT NOT NULL REFERENCES sessions(session_ref) ON DELETE CASCADE,
424
+ tool TEXT NOT NULL,
425
+ source_session_id TEXT NOT NULL,
426
+ timestamp TEXT NOT NULL,
427
+ role TEXT NOT NULL,
428
+ content TEXT NOT NULL,
429
+ message_index INTEGER NOT NULL,
430
+ content_hash TEXT NOT NULL,
431
+ metadata_json TEXT NOT NULL,
432
+ source_pointer TEXT,
433
+ indexed_at TEXT NOT NULL
434
+ );
435
+
436
+ CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(last_activity_at DESC);
437
+ CREATE INDEX IF NOT EXISTS idx_messages_session_order
438
+ ON messages(session_ref, timestamp, message_index, id);
439
+
440
+ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts
441
+ USING fts5(session_ref UNINDEXED, tool UNINDEXED, role UNINDEXED, timestamp UNINDEXED, content);
442
+
443
+ CREATE TABLE IF NOT EXISTS retrieval_units (
444
+ id TEXT PRIMARY KEY,
445
+ session_ref TEXT NOT NULL REFERENCES sessions(session_ref) ON DELETE CASCADE,
446
+ tool TEXT NOT NULL,
447
+ message_start_index INTEGER NOT NULL,
448
+ message_end_index INTEGER NOT NULL,
449
+ started_at TEXT NOT NULL,
450
+ ended_at TEXT NOT NULL,
451
+ content TEXT NOT NULL,
452
+ content_hash TEXT NOT NULL,
453
+ updated_at TEXT NOT NULL
454
+ );
455
+
456
+ CREATE INDEX IF NOT EXISTS idx_retrieval_units_session
457
+ ON retrieval_units(session_ref, message_start_index, message_end_index);
458
+ CREATE INDEX IF NOT EXISTS idx_retrieval_units_tool_time
459
+ ON retrieval_units(tool, ended_at DESC);
460
+
461
+ CREATE VIRTUAL TABLE IF NOT EXISTS retrieval_units_fts
462
+ USING fts5(unit_id UNINDEXED, session_ref UNINDEXED, tool UNINDEXED, content);
463
+
464
+ CREATE TABLE IF NOT EXISTS retrieval_unit_vectors (
465
+ unit_id TEXT PRIMARY KEY REFERENCES retrieval_units(id) ON DELETE CASCADE,
466
+ model TEXT NOT NULL,
467
+ dimensions INTEGER NOT NULL,
468
+ content_hash TEXT NOT NULL,
469
+ vector BLOB NOT NULL,
470
+ created_at TEXT NOT NULL
471
+ );
472
+
473
+ CREATE INDEX IF NOT EXISTS idx_retrieval_unit_vectors_model
474
+ ON retrieval_unit_vectors(model);
475
+
476
+ CREATE TABLE IF NOT EXISTS settings (
477
+ key TEXT PRIMARY KEY,
478
+ value TEXT NOT NULL
479
+ );
480
+ `);
481
+ }
482
+ function retrievalUnitSelect() {
483
+ return `SELECT u.id AS unit_id,
484
+ u.session_ref,
485
+ u.tool,
486
+ u.message_start_index,
487
+ u.message_end_index,
488
+ u.started_at,
489
+ u.ended_at,
490
+ u.content,
491
+ u.content_hash,
492
+ s.started_at AS session_started_at,
493
+ s.last_activity_at AS session_last_activity_at,
494
+ s.message_count AS session_message_count,
495
+ s.preview AS session_preview,
496
+ s.source_path`;
497
+ }
498
+ function formatSessionRow(row) {
499
+ return {
500
+ session_ref: row.session_ref,
501
+ tool: row.tool,
502
+ started_at: row.started_at,
503
+ last_activity_at: row.last_activity_at,
504
+ message_count: row.message_count,
505
+ preview: row.preview ?? undefined,
506
+ source_path: row.source_path ?? undefined,
507
+ };
508
+ }
509
+ function buildMessageWindows(messages, windowSize, windowStride) {
510
+ const windows = [];
511
+ for (let start = 0; start < messages.length; start += windowStride) {
512
+ const slice = messages.slice(start, start + windowSize);
513
+ if (slice.length === 0) {
514
+ continue;
515
+ }
516
+ windows.push({
517
+ start: slice[0],
518
+ end: slice[slice.length - 1],
519
+ messages: slice,
520
+ });
521
+ if (start + windowSize >= messages.length) {
522
+ break;
523
+ }
524
+ }
525
+ return windows;
526
+ }
527
+ function formatRetrievalUnitContent(sessionRef, messages) {
528
+ const lines = [
529
+ `Session: ${sessionRef}`,
530
+ `Chronological window: messages ${messages[0].message_index} through ${messages[messages.length - 1].message_index}`,
531
+ ];
532
+ for (const [index, message] of messages.entries()) {
533
+ lines.push([
534
+ `Turn ${index + 1}/${messages.length}`,
535
+ `message_index=${message.message_index}`,
536
+ `${message.role} @ ${message.timestamp}`,
537
+ ].join(" | "));
538
+ lines.push(message.content);
539
+ }
540
+ return lines.join("\n");
541
+ }
542
+ function groupUnits(rows, keywordScores, retrieval, limit) {
543
+ const timeRange = getTimeRange(rows.map((row) => row.ended_at));
544
+ const scored = rows.map((row) => {
545
+ const keywordScore = keywordScores.get(row.unit_id) ?? 1;
546
+ const recencyScore = scoreRecency(row.ended_at, timeRange);
547
+ const continuityScore = scoreContinuity(row.message_end_index, row.session_message_count);
548
+ return {
549
+ row,
550
+ score: blendScores("keyword", 0, keywordScore, recencyScore, continuityScore),
551
+ semanticScore: 0,
552
+ keywordScore,
553
+ recencyScore,
554
+ continuityScore,
555
+ };
556
+ });
557
+ return groupScoredUnits(scored, retrieval, limit);
558
+ }
559
+ function groupScoredUnits(scored, retrieval, limit) {
560
+ const sessions = new Map();
561
+ for (const item of scored) {
562
+ const existing = sessions.get(item.row.session_ref);
563
+ const match = formatMatch(item);
564
+ if (existing) {
565
+ if ((existing.matches?.length ?? 0) < MAX_MATCHES_PER_SESSION) {
566
+ existing.matches = [...(existing.matches ?? []), match];
567
+ }
568
+ existing.score = Math.max(existing.score ?? 0, item.score);
569
+ continue;
570
+ }
571
+ sessions.set(item.row.session_ref, {
572
+ session_ref: item.row.session_ref,
573
+ tool: item.row.tool,
574
+ started_at: item.row.session_started_at,
575
+ last_activity_at: item.row.session_last_activity_at,
576
+ message_count: item.row.session_message_count,
577
+ preview: item.row.session_preview ?? previewText(item.row.content),
578
+ source_path: item.row.source_path ?? undefined,
579
+ score: item.score,
580
+ retrieval,
581
+ matches: [match],
582
+ });
583
+ if (sessions.size >= limit) {
584
+ break;
585
+ }
586
+ }
587
+ return [...sessions.values()].sort((left, right) => (right.score ?? 0) - (left.score ?? 0));
588
+ }
589
+ function formatMatch(item) {
590
+ return {
591
+ unit_id: item.row.unit_id,
592
+ message_start_index: item.row.message_start_index,
593
+ message_end_index: item.row.message_end_index,
594
+ started_at: item.row.started_at,
595
+ ended_at: item.row.ended_at,
596
+ preview: previewText(item.row.content),
597
+ score: roundScore(item.score),
598
+ semantic_score: roundScore(item.semanticScore),
599
+ keyword_score: roundScore(item.keywordScore),
600
+ recency_score: roundScore(item.recencyScore),
601
+ continuity_score: roundScore(item.continuityScore),
602
+ };
603
+ }
604
+ function rankKeywordRows(rows) {
605
+ const scores = new Map();
606
+ rows.forEach((row, index) => {
607
+ scores.set(row.unit_id, 1 / (index + 1));
608
+ });
609
+ return scores;
610
+ }
611
+ function blendScores(mode, semanticScore, keywordScore, recencyScore, continuityScore) {
612
+ if (mode === "vector") {
613
+ return 0.85 * semanticScore + 0.1 * recencyScore + 0.05 * continuityScore;
614
+ }
615
+ if (mode === "keyword") {
616
+ return 0.75 * keywordScore + 0.15 * recencyScore + 0.1 * continuityScore;
617
+ }
618
+ return (0.65 * semanticScore +
619
+ 0.2 * keywordScore +
620
+ 0.1 * recencyScore +
621
+ 0.05 * continuityScore);
622
+ }
623
+ function normalizeCosine(score) {
624
+ return Math.max(0, Math.min(1, (score + 1) / 2));
625
+ }
626
+ function scoreContinuity(messageEndIndex, messageCount) {
627
+ if (messageCount <= 1) {
628
+ return 1;
629
+ }
630
+ return Math.max(0, Math.min(1, messageEndIndex / (messageCount - 1)));
631
+ }
632
+ function scoreRecency(value, range) {
633
+ const timestamp = Date.parse(value);
634
+ if (!Number.isFinite(timestamp) || range.newest <= range.oldest) {
635
+ return 1;
636
+ }
637
+ return Math.max(0, Math.min(1, (timestamp - range.oldest) / (range.newest - range.oldest)));
638
+ }
639
+ function getTimeRange(values) {
640
+ const timestamps = values
641
+ .map((value) => Date.parse(value))
642
+ .filter((value) => Number.isFinite(value));
643
+ if (timestamps.length === 0) {
644
+ return { oldest: 0, newest: 0 };
645
+ }
646
+ return {
647
+ oldest: Math.min(...timestamps),
648
+ newest: Math.max(...timestamps),
649
+ };
650
+ }
651
+ function previewText(value) {
652
+ return value.replace(/\s+/g, " ").trim().slice(0, 240);
653
+ }
654
+ function roundScore(value) {
655
+ return Math.round(value * 1000) / 1000;
656
+ }
657
+ function normalizeLimit(value, fallback) {
658
+ if (!Number.isFinite(value) || value <= 0) {
659
+ return fallback;
660
+ }
661
+ return Math.min(Math.floor(value), MAX_LIMIT);
662
+ }
663
+ function normalizeSearchMode(value) {
664
+ return value === "keyword" || value === "vector" || value === "hybrid" ? value : "hybrid";
665
+ }
666
+ function normalizeToolFilter(value) {
667
+ if (!Array.isArray(value)) {
668
+ return [];
669
+ }
670
+ return [...new Set(value.filter((item) => typeof item === "string" && item.length > 0))];
671
+ }
672
+ function toFtsQuery(query) {
673
+ return (query
674
+ .toLowerCase()
675
+ .match(/[a-z0-9_./:-]{2,}/g)
676
+ ?.map((term) => `"${term.replace(/"/g, "\"\"")}"`)
677
+ .join(" OR ") ?? "");
678
+ }
679
+ function placeholders(countValue) {
680
+ return Array.from({ length: countValue }, () => "?").join(", ");
681
+ }
682
+ function count(db, table) {
683
+ const row = db.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get();
684
+ return row.count;
685
+ }
686
+ function getSetting(db, key) {
687
+ const row = db.prepare("SELECT value FROM settings WHERE key = ?").get(key);
688
+ return row?.value ?? null;
689
+ }
690
+ function setSetting(db, key, value) {
691
+ db.prepare(`INSERT INTO settings(key, value)
692
+ VALUES (?, ?)
693
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(key, value);
694
+ }
695
+ function sourcePathFromMetadata(metadata) {
696
+ const value = metadata.sourcePath;
697
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
698
+ }
699
+ function overlapTimestamp(value) {
700
+ return new Date(Math.max(0, value.getTime() - SOURCE_CURSOR_OVERLAP_MS));
701
+ }
702
+ function hashParts(parts) {
703
+ const hash = createHash("sha256");
704
+ for (const part of parts) {
705
+ hash.update(part);
706
+ hash.update("\0");
707
+ }
708
+ return hash.digest("hex");
709
+ }
710
+ async function safeDetect(scraper) {
711
+ try {
712
+ return await scraper.detect();
713
+ }
714
+ catch {
715
+ return false;
716
+ }
717
+ }
718
+ //# sourceMappingURL=sqlite-index.js.map