@savestate/cli 0.1.0 → 0.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 +2 -12
- package/dist/adapters/chatgpt.d.ts +113 -0
- package/dist/adapters/chatgpt.d.ts.map +1 -0
- package/dist/adapters/chatgpt.js +668 -0
- package/dist/adapters/chatgpt.js.map +1 -0
- package/dist/adapters/claude-web.d.ts +81 -0
- package/dist/adapters/claude-web.d.ts.map +1 -0
- package/dist/adapters/claude-web.js +903 -0
- package/dist/adapters/claude-web.js.map +1 -0
- package/dist/adapters/gemini.d.ts +108 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +814 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/openai-assistants.d.ts +79 -36
- package/dist/adapters/openai-assistants.d.ts.map +1 -1
- package/dist/adapters/openai-assistants.js +802 -78
- package/dist/adapters/openai-assistants.js.map +1 -1
- package/dist/adapters/registry.d.ts.map +1 -1
- package/dist/adapters/registry.js +6 -4
- package/dist/adapters/registry.js.map +1 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -10
- package/dist/cli.js.map +1 -1
- package/dist/commands/adapters.js +5 -5
- package/dist/commands/adapters.js.map +1 -1
- package/dist/commands/index.d.ts +0 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +0 -1
- package/dist/commands/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Web (claude.ai) Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapter for Claude consumer web interface (claude.ai).
|
|
5
|
+
* This is DIFFERENT from the claude-code adapter which handles
|
|
6
|
+
* Claude Code CLI projects. This adapter handles:
|
|
7
|
+
*
|
|
8
|
+
* - Claude.ai data export (conversations, account info)
|
|
9
|
+
* - Claude memory export (text/markdown memories)
|
|
10
|
+
* - Claude Projects (instructions, uploaded files, conversations)
|
|
11
|
+
*
|
|
12
|
+
* Data sources:
|
|
13
|
+
* 1. SAVESTATE_CLAUDE_EXPORT env var → export directory
|
|
14
|
+
* 2. claude-export/ in current directory
|
|
15
|
+
* 3. .savestate/imports/claude/ directory
|
|
16
|
+
* 4. Standalone memory file (claude-memories.md or .txt)
|
|
17
|
+
*
|
|
18
|
+
* Export format (Anthropic data export):
|
|
19
|
+
* conversations/ — JSON files per conversation
|
|
20
|
+
* account_info.json — user profile
|
|
21
|
+
* projects/ — project data (instructions, files)
|
|
22
|
+
*
|
|
23
|
+
* Conversation JSON format:
|
|
24
|
+
* { uuid, name, created_at, updated_at, chat_messages: [{ uuid, text, sender, created_at, attachments, files }] }
|
|
25
|
+
*/
|
|
26
|
+
import { readFile, writeFile, readdir, stat, mkdir } from 'node:fs/promises';
|
|
27
|
+
import { existsSync } from 'node:fs';
|
|
28
|
+
import { join, extname, basename } from 'node:path';
|
|
29
|
+
import { SAF_VERSION, generateSnapshotId, computeChecksum } from '../format.js';
|
|
30
|
+
// ─── Constants ───────────────────────────────────────────────
|
|
31
|
+
/** Possible export directory names to search for */
|
|
32
|
+
const EXPORT_DIR_CANDIDATES = [
|
|
33
|
+
'claude-export',
|
|
34
|
+
'claude_export',
|
|
35
|
+
'claude-data-export',
|
|
36
|
+
'claude_data_export',
|
|
37
|
+
];
|
|
38
|
+
/** Possible memory file names */
|
|
39
|
+
const MEMORY_FILE_CANDIDATES = [
|
|
40
|
+
'claude-memories.md',
|
|
41
|
+
'claude-memories.txt',
|
|
42
|
+
'claude_memories.md',
|
|
43
|
+
'claude_memories.txt',
|
|
44
|
+
'memories.md',
|
|
45
|
+
'memories.txt',
|
|
46
|
+
];
|
|
47
|
+
/** Maximum file size to read (5MB for export files) */
|
|
48
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
49
|
+
export class ClaudeWebAdapter {
|
|
50
|
+
id = 'claude-web';
|
|
51
|
+
name = 'Claude Web (claude.ai)';
|
|
52
|
+
platform = 'claude-web';
|
|
53
|
+
version = '0.1.0';
|
|
54
|
+
exportDir = null;
|
|
55
|
+
memoryFile = null;
|
|
56
|
+
baseDir;
|
|
57
|
+
warnings = [];
|
|
58
|
+
constructor(baseDir) {
|
|
59
|
+
this.baseDir = baseDir ?? process.cwd();
|
|
60
|
+
}
|
|
61
|
+
async detect() {
|
|
62
|
+
const result = await this.findDataSources();
|
|
63
|
+
return result.found;
|
|
64
|
+
}
|
|
65
|
+
async extract() {
|
|
66
|
+
this.warnings = [];
|
|
67
|
+
const sources = await this.findDataSources();
|
|
68
|
+
if (!sources.found) {
|
|
69
|
+
throw new Error('No Claude.ai data found. Set SAVESTATE_CLAUDE_EXPORT to your export directory, ' +
|
|
70
|
+
'place export in claude-export/, or provide a claude-memories.md file.');
|
|
71
|
+
}
|
|
72
|
+
this.exportDir = sources.exportDir;
|
|
73
|
+
this.memoryFile = sources.memoryFile;
|
|
74
|
+
// Extract all data
|
|
75
|
+
const accountInfo = await this.readAccountInfo();
|
|
76
|
+
const conversations = await this.readConversations();
|
|
77
|
+
const memoryEntries = await this.readMemories();
|
|
78
|
+
const projects = await this.readProjects();
|
|
79
|
+
const knowledge = this.buildKnowledge(projects);
|
|
80
|
+
// Build personality from project instructions
|
|
81
|
+
const personality = this.buildPersonality(projects);
|
|
82
|
+
const snapshotId = generateSnapshotId();
|
|
83
|
+
const now = new Date().toISOString();
|
|
84
|
+
// Report findings
|
|
85
|
+
const convoCount = conversations.length;
|
|
86
|
+
const memoryCount = memoryEntries.length;
|
|
87
|
+
const projectCount = projects.length;
|
|
88
|
+
console.log(` Found ${convoCount} conversations, ${memoryCount} memories, ${projectCount} projects`);
|
|
89
|
+
if (this.warnings.length > 0) {
|
|
90
|
+
for (const w of this.warnings) {
|
|
91
|
+
console.warn(` ⚠ ${w}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Build conversation index
|
|
95
|
+
const conversationIndex = conversations.map((c) => ({
|
|
96
|
+
id: c.id,
|
|
97
|
+
title: c.title,
|
|
98
|
+
createdAt: c.createdAt,
|
|
99
|
+
updatedAt: c.updatedAt,
|
|
100
|
+
messageCount: c.messages.length,
|
|
101
|
+
path: `conversations/${c.id}.json`,
|
|
102
|
+
}));
|
|
103
|
+
const snapshot = {
|
|
104
|
+
manifest: {
|
|
105
|
+
version: SAF_VERSION,
|
|
106
|
+
timestamp: now,
|
|
107
|
+
id: snapshotId,
|
|
108
|
+
platform: this.platform,
|
|
109
|
+
adapter: this.id,
|
|
110
|
+
checksum: '',
|
|
111
|
+
size: 0,
|
|
112
|
+
},
|
|
113
|
+
identity: {
|
|
114
|
+
personality: personality || undefined,
|
|
115
|
+
config: accountInfo ? { accountInfo } : undefined,
|
|
116
|
+
tools: [],
|
|
117
|
+
},
|
|
118
|
+
memory: {
|
|
119
|
+
core: memoryEntries,
|
|
120
|
+
knowledge,
|
|
121
|
+
},
|
|
122
|
+
conversations: {
|
|
123
|
+
total: conversationIndex.length,
|
|
124
|
+
conversations: conversationIndex,
|
|
125
|
+
},
|
|
126
|
+
platform: await this.identify(),
|
|
127
|
+
chain: {
|
|
128
|
+
current: snapshotId,
|
|
129
|
+
ancestors: [],
|
|
130
|
+
},
|
|
131
|
+
restoreHints: {
|
|
132
|
+
platform: this.platform,
|
|
133
|
+
steps: [
|
|
134
|
+
{
|
|
135
|
+
type: 'manual',
|
|
136
|
+
description: 'Import memories via Claude.ai Settings → Memory',
|
|
137
|
+
target: 'memory/',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'manual',
|
|
141
|
+
description: 'Create Claude Projects with restored instructions and files',
|
|
142
|
+
target: 'identity/',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: 'file',
|
|
146
|
+
description: 'Generate claude-restore-guide.md with organized restore instructions',
|
|
147
|
+
target: 'claude-restore-guide.md',
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
manualSteps: [
|
|
151
|
+
'Claude.ai does not support automated restore — data must be manually re-entered',
|
|
152
|
+
'Memories: Copy each memory entry from the restore guide into Claude.ai Settings → Memory',
|
|
153
|
+
'Projects: Create new Projects in Claude.ai and paste instructions + upload files',
|
|
154
|
+
'Conversations: Cannot be restored (read-only history)',
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
return snapshot;
|
|
159
|
+
}
|
|
160
|
+
async restore(snapshot) {
|
|
161
|
+
// Claude consumer has limited restore capabilities.
|
|
162
|
+
// Generate a comprehensive restore guide for manual import.
|
|
163
|
+
const restoreDir = join(this.baseDir, '.savestate', 'restore', 'claude-web');
|
|
164
|
+
await mkdir(restoreDir, { recursive: true });
|
|
165
|
+
const guide = this.generateRestoreGuide(snapshot);
|
|
166
|
+
const guidePath = join(restoreDir, 'claude-restore-guide.md');
|
|
167
|
+
await writeFile(guidePath, guide, 'utf-8');
|
|
168
|
+
// Export memories as a standalone file for easy copy-paste
|
|
169
|
+
if (snapshot.memory.core.length > 0) {
|
|
170
|
+
const memoriesContent = this.formatMemoriesForRestore(snapshot.memory.core);
|
|
171
|
+
const memoriesPath = join(restoreDir, 'memories-to-import.md');
|
|
172
|
+
await writeFile(memoriesPath, memoriesContent, 'utf-8');
|
|
173
|
+
console.log(` 📝 Memories file: ${memoriesPath}`);
|
|
174
|
+
}
|
|
175
|
+
// Export project instructions as individual files
|
|
176
|
+
if (snapshot.identity.personality) {
|
|
177
|
+
const projectsDir = join(restoreDir, 'projects');
|
|
178
|
+
await mkdir(projectsDir, { recursive: true });
|
|
179
|
+
const instructionsPath = join(projectsDir, 'project-instructions.md');
|
|
180
|
+
await writeFile(instructionsPath, snapshot.identity.personality, 'utf-8');
|
|
181
|
+
console.log(` 📋 Project instructions: ${instructionsPath}`);
|
|
182
|
+
}
|
|
183
|
+
console.log(` 📖 Restore guide: ${guidePath}`);
|
|
184
|
+
console.log();
|
|
185
|
+
console.log(' ℹ️ Claude.ai requires manual restore. See the guide for step-by-step instructions.');
|
|
186
|
+
}
|
|
187
|
+
async identify() {
|
|
188
|
+
const accountInfo = await this.readAccountInfo();
|
|
189
|
+
return {
|
|
190
|
+
name: 'Claude Web (claude.ai)',
|
|
191
|
+
version: 'consumer',
|
|
192
|
+
exportMethod: 'data-export',
|
|
193
|
+
accountId: accountInfo?.uuid ?? accountInfo?.email ?? undefined,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// ─── Private: Data Source Discovery ───────────────────────
|
|
197
|
+
async findDataSources() {
|
|
198
|
+
let exportDir = null;
|
|
199
|
+
let memoryFile = null;
|
|
200
|
+
// 1. Check SAVESTATE_CLAUDE_EXPORT env var
|
|
201
|
+
const envDir = process.env.SAVESTATE_CLAUDE_EXPORT;
|
|
202
|
+
if (envDir && existsSync(envDir)) {
|
|
203
|
+
const s = await stat(envDir).catch(() => null);
|
|
204
|
+
if (s?.isDirectory()) {
|
|
205
|
+
exportDir = envDir;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// 2. Check standard directory names in current dir
|
|
209
|
+
if (!exportDir) {
|
|
210
|
+
for (const candidate of EXPORT_DIR_CANDIDATES) {
|
|
211
|
+
const candidatePath = join(this.baseDir, candidate);
|
|
212
|
+
if (existsSync(candidatePath)) {
|
|
213
|
+
const s = await stat(candidatePath).catch(() => null);
|
|
214
|
+
if (s?.isDirectory()) {
|
|
215
|
+
exportDir = candidatePath;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// 3. Check .savestate/imports/claude/
|
|
222
|
+
if (!exportDir) {
|
|
223
|
+
const importsDir = join(this.baseDir, '.savestate', 'imports', 'claude');
|
|
224
|
+
if (existsSync(importsDir)) {
|
|
225
|
+
const s = await stat(importsDir).catch(() => null);
|
|
226
|
+
if (s?.isDirectory()) {
|
|
227
|
+
exportDir = importsDir;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// 4. Check for standalone memory file
|
|
232
|
+
for (const candidate of MEMORY_FILE_CANDIDATES) {
|
|
233
|
+
// Check in base dir
|
|
234
|
+
const basePath = join(this.baseDir, candidate);
|
|
235
|
+
if (existsSync(basePath)) {
|
|
236
|
+
memoryFile = basePath;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
// Check in export dir if found
|
|
240
|
+
if (exportDir) {
|
|
241
|
+
const exportPath = join(exportDir, candidate);
|
|
242
|
+
if (existsSync(exportPath)) {
|
|
243
|
+
memoryFile = exportPath;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Check in .savestate/imports/claude/
|
|
248
|
+
const importPath = join(this.baseDir, '.savestate', 'imports', 'claude', candidate);
|
|
249
|
+
if (existsSync(importPath)) {
|
|
250
|
+
memoryFile = importPath;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
found: exportDir !== null || memoryFile !== null,
|
|
256
|
+
exportDir,
|
|
257
|
+
memoryFile,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// ─── Private: Account Info ────────────────────────────────
|
|
261
|
+
async readAccountInfo() {
|
|
262
|
+
if (!this.exportDir)
|
|
263
|
+
return null;
|
|
264
|
+
const candidates = ['account_info.json', 'account.json', 'profile.json'];
|
|
265
|
+
for (const filename of candidates) {
|
|
266
|
+
const filePath = join(this.exportDir, filename);
|
|
267
|
+
if (existsSync(filePath)) {
|
|
268
|
+
const content = await this.safeReadFile(filePath);
|
|
269
|
+
if (content) {
|
|
270
|
+
try {
|
|
271
|
+
return JSON.parse(content);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
this.warnings.push(`Failed to parse ${filename}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
// ─── Private: Conversations ───────────────────────────────
|
|
282
|
+
async readConversations() {
|
|
283
|
+
if (!this.exportDir)
|
|
284
|
+
return [];
|
|
285
|
+
const conversations = [];
|
|
286
|
+
// Look for conversations/ directory
|
|
287
|
+
const convDirCandidates = ['conversations', 'chats'];
|
|
288
|
+
for (const dirName of convDirCandidates) {
|
|
289
|
+
const convDir = join(this.exportDir, dirName);
|
|
290
|
+
if (!existsSync(convDir))
|
|
291
|
+
continue;
|
|
292
|
+
const s = await stat(convDir).catch(() => null);
|
|
293
|
+
if (!s?.isDirectory())
|
|
294
|
+
continue;
|
|
295
|
+
const files = await readdir(convDir).catch(() => []);
|
|
296
|
+
for (const file of files) {
|
|
297
|
+
if (!file.endsWith('.json'))
|
|
298
|
+
continue;
|
|
299
|
+
const filePath = join(convDir, file);
|
|
300
|
+
const parsed = await this.parseConversationFile(filePath);
|
|
301
|
+
if (parsed) {
|
|
302
|
+
conversations.push(parsed);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Also check if there's a single conversations.json file (some export formats)
|
|
307
|
+
const singleFile = join(this.exportDir, 'conversations.json');
|
|
308
|
+
if (existsSync(singleFile) && conversations.length === 0) {
|
|
309
|
+
const content = await this.safeReadFile(singleFile);
|
|
310
|
+
if (content) {
|
|
311
|
+
try {
|
|
312
|
+
const data = JSON.parse(content);
|
|
313
|
+
// Could be an array of conversations
|
|
314
|
+
if (Array.isArray(data)) {
|
|
315
|
+
for (const raw of data) {
|
|
316
|
+
const parsed = this.parseConversationObject(raw);
|
|
317
|
+
if (parsed) {
|
|
318
|
+
conversations.push(parsed);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
this.warnings.push('Failed to parse conversations.json');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return conversations;
|
|
329
|
+
}
|
|
330
|
+
async parseConversationFile(filePath) {
|
|
331
|
+
const content = await this.safeReadFile(filePath);
|
|
332
|
+
if (!content)
|
|
333
|
+
return null;
|
|
334
|
+
try {
|
|
335
|
+
const raw = JSON.parse(content);
|
|
336
|
+
return this.parseConversationObject(raw);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
this.warnings.push(`Failed to parse conversation: ${basename(filePath)}`);
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
parseConversationObject(raw) {
|
|
344
|
+
if (!raw)
|
|
345
|
+
return null;
|
|
346
|
+
const id = raw.uuid ?? raw.id ?? `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
347
|
+
const title = raw.name ?? raw.title ?? undefined;
|
|
348
|
+
const createdAt = raw.created_at ?? new Date().toISOString();
|
|
349
|
+
const updatedAt = raw.updated_at ?? createdAt;
|
|
350
|
+
// Handle both "chat_messages" and "messages" field names
|
|
351
|
+
const rawMessages = raw.chat_messages ?? raw.messages ?? [];
|
|
352
|
+
const messages = [];
|
|
353
|
+
for (const msg of rawMessages) {
|
|
354
|
+
const parsed = this.parseMessage(msg);
|
|
355
|
+
if (parsed) {
|
|
356
|
+
messages.push(parsed);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (messages.length === 0)
|
|
360
|
+
return null;
|
|
361
|
+
return { id, title, createdAt, updatedAt, messages };
|
|
362
|
+
}
|
|
363
|
+
parseMessage(msg) {
|
|
364
|
+
if (!msg)
|
|
365
|
+
return null;
|
|
366
|
+
const id = msg.uuid ?? msg.id ?? `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
367
|
+
const text = msg.text ?? msg.content ?? '';
|
|
368
|
+
if (!text && (!msg.attachments || msg.attachments.length === 0)) {
|
|
369
|
+
return null; // Skip empty messages
|
|
370
|
+
}
|
|
371
|
+
// Map Claude's sender field to SaveState roles
|
|
372
|
+
const rawRole = msg.sender ?? msg.role ?? 'user';
|
|
373
|
+
const role = this.mapRole(rawRole);
|
|
374
|
+
const timestamp = msg.created_at ?? msg.timestamp ?? new Date().toISOString();
|
|
375
|
+
// Build content including attachment references
|
|
376
|
+
let content = text;
|
|
377
|
+
if (msg.attachments && msg.attachments.length > 0) {
|
|
378
|
+
const attachmentTexts = msg.attachments
|
|
379
|
+
.filter((a) => a.extracted_content)
|
|
380
|
+
.map((a) => `\n[Attachment: ${a.file_name ?? 'unknown'}]\n${a.extracted_content}`);
|
|
381
|
+
if (attachmentTexts.length > 0) {
|
|
382
|
+
content += attachmentTexts.join('\n');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Build metadata for file references
|
|
386
|
+
const metadata = {};
|
|
387
|
+
if (msg.attachments && msg.attachments.length > 0) {
|
|
388
|
+
metadata.attachments = msg.attachments.map((a) => ({
|
|
389
|
+
fileName: a.file_name,
|
|
390
|
+
fileType: a.file_type,
|
|
391
|
+
fileSize: a.file_size,
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
if (msg.files && msg.files.length > 0) {
|
|
395
|
+
metadata.files = msg.files.map((f) => ({
|
|
396
|
+
fileName: f.file_name,
|
|
397
|
+
fileId: f.file_id,
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
id,
|
|
402
|
+
role,
|
|
403
|
+
content,
|
|
404
|
+
timestamp,
|
|
405
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Map Claude's sender/role names to SaveState standard roles.
|
|
410
|
+
* Claude uses "human"/"assistant", SaveState uses "user"/"assistant".
|
|
411
|
+
*/
|
|
412
|
+
mapRole(role) {
|
|
413
|
+
const normalized = role.toLowerCase().trim();
|
|
414
|
+
switch (normalized) {
|
|
415
|
+
case 'human':
|
|
416
|
+
case 'user':
|
|
417
|
+
return 'user';
|
|
418
|
+
case 'assistant':
|
|
419
|
+
case 'claude':
|
|
420
|
+
case 'ai':
|
|
421
|
+
return 'assistant';
|
|
422
|
+
case 'system':
|
|
423
|
+
return 'system';
|
|
424
|
+
case 'tool':
|
|
425
|
+
case 'tool_result':
|
|
426
|
+
return 'tool';
|
|
427
|
+
default:
|
|
428
|
+
return 'user';
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// ─── Private: Memories ────────────────────────────────────
|
|
432
|
+
async readMemories() {
|
|
433
|
+
const entries = [];
|
|
434
|
+
// 1. Read standalone memory file
|
|
435
|
+
if (this.memoryFile) {
|
|
436
|
+
const parsed = await this.parseMemoryFile(this.memoryFile);
|
|
437
|
+
entries.push(...parsed);
|
|
438
|
+
}
|
|
439
|
+
// 2. Look for memory files in export directory
|
|
440
|
+
if (this.exportDir) {
|
|
441
|
+
for (const candidate of MEMORY_FILE_CANDIDATES) {
|
|
442
|
+
const filePath = join(this.exportDir, candidate);
|
|
443
|
+
// Skip if this is the same as the standalone memory file
|
|
444
|
+
if (this.memoryFile && filePath === this.memoryFile)
|
|
445
|
+
continue;
|
|
446
|
+
if (existsSync(filePath)) {
|
|
447
|
+
const parsed = await this.parseMemoryFile(filePath);
|
|
448
|
+
entries.push(...parsed);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Also check for a memories/ directory
|
|
452
|
+
const memoriesDir = join(this.exportDir, 'memories');
|
|
453
|
+
if (existsSync(memoriesDir)) {
|
|
454
|
+
const s = await stat(memoriesDir).catch(() => null);
|
|
455
|
+
if (s?.isDirectory()) {
|
|
456
|
+
const files = await readdir(memoriesDir).catch(() => []);
|
|
457
|
+
for (const file of files) {
|
|
458
|
+
const filePath = join(memoriesDir, file);
|
|
459
|
+
const ext = extname(file).toLowerCase();
|
|
460
|
+
if (ext === '.json') {
|
|
461
|
+
const jsonEntries = await this.parseMemoryJson(filePath);
|
|
462
|
+
entries.push(...jsonEntries);
|
|
463
|
+
}
|
|
464
|
+
else if (ext === '.md' || ext === '.txt') {
|
|
465
|
+
const textEntries = await this.parseMemoryFile(filePath);
|
|
466
|
+
entries.push(...textEntries);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return entries;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Parse a Claude memory text/markdown file.
|
|
476
|
+
* Memories are typically one per line or separated by blank lines.
|
|
477
|
+
* May have bullet points (- or *) or numbered entries.
|
|
478
|
+
*/
|
|
479
|
+
async parseMemoryFile(filePath) {
|
|
480
|
+
const content = await this.safeReadFile(filePath);
|
|
481
|
+
if (!content)
|
|
482
|
+
return [];
|
|
483
|
+
const entries = [];
|
|
484
|
+
const fileStat = await stat(filePath).catch(() => null);
|
|
485
|
+
const fileDate = fileStat?.mtime?.toISOString() ?? new Date().toISOString();
|
|
486
|
+
const source = `claude-memory:${basename(filePath)}`;
|
|
487
|
+
// Split into individual memory entries
|
|
488
|
+
// Handle various formats: bullet lists, numbered lists, blank-line separated
|
|
489
|
+
const lines = content.split('\n');
|
|
490
|
+
let currentEntry = '';
|
|
491
|
+
let entryIndex = 0;
|
|
492
|
+
for (const line of lines) {
|
|
493
|
+
const trimmed = line.trim();
|
|
494
|
+
// Skip empty lines (they separate entries) or headers
|
|
495
|
+
if (!trimmed) {
|
|
496
|
+
if (currentEntry.trim()) {
|
|
497
|
+
entries.push(this.createMemoryEntry(currentEntry.trim(), source, fileDate, entryIndex));
|
|
498
|
+
entryIndex++;
|
|
499
|
+
currentEntry = '';
|
|
500
|
+
}
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
// Skip markdown headers (used as section dividers, not memories themselves)
|
|
504
|
+
if (trimmed.startsWith('# ') || trimmed.startsWith('## ') || trimmed.startsWith('### ')) {
|
|
505
|
+
if (currentEntry.trim()) {
|
|
506
|
+
entries.push(this.createMemoryEntry(currentEntry.trim(), source, fileDate, entryIndex));
|
|
507
|
+
entryIndex++;
|
|
508
|
+
currentEntry = '';
|
|
509
|
+
}
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
// Check if this line starts a new entry (bullet or numbered list)
|
|
513
|
+
const isBullet = /^[-*•]\s/.test(trimmed);
|
|
514
|
+
const isNumbered = /^\d+[.)]\s/.test(trimmed);
|
|
515
|
+
if ((isBullet || isNumbered) && currentEntry.trim()) {
|
|
516
|
+
// Save previous entry and start new one
|
|
517
|
+
entries.push(this.createMemoryEntry(currentEntry.trim(), source, fileDate, entryIndex));
|
|
518
|
+
entryIndex++;
|
|
519
|
+
// Strip the bullet/number prefix
|
|
520
|
+
currentEntry = trimmed.replace(/^[-*•]\s+/, '').replace(/^\d+[.)]\s+/, '');
|
|
521
|
+
}
|
|
522
|
+
else if (isBullet || isNumbered) {
|
|
523
|
+
currentEntry = trimmed.replace(/^[-*•]\s+/, '').replace(/^\d+[.)]\s+/, '');
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
// Continuation of current entry
|
|
527
|
+
currentEntry += (currentEntry ? ' ' : '') + trimmed;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Don't forget the last entry
|
|
531
|
+
if (currentEntry.trim()) {
|
|
532
|
+
entries.push(this.createMemoryEntry(currentEntry.trim(), source, fileDate, entryIndex));
|
|
533
|
+
}
|
|
534
|
+
return entries;
|
|
535
|
+
}
|
|
536
|
+
createMemoryEntry(content, source, date, index) {
|
|
537
|
+
return {
|
|
538
|
+
id: `claude-memory-${index}`,
|
|
539
|
+
content,
|
|
540
|
+
source,
|
|
541
|
+
createdAt: date,
|
|
542
|
+
metadata: { platform: 'claude.ai' },
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Parse a JSON memory file (if Claude exports memories in JSON format).
|
|
547
|
+
*/
|
|
548
|
+
async parseMemoryJson(filePath) {
|
|
549
|
+
const content = await this.safeReadFile(filePath);
|
|
550
|
+
if (!content)
|
|
551
|
+
return [];
|
|
552
|
+
try {
|
|
553
|
+
const data = JSON.parse(content);
|
|
554
|
+
const entries = [];
|
|
555
|
+
// Handle array of memory objects
|
|
556
|
+
const items = Array.isArray(data) ? data : (data.memories ?? data.items ?? []);
|
|
557
|
+
for (let i = 0; i < items.length; i++) {
|
|
558
|
+
const item = items[i];
|
|
559
|
+
if (typeof item === 'string') {
|
|
560
|
+
entries.push(this.createMemoryEntry(item, `claude-memory:${basename(filePath)}`, new Date().toISOString(), i));
|
|
561
|
+
}
|
|
562
|
+
else if (item && typeof item === 'object') {
|
|
563
|
+
const text = item.content ?? item.text ?? item.memory ?? item.value ?? '';
|
|
564
|
+
if (text) {
|
|
565
|
+
entries.push({
|
|
566
|
+
id: item.id ?? item.uuid ?? `claude-memory-${i}`,
|
|
567
|
+
content: String(text),
|
|
568
|
+
source: `claude-memory:${basename(filePath)}`,
|
|
569
|
+
createdAt: item.created_at ?? item.createdAt ?? new Date().toISOString(),
|
|
570
|
+
updatedAt: item.updated_at ?? item.updatedAt ?? undefined,
|
|
571
|
+
metadata: { platform: 'claude.ai' },
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return entries;
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
this.warnings.push(`Failed to parse memory JSON: ${basename(filePath)}`);
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// ─── Private: Projects ────────────────────────────────────
|
|
584
|
+
async readProjects() {
|
|
585
|
+
if (!this.exportDir)
|
|
586
|
+
return [];
|
|
587
|
+
const projects = [];
|
|
588
|
+
// Look for projects/ directory in export
|
|
589
|
+
const projectsDirCandidates = ['projects', 'project'];
|
|
590
|
+
for (const dirName of projectsDirCandidates) {
|
|
591
|
+
const projectsDir = join(this.exportDir, dirName);
|
|
592
|
+
if (!existsSync(projectsDir))
|
|
593
|
+
continue;
|
|
594
|
+
const s = await stat(projectsDir).catch(() => null);
|
|
595
|
+
if (!s?.isDirectory())
|
|
596
|
+
continue;
|
|
597
|
+
const items = await readdir(projectsDir).catch(() => []);
|
|
598
|
+
for (const item of items) {
|
|
599
|
+
const itemPath = join(projectsDir, item);
|
|
600
|
+
const itemStat = await stat(itemPath).catch(() => null);
|
|
601
|
+
if (itemStat?.isDirectory()) {
|
|
602
|
+
// Each subdirectory is a project
|
|
603
|
+
const project = await this.parseProjectDir(itemPath, item);
|
|
604
|
+
if (project)
|
|
605
|
+
projects.push(project);
|
|
606
|
+
}
|
|
607
|
+
else if (item.endsWith('.json') && itemStat?.isFile()) {
|
|
608
|
+
// JSON file might be a project export
|
|
609
|
+
const project = await this.parseProjectJson(itemPath);
|
|
610
|
+
if (project)
|
|
611
|
+
projects.push(project);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return projects;
|
|
616
|
+
}
|
|
617
|
+
async parseProjectDir(dirPath, name) {
|
|
618
|
+
const project = {
|
|
619
|
+
name,
|
|
620
|
+
files: [],
|
|
621
|
+
};
|
|
622
|
+
// Look for instructions file
|
|
623
|
+
const instructionCandidates = [
|
|
624
|
+
'instructions.md',
|
|
625
|
+
'instructions.txt',
|
|
626
|
+
'system_prompt.md',
|
|
627
|
+
'system_prompt.txt',
|
|
628
|
+
'prompt.md',
|
|
629
|
+
];
|
|
630
|
+
for (const candidate of instructionCandidates) {
|
|
631
|
+
const filePath = join(dirPath, candidate);
|
|
632
|
+
if (existsSync(filePath)) {
|
|
633
|
+
const content = await this.safeReadFile(filePath);
|
|
634
|
+
if (content) {
|
|
635
|
+
project.instructions = content;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// Look for project metadata
|
|
641
|
+
const metaPath = join(dirPath, 'project.json');
|
|
642
|
+
if (existsSync(metaPath)) {
|
|
643
|
+
const content = await this.safeReadFile(metaPath);
|
|
644
|
+
if (content) {
|
|
645
|
+
try {
|
|
646
|
+
const meta = JSON.parse(content);
|
|
647
|
+
project.uuid = meta.uuid;
|
|
648
|
+
project.description = meta.description;
|
|
649
|
+
project.created_at = meta.created_at;
|
|
650
|
+
project.updated_at = meta.updated_at;
|
|
651
|
+
if (meta.instructions && !project.instructions) {
|
|
652
|
+
project.instructions = meta.instructions;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
this.warnings.push(`Failed to parse project metadata: ${name}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// Read uploaded files
|
|
661
|
+
const filesDirCandidates = ['files', 'documents', 'uploads'];
|
|
662
|
+
for (const filesDirName of filesDirCandidates) {
|
|
663
|
+
const filesDir = join(dirPath, filesDirName);
|
|
664
|
+
if (!existsSync(filesDir))
|
|
665
|
+
continue;
|
|
666
|
+
const files = await readdir(filesDir).catch(() => []);
|
|
667
|
+
for (const file of files) {
|
|
668
|
+
const filePath = join(filesDir, file);
|
|
669
|
+
const fileStat = await stat(filePath).catch(() => null);
|
|
670
|
+
if (!fileStat?.isFile())
|
|
671
|
+
continue;
|
|
672
|
+
const content = await this.safeReadFile(filePath);
|
|
673
|
+
if (content !== null) {
|
|
674
|
+
project.files?.push({
|
|
675
|
+
file_name: file,
|
|
676
|
+
content,
|
|
677
|
+
file_type: this.guessFileType(file),
|
|
678
|
+
file_size: fileStat.size,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// Also read any loose text files in the project dir itself
|
|
684
|
+
const items = await readdir(dirPath).catch(() => []);
|
|
685
|
+
for (const item of items) {
|
|
686
|
+
if (instructionCandidates.includes(item))
|
|
687
|
+
continue;
|
|
688
|
+
if (item === 'project.json')
|
|
689
|
+
continue;
|
|
690
|
+
if (filesDirCandidates.includes(item))
|
|
691
|
+
continue;
|
|
692
|
+
const itemPath = join(dirPath, item);
|
|
693
|
+
const itemStat = await stat(itemPath).catch(() => null);
|
|
694
|
+
if (!itemStat?.isFile())
|
|
695
|
+
continue;
|
|
696
|
+
const ext = extname(item).toLowerCase();
|
|
697
|
+
if (['.md', '.txt', '.csv', '.json', '.yaml', '.yml', '.xml'].includes(ext)) {
|
|
698
|
+
const content = await this.safeReadFile(itemPath);
|
|
699
|
+
if (content !== null) {
|
|
700
|
+
project.files?.push({
|
|
701
|
+
file_name: item,
|
|
702
|
+
content,
|
|
703
|
+
file_type: this.guessFileType(item),
|
|
704
|
+
file_size: itemStat.size,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Only return project if it has any content
|
|
710
|
+
if (project.instructions || (project.files && project.files.length > 0)) {
|
|
711
|
+
return project;
|
|
712
|
+
}
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
async parseProjectJson(filePath) {
|
|
716
|
+
const content = await this.safeReadFile(filePath);
|
|
717
|
+
if (!content)
|
|
718
|
+
return null;
|
|
719
|
+
try {
|
|
720
|
+
const raw = JSON.parse(content);
|
|
721
|
+
if (raw.instructions || (raw.files && raw.files.length > 0) || raw.name) {
|
|
722
|
+
return {
|
|
723
|
+
uuid: raw.uuid,
|
|
724
|
+
name: raw.name ?? basename(filePath, '.json'),
|
|
725
|
+
description: raw.description,
|
|
726
|
+
instructions: raw.instructions,
|
|
727
|
+
created_at: raw.created_at,
|
|
728
|
+
updated_at: raw.updated_at,
|
|
729
|
+
files: raw.files ?? [],
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
this.warnings.push(`Failed to parse project file: ${basename(filePath)}`);
|
|
735
|
+
}
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
// ─── Private: Build Identity & Knowledge ──────────────────
|
|
739
|
+
/**
|
|
740
|
+
* Build personality string from project instructions.
|
|
741
|
+
* Concatenates all project instructions with separators.
|
|
742
|
+
*/
|
|
743
|
+
buildPersonality(projects) {
|
|
744
|
+
if (projects.length === 0)
|
|
745
|
+
return '';
|
|
746
|
+
const parts = [];
|
|
747
|
+
for (const project of projects) {
|
|
748
|
+
if (!project.instructions)
|
|
749
|
+
continue;
|
|
750
|
+
const name = project.name ?? 'Unnamed Project';
|
|
751
|
+
parts.push(`--- Claude Project: ${name} ---\n${project.instructions}`);
|
|
752
|
+
}
|
|
753
|
+
return parts.join('\n\n');
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Build knowledge documents from project uploaded files.
|
|
757
|
+
*/
|
|
758
|
+
buildKnowledge(projects) {
|
|
759
|
+
const docs = [];
|
|
760
|
+
for (const project of projects) {
|
|
761
|
+
const projectName = project.name ?? 'unnamed';
|
|
762
|
+
if (!project.files)
|
|
763
|
+
continue;
|
|
764
|
+
for (const file of project.files) {
|
|
765
|
+
if (!file.content || !file.file_name)
|
|
766
|
+
continue;
|
|
767
|
+
const buf = Buffer.from(file.content, 'utf-8');
|
|
768
|
+
docs.push({
|
|
769
|
+
id: `project:${projectName}:${file.file_name}`,
|
|
770
|
+
filename: file.file_name,
|
|
771
|
+
mimeType: file.file_type ?? this.guessFileType(file.file_name),
|
|
772
|
+
path: `knowledge/projects/${projectName}/${file.file_name}`,
|
|
773
|
+
size: buf.length,
|
|
774
|
+
checksum: computeChecksum(buf),
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return docs;
|
|
779
|
+
}
|
|
780
|
+
// ─── Private: Restore Guide Generation ────────────────────
|
|
781
|
+
generateRestoreGuide(snapshot) {
|
|
782
|
+
const lines = [];
|
|
783
|
+
lines.push('# Claude.ai Restore Guide');
|
|
784
|
+
lines.push('');
|
|
785
|
+
lines.push(`Generated: ${new Date().toISOString()}`);
|
|
786
|
+
lines.push(`Snapshot: ${snapshot.manifest.id}`);
|
|
787
|
+
lines.push(`Original export: ${snapshot.manifest.timestamp}`);
|
|
788
|
+
lines.push('');
|
|
789
|
+
lines.push('---');
|
|
790
|
+
lines.push('');
|
|
791
|
+
// Memories section
|
|
792
|
+
if (snapshot.memory.core.length > 0) {
|
|
793
|
+
lines.push('## 🧠 Memories');
|
|
794
|
+
lines.push('');
|
|
795
|
+
lines.push('Go to **Claude.ai → Settings → Memory** and add each memory:');
|
|
796
|
+
lines.push('');
|
|
797
|
+
for (const entry of snapshot.memory.core) {
|
|
798
|
+
lines.push(`- ${entry.content}`);
|
|
799
|
+
}
|
|
800
|
+
lines.push('');
|
|
801
|
+
lines.push('> **Tip:** You can tell Claude "Remember that..." for each item,');
|
|
802
|
+
lines.push('> or use the Memory settings to add them directly.');
|
|
803
|
+
lines.push('');
|
|
804
|
+
}
|
|
805
|
+
// Projects section
|
|
806
|
+
if (snapshot.identity.personality) {
|
|
807
|
+
lines.push('## 📁 Projects');
|
|
808
|
+
lines.push('');
|
|
809
|
+
lines.push('Create new Projects in Claude.ai with the following instructions:');
|
|
810
|
+
lines.push('');
|
|
811
|
+
lines.push('```markdown');
|
|
812
|
+
lines.push(snapshot.identity.personality);
|
|
813
|
+
lines.push('```');
|
|
814
|
+
lines.push('');
|
|
815
|
+
}
|
|
816
|
+
// Knowledge / uploaded files
|
|
817
|
+
if (snapshot.memory.knowledge.length > 0) {
|
|
818
|
+
lines.push('## 📄 Project Files');
|
|
819
|
+
lines.push('');
|
|
820
|
+
lines.push('Upload these files to the appropriate Claude Project:');
|
|
821
|
+
lines.push('');
|
|
822
|
+
for (const doc of snapshot.memory.knowledge) {
|
|
823
|
+
lines.push(`- **${doc.filename}** (${(doc.size / 1024).toFixed(1)} KB)`);
|
|
824
|
+
}
|
|
825
|
+
lines.push('');
|
|
826
|
+
}
|
|
827
|
+
// Conversations summary
|
|
828
|
+
if (snapshot.conversations.total > 0) {
|
|
829
|
+
lines.push('## 💬 Conversations');
|
|
830
|
+
lines.push('');
|
|
831
|
+
lines.push(`${snapshot.conversations.total} conversations were backed up. Conversations cannot be`);
|
|
832
|
+
lines.push('restored to Claude.ai but are preserved in the snapshot archive.');
|
|
833
|
+
lines.push('');
|
|
834
|
+
lines.push('Recent conversations:');
|
|
835
|
+
lines.push('');
|
|
836
|
+
const recent = [...snapshot.conversations.conversations]
|
|
837
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
838
|
+
.slice(0, 20);
|
|
839
|
+
for (const conv of recent) {
|
|
840
|
+
const title = conv.title ?? 'Untitled';
|
|
841
|
+
lines.push(`- **${title}** (${conv.messageCount} messages, ${conv.updatedAt.slice(0, 10)})`);
|
|
842
|
+
}
|
|
843
|
+
if (snapshot.conversations.total > 20) {
|
|
844
|
+
lines.push(`- ... and ${snapshot.conversations.total - 20} more`);
|
|
845
|
+
}
|
|
846
|
+
lines.push('');
|
|
847
|
+
}
|
|
848
|
+
lines.push('---');
|
|
849
|
+
lines.push('');
|
|
850
|
+
lines.push('*Generated by SaveState — https://github.com/nicholasgriffintn/savestate*');
|
|
851
|
+
return lines.join('\n');
|
|
852
|
+
}
|
|
853
|
+
formatMemoriesForRestore(memories) {
|
|
854
|
+
const lines = [];
|
|
855
|
+
lines.push('# Claude Memories to Import');
|
|
856
|
+
lines.push('');
|
|
857
|
+
lines.push('Copy each memory below into Claude.ai Settings → Memory,');
|
|
858
|
+
lines.push('or tell Claude "Remember that..." for each item.');
|
|
859
|
+
lines.push('');
|
|
860
|
+
for (let i = 0; i < memories.length; i++) {
|
|
861
|
+
lines.push(`${i + 1}. ${memories[i].content}`);
|
|
862
|
+
}
|
|
863
|
+
lines.push('');
|
|
864
|
+
return lines.join('\n');
|
|
865
|
+
}
|
|
866
|
+
// ─── Private: Utilities ───────────────────────────────────
|
|
867
|
+
async safeReadFile(filePath) {
|
|
868
|
+
try {
|
|
869
|
+
const s = await stat(filePath);
|
|
870
|
+
if (s.size > MAX_FILE_SIZE) {
|
|
871
|
+
this.warnings.push(`Skipped ${basename(filePath)} (${(s.size / 1024 / 1024).toFixed(1)}MB > ${MAX_FILE_SIZE / 1024 / 1024}MB limit)`);
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
return await readFile(filePath, 'utf-8');
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
guessFileType(filename) {
|
|
881
|
+
const ext = extname(filename).toLowerCase();
|
|
882
|
+
const mimeMap = {
|
|
883
|
+
'.md': 'text/markdown',
|
|
884
|
+
'.txt': 'text/plain',
|
|
885
|
+
'.csv': 'text/csv',
|
|
886
|
+
'.json': 'application/json',
|
|
887
|
+
'.yaml': 'text/yaml',
|
|
888
|
+
'.yml': 'text/yaml',
|
|
889
|
+
'.xml': 'application/xml',
|
|
890
|
+
'.html': 'text/html',
|
|
891
|
+
'.pdf': 'application/pdf',
|
|
892
|
+
'.py': 'text/x-python',
|
|
893
|
+
'.js': 'text/javascript',
|
|
894
|
+
'.ts': 'text/typescript',
|
|
895
|
+
'.rs': 'text/x-rust',
|
|
896
|
+
'.go': 'text/x-go',
|
|
897
|
+
'.rb': 'text/x-ruby',
|
|
898
|
+
'.sh': 'text/x-shellscript',
|
|
899
|
+
};
|
|
900
|
+
return mimeMap[ext] ?? 'text/plain';
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
//# sourceMappingURL=claude-web.js.map
|