grepmax 0.26.70 → 0.26.72
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/.claude-plugin/marketplace.json +1 -1
- package/DOCUMENT-SEARCH.md +134 -0
- package/README.md +96 -0
- package/dist/bin.js +11 -2
- package/dist/lib/daemon/daemon.js +50 -0
- package/dist/lib/daemon/document-search-handler.js +194 -0
- package/dist/lib/daemon/ipc-handler.js +29 -2
- package/dist/lib/mcp/document-contract.js +281 -0
- package/dist/lib/mcp/document-search.js +274 -0
- package/dist/lib/store/vector-db.js +13 -0
- package/dist/lib/utils/resource-budget.js +39 -1
- package/dist/lib/workers/embeddings/granite.js +8 -3
- package/dist/lib/workers/embeddings/mlx-client.js +27 -0
- package/dist/lib/workers/orchestrator.js +47 -9
- package/dist/lib/workers/pool.js +192 -4
- package/dist/lib/workers/process-child.js +22 -5
- package/dist/lib/workers/worker.js +4 -1
- package/package.json +5 -3
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "grepmax",
|
|
11
11
|
"source": "./plugins/grepmax",
|
|
12
12
|
"description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
|
|
13
|
-
"version": "0.26.
|
|
13
|
+
"version": "0.26.72",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Robert Owens",
|
|
16
16
|
"email": "robowens@me.com"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Existing-index document search (contract v1)
|
|
2
|
+
|
|
3
|
+
`gmax mcp --existing-index-only` serves two read-only stdio MCP tools. The exact
|
|
4
|
+
entry dispatch precedes the normal CLI. Server identity is
|
|
5
|
+
`gmax-document-search`; the 2025-06-18 initialize exchange is supported by the
|
|
6
|
+
current MCP SDK. Contract version is independent of package version.
|
|
7
|
+
|
|
8
|
+
Every tool returns `structuredContent` with `contractVersion: 1` and capabilities
|
|
9
|
+
`{existingIndexOnly: 1, queryLogging: false, watch: false, runtimeStartup: false}`.
|
|
10
|
+
The text content contains only the result state. No source/snippet bodies are
|
|
11
|
+
returned. Runlist has no gmax, model or SDK runtime dependency.
|
|
12
|
+
|
|
13
|
+
- `document_search_status({paths})` returns `state: ready`, a required positive
|
|
14
|
+
safe-integer `generation`, `project: {root,store,lastIndexed}`,
|
|
15
|
+
actual metadata `covered` paths, `coverage: {requested,indexed,partial}`,
|
|
16
|
+
`embeddingReady`, `queryState` and index state. Readiness does not guarantee
|
|
17
|
+
worker availability when the subsequent query arrives. Coverage is not a
|
|
18
|
+
registry/store-wide chunk count and does not prove current source freshness.
|
|
19
|
+
Covered paths retain a validated `indexedMtimeMs` where available. A missing
|
|
20
|
+
sampled Markdown file reduces coverage instead of discarding other paths;
|
|
21
|
+
malformed or out-of-scope paths still refuse. Counts are recomputed, and
|
|
22
|
+
metadata uses a bounded field allowlist without free-form diagnostics.
|
|
23
|
+
- `semantic_search({query,prefixes})` returns `state: ready`, canonical `root`,
|
|
24
|
+
selected `store`, required `generation`, `matches` and index state. Each pointer is
|
|
25
|
+
`{path,startLine,endLine,score,hash?,hashAlgorithm?}`. Ranges are one-based full
|
|
26
|
+
source ranges, converted from indexed zero-based lines. Score is
|
|
27
|
+
`1/(1+max(0,cosineDistance))`, not a relevance probability.
|
|
28
|
+
- `hashAlgorithm: sha256-bytes` is declared only when the vector row digest agrees
|
|
29
|
+
with current version-1 metadata whose digest hashes the exact indexed bytes.
|
|
30
|
+
Legacy/mismatched provenance retains document-only pointers. Consumers must
|
|
31
|
+
reread and authorize current source, verify the digest and map body lines before
|
|
32
|
+
jumping to a section. This bridge never hashes new bytes to bless old lines.
|
|
33
|
+
|
|
34
|
+
Limits: 500 query characters, 32 explicit prefixes, 2,000 coverage paths and a
|
|
35
|
+
512 KiB serialized path budget, 50 vector candidates, 1 MiB transport frames and
|
|
36
|
+
a 10-second bridge operation deadline. The most specific registered ancestor
|
|
37
|
+
of the canonical checkout selects the primary store; registered spelling is
|
|
38
|
+
preserved for daemon/row lookups. Coverage and prefixes must resolve inside the
|
|
39
|
+
checkout and selected project. Secondary stores are unavailable. No candidate
|
|
40
|
+
refill broadens scope. Symlink targets are rechecked before pointers are returned.
|
|
41
|
+
|
|
42
|
+
Unavailable states: `unsupported_tool`, `unsupported_daemon`, `no_index`,
|
|
43
|
+
`index_unavailable`, `daemon_unavailable`, `store_unavailable`,
|
|
44
|
+
`embedding_unavailable`, `embedding_mismatch`, `host_pressure`, `busy`,
|
|
45
|
+
`cancelled`, `timeout`, `no_coverage`, `search_unavailable`. Envelopes contain
|
|
46
|
+
stable states, never raw query-bearing diagnostics. Schema/unknown-tool errors
|
|
47
|
+
remain standard MCP errors.
|
|
48
|
+
|
|
49
|
+
The supported bridge negotiates daemon `existingIndexOnlySearch: 1` and resource
|
|
50
|
+
generation. Missing, null, non-integer and changed generations refuse; Runlist
|
|
51
|
+
also fences status against subsequent retrieval. Private daemon commands are `documents.status` and `documents.search`;
|
|
52
|
+
Runlist does not call the socket API. The daemon validates roots, stores, bounds
|
|
53
|
+
and generation again under its shared-operation coordinator, including after
|
|
54
|
+
encoding and native retrieval. Paused/contained resources refuse rather than
|
|
55
|
+
return keyword results.
|
|
56
|
+
|
|
57
|
+
Retrieval in this first slice is **dense-only** against an already-open LanceDB
|
|
58
|
+
connection/table. It never invokes normal hybrid search, FTS creation, reranking,
|
|
59
|
+
ColBERT inference, `ensureTable`, schema evolution, index mutation or local-store
|
|
60
|
+
fallback. Query encoding uses a currently idle, already-warm compatible worker:
|
|
61
|
+
CPU requires a loaded Granite tokenizer/session; GPU requires cached positive
|
|
62
|
+
confirmation of the existing configured MLX model. Idle time alone does not revoke
|
|
63
|
+
that identity: each bounded `/embed` response revalidates model and dimensions,
|
|
64
|
+
and successful inference renews the normal health cache. Failure revokes restricted
|
|
65
|
+
eligibility until external work confirms readiness. A worker rechecks before inference;
|
|
66
|
+
no initialization, health polling, retry or backend fallback is allowed. Cold
|
|
67
|
+
workers refuse immediately. When compatible warm workers are occupied, at most
|
|
68
|
+
four document queries may wait FIFO for up to two seconds for a warm worker.
|
|
69
|
+
They get a turn between indexing files; after four consecutive document-query
|
|
70
|
+
assignments, waiting ordinary work gets a turn. A full queue or expired worker
|
|
71
|
+
wait returns `busy`. `queryState: ready` describes admission availability, including
|
|
72
|
+
this bounded wait, and does not guarantee immediate execution or query success.
|
|
73
|
+
Waiting requests live outside the ordinary worker queues and cannot trigger
|
|
74
|
+
worker startup, replacement or scaling. Existing ordinary work retains its own
|
|
75
|
+
admitted lifecycle, including independent expansion/replacement.
|
|
76
|
+
|
|
77
|
+
Admission and warm readiness are rechecked when a worker becomes available.
|
|
78
|
+
Queued cancellation/deadlines withdraw the request; cancellation after dispatch
|
|
79
|
+
keeps the shared worker assigned until its actual terminal response or exit.
|
|
80
|
+
The ten-second request bound includes waiting rather than restarting at dispatch.
|
|
81
|
+
Synchronous admission has its own probe bound and is checked against the absolute
|
|
82
|
+
request deadline before any query is sent. Native retrieval still uses the
|
|
83
|
+
remaining overall deadline. An in-flight indexing file is never interrupted;
|
|
84
|
+
long files or backend outages can still exceed the worker wait and return `busy`.
|
|
85
|
+
|
|
86
|
+
Read-only admission honors the configured host guard policy without creating locks,
|
|
87
|
+
reservations, pruning records or changing containment markers. Under the default
|
|
88
|
+
`strict` policy, existing Darwin inference requires fresh known-normal aggregate
|
|
89
|
+
resources and reads the existing ledger; absent ledgers are treated as empty and
|
|
90
|
+
corrupt or symlinked ledgers refuse. Explicit `critical-only` uses fresh OS/kernel
|
|
91
|
+
pressure probes without aggregate sampling or ledger access: warning/unknown
|
|
92
|
+
measurements remain diagnostic, while confirmed critical pressure refuses.
|
|
93
|
+
Containment is checked before and after sampling in either policy. This read-only
|
|
94
|
+
path never creates a safety latch; the daemon's safety machinery remains authoritative.
|
|
95
|
+
Other platforms retain the existing no-footprint-probe policy; this is not a
|
|
96
|
+
cross-platform native qualification claim. Markdown coverage
|
|
97
|
+
and returned pointers require regular files, including after symlink resolution;
|
|
98
|
+
a directory whose name ends in `.md` can be a prefix but never a source pointer.
|
|
99
|
+
|
|
100
|
+
Cancellation closes only the request/owned bridge. It rejects the caller and
|
|
101
|
+
discards late results; an already executing shared worker remains assigned until
|
|
102
|
+
its terminal response or exit. Caller deadlines/heartbeats cannot trigger worker
|
|
103
|
+
replacement or native session initialization. Idle RSS retirement and consecutive
|
|
104
|
+
oversized completions remain enforced, including after restricted work, without
|
|
105
|
+
filling the worker floor on that path. Further restricted calls then report
|
|
106
|
+
unavailable; ordinary work may expand under ordinary admission. Busy inference is
|
|
107
|
+
never interrupted by RSS retirement. The daemon's independent safety/shutdown
|
|
108
|
+
machinery remains authoritative.
|
|
109
|
+
|
|
110
|
+
Native retrieval receives the remaining overall deadline and holds its shared
|
|
111
|
+
operation until the actual native promise settles. A JavaScript timer cannot
|
|
112
|
+
release resources that native code still uses. Caller cancellation/bridge timeout
|
|
113
|
+
remains bounded and late results are discarded. If the SDK fails to settle even
|
|
114
|
+
after its native deadline, exclusive resource teardown stays blocked rather than
|
|
115
|
+
closing underneath the read; no worker/store replacement is induced to hide the
|
|
116
|
+
failure. Cancellation cannot interrupt an already running ONNX native call.
|
|
117
|
+
Global logging settings are unchanged, and this path never logs query text.
|
|
118
|
+
|
|
119
|
+
## Isolated qualification
|
|
120
|
+
|
|
121
|
+
Use `node scripts/build-document-search-candidate.cjs /absolute/new/output` to
|
|
122
|
+
compile/package without overwriting `dist`, installing, publishing, or invoking
|
|
123
|
+
lifecycle scripts. The script copies the pinned native JS runtime with provenance.
|
|
124
|
+
`node scripts/audit-document-search.cjs /absolute/extracted/package` exercises a
|
|
125
|
+
synthetic daemon and checks packaged files/import boundaries. The normal consumer
|
|
126
|
+
audit invokes that check against its freshly installed tarball too.
|
|
127
|
+
|
|
128
|
+
The optional Runlist fixture is
|
|
129
|
+
`RUNLIST_GMAX_QUALIFICATION_ENTRY=/absolute/extracted/package/dist/bin.js node --test desktop/test/semantic-gmax-contract.test.mjs`.
|
|
130
|
+
Fixture homes/IPC are private temporary directories; no real daemon, index,
|
|
131
|
+
watcher, model or grammar download is used. Reusing checkout dependencies for a
|
|
132
|
+
local package probe must be reported separately from fresh dependency resolution.
|
|
133
|
+
Installation, a shared-daemon restart, watcher recovery and live retrieval require
|
|
134
|
+
separate explicit actions. Source/package fixture success proves none of those.
|
package/README.md
CHANGED
|
@@ -233,6 +233,33 @@ Architecture audits omit unresolved same-name definitions from rankings, cycles
|
|
|
233
233
|
|
|
234
234
|
CLI `gmax skeleton --agent` and MCP `code_skeleton` text show at most 120 lines / 6,000 characters per file and report omitted lines and characters. CLI human output and JSON structure remain complete.
|
|
235
235
|
|
|
236
|
+
## Existing-index document search
|
|
237
|
+
|
|
238
|
+
`gmax mcp --existing-index-only` exposes a separate read-only stdio MCP contract
|
|
239
|
+
for Markdown document coverage and semantic pointers. It uses an existing daemon,
|
|
240
|
+
index and already-warm compatible query worker. It cannot start a daemon, watch a
|
|
241
|
+
project, initialize a model, fall back to another backend or mutate the index.
|
|
242
|
+
Cold, busy or unavailable resources return an explicit unavailable state.
|
|
243
|
+
|
|
244
|
+
Document query admission follows the configured host guard policy. On macOS,
|
|
245
|
+
default `strict` requires fresh aggregate resource admission; explicit
|
|
246
|
+
`critical-only` checks OS/kernel critical pressure without an aggregate scan or
|
|
247
|
+
ledger access. Warning or unavailable measurements stay diagnostic in that mode.
|
|
248
|
+
Both policies preserve containment checks. Up to four document queries can wait
|
|
249
|
+
FIFO for an already-warm compatible worker for up to two seconds, taking a turn
|
|
250
|
+
between indexing files. After four consecutive document queries, waiting ordinary
|
|
251
|
+
work gets a turn. Saturation or wait expiry returns `busy`; cold workers still
|
|
252
|
+
refuse. This wait cannot start, replace or scale workers, and it never interrupts
|
|
253
|
+
an in-flight file. Admission/readiness are checked again before dispatch, and the
|
|
254
|
+
overall ten-second deadline includes waiting. Readiness means a request can be
|
|
255
|
+
admitted, including this bounded wait. Long indexing files can still return `busy`.
|
|
256
|
+
|
|
257
|
+
Results contain canonical paths and one-based source ranges, without cached source
|
|
258
|
+
bodies. The first slice uses dense retrieval with explicit path scope and bounded
|
|
259
|
+
deadlines. Consumers must reread and authorize current source before using a
|
|
260
|
+
pointer. See [the document-search contract](DOCUMENT-SEARCH.md) for tool schemas,
|
|
261
|
+
limits, hash provenance and isolated qualification.
|
|
262
|
+
|
|
236
263
|
## Background Daemon
|
|
237
264
|
|
|
238
265
|
A single daemon watches your projects via native OS file events (FSEvents/inotify). Changes are detected in sub-second and incrementally reindexed. All writes to LanceDB are routed through the daemon via IPC, eliminating lock contention.
|
|
@@ -580,6 +607,75 @@ The candidate trace contains pointers and stage ranks for at most the first 200
|
|
|
580
607
|
|
|
581
608
|
Each traced candidate also has an `outcome`: returned, stage-one/pooled cut, deduplicated, per-file limit or display limit. Display limit means the production loop filled its result window before examining that candidate; it does not assert that the candidate would pass the per-file cap.
|
|
582
609
|
|
|
610
|
+
#### Existing-index document retrieval evaluation
|
|
611
|
+
|
|
612
|
+
In a source checkout, [scripts/eval-documents.cjs](scripts/eval-documents.cjs) measures the released [document MCP contract](DOCUMENT-SEARCH.md) through an explicit installed entry point. It uses only Node built-ins and sequential `document_search_status` / `semantic_search` calls. It performs no model warmup, daemon startup, indexing, refusal retries or fallback to normal hybrid search.
|
|
613
|
+
|
|
614
|
+
```bash
|
|
615
|
+
node scripts/eval-documents.cjs \
|
|
616
|
+
--fixture docs/measurements/documents/fixture-v1.json \
|
|
617
|
+
--sha256 <reviewed-fixture-digest> \
|
|
618
|
+
--root /absolute/already-indexed/project \
|
|
619
|
+
--entry /absolute/installed/grepmax/dist/bin.js \
|
|
620
|
+
--output docs/measurements/documents/baseline-v1.json
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Freeze the JSON fixture before retrieving results. It contains `schemaVersion: 1`, `purpose`, explicit project-relative `prefixes`, and 1–100 `cases`. Each case has a unique `id`, a `query` (maximum 500 characters), and one or more `expected` targets: `{file, startLine, endLine, sourceSha256}`. Files are contained relative Markdown paths; ranges are one-based and answer-bearing; hashes cover exact source bytes. Keep private fixtures and evidence in ignored `docs/measurements/`. Optional `--repeats 1-3` repeats the same frozen cases without retries or warmup.
|
|
624
|
+
|
|
625
|
+
The report separates the rank of the expected document among distinct returned documents from the rank of an overlapping expected section among chunk pointers. Document Recall@10 credits the fraction of declared target documents retrieved; MRR@10 credits only ranks 1–10. Section overlap indicates source-context discovery, not complete answer coverage. Only the first 50 returned candidates are available; document deduplication cannot recover targets outside that window. Reviewed targets are not exhaustive judgments of all useful documents, so these metrics describe the frozen targets, not general recall.
|
|
626
|
+
|
|
627
|
+
Source and indexed hashes must match the fixture before querying; generation and project/store identity must agree across coverage and retrieval. Returned source ranges require reread/hash verification; new artifacts retain both the indexed pointer digest and current-source digest so later reviews can check provenance. Refusals, changed targets and unverified expected pointers are excluded from relevance metrics and counted separately; `usableFraction` includes every planned sample. Background indexing and partial/degraded index context are recorded without excluding targets whose coverage and hashes remain current. No queries, source bodies or raw diagnostics are copied into result artifacts. Reads are bounded to 2 MiB per source; MCP frames to 1 MiB. The report and sibling `.samples.jsonl` checkpoint refuse overwrite and use owner-only permissions. Exit 0 means an evaluation with usable samples completed, not that a quality threshold passed; exit 2 means no usable samples; exit 1 means preflight or transport/contract failure. No consumer rollout is implied.
|
|
628
|
+
|
|
629
|
+
To investigate scope competition, [scripts/eval-document-scopes.cjs](scripts/eval-document-scopes.cjs) compares the original fixture prefixes against declared narrower directories without changing queries or targets:
|
|
630
|
+
|
|
631
|
+
```bash
|
|
632
|
+
node scripts/eval-document-scopes.cjs \
|
|
633
|
+
--fixture docs/measurements/documents/fixture-v1.json --sha256 <digest> \
|
|
634
|
+
--root /absolute/already-indexed/project \
|
|
635
|
+
--entry /absolute/installed/grepmax/dist/bin.js \
|
|
636
|
+
--output docs/measurements/documents/paired-scope-v1.json \
|
|
637
|
+
--narrow-prefix docs/services --narrow-prefix docs/modules
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Declare and freeze the scope hypothesis before querying. Narrower prefixes must stay within the original scope and retain every target. Arm order alternates per case, with a one-second gap between pairs and no retries. Reports show each arm's standalone observations and separately compare only pairs where both arms are usable under the same resource generation, project and store. The live competing corpus can still change; this is a diagnostic comparison rather than an isolated causal test. Scopes chosen from known target families introduce a useful prior, so gains do not establish unscoped or held-out retrieval quality. Owner-only checkpoints, overwrite refusal and source/transport bounds are shared with the base evaluator. Exit 2 means no comparable pairs.
|
|
641
|
+
|
|
642
|
+
### Offline lexical candidate experiment
|
|
643
|
+
|
|
644
|
+
[scripts/eval-document-lexical.cjs](scripts/eval-document-lexical.cjs) tests lexical candidates and fixed document reciprocal-rank fusion against a prior paired scope report. It uses Node built-ins and filesystem Markdown only; it never contacts a daemon, opens a native store, loads a model or changes production retrieval.
|
|
645
|
+
|
|
646
|
+
Freeze an owner-only study plan before scoring, including the original fixture checksum, archived report checksum, canonical root, narrower relative prefixes, fixed algorithm parameters and resource limits. The selected scope must retain every target and match the archived narrow arm. Pass that plan's checksum explicitly:
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
NODE_OPTIONS=--max-old-space-size=384 node scripts/eval-document-lexical.cjs \
|
|
650
|
+
--fixture docs/measurements/documents/fixture-v1.json \
|
|
651
|
+
--plan docs/measurements/documents/lexical-plan.json --plan-sha256 <digest> \
|
|
652
|
+
--archive docs/measurements/documents/paired-scope-v1.json \
|
|
653
|
+
--snapshot docs/measurements/documents/new-private-snapshot \
|
|
654
|
+
--output docs/measurements/documents/new-lexical-report.json
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
The prototype uses fixed 48-line windows with eight-line overlap, BM25 `k1=1.2` / `b=0.75`, 50 positive-score lexical candidates and equal-weight document RRF with constant 60. Targets are used for grading only. Stable path/line ties, no per-case overrides and no zero-score padding keep the experiment reproducible. Bounds are 512 files, 16 MiB total source, 2 MiB per file, 10,000 windows, one million token occurrences and 20,000 directory entries. Missing, aliased, changed or oversized source fails snapshot creation rather than silently dropping competing documents. A private snapshot preserves exact source bytes and a digest manifest; the report stores pointers, digests, IDs and scores without questions or source bodies. Existing output/snapshot paths are refused.
|
|
658
|
+
|
|
659
|
+
Lexical metrics cover cases whose frozen targets match the snapshot. Archive replay additionally requires original ready state/resource identity and every competitor pointer's indexed/current digest and range to match the snapshot; an invalid competitor excludes the entire case. Refused archive cases stay separate. Offline lexical availability does not exercise deployed busy/pressure admission. Archived dense candidates are not re-encoded over this snapshot, and narrower families chosen from known target locations introduce a prior. Report document gains/losses on the verified shared replay cohort; section overlap is diagnostic because window sizes and hybrid representatives differ from production chunks. A passing exploratory gate only supports proposing further matched qualification. No production integration or rollout follows automatically. Exit 0 means scoring completed with usable lexical cases, even when the gate fails; exit 2 means no usable lexical cases, and exit 1 means preflight or evaluation failure.
|
|
660
|
+
|
|
661
|
+
### Protected document fusion qualification
|
|
662
|
+
|
|
663
|
+
[scripts/eval-document-fusion.cjs](scripts/eval-document-fusion.cjs) qualifies a fixed candidate ordering in two modes: replay cached verified lexical/dense candidates, or collect one broader pass through the released existing-only MCP. It interleaves each arm's first three distinct documents (`lex1,dense1,lex2,dense2,lex3,dense3`), deduplicates while preserving the selected arm's pointer, then fills to 50 with the fixed RRF tail. This protects high-ranked lexical-only evidence; spending up to six slots can displace useful lower-ranked dense candidates. Expected targets never affect ranking.
|
|
664
|
+
|
|
665
|
+
```bash
|
|
666
|
+
NODE_OPTIONS=--max-old-space-size=384 node scripts/eval-document-fusion.cjs \
|
|
667
|
+
--mode live --design <frozen-design.json> --design-sha256 <digest> \
|
|
668
|
+
--fixture <frozen-fixture.json> --fixture-sha256 <digest> \
|
|
669
|
+
--entry /absolute/path/to/installed/grepmax/dist/bin.js --entry-sha256 <digest> \
|
|
670
|
+
--snapshot <new-private-snapshot-directory> --output <new-private-report.json>
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
For `--mode replay`, replace the entry arguments with `--input <prior-lexical-report.json> --input-sha256 <digest>` and point `--snapshot` at its immutable snapshot. Replay uses saved lexical candidates without rescoring queries. Live mode snapshots the complete predefined scope and builds the bounded filesystem index before MCP initialization; it permits one case-order pass with one-second gaps, without retries, warmup, runtime startup or native fallback. The existing-only entry must expose the expected restricted capabilities. Coverage, generation, project/store identity and every indexed/current competitor digest and range must verify. Refusals remain separate from relevance. A final full source-byte/file-set fence invalidates comparisons if the corpus changed; checkpoints are provisional and the final JSON is authoritative.
|
|
674
|
+
|
|
675
|
+
The lexical CLI retains its one-million-token default. A frozen fusion design may explicitly allow 1,100,000 token occurrences; all other source/window limits and the 384 MiB Node heap setting remain unchanged. The heap setting is not an operating-system physical memory cap. Outputs and snapshots refuse overwrite and use private permissions; reports exclude question text and source bodies. The broader study requires at least 16 of 20 comparable cases, no dense top-ten target losses, and protected document recall at least as high as either arm on the same cohort. Other predefined positive cohort bounds are supported. Exit 0 means collection completed with some comparable cases, even if the gate fails; exit 2 means none, and exit 1 means interrupted/preflight failure.
|
|
676
|
+
|
|
677
|
+
The October 8 experiment recovered both original residuals: Redis rank 1, People rank 3, 15/15 replay document hits with no dense top-ten target loss. Its broader 393-file, 20-case pass produced only two comparable cases (15 busy and three pressure refusals; one busy case also lacked indexed coverage), below the frozen 16-case minimum. Broader qualification is **insufficient**. Questions were source-curated after the rule freeze and held out from parameter selection, not independently adjudicated real-user questions. Representative/window differences prevent like-for-like section accuracy claims. This evaluation does not change production retrieval or authorize integration, release or rollout.
|
|
678
|
+
|
|
583
679
|
## Attribution
|
|
584
680
|
|
|
585
681
|
grepmax is built upon the foundation of [mgrep](https://github.com/mixedbread-ai/mgrep) by MixedBread. See the [NOTICE](NOTICE) file for details.
|
package/dist/bin.js
CHANGED
|
@@ -152,6 +152,15 @@ function seedConfig(home) {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
if (require.main === module) {
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
if (process.argv.slice(2).join(" ") === "mcp --existing-index-only") {
|
|
156
|
+
require("./lib/mcp/document-search").runDocumentSearchBridge();
|
|
157
|
+
}
|
|
158
|
+
else if (process.argv.includes("--existing-index-only")) {
|
|
159
|
+
process.stderr.write("gmax: use exactly mcp --existing-index-only\n");
|
|
160
|
+
process.exitCode = 2;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
selectHome();
|
|
164
|
+
require("./index");
|
|
165
|
+
}
|
|
157
166
|
}
|
|
@@ -84,6 +84,7 @@ const root_availability_1 = require("../utils/root-availability");
|
|
|
84
84
|
const safety_latch_1 = require("../utils/safety-latch");
|
|
85
85
|
const watcher_store_1 = require("../utils/watcher-store");
|
|
86
86
|
const pool_1 = require("../workers/pool");
|
|
87
|
+
const document_search_handler_1 = require("./document-search-handler");
|
|
87
88
|
const ipc_handler_1 = require("./ipc-handler");
|
|
88
89
|
const mlx_server_manager_1 = require("./mlx-server-manager");
|
|
89
90
|
const paused_reads_1 = require("./paused-reads");
|
|
@@ -1021,6 +1022,55 @@ class Daemon {
|
|
|
1021
1022
|
? { queue: progress.queue }
|
|
1022
1023
|
: {})), { failedFiles: Math.max((_d = progress === null || progress === void 0 ? void 0 : progress.failedFiles) !== null && _d !== void 0 ? _d : 0, (_e = health.failedFiles) !== null && _e !== void 0 ? _e : 0) });
|
|
1023
1024
|
}
|
|
1025
|
+
/** Separate shared-operation path: never paused keyword fallback or runtime setup. */
|
|
1026
|
+
documentSearch(cmd, signal) {
|
|
1027
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1028
|
+
const state = () => this.pausedReason !== null || (0, autostart_1.daemonStartDeniedReason)()
|
|
1029
|
+
? "host_pressure"
|
|
1030
|
+
: this.shuttingDown || this.recycling
|
|
1031
|
+
? "busy"
|
|
1032
|
+
: !this.vectorDb || !this.metaCache
|
|
1033
|
+
? "store_unavailable"
|
|
1034
|
+
: this.activeConfigurationError()
|
|
1035
|
+
? "embedding_mismatch"
|
|
1036
|
+
: "ready";
|
|
1037
|
+
return this.operations.runShared("documents", signal, (operationSignal) => (0, document_search_handler_1.handleDocumentSearch)({
|
|
1038
|
+
home: config_1.PATHS.globalRoot,
|
|
1039
|
+
state,
|
|
1040
|
+
generation: () => this.resourceGenerationId(),
|
|
1041
|
+
embeddingState: (root) => {
|
|
1042
|
+
const project = (0, project_registry_1.getProject)(root);
|
|
1043
|
+
if ((project === null || project === void 0 ? void 0 : project.status) !== "indexed")
|
|
1044
|
+
return "no_index";
|
|
1045
|
+
if (!this.activeGeneration)
|
|
1046
|
+
return "embedding_unavailable";
|
|
1047
|
+
if (this.workerPool &&
|
|
1048
|
+
this.workerPool.generation.fingerprint !==
|
|
1049
|
+
this.activeGeneration.fingerprint)
|
|
1050
|
+
return "embedding_mismatch";
|
|
1051
|
+
try {
|
|
1052
|
+
return (0, embedding_generation_1.compareEmbeddingGeneration)(project, this.activeGeneration)
|
|
1053
|
+
.state === "stale"
|
|
1054
|
+
? "embedding_mismatch"
|
|
1055
|
+
: "ready";
|
|
1056
|
+
}
|
|
1057
|
+
catch (_a) {
|
|
1058
|
+
return "embedding_mismatch";
|
|
1059
|
+
}
|
|
1060
|
+
},
|
|
1061
|
+
meta: (file) => { var _a; return (_a = this.metaCache) === null || _a === void 0 ? void 0 : _a.get(file); },
|
|
1062
|
+
queryState: () => { var _a; var _b; return (_b = (_a = this.workerPool) === null || _a === void 0 ? void 0 : _a.existingQueryState()) !== null && _b !== void 0 ? _b : "embedding_unavailable"; },
|
|
1063
|
+
encode: (query, requestSignal) => this.workerPool
|
|
1064
|
+
? this.workerPool.encodeQueryExisting(query, requestSignal)
|
|
1065
|
+
: Promise.reject(new Error("embedding_unavailable")),
|
|
1066
|
+
table: () => this.vectorDb
|
|
1067
|
+
? this.vectorDb.existingTableForRead()
|
|
1068
|
+
: Promise.reject(new Error("store_unavailable")),
|
|
1069
|
+
indexState: (root) => this.indexState(root),
|
|
1070
|
+
lastIndexed: (root) => { var _a; return (_a = (0, project_registry_1.getProject)(root)) === null || _a === void 0 ? void 0 : _a.lastIndexed; },
|
|
1071
|
+
}, cmd, operationSignal));
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1024
1074
|
search(payload, signal) {
|
|
1025
1075
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1026
1076
|
// Search handling lives in search-handler.ts (Phase 12 split). The daemon
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.documentFilter = documentFilter;
|
|
46
|
+
exports.handleDocumentSearch = handleDocumentSearch;
|
|
47
|
+
const path = __importStar(require("node:path"));
|
|
48
|
+
const document_contract_1 = require("../mcp/document-contract");
|
|
49
|
+
function sqlString(input) {
|
|
50
|
+
return `'${input.replace(/'/g, "''")}'`;
|
|
51
|
+
}
|
|
52
|
+
function documentFilter(prefixes) {
|
|
53
|
+
return `(${prefixes
|
|
54
|
+
.map((prefix) => {
|
|
55
|
+
const dir = prefix.endsWith(path.sep) ? prefix : prefix + path.sep;
|
|
56
|
+
return `(path = ${sqlString(prefix)} OR (path >= ${sqlString(dir)} AND path < ${sqlString(`${dir}\uffff`)}))`;
|
|
57
|
+
})
|
|
58
|
+
.join(" OR ")}) AND lower(path) LIKE '%.md'`;
|
|
59
|
+
}
|
|
60
|
+
function handleDocumentSearch(deps, cmd, signal) {
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
var _a, _b;
|
|
63
|
+
var _c, _d;
|
|
64
|
+
try {
|
|
65
|
+
const deadline = performance.now() + document_contract_1.DOCUMENT_LIMITS.deadlineMs;
|
|
66
|
+
signal = AbortSignal.any([
|
|
67
|
+
signal,
|
|
68
|
+
AbortSignal.timeout(document_contract_1.DOCUMENT_LIMITS.deadlineMs),
|
|
69
|
+
]);
|
|
70
|
+
signal.throwIfAborted();
|
|
71
|
+
if (cmd.contractVersion !== document_contract_1.DOCUMENT_CONTRACT_VERSION)
|
|
72
|
+
return (0, document_contract_1.documentFailure)("unsupported_tool");
|
|
73
|
+
if (deps.state() !== "ready")
|
|
74
|
+
return (0, document_contract_1.documentFailure)(deps.state());
|
|
75
|
+
if (typeof cmd.checkout !== "string")
|
|
76
|
+
return (0, document_contract_1.documentFailure)("no_index");
|
|
77
|
+
const context = (0, document_contract_1.selectDocumentContext)(deps.home, cmd.checkout);
|
|
78
|
+
if (cmd.projectRoot !== context.wireRoot || cmd.store !== context.store)
|
|
79
|
+
return (0, document_contract_1.documentFailure)("store_unavailable");
|
|
80
|
+
const embeddingState = deps.embeddingState(context.wireRoot);
|
|
81
|
+
if (embeddingState !== "ready")
|
|
82
|
+
return (0, document_contract_1.documentFailure)(embeddingState);
|
|
83
|
+
const generation = deps.generation();
|
|
84
|
+
if (!(0, document_contract_1.documentGeneration)(generation))
|
|
85
|
+
return (0, document_contract_1.documentFailure)("index_unavailable");
|
|
86
|
+
if (!(0, document_contract_1.documentGeneration)(cmd.generation) || cmd.generation !== generation)
|
|
87
|
+
return (0, document_contract_1.documentFailure)("embedding_mismatch");
|
|
88
|
+
const recheck = () => {
|
|
89
|
+
signal.throwIfAborted();
|
|
90
|
+
if (performance.now() >= deadline)
|
|
91
|
+
throw new Error("timeout");
|
|
92
|
+
if (deps.state() !== "ready")
|
|
93
|
+
throw new Error(deps.state());
|
|
94
|
+
if (deps.generation() !== generation ||
|
|
95
|
+
deps.embeddingState(context.wireRoot) !== "ready")
|
|
96
|
+
throw new Error("embedding_mismatch");
|
|
97
|
+
};
|
|
98
|
+
const base = {
|
|
99
|
+
ok: true,
|
|
100
|
+
root: context.root,
|
|
101
|
+
store: context.store,
|
|
102
|
+
generation,
|
|
103
|
+
};
|
|
104
|
+
if (cmd.cmd === "documents.status") {
|
|
105
|
+
const { paths, requested } = (0, document_contract_1.documentCoveragePaths)(cmd.paths, context);
|
|
106
|
+
const covered = paths.flatMap((file) => {
|
|
107
|
+
const entry = deps.meta(file);
|
|
108
|
+
return (entry === null || entry === void 0 ? void 0 : entry.hasVectors) === true
|
|
109
|
+
? [
|
|
110
|
+
Object.assign(Object.assign({ path: file }, (entry.hashVersion === 1 && /^[a-f0-9]{64}$/.test(entry.hash)
|
|
111
|
+
? { hash: entry.hash, hashAlgorithm: "sha256-bytes" }
|
|
112
|
+
: {})), ((0, document_contract_1.documentMtime)(entry.mtimeMs) !== null
|
|
113
|
+
? { indexedMtimeMs: entry.mtimeMs }
|
|
114
|
+
: {})),
|
|
115
|
+
]
|
|
116
|
+
: [];
|
|
117
|
+
});
|
|
118
|
+
recheck();
|
|
119
|
+
const state = (0, document_contract_1.documentQueryState)(deps.queryState());
|
|
120
|
+
return Object.assign(Object.assign({}, base), { embeddingReady: state === "ready", queryState: state, lastIndexed: (_c = (0, document_contract_1.documentIndexedAt)((_a = deps.lastIndexed) === null || _a === void 0 ? void 0 : _a.call(deps, context.wireRoot))) !== null && _c !== void 0 ? _c : context.lastIndexed, indexState: (0, document_contract_1.documentIndexState)(deps.indexState(context.wireRoot)), covered, coverage: {
|
|
121
|
+
requested,
|
|
122
|
+
indexed: covered.length,
|
|
123
|
+
partial: covered.length !== requested,
|
|
124
|
+
} });
|
|
125
|
+
}
|
|
126
|
+
if (cmd.cmd !== "documents.search")
|
|
127
|
+
return (0, document_contract_1.documentFailure)("unsupported_tool");
|
|
128
|
+
if (typeof cmd.query !== "string" ||
|
|
129
|
+
!cmd.query.trim() ||
|
|
130
|
+
cmd.query.length > document_contract_1.DOCUMENT_LIMITS.query)
|
|
131
|
+
return (0, document_contract_1.documentFailure)("search_unavailable");
|
|
132
|
+
const prefixes = (0, document_contract_1.documentPaths)(cmd.prefixes, context, "prefixes");
|
|
133
|
+
const state = deps.queryState();
|
|
134
|
+
if (state !== "ready")
|
|
135
|
+
return (0, document_contract_1.documentFailure)(state);
|
|
136
|
+
recheck();
|
|
137
|
+
const encoded = yield deps.encode(cmd.query, signal);
|
|
138
|
+
recheck();
|
|
139
|
+
if (!encoded.dense.length || !encoded.dense.every(Number.isFinite))
|
|
140
|
+
return (0, document_contract_1.documentFailure)("embedding_unavailable");
|
|
141
|
+
const table = yield deps.table();
|
|
142
|
+
recheck();
|
|
143
|
+
const remainingMs = Math.floor(deadline - performance.now());
|
|
144
|
+
if (remainingMs <= 0)
|
|
145
|
+
throw new Error("timeout");
|
|
146
|
+
// Await the actual native promise, not a JS timeout race. The caller's
|
|
147
|
+
// bridge/signal has its own deadline; shared ownership must remain until
|
|
148
|
+
// native completion, even if the SDK fails to honor its native timeout.
|
|
149
|
+
const rows = (yield table
|
|
150
|
+
.vectorSearch(encoded.dense)
|
|
151
|
+
.distanceType("cosine")
|
|
152
|
+
.where(documentFilter(prefixes))
|
|
153
|
+
.select(["path", "hash", "start_line", "end_line", "_distance"])
|
|
154
|
+
.limit(document_contract_1.DOCUMENT_LIMITS.candidates)
|
|
155
|
+
.toArray({ timeoutMs: remainingMs }));
|
|
156
|
+
recheck();
|
|
157
|
+
const matches = rows.slice(0, document_contract_1.DOCUMENT_LIMITS.candidates).flatMap((row) => {
|
|
158
|
+
if (typeof row.path !== "string" ||
|
|
159
|
+
!prefixes.some((prefix) => row.path === prefix || row.path.startsWith(prefix + path.sep)))
|
|
160
|
+
return [];
|
|
161
|
+
const file = (0, document_contract_1.canonicalPointer)(row.path, context);
|
|
162
|
+
if (!file ||
|
|
163
|
+
!Number.isInteger(row.start_line) ||
|
|
164
|
+
row.start_line < 0 ||
|
|
165
|
+
!Number.isInteger(row.end_line) ||
|
|
166
|
+
row.end_line < row.start_line ||
|
|
167
|
+
!Number.isFinite(row._distance))
|
|
168
|
+
return [];
|
|
169
|
+
const entry = deps.meta(row.path);
|
|
170
|
+
// Only a matching current metadata revision proves this row's digest is bytes.
|
|
171
|
+
const hash = (entry === null || entry === void 0 ? void 0 : entry.hashVersion) === 1 &&
|
|
172
|
+
entry.hasVectors === true &&
|
|
173
|
+
row.hash === entry.hash &&
|
|
174
|
+
/^[a-f0-9]{64}$/.test(row.hash)
|
|
175
|
+
? { hash: row.hash, hashAlgorithm: "sha256-bytes" }
|
|
176
|
+
: {};
|
|
177
|
+
return [
|
|
178
|
+
Object.assign({ path: file, startLine: row.start_line + 1, endLine: row.end_line + 1, score: 1 / (1 + Math.max(0, row._distance)) }, hash),
|
|
179
|
+
];
|
|
180
|
+
});
|
|
181
|
+
return Object.assign(Object.assign({}, base), { indexState: (0, document_contract_1.documentIndexState)(deps.indexState(context.wireRoot)), matches, retrieval: "dense", candidateLimit: document_contract_1.DOCUMENT_LIMITS.candidates });
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
const e = error;
|
|
185
|
+
return (0, document_contract_1.documentFailure)(signal.aborted
|
|
186
|
+
? ((_b = signal.reason) === null || _b === void 0 ? void 0 : _b.name) === "TimeoutError"
|
|
187
|
+
? "timeout"
|
|
188
|
+
: "cancelled"
|
|
189
|
+
: e.name === "QueryTimeoutError"
|
|
190
|
+
? "timeout"
|
|
191
|
+
: ((_d = e.code) !== null && _d !== void 0 ? _d : e.message));
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -133,13 +133,17 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
133
133
|
cmd.cmd !== "shutdown" &&
|
|
134
134
|
(daemon.operationStatus() === "closing" ||
|
|
135
135
|
daemon.operationStatus() === "closed")) {
|
|
136
|
-
return
|
|
136
|
+
return cmd.cmd === "documents.status" || cmd.cmd === "documents.search"
|
|
137
|
+
? { ok: false, state: "busy" }
|
|
138
|
+
: errorResponse(new operation_coordinator_1.OperationClosedError());
|
|
137
139
|
}
|
|
138
140
|
// The socket listens before LanceDB/MetaCache are open so liveness probes
|
|
139
141
|
// succeed during slow startup. Gate resource-dependent commands until those
|
|
140
142
|
// stores exist; ping/shutdown must always work (probes + restart).
|
|
141
143
|
if (cmd.cmd !== "ping" && cmd.cmd !== "shutdown" && !daemon.isReady()) {
|
|
142
|
-
return
|
|
144
|
+
return cmd.cmd === "documents.status" || cmd.cmd === "documents.search"
|
|
145
|
+
? { ok: false, state: "index_unavailable" }
|
|
146
|
+
: { ok: false, error: "daemon initializing" };
|
|
143
147
|
}
|
|
144
148
|
switch (cmd.cmd) {
|
|
145
149
|
case "ping":
|
|
@@ -157,6 +161,7 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
157
161
|
exclusiveGenerationRebuild: exports.EXCLUSIVE_GENERATION_REBUILD_PROTOCOL,
|
|
158
162
|
readVerbs: read_verbs_1.READ_VERBS_PROTOCOL,
|
|
159
163
|
searchDiagnostics: 1,
|
|
164
|
+
existingIndexOnlySearch: 1,
|
|
160
165
|
perFileSearch: 1,
|
|
161
166
|
// `watch`/`unwatch` accept holder/pid/ttlMs. A daemon without this
|
|
162
167
|
// treats `unwatch` as unwatch-for-everyone, so clients releasing a
|
|
@@ -225,6 +230,28 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
225
230
|
setImmediate(() => daemon.shutdown());
|
|
226
231
|
return { ok: true };
|
|
227
232
|
}
|
|
233
|
+
case "documents.status":
|
|
234
|
+
case "documents.search": {
|
|
235
|
+
const ac = new AbortController();
|
|
236
|
+
const close = () => ac.abort();
|
|
237
|
+
conn.once("close", close);
|
|
238
|
+
try {
|
|
239
|
+
return yield daemon.documentSearch(cmd, ac.signal);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
state: ac.signal.aborted
|
|
245
|
+
? "cancelled"
|
|
246
|
+
: error instanceof operation_coordinator_1.OperationBusyError
|
|
247
|
+
? "busy"
|
|
248
|
+
: "index_unavailable",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
conn.off("close", close);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
228
255
|
case "search":
|
|
229
256
|
case "search-v2": {
|
|
230
257
|
const isV2 = cmd.cmd === "search-v2";
|