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,152 @@
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
+ /** Default cap for any single event's text length. Head+tail summary applies above this. */
5
+ export const DEFAULT_MAX_EVENT_LENGTH = 2000;
6
+ /**
7
+ * Default cap on total transcript characters fed to the LLM. Chosen for a
8
+ * 32K-token context model with room for the prompt scaffolding (~3K chars)
9
+ * and JSON output (~4K chars). Adjust via {@link PreFilterOptions.maxTotalChars}
10
+ * when targeting larger-context models.
11
+ */
12
+ export const DEFAULT_MAX_TOTAL_CHARS = 80_000;
13
+ /**
14
+ * `akm` subcommands that are read-only / introspective — their invocations
15
+ * are operational noise, not engineering signal. Mutating commands (remember,
16
+ * feedback, accept, reject, extract, import, save, ...) are kept.
17
+ */
18
+ export const DEFAULT_AKM_READONLY_OPS = new Set([
19
+ "show",
20
+ "search",
21
+ "curate",
22
+ "history",
23
+ "info",
24
+ "hints",
25
+ "help",
26
+ "list",
27
+ "completions",
28
+ "lessons",
29
+ "graph",
30
+ "db",
31
+ "events",
32
+ "config",
33
+ "health",
34
+ ]);
35
+ /**
36
+ * Regex patterns that identify post-compact / activity-log noise. Conservative
37
+ * — only matches text that's clearly transcript pollution, not engineering
38
+ * content that happens to contain similar words.
39
+ */
40
+ const NOISE_PATTERNS = [
41
+ // Claude Code injects this caveat block before every bash invocation result.
42
+ /<local-command-caveat>/i,
43
+ // Post-compact dumps embed analysis/summary XML blocks pasted from prior context.
44
+ /<analysis>[\s\S]{200,}<\/analysis>/i,
45
+ /<summary>[\s\S]{200,}<\/summary>/i,
46
+ // System reminders the harness injects every few turns — never carry signal.
47
+ /<system-reminder>/i,
48
+ // Opencode tool-event aggregate dumps look like repeated `akm_search unknown` blocks.
49
+ /^(##\s+\d+.*akm_search unknown\s*\n){3,}/im,
50
+ ];
51
+ /**
52
+ * Apply the drop+truncate rules to a single event. Returns `undefined` when
53
+ * the event should be dropped, or the (possibly truncated) event when kept.
54
+ * The third return tracks why dropped, for stats.
55
+ */
56
+ function classifyEvent(event, akmReadOnlyOps, maxLen) {
57
+ const text = event.text ?? "";
58
+ if (text.trim().length < 10)
59
+ return { keep: false, reason: "too-short" };
60
+ // Rule 1: read-only akm meta-ops. The flattened tool_use shape from the
61
+ // claude-code provider looks like: `[tool:Bash] akm show knowledge:foo`.
62
+ // Match the verb directly after `akm ` (with or without the `[tool:...]`
63
+ // prefix, since some platforms surface the command differently).
64
+ const akmCallMatch = text.match(/\bakm\s+(\w[\w-]*)\b/);
65
+ if (akmCallMatch) {
66
+ const op = (akmCallMatch[1] ?? "").toLowerCase();
67
+ if (akmReadOnlyOps.has(op)) {
68
+ return { keep: false, reason: `akm-readonly-${op}` };
69
+ }
70
+ }
71
+ // Rule 2-5: noise patterns
72
+ for (const pattern of NOISE_PATTERNS) {
73
+ if (pattern.test(text)) {
74
+ return { keep: false, reason: `noise-pattern-${pattern.source.slice(0, 24)}` };
75
+ }
76
+ }
77
+ // Rule 6: bare system events that are pure boilerplate (no engineering content).
78
+ // Heuristic: role=system AND short, OR role=system AND just contains `caveat`/`reminder` markers.
79
+ if (event.role === "system" && (text.length < 200 || /caveat|reminder/i.test(text))) {
80
+ return { keep: false, reason: "system-boilerplate" };
81
+ }
82
+ // Truncate long events to head + tail summary.
83
+ if (text.length > maxLen) {
84
+ const headLen = Math.floor(maxLen * 0.7);
85
+ const tailLen = maxLen - headLen - 32; // 32 chars for the marker
86
+ const truncated = text.slice(0, headLen) +
87
+ `\n... [truncated ${text.length - headLen - tailLen} chars] ...\n` +
88
+ text.slice(text.length - tailLen);
89
+ return { keep: true, event: { ...event, text: truncated }, truncated: true };
90
+ }
91
+ return { keep: true, event, truncated: false };
92
+ }
93
+ export function preFilterSession(data, options = {}) {
94
+ const akmReadOnlyOps = options.akmReadOnlyOps ?? DEFAULT_AKM_READONLY_OPS;
95
+ const maxLen = options.maxEventTextLength ?? DEFAULT_MAX_EVENT_LENGTH;
96
+ const maxTotalChars = options.maxTotalChars ?? DEFAULT_MAX_TOTAL_CHARS;
97
+ const droppedByRule = {};
98
+ const kept = [];
99
+ let truncatedCount = 0;
100
+ const candidates = [];
101
+ for (const event of data.events) {
102
+ const verdict = classifyEvent(event, akmReadOnlyOps, maxLen);
103
+ if (!verdict.keep) {
104
+ droppedByRule[verdict.reason] = (droppedByRule[verdict.reason] ?? 0) + 1;
105
+ continue;
106
+ }
107
+ candidates.push({
108
+ event: verdict.event,
109
+ truncated: verdict.truncated,
110
+ chars: verdict.event.text.length,
111
+ });
112
+ }
113
+ // Second pass: total-budget cap. Walk from the END (most recent first) and
114
+ // accept events until the budget is exhausted. The remaining (head) events
115
+ // are dropped — insight typically emerges later in a session, so this
116
+ // recency-bias is the cheapest sampling heuristic that respects context
117
+ // limits. Maintains original timestamp order in the output.
118
+ let totalChars = 0;
119
+ let budgetDroppedCount = 0;
120
+ const keptIdxFromTail = [];
121
+ for (let i = candidates.length - 1; i >= 0; i--) {
122
+ const c = candidates[i];
123
+ if (!c)
124
+ continue;
125
+ if (totalChars + c.chars > maxTotalChars && keptIdxFromTail.length > 0) {
126
+ budgetDroppedCount += 1;
127
+ continue;
128
+ }
129
+ keptIdxFromTail.push(i);
130
+ totalChars += c.chars;
131
+ }
132
+ keptIdxFromTail.reverse(); // restore timestamp order
133
+ for (const idx of keptIdxFromTail) {
134
+ const c = candidates[idx];
135
+ if (!c)
136
+ continue;
137
+ kept.push(c.event);
138
+ if (c.truncated)
139
+ truncatedCount += 1;
140
+ }
141
+ return {
142
+ events: kept,
143
+ stats: {
144
+ inputCount: data.events.length,
145
+ outputCount: kept.length,
146
+ droppedByRule,
147
+ truncatedCount,
148
+ totalChars,
149
+ budgetDroppedCount,
150
+ },
151
+ };
152
+ }
@@ -0,0 +1,282 @@
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 fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { extractInlineRefMentions } from "../inline-refs";
8
+ const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
9
+ /**
10
+ * Parse a single Claude Code JSONL event into a normalized {@link SessionEvent}.
11
+ * Returns `undefined` for events that don't carry textual content (file
12
+ * snapshots, attachments, queue metadata). Tool calls are flattened from the
13
+ * `message.content` array into a stable text representation so downstream
14
+ * consumers don't need to know the Anthropic-tool-call shape.
15
+ */
16
+ function parseClaudeEvent(entry, sessionId, filePath, fallbackTsMs) {
17
+ if (!entry || typeof entry !== "object")
18
+ return undefined;
19
+ const e = entry;
20
+ const tsRaw = e.timestamp;
21
+ const ts = typeof tsRaw === "number" ? tsRaw : typeof tsRaw === "string" ? Date.parse(tsRaw) || fallbackTsMs : fallbackTsMs;
22
+ const message = e.message ?? undefined;
23
+ const role = typeof message?.role === "string"
24
+ ? message.role
25
+ : (e.type ?? "unknown");
26
+ const content = message?.content;
27
+ let text = "";
28
+ if (typeof content === "string") {
29
+ text = content;
30
+ }
31
+ else if (Array.isArray(content)) {
32
+ // Assistant messages: array of content blocks. Flatten text/thinking/tool_use
33
+ // into a stable representation. tool_use entries become `[tool: <name>] <input>`
34
+ // so the inline-ref scanner can detect `akm remember` / `akm feedback` calls.
35
+ const parts = [];
36
+ for (const block of content) {
37
+ if (!block || typeof block !== "object")
38
+ continue;
39
+ const b = block;
40
+ if (b.type === "text" && typeof b.text === "string")
41
+ parts.push(b.text);
42
+ else if (b.type === "thinking" && typeof b.thinking === "string")
43
+ parts.push(b.thinking);
44
+ else if (b.type === "tool_use") {
45
+ const toolName = typeof b.name === "string" ? b.name : "tool";
46
+ // For shell-like tools, surface the `command` field directly so
47
+ // inline-ref detection can match `akm remember "..."` without
48
+ // JSON-quote escaping mangling the regex.
49
+ const inputObj = b.input;
50
+ let inputText = "";
51
+ if (inputObj && typeof inputObj === "object") {
52
+ const cmd = inputObj.command;
53
+ inputText = typeof cmd === "string" ? cmd : JSON.stringify(inputObj);
54
+ }
55
+ else if (typeof inputObj === "string") {
56
+ inputText = inputObj;
57
+ }
58
+ parts.push(`[tool:${toolName}] ${inputText}`);
59
+ }
60
+ else if (b.type === "tool_result") {
61
+ const out = typeof b.content === "string" ? b.content : JSON.stringify(b.content ?? "");
62
+ parts.push(`[tool_result] ${out}`);
63
+ }
64
+ }
65
+ text = parts.join("\n");
66
+ }
67
+ if (!text || text.length < 1)
68
+ return undefined;
69
+ return {
70
+ harness: "claude-code",
71
+ text,
72
+ ts,
73
+ sessionId,
74
+ role,
75
+ filePath,
76
+ };
77
+ }
78
+ export class ClaudeCodeProvider {
79
+ name = "claude-code";
80
+ isAvailable() {
81
+ return fs.existsSync(CLAUDE_PROJECTS_DIR);
82
+ }
83
+ *readEvents(input) {
84
+ try {
85
+ for (const jsonlPath of this.#walkJsonl(CLAUDE_PROJECTS_DIR)) {
86
+ const stat = fs.statSync(jsonlPath);
87
+ if (stat.mtimeMs < input.sinceMs)
88
+ continue;
89
+ const lines = fs.readFileSync(jsonlPath, "utf8").split("\n").filter(Boolean);
90
+ for (const line of lines) {
91
+ try {
92
+ const entry = JSON.parse(line);
93
+ const text = entry?.message?.content ?? entry?.content ?? "";
94
+ if (typeof text !== "string" || text.length < 10)
95
+ continue;
96
+ yield {
97
+ harness: this.name,
98
+ text,
99
+ ts: typeof entry?.timestamp === "number" ? entry.timestamp : stat.mtimeMs,
100
+ sessionId: typeof entry?.session_id === "string" ? entry.session_id : undefined,
101
+ role: typeof entry?.role === "string" ? entry.role : "unknown",
102
+ filePath: jsonlPath,
103
+ };
104
+ }
105
+ catch {
106
+ // skip malformed lines
107
+ }
108
+ }
109
+ }
110
+ }
111
+ catch {
112
+ return;
113
+ }
114
+ }
115
+ listSessions(input = {}) {
116
+ const root = input.location ?? CLAUDE_PROJECTS_DIR;
117
+ const sinceMs = input.sinceMs ?? 0;
118
+ const summaries = [];
119
+ try {
120
+ for (const jsonlPath of this.#walkJsonl(root)) {
121
+ let stat;
122
+ try {
123
+ stat = fs.statSync(jsonlPath);
124
+ }
125
+ catch {
126
+ continue;
127
+ }
128
+ if (stat.mtimeMs < sinceMs)
129
+ continue;
130
+ const sessionId = path.basename(jsonlPath, ".jsonl");
131
+ const projectHint = path.basename(path.dirname(jsonlPath));
132
+ // Peek first + last non-empty line to derive start/end timestamps and
133
+ // title. Reading the whole file would be wasteful for listing.
134
+ const peek = this.#peekJsonl(jsonlPath);
135
+ summaries.push({
136
+ harness: this.name,
137
+ sessionId,
138
+ filePath: jsonlPath,
139
+ startedAt: peek.firstTsMs ?? stat.ctimeMs,
140
+ endedAt: peek.lastTsMs ?? stat.mtimeMs,
141
+ projectHint,
142
+ ...(peek.title ? { title: peek.title } : {}),
143
+ });
144
+ }
145
+ }
146
+ catch {
147
+ // Root missing or unreadable — return what we have.
148
+ }
149
+ return summaries.sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0));
150
+ }
151
+ readSession(ref) {
152
+ const stat = fs.statSync(ref.filePath);
153
+ const lines = fs.readFileSync(ref.filePath, "utf8").split("\n").filter(Boolean);
154
+ const events = [];
155
+ const inlineRefs = [];
156
+ let title;
157
+ let firstTsMs;
158
+ let lastTsMs;
159
+ const projectHint = path.basename(path.dirname(ref.filePath));
160
+ for (const line of lines) {
161
+ let entry;
162
+ try {
163
+ entry = JSON.parse(line);
164
+ }
165
+ catch {
166
+ continue;
167
+ }
168
+ if (!entry)
169
+ continue;
170
+ if (entry.type === "custom-title" && typeof entry.customTitle === "string") {
171
+ title = entry.customTitle;
172
+ continue;
173
+ }
174
+ const parsed = parseClaudeEvent(entry, ref.sessionId, ref.filePath, stat.mtimeMs);
175
+ if (!parsed)
176
+ continue;
177
+ events.push(parsed);
178
+ if (firstTsMs === undefined || (parsed.ts ?? 0) < firstTsMs)
179
+ firstTsMs = parsed.ts;
180
+ if (lastTsMs === undefined || (parsed.ts ?? 0) > lastTsMs)
181
+ lastTsMs = parsed.ts;
182
+ // Extract inline akm-remember/feedback invocations from this event's text.
183
+ inlineRefs.push(...extractInlineRefMentions(parsed.text, parsed.ts));
184
+ }
185
+ return {
186
+ ref: {
187
+ harness: this.name,
188
+ sessionId: ref.sessionId,
189
+ filePath: ref.filePath,
190
+ startedAt: firstTsMs ?? stat.ctimeMs,
191
+ endedAt: lastTsMs ?? stat.mtimeMs,
192
+ projectHint,
193
+ ...(title ? { title } : {}),
194
+ },
195
+ events,
196
+ inlineRefs,
197
+ };
198
+ }
199
+ /**
200
+ * Cheap metadata peek — reads the first ~4KB to grab the `custom-title`
201
+ * event (always early in the file) and the first event timestamp, then
202
+ * reads the tail (~4KB) for the last timestamp. Avoids slurping multi-MB
203
+ * session files during `listSessions`.
204
+ */
205
+ #peekJsonl(filePath) {
206
+ const result = {};
207
+ try {
208
+ const fd = fs.openSync(filePath, "r");
209
+ try {
210
+ const stat = fs.fstatSync(fd);
211
+ const headSize = Math.min(stat.size, 4096);
212
+ const head = Buffer.alloc(headSize);
213
+ fs.readSync(fd, head, 0, headSize, 0);
214
+ const headLines = head.toString("utf8").split("\n").filter(Boolean);
215
+ // Walk head: track title, first timestamp, and (if file fits in head)
216
+ // also the last timestamp seen — saves a tail read for small files.
217
+ for (const line of headLines) {
218
+ try {
219
+ const e = JSON.parse(line);
220
+ if (e.type === "custom-title" && typeof e.customTitle === "string") {
221
+ result.title = e.customTitle;
222
+ }
223
+ if (typeof e.timestamp === "string") {
224
+ const t = Date.parse(e.timestamp);
225
+ if (!Number.isNaN(t)) {
226
+ if (result.firstTsMs === undefined)
227
+ result.firstTsMs = t;
228
+ result.lastTsMs = t;
229
+ }
230
+ }
231
+ }
232
+ catch {
233
+ // partial line at buffer boundary — fine, skip
234
+ }
235
+ }
236
+ // Large-file tail read overrides lastTsMs with a value closer to EOF.
237
+ if (stat.size > 4096) {
238
+ const tailSize = Math.min(stat.size, 4096);
239
+ const tail = Buffer.alloc(tailSize);
240
+ fs.readSync(fd, tail, 0, tailSize, stat.size - tailSize);
241
+ const tailLines = tail.toString("utf8").split("\n").filter(Boolean);
242
+ for (let i = tailLines.length - 1; i >= 0; i--) {
243
+ try {
244
+ const e = JSON.parse(tailLines[i] ?? "");
245
+ if (typeof e.timestamp === "string") {
246
+ const t = Date.parse(e.timestamp);
247
+ if (!Number.isNaN(t)) {
248
+ result.lastTsMs = t;
249
+ break;
250
+ }
251
+ }
252
+ }
253
+ catch {
254
+ // skip partial lines from buffer boundary
255
+ }
256
+ }
257
+ }
258
+ }
259
+ finally {
260
+ fs.closeSync(fd);
261
+ }
262
+ }
263
+ catch {
264
+ // unreadable / vanished file — caller falls back to stat times
265
+ }
266
+ return result;
267
+ }
268
+ *#walkJsonl(dir) {
269
+ try {
270
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
271
+ const full = path.join(dir, entry.name);
272
+ if (entry.isDirectory())
273
+ yield* this.#walkJsonl(full);
274
+ else if (entry.name.endsWith(".jsonl"))
275
+ yield full;
276
+ }
277
+ }
278
+ catch {
279
+ // permission errors etc.
280
+ }
281
+ }
282
+ }
@@ -0,0 +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/.
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { extractInlineRefMentions } from "../inline-refs";
8
+ function getOpenCodeBaseDir() {
9
+ if (process.platform === "darwin") {
10
+ return path.join(os.homedir(), "Library", "Application Support", "opencode");
11
+ }
12
+ return path.join(os.homedir(), ".local", "share", "opencode");
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
+ */
23
+ export class OpenCodeProvider {
24
+ name = "opencode";
25
+ #baseDir = getOpenCodeBaseDir();
26
+ isAvailable() {
27
+ return fs.existsSync(this.#baseDir);
28
+ }
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 = [];
89
+ try {
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 {
97
+ continue;
98
+ }
99
+ if (!pstat.isDirectory())
100
+ continue;
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;
106
+ try {
107
+ stat = fs.statSync(filePath);
108
+ }
109
+ catch {
110
+ continue;
111
+ }
112
+ if (stat.mtimeMs < sinceMs)
113
+ continue;
114
+ let meta;
115
+ try {
116
+ meta = JSON.parse(fs.readFileSync(filePath, "utf8"));
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
+ });
136
+ }
137
+ }
138
+ }
139
+ catch {
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
+ }
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
+ };
257
+ }
258
+ }