akm-cli 0.5.0 → 0.6.0-rc2
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/CHANGELOG.md +53 -5
- package/README.md +9 -9
- package/dist/cli.js +379 -1448
- package/dist/{completions.js → commands/completions.js} +1 -1
- package/dist/{config-cli.js → commands/config-cli.js} +109 -11
- package/dist/commands/curate.js +263 -0
- package/dist/{info.js → commands/info.js} +17 -11
- package/dist/{init.js → commands/init.js} +4 -4
- package/dist/{install-audit.js → commands/install-audit.js} +14 -2
- package/dist/{installed-kits.js → commands/installed-stashes.js} +122 -50
- package/dist/commands/migration-help.js +141 -0
- package/dist/{registry-search.js → commands/registry-search.js} +68 -9
- package/dist/commands/remember.js +178 -0
- package/dist/{stash-search.js → commands/search.js} +28 -69
- package/dist/{self-update.js → commands/self-update.js} +3 -3
- package/dist/{stash-show.js → commands/show.js} +106 -81
- package/dist/{stash-add.js → commands/source-add.js} +133 -67
- package/dist/{stash-clone.js → commands/source-clone.js} +15 -13
- package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
- package/dist/{vault.js → commands/vault.js} +43 -0
- package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
- package/dist/{asset-registry.js → core/asset-registry.js} +30 -6
- package/dist/{asset-spec.js → core/asset-spec.js} +13 -6
- package/dist/{common.js → core/common.js} +147 -50
- package/dist/{config.js → core/config.js} +288 -29
- package/dist/core/errors.js +90 -0
- package/dist/{frontmatter.js → core/frontmatter.js} +64 -8
- package/dist/{paths.js → core/paths.js} +4 -4
- package/dist/core/write-source.js +280 -0
- package/dist/{local-search.js → indexer/db-search.js} +49 -32
- package/dist/{db.js → indexer/db.js} +210 -81
- package/dist/{file-context.js → indexer/file-context.js} +3 -3
- package/dist/{indexer.js → indexer/indexer.js} +153 -30
- package/dist/{manifest.js → indexer/manifest.js} +10 -10
- package/dist/{matchers.js → indexer/matchers.js} +4 -7
- package/dist/{metadata.js → indexer/metadata.js} +9 -5
- package/dist/{search-source.js → indexer/search-source.js} +97 -55
- package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
- package/dist/{walker.js → indexer/walker.js} +1 -1
- package/dist/{lockfile.js → integrations/lockfile.js} +29 -2
- package/dist/{llm.js → llm/client.js} +12 -48
- package/dist/llm/embedder.js +127 -0
- package/dist/llm/embedders/cache.js +47 -0
- package/dist/llm/embedders/local.js +152 -0
- package/dist/llm/embedders/remote.js +121 -0
- package/dist/llm/embedders/types.js +39 -0
- package/dist/llm/metadata-enhance.js +53 -0
- package/dist/output/cli-hints.js +301 -0
- package/dist/output/context.js +95 -0
- package/dist/{renderers.js → output/renderers.js} +57 -61
- package/dist/output/shapes.js +212 -0
- package/dist/output/text.js +520 -0
- package/dist/{registry-build-index.js → registry/build-index.js} +48 -32
- package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
- package/dist/registry/factory.js +33 -0
- package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
- package/dist/registry/providers/index.js +11 -0
- package/dist/{providers → registry/providers}/skills-sh.js +60 -4
- package/dist/{providers → registry/providers}/static-index.js +126 -56
- package/dist/registry/providers/types.js +25 -0
- package/dist/{registry-resolve.js → registry/resolve.js} +10 -6
- package/dist/{detect.js → setup/detect.js} +0 -27
- package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
- package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
- package/dist/{setup.js → setup/setup.js} +162 -129
- package/dist/setup/steps.js +45 -0
- package/dist/{kit-include.js → sources/include.js} +1 -1
- package/dist/sources/provider-factory.js +36 -0
- package/dist/sources/provider.js +21 -0
- package/dist/sources/providers/filesystem.js +35 -0
- package/dist/{stash-providers → sources/providers}/git.js +218 -28
- package/dist/{stash-providers → sources/providers}/index.js +4 -4
- package/dist/sources/providers/install-types.js +14 -0
- package/dist/sources/providers/npm.js +160 -0
- package/dist/sources/providers/provider-utils.js +173 -0
- package/dist/sources/providers/sync-from-ref.js +45 -0
- package/dist/sources/providers/tar-utils.js +154 -0
- package/dist/{stash-providers → sources/providers}/website.js +60 -20
- package/dist/{stash-resolve.js → sources/resolve.js} +13 -12
- package/dist/{wiki.js → wiki/wiki.js} +18 -17
- package/dist/{workflow-authoring.js → workflows/authoring.js} +48 -17
- package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
- package/dist/{workflow-db.js → workflows/db.js} +1 -1
- package/dist/workflows/document-cache.js +20 -0
- package/dist/workflows/parser.js +379 -0
- package/dist/workflows/renderer.js +78 -0
- package/dist/{workflow-runs.js → workflows/runs.js} +84 -30
- package/dist/workflows/schema.js +11 -0
- package/dist/workflows/validator.js +48 -0
- package/docs/README.md +30 -0
- package/docs/migration/release-notes/0.0.13.md +4 -0
- package/docs/migration/release-notes/0.1.0.md +6 -0
- package/docs/migration/release-notes/0.2.0.md +6 -0
- package/docs/migration/release-notes/0.3.0.md +5 -0
- package/docs/migration/release-notes/0.5.0.md +6 -0
- package/docs/migration/release-notes/0.6.0.md +75 -0
- package/docs/migration/release-notes/README.md +21 -0
- package/package.json +3 -2
- package/dist/embedder.js +0 -351
- package/dist/errors.js +0 -34
- package/dist/migration-help.js +0 -110
- package/dist/registry-factory.js +0 -19
- package/dist/registry-install.js +0 -532
- package/dist/ripgrep.js +0 -2
- package/dist/stash-provider-factory.js +0 -35
- package/dist/stash-provider.js +0 -1
- package/dist/stash-providers/filesystem.js +0 -41
- package/dist/stash-providers/openviking.js +0 -348
- package/dist/stash-providers/provider-utils.js +0 -11
- package/dist/stash-types.js +0 -1
- package/dist/workflow-markdown.js +0 -251
- /package/dist/{markdown.js → core/markdown.js} +0 -0
- /package/dist/{warn.js → core/warn.js} +0 -0
- /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
- /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
- /package/dist/{github.js → integrations/github.js} +0 -0
- /package/dist/{registry-provider.js → registry/types.js} +0 -0
- /package/dist/{registry-types.js → sources/types.js} +0 -0
|
@@ -1,18 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the v2 JSON registry index consumed by the `static-index` registry
|
|
3
|
+
* provider. This module emits artifacts that conform to the v2 schema; the
|
|
4
|
+
* schema itself is the input contract owned by `src/registry/providers/static-index.ts`
|
|
5
|
+
* (see v1 architecture spec §3.3 — "the v2 JSON index schema belongs to
|
|
6
|
+
* static-index"). When the schema changes, both the parser in `static-index.ts`
|
|
7
|
+
* and the JSON Schema in `docs/technical/registry-index.schema.json` must be
|
|
8
|
+
* updated together with this builder.
|
|
9
|
+
*/
|
|
1
10
|
import fs from "node:fs";
|
|
2
11
|
import os from "node:os";
|
|
3
12
|
import path from "node:path";
|
|
4
|
-
import { fetchWithRetry } from "
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
13
|
+
import { fetchWithRetry, jsonWithByteCap } from "../core/common";
|
|
14
|
+
import { generateMetadataFlat, loadStashFile } from "../indexer/metadata";
|
|
15
|
+
import { walkStashFlat } from "../indexer/walker";
|
|
16
|
+
import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "../integrations/github";
|
|
17
|
+
import { copyIncludedPaths, findNearestIncludeConfig } from "../sources/include";
|
|
18
|
+
import { detectStashRoot } from "../sources/providers/provider-utils";
|
|
19
|
+
import { extractTarGzSecure } from "../sources/providers/tar-utils";
|
|
8
20
|
import { parseRegistryIndex } from "./providers/static-index";
|
|
9
|
-
import { detectStashRoot, extractTarGzSecure } from "./registry-install";
|
|
10
|
-
import { walkStashFlat } from "./walker";
|
|
11
21
|
const DEFAULT_NPM_REGISTRY_BASE = "https://registry.npmjs.org";
|
|
12
22
|
const DEFAULT_MANUAL_ENTRIES_PATH = path.resolve("manual-entries.json");
|
|
13
23
|
const DEFAULT_OUTPUT_PATH = path.resolve("index.json");
|
|
14
|
-
const REQUIRED_KEYWORDS = ["akm-
|
|
15
|
-
const GITHUB_TOPICS = ["akm-
|
|
24
|
+
const REQUIRED_KEYWORDS = ["akm-stash"];
|
|
25
|
+
const GITHUB_TOPICS = ["akm-stash"];
|
|
16
26
|
const EXCLUDED_REPOS = new Set(["itlackey/akm"]);
|
|
17
27
|
const EXCLUDED_NPM_PACKAGES = new Set(["akm-cli"]);
|
|
18
28
|
const EMPTY_INSPECTION = {};
|
|
@@ -25,11 +35,11 @@ export async function buildRegistryIndex(options) {
|
|
|
25
35
|
scanNpm(npmRegistryBase),
|
|
26
36
|
scanGithub(githubApiBase),
|
|
27
37
|
]);
|
|
28
|
-
const
|
|
38
|
+
const stashes = deduplicateStashes([...manualKits, ...npmKits, ...githubKits]).sort((a, b) => a.name.localeCompare(b.name));
|
|
29
39
|
const index = {
|
|
30
|
-
version:
|
|
40
|
+
version: 3,
|
|
31
41
|
updatedAt: new Date().toISOString(),
|
|
32
|
-
|
|
42
|
+
stashes,
|
|
33
43
|
};
|
|
34
44
|
return {
|
|
35
45
|
index,
|
|
@@ -37,7 +47,7 @@ export async function buildRegistryIndex(options) {
|
|
|
37
47
|
manual: manualKits.length,
|
|
38
48
|
npm: npmKits.length,
|
|
39
49
|
github: githubKits.length,
|
|
40
|
-
total:
|
|
50
|
+
total: stashes.length,
|
|
41
51
|
},
|
|
42
52
|
paths: {
|
|
43
53
|
manualEntriesPath,
|
|
@@ -51,7 +61,7 @@ export function writeRegistryIndex(index, outPath) {
|
|
|
51
61
|
return resolved;
|
|
52
62
|
}
|
|
53
63
|
async function scanNpm(npmRegistryBase) {
|
|
54
|
-
const
|
|
64
|
+
const stashes = [];
|
|
55
65
|
const seen = new Set();
|
|
56
66
|
for (const keyword of REQUIRED_KEYWORDS) {
|
|
57
67
|
let offset = 0;
|
|
@@ -83,7 +93,7 @@ async function scanNpm(npmRegistryBase) {
|
|
|
83
93
|
}
|
|
84
94
|
const inspection = await inspectNpmPackage(npmRegistryBase, latestMetadata).catch(() => EMPTY_INSPECTION);
|
|
85
95
|
const tags = mergeStrings((pkg.keywords ?? []).filter((value) => !REQUIRED_KEYWORDS.includes(value.toLowerCase())), inspection.tags);
|
|
86
|
-
|
|
96
|
+
stashes.push(normalizeStash({
|
|
87
97
|
id,
|
|
88
98
|
name: pkg.name,
|
|
89
99
|
description: inspection.description ?? pkg.description,
|
|
@@ -103,7 +113,7 @@ async function scanNpm(npmRegistryBase) {
|
|
|
103
113
|
offset += size;
|
|
104
114
|
}
|
|
105
115
|
}
|
|
106
|
-
return
|
|
116
|
+
return stashes;
|
|
107
117
|
}
|
|
108
118
|
async function inspectNpmPackage(_npmRegistryBase, latestMetadata) {
|
|
109
119
|
const dist = asRecord(latestMetadata.dist);
|
|
@@ -121,7 +131,7 @@ async function inspectNpmPackage(_npmRegistryBase, latestMetadata) {
|
|
|
121
131
|
};
|
|
122
132
|
}
|
|
123
133
|
async function scanGithub(githubApiBase) {
|
|
124
|
-
const
|
|
134
|
+
const stashes = [];
|
|
125
135
|
const seen = new Set();
|
|
126
136
|
const headers = githubHeaders();
|
|
127
137
|
for (const topic of GITHUB_TOPICS) {
|
|
@@ -140,7 +150,7 @@ async function scanGithub(githubApiBase) {
|
|
|
140
150
|
seen.add(id);
|
|
141
151
|
const inspection = await inspectArchive(`${githubApiBase}/repos/${repo.full_name}/tarball/${encodeURIComponent(repo.default_branch)}`, headers).catch(() => EMPTY_INSPECTION);
|
|
142
152
|
const topics = repo.topics.filter((value) => !GITHUB_TOPICS.includes(value));
|
|
143
|
-
|
|
153
|
+
stashes.push(normalizeStash({
|
|
144
154
|
id,
|
|
145
155
|
name: repo.name,
|
|
146
156
|
description: inspection.description ?? repo.description ?? undefined,
|
|
@@ -160,7 +170,7 @@ async function scanGithub(githubApiBase) {
|
|
|
160
170
|
page += 1;
|
|
161
171
|
}
|
|
162
172
|
}
|
|
163
|
-
return
|
|
173
|
+
return stashes;
|
|
164
174
|
}
|
|
165
175
|
async function inspectArchive(url, headers) {
|
|
166
176
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-registry-build-"));
|
|
@@ -269,36 +279,42 @@ function applyIncludeConfigForInspection(stashRoot, tempDir, searchRoot) {
|
|
|
269
279
|
async function loadManualEntries(manualEntriesPath) {
|
|
270
280
|
try {
|
|
271
281
|
const raw = JSON.parse(fs.readFileSync(manualEntriesPath, "utf8"));
|
|
272
|
-
const candidateKits = Array.isArray(raw) ? raw : asRecord(raw).
|
|
273
|
-
const parsed = parseRegistryIndex({ version:
|
|
282
|
+
const candidateKits = Array.isArray(raw) ? raw : asRecord(raw).stashes;
|
|
283
|
+
const parsed = parseRegistryIndex({ version: 3, updatedAt: new Date().toISOString(), stashes: candidateKits });
|
|
274
284
|
if (!parsed)
|
|
275
285
|
return [];
|
|
276
|
-
return parsed.
|
|
286
|
+
return parsed.stashes.map((stash) => normalizeStash({ ...stash, curated: stash.curated ?? true }));
|
|
277
287
|
}
|
|
278
288
|
catch {
|
|
279
289
|
return [];
|
|
280
290
|
}
|
|
281
291
|
}
|
|
292
|
+
// npm / GitHub API JSON pages; 25 MB cap covers the largest realistic
|
|
293
|
+
// search result set while still bounding memory against a malicious or
|
|
294
|
+
// misconfigured upstream that streams unbounded JSON.
|
|
295
|
+
const BUILD_INDEX_JSON_BYTE_CAP = 25 * 1024 * 1024;
|
|
282
296
|
async function fetchJson(url, headers) {
|
|
283
297
|
const response = await fetchWithRetry(url, headers ? { headers } : undefined, { timeout: 30_000 });
|
|
284
298
|
if (!response.ok) {
|
|
299
|
+
// Error-body sampling is intentionally small; 4 KB is plenty to
|
|
300
|
+
// include upstream hints in the thrown error.
|
|
285
301
|
const body = await response.text().catch(() => "");
|
|
286
302
|
throw new Error(`HTTP ${response.status} from ${url}: ${body.slice(0, 200)}`);
|
|
287
303
|
}
|
|
288
|
-
return (
|
|
304
|
+
return jsonWithByteCap(response, BUILD_INDEX_JSON_BYTE_CAP);
|
|
289
305
|
}
|
|
290
|
-
function
|
|
306
|
+
function deduplicateStashes(stashes) {
|
|
291
307
|
const byId = new Map();
|
|
292
|
-
for (const
|
|
293
|
-
const existing = byId.get(
|
|
294
|
-
byId.set(
|
|
308
|
+
for (const stash of stashes) {
|
|
309
|
+
const existing = byId.get(stash.id);
|
|
310
|
+
byId.set(stash.id, existing ? mergeEntries(existing, stash) : stash);
|
|
295
311
|
}
|
|
296
312
|
return [...byId.values()];
|
|
297
313
|
}
|
|
298
314
|
function mergeEntries(a, b) {
|
|
299
315
|
const assets = mergeAssets(a.assets, b.assets);
|
|
300
316
|
const assetTypes = mergeStrings(a.assetTypes, b.assetTypes, assets ? deriveAssetTypes(assets) : undefined);
|
|
301
|
-
return
|
|
317
|
+
return normalizeStash({
|
|
302
318
|
id: a.id,
|
|
303
319
|
name: a.name,
|
|
304
320
|
description: a.description ?? b.description,
|
|
@@ -343,12 +359,12 @@ function extractNonReservedKeywords(value) {
|
|
|
343
359
|
.filter((item) => !REQUIRED_KEYWORDS.includes(item.toLowerCase()));
|
|
344
360
|
return filtered.length > 0 ? filtered : undefined;
|
|
345
361
|
}
|
|
346
|
-
function
|
|
347
|
-
const assets =
|
|
362
|
+
function normalizeStash(stash) {
|
|
363
|
+
const assets = stash.assets ? sortAssets(stash.assets) : undefined;
|
|
348
364
|
return {
|
|
349
|
-
...
|
|
350
|
-
...(
|
|
351
|
-
...(
|
|
365
|
+
...stash,
|
|
366
|
+
...(stash.tags && stash.tags.length > 0 ? { tags: stash.tags } : {}),
|
|
367
|
+
...(stash.assetTypes && stash.assetTypes.length > 0 ? { assetTypes: stash.assetTypes } : {}),
|
|
352
368
|
...(assets && assets.length > 0 ? { assets } : {}),
|
|
353
369
|
};
|
|
354
370
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Generic factory-map utility.
|
|
3
3
|
*
|
|
4
4
|
* Creates a lightweight registry that maps string keys to factory functions.
|
|
5
|
-
* Both registry
|
|
6
|
-
* (
|
|
5
|
+
* Both registry/factory.ts (registry discovery) and sources/provider-factory.ts
|
|
6
|
+
* (source providers) are built on this utility.
|
|
7
7
|
*/
|
|
8
8
|
export function createProviderRegistry() {
|
|
9
9
|
const map = new Map();
|
|
@@ -14,5 +14,9 @@ export function createProviderRegistry() {
|
|
|
14
14
|
resolve(type) {
|
|
15
15
|
return map.get(type) ?? null;
|
|
16
16
|
},
|
|
17
|
+
/** Snapshot of all registered keys. Iteration order matches insertion order. */
|
|
18
|
+
list() {
|
|
19
|
+
return [...map.keys()];
|
|
20
|
+
},
|
|
17
21
|
};
|
|
18
22
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry provider factory map.
|
|
3
|
+
*
|
|
4
|
+
* Maps registry provider type identifiers (e.g. "static-index", "skills-sh")
|
|
5
|
+
* to factory functions that create RegistryProvider instances.
|
|
6
|
+
*
|
|
7
|
+
* "Registry" here refers to the kit discovery registries (static index files,
|
|
8
|
+
* skills.sh API) — not to be confused with the source provider factory map in
|
|
9
|
+
* `sources/provider-factory.ts` or the installed-source operations in
|
|
10
|
+
* `installed-stashes.ts`.
|
|
11
|
+
*
|
|
12
|
+
* Phase 6 (v1 architecture refactor): factories are now the
|
|
13
|
+
* `RegistryProviderFactory` type owned by `src/registry/providers/types.ts`.
|
|
14
|
+
* The legacy alias in `src/registry-provider.ts` is kept as a thin re-export
|
|
15
|
+
* for transitional callers and will be removed after the dust settles.
|
|
16
|
+
*/
|
|
17
|
+
import { createProviderRegistry } from "./create-provider-registry";
|
|
18
|
+
// ── Factory map ─────────────────────────────────────────────────────────────
|
|
19
|
+
const registry = createProviderRegistry();
|
|
20
|
+
export function registerProvider(type, factory) {
|
|
21
|
+
registry.register(type, factory);
|
|
22
|
+
}
|
|
23
|
+
export function resolveProviderFactory(type) {
|
|
24
|
+
return registry.resolve(type);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Iterate over all registered registry providers. Used by the orchestrator
|
|
28
|
+
* (`src/commands/registry-search.ts`) to fan out queries through the same
|
|
29
|
+
* `RegistryProvider` interface regardless of provider kind.
|
|
30
|
+
*/
|
|
31
|
+
export function listProviderTypes() {
|
|
32
|
+
return registry.list();
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized registry provider registration.
|
|
3
|
+
*
|
|
4
|
+
* Import this module (side-effect import) to register all built-in registry
|
|
5
|
+
* providers with the provider registry. This replaces the individual
|
|
6
|
+
* side-effect imports that were duplicated in registry-search.ts.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors the pattern used by `sources/providers/index.ts`.
|
|
9
|
+
*/
|
|
10
|
+
import "./static-index";
|
|
11
|
+
import "./skills-sh";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { fetchWithRetry } from "
|
|
4
|
-
import { getRegistryIndexCacheDir } from "
|
|
5
|
-
import { registerProvider } from "../
|
|
3
|
+
import { fetchWithRetry } from "../../core/common";
|
|
4
|
+
import { getRegistryIndexCacheDir } from "../../core/paths";
|
|
5
|
+
import { registerProvider } from "../factory";
|
|
6
6
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
7
7
|
/** Per-query cache TTL in milliseconds (15 minutes). */
|
|
8
8
|
const QUERY_CACHE_TTL_MS = 15 * 60 * 1000;
|
|
@@ -32,6 +32,62 @@ class SkillsShProvider {
|
|
|
32
32
|
return { hits: [], warnings: [`Registry ${label}: ${message}`] };
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
// ── v1-spec §3.1 surface ────────────────────────────────────────────────
|
|
36
|
+
async searchKits(q) {
|
|
37
|
+
const result = await this.search({
|
|
38
|
+
query: q.text,
|
|
39
|
+
limit: q.limit ?? 20,
|
|
40
|
+
includeAssets: false,
|
|
41
|
+
});
|
|
42
|
+
return result.hits.map((hit) => ({
|
|
43
|
+
id: hit.id,
|
|
44
|
+
title: hit.title,
|
|
45
|
+
summary: hit.description,
|
|
46
|
+
installRef: hit.installRef,
|
|
47
|
+
score: hit.score,
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
async searchAssets(q) {
|
|
51
|
+
const result = await this.search({
|
|
52
|
+
query: q.text,
|
|
53
|
+
limit: q.limit ?? 20,
|
|
54
|
+
includeAssets: true,
|
|
55
|
+
});
|
|
56
|
+
return (result.assetHits ?? []).map((hit) => ({
|
|
57
|
+
kitId: hit.stash.id,
|
|
58
|
+
type: hit.assetType,
|
|
59
|
+
name: hit.assetName,
|
|
60
|
+
summary: hit.description,
|
|
61
|
+
cloneRef: hit.action.replace(/^akm add\s+/, ""),
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* skills.sh has no `getKit` API — every entry corresponds to a GitHub
|
|
66
|
+
* repository whose metadata we already include in the search result. We
|
|
67
|
+
* synthesize a manifest from the search hit when the caller knows the kit
|
|
68
|
+
* id; if not present in the most recent results, return null.
|
|
69
|
+
*/
|
|
70
|
+
async getKit(id) {
|
|
71
|
+
if (!id.startsWith("skills-sh:"))
|
|
72
|
+
return null;
|
|
73
|
+
const slug = id.slice("skills-sh:".length);
|
|
74
|
+
// Best-effort: the API gives us search-by-name; extract the leaf segment.
|
|
75
|
+
const segments = slug.split("/").filter(Boolean);
|
|
76
|
+
const leaf = segments[segments.length - 1] ?? slug;
|
|
77
|
+
const result = await this.search({ query: leaf, limit: 50, includeAssets: false });
|
|
78
|
+
const match = result.hits.find((hit) => hit.id === id);
|
|
79
|
+
if (!match)
|
|
80
|
+
return null;
|
|
81
|
+
return { id: match.id, installRef: match.installRef };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* skills.sh entries are always GitHub repositories. Claim only refs whose
|
|
85
|
+
* parsed source is `github`; defer everything else (npm tarballs, local
|
|
86
|
+
* paths, raw git URLs) to other registries.
|
|
87
|
+
*/
|
|
88
|
+
canHandle(ref) {
|
|
89
|
+
return ref.source === "github";
|
|
90
|
+
}
|
|
35
91
|
async fetchSkills(query, limit) {
|
|
36
92
|
// Check per-query cache first
|
|
37
93
|
const cachePath = this.queryCachePath(query, limit);
|
|
@@ -103,7 +159,7 @@ class SkillsShProvider {
|
|
|
103
159
|
type: "registry-asset",
|
|
104
160
|
assetType: "skill",
|
|
105
161
|
assetName: entry.name,
|
|
106
|
-
|
|
162
|
+
stash: { id: `skills-sh:${entry.id}`, name: entry.name },
|
|
107
163
|
registryName,
|
|
108
164
|
action: `akm add github:${ownerRepo}`,
|
|
109
165
|
score: Math.round((entry.installs / maxInstalls) * 1000) / 1000,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { fetchWithRetry, toErrorMessage } from "
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { registerProvider } from "../
|
|
3
|
+
import { fetchWithRetry, jsonWithByteCap, toErrorMessage } from "../../core/common";
|
|
4
|
+
import { getRegistryIndexCacheDir } from "../../core/paths";
|
|
5
|
+
import { asString } from "../../integrations/github";
|
|
6
|
+
import { registerProvider } from "../factory";
|
|
7
7
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
8
8
|
/** Cache TTL in milliseconds (1 hour). */
|
|
9
9
|
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
@@ -18,13 +18,70 @@ class StaticIndexProvider {
|
|
|
18
18
|
}
|
|
19
19
|
async search(options) {
|
|
20
20
|
const warnings = [];
|
|
21
|
+
const allKits = await this.loadAllKits(warnings);
|
|
22
|
+
const hits = scoreKits(allKits, options.query, options.limit);
|
|
23
|
+
let assetHits;
|
|
24
|
+
if (options.includeAssets) {
|
|
25
|
+
const scored = scoreAssets(allKits, options.query, options.limit);
|
|
26
|
+
if (scored.length > 0)
|
|
27
|
+
assetHits = scored;
|
|
28
|
+
}
|
|
29
|
+
return { hits, assetHits, warnings: warnings.length > 0 ? warnings : undefined };
|
|
30
|
+
}
|
|
31
|
+
// ── v1-spec §3.1 surface ────────────────────────────────────────────────
|
|
32
|
+
async searchKits(q) {
|
|
33
|
+
const result = await this.search({
|
|
34
|
+
query: q.text,
|
|
35
|
+
limit: q.limit ?? 20,
|
|
36
|
+
includeAssets: false,
|
|
37
|
+
});
|
|
38
|
+
return result.hits.map(hitToKitResult);
|
|
39
|
+
}
|
|
40
|
+
async searchAssets(q) {
|
|
41
|
+
const result = await this.search({
|
|
42
|
+
query: q.text,
|
|
43
|
+
limit: q.limit ?? 20,
|
|
44
|
+
includeAssets: true,
|
|
45
|
+
});
|
|
46
|
+
return (result.assetHits ?? []).map(assetHitToPreview);
|
|
47
|
+
}
|
|
48
|
+
async getKit(id) {
|
|
49
|
+
const allKits = await this.loadAllKits([]);
|
|
50
|
+
const found = allKits.find(({ stash }) => stash.id === id);
|
|
51
|
+
if (!found)
|
|
52
|
+
return null;
|
|
53
|
+
const installRef = buildInstallRef(found.stash.source, found.stash.ref);
|
|
54
|
+
return {
|
|
55
|
+
id: found.stash.id,
|
|
56
|
+
installRef,
|
|
57
|
+
assets: found.stash.assets?.map((asset) => ({
|
|
58
|
+
kitId: found.stash.id,
|
|
59
|
+
type: asset.type,
|
|
60
|
+
name: asset.name,
|
|
61
|
+
summary: asset.description,
|
|
62
|
+
cloneRef: installRef,
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Static-index doesn't own a URL prefix — any `ParsedRegistryRef` could
|
|
68
|
+
* theoretically be backed by an entry in some static-index registry. We
|
|
69
|
+
* therefore claim every ref. The orchestrator picks the first matching
|
|
70
|
+
* provider, and `static-index` is registered first by `index.ts`, so this
|
|
71
|
+
* is effectively the default catch-all.
|
|
72
|
+
*/
|
|
73
|
+
canHandle(_ref) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
// ── Internals ───────────────────────────────────────────────────────────
|
|
77
|
+
async loadAllKits(warnings) {
|
|
21
78
|
const allKits = [];
|
|
22
79
|
try {
|
|
23
80
|
const index = await loadIndex(this.config);
|
|
24
81
|
if (index) {
|
|
25
82
|
const regName = this.config.name;
|
|
26
|
-
for (const
|
|
27
|
-
allKits.push({
|
|
83
|
+
for (const stash of index.stashes) {
|
|
84
|
+
allKits.push({ stash, registryName: regName });
|
|
28
85
|
}
|
|
29
86
|
}
|
|
30
87
|
}
|
|
@@ -32,16 +89,27 @@ class StaticIndexProvider {
|
|
|
32
89
|
const label = this.config.name ? `${this.config.name} (${this.config.url})` : this.config.url;
|
|
33
90
|
warnings.push(`Registry ${label}: ${toErrorMessage(err)}`);
|
|
34
91
|
}
|
|
35
|
-
|
|
36
|
-
let assetHits;
|
|
37
|
-
if (options.includeAssets) {
|
|
38
|
-
const scored = scoreAssets(allKits, options.query, options.limit);
|
|
39
|
-
if (scored.length > 0)
|
|
40
|
-
assetHits = scored;
|
|
41
|
-
}
|
|
42
|
-
return { hits, assetHits, warnings: warnings.length > 0 ? warnings : undefined };
|
|
92
|
+
return allKits;
|
|
43
93
|
}
|
|
44
94
|
}
|
|
95
|
+
function hitToKitResult(hit) {
|
|
96
|
+
return {
|
|
97
|
+
id: hit.id,
|
|
98
|
+
title: hit.title,
|
|
99
|
+
summary: hit.description,
|
|
100
|
+
installRef: hit.installRef,
|
|
101
|
+
score: hit.score,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function assetHitToPreview(hit) {
|
|
105
|
+
return {
|
|
106
|
+
kitId: hit.stash.id,
|
|
107
|
+
type: hit.assetType,
|
|
108
|
+
name: hit.assetName,
|
|
109
|
+
summary: hit.description,
|
|
110
|
+
cloneRef: hit.action.replace(/^akm add\s+/, ""),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
45
113
|
// ── Self-register ───────────────────────────────────────────────────────────
|
|
46
114
|
registerProvider("static-index", (config) => new StaticIndexProvider(config));
|
|
47
115
|
// ── Index loading with cache ────────────────────────────────────────────────
|
|
@@ -58,7 +126,9 @@ async function loadIndex(entry) {
|
|
|
58
126
|
if (!response.ok) {
|
|
59
127
|
throw new Error(`HTTP ${response.status}`);
|
|
60
128
|
}
|
|
61
|
-
|
|
129
|
+
// Cap at 50 MB — registry indexes can grow large but unbounded
|
|
130
|
+
// responses from a compromised server would OOM us.
|
|
131
|
+
const data = await jsonWithByteCap(response, 50 * 1024 * 1024);
|
|
62
132
|
const index = parseRegistryIndex(data);
|
|
63
133
|
if (index) {
|
|
64
134
|
writeCachedIndex(cachePath, index);
|
|
@@ -120,20 +190,20 @@ export function parseRegistryIndex(data) {
|
|
|
120
190
|
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
121
191
|
return null;
|
|
122
192
|
const obj = data;
|
|
123
|
-
if (typeof obj.version !== "number" ||
|
|
193
|
+
if (typeof obj.version !== "number" || obj.version !== 3)
|
|
124
194
|
return null;
|
|
125
195
|
if (typeof obj.updatedAt !== "string")
|
|
126
196
|
return null;
|
|
127
|
-
if (!Array.isArray(obj.
|
|
197
|
+
if (!Array.isArray(obj.stashes))
|
|
128
198
|
return null;
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
return
|
|
199
|
+
const stashes = obj.stashes.flatMap((raw) => {
|
|
200
|
+
const stash = parseStashEntry(raw);
|
|
201
|
+
return stash ? [stash] : [];
|
|
132
202
|
});
|
|
133
|
-
return { version: obj.version, updatedAt: obj.updatedAt,
|
|
203
|
+
return { version: obj.version, updatedAt: obj.updatedAt, stashes };
|
|
134
204
|
}
|
|
135
|
-
// ──
|
|
136
|
-
function
|
|
205
|
+
// ── Stash entry parsing ───────────────────────────────────────────────────────
|
|
206
|
+
function parseStashEntry(raw) {
|
|
137
207
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
138
208
|
return null;
|
|
139
209
|
const obj = raw;
|
|
@@ -160,23 +230,23 @@ function parseKitEntry(raw) {
|
|
|
160
230
|
};
|
|
161
231
|
}
|
|
162
232
|
// ── Scoring ─────────────────────────────────────────────────────────────────
|
|
163
|
-
function scoreKits(
|
|
233
|
+
function scoreKits(stashes, query, limit) {
|
|
164
234
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
165
235
|
const scored = [];
|
|
166
|
-
for (const {
|
|
167
|
-
const score =
|
|
236
|
+
for (const { stash, registryName } of stashes) {
|
|
237
|
+
const score = scoreStash(stash, tokens);
|
|
168
238
|
if (score > 0) {
|
|
169
|
-
scored.push({
|
|
239
|
+
scored.push({ stash, registryName, score });
|
|
170
240
|
}
|
|
171
241
|
}
|
|
172
242
|
scored.sort((a, b) => b.score - a.score);
|
|
173
|
-
return scored.slice(0, limit).map(({
|
|
243
|
+
return scored.slice(0, limit).map(({ stash, registryName, score }) => toSearchHit(stash, score, registryName));
|
|
174
244
|
}
|
|
175
|
-
function
|
|
245
|
+
function scoreStash(stash, tokens) {
|
|
176
246
|
let score = 0;
|
|
177
|
-
const nameLower =
|
|
178
|
-
const descLower = (
|
|
179
|
-
const tagsLower = (
|
|
247
|
+
const nameLower = stash.name.toLowerCase();
|
|
248
|
+
const descLower = (stash.description ?? "").toLowerCase();
|
|
249
|
+
const tagsLower = (stash.tags ?? []).map((t) => t.toLowerCase());
|
|
180
250
|
for (const token of tokens) {
|
|
181
251
|
// Exact name match is strongest signal
|
|
182
252
|
if (nameLower === token) {
|
|
@@ -197,34 +267,34 @@ function scoreKit(kit, tokens) {
|
|
|
197
267
|
score += 0.2;
|
|
198
268
|
}
|
|
199
269
|
// Author match
|
|
200
|
-
if (
|
|
270
|
+
if (stash.author?.toLowerCase().includes(token)) {
|
|
201
271
|
score += 0.15;
|
|
202
272
|
}
|
|
203
273
|
}
|
|
204
274
|
// Normalize by token count so multi-word queries don't inflate scores
|
|
205
275
|
return tokens.length > 0 ? score / tokens.length : 0;
|
|
206
276
|
}
|
|
207
|
-
function toSearchHit(
|
|
277
|
+
function toSearchHit(stash, score, registryName) {
|
|
208
278
|
const metadata = {};
|
|
209
|
-
if (
|
|
210
|
-
metadata.version =
|
|
211
|
-
if (
|
|
212
|
-
metadata.author =
|
|
213
|
-
if (
|
|
214
|
-
metadata.license =
|
|
215
|
-
if (
|
|
216
|
-
metadata.assetTypes =
|
|
279
|
+
if (stash.latestVersion)
|
|
280
|
+
metadata.version = stash.latestVersion;
|
|
281
|
+
if (stash.author)
|
|
282
|
+
metadata.author = stash.author;
|
|
283
|
+
if (stash.license)
|
|
284
|
+
metadata.license = stash.license;
|
|
285
|
+
if (stash.assetTypes?.length)
|
|
286
|
+
metadata.assetTypes = stash.assetTypes.join(", ");
|
|
217
287
|
return {
|
|
218
|
-
source:
|
|
219
|
-
id:
|
|
220
|
-
title:
|
|
221
|
-
description:
|
|
222
|
-
ref:
|
|
223
|
-
installRef: buildInstallRef(
|
|
224
|
-
homepage:
|
|
288
|
+
source: stash.source,
|
|
289
|
+
id: stash.id,
|
|
290
|
+
title: stash.name,
|
|
291
|
+
description: stash.description,
|
|
292
|
+
ref: stash.ref,
|
|
293
|
+
installRef: buildInstallRef(stash.source, stash.ref),
|
|
294
|
+
homepage: stash.homepage,
|
|
225
295
|
score: Math.round(score * 1000) / 1000,
|
|
226
296
|
metadata,
|
|
227
|
-
curated:
|
|
297
|
+
curated: stash.curated,
|
|
228
298
|
registryName,
|
|
229
299
|
};
|
|
230
300
|
}
|
|
@@ -255,16 +325,16 @@ function parseAssetEntry(raw) {
|
|
|
255
325
|
};
|
|
256
326
|
}
|
|
257
327
|
// ── Asset-level scoring ─────────────────────────────────────────────────────
|
|
258
|
-
function scoreAssets(
|
|
328
|
+
function scoreAssets(stashes, query, limit) {
|
|
259
329
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
260
330
|
if (tokens.length === 0)
|
|
261
331
|
return [];
|
|
262
332
|
const scored = [];
|
|
263
|
-
for (const {
|
|
264
|
-
if (!
|
|
333
|
+
for (const { stash, registryName } of stashes) {
|
|
334
|
+
if (!stash.assets || stash.assets.length === 0)
|
|
265
335
|
continue;
|
|
266
|
-
const installRef = buildInstallRef(
|
|
267
|
-
for (const asset of
|
|
336
|
+
const installRef = buildInstallRef(stash.source, stash.ref);
|
|
337
|
+
for (const asset of stash.assets) {
|
|
268
338
|
const score = scoreAsset(asset, tokens);
|
|
269
339
|
if (score > 0) {
|
|
270
340
|
scored.push({
|
|
@@ -274,7 +344,7 @@ function scoreAssets(kits, query, limit) {
|
|
|
274
344
|
assetName: asset.name,
|
|
275
345
|
description: asset.description,
|
|
276
346
|
estimatedTokens: asset.estimatedTokens,
|
|
277
|
-
|
|
347
|
+
stash: { id: stash.id, name: stash.name },
|
|
278
348
|
registryName,
|
|
279
349
|
action: `akm add ${installRef}`,
|
|
280
350
|
score: Math.round(score * 1000) / 1000,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry provider interface (v1 architecture spec §3.1).
|
|
3
|
+
*
|
|
4
|
+
* A `RegistryProvider` is a read-only catalog that lists installable kits and
|
|
5
|
+
* (optionally) previews assets within them. It is *not* a `SourceProvider`:
|
|
6
|
+
* registry providers do not materialise files to disk — they only answer
|
|
7
|
+
* discovery queries.
|
|
8
|
+
*
|
|
9
|
+
* The two built-in registry providers at v1 are:
|
|
10
|
+
*
|
|
11
|
+
* - `static-index` — reads the v2 JSON index schema (the official akm registry
|
|
12
|
+
* and any static-hosted team registry). The v2 schema is owned by this
|
|
13
|
+
* provider, not by core akm.
|
|
14
|
+
* - `skills-sh` — wraps the skills.sh REST API.
|
|
15
|
+
*
|
|
16
|
+
* Context Hub is **not** a registry provider — it is an ordinary git repository
|
|
17
|
+
* recommended via the official static-index registry (see CLAUDE.md).
|
|
18
|
+
*
|
|
19
|
+
* Note: the simple `search()` method is the v0.6 surface and remains the
|
|
20
|
+
* primary entry point used by the orchestrator. The `searchKits` /
|
|
21
|
+
* `searchAssets` / `getKit` / `canHandle` methods are the v1-spec contract
|
|
22
|
+
* (§3.1) which built-in providers also implement so the orchestrator can be
|
|
23
|
+
* iterated cleanly post-Phase 6 without reaching into provider-specific shapes.
|
|
24
|
+
*/
|
|
25
|
+
export {};
|