akm-cli 0.7.4 → 0.8.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/CHANGELOG.md +224 -1
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +133 -0
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2631 -1440
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +110 -0
  12. package/dist/commands/agent-support.js +68 -0
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +130 -534
  15. package/dist/commands/consolidate.js +2122 -0
  16. package/dist/commands/curate.js +45 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +660 -0
  19. package/dist/commands/distill.js +1081 -73
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +43 -0
  22. package/dist/commands/events.js +15 -24
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +477 -0
  28. package/dist/commands/health.js +1302 -0
  29. package/dist/commands/help/help-accept.md +12 -0
  30. package/dist/commands/help/help-improve.md +69 -0
  31. package/dist/commands/help/help-proposals.md +18 -0
  32. package/dist/commands/help/help-propose.md +17 -0
  33. package/dist/commands/help/help-reject.md +11 -0
  34. package/dist/commands/history.js +54 -46
  35. package/dist/commands/improve-auto-accept.js +97 -0
  36. package/dist/commands/improve-cli.js +217 -0
  37. package/dist/commands/improve-profiles.js +166 -0
  38. package/dist/commands/improve-result-file.js +167 -0
  39. package/dist/commands/improve.js +2373 -0
  40. package/dist/commands/info.js +5 -2
  41. package/dist/commands/init.js +50 -2
  42. package/dist/commands/installed-stashes.js +102 -139
  43. package/dist/commands/knowledge.js +136 -0
  44. package/dist/commands/lint/agent-linter.js +49 -0
  45. package/dist/commands/lint/base-linter.js +479 -0
  46. package/dist/commands/lint/command-linter.js +49 -0
  47. package/dist/commands/lint/default-linter.js +16 -0
  48. package/dist/commands/lint/env-key-rules.js +154 -0
  49. package/dist/commands/lint/index.js +196 -0
  50. package/dist/commands/lint/knowledge-linter.js +16 -0
  51. package/dist/commands/lint/markdown-insertion.js +343 -0
  52. package/dist/commands/lint/memory-linter.js +61 -0
  53. package/dist/commands/lint/registry.js +36 -0
  54. package/dist/commands/lint/skill-linter.js +45 -0
  55. package/dist/commands/lint/task-linter.js +50 -0
  56. package/dist/commands/lint/types.js +4 -0
  57. package/dist/commands/lint/workflow-linter.js +56 -0
  58. package/dist/commands/lint.js +4 -0
  59. package/dist/commands/migration-help.js +3 -0
  60. package/dist/commands/proposal.js +67 -12
  61. package/dist/commands/propose.js +120 -45
  62. package/dist/commands/reflect.js +1104 -60
  63. package/dist/commands/registry-cli.js +150 -0
  64. package/dist/commands/registry-search.js +5 -2
  65. package/dist/commands/remember-cli.js +257 -0
  66. package/dist/commands/remember.js +70 -7
  67. package/dist/commands/schema-repair.js +203 -0
  68. package/dist/commands/search.js +115 -14
  69. package/dist/commands/secret.js +173 -0
  70. package/dist/commands/self-update.js +3 -0
  71. package/dist/commands/show.js +158 -60
  72. package/dist/commands/source-add.js +17 -45
  73. package/dist/commands/source-clone.js +3 -0
  74. package/dist/commands/source-manage.js +14 -19
  75. package/dist/commands/tasks.js +437 -0
  76. package/dist/commands/url-checker.js +42 -0
  77. package/dist/core/action-contributors.js +28 -0
  78. package/dist/core/asset-ref.js +17 -2
  79. package/dist/core/asset-registry.js +12 -17
  80. package/dist/core/asset-serialize.js +88 -0
  81. package/dist/core/asset-spec.js +67 -1
  82. package/dist/core/common.js +182 -0
  83. package/dist/core/concurrent.js +25 -0
  84. package/dist/core/config-io.js +347 -0
  85. package/dist/core/config-migration.js +622 -0
  86. package/dist/core/config-schema.js +534 -0
  87. package/dist/core/config-sources.js +108 -0
  88. package/dist/core/config-types.js +4 -0
  89. package/dist/core/config-walker.js +337 -0
  90. package/dist/core/config.js +364 -968
  91. package/dist/core/errors.js +42 -20
  92. package/dist/core/events.js +105 -135
  93. package/dist/core/file-lock.js +104 -0
  94. package/dist/core/frontmatter.js +75 -8
  95. package/dist/core/lesson-lint.js +3 -0
  96. package/dist/core/markdown.js +20 -0
  97. package/dist/core/memory-belief.js +62 -0
  98. package/dist/core/memory-contradiction-detect.js +274 -0
  99. package/dist/core/memory-improve.js +806 -0
  100. package/dist/core/parse.js +158 -0
  101. package/dist/core/paths.js +280 -14
  102. package/dist/core/proposal-quality-validators.js +380 -0
  103. package/dist/core/proposal-validators.js +69 -0
  104. package/dist/core/proposals.js +512 -42
  105. package/dist/core/state-db.js +1068 -0
  106. package/dist/core/text-truncation.js +107 -0
  107. package/dist/core/time.js +54 -0
  108. package/dist/core/tty.js +59 -0
  109. package/dist/core/warn.js +64 -1
  110. package/dist/core/write-source.js +3 -0
  111. package/dist/indexer/db-backup.js +391 -0
  112. package/dist/indexer/db-search.js +198 -489
  113. package/dist/indexer/db.js +990 -108
  114. package/dist/indexer/ensure-index.js +136 -0
  115. package/dist/indexer/file-context.js +3 -0
  116. package/dist/indexer/graph-boost.js +376 -101
  117. package/dist/indexer/graph-db.js +391 -0
  118. package/dist/indexer/graph-dedup.js +95 -0
  119. package/dist/indexer/graph-extraction.js +550 -114
  120. package/dist/indexer/index-context.js +4 -0
  121. package/dist/indexer/indexer.js +547 -309
  122. package/dist/indexer/llm-cache.js +52 -0
  123. package/dist/indexer/manifest.js +3 -0
  124. package/dist/indexer/matchers.js +167 -160
  125. package/dist/indexer/memory-inference.js +152 -74
  126. package/dist/indexer/metadata-contributors.js +29 -0
  127. package/dist/indexer/metadata.js +275 -196
  128. package/dist/indexer/path-resolver.js +92 -0
  129. package/dist/indexer/project-context.js +192 -0
  130. package/dist/indexer/ranking-contributors.js +331 -0
  131. package/dist/indexer/ranking.js +81 -0
  132. package/dist/indexer/search-fields.js +5 -9
  133. package/dist/indexer/search-hit-enrichers.js +111 -0
  134. package/dist/indexer/search-source.js +44 -10
  135. package/dist/indexer/semantic-status.js +6 -17
  136. package/dist/indexer/staleness-detect.js +447 -0
  137. package/dist/indexer/usage-events.js +12 -9
  138. package/dist/indexer/walker.js +28 -0
  139. package/dist/integrations/agent/builders.js +135 -0
  140. package/dist/integrations/agent/config.js +122 -230
  141. package/dist/integrations/agent/detect.js +3 -0
  142. package/dist/integrations/agent/index.js +7 -13
  143. package/dist/integrations/agent/model-aliases.js +55 -0
  144. package/dist/integrations/agent/profiles.js +70 -5
  145. package/dist/integrations/agent/prompts.js +250 -36
  146. package/dist/integrations/agent/runner.js +151 -0
  147. package/dist/integrations/agent/sdk-runner.js +126 -0
  148. package/dist/integrations/agent/spawn.js +183 -35
  149. package/dist/integrations/github.js +3 -0
  150. package/dist/integrations/lockfile.js +32 -69
  151. package/dist/integrations/session-logs/index.js +69 -0
  152. package/dist/integrations/session-logs/inline-refs.js +35 -0
  153. package/dist/integrations/session-logs/pre-filter.js +152 -0
  154. package/dist/integrations/session-logs/providers/claude-code.js +282 -0
  155. package/dist/integrations/session-logs/providers/opencode.js +258 -0
  156. package/dist/integrations/session-logs/types.js +4 -0
  157. package/dist/llm/call-ai.js +62 -0
  158. package/dist/llm/client.js +79 -88
  159. package/dist/llm/embedder.js +20 -29
  160. package/dist/llm/embedders/cache.js +3 -7
  161. package/dist/llm/embedders/local.js +42 -1
  162. package/dist/llm/embedders/remote.js +20 -8
  163. package/dist/llm/embedders/types.js +3 -7
  164. package/dist/llm/feature-gate.js +95 -48
  165. package/dist/llm/graph-extract.js +676 -72
  166. package/dist/llm/index-passes.js +44 -29
  167. package/dist/llm/memory-infer.js +80 -71
  168. package/dist/llm/metadata-enhance.js +42 -29
  169. package/dist/llm/prompts/extract-session.md +80 -0
  170. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  171. package/dist/output/cli-hints-full.md +292 -0
  172. package/dist/output/cli-hints-short.md +66 -0
  173. package/dist/output/cli-hints.js +7 -311
  174. package/dist/output/context.js +60 -8
  175. package/dist/output/renderers.js +306 -258
  176. package/dist/output/shapes/curate.js +56 -0
  177. package/dist/output/shapes/distill.js +10 -0
  178. package/dist/output/shapes/env-list.js +19 -0
  179. package/dist/output/shapes/events.js +11 -0
  180. package/dist/output/shapes/helpers.js +424 -0
  181. package/dist/output/shapes/history.js +7 -0
  182. package/dist/output/shapes/passthrough.js +102 -0
  183. package/dist/output/shapes/proposal-accept.js +7 -0
  184. package/dist/output/shapes/proposal-diff.js +7 -0
  185. package/dist/output/shapes/proposal-list.js +7 -0
  186. package/dist/output/shapes/proposal-producer.js +11 -0
  187. package/dist/output/shapes/proposal-reject.js +7 -0
  188. package/dist/output/shapes/proposal-show.js +7 -0
  189. package/dist/output/shapes/registry-search.js +6 -0
  190. package/dist/output/shapes/registry.js +30 -0
  191. package/dist/output/shapes/search.js +6 -0
  192. package/dist/output/shapes/secret-list.js +19 -0
  193. package/dist/output/shapes/show.js +6 -0
  194. package/dist/output/shapes/vault-list.js +19 -0
  195. package/dist/output/shapes.js +51 -511
  196. package/dist/output/text/add.js +6 -0
  197. package/dist/output/text/clone.js +6 -0
  198. package/dist/output/text/config.js +6 -0
  199. package/dist/output/text/curate.js +6 -0
  200. package/dist/output/text/distill.js +7 -0
  201. package/dist/output/text/enable-disable.js +7 -0
  202. package/dist/output/text/events.js +10 -0
  203. package/dist/output/text/feedback.js +6 -0
  204. package/dist/output/text/helpers.js +1039 -0
  205. package/dist/output/text/history.js +7 -0
  206. package/dist/output/text/import.js +6 -0
  207. package/dist/output/text/index.js +6 -0
  208. package/dist/output/text/info.js +6 -0
  209. package/dist/output/text/init.js +6 -0
  210. package/dist/output/text/list.js +6 -0
  211. package/dist/output/text/proposal-producer.js +8 -0
  212. package/dist/output/text/proposal.js +11 -0
  213. package/dist/output/text/registry-commands.js +11 -0
  214. package/dist/output/text/registry.js +30 -0
  215. package/dist/output/text/remember.js +6 -0
  216. package/dist/output/text/remove.js +6 -0
  217. package/dist/output/text/save.js +6 -0
  218. package/dist/output/text/search.js +6 -0
  219. package/dist/output/text/show.js +6 -0
  220. package/dist/output/text/update.js +6 -0
  221. package/dist/output/text/upgrade.js +6 -0
  222. package/dist/output/text/vault.js +16 -0
  223. package/dist/output/text/wiki.js +15 -0
  224. package/dist/output/text/workflow.js +14 -0
  225. package/dist/output/text.js +44 -1093
  226. package/dist/registry/build-index.js +3 -0
  227. package/dist/registry/create-provider-registry.js +3 -0
  228. package/dist/registry/factory.js +4 -1
  229. package/dist/registry/origin-resolve.js +3 -0
  230. package/dist/registry/providers/index.js +3 -0
  231. package/dist/registry/providers/skills-sh.js +71 -50
  232. package/dist/registry/providers/static-index.js +53 -48
  233. package/dist/registry/providers/types.js +3 -24
  234. package/dist/registry/resolve.js +11 -16
  235. package/dist/registry/types.js +3 -0
  236. package/dist/scripts/migrate-storage.js +17750 -0
  237. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  238. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  239. package/dist/setup/detect.js +3 -0
  240. package/dist/setup/ripgrep-install.js +3 -0
  241. package/dist/setup/ripgrep-resolve.js +3 -0
  242. package/dist/setup/setup.js +775 -37
  243. package/dist/setup/steps.js +3 -15
  244. package/dist/sources/include.js +3 -0
  245. package/dist/sources/provider-factory.js +5 -12
  246. package/dist/sources/provider.js +3 -20
  247. package/dist/sources/providers/filesystem.js +19 -23
  248. package/dist/sources/providers/git.js +179 -20
  249. package/dist/sources/providers/index.js +3 -0
  250. package/dist/sources/providers/install-types.js +3 -13
  251. package/dist/sources/providers/npm.js +3 -4
  252. package/dist/sources/providers/provider-utils.js +3 -0
  253. package/dist/sources/providers/sync-from-ref.js +3 -11
  254. package/dist/sources/providers/tar-utils.js +3 -0
  255. package/dist/sources/providers/website.js +18 -22
  256. package/dist/sources/resolve.js +3 -0
  257. package/dist/sources/types.js +3 -0
  258. package/dist/sources/website-ingest.js +7 -0
  259. package/dist/tasks/backends/cron.js +203 -0
  260. package/dist/tasks/backends/exec-utils.js +28 -0
  261. package/dist/tasks/backends/index.js +24 -0
  262. package/dist/tasks/backends/launchd-template.xml +19 -0
  263. package/dist/tasks/backends/launchd.js +187 -0
  264. package/dist/tasks/backends/schtasks-template.xml +29 -0
  265. package/dist/tasks/backends/schtasks.js +215 -0
  266. package/dist/tasks/parser.js +211 -0
  267. package/dist/tasks/resolveAkmBin.js +87 -0
  268. package/dist/tasks/runner.js +458 -0
  269. package/dist/tasks/schedule.js +227 -0
  270. package/dist/tasks/schema.js +15 -0
  271. package/dist/tasks/validator.js +62 -0
  272. package/dist/version.js +3 -0
  273. package/dist/wiki/index-template.md +12 -0
  274. package/dist/wiki/ingest-workflow-template.md +54 -0
  275. package/dist/wiki/log-template.md +8 -0
  276. package/dist/wiki/schema-template.md +61 -0
  277. package/dist/wiki/wiki-templates.js +15 -0
  278. package/dist/wiki/wiki.js +13 -61
  279. package/dist/workflows/authoring.js +8 -25
  280. package/dist/workflows/cli.js +3 -0
  281. package/dist/workflows/db.js +141 -2
  282. package/dist/workflows/document-cache.js +3 -10
  283. package/dist/workflows/parser.js +3 -0
  284. package/dist/workflows/renderer.js +11 -3
  285. package/dist/workflows/runs.js +91 -89
  286. package/dist/workflows/schema.js +3 -0
  287. package/dist/workflows/scope-key.js +79 -0
  288. package/dist/workflows/validator.js +4 -8
  289. package/dist/workflows/workflow-template.md +24 -0
  290. package/docs/README.md +10 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.0.md +1 -1
  293. package/docs/migration/release-notes/0.7.4.md +1 -1
  294. package/docs/migration/release-notes/0.7.5.md +20 -0
  295. package/docs/migration/release-notes/0.8.0.md +48 -0
  296. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  297. package/package.json +29 -11
  298. package/dist/commands/install-audit.js +0 -381
  299. package/dist/commands/vault.js +0 -333
  300. package/dist/templates/wiki-templates.js +0 -100
