prompt-exporter 2.0.1 → 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
@@ -17,6 +17,8 @@ src/
17
17
  types.ts # Source interface
18
18
  registry.ts
19
19
  chatgpt/ # chatgpt source (api/, index.ts)
20
+ lumo/ # lumo.proton.me (CDP Redux export)
21
+ perplexity/ # perplexity.ai (REST via CDP cookies)
20
22
  services/ # storage, markdown, backup (source-agnostic)
21
23
  utils/ # paths, credentials, cdp, browser
22
24
  aur/
@@ -25,74 +27,27 @@ aur/
25
27
  publish.sh
26
28
  ```
27
29
 
28
- ## Release (2.0.x)
30
+ ## Build output
29
31
 
30
- `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):
31
33
 
32
- ### 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
33
38
 
34
- ```bash
35
- cd /path/to/prompt-exporter
36
- git push origin master
37
-
38
- git tag -a v2.0.0 -m "prompt-exporter 2.0.0"
39
- git push origin v2.0.0
40
- ```
41
-
42
- ### 2. GitHub Release
43
-
44
- ```bash
45
- # If gh is installed:
46
- gh release create v2.0.0 --title "2.0.0" --notes-file - <<'EOF'
47
- See CHANGELOG.md for breaking changes (rebrand, Apache-2.0, multi-source layout).
48
- EOF
49
-
50
- # Or: GitHub → Releases → Draft from tag v2.0.0 → paste CHANGELOG 2.0.0 section
51
- ```
52
-
53
- ### 3. Fill AUR stable checksum
54
-
55
- ```bash
56
- TARBALL_URL="https://github.com/azbarcea/prompt-exporter/archive/refs/tags/v2.0.0.tar.gz"
57
- SUM=$(curl -sL "$TARBALL_URL" | sha256sum | awk '{print $1}')
58
- echo "$SUM"
59
-
60
- # Update aur/prompt-exporter/PKGBUILD sha256sums and regenerate .SRCINFO:
61
- sed -i "s/sha256sums=.*/sha256sums=('${SUM}')/" aur/prompt-exporter/PKGBUILD
62
- ( 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.
63
40
 
64
- git add aur/prompt-exporter
65
- git commit -m "chore(aur): set sha256 for prompt-exporter 2.0.0"
66
- git push origin master
67
- ```
68
-
69
- ### 4. Publish AUR packages
70
-
71
- Requires SSH key registered on [AUR](https://aur.archlinux.org) (`ssh aur@aur.archlinux.org`).
72
-
73
- ```bash
74
- # First-time package names (if empty clone fails, publish.sh inits and pushes):
75
- ./aur/publish.sh prompt-exporter # stable 2.0.0
76
- ./aur/publish.sh prompt-exporter-git # rolling
77
- ```
78
-
79
- 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.
80
-
81
- ### 5. Optional: npm registry
82
-
83
- Not required (install via `github:azbarcea/prompt-exporter` works). To publish:
84
-
85
- ```bash
86
- npm login
87
- npm publish --access public
88
- ```
41
+ ## Release
89
42
 
90
- ### 6. Smoke after install
43
+ Ship with the Makefile (order: **npmjs → GitHub → AUR**):
91
44
 
92
45
  ```bash
93
- yay -S prompt-exporter
94
- prompt-exporter --version # 2.0.0
95
- 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
96
51
  ```
97
52
 
98
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 source is **chatgpt** (chatgpt.com). Planned next: Claude, Gemini, Copilot, Perplexity, and Grok.
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
@@ -89,10 +99,34 @@ prompt-exporter sync --no-incremental # force full re-download
89
99
  prompt-exporter sources
90
100
  ```
91
101
 
102
+ ### Lumo (Proton)
103
+
104
+ Lumo encrypts payloads on the server. Sync reads the **decrypted** in-app Redux store over CDP after login:
105
+
106
+ ```bash
107
+ prompt-exporter chromium start --url https://lumo.proton.me/
108
+ # log in; wait until chats appear in the sidebar
109
+ prompt-exporter sync --source lumo
110
+ prompt-exporter list --source lumo
111
+ ```
112
+
92
113
  `sync` is incremental by default (skips unchanged `update_time`).
93
114
 
94
115
  Auth prefers a logged-in Chromium CDP session. Node Bearer alone is often blocked by Cloudflare.
95
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
+
96
130
  ## Commands
97
131
 
98
132
  ```text
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "prompt-exporter",
3
- "version": "2.0.1",
4
- "description": "CLI to sync AI conversation prompts from multiple sources (ChatGPT first) 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",
@@ -17,16 +17,16 @@
17
17
  "README.md"
18
18
  ],
