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
@@ -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
  * Agent CLI spawn wrapper (v1 spec §12.2).
3
6
  *
@@ -11,6 +14,10 @@
11
14
  * NEVER imports an LLM SDK. Agents are reachable only via shell-out;
12
15
  * this is a pre-emptive guarantee against the #222 invariant.
13
16
  */
17
+ import fs from "node:fs";
18
+ import os from "node:os";
19
+ import path from "node:path";
20
+ import { getCommandBuilder } from "./builders";
14
21
  import { DEFAULT_AGENT_TIMEOUT_MS } from "./config";
15
22
  /**
16
23
  * Kill the process group of `proc` with `signal`, falling back to
@@ -21,7 +28,7 @@ import { DEFAULT_AGENT_TIMEOUT_MS } from "./config";
21
28
  * reaped alongside the node wrapper. The fallback keeps test fakes working
22
29
  * without modification.
23
30
  */
24
- export function killGroup(proc, signal) {
31
+ function killGroup(proc, signal) {
25
32
  if (typeof proc.pid === "number") {
26
33
  try {
27
34
  process.kill(-proc.pid, signal);
@@ -39,6 +46,60 @@ export function killGroup(proc, signal) {
39
46
  }
40
47
  }
41
48
  const DEFAULT_TIMEOUT_MS = DEFAULT_AGENT_TIMEOUT_MS;
49
+ /**
50
+ * Supplement `existingPath` with well-known user binary directories when
51
+ * running in a scheduler context (cron/launchd) where PATH is stripped.
52
+ *
53
+ * Detection heuristic: if the current PATH does not contain the user's home
54
+ * directory, we are likely in a stripped scheduler env. In an interactive
55
+ * shell the user's home almost always appears (e.g. ~/.bun/bin, ~/.cargo/bin).
56
+ *
57
+ * Only directories that actually exist on disk are prepended, and only if
58
+ * they are not already present, so interactive-shell PATH ordering is never
59
+ * disturbed.
60
+ */
61
+ export function supplementPathForSchedulerContext(existingPath) {
62
+ const home = os.homedir();
63
+ // If PATH already contains the home directory, we are in an interactive
64
+ // shell — skip supplementation entirely.
65
+ if (existingPath.split(path.delimiter).some((d) => d.startsWith(home))) {
66
+ return existingPath;
67
+ }
68
+ const candidates = pathCandidatesForCurrentPlatform(home);
69
+ const existing = new Set(existingPath.split(path.delimiter).filter(Boolean));
70
+ const toAdd = candidates.filter((d) => !existing.has(d) && fs.existsSync(d));
71
+ if (toAdd.length === 0)
72
+ return existingPath;
73
+ return [...toAdd, existingPath].filter(Boolean).join(path.delimiter);
74
+ }
75
+ function pathCandidatesForCurrentPlatform(home) {
76
+ if (process.platform === "win32") {
77
+ // Windows: Bun + Cargo + Scoop + Chocolatey + system tools. Order favors
78
+ // user-local installs over machine-global so the user's chosen toolchain
79
+ // wins. These paths are commonly stripped from Task Scheduler / service
80
+ // environments, mirroring the cron/launchd problem on POSIX.
81
+ const localAppData = process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local");
82
+ const userProfile = process.env.USERPROFILE ?? home;
83
+ const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
84
+ return [
85
+ path.join(userProfile, ".bun", "bin"),
86
+ path.join(localAppData, "Programs", "bun"),
87
+ path.join(userProfile, ".cargo", "bin"),
88
+ path.join(localAppData, "Programs", "Git", "cmd"),
89
+ path.join(userProfile, "scoop", "shims"),
90
+ path.join(programFiles, "Git", "cmd"),
91
+ "C:\\ProgramData\\chocolatey\\bin",
92
+ ];
93
+ }
94
+ return [
95
+ path.join(home, ".bun", "bin"),
96
+ path.join(home, ".cargo", "bin"),
97
+ path.join(home, ".local", "bin"),
98
+ "/opt/homebrew/bin",
99
+ "/opt/homebrew/sbin",
100
+ "/usr/local/bin",
101
+ ];
102
+ }
42
103
  function resolveSpawnFn(options) {
43
104
  if (options.spawn)
44
105
  return options.spawn;
@@ -54,6 +115,10 @@ function resolveSpawnFn(options) {
54
115
  * • Every name in `profile.envPassthrough`.
55
116
  * • Every entry in `profile.env`.
56
117
  * • Every entry in `options.env` (highest precedence).
118
+ *
119
+ * PATH is supplemented with well-known user binary directories when running
120
+ * in a scheduler context (cron/launchd) where the inherited PATH is stripped.
121
+ * See {@link supplementPathForSchedulerContext}.
57
122
  */
58
123
  function buildChildEnv(profile, options) {
59
124
  const source = options.envSource ?? process.env;
@@ -63,6 +128,11 @@ function buildChildEnv(profile, options) {
63
128
  if (value !== undefined)
64
129
  env[name] = value;
65
130
  }
131
+ // Supplement PATH after passthrough so the scheduler-context fix applies to
132
+ // the value actually coming from the environment source.
133
+ if (env.PATH !== undefined) {
134
+ env.PATH = supplementPathForSchedulerContext(env.PATH);
135
+ }
66
136
  if (profile.env) {
67
137
  for (const [k, v] of Object.entries(profile.env))
68
138
  env[k] = v;
@@ -109,19 +179,35 @@ async function readStream(stream, opts) {
109
179
  */
110
180
  export async function runAgent(profile, prompt, options = {}) {
111
181
  const stdioMode = options.stdio ?? profile.stdio;
112
- const timeoutMs = options.timeoutMs ?? profile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
182
+ // null = explicitly disabled (no kill timer). undefined = inherit from profile/default.
183
+ const timeoutMs = options.timeoutMs !== undefined ? options.timeoutMs : (profile.timeoutMs ?? DEFAULT_TIMEOUT_MS);
113
184
  const parseOutput = options.parseOutput ?? profile.parseOutput;
114
185
  const setTimeoutImpl = options.setTimeoutFn ?? setTimeout;
115
186
  const clearTimeoutImpl = options.clearTimeoutFn ?? clearTimeout;
116
- const args = [...profile.args, ...(options.args ?? [])];
117
- if (prompt !== undefined)
118
- args.push(prompt);
119
- const env = buildChildEnv(profile, options);
187
+ // Build argv via the platform-specific builder when dispatch params are
188
+ // provided; fall back to the legacy positional-prompt form otherwise.
189
+ let builtArgv;
190
+ let builtEnv;
191
+ if (options.dispatch !== undefined) {
192
+ const builder = getCommandBuilder(profile.commandBuilder ?? profile.name, options.builderRegistry);
193
+ const built = builder.build(profile, options.dispatch);
194
+ builtArgv = built.argv;
195
+ builtEnv = built.env;
196
+ }
197
+ else {
198
+ const legacyArgs = [...profile.args, ...(options.args ?? [])];
199
+ if (prompt !== undefined)
200
+ legacyArgs.push(prompt);
201
+ builtArgv = [profile.bin, ...legacyArgs];
202
+ }
203
+ // Extra args (e.g. forwarded CLI positionals) are appended after the builder output.
204
+ const finalArgv = [...builtArgv, ...(options.dispatch ? (options.args ?? []) : [])];
205
+ const env = { ...buildChildEnv(profile, options), ...(builtEnv ?? {}) };
120
206
  const start = Date.now();
121
207
  let proc;
122
208
  try {
123
209
  const spawnFn = resolveSpawnFn(options);
124
- proc = spawnFn([profile.bin, ...args], {
210
+ proc = spawnFn(finalArgv, {
125
211
  stdin: stdioMode === "captured" ? (options.stdin !== undefined ? "pipe" : "ignore") : "inherit",
126
212
  stdout: stdioMode === "captured" ? "pipe" : "inherit",
127
213
  stderr: stdioMode === "captured" ? "pipe" : "inherit",
@@ -153,26 +239,34 @@ export async function runAgent(profile, prompt, options = {}) {
153
239
  // BUG-M3: only flag `timedOut` when the child has not already exited. A
154
240
  // timer firing in the same microtask as `proc.exited` resolving could
155
241
  // otherwise label a clean exit as a timeout.
242
+ //
243
+ // When timeoutMs is null the kill timer is skipped entirely — the task runs
244
+ // until the process exits naturally. Intended for long-running local-model
245
+ // tasks where wall-clock time is unpredictable.
156
246
  let timedOut = false;
157
- const timer = setTimeoutImpl(() => {
158
- if (proc.exitCode !== null)
159
- return;
160
- timedOut = true;
161
- killGroup(proc, "SIGTERM");
162
- // Follow up with SIGKILL after 5 s in case the process ignores SIGTERM.
163
- setTimeoutImpl(() => {
164
- if (proc.exitCode !== null)
247
+ let timer;
248
+ if (timeoutMs !== null) {
249
+ timer = setTimeoutImpl(() => {
250
+ if (!proc || proc.exitCode !== null)
165
251
  return;
166
- killGroup(proc, "SIGKILL");
167
- }, 5000);
168
- }, timeoutMs);
252
+ timedOut = true;
253
+ killGroup(proc, "SIGTERM");
254
+ // Follow up with SIGKILL after 5 s in case the process ignores SIGTERM.
255
+ setTimeoutImpl(() => {
256
+ if (!proc || proc.exitCode !== null)
257
+ return;
258
+ killGroup(proc, "SIGKILL");
259
+ }, 5000);
260
+ }, timeoutMs);
261
+ }
169
262
  // Stream-drain timeout: the overall wall-clock budget plus a 2 s grace
170
263
  // period. When a process is killed via SIGTERM/SIGKILL (from our timeout
171
264
  // handler or from outside) some runtimes keep the pipe write-end open in
172
265
  // background threads, which would cause `Response.text()` to block forever.
173
- // Capping stream draining at `timeoutMs + 2 000 ms` ensures the caller
174
- // never hangs past the wall budget regardless of subprocess pipe behaviour.
175
- const streamDrainTimeoutMs = timeoutMs + 2_000;
266
+ // Capping stream draining ensures the caller never hangs past the wall
267
+ // budget regardless of subprocess pipe behaviour.
268
+ // When there is no kill timer, allow up to 30 s for streams to drain.
269
+ const streamDrainTimeoutMs = timeoutMs !== null ? timeoutMs + 2_000 : 30_000;
176
270
  const stdoutPromise = stdioMode === "captured"
177
271
  ? readStream(proc.stdout ?? null, { timeoutMs: streamDrainTimeoutMs })
178
272
  : Promise.resolve("");
@@ -209,7 +303,8 @@ export async function runAgent(profile, prompt, options = {}) {
209
303
  exitCode = await proc.exited;
210
304
  }
211
305
  catch (err) {
212
- clearTimeoutImpl(timer);
306
+ if (timer !== undefined)
307
+ clearTimeoutImpl(timer);
213
308
  // BUG-H2: drain stream readers before the early return so they don't
214
309
  // surface as unhandled rejections after the function resolves.
215
310
  // The streams already carry a built-in drain timeout so this allSettled
@@ -237,7 +332,7 @@ export async function runAgent(profile, prompt, options = {}) {
237
332
  stderr,
238
333
  durationMs,
239
334
  reason: "timeout",
240
- error: `agent CLI "${profile.name}" timed out after ${timeoutMs}ms`,
335
+ error: `agent CLI "${profile.name}" timed out after ${timeoutMs ?? 0}ms`,
241
336
  };
242
337
  }
243
338
  if (exitCode !== 0) {
@@ -252,21 +347,74 @@ export async function runAgent(profile, prompt, options = {}) {
252
347
  };
253
348
  }
254
349
  if (parseOutput === "json" && stdioMode === "captured") {
350
+ // Strip <think> blocks and code fences, then try direct parse with
351
+ // embedded-JSON fallback for local LLMs that emit prose around the payload.
352
+ const cleaned = stdout
353
+ .trim()
354
+ .replace(/<think>[\s\S]*?<\/think>/gi, "")
355
+ .trim()
356
+ .replace(/^```(?:json)?\s*\n?/, "")
357
+ .replace(/\n?```\s*$/, "")
358
+ .trim();
359
+ let parsed;
255
360
  try {
256
- const parsed = JSON.parse(stdout);
257
- return { ok: true, exitCode, stdout, stderr, durationMs, parsed };
361
+ parsed = JSON.parse(cleaned);
258
362
  }
259
- catch (err) {
260
- return {
261
- ok: false,
262
- exitCode,
263
- stdout,
264
- stderr,
265
- durationMs,
266
- reason: "parse_error",
267
- error: err instanceof Error ? err.message : String(err),
268
- };
363
+ catch {
364
+ // Fallback: extract the first balanced {…} from prose output.
365
+ let found;
366
+ for (let s = 0; s < cleaned.length; s++) {
367
+ if (cleaned[s] !== "{")
368
+ continue;
369
+ let depth = 0, inStr = false, esc = false;
370
+ for (let i = s; i < cleaned.length; i++) {
371
+ const c = cleaned[i];
372
+ if (inStr) {
373
+ if (esc) {
374
+ esc = false;
375
+ }
376
+ else if (c === "\\") {
377
+ esc = true;
378
+ }
379
+ else if (c === '"') {
380
+ inStr = false;
381
+ }
382
+ continue;
383
+ }
384
+ if (c === '"') {
385
+ inStr = true;
386
+ continue;
387
+ }
388
+ if (c === "{")
389
+ depth++;
390
+ if (c === "}") {
391
+ depth--;
392
+ if (depth === 0) {
393
+ try {
394
+ found = JSON.parse(cleaned.slice(s, i + 1));
395
+ }
396
+ catch { }
397
+ break;
398
+ }
399
+ }
400
+ }
401
+ if (found !== undefined)
402
+ break;
403
+ }
404
+ if (found === undefined) {
405
+ return {
406
+ ok: false,
407
+ exitCode,
408
+ stdout,
409
+ stderr,
410
+ durationMs,
411
+ reason: "parse_error",
412
+ error: "no JSON object found in agent output",
413
+ };
414
+ }
415
+ parsed = found;
269
416
  }
417
+ return { ok: true, exitCode, stdout, stderr, durationMs, parsed };
270
418
  }
271
419
  return { ok: true, exitCode, stdout, stderr, durationMs };
272
420
  }
@@ -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
  import * as childProcess from "node:child_process";
2
5
  export const GITHUB_API_BASE = "https://api.github.com";
3
6
  const GITHUB_TOKEN_DOMAINS = new Set(["api.github.com", "github.com", "uploads.github.com"]);
@@ -1,77 +1,46 @@
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
- import { getConfigDir } from "../core/config";
6
+ import { writeFileAtomic } from "../core/common";
7
+ import { rethrowIfTestIsolationError } from "../core/errors";
8
+ import { probeLock, releaseLock, tryAcquireLockSync } from "../core/file-lock";
9
+ import { getDataDir } from "../core/paths";
4
10
  // ── Paths ───────────────────────────────────────────────────────────────────
5
11
  const LOCKFILE_NAME = "akm.lock";
6
12
  function getLockfilePath() {
7
- return path.join(getConfigDir(), LOCKFILE_NAME);
13
+ return path.join(getDataDir(), LOCKFILE_NAME);
8
14
  }
9
15
  // ── Lock sentinel ────────────────────────────────────────────────────────────
10
16
  const LOCK_MAX_RETRIES = 3;
11
17
  const LOCK_RETRY_DELAY_MS = 100;
12
18
  function getLockSentinelPath() {
13
- return `${getLockfilePath()}.lck`;
19
+ // The sentinel always lives next to the lock file it guards.
20
+ return `${path.join(getDataDir(), LOCKFILE_NAME)}.lck`;
14
21
  }
15
22
  async function acquireLockSentinel() {
16
23
  const sentinelPath = getLockSentinelPath();
17
- // Ensure the directory exists before attempting to create the sentinel
24
+ // Ensure the directory exists before attempting to create the sentinel.
18
25
  fs.mkdirSync(path.dirname(sentinelPath), { recursive: true });
19
26
  for (let attempt = 0; attempt < LOCK_MAX_RETRIES; attempt++) {
20
- try {
21
- fs.writeFileSync(sentinelPath, String(process.pid), { flag: "wx" });
22
- return true; // Sentinel created — we own the lock
27
+ if (tryAcquireLockSync(sentinelPath, String(process.pid))) {
28
+ return true; // Sentinel created — we own the lock.
23
29
  }
24
- catch (err) {
25
- if (err.code !== "EEXIST")
26
- throw err;
27
- // Check for stale lock — if the owning PID is no longer running, reclaim it
28
- if (tryReclaimStaleSentinel(sentinelPath)) {
29
- continue; // Sentinel removed — retry immediately
30
- }
31
- // Another process holds the lock — wait briefly before retrying
32
- if (attempt < LOCK_MAX_RETRIES - 1) {
33
- await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
34
- }
30
+ if (probeLock(sentinelPath).state === "stale") {
31
+ releaseLock(sentinelPath);
32
+ continue; // Reclaimed — retry immediately.
35
33
  }
36
- }
37
- // Best-effort: proceed without the lock rather than failing the install
38
- return false;
39
- }
40
- /**
41
- * Check if the sentinel was left by a dead process and remove it if so.
42
- * Returns true if the sentinel was reclaimed (removed).
43
- */
44
- function tryReclaimStaleSentinel(sentinelPath) {
45
- try {
46
- const content = fs.readFileSync(sentinelPath, "utf8").trim();
47
- const pid = parseInt(content, 10);
48
- if (Number.isNaN(pid) || pid <= 0) {
49
- // Invalid PID in sentinel — reclaim it
50
- fs.unlinkSync(sentinelPath);
51
- return true;
52
- }
53
- // Check if the process is still alive (signal 0 doesn't kill, just checks)
54
- try {
55
- process.kill(pid, 0);
56
- return false; // Process is alive — lock is valid
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));
57
37
  }
58
- catch {
59
- // Process is dead — reclaim the stale lock
60
- fs.unlinkSync(sentinelPath);
61
- return true;
62
- }
63
- }
64
- catch {
65
- return false; // Can't read or remove — leave it alone
66
38
  }
39
+ // Best-effort: proceed without the lock rather than failing the install.
40
+ return false;
67
41
  }
68
42
  function releaseLockSentinel() {
69
- try {
70
- fs.unlinkSync(getLockSentinelPath());
71
- }
72
- catch {
73
- /* ignore — sentinel may already be gone */
74
- }
43
+ releaseLock(getLockSentinelPath());
75
44
  }
76
45
  // ── Read / Write ────────────────────────────────────────────────────────────
77
46
  export function readLockfile() {
@@ -82,28 +51,20 @@ export function readLockfile() {
82
51
  return [];
83
52
  return raw.filter(isValidLockfileEntry);
84
53
  }
85
- 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);
86
59
  return [];
87
60
  }
88
61
  }
89
62
  export function writeLockfile(entries) {
90
- const lockfilePath = getLockfilePath();
63
+ // Always write to $DATA — never to the legacy $CONFIG location.
64
+ const lockfilePath = path.join(getDataDir(), LOCKFILE_NAME);
91
65
  const dir = path.dirname(lockfilePath);
92
66
  fs.mkdirSync(dir, { recursive: true });
93
- const tmpPath = `${lockfilePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
94
- try {
95
- fs.writeFileSync(tmpPath, `${JSON.stringify(entries, null, 2)}\n`, "utf8");
96
- fs.renameSync(tmpPath, lockfilePath);
97
- }
98
- catch (err) {
99
- try {
100
- fs.unlinkSync(tmpPath);
101
- }
102
- catch {
103
- /* ignore cleanup failure */
104
- }
105
- throw err;
106
- }
67
+ writeFileAtomic(lockfilePath, `${JSON.stringify(entries, null, 2)}\n`);
107
68
  }
108
69
  export async function upsertLockEntry(entry) {
109
70
  const acquired = await acquireLockSentinel();
@@ -118,6 +79,8 @@ export async function upsertLockEntry(entry) {
118
79
  }
119
80
  }
120
81
  export async function removeLockEntry(id) {
82
+ if (!fs.existsSync(getDataDir()))
83
+ return;
121
84
  const acquired = await acquireLockSentinel();
122
85
  try {
123
86
  const entries = readLockfile();
@@ -0,0 +1,69 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { ClaudeCodeProvider } from "./providers/claude-code";
5
+ import { OpenCodeProvider } from "./providers/opencode";
6
+ export { extractInlineRefMentions } from "./inline-refs";
7
+ const HARNESSES = [new ClaudeCodeProvider(), new OpenCodeProvider()];
8
+ const ERROR_PATTERNS = /error|failed|exception|cannot|undefined|null pointer|ENOENT|timeout/i;
9
+ /**
10
+ * Returns all available session log harnesses for the current machine.
11
+ * Add new harnesses to HARNESSES to support additional agent runtimes.
12
+ */
13
+ export function getAvailableHarnesses() {
14
+ return HARNESSES.filter((harness) => harness.isAvailable());
15
+ }
16
+ export function normalizeSessionTopic(text) {
17
+ const normalized = text.replace(/\s+/g, " ").trim().toLowerCase();
18
+ if (normalized.length < 10)
19
+ return undefined;
20
+ return normalized.slice(0, 60);
21
+ }
22
+ export function aggregateSessionEvents(events) {
23
+ const counts = new Map();
24
+ for (const event of events) {
25
+ const topic = normalizeSessionTopic(event.text);
26
+ if (!topic)
27
+ continue;
28
+ const isFailurePattern = ERROR_PATTERNS.test(topic);
29
+ if (!isFailurePattern)
30
+ continue;
31
+ const existing = counts.get(topic) ?? {
32
+ count: 0,
33
+ isFailurePattern,
34
+ sources: new Set(),
35
+ topic,
36
+ };
37
+ existing.count += 1;
38
+ existing.isFailurePattern = existing.isFailurePattern || isFailurePattern;
39
+ existing.sources.add(event.harness);
40
+ counts.set(topic, existing);
41
+ }
42
+ return [...counts.values()]
43
+ .filter((entry) => entry.count >= 2)
44
+ .sort((a, b) => b.count - a.count || a.topic.localeCompare(b.topic))
45
+ .slice(0, 15)
46
+ .map((entry) => ({
47
+ topic: entry.topic,
48
+ frequency: entry.count,
49
+ source: [...entry.sources].sort().join(","),
50
+ isFailurePattern: entry.isFailurePattern,
51
+ }));
52
+ }
53
+ /**
54
+ * Scan recent session logs from all available harnesses and return
55
+ * repeated failure patterns that might warrant new AKM assets.
56
+ */
57
+ export function getExecutionLogCandidates(sinceDays = 7) {
58
+ const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
59
+ const events = [];
60
+ for (const harness of getAvailableHarnesses()) {
61
+ try {
62
+ events.push(...harness.readEvents({ sinceMs }));
63
+ }
64
+ catch {
65
+ // individual harness failures are non-fatal
66
+ }
67
+ }
68
+ return aggregateSessionEvents(events);
69
+ }
@@ -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
+ }