akm-cli 0.5.0 → 0.6.0-rc1

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 (74) hide show
  1. package/CHANGELOG.md +32 -5
  2. package/dist/asset-registry.js +29 -5
  3. package/dist/asset-spec.js +12 -5
  4. package/dist/cli-hints.js +300 -0
  5. package/dist/cli.js +218 -1357
  6. package/dist/common.js +147 -50
  7. package/dist/config.js +224 -13
  8. package/dist/create-provider-registry.js +1 -1
  9. package/dist/curate.js +258 -0
  10. package/dist/{local-search.js → db-search.js} +30 -19
  11. package/dist/db.js +168 -62
  12. package/dist/embedder.js +49 -273
  13. package/dist/embedders/cache.js +47 -0
  14. package/dist/embedders/local.js +152 -0
  15. package/dist/embedders/remote.js +121 -0
  16. package/dist/embedders/types.js +39 -0
  17. package/dist/errors.js +14 -3
  18. package/dist/frontmatter.js +61 -7
  19. package/dist/indexer.js +38 -7
  20. package/dist/info.js +2 -2
  21. package/dist/install-audit.js +16 -1
  22. package/dist/{installed-kits.js → installed-stashes.js} +48 -22
  23. package/dist/llm-client.js +92 -0
  24. package/dist/llm.js +14 -126
  25. package/dist/lockfile.js +28 -1
  26. package/dist/matchers.js +1 -1
  27. package/dist/metadata-enhance.js +53 -0
  28. package/dist/migration-help.js +75 -44
  29. package/dist/output-context.js +77 -0
  30. package/dist/output-shapes.js +198 -0
  31. package/dist/output-text.js +520 -0
  32. package/dist/paths.js +4 -4
  33. package/dist/providers/index.js +11 -0
  34. package/dist/providers/skills-sh.js +1 -1
  35. package/dist/providers/static-index.js +47 -45
  36. package/dist/registry-build-index.js +36 -29
  37. package/dist/registry-factory.js +2 -2
  38. package/dist/registry-resolve.js +8 -4
  39. package/dist/registry-search.js +62 -5
  40. package/dist/remember.js +172 -0
  41. package/dist/renderers.js +52 -0
  42. package/dist/search-source.js +73 -42
  43. package/dist/setup-steps.js +45 -0
  44. package/dist/setup.js +149 -76
  45. package/dist/stash-add.js +94 -38
  46. package/dist/stash-clone.js +4 -4
  47. package/dist/stash-provider-factory.js +2 -2
  48. package/dist/stash-provider.js +3 -1
  49. package/dist/stash-providers/filesystem.js +31 -1
  50. package/dist/stash-providers/git.js +209 -8
  51. package/dist/stash-providers/index.js +1 -0
  52. package/dist/stash-providers/npm.js +159 -0
  53. package/dist/stash-providers/provider-utils.js +162 -0
  54. package/dist/stash-providers/sync-from-ref.js +45 -0
  55. package/dist/stash-providers/tar-utils.js +151 -0
  56. package/dist/stash-providers/website.js +80 -4
  57. package/dist/stash-resolve.js +5 -5
  58. package/dist/stash-search.js +4 -4
  59. package/dist/stash-show.js +3 -3
  60. package/dist/wiki.js +6 -6
  61. package/dist/workflow-authoring.js +12 -4
  62. package/dist/workflow-markdown.js +9 -0
  63. package/dist/workflow-runs.js +12 -2
  64. package/docs/README.md +30 -0
  65. package/docs/migration/release-notes/0.0.13.md +4 -0
  66. package/docs/migration/release-notes/0.1.0.md +6 -0
  67. package/docs/migration/release-notes/0.2.0.md +6 -0
  68. package/docs/migration/release-notes/0.3.0.md +5 -0
  69. package/docs/migration/release-notes/0.5.0.md +6 -0
  70. package/docs/migration/release-notes/0.6.0.md +29 -0
  71. package/docs/migration/release-notes/README.md +21 -0
  72. package/package.json +3 -2
  73. package/dist/registry-install.js +0 -532
  74. /package/dist/{kit-include.js → stash-include.js} +0 -0
