agentikit 0.0.9 → 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 (107) hide show
  1. package/README.md +129 -214
  2. package/dist/index.d.ts +8 -2
  3. package/dist/index.js +4 -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 +201 -75
  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 +19 -6
  14. package/dist/src/config.js +139 -29
  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 +0 -1
  42. package/dist/src/metadata.js +6 -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 +2 -2
  46. package/dist/src/registry-install.js +142 -35
  47. package/dist/src/registry-resolve.js +90 -22
  48. package/dist/src/registry-search.d.ts +22 -0
  49. package/dist/src/registry-search.js +231 -97
  50. package/dist/src/registry-types.d.ts +9 -2
  51. package/dist/src/stash-add.js +4 -4
  52. package/dist/src/stash-clone.d.ts +22 -0
  53. package/dist/src/stash-clone.js +83 -0
  54. package/dist/src/stash-ref.d.ts +27 -3
  55. package/dist/src/stash-ref.js +63 -24
  56. package/dist/src/stash-registry.js +12 -12
  57. package/dist/src/stash-resolve.js +3 -0
  58. package/dist/src/stash-search.js +168 -164
  59. package/dist/src/stash-show.d.ts +1 -1
  60. package/dist/src/stash-show.js +28 -96
  61. package/dist/src/stash-source.d.ts +24 -0
  62. package/dist/src/stash-source.js +81 -0
  63. package/dist/src/stash-types.d.ts +14 -4
  64. package/dist/src/stash.d.ts +6 -0
  65. package/dist/src/stash.js +3 -0
  66. package/dist/src/tool-runner.d.ts +1 -1
  67. package/dist/src/tool-runner.js +18 -5
  68. package/package.json +7 -2
  69. package/src/asset-spec.ts +20 -4
  70. package/src/asset-type-handler.ts +77 -0
  71. package/src/cli.ts +213 -82
  72. package/src/common.ts +23 -5
  73. package/src/config-cli.ts +499 -0
  74. package/src/config.ts +160 -38
  75. package/src/db.ts +411 -0
  76. package/src/embedder.ts +22 -11
  77. package/src/github.ts +21 -0
  78. package/src/handlers/agent-handler.ts +32 -0
  79. package/src/handlers/command-handler.ts +29 -0
  80. package/src/handlers/index.ts +25 -0
  81. package/src/handlers/knowledge-handler.ts +62 -0
  82. package/src/handlers/markdown-helpers.ts +19 -0
  83. package/src/handlers/script-handler.ts +92 -0
  84. package/src/handlers/skill-handler.ts +37 -0
  85. package/src/handlers/tool-handler.ts +71 -0
  86. package/src/indexer.ts +208 -187
  87. package/src/init.ts +17 -9
  88. package/src/llm.ts +4 -3
  89. package/src/metadata.ts +5 -65
  90. package/src/origin-resolve.ts +67 -0
  91. package/src/registry-install.ts +158 -42
  92. package/src/registry-resolve.ts +92 -23
  93. package/src/registry-search.ts +288 -98
  94. package/src/registry-types.ts +10 -2
  95. package/src/stash-add.ts +14 -17
  96. package/src/stash-clone.ts +127 -0
  97. package/src/stash-ref.ts +84 -26
  98. package/src/stash-registry.ts +12 -12
  99. package/src/stash-resolve.ts +3 -0
  100. package/src/stash-search.ts +202 -184
  101. package/src/stash-show.ts +33 -90
  102. package/src/stash-source.ts +103 -0
  103. package/src/stash-types.ts +14 -4
  104. package/src/stash.ts +8 -0
  105. package/src/tool-runner.ts +18 -5
  106. package/dist/src/similarity.d.ts +0 -34
  107. package/src/similarity.ts +0 -271
