akm-cli 0.0.16 → 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/README.md +58 -107
- package/dist/asset-spec.js +9 -9
- package/dist/cli.js +140 -118
- package/dist/common.js +0 -7
- package/dist/config-cli.js +38 -0
- package/dist/config.js +40 -26
- package/dist/file-context.js +3 -28
- package/dist/indexer.js +0 -30
- package/dist/llm.js +1 -1
- package/dist/matchers.js +4 -15
- package/dist/metadata.js +3 -3
- package/dist/registry-install.js +5 -5
- package/dist/registry-search.js +140 -30
- package/dist/renderers.js +1 -1
- package/dist/stash-add.js +2 -2
- package/dist/stash-ref.js +7 -9
- package/dist/stash-registry.js +6 -6
- package/dist/stash-resolve.js +7 -44
- package/dist/stash-search.js +8 -12
- package/dist/stash-show.js +1 -2
- package/dist/stash-source.js +6 -7
- package/dist/walker.js +1 -1
- package/package.json +1 -1
package/dist/file-context.js
CHANGED
|
@@ -22,8 +22,8 @@ export function buildFileContext(stashRoot, absPath) {
|
|
|
22
22
|
const parentDirAbs = path.dirname(absPath);
|
|
23
23
|
const parentDir = path.basename(parentDirAbs);
|
|
24
24
|
// Compute ancestor directory segments from the POSIX relPath's directory portion.
|
|
25
|
-
// For "
|
|
26
|
-
// which splits into ["
|
|
25
|
+
// For "scripts/azure/deploy/run.sh" the dir portion is "scripts/azure/deploy"
|
|
26
|
+
// which splits into ["scripts", "azure", "deploy"].
|
|
27
27
|
const relDir = toPosix(path.dirname(relPath));
|
|
28
28
|
const ancestorDirs = relDir === "." ? [] : relDir.split("/").filter((seg) => seg.length > 0);
|
|
29
29
|
// Lazy caches
|
|
@@ -69,41 +69,16 @@ const matchers = [];
|
|
|
69
69
|
/** Renderer lookup by name. */
|
|
70
70
|
const renderers = new Map();
|
|
71
71
|
let builtinsInitialized = false;
|
|
72
|
-
/** Pluggable initializer set via `setBuiltinRegistrar`. */
|
|
73
|
-
let builtinRegistrar = null;
|
|
74
|
-
/**
|
|
75
|
-
* Set the function that registers built-in matchers and renderers.
|
|
76
|
-
*
|
|
77
|
-
* This breaks the static import cycle: `file-context.ts` does not need to
|
|
78
|
-
* import `matchers.ts` or `renderers.ts` at the top level. Instead, a
|
|
79
|
-
* one-time call from outside (e.g. the test file or CLI entry) provides the
|
|
80
|
-
* registration callback.
|
|
81
|
-
*
|
|
82
|
-
* If no registrar is set by the time `ensureBuiltinsRegistered` runs, it
|
|
83
|
-
* falls back to a dynamic `require()` for backward compatibility.
|
|
84
|
-
*/
|
|
85
|
-
export function setBuiltinRegistrar(fn) {
|
|
86
|
-
builtinRegistrar = fn;
|
|
87
|
-
}
|
|
88
72
|
/**
|
|
89
73
|
* Ensure that built-in matchers and renderers are registered.
|
|
90
74
|
* Called lazily on first use of runMatchers/getRenderer.
|
|
91
|
-
*
|
|
92
|
-
* Uses the registrar set via `setBuiltinRegistrar`, or falls back to
|
|
93
|
-
* a direct import of the registration modules. The dynamic import
|
|
94
|
-
* avoids a static circular dependency between file-context, renderers,
|
|
95
|
-
* and asset-spec.
|
|
96
75
|
*/
|
|
97
76
|
function ensureBuiltinsRegistered() {
|
|
98
77
|
if (builtinsInitialized)
|
|
99
78
|
return;
|
|
100
79
|
builtinsInitialized = true;
|
|
101
|
-
if (builtinRegistrar) {
|
|
102
|
-
builtinRegistrar();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
80
|
// Lazy inline require avoids a top-level static import cycle.
|
|
106
|
-
// These are only evaluated once
|
|
81
|
+
// These are only evaluated once.
|
|
107
82
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
108
83
|
const { registerBuiltinMatchers } = require("./matchers");
|
|
109
84
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
package/dist/indexer.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { deriveCanonicalAssetName, TYPE_DIRS } from "./asset-spec";
|
|
4
3
|
import { resolveStashDir } from "./common";
|
|
5
4
|
import { closeDatabase, DB_VERSION, deleteEntriesByDir, getEntriesByDir, getEntryCount, getMeta, isVecAvailable, openDatabase, rebuildFts, setMeta, upsertEmbedding, upsertEntry, warnIfVecMissing, } from "./db";
|
|
6
|
-
import { buildFileContext } from "./file-context";
|
|
7
5
|
import { generateMetadataFlat, loadStashFile } from "./metadata";
|
|
8
6
|
import { getDbPath } from "./paths";
|
|
9
7
|
import { walkStashFlat } from "./walker";
|
|
@@ -128,10 +126,6 @@ function indexEntries(db, allStashDirs, _stashDir, isIncremental, builtAtMs) {
|
|
|
128
126
|
// Try loading existing .stash.json (user metadata overrides)
|
|
129
127
|
let stash = loadStashFile(dirPath);
|
|
130
128
|
if (stash) {
|
|
131
|
-
// Re-derive canonical names for generated entries using the matcher system.
|
|
132
|
-
// This fixes legacy .stash.json files that may have stale names (e.g. "SKILL"
|
|
133
|
-
// instead of the directory-based canonical name "code-review").
|
|
134
|
-
fixGeneratedEntryNames(stash, files, currentStashDir);
|
|
135
129
|
// Check for files on disk that aren't covered by existing .stash.json entries.
|
|
136
130
|
const coveredFiles = new Set(stash.entries.map((e) => (e.filename ? path.basename(e.filename) : "")).filter((e) => !!e));
|
|
137
131
|
const uncoveredFiles = files.filter((f) => !coveredFiles.has(path.basename(f)));
|
|
@@ -226,30 +220,6 @@ function attachFileSize(entry, entryPath) {
|
|
|
226
220
|
}
|
|
227
221
|
}
|
|
228
222
|
/** Set of all known type directory names */
|
|
229
|
-
const TYPE_DIR_NAMES = new Set(Object.values(TYPE_DIRS));
|
|
230
|
-
/**
|
|
231
|
-
* Re-derive canonical names for generated skill .stash.json entries.
|
|
232
|
-
*
|
|
233
|
-
* Legacy .stash.json files for skills may have name "SKILL" (derived from the
|
|
234
|
-
* filename SKILL.md) instead of the canonical directory name (e.g. "code-review").
|
|
235
|
-
* This fixes those names using the matcher system.
|
|
236
|
-
*/
|
|
237
|
-
function fixGeneratedEntryNames(stash, files, stashRoot) {
|
|
238
|
-
const fileByBaseName = new Map(files.map((f) => [path.basename(f), f]));
|
|
239
|
-
for (const entry of stash.entries) {
|
|
240
|
-
if (entry.quality !== "generated" || entry.type !== "skill")
|
|
241
|
-
continue;
|
|
242
|
-
const filePath = entry.filename ? fileByBaseName.get(path.basename(entry.filename)) : undefined;
|
|
243
|
-
if (!filePath)
|
|
244
|
-
continue;
|
|
245
|
-
const firstAncestor = buildFileContext(stashRoot, filePath).ancestorDirs[0];
|
|
246
|
-
const effectiveRoot = firstAncestor && TYPE_DIR_NAMES.has(firstAncestor) ? path.join(stashRoot, firstAncestor) : stashRoot;
|
|
247
|
-
const canonicalName = deriveCanonicalAssetName("skill", effectiveRoot, filePath);
|
|
248
|
-
if (canonicalName && canonicalName !== entry.name) {
|
|
249
|
-
entry.name = canonicalName;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
223
|
function isDirStale(dirPath, currentFiles, previousEntries, builtAtMs) {
|
|
254
224
|
// Check if file set changed (additions or deletions)
|
|
255
225
|
const prevFileNames = new Set(previousEntries.map((ie) => ie.entry.filename).filter((e) => !!e));
|
package/dist/llm.js
CHANGED
|
@@ -22,7 +22,7 @@ async function chatCompletion(config, messages) {
|
|
|
22
22
|
return json.choices?.[0]?.message?.content?.trim() ?? "";
|
|
23
23
|
}
|
|
24
24
|
// ── Metadata Enhancement ────────────────────────────────────────────────────
|
|
25
|
-
const SYSTEM_PROMPT = `You are a metadata generator for a developer
|
|
25
|
+
const SYSTEM_PROMPT = `You are a metadata generator for a developer asset registry. Given a script/skill/command/agent entry, generate improved metadata. Respond with ONLY valid JSON, no markdown fencing.`;
|
|
26
26
|
/**
|
|
27
27
|
* Use an LLM to enhance a stash entry's metadata: improve description,
|
|
28
28
|
* generate searchHints, and suggest tags.
|
package/dist/matchers.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* at specificity 5 when no signals are found. Command signals (`agent`
|
|
17
17
|
* frontmatter, `$ARGUMENTS`/`$1`-`$3` placeholders) return 18.
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { SCRIPT_EXTENSIONS } from "./asset-spec";
|
|
20
20
|
import { registerMatcher } from "./file-context";
|
|
21
21
|
// ── extensionMatcher (specificity: 3) ────────────────────────────────────────
|
|
22
22
|
/**
|
|
@@ -36,7 +36,7 @@ export function extensionMatcher(ctx) {
|
|
|
36
36
|
return { type: "skill", specificity: 25, renderer: "skill-md" };
|
|
37
37
|
}
|
|
38
38
|
// Known script extensions (excluding .md, handled by smartMdMatcher)
|
|
39
|
-
if (
|
|
39
|
+
if (SCRIPT_EXTENSIONS.has(ctx.ext)) {
|
|
40
40
|
return { type: "script", specificity: 3, renderer: "script-source" };
|
|
41
41
|
}
|
|
42
42
|
return null;
|
|
@@ -45,19 +45,13 @@ export function extensionMatcher(ctx) {
|
|
|
45
45
|
/**
|
|
46
46
|
* Directory-based matcher that boosts specificity when the first ancestor
|
|
47
47
|
* directory segment from the stash root matches a known type name.
|
|
48
|
-
*
|
|
49
|
-
* Accepts ALL known script extensions in both `tools/` and `scripts/`
|
|
50
|
-
* directories -- the distinction is purely organizational.
|
|
51
48
|
*/
|
|
52
49
|
export function directoryMatcher(ctx) {
|
|
53
50
|
const topDir = ctx.ancestorDirs[0];
|
|
54
51
|
if (!topDir)
|
|
55
52
|
return null;
|
|
56
53
|
const ext = ctx.ext;
|
|
57
|
-
if (topDir === "
|
|
58
|
-
return { type: "tool", specificity: 10, renderer: "script-source" };
|
|
59
|
-
}
|
|
60
|
-
if (topDir === "scripts" && SCRIPT_EXTENSIONS_BROAD.has(ext)) {
|
|
54
|
+
if (topDir === "scripts" && SCRIPT_EXTENSIONS.has(ext)) {
|
|
61
55
|
return { type: "script", specificity: 10, renderer: "script-source" };
|
|
62
56
|
}
|
|
63
57
|
if (topDir === "skills" && ctx.fileName === "SKILL.md") {
|
|
@@ -80,15 +74,10 @@ export function directoryMatcher(ctx) {
|
|
|
80
74
|
* the ancestor-based directory matcher because the file might be nested
|
|
81
75
|
* several levels deep, yet its immediate parent can still carry strong
|
|
82
76
|
* naming conventions (e.g. `my-project/agents/planning.md`).
|
|
83
|
-
*
|
|
84
|
-
* Accepts ALL known script extensions in both `tools/` and `scripts/`.
|
|
85
77
|
*/
|
|
86
78
|
export function parentDirHintMatcher(ctx) {
|
|
87
79
|
const { parentDir, ext, fileName } = ctx;
|
|
88
|
-
if (parentDir === "
|
|
89
|
-
return { type: "tool", specificity: 15, renderer: "script-source" };
|
|
90
|
-
}
|
|
91
|
-
if (parentDir === "scripts" && SCRIPT_EXTENSIONS_BROAD.has(ext)) {
|
|
80
|
+
if (parentDir === "scripts" && SCRIPT_EXTENSIONS.has(ext)) {
|
|
92
81
|
return { type: "script", specificity: 15, renderer: "script-source" };
|
|
93
82
|
}
|
|
94
83
|
if (parentDir === "skills" && fileName === "SKILL.md") {
|
package/dist/metadata.js
CHANGED
|
@@ -176,7 +176,7 @@ export function generateMetadata(dirPath, assetType, files, typeRoot = dirPath)
|
|
|
176
176
|
entry.confidence = 0.9;
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
-
// Priority 3: Type-specific metadata extraction (e.g. TOC for knowledge, comments for
|
|
179
|
+
// Priority 3: Type-specific metadata extraction (e.g. TOC for knowledge, comments for scripts)
|
|
180
180
|
const fileCtx = buildFileContext(typeRoot, file);
|
|
181
181
|
const match = runMatchers(fileCtx);
|
|
182
182
|
if (match) {
|
|
@@ -204,7 +204,7 @@ export function generateMetadata(dirPath, assetType, files, typeRoot = dirPath)
|
|
|
204
204
|
}
|
|
205
205
|
return { entries };
|
|
206
206
|
}
|
|
207
|
-
/** Set of all known type directory names (e.g. "
|
|
207
|
+
/** Set of all known type directory names (e.g. "scripts", "skills", "agents") */
|
|
208
208
|
const TYPE_DIR_NAMES = new Set(Object.values(TYPE_DIRS));
|
|
209
209
|
/**
|
|
210
210
|
* Generate metadata for files using the matcher system instead of a fixed asset type.
|
|
@@ -226,7 +226,7 @@ export function generateMetadataFlat(stashRoot, files) {
|
|
|
226
226
|
continue;
|
|
227
227
|
// If the file lives under a known type directory, use that as the root
|
|
228
228
|
// for canonical naming so names don't include the type prefix.
|
|
229
|
-
// e.g.
|
|
229
|
+
// e.g. scripts/deploy.sh → "deploy.sh" not "scripts/deploy.sh"
|
|
230
230
|
const firstAncestor = ctx.ancestorDirs[0];
|
|
231
231
|
const effectiveRoot = firstAncestor && TYPE_DIR_NAMES.has(firstAncestor) ? path.join(stashRoot, firstAncestor) : stashRoot;
|
|
232
232
|
const ext = path.extname(file).toLowerCase();
|
package/dist/registry-install.js
CHANGED
|
@@ -154,23 +154,23 @@ async function installGitRegistryRef(parsed, options) {
|
|
|
154
154
|
}
|
|
155
155
|
export function upsertInstalledRegistryEntry(entry) {
|
|
156
156
|
const current = loadConfig();
|
|
157
|
-
const currentInstalled = current.
|
|
157
|
+
const currentInstalled = current.installed ?? [];
|
|
158
158
|
const withoutExisting = currentInstalled.filter((item) => item.id !== entry.id);
|
|
159
159
|
const nextInstalled = [...withoutExisting, normalizeInstalledEntry(entry)];
|
|
160
160
|
const nextConfig = {
|
|
161
161
|
...current,
|
|
162
|
-
|
|
162
|
+
installed: nextInstalled,
|
|
163
163
|
};
|
|
164
164
|
saveConfig(nextConfig);
|
|
165
165
|
return nextConfig;
|
|
166
166
|
}
|
|
167
167
|
export function removeInstalledRegistryEntry(id) {
|
|
168
168
|
const current = loadConfig();
|
|
169
|
-
const currentInstalled = current.
|
|
169
|
+
const currentInstalled = current.installed ?? [];
|
|
170
170
|
const nextInstalled = currentInstalled.filter((item) => item.id !== id);
|
|
171
171
|
const nextConfig = {
|
|
172
172
|
...current,
|
|
173
|
-
|
|
173
|
+
installed: nextInstalled.length > 0 ? nextInstalled : undefined,
|
|
174
174
|
};
|
|
175
175
|
saveConfig(nextConfig);
|
|
176
176
|
return nextConfig;
|
|
@@ -405,7 +405,7 @@ function countStashDirs(dirPath) {
|
|
|
405
405
|
/**
|
|
406
406
|
* BFS to find the shallowest directory that looks like a stash root.
|
|
407
407
|
* Checks for both `.stash` directories and well-known type directories
|
|
408
|
-
* (
|
|
408
|
+
* (scripts/, skills/, etc.), so nested layouts like `project/my-kit/scripts/`
|
|
409
409
|
* are discovered even without a `.stash` marker.
|
|
410
410
|
*
|
|
411
411
|
* Skips `root` itself since the caller already checked it via `hasStashDirs`.
|
package/dist/registry-search.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fetchWithRetry } from "./common";
|
|
4
|
+
import { DEFAULT_CONFIG, loadConfig } from "./config";
|
|
4
5
|
import { getRegistryIndexCacheDir } from "./paths";
|
|
5
6
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
6
|
-
/** Default registry index URL. Override via config or AKM_REGISTRY_URL env var. */
|
|
7
|
-
const DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/itlackey/akm-registry/main/index.json";
|
|
8
7
|
/** Cache TTL in milliseconds (1 hour). */
|
|
9
8
|
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
10
9
|
/** Maximum age before cache is considered stale but still usable as fallback (7 days). */
|
|
@@ -16,28 +15,59 @@ export async function searchRegistry(query, options) {
|
|
|
16
15
|
return { query: "", hits: [], warnings: [] };
|
|
17
16
|
}
|
|
18
17
|
const limit = clampLimit(options?.limit);
|
|
19
|
-
const
|
|
18
|
+
const entries = (options?.registries ?? resolveRegistries()).filter((r) => r.enabled !== false);
|
|
20
19
|
const warnings = [];
|
|
21
20
|
// Load index from all configured registries, merge kits
|
|
22
21
|
const allKits = [];
|
|
23
|
-
for (const
|
|
22
|
+
for (const entry of entries) {
|
|
24
23
|
try {
|
|
25
|
-
const index = await loadIndex(
|
|
24
|
+
const index = await loadIndex(entry);
|
|
26
25
|
if (index) {
|
|
27
|
-
|
|
26
|
+
const regName = entry.name;
|
|
27
|
+
for (const kit of index.kits) {
|
|
28
|
+
allKits.push({ kit, registryName: regName });
|
|
29
|
+
}
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
catch (err) {
|
|
31
|
-
|
|
33
|
+
const label = entry.name ? `${entry.name} (${entry.url})` : entry.url;
|
|
34
|
+
warnings.push(`Registry ${label}: ${toErrorMessage(err)}`);
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
// Score and rank
|
|
35
38
|
const hits = scoreKits(allKits, trimmed, limit);
|
|
36
|
-
|
|
39
|
+
// When includeAssets is enabled, also search asset-level metadata
|
|
40
|
+
let assetHits = [];
|
|
41
|
+
if (options?.includeAssets) {
|
|
42
|
+
assetHits = scoreAssets(allKits, trimmed, limit);
|
|
43
|
+
}
|
|
44
|
+
return { query: trimmed, hits, warnings, assetHits: assetHits.length > 0 ? assetHits : undefined };
|
|
45
|
+
}
|
|
46
|
+
// ── Registry resolution ─────────────────────────────────────────────────────
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the list of enabled registries.
|
|
49
|
+
*
|
|
50
|
+
* Priority:
|
|
51
|
+
* 1. AKM_REGISTRY_URL env var (CI override, comma-separated)
|
|
52
|
+
* 2. config.registries (filtered by enabled !== false)
|
|
53
|
+
* 3. Default registries from DEFAULT_CONFIG
|
|
54
|
+
*/
|
|
55
|
+
export function resolveRegistries(configRegistries) {
|
|
56
|
+
// Allow env var override (comma-separated URLs) — CI escape hatch
|
|
57
|
+
const envUrls = process.env.AKM_REGISTRY_URL?.trim();
|
|
58
|
+
if (envUrls) {
|
|
59
|
+
return envUrls
|
|
60
|
+
.split(",")
|
|
61
|
+
.map((u) => u.trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((url) => ({ url }));
|
|
64
|
+
}
|
|
65
|
+
const registries = configRegistries ?? loadConfig().registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
66
|
+
return registries.filter((r) => r.enabled !== false);
|
|
37
67
|
}
|
|
38
68
|
// ── Index loading with cache ────────────────────────────────────────────────
|
|
39
|
-
async function loadIndex(
|
|
40
|
-
const cachePath = indexCachePath(url);
|
|
69
|
+
async function loadIndex(entry) {
|
|
70
|
+
const cachePath = indexCachePath(entry.url);
|
|
41
71
|
const cached = readCachedIndex(cachePath);
|
|
42
72
|
// Fresh cache: return immediately
|
|
43
73
|
if (cached && !isCacheExpired(cached.mtime)) {
|
|
@@ -45,7 +75,7 @@ async function loadIndex(url) {
|
|
|
45
75
|
}
|
|
46
76
|
// Try to fetch fresh index
|
|
47
77
|
try {
|
|
48
|
-
const response = await fetchWithRetry(url, undefined, { timeout: 10_000 });
|
|
78
|
+
const response = await fetchWithRetry(entry.url, undefined, { timeout: 10_000 });
|
|
49
79
|
if (!response.ok) {
|
|
50
80
|
throw new Error(`HTTP ${response.status}`);
|
|
51
81
|
}
|
|
@@ -110,7 +140,7 @@ function parseRegistryIndex(data) {
|
|
|
110
140
|
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
111
141
|
return null;
|
|
112
142
|
const obj = data;
|
|
113
|
-
if (typeof obj.version !== "number" || obj.version !== 1)
|
|
143
|
+
if (typeof obj.version !== "number" || (obj.version !== 1 && obj.version !== 2))
|
|
114
144
|
return null;
|
|
115
145
|
if (typeof obj.updatedAt !== "string")
|
|
116
146
|
return null;
|
|
@@ -120,7 +150,7 @@ function parseRegistryIndex(data) {
|
|
|
120
150
|
const kit = parseKitEntry(raw);
|
|
121
151
|
return kit ? [kit] : [];
|
|
122
152
|
});
|
|
123
|
-
return { version:
|
|
153
|
+
return { version: obj.version, updatedAt: obj.updatedAt, kits };
|
|
124
154
|
}
|
|
125
155
|
function parseKitEntry(raw) {
|
|
126
156
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
@@ -141,6 +171,7 @@ function parseKitEntry(raw) {
|
|
|
141
171
|
homepage: asString(obj.homepage),
|
|
142
172
|
tags: asStringArray(obj.tags),
|
|
143
173
|
assetTypes: asStringArray(obj.assetTypes),
|
|
174
|
+
assets: parseAssets(obj.assets),
|
|
144
175
|
author: asString(obj.author),
|
|
145
176
|
license: asString(obj.license),
|
|
146
177
|
latestVersion: asString(obj.latestVersion),
|
|
@@ -151,14 +182,14 @@ function parseKitEntry(raw) {
|
|
|
151
182
|
function scoreKits(kits, query, limit) {
|
|
152
183
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
153
184
|
const scored = [];
|
|
154
|
-
for (const kit of kits) {
|
|
185
|
+
for (const { kit, registryName } of kits) {
|
|
155
186
|
const score = scoreKit(kit, tokens);
|
|
156
187
|
if (score > 0) {
|
|
157
|
-
scored.push({ kit, score });
|
|
188
|
+
scored.push({ kit, registryName, score });
|
|
158
189
|
}
|
|
159
190
|
}
|
|
160
191
|
scored.sort((a, b) => b.score - a.score);
|
|
161
|
-
return scored.slice(0, limit).map(({ kit, score }) => toSearchHit(kit, score));
|
|
192
|
+
return scored.slice(0, limit).map(({ kit, registryName, score }) => toSearchHit(kit, score, registryName));
|
|
162
193
|
}
|
|
163
194
|
function scoreKit(kit, tokens) {
|
|
164
195
|
let score = 0;
|
|
@@ -192,7 +223,7 @@ function scoreKit(kit, tokens) {
|
|
|
192
223
|
// Normalize by token count so multi-word queries don't inflate scores
|
|
193
224
|
return tokens.length > 0 ? score / tokens.length : 0;
|
|
194
225
|
}
|
|
195
|
-
function toSearchHit(kit, score) {
|
|
226
|
+
function toSearchHit(kit, score, registryName) {
|
|
196
227
|
const metadata = {};
|
|
197
228
|
if (kit.latestVersion)
|
|
198
229
|
metadata.version = kit.latestVersion;
|
|
@@ -212,23 +243,102 @@ function toSearchHit(kit, score) {
|
|
|
212
243
|
score: Math.round(score * 1000) / 1000,
|
|
213
244
|
metadata,
|
|
214
245
|
curated: kit.curated,
|
|
246
|
+
registryName,
|
|
215
247
|
};
|
|
216
248
|
}
|
|
217
|
-
// ──
|
|
218
|
-
function
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
249
|
+
// ── Asset parsing ────────────────────────────────────────────────────────────
|
|
250
|
+
function parseAssets(raw) {
|
|
251
|
+
if (!Array.isArray(raw))
|
|
252
|
+
return undefined;
|
|
253
|
+
const parsed = raw.flatMap((item) => {
|
|
254
|
+
const entry = parseAssetEntry(item);
|
|
255
|
+
return entry ? [entry] : [];
|
|
256
|
+
});
|
|
257
|
+
return parsed.length > 0 ? parsed : undefined;
|
|
258
|
+
}
|
|
259
|
+
function parseAssetEntry(raw) {
|
|
260
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
261
|
+
return null;
|
|
262
|
+
const obj = raw;
|
|
263
|
+
const type = asString(obj.type);
|
|
264
|
+
const name = asString(obj.name);
|
|
265
|
+
if (!type || !name)
|
|
266
|
+
return null;
|
|
267
|
+
return {
|
|
268
|
+
type,
|
|
269
|
+
name,
|
|
270
|
+
description: asString(obj.description),
|
|
271
|
+
tags: asStringArray(obj.tags),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// ── Asset-level scoring ──────────────────────────────────────────────────────
|
|
275
|
+
function scoreAssets(kits, query, limit) {
|
|
276
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
277
|
+
if (tokens.length === 0)
|
|
278
|
+
return [];
|
|
279
|
+
const scored = [];
|
|
280
|
+
for (const { kit, registryName } of kits) {
|
|
281
|
+
if (!kit.assets || kit.assets.length === 0)
|
|
282
|
+
continue;
|
|
283
|
+
const installRef = kit.source === "npm"
|
|
284
|
+
? `npm:${kit.ref}`
|
|
285
|
+
: kit.source === "git"
|
|
286
|
+
? `git+${kit.ref}`
|
|
287
|
+
: kit.source === "local"
|
|
288
|
+
? kit.ref
|
|
289
|
+
: `github:${kit.ref}`;
|
|
290
|
+
for (const asset of kit.assets) {
|
|
291
|
+
const score = scoreAsset(asset, tokens);
|
|
292
|
+
if (score > 0) {
|
|
293
|
+
scored.push({
|
|
294
|
+
hit: {
|
|
295
|
+
type: "registry-asset",
|
|
296
|
+
assetType: asset.type,
|
|
297
|
+
assetName: asset.name,
|
|
298
|
+
description: asset.description,
|
|
299
|
+
kit: { id: kit.id, name: kit.name },
|
|
300
|
+
registryName,
|
|
301
|
+
action: `akm add ${installRef}`,
|
|
302
|
+
score: Math.round(score * 1000) / 1000,
|
|
303
|
+
},
|
|
304
|
+
score,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
222
308
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
309
|
+
scored.sort((a, b) => b.score - a.score);
|
|
310
|
+
return scored.slice(0, limit).map(({ hit }) => hit);
|
|
311
|
+
}
|
|
312
|
+
function scoreAsset(asset, tokens) {
|
|
313
|
+
let score = 0;
|
|
314
|
+
const nameLower = asset.name.toLowerCase();
|
|
315
|
+
const descLower = (asset.description ?? "").toLowerCase();
|
|
316
|
+
const tagsLower = (asset.tags ?? []).map((t) => t.toLowerCase());
|
|
317
|
+
const typeLower = asset.type.toLowerCase();
|
|
318
|
+
for (const token of tokens) {
|
|
319
|
+
if (nameLower === token) {
|
|
320
|
+
score += 1.0;
|
|
321
|
+
}
|
|
322
|
+
else if (nameLower.includes(token)) {
|
|
323
|
+
score += 0.6;
|
|
324
|
+
}
|
|
325
|
+
if (typeLower === token) {
|
|
326
|
+
score += 0.4;
|
|
327
|
+
}
|
|
328
|
+
else if (typeLower.includes(token)) {
|
|
329
|
+
score += 0.2;
|
|
330
|
+
}
|
|
331
|
+
if (tagsLower.some((tag) => tag === token)) {
|
|
332
|
+
score += 0.5;
|
|
333
|
+
}
|
|
334
|
+
else if (tagsLower.some((tag) => tag.includes(token))) {
|
|
335
|
+
score += 0.25;
|
|
336
|
+
}
|
|
337
|
+
if (descLower.includes(token)) {
|
|
338
|
+
score += 0.2;
|
|
339
|
+
}
|
|
230
340
|
}
|
|
231
|
-
return
|
|
341
|
+
return tokens.length > 0 ? score / tokens.length : 0;
|
|
232
342
|
}
|
|
233
343
|
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
234
344
|
function clampLimit(limit) {
|
package/dist/renderers.js
CHANGED
|
@@ -112,7 +112,7 @@ export function detectExecHints(filePath) {
|
|
|
112
112
|
}
|
|
113
113
|
// ── Resolution ───────────────────────────────────────────────────────────────
|
|
114
114
|
/**
|
|
115
|
-
* Resolve execution hints for a script
|
|
115
|
+
* Resolve execution hints for a script asset.
|
|
116
116
|
*
|
|
117
117
|
* Resolution order (first non-empty value wins for each field):
|
|
118
118
|
* 1. `.stash.json` fields (`run`/`setup`/`cwd`) take priority
|
package/dist/stash-add.js
CHANGED
|
@@ -11,7 +11,7 @@ export async function agentikitAdd(input) {
|
|
|
11
11
|
throw new UsageError("Install ref or local directory is required.");
|
|
12
12
|
const stashDir = resolveStashDir();
|
|
13
13
|
const installed = await installRegistryRef(ref);
|
|
14
|
-
const replaced = loadConfig().
|
|
14
|
+
const replaced = (loadConfig().installed ?? []).find((entry) => entry.id === installed.id);
|
|
15
15
|
const config = upsertInstalledRegistryEntry({
|
|
16
16
|
id: installed.id,
|
|
17
17
|
source: installed.source,
|
|
@@ -59,7 +59,7 @@ export async function agentikitAdd(input) {
|
|
|
59
59
|
},
|
|
60
60
|
config: {
|
|
61
61
|
searchPaths: config.searchPaths,
|
|
62
|
-
|
|
62
|
+
installedKitCount: config.installed?.length ?? 0,
|
|
63
63
|
},
|
|
64
64
|
index: {
|
|
65
65
|
mode: index.mode,
|
package/dist/stash-ref.js
CHANGED
|
@@ -6,21 +6,19 @@ import { UsageError } from "./errors";
|
|
|
6
6
|
* Build a ref string from components.
|
|
7
7
|
*
|
|
8
8
|
* Examples:
|
|
9
|
-
* makeAssetRef("
|
|
10
|
-
* → "
|
|
11
|
-
* makeAssetRef("
|
|
12
|
-
* → "npm:@scope/pkg//
|
|
9
|
+
* makeAssetRef("script", "deploy.sh")
|
|
10
|
+
* → "script:deploy.sh"
|
|
11
|
+
* makeAssetRef("script", "deploy.sh", "npm:@scope/pkg")
|
|
12
|
+
* → "npm:@scope/pkg//script:deploy.sh"
|
|
13
13
|
* makeAssetRef("skill", "code-review", "local")
|
|
14
14
|
* → "local//skill:code-review"
|
|
15
|
-
* makeAssetRef("
|
|
16
|
-
* → "owner/repo//
|
|
15
|
+
* makeAssetRef("script", "db/migrate/run.sh", "owner/repo")
|
|
16
|
+
* → "owner/repo//script:db/migrate/run.sh"
|
|
17
17
|
*/
|
|
18
18
|
export function makeAssetRef(type, name, origin) {
|
|
19
19
|
validateName(name);
|
|
20
20
|
const normalized = normalizeName(name);
|
|
21
|
-
|
|
22
|
-
const resolvedType = type === "tool" ? "script" : type;
|
|
23
|
-
const asset = `${resolvedType}:${normalized}`;
|
|
21
|
+
const asset = `${type}:${normalized}`;
|
|
24
22
|
if (!origin)
|
|
25
23
|
return asset;
|
|
26
24
|
return `${origin}//${asset}`;
|
package/dist/stash-registry.js
CHANGED
|
@@ -9,7 +9,7 @@ import { parseRegistryRef } from "./registry-resolve";
|
|
|
9
9
|
export async function agentikitList(input) {
|
|
10
10
|
const stashDir = input?.stashDir ?? resolveStashDir();
|
|
11
11
|
const config = loadConfig();
|
|
12
|
-
const installed = config.
|
|
12
|
+
const installed = config.installed ?? [];
|
|
13
13
|
return {
|
|
14
14
|
schemaVersion: 1,
|
|
15
15
|
stashDir,
|
|
@@ -29,7 +29,7 @@ export async function agentikitRemove(input) {
|
|
|
29
29
|
throw new UsageError("Target is required.");
|
|
30
30
|
const stashDir = input.stashDir ?? resolveStashDir();
|
|
31
31
|
const config = loadConfig();
|
|
32
|
-
const installed = config.
|
|
32
|
+
const installed = config.installed ?? [];
|
|
33
33
|
const entry = resolveInstalledTarget(installed, target);
|
|
34
34
|
const updatedConfig = removeInstalledRegistryEntry(entry.id);
|
|
35
35
|
removeLockEntry(entry.id);
|
|
@@ -52,7 +52,7 @@ export async function agentikitRemove(input) {
|
|
|
52
52
|
},
|
|
53
53
|
config: {
|
|
54
54
|
searchPaths: updatedConfig.searchPaths,
|
|
55
|
-
|
|
55
|
+
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
56
56
|
},
|
|
57
57
|
index: {
|
|
58
58
|
mode: index.mode,
|
|
@@ -67,7 +67,7 @@ export async function agentikitUpdate(input) {
|
|
|
67
67
|
const target = input?.target?.trim();
|
|
68
68
|
const all = input?.all === true;
|
|
69
69
|
const force = input?.force === true;
|
|
70
|
-
const installedEntries = loadConfig().
|
|
70
|
+
const installedEntries = loadConfig().installed ?? [];
|
|
71
71
|
const selectedEntries = selectTargets(installedEntries, target, all);
|
|
72
72
|
const processed = [];
|
|
73
73
|
for (const entry of selectedEntries) {
|
|
@@ -116,7 +116,7 @@ export async function agentikitUpdate(input) {
|
|
|
116
116
|
processed,
|
|
117
117
|
config: {
|
|
118
118
|
searchPaths: config.searchPaths,
|
|
119
|
-
|
|
119
|
+
installedKitCount: config.installed?.length ?? 0,
|
|
120
120
|
},
|
|
121
121
|
index: {
|
|
122
122
|
mode: index.mode,
|
|
@@ -156,7 +156,7 @@ function resolveInstalledTarget(installed, target) {
|
|
|
156
156
|
if (byParsedId)
|
|
157
157
|
return byParsedId;
|
|
158
158
|
}
|
|
159
|
-
throw new NotFoundError(`No installed
|
|
159
|
+
throw new NotFoundError(`No installed kit matched target: ${target}`);
|
|
160
160
|
}
|
|
161
161
|
function toInstalledEntry(status) {
|
|
162
162
|
return {
|