akm-cli 0.0.0 → 0.0.17

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.
@@ -0,0 +1,55 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isRelevantAssetFile, resolveAssetPathFromName, TYPE_DIRS } from "./asset-spec";
4
+ import { hasErrnoCode, isWithin } from "./common";
5
+ import { NotFoundError, UsageError } from "./errors";
6
+ /**
7
+ * Resolve an asset path from a stash directory, type, and name.
8
+ */
9
+ export function resolveAssetPath(stashDir, type, name) {
10
+ return resolveInTypeDir(stashDir, TYPE_DIRS[type], type, name);
11
+ }
12
+ /**
13
+ * Try to resolve an asset path within a specific type directory.
14
+ */
15
+ function resolveInTypeDir(stashDir, typeDir, type, name) {
16
+ const root = path.join(stashDir, typeDir);
17
+ const target = resolveAssetPathFromName(type, root, name);
18
+ const resolvedRoot = resolveAndValidateTypeRoot(root, type, name);
19
+ const resolvedTarget = path.resolve(target);
20
+ if (!isWithin(resolvedTarget, resolvedRoot)) {
21
+ throw new UsageError("Ref resolves outside the stash root.");
22
+ }
23
+ if (!fs.existsSync(resolvedTarget) || !fs.statSync(resolvedTarget).isFile()) {
24
+ throw new NotFoundError(`Stash asset not found for ref: ${type}:${name}`);
25
+ }
26
+ const realTarget = fs.realpathSync(resolvedTarget);
27
+ if (!isWithin(realTarget, resolvedRoot)) {
28
+ throw new UsageError("Ref resolves outside the stash root.");
29
+ }
30
+ if (!isRelevantAssetFile(type, path.basename(resolvedTarget))) {
31
+ if (type === "script") {
32
+ throw new NotFoundError("Script ref must resolve to a file with a supported script extension. Refer to the akm documentation for the complete list of supported script extensions.");
33
+ }
34
+ throw new NotFoundError(`Stash asset not found for ref: ${type}:${name}`);
35
+ }
36
+ return realTarget;
37
+ }
38
+ function resolveAndValidateTypeRoot(root, type, name) {
39
+ const rootStat = readTypeRootStat(root, type, name);
40
+ if (!rootStat.isDirectory()) {
41
+ throw new NotFoundError(`Stash type root is not a directory for ref: ${type}:${name}`);
42
+ }
43
+ return fs.realpathSync(root);
44
+ }
45
+ function readTypeRootStat(root, type, name) {
46
+ try {
47
+ return fs.statSync(root);
48
+ }
49
+ catch (error) {
50
+ if (hasErrnoCode(error, "ENOENT")) {
51
+ throw new NotFoundError(`Stash type root not found for ref: ${type}:${name}`);
52
+ }
53
+ throw error;
54
+ }
55
+ }
@@ -0,0 +1,490 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { deriveCanonicalAssetName, TYPE_DIRS } from "./asset-spec";
4
+ import { loadConfig } from "./config";
5
+ import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, openDatabase, searchFts, searchVec, } from "./db";
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
+ 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";
16
+ const DEFAULT_LIMIT = 20;
17
+ export async function agentikitSearch(input) {
18
+ const t0 = Date.now();
19
+ const query = input.query.trim();
20
+ const normalizedQuery = query.toLowerCase();
21
+ const searchType = input.type ?? "any";
22
+ const limit = normalizeLimit(input.limit);
23
+ const source = parseSearchSource(input.source);
24
+ const config = loadConfig();
25
+ const sources = resolveStashSources(undefined, config);
26
+ if (sources.length === 0) {
27
+ return {
28
+ schemaVersion: 1,
29
+ stashDir: "",
30
+ source,
31
+ hits: [],
32
+ warnings: ["No stash sources configured. Run `akm init` first."],
33
+ timing: { totalMs: Date.now() - t0 },
34
+ };
35
+ }
36
+ const stashDir = sources[0].path;
37
+ const localResult = source === "registry"
38
+ ? undefined
39
+ : await searchLocal({
40
+ query: normalizedQuery,
41
+ searchType,
42
+ limit,
43
+ stashDir,
44
+ sources,
45
+ config,
46
+ });
47
+ const registryResult = source === "local" ? undefined : await searchRegistry(query, { limit, registries: config.registries });
48
+ if (source === "local") {
49
+ return {
50
+ schemaVersion: 1,
51
+ stashDir,
52
+ source,
53
+ hits: localResult?.hits ?? [],
54
+ tip: localResult?.tip,
55
+ warnings: localResult?.warnings,
56
+ timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
57
+ };
58
+ }
59
+ const registryHits = (registryResult?.hits ?? []).map((hit) => {
60
+ const installRef = hit.source === "npm" ? `npm:${hit.ref}` : hit.source === "git" ? `git+${hit.ref}` : `github:${hit.ref}`;
61
+ return {
62
+ type: "registry",
63
+ name: hit.title,
64
+ id: hit.id,
65
+ description: hit.description,
66
+ action: `akm add ${installRef} -> then search again`,
67
+ score: hit.score,
68
+ curated: hit.curated,
69
+ registryName: hit.registryName,
70
+ };
71
+ });
72
+ if (source === "registry") {
73
+ const hits = registryHits.slice(0, limit);
74
+ return {
75
+ schemaVersion: 1,
76
+ stashDir,
77
+ source,
78
+ hits,
79
+ tip: hits.length === 0 ? "No matching registry entries were found." : undefined,
80
+ warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
81
+ timing: { totalMs: Date.now() - t0 },
82
+ };
83
+ }
84
+ const mergedHits = mergeSearchHits(localResult?.hits ?? [], registryHits, limit);
85
+ const warnings = [...(localResult?.warnings ?? []), ...(registryResult?.warnings ?? [])];
86
+ return {
87
+ schemaVersion: 1,
88
+ stashDir,
89
+ source,
90
+ hits: mergedHits,
91
+ tip: mergedHits.length === 0 ? "No matching stash assets or registry entries were found." : undefined,
92
+ warnings: warnings.length ? warnings : undefined,
93
+ timing: { totalMs: Date.now() - t0 },
94
+ };
95
+ }
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;
386
+ }
387
+ function normalizeLimit(limit) {
388
+ if (typeof limit !== "number" || Number.isNaN(limit) || limit <= 0) {
389
+ return DEFAULT_LIMIT;
390
+ }
391
+ return Math.min(Math.floor(limit), 200);
392
+ }
393
+ function parseSearchSource(source) {
394
+ if (source === "local" || source === "registry" || source === "both")
395
+ return source;
396
+ if (typeof source === "undefined")
397
+ return "local";
398
+ throw new UsageError(`Invalid search source: ${String(source)}. Expected one of: local|registry|both`);
399
+ }
400
+ function mergeSearchHits(localHits, registryHits, limit) {
401
+ const all = [...localHits, ...registryHits];
402
+ all.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
403
+ return all.slice(0, limit);
404
+ }
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
+ }
@@ -0,0 +1,58 @@
1
+ import { loadConfig } from "./config";
2
+ import { NotFoundError, UsageError } from "./errors";
3
+ import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "./file-context";
4
+ import { resolveSourcesForOrigin } from "./origin-resolve";
5
+ import { parseAssetRef } from "./stash-ref";
6
+ import { resolveAssetPath } from "./stash-resolve";
7
+ import { buildEditHint, findSourceForPath, isEditable, resolveStashSources } from "./stash-source";
8
+ export async function agentikitShow(input) {
9
+ const parsed = parseAssetRef(input.ref);
10
+ const displayType = parsed.type;
11
+ const config = loadConfig();
12
+ const allSources = resolveStashSources();
13
+ const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
14
+ const allStashDirs = searchSources.map((s) => s.path);
15
+ let assetPath;
16
+ let lastError;
17
+ for (const dir of allStashDirs) {
18
+ try {
19
+ assetPath = resolveAssetPath(dir, parsed.type, parsed.name);
20
+ break;
21
+ }
22
+ catch (err) {
23
+ lastError = err instanceof Error ? err : new Error(String(err));
24
+ }
25
+ }
26
+ if (!assetPath && parsed.origin && searchSources.length === 0) {
27
+ const installCmd = `akm add ${parsed.origin}`;
28
+ throw new NotFoundError(`Stash asset not found for ref: ${displayType}:${parsed.name}. ` +
29
+ `Kit "${parsed.origin}" is not installed. Run: ${installCmd}`);
30
+ }
31
+ if (!assetPath) {
32
+ throw lastError ?? new NotFoundError(`Stash asset not found for ref: ${displayType}:${parsed.name}`);
33
+ }
34
+ const source = findSourceForPath(assetPath, allSources);
35
+ const sourceStashDir = source?.path ?? allStashDirs[0];
36
+ if (!sourceStashDir) {
37
+ throw new UsageError(`Could not determine stash root for asset: ${displayType}:${parsed.name}`);
38
+ }
39
+ const fileCtx = buildFileContext(sourceStashDir, assetPath);
40
+ const match = runMatchers(fileCtx);
41
+ if (!match) {
42
+ throw new UsageError(`Could not display asset "${displayType}:${parsed.name}" — unsupported file type or unrecognized layout`);
43
+ }
44
+ match.meta = { ...match.meta, name: parsed.name, view: input.view };
45
+ const renderer = getRenderer(match.renderer);
46
+ if (!renderer) {
47
+ throw new UsageError(`Renderer "${match.renderer}" not found for asset: ${displayType}:${parsed.name}`);
48
+ }
49
+ const renderCtx = buildRenderContext(fileCtx, match, allStashDirs);
50
+ const response = renderer.buildShowResponse(renderCtx);
51
+ const editable = isEditable(assetPath, config);
52
+ return {
53
+ ...response,
54
+ origin: source?.registryId ?? null,
55
+ editable,
56
+ ...(!editable ? { editHint: buildEditHint(assetPath, parsed.type, parsed.name, source?.registryId) } : {}),
57
+ };
58
+ }