package/dist/curate.js ADDED
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Curate logic for `akm curate`.
3
+ *
4
+ * Given a query (and optional type filter / source / limit), pick a small,
5
+ * high-signal set of stash + registry hits and enrich each with the data
6
+ * needed to act (ref, run, parameters, follow-up command).
7
+ *
8
+ * The exported `akmCurate()` API is the single entry point. Internal
9
+ * helpers stay private. Tests can drive the public API or call the smaller
10
+ * pure helpers (`curateSearchResults`, `orderCuratedTypes`,
11
+ * `deriveCurateFallbackQueries`) by importing them directly.
12
+ */
13
+ import { truncateDescription } from "./output-shapes";
14
+ import { akmSearch, parseSearchSource } from "./stash-search";
15
+ import { akmShowUnified } from "./stash-show";
16
+ const CURATE_FALLBACK_FILTER_WORDS = new Set([
17
+ "a",
18
+ "an",
19
+ "and",
20
+ "for",
21
+ "how",
22
+ "i",
23
+ "in",
24
+ "of",
25
+ "or",
26
+ "the",
27
+ "to",
28
+ "with",
29
+ ]);
30
+ const CURATED_TYPE_FALLBACK_ORDER = ["skill", "command", "script", "knowledge", "agent", "memory"];
31
+ const CURATED_TYPE_FALLBACK_INDEX = new Map(CURATED_TYPE_FALLBACK_ORDER.map((type, index) => [type, index]));
32
+ const MIN_CURATE_FALLBACK_TOKEN_LENGTH = 3;
33
+ const MAX_CURATE_FALLBACK_KEYWORDS = 6;
34
+ export const CURATE_SEARCH_LIMIT_MULTIPLIER = 4;
35
+ export const MIN_CURATE_SEARCH_LIMIT = 12;
36
+ const DEFAULT_CURATE_LIMIT = 4;
37
+ /**
38
+ * Public curate entry point. Performs the search itself when
39
+ * `options.searchResponse` is not supplied.
40
+ */
41
+ export async function akmCurate(options) {
42
+ const limit = options.limit && options.limit > 0 ? options.limit : DEFAULT_CURATE_LIMIT;
43
+ const source = options.source ?? parseSearchSource("stash");
44
+ const searchResponse = options.searchResponse ??
45
+ (await searchForCuration({
46
+ query: options.query,
47
+ type: options.type,
48
+ // Search deeper than the final curated count so we can pick one strong
49
+ // match per type and still have room for fallback retries.
50
+ limit: Math.max(limit * CURATE_SEARCH_LIMIT_MULTIPLIER, MIN_CURATE_SEARCH_LIMIT),
51
+ source,
52
+ }));
53
+ return curateSearchResults(options.query, searchResponse, limit, options.type);
54
+ }
55
+ export async function curateSearchResults(query, result, limit, selectedType) {
56
+ const stashHits = result.hits.filter((hit) => hit.type !== "registry");
57
+ const registryHits = result.registryHits ?? [];
58
+ let selectedStashHits;
59
+ if (selectedType && selectedType !== "any") {
60
+ selectedStashHits = stashHits.slice(0, limit);
61
+ }
62
+ else {
63
+ const bestByType = new Map();
64
+ for (const hit of stashHits) {
65
+ if (!bestByType.has(hit.type))
66
+ bestByType.set(hit.type, hit);
67
+ }
68
+ const orderedTypes = orderCuratedTypes(query, Array.from(bestByType.keys()));
69
+ selectedStashHits = orderedTypes
70
+ .map((type) => bestByType.get(type))
71
+ .filter((hit) => Boolean(hit));
72
+ }
73
+ const selectedRegistryHits = selectedStashHits.length >= limit ? [] : registryHits.slice(0, Math.min(2, limit - selectedStashHits.length));
74
+ const items = [
75
+ ...(await Promise.all(selectedStashHits.slice(0, limit).map((hit) => enrichCuratedStashHit(query, hit)))),
76
+ ...selectedRegistryHits.map((hit) => buildCuratedRegistryItem(query, hit)),
77
+ ].slice(0, limit);
78
+ return {
79
+ query,
80
+ summary: buildCurateSummary(query, items),
81
+ items,
82
+ ...(result.warnings?.length ? { warnings: result.warnings } : {}),
83
+ ...(result.tip ? { tip: result.tip } : {}),
84
+ };
85
+ }
86
+ export function orderCuratedTypes(query, types) {
87
+ const lower = query.toLowerCase();
88
+ const boosts = new Map();
89
+ const addBoost = (type, amount) => boosts.set(type, (boosts.get(type) ?? 0) + amount);
90
+ if (/(run|script|bash|shell|cli|execute|automation|deploy|build|test|lint)/.test(lower)) {
91
+ addBoost("script", 6);
92
+ addBoost("command", 4);
93
+ }
94
+ if (/(guide|docs?|readme|reference|how|explain|learn|why)/.test(lower)) {
95
+ addBoost("knowledge", 6);
96
+ addBoost("skill", 4);
97
+ }
98
+ if (/(agent|assistant|planner|review|analy[sz]e|architect|prompt)/.test(lower)) {
99
+ addBoost("agent", 6);
100
+ addBoost("skill", 3);
101
+ }
102
+ if (/(config|template|release|generate|command)/.test(lower)) {
103
+ addBoost("command", 5);
104
+ }
105
+ if (/(memory|context|recall|remember)/.test(lower)) {
106
+ addBoost("memory", 6);
107
+ }
108
+ return [...types].sort((a, b) => {
109
+ const boostDiff = (boosts.get(b) ?? 0) - (boosts.get(a) ?? 0);
110
+ if (boostDiff !== 0)
111
+ return boostDiff;
112
+ return ((CURATED_TYPE_FALLBACK_INDEX.get(a) ?? Number.MAX_SAFE_INTEGER) -
113
+ (CURATED_TYPE_FALLBACK_INDEX.get(b) ?? Number.MAX_SAFE_INTEGER));
114
+ });
115
+ }
116
+ async function enrichCuratedStashHit(query, hit) {
117
+ let shown;
118
+ try {
119
+ shown = await akmShowUnified({ ref: hit.ref });
120
+ }
121
+ catch {
122
+ shown = undefined;
123
+ }
124
+ const description = shown?.description ?? hit.description;
125
+ const preview = buildCuratedPreview(shown, hit);
126
+ return {
127
+ source: "stash",
128
+ type: shown?.type ?? hit.type,
129
+ name: shown?.name ?? hit.name,
130
+ ref: hit.ref,
131
+ ...(description ? { description } : {}),
132
+ ...(preview ? { preview } : {}),
133
+ ...(shown?.parameters?.length ? { parameters: shown.parameters } : {}),
134
+ ...(shown?.run ? { run: shown.run } : {}),
135
+ followUp: `akm show ${hit.ref}`,
136
+ reason: buildCuratedReason(query, shown?.type ?? hit.type),
137
+ ...(hit.score !== undefined ? { score: hit.score } : {}),
138
+ };
139
+ }
140
+ function buildCuratedRegistryItem(query, hit) {
141
+ return {
142
+ source: "registry",
143
+ type: "registry",
144
+ name: hit.name,
145
+ id: hit.id,
146
+ ...(hit.description ? { description: hit.description } : {}),
147
+ followUp: hit.action ?? `akm add ${hit.id}`,
148
+ reason: `Useful external source to explore for ${query}.`,
149
+ ...(hit.score !== undefined ? { score: hit.score } : {}),
150
+ };
151
+ }
152
+ function firstNonEmpty(values) {
153
+ return values.find((value) => typeof value === "string" && value.trim().length > 0);
154
+ }
155
+ function buildCuratedPreview(shown, hit) {
156
+ if (shown?.run)
157
+ return truncateDescription(`run ${shown.run}`, 160);
158
+ const payload = firstNonEmpty([shown?.template, shown?.prompt, shown?.content, hit.description])
159
+ ?.replace(/\s+/g, " ")
160
+ .trim();
161
+ return payload ? truncateDescription(payload, 160) : undefined;
162
+ }
163
+ function buildCuratedReason(query, type) {
164
+ switch (type) {
165
+ case "script":
166
+ return `Best runnable script match for "${query}".`;
167
+ case "command":
168
+ return `Best reusable command/template match for "${query}".`;
169
+ case "knowledge":
170
+ return `Best reference document match for "${query}".`;
171
+ case "skill":
172
+ return `Best instructions/workflow match for "${query}".`;
173
+ case "agent":
174
+ return `Best specialized agent prompt match for "${query}".`;
175
+ case "memory":
176
+ return `Best saved context match for "${query}".`;
177
+ default:
178
+ return `Best ${type} match for "${query}".`;
179
+ }
180
+ }
181
+ function buildCurateSummary(query, items) {
182
+ if (items.length === 0) {
183
+ return `No curated assets were selected for "${query}".`;
184
+ }
185
+ const labels = items.map((item) => `${item.type}:${item.name}`);
186
+ return `Selected ${items.length} high-signal result${items.length === 1 ? "" : "s"}: ${labels.join(", ")}.`;
187
+ }
188
+ function hasSearchResults(result) {
189
+ return result.hits.length > 0 || (result.registryHits?.length ?? 0) > 0;
190
+ }
191
+ /**
192
+ * Extract a small set of fallback keywords when a prompt-style curate query
193
+ * returns no hits as a whole phrase.
194
+ *
195
+ * We keep up to MAX_CURATE_FALLBACK_KEYWORDS distinct keywords and drop short
196
+ * or common filler words so follow-up searches stay inexpensive while focusing
197
+ * on higher-signal terms.
198
+ */
199
+ export function deriveCurateFallbackQueries(query) {
200
+ return Array.from(new Set(query
201
+ .toLowerCase()
202
+ .split(/[^a-z0-9]+/)
203
+ .map((token) => token.trim())
204
+ // Keep longer tokens so fallback stays focused on higher-signal terms
205
+ // and avoids broad one- and two-letter matches that overwhelm curation.
206
+ .filter((token) => token.length >= MIN_CURATE_FALLBACK_TOKEN_LENGTH && !CURATE_FALLBACK_FILTER_WORDS.has(token)))).slice(0, MAX_CURATE_FALLBACK_KEYWORDS);
207
+ }
208
+ export function mergeCurateSearchResponses(base, extras) {
209
+ const hitsByRef = new Map();
210
+ for (const hit of base.hits.filter((entry) => entry.type !== "registry")) {
211
+ hitsByRef.set(hit.ref, hit);
212
+ }
213
+ for (const result of extras) {
214
+ for (const hit of result.hits.filter((entry) => entry.type !== "registry")) {
215
+ const existing = hitsByRef.get(hit.ref);
216
+ if (!existing || (hit.score ?? 0) > (existing.score ?? 0)) {
217
+ hitsByRef.set(hit.ref, hit);
218
+ }
219
+ }
220
+ }
221
+ const registryById = new Map();
222
+ for (const hit of base.registryHits ?? []) {
223
+ registryById.set(hit.id, hit);
224
+ }
225
+ for (const result of extras) {
226
+ for (const hit of result.registryHits ?? []) {
227
+ const existing = registryById.get(hit.id);
228
+ if (!existing || (hit.score ?? 0) > (existing.score ?? 0)) {
229
+ registryById.set(hit.id, hit);
230
+ }
231
+ }
232
+ }
233
+ const warnings = Array.from(new Set([...(base.warnings ?? []), ...extras.flatMap((result) => result.warnings ?? [])]));
234
+ const mergedHits = [...hitsByRef.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
235
+ const mergedRegistryHits = [...registryById.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
236
+ return {
237
+ ...base,
238
+ hits: mergedHits,
239
+ ...(mergedRegistryHits.length > 0 ? { registryHits: mergedRegistryHits } : {}),
240
+ ...(warnings.length > 0 ? { warnings } : {}),
241
+ ...(mergedHits.length > 0 || mergedRegistryHits.length > 0 ? { tip: undefined } : {}),
242
+ };
243
+ }
244
+ export async function searchForCuration(input) {
245
+ const initial = await akmSearch(input);
246
+ if (hasSearchResults(initial))
247
+ return initial;
248
+ const fallbackQueries = deriveCurateFallbackQueries(input.query);
249
+ if (fallbackQueries.length <= 1)
250
+ return initial;
251
+ const fallbackResults = await Promise.all(fallbackQueries.map((token) => akmSearch({
252
+ query: token,
253
+ type: input.type,
254
+ limit: input.limit,
255
+ source: input.source,
256
+ })));
257
+ return mergeCurateSearchResponses(initial, fallbackResults);
258
+ }
@@ -1,32 +1,35 @@
1
1
  /**
2
- * Local (filesystem + SQLite) stash search implementation.
2
+ * Database-backed (SQLite + FTS5/vector) stash search implementation.
3
3
  *
4
4
  * Extracted from stash-search.ts to break the circular import:
5
- * stash-search.ts → stash-providers/filesystem.ts → local-search.ts (no cycle)
5
+ * stash-search.ts → stash-providers/filesystem.ts → db-search.ts (no cycle)
6
6
  *
7
7
  * stash-search.ts imports this module for the `searchLocal` export.
8
8
  * stash-providers/filesystem.ts also imports `searchLocal` from here.
9
+ *
10
+ * Renamed from `local-search.ts` to signal that this is the DB-layer search
11
+ * implementation, not a "local vs. remote" distinction.
9
12
  */
10
13
  import fs from "node:fs";
11
14
  import path from "node:path";
12
- import { ACTION_BUILDERS, TYPE_TO_RENDERER } from "./asset-registry";
15
+ import { defaultRendererRegistry } from "./asset-registry";
13
16
  import { deriveCanonicalAssetNameFromStashRoot } from "./asset-spec";
14
17
  import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, getUtilityScoresByIds, openDatabase, searchFts, searchVec, } from "./db";
15
18
  import { getRenderer } from "./file-context";
16
- import { buildSearchText } from "./indexer";
17
19
  import { generateMetadataFlat, loadStashFile, shouldIndexStashFile } from "./metadata";
18
20
  import { getDbPath } from "./paths";
21
+ import { buildSearchText } from "./search-fields";
19
22
  import { buildEditHint, findSourceForPath, isEditable } from "./search-source";
20
23
  import { deriveSemanticProviderFingerprint, getEffectiveSemanticStatus, isSemanticRuntimeReady, readSemanticStatus, } from "./semantic-status";
21
24
  import { makeAssetRef } from "./stash-ref";
22
25
  import { walkStashFlat } from "./walker";
23
26
  import { warn } from "./warn";
24
- export async function rendererForType(type) {
25
- const name = TYPE_TO_RENDERER[type];
27
+ export async function rendererForType(type, registry = defaultRendererRegistry) {
28
+ const name = registry.rendererNameFor(type);
26
29
  return name ? getRenderer(name) : undefined;
27
30
  }
28
- export function buildLocalAction(type, ref) {
29
- const builder = ACTION_BUILDERS[type];
31
+ export function buildLocalAction(type, ref, registry = defaultRendererRegistry) {
32
+ const builder = registry.actionBuilderFor(type);
30
33
  return builder ? builder(ref) : `akm show ${ref}`;
31
34
  }
32
35
  function resolveSearchHitRef(entry, refName, source) {
@@ -41,6 +44,7 @@ function resolveSearchHitOrigin(source) {
41
44
  // ── Main search entrypoint ───────────────────────────────────────────────────
42
45
  export async function searchLocal(input) {
43
46
  const { query, searchType, limit, stashDir, sources, config } = input;
47
+ const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
44
48
  const allStashDirs = sources.map((s) => s.path);
45
49
  const rawStatus = readSemanticStatus();
46
50
  const semanticStatus = getEffectiveSemanticStatus(config, rawStatus);
@@ -81,7 +85,7 @@ export async function searchLocal(input) {
81
85
  }
82
86
  }
83
87
  if (entryCount > 0 && stashDirMatch) {
84
- const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allStashDirs, config, sources);
88
+ const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allStashDirs, config, sources, rendererRegistry);
85
89
  return {
86
90
  hits,
87
91
  tip: hits.length === 0
@@ -101,7 +105,7 @@ export async function searchLocal(input) {
101
105
  catch (error) {
102
106
  warn("Search index unavailable, falling back to substring search:", error instanceof Error ? error.message : String(error));
103
107
  }
104
- const hitArrays = await Promise.all(allStashDirs.map((dir) => substringSearch(query, searchType, limit, dir, sources, config)));
108
+ const hitArrays = await Promise.all(allStashDirs.map((dir) => substringSearch(query, searchType, limit, dir, sources, config, rendererRegistry)));
105
109
  const hits = hitArrays.flat().slice(0, limit);
106
110
  return {
107
111
  hits,
@@ -110,7 +114,7 @@ export async function searchLocal(input) {
110
114
  };
111
115
  }
112
116
  // ── Database search ─────────────────────────────────────────────────────────
113
- async function searchDatabase(db, query, searchType, limit, stashDir, allStashDirs, config, sources) {
117
+ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDirs, config, sources, rendererRegistry = defaultRendererRegistry) {
114
118
  // Empty query: return all entries
115
119
  if (!query) {
116
120
  const typeFilter = searchType === "any" ? undefined : searchType;
@@ -134,6 +138,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
134
138
  allStashDirs,
135
139
  sources,
136
140
  config,
141
+ rendererRegistry,
137
142
  })));
138
143
  return { hits };
139
144
  }
@@ -371,6 +376,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
371
376
  sources,
372
377
  config,
373
378
  utilityBoosted,
379
+ rendererRegistry,
374
380
  })));
