artifacty 0.1.0 → 0.1.2
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/README.md +142 -33
- package/docs/artifact-schema-v1.md +26 -1
- package/docs/integrations.md +84 -8
- package/docs/network-sharing.md +41 -0
- package/docs/release-checklist.md +13 -0
- package/docs/sarif-csv-artifact-plan.md +65 -0
- package/package.json +15 -3
- package/scripts/smoke.sh +2 -1
- package/src/cli.js +77 -8
- package/src/client/editor.js +35 -2
- package/src/client/viewer.js +67 -0
- package/src/lib/background.js +344 -0
- package/src/lib/converters.js +448 -10
- package/src/lib/editor-assets.js +45 -2
- package/src/lib/installer.js +13 -4
- package/src/lib/render.js +348 -8
- package/src/lib/storage.js +60 -4
- package/src/lib/token.js +25 -0
- package/src/mcp-server.js +8 -6
- package/src/server.js +104 -25
package/src/lib/converters.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { contentTypeForFormat, normalizeFormat } from "./storage.js";
|
|
3
|
+
import { ARTIFACT_TYPES, contentTypeForFormat, normalizeFormat } from "./storage.js";
|
|
4
4
|
|
|
5
5
|
const KNOWN_AGENTS = new Set(["auto", "artifacty", "claude", "codex", "gemini", "generic"]);
|
|
6
6
|
|
|
@@ -68,6 +68,18 @@ export function convertAgentArtifact(input = {}) {
|
|
|
68
68
|
|
|
69
69
|
export function detectFormat({ content = "", contentType = "", fileName = "" } = {}) {
|
|
70
70
|
const type = optionalString(contentType).toLowerCase();
|
|
71
|
+
if (type.includes("vnd.ant.code") || type.includes("source-code")) {
|
|
72
|
+
return "code";
|
|
73
|
+
}
|
|
74
|
+
if (type.includes("svg")) {
|
|
75
|
+
return "svg";
|
|
76
|
+
}
|
|
77
|
+
if (type.includes("vnd.ant.mermaid") || type.includes("mermaid")) {
|
|
78
|
+
return "mermaid";
|
|
79
|
+
}
|
|
80
|
+
if (type.includes("vnd.ant.react") || type.includes("jsx")) {
|
|
81
|
+
return "react";
|
|
82
|
+
}
|
|
71
83
|
if (type.includes("html")) {
|
|
72
84
|
return "html";
|
|
73
85
|
}
|
|
@@ -91,11 +103,32 @@ export function detectFormat({ content = "", contentType = "", fileName = "" } =
|
|
|
91
103
|
if (extension === ".json") {
|
|
92
104
|
return "json";
|
|
93
105
|
}
|
|
106
|
+
if (extension === ".svg") {
|
|
107
|
+
return "svg";
|
|
108
|
+
}
|
|
109
|
+
if (extension === ".mmd" || extension === ".mermaid") {
|
|
110
|
+
return "mermaid";
|
|
111
|
+
}
|
|
112
|
+
if (extension === ".jsx" || extension === ".tsx") {
|
|
113
|
+
return "react";
|
|
114
|
+
}
|
|
115
|
+
if (isCodeExtension(extension)) {
|
|
116
|
+
return "code";
|
|
117
|
+
}
|
|
94
118
|
if (extension === ".txt" || extension === ".log") {
|
|
95
119
|
return "text";
|
|
96
120
|
}
|
|
97
121
|
|
|
98
122
|
const trimmed = optionalString(content);
|
|
123
|
+
if (/^(?:<\?xml[\s\S]*?\?>\s*)?<svg[\s>]/i.test(trimmed)) {
|
|
124
|
+
return "svg";
|
|
125
|
+
}
|
|
126
|
+
if (looksLikeMermaid(trimmed)) {
|
|
127
|
+
return "mermaid";
|
|
128
|
+
}
|
|
129
|
+
if (looksLikeReact(trimmed)) {
|
|
130
|
+
return "react";
|
|
131
|
+
}
|
|
99
132
|
if (/^<!doctype html/i.test(trimmed) || /^<html[\s>]/i.test(trimmed)) {
|
|
100
133
|
return "html";
|
|
101
134
|
}
|
|
@@ -131,22 +164,50 @@ function decodeObjectPayload(value, agent) {
|
|
|
131
164
|
return decodeBundlePayload(artifactObject, agent);
|
|
132
165
|
}
|
|
133
166
|
|
|
167
|
+
const codexContinuation = decodeCodexContinuationPayload(artifactObject, agent);
|
|
168
|
+
if (codexContinuation) {
|
|
169
|
+
return codexContinuation;
|
|
170
|
+
}
|
|
171
|
+
|
|
134
172
|
if (typeof artifactObject.content === "string" && !Array.isArray(artifactObject.content)) {
|
|
173
|
+
const sourceContentType = artifactObject.contentType ||
|
|
174
|
+
artifactObject.content_type ||
|
|
175
|
+
artifactObject.mimeType ||
|
|
176
|
+
artifactObject.mime_type ||
|
|
177
|
+
artifactObject.type;
|
|
135
178
|
return {
|
|
136
179
|
content: artifactObject.content,
|
|
137
|
-
format: safeFormat(artifactObject.format ||
|
|
138
|
-
contentType:
|
|
180
|
+
format: safeFormat(artifactObject.format || sourceContentType),
|
|
181
|
+
contentType: sourceContentType,
|
|
139
182
|
title: artifactObject.title || artifactObject.name,
|
|
140
183
|
sourceAgent: artifactObject.sourceAgent || artifactObject.source_agent || artifactObject.agent,
|
|
141
184
|
artifactType: artifactObject.artifactType || artifactObject.artifact_type,
|
|
142
185
|
tags: artifactObject.tags,
|
|
143
186
|
metadata: {
|
|
144
187
|
originalPayloadShape: "content",
|
|
145
|
-
originalId: artifactObject.id
|
|
188
|
+
originalId: artifactObject.id,
|
|
189
|
+
originalContentType: sourceContentType,
|
|
190
|
+
language: artifactObject.language
|
|
146
191
|
}
|
|
147
192
|
};
|
|
148
193
|
}
|
|
149
194
|
|
|
195
|
+
if (typeof artifactObject.code === "string") {
|
|
196
|
+
return objectContent(artifactObject, "code", artifactObject.code, "code");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (typeof artifactObject.svg === "string") {
|
|
200
|
+
return objectContent(artifactObject, "svg", artifactObject.svg, "svg");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (typeof artifactObject.mermaid === "string") {
|
|
204
|
+
return objectContent(artifactObject, "mermaid", artifactObject.mermaid, "mermaid");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (typeof artifactObject.react === "string" || typeof artifactObject.jsx === "string") {
|
|
208
|
+
return objectContent(artifactObject, "react", artifactObject.react || artifactObject.jsx, "react");
|
|
209
|
+
}
|
|
210
|
+
|
|
150
211
|
if (typeof artifactObject.html === "string") {
|
|
151
212
|
return objectContent(artifactObject, "html", artifactObject.html, "html");
|
|
152
213
|
}
|
|
@@ -160,9 +221,10 @@ function decodeObjectPayload(value, agent) {
|
|
|
160
221
|
}
|
|
161
222
|
|
|
162
223
|
if (typeof artifactObject.returnDisplay === "string") {
|
|
224
|
+
const content = stripMarkdownCodeFence(artifactObject.returnDisplay);
|
|
163
225
|
return {
|
|
164
|
-
content
|
|
165
|
-
format: detectFormat({ content
|
|
226
|
+
content,
|
|
227
|
+
format: detectFormat({ content }) === "json" ? "json" : "markdown",
|
|
166
228
|
title: artifactObject.title || artifactObject.name || "Gemini artifact",
|
|
167
229
|
sourceAgent: agent === "auto" ? "gemini" : agent,
|
|
168
230
|
artifactType: "document",
|
|
@@ -232,16 +294,20 @@ function decodeObjectPayload(value, agent) {
|
|
|
232
294
|
}
|
|
233
295
|
|
|
234
296
|
function objectContent(object, shape, content, format) {
|
|
297
|
+
const sourceContentType = object.contentType || object.content_type || object.mimeType || object.mime_type || object.type;
|
|
235
298
|
return {
|
|
236
299
|
content,
|
|
237
300
|
format,
|
|
301
|
+
contentType: sourceContentType,
|
|
238
302
|
title: object.title || object.name,
|
|
239
303
|
sourceAgent: object.sourceAgent || object.source_agent || object.agent,
|
|
240
304
|
artifactType: object.artifactType || object.artifact_type,
|
|
241
305
|
tags: object.tags,
|
|
242
306
|
metadata: {
|
|
243
307
|
originalPayloadShape: shape,
|
|
244
|
-
originalId: object.id
|
|
308
|
+
originalId: object.id,
|
|
309
|
+
originalContentType: sourceContentType,
|
|
310
|
+
language: object.language
|
|
245
311
|
}
|
|
246
312
|
};
|
|
247
313
|
}
|
|
@@ -249,6 +315,7 @@ function objectContent(object, shape, content, format) {
|
|
|
249
315
|
function decodeBundlePayload(object, agent) {
|
|
250
316
|
const bundle = object.bundle && typeof object.bundle === "object" ? object.bundle : object;
|
|
251
317
|
const files = Array.isArray(bundle.files) ? bundle.files : [];
|
|
318
|
+
const codexMetadata = collectCodexContinuationMetadata({ agent }, bundle, object);
|
|
252
319
|
const normalizedFiles = files.map((file, index) => {
|
|
253
320
|
const content = typeof file.content === "string" ? file.content : "";
|
|
254
321
|
return {
|
|
@@ -268,7 +335,8 @@ function decodeBundlePayload(object, agent) {
|
|
|
268
335
|
schemaVersion: 1,
|
|
269
336
|
artifactType: "bundle",
|
|
270
337
|
title: object.title || bundle.title || "Artifact bundle",
|
|
271
|
-
files: normalizedFiles
|
|
338
|
+
files: normalizedFiles,
|
|
339
|
+
...bundleDocumentMetadata(codexMetadata)
|
|
272
340
|
}, null, 2),
|
|
273
341
|
format: "json",
|
|
274
342
|
contentType: "application/vnd.artifacty.bundle+json; charset=utf-8",
|
|
@@ -279,11 +347,320 @@ function decodeBundlePayload(object, agent) {
|
|
|
279
347
|
metadata: {
|
|
280
348
|
originalPayloadShape: "artifact-bundle",
|
|
281
349
|
fileCount: normalizedFiles.length,
|
|
282
|
-
bundlePolicy: "text-files-stored-inline-in-bundle-json"
|
|
350
|
+
bundlePolicy: "text-files-stored-inline-in-bundle-json",
|
|
351
|
+
...codexMetadata
|
|
283
352
|
}
|
|
284
353
|
};
|
|
285
354
|
}
|
|
286
355
|
|
|
356
|
+
function decodeCodexContinuationPayload(object, agent) {
|
|
357
|
+
if (!isCodexPayload(object, agent) || hasDirectContentField(object)) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const artifactType = inferCodexContinuationType(object);
|
|
362
|
+
if (!artifactType) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const metadata = collectCodexContinuationMetadata({ agent }, object);
|
|
367
|
+
return {
|
|
368
|
+
content: renderCodexContinuationMarkdown(object, artifactType, metadata),
|
|
369
|
+
format: "markdown",
|
|
370
|
+
contentType: "text/markdown; charset=utf-8",
|
|
371
|
+
title: object.title || object.name || titleForCodexType(artifactType),
|
|
372
|
+
sourceAgent: "codex",
|
|
373
|
+
artifactType,
|
|
374
|
+
tags: uniqueStrings([
|
|
375
|
+
artifactType,
|
|
376
|
+
"codex",
|
|
377
|
+
...normalizeTags(object.tags)
|
|
378
|
+
]),
|
|
379
|
+
metadata: {
|
|
380
|
+
originalPayloadShape: "codex-continuation",
|
|
381
|
+
continuationKind: artifactType,
|
|
382
|
+
...metadata
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function hasDirectContentField(object) {
|
|
388
|
+
return typeof object.content === "string" ||
|
|
389
|
+
Array.isArray(object.content) ||
|
|
390
|
+
typeof object.markdown === "string" ||
|
|
391
|
+
typeof object.text === "string" ||
|
|
392
|
+
typeof object.html === "string" ||
|
|
393
|
+
typeof object.code === "string" ||
|
|
394
|
+
typeof object.svg === "string" ||
|
|
395
|
+
typeof object.mermaid === "string" ||
|
|
396
|
+
typeof object.react === "string" ||
|
|
397
|
+
typeof object.jsx === "string";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function isCodexPayload(object, agent) {
|
|
401
|
+
const explicit = normalizeAgent(
|
|
402
|
+
object.sourceAgent ||
|
|
403
|
+
object.source_agent ||
|
|
404
|
+
object.agent ||
|
|
405
|
+
object.producer ||
|
|
406
|
+
object.createdBy ||
|
|
407
|
+
object.created_by ||
|
|
408
|
+
agent
|
|
409
|
+
);
|
|
410
|
+
return explicit === "codex";
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function inferCodexContinuationType(object) {
|
|
414
|
+
const explicit = optionalString(object.artifactType || object.artifact_type || object.kind || object.category || object.purpose || object.type).toLowerCase();
|
|
415
|
+
if (explicit.includes("review")) {
|
|
416
|
+
return "code-review";
|
|
417
|
+
}
|
|
418
|
+
if (explicit.includes("test") || explicit.includes("verification") || explicit.includes("report")) {
|
|
419
|
+
return "test-report";
|
|
420
|
+
}
|
|
421
|
+
if (explicit.includes("diff") || explicit.includes("patch")) {
|
|
422
|
+
return "diff-walkthrough";
|
|
423
|
+
}
|
|
424
|
+
if (explicit.includes("handoff") || explicit.includes("continuation")) {
|
|
425
|
+
return "handoff";
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (hasNonEmptyArray(object.findings) || hasNonEmptyArray(object.review?.findings)) {
|
|
429
|
+
return "code-review";
|
|
430
|
+
}
|
|
431
|
+
if (object.verification || hasNonEmptyArray(object.tests) || hasNonEmptyArray(object.testResults) || hasNonEmptyArray(object.test_results) || optionalString(object.testStatus)) {
|
|
432
|
+
return "test-report";
|
|
433
|
+
}
|
|
434
|
+
if (optionalString(object.diff || object.patch)) {
|
|
435
|
+
return "diff-walkthrough";
|
|
436
|
+
}
|
|
437
|
+
if (
|
|
438
|
+
optionalString(object.goal || object.currentState || object.current_state || object.summary) ||
|
|
439
|
+
hasNonEmptyArray(object.decisions) ||
|
|
440
|
+
hasNonEmptyArray(object.blockers) ||
|
|
441
|
+
hasNonEmptyArray(object.nextSteps) ||
|
|
442
|
+
hasNonEmptyArray(object.next_steps) ||
|
|
443
|
+
hasNonEmptyArray(object.changedFiles) ||
|
|
444
|
+
hasNonEmptyArray(object.changed_files)
|
|
445
|
+
) {
|
|
446
|
+
return "handoff";
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function collectCodexContinuationMetadata(...sources) {
|
|
453
|
+
const objects = sources.filter((item) => item && typeof item === "object");
|
|
454
|
+
if (!objects.some((item) => isCodexPayload(item, "auto"))) {
|
|
455
|
+
return {};
|
|
456
|
+
}
|
|
457
|
+
const source = Object.assign({}, ...objects);
|
|
458
|
+
const continuation = pruneEmpty({
|
|
459
|
+
summary: optionalString(source.summary),
|
|
460
|
+
goal: optionalString(source.goal),
|
|
461
|
+
currentState: optionalString(source.currentState || source.current_state),
|
|
462
|
+
changedFiles: normalizeChangedFiles(firstPresent(source.changedFiles, source.changed_files, source.filesChanged, source.files_changed)),
|
|
463
|
+
commands: normalizeEvents(firstPresent(source.commands, source.commandsRun, source.commands_run, source.verification?.commands)),
|
|
464
|
+
tests: normalizeEvents(firstPresent(source.tests, source.testResults, source.test_results, source.verification?.tests)),
|
|
465
|
+
testStatus: optionalString(source.testStatus || source.test_status || source.verification?.status || source.status),
|
|
466
|
+
blockers: normalizeStringList(firstPresent(source.blockers, source.blockedBy, source.blocked_by)),
|
|
467
|
+
nextSteps: normalizeStringList(firstPresent(source.nextSteps, source.next_steps)),
|
|
468
|
+
decisions: normalizeStringList(source.decisions),
|
|
469
|
+
findings: normalizeFindings(firstPresent(source.findings, source.review?.findings)),
|
|
470
|
+
diff: optionalString(source.diff || source.patch),
|
|
471
|
+
residualRisk: optionalString(source.residualRisk || source.residual_risk)
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
return Object.keys(continuation).length > 0
|
|
475
|
+
? { codexContinuation: continuation }
|
|
476
|
+
: {};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function bundleDocumentMetadata(metadata) {
|
|
480
|
+
return metadata.codexContinuation
|
|
481
|
+
? { codexContinuation: metadata.codexContinuation }
|
|
482
|
+
: {};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function renderCodexContinuationMarkdown(object, artifactType, metadata) {
|
|
486
|
+
const continuation = metadata.codexContinuation || {};
|
|
487
|
+
const title = object.title || object.name || titleForCodexType(artifactType);
|
|
488
|
+
const lines = [`# ${title}`, "", `Source: Codex`, `Artifact type: ${artifactType}`, ""];
|
|
489
|
+
|
|
490
|
+
addTextSection(lines, "Summary", continuation.summary);
|
|
491
|
+
addTextSection(lines, "Goal", continuation.goal);
|
|
492
|
+
addTextSection(lines, "Current State", continuation.currentState);
|
|
493
|
+
addRecordSection(lines, "Changed Files", continuation.changedFiles, formatChangedFile);
|
|
494
|
+
addRecordSection(lines, "Commands", continuation.commands, formatEvent);
|
|
495
|
+
addRecordSection(lines, "Tests", continuation.tests, formatEvent);
|
|
496
|
+
addTextSection(lines, "Test Status", continuation.testStatus);
|
|
497
|
+
addRecordSection(lines, "Findings", continuation.findings, formatFinding);
|
|
498
|
+
addListSection(lines, "Decisions", continuation.decisions);
|
|
499
|
+
addListSection(lines, "Blockers", continuation.blockers);
|
|
500
|
+
addListSection(lines, "Next Steps", continuation.nextSteps);
|
|
501
|
+
addTextSection(lines, "Residual Risk", continuation.residualRisk);
|
|
502
|
+
|
|
503
|
+
if (continuation.diff) {
|
|
504
|
+
lines.push("## Diff", "", "```diff", continuation.diff, "```", "");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function titleForCodexType(artifactType) {
|
|
511
|
+
if (artifactType === "code-review") {
|
|
512
|
+
return "Codex Code Review";
|
|
513
|
+
}
|
|
514
|
+
if (artifactType === "test-report") {
|
|
515
|
+
return "Codex Verification Report";
|
|
516
|
+
}
|
|
517
|
+
if (artifactType === "diff-walkthrough") {
|
|
518
|
+
return "Codex Diff Walkthrough";
|
|
519
|
+
}
|
|
520
|
+
return "Codex Handoff";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function addTextSection(lines, title, value) {
|
|
524
|
+
if (!value) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
lines.push(`## ${title}`, "", value, "");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function addListSection(lines, title, values) {
|
|
531
|
+
if (!values?.length) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
lines.push(`## ${title}`, "", ...values.map((value) => `- ${value}`), "");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function addRecordSection(lines, title, values, formatter) {
|
|
538
|
+
if (!values?.length) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
lines.push(`## ${title}`, "", ...values.map((value) => `- ${formatter(value)}`), "");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function formatChangedFile(file) {
|
|
545
|
+
return [
|
|
546
|
+
file.path ? `\`${file.path}\`` : "",
|
|
547
|
+
file.status,
|
|
548
|
+
file.summary
|
|
549
|
+
].filter(Boolean).join(" — ");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function formatEvent(event) {
|
|
553
|
+
return [
|
|
554
|
+
event.command ? `\`${event.command}\`` : event.name || event.title || "",
|
|
555
|
+
event.status,
|
|
556
|
+
event.summary
|
|
557
|
+
].filter(Boolean).join(" — ");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function formatFinding(finding) {
|
|
561
|
+
return [
|
|
562
|
+
finding.severity,
|
|
563
|
+
finding.file ? `\`${finding.file}${finding.line ? `:${finding.line}` : ""}\`` : "",
|
|
564
|
+
finding.title || finding.message || finding.summary
|
|
565
|
+
].filter(Boolean).join(" — ");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function normalizeChangedFiles(value) {
|
|
569
|
+
return normalizeRecordList(value).map((item) => pruneEmpty({
|
|
570
|
+
path: typeof item === "string" ? optionalString(item) : optionalString(item.path || item.file || item.name),
|
|
571
|
+
status: optionalString(item.status || item.changeType || item.change_type),
|
|
572
|
+
summary: optionalString(item.summary || item.description),
|
|
573
|
+
additions: integerOrUndefined(item.additions),
|
|
574
|
+
deletions: integerOrUndefined(item.deletions)
|
|
575
|
+
})).filter((item) => Object.keys(item).length > 0);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function normalizeEvents(value) {
|
|
579
|
+
return normalizeRecordList(value).map((item) => {
|
|
580
|
+
if (typeof item === "string") {
|
|
581
|
+
return { command: item };
|
|
582
|
+
}
|
|
583
|
+
return pruneEmpty({
|
|
584
|
+
command: optionalString(item.command || item.cmd || item.name),
|
|
585
|
+
title: optionalString(item.title),
|
|
586
|
+
status: optionalString(item.status || item.result),
|
|
587
|
+
summary: optionalString(item.summary || item.output || item.message)
|
|
588
|
+
});
|
|
589
|
+
}).filter((item) => Object.keys(item).length > 0);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function normalizeFindings(value) {
|
|
593
|
+
return normalizeRecordList(value).map((item) => pruneEmpty({
|
|
594
|
+
severity: optionalString(item.severity || item.priority),
|
|
595
|
+
file: optionalString(item.file || item.path),
|
|
596
|
+
line: integerOrUndefined(item.line || item.start),
|
|
597
|
+
title: optionalString(item.title),
|
|
598
|
+
message: typeof item === "string" ? optionalString(item) : optionalString(item.message || item.body),
|
|
599
|
+
summary: optionalString(item.summary)
|
|
600
|
+
})).filter((item) => Object.keys(item).length > 0);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function normalizeStringList(value) {
|
|
604
|
+
if (value === undefined || value === null) {
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
if (Array.isArray(value)) {
|
|
608
|
+
return value.map((item) => {
|
|
609
|
+
if (typeof item === "string") {
|
|
610
|
+
return optionalString(item);
|
|
611
|
+
}
|
|
612
|
+
if (item && typeof item === "object") {
|
|
613
|
+
return optionalString(item.summary || item.title || item.message || JSON.stringify(item));
|
|
614
|
+
}
|
|
615
|
+
return optionalString(item);
|
|
616
|
+
}).filter(Boolean);
|
|
617
|
+
}
|
|
618
|
+
return [optionalString(value)].filter(Boolean);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function normalizeRecordList(value) {
|
|
622
|
+
if (value === undefined || value === null) {
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
if (Array.isArray(value)) {
|
|
626
|
+
return value;
|
|
627
|
+
}
|
|
628
|
+
if (typeof value === "object") {
|
|
629
|
+
return [value];
|
|
630
|
+
}
|
|
631
|
+
return [String(value)];
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function firstPresent(...values) {
|
|
635
|
+
return values.find((value) => value !== undefined && value !== null);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function hasNonEmptyArray(value) {
|
|
639
|
+
return Array.isArray(value) && value.length > 0;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function integerOrUndefined(value) {
|
|
643
|
+
if (value === undefined || value === null || value === "") {
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
const parsed = Number(value);
|
|
647
|
+
return Number.isInteger(parsed) ? parsed : undefined;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function pruneEmpty(value) {
|
|
651
|
+
return Object.fromEntries(
|
|
652
|
+
Object.entries(value).filter(([, item]) => {
|
|
653
|
+
if (item === undefined || item === null || item === "") {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
if (Array.isArray(item) && item.length === 0) {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
return true;
|
|
660
|
+
})
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
287
664
|
function detectAgent(parsed, fileName) {
|
|
288
665
|
if (parsed.kind === "json" && parsed.value && typeof parsed.value === "object") {
|
|
289
666
|
const value = parsed.value.artifact || parsed.value;
|
|
@@ -317,6 +694,18 @@ function safeFormat(value) {
|
|
|
317
694
|
if (normalized === "md") {
|
|
318
695
|
return "markdown";
|
|
319
696
|
}
|
|
697
|
+
if (normalized === "jsx" || normalized === "tsx" || normalized.includes("vnd.ant.react")) {
|
|
698
|
+
return "react";
|
|
699
|
+
}
|
|
700
|
+
if (normalized === "svg" || normalized.includes("svg")) {
|
|
701
|
+
return "svg";
|
|
702
|
+
}
|
|
703
|
+
if (normalized === "mmd" || normalized === "mermaid" || normalized.includes("vnd.ant.mermaid")) {
|
|
704
|
+
return "mermaid";
|
|
705
|
+
}
|
|
706
|
+
if (normalized === "code" || normalized.includes("vnd.ant.code") || normalized.includes("source-code")) {
|
|
707
|
+
return "code";
|
|
708
|
+
}
|
|
320
709
|
if (normalized === "html" || normalized.includes("html")) {
|
|
321
710
|
return "html";
|
|
322
711
|
}
|
|
@@ -477,6 +866,15 @@ function inferArtifactType({ format, fileName, metadata }) {
|
|
|
477
866
|
if (format === "html") {
|
|
478
867
|
return "html-page";
|
|
479
868
|
}
|
|
869
|
+
if (format === "svg" || format === "mermaid") {
|
|
870
|
+
return "diagram";
|
|
871
|
+
}
|
|
872
|
+
if (format === "react") {
|
|
873
|
+
return "component";
|
|
874
|
+
}
|
|
875
|
+
if (format === "code") {
|
|
876
|
+
return "snippet";
|
|
877
|
+
}
|
|
480
878
|
const lowerName = optionalString(fileName).toLowerCase();
|
|
481
879
|
if (lowerName.includes("handoff")) {
|
|
482
880
|
return "handoff";
|
|
@@ -495,7 +893,7 @@ function normalizeArtifactType(value) {
|
|
|
495
893
|
if (!normalized) {
|
|
496
894
|
return "document";
|
|
497
895
|
}
|
|
498
|
-
const allowed = new Set(
|
|
896
|
+
const allowed = new Set(ARTIFACT_TYPES);
|
|
499
897
|
return allowed.has(normalized) ? normalized : "unknown";
|
|
500
898
|
}
|
|
501
899
|
|
|
@@ -530,6 +928,46 @@ function looksLikeJson(value) {
|
|
|
530
928
|
(trimmed.startsWith("[") && trimmed.endsWith("]"));
|
|
531
929
|
}
|
|
532
930
|
|
|
931
|
+
function looksLikeMermaid(value) {
|
|
932
|
+
const firstMeaningfulLine = optionalString(value)
|
|
933
|
+
.split(/\r?\n/)
|
|
934
|
+
.map((line) => line.trim())
|
|
935
|
+
.find((line) => line && !line.startsWith("%%"));
|
|
936
|
+
return /^(graph|flowchart|sequenceDiagram|classDiagram|stateDiagram|stateDiagram-v2|erDiagram|gantt|pie|mindmap|journey)\b/.test(firstMeaningfulLine || "");
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function looksLikeReact(value) {
|
|
940
|
+
const text = optionalString(value);
|
|
941
|
+
return /\b(import\s+React|from\s+['"]react['"]|export\s+default\s+function|export\s+default\s+\()/m.test(text) ||
|
|
942
|
+
/<[A-Z][A-Za-z0-9]*[\s/>]/.test(text);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function isCodeExtension(extension) {
|
|
946
|
+
return new Set([
|
|
947
|
+
".js",
|
|
948
|
+
".ts",
|
|
949
|
+
".py",
|
|
950
|
+
".rb",
|
|
951
|
+
".go",
|
|
952
|
+
".rs",
|
|
953
|
+
".java",
|
|
954
|
+
".c",
|
|
955
|
+
".cc",
|
|
956
|
+
".cpp",
|
|
957
|
+
".cs",
|
|
958
|
+
".php",
|
|
959
|
+
".swift",
|
|
960
|
+
".kt",
|
|
961
|
+
".sh",
|
|
962
|
+
".bash",
|
|
963
|
+
".zsh",
|
|
964
|
+
".sql",
|
|
965
|
+
".toml",
|
|
966
|
+
".yaml",
|
|
967
|
+
".yml"
|
|
968
|
+
]).has(extension);
|
|
969
|
+
}
|
|
970
|
+
|
|
533
971
|
function stripMarkdownCodeFence(value) {
|
|
534
972
|
const trimmed = value.trim();
|
|
535
973
|
const match = /^```(?:\w+)?\n([\s\S]*?)\n```$/.exec(trimmed);
|
package/src/lib/editor-assets.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
export const EDITOR_CLIENT_PATH = "/assets/editor.js";
|
|
4
|
+
export const VIEWER_CLIENT_PATH = "/assets/viewer.js";
|
|
4
5
|
|
|
5
6
|
export const EDITOR_VENDOR_PACKAGES = [
|
|
6
7
|
"@codemirror/autocomplete",
|
|
@@ -24,36 +25,78 @@ export const EDITOR_VENDOR_PACKAGES = [
|
|
|
24
25
|
"@lezer/lr",
|
|
25
26
|
"@lezer/markdown",
|
|
26
27
|
"@marijn/find-cluster-break",
|
|
28
|
+
"@babel/standalone",
|
|
27
29
|
"codemirror",
|
|
28
30
|
"crelt",
|
|
31
|
+
"mermaid",
|
|
32
|
+
"react",
|
|
33
|
+
"react-dom",
|
|
29
34
|
"style-mod",
|
|
30
35
|
"w3c-keyname"
|
|
31
36
|
];
|
|
32
37
|
|
|
33
38
|
const EDITOR_VENDOR_SET = new Set(EDITOR_VENDOR_PACKAGES);
|
|
34
39
|
const EDITOR_VENDOR_ENTRY_OVERRIDES = {
|
|
40
|
+
"@babel/standalone": "babel.min.js",
|
|
35
41
|
"@marijn/find-cluster-break": path.join("src", "index.js"),
|
|
36
42
|
crelt: "index.js",
|
|
43
|
+
mermaid: path.join("dist", "mermaid.esm.min.mjs"),
|
|
44
|
+
react: path.join("umd", "react.production.min.js"),
|
|
45
|
+
"react-dom": path.join("umd", "react-dom.production.min.js"),
|
|
37
46
|
"style-mod": path.join("src", "style-mod.js"),
|
|
38
47
|
"w3c-keyname": "index.js"
|
|
39
48
|
};
|
|
40
49
|
|
|
50
|
+
const EDITOR_VENDOR_IMPORT_OVERRIDES = {
|
|
51
|
+
mermaid: "/vendor/npm/mermaid/dist/mermaid.esm.min.mjs"
|
|
52
|
+
};
|
|
53
|
+
|
|
41
54
|
export function editorImportMapJson() {
|
|
42
55
|
return JSON.stringify({
|
|
43
56
|
imports: Object.fromEntries(
|
|
44
|
-
EDITOR_VENDOR_PACKAGES.map((packageName) => [
|
|
57
|
+
EDITOR_VENDOR_PACKAGES.map((packageName) => [
|
|
58
|
+
packageName,
|
|
59
|
+
EDITOR_VENDOR_IMPORT_OVERRIDES[packageName] || `/vendor/npm/${packageName}`
|
|
60
|
+
])
|
|
45
61
|
)
|
|
46
62
|
});
|
|
47
63
|
}
|
|
48
64
|
|
|
49
|
-
export function editorVendorPath(
|
|
65
|
+
export function editorVendorPath(specifier, packageRoot) {
|
|
66
|
+
const { packageName, subpath } = splitPackageSpecifier(specifier);
|
|
50
67
|
if (!EDITOR_VENDOR_SET.has(packageName)) {
|
|
51
68
|
return null;
|
|
52
69
|
}
|
|
70
|
+
if (subpath) {
|
|
71
|
+
const packageDir = path.join(packageRoot, "node_modules", packageName);
|
|
72
|
+
const candidate = path.normalize(path.join(packageDir, subpath));
|
|
73
|
+
if (!candidate.startsWith(packageDir + path.sep)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return candidate;
|
|
77
|
+
}
|
|
53
78
|
const entry = EDITOR_VENDOR_ENTRY_OVERRIDES[packageName] || path.join("dist", "index.js");
|
|
54
79
|
return path.join(packageRoot, "node_modules", packageName, entry);
|
|
55
80
|
}
|
|
56
81
|
|
|
82
|
+
function splitPackageSpecifier(specifier) {
|
|
83
|
+
const parts = String(specifier || "").split("/").filter(Boolean);
|
|
84
|
+
if (parts[0]?.startsWith("@")) {
|
|
85
|
+
return {
|
|
86
|
+
packageName: parts.slice(0, 2).join("/"),
|
|
87
|
+
subpath: parts.slice(2).join("/")
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
packageName: parts[0] || "",
|
|
92
|
+
subpath: parts.slice(1).join("/")
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
57
96
|
export function editorClientFilePath(packageRoot) {
|
|
58
97
|
return path.join(packageRoot, "src", "client", "editor.js");
|
|
59
98
|
}
|
|
99
|
+
|
|
100
|
+
export function viewerClientFilePath(packageRoot) {
|
|
101
|
+
return path.join(packageRoot, "src", "client", "viewer.js");
|
|
102
|
+
}
|