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.
@@ -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 "tools/azure/deploy/run.sh" the dir portion is "tools/azure/deploy"
26
- // which splits into ["tools", "azure", "deploy"].
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 and only when no explicit registrar was set.
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 tool registry. Given a tool/skill/command/agent entry, generate improved metadata. Respond with ONLY valid JSON, no markdown fencing.`;
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 { SCRIPT_EXTENSIONS_BROAD } from "./asset-spec";
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 (SCRIPT_EXTENSIONS_BROAD.has(ctx.ext)) {
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 === "tools" && SCRIPT_EXTENSIONS_BROAD.has(ext)) {
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 === "tools" && SCRIPT_EXTENSIONS_BROAD.has(ext)) {
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 tools/scripts)
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. "tools", "skills", "scripts") */
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. tools/deploy.sh → "deploy.sh" not "tools/deploy.sh"
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();
@@ -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.registry?.installed ?? [];
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
- registry: { installed: nextInstalled },
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.registry?.installed ?? [];
169
+ const currentInstalled = current.installed ?? [];
170
170
  const nextInstalled = currentInstalled.filter((item) => item.id !== id);
171
171
  const nextConfig = {
172
172
  ...current,
173
- registry: nextInstalled.length > 0 ? { installed: nextInstalled } : undefined,
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
- * (tools/, skills/, etc.), so nested layouts like `project/my-kit/tools/`
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`.
@@ -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 urls = resolveRegistryUrls(options?.registryUrls);
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 url of urls) {
22
+ for (const entry of entries) {
24
23
  try {
25
- const index = await loadIndex(url);
24
+ const index = await loadIndex(entry);
26
25
  if (index) {
27
- allKits.push(...index.kits);
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
- warnings.push(`Registry ${url}: ${toErrorMessage(err)}`);
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
- return { query: trimmed, hits, warnings };
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(url) {
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: 1, updatedAt: obj.updatedAt, kits };
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
- // ── Registry URL resolution ─────────────────────────────────────────────────
218
- function resolveRegistryUrls(override) {
219
- if (override) {
220
- const urls = Array.isArray(override) ? override : [override];
221
- return urls.filter(Boolean);
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
- // Allow env var override (comma-separated)
224
- const envUrls = process.env.AKM_REGISTRY_URL?.trim();
225
- if (envUrls) {
226
- return envUrls
227
- .split(",")
228
- .map((u) => u.trim())
229
- .filter(Boolean);
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 [DEFAULT_REGISTRY_URL];
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/tool asset.
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().registry?.installed.find((entry) => entry.id === installed.id);
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
- installedRegistryCount: config.registry?.installed.length ?? 0,
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("tool", "deploy.sh")
10
- * → "tool:deploy.sh"
11
- * makeAssetRef("tool", "deploy.sh", "npm:@scope/pkg")
12
- * → "npm:@scope/pkg//tool:deploy.sh"
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("tool", "db/migrate/run.sh", "owner/repo")
16
- * → "owner/repo//tool:db/migrate/run.sh"
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
- // "tool" is a transparent alias for "script" -- normalize to "script" in refs
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}`;
@@ -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.registry?.installed ?? [];
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.registry?.installed ?? [];
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
- installedRegistryCount: updatedConfig.registry?.installed.length ?? 0,
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().registry?.installed ?? [];
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
- installedRegistryCount: config.registry?.installed.length ?? 0,
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 registry entry matched target: ${target}`);
159
+ throw new NotFoundError(`No installed kit matched target: ${target}`);
160
160
  }
161
161
  function toInstalledEntry(status) {
162
162
  return {