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,192 +1,329 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { hasErrnoCode, resolveStashDir } from "./common";
4
3
  import { ASSET_TYPES, TYPE_DIRS, deriveCanonicalAssetName } from "./asset-spec";
5
- import { loadSearchIndex, buildSearchText } from "./indexer";
6
- import { TfIdfAdapter } from "./similarity";
7
- import { buildToolInfo } from "./tool-runner";
8
4
  import { walkStash } from "./walker";
9
- import { makeOpenRef } from "./stash-ref";
5
+ import { makeAssetRef } from "./stash-ref";
10
6
  import { loadConfig } from "./config";
7
+ import { searchRegistry } from "./registry-search";
8
+ import { openDatabase, closeDatabase, getDbPath, getMeta, searchFts, searchVec, getAllEntries, getEntryCount, getEntryById, isVecAvailable, } from "./db";
9
+ import { tryGetHandler } from "./asset-type-handler";
10
+ import { resolveStashSources, findSourceForPath } from "./stash-source";
11
11
  const DEFAULT_LIMIT = 20;
12
12
  export async function agentikitSearch(input) {
13
13
  const t0 = Date.now();
14
- const query = input.query.trim().toLowerCase();
14
+ const query = input.query.trim();
15
+ const normalizedQuery = query.toLowerCase();
15
16
  const searchType = input.type ?? "any";
16
17
  const limit = normalizeLimit(input.limit);
17
- const stashDir = resolveStashDir();
18
- const config = loadConfig(stashDir);
19
- const allStashDirs = [
20
- stashDir,
21
- ...config.additionalStashDirs.filter((d) => {
22
- try {
23
- return fs.statSync(d).isDirectory();
24
- }
25
- catch {
26
- return false;
27
- }
28
- }),
29
- ];
30
- // Try indexed search (single unified pipeline: embedding + TF-IDF as weighted features)
31
- const index = loadSearchIndex();
32
- if (index && index.entries && index.entries.length > 0 && index.stashDir === stashDir) {
33
- const { hits, embedMs, rankMs } = await searchIndex(index, query, searchType, limit, stashDir, allStashDirs, config);
18
+ const usageMode = parseSearchUsageMode(input.usage);
19
+ const source = parseSearchSource(input.source);
20
+ const sources = resolveStashSources();
21
+ const stashDir = sources[0].path;
22
+ const localResult = source === "registry"
23
+ ? undefined
24
+ : await searchLocal({
25
+ query: normalizedQuery,
26
+ searchType,
27
+ limit,
28
+ usageMode,
29
+ stashDir,
30
+ sources,
31
+ });
32
+ const config = loadConfig();
33
+ const registryResult = source === "local"
34
+ ? undefined
35
+ : await searchRegistry(query, { limit, registryUrls: config.registryUrls });
36
+ if (source === "local") {
37
+ return {
38
+ stashDir,
39
+ source,
40
+ hits: localResult?.hits ?? [],
41
+ usageGuide: localResult?.usageGuide,
42
+ tip: localResult?.tip,
43
+ timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
44
+ };
45
+ }
46
+ const registryHits = (registryResult?.hits ?? []).map((hit) => {
47
+ const installRef = hit.source === "npm" ? `npm:${hit.ref}` : `github:${hit.ref}`;
48
+ return {
49
+ hitSource: "registry",
50
+ type: "registry",
51
+ name: hit.title,
52
+ id: hit.id,
53
+ registrySource: hit.source,
54
+ ref: hit.ref,
55
+ description: hit.description,
56
+ homepage: hit.homepage,
57
+ score: hit.score,
58
+ metadata: hit.metadata,
59
+ curated: hit.curated,
60
+ installRef,
61
+ installCmd: `akm add ${installRef}`,
62
+ };
63
+ });
64
+ if (source === "registry") {
65
+ const hits = registryHits.slice(0, limit);
34
66
  return {
35
67
  stashDir,
68
+ source,
36
69
  hits,
37
- tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
38
- timing: { totalMs: Date.now() - t0, rankMs, embedMs },
70
+ tip: hits.length === 0 ? "No matching registry entries were found." : undefined,
71
+ warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
72
+ timing: { totalMs: Date.now() - t0 },
39
73
  };
40
74
  }
41
- // No index: fall back to filesystem walk + substring match across all stash dirs
75
+ const mergedHits = mergeSearchHits(localResult?.hits ?? [], registryHits, limit);
76
+ return {
77
+ stashDir,
78
+ source,
79
+ hits: mergedHits,
80
+ usageGuide: localResult?.usageGuide,
81
+ tip: mergedHits.length === 0 ? "No matching stash assets or registry entries were found." : undefined,
82
+ warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
83
+ timing: { totalMs: Date.now() - t0 },
84
+ };
85
+ }
86
+ async function searchLocal(input) {
87
+ const { query, searchType, limit, usageMode, stashDir, sources } = input;
88
+ const config = loadConfig();
89
+ const allStashDirs = sources.map((s) => s.path);
90
+ // Try to open the database
91
+ const dbPath = getDbPath();
92
+ try {
93
+ if (fs.existsSync(dbPath)) {
94
+ const embeddingDim = config.embedding?.dimension;
95
+ const db = openDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined);
96
+ try {
97
+ const entryCount = getEntryCount(db);
98
+ const storedStashDir = getMeta(db, "stashDir");
99
+ if (entryCount > 0 && storedStashDir === stashDir) {
100
+ const { hits, usageGuide, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allStashDirs, config, usageMode, sources);
101
+ return {
102
+ hits,
103
+ usageGuide,
104
+ tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
105
+ embedMs,
106
+ rankMs,
107
+ };
108
+ }
109
+ }
110
+ finally {
111
+ closeDatabase(db);
112
+ }
113
+ }
114
+ }
115
+ catch (error) {
116
+ console.warn("Search index unavailable, falling back to substring search:", error instanceof Error ? error.message : String(error));
117
+ }
42
118
  const hits = allStashDirs
43
- .flatMap((dir) => substringSearch(query, searchType, limit, dir))
119
+ .flatMap((dir) => substringSearch(query, searchType, limit, dir, sources))
44
120
  .slice(0, limit);
121
+ const usageGuide = shouldIncludeUsageGuide(usageMode) ? buildUsageGuide(hits.map((hit) => hit.type), searchType) : undefined;
45
122
  return {
46
- stashDir,
47
123
  hits,
124
+ usageGuide,
48
125
  tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
49
- timing: { totalMs: Date.now() - t0 },
50
126
  };
51
127
  }
