akm-cli 0.0.16 → 0.0.18
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 +61 -108
- package/dist/asset-spec.js +9 -9
- package/dist/cli.js +321 -118
- package/dist/common.js +0 -7
- package/dist/config-cli.js +43 -0
- package/dist/config.js +46 -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/provider-registry.js +8 -0
- package/dist/providers/skills-sh.js +165 -0
- package/dist/providers/static-index.js +340 -0
- package/dist/registry-install.js +5 -5
- package/dist/registry-provider.js +1 -0
- package/dist/registry-search.js +65 -226
- 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/config-cli.js
CHANGED
|
@@ -23,6 +23,8 @@ export function parseConfigValue(key, value) {
|
|
|
23
23
|
return { embedding: parseEmbeddingConnectionValue(value) };
|
|
24
24
|
case "llm":
|
|
25
25
|
return { llm: parseLlmConnectionValue(value) };
|
|
26
|
+
case "registries":
|
|
27
|
+
return { registries: parseRegistriesValue(value) };
|
|
26
28
|
case "output.format":
|
|
27
29
|
return { output: { format: parseOutputFormat(value) } };
|
|
28
30
|
case "output.detail":
|
|
@@ -43,6 +45,8 @@ export function getConfigValue(config, key) {
|
|
|
43
45
|
return config.embedding ?? null;
|
|
44
46
|
case "llm":
|
|
45
47
|
return config.llm ?? null;
|
|
48
|
+
case "registries":
|
|
49
|
+
return config.registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
46
50
|
case "output.format":
|
|
47
51
|
return config.output?.format ?? null;
|
|
48
52
|
case "output.detail":
|
|
@@ -58,6 +62,7 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
58
62
|
case "searchPaths":
|
|
59
63
|
case "embedding":
|
|
60
64
|
case "llm":
|
|
65
|
+
case "registries":
|
|
61
66
|
case "output.format":
|
|
62
67
|
case "output.detail":
|
|
63
68
|
return mergeConfigValue(config, parseConfigValue(key, rawValue));
|
|
@@ -73,6 +78,8 @@ export function unsetConfigValue(config, key) {
|
|
|
73
78
|
return { ...config, embedding: undefined };
|
|
74
79
|
case "llm":
|
|
75
80
|
return { ...config, llm: undefined };
|
|
81
|
+
case "registries":
|
|
82
|
+
return { ...config, registries: undefined };
|
|
76
83
|
case "output.format":
|
|
77
84
|
return { ...config, output: mergeOutputConfig(config.output, { format: undefined }) };
|
|
78
85
|
case "output.detail":
|
|
@@ -89,6 +96,7 @@ export function listConfig(config) {
|
|
|
89
96
|
stashDir: config.stashDir ?? null,
|
|
90
97
|
embedding: config.embedding ?? null,
|
|
91
98
|
llm: config.llm ?? null,
|
|
99
|
+
registries: config.registries ?? DEFAULT_CONFIG.registries ?? [],
|
|
92
100
|
};
|
|
93
101
|
}
|
|
94
102
|
function mergeConfigValue(config, partial) {
|
|
@@ -115,6 +123,41 @@ function parseOutputDetail(value) {
|
|
|
115
123
|
return value;
|
|
116
124
|
throw new UsageError(`Invalid value for output.detail: expected one of brief|normal|full`);
|
|
117
125
|
}
|
|
126
|
+
function parseRegistriesValue(value) {
|
|
127
|
+
if (value === "null" || value === "")
|
|
128
|
+
return undefined;
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(value);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new UsageError(`Invalid value for registries: expected JSON array of {url, name?, enabled?, provider?, options?} objects` +
|
|
135
|
+
` (e.g. '[{"url":"https://example.com/index.json","name":"my-registry"}]')`);
|
|
136
|
+
}
|
|
137
|
+
if (!Array.isArray(parsed)) {
|
|
138
|
+
throw new UsageError(`Invalid value for registries: expected a JSON array`);
|
|
139
|
+
}
|
|
140
|
+
return parsed.map((entry, i) => {
|
|
141
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
142
|
+
throw new UsageError(`Invalid value for registries[${i}]: expected an object with a "url" field`);
|
|
143
|
+
}
|
|
144
|
+
const obj = entry;
|
|
145
|
+
if (typeof obj.url !== "string" || !obj.url) {
|
|
146
|
+
throw new UsageError(`Invalid value for registries[${i}]: "url" is required`);
|
|
147
|
+
}
|
|
148
|
+
const result = { url: obj.url };
|
|
149
|
+
if (typeof obj.name === "string" && obj.name)
|
|
150
|
+
result.name = obj.name;
|
|
151
|
+
if (typeof obj.enabled === "boolean")
|
|
152
|
+
result.enabled = obj.enabled;
|
|
153
|
+
if (typeof obj.provider === "string" && obj.provider)
|
|
154
|
+
result.provider = obj.provider;
|
|
155
|
+
if (typeof obj.options === "object" && obj.options !== null && !Array.isArray(obj.options)) {
|
|
156
|
+
result.options = obj.options;
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
118
161
|
function parseEmbeddingConnectionValue(value) {
|
|
119
162
|
if (value === "null" || value === "")
|
|
120
163
|
return undefined;
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getConfigDir as _getConfigDir, getConfigPath as _getConfigPath } from "
|
|
|
5
5
|
export const DEFAULT_CONFIG = {
|
|
6
6
|
semanticSearch: true,
|
|
7
7
|
searchPaths: [],
|
|
8
|
+
registries: [{ url: "https://raw.githubusercontent.com/itlackey/akm-registry/main/index.json", name: "official" }],
|
|
8
9
|
output: {
|
|
9
10
|
format: "json",
|
|
10
11
|
detail: "brief",
|
|
@@ -113,27 +114,18 @@ function pickKnownKeys(raw) {
|
|
|
113
114
|
if (Array.isArray(raw.searchPaths)) {
|
|
114
115
|
config.searchPaths = raw.searchPaths.filter((d) => typeof d === "string");
|
|
115
116
|
}
|
|
116
|
-
// Backward compat: merge legacy mountedStashDirs into searchPaths
|
|
117
|
-
if (Array.isArray(raw.mountedStashDirs)) {
|
|
118
|
-
const legacy = raw.mountedStashDirs.filter((d) => typeof d === "string");
|
|
119
|
-
const existing = new Set(config.searchPaths);
|
|
120
|
-
for (const d of legacy) {
|
|
121
|
-
if (!existing.has(d))
|
|
122
|
-
config.searchPaths.push(d);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
117
|
const embedding = parseEmbeddingConfig(raw.embedding);
|
|
126
118
|
if (embedding)
|
|
127
119
|
config.embedding = embedding;
|
|
128
120
|
const llm = parseLlmConfig(raw.llm);
|
|
129
121
|
if (llm)
|
|
130
122
|
config.llm = llm;
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
config.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
123
|
+
const installed = parseInstalledEntries(raw.installed);
|
|
124
|
+
if (installed)
|
|
125
|
+
config.installed = installed;
|
|
126
|
+
const registries = parseRegistriesConfig(raw.registries);
|
|
127
|
+
if (registries)
|
|
128
|
+
config.registries = registries;
|
|
137
129
|
const output = parseOutputConfig(raw.output);
|
|
138
130
|
if (output)
|
|
139
131
|
config.output = output;
|
|
@@ -273,23 +265,20 @@ function parseLlmConfig(value) {
|
|
|
273
265
|
}
|
|
274
266
|
return result;
|
|
275
267
|
}
|
|
276
|
-
function
|
|
277
|
-
if (
|
|
268
|
+
function parseInstalledEntries(value) {
|
|
269
|
+
if (!Array.isArray(value))
|
|
278
270
|
return undefined;
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
return undefined;
|
|
282
|
-
const installed = obj.installed
|
|
283
|
-
.map((entry) => parseRegistryInstalledEntry(entry))
|
|
271
|
+
const entries = value
|
|
272
|
+
.map((entry) => parseInstalledKitEntry(entry))
|
|
284
273
|
.filter((entry) => entry !== undefined);
|
|
285
|
-
return
|
|
274
|
+
return entries.length > 0 ? entries : undefined;
|
|
286
275
|
}
|
|
287
|
-
function
|
|
276
|
+
function parseInstalledKitEntry(value) {
|
|
288
277
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
289
278
|
return undefined;
|
|
290
279
|
const obj = value;
|
|
291
280
|
const id = asNonEmptyString(obj.id);
|
|
292
|
-
const source =
|
|
281
|
+
const source = asKitSource(obj.source);
|
|
293
282
|
const ref = asNonEmptyString(obj.ref);
|
|
294
283
|
const artifactUrl = asNonEmptyString(obj.artifactUrl);
|
|
295
284
|
const stashRoot = asNonEmptyString(obj.stashRoot);
|
|
@@ -317,8 +306,39 @@ function parseRegistryInstalledEntry(value) {
|
|
|
317
306
|
function asNonEmptyString(value) {
|
|
318
307
|
return typeof value === "string" && value ? value : undefined;
|
|
319
308
|
}
|
|
320
|
-
function
|
|
309
|
+
function asKitSource(value) {
|
|
321
310
|
if (value === "npm" || value === "github" || value === "git" || value === "local")
|
|
322
311
|
return value;
|
|
323
312
|
return undefined;
|
|
324
313
|
}
|
|
314
|
+
function parseRegistriesConfig(value) {
|
|
315
|
+
if (!Array.isArray(value))
|
|
316
|
+
return undefined;
|
|
317
|
+
const entries = value
|
|
318
|
+
.map((entry) => parseRegistryConfigEntry(entry))
|
|
319
|
+
.filter((entry) => entry !== undefined);
|
|
320
|
+
// Return the array even if empty — an explicit empty array means "no registries"
|
|
321
|
+
// which overrides the default. Only return undefined if the field was not an array.
|
|
322
|
+
return entries;
|
|
323
|
+
}
|
|
324
|
+
function parseRegistryConfigEntry(value) {
|
|
325
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
326
|
+
return undefined;
|
|
327
|
+
const obj = value;
|
|
328
|
+
const url = asNonEmptyString(obj.url);
|
|
329
|
+
if (!url || !url.startsWith("http"))
|
|
330
|
+
return undefined;
|
|
331
|
+
const entry = { url };
|
|
332
|
+
const name = asNonEmptyString(obj.name);
|
|
333
|
+
if (name)
|
|
334
|
+
entry.name = name;
|
|
335
|
+
if (typeof obj.enabled === "boolean")
|
|
336
|
+
entry.enabled = obj.enabled;
|
|
337
|
+
const provider = asNonEmptyString(obj.provider);
|
|
338
|
+
if (provider)
|
|
339
|
+
entry.provider = provider;
|
|
340
|
+
if (typeof obj.options === "object" && obj.options !== null && !Array.isArray(obj.options)) {
|
|
341
|
+
entry.options = obj.options;
|
|
342
|
+
}
|
|
343
|
+
return entry;
|
|
344
|
+
}
|
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();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// ── Factory map ─────────────────────────────────────────────────────────────
|
|
2
|
+
const providers = new Map();
|
|
3
|
+
export function registerProvider(type, factory) {
|
|
4
|
+
providers.set(type, factory);
|
|
5
|
+
}
|
|
6
|
+
export function resolveProviderFactory(type) {
|
|
7
|
+
return providers.get(type) ?? null;
|
|
8
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fetchWithRetry } from "../common";
|
|
4
|
+
import { getRegistryIndexCacheDir } from "../paths";
|
|
5
|
+
import { registerProvider } from "../provider-registry";
|
|
6
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
7
|
+
/** Per-query cache TTL in milliseconds (15 minutes). */
|
|
8
|
+
const QUERY_CACHE_TTL_MS = 15 * 60 * 1000;
|
|
9
|
+
/** Maximum age before query cache is considered stale but still usable (1 day). */
|
|
10
|
+
const QUERY_CACHE_STALE_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
// ── Provider class ──────────────────────────────────────────────────────────
|
|
12
|
+
class SkillsShProvider {
|
|
13
|
+
type = "skills-sh";
|
|
14
|
+
config;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
async search(options) {
|
|
19
|
+
try {
|
|
20
|
+
const entries = await this.fetchSkills(options.query, options.limit);
|
|
21
|
+
const limited = entries.slice(0, options.limit);
|
|
22
|
+
const hits = this.mapToHits(limited);
|
|
23
|
+
let assetHits;
|
|
24
|
+
if (options.includeAssets) {
|
|
25
|
+
assetHits = this.mapToAssetHits(limited);
|
|
26
|
+
}
|
|
27
|
+
return { hits, assetHits };
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const label = this.config.name ?? "skills.sh";
|
|
31
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32
|
+
return { hits: [], warnings: [`Registry ${label}: ${message}`] };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async fetchSkills(query, limit) {
|
|
36
|
+
// Check per-query cache first
|
|
37
|
+
const cachePath = this.queryCachePath(query, limit);
|
|
38
|
+
const cached = this.readQueryCache(cachePath);
|
|
39
|
+
if (cached && !isExpired(cached.mtime, QUERY_CACHE_TTL_MS)) {
|
|
40
|
+
return cached.entries;
|
|
41
|
+
}
|
|
42
|
+
// Fetch from API
|
|
43
|
+
const baseUrl = this.config.url.replace(/\/+$/, "");
|
|
44
|
+
const url = `${baseUrl}/api/search?q=${encodeURIComponent(query)}&limit=${limit}`;
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetchWithRetry(url, undefined, { timeout: 10_000, retries: 1 });
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`HTTP ${response.status}`);
|
|
49
|
+
}
|
|
50
|
+
const data = (await response.json());
|
|
51
|
+
const entries = parseSkillsResponse(data);
|
|
52
|
+
this.writeQueryCache(cachePath, entries);
|
|
53
|
+
return entries;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// Fall back to stale cache if available
|
|
57
|
+
if (cached && !isExpired(cached.mtime, QUERY_CACHE_STALE_MS)) {
|
|
58
|
+
return cached.entries;
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
mapToHits(entries) {
|
|
64
|
+
if (entries.length === 0)
|
|
65
|
+
return [];
|
|
66
|
+
// Assign decreasing synthetic scores for merge compatibility
|
|
67
|
+
const maxInstalls = Math.max(...entries.map((e) => e.installs), 1);
|
|
68
|
+
const registryName = this.config.name ?? "skills.sh";
|
|
69
|
+
const baseUrl = this.config.url.replace(/\/+$/, "");
|
|
70
|
+
return entries.map((entry) => {
|
|
71
|
+
const owner = entry.source.split("/")[0] ?? "";
|
|
72
|
+
const score = Math.round((entry.installs / maxInstalls) * 1000) / 1000;
|
|
73
|
+
return {
|
|
74
|
+
source: "github",
|
|
75
|
+
id: `skills-sh:${entry.id}`,
|
|
76
|
+
title: entry.name,
|
|
77
|
+
ref: entry.source,
|
|
78
|
+
homepage: `${baseUrl}/${entry.id}`,
|
|
79
|
+
score,
|
|
80
|
+
metadata: {
|
|
81
|
+
installs: String(entry.installs),
|
|
82
|
+
...(owner ? { author: owner } : {}),
|
|
83
|
+
},
|
|
84
|
+
registryName,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
mapToAssetHits(entries) {
|
|
89
|
+
if (entries.length === 0)
|
|
90
|
+
return undefined;
|
|
91
|
+
const registryName = this.config.name ?? "skills.sh";
|
|
92
|
+
const maxInstalls = Math.max(...entries.map((e) => e.installs), 1);
|
|
93
|
+
const hits = entries.map((entry) => ({
|
|
94
|
+
type: "registry-asset",
|
|
95
|
+
assetType: "skill",
|
|
96
|
+
assetName: entry.name,
|
|
97
|
+
kit: { id: `skills-sh:${entry.id}`, name: entry.name },
|
|
98
|
+
registryName,
|
|
99
|
+
action: `akm add ${entry.source}`,
|
|
100
|
+
score: Math.round((entry.installs / maxInstalls) * 1000) / 1000,
|
|
101
|
+
}));
|
|
102
|
+
return hits.length > 0 ? hits : undefined;
|
|
103
|
+
}
|
|
104
|
+
// ── Per-query cache ─────────────────────────────────────────────────────
|
|
105
|
+
queryCachePath(query, limit) {
|
|
106
|
+
const cacheDir = getRegistryIndexCacheDir();
|
|
107
|
+
const hasher = new Bun.CryptoHasher("md5");
|
|
108
|
+
hasher.update(this.config.url);
|
|
109
|
+
hasher.update("\0");
|
|
110
|
+
hasher.update(query.trim().toLowerCase());
|
|
111
|
+
hasher.update("\0");
|
|
112
|
+
hasher.update(String(limit));
|
|
113
|
+
const hash = hasher.digest("hex");
|
|
114
|
+
return path.join(cacheDir, `skills-sh-search-${hash}.json`);
|
|
115
|
+
}
|
|
116
|
+
readQueryCache(cachePath) {
|
|
117
|
+
try {
|
|
118
|
+
const stat = fs.statSync(cachePath);
|
|
119
|
+
const raw = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
120
|
+
if (!Array.isArray(raw))
|
|
121
|
+
return null;
|
|
122
|
+
const entries = raw.filter(isValidSkillsEntry);
|
|
123
|
+
return { entries, mtime: stat.mtimeMs };
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
writeQueryCache(cachePath, entries) {
|
|
130
|
+
try {
|
|
131
|
+
const dir = path.dirname(cachePath);
|
|
132
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
133
|
+
const tmpPath = `${cachePath}.tmp.${process.pid}`;
|
|
134
|
+
fs.writeFileSync(tmpPath, JSON.stringify(entries), "utf8");
|
|
135
|
+
fs.renameSync(tmpPath, cachePath);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Best-effort caching
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// ── Self-register ───────────────────────────────────────────────────────────
|
|
143
|
+
registerProvider("skills-sh", (config) => new SkillsShProvider(config));
|
|
144
|
+
// ── Response parsing ────────────────────────────────────────────────────────
|
|
145
|
+
function parseSkillsResponse(data) {
|
|
146
|
+
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
147
|
+
return [];
|
|
148
|
+
const obj = data;
|
|
149
|
+
if (!Array.isArray(obj.skills))
|
|
150
|
+
return [];
|
|
151
|
+
return obj.skills.filter(isValidSkillsEntry);
|
|
152
|
+
}
|
|
153
|
+
function isValidSkillsEntry(entry) {
|
|
154
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
155
|
+
return false;
|
|
156
|
+
const obj = entry;
|
|
157
|
+
return (typeof obj.id === "string" &&
|
|
158
|
+
typeof obj.name === "string" &&
|
|
159
|
+
typeof obj.installs === "number" &&
|
|
160
|
+
typeof obj.source === "string");
|
|
161
|
+
}
|
|
162
|
+
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
163
|
+
function isExpired(mtimeMs, ttlMs) {
|
|
164
|
+
return Date.now() - mtimeMs > ttlMs;
|
|
165
|
+
}
|