19
19
  "engines": {
20
- "node": ">=20"
20
+ "node": ">=24"
21
21
  },
22
22
  "scripts": {
23
- "dev": "tsx src/index.ts",
24
- "build": "tsup src/index.ts --format esm --dts --clean",
25
- "start": "node dist/index.js",
23
+ "dev": "node --disable-warning=DEP0205 --import tsx src/index.ts",
24
+ "build": "tsup",
25
+ "start": "node dist/index.cjs",
26
26
  "prepublishOnly": "npm run build",
27
27
  "typecheck": "tsc --noEmit",
28
- "test": "npm run build && tsx --test tests/*.test.ts",
29
- "test:unit": "tsx --test tests/*.test.ts",
28
+ "test": "npm run build && node --disable-warning=DEP0205 --import tsx --test tests/*.test.ts",
29
+ "test:unit": "node --disable-warning=DEP0205 --import tsx --test tests/*.test.ts",
30
30
  "ci": "npm run typecheck && npm run test"
31
31
  },
32
32
  "repository": {
@@ -1,24 +0,0 @@
1
- import {
2
- activateCdpTarget,
3
- cdpNavigate,
4
- discoverCdpPort,
5
- ensureChatGptSessionTab,
6
- extractAccessTokenViaCdp,
7
- getCdpEndpointInfo,
8
- isCdpRunning,
9
- isChatGptUrl,
10
- listCdpTargets,
11
- openCdpTab
12
- } from "./chunk-YHAS76TP.js";
13
- export {
14
- activateCdpTarget,
15
- cdpNavigate,
16
- discoverCdpPort,
17
- ensureChatGptSessionTab,
18
- extractAccessTokenViaCdp,
19
- getCdpEndpointInfo,
20
- isCdpRunning,
21
- isChatGptUrl,
22
- listCdpTargets,
23
- openCdpTab
24
- };
@@ -1,436 +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 parseTarget(raw) {
196
- if (typeof raw.id !== "string") return null;
197
- return {
198
- id: raw.id,
199
- type: typeof raw.type === "string" ? raw.type : "unknown",
200
- title: typeof raw.title === "string" ? raw.title : "",
201
- url: typeof raw.url === "string" ? raw.url : "",
202
- webSocketDebuggerUrl: typeof raw.webSocketDebuggerUrl === "string" ? raw.webSocketDebuggerUrl : void 0
203
- };
204
- }
205
- async function listCdpTargets(port) {
206
- const data = await cdpHttp(port, "/json/list");
207
- if (!Array.isArray(data)) return [];
208
- return data.map((item) => parseTarget(item)).filter((t) => t !== null);
209
- }
210
- async function openCdpTab(port, url) {
211
- const path2 = `/json/new?${encodeURIComponent(url)}`;
212
- const data = await cdpHttp(port, path2, { method: "PUT" }).catch(
213
- async () => cdpHttp(port, path2)
214
- );
215
- const target = parseTarget(data);
216
- if (!target) {
217
- throw new Error("CDP /json/new did not return a target");
218
- }
219
- return target;
220
- }
221
- async function activateCdpTarget(port, id) {
222
- await cdpHttp(port, `/json/activate/${encodeURIComponent(id)}`, {
223
- allowPlainText: true
224
- });
225
- }
226
- function getWebSocket() {
227
- const WS = globalThis.WebSocket;
228
- if (!WS) {
229
- throw new Error(
230
- "WebSocket is not available in this Node runtime (need Node 22+ or a WebSocket polyfill)"
231
- );
232
- }
233
- return WS;
234
- }
235
- async function withCdpSocket(webSocketDebuggerUrl, fn) {
236
- const WS = getWebSocket();
237
- const ws = new WS(webSocketDebuggerUrl);
238
- let nextId = 1;
239
- const pending = /* @__PURE__ */ new Map();
240
- const send = (method, params) => {
241
- const id = nextId++;
242
- return new Promise((resolve, reject) => {
243
- pending.set(id, { resolve, reject });
244
- ws.send(JSON.stringify({ id, method, params }));
245
- });
246
- };
247
- try {
248
- await new Promise((resolve, reject) => {
249
- const timer = setTimeout(() => reject(new Error("CDP WebSocket connect timeout")), 5e3);
250
- ws.addEventListener("open", () => {
251
- clearTimeout(timer);
252
- resolve();
253
- });
254
- ws.addEventListener("error", () => {
255
- clearTimeout(timer);
256
- reject(new Error("CDP WebSocket connection failed"));
257
- });
258
- });
259
- ws.addEventListener("message", (event) => {
260
- const raw = typeof event.data === "string" ? event.data : String(event.data);
261
- let msg;
262
- try {
263
- msg = JSON.parse(raw);
264
- } catch {
265
- return;
266
- }
267
- const id = msg.id;
268
- if (typeof id !== "number") return;
269
- const waiter = pending.get(id);
270
- if (!waiter) return;
271
- pending.delete(id);
272
- if (msg.error) {
273
- const err = msg.error;
274
- waiter.reject(
275
- new Error(typeof err.message === "string" ? err.message : "CDP error")
276
- );
277
- } else {
278
- waiter.resolve(msg.result ?? {});
279
- }
280
- });
281
- return await fn(send);
282
- } finally {
283
- for (const [, waiter] of pending) {
284
- waiter.reject(new Error("CDP WebSocket closed"));
285
- }
286
- pending.clear();
287
- ws.close();
288
- }
289
- }
290
- async function cdpNavigate(webSocketDebuggerUrl, url) {
291
- const WS = getWebSocket();
292
- const ws = new WS(webSocketDebuggerUrl);
293
- let nextId = 1;
294
- const pending = /* @__PURE__ */ new Map();
295
- const send = (method, params) => {
296
- const id = nextId++;
297
- return new Promise((resolve, reject) => {
298
- pending.set(id, { resolve, reject });
299
- ws.send(JSON.stringify({ id, method, params }));
300
- });
301
- };
302
- try {
303
- await new Promise((resolve, reject) => {
304
- const timer = setTimeout(() => reject(new Error("CDP WebSocket connect timeout")), 5e3);
305
- ws.addEventListener("open", () => {
306
- clearTimeout(timer);
307
- resolve();
308
- });
309
- ws.addEventListener("error", () => {
310
- clearTimeout(timer);
311
- reject(new Error("CDP WebSocket connection failed"));
312
- });
313
- });
314
- let loadResolve;
315
- const loadPromise = new Promise((resolve) => {
316
- loadResolve = resolve;
317
- });
318
- ws.addEventListener("message", (event) => {
319
- const raw = typeof event.data === "string" ? event.data : String(event.data);
320
- let msg;
321
- try {
322
- msg = JSON.parse(raw);
323
- } catch {
324
- return;
325
- }
326
- if (msg.method === "Page.loadEventFired") {
327
- loadResolve?.();
328
- }
329
- const id = msg.id;
330
- if (typeof id !== "number") return;
331
- const waiter = pending.get(id);
332
- if (!waiter) return;
333
- pending.delete(id);
334
- if (msg.error) {
335
- const err = msg.error;
336
- waiter.reject(
337
- new Error(typeof err.message === "string" ? err.message : "CDP error")
338
- );
339
- } else {
340
- waiter.resolve(msg.result ?? {});
341
- }
342
- });
343
- await send("Page.enable");
344
- await send("Page.navigate", { url });
345
- await Promise.race([
346
- loadPromise,
347
- new Promise((r) => setTimeout(r, 5e3))
348
- ]);
349
- await new Promise((r) => setTimeout(r, 300));
350
- } finally {
351
- for (const [, waiter] of pending) {
352
- waiter.reject(new Error("CDP WebSocket closed"));
353
- }
354
- pending.clear();
355
- ws.close();
356
- }
357
- }
358
- async function ensureChatGptSessionTab(port, sessionUrl) {
359
- const targets = await listCdpTargets(port);
360
- const pages = targets.filter((t) => t.type === "page" || t.type === "tab");
361
- const existing = pages.find((t) => t.url.startsWith(sessionUrl)) ?? pages.find((t) => isChatGptUrl(t.url));
362
- if (existing) {
363
- await activateCdpTarget(port, existing.id);
364
- if (existing.webSocketDebuggerUrl && existing.url !== sessionUrl) {
365
- await cdpNavigate(existing.webSocketDebuggerUrl, sessionUrl);
366
- const refreshed = (await listCdpTargets(port)).find((t) => t.id === existing.id);
367
- return { target: refreshed ?? existing, reused: true };
368
- }
369
- if (existing.url !== sessionUrl) {
370
- const created2 = await openCdpTab(port, sessionUrl);
371
- return { target: created2, reused: true };
372
- }
373
- return { target: existing, reused: true };
374
- }
375
- const created = await openCdpTab(port, sessionUrl);
376
- return { target: created, reused: false };
377
- }
378
- async function extractAccessTokenViaCdp(webSocketDebuggerUrl, options = {}) {
379
- const attempts = options.attempts ?? 5;
380
- const delayMs = options.delayMs ?? 400;
381
- return withCdpSocket(webSocketDebuggerUrl, async (send) => {
382
- await send("Runtime.enable");
383
- for (let i = 0; i < attempts; i++) {
384
- const result = await send("Runtime.evaluate", {
385
- expression: `(() => {
386
- const readBody = () => {
387
- const text = document.body && document.body.innerText;
388
- if (!text) return null;
389
- try {
390
- const parsed = JSON.parse(text.trim());
391
- if (parsed && typeof parsed.accessToken === 'string') return parsed.accessToken;
392
- } catch {}
393
- return null;
394
- };
395
- const fromBody = readBody();
396
- if (fromBody) return Promise.resolve(fromBody);
397
- return fetch('https://chatgpt.com/api/auth/session', { credentials: 'include' })
398
- .then((r) => r.json())
399
- .then((j) => (j && typeof j.accessToken === 'string' ? j.accessToken : null))
400
- .catch(() => null);
401
- })()`,
402
- awaitPromise: true,
403
- returnByValue: true
404
- });
405
- const remote = result.result;
406
- const value = remote?.value;
407
- if (typeof value === "string" && value.startsWith("eyJ")) {
408
- return value;
409
- }
410
- if (i < attempts - 1) {
411
- await new Promise((r) => setTimeout(r, delayMs));
412
- }
413
- }
414
- return null;
415
- });
416
- }
417
-
418
- export {
419
- getSourceDataDir,
420
- getDefaultDataDir,
421
- getSourceTokenPath,
422
- resolveChromiumUserDataDir,
423
- DEFAULT_CDP_PORT,
424
- DEFAULT_SOURCE_ID,
425
- ensureHomeLayout,
426
- isCdpRunning,
427
- discoverCdpPort,
428
- getCdpEndpointInfo,
429
- isChatGptUrl,
430
- listCdpTargets,
431
- openCdpTab,
432
- activateCdpTarget,
433
- cdpNavigate,
434
- ensureChatGptSessionTab,
435
- extractAccessTokenViaCdp
436
- };