decant-core 1.0.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/ai/claude.js ADDED
@@ -0,0 +1,410 @@
1
+ import { ChatParser } from './base.js';
2
+ import { convertToMarkdown } from '../utils/html-to-markdown.js';
3
+
4
+ async function getOrganizationId() {
5
+ try {
6
+ const response = await fetch('https://claude.ai/api/organizations', {
7
+ credentials: 'include',
8
+ headers: {
9
+ Accept: 'application/json',
10
+ },
11
+ });
12
+ if (!response.ok) return null;
13
+ const orgs = await response.json();
14
+ if (Array.isArray(orgs) && orgs.length > 0) {
15
+ const chatOrg = orgs.find((org) => org.capabilities && org.capabilities.includes('chat'));
16
+ return chatOrg ? chatOrg.uuid : orgs[0].uuid;
17
+ }
18
+ } catch (e) {
19
+ console.error('[AI Exporter] Failed to detect org ID:', e);
20
+ }
21
+ return null;
22
+ }
23
+
24
+ function getConversationId() {
25
+ try {
26
+ if (typeof window === 'undefined' || !window.location) return null;
27
+ return window.location.pathname.match(/\/chat\/([^/?#]+)/)?.[1] ?? null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ async function fetchConversation(orgId, conversationId) {
34
+ const url = `https://claude.ai/api/organizations/${orgId}/chat_conversations/${conversationId}?tree=True&rendering_mode=messages&render_all_tools=true`;
35
+ const response = await fetch(url, {
36
+ credentials: 'include',
37
+ headers: {
38
+ Accept: 'application/json',
39
+ },
40
+ });
41
+ if (!response.ok) {
42
+ throw new Error(`Failed to fetch Claude conversation: ${response.status}`);
43
+ }
44
+ return response.json();
45
+ }
46
+
47
+ function getCurrentBranch(data) {
48
+ if (!data.chat_messages || !data.current_leaf_message_uuid) {
49
+ return [];
50
+ }
51
+ const messageMap = new Map();
52
+ data.chat_messages.forEach((msg) => {
53
+ if (msg && msg.uuid) {
54
+ messageMap.set(msg.uuid, msg);
55
+ }
56
+ });
57
+
58
+ const branch = [];
59
+ let currentUuid = data.current_leaf_message_uuid;
60
+ while (currentUuid && messageMap.has(currentUuid)) {
61
+ const message = messageMap.get(currentUuid);
62
+ branch.unshift(message);
63
+ currentUuid = message.parent_message_uuid;
64
+ if (!messageMap.has(currentUuid)) {
65
+ break;
66
+ }
67
+ }
68
+ return branch;
69
+ }
70
+
71
+ function extractArtifactsFromText(text) {
72
+ const artifactRegex = /<antArtifact[^>]*>([\s\S]*?)<\/antArtifact>/g;
73
+ const artifacts = [];
74
+ let match;
75
+ while ((match = artifactRegex.exec(text)) !== null) {
76
+ const fullTag = match[0];
77
+ const content = match[1];
78
+
79
+ const titleMatch = fullTag.match(/title="([^"]*)"/);
80
+ const languageMatch = fullTag.match(/language="([^"]*)"/);
81
+
82
+ artifacts.push({
83
+ title: titleMatch ? titleMatch[1] : 'Artifact',
84
+ language: languageMatch ? languageMatch[1] : 'text',
85
+ content: content.trim(),
86
+ });
87
+ }
88
+ return artifacts;
89
+ }
90
+
91
+ function extractArtifacts(message) {
92
+ const artifacts = [];
93
+ if (message.content && Array.isArray(message.content)) {
94
+ for (const content of message.content) {
95
+ if (
96
+ content.type === 'tool_use' &&
97
+ (content.name === 'artifacts' || content.name === 'create_file') &&
98
+ content.display_content
99
+ ) {
100
+ const displayContent = content.display_content;
101
+ if (displayContent.type === 'code_block' && displayContent.code) {
102
+ const filename = displayContent.filename || 'artifact';
103
+ const title = filename
104
+ .split('/')
105
+ .pop()
106
+ .replace(/\.[^.]+$/, '');
107
+ artifacts.push({
108
+ title: title || 'Artifact',
109
+ language: displayContent.language || 'text',
110
+ content: displayContent.code.trim(),
111
+ });
112
+ } else if (displayContent.type === 'json_block' && displayContent.json_block) {
113
+ try {
114
+ const data = JSON.parse(displayContent.json_block);
115
+ if (data.filename) {
116
+ const filename = data.filename;
117
+ const title = filename
118
+ .split('/')
119
+ .pop()
120
+ .replace(/\.[^.]+$/, '');
121
+ artifacts.push({
122
+ title: title || 'Artifact',
123
+ language: data.language || 'text',
124
+ content: (data.code || '').trim(),
125
+ });
126
+ }
127
+ } catch (e) {
128
+ console.warn('[AI Exporter] Failed to parse tool use artifact json:', e);
129
+ }
130
+ }
131
+ }
132
+ if (content.text) {
133
+ artifacts.push(...extractArtifactsFromText(content.text));
134
+ }
135
+ }
136
+ }
137
+ if (message.text) {
138
+ artifacts.push(...extractArtifactsFromText(message.text));
139
+ }
140
+ return artifacts;
141
+ }
142
+
143
+ export class ClaudeParser extends ChatParser {
144
+ name = 'Claude';
145
+ constructor() {
146
+ super();
147
+ this.lastFetch = null;
148
+ }
149
+
150
+ isAvailable(url) {
151
+ return url.includes('claude.ai');
152
+ }
153
+
154
+ async parse(options = {}) {
155
+ const title = document.title || 'Claude Chat';
156
+ const messages = [];
157
+
158
+ const conversationId = getConversationId();
159
+ const parserMode = options.parserMode || 'auto';
160
+
161
+ if (conversationId && parserMode !== 'prefer_dom') {
162
+ const orgId = await getOrganizationId();
163
+ if (orgId) {
164
+ try {
165
+ const now = Date.now();
166
+ let data;
167
+
168
+ if (
169
+ this.lastFetch &&
170
+ this.lastFetch.conversationId === conversationId &&
171
+ now - this.lastFetch.timestamp < 20000
172
+ ) {
173
+ data = this.lastFetch.data;
174
+ } else {
175
+ data = await fetchConversation(orgId, conversationId);
176
+ this.lastFetch = {
177
+ conversationId,
178
+ timestamp: now,
179
+ data,
180
+ };
181
+ }
182
+
183
+ const branch = getCurrentBranch(data);
184
+
185
+ const convTitle = data.name || title;
186
+
187
+ for (const message of branch) {
188
+ const role = message.sender === 'human' ? 'User' : 'Claude';
189
+
190
+ let contentStr = '';
191
+
192
+ // Construct content
193
+ if (message.content && Array.isArray(message.content)) {
194
+ for (const block of message.content) {
195
+ if (block.type === 'thinking' && block.thinking) {
196
+ contentStr += `> **Thinking Process:**\n> \n> ${block.thinking.replace(/\n/g, '\n> ')}\n\n`;
197
+ } else if (block.type === 'text' && block.text) {
198
+ const cleanText = block.text
199
+ .replace(/<antArtifact[^>]*>[\s\S]*?<\/antArtifact>/g, '')
200
+ .trim();
201
+ if (cleanText) {
202
+ contentStr += `${cleanText}\n\n`;
203
+ }
204
+ }
205
+ }
206
+ } else if (message.text) {
207
+ const cleanText = message.text
208
+ .replace(/<antArtifact[^>]*>[\s\S]*?<\/antArtifact>/g, '')
209
+ .trim();
210
+ if (cleanText) {
211
+ contentStr += `${cleanText}\n\n`;
212
+ }
213
+ }
214
+
215
+ // Append attachments (for user messages)
216
+ if (message.attachments && message.attachments.length > 0) {
217
+ for (const attachment of message.attachments) {
218
+ if (attachment.file_name) {
219
+ let header = `### Attachment: ${attachment.file_name}`;
220
+ const meta = [];
221
+ if (attachment.file_size) {
222
+ meta.push(`${(attachment.file_size / 1024).toFixed(1)} KB`);
223
+ }
224
+ if (attachment.file_type) {
225
+ meta.push(attachment.file_type);
226
+ }
227
+ if (meta.length > 0) {
228
+ header += ` _(${meta.join(', ')})_`;
229
+ }
230
+ contentStr += `\n\n${header}\n`;
231
+ if (attachment.extracted_content) {
232
+ contentStr += `\`\`\`\`\n${attachment.extracted_content}\n\`\`\`\`\n\n`;
233
+ }
234
+ } else if (attachment.extracted_content) {
235
+ contentStr += `\n\n### Pasted\n\`\`\`\`\n${attachment.extracted_content}\n\`\`\`\`\n\n`;
236
+ }
237
+ }
238
+ }
239
+
240
+ contentStr = contentStr.trim();
241
+ if (contentStr) {
242
+ messages.push({ role, content: contentStr });
243
+ }
244
+
245
+ // Extract and push artifacts
246
+ const artifacts = extractArtifacts(message);
247
+ for (const artifact of artifacts) {
248
+ let artContent = '';
249
+ const artTitle = artifact.title || 'Artifact';
250
+ const artText = artifact.content || '';
251
+ const artLang = artifact.language || 'text';
252
+
253
+ if (artLang === 'markdown' || artLang === 'text') {
254
+ const quotedContent = artText
255
+ .split('\n')
256
+ .map((line) => `> ${line}`)
257
+ .join('\n');
258
+ artContent = `\n\n> **Artifact: ${artTitle}**\n\n${quotedContent}\n\n`;
259
+ } else {
260
+ artContent = `\n\n> **Artifact: ${artTitle}**\n\`\`\`${artLang}\n${artText}\n\`\`\`\n\n`;
261
+ }
262
+
263
+ messages.push({
264
+ role: 'Claude Artifact',
265
+ content: artContent.trim(),
266
+ });
267
+ }
268
+ }
269
+
270
+ const currentUrl =
271
+ typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
272
+ const metadata = {
273
+ Source: 'Claude',
274
+ Date: new Date().toLocaleString(),
275
+ Link: currentUrl,
276
+ Model: data.model || 'Claude',
277
+ };
278
+
279
+ return { title: convTitle, messages, url: currentUrl, metadata };
280
+ } catch (e) {
281
+ console.error('[AI Exporter] Claude API parse failed, falling back to DOM:', e);
282
+ }
283
+ }
284
+ }
285
+
286
+ // Inject the React reader script if not already injected (DOM Fallback)
287
+ if (!document.getElementById('ai-export-claude-reader')) {
288
+ const script = document.createElement('script');
289
+ script.src = chrome.runtime.getURL('content/claude_react_reader.js');
290
+ script.id = 'ai-export-claude-reader';
291
+ script.onload = function () {
292
+ this.remove(); // Clean up script tag
293
+ };
294
+ (document.head || document.documentElement).appendChild(script);
295
+ // Give it a moment to initialize
296
+ await new Promise((r) => setTimeout(r, 100));
297
+ }
298
+
299
+ // Helper to get artifact info
300
+ const getArtifactInfo = (index) => {
301
+ return new Promise((resolve) => {
302
+ const handler = (event) => {
303
+ if (event.data.type === 'RspAtftInfo' && event.data.idx === index) {
304
+ window.removeEventListener('message', handler);
305
+ resolve(event.data.atftInfo);
306
+ }
307
+ };
308
+ window.addEventListener('message', handler);
309
+ window.postMessage({ type: 'ReqAtftInfo', idx: index }, window.location.origin);
310
+
311
+ // Timeout fallback
312
+ setTimeout(() => {
313
+ window.removeEventListener('message', handler);
314
+ resolve(null);
315
+ }, 1000); // 1s timeout
316
+ });
317
+ };
318
+
319
+ const strictSelectors = [
320
+ '[data-testid="user-message"]',
321
+ '.font-claude-message',
322
+ '.font-claude-response',
323
+ '.artifact-block-cell',
324
+ ].join(', ');
325
+
326
+ const fallbackSelectors = ['div.font-serif'].join(', ');
327
+
328
+ const strictCandidates = Array.from(document.querySelectorAll(strictSelectors));
329
+ const fallbackCandidates = Array.from(document.querySelectorAll(fallbackSelectors));
330
+
331
+ const validFallbacks = fallbackCandidates.filter((fallback) => {
332
+ const overlapsWithError = strictCandidates.some(
333
+ (strict) => strict.contains(fallback) || fallback.contains(strict),
334
+ );
335
+ return !overlapsWithError;
336
+ });
337
+
338
+ const combined = [...new Set([...strictCandidates, ...validFallbacks])];
339
+
340
+ const allElements = combined.sort((a, b) => {
341
+ return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
342
+ });
343
+
344
+ const artifactElements = document.querySelectorAll('.artifact-block-cell');
345
+ const artifactMap = new Map();
346
+ artifactElements.forEach((el, index) => artifactMap.set(el, index));
347
+
348
+ for (const el of allElements) {
349
+ let role = 'Unknown';
350
+ let content = '';
351
+
352
+ if (el.matches('[data-testid="user-message"]')) {
353
+ role = 'User';
354
+ const clone = el.cloneNode(true);
355
+ clone.querySelectorAll('button').forEach((btn) => btn.remove());
356
+ content = convertToMarkdown(clone);
357
+ } else if (
358
+ el.matches('.font-claude-message') ||
359
+ el.matches('.font-claude-response') ||
360
+ el.matches('div.font-serif')
361
+ ) {
362
+ role = 'Claude';
363
+ const clone = el.cloneNode(true);
364
+ clone.querySelectorAll('button').forEach((btn) => btn.remove());
365
+ content = convertToMarkdown(clone);
366
+ } else if (el.matches('.artifact-block-cell')) {
367
+ role = 'Claude Artifact';
368
+
369
+ const index = artifactMap.get(el);
370
+ if (index !== undefined) {
371
+ const info = await getArtifactInfo(index);
372
+ if (info) {
373
+ const artTitle = info.title || 'Artifact';
374
+ const artContent = info.content || '';
375
+ const artLang = info.language || 'text';
376
+ if (artLang === 'markdown' || artLang === 'text') {
377
+ const quotedContent = artContent
378
+ .split('\n')
379
+ .map((line) => `> ${line}`)
380
+ .join('\n');
381
+ content = `\n\n> **Artifact: ${artTitle}**\n\n${quotedContent}\n\n`;
382
+ } else {
383
+ content = `\n\n> **Artifact: ${artTitle}**\n\`\`\`${artLang}\n${artContent}\n\`\`\`\n\n`;
384
+ }
385
+ } else {
386
+ const header =
387
+ el.querySelector('.flex.items-center.gap-2') || el.querySelector('.font-bold');
388
+ const fallbackTitle = header ? header.innerText.split('\n')[0] : 'Unknown Artifact';
389
+ content = `\n> [Artifact: ${fallbackTitle} - content extraction failed]\n`;
390
+ }
391
+ }
392
+ }
393
+
394
+ if (content) {
395
+ messages.push({ role, content });
396
+ }
397
+ }
398
+
399
+ const currentUrl =
400
+ typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
401
+ const metadata = {
402
+ Source: 'Claude',
403
+ Date: new Date().toLocaleString(),
404
+ Link: currentUrl,
405
+ Model: 'Claude',
406
+ };
407
+
408
+ return { title, messages, url: currentUrl, metadata };
409
+ }
410
+ }
@@ -0,0 +1,59 @@
1
+ (() => {
2
+ // Listen for messages from the content script
3
+ window.addEventListener('message', (event) => {
4
+ // Security check: ensure message is from same origin
5
+ if (event.origin !== window.location.origin) return;
6
+
7
+ if (event.data.type === 'ReqAtftInfo') {
8
+ const index = event.data.idx;
9
+ const artifacts = document.querySelectorAll('div.artifact-block-cell');
10
+ const artifactElement = artifacts[index];
11
+
12
+ let artifactInfo = null;
13
+
14
+ if (artifactElement) {
15
+ try {
16
+ // Try to find the React Fiber key
17
+ const key = Object.keys(artifactElement).find((k) => k.startsWith('__reactFiber'));
18
+ if (key) {
19
+ const fiber = artifactElement[key];
20
+ // Navigate React props structure to find the artifact data
21
+ // Path based on reference extension research:
22
+ // memoizedProps -> children -> flatMap(props.properties) -> find(id)
23
+
24
+ const children = fiber.memoizedProps?.children;
25
+ if (Array.isArray(children)) {
26
+ // Try to find the component with properties
27
+ const candidate = children
28
+ .flatMap((c) => {
29
+ // Sometimes structure varies, try to find props.properties
30
+ return c?.props?.properties ? [c.props] : [];
31
+ })
32
+ .find((p) => p.properties && p.properties.id);
33
+
34
+ if (candidate) {
35
+ artifactInfo = candidate.properties;
36
+ }
37
+ }
38
+
39
+ // Fallback: Dump text content if React lookup fails, but mark as raw
40
+ if (!artifactInfo) {
41
+ // artifactInfo = { fallback: true, text: artifactElement.innerText };
42
+ }
43
+ }
44
+ } catch (e) {
45
+ console.error('[AI Export] Error reading React internals:', e);
46
+ }
47
+ }
48
+
49
+ window.postMessage(
50
+ {
51
+ type: 'RspAtftInfo',
52
+ idx: index,
53
+ atftInfo: artifactInfo,
54
+ },
55
+ window.location.origin,
56
+ );
57
+ }
58
+ });
59
+ })();