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