akm-cli 0.7.5 → 0.8.0-rc.6

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 (236) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +113 -2
  2. package/README.md +20 -4
  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.js +1995 -551
  9. package/dist/commands/agent-dispatch.js +110 -0
  10. package/dist/commands/agent-support.js +68 -0
  11. package/dist/commands/completions.js +3 -0
  12. package/dist/commands/config-cli.js +130 -534
  13. package/dist/commands/consolidate.js +1531 -0
  14. package/dist/commands/curate.js +44 -3
  15. package/dist/commands/db-cli.js +23 -0
  16. package/dist/commands/distill-promotion-policy.js +660 -0
  17. package/dist/commands/distill.js +990 -75
  18. package/dist/commands/eval-cases.js +43 -0
  19. package/dist/commands/events.js +5 -23
  20. package/dist/commands/graph.js +477 -0
  21. package/dist/commands/health.js +400 -0
  22. package/dist/commands/help/help-accept.md +9 -0
  23. package/dist/commands/help/help-improve.md +77 -0
  24. package/dist/commands/help/help-proposals.md +15 -0
  25. package/dist/commands/help/help-propose.md +17 -0
  26. package/dist/commands/help/help-reject.md +8 -0
  27. package/dist/commands/history.js +54 -46
  28. package/dist/commands/improve-profiles.js +146 -0
  29. package/dist/commands/improve-result-file.js +103 -0
  30. package/dist/commands/improve.js +2175 -0
  31. package/dist/commands/info.js +5 -2
  32. package/dist/commands/init.js +50 -2
  33. package/dist/commands/installed-stashes.js +102 -139
  34. package/dist/commands/knowledge.js +136 -0
  35. package/dist/commands/lint/agent-linter.js +49 -0
  36. package/dist/commands/lint/base-linter.js +479 -0
  37. package/dist/commands/lint/command-linter.js +49 -0
  38. package/dist/commands/lint/default-linter.js +16 -0
  39. package/dist/commands/lint/index.js +183 -0
  40. package/dist/commands/lint/knowledge-linter.js +16 -0
  41. package/dist/commands/lint/markdown-insertion.js +343 -0
  42. package/dist/commands/lint/memory-linter.js +61 -0
  43. package/dist/commands/lint/registry.js +36 -0
  44. package/dist/commands/lint/skill-linter.js +45 -0
  45. package/dist/commands/lint/task-linter.js +50 -0
  46. package/dist/commands/lint/types.js +4 -0
  47. package/dist/commands/lint/vault-key-rules.js +139 -0
  48. package/dist/commands/lint/workflow-linter.js +56 -0
  49. package/dist/commands/lint.js +4 -0
  50. package/dist/commands/migration-help.js +5 -2
  51. package/dist/commands/proposal.js +66 -12
  52. package/dist/commands/propose.js +86 -31
  53. package/dist/commands/reflect.js +1119 -73
  54. package/dist/commands/registry-search.js +5 -2
  55. package/dist/commands/remember.js +69 -6
  56. package/dist/commands/schema-repair.js +203 -0
  57. package/dist/commands/search.js +115 -14
  58. package/dist/commands/self-update.js +3 -0
  59. package/dist/commands/show.js +144 -25
  60. package/dist/commands/source-add.js +17 -45
  61. package/dist/commands/source-clone.js +3 -0
  62. package/dist/commands/source-manage.js +14 -19
  63. package/dist/commands/tasks.js +438 -0
  64. package/dist/commands/url-checker.js +42 -0
  65. package/dist/commands/vault.js +130 -77
  66. package/dist/core/action-contributors.js +28 -0
  67. package/dist/core/asset-ref.js +7 -0
  68. package/dist/core/asset-registry.js +7 -16
  69. package/dist/core/asset-serialize.js +88 -0
  70. package/dist/core/asset-spec.js +22 -0
  71. package/dist/core/common.js +157 -0
  72. package/dist/core/concurrent.js +25 -0
  73. package/dist/core/config-io.js +347 -0
  74. package/dist/core/config-migration.js +625 -0
  75. package/dist/core/config-schema.js +501 -0
  76. package/dist/core/config-sources.js +108 -0
  77. package/dist/core/config-types.js +4 -0
  78. package/dist/core/config-walker.js +337 -0
  79. package/dist/core/config.js +327 -987
  80. package/dist/core/errors.js +40 -19
  81. package/dist/core/events.js +91 -138
  82. package/dist/core/file-lock.js +104 -0
  83. package/dist/core/frontmatter.js +3 -6
  84. package/dist/core/lesson-lint.js +3 -0
  85. package/dist/core/markdown.js +20 -0
  86. package/dist/core/memory-belief.js +62 -0
  87. package/dist/core/memory-contradiction-detect.js +274 -0
  88. package/dist/core/memory-improve.js +806 -0
  89. package/dist/core/parse.js +158 -0
  90. package/dist/core/paths.js +326 -14
  91. package/dist/core/proposal-quality-validators.js +364 -0
  92. package/dist/core/proposal-validators.js +69 -0
  93. package/dist/core/proposals.js +498 -42
  94. package/dist/core/state-db.js +927 -0
  95. package/dist/core/text-truncation.js +107 -0
  96. package/dist/core/time.js +54 -0
  97. package/dist/core/warn.js +62 -1
  98. package/dist/core/write-source.js +3 -0
  99. package/dist/indexer/db-backup.js +391 -0
  100. package/dist/indexer/db-search.js +152 -253
  101. package/dist/indexer/db.js +933 -103
  102. package/dist/indexer/ensure-index.js +64 -0
  103. package/dist/indexer/file-context.js +3 -0
  104. package/dist/indexer/graph-boost.js +376 -101
  105. package/dist/indexer/graph-db.js +391 -0
  106. package/dist/indexer/graph-dedup.js +95 -0
  107. package/dist/indexer/graph-extraction.js +550 -124
  108. package/dist/indexer/index-context.js +4 -0
  109. package/dist/indexer/indexer.js +506 -291
  110. package/dist/indexer/llm-cache.js +47 -0
  111. package/dist/indexer/manifest.js +3 -0
  112. package/dist/indexer/matchers.js +148 -160
  113. package/dist/indexer/memory-inference.js +99 -74
  114. package/dist/indexer/metadata-contributors.js +29 -0
  115. package/dist/indexer/metadata.js +255 -196
  116. package/dist/indexer/path-resolver.js +92 -0
  117. package/dist/indexer/project-context.js +192 -0
  118. package/dist/indexer/ranking-contributors.js +331 -0
  119. package/dist/indexer/ranking.js +81 -0
  120. package/dist/indexer/search-fields.js +5 -9
  121. package/dist/indexer/search-hit-enrichers.js +111 -0
  122. package/dist/indexer/search-source.js +44 -10
  123. package/dist/indexer/semantic-status.js +5 -16
  124. package/dist/indexer/staleness-detect.js +447 -0
  125. package/dist/indexer/usage-events.js +12 -9
  126. package/dist/indexer/walker.js +28 -0
  127. package/dist/integrations/agent/builders.js +135 -0
  128. package/dist/integrations/agent/config.js +122 -230
  129. package/dist/integrations/agent/detect.js +3 -0
  130. package/dist/integrations/agent/index.js +7 -13
  131. package/dist/integrations/agent/model-aliases.js +55 -0
  132. package/dist/integrations/agent/profiles.js +70 -5
  133. package/dist/integrations/agent/prompts.js +150 -74
  134. package/dist/integrations/agent/runner.js +151 -0
  135. package/dist/integrations/agent/sdk-runner.js +126 -0
  136. package/dist/integrations/agent/spawn.js +118 -23
  137. package/dist/integrations/github.js +3 -0
  138. package/dist/integrations/lockfile.js +32 -69
  139. package/dist/integrations/session-logs/index.js +68 -0
  140. package/dist/integrations/session-logs/providers/claude-code.js +59 -0
  141. package/dist/integrations/session-logs/providers/opencode.js +55 -0
  142. package/dist/integrations/session-logs/types.js +4 -0
  143. package/dist/llm/call-ai.js +62 -0
  144. package/dist/llm/client.js +72 -124
  145. package/dist/llm/embedder.js +3 -19
  146. package/dist/llm/embedders/cache.js +3 -7
  147. package/dist/llm/embedders/local.js +3 -0
  148. package/dist/llm/embedders/remote.js +20 -8
  149. package/dist/llm/embedders/types.js +3 -7
  150. package/dist/llm/feature-gate.js +89 -48
  151. package/dist/llm/graph-extract.js +676 -70
  152. package/dist/llm/index-passes.js +9 -23
  153. package/dist/llm/memory-infer.js +52 -71
  154. package/dist/llm/metadata-enhance.js +42 -29
  155. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  156. package/dist/output/cli-hints-full.md +281 -0
  157. package/dist/output/cli-hints-short.md +65 -0
  158. package/dist/output/cli-hints.js +5 -318
  159. package/dist/output/context.js +3 -0
  160. package/dist/output/renderers.js +223 -256
  161. package/dist/output/shapes.js +150 -105
  162. package/dist/output/text.js +318 -30
  163. package/dist/registry/build-index.js +3 -0
  164. package/dist/registry/create-provider-registry.js +3 -0
  165. package/dist/registry/factory.js +3 -0
  166. package/dist/registry/origin-resolve.js +3 -0
  167. package/dist/registry/providers/index.js +3 -0
  168. package/dist/registry/providers/skills-sh.js +70 -49
  169. package/dist/registry/providers/static-index.js +53 -48
  170. package/dist/registry/providers/types.js +3 -24
  171. package/dist/registry/resolve.js +11 -16
  172. package/dist/registry/types.js +3 -0
  173. package/dist/scripts/migrate-storage.js +17307 -0
  174. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -0
  175. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  176. package/dist/setup/detect.js +3 -0
  177. package/dist/setup/ripgrep-install.js +3 -0
  178. package/dist/setup/ripgrep-resolve.js +3 -0
  179. package/dist/setup/setup.js +775 -37
  180. package/dist/setup/steps.js +3 -15
  181. package/dist/sources/include.js +3 -0
  182. package/dist/sources/provider-factory.js +5 -12
  183. package/dist/sources/provider.js +3 -20
  184. package/dist/sources/providers/filesystem.js +19 -23
  185. package/dist/sources/providers/git.js +7 -5
  186. package/dist/sources/providers/index.js +3 -0
  187. package/dist/sources/providers/install-types.js +3 -13
  188. package/dist/sources/providers/npm.js +3 -4
  189. package/dist/sources/providers/provider-utils.js +3 -0
  190. package/dist/sources/providers/sync-from-ref.js +3 -11
  191. package/dist/sources/providers/tar-utils.js +3 -0
  192. package/dist/sources/providers/website.js +18 -22
  193. package/dist/sources/resolve.js +3 -0
  194. package/dist/sources/types.js +3 -0
  195. package/dist/sources/website-ingest.js +7 -0
  196. package/dist/tasks/backends/cron.js +203 -0
  197. package/dist/tasks/backends/exec-utils.js +28 -0
  198. package/dist/tasks/backends/index.js +24 -0
  199. package/dist/tasks/backends/launchd-template.xml +19 -0
  200. package/dist/tasks/backends/launchd.js +187 -0
  201. package/dist/tasks/backends/schtasks-template.xml +29 -0
  202. package/dist/tasks/backends/schtasks.js +215 -0
  203. package/dist/tasks/parser.js +211 -0
  204. package/dist/tasks/resolveAkmBin.js +87 -0
  205. package/dist/tasks/runner.js +458 -0
  206. package/dist/tasks/schedule.js +211 -0
  207. package/dist/tasks/schema.js +15 -0
  208. package/dist/tasks/validator.js +62 -0
  209. package/dist/version.js +3 -0
  210. package/dist/wiki/index-template.md +12 -0
  211. package/dist/wiki/ingest-workflow-template.md +54 -0
  212. package/dist/wiki/log-template.md +8 -0
  213. package/dist/wiki/schema-template.md +61 -0
  214. package/dist/wiki/wiki-templates.js +15 -0
  215. package/dist/wiki/wiki.js +13 -61
  216. package/dist/workflows/authoring.js +8 -25
  217. package/dist/workflows/cli.js +3 -0
  218. package/dist/workflows/db.js +140 -10
  219. package/dist/workflows/document-cache.js +3 -10
  220. package/dist/workflows/parser.js +3 -0
  221. package/dist/workflows/renderer.js +11 -3
  222. package/dist/workflows/runs.js +62 -91
  223. package/dist/workflows/schema.js +3 -0
  224. package/dist/workflows/scope-key.js +3 -0
  225. package/dist/workflows/validator.js +4 -8
  226. package/dist/workflows/workflow-template.md +24 -0
  227. package/docs/README.md +9 -2
  228. package/docs/data-and-telemetry.md +225 -0
  229. package/docs/migration/release-notes/0.7.0.md +1 -1
  230. package/docs/migration/release-notes/0.7.5.md +2 -2
  231. package/docs/migration/release-notes/0.8.0.md +48 -0
  232. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  233. package/package.json +20 -8
  234. package/.github/LICENSE +0 -374
  235. package/dist/commands/install-audit.js +0 -381
  236. 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) {
@@ -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,68 @@
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
+ const HARNESSES = [new ClaudeCodeProvider(), new OpenCodeProvider()];
7
+ const ERROR_PATTERNS = /error|failed|exception|cannot|undefined|null pointer|ENOENT|timeout/i;
8
+ /**
9
+ * Returns all available session log harnesses for the current machine.
10
+ * Add new harnesses to HARNESSES to support additional agent runtimes.
11
+ */
12
+ export function getAvailableHarnesses() {
13
+ return HARNESSES.filter((harness) => harness.isAvailable());
14
+ }
15
+ export function normalizeSessionTopic(text) {
16
+ const normalized = text.replace(/\s+/g, " ").trim().toLowerCase();
17
+ if (normalized.length < 10)
18
+ return undefined;
19
+ return normalized.slice(0, 60);
20
+ }
21
+ export function aggregateSessionEvents(events) {
22
+ const counts = new Map();
23
+ for (const event of events) {
24
+ const topic = normalizeSessionTopic(event.text);
25
+ if (!topic)
26
+ continue;
27
+ const isFailurePattern = ERROR_PATTERNS.test(topic);
28
+ if (!isFailurePattern)
29
+ continue;
30
+ const existing = counts.get(topic) ?? {
31
+ count: 0,
32
+ isFailurePattern,
33
+ sources: new Set(),
34
+ topic,
35
+ };
36
+ existing.count += 1;
37
+ existing.isFailurePattern = existing.isFailurePattern || isFailurePattern;
38
+ existing.sources.add(event.harness);
39
+ counts.set(topic, existing);
40
+ }
41
+ return [...counts.values()]
42
+ .filter((entry) => entry.count >= 2)
43
+ .sort((a, b) => b.count - a.count || a.topic.localeCompare(b.topic))
44
+ .slice(0, 15)
45
+ .map((entry) => ({
46
+ topic: entry.topic,
47
+ frequency: entry.count,
48
+ source: [...entry.sources].sort().join(","),
49
+ isFailurePattern: entry.isFailurePattern,
50
+ }));
51
+ }
52
+ /**
53
+ * Scan recent session logs from all available harnesses and return
54
+ * repeated failure patterns that might warrant new AKM assets.
55
+ */
56
+ export function getExecutionLogCandidates(sinceDays = 7) {
57
+ const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
58
+ const events = [];
59
+ for (const harness of getAvailableHarnesses()) {
60
+ try {
61
+ events.push(...harness.readEvents({ sinceMs }));
62
+ }
63
+ catch {
64
+ // individual harness failures are non-fatal
65
+ }
66
+ }
67
+ return aggregateSessionEvents(events);
68
+ }
@@ -0,0 +1,59 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
8
+ export class ClaudeCodeProvider {
9
+ name = "claude-code";
10
+ isAvailable() {
11
+ return fs.existsSync(CLAUDE_PROJECTS_DIR);
12
+ }
13
+ *readEvents(input) {
14
+ try {
15
+ for (const jsonlPath of this.#walkJsonl(CLAUDE_PROJECTS_DIR)) {
16
+ const stat = fs.statSync(jsonlPath);
17
+ if (stat.mtimeMs < input.sinceMs)
18
+ continue;
19
+ const lines = fs.readFileSync(jsonlPath, "utf8").split("\n").filter(Boolean);
20
+ for (const line of lines) {
21
+ try {
22
+ const entry = JSON.parse(line);
23
+ const text = entry?.message?.content ?? entry?.content ?? "";
24
+ if (typeof text !== "string" || text.length < 10)
25
+ continue;
26
+ yield {
27
+ harness: this.name,
28
+ text,
29
+ ts: typeof entry?.timestamp === "number" ? entry.timestamp : stat.mtimeMs,
30
+ sessionId: typeof entry?.session_id === "string" ? entry.session_id : undefined,
31
+ role: typeof entry?.role === "string" ? entry.role : "unknown",
32
+ filePath: jsonlPath,
33
+ };
34
+ }
35
+ catch {
36
+ // skip malformed lines
37
+ }
38
+ }
39
+ }
40
+ }
41
+ catch {
42
+ return;
43
+ }
44
+ }
45
+ *#walkJsonl(dir) {
46
+ try {
47
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
48
+ const full = path.join(dir, entry.name);
49
+ if (entry.isDirectory())
50
+ yield* this.#walkJsonl(full);
51
+ else if (entry.name.endsWith(".jsonl"))
52
+ yield full;
53
+ }
54
+ }
55
+ catch {
56
+ // permission errors etc.
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,55 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ function getOpenCodeLogDir() {
8
+ if (process.platform === "darwin") {
9
+ return path.join(os.homedir(), "Library", "Application Support", "opencode");
10
+ }
11
+ return path.join(os.homedir(), ".local", "share", "opencode");
12
+ }
13
+ export class OpenCodeProvider {
14
+ name = "opencode";
15
+ #logDir = getOpenCodeLogDir();
16
+ isAvailable() {
17
+ return fs.existsSync(this.#logDir);
18
+ }
19
+ *readEvents(input) {
20
+ try {
21
+ for (const file of fs.readdirSync(this.#logDir)) {
22
+ const full = path.join(this.#logDir, file);
23
+ const stat = fs.statSync(full);
24
+ if (stat.mtimeMs < input.sinceMs)
25
+ continue;
26
+ if (!file.endsWith(".json") && !file.endsWith(".jsonl") && !file.endsWith(".log"))
27
+ continue;
28
+ const content = fs.readFileSync(full, "utf8");
29
+ const lines = content.includes("\n") ? content.split("\n") : [content];
30
+ for (const line of lines) {
31
+ try {
32
+ const entry = JSON.parse(line);
33
+ const text = entry?.content ?? entry?.message ?? entry?.text ?? "";
34
+ if (typeof text !== "string" || text.length < 10)
35
+ continue;
36
+ yield {
37
+ harness: this.name,
38
+ text,
39
+ ts: typeof entry?.timestamp === "number" ? entry.timestamp : stat.mtimeMs,
40
+ sessionId: typeof entry?.sessionId === "string" ? entry.sessionId : undefined,
41
+ role: typeof entry?.role === "string" ? entry.role : "unknown",
42
+ filePath: full,
43
+ };
44
+ }
45
+ catch {
46
+ // skip malformed
47
+ }
48
+ }
49
+ }
50
+ }
51
+ catch {
52
+ return;
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,4 @@
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
+ export {};
@@ -0,0 +1,62 @@
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 { getDefaultLlmConfig } from "../core/config";
5
+ import { warn } from "../core/warn";
6
+ import { resolveAgentProfile, runAgent } from "../integrations/agent";
7
+ import { chatCompletion } from "./client";
8
+ /**
9
+ * Unified AI call: prefers the default agent profile, falls back to the
10
+ * default LLM profile. When neither is configured, returns a structured
11
+ * error pointing the user at `akm setup`.
12
+ */
13
+ export async function callAi(config, prompt, opts = {}) {
14
+ const defaultAgentName = config.defaults?.agent;
15
+ if (defaultAgentName) {
16
+ try {
17
+ const profile = resolveAgentProfile(defaultAgentName, config.profiles?.agent?.[defaultAgentName]);
18
+ if (!profile) {
19
+ return {
20
+ ok: false,
21
+ error: `Agent profile "${defaultAgentName}" is not built-in and has no \`bin\` override.`,
22
+ };
23
+ }
24
+ const result = await runAgent(profile, prompt, {
25
+ stdio: opts.draftFilePath ? "interactive" : "captured",
26
+ parseOutput: "text",
27
+ timeoutMs: opts.timeoutMs,
28
+ });
29
+ if (!result.ok)
30
+ return { ok: false, error: result.error ?? result.reason ?? "agent failed" };
31
+ return { ok: true, content: result.stdout ?? "", path: "agent-cli" };
32
+ }
33
+ catch (e) {
34
+ return { ok: false, error: String(e) };
35
+ }
36
+ }
37
+ const llmConfig = getDefaultLlmConfig(config);
38
+ if (llmConfig) {
39
+ if (opts.draftFilePath) {
40
+ warn("[akm] No agent CLI configured — falling back to LLM API. " +
41
+ "File-write contract unavailable; expecting JSON in stdout. " +
42
+ "Install an agent CLI and run `akm setup` for full functionality.");
43
+ }
44
+ const messages = [];
45
+ if (opts.systemPrompt)
46
+ messages.push({ role: "system", content: opts.systemPrompt });
47
+ messages.push({ role: "user", content: prompt });
48
+ try {
49
+ const content = await chatCompletion(llmConfig, messages, {
50
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
51
+ });
52
+ return { ok: true, content, path: "llm-http" };
53
+ }
54
+ catch (e) {
55
+ return { ok: false, error: String(e) };
56
+ }
57
+ }
58
+ return {
59
+ ok: false,
60
+ error: "No AI connection configured. Run `akm setup` or set `defaults.agent`/`defaults.llm`.",
61
+ };
62
+ }