@ottocode/server 0.1.265 → 0.1.267

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 (74) hide show
  1. package/package.json +3 -3
  2. package/src/routes/auth/copilot.ts +699 -0
  3. package/src/routes/auth/oauth.ts +578 -0
  4. package/src/routes/auth/onboarding.ts +45 -0
  5. package/src/routes/auth/providers.ts +189 -0
  6. package/src/routes/auth/service.ts +167 -0
  7. package/src/routes/auth/state.ts +23 -0
  8. package/src/routes/auth/status.ts +203 -0
  9. package/src/routes/auth/wallet.ts +229 -0
  10. package/src/routes/auth.ts +12 -2080
  11. package/src/routes/config/models-service.ts +411 -0
  12. package/src/routes/config/models.ts +6 -426
  13. package/src/routes/config/providers-service.ts +237 -0
  14. package/src/routes/config/providers.ts +10 -242
  15. package/src/routes/files/handlers.ts +297 -0
  16. package/src/routes/files/service.ts +313 -0
  17. package/src/routes/files.ts +12 -608
  18. package/src/routes/git/commit-service.ts +207 -0
  19. package/src/routes/git/commit.ts +6 -220
  20. package/src/routes/git/remote-service.ts +116 -0
  21. package/src/routes/git/remote.ts +8 -115
  22. package/src/routes/git/staging-service.ts +111 -0
  23. package/src/routes/git/staging.ts +10 -205
  24. package/src/routes/mcp/auth.ts +338 -0
  25. package/src/routes/mcp/lifecycle.ts +263 -0
  26. package/src/routes/mcp/servers.ts +212 -0
  27. package/src/routes/mcp/service.ts +664 -0
  28. package/src/routes/mcp/state.ts +13 -0
  29. package/src/routes/mcp.ts +6 -1233
  30. package/src/routes/ottorouter/billing.ts +593 -0
  31. package/src/routes/ottorouter/service.ts +92 -0
  32. package/src/routes/ottorouter/topup.ts +301 -0
  33. package/src/routes/ottorouter/wallet.ts +370 -0
  34. package/src/routes/ottorouter.ts +6 -1319
  35. package/src/routes/research/service.ts +339 -0
  36. package/src/routes/research.ts +12 -390
  37. package/src/routes/sessions/crud.ts +563 -0
  38. package/src/routes/sessions/queue.ts +242 -0
  39. package/src/routes/sessions/retry.ts +121 -0
  40. package/src/routes/sessions/service.ts +768 -0
  41. package/src/routes/sessions/share.ts +434 -0
  42. package/src/routes/sessions.ts +8 -1977
  43. package/src/routes/skills/service.ts +221 -0
  44. package/src/routes/skills/spec.ts +309 -0
  45. package/src/routes/skills.ts +31 -909
  46. package/src/routes/terminals/service.ts +326 -0
  47. package/src/routes/terminals.ts +19 -295
  48. package/src/routes/tunnel/service.ts +217 -0
  49. package/src/routes/tunnel.ts +29 -219
  50. package/src/runtime/agent/registry-prompts.ts +147 -0
  51. package/src/runtime/agent/registry.ts +6 -124
  52. package/src/runtime/agent/runner-errors.ts +116 -0
  53. package/src/runtime/agent/runner-reminders.ts +45 -0
  54. package/src/runtime/agent/runner-setup-model.ts +75 -0
  55. package/src/runtime/agent/runner-setup-prompt.ts +185 -0
  56. package/src/runtime/agent/runner-setup-tools.ts +103 -0
  57. package/src/runtime/agent/runner-setup-utils.ts +21 -0
  58. package/src/runtime/agent/runner-setup.ts +54 -288
  59. package/src/runtime/agent/runner-telemetry.ts +112 -0
  60. package/src/runtime/agent/runner-text.ts +108 -0
  61. package/src/runtime/agent/runner-tool-observer.ts +86 -0
  62. package/src/runtime/agent/runner.ts +79 -378
  63. package/src/runtime/prompt/builder.ts +5 -1
  64. package/src/runtime/prompt/capabilities.ts +13 -8
  65. package/src/runtime/provider/custom.ts +73 -0
  66. package/src/runtime/provider/index.ts +2 -85
  67. package/src/runtime/provider/reasoning-builders.ts +280 -0
  68. package/src/runtime/provider/reasoning.ts +67 -264
  69. package/src/tools/adapter/events.ts +116 -0
  70. package/src/tools/adapter/execution.ts +160 -0
  71. package/src/tools/adapter/pending.ts +37 -0
  72. package/src/tools/adapter/persistence.ts +166 -0
  73. package/src/tools/adapter/results.ts +97 -0
  74. package/src/tools/adapter.ts +124 -451
