akm-cli 0.8.0-rc2 → 0.8.1

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 (313) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +238 -3
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/assets/help/help-accept.md +12 -0
  5. package/dist/assets/help/help-improve.md +81 -0
  6. package/dist/{commands → assets}/help/help-proposals.md +7 -4
  7. package/dist/assets/help/help-reject.md +11 -0
  8. package/dist/{output → assets/hints}/cli-hints-full.md +60 -32
  9. package/dist/{output → assets/hints}/cli-hints-short.md +10 -7
  10. package/dist/assets/profiles/default.json +15 -0
  11. package/dist/assets/profiles/graph-refresh.json +13 -0
  12. package/dist/assets/profiles/memory-focus.json +12 -0
  13. package/dist/assets/profiles/quick.json +15 -0
  14. package/dist/assets/profiles/thorough.json +15 -0
  15. package/dist/assets/prompts/extract-session.md +80 -0
  16. package/dist/assets/prompts/graph-extract-user-prompt.md +35 -0
  17. package/dist/assets/tasks/graph-refresh-weekly.yml +10 -0
  18. package/dist/cli/config-migrate.js +144 -0
  19. package/dist/cli/config-validate.js +39 -0
  20. package/dist/cli/confirm.js +73 -0
  21. package/dist/cli/parse-args.js +93 -3
  22. package/dist/cli/shared.js +129 -0
  23. package/dist/cli.js +2141 -1268
  24. package/dist/commands/add-cli.js +279 -0
  25. package/dist/commands/agent-dispatch.js +20 -12
  26. package/dist/commands/agent-support.js +11 -5
  27. package/dist/commands/completions.js +3 -0
  28. package/dist/commands/config-cli.js +129 -517
  29. package/dist/commands/consolidate.js +1557 -147
  30. package/dist/commands/curate.js +44 -3
  31. package/dist/commands/db-cli.js +23 -0
  32. package/dist/commands/distill-promotion-policy.js +5 -3
  33. package/dist/commands/distill.js +906 -100
  34. package/dist/commands/env.js +213 -0
  35. package/dist/commands/eval-cases.js +3 -0
  36. package/dist/commands/events.js +3 -0
  37. package/dist/commands/extract-cli.js +127 -0
  38. package/dist/commands/extract-prompt.js +217 -0
  39. package/dist/commands/extract.js +477 -0
  40. package/dist/commands/feedback-cli.js +331 -0
  41. package/dist/commands/graph.js +260 -5
  42. package/dist/commands/health.js +1042 -55
  43. package/dist/commands/history.js +51 -16
  44. package/dist/commands/improve-auto-accept.js +97 -0
  45. package/dist/commands/improve-cli.js +236 -0
  46. package/dist/commands/improve-profiles.js +138 -0
  47. package/dist/commands/improve-result-file.js +167 -0
  48. package/dist/commands/improve.js +1736 -346
  49. package/dist/commands/info.js +26 -28
  50. package/dist/commands/init.js +49 -1
  51. package/dist/commands/installed-stashes.js +6 -23
  52. package/dist/commands/knowledge.js +3 -0
  53. package/dist/commands/lint/agent-linter.js +3 -0
  54. package/dist/commands/lint/base-linter.js +199 -5
  55. package/dist/commands/lint/command-linter.js +3 -0
  56. package/dist/commands/lint/default-linter.js +3 -0
  57. package/dist/commands/lint/env-key-rules.js +154 -0
  58. package/dist/commands/lint/index.js +92 -3
  59. package/dist/commands/lint/knowledge-linter.js +3 -0
  60. package/dist/commands/lint/markdown-insertion.js +343 -0
  61. package/dist/commands/lint/memory-linter.js +3 -0
  62. package/dist/commands/lint/registry.js +3 -0
  63. package/dist/commands/lint/skill-linter.js +3 -0
  64. package/dist/commands/lint/task-linter.js +15 -12
  65. package/dist/commands/lint/types.js +3 -0
  66. package/dist/commands/lint/workflow-linter.js +3 -0
  67. package/dist/commands/lint.js +3 -0
  68. package/dist/commands/migration-help.js +5 -2
  69. package/dist/commands/proposal-drain-policies.js +128 -0
  70. package/dist/commands/proposal-drain.js +477 -0
  71. package/dist/commands/proposal.js +60 -6
  72. package/dist/commands/propose.js +24 -19
  73. package/dist/commands/reflect.js +1004 -94
  74. package/dist/commands/registry-cli.js +150 -0
  75. package/dist/commands/registry-search.js +3 -0
  76. package/dist/commands/remember-cli.js +257 -0
  77. package/dist/commands/remember.js +15 -6
  78. package/dist/commands/schema-repair.js +88 -15
  79. package/dist/commands/search.js +99 -14
  80. package/dist/commands/secret.js +173 -0
  81. package/dist/commands/self-update.js +3 -0
  82. package/dist/commands/show.js +32 -13
  83. package/dist/commands/source-add.js +7 -35
  84. package/dist/commands/source-clone.js +3 -0
  85. package/dist/commands/source-manage.js +3 -0
  86. package/dist/commands/tasks.js +161 -95
  87. package/dist/commands/url-checker.js +3 -0
  88. package/dist/core/action-contributors.js +3 -0
  89. package/dist/core/asset-ref.js +13 -2
  90. package/dist/core/asset-registry.js +9 -2
  91. package/dist/core/asset-serialize.js +88 -0
  92. package/dist/core/asset-spec.js +61 -5
  93. package/dist/core/common.js +93 -5
  94. package/dist/core/concurrent.js +3 -0
  95. package/dist/core/config-io.js +347 -0
  96. package/dist/core/config-migration.js +622 -0
  97. package/dist/core/config-schema.js +558 -0
  98. package/dist/core/config-sources.js +108 -0
  99. package/dist/core/config-types.js +4 -0
  100. package/dist/core/config-walker.js +337 -0
  101. package/dist/core/config.js +366 -1077
  102. package/dist/core/errors.js +42 -20
  103. package/dist/core/events.js +31 -25
  104. package/dist/core/file-lock.js +104 -0
  105. package/dist/core/frontmatter.js +75 -10
  106. package/dist/core/lesson-lint.js +3 -0
  107. package/dist/core/markdown.js +3 -0
  108. package/dist/core/memory-belief.js +62 -0
  109. package/dist/core/memory-contradiction-detect.js +274 -0
  110. package/dist/core/memory-improve.js +142 -14
  111. package/dist/core/parse.js +3 -0
  112. package/dist/core/paths.js +218 -50
  113. package/dist/core/proposal-quality-validators.js +380 -0
  114. package/dist/core/proposal-validators.js +11 -3
  115. package/dist/core/proposals.js +464 -5
  116. package/dist/core/state-db.js +349 -56
  117. package/dist/core/text-truncation.js +107 -0
  118. package/dist/core/time.js +3 -0
  119. package/dist/core/tty.js +59 -0
  120. package/dist/core/warn.js +7 -2
  121. package/dist/core/write-source.js +12 -0
  122. package/dist/indexer/db-backup.js +391 -0
  123. package/dist/indexer/db-search.js +136 -28
  124. package/dist/indexer/db.js +661 -166
  125. package/dist/indexer/ensure-index.js +3 -0
  126. package/dist/indexer/file-context.js +3 -0
  127. package/dist/indexer/graph-boost.js +162 -40
  128. package/dist/indexer/graph-db.js +241 -51
  129. package/dist/indexer/graph-dedup.js +3 -7
  130. package/dist/indexer/graph-extraction.js +242 -149
  131. package/dist/indexer/index-context.js +3 -9
  132. package/dist/indexer/indexer.js +86 -16
  133. package/dist/indexer/llm-cache.js +24 -19
  134. package/dist/indexer/manifest.js +3 -0
  135. package/dist/indexer/matchers.js +184 -11
  136. package/dist/indexer/memory-inference.js +94 -50
  137. package/dist/indexer/metadata-contributors.js +3 -0
  138. package/dist/indexer/metadata.js +110 -50
  139. package/dist/indexer/path-resolver.js +3 -0
  140. package/dist/indexer/project-context.js +192 -0
  141. package/dist/indexer/ranking-contributors.js +134 -7
  142. package/dist/indexer/ranking.js +8 -1
  143. package/dist/indexer/search-fields.js +5 -9
  144. package/dist/indexer/search-hit-enrichers.js +91 -2
  145. package/dist/indexer/search-source.js +20 -1
  146. package/dist/indexer/semantic-status.js +4 -1
  147. package/dist/indexer/staleness-detect.js +447 -0
  148. package/dist/indexer/usage-events.js +12 -9
  149. package/dist/indexer/walker.js +3 -0
  150. package/dist/integrations/agent/builders.js +135 -0
  151. package/dist/integrations/agent/config.js +121 -401
  152. package/dist/integrations/agent/detect.js +3 -0
  153. package/dist/integrations/agent/index.js +6 -14
  154. package/dist/integrations/agent/model-aliases.js +55 -0
  155. package/dist/integrations/agent/profiles.js +3 -0
  156. package/dist/integrations/agent/prompts.js +137 -8
  157. package/dist/integrations/agent/runner.js +208 -0
  158. package/dist/integrations/agent/sdk-runner.js +8 -2
  159. package/dist/integrations/agent/spawn.js +54 -14
  160. package/dist/integrations/github.js +3 -0
  161. package/dist/integrations/lockfile.js +22 -51
  162. package/dist/integrations/session-logs/index.js +4 -0
  163. package/dist/integrations/session-logs/inline-refs.js +35 -0
  164. package/dist/integrations/session-logs/pre-filter.js +152 -0
  165. package/dist/integrations/session-logs/providers/claude-code.js +226 -0
  166. package/dist/integrations/session-logs/providers/opencode.js +231 -25
  167. package/dist/integrations/session-logs/types.js +3 -0
  168. package/dist/llm/call-ai.js +14 -26
  169. package/dist/llm/client.js +16 -2
  170. package/dist/llm/embedder.js +20 -29
  171. package/dist/llm/embedders/cache.js +3 -7
  172. package/dist/llm/embedders/local.js +42 -1
  173. package/dist/llm/embedders/remote.js +20 -8
  174. package/dist/llm/embedders/types.js +3 -7
  175. package/dist/llm/feature-gate.js +92 -56
  176. package/dist/llm/graph-extract.js +402 -31
  177. package/dist/llm/index-passes.js +44 -29
  178. package/dist/llm/memory-infer.js +30 -2
  179. package/dist/llm/metadata-enhance.js +3 -7
  180. package/dist/output/cli-hints.js +7 -4
  181. package/dist/output/context.js +60 -8
  182. package/dist/output/renderers.js +170 -194
  183. package/dist/output/shapes/curate.js +56 -0
  184. package/dist/output/shapes/distill.js +10 -0
  185. package/dist/output/shapes/env-list.js +19 -0
  186. package/dist/output/shapes/events.js +11 -0
  187. package/dist/output/shapes/helpers.js +424 -0
  188. package/dist/output/shapes/history.js +7 -0
  189. package/dist/output/shapes/passthrough.js +105 -0
  190. package/dist/output/shapes/proposal-accept.js +7 -0
  191. package/dist/output/shapes/proposal-diff.js +7 -0
  192. package/dist/output/shapes/proposal-list.js +7 -0
  193. package/dist/output/shapes/proposal-producer.js +11 -0
  194. package/dist/output/shapes/proposal-reject.js +7 -0
  195. package/dist/output/shapes/proposal-show.js +7 -0
  196. package/dist/output/shapes/registry-search.js +6 -0
  197. package/dist/output/shapes/registry.js +30 -0
  198. package/dist/output/shapes/search.js +6 -0
  199. package/dist/output/shapes/secret-list.js +19 -0
  200. package/dist/output/shapes/show.js +6 -0
  201. package/dist/output/shapes/vault-list.js +19 -0
  202. package/dist/output/shapes.js +51 -549
  203. package/dist/output/text/add.js +6 -0
  204. package/dist/output/text/clone.js +6 -0
  205. package/dist/output/text/config.js +6 -0
  206. package/dist/output/text/curate.js +6 -0
  207. package/dist/output/text/distill.js +7 -0
  208. package/dist/output/text/enable-disable.js +7 -0
  209. package/dist/output/text/events.js +10 -0
  210. package/dist/output/text/feedback.js +6 -0
  211. package/dist/output/text/helpers.js +1059 -0
  212. package/dist/output/text/history.js +7 -0
  213. package/dist/output/text/import.js +6 -0
  214. package/dist/output/text/index.js +6 -0
  215. package/dist/output/text/info.js +6 -0
  216. package/dist/output/text/init.js +6 -0
  217. package/dist/output/text/list.js +6 -0
  218. package/dist/output/text/proposal-producer.js +8 -0
  219. package/dist/output/text/proposal.js +12 -0
  220. package/dist/output/text/registry-commands.js +11 -0
  221. package/dist/output/text/registry.js +30 -0
  222. package/dist/output/text/remember.js +6 -0
  223. package/dist/output/text/remove.js +6 -0
  224. package/dist/output/text/save.js +6 -0
  225. package/dist/output/text/search.js +6 -0
  226. package/dist/output/text/show.js +6 -0
  227. package/dist/output/text/update.js +6 -0
  228. package/dist/output/text/upgrade.js +6 -0
  229. package/dist/output/text/vault.js +16 -0
  230. package/dist/output/text/wiki.js +15 -0
  231. package/dist/output/text/workflow.js +14 -0
  232. package/dist/output/text.js +44 -1329
  233. package/dist/registry/build-index.js +3 -0
  234. package/dist/registry/create-provider-registry.js +3 -0
  235. package/dist/registry/factory.js +4 -1
  236. package/dist/registry/origin-resolve.js +3 -0
  237. package/dist/registry/providers/index.js +3 -0
  238. package/dist/registry/providers/skills-sh.js +11 -2
  239. package/dist/registry/providers/static-index.js +10 -1
  240. package/dist/registry/providers/types.js +3 -24
  241. package/dist/registry/resolve.js +11 -16
  242. package/dist/registry/types.js +3 -0
  243. package/dist/scripts/migrate-storage.js +17767 -0
  244. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  245. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  246. package/dist/setup/detect.js +3 -0
  247. package/dist/setup/ripgrep-install.js +3 -0
  248. package/dist/setup/ripgrep-resolve.js +3 -0
  249. package/dist/setup/setup.js +306 -67
  250. package/dist/setup/steps.js +3 -15
  251. package/dist/sources/include.js +3 -0
  252. package/dist/sources/provider-factory.js +3 -11
  253. package/dist/sources/provider.js +3 -20
  254. package/dist/sources/providers/filesystem.js +19 -23
  255. package/dist/sources/providers/git.js +171 -21
  256. package/dist/sources/providers/index.js +3 -0
  257. package/dist/sources/providers/install-types.js +3 -13
  258. package/dist/sources/providers/npm.js +3 -4
  259. package/dist/sources/providers/provider-utils.js +3 -0
  260. package/dist/sources/providers/sync-from-ref.js +3 -11
  261. package/dist/sources/providers/tar-utils.js +3 -0
  262. package/dist/sources/providers/website.js +18 -22
  263. package/dist/sources/resolve.js +3 -0
  264. package/dist/sources/types.js +3 -0
  265. package/dist/sources/website-ingest.js +3 -0
  266. package/dist/tasks/backends/cron.js +3 -0
  267. package/dist/tasks/backends/exec-utils.js +3 -0
  268. package/dist/tasks/backends/index.js +3 -11
  269. package/dist/tasks/backends/launchd.js +4 -1
  270. package/dist/tasks/backends/schtasks.js +4 -1
  271. package/dist/tasks/parser.js +51 -38
  272. package/dist/tasks/resolveAkmBin.js +3 -0
  273. package/dist/tasks/runner.js +35 -9
  274. package/dist/tasks/schedule.js +20 -1
  275. package/dist/tasks/schema.js +5 -3
  276. package/dist/tasks/validator.js +6 -3
  277. package/dist/version.js +3 -0
  278. package/dist/wiki/wiki-templates.js +6 -3
  279. package/dist/wiki/wiki.js +4 -1
  280. package/dist/workflows/authoring.js +4 -1
  281. package/dist/workflows/cli.js +3 -0
  282. package/dist/workflows/db.js +140 -10
  283. package/dist/workflows/document-cache.js +3 -10
  284. package/dist/workflows/parser.js +3 -0
  285. package/dist/workflows/renderer.js +3 -0
  286. package/dist/workflows/runs.js +18 -1
  287. package/dist/workflows/schema.js +3 -0
  288. package/dist/workflows/scope-key.js +3 -0
  289. package/dist/workflows/validator.js +5 -9
  290. package/docs/README.md +7 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.5.md +2 -2
  293. package/docs/migration/release-notes/0.8.0.md +57 -5
  294. package/docs/migration/v0.7-to-v0.8.md +1378 -0
  295. package/package.json +28 -11
  296. package/.github/LICENSE +0 -374
  297. package/dist/commands/help/help-accept.md +0 -9
  298. package/dist/commands/help/help-improve.md +0 -53
  299. package/dist/commands/help/help-reject.md +0 -8
  300. package/dist/commands/install-audit.js +0 -385
  301. package/dist/commands/vault.js +0 -310
  302. package/dist/indexer/match-contributors.js +0 -141
  303. package/dist/integrations/agent/pipeline.js +0 -39
  304. package/dist/integrations/agent/runners.js +0 -31
  305. package/dist/llm/prompts/graph-extract-user-prompt.md +0 -12
  306. /package/dist/{tasks → assets}/backends/launchd-template.xml +0 -0
  307. /package/dist/{tasks → assets}/backends/schtasks-template.xml +0 -0
  308. /package/dist/{commands → assets}/help/help-propose.md +0 -0
  309. /package/dist/{wiki → assets/wiki}/index-template.md +0 -0
  310. /package/dist/{wiki → assets/wiki}/ingest-workflow-template.md +0 -0
  311. /package/dist/{wiki → assets/wiki}/log-template.md +0 -0
  312. /package/dist/{wiki → assets/wiki}/schema-template.md +0 -0
  313. /package/dist/{workflows → assets/workflows}/workflow-template.md +0 -0
