@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,1136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts user data from Claude Projects including:
|
|
5
|
+
* - System prompts (per-project instructions)
|
|
6
|
+
* - Project knowledge documents
|
|
7
|
+
* - Project files (attachments)
|
|
8
|
+
* - Artifacts (if applicable)
|
|
9
|
+
*
|
|
10
|
+
* Supports both API-based extraction and export file parsing.
|
|
11
|
+
* For API access: Uses Claude Projects API
|
|
12
|
+
* For export: Parses Claude data export files
|
|
13
|
+
*/
|
|
14
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
15
|
+
import { mkdir, writeFile, readFile, stat, readdir, access, constants } from 'node:fs/promises';
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
import { join, extname, basename } from 'node:path';
|
|
18
|
+
// ─── Rate Limiter ────────────────────────────────────────────
|
|
19
|
+
class RateLimiter {
|
|
20
|
+
requests = [];
|
|
21
|
+
config;
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
this.config = {
|
|
24
|
+
requestsPerMinute: config.requestsPerMinute ?? 50,
|
|
25
|
+
initialBackoffMs: config.initialBackoffMs ?? 1000,
|
|
26
|
+
maxBackoffMs: config.maxBackoffMs ?? 60000,
|
|
27
|
+
maxRetries: config.maxRetries ?? 5,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async acquire() {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const windowStart = now - 60000;
|
|
33
|
+
// Remove old requests outside the window
|
|
34
|
+
this.requests = this.requests.filter((t) => t > windowStart);
|
|
35
|
+
// If at limit, wait
|
|
36
|
+
if (this.requests.length >= this.config.requestsPerMinute) {
|
|
37
|
+
const oldestInWindow = this.requests[0];
|
|
38
|
+
const waitTime = oldestInWindow - windowStart + 100;
|
|
39
|
+
await this.sleep(waitTime);
|
|
40
|
+
}
|
|
41
|
+
this.requests.push(Date.now());
|
|
42
|
+
}
|
|
43
|
+
async withRetry(fn) {
|
|
44
|
+
let lastError = null;
|
|
45
|
+
let backoff = this.config.initialBackoffMs;
|
|
46
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
47
|
+
try {
|
|
48
|
+
await this.acquire();
|
|
49
|
+
return await fn();
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
53
|
+
// Check if it's a rate limit error
|
|
54
|
+
if (this.isRateLimitError(lastError)) {
|
|
55
|
+
if (attempt < this.config.maxRetries) {
|
|
56
|
+
await this.sleep(backoff);
|
|
57
|
+
backoff = Math.min(backoff * 2, this.config.maxBackoffMs);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw lastError;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw lastError ?? new Error('Max retries exceeded');
|
|
65
|
+
}
|
|
66
|
+
isRateLimitError(error) {
|
|
67
|
+
return (error.message.includes('429') ||
|
|
68
|
+
error.message.includes('rate limit') ||
|
|
69
|
+
error.message.includes('Rate limit') ||
|
|
70
|
+
error.message.includes('Too Many Requests'));
|
|
71
|
+
}
|
|
72
|
+
sleep(ms) {
|
|
73
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ─── Claude API Client ───────────────────────────────────────
|
|
77
|
+
class ClaudeApiClient {
|
|
78
|
+
apiKey;
|
|
79
|
+
baseUrl;
|
|
80
|
+
organizationId;
|
|
81
|
+
rateLimiter;
|
|
82
|
+
maxRetries;
|
|
83
|
+
retryDelayMs;
|
|
84
|
+
constructor(config) {
|
|
85
|
+
this.apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY || '';
|
|
86
|
+
this.baseUrl = config.baseUrl || 'https://api.anthropic.com';
|
|
87
|
+
this.organizationId = config.organizationId;
|
|
88
|
+
this.rateLimiter = new RateLimiter(config.rateLimit);
|
|
89
|
+
this.maxRetries = config.rateLimit?.maxRetries ?? 3;
|
|
90
|
+
this.retryDelayMs = config.rateLimit?.initialBackoffMs ?? 1000;
|
|
91
|
+
}
|
|
92
|
+
hasApiKey() {
|
|
93
|
+
return !!this.apiKey;
|
|
94
|
+
}
|
|
95
|
+
async request(method, path, body, retryCount = 0) {
|
|
96
|
+
await this.rateLimiter.acquire();
|
|
97
|
+
const headers = {
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
'anthropic-version': '2024-10-01',
|
|
100
|
+
'x-api-key': this.apiKey,
|
|
101
|
+
};
|
|
102
|
+
if (this.organizationId) {
|
|
103
|
+
headers['anthropic-organization'] = this.organizationId;
|
|
104
|
+
}
|
|
105
|
+
const url = `${this.baseUrl}${path}`;
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetch(url, {
|
|
108
|
+
method,
|
|
109
|
+
headers,
|
|
110
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
111
|
+
});
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const error = new Error();
|
|
114
|
+
error.status = response.status;
|
|
115
|
+
// Handle rate limiting
|
|
116
|
+
if (response.status === 429) {
|
|
117
|
+
const retryAfter = response.headers.get('retry-after');
|
|
118
|
+
error.retryAfter = retryAfter ? parseInt(retryAfter, 10) * 1000 : this.retryDelayMs;
|
|
119
|
+
error.code = 'rate_limit_exceeded';
|
|
120
|
+
error.message = 'Rate limit exceeded';
|
|
121
|
+
if (retryCount < this.maxRetries) {
|
|
122
|
+
await this.sleep(error.retryAfter);
|
|
123
|
+
return this.request(method, path, body, retryCount + 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Handle transient errors
|
|
127
|
+
if (response.status >= 500 && retryCount < this.maxRetries) {
|
|
128
|
+
await this.sleep(this.retryDelayMs * Math.pow(2, retryCount));
|
|
129
|
+
return this.request(method, path, body, retryCount + 1);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const errorBody = (await response.json());
|
|
133
|
+
error.message = errorBody.error?.message || `API error: ${response.status}`;
|
|
134
|
+
error.code = errorBody.error?.type;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
error.message = `API error: ${response.status} ${response.statusText}`;
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
return response.json();
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
// Retry on network errors
|
|
145
|
+
if (err instanceof TypeError &&
|
|
146
|
+
err.message.includes('fetch') &&
|
|
147
|
+
retryCount < this.maxRetries) {
|
|
148
|
+
await this.sleep(this.retryDelayMs * Math.pow(2, retryCount));
|
|
149
|
+
return this.request(method, path, body, retryCount + 1);
|
|
150
|
+
}
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async requestBinary(path, retryCount = 0) {
|
|
155
|
+
await this.rateLimiter.acquire();
|
|
156
|
+
const headers = {
|
|
157
|
+
'anthropic-version': '2024-10-01',
|
|
158
|
+
'x-api-key': this.apiKey,
|
|
159
|
+
};
|
|
160
|
+
if (this.organizationId) {
|
|
161
|
+
headers['anthropic-organization'] = this.organizationId;
|
|
162
|
+
}
|
|
163
|
+
const url = `${this.baseUrl}${path}`;
|
|
164
|
+
try {
|
|
165
|
+
const response = await fetch(url, { method: 'GET', headers });
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
if (response.status >= 500 && retryCount < this.maxRetries) {
|
|
168
|
+
await this.sleep(this.retryDelayMs * Math.pow(2, retryCount));
|
|
169
|
+
return this.requestBinary(path, retryCount + 1);
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`Failed to download: ${response.status}`);
|
|
172
|
+
}
|
|
173
|
+
return Buffer.from(await response.arrayBuffer());
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
if (err instanceof TypeError &&
|
|
177
|
+
err.message.includes('fetch') &&
|
|
178
|
+
retryCount < this.maxRetries) {
|
|
179
|
+
await this.sleep(this.retryDelayMs * Math.pow(2, retryCount));
|
|
180
|
+
return this.requestBinary(path, retryCount + 1);
|
|
181
|
+
}
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
sleep(ms) {
|
|
186
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
187
|
+
}
|
|
188
|
+
// ─── Projects API ────────────────────────────────────────
|
|
189
|
+
async listProjects() {
|
|
190
|
+
return this.request('GET', '/v1/projects');
|
|
191
|
+
}
|
|
192
|
+
async getProject(projectId) {
|
|
193
|
+
return this.request('GET', `/v1/projects/${projectId}`);
|
|
194
|
+
}
|
|
195
|
+
async listProjectDocuments(projectId) {
|
|
196
|
+
return this.request('GET', `/v1/projects/${projectId}/docs`);
|
|
197
|
+
}
|
|
198
|
+
async getProjectDocument(projectId, docId) {
|
|
199
|
+
return this.request('GET', `/v1/projects/${projectId}/docs/${docId}`);
|
|
200
|
+
}
|
|
201
|
+
async listProjectFiles(projectId) {
|
|
202
|
+
return this.request('GET', `/v1/projects/${projectId}/files`);
|
|
203
|
+
}
|
|
204
|
+
async downloadProjectFile(projectId, fileId) {
|
|
205
|
+
return this.requestBinary(`/v1/projects/${projectId}/files/${fileId}/content`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// ─── Claude Extractor ────────────────────────────────────────
|
|
209
|
+
export class ClaudeExtractor {
|
|
210
|
+
platform = 'claude';
|
|
211
|
+
version = '1.0.0';
|
|
212
|
+
config;
|
|
213
|
+
client;
|
|
214
|
+
progress = 0;
|
|
215
|
+
constructor(config = {}) {
|
|
216
|
+
this.config = config;
|
|
217
|
+
this.client = new ClaudeApiClient(config);
|
|
218
|
+
}
|
|
219
|
+
async canExtract() {
|
|
220
|
+
// Check if we have either API credentials or an export file
|
|
221
|
+
if (this.config.apiKey || process.env.ANTHROPIC_API_KEY) {
|
|
222
|
+
return this.verifyApiAccess();
|
|
223
|
+
}
|
|
224
|
+
if (this.config.exportPath) {
|
|
225
|
+
return this.verifyExportPath();
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
async extract(options) {
|
|
230
|
+
this.progress = 0;
|
|
231
|
+
const workDir = options.workDir;
|
|
232
|
+
await mkdir(workDir, { recursive: true });
|
|
233
|
+
const contents = {};
|
|
234
|
+
const warnings = [];
|
|
235
|
+
const errors = [];
|
|
236
|
+
const shouldInclude = (type) => !options.include || options.include.includes(type);
|
|
237
|
+
// Determine extraction method
|
|
238
|
+
const useApi = this.client.hasApiKey();
|
|
239
|
+
const useExport = !!this.config.exportPath;
|
|
240
|
+
try {
|
|
241
|
+
if (useApi) {
|
|
242
|
+
// API-based extraction
|
|
243
|
+
const result = await this.extractViaApi(workDir, options, shouldInclude);
|
|
244
|
+
Object.assign(contents, result.contents);
|
|
245
|
+
warnings.push(...result.warnings);
|
|
246
|
+
errors.push(...result.errors);
|
|
247
|
+
}
|
|
248
|
+
else if (useExport) {
|
|
249
|
+
// Export-based extraction
|
|
250
|
+
const result = await this.extractViaExport(workDir, options, shouldInclude);
|
|
251
|
+
Object.assign(contents, result.contents);
|
|
252
|
+
warnings.push(...result.warnings);
|
|
253
|
+
errors.push(...result.errors);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
throw new Error('No API key or export path configured');
|
|
257
|
+
}
|
|
258
|
+
this.progress = 100;
|
|
259
|
+
options.onProgress?.(1.0, 'Extraction complete');
|
|
260
|
+
// Build metadata
|
|
261
|
+
const metadata = {
|
|
262
|
+
totalItems: this.countItems(contents),
|
|
263
|
+
itemCounts: {
|
|
264
|
+
instructions: contents.instructions ? 1 : 0,
|
|
265
|
+
memories: contents.memories?.count ?? 0,
|
|
266
|
+
conversations: contents.conversations?.count ?? 0,
|
|
267
|
+
files: contents.files?.count ?? 0,
|
|
268
|
+
customBots: contents.customBots?.count ?? 0,
|
|
269
|
+
},
|
|
270
|
+
warnings,
|
|
271
|
+
errors,
|
|
272
|
+
};
|
|
273
|
+
// Build bundle
|
|
274
|
+
const bundle = {
|
|
275
|
+
version: '1.0',
|
|
276
|
+
id: `bundle_${randomBytes(8).toString('hex')}`,
|
|
277
|
+
source: {
|
|
278
|
+
platform: 'claude',
|
|
279
|
+
extractedAt: new Date().toISOString(),
|
|
280
|
+
extractorVersion: this.version,
|
|
281
|
+
},
|
|
282
|
+
contents,
|
|
283
|
+
metadata,
|
|
284
|
+
};
|
|
285
|
+
return bundle;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
this.progress = 0;
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
getProgress() {
|
|
293
|
+
return this.progress;
|
|
294
|
+
}
|
|
295
|
+
// ─── API-based Extraction ──────────────────────────────────
|
|
296
|
+
async verifyApiAccess() {
|
|
297
|
+
try {
|
|
298
|
+
await this.client.listProjects();
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async extractViaApi(workDir, options, shouldInclude) {
|
|
306
|
+
const contents = {};
|
|
307
|
+
const warnings = [];
|
|
308
|
+
const errors = [];
|
|
309
|
+
// Fetch all projects
|
|
310
|
+
options.onProgress?.(0.05, 'Fetching Claude projects...');
|
|
311
|
+
const projectsResponse = await this.client.listProjects();
|
|
312
|
+
let projects = projectsResponse.data || [];
|
|
313
|
+
// Filter by project IDs if specified
|
|
314
|
+
if (this.config.projectIds && this.config.projectIds.length > 0) {
|
|
315
|
+
projects = projects.filter((p) => this.config.projectIds.includes(p.id));
|
|
316
|
+
}
|
|
317
|
+
// Apply max projects limit
|
|
318
|
+
if (this.config.maxProjects && projects.length > this.config.maxProjects) {
|
|
319
|
+
projects = projects.slice(0, this.config.maxProjects);
|
|
320
|
+
warnings.push(`Limited to ${this.config.maxProjects} projects (${projectsResponse.data.length} available)`);
|
|
321
|
+
}
|
|
322
|
+
// Filter out archived projects unless explicitly requested
|
|
323
|
+
projects = projects.filter((p) => !p.archived_at);
|
|
324
|
+
this.progress = 10;
|
|
325
|
+
options.onProgress?.(0.1, `Found ${projects.length} projects`);
|
|
326
|
+
if (projects.length === 0) {
|
|
327
|
+
warnings.push('No projects found to extract');
|
|
328
|
+
return { contents, warnings, errors };
|
|
329
|
+
}
|
|
330
|
+
// Extract projects as "customBots" (Claude's equivalent)
|
|
331
|
+
if (shouldInclude('customBots')) {
|
|
332
|
+
const bots = [];
|
|
333
|
+
const allKnowledgeEntries = [];
|
|
334
|
+
const allFiles = [];
|
|
335
|
+
let totalFileSize = 0;
|
|
336
|
+
for (let i = 0; i < projects.length; i++) {
|
|
337
|
+
const project = projects[i];
|
|
338
|
+
const projectProgress = 0.1 + (0.8 * (i + 1)) / projects.length;
|
|
339
|
+
options.onProgress?.(projectProgress, `Extracting project: ${project.name}`);
|
|
340
|
+
try {
|
|
341
|
+
// Get full project details (includes system prompt)
|
|
342
|
+
const fullProject = await this.client.getProject(project.id);
|
|
343
|
+
// Extract project knowledge documents
|
|
344
|
+
const docsResponse = await this.client.listProjectDocuments(project.id);
|
|
345
|
+
const docs = docsResponse.data || [];
|
|
346
|
+
const knowledgeFiles = [];
|
|
347
|
+
for (const doc of docs) {
|
|
348
|
+
// Get full document content
|
|
349
|
+
const fullDoc = await this.client.getProjectDocument(project.id, doc.id);
|
|
350
|
+
knowledgeFiles.push(fullDoc.name);
|
|
351
|
+
// Store as memory entry (knowledge document)
|
|
352
|
+
allKnowledgeEntries.push({
|
|
353
|
+
id: `${project.id}_${doc.id}`,
|
|
354
|
+
content: fullDoc.content,
|
|
355
|
+
createdAt: fullDoc.created_at,
|
|
356
|
+
updatedAt: fullDoc.updated_at,
|
|
357
|
+
category: `Project: ${project.name}`,
|
|
358
|
+
source: 'claude-project-knowledge',
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
// Extract project files
|
|
362
|
+
if (shouldInclude('files')) {
|
|
363
|
+
const filesDir = join(workDir, 'files', this.sanitizeFilename(project.name));
|
|
364
|
+
await mkdir(filesDir, { recursive: true });
|
|
365
|
+
const filesResponse = await this.client.listProjectFiles(project.id);
|
|
366
|
+
const files = filesResponse.data || [];
|
|
367
|
+
for (const file of files) {
|
|
368
|
+
try {
|
|
369
|
+
const content = await this.client.downloadProjectFile(project.id, file.id);
|
|
370
|
+
const safeFilename = this.sanitizeFilename(file.name);
|
|
371
|
+
const filePath = join(filesDir, safeFilename);
|
|
372
|
+
await writeFile(filePath, content);
|
|
373
|
+
allFiles.push({
|
|
374
|
+
id: `${project.id}_${file.id}`,
|
|
375
|
+
filename: safeFilename,
|
|
376
|
+
mimeType: file.content_type,
|
|
377
|
+
size: file.size,
|
|
378
|
+
path: `files/${this.sanitizeFilename(project.name)}/${safeFilename}`,
|
|
379
|
+
uploadedAt: file.created_at,
|
|
380
|
+
});
|
|
381
|
+
totalFileSize += file.size;
|
|
382
|
+
knowledgeFiles.push(file.name);
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
const msg = `Failed to download file ${file.name}: ${err instanceof Error ? err.message : err}`;
|
|
386
|
+
warnings.push(msg);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Add project as a "bot" entry
|
|
391
|
+
bots.push({
|
|
392
|
+
id: project.id,
|
|
393
|
+
name: project.name,
|
|
394
|
+
description: project.description,
|
|
395
|
+
instructions: fullProject.prompt_template || '',
|
|
396
|
+
knowledgeFiles: knowledgeFiles.length > 0 ? knowledgeFiles : undefined,
|
|
397
|
+
createdAt: project.created_at,
|
|
398
|
+
updatedAt: project.updated_at,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
const msg = `Failed to extract project ${project.name}: ${err instanceof Error ? err.message : err}`;
|
|
403
|
+
errors.push(msg);
|
|
404
|
+
}
|
|
405
|
+
this.progress = projectProgress * 100;
|
|
406
|
+
}
|
|
407
|
+
contents.customBots = {
|
|
408
|
+
bots,
|
|
409
|
+
count: bots.length,
|
|
410
|
+
};
|
|
411
|
+
// Store knowledge entries as memories
|
|
412
|
+
if (allKnowledgeEntries.length > 0) {
|
|
413
|
+
contents.memories = {
|
|
414
|
+
entries: allKnowledgeEntries,
|
|
415
|
+
count: allKnowledgeEntries.length,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// Store files
|
|
419
|
+
if (allFiles.length > 0) {
|
|
420
|
+
contents.files = {
|
|
421
|
+
files: allFiles,
|
|
422
|
+
count: allFiles.length,
|
|
423
|
+
totalSize: totalFileSize,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
// Combine system prompts from all projects for instructions
|
|
427
|
+
if (shouldInclude('instructions')) {
|
|
428
|
+
const combinedInstructions = bots
|
|
429
|
+
.filter((b) => b.instructions)
|
|
430
|
+
.map((b) => `# ${b.name}\n\n${b.instructions}`)
|
|
431
|
+
.join('\n\n---\n\n');
|
|
432
|
+
if (combinedInstructions) {
|
|
433
|
+
contents.instructions = {
|
|
434
|
+
content: combinedInstructions,
|
|
435
|
+
length: combinedInstructions.length,
|
|
436
|
+
sections: this.parseInstructionSections(combinedInstructions),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
this.progress = 90;
|
|
442
|
+
return { contents, warnings, errors };
|
|
443
|
+
}
|
|
444
|
+
// ─── Export-based Extraction ───────────────────────────────
|
|
445
|
+
async verifyExportPath() {
|
|
446
|
+
if (!this.config.exportPath)
|
|
447
|
+
return false;
|
|
448
|
+
try {
|
|
449
|
+
await access(this.config.exportPath, constants.R_OK);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async extractViaExport(workDir, options, shouldInclude) {
|
|
457
|
+
const contents = {};
|
|
458
|
+
const warnings = [];
|
|
459
|
+
const errors = [];
|
|
460
|
+
const exportPath = this.config.exportPath;
|
|
461
|
+
// Claude export typically contains:
|
|
462
|
+
// - conversations.json (or folder with individual conversation files)
|
|
463
|
+
// - potentially projects.json if exported via certain methods
|
|
464
|
+
options.onProgress?.(0.1, 'Scanning Claude export...');
|
|
465
|
+
this.progress = 10;
|
|
466
|
+
// Try to find conversations
|
|
467
|
+
if (shouldInclude('conversations')) {
|
|
468
|
+
try {
|
|
469
|
+
const convResult = await this.extractConversationsFromExport(exportPath, workDir, options);
|
|
470
|
+
if (convResult) {
|
|
471
|
+
contents.conversations = convResult.conversations;
|
|
472
|
+
if (convResult.artifacts.length > 0) {
|
|
473
|
+
// Store artifacts as files
|
|
474
|
+
const artifactsDir = join(workDir, 'artifacts');
|
|
475
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
476
|
+
const artifactFiles = [];
|
|
477
|
+
for (const artifact of convResult.artifacts) {
|
|
478
|
+
const filename = this.sanitizeFilename(artifact.filename);
|
|
479
|
+
const filePath = join(artifactsDir, filename);
|
|
480
|
+
await writeFile(filePath, artifact.content);
|
|
481
|
+
artifactFiles.push({
|
|
482
|
+
id: artifact.id,
|
|
483
|
+
filename,
|
|
484
|
+
mimeType: artifact.mimeType,
|
|
485
|
+
size: artifact.content.length,
|
|
486
|
+
path: `artifacts/${filename}`,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
contents.files = {
|
|
490
|
+
files: artifactFiles,
|
|
491
|
+
count: artifactFiles.length,
|
|
492
|
+
totalSize: artifactFiles.reduce((sum, f) => sum + f.size, 0),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
const msg = `Failed to extract conversations: ${err instanceof Error ? err.message : err}`;
|
|
499
|
+
errors.push(msg);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
this.progress = 60;
|
|
503
|
+
options.onProgress?.(0.6, 'Conversations extracted');
|
|
504
|
+
// Try to find projects data
|
|
505
|
+
if (shouldInclude('customBots')) {
|
|
506
|
+
try {
|
|
507
|
+
const projectsJsonPath = join(exportPath, 'projects.json');
|
|
508
|
+
const projectsDir = join(exportPath, 'projects');
|
|
509
|
+
if (existsSync(projectsJsonPath)) {
|
|
510
|
+
// Single projects.json file
|
|
511
|
+
const projectsData = await this.extractProjectsFromExport(projectsJsonPath, workDir);
|
|
512
|
+
if (projectsData) {
|
|
513
|
+
contents.customBots = projectsData.customBots;
|
|
514
|
+
if (projectsData.instructions) {
|
|
515
|
+
contents.instructions = projectsData.instructions;
|
|
516
|
+
}
|
|
517
|
+
if (projectsData.memories) {
|
|
518
|
+
contents.memories = projectsData.memories;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
else if (existsSync(projectsDir)) {
|
|
523
|
+
// Projects folder with individual files
|
|
524
|
+
const projectsData = await this.extractProjectsFromDirectory(projectsDir, workDir);
|
|
525
|
+
if (projectsData) {
|
|
526
|
+
contents.customBots = projectsData.customBots;
|
|
527
|
+
if (projectsData.instructions) {
|
|
528
|
+
contents.instructions = projectsData.instructions;
|
|
529
|
+
}
|
|
530
|
+
if (projectsData.memories) {
|
|
531
|
+
contents.memories = projectsData.memories;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
const msg = `Failed to extract projects: ${err instanceof Error ? err.message : err}`;
|
|
538
|
+
warnings.push(msg);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
this.progress = 80;
|
|
542
|
+
options.onProgress?.(0.8, 'Projects extracted');
|
|
543
|
+
// Check for standalone memories file
|
|
544
|
+
if (shouldInclude('memories') && !contents.memories) {
|
|
545
|
+
try {
|
|
546
|
+
const memoriesPaths = [
|
|
547
|
+
join(exportPath, 'memories.json'),
|
|
548
|
+
join(exportPath, 'claude-memories.md'),
|
|
549
|
+
join(exportPath, 'memories.md'),
|
|
550
|
+
];
|
|
551
|
+
for (const memoriesPath of memoriesPaths) {
|
|
552
|
+
if (existsSync(memoriesPath)) {
|
|
553
|
+
const memoriesData = await this.extractMemoriesFromFile(memoriesPath);
|
|
554
|
+
if (memoriesData && memoriesData.count > 0) {
|
|
555
|
+
contents.memories = memoriesData;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
const msg = `Failed to extract memories: ${err instanceof Error ? err.message : err}`;
|
|
563
|
+
warnings.push(msg);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
this.progress = 85;
|
|
567
|
+
// Check for standalone files directory
|
|
568
|
+
if (shouldInclude('files')) {
|
|
569
|
+
try {
|
|
570
|
+
const filesDir = join(exportPath, 'files');
|
|
571
|
+
if (existsSync(filesDir)) {
|
|
572
|
+
const filesResult = await this.extractFilesFromDirectory(filesDir, workDir);
|
|
573
|
+
if (filesResult && filesResult.count > 0) {
|
|
574
|
+
// Merge with existing files
|
|
575
|
+
if (contents.files) {
|
|
576
|
+
contents.files.files.push(...filesResult.files);
|
|
577
|
+
contents.files.count += filesResult.count;
|
|
578
|
+
contents.files.totalSize += filesResult.totalSize;
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
contents.files = filesResult;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
const msg = `Failed to extract files: ${err instanceof Error ? err.message : err}`;
|
|
588
|
+
warnings.push(msg);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
this.progress = 90;
|
|
592
|
+
return { contents, warnings, errors };
|
|
593
|
+
}
|
|
594
|
+
async extractConversationsFromExport(exportPath, workDir, options) {
|
|
595
|
+
const conversationsPath = join(exportPath, 'conversations.json');
|
|
596
|
+
const outputDir = join(workDir, 'conversations');
|
|
597
|
+
await mkdir(outputDir, { recursive: true });
|
|
598
|
+
const summaries = [];
|
|
599
|
+
let totalMessages = 0;
|
|
600
|
+
const artifacts = [];
|
|
601
|
+
// Try conversations.json first
|
|
602
|
+
if (existsSync(conversationsPath)) {
|
|
603
|
+
const content = await readFile(conversationsPath, 'utf-8');
|
|
604
|
+
const conversations = JSON.parse(content);
|
|
605
|
+
for (let i = 0; i < conversations.length; i++) {
|
|
606
|
+
const conv = conversations[i];
|
|
607
|
+
const progress = (i + 1) / conversations.length;
|
|
608
|
+
options.onProgress?.(0.1 + progress * 0.5, `Processing conversation ${i + 1}/${conversations.length}`);
|
|
609
|
+
const messages = this.extractMessagesFromConversation(conv);
|
|
610
|
+
totalMessages += messages.length;
|
|
611
|
+
// Extract artifacts from messages
|
|
612
|
+
for (const msg of conv.chat_messages || []) {
|
|
613
|
+
if (msg.content) {
|
|
614
|
+
for (const content of msg.content) {
|
|
615
|
+
if (content.type === 'artifact' && content.artifact) {
|
|
616
|
+
const artifact = content.artifact;
|
|
617
|
+
const ext = this.getArtifactExtension(artifact.type, artifact.language);
|
|
618
|
+
artifacts.push({
|
|
619
|
+
id: artifact.id,
|
|
620
|
+
filename: `${this.sanitizeFilename(artifact.title)}${ext}`,
|
|
621
|
+
content: artifact.content,
|
|
622
|
+
mimeType: this.getArtifactMimeType(artifact.type, artifact.language),
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// Save conversation
|
|
629
|
+
const convData = {
|
|
630
|
+
id: conv.uuid,
|
|
631
|
+
title: conv.name || 'Untitled',
|
|
632
|
+
createdAt: conv.created_at,
|
|
633
|
+
updatedAt: conv.updated_at,
|
|
634
|
+
projectId: conv.project_uuid,
|
|
635
|
+
messages,
|
|
636
|
+
};
|
|
637
|
+
const safeConvId = this.sanitizeFilename(conv.uuid);
|
|
638
|
+
const convPath = join(outputDir, `${safeConvId}.json`);
|
|
639
|
+
await writeFile(convPath, JSON.stringify(convData, null, 2));
|
|
640
|
+
summaries.push({
|
|
641
|
+
id: conv.uuid,
|
|
642
|
+
title: conv.name || 'Untitled',
|
|
643
|
+
messageCount: messages.length,
|
|
644
|
+
createdAt: conv.created_at,
|
|
645
|
+
updatedAt: conv.updated_at,
|
|
646
|
+
keyPoints: this.extractKeyPoints(messages),
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
conversations: {
|
|
651
|
+
path: 'conversations/',
|
|
652
|
+
count: summaries.length,
|
|
653
|
+
messageCount: totalMessages,
|
|
654
|
+
summaries,
|
|
655
|
+
},
|
|
656
|
+
artifacts,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
// Try individual conversation files in a folder
|
|
660
|
+
const conversationsDir = join(exportPath, 'conversations');
|
|
661
|
+
if (existsSync(conversationsDir)) {
|
|
662
|
+
const files = await readdir(conversationsDir);
|
|
663
|
+
const jsonFiles = files.filter((f) => f.endsWith('.json'));
|
|
664
|
+
for (let i = 0; i < jsonFiles.length; i++) {
|
|
665
|
+
const file = jsonFiles[i];
|
|
666
|
+
const progress = (i + 1) / jsonFiles.length;
|
|
667
|
+
options.onProgress?.(0.1 + progress * 0.5, `Processing conversation ${i + 1}/${jsonFiles.length}`);
|
|
668
|
+
const convContent = await readFile(join(conversationsDir, file), 'utf-8');
|
|
669
|
+
const conv = JSON.parse(convContent);
|
|
670
|
+
const messages = this.extractMessagesFromConversation(conv);
|
|
671
|
+
totalMessages += messages.length;
|
|
672
|
+
// Extract artifacts
|
|
673
|
+
for (const msg of conv.chat_messages || []) {
|
|
674
|
+
if (msg.content) {
|
|
675
|
+
for (const content of msg.content) {
|
|
676
|
+
if (content.type === 'artifact' && content.artifact) {
|
|
677
|
+
const artifact = content.artifact;
|
|
678
|
+
const ext = this.getArtifactExtension(artifact.type, artifact.language);
|
|
679
|
+
artifacts.push({
|
|
680
|
+
id: artifact.id,
|
|
681
|
+
filename: `${this.sanitizeFilename(artifact.title)}${ext}`,
|
|
682
|
+
content: artifact.content,
|
|
683
|
+
mimeType: this.getArtifactMimeType(artifact.type, artifact.language),
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// Copy to work dir
|
|
690
|
+
const safeConvId = this.sanitizeFilename(conv.uuid);
|
|
691
|
+
const convPath = join(outputDir, `${safeConvId}.json`);
|
|
692
|
+
await writeFile(convPath, JSON.stringify({
|
|
693
|
+
id: conv.uuid,
|
|
694
|
+
title: conv.name || 'Untitled',
|
|
695
|
+
createdAt: conv.created_at,
|
|
696
|
+
updatedAt: conv.updated_at,
|
|
697
|
+
projectId: conv.project_uuid,
|
|
698
|
+
messages,
|
|
699
|
+
}, null, 2));
|
|
700
|
+
summaries.push({
|
|
701
|
+
id: conv.uuid,
|
|
702
|
+
title: conv.name || 'Untitled',
|
|
703
|
+
messageCount: messages.length,
|
|
704
|
+
createdAt: conv.created_at,
|
|
705
|
+
updatedAt: conv.updated_at,
|
|
706
|
+
keyPoints: this.extractKeyPoints(messages),
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
if (summaries.length > 0) {
|
|
710
|
+
return {
|
|
711
|
+
conversations: {
|
|
712
|
+
path: 'conversations/',
|
|
713
|
+
count: summaries.length,
|
|
714
|
+
messageCount: totalMessages,
|
|
715
|
+
summaries,
|
|
716
|
+
},
|
|
717
|
+
artifacts,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
extractMessagesFromConversation(conv) {
|
|
724
|
+
const messages = [];
|
|
725
|
+
for (const msg of conv.chat_messages || []) {
|
|
726
|
+
let content = msg.text || '';
|
|
727
|
+
// Also include text from content blocks
|
|
728
|
+
if (msg.content) {
|
|
729
|
+
const textParts = msg.content
|
|
730
|
+
.filter((c) => c.type === 'text' && c.text)
|
|
731
|
+
.map((c) => c.text)
|
|
732
|
+
.join('\n');
|
|
733
|
+
if (textParts && !content) {
|
|
734
|
+
content = textParts;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (!content.trim())
|
|
738
|
+
continue;
|
|
739
|
+
const attachments = msg.attachments?.map((att) => ({
|
|
740
|
+
id: att.id,
|
|
741
|
+
name: att.file_name,
|
|
742
|
+
mimeType: att.file_type,
|
|
743
|
+
}));
|
|
744
|
+
messages.push({
|
|
745
|
+
id: msg.uuid,
|
|
746
|
+
role: msg.sender === 'human' ? 'user' : 'assistant',
|
|
747
|
+
content,
|
|
748
|
+
timestamp: msg.created_at,
|
|
749
|
+
attachments,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return messages;
|
|
753
|
+
}
|
|
754
|
+
async extractProjectsFromExport(projectsPath, workDir) {
|
|
755
|
+
const content = await readFile(projectsPath, 'utf-8');
|
|
756
|
+
const projects = JSON.parse(content);
|
|
757
|
+
const bots = [];
|
|
758
|
+
const allKnowledge = [];
|
|
759
|
+
for (const project of projects) {
|
|
760
|
+
bots.push({
|
|
761
|
+
id: project.id,
|
|
762
|
+
name: project.name,
|
|
763
|
+
description: project.description,
|
|
764
|
+
instructions: project.prompt_template || '',
|
|
765
|
+
createdAt: project.created_at,
|
|
766
|
+
updatedAt: project.updated_at,
|
|
767
|
+
});
|
|
768
|
+
// If project has knowledge docs in export, extract them
|
|
769
|
+
// This depends on export format - adapt as needed
|
|
770
|
+
}
|
|
771
|
+
const combinedInstructions = bots
|
|
772
|
+
.filter((b) => b.instructions)
|
|
773
|
+
.map((b) => `# ${b.name}\n\n${b.instructions}`)
|
|
774
|
+
.join('\n\n---\n\n');
|
|
775
|
+
return {
|
|
776
|
+
customBots: { bots, count: bots.length },
|
|
777
|
+
instructions: combinedInstructions
|
|
778
|
+
? {
|
|
779
|
+
content: combinedInstructions,
|
|
780
|
+
length: combinedInstructions.length,
|
|
781
|
+
sections: this.parseInstructionSections(combinedInstructions),
|
|
782
|
+
}
|
|
783
|
+
: undefined,
|
|
784
|
+
memories: allKnowledge.length > 0
|
|
785
|
+
? { entries: allKnowledge, count: allKnowledge.length }
|
|
786
|
+
: undefined,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
async extractProjectsFromDirectory(projectsDir, workDir) {
|
|
790
|
+
const bots = [];
|
|
791
|
+
const allKnowledge = [];
|
|
792
|
+
const entries = await readdir(projectsDir);
|
|
793
|
+
for (const entry of entries) {
|
|
794
|
+
const entryPath = join(projectsDir, entry);
|
|
795
|
+
const stats = await stat(entryPath);
|
|
796
|
+
if (stats.isFile() && entry.endsWith('.json')) {
|
|
797
|
+
// Individual project JSON file
|
|
798
|
+
try {
|
|
799
|
+
const content = await readFile(entryPath, 'utf-8');
|
|
800
|
+
const project = JSON.parse(content);
|
|
801
|
+
const knowledgeFiles = [];
|
|
802
|
+
// Extract inline knowledge docs
|
|
803
|
+
const inlineDocs = project.docs ?? project.knowledge ?? [];
|
|
804
|
+
for (const doc of inlineDocs) {
|
|
805
|
+
if (doc.content) {
|
|
806
|
+
const filename = `${doc.title ?? 'doc'}.md`;
|
|
807
|
+
knowledgeFiles.push(filename);
|
|
808
|
+
// Also add to memory entries
|
|
809
|
+
allKnowledge.push({
|
|
810
|
+
id: `${project.id}_${filename}`,
|
|
811
|
+
content: doc.content,
|
|
812
|
+
createdAt: project.created_at,
|
|
813
|
+
source: 'claude-project',
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
bots.push({
|
|
818
|
+
id: project.id,
|
|
819
|
+
name: project.name,
|
|
820
|
+
description: project.description,
|
|
821
|
+
instructions: project.prompt_template ?? project.system_prompt ?? '',
|
|
822
|
+
knowledgeFiles,
|
|
823
|
+
createdAt: project.created_at,
|
|
824
|
+
updatedAt: project.updated_at,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
catch {
|
|
828
|
+
// Skip invalid JSON files
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
else if (stats.isDirectory()) {
|
|
833
|
+
// Project folder with metadata.json or project.json
|
|
834
|
+
const metadataFiles = ['metadata.json', 'project.json', 'info.json'];
|
|
835
|
+
let projectData = null;
|
|
836
|
+
for (const metaFile of metadataFiles) {
|
|
837
|
+
const metaPath = join(entryPath, metaFile);
|
|
838
|
+
if (existsSync(metaPath)) {
|
|
839
|
+
try {
|
|
840
|
+
const content = await readFile(metaPath, 'utf-8');
|
|
841
|
+
projectData = JSON.parse(content);
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (projectData) {
|
|
850
|
+
const knowledgeFiles = [];
|
|
851
|
+
// Check for knowledge subdirectory
|
|
852
|
+
const knowledgeDir = join(entryPath, 'knowledge');
|
|
853
|
+
const docsDir = join(entryPath, 'docs');
|
|
854
|
+
const targetDir = existsSync(knowledgeDir) ? knowledgeDir : existsSync(docsDir) ? docsDir : null;
|
|
855
|
+
if (targetDir) {
|
|
856
|
+
const docs = await readdir(targetDir);
|
|
857
|
+
for (const doc of docs) {
|
|
858
|
+
knowledgeFiles.push(doc);
|
|
859
|
+
// Read content for memory entries
|
|
860
|
+
try {
|
|
861
|
+
const docContent = await readFile(join(targetDir, doc), 'utf-8');
|
|
862
|
+
allKnowledge.push({
|
|
863
|
+
id: `${projectData.id}_${doc}`,
|
|
864
|
+
content: docContent,
|
|
865
|
+
createdAt: projectData.created_at,
|
|
866
|
+
source: 'claude-project',
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
// Skip unreadable files
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
bots.push({
|
|
875
|
+
id: projectData.id,
|
|
876
|
+
name: projectData.name,
|
|
877
|
+
description: projectData.description,
|
|
878
|
+
instructions: projectData.prompt_template ?? projectData.system_prompt ?? '',
|
|
879
|
+
knowledgeFiles,
|
|
880
|
+
createdAt: projectData.created_at,
|
|
881
|
+
updatedAt: projectData.updated_at,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (bots.length === 0) {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
const combinedInstructions = bots
|
|
890
|
+
.filter((b) => b.instructions)
|
|
891
|
+
.map((b) => `# ${b.name}\n\n${b.instructions}`)
|
|
892
|
+
.join('\n\n---\n\n');
|
|
893
|
+
return {
|
|
894
|
+
customBots: { bots, count: bots.length },
|
|
895
|
+
instructions: combinedInstructions
|
|
896
|
+
? {
|
|
897
|
+
content: combinedInstructions,
|
|
898
|
+
length: combinedInstructions.length,
|
|
899
|
+
sections: this.parseInstructionSections(combinedInstructions),
|
|
900
|
+
}
|
|
901
|
+
: undefined,
|
|
902
|
+
memories: allKnowledge.length > 0
|
|
903
|
+
? { entries: allKnowledge, count: allKnowledge.length }
|
|
904
|
+
: undefined,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
async extractMemoriesFromFile(memoriesPath) {
|
|
908
|
+
const content = await readFile(memoriesPath, 'utf-8');
|
|
909
|
+
const entries = [];
|
|
910
|
+
if (memoriesPath.endsWith('.json')) {
|
|
911
|
+
// JSON format
|
|
912
|
+
const data = JSON.parse(content);
|
|
913
|
+
const memoryArray = Array.isArray(data) ? data : data.memories ?? [];
|
|
914
|
+
for (const mem of memoryArray) {
|
|
915
|
+
entries.push({
|
|
916
|
+
id: mem.id ?? mem.uuid ?? `mem_${randomBytes(4).toString('hex')}`,
|
|
917
|
+
content: mem.content ?? mem.text ?? String(mem),
|
|
918
|
+
createdAt: mem.created_at ?? new Date().toISOString(),
|
|
919
|
+
updatedAt: mem.updated_at,
|
|
920
|
+
source: 'claude',
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
// Markdown/text format - parse line by line
|
|
926
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
927
|
+
for (const line of lines) {
|
|
928
|
+
// Skip headers and separators
|
|
929
|
+
if (line.startsWith('#') || line.startsWith('---'))
|
|
930
|
+
continue;
|
|
931
|
+
// Remove bullet points if present
|
|
932
|
+
const cleanLine = line.replace(/^[-*•]\s*/, '').trim();
|
|
933
|
+
if (cleanLine) {
|
|
934
|
+
entries.push({
|
|
935
|
+
id: `mem_${randomBytes(4).toString('hex')}`,
|
|
936
|
+
content: cleanLine,
|
|
937
|
+
createdAt: new Date().toISOString(),
|
|
938
|
+
source: 'claude',
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (entries.length === 0) {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
entries,
|
|
948
|
+
count: entries.length,
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
async extractFilesFromDirectory(filesDir, workDir) {
|
|
952
|
+
const outputDir = join(workDir, 'files');
|
|
953
|
+
await mkdir(outputDir, { recursive: true });
|
|
954
|
+
const entries = [];
|
|
955
|
+
let totalSize = 0;
|
|
956
|
+
const files = await readdir(filesDir);
|
|
957
|
+
for (const filename of files) {
|
|
958
|
+
const sourcePath = join(filesDir, filename);
|
|
959
|
+
const stats = await stat(sourcePath);
|
|
960
|
+
if (stats.isFile()) {
|
|
961
|
+
const content = await readFile(sourcePath);
|
|
962
|
+
const safeFilename = this.sanitizeFilename(filename);
|
|
963
|
+
const destPath = join(outputDir, safeFilename);
|
|
964
|
+
await writeFile(destPath, content);
|
|
965
|
+
entries.push({
|
|
966
|
+
id: createHash('md5').update(safeFilename).digest('hex'),
|
|
967
|
+
filename: safeFilename,
|
|
968
|
+
mimeType: this.guessMimeType(safeFilename),
|
|
969
|
+
size: stats.size,
|
|
970
|
+
path: `files/${safeFilename}`,
|
|
971
|
+
});
|
|
972
|
+
totalSize += stats.size;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
files: entries,
|
|
977
|
+
count: entries.length,
|
|
978
|
+
totalSize,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
// ─── Helpers ───────────────────────────────────────────────
|
|
982
|
+
parseInstructionSections(content) {
|
|
983
|
+
const sections = [];
|
|
984
|
+
const lines = content.split('\n');
|
|
985
|
+
let currentSection = null;
|
|
986
|
+
let currentContent = [];
|
|
987
|
+
for (const line of lines) {
|
|
988
|
+
const headerMatch = line.match(/^(#+)\s+(.+)$/);
|
|
989
|
+
if (headerMatch) {
|
|
990
|
+
// Save previous section
|
|
991
|
+
if (currentSection) {
|
|
992
|
+
currentSection.content = currentContent.join('\n').trim();
|
|
993
|
+
sections.push(currentSection);
|
|
994
|
+
}
|
|
995
|
+
// Start new section
|
|
996
|
+
const level = headerMatch[1].length;
|
|
997
|
+
currentSection = {
|
|
998
|
+
title: headerMatch[2],
|
|
999
|
+
content: '',
|
|
1000
|
+
priority: level === 1 ? 'high' : level === 2 ? 'medium' : 'low',
|
|
1001
|
+
};
|
|
1002
|
+
currentContent = [];
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
currentContent.push(line);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
// Save last section
|
|
1009
|
+
if (currentSection) {
|
|
1010
|
+
currentSection.content = currentContent.join('\n').trim();
|
|
1011
|
+
sections.push(currentSection);
|
|
1012
|
+
}
|
|
1013
|
+
return sections;
|
|
1014
|
+
}
|
|
1015
|
+
extractKeyPoints(messages) {
|
|
1016
|
+
const keyPoints = [];
|
|
1017
|
+
const assistantMessages = messages.filter((m) => m.role === 'assistant');
|
|
1018
|
+
for (const msg of assistantMessages.slice(-5)) {
|
|
1019
|
+
const content = msg.content;
|
|
1020
|
+
// Look for conclusion markers
|
|
1021
|
+
const conclusionPatterns = [
|
|
1022
|
+
/(?:in summary|to summarize|in conclusion|the key points? (?:are|is)):?\s*(.{20,200})/gi,
|
|
1023
|
+
/(?:the (?:main|key) takeaway is):?\s*(.{20,200})/gi,
|
|
1024
|
+
/(?:decided|agreed|concluded) (?:to|that):?\s*(.{20,200})/gi,
|
|
1025
|
+
];
|
|
1026
|
+
for (const pattern of conclusionPatterns) {
|
|
1027
|
+
const match = pattern.exec(content);
|
|
1028
|
+
if (match?.[1]) {
|
|
1029
|
+
keyPoints.push(match[1].trim());
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return keyPoints.slice(0, 5);
|
|
1034
|
+
}
|
|
1035
|
+
sanitizeFilename(filename) {
|
|
1036
|
+
return basename(filename)
|
|
1037
|
+
.replace(/[/\\:*?"<>|]/g, '_')
|
|
1038
|
+
.replace(/\s+/g, '_')
|
|
1039
|
+
.slice(0, 200);
|
|
1040
|
+
}
|
|
1041
|
+
guessMimeType(filename) {
|
|
1042
|
+
const ext = extname(filename).toLowerCase();
|
|
1043
|
+
const mimeTypes = {
|
|
1044
|
+
'.txt': 'text/plain',
|
|
1045
|
+
'.md': 'text/markdown',
|
|
1046
|
+
'.json': 'application/json',
|
|
1047
|
+
'.pdf': 'application/pdf',
|
|
1048
|
+
'.png': 'image/png',
|
|
1049
|
+
'.jpg': 'image/jpeg',
|
|
1050
|
+
'.jpeg': 'image/jpeg',
|
|
1051
|
+
'.gif': 'image/gif',
|
|
1052
|
+
'.webp': 'image/webp',
|
|
1053
|
+
'.svg': 'image/svg+xml',
|
|
1054
|
+
'.csv': 'text/csv',
|
|
1055
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
1056
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
1057
|
+
'.py': 'text/x-python',
|
|
1058
|
+
'.js': 'text/javascript',
|
|
1059
|
+
'.ts': 'text/typescript',
|
|
1060
|
+
'.html': 'text/html',
|
|
1061
|
+
'.css': 'text/css',
|
|
1062
|
+
'.zip': 'application/zip',
|
|
1063
|
+
};
|
|
1064
|
+
return mimeTypes[ext] ?? 'application/octet-stream';
|
|
1065
|
+
}
|
|
1066
|
+
getArtifactExtension(type, language) {
|
|
1067
|
+
// Map artifact types to file extensions
|
|
1068
|
+
const typeExtensions = {
|
|
1069
|
+
'application/vnd.ant.code': language ? `.${language}` : '.txt',
|
|
1070
|
+
'text/markdown': '.md',
|
|
1071
|
+
'text/html': '.html',
|
|
1072
|
+
'image/svg+xml': '.svg',
|
|
1073
|
+
'application/vnd.ant.mermaid': '.mmd',
|
|
1074
|
+
'application/vnd.ant.react': '.tsx',
|
|
1075
|
+
};
|
|
1076
|
+
const languageExtensions = {
|
|
1077
|
+
python: '.py',
|
|
1078
|
+
javascript: '.js',
|
|
1079
|
+
typescript: '.ts',
|
|
1080
|
+
java: '.java',
|
|
1081
|
+
cpp: '.cpp',
|
|
1082
|
+
c: '.c',
|
|
1083
|
+
rust: '.rs',
|
|
1084
|
+
go: '.go',
|
|
1085
|
+
ruby: '.rb',
|
|
1086
|
+
php: '.php',
|
|
1087
|
+
swift: '.swift',
|
|
1088
|
+
kotlin: '.kt',
|
|
1089
|
+
scala: '.scala',
|
|
1090
|
+
shell: '.sh',
|
|
1091
|
+
bash: '.sh',
|
|
1092
|
+
sql: '.sql',
|
|
1093
|
+
html: '.html',
|
|
1094
|
+
css: '.css',
|
|
1095
|
+
json: '.json',
|
|
1096
|
+
yaml: '.yaml',
|
|
1097
|
+
xml: '.xml',
|
|
1098
|
+
};
|
|
1099
|
+
if (language && languageExtensions[language.toLowerCase()]) {
|
|
1100
|
+
return languageExtensions[language.toLowerCase()];
|
|
1101
|
+
}
|
|
1102
|
+
return typeExtensions[type] ?? '.txt';
|
|
1103
|
+
}
|
|
1104
|
+
getArtifactMimeType(type, language) {
|
|
1105
|
+
// Direct type mapping
|
|
1106
|
+
if (type === 'text/markdown')
|
|
1107
|
+
return 'text/markdown';
|
|
1108
|
+
if (type === 'text/html')
|
|
1109
|
+
return 'text/html';
|
|
1110
|
+
if (type === 'image/svg+xml')
|
|
1111
|
+
return 'image/svg+xml';
|
|
1112
|
+
// Language-based mapping
|
|
1113
|
+
const languageMimes = {
|
|
1114
|
+
python: 'text/x-python',
|
|
1115
|
+
javascript: 'text/javascript',
|
|
1116
|
+
typescript: 'text/typescript',
|
|
1117
|
+
html: 'text/html',
|
|
1118
|
+
css: 'text/css',
|
|
1119
|
+
json: 'application/json',
|
|
1120
|
+
shell: 'application/x-sh',
|
|
1121
|
+
bash: 'application/x-sh',
|
|
1122
|
+
};
|
|
1123
|
+
if (language && languageMimes[language.toLowerCase()]) {
|
|
1124
|
+
return languageMimes[language.toLowerCase()];
|
|
1125
|
+
}
|
|
1126
|
+
return 'text/plain';
|
|
1127
|
+
}
|
|
1128
|
+
countItems(contents) {
|
|
1129
|
+
return ((contents.instructions ? 1 : 0) +
|
|
1130
|
+
(contents.memories?.count ?? 0) +
|
|
1131
|
+
(contents.conversations?.count ?? 0) +
|
|
1132
|
+
(contents.files?.count ?? 0) +
|
|
1133
|
+
(contents.customBots?.count ?? 0));
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
//# sourceMappingURL=claude.js.map
|