akm-cli 0.8.0-rc1 → 0.8.0

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 (295) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
  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 +93 -3
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2162 -1258
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +20 -12
  12. package/dist/commands/agent-support.js +11 -5
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +129 -517
  15. package/dist/commands/consolidate.js +1533 -144
  16. package/dist/commands/curate.js +44 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +5 -3
  19. package/dist/commands/distill.js +906 -100
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +3 -0
  22. package/dist/commands/events.js +3 -0
  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 +260 -5
  28. package/dist/commands/health.js +977 -51
  29. package/dist/commands/help/help-accept.md +6 -3
  30. package/dist/commands/help/help-improve.md +36 -8
  31. package/dist/commands/help/help-proposals.md +7 -4
  32. package/dist/commands/help/help-reject.md +5 -2
  33. package/dist/commands/history.js +51 -16
  34. package/dist/commands/improve-auto-accept.js +97 -0
  35. package/dist/commands/improve-cli.js +236 -0
  36. package/dist/commands/improve-profiles.js +184 -0
  37. package/dist/commands/improve-result-file.js +167 -0
  38. package/dist/commands/improve.js +1725 -332
  39. package/dist/commands/info.js +3 -0
  40. package/dist/commands/init.js +49 -1
  41. package/dist/commands/installed-stashes.js +6 -23
  42. package/dist/commands/knowledge.js +3 -0
  43. package/dist/commands/lint/agent-linter.js +3 -0
  44. package/dist/commands/lint/base-linter.js +233 -5
  45. package/dist/commands/lint/command-linter.js +3 -0
  46. package/dist/commands/lint/default-linter.js +3 -0
  47. package/dist/commands/lint/env-key-rules.js +154 -0
  48. package/dist/commands/lint/index.js +92 -3
  49. package/dist/commands/lint/knowledge-linter.js +3 -0
  50. package/dist/commands/lint/markdown-insertion.js +343 -0
  51. package/dist/commands/lint/memory-linter.js +3 -0
  52. package/dist/commands/lint/registry.js +3 -0
  53. package/dist/commands/lint/skill-linter.js +3 -0
  54. package/dist/commands/lint/task-linter.js +15 -12
  55. package/dist/commands/lint/types.js +3 -0
  56. package/dist/commands/lint/workflow-linter.js +3 -0
  57. package/dist/commands/lint.js +3 -0
  58. package/dist/commands/migration-help.js +5 -2
  59. package/dist/commands/proposal-drain-policies.js +128 -0
  60. package/dist/commands/proposal-drain.js +477 -0
  61. package/dist/commands/proposal.js +60 -6
  62. package/dist/commands/propose.js +24 -19
  63. package/dist/commands/reflect.js +1004 -94
  64. package/dist/commands/registry-cli.js +150 -0
  65. package/dist/commands/registry-search.js +3 -0
  66. package/dist/commands/remember-cli.js +257 -0
  67. package/dist/commands/remember.js +15 -6
  68. package/dist/commands/schema-repair.js +88 -15
  69. package/dist/commands/search.js +99 -14
  70. package/dist/commands/secret.js +173 -0
  71. package/dist/commands/self-update.js +3 -0
  72. package/dist/commands/show.js +32 -13
  73. package/dist/commands/source-add.js +7 -35
  74. package/dist/commands/source-clone.js +3 -0
  75. package/dist/commands/source-manage.js +3 -0
  76. package/dist/commands/tasks.js +161 -95
  77. package/dist/commands/url-checker.js +3 -0
  78. package/dist/core/action-contributors.js +3 -0
  79. package/dist/core/asset-ref.js +17 -2
  80. package/dist/core/asset-registry.js +9 -2
  81. package/dist/core/asset-serialize.js +88 -0
  82. package/dist/core/asset-spec.js +61 -5
  83. package/dist/core/common.js +93 -5
  84. package/dist/core/concurrent.js +3 -0
  85. package/dist/core/config-io.js +347 -0
  86. package/dist/core/config-migration.js +622 -0
  87. package/dist/core/config-schema.js +558 -0
  88. package/dist/core/config-sources.js +108 -0
  89. package/dist/core/config-types.js +4 -0
  90. package/dist/core/config-walker.js +337 -0
  91. package/dist/core/config.js +366 -1077
  92. package/dist/core/errors.js +42 -20
  93. package/dist/core/events.js +31 -25
  94. package/dist/core/file-lock.js +104 -0
  95. package/dist/core/frontmatter.js +75 -10
  96. package/dist/core/lesson-lint.js +3 -0
  97. package/dist/core/markdown.js +3 -0
  98. package/dist/core/memory-belief.js +62 -0
  99. package/dist/core/memory-contradiction-detect.js +274 -0
  100. package/dist/core/memory-improve.js +142 -14
  101. package/dist/core/parse.js +3 -0
  102. package/dist/core/paths.js +218 -50
  103. package/dist/core/proposal-quality-validators.js +380 -0
  104. package/dist/core/proposal-validators.js +11 -3
  105. package/dist/core/proposals.js +464 -5
  106. package/dist/core/state-db.js +349 -56
  107. package/dist/core/text-truncation.js +107 -0
  108. package/dist/core/time.js +3 -0
  109. package/dist/core/tty.js +59 -0
  110. package/dist/core/warn.js +7 -2
  111. package/dist/core/write-source.js +12 -0
  112. package/dist/indexer/db-backup.js +391 -0
  113. package/dist/indexer/db-search.js +136 -28
  114. package/dist/indexer/db.js +662 -166
  115. package/dist/indexer/ensure-index.js +3 -0
  116. package/dist/indexer/file-context.js +3 -0
  117. package/dist/indexer/graph-boost.js +162 -40
  118. package/dist/indexer/graph-db.js +241 -51
  119. package/dist/indexer/graph-dedup.js +3 -7
  120. package/dist/indexer/graph-extraction.js +242 -149
  121. package/dist/indexer/index-context.js +3 -9
  122. package/dist/indexer/indexer.js +84 -14
  123. package/dist/indexer/llm-cache.js +24 -19
  124. package/dist/indexer/manifest.js +3 -0
  125. package/dist/indexer/matchers.js +184 -11
  126. package/dist/indexer/memory-inference.js +94 -50
  127. package/dist/indexer/metadata-contributors.js +3 -0
  128. package/dist/indexer/metadata.js +114 -48
  129. package/dist/indexer/path-resolver.js +3 -0
  130. package/dist/indexer/project-context.js +192 -0
  131. package/dist/indexer/ranking-contributors.js +134 -7
  132. package/dist/indexer/ranking.js +8 -1
  133. package/dist/indexer/search-fields.js +5 -9
  134. package/dist/indexer/search-hit-enrichers.js +91 -2
  135. package/dist/indexer/search-source.js +20 -1
  136. package/dist/indexer/semantic-status.js +4 -1
  137. package/dist/indexer/staleness-detect.js +447 -0
  138. package/dist/indexer/usage-events.js +12 -9
  139. package/dist/indexer/walker.js +3 -0
  140. package/dist/integrations/agent/builders.js +135 -0
  141. package/dist/integrations/agent/config.js +121 -401
  142. package/dist/integrations/agent/detect.js +3 -0
  143. package/dist/integrations/agent/index.js +6 -14
  144. package/dist/integrations/agent/model-aliases.js +55 -0
  145. package/dist/integrations/agent/profiles.js +3 -0
  146. package/dist/integrations/agent/prompts.js +137 -8
  147. package/dist/integrations/agent/runner.js +208 -0
  148. package/dist/integrations/agent/sdk-runner.js +8 -2
  149. package/dist/integrations/agent/spawn.js +54 -14
  150. package/dist/integrations/github.js +3 -0
  151. package/dist/integrations/lockfile.js +22 -51
  152. package/dist/integrations/session-logs/index.js +4 -0
  153. package/dist/integrations/session-logs/inline-refs.js +35 -0
  154. package/dist/integrations/session-logs/pre-filter.js +152 -0
  155. package/dist/integrations/session-logs/providers/claude-code.js +226 -0
  156. package/dist/integrations/session-logs/providers/opencode.js +231 -25
  157. package/dist/integrations/session-logs/types.js +3 -0
  158. package/dist/llm/call-ai.js +14 -26
  159. package/dist/llm/client.js +16 -2
  160. package/dist/llm/embedder.js +20 -29
  161. package/dist/llm/embedders/cache.js +3 -7
  162. package/dist/llm/embedders/local.js +42 -1
  163. package/dist/llm/embedders/remote.js +20 -8
  164. package/dist/llm/embedders/types.js +3 -7
  165. package/dist/llm/feature-gate.js +92 -56
  166. package/dist/llm/graph-extract.js +401 -30
  167. package/dist/llm/index-passes.js +44 -29
  168. package/dist/llm/memory-infer.js +30 -2
  169. package/dist/llm/metadata-enhance.js +3 -7
  170. package/dist/llm/prompts/extract-session.md +80 -0
  171. package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
  172. package/dist/output/cli-hints-full.md +60 -32
  173. package/dist/output/cli-hints-short.md +10 -7
  174. package/dist/output/cli-hints.js +5 -2
  175. package/dist/output/context.js +60 -8
  176. package/dist/output/renderers.js +170 -194
  177. package/dist/output/shapes/curate.js +56 -0
  178. package/dist/output/shapes/distill.js +10 -0
  179. package/dist/output/shapes/env-list.js +19 -0
  180. package/dist/output/shapes/events.js +11 -0
  181. package/dist/output/shapes/helpers.js +424 -0
  182. package/dist/output/shapes/history.js +7 -0
  183. package/dist/output/shapes/passthrough.js +105 -0
  184. package/dist/output/shapes/proposal-accept.js +7 -0
  185. package/dist/output/shapes/proposal-diff.js +7 -0
  186. package/dist/output/shapes/proposal-list.js +7 -0
  187. package/dist/output/shapes/proposal-producer.js +11 -0
  188. package/dist/output/shapes/proposal-reject.js +7 -0
  189. package/dist/output/shapes/proposal-show.js +7 -0
  190. package/dist/output/shapes/registry-search.js +6 -0
  191. package/dist/output/shapes/registry.js +30 -0
  192. package/dist/output/shapes/search.js +6 -0
  193. package/dist/output/shapes/secret-list.js +19 -0
  194. package/dist/output/shapes/show.js +6 -0
  195. package/dist/output/shapes/vault-list.js +19 -0
  196. package/dist/output/shapes.js +51 -549
  197. package/dist/output/text/add.js +6 -0
  198. package/dist/output/text/clone.js +6 -0
  199. package/dist/output/text/config.js +6 -0
  200. package/dist/output/text/curate.js +6 -0
  201. package/dist/output/text/distill.js +7 -0
  202. package/dist/output/text/enable-disable.js +7 -0
  203. package/dist/output/text/events.js +10 -0
  204. package/dist/output/text/feedback.js +6 -0
  205. package/dist/output/text/helpers.js +1059 -0
  206. package/dist/output/text/history.js +7 -0
  207. package/dist/output/text/import.js +6 -0
  208. package/dist/output/text/index.js +6 -0
  209. package/dist/output/text/info.js +6 -0
  210. package/dist/output/text/init.js +6 -0
  211. package/dist/output/text/list.js +6 -0
  212. package/dist/output/text/proposal-producer.js +8 -0
  213. package/dist/output/text/proposal.js +12 -0
  214. package/dist/output/text/registry-commands.js +11 -0
  215. package/dist/output/text/registry.js +30 -0
  216. package/dist/output/text/remember.js +6 -0
  217. package/dist/output/text/remove.js +6 -0
  218. package/dist/output/text/save.js +6 -0
  219. package/dist/output/text/search.js +6 -0
  220. package/dist/output/text/show.js +6 -0
  221. package/dist/output/text/update.js +6 -0
  222. package/dist/output/text/upgrade.js +6 -0
  223. package/dist/output/text/vault.js +16 -0
  224. package/dist/output/text/wiki.js +15 -0
  225. package/dist/output/text/workflow.js +14 -0
  226. package/dist/output/text.js +44 -1329
  227. package/dist/registry/build-index.js +3 -0
  228. package/dist/registry/create-provider-registry.js +3 -0
  229. package/dist/registry/factory.js +4 -1
  230. package/dist/registry/origin-resolve.js +3 -0
  231. package/dist/registry/providers/index.js +3 -0
  232. package/dist/registry/providers/skills-sh.js +11 -2
  233. package/dist/registry/providers/static-index.js +10 -1
  234. package/dist/registry/providers/types.js +3 -24
  235. package/dist/registry/resolve.js +11 -16
  236. package/dist/registry/types.js +3 -0
  237. package/dist/scripts/migrate-storage.js +17767 -0
  238. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  239. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  240. package/dist/setup/detect.js +3 -0
  241. package/dist/setup/ripgrep-install.js +3 -0
  242. package/dist/setup/ripgrep-resolve.js +3 -0
  243. package/dist/setup/setup.js +306 -67
  244. package/dist/setup/steps.js +3 -15
  245. package/dist/sources/include.js +3 -0
  246. package/dist/sources/provider-factory.js +3 -11
  247. package/dist/sources/provider.js +3 -20
  248. package/dist/sources/providers/filesystem.js +19 -23
  249. package/dist/sources/providers/git.js +171 -21
  250. package/dist/sources/providers/index.js +3 -0
  251. package/dist/sources/providers/install-types.js +3 -13
  252. package/dist/sources/providers/npm.js +3 -4
  253. package/dist/sources/providers/provider-utils.js +3 -0
  254. package/dist/sources/providers/sync-from-ref.js +3 -11
  255. package/dist/sources/providers/tar-utils.js +3 -0
  256. package/dist/sources/providers/website.js +18 -22
  257. package/dist/sources/resolve.js +3 -0
  258. package/dist/sources/types.js +3 -0
  259. package/dist/sources/website-ingest.js +3 -0
  260. package/dist/tasks/backends/cron.js +3 -0
  261. package/dist/tasks/backends/exec-utils.js +3 -0
  262. package/dist/tasks/backends/index.js +3 -11
  263. package/dist/tasks/backends/launchd.js +3 -0
  264. package/dist/tasks/backends/schtasks.js +3 -0
  265. package/dist/tasks/parser.js +51 -38
  266. package/dist/tasks/resolveAkmBin.js +3 -0
  267. package/dist/tasks/runner.js +35 -9
  268. package/dist/tasks/schedule.js +20 -1
  269. package/dist/tasks/schema.js +5 -3
  270. package/dist/tasks/validator.js +6 -3
  271. package/dist/version.js +3 -0
  272. package/dist/wiki/wiki-templates.js +3 -0
  273. package/dist/wiki/wiki.js +3 -0
  274. package/dist/workflows/authoring.js +3 -0
  275. package/dist/workflows/cli.js +3 -0
  276. package/dist/workflows/db.js +140 -10
  277. package/dist/workflows/document-cache.js +3 -10
  278. package/dist/workflows/parser.js +3 -0
  279. package/dist/workflows/renderer.js +3 -0
  280. package/dist/workflows/runs.js +18 -1
  281. package/dist/workflows/schema.js +3 -0
  282. package/dist/workflows/scope-key.js +3 -0
  283. package/dist/workflows/validator.js +5 -9
  284. package/docs/README.md +7 -2
  285. package/docs/data-and-telemetry.md +225 -0
  286. package/docs/migration/release-notes/0.7.5.md +2 -2
  287. package/docs/migration/release-notes/0.8.0.md +57 -5
  288. package/docs/migration/v0.7-to-v0.8.md +1378 -0
  289. package/package.json +28 -11
  290. package/.github/LICENSE +0 -374
  291. package/dist/commands/install-audit.js +0 -385
  292. package/dist/commands/vault.js +0 -307
  293. package/dist/indexer/match-contributors.js +0 -141
  294. package/dist/integrations/agent/pipeline.js +0 -39
  295. package/dist/integrations/agent/runners.js +0 -31
