claude-transcript-viewer 1.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 (44) hide show
  1. package/README.md +215 -0
  2. package/dist/api/search.d.ts +50 -0
  3. package/dist/api/search.js +181 -0
  4. package/dist/api/search.js.map +1 -0
  5. package/dist/api/snippets.d.ts +2 -0
  6. package/dist/api/snippets.js +49 -0
  7. package/dist/api/snippets.js.map +1 -0
  8. package/dist/config.d.ts +14 -0
  9. package/dist/config.js +52 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/db/chunks.d.ts +14 -0
  12. package/dist/db/chunks.js +43 -0
  13. package/dist/db/chunks.js.map +1 -0
  14. package/dist/db/conversations.d.ts +16 -0
  15. package/dist/db/conversations.js +40 -0
  16. package/dist/db/conversations.js.map +1 -0
  17. package/dist/db/index.d.ts +6 -0
  18. package/dist/db/index.js +40 -0
  19. package/dist/db/index.js.map +1 -0
  20. package/dist/db/schema.d.ts +5 -0
  21. package/dist/db/schema.js +71 -0
  22. package/dist/db/schema.js.map +1 -0
  23. package/dist/embeddings/client.d.ts +16 -0
  24. package/dist/embeddings/client.js +155 -0
  25. package/dist/embeddings/client.js.map +1 -0
  26. package/dist/indexer/changeDetection.d.ts +16 -0
  27. package/dist/indexer/changeDetection.js +81 -0
  28. package/dist/indexer/changeDetection.js.map +1 -0
  29. package/dist/indexer/chunker.d.ts +5 -0
  30. package/dist/indexer/chunker.js +44 -0
  31. package/dist/indexer/chunker.js.map +1 -0
  32. package/dist/indexer/fileUtils.d.ts +2 -0
  33. package/dist/indexer/fileUtils.js +9 -0
  34. package/dist/indexer/fileUtils.js.map +1 -0
  35. package/dist/indexer/index.d.ts +19 -0
  36. package/dist/indexer/index.js +267 -0
  37. package/dist/indexer/index.js.map +1 -0
  38. package/dist/indexer/parser.d.ts +12 -0
  39. package/dist/indexer/parser.js +45 -0
  40. package/dist/indexer/parser.js.map +1 -0
  41. package/dist/server.d.ts +2 -0
  42. package/dist/server.js +1851 -0
  43. package/dist/server.js.map +1 -0
  44. package/package.json +62 -0
