akm-cli 0.7.5 → 0.8.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 (152) hide show
  1. package/.github/CHANGELOG.md +1 -1
  2. package/dist/cli/parse-args.js +43 -0
  3. package/dist/cli.js +853 -479
  4. package/dist/commands/agent-dispatch.js +102 -0
  5. package/dist/commands/agent-support.js +62 -0
  6. package/dist/commands/config-cli.js +68 -84
  7. package/dist/commands/consolidate.js +823 -0
  8. package/dist/commands/distill-promotion-policy.js +658 -0
  9. package/dist/commands/distill.js +244 -52
  10. package/dist/commands/eval-cases.js +40 -0
  11. package/dist/commands/events.js +2 -23
  12. package/dist/commands/graph.js +222 -0
  13. package/dist/commands/health.js +376 -0
  14. package/dist/commands/help/help-accept.md +9 -0
  15. package/dist/commands/help/help-improve.md +53 -0
  16. package/dist/commands/help/help-proposals.md +15 -0
  17. package/dist/commands/help/help-propose.md +17 -0
  18. package/dist/commands/help/help-reject.md +8 -0
  19. package/dist/commands/history.js +3 -30
  20. package/dist/commands/improve.js +1170 -0
  21. package/dist/commands/info.js +2 -2
  22. package/dist/commands/init.js +2 -2
  23. package/dist/commands/install-audit.js +5 -1
  24. package/dist/commands/installed-stashes.js +118 -138
  25. package/dist/commands/knowledge.js +133 -0
  26. package/dist/commands/lint/agent-linter.js +46 -0
  27. package/dist/commands/lint/base-linter.js +285 -0
  28. package/dist/commands/lint/command-linter.js +46 -0
  29. package/dist/commands/lint/default-linter.js +13 -0
  30. package/dist/commands/lint/index.js +107 -0
  31. package/dist/commands/lint/knowledge-linter.js +13 -0
  32. package/dist/commands/lint/memory-linter.js +58 -0
  33. package/dist/commands/lint/registry.js +33 -0
  34. package/dist/commands/lint/skill-linter.js +42 -0
  35. package/dist/commands/lint/task-linter.js +47 -0
  36. package/dist/commands/lint/types.js +1 -0
  37. package/dist/commands/lint/workflow-linter.js +53 -0
  38. package/dist/commands/lint.js +1 -0
  39. package/dist/commands/proposal.js +8 -7
  40. package/dist/commands/propose.js +78 -28
  41. package/dist/commands/reflect.js +143 -35
  42. package/dist/commands/registry-search.js +2 -2
  43. package/dist/commands/remember.js +54 -0
  44. package/dist/commands/schema-repair.js +130 -0
  45. package/dist/commands/search.js +21 -5
  46. package/dist/commands/show.js +121 -17
  47. package/dist/commands/source-add.js +10 -10
  48. package/dist/commands/source-manage.js +11 -19
  49. package/dist/commands/tasks.js +385 -0
  50. package/dist/commands/url-checker.js +39 -0
  51. package/dist/commands/vault.js +8 -26
  52. package/dist/core/action-contributors.js +25 -0
  53. package/dist/core/asset-ref.js +4 -0
  54. package/dist/core/asset-registry.js +4 -16
  55. package/dist/core/asset-spec.js +10 -0
  56. package/dist/core/common.js +94 -0
  57. package/dist/core/concurrent.js +22 -0
  58. package/dist/core/config.js +222 -128
  59. package/dist/core/events.js +73 -126
  60. package/dist/core/frontmatter.js +3 -1
  61. package/dist/core/markdown.js +17 -0
  62. package/dist/core/memory-improve.js +678 -0
  63. package/dist/core/parse.js +155 -0
  64. package/dist/core/paths.js +101 -3
  65. package/dist/core/proposal-validators.js +61 -0
  66. package/dist/core/proposals.js +49 -38
  67. package/dist/core/state-db.js +775 -0
  68. package/dist/core/time.js +51 -0
  69. package/dist/core/warn.js +59 -1
  70. package/dist/indexer/db-search.js +52 -238
  71. package/dist/indexer/db.js +378 -1
  72. package/dist/indexer/ensure-index.js +61 -0
  73. package/dist/indexer/graph-boost.js +247 -94
  74. package/dist/indexer/graph-db.js +201 -0
  75. package/dist/indexer/graph-dedup.js +99 -0
  76. package/dist/indexer/graph-extraction.js +409 -76
  77. package/dist/indexer/index-context.js +10 -0
  78. package/dist/indexer/indexer.js +442 -290
  79. package/dist/indexer/llm-cache.js +47 -0
  80. package/dist/indexer/match-contributors.js +141 -0
  81. package/dist/indexer/matchers.js +24 -190
  82. package/dist/indexer/memory-inference.js +63 -29
  83. package/dist/indexer/metadata-contributors.js +26 -0
  84. package/dist/indexer/metadata.js +194 -175
  85. package/dist/indexer/path-resolver.js +89 -0
  86. package/dist/indexer/ranking-contributors.js +204 -0
  87. package/dist/indexer/ranking.js +74 -0
  88. package/dist/indexer/search-hit-enrichers.js +22 -0
  89. package/dist/indexer/search-source.js +24 -9
  90. package/dist/indexer/semantic-status.js +2 -16
  91. package/dist/indexer/walker.js +25 -0
  92. package/dist/integrations/agent/config.js +175 -3
  93. package/dist/integrations/agent/index.js +3 -1
  94. package/dist/integrations/agent/pipeline.js +39 -0
  95. package/dist/integrations/agent/profiles.js +67 -5
  96. package/dist/integrations/agent/prompts.js +77 -72
  97. package/dist/integrations/agent/runners.js +31 -0
  98. package/dist/integrations/agent/sdk-runner.js +120 -0
  99. package/dist/integrations/agent/spawn.js +71 -16
  100. package/dist/integrations/lockfile.js +10 -18
  101. package/dist/integrations/session-logs/index.js +65 -0
  102. package/dist/integrations/session-logs/providers/claude-code.js +56 -0
  103. package/dist/integrations/session-logs/providers/opencode.js +52 -0
  104. package/dist/integrations/session-logs/types.js +1 -0
  105. package/dist/llm/call-ai.js +74 -0
  106. package/dist/llm/client.js +61 -122
  107. package/dist/llm/feature-gate.js +27 -16
  108. package/dist/llm/graph-extract.js +297 -62
  109. package/dist/llm/memory-infer.js +49 -71
  110. package/dist/llm/metadata-enhance.js +39 -22
  111. package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
  112. package/dist/output/cli-hints-full.md +277 -0
  113. package/dist/output/cli-hints-short.md +65 -0
  114. package/dist/output/cli-hints.js +2 -318
  115. package/dist/output/renderers.js +190 -123
  116. package/dist/output/shapes.js +33 -0
  117. package/dist/output/text.js +239 -2
  118. package/dist/registry/providers/skills-sh.js +61 -49
  119. package/dist/registry/providers/static-index.js +44 -48
  120. package/dist/setup/setup.js +510 -11
  121. package/dist/sources/provider-factory.js +2 -1
  122. package/dist/sources/providers/git.js +2 -2
  123. package/dist/sources/website-ingest.js +4 -0
  124. package/dist/tasks/backends/cron.js +200 -0
  125. package/dist/tasks/backends/exec-utils.js +25 -0
  126. package/dist/tasks/backends/index.js +32 -0
  127. package/dist/tasks/backends/launchd-template.xml +19 -0
  128. package/dist/tasks/backends/launchd.js +184 -0
  129. package/dist/tasks/backends/schtasks-template.xml +29 -0
  130. package/dist/tasks/backends/schtasks.js +212 -0
  131. package/dist/tasks/parser.js +198 -0
  132. package/dist/tasks/resolveAkmBin.js +84 -0
  133. package/dist/tasks/runner.js +432 -0
  134. package/dist/tasks/schedule.js +208 -0
  135. package/dist/tasks/schema.js +13 -0
  136. package/dist/tasks/validator.js +59 -0
  137. package/dist/wiki/index-template.md +12 -0
  138. package/dist/wiki/ingest-workflow-template.md +54 -0
  139. package/dist/wiki/log-template.md +8 -0
  140. package/dist/wiki/schema-template.md +61 -0
  141. package/dist/wiki/wiki-templates.js +12 -0
  142. package/dist/wiki/wiki.js +10 -61
  143. package/dist/workflows/authoring.js +5 -25
  144. package/dist/workflows/renderer.js +8 -3
  145. package/dist/workflows/runs.js +59 -91
  146. package/dist/workflows/validator.js +1 -1
  147. package/dist/workflows/workflow-template.md +24 -0
  148. package/docs/README.md +3 -0
  149. package/docs/migration/release-notes/0.7.0.md +1 -1
  150. package/docs/migration/release-notes/0.8.0.md +43 -0
  151. package/package.json +3 -2
  152. package/dist/templates/wiki-templates.js +0 -100
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Generic LLM-result cache wrapper shared across indexer passes.
3
+ *
4
+ * Each pass that calls an LLM and wants to skip re-processing unchanged
5
+ * content can delegate the cache check/write to `withLlmCache` instead of
6
+ * duplicating the hash-compute → lookup → write pattern inline.
7
+ */
8
+ import { computeBodyHash, getLlmCacheEntry, upsertLlmCacheEntry } from "./db";
9
+ /**
10
+ * Generic LLM cache wrapper. Returns cached result if body unchanged,
11
+ * otherwise calls llmFn(), caches the result, and returns it.
12
+ * Returns undefined if llmFn() returns undefined or throws.
13
+ *
14
+ * @param db - SQLite database holding the LLM result cache.
15
+ * @param cacheKey - Stable identifier for this asset (typically its absolute path).
16
+ * @param body - The content being processed; its hash determines cache validity.
17
+ * @param reEnrich - When true the cache is bypassed and llmFn() is always called.
18
+ * @param llmFn - Async function that performs the actual LLM call.
19
+ * @param validate - Converts the raw parsed JSON back into the pass-specific type;
20
+ * returns undefined when the cached data is unusable.
21
+ */
22
+ export async function withLlmCache(db, cacheKey, body, reEnrich, llmFn, validate) {
23
+ const bodyHash = computeBodyHash(body);
24
+ if (!reEnrich) {
25
+ try {
26
+ const cached = getLlmCacheEntry(db, cacheKey, bodyHash);
27
+ if (cached) {
28
+ const result = validate(JSON.parse(cached.resultJson));
29
+ if (result !== undefined)
30
+ return result;
31
+ }
32
+ }
33
+ catch {
34
+ // Cache corrupt — fall through
35
+ }
36
+ }
37
+ const result = await llmFn();
38
+ if (result !== undefined) {
39
+ try {
40
+ upsertLlmCacheEntry(db, cacheKey, bodyHash, JSON.stringify(result));
41
+ }
42
+ catch {
43
+ // Cache write failure is non-fatal
44
+ }
45
+ }
46
+ return result;
47
+ }
@@ -0,0 +1,141 @@
1
+ import { SCRIPT_EXTENSIONS } from "../core/asset-spec";
2
+ import { looksLikeWorkflow } from "../workflows/parser";
3
+ const DIR_TYPE_MAP = [
4
+ {
5
+ dir: "scripts",
6
+ type: "script",
7
+ test: (ext) => SCRIPT_EXTENSIONS.has(ext),
8
+ },
9
+ {
10
+ dir: "commands",
11
+ type: "command",
12
+ test: (ext) => ext === ".md",
13
+ },
14
+ {
15
+ dir: "agents",
16
+ type: "agent",
17
+ test: (ext) => ext === ".md",
18
+ },
19
+ {
20
+ dir: "knowledge",
21
+ type: "knowledge",
22
+ test: (ext) => ext === ".md",
23
+ },
24
+ {
25
+ dir: "workflows",
26
+ type: "workflow",
27
+ test: (ext) => ext === ".md",
28
+ },
29
+ {
30
+ dir: "memories",
31
+ type: "memory",
32
+ test: (ext) => ext === ".md",
33
+ },
34
+ {
35
+ dir: "lessons",
36
+ type: "lesson",
37
+ test: (ext) => ext === ".md",
38
+ },
39
+ {
40
+ dir: "vaults",
41
+ type: "vault",
42
+ test: (_, fileName) => fileName === ".env" || fileName.endsWith(".env"),
43
+ },
44
+ {
45
+ dir: "tasks",
46
+ type: "task",
47
+ test: (ext) => ext === ".md",
48
+ },
49
+ ];
50
+ function matchDirectoryHint(dirName, ctx, specificity) {
51
+ if (dirName === "skills" && ctx.fileName === "SKILL.md") {
52
+ return { type: "skill", specificity };
53
+ }
54
+ for (const rule of DIR_TYPE_MAP) {
55
+ if (rule.dir === dirName && rule.test(ctx.ext, ctx.fileName)) {
56
+ return { type: rule.type, specificity };
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ const COMMAND_PLACEHOLDER_RE = /\$ARGUMENTS|\$[123]\b/;
62
+ export const extensionContributor = {
63
+ name: "extension",
64
+ classify(ctx) {
65
+ if (ctx.fileName === "SKILL.md" && !ctx.ancestorDirs.includes("wikis")) {
66
+ return { type: "skill", specificity: 25 };
67
+ }
68
+ if (SCRIPT_EXTENSIONS.has(ctx.ext)) {
69
+ return { type: "script", specificity: 3 };
70
+ }
71
+ return null;
72
+ },
73
+ };
74
+ export const directoryContributor = {
75
+ name: "directory",
76
+ classify(ctx) {
77
+ for (const dir of ctx.ancestorDirs) {
78
+ const result = matchDirectoryHint(dir, ctx, 10);
79
+ if (result)
80
+ return result;
81
+ }
82
+ return null;
83
+ },
84
+ };
85
+ export const parentDirHintContributor = {
86
+ name: "parent-dir-hint",
87
+ classify(ctx) {
88
+ const { parentDir, ext, fileName } = ctx;
89
+ if (parentDir === "skills" && (fileName === "SKILL.md" || ext === ".md")) {
90
+ return { type: "skill", specificity: 15 };
91
+ }
92
+ return matchDirectoryHint(parentDir, ctx, 15);
93
+ },
94
+ };
95
+ export const smartMdContributor = {
96
+ name: "smart-md",
97
+ classify(ctx) {
98
+ if (ctx.ext !== ".md")
99
+ return null;
100
+ const body = ctx.content();
101
+ if (looksLikeWorkflow(body)) {
102
+ return { type: "workflow", specificity: 19 };
103
+ }
104
+ const fm = ctx.frontmatter();
105
+ if (fm) {
106
+ if ("toolPolicy" in fm || "tools" in fm) {
107
+ return { type: "agent", specificity: 20 };
108
+ }
109
+ if ("agent" in fm) {
110
+ return { type: "command", specificity: 18 };
111
+ }
112
+ }
113
+ if (COMMAND_PLACEHOLDER_RE.test(body)) {
114
+ return { type: "command", specificity: 18 };
115
+ }
116
+ if (fm && "model" in fm) {
117
+ return { type: "agent", specificity: 8 };
118
+ }
119
+ return { type: "knowledge", specificity: 5 };
120
+ },
121
+ };
122
+ export const wikiContributor = {
123
+ name: "wiki",
124
+ classify(ctx) {
125
+ if (ctx.ext !== ".md")
126
+ return null;
127
+ const idx = ctx.ancestorDirs.indexOf("wikis");
128
+ if (idx < 0)
129
+ return null;
130
+ if (idx + 1 >= ctx.ancestorDirs.length)
131
+ return null;
132
+ return { type: "wiki", specificity: 20 };
133
+ },
134
+ };
135
+ export const builtinMatchContributors = [
136
+ extensionContributor,
137
+ directoryContributor,
138
+ parentDirHintContributor,
139
+ smartMdContributor,
140
+ wikiContributor,
141
+ ];
@@ -1,204 +1,42 @@
1
1
  /**
2
2
  * Built-in asset matchers for the akm file classification system.
3
3
  *
4
- * Five matchers are registered at module load time, each at a different
5
- * specificity level. Extension and content determine type; directories are
6
- * optional specificity boosts, not requirements.
7
- *
8
- * - `extensionMatcher` (3) -- classifies any file by extension alone.
9
- * Ensures every known file type is discoverable regardless of directory.
10
- * - `directoryMatcher` (10) -- boosts specificity when an ancestor
11
- * directory matches a known type name (e.g. `scripts/`, `agents/`).
12
- * - `parentDirHintMatcher` (15) -- boosts specificity based on the
13
- * immediate parent directory name.
14
- * - `smartMdMatcher` (20 / 18 / 8 / 5) -- inspects markdown frontmatter
15
- * and body content for agent/command signals; falls back to "knowledge"
16
- * at specificity 5 when no signals are found. Command signals (`agent`
17
- * frontmatter, `$ARGUMENTS`/`$1`-`$3` placeholders) return 18.
18
- * - `wikiMatcher` (20) -- classifies any `.md` under `wikis/<name>/…` as
19
- * `wiki`. Registered last so the later-wins tiebreaker beats agent at 20.
4
+ * Classification facts now live in `match-contributors.ts`. This module keeps
5
+ * the existing matcher API and registration order intact by adapting those
6
+ * facts back into `MatchResult` values.
20
7
  */
21
- import { SCRIPT_EXTENSIONS } from "../core/asset-spec";
22
- import { looksLikeWorkflow } from "../workflows/parser";
8
+ import { defaultRendererRegistry } from "../core/asset-registry";
23
9
  import { registerMatcher } from "./file-context";
24
- // ── extensionMatcher (specificity: 3) ────────────────────────────────────────
25
- /**
26
- * Base-level matcher that classifies files purely by extension.
27
- *
28
- * This is the foundation of the classification system: every file with a
29
- * known extension gets a type, regardless of what directory it lives in.
30
- * Higher-specificity matchers (directory, content) can override this.
31
- *
32
- * .md files are NOT handled here -- smartMdMatcher provides richer
33
- * classification for markdown via frontmatter inspection.
34
- */
10
+ import { directoryContributor, extensionContributor, parentDirHintContributor, smartMdContributor, wikiContributor, } from "./match-contributors";
11
+ function toMatchResult(ctx, contributor) {
12
+ const fact = contributor.classify(ctx);
13
+ if (!fact)
14
+ return null;
15
+ const renderer = defaultRendererRegistry.rendererNameFor(fact.type);
16
+ if (!renderer)
17
+ return null;
18
+ return {
19
+ type: fact.type,
20
+ specificity: fact.specificity,
21
+ renderer,
22
+ ...(fact.meta ? { meta: fact.meta } : {}),
23
+ };
24
+ }
35
25
  export function extensionMatcher(ctx) {
36
- // SKILL.md is a skill regardless of location — high specificity beats
37
- // smartMdMatcher's knowledge fallback and all directory-based matchers.
38
- // Exception: files under wikis/<name>/… are always wiki pages; the wiki
39
- // directory is an authoritative signal that outranks the filename.
40
- if (ctx.fileName === "SKILL.md" && !ctx.ancestorDirs.includes("wikis")) {
41
- return { type: "skill", specificity: 25, renderer: "skill-md" };
42
- }
43
- // Known script extensions (excluding .md, handled by smartMdMatcher)
44
- if (SCRIPT_EXTENSIONS.has(ctx.ext)) {
45
- return { type: "script", specificity: 3, renderer: "script-source" };
46
- }
47
- return null;
26
+ return toMatchResult(ctx, extensionContributor);
48
27
  }
49
- // ── directoryMatcher (specificity: 10) ──────────────────────────────────────
50
- /**
51
- * Directory-based matcher that boosts specificity when an ancestor
52
- * directory segment from the stash root matches a known type name.
53
- *
54
- * The first matching type-like ancestor wins. This preserves intuitive
55
- * behavior for nested stash layouts such as `agent-stash/agents/blog/foo.md`
56
- * while still honoring earlier type roots like `commands/agents/foo.md`.
57
- */
58
28
  export function directoryMatcher(ctx) {
59
- const ext = ctx.ext;
60
- for (const dir of ctx.ancestorDirs) {
61
- if (dir === "scripts" && SCRIPT_EXTENSIONS.has(ext)) {
62
- return { type: "script", specificity: 10, renderer: "script-source" };
63
- }
64
- if (dir === "skills" && ctx.fileName === "SKILL.md") {
65
- return { type: "skill", specificity: 10, renderer: "skill-md" };
66
- }
67
- if (dir === "commands" && ext === ".md") {
68
- return { type: "command", specificity: 10, renderer: "command-md" };
69
- }
70
- if (dir === "agents" && ext === ".md") {
71
- return { type: "agent", specificity: 10, renderer: "agent-md" };
72
- }
73
- if (dir === "knowledge" && ext === ".md") {
74
- return { type: "knowledge", specificity: 10, renderer: "knowledge-md" };
75
- }
76
- if (dir === "workflows" && ext === ".md") {
77
- return { type: "workflow", specificity: 10, renderer: "workflow-md" };
78
- }
79
- if (dir === "memories" && ext === ".md") {
80
- return { type: "memory", specificity: 10, renderer: "memory-md" };
81
- }
82
- if (dir === "vaults" && (ctx.fileName === ".env" || ctx.fileName.endsWith(".env"))) {
83
- return { type: "vault", specificity: 10, renderer: "vault-env" };
84
- }
85
- }
86
- return null;
29
+ return toMatchResult(ctx, directoryContributor);
87
30
  }
88
- // ── parentDirHintMatcher (specificity: 15) ──────────────────────────────────
89
- /**
90
- * Uses the immediate parent directory name as a hint. More specific than
91
- * the ancestor-based directory matcher because the file might be nested
92
- * several levels deep, yet its immediate parent can still carry strong
93
- * naming conventions (e.g. `my-project/agents/planning.md`).
94
- */
95
31
  export function parentDirHintMatcher(ctx) {
96
- const { parentDir, ext, fileName } = ctx;
97
- if (parentDir === "scripts" && SCRIPT_EXTENSIONS.has(ext)) {
98
- return { type: "script", specificity: 15, renderer: "script-source" };
99
- }
100
- if (parentDir === "skills" && (fileName === "SKILL.md" || ext === ".md")) {
101
- return { type: "skill", specificity: 15, renderer: "skill-md" };
102
- }
103
- if (parentDir === "agents" && ext === ".md") {
104
- return { type: "agent", specificity: 15, renderer: "agent-md" };
105
- }
106
- if (parentDir === "commands" && ext === ".md") {
107
- return { type: "command", specificity: 15, renderer: "command-md" };
108
- }
109
- if (parentDir === "knowledge" && ext === ".md") {
110
- return { type: "knowledge", specificity: 15, renderer: "knowledge-md" };
111
- }
112
- if (parentDir === "workflows" && ext === ".md") {
113
- return { type: "workflow", specificity: 15, renderer: "workflow-md" };
114
- }
115
- if (parentDir === "memories" && ext === ".md") {
116
- return { type: "memory", specificity: 15, renderer: "memory-md" };
117
- }
118
- if (parentDir === "vaults" && (fileName === ".env" || fileName.endsWith(".env"))) {
119
- return { type: "vault", specificity: 15, renderer: "vault-env" };
120
- }
121
- return null;
32
+ return toMatchResult(ctx, parentDirHintContributor);
122
33
  }
123
- // ── smartMdMatcher (specificity: 20 / 18 / 8 / 5) ──────────────────────────
124
- /** Pattern that matches OpenCode command placeholders in markdown body. */
125
- const COMMAND_PLACEHOLDER_RE = /\$ARGUMENTS|\$[123]\b/;
126
- /**
127
- * Content-based matcher for `.md` files. Inspects frontmatter keys and body
128
- * content to classify markdown as agent, command, or knowledge.
129
- *
130
- * Specificity levels:
131
- * 20 -- agent-exclusive signals (`tools`, `toolPolicy`)
132
- * 18 -- command content signals (`agent` frontmatter, `$ARGUMENTS`/`$1`-`$3`)
133
- * 8 -- weak agent signal (`model` alone)
134
- * 5 -- knowledge fallback (any unclassified `.md`)
135
- *
136
- * Command signals at 18 override directory hints (10/15) because the content
137
- * unambiguously identifies a command template. Agent-exclusive signals at 20
138
- * still win over command signals when both are present.
139
- */
140
34
  export function smartMdMatcher(ctx) {
141
- if (ctx.ext !== ".md")
142
- return null;
143
- const body = ctx.content();
144
- if (looksLikeWorkflow(body)) {
145
- return { type: "workflow", specificity: 19, renderer: "workflow-md" };
146
- }
147
- const fm = ctx.frontmatter();
148
- if (fm) {
149
- // Agent-exclusive indicators: toolPolicy or tools
150
- // These return high specificity (20) to override everything else.
151
- if ("toolPolicy" in fm || "tools" in fm) {
152
- return { type: "agent", specificity: 20, renderer: "agent-md" };
153
- }
154
- // Command signal: `agent` frontmatter key names a dispatch target.
155
- // This is an OpenCode convention specific to commands.
156
- if ("agent" in fm) {
157
- return { type: "command", specificity: 18, renderer: "command-md" };
158
- }
159
- }
160
- // Command signal: body contains $ARGUMENTS or $1/$2/$3 placeholders.
161
- // These are definitively command template patterns (OpenCode convention).
162
- if (COMMAND_PLACEHOLDER_RE.test(body)) {
163
- return { type: "command", specificity: 18, renderer: "command-md" };
164
- }
165
- if (fm) {
166
- // model alone is a weaker agent signal (specificity 8) -- it can appear
167
- // on commands too (OpenCode convention). Directory hints (10/15) win
168
- // when the file lives in commands/, but model still classifies an .md
169
- // as agent when no directory hint is present.
170
- if ("model" in fm) {
171
- return { type: "agent", specificity: 8, renderer: "agent-md" };
172
- }
173
- }
174
- // Weak fallback: any .md file is assumed to be knowledge
175
- return { type: "knowledge", specificity: 5, renderer: "knowledge-md" };
35
+ return toMatchResult(ctx, smartMdContributor);
176
36
  }
177
- // ── wikiMatcher (specificity: 20) ──────────────────────────────────────────
178
- /**
179
- * Classify any `.md` file that lives under `wikis/<name>/…` as `wiki`.
180
- *
181
- * Registered AFTER `smartMdMatcher` so the registered-later-wins tiebreaker
182
- * puts wiki ahead of agent at specificity 20. That means a wiki page with
183
- * agent-style frontmatter (e.g. `tools:`) still classifies as a wiki page,
184
- * not an agent. That's intentional — the directory is the authoritative
185
- * signal: files under `wikis/` are wiki content.
186
- *
187
- * Requires at least one path segment after `wikis/` (the wiki name) — a
188
- * stray `.md` at the bare `wikis/` root is not a wiki page.
189
- */
190
37
  export function wikiMatcher(ctx) {
191
- if (ctx.ext !== ".md")
192
- return null;
193
- const idx = ctx.ancestorDirs.indexOf("wikis");
194
- if (idx < 0)
195
- return null;
196
- if (idx + 1 >= ctx.ancestorDirs.length)
197
- return null;
198
- return { type: "wiki", specificity: 20, renderer: "wiki-md" };
38
+ return toMatchResult(ctx, wikiContributor);
199
39
  }
200
- // ── Registration ────────────────────────────────────────────────────────────
201
- /** All built-in matchers in registration order (later wins ties). */
202
40
  const builtinMatchers = [
203
41
  extensionMatcher,
204
42
  directoryMatcher,
@@ -206,10 +44,6 @@ const builtinMatchers = [
206
44
  smartMdMatcher,
207
45
  wikiMatcher,
208
46
  ];
209
- /**
210
- * Register all built-in matchers with the file-context registry.
211
- * Called once from the CLI entry point (or ensureBuiltinsRegistered).
212
- */
213
47
  export function registerBuiltinMatchers() {
214
48
  for (const matcher of builtinMatchers) {
215
49
  registerMatcher(matcher);
@@ -33,11 +33,14 @@ import fs from "node:fs";
33
33
  import path from "node:path";
34
34
  import { stringify as yamlStringify } from "yaml";
35
35
  import { parseAssetRef } from "../core/asset-ref";
36
+ import { concurrentMap } from "../core/concurrent";
36
37
  import { parseFrontmatter, parseFrontmatterBlock } from "../core/frontmatter";
37
38
  import { warn } from "../core/warn";
38
39
  import { writeAssetToSource } from "../core/write-source";
39
40
  import { resolveIndexPassLLM } from "../llm/index-passes";
40
- import { compressMemoryToDerivedMemory } from "../llm/memory-infer";
41
+ import * as memoryInfer from "../llm/memory-infer";
42
+ import { withLlmCache } from "./llm-cache";
43
+ import { walkMarkdownFiles } from "./walker";
41
44
  /**
42
45
  * Frontmatter keys this pass cares about. Constants so a future rename only
43
46
  * needs to touch one site.
@@ -60,7 +63,7 @@ const FM_SOURCE = "source";
60
63
  * Both must allow the call for the pass to run. Either set to `false`
61
64
  * short-circuits to a no-op result.
62
65
  */
63
- export async function runMemoryInferencePass(config, sources, signal) {
66
+ export async function runMemoryInferencePass(config, sources, signal, db, reEnrich, onProgress, options = {}) {
64
67
  const result = {
65
68
  considered: 0,
66
69
  splitParents: 0,
@@ -82,26 +85,75 @@ export async function runMemoryInferencePass(config, sources, signal) {
82
85
  const primary = sources[0];
83
86
  if (!primary)
84
87
  return result;
85
- const pending = collectPendingMemories(primary.path);
88
+ const pending = collectPendingMemories(primary.path).filter((record) => !options.candidateRefs || options.candidateRefs.has(record.ref));
86
89
  result.considered = pending.length;
87
90
  if (pending.length === 0)
88
91
  return result;
89
- for (const record of pending) {
92
+ let processed = 0;
93
+ const total = pending.length;
94
+ onProgress?.({ processed, total, writtenFacts: 0, skippedNoFacts: 0 });
95
+ const perRecordResults = await concurrentMap(pending, async (record) => {
90
96
  if (signal?.aborted)
91
- return result;
92
- const derived = await compressMemoryToDerivedMemory(llmConfig, record.body, signal);
97
+ return undefined;
98
+ // Incremental cache: skip LLM call when body hash is unchanged and
99
+ // --re-enrich was not requested. The cache ref is the absolute file path.
100
+ const validate = (raw) => {
101
+ if (!raw || typeof raw !== "object")
102
+ return undefined;
103
+ const parsed = raw;
104
+ const title = typeof parsed.title === "string" ? parsed.title : "";
105
+ const description = typeof parsed.description === "string" ? parsed.description : "";
106
+ const content = typeof parsed.content === "string" ? parsed.content : "";
107
+ const tags = Array.isArray(parsed.tags) ? parsed.tags.filter((t) => typeof t === "string") : [];
108
+ const searchHints = Array.isArray(parsed.searchHints)
109
+ ? parsed.searchHints.filter((h) => typeof h === "string")
110
+ : [];
111
+ if (title && description && content && tags.length > 0 && searchHints.length > 0) {
112
+ return { title, description, tags, searchHints, content };
113
+ }
114
+ return undefined;
115
+ };
116
+ const derived = db
117
+ ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
118
+ warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
119
+ }), validate)
120
+ : await memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
121
+ warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
122
+ });
93
123
  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;
124
+ return { skipped: true };
98
125
  }
99
126
  const written = await writeDerivedMemory(record, derived);
100
127
  if (written > 0) {
101
128
  markParentProcessed(record);
129
+ return { skipped: false, splitParent: true, written };
130
+ }
131
+ return { skipped: false, splitParent: false, written: 0 };
132
+ },
133
+ // Default concurrency of 4 for cloud APIs. Set `llm.concurrency: 1`
134
+ // in config.json for local model servers (LM Studio, Ollama).
135
+ llmConfig.concurrency ?? 1);
136
+ for (let i = 0; i < perRecordResults.length; i++) {
137
+ const res = perRecordResults[i];
138
+ if (!res)
139
+ continue;
140
+ if (res.skipped) {
141
+ result.skippedNoFacts += 1;
142
+ // Intentionally NOT marked processed — a transient LLM failure should
143
+ // be retried on the next index run.
144
+ }
145
+ else if (res.splitParent) {
102
146
  result.splitParents += 1;
103
- result.writtenFacts += written;
147
+ result.writtenFacts += res.written;
104
148
  }
149
+ processed++;
150
+ onProgress?.({
151
+ processed,
152
+ total,
153
+ writtenFacts: result.writtenFacts,
154
+ skippedNoFacts: result.skippedNoFacts,
155
+ currentRef: pending[i]?.ref,
156
+ });
105
157
  }
106
158
  return result;
107
159
  }
@@ -155,24 +207,6 @@ export function isPendingMemory(frontmatter) {
155
207
  return false;
156
208
  return true;
157
209
  }
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
210
  function toMemoryName(memoriesDir, filePath) {
177
211
  const rel = path.relative(memoriesDir, filePath);
178
212
  if (!rel || rel.startsWith(".."))
@@ -0,0 +1,26 @@
1
+ const contributors = [];
2
+ let builtinsPromise;
3
+ async function ensureBuiltinMetadataContributorsRegistered() {
4
+ if (!builtinsPromise) {
5
+ builtinsPromise = (async () => {
6
+ await import("../output/renderers.js");
7
+ await import("../workflows/renderer.js");
8
+ })();
9
+ }
10
+ return builtinsPromise;
11
+ }
12
+ export function registerMetadataContributor(contributor) {
13
+ contributors.push(contributor);
14
+ }
15
+ export async function getMetadataContributors() {
16
+ await ensureBuiltinMetadataContributorsRegistered();
17
+ return [...contributors];
18
+ }
19
+ export async function applyMetadataContributors(entry, ctx) {
20
+ const activeContributors = await getMetadataContributors();
21
+ for (const contributor of activeContributors) {
22
+ if (!contributor.appliesTo(ctx))
23
+ continue;
24
+ contributor.contribute(entry, ctx);
25
+ }
26
+ }