@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 +7 -0
- package/agent-md-preview.vsix +0 -0
- package/dist/index.js +225 -68
- package/package.json +1 -1
- package/viewer-dist/assets/index-CtGGcFUm.css +1 -0
- package/viewer-dist/assets/index-LKKPT-gN.js +3407 -0
- package/viewer-dist/index.html +2 -2
- package/viewer-dist/assets/index-B7MTTuRJ.css +0 -1
- package/viewer-dist/assets/index-w_fCrfuz.js +0 -3407
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.
|
package/agent-md-preview.vsix
CHANGED
|
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
|
-
|
|
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
|
|
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 --
|
|
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)
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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
|
@@ -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}}
|