@abuswami1996/agent-md 0.1.5 → 0.2.0

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 (95) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/agent-md-preview.vsix +0 -0
  3. package/dist/index.js +316 -60
  4. package/package.json +1 -1
  5. package/viewer-dist/assets/index-CtGGcFUm.css +1 -0
  6. package/viewer-dist/assets/index-LKKPT-gN.js +3407 -0
  7. package/viewer-dist/index.html +2 -20
  8. package/viewer-dist/assets/arc-BGWRvh4A.js +0 -1
  9. package/viewer-dist/assets/architecture-7EHR7CIX-tMtXFVEB.js +0 -1
  10. package/viewer-dist/assets/architectureDiagram-3BPJPVTR-CHGdupUx.js +0 -36
  11. package/viewer-dist/assets/array-CLXCPui0.js +0 -1
  12. package/viewer-dist/assets/blockDiagram-GPEHLZMM-BHk4-xwc.js +0 -132
  13. package/viewer-dist/assets/c4Diagram-AAUBKEIU-BShCidQJ.js +0 -10
  14. package/viewer-dist/assets/channel-BeF3oGSA.js +0 -1
  15. package/viewer-dist/assets/chunk-2J33WTMH-D_hBYQSk.js +0 -1
  16. package/viewer-dist/assets/chunk-3OPIFGDE-yFgTZecx.js +0 -62
  17. package/viewer-dist/assets/chunk-4BX2VUAB-BrcCtoyJ.js +0 -1
  18. package/viewer-dist/assets/chunk-4EGX6M5U-sKH2Isf8.js +0 -1
  19. package/viewer-dist/assets/chunk-55IACEB6-DbDA7kYA.js +0 -1
  20. package/viewer-dist/assets/chunk-5DO6E6H7-DVriMWxp.js +0 -1
  21. package/viewer-dist/assets/chunk-5ZQYHXKU-DQ5fX41l.js +0 -2
  22. package/viewer-dist/assets/chunk-727SXJPM-Bd5SVMFF.js +0 -206
  23. package/viewer-dist/assets/chunk-AQP2D5EJ-CDTLRCLI.js +0 -231
  24. package/viewer-dist/assets/chunk-BR22UD5L-BCm2xrxa.js +0 -1
  25. package/viewer-dist/assets/chunk-BSJP7CBP-B3Tyh4s5.js +0 -1
  26. package/viewer-dist/assets/chunk-CSCIHK7Q-BmRiMG-E.js +0 -123
  27. package/viewer-dist/assets/chunk-FHYWG6QK-B_z4diOF.js +0 -1
  28. package/viewer-dist/assets/chunk-FMBD7UC4-Dxft-q4P.js +0 -15
  29. package/viewer-dist/assets/chunk-KSCS5N6A-eA-nZKPR.js +0 -10
  30. package/viewer-dist/assets/chunk-L5ZTLDWV-CJt5R1DD.js +0 -1
  31. package/viewer-dist/assets/chunk-MPE355IW-BrU23f3r.js +0 -1
  32. package/viewer-dist/assets/chunk-MZUSXYTE-BGj-wpbJ.js +0 -1
  33. package/viewer-dist/assets/chunk-N66VUXT2-Bx9F_D4P.js +0 -1
  34. package/viewer-dist/assets/chunk-ND2GUHAM-DhxorC-R.js +0 -1
  35. package/viewer-dist/assets/chunk-NNHCCRGN-DSaPFNsL.js +0 -159
  36. package/viewer-dist/assets/chunk-NZK2D7GU-DaLfDAf-.js +0 -1
  37. package/viewer-dist/assets/chunk-O5CBEL6O-D0rppU0M.js +0 -70
  38. package/viewer-dist/assets/chunk-PUPMXCY4-C2l0dHY4.js +0 -1
  39. package/viewer-dist/assets/chunk-QZHKN3VN-CqO4Iw07.js +0 -1
  40. package/viewer-dist/assets/chunk-UIBZB4QT-DXVjm6Jo.js +0 -1
  41. package/viewer-dist/assets/chunk-WCWK7LTN-zc10zcLo.js +0 -1
  42. package/viewer-dist/assets/classDiagram-4FO5ZUOK-B2G4RtlK.js +0 -1
  43. package/viewer-dist/assets/classDiagram-v2-Q7XG4LA2-KrH6fTGU.js +0 -1
  44. package/viewer-dist/assets/cose-bilkent-S5V4N54A-SSDlF1_a.js +0 -1
  45. package/viewer-dist/assets/cytoscape.esm-mg8EQp5B.js +0 -321
  46. package/viewer-dist/assets/dagre-BM42HDAG-CbZJzBab.js +0 -4
  47. package/viewer-dist/assets/dagre-CdwzCXQr.js +0 -1
  48. package/viewer-dist/assets/defaultLocale-Dda4OpKy.js +0 -1
  49. package/viewer-dist/assets/diagram-2AECGRRQ-DP-iEOl6.js +0 -43
  50. package/viewer-dist/assets/diagram-5GNKFQAL-R4BNZTo2.js +0 -10
  51. package/viewer-dist/assets/diagram-KO2AKTUF-CYSKyVh3.js +0 -3
  52. package/viewer-dist/assets/diagram-LMA3HP47-DbR6OxnG.js +0 -24
  53. package/viewer-dist/assets/diagram-OG6HWLK6-BRCF7m9B.js +0 -24
  54. package/viewer-dist/assets/dist-Cz_gOj_F.js +0 -1
  55. package/viewer-dist/assets/erDiagram-TEJ5UH35-JdTHfMQr.js +0 -85
  56. package/viewer-dist/assets/eventmodeling-FCH6USID-Cmphiy0Z.js +0 -1
  57. package/viewer-dist/assets/flowDiagram-I6XJVG4X-f3PWQJjH.js +0 -162
  58. package/viewer-dist/assets/ganttDiagram-6RSMTGT7-DAl9kEgl.js +0 -292
  59. package/viewer-dist/assets/gitGraph-WXDBUCRP-CABAU2x_.js +0 -1
  60. package/viewer-dist/assets/gitGraphDiagram-PVQCEYII-DgTF7l00.js +0 -106
  61. package/viewer-dist/assets/graphlib-B0DVs0bw.js +0 -1
  62. package/viewer-dist/assets/index-B9izR71_.js +0 -99
  63. package/viewer-dist/assets/index-Dbc_x6bQ.css +0 -1
  64. package/viewer-dist/assets/info-J43DQDTF-CtHtznhp.js +0 -1
  65. package/viewer-dist/assets/infoDiagram-5YYISTIA-DSPZjE8M.js +0 -2
  66. package/viewer-dist/assets/init-Ct4VudL3.js +0 -1
  67. package/viewer-dist/assets/ishikawaDiagram-YF4QCWOH-BQGxw-7N.js +0 -70
  68. package/viewer-dist/assets/journeyDiagram-JHISSGLW-DTbccBmQ.js +0 -139
  69. package/viewer-dist/assets/kanban-definition-UN3LZRKU-Bu5aPAtn.js +0 -89
  70. package/viewer-dist/assets/katex-pVJiI_rc.js +0 -257
  71. package/viewer-dist/assets/line-CSkXZgM5.js +0 -1
  72. package/viewer-dist/assets/linear-C69MUSpN.js +0 -1
  73. package/viewer-dist/assets/mermaid-parser.core-BxtmlK9F.js +0 -4
  74. package/viewer-dist/assets/mindmap-definition-RKZ34NQL-DtokX49J.js +0 -96
  75. package/viewer-dist/assets/ordinal-bHkc4Rpt.js +0 -1
  76. package/viewer-dist/assets/packet-YPE3B663-DmtvbI4l.js +0 -1
  77. package/viewer-dist/assets/path-ZGOlHDxT.js +0 -1
  78. package/viewer-dist/assets/pie-LRSECV5Y-Y82O9nng.js +0 -1
  79. package/viewer-dist/assets/pieDiagram-4H26LBE5-Cy9e7NsD.js +0 -30
  80. package/viewer-dist/assets/quadrantDiagram-W4KKPZXB-B3RaddW4.js +0 -7
  81. package/viewer-dist/assets/radar-GUYGQ44K-BHcHsGeF.js +0 -1
  82. package/viewer-dist/assets/requirementDiagram-4Y6WPE33-DLPs6o_F.js +0 -84
  83. package/viewer-dist/assets/rough.esm-C6nrIGLg.js +0 -1
  84. package/viewer-dist/assets/sankeyDiagram-5OEKKPKP-B92qVA_9.js +0 -40
  85. package/viewer-dist/assets/sequenceDiagram-3UESZ5HK-CaK3TWOv.js +0 -162
  86. package/viewer-dist/assets/src--c83fjKW.js +0 -1
  87. package/viewer-dist/assets/stateDiagram-AJRCARHV-BhcoQjmg.js +0 -1
  88. package/viewer-dist/assets/stateDiagram-v2-BHNVJYJU-w3fBKjOb.js +0 -1
  89. package/viewer-dist/assets/timeline-definition-PNZ67QCA-BbW2wqZA.js +0 -120
  90. package/viewer-dist/assets/treeView-BLDUP644-Cmu4mPLp.js +0 -1
  91. package/viewer-dist/assets/treemap-LRROVOQU-_N62KlUg.js +0 -1
  92. package/viewer-dist/assets/vennDiagram-CIIHVFJN-D_1NYpLk.js +0 -34
  93. package/viewer-dist/assets/wardley-L42UT6IY-DWSXD3L4.js +0 -1
  94. package/viewer-dist/assets/wardleyDiagram-YWT4CUSO-BJEhhfkA.js +0 -78
  95. package/viewer-dist/assets/xychartDiagram-2RQKCTM6-Bx2aStUx.js +0 -7
