artifacty 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +52 -0
- package/CLAUDE.md +64 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/docs/artifact-schema-v1.md +96 -0
- package/docs/assets/artifacty.png +0 -0
- package/docs/integrations.md +196 -0
- package/docs/release-checklist.md +30 -0
- package/package.json +55 -0
- package/scripts/smoke.sh +104 -0
- package/src/cli.js +348 -0
- package/src/client/editor.js +260 -0
- package/src/lib/backup.js +75 -0
- package/src/lib/check.js +124 -0
- package/src/lib/converters.js +586 -0
- package/src/lib/diff.js +69 -0
- package/src/lib/editor-assets.js +59 -0
- package/src/lib/i18n.js +175 -0
- package/src/lib/installer.js +187 -0
- package/src/lib/render.js +1181 -0
- package/src/lib/security.js +114 -0
- package/src/lib/server-state.js +53 -0
- package/src/lib/service.js +105 -0
- package/src/lib/storage.js +846 -0
- package/src/mcp-server.js +495 -0
- package/src/server.js +576 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { contentTypeForFormat, normalizeFormat } from "./storage.js";
|
|
4
|
+
|
|
5
|
+
const KNOWN_AGENTS = new Set(["auto", "artifacty", "claude", "codex", "gemini", "generic"]);
|
|
6
|
+
|
|
7
|
+
export function convertAgentArtifact(input = {}) {
|
|
8
|
+
const originalAgent = normalizeAgent(input.agent || input.sourceAgent || input.source_agent || "auto");
|
|
9
|
+
const payload = input.payload ?? input.content ?? "";
|
|
10
|
+
const parsed = parsePayload(payload);
|
|
11
|
+
const decoded = decodePayload(parsed, originalAgent);
|
|
12
|
+
const sourcePath = optionalString(input.sourcePath || input.path || input.file);
|
|
13
|
+
const fileName = optionalString(input.fileName || input.filename || (sourcePath ? path.basename(sourcePath) : ""));
|
|
14
|
+
const explicitContentType = optionalString(input.contentType || input.mimeType || input.mime_type);
|
|
15
|
+
const content = String(decoded.content ?? input.content ?? payload ?? "");
|
|
16
|
+
const format = normalizeFormat(
|
|
17
|
+
input.format ||
|
|
18
|
+
decoded.format ||
|
|
19
|
+
detectFormat({ content, contentType: explicitContentType || decoded.contentType, fileName })
|
|
20
|
+
);
|
|
21
|
+
const sourceAgent = optionalString(input.sourceAgent || input.source_agent || decoded.sourceAgent) ||
|
|
22
|
+
(originalAgent === "auto" ? detectAgent(parsed, fileName) : originalAgent);
|
|
23
|
+
|
|
24
|
+
const title =
|
|
25
|
+
optionalString(input.title) ||
|
|
26
|
+
optionalString(decoded.title) ||
|
|
27
|
+
inferTitle({ content, format, fileName, sourceAgent });
|
|
28
|
+
|
|
29
|
+
const contentType = explicitContentType || decoded.contentType || contentTypeForFormat(format);
|
|
30
|
+
const artifactType = normalizeArtifactType(
|
|
31
|
+
input.artifactType ||
|
|
32
|
+
input.artifact_type ||
|
|
33
|
+
decoded.artifactType ||
|
|
34
|
+
inferArtifactType({ format, content, fileName, metadata: decoded.metadata })
|
|
35
|
+
);
|
|
36
|
+
const tags = uniqueStrings([
|
|
37
|
+
"imported",
|
|
38
|
+
sourceAgent,
|
|
39
|
+
...normalizeTags(decoded.tags),
|
|
40
|
+
...normalizeTags(input.tags)
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
title,
|
|
45
|
+
content,
|
|
46
|
+
format,
|
|
47
|
+
contentType,
|
|
48
|
+
artifactType,
|
|
49
|
+
schemaVersion: 1,
|
|
50
|
+
sourceAgent,
|
|
51
|
+
tags,
|
|
52
|
+
metadata: {
|
|
53
|
+
...normalizeMetadata(decoded.metadata),
|
|
54
|
+
...normalizeMetadata(input.metadata),
|
|
55
|
+
artifactyImport: {
|
|
56
|
+
converter: "agent-artifact-v1",
|
|
57
|
+
originalAgent,
|
|
58
|
+
sourceAgent,
|
|
59
|
+
fileName: fileName || undefined,
|
|
60
|
+
sourcePath: sourcePath || undefined,
|
|
61
|
+
contentType,
|
|
62
|
+
artifactType,
|
|
63
|
+
convertedAt: new Date().toISOString()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function detectFormat({ content = "", contentType = "", fileName = "" } = {}) {
|
|
70
|
+
const type = optionalString(contentType).toLowerCase();
|
|
71
|
+
if (type.includes("html")) {
|
|
72
|
+
return "html";
|
|
73
|
+
}
|
|
74
|
+
if (type.includes("markdown")) {
|
|
75
|
+
return "markdown";
|
|
76
|
+
}
|
|
77
|
+
if (type.includes("json")) {
|
|
78
|
+
return "json";
|
|
79
|
+
}
|
|
80
|
+
if (type.startsWith("text/")) {
|
|
81
|
+
return "text";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const extension = path.extname(fileName).toLowerCase();
|
|
85
|
+
if (extension === ".html" || extension === ".htm") {
|
|
86
|
+
return "html";
|
|
87
|
+
}
|
|
88
|
+
if (extension === ".md" || extension === ".markdown") {
|
|
89
|
+
return "markdown";
|
|
90
|
+
}
|
|
91
|
+
if (extension === ".json") {
|
|
92
|
+
return "json";
|
|
93
|
+
}
|
|
94
|
+
if (extension === ".txt" || extension === ".log") {
|
|
95
|
+
return "text";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const trimmed = optionalString(content);
|
|
99
|
+
if (/^<!doctype html/i.test(trimmed) || /^<html[\s>]/i.test(trimmed)) {
|
|
100
|
+
return "html";
|
|
101
|
+
}
|
|
102
|
+
if (looksLikeJson(trimmed)) {
|
|
103
|
+
return "json";
|
|
104
|
+
}
|
|
105
|
+
if (/^#{1,3}\s+\S/m.test(trimmed) || /^[-*]\s+\S/m.test(trimmed)) {
|
|
106
|
+
return "markdown";
|
|
107
|
+
}
|
|
108
|
+
return "text";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function decodePayload(parsed, agent) {
|
|
112
|
+
if (parsed.kind === "json" && parsed.value && typeof parsed.value === "object") {
|
|
113
|
+
return decodeObjectPayload(parsed.value, agent);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
content: parsed.text,
|
|
118
|
+
format: undefined,
|
|
119
|
+
contentType: undefined,
|
|
120
|
+
title: undefined,
|
|
121
|
+
sourceAgent: agent === "auto" ? undefined : agent,
|
|
122
|
+
tags: [],
|
|
123
|
+
metadata: {}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function decodeObjectPayload(value, agent) {
|
|
128
|
+
const artifactObject = value.artifact && typeof value.artifact === "object" ? value.artifact : value;
|
|
129
|
+
|
|
130
|
+
if (Array.isArray(artifactObject.files) || artifactObject.bundle) {
|
|
131
|
+
return decodeBundlePayload(artifactObject, agent);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (typeof artifactObject.content === "string" && !Array.isArray(artifactObject.content)) {
|
|
135
|
+
return {
|
|
136
|
+
content: artifactObject.content,
|
|
137
|
+
format: safeFormat(artifactObject.format || artifactObject.type || artifactObject.mimeType),
|
|
138
|
+
contentType: artifactObject.contentType || artifactObject.mimeType,
|
|
139
|
+
title: artifactObject.title || artifactObject.name,
|
|
140
|
+
sourceAgent: artifactObject.sourceAgent || artifactObject.source_agent || artifactObject.agent,
|
|
141
|
+
artifactType: artifactObject.artifactType || artifactObject.artifact_type,
|
|
142
|
+
tags: artifactObject.tags,
|
|
143
|
+
metadata: {
|
|
144
|
+
originalPayloadShape: "content",
|
|
145
|
+
originalId: artifactObject.id
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof artifactObject.html === "string") {
|
|
151
|
+
return objectContent(artifactObject, "html", artifactObject.html, "html");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (typeof artifactObject.markdown === "string") {
|
|
155
|
+
return objectContent(artifactObject, "markdown", artifactObject.markdown, "markdown");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (typeof artifactObject.text === "string") {
|
|
159
|
+
return objectContent(artifactObject, "text", artifactObject.text, "text");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (typeof artifactObject.returnDisplay === "string") {
|
|
163
|
+
return {
|
|
164
|
+
content: stripMarkdownCodeFence(artifactObject.returnDisplay),
|
|
165
|
+
format: detectFormat({ content: artifactObject.returnDisplay }) === "json" ? "json" : "markdown",
|
|
166
|
+
title: artifactObject.title || artifactObject.name || "Gemini artifact",
|
|
167
|
+
sourceAgent: agent === "auto" ? "gemini" : agent,
|
|
168
|
+
artifactType: "document",
|
|
169
|
+
tags: artifactObject.tags,
|
|
170
|
+
metadata: {
|
|
171
|
+
originalPayloadShape: "gemini-returnDisplay"
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (Array.isArray(artifactObject.llmContent)) {
|
|
177
|
+
const multimodal = collectMultimodalParts(artifactObject.llmContent);
|
|
178
|
+
const hasAssets = multimodal.assets.length > 0;
|
|
179
|
+
const content = hasAssets
|
|
180
|
+
? JSON.stringify(createBundleDocument({
|
|
181
|
+
title: artifactObject.title || artifactObject.name || "Gemini multimodal artifact",
|
|
182
|
+
text: multimodal.text,
|
|
183
|
+
assets: multimodal.assets,
|
|
184
|
+
parts: multimodal.parts
|
|
185
|
+
}), null, 2)
|
|
186
|
+
: multimodal.text;
|
|
187
|
+
return {
|
|
188
|
+
content,
|
|
189
|
+
format: hasAssets ? "json" : detectFormat({ content }),
|
|
190
|
+
contentType: hasAssets ? "application/vnd.artifacty.bundle+json; charset=utf-8" : undefined,
|
|
191
|
+
title: artifactObject.title || artifactObject.name,
|
|
192
|
+
sourceAgent: agent === "auto" ? "gemini" : agent,
|
|
193
|
+
artifactType: hasAssets ? "bundle" : "document",
|
|
194
|
+
tags: artifactObject.tags,
|
|
195
|
+
metadata: {
|
|
196
|
+
originalPayloadShape: "gemini-llmContent",
|
|
197
|
+
partCount: artifactObject.llmContent.length,
|
|
198
|
+
assetCount: multimodal.assets.length,
|
|
199
|
+
assetPolicy: hasAssets ? "base64-assets-stored-inline-in-bundle-json" : undefined
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (Array.isArray(artifactObject.content)) {
|
|
205
|
+
const content = collectTextParts(artifactObject.content);
|
|
206
|
+
return {
|
|
207
|
+
content,
|
|
208
|
+
format: detectFormat({ content }),
|
|
209
|
+
title: artifactObject.title || artifactObject.name,
|
|
210
|
+
sourceAgent: artifactObject.sourceAgent || artifactObject.agent,
|
|
211
|
+
artifactType: "document",
|
|
212
|
+
tags: artifactObject.tags,
|
|
213
|
+
metadata: {
|
|
214
|
+
originalPayloadShape: "content-blocks",
|
|
215
|
+
partCount: artifactObject.content.length
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
content: JSON.stringify(value, null, 2),
|
|
222
|
+
format: "json",
|
|
223
|
+
contentType: "application/json; charset=utf-8",
|
|
224
|
+
title: artifactObject.title || artifactObject.name,
|
|
225
|
+
sourceAgent: artifactObject.sourceAgent || artifactObject.agent || (agent === "auto" ? undefined : agent),
|
|
226
|
+
artifactType: "unknown",
|
|
227
|
+
tags: artifactObject.tags,
|
|
228
|
+
metadata: {
|
|
229
|
+
originalPayloadShape: "json-object"
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function objectContent(object, shape, content, format) {
|
|
235
|
+
return {
|
|
236
|
+
content,
|
|
237
|
+
format,
|
|
238
|
+
title: object.title || object.name,
|
|
239
|
+
sourceAgent: object.sourceAgent || object.source_agent || object.agent,
|
|
240
|
+
artifactType: object.artifactType || object.artifact_type,
|
|
241
|
+
tags: object.tags,
|
|
242
|
+
metadata: {
|
|
243
|
+
originalPayloadShape: shape,
|
|
244
|
+
originalId: object.id
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function decodeBundlePayload(object, agent) {
|
|
250
|
+
const bundle = object.bundle && typeof object.bundle === "object" ? object.bundle : object;
|
|
251
|
+
const files = Array.isArray(bundle.files) ? bundle.files : [];
|
|
252
|
+
const normalizedFiles = files.map((file, index) => {
|
|
253
|
+
const content = typeof file.content === "string" ? file.content : "";
|
|
254
|
+
return {
|
|
255
|
+
path: optionalString(file.path || file.name || `file-${index + 1}`),
|
|
256
|
+
content,
|
|
257
|
+
contentType: optionalString(file.contentType || file.mimeType) || contentTypeForFormat(detectFormat({
|
|
258
|
+
content,
|
|
259
|
+
fileName: file.path || file.name
|
|
260
|
+
})),
|
|
261
|
+
sizeBytes: Buffer.byteLength(content, "utf8"),
|
|
262
|
+
sha256: createHash("sha256").update(content).digest("hex")
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
content: JSON.stringify({
|
|
268
|
+
schemaVersion: 1,
|
|
269
|
+
artifactType: "bundle",
|
|
270
|
+
title: object.title || bundle.title || "Artifact bundle",
|
|
271
|
+
files: normalizedFiles
|
|
272
|
+
}, null, 2),
|
|
273
|
+
format: "json",
|
|
274
|
+
contentType: "application/vnd.artifacty.bundle+json; charset=utf-8",
|
|
275
|
+
title: object.title || bundle.title || "Artifact bundle",
|
|
276
|
+
sourceAgent: object.sourceAgent || object.source_agent || object.agent || (agent === "auto" ? undefined : agent),
|
|
277
|
+
artifactType: "bundle",
|
|
278
|
+
tags: object.tags || bundle.tags,
|
|
279
|
+
metadata: {
|
|
280
|
+
originalPayloadShape: "artifact-bundle",
|
|
281
|
+
fileCount: normalizedFiles.length,
|
|
282
|
+
bundlePolicy: "text-files-stored-inline-in-bundle-json"
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function detectAgent(parsed, fileName) {
|
|
288
|
+
if (parsed.kind === "json" && parsed.value && typeof parsed.value === "object") {
|
|
289
|
+
const value = parsed.value.artifact || parsed.value;
|
|
290
|
+
const explicit = normalizeAgent(value.agent || value.sourceAgent || value.source_agent || "auto");
|
|
291
|
+
if (explicit !== "auto") {
|
|
292
|
+
return explicit;
|
|
293
|
+
}
|
|
294
|
+
if (typeof value.returnDisplay === "string" || Array.isArray(value.llmContent)) {
|
|
295
|
+
return "gemini";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const lowerName = optionalString(fileName).toLowerCase();
|
|
300
|
+
if (lowerName.includes("claude")) {
|
|
301
|
+
return "claude";
|
|
302
|
+
}
|
|
303
|
+
if (lowerName.includes("gemini")) {
|
|
304
|
+
return "gemini";
|
|
305
|
+
}
|
|
306
|
+
if (lowerName.includes("codex")) {
|
|
307
|
+
return "codex";
|
|
308
|
+
}
|
|
309
|
+
return "generic";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function safeFormat(value) {
|
|
313
|
+
const normalized = optionalString(value).toLowerCase();
|
|
314
|
+
if (!normalized) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
if (normalized === "md") {
|
|
318
|
+
return "markdown";
|
|
319
|
+
}
|
|
320
|
+
if (normalized === "html" || normalized.includes("html")) {
|
|
321
|
+
return "html";
|
|
322
|
+
}
|
|
323
|
+
if (normalized === "markdown" || normalized.includes("markdown")) {
|
|
324
|
+
return "markdown";
|
|
325
|
+
}
|
|
326
|
+
if (normalized === "json" || normalized.includes("json")) {
|
|
327
|
+
return "json";
|
|
328
|
+
}
|
|
329
|
+
if (normalized === "text" || normalized.includes("text")) {
|
|
330
|
+
return "text";
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function inferTitle({ content, format, fileName, sourceAgent }) {
|
|
336
|
+
if (format === "html") {
|
|
337
|
+
const htmlTitle = extractHtmlTitle(content);
|
|
338
|
+
if (htmlTitle) {
|
|
339
|
+
return htmlTitle;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (format === "markdown") {
|
|
344
|
+
const markdownTitle = extractMarkdownTitle(content);
|
|
345
|
+
if (markdownTitle) {
|
|
346
|
+
return markdownTitle;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (fileName) {
|
|
351
|
+
return titleFromFileName(fileName);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return `${sourceAgent || "Imported"} artifact`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function extractHtmlTitle(content) {
|
|
358
|
+
const titleMatch = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(content);
|
|
359
|
+
if (titleMatch) {
|
|
360
|
+
return cleanTitle(titleMatch[1]);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const h1Match = /<h1[^>]*>([\s\S]*?)<\/h1>/i.exec(content);
|
|
364
|
+
if (h1Match) {
|
|
365
|
+
return cleanTitle(h1Match[1]);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return "";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function extractMarkdownTitle(content) {
|
|
372
|
+
const match = /^#\s+(.+)$/m.exec(content);
|
|
373
|
+
return match ? cleanTitle(match[1]) : "";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function titleFromFileName(fileName) {
|
|
377
|
+
const parsed = path.parse(fileName);
|
|
378
|
+
return parsed.name
|
|
379
|
+
.replace(/[-_]+/g, " ")
|
|
380
|
+
.replace(/\s+/g, " ")
|
|
381
|
+
.trim() || "Imported artifact";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function collectTextParts(parts) {
|
|
385
|
+
return parts
|
|
386
|
+
.map((part) => {
|
|
387
|
+
if (typeof part === "string") {
|
|
388
|
+
return part;
|
|
389
|
+
}
|
|
390
|
+
if (!part || typeof part !== "object") {
|
|
391
|
+
return "";
|
|
392
|
+
}
|
|
393
|
+
if (typeof part.text === "string") {
|
|
394
|
+
return part.text;
|
|
395
|
+
}
|
|
396
|
+
if (typeof part.content === "string") {
|
|
397
|
+
return part.content;
|
|
398
|
+
}
|
|
399
|
+
if (part.type === "text" && typeof part.value === "string") {
|
|
400
|
+
return part.value;
|
|
401
|
+
}
|
|
402
|
+
return "";
|
|
403
|
+
})
|
|
404
|
+
.filter(Boolean)
|
|
405
|
+
.join("\n\n");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function collectMultimodalParts(parts) {
|
|
409
|
+
const collected = {
|
|
410
|
+
text: "",
|
|
411
|
+
parts: [],
|
|
412
|
+
assets: []
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
for (const [index, part] of parts.entries()) {
|
|
416
|
+
if (typeof part === "string") {
|
|
417
|
+
collected.parts.push({ type: "text", text: part });
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (!part || typeof part !== "object") {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const text = typeof part.text === "string"
|
|
424
|
+
? part.text
|
|
425
|
+
: typeof part.content === "string"
|
|
426
|
+
? part.content
|
|
427
|
+
: part.type === "text" && typeof part.value === "string"
|
|
428
|
+
? part.value
|
|
429
|
+
: "";
|
|
430
|
+
if (text) {
|
|
431
|
+
collected.parts.push({ type: "text", text });
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const inline = part.inlineData || part.inline_data || part.data;
|
|
435
|
+
if (inline && typeof inline === "object") {
|
|
436
|
+
const data = optionalString(inline.data || inline.base64 || inline.content);
|
|
437
|
+
const mimeType = optionalString(inline.mimeType || inline.mime_type || part.mimeType || "application/octet-stream");
|
|
438
|
+
if (data) {
|
|
439
|
+
const asset = {
|
|
440
|
+
id: `asset-${collected.assets.length + 1}`,
|
|
441
|
+
sourcePartIndex: index,
|
|
442
|
+
mimeType,
|
|
443
|
+
encoding: "base64",
|
|
444
|
+
data,
|
|
445
|
+
sizeBytes: Buffer.byteLength(data, "base64"),
|
|
446
|
+
sha256: createHash("sha256").update(Buffer.from(data, "base64")).digest("hex")
|
|
447
|
+
};
|
|
448
|
+
collected.assets.push(asset);
|
|
449
|
+
collected.parts.push({ type: "asset-ref", assetId: asset.id, mimeType });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
collected.text = collected.parts
|
|
455
|
+
.filter((part) => part.type === "text")
|
|
456
|
+
.map((part) => part.text)
|
|
457
|
+
.join("\n\n");
|
|
458
|
+
return collected;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function createBundleDocument({ title, text, assets, parts }) {
|
|
462
|
+
return {
|
|
463
|
+
schemaVersion: 1,
|
|
464
|
+
artifactType: "bundle",
|
|
465
|
+
title,
|
|
466
|
+
text,
|
|
467
|
+
parts,
|
|
468
|
+
assets,
|
|
469
|
+
assetPolicy: "base64-assets-stored-inline; consumers must treat decoded assets as untrusted"
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function inferArtifactType({ format, fileName, metadata }) {
|
|
474
|
+
if (metadata?.originalPayloadShape === "artifact-bundle") {
|
|
475
|
+
return "bundle";
|
|
476
|
+
}
|
|
477
|
+
if (format === "html") {
|
|
478
|
+
return "html-page";
|
|
479
|
+
}
|
|
480
|
+
const lowerName = optionalString(fileName).toLowerCase();
|
|
481
|
+
if (lowerName.includes("handoff")) {
|
|
482
|
+
return "handoff";
|
|
483
|
+
}
|
|
484
|
+
if (lowerName.includes("review")) {
|
|
485
|
+
return "code-review";
|
|
486
|
+
}
|
|
487
|
+
if (lowerName.includes("test") || lowerName.includes("report")) {
|
|
488
|
+
return "test-report";
|
|
489
|
+
}
|
|
490
|
+
return "document";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function normalizeArtifactType(value) {
|
|
494
|
+
const normalized = optionalString(value).toLowerCase();
|
|
495
|
+
if (!normalized) {
|
|
496
|
+
return "document";
|
|
497
|
+
}
|
|
498
|
+
const allowed = new Set(["document", "html-page", "handoff", "code-review", "test-report", "dashboard", "design-option", "diff-walkthrough", "bundle", "asset", "unknown"]);
|
|
499
|
+
return allowed.has(normalized) ? normalized : "unknown";
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function parsePayload(payload) {
|
|
503
|
+
if (payload && typeof payload === "object") {
|
|
504
|
+
return {
|
|
505
|
+
kind: "json",
|
|
506
|
+
value: payload,
|
|
507
|
+
text: JSON.stringify(payload, null, 2)
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const text = String(payload ?? "");
|
|
512
|
+
const trimmed = text.trim();
|
|
513
|
+
if (looksLikeJson(trimmed)) {
|
|
514
|
+
try {
|
|
515
|
+
return {
|
|
516
|
+
kind: "json",
|
|
517
|
+
value: JSON.parse(trimmed),
|
|
518
|
+
text
|
|
519
|
+
};
|
|
520
|
+
} catch {
|
|
521
|
+
return { kind: "text", text };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return { kind: "text", text };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function looksLikeJson(value) {
|
|
528
|
+
const trimmed = optionalString(value);
|
|
529
|
+
return (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
530
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function stripMarkdownCodeFence(value) {
|
|
534
|
+
const trimmed = value.trim();
|
|
535
|
+
const match = /^```(?:\w+)?\n([\s\S]*?)\n```$/.exec(trimmed);
|
|
536
|
+
return match ? match[1] : value;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function cleanTitle(value) {
|
|
540
|
+
return optionalString(value.replace(/<[^>]*>/g, "").replace(/ /g, " "));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function normalizeAgent(value) {
|
|
544
|
+
const normalized = optionalString(value).toLowerCase();
|
|
545
|
+
if (!normalized) {
|
|
546
|
+
return "auto";
|
|
547
|
+
}
|
|
548
|
+
if (normalized === "claude-code" || normalized === "anthropic") {
|
|
549
|
+
return "claude";
|
|
550
|
+
}
|
|
551
|
+
if (normalized === "gemini-cli" || normalized === "google") {
|
|
552
|
+
return "gemini";
|
|
553
|
+
}
|
|
554
|
+
if (normalized === "openai" || normalized === "chatgpt") {
|
|
555
|
+
return "codex";
|
|
556
|
+
}
|
|
557
|
+
if (KNOWN_AGENTS.has(normalized)) {
|
|
558
|
+
return normalized;
|
|
559
|
+
}
|
|
560
|
+
return normalized.replace(/[^a-z0-9-]+/g, "-") || "generic";
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function normalizeTags(tags) {
|
|
564
|
+
if (!Array.isArray(tags)) {
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
return tags.map(optionalString).filter(Boolean);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function uniqueStrings(values) {
|
|
571
|
+
return [...new Set(values.map(optionalString).filter(Boolean))].slice(0, 20);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function normalizeMetadata(metadata) {
|
|
575
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
576
|
+
return {};
|
|
577
|
+
}
|
|
578
|
+
return metadata;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function optionalString(value) {
|
|
582
|
+
if (value === undefined || value === null) {
|
|
583
|
+
return "";
|
|
584
|
+
}
|
|
585
|
+
return String(value).trim();
|
|
586
|
+
}
|
package/src/lib/diff.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function createLineDiff(before, after, options = {}) {
|
|
2
|
+
const beforeLines = splitLines(before);
|
|
3
|
+
const afterLines = splitLines(after);
|
|
4
|
+
const maxCells = options.maxCells || 250000;
|
|
5
|
+
|
|
6
|
+
if (beforeLines.length * afterLines.length > maxCells) {
|
|
7
|
+
return [
|
|
8
|
+
...beforeLines.map((text, index) => ({
|
|
9
|
+
type: "removed",
|
|
10
|
+
beforeLine: index + 1,
|
|
11
|
+
afterLine: "",
|
|
12
|
+
text
|
|
13
|
+
})),
|
|
14
|
+
...afterLines.map((text, index) => ({
|
|
15
|
+
type: "added",
|
|
16
|
+
beforeLine: "",
|
|
17
|
+
afterLine: index + 1,
|
|
18
|
+
text
|
|
19
|
+
}))
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const width = afterLines.length + 1;
|
|
24
|
+
const table = new Uint32Array((beforeLines.length + 1) * width);
|
|
25
|
+
|
|
26
|
+
for (let i = beforeLines.length - 1; i >= 0; i -= 1) {
|
|
27
|
+
for (let j = afterLines.length - 1; j >= 0; j -= 1) {
|
|
28
|
+
const offset = i * width + j;
|
|
29
|
+
if (beforeLines[i] === afterLines[j]) {
|
|
30
|
+
table[offset] = table[(i + 1) * width + j + 1] + 1;
|
|
31
|
+
} else {
|
|
32
|
+
table[offset] = Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const rows = [];
|
|
38
|
+
let i = 0;
|
|
39
|
+
let j = 0;
|
|
40
|
+
while (i < beforeLines.length && j < afterLines.length) {
|
|
41
|
+
if (beforeLines[i] === afterLines[j]) {
|
|
42
|
+
rows.push({ type: "same", beforeLine: i + 1, afterLine: j + 1, text: beforeLines[i] });
|
|
43
|
+
i += 1;
|
|
44
|
+
j += 1;
|
|
45
|
+
} else if (table[(i + 1) * width + j] >= table[i * width + j + 1]) {
|
|
46
|
+
rows.push({ type: "removed", beforeLine: i + 1, afterLine: "", text: beforeLines[i] });
|
|
47
|
+
i += 1;
|
|
48
|
+
} else {
|
|
49
|
+
rows.push({ type: "added", beforeLine: "", afterLine: j + 1, text: afterLines[j] });
|
|
50
|
+
j += 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
while (i < beforeLines.length) {
|
|
55
|
+
rows.push({ type: "removed", beforeLine: i + 1, afterLine: "", text: beforeLines[i] });
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
while (j < afterLines.length) {
|
|
60
|
+
rows.push({ type: "added", beforeLine: "", afterLine: j + 1, text: afterLines[j] });
|
|
61
|
+
j += 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return rows;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function splitLines(value) {
|
|
68
|
+
return String(value).split(/\r?\n/);
|
|
69
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export const EDITOR_CLIENT_PATH = "/assets/editor.js";
|
|
4
|
+
|
|
5
|
+
export const EDITOR_VENDOR_PACKAGES = [
|
|
6
|
+
"@codemirror/autocomplete",
|
|
7
|
+
"@codemirror/commands",
|
|
8
|
+
"@codemirror/lang-css",
|
|
9
|
+
"@codemirror/lang-html",
|
|
10
|
+
"@codemirror/lang-javascript",
|
|
11
|
+
"@codemirror/lang-json",
|
|
12
|
+
"@codemirror/lang-markdown",
|
|
13
|
+
"@codemirror/language",
|
|
14
|
+
"@codemirror/lint",
|
|
15
|
+
"@codemirror/search",
|
|
16
|
+
"@codemirror/state",
|
|
17
|
+
"@codemirror/view",
|
|
18
|
+
"@lezer/common",
|
|
19
|
+
"@lezer/css",
|
|
20
|
+
"@lezer/highlight",
|
|
21
|
+
"@lezer/html",
|
|
22
|
+
"@lezer/javascript",
|
|
23
|
+
"@lezer/json",
|
|
24
|
+
"@lezer/lr",
|
|
25
|
+
"@lezer/markdown",
|
|
26
|
+
"@marijn/find-cluster-break",
|
|
27
|
+
"codemirror",
|
|
28
|
+
"crelt",
|
|
29
|
+
"style-mod",
|
|
30
|
+
"w3c-keyname"
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const EDITOR_VENDOR_SET = new Set(EDITOR_VENDOR_PACKAGES);
|
|
34
|
+
const EDITOR_VENDOR_ENTRY_OVERRIDES = {
|
|
35
|
+
"@marijn/find-cluster-break": path.join("src", "index.js"),
|
|
36
|
+
crelt: "index.js",
|
|
37
|
+
"style-mod": path.join("src", "style-mod.js"),
|
|
38
|
+
"w3c-keyname": "index.js"
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function editorImportMapJson() {
|
|
42
|
+
return JSON.stringify({
|
|
43
|
+
imports: Object.fromEntries(
|
|
44
|
+
EDITOR_VENDOR_PACKAGES.map((packageName) => [packageName, `/vendor/npm/${packageName}`])
|
|
45
|
+
)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function editorVendorPath(packageName, packageRoot) {
|
|
50
|
+
if (!EDITOR_VENDOR_SET.has(packageName)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const entry = EDITOR_VENDOR_ENTRY_OVERRIDES[packageName] || path.join("dist", "index.js");
|
|
54
|
+
return path.join(packageRoot, "node_modules", packageName, entry);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function editorClientFilePath(packageRoot) {
|
|
58
|
+
return path.join(packageRoot, "src", "client", "editor.js");
|
|
59
|
+
}
|