@gmickel/gno 0.7.0 → 0.8.1

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 (209) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -50
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/assets/screenshots/webui-ask-answer.png +0 -0
  5. package/assets/screenshots/webui-collections.png +0 -0
  6. package/assets/screenshots/webui-editor.png +0 -0
  7. package/assets/screenshots/webui-home.png +0 -0
  8. package/assets/skill/SKILL.md +12 -12
  9. package/assets/skill/cli-reference.md +59 -57
  10. package/assets/skill/examples.md +8 -7
  11. package/assets/skill/mcp-reference.md +8 -4
  12. package/package.json +31 -24
  13. package/src/app/constants.ts +43 -42
  14. package/src/cli/colors.ts +1 -1
  15. package/src/cli/commands/ask.ts +44 -43
  16. package/src/cli/commands/cleanup.ts +9 -8
  17. package/src/cli/commands/collection/add.ts +12 -12
  18. package/src/cli/commands/collection/index.ts +4 -4
  19. package/src/cli/commands/collection/list.ts +26 -25
  20. package/src/cli/commands/collection/remove.ts +10 -10
  21. package/src/cli/commands/collection/rename.ts +10 -10
  22. package/src/cli/commands/context/add.ts +1 -1
  23. package/src/cli/commands/context/check.ts +17 -17
  24. package/src/cli/commands/context/index.ts +4 -4
  25. package/src/cli/commands/context/list.ts +11 -11
  26. package/src/cli/commands/context/rm.ts +1 -1
  27. package/src/cli/commands/doctor.ts +86 -84
  28. package/src/cli/commands/embed.ts +30 -28
  29. package/src/cli/commands/get.ts +27 -26
  30. package/src/cli/commands/index-cmd.ts +9 -9
  31. package/src/cli/commands/index.ts +16 -16
  32. package/src/cli/commands/init.ts +13 -12
  33. package/src/cli/commands/ls.ts +20 -19
  34. package/src/cli/commands/mcp/config.ts +30 -28
  35. package/src/cli/commands/mcp/index.ts +4 -4
  36. package/src/cli/commands/mcp/install.ts +17 -17
  37. package/src/cli/commands/mcp/paths.ts +133 -133
  38. package/src/cli/commands/mcp/status.ts +21 -21
  39. package/src/cli/commands/mcp/uninstall.ts +13 -13
  40. package/src/cli/commands/mcp.ts +2 -2
  41. package/src/cli/commands/models/clear.ts +12 -11
  42. package/src/cli/commands/models/index.ts +5 -5
  43. package/src/cli/commands/models/list.ts +31 -30
  44. package/src/cli/commands/models/path.ts +1 -1
  45. package/src/cli/commands/models/pull.ts +19 -18
  46. package/src/cli/commands/models/use.ts +4 -4
  47. package/src/cli/commands/multi-get.ts +38 -36
  48. package/src/cli/commands/query.ts +21 -20
  49. package/src/cli/commands/ref-parser.ts +10 -10
  50. package/src/cli/commands/reset.ts +40 -39
  51. package/src/cli/commands/search.ts +14 -13
  52. package/src/cli/commands/serve.ts +4 -4
  53. package/src/cli/commands/shared.ts +11 -10
  54. package/src/cli/commands/skill/index.ts +5 -5
  55. package/src/cli/commands/skill/install.ts +18 -17
  56. package/src/cli/commands/skill/paths-cmd.ts +11 -10
  57. package/src/cli/commands/skill/paths.ts +23 -23
  58. package/src/cli/commands/skill/show.ts +13 -12
  59. package/src/cli/commands/skill/uninstall.ts +16 -15
  60. package/src/cli/commands/status.ts +25 -24
  61. package/src/cli/commands/update.ts +3 -3
  62. package/src/cli/commands/vsearch.ts +17 -16
  63. package/src/cli/context.ts +5 -5
  64. package/src/cli/errors.ts +3 -3
  65. package/src/cli/format/search-results.ts +37 -37
  66. package/src/cli/options.ts +43 -43
  67. package/src/cli/program.ts +455 -459
  68. package/src/cli/progress.ts +1 -1
  69. package/src/cli/run.ts +24 -23
  70. package/src/collection/add.ts +9 -8
  71. package/src/collection/index.ts +3 -3
  72. package/src/collection/remove.ts +7 -6
  73. package/src/collection/types.ts +6 -6
  74. package/src/config/defaults.ts +1 -1
  75. package/src/config/index.ts +5 -5
  76. package/src/config/loader.ts +19 -18
  77. package/src/config/paths.ts +9 -8
  78. package/src/config/saver.ts +14 -13
  79. package/src/config/types.ts +53 -52
  80. package/src/converters/adapters/markitdownTs/adapter.ts +21 -19
  81. package/src/converters/adapters/officeparser/adapter.ts +18 -16
  82. package/src/converters/canonicalize.ts +12 -12
  83. package/src/converters/errors.ts +26 -22
  84. package/src/converters/index.ts +8 -8
  85. package/src/converters/mime.ts +25 -25
  86. package/src/converters/native/markdown.ts +10 -9
  87. package/src/converters/native/plaintext.ts +8 -7
  88. package/src/converters/path.ts +2 -2
  89. package/src/converters/pipeline.ts +11 -10
  90. package/src/converters/registry.ts +8 -8
  91. package/src/converters/types.ts +14 -14
  92. package/src/converters/versions.ts +4 -4
  93. package/src/index.ts +4 -4
  94. package/src/ingestion/chunker.ts +10 -9
  95. package/src/ingestion/index.ts +6 -6
  96. package/src/ingestion/language.ts +62 -62
  97. package/src/ingestion/sync.ts +50 -49
  98. package/src/ingestion/types.ts +10 -10
  99. package/src/ingestion/walker.ts +14 -13
  100. package/src/llm/cache.ts +51 -49
  101. package/src/llm/errors.ts +40 -36
  102. package/src/llm/index.ts +9 -9
  103. package/src/llm/lockfile.ts +6 -6
  104. package/src/llm/nodeLlamaCpp/adapter.ts +13 -12
  105. package/src/llm/nodeLlamaCpp/embedding.ts +9 -8
  106. package/src/llm/nodeLlamaCpp/generation.ts +7 -6
  107. package/src/llm/nodeLlamaCpp/lifecycle.ts +11 -10
  108. package/src/llm/nodeLlamaCpp/rerank.ts +6 -5
  109. package/src/llm/policy.ts +5 -5
  110. package/src/llm/registry.ts +6 -5
  111. package/src/llm/types.ts +2 -2
  112. package/src/mcp/resources/index.ts +15 -13
  113. package/src/mcp/server.ts +25 -23
  114. package/src/mcp/tools/get.ts +25 -23
  115. package/src/mcp/tools/index.ts +32 -29
  116. package/src/mcp/tools/multi-get.ts +34 -32
  117. package/src/mcp/tools/query.ts +29 -27
  118. package/src/mcp/tools/search.ts +14 -12
  119. package/src/mcp/tools/status.ts +12 -11
  120. package/src/mcp/tools/vsearch.ts +26 -24
  121. package/src/pipeline/answer.ts +9 -9
  122. package/src/pipeline/chunk-lookup.ts +1 -1
  123. package/src/pipeline/contextual.ts +4 -4
  124. package/src/pipeline/expansion.ts +23 -21
  125. package/src/pipeline/explain.ts +21 -21
  126. package/src/pipeline/fusion.ts +9 -9
  127. package/src/pipeline/hybrid.ts +41 -42
  128. package/src/pipeline/index.ts +10 -10
  129. package/src/pipeline/query-language.ts +39 -39
  130. package/src/pipeline/rerank.ts +8 -7
  131. package/src/pipeline/search.ts +22 -22
  132. package/src/pipeline/types.ts +8 -8
  133. package/src/pipeline/vsearch.ts +21 -24
  134. package/src/serve/CLAUDE.md +21 -15
  135. package/src/serve/config-sync.ts +9 -8
  136. package/src/serve/context.ts +19 -18
  137. package/src/serve/index.ts +1 -1
  138. package/src/serve/jobs.ts +7 -7
  139. package/src/serve/public/app.tsx +79 -25
  140. package/src/serve/public/components/AddCollectionDialog.tsx +382 -0
  141. package/src/serve/public/components/CaptureButton.tsx +60 -0
  142. package/src/serve/public/components/CaptureModal.tsx +365 -0
  143. package/src/serve/public/components/IndexingProgress.tsx +333 -0
  144. package/src/serve/public/components/ShortcutHelpModal.tsx +106 -0
  145. package/src/serve/public/components/ai-elements/code-block.tsx +42 -32
  146. package/src/serve/public/components/ai-elements/conversation.tsx +16 -14
  147. package/src/serve/public/components/ai-elements/inline-citation.tsx +33 -32
  148. package/src/serve/public/components/ai-elements/loader.tsx +5 -4
  149. package/src/serve/public/components/ai-elements/message.tsx +39 -37
  150. package/src/serve/public/components/ai-elements/prompt-input.tsx +97 -95
  151. package/src/serve/public/components/ai-elements/sources.tsx +12 -10
  152. package/src/serve/public/components/ai-elements/suggestion.tsx +10 -9
  153. package/src/serve/public/components/editor/CodeMirrorEditor.tsx +142 -0
  154. package/src/serve/public/components/editor/MarkdownPreview.tsx +311 -0
  155. package/src/serve/public/components/editor/index.ts +6 -0
  156. package/src/serve/public/components/preset-selector.tsx +29 -28
  157. package/src/serve/public/components/ui/badge.tsx +13 -12
  158. package/src/serve/public/components/ui/button-group.tsx +13 -12
  159. package/src/serve/public/components/ui/button.tsx +23 -22
  160. package/src/serve/public/components/ui/card.tsx +16 -16
  161. package/src/serve/public/components/ui/carousel.tsx +36 -35
  162. package/src/serve/public/components/ui/collapsible.tsx +1 -1
  163. package/src/serve/public/components/ui/command.tsx +17 -15
  164. package/src/serve/public/components/ui/dialog.tsx +13 -12
  165. package/src/serve/public/components/ui/dropdown-menu.tsx +13 -12
  166. package/src/serve/public/components/ui/hover-card.tsx +6 -5
  167. package/src/serve/public/components/ui/input-group.tsx +45 -43
  168. package/src/serve/public/components/ui/input.tsx +6 -6
  169. package/src/serve/public/components/ui/progress.tsx +5 -4
  170. package/src/serve/public/components/ui/scroll-area.tsx +11 -10
  171. package/src/serve/public/components/ui/select.tsx +19 -18
  172. package/src/serve/public/components/ui/separator.tsx +6 -5
  173. package/src/serve/public/components/ui/table.tsx +18 -18
  174. package/src/serve/public/components/ui/textarea.tsx +4 -4
  175. package/src/serve/public/components/ui/tooltip.tsx +5 -4
  176. package/src/serve/public/globals.css +27 -4
  177. package/src/serve/public/hooks/use-api.ts +8 -8
  178. package/src/serve/public/hooks/useCaptureModal.tsx +83 -0
  179. package/src/serve/public/hooks/useKeyboardShortcuts.ts +85 -0
  180. package/src/serve/public/index.html +4 -4
  181. package/src/serve/public/lib/utils.ts +6 -0
  182. package/src/serve/public/pages/Ask.tsx +27 -26
  183. package/src/serve/public/pages/Browse.tsx +28 -27
  184. package/src/serve/public/pages/Collections.tsx +439 -0
  185. package/src/serve/public/pages/Dashboard.tsx +166 -40
  186. package/src/serve/public/pages/DocView.tsx +258 -73
  187. package/src/serve/public/pages/DocumentEditor.tsx +510 -0
  188. package/src/serve/public/pages/Search.tsx +80 -58
  189. package/src/serve/routes/api.ts +272 -155
  190. package/src/serve/security.ts +4 -4
  191. package/src/serve/server.ts +66 -48
  192. package/src/store/index.ts +5 -5
  193. package/src/store/migrations/001-initial.ts +24 -23
  194. package/src/store/migrations/002-documents-fts.ts +7 -6
  195. package/src/store/migrations/index.ts +4 -4
  196. package/src/store/migrations/runner.ts +17 -15
  197. package/src/store/sqlite/adapter.ts +123 -121
  198. package/src/store/sqlite/fts5-snowball.ts +24 -23
  199. package/src/store/sqlite/index.ts +1 -1
  200. package/src/store/sqlite/setup.ts +12 -12
  201. package/src/store/sqlite/types.ts +4 -4
  202. package/src/store/types.ts +19 -19
  203. package/src/store/vector/index.ts +3 -3
  204. package/src/store/vector/sqlite-vec.ts +23 -20
  205. package/src/store/vector/stats.ts +10 -8
  206. package/src/store/vector/types.ts +2 -2
  207. package/vendor/fts5-snowball/README.md +6 -6
  208. package/assets/screenshots/webui-ask-answer.jpg +0 -0
  209. package/assets/screenshots/webui-home.jpg +0 -0
