@gmickel/gno 0.36.0 → 0.37.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 +192 -9
- package/package.json +8 -1
- package/src/cli/commands/ask.ts +25 -7
- package/src/cli/commands/doctor.ts +17 -0
- package/src/cli/commands/embed.ts +2 -3
- package/src/cli/commands/query.ts +21 -6
- package/src/cli/commands/search.ts +3 -0
- package/src/cli/commands/vsearch.ts +10 -3
- package/src/cli/format/search-results.ts +58 -1
- package/src/cli/program.ts +38 -0
- package/src/config/types.ts +14 -0
- package/src/converters/mime.ts +9 -0
- package/src/ingestion/chunker.ts +186 -5
- package/src/ingestion/sync.ts +2 -1
- package/src/ingestion/types.ts +2 -1
- package/src/llm/registry.ts +22 -2
- package/src/mcp/tools/query.ts +17 -8
- package/src/mcp/tools/vsearch.ts +7 -3
- package/src/sdk/client.ts +34 -6
- package/src/sdk/embed.ts +7 -3
- package/src/sdk/types.ts +1 -0
- package/src/store/sqlite/adapter.ts +199 -25
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GNO
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Local search, retrieval, and synthesis for the files you actually work in.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@gmickel/gno)
|
|
6
6
|
[](./LICENSE)
|
|
@@ -12,7 +12,57 @@
|
|
|
12
12
|
|
|
13
13
|

