agentmemory-cli 1.0.0 → 1.3.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 (75) hide show
  1. package/dist/commands/connect.d.ts +11 -0
  2. package/dist/commands/connect.d.ts.map +1 -0
  3. package/dist/commands/connect.js +232 -0
  4. package/dist/commands/connect.js.map +1 -0
  5. package/dist/commands/delete.d.ts.map +1 -1
  6. package/dist/commands/delete.js +3 -0
  7. package/dist/commands/delete.js.map +1 -1
  8. package/dist/commands/download.d.ts +5 -0
  9. package/dist/commands/download.d.ts.map +1 -0
  10. package/dist/commands/download.js +82 -0
  11. package/dist/commands/download.js.map +1 -0
  12. package/dist/commands/export.d.ts.map +1 -1
  13. package/dist/commands/export.js +3 -0
  14. package/dist/commands/export.js.map +1 -1
  15. package/dist/commands/files.d.ts +6 -0
  16. package/dist/commands/files.d.ts.map +1 -0
  17. package/dist/commands/files.js +101 -0
  18. package/dist/commands/files.js.map +1 -0
  19. package/dist/commands/heartbeat.d.ts +65 -0
  20. package/dist/commands/heartbeat.d.ts.map +1 -0
  21. package/dist/commands/heartbeat.js +176 -0
  22. package/dist/commands/heartbeat.js.map +1 -0
  23. package/dist/commands/import.d.ts.map +1 -1
  24. package/dist/commands/import.js +3 -0
  25. package/dist/commands/import.js.map +1 -1
  26. package/dist/commands/init.d.ts.map +1 -1
  27. package/dist/commands/init.js +39 -1
  28. package/dist/commands/init.js.map +1 -1
  29. package/dist/commands/list.d.ts.map +1 -1
  30. package/dist/commands/list.js +3 -0
  31. package/dist/commands/list.js.map +1 -1
  32. package/dist/commands/search.d.ts.map +1 -1
  33. package/dist/commands/search.js +3 -0
  34. package/dist/commands/search.js.map +1 -1
  35. package/dist/commands/secret.d.ts +25 -0
  36. package/dist/commands/secret.d.ts.map +1 -0
  37. package/dist/commands/secret.js +390 -0
  38. package/dist/commands/secret.js.map +1 -0
  39. package/dist/commands/store.d.ts.map +1 -1
  40. package/dist/commands/store.js +3 -0
  41. package/dist/commands/store.js.map +1 -1
  42. package/dist/commands/upload.d.ts +4 -0
  43. package/dist/commands/upload.d.ts.map +1 -0
  44. package/dist/commands/upload.js +110 -0
  45. package/dist/commands/upload.js.map +1 -0
  46. package/dist/index.js +136 -2
  47. package/dist/index.js.map +1 -1
  48. package/dist/lib/api.d.ts +23 -1
  49. package/dist/lib/api.d.ts.map +1 -1
  50. package/dist/lib/api.js +49 -0
  51. package/dist/lib/api.js.map +1 -1
  52. package/dist/lib/autosync.d.ts +14 -0
  53. package/dist/lib/autosync.d.ts.map +1 -0
  54. package/dist/lib/autosync.js +165 -0
  55. package/dist/lib/autosync.js.map +1 -0
  56. package/dist/types.d.ts +59 -0
  57. package/dist/types.d.ts.map +1 -1
  58. package/package.json +2 -1
  59. package/src/commands/connect.ts +216 -0
  60. package/src/commands/delete.ts +4 -0
  61. package/src/commands/download.ts +105 -0
  62. package/src/commands/export.ts +4 -0
  63. package/src/commands/files.ts +119 -0
  64. package/src/commands/heartbeat.ts +241 -0
  65. package/src/commands/import.ts +4 -0
  66. package/src/commands/init.ts +44 -1
  67. package/src/commands/list.ts +4 -0
  68. package/src/commands/search.ts +4 -0
  69. package/src/commands/secret.ts +438 -0
  70. package/src/commands/store.ts +4 -0
  71. package/src/commands/upload.ts +117 -0
  72. package/src/index.ts +158 -2
  73. package/src/lib/api.ts +86 -1
  74. package/src/lib/autosync.ts +160 -0
  75. package/src/types.ts +67 -0
