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,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
  * Centralized path resolution for all akm directories.
3
6
  *
@@ -5,14 +8,90 @@
5
8
  * following XDG Base Directory conventions on Unix and standard locations
6
9
  * on Windows.
7
10
  */
11
+ import os from "node:os";
8
12
  import path from "node:path";
9
13
  import { IS_WINDOWS } from "./common";
10
14
  import { ConfigError } from "./errors";
15
+ /**
16
+ * Returns true when the current process appears to be running under
17
+ * `bun test` (either via the BUN_TEST sentinel Bun sets on the test
18
+ * worker, or via the conventional NODE_ENV=test).
19
+ *
20
+ * Used by getDataDir to enforce that every test which resolves a data
21
+ * directory ALSO sets XDG_DATA_HOME (or the AKM_DATA_DIR override) to a
22
+ * temp directory. Without that pairing, tests silently write SQLite
23
+ * databases, lockfiles, and task history into the developer's real
24
+ * `~/.local/share/akm`.
25
+ */
26
+ function isUnderBunTest(env) {
27
+ return env.BUN_TEST === "1" || env.NODE_ENV === "test";
28
+ }
29
+ /**
30
+ * Returns true when the given path is in a directory family the OS may
31
+ * reap (or that the user has clearly designated as a sandbox by virtue
32
+ * of placing it under `/tmp` or a macOS per-user temp dir). Used to
33
+ * decide whether `AKM_STASH_DIR=$tmpdir` should also isolate config +
34
+ * cache writes (so a test harness's `akm setup --yes --dir .` cannot
35
+ * silently clobber the user's `~/.config/akm/config.json`). See
36
+ * `docs/technical/incidents/2026-05-23-setup-clobbers-user-config.md`
37
+ * for the incident that motivated this.
38
+ *
39
+ * Both `/var/folders/*` and `/private/var/folders/*` are matched because
40
+ * `os.tmpdir()` on macOS may return either form depending on whether the
41
+ * caller has canonicalised the path (the realpath of `/var/folders` is
42
+ * `/private/var/folders`, but `path.resolve()` does not follow symlinks).
43
+ */
44
+ export function isTransientStashPath(p) {
45
+ return (p.startsWith("/tmp/") ||
46
+ p === "/tmp" ||
47
+ p.startsWith("/var/tmp/") ||
48
+ p === "/var/tmp" ||
49
+ p.startsWith("/private/tmp/") ||
50
+ p.startsWith("/private/var/folders/") ||
51
+ p.startsWith("/var/folders/"));
52
+ }
53
+ /**
54
+ * Build a TEST_ISOLATION_MISSING ConfigError describing which env var(s)
55
+ * must be set so the data path resolves into a temp dir instead of the
56
+ * user's real XDG home.
57
+ */
58
+ function testIsolationError() {
59
+ return new ConfigError("Refusing to resolve data directory under bun test: neither XDG_DATA_HOME nor AKM_DATA_DIR is set. " +
60
+ "This guards against tests writing into the developer's real ~/.local/share/akm. " +
61
+ "Set XDG_DATA_HOME (or AKM_DATA_DIR) to a mktemp-d directory in this test's env block.", "TEST_ISOLATION_MISSING");
62
+ }
11
63
  // ── Config directory ─────────────────────────────────────────────────────────