package/dist/index.js CHANGED
@@ -43,11 +43,32 @@ var primitiveNames = Object.keys(schemas);
43
43
  function issueToDiagnostic(issue, sourcePath, blockType, line) {
44
44
  const unknown = issue.code === "unrecognized_keys";
45
45
  const keys = unknown && "keys" in issue ? issue.keys.join(", ") : issue.path.join(".");
46
- return { severity: unknown ? "warning" : "error", code: unknown ? "unknown_field" : "invalid_field", message: unknown ? `Unknown field "${keys}" on ::${blockType}.` : `${issue.path.join(".") || blockType}: ${issue.message}`, sourcePath, line, blockType };
46
+ const field = keys || void 0;
47
+ return {
48
+ severity: unknown ? "warning" : "error",
49
+ code: unknown ? "unknown_field" : "invalid_field",
50
+ message: unknown ? `Unknown field "${keys}" on ::${blockType}; it will be ignored.` : `Field "${keys || blockType}" is invalid on ::${blockType}: ${issue.message}`,
51
+ sourcePath,
52
+ line,
53
+ blockType,
54
+ field,
55
+ suggestion: unknown ? `Remove "${keys}" or replace it with a supported ::${blockType} field.` : `Update "${keys || blockType}" to match the ::${blockType} schema.`,
56
+ example: primitiveExample(blockType)
57
+ };
58
+ }
59
+ function primitiveExample(blockType) {
60
+ const examples = {
61
+ chart: "type: line\ndata: revenue\nx: month\ny: amount",
62
+ metric: "label: Revenue\ndata: revenue\nfield: amount\naggregate: sum",
63
+ table: "data: revenue\ncolumns: [month, amount]",
64
+ map: "data: locations\nlat: latitude\nlon: longitude",
65
+ embed: "src: ./artifact.md\nmode: preview"
66
+ };
67
+ return examples[blockType];
47
68
  }
