@abuswami1996/agent-md 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +43 -0
  4. package/agent-md-preview.vsix +0 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +932 -0
  7. package/package.json +45 -0
  8. package/viewer-dist/assets/arc-BGWRvh4A.js +1 -0
  9. package/viewer-dist/assets/architecture-7EHR7CIX-tMtXFVEB.js +1 -0
  10. package/viewer-dist/assets/architectureDiagram-3BPJPVTR-BpnMGpeV.js +36 -0
  11. package/viewer-dist/assets/array-CLXCPui0.js +1 -0
  12. package/viewer-dist/assets/blockDiagram-GPEHLZMM-BHk4-xwc.js +132 -0
  13. package/viewer-dist/assets/c4Diagram-AAUBKEIU-BShCidQJ.js +10 -0
  14. package/viewer-dist/assets/channel-BeF3oGSA.js +1 -0
  15. package/viewer-dist/assets/chunk-2J33WTMH-D_hBYQSk.js +1 -0
  16. package/viewer-dist/assets/chunk-3OPIFGDE-yFgTZecx.js +62 -0
  17. package/viewer-dist/assets/chunk-4BX2VUAB-BrcCtoyJ.js +1 -0
  18. package/viewer-dist/assets/chunk-4EGX6M5U-sKH2Isf8.js +1 -0
  19. package/viewer-dist/assets/chunk-55IACEB6-DbDA7kYA.js +1 -0
  20. package/viewer-dist/assets/chunk-5DO6E6H7-DVriMWxp.js +1 -0
  21. package/viewer-dist/assets/chunk-5ZQYHXKU-DQ5fX41l.js +2 -0
  22. package/viewer-dist/assets/chunk-727SXJPM-SK34XSEC.js +206 -0
  23. package/viewer-dist/assets/chunk-AQP2D5EJ-DXK-1dYB.js +231 -0
  24. package/viewer-dist/assets/chunk-BR22UD5L-BCm2xrxa.js +1 -0
  25. package/viewer-dist/assets/chunk-BSJP7CBP-B3Tyh4s5.js +1 -0
  26. package/viewer-dist/assets/chunk-CSCIHK7Q-BmRiMG-E.js +123 -0
  27. package/viewer-dist/assets/chunk-FHYWG6QK-B_z4diOF.js +1 -0
  28. package/viewer-dist/assets/chunk-FMBD7UC4-Dxft-q4P.js +15 -0
  29. package/viewer-dist/assets/chunk-KSCS5N6A-eA-nZKPR.js +10 -0
  30. package/viewer-dist/assets/chunk-L5ZTLDWV-CJt5R1DD.js +1 -0
  31. package/viewer-dist/assets/chunk-MPE355IW-BrU23f3r.js +1 -0
  32. package/viewer-dist/assets/chunk-MZUSXYTE-BGj-wpbJ.js +1 -0
  33. package/viewer-dist/assets/chunk-N66VUXT2-Bx9F_D4P.js +1 -0
  34. package/viewer-dist/assets/chunk-ND2GUHAM-DhxorC-R.js +1 -0
  35. package/viewer-dist/assets/chunk-NNHCCRGN-DSaPFNsL.js +159 -0
  36. package/viewer-dist/assets/chunk-NZK2D7GU-DaLfDAf-.js +1 -0
  37. package/viewer-dist/assets/chunk-O5CBEL6O-D0rppU0M.js +70 -0
  38. package/viewer-dist/assets/chunk-PUPMXCY4-C2l0dHY4.js +1 -0
  39. package/viewer-dist/assets/chunk-QZHKN3VN-CqO4Iw07.js +1 -0
  40. package/viewer-dist/assets/chunk-UIBZB4QT-DXVjm6Jo.js +1 -0
  41. package/viewer-dist/assets/chunk-WCWK7LTN-zc10zcLo.js +1 -0
  42. package/viewer-dist/assets/classDiagram-4FO5ZUOK-tR_J6scJ.js +1 -0
  43. package/viewer-dist/assets/classDiagram-v2-Q7XG4LA2-DStV2Q4p.js +1 -0
  44. package/viewer-dist/assets/cose-bilkent-S5V4N54A-SSDlF1_a.js +1 -0
  45. package/viewer-dist/assets/cytoscape.esm-mg8EQp5B.js +321 -0
  46. package/viewer-dist/assets/dagre-BM42HDAG-CbZJzBab.js +4 -0
  47. package/viewer-dist/assets/dagre-CdwzCXQr.js +1 -0
  48. package/viewer-dist/assets/defaultLocale-Dda4OpKy.js +1 -0
  49. package/viewer-dist/assets/diagram-2AECGRRQ-BB3K5hE5.js +43 -0
  50. package/viewer-dist/assets/diagram-5GNKFQAL-DWmbyuQH.js +10 -0
  51. package/viewer-dist/assets/diagram-KO2AKTUF-CYSKyVh3.js +3 -0
  52. package/viewer-dist/assets/diagram-LMA3HP47-CyTMJfPw.js +24 -0
  53. package/viewer-dist/assets/diagram-OG6HWLK6-72YuHoex.js +24 -0
  54. package/viewer-dist/assets/dist-Cz_gOj_F.js +1 -0
  55. package/viewer-dist/assets/erDiagram-TEJ5UH35-B7Z-qey-.js +85 -0
  56. package/viewer-dist/assets/eventmodeling-FCH6USID-Cmphiy0Z.js +1 -0
  57. package/viewer-dist/assets/flowDiagram-I6XJVG4X-Bd8YQF0F.js +162 -0
  58. package/viewer-dist/assets/ganttDiagram-6RSMTGT7-B0R6fBcN.js +292 -0
  59. package/viewer-dist/assets/gitGraph-WXDBUCRP-CABAU2x_.js +1 -0
  60. package/viewer-dist/assets/gitGraphDiagram-PVQCEYII-DgTF7l00.js +106 -0
  61. package/viewer-dist/assets/graphlib-B0DVs0bw.js +1 -0
  62. package/viewer-dist/assets/index-BiU7pYUM.css +1 -0
  63. package/viewer-dist/assets/index-C_Rab63U.js +99 -0
  64. package/viewer-dist/assets/info-J43DQDTF-CtHtznhp.js +1 -0
  65. package/viewer-dist/assets/infoDiagram-5YYISTIA-D3ae-MRJ.js +2 -0
  66. package/viewer-dist/assets/init-Ct4VudL3.js +1 -0
  67. package/viewer-dist/assets/ishikawaDiagram-YF4QCWOH-BpRQFD12.js +70 -0
  68. package/viewer-dist/assets/journeyDiagram-JHISSGLW-DTbccBmQ.js +139 -0
  69. package/viewer-dist/assets/kanban-definition-UN3LZRKU-DBpDPAaX.js +89 -0
  70. package/viewer-dist/assets/katex-pVJiI_rc.js +257 -0
  71. package/viewer-dist/assets/line-CSkXZgM5.js +1 -0
  72. package/viewer-dist/assets/linear-C69MUSpN.js +1 -0
  73. package/viewer-dist/assets/mermaid-parser.core-BxtmlK9F.js +4 -0
  74. package/viewer-dist/assets/mindmap-definition-RKZ34NQL-CJfLUd4u.js +96 -0
  75. package/viewer-dist/assets/ordinal-bHkc4Rpt.js +1 -0
  76. package/viewer-dist/assets/packet-YPE3B663-DmtvbI4l.js +1 -0
  77. package/viewer-dist/assets/path-ZGOlHDxT.js +1 -0
  78. package/viewer-dist/assets/pie-LRSECV5Y-Y82O9nng.js +1 -0
  79. package/viewer-dist/assets/pieDiagram-4H26LBE5-BsHXB8IC.js +30 -0
  80. package/viewer-dist/assets/quadrantDiagram-W4KKPZXB-B3RaddW4.js +7 -0
  81. package/viewer-dist/assets/radar-GUYGQ44K-BHcHsGeF.js +1 -0
  82. package/viewer-dist/assets/requirementDiagram-4Y6WPE33-CanuKYuP.js +84 -0
  83. package/viewer-dist/assets/rough.esm-C6nrIGLg.js +1 -0
  84. package/viewer-dist/assets/sankeyDiagram-5OEKKPKP-B92qVA_9.js +40 -0
  85. package/viewer-dist/assets/sequenceDiagram-3UESZ5HK-IBhScJkH.js +162 -0
  86. package/viewer-dist/assets/src--c83fjKW.js +1 -0
  87. package/viewer-dist/assets/stateDiagram-AJRCARHV-D9ORfcYk.js +1 -0
  88. package/viewer-dist/assets/stateDiagram-v2-BHNVJYJU-CCmjUx_g.js +1 -0
  89. package/viewer-dist/assets/timeline-definition-PNZ67QCA-D4afYQ3m.js +120 -0
  90. package/viewer-dist/assets/treeView-BLDUP644-Cmu4mPLp.js +1 -0
  91. package/viewer-dist/assets/treemap-LRROVOQU-_N62KlUg.js +1 -0
  92. package/viewer-dist/assets/vennDiagram-CIIHVFJN-DjSaUOWp.js +34 -0
  93. package/viewer-dist/assets/wardley-L42UT6IY-DWSXD3L4.js +1 -0
  94. package/viewer-dist/assets/wardleyDiagram-YWT4CUSO-CBn2PjeZ.js +78 -0
  95. package/viewer-dist/assets/xychartDiagram-2RQKCTM6-CAAAsGIb.js +7 -0
  96. package/viewer-dist/index.html +22 -0
