prompt-exporter 2.1.0 → 2.2.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/docs/DEVELOPER.md CHANGED
@@ -18,6 +18,7 @@ src/
18
18
  registry.ts
19
19
  chatgpt/ # chatgpt source (api/, index.ts)
20
20
  lumo/ # lumo.proton.me (CDP Redux export)
21
+ perplexity/ # perplexity.ai (REST via CDP cookies)
21
22
  services/ # storage, markdown, backup (source-agnostic)
22
23
  utils/ # paths, credentials, cdp, browser
23
24
  aur/
@@ -26,74 +27,27 @@ aur/
26
27
  publish.sh
27
28
  ```
28
29
 
29
- ## Release (2.0.x)
30
+ ## Build output
30
31
 
31
- `package.json` is track `package.json` (currently 2.0.1). Shipping steps:
32
+ `npm run build` uses **tsup** (`tsup.config.ts`) to emit a **single minified CJS** file at `dist/index.cjs` (~500 KB):
32
33
 
33
- ### 1. Push release prep (this commit) and tag
34
+ - All runtime dependencies are bundled (`noExternal`); Node builtins stay external
35
+ - Version is injected as `__PACKAGE_VERSION__`
36
+ - Shebang comes from `src/index.ts`
37
+ - CJS (not ESM) so bundled deps that use `require()` work at runtime
34
38
 
35
- ```bash
36
- cd /path/to/prompt-exporter
37
- git push origin master
38
-
39
- git tag -a v2.0.0 -m "prompt-exporter 2.0.0"
40
- git push origin v2.0.0
41
- ```
42
-
43
- ### 2. GitHub Release
44
-
45
- ```bash
46
- # If gh is installed:
47
- gh release create v2.0.0 --title "2.0.0" --notes-file - <<'EOF'
48
- See CHANGELOG.md for breaking changes (rebrand, Apache-2.0, multi-source layout).
49
- EOF
50
-
51
- # Or: GitHub → Releases → Draft from tag v2.0.0 → paste CHANGELOG 2.0.0 section
52
- ```
53
-
54
- ### 3. Fill AUR stable checksum
55
-
56
- ```bash
57
- TARBALL_URL="https://github.com/azbarcea/prompt-exporter/archive/refs/tags/v2.0.0.tar.gz"
58
- SUM=$(curl -sL "$TARBALL_URL" | sha256sum | awk '{print $1}')
59
- echo "$SUM"
60
-
61
- # Update aur/prompt-exporter/PKGBUILD sha256sums and regenerate .SRCINFO:
62
- sed -i "s/sha256sums=.*/sha256sums=('${SUM}')/" aur/prompt-exporter/PKGBUILD
63
- ( cd aur/prompt-exporter && makepkg --printsrcinfo > .SRCINFO )
39
+ AUR `package()` installs only that file to `/usr/bin/prompt-exporter` (plus docs/licenses). Do **not** copy `node_modules` into the package.
64
40
 
65
- git add aur/prompt-exporter
66
- git commit -m "chore(aur): set sha256 for prompt-exporter 2.0.0"
67
- git push origin master
68
- ```
69
-
70
- ### 4. Publish AUR packages
71
-
72
- Requires SSH key registered on [AUR](https://aur.archlinux.org) (`ssh aur@aur.archlinux.org`).
73
-
74
- ```bash
75
- # First-time package names (if empty clone fails, publish.sh inits and pushes):
76
- ./aur/publish.sh prompt-exporter # stable 2.0.0
77
- ./aur/publish.sh prompt-exporter-git # rolling
78
- ```
79
-
80
- If `prompt-exporter` or `prompt-exporter-git` does not exist yet on AUR, create them once via the AUR web UI (“Submit”) or by pushing a new empty repo with `git push -u origin master` after the script’s `git init` path — you must be logged in as the maintainer.
81
-
82
- ### 5. Optional: npm registry
83
-
84
- Not required (install via `github:azbarcea/prompt-exporter` works). To publish:
85
-
86
- ```bash
87
- npm login
88
- npm publish --access public
89
- ```
41
+ ## Release
90
42
 
91
- ### 6. Smoke after install
43
+ Ship with the Makefile (order: **npmjs → GitHub → AUR**):
92
44
 
93
45
  ```bash
