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,158 @@
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
+ /**
5
+ * Shared JSON parsing utilities for LLM and agent output.
6
+ *
7
+ * Lives in `src/core/` so that both `src/llm/` and `src/integrations/agent/`
8
+ * can import without crossing the one-way boundary defined by v1 spec §9.7
9
+ * (agent/ must not import from llm/).
10
+ *
11
+ * The canonical implementation is ported from `src/llm/client.ts` (most
12
+ * complete version):
13
+ * - Strips `<think>…</think>` reasoning blocks.
14
+ * - Strips markdown code fences (``` or ~~~, optional language tag, with
15
+ * trailing spaces on the fence line).
16
+ * - Escapes unescaped control characters (actual \n, \r, \t bytes) inside
17
+ * JSON string values so `JSON.parse` succeeds on outputs from local LLMs.
18
+ * - Balanced-brace scanner handles both `{…}` and `[…]` top-level
19
+ * structures (spawn.ts v0 only handled `{…}` — that was a bug).
20
+ */
21
+ /**
22
+ * Strips `<think>…</think>` blocks from LLM output (for reasoning-capable
23
+ * models). Also strips leading/trailing whitespace.
24
+ */
25
+ export function stripThinkBlocks(raw) {
26
+ return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
27
+ }
28
+ /**
29
+ * Strips markdown code fences (``` or ~~~, with optional language tag).
30
+ * Handles fences with trailing spaces. Returns trimmed content.
31
+ */
32
+ export function stripCodeFences(raw) {
33
+ return raw
34
+ .trim()
35
+ .replace(/^```(?:json)?\s*\n?/i, "")
36
+ .replace(/\n?```\s*$/i, "")
37
+ .trim();
38
+ }
39
+ /**
40
+ * Escapes unescaped control characters (actual \n, \r, \t bytes) inside JSON
41
+ * string values. Prevents `JSON.parse` failures from embedded newlines in
42
+ * local-LLM output.
43
+ */
44
+ export function escapeJsonStringControls(raw) {
45
+ let out = "";
46
+ let inString = false;
47
+ let escaped = false;
48
+ for (let i = 0; i < raw.length; i++) {
49
+ const ch = raw[i];
50
+ if (escaped) {
51
+ out += ch;
52
+ escaped = false;
53
+ continue;
54
+ }
55
+ if (ch === "\\" && inString) {
56
+ out += ch;
57
+ escaped = true;
58
+ continue;
59
+ }
60
+ if (ch === '"') {
61
+ inString = !inString;
62
+ out += ch;
63
+ continue;
64
+ }
65
+ if (inString) {
66
+ if (ch === "\n") {
67
+ out += "\\n";
68
+ continue;
69
+ }
70
+ if (ch === "\r") {
71
+ out += "\\r";
72
+ continue;
73
+ }
74
+ if (ch === "\t") {
75
+ out += "\\t";
76
+ continue;
77
+ }
78
+ }
79
+ out += ch;
80
+ }
81
+ return out;
82
+ }
83
+ /**
84
+ * Full pipeline: stripThinkBlocks → stripCodeFences → escapeJsonStringControls
85
+ * → JSON.parse. Returns `undefined` on parse failure.
86
+ */
87
+ export function parseJsonResponse(raw) {
88
+ try {
89
+ const cleaned = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw)));
90
+ return JSON.parse(cleaned);
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ }
96
+ /**
97
+ * Attempts `parseJsonResponse` first. On failure, scans for the first
98
+ * balanced `{ }` or `[ ]` structure in the text and attempts to parse that
99
+ * substring. Returns `undefined` if no valid JSON structure is found.
100
+ *
101
+ * Non-array results are preferred: if a `{…}` object is found first, it is
102
+ * returned immediately. Arrays (`[…]`) are captured as a fallback and
103
+ * returned only when no object was found.
104
+ */
105
+ export function parseEmbeddedJsonResponse(raw) {
106
+ const direct = parseJsonResponse(raw);
107
+ if (direct !== undefined)
108
+ return direct;
109
+ const text = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw)));
110
+ let arrayFallback;
111
+ for (let start = 0; start < text.length; start++) {
112
+ const opener = text[start];
113
+ if (opener !== "{" && opener !== "[")
114
+ continue;
115
+ const closer = opener === "{" ? "}" : "]";
116
+ let depth = 0;
117
+ let inString = false;
118
+ let escaped = false;
119
+ for (let i = start; i < text.length; i++) {
120
+ const ch = text[i];
121
+ if (inString) {
122
+ if (escaped) {
123
+ escaped = false;
124
+ }
125
+ else if (ch === "\\") {
126
+ escaped = true;
127
+ }
128
+ else if (ch === '"') {
129
+ inString = false;
130
+ }
131
+ continue;
132
+ }
133
+ if (ch === '"') {
134
+ inString = true;
135
+ continue;
136
+ }
137
+ if (ch === opener)
138
+ depth += 1;
139
+ if (ch === closer) {
140
+ depth -= 1;
141
+ if (depth === 0) {
142
+ try {
143
+ const parsed = JSON.parse(text.slice(start, i + 1));
144
+ if (!Array.isArray(parsed)) {
145
+ return parsed;
146
+ }
147
+ arrayFallback ??= parsed;
148
+ break;
149
+ }
150
+ catch {
151
+ break;
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ return arrayFallback;
158
+ }
@@ -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";
13
+ import { IS_WINDOWS } from "./common";
9
14
  import { ConfigError } from "./errors";
10
- const IS_WINDOWS = process.platform === "win32";
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,28 +132,107 @@ 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");
66
164
  return path.join(home, ".cache", "akm");
67
165
  }
166
+ // ── Data directory ───────────────────────────────────────────────────────────
167
+ /**
168
+ * Returns the XDG data directory for akm (`~/.local/share/akm` on Linux/macOS,
169
+ * `%LOCALAPPDATA%\akm\data` on Windows).
170
+ *
171
+ * Holds durable, non-regenerable application data: SQLite databases
172
+ * (index.db, workflow.db, state.db), akm.lock, and config-backups.
173
+ * Losing this directory loses history and installed state.
174
+ *
175
+ * Env overrides (in priority order):
176
+ * AKM_DATA_DIR — point to any directory
177
+ * XDG_DATA_HOME — (Linux/macOS) override the XDG base; akm subdir is appended
178
+ */
179
+ export function getDataDir(env = process.env, platform = process.platform) {
180
+ const override = env.AKM_DATA_DIR?.trim();
181
+ if (override)
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
+ }
194
+ if (platform === "win32") {
195
+ const localAppData = env.LOCALAPPDATA?.trim();
196
+ if (localAppData)
197
+ return path.join(localAppData, "akm", "data");
198
+ const userProfile = env.USERPROFILE?.trim();
199
+ if (userProfile)
200
+ return path.join(userProfile, "AppData", "Local", "akm", "data");
201
+ const appData = env.APPDATA?.trim();
202
+ if (!appData) {
203
+ throw new ConfigError("Unable to determine data directory. Set LOCALAPPDATA, USERPROFILE, or APPDATA.", "CONFIG_DIR_UNRESOLVABLE");
204
+ }
205
+ return path.join(appData, "..", "Local", "akm", "data");
206
+ }
207
+ const xdgDataHome = env.XDG_DATA_HOME?.trim();
208
+ if (xdgDataHome)
209
+ return path.join(xdgDataHome, "akm");
210
+ const home = env.HOME?.trim();
211
+ if (!home)
212
+ return path.join("/tmp", "akm-data");
213
+ return path.join(home, ".local", "share", "akm");
214
+ }
68
215
  export function getDbPath() {
69
- return path.join(getCacheDir(), "index.db");
216
+ return path.join(getDataDir(), "index.db");
70
217
  }
71
218
  export function getWorkflowDbPath() {
72
- return path.join(getCacheDir(), "workflow.db");
219
+ return path.join(getDataDir(), "workflow.db");
220
+ }
221
+ /** Path to the state.db file in $DATA. */
222
+ export function getStateDbPathInDataDir() {
223
+ return path.join(getDataDir(), "state.db");
224
+ }
225
+ /** Path for the task history directory in $DATA. */
226
+ export function getTaskHistoryStateDir() {
227
+ return path.join(getDataDir(), "tasks", "history");
228
+ }
229
+ /** Path to the akm.lock file in $DATA. */
230
+ export function getLockfilePath() {
231
+ return path.join(getDataDir(), "akm.lock");
232
+ }
233
+ /** Path to the akm.lock.lck write-sentinel in $DATA. */
234
+ export function getLockfileLockPath() {
235
+ return path.join(getDataDir(), "akm.lock.lck");
73
236
  }
74
237
  export function getSemanticStatusPath() {
75
238
  return path.join(getCacheDir(), "semantic-status.json");
@@ -83,6 +246,13 @@ export function getRegistryIndexCacheDir() {
83
246
  export function getBinDir() {
84
247
  return path.join(getCacheDir(), "bin");
85
248
  }
249
+ // ── Scheduled-task runtime directories (logs + history) ──────────────────────
250
+ export function getTaskLogDir() {
251
+ return path.join(getCacheDir(), "tasks", "logs");
252
+ }
253
+ export function getTaskHistoryDir() {
254
+ return path.join(getCacheDir(), "tasks", "history");
255
+ }
86
256
  // ── Default stash directory ──────────────────────────────────────────────────
87
257
  export function getDefaultStashDir() {
88
258
  const override = process.env.AKM_STASH_DIR?.trim();
@@ -100,3 +270,99 @@ export function getDefaultStashDir() {
100
270
  }
101
271
  return path.join(home, "akm");
102
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
+ }