decant-core 1.11.0 → 1.13.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/LICENSE +16 -0
- package/README.md +31 -25
- package/ai/chatgpt.js +269 -13
- package/ai/chatgpt_helper.js +21 -7
- package/ai/claude.js +232 -6
- package/ai/deepseek.js +79 -4
- package/ai/gemini.js +311 -12
- package/ai/z_ai.js +4 -5
- package/data/platforms.json +162 -0
- package/package.json +7 -3
package/ai/claude.js
CHANGED
|
@@ -103,6 +103,124 @@ const MIME_TO_LANG = {
|
|
|
103
103
|
"application/vnd.ant.code": "text",
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
// Binary archives cannot be inlined as text in any export format; they are
|
|
107
|
+
// listed by name so they at least appear in markdown/html/json exports.
|
|
108
|
+
const BINARY_ARCHIVE_MIMES = new Set([
|
|
109
|
+
"application/x-tar",
|
|
110
|
+
"application/gzip",
|
|
111
|
+
"application/zip",
|
|
112
|
+
"application/x-7z-compressed",
|
|
113
|
+
"application/x-rar-compressed",
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
function formatFileSize(bytes) {
|
|
117
|
+
if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0) {
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
121
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function basenameOfPath(filePath) {
|
|
125
|
+
if (typeof filePath !== "string" || !filePath) return "file";
|
|
126
|
+
const base = filePath.split("/").pop();
|
|
127
|
+
return base || "file";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isBinaryArchive(mimeType, filePath) {
|
|
131
|
+
if (mimeType && BINARY_ARCHIVE_MIMES.has(mimeType)) return true;
|
|
132
|
+
return /\.(tar\.gz|tgz|tar|zip|gz|7z|rar)$/i.test(filePath || "");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Collect files surfaced via the `present_files` tool (File Creation
|
|
136
|
+
// integration). The tool_use block carries `input.filepaths`; the matching
|
|
137
|
+
// tool_result block (joined via tool_use_id) carries display names + mime
|
|
138
|
+
// types as `local_resource` entries. Either side may be missing, so resolve
|
|
139
|
+
// metadata when available and fall back to bare paths otherwise.
|
|
140
|
+
function collectPresentedFiles(branch) {
|
|
141
|
+
const pathsByToolUseId = new Map();
|
|
142
|
+
const resourcesByToolUseId = new Map();
|
|
143
|
+
for (const msg of branch) {
|
|
144
|
+
if (!Array.isArray(msg?.content)) continue;
|
|
145
|
+
for (const block of msg.content) {
|
|
146
|
+
if (block?.type === "tool_use" && block?.name === "present_files") {
|
|
147
|
+
const filepaths = block?.input?.filepaths;
|
|
148
|
+
if (Array.isArray(filepaths) && filepaths.length > 0 && block.id) {
|
|
149
|
+
pathsByToolUseId.set(block.id, filepaths);
|
|
150
|
+
}
|
|
151
|
+
} else if (
|
|
152
|
+
block?.type === "tool_result" &&
|
|
153
|
+
Array.isArray(block.content)
|
|
154
|
+
) {
|
|
155
|
+
const resources = block.content.filter(
|
|
156
|
+
(item) => item && item.type === "local_resource" && item.file_path,
|
|
157
|
+
);
|
|
158
|
+
if (resources.length > 0) {
|
|
159
|
+
const toolUseId = block.tool_use_id || block.id;
|
|
160
|
+
if (toolUseId) resourcesByToolUseId.set(toolUseId, resources);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const filesByToolUseId = new Map();
|
|
167
|
+
const seenPaths = new Set();
|
|
168
|
+
const toEntry = (resource, fallbackPath) => {
|
|
169
|
+
const filePath = resource?.file_path || fallbackPath || "";
|
|
170
|
+
const name = resource?.name || basenameOfPath(filePath);
|
|
171
|
+
return {
|
|
172
|
+
key: resource?.uuid || filePath || `${name}`,
|
|
173
|
+
name,
|
|
174
|
+
path: filePath,
|
|
175
|
+
mime: resource?.mime_type || "",
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
for (const [toolUseId, resources] of resourcesByToolUseId.entries()) {
|
|
180
|
+
const entries = [];
|
|
181
|
+
for (const resource of resources) {
|
|
182
|
+
// Skip the inline "say they are below" helper text item (type: text).
|
|
183
|
+
if (!resource.file_path) continue;
|
|
184
|
+
if (seenPaths.has(resource.file_path)) continue;
|
|
185
|
+
seenPaths.add(resource.file_path);
|
|
186
|
+
entries.push(toEntry(resource));
|
|
187
|
+
}
|
|
188
|
+
if (entries.length > 0) filesByToolUseId.set(toolUseId, entries);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const [toolUseId, filepaths] of pathsByToolUseId.entries()) {
|
|
192
|
+
const existing = filesByToolUseId.get(toolUseId) || [];
|
|
193
|
+
const entries = [...existing];
|
|
194
|
+
for (const filePath of filepaths) {
|
|
195
|
+
if (typeof filePath !== "string" || !filePath) continue;
|
|
196
|
+
if (seenPaths.has(filePath)) continue;
|
|
197
|
+
seenPaths.add(filePath);
|
|
198
|
+
entries.push(toEntry(null, filePath));
|
|
199
|
+
}
|
|
200
|
+
if (entries.length > 0) filesByToolUseId.set(toolUseId, entries);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return filesByToolUseId;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function formatGeneratedFilesSection(files) {
|
|
207
|
+
if (!Array.isArray(files) || files.length === 0) return "";
|
|
208
|
+
const lines = files.map((file) => {
|
|
209
|
+
const displayName = file.name || basenameOfPath(file.path);
|
|
210
|
+
const meta = [];
|
|
211
|
+
if (file.mime) meta.push(file.mime);
|
|
212
|
+
if (isBinaryArchive(file.mime, file.path || displayName)) {
|
|
213
|
+
meta.push("binary archive — download from Claude UI");
|
|
214
|
+
}
|
|
215
|
+
const suffix = meta.length > 0 ? ` _(${meta.join(", ")})_` : "";
|
|
216
|
+
const pathSuffix =
|
|
217
|
+
file.path && file.path !== displayName ? ` — \`${file.path}\`` : "";
|
|
218
|
+
return `- \`${displayName}\`${suffix}${pathSuffix}`;
|
|
219
|
+
});
|
|
220
|
+
const label = files.length === 1 ? "Generated file:" : "Generated files:";
|
|
221
|
+
return `\n\n**${label}**\n${lines.join("\n")}\n\n`;
|
|
222
|
+
}
|
|
223
|
+
|
|
106
224
|
function extractArtifactsFromText(text) {
|
|
107
225
|
const artifactRegex = /<antArtifact[^>]*>([\s\S]*?)<\/antArtifact>/g;
|
|
108
226
|
const artifacts = [];
|
|
@@ -267,6 +385,44 @@ function extractArtifacts(message, foldedArtifacts = new Map()) {
|
|
|
267
385
|
return artifacts;
|
|
268
386
|
}
|
|
269
387
|
|
|
388
|
+
// File cards rendered for generated files (markdown docs, tarballs, …).
|
|
389
|
+
// They carry no text content for convertToMarkdown, so extract their display
|
|
390
|
+
// names (aria-label="View <name>") and drop the nodes to avoid button noise.
|
|
391
|
+
function extractFileCards(root) {
|
|
392
|
+
if (!root || typeof root.querySelectorAll !== "function") return [];
|
|
393
|
+
const names = [];
|
|
394
|
+
const cards = root.querySelectorAll('[data-testid="file-card-open"]');
|
|
395
|
+
for (const card of cards) {
|
|
396
|
+
const label = card.getAttribute && card.getAttribute("aria-label");
|
|
397
|
+
const match = typeof label === "string" && label.match(/^View\s+(.+)$/i);
|
|
398
|
+
const name = match ? match[1].trim() : "";
|
|
399
|
+
// No name-based dedup: two cards may legitimately share a display name
|
|
400
|
+
// (same basename in different directories). Each card element is visited
|
|
401
|
+
// exactly once, so nothing is double-counted here.
|
|
402
|
+
if (name) {
|
|
403
|
+
names.push(name);
|
|
404
|
+
}
|
|
405
|
+
// Remove the whole card element so download buttons / type badges
|
|
406
|
+
// don't leak into the markdown conversion — but only when the parent
|
|
407
|
+
// is a dedicated card wrapper. If the button shares its parent with
|
|
408
|
+
// other prose, remove just the button to avoid deleting message text.
|
|
409
|
+
const parent =
|
|
410
|
+
card.parentElement && card.parentElement !== root
|
|
411
|
+
? card.parentElement
|
|
412
|
+
: null;
|
|
413
|
+
const isDedicatedCard =
|
|
414
|
+
!!parent &&
|
|
415
|
+
parent.querySelectorAll('[data-testid="file-card-open"]').length === 1;
|
|
416
|
+
const target = isDedicatedCard ? parent : card;
|
|
417
|
+
if (target && typeof target.remove === "function") {
|
|
418
|
+
target.remove();
|
|
419
|
+
} else if (card.parentNode) {
|
|
420
|
+
card.parentNode.removeChild(card);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return names;
|
|
424
|
+
}
|
|
425
|
+
|
|
270
426
|
function unrollInteractiveElements(root, doc) {
|
|
271
427
|
if (!root || !doc) return;
|
|
272
428
|
|
|
@@ -371,6 +527,8 @@ export class ClaudeParser extends ChatParser {
|
|
|
371
527
|
|
|
372
528
|
const branch = getCurrentBranch(data);
|
|
373
529
|
const foldedArtifacts = collectArtifacts(branch);
|
|
530
|
+
const presentedFilesByToolUseId = collectPresentedFiles(branch);
|
|
531
|
+
const emittedFileKeys = new Set();
|
|
374
532
|
|
|
375
533
|
const toolResultMap = new Map();
|
|
376
534
|
for (const msg of branch) {
|
|
@@ -402,12 +560,30 @@ export class ClaudeParser extends ChatParser {
|
|
|
402
560
|
const role = message.sender === "human" ? "User" : "Claude";
|
|
403
561
|
|
|
404
562
|
let contentStr = "";
|
|
563
|
+
let thinkingStr = "";
|
|
405
564
|
|
|
406
565
|
// Construct content
|
|
407
566
|
if (message.content && Array.isArray(message.content)) {
|
|
408
567
|
for (const block of message.content) {
|
|
409
|
-
if (block.type === "thinking"
|
|
410
|
-
|
|
568
|
+
if (block.type === "thinking") {
|
|
569
|
+
let thoughtText = "";
|
|
570
|
+
if (
|
|
571
|
+
typeof block.thinking === "string" &&
|
|
572
|
+
block.thinking.trim()
|
|
573
|
+
) {
|
|
574
|
+
thoughtText = block.thinking.trim();
|
|
575
|
+
} else if (Array.isArray(block.summaries)) {
|
|
576
|
+
thoughtText = block.summaries
|
|
577
|
+
.map((s) =>
|
|
578
|
+
typeof s === "string" ? s : s?.summary || "",
|
|
579
|
+
)
|
|
580
|
+
.map((s) => s.trim())
|
|
581
|
+
.filter(Boolean)
|
|
582
|
+
.join("\n");
|
|
583
|
+
}
|
|
584
|
+
if (thoughtText) {
|
|
585
|
+
thinkingStr += (thinkingStr ? "\n\n" : "") + thoughtText;
|
|
586
|
+
}
|
|
411
587
|
} else if (block.type === "text" && block.text) {
|
|
412
588
|
const cleanText = block.text
|
|
413
589
|
.replace(/<antArtifact[^>]*>[\s\S]*?<\/antArtifact>/g, "")
|
|
@@ -455,6 +631,37 @@ export class ClaudeParser extends ChatParser {
|
|
|
455
631
|
: `> - ${qText}\n`;
|
|
456
632
|
}
|
|
457
633
|
contentStr += `${qStr}\n`;
|
|
634
|
+
} else if (
|
|
635
|
+
block.name === "present_files" &&
|
|
636
|
+
presentedFilesByToolUseId.has(block.id)
|
|
637
|
+
) {
|
|
638
|
+
// Files surfaced via the File Creation integration
|
|
639
|
+
// (markdown docs, tarball, …). Without this they are
|
|
640
|
+
// silently dropped from every export format.
|
|
641
|
+
const files = presentedFilesByToolUseId.get(block.id);
|
|
642
|
+
const fresh = files.filter(
|
|
643
|
+
(file) => !emittedFileKeys.has(file.key),
|
|
644
|
+
);
|
|
645
|
+
fresh.forEach((file) => emittedFileKeys.add(file.key));
|
|
646
|
+
if (fresh.length > 0) {
|
|
647
|
+
contentStr += formatGeneratedFilesSection(fresh);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
} else if (
|
|
651
|
+
block.type === "tool_result" &&
|
|
652
|
+
Array.isArray(block.content)
|
|
653
|
+
) {
|
|
654
|
+
// Orphan file presentation: the matching tool_use block may
|
|
655
|
+
// sit outside the current branch, so emit unseen
|
|
656
|
+
// local_resource entries directly from the result.
|
|
657
|
+
const toolUseId = block.tool_use_id || block.id;
|
|
658
|
+
const files = presentedFilesByToolUseId.get(toolUseId) || [];
|
|
659
|
+
const fresh = files.filter(
|
|
660
|
+
(file) => !emittedFileKeys.has(file.key),
|
|
661
|
+
);
|
|
662
|
+
fresh.forEach((file) => emittedFileKeys.add(file.key));
|
|
663
|
+
if (fresh.length > 0) {
|
|
664
|
+
contentStr += formatGeneratedFilesSection(fresh);
|
|
458
665
|
}
|
|
459
666
|
}
|
|
460
667
|
}
|
|
@@ -473,8 +680,9 @@ export class ClaudeParser extends ChatParser {
|
|
|
473
680
|
if (attachment.file_name) {
|
|
474
681
|
let header = `### Attachment: ${attachment.file_name}`;
|
|
475
682
|
const meta = [];
|
|
476
|
-
|
|
477
|
-
|
|
683
|
+
const sizeLabel = formatFileSize(attachment.file_size);
|
|
684
|
+
if (sizeLabel) {
|
|
685
|
+
meta.push(sizeLabel);
|
|
478
686
|
}
|
|
479
687
|
if (attachment.file_type) {
|
|
480
688
|
meta.push(attachment.file_type);
|
|
@@ -487,7 +695,9 @@ export class ClaudeParser extends ChatParser {
|
|
|
487
695
|
contentStr += `\`\`\`\`\n${attachment.extracted_content}\n\`\`\`\`\n\n`;
|
|
488
696
|
}
|
|
489
697
|
} else if (attachment.extracted_content) {
|
|
490
|
-
|
|
698
|
+
const sizeLabel = formatFileSize(attachment.file_size);
|
|
699
|
+
const sizeSuffix = sizeLabel ? ` _(${sizeLabel})_` : "";
|
|
700
|
+
contentStr += `\n\n### Pasted content${sizeSuffix}\n\`\`\`\`\n${attachment.extracted_content}\n\`\`\`\`\n\n`;
|
|
491
701
|
}
|
|
492
702
|
}
|
|
493
703
|
}
|
|
@@ -516,9 +726,17 @@ export class ClaudeParser extends ChatParser {
|
|
|
516
726
|
}
|
|
517
727
|
}
|
|
518
728
|
|
|
729
|
+
if (thinkingStr) {
|
|
730
|
+
contentStr = `<think>\n${thinkingStr}\n</think>\n\n` + contentStr;
|
|
731
|
+
}
|
|
732
|
+
|
|
519
733
|
contentStr = contentStr.trim();
|
|
520
734
|
if (contentStr) {
|
|
521
|
-
|
|
735
|
+
const msgObj = { role, content: contentStr };
|
|
736
|
+
if (thinkingStr) {
|
|
737
|
+
msgObj.thinking = thinkingStr;
|
|
738
|
+
}
|
|
739
|
+
messages.push(msgObj);
|
|
522
740
|
}
|
|
523
741
|
|
|
524
742
|
// Extract and push artifacts
|
|
@@ -652,8 +870,16 @@ export class ClaudeParser extends ChatParser {
|
|
|
652
870
|
) {
|
|
653
871
|
role = "Claude";
|
|
654
872
|
const clone = el.cloneNode(true);
|
|
873
|
+
// Extract file cards first: unrollInteractiveElements strips all
|
|
874
|
+
// buttons, which would destroy the card markers.
|
|
875
|
+
const fileCardNames = extractFileCards(clone);
|
|
655
876
|
unrollInteractiveElements(clone, el.ownerDocument || document);
|
|
656
877
|
content = convertToMarkdown(clone);
|
|
878
|
+
if (fileCardNames.length > 0) {
|
|
879
|
+
content += formatGeneratedFilesSection(
|
|
880
|
+
fileCardNames.map((name) => ({ name, path: "", mime: "" })),
|
|
881
|
+
);
|
|
882
|
+
}
|
|
657
883
|
} else if (el.matches(".artifact-block-cell")) {
|
|
658
884
|
role = "Claude Artifact";
|
|
659
885
|
|
package/ai/deepseek.js
CHANGED
|
@@ -98,7 +98,29 @@ async function fetchDeepSeekConversation(sessionId, token) {
|
|
|
98
98
|
const isUser = msgNode.role === "USER" || msgNode.role === "user";
|
|
99
99
|
const role = isUser ? "User" : "DeepSeek";
|
|
100
100
|
const content = extractDeepSeekMessageContent(msgNode);
|
|
101
|
-
|
|
101
|
+
let thinking = "";
|
|
102
|
+
if (!isUser && Array.isArray(msgNode.fragments)) {
|
|
103
|
+
const thinkFragments = msgNode.fragments.filter(
|
|
104
|
+
(f) => f && f.type === "THINK" && typeof f.content === "string",
|
|
105
|
+
);
|
|
106
|
+
thinking = thinkFragments
|
|
107
|
+
.map((f) => f.content.trim())
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join("\n\n");
|
|
110
|
+
}
|
|
111
|
+
let fullContent = "";
|
|
112
|
+
if (thinking) {
|
|
113
|
+
fullContent += `<think>\n${thinking}\n</think>\n\n`;
|
|
114
|
+
}
|
|
115
|
+
if (content.trim()) {
|
|
116
|
+
fullContent += content.trim();
|
|
117
|
+
}
|
|
118
|
+
fullContent = fullContent.trim();
|
|
119
|
+
const msg = { role, content: fullContent };
|
|
120
|
+
if (thinking) {
|
|
121
|
+
msg.thinking = thinking;
|
|
122
|
+
}
|
|
123
|
+
return msg;
|
|
102
124
|
})
|
|
103
125
|
.filter((msg) => msg.content.length > 0);
|
|
104
126
|
}
|
|
@@ -182,15 +204,40 @@ export class DeepSeekParser extends ChatParser {
|
|
|
182
204
|
|
|
183
205
|
outerElements.forEach((el) => {
|
|
184
206
|
let role = "Unknown";
|
|
207
|
+
let thinking = "";
|
|
185
208
|
if (el.matches(userSelector)) {
|
|
186
209
|
role = "User";
|
|
187
210
|
} else if (el.matches(assistantSelector)) {
|
|
188
211
|
role = "DeepSeek";
|
|
212
|
+
const messageContainer =
|
|
213
|
+
(el.closest && el.closest(".ds-message")) || el.parentElement;
|
|
214
|
+
if (messageContainer) {
|
|
215
|
+
const thinkContainers =
|
|
216
|
+
messageContainer.querySelectorAll(".ds-think-content");
|
|
217
|
+
if (thinkContainers.length > 0) {
|
|
218
|
+
thinking = Array.from(thinkContainers)
|
|
219
|
+
.map((tc) => convertToMarkdown(tc).trim())
|
|
220
|
+
.filter(Boolean)
|
|
221
|
+
.join("\n\n");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
189
224
|
}
|
|
190
225
|
|
|
191
226
|
const text = convertToMarkdown(el);
|
|
227
|
+
let fullContent = "";
|
|
228
|
+
if (thinking) {
|
|
229
|
+
fullContent += `<think>\n${thinking}\n</think>\n\n`;
|
|
230
|
+
}
|
|
192
231
|
if (text.trim()) {
|
|
193
|
-
|
|
232
|
+
fullContent += text.trim();
|
|
233
|
+
}
|
|
234
|
+
fullContent = fullContent.trim();
|
|
235
|
+
if (fullContent) {
|
|
236
|
+
const msg = { role, content: fullContent };
|
|
237
|
+
if (thinking) {
|
|
238
|
+
msg.thinking = thinking;
|
|
239
|
+
}
|
|
240
|
+
messages.push(msg);
|
|
194
241
|
}
|
|
195
242
|
});
|
|
196
243
|
|
|
@@ -202,9 +249,37 @@ export class DeepSeekParser extends ChatParser {
|
|
|
202
249
|
messageRows.forEach((row) => {
|
|
203
250
|
const isUser = row.classList.contains("ds-user-message");
|
|
204
251
|
const role = isUser ? "User" : "DeepSeek";
|
|
205
|
-
const
|
|
252
|
+
const rowClone = row.cloneNode(true);
|
|
253
|
+
let thinking = "";
|
|
254
|
+
if (!isUser) {
|
|
255
|
+
const thinkContainers =
|
|
256
|
+
rowClone.querySelectorAll(".ds-think-content");
|
|
257
|
+
if (thinkContainers.length > 0) {
|
|
258
|
+
thinking = Array.from(thinkContainers)
|
|
259
|
+
.map((tc) => {
|
|
260
|
+
const md = convertToMarkdown(tc).trim();
|
|
261
|
+
tc.remove();
|
|
262
|
+
return md;
|
|
263
|
+
})
|
|
264
|
+
.filter(Boolean)
|
|
265
|
+
.join("\n\n");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const text = convertToMarkdown(rowClone);
|
|
269
|
+
let fullContent = "";
|
|
270
|
+
if (thinking) {
|
|
271
|
+
fullContent += `<think>\n${thinking}\n</think>\n\n`;
|
|
272
|
+
}
|
|
206
273
|
if (text.trim()) {
|
|
207
|
-
|
|
274
|
+
fullContent += text.trim();
|
|
275
|
+
}
|
|
276
|
+
fullContent = fullContent.trim();
|
|
277
|
+
if (fullContent) {
|
|
278
|
+
const msg = { role, content: fullContent };
|
|
279
|
+
if (thinking) {
|
|
280
|
+
msg.thinking = thinking;
|
|
281
|
+
}
|
|
282
|
+
messages.push(msg);
|
|
208
283
|
}
|
|
209
284
|
});
|
|
210
285
|
}
|