12
64
  export function getConfigDir(env = process.env, platform = process.platform) {
13
65
  const override = env.AKM_CONFIG_DIR?.trim();
14
66
  if (override)
15
67
  return override;
68
+ // Explicit XDG override wins next — tests and operators that pre-arrange
69
+ // an isolated config dir via XDG_CONFIG_HOME (or %APPDATA% on Windows)
70
+ // must be honored as set, so the AKM_STASH_DIR transient-isolation rule
71
+ // below does not silently move config away from where they pointed it.
72
+ if (platform === "win32") {
73
+ const appData = env.APPDATA?.trim();
74
+ if (appData)
75
+ return path.join(appData, "akm");
76
+ }
77
+ else {
78
+ const xdgConfigHome = env.XDG_CONFIG_HOME?.trim();
79
+ if (xdgConfigHome)
80
+ return path.join(xdgConfigHome, "akm");
81
+ }
82
+ // Isolation safety: when AKM_STASH_DIR points at a transient/sandbox path
83
+ // (/tmp, /var/tmp, /private/var/folders) AND no explicit config dir
84
+ // override is set, route config writes into `${AKM_STASH_DIR}/.akm`
85
+ // instead of the user's host ~/.config/akm. This prevents the documented
86
+ // isolation pattern
87
+ // AKM_DATA_DIR=/tmp/x AKM_STASH_DIR=/tmp/x akm setup --yes --dir .
88
+ // from silently clobbering the host config. See
89
+ // docs/technical/incidents/2026-05-23-setup-clobbers-user-config.md for the incident.
90
+ // Daily users with a persistent AKM_STASH_DIR=~/my-stash are unaffected.
91
+ const stashOverride = env.AKM_STASH_DIR?.trim();
92
+ if (stashOverride && isTransientStashPath(stashOverride)) {
93
+ return path.join(stashOverride, ".akm");
94
+ }
16
95
  if (platform === "win32") {
17
96
  const appData = env.APPDATA?.trim();
18
97
  if (appData)
@@ -40,6 +119,11 @@ export function getCacheDir() {
40
119
  const override = process.env.AKM_CACHE_DIR?.trim();
41
120
  if (override)
42
121
  return override;
122
+ // Explicit XDG/platform overrides win before the transient-stash isolation
123
+ // rule below — tests and operators that pre-arrange XDG_CACHE_HOME (or
124
+ // %LOCALAPPDATA% / %USERPROFILE% / %APPDATA% on Windows) must be honored
125
+ // as set, so the AKM_STASH_DIR transient rule does not silently move cache
126
+ // writes away from where they pointed them.
43
127
  if (IS_WINDOWS) {
44
128
  const localAppData = process.env.LOCALAPPDATA?.trim();
45
129
  if (localAppData)
@@ -48,18 +132,32 @@ export function getCacheDir() {
48
132
  if (userProfile)
49
133
  return path.join(userProfile, "AppData", "Local", "akm");
50
134
  const appData = process.env.APPDATA?.trim();
51
- if (!appData) {
52
- throw new ConfigError("Unable to determine cache directory. Set LOCALAPPDATA, USERPROFILE, or APPDATA.", "CONFIG_DIR_UNRESOLVABLE");
135
+ if (appData) {
136
+ // Heuristic fallback: APPDATA points to %APPDATA% (Roaming), so
137
+ // navigate to the sibling "Local" directory. This is typically
138
+ // C:\Users\<name>\AppData\Roaming → C:\Users\<name>\AppData\Local\akm.
139
+ // Preferred: set LOCALAPPDATA to avoid this navigation.
140
+ return path.join(appData, "..", "Local", "akm");
53
141
  }
54
- // Heuristic fallback: APPDATA points to %APPDATA% (Roaming), so navigate
55
- // to the sibling "Local" directory. This is typically
56
- // C:\Users\<name>\AppData\Roaming C:\Users\<name>\AppData\Local\akm.
57
- // Preferred: set LOCALAPPDATA to avoid this navigation.
58
- return path.join(appData, "..", "Local", "akm");
59
- }
60
- const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
61
- if (xdgCacheHome)
62
- return path.join(xdgCacheHome, "akm");
142
+ }
143
+ else {
144
+ const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
145
+ if (xdgCacheHome)
146
+ return path.join(xdgCacheHome, "akm");
147
+ }
148
+ // Isolation safety (mirrors getConfigDir): when AKM_STASH_DIR points at a
149
+ // transient path AND no explicit cache override is set, route cache writes
150
+ // into `${AKM_STASH_DIR}/.akm/cache` so that config backups, registry-index
151
+ // cache, and other regenerable artifacts do not pollute the user's host
152
+ // ~/.cache/akm directory.
153
+ const stashOverride = process.env.AKM_STASH_DIR?.trim();
154
+ if (stashOverride && isTransientStashPath(stashOverride)) {
155
+ return path.join(stashOverride, ".akm", "cache");
156
+ }
157
+ if (IS_WINDOWS) {
158
+ // None of LOCALAPPDATA / USERPROFILE / APPDATA were set above.
159
+ throw new ConfigError("Unable to determine cache directory. Set LOCALAPPDATA, USERPROFILE, or APPDATA.", "CONFIG_DIR_UNRESOLVABLE");
160
+ }
63
161
  const home = process.env.HOME?.trim();
64
162
  if (!home)
65
163
  return path.join("/tmp", "akm-cache");
@@ -82,6 +180,17 @@ export function getDataDir(env = process.env, platform = process.platform) {
82
180
  const override = env.AKM_DATA_DIR?.trim();
83
181
  if (override)
84
182
  return override;
183
+ // Defense-in-depth: under `bun test`, refuse to fall through to the
184
+ // user's real $XDG_DATA_HOME / ~/.local/share/akm under any condition.
185
+ // Any test that needs a data dir must point it at a mktemp-d directory
186
+ // via XDG_DATA_HOME (or AKM_DATA_DIR). The previous carve-out that only
187
+ // fired when AKM_STASH_DIR was set was a loophole: tests calling
188
+ // openDatabase() or getDbPath() without overriding any env var silently
189
+ // wrote into ~/.local/share/akm/index.db (observed: 4,183-row
190
+ // registry-cache pollution). Item 5 of the 0.8.x critical-review plan.
191
+ if (isUnderBunTest(env) && !env.XDG_DATA_HOME?.trim()) {
192
+ throw testIsolationError();
193
+ }
85
194
  if (platform === "win32") {
86
195
  const localAppData = env.LOCALAPPDATA?.trim();
87
196
  if (localAppData)
@@ -103,43 +212,6 @@ export function getDataDir(env = process.env, platform = process.platform) {
103
212
  return path.join("/tmp", "akm-data");
104
213
  return path.join(home, ".local", "share", "akm");
105
214
  }
106
- // ── State directory ──────────────────────────────────────────────────────────
107
- /**
108
- * Returns the XDG state directory for akm (`~/.local/state/akm` on Linux/macOS,
109
- * `%LOCALAPPDATA%\akm\state` on Windows).
110
- *
111
- * Holds runtime state and log-like files that persist across reboots but are
112
- * less precious than $DATA: task history JSONL files, akm.lock.lck sentinel.
113
- *
114
- * Env overrides (in priority order):
115
- * AKM_STATE_DIR — point to any directory
116
- * XDG_STATE_HOME — (Linux/macOS) override the XDG base; akm subdir is appended
117
- */
118
- export function getStateDir(env = process.env, platform = process.platform) {
119
- const override = env.AKM_STATE_DIR?.trim();
120
- if (override)
121
- return override;
122
- if (platform === "win32") {
123
- const localAppData = env.LOCALAPPDATA?.trim();
124
- if (localAppData)
125
- return path.join(localAppData, "akm", "state");
126
- const userProfile = env.USERPROFILE?.trim();
127
- if (userProfile)
128
- return path.join(userProfile, "AppData", "Local", "akm", "state");
129
- const appData = env.APPDATA?.trim();
130
- if (!appData) {
131
- throw new ConfigError("Unable to determine state directory. Set LOCALAPPDATA, USERPROFILE, or APPDATA.", "CONFIG_DIR_UNRESOLVABLE");
132
- }
133
- return path.join(appData, "..", "Local", "akm", "state");
134
- }
135
- const xdgStateHome = env.XDG_STATE_HOME?.trim();
136
- if (xdgStateHome)
137
- return path.join(xdgStateHome, "akm");
138
- const home = env.HOME?.trim();
139
- if (!home)
140
- return path.join("/tmp", "akm-state");
141
- return path.join(home, ".local", "state", "akm");
142
- }
143
215
  export function getDbPath() {
144
216
  return path.join(getDataDir(), "index.db");
145
217
  }
@@ -150,9 +222,9 @@ export function getWorkflowDbPath() {
150
222
  export function getStateDbPathInDataDir() {
151
223
  return path.join(getDataDir(), "state.db");
152
224
  }
153
- /** Path for the task history directory in $STATE (v2 location). */
225
+ /** Path for the task history directory in $DATA. */
154
226
  export function getTaskHistoryStateDir() {
155
- return path.join(getStateDir(), "tasks", "history");
227
+ return path.join(getDataDir(), "tasks", "history");
156
228
  }
157
229
  /** Path to the akm.lock file in $DATA. */
158
230
  export function getLockfilePath() {
@@ -198,3 +270,99 @@ export function getDefaultStashDir() {
198
270
  }
199
271
  return path.join(home, "akm");
200
272
  }
273
+ // ── Stash directory safety check (#473) ──────────────────────────────────────
274
+ /**
275
+ * Refuse stashDir values that would clobber a sensitive system path or the
276
+ * user's home directory itself. Called from `akm init`, `akm setup`, and the
277
+ * setup-wizard validator before any disk write.
278
+ *
279
+ * Refuses:
280
+ * - The filesystem root (`/` or Windows drive root `C:\`)
281
+ * - Common system roots (`/etc`, `/var`, `/usr`, `/usr/local`, `/opt`,
282
+ * `/sys`, `/proc`, `/boot`, `/bin`, `/sbin`, `/lib`, `/lib64`, `/dev`,
283
+ * `/run`, `/home`, `/root`, `/mnt`, `/media`,
284
+ * `/Library`, `/System`, `/Applications`)
285
+ * - The user's home directory itself (exact match — subdirs are fine)
286
+ * - User-data dotfile parents: `~/.config`, `~/.local`, `~/.cache`,
287
+ * `~/.ssh`, `~/.gnupg`, `~/.aws`, `~/.kube`, `~/.docker`,
288
+ * and the macOS/Windows `~/Documents` and `~/Downloads` parents
289
+ *
290
+ * Subdirectories of any refused path are allowed (so `~/.local/share/akm-test`
291
+ * is fine even though `~/.local` is refused). This catches fat-finger
292
+ * `--dir /` or `--dir ~` without preventing legitimate nested use.
293
+ */
294
+ export function assertSafeStashDir(stashDir) {
295
+ const resolved = path.resolve(stashDir);
296
+ // Filesystem root — POSIX and Windows drive roots.
297
+ if (resolved === "/" || /^[A-Za-z]:[\\/]?$/.test(resolved)) {
298
+ throw new ConfigError(`Refusing stashDir at filesystem root (${resolved}). Pick a subdirectory like ~/akm.`, "UNSAFE_STASH_DIR");
299
+ }
300
+ // System directories — exact match only.
301
+ const SYSTEM_ROOTS = new Set([
302
+ "/etc",
303
+ "/var",
304
+ "/var/tmp",
305
+ "/usr",
306
+ "/usr/local",
307
+ "/opt",
308
+ "/sys",
309
+ "/proc",
310
+ "/boot",
311
+ "/bin",
312
+ "/sbin",
313
+ "/lib",
314
+ "/lib64",
315
+ "/dev",
316
+ "/run",
317
+ "/home",
318
+ "/root",
319
+ "/mnt",
320
+ "/media",
321
+ "/Library",
322
+ "/System",
323
+ "/Applications",
324
+ ]);
325
+ if (SYSTEM_ROOTS.has(resolved)) {
326
+ throw new ConfigError(`Refusing stashDir at system path (${resolved}). Pick a path inside your home directory.`, "UNSAFE_STASH_DIR");
327
+ }
328
+ // User home — exact match only. Subdirs (~/akm, ~/work/stash) are fine.
329
+ // Check BOTH the env-controlled home and the OS-reported home, so the
330
+ // refusal can't be bypassed by unsetting HOME, and so it still fires
331
+ // under bun test (which isolates HOME to a tempdir while os.homedir()
332
+ // still returns the real user's home).
333
+ const candidateHomes = new Set();
334
+ const envHome = (process.env.HOME ?? process.env.USERPROFILE)?.trim();
335
+ if (envHome)
336
+ candidateHomes.add(path.resolve(envHome));
337
+ try {
338
+ const osHome = os.homedir();
339
+ if (osHome)
340
+ candidateHomes.add(path.resolve(osHome));
341
+ }
342
+ catch {
343
+ // os.homedir() can throw on misconfigured systems; ignore.
344
+ }
345
+ const HIDDEN_USER_PARENTS = [
346
+ ".config",
347
+ ".local",
348
+ ".cache",
349
+ ".ssh",
350
+ ".gnupg",
351
+ ".aws",
352
+ ".kube",
353
+ ".docker",
354
+ "Documents",
355
+ "Downloads",
356
+ "AppData",
357
+ ];
358
+ for (const home of candidateHomes) {
359
+ if (resolved === home) {
360
+ throw new ConfigError(`Refusing stashDir at your home directory (${resolved}). Pick a subdirectory like ~/akm.`, "UNSAFE_STASH_DIR");
361
+ }
362
+ for (const sub of HIDDEN_USER_PARENTS) {
363
+ if (resolved === path.join(home, sub)) {
364
+ throw new ConfigError(`Refusing stashDir at sensitive user directory (${resolved}). Pick a subdirectory or a dedicated workspace.`, "UNSAFE_STASH_DIR");
365
+ }
366
+ }
367
+ }
368
+ }