|
|
14
14
|
|
|
15
|
-
GNO is a local knowledge engine
|
|
15
|
+
GNO is a local knowledge engine for notes, code, PDFs, Office docs, meeting transcripts, and reference material. It gives you fast keyword search, semantic retrieval, grounded answers with citations, wiki-style linking, and a real workspace UI, while keeping the whole stack local by default.
|
|
16
|
+
|
|
17
|
+
Use it when:
|
|
18
|
+
|
|
19
|
+
- your notes live in more than one folder
|
|
20
|
+
- your important knowledge is split across Markdown, code, PDFs, and Office files
|
|
21
|
+
- you want one retrieval layer that works from the CLI, browser, MCP, and a Bun/TypeScript SDK
|
|
22
|
+
- you want better local context for agents without shipping your docs to a cloud API
|
|
23
|
+
|
|
24
|
+
### What GNO Gives You
|
|
25
|
+
|
|
26
|
+
- **Fast local search**: BM25 for exact hits, vectors for concepts, hybrid for best quality
|
|
27
|
+
- **Real retrieval surfaces**: CLI, Web UI, REST API, MCP, SDK
|
|
28
|
+
- **Local-first answers**: grounded synthesis with citations when you want answers, raw retrieval when you do not
|
|
29
|
+
- **Connected knowledge**: backlinks, related notes, graph view, cross-collection navigation
|
|
30
|
+
- **Operational fit**: daemon mode, model presets, remote GPU backends, safe config/state on disk
|
|
31
|
+
|
|
32
|
+
### One-Minute Tour
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Install
|
|
36
|
+
bun install -g @gmickel/gno
|
|
37
|
+
|
|
38
|
+
# Add a few collections
|
|
39
|
+
gno init ~/notes --name notes
|
|
40
|
+
gno collection add ~/work/docs --name work-docs --pattern "**/*.{md,pdf,docx}"
|
|
41
|
+
gno collection add ~/work/gno/src --name gno-code --pattern "**/*.{ts,tsx,js,jsx}"
|
|
42
|
+
|
|
43
|
+
# Add context so retrieval results come back with the right framing
|
|
44
|
+
gno context add "notes:" "Personal notes, journal entries, and long-form ideas"
|
|
45
|
+
gno context add "work-docs:" "Architecture docs, runbooks, RFCs, meeting notes"
|
|
46
|
+
gno context add "gno-code:" "Source code for the GNO application"
|
|
47
|
+
|
|
48
|
+
# Index + embed
|
|
49
|
+
gno update --yes
|
|
50
|
+
gno embed
|
|
51
|
+
|
|
52
|
+
# Search in the way that fits the question
|
|
53
|
+
gno search "DEC-0054" # exact keyword / identifier
|
|
54
|
+
gno vsearch "retry failed jobs with backoff" # natural-language semantic lookup
|
|
55
|
+
gno query "JWT refresh token rotation" --explain # hybrid retrieval with score traces
|
|
56
|
+
|
|
57
|
+
# Retrieve documents or export context for an agent
|
|
58
|
+
gno get "gno://work-docs/architecture/auth.md"
|
|
59
|
+
gno multi-get "gno-code/**/*.ts" --max-bytes 30000 --md
|
|
60
|
+
gno query "deployment process" --all --files --min-score 0.35
|
|
61
|
+
|
|
62
|
+
# Run the workspace
|
|
63
|
+
gno serve
|
|
64
|
+
gno daemon
|
|
65
|
+
```
|
|
16
66
|
|
|
17
67
|
---
|
|
18
68
|
|
|
@@ -265,6 +315,14 @@ headless. In v0.30 it is foreground-only and does not expose built-in
|
|
|
265
315
|
|
|
266
316
|
Embed GNO directly in another Bun or TypeScript app. No CLI subprocesses. No local server required.
|
|
267
317
|
|
|
318
|
+
Install:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
bun add @gmickel/gno
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Minimal client:
|
|
325
|
+
|
|
268
326
|
```ts
|
|
269
327
|
import { createDefaultConfig, createGnoClient } from "@gmickel/gno";
|
|
270
328
|
|
|
@@ -295,6 +353,43 @@ console.log(results.results[0]?.uri);
|
|
|
295
353
|
await client.close();
|
|
296
354
|
```
|
|
297
355
|
|
|
356
|
+
More SDK examples:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { createGnoClient } from "@gmickel/gno";
|
|
360
|
+
|
|
361
|
+
const client = await createGnoClient({
|
|
362
|
+
configPath: "/Users/me/.config/gno/index.yml",
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Fast exact search
|
|
366
|
+
const bm25 = await client.search("DEC-0054", {
|
|
367
|
+
collection: "work-docs",
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Semantic code lookup
|
|
371
|
+
const semantic = await client.vsearch("retry failed jobs with backoff", {
|
|
372
|
+
collection: "gno-code",
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Hybrid retrieval with explicit intent
|
|
376
|
+
const hybrid = await client.query("token refresh", {
|
|
377
|
+
collection: "work-docs",
|
|
378
|
+
intent: "JWT refresh token rotation in our auth stack",
|
|
379
|
+
candidateLimit: 12,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Fetch content directly
|
|
383
|
+
const doc = await client.get("gno://work-docs/auth/refresh.md");
|
|
384
|
+
const bundle = await client.multiGet(["gno-code/**/*.ts"], { maxBytes: 25000 });
|
|
385
|
+
|
|
386
|
+
// Indexing / embedding
|
|
387
|
+
await client.update({ collection: "work-docs" });
|
|
388
|
+
await client.embed({ collection: "gno-code" });
|
|
389
|
+
|
|
390
|
+
await client.close();
|
|
391
|
+
```
|
|
392
|
+
|
|
298
393
|
Core SDK surface:
|
|
299
394
|
|
|
300
395
|
- `createGnoClient({ config | configPath, dbPath? })`
|
|
@@ -303,12 +398,6 @@ Core SDK surface:
|
|
|
303
398
|
- `update`, `embed`, `index`
|
|
304
399
|
- `close`
|
|
305
400
|
|
|
306
|
-
Install in an app:
|
|
307
|
-
|
|
308
|
-
```bash
|
|
309
|
-
bun add @gmickel/gno
|
|
310
|
-
```
|
|
311
|
-
|
|
312
401
|
Full guide: [SDK docs](https://gno.sh/docs/SDK/)
|
|
313
402
|
|
|
314
403
|
---
|
|
@@ -338,6 +427,31 @@ gno ask "what did we decide" --answer # AI synthesis
|
|
|
338
427
|
|
|
339
428
|
Output formats: `--json`, `--files`, `--csv`, `--md`, `--xml`
|
|
340
429
|
|
|
430
|
+
### Common CLI Recipes
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
# Search one collection
|
|
434
|
+
gno search "PostgreSQL connection pool" --collection work-docs
|
|
435
|
+
|
|
436
|
+
# Export retrieval results for an agent
|
|
437
|
+
gno query "authentication flow" --json -n 10
|
|
438
|
+
gno query "deployment rollback" --all --files --min-score 0.4
|
|
439
|
+
|
|
440
|
+
# Retrieve a document by URI or docid
|
|
441
|
+
gno get "gno://work-docs/runbooks/deploy.md"
|
|
442
|
+
gno get "#abc123"
|
|
443
|
+
|
|
444
|
+
# Fetch many documents at once
|
|
445
|
+
gno multi-get "work-docs/**/*.md" --max-bytes 20000 --md
|
|
446
|
+
|
|
447
|
+
# Inspect how the hybrid rank was assembled
|
|
448
|
+
gno query "refresh token rotation" --explain
|
|
449
|
+
|
|
450
|
+
# Work with filters
|
|
451
|
+
gno query "meeting notes" --since "last month" --category "meeting,notes"
|
|
452
|
+
gno search "incident review" --tags-all "status/active,team/platform"
|
|
453
|
+
```
|
|
454
|
+
|
|
341
455
|
### Retrieval V2 Controls
|
|
342
456
|
|
|
343
457
|
Existing query calls still work. Retrieval v2 adds optional structured intent control and deeper explain output.
|
|
@@ -382,6 +496,20 @@ gno skill install --scope user
|
|
|
382
496
|
|
|
383
497
|
Then ask your agent: _"Search my notes for the auth discussion"_
|
|
384
498
|
|
|
499
|
+
Agent-friendly CLI examples:
|
|
500
|
+
|
|
501
|
+
```bash
|
|
502
|
+
# Structured retrieval output for an agent
|
|
503
|
+
gno query "authentication" --json -n 10
|
|
504
|
+
|
|
505
|
+
# File list for downstream retrieval
|
|
506
|
+
gno query "error handling" --all --files --min-score 0.35
|
|
507
|
+
|
|
508
|
+
# Full document content when the agent already knows the ref
|
|
509
|
+
gno get "gno://work-docs/api-reference.md" --full
|
|
510
|
+
gno multi-get "work-docs/**/*.md" --md --max-bytes 30000
|
|
511
|
+
```
|
|
512
|
+
|
|
385
513
|
[Skill setup guide →](https://gno.sh/docs/integrations/skills/)
|
|
386
514
|
|
|
387
515
|
### MCP Server
|
|
@@ -655,7 +783,7 @@ See:
|
|
|
655
783
|
Offload inference to a GPU server on your network:
|
|
656
784
|
|
|
657
785
|
```yaml
|
|
658
|
-
# ~/.config/gno/
|
|
786
|
+
# ~/.config/gno/index.yml
|
|
659
787
|
models:
|
|
660
788
|
activePreset: remote-gpu
|
|
661
789
|
presets:
|
|
@@ -715,6 +843,61 @@ bun run eval:hybrid:delta
|
|
|
715
843
|
- Benchmark guide: [evals/README.md](./evals/README.md)
|
|
716
844
|
- Latest baseline snapshot: [evals/fixtures/hybrid-baseline/latest.json](./evals/fixtures/hybrid-baseline/latest.json)
|
|
717
845
|
|
|
846
|
+
### Code Embedding Benchmark Harness
|
|
847
|
+
|
|
848
|
+
GNO also has a dedicated harness for comparing alternate embedding models on code retrieval without touching product defaults:
|
|
849
|
+
|
|
850
|
+
```bash
|
|
851
|
+
# Establish the current incumbent baseline
|
|
852
|
+
bun run bench:code-embeddings --candidate bge-m3-incumbent --write
|
|
853
|
+
|
|
854
|
+
# Add candidate model URIs to the search space, then inspect them
|
|
855
|
+
bun run research:embeddings:autonomous:list-search-candidates
|
|
856
|
+
|
|
857
|
+
# Benchmark one candidate explicitly
|
|
858
|
+
bun run research:embeddings:autonomous:run-candidate bge-m3-incumbent
|
|
859
|
+
|
|
860
|
+
# Or let the bounded search harness walk the remaining candidates later
|
|
861
|
+
bun run research:embeddings:autonomous:search --dry-run
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
See [research/embeddings/README.md](./research/embeddings/README.md).
|
|
865
|
+
|
|
866
|
+
If a model turns out to be better specifically for code, the intended user story is:
|
|
867
|
+
|
|
868
|
+
- keep the default global preset for mixed prose/docs collections
|
|
869
|
+
- use per-collection `models.embed` overrides for code collections
|
|
870
|
+
|
|
871
|
+
That lets GNO stay sane by default while still giving power users a clean path to code-specialist retrieval.
|
|
872
|
+
|
|
873
|
+
Current code-focused recommendation:
|
|
874
|
+
|
|
875
|
+
```yaml
|
|
876
|
+
collections:
|
|
877
|
+
- name: gno-code
|
|
878
|
+
path: /Users/you/work/gno/src
|
|
879
|
+
pattern: "**/*.{ts,tsx,js,jsx,go,rs,py,swift,c}"
|
|
880
|
+
models:
|
|
881
|
+
embed: "hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf"
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
GNO treats that override like any other model URI:
|
|
885
|
+
|
|
886
|
+
- auto-downloads on first use by default
|
|
887
|
+
- manual-only if `GNO_NO_AUTO_DOWNLOAD=1`
|
|
888
|
+
- offline-safe if the model is already cached
|
|
889
|
+
|
|
890
|
+
Why this is the current recommendation:
|
|
891
|
+
|
|
892
|
+
- matches `bge-m3` on the tiny canonical benchmark
|
|
893
|
+
- significantly beats `bge-m3` on the real GNO `src/serve` code slice
|
|
894
|
+
- also beats `bge-m3` on a pinned public-OSS code slice
|
|
895
|
+
|
|
896
|
+
Trade-off:
|
|
897
|
+
|
|
898
|
+
- Qwen is slower to embed than `bge-m3`
|
|
899
|
+
- use it where code retrieval quality matters, not necessarily as the global default for every collection
|
|
900
|
+
|
|
718
901
|
---
|
|
719
902
|
|
|
720
903
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.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",
|
|
@@ -69,6 +69,8 @@
|
|
|
69
69
|
"eval:hybrid": "bun --bun evalite evals/hybrid.eval.ts",
|
|
70
70
|
"eval:hybrid:baseline": "bun scripts/hybrid-benchmark.ts --write",
|
|
71
71
|
"eval:hybrid:delta": "bun scripts/hybrid-benchmark.ts --delta",
|
|
72
|
+
"bench:code-embeddings": "bun scripts/code-embedding-benchmark.ts",
|
|
73
|
+
"bench:code-embeddings:write": "bun scripts/code-embedding-benchmark.ts --write",
|
|
72
74
|
"eval:retrieval-candidates": "bun scripts/retrieval-candidate-benchmark.ts",
|
|
73
75
|
"eval:retrieval-candidates:write": "bun scripts/retrieval-candidate-benchmark.ts --write",
|
|
74
76
|
"eval:watch": "bun --bun evalite watch",
|
|
@@ -83,6 +85,11 @@
|
|
|
83
85
|
"research:finetune:autonomous:confirm-winner": "bun research/finetune/autonomous/scripts/confirm-winner.ts",
|
|
84
86
|
"research:finetune:autonomous:check-promotion-targets": "bun research/finetune/autonomous/scripts/check-promotion-targets.ts",
|
|
85
87
|
"research:finetune:validate": "bun research/finetune/scripts/validate-sandbox.ts",
|
|
88
|
+
"research:embeddings:autonomous:list-search-candidates": "bun research/embeddings/autonomous/scripts/list-search-candidates.ts",
|
|
89
|
+
"research:embeddings:autonomous:run-candidate": "bun research/embeddings/autonomous/scripts/run-candidate.ts",
|
|
90
|
+
"research:embeddings:autonomous:leaderboard": "bun research/embeddings/autonomous/scripts/leaderboard.ts",
|
|
91
|
+
"research:embeddings:autonomous:confirm-winner": "bun research/embeddings/autonomous/scripts/confirm-winner.ts",
|
|
92
|
+
"research:embeddings:autonomous:search": "bun research/embeddings/autonomous/scripts/search.ts",
|
|
86
93
|
"research:finetune:qmd-import:legacy": "bun research/finetune/scripts/import-qmd-training.ts",
|
|
87
94
|
"research:finetune:mlx:build-dataset": "bun research/finetune/scripts/build-mlx-dataset.ts",
|
|
88
95
|
"research:finetune:build-variant-dataset": "bun research/finetune/scripts/build-variant-dataset.ts",
|
package/src/cli/commands/ask.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { AskOptions, AskResult, Citation } from "../../pipeline/types";
|
|
|
14
14
|
|
|
15
15
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
16
16
|
import { resolveDownloadPolicy } from "../../llm/policy";
|
|
17
|
-
import {
|
|
17
|
+
import { resolveModelUri } from "../../llm/registry";
|
|
18
18
|
import {
|
|
19
19
|
generateGroundedAnswer,
|
|
20
20
|
processAnswerResult,
|
|
@@ -90,7 +90,6 @@ export async function ask(
|
|
|
90
90
|
let rerankPort: RerankPort | null = null;
|
|
91
91
|
|
|
92
92
|
try {
|
|
93
|
-
const preset = getActivePreset(config);
|
|
94
93
|
const llm = new LlmAdapter(config);
|
|
95
94
|
|
|
96
95
|
// Resolve download policy from env/flags
|
|
@@ -106,7 +105,12 @@ export async function ask(
|
|
|
106
105
|
: undefined;
|
|
107
106
|
|
|
108
107
|
// Create embedding port
|
|
109
|
-
const embedUri =
|
|
108
|
+
const embedUri = resolveModelUri(
|
|
109
|
+
config,
|
|
110
|
+
"embed",
|
|
111
|
+
options.embedModel,
|
|
112
|
+
options.collection
|
|
113
|
+
);
|
|
110
114
|
const embedResult = await llm.createEmbeddingPort(embedUri, {
|
|
111
115
|
policy,
|
|
112
116
|
onProgress: downloadProgress
|
|
@@ -119,8 +123,12 @@ export async function ask(
|
|
|
119
123
|
|
|
120
124
|
// Create expansion port when expansion is enabled.
|
|
121
125
|
if (!options.noExpand && !options.queryModes?.length) {
|
|
122
|
-
const expandUri =
|
|
123
|
-
|
|
126
|
+
const expandUri = resolveModelUri(
|
|
127
|
+
config,
|
|
128
|
+
"expand",
|
|
129
|
+
options.expandModel ?? options.genModel,
|
|
130
|
+
options.collection
|
|
131
|
+
);
|
|
124
132
|
const genResult = await llm.createExpansionPort(expandUri, {
|
|
125
133
|
policy,
|
|
126
134
|
onProgress: downloadProgress
|
|
@@ -134,7 +142,12 @@ export async function ask(
|
|
|
134
142
|
|
|
135
143
|
// Create answer generation port when answers are requested.
|
|
136
144
|
if (options.answer) {
|
|
137
|
-
const genUri =
|
|
145
|
+
const genUri = resolveModelUri(
|
|
146
|
+
config,
|
|
147
|
+
"gen",
|
|
148
|
+
options.genModel,
|
|
149
|
+
options.collection
|
|
150
|
+
);
|
|
138
151
|
const genResult = await llm.createGenerationPort(genUri, {
|
|
139
152
|
policy,
|
|
140
153
|
onProgress: downloadProgress
|
|
@@ -148,7 +161,12 @@ export async function ask(
|
|
|
148
161
|
|
|
149
162
|
// Create rerank port (unless --fast or --no-rerank)
|
|
150
163
|
if (!options.noRerank) {
|
|
151
|
-
const rerankUri =
|
|
164
|
+
const rerankUri = resolveModelUri(
|
|
165
|
+
config,
|
|
166
|
+
"rerank",
|
|
167
|
+
options.rerankModel,
|
|
168
|
+
options.collection
|
|
169
|
+
);
|
|
152
170
|
const rerankResult = await llm.createRerankPort(rerankUri, {
|
|
153
171
|
policy,
|
|
154
172
|
onProgress: downloadProgress
|
|
@@ -14,6 +14,7 @@ import type { Config } from "../../config/types";
|
|
|
14
14
|
|
|
15
15
|
import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
|
|
16
16
|
import { getConfigPaths, isInitialized, loadConfig } from "../../config";
|
|
17
|
+
import { getCodeChunkingStatus } from "../../ingestion/chunker";
|
|
17
18
|
import { ModelCache } from "../../llm/cache";
|
|
18
19
|
import { getActivePreset } from "../../llm/registry";
|
|
19
20
|
import { loadFts5Snowball } from "../../store/sqlite/fts5-snowball";
|
|
@@ -122,6 +123,19 @@ async function checkModels(config: Config): Promise<DoctorCheck[]> {
|
|
|
122
123
|
return checks;
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
function checkCodeChunking(): DoctorCheck {
|
|
127
|
+
const status = getCodeChunkingStatus();
|
|
128
|
+
return {
|
|
129
|
+
name: "code-chunking",
|
|
130
|
+
status: "ok",
|
|
131
|
+
message: `${status.mode} structural chunking for ${status.supportedExtensions.join(", ")}`,
|
|
132
|
+
details: [
|
|
133
|
+
"Unsupported extensions fall back to the default markdown chunker.",
|
|
134
|
+
"Chunking mode is automatic-only in the first pass.",
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
125
139
|
async function checkNodeLlamaCpp(): Promise<DoctorCheck> {
|
|
126
140
|
try {
|
|
127
141
|
const { getLlama } = await import("node-llama-cpp");
|
|
@@ -319,6 +333,9 @@ export async function doctor(
|
|
|
319
333
|
const sqliteChecks = await checkSqliteExtensions();
|
|
320
334
|
checks.push(...sqliteChecks);
|
|
321
335
|
|
|
336
|
+
// Code chunking capability
|
|
337
|
+
checks.push(checkCodeChunking());
|
|
338
|
+
|
|
322
339
|
// Determine overall health
|
|
323
340
|
const hasErrors = checks.some((c) => c.status === "error");
|
|
324
341
|
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "../../config";
|
|
20
20
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
21
21
|
import { resolveDownloadPolicy } from "../../llm/policy";
|
|
22
|
-
import {
|
|
22
|
+
import { resolveModelUri } from "../../llm/registry";
|
|
23
23
|
import { formatDocForEmbedding } from "../../pipeline/contextual";
|
|
24
24
|
import { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
25
25
|
import { err, ok } from "../../store/types";
|
|
@@ -271,8 +271,7 @@ async function initEmbedContext(
|
|
|
271
271
|
return { ok: false, error: `Collection not found: ${collection}` };
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
const
|
|
275
|
-
const modelUri = model ?? preset.embed;
|
|
274
|
+
const modelUri = resolveModelUri(config, "embed", model, collection);
|
|
276
275
|
|
|
277
276
|
const store = new SqliteAdapter();
|
|
278
277
|
const dbPath = getIndexDbPath();
|
|
@@ -14,7 +14,7 @@ import type { HybridSearchOptions, SearchResults } from "../../pipeline/types";
|
|
|
14
14
|
|
|
15
15
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
16
16
|
import { resolveDownloadPolicy } from "../../llm/policy";
|
|
17
|
-
import {
|
|
17
|
+
import { resolveModelUri } from "../../llm/registry";
|
|
18
18
|
import { type HybridSearchDeps, searchHybrid } from "../../pipeline/hybrid";
|
|
19
19
|
import {
|
|
20
20
|
createVectorIndexPort,
|
|
@@ -58,6 +58,7 @@ export interface QueryFormatOptions {
|
|
|
58
58
|
format: "terminal" | "json" | "files" | "csv" | "md" | "xml";
|
|
59
59
|
full?: boolean;
|
|
60
60
|
lineNumbers?: boolean;
|
|
61
|
+
terminalLinks?: import("../format/search-results").FormatOptions["terminalLinks"];
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export type QueryResult =
|
|
@@ -97,7 +98,6 @@ export async function query(
|
|
|
97
98
|
let rerankPort: RerankPort | null = null;
|
|
98
99
|
|
|
99
100
|
try {
|
|
100
|
-
const preset = getActivePreset(config);
|
|
101
101
|
const llm = new LlmAdapter(config);
|
|
102
102
|
|
|
103
103
|
// Resolve download policy from env/flags
|
|
@@ -113,7 +113,12 @@ export async function query(
|
|
|
113
113
|
: undefined;
|
|
114
114
|
|
|
115
115
|
// Create embedding port (for vector search)
|
|
116
|
-
const embedUri =
|
|
116
|
+
const embedUri = resolveModelUri(
|
|
117
|
+
config,
|
|
118
|
+
"embed",
|
|
119
|
+
options.embedModel,
|
|
120
|
+
options.collection
|
|
121
|
+
);
|
|
117
122
|
const embedResult = await llm.createEmbeddingPort(embedUri, {
|
|
118
123
|
policy,
|
|
119
124
|
onProgress: downloadProgress
|
|
@@ -127,8 +132,12 @@ export async function query(
|
|
|
127
132
|
// Create expansion port - optional.
|
|
128
133
|
// Skip when structured query modes are provided.
|
|
129
134
|
if (!options.noExpand && !options.queryModes?.length) {
|
|
130
|
-
const expandUri =
|
|
131
|
-
|
|
135
|
+
const expandUri = resolveModelUri(
|
|
136
|
+
config,
|
|
137
|
+
"expand",
|
|
138
|
+
options.expandModel ?? options.genModel,
|
|
139
|
+
options.collection
|
|
140
|
+
);
|
|
132
141
|
const genResult = await llm.createExpansionPort(expandUri, {
|
|
133
142
|
policy,
|
|
134
143
|
onProgress: downloadProgress
|
|
@@ -142,7 +151,12 @@ export async function query(
|
|
|
142
151
|
|
|
143
152
|
// Create rerank port - optional
|
|
144
153
|
if (!options.noRerank) {
|
|
145
|
-
const rerankUri =
|
|
154
|
+
const rerankUri = resolveModelUri(
|
|
155
|
+
config,
|
|
156
|
+
"rerank",
|
|
157
|
+
options.rerankModel,
|
|
158
|
+
options.collection
|
|
159
|
+
);
|
|
146
160
|
const rerankResult = await llm.createRerankPort(rerankUri, {
|
|
147
161
|
policy,
|
|
148
162
|
onProgress: downloadProgress
|
|
@@ -260,5 +274,6 @@ export function formatQuery(
|
|
|
260
274
|
format: options.format,
|
|
261
275
|
full: options.full,
|
|
262
276
|
lineNumbers: options.lineNumbers,
|
|
277
|
+
terminalLinks: options.terminalLinks,
|
|
263
278
|
});
|
|
264
279
|
}
|
|
@@ -31,6 +31,8 @@ export type SearchCommandOptions = SearchOptions & {
|
|
|
31
31
|
xml?: boolean;
|
|
32
32
|
/** Output files only */
|
|
33
33
|
files?: boolean;
|
|
34
|
+
/** Terminal hyperlink policy */
|
|
35
|
+
terminalLinks?: FormatOptions["terminalLinks"];
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
export type SearchResult =
|
|
@@ -132,6 +134,7 @@ export function formatSearch(
|
|
|
132
134
|
format: getFormatType(options),
|
|
133
135
|
full: options.full,
|
|
134
136
|
lineNumbers: options.lineNumbers,
|
|
137
|
+
terminalLinks: options.terminalLinks,
|
|
135
138
|
};
|
|
136
139
|
|
|
137
140
|
return formatSearchResults(result.data, formatOpts);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { SearchOptions, SearchResults } from "../../pipeline/types";
|
|
9
9
|
|
|
10
10
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
11
|
-
import {
|
|
11
|
+
import { resolveModelUri } from "../../llm/registry";
|
|
12
12
|
import { formatQueryForEmbedding } from "../../pipeline/contextual";
|
|
13
13
|
import {
|
|
14
14
|
searchVectorWithEmbedding,
|
|
@@ -40,6 +40,8 @@ export type VsearchCommandOptions = SearchOptions & {
|
|
|
40
40
|
xml?: boolean;
|
|
41
41
|
/** Output files only */
|
|
42
42
|
files?: boolean;
|
|
43
|
+
/** Terminal hyperlink policy */
|
|
44
|
+
terminalLinks?: FormatOptions["terminalLinks"];
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
export type VsearchResult =
|
|
@@ -76,8 +78,12 @@ export async function vsearch(
|
|
|
76
78
|
|
|
77
79
|
try {
|
|
78
80
|
// Get model URI from preset
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
+
const modelUri = resolveModelUri(
|
|
82
|
+
config,
|
|
83
|
+
"embed",
|
|
84
|
+
options.model,
|
|
85
|
+
options.collection
|
|
86
|
+
);
|
|
81
87
|
|
|
82
88
|
// Create LLM adapter for embeddings
|
|
83
89
|
const llm = new LlmAdapter(config);
|
|
@@ -187,6 +193,7 @@ export function formatVsearch(
|
|
|
187
193
|
format: getFormatType(options),
|
|
188
194
|
full: options.full,
|
|
189
195
|
lineNumbers: options.lineNumbers,
|
|
196
|
+
terminalLinks: options.terminalLinks,
|
|
190
197
|
};
|
|
191
198
|
|
|
192
199
|
return formatSearchResults(result.data, formatOpts);
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* @module src/cli/format/searchResults
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
|
|
8
10
|
import type { SearchResults } from "../../pipeline/types";
|
|
9
11
|
|
|
10
12
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -15,6 +17,10 @@ export interface FormatOptions {
|
|
|
15
17
|
full?: boolean;
|
|
16
18
|
lineNumbers?: boolean;
|
|
17
19
|
format: "terminal" | "json" | "files" | "csv" | "md" | "xml";
|
|
20
|
+
terminalLinks?: {
|
|
21
|
+
isTTY: boolean;
|
|
22
|
+
editorUriTemplate?: string | null;
|
|
23
|
+
};
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -76,7 +82,9 @@ function formatTerminal(data: SearchResults, options: FormatOptions): string {
|
|
|
76
82
|
|
|
77
83
|
const lines: string[] = [];
|
|
78
84
|
for (const r of data.results) {
|
|
79
|
-
lines.push(
|
|
85
|
+
lines.push(
|
|
86
|
+
`[${r.docid}] ${formatTerminalUri(r, options)} (score: ${r.score.toFixed(2)})`
|
|
87
|
+
);
|
|
80
88
|
if (r.title) {
|
|
81
89
|
lines.push(` ${r.title}`);
|
|
82
90
|
}
|
|
@@ -100,6 +108,55 @@ function formatTerminal(data: SearchResults, options: FormatOptions): string {
|
|
|
100
108
|
return lines.join("\n");
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
function formatTerminalUri(
|
|
112
|
+
result: SearchResults["results"][number],
|
|
113
|
+
options: FormatOptions
|
|
114
|
+
): string {
|
|
115
|
+
const links = options.terminalLinks;
|
|
116
|
+
if (!links?.isTTY || !result.source.absPath) {
|
|
117
|
+
return result.uri;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const target = buildTerminalLinkTarget(
|
|
121
|
+
result.source.absPath,
|
|
122
|
+
result.snippetRange?.startLine,
|
|
123
|
+
links.editorUriTemplate ?? undefined
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (!target) {
|
|
127
|
+
return result.uri;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return wrapOsc8(target, result.uri);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildTerminalLinkTarget(
|
|
134
|
+
absPath: string,
|
|
135
|
+
line: number | undefined,
|
|
136
|
+
template?: string
|
|
137
|
+
): string | null {
|
|
138
|
+
if (template) {
|
|
139
|
+
if (line === undefined && template.includes("{line}")) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const withPath = template.replaceAll("{path}", absPath);
|
|
144
|
+
const withLine = withPath.replaceAll(
|
|
145
|
+
"{line}",
|
|
146
|
+
line !== undefined ? String(line) : ""
|
|
147
|
+
);
|
|
148
|
+
return withLine.replaceAll("{col}", line !== undefined ? "1" : "");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return pathToFileURL(absPath).toString();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function wrapOsc8(target: string, label: string): string {
|
|
155
|
+
const OSC = "\u001B]8;;";
|
|
156
|
+
const BEL = "\u0007";
|
|
157
|
+
return `${OSC}${target}${BEL}${label}${OSC}${BEL}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
103
160
|
function formatMarkdown(data: SearchResults, options: FormatOptions): string {
|
|
104
161
|
const modeLabel = data.meta.mode === "vector" ? "Vector " : "";
|
|
105
162
|
if (data.results.length === 0) {
|