akm-cli 0.0.20 → 0.0.22

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 (45) hide show
  1. package/README.md +8 -5
  2. package/dist/asset-spec.js +96 -9
  3. package/dist/cli.js +195 -55
  4. package/dist/common.js +15 -2
  5. package/dist/config-cli.js +65 -6
  6. package/dist/config.js +206 -22
  7. package/dist/create-provider-registry.js +18 -0
  8. package/dist/db.js +156 -53
  9. package/dist/embedder.js +36 -18
  10. package/dist/errors.js +6 -0
  11. package/dist/file-context.js +18 -19
  12. package/dist/frontmatter.js +19 -3
  13. package/dist/indexer.js +126 -89
  14. package/dist/{stash-registry.js → installed-kits.js} +16 -24
  15. package/dist/kit-include.js +108 -0
  16. package/dist/local-search.js +429 -0
  17. package/dist/lockfile.js +47 -5
  18. package/dist/matchers.js +6 -0
  19. package/dist/metadata.js +22 -16
  20. package/dist/paths.js +4 -0
  21. package/dist/providers/skills-sh.js +3 -2
  22. package/dist/providers/static-index.js +4 -9
  23. package/dist/registry-build-index.js +356 -0
  24. package/dist/registry-factory.js +19 -0
  25. package/dist/registry-install.js +114 -109
  26. package/dist/registry-resolve.js +44 -9
  27. package/dist/registry-search.js +14 -9
  28. package/dist/renderers.js +23 -7
  29. package/dist/ripgrep-install.js +9 -4
  30. package/dist/self-update.js +31 -4
  31. package/dist/stash-add.js +75 -6
  32. package/dist/stash-clone.js +1 -1
  33. package/dist/stash-provider-factory.js +52 -0
  34. package/dist/stash-provider.js +1 -0
  35. package/dist/stash-providers/filesystem.js +42 -0
  36. package/dist/stash-providers/index.js +9 -0
  37. package/dist/stash-providers/openviking.js +337 -0
  38. package/dist/stash-resolve.js +33 -3
  39. package/dist/stash-search.js +70 -402
  40. package/dist/stash-show.js +24 -5
  41. package/dist/stash-source.js +19 -11
  42. package/dist/walker.js +15 -10
  43. package/dist/warn.js +7 -0
  44. package/package.json +1 -1
  45. package/dist/provider-registry.js +0 -8
@@ -1,18 +1,11 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { deriveCanonicalAssetName, TYPE_DIRS } from "./asset-spec";
4
1
  import { loadConfig } from "./config";
5
- import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, openDatabase, searchFts, searchVec, } from "./db";
2
+ import { ACTION_BUILDERS, buildLocalAction, rendererForType, searchLocal, TYPE_TO_RENDERER } from "./local-search";
3
+ import { resolveStashProviders } from "./stash-provider-factory";
4
+ // Eagerly import stash providers to trigger self-registration
5
+ import "./stash-providers/index";
6
6
  import { UsageError } from "./errors";
7
- import { getRenderer } from "./file-context";
8
- import { buildSearchText } from "./indexer";
9
- import { generateMetadataFlat, loadStashFile } from "./metadata";
10
- import { getDbPath } from "./paths";
11
7
  import { searchRegistry } from "./registry-search";
12
- import { makeAssetRef } from "./stash-ref";
13
- import { buildEditHint, findSourceForPath, isEditable, resolveStashSources } from "./stash-source";
14
- import { walkStashFlat } from "./walker";
15
- import { warn } from "./warn";
8
+ import { resolveStashSources } from "./stash-source";
16
9
  const DEFAULT_LIMIT = 20;
