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.
Files changed (118) hide show
  1. package/CHANGELOG.md +53 -5
  2. package/README.md +9 -9
  3. package/dist/cli.js +379 -1448
  4. package/dist/{completions.js → commands/completions.js} +1 -1
  5. package/dist/{config-cli.js → commands/config-cli.js} +109 -11
  6. package/dist/commands/curate.js +263 -0
  7. package/dist/{info.js → commands/info.js} +17 -11
  8. package/dist/{init.js → commands/init.js} +4 -4
  9. package/dist/{install-audit.js → commands/install-audit.js} +14 -2
  10. package/dist/{installed-kits.js → commands/installed-stashes.js} +122 -50
  11. package/dist/commands/migration-help.js +141 -0
  12. package/dist/{registry-search.js → commands/registry-search.js} +68 -9
  13. package/dist/commands/remember.js +178 -0
  14. package/dist/{stash-search.js → commands/search.js} +28 -69
  15. package/dist/{self-update.js → commands/self-update.js} +3 -3
  16. package/dist/{stash-show.js → commands/show.js} +106 -81
  17. package/dist/{stash-add.js → commands/source-add.js} +133 -67
  18. package/dist/{stash-clone.js → commands/source-clone.js} +15 -13
  19. package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
  20. package/dist/{vault.js → commands/vault.js} +43 -0
  21. package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
  22. package/dist/{asset-registry.js → core/asset-registry.js} +30 -6
  23. package/dist/{asset-spec.js → core/asset-spec.js} +13 -6
  24. package/dist/{common.js → core/common.js} +147 -50
  25. package/dist/{config.js → core/config.js} +288 -29
  26. package/dist/core/errors.js +90 -0
  27. package/dist/{frontmatter.js → core/frontmatter.js} +64 -8
  28. package/dist/{paths.js → core/paths.js} +4 -4
  29. package/dist/core/write-source.js +280 -0
  30. package/dist/{local-search.js → indexer/db-search.js} +49 -32
  31. package/dist/{db.js → indexer/db.js} +210 -81
  32. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  33. package/dist/{indexer.js → indexer/indexer.js} +153 -30
  34. package/dist/{manifest.js → indexer/manifest.js} +10 -10
  35. package/dist/{matchers.js → indexer/matchers.js} +4 -7
  36. package/dist/{metadata.js → indexer/metadata.js} +9 -5
  37. package/dist/{search-source.js → indexer/search-source.js} +97 -55
  38. package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
  39. package/dist/{walker.js → indexer/walker.js} +1 -1
  40. package/dist/{lockfile.js → integrations/lockfile.js} +29 -2
  41. package/dist/{llm.js → llm/client.js} +12 -48
  42. package/dist/llm/embedder.js +127 -0
  43. package/dist/llm/embedders/cache.js +47 -0
  44. package/dist/llm/embedders/local.js +152 -0
  45. package/dist/llm/embedders/remote.js +121 -0
  46. package/dist/llm/embedders/types.js +39 -0
  47. package/dist/llm/metadata-enhance.js +53 -0
  48. package/dist/output/cli-hints.js +301 -0
  49. package/dist/output/context.js +95 -0
  50. package/dist/{renderers.js → output/renderers.js} +57 -61
  51. package/dist/output/shapes.js +212 -0
  52. package/dist/output/text.js +520 -0
  53. package/dist/{registry-build-index.js → registry/build-index.js} +48 -32
  54. package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
  55. package/dist/registry/factory.js +33 -0
  56. package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
  57. package/dist/registry/providers/index.js +11 -0
  58. package/dist/{providers → registry/providers}/skills-sh.js +60 -4
  59. package/dist/{providers → registry/providers}/static-index.js +126 -56
  60. package/dist/registry/providers/types.js +25 -0
  61. package/dist/{registry-resolve.js → registry/resolve.js} +10 -6
  62. package/dist/{detect.js → setup/detect.js} +0 -27
  63. package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
  64. package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
  65. package/dist/{setup.js → setup/setup.js} +162 -129
  66. package/dist/setup/steps.js +45 -0
  67. package/dist/{kit-include.js → sources/include.js} +1 -1
  68. package/dist/sources/provider-factory.js +36 -0
  69. package/dist/sources/provider.js +21 -0
  70. package/dist/sources/providers/filesystem.js +35 -0
  71. package/dist/{stash-providers → sources/providers}/git.js +218 -28
  72. package/dist/{stash-providers → sources/providers}/index.js +4 -4
  73. package/dist/sources/providers/install-types.js +14 -0
  74. package/dist/sources/providers/npm.js +160 -0
  75. package/dist/sources/providers/provider-utils.js +173 -0
  76. package/dist/sources/providers/sync-from-ref.js +45 -0
  77. package/dist/sources/providers/tar-utils.js +154 -0
  78. package/dist/{stash-providers → sources/providers}/website.js +60 -20
  79. package/dist/{stash-resolve.js → sources/resolve.js} +13 -12
  80. package/dist/{wiki.js → wiki/wiki.js} +18 -17
  81. package/dist/{workflow-authoring.js → workflows/authoring.js} +48 -17
  82. package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
  83. package/dist/{workflow-db.js → workflows/db.js} +1 -1
  84. package/dist/workflows/document-cache.js +20 -0
  85. package/dist/workflows/parser.js +379 -0
  86. package/dist/workflows/renderer.js +78 -0
  87. package/dist/{workflow-runs.js → workflows/runs.js} +84 -30
  88. package/dist/workflows/schema.js +11 -0
  89. package/dist/workflows/validator.js +48 -0
  90. package/docs/README.md +30 -0
  91. package/docs/migration/release-notes/0.0.13.md +4 -0
  92. package/docs/migration/release-notes/0.1.0.md +6 -0
  93. package/docs/migration/release-notes/0.2.0.md +6 -0
  94. package/docs/migration/release-notes/0.3.0.md +5 -0
  95. package/docs/migration/release-notes/0.5.0.md +6 -0
  96. package/docs/migration/release-notes/0.6.0.md +75 -0
  97. package/docs/migration/release-notes/README.md +21 -0
  98. package/package.json +3 -2
  99. package/dist/embedder.js +0 -351
  100. package/dist/errors.js +0 -34
  101. package/dist/migration-help.js +0 -110
  102. package/dist/registry-factory.js +0 -19
  103. package/dist/registry-install.js +0 -532
  104. package/dist/ripgrep.js +0 -2
  105. package/dist/stash-provider-factory.js +0 -35
  106. package/dist/stash-provider.js +0 -1
  107. package/dist/stash-providers/filesystem.js +0 -41
  108. package/dist/stash-providers/openviking.js +0 -348
  109. package/dist/stash-providers/provider-utils.js +0 -11
  110. package/dist/stash-types.js +0 -1
  111. package/dist/workflow-markdown.js +0 -251
  112. /package/dist/{markdown.js → core/markdown.js} +0 -0
  113. /package/dist/{warn.js → core/warn.js} +0 -0
  114. /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
  115. /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
  116. /package/dist/{github.js → integrations/github.js} +0 -0
  117. /package/dist/{registry-provider.js → registry/types.js} +0 -0
  118. /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 "./common";
