akm-cli 0.7.4 → 0.8.0-rc.10

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 (300) hide show
  1. package/CHANGELOG.md +224 -1
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +133 -0
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2631 -1440
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +110 -0
  12. package/dist/commands/agent-support.js +68 -0
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +130 -534
  15. package/dist/commands/consolidate.js +2122 -0
  16. package/dist/commands/curate.js +45 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +660 -0
  19. package/dist/commands/distill.js +1081 -73
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +43 -0
  22. package/dist/commands/events.js +15 -24
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +477 -0
  28. package/dist/commands/health.js +1302 -0
  29. package/dist/commands/help/help-accept.md +12 -0
  30. package/dist/commands/help/help-improve.md +69 -0
  31. package/dist/commands/help/help-proposals.md +18 -0
  32. package/dist/commands/help/help-propose.md +17 -0
  33. package/dist/commands/help/help-reject.md +11 -0
  34. package/dist/commands/history.js +54 -46
  35. package/dist/commands/improve-auto-accept.js +97 -0
  36. package/dist/commands/improve-cli.js +217 -0
  37. package/dist/commands/improve-profiles.js +166 -0
  38. package/dist/commands/improve-result-file.js +167 -0
  39. package/dist/commands/improve.js +2373 -0
  40. package/dist/commands/info.js +5 -2
  41. package/dist/commands/init.js +50 -2
  42. package/dist/commands/installed-stashes.js +102 -139
  43. package/dist/commands/knowledge.js +136 -0
  44. package/dist/commands/lint/agent-linter.js +49 -0
  45. package/dist/commands/lint/base-linter.js +479 -0
  46. package/dist/commands/lint/command-linter.js +49 -0
  47. package/dist/commands/lint/default-linter.js +16 -0
  48. package/dist/commands/lint/env-key-rules.js +154 -0
  49. package/dist/commands/lint/index.js +196 -0
  50. package/dist/commands/lint/knowledge-linter.js +16 -0
  51. package/dist/commands/lint/markdown-insertion.js +343 -0
  52. package/dist/commands/lint/memory-linter.js +61 -0
  53. package/dist/commands/lint/registry.js +36 -0
  54. package/dist/commands/lint/skill-linter.js +45 -0
  55. package/dist/commands/lint/task-linter.js +50 -0
  56. package/dist/commands/lint/types.js +4 -0
  57. package/dist/commands/lint/workflow-linter.js +56 -0
  58. package/dist/commands/lint.js +4 -0
  59. package/dist/commands/migration-help.js +3 -0
  60. package/dist/commands/proposal.js +67 -12
  61. package/dist/commands/propose.js +120 -45
  62. package/dist/commands/reflect.js +1104 -60
  63. package/dist/commands/registry-cli.js +150 -0
  64. package/dist/commands/registry-search.js +5 -2
  65. package/dist/commands/remember-cli.js +257 -0
  66. package/dist/commands/remember.js +70 -7
  67. package/dist/commands/schema-repair.js +203 -0
  68. package/dist/commands/search.js +115 -14
  69. package/dist/commands/secret.js +173 -0
  70. package/dist/commands/self-update.js +3 -0
  71. package/dist/commands/show.js +158 -60
  72. package/dist/commands/source-add.js +17 -45
  73. package/dist/commands/source-clone.js +3 -0
  74. package/dist/commands/source-manage.js +14 -19
  75. package/dist/commands/tasks.js +437 -0
  76. package/dist/commands/url-checker.js +42 -0
  77. package/dist/core/action-contributors.js +28 -0
  78. package/dist/core/asset-ref.js +17 -2
  79. package/dist/core/asset-registry.js +12 -17
  80. package/dist/core/asset-serialize.js +88 -0
  81. package/dist/core/asset-spec.js +67 -1
  82. package/dist/core/common.js +182 -0
  83. package/dist/core/concurrent.js +25 -0
  84. package/dist/core/config-io.js +347 -0
  85. package/dist/core/config-migration.js +622 -0
  86. package/dist/core/config-schema.js +534 -0
  87. package/dist/core/config-sources.js +108 -0
  88. package/dist/core/config-types.js +4 -0
  89. package/dist/core/config-walker.js +337 -0
  90. package/dist/core/config.js +364 -968
  91. package/dist/core/errors.js +42 -20
  92. package/dist/core/events.js +105 -135
  93. package/dist/core/file-lock.js +104 -0
  94. package/dist/core/frontmatter.js +75 -8
  95. package/dist/core/lesson-lint.js +3 -0
  96. package/dist/core/markdown.js +20 -0
  97. package/dist/core/memory-belief.js +62 -0
  98. package/dist/core/memory-contradiction-detect.js +274 -0
  99. package/dist/core/memory-improve.js +806 -0
  100. package/dist/core/parse.js +158 -0
  101. package/dist/core/paths.js +280 -14
  102. package/dist/core/proposal-quality-validators.js +380 -0
  103. package/dist/core/proposal-validators.js +69 -0
  104. package/dist/core/proposals.js +512 -42
  105. package/dist/core/state-db.js +1068 -0
  106. package/dist/core/text-truncation.js +107 -0
  107. package/dist/core/time.js +54 -0
  108. package/dist/core/tty.js +59 -0
  109. package/dist/core/warn.js +64 -1
  110. package/dist/core/write-source.js +3 -0
  111. package/dist/indexer/db-backup.js +391 -0
  112. package/dist/indexer/db-search.js +198 -489
  113. package/dist/indexer/db.js +990 -108
  114. package/dist/indexer/ensure-index.js +136 -0
  115. package/dist/indexer/file-context.js +3 -0
  116. package/dist/indexer/graph-boost.js +376 -101
  117. package/dist/indexer/graph-db.js +391 -0
  118. package/dist/indexer/graph-dedup.js +95 -0
  119. package/dist/indexer/graph-extraction.js +550 -114
  120. package/dist/indexer/index-context.js +4 -0
  121. package/dist/indexer/indexer.js +547 -309
  122. package/dist/indexer/llm-cache.js +52 -0
  123. package/dist/indexer/manifest.js +3 -0
  124. package/dist/indexer/matchers.js +167 -160
  125. package/dist/indexer/memory-inference.js +152 -74
  126. package/dist/indexer/metadata-contributors.js +29 -0
  127. package/dist/indexer/metadata.js +275 -196
  128. package/dist/indexer/path-resolver.js +92 -0
  129. package/dist/indexer/project-context.js +192 -0
  130. package/dist/indexer/ranking-contributors.js +331 -0
  131. package/dist/indexer/ranking.js +81 -0
  132. package/dist/indexer/search-fields.js +5 -9
  133. package/dist/indexer/search-hit-enrichers.js +111 -0
  134. package/dist/indexer/search-source.js +44 -10
  135. package/dist/indexer/semantic-status.js +6 -17
  136. package/dist/indexer/staleness-detect.js +447 -0
  137. package/dist/indexer/usage-events.js +12 -9
  138. package/dist/indexer/walker.js +28 -0
  139. package/dist/integrations/agent/builders.js +135 -0
  140. package/dist/integrations/agent/config.js +122 -230
  141. package/dist/integrations/agent/detect.js +3 -0
  142. package/dist/integrations/agent/index.js +7 -13
  143. package/dist/integrations/agent/model-aliases.js +55 -0
  144. package/dist/integrations/agent/profiles.js +70 -5
  145. package/dist/integrations/agent/prompts.js +250 -36
  146. package/dist/integrations/agent/runner.js +151 -0
  147. package/dist/integrations/agent/sdk-runner.js +126 -0
  148. package/dist/integrations/agent/spawn.js +183 -35
  149. package/dist/integrations/github.js +3 -0
  150. package/dist/integrations/lockfile.js +32 -69
  151. package/dist/integrations/session-logs/index.js +69 -0
  152. package/dist/integrations/session-logs/inline-refs.js +35 -0
  153. package/dist/integrations/session-logs/pre-filter.js +152 -0
  154. package/dist/integrations/session-logs/providers/claude-code.js +282 -0
  155. package/dist/integrations/session-logs/providers/opencode.js +258 -0
  156. package/dist/integrations/session-logs/types.js +4 -0
  157. package/dist/llm/call-ai.js +62 -0
  158. package/dist/llm/client.js +79 -88
  159. package/dist/llm/embedder.js +20 -29
  160. package/dist/llm/embedders/cache.js +3 -7
  161. package/dist/llm/embedders/local.js +42 -1
  162. package/dist/llm/embedders/remote.js +20 -8
  163. package/dist/llm/embedders/types.js +3 -7
  164. package/dist/llm/feature-gate.js +95 -48
  165. package/dist/llm/graph-extract.js +676 -72
  166. package/dist/llm/index-passes.js +44 -29
  167. package/dist/llm/memory-infer.js +80 -71
  168. package/dist/llm/metadata-enhance.js +42 -29
  169. package/dist/llm/prompts/extract-session.md +80 -0
  170. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  171. package/dist/output/cli-hints-full.md +292 -0
  172. package/dist/output/cli-hints-short.md +66 -0
  173. package/dist/output/cli-hints.js +7 -311
  174. package/dist/output/context.js +60 -8
  175. package/dist/output/renderers.js +306 -258
  176. package/dist/output/shapes/curate.js +56 -0
  177. package/dist/output/shapes/distill.js +10 -0
  178. package/dist/output/shapes/env-list.js +19 -0
  179. package/dist/output/shapes/events.js +11 -0
  180. package/dist/output/shapes/helpers.js +424 -0
  181. package/dist/output/shapes/history.js +7 -0
  182. package/dist/output/shapes/passthrough.js +102 -0
  183. package/dist/output/shapes/proposal-accept.js +7 -0
  184. package/dist/output/shapes/proposal-diff.js +7 -0
  185. package/dist/output/shapes/proposal-list.js +7 -0
  186. package/dist/output/shapes/proposal-producer.js +11 -0
  187. package/dist/output/shapes/proposal-reject.js +7 -0
  188. package/dist/output/shapes/proposal-show.js +7 -0
  189. package/dist/output/shapes/registry-search.js +6 -0
  190. package/dist/output/shapes/registry.js +30 -0
  191. package/dist/output/shapes/search.js +6 -0
  192. package/dist/output/shapes/secret-list.js +19 -0
  193. package/dist/output/shapes/show.js +6 -0
  194. package/dist/output/shapes/vault-list.js +19 -0
  195. package/dist/output/shapes.js +51 -511
  196. package/dist/output/text/add.js +6 -0
  197. package/dist/output/text/clone.js +6 -0
  198. package/dist/output/text/config.js +6 -0
  199. package/dist/output/text/curate.js +6 -0
  200. package/dist/output/text/distill.js +7 -0
  201. package/dist/output/text/enable-disable.js +7 -0
  202. package/dist/output/text/events.js +10 -0
  203. package/dist/output/text/feedback.js +6 -0
  204. package/dist/output/text/helpers.js +1039 -0
  205. package/dist/output/text/history.js +7 -0
  206. package/dist/output/text/import.js +6 -0
  207. package/dist/output/text/index.js +6 -0
  208. package/dist/output/text/info.js +6 -0
  209. package/dist/output/text/init.js +6 -0
  210. package/dist/output/text/list.js +6 -0
  211. package/dist/output/text/proposal-producer.js +8 -0
  212. package/dist/output/text/proposal.js +11 -0
  213. package/dist/output/text/registry-commands.js +11 -0
  214. package/dist/output/text/registry.js +30 -0
  215. package/dist/output/text/remember.js +6 -0
  216. package/dist/output/text/remove.js +6 -0
  217. package/dist/output/text/save.js +6 -0
  218. package/dist/output/text/search.js +6 -0
  219. package/dist/output/text/show.js +6 -0
  220. package/dist/output/text/update.js +6 -0
  221. package/dist/output/text/upgrade.js +6 -0
  222. package/dist/output/text/vault.js +16 -0
  223. package/dist/output/text/wiki.js +15 -0
  224. package/dist/output/text/workflow.js +14 -0
  225. package/dist/output/text.js +44 -1093
  226. package/dist/registry/build-index.js +3 -0
  227. package/dist/registry/create-provider-registry.js +3 -0
  228. package/dist/registry/factory.js +4 -1
  229. package/dist/registry/origin-resolve.js +3 -0
  230. package/dist/registry/providers/index.js +3 -0
  231. package/dist/registry/providers/skills-sh.js +71 -50
  232. package/dist/registry/providers/static-index.js +53 -48
  233. package/dist/registry/providers/types.js +3 -24
  234. package/dist/registry/resolve.js +11 -16
  235. package/dist/registry/types.js +3 -0
  236. package/dist/scripts/migrate-storage.js +17750 -0
  237. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  238. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  239. package/dist/setup/detect.js +3 -0
  240. package/dist/setup/ripgrep-install.js +3 -0
  241. package/dist/setup/ripgrep-resolve.js +3 -0
  242. package/dist/setup/setup.js +775 -37
  243. package/dist/setup/steps.js +3 -15
  244. package/dist/sources/include.js +3 -0
  245. package/dist/sources/provider-factory.js +5 -12
  246. package/dist/sources/provider.js +3 -20
  247. package/dist/sources/providers/filesystem.js +19 -23
  248. package/dist/sources/providers/git.js +179 -20
  249. package/dist/sources/providers/index.js +3 -0
  250. package/dist/sources/providers/install-types.js +3 -13
  251. package/dist/sources/providers/npm.js +3 -4
  252. package/dist/sources/providers/provider-utils.js +3 -0
  253. package/dist/sources/providers/sync-from-ref.js +3 -11
  254. package/dist/sources/providers/tar-utils.js +3 -0
  255. package/dist/sources/providers/website.js +18 -22
  256. package/dist/sources/resolve.js +3 -0
  257. package/dist/sources/types.js +3 -0
  258. package/dist/sources/website-ingest.js +7 -0
  259. package/dist/tasks/backends/cron.js +203 -0
  260. package/dist/tasks/backends/exec-utils.js +28 -0
  261. package/dist/tasks/backends/index.js +24 -0
  262. package/dist/tasks/backends/launchd-template.xml +19 -0
  263. package/dist/tasks/backends/launchd.js +187 -0
  264. package/dist/tasks/backends/schtasks-template.xml +29 -0
  265. package/dist/tasks/backends/schtasks.js +215 -0
  266. package/dist/tasks/parser.js +211 -0
  267. package/dist/tasks/resolveAkmBin.js +87 -0
  268. package/dist/tasks/runner.js +458 -0
  269. package/dist/tasks/schedule.js +227 -0
  270. package/dist/tasks/schema.js +15 -0
  271. package/dist/tasks/validator.js +62 -0
  272. package/dist/version.js +3 -0
  273. package/dist/wiki/index-template.md +12 -0
  274. package/dist/wiki/ingest-workflow-template.md +54 -0
  275. package/dist/wiki/log-template.md +8 -0
  276. package/dist/wiki/schema-template.md +61 -0
  277. package/dist/wiki/wiki-templates.js +15 -0
  278. package/dist/wiki/wiki.js +13 -61
  279. package/dist/workflows/authoring.js +8 -25
  280. package/dist/workflows/cli.js +3 -0
  281. package/dist/workflows/db.js +141 -2
  282. package/dist/workflows/document-cache.js +3 -10
  283. package/dist/workflows/parser.js +3 -0
  284. package/dist/workflows/renderer.js +11 -3
  285. package/dist/workflows/runs.js +91 -89
  286. package/dist/workflows/schema.js +3 -0
  287. package/dist/workflows/scope-key.js +79 -0
  288. package/dist/workflows/validator.js +4 -8
  289. package/dist/workflows/workflow-template.md +24 -0
  290. package/docs/README.md +10 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.0.md +1 -1
  293. package/docs/migration/release-notes/0.7.4.md +1 -1
  294. package/docs/migration/release-notes/0.7.5.md +20 -0
  295. package/docs/migration/release-notes/0.8.0.md +48 -0
  296. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  297. package/package.json +29 -11
  298. package/dist/commands/install-audit.js +0 -381
  299. package/dist/commands/vault.js +0 -333
  300. package/dist/templates/wiki-templates.js +0 -100
