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.
- package/AGENTS.md +2 -2
- package/README.md +17 -8
- package/docs/artifact-schema-v1.md +27 -5
- package/docs/integrations.md +21 -4
- package/docs/sarif-csv-artifact-plan.md +44 -65
- package/package.json +1 -1
- package/src/cli.js +18 -4
- package/src/client/editor.js +76 -4
- package/src/lib/converters.js +429 -44
- package/src/lib/installer.js +58 -4
- package/src/lib/render.js +404 -2
- package/src/lib/storage.js +52 -3
- package/src/mcp-server.js +2 -2
- package/src/server.js +41 -2
package/src/lib/installer.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
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);
|
package/src/lib/storage.js
CHANGED
|
@@ -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." },
|