akm-cli 0.7.5 → 0.8.0-rc.6

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 (236) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +113 -2
  2. package/README.md +20 -4
  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.js +1995 -551
  9. package/dist/commands/agent-dispatch.js +110 -0
  10. package/dist/commands/agent-support.js +68 -0
  11. package/dist/commands/completions.js +3 -0
  12. package/dist/commands/config-cli.js +130 -534
  13. package/dist/commands/consolidate.js +1531 -0
  14. package/dist/commands/curate.js +44 -3
  15. package/dist/commands/db-cli.js +23 -0
  16. package/dist/commands/distill-promotion-policy.js +660 -0
  17. package/dist/commands/distill.js +990 -75
  18. package/dist/commands/eval-cases.js +43 -0
  19. package/dist/commands/events.js +5 -23
  20. package/dist/commands/graph.js +477 -0
  21. package/dist/commands/health.js +400 -0
  22. package/dist/commands/help/help-accept.md +9 -0
  23. package/dist/commands/help/help-improve.md +77 -0
  24. package/dist/commands/help/help-proposals.md +15 -0
  25. package/dist/commands/help/help-propose.md +17 -0
  26. package/dist/commands/help/help-reject.md +8 -0
  27. package/dist/commands/history.js +54 -46
  28. package/dist/commands/improve-profiles.js +146 -0
  29. package/dist/commands/improve-result-file.js +103 -0
  30. package/dist/commands/improve.js +2175 -0
  31. package/dist/commands/info.js +5 -2
  32. package/dist/commands/init.js +50 -2
  33. package/dist/commands/installed-stashes.js +102 -139
  34. package/dist/commands/knowledge.js +136 -0
  35. package/dist/commands/lint/agent-linter.js +49 -0
  36. package/dist/commands/lint/base-linter.js +479 -0
  37. package/dist/commands/lint/command-linter.js +49 -0
  38. package/dist/commands/lint/default-linter.js +16 -0
  39. package/dist/commands/lint/index.js +183 -0
  40. package/dist/commands/lint/knowledge-linter.js +16 -0
  41. package/dist/commands/lint/markdown-insertion.js +343 -0
  42. package/dist/commands/lint/memory-linter.js +61 -0
  43. package/dist/commands/lint/registry.js +36 -0
  44. package/dist/commands/lint/skill-linter.js +45 -0
  45. package/dist/commands/lint/task-linter.js +50 -0
  46. package/dist/commands/lint/types.js +4 -0
  47. package/dist/commands/lint/vault-key-rules.js +139 -0
  48. package/dist/commands/lint/workflow-linter.js +56 -0
  49. package/dist/commands/lint.js +4 -0
  50. package/dist/commands/migration-help.js +5 -2
  51. package/dist/commands/proposal.js +66 -12
  52. package/dist/commands/propose.js +86 -31
  53. package/dist/commands/reflect.js +1119 -73
  54. package/dist/commands/registry-search.js +5 -2
  55. package/dist/commands/remember.js +69 -6
  56. package/dist/commands/schema-repair.js +203 -0
  57. package/dist/commands/search.js +115 -14
  58. package/dist/commands/self-update.js +3 -0
  59. package/dist/commands/show.js +144 -25
  60. package/dist/commands/source-add.js +17 -45
  61. package/dist/commands/source-clone.js +3 -0
  62. package/dist/commands/source-manage.js +14 -19
  63. package/dist/commands/tasks.js +438 -0
  64. package/dist/commands/url-checker.js +42 -0
  65. package/dist/commands/vault.js +130 -77
  66. package/dist/core/action-contributors.js +28 -0
  67. package/dist/core/asset-ref.js +7 -0
  68. package/dist/core/asset-registry.js +7 -16
  69. package/dist/core/asset-serialize.js +88 -0
  70. package/dist/core/asset-spec.js +22 -0
  71. package/dist/core/common.js +157 -0
  72. package/dist/core/concurrent.js +25 -0
  73. package/dist/core/config-io.js +347 -0
  74. package/dist/core/config-migration.js +625 -0
  75. package/dist/core/config-schema.js +501 -0
  76. package/dist/core/config-sources.js +108 -0
  77. package/dist/core/config-types.js +4 -0
  78. package/dist/core/config-walker.js +337 -0
  79. package/dist/core/config.js +327 -987
  80. package/dist/core/errors.js +40 -19
  81. package/dist/core/events.js +91 -138
  82. package/dist/core/file-lock.js +104 -0
  83. package/dist/core/frontmatter.js +3 -6
  84. package/dist/core/lesson-lint.js +3 -0
  85. package/dist/core/markdown.js +20 -0
  86. package/dist/core/memory-belief.js +62 -0
  87. package/dist/core/memory-contradiction-detect.js +274 -0
  88. package/dist/core/memory-improve.js +806 -0
  89. package/dist/core/parse.js +158 -0
  90. package/dist/core/paths.js +326 -14
  91. package/dist/core/proposal-quality-validators.js +364 -0
  92. package/dist/core/proposal-validators.js +69 -0
  93. package/dist/core/proposals.js +498 -42
  94. package/dist/core/state-db.js +927 -0
  95. package/dist/core/text-truncation.js +107 -0
  96. package/dist/core/time.js +54 -0
  97. package/dist/core/warn.js +62 -1
  98. package/dist/core/write-source.js +3 -0
  99. package/dist/indexer/db-backup.js +391 -0
  100. package/dist/indexer/db-search.js +152 -253
  101. package/dist/indexer/db.js +933 -103
  102. package/dist/indexer/ensure-index.js +64 -0
  103. package/dist/indexer/file-context.js +3 -0
  104. package/dist/indexer/graph-boost.js +376 -101
  105. package/dist/indexer/graph-db.js +391 -0
  106. package/dist/indexer/graph-dedup.js +95 -0
  107. package/dist/indexer/graph-extraction.js +550 -124
  108. package/dist/indexer/index-context.js +4 -0
  109. package/dist/indexer/indexer.js +506 -291
  110. package/dist/indexer/llm-cache.js +47 -0
  111. package/dist/indexer/manifest.js +3 -0
  112. package/dist/indexer/matchers.js +148 -160
  113. package/dist/indexer/memory-inference.js +99 -74
  114. package/dist/indexer/metadata-contributors.js +29 -0
  115. package/dist/indexer/metadata.js +255 -196
  116. package/dist/indexer/path-resolver.js +92 -0
  117. package/dist/indexer/project-context.js +192 -0
  118. package/dist/indexer/ranking-contributors.js +331 -0
  119. package/dist/indexer/ranking.js +81 -0
  120. package/dist/indexer/search-fields.js +5 -9
  121. package/dist/indexer/search-hit-enrichers.js +111 -0
  122. package/dist/indexer/search-source.js +44 -10
  123. package/dist/indexer/semantic-status.js +5 -16
  124. package/dist/indexer/staleness-detect.js +447 -0
  125. package/dist/indexer/usage-events.js +12 -9
  126. package/dist/indexer/walker.js +28 -0
  127. package/dist/integrations/agent/builders.js +135 -0
  128. package/dist/integrations/agent/config.js +122 -230
  129. package/dist/integrations/agent/detect.js +3 -0
  130. package/dist/integrations/agent/index.js +7 -13
  131. package/dist/integrations/agent/model-aliases.js +55 -0
  132. package/dist/integrations/agent/profiles.js +70 -5
  133. package/dist/integrations/agent/prompts.js +150 -74
  134. package/dist/integrations/agent/runner.js +151 -0
  135. package/dist/integrations/agent/sdk-runner.js +126 -0
  136. package/dist/integrations/agent/spawn.js +118 -23
  137. package/dist/integrations/github.js +3 -0
  138. package/dist/integrations/lockfile.js +32 -69
  139. package/dist/integrations/session-logs/index.js +68 -0
  140. package/dist/integrations/session-logs/providers/claude-code.js +59 -0
  141. package/dist/integrations/session-logs/providers/opencode.js +55 -0
  142. package/dist/integrations/session-logs/types.js +4 -0
  143. package/dist/llm/call-ai.js +62 -0
  144. package/dist/llm/client.js +72 -124
  145. package/dist/llm/embedder.js +3 -19
  146. package/dist/llm/embedders/cache.js +3 -7
  147. package/dist/llm/embedders/local.js +3 -0
  148. package/dist/llm/embedders/remote.js +20 -8
  149. package/dist/llm/embedders/types.js +3 -7
  150. package/dist/llm/feature-gate.js +89 -48
  151. package/dist/llm/graph-extract.js +676 -70
  152. package/dist/llm/index-passes.js +9 -23
  153. package/dist/llm/memory-infer.js +52 -71
  154. package/dist/llm/metadata-enhance.js +42 -29
  155. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  156. package/dist/output/cli-hints-full.md +281 -0
  157. package/dist/output/cli-hints-short.md +65 -0
  158. package/dist/output/cli-hints.js +5 -318
  159. package/dist/output/context.js +3 -0
  160. package/dist/output/renderers.js +223 -256
  161. package/dist/output/shapes.js +150 -105
  162. package/dist/output/text.js +318 -30
  163. package/dist/registry/build-index.js +3 -0
  164. package/dist/registry/create-provider-registry.js +3 -0
  165. package/dist/registry/factory.js +3 -0
  166. package/dist/registry/origin-resolve.js +3 -0
  167. package/dist/registry/providers/index.js +3 -0
  168. package/dist/registry/providers/skills-sh.js +70 -49
  169. package/dist/registry/providers/static-index.js +53 -48
  170. package/dist/registry/providers/types.js +3 -24
  171. package/dist/registry/resolve.js +11 -16
  172. package/dist/registry/types.js +3 -0
  173. package/dist/scripts/migrate-storage.js +17307 -0
  174. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -0
  175. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  176. package/dist/setup/detect.js +3 -0
  177. package/dist/setup/ripgrep-install.js +3 -0
  178. package/dist/setup/ripgrep-resolve.js +3 -0
  179. package/dist/setup/setup.js +775 -37
  180. package/dist/setup/steps.js +3 -15
  181. package/dist/sources/include.js +3 -0
  182. package/dist/sources/provider-factory.js +5 -12
  183. package/dist/sources/provider.js +3 -20
  184. package/dist/sources/providers/filesystem.js +19 -23
  185. package/dist/sources/providers/git.js +7 -5
  186. package/dist/sources/providers/index.js +3 -0
  187. package/dist/sources/providers/install-types.js +3 -13
  188. package/dist/sources/providers/npm.js +3 -4
  189. package/dist/sources/providers/provider-utils.js +3 -0
  190. package/dist/sources/providers/sync-from-ref.js +3 -11
  191. package/dist/sources/providers/tar-utils.js +3 -0
  192. package/dist/sources/providers/website.js +18 -22
  193. package/dist/sources/resolve.js +3 -0
  194. package/dist/sources/types.js +3 -0
  195. package/dist/sources/website-ingest.js +7 -0
  196. package/dist/tasks/backends/cron.js +203 -0
  197. package/dist/tasks/backends/exec-utils.js +28 -0
  198. package/dist/tasks/backends/index.js +24 -0
  199. package/dist/tasks/backends/launchd-template.xml +19 -0
  200. package/dist/tasks/backends/launchd.js +187 -0
  201. package/dist/tasks/backends/schtasks-template.xml +29 -0
  202. package/dist/tasks/backends/schtasks.js +215 -0
  203. package/dist/tasks/parser.js +211 -0
  204. package/dist/tasks/resolveAkmBin.js +87 -0
  205. package/dist/tasks/runner.js +458 -0
  206. package/dist/tasks/schedule.js +211 -0
  207. package/dist/tasks/schema.js +15 -0
  208. package/dist/tasks/validator.js +62 -0
  209. package/dist/version.js +3 -0
  210. package/dist/wiki/index-template.md +12 -0
  211. package/dist/wiki/ingest-workflow-template.md +54 -0
  212. package/dist/wiki/log-template.md +8 -0
  213. package/dist/wiki/schema-template.md +61 -0
  214. package/dist/wiki/wiki-templates.js +15 -0
  215. package/dist/wiki/wiki.js +13 -61
  216. package/dist/workflows/authoring.js +8 -25
  217. package/dist/workflows/cli.js +3 -0
  218. package/dist/workflows/db.js +140 -10
  219. package/dist/workflows/document-cache.js +3 -10
  220. package/dist/workflows/parser.js +3 -0
  221. package/dist/workflows/renderer.js +11 -3
  222. package/dist/workflows/runs.js +62 -91
  223. package/dist/workflows/schema.js +3 -0
  224. package/dist/workflows/scope-key.js +3 -0
  225. package/dist/workflows/validator.js +4 -8
  226. package/dist/workflows/workflow-template.md +24 -0
  227. package/docs/README.md +9 -2
  228. package/docs/data-and-telemetry.md +225 -0
  229. package/docs/migration/release-notes/0.7.0.md +1 -1
  230. package/docs/migration/release-notes/0.7.5.md +2 -2
  231. package/docs/migration/release-notes/0.8.0.md +48 -0
  232. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  233. package/package.json +20 -8
  234. package/.github/LICENSE +0 -374
  235. package/dist/commands/install-audit.js +0 -381
  236. package/dist/templates/wiki-templates.js +0 -100
