prompt-exporter 2.0.1 → 2.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.
- package/README.md +5 -2
- package/dist/{cdp-PRMCSQGL.js → cdp-5CBMWFGT.js} +7 -3
- package/dist/{chunk-YHAS76TP.js → chunk-O5E5XFJR.js} +10 -0
- package/dist/index.js +549 -69
- package/docs/DEVELOPER.md +1 -0
- package/docs/USER_GUIDE.md +12 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -7,11 +7,14 @@ CLI to **sync AI conversation prompts** from multiple sources to your machine as
|
|
|
7
7
|
**Sources**:
|
|
8
8
|
|
|
9
9
|
- ✅ `chatgpt` — [chatgpt.com](https://chatgpt.com/) history via Chromium CDP
|
|
10
|
+
- ✅ `lumo` — [lumo.proton.me](https://lumo.proton.me/) (Proton) via Chromium CDP / in-app Redux
|
|
10
11
|
- 🎯 `claude` — [claude.ai](https://claude.ai/) (planned)
|
|
11
12
|
- 🎯 `gemini` — [gemini.google.com](https://gemini.google.com/) (planned)
|
|
12
|
-
- 🎯 `
|
|
13
|
+
- 🎯 `deepseek` — [chat.deepseek.com](https://chat.deepseek.com/) (planned)
|
|
13
14
|
- 🎯 `perplexity` — [perplexity.ai](https://www.perplexity.ai/) (planned)
|
|
14
|
-
- 🎯 `
|
|
15
|
+
- 🎯 `copilot` — Microsoft 365 Copilot ([m365.cloud.microsoft](https://m365.cloud.microsoft/chat/) / [copilot.microsoft.com](https://copilot.microsoft.com/)) (planned)
|
|
16
|
+
- 🎯 `grok` — [grok.com](https://grok.com/) / xAI (planned)
|
|
17
|
+
- 🎯 `kimi` — [kimi.com](https://www.kimi.com/) / Moonshot (planned)
|
|
15
18
|
|
|
16
19
|
**Platforms**:
|
|
17
20
|
|
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
getCdpEndpointInfo,
|
|
8
8
|
isCdpRunning,
|
|
9
9
|
isChatGptUrl,
|
|
10
|
+
isLumoUrl,
|
|
10
11
|
listCdpTargets,
|
|
11
|
-
openCdpTab
|
|
12
|
-
|
|
12
|
+
openCdpTab,
|
|
13
|
+
withCdpSocket
|
|
14
|
+
} from "./chunk-O5E5XFJR.js";
|
|
13
15
|
export {
|
|
14
16
|
activateCdpTarget,
|
|
15
17
|
cdpNavigate,
|
|
@@ -19,6 +21,8 @@ export {
|
|
|
19
21
|
getCdpEndpointInfo,
|
|
20
22
|
isCdpRunning,
|
|
21
23
|
isChatGptUrl,
|
|
24
|
+
isLumoUrl,
|
|
22
25
|
listCdpTargets,
|
|
23
|
-
openCdpTab
|
|
26
|
+
openCdpTab,
|
|
27
|
+
withCdpSocket
|
|
24
28
|
};
|
|
@@ -192,6 +192,14 @@ function isChatGptUrl(url) {
|
|
|
192
192
|
return false;
|
|
193
193
|
}
|
|
194
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
|
+
}
|
|
195
203
|
function parseTarget(raw) {
|
|
196
204
|
if (typeof raw.id !== "string") return null;
|
|
197
205
|
return {
|
|
@@ -427,9 +435,11 @@ export {
|
|
|
427
435
|
discoverCdpPort,
|
|
428
436
|
getCdpEndpointInfo,
|
|
429
437
|
isChatGptUrl,
|
|
438
|
+
isLumoUrl,
|
|
430
439
|
listCdpTargets,
|
|
431
440
|
openCdpTab,
|
|
432
441
|
activateCdpTarget,
|
|
442
|
+
withCdpSocket,
|
|
433
443
|
cdpNavigate,
|
|
434
444
|
ensureChatGptSessionTab,
|
|
435
445
|
extractAccessTokenViaCdp
|
package/dist/index.js
CHANGED
|
@@ -13,10 +13,12 @@ import {
|
|
|
13
13
|
getSourceTokenPath,
|
|
14
14
|
isCdpRunning,
|
|
15
15
|
isChatGptUrl,
|
|
16
|
+
isLumoUrl,
|
|
16
17
|
listCdpTargets,
|
|
17
18
|
openCdpTab,
|
|
18
|
-
resolveChromiumUserDataDir
|
|
19
|
-
|
|
19
|
+
resolveChromiumUserDataDir,
|
|
20
|
+
withCdpSocket
|
|
21
|
+
} from "./chunk-O5E5XFJR.js";
|
|
20
22
|
|
|
21
23
|
// src/cli/index.ts
|
|
22
24
|
import { Command } from "commander";
|
|
@@ -285,7 +287,7 @@ var CdpHttpSession = class _CdpHttpSession {
|
|
|
285
287
|
}
|
|
286
288
|
static async connect(preferredPort = DEFAULT_CDP_PORT, options = {}) {
|
|
287
289
|
let port = preferredPort;
|
|
288
|
-
const { isCdpRunning: isCdpRunning2 } = await import("./cdp-
|
|
290
|
+
const { isCdpRunning: isCdpRunning2 } = await import("./cdp-5CBMWFGT.js");
|
|
289
291
|
if (!await isCdpRunning2(port)) {
|
|
290
292
|
const found = await discoverCdpPort(port);
|
|
291
293
|
if (found === null) {
|
|
@@ -1550,8 +1552,14 @@ async function convertDirectory(inputDir, filesDir) {
|
|
|
1550
1552
|
await removeStaleMarkdown(conversationsDir, id, mdName);
|
|
1551
1553
|
await fs4.writeFile(mdPath, markdown, "utf-8");
|
|
1552
1554
|
converted++;
|
|
1553
|
-
} catch {
|
|
1555
|
+
} catch (err) {
|
|
1554
1556
|
errors++;
|
|
1557
|
+
if (process.env.PROMPT_EXPORTER_DEBUG_MD === "1") {
|
|
1558
|
+
console.error(
|
|
1559
|
+
`markdown convert failed for ${jsonPath}:`,
|
|
1560
|
+
err instanceof Error ? err.message : err
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1555
1563
|
}
|
|
1556
1564
|
}
|
|
1557
1565
|
for (const dir of touchedDirs) {
|
|
@@ -1998,11 +2006,452 @@ async function runFileDownloads(client, output, options) {
|
|
|
1998
2006
|
}
|
|
1999
2007
|
}
|
|
2000
2008
|
|
|
2009
|
+
// src/cli/commands/backup-lumo.ts
|
|
2010
|
+
import chalk3 from "chalk";
|
|
2011
|
+
import ora2 from "ora";
|
|
2012
|
+
|
|
2013
|
+
// src/sources/lumo/api/endpoints.ts
|
|
2014
|
+
var LUMO_ORIGIN = "https://lumo.proton.me";
|
|
2015
|
+
var LUMO_HOME = `${LUMO_ORIGIN}/`;
|
|
2016
|
+
|
|
2017
|
+
// src/sources/lumo/api/cdp-export.ts
|
|
2018
|
+
var EXPORT_EXPRESSION = `(() => {
|
|
2019
|
+
const PULL_TIMEOUT_MS = 45000;
|
|
2020
|
+
const PULL_POLL_MS = 250;
|
|
2021
|
+
const INIT_WAIT_MS = 90000;
|
|
2022
|
+
|
|
2023
|
+
function sleep(ms) {
|
|
2024
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
function findReduxStore() {
|
|
2028
|
+
if (window.lumoStore && typeof window.lumoStore.getState === 'function') {
|
|
2029
|
+
return window.lumoStore;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
const tryFiber = (fiber, depth) => {
|
|
2033
|
+
if (!fiber || depth > 80) return null;
|
|
2034
|
+
const props = fiber.memoizedProps || fiber.pendingProps;
|
|
2035
|
+
if (props && props.store && typeof props.store.getState === 'function') {
|
|
2036
|
+
return props.store;
|
|
2037
|
+
}
|
|
2038
|
+
if (props && props.value && props.value.store && typeof props.value.store.getState === 'function') {
|
|
2039
|
+
return props.value.store;
|
|
2040
|
+
}
|
|
2041
|
+
let hook = fiber.memoizedState;
|
|
2042
|
+
for (let i = 0; hook && i < 100; i++) {
|
|
2043
|
+
const ms = hook.memoizedState;
|
|
2044
|
+
if (ms && ms.store && typeof ms.store.getState === 'function') return ms.store;
|
|
2045
|
+
if (ms && typeof ms.getState === 'function' && typeof ms.dispatch === 'function') return ms;
|
|
2046
|
+
hook = hook.next;
|
|
2047
|
+
}
|
|
2048
|
+
return tryFiber(fiber.child, depth + 1) || tryFiber(fiber.sibling, depth + 1);
|
|
2049
|
+
};
|
|
2050
|
+
|
|
2051
|
+
// Lumo has no #root; React mounts on an inner DIV (__reactContainer$).
|
|
2052
|
+
const nodes = document.querySelectorAll('*');
|
|
2053
|
+
for (const el of nodes) {
|
|
2054
|
+
const key = Object.keys(el).find((k) => k.startsWith('__reactContainer'));
|
|
2055
|
+
if (!key) continue;
|
|
2056
|
+
let fiber = el[key];
|
|
2057
|
+
if (fiber && fiber.stateNode) fiber = fiber.stateNode;
|
|
2058
|
+
if (fiber && fiber.child) fiber = fiber.child;
|
|
2059
|
+
const store = tryFiber(fiber, 0);
|
|
2060
|
+
if (store) return store;
|
|
2061
|
+
}
|
|
2062
|
+
for (const el of nodes) {
|
|
2063
|
+
const key = Object.keys(el).find((k) => k.startsWith('__reactFiber'));
|
|
2064
|
+
if (!key) continue;
|
|
2065
|
+
const store = tryFiber(el[key], 0);
|
|
2066
|
+
if (store) return store;
|
|
2067
|
+
}
|
|
2068
|
+
return null;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
function messageText(m) {
|
|
2072
|
+
if (!m) return '';
|
|
2073
|
+
if (typeof m.content === 'string' && m.content.trim()) return m.content;
|
|
2074
|
+
if (Array.isArray(m.blocks)) {
|
|
2075
|
+
return m.blocks
|
|
2076
|
+
.filter((b) => b && b.type === 'text' && typeof b.content === 'string')
|
|
2077
|
+
.map((b) => b.content)
|
|
2078
|
+
.join('\\n')
|
|
2079
|
+
.trim();
|
|
2080
|
+
}
|
|
2081
|
+
return '';
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
function roleOf(m) {
|
|
2085
|
+
const r = (m && m.role) || 'assistant';
|
|
2086
|
+
if (r === 'user' || r === 'assistant' || r === 'system') return r;
|
|
2087
|
+
if (r === 'tool_call' || r === 'tool_result') return 'assistant';
|
|
2088
|
+
return String(r);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function toEpoch(iso) {
|
|
2092
|
+
if (!iso) return 0;
|
|
2093
|
+
const t = Date.parse(iso);
|
|
2094
|
+
return Number.isFinite(t) ? Math.floor(t / 1000) : 0;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function buildDetail(conversation, messages) {
|
|
2098
|
+
const byId = {};
|
|
2099
|
+
for (const m of messages) {
|
|
2100
|
+
byId[m.id] = m;
|
|
2101
|
+
}
|
|
2102
|
+
const mapping = {};
|
|
2103
|
+
const roots = [];
|
|
2104
|
+
for (const m of messages) {
|
|
2105
|
+
const parent = m.parentId && byId[m.parentId] ? m.parentId : null;
|
|
2106
|
+
mapping[m.id] = {
|
|
2107
|
+
id: m.id,
|
|
2108
|
+
parent,
|
|
2109
|
+
children: [],
|
|
2110
|
+
message: {
|
|
2111
|
+
id: m.id,
|
|
2112
|
+
author: { role: roleOf(m) },
|
|
2113
|
+
content: { content_type: 'text', parts: [messageText(m)] },
|
|
2114
|
+
create_time: toEpoch(m.createdAt),
|
|
2115
|
+
},
|
|
2116
|
+
};
|
|
2117
|
+
if (!parent) roots.push(m.id);
|
|
2118
|
+
}
|
|
2119
|
+
for (const m of messages) {
|
|
2120
|
+
if (m.parentId && mapping[m.parentId] && mapping[m.id]) {
|
|
2121
|
+
mapping[m.parentId].children.push(m.id);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
// synthetic root if needed (ChatGPT-style)
|
|
2125
|
+
let current = null;
|
|
2126
|
+
if (messages.length) {
|
|
2127
|
+
const leaves = messages.filter((m) => !(mapping[m.id].children || []).length);
|
|
2128
|
+
const last = leaves.sort((a, b) => toEpoch(a.createdAt) - toEpoch(b.createdAt)).pop();
|
|
2129
|
+
current = last ? last.id : messages[messages.length - 1].id;
|
|
2130
|
+
}
|
|
2131
|
+
if (roots.length === 0 && messages.length) {
|
|
2132
|
+
const rootId = 'client-created-root';
|
|
2133
|
+
mapping[rootId] = {
|
|
2134
|
+
id: rootId,
|
|
2135
|
+
parent: null,
|
|
2136
|
+
children: messages.filter((m) => !m.parentId).map((m) => m.id),
|
|
2137
|
+
message: null,
|
|
2138
|
+
};
|
|
2139
|
+
for (const m of messages) {
|
|
2140
|
+
if (!m.parentId && mapping[m.id]) mapping[m.id].parent = rootId;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
return {
|
|
2145
|
+
id: conversation.id,
|
|
2146
|
+
title: conversation.title || 'Untitled',
|
|
2147
|
+
create_time: toEpoch(conversation.createdAt),
|
|
2148
|
+
update_time: toEpoch(conversation.updatedAt || conversation.createdAt),
|
|
2149
|
+
space_id: conversation.spaceId,
|
|
2150
|
+
starred: !!conversation.starred,
|
|
2151
|
+
mapping,
|
|
2152
|
+
current_node: current,
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
async function waitForStore() {
|
|
2157
|
+
const start = Date.now();
|
|
2158
|
+
while (Date.now() - start < INIT_WAIT_MS) {
|
|
2159
|
+
const store = findReduxStore();
|
|
2160
|
+
if (store) {
|
|
2161
|
+
const state = store.getState();
|
|
2162
|
+
const cred = state.credentials && state.credentials.masterKeyState;
|
|
2163
|
+
const convs = state.conversations || {};
|
|
2164
|
+
const init = state.initialization;
|
|
2165
|
+
const readyCred = cred && (cred.status === 'ready' || cred.status === 'ineligible');
|
|
2166
|
+
const hasConvs = Object.keys(convs).length > 0;
|
|
2167
|
+
// Allow empty account once credentials ready / init done
|
|
2168
|
+
if (readyCred || hasConvs || (init && init.status === 'done')) {
|
|
2169
|
+
return store;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
await sleep(400);
|
|
2173
|
+
}
|
|
2174
|
+
return findReduxStore();
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
async function pullConversation(store, id) {
|
|
2178
|
+
const before = Object.values(store.getState().messages || {}).filter(
|
|
2179
|
+
(m) => m.conversationId === id
|
|
2180
|
+
).length;
|
|
2181
|
+
if (before > 0) return;
|
|
2182
|
+
|
|
2183
|
+
store.dispatch({ type: 'lumo/conversation/pullRequest', payload: { id } });
|
|
2184
|
+
const start = Date.now();
|
|
2185
|
+
while (Date.now() - start < PULL_TIMEOUT_MS) {
|
|
2186
|
+
await sleep(PULL_POLL_MS);
|
|
2187
|
+
const n = Object.values(store.getState().messages || {}).filter(
|
|
2188
|
+
(m) => m.conversationId === id
|
|
2189
|
+
).length;
|
|
2190
|
+
if (n > before) return;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
return (async () => {
|
|
2195
|
+
if (!location.hostname.includes('lumo.proton.me')) {
|
|
2196
|
+
return {
|
|
2197
|
+
ok: false,
|
|
2198
|
+
error: 'Not on lumo.proton.me \u2014 open Lumo in this CDP browser and log in.',
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
const store = await waitForStore();
|
|
2202
|
+
if (!store) {
|
|
2203
|
+
return {
|
|
2204
|
+
ok: false,
|
|
2205
|
+
error:
|
|
2206
|
+
'Could not find Lumo Redux store. Open https://lumo.proton.me/, log in, wait for chats to load, then retry.',
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
const warnings = [];
|
|
2211
|
+
const state0 = store.getState();
|
|
2212
|
+
const cred = state0.credentials && state0.credentials.masterKeyState;
|
|
2213
|
+
if (cred && cred.status === 'failed') {
|
|
2214
|
+
return { ok: false, error: 'Lumo master key failed: ' + (cred.message || 'unknown') };
|
|
2215
|
+
}
|
|
2216
|
+
if (cred && cred.status === 'ineligible') {
|
|
2217
|
+
warnings.push('User not eligible for Lumo cloud sync; exporting local Redux state only.');
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
let conversations = Object.values(state0.conversations || {}).filter((c) => c && !c.deleted && !c.ghost);
|
|
2221
|
+
if (conversations.length === 0) {
|
|
2222
|
+
// give hydrate more time
|
|
2223
|
+
await sleep(2000);
|
|
2224
|
+
conversations = Object.values(store.getState().conversations || {}).filter(
|
|
2225
|
+
(c) => c && !c.deleted && !c.ghost
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
conversations.sort(
|
|
2230
|
+
(a, b) =>
|
|
2231
|
+
Date.parse(b.updatedAt || b.createdAt || 0) - Date.parse(a.updatedAt || a.createdAt || 0)
|
|
2232
|
+
);
|
|
2233
|
+
|
|
2234
|
+
for (const c of conversations) {
|
|
2235
|
+
try {
|
|
2236
|
+
await pullConversation(store, c.id);
|
|
2237
|
+
} catch (e) {
|
|
2238
|
+
warnings.push('pull failed for ' + c.id + ': ' + String(e && e.message ? e.message : e));
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
const messagesAll = Object.values(store.getState().messages || {});
|
|
2243
|
+
const items = [];
|
|
2244
|
+
const details = [];
|
|
2245
|
+
for (const c of conversations) {
|
|
2246
|
+
const msgs = messagesAll
|
|
2247
|
+
.filter((m) => m && m.conversationId === c.id && !m.deleted)
|
|
2248
|
+
.sort((a, b) => Date.parse(a.createdAt || 0) - Date.parse(b.createdAt || 0));
|
|
2249
|
+
if (msgs.length === 0) {
|
|
2250
|
+
warnings.push('no messages for conversation ' + c.id + ' (' + (c.title || '') + ')');
|
|
2251
|
+
}
|
|
2252
|
+
const detail = buildDetail(c, msgs);
|
|
2253
|
+
items.push({
|
|
2254
|
+
id: detail.id,
|
|
2255
|
+
title: detail.title,
|
|
2256
|
+
create_time: detail.create_time,
|
|
2257
|
+
update_time: detail.update_time,
|
|
2258
|
+
space_id: detail.space_id,
|
|
2259
|
+
starred: detail.starred,
|
|
2260
|
+
});
|
|
2261
|
+
details.push(detail);
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
return {
|
|
2265
|
+
ok: true,
|
|
2266
|
+
conversations: items,
|
|
2267
|
+
details,
|
|
2268
|
+
warnings,
|
|
2269
|
+
conversationCount: items.length,
|
|
2270
|
+
messageCount: messagesAll.length,
|
|
2271
|
+
};
|
|
2272
|
+
})();
|
|
2273
|
+
})()`;
|
|
2274
|
+
async function ensureLumoTab(port) {
|
|
2275
|
+
const targets = await listCdpTargets(port);
|
|
2276
|
+
const pages = targets.filter((t) => t.type === "page" || t.type === "tab");
|
|
2277
|
+
let target = pages.find((t) => isLumoUrl(t.url));
|
|
2278
|
+
if (!target) {
|
|
2279
|
+
target = await openCdpTab(port, LUMO_HOME);
|
|
2280
|
+
} else {
|
|
2281
|
+
await activateCdpTarget(port, target.id);
|
|
2282
|
+
}
|
|
2283
|
+
let wsUrl = target.webSocketDebuggerUrl;
|
|
2284
|
+
if (!wsUrl) {
|
|
2285
|
+
const refreshed = (await listCdpTargets(port)).find((t) => t.id === target.id);
|
|
2286
|
+
wsUrl = refreshed?.webSocketDebuggerUrl;
|
|
2287
|
+
}
|
|
2288
|
+
if (!wsUrl) {
|
|
2289
|
+
throw new Error("No DevTools WebSocket for Lumo tab");
|
|
2290
|
+
}
|
|
2291
|
+
return { targetId: target.id, wsUrl };
|
|
2292
|
+
}
|
|
2293
|
+
async function exportLumoViaCdp(options = {}) {
|
|
2294
|
+
let port = options.cdpPort ?? DEFAULT_CDP_PORT;
|
|
2295
|
+
if (!await isCdpRunning(port)) {
|
|
2296
|
+
const found = await discoverCdpPort(port);
|
|
2297
|
+
if (found === null) {
|
|
2298
|
+
throw new Error(
|
|
2299
|
+
"No Chromium CDP endpoint found. Start with: prompt-exporter chromium start --url https://lumo.proton.me/"
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
port = found;
|
|
2303
|
+
}
|
|
2304
|
+
const { wsUrl } = await ensureLumoTab(port);
|
|
2305
|
+
const result = await withCdpSocket(wsUrl, async (send) => {
|
|
2306
|
+
await send("Runtime.enable");
|
|
2307
|
+
await send("Page.enable").catch(() => void 0);
|
|
2308
|
+
const evaluated = await send("Runtime.evaluate", {
|
|
2309
|
+
expression: EXPORT_EXPRESSION,
|
|
2310
|
+
awaitPromise: true,
|
|
2311
|
+
returnByValue: true
|
|
2312
|
+
});
|
|
2313
|
+
if (evaluated.exceptionDetails) {
|
|
2314
|
+
throw new Error(
|
|
2315
|
+
`Lumo export evaluate failed: ${JSON.stringify(evaluated.exceptionDetails).slice(0, 400)}`
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
const remote = evaluated.result;
|
|
2319
|
+
if (!remote) {
|
|
2320
|
+
throw new Error(
|
|
2321
|
+
`Lumo export returned no RemoteObject: ${JSON.stringify(evaluated).slice(0, 300)}`
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
if (remote.type === "undefined" || remote.subtype === "error") {
|
|
2325
|
+
throw new Error(
|
|
2326
|
+
`Lumo export RemoteObject empty/error: ${JSON.stringify(remote).slice(0, 400)}`
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
2329
|
+
if ("value" in remote) {
|
|
2330
|
+
return remote.value;
|
|
2331
|
+
}
|
|
2332
|
+
throw new Error(
|
|
2333
|
+
`Lumo export missing value (type=${String(remote.type)}): ${JSON.stringify(remote).slice(0, 400)}`
|
|
2334
|
+
);
|
|
2335
|
+
});
|
|
2336
|
+
if (!result || typeof result !== "object") {
|
|
2337
|
+
throw new Error(
|
|
2338
|
+
`Lumo export returned empty result (${String(result)}). Is lumo.proton.me open and logged in on the CDP browser?`
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
if (result.ok === false) {
|
|
2342
|
+
throw new Error(String(result.error || "Lumo export failed"));
|
|
2343
|
+
}
|
|
2344
|
+
const conversations = result.conversations ?? [];
|
|
2345
|
+
const details = result.details ?? [];
|
|
2346
|
+
const warnings = result.warnings ?? [];
|
|
2347
|
+
if (options.verbose) {
|
|
2348
|
+
for (const w of warnings) {
|
|
2349
|
+
console.error(`[lumo] ${w}`);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
return { conversations, details, warnings };
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// src/cli/commands/backup-lumo.ts
|
|
2356
|
+
function asConversationDetail(detail) {
|
|
2357
|
+
return detail;
|
|
2358
|
+
}
|
|
2359
|
+
async function lumoBackupCommand(options) {
|
|
2360
|
+
const spinner = ora2("Connecting to Lumo via Chromium CDP\u2026").start();
|
|
2361
|
+
try {
|
|
2362
|
+
const bundle = await exportLumoViaCdp({
|
|
2363
|
+
cdpPort: options.cdpPort,
|
|
2364
|
+
verbose: options.verbose
|
|
2365
|
+
});
|
|
2366
|
+
spinner.succeed(
|
|
2367
|
+
chalk3.green(
|
|
2368
|
+
`Lumo export: ${bundle.conversations.length} conversation(s) from Redux`
|
|
2369
|
+
)
|
|
2370
|
+
);
|
|
2371
|
+
if (bundle.warnings.length && options.verbose) {
|
|
2372
|
+
for (const w of bundle.warnings) {
|
|
2373
|
+
console.log(chalk3.yellow(` \u26A0 ${w}`));
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
const storage = new StorageService(options.output);
|
|
2377
|
+
await storage.initialize();
|
|
2378
|
+
await storage.appendLog(
|
|
2379
|
+
`Lumo CDP export: ${bundle.conversations.length} conversations`
|
|
2380
|
+
);
|
|
2381
|
+
await storage.saveIndex(
|
|
2382
|
+
bundle.conversations
|
|
2383
|
+
);
|
|
2384
|
+
let downloaded = 0;
|
|
2385
|
+
let skipped = 0;
|
|
2386
|
+
for (const detail of bundle.details) {
|
|
2387
|
+
if (options.incremental) {
|
|
2388
|
+
const existing = await storage.getExistingConversationUpdateTime(detail.id);
|
|
2389
|
+
if (existing !== null && existing >= detail.update_time) {
|
|
2390
|
+
skipped++;
|
|
2391
|
+
continue;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
await storage.saveConversation(detail.id, asConversationDetail(detail));
|
|
2395
|
+
downloaded++;
|
|
2396
|
+
}
|
|
2397
|
+
await storage.saveMetadata({
|
|
2398
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2399
|
+
totalConversations: bundle.conversations.length,
|
|
2400
|
+
successfulDownloads: downloaded,
|
|
2401
|
+
failedDownloads: 0,
|
|
2402
|
+
errors: []
|
|
2403
|
+
});
|
|
2404
|
+
console.log();
|
|
2405
|
+
console.log(chalk3.bold("Lumo backup summary"));
|
|
2406
|
+
console.log(` Output: ${options.output}`);
|
|
2407
|
+
console.log(` Listed: ${bundle.conversations.length}`);
|
|
2408
|
+
console.log(` Downloaded: ${downloaded}`);
|
|
2409
|
+
console.log(` Skipped: ${skipped}`);
|
|
2410
|
+
if (bundle.warnings.length) {
|
|
2411
|
+
console.log(` Warnings: ${bundle.warnings.length}`);
|
|
2412
|
+
}
|
|
2413
|
+
const mdSpinner = ora2("Writing Markdown\u2026").start();
|
|
2414
|
+
const md = await convertDirectory(options.output);
|
|
2415
|
+
if (md.converted === 0 && md.errors > 0) {
|
|
2416
|
+
mdSpinner.fail(
|
|
2417
|
+
chalk3.red(`Markdown: 0 written, ${md.errors} errors`)
|
|
2418
|
+
);
|
|
2419
|
+
} else {
|
|
2420
|
+
mdSpinner.succeed(
|
|
2421
|
+
chalk3.green(`Markdown: ${md.converted} written, ${md.errors} errors`)
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
spinner.fail(chalk3.red("Lumo sync failed"));
|
|
2426
|
+
throw error;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
async function lumoListCommand(options) {
|
|
2430
|
+
const bundle = await exportLumoViaCdp({
|
|
2431
|
+
cdpPort: options.cdpPort,
|
|
2432
|
+
verbose: options.verbose
|
|
2433
|
+
});
|
|
2434
|
+
if (options.json) {
|
|
2435
|
+
console.log(JSON.stringify(bundle.conversations, null, 2));
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
console.log();
|
|
2439
|
+
console.log(chalk3.bold(`Lumo conversations (${bundle.conversations.length})`));
|
|
2440
|
+
for (const c of bundle.conversations) {
|
|
2441
|
+
const when = c.update_time ? new Date(c.update_time * 1e3).toISOString().slice(0, 10) : "????-??-??";
|
|
2442
|
+
console.log(` ${when} ${c.title} ${chalk3.dim(c.id)}`);
|
|
2443
|
+
}
|
|
2444
|
+
if (bundle.warnings.length && options.verbose) {
|
|
2445
|
+
console.log();
|
|
2446
|
+
for (const w of bundle.warnings) console.log(chalk3.yellow(w));
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2001
2450
|
// src/cli/commands/chromium.ts
|
|
2002
2451
|
import { spawn } from "child_process";
|
|
2003
2452
|
import fs7 from "fs/promises";
|
|
2004
2453
|
import path7 from "path";
|
|
2005
|
-
import
|
|
2454
|
+
import chalk4 from "chalk";
|
|
2006
2455
|
|
|
2007
2456
|
// src/utils/browser.ts
|
|
2008
2457
|
import { access } from "fs/promises";
|
|
@@ -2033,11 +2482,11 @@ async function resolveBrowser() {
|
|
|
2033
2482
|
// src/cli/commands/chromium.ts
|
|
2034
2483
|
function printNextSteps() {
|
|
2035
2484
|
console.log("Then run:");
|
|
2036
|
-
console.log(
|
|
2485
|
+
console.log(chalk4.cyan(" prompt-exporter token"));
|
|
2037
2486
|
console.log("to read accessToken from this browser session.");
|
|
2038
2487
|
console.log("");
|
|
2039
2488
|
console.log("Then sync conversations:");
|
|
2040
|
-
console.log(
|
|
2489
|
+
console.log(chalk4.cyan(" prompt-exporter sync"));
|
|
2041
2490
|
console.log("");
|
|
2042
2491
|
}
|
|
2043
2492
|
async function isProfileLocked(userDataDir) {
|
|
@@ -2055,13 +2504,13 @@ async function reuseExistingCdp(port, url, existing) {
|
|
|
2055
2504
|
if (!info) {
|
|
2056
2505
|
throw new Error(`No CDP endpoint on port ${port}`);
|
|
2057
2506
|
}
|
|
2058
|
-
console.log(
|
|
2059
|
-
console.log(
|
|
2507
|
+
console.log(chalk4.green(`Reusing Chromium CDP on port ${port}.`));
|
|
2508
|
+
console.log(chalk4.dim(` CDP: http://127.0.0.1:${port}`));
|
|
2060
2509
|
if (info.Browser) {
|
|
2061
|
-
console.log(
|
|
2510
|
+
console.log(chalk4.dim(` Browser: ${info.Browser}`));
|
|
2062
2511
|
}
|
|
2063
2512
|
if (info.webSocketDebuggerUrl) {
|
|
2064
|
-
console.log(
|
|
2513
|
+
console.log(chalk4.dim(` WS: ${info.webSocketDebuggerUrl}`));
|
|
2065
2514
|
}
|
|
2066
2515
|
console.log("");
|
|
2067
2516
|
const targets = await listCdpTargets(port);
|
|
@@ -2070,13 +2519,13 @@ async function reuseExistingCdp(port, url, existing) {
|
|
|
2070
2519
|
const chatgpt = exact ?? (isChatGptUrl(url) ? pages.find((t) => isChatGptUrl(t.url)) : void 0);
|
|
2071
2520
|
if (chatgpt) {
|
|
2072
2521
|
await activateCdpTarget(port, chatgpt.id);
|
|
2073
|
-
console.log(
|
|
2074
|
-
console.log(
|
|
2075
|
-
console.log(
|
|
2522
|
+
console.log(chalk4.green(`Focused existing tab in that browser.`));
|
|
2523
|
+
console.log(chalk4.dim(` ${chatgpt.title || chatgpt.id}`));
|
|
2524
|
+
console.log(chalk4.dim(` ${chatgpt.url}`));
|
|
2076
2525
|
} else {
|
|
2077
2526
|
const created = await openCdpTab(port, url);
|
|
2078
|
-
console.log(
|
|
2079
|
-
console.log(
|
|
2527
|
+
console.log(chalk4.green(`Opened a new tab in that browser (same session).`));
|
|
2528
|
+
console.log(chalk4.dim(` ${created.url || url}`));
|
|
2080
2529
|
}
|
|
2081
2530
|
console.log("");
|
|
2082
2531
|
printNextSteps();
|
|
@@ -2111,12 +2560,12 @@ async function chromiumStartCommand(options) {
|
|
|
2111
2560
|
return;
|
|
2112
2561
|
}
|
|
2113
2562
|
console.log(
|
|
2114
|
-
|
|
2563
|
+
chalk4.yellow("Chromium profile is already in use (browser running).")
|
|
2115
2564
|
);
|
|
2116
|
-
console.log(
|
|
2565
|
+
console.log(chalk4.dim(` Profile: ${userDataDir}`));
|
|
2117
2566
|
console.log("");
|
|
2118
2567
|
console.log(
|
|
2119
|
-
|
|
2568
|
+
chalk4.yellow(
|
|
2120
2569
|
`No CDP answered on port ${port}, so this running browser cannot be controlled.`
|
|
2121
2570
|
)
|
|
2122
2571
|
);
|
|
@@ -2143,13 +2592,13 @@ async function chromiumStartCommand(options) {
|
|
|
2143
2592
|
}
|
|
2144
2593
|
args.push(url);
|
|
2145
2594
|
if (options.print) {
|
|
2146
|
-
console.log(
|
|
2147
|
-
console.log(
|
|
2595
|
+
console.log(chalk4.dim(`Browser: ${browser}`));
|
|
2596
|
+
console.log(chalk4.dim(`Args: ${args.join(" ")}`));
|
|
2148
2597
|
}
|
|
2149
|
-
console.log(
|
|
2150
|
-
console.log(
|
|
2598
|
+
console.log(chalk4.bold("Starting Chromium with remote debugging\u2026"));
|
|
2599
|
+
console.log(chalk4.dim(` CDP: http://127.0.0.1:${port}`));
|
|
2151
2600
|
console.log(
|
|
2152
|
-
|
|
2601
|
+
chalk4.dim(
|
|
2153
2602
|
` Profile: ${userDataDir}` + (mode === "system" ? " (system \u2014 your normal login)" : mode === "isolated" ? " (isolated)" : " (custom)")
|
|
2154
2603
|
)
|
|
2155
2604
|
);
|
|
@@ -2160,15 +2609,15 @@ async function chromiumStartCommand(options) {
|
|
|
2160
2609
|
stdio: "ignore"
|
|
2161
2610
|
});
|
|
2162
2611
|
child.unref();
|
|
2163
|
-
console.log(
|
|
2612
|
+
console.log(chalk4.green(`Chromium started (pid ${child.pid ?? "unknown"}).`));
|
|
2164
2613
|
}
|
|
2165
2614
|
|
|
2166
2615
|
// src/cli/commands/list.ts
|
|
2167
|
-
import
|
|
2168
|
-
import
|
|
2616
|
+
import chalk5 from "chalk";
|
|
2617
|
+
import ora3 from "ora";
|
|
2169
2618
|
async function listCommand(options) {
|
|
2170
2619
|
const { token, delay, verbose, json, project, cdpPort } = options;
|
|
2171
|
-
const spinner =
|
|
2620
|
+
const spinner = ora3("Connecting...").start();
|
|
2172
2621
|
const { client } = await createChatGptClient({ token, verbose, cdpPort });
|
|
2173
2622
|
const storage = new StorageService(getDefaultDataDir());
|
|
2174
2623
|
const service = new BackupService(client, storage);
|
|
@@ -2209,30 +2658,30 @@ async function listCommand(options) {
|
|
|
2209
2658
|
} else {
|
|
2210
2659
|
console.log();
|
|
2211
2660
|
for (const conv of conversations) {
|
|
2212
|
-
const title = conv.title ??
|
|
2661
|
+
const title = conv.title ?? chalk5.dim("(untitled)");
|
|
2213
2662
|
const date = conv.update_time ? new Date(
|
|
2214
2663
|
typeof conv.update_time === "number" ? conv.update_time * 1e3 : conv.update_time
|
|
2215
2664
|
).toLocaleDateString() : "unknown date";
|
|
2216
|
-
console.log(`${
|
|
2665
|
+
console.log(`${chalk5.cyan(conv.id)} ${title} ${chalk5.dim(`[${date}]`)}`);
|
|
2217
2666
|
}
|
|
2218
2667
|
console.log();
|
|
2219
|
-
console.log(
|
|
2668
|
+
console.log(chalk5.green(`Total: ${conversations.length} conversations`));
|
|
2220
2669
|
}
|
|
2221
2670
|
} catch (error) {
|
|
2222
2671
|
spinner.fail("Failed to list conversations");
|
|
2223
2672
|
if (error instanceof Error) {
|
|
2224
2673
|
if (error.name === "AuthenticationError") {
|
|
2225
|
-
console.error(
|
|
2674
|
+
console.error(chalk5.red(`
|
|
2226
2675
|
Authentication failed: ${error.message}`));
|
|
2227
2676
|
console.error(
|
|
2228
|
-
|
|
2677
|
+
chalk5.yellow(
|
|
2229
2678
|
"\nKeep Chromium CDP logged in to chatgpt.com, then retry:"
|
|
2230
2679
|
)
|
|
2231
2680
|
);
|
|
2232
2681
|
console.error(" prompt-exporter chromium start");
|
|
2233
2682
|
console.error(" prompt-exporter list");
|
|
2234
2683
|
} else {
|
|
2235
|
-
console.error(
|
|
2684
|
+
console.error(chalk5.red(`
|
|
2236
2685
|
Error: ${error.message}`));
|
|
2237
2686
|
}
|
|
2238
2687
|
}
|
|
@@ -2243,11 +2692,11 @@ Error: ${error.message}`));
|
|
|
2243
2692
|
}
|
|
2244
2693
|
|
|
2245
2694
|
// src/cli/commands/projects.ts
|
|
2246
|
-
import
|
|
2247
|
-
import
|
|
2695
|
+
import chalk6 from "chalk";
|
|
2696
|
+
import ora4 from "ora";
|
|
2248
2697
|
async function projectsCommand(options) {
|
|
2249
2698
|
const { token, verbose, json, cdpPort } = options;
|
|
2250
|
-
const spinner =
|
|
2699
|
+
const spinner = ora4("Connecting...").start();
|
|
2251
2700
|
const { client } = await createChatGptClient({ token, verbose, cdpPort });
|
|
2252
2701
|
const storage = new StorageService(getDefaultDataDir());
|
|
2253
2702
|
const service = new BackupService(client, storage);
|
|
@@ -2273,19 +2722,19 @@ async function projectsCommand(options) {
|
|
|
2273
2722
|
const id = gizmo.id;
|
|
2274
2723
|
const lastInteracted = gizmo.last_interacted_at ? new Date(gizmo.last_interacted_at).toLocaleDateString() : "never";
|
|
2275
2724
|
console.log(
|
|
2276
|
-
`${
|
|
2725
|
+
`${chalk6.cyan(id)} ${chalk6.bold(name)} ${chalk6.dim(`[last: ${lastInteracted}]`)}`
|
|
2277
2726
|
);
|
|
2278
2727
|
}
|
|
2279
2728
|
console.log();
|
|
2280
|
-
console.log(
|
|
2729
|
+
console.log(chalk6.green(`Total: ${projects.length} projects`));
|
|
2281
2730
|
}
|
|
2282
2731
|
} catch (error) {
|
|
2283
2732
|
spinner.fail("Failed to list projects");
|
|
2284
2733
|
if (error instanceof Error) {
|
|
2285
|
-
console.error(
|
|
2734
|
+
console.error(chalk6.red(`
|
|
2286
2735
|
Error: ${error.message}`));
|
|
2287
2736
|
if (error.name === "AuthenticationError") {
|
|
2288
|
-
console.error(
|
|
2737
|
+
console.error(chalk6.yellow("\nKeep Chromium CDP logged in, then retry:"));
|
|
2289
2738
|
console.error(" prompt-exporter chromium start");
|
|
2290
2739
|
console.error(" prompt-exporter projects");
|
|
2291
2740
|
}
|
|
@@ -2298,7 +2747,7 @@ Error: ${error.message}`));
|
|
|
2298
2747
|
|
|
2299
2748
|
// src/cli/commands/token.ts
|
|
2300
2749
|
import { spawn as spawn2 } from "child_process";
|
|
2301
|
-
import
|
|
2750
|
+
import chalk7 from "chalk";
|
|
2302
2751
|
|
|
2303
2752
|
// src/utils/cdp-session.ts
|
|
2304
2753
|
var SESSION_URL = "https://chatgpt.com/api/auth/session";
|
|
@@ -2312,7 +2761,7 @@ function getWebSocket() {
|
|
|
2312
2761
|
}
|
|
2313
2762
|
return WS;
|
|
2314
2763
|
}
|
|
2315
|
-
async function
|
|
2764
|
+
async function withCdpSocket2(webSocketDebuggerUrl, fn) {
|
|
2316
2765
|
const WS = getWebSocket();
|
|
2317
2766
|
const ws = new WS(webSocketDebuggerUrl);
|
|
2318
2767
|
let nextId = 1;
|
|
@@ -2394,7 +2843,7 @@ function normalizeProbe(raw) {
|
|
|
2394
2843
|
}
|
|
2395
2844
|
async function probeSessionViaCdp(webSocketDebuggerUrl) {
|
|
2396
2845
|
try {
|
|
2397
|
-
return await
|
|
2846
|
+
return await withCdpSocket2(webSocketDebuggerUrl, async (send) => {
|
|
2398
2847
|
await send("Runtime.enable");
|
|
2399
2848
|
const result = await send("Runtime.evaluate", {
|
|
2400
2849
|
expression: `(() => {
|
|
@@ -2534,16 +2983,16 @@ async function waitForAccessToken(port, options = {}) {
|
|
|
2534
2983
|
// src/cli/commands/token.ts
|
|
2535
2984
|
async function persistAndAnnounce(token, show) {
|
|
2536
2985
|
const file = await saveToken(token);
|
|
2537
|
-
console.log(
|
|
2538
|
-
console.log(
|
|
2539
|
-
console.log(
|
|
2986
|
+
console.log(chalk7.green("Saved accessToken for sync."));
|
|
2987
|
+
console.log(chalk7.dim(` ${file}`));
|
|
2988
|
+
console.log(chalk7.dim(` preview: ${token.slice(0, 16)}\u2026 (${token.length} chars)`));
|
|
2540
2989
|
console.log("");
|
|
2541
2990
|
if (show) {
|
|
2542
2991
|
console.log(` export PROMPT_EXPORTER_CHATGPT_TOKEN="${token}"`);
|
|
2543
2992
|
console.log("");
|
|
2544
2993
|
}
|
|
2545
2994
|
console.log("You can sync right away (no manual export needed):");
|
|
2546
|
-
console.log(
|
|
2995
|
+
console.log(chalk7.cyan(" prompt-exporter sync"));
|
|
2547
2996
|
}
|
|
2548
2997
|
function printManualSteps() {
|
|
2549
2998
|
console.log("Steps:");
|
|
@@ -2564,11 +3013,11 @@ async function tokenCommand(options = {}) {
|
|
|
2564
3013
|
}
|
|
2565
3014
|
if (!await isCdpRunning(port)) {
|
|
2566
3015
|
console.log(
|
|
2567
|
-
|
|
3016
|
+
chalk7.yellow(
|
|
2568
3017
|
`No CDP on port ${port}. Start one with: prompt-exporter chromium start`
|
|
2569
3018
|
)
|
|
2570
3019
|
);
|
|
2571
|
-
console.log(
|
|
3020
|
+
console.log(chalk7.dim("Opening a separate Chromium window as fallback\u2026"));
|
|
2572
3021
|
console.log("");
|
|
2573
3022
|
const browser = await resolveBrowser();
|
|
2574
3023
|
console.log(`Open ${SESSION_URL}, then re-run prompt-exporter token`);
|
|
@@ -2583,20 +3032,20 @@ async function tokenCommand(options = {}) {
|
|
|
2583
3032
|
return;
|
|
2584
3033
|
}
|
|
2585
3034
|
const info = await getCdpEndpointInfo(port);
|
|
2586
|
-
console.log(
|
|
3035
|
+
console.log(chalk7.green(`Using existing Chromium CDP on port ${port}.`));
|
|
2587
3036
|
if (info?.Browser) {
|
|
2588
|
-
console.log(
|
|
3037
|
+
console.log(chalk7.dim(` Browser: ${info.Browser}`));
|
|
2589
3038
|
}
|
|
2590
|
-
console.log(
|
|
3039
|
+
console.log(chalk7.dim(` Token file: ${getTokenPath()}`));
|
|
2591
3040
|
console.log("");
|
|
2592
3041
|
try {
|
|
2593
3042
|
const { target, reused, probe } = await prepareSessionTarget(port);
|
|
2594
3043
|
console.log(
|
|
2595
|
-
|
|
3044
|
+
chalk7.dim(
|
|
2596
3045
|
`${reused ? "Reused" : "Opened"} tab: ${target.title || target.id}`
|
|
2597
3046
|
)
|
|
2598
3047
|
);
|
|
2599
|
-
console.log(
|
|
3048
|
+
console.log(chalk7.dim(` ${target.url || SESSION_URL}`));
|
|
2600
3049
|
console.log("");
|
|
2601
3050
|
if (probe.loggedIn && probe.accessToken) {
|
|
2602
3051
|
await persistAndAnnounce(probe.accessToken, show);
|
|
@@ -2604,31 +3053,31 @@ async function tokenCommand(options = {}) {
|
|
|
2604
3053
|
}
|
|
2605
3054
|
if (probe.anonymousBannerOnly || !probe.loggedIn) {
|
|
2606
3055
|
console.log(
|
|
2607
|
-
|
|
3056
|
+
chalk7.yellow(
|
|
2608
3057
|
"No accessToken in the session tab yet (anonymous / not logged in)."
|
|
2609
3058
|
)
|
|
2610
3059
|
);
|
|
2611
3060
|
if (probe.keys.length) {
|
|
2612
|
-
console.log(
|
|
3061
|
+
console.log(chalk7.dim(` session keys: ${probe.keys.join(", ")}`));
|
|
2613
3062
|
}
|
|
2614
3063
|
if (probe.error) {
|
|
2615
|
-
console.log(
|
|
3064
|
+
console.log(chalk7.dim(` ${probe.error}`));
|
|
2616
3065
|
}
|
|
2617
3066
|
console.log("");
|
|
2618
3067
|
const appTab = await ensureChatGptAppTab(port);
|
|
2619
|
-
console.log(
|
|
2620
|
-
console.log(
|
|
3068
|
+
console.log(chalk7.cyan(`Focused ChatGPT for login: ${APP_URL}`));
|
|
3069
|
+
console.log(chalk7.dim(` Tab: ${appTab.url || APP_URL}`));
|
|
2621
3070
|
console.log("");
|
|
2622
3071
|
if (!wait) {
|
|
2623
3072
|
printManualSteps();
|
|
2624
3073
|
return;
|
|
2625
3074
|
}
|
|
2626
3075
|
console.log(
|
|
2627
|
-
|
|
3076
|
+
chalk7.bold(
|
|
2628
3077
|
`Waiting up to ${Math.round(timeoutMs / 1e3)}s for login, then re-reading session\u2026`
|
|
2629
3078
|
)
|
|
2630
3079
|
);
|
|
2631
|
-
console.log(
|
|
3080
|
+
console.log(chalk7.dim("Press Ctrl+C to cancel."));
|
|
2632
3081
|
console.log("");
|
|
2633
3082
|
const finalProbe = await waitForAccessToken(port, {
|
|
2634
3083
|
timeoutMs,
|
|
@@ -2636,7 +3085,7 @@ async function tokenCommand(options = {}) {
|
|
|
2636
3085
|
onTick: (p) => {
|
|
2637
3086
|
if (p.loggedIn) return;
|
|
2638
3087
|
process.stdout.write(
|
|
2639
|
-
|
|
3088
|
+
chalk7.dim(
|
|
2640
3089
|
` waiting\u2026 (${p.anonymousBannerOnly ? "banner-only" : p.keys.join(",") || "no token"})\r`
|
|
2641
3090
|
)
|
|
2642
3091
|
);
|
|
@@ -2647,18 +3096,18 @@ async function tokenCommand(options = {}) {
|
|
|
2647
3096
|
await persistAndAnnounce(finalProbe.accessToken, show);
|
|
2648
3097
|
return;
|
|
2649
3098
|
}
|
|
2650
|
-
console.log(
|
|
3099
|
+
console.log(chalk7.red("Timed out \u2014 accessToken not found."));
|
|
2651
3100
|
printManualSteps();
|
|
2652
3101
|
process.exitCode = 1;
|
|
2653
3102
|
return;
|
|
2654
3103
|
}
|
|
2655
|
-
console.log(
|
|
2656
|
-
if (probe.error) console.log(
|
|
3104
|
+
console.log(chalk7.yellow("Could not read accessToken from the session page."));
|
|
3105
|
+
if (probe.error) console.log(chalk7.dim(` ${probe.error}`));
|
|
2657
3106
|
printManualSteps();
|
|
2658
3107
|
process.exitCode = 1;
|
|
2659
3108
|
} catch (error) {
|
|
2660
3109
|
console.error(
|
|
2661
|
-
|
|
3110
|
+
chalk7.red(
|
|
2662
3111
|
`CDP session failed: ${error instanceof Error ? error.message : error}`
|
|
2663
3112
|
)
|
|
2664
3113
|
);
|
|
@@ -2677,8 +3126,18 @@ var chatgptSource = {
|
|
|
2677
3126
|
}
|
|
2678
3127
|
};
|
|
2679
3128
|
|
|
3129
|
+
// src/sources/lumo/index.ts
|
|
3130
|
+
var lumoSource = {
|
|
3131
|
+
id: "lumo",
|
|
3132
|
+
label: "Lumo",
|
|
3133
|
+
description: "lumo.proton.me (Proton) via Chromium CDP / in-app Redux",
|
|
3134
|
+
defaultDataDir() {
|
|
3135
|
+
return getSourceDataDir("lumo");
|
|
3136
|
+
}
|
|
3137
|
+
};
|
|
3138
|
+
|
|
2680
3139
|
// src/sources/registry.ts
|
|
2681
|
-
var sources = [chatgptSource];
|
|
3140
|
+
var sources = [chatgptSource, lumoSource];
|
|
2682
3141
|
function listSources() {
|
|
2683
3142
|
return sources.map(({ id, label, description }) => ({
|
|
2684
3143
|
id,
|
|
@@ -2712,7 +3171,7 @@ function createCli() {
|
|
|
2712
3171
|
const program2 = new Command();
|
|
2713
3172
|
program2.name("prompt-exporter").description(
|
|
2714
3173
|
"Export and sync AI conversation prompts from multiple sources to local JSON + Markdown"
|
|
2715
|
-
).version("2.
|
|
3174
|
+
).version("2.1.0").hook("preAction", async () => {
|
|
2716
3175
|
await ensureHomeLayout();
|
|
2717
3176
|
}).addHelpText(
|
|
2718
3177
|
"after",
|
|
@@ -2728,6 +3187,8 @@ function createCli() {
|
|
|
2728
3187
|
"Typical flow:",
|
|
2729
3188
|
" prompt-exporter chromium start",
|
|
2730
3189
|
" prompt-exporter sync --source chatgpt",
|
|
3190
|
+
" prompt-exporter chromium start --url https://lumo.proton.me/",
|
|
3191
|
+
" prompt-exporter sync --source lumo",
|
|
2731
3192
|
"",
|
|
2732
3193
|
"Data: ~/.prompt-exporter/{source}/conversations/"
|
|
2733
3194
|
].join("\n")
|
|
@@ -2793,6 +3254,7 @@ function createCli() {
|
|
|
2793
3254
|
"Examples:",
|
|
2794
3255
|
" prompt-exporter chromium start",
|
|
2795
3256
|
" prompt-exporter chromium start --port 9222",
|
|
3257
|
+
" prompt-exporter chromium start --url https://lumo.proton.me/",
|
|
2796
3258
|
" prompt-exporter chromium start --isolated"
|
|
2797
3259
|
].join("\n")
|
|
2798
3260
|
).action(async (options) => {
|
|
@@ -2837,6 +3299,15 @@ function createCli() {
|
|
|
2837
3299
|
);
|
|
2838
3300
|
const runBackup = async (options) => {
|
|
2839
3301
|
const sourceId = resolveSourceId(options.source);
|
|
3302
|
+
if (sourceId === "lumo") {
|
|
3303
|
+
await lumoBackupCommand({
|
|
3304
|
+
output: resolveOutput(options),
|
|
3305
|
+
incremental: options.incremental,
|
|
3306
|
+
verbose: options.verbose,
|
|
3307
|
+
cdpPort: options.port
|
|
3308
|
+
});
|
|
3309
|
+
return;
|
|
3310
|
+
}
|
|
2840
3311
|
if (sourceId !== "chatgpt") {
|
|
2841
3312
|
throw new Error(
|
|
2842
3313
|
`Source "${sourceId}" is registered but sync is not implemented yet`
|
|
@@ -2892,6 +3363,7 @@ function createCli() {
|
|
|
2892
3363
|
"Examples:",
|
|
2893
3364
|
" prompt-exporter sync",
|
|
2894
3365
|
" prompt-exporter sync --source chatgpt",
|
|
3366
|
+
" prompt-exporter sync --source lumo",
|
|
2895
3367
|
" prompt-exporter sync --download-files",
|
|
2896
3368
|
" prompt-exporter sync --no-incremental"
|
|
2897
3369
|
].join("\n")
|
|
@@ -2919,6 +3391,14 @@ function createCli() {
|
|
|
2919
3391
|
"List conversations from a specific project"
|
|
2920
3392
|
).option("-v, --verbose", "Verbose logging", false).option("--json", "Output as JSON", false).action(async (options) => {
|
|
2921
3393
|
const sourceId = resolveSourceId(options.source);
|
|
3394
|
+
if (sourceId === "lumo") {
|
|
3395
|
+
await lumoListCommand({
|
|
3396
|
+
verbose: options.verbose,
|
|
3397
|
+
json: options.json,
|
|
3398
|
+
cdpPort: options.port
|
|
3399
|
+
});
|
|
3400
|
+
return;
|
|
3401
|
+
}
|
|
2922
3402
|
if (sourceId !== "chatgpt") {
|
|
2923
3403
|
throw new Error(`Source "${sourceId}" list not implemented yet`);
|
|
2924
3404
|
}
|
package/docs/DEVELOPER.md
CHANGED
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
|
|
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`.
|
|
12
12
|
|
|
13
13
|
**Platforms:** Linux is supported today; Windows and macOS are in focus for CDP/browser path parity.
|
|
14
14
|
|
|
@@ -89,6 +89,17 @@ prompt-exporter sync --no-incremental # force full re-download
|
|
|
89
89
|
prompt-exporter sources
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
### Lumo (Proton)
|
|
93
|
+
|
|
94
|
+
Lumo encrypts payloads on the server. Sync reads the **decrypted** in-app Redux store over CDP after login:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
prompt-exporter chromium start --url https://lumo.proton.me/
|
|
98
|
+
# log in; wait until chats appear in the sidebar
|
|
99
|
+
prompt-exporter sync --source lumo
|
|
100
|
+
prompt-exporter list --source lumo
|
|
101
|
+
```
|
|
102
|
+
|
|
92
103
|
`sync` is incremental by default (skips unchanged `update_time`).
|
|
93
104
|
|
|
94
105
|
Auth prefers a logged-in Chromium CDP session. Node Bearer alone is often blocked by Cloudflare.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prompt-exporter",
|
|
3
|
-
"version": "2.0
|
|
4
|
-
"description": "CLI to sync AI conversation prompts from multiple sources (ChatGPT
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "CLI to sync AI conversation prompts from multiple sources (ChatGPT, Lumo, …) to local JSON + Markdown",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Alexandru Zbarcea <alexz@apache.org>",
|
|
7
7
|
"type": "module",
|
|
@@ -17,16 +17,16 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=
|
|
20
|
+
"node": ">=24"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"dev": "tsx src/index.ts",
|
|
23
|
+
"dev": "node --disable-warning=DEP0205 --import tsx src/index.ts",
|
|
24
24
|
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
25
25
|
"start": "node dist/index.js",
|
|
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": {
|