akm-cli 0.7.4 → 0.8.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/CHANGELOG.md +224 -1
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +133 -0
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2631 -1440
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +110 -0
  12. package/dist/commands/agent-support.js +68 -0
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +130 -534
  15. package/dist/commands/consolidate.js +2122 -0
  16. package/dist/commands/curate.js +45 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +660 -0
  19. package/dist/commands/distill.js +1081 -73
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +43 -0
  22. package/dist/commands/events.js +15 -24
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +477 -0
  28. package/dist/commands/health.js +1302 -0
  29. package/dist/commands/help/help-accept.md +12 -0
  30. package/dist/commands/help/help-improve.md +69 -0
  31. package/dist/commands/help/help-proposals.md +18 -0
  32. package/dist/commands/help/help-propose.md +17 -0
  33. package/dist/commands/help/help-reject.md +11 -0
  34. package/dist/commands/history.js +54 -46
  35. package/dist/commands/improve-auto-accept.js +97 -0
  36. package/dist/commands/improve-cli.js +217 -0
  37. package/dist/commands/improve-profiles.js +166 -0
  38. package/dist/commands/improve-result-file.js +167 -0
  39. package/dist/commands/improve.js +2373 -0
  40. package/dist/commands/info.js +5 -2
  41. package/dist/commands/init.js +50 -2
  42. package/dist/commands/installed-stashes.js +102 -139
  43. package/dist/commands/knowledge.js +136 -0
  44. package/dist/commands/lint/agent-linter.js +49 -0
  45. package/dist/commands/lint/base-linter.js +479 -0
  46. package/dist/commands/lint/command-linter.js +49 -0
  47. package/dist/commands/lint/default-linter.js +16 -0
  48. package/dist/commands/lint/env-key-rules.js +154 -0
  49. package/dist/commands/lint/index.js +196 -0
  50. package/dist/commands/lint/knowledge-linter.js +16 -0
  51. package/dist/commands/lint/markdown-insertion.js +343 -0
  52. package/dist/commands/lint/memory-linter.js +61 -0
  53. package/dist/commands/lint/registry.js +36 -0
  54. package/dist/commands/lint/skill-linter.js +45 -0
  55. package/dist/commands/lint/task-linter.js +50 -0
  56. package/dist/commands/lint/types.js +4 -0
  57. package/dist/commands/lint/workflow-linter.js +56 -0
  58. package/dist/commands/lint.js +4 -0
  59. package/dist/commands/migration-help.js +3 -0
  60. package/dist/commands/proposal.js +67 -12
  61. package/dist/commands/propose.js +120 -45
  62. package/dist/commands/reflect.js +1104 -60
  63. package/dist/commands/registry-cli.js +150 -0
  64. package/dist/commands/registry-search.js +5 -2
  65. package/dist/commands/remember-cli.js +257 -0
  66. package/dist/commands/remember.js +70 -7
  67. package/dist/commands/schema-repair.js +203 -0
  68. package/dist/commands/search.js +115 -14
  69. package/dist/commands/secret.js +173 -0
  70. package/dist/commands/self-update.js +3 -0
  71. package/dist/commands/show.js +158 -60
  72. package/dist/commands/source-add.js +17 -45
  73. package/dist/commands/source-clone.js +3 -0
  74. package/dist/commands/source-manage.js +14 -19
  75. package/dist/commands/tasks.js +437 -0
  76. package/dist/commands/url-checker.js +42 -0
  77. package/dist/core/action-contributors.js +28 -0
  78. package/dist/core/asset-ref.js +17 -2
  79. package/dist/core/asset-registry.js +12 -17
  80. package/dist/core/asset-serialize.js +88 -0
  81. package/dist/core/asset-spec.js +67 -1
  82. package/dist/core/common.js +182 -0
  83. package/dist/core/concurrent.js +25 -0
  84. package/dist/core/config-io.js +347 -0
  85. package/dist/core/config-migration.js +622 -0
  86. package/dist/core/config-schema.js +534 -0
  87. package/dist/core/config-sources.js +108 -0
  88. package/dist/core/config-types.js +4 -0
  89. package/dist/core/config-walker.js +337 -0
  90. package/dist/core/config.js +364 -968
  91. package/dist/core/errors.js +42 -20
  92. package/dist/core/events.js +105 -135
  93. package/dist/core/file-lock.js +104 -0
  94. package/dist/core/frontmatter.js +75 -8
  95. package/dist/core/lesson-lint.js +3 -0
  96. package/dist/core/markdown.js +20 -0
  97. package/dist/core/memory-belief.js +62 -0
  98. package/dist/core/memory-contradiction-detect.js +274 -0
  99. package/dist/core/memory-improve.js +806 -0
  100. package/dist/core/parse.js +158 -0
  101. package/dist/core/paths.js +280 -14
  102. package/dist/core/proposal-quality-validators.js +380 -0
  103. package/dist/core/proposal-validators.js +69 -0
  104. package/dist/core/proposals.js +512 -42
  105. package/dist/core/state-db.js +1068 -0
  106. package/dist/core/text-truncation.js +107 -0
  107. package/dist/core/time.js +54 -0
  108. package/dist/core/tty.js +59 -0
  109. package/dist/core/warn.js +64 -1
  110. package/dist/core/write-source.js +3 -0
  111. package/dist/indexer/db-backup.js +391 -0
  112. package/dist/indexer/db-search.js +198 -489
  113. package/dist/indexer/db.js +990 -108
  114. package/dist/indexer/ensure-index.js +136 -0
  115. package/dist/indexer/file-context.js +3 -0
  116. package/dist/indexer/graph-boost.js +376 -101
  117. package/dist/indexer/graph-db.js +391 -0
  118. package/dist/indexer/graph-dedup.js +95 -0
  119. package/dist/indexer/graph-extraction.js +550 -114
  120. package/dist/indexer/index-context.js +4 -0
  121. package/dist/indexer/indexer.js +547 -309
  122. package/dist/indexer/llm-cache.js +52 -0
  123. package/dist/indexer/manifest.js +3 -0
  124. package/dist/indexer/matchers.js +167 -160
  125. package/dist/indexer/memory-inference.js +152 -74
  126. package/dist/indexer/metadata-contributors.js +29 -0
  127. package/dist/indexer/metadata.js +275 -196
  128. package/dist/indexer/path-resolver.js +92 -0
  129. package/dist/indexer/project-context.js +192 -0
  130. package/dist/indexer/ranking-contributors.js +331 -0
  131. package/dist/indexer/ranking.js +81 -0
  132. package/dist/indexer/search-fields.js +5 -9
  133. package/dist/indexer/search-hit-enrichers.js +111 -0
  134. package/dist/indexer/search-source.js +44 -10
  135. package/dist/indexer/semantic-status.js +6 -17
  136. package/dist/indexer/staleness-detect.js +447 -0
  137. package/dist/indexer/usage-events.js +12 -9
  138. package/dist/indexer/walker.js +28 -0
  139. package/dist/integrations/agent/builders.js +135 -0
  140. package/dist/integrations/agent/config.js +122 -230
  141. package/dist/integrations/agent/detect.js +3 -0
  142. package/dist/integrations/agent/index.js +7 -13
  143. package/dist/integrations/agent/model-aliases.js +55 -0
  144. package/dist/integrations/agent/profiles.js +70 -5
  145. package/dist/integrations/agent/prompts.js +250 -36
  146. package/dist/integrations/agent/runner.js +151 -0
  147. package/dist/integrations/agent/sdk-runner.js +126 -0
  148. package/dist/integrations/agent/spawn.js +183 -35
  149. package/dist/integrations/github.js +3 -0
  150. package/dist/integrations/lockfile.js +32 -69
  151. package/dist/integrations/session-logs/index.js +69 -0
  152. package/dist/integrations/session-logs/inline-refs.js +35 -0
  153. package/dist/integrations/session-logs/pre-filter.js +152 -0
  154. package/dist/integrations/session-logs/providers/claude-code.js +282 -0
  155. package/dist/integrations/session-logs/providers/opencode.js +258 -0
  156. package/dist/integrations/session-logs/types.js +4 -0
  157. package/dist/llm/call-ai.js +62 -0
  158. package/dist/llm/client.js +79 -88
  159. package/dist/llm/embedder.js +20 -29
  160. package/dist/llm/embedders/cache.js +3 -7
  161. package/dist/llm/embedders/local.js +42 -1
  162. package/dist/llm/embedders/remote.js +20 -8
  163. package/dist/llm/embedders/types.js +3 -7
  164. package/dist/llm/feature-gate.js +95 -48
  165. package/dist/llm/graph-extract.js +676 -72
  166. package/dist/llm/index-passes.js +44 -29
  167. package/dist/llm/memory-infer.js +80 -71
  168. package/dist/llm/metadata-enhance.js +42 -29
  169. package/dist/llm/prompts/extract-session.md +80 -0
  170. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  171. package/dist/output/cli-hints-full.md +292 -0
  172. package/dist/output/cli-hints-short.md +66 -0
  173. package/dist/output/cli-hints.js +7 -311
  174. package/dist/output/context.js +60 -8
  175. package/dist/output/renderers.js +306 -258
  176. package/dist/output/shapes/curate.js +56 -0
  177. package/dist/output/shapes/distill.js +10 -0
  178. package/dist/output/shapes/env-list.js +19 -0
  179. package/dist/output/shapes/events.js +11 -0
  180. package/dist/output/shapes/helpers.js +424 -0
  181. package/dist/output/shapes/history.js +7 -0
  182. package/dist/output/shapes/passthrough.js +102 -0
  183. package/dist/output/shapes/proposal-accept.js +7 -0
  184. package/dist/output/shapes/proposal-diff.js +7 -0
  185. package/dist/output/shapes/proposal-list.js +7 -0
  186. package/dist/output/shapes/proposal-producer.js +11 -0
  187. package/dist/output/shapes/proposal-reject.js +7 -0
  188. package/dist/output/shapes/proposal-show.js +7 -0
  189. package/dist/output/shapes/registry-search.js +6 -0
  190. package/dist/output/shapes/registry.js +30 -0
  191. package/dist/output/shapes/search.js +6 -0
  192. package/dist/output/shapes/secret-list.js +19 -0
  193. package/dist/output/shapes/show.js +6 -0
  194. package/dist/output/shapes/vault-list.js +19 -0
  195. package/dist/output/shapes.js +51 -511
  196. package/dist/output/text/add.js +6 -0
  197. package/dist/output/text/clone.js +6 -0
  198. package/dist/output/text/config.js +6 -0
  199. package/dist/output/text/curate.js +6 -0
  200. package/dist/output/text/distill.js +7 -0
  201. package/dist/output/text/enable-disable.js +7 -0
  202. package/dist/output/text/events.js +10 -0
  203. package/dist/output/text/feedback.js +6 -0
  204. package/dist/output/text/helpers.js +1039 -0
  205. package/dist/output/text/history.js +7 -0
  206. package/dist/output/text/import.js +6 -0
  207. package/dist/output/text/index.js +6 -0
  208. package/dist/output/text/info.js +6 -0
  209. package/dist/output/text/init.js +6 -0
  210. package/dist/output/text/list.js +6 -0
  211. package/dist/output/text/proposal-producer.js +8 -0
  212. package/dist/output/text/proposal.js +11 -0
  213. package/dist/output/text/registry-commands.js +11 -0
  214. package/dist/output/text/registry.js +30 -0
  215. package/dist/output/text/remember.js +6 -0
  216. package/dist/output/text/remove.js +6 -0
  217. package/dist/output/text/save.js +6 -0
  218. package/dist/output/text/search.js +6 -0
  219. package/dist/output/text/show.js +6 -0
  220. package/dist/output/text/update.js +6 -0
  221. package/dist/output/text/upgrade.js +6 -0
  222. package/dist/output/text/vault.js +16 -0
  223. package/dist/output/text/wiki.js +15 -0
  224. package/dist/output/text/workflow.js +14 -0
  225. package/dist/output/text.js +44 -1093
  226. package/dist/registry/build-index.js +3 -0
  227. package/dist/registry/create-provider-registry.js +3 -0
  228. package/dist/registry/factory.js +4 -1
  229. package/dist/registry/origin-resolve.js +3 -0
  230. package/dist/registry/providers/index.js +3 -0
  231. package/dist/registry/providers/skills-sh.js +71 -50
  232. package/dist/registry/providers/static-index.js +53 -48
  233. package/dist/registry/providers/types.js +3 -24
  234. package/dist/registry/resolve.js +11 -16
  235. package/dist/registry/types.js +3 -0
  236. package/dist/scripts/migrate-storage.js +17750 -0
  237. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  238. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  239. package/dist/setup/detect.js +3 -0
  240. package/dist/setup/ripgrep-install.js +3 -0
  241. package/dist/setup/ripgrep-resolve.js +3 -0
  242. package/dist/setup/setup.js +775 -37
  243. package/dist/setup/steps.js +3 -15
  244. package/dist/sources/include.js +3 -0
  245. package/dist/sources/provider-factory.js +5 -12
  246. package/dist/sources/provider.js +3 -20
  247. package/dist/sources/providers/filesystem.js +19 -23
  248. package/dist/sources/providers/git.js +179 -20
  249. package/dist/sources/providers/index.js +3 -0
  250. package/dist/sources/providers/install-types.js +3 -13
  251. package/dist/sources/providers/npm.js +3 -4
  252. package/dist/sources/providers/provider-utils.js +3 -0
  253. package/dist/sources/providers/sync-from-ref.js +3 -11
  254. package/dist/sources/providers/tar-utils.js +3 -0
  255. package/dist/sources/providers/website.js +18 -22
  256. package/dist/sources/resolve.js +3 -0
  257. package/dist/sources/types.js +3 -0
  258. package/dist/sources/website-ingest.js +7 -0
  259. package/dist/tasks/backends/cron.js +203 -0
  260. package/dist/tasks/backends/exec-utils.js +28 -0
  261. package/dist/tasks/backends/index.js +24 -0
  262. package/dist/tasks/backends/launchd-template.xml +19 -0
  263. package/dist/tasks/backends/launchd.js +187 -0
  264. package/dist/tasks/backends/schtasks-template.xml +29 -0
  265. package/dist/tasks/backends/schtasks.js +215 -0
  266. package/dist/tasks/parser.js +211 -0
  267. package/dist/tasks/resolveAkmBin.js +87 -0
  268. package/dist/tasks/runner.js +458 -0
  269. package/dist/tasks/schedule.js +227 -0
  270. package/dist/tasks/schema.js +15 -0
  271. package/dist/tasks/validator.js +62 -0
  272. package/dist/version.js +3 -0
  273. package/dist/wiki/index-template.md +12 -0
  274. package/dist/wiki/ingest-workflow-template.md +54 -0
  275. package/dist/wiki/log-template.md +8 -0
  276. package/dist/wiki/schema-template.md +61 -0
  277. package/dist/wiki/wiki-templates.js +15 -0
  278. package/dist/wiki/wiki.js +13 -61
  279. package/dist/workflows/authoring.js +8 -25
  280. package/dist/workflows/cli.js +3 -0
  281. package/dist/workflows/db.js +141 -2
  282. package/dist/workflows/document-cache.js +3 -10
  283. package/dist/workflows/parser.js +3 -0
  284. package/dist/workflows/renderer.js +11 -3
  285. package/dist/workflows/runs.js +91 -89
  286. package/dist/workflows/schema.js +3 -0
  287. package/dist/workflows/scope-key.js +79 -0
  288. package/dist/workflows/validator.js +4 -8
  289. package/dist/workflows/workflow-template.md +24 -0
  290. package/docs/README.md +10 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.0.md +1 -1
  293. package/docs/migration/release-notes/0.7.4.md +1 -1
  294. package/docs/migration/release-notes/0.7.5.md +20 -0
  295. package/docs/migration/release-notes/0.8.0.md +48 -0
  296. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  297. package/package.json +29 -11
  298. package/dist/commands/install-audit.js +0 -381
  299. package/dist/commands/vault.js +0 -333
  300. package/dist/templates/wiki-templates.js +0 -100
