agentikit 0.0.8 → 0.0.12
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 +135 -117
- package/dist/index.d.ts +13 -3
- package/dist/index.js +7 -1
- package/dist/src/asset-spec.d.ts +2 -0
- package/dist/src/asset-spec.js +22 -3
- package/dist/src/asset-type-handler.d.ts +27 -0
- package/dist/src/asset-type-handler.js +33 -0
- package/dist/src/cli.js +335 -100
- package/dist/src/common.d.ts +6 -1
- package/dist/src/common.js +18 -4
- package/dist/src/config-cli.d.ts +9 -0
- package/dist/src/config-cli.js +473 -0
- package/dist/src/config.d.ts +25 -6
- package/dist/src/config.js +188 -28
- package/dist/src/db.d.ts +46 -0
- package/dist/src/db.js +299 -0
- package/dist/src/embedder.js +12 -7
- package/dist/src/github.d.ts +4 -0
- package/dist/src/github.js +19 -0
- package/dist/src/handlers/agent-handler.d.ts +2 -0
- package/dist/src/handlers/agent-handler.js +26 -0
- package/dist/src/handlers/command-handler.d.ts +2 -0
- package/dist/src/handlers/command-handler.js +23 -0
- package/dist/src/handlers/index.d.ts +6 -0
- package/dist/src/handlers/index.js +23 -0
- package/dist/src/handlers/knowledge-handler.d.ts +2 -0
- package/dist/src/handlers/knowledge-handler.js +56 -0
- package/dist/src/handlers/markdown-helpers.d.ts +7 -0
- package/dist/src/handlers/markdown-helpers.js +15 -0
- package/dist/src/handlers/script-handler.d.ts +2 -0
- package/dist/src/handlers/script-handler.js +78 -0
- package/dist/src/handlers/skill-handler.d.ts +2 -0
- package/dist/src/handlers/skill-handler.js +30 -0
- package/dist/src/handlers/tool-handler.d.ts +2 -0
- package/dist/src/handlers/tool-handler.js +58 -0
- package/dist/src/indexer.d.ts +1 -23
- package/dist/src/indexer.js +162 -155
- package/dist/src/init.d.ts +2 -2
- package/dist/src/init.js +21 -9
- package/dist/src/llm.js +4 -3
- package/dist/src/metadata.d.ts +1 -1
- package/dist/src/metadata.js +22 -64
- package/dist/src/origin-resolve.d.ts +19 -0
- package/dist/src/origin-resolve.js +53 -0
- package/dist/src/registry-install.d.ts +11 -0
- package/dist/src/registry-install.js +315 -0
- package/dist/src/registry-resolve.d.ts +3 -0
- package/dist/src/registry-resolve.js +299 -0
- package/dist/src/registry-search.d.ts +27 -0
- package/dist/src/registry-search.js +263 -0
- package/dist/src/registry-types.d.ts +62 -0
- package/dist/src/registry-types.js +1 -0
- package/dist/src/stash-add.d.ts +4 -0
- package/dist/src/stash-add.js +59 -0
- package/dist/src/stash-clone.d.ts +22 -0
- package/dist/src/stash-clone.js +83 -0
- package/dist/src/stash-ref.d.ts +27 -3
- package/dist/src/stash-ref.js +63 -24
- package/dist/src/stash-registry.d.ts +18 -0
- package/dist/src/stash-registry.js +221 -0
- package/dist/src/stash-resolve.js +3 -0
- package/dist/src/stash-search.d.ts +3 -1
- package/dist/src/stash-search.js +357 -138
- package/dist/src/stash-show.d.ts +1 -1
- package/dist/src/stash-show.js +28 -89
- package/dist/src/stash-source.d.ts +24 -0
- package/dist/src/stash-source.js +81 -0
- package/dist/src/stash-types.d.ts +175 -1
- package/dist/src/stash.d.ts +9 -1
- package/dist/src/stash.js +5 -0
- package/dist/src/tool-runner.d.ts +1 -1
- package/dist/src/tool-runner.js +18 -5
- package/package.json +7 -2
- package/src/asset-spec.ts +20 -4
- package/src/asset-type-handler.ts +77 -0
- package/src/cli.ts +354 -103
- package/src/common.ts +23 -5
- package/src/config-cli.ts +499 -0
- package/src/config.ts +218 -37
- package/src/db.ts +411 -0
- package/src/embedder.ts +22 -11
- package/src/github.ts +21 -0
- package/src/handlers/agent-handler.ts +32 -0
- package/src/handlers/command-handler.ts +29 -0
- package/src/handlers/index.ts +25 -0
- package/src/handlers/knowledge-handler.ts +62 -0
- package/src/handlers/markdown-helpers.ts +19 -0
- package/src/handlers/script-handler.ts +92 -0
- package/src/handlers/skill-handler.ts +37 -0
- package/src/handlers/tool-handler.ts +71 -0
- package/src/indexer.ts +208 -187
- package/src/init.ts +17 -9
- package/src/llm.ts +4 -3
- package/src/metadata.ts +21 -65
- package/src/origin-resolve.ts +67 -0
- package/src/registry-install.ts +361 -0
- package/src/registry-resolve.ts +341 -0
- package/src/registry-search.ts +335 -0
- package/src/registry-types.ts +72 -0
- package/src/stash-add.ts +63 -0
- package/src/stash-clone.ts +127 -0
- package/src/stash-ref.ts +84 -26
- package/src/stash-registry.ts +259 -0
- package/src/stash-resolve.ts +3 -0
- package/src/stash-search.ts +425 -155
- package/src/stash-show.ts +33 -82
- package/src/stash-source.ts +103 -0
- package/src/stash-types.ts +186 -1
- package/src/stash.ts +23 -0
- package/src/tool-runner.ts +18 -5
- package/dist/src/similarity.d.ts +0 -34
- package/src/similarity.ts +0 -271
package/src/stash-search.ts
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
import fs from "node:fs"
|
|
2
2
|
import path from "node:path"
|
|
3
|
-
import { type AgentikitAssetType
|
|
3
|
+
import { type AgentikitAssetType } from "./common"
|
|
4
4
|
import { ASSET_TYPES, TYPE_DIRS, deriveCanonicalAssetName } from "./asset-spec"
|
|
5
|
-
import {
|
|
6
|
-
import { TfIdfAdapter, type ScoredEntry } from "./similarity"
|
|
7
|
-
import { buildToolInfo } from "./tool-runner"
|
|
5
|
+
import { buildSearchText } from "./indexer"
|
|
8
6
|
import { walkStash } from "./walker"
|
|
9
|
-
import {
|
|
10
|
-
import type {
|
|
7
|
+
import { makeAssetRef } from "./stash-ref"
|
|
8
|
+
import type {
|
|
9
|
+
AgentikitSearchType,
|
|
10
|
+
LocalSearchHit,
|
|
11
|
+
RegistrySearchResultHit,
|
|
12
|
+
SearchHit,
|
|
13
|
+
SearchResponse,
|
|
14
|
+
SearchSource,
|
|
15
|
+
SearchUsageMode,
|
|
16
|
+
} from "./stash-types"
|
|
11
17
|
import { loadConfig } from "./config"
|
|
18
|
+
import { searchRegistry } from "./registry-search"
|
|
19
|
+
import {
|
|
20
|
+
openDatabase,
|
|
21
|
+
closeDatabase,
|
|
22
|
+
getDbPath,
|
|
23
|
+
getMeta,
|
|
24
|
+
searchFts,
|
|
25
|
+
searchVec,
|
|
26
|
+
getAllEntries,
|
|
27
|
+
getEntryCount,
|
|
28
|
+
getEntryById,
|
|
29
|
+
isVecAvailable,
|
|
30
|
+
type DbSearchResult,
|
|
31
|
+
} from "./db"
|
|
32
|
+
import { tryGetHandler } from "./asset-type-handler"
|
|
33
|
+
import { type StashSource, resolveStashSources, findSourceForPath } from "./stash-source"
|
|
12
34
|
|
|
13
35
|
type IndexedAsset = {
|
|
14
36
|
type: AgentikitAssetType
|
|
@@ -22,166 +44,324 @@ export async function agentikitSearch(input: {
|
|
|
22
44
|
query: string
|
|
23
45
|
type?: AgentikitSearchType
|
|
24
46
|
limit?: number
|
|
47
|
+
usage?: SearchUsageMode
|
|
48
|
+
source?: SearchSource
|
|
25
49
|
}): Promise<SearchResponse> {
|
|
26
50
|
const t0 = Date.now()
|
|
27
|
-
const query = input.query.trim()
|
|
51
|
+
const query = input.query.trim()
|
|
52
|
+
const normalizedQuery = query.toLowerCase()
|
|
28
53
|
const searchType = input.type ?? "any"
|
|
29
54
|
const limit = normalizeLimit(input.limit)
|
|
30
|
-
const
|
|
31
|
-
const
|
|
55
|
+
const usageMode = parseSearchUsageMode(input.usage)
|
|
56
|
+
const source = parseSearchSource(input.source)
|
|
57
|
+
const sources = resolveStashSources()
|
|
58
|
+
const stashDir = sources[0].path
|
|
59
|
+
const localResult = source === "registry"
|
|
60
|
+
? undefined
|
|
61
|
+
: await searchLocal({
|
|
62
|
+
query: normalizedQuery,
|
|
63
|
+
searchType,
|
|
64
|
+
limit,
|
|
65
|
+
usageMode,
|
|
66
|
+
stashDir,
|
|
67
|
+
sources,
|
|
68
|
+
})
|
|
32
69
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}),
|
|
38
|
-
]
|
|
70
|
+
const config = loadConfig()
|
|
71
|
+
const registryResult = source === "local"
|
|
72
|
+
? undefined
|
|
73
|
+
: await searchRegistry(query, { limit, registryUrls: config.registryUrls })
|
|
39
74
|
|
|
40
|
-
|
|
41
|
-
const index = loadSearchIndex()
|
|
42
|
-
if (index && index.entries && index.entries.length > 0 && index.stashDir === stashDir) {
|
|
43
|
-
const { hits, embedMs, rankMs } = await searchIndex(index, query, searchType, limit, stashDir, allStashDirs, config)
|
|
75
|
+
if (source === "local") {
|
|
44
76
|
return {
|
|
45
77
|
stashDir,
|
|
78
|
+
source,
|
|
79
|
+
hits: localResult?.hits ?? [],
|
|
80
|
+
usageGuide: localResult?.usageGuide,
|
|
81
|
+
tip: localResult?.tip,
|
|
82
|
+
timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const registryHits = (registryResult?.hits ?? []).map((hit): RegistrySearchResultHit => {
|
|
87
|
+
const installRef = hit.source === "npm" ? `npm:${hit.ref}` : `github:${hit.ref}`
|
|
88
|
+
return {
|
|
89
|
+
hitSource: "registry",
|
|
90
|
+
type: "registry",
|
|
91
|
+
name: hit.title,
|
|
92
|
+
id: hit.id,
|
|
93
|
+
registrySource: hit.source,
|
|
94
|
+
ref: hit.ref,
|
|
95
|
+
description: hit.description,
|
|
96
|
+
homepage: hit.homepage,
|
|
97
|
+
score: hit.score,
|
|
98
|
+
metadata: hit.metadata,
|
|
99
|
+
curated: hit.curated,
|
|
100
|
+
installRef,
|
|
101
|
+
installCmd: `akm add ${installRef}`,
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
if (source === "registry") {
|
|
106
|
+
const hits = registryHits.slice(0, limit)
|
|
107
|
+
return {
|
|
108
|
+
stashDir,
|
|
109
|
+
source,
|
|
46
110
|
hits,
|
|
47
|
-
tip: hits.length === 0 ? "No matching
|
|
48
|
-
|
|
111
|
+
tip: hits.length === 0 ? "No matching registry entries were found." : undefined,
|
|
112
|
+
warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
|
|
113
|
+
timing: { totalMs: Date.now() - t0 },
|
|
49
114
|
}
|
|
50
115
|
}
|
|
51
116
|
|
|
52
|
-
|
|
53
|
-
const hits = allStashDirs
|
|
54
|
-
.flatMap((dir) => substringSearch(query, searchType, limit, dir))
|
|
55
|
-
.slice(0, limit)
|
|
117
|
+
const mergedHits = mergeSearchHits(localResult?.hits ?? [], registryHits, limit)
|
|
56
118
|
|
|
57
119
|
return {
|
|
58
120
|
stashDir,
|
|
121
|
+
source,
|
|
122
|
+
hits: mergedHits,
|
|
123
|
+
usageGuide: localResult?.usageGuide,
|
|
124
|
+
tip: mergedHits.length === 0 ? "No matching stash assets or registry entries were found." : undefined,
|
|
125
|
+
warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
|
|
126
|
+
timing: { totalMs: Date.now() - t0 },
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function searchLocal(input: {
|
|
131
|
+
query: string
|
|
132
|
+
searchType: AgentikitSearchType
|
|
133
|
+
limit: number
|
|
134
|
+
usageMode: SearchUsageMode
|
|
135
|
+
stashDir: string
|
|
136
|
+
sources: StashSource[]
|
|
137
|
+
}): Promise<{ hits: LocalSearchHit[]; usageGuide?: Partial<Record<AgentikitAssetType, string[]>>; tip?: string; embedMs?: number; rankMs?: number }> {
|
|
138
|
+
const { query, searchType, limit, usageMode, stashDir, sources } = input
|
|
139
|
+
const config = loadConfig()
|
|
140
|
+
const allStashDirs = sources.map((s) => s.path)
|
|
141
|
+
|
|
142
|
+
// Try to open the database
|
|
143
|
+
const dbPath = getDbPath()
|
|
144
|
+
try {
|
|
145
|
+
if (fs.existsSync(dbPath)) {
|
|
146
|
+
const embeddingDim = config.embedding?.dimension
|
|
147
|
+
const db = openDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined)
|
|
148
|
+
try {
|
|
149
|
+
const entryCount = getEntryCount(db)
|
|
150
|
+
const storedStashDir = getMeta(db, "stashDir")
|
|
151
|
+
if (entryCount > 0 && storedStashDir === stashDir) {
|
|
152
|
+
const { hits, usageGuide, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allStashDirs, config, usageMode, sources)
|
|
153
|
+
return {
|
|
154
|
+
hits,
|
|
155
|
+
usageGuide,
|
|
156
|
+
tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
|
|
157
|
+
embedMs,
|
|
158
|
+
rankMs,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
closeDatabase(db)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.warn("Search index unavailable, falling back to substring search:", error instanceof Error ? error.message : String(error))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const hits = allStashDirs
|
|
170
|
+
.flatMap((dir) => substringSearch(query, searchType, limit, dir, sources))
|
|
171
|
+
.slice(0, limit)
|
|
172
|
+
const usageGuide = shouldIncludeUsageGuide(usageMode) ? buildUsageGuide(hits.map((hit) => hit.type), searchType) : undefined
|
|
173
|
+
return {
|
|
59
174
|
hits,
|
|
175
|
+
usageGuide,
|
|
60
176
|
tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
|
|
61
|
-
timing: { totalMs: Date.now() - t0 },
|
|
62
177
|
}
|
|
63
178
|
}
|
|
64
179
|
|
|
65
|
-
// ──
|
|
180
|
+
// ── Database search ─────────────────────────────────────────────────────────
|
|
66
181
|
|
|
67
|
-
async function
|
|
68
|
-
|
|
182
|
+
async function searchDatabase(
|
|
183
|
+
db: import("bun:sqlite").Database,
|
|
69
184
|
query: string,
|
|
70
185
|
searchType: AgentikitSearchType,
|
|
71
186
|
limit: number,
|
|
72
187
|
stashDir: string,
|
|
73
188
|
allStashDirs: string[],
|
|
74
189
|
config: import("./config").AgentikitConfig,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
candidates = candidates.filter((ie) => ie.entry.type === searchType)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (candidates.length === 0) return { hits: [] }
|
|
83
|
-
|
|
84
|
-
// Empty query: return all entries (no scoring needed)
|
|
190
|
+
usageMode: SearchUsageMode,
|
|
191
|
+
sources: StashSource[],
|
|
192
|
+
): Promise<{ hits: LocalSearchHit[]; usageGuide?: Partial<Record<AgentikitAssetType, string[]>>; embedMs?: number; rankMs?: number }> {
|
|
193
|
+
// Empty query: return all entries
|
|
85
194
|
if (!query) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
195
|
+
const typeFilter = searchType === "any" ? undefined : searchType
|
|
196
|
+
const allEntries = getAllEntries(db, typeFilter)
|
|
197
|
+
const selected = allEntries.slice(0, limit)
|
|
198
|
+
const hits = selected.map((ie) =>
|
|
199
|
+
buildDbHit({
|
|
200
|
+
entry: ie.entry,
|
|
201
|
+
path: ie.filePath,
|
|
202
|
+
score: 1,
|
|
203
|
+
query,
|
|
204
|
+
rankingMode: "fts",
|
|
205
|
+
defaultStashDir: stashDir,
|
|
206
|
+
allStashDirs,
|
|
207
|
+
sources,
|
|
208
|
+
includeItemUsage: shouldIncludeItemUsage(usageMode),
|
|
209
|
+
}),
|
|
210
|
+
)
|
|
211
|
+
return {
|
|
212
|
+
hits,
|
|
213
|
+
usageGuide: shouldIncludeUsageGuide(usageMode)
|
|
214
|
+
? buildUsageGuideFromEntries(selected.map((e) => e.entry), searchType)
|
|
215
|
+
: undefined,
|
|
216
|
+
}
|
|
89
217
|
}
|
|
90
218
|
|
|
91
|
-
// Score
|
|
219
|
+
// Score using FTS5 (BM25) and optionally sqlite-vec
|
|
92
220
|
const tEmbed0 = Date.now()
|
|
93
|
-
const embeddingScores = await
|
|
221
|
+
const embeddingScores = await tryVecScores(db, query, limit * 3, config)
|
|
94
222
|
const embedMs = Date.now() - tEmbed0
|
|
95
223
|
|
|
96
224
|
const tRank0 = Date.now()
|
|
97
|
-
const
|
|
225
|
+
const typeFilter = searchType === "any" ? undefined : searchType
|
|
226
|
+
const ftsResults = searchFts(db, query, limit * 3, typeFilter)
|
|
227
|
+
|
|
228
|
+
// Build score map from FTS results (normalize BM25 scores)
|
|
229
|
+
const ftsScoreMap = new Map<number, { score: number; result: DbSearchResult }>()
|
|
230
|
+
for (const r of ftsResults) {
|
|
231
|
+
// BM25 returns negative scores (more negative = better match), normalize to 0-1
|
|
232
|
+
const absScore = Math.abs(r.bm25Score)
|
|
233
|
+
const normalized = absScore / (1 + absScore)
|
|
234
|
+
ftsScoreMap.set(r.id, { score: normalized, result: r })
|
|
235
|
+
}
|
|
98
236
|
|
|
99
|
-
|
|
237
|
+
// Blend scores
|
|
238
|
+
const scored: Array<{ id: number; entry: import("./metadata").StashEntry; filePath: string; score: number; rankingMode: "semantic" | "fts" }> = []
|
|
239
|
+
const seenIds = new Set<number>()
|
|
100
240
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
241
|
+
// Process FTS results
|
|
242
|
+
for (const [id, { score: ftsScore, result }] of ftsScoreMap) {
|
|
243
|
+
seenIds.add(id)
|
|
244
|
+
const embScore = embeddingScores?.get(id)
|
|
105
245
|
|
|
106
246
|
if (embScore !== undefined) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
scored.push({ ie, score: tfidfScore, rankingMode: "tfidf" })
|
|
247
|
+
const blended = embScore * 0.7 + ftsScore * 0.3
|
|
248
|
+
if (blended > 0) scored.push({ id, entry: result.entry, filePath: result.filePath, score: blended, rankingMode: "semantic" })
|
|
249
|
+
} else if (ftsScore > 0) {
|
|
250
|
+
scored.push({ id, entry: result.entry, filePath: result.filePath, score: ftsScore, rankingMode: "fts" })
|
|
112
251
|
}
|
|
113
252
|
}
|
|
114
253
|
|
|
254
|
+
// Add vec-only results not already in FTS results
|
|
255
|
+
if (embeddingScores) {
|
|
256
|
+
for (const [id, embScore] of embeddingScores) {
|
|
257
|
+
if (seenIds.has(id)) continue
|
|
258
|
+
const found = getEntryById(db, id)
|
|
259
|
+
if (found) {
|
|
260
|
+
if (typeFilter && found.entry.type !== typeFilter) continue
|
|
261
|
+
scored.push({
|
|
262
|
+
id,
|
|
263
|
+
entry: found.entry,
|
|
264
|
+
filePath: found.filePath,
|
|
265
|
+
score: embScore,
|
|
266
|
+
rankingMode: "semantic",
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Apply boosts (tag, intent, name matches)
|
|
273
|
+
const queryTokens = query.toLowerCase().split(/\s+/).filter(Boolean)
|
|
274
|
+
for (const item of scored) {
|
|
275
|
+
const entry = item.entry
|
|
276
|
+
// Tag boost
|
|
277
|
+
if (entry.tags) {
|
|
278
|
+
for (const tag of entry.tags) {
|
|
279
|
+
if (queryTokens.some((t) => tag.toLowerCase() === t)) {
|
|
280
|
+
item.score += 0.15
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Intent boost
|
|
285
|
+
if (entry.intents) {
|
|
286
|
+
for (const intent of entry.intents) {
|
|
287
|
+
const intentLower = intent.toLowerCase()
|
|
288
|
+
for (const token of queryTokens) {
|
|
289
|
+
if (intentLower.includes(token)) {
|
|
290
|
+
item.score += 0.12
|
|
291
|
+
break
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Name boost
|
|
297
|
+
const nameLower = entry.name.toLowerCase().replace(/[-_]/g, " ")
|
|
298
|
+
if (queryTokens.some((t) => nameLower.includes(t))) {
|
|
299
|
+
item.score += 0.1
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
for (const item of scored) {
|
|
304
|
+
item.score = Math.min(item.score, 1.0)
|
|
305
|
+
}
|
|
306
|
+
|
|
115
307
|
scored.sort((a, b) => b.score - a.score)
|
|
116
308
|
const rankMs = Date.now() - tRank0
|
|
117
309
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
310
|
+
const selected = scored.slice(0, limit)
|
|
311
|
+
const hits = selected.map(({ entry, filePath, score, rankingMode }) =>
|
|
312
|
+
buildDbHit({
|
|
313
|
+
entry,
|
|
314
|
+
path: filePath,
|
|
122
315
|
score: Math.round(score * 1000) / 1000,
|
|
123
316
|
query,
|
|
124
317
|
rankingMode,
|
|
125
318
|
defaultStashDir: stashDir,
|
|
126
319
|
allStashDirs,
|
|
320
|
+
sources,
|
|
321
|
+
includeItemUsage: shouldIncludeItemUsage(usageMode),
|
|
127
322
|
}),
|
|
128
|
-
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
embedMs,
|
|
327
|
+
rankMs,
|
|
328
|
+
hits,
|
|
329
|
+
usageGuide: shouldIncludeUsageGuide(usageMode)
|
|
330
|
+
? buildUsageGuideFromEntries(selected.map((item) => item.entry), searchType)
|
|
331
|
+
: undefined,
|
|
332
|
+
}
|
|
129
333
|
}
|
|
130
334
|
|
|
131
|
-
// ──
|
|
335
|
+
// ── Vector scorer ───────────────────────────────────────────────────────────
|
|
132
336
|
|
|
133
|
-
async function
|
|
134
|
-
|
|
337
|
+
async function tryVecScores(
|
|
338
|
+
db: import("bun:sqlite").Database,
|
|
135
339
|
query: string,
|
|
340
|
+
k: number,
|
|
136
341
|
config: import("./config").AgentikitConfig,
|
|
137
|
-
): Promise<Map<
|
|
138
|
-
if (!config.semanticSearch) return null
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (withEmbeddings.length === 0) return null
|
|
342
|
+
): Promise<Map<number, number> | null> {
|
|
343
|
+
if (!config.semanticSearch || !isVecAvailable()) return null
|
|
344
|
+
const hasEmbeddings = getMeta(db, "hasEmbeddings")
|
|
345
|
+
if (hasEmbeddings !== "1") return null
|
|
142
346
|
|
|
143
347
|
try {
|
|
144
|
-
const { embed
|
|
348
|
+
const { embed } = await import("./embedder.js")
|
|
145
349
|
const queryEmbedding = await embed(query, config.embedding)
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
350
|
+
const vecResults = searchVec(db, queryEmbedding, k)
|
|
351
|
+
|
|
352
|
+
const scores = new Map<number, number>()
|
|
353
|
+
for (const { id, distance } of vecResults) {
|
|
354
|
+
// Convert L2 distance to cosine similarity (vectors are normalized)
|
|
355
|
+
const cosineSim = 1 - (distance * distance) / 2
|
|
356
|
+
scores.set(id, Math.max(0, cosineSim))
|
|
149
357
|
}
|
|
150
358
|
return scores
|
|
151
|
-
} catch {
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.warn("Vector search failed, skipping:", error instanceof Error ? error.message : String(error))
|
|
152
361
|
return null
|
|
153
362
|
}
|
|
154
363
|
}
|
|
155
364
|
|
|
156
|
-
// ── TF-IDF scorer ───────────────────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
function computeTfidfScores(
|
|
159
|
-
index: import("./indexer").SearchIndex,
|
|
160
|
-
candidates: IndexedEntry[],
|
|
161
|
-
query: string,
|
|
162
|
-
searchType: AgentikitSearchType,
|
|
163
|
-
): Map<string, number> {
|
|
164
|
-
const candidateScoredEntries = toScoredEntries(candidates)
|
|
165
|
-
|
|
166
|
-
let adapter: TfIdfAdapter
|
|
167
|
-
if (index.tfidf) {
|
|
168
|
-
const allScored = toScoredEntries(index.entries)
|
|
169
|
-
adapter = TfIdfAdapter.deserialize(index.tfidf, allScored)
|
|
170
|
-
} else {
|
|
171
|
-
adapter = new TfIdfAdapter()
|
|
172
|
-
adapter.buildIndex(candidateScoredEntries)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const typeFilter = searchType === "any" ? undefined : searchType
|
|
176
|
-
const results = adapter.search(query, candidates.length, typeFilter)
|
|
177
|
-
|
|
178
|
-
const scores = new Map<string, number>()
|
|
179
|
-
for (const r of results) {
|
|
180
|
-
scores.set(r.path, r.score)
|
|
181
|
-
}
|
|
182
|
-
return scores
|
|
183
|
-
}
|
|
184
|
-
|
|
185
365
|
// ── Substring fallback (no index) ───────────────────────────────────────────
|
|
186
366
|
|
|
187
367
|
function substringSearch(
|
|
@@ -189,77 +369,76 @@ function substringSearch(
|
|
|
189
369
|
searchType: AgentikitSearchType,
|
|
190
370
|
limit: number,
|
|
191
371
|
stashDir: string,
|
|
192
|
-
|
|
372
|
+
sources: StashSource[],
|
|
373
|
+
): LocalSearchHit[] {
|
|
193
374
|
const assets = indexAssets(stashDir, searchType)
|
|
194
375
|
return assets
|
|
195
376
|
.filter((asset) => asset.name.toLowerCase().includes(query))
|
|
196
377
|
.sort(compareAssets)
|
|
197
378
|
.slice(0, limit)
|
|
198
|
-
.map((asset)
|
|
379
|
+
.map((asset) => assetToSearchHit(asset, stashDir, sources))
|
|
199
380
|
}
|
|
200
381
|
|
|
201
382
|
// ── Hit building ────────────────────────────────────────────────────────────
|
|
202
383
|
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
for (const dir of stashDirs) {
|
|
206
|
-
if (resolved.startsWith(path.resolve(dir) + path.sep)) return dir
|
|
207
|
-
}
|
|
208
|
-
return undefined
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function buildIndexedHit(input: {
|
|
212
|
-
entry: IndexedEntry["entry"]
|
|
384
|
+
function buildDbHit(input: {
|
|
385
|
+
entry: import("./metadata").StashEntry
|
|
213
386
|
path: string
|
|
214
387
|
score: number
|
|
215
388
|
query: string
|
|
216
|
-
rankingMode: "semantic" | "
|
|
389
|
+
rankingMode: "semantic" | "fts"
|
|
217
390
|
defaultStashDir: string
|
|
218
391
|
allStashDirs: string[]
|
|
219
|
-
|
|
220
|
-
|
|
392
|
+
sources: StashSource[]
|
|
393
|
+
includeItemUsage: boolean
|
|
394
|
+
}): LocalSearchHit {
|
|
395
|
+
const entryStashDir = findSourceForPath(input.path, input.sources)?.path ?? input.defaultStashDir
|
|
221
396
|
const typeRoot = path.join(entryStashDir, TYPE_DIRS[input.entry.type])
|
|
222
397
|
const openRefName = deriveCanonicalAssetName(input.entry.type, typeRoot, input.path)
|
|
223
398
|
?? input.entry.name
|
|
224
399
|
|
|
225
400
|
const qualityBoost = input.entry.generated === true ? 0 : 0.05
|
|
226
401
|
const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0
|
|
227
|
-
const score = Math.round((input.score + qualityBoost + confidenceBoost) * 1000) / 1000
|
|
402
|
+
const score = Math.min(Math.round((input.score + qualityBoost + confidenceBoost) * 1000) / 1000, 1.0)
|
|
228
403
|
|
|
229
404
|
const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost)
|
|
230
405
|
|
|
231
|
-
const
|
|
406
|
+
const source = findSourceForPath(input.path, input.sources)
|
|
407
|
+
|
|
408
|
+
const hit: LocalSearchHit = {
|
|
409
|
+
hitSource: "local",
|
|
232
410
|
type: input.entry.type,
|
|
233
411
|
name: input.entry.name,
|
|
234
412
|
path: input.path,
|
|
235
|
-
openRef:
|
|
413
|
+
openRef: makeAssetRef(input.entry.type, openRefName, source?.registryId),
|
|
414
|
+
registryId: source?.registryId,
|
|
415
|
+
editable: source?.writable ?? false,
|
|
236
416
|
description: input.entry.description,
|
|
237
417
|
tags: input.entry.tags,
|
|
238
418
|
score,
|
|
239
419
|
whyMatched,
|
|
240
420
|
}
|
|
241
421
|
|
|
242
|
-
if (input.entry.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
422
|
+
if (input.includeItemUsage && input.entry.usage && input.entry.usage.length > 0) {
|
|
423
|
+
hit.usage = input.entry.usage
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const handler = tryGetHandler(input.entry.type)
|
|
427
|
+
if (handler?.enrichSearchHit) {
|
|
428
|
+
handler.enrichSearchHit(hit, entryStashDir)
|
|
250
429
|
}
|
|
251
430
|
|
|
252
431
|
return hit
|
|
253
432
|
}
|
|
254
433
|
|
|
255
434
|
function buildWhyMatched(
|
|
256
|
-
entry:
|
|
435
|
+
entry: import("./metadata").StashEntry,
|
|
257
436
|
query: string,
|
|
258
|
-
rankingMode: "semantic" | "
|
|
437
|
+
rankingMode: "semantic" | "fts",
|
|
259
438
|
qualityBoost: number,
|
|
260
439
|
confidenceBoost: number,
|
|
261
440
|
): string[] {
|
|
262
|
-
const reasons: string[] = [rankingMode === "semantic" ? "semantic similarity" : "
|
|
441
|
+
const reasons: string[] = [rankingMode === "semantic" ? "semantic similarity" : "fts bm25 relevance"]
|
|
263
442
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean)
|
|
264
443
|
|
|
265
444
|
const name = entry.name.toLowerCase()
|
|
@@ -279,33 +458,22 @@ function buildWhyMatched(
|
|
|
279
458
|
|
|
280
459
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
281
460
|
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
path: ie.path,
|
|
288
|
-
}))
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function assetToSearchHit(asset: IndexedAsset, stashDir: string): SearchHit {
|
|
292
|
-
if (asset.type !== "tool") {
|
|
293
|
-
return {
|
|
294
|
-
type: asset.type,
|
|
295
|
-
name: asset.name,
|
|
296
|
-
path: asset.path,
|
|
297
|
-
openRef: makeOpenRef(asset.type, asset.name),
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
const toolInfo = buildToolInfo(stashDir, asset.path)
|
|
301
|
-
return {
|
|
302
|
-
type: "tool",
|
|
461
|
+
function assetToSearchHit(asset: IndexedAsset, stashDir: string, sources: StashSource[]): LocalSearchHit {
|
|
462
|
+
const source = findSourceForPath(asset.path, sources)
|
|
463
|
+
const hit: LocalSearchHit = {
|
|
464
|
+
hitSource: "local",
|
|
465
|
+
type: asset.type,
|
|
303
466
|
name: asset.name,
|
|
304
467
|
path: asset.path,
|
|
305
|
-
openRef:
|
|
306
|
-
|
|
307
|
-
|
|
468
|
+
openRef: makeAssetRef(asset.type, asset.name, source?.registryId),
|
|
469
|
+
registryId: source?.registryId,
|
|
470
|
+
editable: source?.writable ?? false,
|
|
308
471
|
}
|
|
472
|
+
const handler = tryGetHandler(asset.type)
|
|
473
|
+
if (handler?.enrichSearchHit) {
|
|
474
|
+
handler.enrichSearchHit(hit, stashDir)
|
|
475
|
+
}
|
|
476
|
+
return hit
|
|
309
477
|
}
|
|
310
478
|
|
|
311
479
|
function normalizeLimit(limit?: number): number {
|
|
@@ -315,6 +483,108 @@ function normalizeLimit(limit?: number): number {
|
|
|
315
483
|
return Math.min(Math.floor(limit), 200)
|
|
316
484
|
}
|
|
317
485
|
|
|
486
|
+
function parseSearchUsageMode(mode: SearchUsageMode | undefined): SearchUsageMode {
|
|
487
|
+
if (mode === "none" || mode === "both" || mode === "item" || mode === "guide") {
|
|
488
|
+
return mode
|
|
489
|
+
}
|
|
490
|
+
if (typeof mode === "undefined") return "both"
|
|
491
|
+
throw new Error(`Invalid usage mode: ${String(mode)}. Expected one of: none|both|item|guide`)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function parseSearchSource(source: SearchSource | undefined): SearchSource {
|
|
495
|
+
if (source === "local" || source === "registry" || source === "both") return source
|
|
496
|
+
if (typeof source === "undefined") return "local"
|
|
497
|
+
throw new Error(`Invalid search source: ${String(source)}. Expected one of: local|registry|both`)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function mergeSearchHits(localHits: LocalSearchHit[], registryHits: RegistrySearchResultHit[], limit: number): SearchHit[] {
|
|
501
|
+
const merged: SearchHit[] = []
|
|
502
|
+
let localIndex = 0
|
|
503
|
+
let registryIndex = 0
|
|
504
|
+
|
|
505
|
+
while (merged.length < limit && (localIndex < localHits.length || registryIndex < registryHits.length)) {
|
|
506
|
+
if (localIndex < localHits.length) {
|
|
507
|
+
merged.push(localHits[localIndex])
|
|
508
|
+
localIndex += 1
|
|
509
|
+
if (merged.length >= limit) break
|
|
510
|
+
}
|
|
511
|
+
if (registryIndex < registryHits.length) {
|
|
512
|
+
merged.push(registryHits[registryIndex])
|
|
513
|
+
registryIndex += 1
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return merged
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function shouldIncludeUsageGuide(mode: SearchUsageMode): boolean {
|
|
521
|
+
return mode === "both" || mode === "guide"
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function shouldIncludeItemUsage(mode: SearchUsageMode): boolean {
|
|
525
|
+
return mode === "both" || mode === "item"
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function buildUsageGuideFromEntries(
|
|
529
|
+
entries: import("./metadata").StashEntry[],
|
|
530
|
+
searchType: AgentikitSearchType,
|
|
531
|
+
): Partial<Record<AgentikitAssetType, string[]>> | undefined {
|
|
532
|
+
const types = entries.map((entry) => entry.type)
|
|
533
|
+
const fallbackGuide = buildUsageGuide(types, searchType)
|
|
534
|
+
const metadataByType = new Map<AgentikitAssetType, string[]>()
|
|
535
|
+
|
|
536
|
+
for (const entry of entries) {
|
|
537
|
+
if (!entry.usage || entry.usage.length === 0) continue
|
|
538
|
+
const current = metadataByType.get(entry.type) ?? []
|
|
539
|
+
for (const item of entry.usage) {
|
|
540
|
+
const trimmed = item.trim()
|
|
541
|
+
if (trimmed && !current.includes(trimmed)) current.push(trimmed)
|
|
542
|
+
}
|
|
543
|
+
if (current.length > 0) metadataByType.set(entry.type, current)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (!fallbackGuide && metadataByType.size === 0) return undefined
|
|
547
|
+
|
|
548
|
+
const result: Partial<Record<AgentikitAssetType, string[]>> = {}
|
|
549
|
+
for (const assetType of resolveGuideTypes(types, searchType)) {
|
|
550
|
+
const lines: string[] = []
|
|
551
|
+
const metadataLines = metadataByType.get(assetType)
|
|
552
|
+
if (metadataLines && metadataLines.length > 0) {
|
|
553
|
+
lines.push(...metadataLines)
|
|
554
|
+
}
|
|
555
|
+
const fallbackLines = fallbackGuide?.[assetType]
|
|
556
|
+
if (fallbackLines && fallbackLines.length > 0) {
|
|
557
|
+
for (const line of fallbackLines) {
|
|
558
|
+
if (!lines.includes(line)) lines.push(line)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (lines.length > 0) result[assetType] = lines
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function buildUsageGuide(
|
|
568
|
+
hitTypes: AgentikitAssetType[],
|
|
569
|
+
searchType: AgentikitSearchType,
|
|
570
|
+
): Partial<Record<AgentikitAssetType, string[]>> | undefined {
|
|
571
|
+
const result: Partial<Record<AgentikitAssetType, string[]>> = {}
|
|
572
|
+
for (const assetType of resolveGuideTypes(hitTypes, searchType)) {
|
|
573
|
+
result[assetType] = usageGuideByType(assetType)
|
|
574
|
+
}
|
|
575
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function resolveGuideTypes(hitTypes: AgentikitAssetType[], searchType: AgentikitSearchType): AgentikitAssetType[] {
|
|
579
|
+
if (searchType !== "any") return [searchType]
|
|
580
|
+
return Array.from(new Set(hitTypes))
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function usageGuideByType(type: AgentikitAssetType): string[] {
|
|
584
|
+
const handler = tryGetHandler(type)
|
|
585
|
+
return handler?.defaultUsageGuide ?? []
|
|
586
|
+
}
|
|
587
|
+
|
|
318
588
|
function fileToAsset(assetType: AgentikitAssetType, root: string, file: string): IndexedAsset | undefined {
|
|
319
589
|
const name = deriveCanonicalAssetName(assetType, root, file)
|
|
320
590
|
if (!name) return undefined
|