@@ -5,27 +5,28 @@
5
5
  * @module src/serve/routes/api
6
6
  */
7
7
 
8
- import { modelsPull } from '../../cli/commands/models/pull';
9
- import { addCollection, removeCollection } from '../../collection';
10
- import type { Config, ModelPreset } from '../../config/types';
11
- import { defaultSyncService, type SyncResult } from '../../ingestion';
12
- import { getModelConfig, getPreset, listPresets } from '../../llm/registry';
8
+ import type { Config, ModelPreset } from "../../config/types";
9
+ import type { AskResult, Citation, SearchOptions } from "../../pipeline/types";
10
+ import type { SqliteAdapter } from "../../store/sqlite/adapter";
11
+
12
+ import { modelsPull } from "../../cli/commands/models/pull";
13
+ import { addCollection, removeCollection } from "../../collection";
14
+ import { defaultSyncService, type SyncResult } from "../../ingestion";
15
+ import { getModelConfig, getPreset, listPresets } from "../../llm/registry";
13
16
  import {
14
17
  generateGroundedAnswer,
15
18
  processAnswerResult,
16
- } from '../../pipeline/answer';
17
- import { searchHybrid } from '../../pipeline/hybrid';
18
- import { searchBm25 } from '../../pipeline/search';
19
- import type { AskResult, Citation, SearchOptions } from '../../pipeline/types';
20
- import type { SqliteAdapter } from '../../store/sqlite/adapter';
21
- import { applyConfigChange } from '../config-sync';
19
+ } from "../../pipeline/answer";
20
+ import { searchHybrid } from "../../pipeline/hybrid";
21
+ import { searchBm25 } from "../../pipeline/search";
22
+ import { applyConfigChange } from "../config-sync";
22
23
  import {
23
24
  downloadState,
24
25
  reloadServerContext,
25
26
  resetDownloadState,
26
27
  type ServerContext,
27
- } from '../context';
28
- import { getJobStatus, startJob } from '../jobs';
28
+ } from "../context";
29
+ import { getJobStatus, startJob } from "../jobs";
29
30
 
