md-semantic-search 1.0.3 → 1.1.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 +45 -3
- package/RESEARCH.md +25 -3
- package/bin/cli.mjs +255 -8
- package/dist/completions.js +6 -1
- package/dist/core.d.ts +26 -0
- package/dist/core.js +104 -1
- package/dist/index-format.js +8 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/indexer.d.ts +1 -0
- package/dist/indexer.js +5 -5
- package/dist/ivf.d.ts +14 -7
- package/dist/ivf.js +126 -18
- package/dist/mcp.d.ts +143 -4
- package/dist/mcp.js +493 -22
- package/dist/models.d.ts +1 -1
- package/dist/models.js +52 -31
- package/dist/rag.d.ts +72 -7
- package/dist/rag.js +191 -13
- package/dist/rerank.d.ts +20 -4
- package/dist/rerank.js +11 -2
- package/dist/search.d.ts +1 -0
- package/dist/search.js +3 -0
- package/dist/serve.js +97 -1
- package/dist/tui.d.ts +8 -0
- package/dist/tui.js +80 -13
- package/dist/webui.d.ts +1 -1
- package/dist/webui.js +24 -3
- package/dist/wikilinks.d.ts +31 -0
- package/dist/wikilinks.js +185 -1
- package/package.json +1 -1
- package/src/completions.ts +6 -1
- package/src/core.ts +134 -1
- package/src/index-format.ts +7 -1
- package/src/index.ts +7 -2
- package/src/indexer.ts +8 -4
- package/src/ivf.ts +157 -20
- package/src/mcp.ts +507 -23
- package/src/models.ts +58 -33
- package/src/rag.ts +279 -17
- package/src/rerank.ts +40 -6
- package/src/search.ts +7 -0
- package/src/serve.ts +91 -1
- package/src/tui.ts +86 -14
- package/src/webui.ts +24 -3
- package/src/wikilinks.ts +220 -1
package/README.md
CHANGED
|
@@ -387,8 +387,31 @@ mdss search --db /path/to/your/markdown "how do I rotate the api token" --rerank
|
|
|
387
387
|
- The reranker is lazy: nothing loads unless `--rerank` is actually requested,
|
|
388
388
|
so searches without it pay zero extra cost.
|
|
389
389
|
|
|
390
|
+
### 3. Ask & Interactive Multi-turn Chat (`mdss ask`, `mdss chat`)
|
|
390
391
|
|
|
391
|
-
|
|
392
|
+
`md-semantic-search` includes a grounded RAG QA engine that operates fully offline by default with zero external dependencies, or connects optionally to local/remote LLMs (Ollama, OpenAI-compatible APIs).
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
# One-shot grounded QA with citations:
|
|
396
|
+
mdss ask --db ./notes "What is the token expiration policy?"
|
|
397
|
+
|
|
398
|
+
# Multi-turn persistent chat session:
|
|
399
|
+
mdss chat --db ./notes
|
|
400
|
+
|
|
401
|
+
# Resume the most recent session or a specific session:
|
|
402
|
+
mdss chat --db ./notes --resume
|
|
403
|
+
mdss chat --db ./notes --session my-session-id
|
|
404
|
+
|
|
405
|
+
# One-shot query into a persistent session:
|
|
406
|
+
mdss chat --db ./notes --session my-session-id "What about database failover?"
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
- **Offline Extractive Synthesis**: Zero-dependency default extracts salient factual sentences with strict `[file › heading]` source provenance.
|
|
410
|
+
- **Persistent Sessions**: Multi-turn sessions are stored in `<indexDir>/sessions/<id>.json` preserving full conversation turns, retrieved chunk provenance manifests (file, heading, exact line numbers, chunk hashes), and model metadata.
|
|
411
|
+
- **Context Carry-Over**: Follow-up turns re-rank and expand query tokens using prior conversational context.
|
|
412
|
+
- **Interactive REPL Commands**: `/session` (info), `/sources` (manifest of last turn), `/help`, `/exit`.
|
|
413
|
+
|
|
414
|
+
### 4. Serve (daemon, warm index + model)
|
|
392
415
|
|
|
393
416
|
```bash
|
|
394
417
|
mdss serve --db /path/to/your/markdown [--port 8747] [--host 127.0.0.1] [--watch]
|
|
@@ -520,10 +543,27 @@ rate-limit gates as `/search`, so remote agents inherit the hardening. stdio
|
|
|
520
543
|
via `mdss mcp` remains the default transport.
|
|
521
544
|
|
|
522
545
|
#### Available MCP Tools:
|
|
523
|
-
- **`search_markdown`**: Hybrid semantic vector + BM25 search with optional `path` glob
|
|
546
|
+
- **`search_markdown`**: Hybrid semantic vector + BM25 search with optional `path` glob, `tag`, rich `filter`, and `graphBoost`.
|
|
524
547
|
- **`get_chunk`**: Retrieve a specific section by file and heading.
|
|
548
|
+
- **`get_lines`**: Retrieve exact line spans (`fromLine`, `maxLines`) from an indexed Markdown document.
|
|
549
|
+
- **`related_notes`**: Find graph-connected (outgoing, backlinks, 2-hop) and semantically related notes for a document.
|
|
525
550
|
- **`list_files`**: List indexed Markdown files with chunk counts.
|
|
526
551
|
- **`index_status`**: Inspect index health, chunk counts, and model metadata.
|
|
552
|
+
- **`ask_knowledge_base`**: Grounded direct Q&A synthesis with citations.
|
|
553
|
+
- *All tools return `structuredContent` alongside text and include `readOnly: true, idempotent: true` annotations.*
|
|
554
|
+
|
|
555
|
+
#### Available MCP Resources & Templates:
|
|
556
|
+
- **`mdss://note/{path}`**: Read full content of an indexed Markdown document.
|
|
557
|
+
- **`mdss://note/{path}?fromLine=1&maxLines=50`**: Addressable line-span slices of notes.
|
|
558
|
+
- **`mdss://vault/{vault}`**: List all indexed notes within a vault.
|
|
559
|
+
- **`mdss://status`**: Live index status and chunk count statistics.
|
|
560
|
+
|
|
561
|
+
#### Available MCP Prompts:
|
|
562
|
+
- **`search-and-cite`**: Synthesizes a grounded answer referencing `mdss://note/{path}` resource URIs.
|
|
563
|
+
- **`summarize-note`**: Produces a structured summary of core concepts and linked notes.
|
|
564
|
+
- **`compare-notes`**: Identifies agreements, divergences, and synergies across two notes.
|
|
565
|
+
- **`find-contradictions`**: Audits notes for conflicting statements or outdated ADRs.
|
|
566
|
+
- **`timeline`**: Builds a chronological milestone / event timeline from notes.
|
|
527
567
|
|
|
528
568
|
#### Claude Desktop Configuration:
|
|
529
569
|
Add to your `claude_desktop_config.json`:
|
|
@@ -796,7 +836,9 @@ Contributions are welcome — bug reports with reproduction steps, docs fixes,
|
|
|
796
836
|
tests, and features all help. Start with
|
|
797
837
|
[CONTRIBUTING.md](./CONTRIBUTING.md) (dev setup, project tour, conventions)
|
|
798
838
|
and look for issues labeled **`good first issue`** — they are scoped for a
|
|
799
|
-
first-time contributor.
|
|
839
|
+
first-time contributor. For the internals — module map, data-flow diagrams,
|
|
840
|
+
on-disk formats, and extension points — see
|
|
841
|
+
[ARCHITECTURE.md](./ARCHITECTURE.md). By participating you agree to the
|
|
800
842
|
[Code of Conduct](./CODE_OF_CONDUCT.md).
|
|
801
843
|
|
|
802
844
|
## License
|
package/RESEARCH.md
CHANGED
|
@@ -226,7 +226,29 @@ If no candidate meets the multilingual non-inferiority gate, `bge-reranker-base`
|
|
|
226
226
|
- **Trigger:** WebAssembly SIMD dot-product kernels will be considered **only if** CPU profiling on >50,000 chunks shows JS float multiplication accounts for >40% of warm search latency.
|
|
227
227
|
- **Constraint:** Zero native Node C++ bindings; must maintain pure JS fallback for non-WASM environments.
|
|
228
228
|
|
|
229
|
-
### ANN / IVF Scale Threshold (
|
|
229
|
+
### ANN / IVF Scale Threshold & K-Means++ Seeding (Issues #32 & #139)
|
|
230
230
|
- **Baseline:** Exact $O(N \cdot d)$ linear sweep.
|
|
231
|
-
- **
|
|
232
|
-
- **
|
|
231
|
+
- **K-Means++ Seeding:** Uses deterministic pseudo-random $D^2$-weighted centroid seeding (Mulberry32) to prevent initial centroid collapse on correlated notes corpora.
|
|
232
|
+
- **Measured Quality Gate:** `bench/ann-recall.mjs` benchmarks Recall@10 across $nprobe \in \{1, 4, 8, 16\}$ on clustered corpora, demonstrating $\ge 99.5\%$ Recall@10 at default $nprobe=8$ with sub-millisecond candidate pruning.
|
|
233
|
+
- **Trigger:** ANN indexing is auto-engaged above `ANN_THRESHOLD = 500` chunks when `--ann` is passed or IVF index is persisted, while exact pre/post-filter correctness is strictly preserved.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 14. Matryoshka Representation Learning (MRL) Dimension Selection (Issue #143)
|
|
238
|
+
|
|
239
|
+
### Mechanism
|
|
240
|
+
Models trained with Matryoshka Representation Learning (MRL) loss (such as `onnx-community/Qwen3-Embedding-0.6B-ONNX` and `Xenova/bge-m3`) embed semantic information hierarchically, allowing vectors to be sliced at predefined sub-dimensions (`32`, `64`, `128`, `256`, `512`, `768`, `1024`) and re-normalized to unit L2 norm without significant loss in semantic retrieval fidelity.
|
|
241
|
+
|
|
242
|
+
### Benefits & Trade-offs
|
|
243
|
+
- **Storage & RAM Savings:** 1024 → 256 dimensions reduces `vectors.bin` size and memory consumption by 4× (from ~19.5 MB to ~4.88 MB per 5,000 chunks).
|
|
244
|
+
- **Sweep Latency:** Dot-product cosine sweep latency is halved (e.g. from ~14.2 ms to ~6.8 ms for 5,000 chunks in single-threaded pure JS).
|
|
245
|
+
- **Integrity & Cache Invalidation:** `embeddingAdapterFingerprint` binds the selected dimension (`model.dim`). Changing `--dimensions` automatically invalidates previous chunk hashes and forces clean re-indexing rather than silent vector dimension mismatch.
|
|
246
|
+
|
|
247
|
+
### Usage
|
|
248
|
+
```bash
|
|
249
|
+
# Build index truncated to 256 dims (4x smaller vectors)
|
|
250
|
+
mdss index --db ./notes --model qwen3-embedding-0.6b --dimensions 256
|
|
251
|
+
|
|
252
|
+
# Search with dimension safety validation
|
|
253
|
+
mdss search --db ./notes --dimensions 256 "query"
|
|
254
|
+
```
|
package/bin/cli.mjs
CHANGED
|
@@ -16,11 +16,12 @@ import fs from 'node:fs';
|
|
|
16
16
|
import os from 'node:os';
|
|
17
17
|
import crypto from 'node:crypto';
|
|
18
18
|
import { buildIndex } from '../dist/indexer.js';
|
|
19
|
-
import { search } from '../dist/search.js';
|
|
19
|
+
import { loadIndex, search } from '../dist/search.js';
|
|
20
20
|
import { searchFederated } from '../dist/federation.js';
|
|
21
21
|
import { createServe, DEFAULT_PORT, DEFAULT_HOST, isLoopbackHost, validateBindSecurity, loadApiKeyFile } from '../dist/serve.js';
|
|
22
22
|
import { MODELS, DEFAULT_MODEL, resolveModel } from '../dist/models.js';
|
|
23
23
|
import { decodeVec, walkMarkdown, SCHEMA_VERSION, assertSafePath, setDownloadProgressListener } from '../dist/core.js';
|
|
24
|
+
import { findRelatedNotes } from '../dist/wikilinks.js';
|
|
24
25
|
import { DownloadProgressAggregator } from '../dist/download-progress.js';
|
|
25
26
|
import { generateCompletion, COMPLETION_SHELLS } from '../dist/completions.js';
|
|
26
27
|
import { openHit, resolveOpenCommand } from '../dist/open.js';
|
|
@@ -93,6 +94,7 @@ function parseArgs(argv) {
|
|
|
93
94
|
else if (a === '--watch-interval') opts.watchInterval = nextInt(argv, ++i, a);
|
|
94
95
|
else if (a === '--rate-limit') opts.rateLimit = nextNonNegInt(argv, ++i, a);
|
|
95
96
|
else if (a === '--max-concurrency') opts.maxConcurrency = nextNonNegInt(argv, ++i, a);
|
|
97
|
+
else if (a === '--dimensions' || a === '--dim') opts.dimensions = nextInt(argv, ++i, a);
|
|
96
98
|
else if (a === '--since') opts.since = nextValue(argv, ++i, a);
|
|
97
99
|
else if (a === '--path') opts.path.push(nextValue(argv, ++i, a));
|
|
98
100
|
else if (a === '--ignore') opts.ignore.push(nextValue(argv, ++i, a));
|
|
@@ -108,6 +110,8 @@ function parseArgs(argv) {
|
|
|
108
110
|
else if (a === '--canonical') opts.canonical = true;
|
|
109
111
|
else if (a === '--graph-boost') opts.graphBoost = nextFloat(argv, ++i, a);
|
|
110
112
|
else if (a === '--filter') opts.filter = nextValue(argv, ++i, a);
|
|
113
|
+
else if (a === '--direction') opts.direction = nextValue(argv, ++i, a);
|
|
114
|
+
else if (a === '--no-semantic') opts.noSemantic = true;
|
|
111
115
|
else if (a === '--quantize') opts.quantize = nextValue(argv, ++i, a);
|
|
112
116
|
else if (a === '--rag') opts.rag = true;
|
|
113
117
|
else if (a === '--auto-tag') opts.autoTag = true;
|
|
@@ -115,6 +119,8 @@ function parseArgs(argv) {
|
|
|
115
119
|
else if (a === '--llm-endpoint') opts.llmEndpoint = nextValue(argv, ++i, a);
|
|
116
120
|
else if (a === '--llm-model') opts.llmModel = nextValue(argv, ++i, a);
|
|
117
121
|
else if (a === '--system-prompt') opts.systemPrompt = nextValue(argv, ++i, a);
|
|
122
|
+
else if (a === '--session') opts.session = nextValue(argv, ++i, a);
|
|
123
|
+
else if (a === '--resume') opts.resume = true;
|
|
118
124
|
else if (a === '--open') {
|
|
119
125
|
// `--open` opens the top hit; `--open N` opens the Nth hit (issue #110).
|
|
120
126
|
const next = argv[i + 1];
|
|
@@ -202,9 +208,10 @@ const KNOWN_CONFIG_KEYS = new Set([
|
|
|
202
208
|
'healthPublic', 'health-public', 'watch', 'watchInterval', 'watch-interval',
|
|
203
209
|
'watchDelay', 'watch-delay', 'offline', 'rerank', 'semantic', 'tag', 'project', 'type', 'status', 'canonical',
|
|
204
210
|
'watchDebug', 'watch-debug',
|
|
205
|
-
'format', 'noVectors', 'no-vectors', 'output', 'workers', 'ann', 'nprobe', 'graphBoost', 'graph-boost', 'filter', 'quantize',
|
|
211
|
+
'format', 'noVectors', 'no-vectors', 'output', 'workers', 'dimensions', 'dim', 'ann', 'nprobe', 'graphBoost', 'graph-boost', 'filter', 'quantize',
|
|
206
212
|
'vault', 'vaults', 'rag', 'autoTag', 'auto-tag', 'autoSummarize', 'auto-summarize',
|
|
207
213
|
'llmEndpoint', 'llm-endpoint', 'llmModel', 'llm-model', 'systemPrompt', 'system-prompt',
|
|
214
|
+
'session', 'resume',
|
|
208
215
|
'completions', 'open', 'dry-run', 'fix', 'yes', 'no-ui', 'noUi',
|
|
209
216
|
'noQueryCache', 'no-query-cache',
|
|
210
217
|
'mcp',
|
|
@@ -213,6 +220,27 @@ const KNOWN_CONFIG_KEYS = new Set([
|
|
|
213
220
|
'expand', 'expandPassages', 'expand-passages', 'llm-endpoint', 'llmEndpoint'
|
|
214
221
|
]);
|
|
215
222
|
|
|
223
|
+
const CONFIG_NETWORK_KEYS = new Set([
|
|
224
|
+
'port', 'host', 'apiKey', 'api-key', 'apiKeyFile', 'api-key-file',
|
|
225
|
+
'allowUnsecured', 'allow-unsecured', 'allowedHost', 'allowed-host',
|
|
226
|
+
'allowedHosts', 'allowed-hosts', 'corsOrigin', 'cors-origin',
|
|
227
|
+
'rateLimit', 'rate-limit', 'maxConcurrency', 'max-concurrency',
|
|
228
|
+
'healthPublic', 'health-public', 'mcp', 'llmEndpoint', 'llm-endpoint',
|
|
229
|
+
'llmModel', 'llm-model', 'embedder', 'embedder-model', 'embedder-base-url',
|
|
230
|
+
'embedder-key-file', 'embedderModel', 'embedderBaseUrl', 'embedderKeyFile'
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
const CONFIG_EXEC_KEYS = new Set([
|
|
234
|
+
// No execution surfaces permitted in local repo config
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
function getConfigKeyTrustTier(key) {
|
|
238
|
+
if (CONFIG_EXEC_KEYS.has(key)) return 'exec';
|
|
239
|
+
if (CONFIG_NETWORK_KEYS.has(key)) return 'network';
|
|
240
|
+
if (KNOWN_CONFIG_KEYS.has(key)) return 'local-safe';
|
|
241
|
+
return 'unknown';
|
|
242
|
+
}
|
|
243
|
+
|
|
216
244
|
function findConfigFile(explicitPath) {
|
|
217
245
|
if (explicitPath) {
|
|
218
246
|
const abs = path.resolve(explicitPath);
|
|
@@ -252,22 +280,26 @@ function findConfigFile(explicitPath) {
|
|
|
252
280
|
}
|
|
253
281
|
|
|
254
282
|
function loadConfigFile(configPath) {
|
|
255
|
-
if (!configPath) return { config: {}, path: null, unknownKeys: [], error: null };
|
|
283
|
+
if (!configPath) return { config: {}, path: null, unknownKeys: [], trustTiers: {}, error: null };
|
|
256
284
|
try {
|
|
257
285
|
const raw = fs.readFileSync(configPath, 'utf8');
|
|
258
286
|
const parsed = JSON.parse(raw);
|
|
259
287
|
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
260
|
-
return { config: {}, path: configPath, unknownKeys: [], error: `Config at ${configPath} must be a JSON object` };
|
|
288
|
+
return { config: {}, path: configPath, unknownKeys: [], trustTiers: {}, error: `Config at ${configPath} must be a JSON object` };
|
|
261
289
|
}
|
|
262
290
|
const unknownKeys = Object.keys(parsed).filter(k => !KNOWN_CONFIG_KEYS.has(k));
|
|
291
|
+
const trustTiers = {};
|
|
292
|
+
for (const k of Object.keys(parsed)) {
|
|
293
|
+
trustTiers[k] = getConfigKeyTrustTier(k);
|
|
294
|
+
}
|
|
263
295
|
const normalized = {};
|
|
264
296
|
for (const [k, v] of Object.entries(parsed)) {
|
|
265
297
|
const camel = k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
266
298
|
normalized[camel] = v;
|
|
267
299
|
}
|
|
268
|
-
return { config: normalized, path: configPath, unknownKeys, error: null };
|
|
300
|
+
return { config: normalized, path: configPath, unknownKeys, trustTiers, error: null };
|
|
269
301
|
} catch (e) {
|
|
270
|
-
return { config: {}, path: configPath, unknownKeys: [], error: `Failed to parse config at ${configPath}: ${e.message}` };
|
|
302
|
+
return { config: {}, path: configPath, unknownKeys: [], trustTiers: {}, error: `Failed to parse config at ${configPath}: ${e.message}` };
|
|
271
303
|
}
|
|
272
304
|
}
|
|
273
305
|
|
|
@@ -328,6 +360,9 @@ Usage:
|
|
|
328
360
|
mdss check --db <dir> [--json] Diagnose index/db/model cache (alias: doctor)
|
|
329
361
|
mdss export --db <dir> [options] Export index to JSONL / CSV / Parquet
|
|
330
362
|
mdss search --db <dir> [options] "query" Search by meaning
|
|
363
|
+
mdss related --db <dir> [options] <note> Find related notes via graph links and similarity
|
|
364
|
+
mdss ask --db <dir> [options] "question" Grounded QA synthesis with citations
|
|
365
|
+
mdss chat --db <dir> [options] Multi-turn persistent chat with citation manifest
|
|
331
366
|
mdss serve --db <dir> [--port <n>] [--host <ip>] [--watch] Daemon: warm model + index
|
|
332
367
|
mdss mcp --db <dir> [--list-tools] Start MCP server over stdio for LLM agents / IDEs
|
|
333
368
|
mdss models List available models
|
|
@@ -339,6 +374,7 @@ Options:
|
|
|
339
374
|
--index-dir <dir> Where to store the index (default: <db>/.mdss).
|
|
340
375
|
--cache-dir <dir> Model cache dir (default: ~/.cache/mdss, or MDSS_CACHE_DIR).
|
|
341
376
|
--model <name|id> Embedding model (default: ${DEFAULT_MODEL}). See \`mdss models\`.
|
|
377
|
+
--dimensions <n> Truncate vector dimensions for Matryoshka/MRL models (alias: --dim).
|
|
342
378
|
--workers <n> Number of parallel batch workers for indexing (default: 1).
|
|
343
379
|
--format <fmt> Export format: jsonl (default), csv, parquet (export).
|
|
344
380
|
--no-vectors Omit vector embeddings from export (export).
|
|
@@ -499,6 +535,7 @@ async function cmdIndex(opts) {
|
|
|
499
535
|
r = await buildIndex({
|
|
500
536
|
db, indexDir, cacheDir,
|
|
501
537
|
modelName: embedderOverrides.modelName || opts.model || DEFAULT_MODEL,
|
|
538
|
+
dimensions: opts.dimensions,
|
|
502
539
|
...(embedderOverrides.embedFn ? { embedFn: embedderOverrides.embedFn } : {}),
|
|
503
540
|
ignore: opts.ignore,
|
|
504
541
|
offline: resolveOffline(opts),
|
|
@@ -775,6 +812,7 @@ function checkHealth({ db, indexDir, cacheDir, requireOffline = false, config =
|
|
|
775
812
|
path: config.path,
|
|
776
813
|
valid: !config.error && (!config.unknownKeys || config.unknownKeys.length === 0),
|
|
777
814
|
unknownKeys: config.unknownKeys || [],
|
|
815
|
+
trustTiers: config.trustTiers || {},
|
|
778
816
|
error: config.error || (config.unknownKeys && config.unknownKeys.length > 0 ? `unknown config keys: ${config.unknownKeys.join(', ')}` : null),
|
|
779
817
|
};
|
|
780
818
|
if (!report.config.valid) {
|
|
@@ -1088,6 +1126,13 @@ async function cmdTui(opts) {
|
|
|
1088
1126
|
semanticOnly: !!opts.semantic,
|
|
1089
1127
|
offline: resolveOffline(opts),
|
|
1090
1128
|
path: opts.path.length > 0 ? opts.path : undefined,
|
|
1129
|
+
since: opts.since || undefined,
|
|
1130
|
+
filter: opts.filter || undefined,
|
|
1131
|
+
tag: opts.tag.length > 0 ? opts.tag : undefined,
|
|
1132
|
+
project: opts.project || undefined,
|
|
1133
|
+
type: opts.type || undefined,
|
|
1134
|
+
status: opts.status || undefined,
|
|
1135
|
+
graphBoost: typeof opts.graphBoost === 'number' ? opts.graphBoost : undefined,
|
|
1091
1136
|
rerank: !!opts.rerank,
|
|
1092
1137
|
rag: !!opts.rag,
|
|
1093
1138
|
});
|
|
@@ -1129,6 +1174,150 @@ async function cmdAsk(opts) {
|
|
|
1129
1174
|
}
|
|
1130
1175
|
}
|
|
1131
1176
|
|
|
1177
|
+
async function cmdChat(opts) {
|
|
1178
|
+
const db = resolveDb(opts);
|
|
1179
|
+
const indexDir = resolveIndexDir(opts, db);
|
|
1180
|
+
const cacheDir = resolveCache(opts);
|
|
1181
|
+
const k = opts.k || 5;
|
|
1182
|
+
|
|
1183
|
+
const vectorsPath = path.join(indexDir, 'vectors.json');
|
|
1184
|
+
if (!fs.existsSync(vectorsPath)) {
|
|
1185
|
+
die(`No index at ${vectorsPath}. Run \`mdss index --db ${db}\` first.`);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const { chatTurn, loadChatSession, listChatSessions } = await import('../dist/rag.js');
|
|
1189
|
+
const loaded = loadIndex(indexDir);
|
|
1190
|
+
|
|
1191
|
+
let sessionId = opts.session;
|
|
1192
|
+
if (!sessionId && opts.resume) {
|
|
1193
|
+
const existing = listChatSessions(indexDir);
|
|
1194
|
+
if (existing.length > 0) {
|
|
1195
|
+
sessionId = existing[0].id;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (!sessionId) {
|
|
1199
|
+
sessionId = crypto.randomUUID().slice(0, 8);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const session = loadChatSession(indexDir, sessionId, (msg) => {
|
|
1203
|
+
if (!opts.json) process.stderr.write(`${msg}\n`);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
const query = opts._.join(' ').trim();
|
|
1207
|
+
// If one-shot query provided as argument:
|
|
1208
|
+
if (query) {
|
|
1209
|
+
const res = await chatTurn({
|
|
1210
|
+
session,
|
|
1211
|
+
query,
|
|
1212
|
+
loaded,
|
|
1213
|
+
indexDir,
|
|
1214
|
+
cacheDir,
|
|
1215
|
+
k,
|
|
1216
|
+
llmEndpoint: opts.llmEndpoint,
|
|
1217
|
+
llmModel: opts.llmModel,
|
|
1218
|
+
systemPrompt: opts.systemPrompt,
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
if (opts.json) {
|
|
1222
|
+
process.stdout.write(JSON.stringify(res, null, 2) + '\n');
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
process.stdout.write(`\n${res.answer}\n\n`);
|
|
1227
|
+
if (res.citations && res.citations.length > 0) {
|
|
1228
|
+
process.stdout.write('Sources:\n');
|
|
1229
|
+
for (const c of res.citations) {
|
|
1230
|
+
const heading = c.heading ? ` › ${c.heading}` : '';
|
|
1231
|
+
process.stdout.write(` - ${c.file}${heading} [score: ${c.score.toFixed(3)}]\n`);
|
|
1232
|
+
}
|
|
1233
|
+
process.stdout.write('\n');
|
|
1234
|
+
}
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Interactive REPL mode:
|
|
1239
|
+
const readline = await import('node:readline');
|
|
1240
|
+
const rl = readline.createInterface({
|
|
1241
|
+
input: process.stdin,
|
|
1242
|
+
output: process.stdout,
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
const banner = `\x1b[1mmdss chat\x1b[0m (session: \x1b[36m${session.id}\x1b[0m, turns: ${session.turns.length}, chunks: ${loaded.index.chunks.length})\nCommands: /session, /sources, /help, /exit\n`;
|
|
1246
|
+
process.stdout.write(banner);
|
|
1247
|
+
|
|
1248
|
+
const promptUser = () => {
|
|
1249
|
+
rl.question('\n\x1b[1mmdss chat>\x1b[0m ', async (input) => {
|
|
1250
|
+
const line = (input || '').trim();
|
|
1251
|
+
if (!line) {
|
|
1252
|
+
promptUser();
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (line === '/exit' || line === '/quit' || line === ':q') {
|
|
1257
|
+
rl.close();
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (line === '/help') {
|
|
1262
|
+
process.stdout.write('Available commands:\n /session Show current session information\n /sources Show retrieved sources manifest from last turn\n /help Show this help message\n /exit Quit chat\n');
|
|
1263
|
+
promptUser();
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
if (line === '/session') {
|
|
1268
|
+
process.stdout.write(`Session ID: ${session.id}\nCreated: ${session.createdAt}\nTurns: ${session.turns.length}\n`);
|
|
1269
|
+
promptUser();
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if (line === '/sources') {
|
|
1274
|
+
const lastTurn = session.turns[session.turns.length - 1];
|
|
1275
|
+
if (!lastTurn || !lastTurn.manifest || lastTurn.manifest.length === 0) {
|
|
1276
|
+
process.stdout.write('No sources available yet.\n');
|
|
1277
|
+
} else {
|
|
1278
|
+
process.stdout.write('Sources from last turn:\n');
|
|
1279
|
+
for (const m of lastTurn.manifest) {
|
|
1280
|
+
const loc = (m.startLine && m.endLine) ? `:L${m.startLine}-L${m.endLine}` : '';
|
|
1281
|
+
const heading = m.heading ? ` › ${m.heading}` : '';
|
|
1282
|
+
process.stdout.write(` - ${m.file}${heading}${loc} [score: ${m.score.toFixed(3)}]\n`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
promptUser();
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
try {
|
|
1290
|
+
const res = await chatTurn({
|
|
1291
|
+
session,
|
|
1292
|
+
query: line,
|
|
1293
|
+
loaded,
|
|
1294
|
+
indexDir,
|
|
1295
|
+
cacheDir,
|
|
1296
|
+
k,
|
|
1297
|
+
llmEndpoint: opts.llmEndpoint,
|
|
1298
|
+
llmModel: opts.llmModel,
|
|
1299
|
+
systemPrompt: opts.systemPrompt,
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
process.stdout.write(`\n${res.answer}\n`);
|
|
1303
|
+
if (res.citations && res.citations.length > 0) {
|
|
1304
|
+
process.stdout.write('\nSources:\n');
|
|
1305
|
+
for (const c of res.citations) {
|
|
1306
|
+
const heading = c.heading ? ` › ${c.heading}` : '';
|
|
1307
|
+
process.stdout.write(` - ${c.file}${heading} [score: ${c.score.toFixed(3)}]\n`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
} catch (e) {
|
|
1311
|
+
process.stdout.write(`\nError: ${e.message}\n`);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
promptUser();
|
|
1315
|
+
});
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
promptUser();
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1132
1321
|
async function cmdSearch(opts) {
|
|
1133
1322
|
if (opts.interactive) return cmdTui(opts);
|
|
1134
1323
|
const cacheDir = resolveCache(opts);
|
|
@@ -1196,6 +1385,7 @@ async function cmdSearch(opts) {
|
|
|
1196
1385
|
nprobe: opts.nprobe,
|
|
1197
1386
|
graphBoost: opts.graphBoost,
|
|
1198
1387
|
filter: opts.filter,
|
|
1388
|
+
dimensions: opts.dimensions,
|
|
1199
1389
|
});
|
|
1200
1390
|
}
|
|
1201
1391
|
|
|
@@ -1289,9 +1479,10 @@ function cmdModels() {
|
|
|
1289
1479
|
const pooling = m.pooling ?? 'mean';
|
|
1290
1480
|
const normalized = m.normalize !== false ? 'L2' : 'raw';
|
|
1291
1481
|
const rev = m.revision ? ` · rev ${m.revision.slice(0, 8)}` : '';
|
|
1482
|
+
const mrl = m.dimensions ? ` · MRL: ${m.dimensions.join(', ')}` : '';
|
|
1292
1483
|
process.stdout.write(
|
|
1293
1484
|
` ${alias}${star}\n` +
|
|
1294
|
-
` ${m.id} · dim ${m.dim}${rev}\n` +
|
|
1485
|
+
` ${m.id} · dim ${m.dim}${mrl}${rev}\n` +
|
|
1295
1486
|
` ${pooling} · ${normalized} · maxTokens ${m.maxTokens ?? '—'} ` +
|
|
1296
1487
|
(m.family ? `· family ${m.family}\n` : '\n') +
|
|
1297
1488
|
` ${m.note}\n\n`,
|
|
@@ -1414,7 +1605,7 @@ async function cmdMcp(opts) {
|
|
|
1414
1605
|
// on STDERR so stdout stays machine-readable; warm cache emits nothing at all.
|
|
1415
1606
|
// ---------------------------------------------------------------------------
|
|
1416
1607
|
|
|
1417
|
-
const DOWNLOAD_COMMANDS = new Set(['index', 'search', 'ask', 'serve']);
|
|
1608
|
+
const DOWNLOAD_COMMANDS = new Set(['index', 'search', 'ask', 'chat', 'serve']);
|
|
1418
1609
|
|
|
1419
1610
|
function formatMb(bytes) {
|
|
1420
1611
|
if (!Number.isFinite(bytes) || bytes <= 0) return '0 MB';
|
|
@@ -1469,6 +1660,59 @@ async function cmdCompletions(opts) {
|
|
|
1469
1660
|
process.stdout.write(generateCompletion(shell));
|
|
1470
1661
|
}
|
|
1471
1662
|
|
|
1663
|
+
async function cmdRelated(opts) {
|
|
1664
|
+
const db = resolveDb(opts);
|
|
1665
|
+
const indexDir = resolveIndexDir(opts, db);
|
|
1666
|
+
const vectorsPath = path.join(indexDir, 'vectors.json');
|
|
1667
|
+
if (!fs.existsSync(vectorsPath)) {
|
|
1668
|
+
die(`No index at ${vectorsPath}. Run \`mdss index --db ${db}\` first.`);
|
|
1669
|
+
}
|
|
1670
|
+
const target = opts._.shift();
|
|
1671
|
+
if (!target) {
|
|
1672
|
+
die('Missing note path/title/alias to find relations for. e.g. `mdss related --db . "Architecture"`');
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
const loaded = loadIndex(indexDir);
|
|
1676
|
+
const k = Number.isInteger(opts.k) && opts.k > 0 ? opts.k : 10;
|
|
1677
|
+
const direction = opts.direction || 'both';
|
|
1678
|
+
const semantic = !opts.noSemantic;
|
|
1679
|
+
|
|
1680
|
+
let r;
|
|
1681
|
+
try {
|
|
1682
|
+
r = findRelatedNotes({
|
|
1683
|
+
loaded,
|
|
1684
|
+
target,
|
|
1685
|
+
k,
|
|
1686
|
+
direction,
|
|
1687
|
+
semantic,
|
|
1688
|
+
});
|
|
1689
|
+
} catch (e) {
|
|
1690
|
+
die(e.message);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
if (opts.json) {
|
|
1694
|
+
process.stdout.write(JSON.stringify(r, null, 2) + '\n');
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (r.results.length === 0) {
|
|
1699
|
+
process.stdout.write(`No related notes found for "${target}" (${r.resolvedFile}).\n`);
|
|
1700
|
+
if (opts.noSemantic) {
|
|
1701
|
+
process.stdout.write('Hint: Try running without `--no-semantic` to discover conceptually related notes without explicit links.\n');
|
|
1702
|
+
}
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
process.stdout.write(`Related notes for "${target}" (${r.resolvedFile}):\n\n`);
|
|
1707
|
+
r.results.forEach((hit, idx) => {
|
|
1708
|
+
const scoreStr = hit.score.toFixed(2);
|
|
1709
|
+
process.stdout.write(` ${idx + 1}. \x1b[1m${hit.file}\x1b[0m — score: ${scoreStr} [${hit.reason}]\n`);
|
|
1710
|
+
if (hit.title && hit.title !== hit.file) {
|
|
1711
|
+
process.stdout.write(` title: "${hit.title}"\n`);
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1472
1716
|
async function main() {
|
|
1473
1717
|
const argv = process.argv.slice(2);
|
|
1474
1718
|
const opts = parseArgs(argv);
|
|
@@ -1496,7 +1740,9 @@ async function main() {
|
|
|
1496
1740
|
case 'doctor': return await cmdCheck(opts);
|
|
1497
1741
|
case 'export': return await cmdExport(opts);
|
|
1498
1742
|
case 'search': return await cmdSearch(opts);
|
|
1743
|
+
case 'related': return await cmdRelated(opts);
|
|
1499
1744
|
case 'ask': return await cmdAsk(opts);
|
|
1745
|
+
case 'chat': return await cmdChat(opts);
|
|
1500
1746
|
case 'tui': return await cmdTui(opts);
|
|
1501
1747
|
case 'serve': return await cmdServe(opts);
|
|
1502
1748
|
case 'mcp': return await cmdMcp(opts);
|
|
@@ -1542,6 +1788,7 @@ export {
|
|
|
1542
1788
|
parseArgs, nextInt, nextFloat, nextValue,
|
|
1543
1789
|
resolveDb, resolveIndexDir, resolveCache, resolveOffline,
|
|
1544
1790
|
findConfigFile, loadConfigFile, applyConfigDefaults, KNOWN_CONFIG_KEYS,
|
|
1791
|
+
CONFIG_NETWORK_KEYS, CONFIG_EXEC_KEYS, getConfigKeyTrustTier,
|
|
1545
1792
|
checkHealth, cmdExport,
|
|
1546
1793
|
die, main, HELP, VERSION,
|
|
1547
1794
|
};
|
package/dist/completions.js
CHANGED
|
@@ -54,6 +54,7 @@ export const FLAGS = [
|
|
|
54
54
|
{ name: '--index-dir', kind: 'dir' },
|
|
55
55
|
{ name: '--cache-dir', kind: 'dir' },
|
|
56
56
|
{ name: '--model', kind: 'value' },
|
|
57
|
+
{ name: '--dimensions', alias: '--dim', kind: 'number' },
|
|
57
58
|
{ name: '--k', kind: 'number' },
|
|
58
59
|
{ name: '--max-per-file', kind: 'number' },
|
|
59
60
|
{ name: '--max-per-doc', kind: 'number' },
|
|
@@ -79,13 +80,17 @@ export const FLAGS = [
|
|
|
79
80
|
{ name: '--graph-boost', kind: 'number' },
|
|
80
81
|
{ name: '--filter', kind: 'value' },
|
|
81
82
|
{ name: '--quantize', kind: 'value' },
|
|
83
|
+
{ name: '--direction', kind: 'value' },
|
|
84
|
+
{ name: '--no-semantic', kind: 'flag' },
|
|
85
|
+
{ name: '--session', kind: 'value' },
|
|
86
|
+
{ name: '--resume', kind: 'flag' },
|
|
82
87
|
{ name: '--llm-endpoint', kind: 'value' },
|
|
83
88
|
{ name: '--llm-model', kind: 'value' },
|
|
84
89
|
{ name: '--system-prompt', kind: 'value' },
|
|
85
90
|
];
|
|
86
91
|
export const COMMANDS = [
|
|
87
92
|
'index', 'stats', 'check', 'doctor', 'export', 'search',
|
|
88
|
-
'ask', 'tui', 'serve', 'mcp', 'models', 'completions',
|
|
93
|
+
'ask', 'chat', 'tui', 'serve', 'mcp', 'models', 'completions', 'related',
|
|
89
94
|
];
|
|
90
95
|
export const COMPLETION_SHELLS = ['bash', 'zsh', 'fish', 'powershell'];
|
|
91
96
|
const DIR_FLAGS = [...new Set(FLAGS.filter((f) => f.kind === 'dir').flatMap((f) => [f.name, ...(f.alias && !f.alias.startsWith('--') ? [f.alias] : [])]))];
|
package/dist/core.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export declare function prepareEmbeddingRequest(model: ModelDescriptor, texts: s
|
|
|
48
48
|
normalize: boolean;
|
|
49
49
|
};
|
|
50
50
|
};
|
|
51
|
+
export declare function truncateAndNormalizeVector(vec: number[] | Float32Array, targetDim: number, normalize?: boolean): number[];
|
|
51
52
|
export declare function embed(texts: string[], kind: 'query' | 'passage', model: ModelDescriptor, cacheDir: string, offline?: boolean, retryOpts?: {
|
|
52
53
|
maxRetries?: number;
|
|
53
54
|
delays?: number[];
|
|
@@ -88,3 +89,28 @@ export interface ParsedChunkFile {
|
|
|
88
89
|
meta: DocumentMetadata;
|
|
89
90
|
}
|
|
90
91
|
export declare function parseFile(absPath: string, dbDir: string, maxChunk?: number, raw?: string): ParsedChunkFile[];
|
|
92
|
+
export interface GetDocLinesOptions {
|
|
93
|
+
file: string;
|
|
94
|
+
fromLine?: number;
|
|
95
|
+
maxLines?: number;
|
|
96
|
+
dbDir?: string;
|
|
97
|
+
chunks?: Array<{
|
|
98
|
+
file: string;
|
|
99
|
+
text: string;
|
|
100
|
+
startLine?: number;
|
|
101
|
+
endLine?: number;
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
104
|
+
export interface GetDocLinesResult {
|
|
105
|
+
file: string;
|
|
106
|
+
fromLine: number;
|
|
107
|
+
toLine: number;
|
|
108
|
+
lineCount: number;
|
|
109
|
+
totalLines: number;
|
|
110
|
+
text: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Line-range retrieval helper (issue #140).
|
|
114
|
+
* Retrieves an exact line span from disk or fallback chunk data with EOF clamping.
|
|
115
|
+
*/
|
|
116
|
+
export declare function getDocLines(opts: GetDocLinesOptions): GetDocLinesResult;
|