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.
@@ -0,0 +1,91 @@
1
+ import { ChatParser } from './base.js';
2
+ import { convertToMarkdown } from '../utils/html-to-markdown.js';
3
+
4
+ export class GeminiCloudAssistParser extends ChatParser {
5
+ name = 'Gemini Cloud Assist';
6
+ isAvailable(url) {
7
+ return url.includes('console.cloud.google.com/gemini');
8
+ }
9
+
10
+ async parse() {
11
+ const activeItem = document.querySelector(
12
+ 'mat-list-item.mdc-list-item--activated span.cfc-flex-grow-content',
13
+ );
14
+ let title = activeItem?.textContent?.trim() || '';
15
+
16
+ // Fallback if no active item
17
+ if (!title) {
18
+ const firstUserMsg = document.querySelector('.aic-user-message-text')?.textContent?.trim();
19
+ if (firstUserMsg) {
20
+ title = firstUserMsg.length > 50 ? firstUserMsg.substring(0, 50) + '...' : firstUserMsg;
21
+ }
22
+ }
23
+
24
+ if (!title) {
25
+ title = document.title || 'Gemini Cloud Assist';
26
+ }
27
+
28
+ // Clean up title whitespace
29
+ title = title.replace(/\s+/g, ' ').trim();
30
+
31
+ const messages = [];
32
+
33
+ // Find all turns inside aic-conversation or standard turn containers
34
+ const turns = document.querySelectorAll('aic-conversation .aic-turn, .aic-turn');
35
+
36
+ turns.forEach((turn) => {
37
+ // 1. Process user message
38
+ const userEl = turn.querySelector('.aic-user-message');
39
+ if (userEl) {
40
+ const textEl = userEl.querySelector('.aic-user-message-text') || userEl;
41
+ let text = convertToMarkdown(textEl).trim();
42
+ if (text) {
43
+ messages.push({
44
+ role: 'User',
45
+ content: text,
46
+ });
47
+ }
48
+ }
49
+
50
+ // 2. Process assistant message
51
+ const agentEl = turn.querySelector('aic-agent-entry');
52
+ if (agentEl) {
53
+ // Prefer the actual markdown renderer content if present, to avoid feedback actions/buttons noise
54
+ const markdownEl =
55
+ agentEl.querySelector('.ai-markdown-artifact-renderer') ||
56
+ agentEl.querySelector('.aic-markdown-renderer-container') ||
57
+ agentEl.querySelector('.aic-agent-content') ||
58
+ agentEl;
59
+
60
+ // Clone the element to safely strip unwanted components
61
+ const clone = markdownEl.cloneNode(true);
62
+ // Remove thumb up/down feedback buttons or loader/expander elements
63
+ clone
64
+ .querySelectorAll(
65
+ 'button, aic-feedback-actions, .aic-agent-thoughts-container, aic-loading-indicator',
66
+ )
67
+ .forEach((el) => {
68
+ el.remove();
69
+ });
70
+
71
+ let text = convertToMarkdown(clone).trim();
72
+ if (text) {
73
+ messages.push({
74
+ role: 'Model',
75
+ content: text,
76
+ });
77
+ }
78
+ }
79
+ });
80
+
81
+ const currentUrl =
82
+ typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
83
+ const metadata = {
84
+ Source: 'Gemini Cloud Assist',
85
+ Date: new Date().toLocaleString(),
86
+ Link: currentUrl,
87
+ };
88
+
89
+ return { title, messages, url: currentUrl, metadata };
90
+ }
91
+ }
@@ -0,0 +1,116 @@
1
+ import { ChatParser } from './base.js';
2
+ import { convertToMarkdown } from '../utils/html-to-markdown.js';
3
+
4
+ export class GoogleAIStudioParser extends ChatParser {
5
+ name = 'Google AI Studio';
6
+ isAvailable(url) {
7
+ return url.includes('aistudio.google.com');
8
+ }
9
+
10
+ async parse() {
11
+ let title = '';
12
+
13
+ // Title extraction strategies
14
+ const titleInput = document.querySelector(
15
+ 'input[aria-label*="prompt name" i], input[aria-label*="title" i], .prompt-title',
16
+ );
17
+ if (titleInput && titleInput.value) {
18
+ title = titleInput.value.trim();
19
+ }
20
+ if (!title) {
21
+ const headerTitle = document.querySelector('.title-text, h1, .prompt-name');
22
+ if (headerTitle) {
23
+ title = headerTitle.textContent.trim();
24
+ }
25
+ }
26
+ if (!title) {
27
+ title = document.title || 'Google AI Studio Chat';
28
+ }
29
+ title = title.replace(/\s+/g, ' ').trim();
30
+
31
+ const messages = [];
32
+
33
+ // Selectors for chat turns
34
+ const allTurnElements = Array.from(
35
+ document.querySelectorAll('ms-chat-turn, .chat-turn-container, ms-prompt-editor'),
36
+ );
37
+
38
+ // Filter to top-level turn elements only (avoiding child elements nested in parent turn containers)
39
+ const turnElements = allTurnElements.filter(
40
+ (el) => !allTurnElements.some((parent) => parent !== el && parent.contains(el)),
41
+ );
42
+
43
+ if (turnElements.length > 0) {
44
+ turnElements.forEach((turn) => {
45
+ // User prompt element
46
+ const userEl =
47
+ turn.querySelector('.user-prompt, .user-prompt-container, .user-message') ||
48
+ (turn.tagName === 'MS-PROMPT-EDITOR' ? turn : null);
49
+
50
+ // Model output element
51
+ const aiEl = turn.querySelector(
52
+ '.model-response-text, .model-output, ms-model-output, .model-prompt-container',
53
+ );
54
+
55
+ if (userEl) {
56
+ const contentEl = userEl.querySelector('.turn-content, .prompt-text, textarea') || userEl;
57
+ let text;
58
+ if (contentEl.tagName === 'TEXTAREA') {
59
+ text = contentEl.value || contentEl.textContent || '';
60
+ } else {
61
+ text = convertToMarkdown(contentEl);
62
+ }
63
+ text = text.trim();
64
+ if (text) {
65
+ messages.push({ role: 'User', content: text });
66
+ }
67
+ }
68
+
69
+ if (aiEl) {
70
+ const clone = aiEl.cloneNode(true);
71
+ // Remove noise elements like copy buttons or action menus
72
+ clone
73
+ .querySelectorAll(
74
+ 'button, .mat-expansion-panel-header, .author-label, .timestamp, ms-prompt-options-menu, .navigator-container',
75
+ )
76
+ .forEach((el) => el.remove());
77
+
78
+ let text = convertToMarkdown(clone).trim();
79
+ if (text && text !== 'Thinking...' && !text.includes('model_thought_output')) {
80
+ messages.push({ role: 'Google AI Studio', content: text });
81
+ }
82
+ }
83
+ });
84
+ }
85
+
86
+ // Fallback if turnElements linear loop yielded no messages
87
+ if (messages.length === 0) {
88
+ const userPrompts = document.querySelectorAll('.user-prompt, .user-prompt-container');
89
+ const modelOutputs = document.querySelectorAll(
90
+ '.model-response-text, ms-model-output, .model-output',
91
+ );
92
+
93
+ const maxLen = Math.max(userPrompts.length, modelOutputs.length);
94
+ for (let i = 0; i < maxLen; i++) {
95
+ if (userPrompts[i]) {
96
+ const text = (userPrompts[i].value || convertToMarkdown(userPrompts[i])).trim();
97
+ if (text) messages.push({ role: 'User', content: text });
98
+ }
99
+ if (modelOutputs[i]) {
100
+ const text = convertToMarkdown(modelOutputs[i]).trim();
101
+ if (text) messages.push({ role: 'Google AI Studio', content: text });
102
+ }
103
+ }
104
+ }
105
+
106
+ const currentUrl =
107
+ typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
108
+ const metadata = {
109
+ Source: 'Google AI Studio',
110
+ Date: new Date().toLocaleString(),
111
+ Link: currentUrl,
112
+ };
113
+
114
+ return { title, messages, url: currentUrl, metadata };
115
+ }
116
+ }
@@ -0,0 +1,114 @@
1
+ import { ChatParser } from './base.js';
2
+ import { convertToMarkdown } from '../utils/html-to-markdown.js';
3
+
4
+ export class GoogleSearchAIParser extends ChatParser {
5
+ name = 'Google Search AI';
6
+ isAvailable(url) {
7
+ return /google\.[a-z.]+\/search/.test(url);
8
+ }
9
+
10
+ async parse() {
11
+ const title = document.title || 'Google Search AI Overview';
12
+ const messages = [];
13
+
14
+ // 1. Extract the user's queries
15
+ const queries = [];
16
+ const queryElements = document.querySelectorAll('.sUKAcb');
17
+ queryElements.forEach((el) => {
18
+ // Clone the element to avoid mutating the live DOM
19
+ const clone = el.cloneNode(true);
20
+ const prefixEl = clone.querySelector('.iMqumd');
21
+ if (prefixEl) {
22
+ prefixEl.remove();
23
+ }
24
+ let text = clone.textContent || '';
25
+ // Fallback regex cleanup if needed
26
+ text = text.replace(/^\s*You\s+said:\s*/i, '').trim();
27
+ if (text) {
28
+ queries.push(text);
29
+ }
30
+ });
31
+
32
+ // Fallback to single-turn query extraction if no turn query elements are found
33
+ if (queries.length === 0) {
34
+ let userQuery = '';
35
+ const uqEl = document.querySelector('[data-uq]');
36
+ if (uqEl) {
37
+ userQuery = uqEl.getAttribute('data-uq');
38
+ }
39
+ if (!userQuery) {
40
+ const qEl = document.querySelector('[data-q]');
41
+ if (qEl) {
42
+ userQuery = qEl.getAttribute('data-q');
43
+ }
44
+ }
45
+ if (!userQuery) {
46
+ try {
47
+ const url = new URL(window.location.href);
48
+ userQuery = url.searchParams.get('q');
49
+ } catch {
50
+ // Ignore invalid URLs
51
+ }
52
+ }
53
+ if (!userQuery) {
54
+ const textarea =
55
+ document.querySelector('textarea.gLFyf') ||
56
+ document.querySelector('textarea.ITIRGe') ||
57
+ document.querySelector('input[name="q"]');
58
+ if (textarea) {
59
+ userQuery = textarea.value || textarea.textContent;
60
+ }
61
+ }
62
+ if (userQuery) {
63
+ queries.push(userQuery.trim());
64
+ }
65
+ }
66
+
67
+ // 2. Extract SGE / AI Overview responses
68
+ const responseContainers = [];
69
+ const turnElements = document.querySelectorAll('[data-scope-id="turn"]');
70
+ if (turnElements.length > 0) {
71
+ turnElements.forEach((turnEl) => {
72
+ const resEl =
73
+ turnEl.querySelector('[data-container-id="main-col"]') ||
74
+ turnEl.querySelector('[data-container-id="model-response-placeholder"]');
75
+ if (resEl) {
76
+ responseContainers.push(resEl);
77
+ }
78
+ });
79
+ } else {
80
+ // Fallback for pages without data-scope-id="turn"
81
+ const resEl =
82
+ document.querySelector('[data-container-id="main-col"]') ||
83
+ document.querySelector('[data-container-id="model-response-placeholder"]');
84
+ if (resEl) {
85
+ responseContainers.push(resEl);
86
+ }
87
+ }
88
+
89
+ // Pair queries and responses in order
90
+ const minLength = Math.min(queries.length, responseContainers.length);
91
+ for (let i = 0; i < minLength; i++) {
92
+ messages.push({ role: 'User', content: queries[i].trim() });
93
+ const text = convertToMarkdown(responseContainers[i]);
94
+ if (text.trim()) {
95
+ messages.push({ role: 'Model', content: text.trim() });
96
+ }
97
+ }
98
+
99
+ // If there is a trailing user query with no response container (yet)
100
+ if (queries.length > responseContainers.length) {
101
+ messages.push({ role: 'User', content: queries[queries.length - 1].trim() });
102
+ }
103
+
104
+ const currentUrl =
105
+ typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
106
+ const metadata = {
107
+ Source: 'Google Search AI',
108
+ Date: new Date().toLocaleString(),
109
+ Link: currentUrl,
110
+ };
111
+
112
+ return { title, messages, url: currentUrl, metadata };
113
+ }
114
+ }
package/ai/index.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @covai/parser-core — barrel export.
3
+ *
4
+ * Usage:
5
+ * import { ChatGPTParser, detectPlatform } from '@covai/parser-core';
6
+ * import { isAiChatUrl, AI_CHAT_DOMAINS } from '@covai/parser-core';
7
+ */
8
+
9
+ // Base class
10
+ export { ChatParser } from './base.js';
11
+
12
+ // Individual parsers
13
+ export { ChatGPTParser } from './chatgpt.js';
14
+ export { ClaudeParser } from './claude.js';
15
+ export { GeminiParser } from './gemini.js';
16
+ export { CopilotParser } from './copilot.js';
17
+ export { DeepSeekParser } from './deepseek.js';
18
+ export { MetaParser } from './meta.js';
19
+ export { MistralParser } from './mistral.js';
20
+ export { PerplexityParser } from './perplexity.js';
21
+ export { QwenParser } from './qwen.js';
22
+ export { LumoParser } from './lumo.js';
23
+ export { ZAiParser } from './z_ai.js';
24
+ export { GoogleAIStudioParser } from './google_ai_studio.js';
25
+ export { NotebookLMParser } from './notebooklm.js';
26
+ export { GoogleSearchAIParser } from './google_search_ai.js';
27
+ export { GeminiCloudAssistParser } from './gemini_cloud_assist.js';
28
+
29
+ // Utilities
30
+ export { convertToMarkdown, cleanMarkdown } from '../utils/html-to-markdown.js';
31
+
32
+ // Detection
33
+ export { detectPlatform, isAiChatUrl, parsers } from '../detection/detect-platform.js';
34
+ export { AI_CHAT_DOMAINS } from '../detection/domains.js';
package/ai/lumo.js ADDED
@@ -0,0 +1,101 @@
1
+ import { ChatParser } from './base.js';
2
+ import { convertToMarkdown } from '../utils/html-to-markdown.js';
3
+
4
+ export class LumoParser extends ChatParser {
5
+ name = 'Lumo';
6
+ isAvailable(url) {
7
+ return url.includes('lumo.proton.me');
8
+ }
9
+
10
+ async parse() {
11
+ // Extract Title
12
+ let title = '';
13
+ const titleBtn = document.querySelector('.conversation-header-title-view button');
14
+ if (titleBtn && titleBtn.textContent) {
15
+ title = titleBtn.textContent.trim();
16
+ } else if (document.title) {
17
+ title = document.title.replace(/\s*-\s*Lumo.*$/i, '').trim();
18
+ }
19
+ title = title || 'Lumo Conversation';
20
+
21
+ const messages = [];
22
+ const messageElements = document.querySelectorAll('.lumo-chat-item[data-message-role]');
23
+
24
+ for (const el of messageElements) {
25
+ const roleAttr = el.getAttribute('data-message-role');
26
+
27
+ if (roleAttr === 'user') {
28
+ const contentEl =
29
+ el.querySelector('.lumo-markdown') || el.querySelector('.user-msg-container') || el;
30
+ const text = convertToMarkdown(contentEl);
31
+ if (text.trim()) {
32
+ messages.push({
33
+ role: 'User',
34
+ content: text.trim(),
35
+ });
36
+ }
37
+ } else if (roleAttr === 'assistant') {
38
+ const contentEl =
39
+ el.querySelector('.progressive-markdown-content') ||
40
+ el.querySelector('.assistant-msg-container') ||
41
+ el;
42
+
43
+ const ownerDoc = el.ownerDocument || document;
44
+ const contentElClone = contentEl.cloneNode(true);
45
+
46
+ // Preprocess Lumo code blocks to standard <pre><code class="language-xyz">...</code></pre>
47
+ contentElClone
48
+ .querySelectorAll('.lumo-syntax-highlighter, .lumo-code-block')
49
+ .forEach((codeBlock) => {
50
+ const codeEl = codeBlock.querySelector('code');
51
+ if (codeEl) {
52
+ let language = '';
53
+ const match = (codeEl.className || '').match(/language-([^\s]+)/);
54
+ if (match) {
55
+ language = match[1];
56
+ }
57
+
58
+ const codeText = codeEl.textContent || '';
59
+ const pre = ownerDoc.createElement('pre');
60
+ const code = ownerDoc.createElement('code');
61
+ if (language) {
62
+ code.className = `language-${language}`;
63
+ }
64
+ code.textContent = codeText;
65
+ pre.appendChild(code);
66
+
67
+ const parentToReplace = codeBlock.closest('.message-container') || codeBlock;
68
+ if (parentToReplace && parentToReplace.parentNode) {
69
+ parentToReplace.parentNode.replaceChild(pre, parentToReplace);
70
+ }
71
+ }
72
+ });
73
+
74
+ // Clean up UI toolbar and avatar elements from assistant clone
75
+ contentElClone
76
+ .querySelectorAll('.action-toolbar, .lumo-no-copy, .lumo-avatar, button')
77
+ .forEach((noCopy) => {
78
+ noCopy.remove();
79
+ });
80
+
81
+ const markdown = convertToMarkdown(contentElClone);
82
+ if (markdown.trim()) {
83
+ messages.push({
84
+ role: 'Lumo',
85
+ content: markdown.trim(),
86
+ });
87
+ }
88
+ }
89
+ }
90
+
91
+ const currentUrl =
92
+ typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
93
+ const metadata = {
94
+ Source: 'Lumo',
95
+ Date: new Date().toLocaleString(),
96
+ Link: currentUrl,
97
+ };
98
+
99
+ return { title, messages, url: currentUrl, metadata };
100
+ }
101
+ }
package/ai/meta.js ADDED
@@ -0,0 +1,110 @@
1
+ import { ChatParser } from './base.js';
2
+ import { convertToMarkdown } from '../utils/html-to-markdown.js';
3
+
4
+ export class MetaParser extends ChatParser {
5
+ name = 'Meta AI';
6
+ isAvailable(url) {
7
+ return url.includes('meta.ai');
8
+ }
9
+
10
+ async parse() {
11
+ // Try to get the conversation title from the input field or the header button
12
+ const titleInput = document.querySelector('input[placeholder="Conversation title"]');
13
+ const titleButton = document.querySelector('[data-slot="button"] span.truncate');
14
+ const title =
15
+ titleInput && titleInput.value
16
+ ? titleInput.value
17
+ : titleButton
18
+ ? titleButton.innerText
19
+ : 'Meta AI Session';
20
+
21
+ const messages = [];
22
+
23
+ // Find all possible message elements directly to avoid missing any that don't have a wrapper
24
+ const messageElements = Array.from(
25
+ document.querySelectorAll(
26
+ '[data-message-type="user"], [data-testid="assistant-message"], [data-message-id$="_user"], [data-message-id$="_assistant"]',
27
+ ),
28
+ );
29
+
30
+ // Keep only the outer-most elements if there are nested matches
31
+ const uniqueElements = messageElements.filter((el) => {
32
+ let parent = el.parentElement;
33
+ while (parent) {
34
+ if (messageElements.includes(parent)) {
35
+ return false;
36
+ }
37
+ parent = parent.parentElement;
38
+ }
39
+ return true;
40
+ });
41
+
42
+ uniqueElements.forEach((el) => {
43
+ let role = 'Unknown';
44
+ let content = '';
45
+
46
+ // Check if the element itself identifies as user or assistant
47
+ const isUser =
48
+ el.matches('[data-message-type="user"]') ||
49
+ (el.getAttribute('data-message-id') &&
50
+ el.getAttribute('data-message-id').endsWith('_user'));
51
+ const isAssistant =
52
+ el.matches('[data-testid="assistant-message"]') ||
53
+ (el.getAttribute('data-message-id') &&
54
+ el.getAttribute('data-message-id').endsWith('_assistant'));
55
+
56
+ if (isUser) {
57
+ role = 'User';
58
+ // User text is usually in a span with text-response class or simply pre-wrap
59
+ const textEl =
60
+ el.querySelector('[data-slot="text"].text-response') ||
61
+ el.querySelector('.whitespace-pre-wrap');
62
+ if (textEl) {
63
+ content = convertToMarkdown(textEl);
64
+ } else {
65
+ content = convertToMarkdown(el);
66
+ }
67
+ } else if (isAssistant) {
68
+ role = 'Meta AI';
69
+ // Assistant content is typically styled in markdown-content or prose
70
+ const contentEl =
71
+ el.querySelector('.markdown-content') ||
72
+ el.querySelector('.ur-markdown') ||
73
+ el.querySelector('.prose');
74
+ if (contentEl) {
75
+ const clone = contentEl.cloneNode(true);
76
+
77
+ // Remove noise elements (like citation pills, edit buttons, thinking status, etc)
78
+ const noiseSelectors = [
79
+ 'button',
80
+ '.ur-citation-pill',
81
+ 'svg',
82
+ '[data-testid="citation-pill"]',
83
+ '[data-testid="thinking-status"]',
84
+ ];
85
+ noiseSelectors.forEach((sel) => {
86
+ clone.querySelectorAll(sel).forEach((n) => n.remove());
87
+ });
88
+
89
+ content = convertToMarkdown(clone);
90
+ } else {
91
+ content = convertToMarkdown(el);
92
+ }
93
+ }
94
+
95
+ if (content) {
96
+ messages.push({ role, content });
97
+ }
98
+ });
99
+
100
+ const currentUrl =
101
+ typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
102
+ const metadata = {
103
+ Source: 'Meta AI',
104
+ Date: new Date().toLocaleString(),
105
+ Link: currentUrl,
106
+ };
107
+
108
+ return { title, messages, url: currentUrl, metadata };
109
+ }
110
+ }
package/ai/mistral.js ADDED
@@ -0,0 +1,57 @@
1
+ import { ChatParser } from './base.js';
2
+ import { convertToMarkdown } from '../utils/html-to-markdown.js';
3
+
4
+ export class MistralParser extends ChatParser {
5
+ name = 'Mistral';
6
+ isAvailable(url) {
7
+ return url.includes('chat.mistral.ai');
8
+ }
9
+
10
+ async parse() {
11
+ // Extract Title
12
+ const titleElement = document.querySelector('span.truncate.text-sm');
13
+ let title = 'Mistral Conversation';
14
+ if (titleElement && titleElement.innerText) {
15
+ title = titleElement.innerText.trim();
16
+ } else if (document.title) {
17
+ title = document.title.replace(' - Mistral', '').trim();
18
+ }
19
+
20
+ const messages = [];
21
+ const messageElements = document.querySelectorAll('[data-message-author-role]');
22
+
23
+ for (const el of messageElements) {
24
+ const role = el.getAttribute('data-message-author-role');
25
+
26
+ if (role === 'user') {
27
+ const contentEl =
28
+ el.querySelector('.select-text') || el.querySelector('.whitespace-pre-wrap');
29
+ if (contentEl) {
30
+ messages.push({
31
+ role: 'User',
32
+ content: contentEl.innerText.trim(),
33
+ });
34
+ }
35
+ } else if (role === 'assistant') {
36
+ const answerEl = el.querySelector('[data-message-part-type="answer"]');
37
+ if (answerEl) {
38
+ const markdown = convertToMarkdown(answerEl.innerHTML);
39
+ messages.push({
40
+ role: 'Mistral',
41
+ content: markdown,
42
+ });
43
+ }
44
+ }
45
+ }
46
+
47
+ const currentUrl =
48
+ typeof window !== 'undefined' && window.location ? window.location.href || '' : '';
49
+ const metadata = {
50
+ Source: 'Mistral',
51
+ Date: new Date().toLocaleString(),
52
+ Link: currentUrl,
53
+ };
54
+
55
+ return { title, messages, url: currentUrl, metadata };
56
+ }
57
+ }