@tekmidian/pai 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 (66) hide show
  1. package/ARCHITECTURE.md +567 -0
  2. package/FEATURE.md +108 -0
  3. package/LICENSE +21 -0
  4. package/README.md +101 -0
  5. package/dist/auto-route-D7W6RE06.mjs +86 -0
  6. package/dist/auto-route-D7W6RE06.mjs.map +1 -0
  7. package/dist/cli/index.d.mts +1 -0
  8. package/dist/cli/index.mjs +5927 -0
  9. package/dist/cli/index.mjs.map +1 -0
  10. package/dist/config-DBh1bYM2.mjs +151 -0
  11. package/dist/config-DBh1bYM2.mjs.map +1 -0
  12. package/dist/daemon/index.d.mts +1 -0
  13. package/dist/daemon/index.mjs +56 -0
  14. package/dist/daemon/index.mjs.map +1 -0
  15. package/dist/daemon-mcp/index.d.mts +1 -0
  16. package/dist/daemon-mcp/index.mjs +185 -0
  17. package/dist/daemon-mcp/index.mjs.map +1 -0
  18. package/dist/daemon-v5O897D4.mjs +773 -0
  19. package/dist/daemon-v5O897D4.mjs.map +1 -0
  20. package/dist/db-4lSqLFb8.mjs +199 -0
  21. package/dist/db-4lSqLFb8.mjs.map +1 -0
  22. package/dist/db-BcDxXVBu.mjs +110 -0
  23. package/dist/db-BcDxXVBu.mjs.map +1 -0
  24. package/dist/detect-BHqYcjJ1.mjs +86 -0
  25. package/dist/detect-BHqYcjJ1.mjs.map +1 -0
  26. package/dist/detector-DKA83aTZ.mjs +74 -0
  27. package/dist/detector-DKA83aTZ.mjs.map +1 -0
  28. package/dist/embeddings-mfqv-jFu.mjs +91 -0
  29. package/dist/embeddings-mfqv-jFu.mjs.map +1 -0
  30. package/dist/factory-BDAiKtYR.mjs +42 -0
  31. package/dist/factory-BDAiKtYR.mjs.map +1 -0
  32. package/dist/index.d.mts +307 -0
  33. package/dist/index.d.mts.map +1 -0
  34. package/dist/index.mjs +11 -0
  35. package/dist/indexer-B20bPHL-.mjs +677 -0
  36. package/dist/indexer-B20bPHL-.mjs.map +1 -0
  37. package/dist/indexer-backend-BXaocO5r.mjs +360 -0
  38. package/dist/indexer-backend-BXaocO5r.mjs.map +1 -0
  39. package/dist/ipc-client-DPy7s3iu.mjs +156 -0
  40. package/dist/ipc-client-DPy7s3iu.mjs.map +1 -0
  41. package/dist/mcp/index.d.mts +1 -0
  42. package/dist/mcp/index.mjs +373 -0
  43. package/dist/mcp/index.mjs.map +1 -0
  44. package/dist/migrate-Bwj7qPaE.mjs +241 -0
  45. package/dist/migrate-Bwj7qPaE.mjs.map +1 -0
  46. package/dist/pai-marker-DX_mFLum.mjs +186 -0
  47. package/dist/pai-marker-DX_mFLum.mjs.map +1 -0
  48. package/dist/postgres-Ccvpc6fC.mjs +335 -0
  49. package/dist/postgres-Ccvpc6fC.mjs.map +1 -0
  50. package/dist/rolldown-runtime-95iHPtFO.mjs +18 -0
  51. package/dist/schemas-DjdwzIQ8.mjs +3405 -0
  52. package/dist/schemas-DjdwzIQ8.mjs.map +1 -0
  53. package/dist/search-PjftDxxs.mjs +282 -0
  54. package/dist/search-PjftDxxs.mjs.map +1 -0
  55. package/dist/sqlite-CHUrNtbI.mjs +90 -0
  56. package/dist/sqlite-CHUrNtbI.mjs.map +1 -0
  57. package/dist/tools-CLK4080-.mjs +805 -0
  58. package/dist/tools-CLK4080-.mjs.map +1 -0
  59. package/dist/utils-DEWdIFQ0.mjs +160 -0
  60. package/dist/utils-DEWdIFQ0.mjs.map +1 -0
  61. package/package.json +72 -0
  62. package/templates/README.md +181 -0
  63. package/templates/agent-prefs.example.md +362 -0
  64. package/templates/claude-md.template.md +733 -0
  65. package/templates/pai-project.template.md +13 -0
  66. package/templates/voices.example.json +251 -0
