md-semantic-search 0.4.0 → 1.0.1
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 +125 -1
- package/bin/cli.mjs +482 -34
- package/dist/binary-format.d.ts +25 -0
- package/dist/binary-format.js +178 -0
- package/dist/collapse.d.ts +6 -0
- package/dist/collapse.js +20 -0
- package/dist/core.d.ts +85 -0
- package/dist/core.js +471 -0
- package/dist/federation.d.ts +15 -0
- package/dist/federation.js +94 -0
- package/dist/filter.d.ts +29 -0
- package/dist/filter.js +317 -0
- package/dist/frontmatter.d.ts +20 -0
- package/dist/frontmatter.js +150 -0
- package/dist/index-format.d.ts +23 -0
- package/dist/index-format.js +125 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +37 -0
- package/dist/indexer.d.ts +83 -0
- package/dist/indexer.js +466 -0
- package/dist/ivf.d.ts +33 -0
- package/dist/ivf.js +144 -0
- package/dist/lexical.d.ts +44 -0
- package/dist/lexical.js +226 -0
- package/dist/markdown-parser.d.ts +28 -0
- package/dist/markdown-parser.js +311 -0
- package/dist/mcp.d.ts +159 -0
- package/dist/mcp.js +264 -0
- package/dist/metrics.d.ts +43 -0
- package/dist/metrics.js +97 -0
- package/dist/models.d.ts +38 -0
- package/dist/models.js +181 -0
- package/dist/quantization.d.ts +24 -0
- package/dist/quantization.js +85 -0
- package/dist/rag.d.ts +49 -0
- package/dist/rag.js +128 -0
- package/dist/rerank.d.ts +10 -0
- package/dist/rerank.js +41 -0
- package/dist/search.d.ts +149 -0
- package/dist/search.js +644 -0
- package/dist/serve.d.ts +49 -0
- package/dist/serve.js +341 -0
- package/dist/summarize.d.ts +16 -0
- package/dist/summarize.js +82 -0
- package/dist/tui.d.ts +18 -0
- package/dist/tui.js +219 -0
- package/dist/watcher.d.ts +11 -0
- package/dist/watcher.js +130 -0
- package/dist/wikilinks.d.ts +69 -0
- package/dist/wikilinks.js +271 -0
- package/package.json +21 -5
- package/src/binary-format.ts +216 -0
- package/src/{collapse.mjs → collapse.ts} +7 -12
- package/src/core.ts +551 -0
- package/src/federation.ts +118 -0
- package/src/filter.ts +368 -0
- package/src/{frontmatter.mjs → frontmatter.ts} +27 -50
- package/src/{index-format.mjs → index-format.ts} +58 -41
- package/src/{index.mjs → index.ts} +54 -8
- package/src/indexer.ts +617 -0
- package/src/ivf.ts +190 -0
- package/src/{lexical.mjs → lexical.ts} +66 -100
- package/src/{markdown-parser.mjs → markdown-parser.ts} +135 -109
- package/src/mcp.ts +289 -0
- package/src/metrics.ts +135 -0
- package/src/models.ts +219 -0
- package/src/quantization.ts +93 -0
- package/src/rag.ts +189 -0
- package/src/rerank.ts +54 -0
- package/src/search.ts +805 -0
- package/src/serve.ts +393 -0
- package/src/summarize.ts +98 -0
- package/src/tui.ts +251 -0
- package/src/watcher.ts +149 -0
- package/src/wikilinks.ts +376 -0
- package/src/core.mjs +0 -535
- package/src/indexer.mjs +0 -540
- package/src/metrics.mjs +0 -186
- package/src/models.mjs +0 -299
- package/src/rerank.mjs +0 -75
- package/src/search.mjs +0 -636
- package/src/serve.mjs +0 -387
- package/src/wikilinks.mjs +0 -241
package/bin/cli.mjs
CHANGED
|
@@ -14,12 +14,14 @@ import path from 'node:path';
|
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import os from 'node:os';
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
|
+
import { buildIndex } from '../dist/indexer.js';
|
|
19
|
+
import { search } from '../dist/search.js';
|
|
20
|
+
import { searchFederated } from '../dist/federation.js';
|
|
21
|
+
import { createServe, DEFAULT_PORT, DEFAULT_HOST } from '../dist/serve.js';
|
|
22
|
+
import { MODELS, DEFAULT_MODEL, resolveModel } from '../dist/models.js';
|
|
23
|
+
import { decodeVec, walkMarkdown, SCHEMA_VERSION, assertSafePath } from '../dist/core.js';
|
|
24
|
+
import { inspectIndexSchema, validateCurrentChunk, validateIndexEnvelope, validateNumericVector } from '../dist/index-format.js';
|
|
23
25
|
|
|
24
26
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
25
27
|
const PKG_ROOT = path.resolve(HERE, '..');
|
|
@@ -40,7 +42,7 @@ function defaultCacheDir() {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
function parseArgs(argv) {
|
|
43
|
-
const opts = { _: [], ignore: [], path: [] };
|
|
45
|
+
const opts = { _: [], ignore: [], path: [], vaults: [] };
|
|
44
46
|
for (let i = 0; i < argv.length; i++) {
|
|
45
47
|
const a = argv[i];
|
|
46
48
|
if (a === '--json') opts.json = true;
|
|
@@ -49,9 +51,17 @@ function parseArgs(argv) {
|
|
|
49
51
|
else if (a === '--watch') opts.watch = true;
|
|
50
52
|
else if (a === '--rerank') opts.rerank = true;
|
|
51
53
|
else if (a === '--explain') opts.explain = true;
|
|
54
|
+
else if (a === '--interactive' || a === '-i') opts.interactive = true;
|
|
55
|
+
else if (a === '--list-tools') opts.listTools = true;
|
|
52
56
|
else if (a === '--version') opts.version = true;
|
|
53
57
|
else if (a === '-h' || a === '--help') opts.help = true;
|
|
58
|
+
else if (a === '--workers') opts.workers = nextInt(argv, ++i, a);
|
|
59
|
+
else if (a === '--config') opts.config = nextValue(argv, ++i, a);
|
|
60
|
+
else if (a === '--format') opts.format = nextValue(argv, ++i, a);
|
|
61
|
+
else if (a === '--no-vectors' || a === '--no-vector') opts.noVectors = true;
|
|
62
|
+
else if (a === '--output' || a === '-o') opts.output = nextValue(argv, ++i, a);
|
|
54
63
|
else if (a === '--db') opts.db = nextValue(argv, ++i, a);
|
|
64
|
+
else if (a === '--vault') opts.vaults.push(nextValue(argv, ++i, a));
|
|
55
65
|
else if (a === '--index-dir') opts.indexDir = nextValue(argv, ++i, a);
|
|
56
66
|
else if (a === '--cache-dir') opts.cacheDir = nextValue(argv, ++i, a);
|
|
57
67
|
else if (a === '--model') opts.model = nextValue(argv, ++i, a);
|
|
@@ -61,6 +71,8 @@ function parseArgs(argv) {
|
|
|
61
71
|
else if (a === '--target-tokens') opts.targetTokens = nextInt(argv, ++i, a);
|
|
62
72
|
else if (a === '--port') opts.port = nextInt(argv, ++i, a);
|
|
63
73
|
else if (a === '--host') opts.host = nextValue(argv, ++i, a);
|
|
74
|
+
else if (a === '--api-key') opts.apiKey = nextValue(argv, ++i, a);
|
|
75
|
+
else if (a === '--health-public') opts.healthPublic = true;
|
|
64
76
|
else if (a === '--watch-delay') opts.watchDelay = nextInt(argv, ++i, a);
|
|
65
77
|
else if (a === '--watch-interval') opts.watchInterval = nextInt(argv, ++i, a);
|
|
66
78
|
else if (a === '--since') opts.since = nextValue(argv, ++i, a);
|
|
@@ -70,10 +82,21 @@ function parseArgs(argv) {
|
|
|
70
82
|
const v = nextValue(argv, ++i, a);
|
|
71
83
|
opts.tag = opts.tag ? (Array.isArray(opts.tag) ? [...opts.tag, v] : [opts.tag, v]) : v;
|
|
72
84
|
}
|
|
85
|
+
else if (a === '--ann') opts.ann = true;
|
|
86
|
+
else if (a === '--nprobe') opts.nprobe = nextInt(argv, ++i, a);
|
|
73
87
|
else if (a === '--project') opts.project = nextValue(argv, ++i, a);
|
|
74
88
|
else if (a === '--type') opts.type = nextValue(argv, ++i, a);
|
|
75
89
|
else if (a === '--status') opts.status = nextValue(argv, ++i, a);
|
|
76
90
|
else if (a === '--canonical') opts.canonical = true;
|
|
91
|
+
else if (a === '--graph-boost') opts.graphBoost = nextFloat(argv, ++i, a);
|
|
92
|
+
else if (a === '--filter') opts.filter = nextValue(argv, ++i, a);
|
|
93
|
+
else if (a === '--quantize') opts.quantize = nextValue(argv, ++i, a);
|
|
94
|
+
else if (a === '--rag') opts.rag = true;
|
|
95
|
+
else if (a === '--auto-tag') opts.autoTag = true;
|
|
96
|
+
else if (a === '--auto-summarize') opts.autoSummarize = true;
|
|
97
|
+
else if (a === '--llm-endpoint') opts.llmEndpoint = nextValue(argv, ++i, a);
|
|
98
|
+
else if (a === '--llm-model') opts.llmModel = nextValue(argv, ++i, a);
|
|
99
|
+
else if (a === '--system-prompt') opts.systemPrompt = nextValue(argv, ++i, a);
|
|
77
100
|
else if (a.startsWith('-')) die(`unknown option: ${a}. Try \`mdss --help\`.`);
|
|
78
101
|
else opts._.push(a);
|
|
79
102
|
}
|
|
@@ -95,6 +118,96 @@ function nextInt(argv, i, flag) {
|
|
|
95
118
|
return k;
|
|
96
119
|
}
|
|
97
120
|
|
|
121
|
+
/** Like nextValue but requires a non-negative float. */
|
|
122
|
+
function nextFloat(argv, i, flag) {
|
|
123
|
+
const v = nextValue(argv, i, flag);
|
|
124
|
+
const f = Number.parseFloat(v);
|
|
125
|
+
if (!Number.isFinite(f) || f < 0) die(`${flag} must be a non-negative number, got "${v}"`);
|
|
126
|
+
return f;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const KNOWN_CONFIG_KEYS = new Set([
|
|
130
|
+
'db', 'model', 'indexDir', 'index-dir', 'cacheDir', 'cache-dir',
|
|
131
|
+
'ignore', 'path', 'k', 'maxPerFile', 'max-per-file', 'maxPerDoc', 'max-per-doc',
|
|
132
|
+
'targetTokens', 'target-tokens', 'port', 'host', 'apiKey', 'api-key',
|
|
133
|
+
'healthPublic', 'health-public', 'watch', 'watchInterval', 'watch-interval',
|
|
134
|
+
'watchDelay', 'watch-delay', 'offline', 'rerank', 'semantic', 'tag', 'project', 'type', 'status', 'canonical',
|
|
135
|
+
'format', 'noVectors', 'no-vectors', 'output', 'workers', 'ann', 'nprobe', 'graphBoost', 'graph-boost', 'filter', 'quantize',
|
|
136
|
+
'vault', 'vaults', 'rag', 'autoTag', 'auto-tag', 'autoSummarize', 'auto-summarize',
|
|
137
|
+
'llmEndpoint', 'llm-endpoint', 'llmModel', 'llm-model', 'systemPrompt', 'system-prompt'
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
function findConfigFile(explicitPath) {
|
|
141
|
+
if (explicitPath) {
|
|
142
|
+
const abs = path.resolve(explicitPath);
|
|
143
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) die(`config file not found: ${abs}`);
|
|
144
|
+
return abs;
|
|
145
|
+
}
|
|
146
|
+
if (process.env.MDSS_CONFIG) {
|
|
147
|
+
const abs = path.resolve(process.env.MDSS_CONFIG);
|
|
148
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) die(`config file from MDSS_CONFIG not found: ${abs}`);
|
|
149
|
+
return abs;
|
|
150
|
+
}
|
|
151
|
+
const configNames = ['.mdssrc.json', 'mdss.config.json', '.mdssrc'];
|
|
152
|
+
let curr = process.cwd();
|
|
153
|
+
while (true) {
|
|
154
|
+
for (const name of configNames) {
|
|
155
|
+
const candidate = path.join(curr, name);
|
|
156
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
157
|
+
return candidate;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const parent = path.dirname(curr);
|
|
161
|
+
if (parent === curr) break;
|
|
162
|
+
curr = parent;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const home = os.homedir();
|
|
166
|
+
if (home) {
|
|
167
|
+
for (const name of ['.mdssrc.json', '.mdssrc']) {
|
|
168
|
+
const candidate = path.join(home, name);
|
|
169
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
170
|
+
return candidate;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function loadConfigFile(configPath) {
|
|
179
|
+
if (!configPath) return { config: {}, path: null, unknownKeys: [], error: null };
|
|
180
|
+
try {
|
|
181
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
182
|
+
const parsed = JSON.parse(raw);
|
|
183
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
184
|
+
return { config: {}, path: configPath, unknownKeys: [], error: `Config at ${configPath} must be a JSON object` };
|
|
185
|
+
}
|
|
186
|
+
const unknownKeys = Object.keys(parsed).filter(k => !KNOWN_CONFIG_KEYS.has(k));
|
|
187
|
+
const normalized = {};
|
|
188
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
189
|
+
const camel = k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
190
|
+
normalized[camel] = v;
|
|
191
|
+
}
|
|
192
|
+
return { config: normalized, path: configPath, unknownKeys, error: null };
|
|
193
|
+
} catch (e) {
|
|
194
|
+
return { config: {}, path: configPath, unknownKeys: [], error: `Failed to parse config at ${configPath}: ${e.message}` };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function applyConfigDefaults(opts, config) {
|
|
199
|
+
for (const [k, v] of Object.entries(config)) {
|
|
200
|
+
if (k === 'ignore' || k === 'path') {
|
|
201
|
+
if ((!opts[k] || opts[k].length === 0) && v !== undefined) {
|
|
202
|
+
opts[k] = Array.isArray(v) ? [...v] : [v];
|
|
203
|
+
}
|
|
204
|
+
} else if (opts[k] === undefined && v !== undefined) {
|
|
205
|
+
opts[k] = v;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return opts;
|
|
209
|
+
}
|
|
210
|
+
|
|
98
211
|
function resolveDb(opts) {
|
|
99
212
|
const db = opts.db || process.env.MDSS_DB;
|
|
100
213
|
if (!db) die('Missing --db <dir> (or set MDSS_DB). Path to your .md folder.');
|
|
@@ -102,13 +215,23 @@ function resolveDb(opts) {
|
|
|
102
215
|
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
103
216
|
die(`--db is not a directory: ${abs}`);
|
|
104
217
|
}
|
|
105
|
-
|
|
218
|
+
let safeDb;
|
|
219
|
+
try {
|
|
220
|
+
safeDb = assertSafePath(abs);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
die(e.message);
|
|
223
|
+
}
|
|
224
|
+
return safeDb;
|
|
106
225
|
}
|
|
107
226
|
|
|
108
227
|
function resolveIndexDir(opts, db) {
|
|
109
228
|
// Default: <db>/.mdss. Override with --index-dir or MDSS_INDEX_DIR.
|
|
110
229
|
const dir = opts.indexDir || process.env.MDSS_INDEX_DIR || path.join(db, '.mdss');
|
|
111
|
-
|
|
230
|
+
try {
|
|
231
|
+
return assertSafePath(dir);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
die(e.message);
|
|
234
|
+
}
|
|
112
235
|
}
|
|
113
236
|
|
|
114
237
|
const resolveCache = (opts) =>
|
|
@@ -127,15 +250,22 @@ Usage:
|
|
|
127
250
|
mdss index --db <dir> [options] Build/refresh the index
|
|
128
251
|
mdss stats --db <dir> [--json] Index stats without loading the model
|
|
129
252
|
mdss check --db <dir> [--json] Diagnose index/db/model cache (alias: doctor)
|
|
253
|
+
mdss export --db <dir> [options] Export index to JSONL / CSV / Parquet
|
|
130
254
|
mdss search --db <dir> [options] "query" Search by meaning
|
|
131
255
|
mdss serve --db <dir> [--port <n>] [--host <ip>] [--watch] Daemon: warm model + index
|
|
256
|
+
mdss mcp --db <dir> [--list-tools] Start MCP server over stdio for LLM agents / IDEs
|
|
132
257
|
mdss models List available models
|
|
133
258
|
|
|
134
259
|
Options:
|
|
260
|
+
--config <file> Path to config file (default: .mdssrc.json / mdss.config.json).
|
|
135
261
|
--db <dir> Folder of .md files (or env MDSS_DB). Can be anywhere.
|
|
136
262
|
--index-dir <dir> Where to store the index (default: <db>/.mdss).
|
|
137
263
|
--cache-dir <dir> Model cache dir (default: ~/.cache/mdss, or MDSS_CACHE_DIR).
|
|
138
264
|
--model <name|id> Embedding model (default: ${DEFAULT_MODEL}). See \`mdss models\`.
|
|
265
|
+
--workers <n> Number of parallel batch workers for indexing (default: 1).
|
|
266
|
+
--format <fmt> Export format: jsonl (default), csv, parquet (export).
|
|
267
|
+
--no-vectors Omit vector embeddings from export (export).
|
|
268
|
+
--output <file> Output file path (export, default: stdout).
|
|
139
269
|
--ignore <glob> Skip files/paths (repeatable). e.g. --ignore "log.md".
|
|
140
270
|
--path <glob> Search only files matching glob (repeatable). e.g. --path "docs/**".
|
|
141
271
|
--since <date> Search only files modified at/after date (YYYY-MM-DD or ISO).
|
|
@@ -143,6 +273,10 @@ Options:
|
|
|
143
273
|
--json Machine-readable output (index, stats, search).
|
|
144
274
|
--semantic Pure vector ranking, skip lexical/RRF fusion (search).
|
|
145
275
|
--rerank Re-rank candidates with a cross-encoder (search; ~280MB model).
|
|
276
|
+
--graph-boost <n> Boost weight for graph PageRank / Obsidian wikilinks (search, default: 0).
|
|
277
|
+
--filter <expr> Rich boolean filter across frontmatter tags and properties (search).
|
|
278
|
+
--ann Approximate nearest neighbor search via IVF (search).
|
|
279
|
+
--nprobe <n> Number of centroids to probe for ANN (search, default: 8).
|
|
146
280
|
--port <n> HTTP port for serve (default: ${DEFAULT_PORT}).
|
|
147
281
|
--host <ip> Bind address for serve (default: ${DEFAULT_HOST} — loopback
|
|
148
282
|
only; use 0.0.0.0 to expose on the LAN, env MDSS_HOST).
|
|
@@ -164,11 +298,41 @@ Examples:
|
|
|
164
298
|
curl -X POST localhost:8747/search -d '{"query":"rotate api token","k":5}'
|
|
165
299
|
`;
|
|
166
300
|
|
|
301
|
+
function formatDuration(sec) {
|
|
302
|
+
const s = Math.max(0, Math.floor(sec));
|
|
303
|
+
const h = Math.floor(s / 3600);
|
|
304
|
+
const m = Math.floor((s % 3600) / 60);
|
|
305
|
+
const remainingS = s % 60;
|
|
306
|
+
return `${h}:${String(m).padStart(2, '0')}:${String(remainingS).padStart(2, '0')}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
167
309
|
async function cmdIndex(opts) {
|
|
168
310
|
const db = resolveDb(opts);
|
|
169
311
|
const indexDir = resolveIndexDir(opts, db);
|
|
170
312
|
const cacheDir = resolveCache(opts);
|
|
171
313
|
const t0 = Date.now();
|
|
314
|
+
let lastProgressUpdate = 0;
|
|
315
|
+
|
|
316
|
+
const onProgress = (done, total, chunksPerSec) => {
|
|
317
|
+
if (!process.stderr.isTTY || opts.json || total <= 100) return;
|
|
318
|
+
const now = Date.now();
|
|
319
|
+
if (now - lastProgressUpdate < 500 && done < total) return;
|
|
320
|
+
lastProgressUpdate = now;
|
|
321
|
+
const percent = Math.floor((done / total) * 100);
|
|
322
|
+
const barWidth = 10;
|
|
323
|
+
const filled = Math.min(barWidth, Math.max(0, Math.round((done / total) * barWidth)));
|
|
324
|
+
const bar = '[' + '#'.repeat(filled) + '-'.repeat(barWidth - filled) + ']';
|
|
325
|
+
const remaining = total - done;
|
|
326
|
+
const etaSec = chunksPerSec > 0 ? remaining / chunksPerSec : 0;
|
|
327
|
+
const etaStr = formatDuration(etaSec);
|
|
328
|
+
const speedStr = `${chunksPerSec.toFixed(1)} chunks/s`;
|
|
329
|
+
const line = `${bar} ${percent}% │ ${done}/${total} chunks │ ${speedStr} │ ETA ${etaStr}`;
|
|
330
|
+
process.stderr.write(`\r${line}`);
|
|
331
|
+
if (done >= total) {
|
|
332
|
+
process.stderr.write('\n');
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
172
336
|
let r;
|
|
173
337
|
try {
|
|
174
338
|
r = await buildIndex({
|
|
@@ -176,7 +340,14 @@ async function cmdIndex(opts) {
|
|
|
176
340
|
modelName: opts.model || DEFAULT_MODEL,
|
|
177
341
|
ignore: opts.ignore,
|
|
178
342
|
offline: resolveOffline(opts),
|
|
179
|
-
|
|
343
|
+
workers: opts.workers,
|
|
344
|
+
ann: opts.ann,
|
|
345
|
+
quantize: opts.quantize,
|
|
346
|
+
log: s => {
|
|
347
|
+
if (process.stderr.isTTY && !opts.json && /^\s+\d+\/\d+$/.test(s)) return;
|
|
348
|
+
process.stderr.write(s + '\n');
|
|
349
|
+
},
|
|
350
|
+
onProgress,
|
|
180
351
|
});
|
|
181
352
|
} catch (e) {
|
|
182
353
|
// A held index lock (issue #37) is an EXPECTED operational state (a second
|
|
@@ -234,7 +405,7 @@ function cmdStats(opts) {
|
|
|
234
405
|
// File count = keys of .hashes.json (per-file md5 map written by buildIndex).
|
|
235
406
|
// Fall back to unique chunk file paths when the hashes file is missing (a
|
|
236
407
|
// legacy/corrupt index) rather than dying.
|
|
237
|
-
let files
|
|
408
|
+
let files;
|
|
238
409
|
try {
|
|
239
410
|
const hashes = JSON.parse(fs.readFileSync(hashesPath, 'utf8'));
|
|
240
411
|
files = Object.keys(hashes).length;
|
|
@@ -288,6 +459,119 @@ function cmdStats(opts) {
|
|
|
288
459
|
);
|
|
289
460
|
}
|
|
290
461
|
|
|
462
|
+
/**
|
|
463
|
+
* `mdss export` — export the index as JSONL / CSV / Parquet for downstream pipelines (issue #77).
|
|
464
|
+
*/
|
|
465
|
+
async function cmdExport(opts) {
|
|
466
|
+
const db = resolveDb(opts);
|
|
467
|
+
const indexDir = resolveIndexDir(opts, db);
|
|
468
|
+
const vectorsPath = path.join(indexDir, 'vectors.json');
|
|
469
|
+
if (!fs.existsSync(vectorsPath)) {
|
|
470
|
+
die(`No index at ${vectorsPath}. Run \`mdss index\` first.`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let index;
|
|
474
|
+
try {
|
|
475
|
+
index = JSON.parse(fs.readFileSync(vectorsPath, 'utf8'));
|
|
476
|
+
} catch (e) {
|
|
477
|
+
die(`${vectorsPath} is not valid JSON (${e.message}); run \`mdss index\` to rebuild.`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const format = (opts.format || 'jsonl').toLowerCase();
|
|
481
|
+
const noVectors = !!opts.noVectors;
|
|
482
|
+
const outStream = opts.output ? fs.createWriteStream(path.resolve(opts.output), 'utf8') : process.stdout;
|
|
483
|
+
|
|
484
|
+
const chunks = Array.isArray(index.chunks) ? index.chunks : [];
|
|
485
|
+
const dim = index.dim;
|
|
486
|
+
|
|
487
|
+
if (format === 'jsonl') {
|
|
488
|
+
for (const c of chunks) {
|
|
489
|
+
let vec = undefined;
|
|
490
|
+
if (!noVectors && c.vec !== undefined) {
|
|
491
|
+
if (typeof c.vec === 'string') {
|
|
492
|
+
try {
|
|
493
|
+
vec = Array.from(decodeVec(c.vec, dim));
|
|
494
|
+
} catch {
|
|
495
|
+
vec = null;
|
|
496
|
+
}
|
|
497
|
+
} else if (Array.isArray(c.vec) || ArrayBuffer.isView(c.vec)) {
|
|
498
|
+
vec = Array.from(c.vec);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const record = {
|
|
502
|
+
file: c.file,
|
|
503
|
+
title: c.title,
|
|
504
|
+
heading: c.heading,
|
|
505
|
+
headingPath: c.headingPath,
|
|
506
|
+
text: c.text,
|
|
507
|
+
...(noVectors ? {} : { vec }),
|
|
508
|
+
...(c.chunkHash ? { chunkHash: c.chunkHash } : {}),
|
|
509
|
+
...(c.startLine !== undefined ? { startLine: c.startLine } : {}),
|
|
510
|
+
...(c.endLine !== undefined ? { endLine: c.endLine } : {}),
|
|
511
|
+
...(c.meta ? { meta: c.meta } : {}),
|
|
512
|
+
};
|
|
513
|
+
outStream.write(JSON.stringify(record) + '\n');
|
|
514
|
+
}
|
|
515
|
+
if (opts.output) {
|
|
516
|
+
await new Promise((resolve) => outStream.end(resolve));
|
|
517
|
+
}
|
|
518
|
+
} else if (format === 'csv') {
|
|
519
|
+
const escapeCsv = (val) => {
|
|
520
|
+
const str = String(val ?? '');
|
|
521
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
|
|
522
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
523
|
+
}
|
|
524
|
+
return str;
|
|
525
|
+
};
|
|
526
|
+
outStream.write('file,title,heading,headingPath,text\n');
|
|
527
|
+
for (const c of chunks) {
|
|
528
|
+
const pathStr = Array.isArray(c.headingPath) ? c.headingPath.join(' > ') : (c.heading || '');
|
|
529
|
+
const row = [
|
|
530
|
+
escapeCsv(c.file),
|
|
531
|
+
escapeCsv(c.title),
|
|
532
|
+
escapeCsv(c.heading),
|
|
533
|
+
escapeCsv(pathStr),
|
|
534
|
+
escapeCsv(c.text),
|
|
535
|
+
].join(',');
|
|
536
|
+
outStream.write(row + '\n');
|
|
537
|
+
}
|
|
538
|
+
if (opts.output) {
|
|
539
|
+
await new Promise((resolve) => outStream.end(resolve));
|
|
540
|
+
}
|
|
541
|
+
} else if (format === 'parquet') {
|
|
542
|
+
let parquet;
|
|
543
|
+
try {
|
|
544
|
+
// @ts-ignore
|
|
545
|
+
parquet = await import('parquetjs-lite');
|
|
546
|
+
} catch {
|
|
547
|
+
die('Parquet export requires optional dependency "parquetjs-lite". Install with: npm install parquetjs-lite');
|
|
548
|
+
}
|
|
549
|
+
if (!opts.output) {
|
|
550
|
+
die('Parquet export requires an output file via --output <file.parquet>');
|
|
551
|
+
}
|
|
552
|
+
const schemaFields = {
|
|
553
|
+
file: { type: 'UTF8' },
|
|
554
|
+
title: { type: 'UTF8' },
|
|
555
|
+
heading: { type: 'UTF8' },
|
|
556
|
+
text: { type: 'UTF8' },
|
|
557
|
+
};
|
|
558
|
+
const schema = new parquet.ParquetSchema(schemaFields);
|
|
559
|
+
const outFile = path.resolve(opts.output);
|
|
560
|
+
const writer = await parquet.ParquetWriter.openFile(schema, outFile);
|
|
561
|
+
for (const c of chunks) {
|
|
562
|
+
await writer.appendRow({
|
|
563
|
+
file: c.file || '',
|
|
564
|
+
title: c.title || '',
|
|
565
|
+
heading: c.heading || '',
|
|
566
|
+
text: c.text || '',
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
await writer.close();
|
|
570
|
+
} else {
|
|
571
|
+
die(`unknown export format: "${format}". Supported formats: jsonl, csv, parquet.`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
291
575
|
/**
|
|
292
576
|
* Offline diagnostics for the index/db/model-cache trio (issue #43) — the
|
|
293
577
|
* `mdss check` / `mdss doctor` backend. Pure read-only: parses vectors.json +
|
|
@@ -295,7 +579,7 @@ function cmdStats(opts) {
|
|
|
295
579
|
* validator the loader uses), walks the db for staleness, and checks the
|
|
296
580
|
* transformers.js cache layout for the index's model. NEVER loads the
|
|
297
581
|
* embedding model and NEVER touches the network.
|
|
298
|
-
* @param {{db:string, indexDir:string, cacheDir:string, requireOffline?:boolean}} paths
|
|
582
|
+
* @param {{db:string, indexDir:string, cacheDir:string, requireOffline?:boolean, config?:any}} paths
|
|
299
583
|
* @returns {CheckReport}
|
|
300
584
|
*/
|
|
301
585
|
|
|
@@ -312,7 +596,7 @@ function cmdStats(opts) {
|
|
|
312
596
|
* @property {{exists:boolean, stale:boolean, error:(string|null)}} db
|
|
313
597
|
* @property {{id:(string|null), cached:boolean, cachePath:(string|null), error:(string|null)}} model
|
|
314
598
|
*/
|
|
315
|
-
|
|
599
|
+
function checkHealth({ db, indexDir, cacheDir, requireOffline = false, config = null }) {
|
|
316
600
|
const report = {
|
|
317
601
|
healthy: true,
|
|
318
602
|
index: { exists: false, parses: false, schemaVersion: null, format: null, recognized: true, error: null },
|
|
@@ -320,7 +604,21 @@ export function checkHealth({ db, indexDir, cacheDir, requireOffline = false })
|
|
|
320
604
|
chunks: { total: 0, valid: 0, invalid: [] },
|
|
321
605
|
db: { exists: true, stale: false, error: null },
|
|
322
606
|
model: { id: null, cached: false, cachePath: null, error: null },
|
|
607
|
+
config: null,
|
|
323
608
|
};
|
|
609
|
+
|
|
610
|
+
// --- config file: exists, valid JSON, schema / unknown keys (issue #68) ---
|
|
611
|
+
if (config && config.path) {
|
|
612
|
+
report.config = {
|
|
613
|
+
path: config.path,
|
|
614
|
+
valid: !config.error && (!config.unknownKeys || config.unknownKeys.length === 0),
|
|
615
|
+
unknownKeys: config.unknownKeys || [],
|
|
616
|
+
error: config.error || (config.unknownKeys && config.unknownKeys.length > 0 ? `unknown config keys: ${config.unknownKeys.join(', ')}` : null),
|
|
617
|
+
};
|
|
618
|
+
if (!report.config.valid) {
|
|
619
|
+
report.healthy = false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
324
622
|
const vectorsPath = path.join(indexDir, 'vectors.json');
|
|
325
623
|
const hashesPath = path.join(indexDir, '.hashes.json');
|
|
326
624
|
|
|
@@ -332,8 +630,22 @@ export function checkHealth({ db, indexDir, cacheDir, requireOffline = false })
|
|
|
332
630
|
}
|
|
333
631
|
report.index.exists = true;
|
|
334
632
|
let index;
|
|
633
|
+
const raw = fs.readFileSync(vectorsPath, 'utf8');
|
|
634
|
+
const shaPath = path.join(indexDir, 'vectors.json.sha256');
|
|
635
|
+
if (fs.existsSync(shaPath)) {
|
|
636
|
+
try {
|
|
637
|
+
const shaContent = fs.readFileSync(shaPath, 'utf8').trim();
|
|
638
|
+
const expectedHash = shaContent.split(/\s+/)[0];
|
|
639
|
+
const actualHash = crypto.createHash('sha256').update(raw).digest('hex');
|
|
640
|
+
if (expectedHash && actualHash !== expectedHash) {
|
|
641
|
+
report.index.error = `vectors.json integrity check failed (SHA-256 mismatch) — run \`mdss index\` to rebuild`;
|
|
642
|
+
report.healthy = false;
|
|
643
|
+
return report;
|
|
644
|
+
}
|
|
645
|
+
} catch { /* ignore unreadable sha file */ }
|
|
646
|
+
}
|
|
335
647
|
try {
|
|
336
|
-
index = JSON.parse(
|
|
648
|
+
index = JSON.parse(raw);
|
|
337
649
|
report.index.parses = true;
|
|
338
650
|
} catch (e) {
|
|
339
651
|
report.index.error = `${vectorsPath} is not valid JSON (${e.message}); run \`mdss index\` to rebuild`;
|
|
@@ -469,7 +781,11 @@ function cmdCheck(opts) {
|
|
|
469
781
|
const db = resolveDb(opts);
|
|
470
782
|
const indexDir = resolveIndexDir(opts, db);
|
|
471
783
|
const cacheDir = resolveCache(opts);
|
|
472
|
-
const report = checkHealth({
|
|
784
|
+
const report = checkHealth({
|
|
785
|
+
db, indexDir, cacheDir,
|
|
786
|
+
requireOffline: resolveOffline(opts),
|
|
787
|
+
config: opts._config,
|
|
788
|
+
});
|
|
473
789
|
|
|
474
790
|
if (opts.json) {
|
|
475
791
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
@@ -543,6 +859,14 @@ function cmdCheck(opts) {
|
|
|
543
859
|
out += warn('model: index has no model field (legacy index)');
|
|
544
860
|
}
|
|
545
861
|
|
|
862
|
+
if (report.config) {
|
|
863
|
+
if (report.config.valid) {
|
|
864
|
+
out += ok(`config: ${report.config.path}`);
|
|
865
|
+
} else {
|
|
866
|
+
out += fail(`config: ${report.config.path} (${report.config.error})`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
546
870
|
out += report.healthy
|
|
547
871
|
? 'check: healthy (exit 0)\n'
|
|
548
872
|
: 'check: 1+ problem(s) found (exit 1)\n';
|
|
@@ -550,29 +874,116 @@ function cmdCheck(opts) {
|
|
|
550
874
|
process.exitCode = report.healthy ? 0 : 1;
|
|
551
875
|
}
|
|
552
876
|
|
|
553
|
-
async function
|
|
877
|
+
async function cmdTui(opts) {
|
|
554
878
|
const db = resolveDb(opts);
|
|
555
879
|
const indexDir = resolveIndexDir(opts, db);
|
|
556
880
|
const cacheDir = resolveCache(opts);
|
|
557
881
|
const query = opts._.join(' ').trim();
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
k: opts.k || 6,
|
|
882
|
+
const { runTui } = await import('../dist/tui.js');
|
|
883
|
+
await runTui({
|
|
884
|
+
db, indexDir, cacheDir, query,
|
|
885
|
+
k: opts.k || 20,
|
|
563
886
|
semanticOnly: !!opts.semantic,
|
|
564
887
|
offline: resolveOffline(opts),
|
|
565
888
|
path: opts.path.length > 0 ? opts.path : undefined,
|
|
566
|
-
since: opts.since,
|
|
567
889
|
rerank: !!opts.rerank,
|
|
568
|
-
|
|
569
|
-
project: opts.project,
|
|
570
|
-
type: opts.type,
|
|
571
|
-
status: opts.status,
|
|
572
|
-
canonicalOnly: opts.canonical,
|
|
573
|
-
explain: !!opts.explain,
|
|
574
|
-
maxPerFile: opts.maxPerFile || opts.maxPerDoc,
|
|
890
|
+
rag: !!opts.rag,
|
|
575
891
|
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function cmdAsk(opts) {
|
|
895
|
+
const query = opts._.join(' ').trim();
|
|
896
|
+
if (!query) die('missing question to ask. Usage: mdss ask [--db <dir>] "question"');
|
|
897
|
+
|
|
898
|
+
const db = resolveDb(opts);
|
|
899
|
+
const indexDir = resolveIndexDir(opts, db);
|
|
900
|
+
const cacheDir = resolveCache(opts);
|
|
901
|
+
const k = opts.k || 5;
|
|
902
|
+
|
|
903
|
+
const { askQuestion } = await import('../dist/rag.js');
|
|
904
|
+
const result = await askQuestion({
|
|
905
|
+
query,
|
|
906
|
+
indexDir,
|
|
907
|
+
cacheDir,
|
|
908
|
+
k,
|
|
909
|
+
llmEndpoint: opts.llmEndpoint,
|
|
910
|
+
llmModel: opts.llmModel,
|
|
911
|
+
systemPrompt: opts.systemPrompt,
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
if (opts.json) {
|
|
915
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
process.stdout.write(`\n${result.answer}\n\n`);
|
|
920
|
+
if (result.citations && result.citations.length > 0) {
|
|
921
|
+
process.stdout.write('Sources:\n');
|
|
922
|
+
for (const c of result.citations) {
|
|
923
|
+
const heading = c.heading ? ` › ${c.heading}` : '';
|
|
924
|
+
process.stdout.write(` - ${c.file}${heading} [score: ${c.score.toFixed(3)}]\n`);
|
|
925
|
+
}
|
|
926
|
+
process.stdout.write('\n');
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async function cmdSearch(opts) {
|
|
931
|
+
if (opts.interactive) return cmdTui(opts);
|
|
932
|
+
const cacheDir = resolveCache(opts);
|
|
933
|
+
const query = opts._.join(' ').trim();
|
|
934
|
+
if (!query) die('Missing query text. e.g. mdss search --db ./docs "your question"');
|
|
935
|
+
|
|
936
|
+
let results;
|
|
937
|
+
const isFederated = Array.isArray(opts.vaults) && opts.vaults.length > 0;
|
|
938
|
+
|
|
939
|
+
if (isFederated) {
|
|
940
|
+
results = await searchFederated({
|
|
941
|
+
vaults: opts.vaults,
|
|
942
|
+
cacheDir,
|
|
943
|
+
query,
|
|
944
|
+
k: opts.k || 6,
|
|
945
|
+
semanticOnly: !!opts.semantic,
|
|
946
|
+
offline: resolveOffline(opts),
|
|
947
|
+
path: opts.path.length > 0 ? opts.path : undefined,
|
|
948
|
+
since: opts.since,
|
|
949
|
+
rerank: !!opts.rerank,
|
|
950
|
+
tag: opts.tag,
|
|
951
|
+
project: opts.project,
|
|
952
|
+
type: opts.type,
|
|
953
|
+
status: opts.status,
|
|
954
|
+
canonicalOnly: opts.canonical,
|
|
955
|
+
explain: !!opts.explain,
|
|
956
|
+
maxPerFile: opts.maxPerFile || opts.maxPerDoc,
|
|
957
|
+
ann: opts.ann,
|
|
958
|
+
nprobe: opts.nprobe,
|
|
959
|
+
graphBoost: opts.graphBoost,
|
|
960
|
+
filter: opts.filter,
|
|
961
|
+
});
|
|
962
|
+
} else {
|
|
963
|
+
const db = resolveDb(opts);
|
|
964
|
+
const indexDir = resolveIndexDir(opts, db);
|
|
965
|
+
|
|
966
|
+
results = await search({
|
|
967
|
+
indexDir, cacheDir, query,
|
|
968
|
+
k: opts.k || 6,
|
|
969
|
+
semanticOnly: !!opts.semantic,
|
|
970
|
+
offline: resolveOffline(opts),
|
|
971
|
+
path: opts.path.length > 0 ? opts.path : undefined,
|
|
972
|
+
since: opts.since,
|
|
973
|
+
rerank: !!opts.rerank,
|
|
974
|
+
tag: opts.tag,
|
|
975
|
+
project: opts.project,
|
|
976
|
+
type: opts.type,
|
|
977
|
+
status: opts.status,
|
|
978
|
+
canonicalOnly: opts.canonical,
|
|
979
|
+
explain: !!opts.explain,
|
|
980
|
+
maxPerFile: opts.maxPerFile || opts.maxPerDoc,
|
|
981
|
+
ann: opts.ann,
|
|
982
|
+
nprobe: opts.nprobe,
|
|
983
|
+
graphBoost: opts.graphBoost,
|
|
984
|
+
filter: opts.filter,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
576
987
|
|
|
577
988
|
if (opts.json) { process.stdout.write(JSON.stringify(results, null, 2) + '\n'); return; }
|
|
578
989
|
if (results.length === 0) { process.stdout.write('No matches.\n'); return; }
|
|
@@ -582,13 +993,14 @@ async function cmdSearch(opts) {
|
|
|
582
993
|
// Group by file (issue #13): each file becomes a header with its hits below.
|
|
583
994
|
const byFile = new Map();
|
|
584
995
|
for (const r of results) {
|
|
585
|
-
|
|
586
|
-
byFile.
|
|
996
|
+
const fileKey = r.vault ? `[${r.vault}] ${r.file}` : r.file;
|
|
997
|
+
if (!byFile.has(fileKey)) byFile.set(fileKey, []);
|
|
998
|
+
byFile.get(fileKey).push(r);
|
|
587
999
|
}
|
|
588
1000
|
const color = !!process.stdout.isTTY; // bold matched terms only on a real terminal
|
|
589
1001
|
let rank = 0;
|
|
590
|
-
for (const [
|
|
591
|
-
process.stdout.write(`${
|
|
1002
|
+
for (const [fileHeader, hits] of byFile) {
|
|
1003
|
+
process.stdout.write(`${fileHeader}\n`);
|
|
592
1004
|
for (const r of hits) {
|
|
593
1005
|
rank++;
|
|
594
1006
|
let snippet = r.snippet;
|
|
@@ -648,6 +1060,8 @@ async function cmdServe(opts) {
|
|
|
648
1060
|
watch: !!opts.watch,
|
|
649
1061
|
watchInterval: opts.watchInterval,
|
|
650
1062
|
watchDelay: opts.watchDelay,
|
|
1063
|
+
apiKey: opts.apiKey || process.env.MDSS_API_KEY,
|
|
1064
|
+
healthPublic: !!opts.healthPublic || process.env.MDSS_HEALTH_PUBLIC === 'true',
|
|
651
1065
|
log,
|
|
652
1066
|
});
|
|
653
1067
|
|
|
@@ -671,9 +1085,37 @@ async function cmdServe(opts) {
|
|
|
671
1085
|
process.on('SIGTERM', () => stop('SIGTERM'));
|
|
672
1086
|
}
|
|
673
1087
|
|
|
1088
|
+
async function cmdMcp(opts) {
|
|
1089
|
+
if (opts.listTools) {
|
|
1090
|
+
const { MCP_TOOLS } = await import('../dist/mcp.js');
|
|
1091
|
+
process.stdout.write(JSON.stringify(MCP_TOOLS, null, 2) + '\n');
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
const db = resolveDb(opts);
|
|
1095
|
+
const indexDir = resolveIndexDir(opts, db);
|
|
1096
|
+
const cacheDir = resolveCache(opts);
|
|
1097
|
+
const log = s => process.stderr.write(s + '\n');
|
|
1098
|
+
const { startMcpServer } = await import('../dist/mcp.js');
|
|
1099
|
+
await startMcpServer({
|
|
1100
|
+
db, indexDir, cacheDir,
|
|
1101
|
+
modelName: opts.model || DEFAULT_MODEL,
|
|
1102
|
+
ignore: opts.ignore,
|
|
1103
|
+
offline: resolveOffline(opts),
|
|
1104
|
+
log,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
674
1108
|
async function main() {
|
|
675
1109
|
const argv = process.argv.slice(2);
|
|
676
1110
|
const opts = parseArgs(argv);
|
|
1111
|
+
const configPath = findConfigFile(opts.config);
|
|
1112
|
+
const loadedConfig = loadConfigFile(configPath);
|
|
1113
|
+
if (loadedConfig.error && opts.config) {
|
|
1114
|
+
die(loadedConfig.error);
|
|
1115
|
+
}
|
|
1116
|
+
opts._config = loadedConfig;
|
|
1117
|
+
applyConfigDefaults(opts, loadedConfig.config);
|
|
1118
|
+
|
|
677
1119
|
const cmd = opts._.shift();
|
|
678
1120
|
|
|
679
1121
|
if (opts.version) { process.stdout.write(`${VERSION}\n`); return; }
|
|
@@ -683,8 +1125,12 @@ async function main() {
|
|
|
683
1125
|
case 'stats': return cmdStats(opts);
|
|
684
1126
|
case 'check':
|
|
685
1127
|
case 'doctor': return cmdCheck(opts);
|
|
1128
|
+
case 'export': return cmdExport(opts);
|
|
686
1129
|
case 'search': return cmdSearch(opts);
|
|
1130
|
+
case 'ask': return cmdAsk(opts);
|
|
1131
|
+
case 'tui': return cmdTui(opts);
|
|
687
1132
|
case 'serve': return cmdServe(opts);
|
|
1133
|
+
case 'mcp': return cmdMcp(opts);
|
|
688
1134
|
case 'models': return cmdModels();
|
|
689
1135
|
default: die(`unknown command: ${cmd}. Try \`mdss --help\`.`);
|
|
690
1136
|
}
|
|
@@ -700,7 +1146,9 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
700
1146
|
// functions are unit-tested directly; die() is asserted via a mocked
|
|
701
1147
|
// process.exit so a bad flag cannot kill the test runner.
|
|
702
1148
|
export {
|
|
703
|
-
parseArgs, nextInt, nextValue,
|
|
1149
|
+
parseArgs, nextInt, nextFloat, nextValue,
|
|
704
1150
|
resolveDb, resolveIndexDir, resolveCache, resolveOffline,
|
|
1151
|
+
findConfigFile, loadConfigFile, applyConfigDefaults, KNOWN_CONFIG_KEYS,
|
|
1152
|
+
checkHealth, cmdExport,
|
|
705
1153
|
die, main, HELP, VERSION,
|
|
706
1154
|
};
|