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.
Files changed (112) hide show
  1. package/README.md +135 -117
  2. package/dist/index.d.ts +13 -3
  3. package/dist/index.js +7 -1
  4. package/dist/src/asset-spec.d.ts +2 -0
  5. package/dist/src/asset-spec.js +22 -3
  6. package/dist/src/asset-type-handler.d.ts +27 -0
  7. package/dist/src/asset-type-handler.js +33 -0
  8. package/dist/src/cli.js +335 -100
  9. package/dist/src/common.d.ts +6 -1
  10. package/dist/src/common.js +18 -4
  11. package/dist/src/config-cli.d.ts +9 -0
  12. package/dist/src/config-cli.js +473 -0
  13. package/dist/src/config.d.ts +25 -6
  14. package/dist/src/config.js +188 -28
  15. package/dist/src/db.d.ts +46 -0
  16. package/dist/src/db.js +299 -0
  17. package/dist/src/embedder.js +12 -7
  18. package/dist/src/github.d.ts +4 -0
  19. package/dist/src/github.js +19 -0
  20. package/dist/src/handlers/agent-handler.d.ts +2 -0
  21. package/dist/src/handlers/agent-handler.js +26 -0
  22. package/dist/src/handlers/command-handler.d.ts +2 -0
  23. package/dist/src/handlers/command-handler.js +23 -0
  24. package/dist/src/handlers/index.d.ts +6 -0
  25. package/dist/src/handlers/index.js +23 -0
  26. package/dist/src/handlers/knowledge-handler.d.ts +2 -0
  27. package/dist/src/handlers/knowledge-handler.js +56 -0
  28. package/dist/src/handlers/markdown-helpers.d.ts +7 -0
  29. package/dist/src/handlers/markdown-helpers.js +15 -0
  30. package/dist/src/handlers/script-handler.d.ts +2 -0
  31. package/dist/src/handlers/script-handler.js +78 -0
  32. package/dist/src/handlers/skill-handler.d.ts +2 -0
  33. package/dist/src/handlers/skill-handler.js +30 -0
  34. package/dist/src/handlers/tool-handler.d.ts +2 -0
  35. package/dist/src/handlers/tool-handler.js +58 -0
  36. package/dist/src/indexer.d.ts +1 -23
  37. package/dist/src/indexer.js +162 -155
  38. package/dist/src/init.d.ts +2 -2
  39. package/dist/src/init.js +21 -9
  40. package/dist/src/llm.js +4 -3
  41. package/dist/src/metadata.d.ts +1 -1
  42. package/dist/src/metadata.js +22 -64
  43. package/dist/src/origin-resolve.d.ts +19 -0
  44. package/dist/src/origin-resolve.js +53 -0
  45. package/dist/src/registry-install.d.ts +11 -0
  46. package/dist/src/registry-install.js +315 -0
  47. package/dist/src/registry-resolve.d.ts +3 -0
  48. package/dist/src/registry-resolve.js +299 -0
  49. package/dist/src/registry-search.d.ts +27 -0
  50. package/dist/src/registry-search.js +263 -0
  51. package/dist/src/registry-types.d.ts +62 -0
  52. package/dist/src/registry-types.js +1 -0
  53. package/dist/src/stash-add.d.ts +4 -0
  54. package/dist/src/stash-add.js +59 -0
  55. package/dist/src/stash-clone.d.ts +22 -0
  56. package/dist/src/stash-clone.js +83 -0
  57. package/dist/src/stash-ref.d.ts +27 -3
  58. package/dist/src/stash-ref.js +63 -24
  59. package/dist/src/stash-registry.d.ts +18 -0
  60. package/dist/src/stash-registry.js +221 -0
  61. package/dist/src/stash-resolve.js +3 -0
  62. package/dist/src/stash-search.d.ts +3 -1
  63. package/dist/src/stash-search.js +357 -138
  64. package/dist/src/stash-show.d.ts +1 -1
  65. package/dist/src/stash-show.js +28 -89
  66. package/dist/src/stash-source.d.ts +24 -0
  67. package/dist/src/stash-source.js +81 -0
  68. package/dist/src/stash-types.d.ts +175 -1
  69. package/dist/src/stash.d.ts +9 -1
  70. package/dist/src/stash.js +5 -0
  71. package/dist/src/tool-runner.d.ts +1 -1
  72. package/dist/src/tool-runner.js +18 -5
  73. package/package.json +7 -2
  74. package/src/asset-spec.ts +20 -4
  75. package/src/asset-type-handler.ts +77 -0
  76. package/src/cli.ts +354 -103
  77. package/src/common.ts +23 -5
  78. package/src/config-cli.ts +499 -0
  79. package/src/config.ts +218 -37
  80. package/src/db.ts +411 -0
  81. package/src/embedder.ts +22 -11
  82. package/src/github.ts +21 -0
  83. package/src/handlers/agent-handler.ts +32 -0
  84. package/src/handlers/command-handler.ts +29 -0
  85. package/src/handlers/index.ts +25 -0
  86. package/src/handlers/knowledge-handler.ts +62 -0
  87. package/src/handlers/markdown-helpers.ts +19 -0
  88. package/src/handlers/script-handler.ts +92 -0
  89. package/src/handlers/skill-handler.ts +37 -0
  90. package/src/handlers/tool-handler.ts +71 -0
  91. package/src/indexer.ts +208 -187
  92. package/src/init.ts +17 -9
  93. package/src/llm.ts +4 -3
  94. package/src/metadata.ts +21 -65
  95. package/src/origin-resolve.ts +67 -0
  96. package/src/registry-install.ts +361 -0
  97. package/src/registry-resolve.ts +341 -0
  98. package/src/registry-search.ts +335 -0
  99. package/src/registry-types.ts +72 -0
  100. package/src/stash-add.ts +63 -0
  101. package/src/stash-clone.ts +127 -0
  102. package/src/stash-ref.ts +84 -26
  103. package/src/stash-registry.ts +259 -0
  104. package/src/stash-resolve.ts +3 -0
  105. package/src/stash-search.ts +425 -155
  106. package/src/stash-show.ts +33 -82
  107. package/src/stash-source.ts +103 -0
  108. package/src/stash-types.ts +186 -1
  109. package/src/stash.ts +23 -0
  110. package/src/tool-runner.ts +18 -5
  111. package/dist/src/similarity.d.ts +0 -34
  112. package/src/similarity.ts +0 -271
