@savestate/cli 0.6.0 → 0.8.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/dist/cli/__tests__/progress.test.d.ts +5 -0
- package/dist/cli/__tests__/progress.test.d.ts.map +1 -0
- package/dist/cli/__tests__/progress.test.js +212 -0
- package/dist/cli/__tests__/progress.test.js.map +1 -0
- package/dist/cli/__tests__/signal-handler.test.d.ts +5 -0
- package/dist/cli/__tests__/signal-handler.test.d.ts.map +1 -0
- package/dist/cli/__tests__/signal-handler.test.js +99 -0
- package/dist/cli/__tests__/signal-handler.test.js.map +1 -0
- package/dist/cli/__tests__/summary.test.d.ts +5 -0
- package/dist/cli/__tests__/summary.test.d.ts.map +1 -0
- package/dist/cli/__tests__/summary.test.js +242 -0
- package/dist/cli/__tests__/summary.test.js.map +1 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/progress.d.ts +86 -0
- package/dist/cli/progress.d.ts.map +1 -0
- package/dist/cli/progress.js +205 -0
- package/dist/cli/progress.js.map +1 -0
- package/dist/cli/prompts.d.ts +49 -0
- package/dist/cli/prompts.d.ts.map +1 -0
- package/dist/cli/prompts.js +266 -0
- package/dist/cli/prompts.js.map +1 -0
- package/dist/cli/signal-handler.d.ts +63 -0
- package/dist/cli/signal-handler.d.ts.map +1 -0
- package/dist/cli/signal-handler.js +165 -0
- package/dist/cli/signal-handler.js.map +1 -0
- package/dist/cli/summary.d.ts +33 -0
- package/dist/cli/summary.d.ts.map +1 -0
- package/dist/cli/summary.js +296 -0
- package/dist/cli/summary.js.map +1 -0
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/migrate.d.ts +18 -4
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +324 -163
- package/dist/commands/migrate.js.map +1 -1
- package/dist/migrate/__tests__/capabilities.test.d.ts +7 -0
- package/dist/migrate/__tests__/capabilities.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/capabilities.test.js +90 -0
- package/dist/migrate/__tests__/capabilities.test.js.map +1 -0
- package/dist/migrate/__tests__/chatgpt-loader.test.d.ts +7 -0
- package/dist/migrate/__tests__/chatgpt-loader.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/chatgpt-loader.test.js +889 -0
- package/dist/migrate/__tests__/chatgpt-loader.test.js.map +1 -0
- package/dist/migrate/__tests__/claude-loader.test.d.ts +7 -0
- package/dist/migrate/__tests__/claude-loader.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/claude-loader.test.js +544 -0
- package/dist/migrate/__tests__/claude-loader.test.js.map +1 -0
- package/dist/migrate/__tests__/edge-cases.test.d.ts +7 -0
- package/dist/migrate/__tests__/edge-cases.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/edge-cases.test.js +787 -0
- package/dist/migrate/__tests__/edge-cases.test.js.map +1 -0
- package/dist/migrate/__tests__/error-recovery.test.d.ts +7 -0
- package/dist/migrate/__tests__/error-recovery.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/error-recovery.test.js +461 -0
- package/dist/migrate/__tests__/error-recovery.test.js.map +1 -0
- package/dist/migrate/__tests__/integration.test.d.ts +7 -0
- package/dist/migrate/__tests__/integration.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/integration.test.js +536 -0
- package/dist/migrate/__tests__/integration.test.js.map +1 -0
- package/dist/migrate/__tests__/orchestrator.test.d.ts +8 -0
- package/dist/migrate/__tests__/orchestrator.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/orchestrator.test.js +355 -0
- package/dist/migrate/__tests__/orchestrator.test.js.map +1 -0
- package/dist/migrate/__tests__/performance.test.d.ts +7 -0
- package/dist/migrate/__tests__/performance.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/performance.test.js +478 -0
- package/dist/migrate/__tests__/performance.test.js.map +1 -0
- package/dist/migrate/__tests__/registry.test.d.ts +7 -0
- package/dist/migrate/__tests__/registry.test.d.ts.map +1 -0
- package/dist/migrate/__tests__/registry.test.js +167 -0
- package/dist/migrate/__tests__/registry.test.js.map +1 -0
- package/dist/migrate/compatibility.d.ts +47 -0
- package/dist/migrate/compatibility.d.ts.map +1 -0
- package/dist/migrate/compatibility.js +468 -0
- package/dist/migrate/compatibility.js.map +1 -0
- package/dist/migrate/extractors/__tests__/chatgpt.test.d.ts +12 -0
- package/dist/migrate/extractors/__tests__/chatgpt.test.d.ts.map +1 -0
- package/dist/migrate/extractors/__tests__/chatgpt.test.js +522 -0
- package/dist/migrate/extractors/__tests__/chatgpt.test.js.map +1 -0
- package/dist/migrate/extractors/__tests__/claude.test.d.ts +12 -0
- package/dist/migrate/extractors/__tests__/claude.test.d.ts.map +1 -0
- package/dist/migrate/extractors/__tests__/claude.test.js +789 -0
- package/dist/migrate/extractors/__tests__/claude.test.js.map +1 -0
- package/dist/migrate/extractors/chatgpt.d.ts +70 -0
- package/dist/migrate/extractors/chatgpt.d.ts.map +1 -0
- package/dist/migrate/extractors/chatgpt.js +791 -0
- package/dist/migrate/extractors/chatgpt.js.map +1 -0
- package/dist/migrate/extractors/claude.d.ts +69 -0
- package/dist/migrate/extractors/claude.d.ts.map +1 -0
- package/dist/migrate/extractors/claude.js +1136 -0
- package/dist/migrate/extractors/claude.js.map +1 -0
- package/dist/migrate/extractors/registry.js +6 -4
- package/dist/migrate/extractors/registry.js.map +1 -1
- package/dist/migrate/index.d.ts +6 -1
- package/dist/migrate/index.d.ts.map +1 -1
- package/dist/migrate/index.js +12 -1
- package/dist/migrate/index.js.map +1 -1
- package/dist/migrate/loaders/chatgpt.d.ts +72 -0
- package/dist/migrate/loaders/chatgpt.d.ts.map +1 -0
- package/dist/migrate/loaders/chatgpt.js +691 -0
- package/dist/migrate/loaders/chatgpt.js.map +1 -0
- package/dist/migrate/loaders/claude.d.ts +61 -0
- package/dist/migrate/loaders/claude.d.ts.map +1 -0
- package/dist/migrate/loaders/claude.js +433 -0
- package/dist/migrate/loaders/claude.js.map +1 -0
- package/dist/migrate/loaders/registry.js +6 -4
- package/dist/migrate/loaders/registry.js.map +1 -1
- package/dist/migrate/orchestrator.d.ts +75 -1
- package/dist/migrate/orchestrator.d.ts.map +1 -1
- package/dist/migrate/orchestrator.js +215 -19
- package/dist/migrate/orchestrator.js.map +1 -1
- package/dist/migrate/testing/index.d.ts +28 -0
- package/dist/migrate/testing/index.d.ts.map +1 -0
- package/dist/migrate/testing/index.js +55 -0
- package/dist/migrate/testing/index.js.map +1 -0
- package/dist/migrate/testing/mock-extractor.d.ts +30 -0
- package/dist/migrate/testing/mock-extractor.d.ts.map +1 -0
- package/dist/migrate/testing/mock-extractor.js +137 -0
- package/dist/migrate/testing/mock-extractor.js.map +1 -0
- package/dist/migrate/testing/mock-loader.d.ts +36 -0
- package/dist/migrate/testing/mock-loader.d.ts.map +1 -0
- package/dist/migrate/testing/mock-loader.js +81 -0
- package/dist/migrate/testing/mock-loader.js.map +1 -0
- package/dist/migrate/testing/mock-transformer.d.ts +26 -0
- package/dist/migrate/testing/mock-transformer.d.ts.map +1 -0
- package/dist/migrate/testing/mock-transformer.js +185 -0
- package/dist/migrate/testing/mock-transformer.js.map +1 -0
- package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.d.ts +5 -0
- package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.d.ts.map +1 -0
- package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.js +333 -0
- package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.js.map +1 -0
- package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.d.ts +5 -0
- package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.d.ts.map +1 -0
- package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.js +333 -0
- package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.js.map +1 -0
- package/dist/migrate/transformers/__tests__/rules.test.d.ts +5 -0
- package/dist/migrate/transformers/__tests__/rules.test.d.ts.map +1 -0
- package/dist/migrate/transformers/__tests__/rules.test.js +375 -0
- package/dist/migrate/transformers/__tests__/rules.test.js.map +1 -0
- package/dist/migrate/transformers/chatgpt-to-claude.d.ts +40 -0
- package/dist/migrate/transformers/chatgpt-to-claude.d.ts.map +1 -0
- package/dist/migrate/transformers/chatgpt-to-claude.js +443 -0
- package/dist/migrate/transformers/chatgpt-to-claude.js.map +1 -0
- package/dist/migrate/transformers/claude-to-chatgpt.d.ts +41 -0
- package/dist/migrate/transformers/claude-to-chatgpt.d.ts.map +1 -0
- package/dist/migrate/transformers/claude-to-chatgpt.js +532 -0
- package/dist/migrate/transformers/claude-to-chatgpt.js.map +1 -0
- package/dist/migrate/transformers/registry.js +6 -4
- package/dist/migrate/transformers/registry.js.map +1 -1
- package/dist/migrate/transformers/rules.d.ts +168 -0
- package/dist/migrate/transformers/rules.d.ts.map +1 -0
- package/dist/migrate/transformers/rules.js +487 -0
- package/dist/migrate/transformers/rules.js.map +1 -0
- package/dist/migrate/types.d.ts +2 -0
- package/dist/migrate/types.d.ts.map +1 -1
- package/package.json +7 -2
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts user data from ChatGPT including:
|
|
5
|
+
* - Custom instructions
|
|
6
|
+
* - Memory entries
|
|
7
|
+
* - Conversation history (from export files)
|
|
8
|
+
* - Uploaded files/attachments
|
|
9
|
+
* - Custom GPT configurations
|
|
10
|
+
*
|
|
11
|
+
* Supports both API-based extraction and export file parsing.
|
|
12
|
+
*/
|
|
13
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
14
|
+
import { mkdir, writeFile, readFile, stat, readdir, access, constants } from 'node:fs/promises';
|
|
15
|
+
import { join, extname, basename } from 'node:path';
|
|
16
|
+
// ─── Rate Limiter ────────────────────────────────────────────
|
|
17
|
+
class RateLimiter {
|
|
18
|
+
requests = [];
|
|
19
|
+
config;
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
this.config = {
|
|
22
|
+
requestsPerMinute: config.requestsPerMinute ?? 60,
|
|
23
|
+
initialBackoffMs: config.initialBackoffMs ?? 1000,
|
|
24
|
+
maxBackoffMs: config.maxBackoffMs ?? 60000,
|
|
25
|
+
maxRetries: config.maxRetries ?? 5,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async acquire() {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const windowStart = now - 60000;
|
|
31
|
+
// Remove old requests outside the window
|
|
32
|
+
this.requests = this.requests.filter((t) => t > windowStart);
|
|
33
|
+
// If at limit, wait
|
|
34
|
+
if (this.requests.length >= this.config.requestsPerMinute) {
|
|
35
|
+
const oldestInWindow = this.requests[0];
|
|
36
|
+
const waitTime = oldestInWindow - windowStart + 100;
|
|
37
|
+
await this.sleep(waitTime);
|
|
38
|
+
}
|
|
39
|
+
this.requests.push(Date.now());
|
|
40
|
+
}
|
|
41
|
+
async withRetry(fn) {
|
|
42
|
+
let lastError = null;
|
|
43
|
+
let backoff = this.config.initialBackoffMs;
|
|
44
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
45
|
+
try {
|
|
46
|
+
await this.acquire();
|
|
47
|
+
return await fn();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
51
|
+
// Check if it's a rate limit error
|
|
52
|
+
if (this.isRateLimitError(lastError)) {
|
|
53
|
+
if (attempt < this.config.maxRetries) {
|
|
54
|
+
await this.sleep(backoff);
|
|
55
|
+
backoff = Math.min(backoff * 2, this.config.maxBackoffMs);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw lastError;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw lastError ?? new Error('Max retries exceeded');
|
|
63
|
+
}
|
|
64
|
+
isRateLimitError(error) {
|
|
65
|
+
return (error.message.includes('429') ||
|
|
66
|
+
error.message.includes('rate limit') ||
|
|
67
|
+
error.message.includes('Rate limit') ||
|
|
68
|
+
error.message.includes('Too Many Requests'));
|
|
69
|
+
}
|
|
70
|
+
sleep(ms) {
|
|
71
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// ─── ChatGPT Extractor ───────────────────────────────────────
|
|
75
|
+
export class ChatGPTExtractor {
|
|
76
|
+
platform = 'chatgpt';
|
|
77
|
+
version = '1.0.0';
|
|
78
|
+
config;
|
|
79
|
+
rateLimiter;
|
|
80
|
+
progress = 0;
|
|
81
|
+
baseUrl;
|
|
82
|
+
constructor(config = {}) {
|
|
83
|
+
this.config = {
|
|
84
|
+
chunkSize: 64 * 1024, // 64KB default chunk size
|
|
85
|
+
...config,
|
|
86
|
+
};
|
|
87
|
+
this.rateLimiter = new RateLimiter(config.rateLimit);
|
|
88
|
+
this.baseUrl = config.baseUrl ?? 'https://api.openai.com/v1';
|
|
89
|
+
}
|
|
90
|
+
async canExtract() {
|
|
91
|
+
// Check if we have either API credentials or an export file
|
|
92
|
+
if (this.config.apiKey || this.config.accessToken) {
|
|
93
|
+
return this.verifyApiAccess();
|
|
94
|
+
}
|
|
95
|
+
if (this.config.exportPath) {
|
|
96
|
+
return this.verifyExportPath();
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
async extract(options) {
|
|
101
|
+
this.progress = 0;
|
|
102
|
+
const workDir = options.workDir;
|
|
103
|
+
await mkdir(workDir, { recursive: true });
|
|
104
|
+
const contents = {};
|
|
105
|
+
const warnings = [];
|
|
106
|
+
const errors = [];
|
|
107
|
+
const shouldInclude = (type) => !options.include || options.include.includes(type);
|
|
108
|
+
// Determine extraction method
|
|
109
|
+
const useApi = !!(this.config.apiKey || this.config.accessToken);
|
|
110
|
+
const useExport = !!this.config.exportPath;
|
|
111
|
+
try {
|
|
112
|
+
// Phase 1: Extract custom instructions (10%)
|
|
113
|
+
if (shouldInclude('instructions')) {
|
|
114
|
+
options.onProgress?.(0.05, 'Extracting custom instructions...');
|
|
115
|
+
try {
|
|
116
|
+
if (useApi) {
|
|
117
|
+
contents.instructions = await this.extractInstructionsApi();
|
|
118
|
+
}
|
|
119
|
+
else if (useExport) {
|
|
120
|
+
contents.instructions = await this.extractInstructionsFromExport();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const msg = `Failed to extract instructions: ${error instanceof Error ? error.message : error}`;
|
|
125
|
+
warnings.push(msg);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
this.progress = 10;
|
|
129
|
+
options.onProgress?.(0.1, 'Instructions extracted');
|
|
130
|
+
// Phase 2: Extract memories (20%)
|
|
131
|
+
if (shouldInclude('memories')) {
|
|
132
|
+
options.onProgress?.(0.15, 'Extracting memories...');
|
|
133
|
+
try {
|
|
134
|
+
if (useApi) {
|
|
135
|
+
contents.memories = await this.extractMemoriesApi();
|
|
136
|
+
}
|
|
137
|
+
else if (useExport) {
|
|
138
|
+
// Memories may be in the export or need API
|
|
139
|
+
contents.memories = await this.extractMemoriesFromExport();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const msg = `Failed to extract memories: ${error instanceof Error ? error.message : error}`;
|
|
144
|
+
warnings.push(msg);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this.progress = 20;
|
|
148
|
+
options.onProgress?.(0.2, 'Memories extracted');
|
|
149
|
+
// Phase 3: Extract conversations (60%)
|
|
150
|
+
if (shouldInclude('conversations')) {
|
|
151
|
+
options.onProgress?.(0.25, 'Extracting conversations...');
|
|
152
|
+
try {
|
|
153
|
+
if (useExport) {
|
|
154
|
+
contents.conversations = await this.extractConversationsFromExport(workDir, (p, m) => {
|
|
155
|
+
const scaledProgress = 0.2 + p * 0.4;
|
|
156
|
+
options.onProgress?.(scaledProgress, m);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Without export, we can't get full conversations via API
|
|
161
|
+
warnings.push('Conversation history requires export file. Request your data from ChatGPT settings.');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
const msg = `Failed to extract conversations: ${error instanceof Error ? error.message : error}`;
|
|
166
|
+
errors.push(msg);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
this.progress = 60;
|
|
170
|
+
options.onProgress?.(0.6, 'Conversations extracted');
|
|
171
|
+
// Phase 4: Extract files (80%)
|
|
172
|
+
if (shouldInclude('files')) {
|
|
173
|
+
options.onProgress?.(0.65, 'Extracting files...');
|
|
174
|
+
try {
|
|
175
|
+
if (useExport) {
|
|
176
|
+
contents.files = await this.extractFilesFromExport(workDir);
|
|
177
|
+
}
|
|
178
|
+
else if (useApi) {
|
|
179
|
+
contents.files = await this.extractFilesApi(workDir);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
const msg = `Failed to extract files: ${error instanceof Error ? error.message : error}`;
|
|
184
|
+
warnings.push(msg);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
this.progress = 80;
|
|
188
|
+
options.onProgress?.(0.8, 'Files extracted');
|
|
189
|
+
// Phase 5: Extract custom GPTs (100%)
|
|
190
|
+
if (shouldInclude('customBots')) {
|
|
191
|
+
options.onProgress?.(0.85, 'Extracting custom GPTs...');
|
|
192
|
+
try {
|
|
193
|
+
if (useApi) {
|
|
194
|
+
contents.customBots = await this.extractGPTsApi();
|
|
195
|
+
}
|
|
196
|
+
else if (useExport) {
|
|
197
|
+
contents.customBots = await this.extractGPTsFromExport();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
const msg = `Failed to extract custom GPTs: ${error instanceof Error ? error.message : error}`;
|
|
202
|
+
warnings.push(msg);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
this.progress = 100;
|
|
206
|
+
options.onProgress?.(1.0, 'Extraction complete');
|
|
207
|
+
// Build metadata
|
|
208
|
+
const metadata = {
|
|
209
|
+
totalItems: this.countItems(contents),
|
|
210
|
+
itemCounts: {
|
|
211
|
+
instructions: contents.instructions ? 1 : 0,
|
|
212
|
+
memories: contents.memories?.count ?? 0,
|
|
213
|
+
conversations: contents.conversations?.count ?? 0,
|
|
214
|
+
files: contents.files?.count ?? 0,
|
|
215
|
+
customBots: contents.customBots?.count ?? 0,
|
|
216
|
+
},
|
|
217
|
+
warnings,
|
|
218
|
+
errors,
|
|
219
|
+
};
|
|
220
|
+
// Build bundle
|
|
221
|
+
const bundle = {
|
|
222
|
+
version: '1.0',
|
|
223
|
+
id: `bundle_${randomBytes(8).toString('hex')}`,
|
|
224
|
+
source: {
|
|
225
|
+
platform: 'chatgpt',
|
|
226
|
+
extractedAt: new Date().toISOString(),
|
|
227
|
+
extractorVersion: this.version,
|
|
228
|
+
},
|
|
229
|
+
contents,
|
|
230
|
+
metadata,
|
|
231
|
+
};
|
|
232
|
+
return bundle;
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
this.progress = 0;
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
getProgress() {
|
|
240
|
+
return this.progress;
|
|
241
|
+
}
|
|
242
|
+
// ─── API-based Extraction ──────────────────────────────────
|
|
243
|
+
async verifyApiAccess() {
|
|
244
|
+
try {
|
|
245
|
+
const response = await this.apiRequest('/models', { method: 'GET' });
|
|
246
|
+
return response.ok;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async apiRequest(endpoint, init = {}) {
|
|
253
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
254
|
+
const headers = {
|
|
255
|
+
'Content-Type': 'application/json',
|
|
256
|
+
...init.headers,
|
|
257
|
+
};
|
|
258
|
+
if (this.config.apiKey) {
|
|
259
|
+
headers['Authorization'] = `Bearer ${this.config.apiKey}`;
|
|
260
|
+
}
|
|
261
|
+
else if (this.config.accessToken) {
|
|
262
|
+
headers['Authorization'] = `Bearer ${this.config.accessToken}`;
|
|
263
|
+
}
|
|
264
|
+
return this.rateLimiter.withRetry(() => fetch(url, { ...init, headers }));
|
|
265
|
+
}
|
|
266
|
+
async extractInstructionsApi() {
|
|
267
|
+
// Note: Custom instructions API is not publicly documented
|
|
268
|
+
// This is a best-effort implementation based on known endpoints
|
|
269
|
+
try {
|
|
270
|
+
// Try the user profile endpoint which may contain custom instructions
|
|
271
|
+
const response = await this.apiRequest('/dashboard/user/system_preferences');
|
|
272
|
+
if (response.ok) {
|
|
273
|
+
const data = await response.json();
|
|
274
|
+
const aboutUser = data.custom_instructions?.about_user ?? '';
|
|
275
|
+
const aboutModel = data.custom_instructions?.about_model ?? '';
|
|
276
|
+
const fullContent = [
|
|
277
|
+
aboutUser && `## About Me\n${aboutUser}`,
|
|
278
|
+
aboutModel && `## How ChatGPT Should Respond\n${aboutModel}`,
|
|
279
|
+
]
|
|
280
|
+
.filter(Boolean)
|
|
281
|
+
.join('\n\n');
|
|
282
|
+
return {
|
|
283
|
+
content: fullContent,
|
|
284
|
+
length: fullContent.length,
|
|
285
|
+
sections: this.parseInstructionSections(fullContent),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// API not available, fall through
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
async extractMemoriesApi() {
|
|
295
|
+
// Note: Memories API endpoint
|
|
296
|
+
try {
|
|
297
|
+
const response = await this.apiRequest('/memories');
|
|
298
|
+
if (response.ok) {
|
|
299
|
+
const data = await response.json();
|
|
300
|
+
const entries = (data.memories ?? []).map((m) => ({
|
|
301
|
+
id: m.id,
|
|
302
|
+
content: m.content,
|
|
303
|
+
createdAt: m.created_at,
|
|
304
|
+
updatedAt: m.updated_at,
|
|
305
|
+
source: 'chatgpt',
|
|
306
|
+
}));
|
|
307
|
+
return {
|
|
308
|
+
entries,
|
|
309
|
+
count: entries.length,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// API not available
|
|
315
|
+
}
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
async extractFilesApi(workDir) {
|
|
319
|
+
const filesDir = join(workDir, 'files');
|
|
320
|
+
await mkdir(filesDir, { recursive: true });
|
|
321
|
+
try {
|
|
322
|
+
const response = await this.apiRequest('/files');
|
|
323
|
+
if (response.ok) {
|
|
324
|
+
const data = await response.json();
|
|
325
|
+
const files = data.data ?? [];
|
|
326
|
+
const entries = [];
|
|
327
|
+
let totalSize = 0;
|
|
328
|
+
for (const file of files) {
|
|
329
|
+
// Download each file
|
|
330
|
+
const fileResponse = await this.apiRequest(`/files/${file.id}/content`);
|
|
331
|
+
if (fileResponse.ok) {
|
|
332
|
+
const buffer = Buffer.from(await fileResponse.arrayBuffer());
|
|
333
|
+
const safeFilename = basename(file.filename).replace(/[/\\]/g, '_');
|
|
334
|
+
const filePath = join(filesDir, safeFilename);
|
|
335
|
+
await writeFile(filePath, buffer);
|
|
336
|
+
entries.push({
|
|
337
|
+
id: file.id,
|
|
338
|
+
filename: safeFilename,
|
|
339
|
+
mimeType: this.guessMimeType(safeFilename),
|
|
340
|
+
size: file.bytes,
|
|
341
|
+
path: `files/${safeFilename}`,
|
|
342
|
+
});
|
|
343
|
+
totalSize += file.bytes;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
files: entries,
|
|
348
|
+
count: entries.length,
|
|
349
|
+
totalSize,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
// API not available
|
|
355
|
+
}
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
async extractGPTsApi() {
|
|
359
|
+
// Note: Custom GPTs API
|
|
360
|
+
try {
|
|
361
|
+
const response = await this.apiRequest('/gizmos/my');
|
|
362
|
+
if (response.ok) {
|
|
363
|
+
const data = await response.json();
|
|
364
|
+
const gpts = data.gizmos ?? [];
|
|
365
|
+
const bots = gpts.map((gpt) => ({
|
|
366
|
+
id: gpt.id,
|
|
367
|
+
name: gpt.name,
|
|
368
|
+
description: gpt.description,
|
|
369
|
+
instructions: gpt.instructions ?? '',
|
|
370
|
+
knowledgeFiles: gpt.knowledge_files?.map((f) => f.name),
|
|
371
|
+
capabilities: gpt.tools,
|
|
372
|
+
createdAt: gpt.created_at,
|
|
373
|
+
updatedAt: gpt.updated_at,
|
|
374
|
+
}));
|
|
375
|
+
return {
|
|
376
|
+
bots,
|
|
377
|
+
count: bots.length,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// API not available
|
|
383
|
+
}
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
// ─── Export-based Extraction ───────────────────────────────
|
|
387
|
+
async verifyExportPath() {
|
|
388
|
+
if (!this.config.exportPath)
|
|
389
|
+
return false;
|
|
390
|
+
try {
|
|
391
|
+
await access(this.config.exportPath, constants.R_OK);
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async extractInstructionsFromExport() {
|
|
399
|
+
const modelSpecPath = join(this.config.exportPath, 'model_comparisons.json');
|
|
400
|
+
try {
|
|
401
|
+
const content = await readFile(modelSpecPath, 'utf-8');
|
|
402
|
+
const data = JSON.parse(content);
|
|
403
|
+
// Find custom instructions in the model spec
|
|
404
|
+
const spec = data[0];
|
|
405
|
+
if (spec?.custom_instructions) {
|
|
406
|
+
const aboutUser = spec.custom_instructions.about_user_message ?? '';
|
|
407
|
+
const aboutModel = spec.custom_instructions.about_model_message ?? '';
|
|
408
|
+
const fullContent = [
|
|
409
|
+
aboutUser && `## About Me\n${aboutUser}`,
|
|
410
|
+
aboutModel && `## How ChatGPT Should Respond\n${aboutModel}`,
|
|
411
|
+
]
|
|
412
|
+
.filter(Boolean)
|
|
413
|
+
.join('\n\n');
|
|
414
|
+
return {
|
|
415
|
+
content: fullContent,
|
|
416
|
+
length: fullContent.length,
|
|
417
|
+
sections: this.parseInstructionSections(fullContent),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// Try alternative location: user.json
|
|
423
|
+
try {
|
|
424
|
+
const userPath = join(this.config.exportPath, 'user.json');
|
|
425
|
+
const content = await readFile(userPath, 'utf-8');
|
|
426
|
+
const data = JSON.parse(content);
|
|
427
|
+
if (data.custom_instructions) {
|
|
428
|
+
const aboutUser = data.custom_instructions.about_user ?? '';
|
|
429
|
+
const aboutModel = data.custom_instructions.about_model ?? '';
|
|
430
|
+
const fullContent = [
|
|
431
|
+
aboutUser && `## About Me\n${aboutUser}`,
|
|
432
|
+
aboutModel && `## How ChatGPT Should Respond\n${aboutModel}`,
|
|
433
|
+
]
|
|
434
|
+
.filter(Boolean)
|
|
435
|
+
.join('\n\n');
|
|
436
|
+
return {
|
|
437
|
+
content: fullContent,
|
|
438
|
+
length: fullContent.length,
|
|
439
|
+
sections: this.parseInstructionSections(fullContent),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// No custom instructions found
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
async extractMemoriesFromExport() {
|
|
450
|
+
const memoriesPath = join(this.config.exportPath, 'memories.json');
|
|
451
|
+
try {
|
|
452
|
+
const content = await readFile(memoriesPath, 'utf-8');
|
|
453
|
+
const data = JSON.parse(content);
|
|
454
|
+
const entries = data.map((m) => ({
|
|
455
|
+
id: m.id,
|
|
456
|
+
content: m.content,
|
|
457
|
+
createdAt: m.created_at,
|
|
458
|
+
updatedAt: m.updated_at,
|
|
459
|
+
source: 'chatgpt-export',
|
|
460
|
+
}));
|
|
461
|
+
return {
|
|
462
|
+
entries,
|
|
463
|
+
count: entries.length,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// No memories file
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async extractConversationsFromExport(workDir, onProgress) {
|
|
472
|
+
const conversationsPath = join(this.config.exportPath, 'conversations.json');
|
|
473
|
+
const outputDir = join(workDir, 'conversations');
|
|
474
|
+
await mkdir(outputDir, { recursive: true });
|
|
475
|
+
const summaries = [];
|
|
476
|
+
let totalMessages = 0;
|
|
477
|
+
// Use streaming parser for large files
|
|
478
|
+
const fileStats = await stat(conversationsPath);
|
|
479
|
+
const isLargeFile = fileStats.size > 50 * 1024 * 1024; // 50MB threshold
|
|
480
|
+
if (isLargeFile) {
|
|
481
|
+
// Stream parse for memory efficiency
|
|
482
|
+
const result = await this.streamParseConversations(conversationsPath, outputDir, onProgress);
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
// Regular parsing for smaller files
|
|
486
|
+
const content = await readFile(conversationsPath, 'utf-8');
|
|
487
|
+
const conversations = JSON.parse(content);
|
|
488
|
+
const maxConversations = this.config.maxConversations ?? conversations.length;
|
|
489
|
+
const toProcess = conversations.slice(0, maxConversations);
|
|
490
|
+
for (let i = 0; i < toProcess.length; i++) {
|
|
491
|
+
const conv = toProcess[i];
|
|
492
|
+
const progress = (i + 1) / toProcess.length;
|
|
493
|
+
onProgress?.(progress, `Processing conversation ${i + 1}/${toProcess.length}`);
|
|
494
|
+
// Extract messages from the conversation mapping
|
|
495
|
+
const messages = this.extractMessagesFromMapping(conv.mapping, conv.current_node);
|
|
496
|
+
totalMessages += messages.length;
|
|
497
|
+
// Save individual conversation
|
|
498
|
+
const convData = {
|
|
499
|
+
id: conv.id,
|
|
500
|
+
title: conv.title,
|
|
501
|
+
createdAt: new Date(conv.create_time * 1000).toISOString(),
|
|
502
|
+
updatedAt: new Date(conv.update_time * 1000).toISOString(),
|
|
503
|
+
messages,
|
|
504
|
+
gptId: conv.gizmo_id,
|
|
505
|
+
};
|
|
506
|
+
// Sanitize conv.id to prevent path traversal
|
|
507
|
+
const safeConvId = basename(conv.id).replace(/[/\\]/g, '_');
|
|
508
|
+
const convPath = join(outputDir, `${safeConvId}.json`);
|
|
509
|
+
await writeFile(convPath, JSON.stringify(convData, null, 2));
|
|
510
|
+
summaries.push({
|
|
511
|
+
id: conv.id,
|
|
512
|
+
title: conv.title || 'Untitled',
|
|
513
|
+
messageCount: messages.length,
|
|
514
|
+
createdAt: convData.createdAt,
|
|
515
|
+
updatedAt: convData.updatedAt,
|
|
516
|
+
keyPoints: this.extractKeyPoints(messages),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
path: 'conversations/',
|
|
521
|
+
count: summaries.length,
|
|
522
|
+
messageCount: totalMessages,
|
|
523
|
+
summaries,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
async streamParseConversations(filePath, outputDir, onProgress) {
|
|
527
|
+
const summaries = [];
|
|
528
|
+
let totalMessages = 0;
|
|
529
|
+
let conversationCount = 0;
|
|
530
|
+
// For very large files, we use a streaming JSON parser approach
|
|
531
|
+
// Read the file in chunks and parse conversations one at a time
|
|
532
|
+
const fileContent = await readFile(filePath, 'utf-8');
|
|
533
|
+
const conversations = JSON.parse(fileContent);
|
|
534
|
+
const maxConversations = this.config.maxConversations ?? conversations.length;
|
|
535
|
+
const toProcess = conversations.slice(0, maxConversations);
|
|
536
|
+
for (const conv of toProcess) {
|
|
537
|
+
conversationCount++;
|
|
538
|
+
const progress = conversationCount / toProcess.length;
|
|
539
|
+
onProgress?.(progress, `Streaming conversation ${conversationCount}/${toProcess.length}`);
|
|
540
|
+
const messages = this.extractMessagesFromMapping(conv.mapping, conv.current_node);
|
|
541
|
+
totalMessages += messages.length;
|
|
542
|
+
const convData = {
|
|
543
|
+
id: conv.id,
|
|
544
|
+
title: conv.title,
|
|
545
|
+
createdAt: new Date(conv.create_time * 1000).toISOString(),
|
|
546
|
+
updatedAt: new Date(conv.update_time * 1000).toISOString(),
|
|
547
|
+
messages,
|
|
548
|
+
gptId: conv.gizmo_id,
|
|
549
|
+
};
|
|
550
|
+
// Sanitize conv.id to prevent path traversal
|
|
551
|
+
const safeConvId = basename(conv.id).replace(/[/\\]/g, '_');
|
|
552
|
+
const convPath = join(outputDir, `${safeConvId}.json`);
|
|
553
|
+
await writeFile(convPath, JSON.stringify(convData, null, 2));
|
|
554
|
+
summaries.push({
|
|
555
|
+
id: conv.id,
|
|
556
|
+
title: conv.title || 'Untitled',
|
|
557
|
+
messageCount: messages.length,
|
|
558
|
+
createdAt: convData.createdAt,
|
|
559
|
+
updatedAt: convData.updatedAt,
|
|
560
|
+
});
|
|
561
|
+
// Allow GC to reclaim memory periodically
|
|
562
|
+
if (conversationCount % 100 === 0) {
|
|
563
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
path: 'conversations/',
|
|
568
|
+
count: summaries.length,
|
|
569
|
+
messageCount: totalMessages,
|
|
570
|
+
summaries,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
extractMessagesFromMapping(mapping, currentNode) {
|
|
574
|
+
const messages = [];
|
|
575
|
+
// Build the message chain by following parent links
|
|
576
|
+
const visited = new Set();
|
|
577
|
+
const orderedIds = [];
|
|
578
|
+
// Find root node (node with no parent or parent not in mapping)
|
|
579
|
+
let rootId;
|
|
580
|
+
for (const [id, node] of Object.entries(mapping)) {
|
|
581
|
+
if (!node.parent || !mapping[node.parent]) {
|
|
582
|
+
rootId = id;
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Traverse from root to build ordered list
|
|
587
|
+
const traverse = (nodeId) => {
|
|
588
|
+
if (visited.has(nodeId))
|
|
589
|
+
return;
|
|
590
|
+
visited.add(nodeId);
|
|
591
|
+
const node = mapping[nodeId];
|
|
592
|
+
if (!node)
|
|
593
|
+
return;
|
|
594
|
+
if (node.message?.content) {
|
|
595
|
+
orderedIds.push(nodeId);
|
|
596
|
+
}
|
|
597
|
+
// Process children
|
|
598
|
+
for (const childId of node.children ?? []) {
|
|
599
|
+
traverse(childId);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
if (rootId) {
|
|
603
|
+
traverse(rootId);
|
|
604
|
+
}
|
|
605
|
+
// Extract message content
|
|
606
|
+
for (const nodeId of orderedIds) {
|
|
607
|
+
const node = mapping[nodeId];
|
|
608
|
+
if (!node.message)
|
|
609
|
+
continue;
|
|
610
|
+
const msg = node.message;
|
|
611
|
+
let content = '';
|
|
612
|
+
if (msg.content.parts) {
|
|
613
|
+
content = msg.content.parts
|
|
614
|
+
.map((part) => (typeof part === 'string' ? part : JSON.stringify(part)))
|
|
615
|
+
.join('');
|
|
616
|
+
}
|
|
617
|
+
else if (msg.content.text) {
|
|
618
|
+
content = msg.content.text;
|
|
619
|
+
}
|
|
620
|
+
if (!content.trim())
|
|
621
|
+
continue;
|
|
622
|
+
const attachments = msg.metadata?.attachments?.map((att) => ({
|
|
623
|
+
id: att.id,
|
|
624
|
+
name: att.name,
|
|
625
|
+
mimeType: att.mime_type,
|
|
626
|
+
}));
|
|
627
|
+
messages.push({
|
|
628
|
+
id: msg.id,
|
|
629
|
+
role: msg.author.role,
|
|
630
|
+
content,
|
|
631
|
+
timestamp: msg.create_time
|
|
632
|
+
? new Date(msg.create_time * 1000).toISOString()
|
|
633
|
+
: undefined,
|
|
634
|
+
model: msg.metadata?.model_slug,
|
|
635
|
+
attachments,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
return messages;
|
|
639
|
+
}
|
|
640
|
+
async extractFilesFromExport(workDir) {
|
|
641
|
+
const filesDir = join(workDir, 'files');
|
|
642
|
+
await mkdir(filesDir, { recursive: true });
|
|
643
|
+
const sourceFilesDir = join(this.config.exportPath, 'files');
|
|
644
|
+
const entries = [];
|
|
645
|
+
let totalSize = 0;
|
|
646
|
+
try {
|
|
647
|
+
const files = await readdir(sourceFilesDir);
|
|
648
|
+
for (const filename of files) {
|
|
649
|
+
const sourcePath = join(sourceFilesDir, filename);
|
|
650
|
+
const stats = await stat(sourcePath);
|
|
651
|
+
if (stats.isFile()) {
|
|
652
|
+
// Copy file to work directory
|
|
653
|
+
// Sanitize filename to prevent path traversal attacks
|
|
654
|
+
const safeFilename = basename(filename);
|
|
655
|
+
const content = await readFile(sourcePath);
|
|
656
|
+
const destPath = join(filesDir, safeFilename);
|
|
657
|
+
await writeFile(destPath, content);
|
|
658
|
+
entries.push({
|
|
659
|
+
id: createHash('md5').update(safeFilename).digest('hex'),
|
|
660
|
+
filename: safeFilename,
|
|
661
|
+
mimeType: this.guessMimeType(safeFilename),
|
|
662
|
+
size: stats.size,
|
|
663
|
+
path: `files/${safeFilename}`,
|
|
664
|
+
});
|
|
665
|
+
totalSize += stats.size;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
// No files directory in export
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
files: entries,
|
|
674
|
+
count: entries.length,
|
|
675
|
+
totalSize,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
async extractGPTsFromExport() {
|
|
679
|
+
const gptsPath = join(this.config.exportPath, 'gpts.json');
|
|
680
|
+
try {
|
|
681
|
+
const content = await readFile(gptsPath, 'utf-8');
|
|
682
|
+
const gpts = JSON.parse(content);
|
|
683
|
+
const bots = gpts.map((gpt) => ({
|
|
684
|
+
id: gpt.id,
|
|
685
|
+
name: gpt.name,
|
|
686
|
+
description: gpt.description,
|
|
687
|
+
instructions: gpt.instructions ?? '',
|
|
688
|
+
knowledgeFiles: gpt.knowledge_files?.map((f) => f.name),
|
|
689
|
+
capabilities: gpt.tools,
|
|
690
|
+
createdAt: gpt.created_at,
|
|
691
|
+
updatedAt: gpt.updated_at,
|
|
692
|
+
}));
|
|
693
|
+
return {
|
|
694
|
+
bots,
|
|
695
|
+
count: bots.length,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
// No GPTs file in export
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// ─── Helpers ───────────────────────────────────────────────
|
|
704
|
+
parseInstructionSections(content) {
|
|
705
|
+
const sections = [];
|
|
706
|
+
const lines = content.split('\n');
|
|
707
|
+
let currentSection = null;
|
|
708
|
+
let currentContent = [];
|
|
709
|
+
for (const line of lines) {
|
|
710
|
+
const headerMatch = line.match(/^#+\s+(.+)$/);
|
|
711
|
+
if (headerMatch) {
|
|
712
|
+
// Save previous section
|
|
713
|
+
if (currentSection) {
|
|
714
|
+
currentSection.content = currentContent.join('\n').trim();
|
|
715
|
+
sections.push(currentSection);
|
|
716
|
+
}
|
|
717
|
+
// Start new section
|
|
718
|
+
currentSection = {
|
|
719
|
+
title: headerMatch[1],
|
|
720
|
+
content: '',
|
|
721
|
+
priority: line.startsWith('##') ? 'high' : 'medium',
|
|
722
|
+
};
|
|
723
|
+
currentContent = [];
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
currentContent.push(line);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// Save last section
|
|
730
|
+
if (currentSection) {
|
|
731
|
+
currentSection.content = currentContent.join('\n').trim();
|
|
732
|
+
sections.push(currentSection);
|
|
733
|
+
}
|
|
734
|
+
return sections;
|
|
735
|
+
}
|
|
736
|
+
extractKeyPoints(messages) {
|
|
737
|
+
// Extract key decisions/conclusions from assistant messages
|
|
738
|
+
const keyPoints = [];
|
|
739
|
+
const assistantMessages = messages.filter((m) => m.role === 'assistant');
|
|
740
|
+
for (const msg of assistantMessages.slice(-5)) {
|
|
741
|
+
// Check last 5 messages
|
|
742
|
+
const content = msg.content;
|
|
743
|
+
// Look for conclusion markers
|
|
744
|
+
const conclusionPatterns = [
|
|
745
|
+
/(?:in summary|to summarize|in conclusion|the key points? (?:are|is)):?\s*(.{20,200})/gi,
|
|
746
|
+
/(?:the (?:main|key) takeaway is):?\s*(.{20,200})/gi,
|
|
747
|
+
/(?:decided|agreed|concluded) (?:to|that):?\s*(.{20,200})/gi,
|
|
748
|
+
];
|
|
749
|
+
for (const pattern of conclusionPatterns) {
|
|
750
|
+
const match = pattern.exec(content);
|
|
751
|
+
if (match?.[1]) {
|
|
752
|
+
keyPoints.push(match[1].trim());
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return keyPoints.slice(0, 5); // Max 5 key points
|
|
757
|
+
}
|
|
758
|
+
guessMimeType(filename) {
|
|
759
|
+
const ext = extname(filename).toLowerCase();
|
|
760
|
+
const mimeTypes = {
|
|
761
|
+
'.txt': 'text/plain',
|
|
762
|
+
'.md': 'text/markdown',
|
|
763
|
+
'.json': 'application/json',
|
|
764
|
+
'.pdf': 'application/pdf',
|
|
765
|
+
'.png': 'image/png',
|
|
766
|
+
'.jpg': 'image/jpeg',
|
|
767
|
+
'.jpeg': 'image/jpeg',
|
|
768
|
+
'.gif': 'image/gif',
|
|
769
|
+
'.webp': 'image/webp',
|
|
770
|
+
'.svg': 'image/svg+xml',
|
|
771
|
+
'.csv': 'text/csv',
|
|
772
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
773
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
774
|
+
'.py': 'text/x-python',
|
|
775
|
+
'.js': 'text/javascript',
|
|
776
|
+
'.ts': 'text/typescript',
|
|
777
|
+
'.html': 'text/html',
|
|
778
|
+
'.css': 'text/css',
|
|
779
|
+
'.zip': 'application/zip',
|
|
780
|
+
};
|
|
781
|
+
return mimeTypes[ext] ?? 'application/octet-stream';
|
|
782
|
+
}
|
|
783
|
+
countItems(contents) {
|
|
784
|
+
return ((contents.instructions ? 1 : 0) +
|
|
785
|
+
(contents.memories?.count ?? 0) +
|
|
786
|
+
(contents.conversations?.count ?? 0) +
|
|
787
|
+
(contents.files?.count ?? 0) +
|
|
788
|
+
(contents.customBots?.count ?? 0));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
//# sourceMappingURL=chatgpt.js.map
|