akm-cli 0.6.0-rc1 → 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 (108) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +9 -9
  3. package/dist/cli.js +181 -111
  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} +3 -3
  16. package/dist/{stash-show.js → commands/show.js} +103 -78
  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} +79 -31
  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} +70 -47
  30. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  31. package/dist/{indexer.js → indexer/indexer.js} +123 -31
  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} +1 -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} +1 -1
  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} +12 -11
  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 +69 -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
@@ -1,10 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { deriveCanonicalAssetName, deriveCanonicalAssetNameFromStashRoot, isRelevantAssetFile } from "./asset-spec";
4
- import { isAssetType } from "./common";
3
+ import { deriveCanonicalAssetName, deriveCanonicalAssetNameFromStashRoot, isRelevantAssetFile, } from "../core/asset-spec";
4
+ import { isAssetType } from "../core/common";
5
+ import { parseFrontmatter, toStringOrUndefined } from "../core/frontmatter";
6
+ import { warn } from "../core/warn";
5
7
  import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "./file-context";
6
- import { parseFrontmatter, toStringOrUndefined } from "./frontmatter";
7
- import { warn } from "./warn";
8
8
  // ── Load / Write ────────────────────────────────────────────────────────────
9
9
  const STASH_FILENAME = ".stash.json";
10
10
  export function stashFilePath(dirPath) {
@@ -568,7 +568,11 @@ export async function generateMetadataFlat(stashRoot, files) {
568
568
  }
569
569
  function buildMetadataSkipWarning(filePath, assetType, error) {
570
570
  const detail = error instanceof Error ? error.message : String(error);
571
- const warning = `Skipped malformed ${assetType} asset at ${filePath}: ${detail}`;
571
+ // Workflow errors are already multi-line `path:line message` blocks; print
572
+ // them as-is so the author sees a flat list without a redundant prefix.
573
+ const warning = assetType === "workflow"
574
+ ? `Skipped workflow ${filePath}:\n${detail}`
575
+ : `Skipped malformed ${assetType} asset at ${filePath}: ${detail}`;
572
576
  warn(warning);
573
577
  return warning;
574
578
  }
@@ -1,10 +1,14 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { resolveStashDir } from "./common";
4
- import { loadConfig } from "./config";
5
- import { ensureGitMirror, getCachePaths, parseGitRepoUrl } from "./stash-providers/git";
6
- import { ensureWebsiteMirror, getCachePaths as getWebsiteCachePaths } from "./stash-providers/website";
7
- import { warn } from "./warn";
3
+ import { resolveStashDir } from "../core/common";
4
+ import { loadConfig } from "../core/config";
5
+ import { resolveSourceProviderFactory } from "../sources/provider-factory";
6
+ // Eager side-effect imports so all built-in source providers self-register
7
+ // before resolveEntryContentDir() runs.
8
+ import "../sources/providers/index";
9
+ import { warn } from "../core/warn";
10
+ import { ensureGitMirror, getCachePaths, parseGitRepoUrl } from "../sources/providers/git";
11
+ import { ensureWebsiteMirror } from "../sources/providers/website";
8
12
  // Legacy "context-hub" / "github" type aliases are normalized to "git" at
9
13
  // config-load time (see src/config.ts), so this set only contains the canonical
10
14
  // type.
@@ -17,7 +21,7 @@ const GIT_STASH_TYPES = new Set(["git"]);
17
21
  * 1. The primary stash directory (the entry marked `primary: true`, or the
18
22
  * legacy top-level `stashDir`). Always emitted, even when the directory
19
23
  * does not yet exist on disk, so callers can use it as the clone target.
20
- * 2. Each entry in `config.stashes[]` (in declared order), excluding the
24
+ * 2. Each entry in `config.sources ?? config.stashes[]` (in declared order), excluding the
21
25
  * one already emitted as the primary.
22
26
  * 3. Each entry in `config.installed[]` (registry-managed stashes).
23
27
  *
@@ -25,7 +29,7 @@ const GIT_STASH_TYPES = new Set(["git"]);
25
29
  * for each provider kind. Disabled entries (`enabled: false`) and entries
26
30
  * whose disk path doesn't exist are filtered after deduplication.
27
31
  */
28
- export function resolveStashSources(overrideStashDir, existingConfig) {
32
+ export function resolveSourceEntries(overrideStashDir, existingConfig) {
29
33
  const stashDir = overrideStashDir ?? resolveStashDir();
30
34
  const config = existingConfig ?? loadConfig();
31
35
  const sources = [{ path: stashDir }];
@@ -49,7 +53,7 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
49
53
  // (1) + (2) Single pass over declared stashes — primary first if present,
50
54
  // then the rest in declared order. The primary's directory is already
51
55
  // injected as `sources[0]` above, so we only need to dedupe the source set.
52
- const stashes = config.stashes ?? [];
56
+ const stashes = config.sources ?? config.stashes ?? [];
53
57
  const primaryIdx = stashes.findIndex((entry) => entry.primary === true);
54
58
  const ordered = [];
55
59
  if (primaryIdx >= 0) {
@@ -78,43 +82,54 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
78
82
  }
79
83
  /**
80
84
  * Resolve the content directory the indexer should walk for a given config
81
- * entry. Returns `undefined` if the entry has no walkable content (e.g. an
82
- * `openviking` remote stash) so the caller can skip it.
85
+ * entry. Returns `undefined` if the entry has no walkable content
86
+ * so the caller can skip it.
87
+ *
88
+ * Single source of truth: each provider owns its own path. We instantiate the
89
+ * registered {@link import("../sources/provider").SourceProvider} for the entry
90
+ * and call `provider.path()`. This replaces the old per-kind switch ladder
91
+ * (filesystem path / git cache / website cache) that lived here in 0.6.0 —
92
+ * see spec §10 step 4 and §7 "Removed from 0.6.0".
93
+ *
94
+ * The git case still does one extra step: the provider returns the cloned
95
+ * repo dir, but the indexer walks the `content/` subdirectory inside it.
96
+ * That convention is part of the akm content layout, not a provider concern,
97
+ * so it stays here.
83
98
  */
84
99
  function resolveEntryContentDir(entry) {
85
- if (entry.type === "filesystem" && entry.path) {
86
- return entry.path;
100
+ const factory = resolveSourceProviderFactory(entry.type);
101
+ if (!factory)
102
+ return undefined;
103
+ let provider;
104
+ try {
105
+ provider = factory(entry);
87
106
  }
88
- if (GIT_STASH_TYPES.has(entry.type) && entry.url) {
89
- try {
90
- const repo = parseGitRepoUrl(entry.url);
91
- const cachePaths = getCachePaths(repo.canonicalUrl);
92
- // The content/ subdirectory inside the extracted repo is the actual
93
- // stash root containing DOC.md / SKILL.md files that the walker indexes.
94
- return path.join(cachePaths.repoDir, "content");
95
- }
96
- catch (err) {
97
- warn(`Warning: failed to resolve git stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
98
- return undefined;
99
- }
107
+ catch (err) {
108
+ warn(`Warning: failed to construct ${entry.type} source provider for "${entry.name ?? entry.url ?? entry.path}": ${err instanceof Error ? err.message : String(err)}`);
109
+ return undefined;
100
110
  }
101
- if (entry.type === "website" && entry.url) {
102
- try {
103
- return getWebsiteCachePaths(entry.url).stashDir;
104
- }
105
- catch (err) {
106
- warn(`Warning: failed to resolve website stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
107
- return undefined;
108
- }
111
+ let dir;
112
+ try {
113
+ dir = provider.path();
109
114
  }
110
- // Remote-only providers (openviking) have no walkable directory.
111
- return undefined;
115
+ catch (err) {
116
+ warn(`Warning: failed to resolve ${entry.type} source path for "${entry.name ?? entry.url ?? entry.path}": ${err instanceof Error ? err.message : String(err)}`);
117
+ return undefined;
118
+ }
119
+ // Git providers expose the cloned repo root as their path. The akm content
120
+ // layout puts indexable files under `<repo>/content/`, so the walker needs
121
+ // that subdirectory. This is a content-layout convention, not a provider
122
+ // capability — keep it here.
123
+ if (GIT_STASH_TYPES.has(entry.type)) {
124
+ return path.join(dir, "content");
125
+ }
126
+ return dir;
112
127
  }
113
128
  /**
114
129
  * Convenience: returns just the directory paths, preserving priority order.
115
130
  */
116
131
  export function resolveAllStashDirs(overrideStashDir) {
117
- return resolveStashSources(overrideStashDir).map((s) => s.path);
132
+ return resolveSourceEntries(overrideStashDir).map((s) => s.path);
118
133
  }
119
134
  /**
120
135
  * Find which source a file path belongs to.
@@ -204,10 +219,10 @@ function isValidDirectory(dir) {
204
219
  /**
205
220
  * Ensure all cache-backed stash providers are refreshed so their cache
206
221
  * directories exist on disk. Must be called (async) before
207
- * `resolveStashSources()` so the content directories pass the
222
+ * `resolveSourceEntries()` so the content directories pass the
208
223
  * `isValidDirectory()` check.
209
224
  */
210
- export async function ensureStashCaches(config) {
225
+ export async function ensureSourceCaches(config) {
211
226
  const cfg = config ?? loadConfig();
212
227
  for (const entry of cfg.stashes ?? []) {
213
228
  if (!GIT_STASH_TYPES.has(entry.type) || !entry.url || entry.enabled === false)
@@ -232,7 +247,3 @@ export async function ensureStashCaches(config) {
232
247
  }
233
248
  }
234
249
  }
235
- /** @deprecated Use ensureStashCaches instead. */
236
- export const ensureGitCaches = ensureStashCaches;
237
- /** @deprecated Use ensureStashCaches instead. */
238
- export const ensureContextHubCaches = ensureStashCaches;
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { DEFAULT_LOCAL_MODEL } from "./embedder";
4
- import { getCacheDir, getSemanticStatusPath } from "./paths";
3
+ import { getCacheDir, getSemanticStatusPath } from "../core/paths";
4
+ import { DEFAULT_LOCAL_MODEL } from "../llm/embedder";
5
5
  export function deriveSemanticProviderFingerprint(embedding) {
6
6
  if (embedding?.endpoint) {
7
7
  return `remote:${embedding.endpoint}|${embedding.model}|${embedding.dimension ?? "default"}`;
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
- import { isRelevantAssetFile } from "./asset-spec";
10
+ import { isRelevantAssetFile } from "../core/asset-spec";
11
11
  import { buildFileContext } from "./file-context";
12
12
  const SKIP_DIRS = new Set([".git", "node_modules", "bin", ".cache"]);
13
13
  /**
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { getConfigDir } from "./config";
3
+ import { getConfigDir } from "../core/config";
4
4
  // ── Paths ───────────────────────────────────────────────────────────────────
5
5
  const LOCKFILE_NAME = "akm.lock";
6
6
  const LEGACY_LOCKFILE_NAME = "stash.lock";
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * `llm.ts` re-exports everything from this module for backward compatibility.
9
9
  */
10
- import { fetchWithTimeout } from "./common";
10
+ import { fetchWithTimeout } from "../core/common";
11
11
  export async function chatCompletion(config, messages, options) {
12
12
  const headers = { "Content-Type": "application/json" };
13
13
  if (config.apiKey) {
@@ -7,8 +7,8 @@
7
7
  * shared instance for the production code path.
8
8
  */
9
9
  import path from "node:path";
10
- import { getCacheDir } from "../paths";
11
- import { warn } from "../warn";
10
+ import { getCacheDir } from "../../core/paths";
11
+ import { warn } from "../../core/warn";
12
12
  /**
13
13
  * Default local transformer model for embeddings.
14
14
  * `bge-small-en-v1.5` scores higher on MTEB benchmarks than the previous
@@ -4,7 +4,7 @@
4
4
  * Calls the configured `/embeddings` endpoint and L2-normalizes the returned
5
5
  * vectors so the scoring pipeline's L2-to-cosine conversion is correct.
6
6
  */
7
- import { fetchWithTimeout, isHttpUrl } from "../common";
7
+ import { fetchWithTimeout, isHttpUrl } from "../../core/common";
8
8
  const REMOTE_BATCH_SIZE = 100;
9
9
  export class RemoteEmbedder {
10
10
  config;
@@ -36,4 +36,4 @@ export function cosineSimilarity(a, b) {
36
36
  }
37
37
  // Imported lazily to keep this types module dependency-free where possible;
38
38
  // `warn` is a thin printf wrapper so the cost is negligible.
39
- import { warn } from "../warn";
39
+ import { warn } from "../../core/warn";
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Split out of `llm.ts` so the higher-level workflow (prompting the LLM to
5
5
  * improve descriptions/tags/searchHints) lives separately from the low-level
6
- * transport client in `llm-client.ts`.
6
+ * transport client in `client.ts`.
7
7
  */
8
- import { chatCompletion, parseJsonResponse } from "./llm-client";
8
+ import { chatCompletion, parseJsonResponse } from "./client";
9
9
  const SYSTEM_PROMPT = `You are a metadata generator for a developer asset registry. Given a script/skill/command/agent entry, generate improved metadata. Respond with ONLY valid JSON, no markdown fencing.`;
10
10
  /**
11
11
  * Use an LLM to enhance a stash entry's metadata: improve description,
@@ -121,6 +121,7 @@ akm remember --name release-retro < notes.md # Save multiline memory from stdi
121
121
  akm import ./docs/auth-flow.md # Import a file as knowledge
122
122
  akm import - --name scratch-notes < notes.md # Import stdin as a knowledge doc
123
123
  akm workflow create ship-release # Create a workflow asset in the stash
124
+ akm workflow validate workflows/foo.md # Validate a workflow file or ref; lists every error
124
125
  akm workflow next workflow:ship-release # Start or resume the next workflow step
125
126
  akm feedback skill:code-review --positive # Record that an asset helped
126
127
  akm feedback agent:reviewer --negative # Record that an asset missed the mark
@@ -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;
@@ -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";