@@ -1,52 +1,258 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
1
4
  import fs from "node:fs";
2
5
  import os from "node:os";
3
6
  import path from "node:path";
4
- function getOpenCodeLogDir() {
7
+ import { extractInlineRefMentions } from "../inline-refs";
8
+ function getOpenCodeBaseDir() {
5
9
  if (process.platform === "darwin") {
6
10
  return path.join(os.homedir(), "Library", "Application Support", "opencode");
7
11
  }
8
12
  return path.join(os.homedir(), ".local", "share", "opencode");
9
13
  }
14
+ /**
15
+ * Opencode storage layout (observed 2026-05):
16
+ * <base>/storage/session/<projectId>/<sessionId>.json — metadata
17
+ * <base>/storage/message/<sessionId>/<messageId>.json — one per message
18
+ *
19
+ * Older builds wrote logs directly into `<base>/log/` and `<base>/*.log`;
20
+ * those are still scanned by {@link OpenCodeProvider.readEvents} for
21
+ * backward compatibility with the existing failure-pattern aggregator.
22
+ */
10
23
  export class OpenCodeProvider {
11
24
  name = "opencode";
12
- #logDir = getOpenCodeLogDir();
25
+ #baseDir = getOpenCodeBaseDir();
13
26
  isAvailable() {
14
- return fs.existsSync(this.#logDir);
27
+ return fs.existsSync(this.#baseDir);
15
28
  }
16
29
  *readEvents(input) {
30
+ // Legacy behavior: stream raw log lines from the top-level dir and `log/`
31
+ // subdirectory. Kept to keep `getExecutionLogCandidates` working without
32
+ // a coordinated change to its caller. New code should use
33
+ // {@link listSessions} + {@link readSession} instead.
34
+ const candidates = [this.#baseDir, path.join(this.#baseDir, "log")];
35
+ for (const dir of candidates) {
36
+ if (!fs.existsSync(dir))
37
+ continue;
38
+ try {
39
+ for (const file of fs.readdirSync(dir)) {
40
+ const full = path.join(dir, file);
41
+ let stat;
42
+ try {
43
+ stat = fs.statSync(full);
44
+ }
45
+ catch {
46
+ continue;
47
+ }
48
+ if (!stat.isFile())
49
+ continue;
50
+ if (stat.mtimeMs < input.sinceMs)
51
+ continue;
52
+ if (!file.endsWith(".json") && !file.endsWith(".jsonl") && !file.endsWith(".log"))
53
+ continue;
54
+ const content = fs.readFileSync(full, "utf8");
55
+ const lines = content.includes("\n") ? content.split("\n") : [content];
56
+ for (const line of lines) {
57
+ try {
58
+ const entry = JSON.parse(line);
59
+ const text = entry?.content ?? entry?.message ?? entry?.text ?? "";
60
+ if (typeof text !== "string" || text.length < 10)
61
+ continue;
62
+ yield {
63
+ harness: this.name,
64
+ text,
65
+ ts: typeof entry?.timestamp === "number" ? entry.timestamp : stat.mtimeMs,
66
+ sessionId: typeof entry?.sessionId === "string" ? entry.sessionId : undefined,
67
+ role: typeof entry?.role === "string" ? entry.role : "unknown",
68
+ filePath: full,
69
+ };
70
+ }
71
+ catch {
72
+ // skip malformed
73
+ }
74
+ }
75
+ }
76
+ }
77
+ catch {
78
+ // unreadable dir — skip
79
+ }
80
+ }
81
+ }
82
+ listSessions(input = {}) {
83
+ const base = input.location ?? this.#baseDir;
84
+ const sinceMs = input.sinceMs ?? 0;
85
+ const sessionRoot = path.join(base, "storage", "session");
86
+ if (!fs.existsSync(sessionRoot))
87
+ return [];
88
+ const summaries = [];
17
89
  try {
18
- for (const file of fs.readdirSync(this.#logDir)) {
19
- const full = path.join(this.#logDir, file);
20
- const stat = fs.statSync(full);
21
- if (stat.mtimeMs < input.sinceMs)
90
+ for (const projectId of fs.readdirSync(sessionRoot)) {
91
+ const projectDir = path.join(sessionRoot, projectId);
92
+ let pstat;
93
+ try {
94
+ pstat = fs.statSync(projectDir);
95
+ }
96
+ catch {
22
97
  continue;
23
- if (!file.endsWith(".json") && !file.endsWith(".jsonl") && !file.endsWith(".log"))
98
+ }
99
+ if (!pstat.isDirectory())
24
100
  continue;
25
- const content = fs.readFileSync(full, "utf8");
26
- const lines = content.includes("\n") ? content.split("\n") : [content];
27
- for (const line of lines) {
101
+ for (const file of fs.readdirSync(projectDir)) {
102
+ if (!file.endsWith(".json"))
103
+ continue;
104
+ const filePath = path.join(projectDir, file);
105
+ let stat;
28
106
  try {
29
- const entry = JSON.parse(line);
30
- const text = entry?.content ?? entry?.message ?? entry?.text ?? "";
31
- if (typeof text !== "string" || text.length < 10)
32
- continue;
33
- yield {
34
- harness: this.name,
35
- text,
36
- ts: typeof entry?.timestamp === "number" ? entry.timestamp : stat.mtimeMs,
37
- sessionId: typeof entry?.sessionId === "string" ? entry.sessionId : undefined,
38
- role: typeof entry?.role === "string" ? entry.role : "unknown",
39
- filePath: full,
40
- };
107
+ stat = fs.statSync(filePath);
41
108
  }
42
109
  catch {
43
- // skip malformed
110
+ continue;
111
+ }
112
+ if (stat.mtimeMs < sinceMs)
113
+ continue;
114
+ let meta;
115
+ try {
116
+ meta = JSON.parse(fs.readFileSync(filePath, "utf8"));
44
117
  }
118
+ catch {
119
+ continue;
120
+ }
121
+ const sessionId = typeof meta?.id === "string" ? meta.id : path.basename(file, ".json");
122
+ const time = meta?.time ?? undefined;
123
+ const startedAt = typeof time?.created === "number" ? time.created : stat.ctimeMs;
124
+ const endedAt = typeof time?.updated === "number" ? time.updated : stat.mtimeMs;
125
+ const title = typeof meta?.title === "string" ? meta.title : undefined;
126
+ const projectHint = typeof meta?.directory === "string" ? meta.directory : projectId;
127
+ summaries.push({
128
+ harness: this.name,
129
+ sessionId,
130
+ filePath,
131
+ startedAt,
132
+ endedAt,
133
+ projectHint,
134
+ ...(title ? { title } : {}),
135
+ });
45
136
  }
46
137
  }
47
138
  }
48
139
  catch {
49
- return;
140
+ // unreadable session root — return what we have
141
+ }
142
+ return summaries.sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0));
143
+ }
144
+ readSession(ref) {
145
+ let meta = {};
146
+ try {
147
+ meta = JSON.parse(fs.readFileSync(ref.filePath, "utf8"));
148
+ }
149
+ catch {
150
+ // metadata missing — proceed with empty defaults
151
+ }
152
+ const time = meta.time ?? undefined;
153
+ const startedAt = typeof time?.created === "number" ? time.created : undefined;
154
+ const endedAt = typeof time?.updated === "number" ? time.updated : undefined;
155
+ const title = typeof meta.title === "string" ? meta.title : undefined;
156
+ const projectHint = typeof meta.directory === "string" ? meta.directory : undefined;
157
+ const events = [];
158
+ const inlineRefs = [];
159
+ // Resolve message directory: <baseDir>/storage/message/<sessionId>/
160
+ const inferredBase = this.#inferBaseFromSessionPath(ref.filePath) ?? this.#baseDir;
161
+ const msgDir = path.join(inferredBase, "storage", "message", ref.sessionId);
162
+ if (fs.existsSync(msgDir)) {
163
+ try {
164
+ const files = fs.readdirSync(msgDir).filter((f) => f.endsWith(".json"));
165
+ for (const file of files) {
166
+ const full = path.join(msgDir, file);
167
+ let msg;
168
+ try {
169
+ msg = JSON.parse(fs.readFileSync(full, "utf8"));
170
+ }
171
+ catch {
172
+ continue;
173
+ }
174
+ if (!msg)
175
+ continue;
176
+ const evt = this.#messageToEvent(msg, ref.sessionId, full);
177
+ if (evt) {
178
+ events.push(evt);
179
+ inlineRefs.push(...extractInlineRefMentions(evt.text, evt.ts));
180
+ }
181
+ }
182
+ }
183
+ catch {
184
+ // unreadable msg dir — skip
185
+ }
186
+ }
187
+ events.sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
188
+ return {
189
+ ref: {
190
+ harness: this.name,
191
+ sessionId: ref.sessionId,
192
+ filePath: ref.filePath,
193
+ ...(startedAt !== undefined ? { startedAt } : {}),
194
+ ...(endedAt !== undefined ? { endedAt } : {}),
195
+ ...(projectHint ? { projectHint } : {}),
196
+ ...(title ? { title } : {}),
197
+ },
198
+ events,
199
+ inlineRefs,
200
+ };
201
+ }
202
+ /**
203
+ * Derive opencode base dir from a session metadata file path so a caller
204
+ * passing a custom `--location` can still find the message dir.
205
+ * Layout: `<base>/storage/session/<projectId>/<id>.json` → base.
206
+ */
207
+ #inferBaseFromSessionPath(filePath) {
208
+ // Walk up: <id>.json → <projectId> → session → storage → <base>
209
+ const dir = path.dirname(filePath);
210
+ const parts = dir.split(path.sep);
211
+ if (parts.length < 3)
212
+ return undefined;
213
+ const last = parts[parts.length - 1];
214
+ const sndLast = parts[parts.length - 2];
215
+ const thirdLast = parts[parts.length - 3];
216
+ if (sndLast !== "session" || thirdLast !== "storage" || !last)
217
+ return undefined;
218
+ return parts.slice(0, parts.length - 3).join(path.sep);
219
+ }
220
+ #messageToEvent(msg, sessionId, filePath) {
221
+ const time = msg.time ?? undefined;
222
+ const ts = typeof time?.created === "number" ? time.created : typeof msg.timestamp === "number" ? msg.timestamp : 0;
223
+ const role = typeof msg.role === "string" ? msg.role : "unknown";
224
+ // Opencode message bodies live in summary.title / summary.diffs[].before/after /
225
+ // parts (referenced from storage/part/<msg-id>/). For listing+extraction
226
+ // purposes the summary block is sufficient — it's what the platform itself
227
+ // surfaces as the message preview.
228
+ const summary = msg.summary;
229
+ const parts = [];
230
+ if (typeof summary?.title === "string")
231
+ parts.push(summary.title);
232
+ if (Array.isArray(summary?.parts)) {
233
+ for (const p of summary.parts) {
234
+ if (typeof p === "string")
235
+ parts.push(p);
236
+ else if (p && typeof p === "object") {
237
+ const text = p.text;
238
+ if (typeof text === "string")
239
+ parts.push(text);
240
+ }
241
+ }
50
242
  }
243
+ // content field for some opencode versions
244
+ if (typeof msg.content === "string")
245
+ parts.push(msg.content);
246
+ const text = parts.join("\n").trim();
247
+ if (text.length < 1)
248
+ return undefined;
249
+ return {
250
+ harness: this.name,
251
+ text,
252
+ ts: ts || undefined,
253
+ sessionId,
254
+ role,
255
+ filePath,
256
+ };
51
257
  }
52
258
  }
@@ -1 +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/.
1
4
  export {};
@@ -1,37 +1,24 @@
1
- /**
2
- * Unified AI call adapter: prefers `config.agent` (agent CLI shell-out),
3
- * falls back to `config.llm` (HTTP chat-completions).
4
- *
5
- * NOT for use by background indexer passes — those call `chatCompletion`
6
- * directly to avoid the agent-CLI overhead and to stay on the HTTP path that
7
- * the indexer was designed around.
8
- */
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";
9
5
  import { warn } from "../core/warn";
10
6
  import { resolveAgentProfile, runAgent } from "../integrations/agent";
11
7
  import { chatCompletion } from "./client";
12
8
  /**
13
- * Unified AI call: prefers `config.agent` (agent CLI), falls back to
14
- * `config.llm` (HTTP). When neither is configured, returns a structured
9
+ * Unified AI call: prefers the default agent profile, falls back to the
10
+ * default LLM profile. When neither is configured, returns a structured
15
11
  * error pointing the user at `akm setup`.
16
- *
17
- * NOT for use by background indexer passes — those call `chatCompletion`
18
- * directly.
19
12
  */
20
13
  export async function callAi(config, prompt, opts = {}) {
21
- if (config.agent) {
14
+ const defaultAgentName = config.defaults?.agent;
15
+ if (defaultAgentName) {
22
16
  try {
23
- const defaultName = config.agent.default;
24
- if (!defaultName) {
25
- return {
26
- ok: false,
27
- error: "No default agent profile configured. Set `agent.default` in config.json or run `akm setup`.",
28
- };
29
- }
30
- const profile = resolveAgentProfile(defaultName, config.agent.profiles?.[defaultName]);
17
+ const profile = resolveAgentProfile(defaultAgentName, config.profiles?.agent?.[defaultAgentName]);
31
18
  if (!profile) {
32
19
  return {
33
20
  ok: false,
34
- error: `Agent profile "${defaultName}" is not built-in and has no \`bin\` override.`,
21
+ error: `Agent profile "${defaultAgentName}" is not built-in and has no \`bin\` override.`,
35
22
  };
36
23
  }
37
24
  const result = await runAgent(profile, prompt, {
@@ -47,7 +34,8 @@ export async function callAi(config, prompt, opts = {}) {
47
34
  return { ok: false, error: String(e) };
48
35
  }
49
36
  }
50
- if (config.llm) {
37
+ const llmConfig = getDefaultLlmConfig(config);
38
+ if (llmConfig) {
51
39
  if (opts.draftFilePath) {
52
40
  warn("[akm] No agent CLI configured — falling back to LLM API. " +
53
41
  "File-write contract unavailable; expecting JSON in stdout. " +
@@ -58,7 +46,7 @@ export async function callAi(config, prompt, opts = {}) {
58
46
  messages.push({ role: "system", content: opts.systemPrompt });
59
47
  messages.push({ role: "user", content: prompt });
60
48
  try {
61
- const content = await chatCompletion(config.llm, messages, {
49
+ const content = await chatCompletion(llmConfig, messages, {
62
50
  ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
63
51
  });
64
52
  return { ok: true, content, path: "llm-http" };
@@ -69,6 +57,6 @@ export async function callAi(config, prompt, opts = {}) {
69
57
  }
70
58
  return {
71
59
  ok: false,
72
- error: "No AI connection configured. Run `akm setup` or set `agent` or `llm` in your config.",
60
+ error: "No AI connection configured. Run `akm setup` or set `defaults.agent`/`defaults.llm`.",
73
61
  };
74
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,7 @@
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";
11
15
  import { escapeJsonStringControls, parseJsonResponse, stripCodeFences, stripThinkBlocks } from "../core/parse";
12
16
  // Re-export shared parse utilities so existing importers of `client.ts` continue
13
17
  // to resolve `parseJsonResponse` and `parseEmbeddedJsonResponse` from this module.
@@ -55,13 +59,17 @@ export class LlmCallError extends Error {
55
59
  export async function chatCompletion(config, messages, options) {
56
60
  const timeoutMs = options?.timeoutMs ?? config.timeoutMs ?? 120_000;
57
61
  const headers = { "Content-Type": "application/json" };
58
- if (config.apiKey) {
59
- headers.Authorization = `Bearer ${config.apiKey}`;
62
+ const resolvedKey = resolveSecret(config.apiKey);
63
+ if (resolvedKey) {
64
+ headers.Authorization = `Bearer ${resolvedKey}`;
60
65
  }
61
66
  // Only include max_tokens when explicitly set. The model/API knows its own
62
67
  // limits; a hardcoded default creates silent truncation failures when the
63
68
  // guess is wrong. Users who need a cap can set llm.maxTokens in config.
64
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
+ : {};
65
73
  let response;
66
74
  try {
67
75
  response = await fetchWithTimeout(config.endpoint, {
@@ -72,6 +80,12 @@ export async function chatCompletion(config, messages, options) {
72
80
  messages,
73
81
  temperature: options?.temperature ?? config.temperature ?? 0.3,
74
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
+ : {}),
75
89
  ...config.extraParams,
76
90
  }),
77
91
  }, timeoutMs, options?.signal);
@@ -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
  }