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/README.md +3 -1
- package/dist/index.cjs +523 -0
- package/docs/DEVELOPER.md +15 -61
- package/docs/USER_GUIDE.md +24 -1
- package/package.json +6 -6
- package/dist/cdp-5CBMWFGT.js +0 -28
- package/dist/chunk-O5E5XFJR.js +0 -446
- package/dist/index.js +0 -3441
- /package/dist/{index.d.ts → index.d.cts} +0 -0
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
|
-
##
|
|
30
|
+
## Build output
|
|
30
31
|
|
|
31
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
Ship with the Makefile (order: **npmjs → GitHub → AUR**):
|
|
92
44
|
|
|
93
45
|
```bash
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
package/docs/USER_GUIDE.md
CHANGED
|
@@ -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)
|
|
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.
|
|
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.
|
|
8
|
+
"main": "dist/index.cjs",
|
|
9
9
|
"bin": {
|
|
10
|
-
"prompt-exporter": "dist/index.
|
|
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
|
|
25
|
-
"start": "node dist/index.
|
|
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",
|
package/dist/cdp-5CBMWFGT.js
DELETED
|
@@ -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
|
-
};
|
package/dist/chunk-O5E5XFJR.js
DELETED
|
@@ -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
|
-
};
|