@@ -0,0 +1,4 @@
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
+ export {};
@@ -0,0 +1,62 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { getDefaultLlmConfig } from "../core/config";
5
+ import { warn } from "../core/warn";
6
+ import { resolveAgentProfile, runAgent } from "../integrations/agent";
7
+ import { chatCompletion } from "./client";
8
+ /**
9
+ * Unified AI call: prefers the default agent profile, falls back to the
10
+ * default LLM profile. When neither is configured, returns a structured
11
+ * error pointing the user at `akm setup`.
12
+ */
13
+ export async function callAi(config, prompt, opts = {}) {
14
+ const defaultAgentName = config.defaults?.agent;
15
+ if (defaultAgentName) {
16
+ try {
17
+ const profile = resolveAgentProfile(defaultAgentName, config.profiles?.agent?.[defaultAgentName]);
18
+ if (!profile) {
19
+ return {
20
+ ok: false,
21
+ error: `Agent profile "${defaultAgentName}" is not built-in and has no \`bin\` override.`,
22
+ };
23
+ }
24
+ const result = await runAgent(profile, prompt, {
25
+ stdio: opts.draftFilePath ? "interactive" : "captured",
26
+ parseOutput: "text",
27
+ timeoutMs: opts.timeoutMs,
28
+ });
29
+ if (!result.ok)
30
+ return { ok: false, error: result.error ?? result.reason ?? "agent failed" };
31
+ return { ok: true, content: result.stdout ?? "", path: "agent-cli" };
32
+ }
33
+ catch (e) {
34
+ return { ok: false, error: String(e) };
35
+ }
36
+ }
37
+ const llmConfig = getDefaultLlmConfig(config);
38
+ if (llmConfig) {
39
+ if (opts.draftFilePath) {
40
+ warn("[akm] No agent CLI configured — falling back to LLM API. " +
41
+ "File-write contract unavailable; expecting JSON in stdout. " +
42
+ "Install an agent CLI and run `akm setup` for full functionality.");
43
+ }
44
+ const messages = [];
45
+ if (opts.systemPrompt)
46
+ messages.push({ role: "system", content: opts.systemPrompt });
47
+ messages.push({ role: "user", content: prompt });
48
+ try {
49
+ const content = await chatCompletion(llmConfig, messages, {
50
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
51
+ });
52
+ return { ok: true, content, path: "llm-http" };
53
+ }
54
+ catch (e) {
55
+ return { ok: false, error: String(e) };
56
+ }
57
+ }
58
+ return {
59
+ ok: false,
60
+ error: "No AI connection configured. Run `akm setup` or set `defaults.agent`/`defaults.llm`.",
61
+ };
62
+ }
@@ -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,105 +46,88 @@ 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) {
60
+ const timeoutMs = options?.timeoutMs ?? config.timeoutMs ?? 120_000;
42
61
  const headers = { "Content-Type": "application/json" };
43
- if (config.apiKey) {
44
- headers.Authorization = `Bearer ${config.apiKey}`;
62
+ const resolvedKey = resolveSecret(config.apiKey);
63
+ if (resolvedKey) {
64
+ headers.Authorization = `Bearer ${resolvedKey}`;
65
+ }
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
+ ...(options?.enableThinking !== undefined
85
+ ? { enable_thinking: options.enableThinking }
86
+ : config.enableThinking !== undefined
87
+ ? { enable_thinking: config.enableThinking }
88
+ : {}),
89
+ ...config.extraParams,
90
+ }),
91
+ }, timeoutMs, options?.signal);
92
+ }
93
+ catch (err) {
94
+ // fetchWithTimeout throws a plain Error with a message containing
95
+ // "timed out" for AbortController-driven timeouts, or "aborted" for
96
+ // caller-driven cancellations. Map both to typed LlmCallError.
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ if (err instanceof DOMException && err.name === "AbortError") {
99
+ throw new LlmCallError(`Request timed out after ${timeoutMs}ms`, "timeout");
100
+ }
101
+ if (msg.includes("timed out")) {
102
+ throw new LlmCallError(`Request timed out after ${timeoutMs}ms`, "timeout");
103
+ }
104
+ throw new LlmCallError(`Network error: ${msg}`, "network_error");
45
105
  }
