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.
- package/README.md +215 -0
- package/dist/api/search.d.ts +50 -0
- package/dist/api/search.js +181 -0
- package/dist/api/search.js.map +1 -0
- package/dist/api/snippets.d.ts +2 -0
- package/dist/api/snippets.js +49 -0
- package/dist/api/snippets.js.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +52 -0
- package/dist/config.js.map +1 -0
- package/dist/db/chunks.d.ts +14 -0
- package/dist/db/chunks.js +43 -0
- package/dist/db/chunks.js.map +1 -0
- package/dist/db/conversations.d.ts +16 -0
- package/dist/db/conversations.js +40 -0
- package/dist/db/conversations.js.map +1 -0
- package/dist/db/index.d.ts +6 -0
- package/dist/db/index.js +40 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +5 -0
- package/dist/db/schema.js +71 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/embeddings/client.d.ts +16 -0
- package/dist/embeddings/client.js +155 -0
- package/dist/embeddings/client.js.map +1 -0
- package/dist/indexer/changeDetection.d.ts +16 -0
- package/dist/indexer/changeDetection.js +81 -0
- package/dist/indexer/changeDetection.js.map +1 -0
- package/dist/indexer/chunker.d.ts +5 -0
- package/dist/indexer/chunker.js +44 -0
- package/dist/indexer/chunker.js.map +1 -0
- package/dist/indexer/fileUtils.d.ts +2 -0
- package/dist/indexer/fileUtils.js +9 -0
- package/dist/indexer/fileUtils.js.map +1 -0
- package/dist/indexer/index.d.ts +19 -0
- package/dist/indexer/index.js +267 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/parser.d.ts +12 -0
- package/dist/indexer/parser.js +45 -0
- package/dist/indexer/parser.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +1851 -0
- package/dist/server.js.map +1 -0
- 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":"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"}
|
|
@@ -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, "&")
|
|
31
|
+
.replace(/</g, "<")
|
|
32
|
+
.replace(/>/g, ">")
|
|
33
|
+
.replace(/"/g, """)
|
|
34
|
+
.replace(/'/g, "'");
|
|
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"}
|
package/dist/config.d.ts
ADDED
|
@@ -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[];
|