md-semantic-search 1.0.2 → 1.0.3
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 +154 -6
- package/bin/cli.mjs +403 -22
- package/dist/completions.d.ts +25 -0
- package/dist/completions.js +233 -0
- package/dist/core.d.ts +5 -0
- package/dist/core.js +17 -0
- package/dist/download-progress.d.ts +52 -0
- package/dist/download-progress.js +112 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/mcp.js +6 -1
- package/dist/open.d.ts +43 -0
- package/dist/open.js +84 -0
- package/dist/providers.d.ts +21 -0
- package/dist/providers.js +104 -0
- package/dist/query-cache-disk.d.ts +14 -0
- package/dist/query-cache-disk.js +175 -0
- package/dist/repair.d.ts +36 -0
- package/dist/repair.js +208 -0
- package/dist/rerank.js +3 -2
- package/dist/search.d.ts +34 -0
- package/dist/search.js +172 -5
- package/dist/serve.d.ts +126 -0
- package/dist/serve.js +579 -45
- package/dist/watcher.d.ts +16 -0
- package/dist/watcher.js +41 -0
- package/dist/webui.d.ts +13 -0
- package/dist/webui.js +231 -0
- package/package.json +1 -1
- package/src/completions.ts +250 -0
- package/src/core.ts +26 -0
- package/src/download-progress.ts +146 -0
- package/src/index.ts +4 -0
- package/src/mcp.ts +10 -3
- package/src/open.ts +119 -0
- package/src/providers.ts +134 -0
- package/src/query-cache-disk.ts +191 -0
- package/src/repair.ts +245 -0
- package/src/rerank.ts +3 -2
- package/src/search.ts +192 -6
- package/src/serve.ts +635 -42
- package/src/watcher.ts +46 -0
- package/src/webui.ts +234 -0
package/dist/watcher.d.ts
CHANGED
|
@@ -8,4 +8,20 @@ export interface FileWatcher {
|
|
|
8
8
|
close: () => void;
|
|
9
9
|
isNative: boolean;
|
|
10
10
|
}
|
|
11
|
+
export type FsErrorClass = 'transient' | 'vanished' | 'permanent';
|
|
12
|
+
/** Bucket a filesystem failure so callers can retry, skip, or warn. */
|
|
13
|
+
export declare function classifyFsError(err: unknown): FsErrorClass;
|
|
14
|
+
/** Retry `fn` while its failures classify as transient; capped exponential backoff. */
|
|
15
|
+
export declare function readWithRetry<T>(fn: () => T, opts?: {
|
|
16
|
+
attempts?: number;
|
|
17
|
+
baseDelayMs?: number;
|
|
18
|
+
onRetry?: (attempt: number, err: unknown) => void;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
ok: true;
|
|
21
|
+
value: T;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
err: unknown;
|
|
25
|
+
cls: FsErrorClass;
|
|
26
|
+
}>;
|
|
11
27
|
export declare function createFileWatcher(dbPath: string, onChange: () => void | Promise<void>, options?: WatcherOptions): FileWatcher;
|
package/dist/watcher.js
CHANGED
|
@@ -7,6 +7,47 @@
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { globToRegExp } from './core.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// FS error classification + bounded retry (issue #116): Windows AV / OneDrive
|
|
12
|
+
// / atomic-save editors produce EBUSY/EPERM bursts that previously became
|
|
13
|
+
// silent 'unreadable' fingerprints and missed updates.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const TRANSIENT_CODES = new Set(['EBUSY', 'EPERM', 'EACCES', 'EMFILE', 'ENFILE', 'EAGAIN']);
|
|
16
|
+
/** Bucket a filesystem failure so callers can retry, skip, or warn. */
|
|
17
|
+
export function classifyFsError(err) {
|
|
18
|
+
const e = err;
|
|
19
|
+
if (!e)
|
|
20
|
+
return 'permanent';
|
|
21
|
+
if (e.code === 'ENOENT')
|
|
22
|
+
return 'vanished';
|
|
23
|
+
if (e.code && TRANSIENT_CODES.has(e.code))
|
|
24
|
+
return 'transient';
|
|
25
|
+
if (/temporarily unavailable|being written|locked by another process/i.test(String(e.message || ''))) {
|
|
26
|
+
return 'transient';
|
|
27
|
+
}
|
|
28
|
+
return 'permanent';
|
|
29
|
+
}
|
|
30
|
+
/** Retry `fn` while its failures classify as transient; capped exponential backoff. */
|
|
31
|
+
export async function readWithRetry(fn, opts = {}) {
|
|
32
|
+
const attempts = opts.attempts ?? 3;
|
|
33
|
+
const baseDelayMs = opts.baseDelayMs ?? 200;
|
|
34
|
+
let lastErr;
|
|
35
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
36
|
+
try {
|
|
37
|
+
return { ok: true, value: fn() };
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
lastErr = err;
|
|
41
|
+
const cls = classifyFsError(err);
|
|
42
|
+
if (cls !== 'transient' || attempt === attempts) {
|
|
43
|
+
return { ok: false, err, cls };
|
|
44
|
+
}
|
|
45
|
+
opts.onRetry?.(attempt, err);
|
|
46
|
+
await new Promise((r) => setTimeout(r, baseDelayMs * attempt)); // 200ms, 400ms — capped
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { ok: false, err: lastErr, cls: classifyFsError(lastErr) };
|
|
50
|
+
}
|
|
10
51
|
export function createFileWatcher(dbPath, onChange, options = {}) {
|
|
11
52
|
const { debounceMs = 250, ignore = [], log = () => { }, fallbackIntervalMs = 50, } = options;
|
|
12
53
|
let closed = false;
|
package/dist/webui.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in web UI assets for `mdss serve` (issue #111).
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies, zero build step: plain HTML + CSS + JS shipped as
|
|
5
|
+
* compiled-in strings (`files` already includes src/ -> dist/). Split into
|
|
6
|
+
* THREE assets so the CSP can stay strict (no inline script/style):
|
|
7
|
+
* GET / -> WEBUI_HTML (shell, references ./ui.js + ./ui.css)
|
|
8
|
+
* GET /ui.js -> WEBUI_JS (all behaviour; addEventListener only, no eval)
|
|
9
|
+
* GET /ui.css-> WEBUI_CSS (dark/light via prefers-color-scheme + override)
|
|
10
|
+
*/
|
|
11
|
+
export declare const WEBUI_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>mdss \u2014 semantic search</title>\n<link rel=\"stylesheet\" href=\"./ui.css\">\n<script src=\"./ui.js\" defer></script>\n</head>\n<body>\n<header>\n <div class=\"brand\">mdss<span id=\"health\" title=\"loading\u2026\"></span></div>\n <button id=\"theme\" type=\"button\" aria-label=\"toggle theme\">\u25D0</button>\n</header>\n<main>\n <div id=\"authbar\" hidden>\n <input id=\"key\" type=\"password\" autocomplete=\"off\" placeholder=\"API key (Bearer)\">\n <button id=\"keysafe\" type=\"button\">Save</button>\n <span id=\"authmsg\"></span>\n </div>\n <searchbox>\n <input id=\"q\" type=\"search\" autofocus\n placeholder='Search by meaning \u2014 press \"/\" to focus' autocomplete=\"off\">\n </searchbox>\n <filters>\n <input id=\"f-tag\" placeholder=\"tag\">\n <input id=\"f-type\" placeholder=\"type\">\n <input id=\"f-status\" placeholder=\"status\">\n <input id=\"f-limit\" type=\"number\" min=\"1\" max=\"50\" placeholder=\"max/file\">\n </filters>\n <div id=\"meta\"></div>\n <ol id=\"results\"></ol>\n</main>\n<footer><code>POST /search</code> \u00B7 <a href=\"/help\">api</a> \u00B7 mdss web ui</footer>\n</body>\n</html>";
|
|
12
|
+
export declare const WEBUI_CSS = "/* mdss web ui (issue #111) \u2014 dark/light via prefers-color-scheme */\n:root { color-scheme: light dark;\n --bg:#fbfbfd; --fg:#1c1e21; --mut:#6b7280; --acc:#2563eb; --line:#e5e7eb;\n --card:#ffffff; --mark:#fef08a; --bar:#dbeafe;\n}\n@media (prefers-color-scheme: dark) { :root {\n --bg:#0f1115; --fg:#e6e6e6; --mut:#9aa0aa; --acc:#60a5fa; --line:#262b33;\n --card:#161a21; --mark:#854d0e; --bar:#1e3a8a;\n}}\n:root[data-theme=\"dark\"] { --bg:#0f1115; --fg:#e6e6e6; --mut:#9aa0aa; --acc:#60a5fa; --line:#262b33; --card:#161a21; --mark:#854d0e; --bar:#1e3a8a }\n:root[data-theme=\"light\"] { --bg:#fbfbfd; --fg:#1c1e21; --mut:#6b7280; --acc:#2563eb; --line:#e5e7eb; --card:#fff; --mark:#fef08a; --bar:#dbeafe }\n\n* { box-sizing:border-box }\nbody { margin:0 auto; max-width:56rem; padding:0 1rem 3rem; background:var(--bg); color:var(--fg);\n font:15px/1.55 system-ui,-apple-system,\"Segoe UI\",sans-serif }\nheader { display:flex; justify-content:space-between; align-items:center; padding:.9rem 0 }\n.brand { font-weight:700; letter-spacing:.02em }\n.brand span { margin-left:.6rem; font-weight:400; font-size:.8rem; color:var(--mut) }\n#theme { background:none; border:1px solid var(--line); border-radius:.5rem; color:var(--fg);\n cursor:pointer; font-size:1rem; line-height:1; padding:.35rem .6rem }\n#q { width:100%; font-size:1.05rem; padding:.75rem 1rem; border:1px solid var(--line);\n border-radius:.7rem; background:var(--card); color:var(--fg) }\n#q:focus { outline:2px solid var(--acc); outline-offset:1px }\nfilters { display:flex; gap:.5rem; margin-top:.6rem; flex-wrap:wrap }\nfilters input { flex:1 1 7rem; min-width:0; padding:.4rem .6rem; border:1px solid var(--line);\n border-radius:.5rem; background:var(--card); color:var(--fg); font-size:.85rem }\n#meta { color:var(--mut); font-size:.85rem; margin:.7rem 0 .2rem }\n#results { list-style:none; margin:0; padding:0 }\n#results li { border:1px solid var(--line); background:var(--card); border-radius:.7rem;\n padding:.7rem .9rem; margin-top:.6rem; cursor:pointer }\n#results li.active { outline:2px solid var(--acc) }\n.hit-head { display:flex; gap:.6rem; align-items:baseline }\n.hit-title { font-weight:600 }\n.hit-file { color:var(--acc); font-size:.82rem; white-space:nowrap }\n.hit-path { color:var(--mut); font-size:.82rem }\nmark { background:var(--mark); color:inherit; border-radius:.2rem; padding:0 .1em }\n.bar { height:4px; background:var(--bar); border-radius:2px; margin:.45rem 0 .2rem; overflow:hidden }\n.bar i { display:block; height:100%; background:var(--acc) }\n.snippet { overflow:hidden; max-height:3.2em; transition:max-height .15s ease }\nli.open .snippet { max-height:none }\n.score { margin-left:auto; color:var(--mut); font-size:.78rem }\n#authbar { display:flex; gap:.5rem; align-items:center; margin-bottom:.6rem;\n padding:.5rem .7rem; border:1px dashed var(--line); border-radius:.6rem }\n#authbar[hidden] { display:none }\nfooter { margin-top:2rem; color:var(--mut); font-size:.8rem }\n";
|
|
13
|
+
export declare const WEBUI_JS = "/* mdss web ui behaviour (issue #111) \u2014 no frameworks, no eval */\n(function () {\n 'use strict';\n var q = document.getElementById('q');\n var resultsEl = document.getElementById('results');\n var meta = document.getElementById('meta');\n var healthEl = document.getElementById('health');\n var authbar = document.getElementById('authbar');\n var keyInput = document.getElementById('key');\n\n var KEY_STORE = 'mdss_key';\n var key = sessionStorage.getItem(KEY_STORE) || '';\n var activeIdx = -1;\n\n function headers() {\n var h = { 'Content-Type': 'application/json' };\n if (key) h['Authorization'] = 'Bearer ' + key;\n return h;\n }\n function esc(s) {\n return String(s).replace(/[&<>\"']/g, function (c) {\n return { '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[c];\n });\n }\n function highlight(text, terms) {\n var safe = esc(text == null ? '' : text);\n if (!terms || !terms.length) return safe;\n var re = new RegExp('(' + terms.map(esc).join('|') + ')', 'gi');\n return safe.replace(re, '<mark>$1</mark>');\n }\n function askKey(msg) {\n authbar.hidden = false;\n document.getElementById('authmsg').textContent = msg || '';\n keyInput.focus();\n }\n document.getElementById('keysafe').addEventListener('click', function () {\n key = keyInput.value.trim();\n sessionStorage.setItem(KEY_STORE, key);\n authbar.hidden = true;\n run();\n });\n\n function api(path, opts) {\n opts = opts || {};\n opts.headers = headers();\n return fetch(path, opts).then(function (res) {\n if (res.status === 401) { askKey('401 \u2014 this daemon requires an API key'); }\n if (!res.ok) throw new Error('HTTP ' + res.status);\n return res.json();\n });\n }\n\n function render(data, terms) {\n var rs = data.results || [];\n meta.textContent = rs.length ? (data.count + ' hit(s) for \u201C' + data.query + '\u201D') : 'No matches.';\n activeIdx = -1;\n resultsEl.innerHTML = '';\n rs.forEach(function (r, i) {\n var li = document.createElement('li');\n var pct = Math.max(2, Math.min(100, Math.round((r.cosine || 0) * 100)));\n var headPath = (r.headingPath || []).slice(0, -1).join(' \u203A ');\n li.innerHTML =\n '<div class=\"hit-head\"><span class=\"hit-title\">' + highlight(r.title, terms) + '</span>' +\n '<span class=\"score\">' + Number(r.cosine || 0).toFixed(3) + '</span></div>' +\n ((r.heading && r.heading !== r.title) ? '<div class=\"hit-path\">' + highlight(headPath || r.heading, terms) + '</div>' : '') +\n '<div class=\"bar\"><i style=\"width:' + pct + '%\"></i></div>' +\n '<div class=\"snippet\">' + highlight(r.snippet, terms) + '</div>' +\n '<div class=\"hit-file\">' + esc(r.file) + (r.startLine != null ? ':' + r.startLine : '') + '</div>';\n li.addEventListener('click', function () { li.classList.toggle('open'); });\n resultsEl.appendChild(li);\n });\n }\n\n var timer = null;\n function run() {\n var query = q.value.trim();\n if (!query) { resultsEl.innerHTML = ''; meta.textContent = ''; return; }\n var filters = {};\n ['f-tag', 'f-type', 'f-status'].forEach(function (id) {\n var v = document.getElementById(id).value.trim();\n if (v) filters[id.slice(2)] = v; // f-tag -> tag \u2026\n });\n var lim = parseInt(document.getElementById('f-limit').value, 10);\n if (lim > 0) filters.maxPerFile = lim;\n\n api('/search', { method: 'POST', body: JSON.stringify(Object.assign({ query: query, k: 20 }, filters)) })\n .then(function (data) {\n var lastTerms = (data.results || []).length\n ? (data.results[0].matches || []) : [];\n render(data, lastTerms);\n })\n .catch(function (e) { if (String(e.message) !== 'HTTP 401') meta.textContent = 'error: ' + e.message; });\n }\n q.addEventListener('input', function () {\n clearTimeout(timer);\n timer = setTimeout(run, 250);\n });\n document.querySelectorAll('filters input').forEach(function (el) {\n el.addEventListener('input', function () { clearTimeout(timer); timer = setTimeout(run, 350); });\n });\n\n // keyboard-first: \"/\" focuses, arrows navigate, Enter toggles\n document.addEventListener('keydown', function (ev) {\n if (ev.key === '/' && document.activeElement !== q) { ev.preventDefault(); q.focus(); q.select(); return; }\n if (ev.key === 'Escape') { q.blur(); return; }\n var items = resultsEl.children;\n if (!items.length) return;\n if (ev.key === 'ArrowDown') { ev.preventDefault(); setActive(Math.min(items.length - 1, activeIdx + 1)); }\n else if (ev.key === 'ArrowUp') { ev.preventDefault(); setActive(Math.max(0, activeIdx - 1)); }\n else if (ev.key === 'Enter' && activeIdx >= 0) { items[activeIdx].classList.toggle('open'); }\n });\n function setActive(i) {\n var items = resultsEl.children;\n if (activeIdx >= 0 && items[activeIdx]) items[activeIdx].classList.remove('active');\n activeIdx = i;\n if (items[i]) {\n items[i].classList.add('active');\n items[i].scrollIntoView({ block: 'nearest' });\n }\n }\n\n // theme: follow system until user overrides (persisted)\n var root = document.documentElement;\n var saved = null;\n try { saved = localStorage.getItem('mdss_theme'); } catch (e) {}\n if (saved) root.dataset.theme = saved;\n document.getElementById('theme').addEventListener('click', function () {\n var sysDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n var cur = root.dataset.theme || (sysDark ? 'dark' : 'light');\n var next = cur === 'dark' ? 'light' : 'dark';\n root.dataset.theme = next;\n try { localStorage.setItem('mdss_theme', next); } catch (e) {}\n });\n\n api('/health').then(function (h) {\n healthEl.textContent = h.chunks + ' chunks \u00B7 ' + h.model;\n }).catch(function () { healthEl.textContent = ''; });\n})();\n";
|
package/dist/webui.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in web UI assets for `mdss serve` (issue #111).
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies, zero build step: plain HTML + CSS + JS shipped as
|
|
5
|
+
* compiled-in strings (`files` already includes src/ -> dist/). Split into
|
|
6
|
+
* THREE assets so the CSP can stay strict (no inline script/style):
|
|
7
|
+
* GET / -> WEBUI_HTML (shell, references ./ui.js + ./ui.css)
|
|
8
|
+
* GET /ui.js -> WEBUI_JS (all behaviour; addEventListener only, no eval)
|
|
9
|
+
* GET /ui.css-> WEBUI_CSS (dark/light via prefers-color-scheme + override)
|
|
10
|
+
*/
|
|
11
|
+
export const WEBUI_HTML = `<!doctype html>
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<head>
|
|
14
|
+
<meta charset="utf-8">
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
16
|
+
<title>mdss — semantic search</title>
|
|
17
|
+
<link rel="stylesheet" href="./ui.css">
|
|
18
|
+
<script src="./ui.js" defer></script>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<header>
|
|
22
|
+
<div class="brand">mdss<span id="health" title="loading…"></span></div>
|
|
23
|
+
<button id="theme" type="button" aria-label="toggle theme">◐</button>
|
|
24
|
+
</header>
|
|
25
|
+
<main>
|
|
26
|
+
<div id="authbar" hidden>
|
|
27
|
+
<input id="key" type="password" autocomplete="off" placeholder="API key (Bearer)">
|
|
28
|
+
<button id="keysafe" type="button">Save</button>
|
|
29
|
+
<span id="authmsg"></span>
|
|
30
|
+
</div>
|
|
31
|
+
<searchbox>
|
|
32
|
+
<input id="q" type="search" autofocus
|
|
33
|
+
placeholder='Search by meaning — press "/" to focus' autocomplete="off">
|
|
34
|
+
</searchbox>
|
|
35
|
+
<filters>
|
|
36
|
+
<input id="f-tag" placeholder="tag">
|
|
37
|
+
<input id="f-type" placeholder="type">
|
|
38
|
+
<input id="f-status" placeholder="status">
|
|
39
|
+
<input id="f-limit" type="number" min="1" max="50" placeholder="max/file">
|
|
40
|
+
</filters>
|
|
41
|
+
<div id="meta"></div>
|
|
42
|
+
<ol id="results"></ol>
|
|
43
|
+
</main>
|
|
44
|
+
<footer><code>POST /search</code> · <a href="/help">api</a> · mdss web ui</footer>
|
|
45
|
+
</body>
|
|
46
|
+
</html>`;
|
|
47
|
+
export const WEBUI_CSS = `/* mdss web ui (issue #111) — dark/light via prefers-color-scheme */
|
|
48
|
+
:root { color-scheme: light dark;
|
|
49
|
+
--bg:#fbfbfd; --fg:#1c1e21; --mut:#6b7280; --acc:#2563eb; --line:#e5e7eb;
|
|
50
|
+
--card:#ffffff; --mark:#fef08a; --bar:#dbeafe;
|
|
51
|
+
}
|
|
52
|
+
@media (prefers-color-scheme: dark) { :root {
|
|
53
|
+
--bg:#0f1115; --fg:#e6e6e6; --mut:#9aa0aa; --acc:#60a5fa; --line:#262b33;
|
|
54
|
+
--card:#161a21; --mark:#854d0e; --bar:#1e3a8a;
|
|
55
|
+
}}
|
|
56
|
+
:root[data-theme="dark"] { --bg:#0f1115; --fg:#e6e6e6; --mut:#9aa0aa; --acc:#60a5fa; --line:#262b33; --card:#161a21; --mark:#854d0e; --bar:#1e3a8a }
|
|
57
|
+
:root[data-theme="light"] { --bg:#fbfbfd; --fg:#1c1e21; --mut:#6b7280; --acc:#2563eb; --line:#e5e7eb; --card:#fff; --mark:#fef08a; --bar:#dbeafe }
|
|
58
|
+
|
|
59
|
+
* { box-sizing:border-box }
|
|
60
|
+
body { margin:0 auto; max-width:56rem; padding:0 1rem 3rem; background:var(--bg); color:var(--fg);
|
|
61
|
+
font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif }
|
|
62
|
+
header { display:flex; justify-content:space-between; align-items:center; padding:.9rem 0 }
|
|
63
|
+
.brand { font-weight:700; letter-spacing:.02em }
|
|
64
|
+
.brand span { margin-left:.6rem; font-weight:400; font-size:.8rem; color:var(--mut) }
|
|
65
|
+
#theme { background:none; border:1px solid var(--line); border-radius:.5rem; color:var(--fg);
|
|
66
|
+
cursor:pointer; font-size:1rem; line-height:1; padding:.35rem .6rem }
|
|
67
|
+
#q { width:100%; font-size:1.05rem; padding:.75rem 1rem; border:1px solid var(--line);
|
|
68
|
+
border-radius:.7rem; background:var(--card); color:var(--fg) }
|
|
69
|
+
#q:focus { outline:2px solid var(--acc); outline-offset:1px }
|
|
70
|
+
filters { display:flex; gap:.5rem; margin-top:.6rem; flex-wrap:wrap }
|
|
71
|
+
filters input { flex:1 1 7rem; min-width:0; padding:.4rem .6rem; border:1px solid var(--line);
|
|
72
|
+
border-radius:.5rem; background:var(--card); color:var(--fg); font-size:.85rem }
|
|
73
|
+
#meta { color:var(--mut); font-size:.85rem; margin:.7rem 0 .2rem }
|
|
74
|
+
#results { list-style:none; margin:0; padding:0 }
|
|
75
|
+
#results li { border:1px solid var(--line); background:var(--card); border-radius:.7rem;
|
|
76
|
+
padding:.7rem .9rem; margin-top:.6rem; cursor:pointer }
|
|
77
|
+
#results li.active { outline:2px solid var(--acc) }
|
|
78
|
+
.hit-head { display:flex; gap:.6rem; align-items:baseline }
|
|
79
|
+
.hit-title { font-weight:600 }
|
|
80
|
+
.hit-file { color:var(--acc); font-size:.82rem; white-space:nowrap }
|
|
81
|
+
.hit-path { color:var(--mut); font-size:.82rem }
|
|
82
|
+
mark { background:var(--mark); color:inherit; border-radius:.2rem; padding:0 .1em }
|
|
83
|
+
.bar { height:4px; background:var(--bar); border-radius:2px; margin:.45rem 0 .2rem; overflow:hidden }
|
|
84
|
+
.bar i { display:block; height:100%; background:var(--acc) }
|
|
85
|
+
.snippet { overflow:hidden; max-height:3.2em; transition:max-height .15s ease }
|
|
86
|
+
li.open .snippet { max-height:none }
|
|
87
|
+
.score { margin-left:auto; color:var(--mut); font-size:.78rem }
|
|
88
|
+
#authbar { display:flex; gap:.5rem; align-items:center; margin-bottom:.6rem;
|
|
89
|
+
padding:.5rem .7rem; border:1px dashed var(--line); border-radius:.6rem }
|
|
90
|
+
#authbar[hidden] { display:none }
|
|
91
|
+
footer { margin-top:2rem; color:var(--mut); font-size:.8rem }
|
|
92
|
+
`;
|
|
93
|
+
export const WEBUI_JS = `/* mdss web ui behaviour (issue #111) — no frameworks, no eval */
|
|
94
|
+
(function () {
|
|
95
|
+
'use strict';
|
|
96
|
+
var q = document.getElementById('q');
|
|
97
|
+
var resultsEl = document.getElementById('results');
|
|
98
|
+
var meta = document.getElementById('meta');
|
|
99
|
+
var healthEl = document.getElementById('health');
|
|
100
|
+
var authbar = document.getElementById('authbar');
|
|
101
|
+
var keyInput = document.getElementById('key');
|
|
102
|
+
|
|
103
|
+
var KEY_STORE = 'mdss_key';
|
|
104
|
+
var key = sessionStorage.getItem(KEY_STORE) || '';
|
|
105
|
+
var activeIdx = -1;
|
|
106
|
+
|
|
107
|
+
function headers() {
|
|
108
|
+
var h = { 'Content-Type': 'application/json' };
|
|
109
|
+
if (key) h['Authorization'] = 'Bearer ' + key;
|
|
110
|
+
return h;
|
|
111
|
+
}
|
|
112
|
+
function esc(s) {
|
|
113
|
+
return String(s).replace(/[&<>"']/g, function (c) {
|
|
114
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function highlight(text, terms) {
|
|
118
|
+
var safe = esc(text == null ? '' : text);
|
|
119
|
+
if (!terms || !terms.length) return safe;
|
|
120
|
+
var re = new RegExp('(' + terms.map(esc).join('|') + ')', 'gi');
|
|
121
|
+
return safe.replace(re, '<mark>$1</mark>');
|
|
122
|
+
}
|
|
123
|
+
function askKey(msg) {
|
|
124
|
+
authbar.hidden = false;
|
|
125
|
+
document.getElementById('authmsg').textContent = msg || '';
|
|
126
|
+
keyInput.focus();
|
|
127
|
+
}
|
|
128
|
+
document.getElementById('keysafe').addEventListener('click', function () {
|
|
129
|
+
key = keyInput.value.trim();
|
|
130
|
+
sessionStorage.setItem(KEY_STORE, key);
|
|
131
|
+
authbar.hidden = true;
|
|
132
|
+
run();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
function api(path, opts) {
|
|
136
|
+
opts = opts || {};
|
|
137
|
+
opts.headers = headers();
|
|
138
|
+
return fetch(path, opts).then(function (res) {
|
|
139
|
+
if (res.status === 401) { askKey('401 — this daemon requires an API key'); }
|
|
140
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
141
|
+
return res.json();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function render(data, terms) {
|
|
146
|
+
var rs = data.results || [];
|
|
147
|
+
meta.textContent = rs.length ? (data.count + ' hit(s) for “' + data.query + '”') : 'No matches.';
|
|
148
|
+
activeIdx = -1;
|
|
149
|
+
resultsEl.innerHTML = '';
|
|
150
|
+
rs.forEach(function (r, i) {
|
|
151
|
+
var li = document.createElement('li');
|
|
152
|
+
var pct = Math.max(2, Math.min(100, Math.round((r.cosine || 0) * 100)));
|
|
153
|
+
var headPath = (r.headingPath || []).slice(0, -1).join(' › ');
|
|
154
|
+
li.innerHTML =
|
|
155
|
+
'<div class="hit-head"><span class="hit-title">' + highlight(r.title, terms) + '</span>' +
|
|
156
|
+
'<span class="score">' + Number(r.cosine || 0).toFixed(3) + '</span></div>' +
|
|
157
|
+
((r.heading && r.heading !== r.title) ? '<div class="hit-path">' + highlight(headPath || r.heading, terms) + '</div>' : '') +
|
|
158
|
+
'<div class="bar"><i style="width:' + pct + '%"></i></div>' +
|
|
159
|
+
'<div class="snippet">' + highlight(r.snippet, terms) + '</div>' +
|
|
160
|
+
'<div class="hit-file">' + esc(r.file) + (r.startLine != null ? ':' + r.startLine : '') + '</div>';
|
|
161
|
+
li.addEventListener('click', function () { li.classList.toggle('open'); });
|
|
162
|
+
resultsEl.appendChild(li);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var timer = null;
|
|
167
|
+
function run() {
|
|
168
|
+
var query = q.value.trim();
|
|
169
|
+
if (!query) { resultsEl.innerHTML = ''; meta.textContent = ''; return; }
|
|
170
|
+
var filters = {};
|
|
171
|
+
['f-tag', 'f-type', 'f-status'].forEach(function (id) {
|
|
172
|
+
var v = document.getElementById(id).value.trim();
|
|
173
|
+
if (v) filters[id.slice(2)] = v; // f-tag -> tag …
|
|
174
|
+
});
|
|
175
|
+
var lim = parseInt(document.getElementById('f-limit').value, 10);
|
|
176
|
+
if (lim > 0) filters.maxPerFile = lim;
|
|
177
|
+
|
|
178
|
+
api('/search', { method: 'POST', body: JSON.stringify(Object.assign({ query: query, k: 20 }, filters)) })
|
|
179
|
+
.then(function (data) {
|
|
180
|
+
var lastTerms = (data.results || []).length
|
|
181
|
+
? (data.results[0].matches || []) : [];
|
|
182
|
+
render(data, lastTerms);
|
|
183
|
+
})
|
|
184
|
+
.catch(function (e) { if (String(e.message) !== 'HTTP 401') meta.textContent = 'error: ' + e.message; });
|
|
185
|
+
}
|
|
186
|
+
q.addEventListener('input', function () {
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
timer = setTimeout(run, 250);
|
|
189
|
+
});
|
|
190
|
+
document.querySelectorAll('filters input').forEach(function (el) {
|
|
191
|
+
el.addEventListener('input', function () { clearTimeout(timer); timer = setTimeout(run, 350); });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// keyboard-first: "/" focuses, arrows navigate, Enter toggles
|
|
195
|
+
document.addEventListener('keydown', function (ev) {
|
|
196
|
+
if (ev.key === '/' && document.activeElement !== q) { ev.preventDefault(); q.focus(); q.select(); return; }
|
|
197
|
+
if (ev.key === 'Escape') { q.blur(); return; }
|
|
198
|
+
var items = resultsEl.children;
|
|
199
|
+
if (!items.length) return;
|
|
200
|
+
if (ev.key === 'ArrowDown') { ev.preventDefault(); setActive(Math.min(items.length - 1, activeIdx + 1)); }
|
|
201
|
+
else if (ev.key === 'ArrowUp') { ev.preventDefault(); setActive(Math.max(0, activeIdx - 1)); }
|
|
202
|
+
else if (ev.key === 'Enter' && activeIdx >= 0) { items[activeIdx].classList.toggle('open'); }
|
|
203
|
+
});
|
|
204
|
+
function setActive(i) {
|
|
205
|
+
var items = resultsEl.children;
|
|
206
|
+
if (activeIdx >= 0 && items[activeIdx]) items[activeIdx].classList.remove('active');
|
|
207
|
+
activeIdx = i;
|
|
208
|
+
if (items[i]) {
|
|
209
|
+
items[i].classList.add('active');
|
|
210
|
+
items[i].scrollIntoView({ block: 'nearest' });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// theme: follow system until user overrides (persisted)
|
|
215
|
+
var root = document.documentElement;
|
|
216
|
+
var saved = null;
|
|
217
|
+
try { saved = localStorage.getItem('mdss_theme'); } catch (e) {}
|
|
218
|
+
if (saved) root.dataset.theme = saved;
|
|
219
|
+
document.getElementById('theme').addEventListener('click', function () {
|
|
220
|
+
var sysDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
221
|
+
var cur = root.dataset.theme || (sysDark ? 'dark' : 'light');
|
|
222
|
+
var next = cur === 'dark' ? 'light' : 'dark';
|
|
223
|
+
root.dataset.theme = next;
|
|
224
|
+
try { localStorage.setItem('mdss_theme', next); } catch (e) {}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
api('/health').then(function (h) {
|
|
228
|
+
healthEl.textContent = h.chunks + ' chunks · ' + h.model;
|
|
229
|
+
}).catch(function () { healthEl.textContent = ''; });
|
|
230
|
+
})();
|
|
231
|
+
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md-semantic-search",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Local, private semantic (vector) search over any folder of Markdown files. Finds passages by meaning — cross-lingual — using transformers.js. No API keys, no cloud, no vector database.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completions generator (issue #107).
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth: the FLAGS table below drives every shell script AND
|
|
5
|
+
* the drift-guard test (test/completions.test.mjs) that diffs it against the
|
|
6
|
+
* flags actually parsed in bin/cli.mjs. Adding a flag in parseArgs without
|
|
7
|
+
* updating this table fails the test — and vice versa.
|
|
8
|
+
*
|
|
9
|
+
* Static generation only: no runtime dependency, `mdss completions <shell>`
|
|
10
|
+
* prints a self-contained script to stdout.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type FlagKind = 'flag' | 'value' | 'file' | 'dir' | 'number';
|
|
14
|
+
|
|
15
|
+
export interface FlagSpec {
|
|
16
|
+
/** Primary long form, e.g. "--index-dir". */
|
|
17
|
+
name: string;
|
|
18
|
+
alias?: string;
|
|
19
|
+
kind: FlagKind;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Every option bin/cli.mjs parseArgs understands (issue #107 drift guard). */
|
|
23
|
+
export const FLAGS: FlagSpec[] = [
|
|
24
|
+
// boolean switches
|
|
25
|
+
{ name: '--json', kind: 'flag' },
|
|
26
|
+
{ name: '--semantic', kind: 'flag' },
|
|
27
|
+
{ name: '--offline', kind: 'flag' },
|
|
28
|
+
{ name: '--watch', kind: 'flag' },
|
|
29
|
+
{ name: '--watch-debug', kind: 'flag' },
|
|
30
|
+
{ name: '--no-ui', kind: 'flag' },
|
|
31
|
+
{ name: '--mcp', kind: 'flag' },
|
|
32
|
+
{ name: '--rerank', kind: 'flag' },
|
|
33
|
+
{ name: '--explain', kind: 'flag' },
|
|
34
|
+
{ name: '--interactive', alias: '-i', kind: 'flag' },
|
|
35
|
+
{ name: '--list-tools', kind: 'flag' },
|
|
36
|
+
{ name: '--no-vectors', alias: '--no-vector', kind: 'flag' },
|
|
37
|
+
{ name: '--no-query-cache', kind: 'flag' },
|
|
38
|
+
{ name: '--recency', kind: 'number' },
|
|
39
|
+
{ name: '--embedder', kind: 'value' },
|
|
40
|
+
{ name: '--embedder-model', kind: 'value' },
|
|
41
|
+
{ name: '--embedder-base-url', kind: 'value' },
|
|
42
|
+
{ name: '--embedder-key-file', kind: 'file' },
|
|
43
|
+
{ name: '--expand', kind: 'value' },
|
|
44
|
+
{ name: '--expand-passages', kind: 'number' },
|
|
45
|
+
{ name: '--health-public', kind: 'flag' },
|
|
46
|
+
{ name: '--allow-unsecured', kind: 'flag' },
|
|
47
|
+
{ name: '--ann', kind: 'flag' },
|
|
48
|
+
{ name: '--canonical', kind: 'flag' },
|
|
49
|
+
{ name: '--rag', kind: 'flag' },
|
|
50
|
+
{ name: '--auto-tag', kind: 'flag' },
|
|
51
|
+
{ name: '--auto-summarize', kind: 'flag' },
|
|
52
|
+
{ name: '--help', alias: '-h', kind: 'flag' },
|
|
53
|
+
{ name: '--version', kind: 'flag' },
|
|
54
|
+
{ name: '--open', kind: 'flag' }, // optional N consumed by parseArgs itself
|
|
55
|
+
{ name: '--fix', kind: 'flag' },
|
|
56
|
+
{ name: '--dry-run', kind: 'flag' },
|
|
57
|
+
// values
|
|
58
|
+
{ name: '--workers', kind: 'number' },
|
|
59
|
+
{ name: '--config', kind: 'file' },
|
|
60
|
+
{ name: '--format', kind: 'value' },
|
|
61
|
+
{ name: '--output', alias: '-o', kind: 'file' },
|
|
62
|
+
{ name: '--db', kind: 'dir' },
|
|
63
|
+
{ name: '--vault', kind: 'dir' },
|
|
64
|
+
{ name: '--index-dir', kind: 'dir' },
|
|
65
|
+
{ name: '--cache-dir', kind: 'dir' },
|
|
66
|
+
{ name: '--model', kind: 'value' },
|
|
67
|
+
{ name: '--k', kind: 'number' },
|
|
68
|
+
{ name: '--max-per-file', kind: 'number' },
|
|
69
|
+
{ name: '--max-per-doc', kind: 'number' },
|
|
70
|
+
{ name: '--target-tokens', kind: 'number' },
|
|
71
|
+
{ name: '--port', kind: 'number' },
|
|
72
|
+
{ name: '--host', kind: 'value' },
|
|
73
|
+
{ name: '--api-key', kind: 'value' },
|
|
74
|
+
{ name: '--api-key-file', kind: 'file' },
|
|
75
|
+
{ name: '--allowed-host', kind: 'value' },
|
|
76
|
+
{ name: '--cors-origin', kind: 'value' },
|
|
77
|
+
{ name: '--watch-delay', kind: 'number' },
|
|
78
|
+
{ name: '--watch-interval', kind: 'number' },
|
|
79
|
+
{ name: '--rate-limit', kind: 'number' },
|
|
80
|
+
{ name: '--max-concurrency', kind: 'number' },
|
|
81
|
+
{ name: '--since', kind: 'value' },
|
|
82
|
+
{ name: '--path', kind: 'value' },
|
|
83
|
+
{ name: '--ignore', kind: 'value' },
|
|
84
|
+
{ name: '--tag', kind: 'value' },
|
|
85
|
+
{ name: '--nprobe', kind: 'number' },
|
|
86
|
+
{ name: '--project', kind: 'value' },
|
|
87
|
+
{ name: '--type', kind: 'value' },
|
|
88
|
+
{ name: '--status', kind: 'value' },
|
|
89
|
+
{ name: '--graph-boost', kind: 'number' },
|
|
90
|
+
{ name: '--filter', kind: 'value' },
|
|
91
|
+
{ name: '--quantize', kind: 'value' },
|
|
92
|
+
{ name: '--llm-endpoint', kind: 'value' },
|
|
93
|
+
{ name: '--llm-model', kind: 'value' },
|
|
94
|
+
{ name: '--system-prompt', kind: 'value' },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
export const COMMANDS = [
|
|
98
|
+
'index', 'stats', 'check', 'doctor', 'export', 'search',
|
|
99
|
+
'ask', 'tui', 'serve', 'mcp', 'models', 'completions',
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
export const COMPLETION_SHELLS = ['bash', 'zsh', 'fish', 'powershell'] as const;
|
|
103
|
+
export type CompletionShell = (typeof COMPLETION_SHELLS)[number];
|
|
104
|
+
|
|
105
|
+
const DIR_FLAGS = [...new Set(FLAGS.filter((f) => f.kind === 'dir').flatMap((f) => [f.name, ...(f.alias && !f.alias.startsWith('--') ? [f.alias] : [])]))];
|
|
106
|
+
const FILE_FLAGS = [...new Set(FLAGS.filter((f) => f.kind === 'file').flatMap((f) => [f.name, ...(f.alias && !f.alias.startsWith('--') ? [f.alias] : [])]))];
|
|
107
|
+
const ALL_FLAG_WORDS = FLAGS.flatMap((f) => [f.name, ...(f.alias && f.alias.startsWith('--') ? [f.alias] : [])]);
|
|
108
|
+
|
|
109
|
+
function bash(): string {
|
|
110
|
+
const dirCase = DIR_FLAGS.join('|');
|
|
111
|
+
const fileCase = FILE_FLAGS.join('|');
|
|
112
|
+
return `# bash completion for mdss (md-semantic-search)
|
|
113
|
+
# Generated by \`mdss completions bash\` (issue #107).
|
|
114
|
+
# Install: eval "$(mdss completions bash)"
|
|
115
|
+
_mdss_completion() {
|
|
116
|
+
local cur prev commands flags
|
|
117
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
118
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
119
|
+
commands="${COMMANDS.join(' ')}"
|
|
120
|
+
flags="${ALL_FLAG_WORDS.join(' ')}"
|
|
121
|
+
|
|
122
|
+
case "$prev" in
|
|
123
|
+
${dirCase})
|
|
124
|
+
mapfile -t COMPREPLY < <(compgen -d -- "$cur"); return 0 ;;
|
|
125
|
+
${fileCase})
|
|
126
|
+
mapfile -t COMPREPLY < <(compgen -f -- "$cur"); return 0 ;;
|
|
127
|
+
esac
|
|
128
|
+
|
|
129
|
+
if [[ "$cur" == -* ]]; then
|
|
130
|
+
mapfile -t COMPREPLY < <(compgen -W "$flags" -- "$cur")
|
|
131
|
+
return 0
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
if (( COMP_CWORD <= 2 )); then
|
|
135
|
+
mapfile -t COMPREPLY < <(compgen -W "$commands" -- "$cur")
|
|
136
|
+
fi
|
|
137
|
+
}
|
|
138
|
+
complete -o default -F _mdss_completion mdss md-semantic-search
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function zsh(): string {
|
|
143
|
+
const cmdDescs = COMMANDS.map((c) => `'${c}'`).join(' ');
|
|
144
|
+
return `#compdef mdss
|
|
145
|
+
# zsh completion for mdss — generated by \`mdss completions zsh\` (issue #107).
|
|
146
|
+
# Install: mkdir -p ~/.zfunc && mdss completions zsh > ~/.zfunc/_mdss
|
|
147
|
+
# (ensure ~/.zfunc is in fpath BEFORE compinit)
|
|
148
|
+
local -a commands options dir_flags file_flags
|
|
149
|
+
commands=(${cmdDescs})
|
|
150
|
+
options=(${ALL_FLAG_WORDS.map((f) => `'${f}'`).join(' ')})
|
|
151
|
+
dir_flags=(${DIR_FLAGS.map((f) => `'${f}'`).join(' ')})
|
|
152
|
+
file_flags=(${FILE_FLAGS.map((f) => `'${f}'`).join(' ')})
|
|
153
|
+
|
|
154
|
+
_mdss() {
|
|
155
|
+
local cur prev
|
|
156
|
+
cur="\${words[CURRENT]}"
|
|
157
|
+
prev="\${words[CURRENT-1]}"
|
|
158
|
+
if (( CURRENT == 2 )) && [[ "$cur" != -* ]]; then
|
|
159
|
+
_describe -t commands 'mdss command' commands
|
|
160
|
+
return
|
|
161
|
+
fi
|
|
162
|
+
if (( \${dir_flags[(Ie)$prev]} )); then
|
|
163
|
+
_directories
|
|
164
|
+
return
|
|
165
|
+
fi
|
|
166
|
+
if (( \${file_flags[(Ie)$prev]} )); then
|
|
167
|
+
_files
|
|
168
|
+
return
|
|
169
|
+
fi
|
|
170
|
+
if [[ "$cur" == -* ]]; then
|
|
171
|
+
_describe -t options 'mdss option' options
|
|
172
|
+
return
|
|
173
|
+
fi
|
|
174
|
+
}
|
|
175
|
+
compdef _mdss mdss md-semantic-search
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function fish(): string {
|
|
180
|
+
const lines: string[] = [
|
|
181
|
+
'# fish completion for mdss — generated by `mdss completions fish` (issue #107).',
|
|
182
|
+
'# Install: mdss completions fish > ~/.config/fish/completions/mdss.fish',
|
|
183
|
+
'',
|
|
184
|
+
`complete -c mdss -n '__fish_is_first_arg' -a '${COMMANDS.join(' ')}'`,
|
|
185
|
+
'',
|
|
186
|
+
];
|
|
187
|
+
const seenShorts = new Set<string>();
|
|
188
|
+
for (const f of FLAGS) {
|
|
189
|
+
const parts = ['complete -c mdss'];
|
|
190
|
+
if (f.alias && !f.alias.startsWith('--') && !seenShorts.has(f.alias)) {
|
|
191
|
+
parts.push(`-s ${f.alias.slice(1)}`);
|
|
192
|
+
seenShorts.add(f.alias);
|
|
193
|
+
}
|
|
194
|
+
parts.push(`-l ${f.name.slice(2)}`);
|
|
195
|
+
if (f.kind !== 'flag') parts.push('-r');
|
|
196
|
+
if (f.kind === 'file') parts.push('-F');
|
|
197
|
+
if (f.kind === 'dir') parts.push('-a "(__fish_complete_directories)"');
|
|
198
|
+
lines.push(parts.join(' '));
|
|
199
|
+
}
|
|
200
|
+
return lines.join('\n') + '\n';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function powershell(): string {
|
|
204
|
+
return `# PowerShell completion for mdss — generated by \`mdss completions powershell\` (issue #107).
|
|
205
|
+
# Install: paste into your $PROFILE (or dot-source it from there).
|
|
206
|
+
Register-ArgumentCompleter -CommandName 'mdss','md-semantic-search' -Native -ScriptBlock {
|
|
207
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
208
|
+
$commands = @('${COMMANDS.join("','")}')
|
|
209
|
+
$dirFlags = @('${DIR_FLAGS.join("','")}')
|
|
210
|
+
$fileFlags = @('${FILE_FLAGS.join("','")}')
|
|
211
|
+
$flags = @('${ALL_FLAG_WORDS.join("','")}')
|
|
212
|
+
|
|
213
|
+
$elements = @($commandAst.CommandElements)
|
|
214
|
+
if ($elements.Count -le 1) {
|
|
215
|
+
$commands | Where-Object { $_ -like "$wordToComplete*" } |
|
|
216
|
+
ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'Command', $_) }
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
$prev = ''
|
|
221
|
+
if ($elements.Count -ge 2) { $prev = $elements[$elements.Count - 2].ToString() }
|
|
222
|
+
|
|
223
|
+
if (($dirFlags -contains $prev) -or ($fileFlags -contains $prev)) {
|
|
224
|
+
return # defer path completion to PowerShell's native provider
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if ("$wordToComplete".StartsWith('-')) {
|
|
228
|
+
$flags | Where-Object { $_ -like "$wordToComplete*" } |
|
|
229
|
+
ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_) }
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if ($elements.Count -eq 2) {
|
|
234
|
+
$commands | Where-Object { $_ -like "$wordToComplete*" } |
|
|
235
|
+
ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'Command', $_) }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const GENERATORS: Record<CompletionShell, () => string> = { bash, zsh, fish, powershell };
|
|
242
|
+
|
|
243
|
+
/** Generate a self-contained completion script; throws on an unknown shell. */
|
|
244
|
+
export function generateCompletion(shell: string): string {
|
|
245
|
+
const gen = GENERATORS[shell as CompletionShell];
|
|
246
|
+
if (!gen) {
|
|
247
|
+
throw new Error(`unknown shell "${shell}" — use one of: ${COMPLETION_SHELLS.join('|')}`);
|
|
248
|
+
}
|
|
249
|
+
return gen();
|
|
250
|
+
}
|
package/src/core.ts
CHANGED
|
@@ -18,6 +18,31 @@ const _extractors = new Map<string, any>();
|
|
|
18
18
|
export const LOCK_FILENAME = '.mdss.lock';
|
|
19
19
|
const LOCK_STALE_MS = 10 * 60 * 1000;
|
|
20
20
|
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Model-download progress hook (issue #108): transformers.js fires per-file
|
|
23
|
+
// events while a model streams in; loaders below forward them to ONE process-
|
|
24
|
+
// wide listener so the CLI can render a single aggregated bar.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export type DownloadProgressListener = (e: import('./download-progress.js').DownloadProgressEvent) => void;
|
|
28
|
+
|
|
29
|
+
let downloadListener: DownloadProgressListener | null = null;
|
|
30
|
+
|
|
31
|
+
/** Observe model-file download events from every pipeline load (issue #108). Pass null to detach. */
|
|
32
|
+
export function setDownloadProgressListener(cb: DownloadProgressListener | null): void {
|
|
33
|
+
downloadListener = typeof cb === 'function' ? cb : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Low-level emit used by the loaders; exported so tests/tools can simulate events. */
|
|
37
|
+
export function emitDownloadEvent(e: import('./download-progress.js').DownloadProgressEvent): void {
|
|
38
|
+
if (!downloadListener) return;
|
|
39
|
+
try {
|
|
40
|
+
downloadListener(e);
|
|
41
|
+
} catch {
|
|
42
|
+
// a broken UI listener must never abort an in-flight model download
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
21
46
|
export function pidAlive(pid: number | null): boolean {
|
|
22
47
|
if (!Number.isInteger(pid) || pid === null || pid <= 0) return false;
|
|
23
48
|
try {
|
|
@@ -240,6 +265,7 @@ export async function getExtractor(
|
|
|
240
265
|
const pipeOpts: any = {
|
|
241
266
|
revision,
|
|
242
267
|
dtype: model.dtype || 'q8',
|
|
268
|
+
progress_callback: emitDownloadEvent,
|
|
243
269
|
...(sessionOptions ? { session_options: sessionOptions } : {}),
|
|
244
270
|
};
|
|
245
271
|
const ext = await pipeline('feature-extraction', source, pipeOpts);
|