artifacty 0.1.2 → 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.
@@ -3,7 +3,8 @@ import { existsSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
 
6
- const SUPPORTED_AGENTS = new Set(["all", "claude", "codex", "gemini"]);
6
+ const INSTALL_TARGETS = ["claude", "codex", "gemini", "copilot", "cursor"];
7
+ const SUPPORTED_AGENTS = new Set(["all", ...INSTALL_TARGETS, "github-copilot", "vscode"]);
7
8
  const DEFAULT_MCP_TIMEOUT_MS = 30000;
8
9
 
9
10
  export async function installAgent(agent, options = {}) {
@@ -14,7 +15,7 @@ export async function installAgent(agent, options = {}) {
14
15
 
15
16
  if (normalized === "all") {
16
17
  const results = [];
17
- for (const target of ["claude", "codex", "gemini"]) {
18
+ for (const target of INSTALL_TARGETS) {
18
19
  results.push(await installAgent(target, options));
19
20
  }
20
21
  return {
@@ -29,7 +30,13 @@ export async function installAgent(agent, options = {}) {
29
30
  if (normalized === "codex") {
30
31
  return installCodex(options);
31
32
  }
32
- return installGemini(options);
33
+ if (normalized === "gemini") {
34
+ return installGemini(options);
35
+ }
36
+ if (normalized === "copilot") {
37
+ return installCopilot(options);
38
+ }
39
+ return installCursor(options);
33
40
  }
34
41
 
35
42
  export function createMcpServerConfig(options = {}) {
@@ -96,6 +103,49 @@ export async function installGemini(options = {}) {
96
103
  });
97
104
  }
98
105
 
106
+ export async function installCopilot(options = {}) {
107
+ const projectDir = path.resolve(options.projectDir || process.cwd());
108
+ const targetPath = path.resolve(options.configPath || path.join(projectDir, ".vscode", "mcp.json"));
109
+ const existing = await readJsonFile(targetPath, {});
110
+ const next = {
111
+ ...existing,
112
+ servers: {
113
+ ...(existing.servers || {}),
114
+ artifacty: {
115
+ type: "stdio",
116
+ ...createMcpServerConfig(options)
117
+ }
118
+ }
119
+ };
120
+
121
+ return writeInstallFile({
122
+ agent: "copilot",
123
+ path: targetPath,
124
+ dryRun: options.dryRun,
125
+ content: `${JSON.stringify(next, null, 2)}\n`
126
+ });
127
+ }
128
+
129
+ export async function installCursor(options = {}) {
130
+ const projectDir = path.resolve(options.projectDir || process.cwd());
131
+ const targetPath = path.resolve(options.configPath || path.join(projectDir, ".cursor", "mcp.json"));
132
+ const existing = await readJsonFile(targetPath, {});
133
+ const next = {
134
+ ...existing,
135
+ mcpServers: {
136
+ ...(existing.mcpServers || {}),
137
+ artifacty: createMcpServerConfig(options)
138
+ }
139
+ };
140
+
141
+ return writeInstallFile({
142
+ agent: "cursor",
143
+ path: targetPath,
144
+ dryRun: options.dryRun,
145
+ content: `${JSON.stringify(next, null, 2)}\n`
146
+ });
147
+ }
148
+
99
149
  export async function installCodex(options = {}) {
100
150
  const targetPath = path.resolve(options.configPath || path.join(homedir(), ".codex", "config.toml"));
101
151
  const existing = await readTextFile(targetPath, "");
@@ -179,7 +229,11 @@ async function readTextFile(filePath, fallback) {
179
229
  }
180
230
 
181
231
  function normalizeAgent(agent) {
182
- return String(agent || "").trim().toLowerCase();
232
+ const normalized = String(agent || "").trim().toLowerCase();
233
+ if (normalized === "github-copilot" || normalized === "vscode") {
234
+ return "copilot";
235
+ }
236
+ return normalized;
183
237
  }
184
238
 
185
239
  function normalizeTimeoutMs(value) {
package/src/lib/render.js CHANGED
@@ -163,6 +163,8 @@ export function renderImportArtifactPage({ baseUrl, authToken = "", locale = DEF
163
163
  <option value="claude">Claude</option>
164
164
  <option value="codex">Codex</option>
165
165
  <option value="gemini">Gemini</option>
166
+ <option value="copilot">GitHub Copilot</option>
167
+ <option value="cursor">Cursor</option>
166
168
  <option value="generic">Generic</option>
167
169
  </select>
168
170
  </label>
@@ -208,8 +210,10 @@ export function renderArtifactPage({ artifact, version, content, baseUrl, authTo
208
210
  const rendered = renderContent(version.format, content, version.metadata || {}, {
209
211
  reactFrameUrl: reactRendererEnabled()
210
212
  ? view.href(`/artifacts/${encodeURIComponent(artifact.id)}/react-frame?version=${version.version}`)
211
- : ""
213
+ : "",
214
+ rawUrl: view.href(`/artifacts/${encodeURIComponent(artifact.id)}/raw?version=${version.version}`)
212
215
  });
216
+ const viewClass = artifactViewClass({ artifact, version });
213
217
  const needsViewerScript = version.format === "code";
214
218
  const rawUrl = `/artifacts/${encodeURIComponent(artifact.id)}/raw?version=${version.version}`;
215
219
  const archiveAction = artifact.archivedAt ? "restore" : "archive";
@@ -231,7 +235,7 @@ export function renderArtifactPage({ artifact, version, content, baseUrl, authTo
231
235
  ${languageSwitcher(view)}
232
236
  </nav>
233
237
  </header>
234
- <main class="artifact-view">
238
+ <main class="${viewClass}">
235
239
  <section class="meta-strip">
236
240
  ${formatBadge(version.format)}
237
241
  ${typeBadge(artifact.artifactType || "document")}
@@ -256,6 +260,16 @@ export function renderArtifactPage({ artifact, version, content, baseUrl, authTo
256
260
  });
257
261
  }
258
262
 
263
+ function artifactViewClass({ artifact, version }) {
264
+ const wideFormats = new Set(["html", "svg", "mermaid", "react", "sarif", "csv", "image", "video"]);
265
+ const wideTypes = new Set(["dashboard", "design-option", "diff-walkthrough"]);
266
+ const classes = ["artifact-view"];
267
+ if (wideFormats.has(version.format) || wideTypes.has(artifact.artifactType)) {
268
+ classes.push("artifact-view-wide");
269
+ }
270
+ return classes.join(" ");
271
+ }
272
+
259
273
  export function renderDiffPage({ artifact, fromVersion, toVersion, fromContent, toContent, diffRows, baseUrl, locale = DEFAULT_LOCALE, currentPath = "/" }) {
260
274
  const view = viewContext(locale, currentPath);
261
275
  const title = view.raw("diff.title", { title: artifact.title });
@@ -342,6 +356,22 @@ export function renderContent(format, content, metadata = {}, options = {}) {
342
356
  return `<pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>`;
343
357
  }
344
358
 
359
+ if (format === "sarif") {
360
+ return renderSarif(content);
361
+ }
362
+
363
+ if (format === "csv") {
364
+ return renderCsv(content);
365
+ }
366
+
367
+ if (format === "image") {
368
+ return renderMediaArtifact("image", content, metadata, options);
369
+ }
370
+
371
+ if (format === "video") {
372
+ return renderMediaArtifact("video", content, metadata, options);
373
+ }
374
+
345
375
  if (format === "code") {
346
376
  const language = metadata.language || metadata.artifactyImport?.language || "";
347
377
  return `<section class="artifact-code-viewer" data-artifacty-code-viewer data-language="${escapeAttribute(language)}">
@@ -598,6 +628,9 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
598
628
  width: min(1180px, calc(100vw - 32px));
599
629
  margin: 28px auto 72px;
600
630
  }
631
+ .artifact-view-wide {
632
+ width: min(1760px, calc(100vw - 24px));
633
+ }
601
634
  .toolbar {
602
635
  font-family: var(--mono);
603
636
  font-size: 12px;
@@ -905,6 +938,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
905
938
  .badge.t-diagram { --bh: #0ea5e9; }
906
939
  .badge.t-component { --bh: #7c3aed; }
907
940
  .badge.t-snippet { --bh: #64748b; }
941
+ .badge.t-analysis-report { --bh: #dc2626; }
942
+ .badge.t-table { --bh: #0891b2; }
908
943
  .badge.t-unknown { --bh: #94a3b8; }
909
944
  .badge.f-html { --bh: #e0795b; }
910
945
  .badge.f-markdown { --bh: #3b82f6; }
@@ -914,6 +949,10 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
914
949
  .badge.f-svg { --bh: #0ea5e9; }
915
950
  .badge.f-mermaid { --bh: #14b8a6; }
916
951
  .badge.f-react { --bh: #7c3aed; }
952
+ .badge.f-sarif { --bh: #dc2626; }
953
+ .badge.f-csv { --bh: #0891b2; }
954
+ .badge.f-image { --bh: #f59e0b; }
955
+ .badge.f-video { --bh: #db2777; }
917
956
  .badge.s-archived { --bh: #94a3b8; }
918
957
  .empty {
919
958
  padding: 28px;
@@ -1076,6 +1115,105 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
1076
1115
  }
1077
1116
  .artifact-table .align-center { text-align: center; }
1078
1117
  .artifact-table .align-right { text-align: right; }
1118
+ .artifact-csv,
1119
+ .artifact-sarif {
1120
+ display: grid;
1121
+ gap: 14px;
1122
+ }
1123
+ .artifact-csv-note,
1124
+ .artifact-sarif-note {
1125
+ margin: 0;
1126
+ padding: 10px 12px;
1127
+ border: 1px solid var(--line);
1128
+ border-radius: 8px;
1129
+ background: var(--panel);
1130
+ color: var(--muted);
1131
+ font-size: 13px;
1132
+ }
1133
+ .artifact-summary-grid {
1134
+ display: grid;
1135
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
1136
+ gap: 10px;
1137
+ }
1138
+ .summary-card {
1139
+ display: grid;
1140
+ gap: 3px;
1141
+ min-height: 72px;
1142
+ padding: 12px;
1143
+ border: 1px solid var(--line);
1144
+ border-radius: 8px;
1145
+ background: var(--panel);
1146
+ }
1147
+ .summary-card span {
1148
+ color: var(--muted);
1149
+ font-family: var(--mono);
1150
+ font-size: 11.5px;
1151
+ text-transform: uppercase;
1152
+ letter-spacing: 0.06em;
1153
+ }
1154
+ .summary-card strong {
1155
+ font-size: 20px;
1156
+ line-height: 1.2;
1157
+ }
1158
+ .sarif-level {
1159
+ display: inline-flex;
1160
+ min-width: 68px;
1161
+ justify-content: center;
1162
+ padding: 2px 8px;
1163
+ border-radius: 999px;
1164
+ background: color-mix(in srgb, var(--bh, var(--muted)) 14%, transparent);
1165
+ color: color-mix(in srgb, var(--bh, var(--muted)) 70%, var(--text));
1166
+ font-family: var(--mono);
1167
+ font-size: 12px;
1168
+ }
1169
+ .sarif-level.error { --bh: #dc2626; }
1170
+ .sarif-level.warning { --bh: #d97706; }
1171
+ .sarif-level.note { --bh: #2563eb; }
1172
+ .sarif-level.none { --bh: #64748b; }
1173
+ .artifact-raw-details {
1174
+ border: 1px solid var(--line);
1175
+ border-radius: 12px;
1176
+ background: var(--panel);
1177
+ overflow: hidden;
1178
+ }
1179
+ .artifact-raw-details summary {
1180
+ padding: 10px 14px;
1181
+ cursor: pointer;
1182
+ color: var(--muted);
1183
+ font-family: var(--mono);
1184
+ font-size: 12.5px;
1185
+ }
1186
+ .artifact-raw-details .artifact-code {
1187
+ border: 0;
1188
+ border-top: 1px solid var(--line);
1189
+ border-radius: 0;
1190
+ }
1191
+ .artifact-media {
1192
+ display: grid;
1193
+ gap: 12px;
1194
+ margin: 0;
1195
+ padding: 14px;
1196
+ border: 1px solid var(--line);
1197
+ border-radius: 12px;
1198
+ background: var(--panel);
1199
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
1200
+ }
1201
+ .artifact-media img,
1202
+ .artifact-media video {
1203
+ display: block;
1204
+ width: 100%;
1205
+ max-height: 76vh;
1206
+ object-fit: contain;
1207
+ border-radius: 8px;
1208
+ background: var(--panel-2);
1209
+ }
1210
+ .artifact-media figcaption,
1211
+ .artifact-media-note {
1212
+ color: var(--muted);
1213
+ font-family: var(--mono);
1214
+ font-size: 12.5px;
1215
+ overflow-wrap: anywhere;
1216
+ }
1079
1217
  .artifact-code {
1080
1218
  padding: 22px;
1081
1219
  overflow: auto;
@@ -1512,6 +1650,270 @@ function alignmentAttribute(alignment) {
1512
1650
  return alignment ? ` class="align-${alignment}"` : "";
1513
1651
  }
1514
1652
 
1653
+ const CSV_RENDER_ROW_LIMIT = 1000;
1654
+ const CSV_RENDER_COLUMN_LIMIT = 80;
1655
+ const SARIF_RENDER_RESULT_LIMIT = 500;
1656
+
1657
+ function renderMediaArtifact(format, content, metadata = {}, options = {}) {
1658
+ const mimeType = mediaMimeType(metadata, format);
1659
+ const source = options.rawUrl || mediaDataUrl(content, mimeType);
1660
+ if (!source) {
1661
+ return `<section class="artifact-media-note">
1662
+ Media preview is unavailable because this artifact does not contain valid base64 media content.
1663
+ </section>
1664
+ <pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>`;
1665
+ }
1666
+
1667
+ const label = [
1668
+ mimeType || format,
1669
+ metadata.encoding ? `encoding: ${metadata.encoding}` : "",
1670
+ metadata.originalEncoding ? `source: ${metadata.originalEncoding}` : ""
1671
+ ].filter(Boolean).join(" · ");
1672
+
1673
+ if (format === "video") {
1674
+ return `<figure class="artifact-media artifact-video">
1675
+ <video controls preload="metadata" src="${escapeAttribute(source)}"></video>
1676
+ <figcaption>${escapeHtml(label || "video artifact")}</figcaption>
1677
+ </figure>`;
1678
+ }
1679
+
1680
+ return `<figure class="artifact-media artifact-image">
1681
+ <img src="${escapeAttribute(source)}" alt="Image artifact">
1682
+ <figcaption>${escapeHtml(label || "image artifact")}</figcaption>
1683
+ </figure>`;
1684
+ }
1685
+
1686
+ function mediaMimeType(metadata = {}, format) {
1687
+ const explicit = String(metadata.mimeType || "").toLowerCase();
1688
+ if (explicit.startsWith("image/") || explicit.startsWith("video/")) {
1689
+ return explicit;
1690
+ }
1691
+ return format === "video" ? "video/mp4" : "image/png";
1692
+ }
1693
+
1694
+ function mediaDataUrl(content, mimeType) {
1695
+ const value = String(content || "").trim();
1696
+ if (/^data:[^;,]+;base64,/i.test(value)) {
1697
+ return value;
1698
+ }
1699
+ const base64 = value.replace(/\s+/g, "");
1700
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(base64)) {
1701
+ return "";
1702
+ }
1703
+ return `data:${mimeType};base64,${base64}`;
1704
+ }
1705
+
1706
+ function renderCsv(content) {
1707
+ const parsed = parseCsv(content);
1708
+ if (!parsed.ok) {
1709
+ return `<section class="artifact-csv">
1710
+ <p class="artifact-csv-note">CSV parsing failed: ${escapeHtml(parsed.error)}. Showing escaped source.</p>
1711
+ <pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
1712
+ </section>`;
1713
+ }
1714
+
1715
+ if (parsed.rows.length === 0) {
1716
+ return `<section class="artifact-csv"><p class="artifact-csv-note">Empty CSV artifact.</p></section>`;
1717
+ }
1718
+
1719
+ const columnCount = Math.max(...parsed.rows.map((row) => row.length));
1720
+ const visibleColumns = Math.min(columnCount, CSV_RENDER_COLUMN_LIMIT);
1721
+ const header = parsed.rows[0];
1722
+ const bodyRows = parsed.rows.slice(1, CSV_RENDER_ROW_LIMIT + 1);
1723
+ const rowTruncated = parsed.rows.length - 1 > CSV_RENDER_ROW_LIMIT;
1724
+ const columnTruncated = columnCount > CSV_RENDER_COLUMN_LIMIT;
1725
+ const headerHtml = Array.from({ length: visibleColumns }, (_, index) =>
1726
+ `<th>${escapeHtml(header[index] || `Column ${index + 1}`)}</th>`
1727
+ ).join("");
1728
+ const bodyHtml = bodyRows.map((row) =>
1729
+ `<tr>${Array.from({ length: visibleColumns }, (_, index) =>
1730
+ `<td>${escapeHtml(row[index] || "")}</td>`
1731
+ ).join("")}</tr>`
1732
+ ).join("\n");
1733
+ const notes = [
1734
+ `${parsed.rows.length - 1} data rows`,
1735
+ `${columnCount} columns`,
1736
+ rowTruncated ? `showing first ${CSV_RENDER_ROW_LIMIT} rows` : "",
1737
+ columnTruncated ? `showing first ${CSV_RENDER_COLUMN_LIMIT} columns` : ""
1738
+ ].filter(Boolean).join(" · ");
1739
+
1740
+ return `<section class="artifact-csv">
1741
+ <p class="artifact-csv-note">${escapeHtml(notes)}</p>
1742
+ <div class="artifact-table-scroll">
1743
+ <table class="artifact-table">
1744
+ <thead><tr>${headerHtml}</tr></thead>
1745
+ <tbody>${bodyHtml}</tbody>
1746
+ </table>
1747
+ </div>
1748
+ </section>`;
1749
+ }
1750
+
1751
+ function parseCsv(content) {
1752
+ const text = String(content || "");
1753
+ const rows = [];
1754
+ let row = [];
1755
+ let cell = "";
1756
+ let inQuotes = false;
1757
+
1758
+ for (let index = 0; index < text.length; index += 1) {
1759
+ const char = text[index];
1760
+ if (char === "\"") {
1761
+ if (inQuotes && text[index + 1] === "\"") {
1762
+ cell += "\"";
1763
+ index += 1;
1764
+ } else {
1765
+ inQuotes = !inQuotes;
1766
+ }
1767
+ } else if (char === "," && !inQuotes) {
1768
+ row.push(cell);
1769
+ cell = "";
1770
+ } else if ((char === "\n" || char === "\r") && !inQuotes) {
1771
+ if (char === "\r" && text[index + 1] === "\n") {
1772
+ index += 1;
1773
+ }
1774
+ row.push(cell);
1775
+ if (row.length > 1 || row[0] !== "") {
1776
+ rows.push(row);
1777
+ }
1778
+ row = [];
1779
+ cell = "";
1780
+ } else {
1781
+ cell += char;
1782
+ }
1783
+ }
1784
+
1785
+ if (inQuotes) {
1786
+ return { ok: false, error: "unterminated quoted field", rows: [] };
1787
+ }
1788
+ row.push(cell);
1789
+ if (row.length > 1 || row[0] !== "") {
1790
+ rows.push(row);
1791
+ }
1792
+ return { ok: true, rows };
1793
+ }
1794
+
1795
+ function renderSarif(content) {
1796
+ let sarif;
1797
+ try {
1798
+ sarif = JSON.parse(content);
1799
+ } catch {
1800
+ return `<section class="artifact-sarif">
1801
+ <p class="artifact-sarif-note">Invalid SARIF JSON. Showing escaped source.</p>
1802
+ <pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
1803
+ </section>`;
1804
+ }
1805
+
1806
+ if (!isSarifDocument(sarif)) {
1807
+ return `<section class="artifact-sarif">
1808
+ <p class="artifact-sarif-note">This JSON does not match the expected SARIF top-level shape. Showing formatted JSON.</p>
1809
+ <pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>
1810
+ </section>`;
1811
+ }
1812
+
1813
+ const results = collectSarifResults(sarif);
1814
+ const counts = countBy(results, (result) => result.level);
1815
+ const visibleResults = results.slice(0, SARIF_RENDER_RESULT_LIMIT);
1816
+ const rowHtml = visibleResults.map((result) => `<tr>
1817
+ <td><span class="sarif-level ${escapeAttribute(result.level)}">${escapeHtml(result.level)}</span></td>
1818
+ <td>${escapeHtml(result.ruleId)}</td>
1819
+ <td>${escapeHtml(result.message)}</td>
1820
+ <td>${escapeHtml(result.location)}</td>
1821
+ <td>${escapeHtml(result.tool)}</td>
1822
+ </tr>`).join("\n");
1823
+ const note = results.length > SARIF_RENDER_RESULT_LIMIT
1824
+ ? `Showing first ${SARIF_RENDER_RESULT_LIMIT} of ${results.length} results.`
1825
+ : `${results.length} results.`;
1826
+
1827
+ return `<section class="artifact-sarif">
1828
+ <div class="artifact-summary-grid">
1829
+ ${summaryCard("Runs", sarif.runs.length)}
1830
+ ${summaryCard("Results", results.length)}
1831
+ ${summaryCard("Errors", counts.error || 0)}
1832
+ ${summaryCard("Warnings", counts.warning || 0)}
1833
+ ${summaryCard("Notes", counts.note || 0)}
1834
+ </div>
1835
+ <p class="artifact-sarif-note">${escapeHtml(note)}</p>
1836
+ <div class="artifact-table-scroll">
1837
+ <table class="artifact-table">
1838
+ <thead><tr><th>Level</th><th>Rule</th><th>Message</th><th>Location</th><th>Tool</th></tr></thead>
1839
+ <tbody>${rowHtml}</tbody>
1840
+ </table>
1841
+ </div>
1842
+ <details class="artifact-raw-details">
1843
+ <summary>Raw SARIF JSON</summary>
1844
+ <pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>
1845
+ </details>
1846
+ </section>`;
1847
+ }
1848
+
1849
+ function isSarifDocument(value) {
1850
+ return value &&
1851
+ typeof value === "object" &&
1852
+ !Array.isArray(value) &&
1853
+ Array.isArray(value.runs) &&
1854
+ (typeof value.version === "string" || String(value.$schema || "").toLowerCase().includes("sarif"));
1855
+ }
1856
+
1857
+ function collectSarifResults(sarif) {
1858
+ return sarif.runs.flatMap((run) => {
1859
+ const tool = run?.tool?.driver?.name || run?.tool?.driver?.fullName || "unknown";
1860
+ const rules = new Map();
1861
+ for (const [index, rule] of (run?.tool?.driver?.rules || []).entries()) {
1862
+ if (rule?.id) {
1863
+ rules.set(rule.id, rule);
1864
+ }
1865
+ rules.set(index, rule);
1866
+ }
1867
+ return (run?.results || []).map((result) => {
1868
+ const rule = rules.get(result.ruleId) || rules.get(result.ruleIndex);
1869
+ return {
1870
+ level: normalizeSarifLevel(result.level || rule?.defaultConfiguration?.level),
1871
+ ruleId: result.ruleId || rule?.id || (Number.isInteger(result.ruleIndex) ? `#${result.ruleIndex}` : "unknown"),
1872
+ message: sarifMessage(result.message) || sarifMessage(rule?.shortDescription) || sarifMessage(rule?.fullDescription) || "",
1873
+ location: sarifLocation(result),
1874
+ tool
1875
+ };
1876
+ });
1877
+ });
1878
+ }
1879
+
1880
+ function normalizeSarifLevel(value) {
1881
+ const normalized = String(value || "warning").toLowerCase();
1882
+ return ["error", "warning", "note", "none"].includes(normalized) ? normalized : "warning";
1883
+ }
1884
+
1885
+ function sarifMessage(message) {
1886
+ if (!message || typeof message !== "object") {
1887
+ return "";
1888
+ }
1889
+ return String(message.text || message.markdown || "").trim();
1890
+ }
1891
+
1892
+ function sarifLocation(result) {
1893
+ const physical = result?.locations?.[0]?.physicalLocation;
1894
+ const uri = physical?.artifactLocation?.uri || physical?.artifactLocation?.uriBaseId || "";
1895
+ const region = physical?.region || {};
1896
+ const line = Number.isInteger(region.startLine) ? region.startLine : "";
1897
+ const column = Number.isInteger(region.startColumn) ? region.startColumn : "";
1898
+ return [
1899
+ uri || "unknown",
1900
+ line ? `:${line}` : "",
1901
+ column ? `:${column}` : ""
1902
+ ].join("");
1903
+ }
1904
+
1905
+ function countBy(values, keyFn) {
1906
+ return values.reduce((counts, value) => {
1907
+ const key = keyFn(value);
1908
+ counts[key] = (counts[key] || 0) + 1;
1909
+ return counts;
1910
+ }, {});
1911
+ }
1912
+
1913
+ function summaryCard(label, value) {
1914
+ return `<div class="summary-card"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
1915
+ }
1916
+
1515
1917
  function formatJson(content) {
1516
1918
  try {
1517
1919
  return JSON.stringify(JSON.parse(content), null, 2);
@@ -17,7 +17,11 @@ export const ARTIFACT_FORMATS = [
17
17
  "code",
18
18
  "svg",
19
19
  "mermaid",
20
- "react"
20
+ "react",
21
+ "sarif",
22
+ "csv",
23
+ "image",
24
+ "video"
21
25
  ];
22
26
  export const ARTIFACT_TYPES = [
23
27
  "document",
@@ -33,6 +37,8 @@ export const ARTIFACT_TYPES = [
33
37
  "diagram",
34
38
  "component",
35
39
  "snippet",
40
+ "analysis-report",
41
+ "table",
36
42
  "unknown"
37
43
  ];
38
44
 
@@ -44,7 +50,11 @@ const FORMAT_TO_EXTENSION = {
44
50
  code: "code",
45
51
  svg: "svg",
46
52
  mermaid: "mmd",
47
- react: "jsx"
53
+ react: "jsx",
54
+ sarif: "sarif",
55
+ csv: "csv",
56
+ image: "image",
57
+ video: "video"
48
58
  };
49
59
 
50
60
  const FORMAT_TO_CONTENT_TYPE = {
@@ -55,7 +65,11 @@ const FORMAT_TO_CONTENT_TYPE = {
55
65
  code: "text/x-source-code; charset=utf-8",
56
66
  svg: "image/svg+xml; charset=utf-8",
57
67
  mermaid: "text/vnd.mermaid; charset=utf-8",
58
- react: "text/jsx; charset=utf-8"
68
+ react: "text/jsx; charset=utf-8",
69
+ sarif: "application/sarif+json; charset=utf-8",
70
+ csv: "text/csv; charset=utf-8",
71
+ image: "application/vnd.artifacty.image+base64; charset=utf-8",
72
+ video: "application/vnd.artifacty.video+base64; charset=utf-8"
59
73
  };
60
74
 
61
75
  export function createStore(options = {}) {
@@ -398,6 +412,9 @@ export function normalizeFormat(value = "text") {
398
412
  if (normalized === "jsx" || normalized === "tsx") {
399
413
  return "react";
400
414
  }
415
+ if (normalized === "sarif+json") {
416
+ return "sarif";
417
+ }
401
418
  if (ARTIFACT_FORMATS.includes(normalized)) {
402
419
  return normalized;
403
420
  }
@@ -805,9 +822,29 @@ function inferArtifactType(input) {
805
822
  if (format === "code") {
806
823
  return "snippet";
807
824
  }
825
+ if (format === "sarif") {
826
+ return "analysis-report";
827
+ }
828
+ if (format === "csv") {
829
+ return looksLikeAnalysisCsv(input.content) ||
830
+ /findings?|security|review|scan/i.test(normalizeOptionalString(input.title))
831
+ ? "analysis-report"
832
+ : "table";
833
+ }
834
+ if (format === "image" || format === "video") {
835
+ return "asset";
836
+ }
808
837
  return "document";
809
838
  }
810
839
 
840
+ function looksLikeAnalysisCsv(content) {
841
+ const [header = ""] = normalizeOptionalString(content).split(/\r?\n/, 1);
842
+ const normalized = header.toLowerCase();
843
+ return normalized.includes("severity") &&
844
+ (normalized.includes("message") || normalized.includes("description")) &&
845
+ (normalized.includes("file") || normalized.includes("path") || normalized.includes("rule"));
846
+ }
847
+
811
848
  function normalizeTags(tags) {
812
849
  if (!Array.isArray(tags)) {
813
850
  return [];
@@ -834,9 +871,21 @@ function inferFormat(contentType) {
834
871
  if (value.includes("vnd.ant.code") || value.includes("source-code")) {
835
872
  return "code";
836
873
  }
874
+ if (value.includes("sarif")) {
875
+ return "sarif";
876
+ }
877
+ if (value.includes("csv")) {
878
+ return "csv";
879
+ }
837
880
  if (value.includes("svg")) {
838
881
  return "svg";
839
882
  }
883
+ if (value.startsWith("image/")) {
884
+ return "image";
885
+ }
886
+ if (value.startsWith("video/")) {
887
+ return "video";
888
+ }
840
889
  if (value.includes("vnd.ant.mermaid") || value.includes("mermaid")) {
841
890
  return "mermaid";
842
891
  }
package/src/mcp-server.js CHANGED
@@ -97,13 +97,13 @@ const tools = [
97
97
  {
98
98
  name: "artifacty_import",
99
99
  title: "Import Agent Artifact",
100
- description: "Convert an artifact produced by Claude, Codex, Gemini, or another agent into Artifacty format and save it.",
100
+ description: "Convert an artifact produced by Claude, Codex, Gemini, GitHub Copilot, Cursor, or another agent into Artifacty format and save it.",
101
101
  inputSchema: {
102
102
  type: "object",
103
103
  properties: {
104
104
  agent: {
105
105
  type: "string",
106
- enum: ["auto", "claude", "codex", "gemini", "artifacty", "generic"],
106
+ enum: ["auto", "claude", "codex", "gemini", "copilot", "cursor", "artifacty", "generic"],
107
107
  description: "Original agent family. Use auto when unsure."
108
108
  },
109
109
  title: { type: "string", description: "Optional title override." },