@@ -0,0 +1,438 @@
1
+ import chalk from 'chalk';
2
+ import * as fs from 'fs';
3
+ import { createInterface } from 'readline';
4
+ import { getApiKey } from '../lib/config.js';
5
+ import { listSecrets, getSecret, setSecret, deleteSecret, getAllSecrets } from '../lib/api.js';
6
+ import { autoSync } from '../lib/autosync.js';
7
+ import type { SecretType, SecretListItem } from '../types.js';
8
+
9
+ // Prompt helper
10
+ async function prompt(question: string): Promise<string> {
11
+ const rl = createInterface({
12
+ input: process.stdin,
13
+ output: process.stdout,
14
+ });
15
+
16
+ return new Promise((resolve) => {
17
+ rl.question(question, (answer) => {
18
+ rl.close();
19
+ resolve(answer.trim());
20
+ });
21
+ });
22
+ }
23
+
24
+ async function confirm(question: string): Promise<boolean> {
25
+ const answer = await prompt(question);
26
+ return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
27
+ }
28
+
29
+ async function promptPassword(question: string): Promise<string> {
30
+ // Simple password prompt (in production, use a library that masks input)
31
+ return prompt(question);
32
+ }
33
+
34
+ const VALID_TYPES: SecretType[] = ['api_key', 'credential', 'connection_string', 'env_var', 'generic'];
35
+
36
+ // Helper to mask secret values
37
+ function maskValue(value: string): string {
38
+ if (value.length <= 8) {
39
+ return '*'.repeat(value.length);
40
+ }
41
+ return value.slice(0, 4) + '*'.repeat(value.length - 8) + value.slice(-4);
42
+ }
43
+
44
+ // Format secret type with icon
45
+ function formatType(type: SecretType): string {
46
+ const icons: Record<SecretType, string> = {
47
+ api_key: '🔑',
48
+ credential: '🔐',
49
+ connection_string: '🔗',
50
+ env_var: '📦',
51
+ generic: '📄',
52
+ };
53
+ return `${icons[type] || '📄'} ${type}`;
54
+ }
55
+
56
+ // Set a secret
57
+ export async function secretSetCommand(
58
+ name: string,
59
+ value: string,
60
+ options: {
61
+ type?: string;
62
+ description?: string;
63
+ json?: boolean;
64
+ }
65
+ ): Promise<void> {
66
+ const apiKey = getApiKey();
67
+
68
+ // Auto-sync in background (silent)
69
+ autoSync();
70
+
71
+ if (!apiKey) {
72
+ console.error(chalk.red('Error: Not configured. Run "agentmemory init" first.'));
73
+ process.exit(1);
74
+ }
75
+
76
+ // Validate type
77
+ const type = (options.type || 'generic') as SecretType;
78
+ if (!VALID_TYPES.includes(type)) {
79
+ console.error(chalk.red(`Error: Invalid type. Must be one of: ${VALID_TYPES.join(', ')}`));
80
+ process.exit(1);
81
+ }
82
+
83
+ try {
84
+ const response = await setSecret(name, value, type, options.description);
85
+
86
+ if (options.json) {
87
+ console.log(JSON.stringify(response, null, 2));
88
+ } else {
89
+ const action = response.message?.includes('updated') ? 'updated' : 'created';
90
+ console.log(chalk.green(`✓ Secret "${name}" ${action} successfully`));
91
+ console.log(chalk.gray(` Type: ${formatType(type)}`));
92
+ if (options.description) {
93
+ console.log(chalk.gray(` Description: ${options.description}`));
94
+ }
95
+ }
96
+ } catch (error) {
97
+ if (options.json) {
98
+ console.log(JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }));
99
+ } else {
100
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
101
+ }
102
+ process.exit(1);
103
+ }
104
+ }
105
+
106
+ // Get a secret value
107
+ export async function secretGetCommand(
108
+ name: string,
109
+ options: {
110
+ show?: boolean;
111
+ json?: boolean;
112
+ }
113
+ ): Promise<void> {
114
+ const apiKey = getApiKey();
115
+
116
+ // Auto-sync in background (silent)
117
+ autoSync();
118
+
119
+ if (!apiKey) {
120
+ console.error(chalk.red('Error: Not configured. Run "agentmemory init" first.'));
121
+ process.exit(1);
122
+ }
123
+
124
+ try {
125
+ const secret = await getSecret(name);
126
+
127
+ if (options.json) {
128
+ // In JSON mode, always show full value
129
+ console.log(JSON.stringify(secret, null, 2));
130
+ } else {
131
+ console.log(chalk.cyan(`Secret: ${secret.name}`));
132
+ console.log(chalk.gray(`Type: ${formatType(secret.type)}`));
133
+ if (secret.description) {
134
+ console.log(chalk.gray(`Description: ${secret.description}`));
135
+ }
136
+ console.log();
137
+
138
+ // Show value (masked by default)
139
+ if (options.show) {
140
+ console.log(chalk.yellow('Value:'), secret.value);
141
+ } else {
142
+ console.log(chalk.yellow('Value:'), maskValue(secret.value || ''));
143
+ console.log(chalk.gray(' (use --show to reveal full value)'));
144
+ }
145
+ }
146
+ } catch (error) {
147
+ if (options.json) {
148
+ console.log(JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }));
149
+ } else {
150
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
151
+ }
152
+ process.exit(1);
153
+ }
154
+ }
155
+
156
+ // List all secrets
157
+ export async function secretListCommand(
158
+ options: {
159
+ type?: string;
160
+ json?: boolean;
161
+ }
162
+ ): Promise<void> {
163
+ const apiKey = getApiKey();
164
+
165
+ // Auto-sync in background (silent)
166
+ autoSync();
167
+
168
+ if (!apiKey) {
169
+ console.error(chalk.red('Error: Not configured. Run "agentmemory init" first.'));
170
+ process.exit(1);
171
+ }
172
+
173
+ try {
174
+ const type = options.type as SecretType | undefined;
175
+ if (type && !VALID_TYPES.includes(type)) {
176
+ console.error(chalk.red(`Error: Invalid type. Must be one of: ${VALID_TYPES.join(', ')}`));
177
+ process.exit(1);
178
+ }
179
+
180
+ const response = await listSecrets(500, type);
181
+
182
+ if (options.json) {
183
+ console.log(JSON.stringify(response, null, 2));
184
+ } else {
185
+ if (response.secrets.length === 0) {
186
+ console.log(chalk.yellow('No secrets found.'));
187
+ return;
188
+ }
189
+
190
+ console.log(chalk.cyan(`\nSecrets (${response.total} total):\n`));
191
+
192
+ for (const secret of response.secrets) {
193
+ console.log(` ${chalk.white(secret.name)}`);
194
+ console.log(` ${formatType(secret.type)}`);
195
+ if (secret.description) {
196
+ console.log(` ${chalk.gray(secret.description)}`);
197
+ }
198
+ console.log();
199
+ }
200
+ }
201
+ } catch (error) {
202
+ if (options.json) {
203
+ console.log(JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }));
204
+ } else {
205
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
206
+ }
207
+ process.exit(1);
208
+ }
209
+ }
210
+
211
+ // Delete a secret
212
+ export async function secretDeleteCommand(
213
+ name: string,
214
+ options: {
215
+ force?: boolean;
216
+ json?: boolean;
217
+ }
218
+ ): Promise<void> {
219
+ const apiKey = getApiKey();
220
+
221
+ // Auto-sync in background (silent)
222
+ autoSync();
223
+
224
+ if (!apiKey) {
225
+ console.error(chalk.red('Error: Not configured. Run "agentmemory init" first.'));
226
+ process.exit(1);
227
+ }
228
+
229
+ // Confirmation (unless --force)
230
+ if (!options.force && !options.json) {
231
+ const confirmed = await confirm(`Are you sure you want to delete secret "${name}"? (y/N): `);
232
+
233
+ if (!confirmed) {
234
+ console.log(chalk.yellow('Cancelled.'));
235
+ return;
236
+ }
237
+ }
238
+
239
+ try {
240
+ await deleteSecret(name);
241
+
242
+ if (options.json) {
243
+ console.log(JSON.stringify({ success: true, deleted: name }));
244
+ } else {
245
+ console.log(chalk.green(`✓ Secret "${name}" deleted successfully`));
246
+ }
247
+ } catch (error) {
248
+ if (options.json) {
249
+ console.log(JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }));
250
+ } else {
251
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
252
+ }
253
+ process.exit(1);
254
+ }
255
+ }
256
+
257
+ // Export secrets to file (encrypted with password)
258
+ export async function secretsExportCommand(
259
+ options: {
260
+ output?: string;
261
+ json?: boolean;
262
+ }
263
+ ): Promise<void> {
264
+ const apiKey = getApiKey();
265
+
266
+ // Auto-sync in background (silent)
267
+ autoSync();
268
+
269
+ if (!apiKey) {
270
+ console.error(chalk.red('Error: Not configured. Run "agentmemory init" first.'));
271
+ process.exit(1);
272
+ }
273
+
274
+ try {
275
+ // Fetch all secrets with values
276
+ const secretsList = await getAllSecrets();
277
+
278
+ // Get full values for each secret
279
+ const secretsWithValues: Array<{
280
+ name: string;
281
+ value: string;
282
+ type: string;
283
+ description?: string;
284
+ }> = [];
285
+
286
+ for (const secretItem of secretsList) {
287
+ const fullSecret = await getSecret(secretItem.name);
288
+ secretsWithValues.push({
289
+ name: fullSecret.name,
290
+ value: fullSecret.value || '',
291
+ type: fullSecret.type,
292
+ description: fullSecret.description,
293
+ });
294
+ }
295
+
296
+ // Ask for password for encryption
297
+ const password = await promptPassword('Enter password to encrypt secrets (min 8 chars): ');
298
+
299
+ if (!password || password.length < 8) {
300
+ console.error(chalk.red('Error: Password must be at least 8 characters'));
301
+ process.exit(1);
302
+ }
303
+
304
+ // Simple XOR encryption (for demonstration - in production use proper crypto)
305
+ const exportData = {
306
+ version: 1,
307
+ encrypted: true,
308
+ secrets: secretsWithValues.map(s => ({
309
+ ...s,
310
+ value: simpleEncrypt(s.value, password),
311
+ })),
312
+ exported_at: new Date().toISOString(),
313
+ };
314
+
315
+ const outputPath = options.output || 'secrets-export.json';
316
+ fs.writeFileSync(outputPath, JSON.stringify(exportData, null, 2));
317
+
318
+ if (options.json) {
319
+ console.log(JSON.stringify({ success: true, file: outputPath, count: secretsWithValues.length }));
320
+ } else {
321
+ console.log(chalk.green(`✓ Exported ${secretsWithValues.length} secrets to ${outputPath}`));
322
+ console.log(chalk.yellow('⚠ Keep this file secure! It contains encrypted secrets.'));
323
+ }
324
+ } catch (error) {
325
+ if (options.json) {
326
+ console.log(JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }));
327
+ } else {
328
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
329
+ }
330
+ process.exit(1);
331
+ }
332
+ }
333
+
334
+ // Import secrets from file
335
+ export async function secretsImportCommand(
336
+ file: string,
337
+ options: {
338
+ json?: boolean;
339
+ }
340
+ ): Promise<void> {
341
+ const apiKey = getApiKey();
342
+
343
+ // Auto-sync in background (silent)
344
+ autoSync();
345
+
346
+ if (!apiKey) {
347
+ console.error(chalk.red('Error: Not configured. Run "agentmemory init" first.'));
348
+ process.exit(1);
349
+ }
350
+
351
+ if (!fs.existsSync(file)) {
352
+ console.error(chalk.red(`Error: File not found: ${file}`));
353
+ process.exit(1);
354
+ }
355
+
356
+ try {
357
+ const content = fs.readFileSync(file, 'utf-8');
358
+ const data = JSON.parse(content) as {
359
+ version: number;
360
+ encrypted: boolean;
361
+ secrets: Array<{
362
+ name: string;
363
+ value: string;
364
+ type: string;
365
+ description?: string;
366
+ }>;
367
+ };
368
+
369
+ let secrets = data.secrets;
370
+
371
+ // Decrypt if encrypted
372
+ if (data.encrypted) {
373
+ const password = await promptPassword('Enter password to decrypt secrets: ');
374
+
375
+ secrets = secrets.map(s => ({
376
+ ...s,
377
+ value: simpleDecrypt(s.value, password),
378
+ }));
379
+ }
380
+
381
+ // Import secrets
382
+ let imported = 0;
383
+ let errors = 0;
384
+
385
+ for (const secret of secrets) {
386
+ try {
387
+ await setSecret(
388
+ secret.name,
389
+ secret.value,
390
+ secret.type as SecretType,
391
+ secret.description
392
+ );
393
+ imported++;
394
+ if (!options.json) {
395
+ console.log(chalk.green(`✓ Imported: ${secret.name}`));
396
+ }
397
+ } catch (err) {
398
+ errors++;
399
+ if (!options.json) {
400
+ console.log(chalk.red(`✗ Failed: ${secret.name} - ${err instanceof Error ? err.message : 'Unknown error'}`));
401
+ }
402
+ }
403
+ }
404
+
405
+ if (options.json) {
406
+ console.log(JSON.stringify({ success: true, imported, errors, total: secrets.length }));
407
+ } else {
408
+ console.log();
409
+ console.log(chalk.cyan(`Import complete: ${imported} imported, ${errors} errors`));
410
+ }
411
+ } catch (error) {
412
+ if (options.json) {
413
+ console.log(JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }));
414
+ } else {
415
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
416
+ }
417
+ process.exit(1);
418
+ }
419
+ }
420
+
421
+ // Simple XOR-based encryption (for demonstration)
422
+ // In production, use Node.js crypto module with AES-256-GCM
423
+ function simpleEncrypt(text: string, password: string): string {
424
+ const result: number[] = [];
425
+ for (let i = 0; i < text.length; i++) {
426
+ result.push(text.charCodeAt(i) ^ password.charCodeAt(i % password.length));
427
+ }
428
+ return Buffer.from(result).toString('base64');
429
+ }
430
+
431
+ function simpleDecrypt(encoded: string, password: string): string {
432
+ const data = Buffer.from(encoded, 'base64');
433
+ const result: string[] = [];
434
+ for (let i = 0; i < data.length; i++) {
435
+ result.push(String.fromCharCode(data[i] ^ password.charCodeAt(i % password.length)));
436
+ }
437
+ return result.join('');
438
+ }
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { storeMemory } from '../lib/api.js';
3
3
  import { isConfigured } from '../lib/config.js';
