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.
- package/LICENSE +385 -0
- package/README.md +249 -6
- package/dist/asset-spec.js +70 -0
- package/dist/cli.js +934 -0
- package/dist/common.js +192 -0
- package/dist/config-cli.js +233 -0
- package/dist/config.js +338 -0
- package/dist/db.js +371 -0
- package/dist/embedder.js +150 -0
- package/dist/errors.js +28 -0
- package/dist/file-context.js +162 -0
- package/dist/frontmatter.js +86 -0
- package/dist/github.js +17 -0
- package/dist/indexer.js +311 -0
- package/dist/init.js +43 -0
- package/dist/llm.js +87 -0
- package/dist/lockfile.js +60 -0
- package/dist/markdown.js +77 -0
- package/dist/matchers.js +159 -0
- package/dist/metadata.js +408 -0
- package/dist/origin-resolve.js +54 -0
- package/dist/paths.js +92 -0
- package/dist/registry-install.js +459 -0
- package/dist/registry-resolve.js +486 -0
- package/dist/registry-search.js +365 -0
- package/dist/registry-types.js +1 -0
- package/dist/renderers.js +386 -0
- package/dist/ripgrep-install.js +155 -0
- package/dist/ripgrep-resolve.js +78 -0
- package/dist/ripgrep.js +2 -0
- package/dist/self-update.js +226 -0
- package/dist/stash-add.js +71 -0
- package/dist/stash-clone.js +115 -0
- package/dist/stash-ref.js +73 -0
- package/dist/stash-registry.js +206 -0
- package/dist/stash-resolve.js +55 -0
- package/dist/stash-search.js +490 -0
- package/dist/stash-show.js +58 -0
- package/dist/stash-source.js +130 -0
- package/dist/stash-types.js +1 -0
- package/dist/walker.js +163 -0
- package/dist/warn.js +20 -0
- package/package.json +53 -7
- package/index.js +0 -4
|
@@ -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
|
+
}
|