decant-core 1.0.0 → 1.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.
@@ -8,7 +8,9 @@ if (!window.__chatgptHelperInjected) {
8
8
 
9
9
  function getCookieDeviceId() {
10
10
  try {
11
- const match = typeof document !== 'undefined' && document.cookie.match(/oai-did=([^;]+)/);
11
+ const match =
12
+ typeof document !== "undefined" &&
13
+ document.cookie.match(/oai-did=([^;]+)/);
12
14
  return match ? match[1] : null;
13
15
  } catch {
14
16
  return null;
@@ -16,19 +18,23 @@ if (!window.__chatgptHelperInjected) {
16
18
  }
17
19
 
18
20
  // Hook fetch to store Authorization and custom headers when page performs network calls
19
- if (typeof window !== 'undefined' && window.fetch) {
21
+ if (typeof window !== "undefined" && window.fetch) {
20
22
  const originalFetch = window.fetch;
21
23
  window.fetch = function (...args) {
22
24
  try {
23
- const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
24
- if (url && (url.includes('/backend-api/') || url.includes('/api/auth/'))) {
25
+ const url = typeof args[0] === "string" ? args[0] : args[0]?.url;
26
+ if (
27
+ url &&
28
+ (url.includes("/backend-api/") || url.includes("/api/auth/"))
29
+ ) {
25
30
  const options = args[1];
26
31
  if (options?.headers) {
27
32
  let auth = null;
28
33
  if (options.headers instanceof Headers) {
29
- auth = options.headers.get('Authorization');
30
- } else if (typeof options.headers === 'object') {
31
- auth = options.headers.Authorization || options.headers.authorization;
34
+ auth = options.headers.get("Authorization");
35
+ } else if (typeof options.headers === "object") {
36
+ auth =
37
+ options.headers.Authorization || options.headers.authorization;
32
38
  }
33
39
  if (auth) {
34
40
  window.capturedAuthStore.authorization = auth;
@@ -42,31 +48,36 @@ if (!window.__chatgptHelperInjected) {
42
48
  };
43
49
  }
44
50
 
45
- window.addEventListener('message', async (event) => {
46
- if (event.origin !== 'https://chatgpt.com') return;
47
- if (event.data?.source !== 'chatgpt-exporter-ext') return;
48
- if (event.data?.type !== 'fetch_conversation') return;
51
+ window.addEventListener("message", async (event) => {
52
+ if (event.origin !== "https://chatgpt.com") return;
53
+ if (event.data?.source !== "chatgpt-exporter-ext") return;
54
+ if (event.data?.type !== "fetch_conversation") return;
49
55
 
50
56
  const { convId, token, requestId, includeImages } = event.data;
51
57
 
52
58
  try {
53
59
  const headers = {
54
- Accept: 'application/json',
60
+ Accept: "application/json",
55
61
  };
56
62
 
57
- const authToken = token ? `Bearer ${token}` : window.capturedAuthStore?.authorization;
63
+ const authToken = token
64
+ ? `Bearer ${token}`
65
+ : window.capturedAuthStore?.authorization;
58
66
  if (authToken) {
59
67
  headers.Authorization = authToken;
60
68
  }
61
69
 
62
70
  const deviceId = getCookieDeviceId();
63
71
  if (deviceId) {
64
- headers['oai-device-id'] = deviceId;
72
+ headers["oai-device-id"] = deviceId;
65
73
  }
66
74
 
67
- const res = await fetch(`https://chatgpt.com/backend-api/conversation/${convId}`, {
68
- headers,
69
- });
75
+ const res = await fetch(
76
+ `https://chatgpt.com/backend-api/conversation/${convId}`,
77
+ {
78
+ headers,
79
+ },
80
+ );
70
81
  if (!res.ok) throw new Error(`API returned ${res.status}`);
71
82
  const data = await res.json();
72
83
 
@@ -77,8 +88,12 @@ if (!window.__chatgptHelperInjected) {
77
88
  const msg = node.message;
78
89
  if (msg && msg.content && Array.isArray(msg.content.parts)) {
79
90
  for (const part of msg.content.parts) {
80
- if (part && part.content_type === 'image_asset_pointer' && part.asset_pointer) {
81
- fileIds.add(part.asset_pointer.split('://')[1]);
91
+ if (
92
+ part &&
93
+ part.content_type === "image_asset_pointer" &&
94
+ part.asset_pointer
95
+ ) {
96
+ fileIds.add(part.asset_pointer.split("://")[1]);
82
97
  }
83
98
  }
84
99
  }
@@ -90,7 +105,7 @@ if (!window.__chatgptHelperInjected) {
90
105
  const b64 = await fetchImageAsBase64(id, token);
91
106
  return [id, b64];
92
107
  } catch (e) {
93
- console.error('[AI Exporter] Error fetching image:', id, e);
108
+ console.error("[AI Exporter] Error fetching image:", id, e);
94
109
  return [id, null];
95
110
  }
96
111
  }),
@@ -99,22 +114,25 @@ if (!window.__chatgptHelperInjected) {
99
114
  }
100
115
 
101
116
  window.postMessage(
102
- { source: 'chatgpt-exporter-page', requestId, data, images },
103
- 'https://chatgpt.com',
117
+ { source: "chatgpt-exporter-page", requestId, data, images },
118
+ "https://chatgpt.com",
104
119
  );
105
120
  } catch (err) {
106
121
  window.postMessage(
107
- { source: 'chatgpt-exporter-page', requestId, error: err.message },
108
- 'https://chatgpt.com',
122
+ { source: "chatgpt-exporter-page", requestId, error: err.message },
123
+ "https://chatgpt.com",
109
124
  );
110
125
  }
111
126
  });
112
127
 
113
128
  async function fetchImageAsBase64(fileId, token) {
114
129
  try {
115
- const dlRes = await fetch(`https://chatgpt.com/backend-api/files/download/${fileId}`, {
116
- headers: { Authorization: `Bearer ${token}` },
117
- });
130
+ const dlRes = await fetch(
131
+ `https://chatgpt.com/backend-api/files/download/${fileId}`,
132
+ {
133
+ headers: { Authorization: `Bearer ${token}` },
134
+ },
135
+ );
118
136
  if (!dlRes.ok) return null;
119
137
  const { download_url } = await dlRes.json();
120
138
  if (!download_url) return null;
@@ -130,7 +148,7 @@ if (!window.__chatgptHelperInjected) {
130
148
  reader.readAsDataURL(blob);
131
149
  });
132
150
  } catch (e) {
133
- console.error('[AI Exporter] fetchImageAsBase64 error:', e);
151
+ console.error("[AI Exporter] fetchImageAsBase64 error:", e);
134
152
  return null;
135
153
  }
136
154
  }
@@ -12,7 +12,7 @@ function isScrollable(element) {
12
12
 
13
13
  function messageKey(message) {
14
14
  if (message.key) return message.key;
15
- return `${message.role}:${message.content.replace(/\s+/g, ' ').trim()}`;
15
+ return `${message.role}:${message.content.replace(/\s+/g, " ").trim()}`;
16
16
  }
17
17
 
18
18
  function publicMessage(message) {
@@ -23,17 +23,18 @@ function publicMessage(message) {
23
23
  }
24
24
 
25
25
  export function getConversationTurnIndex(turn) {
26
- if (!turn || typeof turn.getAttribute !== 'function') return Number.POSITIVE_INFINITY;
27
- const testId = turn.getAttribute('data-testid') || '';
26
+ if (!turn || typeof turn.getAttribute !== "function")
27
+ return Number.POSITIVE_INFINITY;
28
+ const testId = turn.getAttribute("data-testid") || "";
28
29
  const match = testId.match(/^conversation-turn-(\d+)$/);
29
30
  if (match) return Number(match[1]);
30
- const turnId = turn.getAttribute('data-turn-id');
31
+ const turnId = turn.getAttribute("data-turn-id");
31
32
  if (turnId && /^\d+$/.test(turnId)) return Number(turnId);
32
33
  return Number.POSITIVE_INFINITY;
33
34
  }
34
35
 
35
36
  export function getConversationTurns(doc = document) {
36
- if (!doc || typeof doc.querySelectorAll !== 'function') return [];
37
+ if (!doc || typeof doc.querySelectorAll !== "function") return [];
37
38
  const turns = Array.from(doc.querySelectorAll(TURN_SELECTOR));
38
39
  // Filter out nested duplicates (e.g. [data-message-author-role] inside an article)
39
40
  const filtered = turns.filter((el) => {
@@ -44,7 +45,7 @@ export function getConversationTurns(doc = document) {
44
45
  const idxA = getConversationTurnIndex(a);
45
46
  const idxB = getConversationTurnIndex(b);
46
47
  if (idxA !== idxB) return idxA - idxB;
47
- if (typeof a.compareDocumentPosition === 'function') {
48
+ if (typeof a.compareDocumentPosition === "function") {
48
49
  return a.compareDocumentPosition(b) &
49
50
  (doc.defaultView?.Node?.DOCUMENT_POSITION_FOLLOWING || 4)
50
51
  ? -1
@@ -63,35 +64,37 @@ export function findChatGPTScrollRoot(turns, doc = document) {
63
64
  current = current.parentElement;
64
65
  }
65
66
 
66
- const main = doc.querySelector('main');
67
+ const main = doc.querySelector("main");
67
68
  if (isScrollable(main)) return main;
68
69
 
69
70
  return doc.scrollingElement || doc.documentElement || doc.body;
70
71
  }
71
72
 
72
73
  function createProgressOverlay(doc) {
73
- if (!doc || typeof doc.createElement !== 'function' || !doc.body) return null;
74
+ if (!doc || typeof doc.createElement !== "function" || !doc.body) return null;
74
75
  try {
75
- const overlay = doc.createElement('div');
76
- overlay.id = 'ai-exporter-progress-overlay';
76
+ const overlay = doc.createElement("div");
77
+ overlay.id = "ai-exporter-progress-overlay";
77
78
  Object.assign(overlay.style, {
78
- position: 'fixed',
79
- top: '16px',
80
- left: '50%',
81
- transform: 'translateX(-50%)',
82
- zIndex: '999999',
83
- padding: '10px 20px',
84
- background: 'rgba(15, 23, 42, 0.92)',
85
- color: '#f8fafc',
86
- borderRadius: '8px',
87
- fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
88
- fontSize: '14px',
89
- fontWeight: '500',
90
- boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3)',
91
- pointerEvents: 'none',
92
- transition: 'opacity 0.2s ease',
79
+ position: "fixed",
80
+ top: "16px",
81
+ left: "50%",
82
+ transform: "translateX(-50%)",
83
+ zIndex: "999999",
84
+ padding: "10px 20px",
85
+ background: "rgba(15, 23, 42, 0.92)",
86
+ color: "#f8fafc",
87
+ borderRadius: "8px",
88
+ fontFamily:
89
+ 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
90
+ fontSize: "14px",
91
+ fontWeight: "500",
92
+ boxShadow:
93
+ "0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3)",
94
+ pointerEvents: "none",
95
+ transition: "opacity 0.2s ease",
93
96
  });
94
- overlay.textContent = 'Preparing conversation export...';
97
+ overlay.textContent = "Preparing conversation export...";
95
98
  doc.body.appendChild(overlay);
96
99
  return overlay;
97
100
  } catch {
@@ -108,7 +111,7 @@ function updateProgressOverlay(overlay, current, total) {
108
111
  function removeProgressOverlay(overlay) {
109
112
  if (!overlay) return;
110
113
  try {
111
- overlay.style.opacity = '0';
114
+ overlay.style.opacity = "0";
112
115
  setTimeout(() => overlay.remove(), 200);
113
116
  } catch {
114
117
  // Ignore cleanup errors
@@ -122,14 +125,14 @@ export async function collectMountedTurnMessages({
122
125
  waitForRender = delay,
123
126
  renderWaitMs = DEFAULT_RENDER_WAIT_MS,
124
127
  renderAttempts = 4,
125
- doc = typeof document !== 'undefined' ? document : null,
128
+ doc = typeof document !== "undefined" ? document : null,
126
129
  }) {
127
130
  const originalTop = scrollRoot?.scrollTop;
128
131
  const originalBehavior = scrollRoot?.style?.scrollBehavior;
129
132
  const overlay = createProgressOverlay(doc);
130
133
 
131
134
  if (scrollRoot && scrollRoot.style) {
132
- scrollRoot.style.scrollBehavior = 'auto';
135
+ scrollRoot.style.scrollBehavior = "auto";
133
136
  }
134
137
 
135
138
  const seen = new Set();
@@ -137,7 +140,7 @@ export async function collectMountedTurnMessages({
137
140
 
138
141
  try {
139
142
  // Scroll to top first to trigger un-virtualization of earlier turns
140
- if (scrollRoot && typeof scrollRoot.scrollTop === 'number') {
143
+ if (scrollRoot && typeof scrollRoot.scrollTop === "number") {
141
144
  scrollRoot.scrollTop = 0;
142
145
  await waitForRender(renderWaitMs);
143
146
  }
@@ -158,8 +161,8 @@ export async function collectMountedTurnMessages({
158
161
  const turn = orderedTurns[idx];
159
162
  updateProgressOverlay(overlay, idx + 1, totalTurns);
160
163
 
161
- if (typeof turn.scrollIntoView === 'function') {
162
- turn.scrollIntoView({ block: 'center' });
164
+ if (typeof turn.scrollIntoView === "function") {
165
+ turn.scrollIntoView({ block: "center" });
163
166
  }
164
167
 
165
168
  let message = null;
@@ -181,7 +184,7 @@ export async function collectMountedTurnMessages({
181
184
  removeProgressOverlay(overlay);
182
185
 
183
186
  if (scrollRoot && scrollRoot.style) {
184
- scrollRoot.style.scrollBehavior = originalBehavior || '';
187
+ scrollRoot.style.scrollBehavior = originalBehavior || "";
185
188
  }
186
189
 
187
190
  if (scrollRoot && Number.isFinite(originalTop)) {
package/ai/chub.js ADDED
@@ -0,0 +1,90 @@
1
+ import { ChatParser } from "./base.js";
2
+ import { convertToMarkdown } from "../utils/html-to-markdown.js";
3
+
4
+ export class ChubParser extends ChatParser {
5
+ name = "Chub";
6
+
7
+ isAvailable(url) {
8
+ return url.includes("chub.ai") || url.includes("characterhub.org");
9
+ }
10
+
11
+ async parse() {
12
+ // Extract Character Name
13
+ const charLink = document.querySelector('a[href*="/characters/"]');
14
+ const character = charLink
15
+ ? charLink.textContent.trim().replace(/\s+/g, " ")
16
+ : "";
17
+
18
+ // Extract User Name
19
+ const userLink = document.querySelector('a[href*="/users/"]');
20
+ let user = "";
21
+ if (userLink) {
22
+ const href = userLink.getAttribute("href") || "";
23
+ const match = href.match(/\/users\/([^/?#]+)/);
24
+ user = match ? match[1] : userLink.textContent.trim();
25
+ }
26
+
27
+ // Extract Title
28
+ let title = document.title
29
+ ? document.title.trim().replace(/\s+/g, " ")
30
+ : "";
31
+ if (
32
+ !title ||
33
+ title.toLowerCase() === "chub" ||
34
+ title.toLowerCase() === "chub ai"
35
+ ) {
36
+ title = character ? `Chat with ${character}` : "Chub AI Conversation";
37
+ }
38
+
39
+ // Select message items
40
+ let items = Array.from(
41
+ document.querySelectorAll("li.ant-list-item.message-full"),
42
+ );
43
+ if (items.length === 0) {
44
+ const allMsg = Array.from(document.querySelectorAll(".message-full"));
45
+ items = allMsg.filter(
46
+ (el) => !allMsg.some((parent) => parent !== el && parent.contains(el)),
47
+ );
48
+ }
49
+
50
+ const messages = [];
51
+ for (const item of items) {
52
+ const isUser =
53
+ !!item.querySelector('a[href*="/users/"]') ||
54
+ !item.querySelector('a[href*="/characters/"]');
55
+ const role = isUser ? "User" : "Chub";
56
+
57
+ const contentEl =
58
+ item.querySelector(".msg-mkdn-container") ||
59
+ item.querySelector(".ant-list-item-meta-description") ||
60
+ item;
61
+ const clone = contentEl.cloneNode(true);
62
+
63
+ // Clean up UI controls and noise elements
64
+ clone
65
+ .querySelectorAll(
66
+ "button, .message-control-buttons, .message-title, .ant-image-mask, .anticon",
67
+ )
68
+ .forEach((el) => el.remove());
69
+
70
+ const markdown = convertToMarkdown(clone);
71
+ if (markdown.trim()) {
72
+ messages.push({ role, content: markdown.trim() });
73
+ }
74
+ }
75
+
76
+ const currentUrl =
77
+ typeof window !== "undefined" && window.location
78
+ ? window.location.href || ""
79
+ : "";
80
+ const metadata = {
81
+ Source: "Chub",
82
+ Date: new Date().toLocaleString(),
83
+ Link: currentUrl,
84
+ };
85
+ if (character) metadata.Character = character;
86
+ if (user) metadata.User = user;
87
+
88
+ return { title, messages, url: currentUrl, metadata };
89
+ }
90
+ }