46
- const response = await fetchWithTimeout(config.endpoint, {
47
- method: "POST",
48
- headers,
49
- body: JSON.stringify({
50
- model: config.model,
51
- messages,
52
- temperature: options?.temperature ?? config.temperature ?? 0.3,
53
- max_tokens: options?.maxTokens ?? config.maxTokens ?? 512,
54
- ...config.extraParams,
55
- }),
56
- }, 30_000, options?.signal);
57
106
  if (!response.ok) {
58
107
  const rawBody = await response.text().catch(() => "");
59
108
  const safeBody = redactErrorBody(rawBody);
60
- throw new Error(`LLM request failed (${response.status}) ${config.endpoint}: ${safeBody}`);
109
+ const status = response.status;
110
+ if (status === 429) {
111
+ throw new LlmCallError(`LLM request rate limited (429) ${config.endpoint}: ${safeBody}`, "rate_limited", status);
112
+ }
113
+ if (status >= 500) {
114
+ throw new LlmCallError(`LLM provider error (${status}) ${config.endpoint}: ${safeBody}`, "provider_error", status);
115
+ }
116
+ throw new LlmCallError(`LLM request failed (${status}) ${config.endpoint}: ${safeBody}`, "provider_error", status);
61
117
  }
62
118
  const json = (await response.json());