@@ -1,57 +1,11 @@
1
1
  import type { Hono } from 'hono';
2
- import {
3
- loadConfig,
4
- removeProviderSettings,
5
- writeProviderSettings,
6
- discoverOllamaModels,
7
- isBuiltInProviderId,
8
- type ModelInfo,
9
- type ProviderCompatibility,
10
- type ProviderPromptFamily,
11
- type ProviderId,
12
- type ProviderSettingsEntry,
13
- } from '@ottocode/sdk';
14
- import type { EmbeddedAppConfig } from '../../index.ts';
15
- import { logger } from '@ottocode/sdk';
16
- import { serializeError } from '../../runtime/errors/api-error.ts';
17
- import {
18
- getAuthorizedProviders,
19
- getDefault,
20
- getProviderDetails,
21
- } from './utils.ts';
22
2
  import { openApiRoute } from '../../openapi/route.ts';
23
-
24
- type ProviderMutationBody = {
25
- enabled?: boolean;
26
- custom?: boolean;
27
- label?: string;
28
- compatibility?: ProviderCompatibility;
29
- family?: ProviderPromptFamily;
30
- baseURL?: string | null;
31
- apiKey?: string | null;
32
- apiKeyEnv?: string | null;
33
- models?: string[];
34
- allowAnyModel?: boolean;
35
- };
36
-
37
- type ProviderDiscoveryBody = {
38
- compatibility?: ProviderCompatibility;
39
- baseURL?: string;
40
- apiKey?: string;
41
- };
42
-
43
- function toDiscoveredModel(model: ModelInfo) {
44
- return {
45
- id: model.id,
46
- label: model.label || model.id,
47
- toolCall: model.toolCall,
48
- reasoningText: model.reasoningText,
49
- vision: model.modalities?.input?.includes('image') ?? false,
50
- attachment: model.attachment ?? false,
51
- contextWindow: model.limit?.context,
52
- maxOutputTokens: model.limit?.output,
53
- };
54
- }
3
+ import {
4
+ handleDeleteProviderSettings,
5
+ handleDiscoverProviderModels,
6
+ handleGetProviders,
7
+ handleUpdateProviderSettings,
8
+ } from './providers-service.ts';
55
9
 
56
10
  export function registerProvidersRoute(app: Hono) {
57
11
  openApiRoute(
@@ -106,62 +60,7 @@ export function registerProvidersRoute(app: Hono) {
106
60
  },
107
61
  },
108
62
  },
109
- async (c) => {
110
- try {
111
- const embeddedConfig = (
112
- c as unknown as {
113
- get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
114
- }
115
- ).get('embeddedConfig');
116
-
117
- if (embeddedConfig && Object.keys(embeddedConfig).length > 0) {
118
- const providers = embeddedConfig.auth
119
- ? (Object.keys(embeddedConfig.auth) as ProviderId[])
120
- : embeddedConfig.provider
121
- ? [embeddedConfig.provider]
122
- : [];
123
-
124
- return c.json({
125
- providers,
126
- details: providers.map((provider) => ({
127
- id: provider,
128
- label: provider,
129
- source: 'built-in',
130
- enabled: true,
131
- authorized: true,
132
- custom: false,
133
- hasApiKey: false,
134
- allowAnyModel: false,
135
- modelCount: 0,
136
- })),
137
- default: getDefault(
138
- embeddedConfig.provider,
139
- embeddedConfig.defaults?.provider,
140
- undefined,
141
- ),
142
- });
143
- }
144
-
145
- const projectRoot = c.req.query('project') || process.cwd();
146
- const cfg = await loadConfig(projectRoot);
147
-
148
- const authorizedProviders = await getAuthorizedProviders(
149
- undefined,
150
- cfg,
151
- );
152
- const details = await getProviderDetails(undefined, cfg);
153
-
154
- return c.json({
155
- providers: authorizedProviders,
156
- details,
157
- default: cfg.defaults.provider,
158
- });
159
- } catch (error) {
160
- logger.error('Failed to get providers', error);
161
- const errorResponse = serializeError(error);
162
- return c.json(errorResponse, errorResponse.error.status || 500);
163
- }
164
- },
63
+ handleGetProviders,
165
64
  );
