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.
@@ -2,7 +2,8 @@ import path from "node:path";
2
2
  import { createHash } from "node:crypto";
3
3
  import { ARTIFACT_TYPES, contentTypeForFormat, normalizeFormat } from "./storage.js";
4
4
 
5
- const KNOWN_AGENTS = new Set(["auto", "artifacty", "claude", "codex", "gemini", "generic"]);
5
+ const CONTINUATION_AGENTS = new Set(["codex", "copilot", "cursor"]);
6
+ const KNOWN_AGENTS = new Set(["auto", "artifacty", "claude", "codex", "copilot", "cursor", "gemini", "generic"]);
6
7
 
7
8
  export function convertAgentArtifact(input = {}) {
8
9
  const originalAgent = normalizeAgent(input.agent || input.sourceAgent || input.source_agent || "auto");
@@ -12,12 +13,21 @@ export function convertAgentArtifact(input = {}) {
12
13
  const sourcePath = optionalString(input.sourcePath || input.path || input.file);
13
14
  const fileName = optionalString(input.fileName || input.filename || (sourcePath ? path.basename(sourcePath) : ""));
14
15
  const explicitContentType = optionalString(input.contentType || input.mimeType || input.mime_type);
15
- const content = String(decoded.content ?? input.content ?? payload ?? "");
16
+ let content = String(decoded.content ?? input.content ?? payload ?? "");
16
17
  const format = normalizeFormat(
17
18
  input.format ||
18
19
  decoded.format ||
19
20
  detectFormat({ content, contentType: explicitContentType || decoded.contentType, fileName })
20
21
  );
22
+ const media = normalizeMediaContent({
23
+ content,
24
+ format,
25
+ contentType: explicitContentType || decoded.contentType,
26
+ fileName
27
+ });
28
+ if (media) {
29
+ content = media.content;
30
+ }
21
31
  const sourceAgent = optionalString(input.sourceAgent || input.source_agent || decoded.sourceAgent) ||
22
32
  (originalAgent === "auto" ? detectAgent(parsed, fileName) : originalAgent);
23
33
 
@@ -26,7 +36,7 @@ export function convertAgentArtifact(input = {}) {
26
36
  optionalString(decoded.title) ||
27
37
  inferTitle({ content, format, fileName, sourceAgent });
28
38
 
29
- const contentType = explicitContentType || decoded.contentType || contentTypeForFormat(format);
39
+ const contentType = explicitContentType || decoded.contentType || media?.mimeType || contentTypeForFormat(format);
30
40
  const artifactType = normalizeArtifactType(
31
41
  input.artifactType ||
32
42
  input.artifact_type ||
@@ -52,6 +62,7 @@ export function convertAgentArtifact(input = {}) {
52
62
  metadata: {
53
63
  ...normalizeMetadata(decoded.metadata),
54
64
  ...normalizeMetadata(input.metadata),
65
+ ...mediaMetadata(media),
55
66
  artifactyImport: {
56
67
  converter: "agent-artifact-v1",
57
68
  originalAgent,
@@ -71,9 +82,21 @@ export function detectFormat({ content = "", contentType = "", fileName = "" } =
71
82
  if (type.includes("vnd.ant.code") || type.includes("source-code")) {
72
83
  return "code";
73
84
  }
85
+ if (type.includes("sarif")) {
86
+ return "sarif";
87
+ }
88
+ if (type.includes("csv")) {
89
+ return "csv";
90
+ }
74
91
  if (type.includes("svg")) {
75
92
  return "svg";
76
93
  }
94
+ if (isSupportedImageMime(type)) {
95
+ return "image";
96
+ }
97
+ if (isSupportedVideoMime(type)) {
98
+ return "video";
99
+ }
77
100
  if (type.includes("vnd.ant.mermaid") || type.includes("mermaid")) {
78
101
  return "mermaid";
79
102
  }
@@ -93,7 +116,21 @@ export function detectFormat({ content = "", contentType = "", fileName = "" } =
93
116
  return "text";
94
117
  }
95
118
 
96
- const extension = path.extname(fileName).toLowerCase();
119
+ const lowerName = optionalString(fileName).toLowerCase();
120
+ if (lowerName.endsWith(".sarif") || lowerName.endsWith(".sarif.json")) {
121
+ return "sarif";
122
+ }
123
+ if (lowerName.endsWith(".csv")) {
124
+ return "csv";
125
+ }
126
+ if (isImageFileName(lowerName)) {
127
+ return "image";
128
+ }
129
+ if (isVideoFileName(lowerName)) {
130
+ return "video";
131
+ }
132
+
133
+ const extension = path.extname(lowerName);
97
134
  if (extension === ".html" || extension === ".htm") {
98
135
  return "html";
99
136
  }
@@ -129,12 +166,24 @@ export function detectFormat({ content = "", contentType = "", fileName = "" } =
129
166
  if (looksLikeReact(trimmed)) {
130
167
  return "react";
131
168
  }
169
+ if (looksLikeMediaDataUrl(trimmed, "image")) {
170
+ return "image";
171
+ }
172
+ if (looksLikeMediaDataUrl(trimmed, "video")) {
173
+ return "video";
174
+ }
132
175
  if (/^<!doctype html/i.test(trimmed) || /^<html[\s>]/i.test(trimmed)) {
133
176
  return "html";
134
177
  }
178
+ if (looksLikeSarif(trimmed)) {
179
+ return "sarif";
180
+ }
135
181
  if (looksLikeJson(trimmed)) {
136
182
  return "json";
137
183
  }
184
+ if (looksLikeCsv(trimmed)) {
185
+ return "csv";
186
+ }
138
187
  if (/^#{1,3}\s+\S/m.test(trimmed) || /^[-*]\s+\S/m.test(trimmed)) {
139
188
  return "markdown";
140
189
  }
@@ -160,13 +209,28 @@ function decodePayload(parsed, agent) {
160
209
  function decodeObjectPayload(value, agent) {
161
210
  const artifactObject = value.artifact && typeof value.artifact === "object" ? value.artifact : value;
162
211
 
163
- if (Array.isArray(artifactObject.files) || artifactObject.bundle) {
212
+ if (Array.isArray(artifactObject.files) || Array.isArray(artifactObject.assets) || Array.isArray(artifactObject.media) || Array.isArray(artifactObject.attachments) || artifactObject.bundle) {
164
213
  return decodeBundlePayload(artifactObject, agent);
165
214
  }
166
215
 
167
- const codexContinuation = decodeCodexContinuationPayload(artifactObject, agent);
168
- if (codexContinuation) {
169
- return codexContinuation;
216
+ const continuation = decodeContinuationPayload(artifactObject, agent);
217
+ if (continuation) {
218
+ return continuation;
219
+ }
220
+
221
+ if (isSarifObject(artifactObject)) {
222
+ return {
223
+ content: JSON.stringify(artifactObject, null, 2),
224
+ format: "sarif",
225
+ contentType: "application/sarif+json; charset=utf-8",
226
+ title: artifactObject.title || artifactObject.name || "SARIF report",
227
+ sourceAgent: artifactObject.sourceAgent || artifactObject.source_agent || artifactObject.agent || (agent === "auto" ? undefined : agent),
228
+ artifactType: artifactObject.artifactType || artifactObject.artifact_type || "analysis-report",
229
+ tags: artifactObject.tags,
230
+ metadata: {
231
+ originalPayloadShape: "sarif"
232
+ }
233
+ };
170
234
  }
171
235
 
172
236
  if (typeof artifactObject.content === "string" && !Array.isArray(artifactObject.content)) {
@@ -208,6 +272,22 @@ function decodeObjectPayload(value, agent) {
208
272
  return objectContent(artifactObject, "react", artifactObject.react || artifactObject.jsx, "react");
209
273
  }
210
274
 
275
+ if (artifactObject.image !== undefined || artifactObject.screenshot !== undefined) {
276
+ return mediaObjectContent(artifactObject, "image", artifactObject.image ?? artifactObject.screenshot);
277
+ }
278
+
279
+ if (artifactObject.video !== undefined || artifactObject.recording !== undefined || artifactObject.demo !== undefined) {
280
+ return mediaObjectContent(artifactObject, "video", artifactObject.video ?? artifactObject.recording ?? artifactObject.demo);
281
+ }
282
+
283
+ if (typeof artifactObject.sarif === "string") {
284
+ return objectContent(artifactObject, "sarif", artifactObject.sarif, "sarif");
285
+ }
286
+
287
+ if (typeof artifactObject.csv === "string") {
288
+ return objectContent(artifactObject, "csv", artifactObject.csv, "csv");
289
+ }
290
+
211
291
  if (typeof artifactObject.html === "string") {
212
292
  return objectContent(artifactObject, "html", artifactObject.html, "html");
213
293
  }
@@ -312,10 +392,38 @@ function objectContent(object, shape, content, format) {
312
392
  };
313
393
  }
314
394
 
395
+ function mediaObjectContent(object, format, mediaValue) {
396
+ const sourceContentType = object.contentType || object.content_type || object.mimeType || object.mime_type || object.type;
397
+ const content = typeof mediaValue === "string"
398
+ ? mediaValue
399
+ : optionalString(mediaValue?.data || mediaValue?.base64 || mediaValue?.content || mediaValue?.url);
400
+ const contentType = optionalString(mediaValue?.mimeType || mediaValue?.mime_type || sourceContentType);
401
+ return {
402
+ content,
403
+ format,
404
+ contentType,
405
+ title: object.title || object.name,
406
+ sourceAgent: object.sourceAgent || object.source_agent || object.agent,
407
+ artifactType: object.artifactType || object.artifact_type || "asset",
408
+ tags: object.tags,
409
+ metadata: {
410
+ originalPayloadShape: format,
411
+ originalId: object.id,
412
+ originalContentType: sourceContentType || contentType
413
+ }
414
+ };
415
+ }
416
+
315
417
  function decodeBundlePayload(object, agent) {
316
418
  const bundle = object.bundle && typeof object.bundle === "object" ? object.bundle : object;
317
419
  const files = Array.isArray(bundle.files) ? bundle.files : [];
318
- const codexMetadata = collectCodexContinuationMetadata({ agent }, bundle, object);
420
+ const assets = normalizeBundleAssets(firstPresent(bundle.assets, bundle.media, bundle.attachments));
421
+ const parts = normalizeBundleParts(bundle.parts, assets, optionalString(bundle.summary || object.summary || bundle.text));
422
+ const sourceAgent = object.sourceAgent || object.source_agent || object.agent || agent;
423
+ const continuationAgent = detectContinuationAgent({ agent: sourceAgent }, "auto");
424
+ const continuationMetadata = continuationAgent
425
+ ? collectContinuationMetadata(continuationAgent, { agent: sourceAgent }, bundle, object)
426
+ : {};
319
427
  const normalizedFiles = files.map((file, index) => {
320
428
  const content = typeof file.content === "string" ? file.content : "";
321
429
  return {
@@ -335,8 +443,11 @@ function decodeBundlePayload(object, agent) {
335
443
  schemaVersion: 1,
336
444
  artifactType: "bundle",
337
445
  title: object.title || bundle.title || "Artifact bundle",
446
+ text: optionalString(bundle.text || bundle.summary || object.summary),
447
+ parts,
448
+ assets,
338
449
  files: normalizedFiles,
339
- ...bundleDocumentMetadata(codexMetadata)
450
+ ...bundleDocumentMetadata(continuationMetadata)
340
451
  }, null, 2),
341
452
  format: "json",
342
453
  contentType: "application/vnd.artifacty.bundle+json; charset=utf-8",
@@ -347,37 +458,83 @@ function decodeBundlePayload(object, agent) {
347
458
  metadata: {
348
459
  originalPayloadShape: "artifact-bundle",
349
460
  fileCount: normalizedFiles.length,
461
+ assetCount: assets.length,
350
462
  bundlePolicy: "text-files-stored-inline-in-bundle-json",
351
- ...codexMetadata
463
+ assetPolicy: assets.length > 0 ? "base64-assets-stored-inline-in-bundle-json" : undefined,
464
+ ...continuationMetadata
352
465
  }
353
466
  };
354
467
  }
355
468
 
356
- function decodeCodexContinuationPayload(object, agent) {
357
- if (!isCodexPayload(object, agent) || hasDirectContentField(object)) {
469
+ function normalizeBundleAssets(value) {
470
+ if (!Array.isArray(value)) {
471
+ return [];
472
+ }
473
+ return value.map((asset, index) => {
474
+ if (!asset || typeof asset !== "object") {
475
+ return null;
476
+ }
477
+ const rawData = optionalString(asset.data || asset.base64 || asset.content || asset.url);
478
+ const dataUrl = parseDataUrl(rawData);
479
+ const mimeType = dataUrl?.mimeType ||
480
+ mediaMimeTypeFromContentType(asset.mimeType || asset.mime_type || asset.contentType || asset.content_type || asset.type, "image") ||
481
+ mediaMimeTypeFromContentType(asset.mimeType || asset.mime_type || asset.contentType || asset.content_type || asset.type, "video") ||
482
+ mediaMimeTypeFromFileName(asset.fileName || asset.filename || asset.name || asset.path) ||
483
+ "application/octet-stream";
484
+ const data = dataUrl?.base64 || normalizeBase64(rawData);
485
+ if (!data) {
486
+ return null;
487
+ }
488
+ return pruneEmpty({
489
+ id: optionalString(asset.id) || `asset-${index + 1}`,
490
+ name: optionalString(asset.name || asset.fileName || asset.filename || asset.path),
491
+ role: optionalString(asset.role || asset.kind || asset.type),
492
+ mimeType,
493
+ encoding: "base64",
494
+ data,
495
+ sizeBytes: Buffer.byteLength(data, "base64"),
496
+ sha256: createHash("sha256").update(Buffer.from(data, "base64")).digest("hex"),
497
+ caption: optionalString(asset.caption || asset.alt || asset.description)
498
+ });
499
+ }).filter(Boolean);
500
+ }
501
+
502
+ function normalizeBundleParts(value, assets, text) {
503
+ if (Array.isArray(value) && value.length > 0) {
504
+ return value;
505
+ }
506
+ return [
507
+ text ? { type: "text", text } : null,
508
+ ...assets.map((asset) => ({ type: "asset-ref", assetId: asset.id, mimeType: asset.mimeType }))
509
+ ].filter(Boolean);
510
+ }
511
+
512
+ function decodeContinuationPayload(object, agent) {
513
+ const sourceAgent = detectContinuationAgent(object, agent);
514
+ if (!sourceAgent || hasDirectContentField(object)) {
358
515
  return null;
359
516
  }
360
517
 
361
- const artifactType = inferCodexContinuationType(object);
518
+ const artifactType = inferContinuationType(object);
362
519
  if (!artifactType) {
363
520
  return null;
364
521
  }
365
522
 
366
- const metadata = collectCodexContinuationMetadata({ agent }, object);
523
+ const metadata = collectContinuationMetadata(sourceAgent, { agent }, object);
367
524
  return {
368
- content: renderCodexContinuationMarkdown(object, artifactType, metadata),
525
+ content: renderContinuationMarkdown(sourceAgent, object, artifactType, metadata),
369
526
  format: "markdown",
370
527
  contentType: "text/markdown; charset=utf-8",
371
- title: object.title || object.name || titleForCodexType(artifactType),
372
- sourceAgent: "codex",
528
+ title: object.title || object.name || titleForContinuationType(sourceAgent, artifactType),
529
+ sourceAgent,
373
530
  artifactType,
374
531
  tags: uniqueStrings([
375
532
  artifactType,
376
- "codex",
533
+ sourceAgent,
377
534
  ...normalizeTags(object.tags)
378
535
  ]),
379
536
  metadata: {
380
- originalPayloadShape: "codex-continuation",
537
+ originalPayloadShape: `${sourceAgent}-continuation`,
381
538
  continuationKind: artifactType,
382
539
  ...metadata
383
540
  }
@@ -394,10 +551,17 @@ function hasDirectContentField(object) {
394
551
  typeof object.svg === "string" ||
395
552
  typeof object.mermaid === "string" ||
396
553
  typeof object.react === "string" ||
397
- typeof object.jsx === "string";
554
+ typeof object.jsx === "string" ||
555
+ typeof object.sarif === "string" ||
556
+ typeof object.csv === "string" ||
557
+ object.image !== undefined ||
558
+ object.screenshot !== undefined ||
559
+ object.video !== undefined ||
560
+ object.recording !== undefined ||
561
+ object.demo !== undefined;
398
562
  }
399
563
 
400
- function isCodexPayload(object, agent) {
564
+ function detectContinuationAgent(object, agent) {
401
565
  const explicit = normalizeAgent(
402
566
  object.sourceAgent ||
403
567
  object.source_agent ||
@@ -407,10 +571,10 @@ function isCodexPayload(object, agent) {
407
571
  object.created_by ||
408
572
  agent
409
573
  );
410
- return explicit === "codex";
574
+ return CONTINUATION_AGENTS.has(explicit) ? explicit : "";
411
575
  }
412
576
 
413
- function inferCodexContinuationType(object) {
577
+ function inferContinuationType(object) {
414
578
  const explicit = optionalString(object.artifactType || object.artifact_type || object.kind || object.category || object.purpose || object.type).toLowerCase();
415
579
  if (explicit.includes("review")) {
416
580
  return "code-review";
@@ -449,11 +613,8 @@ function inferCodexContinuationType(object) {
449
613
  return null;
450
614
  }
451
615
 
452
- function collectCodexContinuationMetadata(...sources) {
616
+ function collectContinuationMetadata(sourceAgent, ...sources) {
453
617
  const objects = sources.filter((item) => item && typeof item === "object");
454
- if (!objects.some((item) => isCodexPayload(item, "auto"))) {
455
- return {};
456
- }
457
618
  const source = Object.assign({}, ...objects);
458
619
  const continuation = pruneEmpty({
459
620
  summary: optionalString(source.summary),
@@ -471,21 +632,36 @@ function collectCodexContinuationMetadata(...sources) {
471
632
  residualRisk: optionalString(source.residualRisk || source.residual_risk)
472
633
  });
473
634
 
474
- return Object.keys(continuation).length > 0
475
- ? { codexContinuation: continuation }
476
- : {};
635
+ if (Object.keys(continuation).length === 0) {
636
+ return {};
637
+ }
638
+
639
+ const metadata = { continuation };
640
+ metadata[`${sourceAgent}Continuation`] = continuation;
641
+ return metadata;
477
642
  }
478
643
 
479
644
  function bundleDocumentMetadata(metadata) {
480
- return metadata.codexContinuation
481
- ? { codexContinuation: metadata.codexContinuation }
482
- : {};
645
+ const result = {};
646
+ if (metadata.continuation) {
647
+ result.continuation = metadata.continuation;
648
+ }
649
+ if (metadata.codexContinuation) {
650
+ result.codexContinuation = metadata.codexContinuation;
651
+ }
652
+ if (metadata.copilotContinuation) {
653
+ result.copilotContinuation = metadata.copilotContinuation;
654
+ }
655
+ if (metadata.cursorContinuation) {
656
+ result.cursorContinuation = metadata.cursorContinuation;
657
+ }
658
+ return result;
483
659
  }
484
660
 
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}`, ""];
661
+ function renderContinuationMarkdown(sourceAgent, object, artifactType, metadata) {
662
+ const continuation = metadata.continuation || metadata[`${sourceAgent}Continuation`] || {};
663
+ const title = object.title || object.name || titleForContinuationType(sourceAgent, artifactType);
664
+ const lines = [`# ${title}`, "", `Source: ${displayAgentName(sourceAgent)}`, `Artifact type: ${artifactType}`, ""];
489
665
 
490
666
  addTextSection(lines, "Summary", continuation.summary);
491
667
  addTextSection(lines, "Goal", continuation.goal);
@@ -507,17 +683,31 @@ function renderCodexContinuationMarkdown(object, artifactType, metadata) {
507
683
  return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
508
684
  }
509
685
 
510
- function titleForCodexType(artifactType) {
686
+ function titleForContinuationType(sourceAgent, artifactType) {
687
+ const displayName = displayAgentName(sourceAgent);
511
688
  if (artifactType === "code-review") {
512
- return "Codex Code Review";
689
+ return `${displayName} Code Review`;
513
690
  }
514
691
  if (artifactType === "test-report") {
515
- return "Codex Verification Report";
692
+ return `${displayName} Verification Report`;
516
693
  }
517
694
  if (artifactType === "diff-walkthrough") {
518
- return "Codex Diff Walkthrough";
695
+ return `${displayName} Diff Walkthrough`;
696
+ }
697
+ return `${displayName} Handoff`;
698
+ }
699
+
700
+ function displayAgentName(sourceAgent) {
701
+ if (sourceAgent === "copilot") {
702
+ return "GitHub Copilot";
519
703
  }
520
- return "Codex Handoff";
704
+ if (sourceAgent === "cursor") {
705
+ return "Cursor";
706
+ }
707
+ if (sourceAgent === "codex") {
708
+ return "Codex";
709
+ }
710
+ return sourceAgent || "Agent";
521
711
  }
522
712
 
523
713
  function addTextSection(lines, title, value) {
@@ -683,6 +873,12 @@ function detectAgent(parsed, fileName) {
683
873
  if (lowerName.includes("codex")) {
684
874
  return "codex";
685
875
  }
876
+ if (lowerName.includes("copilot")) {
877
+ return "copilot";
878
+ }
879
+ if (lowerName.includes("cursor")) {
880
+ return "cursor";
881
+ }
686
882
  return "generic";
687
883
  }
688
884
 
@@ -706,6 +902,18 @@ function safeFormat(value) {
706
902
  if (normalized === "code" || normalized.includes("vnd.ant.code") || normalized.includes("source-code")) {
707
903
  return "code";
708
904
  }
905
+ if (normalized === "sarif" || normalized.includes("sarif")) {
906
+ return "sarif";
907
+ }
908
+ if (normalized === "csv" || normalized.includes("csv")) {
909
+ return "csv";
910
+ }
911
+ if (isSupportedImageMime(normalized) || normalized === "image") {
912
+ return "image";
913
+ }
914
+ if (isSupportedVideoMime(normalized) || normalized === "video") {
915
+ return "video";
916
+ }
709
917
  if (normalized === "html" || normalized.includes("html")) {
710
918
  return "html";
711
919
  }
@@ -859,7 +1067,7 @@ function createBundleDocument({ title, text, assets, parts }) {
859
1067
  };
860
1068
  }
861
1069
 
862
- function inferArtifactType({ format, fileName, metadata }) {
1070
+ function inferArtifactType({ format, content, fileName, metadata }) {
863
1071
  if (metadata?.originalPayloadShape === "artifact-bundle") {
864
1072
  return "bundle";
865
1073
  }
@@ -875,6 +1083,18 @@ function inferArtifactType({ format, fileName, metadata }) {
875
1083
  if (format === "code") {
876
1084
  return "snippet";
877
1085
  }
1086
+ if (format === "sarif") {
1087
+ return "analysis-report";
1088
+ }
1089
+ if (format === "csv") {
1090
+ return looksLikeAnalysisCsv(content) ||
1091
+ /findings?|security|review|scan/i.test(optionalString(fileName))
1092
+ ? "analysis-report"
1093
+ : "table";
1094
+ }
1095
+ if (format === "image" || format === "video") {
1096
+ return "asset";
1097
+ }
878
1098
  const lowerName = optionalString(fileName).toLowerCase();
879
1099
  if (lowerName.includes("handoff")) {
880
1100
  return "handoff";
@@ -888,6 +1108,14 @@ function inferArtifactType({ format, fileName, metadata }) {
888
1108
  return "document";
889
1109
  }
890
1110
 
1111
+ function looksLikeAnalysisCsv(content) {
1112
+ const [header = ""] = optionalString(content).split(/\r?\n/, 1);
1113
+ const normalized = header.toLowerCase();
1114
+ return normalized.includes("severity") &&
1115
+ (normalized.includes("message") || normalized.includes("description")) &&
1116
+ (normalized.includes("file") || normalized.includes("path") || normalized.includes("rule"));
1117
+ }
1118
+
891
1119
  function normalizeArtifactType(value) {
892
1120
  const normalized = optionalString(value).toLowerCase();
893
1121
  if (!normalized) {
@@ -928,6 +1156,157 @@ function looksLikeJson(value) {
928
1156
  (trimmed.startsWith("[") && trimmed.endsWith("]"));
929
1157
  }
930
1158
 
1159
+ function looksLikeSarif(value) {
1160
+ if (!looksLikeJson(value)) {
1161
+ return false;
1162
+ }
1163
+ try {
1164
+ return isSarifObject(JSON.parse(optionalString(value)));
1165
+ } catch {
1166
+ return false;
1167
+ }
1168
+ }
1169
+
1170
+ function isSarifObject(value) {
1171
+ return value &&
1172
+ typeof value === "object" &&
1173
+ !Array.isArray(value) &&
1174
+ Array.isArray(value.runs) &&
1175
+ (typeof value.version === "string" || optionalString(value.$schema).toLowerCase().includes("sarif"));
1176
+ }
1177
+
1178
+ function looksLikeCsv(value) {
1179
+ const lines = optionalString(value)
1180
+ .split(/\r?\n/)
1181
+ .map((line) => line.trimEnd())
1182
+ .filter(Boolean)
1183
+ .slice(0, 5);
1184
+ if (lines.length < 2 || lines[0].trimStart().startsWith("|")) {
1185
+ return false;
1186
+ }
1187
+ const counts = lines.map(csvFieldCount);
1188
+ return counts[0] > 1 && counts.every((count) => count === counts[0]);
1189
+ }
1190
+
1191
+ function csvFieldCount(line) {
1192
+ let count = 1;
1193
+ let inQuotes = false;
1194
+ for (let index = 0; index < line.length; index += 1) {
1195
+ const char = line[index];
1196
+ if (char === "\"") {
1197
+ if (inQuotes && line[index + 1] === "\"") {
1198
+ index += 1;
1199
+ } else {
1200
+ inQuotes = !inQuotes;
1201
+ }
1202
+ } else if (char === "," && !inQuotes) {
1203
+ count += 1;
1204
+ }
1205
+ }
1206
+ return count;
1207
+ }
1208
+
1209
+ function looksLikeMediaDataUrl(value, kind) {
1210
+ const match = /^data:([^;,]+);base64,/i.exec(optionalString(value));
1211
+ if (!match) {
1212
+ return false;
1213
+ }
1214
+ const mimeType = match[1].toLowerCase();
1215
+ return kind === "image" ? isSupportedImageMime(mimeType) : isSupportedVideoMime(mimeType);
1216
+ }
1217
+
1218
+ function normalizeMediaContent({ content, format, contentType, fileName }) {
1219
+ if (format !== "image" && format !== "video") {
1220
+ return null;
1221
+ }
1222
+ const dataUrl = parseDataUrl(content);
1223
+ const mimeType = dataUrl?.mimeType ||
1224
+ mediaMimeTypeFromContentType(contentType, format) ||
1225
+ mediaMimeTypeFromFileName(fileName) ||
1226
+ "";
1227
+ const normalized = dataUrl?.base64 || normalizeBase64(content);
1228
+ if (!normalized) {
1229
+ return null;
1230
+ }
1231
+ return {
1232
+ content: normalized,
1233
+ mimeType,
1234
+ encoding: "base64",
1235
+ originalEncoding: dataUrl ? "data-url" : "base64"
1236
+ };
1237
+ }
1238
+
1239
+ function mediaMetadata(media) {
1240
+ if (!media) {
1241
+ return {};
1242
+ }
1243
+ return {
1244
+ mimeType: media.mimeType || undefined,
1245
+ encoding: media.encoding,
1246
+ mediaPolicy: "base64-media-stored-inline; /raw decodes bytes for preview and download",
1247
+ originalEncoding: media.originalEncoding
1248
+ };
1249
+ }
1250
+
1251
+ function parseDataUrl(value) {
1252
+ const match = /^data:([^;,]+);base64,([A-Za-z0-9+/=_-]+)$/i.exec(optionalString(value).replace(/\s+/g, ""));
1253
+ if (!match) {
1254
+ return null;
1255
+ }
1256
+ return {
1257
+ mimeType: match[1].toLowerCase(),
1258
+ base64: normalizeBase64(match[2])
1259
+ };
1260
+ }
1261
+
1262
+ function normalizeBase64(value) {
1263
+ const normalized = optionalString(value).replace(/\s+/g, "").replaceAll("-", "+").replaceAll("_", "/");
1264
+ if (!normalized || !/^[A-Za-z0-9+/]+={0,2}$/.test(normalized)) {
1265
+ return "";
1266
+ }
1267
+ return normalized;
1268
+ }
1269
+
1270
+ function mediaMimeTypeFromContentType(contentType, format) {
1271
+ const normalized = optionalString(contentType).toLowerCase().split(";")[0];
1272
+ if (format === "image" && isSupportedImageMime(normalized)) {
1273
+ return normalized;
1274
+ }
1275
+ if (format === "video" && isSupportedVideoMime(normalized)) {
1276
+ return normalized;
1277
+ }
1278
+ return "";
1279
+ }
1280
+
1281
+ function mediaMimeTypeFromFileName(fileName) {
1282
+ const extension = path.extname(optionalString(fileName).toLowerCase());
1283
+ return {
1284
+ ".png": "image/png",
1285
+ ".jpg": "image/jpeg",
1286
+ ".jpeg": "image/jpeg",
1287
+ ".gif": "image/gif",
1288
+ ".webp": "image/webp",
1289
+ ".mp4": "video/mp4",
1290
+ ".webm": "video/webm"
1291
+ }[extension] || "";
1292
+ }
1293
+
1294
+ function isImageFileName(fileName) {
1295
+ return isSupportedImageMime(mediaMimeTypeFromFileName(fileName));
1296
+ }
1297
+
1298
+ function isVideoFileName(fileName) {
1299
+ return isSupportedVideoMime(mediaMimeTypeFromFileName(fileName));
1300
+ }
1301
+
1302
+ function isSupportedImageMime(value) {
1303
+ return new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]).has(optionalString(value).toLowerCase().split(";")[0]);
1304
+ }
1305
+
1306
+ function isSupportedVideoMime(value) {
1307
+ return new Set(["video/mp4", "video/webm"]).has(optionalString(value).toLowerCase().split(";")[0]);
1308
+ }
1309
+
931
1310
  function looksLikeMermaid(value) {
932
1311
  const firstMeaningfulLine = optionalString(value)
933
1312
  .split(/\r?\n/)
@@ -989,6 +1368,12 @@ function normalizeAgent(value) {
989
1368
  if (normalized === "gemini-cli" || normalized === "google") {
990
1369
  return "gemini";
991
1370
  }
1371
+ if (normalized === "github-copilot" || normalized === "copilot-chat" || normalized === "vscode-copilot" || normalized === "vs-code-copilot") {
1372
+ return "copilot";
1373
+ }
1374
+ if (normalized === "cursor-ai") {
1375
+ return "cursor";
1376
+ }
992
1377
  if (normalized === "openai" || normalized === "chatgpt") {
993
1378
  return "codex";
994
1379
  }