akm-cli 0.6.0-rc1 → 0.6.0

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 (108) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +9 -9
  3. package/dist/cli.js +199 -114
  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/{curate.js → commands/curate.js} +8 -3
  7. package/dist/{info.js → commands/info.js} +15 -9
  8. package/dist/{init.js → commands/init.js} +4 -4
  9. package/dist/{install-audit.js → commands/install-audit.js} +4 -7
  10. package/dist/{installed-stashes.js → commands/installed-stashes.js} +77 -31
  11. package/dist/{migration-help.js → commands/migration-help.js} +2 -2
  12. package/dist/{registry-search.js → commands/registry-search.js} +8 -6
  13. package/dist/{remember.js → commands/remember.js} +55 -49
  14. package/dist/{stash-search.js → commands/search.js} +28 -69
  15. package/dist/{self-update.js → commands/self-update.js} +69 -3
  16. package/dist/{stash-show.js → commands/show.js} +104 -84
  17. package/dist/{stash-add.js → commands/source-add.js} +42 -32
  18. package/dist/{stash-clone.js → commands/source-clone.js} +12 -10
  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} +1 -1
  23. package/dist/{asset-spec.js → core/asset-spec.js} +1 -1
  24. package/dist/{config.js → core/config.js} +133 -56
  25. package/dist/core/errors.js +90 -0
  26. package/dist/{frontmatter.js → core/frontmatter.js} +5 -3
  27. package/dist/core/write-source.js +280 -0
  28. package/dist/{db-search.js → indexer/db-search.js} +25 -19
  29. package/dist/{db.js → indexer/db.js} +79 -47
  30. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  31. package/dist/{indexer.js → indexer/indexer.js} +132 -33
  32. package/dist/{manifest.js → indexer/manifest.js} +10 -10
  33. package/dist/{matchers.js → indexer/matchers.js} +3 -6
  34. package/dist/{metadata.js → indexer/metadata.js} +9 -5
  35. package/dist/{search-source.js → indexer/search-source.js} +52 -41
  36. package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
  37. package/dist/{walker.js → indexer/walker.js} +1 -1
  38. package/dist/{lockfile.js → integrations/lockfile.js} +1 -1
  39. package/dist/{llm-client.js → llm/client.js} +1 -1
  40. package/dist/{embedders → llm/embedders}/local.js +2 -2
  41. package/dist/{embedders → llm/embedders}/remote.js +1 -1
  42. package/dist/{embedders → llm/embedders}/types.js +1 -1
  43. package/dist/{metadata-enhance.js → llm/metadata-enhance.js} +2 -2
  44. package/dist/{cli-hints.js → output/cli-hints.js} +3 -0
  45. package/dist/{output-context.js → output/context.js} +21 -3
  46. package/dist/{renderers.js → output/renderers.js} +9 -65
  47. package/dist/{output-shapes.js → output/shapes.js} +18 -4
  48. package/dist/{output-text.js → output/text.js} +2 -2
  49. package/dist/{registry-build-index.js → registry/build-index.js} +16 -7
  50. package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
  51. package/dist/registry/factory.js +33 -0
  52. package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
  53. package/dist/{providers → registry/providers}/index.js +1 -1
  54. package/dist/{providers → registry/providers}/skills-sh.js +59 -3
  55. package/dist/{providers → registry/providers}/static-index.js +80 -12
  56. package/dist/registry/providers/types.js +25 -0
  57. package/dist/{registry-resolve.js → registry/resolve.js} +3 -3
  58. package/dist/{detect.js → setup/detect.js} +0 -27
  59. package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
  60. package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
  61. package/dist/{setup.js → setup/setup.js} +16 -56
  62. package/dist/{stash-include.js → sources/include.js} +1 -1
  63. package/dist/sources/provider-factory.js +36 -0
  64. package/dist/sources/provider.js +21 -0
  65. package/dist/sources/providers/filesystem.js +35 -0
  66. package/dist/{stash-providers → sources/providers}/git.js +53 -64
  67. package/dist/{stash-providers → sources/providers}/index.js +3 -4
  68. package/dist/sources/providers/install-types.js +14 -0
  69. package/dist/{stash-providers → sources/providers}/npm.js +42 -41
  70. package/dist/{stash-providers → sources/providers}/provider-utils.js +3 -3
  71. package/dist/{stash-providers → sources/providers}/sync-from-ref.js +2 -2
  72. package/dist/{stash-providers → sources/providers}/tar-utils.js +11 -8
  73. package/dist/{stash-providers → sources/providers}/website.js +29 -65
  74. package/dist/{stash-resolve.js → sources/resolve.js} +8 -7
  75. package/dist/{wiki.js → wiki/wiki.js} +34 -18
  76. package/dist/{workflow-authoring.js → workflows/authoring.js} +37 -14
  77. package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
  78. package/dist/{workflow-db.js → workflows/db.js} +1 -1
  79. package/dist/workflows/document-cache.js +20 -0
  80. package/dist/workflows/parser.js +379 -0
  81. package/dist/workflows/renderer.js +78 -0
  82. package/dist/{workflow-runs.js → workflows/runs.js} +72 -28
  83. package/dist/workflows/schema.js +11 -0
  84. package/dist/workflows/validator.js +48 -0
  85. package/docs/migration/release-notes/0.6.0.md +91 -23
  86. package/package.json +1 -1
  87. package/dist/errors.js +0 -45
  88. package/dist/llm.js +0 -16
  89. package/dist/registry-factory.js +0 -19
  90. package/dist/ripgrep.js +0 -2
  91. package/dist/stash-provider-factory.js +0 -35
  92. package/dist/stash-provider.js +0 -3
  93. package/dist/stash-providers/filesystem.js +0 -71
  94. package/dist/stash-providers/openviking.js +0 -348
  95. package/dist/stash-types.js +0 -1
  96. package/dist/workflow-markdown.js +0 -260
  97. /package/dist/{common.js → core/common.js} +0 -0
  98. /package/dist/{markdown.js → core/markdown.js} +0 -0
  99. /package/dist/{paths.js → core/paths.js} +0 -0
  100. /package/dist/{warn.js → core/warn.js} +0 -0
  101. /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
  102. /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
  103. /package/dist/{github.js → integrations/github.js} +0 -0
  104. /package/dist/{embedder.js → llm/embedder.js} +0 -0
  105. /package/dist/{embedders → llm/embedders}/cache.js +0 -0
  106. /package/dist/{registry-provider.js → registry/types.js} +0 -0
  107. /package/dist/{setup-steps.js → setup/steps.js} +0 -0
  108. /package/dist/{registry-types.js → sources/types.js} +0 -0
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Initialized from `cli.ts` before `runMain`.
10
10
  */
