prompt-exporter 2.0.1

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,436 @@
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
+ };
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node