@gmickel/gno 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1
- package/package.json +4 -1
- package/src/cli/commands/ask.ts +9 -0
- package/src/cli/commands/query.ts +3 -2
- package/src/cli/pager.ts +1 -1
- package/src/cli/program.ts +89 -0
- package/src/core/links.ts +92 -20
- package/src/ingestion/sync.ts +267 -23
- package/src/ingestion/types.ts +2 -0
- package/src/ingestion/walker.ts +2 -1
- package/src/mcp/tools/index.ts +30 -1
- package/src/mcp/tools/query.ts +22 -2
- package/src/mcp/tools/search.ts +8 -0
- package/src/mcp/tools/vsearch.ts +8 -0
- package/src/pipeline/answer.ts +324 -7
- package/src/pipeline/expansion.ts +243 -7
- package/src/pipeline/explain.ts +93 -5
- package/src/pipeline/hybrid.ts +240 -57
- package/src/pipeline/query-modes.ts +125 -0
- package/src/pipeline/rerank.ts +34 -13
- package/src/pipeline/search.ts +41 -3
- package/src/pipeline/temporal.ts +257 -0
- package/src/pipeline/types.ts +58 -0
- package/src/pipeline/vsearch.ts +107 -9
- package/src/serve/public/app.tsx +1 -3
- package/src/serve/public/globals.built.css +2 -2
- package/src/serve/public/lib/retrieval-filters.ts +167 -0
- package/src/serve/public/pages/Ask.tsx +339 -109
- package/src/serve/public/pages/Browse.tsx +71 -5
- package/src/serve/public/pages/DocView.tsx +2 -21
- package/src/serve/public/pages/Search.tsx +507 -120
- package/src/serve/routes/api.ts +202 -2
- package/src/store/migrations/006-document-metadata.ts +104 -0
- package/src/store/migrations/007-document-date-fields.ts +24 -0
- package/src/store/migrations/index.ts +3 -1
- package/src/store/sqlite/adapter.ts +218 -5
- package/src/store/types.ts +46 -0
package/src/ingestion/sync.ts
CHANGED
|
@@ -67,7 +67,7 @@ const MAX_CONCURRENCY = 16;
|
|
|
67
67
|
* Increment when ingestion adds new derived data (tags, metadata, etc.)
|
|
68
68
|
* Documents with ingestVersion < INGEST_VERSION will be re-processed.
|
|
69
69
|
*/
|
|
70
|
-
export const INGEST_VERSION =
|
|
70
|
+
export const INGEST_VERSION = 5;
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Decide whether to process a file or skip it.
|
|
@@ -141,6 +141,171 @@ function extractTags(markdown: string): string[] {
|
|
|
141
141
|
return [...tags];
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
interface DocumentMetadata {
|
|
145
|
+
contentType?: string;
|
|
146
|
+
categories?: string[];
|
|
147
|
+
author?: string;
|
|
148
|
+
frontmatterDate?: string;
|
|
149
|
+
dateFields?: Record<string, string>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const CODE_EXTENSIONS = new Set([
|
|
153
|
+
".c",
|
|
154
|
+
".cc",
|
|
155
|
+
".cpp",
|
|
156
|
+
".cs",
|
|
157
|
+
".go",
|
|
158
|
+
".java",
|
|
159
|
+
".js",
|
|
160
|
+
".jsx",
|
|
161
|
+
".m",
|
|
162
|
+
".mm",
|
|
163
|
+
".php",
|
|
164
|
+
".py",
|
|
165
|
+
".rb",
|
|
166
|
+
".rs",
|
|
167
|
+
".swift",
|
|
168
|
+
".ts",
|
|
169
|
+
".tsx",
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
const AUTHOR_KEYS = ["author", "by", "owner", "creator"] as const;
|
|
173
|
+
const DATE_KEYS = [
|
|
174
|
+
"date",
|
|
175
|
+
"published",
|
|
176
|
+
"published_at",
|
|
177
|
+
"created",
|
|
178
|
+
"created_at",
|
|
179
|
+
"updated",
|
|
180
|
+
"updated_at",
|
|
181
|
+
] as const;
|
|
182
|
+
const DATE_FIELD_KEY_REGEX =
|
|
183
|
+
/(^|_)(date|time|created|updated|published|modified|deadline|expires|expiry|start|end)(_|$)/;
|
|
184
|
+
|
|
185
|
+
function normalizeMetadataKey(rawKey: string): string {
|
|
186
|
+
return rawKey
|
|
187
|
+
.trim()
|
|
188
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
189
|
+
.toLowerCase()
|
|
190
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
191
|
+
.replace(/^_+|_+$/g, "")
|
|
192
|
+
.replace(/_+/g, "_");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeDate(value: unknown): string | undefined {
|
|
196
|
+
if (value instanceof Date) {
|
|
197
|
+
return Number.isNaN(value.getTime()) ? undefined : value.toISOString();
|
|
198
|
+
}
|
|
199
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
const normalizedValue =
|
|
203
|
+
typeof value === "string"
|
|
204
|
+
? value.trim().replace(/^["'](.*)["']$/, "$1")
|
|
205
|
+
: value;
|
|
206
|
+
const parsed = new Date(normalizedValue);
|
|
207
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return parsed.toISOString();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function inferContentType(relPath: string, ext: string): string {
|
|
214
|
+
const lowerPath = relPath.toLowerCase();
|
|
215
|
+
if (CODE_EXTENSIONS.has(ext.toLowerCase())) {
|
|
216
|
+
return "code";
|
|
217
|
+
}
|
|
218
|
+
if (/(meeting|standup|retro|minutes)/.test(lowerPath)) {
|
|
219
|
+
return "meeting";
|
|
220
|
+
}
|
|
221
|
+
if (/(spec|rfc|adr|design)/.test(lowerPath)) {
|
|
222
|
+
return "spec";
|
|
223
|
+
}
|
|
224
|
+
if (/(notes|journal|log)/.test(lowerPath)) {
|
|
225
|
+
return "notes";
|
|
226
|
+
}
|
|
227
|
+
return "prose";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseCategories(input: unknown): string[] {
|
|
231
|
+
if (Array.isArray(input)) {
|
|
232
|
+
return input
|
|
233
|
+
.filter((v): v is string => typeof v === "string")
|
|
234
|
+
.map((v) => v.trim().toLowerCase())
|
|
235
|
+
.filter((v) => v.length > 0);
|
|
236
|
+
}
|
|
237
|
+
if (typeof input === "string") {
|
|
238
|
+
return input
|
|
239
|
+
.split(",")
|
|
240
|
+
.map((v) => v.trim().toLowerCase())
|
|
241
|
+
.filter((v) => v.length > 0);
|
|
242
|
+
}
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function extractDocumentMetadata(
|
|
247
|
+
markdown: string,
|
|
248
|
+
relPath: string,
|
|
249
|
+
ext: string
|
|
250
|
+
): DocumentMetadata {
|
|
251
|
+
const parsed = parseFrontmatter(markdown);
|
|
252
|
+
const metadata = parsed.metadata;
|
|
253
|
+
const contentType = inferContentType(relPath, ext);
|
|
254
|
+
const categories = new Set<string>([contentType]);
|
|
255
|
+
|
|
256
|
+
const fmCategories = parseCategories(
|
|
257
|
+
metadata.category ?? metadata.categories ?? metadata.type
|
|
258
|
+
);
|
|
259
|
+
for (const category of fmCategories) {
|
|
260
|
+
categories.add(category);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let author: string | undefined;
|
|
264
|
+
for (const key of AUTHOR_KEYS) {
|
|
265
|
+
const value = metadata[key];
|
|
266
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
267
|
+
author = value.trim();
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const normalizedMetadata = new Map<string, unknown>();
|
|
273
|
+
for (const [rawKey, value] of Object.entries(metadata)) {
|
|
274
|
+
const key = normalizeMetadataKey(rawKey);
|
|
275
|
+
if (key.length > 0 && !normalizedMetadata.has(key)) {
|
|
276
|
+
normalizedMetadata.set(key, value);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let frontmatterDate: string | undefined;
|
|
281
|
+
for (const key of DATE_KEYS) {
|
|
282
|
+
const normalized = normalizeDate(normalizedMetadata.get(key));
|
|
283
|
+
if (normalized) {
|
|
284
|
+
frontmatterDate = normalized;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const dateFields: Record<string, string> = {};
|
|
290
|
+
for (const [key, value] of normalizedMetadata.entries()) {
|
|
291
|
+
if (!DATE_FIELD_KEY_REGEX.test(key)) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const normalized = normalizeDate(value);
|
|
295
|
+
if (normalized) {
|
|
296
|
+
dateFields[key] = normalized;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
contentType,
|
|
302
|
+
categories: [...categories],
|
|
303
|
+
author,
|
|
304
|
+
frontmatterDate,
|
|
305
|
+
dateFields: Object.keys(dateFields).length > 0 ? dateFields : undefined,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
144
309
|
/**
|
|
145
310
|
* Check if path is a git repository (supports worktrees and submodules).
|
|
146
311
|
* Uses git rev-parse which handles all git directory layouts.
|
|
@@ -268,15 +433,63 @@ export class SyncService {
|
|
|
268
433
|
};
|
|
269
434
|
|
|
270
435
|
try {
|
|
271
|
-
// 1.
|
|
436
|
+
// 1. Re-stat before read to enforce maxBytes on current file size
|
|
437
|
+
let sourceSize = entry.size;
|
|
438
|
+
let sourceMtime = entry.mtime;
|
|
439
|
+
let sourceCtime = entry.ctime;
|
|
440
|
+
try {
|
|
441
|
+
const sourceStat = await stat(entry.absPath);
|
|
442
|
+
if (!sourceStat.isFile()) {
|
|
443
|
+
return {
|
|
444
|
+
relPath: entry.relPath,
|
|
445
|
+
status: "error",
|
|
446
|
+
errorCode: "NOT_FILE",
|
|
447
|
+
errorMessage: "Path is not a file",
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
sourceSize = sourceStat.size;
|
|
451
|
+
sourceMtime = sourceStat.mtime.toISOString();
|
|
452
|
+
sourceCtime = (
|
|
453
|
+
sourceStat.birthtime ??
|
|
454
|
+
sourceStat.ctime ??
|
|
455
|
+
sourceStat.mtime
|
|
456
|
+
).toISOString();
|
|
457
|
+
} catch {
|
|
458
|
+
return {
|
|
459
|
+
relPath: entry.relPath,
|
|
460
|
+
status: "error",
|
|
461
|
+
errorCode: "NOT_FOUND",
|
|
462
|
+
errorMessage: "File not found",
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (sourceSize > limits.maxBytes) {
|
|
467
|
+
const message = `File size ${sourceSize} exceeds limit ${limits.maxBytes}`;
|
|
468
|
+
await store
|
|
469
|
+
.recordError({
|
|
470
|
+
collection: collection.name,
|
|
471
|
+
relPath: entry.relPath,
|
|
472
|
+
code: "TOO_LARGE",
|
|
473
|
+
message,
|
|
474
|
+
})
|
|
475
|
+
.catch(() => undefined);
|
|
476
|
+
return {
|
|
477
|
+
relPath: entry.relPath,
|
|
478
|
+
status: "skipped",
|
|
479
|
+
errorCode: "TOO_LARGE",
|
|
480
|
+
errorMessage: message,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 2. Read file bytes
|
|
272
485
|
const bytes = await Bun.file(entry.absPath).bytes();
|
|
273
486
|
|
|
274
|
-
//
|
|
487
|
+
// 3. Compute sourceHash
|
|
275
488
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
276
489
|
hasher.update(bytes);
|
|
277
490
|
const sourceHash = hasher.digest("hex");
|
|
278
491
|
|
|
279
|
-
//
|
|
492
|
+
// 4. Check existing doc for skip/repair decision
|
|
280
493
|
const existingResult = await store.getDocument(
|
|
281
494
|
collection.name,
|
|
282
495
|
entry.relPath
|
|
@@ -288,10 +501,10 @@ export class SyncService {
|
|
|
288
501
|
return { relPath: entry.relPath, status: "unchanged" };
|
|
289
502
|
}
|
|
290
503
|
|
|
291
|
-
//
|
|
504
|
+
// 5. Detect MIME (bytes is already Uint8Array from Bun.file().bytes())
|
|
292
505
|
const mime = this.mimeDetector.detect(entry.absPath, bytes);
|
|
293
506
|
|
|
294
|
-
//
|
|
507
|
+
// 6. Convert via pipeline
|
|
295
508
|
const convertResult = await this.pipeline.convert({
|
|
296
509
|
sourcePath: entry.absPath,
|
|
297
510
|
relativePath: entry.relPath,
|
|
@@ -323,8 +536,9 @@ export class SyncService {
|
|
|
323
536
|
sourceHash,
|
|
324
537
|
sourceMime: mime.mime,
|
|
325
538
|
sourceExt: mime.ext,
|
|
326
|
-
sourceSize
|
|
327
|
-
sourceMtime
|
|
539
|
+
sourceSize,
|
|
540
|
+
sourceMtime,
|
|
541
|
+
sourceCtime,
|
|
328
542
|
lastErrorCode: convertResult.error.code,
|
|
329
543
|
lastErrorMessage: convertResult.error.message,
|
|
330
544
|
// mirrorHash intentionally omitted (will be null)
|
|
@@ -348,21 +562,32 @@ export class SyncService {
|
|
|
348
562
|
}
|
|
349
563
|
|
|
350
564
|
const artifact = convertResult.value;
|
|
565
|
+
const extractedMetadata = extractDocumentMetadata(
|
|
566
|
+
artifact.markdown,
|
|
567
|
+
entry.relPath,
|
|
568
|
+
mime.ext
|
|
569
|
+
);
|
|
351
570
|
|
|
352
|
-
//
|
|
571
|
+
// 7. Upsert document - EXPLICITLY clear error fields on success
|
|
353
572
|
const docidResult = await store.upsertDocument({
|
|
354
573
|
collection: collection.name,
|
|
355
574
|
relPath: entry.relPath,
|
|
356
575
|
sourceHash,
|
|
357
576
|
sourceMime: mime.mime,
|
|
358
577
|
sourceExt: mime.ext,
|
|
359
|
-
sourceSize
|
|
360
|
-
sourceMtime
|
|
578
|
+
sourceSize,
|
|
579
|
+
sourceMtime,
|
|
580
|
+
sourceCtime,
|
|
361
581
|
title: artifact.title,
|
|
362
582
|
mirrorHash: artifact.mirrorHash,
|
|
363
583
|
converterId: artifact.meta.converterId,
|
|
364
584
|
converterVersion: artifact.meta.converterVersion,
|
|
365
585
|
languageHint: artifact.languageHint ?? collection.languageHint,
|
|
586
|
+
contentType: extractedMetadata.contentType,
|
|
587
|
+
categories: extractedMetadata.categories,
|
|
588
|
+
author: extractedMetadata.author,
|
|
589
|
+
frontmatterDate: extractedMetadata.frontmatterDate,
|
|
590
|
+
dateFields: extractedMetadata.dateFields,
|
|
366
591
|
// Clear error fields on success (requires store to handle undefined → null)
|
|
367
592
|
lastErrorCode: undefined,
|
|
368
593
|
lastErrorMessage: undefined,
|
|
@@ -374,7 +599,7 @@ export class SyncService {
|
|
|
374
599
|
relPath: entry.relPath,
|
|
375
600
|
});
|
|
376
601
|
|
|
377
|
-
//
|
|
602
|
+
// 8. Upsert content (content-addressed dedupe) - CHECKED
|
|
378
603
|
const contentResult = await store.upsertContent(
|
|
379
604
|
artifact.mirrorHash,
|
|
380
605
|
artifact.markdown
|
|
@@ -383,14 +608,14 @@ export class SyncService {
|
|
|
383
608
|
mirrorHash: artifact.mirrorHash,
|
|
384
609
|
});
|
|
385
610
|
|
|
386
|
-
//
|
|
611
|
+
// 9. Chunk content
|
|
387
612
|
const chunks = this.chunker.chunk(
|
|
388
613
|
artifact.markdown,
|
|
389
614
|
DEFAULT_CHUNK_PARAMS,
|
|
390
615
|
artifact.languageHint ?? collection.languageHint
|
|
391
616
|
);
|
|
392
617
|
|
|
393
|
-
//
|
|
618
|
+
// 10. Convert to ChunkInput for store
|
|
394
619
|
const chunkInputs: ChunkInput[] = chunks.map((c) => ({
|
|
395
620
|
seq: c.seq,
|
|
396
621
|
pos: c.pos,
|
|
@@ -401,7 +626,7 @@ export class SyncService {
|
|
|
401
626
|
tokenCount: c.tokenCount ?? undefined,
|
|
402
627
|
}));
|
|
403
628
|
|
|
404
|
-
//
|
|
629
|
+
// 11. Upsert chunks - CHECKED
|
|
405
630
|
const chunksResult = await store.upsertChunks(
|
|
406
631
|
artifact.mirrorHash,
|
|
407
632
|
chunkInputs
|
|
@@ -411,13 +636,13 @@ export class SyncService {
|
|
|
411
636
|
chunkCount: chunkInputs.length,
|
|
412
637
|
});
|
|
413
638
|
|
|
414
|
-
//
|
|
639
|
+
// 12. Rebuild FTS for this hash - CHECKED
|
|
415
640
|
const ftsResult = await store.rebuildFtsForHash(artifact.mirrorHash);
|
|
416
641
|
mustOk(ftsResult, "rebuildFtsForHash", {
|
|
417
642
|
mirrorHash: artifact.mirrorHash,
|
|
418
643
|
});
|
|
419
644
|
|
|
420
|
-
//
|
|
645
|
+
// 13. Extract and store tags from frontmatter and body hashtags
|
|
421
646
|
// Always call setDocTags to clear removed tags on re-sync
|
|
422
647
|
const extractedTags = extractTags(artifact.markdown);
|
|
423
648
|
const tagsResult = await store.setDocTags(
|
|
@@ -430,7 +655,7 @@ export class SyncService {
|
|
|
430
655
|
tagCount: extractedTags.length,
|
|
431
656
|
});
|
|
432
657
|
|
|
433
|
-
//
|
|
658
|
+
// 14. Extract and store links (wiki and markdown links)
|
|
434
659
|
const excludedRanges = getExcludedRanges(artifact.markdown);
|
|
435
660
|
const lineOffsets = buildLineOffsets(artifact.markdown);
|
|
436
661
|
const parsedLinks = parseLinks(
|
|
@@ -521,6 +746,9 @@ export class SyncService {
|
|
|
521
746
|
sourceExt: existingResult.value.sourceExt,
|
|
522
747
|
sourceSize: existingResult.value.sourceSize,
|
|
523
748
|
sourceMtime: existingResult.value.sourceMtime,
|
|
749
|
+
sourceCtime:
|
|
750
|
+
existingResult.value.sourceCtime ??
|
|
751
|
+
existingResult.value.sourceMtime,
|
|
524
752
|
lastErrorCode: code,
|
|
525
753
|
lastErrorMessage: message,
|
|
526
754
|
});
|
|
@@ -579,6 +807,7 @@ export class SyncService {
|
|
|
579
807
|
relPath,
|
|
580
808
|
size: stats.size,
|
|
581
809
|
mtime: stats.mtime.toISOString(),
|
|
810
|
+
ctime: (stats.birthtime ?? stats.ctime ?? stats.mtime).toISOString(),
|
|
582
811
|
};
|
|
583
812
|
|
|
584
813
|
const result = await this.processFile(collection, entry, store, options);
|
|
@@ -661,6 +890,7 @@ export class SyncService {
|
|
|
661
890
|
let updated = 0;
|
|
662
891
|
let unchanged = 0;
|
|
663
892
|
let errored = 0;
|
|
893
|
+
let dynamicSkipped = 0;
|
|
664
894
|
|
|
665
895
|
if (concurrency === 1) {
|
|
666
896
|
// Sequential processing with batched transactions (Windows perf)
|
|
@@ -696,8 +926,15 @@ export class SyncService {
|
|
|
696
926
|
});
|
|
697
927
|
}
|
|
698
928
|
break;
|
|
699
|
-
|
|
700
|
-
|
|
929
|
+
case "skipped":
|
|
930
|
+
dynamicSkipped += 1;
|
|
931
|
+
if (result.errorCode && result.errorMessage) {
|
|
932
|
+
errors.push({
|
|
933
|
+
relPath: result.relPath,
|
|
934
|
+
code: result.errorCode,
|
|
935
|
+
message: result.errorMessage,
|
|
936
|
+
});
|
|
937
|
+
}
|
|
701
938
|
break;
|
|
702
939
|
}
|
|
703
940
|
}
|
|
@@ -763,8 +1000,15 @@ export class SyncService {
|
|
|
763
1000
|
});
|
|
764
1001
|
}
|
|
765
1002
|
break;
|
|
766
|
-
|
|
767
|
-
|
|
1003
|
+
case "skipped":
|
|
1004
|
+
dynamicSkipped += 1;
|
|
1005
|
+
if (result.errorCode && result.errorMessage) {
|
|
1006
|
+
errors.push({
|
|
1007
|
+
relPath: result.relPath,
|
|
1008
|
+
code: result.errorCode,
|
|
1009
|
+
message: result.errorMessage,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
768
1012
|
break;
|
|
769
1013
|
}
|
|
770
1014
|
}
|
|
@@ -796,7 +1040,7 @@ export class SyncService {
|
|
|
796
1040
|
filesUpdated: updated,
|
|
797
1041
|
filesUnchanged: unchanged,
|
|
798
1042
|
filesErrored: errored,
|
|
799
|
-
filesSkipped: skipped.length,
|
|
1043
|
+
filesSkipped: skipped.length + dynamicSkipped,
|
|
800
1044
|
filesMarkedInactive: markedInactive,
|
|
801
1045
|
durationMs: Date.now() - startTime,
|
|
802
1046
|
errors,
|
package/src/ingestion/types.ts
CHANGED
package/src/ingestion/walker.ts
CHANGED
|
@@ -200,7 +200,7 @@ export class FileWalker implements WalkerPort {
|
|
|
200
200
|
|
|
201
201
|
// Stat file
|
|
202
202
|
const file = Bun.file(absPath);
|
|
203
|
-
let stat: { size: number; mtime: Date };
|
|
203
|
+
let stat: { size: number; mtime: Date; ctime?: Date; birthtime?: Date };
|
|
204
204
|
try {
|
|
205
205
|
stat = await file.stat();
|
|
206
206
|
} catch {
|
|
@@ -224,6 +224,7 @@ export class FileWalker implements WalkerPort {
|
|
|
224
224
|
relPath,
|
|
225
225
|
size: stat.size,
|
|
226
226
|
mtime: stat.mtime.toISOString(),
|
|
227
|
+
ctime: (stat.birthtime ?? stat.ctime ?? stat.mtime).toISOString(),
|
|
227
228
|
});
|
|
228
229
|
}
|
|
229
230
|
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -56,6 +56,10 @@ const searchInputSchema = z.object({
|
|
|
56
56
|
limit: z.number().int().min(1).max(100).default(5),
|
|
57
57
|
minScore: z.number().min(0).max(1).optional(),
|
|
58
58
|
lang: z.string().optional(),
|
|
59
|
+
since: z.string().optional(),
|
|
60
|
+
until: z.string().optional(),
|
|
61
|
+
categories: z.array(z.string()).optional(),
|
|
62
|
+
author: z.string().optional(),
|
|
59
63
|
tagsAll: z.array(z.string()).optional(),
|
|
60
64
|
tagsAny: z.array(z.string()).optional(),
|
|
61
65
|
});
|
|
@@ -101,16 +105,41 @@ const vsearchInputSchema = z.object({
|
|
|
101
105
|
limit: z.number().int().min(1).max(100).default(5),
|
|
102
106
|
minScore: z.number().min(0).max(1).optional(),
|
|
103
107
|
lang: z.string().optional(),
|
|
108
|
+
since: z.string().optional(),
|
|
109
|
+
until: z.string().optional(),
|
|
110
|
+
categories: z.array(z.string()).optional(),
|
|
111
|
+
author: z.string().optional(),
|
|
104
112
|
tagsAll: z.array(z.string()).optional(),
|
|
105
113
|
tagsAny: z.array(z.string()).optional(),
|
|
106
114
|
});
|
|
107
115
|
|
|
108
|
-
const
|
|
116
|
+
const queryModeInputSchema = z.object({
|
|
117
|
+
mode: z.enum(["term", "intent", "hyde"]),
|
|
118
|
+
text: z.string().trim().min(1, "Query mode text cannot be empty"),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export const queryInputSchema = z.object({
|
|
109
122
|
query: z.string().min(1, "Query cannot be empty"),
|
|
110
123
|
collection: z.string().optional(),
|
|
111
124
|
limit: z.number().int().min(1).max(100).default(5),
|
|
112
125
|
minScore: z.number().min(0).max(1).optional(),
|
|
113
126
|
lang: z.string().optional(),
|
|
127
|
+
since: z.string().optional(),
|
|
128
|
+
until: z.string().optional(),
|
|
129
|
+
categories: z.array(z.string()).optional(),
|
|
130
|
+
author: z.string().optional(),
|
|
131
|
+
queryModes: z
|
|
132
|
+
.array(queryModeInputSchema)
|
|
133
|
+
.superRefine((entries, ctx) => {
|
|
134
|
+
const hydeCount = entries.filter((entry) => entry.mode === "hyde").length;
|
|
135
|
+
if (hydeCount > 1) {
|
|
136
|
+
ctx.addIssue({
|
|
137
|
+
code: z.ZodIssueCode.custom,
|
|
138
|
+
message: "Only one hyde mode is allowed in queryModes",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
.optional(),
|
|
114
143
|
fast: z.boolean().default(false),
|
|
115
144
|
thorough: z.boolean().default(false),
|
|
116
145
|
expand: z.boolean().default(false), // Default: skip expansion
|
package/src/mcp/tools/query.ts
CHANGED
|
@@ -11,7 +11,11 @@ import type {
|
|
|
11
11
|
GenerationPort,
|
|
12
12
|
RerankPort,
|
|
13
13
|
} from "../../llm/types";
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
QueryModeInput,
|
|
16
|
+
SearchResult,
|
|
17
|
+
SearchResults,
|
|
18
|
+
} from "../../pipeline/types";
|
|
15
19
|
import type { ToolContext } from "../server";
|
|
16
20
|
|
|
17
21
|
import { parseUri } from "../../app/constants";
|
|
@@ -32,6 +36,11 @@ interface QueryInput {
|
|
|
32
36
|
limit?: number;
|
|
33
37
|
minScore?: number;
|
|
34
38
|
lang?: string;
|
|
39
|
+
since?: string;
|
|
40
|
+
until?: string;
|
|
41
|
+
categories?: string[];
|
|
42
|
+
author?: string;
|
|
43
|
+
queryModes?: QueryModeInput[];
|
|
35
44
|
fast?: boolean;
|
|
36
45
|
thorough?: boolean;
|
|
37
46
|
expand?: boolean;
|
|
@@ -158,6 +167,7 @@ export function handleQuery(
|
|
|
158
167
|
// Determine noExpand/noRerank based on mode flags
|
|
159
168
|
// Priority: fast > thorough > expand/rerank params > defaults
|
|
160
169
|
// Default: noExpand=true (skip expansion), noRerank=false (with reranking)
|
|
170
|
+
const hasStructuredModes = Boolean(args.queryModes?.length);
|
|
161
171
|
let noExpand = true;
|
|
162
172
|
let noRerank = false;
|
|
163
173
|
|
|
@@ -177,8 +187,13 @@ export function handleQuery(
|
|
|
177
187
|
}
|
|
178
188
|
}
|
|
179
189
|
|
|
190
|
+
// Structured query modes replace generated expansion.
|
|
191
|
+
if (hasStructuredModes) {
|
|
192
|
+
noExpand = true;
|
|
193
|
+
}
|
|
194
|
+
|
|
180
195
|
// Create generation port (for expansion) - optional
|
|
181
|
-
if (!noExpand) {
|
|
196
|
+
if (!noExpand && !hasStructuredModes) {
|
|
182
197
|
const genResult = await llm.createGenerationPort(preset.gen, {
|
|
183
198
|
policy,
|
|
184
199
|
onProgress: (progress) => downloadProgress("gen", progress),
|
|
@@ -232,8 +247,13 @@ export function handleQuery(
|
|
|
232
247
|
minScore: args.minScore,
|
|
233
248
|
collection: args.collection,
|
|
234
249
|
queryLanguageHint: args.lang, // Affects expansion prompt, not retrieval
|
|
250
|
+
since: args.since,
|
|
251
|
+
until: args.until,
|
|
252
|
+
categories: args.categories,
|
|
253
|
+
author: args.author,
|
|
235
254
|
noExpand,
|
|
236
255
|
noRerank,
|
|
256
|
+
queryModes: args.queryModes,
|
|
237
257
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
238
258
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
239
259
|
});
|
package/src/mcp/tools/search.ts
CHANGED
|
@@ -19,6 +19,10 @@ interface SearchInput {
|
|
|
19
19
|
limit?: number;
|
|
20
20
|
minScore?: number;
|
|
21
21
|
lang?: string;
|
|
22
|
+
since?: string;
|
|
23
|
+
until?: string;
|
|
24
|
+
categories?: string[];
|
|
25
|
+
author?: string;
|
|
22
26
|
tagsAll?: string[];
|
|
23
27
|
tagsAny?: string[];
|
|
24
28
|
}
|
|
@@ -104,6 +108,10 @@ export function handleSearch(
|
|
|
104
108
|
minScore: args.minScore,
|
|
105
109
|
collection: args.collection,
|
|
106
110
|
lang: args.lang,
|
|
111
|
+
since: args.since,
|
|
112
|
+
until: args.until,
|
|
113
|
+
categories: args.categories,
|
|
114
|
+
author: args.author,
|
|
107
115
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
108
116
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
109
117
|
});
|
package/src/mcp/tools/vsearch.ts
CHANGED
|
@@ -28,6 +28,10 @@ interface VsearchInput {
|
|
|
28
28
|
limit?: number;
|
|
29
29
|
minScore?: number;
|
|
30
30
|
lang?: string;
|
|
31
|
+
since?: string;
|
|
32
|
+
until?: string;
|
|
33
|
+
categories?: string[];
|
|
34
|
+
author?: string;
|
|
31
35
|
tagsAll?: string[];
|
|
32
36
|
tagsAny?: string[];
|
|
33
37
|
}
|
|
@@ -188,6 +192,10 @@ export function handleVsearch(
|
|
|
188
192
|
limit: args.limit ?? 5,
|
|
189
193
|
minScore: args.minScore,
|
|
190
194
|
collection: args.collection,
|
|
195
|
+
since: args.since,
|
|
196
|
+
until: args.until,
|
|
197
|
+
categories: args.categories,
|
|
198
|
+
author: args.author,
|
|
191
199
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
192
200
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
193
201
|
}
|