@@ -1,12 +1,10 @@
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"
7
+ import { makeAssetRef } from "./stash-ref"
10
8
  import type {
11
9
  AgentikitSearchType,
12
10
  LocalSearchHit,
@@ -18,6 +16,21 @@ import type {
18
16
  } from "./stash-types"
19
17
  import { loadConfig } from "./config"
20
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"
21
34
 
22
35
  type IndexedAsset = {
23
36
  type: AgentikitAssetType
@@ -27,29 +40,6 @@ type IndexedAsset = {
27
40
 
28
41
  const DEFAULT_LIMIT = 20
29
42
 
30
- const DEFAULT_USAGE_GUIDE_BY_TYPE: Record<AgentikitAssetType, string[]> = {
31
- tool: [
32
- "Use the hit's runCmd for execution so runtime and working directory stay correct.",
33
- "Use `akm show <openRef>` to inspect the tool before running it.",
34
- ],
35
- skill: [
36
- "Read and apply the skill instructions as written, then adapt examples to your current repo state and task.",
37
- "Use `akm show <openRef>` to read the full SKILL.md for required steps and constraints.",
38
- ],
39
- command: [
40
- "Read the .md file, fill placeholders, and run it in the current repo context.",
41
- "Use `akm show <openRef>` to retrieve the command template body.",
42
- ],
43
- agent: [
44
- "Read the .md file and dispatch and agent using the content of the file. Use modelHint/toolPolicy when present to run the agent with compatible settings.",
45
- "Use with `akm show <openRef>` to get the full prompt payload.",
46
- ],
47
- knowledge: [
48
- "Use `akm show <openRef>` to read the document; start with `--view toc` for large files.",
49
- "Use `--view section` or `--view lines` to load only the part you need.",
50
- ],
51
- }
52
-
53
43
  export async function agentikitSearch(input: {
54
44
  query: string
55
45
  type?: AgentikitSearchType
@@ -64,7 +54,8 @@ export async function agentikitSearch(input: {
64
54
  const limit = normalizeLimit(input.limit)
65
55
  const usageMode = parseSearchUsageMode(input.usage)
66
56
  const source = parseSearchSource(input.source)
67
- const stashDir = resolveStashDir()
57
+ const sources = resolveStashSources()
58
+ const stashDir = sources[0].path
68
59
  const localResult = source === "registry"
69
60
  ? undefined
70
61
  : await searchLocal({
@@ -73,11 +64,13 @@ export async function agentikitSearch(input: {
73
64
  limit,
74
65
  usageMode,
75
66
  stashDir,
67
+ sources,
76
68
  })
77
69
 
70
+ const config = loadConfig()
78
71
  const registryResult = source === "local"
79
72
  ? undefined
80
- : await searchRegistry(query, { limit })
73
+ : await searchRegistry(query, { limit, registryUrls: config.registryUrls })
81
74
 
82
75
  if (source === "local") {
83
76
  return {
@@ -103,6 +96,7 @@ export async function agentikitSearch(input: {
103
96
  homepage: hit.homepage,
104
97
  score: hit.score,
105
98
  metadata: hit.metadata,
99
+ curated: hit.curated,
106
100
  installRef,
107
101
  installCmd: `akm add ${installRef}`,
108
102
  }
@@ -139,30 +133,41 @@ async function searchLocal(input: {
139
133
  limit: number
140
134
  usageMode: SearchUsageMode
141
135
  stashDir: string
136
+ sources: StashSource[]
142
137
  }): Promise<{ hits: LocalSearchHit[]; usageGuide?: Partial<Record<AgentikitAssetType, string[]>>; tip?: string; embedMs?: number; rankMs?: number }> {
143
- const { query, searchType, limit, usageMode, stashDir } = input
144
- const config = loadConfig(stashDir)
145
- const allStashDirs = [
146
- stashDir,
147
- ...config.additionalStashDirs.filter((d) => {
148
- try { return fs.statSync(d).isDirectory() } catch { return false }
149
- }),
150
- ]
138
+ const { query, searchType, limit, usageMode, stashDir, sources } = input
139
+ const config = loadConfig()
140
+ const allStashDirs = sources.map((s) => s.path)
151
141
 
152
- const index = loadSearchIndex()
153
- if (index && index.entries && index.entries.length > 0 && index.stashDir === stashDir) {
154
- const { hits, usageGuide, embedMs, rankMs } = await searchIndex(index, query, searchType, limit, stashDir, allStashDirs, config, usageMode)
155
- return {
156
- hits,
157
- usageGuide,
158
- tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
159
- embedMs,
160
- rankMs,
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
+ }
161
164
  }
165
+ } catch (error) {
166
+ console.warn("Search index unavailable, falling back to substring search:", error instanceof Error ? error.message : String(error))
162
167
  }
163
168
 
164
169
  const hits = allStashDirs
165
- .flatMap((dir) => substringSearch(query, searchType, limit, dir))
170
+ .flatMap((dir) => substringSearch(query, searchType, limit, dir, sources))
166
171
  .slice(0, limit)
167
172
  const usageGuide = shouldIncludeUsageGuide(usageMode) ? buildUsageGuide(hits.map((hit) => hit.type), searchType) : undefined
168
173
  return {
@@ -172,10 +177,10 @@ async function searchLocal(input: {
172
177
  }
173
178
  }
174
179
 
175
- // ── Unified indexed search ──────────────────────────────────────────────────
180
+ // ── Database search ─────────────────────────────────────────────────────────
176
181
 
177
- async function searchIndex(
178
- index: import("./indexer").SearchIndex,
182
+ async function searchDatabase(
183
+ db: import("bun:sqlite").Database,
179
184
  query: string,
180
185
  searchType: AgentikitSearchType,
181
186
  limit: number,
@@ -183,80 +188,136 @@ async function searchIndex(
183
188
  allStashDirs: string[],
184
189
  config: import("./config").AgentikitConfig,
185
190
  usageMode: SearchUsageMode,
191
+ sources: StashSource[],
186
192
  ): Promise<{ hits: LocalSearchHit[]; usageGuide?: Partial<Record<AgentikitAssetType, string[]>>; embedMs?: number; rankMs?: number }> {
187
- // Filter candidates by type
188
- let candidates = index.entries
189
- if (searchType !== "any") {
190
- candidates = candidates.filter((ie) => ie.entry.type === searchType)
191
- }
192
-
193
- if (candidates.length === 0) {
194
- return {
195
- hits: [],
196
- usageGuide: shouldIncludeUsageGuide(usageMode) ? buildUsageGuide([], searchType) : undefined,
197
- }
198
- }
199
-
200
- // Empty query: return all entries (no scoring needed)
193
+ // Empty query: return all entries
201
194
  if (!query) {
202
- const selectedCandidates = candidates.slice(0, limit)
203
- const hits = selectedCandidates.map((ie) =>
204
- buildIndexedHit({
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({
205
200
  entry: ie.entry,
206
- path: ie.path,
201
+ path: ie.filePath,
207
202
  score: 1,
208
203
  query,
209
- rankingMode: "tfidf",
204
+ rankingMode: "fts",
210
205
  defaultStashDir: stashDir,
211
206
  allStashDirs,
207
+ sources,
212
208
  includeItemUsage: shouldIncludeItemUsage(usageMode),
213
209
  }),
214
210
  )
215
211
  return {
216
212
  hits,
217
213
  usageGuide: shouldIncludeUsageGuide(usageMode)
218
- ? buildUsageGuideFromEntries(selectedCandidates.map((candidate) => candidate.entry), searchType)
214
+ ? buildUsageGuideFromEntries(selected.map((e) => e.entry), searchType)
219
215
  : undefined,
220
216
  }
221
217
  }
222
218
 
223
- // Score each candidate using available signals
219
+ // Score using FTS5 (BM25) and optionally sqlite-vec
224
220
  const tEmbed0 = Date.now()
225
- const embeddingScores = await tryEmbeddingScores(candidates, query, config)
221
+ const embeddingScores = await tryVecScores(db, query, limit * 3, config)
226
222
  const embedMs = Date.now() - tEmbed0
227
223
 
228
224
  const tRank0 = Date.now()
229
- 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
+ }
230
236
 
231
- 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>()
232
240
 
233
- for (const ie of candidates) {
234
- const key = ie.path
235
- const embScore = embeddingScores?.get(key)
236
- 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)
237
245
 
238
246
  if (embScore !== undefined) {
239
- // Weighted blend: embedding dominates when available, TF-IDF boosts lexical matches
240
- const blended = embScore * 0.7 + tfidfScore * 0.3
241
- if (blended > 0) scored.push({ ie, score: blended, rankingMode: "semantic" })
242
- } else if (tfidfScore > 0) {
243
- 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" })
251
+ }
252
+ }
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
244
300
  }
245
301
  }
246
302
 
303
+ for (const item of scored) {
304
+ item.score = Math.min(item.score, 1.0)
305
+ }
306
+
247
307
  scored.sort((a, b) => b.score - a.score)
248
308
  const rankMs = Date.now() - tRank0
249
309
 
250
310
  const selected = scored.slice(0, limit)
251
- const hits = selected.map(({ ie, score, rankingMode }) =>
252
- buildIndexedHit({
253
- entry: ie.entry,
254
- path: ie.path,
311
+ const hits = selected.map(({ entry, filePath, score, rankingMode }) =>
312
+ buildDbHit({
313
+ entry,
314
+ path: filePath,
255
315
  score: Math.round(score * 1000) / 1000,
256
316
  query,
257
317
  rankingMode,
258
318
  defaultStashDir: stashDir,
259
319
  allStashDirs,
320
+ sources,
260
321
  includeItemUsage: shouldIncludeItemUsage(usageMode),
261
322
  }),
262
323
  )
@@ -266,65 +327,41 @@ async function searchIndex(
266
327
  rankMs,
267
328
  hits,
268
329
  usageGuide: shouldIncludeUsageGuide(usageMode)
269
- ? buildUsageGuideFromEntries(selected.map((item) => item.ie.entry), searchType)
330
+ ? buildUsageGuideFromEntries(selected.map((item) => item.entry), searchType)
270
331
  : undefined,
271
332
  }
272
333
  }
273
334
 
274
- // ── Embedding scorer ────────────────────────────────────────────────────────
335
+ // ── Vector scorer ───────────────────────────────────────────────────────────
275
336
 
276
- async function tryEmbeddingScores(
277
- candidates: IndexedEntry[],
337
+ async function tryVecScores(
338
+ db: import("bun:sqlite").Database,
278
339
  query: string,
340
+ k: number,
279
341
  config: import("./config").AgentikitConfig,
280
- ): Promise<Map<string, number> | null> {
281
- if (!config.semanticSearch) return null
282
-
283
- const withEmbeddings = candidates.filter((ie) => ie.embedding && ie.embedding.length > 0)
284
- 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
285
346
 
286
347
  try {
287
- const { embed, cosineSimilarity } = await import("./embedder.js")
348
+ const { embed } = await import("./embedder.js")
288
349
  const queryEmbedding = await embed(query, config.embedding)
289
- const scores = new Map<string, number>()
290
- for (const ie of withEmbeddings) {
291
- 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))
292
357
  }
293
358
  return scores
294
- } catch {
359
+ } catch (error) {
360
+ console.warn("Vector search failed, skipping:", error instanceof Error ? error.message : String(error))
295
361
  return null
296
362
  }
297
363
  }
298
364
 
299
- // ── TF-IDF scorer ───────────────────────────────────────────────────────────
300
-
301
- function computeTfidfScores(
302
- index: import("./indexer").SearchIndex,
303
- candidates: IndexedEntry[],
304
- query: string,
305
- searchType: AgentikitSearchType,
306
- ): Map<string, number> {
307
- const candidateScoredEntries = toScoredEntries(candidates)
308
-
309
- let adapter: TfIdfAdapter
310
- if (index.tfidf) {
311
- const allScored = toScoredEntries(index.entries)
312
- adapter = TfIdfAdapter.deserialize(index.tfidf, allScored)
313
- } else {
314
- adapter = new TfIdfAdapter()
315
- adapter.buildIndex(candidateScoredEntries)
316
- }
317
-
318
- const typeFilter = searchType === "any" ? undefined : searchType
319
- const results = adapter.search(query, candidates.length, typeFilter)
320
-
321
- const scores = new Map<string, number>()
322
- for (const r of results) {
323
- scores.set(r.path, r.score)
324
- }
325
- return scores
326
- }
327
-
328
365
  // ── Substring fallback (no index) ───────────────────────────────────────────
329
366
 
330
367
  function substringSearch(
@@ -332,52 +369,50 @@ function substringSearch(
332
369
  searchType: AgentikitSearchType,
333
370
  limit: number,
334
371
  stashDir: string,
372
+ sources: StashSource[],
335
373
  ): LocalSearchHit[] {
336
374
  const assets = indexAssets(stashDir, searchType)
337
375
  return assets
338
376
  .filter((asset) => asset.name.toLowerCase().includes(query))
339
377
  .sort(compareAssets)
340
378
  .slice(0, limit)
341
- .map((asset) => assetToSearchHit(asset, stashDir))
379
+ .map((asset) => assetToSearchHit(asset, stashDir, sources))
342
380
  }
343
381
 
344
382
  // ── Hit building ────────────────────────────────────────────────────────────
345
383
 
346
- function findStashDirForPath(filePath: string, stashDirs: string[]): string | undefined {
347
- const resolved = path.resolve(filePath)
348
- for (const dir of stashDirs) {
349
- if (resolved.startsWith(path.resolve(dir) + path.sep)) return dir
350
- }
351
- return undefined
352
- }
353
-
354
- function buildIndexedHit(input: {
355
- entry: IndexedEntry["entry"]
384
+ function buildDbHit(input: {
385
+ entry: import("./metadata").StashEntry
356
386
  path: string
357
387
  score: number
358
388
  query: string
359
- rankingMode: "semantic" | "tfidf"
389
+ rankingMode: "semantic" | "fts"
360
390
  defaultStashDir: string
361
391
  allStashDirs: string[]
392
+ sources: StashSource[]
362
393
  includeItemUsage: boolean
363
394
  }): LocalSearchHit {
364
- const entryStashDir = findStashDirForPath(input.path, input.allStashDirs) ?? input.defaultStashDir
395
+ const entryStashDir = findSourceForPath(input.path, input.sources)?.path ?? input.defaultStashDir
365
396
  const typeRoot = path.join(entryStashDir, TYPE_DIRS[input.entry.type])
366
397
  const openRefName = deriveCanonicalAssetName(input.entry.type, typeRoot, input.path)
367
398
  ?? input.entry.name
368
399
 
369
400
  const qualityBoost = input.entry.generated === true ? 0 : 0.05
370
401
  const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0
371
- 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)
372
403
 
373
404
  const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost)
374
405
 
406
+ const source = findSourceForPath(input.path, input.sources)
407
+
375
408
  const hit: LocalSearchHit = {
376
409
  hitSource: "local",
377
410
  type: input.entry.type,
378
411
  name: input.entry.name,
379
412
  path: input.path,
380
- openRef: makeOpenRef(input.entry.type, openRefName),
413
+ openRef: makeAssetRef(input.entry.type, openRefName, source?.registryId),
414
+ registryId: source?.registryId,
415
+ editable: source?.writable ?? false,
381
416
  description: input.entry.description,
382
417
  tags: input.entry.tags,
383
418
  score,
@@ -388,27 +423,22 @@ function buildIndexedHit(input: {
388
423
  hit.usage = input.entry.usage
389
424
  }
390
425
 
391
- if (input.entry.type === "tool") {
392
- try {
393
- const toolInfo = buildToolInfo(entryStashDir, input.path)
394
- hit.runCmd = toolInfo.runCmd
395
- hit.kind = toolInfo.kind
396
- } catch (error: unknown) {
397
- if (!hasErrnoCode(error, "ENOENT")) throw error
398
- }
426
+ const handler = tryGetHandler(input.entry.type)
427
+ if (handler?.enrichSearchHit) {
428
+ handler.enrichSearchHit(hit, entryStashDir)
399
429
  }
400
430
 
401
431
  return hit
402
432
  }
403
433
 
404
434
  function buildWhyMatched(
405
- entry: IndexedEntry["entry"],
435
+ entry: import("./metadata").StashEntry,
406
436
  query: string,
407
- rankingMode: "semantic" | "tfidf",
437
+ rankingMode: "semantic" | "fts",
408
438
  qualityBoost: number,
409
439
  confidenceBoost: number,
410
440
  ): string[] {
411
- const reasons: string[] = [rankingMode === "semantic" ? "semantic similarity" : "tf-idf lexical relevance"]
441
+ const reasons: string[] = [rankingMode === "semantic" ? "semantic similarity" : "fts bm25 relevance"]
412
442
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean)
413
443
 
414
444
  const name = entry.name.toLowerCase()
@@ -428,35 +458,22 @@ function buildWhyMatched(
428
458
 
429
459
  // ── Helpers ─────────────────────────────────────────────────────────────────
430
460
 
431
- function toScoredEntries(entries: IndexedEntry[]): ScoredEntry[] {
432
- return entries.map((ie) => ({
433
- id: `${ie.entry.type}:${ie.entry.name}`,
434
- text: buildSearchText(ie.entry),
435
- entry: ie.entry,
436
- path: ie.path,
437
- }))
438
- }
439
-
440
- function assetToSearchHit(asset: IndexedAsset, stashDir: string): LocalSearchHit {
441
- if (asset.type !== "tool") {
442
- return {
443
- hitSource: "local",
444
- type: asset.type,
445
- name: asset.name,
446
- path: asset.path,
447
- openRef: makeOpenRef(asset.type, asset.name),
448
- }
449
- }
450
- const toolInfo = buildToolInfo(stashDir, asset.path)
451
- return {
461
+ function assetToSearchHit(asset: IndexedAsset, stashDir: string, sources: StashSource[]): LocalSearchHit {
462
+ const source = findSourceForPath(asset.path, sources)
463
+ const hit: LocalSearchHit = {
452
464
  hitSource: "local",
453
- type: "tool",
465
+ type: asset.type,
454
466
  name: asset.name,
455
467
  path: asset.path,
456
- openRef: makeOpenRef("tool", asset.name),
457
- runCmd: toolInfo.runCmd,
458
- kind: toolInfo.kind,
468
+ openRef: makeAssetRef(asset.type, asset.name, source?.registryId),
469
+ registryId: source?.registryId,
470
+ editable: source?.writable ?? false,
471
+ }
472
+ const handler = tryGetHandler(asset.type)
473
+ if (handler?.enrichSearchHit) {
474
+ handler.enrichSearchHit(hit, stashDir)
459
475
  }
476
+ return hit
460
477
  }
461
478
 
462
479
  function normalizeLimit(limit?: number): number {
@@ -509,7 +526,7 @@ function shouldIncludeItemUsage(mode: SearchUsageMode): boolean {
509
526
  }
510
527
 
511
528
  function buildUsageGuideFromEntries(
512
- entries: IndexedEntry["entry"][],
529
+ entries: import("./metadata").StashEntry[],
513
530
  searchType: AgentikitSearchType,
514
531
  ): Partial<Record<AgentikitAssetType, string[]>> | undefined {
515
532
  const types = entries.map((entry) => entry.type)
@@ -564,7 +581,8 @@ function resolveGuideTypes(hitTypes: AgentikitAssetType[], searchType: Agentikit
564
581
  }
565
582
 
566
583
  function usageGuideByType(type: AgentikitAssetType): string[] {
567
- return DEFAULT_USAGE_GUIDE_BY_TYPE[type]
584
+ const handler = tryGetHandler(type)
585
+ return handler?.defaultUsageGuide ?? []
568
586
  }
569
587
 
570
588
  function fileToAsset(assetType: AgentikitAssetType, root: string, file: string): IndexedAsset | undefined {