@@ -0,0 +1,282 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
2
+ import { n as cosineSimilarity, r as deserializeEmbedding } from "./embeddings-mfqv-jFu.mjs";
3
+
4
+ //#region src/memory/search.ts
5
+ var search_exports = /* @__PURE__ */ __exportAll({
6
+ buildFtsQuery: () => buildFtsQuery,
7
+ populateSlugs: () => populateSlugs,
8
+ searchMemory: () => searchMemory,
9
+ searchMemoryHybrid: () => searchMemoryHybrid,
10
+ searchMemorySemantic: () => searchMemorySemantic
11
+ });
12
+ const STOP_WORDS = new Set([
13
+ "a",
14
+ "an",
15
+ "and",
16
+ "are",
17
+ "as",
18
+ "at",
19
+ "be",
20
+ "been",
21
+ "but",
22
+ "by",
23
+ "do",
24
+ "for",
25
+ "from",
26
+ "has",
27
+ "have",
28
+ "he",
29
+ "her",
30
+ "him",
31
+ "his",
32
+ "how",
33
+ "i",
34
+ "if",
35
+ "in",
36
+ "is",
37
+ "it",
38
+ "its",
39
+ "me",
40
+ "my",
41
+ "not",
42
+ "of",
43
+ "on",
44
+ "or",
45
+ "our",
46
+ "out",
47
+ "she",
48
+ "so",
49
+ "that",
50
+ "the",
51
+ "their",
52
+ "them",
53
+ "they",
54
+ "this",
55
+ "to",
56
+ "up",
57
+ "us",
58
+ "was",
59
+ "we",
60
+ "were",
61
+ "what",
62
+ "when",
63
+ "who",
64
+ "will",
65
+ "with",
66
+ "you",
67
+ "your"
68
+ ]);
69
+ /**
70
+ * Convert a free-text query into an FTS5 query string.
71
+ *
72
+ * Strategy:
73
+ * 1. Tokenise by whitespace and punctuation
74
+ * 2. Remove stop words and tokens shorter than 2 characters
75
+ * 3. Double-quote each remaining token (exact word form)
76
+ * 4. Join with OR so that any matching token returns a result
77
+ *
78
+ * Using OR instead of AND is critical for multi-word queries: the words rarely
79
+ * all appear in the same chunk, so AND would return zero results. FTS5 BM25
80
+ * scoring naturally ranks chunks where more terms match higher, so the most
81
+ * relevant chunks still surface at the top.
82
+ *
83
+ * Example: "Synchrotech interview follow-up Gilles"
84
+ * → `"synchrotech" OR "interview" OR "follow" OR "gilles"`
85
+ * → chunks matching any term, ranked by how many terms match
86
+ */
87
+ function buildFtsQuery(query) {
88
+ const tokens = query.toLowerCase().split(/[\s\p{P}]+/u).filter(Boolean).filter((t) => t.length >= 2).filter((t) => !STOP_WORDS.has(t)).map((t) => `"${t.replace(/"/g, "\"\"")}"`);
89
+ if (tokens.length === 0) return `"${query.replace(/"/g, "\"\"")}"`;
90
+ return tokens.join(" OR ");
91
+ }
92
+ /**
93
+ * Search across all indexed memory using FTS5 BM25 ranking.
94
+ *
95
+ * Results are ordered by BM25 score (most relevant first).
96
+ * FTS5 bm25() returns negative values; closer to 0 = more relevant.
97
+ * We negate the score so callers get positive values where higher = better.
98
+ *
99
+ * Multilingual note: SQLite FTS5 uses the `unicode61` tokenizer by default,
100
+ * which handles Unicode correctly (German umlauts, French accents, etc.) without
101
+ * language-specific stemming. No changes needed here — it is already
102
+ * multilingual-safe.
103
+ */
104
+ function searchMemory(db, query, opts) {
105
+ const maxResults = opts?.maxResults ?? 10;
106
+ const ftsQuery = buildFtsQuery(query);
107
+ const conditions = [];
108
+ const params = [ftsQuery];
109
+ if (opts?.projectIds && opts.projectIds.length > 0) {
110
+ const placeholders = opts.projectIds.map(() => "?").join(", ");
111
+ conditions.push(`c.project_id IN (${placeholders})`);
112
+ params.push(...opts.projectIds);
113
+ }
114
+ if (opts?.sources && opts.sources.length > 0) {
115
+ const placeholders = opts.sources.map(() => "?").join(", ");
116
+ conditions.push(`c.source IN (${placeholders})`);
117
+ params.push(...opts.sources);
118
+ }
119
+ if (opts?.tiers && opts.tiers.length > 0) {
120
+ const placeholders = opts.tiers.map(() => "?").join(", ");
121
+ conditions.push(`c.tier IN (${placeholders})`);
122
+ params.push(...opts.tiers);
123
+ }
124
+ const whereClause = conditions.length > 0 ? "AND " + conditions.join(" AND ") : "";
125
+ params.push(maxResults);
126
+ const sql = `
127
+ SELECT
128
+ c.project_id,
129
+ c.path,
130
+ c.start_line,
131
+ c.end_line,
132
+ c.text AS snippet,
133
+ c.tier,
134
+ c.source,
135
+ bm25(memory_fts) AS bm25_score
136
+ FROM memory_fts
137
+ JOIN memory_chunks c ON memory_fts.id = c.id
138
+ WHERE memory_fts MATCH ?
139
+ ${whereClause}
140
+ ORDER BY bm25_score
141
+ LIMIT ?
142
+ `;
143
+ let rows;
144
+ try {
145
+ rows = db.prepare(sql).all(...params);
146
+ } catch {
147
+ return [];
148
+ }
149
+ const minScore = opts?.minScore ?? 0;
150
+ return rows.map((row) => ({
151
+ projectId: row.project_id,
152
+ path: row.path,
153
+ startLine: row.start_line,
154
+ endLine: row.end_line,
155
+ snippet: row.snippet,
156
+ score: -row.bm25_score,
157
+ tier: row.tier,
158
+ source: row.source
159
+ })).filter((r) => r.score >= minScore);
160
+ }
161
+ /**
162
+ * Search chunks using brute-force cosine similarity over stored embeddings.
163
+ *
164
+ * Only chunks that have a non-null embedding BLOB are considered. Chunks
165
+ * without embeddings are silently skipped (they can be embedded later via
166
+ * `embedChunks()`).
167
+ *
168
+ * @param queryEmbedding Pre-computed Float32Array for the search query.
169
+ */
170
+ function searchMemorySemantic(db, queryEmbedding, opts) {
171
+ const maxResults = opts?.maxResults ?? 10;
172
+ const conditions = ["embedding IS NOT NULL"];
173
+ const params = [];
174
+ if (opts?.projectIds && opts.projectIds.length > 0) {
175
+ const placeholders = opts.projectIds.map(() => "?").join(", ");
176
+ conditions.push(`project_id IN (${placeholders})`);
177
+ params.push(...opts.projectIds);
178
+ }
179
+ if (opts?.sources && opts.sources.length > 0) {
180
+ const placeholders = opts.sources.map(() => "?").join(", ");
181
+ conditions.push(`source IN (${placeholders})`);
182
+ params.push(...opts.sources);
183
+ }
184
+ if (opts?.tiers && opts.tiers.length > 0) {
185
+ const placeholders = opts.tiers.map(() => "?").join(", ");
186
+ conditions.push(`tier IN (${placeholders})`);
187
+ params.push(...opts.tiers);
188
+ }
189
+ const sql = `
190
+ SELECT id, project_id, path, start_line, end_line, text, tier, source, embedding
191
+ FROM memory_chunks
192
+ ${"WHERE " + conditions.join(" AND ")}
193
+ LIMIT 5000
194
+ `;
195
+ const rows = db.prepare(sql).all(...params);
196
+ if (rows.length === 0) return [];
197
+ const scored = rows.map((row) => {
198
+ const score = cosineSimilarity(queryEmbedding, deserializeEmbedding(row.embedding));
199
+ return {
200
+ projectId: row.project_id,
201
+ path: row.path,
202
+ startLine: row.start_line,
203
+ endLine: row.end_line,
204
+ snippet: row.text,
205
+ score,
206
+ tier: row.tier,
207
+ source: row.source
208
+ };
209
+ });
210
+ const minScore = opts?.minScore ?? -Infinity;
211
+ return scored.filter((r) => r.score >= minScore).sort((a, b) => b.score - a.score).slice(0, maxResults);
212
+ }
213
+ /**
214
+ * Combine BM25 keyword search and semantic search using normalized scores.
215
+ *
216
+ * Both score sets are min-max normalized to [0,1] before combining, so neither
217
+ * dominates the other regardless of their raw scales.
218
+ *
219
+ * @param queryEmbedding Pre-computed embedding for the query.
220
+ * @param keywordWeight Weight for BM25 score (default 0.5).
221
+ * @param semanticWeight Weight for cosine similarity score (default 0.5).
222
+ */
223
+ function searchMemoryHybrid(db, query, queryEmbedding, opts) {
224
+ const maxResults = opts?.maxResults ?? 10;
225
+ const kw = opts?.keywordWeight ?? .5;
226
+ const sw = opts?.semanticWeight ?? .5;
227
+ const keywordResults = searchMemory(db, query, {
228
+ ...opts,
229
+ maxResults: 50
230
+ });
231
+ const semanticResults = searchMemorySemantic(db, queryEmbedding, {
232
+ ...opts,
233
+ maxResults: 50
234
+ });
235
+ if (keywordResults.length === 0 && semanticResults.length === 0) return [];
236
+ const keyFor = (r) => `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;
237
+ function minMaxNormalize(items) {
238
+ if (items.length === 0) return /* @__PURE__ */ new Map();
239
+ const min = Math.min(...items.map((r) => r.score));
240
+ const range = Math.max(...items.map((r) => r.score)) - min;
241
+ const m = /* @__PURE__ */ new Map();
242
+ for (const r of items) m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);
243
+ return m;
244
+ }
245
+ const kwNorm = minMaxNormalize(keywordResults);
246
+ const semNorm = minMaxNormalize(semanticResults);
247
+ const allKeys = new Set([...keywordResults.map(keyFor), ...semanticResults.map(keyFor)]);
248
+ const metaMap = /* @__PURE__ */ new Map();
249
+ for (const r of [...keywordResults, ...semanticResults]) metaMap.set(keyFor(r), r);
250
+ const combined = [];
251
+ for (const key of allKeys) {
252
+ const meta = metaMap.get(key);
253
+ const kwScore = kwNorm.get(key) ?? 0;
254
+ const semScore = semNorm.get(key) ?? 0;
255
+ const combinedScore = kw * kwScore + sw * semScore;
256
+ combined.push({
257
+ ...meta,
258
+ score: combinedScore,
259
+ combinedScore
260
+ });
261
+ }
262
+ return combined.sort((a, b) => b.score - a.score).slice(0, maxResults).map(({ combinedScore: _unused, ...r }) => r);
263
+ }
264
+ /**
265
+ * Populate the projectSlug field on search results by looking up project IDs
266
+ * in the registry database.
267
+ */
268
+ function populateSlugs(results, registryDb) {
269
+ if (results.length === 0) return results;
270
+ const ids = [...new Set(results.map((r) => r.projectId))];
271
+ const placeholders = ids.map(() => "?").join(", ");
272
+ const rows = registryDb.prepare(`SELECT id, slug FROM projects WHERE id IN (${placeholders})`).all(...ids);
273
+ const slugMap = new Map(rows.map((r) => [r.id, r.slug]));
274
+ return results.map((r) => ({
275
+ ...r,
276
+ projectSlug: slugMap.get(r.projectId)
277
+ }));
278
+ }
279
+
280
+ //#endregion
281
+ export { searchMemorySemantic as a, searchMemoryHybrid as i, populateSlugs as n, search_exports as o, searchMemory as r, buildFtsQuery as t };
282
+ //# sourceMappingURL=search-PjftDxxs.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-PjftDxxs.mjs","names":[],"sources":["../src/memory/search.ts"],"sourcesContent":["/**\n * Search over the PAI federation memory index.\n *\n * Provides three search modes:\n * - keyword — BM25 full-text search (default, fast, no ML required)\n * - semantic — Brute-force cosine similarity over pre-computed embeddings\n * - hybrid — Normalized combination of BM25 + cosine scores\n *\n * BM25 uses SQLite's FTS5 extension. Semantic search requires embeddings to\n * have been generated first via `embedChunks()` in the indexer.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { deserializeEmbedding, cosineSimilarity } from \"./embeddings.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SearchResult {\n projectId: number;\n projectSlug?: string; // populated from registry after search when available\n path: string;\n startLine: number;\n endLine: number;\n snippet: string;\n score: number; // raw BM25 score (lower = more relevant in FTS5)\n tier: string;\n source: string;\n}\n\nexport interface SearchOptions {\n /** Restrict search to these project IDs. */\n projectIds?: number[];\n /** Restrict to 'memory' or 'notes' sources. */\n sources?: string[];\n /** Restrict to specific tier(s): 'evergreen' | 'daily' | 'topic' | 'session' */\n tiers?: string[];\n /** Maximum number of results to return. Default 10. */\n maxResults?: number;\n /** Minimum BM25 score threshold (FTS5 scores are negative; 0.0 means no filter). */\n minScore?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Stop words\n// ---------------------------------------------------------------------------\n\nconst STOP_WORDS = new Set([\n \"a\", \"an\", \"and\", \"are\", \"as\", \"at\", \"be\", \"been\", \"but\", \"by\",\n \"do\", \"for\", \"from\", \"has\", \"have\", \"he\", \"her\", \"him\", \"his\",\n \"how\", \"i\", \"if\", \"in\", \"is\", \"it\", \"its\", \"me\", \"my\", \"not\",\n \"of\", \"on\", \"or\", \"our\", \"out\", \"she\", \"so\", \"that\", \"the\",\n \"their\", \"them\", \"they\", \"this\", \"to\", \"up\", \"us\", \"was\", \"we\",\n \"were\", \"what\", \"when\", \"who\", \"will\", \"with\", \"you\", \"your\",\n]);\n\n// ---------------------------------------------------------------------------\n// Query builder\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a free-text query into an FTS5 query string.\n *\n * Strategy:\n * 1. Tokenise by whitespace and punctuation\n * 2. Remove stop words and tokens shorter than 2 characters\n * 3. Double-quote each remaining token (exact word form)\n * 4. Join with OR so that any matching token returns a result\n *\n * Using OR instead of AND is critical for multi-word queries: the words rarely\n * all appear in the same chunk, so AND would return zero results. FTS5 BM25\n * scoring naturally ranks chunks where more terms match higher, so the most\n * relevant chunks still surface at the top.\n *\n * Example: \"Synchrotech interview follow-up Gilles\"\n * → `\"synchrotech\" OR \"interview\" OR \"follow\" OR \"gilles\"`\n * → chunks matching any term, ranked by how many terms match\n */\nexport function buildFtsQuery(query: string): string {\n const tokens = query\n .toLowerCase()\n .split(/[\\s\\p{P}]+/u)\n .filter(Boolean)\n .filter((t) => t.length >= 2)\n .filter((t) => !STOP_WORDS.has(t))\n // Escape any double-quotes inside the token (FTS5 uses them as delimiters)\n .map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`)\n\n if (tokens.length === 0) {\n // Fallback: use original query as a raw string (may produce no results)\n return `\"${query.replace(/\"/g, '\"\"')}\"`;\n }\n\n return tokens.join(\" OR \");\n}\n\n// ---------------------------------------------------------------------------\n// Search\n// ---------------------------------------------------------------------------\n\n/**\n * Search across all indexed memory using FTS5 BM25 ranking.\n *\n * Results are ordered by BM25 score (most relevant first).\n * FTS5 bm25() returns negative values; closer to 0 = more relevant.\n * We negate the score so callers get positive values where higher = better.\n *\n * Multilingual note: SQLite FTS5 uses the `unicode61` tokenizer by default,\n * which handles Unicode correctly (German umlauts, French accents, etc.) without\n * language-specific stemming. No changes needed here — it is already\n * multilingual-safe.\n */\nexport function searchMemory(\n db: Database,\n query: string,\n opts?: SearchOptions,\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n const ftsQuery = buildFtsQuery(query);\n\n // Build the SQL with optional filters\n const conditions: string[] = [];\n const params: (string | number)[] = [ftsQuery];\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => \"?\").join(\", \");\n conditions.push(`c.project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => \"?\").join(\", \");\n conditions.push(`c.source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => \"?\").join(\", \");\n conditions.push(`c.tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n const whereClause = conditions.length > 0\n ? \"AND \" + conditions.join(\" AND \")\n : \"\";\n\n params.push(maxResults);\n\n // FTS5: join memory_fts with memory_chunks to get metadata\n // bm25(memory_fts) returns negative values (lower = better match)\n const sql = `\n SELECT\n c.project_id,\n c.path,\n c.start_line,\n c.end_line,\n c.text AS snippet,\n c.tier,\n c.source,\n bm25(memory_fts) AS bm25_score\n FROM memory_fts\n JOIN memory_chunks c ON memory_fts.id = c.id\n WHERE memory_fts MATCH ?\n ${whereClause}\n ORDER BY bm25_score\n LIMIT ?\n `;\n\n let rows: Array<{\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n snippet: string;\n tier: string;\n source: string;\n bm25_score: number;\n }>;\n\n try {\n rows = db.prepare(sql).all(...params) as typeof rows;\n } catch {\n // FTS5 MATCH throws when the query is invalid — return empty results\n return [];\n }\n\n const minScore = opts?.minScore ?? 0.0;\n\n return rows\n .map((row) => ({\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.snippet,\n // Negate so higher = better match for callers\n score: -row.bm25_score,\n tier: row.tier,\n source: row.source,\n }))\n .filter((r) => r.score >= minScore);\n}\n\n// ---------------------------------------------------------------------------\n// Semantic search\n// ---------------------------------------------------------------------------\n\n/**\n * Search chunks using brute-force cosine similarity over stored embeddings.\n *\n * Only chunks that have a non-null embedding BLOB are considered. Chunks\n * without embeddings are silently skipped (they can be embedded later via\n * `embedChunks()`).\n *\n * @param queryEmbedding Pre-computed Float32Array for the search query.\n */\nexport function searchMemorySemantic(\n db: Database,\n queryEmbedding: Float32Array,\n opts?: SearchOptions,\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n\n // Build the SQL filter conditions\n const conditions: string[] = [\"embedding IS NOT NULL\"];\n const params: (string | number)[] = [];\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => \"?\").join(\", \");\n conditions.push(`project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => \"?\").join(\", \");\n conditions.push(`source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => \"?\").join(\", \");\n conditions.push(`tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n const where = \"WHERE \" + conditions.join(\" AND \");\n\n // Hard cap for SQLite semantic path — prevents OOM on large corpora.\n // Use Postgres for production semantic search.\n const sql = `\n SELECT id, project_id, path, start_line, end_line, text, tier, source, embedding\n FROM memory_chunks\n ${where}\n LIMIT 5000\n `;\n\n const rows = db.prepare(sql).all(...params) as Array<{\n id: string;\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n text: string;\n tier: string;\n source: string;\n embedding: Buffer;\n }>;\n\n if (rows.length === 0) return [];\n\n // Compute cosine similarity for every chunk\n const scored = rows.map((row) => {\n const vec = deserializeEmbedding(row.embedding);\n const score = cosineSimilarity(queryEmbedding, vec);\n return {\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.text,\n score,\n tier: row.tier,\n source: row.source,\n };\n });\n\n // Sort by descending similarity, apply optional min score filter, limit\n const minScore = opts?.minScore ?? -Infinity;\n\n return scored\n .filter((r) => r.score >= minScore)\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults);\n}\n\n// ---------------------------------------------------------------------------\n// Hybrid search\n// ---------------------------------------------------------------------------\n\n/**\n * Combine BM25 keyword search and semantic search using normalized scores.\n *\n * Both score sets are min-max normalized to [0,1] before combining, so neither\n * dominates the other regardless of their raw scales.\n *\n * @param queryEmbedding Pre-computed embedding for the query.\n * @param keywordWeight Weight for BM25 score (default 0.5).\n * @param semanticWeight Weight for cosine similarity score (default 0.5).\n */\nexport function searchMemoryHybrid(\n db: Database,\n query: string,\n queryEmbedding: Float32Array,\n opts?: SearchOptions & { keywordWeight?: number; semanticWeight?: number },\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n const kw = opts?.keywordWeight ?? 0.5;\n const sw = opts?.semanticWeight ?? 0.5;\n\n // Fetch keyword results — 50 candidates is sufficient for min-max normalization\n const keywordResults = searchMemory(db, query, {\n ...opts,\n maxResults: 50,\n });\n\n // Fetch semantic results — 50 candidates is sufficient for min-max normalization\n const semanticResults = searchMemorySemantic(db, queryEmbedding, {\n ...opts,\n maxResults: 50,\n });\n\n if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n // Build a map of chunk ID → combined result\n // Use \"projectId:path:startLine:endLine\" as a stable key (same as chunk IDs)\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n // Min-max normalize helper\n function minMaxNormalize(items: SearchResult[]): Map<string, number> {\n if (items.length === 0) return new Map();\n const min = Math.min(...items.map((r) => r.score));\n const max = Math.max(...items.map((r) => r.score));\n const range = max - min;\n const m = new Map<string, number>();\n for (const r of items) {\n m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);\n }\n return m;\n }\n\n const kwNorm = minMaxNormalize(keywordResults);\n const semNorm = minMaxNormalize(semanticResults);\n\n // Union of all chunk keys\n const allKeys = new Set<string>([\n ...keywordResults.map(keyFor),\n ...semanticResults.map(keyFor),\n ]);\n\n // Build a lookup from key → result metadata\n const metaMap = new Map<string, SearchResult>();\n for (const r of [...keywordResults, ...semanticResults]) {\n metaMap.set(keyFor(r), r);\n }\n\n // Combine scores\n const combined: Array<SearchResult & { combinedScore: number }> = [];\n for (const key of allKeys) {\n const meta = metaMap.get(key)!;\n const kwScore = kwNorm.get(key) ?? 0;\n const semScore = semNorm.get(key) ?? 0;\n const combinedScore = kw * kwScore + sw * semScore;\n combined.push({ ...meta, score: combinedScore, combinedScore });\n }\n\n // Sort by combined score descending\n return combined\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults)\n .map(({ combinedScore: _unused, ...r }) => r);\n}\n\n// ---------------------------------------------------------------------------\n// Slug lookup helper\n// ---------------------------------------------------------------------------\n\n/**\n * Populate the projectSlug field on search results by looking up project IDs\n * in the registry database.\n */\nexport function populateSlugs(\n results: SearchResult[],\n registryDb: Database,\n): SearchResult[] {\n if (results.length === 0) return results;\n\n const ids = [...new Set(results.map((r) => r.projectId))];\n const placeholders = ids.map(() => \"?\").join(\", \");\n const rows = registryDb\n .prepare(`SELECT id, slug FROM projects WHERE id IN (${placeholders})`)\n .all(...ids) as Array<{ id: number; slug: string }>;\n\n const slugMap = new Map(rows.map((r) => [r.id, r.slug]));\n\n return results.map((r) => ({\n ...r,\n projectSlug: slugMap.get(r.projectId),\n }));\n}\n"],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import "./embeddings-mfqv-jFu.mjs";
2
+ import { a as searchMemorySemantic, r as searchMemory } from "./search-PjftDxxs.mjs";
3
+
4
+ //#region src/storage/sqlite.ts
5
+ var SQLiteBackend = class {
6
+ backendType = "sqlite";
7
+ db;
8
+ constructor(db) {
9
+ this.db = db;
10
+ }
11
+ /**
12
+ * Expose the raw better-sqlite3 Database handle.
13
+ * Used by the daemon to pass to indexAll() which still uses the synchronous API directly.
14
+ */
15
+ getRawDb() {
16
+ return this.db;
17
+ }
18
+ async close() {
19
+ try {
20
+ this.db.close();
21
+ } catch {}
22
+ }
23
+ async getStats() {
24
+ return {
25
+ files: this.db.prepare("SELECT COUNT(*) AS n FROM memory_files").get().n,
26
+ chunks: this.db.prepare("SELECT COUNT(*) AS n FROM memory_chunks").get().n
27
+ };
28
+ }
29
+ async getFileHash(projectId, path) {
30
+ return this.db.prepare("SELECT hash FROM memory_files WHERE project_id = ? AND path = ?").get(projectId, path)?.hash;
31
+ }
32
+ async upsertFile(file) {
33
+ this.db.prepare(`INSERT INTO memory_files (project_id, path, source, tier, hash, mtime, size)
34
+ VALUES (?, ?, ?, ?, ?, ?, ?)
35
+ ON CONFLICT(project_id, path) DO UPDATE SET
36
+ source = excluded.source,
37
+ tier = excluded.tier,
38
+ hash = excluded.hash,
39
+ mtime = excluded.mtime,
40
+ size = excluded.size`).run(file.projectId, file.path, file.source, file.tier, file.hash, file.mtime, file.size);
41
+ }
42
+ async getChunkIds(projectId, path) {
43
+ return this.db.prepare("SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?").all(projectId, path).map((r) => r.id);
44
+ }
45
+ async deleteChunksForFile(projectId, path) {
46
+ const ids = await this.getChunkIds(projectId, path);
47
+ const deleteFts = this.db.prepare("DELETE FROM memory_fts WHERE id = ?");
48
+ const deleteChunks = this.db.prepare("DELETE FROM memory_chunks WHERE project_id = ? AND path = ?");
49
+ this.db.transaction(() => {
50
+ for (const id of ids) deleteFts.run(id);
51
+ deleteChunks.run(projectId, path);
52
+ })();
53
+ }
54
+ async insertChunks(chunks) {
55
+ if (chunks.length === 0) return;
56
+ const insertChunk = this.db.prepare(`INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)
57
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
58
+ const insertFts = this.db.prepare(`INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)
59
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
60
+ this.db.transaction(() => {
61
+ for (const c of chunks) {
62
+ insertChunk.run(c.id, c.projectId, c.source, c.tier, c.path, c.startLine, c.endLine, c.hash, c.text, c.updatedAt);
63
+ insertFts.run(c.text, c.id, c.projectId, c.path, c.source, c.tier, c.startLine, c.endLine);
64
+ }
65
+ })();
66
+ }
67
+ async getUnembeddedChunkIds(projectId) {
68
+ const conditions = ["embedding IS NULL"];
69
+ const params = [];
70
+ if (projectId !== void 0) {
71
+ conditions.push("project_id = ?");
72
+ params.push(projectId);
73
+ }
74
+ const where = "WHERE " + conditions.join(" AND ");
75
+ return this.db.prepare(`SELECT id, text FROM memory_chunks ${where} ORDER BY id`).all(...params);
76
+ }
77
+ async updateEmbedding(chunkId, embedding) {
78
+ this.db.prepare("UPDATE memory_chunks SET embedding = ? WHERE id = ?").run(embedding, chunkId);
79
+ }
80
+ async searchKeyword(query, opts) {
81
+ return searchMemory(this.db, query, opts);
82
+ }
83
+ async searchSemantic(queryEmbedding, opts) {
84
+ return searchMemorySemantic(this.db, queryEmbedding, opts);
85
+ }
86
+ };
87
+
88
+ //#endregion
89
+ export { SQLiteBackend };
90
+ //# sourceMappingURL=sqlite-CHUrNtbI.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-CHUrNtbI.mjs","names":[],"sources":["../src/storage/sqlite.ts"],"sourcesContent":["/**\n * SQLiteBackend — wraps the existing better-sqlite3 federation.db\n * behind the StorageBackend interface.\n *\n * This is a thin adapter. The heavy lifting is all in the existing\n * memory/indexer.ts and memory/search.ts code; we just provide a\n * backend-agnostic surface so the daemon and tools can call either\n * SQLite or Postgres transparently.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend, ChunkRow, FileRow, FederationStats } from \"./interface.js\";\nimport type { SearchResult, SearchOptions } from \"../memory/search.js\";\nimport { searchMemory, searchMemorySemantic } from \"../memory/search.js\";\n\nexport class SQLiteBackend implements StorageBackend {\n readonly backendType = \"sqlite\" as const;\n\n private db: Database;\n\n constructor(db: Database) {\n this.db = db;\n }\n\n /**\n * Expose the raw better-sqlite3 Database handle.\n * Used by the daemon to pass to indexAll() which still uses the synchronous API directly.\n */\n getRawDb(): Database {\n return this.db;\n }\n\n // -------------------------------------------------------------------------\n // Lifecycle\n // -------------------------------------------------------------------------\n\n async close(): Promise<void> {\n try {\n this.db.close();\n } catch {\n // ignore\n }\n }\n\n async getStats(): Promise<FederationStats> {\n const files = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_files\").get() as { n: number }\n ).n;\n const chunks = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_chunks\").get() as { n: number }\n ).n;\n return { files, chunks };\n }\n\n // -------------------------------------------------------------------------\n // File tracking\n // -------------------------------------------------------------------------\n\n async getFileHash(projectId: number, path: string): Promise<string | undefined> {\n const row = this.db\n .prepare(\"SELECT hash FROM memory_files WHERE project_id = ? AND path = ?\")\n .get(projectId, path) as { hash: string } | undefined;\n return row?.hash;\n }\n\n async upsertFile(file: FileRow): Promise<void> {\n this.db\n .prepare(\n `INSERT INTO memory_files (project_id, path, source, tier, hash, mtime, size)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(project_id, path) DO UPDATE SET\n source = excluded.source,\n tier = excluded.tier,\n hash = excluded.hash,\n mtime = excluded.mtime,\n size = excluded.size`\n )\n .run(file.projectId, file.path, file.source, file.tier, file.hash, file.mtime, file.size);\n }\n\n // -------------------------------------------------------------------------\n // Chunk management\n // -------------------------------------------------------------------------\n\n async getChunkIds(projectId: number, path: string): Promise<string[]> {\n const rows = this.db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, path) as Array<{ id: string }>;\n return rows.map((r) => r.id);\n }\n\n async deleteChunksForFile(projectId: number, path: string): Promise<void> {\n const ids = await this.getChunkIds(projectId, path);\n const deleteFts = this.db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = this.db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\"\n );\n this.db.transaction(() => {\n for (const id of ids) {\n deleteFts.run(id);\n }\n deleteChunks.run(projectId, path);\n })();\n }\n\n async insertChunks(chunks: ChunkRow[]): Promise<void> {\n if (chunks.length === 0) return;\n\n const insertChunk = this.db.prepare(\n `INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`\n );\n const insertFts = this.db.prepare(\n `INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n );\n\n this.db.transaction(() => {\n for (const c of chunks) {\n insertChunk.run(\n c.id,\n c.projectId,\n c.source,\n c.tier,\n c.path,\n c.startLine,\n c.endLine,\n c.hash,\n c.text,\n c.updatedAt\n );\n insertFts.run(\n c.text,\n c.id,\n c.projectId,\n c.path,\n c.source,\n c.tier,\n c.startLine,\n c.endLine\n );\n }\n })();\n }\n\n async getUnembeddedChunkIds(projectId?: number): Promise<Array<{ id: string; text: string }>> {\n const conditions = [\"embedding IS NULL\"];\n const params: (string | number)[] = [];\n\n if (projectId !== undefined) {\n conditions.push(\"project_id = ?\");\n params.push(projectId);\n }\n\n const where = \"WHERE \" + conditions.join(\" AND \");\n const rows = this.db\n .prepare(`SELECT id, text FROM memory_chunks ${where} ORDER BY id`)\n .all(...params) as Array<{ id: string; text: string }>;\n return rows;\n }\n\n async updateEmbedding(chunkId: string, embedding: Buffer): Promise<void> {\n this.db\n .prepare(\"UPDATE memory_chunks SET embedding = ? WHERE id = ?\")\n .run(embedding, chunkId);\n }\n\n // -------------------------------------------------------------------------\n // Search\n // -------------------------------------------------------------------------\n\n async searchKeyword(query: string, opts?: SearchOptions): Promise<SearchResult[]> {\n return searchMemory(this.db, query, opts);\n }\n\n async searchSemantic(queryEmbedding: Float32Array, opts?: SearchOptions): Promise<SearchResult[]> {\n return searchMemorySemantic(this.db, queryEmbedding, opts);\n }\n}\n"],"mappings":";;;;AAeA,IAAa,gBAAb,MAAqD;CACnD,AAAS,cAAc;CAEvB,AAAQ;CAER,YAAY,IAAc;AACxB,OAAK,KAAK;;;;;;CAOZ,WAAqB;AACnB,SAAO,KAAK;;CAOd,MAAM,QAAuB;AAC3B,MAAI;AACF,QAAK,GAAG,OAAO;UACT;;CAKV,MAAM,WAAqC;AAOzC,SAAO;GAAE,OALP,KAAK,GAAG,QAAQ,yCAAyC,CAAC,KAAK,CAC/D;GAIc,QAFd,KAAK,GAAG,QAAQ,0CAA0C,CAAC,KAAK,CAChE;GACsB;;CAO1B,MAAM,YAAY,WAAmB,MAA2C;AAI9E,SAHY,KAAK,GACd,QAAQ,kEAAkE,CAC1E,IAAI,WAAW,KAAK,EACX;;CAGd,MAAM,WAAW,MAA8B;AAC7C,OAAK,GACF,QACC;;;;;;;mCAQD,CACA,IAAI,KAAK,WAAW,KAAK,MAAM,KAAK,QAAQ,KAAK,MAAM,KAAK,MAAM,KAAK,OAAO,KAAK,KAAK;;CAO7F,MAAM,YAAY,WAAmB,MAAiC;AAIpE,SAHa,KAAK,GACf,QAAQ,iEAAiE,CACzE,IAAI,WAAW,KAAK,CACX,KAAK,MAAM,EAAE,GAAG;;CAG9B,MAAM,oBAAoB,WAAmB,MAA6B;EACxE,MAAM,MAAM,MAAM,KAAK,YAAY,WAAW,KAAK;EACnD,MAAM,YAAY,KAAK,GAAG,QAAQ,sCAAsC;EACxE,MAAM,eAAe,KAAK,GAAG,QAC3B,8DACD;AACD,OAAK,GAAG,kBAAkB;AACxB,QAAK,MAAM,MAAM,IACf,WAAU,IAAI,GAAG;AAEnB,gBAAa,IAAI,WAAW,KAAK;IACjC,EAAE;;CAGN,MAAM,aAAa,QAAmC;AACpD,MAAI,OAAO,WAAW,EAAG;EAEzB,MAAM,cAAc,KAAK,GAAG,QAC1B;8CAED;EACD,MAAM,YAAY,KAAK,GAAG,QACxB;wCAED;AAED,OAAK,GAAG,kBAAkB;AACxB,QAAK,MAAM,KAAK,QAAQ;AACtB,gBAAY,IACV,EAAE,IACF,EAAE,WACF,EAAE,QACF,EAAE,MACF,EAAE,MACF,EAAE,WACF,EAAE,SACF,EAAE,MACF,EAAE,MACF,EAAE,UACH;AACD,cAAU,IACR,EAAE,MACF,EAAE,IACF,EAAE,WACF,EAAE,MACF,EAAE,QACF,EAAE,MACF,EAAE,WACF,EAAE,QACH;;IAEH,EAAE;;CAGN,MAAM,sBAAsB,WAAkE;EAC5F,MAAM,aAAa,CAAC,oBAAoB;EACxC,MAAM,SAA8B,EAAE;AAEtC,MAAI,cAAc,QAAW;AAC3B,cAAW,KAAK,iBAAiB;AACjC,UAAO,KAAK,UAAU;;EAGxB,MAAM,QAAQ,WAAW,WAAW,KAAK,QAAQ;AAIjD,SAHa,KAAK,GACf,QAAQ,sCAAsC,MAAM,cAAc,CAClE,IAAI,GAAG,OAAO;;CAInB,MAAM,gBAAgB,SAAiB,WAAkC;AACvE,OAAK,GACF,QAAQ,sDAAsD,CAC9D,IAAI,WAAW,QAAQ;;CAO5B,MAAM,cAAc,OAAe,MAA+C;AAChF,SAAO,aAAa,KAAK,IAAI,OAAO,KAAK;;CAG3C,MAAM,eAAe,gBAA8B,MAA+C;AAChG,SAAO,qBAAqB,KAAK,IAAI,gBAAgB,KAAK"}