akm-cli 0.5.0 → 0.6.0-rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +53 -5
  2. package/README.md +9 -9
  3. package/dist/cli.js +379 -1448
  4. package/dist/{completions.js → commands/completions.js} +1 -1
  5. package/dist/{config-cli.js → commands/config-cli.js} +109 -11
  6. package/dist/commands/curate.js +263 -0
  7. package/dist/{info.js → commands/info.js} +17 -11
  8. package/dist/{init.js → commands/init.js} +4 -4
  9. package/dist/{install-audit.js → commands/install-audit.js} +14 -2
  10. package/dist/{installed-kits.js → commands/installed-stashes.js} +122 -50
  11. package/dist/commands/migration-help.js +141 -0
  12. package/dist/{registry-search.js → commands/registry-search.js} +68 -9
  13. package/dist/commands/remember.js +178 -0
  14. package/dist/{stash-search.js → commands/search.js} +28 -69
  15. package/dist/{self-update.js → commands/self-update.js} +3 -3
  16. package/dist/{stash-show.js → commands/show.js} +106 -81
  17. package/dist/{stash-add.js → commands/source-add.js} +133 -67
  18. package/dist/{stash-clone.js → commands/source-clone.js} +15 -13
  19. package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
  20. package/dist/{vault.js → commands/vault.js} +43 -0
  21. package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
  22. package/dist/{asset-registry.js → core/asset-registry.js} +30 -6
  23. package/dist/{asset-spec.js → core/asset-spec.js} +13 -6
  24. package/dist/{common.js → core/common.js} +147 -50
  25. package/dist/{config.js → core/config.js} +288 -29
  26. package/dist/core/errors.js +90 -0
  27. package/dist/{frontmatter.js → core/frontmatter.js} +64 -8
  28. package/dist/{paths.js → core/paths.js} +4 -4
  29. package/dist/core/write-source.js +280 -0
  30. package/dist/{local-search.js → indexer/db-search.js} +49 -32
  31. package/dist/{db.js → indexer/db.js} +210 -81
  32. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  33. package/dist/{indexer.js → indexer/indexer.js} +153 -30
  34. package/dist/{manifest.js → indexer/manifest.js} +10 -10
  35. package/dist/{matchers.js → indexer/matchers.js} +4 -7
  36. package/dist/{metadata.js → indexer/metadata.js} +9 -5
  37. package/dist/{search-source.js → indexer/search-source.js} +97 -55
  38. package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
  39. package/dist/{walker.js → indexer/walker.js} +1 -1
  40. package/dist/{lockfile.js → integrations/lockfile.js} +29 -2
  41. package/dist/{llm.js → llm/client.js} +12 -48
  42. package/dist/llm/embedder.js +127 -0
  43. package/dist/llm/embedders/cache.js +47 -0
  44. package/dist/llm/embedders/local.js +152 -0
  45. package/dist/llm/embedders/remote.js +121 -0
  46. package/dist/llm/embedders/types.js +39 -0
  47. package/dist/llm/metadata-enhance.js +53 -0
  48. package/dist/output/cli-hints.js +301 -0
  49. package/dist/output/context.js +95 -0
  50. package/dist/{renderers.js → output/renderers.js} +57 -61
  51. package/dist/output/shapes.js +212 -0
  52. package/dist/output/text.js +520 -0
  53. package/dist/{registry-build-index.js → registry/build-index.js} +48 -32
  54. package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
  55. package/dist/registry/factory.js +33 -0
  56. package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
  57. package/dist/registry/providers/index.js +11 -0
  58. package/dist/{providers → registry/providers}/skills-sh.js +60 -4
  59. package/dist/{providers → registry/providers}/static-index.js +126 -56
  60. package/dist/registry/providers/types.js +25 -0
  61. package/dist/{registry-resolve.js → registry/resolve.js} +10 -6
  62. package/dist/{detect.js → setup/detect.js} +0 -27
  63. package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
  64. package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
  65. package/dist/{setup.js → setup/setup.js} +162 -129
  66. package/dist/setup/steps.js +45 -0
  67. package/dist/{kit-include.js → sources/include.js} +1 -1
  68. package/dist/sources/provider-factory.js +36 -0
  69. package/dist/sources/provider.js +21 -0
  70. package/dist/sources/providers/filesystem.js +35 -0
  71. package/dist/{stash-providers → sources/providers}/git.js +218 -28
  72. package/dist/{stash-providers → sources/providers}/index.js +4 -4
  73. package/dist/sources/providers/install-types.js +14 -0
  74. package/dist/sources/providers/npm.js +160 -0
  75. package/dist/sources/providers/provider-utils.js +173 -0
  76. package/dist/sources/providers/sync-from-ref.js +45 -0
  77. package/dist/sources/providers/tar-utils.js +154 -0
  78. package/dist/{stash-providers → sources/providers}/website.js +60 -20
  79. package/dist/{stash-resolve.js → sources/resolve.js} +13 -12
  80. package/dist/{wiki.js → wiki/wiki.js} +18 -17
  81. package/dist/{workflow-authoring.js → workflows/authoring.js} +48 -17
  82. package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
  83. package/dist/{workflow-db.js → workflows/db.js} +1 -1
  84. package/dist/workflows/document-cache.js +20 -0
  85. package/dist/workflows/parser.js +379 -0
  86. package/dist/workflows/renderer.js +78 -0
  87. package/dist/{workflow-runs.js → workflows/runs.js} +84 -30
  88. package/dist/workflows/schema.js +11 -0
  89. package/dist/workflows/validator.js +48 -0
  90. package/docs/README.md +30 -0
  91. package/docs/migration/release-notes/0.0.13.md +4 -0
  92. package/docs/migration/release-notes/0.1.0.md +6 -0
  93. package/docs/migration/release-notes/0.2.0.md +6 -0
  94. package/docs/migration/release-notes/0.3.0.md +5 -0
  95. package/docs/migration/release-notes/0.5.0.md +6 -0
  96. package/docs/migration/release-notes/0.6.0.md +75 -0
  97. package/docs/migration/release-notes/README.md +21 -0
  98. package/package.json +3 -2
  99. package/dist/embedder.js +0 -351
  100. package/dist/errors.js +0 -34
  101. package/dist/migration-help.js +0 -110
  102. package/dist/registry-factory.js +0 -19
  103. package/dist/registry-install.js +0 -532
  104. package/dist/ripgrep.js +0 -2
  105. package/dist/stash-provider-factory.js +0 -35
  106. package/dist/stash-provider.js +0 -1
  107. package/dist/stash-providers/filesystem.js +0 -41
  108. package/dist/stash-providers/openviking.js +0 -348
  109. package/dist/stash-providers/provider-utils.js +0 -11
  110. package/dist/stash-types.js +0 -1
  111. package/dist/workflow-markdown.js +0 -251
  112. /package/dist/{markdown.js → core/markdown.js} +0 -0
  113. /package/dist/{warn.js → core/warn.js} +0 -0
  114. /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
  115. /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
  116. /package/dist/{github.js → integrations/github.js} +0 -0
  117. /package/dist/{registry-provider.js → registry/types.js} +0 -0
  118. /package/dist/{registry-types.js → sources/types.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { getAssetTypes } from "./asset-spec";
4
+ import { getAssetTypes } from "../core/asset-spec";
5
5
  // ── Known flag values ────────────────────────────────────────────────────────
6
6
  const FLAG_VALUES = {
7
7
  "--format": ["json", "text", "yaml", "jsonl"],
@@ -1,9 +1,18 @@
1
- import { DEFAULT_CONFIG, } from "./config";
2
- import { UsageError } from "./errors";
1
+ import { DEFAULT_CONFIG, } from "../core/config";
2
+ import { UsageError } from "../core/errors";
3
+ // ── Merge helpers for LLM/embedding subkey set ───────────────────────────────
4
+ function mergeLlmLike(base, patch) {
5
+ return { endpoint: "", model: "", ...(base ?? {}), ...patch };
6
+ }
7
+ function mergeLlmLikeEmbedding(base, patch) {
8
+ return { endpoint: "", model: "", ...(base ?? {}), ...patch };
9
+ }
3
10
  export function parseConfigValue(key, value) {
4
11
  switch (key) {
5
12
  case "stashDir":
6
13
  return { stashDir: requireNonEmptyString(value, key) };
14
+ case "defaultWriteTarget":
15
+ return { defaultWriteTarget: requireNonEmptyString(value, key) };
7
16
  case "semanticSearchMode":
8
17
  // Accept legacy boolean-style strings from CLI
9
18
  if (value === "true")
@@ -16,12 +25,26 @@ export function parseConfigValue(key, value) {
16
25
  return { semanticSearchMode: value };
17
26
  case "embedding":
18
27
  return { embedding: parseEmbeddingConnectionValue(value) };
28
+ case "embedding.endpoint":
29
+ return { embedding: mergeLlmLikeEmbedding(undefined, { endpoint: requireNonEmptyString(value, key) }) };
30
+ case "embedding.model":
31
+ return { embedding: mergeLlmLikeEmbedding(undefined, { model: requireNonEmptyString(value, key) }) };
32
+ case "embedding.apiKey":
33
+ return { embedding: mergeLlmLikeEmbedding(undefined, { apiKey: requireNonEmptyString(value, key) }) };
19
34
  case "llm":
20
35
  return { llm: parseLlmConnectionValue(value) };
36
+ case "llm.endpoint":
37
+ return { llm: mergeLlmLike(undefined, { endpoint: requireNonEmptyString(value, key) }) };
38
+ case "llm.model":
39
+ return { llm: mergeLlmLike(undefined, { model: requireNonEmptyString(value, key) }) };
40
+ case "llm.apiKey":
41
+ return { llm: mergeLlmLike(undefined, { apiKey: requireNonEmptyString(value, key) }) };
21
42
  case "registries":
22
43
  return { registries: parseRegistriesValue(value) };
44
+ case "sources":
23
45
  case "stashes":
24
- return { stashes: parseStashesValue(value) };
46
+ // "stashes" is kept as an alias for backwards-compat; both write to `sources`.
47
+ return { sources: parseStashesValue(value) };
25
48
  case "output.format":
26
49
  return { output: { format: parseOutputFormat(value) } };
27
50
  case "output.detail":
@@ -46,16 +69,32 @@ export function getConfigValue(config, key) {
46
69
  switch (key) {
47
70
  case "stashDir":
48
71
  return config.stashDir ?? null;
72
+ case "defaultWriteTarget":
73
+ return config.defaultWriteTarget ?? null;
49
74
  case "semanticSearchMode":
50
75
  return config.semanticSearchMode;
51
76
  case "embedding":
52
77
  return config.embedding ?? null;
78
+ case "embedding.endpoint":
79
+ return config.embedding?.endpoint ?? null;
80
+ case "embedding.model":
81
+ return config.embedding?.model ?? null;
82
+ case "embedding.apiKey":
83
+ return config.embedding?.apiKey ?? null;
53
84
  case "llm":
54
85
  return config.llm ?? null;
86
+ case "llm.endpoint":
87
+ return config.llm?.endpoint ?? null;
88
+ case "llm.model":
89
+ return config.llm?.model ?? null;
90
+ case "llm.apiKey":
91
+ return config.llm?.apiKey ?? null;
55
92
  case "registries":
56
93
  return config.registries ?? DEFAULT_CONFIG.registries ?? [];
94
+ case "sources":
57
95
  case "stashes":
58
- return config.stashes ?? [];
96
+ // "stashes" is an alias for "sources" for backwards-compat.
97
+ return config.sources ?? config.stashes ?? [];
59
98
  case "output.format":
60
99
  return config.output?.format ?? null;
61
100
  case "output.detail":
@@ -85,6 +124,7 @@ export function setConfigValue(config, key, rawValue) {
85
124
  case "embedding":
86
125
  case "llm":
87
126
  case "registries":
127
+ case "sources":
88
128
  case "stashes":
89
129
  case "output.format":
90
130
  case "output.detail":
@@ -95,6 +135,38 @@ export function setConfigValue(config, key, rawValue) {
95
135
  case "security.installAudit.registryWhitelist":
96
136
  case "security.installAudit.allowedFindings":
97
137
  return mergeConfigValue(config, parseConfigValue(key, rawValue));
138
+ // Subkey setters use deep-merge so sibling fields are preserved
139
+ case "embedding.endpoint":
140
+ return {
141
+ ...config,
142
+ embedding: mergeLlmLikeEmbedding(config.embedding, { endpoint: requireNonEmptyString(rawValue, key) }),
143
+ };
144
+ case "embedding.model":
145
+ return {
146
+ ...config,
147
+ embedding: mergeLlmLikeEmbedding(config.embedding, { model: requireNonEmptyString(rawValue, key) }),
148
+ };
149
+ case "embedding.apiKey":
150
+ return {
151
+ ...config,
152
+ embedding: mergeLlmLikeEmbedding(config.embedding, { apiKey: requireNonEmptyString(rawValue, key) }),
153
+ };
154
+ case "llm.endpoint":
155
+ return { ...config, llm: mergeLlmLike(config.llm, { endpoint: requireNonEmptyString(rawValue, key) }) };
156
+ case "llm.model":
157
+ return { ...config, llm: mergeLlmLike(config.llm, { model: requireNonEmptyString(rawValue, key) }) };
158
+ case "llm.apiKey":
159
+ return { ...config, llm: mergeLlmLike(config.llm, { apiKey: requireNonEmptyString(rawValue, key) }) };
160
+ case "defaultWriteTarget": {
161
+ const name = requireNonEmptyString(rawValue, key);
162
+ const knownNames = (config.sources ?? config.stashes ?? [])
163
+ .map((s) => s.name)
164
+ .filter((n) => typeof n === "string");
165
+ if (knownNames.length > 0 && !knownNames.includes(name)) {
166
+ throw new UsageError(`Unknown source name "${name}" for defaultWriteTarget; configured source names: ${knownNames.map((n) => `"${n}"`).join(", ")}`);
167
+ }
168
+ return { ...config, defaultWriteTarget: name };
169
+ }
98
170
  default:
99
171
  throw new UsageError(`Unknown config key: ${key}`);
100
172
  }
@@ -103,14 +175,38 @@ export function unsetConfigValue(config, key) {
103
175
  switch (key) {
104
176
  case "stashDir":
105
177
  return { ...config, stashDir: undefined };
178
+ case "defaultWriteTarget":
179
+ return { ...config, defaultWriteTarget: undefined };
106
180
  case "embedding":
107
181
  return { ...config, embedding: undefined };
182
+ case "embedding.endpoint":
183
+ return { ...config, embedding: mergeLlmLikeEmbedding(config.embedding, { endpoint: "" }) };
184
+ case "embedding.model":
185
+ return { ...config, embedding: mergeLlmLikeEmbedding(config.embedding, { model: "" }) };
186
+ case "embedding.apiKey": {
187
+ if (!config.embedding)
188
+ return config;
189
+ const { apiKey: _a, ...rest } = config.embedding;
190
+ return { ...config, embedding: rest };
191
+ }
108
192
  case "llm":
109
193
  return { ...config, llm: undefined };
194
+ case "llm.endpoint":
195
+ return { ...config, llm: mergeLlmLike(config.llm, { endpoint: "" }) };
196
+ case "llm.model":
197
+ return { ...config, llm: mergeLlmLike(config.llm, { model: "" }) };
198
+ case "llm.apiKey": {
199
+ if (!config.llm)
200
+ return config;
201
+ const { apiKey: _b, ...restLlm } = config.llm;
202
+ return { ...config, llm: restLlm };
203
+ }
110
204
  case "registries":
111
205
  return { ...config, registries: undefined };
206
+ case "sources":
112
207
  case "stashes":
113
- return { ...config, stashes: undefined };
208
+ // "stashes" is kept as an alias for backwards-compat; both clear `sources`.
209
+ return { ...config, sources: undefined, stashes: undefined };
114
210
  case "output.format":
115
211
  return { ...config, output: mergeOutputConfig(config.output, { format: undefined }) };
116
212
  case "output.detail":
@@ -155,8 +251,10 @@ export function listConfig(config) {
155
251
  output: mergeOutputConfig(DEFAULT_CONFIG.output, config.output) ?? null,
156
252
  stashDir: config.stashDir ?? null,
157
253
  installed: config.installed ?? [],
158
- stashes: config.stashes ?? [],
254
+ sources: config.sources ?? config.stashes ?? [],
159
255
  };
256
+ if (config.defaultWriteTarget)
257
+ result.defaultWriteTarget = config.defaultWriteTarget;
160
258
  if (config.embedding)
161
259
  result.embedding = config.embedding;
162
260
  if (config.llm)
@@ -349,7 +447,7 @@ function parseJsonObject(value, key, example) {
349
447
  }
350
448
  catch {
351
449
  throw new UsageError(`Invalid value for ${key}: expected JSON object with endpoint and model` +
352
- ` (e.g. '{"endpoint":"${example.endpoint}","model":"${example.model}"}')`);
450
+ ` (e.g. '{"endpoint":"${example.endpoint}","model":"${example.model}"}')`, "INVALID_JSON_CONFIG_VALUE");
353
451
  }
354
452
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
355
453
  throw new UsageError(`Invalid value for ${key}: expected a JSON object`);
@@ -388,18 +486,18 @@ function parseStashesValue(value) {
388
486
  parsed = JSON.parse(value);
389
487
  }
390
488
  catch {
391
- throw new UsageError(`Invalid value for stashes: expected JSON array of {type, path?, url?, name?, enabled?, options?} objects`);
489
+ throw new UsageError(`Invalid value for sources: expected JSON array of {type, path?, url?, name?, enabled?, options?} objects`);
392
490
  }
393
491
  if (!Array.isArray(parsed)) {
394
- throw new UsageError(`Invalid value for stashes: expected a JSON array`);
492
+ throw new UsageError(`Invalid value for sources: expected a JSON array`);
395
493
  }
396
494
  return parsed.map((entry, i) => {
397
495
  if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
398
- throw new UsageError(`Invalid value for stashes[${i}]: expected an object with a "type" field`);
496
+ throw new UsageError(`Invalid value for sources[${i}]: expected an object with a "type" field`);
399
497
  }
400
498
  const obj = entry;
401
499
  if (typeof obj.type !== "string" || !obj.type) {
402
- throw new UsageError(`Invalid value for stashes[${i}]: "type" is required`);
500
+ throw new UsageError(`Invalid value for sources[${i}]: "type" is required`);
403
501
  }
404
502
  const result = { type: obj.type };
405
503
  if (typeof obj.path === "string" && obj.path)
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Curate logic for `akm curate`.
3
+ *
4
+ * Given a query (and optional type filter / source / limit), pick a small,
5
+ * high-signal set of stash + registry hits and enrich each with the data
6
+ * needed to act (ref, run, parameters, follow-up command).
7
+ *
8
+ * The exported `akmCurate()` API is the single entry point. Internal
9
+ * helpers stay private. Tests can drive the public API or call the smaller
10
+ * pure helpers (`curateSearchResults`, `orderCuratedTypes`,
11
+ * `deriveCurateFallbackQueries`) by importing them directly.
12
+ */
13
+ import { UsageError } from "../core/errors";
14
+ import { truncateDescription } from "../output/shapes";
15
+ import { akmSearch, parseSearchSource } from "./search";
16
+ import { akmShowUnified } from "./show";
17
+ const CURATE_FALLBACK_FILTER_WORDS = new Set([
18
+ "a",
19
+ "an",
20
+ "and",
21
+ "for",
22
+ "how",
23
+ "i",
24
+ "in",
25
+ "of",
26
+ "or",
27
+ "the",
28
+ "to",
29
+ "with",
30
+ ]);
31
+ const CURATED_TYPE_FALLBACK_ORDER = ["skill", "command", "script", "knowledge", "agent", "memory"];
32
+ const CURATED_TYPE_FALLBACK_INDEX = new Map(CURATED_TYPE_FALLBACK_ORDER.map((type, index) => [type, index]));
33
+ const MIN_CURATE_FALLBACK_TOKEN_LENGTH = 3;
34
+ const MAX_CURATE_FALLBACK_KEYWORDS = 6;
35
+ export const CURATE_SEARCH_LIMIT_MULTIPLIER = 4;
36
+ export const MIN_CURATE_SEARCH_LIMIT = 12;
37
+ const DEFAULT_CURATE_LIMIT = 4;
38
+ /**
39
+ * Public curate entry point. Performs the search itself when
40
+ * `options.searchResponse` is not supplied.
41
+ */
42
+ export async function akmCurate(options) {
43
+ const trimmedQuery = options.query.trim();
44
+ if (!trimmedQuery) {
45
+ throw new UsageError('A curation query is required. Usage: akm curate "<task or prompt>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT");
46
+ }
47
+ const limit = options.limit && options.limit > 0 ? options.limit : DEFAULT_CURATE_LIMIT;
48
+ const source = options.source ?? parseSearchSource("stash");
49
+ const searchResponse = options.searchResponse ??
50
+ (await searchForCuration({
51
+ query: options.query,
52
+ type: options.type,
53
+ // Search deeper than the final curated count so we can pick one strong
54
+ // match per type and still have room for fallback retries.
55
+ limit: Math.max(limit * CURATE_SEARCH_LIMIT_MULTIPLIER, MIN_CURATE_SEARCH_LIMIT),
56
+ source,
57
+ }));
58
+ return curateSearchResults(options.query, searchResponse, limit, options.type);
59
+ }
60
+ export async function curateSearchResults(query, result, limit, selectedType) {
61
+ const stashHits = result.hits.filter((hit) => hit.type !== "registry");
62
+ const registryHits = result.registryHits ?? [];
63
+ let selectedStashHits;
64
+ if (selectedType && selectedType !== "any") {
65
+ selectedStashHits = stashHits.slice(0, limit);
66
+ }
67
+ else {
68
+ const bestByType = new Map();
69
+ for (const hit of stashHits) {
70
+ if (!bestByType.has(hit.type))
71
+ bestByType.set(hit.type, hit);
72
+ }
73
+ const orderedTypes = orderCuratedTypes(query, Array.from(bestByType.keys()));
74
+ selectedStashHits = orderedTypes
75
+ .map((type) => bestByType.get(type))
76
+ .filter((hit) => Boolean(hit));
77
+ }
78
+ const selectedRegistryHits = selectedStashHits.length >= limit ? [] : registryHits.slice(0, Math.min(2, limit - selectedStashHits.length));
79
+ const items = [
80
+ ...(await Promise.all(selectedStashHits.slice(0, limit).map((hit) => enrichCuratedStashHit(query, hit)))),
81
+ ...selectedRegistryHits.map((hit) => buildCuratedRegistryItem(query, hit)),
82
+ ].slice(0, limit);
83
+ return {
84
+ query,
85
+ summary: buildCurateSummary(query, items),
86
+ items,
87
+ ...(result.warnings?.length ? { warnings: result.warnings } : {}),
88
+ ...(result.tip ? { tip: result.tip } : {}),
89
+ };
90
+ }
91
+ export function orderCuratedTypes(query, types) {
92
+ const lower = query.toLowerCase();
93
+ const boosts = new Map();
94
+ const addBoost = (type, amount) => boosts.set(type, (boosts.get(type) ?? 0) + amount);
95
+ if (/(run|script|bash|shell|cli|execute|automation|deploy|build|test|lint)/.test(lower)) {
96
+ addBoost("script", 6);
97
+ addBoost("command", 4);
98
+ }
99
+ if (/(guide|docs?|readme|reference|how|explain|learn|why)/.test(lower)) {
100
+ addBoost("knowledge", 6);
101
+ addBoost("skill", 4);
102
+ }
103
+ if (/(agent|assistant|planner|review|analy[sz]e|architect|prompt)/.test(lower)) {
104
+ addBoost("agent", 6);
105
+ addBoost("skill", 3);
106
+ }
107
+ if (/(config|template|release|generate|command)/.test(lower)) {
108
+ addBoost("command", 5);
109
+ }
110
+ if (/(memory|context|recall|remember)/.test(lower)) {
111
+ addBoost("memory", 6);
112
+ }
113
+ return [...types].sort((a, b) => {
114
+ const boostDiff = (boosts.get(b) ?? 0) - (boosts.get(a) ?? 0);
115
+ if (boostDiff !== 0)
116
+ return boostDiff;
117
+ return ((CURATED_TYPE_FALLBACK_INDEX.get(a) ?? Number.MAX_SAFE_INTEGER) -
118
+ (CURATED_TYPE_FALLBACK_INDEX.get(b) ?? Number.MAX_SAFE_INTEGER));
119
+ });
120
+ }
121
+ async function enrichCuratedStashHit(query, hit) {
122
+ let shown;
123
+ try {
124
+ shown = await akmShowUnified({ ref: hit.ref });
125
+ }
126
+ catch {
127
+ shown = undefined;
128
+ }
129
+ const description = shown?.description ?? hit.description;
130
+ const preview = buildCuratedPreview(shown, hit);
131
+ return {
132
+ source: "stash",
133
+ type: shown?.type ?? hit.type,
134
+ name: shown?.name ?? hit.name,
135
+ ref: hit.ref,
136
+ ...(description ? { description } : {}),
137
+ ...(preview ? { preview } : {}),
138
+ ...(shown?.parameters?.length ? { parameters: shown.parameters } : {}),
139
+ ...(shown?.run ? { run: shown.run } : {}),
140
+ followUp: `akm show ${hit.ref}`,
141
+ reason: buildCuratedReason(query, shown?.type ?? hit.type),
142
+ ...(hit.score !== undefined ? { score: hit.score } : {}),
143
+ };
144
+ }
145
+ function buildCuratedRegistryItem(query, hit) {
146
+ return {
147
+ source: "registry",
148
+ type: "registry",
149
+ name: hit.name,
150
+ id: hit.id,
151
+ ...(hit.description ? { description: hit.description } : {}),
152
+ followUp: hit.action ?? `akm add ${hit.id}`,
153
+ reason: `Useful external source to explore for ${query}.`,
154
+ ...(hit.score !== undefined ? { score: hit.score } : {}),
155
+ };
156
+ }
157
+ function firstNonEmpty(values) {
158
+ return values.find((value) => typeof value === "string" && value.trim().length > 0);
159
+ }
160
+ function buildCuratedPreview(shown, hit) {
161
+ if (shown?.run)
162
+ return truncateDescription(`run ${shown.run}`, 160);
163
+ const payload = firstNonEmpty([shown?.template, shown?.prompt, shown?.content, hit.description])
164
+ ?.replace(/\s+/g, " ")
165
+ .trim();
166
+ return payload ? truncateDescription(payload, 160) : undefined;
167
+ }
168
+ function buildCuratedReason(query, type) {
169
+ switch (type) {
170
+ case "script":
171
+ return `Best runnable script match for "${query}".`;
172
+ case "command":
173
+ return `Best reusable command/template match for "${query}".`;
174
+ case "knowledge":
175
+ return `Best reference document match for "${query}".`;
176
+ case "skill":
177
+ return `Best instructions/workflow match for "${query}".`;
178
+ case "agent":
179
+ return `Best specialized agent prompt match for "${query}".`;
180
+ case "memory":
181
+ return `Best saved context match for "${query}".`;
182
+ default:
183
+ return `Best ${type} match for "${query}".`;
184
+ }
185
+ }
186
+ function buildCurateSummary(query, items) {
187
+ if (items.length === 0) {
188
+ return `No curated assets were selected for "${query}".`;
189
+ }
190
+ const labels = items.map((item) => `${item.type}:${item.name}`);
191
+ return `Selected ${items.length} high-signal result${items.length === 1 ? "" : "s"}: ${labels.join(", ")}.`;
192
+ }
193
+ function hasSearchResults(result) {
194
+ return result.hits.length > 0 || (result.registryHits?.length ?? 0) > 0;
195
+ }
196
+ /**
197
+ * Extract a small set of fallback keywords when a prompt-style curate query
198
+ * returns no hits as a whole phrase.
199
+ *
200
+ * We keep up to MAX_CURATE_FALLBACK_KEYWORDS distinct keywords and drop short
201
+ * or common filler words so follow-up searches stay inexpensive while focusing
202
+ * on higher-signal terms.
203
+ */
204
+ export function deriveCurateFallbackQueries(query) {
205
+ return Array.from(new Set(query
206
+ .toLowerCase()
207
+ .split(/[^a-z0-9]+/)
208
+ .map((token) => token.trim())
209
+ // Keep longer tokens so fallback stays focused on higher-signal terms
210
+ // and avoids broad one- and two-letter matches that overwhelm curation.
211
+ .filter((token) => token.length >= MIN_CURATE_FALLBACK_TOKEN_LENGTH && !CURATE_FALLBACK_FILTER_WORDS.has(token)))).slice(0, MAX_CURATE_FALLBACK_KEYWORDS);
212
+ }
213
+ export function mergeCurateSearchResponses(base, extras) {
214
+ const hitsByRef = new Map();
215
+ for (const hit of base.hits.filter((entry) => entry.type !== "registry")) {
216
+ hitsByRef.set(hit.ref, hit);
217
+ }
218
+ for (const result of extras) {
219
+ for (const hit of result.hits.filter((entry) => entry.type !== "registry")) {
220
+ const existing = hitsByRef.get(hit.ref);
221
+ if (!existing || (hit.score ?? 0) > (existing.score ?? 0)) {
222
+ hitsByRef.set(hit.ref, hit);
223
+ }
224
+ }
225
+ }
226
+ const registryById = new Map();
227
+ for (const hit of base.registryHits ?? []) {
228
+ registryById.set(hit.id, hit);
229
+ }
230
+ for (const result of extras) {
231
+ for (const hit of result.registryHits ?? []) {
232
+ const existing = registryById.get(hit.id);
233
+ if (!existing || (hit.score ?? 0) > (existing.score ?? 0)) {
234
+ registryById.set(hit.id, hit);
235
+ }
236
+ }
237
+ }
238
+ const warnings = Array.from(new Set([...(base.warnings ?? []), ...extras.flatMap((result) => result.warnings ?? [])]));
239
+ const mergedHits = [...hitsByRef.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
240
+ const mergedRegistryHits = [...registryById.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
241
+ return {
242
+ ...base,
243
+ hits: mergedHits,
244
+ ...(mergedRegistryHits.length > 0 ? { registryHits: mergedRegistryHits } : {}),
245
+ ...(warnings.length > 0 ? { warnings } : {}),
246
+ ...(mergedHits.length > 0 || mergedRegistryHits.length > 0 ? { tip: undefined } : {}),
247
+ };
248
+ }
249
+ export async function searchForCuration(input) {
250
+ const initial = await akmSearch(input);
251
+ if (hasSearchResults(initial))
252
+ return initial;
253
+ const fallbackQueries = deriveCurateFallbackQueries(input.query);
254
+ if (fallbackQueries.length <= 1)
255
+ return initial;
256
+ const fallbackResults = await Promise.all(fallbackQueries.map((token) => akmSearch({
257
+ query: token,
258
+ type: input.type,
259
+ limit: input.limit,
260
+ source: input.source,
261
+ })));
262
+ return mergeCurateSearchResponses(initial, fallbackResults);
263
+ }
@@ -1,10 +1,10 @@
1
1
  import fs from "node:fs";
2
- import { getAssetTypes } from "./asset-spec";
3
- import { loadConfig } from "./config";
4
- import { closeDatabase, getEntryCount, getMeta, isVecAvailable, openDatabase } from "./db";
5
- import { getDbPath } from "./paths";
6
- import { getEffectiveSemanticStatus, readSemanticStatus } from "./semantic-status";
7
- import { pkgVersion } from "./version";
2
+ import { getAssetTypes } from "../core/asset-spec";
3
+ import { loadConfig } from "../core/config";
4
+ import { getDbPath } from "../core/paths";
5
+ import { closeDatabase, getEntryCount, getMeta, isVecAvailable, openDatabase } from "../indexer/db";
6
+ import { getEffectiveSemanticStatus, readSemanticStatus } from "../indexer/semantic-status";
7
+ import { pkgVersion } from "../version";
8
8
  /**
9
9
  * Assemble system info describing the current capabilities, configuration,
10
10
  * and index state. Used by `akm info`.
@@ -13,8 +13,8 @@ import { pkgVersion } from "./version";
13
13
  */
14
14
  export function assembleInfo(options) {
15
15
  const config = loadConfig();
16
- // Asset types
17
- const assetTypes = getAssetTypes();
16
+ // Asset types (copy into a mutable array — `getAssetTypes()` returns readonly)
17
+ const assetTypes = [...getAssetTypes()];
18
18
  const semanticRuntime = readSemanticStatus();
19
19
  const semanticStatus = getEffectiveSemanticStatus(config, semanticRuntime);
20
20
  // Search modes
@@ -29,8 +29,14 @@ export function assembleInfo(options) {
29
29
  ...(r.provider ? { provider: r.provider } : {}),
30
30
  ...(r.enabled !== undefined ? { enabled: r.enabled } : {}),
31
31
  }));
32
- // Stash providers
33
- const stashProviders = (config.stashes ?? []).map((s) => ({
32
+ // Stash providers — prefer `sources[]`; fall back to `stashDir` when the
33
+ // user has not yet migrated to the sources[] config shape so that info
34
+ // always reflects at least one provider when a stash is configured.
35
+ const configuredSources = config.sources ?? config.stashes ?? [];
36
+ const stashesList = configuredSources.length === 0 && config.stashDir
37
+ ? [{ type: "filesystem", path: config.stashDir, name: "primary" }]
38
+ : configuredSources;
39
+ const sourceProviders = stashesList.map((s) => ({
34
40
  type: s.type,
35
41
  ...(s.name ? { name: s.name } : {}),
36
42
  ...(s.path ? { path: s.path } : {}),
@@ -51,7 +57,7 @@ export function assembleInfo(options) {
51
57
  ...(semanticRuntime?.message ? { message: semanticRuntime.message } : {}),
52
58
  },
53
59
  registries,
54
- stashProviders,
60
+ sourceProviders,
55
61
  indexStats,
56
62
  };
57
63
  }
@@ -7,10 +7,10 @@
7
7
  import { spawnSync } from "node:child_process";
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
- import { TYPE_DIRS } from "./asset-spec";
11
- import { getConfigPath, loadUserConfig, saveConfig } from "./config";
12
- import { getBinDir, getDefaultStashDir } from "./paths";
13
- import { ensureRg } from "./ripgrep-install";
10
+ import { TYPE_DIRS } from "../core/asset-spec";
11
+ import { getConfigPath, loadUserConfig, saveConfig } from "../core/config";
12
+ import { getBinDir, getDefaultStashDir } from "../core/paths";
13
+ import { ensureRg } from "../setup/ripgrep-install";
14
14
  export async function akmInit(options) {
15
15
  const stashDir = options?.dir ? path.resolve(options.dir) : getDefaultStashDir();
16
16
  let created = false;
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { filterNonEmptyStrings } from "./common";
3
+ import { filterNonEmptyStrings, toPosix } from "../core/common";
4
4
  const DEFAULT_INSTALL_AUDIT_CONFIG = {
5
5
  enabled: true,
6
6
  blockOnCritical: true,
@@ -346,16 +346,28 @@ function splitAllowedFindings(findings, ref, allowedFindings) {
346
346
  return { findings: active, waivedFindings: waived };
347
347
  }
348
348
  function matchesAllowedFinding(finding, ref, allowedFindings) {
349
+ // Normalize paths so a waiver written against `scripts/setup.sh` matches
350
+ // a finding emitted as `./scripts/setup.sh` or `scripts//setup.sh`. On
351
+ // Windows we also fold case, mirroring `isWithin`'s comparison rules.
352
+ const findingPathNormalized = normalizeWaiverPath(finding.file);
349
353
  return allowedFindings.some((allowed) => {
350
354
  if (allowed.id !== finding.id)
351
355
  return false;
352
356
  if (allowed.ref && allowed.ref !== ref)
353
357
  return false;
354
- if (allowed.path && allowed.path !== finding.file)
358
+ if (allowed.path && normalizeWaiverPath(allowed.path) !== findingPathNormalized)
355
359
  return false;
356
360
  return true;
357
361
  });
358
362
  }
363
+ function normalizeWaiverPath(value) {
364
+ if (!value)
365
+ return value;
366
+ // Strip a leading `./` and POSIX-ify after path.normalize so Windows path
367
+ // separators don't trigger spurious mismatches.
368
+ const normalized = toPosix(path.normalize(value)).replace(/^\.\/+/, "");
369
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
370
+ }
359
371
  function addUrlLabels(labels, rawUrl) {
360
372
  if (!rawUrl)
361
373
  return;