63
- return json.choices?.[0]?.message?.content?.trim() ?? "";
64
- }
65
- /** Strip leading/trailing markdown code fences from an LLM response. */
66
- export function stripJsonFences(raw) {
67
- return raw
68
- .trim()
69
- .replace(/<think>[\s\S]*?<\/think>/gi, "")
70
- .replace(/^```(?:json)?\s*\n?/i, "")
71
- .replace(/\n?```\s*$/i, "")
72
- .trim();
73
- }
74
- /** Parse a possibly-fenced JSON response. Returns undefined if invalid. */
75
- export function parseJsonResponse(raw) {
76
- try {
77
- return JSON.parse(stripJsonFences(raw));
78
- }
79
- catch {
80
- return undefined;
81
- }
119
+ const content = (json.choices?.[0]?.message?.content ?? "").trim();
120
+ const reasoning = (json.choices?.[0]?.message?.reasoning_content ?? "").trim();
121
+ return content || reasoning;
82
122
  }
83
123
  /**
84
- * Best-effort recovery for providers that wrap JSON in extra prose or fenced
85
- * blocks. Extracts the first balanced top-level object/array and parses it.
124
+ * Strip `<think>` blocks, code fences, and escape control characters in JSON
125
+ * strings. Thin wrapper kept for backward compatibility with call sites that
126
+ * import `stripJsonFences` from this module. New code should prefer the
127
+ * granular helpers from `../core/parse`.
86
128
  */
