@bojackduy/opencode-telescope 0.1.24 → 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.
- package/components/preview.tsx +30 -143
- package/components/result-list.tsx +17 -1
- package/package.json +2 -1
- package/search/db-path.ts +67 -0
- package/search/dependencies.test.ts +38 -0
- package/search/dependencies.ts +63 -0
- package/search/embedding.test.ts +83 -0
- package/search/embedding.ts +67 -0
- package/search/queries.ts +813 -0
- package/search/schema.ts +120 -0
- package/search/text.ts +348 -0
- package/search/types.ts +131 -0
- package/search/vector.ts +223 -0
- package/search.ts +47 -1510
- package/telescope.tsx +2 -2
- package/ui/format.test.ts +120 -0
- package/ui/preview-utils.test.ts +161 -0
- package/ui/preview-utils.ts +136 -0
- package/ui/render-target.test.ts +55 -0
package/search/schema.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
|
|
3
|
+
export const SEARCH_INDEX_VERSION = "7"
|
|
4
|
+
export const DOCUMENT_EXTRACTOR_VERSION = "1"
|
|
5
|
+
|
|
6
|
+
export function migrateSearchIndex(db: Database) {
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS index_meta(
|
|
9
|
+
key TEXT PRIMARY KEY,
|
|
10
|
+
value TEXT NOT NULL
|
|
11
|
+
);
|
|
12
|
+
CREATE TABLE IF NOT EXISTS document(
|
|
13
|
+
rowid INTEGER PRIMARY KEY,
|
|
14
|
+
doc_id TEXT UNIQUE NOT NULL,
|
|
15
|
+
part_id TEXT NOT NULL,
|
|
16
|
+
message_id TEXT NOT NULL,
|
|
17
|
+
session_id TEXT NOT NULL,
|
|
18
|
+
session_title TEXT NOT NULL,
|
|
19
|
+
directory TEXT NOT NULL,
|
|
20
|
+
role TEXT NOT NULL,
|
|
21
|
+
part_type TEXT NOT NULL,
|
|
22
|
+
tool TEXT,
|
|
23
|
+
time_created INTEGER NOT NULL,
|
|
24
|
+
chunk_index INTEGER NOT NULL,
|
|
25
|
+
text TEXT NOT NULL,
|
|
26
|
+
source_hash TEXT NOT NULL,
|
|
27
|
+
extractor_version TEXT NOT NULL,
|
|
28
|
+
indexed_at INTEGER NOT NULL
|
|
29
|
+
);
|
|
30
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS document_fts USING fts5(
|
|
31
|
+
id UNINDEXED,
|
|
32
|
+
message_id UNINDEXED,
|
|
33
|
+
session_id UNINDEXED,
|
|
34
|
+
session_title,
|
|
35
|
+
directory UNINDEXED,
|
|
36
|
+
role UNINDEXED,
|
|
37
|
+
part_type UNINDEXED,
|
|
38
|
+
tool UNINDEXED,
|
|
39
|
+
time_created UNINDEXED,
|
|
40
|
+
text,
|
|
41
|
+
tokenize='unicode61'
|
|
42
|
+
);
|
|
43
|
+
CREATE TABLE IF NOT EXISTS document_index(
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
message_id TEXT NOT NULL,
|
|
46
|
+
session_id TEXT NOT NULL,
|
|
47
|
+
session_title TEXT NOT NULL,
|
|
48
|
+
directory TEXT NOT NULL,
|
|
49
|
+
role TEXT NOT NULL,
|
|
50
|
+
part_type TEXT NOT NULL,
|
|
51
|
+
tool TEXT,
|
|
52
|
+
time_created INTEGER NOT NULL,
|
|
53
|
+
text TEXT NOT NULL
|
|
54
|
+
);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS document_index_recent_idx
|
|
56
|
+
ON document_index(directory, role, time_created DESC);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS document_index_recent_text_idx
|
|
58
|
+
ON document_index(directory, role, part_type, time_created DESC);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS document_index_time_idx
|
|
60
|
+
ON document_index(time_created DESC);
|
|
61
|
+
`)
|
|
62
|
+
|
|
63
|
+
const columns = db.query<{ name: string }, []>("PRAGMA table_info(document_fts)").all().map((column) => column.name)
|
|
64
|
+
if (!columns.includes("part_type") || !columns.includes("tool")) {
|
|
65
|
+
db.exec("DROP TABLE document_fts")
|
|
66
|
+
db.exec(`
|
|
67
|
+
CREATE VIRTUAL TABLE document_fts USING fts5(
|
|
68
|
+
id UNINDEXED,
|
|
69
|
+
message_id UNINDEXED,
|
|
70
|
+
session_id UNINDEXED,
|
|
71
|
+
session_title,
|
|
72
|
+
directory UNINDEXED,
|
|
73
|
+
role UNINDEXED,
|
|
74
|
+
part_type UNINDEXED,
|
|
75
|
+
tool UNINDEXED,
|
|
76
|
+
time_created UNINDEXED,
|
|
77
|
+
text,
|
|
78
|
+
tokenize='unicode61'
|
|
79
|
+
);
|
|
80
|
+
`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const indexColumns = db.query<{ name: string }, []>("PRAGMA table_info(document_index)").all().map((column) => column.name)
|
|
84
|
+
if (!["part_type", "tool", "time_created", "text"].every((name) => indexColumns.includes(name))) {
|
|
85
|
+
db.exec("DROP TABLE IF EXISTS document_index")
|
|
86
|
+
db.exec(`
|
|
87
|
+
CREATE TABLE document_index(
|
|
88
|
+
id TEXT PRIMARY KEY,
|
|
89
|
+
message_id TEXT NOT NULL,
|
|
90
|
+
session_id TEXT NOT NULL,
|
|
91
|
+
session_title TEXT NOT NULL,
|
|
92
|
+
directory TEXT NOT NULL,
|
|
93
|
+
role TEXT NOT NULL,
|
|
94
|
+
part_type TEXT NOT NULL,
|
|
95
|
+
tool TEXT,
|
|
96
|
+
time_created INTEGER NOT NULL,
|
|
97
|
+
text TEXT NOT NULL
|
|
98
|
+
);
|
|
99
|
+
`)
|
|
100
|
+
}
|
|
101
|
+
db.exec(`
|
|
102
|
+
CREATE INDEX IF NOT EXISTS document_index_recent_idx
|
|
103
|
+
ON document_index(directory, role, time_created DESC);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS document_index_recent_text_idx
|
|
105
|
+
ON document_index(directory, role, part_type, time_created DESC);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS document_index_time_idx
|
|
107
|
+
ON document_index(time_created DESC);
|
|
108
|
+
`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getMeta(db: Database, key: string) {
|
|
112
|
+
return db.query<{ value: string }, [string]>("SELECT value FROM index_meta WHERE key = ?").get(key)?.value
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function setMeta(db: Database, key: string, value: string) {
|
|
116
|
+
db.query<unknown, [string, string]>(`
|
|
117
|
+
INSERT INTO index_meta(key, value) VALUES (?, ?)
|
|
118
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
119
|
+
`).run(key, value)
|
|
120
|
+
}
|
package/search/text.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import type { IndexSourceRow, Row, SearchResult } from "./types.ts"
|
|
2
|
+
|
|
3
|
+
export function rowToSearchResult(row: Row, query: string): SearchResult | undefined {
|
|
4
|
+
const text = row.text.trim()
|
|
5
|
+
const match = findMatch(text, query)
|
|
6
|
+
if (!match) return
|
|
7
|
+
const preview = focusedPreview(text, match.start, match.end)
|
|
8
|
+
return {
|
|
9
|
+
id: row.id,
|
|
10
|
+
messageID: row.message_id,
|
|
11
|
+
sessionID: row.session_id,
|
|
12
|
+
sessionTitle: row.session_title || "Untitled session",
|
|
13
|
+
directory: row.directory,
|
|
14
|
+
role: row.role,
|
|
15
|
+
partType: row.part_type ?? "text",
|
|
16
|
+
tool: row.tool ?? undefined,
|
|
17
|
+
timeCreated: row.time_created,
|
|
18
|
+
snippet: makeSnippet(text, query),
|
|
19
|
+
matchStart: match.start,
|
|
20
|
+
matchEnd: match.end,
|
|
21
|
+
before: match.before,
|
|
22
|
+
match: match.match,
|
|
23
|
+
after: match.after,
|
|
24
|
+
excerpt: match.excerpt,
|
|
25
|
+
previewBefore: preview.before,
|
|
26
|
+
previewMatch: preview.match,
|
|
27
|
+
previewAfter: preview.after,
|
|
28
|
+
previewMode: preview.mode,
|
|
29
|
+
previewHighlight: preview.highlight,
|
|
30
|
+
text,
|
|
31
|
+
isVectorMatch: false,
|
|
32
|
+
semanticScore: 0,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function rowToVectorResult(row: Row, vectorScore = 0): SearchResult | undefined {
|
|
37
|
+
const text = row.text.trim()
|
|
38
|
+
if (!text) return
|
|
39
|
+
const excerpt = text.slice(0, 200)
|
|
40
|
+
return {
|
|
41
|
+
id: row.id,
|
|
42
|
+
messageID: row.message_id,
|
|
43
|
+
sessionID: row.session_id,
|
|
44
|
+
sessionTitle: row.session_title || "Untitled session",
|
|
45
|
+
directory: row.directory,
|
|
46
|
+
role: row.role,
|
|
47
|
+
partType: row.part_type ?? "text",
|
|
48
|
+
tool: row.tool ?? undefined,
|
|
49
|
+
timeCreated: row.time_created,
|
|
50
|
+
snippet: excerpt,
|
|
51
|
+
matchStart: -1,
|
|
52
|
+
matchEnd: -1,
|
|
53
|
+
before: "",
|
|
54
|
+
match: "",
|
|
55
|
+
after: excerpt,
|
|
56
|
+
excerpt,
|
|
57
|
+
previewBefore: text.slice(0, Math.min(text.length, 1400)),
|
|
58
|
+
previewMatch: "",
|
|
59
|
+
previewAfter: "",
|
|
60
|
+
previewMode: "markdown" as const,
|
|
61
|
+
previewHighlight: false,
|
|
62
|
+
text,
|
|
63
|
+
isVectorMatch: true,
|
|
64
|
+
semanticScore: vectorScore,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function makeSnippet(text: string, query: string, radius = 72) {
|
|
69
|
+
const haystack = text.replace(/\s+/g, " ").trim()
|
|
70
|
+
const tokens = query.trim().split(/\s+/)
|
|
71
|
+
const lower = haystack.toLowerCase()
|
|
72
|
+
let searchPos = 0
|
|
73
|
+
let firstIndex = -1
|
|
74
|
+
let lastEnd = 0
|
|
75
|
+
for (const token of tokens) {
|
|
76
|
+
const index = lower.indexOf(token.toLowerCase(), searchPos)
|
|
77
|
+
if (index === -1) return truncate(haystack, radius * 2)
|
|
78
|
+
if (firstIndex === -1) firstIndex = index
|
|
79
|
+
searchPos = index + token.length
|
|
80
|
+
lastEnd = searchPos
|
|
81
|
+
}
|
|
82
|
+
const start = Math.max(0, firstIndex - radius)
|
|
83
|
+
const end = Math.min(haystack.length, lastEnd + radius)
|
|
84
|
+
return `${start > 0 ? "..." : ""}${haystack.slice(start, end)}${end < haystack.length ? "..." : ""}`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function extractSearchText(data: string) {
|
|
88
|
+
try {
|
|
89
|
+
return extractFromValue(JSON.parse(data)).replace(/\s+/g, " ").trim()
|
|
90
|
+
} catch {
|
|
91
|
+
return data.replace(/\s+/g, " ").trim()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function extractIndexText(data: string) {
|
|
96
|
+
try {
|
|
97
|
+
const value = JSON.parse(data) as unknown
|
|
98
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return ""
|
|
99
|
+
const record = value as Record<string, unknown>
|
|
100
|
+
if (record.type === "text") return typeof record.text === "string" ? record.text.trim() : ""
|
|
101
|
+
if (record.type !== "tool") return ""
|
|
102
|
+
return extractToolIndexText(record).replace(/\s+/g, " ").trim()
|
|
103
|
+
} catch {
|
|
104
|
+
return ""
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function indexSourceRowToRows(row: IndexSourceRow): Row[] {
|
|
109
|
+
const text = extractIndexText(row.data)
|
|
110
|
+
if (!text) return []
|
|
111
|
+
return [{ ...row, text }]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function ftsQuery(query: string) {
|
|
115
|
+
const tokens = query.trim().split(/\s+/)
|
|
116
|
+
.map((token) => token.replace(/["*^:()]/g, " ").trim())
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
if (!tokens.length) return ""
|
|
119
|
+
return tokens.map((token) => `"${token.replace(/"/g, '""')}"*`).join(" AND ")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function findMatch(text: string, query: string, radius = 96) {
|
|
123
|
+
const needle = query.trim()
|
|
124
|
+
if (!needle) {
|
|
125
|
+
const collapsed = text.replace(/\s+/g, " ").trim()
|
|
126
|
+
const end = Math.min(collapsed.length, radius * 2)
|
|
127
|
+
return {
|
|
128
|
+
start: 0,
|
|
129
|
+
end: 0,
|
|
130
|
+
before: "",
|
|
131
|
+
match: "",
|
|
132
|
+
after: collapsed.slice(0, end),
|
|
133
|
+
excerpt: collapsed.slice(0, end),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const tokens = needle.split(/\s+/)
|
|
137
|
+
const lowerText = text.toLowerCase()
|
|
138
|
+
let searchPos = 0
|
|
139
|
+
let firstStart = -1
|
|
140
|
+
let lastEnd = -1
|
|
141
|
+
for (const token of tokens) {
|
|
142
|
+
const pos = lowerText.indexOf(token.toLowerCase(), searchPos)
|
|
143
|
+
if (pos === -1) return
|
|
144
|
+
if (firstStart === -1) firstStart = pos
|
|
145
|
+
searchPos = pos + token.length
|
|
146
|
+
lastEnd = searchPos
|
|
147
|
+
}
|
|
148
|
+
const start = firstStart
|
|
149
|
+
const end = lastEnd
|
|
150
|
+
const lineStart = text.lastIndexOf("\n", start - 1) + 1
|
|
151
|
+
const nextLine = text.indexOf("\n", end)
|
|
152
|
+
const lineEnd = nextLine === -1 ? text.length : nextLine
|
|
153
|
+
const line = text.slice(lineStart, lineEnd)
|
|
154
|
+
const matchLen = end - start
|
|
155
|
+
const lineMatchStart = Math.max(0, start - lineStart)
|
|
156
|
+
const lineMatchEnd = lineMatchStart + matchLen
|
|
157
|
+
const excerptStart = Math.max(0, lineMatchStart - radius)
|
|
158
|
+
const excerptEnd = Math.min(line.length, lineMatchEnd + radius)
|
|
159
|
+
const before = normalizeSnippetSegment(line.slice(excerptStart, lineMatchStart))
|
|
160
|
+
const after = normalizeSnippetSegment(line.slice(lineMatchEnd, excerptEnd))
|
|
161
|
+
return {
|
|
162
|
+
start,
|
|
163
|
+
end,
|
|
164
|
+
before: `${excerptStart > 0 ? "..." : ""}${before}`,
|
|
165
|
+
match: text.slice(start, end),
|
|
166
|
+
after: `${after}${excerptEnd < line.length ? "..." : ""}`,
|
|
167
|
+
excerpt: `${excerptStart > 0 ? "..." : ""}${normalizeSnippetSegment(line.slice(excerptStart, excerptEnd))}${excerptEnd < line.length ? "..." : ""}`,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function focusedPreview(text: string, matchStart: number, matchEnd: number) {
|
|
172
|
+
if (matchStart === matchEnd) {
|
|
173
|
+
const preview = text.slice(0, Math.min(text.length, 1400))
|
|
174
|
+
return { before: preview, match: "", after: "", mode: "markdown" as const, highlight: false }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const lineStart = text.lastIndexOf("\n", matchStart - 1) + 1
|
|
178
|
+
const nextLine = text.indexOf("\n", matchEnd)
|
|
179
|
+
const lineEnd = nextLine === -1 ? text.length : nextLine
|
|
180
|
+
const line = text.slice(lineStart, lineEnd)
|
|
181
|
+
|
|
182
|
+
if (line.length > 260) {
|
|
183
|
+
const beforeStart = Math.max(0, matchStart - 180)
|
|
184
|
+
const afterEnd = Math.min(text.length, matchEnd + 220)
|
|
185
|
+
return {
|
|
186
|
+
before: `${beforeStart > 0 ? "..." : ""}${text.slice(beforeStart, matchStart)}`,
|
|
187
|
+
match: text.slice(matchStart, matchEnd),
|
|
188
|
+
after: `${text.slice(matchEnd, afterEnd)}${afterEnd < text.length ? "..." : ""}`,
|
|
189
|
+
mode: "text" as const,
|
|
190
|
+
highlight: true,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const window = lineWindow(text, matchStart, matchEnd, 40, 80)
|
|
195
|
+
const windowText = text.slice(window.start, window.end)
|
|
196
|
+
const relativeStart = matchStart - window.start
|
|
197
|
+
const relativeEnd = matchEnd - window.start
|
|
198
|
+
|
|
199
|
+
const insideCodeFence = isInsideCodeFence(text, matchStart)
|
|
200
|
+
return {
|
|
201
|
+
before: `${window.start > 0 ? "...\n" : ""}${windowText.slice(0, relativeStart)}`,
|
|
202
|
+
match: windowText.slice(relativeStart, relativeEnd),
|
|
203
|
+
after: `${windowText.slice(relativeEnd)}${window.end < text.length ? "\n..." : ""}`,
|
|
204
|
+
mode: "markdown" as const,
|
|
205
|
+
highlight: !insideCodeFence,
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function lineWindow(text: string, matchStart: number, matchEnd: number, before: number, after: number) {
|
|
210
|
+
const starts = [0]
|
|
211
|
+
for (let index = text.indexOf("\n"); index !== -1; index = text.indexOf("\n", index + 1)) {
|
|
212
|
+
starts.push(index + 1)
|
|
213
|
+
}
|
|
214
|
+
const matchLine = findLastStartIndex(starts, matchStart)
|
|
215
|
+
const startLine = Math.max(0, matchLine - before)
|
|
216
|
+
const endLine = Math.min(starts.length - 1, matchLine + after)
|
|
217
|
+
const start = starts[startLine] ?? 0
|
|
218
|
+
const end = starts[endLine + 1] ? starts[endLine + 1] - 1 : text.length
|
|
219
|
+
return { start, end: Math.max(end, matchEnd) }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function findLastStartIndex(starts: number[], offset: number) {
|
|
223
|
+
for (let index = starts.length - 1; index >= 0; index--) {
|
|
224
|
+
if (starts[index]! <= offset) return index
|
|
225
|
+
}
|
|
226
|
+
return 0
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isInsideCodeFence(text: string, offset: number) {
|
|
230
|
+
const before = text.slice(0, offset)
|
|
231
|
+
const fences = before.match(/^```/gm)
|
|
232
|
+
return Boolean(fences && fences.length % 2 === 1)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function normalizeSnippetSegment(value: string) {
|
|
236
|
+
return value.replace(/\s+/g, " ")
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function extractFromValue(value: unknown): string {
|
|
240
|
+
if (typeof value === "string") return value
|
|
241
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value)
|
|
242
|
+
if (!value) return ""
|
|
243
|
+
if (Array.isArray(value)) return value.map(extractFromValue).filter(Boolean).join("\n")
|
|
244
|
+
if (typeof value !== "object") return ""
|
|
245
|
+
return Object.entries(value)
|
|
246
|
+
.filter(([key]) => !["id", "sessionID", "messageID", "time", "timeCreated", "timeUpdated", "tokens", "cost"].includes(key))
|
|
247
|
+
.map(([, item]) => extractFromValue(item))
|
|
248
|
+
.filter(Boolean)
|
|
249
|
+
.join("\n")
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function extractToolIndexText(part: Record<string, unknown>) {
|
|
253
|
+
const tool = typeof part.tool === "string" ? part.tool : ""
|
|
254
|
+
const state = recordValue(part.state)
|
|
255
|
+
const input = recordValue(state?.input)
|
|
256
|
+
const metadata = recordValue(state?.metadata)
|
|
257
|
+
|
|
258
|
+
if (tool === "apply_patch") {
|
|
259
|
+
const files = Array.isArray(metadata?.files) ? metadata.files : []
|
|
260
|
+
const renderedPatches = files.map(applyPatchFileIndexText).filter(Boolean).join("\n")
|
|
261
|
+
const patchText = stringValue(input?.patchText)
|
|
262
|
+
return [renderedPatches, patchText].filter(Boolean).join("\n")
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (tool === "edit") {
|
|
266
|
+
const filediff = recordValue(metadata?.filediff)
|
|
267
|
+
return [
|
|
268
|
+
stringValue(input?.filePath),
|
|
269
|
+
stringValue(metadata?.diff),
|
|
270
|
+
stringValue(filediff?.patch),
|
|
271
|
+
stringValue(input?.oldString),
|
|
272
|
+
stringValue(input?.newString),
|
|
273
|
+
].filter(Boolean).join("\n")
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (tool === "write") {
|
|
277
|
+
return [stringValue(input?.filePath), stringValue(input?.content)].filter(Boolean).join("\n")
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return ""
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function applyPatchFileIndexText(value: unknown) {
|
|
284
|
+
const file = recordValue(value)
|
|
285
|
+
if (!file) return ""
|
|
286
|
+
return [
|
|
287
|
+
stringValue(file.filePath),
|
|
288
|
+
stringValue(file.relativePath),
|
|
289
|
+
stringValue(file.patch),
|
|
290
|
+
].filter(Boolean).join("\n")
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
294
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return
|
|
295
|
+
return value as Record<string, unknown>
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function stringValue(value: unknown) {
|
|
299
|
+
return typeof value === "string" ? value : undefined
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function truncate(value: string, length: number) {
|
|
303
|
+
if (value.length <= length) return value
|
|
304
|
+
return `${value.slice(0, length - 3)}...`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const synonymMap: Record<string, string[]> = {
|
|
308
|
+
greeting: ["hello", "hi", "hey", "howdy", "greetings", "good morning", "good afternoon", "good evening"],
|
|
309
|
+
hello: ["hi", "hey", "howdy", "greeting", "greetings"],
|
|
310
|
+
hi: ["hello", "hey", "howdy", "greeting"],
|
|
311
|
+
hey: ["hello", "hi", "howdy", "greeting"],
|
|
312
|
+
thanks: ["thank you", "thx", "ty", "appreciate", "grateful"],
|
|
313
|
+
goodbye: ["bye", "see you", "farewell", "later", "cya"],
|
|
314
|
+
bye: ["goodbye", "see you", "farewell", "later"],
|
|
315
|
+
error: ["bug", "issue", "failed", "failure", "problem", "exception"],
|
|
316
|
+
bug: ["error", "issue", "problem", "defect"],
|
|
317
|
+
fix: ["repair", "patch", "resolve", "correct", "solve", "bugfix"],
|
|
318
|
+
help: ["assist", "support", "guide", "how to", "tutorial"],
|
|
319
|
+
explain: ["describe", "clarify", "elaborate", "what is", "how does"],
|
|
320
|
+
code: ["program", "source", "implementation", "script", "function"],
|
|
321
|
+
test: ["spec", "unit test", "assertion", "verify", "check"],
|
|
322
|
+
refactor: ["restructure", "rewrite", "improve", "reorganize", "clean up"],
|
|
323
|
+
optimize: ["improve", "speed up", "performance", "efficient", "fast"],
|
|
324
|
+
config: ["configuration", "setting", "option", "setup"],
|
|
325
|
+
deploy: ["release", "publish", "ship", "rollout", "launch"],
|
|
326
|
+
security: ["auth", "permission", "access", "vulnerability", "secure"],
|
|
327
|
+
database: ["db", "sql", "query", "schema", "storage", "persist"],
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function expandQuery(query: string): string {
|
|
331
|
+
const trimmed = query.trim()
|
|
332
|
+
const tokens = trimmed.toLowerCase().split(/\s+/).filter(Boolean)
|
|
333
|
+
if (tokens.length > 1) return trimmed
|
|
334
|
+
const word = tokens[0]
|
|
335
|
+
if (!word) return trimmed
|
|
336
|
+
const synonyms = synonymMap[word]
|
|
337
|
+
if (!synonyms) return trimmed
|
|
338
|
+
const seen = new Set([word])
|
|
339
|
+
const expanded = [trimmed]
|
|
340
|
+
for (const syn of synonyms) {
|
|
341
|
+
const key = syn.toLowerCase()
|
|
342
|
+
if (!seen.has(key)) {
|
|
343
|
+
expanded.push(syn)
|
|
344
|
+
seen.add(key)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return expanded.join(" ")
|
|
348
|
+
}
|
package/search/types.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export type SearchResult = {
|
|
2
|
+
id: string
|
|
3
|
+
messageID: string
|
|
4
|
+
sessionID: string
|
|
5
|
+
sessionTitle: string
|
|
6
|
+
directory: string
|
|
7
|
+
role: "user" | "assistant"
|
|
8
|
+
partType: "text" | "reasoning" | "tool"
|
|
9
|
+
tool?: string
|
|
10
|
+
timeCreated: number
|
|
11
|
+
snippet: string
|
|
12
|
+
matchStart: number
|
|
13
|
+
matchEnd: number
|
|
14
|
+
before: string
|
|
15
|
+
match: string
|
|
16
|
+
after: string
|
|
17
|
+
excerpt: string
|
|
18
|
+
previewBefore: string
|
|
19
|
+
previewMatch: string
|
|
20
|
+
previewAfter: string
|
|
21
|
+
previewMode: "markdown" | "text"
|
|
22
|
+
previewHighlight: boolean
|
|
23
|
+
text: string
|
|
24
|
+
isVectorMatch: boolean
|
|
25
|
+
semanticScore: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type SearchRole = "user" | "assistant"
|
|
29
|
+
|
|
30
|
+
export type ConversationPreviewPart = {
|
|
31
|
+
id: string
|
|
32
|
+
messageID: string
|
|
33
|
+
sessionID: string
|
|
34
|
+
role: "user" | "assistant"
|
|
35
|
+
type: "text" | "reasoning" | "tool"
|
|
36
|
+
timeCreated: number
|
|
37
|
+
text: string
|
|
38
|
+
tool?: string
|
|
39
|
+
state?: ToolState
|
|
40
|
+
target: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ConversationPreviewPage = {
|
|
44
|
+
parts: ConversationPreviewPart[]
|
|
45
|
+
hasMoreBefore: boolean
|
|
46
|
+
hasMoreAfter: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ConversationPreviewCursor = {
|
|
50
|
+
id: string
|
|
51
|
+
timeCreated: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ToolState = {
|
|
55
|
+
status: "pending" | "running" | "completed" | "error"
|
|
56
|
+
input?: unknown
|
|
57
|
+
metadata?: unknown
|
|
58
|
+
output?: string
|
|
59
|
+
error?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type SemanticConfig = {
|
|
63
|
+
embedBaseUrl: string
|
|
64
|
+
embedModel?: string
|
|
65
|
+
disableVector: boolean
|
|
66
|
+
sqliteLibPath?: string
|
|
67
|
+
sqliteVecExtension?: string
|
|
68
|
+
hybridAlpha: number
|
|
69
|
+
documentPrefix: string
|
|
70
|
+
queryPrefix: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type VectorState = "enabled" | "disabled" | "unavailable" | "stale"
|
|
74
|
+
|
|
75
|
+
export type ScoredRow = Row & {
|
|
76
|
+
score: number
|
|
77
|
+
keywordScore: number
|
|
78
|
+
vectorScore: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type HybridSearchOptions = {
|
|
82
|
+
limit?: number
|
|
83
|
+
offset?: number
|
|
84
|
+
directory?: string
|
|
85
|
+
role?: SearchRole
|
|
86
|
+
dbPath?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type DocumentRow = {
|
|
90
|
+
doc_id: string
|
|
91
|
+
part_id: string
|
|
92
|
+
message_id: string
|
|
93
|
+
session_id: string
|
|
94
|
+
session_title: string
|
|
95
|
+
directory: string
|
|
96
|
+
role: string
|
|
97
|
+
part_type: string
|
|
98
|
+
tool: string | null
|
|
99
|
+
time_created: number
|
|
100
|
+
chunk_index: number
|
|
101
|
+
text: string
|
|
102
|
+
source_hash: string
|
|
103
|
+
extractor_version: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type Row = {
|
|
107
|
+
id: string
|
|
108
|
+
message_id: string
|
|
109
|
+
session_id: string
|
|
110
|
+
session_title: string | null
|
|
111
|
+
directory: string
|
|
112
|
+
role: SearchRole
|
|
113
|
+
part_type?: SearchResult["partType"]
|
|
114
|
+
tool?: string | null
|
|
115
|
+
time_created: number
|
|
116
|
+
text: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type IndexSourceRow = Omit<Row, "text"> & {
|
|
120
|
+
data: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type ConversationRow = {
|
|
124
|
+
id: string
|
|
125
|
+
message_id: string
|
|
126
|
+
session_id: string
|
|
127
|
+
role: SearchRole
|
|
128
|
+
type: "text" | "reasoning" | "tool"
|
|
129
|
+
time_created: number
|
|
130
|
+
data: string
|
|
131
|
+
}
|