@bojackduy/opencode-telescope 0.1.24 → 0.1.26

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,239 @@
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
+ const vectorRebuilds = new Map<string, Promise<void>>()
72
+
73
+ export function setupVectorTable(index: Database, config: SemanticConfig, indexPath: string): void {
74
+ const dims = getMeta(index, "embedding_dimensions")
75
+ if (dims) {
76
+ setMeta(index, "vector_state", "enabled")
77
+ debug.log("vector:already-indexed", { dimensions: dims })
78
+ return
79
+ }
80
+ if (vectorRebuilds.has(indexPath)) {
81
+ setMeta(index, "vector_state", "stale")
82
+ debug.log("vector:rebuild:already-running", { indexPath })
83
+ return
84
+ }
85
+
86
+ setMeta(index, "vector_state", "stale")
87
+ const rebuild = new Promise<void>((resolve) => {
88
+ const timer = setTimeout(() => {
89
+ rebuildVectorIndex(indexPath, config)
90
+ .catch((err) => {
91
+ debug.log("vector:rebuild:error", err instanceof Error ? err.message : String(err))
92
+ })
93
+ .finally(resolve)
94
+ }, 1)
95
+ ;(timer as { unref?: () => void }).unref?.()
96
+ }).finally(() => {
97
+ vectorRebuilds.delete(indexPath)
98
+ })
99
+ vectorRebuilds.set(indexPath, rebuild)
100
+ }
101
+
102
+ let customSQLiteConfigured = false
103
+
104
+ export function configureCustomSQLite() {
105
+ if (customSQLiteConfigured) return
106
+ const config = parseSemanticConfigForVector()
107
+ if (config.disableVector) return
108
+ customSQLiteConfigured = true
109
+
110
+ const candidates = [
111
+ config.sqliteLibPath,
112
+ process.platform === "darwin" ? "/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib" : undefined,
113
+ process.platform === "darwin" ? "/usr/local/opt/sqlite/lib/libsqlite3.dylib" : undefined,
114
+ ].filter((item): item is string => Boolean(item))
115
+
116
+ for (const candidate of candidates) {
117
+ if (existsSync(candidate)) {
118
+ try {
119
+ Database.setCustomSQLite(candidate)
120
+ debug.log("custom-sqlite:set", { path: candidate })
121
+ return
122
+ } catch (err) {
123
+ debug.log("custom-sqlite:error", { path: candidate, error: err instanceof Error ? err.message : String(err) })
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ async function rebuildVectorIndex(indexPath: string, config: SemanticConfig) {
130
+ configureCustomSQLite()
131
+ const db = new Database(indexPath)
132
+ try {
133
+ const loaded = await loadVecExtension(db)
134
+ if (!loaded) {
135
+ setMeta(db, "vector_state", "unavailable")
136
+ return
137
+ }
138
+
139
+ const client = new LlamaEmbeddingClient({
140
+ baseUrl: config.embedBaseUrl,
141
+ model: config.embedModel,
142
+ documentPrefix: config.documentPrefix,
143
+ queryPrefix: config.queryPrefix,
144
+ })
145
+ const healthy = await client.health()
146
+ if (!healthy) {
147
+ setMeta(db, "vector_state", "unavailable")
148
+ return
149
+ }
150
+
151
+ const docs = db.query<{ rowid: number; text: string }, []>("SELECT rowid, text FROM document ORDER BY rowid ASC").all()
152
+ if (!docs.length) {
153
+ setMeta(db, "vector_state", "enabled")
154
+ return
155
+ }
156
+
157
+ const truncate = (text: string) => text.length > 800 ? text.slice(0, 800) : text
158
+
159
+ const batchSize = 128
160
+ const embeddings: Float32Array[] = []
161
+ debug.log("vector:embed:start", { count: docs.length, batchSize })
162
+ for (let i = 0; i < docs.length; i += batchSize) {
163
+ const batch = docs.slice(i, i + batchSize)
164
+ const batchEmbeddings = await client.embedDocuments(batch.map((d) => truncate(d.text)))
165
+ embeddings.push(...batchEmbeddings)
166
+ debug.log("vector:embed:progress", { done: Math.min(i + batchSize, docs.length), total: docs.length })
167
+ }
168
+ const dims = embeddings[0]?.length
169
+ if (!dims) {
170
+ setMeta(db, "vector_state", "unavailable")
171
+ return
172
+ }
173
+
174
+ db.exec("DROP TABLE IF EXISTS document_vec")
175
+ db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS document_vec USING vec0(embedding float[${dims}])`)
176
+ debug.log("vector:table:recreated", { dimensions: dims })
177
+
178
+ const insert = db.prepare<unknown, [number, Float32Array]>("INSERT INTO document_vec(rowid, embedding) VALUES (?, vec_f32(?))")
179
+ const transaction = db.transaction(() => {
180
+ for (const [i, doc] of docs.entries()) {
181
+ insert.run(doc.rowid, embeddings[i])
182
+ }
183
+ })
184
+ transaction()
185
+
186
+ setMeta(db, "vector_state", "enabled")
187
+ setMeta(db, "embedding_dimensions", String(dims))
188
+ if (config.embedModel) setMeta(db, "embedding_model", config.embedModel)
189
+ debug.log("vector:rebuild:done", { vectors: docs.length, dimensions: dims })
190
+ } catch (err) {
191
+ setMeta(db, "vector_state", "unavailable")
192
+ debug.log("vector:rebuild:error", err instanceof Error ? err.message : String(err))
193
+ } finally {
194
+ db.close()
195
+ }
196
+ }
197
+
198
+ export async function loadVecExtension(db: Database): Promise<boolean> {
199
+ try {
200
+ const sqliteVec = await importPackage("sqlite-vec").catch(() => undefined)
201
+ if (sqliteVec?.load) {
202
+ sqliteVec.load(db)
203
+ debug.log("vector:extension:loaded", { source: "npm" })
204
+ return true
205
+ }
206
+ } catch {
207
+ debug.log("vector:extension:npm-failed")
208
+ }
209
+
210
+ const config = parseSemanticConfigForVector()
211
+ const explicitPath = config.sqliteVecExtension || process.env.OPENCODE_TELESCOPE_SQLITE_VEC_EXT
212
+ if (explicitPath && existsSync(explicitPath)) {
213
+ try {
214
+ db.loadExtension(explicitPath)
215
+ debug.log("vector:extension:loaded", { source: "path", path: explicitPath })
216
+ return true
217
+ } catch (err) {
218
+ debug.log("vector:extension:path-failed", { path: explicitPath, error: err instanceof Error ? err.message : String(err) })
219
+ }
220
+ }
221
+
222
+ return false
223
+ }
224
+
225
+ function importPackage(specifier: string) {
226
+ return new Function("specifier", "return import(specifier)")(specifier) as Promise<{ load?: (db: Database) => void; getLoadablePath?: () => string }>
227
+ }
228
+
229
+ function parseSemanticConfigForVector(): { disableVector: boolean; sqliteLibPath?: string; sqliteVecExtension?: string; embedBaseUrl: string; embedModel?: string; documentPrefix: string; queryPrefix: string } {
230
+ return {
231
+ disableVector: process.env.OPENCODE_TELESCOPE_DISABLE_VECTOR === "1" || process.env.OPENCODE_TELESCOPE_DISABLE_VECTOR === "true",
232
+ sqliteLibPath: process.env.OPENCODE_TELESCOPE_SQLITE_LIB || undefined,
233
+ sqliteVecExtension: process.env.OPENCODE_TELESCOPE_SQLITE_VEC_EXT || undefined,
234
+ embedBaseUrl: process.env.OPENCODE_TELESCOPE_EMBED_BASE_URL ?? "http://127.0.0.1:8081",
235
+ embedModel: process.env.OPENCODE_TELESCOPE_EMBED_MODEL || undefined,
236
+ documentPrefix: "search_document: ",
237
+ queryPrefix: "search_query: ",
238
+ }
239
+ }