4
+ import { autoSync } from '../lib/autosync.js';
4
5
 
5
6
  interface StoreOptions {
6
7
  category?: string;
@@ -17,6 +18,9 @@ export async function storeCommand(
17
18
  process.exit(1);
18
19
  }
19
20
 
21
+ // Auto-sync in background (silent)
22
+ autoSync();
23
+
20
24
  try {
21
25
  // Build metadata
22
26
  let metadata: Record<string, unknown> = {};
@@ -0,0 +1,117 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { getApiKey, getApiUrl } from '../lib/config.js';
5
+ import { autoSync } from '../lib/autosync.js';
6
+
7
+ export async function uploadCommand(
8
+ filePath: string,
9
+ options: { description?: string }
10
+ ): Promise<void> {
11
+ const apiKey = getApiKey();
12
+ if (!apiKey) {
13
+ console.error(chalk.red('Not configured. Run: agentmemory init'));
14
+ process.exit(1);
15
+ }
16
+
17
+ // Auto-sync in background (silent)
18
+ autoSync();
19
+
20
+ // Check if file exists
21
+ if (!fs.existsSync(filePath)) {
22
+ console.error(chalk.red(`File not found: ${filePath}`));
23
+ process.exit(1);
24
+ }
25
+
26
+ const absolutePath = path.resolve(filePath);
27
+ const fileName = path.basename(absolutePath);
28
+ const fileBuffer = fs.readFileSync(absolutePath);
29
+ const fileSize = fileBuffer.length;
30
+
31
+ // Check file size (100MB limit)
32
+ if (fileSize > 100 * 1024 * 1024) {
33
+ console.error(chalk.red('File too large. Maximum size is 100MB.'));
34
+ process.exit(1);
35
+ }
36
+
37
+ console.log(chalk.blue(`Uploading ${fileName} (${formatSize(fileSize)})...`));
38
+
39
+ try {
40
+ // Create FormData
41
+ const FormData = (await import('form-data')).default;
42
+ const form = new FormData();
43
+ form.append('file', fileBuffer, {
44
+ filename: fileName,
45
+ contentType: getMimeType(fileName),
46
+ });
47
+ if (options.description) {
48
+ form.append('description', options.description);
49
+ }
50
+
51
+ const apiUrl = getApiUrl();
52
+
53
+ // Use node-fetch with form-data
54
+ const nodeFetch = (await import('node-fetch')).default;
55
+ const response = await nodeFetch(`${apiUrl}/files`, {
56
+ method: 'POST',
57
+ headers: {
58
+ 'Authorization': `Bearer ${apiKey}`,
59
+ ...form.getHeaders(),
60
+ },
61
+ body: form,
62
+ }) as unknown as Response;
63
+
64
+ const data = await response.json() as { success?: boolean; memory?: { id: string; file_name: string }; error?: string };
65
+
66
+ if (!response.ok) {
67
+ console.error(chalk.red(`Error: ${data.error || 'Upload failed'}`));
68
+ process.exit(1);
69
+ }
70
+
71
+ console.log(chalk.green('✓ File uploaded successfully!'));
72
+ console.log(chalk.gray(` ID: ${data.memory?.id}`));
73
+ console.log(chalk.gray(` Name: ${data.memory?.file_name}`));
74
+ } catch (error) {
75
+ console.error(chalk.red(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`));
76
+ process.exit(1);
77
+ }
78
+ }
79
+
80
+ function formatSize(bytes: number): string {
81
+ if (bytes < 1024) return `${bytes} B`;
82
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
83
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
84
+ }
85
+
86
+ function getMimeType(fileName: string): string {
87
+ const ext = path.extname(fileName).toLowerCase();
88
+ const mimeTypes: Record<string, string> = {
89
+ '.txt': 'text/plain',
90
+ '.md': 'text/markdown',
91
+ '.json': 'application/json',
92
+ '.pdf': 'application/pdf',
93
+ '.jpg': 'image/jpeg',
94
+ '.jpeg': 'image/jpeg',
95
+ '.png': 'image/png',
96
+ '.gif': 'image/gif',
97
+ '.webp': 'image/webp',
98
+ '.svg': 'image/svg+xml',
99
+ '.mp3': 'audio/mpeg',
100
+ '.wav': 'audio/wav',
101
+ '.mp4': 'video/mp4',
102
+ '.webm': 'video/webm',
103
+ '.doc': 'application/msword',
104
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
105
+ '.xls': 'application/vnd.ms-excel',
106
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
107
+ '.zip': 'application/zip',
108
+ '.js': 'text/javascript',
109
+ '.ts': 'text/typescript',
110
+ '.py': 'text/x-python',
111
+ '.html': 'text/html',
112
+ '.css': 'text/css',
113
+ '.yaml': 'text/yaml',
114
+ '.yml': 'text/yaml',
115
+ };
116
+ return mimeTypes[ext] || 'application/octet-stream';
117
+ }