@@ -1,20 +1,24 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
1
4
  import fs from "node:fs";
2
5
  import path from "node:path";
3
6
  import { deriveCanonicalAssetName, deriveCanonicalAssetNameFromStashRoot, isRelevantAssetFile, } from "../core/asset-spec";
4
- import { isAssetType } from "../core/common";
5
- import { parseFrontmatter, toStringOrUndefined } from "../core/frontmatter";
7
+ import { asNonEmptyString, isAssetType, writeFileAtomic } from "../core/common";
8
+ import { parseFrontmatter } from "../core/frontmatter";
6
9
  import { isVerbose, warn } from "../core/warn";
7
10
  import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "./file-context";
11
+ import { applyMetadataContributors } from "./metadata-contributors";
8
12
  export const SCOPE_KEYS = ["user", "agent", "run", "channel"];
9
13
  // ── Load / Write ────────────────────────────────────────────────────────────
10
14
  const STASH_FILENAME = ".stash.json";
11
15
  // ── Quality semantics (v1 spec §4.2) ────────────────────────────────────────
12
16
  /**
13
- * Well-known quality values. `generated` and `curated` are included in
17
+ * Well-known quality values. `generated`, `curated`, and `enriched` are included in
14
18
  * default search; `proposed` is excluded by default and opt-in via
15
19
  * `--include-proposed`. Unknown values warn once and remain searchable.
16
20
  */