52
- // ── Unified indexed search ──────────────────────────────────────────────────
53
- async function searchIndex(index, query, searchType, limit, stashDir, allStashDirs, config) {
54
- // Filter candidates by type
55
- let candidates = index.entries;
56
- if (searchType !== "any") {
57
- candidates = candidates.filter((ie) => ie.entry.type === searchType);
58
- }
59
- if (candidates.length === 0)
60
- return { hits: [] };
61
- // Empty query: return all entries (no scoring needed)
128
+ // ── Database search ─────────────────────────────────────────────────────────
129
+ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDirs, config, usageMode, sources) {
130
+ // Empty query: return all entries
62
131
  if (!query) {
63
- return { hits: candidates.slice(0, limit).map((ie) => buildIndexedHit({ entry: ie.entry, path: ie.path, score: 1, query, rankingMode: "tfidf", defaultStashDir: stashDir, allStashDirs })) };
132
+ const typeFilter = searchType === "any" ? undefined : searchType;
133
+ const allEntries = getAllEntries(db, typeFilter);
134
+ const selected = allEntries.slice(0, limit);
135
+ const hits = selected.map((ie) => buildDbHit({
136
+ entry: ie.entry,
137
+ path: ie.filePath,
138
+ score: 1,
139
+ query,
140
+ rankingMode: "fts",
141
+ defaultStashDir: stashDir,
142
+ allStashDirs,
143
+ sources,
144
+ includeItemUsage: shouldIncludeItemUsage(usageMode),
145
+ }));
146
+ return {
147
+ hits,
148
+ usageGuide: shouldIncludeUsageGuide(usageMode)
149
+ ? buildUsageGuideFromEntries(selected.map((e) => e.entry), searchType)
150
+ : undefined,
151
+ };
64
152
  }
65
- // Score each candidate using available signals
153
+ // Score using FTS5 (BM25) and optionally sqlite-vec
66
154
  const tEmbed0 = Date.now();
67
- const embeddingScores = await tryEmbeddingScores(candidates, query, config);
155
+ const embeddingScores = await tryVecScores(db, query, limit * 3, config);
68
156
  const embedMs = Date.now() - tEmbed0;
69
157
  const tRank0 = Date.now();
70
- const tfidfScores = computeTfidfScores(index, candidates, query, searchType);
158
+ const typeFilter = searchType === "any" ? undefined : searchType;
159
+ const ftsResults = searchFts(db, query, limit * 3, typeFilter);
160
+ // Build score map from FTS results (normalize BM25 scores)
161
+ const ftsScoreMap = new Map();
162
+ for (const r of ftsResults) {
163
+ // BM25 returns negative scores (more negative = better match), normalize to 0-1
164
+ const absScore = Math.abs(r.bm25Score);
165
+ const normalized = absScore / (1 + absScore);
166
+ ftsScoreMap.set(r.id, { score: normalized, result: r });
167
+ }
168
+ // Blend scores
71
169
  const scored = [];
72
- for (const ie of candidates) {
73
- const key = ie.path;
74
- const embScore = embeddingScores?.get(key);
75
- const tfidfScore = tfidfScores.get(key) ?? 0;
170
+ const seenIds = new Set();
171
+ // Process FTS results
172
+ for (const [id, { score: ftsScore, result }] of ftsScoreMap) {
173
+ seenIds.add(id);
174
+ const embScore = embeddingScores?.get(id);
76
175
  if (embScore !== undefined) {
77
- // Weighted blend: embedding dominates when available, TF-IDF boosts lexical matches
78
- const blended = embScore * 0.7 + tfidfScore * 0.3;
176
+ const blended = embScore * 0.7 + ftsScore * 0.3;
79
177
  if (blended > 0)
80
- scored.push({ ie, score: blended, rankingMode: "semantic" });
178
+ scored.push({ id, entry: result.entry, filePath: result.filePath, score: blended, rankingMode: "semantic" });
81
179
  }
82
- else if (tfidfScore > 0) {
83
- scored.push({ ie, score: tfidfScore, rankingMode: "tfidf" });
180
+ else if (ftsScore > 0) {
181
+ scored.push({ id, entry: result.entry, filePath: result.filePath, score: ftsScore, rankingMode: "fts" });
84
182
  }
85
183
  }
184
+ // Add vec-only results not already in FTS results
185
+ if (embeddingScores) {
186
+ for (const [id, embScore] of embeddingScores) {
187
+ if (seenIds.has(id))
188
+ continue;
189
+ const found = getEntryById(db, id);
190
+ if (found) {
191
+ if (typeFilter && found.entry.type !== typeFilter)
192
+ continue;
193
+ scored.push({
194
+ id,
195
+ entry: found.entry,
196
+ filePath: found.filePath,
197
+ score: embScore,
198
+ rankingMode: "semantic",
199
+ });
200
+ }
201
+ }
202
+ }
203
+ // Apply boosts (tag, intent, name matches)
204
+ const queryTokens = query.toLowerCase().split(/\s+/).filter(Boolean);
205
+ for (const item of scored) {
206
+ const entry = item.entry;
207
+ // Tag boost
208
+ if (entry.tags) {
209
+ for (const tag of entry.tags) {
210
+ if (queryTokens.some((t) => tag.toLowerCase() === t)) {
211
+ item.score += 0.15;
212
+ }
213
+ }
214
+ }
215
+ // Intent boost
216
+ if (entry.intents) {
217
+ for (const intent of entry.intents) {
218
+ const intentLower = intent.toLowerCase();
219
+ for (const token of queryTokens) {
220
+ if (intentLower.includes(token)) {
221
+ item.score += 0.12;
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ }
227
+ // Name boost
228
+ const nameLower = entry.name.toLowerCase().replace(/[-_]/g, " ");
229
+ if (queryTokens.some((t) => nameLower.includes(t))) {
230
+ item.score += 0.1;
231
+ }
232
+ }
233
+ for (const item of scored) {
234
+ item.score = Math.min(item.score, 1.0);
235
+ }
86
236
  scored.sort((a, b) => b.score - a.score);
87
237
  const rankMs = Date.now() - tRank0;
88
- return { embedMs, rankMs, hits: scored.slice(0, limit).map(({ ie, score, rankingMode }) => buildIndexedHit({
89
- entry: ie.entry,
90
- path: ie.path,
91
- score: Math.round(score * 1000) / 1000,
92
- query,
93
- rankingMode,
94
- defaultStashDir: stashDir,
95
- allStashDirs,
96
- })) };
238
+ const selected = scored.slice(0, limit);
239
+ const hits = selected.map(({ entry, filePath, score, rankingMode }) => buildDbHit({
240
+ entry,
241
+ path: filePath,
242
+ score: Math.round(score * 1000) / 1000,
243
+ query,
244
+ rankingMode,
245
+ defaultStashDir: stashDir,
246
+ allStashDirs,
247
+ sources,
248
+ includeItemUsage: shouldIncludeItemUsage(usageMode),
249
+ }));
250
+ return {
251
+ embedMs,
252
+ rankMs,
253
+ hits,
254
+ usageGuide: shouldIncludeUsageGuide(usageMode)
255
+ ? buildUsageGuideFromEntries(selected.map((item) => item.entry), searchType)
256
+ : undefined,
257
+ };
97
258
  }
98
- // ── Embedding scorer ────────────────────────────────────────────────────────
99
- async function tryEmbeddingScores(candidates, query, config) {
100
- if (!config.semanticSearch)
259
+ // ── Vector scorer ───────────────────────────────────────────────────────────
260
+ async function tryVecScores(db, query, k, config) {
261
+ if (!config.semanticSearch || !isVecAvailable())
101
262
  return null;
102
- const withEmbeddings = candidates.filter((ie) => ie.embedding && ie.embedding.length > 0);
103
- if (withEmbeddings.length === 0)
263
+ const hasEmbeddings = getMeta(db, "hasEmbeddings");
264
+ if (hasEmbeddings !== "1")
104
265
  return null;
105
266
  try {
106
- const { embed, cosineSimilarity } = await import("./embedder.js");
267
+ const { embed } = await import("./embedder.js");
107
268
  const queryEmbedding = await embed(query, config.embedding);
269
+ const vecResults = searchVec(db, queryEmbedding, k);
108
270
  const scores = new Map();
109
- for (const ie of withEmbeddings) {
110
- scores.set(ie.path, cosineSimilarity(queryEmbedding, ie.embedding));
271
+ for (const { id, distance } of vecResults) {
272
+ // Convert L2 distance to cosine similarity (vectors are normalized)
273
+ const cosineSim = 1 - (distance * distance) / 2;
274
+ scores.set(id, Math.max(0, cosineSim));
111
275
  }
112
276
  return scores;
113
277
  }
114
- catch {
278
+ catch (error) {
279
+ console.warn("Vector search failed, skipping:", error instanceof Error ? error.message : String(error));
115
280
  return null;
116
281
  }
117
282
  }
118
- // ── TF-IDF scorer ───────────────────────────────────────────────────────────
119
- function computeTfidfScores(index, candidates, query, searchType) {
120
- const candidateScoredEntries = toScoredEntries(candidates);
121
- let adapter;
122
- if (index.tfidf) {
123
- const allScored = toScoredEntries(index.entries);
124
- adapter = TfIdfAdapter.deserialize(index.tfidf, allScored);
125
- }
126
- else {
127
- adapter = new TfIdfAdapter();
128
- adapter.buildIndex(candidateScoredEntries);
129
- }
130
- const typeFilter = searchType === "any" ? undefined : searchType;
131
- const results = adapter.search(query, candidates.length, typeFilter);
132
- const scores = new Map();
133
- for (const r of results) {
134
- scores.set(r.path, r.score);
135
- }
136
- return scores;
137
- }
138
283
  // ── Substring fallback (no index) ───────────────────────────────────────────
139
- function substringSearch(query, searchType, limit, stashDir) {
284
+ function substringSearch(query, searchType, limit, stashDir, sources) {
140
285
  const assets = indexAssets(stashDir, searchType);
141
286
  return assets
142
287
  .filter((asset) => asset.name.toLowerCase().includes(query))
143
288
  .sort(compareAssets)
144
289
  .slice(0, limit)
145
- .map((asset) => assetToSearchHit(asset, stashDir));
290
+ .map((asset) => assetToSearchHit(asset, stashDir, sources));
146
291
  }
147
292
  // ── Hit building ────────────────────────────────────────────────────────────
148
- function findStashDirForPath(filePath, stashDirs) {
149
- const resolved = path.resolve(filePath);
150
- for (const dir of stashDirs) {
151
- if (resolved.startsWith(path.resolve(dir) + path.sep))
152
- return dir;
153
- }
154
- return undefined;
155
- }
156
- function buildIndexedHit(input) {
157
- const entryStashDir = findStashDirForPath(input.path, input.allStashDirs) ?? input.defaultStashDir;
293
+ function buildDbHit(input) {
294
+ const entryStashDir = findSourceForPath(input.path, input.sources)?.path ?? input.defaultStashDir;
158
295
  const typeRoot = path.join(entryStashDir, TYPE_DIRS[input.entry.type]);
159
296
  const openRefName = deriveCanonicalAssetName(input.entry.type, typeRoot, input.path)
160
297
  ?? input.entry.name;
161
298
  const qualityBoost = input.entry.generated === true ? 0 : 0.05;
162
299
  const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0;
163
- const score = Math.round((input.score + qualityBoost + confidenceBoost) * 1000) / 1000;
300
+ const score = Math.min(Math.round((input.score + qualityBoost + confidenceBoost) * 1000) / 1000, 1.0);
164
301
  const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost);
302
+ const source = findSourceForPath(input.path, input.sources);
165
303
  const hit = {
304
+ hitSource: "local",
166
305
  type: input.entry.type,
167
306
  name: input.entry.name,
168
307
  path: input.path,
169
- openRef: makeOpenRef(input.entry.type, openRefName),
308
+ openRef: makeAssetRef(input.entry.type, openRefName, source?.registryId),
309
+ registryId: source?.registryId,
310
+ editable: source?.writable ?? false,
170
311
  description: input.entry.description,
171
312
  tags: input.entry.tags,
172
313
  score,
173
314
  whyMatched,
174
315
  };
175
- if (input.entry.type === "tool") {
176
- try {
177
- const toolInfo = buildToolInfo(entryStashDir, input.path);
178
- hit.runCmd = toolInfo.runCmd;
179
- hit.kind = toolInfo.kind;
180
- }
181
- catch (error) {
182
- if (!hasErrnoCode(error, "ENOENT"))
183
- throw error;
184
- }
316
+ if (input.includeItemUsage && input.entry.usage && input.entry.usage.length > 0) {
317
+ hit.usage = input.entry.usage;
318
+ }
319
+ const handler = tryGetHandler(input.entry.type);
320
+ if (handler?.enrichSearchHit) {
321
+ handler.enrichSearchHit(hit, entryStashDir);
185
322
  }
186
323
  return hit;
187
324
  }
188
325
  function buildWhyMatched(entry, query, rankingMode, qualityBoost, confidenceBoost) {
189
- const reasons = [rankingMode === "semantic" ? "semantic similarity" : "tf-idf lexical relevance"];
326
+ const reasons = [rankingMode === "semantic" ? "semantic similarity" : "fts bm25 relevance"];
190
327
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
191
328
  const name = entry.name.toLowerCase();
192
329
  const tags = entry.tags?.join(" ").toLowerCase() ?? "";
@@ -207,32 +344,22 @@ function buildWhyMatched(entry, query, rankingMode, qualityBoost, confidenceBoos
207
344
  return reasons;
208
345
  }
209
346
  // ── Helpers ─────────────────────────────────────────────────────────────────
210
- function toScoredEntries(entries) {
211
- return entries.map((ie) => ({
212
- id: `${ie.entry.type}:${ie.entry.name}`,
213
- text: buildSearchText(ie.entry),
214
- entry: ie.entry,
215
- path: ie.path,
216
- }));
217
- }
218
- function assetToSearchHit(asset, stashDir) {
219
- if (asset.type !== "tool") {
220
- return {
221
- type: asset.type,
222
- name: asset.name,
223
- path: asset.path,
224
- openRef: makeOpenRef(asset.type, asset.name),
225
- };
226
- }
227
- const toolInfo = buildToolInfo(stashDir, asset.path);
228
- return {
229
- type: "tool",
347
+ function assetToSearchHit(asset, stashDir, sources) {
348
+ const source = findSourceForPath(asset.path, sources);
349
+ const hit = {
350
+ hitSource: "local",
351
+ type: asset.type,
230
352
  name: asset.name,
231
353
  path: asset.path,
232
- openRef: makeOpenRef("tool", asset.name),
233
- runCmd: toolInfo.runCmd,
234
- kind: toolInfo.kind,
354
+ openRef: makeAssetRef(asset.type, asset.name, source?.registryId),
355
+ registryId: source?.registryId,
356
+ editable: source?.writable ?? false,
235
357
  };
358
+ const handler = tryGetHandler(asset.type);
359
+ if (handler?.enrichSearchHit) {
360
+ handler.enrichSearchHit(hit, stashDir);
361
+ }
362
+ return hit;
236
363
  }
237
364
  function normalizeLimit(limit) {
238
365
  if (typeof limit !== "number" || Number.isNaN(limit) || limit <= 0) {
@@ -240,6 +367,98 @@ function normalizeLimit(limit) {
240
367
  }
241
368
  return Math.min(Math.floor(limit), 200);
242
369
  }
370
+ function parseSearchUsageMode(mode) {
371
+ if (mode === "none" || mode === "both" || mode === "item" || mode === "guide") {
372
+ return mode;
373
+ }
374
+ if (typeof mode === "undefined")
375
+ return "both";
376
+ throw new Error(`Invalid usage mode: ${String(mode)}. Expected one of: none|both|item|guide`);
377
+ }
378
+ function parseSearchSource(source) {
379
+ if (source === "local" || source === "registry" || source === "both")
380
+ return source;
381
+ if (typeof source === "undefined")
382
+ return "local";
383
+ throw new Error(`Invalid search source: ${String(source)}. Expected one of: local|registry|both`);
384
+ }
385
+ function mergeSearchHits(localHits, registryHits, limit) {
386
+ const merged = [];
387
+ let localIndex = 0;
388
+ let registryIndex = 0;
389
+ while (merged.length < limit && (localIndex < localHits.length || registryIndex < registryHits.length)) {
390
+ if (localIndex < localHits.length) {
391
+ merged.push(localHits[localIndex]);
392
+ localIndex += 1;
393
+ if (merged.length >= limit)
394
+ break;
395
+ }
396
+ if (registryIndex < registryHits.length) {
397
+ merged.push(registryHits[registryIndex]);
398
+ registryIndex += 1;
399
+ }
400
+ }
401
+ return merged;
402
+ }
403
+ function shouldIncludeUsageGuide(mode) {
404
+ return mode === "both" || mode === "guide";
405
+ }
406
+ function shouldIncludeItemUsage(mode) {
407
+ return mode === "both" || mode === "item";
408
+ }
409
+ function buildUsageGuideFromEntries(entries, searchType) {
410
+ const types = entries.map((entry) => entry.type);
411
+ const fallbackGuide = buildUsageGuide(types, searchType);
412
+ const metadataByType = new Map();
413
+ for (const entry of entries) {
414
+ if (!entry.usage || entry.usage.length === 0)
415
+ continue;
416
+ const current = metadataByType.get(entry.type) ?? [];
417
+ for (const item of entry.usage) {
418
+ const trimmed = item.trim();
419
+ if (trimmed && !current.includes(trimmed))
420
+ current.push(trimmed);
421
+ }
422
+ if (current.length > 0)
423
+ metadataByType.set(entry.type, current);
424
+ }
425
+ if (!fallbackGuide && metadataByType.size === 0)
426
+ return undefined;
427
+ const result = {};
428
+ for (const assetType of resolveGuideTypes(types, searchType)) {
429
+ const lines = [];
430
+ const metadataLines = metadataByType.get(assetType);
431
+ if (metadataLines && metadataLines.length > 0) {
432
+ lines.push(...metadataLines);
433
+ }
434
+ const fallbackLines = fallbackGuide?.[assetType];
435
+ if (fallbackLines && fallbackLines.length > 0) {
436
+ for (const line of fallbackLines) {
437
+ if (!lines.includes(line))
438
+ lines.push(line);
439
+ }
440
+ }
441
+ if (lines.length > 0)
442
+ result[assetType] = lines;
443
+ }
444
+ return Object.keys(result).length > 0 ? result : undefined;
445
+ }
446
+ function buildUsageGuide(hitTypes, searchType) {
447
+ const result = {};
448
+ for (const assetType of resolveGuideTypes(hitTypes, searchType)) {
449
+ result[assetType] = usageGuideByType(assetType);
450
+ }
451
+ return Object.keys(result).length > 0 ? result : undefined;
452
+ }
453
+ function resolveGuideTypes(hitTypes, searchType) {
454
+ if (searchType !== "any")
455
+ return [searchType];
456
+ return Array.from(new Set(hitTypes));
457
+ }
458
+ function usageGuideByType(type) {
459
+ const handler = tryGetHandler(type);
460
+ return handler?.defaultUsageGuide ?? [];
461
+ }
243
462
  function fileToAsset(assetType, root, file) {
244
463
  const name = deriveCanonicalAssetName(assetType, root, file);
245
464
  if (!name)
@@ -2,4 +2,4 @@ import type { KnowledgeView, ShowResponse } from "./stash-types";
2
2
  export declare function agentikitShow(input: {
3
3
  ref: string;
4
4
  view?: KnowledgeView;
5
- }): ShowResponse;
5
+ }): Promise<ShowResponse>;