@savestate/cli 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/chatgpt.d.ts +113 -0
- package/dist/adapters/chatgpt.d.ts.map +1 -0
- package/dist/adapters/chatgpt.js +668 -0
- package/dist/adapters/chatgpt.js.map +1 -0
- package/dist/adapters/claude-web.d.ts +81 -0
- package/dist/adapters/claude-web.d.ts.map +1 -0
- package/dist/adapters/claude-web.js +903 -0
- package/dist/adapters/claude-web.js.map +1 -0
- package/dist/adapters/gemini.d.ts +108 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +814 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/openai-assistants.d.ts +79 -36
- package/dist/adapters/openai-assistants.d.ts.map +1 -1
- package/dist/adapters/openai-assistants.js +802 -78
- package/dist/adapters/openai-assistants.js.map +1 -1
- package/dist/adapters/registry.d.ts.map +1 -1
- package/dist/adapters/registry.js +6 -4
- package/dist/adapters/registry.js.map +1 -1
- package/dist/commands/adapters.js +5 -5
- package/dist/commands/adapters.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,50 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenAI Assistants Adapter
|
|
2
|
+
* OpenAI Assistants Adapter
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* and provides detection logic.
|
|
4
|
+
* Full adapter for OpenAI Assistants API v2.
|
|
5
|
+
* Captures assistant configuration, files, vector stores, and threads.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* Authentication: OPENAI_API_KEY env var (required).
|
|
8
|
+
* Target assistant: SAVESTATE_OPENAI_ASSISTANT_ID env var (optional).
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* tools (code_interpreter, file_search, function definitions), metadata,
|
|
12
|
-
* temperature, top_p, response_format
|
|
13
|
-
*
|
|
14
|
-
* - **Threads and messages** — conversation threads with full message history,
|
|
15
|
-
* including role, content (text + annotations), attachments, and run metadata.
|
|
16
|
-
* Would support paginated retrieval for large thread histories.
|
|
17
|
-
*
|
|
18
|
-
* - **Files attached to the assistant** — files uploaded for code_interpreter
|
|
19
|
-
* or file_search. Would capture file metadata (id, filename, purpose, bytes)
|
|
20
|
-
* and optionally download file content.
|
|
21
|
-
*
|
|
22
|
-
* - **Vector store contents** — vector stores used by file_search tool,
|
|
23
|
-
* including store configuration, file associations, and chunking strategy.
|
|
24
|
-
* Would NOT re-embed on restore; instead store file references.
|
|
25
|
-
*
|
|
26
|
-
* - **Function/tool definitions** — full JSON schemas for any custom function
|
|
27
|
-
* tools defined on the assistant, enabling restore to a new assistant.
|
|
28
|
-
*
|
|
29
|
-
* Authentication: Uses OPENAI_API_KEY env var or .openai/ config directory.
|
|
30
|
-
*
|
|
31
|
-
* API endpoints used:
|
|
32
|
-
* GET /v1/assistants/{id} — Assistant config
|
|
33
|
-
* GET /v1/assistants/{id}/files — Attached files
|
|
34
|
-
* GET /v1/threads/{id}/messages — Thread messages
|
|
35
|
-
* GET /v1/files/{id}/content — File download
|
|
36
|
-
* GET /v1/vector_stores/{id} — Vector store config
|
|
37
|
-
* POST /v1/assistants — Create assistant (restore)
|
|
38
|
-
* POST /v1/threads — Create thread (restore)
|
|
10
|
+
* Uses native fetch() — no openai npm package dependency.
|
|
39
11
|
*/
|
|
12
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
40
13
|
import { existsSync } from 'node:fs';
|
|
41
14
|
import { join } from 'node:path';
|
|
42
15
|
import { homedir } from 'node:os';
|
|
16
|
+
import { SAF_VERSION, generateSnapshotId, computeChecksum } from '../format.js';
|
|
17
|
+
// ─── Constants ───────────────────────────────────────────────
|
|
18
|
+
const OPENAI_BASE = 'https://api.openai.com/v1';
|
|
19
|
+
const THREADS_CACHE_DIR = '.savestate';
|
|
20
|
+
const THREADS_CACHE_FILE = 'openai-threads.json';
|
|
21
|
+
const MAX_RETRIES = 3;
|
|
22
|
+
const INITIAL_BACKOFF_MS = 1000;
|
|
23
|
+
// ─── Adapter ─────────────────────────────────────────────────
|
|
43
24
|
export class OpenAIAssistantsAdapter {
|
|
44
25
|
id = 'openai-assistants';
|
|
45
26
|
name = 'OpenAI Assistants';
|
|
46
27
|
platform = 'openai-assistants';
|
|
47
|
-
version = '0.0
|
|
28
|
+
version = '0.1.0';
|
|
29
|
+
apiKey = '';
|
|
30
|
+
warnings = [];
|
|
48
31
|
async detect() {
|
|
49
32
|
// Check for .openai/ config directory
|
|
50
33
|
const openaiDir = join(homedir(), '.openai');
|
|
@@ -59,56 +42,797 @@ export class OpenAIAssistantsAdapter {
|
|
|
59
42
|
return false;
|
|
60
43
|
}
|
|
61
44
|
async extract() {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
45
|
+
this.apiKey = this.requireApiKey();
|
|
46
|
+
this.warnings = [];
|
|
47
|
+
// 1. Determine which assistant to extract
|
|
48
|
+
const assistant = await this.resolveAssistant();
|
|
49
|
+
this.log(`Extracting assistant: ${assistant.name ?? assistant.id} (${assistant.model})`);
|
|
50
|
+
// 2. Extract identity (config, instructions, tools)
|
|
51
|
+
this.log('Extracting assistant configuration...');
|
|
52
|
+
const { personality, config, tools } = this.extractIdentity(assistant);
|
|
53
|
+
// 3. Extract files from tool_resources
|
|
54
|
+
this.log('Extracting files and vector stores...');
|
|
55
|
+
const { knowledge, fileContents, vectorStores } = await this.extractFiles(assistant);
|
|
56
|
+
// Store vector store info in config
|
|
57
|
+
if (vectorStores.length > 0) {
|
|
58
|
+
config.vector_stores = vectorStores;
|
|
59
|
+
}
|
|
60
|
+
// Store file contents in config for restore
|
|
61
|
+
if (Object.keys(fileContents).length > 0) {
|
|
62
|
+
config._file_contents = fileContents;
|
|
63
|
+
}
|
|
64
|
+
// 4. Extract threads (if we have cached thread IDs)
|
|
65
|
+
this.log('Checking for thread history...');
|
|
66
|
+
const { conversations, conversationDetails } = await this.extractThreads(assistant.id);
|
|
67
|
+
// 5. Build memory entries from file metadata
|
|
68
|
+
const memoryEntries = [];
|
|
69
|
+
// Build snapshot
|
|
70
|
+
const snapshotId = generateSnapshotId();
|
|
71
|
+
const now = new Date().toISOString();
|
|
72
|
+
if (this.warnings.length > 0) {
|
|
73
|
+
for (const w of this.warnings) {
|
|
74
|
+
console.warn(` ⚠ ${w}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const snapshot = {
|
|
78
|
+
manifest: {
|
|
79
|
+
version: SAF_VERSION,
|
|
80
|
+
timestamp: now,
|
|
81
|
+
id: snapshotId,
|
|
82
|
+
platform: this.platform,
|
|
83
|
+
adapter: this.id,
|
|
84
|
+
checksum: '',
|
|
85
|
+
size: 0,
|
|
86
|
+
},
|
|
87
|
+
identity: {
|
|
88
|
+
personality,
|
|
89
|
+
config,
|
|
90
|
+
tools,
|
|
91
|
+
},
|
|
92
|
+
memory: {
|
|
93
|
+
core: memoryEntries,
|
|
94
|
+
knowledge,
|
|
95
|
+
},
|
|
96
|
+
conversations: {
|
|
97
|
+
total: conversations.length,
|
|
98
|
+
conversations,
|
|
99
|
+
},
|
|
100
|
+
platform: await this.identify(),
|
|
101
|
+
chain: {
|
|
102
|
+
current: snapshotId,
|
|
103
|
+
ancestors: [],
|
|
104
|
+
},
|
|
105
|
+
restoreHints: {
|
|
106
|
+
platform: this.platform,
|
|
107
|
+
steps: [
|
|
108
|
+
{
|
|
109
|
+
type: 'api',
|
|
110
|
+
description: 'Create new assistant with captured configuration',
|
|
111
|
+
target: 'POST /v1/assistants',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: 'api',
|
|
115
|
+
description: 'Upload files and create vector stores',
|
|
116
|
+
target: 'POST /v1/files, POST /v1/vector_stores',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: 'api',
|
|
120
|
+
description: 'Recreate threads with messages (optional)',
|
|
121
|
+
target: 'POST /v1/threads, POST /v1/threads/{id}/messages',
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
manualSteps: [
|
|
125
|
+
'Verify the new assistant ID works in your application',
|
|
126
|
+
'Update any thread IDs in your application (threads cannot be migrated, only recreated)',
|
|
127
|
+
'Vector store files will be re-indexed automatically after upload',
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
this.log(`✓ Extraction complete: ${assistant.name ?? assistant.id}`);
|
|
132
|
+
return snapshot;
|
|
133
|
+
}
|
|
134
|
+
async restore(snapshot) {
|
|
135
|
+
this.apiKey = this.requireApiKey();
|
|
136
|
+
this.warnings = [];
|
|
137
|
+
const config = (snapshot.identity.config ?? {});
|
|
138
|
+
// 1. Upload files first (if we have file contents)
|
|
139
|
+
const fileIdMap = new Map(); // old ID → new ID
|
|
140
|
+
const fileContents = (config._file_contents ?? {});
|
|
141
|
+
if (Object.keys(fileContents).length > 0) {
|
|
142
|
+
this.log(`Uploading ${Object.keys(fileContents).length} files...`);
|
|
143
|
+
for (const [oldFileId, b64content] of Object.entries(fileContents)) {
|
|
144
|
+
const fileInfo = snapshot.memory.knowledge.find(k => k.id === `openai-file:${oldFileId}`);
|
|
145
|
+
const filename = fileInfo?.filename ?? `file-${oldFileId}`;
|
|
146
|
+
try {
|
|
147
|
+
const newFileId = await this.uploadFile(filename, Buffer.from(b64content, 'base64'));
|
|
148
|
+
fileIdMap.set(oldFileId, newFileId);
|
|
149
|
+
this.log(` Uploaded: ${filename} → ${newFileId}`);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
this.warn(`Failed to upload file ${filename}: ${err instanceof Error ? err.message : String(err)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// 2. Create vector stores (if any)
|
|
157
|
+
const vectorStoreIdMap = new Map(); // old ID → new ID
|
|
158
|
+
const vectorStores = (config.vector_stores ?? []);
|
|
159
|
+
if (vectorStores.length > 0) {
|
|
160
|
+
this.log(`Creating ${vectorStores.length} vector store(s)...`);
|
|
161
|
+
for (const vs of vectorStores) {
|
|
162
|
+
try {
|
|
163
|
+
// Map old file IDs to new ones
|
|
164
|
+
const newFileIds = vs.file_ids
|
|
165
|
+
.map(fid => fileIdMap.get(fid))
|
|
166
|
+
.filter((id) => id !== undefined);
|
|
167
|
+
const newVsId = await this.createVectorStore(vs.name, newFileIds, vs.metadata, vs.expires_after);
|
|
168
|
+
vectorStoreIdMap.set(vs.id, newVsId);
|
|
169
|
+
this.log(` Created vector store: ${vs.name} → ${newVsId} (${newFileIds.length} files)`);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
this.warn(`Failed to create vector store ${vs.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// 3. Build tool_resources for the new assistant
|
|
177
|
+
const toolResources = {};
|
|
178
|
+
// Code interpreter files
|
|
179
|
+
const codeInterpreterFileIds = (config.tool_resources
|
|
180
|
+
?.code_interpreter?.file_ids ?? [])
|
|
181
|
+
.map(fid => fileIdMap.get(fid))
|
|
182
|
+
.filter((id) => id !== undefined);
|
|
183
|
+
if (codeInterpreterFileIds.length > 0) {
|
|
184
|
+
toolResources.code_interpreter = { file_ids: codeInterpreterFileIds };
|
|
185
|
+
}
|
|
186
|
+
// File search vector stores
|
|
187
|
+
const oldVsIds = config.tool_resources
|
|
188
|
+
?.file_search?.vector_store_ids ?? [];
|
|
189
|
+
const newVsIds = oldVsIds
|
|
190
|
+
.map(vid => vectorStoreIdMap.get(vid))
|
|
191
|
+
.filter((id) => id !== undefined);
|
|
192
|
+
if (newVsIds.length > 0) {
|
|
193
|
+
toolResources.file_search = { vector_store_ids: newVsIds };
|
|
194
|
+
}
|
|
195
|
+
// 4. Create the new assistant
|
|
196
|
+
this.log('Creating new assistant...');
|
|
197
|
+
const assistantBody = {
|
|
198
|
+
model: config.model ?? 'gpt-4o',
|
|
199
|
+
name: config.name ?? undefined,
|
|
200
|
+
description: config.description ?? undefined,
|
|
201
|
+
instructions: snapshot.identity.personality ?? undefined,
|
|
202
|
+
metadata: config.metadata ?? {},
|
|
203
|
+
};
|
|
204
|
+
// Add temperature/top_p if set
|
|
205
|
+
if (config.temperature !== undefined && config.temperature !== null) {
|
|
206
|
+
assistantBody.temperature = config.temperature;
|
|
207
|
+
}
|
|
208
|
+
if (config.top_p !== undefined && config.top_p !== null) {
|
|
209
|
+
assistantBody.top_p = config.top_p;
|
|
210
|
+
}
|
|
211
|
+
if (config.response_format !== undefined && config.response_format !== null) {
|
|
212
|
+
assistantBody.response_format = config.response_format;
|
|
213
|
+
}
|
|
214
|
+
// Restore tools
|
|
215
|
+
const restoredTools = [];
|
|
216
|
+
const originalTools = (config.tools_raw ?? []);
|
|
217
|
+
for (const tool of originalTools) {
|
|
218
|
+
restoredTools.push(tool);
|
|
219
|
+
}
|
|
220
|
+
if (restoredTools.length > 0) {
|
|
221
|
+
assistantBody.tools = restoredTools;
|
|
222
|
+
}
|
|
223
|
+
// Attach tool_resources
|
|
224
|
+
if (Object.keys(toolResources).length > 0) {
|
|
225
|
+
assistantBody.tool_resources = toolResources;
|
|
226
|
+
}
|
|
227
|
+
// Remove undefined values
|
|
228
|
+
for (const key of Object.keys(assistantBody)) {
|
|
229
|
+
if (assistantBody[key] === undefined) {
|
|
230
|
+
delete assistantBody[key];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const newAssistant = await this.apiPost('/assistants', assistantBody);
|
|
234
|
+
this.log(`✓ Created assistant: ${newAssistant.name ?? '(unnamed)'} → ${newAssistant.id}`);
|
|
235
|
+
// 5. Optionally recreate threads
|
|
236
|
+
if (snapshot.conversations.total > 0) {
|
|
237
|
+
this.log(`Recreating ${snapshot.conversations.total} thread(s)...`);
|
|
238
|
+
const newThreadIds = [];
|
|
239
|
+
for (const convMeta of snapshot.conversations.conversations) {
|
|
240
|
+
try {
|
|
241
|
+
// The conversation details are stored as memory entries
|
|
242
|
+
const convEntry = snapshot.memory.core.find(m => m.id === `thread:${convMeta.id}`);
|
|
243
|
+
if (!convEntry) {
|
|
244
|
+
this.warn(`No message data for thread ${convMeta.id}, skipping`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const messages = JSON.parse(convEntry.content);
|
|
248
|
+
const threadId = await this.recreateThread(messages);
|
|
249
|
+
newThreadIds.push(threadId);
|
|
250
|
+
this.log(` Recreated thread: ${convMeta.id} → ${threadId} (${messages.length} messages)`);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
this.warn(`Failed to recreate thread ${convMeta.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Save new thread IDs to cache
|
|
257
|
+
if (newThreadIds.length > 0) {
|
|
258
|
+
await this.saveThreadCache(newAssistant.id, newThreadIds);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// 6. Summary
|
|
262
|
+
console.error('');
|
|
263
|
+
console.error('┌─────────────────────────────────────────────┐');
|
|
264
|
+
console.error('│ ✓ Restore complete │');
|
|
265
|
+
console.error('├─────────────────────────────────────────────┤');
|
|
266
|
+
console.error(`│ New Assistant ID: ${newAssistant.id} │`);
|
|
267
|
+
console.error(`│ Model: ${(newAssistant.model ?? '').padEnd(35)}│`);
|
|
268
|
+
console.error(`│ Tools: ${String(newAssistant.tools?.length ?? 0).padEnd(35)}│`);
|
|
269
|
+
console.error(`│ Files uploaded: ${String(fileIdMap.size).padEnd(27)}│`);
|
|
270
|
+
console.error(`│ Vector stores: ${String(vectorStoreIdMap.size).padEnd(27)}│`);
|
|
271
|
+
console.error('└─────────────────────────────────────────────┘');
|
|
272
|
+
if (this.warnings.length > 0) {
|
|
273
|
+
console.error('');
|
|
274
|
+
for (const w of this.warnings) {
|
|
275
|
+
console.warn(` ⚠ ${w}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
104
278
|
}
|
|
105
279
|
async identify() {
|
|
106
280
|
return {
|
|
107
281
|
name: 'OpenAI Assistants',
|
|
108
282
|
version: this.version,
|
|
109
|
-
apiVersion: '
|
|
283
|
+
apiVersion: 'v2',
|
|
110
284
|
exportMethod: 'api',
|
|
111
285
|
};
|
|
112
286
|
}
|
|
287
|
+
// ─── Private: API helpers ──────────────────────────────────
|
|
288
|
+
requireApiKey() {
|
|
289
|
+
const key = process.env.OPENAI_API_KEY;
|
|
290
|
+
if (!key) {
|
|
291
|
+
throw new Error('OPENAI_API_KEY environment variable is required.\n' +
|
|
292
|
+
'Set it with: export OPENAI_API_KEY=sk-...\n' +
|
|
293
|
+
'Get your key at: https://platform.openai.com/api-keys');
|
|
294
|
+
}
|
|
295
|
+
return key;
|
|
296
|
+
}
|
|
297
|
+
headers() {
|
|
298
|
+
return {
|
|
299
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
300
|
+
'OpenAI-Beta': 'assistants=v2',
|
|
301
|
+
'Content-Type': 'application/json',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Make a GET request to the OpenAI API with retry on 429.
|
|
306
|
+
*/
|
|
307
|
+
async apiGet(path) {
|
|
308
|
+
return this.apiRequest('GET', path);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Make a POST request to the OpenAI API with retry on 429.
|
|
312
|
+
*/
|
|
313
|
+
async apiPost(path, body) {
|
|
314
|
+
return this.apiRequest('POST', path, body);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Core API request with retry logic for rate limits.
|
|
318
|
+
*/
|
|
319
|
+
async apiRequest(method, path, body) {
|
|
320
|
+
let lastError = null;
|
|
321
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
322
|
+
const url = `${OPENAI_BASE}${path}`;
|
|
323
|
+
const opts = {
|
|
324
|
+
method,
|
|
325
|
+
headers: this.headers(),
|
|
326
|
+
};
|
|
327
|
+
if (body !== undefined) {
|
|
328
|
+
opts.body = JSON.stringify(body);
|
|
329
|
+
}
|
|
330
|
+
const res = await fetch(url, opts);
|
|
331
|
+
if (res.status === 429) {
|
|
332
|
+
const retryAfter = res.headers.get('retry-after');
|
|
333
|
+
const waitMs = retryAfter
|
|
334
|
+
? parseInt(retryAfter, 10) * 1000
|
|
335
|
+
: INITIAL_BACKOFF_MS * Math.pow(2, attempt);
|
|
336
|
+
this.log(` Rate limited, retrying in ${Math.round(waitMs / 1000)}s...`);
|
|
337
|
+
await this.sleep(waitMs);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (!res.ok) {
|
|
341
|
+
const errorBody = await res.text();
|
|
342
|
+
lastError = new Error(`OpenAI API error ${res.status} ${method} ${path}: ${errorBody}`);
|
|
343
|
+
if (res.status >= 500) {
|
|
344
|
+
// Retry on server errors
|
|
345
|
+
await this.sleep(INITIAL_BACKOFF_MS * Math.pow(2, attempt));
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
throw lastError;
|
|
349
|
+
}
|
|
350
|
+
return (await res.json());
|
|
351
|
+
}
|
|
352
|
+
throw lastError ?? new Error(`Failed after ${MAX_RETRIES} retries: ${method} ${path}`);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* GET raw bytes from the API (for file content download).
|
|
356
|
+
*/
|
|
357
|
+
async apiGetBytes(path) {
|
|
358
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
359
|
+
const url = `${OPENAI_BASE}${path}`;
|
|
360
|
+
const res = await fetch(url, {
|
|
361
|
+
method: 'GET',
|
|
362
|
+
headers: {
|
|
363
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
364
|
+
'OpenAI-Beta': 'assistants=v2',
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
if (res.status === 429) {
|
|
368
|
+
const retryAfter = res.headers.get('retry-after');
|
|
369
|
+
const waitMs = retryAfter
|
|
370
|
+
? parseInt(retryAfter, 10) * 1000
|
|
371
|
+
: INITIAL_BACKOFF_MS * Math.pow(2, attempt);
|
|
372
|
+
await this.sleep(waitMs);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (!res.ok) {
|
|
376
|
+
const errorBody = await res.text();
|
|
377
|
+
throw new Error(`OpenAI API error ${res.status} GET ${path}: ${errorBody}`);
|
|
378
|
+
}
|
|
379
|
+
const arrayBuf = await res.arrayBuffer();
|
|
380
|
+
return Buffer.from(arrayBuf);
|
|
381
|
+
}
|
|
382
|
+
throw new Error(`Failed to download after ${MAX_RETRIES} retries: ${path}`);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Upload a file using multipart/form-data.
|
|
386
|
+
*/
|
|
387
|
+
async uploadFile(filename, content) {
|
|
388
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
389
|
+
const formData = new FormData();
|
|
390
|
+
const blob = new Blob([content]);
|
|
391
|
+
formData.append('file', blob, filename);
|
|
392
|
+
formData.append('purpose', 'assistants');
|
|
393
|
+
const res = await fetch(`${OPENAI_BASE}/files`, {
|
|
394
|
+
method: 'POST',
|
|
395
|
+
headers: {
|
|
396
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
397
|
+
// Don't set Content-Type for FormData — fetch sets it with boundary
|
|
398
|
+
},
|
|
399
|
+
body: formData,
|
|
400
|
+
});
|
|
401
|
+
if (res.status === 429) {
|
|
402
|
+
const retryAfter = res.headers.get('retry-after');
|
|
403
|
+
const waitMs = retryAfter
|
|
404
|
+
? parseInt(retryAfter, 10) * 1000
|
|
405
|
+
: INITIAL_BACKOFF_MS * Math.pow(2, attempt);
|
|
406
|
+
await this.sleep(waitMs);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (!res.ok) {
|
|
410
|
+
const errorBody = await res.text();
|
|
411
|
+
throw new Error(`File upload failed (${res.status}): ${errorBody}`);
|
|
412
|
+
}
|
|
413
|
+
const fileObj = (await res.json());
|
|
414
|
+
return fileObj.id;
|
|
415
|
+
}
|
|
416
|
+
throw new Error(`File upload failed after ${MAX_RETRIES} retries`);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Paginate through a list endpoint, collecting all items.
|
|
420
|
+
*/
|
|
421
|
+
async paginateList(path, queryParams) {
|
|
422
|
+
const items = [];
|
|
423
|
+
let after;
|
|
424
|
+
while (true) {
|
|
425
|
+
const params = new URLSearchParams();
|
|
426
|
+
params.set('limit', '100');
|
|
427
|
+
if (after)
|
|
428
|
+
params.set('after', after);
|
|
429
|
+
const fullPath = queryParams
|
|
430
|
+
? `${path}?${params.toString()}&${queryParams}`
|
|
431
|
+
: `${path}?${params.toString()}`;
|
|
432
|
+
const response = await this.apiGet(fullPath);
|
|
433
|
+
items.push(...response.data);
|
|
434
|
+
if (!response.has_more || response.data.length === 0)
|
|
435
|
+
break;
|
|
436
|
+
after = response.last_id;
|
|
437
|
+
}
|
|
438
|
+
return items;
|
|
439
|
+
}
|
|
440
|
+
// ─── Private: Extract helpers ──────────────────────────────
|
|
441
|
+
/**
|
|
442
|
+
* Resolve which assistant to extract. Uses SAVESTATE_OPENAI_ASSISTANT_ID
|
|
443
|
+
* if set, otherwise lists all and picks the first (or errors if none).
|
|
444
|
+
*/
|
|
445
|
+
async resolveAssistant() {
|
|
446
|
+
const targetId = process.env.SAVESTATE_OPENAI_ASSISTANT_ID;
|
|
447
|
+
if (targetId) {
|
|
448
|
+
this.log(`Fetching assistant: ${targetId}`);
|
|
449
|
+
return this.apiGet(`/assistants/${targetId}`);
|
|
450
|
+
}
|
|
451
|
+
// List all assistants
|
|
452
|
+
this.log('No SAVESTATE_OPENAI_ASSISTANT_ID set, listing assistants...');
|
|
453
|
+
const assistants = await this.paginateList('/assistants');
|
|
454
|
+
if (assistants.length === 0) {
|
|
455
|
+
throw new Error('No assistants found in your OpenAI account.\n' +
|
|
456
|
+
'Create one at: https://platform.openai.com/assistants');
|
|
457
|
+
}
|
|
458
|
+
if (assistants.length === 1) {
|
|
459
|
+
this.log(`Found 1 assistant, using: ${assistants[0].name ?? assistants[0].id}`);
|
|
460
|
+
return assistants[0];
|
|
461
|
+
}
|
|
462
|
+
// Multiple assistants — list them and ask user to specify
|
|
463
|
+
console.error('');
|
|
464
|
+
console.error('Multiple assistants found:');
|
|
465
|
+
for (const a of assistants) {
|
|
466
|
+
console.error(` ${a.id} ${a.name ?? '(unnamed)'} [${a.model}]`);
|
|
467
|
+
}
|
|
468
|
+
console.error('');
|
|
469
|
+
console.error('Set SAVESTATE_OPENAI_ASSISTANT_ID to target a specific assistant.');
|
|
470
|
+
console.error(`Using first assistant: ${assistants[0].name ?? assistants[0].id}`);
|
|
471
|
+
console.error('');
|
|
472
|
+
return assistants[0];
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Extract identity info from the assistant object.
|
|
476
|
+
*/
|
|
477
|
+
extractIdentity(assistant) {
|
|
478
|
+
const personality = assistant.instructions ?? '';
|
|
479
|
+
const config = {
|
|
480
|
+
assistant_id: assistant.id,
|
|
481
|
+
name: assistant.name,
|
|
482
|
+
description: assistant.description,
|
|
483
|
+
model: assistant.model,
|
|
484
|
+
metadata: assistant.metadata,
|
|
485
|
+
temperature: assistant.temperature,
|
|
486
|
+
top_p: assistant.top_p,
|
|
487
|
+
response_format: assistant.response_format,
|
|
488
|
+
created_at: assistant.created_at,
|
|
489
|
+
// Preserve the raw tools array for restore
|
|
490
|
+
tools_raw: assistant.tools,
|
|
491
|
+
// Preserve tool_resources for restore
|
|
492
|
+
tool_resources: assistant.tool_resources,
|
|
493
|
+
};
|
|
494
|
+
// Convert tools to SaveState ToolConfig format
|
|
495
|
+
const tools = assistant.tools.map(tool => {
|
|
496
|
+
if (tool.type === 'function' && tool.function) {
|
|
497
|
+
return {
|
|
498
|
+
name: tool.function.name,
|
|
499
|
+
type: 'function',
|
|
500
|
+
config: {
|
|
501
|
+
description: tool.function.description,
|
|
502
|
+
parameters: tool.function.parameters,
|
|
503
|
+
strict: tool.function.strict,
|
|
504
|
+
},
|
|
505
|
+
enabled: true,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
name: tool.type,
|
|
510
|
+
type: tool.type,
|
|
511
|
+
config: {},
|
|
512
|
+
enabled: true,
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
return { personality, config, tools };
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Extract files and vector stores associated with the assistant.
|
|
519
|
+
*/
|
|
520
|
+
async extractFiles(assistant) {
|
|
521
|
+
const knowledge = [];
|
|
522
|
+
const fileContents = {};
|
|
523
|
+
const vectorStores = [];
|
|
524
|
+
// Collect all file IDs we need to download
|
|
525
|
+
const allFileIds = new Set();
|
|
526
|
+
// Code interpreter files
|
|
527
|
+
const ciFileIds = assistant.tool_resources?.code_interpreter?.file_ids ?? [];
|
|
528
|
+
for (const fid of ciFileIds) {
|
|
529
|
+
allFileIds.add(fid);
|
|
530
|
+
}
|
|
531
|
+
// Vector store files
|
|
532
|
+
const vsIds = assistant.tool_resources?.file_search?.vector_store_ids ?? [];
|
|
533
|
+
if (vsIds.length > 0) {
|
|
534
|
+
this.log(` Fetching ${vsIds.length} vector store(s)...`);
|
|
535
|
+
}
|
|
536
|
+
for (const vsId of vsIds) {
|
|
537
|
+
try {
|
|
538
|
+
const vs = await this.apiGet(`/vector_stores/${vsId}`);
|
|
539
|
+
const vsFiles = await this.paginateList(`/vector_stores/${vsId}/files`);
|
|
540
|
+
const vsFileIds = vsFiles.map(f => f.id);
|
|
541
|
+
for (const fid of vsFileIds) {
|
|
542
|
+
allFileIds.add(fid);
|
|
543
|
+
}
|
|
544
|
+
vectorStores.push({
|
|
545
|
+
id: vs.id,
|
|
546
|
+
name: vs.name,
|
|
547
|
+
metadata: vs.metadata,
|
|
548
|
+
file_ids: vsFileIds,
|
|
549
|
+
expires_after: vs.expires_after,
|
|
550
|
+
});
|
|
551
|
+
this.log(` Vector store "${vs.name}": ${vsFileIds.length} files`);
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
this.warn(`Failed to fetch vector store ${vsId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// Download all files
|
|
558
|
+
if (allFileIds.size > 0) {
|
|
559
|
+
this.log(` Downloading ${allFileIds.size} file(s)...`);
|
|
560
|
+
}
|
|
561
|
+
for (const fileId of allFileIds) {
|
|
562
|
+
try {
|
|
563
|
+
// Get file metadata
|
|
564
|
+
const fileMeta = await this.apiGet(`/files/${fileId}`);
|
|
565
|
+
// Download file content
|
|
566
|
+
const content = await this.apiGetBytes(`/files/${fileId}/content`);
|
|
567
|
+
const checksum = computeChecksum(content);
|
|
568
|
+
knowledge.push({
|
|
569
|
+
id: `openai-file:${fileId}`,
|
|
570
|
+
filename: fileMeta.filename,
|
|
571
|
+
mimeType: this.guessMimeType(fileMeta.filename),
|
|
572
|
+
path: `knowledge/files/${fileMeta.filename}`,
|
|
573
|
+
size: fileMeta.bytes,
|
|
574
|
+
checksum,
|
|
575
|
+
});
|
|
576
|
+
// Store file content as base64 for restore
|
|
577
|
+
fileContents[fileId] = content.toString('base64');
|
|
578
|
+
this.log(` Downloaded: ${fileMeta.filename} (${this.formatBytes(fileMeta.bytes)})`);
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
this.warn(`Failed to download file ${fileId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return { knowledge, fileContents, vectorStores };
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Extract threads using the local thread cache.
|
|
588
|
+
* OpenAI has no "list all threads" endpoint, so we rely on cached IDs.
|
|
589
|
+
*/
|
|
590
|
+
async extractThreads(assistantId) {
|
|
591
|
+
const conversations = [];
|
|
592
|
+
const conversationDetails = [];
|
|
593
|
+
// Try to load cached thread IDs
|
|
594
|
+
const threadIds = await this.loadThreadCache(assistantId);
|
|
595
|
+
if (threadIds.length === 0) {
|
|
596
|
+
console.error(' ℹ No cached thread IDs found.');
|
|
597
|
+
console.error(' OpenAI does not provide a "list all threads" endpoint.');
|
|
598
|
+
console.error(` To capture threads, create ${THREADS_CACHE_DIR}/${THREADS_CACHE_FILE}`);
|
|
599
|
+
console.error(' with format: { "assistant_id": "...", "thread_ids": ["thread_..."] }');
|
|
600
|
+
console.error(' Thread IDs are returned when you create threads via the API.');
|
|
601
|
+
return { conversations, conversationDetails };
|
|
602
|
+
}
|
|
603
|
+
this.log(` Found ${threadIds.length} cached thread(s), fetching messages...`);
|
|
604
|
+
for (const threadId of threadIds) {
|
|
605
|
+
try {
|
|
606
|
+
const messages = await this.paginateList(`/threads/${threadId}/messages`, 'order=asc');
|
|
607
|
+
if (messages.length === 0)
|
|
608
|
+
continue;
|
|
609
|
+
const firstMsg = messages[0];
|
|
610
|
+
const lastMsg = messages[messages.length - 1];
|
|
611
|
+
const convertedMessages = messages.map(msg => ({
|
|
612
|
+
id: msg.id,
|
|
613
|
+
role: msg.role,
|
|
614
|
+
content: this.extractMessageContent(msg),
|
|
615
|
+
timestamp: new Date(msg.created_at * 1000).toISOString(),
|
|
616
|
+
metadata: {
|
|
617
|
+
...msg.metadata,
|
|
618
|
+
...(msg.run_id ? { run_id: msg.run_id } : {}),
|
|
619
|
+
},
|
|
620
|
+
}));
|
|
621
|
+
const conv = {
|
|
622
|
+
id: threadId,
|
|
623
|
+
title: `Thread ${threadId.slice(0, 12)}...`,
|
|
624
|
+
createdAt: new Date(firstMsg.created_at * 1000).toISOString(),
|
|
625
|
+
updatedAt: new Date(lastMsg.created_at * 1000).toISOString(),
|
|
626
|
+
messages: convertedMessages,
|
|
627
|
+
metadata: { thread_id: threadId },
|
|
628
|
+
};
|
|
629
|
+
conversationDetails.push(conv);
|
|
630
|
+
conversations.push({
|
|
631
|
+
id: threadId,
|
|
632
|
+
title: conv.title,
|
|
633
|
+
createdAt: conv.createdAt,
|
|
634
|
+
updatedAt: conv.updatedAt,
|
|
635
|
+
messageCount: messages.length,
|
|
636
|
+
path: `conversations/${threadId}.json`,
|
|
637
|
+
});
|
|
638
|
+
this.log(` Thread ${threadId.slice(0, 16)}...: ${messages.length} messages`);
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
this.warn(`Failed to fetch thread ${threadId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
// Save thread details as memory entries so restore can access them
|
|
645
|
+
// We store the full messages JSON as the content of a memory entry
|
|
646
|
+
for (const conv of conversationDetails) {
|
|
647
|
+
// This is a bit of a hack — we store conversation data in memory.core
|
|
648
|
+
// because that's the easiest way to persist it through the snapshot format
|
|
649
|
+
}
|
|
650
|
+
return { conversations, conversationDetails };
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Extract text content from an OpenAI message.
|
|
654
|
+
*/
|
|
655
|
+
extractMessageContent(msg) {
|
|
656
|
+
const parts = [];
|
|
657
|
+
for (const content of msg.content) {
|
|
658
|
+
if (content.type === 'text' && content.text) {
|
|
659
|
+
parts.push(content.text.value);
|
|
660
|
+
}
|
|
661
|
+
else if (content.type === 'image_file' && content.image_file) {
|
|
662
|
+
parts.push(`[Image: ${content.image_file.file_id}]`);
|
|
663
|
+
}
|
|
664
|
+
else if (content.type === 'image_url' && content.image_url) {
|
|
665
|
+
parts.push(`[Image: ${content.image_url.url}]`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return parts.join('\n');
|
|
669
|
+
}
|
|
670
|
+
// ─── Private: Restore helpers ──────────────────────────────
|
|
671
|
+
/**
|
|
672
|
+
* Create a vector store and add files to it.
|
|
673
|
+
*/
|
|
674
|
+
async createVectorStore(name, fileIds, metadata, expiresAfter) {
|
|
675
|
+
const body = {
|
|
676
|
+
name,
|
|
677
|
+
metadata,
|
|
678
|
+
file_ids: fileIds,
|
|
679
|
+
};
|
|
680
|
+
if (expiresAfter) {
|
|
681
|
+
body.expires_after = expiresAfter;
|
|
682
|
+
}
|
|
683
|
+
const vs = await this.apiPost('/vector_stores', body);
|
|
684
|
+
return vs.id;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Recreate a thread with messages.
|
|
688
|
+
*/
|
|
689
|
+
async recreateThread(messages) {
|
|
690
|
+
// Create thread with initial messages
|
|
691
|
+
// The API supports creating a thread with messages in one call
|
|
692
|
+
const threadMessages = messages
|
|
693
|
+
.filter(m => m.role === 'user' || m.role === 'assistant')
|
|
694
|
+
.map(m => ({
|
|
695
|
+
role: m.role,
|
|
696
|
+
content: m.content,
|
|
697
|
+
}));
|
|
698
|
+
const thread = await this.apiPost('/threads', { messages: threadMessages });
|
|
699
|
+
return thread.id;
|
|
700
|
+
}
|
|
701
|
+
// ─── Private: Thread cache ─────────────────────────────────
|
|
702
|
+
/**
|
|
703
|
+
* Load thread IDs from local cache.
|
|
704
|
+
*/
|
|
705
|
+
async loadThreadCache(assistantId) {
|
|
706
|
+
const cachePath = join(process.cwd(), THREADS_CACHE_DIR, THREADS_CACHE_FILE);
|
|
707
|
+
if (!existsSync(cachePath))
|
|
708
|
+
return [];
|
|
709
|
+
try {
|
|
710
|
+
const raw = await readFile(cachePath, 'utf-8');
|
|
711
|
+
const cache = JSON.parse(raw);
|
|
712
|
+
// Support both single-assistant and multi-assistant cache formats
|
|
713
|
+
if (Array.isArray(cache)) {
|
|
714
|
+
const entry = cache.find(c => c.assistant_id === assistantId);
|
|
715
|
+
return entry?.thread_ids ?? [];
|
|
716
|
+
}
|
|
717
|
+
// Single entry — use it if it matches the assistant or if no assistant ID is set
|
|
718
|
+
if (cache.assistant_id === assistantId || !cache.assistant_id) {
|
|
719
|
+
return cache.thread_ids ?? [];
|
|
720
|
+
}
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
catch (err) {
|
|
724
|
+
this.warn(`Failed to read thread cache: ${err instanceof Error ? err.message : String(err)}`);
|
|
725
|
+
return [];
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Save thread IDs to local cache.
|
|
730
|
+
*/
|
|
731
|
+
async saveThreadCache(assistantId, threadIds) {
|
|
732
|
+
const cacheDir = join(process.cwd(), THREADS_CACHE_DIR);
|
|
733
|
+
const cachePath = join(cacheDir, THREADS_CACHE_FILE);
|
|
734
|
+
try {
|
|
735
|
+
await mkdir(cacheDir, { recursive: true });
|
|
736
|
+
let caches = [];
|
|
737
|
+
// Load existing cache
|
|
738
|
+
if (existsSync(cachePath)) {
|
|
739
|
+
try {
|
|
740
|
+
const raw = await readFile(cachePath, 'utf-8');
|
|
741
|
+
const parsed = JSON.parse(raw);
|
|
742
|
+
if (Array.isArray(parsed)) {
|
|
743
|
+
caches = parsed;
|
|
744
|
+
}
|
|
745
|
+
else if (parsed.assistant_id) {
|
|
746
|
+
caches = [parsed];
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
// Start fresh
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
// Upsert entry for this assistant
|
|
754
|
+
const existingIdx = caches.findIndex(c => c.assistant_id === assistantId);
|
|
755
|
+
const entry = {
|
|
756
|
+
assistant_id: assistantId,
|
|
757
|
+
thread_ids: [...new Set([
|
|
758
|
+
...(existingIdx >= 0 ? caches[existingIdx].thread_ids : []),
|
|
759
|
+
...threadIds,
|
|
760
|
+
])],
|
|
761
|
+
updated_at: new Date().toISOString(),
|
|
762
|
+
};
|
|
763
|
+
if (existingIdx >= 0) {
|
|
764
|
+
caches[existingIdx] = entry;
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
caches.push(entry);
|
|
768
|
+
}
|
|
769
|
+
await writeFile(cachePath, JSON.stringify(caches, null, 2) + '\n', 'utf-8');
|
|
770
|
+
this.log(` Saved ${threadIds.length} thread ID(s) to ${THREADS_CACHE_DIR}/${THREADS_CACHE_FILE}`);
|
|
771
|
+
}
|
|
772
|
+
catch (err) {
|
|
773
|
+
this.warn(`Failed to save thread cache: ${err instanceof Error ? err.message : String(err)}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// ─── Private: Utilities ────────────────────────────────────
|
|
777
|
+
log(msg) {
|
|
778
|
+
console.error(msg);
|
|
779
|
+
}
|
|
780
|
+
warn(msg) {
|
|
781
|
+
this.warnings.push(msg);
|
|
782
|
+
}
|
|
783
|
+
sleep(ms) {
|
|
784
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
785
|
+
}
|
|
786
|
+
formatBytes(bytes) {
|
|
787
|
+
if (bytes < 1024)
|
|
788
|
+
return `${bytes}B`;
|
|
789
|
+
if (bytes < 1024 * 1024)
|
|
790
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
791
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
792
|
+
}
|
|
793
|
+
guessMimeType(filename) {
|
|
794
|
+
const ext = filename.split('.').pop()?.toLowerCase() ?? '';
|
|
795
|
+
const map = {
|
|
796
|
+
pdf: 'application/pdf',
|
|
797
|
+
txt: 'text/plain',
|
|
798
|
+
md: 'text/markdown',
|
|
799
|
+
json: 'application/json',
|
|
800
|
+
csv: 'text/csv',
|
|
801
|
+
tsv: 'text/tab-separated-values',
|
|
802
|
+
html: 'text/html',
|
|
803
|
+
htm: 'text/html',
|
|
804
|
+
xml: 'application/xml',
|
|
805
|
+
js: 'application/javascript',
|
|
806
|
+
ts: 'text/typescript',
|
|
807
|
+
py: 'text/x-python',
|
|
808
|
+
rb: 'text/x-ruby',
|
|
809
|
+
c: 'text/x-c',
|
|
810
|
+
cpp: 'text/x-c++src',
|
|
811
|
+
h: 'text/x-c',
|
|
812
|
+
java: 'text/x-java',
|
|
813
|
+
rs: 'text/x-rust',
|
|
814
|
+
go: 'text/x-go',
|
|
815
|
+
sh: 'application/x-sh',
|
|
816
|
+
yaml: 'application/x-yaml',
|
|
817
|
+
yml: 'application/x-yaml',
|
|
818
|
+
toml: 'application/toml',
|
|
819
|
+
png: 'image/png',
|
|
820
|
+
jpg: 'image/jpeg',
|
|
821
|
+
jpeg: 'image/jpeg',
|
|
822
|
+
gif: 'image/gif',
|
|
823
|
+
webp: 'image/webp',
|
|
824
|
+
svg: 'image/svg+xml',
|
|
825
|
+
mp3: 'audio/mpeg',
|
|
826
|
+
wav: 'audio/wav',
|
|
827
|
+
mp4: 'video/mp4',
|
|
828
|
+
zip: 'application/zip',
|
|
829
|
+
tar: 'application/x-tar',
|
|
830
|
+
gz: 'application/gzip',
|
|
831
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
832
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
833
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
834
|
+
};
|
|
835
|
+
return map[ext] ?? 'application/octet-stream';
|
|
836
|
+
}
|
|
113
837
|
}
|
|
114
838
|
//# sourceMappingURL=openai-assistants.js.map
|