package/dist/index.js ADDED
@@ -0,0 +1,932 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import fs2 from "fs/promises";
5
+ import path2 from "path";
6
+ import http from "http";
7
+ import { fileURLToPath } from "url";
8
+ import { Command } from "commander";
9
+ import pc from "picocolors";
10
+ import fg from "fast-glob";
11
+ import chokidar from "chokidar";
12
+ import { WebSocketServer } from "ws";
13
+
14
+ // ../schema/src/index.ts
15
+ import { z } from "zod";
16
+ var defaultConfig = {
17
+ version: "0.1",
18
+ include: ["**/*.agent.md", "**/*.amd.md"],
19
+ exclude: ["node_modules/**", ".git/**", "dist/**", "build/**"],
20
+ server: { port: 3847, host: "localhost", open: true },
21
+ security: { allowRawHtml: false, allowRemoteData: false, allowHtmlEmbeds: false, allowCustomComponents: false, projectRootOnly: true, maxEmbedSizeMb: 25 },
22
+ components: { registry: ".agent-md/components.json" },
23
+ limits: { maxMarkdownSizeMb: 2, maxDataSizeMb: 10, maxEmbedSizeMb: 25, maxChartRows: 1e4 }
24
+ };
25
+ var positiveNumber = z.number().positive();
26
+ var stringArray = z.array(z.string());
27
+ var chartBase = z.object({ type: z.enum(["line", "bar", "area", "scatter", "pie"]), title: z.string().optional(), description: z.string().optional(), data: z.string(), x: z.string().optional(), y: z.union([z.string(), stringArray]).optional(), label: z.string().optional(), value: z.string().optional(), series: z.string().optional(), xLabel: z.string().optional(), yLabel: z.string().optional(), height: positiveNumber.optional(), width: positiveNumber.optional(), stacked: z.boolean().optional(), legend: z.boolean().optional(), tooltip: z.boolean().optional() }).strict();
28
+ var metricSchema = z.object({ label: z.string(), value: z.union([z.string(), z.number()]).optional(), delta: z.union([z.string(), z.number()]).optional(), trend: z.enum(["up", "down", "neutral"]).optional(), description: z.string().optional(), data: z.string().optional(), field: z.string().optional(), format: z.string().optional(), aggregate: z.enum(["sum", "avg", "min", "max", "count"]).optional() }).strict();
29
+ var tableSchema = z.object({ title: z.string().optional(), data: z.string(), columns: stringArray.optional(), sortable: z.boolean().optional(), filterable: z.boolean().optional(), pagination: z.boolean().optional(), pageSize: positiveNumber.optional(), search: z.boolean().optional() }).strict();
30
+ var diagramSchema = z.object({ type: z.enum(["flowchart", "sequence", "tree"]), title: z.string().optional(), source: z.string().optional(), src: z.string().optional(), direction: z.enum(["TB", "LR", "BT", "RL"]).optional(), height: positiveNumber.optional() }).strict();
31
+ var mapSchema = z.object({ title: z.string().optional(), data: z.string(), lat: z.string().optional(), lon: z.string().optional(), label: z.string().optional(), value: z.string().optional(), height: positiveNumber.optional(), zoom: z.number().optional(), center: z.tuple([z.number(), z.number()]).optional() }).strict();
32
+ var timelineSchema = z.object({ data: z.string().optional(), date: z.string().optional(), title: z.string().optional(), description: z.string().optional(), group: z.string().optional(), sort: z.enum(["asc", "desc"]).optional(), layout: z.enum(["vertical", "horizontal"]).optional(), events: z.array(z.object({ date: z.string(), title: z.string(), description: z.string().optional(), group: z.string().optional() })).optional() }).strict();
33
+ var tabsSchema = z.object({ default: z.string().optional(), variant: z.enum(["line", "pill", "card"]).optional() }).strict();
34
+ var calloutSchema = z.object({ type: z.enum(["note", "info", "warning", "error", "success", "decision", "risk", "tip"]).optional(), title: z.string().optional(), body: z.string().optional() }).strict();
35
+ var embedSchema = z.object({ src: z.string(), title: z.string().optional(), height: positiveNumber.optional(), width: positiveNumber.optional(), caption: z.string().optional(), mode: z.enum(["preview", "link", "inline"]).optional() }).strict();
36
+ var formFieldSchema = z.object({ name: z.string(), label: z.string().optional(), type: z.enum(["text", "number", "select", "checkbox", "date"]), default: z.unknown().optional(), placeholder: z.string().optional(), required: z.boolean().optional(), options: stringArray.optional(), min: z.number().optional(), max: z.number().optional(), step: z.number().optional() }).strict();
37
+ var formSchema = z.object({ title: z.string().optional(), description: z.string().optional(), submitLabel: z.string().optional(), fields: z.array(formFieldSchema) }).strict();
38
+ var queryPredicateSchema = z.union([z.string(), z.number(), z.boolean(), z.object({ eq: z.unknown().optional(), neq: z.unknown().optional(), gt: z.number().optional(), gte: z.number().optional(), lt: z.number().optional(), lte: z.number().optional(), contains: z.string().optional(), in: z.array(z.unknown()).optional() }).strict()]);
39
+ var querySchema = z.object({ data: z.string(), where: z.record(z.string(), queryPredicateSchema).optional(), select: stringArray.optional(), sort: z.object({ by: z.string(), direction: z.enum(["asc", "desc"]).optional() }).strict().optional(), limit: positiveNumber.optional(), view: z.enum(["table", "json", "cards"]).optional() }).strict();
40
+ var componentSchema = z.object({ name: z.string(), props: z.record(z.string(), z.unknown()).optional() }).strict();
41
+ var schemas = { chart: chartBase, metric: metricSchema, table: tableSchema, diagram: diagramSchema, map: mapSchema, timeline: timelineSchema, tabs: tabsSchema, callout: calloutSchema, embed: embedSchema, form: formSchema, query: querySchema, component: componentSchema };
42
+ var primitiveNames = Object.keys(schemas);
43
+ function issueToDiagnostic(issue, sourcePath, blockType, line) {
44
+ const unknown = issue.code === "unrecognized_keys";
45
+ const keys = unknown && "keys" in issue ? issue.keys.join(", ") : issue.path.join(".");
46
+ return { severity: unknown ? "warning" : "error", code: unknown ? "unknown_field" : "invalid_field", message: unknown ? `Unknown field "${keys}" on ::${blockType}.` : `${issue.path.join(".") || blockType}: ${issue.message}`, sourcePath, line, blockType };
47
+ }
48
+ function validatePrimitive(name, attrs, sourcePath, line) {
49
+ if (!primitiveNames.includes(name)) {
50
+ return { diagnostics: [{ severity: "error", code: "unknown_primitive", message: `Unknown directive ::${name}.`, sourcePath, line, blockType: name }] };
51
+ }
52
+ const result = schemas[name].safeParse(attrs);
53
+ if (result.success) return { attrs: result.data, diagnostics: [] };
54
+ return { attrs, diagnostics: result.error.issues.map((issue) => issueToDiagnostic(issue, sourcePath, name, line)) };
55
+ }
56
+ function normalizePrimitive(name, attrs, children, raw, sourcePath, line) {
57
+ const validation = validatePrimitive(name, attrs, sourcePath, line);
58
+ const hasError = validation.diagnostics.some((diagnostic) => diagnostic.severity === "error");
59
+ if (hasError || !validation.attrs) return { node: { type: "error", message: `Invalid ::${name} directive`, raw, line }, diagnostics: validation.diagnostics };
60
+ const data = validation.attrs;
61
+ switch (name) {
62
+ case "chart":
63
+ return { node: { ...data, type: "chart", chartType: data.type }, diagnostics: validation.diagnostics };
64
+ case "metric":
65
+ return { node: { ...data, type: "metric" }, diagnostics: validation.diagnostics };
66
+ case "table":
67
+ return { node: { ...data, type: "table" }, diagnostics: validation.diagnostics };
68
+ case "diagram":
69
+ return { node: { ...data, type: "diagram", diagramType: data.type }, diagnostics: validation.diagnostics };
70
+ case "map":
71
+ return { node: { ...data, type: "map" }, diagnostics: validation.diagnostics };
72
+ case "timeline":
73
+ return { node: { ...data, type: "timeline" }, diagnostics: validation.diagnostics };
74
+ case "tabs": {
75
+ const tabs = children.filter((child) => child.type === "component" && child.name === "__tab");
76
+ 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
+ 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" });
79
+ const labels = /* @__PURE__ */ new Set();
80
+ 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" });
83
+ labels.add(tab.label);
84
+ }
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 };
87
+ }
88
+ case "callout":
89
+ return { node: { type: "callout", calloutType: data.type ?? "note", title: data.title, body: data.body, children }, diagnostics: validation.diagnostics };
90
+ case "embed":
91
+ return { node: { ...data, type: "embed" }, diagnostics: validation.diagnostics };
92
+ 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 };
94
+ case "query":
95
+ return { node: { ...data, type: "query" }, diagnostics: validation.diagnostics };
96
+ case "component":
97
+ return { node: { ...data, type: "component" }, diagnostics: validation.diagnostics };
98
+ default:
99
+ return { node: { type: "error", message: `Unsupported directive ::${name}`, raw, line }, diagnostics: validation.diagnostics };
100
+ }
101
+ }
102
+ function collectNodeDataRefs(node) {
103
+ switch (node.type) {
104
+ case "chart":
105
+ return [node.data];
106
+ case "metric":
107
+ return node.data ? [node.data] : [];
108
+ case "table":
109
+ return [node.data];
110
+ case "map":
111
+ return [node.data];
112
+ case "timeline":
113
+ return node.data ? [node.data] : [];
114
+ case "query":
115
+ return [node.data];
116
+ case "tabs":
117
+ return node.tabs.flatMap((tab) => tab.children.flatMap(collectNodeDataRefs));
118
+ case "callout":
119
+ return node.children?.flatMap(collectNodeDataRefs) ?? [];
120
+ default:
121
+ return [];
122
+ }
123
+ }
124
+ function getReferencedColumns(node) {
125
+ if (node.type === "chart") return { [node.data]: [node.x, ...Array.isArray(node.y) ? node.y : [node.y], node.label, node.value, node.series].filter(Boolean) };
126
+ if (node.type === "metric" && node.data) return { [node.data]: [node.field].filter(Boolean) };
127
+ if (node.type === "table") return { [node.data]: node.columns ?? [] };
128
+ if (node.type === "map") return { [node.data]: [node.lat, node.lon, node.label, node.value].filter(Boolean) };
129
+ if (node.type === "timeline" && node.data) return { [node.data]: [node.date, node.title, node.description, node.group].filter(Boolean) };
130
+ if (node.type === "query") return { [node.data]: [...node.select ?? [], ...Object.keys(node.where ?? {}), node.sort?.by].filter(Boolean) };
131
+ return {};
132
+ }
133
+
134
+ // ../parser/src/index.ts
135
+ import YAML from "yaml";
136
+ function parseAgentMarkdown({ source, sourcePath }) {
137
+ const diagnostics = [];
138
+ 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 });
140
+ const { body: withoutData, dataSources } = extractDataBlocks(body, sourcePath, diagnostics);
141
+ const nodes = parseDocumentBlocks(withoutData.split(/\r?\n/), sourcePath, diagnostics, 0, 1);
142
+ return { format: "agent-md", version: String(frontmatter?.version ?? "0.1"), sourcePath, frontmatter, nodes, dataSources, diagnostics };
143
+ }
144
+ function extractFrontmatter(source, sourcePath, diagnostics) {
145
+ if (!source.startsWith("---\n")) return { frontmatter: void 0, body: source };
146
+ const end = source.indexOf("\n---", 4);
147
+ if (end === -1) return { frontmatter: void 0, body: source };
148
+ const raw = source.slice(4, end);
149
+ try {
150
+ const parsed = YAML.parse(raw) ?? {};
151
+ return { frontmatter: typeof parsed === "object" ? parsed : {}, body: source.slice(end + 5).replace(/^\r?\n/, "") };
152
+ } catch (error) {
153
+ diagnostics.push({ severity: "error", code: "frontmatter_parse_error", message: error instanceof Error ? error.message : "Invalid frontmatter", sourcePath, line: 1 });
154
+ return { frontmatter: void 0, body: source.slice(end + 5).replace(/^\r?\n/, "") };
155
+ }
156
+ }
157
+ function extractDataBlocks(source, sourcePath, diagnostics) {
158
+ const dataSources = {};
159
+ const lines = source.split(/\r?\n/);
160
+ const kept = [];
161
+ for (let index = 0; index < lines.length; index++) {
162
+ const line = lines[index];
163
+ const match = line.match(/^```(?:(data)\s+([A-Za-z0-9_-]+)|(?:(json|yaml|yml|csv|tsv)\s+data=([A-Za-z0-9_-]+)))\s*$/);
164
+ if (!match) {
165
+ kept.push(line);
166
+ continue;
167
+ }
168
+ const format = match[3] === "yml" ? "yaml" : match[3] || "csv";
169
+ const id = match[2] || match[4];
170
+ const startLine = index + 1;
171
+ const content = [];
172
+ index++;
173
+ while (index < lines.length && !lines[index].startsWith("```")) content.push(lines[index++]);
174
+ try {
175
+ dataSources[id] = parseInlineData(id, format, content.join("\n"));
176
+ } 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" });
178
+ }
179
+ }
180
+ return { body: kept.join("\n"), dataSources };
181
+ }
182
+ function parseInlineData(id, format, raw) {
183
+ if (format === "json") {
184
+ const object = JSON.parse(raw);
185
+ return { id, origin: "inline", format, rows: Array.isArray(object) && object.every((row) => row && typeof row === "object" && !Array.isArray(row)) ? object : void 0, object, diagnostics: [] };
186
+ }
187
+ if (format === "yaml") {
188
+ const object = YAML.parse(raw);
189
+ return { id, origin: "inline", format, rows: Array.isArray(object) && object.every((row) => row && typeof row === "object" && !Array.isArray(row)) ? object : void 0, object, diagnostics: [] };
190
+ }
191
+ const delimiter = format === "tsv" ? " " : ",";
192
+ const [headerLine, ...rowLines] = raw.trim().split(/\r?\n/).filter(Boolean);
193
+ const headers = headerLine.split(delimiter).map((value) => value.trim());
194
+ const rows = rowLines.map((row) => Object.fromEntries(row.split(delimiter).map((value, index) => [headers[index], coerceScalar(value.trim())])));
195
+ return { id, origin: "inline", format, rows, columns: inferInlineColumns(rows), diagnostics: [] };
196
+ }
197
+ function inferInlineColumns(rows) {
198
+ const names = [...new Set(rows.flatMap((row) => Object.keys(row)))];
199
+ return names.map((name) => {
200
+ const values = rows.map((row) => row[name]);
201
+ const nonNull = values.filter((value) => value !== null && value !== void 0 && value !== "");
202
+ return { name, type: inferInlineType(nonNull), nullable: nonNull.length !== values.length, sampleValues: nonNull.slice(0, 5) };
203
+ });
204
+ }
205
+ function inferInlineType(values) {
206
+ if (values.length === 0) return "unknown";
207
+ if (values.every((value) => typeof value === "number")) return "number";
208
+ if (values.every((value) => typeof value === "boolean")) return "boolean";
209
+ if (values.every((value) => typeof value === "string" && !Number.isNaN(Date.parse(value)))) return "date";
210
+ if (values.every((value) => typeof value === "string")) return "string";
211
+ return "unknown";
212
+ }
213
+ function coerceScalar(value) {
214
+ if (value === "") return null;
215
+ if (value === "true") return true;
216
+ if (value === "false") return false;
217
+ const numeric = Number(value);
218
+ return Number.isFinite(numeric) && value.trim() !== "" ? numeric : value;
219
+ }
220
+ function parseDocumentBlocks(lines, sourcePath, diagnostics, depth, baseLine) {
221
+ const nodes = [];
222
+ let markdown = [];
223
+ let markdownStart = baseLine;
224
+ const flushMarkdown = (nextLine) => {
225
+ const value = markdown.join("\n").trim();
226
+ if (value) nodes.push({ type: "markdown", value, line: markdownStart });
227
+ markdown = [];
228
+ markdownStart = nextLine;
229
+ };
230
+ for (let index = 0; index < lines.length; index++) {
231
+ const start = lines[index].match(/^(::{1,})([A-Za-z][A-Za-z0-9_-]*)\s*$/);
232
+ if (!start) {
233
+ if (markdown.length === 0) markdownStart = baseLine + index;
234
+ markdown.push(lines[index]);
235
+ continue;
236
+ }
237
+ flushMarkdown(baseLine + index);
238
+ const marker = start[1];
239
+ const name = start[2];
240
+ const block = { name, marker, startLine: baseLine + index, raw: [lines[index]], content: [] };
241
+ index++;
242
+ let nested = 0;
243
+ for (; index < lines.length; index++) {
244
+ const current = lines[index];
245
+ if (current.match(/^(::{1,})[A-Za-z][A-Za-z0-9_-]*\s*$/)) nested++;
246
+ if (isClosingFence(current, marker) && nested === 0) {
247
+ block.raw.push(current);
248
+ break;
249
+ }
250
+ if (current.trim().match(/^::{1,}$/) && nested > 0) nested--;
251
+ block.raw.push(current);
252
+ block.content.push(current);
253
+ }
254
+ if (depth >= 5) {
255
+ diagnostics.push({ severity: "error", code: "max_nesting_depth", message: "Directive nesting depth exceeds 5.", sourcePath, line: block.startLine, blockType: name });
256
+ nodes.push({ type: "error", message: "Directive nesting depth exceeds 5.", raw: block.raw.join("\n"), line: block.startLine });
257
+ continue;
258
+ }
259
+ nodes.push(parseDirective(block, sourcePath, diagnostics, depth));
260
+ }
261
+ flushMarkdown(baseLine + lines.length);
262
+ return nodes;
263
+ }
264
+ function isClosingFence(line, marker) {
265
+ const trimmed = line.trim();
266
+ return /^::{1,}$/.test(trimmed) && trimmed.length >= marker.length;
267
+ }
268
+ function parseDirective(block, sourcePath, diagnostics, depth) {
269
+ if (block.name === "tab") {
270
+ const parsed2 = splitPropsAndBody(block.content, sourcePath, block.startLine, diagnostics);
271
+ const children2 = parseDocumentBlocks(parsed2.body.split(/\r?\n/), sourcePath, diagnostics, depth + 1, block.startLine + parsed2.bodyOffset);
272
+ return { type: "component", name: "__tab", props: { ...parsed2.attrs, children: children2 } };
273
+ }
274
+ const parsed = splitPropsAndBody(block.content, sourcePath, block.startLine, diagnostics);
275
+ const childLines = parsed.body.trim() ? parsed.body.split(/\r?\n/) : [];
276
+ const children = childLines.length ? parseDocumentBlocks(childLines, sourcePath, diagnostics, depth + 1, block.startLine + parsed.bodyOffset) : [];
277
+ if (block.name === "callout" && parsed.body.trim() && parsed.attrs.body == null && children.every((node) => node.type === "markdown")) parsed.attrs.body = parsed.body.trim();
278
+ const normalized = normalizePrimitive(block.name, parsed.attrs, children, block.raw.join("\n"), sourcePath, block.startLine);
279
+ diagnostics.push(...normalized.diagnostics);
280
+ return normalized.node;
281
+ }
282
+ function splitPropsAndBody(lines, sourcePath, startLine, diagnostics) {
283
+ let splitAt = 0;
284
+ const propLines = [];
285
+ for (; splitAt < lines.length; splitAt++) {
286
+ const line = lines[splitAt];
287
+ if (line.trim() === "") {
288
+ propLines.push(line);
289
+ continue;
290
+ }
291
+ if (/^\s+[\w-]+:/.test(line) || /^\s*-\s+/.test(line) || /^[A-Za-z_][\w-]*\s*:/.test(line) || /^\s+/.test(line)) {
292
+ propLines.push(line);
293
+ continue;
294
+ }
295
+ break;
296
+ }
297
+ let attrs = {};
298
+ const yamlText = propLines.join("\n").trim();
299
+ if (yamlText) {
300
+ try {
301
+ const parsed = YAML.parse(yamlText);
302
+ attrs = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
303
+ } catch (error) {
304
+ diagnostics.push({ severity: "error", code: "directive_yaml_error", message: error instanceof Error ? error.message : "Invalid directive YAML", sourcePath, line: startLine });
305
+ }
306
+ }
307
+ return { attrs, body: lines.slice(splitAt).join("\n"), bodyOffset: splitAt + 1 };
308
+ }
309
+
310
+ // ../skill/src/index.ts
311
+ var skillMarkdown = `# Agent Markdown Skill
312
+
313
+ Use Agent Markdown when the user asks for interactive reports, dashboards, visual analysis, charts or tables in Markdown, or local visualization documents.
314
+
315
+ Do not use Agent Markdown for ordinary README files, simple notes, documents that must render on GitHub without a viewer, or cases where the user asks for plain Markdown.
316
+
317
+ Rules:
318
+ - Use normal Markdown for prose.
319
+ - Use directives for interactive primitives.
320
+ - Do not emit raw HTML.
321
+ - Do not emit JavaScript.
322
+ - Prefer named data blocks or local files.
323
+ - Add frontmatter with format: agent-md and version: 0.1.
324
+ - Run agent-md validate before considering a document complete.
325
+
326
+ Supported MVP primitives:
327
+ - ::metric for KPI cards.
328
+ - ::chart for line, bar, area, scatter, and pie charts.
329
+ - ::table for sortable/filterable local data tables.
330
+ - ::callout for notes, warnings, decisions, risks, and tips.
331
+ - ::tabs for grouped alternative views.
332
+ - ::diagram, ::timeline, ::query, ::embed, ::form, ::map, and ::component are supported with conservative validation and graceful fallbacks.
333
+ `;
334
+ var exampleAgentMarkdown = `---
335
+ format: agent-md
336
+ version: 0.1
337
+ ---
338
+ # Q4 Revenue Dashboard
339
+
340
+ ::callout
341
+ type: decision
342
+ title: Local-only MVP
343
+ This dashboard uses only local inline data and local files.
344
+ :::
345
+
346
+ ::metric
347
+ label: Total revenue
348
+ data: revenue
349
+ field: amount
350
+ aggregate: sum
351
+ format: currency
352
+ :::
353
+
354
+ ::chart
355
+ type: line
356
+ title: Revenue by month
357
+ data: revenue
358
+ x: month
359
+ y: amount
360
+ :::
361
+
362
+ ::tabs
363
+ ::::tab
364
+ label: Table
365
+ ::table
366
+ data: revenue
367
+ columns: [month, segment, amount]
368
+ sortable: true
369
+ filterable: true
370
+ :::
371
+ ::::
372
+ ::::tab
373
+ label: Enterprise only
374
+ ::query
375
+ data: revenue
376
+ where:
377
+ segment: Enterprise
378
+ select: [month, amount]
379
+ sort:
380
+ by: amount
381
+ direction: desc
382
+ view: table
383
+ :::
384
+ ::::
385
+ :::
386
+
387
+ \`\`\`data revenue
388
+ month,segment,amount
389
+ Oct,SMB,100000
390
+ Oct,Enterprise,400000
391
+ Nov,SMB,120000
392
+ Nov,Enterprise,450000
393
+ Dec,SMB,140000
394
+ Dec,Enterprise,550000
395
+ \`\`\`
396
+ `;
397
+ function configJson() {
398
+ return JSON.stringify(defaultConfig, null, 2) + "\n";
399
+ }
400
+ function componentsJson() {
401
+ return JSON.stringify({ components: { Funnel: { module: "./components/Funnel.js", schema: "./components/Funnel.schema.json", description: "Renders a funnel conversion chart" } } }, null, 2) + "\n";
402
+ }
403
+ function schemaJson() {
404
+ return JSON.stringify({ $schema: "https://json-schema.org/draft/2020-12/schema", title: "Agent Markdown", version: "0.1", primitives: ["chart", "metric", "table", "diagram", "map", "timeline", "tabs", "callout", "embed", "form", "query", "component"] }, null, 2) + "\n";
405
+ }
406
+
407
+ // ../resolver/src/index.ts
408
+ import fs from "fs/promises";
409
+ import path from "path";
410
+ import YAML2 from "yaml";
411
+ import Papa from "papaparse";
412
+ var dataExtensions = /* @__PURE__ */ new Set([".csv", ".tsv", ".json", ".yaml", ".yml", ".geojson"]);
413
+ var artifactExtensions = /* @__PURE__ */ new Set([".pdf", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webm", ".mp4", ".html", ".txt", ".md", ".mmd", ".mermaid", ".csv", ".json"]);
414
+ function isRemoteRef(ref) {
415
+ return /^https?:\/\//i.test(ref);
416
+ }
417
+ function resolveSafePath(projectRoot, sourcePath, ref) {
418
+ if (isRemoteRef(ref)) throw new Error("Remote references are disabled in the MVP");
419
+ if (ref.startsWith("~")) throw new Error("Home-directory references are not allowed");
420
+ const root = path.resolve(projectRoot);
421
+ const absolute = path.resolve(path.dirname(sourcePath), ref);
422
+ const relative = path.relative(root, absolute);
423
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("Path escapes project root");
424
+ return absolute;
425
+ }
426
+ async function resolveSafeRealPath(projectRoot, sourcePath, ref) {
427
+ const absolute = resolveSafePath(projectRoot, sourcePath, ref);
428
+ const [realRoot, realAbsolute] = await Promise.all([fs.realpath(projectRoot), fs.realpath(absolute)]);
429
+ const relative = path.relative(realRoot, realAbsolute);
430
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("Path escapes project root");
431
+ return realAbsolute;
432
+ }
433
+ async function loadConfig(projectRoot, configPath = "agent-md.config.json") {
434
+ const absolute = path.resolve(projectRoot, configPath);
435
+ try {
436
+ const raw = await fs.readFile(absolute, "utf8");
437
+ const parsed = JSON.parse(raw);
438
+ return {
439
+ ...defaultConfig,
440
+ ...parsed,
441
+ server: { ...defaultConfig.server, ...parsed.server },
442
+ security: { ...defaultConfig.security, ...parsed.security },
443
+ components: { ...defaultConfig.components, ...parsed.components },
444
+ limits: { ...defaultConfig.limits, ...parsed.limits }
445
+ };
446
+ } catch (error) {
447
+ if (error.code === "ENOENT") return defaultConfig;
448
+ throw error;
449
+ }
450
+ }
451
+ async function loadDataFile(projectRoot, sourcePath, ref, limits = defaultConfig.limits) {
452
+ const absolute = await resolveSafeRealPath(projectRoot, sourcePath, ref);
453
+ const ext = path.extname(absolute).toLowerCase();
454
+ if (!dataExtensions.has(ext)) throw new Error(`Unsupported data file extension: ${ext}`);
455
+ const stat = await fs.stat(absolute);
456
+ if (stat.size > limits.maxDataSizeMb * 1024 * 1024) throw new Error(`Data file exceeds ${limits.maxDataSizeMb} MB limit`);
457
+ const raw = await fs.readFile(absolute, "utf8");
458
+ const format = ext === ".yml" ? "yaml" : ext.slice(1);
459
+ let rows;
460
+ let object;
461
+ if (format === "csv" || format === "tsv") {
462
+ const parsed = Papa.parse(raw, { header: true, dynamicTyping: true, skipEmptyLines: true, delimiter: format === "tsv" ? " " : void 0 });
463
+ rows = parsed.data;
464
+ object = parsed.data;
465
+ } else if (format === "json" || format === "geojson") {
466
+ object = JSON.parse(raw);
467
+ rows = Array.isArray(object) && object.every(isRecord) ? object : void 0;
468
+ } else {
469
+ object = YAML2.parse(raw);
470
+ rows = Array.isArray(object) && object.every(isRecord) ? object : void 0;
471
+ }
472
+ return { id: ref, origin: "file", source: absolute, format, rows, object, columns: rows ? inferColumns(rows) : void 0, diagnostics: [] };
473
+ }
474
+ function isRecord(value) {
475
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
476
+ }
477
+ function inferColumns(rows) {
478
+ const names = [...new Set(rows.flatMap((row) => Object.keys(row)))];
479
+ return names.map((name) => {
480
+ const values = rows.map((row) => row[name]);
481
+ const nonNull = values.filter((value) => value !== null && value !== void 0 && value !== "");
482
+ return { name, type: inferType(nonNull), nullable: nonNull.length !== values.length, sampleValues: nonNull.slice(0, 5) };
483
+ });
484
+ }
485
+ function inferType(values) {
486
+ if (values.length === 0) return "unknown";
487
+ if (values.every((value) => typeof value === "number")) return "number";
488
+ if (values.every((value) => typeof value === "boolean")) return "boolean";
489
+ if (values.every((value) => typeof value === "string" && !Number.isNaN(Date.parse(value)))) return "date";
490
+ if (values.every((value) => typeof value === "string")) return "string";
491
+ return "unknown";
492
+ }
493
+ async function resolveDocumentData(document, projectRoot, config = defaultConfig) {
494
+ const diagnostics = [...document.diagnostics];
495
+ const dataSources = { ...document.dataSources };
496
+ for (const source of Object.values(dataSources)) if (source.rows && !source.columns) source.columns = inferColumns(source.rows);
497
+ const refs = [...new Set(document.nodes.flatMap(collectNodeDataRefs))];
498
+ for (const ref of refs) {
499
+ if (dataSources[ref]) continue;
500
+ if (isRemoteRef(ref)) {
501
+ diagnostics.push({ severity: "error", code: "remote_data_blocked", message: `Remote data reference is blocked: ${ref}`, sourcePath: document.sourcePath });
502
+ continue;
503
+ }
504
+ if (dataExtensions.has(path.extname(ref).toLowerCase())) {
505
+ try {
506
+ const loaded = await loadDataFile(projectRoot, document.sourcePath, ref, config.limits);
507
+ dataSources[ref] = loaded;
508
+ } catch (error) {
509
+ diagnostics.push({ severity: "error", code: "data_file_error", message: error instanceof Error ? error.message : `Unable to load data source ${ref}`, sourcePath: document.sourcePath });
510
+ }
511
+ }
512
+ }
513
+ diagnostics.push(...validateReferences(document.nodes, dataSources, document.sourcePath, config));
514
+ diagnostics.push(...await validateLocalArtifacts(document.nodes, projectRoot, document.sourcePath, config));
515
+ return { ...document, dataSources, diagnostics };
516
+ }
517
+ function validateReferences(nodes, dataSources, sourcePath, config = defaultConfig) {
518
+ const diagnostics = [];
519
+ const visit = (node) => {
520
+ const refs = collectNodeDataRefs(node);
521
+ for (const ref of refs) if (!dataSources[ref] && !dataExtensions.has(path.extname(ref).toLowerCase())) diagnostics.push({ severity: "error", code: "data_not_found", message: `Data source "${ref}" was not found.`, sourcePath, blockType: node.type });
522
+ const columnsByData = getReferencedColumns(node);
523
+ for (const [data, columns] of Object.entries(columnsByData)) {
524
+ const source = dataSources[data];
525
+ if (!source?.columns || columns.length === 0) continue;
526
+ const known = new Set(source.columns.map((column) => column.name));
527
+ for (const column of columns) if (!known.has(column)) diagnostics.push({ severity: "error", code: "column_not_found", message: `Column "${column}" was not found in data source "${data}".`, sourcePath, blockType: node.type });
528
+ }
529
+ if (node.type === "chart") {
530
+ const rows = dataSources[node.data]?.rows?.length ?? 0;
531
+ 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" });
532
+ }
533
+ if (node.type === "table") {
534
+ const rows = dataSources[node.data]?.rows?.length ?? 0;
535
+ 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" });
536
+ }
537
+ 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" });
538
+ diagnostics.push(...validatePrimitiveSemantics(node, dataSources, sourcePath));
539
+ if (node.type === "tabs") node.tabs.forEach((tab) => tab.children.forEach(visit));
540
+ if (node.type === "callout") node.children?.forEach(visit);
541
+ };
542
+ nodes.forEach(visit);
543
+ return diagnostics;
544
+ }
545
+ async function validateLocalArtifacts(nodes, projectRoot, sourcePath, config) {
546
+ const diagnostics = [];
547
+ const visit = async (node) => {
548
+ if (node.type === "embed") await validateArtifactRef(node.src, "embed", projectRoot, sourcePath, config, diagnostics);
549
+ if (node.type === "diagram" && node.src) await validateArtifactRef(node.src, "diagram", projectRoot, sourcePath, config, diagnostics);
550
+ if (node.type === "tabs") for (const tab of node.tabs) for (const child of tab.children) await visit(child);
551
+ if (node.type === "callout") for (const child of node.children ?? []) await visit(child);
552
+ };
553
+ for (const node of nodes) await visit(node);
554
+ return diagnostics;
555
+ }
556
+ async function validateArtifactRef(ref, blockType, projectRoot, sourcePath, config, diagnostics) {
557
+ if (isRemoteRef(ref)) {
558
+ diagnostics.push({ severity: "error", code: "remote_artifact_blocked", message: `Remote artifact reference is blocked: ${ref}`, sourcePath, blockType });
559
+ return;
560
+ }
561
+ try {
562
+ const absolute = await resolveSafeRealPath(projectRoot, sourcePath, ref);
563
+ const ext = path.extname(absolute).toLowerCase();
564
+ if (!artifactExtensions.has(ext) && blockType === "embed") diagnostics.push({ severity: "error", code: "unsupported_artifact", message: `Unsupported artifact extension: ${ext}`, sourcePath, blockType });
565
+ if (ext === ".html" && !config.security.allowHtmlEmbeds) diagnostics.push({ severity: "error", code: "html_embed_blocked", message: "HTML embeds are blocked by default.", sourcePath, blockType });
566
+ const stat = await fs.stat(absolute);
567
+ 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 });
568
+ } catch (error) {
569
+ diagnostics.push({ severity: "error", code: "artifact_file_error", message: error instanceof Error ? error.message : `Unable to access artifact ${ref}`, sourcePath, blockType });
570
+ }
571
+ }
572
+ function validatePrimitiveSemantics(node, dataSources, sourcePath) {
573
+ const diagnostics = [];
574
+ if (node.type === "chart") {
575
+ const source = dataSources[node.data];
576
+ const numericColumns = Array.isArray(node.y) ? node.y : node.y ? [node.y] : [];
577
+ if (node.chartType === "pie" && node.value) numericColumns.push(node.value);
578
+ for (const column of numericColumns) {
579
+ 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" });
580
+ }
581
+ }
582
+ if (node.type === "metric" && node.data && node.field && node.aggregate && node.aggregate !== "count") {
583
+ 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" });
584
+ }
585
+ if (node.type === "map") {
586
+ const rows = dataSources[node.data]?.rows ?? [];
587
+ 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" });
588
+ 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" });
589
+ for (const row of rows) {
590
+ const lat = node.lat ? Number(row[node.lat]) : void 0;
591
+ const lon = node.lon ? Number(row[node.lon]) : void 0;
592
+ 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" });
593
+ 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" });
594
+ }
595
+ }
596
+ if (node.type === "timeline") {
597
+ 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" });
598
+ }
599
+ if (node.type === "form") {
600
+ const names = /* @__PURE__ */ new Set();
601
+ for (const field of node.fields) {
602
+ if (names.has(field.name)) diagnostics.push({ severity: "error", code: "duplicate_field", message: `Duplicate form field "${field.name}".`, sourcePath, blockType: "form" });
603
+ names.add(field.name);
604
+ 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" });
605
+ 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" });
606
+ }
607
+ }
608
+ return diagnostics;
609
+ }
610
+ function columnType(source, column) {
611
+ return source?.columns?.find((item) => item.name === column)?.type;
612
+ }
613
+
614
+ // src/index.ts
615
+ var program = new Command();
616
+ var cliDir = path2.dirname(fileURLToPath(import.meta.url));
617
+ var extensionId = "AbhinavSwaminathan.agent-md-preview";
618
+ var viewerDistCandidates = [
619
+ path2.resolve(cliDir, "../viewer-dist"),
620
+ path2.resolve(cliDir, "../../viewer/dist")
621
+ ];
622
+ var vsixCandidates = [
623
+ path2.resolve(cliDir, "../agent-md-preview.vsix"),
624
+ path2.resolve(cliDir, "../../vscode-extension/dist/agent-md-preview.vsix")
625
+ ];
626
+ program.name("agent-md").description("Local-first Agent Markdown runtime").version("0.1.0");
627
+ program.command("init").option("--agent <agent>", "agent skill flavor", "generic").action(async (options) => {
628
+ const root = process.cwd();
629
+ await writeIfMissing(path2.join(root, "agent-md.config.json"), configJson());
630
+ await fs2.mkdir(path2.join(root, ".agent-md"), { recursive: true });
631
+ await fs2.mkdir(path2.join(root, "examples"), { recursive: true });
632
+ await writeIfMissing(path2.join(root, ".agent-md", "skill.md"), skillMarkdown);
633
+ await writeIfMissing(path2.join(root, ".agent-md", "schema.json"), schemaJson());
634
+ await writeIfMissing(path2.join(root, ".agent-md", "components.json"), componentsJson());
635
+ await writeIfMissing(path2.join(root, "examples", "example.agent.md"), exampleAgentMarkdown);
636
+ if (["cursor", "vscode"].includes(String(options.agent).toLowerCase())) await mergeVsCodeRecommendation(root);
637
+ console.log(pc.green("Agent Markdown project initialized."));
638
+ if (["cursor", "vscode"].includes(String(options.agent).toLowerCase())) {
639
+ console.log(pc.gray(`Recommended extension added: ${extensionId}`));
640
+ console.log(pc.gray("Open an .agent.md file and run: Agent Markdown: Open Preview"));
641
+ console.log(pc.gray("Browser fallback: npx agent-md serve"));
642
+ }
643
+ });
644
+ 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) => {
645
+ const root = path2.resolve(options.root);
646
+ const config = await loadConfig(root, options.config);
647
+ const files = options.file ? [path2.resolve(root, options.file)] : await scanMarkdownFiles(root, config, false);
648
+ const results = await Promise.all(files.map((file) => parseAndResolve(file, root, config)));
649
+ const diagnostics = results.flatMap((result) => result.diagnostics);
650
+ if (options.json) {
651
+ console.log(JSON.stringify({ files: results, diagnostics }, null, 2));
652
+ } else {
653
+ printDiagnostics(results);
654
+ }
655
+ const hasFailure = diagnostics.some((diagnostic) => diagnostic.severity === "error" || options.strict && diagnostic.severity === "warning");
656
+ process.exitCode = hasFailure ? 1 : 0;
657
+ });
658
+ program.command("serve").option("--port <port>", "port", String(defaultConfig.server.port)).option("--host <host>", "host", defaultConfig.server.host).option("--root <root>", "project root", ".").option("--open", "open browser").option("--no-open", "do not open browser").option("--all-md", "include all Markdown files").option("--config <config>", "config path", "agent-md.config.json").action(async (options) => {
659
+ const root = path2.resolve(options.root);
660
+ const config = await loadConfig(root, options.config);
661
+ const port = Number(options.port ?? config.server.port);
662
+ const host = String(options.host ?? config.server.host);
663
+ const server = http.createServer(async (req, res) => {
664
+ try {
665
+ await handleRequest(req, res, root, config, Boolean(options.allMd));
666
+ } catch (error) {
667
+ sendJson(res, 500, { error: error instanceof Error ? error.message : "Unknown error" });
668
+ }
669
+ });
670
+ const wss = new WebSocketServer({ server, path: "/ws" });
671
+ const watcher = chokidar.watch([...config.include ?? [], "agent-md.config.json", ".agent-md/components.json"], { cwd: root, ignored: config.exclude, ignoreInitial: true });
672
+ watcher.on("all", () => {
673
+ for (const client of wss.clients) client.send(JSON.stringify({ type: "reload" }));
674
+ });
675
+ server.listen(port, host, () => {
676
+ const url = `http://${host}:${port}`;
677
+ console.log(pc.green(`Agent Markdown running at ${url}`));
678
+ const shouldOpen = options.open === true || options.open !== false && config.server.open;
679
+ if (shouldOpen) openBrowser(url).catch(() => void 0);
680
+ });
681
+ });
682
+ program.command("export").argument("file").option("--format <format>", "html | json | markdown-fallback", "json").option("--root <root>", "project root", ".").action(async (file, options) => {
683
+ const root = path2.resolve(options.root);
684
+ const config = await loadConfig(root);
685
+ const document = await parseAndResolve(path2.resolve(root, file), root, config);
686
+ if (options.format === "json") console.log(JSON.stringify(document, null, 2));
687
+ else if (options.format === "markdown-fallback") console.log(renderFallback(document));
688
+ else console.log(renderStaticHtml(document));
689
+ });
690
+ 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) => {
691
+ const vsix = await findVsix();
692
+ if (!vsix) {
693
+ console.log(pc.yellow("No bundled Agent Markdown VSIX was found."));
694
+ console.log("Build it with: npm run package -w agent-md-preview");
695
+ console.log(`Then install it with: ${options.editor} --install-extension packages/vscode-extension/dist/agent-md-preview.vsix --force`);
696
+ return;
697
+ }
698
+ if (options.install) {
699
+ await installVsix(String(options.editor), vsix, Boolean(options.force));
700
+ return;
701
+ }
702
+ console.log(`Agent Markdown VSIX: ${vsix}`);
703
+ console.log(`Install with: ${options.editor} --install-extension "${vsix}" --force`);
704
+ });
705
+ program.parseAsync(process.argv);
706
+ async function writeIfMissing(file, content) {
707
+ try {
708
+ await fs2.writeFile(file, content, { flag: "wx" });
709
+ } catch (error) {
710
+ if (error.code !== "EEXIST") throw error;
711
+ }
712
+ }
713
+ async function mergeVsCodeRecommendation(root) {
714
+ const vscodeDir = path2.join(root, ".vscode");
715
+ const extensionsPath = path2.join(vscodeDir, "extensions.json");
716
+ await fs2.mkdir(vscodeDir, { recursive: true });
717
+ let data = {};
718
+ try {
719
+ data = JSON.parse(await fs2.readFile(extensionsPath, "utf8"));
720
+ } catch (error) {
721
+ if (error.code !== "ENOENT") throw error;
722
+ }
723
+ const recommendations = new Set(data.recommendations ?? []);
724
+ recommendations.add(extensionId);
725
+ await fs2.writeFile(extensionsPath, JSON.stringify({ ...data, recommendations: [...recommendations].sort() }, null, 2) + "\n");
726
+ }
727
+ async function findVsix() {
728
+ for (const candidate of vsixCandidates) {
729
+ try {
730
+ await fs2.access(candidate);
731
+ return candidate;
732
+ } catch {
733
+ }
734
+ }
735
+ return void 0;
736
+ }
737
+ async function installVsix(editor, vsix, force) {
738
+ const { execFile } = await import("child_process");
739
+ const args = ["--install-extension", vsix, ...force ? ["--force"] : []];
740
+ await new Promise((resolve, reject) => {
741
+ execFile(editor, args, (error, stdout, stderr) => {
742
+ if (stdout) process.stdout.write(stdout);
743
+ if (stderr) process.stderr.write(stderr);
744
+ if (error) reject(error);
745
+ else resolve();
746
+ });
747
+ });
748
+ console.log(pc.green(`Installed Agent Markdown extension with ${editor}.`));
749
+ }
750
+ async function scanMarkdownFiles(root, config, allMd) {
751
+ const include = allMd ? ["**/*.md"] : config.include;
752
+ const files = await fg(include, { cwd: root, ignore: config.exclude, absolute: true, onlyFiles: true });
753
+ const safeFiles = [];
754
+ for (const file of files) {
755
+ try {
756
+ safeFiles.push(await resolveSafeRealPath(root, path2.join(root, "agent-md.config.json"), file));
757
+ } catch {
758
+ }
759
+ }
760
+ if (!allMd) return safeFiles;
761
+ const matching = [];
762
+ for (const file of safeFiles) if ((await fs2.readFile(file, "utf8")).includes("format: agent-md")) matching.push(file);
763
+ return matching;
764
+ }
765
+ async function parseAndResolve(file, root, config) {
766
+ const safeFile = await resolveSafeRealPath(root, path2.join(root, "agent-md.config.json"), file);
767
+ const stat = await fs2.stat(safeFile);
768
+ const diagnostics = [];
769
+ 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 });
770
+ const source = await fs2.readFile(safeFile, "utf8");
771
+ const parsed = parseAgentMarkdown({ source, sourcePath: safeFile });
772
+ const resolved = await resolveDocumentData(parsed, root, config);
773
+ return { ...resolved, diagnostics: [...diagnostics, ...resolved.diagnostics] };
774
+ }
775
+ function printDiagnostics(results) {
776
+ for (const result of results) {
777
+ console.log(pc.bold(path2.relative(process.cwd(), result.sourcePath)));
778
+ if (result.diagnostics.length === 0) {
779
+ console.log(pc.green(" OK"));
780
+ continue;
781
+ }
782
+ for (const diagnostic of result.diagnostics) {
783
+ const color = diagnostic.severity === "error" ? pc.red : diagnostic.severity === "warning" ? pc.yellow : pc.blue;
784
+ console.log(color(` ${diagnostic.severity.toUpperCase()}: ${diagnostic.message}`));
785
+ if (diagnostic.line) console.log(` Line: ${diagnostic.line}`);
786
+ if (diagnostic.blockType) console.log(` Block: ::${diagnostic.blockType}`);
787
+ if (diagnostic.suggestion) console.log(` Suggestion: ${diagnostic.suggestion}`);
788
+ }
789
+ }
790
+ }
791
+ async function resolveRequestedDocument(root, config, allMd, file) {
792
+ const requested = await resolveSafeRealPath(root, path2.join(root, "agent-md.config.json"), file);
793
+ const allowed = await scanMarkdownFiles(root, config, allMd);
794
+ const allowedRealPaths = new Set(await Promise.all(allowed.map((item) => fs2.realpath(item))));
795
+ if (!allowedRealPaths.has(requested)) throw new Error("Requested file is not an Agent Markdown document");
796
+ return requested;
797
+ }
798
+ function collectArtifactRefs(nodes) {
799
+ const refs = /* @__PURE__ */ new Set();
800
+ const visit = (node) => {
801
+ if ((node.type === "embed" || node.type === "diagram") && node.src) refs.add(node.src);
802
+ if (node.type === "tabs") node.tabs.forEach((tab) => tab.children.forEach(visit));
803
+ if (node.type === "callout") node.children?.forEach(visit);
804
+ };
805
+ nodes.forEach(visit);
806
+ return refs;
807
+ }
808
+ async function handleRequest(req, res, root, config, allMd) {
809
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
810
+ if (url.pathname === "/api/files") {
811
+ const files = await scanMarkdownFiles(root, config, allMd);
812
+ const items = await Promise.all(files.map(async (file) => {
813
+ const document = await parseAndResolve(file, root, config).catch((error) => ({ diagnostics: [{ severity: "error", code: "parse_error", message: error instanceof Error ? error.message : "Unable to parse", sourcePath: file }] }));
814
+ return { path: path2.relative(root, file), diagnostics: document.diagnostics };
815
+ }));
816
+ return sendJson(res, 200, { files: items });
817
+ }
818
+ if (url.pathname === "/api/document") {
819
+ const file = url.searchParams.get("file");
820
+ if (!file) return sendJson(res, 400, { error: "file is required" });
821
+ const absolute = await resolveRequestedDocument(root, config, allMd, file);
822
+ return sendJson(res, 200, await parseAndResolve(absolute, root, config));
823
+ }
824
+ if (url.pathname === "/api/source") {
825
+ const file = url.searchParams.get("file");
826
+ if (!file) return sendJson(res, 400, { error: "file is required" });
827
+ const absolute = await resolveRequestedDocument(root, config, allMd, file);
828
+ return sendJson(res, 200, { source: await fs2.readFile(absolute, "utf8") });
829
+ }
830
+ if (url.pathname === "/artifact") {
831
+ const file = url.searchParams.get("file");
832
+ const src = url.searchParams.get("src");
833
+ if (!file || !src) return sendJson(res, 400, { error: "file and src are required" });
834
+ const sourceFile = await resolveRequestedDocument(root, config, allMd, file);
835
+ const document = await parseAndResolve(sourceFile, root, config);
836
+ if (!collectArtifactRefs(document.nodes).has(src)) return sendJson(res, 403, { error: "artifact is not referenced by document" });
837
+ const artifact = await resolveSafeRealPath(root, sourceFile, src);
838
+ const ext = path2.extname(artifact).toLowerCase();
839
+ if (!artifactExtensions.has(ext) && !dataExtensions.has(ext)) return sendJson(res, 400, { error: "unsupported artifact" });
840
+ if (ext === ".html" && !config.security.allowHtmlEmbeds) return sendJson(res, 403, { error: "HTML embeds are blocked" });
841
+ return fs2.readFile(artifact).then((buffer) => {
842
+ res.statusCode = 200;
843
+ res.setHeader("content-type", contentType(artifact));
844
+ res.end(buffer);
845
+ });
846
+ }
847
+ return serveViewerAsset(url.pathname, res);
848
+ }
849
+ function sendJson(res, status, body) {
850
+ res.statusCode = status;
851
+ res.setHeader("content-type", "application/json");
852
+ res.end(JSON.stringify(body));
853
+ }
854
+ function sendHtml(res, body) {
855
+ res.statusCode = 200;
856
+ res.setHeader("content-type", "text/html; charset=utf8");
857
+ res.end(body);
858
+ }
859
+ async function serveViewerAsset(pathname, res) {
860
+ const viewerDistDir = await findViewerDistDir();
861
+ if (!viewerDistDir) {
862
+ if (pathname === "/") return sendHtml(res, viewerHtml());
863
+ return sendJson(res, 404, { error: "Viewer assets have not been built. Run npm run build." });
864
+ }
865
+ const relative = pathname === "/" ? "index.html" : decodeURIComponent(pathname.replace(/^\/+/, ""));
866
+ const absolute = path2.resolve(viewerDistDir, relative);
867
+ const insideViewer = !path2.relative(viewerDistDir, absolute).startsWith("..") && !path2.isAbsolute(path2.relative(viewerDistDir, absolute));
868
+ if (!insideViewer) return sendJson(res, 403, { error: "Forbidden" });
869
+ try {
870
+ const body = await fs2.readFile(absolute);
871
+ res.statusCode = 200;
872
+ res.setHeader("content-type", contentType(absolute));
873
+ res.end(body);
874
+ } catch (error) {
875
+ if (pathname === "/" && error.code === "ENOENT") return sendHtml(res, viewerHtml());
876
+ sendJson(res, 404, { error: "Not found" });
877
+ }
878
+ }
879
+ async function findViewerDistDir() {
880
+ for (const candidate of viewerDistCandidates) {
881
+ try {
882
+ await fs2.access(path2.join(candidate, "index.html"));
883
+ return candidate;
884
+ } catch {
885
+ }
886
+ }
887
+ return void 0;
888
+ }
889
+ function contentType(file) {
890
+ const ext = path2.extname(file);
891
+ if (ext === ".html") return "text/html; charset=utf8";
892
+ if (ext === ".js") return "text/javascript; charset=utf8";
893
+ if (ext === ".css") return "text/css; charset=utf8";
894
+ if (ext === ".svg") return "image/svg+xml";
895
+ if (ext === ".json") return "application/json";
896
+ return "application/octet-stream";
897
+ }
898
+ function renderFallback(document) {
899
+ return document.nodes.map((node) => node.type === "markdown" ? node.value : `[${node.type}]`).join("\n\n");
900
+ }
901
+ function renderStaticHtml(document) {
902
+ return `<!doctype html><meta charset="utf8"><title>Agent Markdown</title><pre>${escapeHtml(JSON.stringify(document, null, 2))}</pre>`;
903
+ }
904
+ function escapeHtml(value) {
905
+ return value.replace(/[&<>"]/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[char]);
906
+ }
907
+ async function openBrowser(url) {
908
+ await import("child_process").then(({ execFile }) => execFile(process.platform === "darwin" ? "open" : "xdg-open", [url]));
909
+ }
910
+ function viewerHtml() {
911
+ return `<!doctype html>
912
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Agent Markdown</title>
913
+ <style>
914
+ :root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#111;background:#f8fafc}body{margin:0}.app{display:grid;grid-template-columns:300px 1fr;min-height:100vh}.top{grid-column:1/3;padding:12px 18px;background:#111827;color:white;font-weight:700}.side{border-right:1px solid #e5e7eb;padding:16px;background:white}.main{padding:24px;max-width:1100px}.file{display:block;width:100%;text-align:left;border:1px solid #e5e7eb;background:#fff;padding:8px;margin:6px 0;border-radius:8px;cursor:pointer}.file.active{border-color:#2563eb;background:#eff6ff}.card{border:1px solid #e5e7eb;border-radius:12px;background:#fff;padding:16px;margin:12px 0;box-shadow:0 1px 2px #0001}.error{border-color:#ef4444;background:#fef2f2}.warning{border-color:#f59e0b;background:#fffbeb}.grid{display:grid;gap:12px}.tabs button{margin-right:8px}.diag{font-size:13px;margin:8px 0;padding:8px;border-radius:8px;background:#f3f4f6}pre{white-space:pre-wrap;background:#111827;color:#e5e7eb;padding:16px;border-radius:12px;overflow:auto}table{border-collapse:collapse;width:100%;background:white}th,td{border:1px solid #e5e7eb;padding:8px;text-align:left}th{background:#f9fafb}input,select{padding:8px;border:1px solid #d1d5db;border-radius:8px}</style>
915
+ </head><body><div id="app" class="app"><div class="top">Agent Markdown</div><aside class="side"><h3>Project files</h3><div id="files">Loading...</div><h3>Diagnostics</h3><div id="diagnostics"></div></aside><main class="main"><button id="toggle">Source</button><div id="document"></div></main></div>
916
+ <script>
917
+ let selected=null, doc=null, showSource=false;
918
+ const ws = new WebSocket('ws://' + location.host + '/ws'); ws.onmessage = () => loadFiles().then(()=> selected && loadDoc(selected));
919
+ document.getElementById('toggle').onclick=()=>{showSource=!showSource; renderDoc();};
920
+ async function loadFiles(){const data=await fetch('/api/files').then(r=>r.json()); const el=document.getElementById('files'); el.innerHTML=''; data.files.forEach(f=>{const b=document.createElement('button'); b.className='file'+(f.path===selected?' active':''); b.textContent=f.path+' '+(f.diagnostics.some(d=>d.severity==='error')?'\u2715':'\u2713'); b.onclick=()=>loadDoc(f.path); el.appendChild(b);}); if(!selected && data.files[0]) await loadDoc(data.files[0].path);}
921
+ async function loadDoc(file){selected=file; doc=await fetch('/api/document?file='+encodeURIComponent(file)).then(r=>r.json()); showSource=false; await renderDoc(); loadFiles();}
922
+ function diagnostics(){const el=document.getElementById('diagnostics'); el.innerHTML=''; (doc?.diagnostics||[]).forEach(d=>{const x=document.createElement('div'); x.className='diag '+d.severity; x.textContent=d.severity.toUpperCase()+': '+d.message+(d.line?' line '+d.line:''); el.appendChild(x);});}
923
+ async function renderDoc(){diagnostics(); const el=document.getElementById('document'); if(!doc){el.textContent='Select a file';return;} if(showSource){const src=await fetch('/api/source?file='+encodeURIComponent(selected)).then(r=>r.json()); el.innerHTML='<pre></pre>'; el.querySelector('pre').textContent=src.source; return;} el.innerHTML=''; doc.nodes.forEach(n=>el.appendChild(renderNode(n)));}
924
+ function renderNode(n){const d=document.createElement('div'); if(n.type==='markdown'){d.innerHTML=md(n.value); return d;} d.className='card'; if(n.type==='error'){d.className='card error'; d.textContent=n.message; return d;} if(n.type==='metric'){d.innerHTML='<strong>'+esc(n.label)+'</strong><h2>'+esc(metricValue(n))+'</h2>'+(n.delta?'<p>'+esc(n.delta)+'</p>':''); return d;} if(n.type==='chart'){d.innerHTML='<strong>'+esc(n.title||'Chart')+'</strong><pre>'+esc('[Chart: '+n.chartType+' chart of '+(n.y||n.value||'value')+' by '+(n.x||n.label||'label')+']')+'</pre>'; return d;} if(n.type==='table'||n.type==='query'){const rows=rowsFor(n); d.innerHTML='<strong>'+esc(n.title||n.type)+'</strong>'+table(rows,n.columns||n.select); return d;} if(n.type==='callout'){d.className='card '+n.calloutType; d.innerHTML='<strong>'+esc(n.title||n.calloutType)+'</strong><div>'+md(n.body||'')+'</div>'; return d;} if(n.type==='tabs'){d.className='card tabs'; let active=0; const nav=document.createElement('div'), body=document.createElement('div'); const show=i=>{active=i; body.innerHTML=''; n.tabs[i].children.forEach(c=>body.appendChild(renderNode(c)));}; n.tabs.forEach((t,i)=>{const b=document.createElement('button'); b.textContent=t.label; b.onclick=()=>show(i); nav.appendChild(b);}); d.append(nav,body); show(0); return d;} if(n.type==='timeline'){d.innerHTML='<strong>Timeline</strong>'+((n.events||[]).map(e=>'<p><b>'+esc(e.date)+'</b> '+esc(e.title)+'</p>').join('')); return d;} if(n.type==='form'){d.innerHTML='<strong>'+esc(n.title||'Form')+'</strong>'+n.fields.map(f=>'<label><p>'+esc(f.label||f.name)+'</p><input type="'+(f.fieldType==='number'?'number':f.fieldType==='date'?'date':'text')+'" value="'+esc(f.default||'')+'"></label>').join(''); return d;} if(n.type==='embed'){d.innerHTML='<strong>'+esc(n.title||n.src)+'</strong><p><a href="/artifact?file='+encodeURIComponent(selected)+'&src='+encodeURIComponent(n.src)+'">Open artifact</a></p>'; return d;} d.innerHTML='<pre>'+esc('['+n.type+']')+'</pre>'; return d;}
925
+ function rowsFor(n){const ds=doc.dataSources[n.data]; if(!ds?.rows)return[]; let rows=[...ds.rows]; if(n.where) rows=rows.filter(r=>Object.entries(n.where).every(([k,v])=>typeof v==='object'?true:r[k]===v)); if(n.sort) rows.sort((a,b)=>a[n.sort.by]>b[n.sort.by]?1:-1); if(n.limit) rows=rows.slice(0,n.limit); return rows;}
926
+ function metricValue(n){if(n.value!=null)return n.value; const rows=doc.dataSources[n.data]?.rows||[]; const vals=rows.map(r=>Number(r[n.field])).filter(Number.isFinite); if(n.aggregate==='count')return rows.length; if(n.aggregate==='avg')return vals.reduce((a,b)=>a+b,0)/Math.max(vals.length,1); if(n.aggregate==='min')return Math.min(...vals); if(n.aggregate==='max')return Math.max(...vals); return vals.reduce((a,b)=>a+b,0);}
927
+ function table(rows, cols){cols=cols||Object.keys(rows[0]||{}); return '<table><thead><tr>'+cols.map(c=>'<th>'+esc(c)+'</th>').join('')+'</tr></thead><tbody>'+rows.slice(0,500).map(r=>'<tr>'+cols.map(c=>'<td>'+esc(r[c])+'</td>').join('')+'</tr>').join('')+'</tbody></table>';}
928
+ function md(s){return esc(s).replace(/^# (.*)$/gm,'<h1>$1</h1>').replace(/^## (.*)$/gm,'<h2>$1</h2>').replace(/\\n/g,'<br>');}
929
+ function esc(s){return String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
930
+ loadFiles();
931
+ </script></body></html>`;
932
+ }