166
65
 
167
66
  openApiRoute(
@@ -237,48 +136,7 @@ export function registerProvidersRoute(app: Hono) {
237
136
  '400': { description: 'Invalid discovery request' },
238
137
  },
239
138
  },
240
- async (c) => {
241
- try {
242
- const embeddedConfig = (
243
- c as unknown as {
244
- get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
245
- }
246
- ).get('embeddedConfig');
247
- if (embeddedConfig && Object.keys(embeddedConfig).length > 0) {
248
- return c.json({ error: 'Embedded config cannot be modified' }, 400);
249
- }
250
-
251
- const body = await c.req.json<ProviderDiscoveryBody>();
252
- const compatibility = body.compatibility || 'openai-compatible';
253
- const baseURL = body.baseURL?.trim();
254
- const apiKey = body.apiKey?.trim() || undefined;
255
- if (!baseURL) return c.json({ error: 'Base URL is required' }, 400);
256
-
257
- if (compatibility !== 'ollama') {
258
- return c.json({
259
- models: [],
260
- unsupported: true,
261
- message:
262
- 'Model discovery is currently available for Ollama providers.',
263
- });
264
- }
265
-
266
- const discovered = await discoverOllamaModels({
267
- baseURL,
268
- apiKey,
269
- includeDetails: true,
270
- });
271
-
272
- return c.json({
273
- baseURL: discovered.baseURL,
274
- models: discovered.models.map(toDiscoveredModel),
275
- });
276
- } catch (error) {
277
- logger.error('Failed to discover provider models', error);
278
- const errorResponse = serializeError(error);
279
- return c.json(errorResponse, errorResponse.error.status || 500);
280
- }
281
- },
139
+ handleDiscoverProviderModels,
282
140
  );
283
141
 
284
142
  openApiRoute(
@@ -385,73 +243,7 @@ export function registerProvidersRoute(app: Hono) {
385
243
  },
386
244
  },
387
245
  },
388
- async (c) => {
389
- try {
390
- const embeddedConfig = (
391
- c as unknown as {
392
- get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
393
- }
394
- ).get('embeddedConfig');
395
- if (embeddedConfig && Object.keys(embeddedConfig).length > 0) {
396
- return c.json({ error: 'Embedded config cannot be modified' }, 400);
397
- }
398
-
399
- const projectRoot = c.req.query('project') || process.cwd();
400
- const provider = c.req.param('provider').trim();
401
- const body = await c.req.json<ProviderMutationBody>();
402
- if (!provider) return c.json({ error: 'Provider is required' }, 400);
403
-
404
- const updates: ProviderSettingsEntry = {
405
- enabled: body.enabled ?? true,
406
- custom: isBuiltInProviderId(provider)
407
- ? body.custom
408
- : (body.custom ?? true),
409
- };
410
-
411
- if (body.label !== undefined)
412
- updates.label = body.label.trim() || undefined;
413
- if (body.compatibility !== undefined) {
414
- updates.compatibility = body.compatibility;
415
- }
416
- if (body.family !== undefined) updates.family = body.family;
417
- if (body.baseURL !== undefined) {
418
- updates.baseURL = body.baseURL?.trim() || undefined;
419
- }
420
- if (body.apiKey !== undefined)
421
- updates.apiKey = body.apiKey?.trim() || undefined;
422
- if (body.apiKeyEnv !== undefined) {
423
- updates.apiKeyEnv = body.apiKeyEnv?.trim() || undefined;
424
- }
425
- if (body.models !== undefined) {
426
- updates.models = body.models
427
- .map((model) => model.trim())
428
- .filter(Boolean);
429
- }
430
- if (body.allowAnyModel !== undefined) {
431
- updates.allowAnyModel = body.allowAnyModel;
432
- }
433
-
434
- if (!isBuiltInProviderId(provider) && !updates.compatibility) {
435
- return c.json(
436
- { error: 'Custom providers require compatibility' },
437
- 400,
438
- );
439
- }
440
-
441
- await writeProviderSettings('global', provider, updates, projectRoot);
442
- const cfg = await loadConfig(projectRoot);
443
- const details = await getProviderDetails(undefined, cfg);
444
- return c.json({
445
- success: true,
446
- provider,
447
- details,
448
- });
449
- } catch (error) {
450
- logger.error('Failed to update provider settings', error);
451
- const errorResponse = serializeError(error);
452
- return c.json(errorResponse, errorResponse.error.status || 500);
453
- }
454
- },
246
+ handleUpdateProviderSettings,
455
247
  );
