@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.
- package/LICENSE +21 -0
- package/README.md +90 -50
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/assets/screenshots/webui-ask-answer.png +0 -0
- package/assets/screenshots/webui-collections.png +0 -0
- package/assets/screenshots/webui-editor.png +0 -0
- package/assets/screenshots/webui-home.png +0 -0
- package/assets/skill/SKILL.md +12 -12
- package/assets/skill/cli-reference.md +59 -57
- package/assets/skill/examples.md +8 -7
- package/assets/skill/mcp-reference.md +8 -4
- package/package.json +31 -24
- package/src/app/constants.ts +43 -42
- package/src/cli/colors.ts +1 -1
- package/src/cli/commands/ask.ts +44 -43
- package/src/cli/commands/cleanup.ts +9 -8
- package/src/cli/commands/collection/add.ts +12 -12
- package/src/cli/commands/collection/index.ts +4 -4
- package/src/cli/commands/collection/list.ts +26 -25
- package/src/cli/commands/collection/remove.ts +10 -10
- package/src/cli/commands/collection/rename.ts +10 -10
- package/src/cli/commands/context/add.ts +1 -1
- package/src/cli/commands/context/check.ts +17 -17
- package/src/cli/commands/context/index.ts +4 -4
- package/src/cli/commands/context/list.ts +11 -11
- package/src/cli/commands/context/rm.ts +1 -1
- package/src/cli/commands/doctor.ts +86 -84
- package/src/cli/commands/embed.ts +30 -28
- package/src/cli/commands/get.ts +27 -26
- package/src/cli/commands/index-cmd.ts +9 -9
- package/src/cli/commands/index.ts +16 -16
- package/src/cli/commands/init.ts +13 -12
- package/src/cli/commands/ls.ts +20 -19
- package/src/cli/commands/mcp/config.ts +30 -28
- package/src/cli/commands/mcp/index.ts +4 -4
- package/src/cli/commands/mcp/install.ts +17 -17
- package/src/cli/commands/mcp/paths.ts +133 -133
- package/src/cli/commands/mcp/status.ts +21 -21
- package/src/cli/commands/mcp/uninstall.ts +13 -13
- package/src/cli/commands/mcp.ts +2 -2
- package/src/cli/commands/models/clear.ts +12 -11
- package/src/cli/commands/models/index.ts +5 -5
- package/src/cli/commands/models/list.ts +31 -30
- package/src/cli/commands/models/path.ts +1 -1
- package/src/cli/commands/models/pull.ts +19 -18
- package/src/cli/commands/models/use.ts +4 -4
- package/src/cli/commands/multi-get.ts +38 -36
- package/src/cli/commands/query.ts +21 -20
- package/src/cli/commands/ref-parser.ts +10 -10
- package/src/cli/commands/reset.ts +40 -39
- package/src/cli/commands/search.ts +14 -13
- package/src/cli/commands/serve.ts +4 -4
- package/src/cli/commands/shared.ts +11 -10
- package/src/cli/commands/skill/index.ts +5 -5
- package/src/cli/commands/skill/install.ts +18 -17
- package/src/cli/commands/skill/paths-cmd.ts +11 -10
- package/src/cli/commands/skill/paths.ts +23 -23
- package/src/cli/commands/skill/show.ts +13 -12
- package/src/cli/commands/skill/uninstall.ts +16 -15
- package/src/cli/commands/status.ts +25 -24
- package/src/cli/commands/update.ts +3 -3
- package/src/cli/commands/vsearch.ts +17 -16
- package/src/cli/context.ts +5 -5
- package/src/cli/errors.ts +3 -3
- package/src/cli/format/search-results.ts +37 -37
- package/src/cli/options.ts +43 -43
- package/src/cli/program.ts +455 -459
- package/src/cli/progress.ts +1 -1
- package/src/cli/run.ts +24 -23
- package/src/collection/add.ts +9 -8
- package/src/collection/index.ts +3 -3
- package/src/collection/remove.ts +7 -6
- package/src/collection/types.ts +6 -6
- package/src/config/defaults.ts +1 -1
- package/src/config/index.ts +5 -5
- package/src/config/loader.ts +19 -18
- package/src/config/paths.ts +9 -8
- package/src/config/saver.ts +14 -13
- package/src/config/types.ts +53 -52
- package/src/converters/adapters/markitdownTs/adapter.ts +21 -19
- package/src/converters/adapters/officeparser/adapter.ts +18 -16
- package/src/converters/canonicalize.ts +12 -12
- package/src/converters/errors.ts +26 -22
- package/src/converters/index.ts +8 -8
- package/src/converters/mime.ts +25 -25
- package/src/converters/native/markdown.ts +10 -9
- package/src/converters/native/plaintext.ts +8 -7
- package/src/converters/path.ts +2 -2
- package/src/converters/pipeline.ts +11 -10
- package/src/converters/registry.ts +8 -8
- package/src/converters/types.ts +14 -14
- package/src/converters/versions.ts +4 -4
- package/src/index.ts +4 -4
- package/src/ingestion/chunker.ts +10 -9
- package/src/ingestion/index.ts +6 -6
- package/src/ingestion/language.ts +62 -62
- package/src/ingestion/sync.ts +50 -49
- package/src/ingestion/types.ts +10 -10
- package/src/ingestion/walker.ts +14 -13
- package/src/llm/cache.ts +51 -49
- package/src/llm/errors.ts +40 -36
- package/src/llm/index.ts +9 -9
- package/src/llm/lockfile.ts +6 -6
- package/src/llm/nodeLlamaCpp/adapter.ts +13 -12
- package/src/llm/nodeLlamaCpp/embedding.ts +9 -8
- package/src/llm/nodeLlamaCpp/generation.ts +7 -6
- package/src/llm/nodeLlamaCpp/lifecycle.ts +11 -10
- package/src/llm/nodeLlamaCpp/rerank.ts +6 -5
- package/src/llm/policy.ts +5 -5
- package/src/llm/registry.ts +6 -5
- package/src/llm/types.ts +2 -2
- package/src/mcp/resources/index.ts +15 -13
- package/src/mcp/server.ts +25 -23
- package/src/mcp/tools/get.ts +25 -23
- package/src/mcp/tools/index.ts +32 -29
- package/src/mcp/tools/multi-get.ts +34 -32
- package/src/mcp/tools/query.ts +29 -27
- package/src/mcp/tools/search.ts +14 -12
- package/src/mcp/tools/status.ts +12 -11
- package/src/mcp/tools/vsearch.ts +26 -24
- package/src/pipeline/answer.ts +9 -9
- package/src/pipeline/chunk-lookup.ts +1 -1
- package/src/pipeline/contextual.ts +4 -4
- package/src/pipeline/expansion.ts +23 -21
- package/src/pipeline/explain.ts +21 -21
- package/src/pipeline/fusion.ts +9 -9
- package/src/pipeline/hybrid.ts +41 -42
- package/src/pipeline/index.ts +10 -10
- package/src/pipeline/query-language.ts +39 -39
- package/src/pipeline/rerank.ts +8 -7
- package/src/pipeline/search.ts +22 -22
- package/src/pipeline/types.ts +8 -8
- package/src/pipeline/vsearch.ts +21 -24
- package/src/serve/CLAUDE.md +21 -15
- package/src/serve/config-sync.ts +9 -8
- package/src/serve/context.ts +19 -18
- package/src/serve/index.ts +1 -1
- package/src/serve/jobs.ts +7 -7
- package/src/serve/public/app.tsx +79 -25
- package/src/serve/public/components/AddCollectionDialog.tsx +382 -0
- package/src/serve/public/components/CaptureButton.tsx +60 -0
- package/src/serve/public/components/CaptureModal.tsx +365 -0
- package/src/serve/public/components/IndexingProgress.tsx +333 -0
- package/src/serve/public/components/ShortcutHelpModal.tsx +106 -0
- package/src/serve/public/components/ai-elements/code-block.tsx +42 -32
- package/src/serve/public/components/ai-elements/conversation.tsx +16 -14
- package/src/serve/public/components/ai-elements/inline-citation.tsx +33 -32
- package/src/serve/public/components/ai-elements/loader.tsx +5 -4
- package/src/serve/public/components/ai-elements/message.tsx +39 -37
- package/src/serve/public/components/ai-elements/prompt-input.tsx +97 -95
- package/src/serve/public/components/ai-elements/sources.tsx +12 -10
- package/src/serve/public/components/ai-elements/suggestion.tsx +10 -9
- package/src/serve/public/components/editor/CodeMirrorEditor.tsx +142 -0
- package/src/serve/public/components/editor/MarkdownPreview.tsx +311 -0
- package/src/serve/public/components/editor/index.ts +6 -0
- package/src/serve/public/components/preset-selector.tsx +29 -28
- package/src/serve/public/components/ui/badge.tsx +13 -12
- package/src/serve/public/components/ui/button-group.tsx +13 -12
- package/src/serve/public/components/ui/button.tsx +23 -22
- package/src/serve/public/components/ui/card.tsx +16 -16
- package/src/serve/public/components/ui/carousel.tsx +36 -35
- package/src/serve/public/components/ui/collapsible.tsx +1 -1
- package/src/serve/public/components/ui/command.tsx +17 -15
- package/src/serve/public/components/ui/dialog.tsx +13 -12
- package/src/serve/public/components/ui/dropdown-menu.tsx +13 -12
- package/src/serve/public/components/ui/hover-card.tsx +6 -5
- package/src/serve/public/components/ui/input-group.tsx +45 -43
- package/src/serve/public/components/ui/input.tsx +6 -6
- package/src/serve/public/components/ui/progress.tsx +5 -4
- package/src/serve/public/components/ui/scroll-area.tsx +11 -10
- package/src/serve/public/components/ui/select.tsx +19 -18
- package/src/serve/public/components/ui/separator.tsx +6 -5
- package/src/serve/public/components/ui/table.tsx +18 -18
- package/src/serve/public/components/ui/textarea.tsx +4 -4
- package/src/serve/public/components/ui/tooltip.tsx +5 -4
- package/src/serve/public/globals.css +27 -4
- package/src/serve/public/hooks/use-api.ts +8 -8
- package/src/serve/public/hooks/useCaptureModal.tsx +83 -0
- package/src/serve/public/hooks/useKeyboardShortcuts.ts +85 -0
- package/src/serve/public/index.html +4 -4
- package/src/serve/public/lib/utils.ts +6 -0
- package/src/serve/public/pages/Ask.tsx +27 -26
- package/src/serve/public/pages/Browse.tsx +28 -27
- package/src/serve/public/pages/Collections.tsx +439 -0
- package/src/serve/public/pages/Dashboard.tsx +166 -40
- package/src/serve/public/pages/DocView.tsx +258 -73
- package/src/serve/public/pages/DocumentEditor.tsx +510 -0
- package/src/serve/public/pages/Search.tsx +80 -58
- package/src/serve/routes/api.ts +272 -155
- package/src/serve/security.ts +4 -4
- package/src/serve/server.ts +66 -48
- package/src/store/index.ts +5 -5
- package/src/store/migrations/001-initial.ts +24 -23
- package/src/store/migrations/002-documents-fts.ts +7 -6
- package/src/store/migrations/index.ts +4 -4
- package/src/store/migrations/runner.ts +17 -15
- package/src/store/sqlite/adapter.ts +123 -121
- package/src/store/sqlite/fts5-snowball.ts +24 -23
- package/src/store/sqlite/index.ts +1 -1
- package/src/store/sqlite/setup.ts +12 -12
- package/src/store/sqlite/types.ts +4 -4
- package/src/store/types.ts +19 -19
- package/src/store/vector/index.ts +3 -3
- package/src/store/vector/sqlite-vec.ts +23 -20
- package/src/store/vector/stats.ts +10 -8
- package/src/store/vector/types.ts +2 -2
- package/vendor/fts5-snowball/README.md +6 -6
- package/assets/screenshots/webui-ask-answer.jpg +0 -0
- package/assets/screenshots/webui-home.jpg +0 -0
package/src/serve/routes/api.ts
CHANGED
|
@@ -5,27 +5,28 @@
|
|
|
5
5
|
* @module src/serve/routes/api
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import type {
|
|
11
|
-
|
|
12
|
-
import {
|
|
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
|
|
17
|
-
import { searchHybrid } from
|
|
18
|
-
import { searchBm25 } from
|
|
19
|
-
import
|
|
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
|
|
28
|
-
import { getJobStatus, startJob } from
|
|
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(
|
|
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(
|
|
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(
|
|
186
|
+
return errorResponse("VALIDATION", "Invalid JSON body");
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
// Validate required fields
|
|
185
|
-
if (!body.path || typeof body.path !==
|
|
186
|
-
return errorResponse(
|
|
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 !==
|
|
191
|
-
return errorResponse(
|
|
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 !==
|
|
194
|
-
return errorResponse(
|
|
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 !==
|
|
203
|
+
typeof body.include !== "string" &&
|
|
199
204
|
!Array.isArray(body.include)
|
|
200
205
|
) {
|
|
201
|
-
return errorResponse(
|
|
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 !==
|
|
210
|
+
typeof body.exclude !== "string" &&
|
|
206
211
|
!Array.isArray(body.exclude)
|
|
207
212
|
) {
|
|
208
|
-
return errorResponse(
|
|
213
|
+
return errorResponse("VALIDATION", "exclude must be a string or array");
|
|
209
214
|
}
|
|
210
|
-
if (body.gitPull !== undefined && typeof body.gitPull !==
|
|
211
|
-
return errorResponse(
|
|
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
|
|
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(
|
|
251
|
+
return errorResponse("RUNTIME", "Collection not found after add", 500);
|
|
247
252
|
}
|
|
248
|
-
const jobResult = startJob(
|
|
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(
|
|
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:
|
|
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(
|
|
340
|
+
return errorResponse("VALIDATION", "Invalid JSON body");
|
|
336
341
|
}
|
|
337
342
|
|
|
338
343
|
// Validate optional fields
|
|
339
|
-
if (body.collection !== undefined && typeof body.collection !==
|
|
340
|
-
return errorResponse(
|
|
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 !==
|
|
343
|
-
return errorResponse(
|
|
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
|
-
|
|
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(
|
|
368
|
+
return errorResponse("VALIDATION", "No collections to sync");
|
|
364
369
|
}
|
|
365
370
|
|
|
366
371
|
// Start background sync job
|
|
367
|
-
const jobResult = startJob(
|
|
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(
|
|
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(
|
|
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(
|
|
398
|
+
const limitParam = Number(url.searchParams.get("limit"));
|
|
394
399
|
if (
|
|
395
|
-
url.searchParams.has(
|
|
400
|
+
url.searchParams.has("limit") &&
|
|
396
401
|
(Number.isNaN(limitParam) || limitParam < 1)
|
|
397
402
|
) {
|
|
398
|
-
return errorResponse(
|
|
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(
|
|
408
|
+
const offsetParam = Number(url.searchParams.get("offset"));
|
|
404
409
|
if (
|
|
405
|
-
url.searchParams.has(
|
|
410
|
+
url.searchParams.has("offset") &&
|
|
406
411
|
(Number.isNaN(offsetParam) || offsetParam < 0)
|
|
407
412
|
) {
|
|
408
|
-
return errorResponse(
|
|
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(
|
|
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(
|
|
455
|
+
const uri = url.searchParams.get("uri");
|
|
451
456
|
if (!uri) {
|
|
452
|
-
return errorResponse(
|
|
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(
|
|
462
|
+
return errorResponse("RUNTIME", docResult.error.message, 500);
|
|
458
463
|
}
|
|
459
464
|
if (!docResult.value) {
|
|
460
|
-
return errorResponse(
|
|
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(
|
|
506
|
+
return errorResponse("RUNTIME", docResult.error.message, 500);
|
|
502
507
|
}
|
|
503
508
|
if (!docResult.value) {
|
|
504
|
-
return errorResponse(
|
|
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(
|
|
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:
|
|
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
|
|
531
|
-
if (isAbsolute(relPath)) {
|
|
532
|
-
return { error:
|
|
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(
|
|
535
|
-
return { error:
|
|
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:
|
|
651
|
+
const normalizedPath = nodePath.normalize(relPath);
|
|
652
|
+
if (normalizedPath.startsWith("..")) {
|
|
653
|
+
return { error: "relPath cannot escape collection root" };
|
|
540
654
|
}
|
|
541
|
-
return { fullPath:
|
|
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(
|
|
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 !==
|
|
563
|
-
return errorResponse(
|
|
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 !==
|
|
566
|
-
return errorResponse(
|
|
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 !==
|
|
569
|
-
return errorResponse(
|
|
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 !==
|
|
572
|
-
return errorResponse(
|
|
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
|
-
|
|
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 (
|
|
591
|
-
return errorResponse(
|
|
704
|
+
if ("error" in pathValidation) {
|
|
705
|
+
return errorResponse("VALIDATION", pathValidation.error);
|
|
592
706
|
}
|
|
593
707
|
|
|
594
|
-
const
|
|
595
|
-
const fullPath = join(
|
|
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
|
-
|
|
603
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
?
|
|
655
|
-
:
|
|
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
|
-
|
|
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(
|
|
798
|
+
return errorResponse("VALIDATION", "Invalid JSON body");
|
|
682
799
|
}
|
|
683
800
|
|
|
684
|
-
if (!body.query || typeof body.query !==
|
|
685
|
-
return errorResponse(
|
|
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(
|
|
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 !==
|
|
813
|
+
(typeof body.limit !== "number" || body.limit < 1)
|
|
697
814
|
) {
|
|
698
|
-
return errorResponse(
|
|
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 !==
|
|
821
|
+
(typeof body.minScore !== "number" ||
|
|
705
822
|
body.minScore < 0 ||
|
|
706
823
|
body.minScore > 1)
|
|
707
824
|
) {
|
|
708
825
|
return errorResponse(
|
|
709
|
-
|
|
710
|
-
|
|
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(
|
|
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(
|
|
860
|
+
return errorResponse("VALIDATION", "Invalid JSON body");
|
|
744
861
|
}
|
|
745
862
|
|
|
746
|
-
if (!body.query || typeof body.query !==
|
|
747
|
-
return errorResponse(
|
|
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(
|
|
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 !==
|
|
875
|
+
(typeof body.limit !== "number" || body.limit < 1)
|
|
759
876
|
) {
|
|
760
|
-
return errorResponse(
|
|
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 !==
|
|
883
|
+
(typeof body.minScore !== "number" ||
|
|
767
884
|
body.minScore < 0 ||
|
|
768
885
|
body.minScore > 1)
|
|
769
886
|
) {
|
|
770
887
|
return errorResponse(
|
|
771
|
-
|
|
772
|
-
|
|
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(
|
|
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(
|
|
933
|
+
return errorResponse("VALIDATION", "Invalid JSON body");
|
|
817
934
|
}
|
|
818
935
|
|
|
819
|
-
if (!body.query || typeof body.query !==
|
|
820
|
-
return errorResponse(
|
|
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(
|
|
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
|
-
|
|
832
|
-
|
|
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(
|
|
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 ?
|
|
888
|
-
queryLanguage: searchResult.value.meta.queryLanguage ??
|
|
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(
|
|
1083
|
+
return errorResponse("VALIDATION", "Invalid JSON body");
|
|
967
1084
|
}
|
|
968
1085
|
|
|
969
|
-
if (!body.presetId || typeof body.presetId !==
|
|
970
|
-
return errorResponse(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 ??
|
|
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(
|
|
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(
|
|
1194
|
+
console.log("Context reloaded");
|
|
1078
1195
|
} catch (e) {
|
|
1079
|
-
console.error(
|
|
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(
|
|
1204
|
+
console.error("Model download failed:", e);
|
|
1088
1205
|
downloadState.active = false;
|
|
1089
1206
|
downloadState.failed.push({
|
|
1090
|
-
type: downloadState.currentType ??
|
|
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:
|
|
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(
|
|
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
|
-
//
|
|
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 !==
|
|
1136
|
-
const origin = req.headers.get(
|
|
1137
|
-
const secFetchSite = req.headers.get(
|
|
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 !==
|
|
1144
|
-
originUrl.hostname !==
|
|
1260
|
+
originUrl.hostname !== "127.0.0.1" &&
|
|
1261
|
+
originUrl.hostname !== "localhost"
|
|
1145
1262
|
) {
|
|
1146
1263
|
return errorResponse(
|
|
1147
|
-
|
|
1148
|
-
|
|
1264
|
+
"FORBIDDEN",
|
|
1265
|
+
"Cross-origin requests not allowed",
|
|
1149
1266
|
403
|
|
1150
1267
|
);
|
|
1151
1268
|
}
|
|
1152
1269
|
} else if (
|
|
1153
1270
|
secFetchSite &&
|
|
1154
|
-
secFetchSite !==
|
|
1155
|
-
secFetchSite !==
|
|
1271
|
+
secFetchSite !== "same-origin" &&
|
|
1272
|
+
secFetchSite !== "none"
|
|
1156
1273
|
) {
|
|
1157
1274
|
return errorResponse(
|
|
1158
|
-
|
|
1159
|
-
|
|
1275
|
+
"FORBIDDEN",
|
|
1276
|
+
"Cross-origin requests not allowed",
|
|
1160
1277
|
403
|
|
1161
1278
|
);
|
|
1162
1279
|
}
|
|
1163
1280
|
}
|
|
1164
1281
|
|
|
1165
|
-
if (path ===
|
|
1282
|
+
if (path === "/api/health") {
|
|
1166
1283
|
return handleHealth();
|
|
1167
1284
|
}
|
|
1168
1285
|
|
|
1169
|
-
if (path ===
|
|
1286
|
+
if (path === "/api/status") {
|
|
1170
1287
|
return handleStatus(store);
|
|
1171
1288
|
}
|
|
1172
1289
|
|
|
1173
|
-
if (path ===
|
|
1290
|
+
if (path === "/api/collections") {
|
|
1174
1291
|
return handleCollections(store);
|
|
1175
1292
|
}
|
|
1176
1293
|
|
|
1177
|
-
if (path ===
|
|
1294
|
+
if (path === "/api/docs") {
|
|
1178
1295
|
return handleDocs(store, url);
|
|
1179
1296
|
}
|
|
1180
1297
|
|
|
1181
|
-
if (path ===
|
|
1298
|
+
if (path === "/api/doc") {
|
|
1182
1299
|
return handleDoc(store, url);
|
|
1183
1300
|
}
|
|
1184
1301
|
|
|
1185
|
-
if (path ===
|
|
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(
|
|
1191
|
-
return errorResponse(
|
|
1307
|
+
if (path.startsWith("/api/")) {
|
|
1308
|
+
return errorResponse("NOT_FOUND", `Unknown API endpoint: ${path}`, 404);
|
|
1192
1309
|
}
|
|
1193
1310
|
|
|
1194
1311
|
return null;
|