@@ -1,9 +1,14 @@
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";
6
+ import { parse as parseYaml } from "yaml";
3
7
  import { resolveStashDir } from "../../core/common";
4
8
  import { loadConfig } from "../../core/config";
5
9
  import { parseFrontmatter } from "../../core/frontmatter";
6
10
  import { resolveSourceEntries } from "../../indexer/search-source";
11
+ import { checkVaultForDangerousKeys } from "./env-key-rules";
7
12
  import { getLinterForType } from "./registry";
8
13
  // ── Constants ─────────────────────────────────────────────────────────────────
9
14
  const STASH_SUBDIRS = [
@@ -17,6 +22,21 @@ const STASH_SUBDIRS = [
17
22
  "knowledge",
18
23
  ];
19
24
  // ── Helpers ───────────────────────────────────────────────────────────────────
25
+ function collectYamlFiles(dir) {
26
+ if (!fs.existsSync(dir))
27
+ return [];
28
+ const results = [];
29
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
30
+ const full = path.join(dir, entry.name);
31
+ if (entry.isDirectory()) {
32
+ results.push(...collectYamlFiles(full));
33
+ }
34
+ else if (entry.isFile() && entry.name.endsWith(".yml")) {
35
+ results.push(full);
36
+ }
37
+ }
38
+ return results;
39
+ }
20
40
  function collectMarkdownFiles(dir) {
21
41
  if (!fs.existsSync(dir))
22
42
  return [];
@@ -32,6 +52,22 @@ function collectMarkdownFiles(dir) {
32
52
  }
33
53
  return results;
34
54
  }