17
10
  export async function agentikitSearch(input) {
18
11
  const t0 = Date.now();
@@ -20,10 +13,12 @@ export async function agentikitSearch(input) {
20
13
  const normalizedQuery = query.toLowerCase();
21
14
  const searchType = input.type ?? "any";
22
15
  const limit = normalizeLimit(input.limit);
23
- const source = parseSearchSource(input.source);
16
+ const source = parseSearchSource(input.source ?? "stash");
24
17
  const config = loadConfig();
25
18
  const sources = resolveStashSources(undefined, config);
26
19
  if (sources.length === 0) {
20
+ // stashDir: "" is a safe sentinel here — the response carries zero hits
21
+ // and a warning, so no downstream code will try to use the empty path.
27
22
  return {
28
23
  schemaVersion: 1,
29
24
  stashDir: "",
@@ -33,7 +28,11 @@ export async function agentikitSearch(input) {
33
28
  timing: { totalMs: Date.now() - t0 },
34
29
  };
35
30
  }
31
+ // Primary stash directory — used for DB path lookups and as the default
32
+ // stash root. Safe because the empty-sources case is handled above.
36
33
  const stashDir = sources[0].path;
34
+ // Resolve additional stash providers (e.g. OpenViking) from config
35
+ const additionalStashProviders = resolveStashProviders(config);
37
36
  const localResult = source === "registry"
38
37
  ? undefined
39
38
  : await searchLocal({
@@ -44,15 +43,35 @@ export async function agentikitSearch(input) {
44
43
  sources,
45
44
  config,
46
45
  });
47
- const registryResult = source === "local" ? undefined : await searchRegistry(query, { limit, registries: config.registries });
48
- if (source === "local") {
46
+ // Query additional stash providers (e.g. OpenViking)
47
+ const additionalStashResults = source === "registry" || additionalStashProviders.length === 0 || !query
48
+ ? []
49
+ : await Promise.all(additionalStashProviders.map(async (provider) => {
50
+ try {
51
+ return await provider.search({ query, type: searchType === "any" ? undefined : searchType, limit });
52
+ }
53
+ catch (err) {
54
+ return {
55
+ hits: [],
56
+ warnings: [`Stash ${provider.name}: ${err instanceof Error ? err.message : String(err)}`],
57
+ };
58
+ }
59
+ }));
60
+ // Merge stash hits from all providers
61
+ const additionalHits = additionalStashResults.flatMap((r) => r.hits);
62
+ const additionalWarnings = additionalStashResults.flatMap((r) => r.warnings ?? []);
63
+ const registryResult = source === "stash" ? undefined : await searchRegistry(query, { limit, registries: config.registries });
64
+ if (source === "stash") {
65
+ const allStashHits = mergeStashHits(localResult?.hits ?? [], additionalHits, limit);
66
+ const localWarnings = [...(localResult?.warnings ?? []), ...additionalWarnings];
67
+ const hasResults = allStashHits.length > 0;
49
68
  return {
50
69
  schemaVersion: 1,
51
70
  stashDir,
52
71
  source,
53
- hits: localResult?.hits ?? [],
54
- tip: localResult?.tip,
55
- warnings: localResult?.warnings,
72
+ hits: allStashHits,
73
+ tip: hasResults ? undefined : localResult?.tip,
74
+ warnings: localWarnings.length > 0 ? localWarnings : undefined,
56
75
  timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
57
76
  };
58
77
  }
@@ -71,318 +90,50 @@ export async function agentikitSearch(input) {
71
90
  });
72
91
  if (source === "registry") {
73
92
  const hits = registryHits.slice(0, limit);
93
+ const hasResults = hits.length > 0;
74
94
  return {
75
95
  schemaVersion: 1,
76
96
  stashDir,
77
97
  source,
78
98
  hits,
79
- tip: hits.length === 0 ? "No matching registry entries were found." : undefined,
99
+ tip: hasResults ? undefined : "No matching registry entries were found.",
80
100
  warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
81
101
  timing: { totalMs: Date.now() - t0 },
82
102
  };
83
103
  }
84
- const mergedHits = mergeSearchHits(localResult?.hits ?? [], registryHits, limit);
85
- const warnings = [...(localResult?.warnings ?? []), ...(registryResult?.warnings ?? [])];
104
+ // source === "both"
105
+ const allStashHits = mergeStashHits(localResult?.hits ?? [], additionalHits, limit * 2);
106
+ const mergedHits = mergeSearchHits(allStashHits, registryHits, limit);
107
+ const warnings = [...(localResult?.warnings ?? []), ...additionalWarnings, ...(registryResult?.warnings ?? [])];
108
+ const hasResults = mergedHits.length > 0;
86
109
  return {
87
110
  schemaVersion: 1,
88
111
  stashDir,
89
112
  source,
90
113
  hits: mergedHits,
91
- tip: mergedHits.length === 0 ? "No matching stash assets or registry entries were found." : undefined,
114
+ tip: hasResults ? undefined : "No matching stash assets or registry entries were found.",
92
115
  warnings: warnings.length ? warnings : undefined,
93
116
  timing: { totalMs: Date.now() - t0 },
94
117
  };
95
118
  }
96
- async function searchLocal(input) {
97
- const { query, searchType, limit, stashDir, sources, config } = input;
98
- const allStashDirs = sources.map((s) => s.path);
99
- // Try to open the database
100
- const dbPath = getDbPath();
101
- try {
102
- if (fs.existsSync(dbPath)) {
103
- const embeddingDim = config.embedding?.dimension;
104
- const db = openDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined);
105
- try {
106
- const entryCount = getEntryCount(db);
107
- const storedStashDir = getMeta(db, "stashDir");
108
- if (entryCount > 0 && storedStashDir === stashDir) {
109
- const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allStashDirs, config, sources);
110
- return {
111
- hits,
112
- tip: hits.length === 0
113
- ? "No matching stash assets were found. Try running 'akm index' to rebuild."
114
- : undefined,
115
- embedMs,
116
- rankMs,
117
- };
118
- }
119
- }
120
- finally {
121
- closeDatabase(db);
122
- }
123
- }
124
- }
125
- catch (error) {
126
- warn("Search index unavailable, falling back to substring search:", error instanceof Error ? error.message : String(error));
127
- }
128
- const hits = allStashDirs
129
- .flatMap((dir) => substringSearch(query, searchType, limit, dir, sources, config))
130
- .slice(0, limit);
131
- return {
132
- hits,
133
- tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
134
- };
135
- }
136
- // ── Database search ─────────────────────────────────────────────────────────
137
- async function searchDatabase(db, query, searchType, limit, stashDir, allStashDirs, config, sources) {
138
- // Empty query: return all entries
139
- if (!query) {
140
- const typeFilter = searchType === "any" ? undefined : searchType;
141
- const allEntries = getAllEntries(db, typeFilter);
142
- const selected = allEntries.slice(0, limit);
143
- const hits = selected.map((ie) => buildDbHit({
144
- entry: ie.entry,
145
- path: ie.filePath,
146
- score: 1,
147
- query,
148
- rankingMode: "fts",
149
- defaultStashDir: stashDir,
150
- allStashDirs,
151
- sources,
152
- config,
153
- }));
154
- return {
155
- hits,
156
- };
157
- }
158
- // Score using FTS5 (BM25) and optionally sqlite-vec
159
- const tEmbed0 = Date.now();
160
- const embeddingScores = await tryVecScores(db, query, limit * 3, config);
161
- const embedMs = Date.now() - tEmbed0;
162
- const tRank0 = Date.now();
163
- const typeFilter = searchType === "any" ? undefined : searchType;
164
- const ftsResults = searchFts(db, query, limit * 3, typeFilter);
165
- // Reciprocal Rank Fusion (RRF) constant
166
- const RRF_K = 60;
167
- // Build FTS rank map: rank 1 = best BM25, rank 2 = second best, etc.
168
- // FTS results are already sorted by bm25Score (ascending, more negative = better)
169
- const ftsRankMap = new Map();
170
- for (let i = 0; i < ftsResults.length; i++) {
171
- const r = ftsResults[i];
172
- ftsRankMap.set(r.id, { rank: i + 1, result: r });
173
- }
174
- // Build embedding rank map: sort by cosine similarity descending
175
- const embedRankMap = new Map();
176
- if (embeddingScores) {
177
- const sortedEmbeddings = [...embeddingScores.entries()].sort((a, b) => b[1] - a[1]);
178
- for (let i = 0; i < sortedEmbeddings.length; i++) {
179
- embedRankMap.set(sortedEmbeddings[i][0], i + 1);
180
- }
181
- }
182
- // Merge results using RRF
183
- const scored = [];
184
- const seenIds = new Set();
185
- // Process FTS results
186
- for (const [id, { rank, result }] of ftsRankMap) {
187
- seenIds.add(id);
188
- const ftsRrf = 1 / (RRF_K + rank);
189
- const embedRank = embedRankMap.get(id);
190
- const embedRrf = embedRank !== undefined ? 1 / (RRF_K + embedRank) : 0;
191
- const rrfScore = ftsRrf + embedRrf;
192
- const rankingMode = embedRrf > 0 ? "semantic" : "fts";
193
- scored.push({ id, entry: result.entry, filePath: result.filePath, score: rrfScore, rankingMode });
194
- }
195
- // Add vec-only results not already in FTS results
196
- if (embeddingScores) {
197
- for (const [id] of embeddingScores) {
198
- if (seenIds.has(id))
199
- continue;
200
- const embedRank = embedRankMap.get(id);
201
- if (embedRank === undefined)
202
- continue;
203
- const found = getEntryById(db, id);
204
- if (found) {
205
- if (typeFilter && found.entry.type !== typeFilter)
206
- continue;
207
- const rrfScore = 1 / (RRF_K + embedRank);
208
- scored.push({
209
- id,
210
- entry: found.entry,
211
- filePath: found.filePath,
212
- score: rrfScore,
213
- rankingMode: "semantic",
214
- });
215
- }
216
- }
217
- }
218
- // Apply boosts as multiplicative factors
219
- const queryTokens = query.toLowerCase().split(/\s+/).filter(Boolean);
220
- for (const item of scored) {
221
- const entry = item.entry;
222
- let boostSum = 0;
223
- // Tag boost
224
- if (entry.tags) {
225
- for (const tag of entry.tags) {
226
- if (queryTokens.some((t) => tag.toLowerCase() === t)) {
227
- boostSum += 0.15;
228
- }
229
- }
230
- }
231
- // Search hint boost
232
- if (entry.searchHints) {
233
- for (const hint of entry.searchHints) {
234
- const hintLower = hint.toLowerCase();
235
- for (const token of queryTokens) {
236
- if (hintLower.includes(token)) {
237
- boostSum += 0.12;
238
- break;
239
- }
240
- }
241
- }
242
- }
243
- // Name boost
244
- const nameLower = entry.name.toLowerCase().replace(/[-_]/g, " ");
245
- if (queryTokens.some((t) => nameLower.includes(t))) {
246
- boostSum += 0.1;
247
- }
248
- item.score = item.score * (1 + boostSum);
249
- }
250
- scored.sort((a, b) => b.score - a.score);
251
- const rankMs = Date.now() - tRank0;
252
- const selected = scored.slice(0, limit);
253
- const hits = selected.map(({ entry, filePath, score, rankingMode }) => buildDbHit({
254
- entry,
255
- path: filePath,
256
- score: Math.round(score * 100) / 100,
257
- query,
258
- rankingMode,
259
- defaultStashDir: stashDir,
260
- allStashDirs,
261
- sources,
262
- config,
263
- }));
264
- return {
265
- embedMs,
266
- rankMs,
267
- hits,
268
- };
269
- }
270
- // ── Vector scorer ───────────────────────────────────────────────────────────
271
- async function tryVecScores(db, query, k, config) {
272
- if (!config.semanticSearch)
273
- return null;
274
- const hasEmbeddings = getMeta(db, "hasEmbeddings");
275
- if (hasEmbeddings !== "1")
276
- return null;
277
- try {
278
- const { embed } = await import("./embedder.js");
279
- const queryEmbedding = await embed(query, config.embedding);
280
- const vecResults = searchVec(db, queryEmbedding, k);
281
- const scores = new Map();
282
- for (const { id, distance } of vecResults) {
283
- // Convert L2 distance to cosine similarity (vectors are normalized)
284
- const cosineSim = 1 - (distance * distance) / 2;
285
- scores.set(id, Math.max(0, cosineSim));
286
- }
287
- return scores;
288
- }
289
- catch (error) {
290
- warn("Vector search failed, skipping:", error instanceof Error ? error.message : String(error));
291
- return null;
292
- }
293
- }
294
- // ── Substring fallback (no index) ───────────────────────────────────────────
295
- function substringSearch(query, searchType, limit, stashDir, sources, config) {
296
- const assets = indexAssets(stashDir, searchType);
297
- return assets
298
- .filter((asset) => !query || buildSearchText(asset.entry).includes(query))
299
- .sort(compareAssets)
300
- .slice(0, limit)
301
- .map((asset) => assetToSearchHit(asset, stashDir, sources, config));
302
- }
303
- // ── Hit building ────────────────────────────────────────────────────────────
304
- function buildDbHit(input) {
305
- const entryStashDir = findSourceForPath(input.path, input.sources)?.path ?? input.defaultStashDir;
306
- const typeRoot = path.join(entryStashDir, TYPE_DIRS[input.entry.type]);
307
- const canonical = deriveCanonicalAssetName(input.entry.type, typeRoot, input.path);
308
- // Guard against path traversal when the file is outside the expected type root
309
- // (e.g. source detection fell back to defaultStashDir for a file from another source)
310
- const refName = canonical && !canonical.startsWith("../") && !canonical.startsWith("..\\") ? canonical : input.entry.name;
311
- const qualityBoost = input.entry.quality === "generated" ? 0 : 0.05;
312
- const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0;
313
- const score = Math.round(input.score * (1 + qualityBoost + confidenceBoost) * 100) / 100;
314
- const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost);
315
- const source = findSourceForPath(input.path, input.sources);
316
- const editable = isEditable(input.path, input.config);
317
- const hit = {
318
- type: input.entry.type,
319
- name: input.entry.name,
320
- path: input.path,
321
- ref: makeAssetRef(input.entry.type, refName, source?.registryId),
322
- origin: source?.registryId ?? null,
323
- editable,
324
- ...(!editable ? { editHint: buildEditHint(input.path, input.entry.type, refName, source?.registryId) } : {}),
325
- description: input.entry.description,
326
- tags: input.entry.tags,
327
- size: deriveSize(input.entry.fileSize),
328
- action: buildLocalAction(input.entry.type, makeAssetRef(input.entry.type, refName, source?.registryId)),
329
- score,
330
- whyMatched,
331
- };
332
- const renderer = rendererForType(input.entry.type);
333
- if (renderer?.enrichSearchHit) {
334
- renderer.enrichSearchHit(hit, entryStashDir);
335
- }
336
- return hit;
337
- }
338
- function buildWhyMatched(entry, query, rankingMode, qualityBoost, confidenceBoost) {
339
- const reasons = [rankingMode === "semantic" ? "semantic similarity" : "fts bm25 relevance"];
340
- const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
341
- const name = entry.name.toLowerCase();
342
- const tags = entry.tags?.join(" ").toLowerCase() ?? "";
343
- const searchHints = entry.searchHints?.join(" ").toLowerCase() ?? "";
344
- const aliases = entry.aliases?.join(" ").toLowerCase() ?? "";
345
- if (tokens.some((t) => name.includes(t)))
346
- reasons.push("matched name tokens");
347
- if (tokens.some((t) => tags.includes(t)))
348
- reasons.push("matched tags");
349
- if (tokens.some((t) => searchHints.includes(t)))
350
- reasons.push("matched searchHints");
351
- if (tokens.some((t) => aliases.includes(t)))
352
- reasons.push("matched aliases");
353
- if (qualityBoost > 0)
354
- reasons.push("curated metadata boost");
355
- if (confidenceBoost > 0)
356
- reasons.push("metadata confidence boost");
357
- return reasons;
358
- }
359
- // ── Helpers ─────────────────────────────────────────────────────────────────
360
- function assetToSearchHit(asset, stashDir, sources, config) {
361
- const source = findSourceForPath(asset.path, sources);
362
- const editable = isEditable(asset.path, config);
363
- const ref = makeAssetRef(asset.entry.type, asset.entry.name, source?.registryId);
364
- const fileSize = readFileSize(asset.path);
365
- const size = deriveSize(fileSize);
366
- const hit = {
367
- type: asset.entry.type,
368
- name: asset.entry.name,
369
- path: asset.path,
370
- ref,
371
- origin: source?.registryId ?? null,
372
- editable,
373
- ...(!editable
374
- ? { editHint: buildEditHint(asset.path, asset.entry.type, asset.entry.name, source?.registryId) }
375
- : {}),
376
- description: asset.entry.description,
377
- tags: asset.entry.tags,
378
- ...(size ? { size } : {}),
379
- action: buildLocalAction(asset.entry.type, ref),
380
- };
381
- const renderer = rendererForType(asset.entry.type);
382
- if (renderer?.enrichSearchHit) {
383
- renderer.enrichSearchHit(hit, stashDir);
384
- }
385
- return hit;
119
+ // Re-export searchLocal so existing callers (filesystem.ts) still work via this module
120
+ export { searchLocal };
121
+ // ── Type renderer and action builder registration ────────────────────────────
122
+ export function registerTypeRenderer(type, rendererName) {
123
+ TYPE_TO_RENDERER[type] = rendererName;
124
+ }
125
+ export function registerActionBuilder(type, builder) {
126
+ ACTION_BUILDERS[type] = builder;
127
+ }
128
+ // Re-export for consumers that were already importing from stash-search
129
+ export { buildLocalAction, rendererForType };
130
+ // ── Helpers ──────────────────────────────────────────────────────────────────
131
+ function mergeStashHits(localHits, additionalHits, limit) {
132
+ if (additionalHits.length === 0)
133
+ return localHits.slice(0, limit);
134
+ const all = [...localHits, ...additionalHits];
135
+ all.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
136
+ return all.slice(0, limit);
386
137
  }
387
138
  function normalizeLimit(limit) {
388
139
  if (typeof limit !== "number" || Number.isNaN(limit) || limit <= 0) {
@@ -390,101 +141,18 @@ function normalizeLimit(limit) {
390
141
  }
391
142
  return Math.min(Math.floor(limit), 200);
392
143
  }
393
- function parseSearchSource(source) {
394
- if (source === "local" || source === "registry" || source === "both")
144
+ export function parseSearchSource(source) {
145
+ if (source === "stash" || source === "registry" || source === "both")
395
146
  return source;
147
+ // Accept "local" as alias for "stash"
148
+ if (source === "local")
149
+ return "stash";
396
150
  if (typeof source === "undefined")
397
- return "local";
398
- throw new UsageError(`Invalid search source: ${String(source)}. Expected one of: local|registry|both`);
151
+ return "stash";
152
+ throw new UsageError(`Invalid value for --source: ${String(source)}. Expected one of: stash|registry|both`);
399
153
  }
400
154
  function mergeSearchHits(localHits, registryHits, limit) {
401
155
  const all = [...localHits, ...registryHits];
402
156
  all.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
403
157
  return all.slice(0, limit);
404
158
  }
405
- /** Map asset types to their primary renderer names. */
406
- const TYPE_TO_RENDERER = {
407
- script: "script-source",
408
- skill: "skill-md",
409
- command: "command-md",
410
- agent: "agent-md",
411
- knowledge: "knowledge-md",
412
- };
413
- function rendererForType(type) {
414
- return getRenderer(TYPE_TO_RENDERER[type]);
415
- }
416
- function buildLocalAction(type, ref) {
417
- switch (type) {
418
- case "script":
419
- return `akm show ${ref} -> execute the run command`;
420
- case "skill":
421
- return `akm show ${ref} -> follow the instructions`;
422
- case "command":
423
- return `akm show ${ref} -> fill placeholders and dispatch`;
424
- case "agent":
425
- return `akm show ${ref} -> dispatch with full prompt`;
426
- case "knowledge":
427
- return `akm show ${ref} -> read reference material`;
428
- }
429
- }
430
- function deriveSize(bytes) {
431
- if (bytes === undefined)
432
- return undefined;
433
- if (bytes < 1024)
434
- return "small";
435
- if (bytes < 10240)
436
- return "medium";
437
- return "large";
438
- }
439
- function readFileSize(filePath) {
440
- try {
441
- return fs.statSync(filePath).size;
442
- }
443
- catch {
444
- return undefined;
445
- }
446
- }
447
- function indexAssets(stashDir, type) {
448
- const assets = [];
449
- const filterType = type === "any" ? undefined : type;
450
- const fileContexts = walkStashFlat(stashDir);
451
- const dirGroups = new Map();
452
- for (const ctx of fileContexts) {
453
- const group = dirGroups.get(ctx.parentDirAbs);
454
- if (group)
455
- group.push(ctx.absPath);
456
- else
457
- dirGroups.set(ctx.parentDirAbs, [ctx.absPath]);
458
- }
459
- for (const [dirPath, files] of dirGroups) {
460
- let stash = loadStashFile(dirPath);
461
- if (stash) {
462
- const coveredFiles = new Set(stash.entries.map((entry) => entry.filename).filter((entry) => !!entry));
463
- const uncoveredFiles = files.filter((file) => !coveredFiles.has(path.basename(file)));
464
- if (uncoveredFiles.length > 0) {
465
- const generated = generateMetadataFlat(stashDir, uncoveredFiles);
466
- if (generated.entries.length > 0) {
467
- stash = { entries: [...stash.entries, ...generated.entries] };
468
- }
469
- }
470
- }
471
- else {
472
- const generated = generateMetadataFlat(stashDir, files);
473
- if (generated.entries.length === 0)
474
- continue;
475
- stash = generated;
476
- }
477
- for (const entry of stash.entries) {
478
- if (filterType && entry.type !== filterType)
479
- continue;
480
- const entryPath = entry.filename ? path.join(dirPath, entry.filename) : files[0] || dirPath;
481
- assets.push({ entry, path: entryPath });
482
- }
483
- }
484
- return assets;
485
- }
486
- function compareAssets(a, b) {
487
- if (a.entry.type !== b.entry.type)
488
- return a.entry.type.localeCompare(b.entry.type);
489
- return a.entry.name.localeCompare(b.entry.name);
490
- }
@@ -2,21 +2,40 @@ import { loadConfig } from "./config";
2
2
  import { NotFoundError, UsageError } from "./errors";
3
3
  import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "./file-context";
4
4
  import { resolveSourcesForOrigin } from "./origin-resolve";
5
+ import { resolveStashProviders } from "./stash-provider-factory";
5
6
  import { parseAssetRef } from "./stash-ref";
6
7
  import { resolveAssetPath } from "./stash-resolve";
7
8
  import { buildEditHint, findSourceForPath, isEditable, resolveStashSources } from "./stash-source";
8
- export async function agentikitShow(input) {
9
+ // Eagerly import stash providers to trigger self-registration
10
+ import "./stash-providers/index";
11
+ /**
12
+ * Unified show: routes to the first stash provider that can handle the ref.
13
+ * viking:// refs are handled by OpenViking provider; everything else by filesystem show.
14
+ */
15
+ export async function agentikitShowUnified(input) {
16
+ const ref = input.ref.trim();
17
+ // Try stash providers first (e.g. OpenViking for viking:// URIs)
18
+ const config = loadConfig();
19
+ const provider = resolveStashProviders(config).find((p) => p.canShow(ref));
20
+ if (provider) {
21
+ return provider.show(ref, input.view);
22
+ }
23
+ // Default: local filesystem show
24
+ return showLocal(input);
25
+ }
26
+ /** @internal Use agentikitShowUnified() for all external callers. */
27
+ export async function showLocal(input) {
9
28
  const parsed = parseAssetRef(input.ref);
10
29
  const displayType = parsed.type;
11
30
  const config = loadConfig();
12
- const allSources = resolveStashSources();
31
+ const allSources = resolveStashSources(input.stashDir);
13
32
  const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
14
33
  const allStashDirs = searchSources.map((s) => s.path);
15
34
  let assetPath;
16
35
  let lastError;
17
36
  for (const dir of allStashDirs) {
18
37
  try {
19
- assetPath = resolveAssetPath(dir, parsed.type, parsed.name);
38
+ assetPath = await resolveAssetPath(dir, parsed.type, parsed.name);
20
39
  break;
21
40
  }
22
41
  catch (err) {
@@ -37,12 +56,12 @@ export async function agentikitShow(input) {
37
56
  throw new UsageError(`Could not determine stash root for asset: ${displayType}:${parsed.name}`);
38
57
  }
39
58
  const fileCtx = buildFileContext(sourceStashDir, assetPath);
40
- const match = runMatchers(fileCtx);
59
+ const match = await runMatchers(fileCtx);
41
60
  if (!match) {
42
61
  throw new UsageError(`Could not display asset "${displayType}:${parsed.name}" — unsupported file type or unrecognized layout`);
43
62
  }
44
63
  match.meta = { ...match.meta, name: parsed.name, view: input.view };
45
- const renderer = getRenderer(match.renderer);
64
+ const renderer = await getRenderer(match.renderer);
46
65
  if (!renderer) {
47
66
  throw new UsageError(`Renderer "${match.renderer}" not found for asset: ${displayType}:${parsed.name}`);
48
67
  }
@@ -17,25 +17,33 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
17
17
  const stashDir = overrideStashDir ?? resolveStashDir();
18
18
  const config = existingConfig ?? loadConfig();
19
19
  const sources = [{ path: stashDir }];
20
- for (const dir of config.searchPaths) {
20
+ const seen = new Set([path.resolve(stashDir)]);
21
+ const addSource = (dir, registryId) => {
22
+ const resolved = path.resolve(dir);
23
+ if (seen.has(resolved))
24
+ return;
25
+ seen.add(resolved);
21
26
  if (isSuspiciousStashRoot(dir)) {
22
27
  warn(`Warning: stash root "${dir}" appears to be a system directory. This may be unintentional.`);
23
28
  }
24
29
  if (isValidDirectory(dir)) {
25
- sources.push({ path: dir });
30
+ sources.push({ path: resolved, ...(registryId ? { registryId } : {}) });
26
31
  }
32
+ };
33
+ // Legacy: searchPaths[]
34
+ for (const dir of config.searchPaths) {
35
+ addSource(dir);
27
36
  }
28
- for (const entry of config.installed ?? []) {
29
- if (isSuspiciousStashRoot(entry.stashRoot)) {
30
- warn(`Warning: stash root "${entry.stashRoot}" appears to be a system directory. This may be unintentional.`);
31
- }
32
- if (isValidDirectory(entry.stashRoot)) {
33
- sources.push({
34
- path: entry.stashRoot,
35
- registryId: entry.id,
36
- });
37
+ // Filesystem entries from stashes[]
38
+ for (const entry of config.stashes ?? []) {
39
+ if (entry.type === "filesystem" && entry.path && entry.enabled !== false) {
40
+ addSource(entry.path, entry.name);
37
41
  }
38
42
  }
43
+ // Installed kits (registry and local)
44
+ for (const entry of config.installed ?? []) {
45
+ addSource(entry.stashRoot, entry.id);
46
+ }
39
47
  return sources;
40
48
  }
41
49
  /**