48
69
  function validatePrimitive(name, attrs, sourcePath, line) {
49
70
  if (!primitiveNames.includes(name)) {
50
- return { diagnostics: [{ severity: "error", code: "unknown_primitive", message: `Unknown directive ::${name}.`, sourcePath, line, blockType: name }] };
71
+ return { diagnostics: [{ severity: "error", code: "unknown_primitive", message: `Unknown directive ::${name}.`, sourcePath, line, blockType: name, suggestion: "Use a supported Agent Markdown primitive or remove this directive.", example: `Supported primitives: ${primitiveNames.map((primitive) => `::${primitive}`).join(", ")}` }] };
51
72
  }
52
73
  const result = schemas[name].safeParse(attrs);
53
74
  if (result.success) return { attrs: result.data, diagnostics: [] };
@@ -60,41 +81,41 @@ function normalizePrimitive(name, attrs, children, raw, sourcePath, line) {
60
81
  const data = validation.attrs;
61
82
  switch (name) {
62
83
  case "chart":
63
- return { node: { ...data, type: "chart", chartType: data.type }, diagnostics: validation.diagnostics };
84
+ return { node: { ...data, type: "chart", chartType: data.type, line }, diagnostics: validation.diagnostics };
64
85
  case "metric":
65
- return { node: { ...data, type: "metric" }, diagnostics: validation.diagnostics };
86
+ return { node: { ...data, type: "metric", line }, diagnostics: validation.diagnostics };
66
87
  case "table":
67
- return { node: { ...data, type: "table" }, diagnostics: validation.diagnostics };
88
+ return { node: { ...data, type: "table", line }, diagnostics: validation.diagnostics };
68
89
  case "diagram":
69
- return { node: { ...data, type: "diagram", diagramType: data.type }, diagnostics: validation.diagnostics };
90
+ return { node: { ...data, type: "diagram", diagramType: data.type, line }, diagnostics: validation.diagnostics };
70
91
  case "map":
71
- return { node: { ...data, type: "map" }, diagnostics: validation.diagnostics };
92
+ return { node: { ...data, type: "map", line }, diagnostics: validation.diagnostics };
72
93
  case "timeline":
73
- return { node: { ...data, type: "timeline" }, diagnostics: validation.diagnostics };
94
+ return { node: { ...data, type: "timeline", line }, diagnostics: validation.diagnostics };
74
95
  case "tabs": {
75
96
  const tabs = children.filter((child) => child.type === "component" && child.name === "__tab");
76
97
  const tabNodes = tabs.map((tab) => ({ label: String(tab.props?.label ?? ""), value: tab.props?.value ? String(tab.props.value) : void 0, children: Array.isArray(tab.props?.children) ? tab.props.children : [] }));
77
98
  const diagnostics = [...validation.diagnostics];
78
- if (tabNodes.length === 0) diagnostics.push({ severity: "error", code: "tabs_empty", message: "::tabs requires at least one child ::tab.", sourcePath, line, blockType: "tabs" });
99
+ if (tabNodes.length === 0) diagnostics.push({ severity: "error", code: "tabs_empty", message: "::tabs requires at least one child ::tab.", sourcePath, line, blockType: "tabs", suggestion: "Add at least one nested ::tab block with a label.", example: ":::tabs\n::::tab\nlabel: Summary\nContent\n::::\n:::" });
79
100
  const labels = /* @__PURE__ */ new Set();
80
101
  for (const tab of tabNodes) {
81
- if (!tab.label) diagnostics.push({ severity: "error", code: "tab_label_required", message: "Each ::tab requires a label.", sourcePath, line, blockType: "tabs" });
82
- if (labels.has(tab.label)) diagnostics.push({ severity: "error", code: "tab_label_duplicate", message: `Duplicate tab label "${tab.label}".`, sourcePath, line, blockType: "tabs" });
102
+ if (!tab.label) diagnostics.push({ severity: "error", code: "tab_label_required", message: "Each ::tab requires a label.", sourcePath, line, blockType: "tabs", field: "label", suggestion: "Add a unique label to each child ::tab.", example: "label: Summary" });
103
+ if (labels.has(tab.label)) diagnostics.push({ severity: "error", code: "tab_label_duplicate", message: `Duplicate tab label "${tab.label}".`, sourcePath, line, blockType: "tabs", field: "label", suggestion: "Rename one of the duplicate tab labels so each label is unique.", example: "label: Details" });
83
104
  labels.add(tab.label);
84
105
  }
85
- if (typeof data.default === "string" && !labels.has(data.default)) diagnostics.push({ severity: "error", code: "tab_default_missing", message: `Default tab "${data.default}" does not exist.`, sourcePath, line, blockType: "tabs" });
86
- return { node: { type: "tabs", default: data.default, variant: data.variant, tabs: tabNodes }, diagnostics };
106
+ if (typeof data.default === "string" && !labels.has(data.default)) diagnostics.push({ severity: "error", code: "tab_default_missing", message: `Default tab "${data.default}" does not exist.`, sourcePath, line, blockType: "tabs", field: "default", suggestion: "Set default to one of the existing tab labels or remove the default field.", example: tabNodes[0]?.label ? `default: ${tabNodes[0].label}` : void 0 });
107
+ return { node: { type: "tabs", default: data.default, variant: data.variant, tabs: tabNodes, line }, diagnostics };
87
108
  }
88
109
  case "callout":
89
- return { node: { type: "callout", calloutType: data.type ?? "note", title: data.title, body: data.body, children }, diagnostics: validation.diagnostics };
110
+ return { node: { type: "callout", calloutType: data.type ?? "note", title: data.title, body: data.body, children, line }, diagnostics: validation.diagnostics };
90
111
  case "embed":
91
- return { node: { ...data, type: "embed" }, diagnostics: validation.diagnostics };
112
+ return { node: { ...data, type: "embed", line }, diagnostics: validation.diagnostics };
92
113
  case "form":
93
- return { node: { type: "form", title: data.title, description: data.description, submitLabel: data.submitLabel, fields: data.fields.map((field) => ({ ...field, fieldType: field.type })) }, diagnostics: validation.diagnostics };
114
+ return { node: { type: "form", title: data.title, description: data.description, submitLabel: data.submitLabel, fields: data.fields.map((field) => ({ ...field, fieldType: field.type })), line }, diagnostics: validation.diagnostics };
94
115
  case "query":
95
- return { node: { ...data, type: "query" }, diagnostics: validation.diagnostics };
116
+ return { node: { ...data, type: "query", line }, diagnostics: validation.diagnostics };
96
117
  case "component":
97
- return { node: { ...data, type: "component" }, diagnostics: validation.diagnostics };
118
+ return { node: { ...data, type: "component", line }, diagnostics: validation.diagnostics };
98
119
  default:
99
120
  return { node: { type: "error", message: `Unsupported directive ::${name}`, raw, line }, diagnostics: validation.diagnostics };
100
121
  }
@@ -136,7 +157,7 @@ import YAML from "yaml";
136
157
  function parseAgentMarkdown({ source, sourcePath }) {
137
158
  const diagnostics = [];
138
159
  const { frontmatter, body } = extractFrontmatter(source, sourcePath, diagnostics);
139
- if (/<script[\s>]/i.test(body)) diagnostics.push({ severity: "error", code: "script_blocked", message: "Scripts must never execute from Markdown content.", sourcePath });
160
+ if (/<script[\s>]/i.test(body)) diagnostics.push({ severity: "error", code: "script_blocked", message: "Scripts must never execute from Markdown content.", sourcePath, suggestion: "Remove the script tag and use Agent Markdown primitives for interactive content." });
140
161
  const { body: withoutData, dataSources } = extractDataBlocks(body, sourcePath, diagnostics);
141
162
  const nodes = parseDocumentBlocks(withoutData.split(/\r?\n/), sourcePath, diagnostics, 0, 1);
142
163
  return { format: "agent-md", version: String(frontmatter?.version ?? "0.1"), sourcePath, frontmatter, nodes, dataSources, diagnostics };
@@ -150,7 +171,7 @@ function extractFrontmatter(source, sourcePath, diagnostics) {
150
171
  const parsed = YAML.parse(raw) ?? {};
151
172
  return { frontmatter: typeof parsed === "object" ? parsed : {}, body: source.slice(end + 5).replace(/^\r?\n/, "") };
152
173
  } catch (error) {
153
- diagnostics.push({ severity: "error", code: "frontmatter_parse_error", message: error instanceof Error ? error.message : "Invalid frontmatter", sourcePath, line: 1 });
174
+ diagnostics.push({ severity: "error", code: "frontmatter_parse_error", message: error instanceof Error ? error.message : "Invalid frontmatter", sourcePath, line: 1, suggestion: "Fix the YAML frontmatter or remove the frontmatter block.", example: "---\nformat: agent-md\nversion: 0.1\n---" });
154
175
  return { frontmatter: void 0, body: source.slice(end + 5).replace(/^\r?\n/, "") };
155
176
  }
156
177
  }
@@ -174,7 +195,7 @@ function extractDataBlocks(source, sourcePath, diagnostics) {
174
195
  try {
175
196
  dataSources[id] = parseInlineData(id, format, content.join("\n"));
176
197
  } catch (error) {
177
- diagnostics.push({ severity: "error", code: "data_parse_error", message: error instanceof Error ? error.message : `Unable to parse data source ${id}`, sourcePath, line: startLine, blockType: "data" });
198
+ diagnostics.push({ severity: "error", code: "data_parse_error", message: error instanceof Error ? error.message : `Unable to parse data source ${id}`, sourcePath, line: startLine, blockType: "data", field: id, suggestion: `Fix the inline ${format} data block for "${id}" or replace it with a local data file reference.`, example: "```data revenue\nmonth,amount\nJan,10\n```" });
178
199
  }
179
200
  }
180
201
  return { body: kept.join("\n"), dataSources };
@@ -252,7 +273,7 @@ function parseDocumentBlocks(lines, sourcePath, diagnostics, depth, baseLine) {
252
273
  block.content.push(current);
253
274
  }
254
275
  if (depth >= 5) {
255
- diagnostics.push({ severity: "error", code: "max_nesting_depth", message: "Directive nesting depth exceeds 5.", sourcePath, line: block.startLine, blockType: name });
276
+ diagnostics.push({ severity: "error", code: "max_nesting_depth", message: "Directive nesting depth exceeds 5.", sourcePath, line: block.startLine, blockType: name, suggestion: "Flatten nested directives so no block is nested more than five levels deep." });
256
277
  nodes.push({ type: "error", message: "Directive nesting depth exceeds 5.", raw: block.raw.join("\n"), line: block.startLine });
257
278
  continue;
258
279
  }
@@ -301,7 +322,7 @@ function splitPropsAndBody(lines, sourcePath, startLine, diagnostics) {
301
322
  const parsed = YAML.parse(yamlText);
302
323
  attrs = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
303
324
  } catch (error) {
304
- diagnostics.push({ severity: "error", code: "directive_yaml_error", message: error instanceof Error ? error.message : "Invalid directive YAML", sourcePath, line: startLine });
325
+ diagnostics.push({ severity: "error", code: "directive_yaml_error", message: error instanceof Error ? error.message : "Invalid directive YAML", sourcePath, line: startLine, suggestion: "Fix the YAML fields at the top of this directive before the body content.", example: "title: Example\ndata: revenue" });
305
326
  }
306
327
  }
307
328
  return { attrs, body: lines.slice(splitAt).join("\n"), bodyOffset: splitAt + 1 };
@@ -328,7 +349,10 @@ Rules:
328
349
  - Do not emit JavaScript.
329
350
  - Prefer named data blocks or local files.
330
351
  - Add frontmatter with format: agent-md and version: 0.1.
331
- - Run agent-md validate before considering a document complete.
352
+ - Run agent-md validate --file <file.agent.md> --json --for-agent before considering a document complete.
353
+ - If validation returns errors, repair the document and validate again. Treat warnings as worth reviewing, not necessarily fatal unless the user asked for strict validation.
354
+ - When the user wants a shareable rendered artifact, run agent-md convert --file_name <file.agent.md> --html after validation.
355
+ - When browser/manual preview is useful, run agent-md serve --root <project> --host 127.0.0.1 --port <port> --no-open and inspect the local viewer.
332
356
 
333
357
  Supporting files in this skill directory:
334
358
  - agent-md.config.json: default runtime configuration.
@@ -343,6 +367,42 @@ Supported MVP primitives:
343
367
  - ::callout for notes, warnings, decisions, risks, and tips.
344
368
  - ::tabs for grouped alternative views.
345
369
  - ::diagram, ::timeline, ::query, ::embed, ::form, ::map, and ::component are supported with conservative validation and graceful fallbacks.
370
+
371
+ Authoring workflow for agents:
372
+ - Start with a small valid report, then add primitives incrementally.
373
+ - Prefer inline summary data for charts when source files are large; use the full local file for tables or queries.
374
+ - Keep directive YAML fields aligned to column 1 inside the directive block. Do not indent top-level fields like data:, x:, y:, or fields:.
375
+ - Use YAML block scalars for prose containing colons, quotes, or backticks, for example body: >- on callouts.
376
+ - For callouts, prefer an explicit body: field when the body is short prose.
377
+ - For forms, each field uses type: text, number, select, checkbox, or date. Select fields need options.
378
+ - For tabs, each child tab needs a unique label, and default must match one of those labels.
379
+ - For diagrams, inline source is safest. src may reference a local .mmd or .mermaid file inside the project.
380
+ - For embeds, use local project files. Markdown, text, JSON, CSV, TSV, images, PDFs, and videos are the safest choices. HTML embeds are blocked unless trusted config explicitly allows them.
381
+ - Do not reference remote data or artifacts unless the project config explicitly allows it; the default config is local-only.
382
+ - Custom components render as safe placeholders unless allowCustomComponents is enabled for a trusted project.
383
+
384
+ Useful commands:
385
+ - agent-md init --agent cursor
386
+ - agent-md validate --file <file.agent.md> --json --for-agent
387
+ - agent-md validate --file <file.agent.md> --strict
388
+ - agent-md validate --root <project-root> --config agent-md.config.json
389
+ - agent-md convert --file_name <file.agent.md> --html
390
+ - agent-md convert --file-name <file.agent.md> --html --output <output.html>
391
+ - agent-md convert --file_name <file.agent.md> --html --root <project-root> --config agent-md.config.json
392
+ - agent-md serve --root <project-root> --host 127.0.0.1 --port 3847 --no-open
393
+ - agent-md serve --root <project-root> --all-md --no-open
394
+ - agent-md export <file.agent.md> --format json
395
+ - agent-md export <file.agent.md> --format markdown-fallback
396
+ - agent-md vscode-extension --editor cursor
397
+
398
+ Validation and repair notes:
399
+ - --json --for-agent gives compact deterministic diagnostics with sourcePath, line, blockType, field, suggestion, and example.
400
+ - Error diagnostics must be fixed before sharing. Info diagnostics, such as table pagination, can be acceptable.
401
+ - unknown_field warnings usually mean the field will be ignored; remove the field or replace it with a supported schema field.
402
+ - directive_yaml_error usually means malformed YAML inside a directive; check indentation, colons in prose, and unmatched brackets.
403
+ - column_not_found and column_not_numeric mean the data loaded, but a primitive points at the wrong column or type.
404
+ - data_file_error and artifact_file_error usually mean the referenced local path is missing, escapes the project root, or uses an unsupported extension.
405
+ - After a clean validation, convert to HTML. When you're finished, tell the user how they can view the report either in their IDE or in a browser.
346
406
  `;
347
407
  var exampleAgentMarkdown = `---
348
408
  format: agent-md
@@ -423,7 +483,7 @@ import path from "path";
423
483
  import YAML2 from "yaml";
424
484
  import Papa from "papaparse";
425
485
  var dataExtensions = /* @__PURE__ */ new Set([".csv", ".tsv", ".json", ".yaml", ".yml", ".geojson"]);
426
- var artifactExtensions = /* @__PURE__ */ new Set([".pdf", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webm", ".mp4", ".html", ".txt", ".md", ".mmd", ".mermaid", ".csv", ".json"]);
486
+ var artifactExtensions = /* @__PURE__ */ new Set([".pdf", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webm", ".mp4", ".html", ".txt", ".md", ".mmd", ".mermaid", ".csv", ".tsv", ".json"]);
427
487
  function isRemoteRef(ref) {
428
488
  return /^https?:\/\//i.test(ref);
429
489
  }
@@ -510,16 +570,13 @@ async function resolveDocumentData(document, projectRoot, config = defaultConfig
510
570
  const refs = [...new Set(document.nodes.flatMap(collectNodeDataRefs))];
511
571
  for (const ref of refs) {
512
572
  if (dataSources[ref]) continue;
513
- if (isRemoteRef(ref)) {
514
- diagnostics.push({ severity: "error", code: "remote_data_blocked", message: `Remote data reference is blocked: ${ref}`, sourcePath: document.sourcePath });
515
- continue;
516
- }
573
+ if (isRemoteRef(ref)) continue;
517
574
  if (dataExtensions.has(path.extname(ref).toLowerCase())) {
518
575
  try {
519
576
  const loaded = await loadDataFile(projectRoot, document.sourcePath, ref, config.limits);
520
577
  dataSources[ref] = loaded;
521
578
  } catch (error) {
522
- diagnostics.push({ severity: "error", code: "data_file_error", message: error instanceof Error ? error.message : `Unable to load data source ${ref}`, sourcePath: document.sourcePath });
579
+ diagnostics.push({ severity: "error", code: "data_file_error", message: error instanceof Error ? error.message : `Unable to load data source ${ref}`, sourcePath: document.sourcePath, field: ref, suggestion: `Confirm "${ref}" exists inside the project and uses a supported data extension.`, example: "data: ./data/revenue.csv" });
523
580
  }
524
581
  }
525
582
  }
@@ -531,23 +588,33 @@ function validateReferences(nodes, dataSources, sourcePath, config = defaultConf
531
588
  const diagnostics = [];
532
589
  const visit = (node) => {
533
590
  const refs = collectNodeDataRefs(node);
534
- for (const ref of refs) if (!dataSources[ref] && !dataExtensions.has(path.extname(ref).toLowerCase())) diagnostics.push({ severity: "error", code: "data_not_found", message: `Data source "${ref}" was not found.`, sourcePath, blockType: node.type });
591
+ for (const ref of refs) {
592
+ if (isRemoteRef(ref)) {
593
+ diagnostics.push({ severity: "error", code: "remote_data_blocked", message: `Remote data reference is blocked because remote data is disabled: ${ref}`, sourcePath, line: node.line, blockType: node.type, field: "data", suggestion: "Use a local inline data block or a local file path instead of an HTTP URL.", example: "data: ./data/revenue.csv" });
594
+ } else if (!dataSources[ref] && !dataExtensions.has(path.extname(ref).toLowerCase())) {
595
+ diagnostics.push({ severity: "error", code: "data_not_found", message: `Data source "${ref}" was not found.`, sourcePath, line: node.line, blockType: node.type, field: "data", suggestion: `Add an inline data block named "${ref}" or change the data field to an existing data source.`, example: `\`\`\`data ${ref}
596
+ name,value
597
+ Example,1
598
+ \`\`\`` });
599
+ }
600
+ }
535
601
  const columnsByData = getReferencedColumns(node);
536
602
  for (const [data, columns] of Object.entries(columnsByData)) {
537
603
  const source = dataSources[data];
538
604
  if (!source?.columns || columns.length === 0) continue;
539
605
  const known = new Set(source.columns.map((column) => column.name));
540
- for (const column of columns) if (!known.has(column)) diagnostics.push({ severity: "error", code: "column_not_found", message: `Column "${column}" was not found in data source "${data}".`, sourcePath, blockType: node.type });
606
+ const available = source.columns.map((column) => column.name).join(", ");
607
+ for (const column of columns) if (!known.has(column)) diagnostics.push({ severity: "error", code: "column_not_found", message: `Column "${column}" was not found in data source "${data}".`, sourcePath, line: node.line, blockType: node.type, field: column, suggestion: available ? `Use one of the available columns: ${available}.` : `Add "${column}" to data source "${data}" or update the directive field.`, example: columnExample(node, source.columns[0]?.name) });
541
608
  }
542
609
  if (node.type === "chart") {
543
610
  const rows = dataSources[node.data]?.rows?.length ?? 0;
544
- if (rows > config.limits.maxChartRows) diagnostics.push({ severity: "warning", code: "chart_row_limit", message: `Chart uses ${rows} rows, above the ${config.limits.maxChartRows} row target.`, sourcePath, blockType: "chart" });
611
+ if (rows > config.limits.maxChartRows) diagnostics.push({ severity: "warning", code: "chart_row_limit", message: `Chart uses ${rows} rows, above the ${config.limits.maxChartRows} row target.`, sourcePath, line: node.line, blockType: "chart", suggestion: "Filter, aggregate, or sample the data before charting it." });
545
612
  }
546
613
  if (node.type === "table") {
547
614
  const rows = dataSources[node.data]?.rows?.length ?? 0;
548
- if (rows > 500 && node.pagination !== false) diagnostics.push({ severity: "info", code: "table_paginated", message: "Tables over 500 rows are paginated by default.", sourcePath, blockType: "table" });
615
+ if (rows > 500 && node.pagination !== false) diagnostics.push({ severity: "info", code: "table_paginated", message: "Tables over 500 rows are paginated by default.", sourcePath, line: node.line, blockType: "table", suggestion: "Set pageSize or filter the data if the first page is too broad." });
549
616
  }
550
- if (node.type === "component" && !config.security.allowCustomComponents) diagnostics.push({ severity: "warning", code: "custom_component_disabled", message: `Registered component "${node.name}" will render as a placeholder because custom components are disabled.`, sourcePath, blockType: "component" });
617
+ if (node.type === "component" && !config.security.allowCustomComponents) diagnostics.push({ severity: "warning", code: "custom_component_disabled", message: `Registered component "${node.name}" will render as a placeholder because custom components are disabled.`, sourcePath, line: node.line, blockType: "component", suggestion: "Enable custom components in config only for trusted projects, or replace this with a built-in primitive." });
551
618
  diagnostics.push(...validatePrimitiveSemantics(node, dataSources, sourcePath));
552
619
  if (node.type === "tabs") node.tabs.forEach((tab) => tab.children.forEach(visit));
553
620
  if (node.type === "callout") node.children?.forEach(visit);
@@ -558,28 +625,28 @@ function validateReferences(nodes, dataSources, sourcePath, config = defaultConf
558
625
  async function validateLocalArtifacts(nodes, projectRoot, sourcePath, config) {
559
626
  const diagnostics = [];
560
627
  const visit = async (node) => {
561
- if (node.type === "embed") await validateArtifactRef(node.src, "embed", projectRoot, sourcePath, config, diagnostics);
562
- if (node.type === "diagram" && node.src) await validateArtifactRef(node.src, "diagram", projectRoot, sourcePath, config, diagnostics);
628
+ if (node.type === "embed") await validateArtifactRef(node.src, "embed", projectRoot, sourcePath, config, diagnostics, node.line);
629
+ if (node.type === "diagram" && node.src) await validateArtifactRef(node.src, "diagram", projectRoot, sourcePath, config, diagnostics, node.line);
563
630
  if (node.type === "tabs") for (const tab of node.tabs) for (const child of tab.children) await visit(child);
564
631
  if (node.type === "callout") for (const child of node.children ?? []) await visit(child);
565
632
  };
566
633
  for (const node of nodes) await visit(node);
567
634
  return diagnostics;
568
635
  }
569
- async function validateArtifactRef(ref, blockType, projectRoot, sourcePath, config, diagnostics) {
636
+ async function validateArtifactRef(ref, blockType, projectRoot, sourcePath, config, diagnostics, line) {
570
637
  if (isRemoteRef(ref)) {
571
- diagnostics.push({ severity: "error", code: "remote_artifact_blocked", message: `Remote artifact reference is blocked: ${ref}`, sourcePath, blockType });
638
+ diagnostics.push({ severity: "error", code: "remote_artifact_blocked", message: `Remote artifact reference is blocked because remote artifacts are not loaded inline: ${ref}`, sourcePath, line, blockType, field: "src", suggestion: "Download the artifact into the project and reference it with a local relative path.", example: "src: ./artifacts/summary.md" });
572
639
  return;
573
640
  }
574
641
  try {
575
642
  const absolute = await resolveSafeRealPath(projectRoot, sourcePath, ref);
576
643
  const ext = path.extname(absolute).toLowerCase();
577
- if (!artifactExtensions.has(ext) && blockType === "embed") diagnostics.push({ severity: "error", code: "unsupported_artifact", message: `Unsupported artifact extension: ${ext}`, sourcePath, blockType });
578
- if (ext === ".html" && !config.security.allowHtmlEmbeds) diagnostics.push({ severity: "error", code: "html_embed_blocked", message: "HTML embeds are blocked by default.", sourcePath, blockType });
644
+ if (!artifactExtensions.has(ext) && blockType === "embed") diagnostics.push({ severity: "error", code: "unsupported_artifact", message: `Unsupported artifact extension: ${ext}`, sourcePath, line, blockType, field: "src", suggestion: "Use a supported local artifact type or link to the file from regular Markdown." });
645
+ if (ext === ".html" && !config.security.allowHtmlEmbeds) diagnostics.push({ severity: "error", code: "html_embed_blocked", message: "HTML embeds are opened or blocked for safety rather than executed inline.", sourcePath, line, blockType, field: "src", suggestion: "Use mode: link for HTML artifacts, or convert the content to Markdown/data primitives.", example: "mode: link" });
579
646
  const stat = await fs.stat(absolute);
580
- if (stat.size > config.limits.maxEmbedSizeMb * 1024 * 1024) diagnostics.push({ severity: "error", code: "embed_size_limit", message: `Artifact exceeds ${config.limits.maxEmbedSizeMb} MB limit.`, sourcePath, blockType });
647
+ if (stat.size > config.limits.maxEmbedSizeMb * 1024 * 1024) diagnostics.push({ severity: "error", code: "embed_size_limit", message: `Artifact exceeds ${config.limits.maxEmbedSizeMb} MB limit.`, sourcePath, line, blockType, field: "src", suggestion: "Reduce the artifact size or link to it instead of previewing it." });
581
648
  } catch (error) {
582
- diagnostics.push({ severity: "error", code: "artifact_file_error", message: error instanceof Error ? error.message : `Unable to access artifact ${ref}`, sourcePath, blockType });
649
+ diagnostics.push({ severity: "error", code: "artifact_file_error", message: error instanceof Error ? error.message : `Unable to access artifact ${ref}`, sourcePath, line, blockType, field: "src", suggestion: `Confirm "${ref}" exists inside the project and does not escape the project root.` });
583
650
  }
584
651
  }
585
652
  function validatePrimitiveSemantics(node, dataSources, sourcePath) {
@@ -589,37 +656,46 @@ function validatePrimitiveSemantics(node, dataSources, sourcePath) {
589
656
  const numericColumns = Array.isArray(node.y) ? node.y : node.y ? [node.y] : [];
590
657
  if (node.chartType === "pie" && node.value) numericColumns.push(node.value);
591
658
  for (const column of numericColumns) {
592
- if (columnType(source, column) && columnType(source, column) !== "number") diagnostics.push({ severity: "error", code: "column_not_numeric", message: `Column "${column}" must be numeric for ::chart.`, sourcePath, blockType: "chart" });
659
+ if (columnType(source, column) && columnType(source, column) !== "number") diagnostics.push({ severity: "error", code: "column_not_numeric", message: `Column "${column}" must be numeric for ::chart.`, sourcePath, line: node.line, blockType: "chart", field: column, suggestion: `Use a numeric column for "${column}" or convert the data values to numbers.`, example: `${node.chartType === "pie" ? "value" : "y"}: amount` });
593
660
  }
594
661
  }
595
662
  if (node.type === "metric" && node.data && node.field && node.aggregate && node.aggregate !== "count") {
596
- if (columnType(dataSources[node.data], node.field) && columnType(dataSources[node.data], node.field) !== "number") diagnostics.push({ severity: "error", code: "column_not_numeric", message: `Column "${node.field}" must be numeric for metric aggregation.`, sourcePath, blockType: "metric" });
663
+ if (columnType(dataSources[node.data], node.field) && columnType(dataSources[node.data], node.field) !== "number") diagnostics.push({ severity: "error", code: "column_not_numeric", message: `Column "${node.field}" must be numeric for metric aggregation.`, sourcePath, line: node.line, blockType: "metric", field: node.field, suggestion: "Use aggregate: count for non-numeric data, or point field at a numeric column.", example: "aggregate: count" });
597
664
  }
598
665
  if (node.type === "map") {
599
666
  const rows = dataSources[node.data]?.rows ?? [];
600
- if (node.lat && columnType(dataSources[node.data], node.lat) !== "number") diagnostics.push({ severity: "error", code: "lat_not_numeric", message: `Latitude column "${node.lat}" must be numeric.`, sourcePath, blockType: "map" });
601
- if (node.lon && columnType(dataSources[node.data], node.lon) !== "number") diagnostics.push({ severity: "error", code: "lon_not_numeric", message: `Longitude column "${node.lon}" must be numeric.`, sourcePath, blockType: "map" });
667
+ if (node.lat && columnType(dataSources[node.data], node.lat) && columnType(dataSources[node.data], node.lat) !== "number") diagnostics.push({ severity: "error", code: "lat_not_numeric", message: `Latitude column "${node.lat}" must be numeric.`, sourcePath, line: node.line, blockType: "map", field: node.lat, suggestion: "Use a latitude column with numeric decimal degree values.", example: "lat: latitude" });
668
+ if (node.lon && columnType(dataSources[node.data], node.lon) && columnType(dataSources[node.data], node.lon) !== "number") diagnostics.push({ severity: "error", code: "lon_not_numeric", message: `Longitude column "${node.lon}" must be numeric.`, sourcePath, line: node.line, blockType: "map", field: node.lon, suggestion: "Use a longitude column with numeric decimal degree values.", example: "lon: longitude" });
602
669
  for (const row of rows) {
603
670
  const lat = node.lat ? Number(row[node.lat]) : void 0;
604
671
  const lon = node.lon ? Number(row[node.lon]) : void 0;
605
- if (lat != null && Number.isFinite(lat) && (lat < -90 || lat > 90)) diagnostics.push({ severity: "error", code: "lat_out_of_range", message: "Latitude values must be between -90 and 90.", sourcePath, blockType: "map" });
606
- if (lon != null && Number.isFinite(lon) && (lon < -180 || lon > 180)) diagnostics.push({ severity: "error", code: "lon_out_of_range", message: "Longitude values must be between -180 and 180.", sourcePath, blockType: "map" });
672
+ if (lat != null && Number.isFinite(lat) && (lat < -90 || lat > 90)) diagnostics.push({ severity: "error", code: "lat_out_of_range", message: "Latitude values must be between -90 and 90.", sourcePath, line: node.line, blockType: "map", field: node.lat, suggestion: "Fix latitude values so every row is a decimal degree between -90 and 90.", example: "37.7749" });
673
+ if (lon != null && Number.isFinite(lon) && (lon < -180 || lon > 180)) diagnostics.push({ severity: "error", code: "lon_out_of_range", message: "Longitude values must be between -180 and 180.", sourcePath, line: node.line, blockType: "map", field: node.lon, suggestion: "Fix longitude values so every row is a decimal degree between -180 and 180.", example: "-122.4194" });
607
674
  }
608
675
  }
609
676
  if (node.type === "timeline") {
610
- for (const event of node.events ?? []) if (Number.isNaN(Date.parse(event.date))) diagnostics.push({ severity: "error", code: "invalid_date", message: `Timeline event date "${event.date}" is invalid.`, sourcePath, blockType: "timeline" });
677
+ for (const event of node.events ?? []) if (Number.isNaN(Date.parse(event.date))) diagnostics.push({ severity: "error", code: "invalid_date", message: `Timeline event date "${event.date}" is invalid.`, sourcePath, line: node.line, blockType: "timeline", field: "date", suggestion: "Use ISO-style dates or another format JavaScript can parse reliably.", example: "date: 2026-05-13" });
611
678
  }
612
679
  if (node.type === "form") {
613
680
  const names = /* @__PURE__ */ new Set();
614
681
  for (const field of node.fields) {
615
- if (names.has(field.name)) diagnostics.push({ severity: "error", code: "duplicate_field", message: `Duplicate form field "${field.name}".`, sourcePath, blockType: "form" });
682
+ if (names.has(field.name)) diagnostics.push({ severity: "error", code: "duplicate_field", message: `Duplicate form field "${field.name}".`, sourcePath, line: node.line, blockType: "form", field: field.name, suggestion: "Rename one of the duplicate form fields." });
616
683
  names.add(field.name);
617
- if (field.fieldType === "select" && (!field.options || field.options.length === 0)) diagnostics.push({ severity: "error", code: "select_options_required", message: `Select field "${field.name}" requires options.`, sourcePath, blockType: "form" });
618
- if (field.fieldType === "number" && field.default != null && typeof field.default !== "number") diagnostics.push({ severity: "error", code: "default_type_mismatch", message: `Default for "${field.name}" must be a number.`, sourcePath, blockType: "form" });
684
+ if (field.fieldType === "select" && (!field.options || field.options.length === 0)) diagnostics.push({ severity: "error", code: "select_options_required", message: `Select field "${field.name}" requires options.`, sourcePath, line: node.line, blockType: "form", field: field.name, suggestion: "Add at least one option to this select field.", example: "options: [A, B]" });
685
+ if (field.fieldType === "number" && field.default != null && typeof field.default !== "number") diagnostics.push({ severity: "error", code: "default_type_mismatch", message: `Default for "${field.name}" must be a number.`, sourcePath, line: node.line, blockType: "form", field: field.name, suggestion: "Use a numeric default value or remove the default.", example: "default: 10" });
619
686
  }
620
687
  }
621
688
  return diagnostics;
622
689
  }
690
+ function columnExample(node, fallbackColumn) {
691
+ const column = fallbackColumn ?? "amount";
692
+ if (node.type === "chart") return `${node.chartType === "pie" ? "value" : "y"}: ${column}`;
693
+ if (node.type === "metric") return `field: ${column}`;
694
+ if (node.type === "table") return `columns: [${column}]`;
695
+ if (node.type === "map") return `lat: ${column}`;
696
+ if (node.type === "query") return `select: [${column}]`;
697
+ return void 0;
698
+ }
623
699
  function columnType(source, column) {
624
700
  return source?.columns?.find((item) => item.name === column)?.type;
625
701
  }
@@ -627,7 +703,7 @@ function columnType(source, column) {
627
703
  // src/index.ts
628
704
  var program = new Command();
629
705
  var cliDir = path2.dirname(fileURLToPath(import.meta.url));
630
- var cliVersion = "0.1.5";
706
+ var cliVersion = "0.1.6";
631
707
  var extensionId = "AbhinavSwaminathan.agent-md-preview";
632
708
  var viewerDistCandidates = [
633
709
  path2.resolve(cliDir, "../viewer-dist"),
@@ -657,14 +733,15 @@ program.command("init").option("--agent <agent>", "agent skill flavor", "generic
657
733
  console.log(pc.gray("Browser fallback: npx agent-md serve"));
658
734
  }
659
735
  });
660
- program.command("validate").option("--file <file>", "validate a single file").option("--json", "print JSON diagnostics").option("--strict", "treat warnings as failures").option("--root <root>", "project root", ".").option("--config <config>", "config path", "agent-md.config.json").action(async (options) => {
736
+ program.command("validate").option("--file <file>", "validate a single file").option("--json", "print JSON diagnostics").option("--for-agent", "print compact deterministic JSON for repair agents").option("--strict", "treat warnings as failures").option("--root <root>", "project root", ".").option("--config <config>", "config path", "agent-md.config.json").action(async (options, command) => {
737
+ if (options.forAgent && !options.json) command.error("error: --for-agent requires --json");
661
738
  const root = path2.resolve(options.root);
662
739
  const config = await loadConfig(root, options.config);
663
740
  const files = options.file ? [path2.resolve(root, options.file)] : await scanMarkdownFiles(root, config, false);
664
741
  const results = await Promise.all(files.map((file) => parseAndResolve(file, root, config)));
665
742
  const diagnostics = results.flatMap((result) => result.diagnostics);
666
743
  if (options.json) {
667
- console.log(JSON.stringify({ files: results, diagnostics }, null, 2));
744
+ console.log(JSON.stringify(options.forAgent ? formatForAgent(results) : { files: results, diagnostics }, null, 2));
668
745
  } else {
669
746
  printDiagnostics(results);
670
747
  }
@@ -703,6 +780,24 @@ program.command("export").argument("file").option("--format <format>", "html | j
703
780
  else if (options.format === "markdown-fallback") console.log(renderFallback(document));
704
781
  else console.log(renderStaticHtml(document));
705
782
  });
783
+ program.command("convert").description("Convert an Agent Markdown document to another format").option("--file_name <file>", "Agent Markdown file to convert").option("--file-name <file>", "Agent Markdown file to convert").option("--html", "write a static HTML file").option("--output <file>", "output file path").option("--root <root>", "project root", ".").option("--config <config>", "config path", "agent-md.config.json").action(async (options, command) => {
784
+ const root = path2.resolve(options.root);
785
+ const file = options.file_name ?? options.fileName;
786
+ if (!file) command.error("error: required option '--file_name <file>' not specified");
787
+ if (!options.html) command.error("error: convert currently requires '--html'");
788
+ if (path2.extname(file) !== ".md" || !file.endsWith(".agent.md")) command.error("error: --file_name must point to a .agent.md file");
789
+ const config = await loadConfig(root, options.config);
790
+ const inputFile = path2.resolve(root, file);
791
+ const document = await parseAndResolve(inputFile, root, config);
792
+ const source = await fs2.readFile(document.sourcePath, "utf8");
793
+ const outputFile = path2.resolve(root, options.output ?? defaultHtmlOutputPath(file));
794
+ const html = await buildStaticHtml(document, source, root, config);
795
+ await fs2.mkdir(path2.dirname(outputFile), { recursive: true });
796
+ await fs2.writeFile(outputFile, html);
797
+ console.log(pc.green(`Wrote ${path2.relative(root, outputFile)}`));
798
+ printDiagnostics([document]);
799
+ process.exitCode = document.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
800
+ });
706
801
  program.command("vscode-extension").description("Print or run local VSCode/Cursor extension install instructions").option("--editor <editor>", "editor CLI to use: cursor or code", "cursor").option("--install", "install the bundled VSIX with the editor CLI").option("--force", "force reinstall when using --install", true).action(async (options) => {
707
802
  const vsix = await findVsix();
708
803
  if (!vsix) {
@@ -812,7 +907,7 @@ async function parseAndResolve(file, root, config) {
812
907
  const safeFile = await resolveSafeRealPath(root, path2.join(root, "agent-md.config.json"), file);
813
908
  const stat = await fs2.stat(safeFile);
814
909
  const diagnostics = [];
815
- if (stat.size > config.limits.maxMarkdownSizeMb * 1024 * 1024) diagnostics.push({ severity: "warning", code: "markdown_size", message: `Markdown file exceeds ${config.limits.maxMarkdownSizeMb} MB target.`, sourcePath: safeFile });
910
+ if (stat.size > config.limits.maxMarkdownSizeMb * 1024 * 1024) diagnostics.push({ severity: "warning", code: "markdown_size", message: `Markdown file exceeds ${config.limits.maxMarkdownSizeMb} MB target.`, sourcePath: safeFile, suggestion: "Split the report or move large data into local data files." });
816
911
  const source = await fs2.readFile(safeFile, "utf8");
817
912
  const parsed = parseAgentMarkdown({ source, sourcePath: safeFile });
818
913
  const resolved = await resolveDocumentData(parsed, root, config);
@@ -828,12 +923,52 @@ function printDiagnostics(results) {
828
923
  for (const diagnostic of result.diagnostics) {
829
924
  const color = diagnostic.severity === "error" ? pc.red : diagnostic.severity === "warning" ? pc.yellow : pc.blue;
830
925
  console.log(color(` ${diagnostic.severity.toUpperCase()}: ${diagnostic.message}`));
926
+ console.log(` Code: ${diagnostic.code}`);
831
927
  if (diagnostic.line) console.log(` Line: ${diagnostic.line}`);
832
928
  if (diagnostic.blockType) console.log(` Block: ::${diagnostic.blockType}`);
929
+ if (diagnostic.field) console.log(` Field: ${diagnostic.field}`);
833
930
  if (diagnostic.suggestion) console.log(` Suggestion: ${diagnostic.suggestion}`);
931
+ if (diagnostic.example) console.log(` Example: ${diagnostic.example}`);
834
932
  }
835
933
  }
836
934
  }
935
+ function formatForAgent(results) {
936
+ const files = results.map((result) => {
937
+ const diagnostics = sortDiagnostics(result.diagnostics).map(compactDiagnostic);
938
+ return {
939
+ sourcePath: result.sourcePath,
940
+ ok: !diagnostics.some((diagnostic) => diagnostic.severity === "error"),
941
+ diagnostics
942
+ };
943
+ }).sort((left, right) => left.sourcePath.localeCompare(right.sourcePath));
944
+ return {
945
+ version: 1,
946
+ ok: files.every((file) => file.ok),
947
+ files,
948
+ diagnostics: files.flatMap((file) => file.diagnostics)
949
+ };
950
+ }
951
+ function sortDiagnostics(diagnostics) {
952
+ return [...diagnostics].sort(
953
+ (left, right) => left.sourcePath.localeCompare(right.sourcePath) || (left.line ?? 0) - (right.line ?? 0) || left.severity.localeCompare(right.severity) || left.code.localeCompare(right.code) || left.message.localeCompare(right.message)
954
+ );
955
+ }
956
+ function compactDiagnostic(diagnostic) {
957
+ return omitUndefined({
958
+ severity: diagnostic.severity,
959
+ code: diagnostic.code,
960
+ message: diagnostic.message,
961
+ sourcePath: diagnostic.sourcePath,
962
+ line: diagnostic.line,
963
+ blockType: diagnostic.blockType,
964
+ field: diagnostic.field,
965
+ suggestion: diagnostic.suggestion,
966
+ example: diagnostic.example
967
+ });
968
+ }
969
+ function omitUndefined(value) {
970
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0));
971
+ }
837
972
  async function resolveRequestedDocument(root, config, allMd, file) {
838
973
  const requested = await resolveSafeRealPath(root, path2.join(root, "agent-md.config.json"), file);
839
974
  const allowed = await scanMarkdownFiles(root, config, allMd);
@@ -932,6 +1067,102 @@ async function findViewerDistDir() {
932
1067
  }
933
1068
  return void 0;
934
1069
  }
1070
+ function defaultHtmlOutputPath(file) {
1071
+ if (file.endsWith(".agent.md")) return `${file.slice(0, -".agent.md".length)}.html`;
1072
+ return `${file.slice(0, -path2.extname(file).length)}.html`;
1073
+ }
1074
+ async function buildStaticHtml(document, source, root, config) {
1075
+ const viewerDistDir = await findViewerDistDir();
1076
+ const payload = await buildStaticPayload(document, source, root, config);
1077
+ if (!viewerDistDir) return renderStaticHtml(document, source, payload);
1078
+ const indexPath = path2.join(viewerDistDir, "index.html");
1079
+ let html = await fs2.readFile(indexPath, "utf8");
1080
+ html = stripModulePreloadLinks(html);
1081
+ html = await inlineCssAssets(html, viewerDistDir);
1082
+ html = await inlineScriptAssets(html, viewerDistDir);
1083
+ return html.replace("</head>", `${staticPayloadScript(payload)}</head>`);
1084
+ }
1085
+ function stripModulePreloadLinks(html) {
1086
+ return html.replace(/<link\b[^>]*\brel="modulepreload"[^>]*>/g, "");
1087
+ }
1088
+ async function inlineCssAssets(html, viewerDistDir) {
1089
+ const cssLinkPattern = /<link\b([^>]*?)href="([^"]+\.css)"([^>]*)>/g;
1090
+ return replaceAsync(html, cssLinkPattern, async (_match, before, href, after) => {
1091
+ const css = await readViewerAsset(viewerDistDir, href);
1092
+ return `<style data-agent-md-asset="${escapeHtmlAttribute(href)}"${before.includes("media=") || after.includes("media=") ? `${before}${after}`.match(/\smedia="[^"]+"/)?.[0] ?? "" : ""}>
1093
+ ${css}
1094
+ </style>`;
1095
+ });
1096
+ }
1097
+ async function inlineScriptAssets(html, viewerDistDir) {
1098
+ const scriptPattern = /<script\b([^>]*?)src="([^"]+\.js)"([^>]*)><\/script>/g;
1099
+ return replaceAsync(html, scriptPattern, async (_match, before, src, after) => {
1100
+ const js = await readViewerAsset(viewerDistDir, src);
1101
+ const isModule = `${before}${after}`.includes('type="module"');
1102
+ const dataUrl = `data:text/javascript;base64,${Buffer.from(js).toString("base64")}`;
1103
+ return `<script${isModule ? ' type="module"' : ""} data-agent-md-asset="${escapeHtmlAttribute(src)}" src="${dataUrl}"></script>`;
1104
+ });
1105
+ }
1106
+ async function readViewerAsset(viewerDistDir, assetPath) {
1107
+ const relative = decodeURIComponent(assetPath.replace(/^\/+/, ""));
1108
+ const absolute = path2.resolve(viewerDistDir, relative);
1109
+ const insideViewer = !path2.relative(viewerDistDir, absolute).startsWith("..") && !path2.isAbsolute(path2.relative(viewerDistDir, absolute));
1110
+ if (!insideViewer) throw new Error(`Viewer asset escapes dist directory: ${assetPath}`);
1111
+ return fs2.readFile(absolute, "utf8");
1112
+ }
1113
+ async function replaceAsync(source, pattern, replacer) {
1114
+ const matches = [...source.matchAll(pattern)];
1115
+ const replacements = await Promise.all(matches.map((match) => replacer(...match.map((value) => value ?? ""))));
1116
+ let result = source;
1117
+ for (let index = matches.length - 1; index >= 0; index--) {
1118
+ const match = matches[index];
1119
+ result = `${result.slice(0, match.index)}${replacements[index]}${result.slice((match.index ?? 0) + match[0].length)}`;
1120
+ }
1121
+ return result;
1122
+ }
1123
+ async function buildStaticPayload(document, source, root, config) {
1124
+ return {
1125
+ document,
1126
+ source,
1127
+ artifacts: await collectStaticArtifacts(document, root, config),
1128
+ sourcePathLabel: path2.relative(root, document.sourcePath),
1129
+ title: typeof document.frontmatter?.title === "string" ? document.frontmatter.title : void 0
1130
+ };
1131
+ }
1132
+ async function collectStaticArtifacts(document, root, config) {
1133
+ const artifacts = {};
1134
+ for (const ref of collectArtifactRefs(document.nodes)) {
1135
+ try {
1136
+ const absolute = await resolveSafeRealPath(root, document.sourcePath, ref);
1137
+ const ext = path2.extname(absolute).toLowerCase();
1138
+ if (!artifactExtensions.has(ext) && !dataExtensions.has(ext)) continue;
1139
+ if (ext === ".html" && !config.security.allowHtmlEmbeds) continue;
1140
+ const stat = await fs2.stat(absolute);
1141
+ if (stat.size > config.limits.maxEmbedSizeMb * 1024 * 1024) continue;
1142
+ const mime = contentType(absolute);
1143
+ if (isTextArtifact(ext)) {
1144
+ artifacts[ref] = { kind: "text", mime, content: await fs2.readFile(absolute, "utf8") };
1145
+ } else {
1146
+ const body = await fs2.readFile(absolute);
1147
+ artifacts[ref] = { kind: "data", mime, dataUrl: `data:${dataUrlMime(mime)};base64,${body.toString("base64")}` };
1148
+ }
1149
+ } catch {
1150
+ }
1151
+ }
1152
+ return artifacts;
1153
+ }
1154
+ function isTextArtifact(ext) {
1155
+ return [".md", ".mmd", ".mermaid", ".txt", ".json", ".csv", ".tsv"].includes(ext);
1156
+ }
1157
+ function dataUrlMime(mime) {
1158
+ return mime.split(";")[0]?.trim() || "application/octet-stream";
1159
+ }
1160
+ function staticPayloadScript(payload) {
1161
+ return `<script>window.__AGENT_MD_STATIC__=${jsonForScript(payload)};</script>`;
1162
+ }
1163
+ function jsonForScript(value) {
1164
+ return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
1165
+ }
935
1166
  function contentType(file) {
936
1167
  const ext = path2.extname(file);
937
1168
  if (ext === ".html") return "text/html; charset=utf8";
@@ -951,14 +1182,39 @@ function contentType(file) {
951
1182
  return "application/octet-stream";
952
1183
  }
953
1184
  function renderFallback(document) {
954
- return document.nodes.map((node) => node.type === "markdown" ? node.value : `[${node.type}]`).join("\n\n");
1185
+ const diagnostics = document.diagnostics.length ? `Diagnostics
1186
+ ${document.diagnostics.map((diagnostic) => `- ${diagnostic.severity}: ${diagnostic.message}${diagnostic.suggestion ? ` Suggestion: ${diagnostic.suggestion}` : ""}`).join("\n")}
1187
+
1188
+ ` : "";
1189
+ return diagnostics + document.nodes.map((node) => node.type === "markdown" ? node.value : `[${node.type}]`).join("\n\n");
955
1190
  }
956
- function renderStaticHtml(document) {
957
- return `<!doctype html><meta charset="utf8"><title>Agent Markdown</title><pre>${escapeHtml(JSON.stringify(document, null, 2))}</pre>`;
1191
+ function renderStaticHtml(document, source, payload) {
1192
+ return `<!doctype html>
1193
+ <html><head><meta charset="utf8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Agent Markdown</title>
1194
+ ${staticPayloadScript(payload ?? { document, source: source ?? "" })}
1195
+ <style>:root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#0f172a;background:#f8fafc}body{margin:0;padding:24px}.agent-md-card{border:1px solid #e2e8f0;border-radius:10px;background:white;padding:16px;margin:12px 0}.agent-md-error{border-color:#ef4444;background:#fef2f2}.diagnostics-panel{border:1px solid #cbd5e1;border-radius:12px;background:white;padding:16px;margin:18px 0}.diagnostic{border-left:4px solid #94a3b8;padding:10px 12px;margin:10px 0;background:#f8fafc}.diagnostic.error{border-color:#ef4444}.diagnostic.warning{border-color:#f59e0b}.diagnostic.info{border-color:#3b82f6}.meta{color:#475569;font-size:13px}pre{white-space:pre-wrap;background:#0f172a;color:#e2e8f0;padding:16px;border-radius:10px;overflow:auto}table{border-collapse:collapse;width:100%}th,td{border:1px solid #e2e8f0;padding:8px;text-align:left}th{background:#f1f5f9}</style></head>
1196
+ <body><main><h1>Agent Markdown</h1>${renderStaticDiagnostics(document.diagnostics)}${document.nodes.map(renderStaticNode).join("\n")}</main></body></html>`;
958
1197
  }
959
1198
  function escapeHtml(value) {
960
1199
  return value.replace(/[&<>"]/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[char]);
961
1200
  }
1201
+ function escapeHtmlAttribute(value) {
1202
+ return escapeHtml(value);
1203
+ }
1204
+ function renderStaticNode(node) {
1205
+ if (node.type === "markdown") return `<section>${escapeHtml(node.value).replace(/\n/g, "<br>")}</section>`;
1206
+ if (node.type === "metric") return `<section class="agent-md-card"><strong>${escapeHtml(node.label)}</strong><h2>${escapeHtml(String(node.value ?? ""))}</h2></section>`;
1207
+ if (node.type === "table" || node.type === "query") return `<section class="agent-md-card"><strong>${escapeHtml(node.type)}</strong><pre>${escapeHtml(JSON.stringify(node, null, 2))}</pre></section>`;
1208
+ if (node.type === "callout") return `<section class="agent-md-card"><strong>${escapeHtml(node.title ?? node.calloutType)}</strong><p>${escapeHtml(node.body ?? "")}</p></section>`;
1209
+ if (node.type === "tabs") return `<section class="agent-md-card"><strong>Tabs</strong>${node.tabs.map((tab) => `<h3>${escapeHtml(tab.label)}</h3>${tab.children.map(renderStaticNode).join("")}`).join("")}</section>`;
1210
+ if (node.type === "error") return `<section class="agent-md-card agent-md-error"><strong>${escapeHtml(node.message)}</strong><p>Check this block's fields and data references, then run agent-md validate again.</p></section>`;
1211
+ return `<section class="agent-md-card"><strong>${escapeHtml(node.type)}</strong><pre>${escapeHtml(JSON.stringify(node, null, 2))}</pre></section>`;
1212
+ }
1213
+ function renderStaticDiagnostics(diagnostics) {
1214
+ if (diagnostics.length === 0) return "";
1215
+ const groups = ["error", "warning", "info"].map((severity) => [severity, diagnostics.filter((diagnostic) => diagnostic.severity === severity)]).filter(([, items]) => items.length > 0);
1216
+ return `<section class="diagnostics-panel" aria-label="Diagnostics"><h2>Diagnostics</h2>${groups.map(([severity, items]) => `<h3>${escapeHtml(severity)} (${items.length})</h3>${items.map((diagnostic) => `<article class="diagnostic ${escapeHtml(diagnostic.severity)}"><strong>${escapeHtml(diagnostic.message)}</strong><p class="meta">${escapeHtml([diagnostic.code, diagnostic.line ? `line ${diagnostic.line}` : "", diagnostic.blockType ? `::${diagnostic.blockType}` : "", diagnostic.field ?? ""].filter(Boolean).join(" \xB7 "))}</p>${diagnostic.suggestion ? `<p>Suggestion: ${escapeHtml(diagnostic.suggestion)}</p>` : ""}${diagnostic.example ? `<pre>${escapeHtml(diagnostic.example)}</pre>` : ""}</article>`).join("")}`).join("")}</section>`;
1217
+ }
962
1218
  async function openBrowser(url) {
963
1219
  await import("child_process").then(({ execFile }) => execFile(process.platform === "darwin" ? "open" : "xdg-open", [url]));
964
1220
  }