agentic-mermaid 0.1.0 → 0.1.2

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 (333) hide show
  1. package/CHANGELOG.md +149 -4
  2. package/LICENSES/Apache-2.0.txt +202 -0
  3. package/README.md +25 -32
  4. package/THIRD_PARTY_NOTICES.md +29 -0
  5. package/assets/fonts/FONT-LICENSES.md +9 -5
  6. package/assets/fonts/Inter-Bold.ttf +0 -0
  7. package/assets/fonts/Inter-Medium.ttf +0 -0
  8. package/assets/fonts/Inter-Regular.ttf +0 -0
  9. package/assets/fonts/Inter-SemiBold.ttf +0 -0
  10. package/dist/agent-core.d.ts +463 -0
  11. package/dist/agent-core.js +397 -0
  12. package/dist/agent.d.ts +81 -1822
  13. package/dist/agent.js +336 -36839
  14. package/dist/agentic-mermaid-mcp.js +7 -37908
  15. package/dist/am.js +33638 -36261
  16. package/dist/chunk-4QECACIA.js +4702 -0
  17. package/dist/chunk-CDPKJ2BQ.js +37 -0
  18. package/dist/chunk-QKDLMOJJ.js +3487 -0
  19. package/dist/chunk-WI35L7IR.js +615 -0
  20. package/dist/chunk-WN3YDOBG.js +149 -0
  21. package/dist/chunk-ZYMMKQ3I.js +62637 -0
  22. package/dist/index-D54dqSdC.d.ts +6777 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +310 -32836
  25. package/dist/mcp-cli-SBQSTJFC.js +17 -0
  26. package/docs/README.md +14 -6
  27. package/docs/schemas/style-spec.schema.json +1221 -30
  28. package/examples/styles/catalog.json +54 -0
  29. package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
  30. package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
  31. package/examples/styles/cupertino-prototype.mmd +10 -0
  32. package/examples/styles/cupertino-prototype.style.json +63 -0
  33. package/examples/styles/mid-century-report.style.json +2 -2
  34. package/examples/styles/star-chart-atlas.style.json +2 -1
  35. package/examples/styles/transit-route-map.style.json +2 -2
  36. package/examples/styles/vercel-inspired-prototype.mmd +6 -0
  37. package/examples/styles/vercel-inspired-prototype.style.json +87 -0
  38. package/package.json +70 -43
  39. package/server.json +41 -0
  40. package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
  41. package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
  42. package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
  43. package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
  44. package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
  45. package/AGENT_NATIVE.md +0 -602
  46. package/Instructions_for_agents.md +0 -83
  47. package/TODO.md +0 -662
  48. package/assets/hero.png +0 -0
  49. package/dist/index-x2Cj1dVv.d.ts +0 -1089
  50. package/docs/agent-api-cookbook.md +0 -308
  51. package/docs/agent-mutation-policy.md +0 -104
  52. package/docs/agent-workflow-examples.md +0 -46
  53. package/docs/api.md +0 -203
  54. package/docs/ascii.md +0 -103
  55. package/docs/assets/improvements/box-start-connector.png +0 -0
  56. package/docs/assets/improvements/comment-dropped-lint.png +0 -0
  57. package/docs/assets/improvements/er-content-loss.png +0 -0
  58. package/docs/assets/improvements/fan-in-trunks.png +0 -0
  59. package/docs/assets/improvements/gantt-family.png +0 -0
  60. package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
  61. package/docs/assets/improvements/gantt-verify-seam.png +0 -0
  62. package/docs/assets/improvements/subgraph-container-edges.png +0 -0
  63. package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
  64. package/docs/assets/improvements/wrapper-init-directive.png +0 -0
  65. package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
  66. package/docs/assets/style-cookbook/mid-century-report.png +0 -0
  67. package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
  68. package/docs/assets/style-cookbook/transit-route-map.png +0 -0
  69. package/docs/comparison.md +0 -94
  70. package/docs/config.md +0 -105
  71. package/docs/contributing/adding-diagram-types.md +0 -144
  72. package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
  73. package/docs/contributing/diagram-family-citizenship.md +0 -82
  74. package/docs/contributing/harvesting-upstream-tests.md +0 -126
  75. package/docs/contributing/lessons-learned.md +0 -276
  76. package/docs/contributing/releasing.md +0 -71
  77. package/docs/contributing/visual-review-evidence.md +0 -53
  78. package/docs/custom-style-cookbook.md +0 -154
  79. package/docs/design/families/README.md +0 -26
  80. package/docs/design/families/architecture-beta.md +0 -48
  81. package/docs/design/families/flowchart-parser-conformance.md +0 -22
  82. package/docs/design/families/gantt-research.md +0 -137
  83. package/docs/design/families/gantt.md +0 -329
  84. package/docs/design/families/journey-agentic-cards-before.png +0 -0
  85. package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
  86. package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
  87. package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
  88. package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
  89. package/docs/design/families/journey-migration-parity.md +0 -433
  90. package/docs/design/families/journey-usage-research.md +0 -423
  91. package/docs/design/families/journey.md +0 -125
  92. package/docs/design/families/xychart.md +0 -85
  93. package/docs/design/system/README.md +0 -45
  94. package/docs/design/system/abstraction-audit.md +0 -224
  95. package/docs/design/system/abstraction-recommendations.md +0 -655
  96. package/docs/design/system/architecture.mmd +0 -29
  97. package/docs/design/system/architecture.svg +0 -137
  98. package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
  99. package/docs/design/system/layout-rubric.md +0 -138
  100. package/docs/design/system/route-contracts.md +0 -848
  101. package/docs/design/system/source-preservation-ladder.md +0 -52
  102. package/docs/design/system/styles-rollout.md +0 -216
  103. package/docs/design/system/ugly-layouts.md +0 -119
  104. package/docs/diagram-families.md +0 -193
  105. package/docs/features.md +0 -147
  106. package/docs/fork-differences.md +0 -130
  107. package/docs/getting-started.md +0 -175
  108. package/docs/issue-derived-test-cases.md +0 -156
  109. package/docs/layout-characterization/README.md +0 -245
  110. package/docs/layout-characterization/contact-sheet-families.md +0 -496
  111. package/docs/layout-characterization/contact-sheet.md +0 -638
  112. package/docs/layout-characterization/properties.md +0 -281
  113. package/docs/layout-characterization/visual-quality.md +0 -160
  114. package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
  115. package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
  116. package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
  117. package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
  118. package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
  119. package/docs/layout-characterization/visual-snapshots/journey.svg +0 -118
  120. package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
  121. package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
  122. package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
  123. package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
  124. package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
  125. package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
  126. package/docs/mcp-code-mode-rationale.md +0 -101
  127. package/docs/mcp-http-transport.md +0 -223
  128. package/docs/mermaid-layout-complaints.md +0 -837
  129. package/docs/mutation-testing.md +0 -206
  130. package/docs/project/divergences.md +0 -98
  131. package/docs/project/dogfooding-docs-strategy.md +0 -66
  132. package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
  133. package/docs/project/lessons-learned.md +0 -1019
  134. package/docs/project/remove-role-styling-plan.md +0 -176
  135. package/docs/quality.md +0 -212
  136. package/docs/react.md +0 -97
  137. package/docs/style-authoring.md +0 -133
  138. package/docs/testing-strategy.md +0 -335
  139. package/docs/theming.md +0 -162
  140. package/examples/agent-improve-auth-flow.ts +0 -206
  141. package/examples/agent-loop.ts +0 -53
  142. package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
  143. package/llms.txt +0 -104
  144. package/skill-evals/README.md +0 -77
  145. package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
  146. package/skill-evals/fixtures/class-payment/input.mmd +0 -4
  147. package/skill-evals/fixtures/er-orders/input.mmd +0 -10
  148. package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
  149. package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
  150. package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
  151. package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
  152. package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
  153. package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
  154. package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
  155. package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
  156. package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
  157. package/skill-evals/shared-benchmark.json +0 -510
  158. package/src/agent/analyze.ts +0 -157
  159. package/src/agent/apply.ts +0 -146
  160. package/src/agent/architecture-body.ts +0 -458
  161. package/src/agent/certificates.ts +0 -110
  162. package/src/agent/class-body.ts +0 -319
  163. package/src/agent/core.ts +0 -207
  164. package/src/agent/create.ts +0 -106
  165. package/src/agent/describe.ts +0 -364
  166. package/src/agent/er-body.ts +0 -224
  167. package/src/agent/facts.ts +0 -337
  168. package/src/agent/families-builtin.ts +0 -934
  169. package/src/agent/families.ts +0 -232
  170. package/src/agent/family-layouts.ts +0 -826
  171. package/src/agent/flowchart-body.ts +0 -372
  172. package/src/agent/flowchart-unsupported.ts +0 -243
  173. package/src/agent/gantt-body.ts +0 -495
  174. package/src/agent/index.ts +0 -10
  175. package/src/agent/journey-body.ts +0 -414
  176. package/src/agent/label-metrics.ts +0 -28
  177. package/src/agent/layout-to-rendered.ts +0 -63
  178. package/src/agent/mutate.ts +0 -97
  179. package/src/agent/mutation-ops.ts +0 -51
  180. package/src/agent/op-schema.ts +0 -439
  181. package/src/agent/parse.ts +0 -223
  182. package/src/agent/pie-body.ts +0 -270
  183. package/src/agent/png.ts +0 -117
  184. package/src/agent/quadrant-body.ts +0 -336
  185. package/src/agent/quality.ts +0 -363
  186. package/src/agent/readability-audit.ts +0 -92
  187. package/src/agent/rendered-route-audit.ts +0 -61
  188. package/src/agent/sequence-body.ts +0 -345
  189. package/src/agent/serialize.ts +0 -214
  190. package/src/agent/state-body.ts +0 -589
  191. package/src/agent/structural-count.ts +0 -150
  192. package/src/agent/timeline-body.ts +0 -280
  193. package/src/agent/trace-log.ts +0 -43
  194. package/src/agent/types.ts +0 -979
  195. package/src/agent/verify.ts +0 -571
  196. package/src/agent/xychart-body.ts +0 -472
  197. package/src/architecture/config.ts +0 -282
  198. package/src/architecture/layout.ts +0 -496
  199. package/src/architecture/parser.ts +0 -375
  200. package/src/architecture/renderer.ts +0 -611
  201. package/src/architecture/types.ts +0 -113
  202. package/src/ascii/ansi.ts +0 -423
  203. package/src/ascii/architecture.ts +0 -210
  204. package/src/ascii/canvas.ts +0 -443
  205. package/src/ascii/class-diagram.ts +0 -697
  206. package/src/ascii/converter.ts +0 -475
  207. package/src/ascii/draw.ts +0 -1693
  208. package/src/ascii/edge-bundling.ts +0 -328
  209. package/src/ascii/edge-routing.ts +0 -330
  210. package/src/ascii/er-diagram.ts +0 -439
  211. package/src/ascii/gantt.ts +0 -276
  212. package/src/ascii/grid.ts +0 -747
  213. package/src/ascii/index.ts +0 -196
  214. package/src/ascii/journey.ts +0 -189
  215. package/src/ascii/meta.ts +0 -244
  216. package/src/ascii/multiline-utils.ts +0 -80
  217. package/src/ascii/pathfinder.ts +0 -301
  218. package/src/ascii/pie.ts +0 -67
  219. package/src/ascii/quadrant.ts +0 -158
  220. package/src/ascii/reverse.ts +0 -216
  221. package/src/ascii/sequence.ts +0 -499
  222. package/src/ascii/shapes/circle.ts +0 -27
  223. package/src/ascii/shapes/corners.ts +0 -143
  224. package/src/ascii/shapes/diamond.ts +0 -27
  225. package/src/ascii/shapes/hexagon.ts +0 -27
  226. package/src/ascii/shapes/index.ts +0 -106
  227. package/src/ascii/shapes/rectangle.ts +0 -168
  228. package/src/ascii/shapes/rounded.ts +0 -27
  229. package/src/ascii/shapes/special.ts +0 -332
  230. package/src/ascii/shapes/stadium.ts +0 -110
  231. package/src/ascii/shapes/state.ts +0 -192
  232. package/src/ascii/shapes/types.ts +0 -73
  233. package/src/ascii/timeline.ts +0 -128
  234. package/src/ascii/types.ts +0 -312
  235. package/src/ascii/validate.ts +0 -120
  236. package/src/ascii/width.ts +0 -81
  237. package/src/ascii/xychart.ts +0 -919
  238. package/src/browser.ts +0 -29
  239. package/src/class/layout.ts +0 -240
  240. package/src/class/parser.ts +0 -332
  241. package/src/class/renderer.ts +0 -638
  242. package/src/class/types.ts +0 -129
  243. package/src/cli/agent-instructions.ts +0 -1
  244. package/src/cli/am-bin.ts +0 -6
  245. package/src/cli/exit-codes.ts +0 -18
  246. package/src/cli/index.ts +0 -1117
  247. package/src/cli/init-agent.ts +0 -147
  248. package/src/color-resolver.ts +0 -152
  249. package/src/elk-instance.ts +0 -114
  250. package/src/er/layout.ts +0 -187
  251. package/src/er/parser.ts +0 -226
  252. package/src/er/renderer.ts +0 -792
  253. package/src/er/types.ts +0 -99
  254. package/src/font-manifest.ts +0 -27
  255. package/src/gantt/layout.ts +0 -365
  256. package/src/gantt/parser.ts +0 -282
  257. package/src/gantt/pipeline.ts +0 -27
  258. package/src/gantt/renderer.ts +0 -414
  259. package/src/gantt/schedule.ts +0 -513
  260. package/src/gantt/types.ts +0 -255
  261. package/src/index.ts +0 -368
  262. package/src/journey/layout.ts +0 -520
  263. package/src/journey/parser.ts +0 -218
  264. package/src/journey/renderer.ts +0 -811
  265. package/src/journey/types.ts +0 -144
  266. package/src/layout/geometry.ts +0 -182
  267. package/src/layout/pass.ts +0 -142
  268. package/src/layout/passes/index.ts +0 -3916
  269. package/src/layout-engine.ts +0 -1565
  270. package/src/layout-rubric.ts +0 -659
  271. package/src/mcp/artifacts.ts +0 -137
  272. package/src/mcp/dynamic-harness.ts +0 -31
  273. package/src/mcp/dynamic-modules.d.ts +0 -7
  274. package/src/mcp/facade.ts +0 -608
  275. package/src/mcp/harness-runtime.ts +0 -203
  276. package/src/mcp/hosted-server.ts +0 -468
  277. package/src/mcp/mcp-bin.ts +0 -3
  278. package/src/mcp/mcp-cli.ts +0 -82
  279. package/src/mcp/protocol.ts +0 -13
  280. package/src/mcp/sandbox.ts +0 -195
  281. package/src/mcp/sdk-decl.ts +0 -387
  282. package/src/mcp/server.ts +0 -362
  283. package/src/mcp/tool-surface.ts +0 -146
  284. package/src/mermaid-source.ts +0 -655
  285. package/src/multiline-utils.ts +0 -228
  286. package/src/parser.ts +0 -1031
  287. package/src/pie/layout.ts +0 -168
  288. package/src/pie/parser.ts +0 -136
  289. package/src/pie/renderer.ts +0 -234
  290. package/src/pie/types.ts +0 -82
  291. package/src/quadrant/layout.ts +0 -191
  292. package/src/quadrant/parser.ts +0 -183
  293. package/src/quadrant/renderer.ts +0 -322
  294. package/src/quadrant/types.ts +0 -113
  295. package/src/render-family-hooks.ts +0 -244
  296. package/src/renderer.ts +0 -1099
  297. package/src/route-contracts.ts +0 -2723
  298. package/src/scene/backend.ts +0 -76
  299. package/src/scene/fidelity.ts +0 -183
  300. package/src/scene/hybrid-backend.ts +0 -174
  301. package/src/scene/ir.ts +0 -181
  302. package/src/scene/marks.ts +0 -127
  303. package/src/scene/rough-backend.ts +0 -378
  304. package/src/scene/seed.ts +0 -46
  305. package/src/scene/style-registry.ts +0 -556
  306. package/src/sequence/layout.ts +0 -456
  307. package/src/sequence/parser.ts +0 -255
  308. package/src/sequence/renderer.ts +0 -604
  309. package/src/sequence/types.ts +0 -154
  310. package/src/shape-clipping.ts +0 -351
  311. package/src/shared/accessibility-directives.ts +0 -22
  312. package/src/shared/batched.ts +0 -61
  313. package/src/shared/color-math.ts +0 -72
  314. package/src/shared/svg-a11y.ts +0 -29
  315. package/src/shared/syntax-error.ts +0 -17
  316. package/src/shared/unicode-ranges.ts +0 -135
  317. package/src/styles.ts +0 -318
  318. package/src/svg-paths.ts +0 -28
  319. package/src/text-metrics.ts +0 -248
  320. package/src/theme.ts +0 -639
  321. package/src/timeline/layout.ts +0 -381
  322. package/src/timeline/parser.ts +0 -215
  323. package/src/timeline/renderer.ts +0 -538
  324. package/src/timeline/types.ts +0 -105
  325. package/src/types.ts +0 -454
  326. package/src/website-worker-modules.d.ts +0 -18
  327. package/src/xychart/axis-utils.ts +0 -109
  328. package/src/xychart/colors.ts +0 -124
  329. package/src/xychart/config.ts +0 -88
  330. package/src/xychart/layout.ts +0 -656
  331. package/src/xychart/parser.ts +0 -504
  332. package/src/xychart/renderer.ts +0 -640
  333. package/src/xychart/types.ts +0 -282