@@ -1,14 +1,36 @@
1
1
  import fs from "node:fs"
2
2
  import path from "node:path"
3
- import { type AgentikitAssetType, hasErrnoCode, resolveStashDir } from "./common"
3
+ import { type AgentikitAssetType } from "./common"
4
4
  import { ASSET_TYPES, TYPE_DIRS, deriveCanonicalAssetName } from "./asset-spec"
5
- import { loadSearchIndex, buildSearchText, type IndexedEntry } from "./indexer"
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 { makeOpenRef } from "./stash-ref"
10
- import type { AgentikitSearchType, SearchHit, SearchResponse } from "./stash-types"
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().toLowerCase()
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 stashDir = resolveStashDir()
31
- const config = loadConfig(stashDir)
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 allStashDirs = [
34
- stashDir,
35
- ...config.additionalStashDirs.filter((d) => {
36
- try { return fs.statSync(d).isDirectory() } catch { return false }
37
- }),
38
- ]
70
+ const config = loadConfig()
71
+ const registryResult = source === "local"
72
+ ? undefined
73
+ : await searchRegistry(query, { limit, registryUrls: config.registryUrls })
39
74
 
40
- // Try indexed search (single unified pipeline: embedding + TF-IDF as weighted features)
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 stash assets were found. Try running 'akm index' to rebuild." : undefined,
48
- timing: { totalMs: Date.now() - t0, rankMs, embedMs },
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
- // No index: fall back to filesystem walk + substring match across all stash dirs
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
- // ── Unified indexed search ──────────────────────────────────────────────────
180
+ // ── Database search ─────────────────────────────────────────────────────────
66
181
 