@@ -1,43 +1,19 @@
1
- /**
2
- * Memory inference pass for `akm index` (#201).
3
- *
4
- * Detects memories pending inference, asks the configured LLM to compress each
5
- * into one higher-signal derived memory, and writes the result back as a new
6
- * memory file with frontmatter `inferred: true` + a `source:` backref to the
7
- * parent memory.
8
- *
9
- * Pending predicate (see {@link isPendingMemory}):
10
- * - File lives under `<stashRoot>/memories/` and ends in `.md`.
11
- * - Frontmatter does NOT have `inferenceProcessed: true` (parent already split).
12
- * - Frontmatter does NOT have `inferred: true` (this is itself a child fact).
13
- *
14
- * Idempotency: after a successful split the parent's frontmatter is rewritten
15
- * with `inferenceProcessed: true`. A subsequent `akm index` therefore skips
16
- * the parent without re-running the LLM.
17
- *
18
- * Disabling — two orthogonal gates per v1 spec §14:
19
- * 1. `llm.features.memory_inference = false` blocks the pass at the
20
- * locked feature-flag layer (no network call may ever issue).
21
- * 2. `index.memory.llm = false` (or no `akm.llm` block at all) opts the
22
- * pass out at the per-pass layer (#208).
23
- * A pass runs iff both layers allow it. Existing inferred children are
24
- * NEVER deleted — the user keeps what was already produced.
25
- *
26
- * Locked v1 contract:
27
- * - LLM access is exclusively via `resolveIndexPassLLM("memory", config)`.
28
- * - All child memory writes go through `writeAssetToSource` in
29
- * `src/core/write-source.ts`. The parent's frontmatter rewrite is an
30
- * explicit narrow exception — see {@link markParentProcessed}.
31
- */
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/.
32
4
  import fs from "node:fs";