30
31
  /** Mutable context holder for hot-reloading presets */
31
32
  export interface ContextHolder {
@@ -91,6 +92,10 @@ export interface CreateDocRequestBody {
91
92
  overwrite?: boolean;
92
93
  }
93
94
 
95
+ export interface UpdateDocRequestBody {
96
+ content: string;
97
+ }
98
+
94
99
  // ─────────────────────────────────────────────────────────────────────────────
95
100
  // Helpers
96
101
  // ─────────────────────────────────────────────────────────────────────────────
@@ -122,7 +127,7 @@ export function handleHealth(): Response {
122
127
  export async function handleStatus(store: SqliteAdapter): Promise<Response> {
123
128
  const result = await store.getStatus();
124
129
  if (!result.ok) {
125
- return errorResponse('RUNTIME', result.error.message, 500);
130
+ return errorResponse("RUNTIME", result.error.message, 500);
126
131
  }
127
132
 
128
133
  const s = result.value;
@@ -154,7 +159,7 @@ export async function handleCollections(
154
159
  ): Promise<Response> {
155
160
  const result = await store.getCollections();
156
161
  if (!result.ok) {
157
- return errorResponse('RUNTIME', result.error.message, 500);
162
+ return errorResponse("RUNTIME", result.error.message, 500);
158
163
  }
159
164
 
160
165
  return jsonResponse(
@@ -178,42 +183,42 @@ export async function handleCreateCollection(
178
183
  try {
179
184
  body = (await req.json()) as CreateCollectionRequestBody;
180
185
  } catch {
181
- return errorResponse('VALIDATION', 'Invalid JSON body');
186
+ return errorResponse("VALIDATION", "Invalid JSON body");
182
187
  }
183
188
 
184
189
  // Validate required fields
185
- if (!body.path || typeof body.path !== 'string') {
186
- return errorResponse('VALIDATION', 'Missing or invalid path');
190
+ if (!body.path || typeof body.path !== "string") {
191
+ return errorResponse("VALIDATION", "Missing or invalid path");
187
192
  }
188
193
 
189
194
  // Validate optional fields have correct types
190
- if (body.name !== undefined && typeof body.name !== 'string') {
191
- return errorResponse('VALIDATION', 'name must be a string');
195
+ if (body.name !== undefined && typeof body.name !== "string") {
196
+ return errorResponse("VALIDATION", "name must be a string");
192
197
  }
193
- if (body.pattern !== undefined && typeof body.pattern !== 'string') {
194
- return errorResponse('VALIDATION', 'pattern must be a string');
198
+ if (body.pattern !== undefined && typeof body.pattern !== "string") {
199
+ return errorResponse("VALIDATION", "pattern must be a string");
195
200
  }
196
201
  if (
197
202
  body.include !== undefined &&
198
- typeof body.include !== 'string' &&
203
+ typeof body.include !== "string" &&
199
204
  !Array.isArray(body.include)
200
205
  ) {
201
- return errorResponse('VALIDATION', 'include must be a string or array');
206
+ return errorResponse("VALIDATION", "include must be a string or array");
202
207
  }
203
208
  if (
204
209
  body.exclude !== undefined &&
205
- typeof body.exclude !== 'string' &&
210
+ typeof body.exclude !== "string" &&
206
211
  !Array.isArray(body.exclude)
207
212
  ) {
208
- return errorResponse('VALIDATION', 'exclude must be a string or array');
213
+ return errorResponse("VALIDATION", "exclude must be a string or array");
209
214
  }
210
- if (body.gitPull !== undefined && typeof body.gitPull !== 'boolean') {
211
- return errorResponse('VALIDATION', 'gitPull must be a boolean');
215
+ if (body.gitPull !== undefined && typeof body.gitPull !== "boolean") {
216
+ return errorResponse("VALIDATION", "gitPull must be a boolean");
212
217
  }
213
218
 
214
219
  // Derive name from path if not provided
215
- const { basename } = await import('node:path'); // no bun equivalent
216
- const name = body.name || basename(body.path);
220
+ const path = await import("node:path"); // no bun equivalent
221
+ const name = body.name || path.basename(body.path);
217
222
 
218
223
  // Persist config and sync to DB (mutation happens inside with fresh config)
219
224
  const syncResult = await applyConfigChange(ctxHolder, store, async (cfg) => {
@@ -243,9 +248,9 @@ export async function handleCreateCollection(
243
248
  // Find the newly added collection from config
244
249
  const collection = syncResult.config.collections.find((c) => c.name === name);
245
250
  if (!collection) {
246
- return errorResponse('RUNTIME', 'Collection not found after add', 500);
251
+ return errorResponse("RUNTIME", "Collection not found after add", 500);
247
252
  }
248
- const jobResult = startJob('add', async (): Promise<SyncResult> => {
253
+ const jobResult = startJob("add", async (): Promise<SyncResult> => {
249
254
  const result = await defaultSyncService.syncCollection(collection, store, {
250
255
  gitPull: body.gitPull,
251
256
  runUpdateCmd: true,
@@ -262,13 +267,13 @@ export async function handleCreateCollection(
262
267
  });
263
268
 
264
269
  if (!jobResult.ok) {
265
- return errorResponse('CONFLICT', jobResult.error, 409);
270
+ return errorResponse("CONFLICT", jobResult.error, 409);
266
271
  }
267
272
 
268
273
  return jsonResponse(
269
274
  {
270
275
  jobId: jobResult.jobId,
271
- collection: collection.name,
276
+ collection: { name: collection.name, path: collection.path },
272
277
  },
273
278
  202
274
279
  );
@@ -312,7 +317,7 @@ export async function handleDeleteCollection(
312
317
  return jsonResponse({
313
318
  success: true,
314
319
  collection: name,
315
- note: 'Collection removed from config. Indexed documents remain in DB.',
320
+ note: "Collection removed from config. Indexed documents remain in DB.",
316
321
  });
317
322
  }
318
323
 
@@ -332,15 +337,15 @@ export async function handleSync(
332
337
  body = JSON.parse(text) as SyncRequestBody;
333
338
  }
334
339
  } catch {
335
- return errorResponse('VALIDATION', 'Invalid JSON body');
340
+ return errorResponse("VALIDATION", "Invalid JSON body");
336
341
  }
337
342
 
338
343
  // Validate optional fields
339
- if (body.collection !== undefined && typeof body.collection !== 'string') {
340
- return errorResponse('VALIDATION', 'collection must be a string');
344
+ if (body.collection !== undefined && typeof body.collection !== "string") {
345
+ return errorResponse("VALIDATION", "collection must be a string");
341
346
  }
342
- if (body.gitPull !== undefined && typeof body.gitPull !== 'boolean') {
343
- return errorResponse('VALIDATION', 'gitPull must be a boolean');
347
+ if (body.gitPull !== undefined && typeof body.gitPull !== "boolean") {
348
+ return errorResponse("VALIDATION", "gitPull must be a boolean");
344
349
  }
345
350
 
346
351
  // Get collections to sync (case-insensitive matching)
@@ -353,18 +358,18 @@ export async function handleSync(
353
358
 
354
359
  if (body.collection && collections.length === 0) {
355
360
  return errorResponse(
356
- 'NOT_FOUND',
361
+ "NOT_FOUND",
357
362
  `Collection not found: ${body.collection}`,
358
363
  404
359
364
  );
360
365
  }
361
366
 
362
367
  if (collections.length === 0) {
363
- return errorResponse('VALIDATION', 'No collections to sync');
368
+ return errorResponse("VALIDATION", "No collections to sync");
364
369
  }
365
370
 
366
371
  // Start background sync job
367
- const jobResult = startJob('sync', async (): Promise<SyncResult> => {
372
+ const jobResult = startJob("sync", async (): Promise<SyncResult> => {
368
373
  return await defaultSyncService.syncAll(collections, store, {
369
374
  gitPull: body.gitPull,
370
375
  runUpdateCmd: true,
@@ -372,7 +377,7 @@ export async function handleSync(
372
377
  });
373
378
 
374
379
  if (!jobResult.ok) {
375
- return errorResponse('CONFLICT', jobResult.error, 409);
380
+ return errorResponse("CONFLICT", jobResult.error, 409);
376
381
  }
377
382
 
378
383
  return jsonResponse({ jobId: jobResult.jobId }, 202);
@@ -387,25 +392,25 @@ export async function handleDocs(
387
392
  store: SqliteAdapter,
388
393
  url: URL
389
394
  ): Promise<Response> {
390
- const collection = url.searchParams.get('collection') || undefined;
395
+ const collection = url.searchParams.get("collection") || undefined;
391
396
 
392
397
  // Validate limit: positive integer, max 100
393
- const limitParam = Number(url.searchParams.get('limit'));
398
+ const limitParam = Number(url.searchParams.get("limit"));
394
399
  if (
395
- url.searchParams.has('limit') &&
400
+ url.searchParams.has("limit") &&
396
401
  (Number.isNaN(limitParam) || limitParam < 1)
397
402
  ) {
398
- return errorResponse('VALIDATION', 'limit must be a positive integer');
403
+ return errorResponse("VALIDATION", "limit must be a positive integer");
399
404
  }
400
405
  const limit = Math.min(limitParam || 20, 100);
401
406
 
402
407
  // Validate offset: non-negative integer
403
- const offsetParam = Number(url.searchParams.get('offset'));
408
+ const offsetParam = Number(url.searchParams.get("offset"));
404
409
  if (
405
- url.searchParams.has('offset') &&
410
+ url.searchParams.has("offset") &&
406
411
  (Number.isNaN(offsetParam) || offsetParam < 0)
407
412
  ) {
408
- return errorResponse('VALIDATION', 'offset must be a non-negative integer');
413
+ return errorResponse("VALIDATION", "offset must be a non-negative integer");
409
414
  }
410
415
  const offset = offsetParam || 0;
411
416
 
@@ -416,7 +421,7 @@ export async function handleDocs(
416
421
  });
417
422
 
418
423
  if (!result.ok) {
419
- return errorResponse('RUNTIME', result.error.message, 500);
424
+ return errorResponse("RUNTIME", result.error.message, 500);
420
425
  }
421
426
 
422
427
  const { documents, total } = result.value;
@@ -447,17 +452,17 @@ export async function handleDoc(
447
452
  store: SqliteAdapter,
448
453
  url: URL
449
454
  ): Promise<Response> {
450
- const uri = url.searchParams.get('uri');
455
+ const uri = url.searchParams.get("uri");
451
456
  if (!uri) {
452
- return errorResponse('VALIDATION', 'Missing uri parameter');
457
+ return errorResponse("VALIDATION", "Missing uri parameter");
453
458
  }
454
459
 
455
460
  const docResult = await store.getDocumentByUri(uri);
456
461
  if (!docResult.ok) {
457
- return errorResponse('RUNTIME', docResult.error.message, 500);
462
+ return errorResponse("RUNTIME", docResult.error.message, 500);
458
463
  }
459
464
  if (!docResult.value) {
460
- return errorResponse('NOT_FOUND', 'Document not found', 404);
465
+ return errorResponse("NOT_FOUND", "Document not found", 404);
461
466
  }
462
467
 
463
468
  const doc = docResult.value;
@@ -498,10 +503,10 @@ export async function handleDeactivateDoc(
498
503
  // Get document to verify it exists and get collection/relPath
499
504
  const docResult = await store.getDocumentByDocid(docId);
500
505
  if (!docResult.ok) {
501
- return errorResponse('RUNTIME', docResult.error.message, 500);
506
+ return errorResponse("RUNTIME", docResult.error.message, 500);
502
507
  }
503
508
  if (!docResult.value) {
504
- return errorResponse('NOT_FOUND', 'Document not found', 404);
509
+ return errorResponse("NOT_FOUND", "Document not found", 404);
505
510
  }
506
511
 
507
512
  const doc = docResult.value;
@@ -509,17 +514,126 @@ export async function handleDeactivateDoc(
509
514
  // Mark as inactive
510
515
  const result = await store.markInactive(doc.collection, [doc.relPath]);
511
516
  if (!result.ok) {
512
- return errorResponse('RUNTIME', result.error.message, 500);
517
+ return errorResponse("RUNTIME", result.error.message, 500);
513
518
  }
514
519
 
515
520
  return jsonResponse({
516
521
  success: true,
517
522
  docId: doc.docid,
518
523
  path: doc.uri,
519
- warning: 'File still exists on disk. Will be re-indexed unless excluded.',
524
+ warning: "File still exists on disk. Will be re-indexed unless excluded.",
520
525
  });
521
526
  }
522
527
 
528
+ /**
529
+ * PUT /api/docs/:id
530
+ * Update an existing document's content.
531
+ */
532
+ export async function handleUpdateDoc(
533
+ ctxHolder: ContextHolder,
534
+ store: SqliteAdapter,
535
+ docId: string,
536
+ req: Request
537
+ ): Promise<Response> {
538
+ let body: UpdateDocRequestBody;
539
+ try {
540
+ body = (await req.json()) as UpdateDocRequestBody;
541
+ } catch {
542
+ return errorResponse("VALIDATION", "Invalid JSON body");
543
+ }
544
+
545
+ // Validate content field (allow empty string)
546
+ if (typeof body.content !== "string") {
547
+ return errorResponse("VALIDATION", "Missing or invalid content");
548
+ }
549
+
550
+ // Get document to verify it exists
551
+ const docResult = await store.getDocumentByDocid(docId);
552
+ if (!docResult.ok) {
553
+ return errorResponse("RUNTIME", docResult.error.message, 500);
554
+ }
555
+ if (!docResult.value) {
556
+ return errorResponse("NOT_FOUND", "Document not found", 404);
557
+ }
558
+
559
+ const doc = docResult.value;
560
+
561
+ // Find collection config (case-insensitive)
562
+ const collectionName = doc.collection.toLowerCase();
563
+ const collection = ctxHolder.config.collections.find(
564
+ (c) => c.name.toLowerCase() === collectionName
565
+ );
566
+ if (!collection) {
567
+ return errorResponse(
568
+ "NOT_FOUND",
569
+ `Collection not found: ${doc.collection}`,
570
+ 404
571
+ );
572
+ }
573
+
574
+ // Resolve full path
575
+ const nodePath = await import("node:path"); // no bun equivalent
576
+ const fullPath = nodePath.join(collection.path, doc.relPath);
577
+
578
+ // Verify file exists
579
+ const file = Bun.file(fullPath);
580
+ if (!(await file.exists())) {
581
+ return errorResponse("FILE_NOT_FOUND", "Source file no longer exists", 404);
582
+ }
583
+
584
+ try {
585
+ // Write atomically via temp file + rename
586
+ const { rename, unlink } = await import("node:fs/promises"); // structure ops need fs
587
+ const tempPath = `${fullPath}.tmp.${crypto.randomUUID()}`;
588
+ await Bun.write(tempPath, body.content);
589
+ try {
590
+ await rename(tempPath, fullPath);
591
+ } catch (renameError) {
592
+ // Clean up temp file on failure
593
+ await unlink(tempPath).catch(() => {
594
+ /* ignore cleanup errors */
595
+ });
596
+ throw renameError;
597
+ }
598
+
599
+ // Build proper file:// URI using node:url
600
+ const { pathToFileURL } = await import("node:url");
601
+ const fileUri = pathToFileURL(fullPath).href;
602
+
603
+ // Run sync via job system (non-blocking)
604
+ const jobResult = startJob("sync", async (): Promise<SyncResult> => {
605
+ const result = await defaultSyncService.syncCollection(
606
+ collection,
607
+ store,
608
+ { runUpdateCmd: false }
609
+ );
610
+ return {
611
+ collections: [result],
612
+ totalDurationMs: result.durationMs,
613
+ totalFilesProcessed: result.filesProcessed,
614
+ totalFilesAdded: result.filesAdded,
615
+ totalFilesUpdated: result.filesUpdated,
616
+ totalFilesErrored: result.filesErrored,
617
+ totalFilesSkipped: result.filesSkipped,
618
+ };
619
+ });
620
+
621
+ return jsonResponse({
622
+ success: true,
623
+ docId: doc.docid,
624
+ uri: fileUri,
625
+ path: fullPath,
626
+ jobId: jobResult.ok ? jobResult.jobId : null,
627
+ });
628
+ } catch (e) {
629
+ return errorResponse(
630
+ "RUNTIME",
631
+ `Failed to update document: ${e instanceof Error ? e.message : String(e)}`,
632
+ 500
633
+ );
634
+ }
635
+ }
636
+
523
637
  /**
524
638
  * Validate relative path for document creation.
525
639
  * Returns error message if invalid, null if valid.
@@ -527,18 +641,18 @@ export async function handleDeactivateDoc(
527
641
  async function validateRelPath(
528
642
  relPath: string
529
643
  ): Promise<{ error: string } | { fullPath: string; normalizedPath: string }> {
530
- const { normalize, isAbsolute } = await import('node:path');
531
- if (isAbsolute(relPath)) {
532
- return { error: 'relPath must be relative' };
644
+ const nodePath = await import("node:path");
645
+ if (nodePath.isAbsolute(relPath)) {
646
+ return { error: "relPath must be relative" };
533
647
  }
534
- if (relPath.includes('\0')) {
535
- return { error: 'relPath contains invalid characters' };
648
+ if (relPath.includes("\0")) {
649
+ return { error: "relPath contains invalid characters" };
536
650
  }
537
- const normalizedPath = normalize(relPath);
538
- if (normalizedPath.startsWith('..')) {
539
- return { error: 'relPath cannot escape collection root' };
651
+ const normalizedPath = nodePath.normalize(relPath);
652
+ if (normalizedPath.startsWith("..")) {
653
+ return { error: "relPath cannot escape collection root" };
540
654
  }
541
- return { fullPath: '', normalizedPath };
655
+ return { fullPath: "", normalizedPath };
542
656
  }
543
657
 
544
658
  /**
@@ -555,21 +669,21 @@ export async function handleCreateDoc(
555
669
  try {
556
670
  body = (await req.json()) as CreateDocRequestBody;
557
671
  } catch {
558
- return errorResponse('VALIDATION', 'Invalid JSON body');
672
+ return errorResponse("VALIDATION", "Invalid JSON body");
559
673
  }
560
674
 
561
675
  // Validate required fields with type checks
562
- if (!body.collection || typeof body.collection !== 'string') {
563
- return errorResponse('VALIDATION', 'Missing or invalid collection');
676
+ if (!body.collection || typeof body.collection !== "string") {
677
+ return errorResponse("VALIDATION", "Missing or invalid collection");
564
678
  }
565
- if (!body.relPath || typeof body.relPath !== 'string') {
566
- return errorResponse('VALIDATION', 'Missing or invalid relPath');
679
+ if (!body.relPath || typeof body.relPath !== "string") {
680
+ return errorResponse("VALIDATION", "Missing or invalid relPath");
567
681
  }
568
- if (!body.content || typeof body.content !== 'string') {
569
- return errorResponse('VALIDATION', 'Missing or invalid content');
682
+ if (!body.content || typeof body.content !== "string") {
683
+ return errorResponse("VALIDATION", "Missing or invalid content");
570
684
  }
571
- if (body.overwrite !== undefined && typeof body.overwrite !== 'boolean') {
572
- return errorResponse('VALIDATION', 'overwrite must be a boolean');
685
+ if (body.overwrite !== undefined && typeof body.overwrite !== "boolean") {
686
+ return errorResponse("VALIDATION", "overwrite must be a boolean");
573
687
  }
574
688
 
575
689
  // Find collection (case-insensitive)
@@ -579,7 +693,7 @@ export async function handleCreateDoc(
579
693
  );
580
694
  if (!collection) {
581
695
  return errorResponse(
582
- 'NOT_FOUND',
696
+ "NOT_FOUND",
583
697
  `Collection not found: ${body.collection}`,
584
698
  404
585
699
  );
@@ -587,27 +701,30 @@ export async function handleCreateDoc(
587
701
 
588
702
  // Validate relPath - no path traversal
589
703
  const pathValidation = await validateRelPath(body.relPath);
590
- if ('error' in pathValidation) {
591
- return errorResponse('VALIDATION', pathValidation.error);
704
+ if ("error" in pathValidation) {
705
+ return errorResponse("VALIDATION", pathValidation.error);
592
706
  }
593
707
 
594
- const { join, dirname } = await import('node:path'); // no bun equivalent
595
- const fullPath = join(collection.path, pathValidation.normalizedPath);
708
+ const nodePath = await import("node:path"); // no bun equivalent
709
+ const fullPath = nodePath.join(
710
+ collection.path,
711
+ pathValidation.normalizedPath
712
+ );
596
713
 
597
714
  try {
598
715
  // Check if file already exists
599
716
  const file = Bun.file(fullPath);
600
717
  if ((await file.exists()) && !body.overwrite) {
601
718
  return errorResponse(
602
- 'CONFLICT',
603
- 'File already exists. Set overwrite=true to replace.',
719
+ "CONFLICT",
720
+ "File already exists. Set overwrite=true to replace.",
604
721
  409
605
722
  );
606
723
  }
607
724
 
608
725
  // Ensure parent directory exists
609
- const parentDir = dirname(fullPath);
610
- const { mkdir, rename, unlink } = await import('node:fs/promises'); // structure ops need fs
726
+ const parentDir = nodePath.dirname(fullPath);
727
+ const { mkdir, rename, unlink } = await import("node:fs/promises"); // structure ops need fs
611
728
  await mkdir(parentDir, { recursive: true });
612
729
 
613
730
  // Write file atomically (temp file + rename)
@@ -624,11 +741,11 @@ export async function handleCreateDoc(
624
741
  }
625
742
 
626
743
  // Build proper file:// URI using node:url
627
- const { pathToFileURL } = await import('node:url');
744
+ const { pathToFileURL } = await import("node:url");
628
745
  const fileUri = pathToFileURL(fullPath).href;
629
746
 
630
747
  // Run sync via job system (non-blocking)
631
- const jobResult = startJob('sync', async (): Promise<SyncResult> => {
748
+ const jobResult = startJob("sync", async (): Promise<SyncResult> => {
632
749
  const result = await defaultSyncService.syncCollection(
633
750
  collection,
634
751
  store,
@@ -651,14 +768,14 @@ export async function handleCreateDoc(
651
768
  path: fullPath,
652
769
  jobId: jobResult.ok ? jobResult.jobId : null,
653
770
  note: jobResult.ok
654
- ? 'File created. Sync job started - poll /api/jobs/:id for status.'
655
- : 'File created. Sync skipped (another job running).',
771
+ ? "File created. Sync job started - poll /api/jobs/:id for status."
772
+ : "File created. Sync skipped (another job running).",
656
773
  },
657
774
  202
658
775
  );
659
776
  } catch (e) {
660
777
  return errorResponse(
661
- 'RUNTIME',
778
+ "RUNTIME",
662
779
  `Failed to create document: ${e instanceof Error ? e.message : String(e)}`,
663
780
  500
664
781
  );
@@ -678,36 +795,36 @@ export async function handleSearch(
678
795
  try {
679
796
  body = (await req.json()) as SearchRequestBody;
680
797
  } catch {
681
- return errorResponse('VALIDATION', 'Invalid JSON body');
798
+ return errorResponse("VALIDATION", "Invalid JSON body");
682
799
  }
683
800
 
684
- if (!body.query || typeof body.query !== 'string') {
685
- return errorResponse('VALIDATION', 'Missing or invalid query');
801
+ if (!body.query || typeof body.query !== "string") {
802
+ return errorResponse("VALIDATION", "Missing or invalid query");
686
803
  }
687
804
 
688
805
  const query = body.query.trim();
689
806
  if (!query) {
690
- return errorResponse('VALIDATION', 'Query cannot be empty');
807
+ return errorResponse("VALIDATION", "Query cannot be empty");
691
808
  }
692
809
 
693
810
  // Validate limit: positive integer
694
811
  if (
695
812
  body.limit !== undefined &&
696
- (typeof body.limit !== 'number' || body.limit < 1)
813
+ (typeof body.limit !== "number" || body.limit < 1)
697
814
  ) {
698
- return errorResponse('VALIDATION', 'limit must be a positive integer');
815
+ return errorResponse("VALIDATION", "limit must be a positive integer");
699
816
  }
700
817
 
701
818
  // Validate minScore: number between 0 and 1
702
819
  if (
703
820
  body.minScore !== undefined &&
704
- (typeof body.minScore !== 'number' ||
821
+ (typeof body.minScore !== "number" ||
705
822
  body.minScore < 0 ||
706
823
  body.minScore > 1)
707
824
  ) {
708
825
  return errorResponse(
709
- 'VALIDATION',
710
- 'minScore must be a number between 0 and 1'
826
+ "VALIDATION",
827
+ "minScore must be a number between 0 and 1"
711
828
  );
712
829
  }
713
830
 
@@ -721,7 +838,7 @@ export async function handleSearch(
721
838
  const result = await searchBm25(store, query, options);
722
839
 
723
840
  if (!result.ok) {
724
- return errorResponse('RUNTIME', result.error.message, 500);
841
+ return errorResponse("RUNTIME", result.error.message, 500);
725
842
  }
726
843
 
727
844
  return jsonResponse(result.value);
@@ -740,36 +857,36 @@ export async function handleQuery(
740
857
  try {
741
858
  body = (await req.json()) as QueryRequestBody;
742
859
  } catch {
743
- return errorResponse('VALIDATION', 'Invalid JSON body');
860
+ return errorResponse("VALIDATION", "Invalid JSON body");
744
861
  }
745
862
 
746
- if (!body.query || typeof body.query !== 'string') {
747
- return errorResponse('VALIDATION', 'Missing or invalid query');
863
+ if (!body.query || typeof body.query !== "string") {
864
+ return errorResponse("VALIDATION", "Missing or invalid query");
748
865
  }
749
866
 
750
867
  const query = body.query.trim();
751
868
  if (!query) {
752
- return errorResponse('VALIDATION', 'Query cannot be empty');
869
+ return errorResponse("VALIDATION", "Query cannot be empty");
753
870
  }
754
871
 
755
872
  // Validate limit
756
873
  if (
757
874
  body.limit !== undefined &&
758
- (typeof body.limit !== 'number' || body.limit < 1)
875
+ (typeof body.limit !== "number" || body.limit < 1)
759
876
  ) {
760
- return errorResponse('VALIDATION', 'limit must be a positive integer');
877
+ return errorResponse("VALIDATION", "limit must be a positive integer");
761
878
  }
762
879
 
763
880
  // Validate minScore
764
881
  if (
765
882
  body.minScore !== undefined &&
766
- (typeof body.minScore !== 'number' ||
883
+ (typeof body.minScore !== "number" ||
767
884
  body.minScore < 0 ||
768
885
  body.minScore > 1)
769
886
  ) {
770
887
  return errorResponse(
771
- 'VALIDATION',
772
- 'minScore must be a number between 0 and 1'
888
+ "VALIDATION",
889
+ "minScore must be a number between 0 and 1"
773
890
  );
774
891
  }
775
892
 
@@ -794,7 +911,7 @@ export async function handleQuery(
794
911
  );
795
912
 
796
913
  if (!result.ok) {
797
- return errorResponse('RUNTIME', result.error.message, 500);
914
+ return errorResponse("RUNTIME", result.error.message, 500);
798
915
  }
799
916
 
800
917
  return jsonResponse(result.value);
@@ -813,23 +930,23 @@ export async function handleAsk(
813
930
  try {
814
931
  body = (await req.json()) as AskRequestBody;
815
932
  } catch {
816
- return errorResponse('VALIDATION', 'Invalid JSON body');
933
+ return errorResponse("VALIDATION", "Invalid JSON body");
817
934
  }
818
935
 
819
- if (!body.query || typeof body.query !== 'string') {
820
- return errorResponse('VALIDATION', 'Missing or invalid query');
936
+ if (!body.query || typeof body.query !== "string") {
937
+ return errorResponse("VALIDATION", "Missing or invalid query");
821
938
  }
822
939
 
823
940
  const query = body.query.trim();
824
941
  if (!query) {
825
- return errorResponse('VALIDATION', 'Query cannot be empty');
942
+ return errorResponse("VALIDATION", "Query cannot be empty");
826
943
  }
827
944
 
828
945
  // Check if answer generation is available
829
946
  if (!ctx.capabilities.answer) {
830
947
  return errorResponse(
831
- 'UNAVAILABLE',
832
- 'Answer generation not available. No generation model loaded.',
948
+ "UNAVAILABLE",
949
+ "Answer generation not available. No generation model loaded.",
833
950
  503
834
951
  );
835
952
  }
@@ -855,7 +972,7 @@ export async function handleAsk(
855
972
  );
856
973
 
857
974
  if (!searchResult.ok) {
858
- return errorResponse('RUNTIME', searchResult.error.message, 500);
975
+ return errorResponse("RUNTIME", searchResult.error.message, 500);
859
976
  }
860
977
 
861
978
  const results = searchResult.value.results;
@@ -884,8 +1001,8 @@ export async function handleAsk(
884
1001
 
885
1002
  const askResult: AskResult = {
886
1003
  query,
887
- mode: searchResult.value.meta.vectorsUsed ? 'hybrid' : 'bm25_only',
888
- queryLanguage: searchResult.value.meta.queryLanguage ?? 'und',
1004
+ mode: searchResult.value.meta.vectorsUsed ? "hybrid" : "bm25_only",
1005
+ queryLanguage: searchResult.value.meta.queryLanguage ?? "und",
889
1006
  answer,
890
1007
  citations,
891
1008
  results,
@@ -963,17 +1080,17 @@ export async function handleSetPreset(
963
1080
  try {
964
1081
  body = (await req.json()) as SetPresetRequestBody;
965
1082
  } catch {
966
- return errorResponse('VALIDATION', 'Invalid JSON body');
1083
+ return errorResponse("VALIDATION", "Invalid JSON body");
967
1084
  }
968
1085
 
969
- if (!body.presetId || typeof body.presetId !== 'string') {
970
- return errorResponse('VALIDATION', 'Missing or invalid presetId');
1086
+ if (!body.presetId || typeof body.presetId !== "string") {
1087
+ return errorResponse("VALIDATION", "Missing or invalid presetId");
971
1088
  }
972
1089
 
973
1090
  // Validate preset exists
974
1091
  const preset = getPreset(ctxHolder.config, body.presetId);
975
1092
  if (!preset) {
976
- return errorResponse('NOT_FOUND', `Unknown preset: ${body.presetId}`, 404);
1093
+ return errorResponse("NOT_FOUND", `Unknown preset: ${body.presetId}`, 404);
977
1094
  }
978
1095
 
979
1096
  // Update config with new active preset (use getModelConfig to get defaults)
@@ -994,7 +1111,7 @@ export async function handleSetPreset(
994
1111
  ctxHolder.config = newConfig;
995
1112
  } catch (e) {
996
1113
  return errorResponse(
997
- 'RUNTIME',
1114
+ "RUNTIME",
998
1115
  `Failed to reload context: ${e instanceof Error ? e.message : String(e)}`,
999
1116
  500
1000
1117
  );
@@ -1034,7 +1151,7 @@ export function handleModelStatus(): Response {
1034
1151
  export function handleModelPull(ctxHolder: ContextHolder): Response {
1035
1152
  // Don't start if already downloading
1036
1153
  if (downloadState.active) {
1037
- return errorResponse('CONFLICT', 'Download already in progress', 409);
1154
+ return errorResponse("CONFLICT", "Download already in progress", 409);
1038
1155
  }
1039
1156
 
1040
1157
  // Reset and start
@@ -1062,21 +1179,21 @@ export function handleModelPull(ctxHolder: ContextHolder): Response {
1062
1179
  } else {
1063
1180
  downloadState.failed.push({
1064
1181
  type: r.type,
1065
- error: r.error ?? 'Unknown error',
1182
+ error: r.error ?? "Unknown error",
1066
1183
  });
1067
1184
  }
1068
1185
  }
1069
1186
 
1070
1187
  // Reload context to pick up new models
1071
- console.log('Models downloaded, reloading context...');
1188
+ console.log("Models downloaded, reloading context...");
1072
1189
  try {
1073
1190
  ctxHolder.current = await reloadServerContext(
1074
1191
  ctxHolder.current,
1075
1192
  ctxHolder.config
1076
1193
  );
1077
- console.log('Context reloaded');
1194
+ console.log("Context reloaded");
1078
1195
  } catch (e) {
1079
- console.error('Failed to reload context:', e);
1196
+ console.error("Failed to reload context:", e);
1080
1197
  }
1081
1198
 
1082
1199
  downloadState.active = false;
@@ -1084,17 +1201,17 @@ export function handleModelPull(ctxHolder: ContextHolder): Response {
1084
1201
  downloadState.progress = null;
1085
1202
  })
1086
1203
  .catch((e) => {
1087
- console.error('Model download failed:', e);
1204
+ console.error("Model download failed:", e);
1088
1205
  downloadState.active = false;
1089
1206
  downloadState.failed.push({
1090
- type: downloadState.currentType ?? 'embed',
1207
+ type: downloadState.currentType ?? "embed",
1091
1208
  error: e instanceof Error ? e.message : String(e),
1092
1209
  });
1093
1210
  });
1094
1211
 
1095
1212
  return jsonResponse({
1096
1213
  started: true,
1097
- message: 'Download started. Poll /api/models/status for progress.',
1214
+ message: "Download started. Poll /api/models/status for progress.",
1098
1215
  });
1099
1216
  }
1100
1217
 
@@ -1109,7 +1226,7 @@ export function handleModelPull(ctxHolder: ContextHolder): Response {
1109
1226
  export function handleJob(jobId: string): Response {
1110
1227
  const status = getJobStatus(jobId);
1111
1228
  if (!status) {
1112
- return errorResponse('NOT_FOUND', 'Job not found or expired', 404);
1229
+ return errorResponse("NOT_FOUND", "Job not found or expired", 404);
1113
1230
  }
1114
1231
  return jsonResponse(status);
1115
1232
  }
@@ -1123,7 +1240,7 @@ export function handleJob(jobId: string): Response {
1123
1240
  * Returns null if the path is not an API route.
1124
1241
  * Note: Currently unused since we use routes object in Bun.serve().
1125
1242
  */
1126
- // biome-ignore lint/suspicious/useAwait: handlers are async, kept for potential future use
1243
+ // oxlint-disable-next-line typescript-eslint/require-await -- handlers are async, kept for future use
1127
1244
  export async function routeApi(
1128
1245
  store: SqliteAdapter,
1129
1246
  req: Request,
@@ -1132,63 +1249,63 @@ export async function routeApi(
1132
1249
  const path = url.pathname;
1133
1250
 
1134
1251
  // CSRF protection: validate Origin for non-GET requests
1135
- if (req.method !== 'GET' && req.method !== 'HEAD') {
1136
- const origin = req.headers.get('origin');
1137
- const secFetchSite = req.headers.get('sec-fetch-site');
1252
+ if (req.method !== "GET" && req.method !== "HEAD") {
1253
+ const origin = req.headers.get("origin");
1254
+ const secFetchSite = req.headers.get("sec-fetch-site");
1138
1255
 
1139
1256
  // Reject cross-origin requests (allow same-origin or no origin for curl)
1140
1257
  if (origin) {
1141
1258
  const originUrl = new URL(origin);
1142
1259
  if (
1143
- originUrl.hostname !== '127.0.0.1' &&
1144
- originUrl.hostname !== 'localhost'
1260
+ originUrl.hostname !== "127.0.0.1" &&
1261
+ originUrl.hostname !== "localhost"
1145
1262
  ) {
1146
1263
  return errorResponse(
1147
- 'FORBIDDEN',
1148
- 'Cross-origin requests not allowed',
1264
+ "FORBIDDEN",
1265
+ "Cross-origin requests not allowed",
1149
1266
  403
1150
1267
  );
1151
1268
  }
1152
1269
  } else if (
1153
1270
  secFetchSite &&
1154
- secFetchSite !== 'same-origin' &&
1155
- secFetchSite !== 'none'
1271
+ secFetchSite !== "same-origin" &&
1272
+ secFetchSite !== "none"
1156
1273
  ) {
1157
1274
  return errorResponse(
1158
- 'FORBIDDEN',
1159
- 'Cross-origin requests not allowed',
1275
+ "FORBIDDEN",
1276
+ "Cross-origin requests not allowed",
1160
1277
  403
1161
1278
  );
1162
1279
  }
1163
1280
  }
1164
1281
 
1165
- if (path === '/api/health') {
1282
+ if (path === "/api/health") {
1166
1283
  return handleHealth();
1167
1284
  }
1168
1285
 
1169
- if (path === '/api/status') {
1286
+ if (path === "/api/status") {
1170
1287
  return handleStatus(store);
1171
1288
  }
1172
1289
 
1173
- if (path === '/api/collections') {
1290
+ if (path === "/api/collections") {
1174
1291
  return handleCollections(store);
1175
1292
  }
1176
1293
 
1177
- if (path === '/api/docs') {
1294
+ if (path === "/api/docs") {
1178
1295
  return handleDocs(store, url);
1179
1296
  }
1180
1297
 
1181
- if (path === '/api/doc') {
1298
+ if (path === "/api/doc") {
1182
1299
  return handleDoc(store, url);
1183
1300
  }
1184
1301
 
1185
- if (path === '/api/search' && req.method === 'POST') {
1302
+ if (path === "/api/search" && req.method === "POST") {
1186
1303
  return handleSearch(store, req);
1187
1304
  }
1188
1305
 
1189
1306
  // Unknown API route
1190
- if (path.startsWith('/api/')) {
1191
- return errorResponse('NOT_FOUND', `Unknown API endpoint: ${path}`, 404);
1307
+ if (path.startsWith("/api/")) {
1308
+ return errorResponse("NOT_FOUND", `Unknown API endpoint: ${path}`, 404);
1192
1309
  }
1193
1310
 
1194
1311
  return null;