17
- export const KNOWN_QUALITY_VALUES = new Set(["generated", "curated", "proposed"]);
21
+ export const KNOWN_QUALITY_VALUES = new Set(["generated", "curated", "enriched", "proposed"]);
18
22
  /** Tracks unknown quality values we've already warned about (one warn per value per process). */
19
23
  const warnedUnknownQualityValues = new Set();
20
24
  /**
@@ -80,20 +84,7 @@ export function loadStashFile(dirPath, options) {
80
84
  }
81
85
  export function writeStashFile(dirPath, stash) {
82
86
  const filePath = stashFilePath(dirPath);
83
- const tmpPath = `${filePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
84
- try {
85
- fs.writeFileSync(tmpPath, `${JSON.stringify(stash, null, 2)}\n`, "utf8");
86
- fs.renameSync(tmpPath, filePath);
87
- }
88
- catch (err) {
89
- try {
90
- fs.unlinkSync(tmpPath);
91
- }
92
- catch {
93
- /* ignore cleanup failure */
94
- }
95
- throw err;
96
- }
87
+ writeFileAtomic(filePath, `${JSON.stringify(stash, null, 2)}\n`);
97
88
  }
98
89
  /**
99
90
  * Validate and normalize a raw object into a `StashEntry`.
@@ -185,19 +176,38 @@ export function validateStashEntry(entry) {
185
176
  if (typeof e.pageKind === "string" && e.pageKind.trim().length > 0) {
186
177
  result.pageKind = e.pageKind.trim();
187
178
  }
188
- if (Array.isArray(e.xrefs)) {
189
- const filtered = e.xrefs
190
- .filter((x) => typeof x === "string" && x.trim().length > 0)
191
- .map((x) => x.trim());
192
- if (filtered.length > 0)
193
- result.xrefs = filtered;
179
+ const xrefs = normalizeNonEmptyStringList(e.xrefs);
180
+ if (xrefs)
181
+ result.xrefs = xrefs;
182
+ const sources = normalizeNonEmptyStringList(e.sources);
183
+ if (sources)
184
+ result.sources = sources;
185
+ if (typeof e.beliefState === "string" && e.beliefState.trim().length > 0) {
186
+ result.beliefState = e.beliefState.trim();
194
187
  }
195
- if (Array.isArray(e.sources)) {
196
- const filtered = e.sources
197
- .filter((s) => typeof s === "string" && s.trim().length > 0)
198
- .map((s) => s.trim());
199
- if (filtered.length > 0)
200
- result.sources = filtered;
188
+ const supersededBy = normalizeNonEmptyStringList(e.supersededBy);
189
+ if (supersededBy)
190
+ result.supersededBy = supersededBy;
191
+ const contradictedBy = normalizeNonEmptyStringList(e.contradictedBy);
192
+ if (contradictedBy)
193
+ result.contradictedBy = contradictedBy;
194
+ const currentBeliefRefs = normalizeNonEmptyStringList(e.currentBeliefRefs);
195
+ if (currentBeliefRefs)
196
+ result.currentBeliefRefs = currentBeliefRefs;
197
+ if (e.captureMode === "hot" || e.captureMode === "background") {
198
+ result.captureMode = e.captureMode;
199
+ }
200
+ if (typeof e.whenToUse === "string" && e.whenToUse.trim().length > 0) {
201
+ result.whenToUse = e.whenToUse.trim();
202
+ }
203
+ if (typeof e.lessonStrength === "number" && Number.isFinite(e.lessonStrength) && e.lessonStrength >= 0) {
204
+ result.lessonStrength = Math.floor(e.lessonStrength);
205
+ }
206
+ const evidenceSources = normalizeNonEmptyStringList(e.evidenceSources);
207
+ if (evidenceSources)
208
+ result.evidenceSources = evidenceSources;
209
+ if (typeof e.derivedFrom === "string" && e.derivedFrom.trim().length > 0) {
210
+ result.derivedFrom = e.derivedFrom.trim();
201
211
  }
202
212
  if (typeof e.scope === "object" && e.scope !== null && !Array.isArray(e.scope)) {
203
213
  const scope = normalizeScopeObject(e.scope);
@@ -275,9 +285,9 @@ function normalizeIntent(value) {
275
285
  return undefined;
276
286
  const raw = value;
277
287
  const intent = {};
278
- const when = toStringOrUndefined(raw.when);
279
- const input = toStringOrUndefined(raw.input);
280
- const output = toStringOrUndefined(raw.output);
288
+ const when = asNonEmptyString(raw.when);
289
+ const input = asNonEmptyString(raw.input);
290
+ const output = asNonEmptyString(raw.output);
281
291
  if (when)
282
292
  intent.when = when;
283
293
  if (input)
@@ -290,7 +300,7 @@ function normalizeStringListOrUndefined(value) {
290
300
  return normalizeNonEmptyStringList(value);
291
301
  }
292
302
  export function applyCuratedFrontmatter(entry, fmData) {
293
- const description = toStringOrUndefined(fmData.description);
303
+ const description = asNonEmptyString(fmData.description);
294
304
  if (description) {
295
305
  entry.description = description;
296
306
  entry.source = "frontmatter";
@@ -311,18 +321,72 @@ export function applyCuratedFrontmatter(entry, fmData) {
311
321
  const examples = normalizeStringListOrUndefined(fmData.examples);
312
322
  if (examples)
313
323
  entry.examples = examples;
314
- const run = toStringOrUndefined(fmData.run);
324
+ const run = asNonEmptyString(fmData.run);
315
325
  if (run)
316
326
  entry.run = run;
317
- const setup = toStringOrUndefined(fmData.setup);
327
+ const setup = asNonEmptyString(fmData.setup);
318
328
  if (setup)
319
329
  entry.setup = setup;
320
- const cwd = toStringOrUndefined(fmData.cwd);
330
+ const cwd = asNonEmptyString(fmData.cwd);
321
331
  if (cwd)
322
332
  entry.cwd = cwd;
323
- const quality = toStringOrUndefined(fmData.quality);
333
+ const quality = asNonEmptyString(fmData.quality);
324
334
  if (quality)
325
335
  entry.quality = normalizeQuality(quality);
336
+ const beliefState = asNonEmptyString(fmData.beliefState);
337
+ if (beliefState)
338
+ entry.beliefState = beliefState;
339
+ const supersededBy = normalizeStringListOrUndefined(fmData.supersededBy);
340
+ if (supersededBy)
341
+ entry.supersededBy = supersededBy;
342
+ const contradictedBy = normalizeStringListOrUndefined(fmData.contradictedBy);
343
+ if (contradictedBy)
344
+ entry.contradictedBy = contradictedBy;
345
+ const currentBeliefRefs = normalizeStringListOrUndefined(fmData.currentBeliefRefs);
346
+ if (currentBeliefRefs)
347
+ entry.currentBeliefRefs = currentBeliefRefs;
348
+ // captureMode: "hot" | "background" — strict whitelist; unknown values are ignored.
349
+ if (fmData.captureMode === "hot" || fmData.captureMode === "background") {
350
+ entry.captureMode = fmData.captureMode;
351
+ }
352
+ // when_to_use → whenToUse — free-form guidance for retrieval/intent matching.
353
+ const whenToUse = asNonEmptyString(fmData.when_to_use);
354
+ if (whenToUse)
355
+ entry.whenToUse = whenToUse;
356
+ // lessonStrength: array → length, number → direct. Negative numbers clamp to 0.
357
+ if (Array.isArray(fmData.lessonStrength)) {
358
+ entry.lessonStrength = fmData.lessonStrength.length;
359
+ }
360
+ else if (typeof fmData.lessonStrength === "number" && Number.isFinite(fmData.lessonStrength)) {
361
+ entry.lessonStrength = Math.max(0, Math.floor(fmData.lessonStrength));
362
+ }
363
+ const evidenceSources = normalizeStringListOrUndefined(fmData.evidenceSources);
364
+ if (evidenceSources)
365
+ entry.evidenceSources = evidenceSources;
366
+ // Phase 5A / Advantage D5: capture parent ref for derived memories.
367
+ // Memory-inference writes `source: "memory:<parent>"` and `inferred: true`
368
+ // (and a derived child name suffix `.derived`). We mirror that source ref
369
+ // into `entry.derivedFrom` so the indexer can populate the dedicated
370
+ // `derived_from` column. Non-derived entries leave this field unset.
371
+ if (entry.type === "memory") {
372
+ const isDerivedByName = entry.name.toLowerCase().endsWith(".derived");
373
+ const isDerivedByFm = fmData.inferred === true;
374
+ if (isDerivedByName || isDerivedByFm) {
375
+ const sourceStr = asNonEmptyString(fmData.source);
376
+ if (sourceStr?.includes(":")) {
377
+ entry.derivedFrom = sourceStr;
378
+ }
379
+ else {
380
+ // Fallback: some legacy renderings store only `derivedFrom: <name>`
381
+ // (a bare parent name). Promote it to a `memory:` ref so the lookup
382
+ // column stays consistent.
383
+ const derivedFromName = asNonEmptyString(fmData.derivedFrom);
384
+ if (derivedFromName) {
385
+ entry.derivedFrom = derivedFromName.includes(":") ? derivedFromName : `memory:${derivedFromName}`;
386
+ }
387
+ }
388
+ }
389
+ }
326
390
  const intent = normalizeIntent(fmData.intent);
327
391
  if (intent)
328
392
  entry.intent = intent;
@@ -416,6 +480,29 @@ export function shouldIndexStashFile(stashRoot, file, options) {
416
480
  const segments = relPath.split(/[\\/]+/).filter(Boolean);
417
481
  if (segments.length === 0)
418
482
  return true;
483
+ // Skip env / vault .env files that have a sibling .sensitive marker file.
484
+ if ((segments[0] === "env" || segments[0] === "vaults") &&
485
+ (file.endsWith(".env") || path.basename(file) === ".env")) {
486
+ const markerPath = file.replace(/\.env$/, ".sensitive");
487
+ if (fs.existsSync(markerPath))
488
+ return false;
489
+ }
490
+ // Deprecation: once a stash has migrated to the `env/` directory, the legacy
491
+ // `vaults/` copy is frozen. Skip indexing it so the same keys are not
492
+ // double-surfaced under both `vault:` and `env:`. (Pre-migration stashes
493
+ // with no `env/` dir still index `vaults/` normally.)
494
+ if (segments[0] === "vaults" && fs.existsSync(path.join(stashRoot, "env"))) {
495
+ return false;
496
+ }
497
+ // Skip secret files that are themselves a `.sensitive` marker, or that have a
498
+ // sibling `<name>.sensitive` marker. Secrets are otherwise indexed by name
499
+ // only (their bytes are never read — see buildEntryFromFile guards).
500
+ if (segments[0] === "secrets") {
501
+ if (file.endsWith(".sensitive") || file.endsWith(".lock"))
502
+ return false;
503
+ if (fs.existsSync(`${file}.sensitive`))
504
+ return false;
505
+ }
419
506
  if (options?.treatStashRootAsWikiRoot) {
420
507
  return !(segments.length === 1 && WIKI_INFRA_FILES.has(segments[0]));
421
508
  }
@@ -681,7 +768,140 @@ function mergeAliases(existing, generated) {
681
768
  const merged = normalizeTerms([...(existing ?? []), ...generated]);
682
769
  return merged.length > 0 ? merged : undefined;
683
770
  }
771
+ // ── Enrichment Completeness ─────────────────────────────────────────────────
772
+ /**
773
+ * Returns `true` when a stash entry already has enough LLM-quality metadata
774
+ * that calling the LLM would produce no meaningful improvement.
775
+ *
776
+ * An entry is considered complete when ALL of the following hold:
777
+ * - `description` is a non-empty string
778
+ * - `tags` is a non-empty array
779
+ * - `searchHints` is a non-empty array
780
+ *
781
+ * This predicate is used by `enhanceDirsWithLlm` to skip the LLM call for
782
+ * entries that were previously enriched and already carry all three fields.
783
+ * Pass `reEnrich = true` in the caller to bypass this check.
784
+ */
785
+ export function isEnrichmentComplete(entry) {
786
+ const hasDescription = typeof entry.description === "string" && entry.description.trim().length > 0;
787
+ const hasTags = Array.isArray(entry.tags) && entry.tags.length > 0;
788
+ const hasSearchHints = Array.isArray(entry.searchHints) && entry.searchHints.length > 0;
789
+ return hasDescription && hasTags && hasSearchHints;
790
+ }
684
791
  // ── Metadata Generation ─────────────────────────────────────────────────────