375
381
  return { embedMs, rankMs, hits };
376
382
  }
@@ -401,20 +407,24 @@ async function tryVecScores(db, query, k, config) {
401
407
  }
402
408
  }
403
409
  // ── Substring fallback (no index) ───────────────────────────────────────────
404
- async function substringSearch(query, searchType, limit, stashDir, sources, config) {
410
+ async function substringSearch(query, searchType, limit, stashDir, sources, config, rendererRegistry = defaultRendererRegistry) {
405
411
  const assets = await indexAssets(stashDir, searchType, sources);
406
412
  const matched = assets.filter((asset) => !query || buildSearchText(asset.entry).includes(query));
407
413
  if (!query) {
408
414
  const sorted = matched.sort(compareAssets);
409
415
  const unique = deduplicateAssetsByPath(sorted);
410
- return Promise.all(unique.slice(0, limit).map((asset) => assetToSearchHit(asset, stashDir, sources, config)));
416
+ return Promise.all(unique
417
+ .slice(0, limit)
418
+ .map((asset) => assetToSearchHit(asset, stashDir, sources, config, undefined, rendererRegistry)));
411
419
  }
412
420
  // Score and sort by relevance
413
421
  const scored = matched.map((asset) => ({ asset, score: scoreSubstringMatch(asset.entry, query) }));
414
422
  scored.sort((a, b) => b.score - a.score || compareAssets(a.asset, b.asset));
415
423
  // Deduplicate by path — keep highest-scored entry per file
416
424
  const dedupedScored = deduplicateByPath(scored.map((s) => ({ ...s, filePath: s.asset.path })));
417
- return Promise.all(dedupedScored.slice(0, limit).map(({ asset, score }) => assetToSearchHit(asset, stashDir, sources, config, score)));
425
+ return Promise.all(dedupedScored
426
+ .slice(0, limit)
427
+ .map(({ asset, score }) => assetToSearchHit(asset, stashDir, sources, config, score, rendererRegistry)));
418
428
  }
419
429
  function scoreSubstringMatch(entry, query) {
420
430
  const tokens = query.split(/\s+/).filter(Boolean);
@@ -444,6 +454,7 @@ function scoreSubstringMatch(entry, query) {
444
454
  }
445
455
  // ── Hit building ────────────────────────────────────────────────────────────
446
456
  export async function buildDbHit(input) {
457
+ const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
447
458
  const entryStashDir = findSourceForPath(input.path, input.sources)?.path ?? input.defaultStashDir;
448
459
  const canonical = deriveCanonicalAssetNameFromStashRoot(input.entry.type, entryStashDir, input.path);
449
460
  const refName = canonical && !canonical.startsWith("../") && !canonical.startsWith("..\\") ? canonical : input.entry.name;
@@ -471,12 +482,12 @@ export async function buildDbHit(input) {
471
482
  description: input.entry.description,
472
483
  tags: input.entry.tags,
473
484
  size: deriveSize(input.entry.fileSize),
474
- action: buildLocalAction(input.entry.type, ref),
485
+ action: buildLocalAction(input.entry.type, ref, rendererRegistry),
475
486
  score,
476
487
  whyMatched,
477
488
  ...(estimatedTokens !== undefined ? { estimatedTokens } : {}),
478
489
  };
479
- const renderer = await rendererForType(input.entry.type);
490
+ const renderer = await rendererForType(input.entry.type, rendererRegistry);
480
491
  if (renderer?.enrichSearchHit) {
481
492
  renderer.enrichSearchHit(hit, entryStashDir);
482
493
  }
@@ -530,7 +541,7 @@ rankingMode, qualityBoost, confidenceBoost, utilityBoosted) {
530
541
  reasons.push("usage history boost");
531
542
  return reasons;
532
543
  }
533
- async function assetToSearchHit(asset, stashDir, sources, config, score) {
544
+ async function assetToSearchHit(asset, stashDir, sources, config, score, rendererRegistry = defaultRendererRegistry) {
534
545
  const source = findSourceForPath(asset.path, sources);
535
546
  const editable = isEditable(asset.path, config);
536
547
  const ref = resolveSearchHitRef(asset.entry, asset.entry.name, source);
@@ -550,11 +561,11 @@ async function assetToSearchHit(asset, stashDir, sources, config, score) {
550
561
  description: asset.entry.description,
551
562
  tags: asset.entry.tags,
552
563
  ...(size ? { size } : {}),
553
- action: buildLocalAction(asset.entry.type, ref),
564
+ action: buildLocalAction(asset.entry.type, ref, rendererRegistry),
554
565
  ...(score !== undefined ? { score } : {}),
555
566
  ...(estimatedTokens !== undefined ? { estimatedTokens } : {}),
556
567
  };
557
- const renderer = await rendererForType(asset.entry.type);
568
+ const renderer = await rendererForType(asset.entry.type, rendererRegistry);
558
569
  if (renderer?.enrichSearchHit) {
559
570
  renderer.enrichSearchHit(hit, stashDir);
560
571
  }