11
- import { UsageError } from "./errors";
11
+ import { UsageError } from "../core/errors";
12
12
  export const OUTPUT_FORMATS = ["json", "yaml", "text", "jsonl"];
13
13
  export const DETAIL_LEVELS = ["brief", "normal", "full", "summary", "agent"];
14
14
  export function parseOutputFormat(value) {
@@ -16,14 +16,14 @@ export function parseOutputFormat(value) {
16
16
  return undefined;
17
17
  if (OUTPUT_FORMATS.includes(value))
18
18
  return value;
19
- throw new UsageError(`Invalid value for --format: ${value}. Expected one of: ${OUTPUT_FORMATS.join("|")}`);
19
+ throw new UsageError(`Invalid value for --format: ${value}. Expected one of: ${OUTPUT_FORMATS.join("|")}`, "INVALID_FORMAT_VALUE");
20
20
  }
21
21
  export function parseDetailLevel(value) {
22
22
  if (!value)
23
23
  return undefined;
24
24
  if (DETAIL_LEVELS.includes(value))
25
25
  return value;
26
- throw new UsageError(`Invalid value for --detail: ${value}. Expected one of: ${DETAIL_LEVELS.join("|")}`);
26
+ throw new UsageError(`Invalid value for --detail: ${value}. Expected one of: ${DETAIL_LEVELS.join("|")}`, "INVALID_DETAIL_VALUE");
27
27
  }
28
28
  export function parseFlagValue(argv, flag) {
29
29
  for (let i = 0; i < argv.length; i++) {
@@ -38,6 +38,24 @@ export function parseFlagValue(argv, flag) {
38
38
  export function hasBooleanFlag(argv, flag) {
39
39
  return argv.some((arg) => arg === flag || arg === `${flag}=true`);
40
40
  }
41
+ /**
42
+ * Read a hyphenated arg out of citty's parsed `args` object.
43
+ *
44
+ * citty does not auto-camelise hyphenated arg keys (see `--max-pages`,
45
+ * `--with-sources` for the existing convention), so command handlers end up
46
+ * casting `args` to a string-indexed record at every read site. This helper
47
+ * encapsulates the cast.
48
+ */
49
+ export function getHyphenatedArg(args, key) {
50
+ if (typeof args !== "object" || args === null)
51
+ return undefined;
52
+ const value = args[key];
53
+ return value === undefined ? undefined : value;
54
+ }
55
+ /** Boolean variant of {@link getHyphenatedArg} for `--<flag>` switches. */
56
+ export function getHyphenatedBoolean(args, key) {
57
+ return Boolean(getHyphenatedArg(args, key));
58
+ }
41
59
  /**
42
60
  * Resolve output mode from a synthetic argv array and config defaults.
43
61
  * Pure function — no IO. Suitable for unit tests.
@@ -8,15 +8,13 @@
8
8
  */
9
9
  import fs from "node:fs";
10
10
  import path from "node:path";
11
- import { hasErrnoCode } from "./common";
12
- import { UsageError } from "./errors";
13
- import { registerRenderer } from "./file-context";
14
- import { parseFrontmatter, toStringOrUndefined } from "./frontmatter";
15
- import { extractFrontmatterOnly, extractLineRange, extractSection, formatToc, parseMarkdownToc } from "./markdown";
16
- import { extractDescriptionFromComments, loadStashFile } from "./metadata";
17
- import { makeAssetRef } from "./stash-ref";
18
- import { listKeys as listVaultKeys } from "./vault";
19
- import { parseWorkflowMarkdown, WorkflowValidationError } from "./workflow-markdown";
11
+ import { listKeys as listVaultKeys } from "../commands/vault";
12
+ import { hasErrnoCode } from "../core/common";
13
+ import { parseFrontmatter, toStringOrUndefined } from "../core/frontmatter";
14
+ import { extractFrontmatterOnly, extractLineRange, extractSection, formatToc, parseMarkdownToc, } from "../core/markdown";
15
+ import { registerRenderer } from "../indexer/file-context";
16
+ import { extractDescriptionFromComments, loadStashFile } from "../indexer/metadata";
17
+ import { buildWorkflowAction, workflowMdRenderer } from "../workflows/renderer";
20
18
  // ── Interpreter auto-detection map ───────────────────────────────────────────
21
19
  const INTERPRETER_MAP = {
22
20
  ".sh": "bash",
@@ -153,12 +151,7 @@ function deriveName(ctx) {
153
151
  const ext = path.extname(ctx.relPath);
154
152
  return ext ? ctx.relPath.slice(0, -ext.length) : ctx.relPath;
155
153
  }
156
- function shellQuote(value) {
157
- return `'${value.replace(/'/g, `'\\''`)}'`;
158
- }
159
- export function buildWorkflowAction(ref) {
160
- return `Resume the active run or start a new run with \`akm workflow next ${shellQuote(ref)}\`.`;
161
- }
154
+ export { buildWorkflowAction };
162
155
  /**
163
156
  * Load the matching StashEntry for a file path from the directory's .stash.json.
164
157
  */
@@ -465,56 +458,7 @@ const memoryMdRenderer = {
465
458
  },
466
459
  };
467
460
  // ── 6. workflow-md ───────────────────────────────────────────────────────────
468
- const workflowMdRenderer = {
469
- name: "workflow-md",
470
- buildShowResponse(ctx) {
471
- const name = deriveName(ctx);
472
- const workflow = parseWorkflowForRendering(ctx.content());
473
- const ref = makeAssetRef("workflow", name, ctx.origin);
474
- return {
475
- type: "workflow",
476
- name,
477
- path: ctx.absPath,
478
- action: buildWorkflowAction(ref),
479
- description: workflow.description,
480
- workflowTitle: workflow.title,
481
- parameters: workflow.parameters?.map((parameter) => parameter.name),
482
- workflowParameters: workflow.parameters,
483
- steps: workflow.steps,
484
- };
485
- },
486
- extractMetadata(entry, ctx) {
487
- const workflow = parseWorkflowForRendering(ctx.content());
488
- const hints = new Set(entry.searchHints ?? []);
489
- hints.add(workflow.title);
490
- for (const step of workflow.steps) {
491
- hints.add(step.title);
492
- hints.add(step.id);
493
- hints.add(step.instructions);
494
- for (const criterion of step.completionCriteria ?? []) {
495
- hints.add(criterion);
496
- }
497
- }
498
- entry.searchHints = Array.from(hints).filter(Boolean);
499
- if (workflow.parameters?.length) {
500
- entry.parameters = workflow.parameters.map((parameter) => ({
501
- name: parameter.name,
502
- ...(parameter.description ? { description: parameter.description } : {}),
503
- }));
504
- }
505
- },
506
- };
507
- function parseWorkflowForRendering(content) {
508
- try {
509
- return parseWorkflowMarkdown(content);
510
- }
511
- catch (error) {
512
- if (error instanceof WorkflowValidationError) {
513
- throw new UsageError(error.message);
514
- }
515
- throw error;
516
- }
517
- }
461
+ // Defined in src/workflows/renderer.ts and imported above.
518
462
  // ── 7. script-source ─────────────────────────────────────────────────────────
519
463
  const scriptSourceRenderer = {
520
464
  name: "script-source",
@@ -81,10 +81,20 @@ export function shapeAssetHit(hit, detail) {
81
81
  }
82
82
  export function shapeSearchHit(hit, detail) {
83
83
  if (hit.type === "registry") {
84
- if (detail === "brief")
85
- return pickFields(hit, ["name", "action"]);
84
+ if (detail === "brief") {
85
+ // RegistrySearchHit uses `title` (not `name`); always project installRef
86
+ // and score so callers can use the result without --detail full (QA #28).
87
+ const out = pickFields(hit, ["title", "name", "installRef", "score"]);
88
+ // Normalise: if only title exists, expose it as `name` for consistency
89
+ if (out.title && !out.name)
90
+ out.name = out.title;
91
+ return out;
92
+ }
86
93
  if (detail === "normal") {
87
- return capDescription(pickFields(hit, ["name", "description", "action", "curated"]), NORMAL_DESCRIPTION_LIMIT);
94
+ const out = capDescription(pickFields(hit, ["title", "name", "description", "action", "installRef", "score", "curated"]), NORMAL_DESCRIPTION_LIMIT);
95
+ if (out.title && !out.name)
96
+ out.name = out.title;
97
+ return out;
88
98
  }
89
99
  return hit;
90
100
  }
@@ -176,6 +186,10 @@ export function shapeShowOutput(result, detail, forAgent = false) {
176
186
  "cwd",
177
187
  "keys",
178
188
  "comments",
189
+ // path and editable are always projected so JSON consumers can locate and
190
+ // edit the asset without needing --detail full (QA #7).
191
+ "path",
192
+ "editable",
179
193
  ]);
180
194
  if (detail !== "full") {
181
195
  return base;
@@ -183,7 +197,7 @@ export function shapeShowOutput(result, detail, forAgent = false) {
183
197
  return {
184
198
  schemaVersion: 1,
185
199
  ...base,
186
- ...pickFields(result, ["path", "editable", "editHint"]),
200
+ ...pickFields(result, ["editHint"]),
187
201
  };
188
202
  }
189
203
  export function pickFields(source, fields) {
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Pure functions — no IO.
7
7
  */
8
- import { formatInstallAuditSummary } from "./install-audit";
8
+ import { formatInstallAuditSummary } from "../commands/install-audit";
9
9
  export function outputJsonl(command, shaped) {
10
10
  if (command === "search" || command === "registry-search") {
11
11
  const r = shaped;
@@ -433,7 +433,7 @@ export function formatWikiRemovePlain(r) {
433
433
  const preserved = r.preservedRaw === true;
434
434
  const removed = Array.isArray(r.removed) ? r.removed.length : 0;
435
435
  const base = `Removed wiki ${String(r.name ?? "?")} (${removed} path(s))`;
436
- return preserved ? `${base}; preserved ${String(r.rawPath ?? "raw/")}` : base;
436
+ return preserved ? `${base}; raw/ preserved at ${String(r.rawPath ?? "raw/")}` : base;
437
437
  }
438
438
  export function formatWikiPagesPlain(r) {
439
439
  const pages = Array.isArray(r.pages) ? r.pages : [];
@@ -1,14 +1,23 @@
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, jsonWithByteCap } from "./common";
5
- import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "./github";
6
- 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";
7
20
  import { parseRegistryIndex } from "./providers/static-index";
8
- import { copyIncludedPaths, findNearestIncludeConfig } from "./stash-include";
9
- import { detectStashRoot } from "./stash-providers/provider-utils";
10
- import { extractTarGzSecure } from "./stash-providers/tar-utils";
11
- import { walkStashFlat } from "./walker";
12
21
  const DEFAULT_NPM_REGISTRY_BASE = "https://registry.npmjs.org";
13
22
  const DEFAULT_MANUAL_ENTRIES_PATH = path.resolve("manual-entries.json");
14
23
  const DEFAULT_OUTPUT_PATH = path.resolve("index.json");
@@ -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 (stash 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.
@@ -5,7 +5,7 @@
5
5
  * providers with the provider registry. This replaces the individual
6
6
  * side-effect imports that were duplicated in registry-search.ts.
7
7
  *
8
- * Mirrors the pattern used by `stash-providers/index.ts`.
8
+ * Mirrors the pattern used by `sources/providers/index.ts`.
9
9
  */
10
10
  import "./static-index";
11
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);
@@ -1,9 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { fetchWithRetry, jsonWithByteCap, 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,6 +18,63 @@ 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);
@@ -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 ────────────────────────────────────────────────
@@ -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 {};
@@ -3,9 +3,9 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
- import { fetchWithRetry, jsonWithByteCap } from "./common";
7
- import { UsageError } from "./errors";
8
- import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "./github";
6
+ import { fetchWithRetry, jsonWithByteCap } from "../core/common";
7
+ import { UsageError } from "../core/errors";
8
+ import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "../integrations/github";
9
9
  /**
10
10
  * Validate that a URL is safe to pass to git.
11
11
  * Allowlists https:, http:, ssh:, git: schemes and git@ SSH shorthand.
@@ -91,30 +91,3 @@ export function detectAgentPlatforms() {
91
91
  path: path.join(home, p.relPath),
92
92
  }));
93
93
  }
94
- // ── OpenViking Detection ────────────────────────────────────────────────────
95
- /**
96
- * Check if an OpenViking server is reachable at the given URL.
97
- * Uses the lightweight /api/v1/fs/stat endpoint (GET) rather than
98
- * the search endpoint which requires a running search index.
99
- */
100
- export async function detectOpenViking(url) {
101
- const normalized = url.replace(/\/+$/, "");
102
- try {
103
- // Any HTTP response (even non-2xx) from the API endpoint means the server is reachable.
104
- // Only network errors / timeouts indicate the server is truly unavailable.
105
- await fetch(`${normalized}/api/v1/fs/stat?uri=${encodeURIComponent("viking://")}`, {
106
- signal: AbortSignal.timeout(5000),
107
- });
108
- return { available: true, url: normalized };
109
- }
110
- catch {
111
- // stat endpoint unreachable — try root URL as fallback
112
- try {
113
- await fetch(normalized, { signal: AbortSignal.timeout(5000) });
114
- return { available: true, url: normalized };
115
- }
116
- catch {
117
- return { available: false, url: normalized };
118
- }
119
- }
120
- }
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { IS_WINDOWS } from "./common";
4
+ import { IS_WINDOWS } from "../core/common";
5
5
  import { RG_BINARY, resolveRg } from "./ripgrep-resolve";
6
6
  /**
7
7
  * Platform and architecture detection for ripgrep binary downloads.
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { IS_WINDOWS } from "./common";
4
- import { getBinDir } from "./paths";
3
+ import { IS_WINDOWS } from "../core/common";
4
+ import { getBinDir } from "../core/paths";
5
5
  export const RG_BINARY = IS_WINDOWS ? "rg.exe" : "rg";
6
6
  function canExecute(filePath) {
7
7
  if (!fs.existsSync(filePath))