@abuswami1996/agent-md 0.1.6 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ - Fixed rendered primitive behavior in the bundled browser viewer, including forms, tabs, timelines, query sorting, and flowchart labels.
6
+ - Added safe static artifact bundling for converted HTML so local embeds and diagram source files render without the live `/artifact` endpoint.
7
+ - Improved live viewer UX for loading, root-level file grouping, stale document selection, diagnostics, and source/sidebar click stability.
8
+ - Expanded the generated Agent Markdown skill with agent-friendly validation, preview, conversion, and repair guidance.
9
+
3
10
  ## 0.1.6
4
11
 
5
12
  - Added `agent-md convert --file_name <file.agent.md> --html` for writing static HTML files.
Binary file
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,8 +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.
332
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.
333
356
 
334
357
  Supporting files in this skill directory:
335
358
  - agent-md.config.json: default runtime configuration.
@@ -345,10 +368,41 @@ Supported MVP primitives:
345
368
  - ::tabs for grouped alternative views.
346
369
  - ::diagram, ::timeline, ::query, ::embed, ::form, ::map, and ::component are supported with conservative validation and graceful fallbacks.
347
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
+
348
384
  Useful commands:
349
- - agent-md validate --file <file.agent.md>
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
350
389
  - agent-md convert --file_name <file.agent.md> --html
351
- - agent-md convert --file_name <file.agent.md> --html --output <output.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.
352
406
  `;
353
407
  var exampleAgentMarkdown = `---
354
408
  format: agent-md
@@ -429,7 +483,7 @@ import path from "path";
429
483
  import YAML2 from "yaml";
430
484
  import Papa from "papaparse";
431
485
  var dataExtensions = /* @__PURE__ */ new Set([".csv", ".tsv", ".json", ".yaml", ".yml", ".geojson"]);
432
- 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"]);
433
487
  function isRemoteRef(ref) {
434
488
  return /^https?:\/\//i.test(ref);
435
489
  }
@@ -516,16 +570,13 @@ async function resolveDocumentData(document, projectRoot, config = defaultConfig
516
570
  const refs = [...new Set(document.nodes.flatMap(collectNodeDataRefs))];
517
571
  for (const ref of refs) {
518
572
  if (dataSources[ref]) continue;
519
- if (isRemoteRef(ref)) {
520
- diagnostics.push({ severity: "error", code: "remote_data_blocked", message: `Remote data reference is blocked: ${ref}`, sourcePath: document.sourcePath });
521
- continue;
522
- }
573
+ if (isRemoteRef(ref)) continue;
523
574
  if (dataExtensions.has(path.extname(ref).toLowerCase())) {
524
575
  try {
525
576
  const loaded = await loadDataFile(projectRoot, document.sourcePath, ref, config.limits);
526
577
  dataSources[ref] = loaded;
527
578
  } catch (error) {
528
- 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" });
529
580
  }
530
581
  }
531
582
  }
@@ -537,23 +588,33 @@ function validateReferences(nodes, dataSources, sourcePath, config = defaultConf
537
588
  const diagnostics = [];
538
589
  const visit = (node) => {
539
590
  const refs = collectNodeDataRefs(node);
540
- 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
+ }
541
601
  const columnsByData = getReferencedColumns(node);
542
602
  for (const [data, columns] of Object.entries(columnsByData)) {
543
603
  const source = dataSources[data];
544
604
  if (!source?.columns || columns.length === 0) continue;
545
605
  const known = new Set(source.columns.map((column) => column.name));
546
- 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) });
547
608
  }
548
609
  if (node.type === "chart") {
549
610
  const rows = dataSources[node.data]?.rows?.length ?? 0;
550
- 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." });
551
612
  }
552
613
  if (node.type === "table") {
553
614
  const rows = dataSources[node.data]?.rows?.length ?? 0;
554
- 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." });
555
616
  }
556
- 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." });
557
618
  diagnostics.push(...validatePrimitiveSemantics(node, dataSources, sourcePath));