33
5
  import path from "node:path";
34
- import { stringify as yamlStringify } from "yaml";
35
6
  import { parseAssetRef } from "../core/asset-ref";
7
+ import { assembleAsset } from "../core/asset-serialize";
8
+ import { concurrentMap } from "../core/concurrent";
36
9
  import { parseFrontmatter, parseFrontmatterBlock } from "../core/frontmatter";
37
10
  import { warn } from "../core/warn";
38
11
  import { writeAssetToSource } from "../core/write-source";
12
+ import { isProcessEnabled } from "../llm/feature-gate";
39
13
  import { resolveIndexPassLLM } from "../llm/index-passes";
40
- import { compressMemoryToDerivedMemory } from "../llm/memory-infer";
14
+ import * as memoryInfer from "../llm/memory-infer";
15
+ import { withLlmCache } from "./llm-cache";
16
+ import { walkMarkdownFiles } from "./walker";
41
17
  /**
42
18
  * Frontmatter keys this pass cares about. Constants so a future rename only
43
19
  * needs to touch one site.
@@ -45,14 +21,15 @@ import { compressMemoryToDerivedMemory } from "../llm/memory-infer";
45
21
  const FM_INFERRED = "inferred";
46
22
  const FM_INFERENCE_PROCESSED = "inferenceProcessed";
47
23
  const FM_SOURCE = "source";
24
+ const FM_CAPTURE_MODE = "captureMode";
48
25
  /**
49
26
  * Top-level entry point. Returns a no-op result when the pass is disabled.
50
27
  *
51
- * Two orthogonal gates per v1 spec §14:
28
+ * Two orthogonal gates:
52
29
  *
53
- * 1. **Feature gate** — `llm.features.memory_inference` (defaults to
54
- * `true`). When `false`, no network call may issue regardless of
55
- * per-pass settings. This is the locked spec-§14 gate.
30
+ * 1. **Feature gate** — `profiles.improve.default.processes.memoryInference.enabled`
31
+ * (defaults to `true`). When `false`, no network call may issue regardless
32
+ * of per-pass settings.
56
33
  * 2. **Per-pass gate** — `resolveIndexPassLLM("memory", config)` (which
57
34
  * reads `index.memory.llm`). When `false`, the indexer simply skips
58
35
  * this pass for the current run.
@@ -60,16 +37,17 @@ const FM_SOURCE = "source";
60
37
  * Both must allow the call for the pass to run. Either set to `false`
61
38
  * short-circuits to a no-op result.
62
39
  */