package/src/cli/index.ts DELETED
@@ -1,1117 +0,0 @@
1
- // ============================================================================
2
- // am — agentic-mermaid CLI (v4).
3
- // ============================================================================
4
-
5
- import { readFileSync, existsSync, writeFileSync, mkdtempSync, watch } from 'node:fs'
6
- import { spawnSync } from 'node:child_process'
7
- import { join, resolve } from 'node:path'
8
- import { tmpdir } from 'node:os'
9
- import { parseMermaid } from '../agent/parse.ts'
10
- import { logToolInvocation } from '../agent/trace-log.ts'
11
- import { serializeMermaid, synthesizeFromGraph } from '../agent/serialize.ts'
12
- import { mutateChecked } from '../agent/mutate.ts'
13
- import { verifyMermaid } from '../agent/verify.ts'
14
- import { renderMermaidSVG, renderMermaidASCII, renderMermaidPNG, layoutMermaid } from '../agent/index.ts'
15
- import { describeMermaid } from '../agent/describe.ts'
16
- import { collectBatched } from '../shared/batched.ts'
17
- import type {
18
- ValidDiagram, WarningCode, AnyMutationOp,
19
- MutationError, Result, MutableValidDiagram,
20
- } from '../agent/types.ts'
21
- import { WARNING_SEVERITY, WARNING_TIER } from '../agent/types.ts'
22
- import { BUILTIN_FAMILY_METADATA, builtinFamilyMetadata, knownFamilies, getFamily } from '../agent/families.ts'
23
- import { knownStyles, getStyle, validateStyleSpec, inferBackend, resolveStyleStack, styleKind } from '../scene/style-registry.ts'
24
- import type { StyleInput, StyleSpec } from '../scene/style-registry.ts'
25
- import type { BuiltinFamilyId } from '../agent/families.ts'
26
- import '../agent/families-builtin.ts'
27
- import { AGENT_INSTRUCTIONS } from './agent-instructions.ts'
28
- import { initAgentFiles } from './init-agent.ts'
29
- import { EXIT_OK, EXIT_ARG_ERROR, EXIT_VERIFY_FAILED, EXIT_INTERNAL } from './exit-codes.ts'
30
- import type { ParseError } from '../agent/types.ts'
31
-
32
- /**
33
- * Loop 12 M1: build a structured CLI error envelope. Keeps `message` a short
34
- * human string (the first error's message) and carries the full structured
35
- * ParseError[] in `details` — so an agent reads `error.details` instead of
36
- * re-parsing a JSON string buried in `error.message`.
37
- */
38
- function parseErrorEnvelope(errors: ParseError[]): { ok: false; error: { code: string; message: string; details: ParseError[] } } {
39
- const first = errors[0]
40
- const message = first ? `${first.code}: ${first.message}` : 'parse error'
41
- return { ok: false, error: { code: 'PARSE_FAILED', message, details: errors } }
42
- }
43
-
44
- export interface ParsedArgs { command?: string; positional: string[]; flags: Record<string, string | boolean> }
45
-
46
- // Single source of truth for CLI flags. A flag with no `arg` is a boolean;
47
- // `arg` is the usage placeholder shown in `[--flag <arg>]`. BOOLEAN_FLAGS is
48
- // DERIVED from this, so the parser's boolean classification cannot drift from
49
- // the documented usage.
50
- export const FLAG_SPECS: Record<string, { arg?: string }> = {
51
- // booleans
52
- 'agent-instructions': {}, 'ascii': {}, 'certificates': {}, 'help': {}, 'json': {},
53
- 'watch': {}, 'open': {}, 'force': {}, 'canonical-wrapper': {},
54
- // value flags (placeholder = what the usage shows after the flag)
55
- 'suppress': { arg: 'CODES' }, 'label-cap': { arg: 'N' }, 'op': { arg: 'JSON' },
56
- 'style': { arg: 'NAMES|file' }, 'seed': { arg: 'N' },
57
- 'ops': { arg: 'JSON|file' }, 'output': { arg: 'FILE' }, 'format': { arg: 'fmt' },
58
- 'security': { arg: 'mode' }, 'dir': { arg: 'DIR' },
59
- }
60
-
61
- export const BOOLEAN_FLAGS = new Set(Object.keys(FLAG_SPECS).filter(name => !FLAG_SPECS[name]!.arg))
62
-
63
- export function parseArgs(argv: string[]): ParsedArgs {
64
- const out: ParsedArgs = { positional: [], flags: {} }
65
- let i = 0
66
- while (i < argv.length) {
67
- const arg = argv[i]!
68
- if (arg.startsWith('--')) {
69
- const eq = arg.indexOf('=')
70
- if (eq !== -1) out.flags[arg.slice(2, eq)] = arg.slice(eq + 1)
71
- else {
72
- const name = arg.slice(2)
73
- const next = argv[i + 1]
74
- if (BOOLEAN_FLAGS.has(name) || next === undefined || next.startsWith('--')) out.flags[name] = true
75
- else { out.flags[name] = next; i++ }
76
- }
77
- } else if (!out.command) out.command = arg
78
- else out.positional.push(arg)
79
- i++
80
- }
81
- return out
82
- }
83
-
84
- function readSourceArg(arg: string | undefined): string {
85
- if (!arg || arg === '-') {
86
- // Loop 9 M5: TTY-stdin guard. When stdin is an interactive TTY (no
87
- // pipe), the read blocks forever waiting for the user to paste + Ctrl-D
88
- // — confusing UX. Fail fast with a clear hint.
89
- if (process.stdin.isTTY) throw new Error('needs a file argument or piped stdin')
90
- try { return readFileSync(0).toString('utf8') } catch { return '' }
91
- }
92
- if (!existsSync(arg)) throw new Error(`File not found: ${arg}`)
93
- return readFileSync(arg, 'utf8')
94
- }
95
-
96
- function replacer(_k: string, v: unknown): unknown { return v instanceof Map ? Object.fromEntries(v) : v }
97
-
98
- export const GLOBAL_USAGE = `Usage: am <command> [options] [file|-]
99
-
100
- Commands:
101
- render <file|-> Render to SVG/ASCII/Unicode/layout JSON/PNG
102
- verify <file|-> Verify; emits structured JSON warnings
103
- parse <file|-> Parse; emits ValidDiagram JSON
104
- serialize Read ValidDiagram JSON from stdin; emit canonical source
105
- mutate <file|-> --op '<JSON>' Apply MutationOp(s); verify + emit source
106
- preview <file|-> Write standalone HTML preview; optionally --open
107
- format <file|-> Idempotent reformat
108
- describe <file|-> Prose summary or AX-tree JSON
109
- capabilities Emit JSON describing families + warning codes + mutation ops
110
- batch Read JSONL ops from stdin; emit one JSON envelope per line
111
- render-markdown <file> Render fenced mermaid blocks in Markdown (SVG or --ascii)
112
- styles List registered styles (looks + themes); see also render --style
113
- llms-txt Emit the agent discovery digest
114
- init-agent Write a repo-local agent drop-in (AGENTS section, skill, MCP config)
115
-
116
- Flags:
117
- --json Structured JSON output
118
- --ascii For render/render-markdown: ASCII instead of SVG
119
- --op <JSON> For mutate: one MutationOp
120
- --ops <JSON|file> For mutate: JSON array of MutationOps
121
- --output <FILE> For render png / preview output path
122
- --open For preview: open generated HTML in browser
123
- --force For init-agent: refresh generated skill/MCP files
124
- --style <NAMES|file> For render svg/png: style stack — comma-separated names and/or .json spec files
125
- --seed <N> For render svg/png: re-roll ink wobble of styled looks (never layout)
126
- --suppress <CODES> For verify: comma-separated WarningCodes to suppress
127
- --label-cap <N> For verify: LABEL_OVERFLOW char cap (default 40)
128
- --certificates For render --format json: include route/family certificates
129
- --agent-instructions Print the canonical agent-use guide
130
- --help Show this message (or per-command help: am <cmd> --help)
131
-
132
- Exit codes:
133
- 0 ok
134
- 2 arg / parse error (bad flag, missing file, malformed JSON)
135
- 3 verify reported errors (severity 'error')
136
- 4 uncaught internal failure
137
- `
138
-
139
- export const COMMAND_HELP: Record<string, string> = {
140
- render: `am render <file|-> [--format svg|ascii|unicode|json|png] [--ascii] [--json]
141
- Render a diagram. Default is SVG.
142
- --format svg SVG markup (default)
143
- --format ascii 7-bit ASCII art (also via --ascii)
144
- --format unicode Unicode box-drawing ASCII art
145
- --format json Layout JSON (nodes, edges, groups, bounds)
146
- --certificates With --format json, include route/family certificates
147
- --format png PNG bytes; requires --output <file.png>; no watch/multi-input
148
- --style <S> Style stack: comma-separated names and/or .json spec files,
149
- merged left → right (e.g. --style hand-drawn,dracula or
150
- --style ./brand.json). Applies to svg/png. See: am styles
151
- --seed <N> Re-roll ink wobble of styled looks; never moves layout
152
- --security strict Remove external-fetch refs from SVG output
153
- --watch Re-render one input file on change (non-PNG only)
154
- Multiple inputs emit a JSON results array for non-PNG formats.
155
- With --json, the svg/ascii/unicode forms wrap output as {"<format>": "..."}.`,
156
- styles: `am styles [--json]
157
- List every registered style. A style is a partial description of how diagrams
158
- look; a colors-only style is a theme, a full look sets stroke/fill/typography
159
- too. Stack them with render --style (e.g. --style hand-drawn,dracula) and
160
- re-roll ink with --seed. Custom styles are JSON records (docs/style-authoring.md);
161
- pass a .json file path anywhere a name is accepted.
162
- With --json: [{ name, kind: default|look|theme, backend, intent?, blurb }].`,
163
- verify: `am verify <file|-> [--suppress A,B] [--label-cap N]
164
- Always emits JSON: {ok, warnings[], layout}.
165
- Tier-1 error codes flip ok=false:
166
- EMPTY_DIAGRAM, EDGE_MISANCHORED, OFF_CANVAS, GROUP_BREACH, UNRESOLVABLE_SCHEDULE,
167
- RENDER_FAILED (source verifies structurally but the render parser rejects it). Warning codes:
168
- UNKNOWN_SHAPE, LABEL_OVERFLOW (char-cap),
169
- NODE_OVERLAP, ROUTE_SELF_CROSS, ROUTE_HITCH, ROUTE_UNEXPLAINED_BEND, ROUTE_LABEL_ON_SHARED_TRUNK,
170
- ROUTE_CONTAINER_MISANCHOR, ROUTE_SHAPE_MISANCHOR, ROUTE_STALE_AFTER_NODE_MOVE,
171
- DUPLICATE_EDGE, UNREACHABLE_NODE, DECISION_BRANCH_UNLABELED, COMMENT_DROPPED, UNSUPPORTED_SYNTAX,
172
- CONTENT_DROPPED_ON_ROUNDTRIP. Tier-3 lint is advisory.
173
- Exit 0 if ok, 3 if verify reports severity='error'.`,
174
- parse: `am parse <file|->
175
- Emits ValidDiagram JSON (Maps serialized to objects). Exit 2 on parse error.
176
- Pipe to 'am serialize' to round-trip.`,
177
- serialize: `am serialize (reads ValidDiagram JSON on stdin)
178
- Emits canonical Mermaid source. Accepts the JSON shape that 'am parse' emits;
179
- rebuilds the diagram via synthesizeFromGraph without re-parsing source.`,
180
- mutate: `am mutate <file|-> (--op '<JSON>' | --ops '<JSON array|file.json>') [--json]
181
- Applies one or more MutationOps, verifies the final diagram, then emits source.
182
- Flowchart/state, sequence, timeline, class, ER, journey, architecture, xychart,
183
- pie, quadrant, and gantt have typed mutation ops; opaque-fallback diagrams (unmodeled
184
- syntax) return a structured UNSUPPORTED_FAMILY error (exit 2). Verify failures
185
- exit 3 and omit source.`,
186
- preview: `am preview <file|-> [--output preview.html] [--open] [--json] [--security strict]
187
- Writes a standalone HTML preview containing strict-mode rendered SVG. Without
188
- --output, emits HTML to stdout unless --open is set, in which case a temp HTML
189
- file is written and opened. With --json, emits {ok,path,opened,bytes} for file output.`,
190
- format: `am format <file|-> [--canonical-wrapper]
191
- Parse then re-serialize. Idempotent. The leading source wrapper (frontmatter,
192
- %%{init}%% directives, comments before the header) is preserved byte-verbatim
193
- by default; --canonical-wrapper instead synthesizes Mermaid's documented shape
194
- (title/displayMode top-level, everything else under config:, directives folded).`,
195
- describe: `am describe <file|-> [--format text|json|facts] [--json]
196
- Summarize a Mermaid diagram. Text format emits prose by default; --json wraps
197
- it as {ok,text}. --format json emits the structured AX tree
198
- {kind,nodes,edges,entryPoints,sinks}; with --json it wraps as {ok,tree}.
199
- --format facts emits deterministic semantic fact lines; with --json it wraps as {ok,facts}.`,
200
- capabilities: `am capabilities [--json]
201
- Emits a single JSON object describing the SDK's capability surface:
202
- { sdkVersion, families: [{ id, hasMutate, hasExtractLabels, mutationOps, editPolicy, example }],
203
- warningCodes: [{ code, tier, severity }],
204
- outputFormats: ["svg", "ascii", "unicode", "png", "json"] }
205
- editPolicy is "structured-when-narrowed" or "source-level-only". Use this to
206
- introspect what the CLI can do without running every command.`,
207
- batch: `am batch (reads JSONL from stdin)
208
- Each line: { op: "render"|"verify"|"parse"|"serialize"|"mutate", source: string,
209
- options?: {}, mutation?: MutationOp, mutations?: MutationOp[] }. Emits one JSON
210
- envelope per line: { ok, op, data?, error? }. Malformed JSON or unknown ops
211
- surface as ok:false; the batch continues and exits 0 even if lines errored.`,
212
- 'render-markdown': `am render-markdown <file.md> [--ascii]
213
- Render fenced \`\`\`mermaid blocks. Bad diagrams yield ok:false entries and do
214
- not abort the rest of the Markdown file.`,
215
- 'llms-txt': `am llms-txt
216
- Emit the committed agent-discovery digest generated from current capabilities.`,
217
- 'init-agent': `am init-agent [--dir <path>] [--force] [--json]
218
- Write a repo-local, agent-agnostic drop-in so coding agents discover the
219
- parse → narrow → mutate → verify → serialize contract automatically. Creates
220
- without clobbering by default:
221
- - AGENTS.md marked section pointing agents at the workflow + hosted docs
222
- - skills/agentic-mermaid-diagram-workflow/SKILL.md generic skill bundle
223
- - .mcp.json sample agentic-mermaid MCP server config
224
- --dir defaults to the current directory. --force overwrites skill/MCP files;
225
- the AGENTS.md section is always appended only once, guarded by a marker.`,
226
- }
227
-
228
- export function runCli(argv: string[]): number {
229
- const args = parseArgs(argv)
230
- if (args.flags['agent-instructions']) { process.stdout.write(AGENT_INSTRUCTIONS); return EXIT_OK }
231
- if (args.flags.help && !args.command) { process.stdout.write(GLOBAL_USAGE); return EXIT_OK }
232
- if (!args.command) { process.stdout.write(GLOBAL_USAGE); return EXIT_ARG_ERROR }
233
- if (args.flags.help) {
234
- process.stdout.write((COMMAND_HELP[args.command] ?? GLOBAL_USAGE) + '\n')
235
- return EXIT_OK
236
- }
237
- const json = Boolean(args.flags.json)
238
- // Opt-in invocation logging: when AM_TRACE_LOG names a file, append one JSON
239
- // line per real command run. Shares the sink with the library and hosted MCP
240
- // (src/agent/trace-log.ts) so the agent-usage eval can grade traceOk from
241
- // observed calls on ANY channel. The library verify/mutate/create the CLI
242
- // commands call also log, so `am verify`/`am mutate` are covered even here.
243
- logToolInvocation(args.command)
244
- try {
245
- switch (args.command) {
246
- case 'render': return cmdRender(args, json)
247
- case 'verify': return cmdVerify(args)
248
- case 'parse': return cmdParse(args)
249
- case 'serialize': return cmdSerialize()
250
- case 'mutate': return cmdMutate(args, json)
251
- case 'preview': return cmdPreview(args, json)
252
- case 'format': return cmdFormat(args)
253
- case 'describe': return cmdDescribe(args, json)
254
- case 'capabilities': return cmdCapabilities()
255
- case 'styles': return cmdStyles(json)
256
- case 'llms-txt': return cmdLlmsTxt()
257
- case 'init-agent': return cmdInitAgent(args, json)
258
- case 'batch': return cmdBatch()
259
- case 'render-markdown': return cmdRenderMarkdown(args)
260
- default:
261
- process.stderr.write(`Unknown command: ${args.command}\n${GLOBAL_USAGE}`)
262
- return EXIT_ARG_ERROR
263
- }
264
- } catch (e) {
265
- const msg = e instanceof Error ? e.message : String(e)
266
- // Loop 9 M5: argument-shape errors (missing file, TTY stdin, etc.) get
267
- // exit 2 per the documented contract. Heuristic: messages thrown from
268
- // readSourceArg / arg parsing are advisory rather than internal bugs.
269
- const isArgError = /^needs a file argument|^File not found:/.test(msg)
270
- if (json) process.stdout.write(JSON.stringify({ ok: false, error: { code: isArgError ? 'ARG' : 'INTERNAL', message: msg } }) + '\n')
271
- else process.stderr.write(`Error: ${msg}\n`)
272
- return isArgError ? EXIT_ARG_ERROR : EXIT_INTERNAL
273
- }
274
- }
275
-
276
- function cmdRender(args: ParsedArgs, json: boolean): number {
277
- const format = typeof args.flags.format === 'string' ? args.flags.format : (args.flags.ascii ? 'ascii' : 'svg')
278
- const security = args.flags.security === 'strict' ? 'strict' as const : 'default' as const
279
- if (!['svg', 'ascii', 'unicode', 'json', 'png'].includes(format)) {
280
- process.stderr.write(`am render: unsupported --format ${format}; expected svg, ascii, unicode, json, or png\n`)
281
- return EXIT_ARG_ERROR
282
- }
283
- if (args.flags.security !== undefined && args.flags.security !== 'strict') {
284
- process.stderr.write('am render --security accepts only strict\n')
285
- return EXIT_ARG_ERROR
286
- }
287
- if (args.flags.watch && args.positional.length > 1) {
288
- process.stderr.write('am render --watch accepts exactly one input file\n')
289
- return EXIT_ARG_ERROR
290
- }
291
-
292
- // --style: a stack of names and/or .json spec files; fail fast (exit 2) on
293
- // unknown names or invalid specs instead of surfacing an internal error.
294
- let style: StyleInput[] | undefined
295
- if (typeof args.flags.style === 'string') {
296
- try {
297
- style = parseStyleFlag(args.flags.style)
298
- resolveStyleStack(style)
299
- } catch (e) {
300
- process.stderr.write(`am render --style: ${e instanceof Error ? e.message : String(e)}\n`)
301
- return EXIT_ARG_ERROR
302
- }
303
- if (format === 'ascii' || format === 'unicode' || format === 'json') {
304
- process.stderr.write(`am render: --style applies to svg/png output; ignored for --format ${format}\n`)
305
- }
306
- }
307
- const seed = typeof args.flags.seed === 'string' ? Number(args.flags.seed) : undefined
308
- if (seed !== undefined && !Number.isFinite(seed)) {
309
- process.stderr.write('am render --seed expects a finite number\n')
310
- return EXIT_ARG_ERROR
311
- }
312
-
313
- if (args.positional.length > 1 && format === 'png') {
314
- process.stderr.write('am render --format png accepts exactly one input; run once per file with a distinct --output path\n')
315
- return EXIT_ARG_ERROR
316
- }
317
-
318
- // #959: multi-input — when more than one positional file is given, render
319
- // each and emit a JSON array of results, skipping bad files (like #543).
320
- // Single-input keeps the existing single-output behavior. PNG is excluded
321
- // because it writes bytes to a single --output path.
322
- if (args.positional.length > 1) {
323
- const results = args.positional.map((file, index) => {
324
- try {
325
- const src = readSourceArg(file)
326
- const output = renderSourceToFormat(src, format, { security, certificates: args.flags.certificates === true, style, seed })
327
- return { index, file, ok: true, output }
328
- } catch (e) {
329
- const parseEnvelope = e as { ok?: false; error?: { code?: string; message?: string; details?: unknown } }
330
- if (parseEnvelope?.ok === false && parseEnvelope.error?.code === 'PARSE_FAILED') {
331
- return { index, file, ok: false, error: parseEnvelope.error }
332
- }
333
- return { index, file, ok: false, error: { code: 'RENDER_FAILED', message: e instanceof Error ? e.message : String(e) } }
334
- }
335
- })
336
- process.stdout.write(JSON.stringify({ ok: true, files: results }) + '\n')
337
- return EXIT_OK
338
- }
339
-
340
- // #930: watch mode — re-render on file change.
341
- if (args.flags.watch && format === 'png') {
342
- process.stderr.write('am render --format png does not support --watch; run non-watch PNG renders with --output <file.png>\n')
343
- return EXIT_ARG_ERROR
344
- }
345
- if (args.flags.watch && typeof args.positional[0] === 'string' && args.positional[0] !== '-') {
346
- return cmdRenderWatch(args.positional[0], format, args, json, { security, certificates: args.flags.certificates === true, style, seed })
347
- }
348
-
349
- const source = readSourceArg(args.positional[0])
350
- const parsed = parseMermaid(source)
351
- if (!parsed.ok) {
352
- process.stdout.write(JSON.stringify(parseErrorEnvelope(parsed.error)) + '\n')
353
- return EXIT_ARG_ERROR
354
- }
355
-
356
- if (format === 'png') {
357
- const outFile = typeof args.flags.o === 'string' ? args.flags.o : (typeof args.flags.output === 'string' ? args.flags.output : '')
358
- if (!outFile) {
359
- process.stderr.write('am render --format png requires --output <file.png> (PNG bytes corrupt terminals if piped to stdout)\n')
360
- return EXIT_ARG_ERROR
361
- }
362
- const scale = typeof args.flags.scale === 'string' ? Number(args.flags.scale) : 2
363
- const background = typeof args.flags.bg === 'string' ? args.flags.bg : 'white'
364
- // PNG render is native-sync via resvg; keep bytes off stdout and write the
365
- // raster artifact explicitly to the requested output path.
366
- return renderPngSync(source, { scale, background, style, seed }, outFile, json)
367
- }
368
- // Loop 9 M3/M4, #7645: json = layout shape; unicode is the default text
369
- // renderer; `--security strict` strips external-fetch refs from SVG.
370
- const out = renderSourceToFormat(source, format, { security, certificates: args.flags.certificates === true, style, seed })
371
- if (typeof out !== 'string') {
372
- process.stdout.write(JSON.stringify(out) + '\n')
373
- return EXIT_OK
374
- }
375
- process.stdout.write(json ? JSON.stringify({ [format]: out }) + '\n' : (out.endsWith('\n') ? out : out + '\n'))
376
- return EXIT_OK
377
- }
378
-
379
- interface RenderFormatOptions { security?: 'default' | 'strict'; certificates?: boolean; style?: StyleInput[]; seed?: number }
380
-
381
- /** The ONE format dispatch behind every render path — single-input,
382
- * multi-input, and watch differ only in I/O and error envelopes, so they
383
- * share this core instead of re-enumerating formats (the style/seed
384
- * threading previously had to touch three copies). JSON parse failures
385
- * throw the structured envelope; callers decide how to surface it. */
386
- function renderSourceToFormat(source: string, format: string, opts: RenderFormatOptions = {}): string | object {
387
- if (format === 'ascii' || format === 'unicode') return renderMermaidASCII(source, { useAscii: format === 'ascii' })
388
- if (format === 'json') {
389
- const parsed = parseMermaid(source)
390
- if (!parsed.ok) throw parseErrorEnvelope(parsed.error)
391
- return layoutMermaid(parsed.value, { debug: opts.certificates === true })
392
- }
393
- return renderMermaidSVG(source, { security: opts.security, style: opts.style, seed: opts.seed })
394
- }
395
-
396
- /**
397
- * #930: pure re-render step for watch mode — reads the file, renders to the
398
- * requested format, returns the output string. Extracted so it's unit-testable
399
- * without fs.watch timing.
400
- */
401
- export function renderFileOnce(file: string, format: string, opts: RenderFormatOptions = {}): string {
402
- const src = readFileSync(file, 'utf8')
403
- try {
404
- const out = renderSourceToFormat(src, format, opts)
405
- return typeof out === 'string' ? out : JSON.stringify(out)
406
- } catch (e) {
407
- // Watch mode wants a printable line, not a throw: the structured JSON
408
- // parse envelope serializes; real render errors keep propagating.
409
- if ((e as { ok?: boolean; error?: unknown })?.ok === false) return JSON.stringify(e)
410
- throw e
411
- }
412
- }
413
-
414
- function cmdRenderWatch(file: string, format: string, args: ParsedArgs, _json: boolean, opts: { security?: 'default' | 'strict'; certificates?: boolean; style?: StyleInput[]; seed?: number } = {}): number {
415
- const outFile = typeof args.flags.output === 'string' ? args.flags.output : ''
416
- const emit = () => {
417
- try {
418
- const out = renderFileOnce(file, format, opts)
419
- if (outFile) { writeFileSync(outFile, out) ; process.stderr.write(`rendered → ${outFile}\n`) }
420
- else process.stdout.write(out + (out.endsWith('\n') ? '' : '\n'))
421
- } catch (e) { process.stderr.write(`render error: ${(e as Error).message}\n`) }
422
- }
423
- emit() // initial render
424
- process.stderr.write(`watching ${file} (Ctrl-C to stop)…\n`)
425
- watch(file, { persistent: true }, (event) => { if (event === 'change') emit() })
426
- // Block forever — the watcher keeps the event loop alive.
427
- return EXIT_OK
428
- }
429
-
430
- function renderPngSync(source: string, opts: { scale: number; background: string; style?: StyleInput[]; seed?: number }, outFile: string, json: boolean): number {
431
- try {
432
- const png = renderMermaidPNG(source, opts)
433
- writeFileSync(outFile, png)
434
- if (json) process.stdout.write(JSON.stringify({ ok: true, path: outFile, bytes: png.length }) + '\n')
435
- return EXIT_OK
436
- } catch (e) {
437
- const msg = e instanceof Error ? e.message : String(e)
438
- process.stderr.write(`am render --format png: ${msg}\n`)
439
- return EXIT_INTERNAL
440
- }
441
- }
442
-
443
- function cmdVerify(args: ParsedArgs): number {
444
- const source = readSourceArg(args.positional[0])
445
- const suppressRaw = typeof args.flags.suppress === 'string' ? args.flags.suppress : ''
446
- const suppress = suppressRaw ? (suppressRaw.split(',').map(s => s.trim()).filter(Boolean) as WarningCode[]) : undefined
447
- const labelCharCap = typeof args.flags['label-cap'] === 'string' ? parseInt(args.flags['label-cap'], 10) : undefined
448
- const r = verifyMermaid(source, { suppress, labelCharCap })
449
- process.stdout.write(JSON.stringify(r, replacer) + '\n')
450
- return r.ok ? EXIT_OK : EXIT_VERIFY_FAILED
451
- }
452
-
453
- function cmdParse(args: ParsedArgs): number {
454
- const r = parseMermaid(readSourceArg(args.positional[0]))
455
- if (!r.ok) {
456
- // Error envelope matches the documented batch shape (cli/index.ts:107).
457
- // Success emits the bare ValidDiagram payload — pipeable into `am serialize`
458
- // without unwrapping, which preserves existing consumer contracts.
459
- process.stdout.write(JSON.stringify(parseErrorEnvelope(r.error)) + '\n')
460
- return EXIT_ARG_ERROR
461
- }
462
- process.stdout.write(JSON.stringify(toJsonSafe(r.value), replacer) + '\n')
463
- return EXIT_OK
464
- }
465
-
466
- function cmdSerialize(): number {
467
- const stdin = readSourceArg('-')
468
- let payload: unknown
469
- try { payload = JSON.parse(stdin) } catch (e) { process.stderr.write(`serialize: invalid JSON: ${(e as Error).message}\n`); return EXIT_ARG_ERROR }
470
- const r = synthesizeFromGraph(payload as Parameters<typeof synthesizeFromGraph>[0])
471
- if (!r.ok) { process.stderr.write(`serialize: ${JSON.stringify(r.error)}\n`); return EXIT_ARG_ERROR }
472
- process.stdout.write(serializeMermaid(r.value))
473
- return EXIT_OK
474
- }
475
-
476
- type CliMutationError = MutationError | { code: 'UNSUPPORTED_FAMILY' | 'VERIFY_FAILED' | 'PARSE_FAILED' | 'INVALID_OP'; message: string; details?: unknown }
477
-
478
- type MutationRunResult =
479
- | { ok: true; source: string; verify: ReturnType<typeof verifyMermaid> }
480
- | { ok: false; error: CliMutationError; verify?: ReturnType<typeof verifyMermaid> }
481
-
482
- function parseMutationOpsFlag(args: ParsedArgs): Result<AnyMutationOp[], CliMutationError> {
483
- const opStr = typeof args.flags.op === 'string' ? args.flags.op : ''
484
- const opsStr = typeof args.flags.ops === 'string' ? args.flags.ops : ''
485
- if (opStr && opsStr) return { ok: false, error: { code: 'INVALID_OP', message: 'mutate accepts either --op or --ops, not both' } }
486
- if (!opStr && !opsStr) return { ok: false, error: { code: 'INVALID_OP', message: 'mutate requires --op <JSON> or --ops <JSON array|file.json>' } }
487
- try {
488
- if (opStr) return { ok: true, value: [JSON.parse(opStr) as AnyMutationOp] }
489
- const raw = existsSync(opsStr) ? readFileSync(opsStr, 'utf8') : opsStr
490
- const parsed = JSON.parse(raw) as unknown
491
- if (!Array.isArray(parsed)) return { ok: false, error: { code: 'INVALID_OP', message: '--ops must be a JSON array of MutationOps' } }
492
- if (parsed.length === 0) return { ok: false, error: { code: 'INVALID_OP', message: '--ops must contain at least one MutationOp' } }
493
- return { ok: true, value: parsed as AnyMutationOp[] }
494
- } catch (e) {
495
- return { ok: false, error: { code: 'INVALID_OP', message: `invalid mutation JSON: ${(e as Error).message}` } }
496
- }
497
- }
498
-
499
- function mutateAny(d: ValidDiagram, op: AnyMutationOp): Result<MutableValidDiagram, CliMutationError> {
500
- // mutate() dispatches to the family's registered mutator by diagram kind,
501
- // so the CLI only rules out what the registry cannot handle: opaque bodies
502
- // (source-preserved syntax has no structured ops) and families without a
503
- // mutate hook. The per-family narrowing cascade this replaces re-encoded
504
- // the family list a 13th time.
505
- const plugin = getFamily(d.kind)
506
- if (d.body.kind !== 'opaque' && plugin?.mutate && plugin.serialize) {
507
- // `--op`/`--ops` arrive as untyped JSON, so the CLI funnels through the same
508
- // mutateChecked choke point the MCP paths use: shape is validated (a wrong/
509
- // missing/mistyped field is a prescriptive INVALID_OP) before the mutator.
510
- return mutateChecked(d as MutableValidDiagram, op)
511
- }
512
- return {
513
- ok: false,
514
- error: { code: 'UNSUPPORTED_FAMILY', message: `mutate supports ${knownFamilies().join(', ')} diagrams; got ${d.kind}${d.body.kind === 'opaque' ? ' (source-level/opaque body — structured mutation is not exposed for this family or syntax)' : ''}` },
515
- }
516
- }
517
-
518
- export function mutateSource(source: string, ops: AnyMutationOp[]): MutationRunResult {
519
- const r0 = parseMermaid(source)
520
- if (!r0.ok) {
521
- const env = parseErrorEnvelope(r0.error)
522
- return { ok: false, error: { code: 'PARSE_FAILED', message: env.error.message, details: env.error.details } }
523
- }
524
- let current: ValidDiagram = r0.value
525
- for (let index = 0; index < ops.length; index++) {
526
- const op = ops[index]!
527
- const next = mutateAny(current, op)
528
- if (!next.ok) {
529
- const details = (next.error as { details?: unknown }).details
530
- return { ok: false, error: { ...next.error, details: { index, op, ...(typeof details === 'object' && details ? details as Record<string, unknown> : {}) } } }
531
- }
532
- current = next.value
533
- }
534
- const verify = verifyMermaid(current)
535
- if (!verify.ok) {
536
- return {
537
- ok: false,
538
- error: { code: 'VERIFY_FAILED', message: 'mutated diagram failed verify; source was not emitted', details: verify.warnings },
539
- verify,
540
- }
541
- }
542
- return { ok: true, source: serializeMermaid(current), verify }
543
- }
544
-
545
- function cmdMutate(args: ParsedArgs, json: boolean): number {
546
- const source = readSourceArg(args.positional[0])
547
- const ops = parseMutationOpsFlag(args)
548
- if (!ops.ok) { process.stderr.write(`mutate: ${ops.error.message}\n`); return EXIT_ARG_ERROR }
549
- return emitMutationRun(mutateSource(source, ops.value), json)
550
- }
551
-
552
- function emitMutationRun(r: MutationRunResult, json: boolean): number {
553
- if (!r.ok) {
554
- process.stdout.write(JSON.stringify({ ok: false, error: r.error, verify: r.verify }, replacer) + '\n')
555
- return r.error.code === 'VERIFY_FAILED' ? EXIT_VERIFY_FAILED : EXIT_ARG_ERROR
556
- }
557
- process.stdout.write(json ? JSON.stringify({ ok: true, source: r.source, verify: r.verify }, replacer) + '\n' : r.source)
558
- return EXIT_OK
559
- }
560
-
561
- export function buildPreviewHtml(source: string, opts: { security?: 'default' | 'strict' } = {}): Result<string, { code: string; message: string; details?: unknown }> {
562
- const parsed = parseMermaid(source)
563
- if (!parsed.ok) {
564
- const env = parseErrorEnvelope(parsed.error)
565
- return { ok: false, error: env.error }
566
- }
567
- const svg = renderMermaidSVG(source, { security: opts.security ?? 'strict' })
568
- const title = parsed.value.meta.frontmatter?.title ?? 'Mermaid preview'
569
- const escapedTitle = escapeHtml(String(title))
570
- const escapedSource = escapeHtml(source)
571
- return { ok: true, value: `<!doctype html>
572
- <html lang="en">
573
- <head>
574
- <meta charset="utf-8" />
575
- <meta name="viewport" content="width=device-width, initial-scale=1" />
576
- <title>${escapedTitle}</title>
577
- <style>
578
- body { margin: 0; min-height: 100vh; display: grid; grid-template-rows: auto 1fr; background: #f8fafc; color: #0f172a; font-family: system-ui, sans-serif; }
579
- header { padding: 12px 16px; border-bottom: 1px solid #e2e8f0; background: white; }
580
- main { padding: 24px; overflow: auto; }
581
- .canvas { width: max-content; max-width: 100%; margin: 0 auto; background: white; box-shadow: 0 18px 70px rgba(15,23,42,.12); }
582
- svg { display: block; max-width: 100%; height: auto; }
583
- details { margin-top: 16px; }
584
- pre { white-space: pre-wrap; background: #0f172a; color: #e2e8f0; padding: 12px; border-radius: 8px; }
585
- </style>
586
- </head>
587
- <body>
588
- <header><strong>${escapedTitle}</strong></header>
589
- <main>
590
- <div class="canvas">${svg}</div>
591
- <details><summary>Mermaid source</summary><pre>${escapedSource}</pre></details>
592
- </main>
593
- </body>
594
- </html>` }
595
- }
596
-
597
- function escapeHtml(s: string): string {
598
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
599
- }
600
-
601
- export function openPreviewFile(path: string): { ok: true } | { ok: false; error: string } {
602
- const override = process.env.AM_OPEN_COMMAND
603
- const command = override || (process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open')
604
- const args = override ? [path] : process.platform === 'win32' ? ['/c', 'start', '', path] : [path]
605
- const r = spawnSync(command, args, { stdio: 'ignore' })
606
- if (r.error || r.status !== 0) return { ok: false, error: `open command failed: ${command}` }
607
- return { ok: true }
608
- }
609
-
610
- function cmdPreview(args: ParsedArgs, json: boolean): number {
611
- const source = readSourceArg(args.positional[0])
612
- const security = 'strict' as const
613
- if (args.flags.security !== undefined && args.flags.security !== 'strict') {
614
- process.stderr.write('am preview --security accepts only strict\n')
615
- return EXIT_ARG_ERROR
616
- }
617
- const html = buildPreviewHtml(source, { security })
618
- if (!html.ok) { process.stdout.write(JSON.stringify({ ok: false, error: html.error }) + '\n'); return EXIT_ARG_ERROR }
619
- let outFile = typeof args.flags.output === 'string' ? args.flags.output : ''
620
- if (!outFile && args.flags.open) outFile = join(mkdtempSync(join(tmpdir(), 'am-preview-')), 'preview.html')
621
- if (outFile) {
622
- const path = resolve(outFile)
623
- writeFileSync(path, html.value)
624
- let opened = false
625
- if (args.flags.open) {
626
- const openedResult = openPreviewFile(path)
627
- if (!openedResult.ok) { process.stderr.write(`am preview --open: ${openedResult.error}\n`); return EXIT_INTERNAL }
628
- opened = true
629
- }
630
- if (json) process.stdout.write(JSON.stringify({ ok: true, path, opened, bytes: html.value.length }) + '\n')
631
- else process.stdout.write(`${path}\n`)
632
- return EXIT_OK
633
- }
634
- process.stdout.write(json ? JSON.stringify({ ok: true, html: html.value }) + '\n' : html.value)
635
- return EXIT_OK
636
- }
637
-
638
- function cmdFormat(args: ParsedArgs): number {
639
- const r = parseMermaid(readSourceArg(args.positional[0]))
640
- if (!r.ok) { process.stderr.write(`format: parse failed: ${JSON.stringify(r.error)}\n`); return EXIT_ARG_ERROR }
641
- const wrapper = args.flags['canonical-wrapper'] ? 'canonical' as const : 'verbatim' as const
642
- process.stdout.write(serializeMermaid(r.value, { wrapper }))
643
- return EXIT_OK
644
- }
645
-
646
- function cmdDescribe(args: ParsedArgs, json: boolean): number {
647
- const source = readSourceArg(args.positional[0])
648
- const rawFormat = args.flags.format
649
- const format = rawFormat === 'json' || rawFormat === 'facts' || rawFormat === 'text' || rawFormat === undefined
650
- ? (rawFormat ?? 'text') as 'text' | 'json' | 'facts'
651
- : undefined
652
- if (!format) {
653
- process.stderr.write('am describe --format must be one of: text, json, facts\n')
654
- return EXIT_ARG_ERROR
655
- }
656
- const parsed = parseMermaid(source)
657
- if (!parsed.ok) {
658
- const env = parseErrorEnvelope(parsed.error)
659
- if (json || format === 'json') process.stdout.write(JSON.stringify(env) + '\n')
660
- else process.stderr.write(`describe: parse failed: ${env.error.message}\n`)
661
- return EXIT_ARG_ERROR
662
- }
663
- const described = describeMermaid(parsed.value, { format })
664
- if (format === 'json') {
665
- const tree = JSON.parse(described)
666
- process.stdout.write(JSON.stringify(json ? { ok: true, tree } : tree) + '\n')
667
- return EXIT_OK
668
- }
669
- if (format === 'facts') {
670
- const facts = described.split('\n').filter(Boolean)
671
- process.stdout.write(json ? JSON.stringify({ ok: true, facts }) + '\n' : facts.join('\n') + '\n')
672
- return EXIT_OK
673
- }
674
- process.stdout.write(json ? JSON.stringify({ ok: true, text: described }) + '\n' : described + '\n')
675
- return EXIT_OK
676
- }
677
-
678
- // ---- Loop 7 / A3.1: capabilities ------------------------------------------
679
-
680
- type FamilyEditPolicy = 'structured-when-narrowed' | 'source-level-only'
681
-
682
- interface FamilyCapability {
683
- id: string
684
- hasMutate: boolean
685
- hasExtractLabels: boolean
686
- mutationOps: string[]
687
- /** Full field shapes for each op — { opKind: [{name, required, type}] }, with
688
- * enum vocabularies spelled out inline in `type`. Lets a model fill an op
689
- * correctly on the first try instead of guessing field names/values and
690
- * learning them only from the INVALID_OP error. Absent for source-level-only
691
- * families (no structured ops). */
692
- opFields?: Record<string, OpFieldDoc[]>
693
- editPolicy: FamilyEditPolicy
694
- /** The `as*` narrower to call before structured mutation (e.g. `asState`). */
695
- narrower?: string
696
- /** Mermaid header keyword(s) that open this family (e.g. `stateDiagram-v2`,
697
- * `architecture-beta`) — so a model authoring from blank opens with the right
698
- * header instead of inferring it from `example` or defaulting to flowchart. */
699
- headers?: readonly string[]
700
- /** Minimal canonical source (header + core syntax); absent for
701
- * registered non-builtin families that don't declare one. */
702
- example?: string
703
- }
704
-
705
- interface WarningCodeCapability {
706
- code: WarningCode
707
- tier: string
708
- severity: string
709
- }
710
-
711
- interface CapabilitiesEnvelope {
712
- sdkVersion: string
713
- families: FamilyCapability[]
714
- warningCodes: WarningCodeCapability[]
715
- outputFormats: string[]
716
- }
717
-
718
- // Source of truth now lives in the agent layer (src/agent/mutation-ops.ts) so
719
- // the mutators can name their valid ops in errors; imported and re-exported here
720
- // to keep the capabilities envelope and existing `from '../cli/index.ts'`
721
- // importers working.
722
- import { MUTATION_OPS_BY_FAMILY } from '../agent/mutation-ops.ts'
723
- export { MUTATION_OPS_BY_FAMILY }
724
- import { describeOps, hasOpSchema, type OpFieldDoc } from '../agent/op-schema.ts'
725
-
726
- type MutableFamilyId = keyof typeof MUTATION_OPS_BY_FAMILY
727
-
728
- export function buildCapabilities(): CapabilitiesEnvelope {
729
- const sdkVersion = (() => {
730
- try {
731
- const pkg = require('../../package.json') as { version: string }
732
- return pkg.version
733
- } catch {
734
- return 'unknown'
735
- }
736
- })()
737
- const mutableFamilies = new Set(Object.keys(MUTATION_OPS_BY_FAMILY))
738
- const families: FamilyCapability[] = knownFamilies().map((id) => {
739
- const p = getFamily(id)!
740
- const mutationOps = id in MUTATION_OPS_BY_FAMILY ? [...MUTATION_OPS_BY_FAMILY[id as MutableFamilyId]] : []
741
- const editPolicy: FamilyEditPolicy = mutationOps.length > 0 ? 'structured-when-narrowed' : 'source-level-only'
742
- return {
743
- id,
744
- // Capabilities describe the public agent surface, not whether the
745
- // implementation currently lives in a FamilyPlugin hook or central
746
- // dispatch. Every registered family parses, serializes, verifies, and
747
- // renders — so those constant booleans are omitted (they read as a
748
- // "probe-me" menu with nothing to branch on). Only what actually varies
749
- // is emitted: whether structured mutation and label extraction apply.
750
- hasMutate: mutableFamilies.has(id),
751
- hasExtractLabels: Boolean(p.extractLabels),
752
- mutationOps,
753
- opFields: hasOpSchema(id) ? describeOps(id) : undefined,
754
- editPolicy,
755
- narrower: builtinFamilyMetadata(id)?.narrower,
756
- headers: builtinFamilyMetadata(id)?.headers,
757
- example: builtinFamilyMetadata(id)?.example,
758
- }
759
- })
760
- const warningCodes: WarningCodeCapability[] = (Object.keys(WARNING_SEVERITY) as WarningCode[]).map(code => ({
761
- code,
762
- tier: WARNING_TIER[code],
763
- severity: WARNING_SEVERITY[code],
764
- }))
765
- return { sdkVersion, families, warningCodes, outputFormats: ['svg', 'ascii', 'unicode', 'png', 'json'] }
766
- }
767
-
768
- /** Resolve the --style flag: comma-separated style names and/or .json spec
769
- * files forming a stack (merged left → right by resolveStyleStack). JSON
770
- * specs are validated before use so bad files are arg errors, not throws. */
771
- function parseStyleFlag(value: string): StyleInput[] {
772
- return value.split(',').map(entry => entry.trim()).filter(Boolean).map(entry => {
773
- if (entry.endsWith('.json') || existsSync(entry)) {
774
- if (!existsSync(entry)) throw new Error(`style spec file not found: ${entry}`)
775
- const spec = JSON.parse(readFileSync(entry, 'utf8')) as unknown
776
- const problems = validateStyleSpec(spec)
777
- if (problems.length > 0) throw new Error(`invalid style spec ${entry}: ${problems.join('; ')}`)
778
- return spec as StyleSpec
779
- }
780
- return entry
781
- })
782
- }
783
-
784
- function cmdStyles(json: boolean): number {
785
- const rows = knownStyles().map(name => {
786
- if (name === 'crisp') {
787
- return { name, kind: 'default' as const, backend: 'default' as const, blurb: 'The byte-identical default renderer (style unset).' }
788
- }
789
- const spec = getStyle(name)!
790
- return {
791
- name,
792
- kind: styleKind(spec),
793
- backend: inferBackend(spec),
794
- ...(spec.intent ? { intent: spec.intent } : {}),
795
- blurb: spec.blurb ?? '',
796
- }
797
- })
798
- if (json) {
799
- process.stdout.write(JSON.stringify(rows) + '\n')
800
- return EXIT_OK
801
- }
802
- const nameWidth = Math.max(...rows.map(r => r.name.length))
803
- for (const r of rows) {
804
- process.stdout.write(`${r.name.padEnd(nameWidth)} ${r.kind.padEnd(7)} ${r.blurb}\n`)
805
- }
806
- process.stdout.write(`\nStack styles with: am render <file> --style <name[,name|file.json...]> [--seed N]\nAuthor your own: docs/style-authoring.md (JSON records; validate with validateStyleSpec).\n`)
807
- return EXIT_OK
808
- }
809
-
810
- function cmdCapabilities(): number {
811
- process.stdout.write(JSON.stringify(buildCapabilities()) + '\n')
812
- return EXIT_OK
813
- }
814
-
815
- // ---- Loop 11 / M4 (#6430): llms.txt agent-discovery digest ----------------
816
-
817
- /**
818
- * Build an llms.txt digest (https://llmstxt.org) for agent discovery.
819
- * Derived from buildCapabilities() so the family list, warning codes, and
820
- * output formats stay in sync with the actual SDK surface.
821
- */
822
- export function buildLlmsTxt(): string {
823
- const cap = buildCapabilities()
824
- const families = cap.families.map(f => f.id).join(', ')
825
- const structured = cap.families.filter(f => f.hasMutate).map(f => f.id)
826
- const narrowers = BUILTIN_FAMILY_METADATA.map(f => f.narrower).join(', ')
827
- const formats = cap.outputFormats.join(', ')
828
- const codes = cap.warningCodes.map(w => `${w.code} (${w.tier}/${w.severity})`).join(', ')
829
- return `# Agentic Mermaid
830
-
831
- > Agent-native Mermaid runtime: parse, verify, mutate, and round-trip
832
- > Mermaid diagrams with a typed IR. Deterministic ASCII / PNG / SVG. No
833
- > browser required. CLI + MCP + library.
834
-
835
- Version: ${cap.sdkVersion}
836
-
837
- ## What it does
838
-
839
- A typed editing surface over Mermaid for AI agents. Parse a diagram to a
840
- ValidDiagram, mutate it with typed ops, verify structurally (no pixels),
841
- serialize back to canonical source. Layout is deterministic (verified
842
- cross-process and same-machine cross-runtime on x86_64/ARM64).
843
-
844
- ## The agent loop
845
-
846
- New diagrams: author Mermaid source → parse → verify → render/return.
847
- Existing structured diagrams: parse → narrow → mutate → verify → serialize.
848
- Run verify at every commit point. Never serialize a diagram whose verify result
849
- you haven't inspected.
850
-
851
- ## CLI verbs (\`am <verb>\`)
852
-
853
- - render --format svg, ascii, unicode, json [--security strict] — render a diagram
854
- - render --format png --output file.png — render one input to PNG (no PNG bytes on stdout, no multi-input/watch)
855
- - render --style <names|file.json> --seed N — styled svg/png; comma-separate to stack (--style hand-drawn,dracula)
856
- - styles [--json] — list registered styles (default + full looks + palette-only themes)
857
- - parse — diagram → ValidDiagram JSON
858
- - verify — structural validation (exit 3 if invalid)
859
- - mutate --op '<json>' / --ops '<json array|file>' — apply typed mutation(s), verify, then emit source
860
- - preview [--output file.html] [--open] — standalone strict-mode HTML preview for user inspection
861
- - format — normalize / canonicalize source
862
- - describe [--format text|json|facts] — natural-language, AX-tree, or semantic facts summary
863
- - capabilities --json — machine-readable capability envelope incl. editPolicy + mutationOps
864
- - batch --jsonl — bulk render/verify/parse/serialize/mutate ops, one JSON envelope per line
865
- - render-markdown <file.md> [--ascii] — render fenced mermaid blocks, skip invalid ones
866
- - llms-txt — this document
867
- - init-agent [--dir .] [--force] — write AGENTS.md section, root skills/ bundle, and .mcp.json sample
868
-
869
- Exit codes: 0 ok, 2 arg error, 3 verify-failed, 4 internal.
870
-
871
- ## MCP tools
872
-
873
- Code Mode \`execute(code)\` (JavaScript in a node:vm sandbox with a typed
874
- mermaid.* SDK declaration) plus narrow helper tools: render_png and describe.
875
- render_png is offline.
876
-
877
- ## Output formats
878
-
879
- Agentic Mermaid outputs SVG, PNG, ASCII, Unicode, and JSON layout. Full capability list: ${formats}. SVG strict mode (security: 'strict') emits zero external-fetch
880
- references — safe for untrusted/agent-generated diagrams. See SECURITY.md.
881
-
882
- ## Diagram families
883
-
884
- All families parse, verify, render, round-trip: ${families}.
885
- Structured mutation (${cap.families.find(f => f.hasMutate)?.editPolicy}): ${structured.join(', ')}.
886
- Narrowers: ${narrowers}.
887
- State diagrams own a dedicated body (BUILD-19): narrow with asState; state-shaped ops apply (asFlowchart returns null on them).
888
- Source-level-only: ${cap.families.filter(f => !f.hasMutate).map(f => f.id).join(', ') || 'none — every renderable family ships structured mutation'}.
889
- Opaque-fallback bodies (unmodeled syntax) round-trip losslessly via preserved source (never silently dropped) and stay source-level only.
890
-
891
- ## Warning codes
892
-
893
- ${codes}
894
-
895
- ## Library
896
-
897
- \`import { parseMermaid, mutate, verifyMermaid, analyzeMermaid,
898
- analyzeMermaidSource, serializeMermaid, renderMermaidASCII,
899
- renderMermaidPNG, renderMermaidSVG, renderMermaidASCIIWithMeta,
900
- describeMermaid, asciiToMermaid, verifyNoExternalRefs,
901
- registerStyle, knownStyles, validateStyleSpec } from 'agentic-mermaid/agent'\`
902
-
903
- ## Styles
904
-
905
- Every render call accepts style: a name ('hand-drawn', 'excalidraw',
906
- 'pen-and-ink', 'freehand', 'watercolor', 'blueprint', 'tufte', or any theme
907
- name — a theme is a palette-only style), an inline JSON spec, or a stack
908
- merged left → right ({ style: ['hand-drawn', 'dracula'] }). seed re-rolls
909
- ink wobble, never layout — (source, style, seed) reproduces an image
910
- exactly. Custom styles are data: validateStyleSpec(json) checks untrusted
911
- records, registerStyle({ name, … }) makes them addressable by name.
912
- Authoring guide + quality rubric: docs/style-authoring.md.
913
-
914
- ## Docs
915
-
916
- - docs/README.md — documentation index and structure
917
- - Instructions_for_agents.md — canonical agent-use guide
918
- - AGENT_NATIVE.md — architecture/spec rationale
919
- - docs/features.md — current capability inventory
920
- - docs/api.md — library/CLI/MCP API reference, including SVG/PNG/ASCII output
921
- - docs/diagram-families.md — family examples and edit policy
922
- - docs/theming.md — themes, CSS variables, and Shiki compatibility
923
- - docs/style-authoring.md — styles: the composable look system (a theme is a palette-only style; stack names/specs; author custom styles as data records with the quality rubric)
924
- - docs/quality.md — determinism + "good looking" rubric
925
- - TODO.md — only active backlog
926
- - SECURITY.md — threat model + strict-mode guarantee
927
- - docs/agent-mutation-policy.md — structured-vs-source-level policy
928
- - docs/agent-api-cookbook.md — copy-pasteable library/CLI/MCP recipes
929
- - docs/mcp-code-mode-rationale.md — MCP surface rationale
930
- - docs/agent-workflow-examples.md — runnable MCP/CLI + agent-improvement examples
931
- - skills/ — agent-agnostic SKILL.md bundles for diagram workflow and live-editor development
932
- - skill-evals/ — skill-eval-harness manifest, fixtures, and benchmark instructions
933
- `
934
- }
935
-
936
- function cmdLlmsTxt(): number {
937
- process.stdout.write(buildLlmsTxt())
938
- return EXIT_OK
939
- }
940
-
941
- function cmdInitAgent(args: ParsedArgs, json: boolean): number {
942
- const dir = typeof args.flags.dir === 'string' ? resolve(args.flags.dir) : process.cwd()
943
- const force = Boolean(args.flags.force)
944
- const result = initAgentFiles({ dir, force })
945
- if (json) {
946
- process.stdout.write(JSON.stringify({ ok: true, ...result }) + '\n')
947
- return EXIT_OK
948
- }
949
- const rel = (p: string) => p.startsWith(dir) ? '.' + p.slice(dir.length) : p
950
- for (const p of result.written) process.stdout.write(` created ${rel(p)}\n`)
951
- for (const p of result.appended) process.stdout.write(` updated ${rel(p)}\n`)
952
- for (const p of result.skipped) process.stdout.write(` skipped ${rel(p)} (exists; use --force if applicable)\n`)
953
- process.stdout.write('\nNext: read AGENTS.md, connect the MCP server in .mcp.json, or run `am --agent-instructions`.\n')
954
- return EXIT_OK
955
- }
956
-
957
- // ---- Loop 12 M5 (#543): render-markdown — convert fenced blocks ------------
958
-
959
- export interface MarkdownBlockResult {
960
- index: number
961
- ok: boolean
962
- format?: string
963
- output?: string
964
- error?: { code: string; message: string }
965
- }
966
-
967
- /**
968
- * Extract ```mermaid fenced blocks from markdown and render each. A bad
969
- * diagram yields an { ok:false } entry and does NOT abort the rest (#543:
970
- * "skip invalid diagrams instead of failing the whole conversion").
971
- */
972
- export function renderMarkdownBlocks(md: string, format: 'svg' | 'ascii' = 'svg'): MarkdownBlockResult[] {
973
- const FENCE = /```mermaid[ \t]*\r?\n([\s\S]*?)\r?\n```/g
974
- const blocks: string[] = []
975
- for (const m of md.matchAll(FENCE)) blocks.push(m[1]!)
976
- return collectBatched(blocks, (src, i): MarkdownBlockResult => {
977
- try {
978
- const output = format === 'ascii' ? renderMermaidASCII(src, { useAscii: true }) : renderMermaidSVG(src)
979
- return { index: i, ok: true, format, output }
980
- } catch (e) {
981
- return { index: i, ok: false, error: { code: 'RENDER_FAILED', message: (e as Error).message } }
982
- }
983
- }, 'MARKDOWN_BLOCK_ERROR').map((r, i) => r.ok ? r.value : { index: i, ok: false, error: r.error })
984
- }
985
-
986
- function cmdRenderMarkdown(args: ParsedArgs): number {
987
- const md = readSourceArg(args.positional[0])
988
- const format = args.flags.ascii ? 'ascii' as const : 'svg' as const
989
- const results = renderMarkdownBlocks(md, format)
990
- process.stdout.write(JSON.stringify({ ok: true, blocks: results }) + '\n')
991
- // Per-block failures don't fail the command (#543) — exit OK.
992
- return EXIT_OK
993
- }
994
-
995
- // ---- Loop 7 / A3.2: batch -------------------------------------------------
996
-
997
- interface BatchLine {
998
- op: 'render' | 'verify' | 'parse' | 'serialize' | 'mutate'
999
- source: string
1000
- options?: Record<string, unknown>
1001
- mutation?: AnyMutationOp
1002
- mutations?: AnyMutationOp[]
1003
- }
1004
-
1005
- interface BatchOutput {
1006
- ok: boolean
1007
- op?: string
1008
- data?: unknown
1009
- error?: { code: string; message: string }
1010
- }
1011
-
1012
- /**
1013
- * Execute a single batch line. Exported for tests; the CLI just shuttles
1014
- * lines into and out of this function.
1015
- */
1016
- export function runBatchLine(rawLine: string, lineIndex = 0): BatchOutput {
1017
- let parsed: BatchLine
1018
- try {
1019
- parsed = JSON.parse(rawLine) as BatchLine
1020
- } catch (e) {
1021
- return { ok: false, error: { code: 'INVALID_JSON', message: (e as Error).message } }
1022
- }
1023
- if (!parsed || typeof parsed !== 'object' || typeof parsed.op !== 'string') {
1024
- return { ok: false, error: { code: 'INVALID_PAYLOAD', message: 'missing op string' } }
1025
- }
1026
- const op = parsed.op
1027
- try {
1028
- switch (op) {
1029
- case 'render': {
1030
- const options = parsed.options ?? {}
1031
- const asAscii = Boolean((options as { ascii?: boolean }).ascii)
1032
- // #7540: auto-namespace SVG ids per batch line so the rendered
1033
- // diagrams can coexist on one HTML page without def-id collisions.
1034
- const out = asAscii
1035
- ? renderMermaidASCII(parsed.source)
1036
- : renderMermaidSVG(parsed.source, { idPrefix: `d${lineIndex}-` })
1037
- return { ok: true, op, data: asAscii ? { ascii: out } : { svg: out } }
1038
- }
1039
- case 'verify': {
1040
- const options = parsed.options as { suppress?: WarningCode[]; labelCharCap?: number } | undefined
1041
- const r = verifyMermaid(parsed.source, options ?? {})
1042
- return { ok: true, op, data: JSON.parse(JSON.stringify(r, replacer)) }
1043
- }
1044
- case 'parse': {
1045
- const r = parseMermaid(parsed.source)
1046
- if (!r.ok) { const env = parseErrorEnvelope(r.error); return { ok: false, op, error: env.error } }
1047
- return { ok: true, op, data: JSON.parse(JSON.stringify(toJsonSafe(r.value), replacer)) }
1048
- }
1049
- case 'serialize': {
1050
- // For batch, the line's `source` field is a JSON-stringified ValidDiagram
1051
- // (same shape as `am parse` emits). We re-hydrate it via synthesizeFromGraph.
1052
- let payload: unknown
1053
- try {
1054
- payload = JSON.parse(parsed.source)
1055
- } catch (e) {
1056
- return { ok: false, op, error: { code: 'INVALID_JSON', message: (e as Error).message } }
1057
- }
1058
- const r = synthesizeFromGraph(payload as Parameters<typeof synthesizeFromGraph>[0])
1059
- if (!r.ok) return { ok: false, op, error: { code: 'SYNTHESIZE_FAILED', message: JSON.stringify(r.error) } }
1060
- return { ok: true, op, data: { source: serializeMermaid(r.value) } }
1061
- }
1062
- case 'mutate': {
1063
- const ops = Array.isArray(parsed.mutations)
1064
- ? parsed.mutations
1065
- : parsed.mutation
1066
- ? [parsed.mutation]
1067
- : []
1068
- if (ops.length === 0) return { ok: false, op, error: { code: 'INVALID_OP', message: 'mutate batch line requires mutation or mutations[]' } }
1069
- const r = mutateSource(parsed.source, ops)
1070
- if (!r.ok) return { ok: false, op, error: { code: r.error.code, message: r.error.message } }
1071
- return { ok: true, op, data: { source: r.source, verify: JSON.parse(JSON.stringify(r.verify, replacer)) } }
1072
- }
1073
- default:
1074
- return { ok: false, op, error: { code: 'UNKNOWN_OP', message: `unknown op: ${op}` } }
1075
- }
1076
- } catch (e) {
1077
- return { ok: false, op, error: { code: 'INTERNAL', message: (e as Error).message } }
1078
- }
1079
- }
1080
-
1081
- function cmdBatch(): number {
1082
- const stdin = readSourceArg('-')
1083
- const lines = stdin.split('\n').filter(l => l.trim() !== '')
1084
- // Loop 9 M8: shared per-item iteration via collectBatched. Same scaffold
1085
- // as runWithJudge — see src/shared/batched.ts.
1086
- const results = collectBatched(lines, (line, i) => runBatchLine(line, i), 'BATCH_HANDLER_ERROR')
1087
- for (const r of results) {
1088
- if (r.ok) process.stdout.write(JSON.stringify(r.value) + '\n')
1089
- else process.stdout.write(JSON.stringify({ ok: false, error: r.error }) + '\n')
1090
- }
1091
- // Per-line errors don't abort the batch; the process always exits OK.
1092
- return EXIT_OK
1093
- }
1094
-
1095
- function toJsonSafe(d: ValidDiagram): unknown {
1096
- if (d.body.kind === 'flowchart') {
1097
- const g = d.body.graph
1098
- return {
1099
- kind: d.kind, meta: d.meta,
1100
- body: { kind: 'flowchart', graph: {
1101
- direction: g.direction,
1102
- nodes: Object.fromEntries(g.nodes),
1103
- edges: g.edges,
1104
- subgraphs: g.subgraphs,
1105
- // Preserve styling so `am parse | am serialize` is lossless.
1106
- classDefs: Object.fromEntries(g.classDefs),
1107
- classAssignments: Object.fromEntries(g.classAssignments),
1108
- nodeStyles: Object.fromEntries(g.nodeStyles),
1109
- linkStyles: Object.fromEntries([...g.linkStyles].map(([k, v]) => [String(k), v])),
1110
- } },
1111
- canonicalSource: d.canonicalSource,
1112
- }
1113
- }
1114
- return { kind: d.kind, meta: d.meta, body: d.body, canonicalSource: d.canonicalSource }
1115
- }
1116
-
1117
- if (import.meta.main) process.exit(runCli(process.argv.slice(2)))