55
+ function collectEnvFiles(dir) {
56
+ const results = [];
57
+ try {
58
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
59
+ const full = path.join(dir, entry.name);
60
+ if (entry.isDirectory())
61
+ results.push(...collectEnvFiles(full));
62
+ else if (entry.isFile() && entry.name.endsWith(".env"))
63
+ results.push(full);
64
+ }
65
+ }
66
+ catch {
67
+ /* dir may not exist */
68
+ }
69
+ return results;
70
+ }
35
71
  /** True when the issue represents a file deletion that was successfully applied. */
36
72
  function isFileDeletion(issue) {
37
73
  return issue.fixed === true && (issue.issue === "orphaned-stub" || issue.issue === "placeholder-stub");
@@ -51,7 +87,8 @@ export function akmLint(options = {}) {
51
87
  const flagged = [];
52
88
  for (const subdir of STASH_SUBDIRS) {
53
89
  const dirPath = path.join(stashRoot, subdir);
54
- const files = collectMarkdownFiles(dirPath);
90
+ // Tasks are .yml files; everything else is .md
91
+ const files = subdir === "tasks" ? collectYamlFiles(dirPath) : collectMarkdownFiles(dirPath);
55
92
  const linter = getLinterForType(subdir);
56
93
  // If the linter supports directory-level checks, run them for each direct
57
94
  // subdirectory once before the per-file loop.
@@ -79,7 +116,25 @@ export function akmLint(options = {}) {
79
116
  catch {
80
117
  continue;
81
118
  }
82
- const { data, content: body, frontmatter } = parseFrontmatter(raw);
119
+ let data;
120
+ let body;
121
+ let frontmatter;
122
+ if (subdir === "tasks") {
123
+ // Task files are pure YAML — parseFrontmatter returns empty data for them.
124
+ try {
125
+ const parsed = parseYaml(raw);
126
+ data =
127
+ parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
128
+ }
129
+ catch {
130
+ data = {};
131
+ }
132
+ body = raw;
133
+ frontmatter = null;
134
+ }
135
+ else {
136
+ ({ data, content: body, frontmatter } = parseFrontmatter(raw));
137
+ }
83
138
  const issues = linter.lint({ filePath, relPath, raw, data, body, frontmatter, fix, stashRoot, extraStashRoots });
84
139
  let fileDeleted = false;
85
140
  for (const issue of issues) {
@@ -98,8 +153,42 @@ export function akmLint(options = {}) {
98
153
  continue; // file is gone — skip any remaining checks
99
154
  }
100
155
  }
156
+ // ── Env dangerous-key pass ─────────────────────────────────────────────────
157
+ // Scan every `.env` file under <stashRoot>/env/ (and the deprecated
158
+ // <stashRoot>/vaults/) across all stash roots for keys that are known to
159
+ // enable process-execution hijacking. Warn-only — findings go into `flagged`,
160
+ // never `fixed`.
161
+ const envRoots = [stashRoot, ...extraStashRoots];
162
+ for (const root of envRoots) {
163
+ for (const [subdir, prefix] of [
164
+ ["env", "env"],
165
+ ["vaults", "vault"],
166
+ ]) {
167
+ const dir = path.join(root, subdir);
168
+ if (!fs.existsSync(dir))
169
+ continue;
170
+ for (const envPath of collectEnvFiles(dir)) {
171
+ const baseName = path.basename(envPath, ".env");
172
+ // "default" (or empty) maps to ".env" → <prefix>:default
173
+ const ref = baseName === "" ? `${prefix}:default` : `${prefix}:${baseName}`;
174
+ const relPath = path.relative(root, envPath);
175
+ for (const issue of checkVaultForDangerousKeys(envPath, relPath, ref)) {
176
+ flagged.push(issue);
177
+ }
178
+ }
179
+ }
180
+ }
181
+ // `ok` reflects whether the lint run completed successfully — NOT whether
182
+ // it found anything. Findings are surfaced via `summary.flagged`; the CLI
183
+ // gates its exit code on `--fail-on-flagged`. Conflating "issues exist"
184
+ // with "command failed" caused two downstream problems:
185
+ // 1. `akm lint --json | jq …` saw stdout-flush races on Bun's non-zero
186
+ // exit, intermittently truncating the JSON the consumer read.
187
+ // 2. `ok` is the shared `{ok, error, code}` failure indicator across the
188
+ // whole CLI; reusing it for "found stuff" forced callers to disambiguate
189
+ // a successful-but-flagged run from a hard error by inspecting fields.
101
190
  return {
102
- ok: flagged.length === 0,
191
+ ok: true,
103
192
  fixed,
104
193
  flagged,
105
194
  summary: { fixed: fixed.length, flagged: flagged.length },
@@ -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 { BaseLinter } from "./base-linter";
2
5
  /**
3
6
  * Linter for `knowledge/` assets.
@@ -0,0 +1,343 @@
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
+ * Table-aware insertion-point selection for markdown auto-fixers.
6
+ *
7
+ * Background: an earlier `lint --fix` rule that auto-inserted a callout note
8
+ * landed it INSIDE a markdown table in `knowledge/akm-cli-reference.md`, which
9
+ * split the table fence and broke rendering. This module centralises the
10
+ * "where is it safe to insert a new block?" decision so any current or future
11
+ * fixer that wants to inject content into a markdown body can route through
12
+ * `findSafeInsertionPoint` and avoid the same class of bug.
13
+ *
14
+ * The helper is intentionally pure: it takes a `string[]` of body lines plus a
15
+ * proposed insertion line, and returns an adjusted insertion line that is
16
+ * guaranteed to fall outside of any of the following no-insert regions:
17
+ *
18
+ * - Markdown pipe tables (header row + `|---|---|` separator + data rows)
19
+ * - HTML tables (`<table>…</table>`)
20
+ * - Fenced code blocks (``` or ~~~ fences)
21
+ * - Indented code blocks (4+ leading spaces or a tab, after a blank line)
22
+ *
23
+ * Frontmatter is intentionally NOT detected here — callers should already
24
+ * strip the frontmatter and operate on the body, or pass the full content
25
+ * including frontmatter (in which case the helper treats it like prose and
26
+ * will not detect it as a no-insert region; the existing
27
+ * `fixMissingUpdated` flow injects into the frontmatter via regex without
28
+ * needing this helper).
29
+ *
30
+ * Line numbers are 0-based throughout this module to match `Array.splice`
31
+ * semantics. Callers using 1-based line numbers (e.g. from
32
+ * `parseMarkdownToc`) must subtract 1 before passing in.
33
+ */
34
+ // ── Pipe-table detection ─────────────────────────────────────────────────────
35
+ /**
36
+ * Pattern matching a markdown table separator row, e.g. `|---|---|`,
37
+ * `| :--- | ---: |`, or `:---|---:` (pipe-less style).
38
+ *
39
+ * Allows optional leading/trailing whitespace, optional outer pipes, and
40
+ * alignment colons. Requires at least two cells (i.e. at least one inner
41
+ * pipe between dash sequences) so we don't false-positive on a horizontal
42
+ * rule like `---`.
43
+ */
44
+ const TABLE_SEPARATOR_RE = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/;
45
+ /**
46
+ * Pattern matching a plausible markdown table header/data row. Must contain
47
+ * at least one pipe character that is not at the very start AND not part of
48
+ * an inline code span. We don't try to be perfect here — the existence of a
49
+ * matching separator row on the next line is the real signal that this is a
50
+ * table.
51
+ */
52
+ function looksLikeTableRow(line) {
53
+ const trimmed = line.trim();
54
+ if (trimmed === "")
55
+ return false;
56
+ // Must contain at least one pipe.
57
+ if (!trimmed.includes("|"))
58
+ return false;
59
+ // Exclude lines that are obviously not table rows: headings, list items
60
+ // starting with `- |` are rare but possible; we lean permissive here
61
+ // because the separator-row check below is the real gate.
62
+ if (/^#{1,6}\s/.test(trimmed))
63
+ return false;
64
+ return true;
65
+ }
66
+ /**
67
+ * Given the start line of a candidate table (the header row), return the
68
+ * **exclusive** end line — the first line after the table that is NOT part
69
+ * of it (either a blank line, EOF, or a line that doesn't look like a table
70
+ * row). Returns -1 if the candidate is not actually a table.
71
+ *
72
+ * @param lines Full body as a `string[]`.
73
+ * @param headerLine 0-based index of the candidate header row.
74
+ */
75
+ export function findEndOfTable(lines, headerLine) {
76
+ if (headerLine < 0 || headerLine >= lines.length)
77
+ return -1;
78
+ if (!looksLikeTableRow(lines[headerLine]))
79
+ return -1;
80
+ const sepLine = headerLine + 1;
81
+ if (sepLine >= lines.length)
82
+ return -1;
83
+ if (!TABLE_SEPARATOR_RE.test(lines[sepLine]))
84
+ return -1;
85
+ // Walk forward through data rows. A blank line, EOF, or a line that does
86
+ // not look like a table row terminates the table.
87
+ let i = sepLine + 1;
88
+ while (i < lines.length) {
89
+ if (lines[i].trim() === "")
90
+ break;
91
+ if (!looksLikeTableRow(lines[i]))
92
+ break;
93
+ i += 1;
94
+ }
95
+ return i;
96
+ }
97
+ /**
98
+ * If `lineIdx` falls inside a markdown pipe table, return the exclusive end
99
+ * line of that table. Otherwise return -1.
100
+ *
101
+ * "Inside" includes the header row, the separator row, and any data row.
102
+ */
103
+ export function isInsideTable(lines, lineIdx) {
104
+ if (lineIdx < 0 || lineIdx >= lines.length)
105
+ return -1;
106
+ // Walk backwards from lineIdx looking for a plausible table header
107
+ // (i.e. a line followed by a separator row), up to the nearest blank
108
+ // line or start-of-file.
109
+ for (let i = lineIdx; i >= 0; i -= 1) {
110
+ if (lines[i].trim() === "")
111
+ return -1; // blank line — out of any table
112
+ if (!looksLikeTableRow(lines[i]))
113
+ return -1;
114
+ const end = findEndOfTable(lines, i);
115
+ if (end !== -1 && lineIdx < end)
116
+ return end;
117
+ // Continue scanning backwards — this row looks like a table row but
118
+ // the table doesn't start here (could be a data row).
119
+ }
120
+ return -1;
121
+ }
122
+ // ── Fenced code block detection ───────────────────────────────────────────────
123
+ /**
124
+ * Match a fenced code block opener/closer: ```` ``` ```` or `~~~`, with
125
+ * optional leading whitespace and optional language identifier. The fence
126
+ * character must repeat at least three times; the matched group is the
127
+ * fence character + repeat count so we can detect matching closers.
128
+ */
129
+ const FENCE_RE = /^(\s*)(`{3,}|~{3,})(.*)$/;
130
+ /**
131
+ * Return all fenced-code-block regions in `lines`. A fence is considered
132
+ * unterminated if EOF is reached without a matching closer — in that case
133
+ * the region extends to the last line. This matches CommonMark behaviour
134
+ * and means "EOF closes any open fence" so we still treat the tail as a
135
+ * no-insert region (otherwise a fixer could inject content into what the
136
+ * author meant as a multi-line code sample).
137
+ */
138
+ export function findFenceRegions(lines) {
139
+ const regions = [];
140
+ let openIdx = -1;
141
+ let openFence = "";
142
+ for (let i = 0; i < lines.length; i += 1) {
143
+ const match = lines[i].match(FENCE_RE);
144
+ if (!match)
145
+ continue;
146
+ const fence = match[2];
147
+ if (openIdx === -1) {
148
+ // Opening fence
149
+ openIdx = i;
150
+ openFence = fence[0]; // ``` or ~~~
151
+ continue;
152
+ }
153
+ // Inside a fence — only a matching fence character closes it, and the
154
+ // closer must be at least as long. Per CommonMark we ignore any info
155
+ // string on the closer (`match[3]` is allowed but typically empty).
156
+ if (fence[0] === openFence && fence.length >= openFence.length) {
157
+ regions.push({ start: openIdx, end: i });
158
+ openIdx = -1;
159
+ openFence = "";
160
+ }
161
+ }
162
+ if (openIdx !== -1) {
163
+ // Unterminated fence — extends to EOF.
164
+ regions.push({ start: openIdx, end: lines.length - 1 });
165
+ }
166
+ return regions;
167
+ }
168
+ /**
169
+ * If `lineIdx` falls inside any fenced code block, return the exclusive
170
+ * end line (one past the closing fence). Otherwise return -1.
171
+ */
172
+ export function isInsideCodeFence(lines, lineIdx) {
173
+ if (lineIdx < 0 || lineIdx >= lines.length)
174
+ return -1;
175
+ for (const region of findFenceRegions(lines)) {
176
+ if (lineIdx >= region.start && lineIdx <= region.end) {
177
+ return region.end + 1;
178
+ }
179
+ }
180
+ return -1;
181
+ }
182
+ // ── HTML table detection ─────────────────────────────────────────────────────
183
+ /**
184
+ * If `lineIdx` falls inside an HTML `<table>…</table>` block, return the
185
+ * exclusive end line (one past the `</table>`). Otherwise return -1.
186
+ *
187
+ * We do a deliberately simple scan: detect `<table` on any prior line (case
188
+ * insensitive, allowing attributes) and require a `</table>` on or after
189
+ * `lineIdx`. Nested tables are NOT supported — that's a markdown
190
+ * anti-pattern and we'd rather under-detect than over-detect.
191
+ */
192
+ export function isInsideHtmlTable(lines, lineIdx) {
193
+ if (lineIdx < 0 || lineIdx >= lines.length)
194
+ return -1;
195
+ let openIdx = -1;
196
+ for (let i = 0; i <= lineIdx; i += 1) {
197
+ if (/<table[\s>]/i.test(lines[i]))
198
+ openIdx = i;
199
+ if (/<\/table\s*>/i.test(lines[i]) && openIdx !== -1 && i >= openIdx) {
200
+ // Closing tag before lineIdx — table already finished, reset.
201
+ if (i < lineIdx)
202
+ openIdx = -1;
203
+ else
204
+ return i + 1;
205
+ }
206
+ }
207
+ if (openIdx === -1)
208
+ return -1;
209
+ // We're after a `<table` opener — find the matching `</table>`.
210
+ for (let i = lineIdx; i < lines.length; i += 1) {
211
+ if (/<\/table\s*>/i.test(lines[i]))
212
+ return i + 1;
213
+ }
214
+ // Unterminated table — extend to EOF so we don't inject into malformed HTML.
215
+ return lines.length;
216
+ }
217
+ // ── Indented code block detection ────────────────────────────────────────────
218
+ /**
219
+ * Per CommonMark, an indented code block is a sequence of lines indented by
220
+ * 4+ spaces (or one tab), preceded by a blank line. We use a simplified
221
+ * detection: if `lineIdx` is indented 4+ spaces / starts with a tab AND
222
+ * either is the first line of the body or follows a blank line, treat it
223
+ * as part of an indented code block and skip to the next non-indented
224
+ * non-blank line.
225
+ *
226
+ * Returns the exclusive end of the code block if `lineIdx` is inside one,
227
+ * otherwise -1.
228
+ */
229
+ export function isInsideIndentedCode(lines, lineIdx) {
230
+ if (lineIdx < 0 || lineIdx >= lines.length)
231
+ return -1;
232
+ const isIndented = (s) => /^( {4}|\t)/.test(s);
233
+ if (!isIndented(lines[lineIdx]))
234
+ return -1;
235
+ // Walk backwards: every line above must be either indented or blank, and
236
+ // we must eventually hit a blank line (or BOF) before any non-indented
237
+ // non-blank line. If we find a non-indented non-blank line first, this
238
+ // isn't an indented code block (it's just a continuation of a list item
239
+ // or paragraph).
240
+ let foundBlankBoundary = false;
241
+ for (let i = lineIdx - 1; i >= 0; i -= 1) {
242
+ if (lines[i].trim() === "") {
243
+ foundBlankBoundary = true;
244
+ break;
245
+ }
246
+ if (!isIndented(lines[i])) {
247
+ return -1; // probably a list continuation, not a code block
248
+ }
249
+ }
250
+ if (!foundBlankBoundary && lineIdx > 0) {
251
+ // Walked all the way to BOF without a blank line — but lineIdx > 0
252
+ // means there was a non-indented non-blank line above, which would
253
+ // have returned -1 already. This branch is for safety only.
254
+ return -1;
255
+ }
256
+ // Walk forwards to find the end of the block.
257
+ let i = lineIdx + 1;
258
+ while (i < lines.length) {
259
+ if (lines[i].trim() === "") {
260
+ // A blank line MAY terminate the block, but per CommonMark a single
261
+ // blank line followed by more indented lines is still part of the
262
+ // same block. Peek ahead.
263
+ let j = i + 1;
264
+ while (j < lines.length && lines[j].trim() === "")
265
+ j += 1;
266
+ if (j >= lines.length || !isIndented(lines[j])) {
267
+ break; // block ends at the blank line
268
+ }
269
+ i = j;
270
+ continue;
271
+ }
272
+ if (!isIndented(lines[i]))
273
+ break;
274
+ i += 1;
275
+ }
276
+ return i;
277
+ }
278
+ // ── Composite: find a safe insertion point ───────────────────────────────────
279
+ /**
280
+ * Given a proposed 0-based insertion line, return an adjusted 0-based line
281
+ * that is guaranteed to fall outside of any markdown table, HTML table,
282
+ * fenced code block, or indented code block.
283
+ *
284
+ * Strategy: if the proposed line falls inside a no-insert region, push it
285
+ * to the line immediately AFTER that region. We never push it before —
286
+ * most callouts are forward references to surrounding content, so
287
+ * post-region is the safer choice (and prevents the very bug this helper
288
+ * exists to fix: a callout landing between the header separator and the
289
+ * first data row).
290
+ *
291
+ * The check is iterative: pushing past one region may land inside another
292
+ * (e.g. table immediately followed by code fence), so we re-check until a
293
+ * stable safe point is reached or we hit EOF. The iteration is bounded by
294
+ * line count to guarantee termination.
295
+ *
296
+ * @param lines Body as a `string[]`.
297
+ * @param proposedLineNumber 0-based index where the caller wants to insert.
298
+ * @returns 0-based safe insertion index (may equal `lines.length`).
299
+ */
300
+ export function findSafeInsertionPoint(lines, proposedLineNumber) {
301
+ if (lines.length === 0)
302
+ return 0;
303
+ let target = Math.max(0, Math.min(proposedLineNumber, lines.length));
304
+ // Iterate at most `lines.length` times — each iteration that finds a
305
+ // region only moves `target` forward, so we cannot loop forever.
306
+ for (let guard = 0; guard <= lines.length; guard += 1) {
307
+ if (target >= lines.length)
308
+ return lines.length;
309
+ const tableEnd = isInsideTable(lines, target);
310
+ if (tableEnd !== -1) {
311
+ target = tableEnd;
312
+ continue;
313
+ }
314
+ const fenceEnd = isInsideCodeFence(lines, target);
315
+ if (fenceEnd !== -1) {
316
+ target = fenceEnd;
317
+ continue;
318
+ }
319
+ const htmlEnd = isInsideHtmlTable(lines, target);
320
+ if (htmlEnd !== -1) {
321
+ target = htmlEnd;
322
+ continue;
323
+ }
324
+ const indentedEnd = isInsideIndentedCode(lines, target);
325
+ if (indentedEnd !== -1) {
326
+ target = indentedEnd;
327
+ continue;
328
+ }
329
+ return target;
330
+ }
331
+ // Defensive fallback — should be unreachable given the guard above.
332
+ return Math.min(target, lines.length);
333
+ }
334
+ /**
335
+ * Convenience wrapper that operates on a raw string (splits on `\r?\n` and
336
+ * accepts 0-based line numbers). Returns the adjusted 0-based line.
337
+ *
338
+ * Useful when a caller has the markdown as a single string and only wants
339
+ * to know "where can I safely splice in N more lines?"
340
+ */
341
+ export function findSafeInsertionPointInText(content, proposedLineNumber) {
342
+ return findSafeInsertionPoint(content.split(/\r?\n/), proposedLineNumber);
343
+ }
@@ -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 fs from "node:fs";
2
5
  import { BaseLinter } from "./base-linter";
3
6
  /**
@@ -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 { AgentLinter } from "./agent-linter";
2
5
  import { CommandLinter } from "./command-linter";
3
6
  import { DefaultLinter } from "./default-linter";
@@ -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 fs from "node:fs";
2
5
  import path from "node:path";
3
6
  import { BaseLinter } from "./base-linter";
@@ -1,24 +1,27 @@
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 { BaseLinter } from "./base-linter";
2
5
  /**
3
6
  * Linter for `tasks/` assets.
4
7
  *
5
- * Tasks are `.md` files with YAML frontmatter. In addition to the base checks
6
- * this linter validates the required task fields:
8
+ * Tasks are pure YAML files at `<stash>/tasks/<id>.yml`. In addition to the
9
+ * base checks this linter validates the required task fields:
7
10
  *
8
11
  * - `schedule` (string, non-empty) — cron expression or `@`-alias
9
12
  * - `enabled` (boolean)
10
- * - At least one of: `prompt` or `workflow` field present
13
+ * - At least one of: `prompt`, `workflow`, or `command` field present
11
14
  *
12
- * All issues are reported as `invalid-task-frontmatter` and are **not**
13
- * auto-fixable. Cron expression syntax validation is intentionally out of
14
- * scope (that belongs to `parseSchedule()`).
15
+ * All issues are reported as `invalid-task-yaml` and are **not** auto-fixable.
16
+ * Cron expression syntax validation is intentionally out of scope (that
17
+ * belongs to `parseSchedule()`).
15
18
  */
16
19
  export class TaskLinter extends BaseLinter {
17
20
  types = ["tasks"];
18
21
  lint(ctx) {
19
22
  const issues = this.runBaseChecks(ctx);
20
- // Only validate frontmatter fields when frontmatter is present.
21
- if (ctx.frontmatter === null)
23
+ // Skip files that failed to parse `data` will be empty.
24
+ if (ctx.data === null || Object.keys(ctx.data).length === 0)
22
25
  return issues;
23
26
  const missing = [];
24
27
  // schedule: must be present and non-empty
@@ -29,15 +32,15 @@ export class TaskLinter extends BaseLinter {
29
32
  if (!("enabled" in ctx.data)) {
30
33
  missing.push("enabled");
31
34
  }
32
- // At least one of: prompt or workflow
33
- const hasTarget = "prompt" in ctx.data || "workflow" in ctx.data;
35
+ // At least one of: prompt, workflow, or command
36
+ const hasTarget = "prompt" in ctx.data || "workflow" in ctx.data || "command" in ctx.data;
34
37
  if (!hasTarget) {
35
- missing.push("prompt or workflow");
38
+ missing.push("prompt, workflow, or command");
36
39
  }
37
40
  if (missing.length > 0) {
38
41
  issues.push({
39
42
  file: ctx.relPath,
40
- issue: "invalid-task-frontmatter",
43
+ issue: "invalid-task-yaml",
41
44
  detail: `missing required fields: ${missing.join(", ")}`,
42
45
  fixed: false,
43
46
  });
@@ -1 +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/.
1
4
  export {};
@@ -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 fs from "node:fs";
2
5
  import { BaseLinter } from "./base-linter";
3
6
  const PLACEHOLDER_STRINGS = ["Describe what this workflow accomplishes", "Example Workflow"];
@@ -1 +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/.
1
4
  export { akmLint } from "./lint/index";
@@ -1,6 +1,9 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
1
4
  import fs from "node:fs";
2
5
  import path from "node:path";
3
- const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main/.github/CHANGELOG.md";
6
+ const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main/CHANGELOG.md";
4
7
  const MIGRATION_DOC_URL = "https://github.com/itlackey/akm/blob/main/docs/migration/v0.5-to-v0.6.md";
5
8
  /**
6
9
  * Directory containing per-version release notes. Resolved relative to
@@ -14,7 +17,7 @@ function releaseNotesDir() {
14
17
  }
15
18
  function loadChangelog() {
16
19
  try {
17
- const changelogPath = path.resolve(import.meta.dir, "../../.github/CHANGELOG.md");
20
+ const changelogPath = path.resolve(import.meta.dir, "../../CHANGELOG.md");
18
21
  if (fs.existsSync(changelogPath)) {
19
22
  return fs.readFileSync(changelogPath, "utf8");
20
23
  }