792
+ /**
793
+ * Shared pipeline (steps 2-6) for building a single StashEntry from a file.
794
+ *
795
+ * Both `generateMetadata` and `generateMetadataFlat` perform identical work
796
+ * once the initial `entry` object has been seeded with type and canonical name.
797
+ * This helper encapsulates that shared pipeline so the two callers only differ
798
+ * in how they determine the asset type and canonical name (step 1):
799
+ *
800
+ * - `generateMetadata` — explicit `assetType` arg + `deriveCanonicalAssetName`
801
+ * - `generateMetadataFlat` — type from `runMatchers()` + `deriveCanonicalAssetNameFromStashRoot`
802
+ *
803
+ * @param file Absolute path to the file being processed.
804
+ * @param assetType Resolved asset type string (already validated by caller).
805
+ * @param canonicalName Resolved canonical name (already computed by caller).
806
+ * @param dirPath Directory containing the file (used for tag fallback).
807
+ * @param pkgMeta Pre-loaded package.json metadata for this directory (may be null/undefined).
808
+ * @param stashRoot Stash root used for renderer search hints context.
809
+ * @param ctx FileContext for the file (may be pre-built by the caller).
810
+ * @param match Pre-resolved MatchResult when available (from `generateMetadataFlat`).
811
+ * @returns The populated entry, or `{ skip: true, warning: string }` when the
812
+ * renderer throws and the file should be dropped.
813
+ */
814
+ async function buildEntryFromFile(file, assetType, canonicalName, dirPath, pkgMeta, stashRoot, ctx, match) {
815
+ const ext = path.extname(file).toLowerCase();
816
+ const baseName = path.basename(file, ext);
817
+ const entry = {
818
+ name: canonicalName,
819
+ type: assetType,
820
+ quality: "generated",
821
+ confidence: 0.55,
822
+ source: "filename",
823
+ };
824
+ // Priority 1: Package.json metadata
825
+ if (pkgMeta) {
826
+ if (pkgMeta.description && !entry.description) {
827
+ entry.description = pkgMeta.description;
828
+ entry.source = "package";
829
+ entry.confidence = 0.8;
830
+ }
831
+ if (pkgMeta.keywords && pkgMeta.keywords.length > 0)
832
+ entry.tags = normalizeTerms(pkgMeta.keywords);
833
+ }
834
+ // Priority 2: Frontmatter (for .md files -- overrides package.json description)
835
+ // Secrets are excluded even when the file happens to be `.md`: the whole file
836
+ // is the secret value and must never be read for frontmatter or any metadata.
837
+ if (ext === ".md" && assetType !== "secret") {
838
+ const content = ctx.content();
839
+ const parsed = parseFrontmatter(content);
840
+ applyCuratedFrontmatter(entry, parsed.data);
841
+ // Extract parameters from frontmatter params: key
842
+ const fmParams = extractFrontmatterParameters(parsed.data);
843
+ if (fmParams)
844
+ entry.parameters = fmParams;
845
+ // Pass wiki-pattern frontmatter through onto the entry
846
+ applyWikiFrontmatter(entry, parsed.data);
847
+ // Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
848
+ if (entry.type === "command") {
849
+ const cmdParams = extractCommandParameters(parsed.content);
850
+ if (cmdParams) {
851
+ entry.parameters = mergeParameters(entry.parameters, cmdParams);
852
+ }
853
+ }
854
+ }
855
+ // Extract @param from script files.
856
+ // Env / vault files (.env) and secret files (whole-file secrets) are
857
+ // deliberately excluded — their contents are secrets and must never be
858
+ // parsed for @param or any other metadata that could embed a value into the
859
+ // entry.
860
+ if (ext !== ".md" && assetType !== "env" && assetType !== "vault" && assetType !== "secret") {
861
+ const content = ctx.content();
862
+ const scriptParams = extractScriptParameters(file, content);
863
+ if (scriptParams)
864
+ entry.parameters = scriptParams;
865
+ applyCommentMetadata(entry, extractCommentMetadata(file, content));
866
+ }
867
+ // Priority 3: Renderer metadata extraction
868
+ // When no pre-resolved match is available (generateMetadata path), run
869
+ // matchers now so the renderer can extract type-specific metadata.
870
+ const resolvedMatch = match ?? (await runMatchers(ctx));
871
+ if (resolvedMatch) {
872
+ const renderer = await getRenderer(resolvedMatch.renderer);
873
+ if (renderer) {
874
+ const renderCtx = buildRenderContext(ctx, resolvedMatch, [stashRoot]);
875
+ try {
876
+ await applyMetadataContributors(entry, {
877
+ rendererName: renderer.name,
878
+ renderContext: renderCtx,
879
+ });
880
+ }
881
+ catch (error) {
882
+ return {
883
+ skip: true,
884
+ warning: buildMetadataSkipWarning(file, assetType, error),
885
+ };
886
+ }
887
+ }
888
+ }
889
+ // Priority 4: Filename heuristics (fallback)
890
+ if (!entry.description) {
891
+ entry.description = fileNameToDescription(baseName);
892
+ entry.source = "filename";
893
+ entry.confidence = Math.min(entry.confidence ?? 0.55, 0.55);
894
+ }
895
+ if (!entry.tags || entry.tags.length === 0) {
896
+ entry.tags = extractTagsFromPath(file, dirPath);
897
+ }
898
+ entry.tags = normalizeTerms(entry.tags ?? []);
899
+ entry.aliases = mergeAliases(entry.aliases, buildAliases(canonicalName, entry.tags));
900
+ // Search hints are only generated when LLM is configured (via enhanceStashWithLlm)
901
+ // Heuristic search hints are too noisy to be useful for search quality
902
+ entry.filename = path.basename(file);
903
+ return entry;
904
+ }
685
905
  export async function generateMetadata(dirPath, assetType, files, typeRoot = dirPath) {
686
906
  const entries = [];
687
907
  const warnings = [];
@@ -694,84 +914,16 @@ export async function generateMetadata(dirPath, assetType, files, typeRoot = dir
694
914
  if (!isRelevantAssetFile(assetType, fileName))
695
915
  continue;
696
916
  const canonicalName = deriveCanonicalAssetName(assetType, typeRoot, file) ?? baseName;
697
- const entry = {
698
- name: canonicalName,
699
- type: assetType,
700
- quality: "generated",
701
- confidence: 0.55,
702
- source: "filename",
703
- };
704
- // Priority 1: Package.json metadata
705
- if (pkgMeta) {
706
- if (pkgMeta.description && !entry.description) {
707
- entry.description = pkgMeta.description;
708
- entry.source = "package";
709
- entry.confidence = 0.8;
710
- }
711
- if (pkgMeta.keywords && pkgMeta.keywords.length > 0)
712
- entry.tags = normalizeTerms(pkgMeta.keywords);
713
- }
714
- // Priority 2: Frontmatter (for .md files -- overrides package.json description)
715
- if (ext === ".md") {
716
- const content = fs.readFileSync(file, "utf8");
717
- const parsed = parseFrontmatter(content);
718
- applyCuratedFrontmatter(entry, parsed.data);
719
- // Extract parameters from frontmatter params: key
720
- const fmParams = extractFrontmatterParameters(parsed.data);
721
- if (fmParams)
722
- entry.parameters = fmParams;
723
- // Pass wiki-pattern frontmatter through onto the entry
724
- applyWikiFrontmatter(entry, parsed.data);
725
- // Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
726
- if (entry.type === "command") {
727
- const cmdParams = extractCommandParameters(parsed.content);
728
- if (cmdParams) {
729
- entry.parameters = mergeParameters(entry.parameters, cmdParams);
730
- }
731
- }
732
- }
733
- // Extract @param from script files.
734
- // Vault files (.env) are deliberately excluded — their contents are secrets
735
- // and must never be parsed for @param or any other metadata that could
736
- // embed a value into the entry.
737
- if (ext !== ".md" && assetType !== "vault") {
738
- const content = fs.readFileSync(file, "utf8");
739
- const scriptParams = extractScriptParameters(file, content);
740
- if (scriptParams)
741
- entry.parameters = scriptParams;
742
- applyCommentMetadata(entry, extractCommentMetadata(file, content));
743
- }
744
- // Priority 3: Type-specific metadata extraction (e.g. TOC for knowledge, comments for scripts)
917
+ // Build file context with typeRoot as the stash root so renderer context
918
+ // and search hints are scoped to the type directory.
745
919
  const fileCtx = buildFileContext(typeRoot, file);
746
- const match = await runMatchers(fileCtx);
747
- if (match) {
748
- const renderer = await getRenderer(match.renderer);
749
- if (renderer?.extractMetadata) {
750
- const renderCtx = buildRenderContext(fileCtx, match, [typeRoot]);
751
- try {
752
- renderer.extractMetadata(entry, renderCtx);
753
- }
754
- catch (error) {
755
- warnings.push(buildMetadataSkipWarning(file, assetType, error));
756
- continue;
757
- }
758
- }
759
- }
760
- // Priority 4: Filename heuristics (fallback)
761
- if (!entry.description) {
762
- entry.description = fileNameToDescription(baseName);
763
- entry.source = "filename";
764
- entry.confidence = Math.min(entry.confidence ?? 0.55, 0.55);
765
- }
766
- if (!entry.tags || entry.tags.length === 0) {
767
- entry.tags = extractTagsFromPath(file, dirPath);
920
+ // Step 1: type is explicit; delegate steps 2-6 to the shared pipeline.
921
+ const result = await buildEntryFromFile(file, assetType, canonicalName, dirPath, pkgMeta, typeRoot, fileCtx, null);
922
+ if ("skip" in result) {
923
+ warnings.push(result.warning);
924
+ continue;
768
925
  }
769
- entry.tags = normalizeTerms(entry.tags ?? []);
770
- entry.aliases = mergeAliases(entry.aliases, buildAliases(canonicalName, entry.tags));
771
- // Search hints are only generated when LLM is configured (via enhanceStashWithLlm)
772
- // Heuristic search hints are too noisy to be useful for search quality
773
- entry.filename = path.basename(file);
774
- entries.push(entry);
926
+ entries.push(result);
775
927
  }
776
928
  return warnings.length > 0 ? { entries, warnings } : { entries };
777
929
  }
@@ -789,6 +941,7 @@ export async function generateMetadataFlat(stashRoot, files) {
789
941
  for (const file of files) {
790
942
  if (!shouldIndexStashFile(stashRoot, file))
791
943
  continue;
944
+ // Step 1: determine type and canonical name via the matcher system.
792
945
  const ctx = buildFileContext(stashRoot, file);
793
946
  const match = await runMatchers(ctx);
794
947
  if (!match)
@@ -802,83 +955,20 @@ export async function generateMetadataFlat(stashRoot, files) {
802
955
  const ext = path.extname(file).toLowerCase();
803
956
  const baseName = path.basename(file, ext);
804
957
  const canonicalName = deriveCanonicalAssetNameFromStashRoot(assetType, stashRoot, file) ?? baseName;
805
- const entry = {
806
- name: canonicalName,
807
- type: assetType,
808
- quality: "generated",
809
- confidence: 0.55,
810
- source: "filename",
811
- };
812
- // Package.json metadata
958
+ // Resolve package.json metadata with a per-directory cache.
813
959
  const dirPath = path.dirname(file);
814
960
  if (!pkgMetaCache.has(dirPath)) {
815
961
  pkgMetaCache.set(dirPath, extractPackageMetadata(dirPath));
816
962
  }
817
963
  const pkgMeta = pkgMetaCache.get(dirPath);
818
- if (pkgMeta) {
819
- if (pkgMeta.description && !entry.description) {
820
- entry.description = pkgMeta.description;
821
- entry.source = "package";
822
- entry.confidence = 0.8;
823
- }
824
- if (pkgMeta.keywords?.length)
825
- entry.tags = normalizeTerms(pkgMeta.keywords);
826
- }
827
- // Frontmatter
828
- if (ext === ".md") {
829
- const content = ctx.content();
830
- const parsed = parseFrontmatter(content);
831
- applyCuratedFrontmatter(entry, parsed.data);
832
- // Extract parameters from frontmatter params: key
833
- const fmParams = extractFrontmatterParameters(parsed.data);
834
- if (fmParams)
835
- entry.parameters = fmParams;
836
- // Pass wiki-pattern frontmatter through onto the entry
837
- applyWikiFrontmatter(entry, parsed.data);
838
- // Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
839
- if (entry.type === "command") {
840
- const cmdParams = extractCommandParameters(parsed.content);
841
- if (cmdParams) {
842
- entry.parameters = mergeParameters(entry.parameters, cmdParams);
843
- }
844
- }
845
- }
846
- // Extract @param from script files.
847
- // Vault files (.env) are deliberately excluded — their contents are secrets
848
- // and must never be parsed for @param or any other metadata that could
849
- // embed a value into the entry.
850
- if (ext !== ".md" && assetType !== "vault") {
851
- const content = ctx.content();
852
- const scriptParams = extractScriptParameters(file, content);
853
- if (scriptParams)
854
- entry.parameters = scriptParams;
855
- applyCommentMetadata(entry, extractCommentMetadata(file, content));
856
- }
857
- // Renderer metadata extraction
858
- const renderer = await getRenderer(match.renderer);
859
- if (renderer?.extractMetadata) {
860
- const renderCtx = buildRenderContext(ctx, match, [stashRoot]);
861
- try {
862
- renderer.extractMetadata(entry, renderCtx);
863
- }
864
- catch (error) {
865
- warnings.push(buildMetadataSkipWarning(file, assetType, error));
866
- continue;
867
- }
868
- }
869
- // Filename heuristics fallback
870
- if (!entry.description) {
871
- entry.description = fileNameToDescription(baseName);
872
- entry.source = "filename";
873
- entry.confidence = Math.min(entry.confidence ?? 0.55, 0.55);
874
- }
875
- if (!entry.tags || entry.tags.length === 0) {
876
- entry.tags = extractTagsFromPath(file, dirPath);
964
+ // Steps 2-6: delegate to the shared pipeline; pass the pre-resolved match
965
+ // so we don't run matchers a second time.
966
+ const result = await buildEntryFromFile(file, assetType, canonicalName, dirPath, pkgMeta, stashRoot, ctx, match);
967
+ if ("skip" in result) {
968
+ warnings.push(result.warning);
969
+ continue;
877
970
  }
878
- entry.tags = normalizeTerms(entry.tags ?? []);
879
- entry.aliases = mergeAliases(entry.aliases, buildAliases(canonicalName, entry.tags));
880
- entry.filename = path.basename(file);
881
- entries.push(entry);
971
+ entries.push(result);
882
972
  }
883
973
  return warnings.length > 0 ? { entries, warnings } : { entries };
884
974
  }
@@ -980,17 +1070,6 @@ export function extractDescriptionFromComments(filePath) {
980
1070
  return hashLines.join(" ");
981
1071
  return null;
982
1072
  }
983
- export function extractFrontmatterDescription(filePath) {
984
- let content;
985
- try {
986
- content = fs.readFileSync(filePath, "utf8");
987
- }
988
- catch {
989
- return null;
990
- }
991
- const parsed = parseFrontmatter(content);
992
- return toStringOrUndefined(parsed.data.description) ?? null;
993
- }
994
1073
  export function extractPackageMetadata(dirPath) {
995
1074
  const pkgPath = path.join(dirPath, "package.json");
996
1075
  if (!fs.existsSync(pkgPath))
@@ -0,0 +1,92 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { parseAssetRef } from "../core/asset-ref";
7
+ import { resolveAssetPathFromName, TYPE_DIRS } from "../core/asset-spec";
8
+ import { isWithin } from "../core/common";
9
+ import { resolveSourcesForOrigin } from "../registry/origin-resolve";
10
+ import { lookup } from "./indexer";
11
+ import { resolveSourceEntries } from "./search-source";
12
+ function normalizeRef(ref) {
13
+ return typeof ref === "string" ? parseAssetRef(ref) : ref;
14
+ }
15
+ function buildDiskCandidates(sourcePath, ref, preserveDirectNameFallback) {
16
+ const typeDir = path.join(sourcePath, TYPE_DIRS[ref.type] ?? `${ref.type}s`);
17
+ const candidates = [
18
+ resolveAssetPathFromName(ref.type, typeDir, ref.name),
19
+ path.join(sourcePath, ref.type, `${ref.name}.md`),
20
+ path.join(sourcePath, ref.type, ref.name),
21
+ ];
22
+ if (preserveDirectNameFallback) {
23
+ candidates.push(path.join(sourcePath, `${ref.name}.md`), path.join(sourcePath, ref.name));
24
+ }
25
+ return candidates;
26
+ }
27
+ function resolveDirectoryEntry(filePath, directoryIndexNames) {
28
+ let stat;
29
+ try {
30
+ stat = fs.statSync(filePath);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ if (stat.isFile())
36
+ return filePath;
37
+ if (!stat.isDirectory())
38
+ return null;
39
+ for (const indexName of directoryIndexNames) {
40
+ const candidate = path.join(filePath, indexName);
41
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
42
+ return candidate;
43
+ }
44
+ return null;
45
+ }
46
+ async function resolveViaIndex(ref) {
47
+ try {
48
+ const entry = await lookup(ref);
49
+ return entry?.filePath ?? null;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ function resolveViaDisk(ref, options) {
56
+ let sources = resolveSourceEntries(options.stashDir);
57
+ if (options.honorOrigin !== false) {
58
+ sources = resolveSourcesForOrigin(ref.origin, sources);
59
+ }
60
+ const directoryIndexNames = options.directoryIndexNames ?? ["SKILL.md"];
61
+ const preserveDirectNameFallback = options.preserveDirectNameFallback ?? true;
62
+ for (const source of sources) {
63
+ if (options.writableDirSet && !options.writableDirSet.has(path.resolve(source.path)))
64
+ continue;
65
+ const candidates = buildDiskCandidates(source.path, ref, preserveDirectNameFallback);
66
+ for (const candidate of candidates) {
67
+ if (!fs.existsSync(candidate))
68
+ continue;
69
+ const resolved = resolveDirectoryEntry(candidate, directoryIndexNames);
70
+ if (!resolved)
71
+ continue;
72
+ const resolvedRoot = fs.realpathSync(source.path);
73
+ const realTarget = fs.realpathSync(resolved);
74
+ if (!isWithin(realTarget, resolvedRoot))
75
+ continue;
76
+ return realTarget;
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+ export async function resolveAssetPath(ref, options = {}) {
82
+ const parsed = normalizeRef(ref);
83
+ const mode = options.mode ?? "index-first";
84
+ if (mode !== "disk-only") {
85
+ const indexed = await resolveViaIndex(parsed);
86
+ if (indexed)
87
+ return indexed;
88
+ if (mode === "index-only")
89
+ return null;
90
+ }
91
+ return resolveViaDisk(parsed, options);
92
+ }