456
248
 
457
249
  openApiRoute(
@@ -510,30 +302,6 @@ export function registerProvidersRoute(app: Hono) {
510
302
  },
511
303
  },
512
304
  },
513
- async (c) => {
514
- try {
515
- const embeddedConfig = (
516
- c as unknown as {
517
- get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
518
- }
519
- ).get('embeddedConfig');
520
- if (embeddedConfig && Object.keys(embeddedConfig).length > 0) {
521
- return c.json({ error: 'Embedded config cannot be modified' }, 400);
522
- }
523
-
524
- const projectRoot = c.req.query('project') || process.cwd();
525
- const provider = c.req.param('provider').trim();
526
- if (!provider) return c.json({ error: 'Provider is required' }, 400);
527
-
528
- await removeProviderSettings('global', provider, projectRoot);
529
- const cfg = await loadConfig(projectRoot);
530
- const details = await getProviderDetails(undefined, cfg);
531
- return c.json({ success: true, provider, details });
532
- } catch (error) {
533
- logger.error('Failed to remove provider settings', error);
534
- const errorResponse = serializeError(error);
535
- return c.json(errorResponse, errorResponse.error.status || 500);
536
- }
537
- },
305
+ handleDeleteProviderSettings,
538
306
  );
539
307
  }
@@ -0,0 +1,297 @@
1
+ import { logger } from '@ottocode/sdk';
2
+ import type { Context } from 'hono';
3
+ import { readdir, readFile } from 'node:fs/promises';
4
+ import { join, relative, resolve } from 'node:path';
5
+ import { serializeError } from '../../runtime/errors/api-error.ts';
6
+ import {
7
+ TREE_ENTRY_LIMIT,
8
+ clampNumber,
9
+ getChangedFiles,
10
+ getGitIgnoredFiles,
11
+ getSearchPolicy,
12
+ isHomeDirectory,
13
+ listFilesWithRg,
14
+ matchesGitignorePattern,
15
+ parseGitignore,
16
+ shouldExcludeDir,
17
+ shouldExcludeFile,
18
+ traverseDirectory,
19
+ } from './service.ts';
20
+
21
+ async function getSortedFileResult(args: {
22
+ projectRoot: string;
23
+ maxDepth: number;
24
+ limit: number;
25
+ query?: string;
26
+ }) {
27
+ const policy = getSearchPolicy(args.projectRoot);
28
+ let result = await listFilesWithRg(
29
+ args.projectRoot,
30
+ args.maxDepth,
31
+ args.limit,
32
+ policy.includeIgnored,
33
+ args.query,
34
+ );
35
+
36
+ if (result.files.length === 0) {
37
+ const gitignorePatterns = await parseGitignore(args.projectRoot);
38
+ result = await traverseDirectory(
39
+ args.projectRoot,
40
+ args.projectRoot,
41
+ args.maxDepth,
42
+ 0,
43
+ args.limit,
44
+ [],
45
+ gitignorePatterns,
46
+ );
47
+ const normalizedQuery = args.query?.trim().toLowerCase();
48
+ if (normalizedQuery) {
49
+ const files = result.files.filter((file) =>
50
+ file.toLowerCase().includes(normalizedQuery),
51
+ );
52
+ result = {
53
+ files: files.slice(0, args.limit),
54
+ truncated: files.length > args.limit,
55
+ };
56
+ }
57
+ }
58
+
59
+ const [changedFiles, ignoredFiles] = await Promise.all([
60
+ getChangedFiles(args.projectRoot),
61
+ getGitIgnoredFiles(args.projectRoot, result.files),
62
+ ]);
63
+
64
+ result.files.sort((a, b) => {
65
+ const aIgnored = ignoredFiles.has(a);
66
+ const bIgnored = ignoredFiles.has(b);
67
+ if (aIgnored !== bIgnored) return aIgnored ? 1 : -1;
68
+ const aChanged = changedFiles.has(a);
69
+ const bChanged = changedFiles.has(b);
70
+ if (aChanged && !bChanged) return -1;
71
+ if (!aChanged && bChanged) return 1;
72
+ return a.localeCompare(b);
73
+ });
74
+
75
+ return { result, changedFiles, ignoredFiles };
76
+ }
77
+
78
+ function getFilePolicyFromQuery(c: Context) {
79
+ const projectRoot = c.req.query('project') || process.cwd();
80
+ const policy = getSearchPolicy(projectRoot);
81
+ const maxDepth = clampNumber(
82
+ Number.parseInt(c.req.query('maxDepth') || String(policy.maxDepth), 10),
83
+ 1,
84
+ policy.maxDepth,
85
+ );
86
+ const limit = clampNumber(
87
+ Number.parseInt(c.req.query('limit') || String(policy.limit), 10),
88
+ 1,
89
+ policy.limit,
90
+ );
91
+ return { projectRoot, maxDepth, limit };
92
+ }
93
+
94
+ function fileListResponse(args: {
95
+ projectRoot: string;
96
+ maxDepth: number;
97
+ limit: number;
98
+ result: { files: string[]; truncated: boolean };
99
+ changedFiles: Map<string, string>;
100
+ ignoredFiles: Set<string>;
101
+ }) {
102
+ return {
103
+ files: args.result.files,
104
+ ignoredFiles: Array.from(args.ignoredFiles),
105
+ changedFiles: Array.from(args.changedFiles.entries()).map(
106
+ ([path, status]) => ({
107
+ path,
108
+ status,
109
+ }),
110
+ ),
111
+ truncated: args.result.truncated,
112
+ policy: {
113
+ maxDepth: args.maxDepth,
114
+ limit: args.limit,
115
+ home: isHomeDirectory(args.projectRoot),
116
+ },
117
+ };
118
+ }
119
+
120
+ export async function handleListFiles(c: Context) {
121
+ try {
122
+ const { projectRoot, maxDepth, limit } = getFilePolicyFromQuery(c);
123
+ const { result, changedFiles, ignoredFiles } = await getSortedFileResult({
124
+ projectRoot,
125
+ maxDepth,
126
+ limit,
127
+ });
128
+ return c.json(
129
+ fileListResponse({
130
+ projectRoot,
131
+ maxDepth,
132
+ limit,
133
+ result,
134
+ changedFiles,
135
+ ignoredFiles,
136
+ }),
137
+ );
138
+ } catch (err) {
139
+ logger.error('Files route error:', err);
140
+ return c.json({ error: serializeError(err) }, 500);
141
+ }
142
+ }
143
+
144
+ export async function handleSearchFiles(c: Context) {
145
+ try {
146
+ const { projectRoot, maxDepth, limit } = getFilePolicyFromQuery(c);
147
+ const query = c.req.query('q') || '';
148
+ const { result, changedFiles, ignoredFiles } = await getSortedFileResult({
149
+ projectRoot,
150
+ maxDepth,
151
+ limit,
152
+ query,
153
+ });
154
+ return c.json(
155
+ fileListResponse({
156
+ projectRoot,
157
+ maxDepth,
158
+ limit,
159
+ result,
160
+ changedFiles,
161
+ ignoredFiles,
162
+ }),
163
+ );
164
+ } catch (err) {
165
+ logger.error('Files search route error:', err);
166
+ return c.json({ error: serializeError(err) }, 500);
167
+ }
168
+ }
169
+
170
+ export async function handleFileTree(c: Context) {
171
+ try {
172
+ const projectRoot = c.req.query('project') || process.cwd();
173
+ const dirPath = c.req.query('path') || '.';
174
+ const targetDir = resolve(projectRoot, dirPath);
175
+ if (!targetDir.startsWith(resolve(projectRoot))) {
176
+ return c.json({ error: 'Path traversal not allowed' }, 403);
177
+ }
178
+
179
+ const gitignorePatterns = await parseGitignore(projectRoot);
180
+ const entries = await readdir(targetDir, { withFileTypes: true });
181
+ const truncated = entries.length > TREE_ENTRY_LIMIT;
182
+ const items: Array<{
183
+ name: string;
184
+ path: string;
185
+ type: 'file' | 'directory';
186
+ gitignored?: boolean;
187
+ vendor?: boolean;
188
+ searchable?: boolean;
189
+ }> = [];
190
+
191
+ for (const entry of entries.slice(0, TREE_ENTRY_LIMIT)) {
192
+ const relPath = relative(projectRoot, join(targetDir, entry.name));
193
+ if (entry.isDirectory()) {
194
+ const ignored = matchesGitignorePattern(relPath, gitignorePatterns);
195
+ const vendor = shouldExcludeDir(entry.name);
196
+ items.push({
197
+ name: entry.name,
198
+ path: relPath,
199
+ type: 'directory',
200
+ gitignored: ignored || undefined,
201
+ vendor: vendor || undefined,
202
+ searchable: vendor || ignored ? false : undefined,
203
+ });
204
+ } else if (entry.isFile()) {
205
+ if (shouldExcludeFile(entry.name)) continue;
206
+ const ignored = matchesGitignorePattern(relPath, gitignorePatterns);
207
+ items.push({
208
+ name: entry.name,
209
+ path: relPath,
210
+ type: 'file',
211
+ gitignored: ignored || undefined,
212
+ searchable: ignored ? false : undefined,
213
+ });
214
+ }
215
+ }
216
+
217
+ items.sort((a, b) => {
218
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
219
+ const aIgnored = a.gitignored ?? false;
220
+ const bIgnored = b.gitignored ?? false;
221
+ if (aIgnored !== bIgnored) return aIgnored ? 1 : -1;
222
+ return a.name.localeCompare(b.name);
223
+ });
224
+
225
+ return c.json({ items, path: dirPath, truncated });
226
+ } catch (err) {
227
+ logger.error('Files tree route error:', err);
228
+ return c.json({ error: serializeError(err) }, 500);
229
+ }
230
+ }
231
+
232
+ function getSafeFilePath(c: Context) {
233
+ const projectRoot = c.req.query('project') || process.cwd();
234
+ const filePath = c.req.query('path');
235
+ if (!filePath) {
236
+ return { error: 'Missing required query parameter: path' as const };
237
+ }
238
+ const absPath = join(projectRoot, filePath);
239
+ if (!absPath.startsWith(projectRoot)) {
240
+ return {
241
+ error: 'Path traversal not allowed' as const,
242
+ status: 403 as const,
243
+ };
244
+ }
245
+ return { projectRoot, filePath, absPath };
246
+ }
247
+
248
+ export async function handleReadFile(c: Context) {
249
+ try {
250
+ const target = getSafeFilePath(c);
251
+ if ('error' in target)
252
+ return c.json({ error: target.error }, target.status ?? 400);
253
+ const content = await readFile(target.absPath, 'utf-8');
254
+ const extension = target.filePath.split('.').pop()?.toLowerCase() ?? '';
255
+ const lineCount = content.split('\n').length;
256
+ return c.json({
257
+ content,
258
+ path: target.filePath,
259
+ extension,
260
+ lineCount,
261
+ });
262
+ } catch (err) {
263
+ logger.error('Files read route error:', err);
264
+ return c.json({ error: serializeError(err) }, 500);
265
+ }
266
+ }
267
+
268
+ export async function handleRawFile(c: Context) {
269
+ try {
270
+ const target = getSafeFilePath(c);
271
+ if ('error' in target)
272
+ return c.json({ error: target.error }, target.status ?? 400);
273
+ const ext = target.filePath.split('.').pop()?.toLowerCase() ?? '';
274
+ const mimeTypes: Record<string, string> = {
275
+ png: 'image/png',
276
+ jpg: 'image/jpeg',
277
+ jpeg: 'image/jpeg',
278
+ gif: 'image/gif',
279
+ svg: 'image/svg+xml',
280
+ webp: 'image/webp',
281
+ ico: 'image/x-icon',
282
+ bmp: 'image/bmp',
283
+ avif: 'image/avif',
284
+ };
285
+ const contentType = mimeTypes[ext] || 'application/octet-stream';
286
+ const data = await readFile(target.absPath);
287
+ return new Response(data, {
288
+ headers: {
289
+ 'Content-Type': contentType,
290
+ 'Cache-Control': 'no-cache',
291
+ },
292
+ });
293
+ } catch (err) {
294
+ logger.error('Files raw route error:', err);
295
+ return c.json({ error: serializeError(err) }, 500);
296
+ }
297
+ }