63
- export async function runMemoryInferencePass(config, sources, signal) {
40
+ export async function runMemoryInferencePass(config, sources, signal, db, reEnrich, onProgress, options = {}) {
64
41
  const result = {
65
42
  considered: 0,
66
43
  splitParents: 0,
67
44
  writtenFacts: 0,
68
45
  skippedNoFacts: 0,
69
46
  };
70
- // Gate 1 — locked feature flag (§14). Defaults to enabled; only an
71
- // explicit `false` disables the pass entirely.
72
- if (config.llm?.features?.memory_inference === false)
47
+ // Gate 1 — feature gate via isProcessEnabled, which reads the 0.8.0 path
48
+ // (profiles.improve.default.processes.memoryInference.enabled). Defaults to
49
+ // enabled when the key is absent.
50
+ if (!isProcessEnabled("index", "memory_inference", config))
73
51
  return result;
74
52
  // Gate 2 — per-pass opt-out (#208). Returns the resolved llm config or
75
53
  // `undefined` when the pass should not run.
@@ -82,26 +60,75 @@ export async function runMemoryInferencePass(config, sources, signal) {
82
60
  const primary = sources[0];
83
61
  if (!primary)
84
62
  return result;
85
- const pending = collectPendingMemories(primary.path);
63
+ const pending = collectPendingMemories(primary.path).filter((record) => !options.candidateRefs || options.candidateRefs.has(record.ref));
86
64
  result.considered = pending.length;
87
65
  if (pending.length === 0)
88
66
  return result;
89
- for (const record of pending) {
67
+ let processed = 0;
68
+ const total = pending.length;
69
+ onProgress?.({ processed, total, writtenFacts: 0, skippedNoFacts: 0 });
70
+ const perRecordResults = await concurrentMap(pending, async (record) => {
90
71
  if (signal?.aborted)
91
- return result;
92
- const derived = await compressMemoryToDerivedMemory(llmConfig, record.body, signal);
72
+ return undefined;
73
+ // Incremental cache: skip LLM call when body hash is unchanged and
74
+ // --re-enrich was not requested. The cache ref is the absolute file path.
75
+ const validate = (raw) => {
76
+ if (!raw || typeof raw !== "object")
77
+ return undefined;
78
+ const parsed = raw;
79
+ const title = typeof parsed.title === "string" ? parsed.title : "";
80
+ const description = typeof parsed.description === "string" ? parsed.description : "";
81
+ const content = typeof parsed.content === "string" ? parsed.content : "";
82
+ const tags = Array.isArray(parsed.tags) ? parsed.tags.filter((t) => typeof t === "string") : [];
83
+ const searchHints = Array.isArray(parsed.searchHints)
84
+ ? parsed.searchHints.filter((h) => typeof h === "string")
85
+ : [];
86
+ if (title && description && content && tags.length > 0 && searchHints.length > 0) {
87
+ return { title, description, tags, searchHints, content };
88
+ }
89
+ return undefined;
90
+ };
91
+ const derived = db
92
+ ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
93
+ warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
94
+ }), validate)
95
+ : await memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
96
+ warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
97
+ });
93
98
  if (!derived) {
94
- result.skippedNoFacts += 1;
95
- // Intentionally NOT marked processed — a transient LLM failure should
96
- // be retried on the next index run.
97
- continue;
99
+ return { skipped: true };
98
100
  }
99
101
  const written = await writeDerivedMemory(record, derived);
100
102
  if (written > 0) {
101
103
  markParentProcessed(record);
104
+ return { skipped: false, splitParent: true, written };
105
+ }
106
+ return { skipped: false, splitParent: false, written: 0 };
107
+ },
108
+ // Default concurrency of 4 for cloud APIs. Set `llm.concurrency: 1`
109
+ // in config.json for local model servers (LM Studio, Ollama).
110
+ llmConfig.concurrency ?? 1);
111
+ for (let i = 0; i < perRecordResults.length; i++) {
112
+ const res = perRecordResults[i];
113
+ if (!res)
114
+ continue;
115
+ if (res.skipped) {
116
+ result.skippedNoFacts += 1;
117
+ // Intentionally NOT marked processed — a transient LLM failure should
118
+ // be retried on the next index run.
119
+ }
120
+ else if (res.splitParent) {
102
121
  result.splitParents += 1;
103
- result.writtenFacts += written;
122
+ result.writtenFacts += res.written;
104
123
  }
124
+ processed++;
125
+ onProgress?.({
126
+ processed,
127
+ total,
128
+ writtenFacts: result.writtenFacts,
129
+ skippedNoFacts: result.skippedNoFacts,
130
+ currentRef: pending[i]?.ref,
131
+ });
105
132
  }