@@ -0,0 +1,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
+ }
@@ -0,0 +1,61 @@
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 { BaseLinter } from "./base-linter";
6
+ /**
7
+ * Linter for `memories/` assets.
8
+ *
9
+ * Extra check beyond base:
10
+ * - `orphaned-stub`: `inferenceProcessed: true` in frontmatter AND body < 100
11
+ * chars AND no sibling `.derived.md` file. Fix: delete the stub file.
12
+ */
13
+ export class MemoryLinter extends BaseLinter {
14
+ types = ["memories"];
15
+ lint(ctx) {
16
+ const issues = this.runBaseChecks(ctx);
17
+ // After base checks the file might have been mutated; re-parse body from
18
+ // ctx.raw which was updated in place by BaseLinter when fix === true.
19
+ const body = ctx.body;
20
+ if (this.#isOrphanedStub(ctx.data, body, ctx.filePath)) {
21
+ if (ctx.fix) {
22
+ try {
23
+ fs.unlinkSync(ctx.filePath);
24
+ issues.push({
25
+ file: ctx.relPath,
26
+ issue: "orphaned-stub",
27
+ detail: "deleted orphaned stub",
28
+ fixed: true,
29
+ });
30
+ }
31
+ catch (e) {
32
+ issues.push({
33
+ file: ctx.relPath,
34
+ issue: "orphaned-stub",
35
+ detail: `could not delete: ${e instanceof Error ? e.message : String(e)}`,
36
+ fixed: false,
37
+ });
38
+ }
39
+ // Signal caller to skip remaining checks via a sentinel issue
40
+ // (caller must handle the deletion path; we mark the file as gone)
41
+ return issues;
42
+ }
43
+ issues.push({
44
+ file: ctx.relPath,
45
+ issue: "orphaned-stub",
46
+ detail: "inferenceProcessed stub with no derived sibling",
47
+ fixed: false,
48
+ });
49
+ }
50
+ return issues;
51
+ }
52
+ #isOrphanedStub(data, body, filePath) {
53
+ if (data.inferenceProcessed !== true)
54
+ return false;
55
+ if (body.trim().length >= 100)
56
+ return false;
57
+ const baseName = filePath.replace(/\.md$/, "");
58
+ const derivedPath = `${baseName}.derived.md`;
59
+ return !fs.existsSync(derivedPath);
60
+ }
61
+ }
@@ -0,0 +1,36 @@
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 { AgentLinter } from "./agent-linter";
5
+ import { CommandLinter } from "./command-linter";
6
+ import { DefaultLinter } from "./default-linter";
7
+ import { KnowledgeLinter } from "./knowledge-linter";
8
+ import { MemoryLinter } from "./memory-linter";
9
+ import { SkillLinter } from "./skill-linter";
10
+ import { TaskLinter } from "./task-linter";
11
+ import { WorkflowLinter } from "./workflow-linter";
12
+ // Singleton instances — one per type, shared across all lint runs.
13
+ const LINTERS = [
14
+ new AgentLinter(),
15
+ new MemoryLinter(),
16
+ new WorkflowLinter(),
17
+ new CommandLinter(),
18
+ new KnowledgeLinter(),
19
+ new SkillLinter(),
20
+ new TaskLinter(),
21
+ new DefaultLinter(),
22
+ ];
23
+ const LINTER_MAP = new Map();
24
+ for (const linter of LINTERS) {
25
+ for (const t of linter.types) {
26
+ LINTER_MAP.set(t, linter);
27
+ }
28
+ }
29
+ const DEFAULT_LINTER = new DefaultLinter();
30
+ /**
31
+ * Return the appropriate linter for the given stash subdirectory name.
32
+ * Falls back to `DefaultLinter` for unknown types.
33
+ */
34
+ export function getLinterForType(subdir) {
35
+ return LINTER_MAP.get(subdir) ?? DEFAULT_LINTER;
36
+ }
@@ -0,0 +1,45 @@
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 path from "node:path";
6
+ import { BaseLinter } from "./base-linter";
7
+ /**
8
+ * Linter for `skills/` assets.
9
+ *
10
+ * Skills are **directory bundles**: each skill lives at `skills/<name>/` and
11
+ * must contain a `SKILL.md` entry-point file.
12
+ *
13
+ * Directory-level check (via `lintDirectory`):
14
+ * - `missing-skill-md`: a skill subdirectory has no `SKILL.md`. Not
15
+ * auto-fixable — flagged with detail `"no SKILL.md in skills/<name>/"`.
16
+ *
17
+ * Per-file check:
18
+ * - Base checks (`unquoted-colon`, `missing-updated`) are run against any
19
+ * `.md` files found inside skill subdirectories.
20
+ */
21
+ export class SkillLinter extends BaseLinter {
22
+ types = ["skills"];
23
+ /**
24
+ * Called once per direct subdirectory of `skills/`. Reports a
25
+ * `missing-skill-md` issue when the directory does not contain a `SKILL.md`.
26
+ */
27
+ lintDirectory(subdirPath, stashRoot) {
28
+ const skillMdPath = path.join(subdirPath, "SKILL.md");
29
+ if (!fs.existsSync(skillMdPath)) {
30
+ const relDir = path.relative(stashRoot, subdirPath);
31
+ return [
32
+ {
33
+ file: relDir,
34
+ issue: "missing-skill-md",
35
+ detail: `no SKILL.md in ${relDir}/`,
36
+ fixed: false,
37
+ },
38
+ ];
39
+ }
40
+ return [];
41
+ }
42
+ lint(ctx) {
43
+ return this.runBaseChecks(ctx);
44
+ }
45
+ }
@@ -0,0 +1,50 @@
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 { BaseLinter } from "./base-linter";
5
+ /**
6
+ * Linter for `tasks/` assets.
7
+ *
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:
10
+ *
11
+ * - `schedule` (string, non-empty) — cron expression or `@`-alias
12
+ * - `enabled` (boolean)
13
+ * - At least one of: `prompt`, `workflow`, or `command` field present
14
+ *
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()`).
18
+ */
19
+ export class TaskLinter extends BaseLinter {
20
+ types = ["tasks"];
21
+ lint(ctx) {
22
+ const issues = this.runBaseChecks(ctx);
23
+ // Skip files that failed to parse — `data` will be empty.
24
+ if (ctx.data === null || Object.keys(ctx.data).length === 0)
25
+ return issues;
26
+ const missing = [];
27
+ // schedule: must be present and non-empty
28
+ if (!("schedule" in ctx.data) || typeof ctx.data.schedule !== "string" || ctx.data.schedule.trim() === "") {
29
+ missing.push("schedule");
30
+ }
31
+ // enabled: must be present (boolean — value of false is valid)
32
+ if (!("enabled" in ctx.data)) {
33
+ missing.push("enabled");
34
+ }
35
+ // At least one of: prompt, workflow, or command
36
+ const hasTarget = "prompt" in ctx.data || "workflow" in ctx.data || "command" in ctx.data;
37
+ if (!hasTarget) {
38
+ missing.push("prompt, workflow, or command");
39
+ }
40
+ if (missing.length > 0) {
41
+ issues.push({
42
+ file: ctx.relPath,
43
+ issue: "invalid-task-yaml",
44
+ detail: `missing required fields: ${missing.join(", ")}`,
45
+ fixed: false,
46
+ });
47
+ }
48
+ return issues;
49
+ }
50
+ }
@@ -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,56 @@
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 { BaseLinter } from "./base-linter";
6
+ const PLACEHOLDER_STRINGS = ["Describe what this workflow accomplishes", "Example Workflow"];
7
+ /**
8
+ * Linter for `workflows/` assets.
9
+ *
10
+ * Extra check beyond base:
11
+ * - `placeholder-stub`: body contains a known placeholder string.
12
+ * Fix: delete the file.
13
+ */
14
+ export class WorkflowLinter extends BaseLinter {
15
+ types = ["workflows"];
16
+ lint(ctx) {
17
+ const issues = this.runBaseChecks(ctx);
18
+ const placeholderMatch = this.#checkPlaceholderStub(ctx.body);
19
+ if (placeholderMatch) {
20
+ if (ctx.fix) {
21
+ try {
22
+ fs.unlinkSync(ctx.filePath);
23
+ issues.push({
24
+ file: ctx.relPath,
25
+ issue: "placeholder-stub",
26
+ detail: `deleted: found "${placeholderMatch}"`,
27
+ fixed: true,
28
+ });
29
+ }
30
+ catch (e) {
31
+ issues.push({
32
+ file: ctx.relPath,
33
+ issue: "placeholder-stub",
34
+ detail: `could not delete: ${e instanceof Error ? e.message : String(e)}`,
35
+ fixed: false,
36
+ });
37
+ }
38
+ return issues;
39
+ }
40
+ issues.push({
41
+ file: ctx.relPath,
42
+ issue: "placeholder-stub",
43
+ detail: `placeholder text: "${placeholderMatch}"`,
44
+ fixed: false,
45
+ });
46
+ }
47
+ return issues;
48
+ }
49
+ #checkPlaceholderStub(body) {
50
+ for (const placeholder of PLACEHOLDER_STRINGS) {
51
+ if (body.includes(placeholder))
52
+ return placeholder;
53
+ }
54
+ return null;
55
+ }
56
+ }
@@ -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 { akmLint } from "./lint/index";
@@ -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
  const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main/CHANGELOG.md";