87
- export function parseEmbeddedJsonResponse(raw) {
88
- const direct = parseJsonResponse(raw);
89
- if (direct !== undefined)
90
- return direct;
91
- const text = stripJsonFences(raw);
92
- let arrayFallback;
93
- for (let start = 0; start < text.length; start++) {
94
- const opener = text[start];
95
- if (opener !== "{" && opener !== "[")
96
- continue;
97
- const closer = opener === "{" ? "}" : "]";
98
- let depth = 0;
99
- let inString = false;
100
- let escaped = false;
101
- for (let i = start; i < text.length; i++) {
102
- const ch = text[i];
103
- if (inString) {
104
- if (escaped) {
105
- escaped = false;
106
- }
107
- else if (ch === "\\") {
108
- escaped = true;
109
- }
110
- else if (ch === '"') {
111
- inString = false;
112
- }
113
- continue;
114
- }
115
- if (ch === '"') {
116
- inString = true;
117
- continue;
118
- }
119
- if (ch === opener)
120
- depth += 1;
121
- if (ch === closer) {
122
- depth -= 1;
123
- if (depth === 0) {
124
- try {
125
- const parsed = JSON.parse(text.slice(start, i + 1));
126
- if (!Array.isArray(parsed)) {
127
- return parsed;
128
- }
129
- arrayFallback ??= parsed;
130
- break;
131
- }
132
- catch {
133
- break;
134
- }
135
- }
136
- }
137
- }
138
- }
139
- return arrayFallback;
129
+ export function stripJsonFences(raw) {
130
+ return escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw)));
140
131
  }
