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
@@ -30,7 +30,7 @@ export function makeAssetRef(type, name, origin) {
30
30
  export function parseAssetRef(ref) {
31
31
  const trimmed = ref.trim();
32
32
  if (!trimmed)
33
- throw new UsageError("Empty ref.");
33
+ throw new UsageError("Empty ref.", "MISSING_REQUIRED_ARGUMENT");
34
34
  let origin;
35
35
  let body = trimmed;
36
36
  const boundary = trimmed.indexOf("//");
@@ -38,16 +38,16 @@ export function parseAssetRef(ref) {
38
38
  origin = trimmed.slice(0, boundary);
39
39
  body = trimmed.slice(boundary + 2);
40
40
  if (!origin)
41
- throw new UsageError("Empty origin in ref.");
41
+ throw new UsageError("Empty origin in ref.", "MISSING_REQUIRED_ARGUMENT");
42
42
  }
43
43
  const colon = body.indexOf(":");
44
44
  if (colon <= 0) {
45
- throw new UsageError(`Invalid ref "${trimmed}". Expected [origin//]type:name`);
45
+ throw new UsageError(`Invalid ref "${trimmed}". Expected [origin//]type:name, e.g. skill:deploy or knowledge:guide.md`, "MISSING_REQUIRED_ARGUMENT");
46
46
  }
47
47
  const rawType = body.slice(0, colon);
48
48
  const rawName = body.slice(colon + 1);
49
49
  if (!isAssetType(rawType)) {
50
- throw new UsageError(`Invalid asset type: "${rawType}".`);
50
+ throw new UsageError(`Invalid asset type: "${rawType}".`, "MISSING_REQUIRED_ARGUMENT");
51
51
  }
52
52
  validateName(rawName);
53
53
  const name = normalizeName(rawName);
@@ -10,7 +10,7 @@
10
10
  * `db-search.ts` import from, eliminating the import-order dependency
11
11
  * entirely.
12
12
  */
13
- import { buildWorkflowAction } from "./renderers";
13
+ import { buildWorkflowAction } from "../output/renderers";
14
14
  /** Map asset types to their primary renderer names. */
15
15
  export const TYPE_TO_RENDERER = {
16
16
  script: "script-source",
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
+ import { buildWorkflowAction } from "../output/renderers";
2
3
  import { registerActionBuilder, registerTypeRenderer } from "./asset-registry";
3
4
  import { toPosix } from "./common";
4
- import { buildWorkflowAction } from "./renderers";
5
5
  const markdownSpec = {
6
6
  isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
7
7
  toCanonicalName: (typeRoot, filePath) => {
@@ -2,7 +2,9 @@ import { createHash } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { filterNonEmptyStrings } from "./common";
5
+ import { ConfigError } from "./errors";
5
6
  import { getConfigDir as _getConfigDir, getConfigPath as _getConfigPath } from "./paths";
7
+ import { warn } from "./warn";
6
8
  // ── Defaults ────────────────────────────────────────────────────────────────
7
9
  export const DEFAULT_CONFIG = {
8
10
  semanticSearchMode: "auto",
@@ -106,20 +108,7 @@ export function saveConfig(config) {
106
108
  const dir = path.dirname(configPath);
107
109
  fs.mkdirSync(dir, { recursive: true });
108
110
  const sanitized = sanitizeConfigForWrite(config);
109
- const tmpPath = `${configPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
110
- try {
111
- fs.writeFileSync(tmpPath, `${JSON.stringify(sanitized, null, 2)}\n`, "utf8");
112
- fs.renameSync(tmpPath, configPath);
113
- }
114
- catch (err) {
115
- try {
116
- fs.unlinkSync(tmpPath);
117
- }
118
- catch {
119
- /* ignore cleanup failure */
120
- }
121
- throw err;
122
- }
111
+ writeConfigObject(configPath, sanitized);
123
112
  }
124
113
  /**
125
114
  * Strip apiKey fields before writing config to disk.
@@ -186,16 +175,16 @@ function pickKnownKeys(raw) {
186
175
  const legacySemanticSearch = raw.semanticSearch;
187
176
  config.semanticSearchMode = legacySemanticSearch ? "auto" : "off";
188
177
  }
189
- // Migrate legacy searchPaths into stashes
178
+ // Migrate legacy searchPaths into sources
190
179
  if (Array.isArray(raw.searchPaths)) {
191
180
  const legacyPaths = raw.searchPaths.filter((d) => typeof d === "string");
192
181
  if (legacyPaths.length > 0) {
193
- const existing = config.stashes ?? [];
182
+ const existing = config.sources ?? [];
194
183
  const migrated = legacyPaths
195
184
  .filter((p) => !existing.some((s) => s.type === "filesystem" && s.path === p))
196
185
  .map((p) => ({ type: "filesystem", path: p }));
197
186
  if (migrated.length > 0) {
198
- config.stashes = [...existing, ...migrated];
187
+ config.sources = [...existing, ...migrated];
199
188
  }
200
189
  }
201
190
  }
@@ -220,9 +209,20 @@ function pickKnownKeys(raw) {
220
209
  config.stashInheritance = raw.disableGlobalStashes ? "replace" : "merge";
221
210
  config.disableGlobalStashes = raw.disableGlobalStashes;
222
211
  }
223
- const stashes = parseStashesConfig(raw.stashes);
224
- if (stashes)
225
- config.stashes = stashes;
212
+ // Load `sources` (new key) first, then fall back to legacy `stashes` key.
213
+ const sources = parseStashesConfig(raw.sources);
214
+ if (sources) {
215
+ config.sources = sources;
216
+ }
217
+ else {
218
+ const legacyStashes = parseStashesConfig(raw.stashes);
219
+ if (legacyStashes) {
220
+ // Backwards-compat fallback: configs that still carry `stashes[]` are
221
+ // normalized to `sources[]` after the raw file loader has had a chance to
222
+ // auto-migrate the on-disk key.
223
+ config.sources = legacyStashes;
224
+ }
225
+ }
226
226
  const security = parseSecurityConfig(raw.security);
227
227
  if (security)
228
228
  config.security = security;
@@ -232,24 +232,32 @@ function pickKnownKeys(raw) {
232
232
  if (typeof raw.writable === "boolean") {
233
233
  config.writable = raw.writable;
234
234
  }
235
+ if (typeof raw.defaultWriteTarget === "string" && raw.defaultWriteTarget.trim()) {
236
+ config.defaultWriteTarget = raw.defaultWriteTarget.trim();
237
+ }
238
+ if (typeof raw.search === "object" && raw.search !== null && !Array.isArray(raw.search)) {
239
+ const searchRaw = raw.search;
240
+ const searchConfig = {};
241
+ if (typeof searchRaw.minScore === "number" && Number.isFinite(searchRaw.minScore) && searchRaw.minScore >= 0) {
242
+ searchConfig.minScore = searchRaw.minScore;
243
+ }
244
+ if (Object.keys(searchConfig).length > 0)
245
+ config.search = searchConfig;
246
+ }
235
247
  return config;
236
248
  }
237
249
  function readNormalizedConfig(configPath) {
238
250
  const raw = readConfigObject(configPath);
239
- const expanded = raw ? expandEnvVars(raw) : undefined;
251
+ const migrated = raw ? maybeAutoMigrateLegacyStashes(configPath, raw) : undefined;
252
+ const expanded = migrated ? expandEnvVars(migrated) : undefined;
240
253
  return expanded ? pickKnownKeys(expanded) : undefined;
241
254
  }
242
- function readNormalizedConfigFromText(_configPath, text) {
243
- let raw;
244
- try {
245
- raw = JSON.parse(stripJsonComments(text));
246
- }
247
- catch {
248
- return undefined;
249
- }
250
- if (typeof raw !== "object" || raw === null || Array.isArray(raw))
255
+ function readNormalizedConfigFromText(configPath, text) {
256
+ const raw = parseConfigObjectFromText(text);
257
+ if (!raw)
251
258
  return undefined;
252
- const expanded = expandEnvVars(raw);
259
+ const migrated = maybeAutoMigrateLegacyStashes(configPath, raw);
260
+ const expanded = expandEnvVars(migrated);
253
261
  return pickKnownKeys(expanded);
254
262
  }
255
263
  function parseOutputConfig(value) {
@@ -312,6 +320,14 @@ function expandEnvVars(value, fieldName) {
312
320
  function readConfigObject(configPath) {
313
321
  try {
314
322
  const text = fs.readFileSync(configPath, "utf8");
323
+ return parseConfigObjectFromText(text);
324
+ }
325
+ catch {
326
+ return undefined;
327
+ }
328
+ }
329
+ function parseConfigObjectFromText(text) {
330
+ try {
315
331
  const raw = JSON.parse(stripJsonComments(text));
316
332
  if (typeof raw !== "object" || raw === null || Array.isArray(raw))
317
333
  return undefined;
@@ -321,6 +337,47 @@ function readConfigObject(configPath) {
321
337
  return undefined;
322
338
  }
323
339
  }
340
+ /**
341
+ * Best-effort on-disk config migration for the legacy `stashes` key.
342
+ *
343
+ * When a config file still uses `stashes` and does not already define
344
+ * `sources`, rewrite the file in place with `sources` replacing `stashes`,
345
+ * emit a one-time notice on success, and return the migrated object. If the
346
+ * rewrite fails, emit a warning and return the original object so the loader
347
+ * can still continue with an in-memory fallback.
348
+ */
349
+ function maybeAutoMigrateLegacyStashes(configPath, raw) {
350
+ if (Object.hasOwn(raw, "sources") || !Object.hasOwn(raw, "stashes")) {
351
+ return raw;
352
+ }
353
+ const migrated = Object.fromEntries(Object.entries(raw).map(([key, value]) => (key === "stashes" ? ["sources", value] : [key, value])));
354
+ try {
355
+ writeConfigObject(configPath, migrated);
356
+ warn('Config migrated: "stashes" → "sources" in config.json');
357
+ return migrated;
358
+ }
359
+ catch {
360
+ warn('Failed to migrate "stashes" → "sources" in config.json; continuing with the legacy key in memory. ' +
361
+ "Check file permissions or rename the key manually if this persists.");
362
+ return raw;
363
+ }
364
+ }
365
+ function writeConfigObject(configPath, config) {
366
+ const tmpPath = `${configPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
367
+ try {
368
+ fs.writeFileSync(tmpPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
369
+ fs.renameSync(tmpPath, configPath);
370
+ }
371
+ catch (err) {
372
+ try {
373
+ fs.unlinkSync(tmpPath);
374
+ }
375
+ catch {
376
+ /* ignore cleanup failure */
377
+ }
378
+ throw err;
379
+ }
380
+ }
324
381
  /**
325
382
  * Strip JavaScript-style comments from a JSON string (JSONC support).
326
383
  * Handles // line comments and /* block comments while preserving
@@ -435,11 +492,10 @@ function parseLlmConfig(value) {
435
492
  console.warn(`[akm] Ignoring llm config: endpoint must start with http:// or https://, got "${obj.endpoint}"`);
436
493
  return undefined;
437
494
  }
438
- if (typeof obj.model !== "string" || !obj.model)
439
- return undefined;
495
+ const model = typeof obj.model === "string" ? obj.model : "";
440
496
  const result = {
441
497
  endpoint: obj.endpoint,
442
- model: obj.model,
498
+ model,
443
499
  };
444
500
  if (typeof obj.provider === "string" && obj.provider) {
445
501
  result.provider = obj.provider;
@@ -531,7 +587,7 @@ function asNonEmptyString(value) {
531
587
  * Restricted to the four kinds that the install pipeline produces
532
588
  * (`"npm" | "github" | "git" | "local"`). The full {@link KitSource} union is
533
589
  * wider, but persisted `installed[]` entries should never carry the runtime
534
- * provider kinds (`"filesystem" | "website" | "openviking"`).
590
+ * provider kinds (`"filesystem" | "website"`).
535
591
  */
536
592
  function asKitSource(value) {
537
593
  if (value === "npm" || value === "github" || value === "git" || value === "local")
@@ -552,7 +608,7 @@ function parseStashesConfig(value) {
552
608
  if (!Array.isArray(value))
553
609
  return undefined;
554
610
  const entries = value
555
- .map((entry) => parseStashConfigEntry(entry))
611
+ .map((entry) => parseSourceConfigEntry(entry))
556
612
  .filter((entry) => entry !== undefined);
557
613
  return entries;
558
614
  }
@@ -623,13 +679,17 @@ const STASH_TYPE_ALIASES = {
623
679
  "context-hub": "git",
624
680
  github: "git",
625
681
  };
626
- function parseStashConfigEntry(value) {
682
+ function parseSourceConfigEntry(value) {
627
683
  if (typeof value !== "object" || value === null || Array.isArray(value))
628
684
  return undefined;
629
685
  const obj = value;
630
686
  const rawType = asNonEmptyString(obj.type);
631
687
  if (!rawType)
632
688
  return undefined;
689
+ if (rawType === "openviking") {
690
+ const name = asNonEmptyString(obj.name) ?? "unnamed";
691
+ throw new ConfigError(`openviking is not supported in akm v1. API-backed sources will return as a\nseparate QuerySource tier post-v1. Remove the source named "${name}" from your config file\nor downgrade to 0.6.x. See docs/migration/v1.md.`, "INVALID_CONFIG_FILE", `Run \`akm remove ${name}\` then re-run, or edit your config file directly at ${_getConfigPath()} to remove the openviking entry.`);
692
+ }
633
693
  const type = STASH_TYPE_ALIASES[rawType] ?? rawType;
634
694
  const entry = { type };
635
695
  const entryPath = asNonEmptyString(obj.path);
@@ -647,6 +707,14 @@ function parseStashConfigEntry(value) {
647
707
  entry.writable = obj.writable;
648
708
  if (typeof obj.primary === "boolean")
649
709
  entry.primary = obj.primary;
710
+ // Locked decision 4 (§6 v1 implementation plan): reject writable: true on
711
+ // website / npm sources at config load. The next sync() would clobber
712
+ // writes — allowing this is a footgun, not a feature. Throw early so the
713
+ // user sees the problem at `akm` startup, not when they try to write.
714
+ if (entry.writable === true && (type === "website" || type === "npm")) {
715
+ const label = entry.name ? ` "${entry.name}"` : "";
716
+ throw new ConfigError(`writable: true is only supported on filesystem and git sources (got "${type}" on source${label}).`, "INVALID_CONFIG_FILE", "To author into a checked-out package, add the same path as a separate filesystem source.");
717
+ }
650
718
  if (typeof obj.options === "object" && obj.options !== null && !Array.isArray(obj.options)) {
651
719
  entry.options = obj.options;
652
720
  }
@@ -655,9 +723,9 @@ function parseStashConfigEntry(value) {
655
723
  entry.wikiName = wikiName;
656
724
  return entry;
657
725
  }
658
- // ── StashEntry runtime construction ─────────────────────────────────────────
726
+ // ── ConfiguredSource runtime construction ─────────────────────────────────────────
659
727
  /**
660
- * Synthesize a stable identifier when a {@link StashConfigEntry} omits its
728
+ * Synthesize a stable identifier when a {@link SourceConfigEntry} omits its
661
729
  * `name`. Uses a short hash of the discriminating fields so two equivalent
662
730
  * entries collapse to the same generated name.
663
731
  */
@@ -673,8 +741,8 @@ function deriveStashEntryName(entry) {
673
741
  return `${entry.type}-${hash}`;
674
742
  }
675
743
  /**
676
- * Convert a persisted {@link StashConfigEntry} into the runtime
677
- * {@link StashSource} discriminated union. Returns `undefined` when the
744
+ * Convert a persisted {@link SourceConfigEntry} into the runtime
745
+ * {@link SourceSpec} discriminated union. Returns `undefined` when the
678
746
  * entry is missing the fields its provider type requires (e.g. a
679
747
  * `filesystem` entry with no `path`); callers should drop or warn for those.
680
748
  *
@@ -682,7 +750,7 @@ function deriveStashEntryName(entry) {
682
750
  * legacy aliases (`"context-hub"`, `"github"` for git) still produce a usable
683
751
  * runtime value when a path/url is supplied.
684
752
  */
685
- export function parseStashEntrySource(entry) {
753
+ export function parseSourceSpec(entry) {
686
754
  switch (entry.type) {
687
755
  case "filesystem":
688
756
  return entry.path ? { type: "filesystem", path: entry.path } : undefined;
@@ -700,8 +768,6 @@ export function parseStashEntrySource(entry) {
700
768
  ...(typeof entry.options?.maxPages === "number" ? { maxPages: entry.options.maxPages } : {}),
701
769
  }
702
770
  : undefined;
703
- case "openviking":
704
- return entry.url ? { type: "openviking", url: entry.url } : undefined;
705
771
  case "npm":
706
772
  // Persisted `npm` stash entries are unusual but supported for symmetry.
707
773
  return entry.path ? { type: "npm", package: entry.path } : undefined;
@@ -711,29 +777,31 @@ export function parseStashEntrySource(entry) {
711
777
  }
712
778
  }
713
779
  /**
714
- * Build the full ordered list of runtime {@link StashEntry} values from a
780
+ * Build the full ordered list of runtime {@link ConfiguredSource} values from a
715
781
  * loaded {@link AkmConfig}. Order is the canonical iteration order:
716
782
  *
717
783
  * 1. The entry marked `primary: true` (or, as a backwards-compat shim,
718
784
  * a synthetic filesystem entry built from the top-level `stashDir`).
719
- * 2. Remaining `stashes[]` entries in declared order.
785
+ * 2. Remaining `sources[]` entries in declared order.
720
786
  * 3. Legacy `installed[]` entries, mapped into runtime entries.
721
787
  *
722
788
  * Entries with `enabled: false` are still emitted — callers decide whether
723
789
  * to honour the flag (mirrors how `installed[]` entries have always been
724
- * unconditional). Entries that fail {@link parseStashEntrySource} are
790
+ * unconditional). Entries that fail {@link parseSourceSpec} are
725
791
  * dropped silently.
726
792
  */
727
- export function resolveStashEntries(config) {
793
+ export function resolveConfiguredSources(config) {
728
794
  const entries = [];
729
- const stashes = config.stashes ?? [];
795
+ // `sources` is the canonical key. `stashes` is the legacy key that the loader
796
+ // migrates in-memory; only one of the two should be set at runtime.
797
+ const stashes = config.sources ?? config.stashes ?? [];
730
798
  // (1) Primary entry: explicit `primary: true` wins; fall back to top-level stashDir.
731
799
  let primary = stashes.find((entry) => entry.primary === true);
732
800
  if (!primary && config.stashDir) {
733
801
  primary = { type: "filesystem", path: config.stashDir, primary: true };
734
802
  }
735
803
  if (primary) {
736
- const runtime = toStashEntry(primary, true);
804
+ const runtime = toConfiguredSource(primary, true);
737
805
  if (runtime)
738
806
  entries.push(runtime);
739
807
  }
@@ -741,7 +809,7 @@ export function resolveStashEntries(config) {
741
809
  for (const entry of stashes) {
742
810
  if (entry === primary)
743
811
  continue;
744
- const runtime = toStashEntry(entry, false);
812
+ const runtime = toConfiguredSource(entry, false);
745
813
  if (runtime)
746
814
  entries.push(runtime);
747
815
  }
@@ -758,8 +826,8 @@ export function resolveStashEntries(config) {
758
826
  }
759
827
  return entries;
760
828
  }
761
- function toStashEntry(persisted, isPrimary) {
762
- const source = parseStashEntrySource(persisted);
829
+ function toConfiguredSource(persisted, isPrimary) {
830
+ const source = parseSourceSpec(persisted);
763
831
  if (!source)
764
832
  return undefined;
765
833
  return {
@@ -841,12 +909,21 @@ function mergeLoadedConfig(base, override) {
841
909
  // `disableGlobalStashes` boolean so old config files behave identically.
842
910
  const replaceStashes = override.stashInheritance === "replace" ||
843
911
  (override.stashInheritance === undefined && override.disableGlobalStashes === true);
912
+ // Merge `sources` (canonical key). Legacy `stashes` key is handled via the
913
+ // pickKnownKeys migration which promotes it to `sources` at load time.
914
+ const overrideSources = override.sources ?? override.stashes ?? [];
915
+ const baseSources = base.sources ?? base.stashes ?? [];
844
916
  if (replaceStashes) {
845
- merged.stashes = [...(override.stashes ?? [])];
917
+ merged.sources = [...overrideSources];
918
+ }
919
+ else if (overrideSources.length > 0) {
920
+ merged.sources = [...baseSources, ...overrideSources];
846
921
  }
847
- else if (override.stashes) {
848
- merged.stashes = [...(base.stashes ?? []), ...override.stashes];
922
+ else if (baseSources.length > 0) {
923
+ merged.sources = [...baseSources];
849
924
  }
925
+ // Clear deprecated stashes field on the merged result — sources is canonical.
926
+ delete merged.stashes;
850
927
  return merged;
851
928
  }
852
929
  function applyRuntimeEnvApiKeys(config) {
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Typed error classes for structured exit code classification.
3
+ *
4
+ * - ConfigError -> exit 78 (configuration / environment problems)
5
+ * - UsageError -> exit 2 (bad CLI arguments or invalid input)
6
+ * - NotFoundError -> exit 1 (requested resource missing)
7
+ *
8
+ * Each error carries a machine-readable `code` field. Codes are stable
9
+ * identifiers safe to consume from scripts and JSON output. Existing throw
10
+ * sites without an explicit code receive a default code per error class so
11
+ * older call sites continue to compile and behave unchanged.
12
+ *
13
+ * Each error also exposes a `hint()` method returning an actionable hint
14
+ * string (or `undefined`). Hints can be supplied at construction time or
15
+ * derived from the error `code` via the per-class default mapping below.
16
+ * The CLI surfaces this via `error.hint()` rather than message-regex parsing.
17
+ */
18
+ /**
19
+ * Default hint for each ConfigError code. Keep these short, actionable, and
20
+ * imperative. Returning undefined means "no canned hint".
21
+ */
22
+ const CONFIG_HINTS = {
23
+ STASH_DIR_NOT_FOUND: "Run `akm init` to create the default stash, or set stashDir in your config.",
24
+ STASH_DIR_NOT_A_DIRECTORY: "The configured stashDir exists but isn't a directory. Update stashDir to point at a folder.",
25
+ STASH_DIR_UNREADABLE: "Check the path exists and your user has read permission, or update stashDir.",
26
+ EMBEDDING_NOT_CONFIGURED: 'Run `akm config set embedding \'{"endpoint":"...","model":"..."}\'` to enable embeddings.',
27
+ LLM_NOT_CONFIGURED: 'Run `akm config set llm \'{"endpoint":"...","model":"..."}\'` to configure the LLM.',
28
+ };
29
+ /** Default hint for each UsageError code. */
30
+ const USAGE_HINTS = {
31
+ INVALID_SOURCE_VALUE: "Pick one of: stash, registry, both.",
32
+ INVALID_FORMAT_VALUE: "Pick one of: json, jsonl, text, yaml.",
33
+ INVALID_DETAIL_VALUE: "Pick one of: brief, normal, full, summary, agent.",
34
+ INVALID_JSON_CONFIG_VALUE: 'Quote JSON values in your shell, for example: akm config set embedding \'{"endpoint":"http://localhost:11434/v1/embeddings","model":"nomic-embed-text"}\'.',
35
+ MISSING_OR_AMBIGUOUS_TARGET: "Use `akm update --all` or pass a target like `akm update npm:@scope/pkg` (not both).",
36
+ TARGET_NOT_UPDATABLE: "Run `akm list` to view your sources, then retry with one of those values.",
37
+ MISSING_REQUIRED_ARGUMENT: "Refs use the form type:name, e.g. `akm show skill:deploy` or `akm show knowledge:guide.md`.",
38
+ };
39
+ /** Default hint for each NotFoundError code. */
40
+ const NOT_FOUND_HINTS = {
41
+ SOURCE_NOT_FOUND: "Run `akm list` to view your sources, then retry with one of those values.",
42
+ };
43
+ /** Raised when configuration or environment is invalid or missing. */
44
+ export class ConfigError extends Error {
45
+ code;
46
+ _hint;
47
+ constructor(msg, code = "INVALID_CONFIG_FILE", hint) {
48
+ super(msg);
49
+ this.name = "ConfigError";
50
+ this.code = code;
51
+ this._hint = hint;
52
+ // Fixes `instanceof` checks under ES5 transpilation targets.
53
+ Object.setPrototypeOf(this, new.target.prototype);
54
+ }
55
+ hint() {
56
+ return this._hint ?? CONFIG_HINTS[this.code];
57
+ }
58
+ }
59
+ /** Raised when the user supplies invalid arguments or input. */
60
+ export class UsageError extends Error {
61
+ code;
62
+ _hint;
63
+ constructor(msg, code = "INVALID_FLAG_VALUE", hint) {
64
+ super(msg);
65
+ this.name = "UsageError";
66
+ this.code = code;
67
+ this._hint = hint;
68
+ // Fixes `instanceof` checks under ES5 transpilation targets.
69
+ Object.setPrototypeOf(this, new.target.prototype);
70
+ }
71
+ hint() {
72
+ return this._hint ?? USAGE_HINTS[this.code];
73
+ }
74
+ }
75
+ /** Raised when a requested resource (asset, entry, file) is not found. */
76
+ export class NotFoundError extends Error {
77
+ code;
78
+ _hint;
79
+ constructor(msg, code = "ASSET_NOT_FOUND", hint) {
80
+ super(msg);
81
+ this.name = "NotFoundError";
82
+ this.code = code;
83
+ this._hint = hint;
84
+ // Fixes `instanceof` checks under ES5 transpilation targets.
85
+ Object.setPrototypeOf(this, new.target.prototype);
86
+ }
87
+ hint() {
88
+ return this._hint ?? NOT_FOUND_HINTS[this.code];
89
+ }
90
+ }
@@ -11,11 +11,13 @@
11
11
  *
12
12
  * **Limitations**: This is a hand-rolled YAML-subset parser with intentional
13
13
  * constraints for simplicity and safety:
14
- * - **List support**: YAML block sequences (`- item`) and flow arrays
15
- * (`[a, b, c]`) are both supported for top-level list-valued keys.
14
+ * - **Top-level values**: string, boolean, and number scalars are supported,
15
+ * as well as top-level list-valued keys using YAML block sequences
16
+ * (`- item`) or flow arrays (`[a, b, c]`).
17
+ * - **List item types**: list items must be scalar values and may be strings,
18
+ * booleans, or numbers.
16
19
  * - **No nested objects beyond one level**: Only a single level of indented
17
20
  * key-value pairs is supported.
18
- * - **Scalar values only**: string, boolean, and number scalars are supported.
19
21
  */
20
22
  export function parseFrontmatter(raw) {
21
23
  const parsedBlock = parseFrontmatterBlock(raw);