94
- yay -S prompt-exporter
95
- prompt-exporter --version # 2.0.0
96
- prompt-exporter sources
46
+ make help
47
+ make bump VERSION=x.y.z # optional; commits version + CHANGELOG stub
48
+ make npm-auth # npm login or export NPM_TOKEN=…
49
+ make release CONFIRM=1
50
+ # resume past npm if needed: make release CONFIRM=1 SKIP_NPM=1
97
51
  ```
98
52
 
99
53
  ## Develop
@@ -8,7 +8,7 @@
8
8
  - Re-sync incrementally as chats change
9
9
  - Hand transcripts to another AI as context
10
10
 
11
- The first implemented sources are **chatgpt** (chatgpt.com) and **lumo** ([lumo.proton.me](https://lumo.proton.me/)). Planned next: `claude`, `gemini`, `deepseek`, `perplexity`, `copilot` (Microsoft 365), `grok`, and `kimi`.
11
+ The first implemented sources are **chatgpt** (chatgpt.com), **lumo** ([lumo.proton.me](https://lumo.proton.me/)), and **perplexity** ([perplexity.ai](https://www.perplexity.ai/)). Planned next: `claude`, `gemini`, `deepseek`, `copilot` (Microsoft 365), `grok`, and `kimi`.
12
12
 
13
13
  **Platforms:** Linux is supported today; Windows and macOS are in focus for CDP/browser path parity.
14
14
 
@@ -24,6 +24,16 @@ yay -S prompt-exporter
24
24
  yay -S prompt-exporter-git
25
25
  ```
26
26
 
27
+ The AUR package installs a single bundled binary at `/usr/bin/prompt-exporter` (no `node_modules` on disk).
28
+
29
+ If you previously used `npm link` / a user install under `~/.local/bin` and then removed that path, bash may still **hash** the old location:
30
+
31
+ ```bash
32
+ type prompt-exporter # may show hashed ~/.local/bin/...
33
+ hash -r # clear the command hash table
34
+ type prompt-exporter # should resolve to /usr/bin/prompt-exporter
35
+ ```
36
+
27
37
  ### From GitHub (npm package)
28
38
 
29
39
  ```bash
@@ -104,6 +114,19 @@ prompt-exporter list --source lumo
104
114
 
105
115
  Auth prefers a logged-in Chromium CDP session. Node Bearer alone is often blocked by Cloudflare.
106
116
 
117
+ ### Perplexity
118
+
119
+ There is no official public API for thread history. Sync calls Perplexity’s internal REST endpoints (`/rest/thread/list_ask_threads`, `/rest/thread/{uuid}`) from a logged-in Chromium tab (cookies / Cloudflare):
120
+
121
+ ```bash
122
+ prompt-exporter chromium start --url https://www.perplexity.ai/
123
+ # log in; Library threads should be visible
124
+ prompt-exporter sync --source perplexity
125
+ prompt-exporter list --source perplexity
126
+ ```
127
+
128
+ `sync` is incremental by default (skips unchanged `update_time`).
129
+
107
130
  ## Commands
108
131
 
109
132
  ```text
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "prompt-exporter",
3
- "version": "2.1.0",
4
- "description": "CLI to sync AI conversation prompts from multiple sources (ChatGPT, Lumo, …) to local JSON + Markdown",
3
+ "version": "2.2.0",
4
+ "description": "CLI to sync AI conversation prompts from multiple sources (ChatGPT, Lumo, Perplexity, …) to local JSON + Markdown",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Alexandru Zbarcea <alexz@apache.org>",
7
7
  "type": "module",