141
132
  // ── Availability check ──────────────────────────────────────────────────────
142
133
  /**
@@ -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";
@@ -24,18 +8,25 @@ import { hasRemoteEndpoint, RemoteEmbedder } from "./embedders/remote";
24
8
  export { clearEmbeddingCache } from "./embedders/cache";
25
9
  export { DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "./embedders/local";
26
10
  // ── Singleton local embedder ────────────────────────────────────────────────
27
- // `localEmbedder` is an intentional module-level singleton. The underlying
28
- // @huggingface/transformers pipeline is expensive to initialise (model download
29
- // + WASM compilation) and is safe to share across calls because it is
30
- // stateless once created. Storing it here avoids re-initialising on every
31
- // embed() call.
32
- const localEmbedder = new LocalEmbedder();
11
+ // `_localEmbedder` is an intentional module-level singleton but constructed
12
+ // lazily on first use. The underlying @huggingface/transformers pipeline is
13
+ // expensive to initialise (model download + WASM compilation) and is safe to
14
+ // share across calls because it is stateless once created. Deferring
15
+ // construction to first call keeps the module side-effect-free at import time,
16
+ // which matters for the test suite (single Bun process, ~120 test files).
17
+ let _localEmbedder;
18
+ function getLocalEmbedder() {
19
+ if (!_localEmbedder) {
20
+ _localEmbedder = new LocalEmbedder();
21
+ }
22
+ return _localEmbedder;
23
+ }
33
24
  /**
34
25
  * Reset the cached local embedder pipeline. Used by tests that want a fresh
35
26
  * pipeline construction (e.g. to assert the dtype-fallback retry logic).
36
27
  */
