@gmickel/gno 0.16.0 → 0.18.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 +55 -2
- package/package.json +4 -1
- package/src/cli/commands/ask.ts +13 -0
- package/src/cli/commands/models/use.ts +1 -0
- package/src/cli/commands/query.ts +3 -2
- package/src/cli/pager.ts +1 -1
- package/src/cli/program.ts +107 -0
- package/src/config/types.ts +2 -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/llm/nodeLlamaCpp/generation.ts +3 -1
- package/src/llm/registry.ts +1 -0
- package/src/llm/types.ts +2 -0
- package/src/mcp/tools/index.ts +34 -1
- package/src/mcp/tools/query.ts +26 -2
- package/src/mcp/tools/search.ts +10 -0
- package/src/mcp/tools/vsearch.ts +10 -0
- package/src/pipeline/answer.ts +324 -7
- package/src/pipeline/expansion.ts +282 -11
- package/src/pipeline/explain.ts +93 -5
- package/src/pipeline/hybrid.ts +273 -70
- package/src/pipeline/intent.ts +152 -0
- package/src/pipeline/query-modes.ts +125 -0
- package/src/pipeline/rerank.ts +109 -51
- package/src/pipeline/search.ts +58 -4
- package/src/pipeline/temporal.ts +257 -0
- package/src/pipeline/types.ts +67 -0
- package/src/pipeline/vsearch.ts +121 -10
- 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 +174 -0
- package/src/serve/public/pages/Ask.tsx +378 -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 +561 -120
- package/src/serve/routes/api.ts +247 -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/README.md
CHANGED
|
@@ -32,7 +32,22 @@ GNO is a local knowledge engine that turns your documents into a searchable, con
|
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
|
-
## What's New in v0.
|
|
35
|
+
## What's New in v0.18
|
|
36
|
+
|
|
37
|
+
- **Intent Steering**: optional `intent` control for ambiguous queries across CLI, API, Web, and MCP query flows
|
|
38
|
+
- **Rerank Controls**: `candidateLimit` lets you tune rerank cost vs. recall on slower or memory-constrained machines
|
|
39
|
+
- **Stability**: query expansion now uses a bounded configurable context size (`models.expandContextSize`, default `2048`)
|
|
40
|
+
- **Rerank Efficiency**: identical chunk texts are deduplicated before scoring and expanded back out deterministically
|
|
41
|
+
|
|
42
|
+
### v0.17
|
|
43
|
+
|
|
44
|
+
- **Structured Query Modes**: `term`, `intent`, and `hyde` controls across CLI, API, MCP, and Web
|
|
45
|
+
- **Temporal Retrieval Upgrades**: `since`/`until`, date-range parsing, and recency sorting with frontmatter-date fallback
|
|
46
|
+
- **Web Retrieval UX Polish**: richer advanced controls in Search and Ask (collection/date/category/author/tags + query modes)
|
|
47
|
+
- **Metadata-Aware Retrieval**: ingestion now materializes document metadata/date fields for better filtering and ranking
|
|
48
|
+
- **Migration Reliability**: SQLite-compatible migration path for existing indexes (including older SQLite engines)
|
|
49
|
+
|
|
50
|
+
### v0.15
|
|
36
51
|
|
|
37
52
|
- **HTTP Backends**: Offload embedding, reranking, and generation to remote GPU servers
|
|
38
53
|
- Simple URI config: `http://host:port/path#modelname`
|
|
@@ -139,16 +154,41 @@ gno skill install --target all # Both Claude + Codex
|
|
|
139
154
|
|
|
140
155
|
**BM25** indexes full documents (not chunks) with Snowball stemming, so "running" matches "run".
|
|
141
156
|
**Vector** embeds chunks with document titles for context awareness.
|
|
157
|
+
All retrieval modes also support metadata filters: `--since`, `--until`, `--category`, `--author`, `--tags-all`, `--tags-any`.
|
|
142
158
|
|
|
143
159
|
```bash
|
|
144
160
|
gno search "handleAuth" # Find exact matches
|
|
145
161
|
gno vsearch "error handling patterns" # Semantic similarity
|
|
146
162
|
gno query "database optimization" # Full pipeline
|
|
163
|
+
gno query "meeting decisions" --since "last month" --category "meeting,notes" --author "gordon"
|
|
164
|
+
gno query "performance" --intent "web performance and latency"
|
|
147
165
|
gno ask "what did we decide" --answer # AI synthesis
|
|
148
166
|
```
|
|
149
167
|
|
|
150
168
|
Output formats: `--json`, `--files`, `--csv`, `--md`, `--xml`
|
|
151
169
|
|
|
170
|
+
### Retrieval V2 Controls
|
|
171
|
+
|
|
172
|
+
Existing query calls still work. Retrieval v2 adds optional structured intent control and deeper explain output.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Existing call (unchanged)
|
|
176
|
+
gno query "auth flow" --thorough
|
|
177
|
+
|
|
178
|
+
# Structured retrieval intent
|
|
179
|
+
gno query "auth flow" \
|
|
180
|
+
--intent "web authentication and token lifecycle" \
|
|
181
|
+
--candidate-limit 12 \
|
|
182
|
+
--query-mode term:"jwt refresh token -oauth1" \
|
|
183
|
+
--query-mode intent:"how refresh token rotation works" \
|
|
184
|
+
--query-mode hyde:"Refresh tokens rotate on each use and previous tokens are revoked." \
|
|
185
|
+
--explain
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- Modes: `term` (BM25-focused), `intent` (semantic-focused), `hyde` (single hypothetical passage)
|
|
189
|
+
- Explain includes stage timings, fallback/cache counters, and per-result score components
|
|
190
|
+
- `gno ask --json` includes `meta.answerContext` for adaptive source selection traces
|
|
191
|
+
|
|
152
192
|
---
|
|
153
193
|
|
|
154
194
|
## Agent Integration
|
|
@@ -372,7 +412,7 @@ graph TD
|
|
|
372
412
|
|
|
373
413
|
## Local Models
|
|
374
414
|
|
|
375
|
-
Models auto-download on first use to `~/.cache/gno/models/`. Alternatively, offload to a GPU server on your network using HTTP backends.
|
|
415
|
+
Models auto-download on first use to `~/.cache/gno/models/`. For deterministic startup, set `GNO_NO_AUTO_DOWNLOAD=1` and use `gno models pull` explicitly. Alternatively, offload to a GPU server on your network using HTTP backends.
|
|
376
416
|
|
|
377
417
|
| Model | Purpose | Size |
|
|
378
418
|
| :------------------ | :------------------------------------ | :----------- |
|
|
@@ -444,6 +484,19 @@ bun run lint && bun run typecheck
|
|
|
444
484
|
|
|
445
485
|
> **Contributing**: [CONTRIBUTING.md](.github/CONTRIBUTING.md)
|
|
446
486
|
|
|
487
|
+
### Evals and Benchmark Deltas
|
|
488
|
+
|
|
489
|
+
Use retrieval benchmark commands to track quality and latency over time:
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
bun run eval:hybrid
|
|
493
|
+
bun run eval:hybrid:baseline
|
|
494
|
+
bun run eval:hybrid:delta
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
- Benchmark guide: [evals/README.md](./evals/README.md)
|
|
498
|
+
- Latest baseline snapshot: [evals/fixtures/hybrid-baseline/latest.json](./evals/fixtures/hybrid-baseline/latest.json)
|
|
499
|
+
|
|
447
500
|
---
|
|
448
501
|
|
|
449
502
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"embeddings",
|
|
@@ -51,6 +51,9 @@
|
|
|
51
51
|
"test:fixtures": "bun scripts/generate-test-fixtures.ts",
|
|
52
52
|
"evals": "bun scripts/update-eval-scores.ts",
|
|
53
53
|
"eval": "bun --bun evalite",
|
|
54
|
+
"eval:hybrid": "bun --bun evalite evals/hybrid.eval.ts",
|
|
55
|
+
"eval:hybrid:baseline": "bun scripts/hybrid-benchmark.ts --write",
|
|
56
|
+
"eval:hybrid:delta": "bun scripts/hybrid-benchmark.ts --delta",
|
|
54
57
|
"eval:watch": "bun --bun evalite watch",
|
|
55
58
|
"reset": "bun run src/index.ts reset --confirm",
|
|
56
59
|
"docs:verify": "bun run scripts/docs-verify.ts",
|
package/src/cli/commands/ask.ts
CHANGED
|
@@ -192,8 +192,16 @@ export async function ask(
|
|
|
192
192
|
limit,
|
|
193
193
|
collection: options.collection,
|
|
194
194
|
lang: options.lang,
|
|
195
|
+
intent: options.intent,
|
|
196
|
+
since: options.since,
|
|
197
|
+
until: options.until,
|
|
198
|
+
categories: options.categories,
|
|
199
|
+
author: options.author,
|
|
200
|
+
tagsAll: options.tagsAll,
|
|
201
|
+
tagsAny: options.tagsAny,
|
|
195
202
|
noExpand: options.noExpand,
|
|
196
203
|
noRerank: options.noRerank,
|
|
204
|
+
candidateLimit: options.candidateLimit,
|
|
197
205
|
});
|
|
198
206
|
|
|
199
207
|
if (!searchResult.ok) {
|
|
@@ -205,6 +213,7 @@ export async function ask(
|
|
|
205
213
|
// Generate grounded answer if requested
|
|
206
214
|
let answer: string | undefined;
|
|
207
215
|
let citations: Citation[] | undefined;
|
|
216
|
+
let answerContext: AskResult["meta"]["answerContext"] | undefined;
|
|
208
217
|
let answerGenerated = false;
|
|
209
218
|
|
|
210
219
|
// Only generate answer if:
|
|
@@ -236,6 +245,7 @@ export async function ask(
|
|
|
236
245
|
const processed = processAnswerResult(rawResult);
|
|
237
246
|
answer = processed.answer;
|
|
238
247
|
citations = processed.citations;
|
|
248
|
+
answerContext = processed.answerContext;
|
|
239
249
|
answerGenerated = true;
|
|
240
250
|
}
|
|
241
251
|
|
|
@@ -250,8 +260,11 @@ export async function ask(
|
|
|
250
260
|
expanded: searchResult.value.meta.expanded ?? false,
|
|
251
261
|
reranked: searchResult.value.meta.reranked ?? false,
|
|
252
262
|
vectorsUsed: searchResult.value.meta.vectorsUsed ?? false,
|
|
263
|
+
intent: searchResult.value.meta.intent,
|
|
264
|
+
candidateLimit: searchResult.value.meta.candidateLimit,
|
|
253
265
|
answerGenerated,
|
|
254
266
|
totalResults: results.length,
|
|
267
|
+
answerContext,
|
|
255
268
|
},
|
|
256
269
|
};
|
|
257
270
|
|
|
@@ -58,6 +58,7 @@ export async function modelsUse(
|
|
|
58
58
|
presets: config.models?.presets ?? [],
|
|
59
59
|
loadTimeout: config.models?.loadTimeout ?? 60_000,
|
|
60
60
|
inferenceTimeout: config.models?.inferenceTimeout ?? 30_000,
|
|
61
|
+
expandContextSize: config.models?.expandContextSize ?? 2_048,
|
|
61
62
|
warmModelTtl: config.models?.warmModelTtl ?? 300_000,
|
|
62
63
|
},
|
|
63
64
|
};
|
|
@@ -121,8 +121,9 @@ export async function query(
|
|
|
121
121
|
embedPort = embedResult.value;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
// Create generation port (for expansion) - optional
|
|
125
|
-
|
|
124
|
+
// Create generation port (for expansion) - optional.
|
|
125
|
+
// Skip when structured query modes are provided.
|
|
126
|
+
if (!options.noExpand && !options.queryModes?.length) {
|
|
126
127
|
const genUri = options.genModel ?? preset.gen;
|
|
127
128
|
const genResult = await llm.createGenerationPort(genUri, {
|
|
128
129
|
policy,
|
package/src/cli/pager.ts
CHANGED
package/src/cli/program.ts
CHANGED
|
@@ -130,6 +130,17 @@ async function writeOutput(
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function parseCsvValues(raw: unknown): string[] | undefined {
|
|
134
|
+
if (typeof raw !== "string") {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const values = raw
|
|
138
|
+
.split(",")
|
|
139
|
+
.map((v) => v.trim().toLowerCase())
|
|
140
|
+
.filter((v) => v.length > 0);
|
|
141
|
+
return values.length > 0 ? values : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
133
144
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
134
145
|
// Program Factory
|
|
135
146
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -203,6 +214,17 @@ function wireSearchCommands(program: Command): void {
|
|
|
203
214
|
.option("--min-score <num>", "minimum score threshold")
|
|
204
215
|
.option("-c, --collection <name>", "filter by collection")
|
|
205
216
|
.option("--lang <code>", "language filter/hint (BCP-47)")
|
|
217
|
+
.option(
|
|
218
|
+
"--since <date>",
|
|
219
|
+
"modified-at lower bound (ISO date/time or token)"
|
|
220
|
+
)
|
|
221
|
+
.option(
|
|
222
|
+
"--until <date>",
|
|
223
|
+
"modified-at upper bound (ISO date/time or token)"
|
|
224
|
+
)
|
|
225
|
+
.option("--category <values>", "require category match (comma-separated)")
|
|
226
|
+
.option("--author <text>", "filter by author (case-insensitive contains)")
|
|
227
|
+
.option("--intent <text>", "disambiguating context for ambiguous queries")
|
|
206
228
|
.option("--tags-all <tags>", "require ALL tags (comma-separated)")
|
|
207
229
|
.option("--tags-any <tags>", "require ANY tag (comma-separated)")
|
|
208
230
|
.option("--full", "include full content")
|
|
@@ -247,6 +269,7 @@ function wireSearchCommands(program: Command): void {
|
|
|
247
269
|
const limit = cmdOpts.limit
|
|
248
270
|
? parsePositiveInt("limit", cmdOpts.limit)
|
|
249
271
|
: getDefaultLimit(format);
|
|
272
|
+
const categories = parseCsvValues(cmdOpts.category);
|
|
250
273
|
|
|
251
274
|
const { search, formatSearch } = await import("./commands/search");
|
|
252
275
|
const result = await search(queryText, {
|
|
@@ -254,6 +277,11 @@ function wireSearchCommands(program: Command): void {
|
|
|
254
277
|
minScore,
|
|
255
278
|
collection: cmdOpts.collection as string | undefined,
|
|
256
279
|
lang: cmdOpts.lang as string | undefined,
|
|
280
|
+
since: cmdOpts.since as string | undefined,
|
|
281
|
+
until: cmdOpts.until as string | undefined,
|
|
282
|
+
categories,
|
|
283
|
+
author: cmdOpts.author as string | undefined,
|
|
284
|
+
intent: cmdOpts.intent as string | undefined,
|
|
257
285
|
tagsAll,
|
|
258
286
|
tagsAny,
|
|
259
287
|
full: Boolean(cmdOpts.full),
|
|
@@ -293,6 +321,17 @@ function wireSearchCommands(program: Command): void {
|
|
|
293
321
|
.option("--min-score <num>", "minimum score threshold")
|
|
294
322
|
.option("-c, --collection <name>", "filter by collection")
|
|
295
323
|
.option("--lang <code>", "language filter/hint (BCP-47)")
|
|
324
|
+
.option(
|
|
325
|
+
"--since <date>",
|
|
326
|
+
"modified-at lower bound (ISO date/time or token)"
|
|
327
|
+
)
|
|
328
|
+
.option(
|
|
329
|
+
"--until <date>",
|
|
330
|
+
"modified-at upper bound (ISO date/time or token)"
|
|
331
|
+
)
|
|
332
|
+
.option("--category <values>", "require category match (comma-separated)")
|
|
333
|
+
.option("--author <text>", "filter by author (case-insensitive contains)")
|
|
334
|
+
.option("--intent <text>", "disambiguating context for ambiguous queries")
|
|
296
335
|
.option("--tags-all <tags>", "require ALL tags (comma-separated)")
|
|
297
336
|
.option("--tags-any <tags>", "require ANY tag (comma-separated)")
|
|
298
337
|
.option("--full", "include full content")
|
|
@@ -337,6 +376,7 @@ function wireSearchCommands(program: Command): void {
|
|
|
337
376
|
const limit = cmdOpts.limit
|
|
338
377
|
? parsePositiveInt("limit", cmdOpts.limit)
|
|
339
378
|
: getDefaultLimit(format);
|
|
379
|
+
const categories = parseCsvValues(cmdOpts.category);
|
|
340
380
|
|
|
341
381
|
const { vsearch, formatVsearch } = await import("./commands/vsearch");
|
|
342
382
|
const result = await vsearch(queryText, {
|
|
@@ -344,6 +384,11 @@ function wireSearchCommands(program: Command): void {
|
|
|
344
384
|
minScore,
|
|
345
385
|
collection: cmdOpts.collection as string | undefined,
|
|
346
386
|
lang: cmdOpts.lang as string | undefined,
|
|
387
|
+
since: cmdOpts.since as string | undefined,
|
|
388
|
+
until: cmdOpts.until as string | undefined,
|
|
389
|
+
categories,
|
|
390
|
+
author: cmdOpts.author as string | undefined,
|
|
391
|
+
intent: cmdOpts.intent as string | undefined,
|
|
347
392
|
tagsAll,
|
|
348
393
|
tagsAny,
|
|
349
394
|
full: Boolean(cmdOpts.full),
|
|
@@ -378,6 +423,17 @@ function wireSearchCommands(program: Command): void {
|
|
|
378
423
|
.option("--min-score <num>", "minimum score threshold")
|
|
379
424
|
.option("-c, --collection <name>", "filter by collection")
|
|
380
425
|
.option("--lang <code>", "language hint (BCP-47)")
|
|
426
|
+
.option(
|
|
427
|
+
"--since <date>",
|
|
428
|
+
"modified-at lower bound (ISO date/time or token)"
|
|
429
|
+
)
|
|
430
|
+
.option(
|
|
431
|
+
"--until <date>",
|
|
432
|
+
"modified-at upper bound (ISO date/time or token)"
|
|
433
|
+
)
|
|
434
|
+
.option("--category <values>", "require category match (comma-separated)")
|
|
435
|
+
.option("--author <text>", "filter by author (case-insensitive contains)")
|
|
436
|
+
.option("--intent <text>", "disambiguating context for ambiguous queries")
|
|
381
437
|
.option("--tags-all <tags>", "require ALL tags (comma-separated)")
|
|
382
438
|
.option("--tags-any <tags>", "require ANY tag (comma-separated)")
|
|
383
439
|
.option("--full", "include full content")
|
|
@@ -386,6 +442,13 @@ function wireSearchCommands(program: Command): void {
|
|
|
386
442
|
.option("--thorough", "enable query expansion (slower, ~5-8s)")
|
|
387
443
|
.option("--no-expand", "disable query expansion")
|
|
388
444
|
.option("--no-rerank", "disable reranking")
|
|
445
|
+
.option(
|
|
446
|
+
"--query-mode <mode:text>",
|
|
447
|
+
"structured mode entry (repeatable): term:<text>, intent:<text>, or hyde:<text>",
|
|
448
|
+
(value: string, previous: string[] = []) => [...previous, value],
|
|
449
|
+
[]
|
|
450
|
+
)
|
|
451
|
+
.option("-C, --candidate-limit <num>", "max candidates passed to reranking")
|
|
389
452
|
.option("--explain", "include scoring explanation")
|
|
390
453
|
.option("--json", "JSON output")
|
|
391
454
|
.option("--md", "Markdown output")
|
|
@@ -407,6 +470,17 @@ function wireSearchCommands(program: Command): void {
|
|
|
407
470
|
throw new CliError("VALIDATION", "--min-score must be between 0 and 1");
|
|
408
471
|
}
|
|
409
472
|
|
|
473
|
+
// Parse optional structured query modes
|
|
474
|
+
let queryModes: import("../pipeline/types").QueryModeInput[] | undefined;
|
|
475
|
+
if (Array.isArray(cmdOpts.queryMode) && cmdOpts.queryMode.length > 0) {
|
|
476
|
+
const { parseQueryModeSpecs } = await import("../pipeline/query-modes");
|
|
477
|
+
const parsed = parseQueryModeSpecs(cmdOpts.queryMode as string[]);
|
|
478
|
+
if (!parsed.ok) {
|
|
479
|
+
throw new CliError("VALIDATION", parsed.error.message);
|
|
480
|
+
}
|
|
481
|
+
queryModes = parsed.value;
|
|
482
|
+
}
|
|
483
|
+
|
|
410
484
|
// Parse and validate tag filters
|
|
411
485
|
let tagsAll: string[] | undefined;
|
|
412
486
|
let tagsAny: string[] | undefined;
|
|
@@ -427,6 +501,10 @@ function wireSearchCommands(program: Command): void {
|
|
|
427
501
|
const limit = cmdOpts.limit
|
|
428
502
|
? parsePositiveInt("limit", cmdOpts.limit)
|
|
429
503
|
: getDefaultLimit(format);
|
|
504
|
+
const candidateLimit = cmdOpts.candidateLimit
|
|
505
|
+
? parsePositiveInt("candidate-limit", cmdOpts.candidateLimit)
|
|
506
|
+
: undefined;
|
|
507
|
+
const categories = parseCsvValues(cmdOpts.category);
|
|
430
508
|
|
|
431
509
|
// Determine expansion/rerank settings based on flags
|
|
432
510
|
// Priority: --fast > --thorough > --no-expand/--no-rerank > default
|
|
@@ -458,12 +536,19 @@ function wireSearchCommands(program: Command): void {
|
|
|
458
536
|
minScore,
|
|
459
537
|
collection: cmdOpts.collection as string | undefined,
|
|
460
538
|
lang: cmdOpts.lang as string | undefined,
|
|
539
|
+
since: cmdOpts.since as string | undefined,
|
|
540
|
+
until: cmdOpts.until as string | undefined,
|
|
541
|
+
categories,
|
|
542
|
+
author: cmdOpts.author as string | undefined,
|
|
543
|
+
intent: cmdOpts.intent as string | undefined,
|
|
461
544
|
tagsAll,
|
|
462
545
|
tagsAny,
|
|
463
546
|
full: Boolean(cmdOpts.full),
|
|
464
547
|
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
465
548
|
noExpand,
|
|
466
549
|
noRerank,
|
|
550
|
+
candidateLimit,
|
|
551
|
+
queryModes,
|
|
467
552
|
explain: Boolean(cmdOpts.explain),
|
|
468
553
|
json: format === "json",
|
|
469
554
|
md: format === "md",
|
|
@@ -490,8 +575,20 @@ function wireSearchCommands(program: Command): void {
|
|
|
490
575
|
.option("-n, --limit <num>", "max source results")
|
|
491
576
|
.option("-c, --collection <name>", "filter by collection")
|
|
492
577
|
.option("--lang <code>", "language hint (BCP-47)")
|
|
578
|
+
.option(
|
|
579
|
+
"--since <date>",
|
|
580
|
+
"modified-at lower bound (ISO date/time or token)"
|
|
581
|
+
)
|
|
582
|
+
.option(
|
|
583
|
+
"--until <date>",
|
|
584
|
+
"modified-at upper bound (ISO date/time or token)"
|
|
585
|
+
)
|
|
586
|
+
.option("--category <values>", "require category match (comma-separated)")
|
|
587
|
+
.option("--author <text>", "filter by author (case-insensitive contains)")
|
|
588
|
+
.option("--intent <text>", "disambiguating context for ambiguous queries")
|
|
493
589
|
.option("--fast", "skip expansion and reranking (fastest)")
|
|
494
590
|
.option("--thorough", "enable query expansion (slower)")
|
|
591
|
+
.option("-C, --candidate-limit <num>", "max candidates passed to reranking")
|
|
495
592
|
.option("--answer", "generate short grounded answer")
|
|
496
593
|
.option("--no-answer", "force retrieval-only output")
|
|
497
594
|
.option("--max-answer-tokens <num>", "max answer tokens")
|
|
@@ -510,11 +607,15 @@ function wireSearchCommands(program: Command): void {
|
|
|
510
607
|
const limit = cmdOpts.limit
|
|
511
608
|
? parsePositiveInt("limit", cmdOpts.limit)
|
|
512
609
|
: getDefaultLimit(format);
|
|
610
|
+
const candidateLimit = cmdOpts.candidateLimit
|
|
611
|
+
? parsePositiveInt("candidate-limit", cmdOpts.candidateLimit)
|
|
612
|
+
: undefined;
|
|
513
613
|
|
|
514
614
|
// Parse max-answer-tokens (optional, defaults to 512 in command impl)
|
|
515
615
|
const maxAnswerTokens = cmdOpts.maxAnswerTokens
|
|
516
616
|
? parsePositiveInt("max-answer-tokens", cmdOpts.maxAnswerTokens)
|
|
517
617
|
: undefined;
|
|
618
|
+
const categories = parseCsvValues(cmdOpts.category);
|
|
518
619
|
|
|
519
620
|
// Determine expansion/rerank settings based on flags
|
|
520
621
|
// Default: skip expansion (balanced mode)
|
|
@@ -535,8 +636,14 @@ function wireSearchCommands(program: Command): void {
|
|
|
535
636
|
limit,
|
|
536
637
|
collection: cmdOpts.collection as string | undefined,
|
|
537
638
|
lang: cmdOpts.lang as string | undefined,
|
|
639
|
+
since: cmdOpts.since as string | undefined,
|
|
640
|
+
until: cmdOpts.until as string | undefined,
|
|
641
|
+
categories,
|
|
642
|
+
author: cmdOpts.author as string | undefined,
|
|
643
|
+
intent: cmdOpts.intent as string | undefined,
|
|
538
644
|
noExpand,
|
|
539
645
|
noRerank,
|
|
646
|
+
candidateLimit,
|
|
540
647
|
// Per spec: --answer defaults to false, --no-answer forces retrieval-only
|
|
541
648
|
// Commander creates separate cmdOpts.noAnswer for --no-answer flag
|
|
542
649
|
answer: Boolean(cmdOpts.answer),
|
package/src/config/types.ts
CHANGED
|
@@ -209,6 +209,8 @@ export const ModelConfigSchema = z.object({
|
|
|
209
209
|
loadTimeout: z.number().default(60_000),
|
|
210
210
|
/** Inference timeout in ms */
|
|
211
211
|
inferenceTimeout: z.number().default(30_000),
|
|
212
|
+
/** Context size used for query expansion generation */
|
|
213
|
+
expandContextSize: z.number().int().min(256).default(2_048),
|
|
212
214
|
/** Keep warm model TTL in ms (5 min) */
|
|
213
215
|
warmModelTtl: z.number().default(300_000),
|
|
214
216
|
});
|
package/src/core/links.ts
CHANGED
|
@@ -80,6 +80,10 @@ const UNSAFE_PERCENT_CODES = new Set(["%2F", "%5C", "%00", "%2f", "%5c"]);
|
|
|
80
80
|
*/
|
|
81
81
|
const WIKI_LINK_REGEX = /\[\[([^\]|]+(?:\|[^\]]+)?)\]\]/g;
|
|
82
82
|
|
|
83
|
+
/** Logseq embed: {{embed [[Page]]}} or {{embed ((block-id))}} */
|
|
84
|
+
const LOGSEQ_EMBED_REGEX =
|
|
85
|
+
/\{\{\s*embed\s+(\[\[[^\]]+\]\]|\(\([^)]+\)\))\s*\}\}/gi;
|
|
86
|
+
|
|
83
87
|
/**
|
|
84
88
|
* Markdown inline link: [text](url)
|
|
85
89
|
* Captures: 1=text, 2=url (path and optional anchor)
|
|
@@ -273,6 +277,43 @@ export function parseLinks(
|
|
|
273
277
|
): ParsedLink[] {
|
|
274
278
|
const links: ParsedLink[] = [];
|
|
275
279
|
|
|
280
|
+
const pushWikiLink = (
|
|
281
|
+
raw: string,
|
|
282
|
+
target: string,
|
|
283
|
+
startOffset: number,
|
|
284
|
+
endOffset: number,
|
|
285
|
+
displayText?: string
|
|
286
|
+
): void => {
|
|
287
|
+
const trimmedTarget = target.trim();
|
|
288
|
+
const hasScheme =
|
|
289
|
+
/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmedTarget) ||
|
|
290
|
+
trimmedTarget.startsWith("mailto:");
|
|
291
|
+
if (hasScheme || trimmedTarget.startsWith("//")) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const parts = parseTargetParts(trimmedTarget);
|
|
296
|
+
if (!parts.ref) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const startPos = offsetToPosition(startOffset, lineOffsets);
|
|
301
|
+
const endPos = offsetToPosition(endOffset, lineOffsets);
|
|
302
|
+
|
|
303
|
+
links.push({
|
|
304
|
+
kind: "wiki",
|
|
305
|
+
raw,
|
|
306
|
+
targetRef: parts.ref,
|
|
307
|
+
targetAnchor: parts.anchor,
|
|
308
|
+
targetCollection: parts.collection,
|
|
309
|
+
displayText,
|
|
310
|
+
startLine: startPos.line,
|
|
311
|
+
startCol: startPos.col,
|
|
312
|
+
endLine: endPos.line,
|
|
313
|
+
endCol: endPos.col,
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
|
|
276
317
|
// Parse wiki links
|
|
277
318
|
WIKI_LINK_REGEX.lastIndex = 0;
|
|
278
319
|
let match: RegExpExecArray | null;
|
|
@@ -281,6 +322,14 @@ export function parseLinks(
|
|
|
281
322
|
const startOffset = match.index;
|
|
282
323
|
const endOffset = startOffset + match[0].length;
|
|
283
324
|
|
|
325
|
+
// Skip [[target]] nested in Logseq alias syntax: [Display]([[target]])
|
|
326
|
+
if (
|
|
327
|
+
markdown.slice(Math.max(0, startOffset - 2), startOffset) === "](" &&
|
|
328
|
+
markdown.slice(endOffset, endOffset + 1) === ")"
|
|
329
|
+
) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
284
333
|
// Skip if inside excluded range
|
|
285
334
|
if (rangeIntersectsExcluded(startOffset, endOffset, excludedRanges)) {
|
|
286
335
|
continue;
|
|
@@ -307,31 +356,35 @@ export function parseLinks(
|
|
|
307
356
|
}
|
|
308
357
|
|
|
309
358
|
const trimmedTarget = targetPart.trim();
|
|
310
|
-
|
|
311
|
-
/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmedTarget) ||
|
|
312
|
-
trimmedTarget.startsWith("mailto:");
|
|
313
|
-
if (hasScheme || trimmedTarget.startsWith("//")) {
|
|
359
|
+
if (!trimmedTarget) {
|
|
314
360
|
continue;
|
|
315
361
|
}
|
|
362
|
+
pushWikiLink(match[0], trimmedTarget, startOffset, endOffset, displayText);
|
|
363
|
+
}
|
|
316
364
|
|
|
317
|
-
|
|
318
|
-
|
|
365
|
+
// Parse Logseq embeds as links
|
|
366
|
+
LOGSEQ_EMBED_REGEX.lastIndex = 0;
|
|
319
367
|
|
|
320
|
-
|
|
321
|
-
const
|
|
368
|
+
while ((match = LOGSEQ_EMBED_REGEX.exec(markdown)) !== null) {
|
|
369
|
+
const startOffset = match.index;
|
|
370
|
+
const endOffset = startOffset + match[0].length;
|
|
322
371
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
372
|
+
if (rangeIntersectsExcluded(startOffset, endOffset, excludedRanges)) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const embedTarget = match[1]?.trim();
|
|
377
|
+
if (!embedTarget) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (embedTarget.startsWith("((") && embedTarget.endsWith("))")) {
|
|
382
|
+
const blockId = embedTarget.slice(2, -2).trim();
|
|
383
|
+
if (blockId.length === 0) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
pushWikiLink(match[0], blockId, startOffset, endOffset);
|
|
387
|
+
}
|
|
335
388
|
}
|
|
336
389
|
|
|
337
390
|
// Parse markdown links
|
|
@@ -350,6 +403,25 @@ export function parseLinks(
|
|
|
350
403
|
const url = match[2];
|
|
351
404
|
if (!url) continue;
|
|
352
405
|
|
|
406
|
+
// Logseq alias syntax: [Display]([[Target]])
|
|
407
|
+
if (url.startsWith("[[") && url.endsWith("]]")) {
|
|
408
|
+
const innerTarget = url.slice(2, -2).trim();
|
|
409
|
+
if (innerTarget.length > 0) {
|
|
410
|
+
const displayText =
|
|
411
|
+
linkText && linkText !== innerTarget
|
|
412
|
+
? truncateText(linkText, MAX_DISPLAY_TEXT_GRAPHEMES)
|
|
413
|
+
: undefined;
|
|
414
|
+
pushWikiLink(
|
|
415
|
+
match[0],
|
|
416
|
+
innerTarget,
|
|
417
|
+
startOffset,
|
|
418
|
+
endOffset,
|
|
419
|
+
displayText
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
353
425
|
// Skip external URLs
|
|
354
426
|
if (EXTERNAL_URL_REGEX.test(url)) {
|
|
355
427
|
continue;
|