package/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # Claude Transcript Viewer
2
+
3
+ A web server for browsing and searching Claude Code conversation transcripts with semantic search, infinite scroll, and collapsible cells.
4
+
5
+ ## Features
6
+
7
+ - **Semantic Search** - Hybrid FTS + vector search with highlighted snippets
8
+ - **Auto Archive Generation** - Automatically generates HTML from JSONL transcripts on startup
9
+ - **Background Indexing** - Non-blocking indexing of conversations for search
10
+ - **Enhanced Viewing** - Collapsible cells, preview text, infinite scroll
11
+ - **Search Everywhere** - Search bar on every page with live dropdown results
12
+ - **Full Search Page** - Dedicated search results page with filters (project, role, date)
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ # Install dependencies
18
+ npm install
19
+
20
+ # Start with auto-generation (recommended)
21
+ SOURCE_DIR=~/.claude/projects npm run dev
22
+
23
+ # Or specify output directory
24
+ ARCHIVE_DIR=./archive SOURCE_DIR=~/.claude/projects npm run dev
25
+ ```
26
+
27
+ Open http://localhost:3000 to browse transcripts.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ git clone <repo>
33
+ cd claude-transcript-viewer
34
+ npm install
35
+ ```
36
+
37
+ ### Requirements
38
+
39
+ - Node.js 18+
40
+ - [claude-code-transcripts](https://github.com/simonw/claude-code-transcripts) Python CLI (for HTML generation)
41
+
42
+ ```bash
43
+ # Install the Python CLI
44
+ pip install claude-code-transcripts
45
+ # or
46
+ uv pip install claude-code-transcripts
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ### Basic Usage
52
+
53
+ ```bash
54
+ # Auto-generate archive and start server
55
+ SOURCE_DIR=~/.claude/projects npm run dev
56
+
57
+ # Use existing archive (no generation)
58
+ npm run dev -- /path/to/existing/archive
59
+
60
+ # Specify all paths explicitly
61
+ ARCHIVE_DIR=./archive \
62
+ SOURCE_DIR=~/.claude/projects \
63
+ DATABASE_PATH=./search.db \
64
+ npm run dev
65
+ ```
66
+
67
+ ### Manual Indexing
68
+
69
+ If you only want to index without running the server:
70
+
71
+ ```bash
72
+ npm run index ~/.claude/projects ./search.db
73
+ ```
74
+
75
+ ### With Embedding Server
76
+
77
+ For semantic vector search (optional), run a compatible embedding server:
78
+
79
+ ```bash
80
+ # Start embedding server (e.g., qwen3-embeddings-mlx)
81
+ EMBED_SOCKET=/tmp/qwen-embed.sock npm run dev
82
+ ```
83
+
84
+ ## Configuration
85
+
86
+ | Environment Variable | Default | Description |
87
+ |---------------------|---------|-------------|
88
+ | `PORT` | `3000` | Server port |
89
+ | `ARCHIVE_DIR` | `./claude-archive` | Directory for HTML files |
90
+ | `SOURCE_DIR` | (none) | JSONL source directory (enables auto-generation) |
91
+ | `DATABASE_PATH` | `ARCHIVE_DIR/.search.db` | SQLite database path |
92
+ | `EMBED_SOCKET` | `/tmp/qwen-embed.sock` | Unix socket for embedding server |
93
+
94
+ ## API Endpoints
95
+
96
+ ### Search
97
+
98
+ ```bash
99
+ # Search conversations
100
+ GET /api/search?q=sqlite&limit=20
101
+
102
+ # With filters
103
+ GET /api/search?q=typescript&project=my-project&role=assistant
104
+
105
+ # Parameters:
106
+ # - q: Search query
107
+ # - project: Filter by project name
108
+ # - role: Filter by role (user/assistant)
109
+ # - after: Filter by date (YYYY-MM-DD)
110
+ # - before: Filter by date (YYYY-MM-DD)
111
+ # - limit: Max results (default: 20)
112
+ # - offset: Pagination offset
113
+ ```
114
+
115
+ ### Status
116
+
117
+ ```bash
118
+ # Get index and archive status
119
+ GET /api/index/status
120
+
121
+ # Response:
122
+ {
123
+ "status": "ready",
124
+ "conversations": 3471,
125
+ "chunks": 596168,
126
+ "embedding_server": "unavailable",
127
+ "archive": {
128
+ "isGenerating": false,
129
+ "progress": "Complete",
130
+ "lastRun": "2026-01-21T19:21:27Z"
131
+ },
132
+ "indexing": {
133
+ "isIndexing": false,
134
+ "progress": "Complete",
135
+ "lastStats": { "added": 37, "modified": 0, "deleted": 0, "chunks": 16513 }
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### Manual Triggers
141
+
142
+ ```bash
143
+ # Regenerate HTML archive and re-index
144
+ POST /api/archive/regenerate
145
+
146
+ # Re-index only (no HTML regeneration)
147
+ POST /api/index/reindex
148
+ ```
149
+
150
+ ## Pages
151
+
152
+ | Route | Description |
153
+ |-------|-------------|
154
+ | `/` | Landing page with projects and recent conversations |
155
+ | `/search?q=...` | Full search results page with filters |
156
+ | `/*.html` | Enhanced transcript pages with search bar |
157
+
158
+ ## Architecture
159
+
160
+ ```
161
+ JSONL files ─┬─> claude-code-transcripts (Python) ─> HTML files
162
+
163
+ └─> transcript-viewer indexer ─> SQLite (FTS + vectors)
164
+
165
+ HTML files ─> Express server ─> Enhanced HTML + Search API
166
+ ```
167
+
168
+ ### Search Pipeline
169
+
170
+ 1. **FTS5** - Full-text search with trigram tokenizer for substring matching
171
+ 2. **Vector Search** - Semantic similarity using sqlite-vec (when embedding server available)
172
+ 3. **RRF Merge** - Reciprocal Rank Fusion combines both result sets
173
+ 4. **Snippets** - Generates highlighted snippets around matched terms
174
+
175
+ ## Development
176
+
177
+ ```bash
178
+ # Run tests
179
+ npm test
180
+
181
+ # Run tests in watch mode
182
+ npm run test:watch
183
+
184
+ # Build for production
185
+ npm run build
186
+
187
+ # Start production server
188
+ npm start
189
+ ```
190
+
191
+ ### Project Structure
192
+
193
+ ```
194
+ src/
195
+ ├── server.ts # Express server, routes, HTML enhancement
196
+ ├── api/
197
+ │ ├── search.ts # Hybrid search (FTS + vector + RRF)
198
+ │ └── snippets.ts # Snippet generation and highlighting
199
+ ├── db/
200
+ │ ├── index.ts # Database initialization
201
+ │ ├── schema.ts # Tables, indexes, triggers
202
+ │ ├── chunks.ts # Chunk CRUD operations
203
+ │ └── conversations.ts # Conversation CRUD operations
204
+ ├── indexer/
205
+ │ ├── index.ts # Main indexing logic
206
+ │ ├── parser.ts # JSONL transcript parser
207
+ │ ├── chunker.ts # Text chunking
208
+ │ └── fileUtils.ts # File hashing, mtime detection
209
+ └── embeddings/
210
+ └── client.ts # Unix socket client for embedding server
211
+ ```
212
+
213
+ ## License
214
+
215
+ MIT
@@ -0,0 +1,50 @@
1
+ import { Conversation } from '../db/conversations.js';
2
+ import { EmbeddingClient } from '../embeddings/client.js';
3
+ export interface SearchOptions {
4
+ project?: string;
5
+ role?: 'user' | 'assistant';
6
+ after?: string;
7
+ before?: string;
8
+ limit?: number;
9
+ offset?: number;
10
+ }
11
+ export interface SearchResult {
12
+ chunk_id: number;
13
+ conversation_id: string;
14
+ title: string | null;
15
+ project: string;
16
+ role: string;
17
+ content: string;
18
+ page_number: number | null;
19
+ score: number;
20
+ }
21
+ export declare function searchVector(queryEmbedding: Buffer, opts: SearchOptions): SearchResult[];
22
+ export declare function searchFTS(query: string, opts: SearchOptions): SearchResult[];
23
+ export interface RRFOptions {
24
+ limit?: number;
25
+ k?: number;
26
+ }
27
+ /**
28
+ * Merge results from vector and FTS search using Reciprocal Rank Fusion.
29
+ * RRF combines rankings by computing: score(doc) = Σ 1/(k + rank)
30
+ * Documents appearing in both result sets get boosted scores.
31
+ */
32
+ export declare function mergeResultsRRF(vectorResults: SearchResult[], ftsResults: SearchResult[], options?: RRFOptions): SearchResult[];
33
+ export interface HybridSearchResult {
34
+ type: 'hybrid' | 'fts_only' | 'recent';
35
+ results?: SearchResult[];
36
+ conversations?: Conversation[];
37
+ embeddingStatus?: 'available' | 'unavailable';
38
+ }
39
+ /**
40
+ * Perform hybrid search combining vector and FTS results with RRF.
41
+ * Falls back to FTS-only when embeddings unavailable.
42
+ * Returns recent conversations for empty queries.
43
+ */
44
+ export declare function searchHybrid(query: string, opts: SearchOptions, embeddingClient?: EmbeddingClient): Promise<HybridSearchResult>;
45
+ export interface SearchWithFallbackResult {
46
+ type: 'search' | 'recent';
47
+ results?: SearchResult[];
48
+ conversations?: Conversation[];
49
+ }
50
+ export declare function searchWithFallback(query: string, opts: SearchOptions): SearchWithFallbackResult;
@@ -0,0 +1,181 @@
1
+ import { getDatabase } from '../db/index.js';
2
+ import { sanitizeFTSQuery } from '../db/chunks.js';
3
+ import { getRecentConversations, listConversations, } from '../db/conversations.js';
4
+ export function searchVector(queryEmbedding, opts) {
5
+ const db = getDatabase();
6
+ const { project, role, after, before, limit = 100, offset = 0 } = opts;
7
+ if (limit < 0 || offset < 0)
8
+ return [];
9
+ let sql = `
10
+ SELECT
11
+ c.id as chunk_id,
12
+ c.conversation_id,
13
+ conv.title,
14
+ conv.project,
15
+ c.role,
16
+ c.content,
17
+ c.page_number,
18
+ vec_distance_cosine(cv.embedding, ?) as score
19
+ FROM chunks c
20
+ JOIN chunks_vec cv ON cv.rowid = c.id
21
+ JOIN conversations conv ON conv.id = c.conversation_id
22
+ WHERE 1=1
23
+ `;
24
+ const params = [queryEmbedding];
25
+ if (project) {
26
+ sql += ' AND conv.project = ?';
27
+ params.push(project);
28
+ }
29
+ if (role) {
30
+ sql += ' AND c.role = ?';
31
+ params.push(role);
32
+ }
33
+ if (after) {
34
+ sql += ' AND conv.created_at >= ?';
35
+ params.push(after);
36
+ }
37
+ if (before) {
38
+ sql += ' AND conv.created_at <= ?';
39
+ params.push(before);
40
+ }
41
+ sql += ' ORDER BY score ASC LIMIT ? OFFSET ?';
42
+ params.push(limit, offset);
43
+ return db.prepare(sql).all(...params);
44
+ }
45
+ export function searchFTS(query, opts) {
46
+ const db = getDatabase();
47
+ const { project, role, after, before, limit = 20, offset = 0 } = opts;
48
+ if (limit < 0 || offset < 0)
49
+ return [];
50
+ const sanitized = sanitizeFTSQuery(query);
51
+ if (!sanitized)
52
+ return [];
53
+ let sql = `
54
+ SELECT
55
+ c.id as chunk_id,
56
+ c.conversation_id,
57
+ conv.title,
58
+ conv.project,
59
+ c.role,
60
+ c.content,
61
+ c.page_number,
62
+ bm25(chunks_fts) as score
63
+ FROM chunks_fts fts
64
+ JOIN chunks c ON c.id = fts.rowid
65
+ JOIN conversations conv ON conv.id = c.conversation_id
66
+ WHERE chunks_fts MATCH ?
67
+ `;
68
+ const params = [sanitized];
69
+ if (project) {
70
+ sql += ' AND conv.project = ?';
71
+ params.push(project);
72
+ }
73
+ if (role) {
74
+ sql += ' AND c.role = ?';
75
+ params.push(role);
76
+ }
77
+ if (after) {
78
+ sql += ' AND conv.created_at >= ?';
79
+ params.push(after);
80
+ }
81
+ if (before) {
82
+ sql += ' AND conv.created_at <= ?';
83
+ params.push(before);
84
+ }
85
+ sql += ' ORDER BY score LIMIT ? OFFSET ?';
86
+ params.push(limit, offset);
87
+ return db.prepare(sql).all(...params);
88
+ }
89
+ /**
90
+ * Merge results from vector and FTS search using Reciprocal Rank Fusion.
91
+ * RRF combines rankings by computing: score(doc) = Σ 1/(k + rank)
92
+ * Documents appearing in both result sets get boosted scores.
93
+ */
94
+ export function mergeResultsRRF(vectorResults, ftsResults, options = {}) {
95
+ const { limit = 20, k = 60 } = options;
96
+ // Map chunk_id to combined RRF score and first-seen result data
97
+ const scores = new Map();
98
+ const resultData = new Map();
99
+ // Process vector results
100
+ vectorResults.forEach((result, rank) => {
101
+ const rrfScore = 1 / (k + rank);
102
+ scores.set(result.chunk_id, (scores.get(result.chunk_id) || 0) + rrfScore);
103
+ if (!resultData.has(result.chunk_id)) {
104
+ resultData.set(result.chunk_id, result);
105
+ }
106
+ });
107
+ // Process FTS results
108
+ ftsResults.forEach((result, rank) => {
109
+ const rrfScore = 1 / (k + rank);
110
+ scores.set(result.chunk_id, (scores.get(result.chunk_id) || 0) + rrfScore);
111
+ if (!resultData.has(result.chunk_id)) {
112
+ resultData.set(result.chunk_id, result);
113
+ }
114
+ });
115
+ // Sort by combined score (descending) and limit
116
+ const sortedIds = [...scores.entries()]
117
+ .sort((a, b) => b[1] - a[1])
118
+ .slice(0, limit)
119
+ .map(([id]) => id);
120
+ // Build final results with updated scores
121
+ return sortedIds.map((id) => ({
122
+ ...resultData.get(id),
123
+ score: scores.get(id),
124
+ }));
125
+ }
126
+ /**
127
+ * Perform hybrid search combining vector and FTS results with RRF.
128
+ * Falls back to FTS-only when embeddings unavailable.
129
+ * Returns recent conversations for empty queries.
130
+ */
131
+ export async function searchHybrid(query, opts, embeddingClient) {
132
+ const trimmed = query.trim();
133
+ // Empty query: return recent conversations
134
+ if (!trimmed) {
135
+ const conversations = opts.project
136
+ ? listConversations(opts.project)
137
+ : getRecentConversations(opts.limit || 20);
138
+ return { type: 'recent', conversations };
139
+ }
140
+ // Try to get embedding for the query
141
+ let queryEmbedding = null;
142
+ if (embeddingClient) {
143
+ const embeddingResult = await embeddingClient.embed(trimmed);
144
+ if (embeddingResult) {
145
+ // Convert number[] to Float32Array buffer
146
+ queryEmbedding = Buffer.from(new Float32Array(embeddingResult.embedding).buffer);
147
+ }
148
+ }
149
+ // Always get FTS results
150
+ const ftsResults = searchFTS(query, { ...opts, limit: 100 });
151
+ // If we have embeddings, do hybrid search
152
+ if (queryEmbedding) {
153
+ const vectorResults = searchVector(queryEmbedding, { ...opts, limit: 100 });
154
+ const merged = mergeResultsRRF(vectorResults, ftsResults, {
155
+ limit: opts.limit || 20,
156
+ });
157
+ return {
158
+ type: 'hybrid',
159
+ results: merged,
160
+ embeddingStatus: 'available',
161
+ };
162
+ }
163
+ // Fallback to FTS-only
164
+ return {
165
+ type: 'fts_only',
166
+ results: ftsResults.slice(0, opts.limit || 20),
167
+ embeddingStatus: 'unavailable',
168
+ };
169
+ }
170
+ export function searchWithFallback(query, opts) {
171
+ const trimmed = query.trim();
172
+ if (!trimmed) {
173
+ const conversations = opts.project
174
+ ? listConversations(opts.project)
175
+ : getRecentConversations(opts.limit || 20);
176
+ return { type: 'recent', conversations };
177
+ }
178
+ const results = searchFTS(query, opts);
179
+ return { type: 'search', results };
180
+ }
181
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/api/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EACL,sBAAsB,EACtB,iBAAiB,GAElB,MAAM,wBAAwB,CAAC;AAuBhC,MAAM,UAAU,YAAY,CAC1B,cAAsB,EACtB,IAAmB;IAEnB,MAAM,EAAE,GAAG,WAAW,EAAE,CAAC;IACzB,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAG,GAAG,EAAE,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC;IAEvE,IAAI,KAAK,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvC,IAAI,GAAG,GAAG;;;;;;;;;;;;;;GAcT,CAAC;IACF,MAAM,MAAM,GAAiC,CAAC,cAAc,CAAC,CAAC;IAE9D,IAAI,OAAO,EAAE,CAAC;QACZ,GAAG,IAAI,uBAAuB,CAAC;QAC/B,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IACD,IAAI,IAAI,EAAE,CAAC;QACT,GAAG,IAAI,iBAAiB,CAAC;QACzB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;IACD,IAAI,KAAK,EAAE,CAAC;QACV,GAAG,IAAI,2BAA2B,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,IAAI,MAAM,EAAE,CAAC;QACX,GAAG,IAAI,2BAA2B,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC;IAED,GAAG,IAAI,sCAAsC,CAAC;IAC9C,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAE3B,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAmB,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,IAAmB;IAC1D,MAAM,EAAE,GAAG,WAAW,EAAE,CAAC;IACzB,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAG,EAAE,EAAE,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC;IAEtE,IAAI,KAAK,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvC,MAAM,SAAS,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,CAAC;IAE1B,IAAI,GAAG,GAAG;;;;;;;;;;;;;;GAcT,CAAC;IACF,MAAM,MAAM,GAAwB,CAAC,SAAS,CAAC,CAAC;IAEhD,IAAI,OAAO,EAAE,CAAC;QACZ,GAAG,IAAI,uBAAuB,CAAC;QAC/B,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IACD,IAAI,IAAI,EAAE,CAAC;QACT,GAAG,IAAI,iBAAiB,CAAC;QACzB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;IACD,IAAI,KAAK,EAAE,CAAC;QACV,GAAG,IAAI,2BAA2B,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,IAAI,MAAM,EAAE,CAAC;QACX,GAAG,IAAI,2BAA2B,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC;IAED,GAAG,IAAI,kCAAkC,CAAC;IAC1C,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAE3B,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAmB,CAAC;AAC1D,CAAC;AAOD;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAC7B,aAA6B,EAC7B,UAA0B,EAC1B,UAAsB,EAAE;IAExB,MAAM,EAAE,KAAK,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC;IAEvC,gEAAgE;IAChE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAwB,CAAC;IAEnD,yBAAyB;IACzB,aAAa,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE;QACrC,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC;QAC3E,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,sBAAsB;IACtB,UAAU,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE;QAClC,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC;QAC3E,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,gDAAgD;IAChD,MAAM,SAAS,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;SACpC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;SAC3B,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;IAErB,0CAA0C;IAC1C,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAC5B,GAAG,UAAU,CAAC,GAAG,CAAC,EAAE,CAAE;QACtB,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAE;KACvB,CAAC,CAAC,CAAC;AACN,CAAC;AASD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,KAAa,EACb,IAAmB,EACnB,eAAiC;IAEjC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAE7B,2CAA2C;IAC3C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO;YAChC,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC;YACjC,CAAC,CAAC,sBAAsB,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QAC7C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC3C,CAAC;IAED,qCAAqC;IACrC,IAAI,cAAc,GAAkB,IAAI,CAAC;IACzC,IAAI,eAAe,EAAE,CAAC;QACpB,MAAM,eAAe,GAAG,MAAM,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7D,IAAI,eAAe,EAAE,CAAC;YACpB,0CAA0C;YAC1C,cAAc,GAAG,MAAM,CAAC,IAAI,CAC1B,IAAI,YAAY,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,MAAM,CACnD,CAAC;QACJ,CAAC;IACH,CAAC;IAED,yBAAyB;IACzB,MAAM,UAAU,GAAG,SAAS,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAE7D,0CAA0C;IAC1C,IAAI,cAAc,EAAE,CAAC;QACnB,MAAM,aAAa,GAAG,YAAY,CAAC,cAAc,EAAE,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QAC5E,MAAM,MAAM,GAAG,eAAe,CAAC,aAAa,EAAE,UAAU,EAAE;YACxD,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE;SACxB,CAAC,CAAC;QACH,OAAO;YACL,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,MAAM;YACf,eAAe,EAAE,WAAW;SAC7B,CAAC;IACJ,CAAC;IAED,uBAAuB;IACvB,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC9C,eAAe,EAAE,aAAa;KAC/B,CAAC;AACJ,CAAC;AAQD,MAAM,UAAU,kBAAkB,CAChC,KAAa,EACb,IAAmB;IAEnB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAE7B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO;YAChC,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC;YACjC,CAAC,CAAC,sBAAsB,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QAC7C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC3C,CAAC;IAED,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACvC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AACrC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function generateSnippet(content: string, query: string, contextChars?: number): string;
2
+ export declare function highlightTerms(text: string, terms: string[]): string;
@@ -0,0 +1,49 @@
1
+ export function generateSnippet(content, query, contextChars = 75) {
2
+ // Strip markdown bold markers to avoid double-highlighting
3
+ const cleanContent = content.replace(/\*\*/g, '');
4
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
5
+ const lower = cleanContent.toLowerCase();
6
+ let matchIdx = -1;
7
+ for (const term of terms) {
8
+ const idx = lower.indexOf(term);
9
+ if (idx !== -1 && (matchIdx === -1 || idx < matchIdx)) {
10
+ matchIdx = idx;
11
+ }
12
+ }
13
+ if (matchIdx === -1) {
14
+ const maxLen = contextChars * 2;
15
+ return cleanContent.length > maxLen
16
+ ? cleanContent.slice(0, maxLen) + '...'
17
+ : cleanContent;
18
+ }
19
+ const start = Math.max(0, matchIdx - contextChars);
20
+ const end = Math.min(cleanContent.length, matchIdx + contextChars);
21
+ let snippet = cleanContent.slice(start, end);
22
+ if (start > 0)
23
+ snippet = '...' + snippet;
24
+ if (end < cleanContent.length)
25
+ snippet = snippet + '...';
26
+ return snippet;
27
+ }
28
+ function escapeHtml(str) {
29
+ return str
30
+ .replace(/&/g, "&amp;")
31
+ .replace(/</g, "&lt;")
32
+ .replace(/>/g, "&gt;")
33
+ .replace(/"/g, "&quot;")
34
+ .replace(/'/g, "&#39;");
35
+ }
36
+ export function highlightTerms(text, terms) {
37
+ // First escape HTML to prevent XSS
38
+ let result = escapeHtml(text);
39
+ // Then wrap matched terms in <strong> tags
40
+ for (const term of terms) {
41
+ if (!term)
42
+ continue;
43
+ const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
44
+ const regex = new RegExp(`(${escaped})`, 'gi');
45
+ result = result.replace(regex, '<strong>$1</strong>');
46
+ }
47
+ return result;
48
+ }
49
+ //# sourceMappingURL=snippets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snippets.js","sourceRoot":"","sources":["../../src/api/snippets.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,KAAa,EAAE,YAAY,GAAG,EAAE;IAC/E,2DAA2D;IAC3D,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,YAAY,CAAC,WAAW,EAAE,CAAC;IAEzC,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC;IAClB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC,IAAI,GAAG,GAAG,QAAQ,CAAC,EAAE,CAAC;YACtD,QAAQ,GAAG,GAAG,CAAC;QACjB,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;QACpB,MAAM,MAAM,GAAG,YAAY,GAAG,CAAC,CAAC;QAChC,OAAO,YAAY,CAAC,MAAM,GAAG,MAAM;YACjC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,KAAK;YACvC,CAAC,CAAC,YAAY,CAAC;IACnB,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC,CAAC;IACnD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,GAAG,YAAY,CAAC,CAAC;IAEnE,IAAI,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAE7C,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IACzC,IAAI,GAAG,GAAG,YAAY,CAAC,MAAM;QAAE,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;IAEzD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,GAAG;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,KAAe;IAC1D,mCAAmC;IACnC,IAAI,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAE9B,2CAA2C;IAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI;YAAE,SAAS;QAEpB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;QAC5D,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,IAAI,OAAO,GAAG,EAAE,IAAI,CAAC,CAAC;QAC/C,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,qBAAqB,CAAC,CAAC;IACxD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,14 @@
1
+ export interface Config {
2
+ ARCHIVE_DIR: string;
3
+ SOURCE_DIR: string;
4
+ DATABASE_PATH: string;
5
+ EMBED_SOCKET: string;
6
+ AUTO_UPDATE: boolean;
7
+ PYTHON_CMD: string;
8
+ CHUNK_SIZE: number;
9
+ CHUNK_OVERLAP: number;
10
+ EMBEDDING_MODEL: string;
11
+ EMBEDDING_DIM: number;
12
+ }
13
+ export declare function getConfig(): Config;
14
+ export declare function validateConfig(config: Config): string[];
package/dist/config.js ADDED
@@ -0,0 +1,52 @@
1
+ import { existsSync } from 'fs';
2
+ const defaults = {
3
+ ARCHIVE_DIR: './archive',
4
+ SOURCE_DIR: './source',
5
+ DATABASE_PATH: './search.db',
6
+ EMBED_SOCKET: '/tmp/qwen3-embed.sock',
7
+ AUTO_UPDATE: true,
8
+ PYTHON_CMD: 'python3',
9
+ CHUNK_SIZE: 300,
10
+ CHUNK_OVERLAP: 50,
11
+ EMBEDDING_MODEL: 'qwen3-small',
12
+ EMBEDDING_DIM: 1024,
13
+ };
14
+ // Helper to parse integer env vars with NaN fallback to default
15
+ function parseIntOrDefault(envValue, defaultValue) {
16
+ if (envValue === undefined)
17
+ return defaultValue;
18
+ const parsed = parseInt(envValue, 10);
19
+ return Number.isNaN(parsed) ? defaultValue : parsed;
20
+ }
21
+ export function getConfig() {
22
+ return {
23
+ ARCHIVE_DIR: process.env.ARCHIVE_DIR || defaults.ARCHIVE_DIR,
24
+ SOURCE_DIR: process.env.SOURCE_DIR || defaults.SOURCE_DIR,
25
+ DATABASE_PATH: process.env.DATABASE_PATH || defaults.DATABASE_PATH,
26
+ EMBED_SOCKET: process.env.EMBED_SOCKET || defaults.EMBED_SOCKET,
27
+ // AUTO_UPDATE is enabled by default; only explicitly setting to 'false' disables it
28
+ AUTO_UPDATE: process.env.AUTO_UPDATE !== 'false',
29
+ PYTHON_CMD: process.env.PYTHON_CMD || defaults.PYTHON_CMD,
30
+ CHUNK_SIZE: parseIntOrDefault(process.env.CHUNK_SIZE, defaults.CHUNK_SIZE),
31
+ CHUNK_OVERLAP: parseIntOrDefault(process.env.CHUNK_OVERLAP, defaults.CHUNK_OVERLAP),
32
+ EMBEDDING_MODEL: process.env.EMBEDDING_MODEL || defaults.EMBEDDING_MODEL,
33
+ EMBEDDING_DIM: parseIntOrDefault(process.env.EMBEDDING_DIM, defaults.EMBEDDING_DIM),
34
+ };
35
+ }
36
+ export function validateConfig(config) {
37
+ const errors = [];
38
+ if (!existsSync(config.ARCHIVE_DIR)) {
39
+ errors.push(`ARCHIVE_DIR does not exist: ${config.ARCHIVE_DIR}`);
40
+ }
41
+ if (!existsSync(config.SOURCE_DIR)) {
42
+ errors.push(`SOURCE_DIR does not exist: ${config.SOURCE_DIR}`);
43
+ }
44
+ if (config.CHUNK_SIZE < 100 || config.CHUNK_SIZE > 1000) {
45
+ errors.push(`CHUNK_SIZE must be between 100 and 1000: ${config.CHUNK_SIZE}`);
46
+ }
47
+ if (config.CHUNK_OVERLAP < 0 || config.CHUNK_OVERLAP >= config.CHUNK_SIZE) {
48
+ errors.push(`CHUNK_OVERLAP must be between 0 and CHUNK_SIZE: ${config.CHUNK_OVERLAP}`);
49
+ }
50
+ return errors;
51
+ }
52
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAehC,MAAM,QAAQ,GAAW;IACvB,WAAW,EAAE,WAAW;IACxB,UAAU,EAAE,UAAU;IACtB,aAAa,EAAE,aAAa;IAC5B,YAAY,EAAE,uBAAuB;IACrC,WAAW,EAAE,IAAI;IACjB,UAAU,EAAE,SAAS;IACrB,UAAU,EAAE,GAAG;IACf,aAAa,EAAE,EAAE;IACjB,eAAe,EAAE,aAAa;IAC9B,aAAa,EAAE,IAAI;CACpB,CAAC;AAEF,gEAAgE;AAChE,SAAS,iBAAiB,CAAC,QAA4B,EAAE,YAAoB;IAC3E,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,YAAY,CAAC;IAChD,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACtC,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO;QACL,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,QAAQ,CAAC,WAAW;QAC5D,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU;QACzD,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,QAAQ,CAAC,aAAa;QAClE,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,QAAQ,CAAC,YAAY;QAC/D,oFAAoF;QACpF,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,KAAK,OAAO;QAChD,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU;QACzD,UAAU,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC;QAC1E,aAAa,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,QAAQ,CAAC,aAAa,CAAC;QACnF,eAAe,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,QAAQ,CAAC,eAAe;QACxE,aAAa,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,QAAQ,CAAC,aAAa,CAAC;KACpF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC;QACpC,MAAM,CAAC,IAAI,CAAC,+BAA+B,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,8BAA8B,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,GAAG,GAAG,IAAI,MAAM,CAAC,UAAU,GAAG,IAAI,EAAE,CAAC;QACxD,MAAM,CAAC,IAAI,CAAC,4CAA4C,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IAC/E,CAAC;IACD,IAAI,MAAM,CAAC,aAAa,GAAG,CAAC,IAAI,MAAM,CAAC,aAAa,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QAC1E,MAAM,CAAC,IAAI,CAAC,mDAAmD,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;IACzF,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,14 @@
1
+ export interface Chunk {
2
+ id?: number;
3
+ conversation_id: string;
4
+ chunk_index: number;
5
+ page_number: number | null;
6
+ role: 'user' | 'assistant';
7
+ content: string;
8
+ embedding: Buffer | null;
9
+ }
10
+ export declare function insertChunk(c: Chunk): number;
11
+ export declare function getChunksForConversation(conversationId: string): Chunk[];
12
+ export declare function searchChunksFTS(query: string, limit?: number): Chunk[];
13
+ export declare function sanitizeFTSQuery(query: string): string;
14
+ export declare function deleteChunksForConversation(conversationId: string): void;
@@ -0,0 +1,43 @@
1
+ import { getDatabase } from './index.js';
2
+ export function insertChunk(c) {
3
+ const result = getDatabase()
4
+ .prepare(`INSERT INTO chunks
5
+ (conversation_id, chunk_index, page_number, role, content, embedding)
6
+ VALUES (?, ?, ?, ?, ?, ?)`)
7
+ .run(c.conversation_id, c.chunk_index, c.page_number, c.role, c.content, c.embedding);
8
+ return result.lastInsertRowid;
9
+ }
10
+ export function getChunksForConversation(conversationId) {
11
+ return getDatabase()
12
+ .prepare('SELECT * FROM chunks WHERE conversation_id = ? ORDER BY chunk_index')
13
+ .all(conversationId);
14
+ }
15
+ export function searchChunksFTS(query, limit = 100) {
16
+ const sanitized = sanitizeFTSQuery(query);
17
+ if (!sanitized)
18
+ return [];
19
+ return getDatabase()
20
+ .prepare(`SELECT c.* FROM chunks_fts fts
21
+ JOIN chunks c ON c.id = fts.rowid
22
+ WHERE chunks_fts MATCH ?
23
+ ORDER BY bm25(chunks_fts)
24
+ LIMIT ?`)
25
+ .all(sanitized, limit);
26
+ }
27
+ export function sanitizeFTSQuery(query) {
28
+ // Remove FTS5 special characters and operators
29
+ return query
30
+ .replace(/["\*\(\)]/g, ' ') // Remove quotes, wildcards, parentheses
31
+ .replace(/\b(AND|OR|NOT)\b/gi, ' ') // Remove boolean operators
32
+ .trim()
33
+ .split(/\s+/)
34
+ .filter(Boolean)
35
+ .map((token) => `"${token}"`) // Wrap each token in quotes for exact matching
36
+ .join(' ');
37
+ }
38
+ export function deleteChunksForConversation(conversationId) {
39
+ getDatabase()
40
+ .prepare('DELETE FROM chunks WHERE conversation_id = ?')
41
+ .run(conversationId);
42
+ }
43
+ //# sourceMappingURL=chunks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunks.js","sourceRoot":"","sources":["../../src/db/chunks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAYzC,MAAM,UAAU,WAAW,CAAC,CAAQ;IAClC,MAAM,MAAM,GAAG,WAAW,EAAE;SACzB,OAAO,CACN;;iCAE2B,CAC5B;SACA,GAAG,CAAC,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;IACxF,OAAO,MAAM,CAAC,eAAyB,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,cAAsB;IAC7D,OAAO,WAAW,EAAE;SACjB,OAAO,CAAC,qEAAqE,CAAC;SAC9E,GAAG,CAAC,cAAc,CAAY,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAa,EAAE,KAAK,GAAG,GAAG;IACxD,MAAM,SAAS,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,CAAC;IAE1B,OAAO,WAAW,EAAE;SACjB,OAAO,CACN;;;;eAIS,CACV;SACA,GAAG,CAAC,SAAS,EAAE,KAAK,CAAY,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,+CAA+C;IAC/C,OAAO,KAAK;SACT,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,CAAW,wCAAwC;SAC7E,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAG,2BAA2B;SAChE,IAAI,EAAE;SACN,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,OAAO,CAAC;SACf,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,KAAK,GAAG,CAAC,CAAU,+CAA+C;SACrF,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,cAAsB;IAChE,WAAW,EAAE;SACV,OAAO,CAAC,8CAA8C,CAAC;SACvD,GAAG,CAAC,cAAc,CAAC,CAAC;AACzB,CAAC"}
@@ -0,0 +1,16 @@
1
+ export interface Conversation {
2
+ id: string;
3
+ project: string;
4
+ title: string | null;
5
+ created_at: string | null;
6
+ file_path: string;
7
+ content_hash: string;
8
+ source_mtime: number;
9
+ indexed_at?: string;
10
+ }
11
+ export declare function insertConversation(c: Conversation): void;
12
+ export declare function getConversation(id: string): Conversation | null;
13
+ export declare function getConversationByPath(path: string): Conversation | null;
14
+ export declare function deleteConversation(id: string): void;
15
+ export declare function listConversations(project?: string): Conversation[];
16
+ export declare function getRecentConversations(limit?: number): Conversation[];