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,6 +1,11 @@
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 path from "node:path";
3
6
  import { writeFileAtomic } from "../core/common";
7
+ import { rethrowIfTestIsolationError } from "../core/errors";
8
+ import { probeLock, releaseLock, tryAcquireLockSync } from "../core/file-lock";
4
9
  import { getDataDir } from "../core/paths";
5
10
  // ── Paths ───────────────────────────────────────────────────────────────────
6
11
  const LOCKFILE_NAME = "akm.lock";
@@ -16,64 +21,26 @@ function getLockSentinelPath() {
16
21
  }
17
22
  async function acquireLockSentinel() {
18
23
  const sentinelPath = getLockSentinelPath();
19
- // Ensure the directory exists before attempting to create the sentinel
24
+ // Ensure the directory exists before attempting to create the sentinel.
20
25
  fs.mkdirSync(path.dirname(sentinelPath), { recursive: true });
21
26
  for (let attempt = 0; attempt < LOCK_MAX_RETRIES; attempt++) {
22
- try {
23
- fs.writeFileSync(sentinelPath, String(process.pid), { flag: "wx" });
24
- return true; // Sentinel created — we own the lock
27
+ if (tryAcquireLockSync(sentinelPath, String(process.pid))) {
28
+ return true; // Sentinel created — we own the lock.
25
29
  }
26
- catch (err) {
27
- if (err.code !== "EEXIST")
28
- throw err;
29
- // Check for stale lock — if the owning PID is no longer running, reclaim it
30
- if (tryReclaimStaleSentinel(sentinelPath)) {
31
- continue; // Sentinel removed — retry immediately
32
- }
33
- // Another process holds the lock — wait briefly before retrying
34
- if (attempt < LOCK_MAX_RETRIES - 1) {
35
- await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
36
- }
30
+ if (probeLock(sentinelPath).state === "stale") {
31
+ releaseLock(sentinelPath);
32
+ continue; // Reclaimed — retry immediately.
37
33
  }
38
- }
39
- // Best-effort: proceed without the lock rather than failing the install
40
- return false;
41
- }
42
- /**
43
- * Check if the sentinel was left by a dead process and remove it if so.
44
- * Returns true if the sentinel was reclaimed (removed).
45
- */
46
- function tryReclaimStaleSentinel(sentinelPath) {
47
- try {
48
- const content = fs.readFileSync(sentinelPath, "utf8").trim();
49
- const pid = parseInt(content, 10);
50
- if (Number.isNaN(pid) || pid <= 0) {
51
- // Invalid PID in sentinel — reclaim it
52
- fs.unlinkSync(sentinelPath);
53
- return true;
54
- }
55
- // Check if the process is still alive (signal 0 doesn't kill, just checks)
56
- try {
57
- process.kill(pid, 0);
58
- return false; // Process is alive — lock is valid
59
- }
60
- catch {
61
- // Process is dead — reclaim the stale lock
62
- fs.unlinkSync(sentinelPath);
63
- return true;
34
+ // Another process holds the lock — wait briefly before retrying.
35
+ if (attempt < LOCK_MAX_RETRIES - 1) {
36
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
64
37
  }
65
38
  }
66
- catch {
67
- return false; // Can't read or remove — leave it alone
68
- }
39
+ // Best-effort: proceed without the lock rather than failing the install.
40
+ return false;
69
41
  }
70
42
  function releaseLockSentinel() {
71
- try {
72
- fs.unlinkSync(getLockSentinelPath());
73
- }
74
- catch {
75
- /* ignore — sentinel may already be gone */
76
- }
43
+ releaseLock(getLockSentinelPath());
77
44
  }
78
45
  // ── Read / Write ────────────────────────────────────────────────────────────
79
46
  export function readLockfile() {
@@ -84,7 +51,11 @@ export function readLockfile() {
84
51
  return [];
85
52
  return raw.filter(isValidLockfileEntry);
86
53
  }
87
- catch {
54
+ catch (err) {
55
+ // Defense-in-depth: getLockfilePath() is outside this try block, but a
56
+ // future refactor that pushes a getDataDir() call inside must not mask
57
+ // the bun-test isolation guard as "empty lockfile".
58
+ rethrowIfTestIsolationError(err);
88
59
  return [];
89
60
  }
90
61
  }
@@ -1,5 +1,9 @@
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 { ClaudeCodeProvider } from "./providers/claude-code";
2
5
  import { OpenCodeProvider } from "./providers/opencode";
6
+ export { extractInlineRefMentions } from "./inline-refs";
3
7
  const HARNESSES = [new ClaudeCodeProvider(), new OpenCodeProvider()];
4
8
  const ERROR_PATTERNS = /error|failed|exception|cannot|undefined|null pointer|ENOENT|timeout/i;
5
9
  /**
@@ -0,0 +1,35 @@
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
+ const REMEMBER_RE = /\bakm\s+remember\s+(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)')/g;
5
+ const FEEDBACK_RE = /\bakm\s+feedback\s+(\S+)(?:\s+--[a-z-]+)*\s+(?:--note|-n)\s+(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)')/g;
6
+ export function extractInlineRefMentions(text, ts) {
7
+ if (!text || text.length < 10)
8
+ return [];
9
+ const out = [];
10
+ REMEMBER_RE.lastIndex = 0;
11
+ for (const m of text.matchAll(REMEMBER_RE)) {
12
+ const body = m[1] ?? m[2] ?? "";
13
+ if (!body.trim())
14
+ continue;
15
+ out.push({
16
+ kind: "remember",
17
+ text: body,
18
+ ...(ts !== undefined ? { ts } : {}),
19
+ });
20
+ }
21
+ FEEDBACK_RE.lastIndex = 0;
22
+ for (const m of text.matchAll(FEEDBACK_RE)) {
23
+ const ref = m[1] ?? "";
24
+ const note = m[2] ?? m[3] ?? "";
25
+ if (!ref)
26
+ continue;
27
+ out.push({
28
+ kind: "feedback",
29
+ ref,
30
+ text: note,
31
+ ...(ts !== undefined ? { ts } : {}),
32
+ });
33
+ }
34
+ return out;
35
+ }
@@ -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
+ }
@@ -1,7 +1,80 @@
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";
7
+ import { extractInlineRefMentions } from "../inline-refs";
4
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
+ }
5
78
  export class ClaudeCodeProvider {
6
79
  name = "claude-code";
7
80
  isAvailable() {
@@ -39,6 +112,159 @@ export class ClaudeCodeProvider {
39
112
  return;
40
113
  }
41
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
+ }
42
268
  *#walkJsonl(dir) {
43
269
  try {
44
270
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {