@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.
Files changed (159) hide show
  1. package/dist/cli/__tests__/progress.test.d.ts +5 -0
  2. package/dist/cli/__tests__/progress.test.d.ts.map +1 -0
  3. package/dist/cli/__tests__/progress.test.js +212 -0
  4. package/dist/cli/__tests__/progress.test.js.map +1 -0
  5. package/dist/cli/__tests__/signal-handler.test.d.ts +5 -0
  6. package/dist/cli/__tests__/signal-handler.test.d.ts.map +1 -0
  7. package/dist/cli/__tests__/signal-handler.test.js +99 -0
  8. package/dist/cli/__tests__/signal-handler.test.js.map +1 -0
  9. package/dist/cli/__tests__/summary.test.d.ts +5 -0
  10. package/dist/cli/__tests__/summary.test.d.ts.map +1 -0
  11. package/dist/cli/__tests__/summary.test.js +242 -0
  12. package/dist/cli/__tests__/summary.test.js.map +1 -0
  13. package/dist/cli/index.d.ts +10 -0
  14. package/dist/cli/index.d.ts.map +1 -0
  15. package/dist/cli/index.js +10 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/cli/progress.d.ts +86 -0
  18. package/dist/cli/progress.d.ts.map +1 -0
  19. package/dist/cli/progress.js +205 -0
  20. package/dist/cli/progress.js.map +1 -0
  21. package/dist/cli/prompts.d.ts +49 -0
  22. package/dist/cli/prompts.d.ts.map +1 -0
  23. package/dist/cli/prompts.js +266 -0
  24. package/dist/cli/prompts.js.map +1 -0
  25. package/dist/cli/signal-handler.d.ts +63 -0
  26. package/dist/cli/signal-handler.d.ts.map +1 -0
  27. package/dist/cli/signal-handler.js +165 -0
  28. package/dist/cli/signal-handler.js.map +1 -0
  29. package/dist/cli/summary.d.ts +33 -0
  30. package/dist/cli/summary.d.ts.map +1 -0
  31. package/dist/cli/summary.js +296 -0
  32. package/dist/cli/summary.js.map +1 -0
  33. package/dist/cli.js +7 -1
  34. package/dist/cli.js.map +1 -1
  35. package/dist/commands/migrate.d.ts +18 -4
  36. package/dist/commands/migrate.d.ts.map +1 -1
  37. package/dist/commands/migrate.js +324 -163
  38. package/dist/commands/migrate.js.map +1 -1
  39. package/dist/migrate/__tests__/capabilities.test.d.ts +7 -0
  40. package/dist/migrate/__tests__/capabilities.test.d.ts.map +1 -0
  41. package/dist/migrate/__tests__/capabilities.test.js +90 -0
  42. package/dist/migrate/__tests__/capabilities.test.js.map +1 -0
  43. package/dist/migrate/__tests__/chatgpt-loader.test.d.ts +7 -0
  44. package/dist/migrate/__tests__/chatgpt-loader.test.d.ts.map +1 -0
  45. package/dist/migrate/__tests__/chatgpt-loader.test.js +889 -0
  46. package/dist/migrate/__tests__/chatgpt-loader.test.js.map +1 -0
  47. package/dist/migrate/__tests__/claude-loader.test.d.ts +7 -0
  48. package/dist/migrate/__tests__/claude-loader.test.d.ts.map +1 -0
  49. package/dist/migrate/__tests__/claude-loader.test.js +544 -0
  50. package/dist/migrate/__tests__/claude-loader.test.js.map +1 -0
  51. package/dist/migrate/__tests__/edge-cases.test.d.ts +7 -0
  52. package/dist/migrate/__tests__/edge-cases.test.d.ts.map +1 -0
  53. package/dist/migrate/__tests__/edge-cases.test.js +787 -0
  54. package/dist/migrate/__tests__/edge-cases.test.js.map +1 -0
  55. package/dist/migrate/__tests__/error-recovery.test.d.ts +7 -0
  56. package/dist/migrate/__tests__/error-recovery.test.d.ts.map +1 -0
  57. package/dist/migrate/__tests__/error-recovery.test.js +461 -0
  58. package/dist/migrate/__tests__/error-recovery.test.js.map +1 -0
  59. package/dist/migrate/__tests__/integration.test.d.ts +7 -0
  60. package/dist/migrate/__tests__/integration.test.d.ts.map +1 -0
  61. package/dist/migrate/__tests__/integration.test.js +536 -0
  62. package/dist/migrate/__tests__/integration.test.js.map +1 -0
  63. package/dist/migrate/__tests__/orchestrator.test.d.ts +8 -0
  64. package/dist/migrate/__tests__/orchestrator.test.d.ts.map +1 -0
  65. package/dist/migrate/__tests__/orchestrator.test.js +355 -0
  66. package/dist/migrate/__tests__/orchestrator.test.js.map +1 -0
  67. package/dist/migrate/__tests__/performance.test.d.ts +7 -0
  68. package/dist/migrate/__tests__/performance.test.d.ts.map +1 -0
  69. package/dist/migrate/__tests__/performance.test.js +478 -0
  70. package/dist/migrate/__tests__/performance.test.js.map +1 -0
  71. package/dist/migrate/__tests__/registry.test.d.ts +7 -0
  72. package/dist/migrate/__tests__/registry.test.d.ts.map +1 -0
  73. package/dist/migrate/__tests__/registry.test.js +167 -0
  74. package/dist/migrate/__tests__/registry.test.js.map +1 -0
  75. package/dist/migrate/compatibility.d.ts +47 -0
  76. package/dist/migrate/compatibility.d.ts.map +1 -0
  77. package/dist/migrate/compatibility.js +468 -0
  78. package/dist/migrate/compatibility.js.map +1 -0
  79. package/dist/migrate/extractors/__tests__/chatgpt.test.d.ts +12 -0
  80. package/dist/migrate/extractors/__tests__/chatgpt.test.d.ts.map +1 -0
  81. package/dist/migrate/extractors/__tests__/chatgpt.test.js +522 -0
  82. package/dist/migrate/extractors/__tests__/chatgpt.test.js.map +1 -0
  83. package/dist/migrate/extractors/__tests__/claude.test.d.ts +12 -0
  84. package/dist/migrate/extractors/__tests__/claude.test.d.ts.map +1 -0
  85. package/dist/migrate/extractors/__tests__/claude.test.js +789 -0
  86. package/dist/migrate/extractors/__tests__/claude.test.js.map +1 -0
  87. package/dist/migrate/extractors/chatgpt.d.ts +70 -0
  88. package/dist/migrate/extractors/chatgpt.d.ts.map +1 -0
  89. package/dist/migrate/extractors/chatgpt.js +791 -0
  90. package/dist/migrate/extractors/chatgpt.js.map +1 -0
  91. package/dist/migrate/extractors/claude.d.ts +69 -0
  92. package/dist/migrate/extractors/claude.d.ts.map +1 -0
  93. package/dist/migrate/extractors/claude.js +1136 -0
  94. package/dist/migrate/extractors/claude.js.map +1 -0
  95. package/dist/migrate/extractors/registry.js +6 -4
  96. package/dist/migrate/extractors/registry.js.map +1 -1
  97. package/dist/migrate/index.d.ts +6 -1
  98. package/dist/migrate/index.d.ts.map +1 -1
  99. package/dist/migrate/index.js +12 -1
  100. package/dist/migrate/index.js.map +1 -1
  101. package/dist/migrate/loaders/chatgpt.d.ts +72 -0
  102. package/dist/migrate/loaders/chatgpt.d.ts.map +1 -0
  103. package/dist/migrate/loaders/chatgpt.js +691 -0
  104. package/dist/migrate/loaders/chatgpt.js.map +1 -0
  105. package/dist/migrate/loaders/claude.d.ts +61 -0
  106. package/dist/migrate/loaders/claude.d.ts.map +1 -0
  107. package/dist/migrate/loaders/claude.js +433 -0
  108. package/dist/migrate/loaders/claude.js.map +1 -0
  109. package/dist/migrate/loaders/registry.js +6 -4
  110. package/dist/migrate/loaders/registry.js.map +1 -1
  111. package/dist/migrate/orchestrator.d.ts +75 -1
  112. package/dist/migrate/orchestrator.d.ts.map +1 -1
  113. package/dist/migrate/orchestrator.js +215 -19
  114. package/dist/migrate/orchestrator.js.map +1 -1
  115. package/dist/migrate/testing/index.d.ts +28 -0
  116. package/dist/migrate/testing/index.d.ts.map +1 -0
  117. package/dist/migrate/testing/index.js +55 -0
  118. package/dist/migrate/testing/index.js.map +1 -0
  119. package/dist/migrate/testing/mock-extractor.d.ts +30 -0
  120. package/dist/migrate/testing/mock-extractor.d.ts.map +1 -0
  121. package/dist/migrate/testing/mock-extractor.js +137 -0
  122. package/dist/migrate/testing/mock-extractor.js.map +1 -0
  123. package/dist/migrate/testing/mock-loader.d.ts +36 -0
  124. package/dist/migrate/testing/mock-loader.d.ts.map +1 -0
  125. package/dist/migrate/testing/mock-loader.js +81 -0
  126. package/dist/migrate/testing/mock-loader.js.map +1 -0
  127. package/dist/migrate/testing/mock-transformer.d.ts +26 -0
  128. package/dist/migrate/testing/mock-transformer.d.ts.map +1 -0
  129. package/dist/migrate/testing/mock-transformer.js +185 -0
  130. package/dist/migrate/testing/mock-transformer.js.map +1 -0
  131. package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.d.ts +5 -0
  132. package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.d.ts.map +1 -0
  133. package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.js +333 -0
  134. package/dist/migrate/transformers/__tests__/chatgpt-to-claude.test.js.map +1 -0
  135. package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.d.ts +5 -0
  136. package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.d.ts.map +1 -0
  137. package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.js +333 -0
  138. package/dist/migrate/transformers/__tests__/claude-to-chatgpt.test.js.map +1 -0
  139. package/dist/migrate/transformers/__tests__/rules.test.d.ts +5 -0
  140. package/dist/migrate/transformers/__tests__/rules.test.d.ts.map +1 -0
  141. package/dist/migrate/transformers/__tests__/rules.test.js +375 -0
  142. package/dist/migrate/transformers/__tests__/rules.test.js.map +1 -0
  143. package/dist/migrate/transformers/chatgpt-to-claude.d.ts +40 -0
  144. package/dist/migrate/transformers/chatgpt-to-claude.d.ts.map +1 -0
  145. package/dist/migrate/transformers/chatgpt-to-claude.js +443 -0
  146. package/dist/migrate/transformers/chatgpt-to-claude.js.map +1 -0
  147. package/dist/migrate/transformers/claude-to-chatgpt.d.ts +41 -0
  148. package/dist/migrate/transformers/claude-to-chatgpt.d.ts.map +1 -0
  149. package/dist/migrate/transformers/claude-to-chatgpt.js +532 -0
  150. package/dist/migrate/transformers/claude-to-chatgpt.js.map +1 -0
  151. package/dist/migrate/transformers/registry.js +6 -4
  152. package/dist/migrate/transformers/registry.js.map +1 -1
  153. package/dist/migrate/transformers/rules.d.ts +168 -0
  154. package/dist/migrate/transformers/rules.d.ts.map +1 -0
  155. package/dist/migrate/transformers/rules.js +487 -0
  156. package/dist/migrate/transformers/rules.js.map +1 -0
  157. package/dist/migrate/types.d.ts +2 -0
  158. package/dist/migrate/types.d.ts.map +1 -1
  159. 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