37
28
  export function resetLocalEmbedder() {
38
- localEmbedder.reset();
29
+ getLocalEmbedder().reset();
39
30
  }
40
31
  // ── Public API ──────────────────────────────────────────────────────────────
41
32
  /**
@@ -54,7 +45,7 @@ export async function embed(text, embeddingConfig, signal) {
54
45
  return cached;
55
46
  const result = embeddingConfig && hasRemoteEndpoint(embeddingConfig)
56
47
  ? await new RemoteEmbedder(embeddingConfig).embed(text, signal)
57
- : await localEmbedder.embed(text, signal);
48
+ : await getLocalEmbedder().embed(text, signal);
58
49
  setCachedEmbedding(key, result);
59
50
  return result;
60
51
  }
@@ -76,7 +67,7 @@ export async function embedBatch(texts, embeddingConfig, signal) {
76
67
  if (signal?.aborted) {
77
68
  throw signal.reason instanceof Error ? signal.reason : new Error("embedding interrupted");
78
69
  }
79
- results.push(await localEmbedder.embedWithModel(text, localModel));
70
+ results.push(await getLocalEmbedder().embedWithModel(text, localModel));
80
71
  }
81
72
  return results;
82
73
  }
@@ -113,7 +104,7 @@ export async function checkEmbeddingAvailability(embeddingConfig) {
113
104
  };
114
105
  }
115
106
  try {
116
- await localEmbedder.getPipeline(embeddingConfig?.localModel);
107
+ await getLocalEmbedder().getPipeline(embeddingConfig?.localModel);
117
108
  return { available: true };
118
109
  }
119
110
  catch (err) {
@@ -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
  *
@@ -25,6 +28,31 @@ const LOCAL_EMBEDDER_FALLBACK_DTYPE = "auto";
25
28
  function resolveLocalModelName(overrideModel) {
26
29
  return overrideModel || DEFAULT_LOCAL_MODEL;
27
30
  }
31
+ /**
32
+ * Detect whether the current process is running from a Bun-compiled binary
33
+ * (i.e. `bun build --compile` produced a single executable). Bun marks the
34
+ * compiled binary with a synthesized `process.execPath` that ends in the
35
+ * binary name rather than `bun`, AND sets a flag we can probe.
36
+ *
37
+ * Used to gate the "install @huggingface/transformers" hint — that advice
38
+ * is impossible to follow from a single-binary install, so we replace it
39
+ * with the only working remediation (switch to npm/Bun install, or turn
40
+ * semantic search off). See #482.
41
+ */
42
+ function isCompiledBinary() {
43
+ try {
44
+ const flag = Bun.embeddedFiles;
45
+ if (flag !== undefined)
46
+ return true;
47
+ }
48
+ catch {
49
+ // Bun not available (under Node tests, for example) — treat as not-binary.
50
+ }
51
+ const exec = (process.execPath || "").toLowerCase();
52
+ if (exec.endsWith("/akm") || exec.endsWith("\\akm.exe"))
53
+ return true;
54
+ return false;
55
+ }
28
56
  export class LocalEmbedder {
29
57
  defaultModel;
30
58
  /**
@@ -93,7 +121,20 @@ export class LocalEmbedder {
93
121
  catch (importError) {
94
122
  const msg = importError instanceof Error ? importError.message : String(importError);
95
123
  if (/Cannot find module|MODULE_NOT_FOUND|Cannot resolve/i.test(msg)) {
96
- throw new Error("Semantic search requires @huggingface/transformers. Install it with: bun add @huggingface/transformers");
124
+ // #482: the prebuilt binary build is invoked with
125
+ // `bun install --omit optional` (release.yml), so binary users
126
+ // can NEVER load @huggingface/transformers. Telling them to
127
+ // `bun add` it is a dead-end — there is no install target.
128
+ // Detect the binary execution path and give the only working
129
+ // remediation: switch to the npm/Bun install of akm-cli, or
130
+ // turn off semantic search.
131
+ const isBinary = isCompiledBinary();
132
+ const hint = isBinary
133
+ ? "You are running the prebuilt akm binary, which cannot load optional native dependencies. " +
134
+ "To enable semantic search, install akm-cli via Bun: `curl -fsSL https://bun.sh/install | bash && bun install -g akm-cli`. " +
135
+ "To keep using the binary, set `semanticSearchMode: off` in your config and use keyword-only FTS."
136
+ : "Install it with: `bun add @huggingface/transformers` (or `npm install @huggingface/transformers`).";
137
+ throw new Error(`Semantic search requires @huggingface/transformers. ${hint}`);
97
138
  }
98
139
  throw new Error(`Failed to load embedding runtime: ${msg}. Check platform compatibility.`);
99
140
  }
@@ -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
  *