558
619
  if (node.type === "tabs") node.tabs.forEach((tab) => tab.children.forEach(visit));
559
620
  if (node.type === "callout") node.children?.forEach(visit);
@@ -564,28 +625,28 @@ function validateReferences(nodes, dataSources, sourcePath, config = defaultConf
564
625
  async function validateLocalArtifacts(nodes, projectRoot, sourcePath, config) {
565
626
  const diagnostics = [];
566
627
  const visit = async (node) => {
567
- if (node.type === "embed") await validateArtifactRef(node.src, "embed", projectRoot, sourcePath, config, diagnostics);
568
- 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);
569
630
  if (node.type === "tabs") for (const tab of node.tabs) for (const child of tab.children) await visit(child);
570
631
  if (node.type === "callout") for (const child of node.children ?? []) await visit(child);
571
632
  };
572
633
  for (const node of nodes) await visit(node);
573
634
  return diagnostics;
574
635
  }
575
- async function validateArtifactRef(ref, blockType, projectRoot, sourcePath, config, diagnostics) {
636
+ async function validateArtifactRef(ref, blockType, projectRoot, sourcePath, config, diagnostics, line) {
576
637
  if (isRemoteRef(ref)) {
577
- 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" });
578
639
  return;
579
640
  }
580
641
  try {
581
642
  const absolute = await resolveSafeRealPath(projectRoot, sourcePath, ref);
582
643
  const ext = path.extname(absolute).toLowerCase();
583
- if (!artifactExtensions.has(ext) && blockType === "embed") diagnostics.push({ severity: "error", code: "unsupported_artifact", message: `Unsupported artifact extension: ${ext}`, sourcePath, blockType });
584
- 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" });
585
646
  const stat = await fs.stat(absolute);
586
- 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." });
587
648
  } catch (error) {
588
- 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.` });
589
650
  }
590
651
  }
591
652
  function validatePrimitiveSemantics(node, dataSources, sourcePath) {
@@ -595,37 +656,46 @@ function validatePrimitiveSemantics(node, dataSources, sourcePath) {
595
656
  const numericColumns = Array.isArray(node.y) ? node.y : node.y ? [node.y] : [];
596
657
  if (node.chartType === "pie" && node.value) numericColumns.push(node.value);
597
658
  for (const column of numericColumns) {
598
- 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` });
599
660
  }
600
661
  }
601
662
  if (node.type === "metric" && node.data && node.field && node.aggregate && node.aggregate !== "count") {
602
- 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" });
603
664
  }
604
665
  if (node.type === "map") {
605
666
  const rows = dataSources[node.data]?.rows ?? [];
606
- 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" });
607
- 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" });
608
669
  for (const row of rows) {
609
670
  const lat = node.lat ? Number(row[node.lat]) : void 0;
610
671
  const lon = node.lon ? Number(row[node.lon]) : void 0;
611
- 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" });
612
- 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" });
613
674
  }
614
675
  }
615
676
  if (node.type === "timeline") {
616
- 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" });
617
678
  }
618
679
  if (node.type === "form") {
619
680
  const names = /* @__PURE__ */ new Set();
620
681
  for (const field of node.fields) {
621
- 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." });
622
683
  names.add(field.name);
623
- 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" });
624
- 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" });
625
686
  }
626
687
  }
627
688
  return diagnostics;
628
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
+ }
629
699
  function columnType(source, column) {
630
700
  return source?.columns?.find((item) => item.name === column)?.type;
631
701
  }
@@ -663,14 +733,15 @@ program.command("init").option("--agent <agent>", "agent skill flavor", "generic
663
733
  console.log(pc.gray("Browser fallback: npx agent-md serve"));
664
734
  }
665
735
  });
666
- 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");
667
738
  const root = path2.resolve(options.root);
668
739
  const config = await loadConfig(root, options.config);
