@bojackduy/opencode-telescope 0.1.23 → 0.1.25

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.
@@ -0,0 +1,223 @@
1
+ import { Database } from "bun:sqlite"
2
+ import { existsSync } from "node:fs"
3
+ import type { Row, ScoredRow, SemanticConfig } from "./types.ts"
4
+ import { LlamaEmbeddingClient } from "./embedding.ts"
5
+ import { setMeta, getMeta } from "./schema.ts"
6
+ import { debug } from "../ui/debug.ts"
7
+
8
+ export function hybridBlend(keyword: Row[], vector: Row[], alpha: number): ScoredRow[] {
9
+ const merged = new Map<string, ScoredRow>()
10
+ const defaultScore = 0.5
11
+
12
+ for (const [i, row] of keyword.entries()) {
13
+ merged.set(row.id, {
14
+ ...row,
15
+ score: 0,
16
+ keywordScore: keyword.length > 1 ? 1 - i / (keyword.length - 1) : 1,
17
+ vectorScore: 0,
18
+ })
19
+ }
20
+
21
+ for (const [i, row] of vector.entries()) {
22
+ const existing = merged.get(row.id)
23
+ const vectorScore = vector.length > 1 ? 1 - i / (vector.length - 1) : 1
24
+ if (existing) {
25
+ existing.vectorScore = vectorScore
26
+ } else {
27
+ merged.set(row.id, {
28
+ ...row,
29
+ score: 0,
30
+ keywordScore: defaultScore,
31
+ vectorScore,
32
+ })
33
+ }
34
+ }
35
+
36
+ const keywordValues = [...merged.values()].map((r) => r.keywordScore)
37
+ const vectorValues = [...merged.values()].map((r) => r.vectorScore)
38
+ const kwMin = Math.min(...keywordValues)
39
+ const kwMax = Math.max(...keywordValues)
40
+ const vecMin = Math.min(...vectorValues)
41
+ const vecMax = Math.max(...vectorValues)
42
+
43
+ const normalized = [...merged.values()].map((r) => {
44
+ const kn = kwMax === kwMin ? 0.5 : (r.keywordScore - kwMin) / (kwMax - kwMin)
45
+ const vn = vecMax === vecMin ? 0.5 : (r.vectorScore - vecMin) / (vecMax - vecMin)
46
+ return {
47
+ ...r,
48
+ keywordScore: kn,
49
+ vectorScore: vn,
50
+ score: (1 - alpha) * kn + alpha * vn,
51
+ }
52
+ })
53
+
54
+ return normalized.sort((a, b) => b.score - a.score)
55
+ }
56
+
57
+ export function searchVector(index: Database, embedding: Float32Array, limit: number): Row[] {
58
+ const count = index.query<{ count: number }, []>("SELECT COUNT(*) as count FROM document_vec").get()?.count ?? 0
59
+ if (!count) return []
60
+ const k = Math.min(count, Math.max(limit * 4, 200))
61
+ return index.query<Row, [Float32Array, number]>(`
62
+ SELECT d.part_id AS id, d.message_id, d.session_id, d.session_title, d.directory, d.role,
63
+ d.part_type, d.tool, CAST(d.time_created AS INTEGER) AS time_created, d.text
64
+ FROM document_vec v
65
+ JOIN document d ON d.rowid = v.rowid
66
+ WHERE v.embedding MATCH vec_f32(?) AND k = ?
67
+ ORDER BY v.distance
68
+ `).all(embedding, k)
69
+ }
70
+
71
+ export async function setupVectorTable(index: Database, config: SemanticConfig, indexPath: string): Promise<void> {
72
+ const dims = getMeta(index, "embedding_dimensions")
73
+ if (dims) {
74
+ setMeta(index, "vector_state", "enabled")
75
+ debug.log("vector:already-indexed", { dimensions: dims })
76
+ return
77
+ }
78
+ setMeta(index, "vector_state", "stale")
79
+ try {
80
+ await rebuildVectorIndex(indexPath, config)
81
+ } catch (err) {
82
+ debug.log("vector:rebuild:error", err instanceof Error ? err.message : String(err))
83
+ }
84
+ }
85
+
86
+ let customSQLiteConfigured = false
87
+
88
+ export function configureCustomSQLite() {
89
+ if (customSQLiteConfigured) return
90
+ const config = parseSemanticConfigForVector()
91
+ if (config.disableVector) return
92
+ customSQLiteConfigured = true
93
+
94
+ const candidates = [
95
+ config.sqliteLibPath,
96
+ process.platform === "darwin" ? "/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib" : undefined,
97
+ process.platform === "darwin" ? "/usr/local/opt/sqlite/lib/libsqlite3.dylib" : undefined,
98
+ ].filter((item): item is string => Boolean(item))
99
+
100
+ for (const candidate of candidates) {
101
+ if (existsSync(candidate)) {
102
+ try {
103
+ Database.setCustomSQLite(candidate)
104
+ debug.log("custom-sqlite:set", { path: candidate })
105
+ return
106
+ } catch (err) {
107
+ debug.log("custom-sqlite:error", { path: candidate, error: err instanceof Error ? err.message : String(err) })
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ async function rebuildVectorIndex(indexPath: string, config: SemanticConfig) {
114
+ configureCustomSQLite()
115
+ const db = new Database(indexPath)
116
+ try {
117
+ const loaded = await loadVecExtension(db)
118
+ if (!loaded) {
119
+ setMeta(db, "vector_state", "unavailable")
120
+ return
121
+ }
122
+
123
+ const client = new LlamaEmbeddingClient({
124
+ baseUrl: config.embedBaseUrl,
125
+ model: config.embedModel,
126
+ documentPrefix: config.documentPrefix,
127
+ queryPrefix: config.queryPrefix,
128
+ })
129
+ const healthy = await client.health()
130
+ if (!healthy) {
131
+ setMeta(db, "vector_state", "unavailable")
132
+ return
133
+ }
134
+
135
+ const docs = db.query<{ rowid: number; text: string }, []>("SELECT rowid, text FROM document ORDER BY rowid ASC").all()
136
+ if (!docs.length) {
137
+ setMeta(db, "vector_state", "enabled")
138
+ return
139
+ }
140
+
141
+ const truncate = (text: string) => text.length > 800 ? text.slice(0, 800) : text
142
+
143
+ const batchSize = 128
144
+ const embeddings: Float32Array[] = []
145
+ debug.log("vector:embed:start", { count: docs.length, batchSize })
146
+ for (let i = 0; i < docs.length; i += batchSize) {
147
+ const batch = docs.slice(i, i + batchSize)
148
+ const batchEmbeddings = await client.embedDocuments(batch.map((d) => truncate(d.text)))
149
+ embeddings.push(...batchEmbeddings)
150
+ debug.log("vector:embed:progress", { done: Math.min(i + batchSize, docs.length), total: docs.length })
151
+ }
152
+ const dims = embeddings[0]?.length
153
+ if (!dims) {
154
+ setMeta(db, "vector_state", "unavailable")
155
+ return
156
+ }
157
+
158
+ db.exec("DROP TABLE IF EXISTS document_vec")
159
+ db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS document_vec USING vec0(embedding float[${dims}])`)
160
+ debug.log("vector:table:recreated", { dimensions: dims })
161
+
162
+ const insert = db.prepare<unknown, [number, Float32Array]>("INSERT INTO document_vec(rowid, embedding) VALUES (?, vec_f32(?))")
163
+ const transaction = db.transaction(() => {
164
+ for (const [i, doc] of docs.entries()) {
165
+ insert.run(doc.rowid, embeddings[i])
166
+ }
167
+ })
168
+ transaction()
169
+
170
+ setMeta(db, "vector_state", "enabled")
171
+ setMeta(db, "embedding_dimensions", String(dims))
172
+ if (config.embedModel) setMeta(db, "embedding_model", config.embedModel)
173
+ debug.log("vector:rebuild:done", { vectors: docs.length, dimensions: dims })
174
+ } catch (err) {
175
+ setMeta(db, "vector_state", "unavailable")
176
+ debug.log("vector:rebuild:error", err instanceof Error ? err.message : String(err))
177
+ } finally {
178
+ db.close()
179
+ }
180
+ }
181
+
182
+ export async function loadVecExtension(db: Database): Promise<boolean> {
183
+ try {
184
+ const sqliteVec = await importPackage("sqlite-vec").catch(() => undefined)
185
+ if (sqliteVec?.load) {
186
+ sqliteVec.load(db)
187
+ debug.log("vector:extension:loaded", { source: "npm" })
188
+ return true
189
+ }
190
+ } catch {
191
+ debug.log("vector:extension:npm-failed")
192
+ }
193
+
194
+ const config = parseSemanticConfigForVector()
195
+ const explicitPath = config.sqliteVecExtension || process.env.OPENCODE_TELESCOPE_SQLITE_VEC_EXT
196
+ if (explicitPath && existsSync(explicitPath)) {
197
+ try {
198
+ db.loadExtension(explicitPath)
199
+ debug.log("vector:extension:loaded", { source: "path", path: explicitPath })
200
+ return true
201
+ } catch (err) {
202
+ debug.log("vector:extension:path-failed", { path: explicitPath, error: err instanceof Error ? err.message : String(err) })
203
+ }
204
+ }
205
+
206
+ return false
207
+ }
208
+
209
+ function importPackage(specifier: string) {
210
+ return new Function("specifier", "return import(specifier)")(specifier) as Promise<{ load?: (db: Database) => void; getLoadablePath?: () => string }>
211
+ }
212
+
213
+ function parseSemanticConfigForVector(): { disableVector: boolean; sqliteLibPath?: string; sqliteVecExtension?: string; embedBaseUrl: string; embedModel?: string; documentPrefix: string; queryPrefix: string } {
214
+ return {
215
+ disableVector: process.env.OPENCODE_TELESCOPE_DISABLE_VECTOR === "1" || process.env.OPENCODE_TELESCOPE_DISABLE_VECTOR === "true",
216
+ sqliteLibPath: process.env.OPENCODE_TELESCOPE_SQLITE_LIB || undefined,
217
+ sqliteVecExtension: process.env.OPENCODE_TELESCOPE_SQLITE_VEC_EXT || undefined,
218
+ embedBaseUrl: process.env.OPENCODE_TELESCOPE_EMBED_BASE_URL ?? "http://127.0.0.1:8081",
219
+ embedModel: process.env.OPENCODE_TELESCOPE_EMBED_MODEL || undefined,
220
+ documentPrefix: "search_document: ",
221
+ queryPrefix: "search_query: ",
222
+ }
223
+ }