5
- import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "./github";
6
- import { copyIncludedPaths, findNearestIncludeConfig } from "./kit-include";
7
- import { generateMetadataFlat, loadStashFile } from "./metadata";
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-kit"];
15
- const GITHUB_TOPICS = ["akm-kit"];
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 kits = deduplicateKits([...manualKits, ...npmKits, ...githubKits]).sort((a, b) => a.name.localeCompare(b.name));
38
+ const stashes = deduplicateStashes([...manualKits, ...npmKits, ...githubKits]).sort((a, b) => a.name.localeCompare(b.name));
29
39
  const index = {
30
- version: 2,
40
+ version: 3,
31
41
  updatedAt: new Date().toISOString(),
32
- kits,
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: kits.length,
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 kits = [];
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
- kits.push(normalizeKit({
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 kits;
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 kits = [];
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
- kits.push(normalizeKit({
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 kits;
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).kits;
273
- const parsed = parseRegistryIndex({ version: 2, updatedAt: new Date().toISOString(), kits: candidateKits });
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.kits.map((kit) => normalizeKit({ ...kit, curated: kit.curated ?? true }));
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 (await response.json());
304
+ return jsonWithByteCap(response, BUILD_INDEX_JSON_BYTE_CAP);
289
305
  }
290
- function deduplicateKits(kits) {
306
+ function deduplicateStashes(stashes) {
291
307
  const byId = new Map();
292
- for (const kit of kits) {
293
- const existing = byId.get(kit.id);
294
- byId.set(kit.id, existing ? mergeEntries(existing, kit) : kit);
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 normalizeKit({
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 normalizeKit(kit) {
347
- const assets = kit.assets ? sortAssets(kit.assets) : undefined;
362
+ function normalizeStash(stash) {
363
+ const assets = stash.assets ? sortAssets(stash.assets) : undefined;
348
364
  return {
349
- ...kit,
350
- ...(kit.tags && kit.tags.length > 0 ? { tags: kit.tags } : {}),
351
- ...(kit.assetTypes && kit.assetTypes.length > 0 ? { assetTypes: kit.assetTypes } : {}),
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-factory.ts (kit discovery) and stash-provider-factory.ts
6
- * (stash source providers) are built on this utility.
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
+ }
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { parseRegistryRef } from "./registry-resolve";
2
+ import { parseRegistryRef } from "./resolve";
3
3
  /**
4
4
  * Given an origin string (from an AssetRef) and the full list of stash
5
5
  * sources, return the subset of sources to search.
@@ -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 "../common";
4
- import { getRegistryIndexCacheDir } from "../paths";
5
- import { registerProvider } from "../registry-factory";
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
- kit: { id: `skills-sh:${entry.id}`, name: entry.name },
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 "../common";
4
- import { asString } from "../github";
5
- import { getRegistryIndexCacheDir } from "../paths";
6
- import { registerProvider } from "../registry-factory";
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 kit of index.kits) {
27
- allKits.push({ kit, registryName: regName });
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
- const hits = scoreKits(allKits, options.query, options.limit);
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
- const data = (await response.json());
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" || (obj.version !== 1 && obj.version !== 2))
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.kits))
197
+ if (!Array.isArray(obj.stashes))
128
198
  return null;
129
- const kits = obj.kits.flatMap((raw) => {
130
- const kit = parseKitEntry(raw);
131
- return kit ? [kit] : [];
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, kits };
203
+ return { version: obj.version, updatedAt: obj.updatedAt, stashes };
134
204
  }
135
- // ── Kit entry parsing ───────────────────────────────────────────────────────
136
- function parseKitEntry(raw) {
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(kits, query, limit) {
233
+ function scoreKits(stashes, query, limit) {
164
234
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
165
235
  const scored = [];
166
- for (const { kit, registryName } of kits) {
167
- const score = scoreKit(kit, tokens);
236
+ for (const { stash, registryName } of stashes) {
237
+ const score = scoreStash(stash, tokens);
168
238
  if (score > 0) {
169
- scored.push({ kit, registryName, score });
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(({ kit, registryName, score }) => toSearchHit(kit, score, registryName));
243
+ return scored.slice(0, limit).map(({ stash, registryName, score }) => toSearchHit(stash, score, registryName));
174
244
  }
175
- function scoreKit(kit, tokens) {
245
+ function scoreStash(stash, tokens) {
176
246
  let score = 0;
177
- const nameLower = kit.name.toLowerCase();
178
- const descLower = (kit.description ?? "").toLowerCase();
179
- const tagsLower = (kit.tags ?? []).map((t) => t.toLowerCase());
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 (kit.author?.toLowerCase().includes(token)) {
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(kit, score, registryName) {
277
+ function toSearchHit(stash, score, registryName) {
208
278
  const metadata = {};
209
- if (kit.latestVersion)
210
- metadata.version = kit.latestVersion;
211
- if (kit.author)
212
- metadata.author = kit.author;
213
- if (kit.license)
214
- metadata.license = kit.license;
215
- if (kit.assetTypes?.length)
216
- metadata.assetTypes = kit.assetTypes.join(", ");
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: kit.source,
219
- id: kit.id,
220
- title: kit.name,
221
- description: kit.description,
222
- ref: kit.ref,
223
- installRef: buildInstallRef(kit.source, kit.ref),
224
- homepage: kit.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: kit.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(kits, query, limit) {
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 { kit, registryName } of kits) {
264
- if (!kit.assets || kit.assets.length === 0)
333
+ for (const { stash, registryName } of stashes) {
334
+ if (!stash.assets || stash.assets.length === 0)
265
335
  continue;
266
- const installRef = buildInstallRef(kit.source, kit.ref);
267
- for (const asset of kit.assets) {
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
- kit: { id: kit.id, name: kit.name },
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 {};