669
740
  const files = options.file ? [path2.resolve(root, options.file)] : await scanMarkdownFiles(root, config, false);
670
741
  const results = await Promise.all(files.map((file) => parseAndResolve(file, root, config)));
671
742
  const diagnostics = results.flatMap((result) => result.diagnostics);
672
743
  if (options.json) {
673
- console.log(JSON.stringify({ files: results, diagnostics }, null, 2));
744
+ console.log(JSON.stringify(options.forAgent ? formatForAgent(results) : { files: results, diagnostics }, null, 2));
674
745
  } else {
675
746
  printDiagnostics(results);
676
747
  }
@@ -720,7 +791,7 @@ program.command("convert").description("Convert an Agent Markdown document to an
720
791
  const document = await parseAndResolve(inputFile, root, config);
721
792
  const source = await fs2.readFile(document.sourcePath, "utf8");
722
793
  const outputFile = path2.resolve(root, options.output ?? defaultHtmlOutputPath(file));
723
- const html = await buildStaticHtml(document, source);
794
+ const html = await buildStaticHtml(document, source, root, config);
724
795
  await fs2.mkdir(path2.dirname(outputFile), { recursive: true });
725
796
  await fs2.writeFile(outputFile, html);
726
797
  console.log(pc.green(`Wrote ${path2.relative(root, outputFile)}`));
@@ -836,7 +907,7 @@ async function parseAndResolve(file, root, config) {
836
907
  const safeFile = await resolveSafeRealPath(root, path2.join(root, "agent-md.config.json"), file);
837
908
  const stat = await fs2.stat(safeFile);
838
909
  const diagnostics = [];
839
- 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." });
840
911
  const source = await fs2.readFile(safeFile, "utf8");
841
912
  const parsed = parseAgentMarkdown({ source, sourcePath: safeFile });
842
913
  const resolved = await resolveDocumentData(parsed, root, config);
@@ -852,12 +923,52 @@ function printDiagnostics(results) {
852
923
  for (const diagnostic of result.diagnostics) {
853
924
  const color = diagnostic.severity === "error" ? pc.red : diagnostic.severity === "warning" ? pc.yellow : pc.blue;
854
925
  console.log(color(` ${diagnostic.severity.toUpperCase()}: ${diagnostic.message}`));
926
+ console.log(` Code: ${diagnostic.code}`);
855
927
  if (diagnostic.line) console.log(` Line: ${diagnostic.line}`);
856
928
  if (diagnostic.blockType) console.log(` Block: ::${diagnostic.blockType}`);
929
+ if (diagnostic.field) console.log(` Field: ${diagnostic.field}`);
857
930
  if (diagnostic.suggestion) console.log(` Suggestion: ${diagnostic.suggestion}`);
931
+ if (diagnostic.example) console.log(` Example: ${diagnostic.example}`);
858
932
  }
859
933
  }
860
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
+ }
861
972
  async function resolveRequestedDocument(root, config, allMd, file) {
862
973
  const requested = await resolveSafeRealPath(root, path2.join(root, "agent-md.config.json"), file);
863
974
  const allowed = await scanMarkdownFiles(root, config, allMd);
@@ -960,10 +1071,10 @@ function defaultHtmlOutputPath(file) {
960
1071
  if (file.endsWith(".agent.md")) return `${file.slice(0, -".agent.md".length)}.html`;
961
1072
  return `${file.slice(0, -path2.extname(file).length)}.html`;
962
1073
  }
963
- async function buildStaticHtml(document, source) {
1074
+ async function buildStaticHtml(document, source, root, config) {
964
1075
  const viewerDistDir = await findViewerDistDir();
965
- const payload = { document, source };
966
- if (!viewerDistDir) return renderStaticHtml(document, source);
1076
+ const payload = await buildStaticPayload(document, source, root, config);
1077
+ if (!viewerDistDir) return renderStaticHtml(document, source, payload);
967
1078
  const indexPath = path2.join(viewerDistDir, "index.html");
968
1079
  let html = await fs2.readFile(indexPath, "utf8");
969
1080
  html = stripModulePreloadLinks(html);
@@ -1009,6 +1120,43 @@ async function replaceAsync(source, pattern, replacer) {
1009
1120
  }
1010
1121
  return result;
1011
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
+ }
1012
1160
  function staticPayloadScript(payload) {
1013
1161
  return `<script>window.__AGENT_MD_STATIC__=${jsonForScript(payload)};</script>`;
1014
1162
  }
@@ -1034,14 +1182,18 @@ function contentType(file) {
1034
1182
  return "application/octet-stream";
1035
1183
  }
1036
1184
  function renderFallback(document) {
1037
- 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");
1038
1190
  }
1039
- function renderStaticHtml(document, source) {
1191
+ function renderStaticHtml(document, source, payload) {
1040
1192
  return `<!doctype html>
1041
1193
  <html><head><meta charset="utf8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Agent Markdown</title>
1042
- ${staticPayloadScript({ document, source: source ?? "" })}
1043
- <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}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>
1044
- <body><main><h1>Agent Markdown</h1>${document.nodes.map(renderStaticNode).join("\n")}</main></body></html>`;
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>`;
1045
1197
  }
1046
1198
  function escapeHtml(value) {
1047
1199
  return value.replace(/[&<>"]/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[char]);
@@ -1055,9 +1207,14 @@ function renderStaticNode(node) {
1055
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>`;
1056
1208
  if (node.type === "callout") return `<section class="agent-md-card"><strong>${escapeHtml(node.title ?? node.calloutType)}</strong><p>${escapeHtml(node.body ?? "")}</p></section>`;
1057
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>`;
1058
- if (node.type === "error") return `<section class="agent-md-card"><strong>${escapeHtml(node.message)}</strong></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>`;
1059
1211
  return `<section class="agent-md-card"><strong>${escapeHtml(node.type)}</strong><pre>${escapeHtml(JSON.stringify(node, null, 2))}</pre></section>`;
1060
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
+ }
1061
1218
  async function openBrowser(url) {
1062
1219
  await import("child_process").then(({ execFile }) => execFile(process.platform === "darwin" ? "open" : "xdg-open", [url]));
1063
1220
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abuswami1996/agent-md",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Local-first Agent Markdown runtime, validator, browser viewer, and agent skill installer.",
5
5
  "license": "MIT",
6
6
  "author": "Abhinav Swaminathan",
@@ -0,0 +1 @@
1
+ :root{--background:#f8fafc;--card:#fff;--muted:#f1f5f9;--muted-foreground:#64748b;--foreground:#0f172a;--border:#e2e8f0;--primary:#2563eb;--danger:#dc2626;--warning:#d97706;--agent-md-chart-1:#2563eb;--agent-md-chart-2:#16a34a;--agent-md-chart-3:#f59e0b;color:var(--foreground);background:var(--background);font-family:Inter,ui-sans-serif,system-ui,sans-serif}body{margin:0}.app-frame{background:var(--background);min-height:100vh;padding:16px}.browser-shell{border:1px solid var(--border);background:var(--card);border-radius:10px;flex-direction:column;min-height:min(75vh,680px);display:flex;overflow:hidden}.browser-shell.static-shell{min-height:auto}.file-sidebar{z-index:2;border-bottom:1px solid color-mix(in srgb,var(--border) 60%,transparent);background:var(--card);width:100%;position:relative}.sidebar-strip{background:color-mix(in srgb,var(--muted) 40%,transparent);border-bottom:1px solid color-mix(in srgb,var(--border) 60%,transparent);color:var(--muted-foreground);padding:9px 12px;font-size:12px;font-weight:500}.file-tree{overscroll-behavior:contain;max-height:min(40vh,320px);overflow:auto}.folder-group{border-bottom:1px solid color-mix(in srgb,var(--border) 60%,transparent)}.folder-group:last-child{border-bottom:0}.folder-row{background:color-mix(in srgb,var(--muted) 25%,transparent);align-items:flex-start;gap:8px;padding:8px 12px;display:flex}.folder-name{letter-spacing:.06em;text-transform:uppercase;color:var(--muted-foreground);font-size:11px;font-weight:700}.folder-count{color:color-mix(in srgb,var(--muted-foreground) 80%,transparent);font-size:10px}ul{margin:0;padding:4px 0;list-style:none}.file-row{text-align:left;width:100%;color:var(--muted-foreground);cursor:pointer;background:0 0;border:0;align-items:flex-start;gap:8px;padding:7px 12px;font-size:12px;line-height:1.25;transition:background-color .12s,color .12s;display:flex}.file-row span:nth-child(2){overflow-wrap:anywhere;min-width:0}.file-row:hover{background:color-mix(in srgb,var(--muted) 80%,transparent);color:var(--foreground)}.file-row.active{background:color-mix(in srgb,var(--primary) 10%,transparent);color:var(--foreground);font-weight:500}.tree-icon{flex:none;width:14px;height:14px}.folder-icon{color:var(--muted-foreground)}.file-icon{opacity:.7;margin-top:2px}.status{border-radius:999px;flex:none;width:7px;height:7px;margin-top:4px;margin-left:auto}.ok-dot{background:#16a34a}.warning-dot{background:var(--warning)}.error-dot{background:var(--danger)}.preview-pane{z-index:1;flex-direction:column;flex:1;min-width:0;display:flex;position:relative;overflow:auto}.preview-header{z-index:3;border-bottom:1px solid color-mix(in srgb,var(--border) 60%,transparent);background:var(--card);justify-content:space-between;align-items:flex-start;gap:16px;padding:14px 16px;display:flex;position:sticky;top:0}.preview-header h3{overflow-wrap:anywhere;margin:0;font-size:13px;font-weight:600;line-height:1.35}.preview-header p{color:var(--muted-foreground);margin:3px 0 0;font-size:11px}.mode-button{border:1px solid var(--border);background:var(--card);color:var(--muted-foreground);cursor:pointer;border-radius:6px;padding:5px 8px;font-size:11px}.mode-button:hover{background:var(--muted);color:var(--foreground)}.preview-body{min-width:0;padding:16px;overflow:auto}.diagnostics-panel{border-top:1px solid color-mix(in srgb,var(--border) 60%,transparent);background:color-mix(in srgb,var(--muted) 30%,transparent);padding:8px 12px}.diagnostics-panel p{color:var(--muted-foreground);margin:4px 0;font-size:11px}.empty-state{color:var(--muted-foreground);font-size:13px}.agent-md-document{font-size:13px;line-height:1.55}.agent-md-document h1{margin:0 0 14px;font-size:24px}.agent-md-document h2{margin:18px 0 10px;font-size:18px}.agent-md-document h3{margin:0 0 10px;font-size:13px}.agent-md-card{border:1px solid color-mix(in srgb,var(--border) 75%,transparent);background:var(--card);box-shadow:none;border-radius:8px;margin:10px 0;padding:12px}.agent-md-embed-header{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.agent-md-embed-header h3{margin-bottom:2px}.agent-md-embed-header small,.agent-md-embed-caption,.agent-md-embed-loading{color:var(--muted-foreground);font-size:11px}.agent-md-embed-header a{color:var(--primary);font-size:12px;text-decoration:none}.agent-md-embed-header a:hover{text-decoration:underline}.agent-md-embed-image,.agent-md-embed-media{border:1px solid var(--border);border-radius:8px;max-width:100%;display:block}.agent-md-embed-markdown{border-top:1px solid var(--border);margin-top:10px;padding-top:10px}.agent-md-embed-text{max-height:360px}.agent-md-metric strong{margin-top:6px;font-size:22px;display:block}.agent-md-error,.error{border-color:color-mix(in srgb,var(--danger) 35%,var(--border));background:color-mix(in srgb,var(--danger) 6%,var(--card))}.warning{border-color:color-mix(in srgb,var(--warning) 35%,var(--border));background:color-mix(in srgb,var(--warning) 6%,var(--card))}.agent-md-tabs>div:first-child{border-bottom:1px solid var(--border);gap:4px;margin-bottom:10px;display:flex}.agent-md-tabs button{color:var(--muted-foreground);cursor:pointer;background:0 0;border:0;padding:6px 8px;font-size:12px}.agent-md-tabs button:hover,.agent-md-tabs button.active{background:var(--muted);color:var(--foreground)}.agent-md-tabs-pill>div:first-child{border-bottom:0}.agent-md-tabs-pill button{border-radius:999px}.agent-md-tabs-pill button.active{background:var(--primary);color:#fff}.agent-md-tabs-card>div:first-child{border-bottom:0}.agent-md-tabs-card button{border:1px solid var(--border);background:var(--card);border-radius:6px}.agent-md-tabs-card button.active{background:var(--muted);border-color:var(--primary)}.agent-md-timeline-horizontal{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;display:grid}.agent-md-timeline-event{border-left:2px solid var(--border);margin:10px 0;padding-left:10px}.agent-md-timeline-horizontal .agent-md-timeline-event{border-left:0;border-top:2px solid var(--border);padding-top:10px;padding-left:0}.agent-md-timeline-group,.agent-md-timeline-description{color:var(--muted-foreground)}.agent-md-form-fields{gap:10px;margin:10px 0;display:grid}.agent-md-form-field{gap:4px;display:grid}.agent-md-form-field-checkbox{align-items:center;gap:8px;display:flex}.agent-md-form-field-checkbox input{width:auto}.agent-md-form input,.agent-md-form select{max-width:360px}.agent-md-form-submit{border:1px solid var(--border);background:var(--primary);color:#fff;cursor:pointer;border-radius:6px;padding:6px 10px;font-size:12px}.agent-md-component-placeholder p{color:var(--muted-foreground);margin-bottom:0}.diagnostics-panel .info{border-color:color-mix(in srgb,var(--primary) 25%,var(--border));background:color-mix(in srgb,var(--primary) 5%,var(--card))}.agent-md-card svg text,.agent-md-card svg tspan{fill:var(--foreground)}.agent-md-card svg foreignObject,.agent-md-card svg .nodeLabel,.agent-md-card svg .nodeLabel p{color:var(--foreground)}.agent-md-simple-flow{color:var(--foreground);flex-wrap:wrap;align-items:center;gap:12px;padding:18px 8px;display:flex}.agent-md-simple-flow-vertical{flex-direction:column;align-items:flex-start}.agent-md-simple-flow-node{color:#0f172a;text-align:center;background:#eef2ff;border:2px solid #8b5cf6;border-radius:8px;min-width:120px;padding:18px 20px;font-weight:700}.agent-md-simple-flow-arrow{color:#0f172a;font-weight:700}table{border-collapse:collapse;width:100%;font-size:12px}th,td{border:1px solid color-mix(in srgb,var(--border) 70%,transparent);text-align:left;padding:6px 8px}th{background:color-mix(in srgb,var(--muted) 45%,transparent);cursor:pointer;color:var(--muted-foreground);font-size:11px}input,select{border:1px solid var(--border);border-radius:6px;padding:6px 8px;font-size:12px}pre{color:#e2e8f0;background:#0f172a;border-radius:8px;padding:12px;font-size:12px;overflow:auto}@media (width>=760px){.browser-shell{flex-direction:row}.file-sidebar{border-bottom:0;border-right:1px solid color-mix(in srgb,var(--border) 60%,transparent);width:min(100%,280px)}.file-tree{max-height:min(75vh,720px)}.preview-body{padding:18px 22px}}@media (width>=1280px){.file-sidebar{width:320px}}