106
133
  return result;
107
134
  }
@@ -125,7 +152,7 @@ export function collectPendingMemories(stashRoot) {
125
152
  continue;
126
153
  }
127
154
  const parsed = parseFrontmatter(raw);
128
- if (!isPendingMemory(parsed.data))
155
+ if (!isPendingMemory(parsed.data, filePath))
129
156
  continue;
130
157
  const relName = toMemoryName(memoriesDir, filePath);
131
158
  if (!relName)
@@ -145,34 +172,33 @@ export function collectPendingMemories(stashRoot) {
145
172
  * Predicate: true when the parsed frontmatter indicates the memory has not
146
173
  * yet been split AND is not itself an inferred child.
147
174
  *
175
+ * Also guards against `.derived` files whose `inferred:` frontmatter key has
176
+ * been dropped by a manual edit or schema-repair rewrite. The file name suffix
177
+ * is structural and immutable; frontmatter flags are mutable. A file whose
178
+ * path contains `.derived` is always treated as a derived child regardless of
179
+ * its frontmatter state — this prevents `<name>.derived.derived.md` chains.
180
+ *
181
+ * @param frontmatter - Parsed YAML frontmatter from the memory file.
182
+ * @param filePath - Optional absolute path to the memory file. When
183
+ * supplied, the name-based guard is applied.
184
+ *
148
185
  * Exported for direct unit testing — keeping the predicate in one place
149
186
  * avoids drift between the walker, tests, and any future consumers.
150
187
  */
151
- export function isPendingMemory(frontmatter) {
188
+ export function isPendingMemory(frontmatter, filePath) {
189
+ // Name-based guard: a `.derived` suffix in the path means this file is a
190
+ // derived child regardless of what its frontmatter currently says.
191
+ if (filePath !== undefined) {
192
+ const base = path.basename(filePath, ".md");
193
+ if (base.endsWith(".derived"))
194
+ return false;
195
+ }
152
196
  if (frontmatter[FM_INFERRED] === true)
153
197
  return false;
154
198
  if (frontmatter[FM_INFERENCE_PROCESSED] === true)
155
199
  return false;
156
200
  return true;
157
201
  }
158
- function* walkMarkdownFiles(root) {
159
- let entries;
160
- try {
161
- entries = fs.readdirSync(root, { withFileTypes: true });
162
- }
163
- catch {
164
- return;
165
- }
166
- for (const entry of entries) {
167
- const full = path.join(root, entry.name);
168
- if (entry.isDirectory()) {
169
- yield* walkMarkdownFiles(full);
170
- }
171
- else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
172
- yield full;
173
- }
174
- }
175
- }
176
202
  function toMemoryName(memoriesDir, filePath) {
177
203
  const rel = path.relative(memoriesDir, filePath);
178
204
  if (!rel || rel.startsWith(".."))
@@ -214,6 +240,7 @@ async function writeDerivedMemory(parent, derived) {
214
240
  function renderDerivedMemory(parent, derived) {
215
241
  const fm = {
216
242
  [FM_INFERRED]: true,
243
+ [FM_CAPTURE_MODE]: "background",
217
244
  [FM_SOURCE]: parent.ref,
218
245
  description: derived.description,
219
246
  tags: derived.tags,
@@ -221,8 +248,7 @@ function renderDerivedMemory(parent, derived) {
221
248
  title: derived.title,
222
249
  derivedFrom: parent.name,
223
250
  };
224
- const yaml = yamlStringify(fm).trimEnd();
225
- return `---\n${yaml}\n---\n\n# ${derived.title.trim()}\n\n${derived.content.trim()}\n`;
251
+ return assembleAsset(fm, `# ${derived.title.trim()}\n\n${derived.content.trim()}\n`);
226
252
  }
227
253
  function markParentProcessed(parent) {
228
254
  // Frontmatter-only rewrite of an existing asset: not a new asset write,
@@ -239,10 +265,9 @@ function markParentProcessed(parent) {
239
265
  return;
240
266
  }
241
267
  const updatedFm = { ...parent.data, [FM_INFERENCE_PROCESSED]: true };
242
- const yaml = yamlStringify(updatedFm).trimEnd();
243
268
  const block = parseFrontmatterBlock(raw);
244
269
  const body = block?.content ?? raw;
245
- const next = `---\n${yaml}\n---\n${body.startsWith("\n") ? "" : "\n"}${body}`;
270
+ const next = assembleAsset(updatedFm, body);
246
271
  try {
247
272
  fs.writeFileSync(parent.filePath, next, "utf8");
248
273
  }
@@ -0,0 +1,29 @@
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
+ const contributors = [];
5
+ let builtinsPromise;
6
+ async function ensureBuiltinMetadataContributorsRegistered() {
7
+ if (!builtinsPromise) {
8
+ builtinsPromise = (async () => {
9
+ await import("../output/renderers.js");
10
+ await import("../workflows/renderer.js");
11
+ })();
12
+ }
13
+ return builtinsPromise;
14
+ }
15
+ export function registerMetadataContributor(contributor) {
16
+ contributors.push(contributor);
17
+ }
18
+ export async function getMetadataContributors() {
19
+ await ensureBuiltinMetadataContributorsRegistered();
20
+ return [...contributors];
21
+ }
22
+ export async function applyMetadataContributors(entry, ctx) {
23
+ const activeContributors = await getMetadataContributors();
24
+ for (const contributor of activeContributors) {
25
+ if (!contributor.appliesTo(ctx))
26
+ continue;
27
+ contributor.contribute(entry, ctx);
28
+ }
29
+ }