8
- "main": "dist/index.js",
8
+ "main": "dist/index.cjs",
9
9
  "bin": {
10
- "prompt-exporter": "dist/index.js"
10
+ "prompt-exporter": "dist/index.cjs"
11
11
  },
12
12
  "files": [
13
13
  "dist",
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "scripts": {
23
23
  "dev": "node --disable-warning=DEP0205 --import tsx src/index.ts",
24
- "build": "tsup src/index.ts --format esm --dts --clean",
25
- "start": "node dist/index.js",
24
+ "build": "tsup",
25
+ "start": "node dist/index.cjs",
26
26
  "prepublishOnly": "npm run build",
27
27
  "typecheck": "tsc --noEmit",
28
28
  "test": "npm run build && node --disable-warning=DEP0205 --import tsx --test tests/*.test.ts",
@@ -1,28 +0,0 @@
1
- import {
2
- activateCdpTarget,
3
- cdpNavigate,
4
- discoverCdpPort,
5
- ensureChatGptSessionTab,
6
- extractAccessTokenViaCdp,
7
- getCdpEndpointInfo,
8
- isCdpRunning,
9
- isChatGptUrl,
10
- isLumoUrl,
11
- listCdpTargets,
12
- openCdpTab,
13
- withCdpSocket
14
- } from "./chunk-O5E5XFJR.js";
15
- export {
16
- activateCdpTarget,
17
- cdpNavigate,
18
- discoverCdpPort,
19
- ensureChatGptSessionTab,
20
- extractAccessTokenViaCdp,
21
- getCdpEndpointInfo,
22
- isCdpRunning,
23
- isChatGptUrl,
24
- isLumoUrl,
25
- listCdpTargets,
26
- openCdpTab,
27
- withCdpSocket
28
- };
@@ -1,446 +0,0 @@
1
- // src/utils/paths.ts
2
- import fs from "fs/promises";
3
- import os from "os";
4
- import path from "path";
5
- function getConfigDir() {
6
- return process.env.PROMPT_EXPORTER_HOME?.trim() || path.join(os.homedir(), ".prompt-exporter");
7
- }
8
- function getDataParentDir() {
9
- return process.env.PROMPT_EXPORTER_DATA?.trim() || process.env.CHATGPT_EXPORTER_DATA?.trim() || getConfigDir();
10
- }
11
- function getSourceDataDir(sourceId) {
12
- return path.join(getDataParentDir(), sourceId);
13
- }
14
- function getDefaultDataDir() {
15
- return getSourceDataDir("chatgpt");
16
- }
17
- function getSourceTokenPath(sourceId) {
18
- return path.join(getConfigDir(), "sources", sourceId, "token");
19
- }
20
- function getIsolatedChromiumProfileDir() {
21
- return path.join(getConfigDir(), "chromium");
22
- }
23
- function getSystemChromiumUserDataDir(browserPath) {
24
- const home = os.homedir();
25
- const binary = (browserPath || process.env.PROMPT_EXPORTER_BROWSER || process.env.CHATGPT_EXPORTER_BROWSER || "").toLowerCase();
26
- if (process.platform === "darwin") {
27
- if (binary.includes("chrome")) {
28
- return path.join(home, "Library", "Application Support", "Google", "Chrome");
29
- }
30
- return path.join(home, "Library", "Application Support", "Chromium");
31
- }
32
- if (process.platform === "win32") {
33
- const local = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
34
- if (binary.includes("chrome")) {
35
- return path.join(local, "Google", "Chrome", "User Data");
36
- }
37
- return path.join(local, "Chromium", "User Data");
38
- }
39
- const config = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
40
- if (binary.includes("google-chrome") || /\/chrome$/.test(binary)) {
41
- return path.join(config, "google-chrome");
42
- }
43
- return path.join(config, "chromium");
44
- }
45
- function resolveChromiumUserDataDir(options) {
46
- const custom = options.userDataDir?.trim() || process.env.PROMPT_EXPORTER_USER_DATA_DIR?.trim() || process.env.CHATGPT_EXPORTER_USER_DATA_DIR?.trim();
47
- if (custom) {
48
- return { dir: custom, mode: "custom" };
49
- }
50
- if (options.isolated) {
51
- return { dir: getIsolatedChromiumProfileDir(), mode: "isolated" };
52
- }
53
- return {
54
- dir: getSystemChromiumUserDataDir(options.browserPath),
55
- mode: "system"
56
- };
57
- }
58
- var DEFAULT_CDP_PORT = 9222;
59
- var DEFAULT_SOURCE_ID = "chatgpt";
60
- async function ensureHomeLayout() {
61
- const home = getConfigDir();
62
- await fs.mkdir(home, { recursive: true });
63
- await fs.mkdir(path.join(home, "sources"), { recursive: true });
64
- const legacyHome = path.join(os.homedir(), ".chatgpt-exporter");
65
- if (home !== legacyHome) {
66
- try {
67
- await fs.access(legacyHome);
68
- const entries = await fs.readdir(legacyHome, { withFileTypes: true });
69
- for (const entry of entries) {
70
- const from = path.join(legacyHome, entry.name);
71
- if (entry.name === "token") {
72
- const destDir = path.join(home, "sources", "chatgpt");
73
- await fs.mkdir(destDir, { recursive: true });
74
- const dest = path.join(destDir, "token");
75
- try {
76
- await fs.access(dest);
77
- } catch {
78
- await fs.rename(from, dest).catch(async () => {
79
- await fs.copyFile(from, dest);
80
- });
81
- }
82
- continue;
83
- }
84
- if (entry.name === "data") {
85
- const dest = path.join(home, "chatgpt");
86
- try {
87
- await fs.access(dest);
88
- } catch {
89
- await fs.rename(from, dest).catch(() => void 0);
90
- }
91
- continue;
92
- }
93
- if (entry.name === "chromium" || entry.name === "chatgpt") {
94
- const dest = path.join(home, entry.name);
95
- try {
96
- await fs.access(dest);
97
- } catch {
98
- await fs.rename(from, dest).catch(() => void 0);
99
- }
100
- }
101
- }
102
- } catch {
103
- }
104
- }
105
- const flatToken = path.join(home, "token");
106
- const nestedToken = getSourceTokenPath("chatgpt");
107
- try {
108
- await fs.access(flatToken);
109
- await fs.mkdir(path.dirname(nestedToken), { recursive: true });
110
- try {
111
- await fs.access(nestedToken);
112
- } catch {
113
- await fs.rename(flatToken, nestedToken).catch(async () => {
114
- await fs.copyFile(flatToken, nestedToken);
115
- });
116
- }
117
- } catch {
118
- }
119
- }
120
-
121
- // src/utils/cdp.ts
122
- async function cdpHttp(port, path2, options = {}) {
123
- const timeoutMs = options.timeoutMs ?? 2e3;
124
- const url = `http://127.0.0.1:${port}${path2}`;
125
- const controller = new AbortController();
126
- const timer = setTimeout(() => controller.abort(), timeoutMs);
127
- try {
128
- const response = await fetch(url, {
129
- method: options.method ?? "GET",
130
- signal: controller.signal,
131
- headers: { Accept: "application/json" }
132
- });
133
- if (!response.ok) {
134
- throw new Error(`CDP HTTP ${response.status} for ${path2}`);
135
- }
136
- const text = await response.text();
137
- if (!text) {
138
- return void 0;
139
- }
140
- try {
141
- return JSON.parse(text);
142
- } catch {
143
- if (options.allowPlainText) {
144
- return text;
145
- }
146
- throw new Error(`CDP HTTP non-JSON response for ${path2}: ${text.slice(0, 80)}`);
147
- }
148
- } finally {
149
- clearTimeout(timer);
150
- }
151
- }
152
- async function isCdpRunning(port = DEFAULT_CDP_PORT, options = {}) {
153
- const info = await getCdpEndpointInfo(port, options);
154
- return info !== null;
155
- }
156
- async function discoverCdpPort(preferred = DEFAULT_CDP_PORT) {
157
- const fromEnv = (process.env.PROMPT_EXPORTER_CDP_PORT || process.env.CHATGPT_EXPORTER_CDP_PORT)?.trim();
158
- const candidates = [
159
- fromEnv ? Number.parseInt(fromEnv, 10) : NaN,
160
- preferred,
161
- 9222,
162
- 9223,
163
- 9229,
164
- 9333
165
- ].filter((n) => Number.isFinite(n) && n > 0 && n <= 65535);
166
- const seen = /* @__PURE__ */ new Set();
167
- for (const port of candidates) {
168
- if (seen.has(port)) continue;
169
- seen.add(port);
170
- if (await isCdpRunning(port)) return port;
171
- }
172
- return null;
173
- }
174
- async function getCdpEndpointInfo(port = DEFAULT_CDP_PORT, options = {}) {
175
- try {
176
- const data = await cdpHttp(port, "/json/version", options);
177
- return {
178
- port,
179
- webSocketDebuggerUrl: typeof data.webSocketDebuggerUrl === "string" ? data.webSocketDebuggerUrl : void 0,
180
- Browser: typeof data.Browser === "string" ? data.Browser : void 0,
181
- "Protocol-Version": typeof data["Protocol-Version"] === "string" ? data["Protocol-Version"] : void 0
182
- };
183
- } catch {
184
- return null;
185
- }
186
- }
187
- function isChatGptUrl(url) {
188
- try {
189
- const parsed = new URL(url);
190
- return parsed.hostname === "chatgpt.com" || parsed.hostname === "www.chatgpt.com" || parsed.hostname === "chat.openai.com";
191
- } catch {
192
- return false;
193
- }
194
- }
195
- function isLumoUrl(url) {
196
- try {
197
- const parsed = new URL(url);
198
- return parsed.hostname === "lumo.proton.me" || parsed.hostname.endsWith(".lumo.proton.me");
199
- } catch {
200
- return false;
201
- }
202
- }
203
- function parseTarget(raw) {
204
- if (typeof raw.id !== "string") return null;
205
- return {
206
- id: raw.id,
207
- type: typeof raw.type === "string" ? raw.type : "unknown",
208
- title: typeof raw.title === "string" ? raw.title : "",
209
- url: typeof raw.url === "string" ? raw.url : "",
210
- webSocketDebuggerUrl: typeof raw.webSocketDebuggerUrl === "string" ? raw.webSocketDebuggerUrl : void 0
211
- };
212
- }
213
- async function listCdpTargets(port) {
214
- const data = await cdpHttp(port, "/json/list");
215
- if (!Array.isArray(data)) return [];
216
- return data.map((item) => parseTarget(item)).filter((t) => t !== null);
217
- }
218
- async function openCdpTab(port, url) {
219
- const path2 = `/json/new?${encodeURIComponent(url)}`;
220
- const data = await cdpHttp(port, path2, { method: "PUT" }).catch(
221
- async () => cdpHttp(port, path2)
222
- );
223
- const target = parseTarget(data);
224
- if (!target) {
225
- throw new Error("CDP /json/new did not return a target");
226
- }
227
- return target;
228
- }
229
- async function activateCdpTarget(port, id) {
230
- await cdpHttp(port, `/json/activate/${encodeURIComponent(id)}`, {
231
- allowPlainText: true
232
- });
233
- }
234
- function getWebSocket() {
235
- const WS = globalThis.WebSocket;
236
- if (!WS) {
237
- throw new Error(
238
- "WebSocket is not available in this Node runtime (need Node 22+ or a WebSocket polyfill)"
239
- );
240
- }
241
- return WS;
242
- }
243
- async function withCdpSocket(webSocketDebuggerUrl, fn) {
244
- const WS = getWebSocket();
245
- const ws = new WS(webSocketDebuggerUrl);
246
- let nextId = 1;
247
- const pending = /* @__PURE__ */ new Map();
248
- const send = (method, params) => {
249
- const id = nextId++;
250
- return new Promise((resolve, reject) => {
251
- pending.set(id, { resolve, reject });
252
- ws.send(JSON.stringify({ id, method, params }));
253
- });
254
- };
255
- try {
256
- await new Promise((resolve, reject) => {
257
- const timer = setTimeout(() => reject(new Error("CDP WebSocket connect timeout")), 5e3);
258
- ws.addEventListener("open", () => {
259
- clearTimeout(timer);
260
- resolve();
261
- });
262
- ws.addEventListener("error", () => {
263
- clearTimeout(timer);
264
- reject(new Error("CDP WebSocket connection failed"));
265
- });
266
- });
267
- ws.addEventListener("message", (event) => {
268
- const raw = typeof event.data === "string" ? event.data : String(event.data);
269
- let msg;
270
- try {
271
- msg = JSON.parse(raw);
272
- } catch {
273
- return;
274
- }
275
- const id = msg.id;
276
- if (typeof id !== "number") return;
277
- const waiter = pending.get(id);
278
- if (!waiter) return;
279
- pending.delete(id);
280
- if (msg.error) {
281
- const err = msg.error;
282
- waiter.reject(
283
- new Error(typeof err.message === "string" ? err.message : "CDP error")
284
- );
285
- } else {
286
- waiter.resolve(msg.result ?? {});
287
- }
288
- });
289
- return await fn(send);
290
- } finally {
291
- for (const [, waiter] of pending) {
292
- waiter.reject(new Error("CDP WebSocket closed"));
293
- }
294
- pending.clear();
295
- ws.close();
296
- }
297
- }
298
- async function cdpNavigate(webSocketDebuggerUrl, url) {
299
- const WS = getWebSocket();
300
- const ws = new WS(webSocketDebuggerUrl);
301
- let nextId = 1;
302
- const pending = /* @__PURE__ */ new Map();
303
- const send = (method, params) => {
304
- const id = nextId++;
305
- return new Promise((resolve, reject) => {
306
- pending.set(id, { resolve, reject });
307
- ws.send(JSON.stringify({ id, method, params }));
308
- });
309
- };
310
- try {
311
- await new Promise((resolve, reject) => {
312
- const timer = setTimeout(() => reject(new Error("CDP WebSocket connect timeout")), 5e3);
313
- ws.addEventListener("open", () => {
314
- clearTimeout(timer);
315
- resolve();
316
- });
317
- ws.addEventListener("error", () => {
318
- clearTimeout(timer);
319
- reject(new Error("CDP WebSocket connection failed"));
320
- });
321
- });
322
- let loadResolve;
323
- const loadPromise = new Promise((resolve) => {
324
- loadResolve = resolve;
325
- });
326
- ws.addEventListener("message", (event) => {
327
- const raw = typeof event.data === "string" ? event.data : String(event.data);
328
- let msg;
329
- try {
330
- msg = JSON.parse(raw);
331
- } catch {
332
- return;
333
- }
334
- if (msg.method === "Page.loadEventFired") {
335
- loadResolve?.();
336
- }
337
- const id = msg.id;
338
- if (typeof id !== "number") return;
339
- const waiter = pending.get(id);
340
- if (!waiter) return;
341
- pending.delete(id);
342
- if (msg.error) {
343
- const err = msg.error;
344
- waiter.reject(
345
- new Error(typeof err.message === "string" ? err.message : "CDP error")
346
- );
347
- } else {
348
- waiter.resolve(msg.result ?? {});
349
- }
350
- });
351
- await send("Page.enable");
352
- await send("Page.navigate", { url });
353
- await Promise.race([
354
- loadPromise,
355
- new Promise((r) => setTimeout(r, 5e3))
356
- ]);
357
- await new Promise((r) => setTimeout(r, 300));
358
- } finally {
359
- for (const [, waiter] of pending) {
360
- waiter.reject(new Error("CDP WebSocket closed"));
361
- }
362
- pending.clear();
363
- ws.close();
364
- }
365
- }
366
- async function ensureChatGptSessionTab(port, sessionUrl) {
367
- const targets = await listCdpTargets(port);
368
- const pages = targets.filter((t) => t.type === "page" || t.type === "tab");
369
- const existing = pages.find((t) => t.url.startsWith(sessionUrl)) ?? pages.find((t) => isChatGptUrl(t.url));
370
- if (existing) {
371
- await activateCdpTarget(port, existing.id);
372
- if (existing.webSocketDebuggerUrl && existing.url !== sessionUrl) {
373
- await cdpNavigate(existing.webSocketDebuggerUrl, sessionUrl);
374
- const refreshed = (await listCdpTargets(port)).find((t) => t.id === existing.id);
375
- return { target: refreshed ?? existing, reused: true };
376
- }
377
- if (existing.url !== sessionUrl) {
378
- const created2 = await openCdpTab(port, sessionUrl);
379
- return { target: created2, reused: true };
380
- }
381
- return { target: existing, reused: true };
382
- }
383
- const created = await openCdpTab(port, sessionUrl);
384
- return { target: created, reused: false };
385
- }
386
- async function extractAccessTokenViaCdp(webSocketDebuggerUrl, options = {}) {
387
- const attempts = options.attempts ?? 5;
388
- const delayMs = options.delayMs ?? 400;
389
- return withCdpSocket(webSocketDebuggerUrl, async (send) => {
390
- await send("Runtime.enable");
391
- for (let i = 0; i < attempts; i++) {
392
- const result = await send("Runtime.evaluate", {
393
- expression: `(() => {
394
- const readBody = () => {
395
- const text = document.body && document.body.innerText;
396
- if (!text) return null;
397
- try {
398
- const parsed = JSON.parse(text.trim());
399
- if (parsed && typeof parsed.accessToken === 'string') return parsed.accessToken;
400
- } catch {}
401
- return null;
402
- };
403
- const fromBody = readBody();
404
- if (fromBody) return Promise.resolve(fromBody);
405
- return fetch('https://chatgpt.com/api/auth/session', { credentials: 'include' })
406
- .then((r) => r.json())
407
- .then((j) => (j && typeof j.accessToken === 'string' ? j.accessToken : null))
408
- .catch(() => null);
409
- })()`,
410
- awaitPromise: true,
411
- returnByValue: true
412
- });
413
- const remote = result.result;
414
- const value = remote?.value;
415
- if (typeof value === "string" && value.startsWith("eyJ")) {
416
- return value;
417
- }
418
- if (i < attempts - 1) {
419
- await new Promise((r) => setTimeout(r, delayMs));
420
- }
421
- }
422
- return null;
423
- });
424
- }
425
-
426
- export {
427
- getSourceDataDir,
428
- getDefaultDataDir,
429
- getSourceTokenPath,
430
- resolveChromiumUserDataDir,
431
- DEFAULT_CDP_PORT,
432
- DEFAULT_SOURCE_ID,
433
- ensureHomeLayout,
434
- isCdpRunning,
435
- discoverCdpPort,
436
- getCdpEndpointInfo,
437
- isChatGptUrl,
438
- isLumoUrl,
439
- listCdpTargets,
440
- openCdpTab,
441
- activateCdpTarget,
442
- withCdpSocket,
443
- cdpNavigate,
444
- ensureChatGptSessionTab,
445
- extractAccessTokenViaCdp
446
- };