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,3 +1,6 @@
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
  /**
2
5
  * Low-level OpenAI-compatible chat completions client and capability probing.
3
6
  *
@@ -8,6 +11,11 @@
8
11
  * `llm.ts` re-exports everything from this module for backward compatibility.
9
12
  */
10
13
  import { fetchWithTimeout } from "../core/common";
14
+ import { resolveSecret } from "../core/config";
15
+ import { escapeJsonStringControls, parseJsonResponse, stripCodeFences, stripThinkBlocks } from "../core/parse";
16
+ // Re-export shared parse utilities so existing importers of `client.ts` continue
17
+ // to resolve `parseJsonResponse` and `parseEmbeddedJsonResponse` from this module.
18
+ export { escapeJsonStringControls, parseEmbeddedJsonResponse, parseJsonResponse, stripCodeFences, stripThinkBlocks, } from "../core/parse";
11
19
  /** Maximum length of an LLM error response body included in thrown errors. */
12
20
  const ERROR_BODY_MAX_LEN = 200;
13
21
  /**
@@ -38,143 +46,83 @@ export function redactErrorBody(input) {
38
46
  }
39
47
  return out;
40
48
  }
49
+ export class LlmCallError extends Error {
50
+ code;
51
+ statusCode;
52
+ constructor(message, code, statusCode) {
53
+ super(message);
54
+ this.code = code;
55
+ this.statusCode = statusCode;
56
+ this.name = "LlmCallError";
57
+ }
58
+ }
41
59
  export async function chatCompletion(config, messages, options) {
42
60
  const timeoutMs = options?.timeoutMs ?? config.timeoutMs ?? 120_000;
43
61
  const headers = { "Content-Type": "application/json" };
44
- if (config.apiKey) {
45
- headers.Authorization = `Bearer ${config.apiKey}`;
62
+ const resolvedKey = resolveSecret(config.apiKey);
63
+ if (resolvedKey) {
64
+ headers.Authorization = `Bearer ${resolvedKey}`;
46
65
  }
47
- const response = await fetchWithTimeout(config.endpoint, {
48
- method: "POST",
49
- headers,
50
- body: JSON.stringify({
51
- model: config.model,
52
- messages,
53
- temperature: options?.temperature ?? config.temperature ?? 0.3,
54
- max_tokens: options?.maxTokens ?? config.maxTokens ?? 512,
55
- ...config.extraParams,
56
- }),
57
- }, timeoutMs, options?.signal);
58
- if (!response.ok) {
59
- const rawBody = await response.text().catch(() => "");
60
- const safeBody = redactErrorBody(rawBody);
61
- throw new Error(`LLM request failed (${response.status}) ${config.endpoint}: ${safeBody}`);
66
+ // Only include max_tokens when explicitly set. The model/API knows its own
67
+ // limits; a hardcoded default creates silent truncation failures when the
68
+ // guess is wrong. Users who need a cap can set llm.maxTokens in config.
69
+ const resolvedMaxTokens = options?.maxTokens ?? config.maxTokens;
70
+ const responseFormat = options?.responseSchema && config.supportsJsonSchema
71
+ ? { response_format: { type: "json_schema", json_schema: { schema: options.responseSchema, strict: true } } }
72
+ : {};
73
+ let response;
74
+ try {
75
+ response = await fetchWithTimeout(config.endpoint, {
76
+ method: "POST",
77
+ headers,
78
+ body: JSON.stringify({
79
+ model: config.model,
80
+ messages,
81
+ temperature: options?.temperature ?? config.temperature ?? 0.3,
82
+ ...(resolvedMaxTokens !== undefined ? { max_tokens: resolvedMaxTokens } : {}),
83
+ ...responseFormat,
84
+ ...config.extraParams,
85
+ }),
86
+ }, timeoutMs, options?.signal);
62
87
  }
63
- const json = (await response.json());
64
- return json.choices?.[0]?.message?.content?.trim() ?? "";
65
- }
66
- /** Strip leading/trailing markdown code fences from an LLM response. */
67
- export function stripJsonFences(raw) {
68
- const repaired = raw
69
- .trim()
70
- .replace(/<think>[\s\S]*?<\/think>/gi, "")
71
- .replace(/^```(?:json)?\s*\n?/i, "")
72
- .replace(/\n?```\s*$/i, "")
73
- .trim();
74
- let out = "";
75
- let inString = false;
76
- let escaped = false;
77
- for (let i = 0; i < repaired.length; i++) {
78
- const ch = repaired[i];
79
- if (escaped) {
80
- out += ch;
81
- escaped = false;
82
- continue;
88
+ catch (err) {
89
+ // fetchWithTimeout throws a plain Error with a message containing
90
+ // "timed out" for AbortController-driven timeouts, or "aborted" for
91
+ // caller-driven cancellations. Map both to typed LlmCallError.
92
+ const msg = err instanceof Error ? err.message : String(err);
93
+ if (err instanceof DOMException && err.name === "AbortError") {
94
+ throw new LlmCallError(`Request timed out after ${timeoutMs}ms`, "timeout");
83
95
  }
84
- if (ch === "\\" && inString) {
85
- out += ch;
86
- escaped = true;
87
- continue;
96
+ if (msg.includes("timed out")) {
97
+ throw new LlmCallError(`Request timed out after ${timeoutMs}ms`, "timeout");
88
98
  }
89
- if (ch === '"') {
90
- inString = !inString;
91
- out += ch;
92
- continue;
99
+ throw new LlmCallError(`Network error: ${msg}`, "network_error");
100
+ }
101
+ if (!response.ok) {
102
+ const rawBody = await response.text().catch(() => "");
103
+ const safeBody = redactErrorBody(rawBody);
104
+ const status = response.status;
105
+ if (status === 429) {
106
+ throw new LlmCallError(`LLM request rate limited (429) ${config.endpoint}: ${safeBody}`, "rate_limited", status);
93
107
  }
94
- if (inString) {
95
- if (ch === "\n") {
96
- out += "\\n";
97
- continue;
98
- }
99
- if (ch === "\r") {
100
- out += "\\r";
101
- continue;
102
- }
103
- if (ch === "\t") {
104
- out += "\\t";
105
- continue;
106
- }
108
+ if (status >= 500) {
109
+ throw new LlmCallError(`LLM provider error (${status}) ${config.endpoint}: ${safeBody}`, "provider_error", status);
107
110
  }
108
- out += ch;
109
- }
110
- return out;
111
- }
112
- /** Parse a possibly-fenced JSON response. Returns undefined if invalid. */
113
- export function parseJsonResponse(raw) {
114
- try {
115
- return JSON.parse(stripJsonFences(raw));
116
- }
117
- catch {
118
- return undefined;
111
+ throw new LlmCallError(`LLM request failed (${status}) ${config.endpoint}: ${safeBody}`, "provider_error", status);
119
112
  }
113
+ const json = (await response.json());
114
+ const content = (json.choices?.[0]?.message?.content ?? "").trim();
115
+ const reasoning = (json.choices?.[0]?.message?.reasoning_content ?? "").trim();
116
+ return content || reasoning;
120
117
  }
121
118
  /**
122
- * Best-effort recovery for providers that wrap JSON in extra prose or fenced
123
- * blocks. Extracts the first balanced top-level object/array and parses it.
119
+ * Strip `<think>` blocks, code fences, and escape control characters in JSON
120
+ * strings. Thin wrapper kept for backward compatibility with call sites that
121
+ * import `stripJsonFences` from this module. New code should prefer the
122
+ * granular helpers from `../core/parse`.
124
123
  */
125
- export function parseEmbeddedJsonResponse(raw) {
126
- const direct = parseJsonResponse(raw);
127
- if (direct !== undefined)
128
- return direct;
129
- const text = stripJsonFences(raw);
130
- let arrayFallback;
131
- for (let start = 0; start < text.length; start++) {
132
- const opener = text[start];
133
- if (opener !== "{" && opener !== "[")
134
- continue;
135
- const closer = opener === "{" ? "}" : "]";
136
- let depth = 0;
137
- let inString = false;
138
- let escaped = false;
139
- for (let i = start; i < text.length; i++) {
140
- const ch = text[i];
141
- if (inString) {
142
- if (escaped) {
143
- escaped = false;
144
- }
145
- else if (ch === "\\") {
146
- escaped = true;
147
- }
148
- else if (ch === '"') {
149
- inString = false;
150
- }
151
- continue;
152
- }
153
- if (ch === '"') {
154
- inString = true;
155
- continue;
156
- }
157
- if (ch === opener)
158
- depth += 1;
159
- if (ch === closer) {
160
- depth -= 1;
161
- if (depth === 0) {
162
- try {
163
- const parsed = JSON.parse(text.slice(start, i + 1));
164
- if (!Array.isArray(parsed)) {
165
- return parsed;
166
- }
167
- arrayFallback ??= parsed;
168
- break;
169
- }
170
- catch {
171
- break;
172
- }
173
- }
174
- }
175
- }
176
- }
177
- return arrayFallback;
124
+ export function stripJsonFences(raw) {
125
+ return escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw)));
178
126
  }
179
127
  // ── Availability check ──────────────────────────────────────────────────────
180
128
  /**
@@ -1,22 +1,6 @@
1
- /**
2
- * Backward-compatible facade for the embedder module.
3
- *
4
- * The implementation has been split into:
5
- * - `./embedders/types` — `EmbeddingVector`, `Embedder`, `EmbeddingCheckResult`
6
- * - `./embedders/local` — `LocalEmbedder`, `DEFAULT_LOCAL_MODEL`,
7
- * `isTransformersAvailable`
8
- * - `./embedders/remote` — `RemoteEmbedder`, `hasRemoteEndpoint`
9
- * - `./embedders/cache` — LRU `embedCache`, `clearEmbeddingCache`,
10
- * `embedCacheKey`
11
- *
12
- * This module wires them together: it picks the right implementation from the
13
- * (optional) embedding config, applies the cache layer, and re-exports the
14
- * existing public API so call sites (`db-search.ts`, `indexer.ts`, `db.ts`,
15
- * `setup.ts`, `semantic-status.ts`, tests) keep working unmodified.
16
- *
17
- * Tests can construct fresh `LocalEmbedder` / `RemoteEmbedder` instances
18
- * directly from their submodules to avoid module-level state pollution.
19
- */
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/.
20
4
  import { embedCacheKey, getCachedEmbedding, setCachedEmbedding } from "./embedders/cache";
21
5
  import { isTransformersAvailable, LocalEmbedder } from "./embedders/local";
22
6
  import { hasRemoteEndpoint, RemoteEmbedder } from "./embedders/remote";
@@ -1,10 +1,6 @@
1
- /**
2
- * LRU embedding cache shared by the embedder facade.
3
- *
4
- * Caches query embeddings to avoid redundant computation for repeated
5
- * queries. Uses a simple Map with LRU eviction (delete + re-insert to move
6
- * an entry to the most-recently-used end).
7
- */
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/.
8
4
  const EMBED_CACHE_MAX = 100;
9
5
  const embedCache = new Map();
10
6
  /**
@@ -1,3 +1,6 @@
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
  /**
2
5
  * Local @huggingface/transformers embedder.
3
6
  *
@@ -1,3 +1,6 @@
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
  /**
2
5
  * OpenAI-compatible remote embedder.
3
6
  *
@@ -5,6 +8,7 @@
5
8
  * vectors so the scoring pipeline's L2-to-cosine conversion is correct.
6
9
  */
7
10
  import { fetchWithTimeout, isHttpUrl } from "../../core/common";
11
+ import { resolveSecret } from "../../core/config";
8
12
  const DEFAULT_REMOTE_BATCH_SIZE = 100;
9
13
  /** Cheap token estimator: 4 chars ≈ 1 token. Used in verbose logging and error messages. */
10
14
  export function estimateTokenCount(text) {
@@ -12,14 +16,21 @@ export function estimateTokenCount(text) {
12
16
  }
13
17
  export class RemoteEmbedder {
14
18
  config;
19
+ endpoint;
20
+ model;
15
21
  constructor(config) {
16
22
  this.config = config;
23
+ if (!config.endpoint || !config.model) {
24
+ throw new Error("RemoteEmbedder requires both endpoint and model on the embedding config.");
25
+ }
26
+ this.endpoint = config.endpoint;
27
+ this.model = config.model;
17
28
  }
18
29
  async embed(text, signal) {
19
30
  const headers = this.buildHeaders();
20
31
  const body = {
21
32
  input: text,
22
- model: this.config.model,
33
+ model: this.model,
23
34
  };
24
35
  if (this.config.dimension) {
25
36
  body.dimensions = this.config.dimension;
@@ -28,7 +39,7 @@ export class RemoteEmbedder {
28
39
  if (ollamaOpts) {
29
40
  body.options = ollamaOpts;
30
41
  }
31
- const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.config.endpoint), {
42
+ const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.endpoint), {
32
43
  method: "POST",
33
44
  headers,
34
45
  body: JSON.stringify(body),
@@ -40,7 +51,7 @@ export class RemoteEmbedder {
40
51
  }
41
52
  const json = (await response.json());
42
53
  if (!json.data?.[0]?.embedding) {
43
- throw new Error(`Unexpected embedding response format: missing data[0].embedding.${embeddingEndpointPathHint(this.config.endpoint)}`);
54
+ throw new Error(`Unexpected embedding response format: missing data[0].embedding.${embeddingEndpointPathHint(this.endpoint)}`);
44
55
  }
45
56
  return l2Normalize(json.data[0].embedding);
46
57
  }
@@ -55,7 +66,7 @@ export class RemoteEmbedder {
55
66
  const batch = texts.slice(i, i + batchSize);
56
67
  const body = {
57
68
  input: batch,
58
- model: this.config.model,
69
+ model: this.model,
59
70
  };
60
71
  if (this.config.dimension) {
61
72
  body.dimensions = this.config.dimension;
@@ -63,7 +74,7 @@ export class RemoteEmbedder {
63
74
  if (ollamaOpts) {
64
75
  body.options = ollamaOpts;
65
76
  }
66
- const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.config.endpoint), {
77
+ const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.endpoint), {
67
78
  method: "POST",
68
79
  headers,
69
80
  body: JSON.stringify(body),
@@ -75,7 +86,7 @@ export class RemoteEmbedder {
75
86
  }
76
87
  const json = (await response.json());
77
88
  if (!json.data || json.data.length !== batch.length) {
78
- throw new Error(`Unexpected embedding batch response: expected ${batch.length} embeddings, got ${json.data?.length ?? 0}.${embeddingEndpointPathHint(this.config.endpoint)}`);
89
+ throw new Error(`Unexpected embedding batch response: expected ${batch.length} embeddings, got ${json.data?.length ?? 0}.${embeddingEndpointPathHint(this.endpoint)}`);
79
90
  }
80
91
  // Sort by index to guarantee correct order (OpenAI API doesn't guarantee order)
81
92
  const sorted = [...json.data].sort((a, b) => a.index - b.index);
@@ -90,8 +101,9 @@ export class RemoteEmbedder {
90
101
  }
91
102
  buildHeaders() {
92
103
  const headers = { "Content-Type": "application/json" };
93
- if (this.config.apiKey) {
94
- headers.Authorization = `Bearer ${this.config.apiKey}`;
104
+ const resolvedKey = resolveSecret(this.config.apiKey);
105
+ if (resolvedKey) {
106
+ headers.Authorization = `Bearer ${resolvedKey}`;
95
107
  }
96
108
  return headers;
97
109
  }
@@ -1,10 +1,6 @@
1
- /**
2
- * Shared embedder types.
3
- *
4
- * Pulled out of `embedder.ts` so concrete implementations (`local.ts`,
5
- * `remote.ts`) and the cache layer can depend on a small, stable types
6
- * module without dragging in the facade or a sibling implementation.
7
- */
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/.
8
4
  /**
9
5
  * Cosine similarity between two embedding vectors.
10
6
  *
@@ -1,63 +1,55 @@
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
  /**
2
- * Per-feature LLM gates (v1 spec §14).
3
- *
4
- * Every bounded in-tree LLM call site in akm is addressed by exactly one
5
- * feature key under `llm.features.*`. This module is the single seam call
6
- * sites use to ask "should I run?" and "if I run and fail, what do I return?"
7
- *
8
- * The seam is intentionally tiny:
9
- *
10
- * - `isLlmFeatureEnabled(config, feature)` — pure predicate, no side
11
- * effects, no I/O. Returns `true` only when the feature flag is the
12
- * literal boolean `true` in config. Defaults are `false` per v1
13
- * spec §14 — adding a flag to the schema is a non-event until the user
14
- * opts in.
15
- * - `tryLlmFeature(feature, config, fn, fallback, opts?)` — single-call
16
- * wrapper that runs `fn()` only when the gate is open, enforces a hard
17
- * timeout (default 30s — overridable per call), and returns `fallback`
18
- * on disablement, throw, or timeout. The wrapper is referentially
19
- * transparent for any given (gate-state, fn-result) pair: no module
20
- * state is mutated.
21
- *
22
- * Statelessness invariant (v1 spec §14.4): nothing in this module holds
23
- * state across calls. There are no caches, no module-level singletons, no
24
- * persistent connections. The architecture seam test
25
- * (`tests/architecture/llm-stateless-seam.test.ts`) does not currently
26
- * inspect this file but the same rule applies — keep all exports as pure
27
- * functions.
5
+ * For each feature key, return the effective enabled state by reading the
6
+ * new 0.8.0 config shape. Defaults match the legacy `LlmFeatureFlags` docstrings.
28
7
  */
8
+ // Defaults below mirror the legacy LlmFeatureFlags docstrings so existing
9
+ // behaviour is preserved when a config is silent on a flag.
10
+ const FEATURE_LOCATION = {
11
+ // Legacy default: false → memory_consolidation only runs when explicitly enabled
12
+ // (either via the user's improve profile or the built-in `default` profile).
13
+ memory_consolidation: (cfg) => cfg.profiles?.improve?.default?.processes?.consolidate?.enabled ?? false,
14
+ // Legacy default: false
15
+ feedback_distillation: (cfg) => cfg.profiles?.improve?.default?.processes?.feedbackDistillation?.enabled ?? false,
16
+ // Legacy default: true
17
+ memory_inference: (cfg) => cfg.profiles?.improve?.default?.processes?.memoryInference?.enabled ?? true,
18
+ // Legacy default: true
19
+ graph_extraction: (cfg) => cfg.profiles?.improve?.default?.processes?.graphExtraction?.enabled ?? true,
20
+ // Legacy default: false
21
+ metadata_enhance: (cfg) => cfg.index?.metadataEnhance?.enabled ?? false,
22
+ // Legacy default: false
23
+ curate_rerank: (cfg) => cfg.search?.curateRerank?.enabled ?? false,
24
+ // Legacy default: false
25
+ lesson_quality_gate: (cfg) => cfg.profiles?.improve?.default?.processes?.distill?.qualityGate?.enabled ?? false,
26
+ // Legacy default: false
27
+ proposal_quality_gate: (cfg) => cfg.profiles?.improve?.default?.processes?.reflect?.qualityGate?.enabled ?? false,
28
+ // Legacy default: false
29
+ memory_contradiction_detection: (cfg) => cfg.profiles?.improve?.default?.processes?.consolidate?.contradictionDetection?.enabled ?? false,
30
+ };
29
31
  /**
30
- * Pure predicate: is the named feature gate explicitly enabled in `config`?
32
+ * Pure predicate: is the named feature gate enabled in `config`?
31
33
  *
32
- * Returns `false` when:
33
- * - the LLM block is missing,
34
- * - the `features` block is missing,
35
- * - the key is absent (defaults are `false`),
36
- * - the key is set to `false`.
34
+ * Reads from the unified 0.8.0 config shape. Defaults follow the legacy
35
+ * `LlmFeatureFlags` docstring defaults.
37
36
  */
38
37
  export function isLlmFeatureEnabled(config, feature) {
39
- if (!config?.llm?.features)
38
+ if (!config)
39
+ return false;
40
+ const resolver = FEATURE_LOCATION[feature];
41
+ if (!resolver)
40
42
  return false;
41
- return config.llm.features[feature] === true;
43
+ return resolver(config);
42
44
  }
43
- const DEFAULT_TIMEOUT_MS = 30_000;
45
+ /**
46
+ * Default hard timeout for every bounded in-tree LLM call.
47
+ */
48
+ const DEFAULT_TIMEOUT_MS = 600_000;
44
49
  /**
45
50
  * Run `fn()` only if `isLlmFeatureEnabled(config, feature)` is `true`. On
46
51
  * disablement, throw, or timeout, return `fallback` (or — if it is a
47
52
  * thunk — the value produced by calling it).
48
- *
49
- * The fallback may be a value or a synchronous/async function returning a
50
- * value. The thunk form lets call sites encode "run the deterministic
51
- * pipeline" without paying for it in the success path:
52
- *
53
- * ```ts
54
- * const ranked = await tryLlmFeature(
55
- * "curate_rerank",
56
- * config,
57
- * () => llmRerank(candidates),
58
- * () => deterministicRerank(candidates),
59
- * );
60
- * ```
61
53
  */
62
54
  export async function tryLlmFeature(feature, config, fn, fallback, opts) {
63
55
  const resolveFallback = async () => typeof fallback === "function" ? await fallback() : fallback;
@@ -79,6 +71,55 @@ export async function tryLlmFeature(feature, config, fn, fallback, opts) {
79
71
  return resolveFallback();
80
72
  }
81
73
  }
74
+ /**
75
+ * Section-agnostic process gate. After the 0.8.0 migration, the canonical
76
+ * accessor is the `FEATURE_LOCATION` map above; this helper exists so older
77
+ * call sites that knew the (section, processName) pair don't all need to
78
+ * relearn the new mapping.
79
+ *
80
+ * For unknown (section, processName) pairs the result is `false`.
81
+ */
82
+ export function isProcessEnabled(section, processName, config) {
83
+ if (!config)
84
+ return false;
85
+ // index.metadataEnhance / index.stalenessDetection are first-class new-shape entries.
86
+ if (section === "index") {
87
+ if (processName === "metadata_enhance" || processName === "metadataEnhance") {
88
+ return config.index?.metadataEnhance?.enabled ?? true;
89
+ }
90
+ if (processName === "staleness_detection" || processName === "stalenessDetection") {
91
+ return config.index?.stalenessDetection?.enabled ?? false;
92
+ }
93
+ if (processName === "memory_inference" || processName === "memoryInference") {
94
+ return isLlmFeatureEnabled(config, "memory_inference");
95
+ }
96
+ if (processName === "graph_extraction" || processName === "graphExtraction") {
97
+ return isLlmFeatureEnabled(config, "graph_extraction");
98
+ }
99
+ }
100
+ if (section === "search" && (processName === "curate_rerank" || processName === "curateRerank")) {
101
+ return config.search?.curateRerank?.enabled ?? false;
102
+ }
103
+ if (section === "improve") {
104
+ const processes = config.profiles?.improve?.default?.processes;
105
+ const entry = processes?.[processName];
106
+ if (entry && typeof entry.enabled === "boolean")
107
+ return entry.enabled;
108
+ // Fallback to default-enabled state for known processes.
109
+ switch (processName) {
110
+ case "reflect":
111
+ case "distill":
112
+ case "consolidate":
113
+ case "memoryInference":
114
+ case "graphExtraction":
115
+ case "feedbackDistillation":
116
+ return true;
117
+ default:
118
+ return false;
119
+ }
120
+ }
121
+ return false;
122
+ }
82
123
  /** Specific error class so call sites and the wrapper can tell timeouts apart from generic throws. */
83
124
  export class LlmFeatureTimeoutError extends Error {
84
125
  feature;