67
- async function searchIndex(
68
- index: import("./indexer").SearchIndex,
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
- ): Promise<{ hits: SearchHit[]; embedMs?: number; rankMs?: number }> {
76
- // Filter candidates by type
77
- let candidates = index.entries
78
- if (searchType !== "any") {
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
- return { hits: candidates.slice(0, limit).map((ie) =>
87
- buildIndexedHit({ entry: ie.entry, path: ie.path, score: 1, query, rankingMode: "tfidf", defaultStashDir: stashDir, allStashDirs }),
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 each candidate using available signals
219
+ // Score using FTS5 (BM25) and optionally sqlite-vec
92
220
  const tEmbed0 = Date.now()
93
- const embeddingScores = await tryEmbeddingScores(candidates, query, config)
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 tfidfScores = computeTfidfScores(index, candidates, query, searchType)
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
- const scored: Array<{ ie: IndexedEntry; score: number; rankingMode: "semantic" | "tfidf" }> = []
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
- for (const ie of candidates) {
102
- const key = ie.path
103
- const embScore = embeddingScores?.get(key)
104
- const tfidfScore = tfidfScores.get(key) ?? 0
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
- // Weighted blend: embedding dominates when available, TF-IDF boosts lexical matches
108
- const blended = embScore * 0.7 + tfidfScore * 0.3
109
- if (blended > 0) scored.push({ ie, score: blended, rankingMode: "semantic" })
110
- } else if (tfidfScore > 0) {
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
- return { embedMs, rankMs, hits: scored.slice(0, limit).map(({ ie, score, rankingMode }) =>
119
- buildIndexedHit({
120
- entry: ie.entry,
121
- path: ie.path,
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
- // ── Embedding scorer ────────────────────────────────────────────────────────
335
+ // ── Vector scorer ───────────────────────────────────────────────────────────
132
336
 
133
- async function tryEmbeddingScores(
134
- candidates: IndexedEntry[],
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<string, number> | null> {
138
- if (!config.semanticSearch) return null
139
-
140
- const withEmbeddings = candidates.filter((ie) => ie.embedding && ie.embedding.length > 0)
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, cosineSimilarity } = await import("./embedder.js")
348
+ const { embed } = await import("./embedder.js")
145
349
  const queryEmbedding = await embed(query, config.embedding)
146
- const scores = new Map<string, number>()
147
- for (const ie of withEmbeddings) {
148
- scores.set(ie.path, cosineSimilarity(queryEmbedding, ie.embedding!))
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
- ): SearchHit[] {
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): SearchHit => assetToSearchHit(asset, stashDir))
379
+ .map((asset) => assetToSearchHit(asset, stashDir, sources))
199
380
  }
200
381
 
201
382
  // ── Hit building ────────────────────────────────────────────────────────────
202
383
 
203
- function findStashDirForPath(filePath: string, stashDirs: string[]): string | undefined {
204
- const resolved = path.resolve(filePath)
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" | "tfidf"
389
+ rankingMode: "semantic" | "fts"
217
390
  defaultStashDir: string
218
391
  allStashDirs: string[]
219
- }): SearchHit {
220
- const entryStashDir = findStashDirForPath(input.path, input.allStashDirs) ?? input.defaultStashDir
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 hit: SearchHit = {
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: makeOpenRef(input.entry.type, openRefName),
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.type === "tool") {
243
- try {
244
- const toolInfo = buildToolInfo(entryStashDir, input.path)
245
- hit.runCmd = toolInfo.runCmd
246
- hit.kind = toolInfo.kind
247
- } catch (error: unknown) {
248
- if (!hasErrnoCode(error, "ENOENT")) throw error
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: IndexedEntry["entry"],
435
+ entry: import("./metadata").StashEntry,
257
436
  query: string,
258
- rankingMode: "semantic" | "tfidf",
437
+ rankingMode: "semantic" | "fts",
259
438
  qualityBoost: number,
260
439
  confidenceBoost: number,
261
440
  ): string[] {
262
- const reasons: string[] = [rankingMode === "semantic" ? "semantic similarity" : "tf-idf lexical relevance"]
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 toScoredEntries(entries: IndexedEntry[]): ScoredEntry[] {
283
- return entries.map((ie) => ({
284
- id: `${ie.entry.type}:${ie.entry.name}`,
285
- text: buildSearchText(ie.entry),
286
- entry: ie.entry,
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: makeOpenRef("tool", asset.name),
306
- runCmd: toolInfo.runCmd,
307
- kind: toolInfo.kind,
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