sweet-search 2.5.5 → 2.5.7
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 +321 -109
- package/assets/banner/banner-frames.webp +0 -0
- package/assets/banner/banner-manifest.json +10 -0
- package/core/banner/render-banner.js +209 -0
- package/core/banner/sixel.js +126 -0
- package/core/embedding/embedding-local-model.js +1 -0
- package/core/graph/relationship-resolver.js +5 -1
- package/core/indexing/index-codebase-v21.js +7 -6
- package/core/indexing/indexer-ann.js +4 -4
- package/core/indexing/indexer-build.js +1 -1
- package/core/indexing/indexer-utils.js +64 -17
- package/core/infrastructure/onnx-session-utils.js +1 -0
- package/core/infrastructure/simd-distance.js +11 -6
- package/core/ranking/late-interaction-model.js +1 -0
- package/package.json +16 -10
- package/scripts/init.js +28 -6
- package/scripts/postinstall-banner.js +34 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animated terminal banner — universal renderer.
|
|
3
|
+
*
|
|
4
|
+
* Decodes the pre-baked lossless WebP sprite-sheet (assets/banner/) once with sharp,
|
|
5
|
+
* then animates it via the best path the terminal supports:
|
|
6
|
+
*
|
|
7
|
+
* Kitty graphics (Ghostty, kitty, WezTerm, Konsole) — pixel-perfect
|
|
8
|
+
* iTerm2 inline (iTerm.app) — pixel-perfect
|
|
9
|
+
* Sixel (Windows Terminal, Konsole, xterm, foot…) — crisp raster
|
|
10
|
+
* half-blocks (everything else: Apple Terminal, VS Code, SSH, CI-tty) — truecolor ▀
|
|
11
|
+
*
|
|
12
|
+
* Zero prerequisites for users: the only dependency is sharp (already a package dep,
|
|
13
|
+
* auto-installed per-platform by npm). Chrome is used ONLY at bake time (scripts/bake-banner.mjs).
|
|
14
|
+
*
|
|
15
|
+
* Safe by construction: renders only to an interactive TTY, never throws, honours
|
|
16
|
+
* NO_BANNER / SWEET_SEARCH_NO_BANNER / CI, and leaves the final frame in place when done.
|
|
17
|
+
*/
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
import { encodeSixelFrame, buildPalette, makeMapper, sampleColors } from './sixel.js';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
|
|
26
|
+
const ASSET_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'assets', 'banner');
|
|
27
|
+
const ESC = '\x1b', BEL = '\x07', ST = ESC + '\\';
|
|
28
|
+
const SYNC_ON = `${ESC}[?2026h`, SYNC_OFF = `${ESC}[?2026l`;
|
|
29
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
30
|
+
|
|
31
|
+
// ---------------- gating ----------------
|
|
32
|
+
function shouldRender(stream, env) {
|
|
33
|
+
if (env.SWEET_SEARCH_NO_BANNER || env.NO_BANNER) return false;
|
|
34
|
+
if (env.CI) return false;
|
|
35
|
+
if (!stream || !stream.isTTY) return false;
|
|
36
|
+
if ((env.TERM || '') === 'dumb') return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------- terminal capability query (DA1 + text-area size) ----------------
|
|
41
|
+
function queryTerminal(stream, env, timeout = 140) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const stdin = process.stdin;
|
|
44
|
+
if (env.SS_NO_QUERY || !stdin || !stdin.isTTY) { resolve({}); return; }
|
|
45
|
+
let buf = '', done = false, t;
|
|
46
|
+
const finish = () => {
|
|
47
|
+
if (done) return; done = true;
|
|
48
|
+
try { stdin.removeListener('data', onData); stdin.setRawMode(false); stdin.pause(); } catch { /* noop */ }
|
|
49
|
+
clearTimeout(t);
|
|
50
|
+
const da1 = (buf.match(/\x1b\[\?([0-9;]+)c/) || [])[1] || '';
|
|
51
|
+
const size = buf.match(/\x1b\[4;(\d+);(\d+)t/);
|
|
52
|
+
resolve({ sixel: da1.split(';').includes('4'), areaH: size ? +size[1] : 0, areaW: size ? +size[2] : 0 });
|
|
53
|
+
};
|
|
54
|
+
const onData = (d) => { buf += d.toString('latin1'); if (/\x1b\[\?[0-9;]+c/.test(buf) && (/\x1b\[4;\d+;\d+t/.test(buf) || buf.length > 64)) finish(); };
|
|
55
|
+
try { stdin.setRawMode(true); stdin.resume(); stdin.on('data', onData); } catch { resolve({}); return; }
|
|
56
|
+
stream.write(`${ESC}[14t${ESC}[c`); // text-area pixel size, then primary device attributes
|
|
57
|
+
t = setTimeout(finish, timeout);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function detectProto(env, caps) {
|
|
62
|
+
if (env.SS_PROTO) return env.SS_PROTO;
|
|
63
|
+
const tp = env.TERM_PROGRAM || '';
|
|
64
|
+
if (env.KITTY_WINDOW_ID || env.GHOSTTY_RESOURCES_DIR || tp === 'ghostty' || tp === 'WezTerm' || /kitty/i.test(env.TERM || '')) return 'kitty';
|
|
65
|
+
if (tp === 'iTerm.app') return 'iterm';
|
|
66
|
+
if (caps.sixel || env.WT_SESSION || /foot|contour|mlterm/i.test(env.TERM || '')) return 'sixel';
|
|
67
|
+
return 'blocks';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------- color (truecolor default; 256 fallback) ----------------
|
|
71
|
+
function rgb256(r, g, b) {
|
|
72
|
+
if (Math.abs(r - g) < 10 && Math.abs(g - b) < 10) { if (r < 8) return 16; if (r > 248) return 231; return 232 + Math.round(((r - 8) / 247) * 24); }
|
|
73
|
+
return 16 + 36 * Math.round(r / 255 * 5) + 6 * Math.round(g / 255 * 5) + Math.round(b / 255 * 5);
|
|
74
|
+
}
|
|
75
|
+
const GHALF = [' ', '▀', '▄', '█'];
|
|
76
|
+
function sextantChar(v) {
|
|
77
|
+
if (v === 0) return ' '; if (v === 63) return '█'; if (v === 21) return '▌'; if (v === 42) return '▐';
|
|
78
|
+
return String.fromCodePoint(0x1FB00 + (v - 1 - (v > 21 ? 1 : 0) - (v > 42 ? 1 : 0)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------- main ----------------
|
|
82
|
+
export async function showBanner(opts = {}) {
|
|
83
|
+
const env = opts.env || process.env;
|
|
84
|
+
const out = opts.stream || process.stdout;
|
|
85
|
+
let onSig = null;
|
|
86
|
+
try {
|
|
87
|
+
if (!shouldRender(out, env)) return false;
|
|
88
|
+
|
|
89
|
+
const sharp = require('sharp');
|
|
90
|
+
const man = JSON.parse(readFileSync(join(ASSET_DIR, 'banner-manifest.json'), 'utf8'));
|
|
91
|
+
const { gridCols: GC, cellW: CW, cellH: CH, count: N, frameMs } = man;
|
|
92
|
+
const maxMs = opts.maxMs ?? Number(env.SS_BANNER_MS || 2600);
|
|
93
|
+
const color = opts.color || env.SS_COLOR || 'truecolor';
|
|
94
|
+
const cellMode = opts.cells || env.SS_CELLS || 'half';
|
|
95
|
+
|
|
96
|
+
const caps = opts.query === false ? {} : await queryTerminal(out, env);
|
|
97
|
+
const proto = opts.proto || detectProto(env, caps);
|
|
98
|
+
const capCols = Number(opts.cols || env.SS_COLS || 120);
|
|
99
|
+
const cols = Math.max(20, Math.min((out.columns || 80) - 2, capCols));
|
|
100
|
+
|
|
101
|
+
// decode the sprite sheet once
|
|
102
|
+
const { data: big, info } = await sharp(join(ASSET_DIR, man.file)).raw().toBuffer({ resolveWithObject: true });
|
|
103
|
+
const BW = info.width, ch = info.channels;
|
|
104
|
+
const frameRaw = (i) => {
|
|
105
|
+
const gx = (i % GC) * CW, gy = Math.floor(i / GC) * CH, f = Buffer.allocUnsafe(CW * CH * 4);
|
|
106
|
+
for (let y = 0; y < CH; y++) { const so = ((gy + y) * BW + gx) * ch; big.copy(f, y * CW * 4, so, so + CW * 4); }
|
|
107
|
+
return f;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const fg = color === '256' ? (r, g, b) => `${ESC}[38;5;${rgb256(r, g, b)}m` : (r, g, b) => `${ESC}[38;2;${r};${g};${b}m`;
|
|
111
|
+
const bg = color === '256' ? (r, g, b) => `${ESC}[48;5;${rgb256(r, g, b)}m` : (r, g, b) => `${ESC}[48;2;${r};${g};${b}m`;
|
|
112
|
+
const RESET = `${ESC}[0m`;
|
|
113
|
+
|
|
114
|
+
// ---------------- per-protocol lazy frame builder (build only frames we show) ----------------
|
|
115
|
+
let renderRows = Math.max(4, Math.round(cols / 6));
|
|
116
|
+
const cache = new Map();
|
|
117
|
+
let buildOne;
|
|
118
|
+
|
|
119
|
+
if (proto === 'kitty' || proto === 'iterm') {
|
|
120
|
+
buildOne = async (i) => (await sharp(frameRaw(i), { raw: { width: CW, height: CH, channels: 4 } }).png().toBuffer()).toString('base64');
|
|
121
|
+
} else if (proto === 'sixel') {
|
|
122
|
+
const cellWpx = caps.areaW && out.columns ? caps.areaW / out.columns : 8;
|
|
123
|
+
const Wp = Math.min(1000, Math.max(360, Math.round(cols * cellWpx)));
|
|
124
|
+
const Hp = Math.round(Wp / 3);
|
|
125
|
+
const cellHpx = caps.areaH && out.rows ? caps.areaH / out.rows : 16;
|
|
126
|
+
renderRows = Math.max(4, Math.round(Hp / cellHpx));
|
|
127
|
+
const sixelPx = async (i) => (await sharp(frameRaw(i), { raw: { width: CW, height: CH, channels: 4 } }).resize(Wp, Hp, { fit: 'fill' }).raw().toBuffer());
|
|
128
|
+
// global palette from 2 representative frames (colours are stable across the loop)
|
|
129
|
+
const samp = [...sampleColors(await sixelPx(0), 9), ...sampleColors(await sixelPx((N / 2) | 0), 9)];
|
|
130
|
+
const palette = buildPalette(samp, 255), mapper = makeMapper(palette);
|
|
131
|
+
buildOne = async (i) => encodeSixelFrame(await sixelPx(i), Wp, Hp, palette, mapper);
|
|
132
|
+
} else {
|
|
133
|
+
const [sc, sr] = cellMode === 'sextant' ? [2, 3] : [1, 2];
|
|
134
|
+
const R = Math.max(3, Math.round((cols * sc) * CH / CW / sr)); renderRows = R;
|
|
135
|
+
const W = cols * sc, H = R * sr;
|
|
136
|
+
const glyph = cellMode === 'sextant' ? sextantChar : (v) => GHALF[v];
|
|
137
|
+
buildOne = async (i) => {
|
|
138
|
+
const { data: px } = await sharp(frameRaw(i), { raw: { width: CW, height: CH, channels: 4 } }).resize(W, H, { fit: 'fill', kernel: 'lanczos3' }).sharpen({ sigma: 0.7 }).raw().toBuffer({ resolveWithObject: true });
|
|
139
|
+
let s = '';
|
|
140
|
+
for (let cy = 0; cy < R; cy++) {
|
|
141
|
+
for (let cx = 0; cx < cols; cx++) {
|
|
142
|
+
const sub = []; let anyT = false, anyO = false;
|
|
143
|
+
for (let y = 0; y < sr; y++) for (let x = 0; x < sc; x++) { const o = ((cy * sr + y) * W + (cx * sc + x)) * 4, a = px[o + 3]; sub.push({ r: px[o], g: px[o + 1], b: px[o + 2], a }); if (a < 128) anyT = true; else anyO = true; }
|
|
144
|
+
if (!anyO) { s += RESET + ' '; continue; }
|
|
145
|
+
const op = sub.filter(p => p.a >= 128), lum = p => 0.299 * p.r + 0.587 * p.g + 0.114 * p.b;
|
|
146
|
+
let lo = op[0], hi = op[0];
|
|
147
|
+
for (const p of op) { if (lum(p) < lum(lo)) lo = p; if (lum(p) > lum(hi)) hi = p; }
|
|
148
|
+
const dist = (p, q) => (p.r - q.r) ** 2 + (p.g - q.g) ** 2 + (p.b - q.b) ** 2;
|
|
149
|
+
let fr = 0, fG = 0, fb = 0, fn = 0, br = 0, bG = 0, bb = 0, bn = 0, bits = 0;
|
|
150
|
+
for (let k = 0; k < sub.length; k++) { const p = sub[k]; if (p.a < 128) continue; if (dist(p, hi) <= dist(p, lo)) { bits |= (1 << k); fr += p.r; fG += p.g; fb += p.b; fn++; } else { br += p.r; bG += p.g; bb += p.b; bn++; } }
|
|
151
|
+
const fc = fn ? [Math.round(fr / fn), Math.round(fG / fn), Math.round(fb / fn)] : null;
|
|
152
|
+
const bc = bn ? [Math.round(br / bn), Math.round(bG / bn), Math.round(bb / bn)] : null;
|
|
153
|
+
const full = (1 << sub.length) - 1;
|
|
154
|
+
if (anyT) s += bits === 0 ? RESET + ' ' : RESET + fg(...(fc || bc)) + glyph(bits);
|
|
155
|
+
else if (bits === 0) s += bg(...bc) + ' ';
|
|
156
|
+
else if (bits === full) s += fg(...fc) + glyph(bits);
|
|
157
|
+
else s += bg(...bc) + fg(...fc) + glyph(bits);
|
|
158
|
+
}
|
|
159
|
+
s += RESET; if (cy < R - 1) s += '\r\n';
|
|
160
|
+
}
|
|
161
|
+
return s;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const getFrame = async (i) => { let v = cache.get(i); if (v === undefined) { v = await buildOne(i); cache.set(i, v); } return v; };
|
|
165
|
+
|
|
166
|
+
// ---------------- emit / animate (bounded) ----------------
|
|
167
|
+
const CHUNK = 4096;
|
|
168
|
+
const kittyFrame = (b64, id) => {
|
|
169
|
+
if (b64.length <= CHUNK) { out.write(`${ESC}_Gf=100,a=T,q=2,c=${cols},C=1,i=${id};${b64}${ST}`); return; }
|
|
170
|
+
for (let i = 0; i < b64.length; i += CHUNK) { const piece = b64.slice(i, i + CHUNK), more = i + CHUNK < b64.length ? 1 : 0; out.write(i === 0 ? `${ESC}_Gf=100,a=T,q=2,c=${cols},C=1,i=${id},m=1;${piece}${ST}` : `${ESC}_Gm=${more};${piece}${ST}`); }
|
|
171
|
+
};
|
|
172
|
+
const kittyDel = (id) => out.write(`${ESC}_Ga=d,d=i,i=${id},q=2${ST}`);
|
|
173
|
+
|
|
174
|
+
await getFrame(0); // build first frame before clearing space (hide latency)
|
|
175
|
+
out.write(ESC + '[?25l');
|
|
176
|
+
// Restore the cursor if interrupted mid-animation (else Ctrl-C leaves it hidden).
|
|
177
|
+
onSig = () => { try { out.write(`${ESC}[0m${ESC}[?25h`); } catch { /* noop */ } process.exit(130); };
|
|
178
|
+
process.once('SIGINT', onSig);
|
|
179
|
+
if (proto === 'blocks' || proto === 'sixel') { for (let r = 0; r < renderRows; r++) out.write('\n'); out.write(`${ESC}[${renderRows}A`); }
|
|
180
|
+
else out.write('\n');
|
|
181
|
+
out.write(ESC + '7');
|
|
182
|
+
|
|
183
|
+
let prevId = 0, flip = 1;
|
|
184
|
+
const start = Date.now();
|
|
185
|
+
let i = 0;
|
|
186
|
+
while (Date.now() - start < maxMs) {
|
|
187
|
+
const frame = await getFrame(i);
|
|
188
|
+
out.write(SYNC_ON + ESC + '8');
|
|
189
|
+
if (proto === 'kitty') { const id = flip; kittyFrame(frame, id); if (prevId) kittyDel(prevId); prevId = id; flip = flip === 1 ? 2 : 1; }
|
|
190
|
+
else if (proto === 'iterm') out.write(`${ESC}]1337;File=inline=1;width=${cols};height=${renderRows};preserveAspectRatio=1:${frame}${BEL}`);
|
|
191
|
+
else out.write(frame);
|
|
192
|
+
out.write(SYNC_OFF);
|
|
193
|
+
await sleep(frameMs);
|
|
194
|
+
i = (i + 1) % N;
|
|
195
|
+
}
|
|
196
|
+
out.write(ESC + '8'); // settle: leave final frame, move below
|
|
197
|
+
out.write('\n'.repeat(renderRows + 1));
|
|
198
|
+
out.write(RESET + ESC + '[?25h');
|
|
199
|
+
if (onSig) process.removeListener('SIGINT', onSig); // hand Ctrl-C back to the caller
|
|
200
|
+
return true;
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (onSig) process.removeListener('SIGINT', onSig);
|
|
203
|
+
if (env.SS_BANNER_DEBUG) process.stderr.write(`[banner] ${err && err.stack || err}\n`);
|
|
204
|
+
try { out.write(`${ESC}[0m${ESC}[?25h`); } catch { /* noop */ }
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export default showBanner;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, dependency-free Sixel encoder for the terminal banner.
|
|
3
|
+
*
|
|
4
|
+
* Sixel renders crisp raster images on Windows Terminal (>=1.22), Konsole, xterm,
|
|
5
|
+
* foot, Contour and much of Linux — terminals that lack the Kitty graphics protocol.
|
|
6
|
+
*
|
|
7
|
+
* We build ONE shared palette across all frames (pixel art reuses colours, so a global
|
|
8
|
+
* palette is both faster and lets frames share colour definitions) and emit each frame
|
|
9
|
+
* with run-length-encoded sixel bands. Fully-transparent pixels are left unset so the
|
|
10
|
+
* terminal background shows through (rounded banner corners).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const TRANSPARENT = -1;
|
|
14
|
+
|
|
15
|
+
function boxStats(box) {
|
|
16
|
+
let rMin = 255, rMax = 0, gMin = 255, gMax = 0, bMin = 255, bMax = 0;
|
|
17
|
+
for (const [r, g, b] of box) {
|
|
18
|
+
if (r < rMin) rMin = r; if (r > rMax) rMax = r;
|
|
19
|
+
if (g < gMin) gMin = g; if (g > gMax) gMax = g;
|
|
20
|
+
if (b < bMin) bMin = b; if (b > bMax) bMax = b;
|
|
21
|
+
}
|
|
22
|
+
const dr = rMax - rMin, dg = gMax - gMin, db = bMax - bMin;
|
|
23
|
+
const range = Math.max(dr, dg, db);
|
|
24
|
+
const channel = range === dr ? 0 : range === dg ? 1 : 2;
|
|
25
|
+
return { range, channel };
|
|
26
|
+
}
|
|
27
|
+
function avgColor(box) {
|
|
28
|
+
let r = 0, g = 0, b = 0;
|
|
29
|
+
for (const c of box) { r += c[0]; g += c[1]; b += c[2]; }
|
|
30
|
+
const n = box.length || 1;
|
|
31
|
+
return [Math.round(r / n), Math.round(g / n), Math.round(b / n)];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Median-cut quantisation over a sample of opaque colours. Returns up to maxColors [r,g,b]. */
|
|
35
|
+
export function buildPalette(samples, maxColors = 255) {
|
|
36
|
+
if (samples.length === 0) return [[0, 0, 0]];
|
|
37
|
+
let boxes = [samples];
|
|
38
|
+
while (boxes.length < maxColors) {
|
|
39
|
+
let bi = -1, best = -1;
|
|
40
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
41
|
+
if (boxes[i].length < 2) continue;
|
|
42
|
+
const { range } = boxStats(boxes[i]);
|
|
43
|
+
if (range > best) { best = range; bi = i; }
|
|
44
|
+
}
|
|
45
|
+
if (bi < 0) break;
|
|
46
|
+
const box = boxes[bi];
|
|
47
|
+
const { channel } = boxStats(box);
|
|
48
|
+
box.sort((a, b) => a[channel] - b[channel]);
|
|
49
|
+
const mid = box.length >> 1;
|
|
50
|
+
boxes.splice(bi, 1, box.slice(0, mid), box.slice(mid));
|
|
51
|
+
}
|
|
52
|
+
return boxes.map(avgColor);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Fast rgb->palette-index mapper with a per-colour cache (pixel art has few unique colours). */
|
|
56
|
+
export function makeMapper(palette) {
|
|
57
|
+
const cache = new Map();
|
|
58
|
+
return (r, g, b) => {
|
|
59
|
+
const key = (r << 16) | (g << 8) | b;
|
|
60
|
+
const hit = cache.get(key);
|
|
61
|
+
if (hit !== undefined) return hit;
|
|
62
|
+
let best = 0, bestD = Infinity;
|
|
63
|
+
for (let i = 0; i < palette.length; i++) {
|
|
64
|
+
const p = palette[i];
|
|
65
|
+
const d = (p[0] - r) ** 2 + (p[1] - g) ** 2 + (p[2] - b) ** 2;
|
|
66
|
+
if (d < bestD) { bestD = d; best = i; }
|
|
67
|
+
}
|
|
68
|
+
cache.set(key, best);
|
|
69
|
+
return best;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Collect a colour sample from a raw RGBA frame (every `step`-th opaque pixel). */
|
|
74
|
+
export function sampleColors(rgba, step = 7) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (let i = 0; i < rgba.length; i += 4 * step) {
|
|
77
|
+
if (rgba[i + 3] >= 128) out.push([rgba[i], rgba[i + 1], rgba[i + 2]]);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Encode one RGBA frame to a Sixel string using a shared palette + mapper. */
|
|
83
|
+
export function encodeSixelFrame(rgba, width, height, palette, mapper) {
|
|
84
|
+
// index buffer: palette index per pixel, or TRANSPARENT
|
|
85
|
+
const idx = new Int16Array(width * height);
|
|
86
|
+
for (let p = 0, o = 0; p < idx.length; p++, o += 4) {
|
|
87
|
+
idx[p] = rgba[o + 3] < 128 ? TRANSPARENT : mapper(rgba[o], rgba[o + 1], rgba[o + 2]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let s = '\x1bP0;1;0q'; // DCS; P2=1 => 0-bit pixels stay transparent
|
|
91
|
+
s += `"1;1;${width};${height}`; // raster attributes (1:1 aspect)
|
|
92
|
+
for (let i = 0; i < palette.length; i++) {
|
|
93
|
+
const [r, g, b] = palette[i];
|
|
94
|
+
s += `#${i};2;${Math.round(r / 255 * 100)};${Math.round(g / 255 * 100)};${Math.round(b / 255 * 100)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const used = [];
|
|
98
|
+
for (let by = 0; by < height; by += 6) {
|
|
99
|
+
const bandH = Math.min(6, height - by);
|
|
100
|
+
// which palette indices appear in this band
|
|
101
|
+
used.length = 0;
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
for (let y = by; y < by + bandH; y++) {
|
|
104
|
+
const row = y * width;
|
|
105
|
+
for (let x = 0; x < width; x++) { const c = idx[row + x]; if (c !== TRANSPARENT && !seen.has(c)) { seen.add(c); used.push(c); } }
|
|
106
|
+
}
|
|
107
|
+
for (let u = 0; u < used.length; u++) {
|
|
108
|
+
const ci = used[u];
|
|
109
|
+
if (u > 0) s += '$'; // return to band start, overlay next colour
|
|
110
|
+
s += `#${ci}`;
|
|
111
|
+
let runChar = -1, runLen = 0, line = '';
|
|
112
|
+
for (let x = 0; x < width; x++) {
|
|
113
|
+
let bits = 0;
|
|
114
|
+
for (let k = 0; k < bandH; k++) if (idx[(by + k) * width + x] === ci) bits |= (1 << k);
|
|
115
|
+
const ch = 63 + bits;
|
|
116
|
+
if (ch === runChar) { runLen++; }
|
|
117
|
+
else { if (runLen) line += runLen > 3 ? `!${runLen}${String.fromCharCode(runChar)}` : String.fromCharCode(runChar).repeat(runLen); runChar = ch; runLen = 1; }
|
|
118
|
+
}
|
|
119
|
+
if (runLen) line += runLen > 3 ? `!${runLen}${String.fromCharCode(runChar)}` : String.fromCharCode(runChar).repeat(runLen);
|
|
120
|
+
s += line;
|
|
121
|
+
}
|
|
122
|
+
s += '-'; // graphics newline
|
|
123
|
+
}
|
|
124
|
+
s += '\x1b\\'; // ST
|
|
125
|
+
return s;
|
|
126
|
+
}
|
|
@@ -172,6 +172,7 @@ export function buildLocalSessionOptions(quantLabel = 'q8', coremlAvailable = fa
|
|
|
172
172
|
|
|
173
173
|
const sessionOptions = {
|
|
174
174
|
graphOptimizationLevel: 'all',
|
|
175
|
+
logSeverityLevel: 3, // ERROR — silence ORT's expected "optimized model is machine-specific" warning
|
|
175
176
|
intraOpNumThreads: intraOpThreads,
|
|
176
177
|
interOpNumThreads: interOpThreads,
|
|
177
178
|
executionMode,
|
|
@@ -160,7 +160,11 @@ export function resolveRelationshipTargets(db) {
|
|
|
160
160
|
|
|
161
161
|
resolveAll();
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
if (resolved > 0) {
|
|
164
|
+
console.log(` ✓ Linked ${resolved}/${unresolved.length} references to local definitions`);
|
|
165
|
+
} else {
|
|
166
|
+
console.log(` ${unresolved.length} references resolve to external/library symbols (no local definition to link)`);
|
|
167
|
+
}
|
|
164
168
|
if (ambiguous > 0) {
|
|
165
169
|
console.log(` ⚠ ${ambiguous} ambiguous targets (multiple matches)`);
|
|
166
170
|
}
|
|
@@ -121,6 +121,11 @@ async function main() {
|
|
|
121
121
|
setVerboseMode(true);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// Animated banner (best-effort; interactive TTY only, never in CI / quiet / stdin-fed runs).
|
|
125
|
+
if (!quiet && !help && !filesFromStdin && process.stdout.isTTY && !process.env.CI && !process.env.NO_BANNER && !process.env.SWEET_SEARCH_NO_BANNER) {
|
|
126
|
+
try { const { showBanner } = await import('../banner/render-banner.js'); await showBanner(); } catch { /* non-fatal */ }
|
|
127
|
+
}
|
|
128
|
+
|
|
124
129
|
// Apply late interaction model overrides before any model code runs.
|
|
125
130
|
// Precedence: --no-late-interaction > --late-interaction-model=… > env
|
|
126
131
|
// var (already honoured by LATE_INTERACTION_CONFIG.model at module load) >
|
|
@@ -135,10 +140,6 @@ async function main() {
|
|
|
135
140
|
applyPersistedLiModel(process.env.SWEET_SEARCH_PROJECT_ROOT || process.cwd());
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
log(`${colors.bright}╔═══════════════════════════════════════════════════╗${colors.reset}`, 'bright');
|
|
139
|
-
log(`${colors.bright}║ Sweet Search Codebase Indexer v2.3 (SOTA Dec'25) ║${colors.reset}`, 'bright');
|
|
140
|
-
log(`${colors.bright}╚═══════════════════════════════════════════════════╝${colors.reset}`, 'bright');
|
|
141
|
-
|
|
142
143
|
if (vectorsOnly) {
|
|
143
144
|
log('⚠ WARNING: --vectors-only skips code graph rebuild', 'yellow');
|
|
144
145
|
log(' GraphRAG structural queries will use stale data', 'yellow');
|
|
@@ -333,13 +334,13 @@ Output:
|
|
|
333
334
|
}
|
|
334
335
|
|
|
335
336
|
// =========================================================================
|
|
336
|
-
// PHASE
|
|
337
|
+
// PHASE 1: Code Graph (if not --vectors-only)
|
|
337
338
|
// =========================================================================
|
|
338
339
|
let graphStats = { entities: 0, relationships: 0 };
|
|
339
340
|
let hcgsPromise = null;
|
|
340
341
|
|
|
341
342
|
if (!vectorsOnly) {
|
|
342
|
-
const graphResult = await runPhase('Code Graph
|
|
343
|
+
const graphResult = await runPhase('Code Graph', buildCodeGraphWithHCGSPhase, {
|
|
343
344
|
allFiles,
|
|
344
345
|
filesToIndex,
|
|
345
346
|
dryRun,
|
|
@@ -396,7 +396,7 @@ function diversityFirstPermutationRowids(filePaths) {
|
|
|
396
396
|
// =============================================================================
|
|
397
397
|
|
|
398
398
|
export async function incrementalUpdateHNSW(dbPath, changedFiles, dryRun = false) {
|
|
399
|
-
log('\n━━━ Phase
|
|
399
|
+
log('\n━━━ Phase 4: HNSW Index (Incremental) ━━━', 'bright');
|
|
400
400
|
|
|
401
401
|
if (dryRun) {
|
|
402
402
|
log('DRY RUN: Skipping HNSW incremental update', 'magenta');
|
|
@@ -510,7 +510,7 @@ export async function incrementalUpdateHNSW(dbPath, changedFiles, dryRun = false
|
|
|
510
510
|
// =============================================================================
|
|
511
511
|
|
|
512
512
|
export async function buildHNSWIndex(dbPath, dryRun = false) {
|
|
513
|
-
log('\n━━━ Phase
|
|
513
|
+
log('\n━━━ Phase 4: HNSW Index ━━━', 'bright');
|
|
514
514
|
|
|
515
515
|
if (dryRun) {
|
|
516
516
|
log('DRY RUN: Skipping HNSW index', 'magenta');
|
|
@@ -650,7 +650,7 @@ export async function buildHNSWIndex(dbPath, dryRun = false) {
|
|
|
650
650
|
});
|
|
651
651
|
fsyncFile(sidecarPath);
|
|
652
652
|
fsyncDirectory(path.dirname(checkpointPath));
|
|
653
|
-
log(` checkpoint: ${added}/${totalVectors} vectors`, 'dim');
|
|
653
|
+
if (process.env.DEBUG) log(` checkpoint: ${added}/${totalVectors} vectors`, 'dim');
|
|
654
654
|
}
|
|
655
655
|
lastCheckpointTime = Date.now();
|
|
656
656
|
vectorsSinceCheckpoint = 0;
|
|
@@ -705,7 +705,7 @@ export async function buildLateInteractionIndex(chunks, dryRun = false, filesToR
|
|
|
705
705
|
segmentSize = null, // override SSLX-v3 segment threshold (default 10k)
|
|
706
706
|
projectRoot, // honored by LI skip policy for .sweet-search.config.json excludes
|
|
707
707
|
} = options;
|
|
708
|
-
log('\n━━━ Phase
|
|
708
|
+
log('\n━━━ Phase 3: Late Interaction Index (LateOn-Code) ━━━', 'bright');
|
|
709
709
|
|
|
710
710
|
if (dryRun) {
|
|
711
711
|
log('DRY RUN: Skipping late interaction index', 'magenta');
|
|
@@ -643,7 +643,7 @@ export async function chunkFiles(files) {
|
|
|
643
643
|
try {
|
|
644
644
|
const enriched = await enrichChunksFromGraph(allChunks, ASTChunker);
|
|
645
645
|
if (enriched > 0) {
|
|
646
|
-
log(`✓
|
|
646
|
+
log(`✓ Added scope/import context to ${enriched} code chunks`, 'green');
|
|
647
647
|
}
|
|
648
648
|
} catch (err) {
|
|
649
649
|
log(`⚠ Chunk enrichment skipped: ${err.message}`, 'yellow');
|
|
@@ -109,32 +109,79 @@ export function isVerboseMode() {
|
|
|
109
109
|
return verboseMode;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Progress rendering — a live region of animated, in-place bars.
|
|
114
|
+
//
|
|
115
|
+
// On a TTY (verbose included), each phase's bar animates in place via cursor
|
|
116
|
+
// moves + erase-to-EOL, with smooth 1/8-block fill. Multiple bars can run at
|
|
117
|
+
// once (e.g. Embedding + Late Interaction in parallel) — they share one pinned
|
|
118
|
+
// region at the bottom and update independently. While bars are live, log()
|
|
119
|
+
// prints its line above the region and redraws the bars below, so diagnostics
|
|
120
|
+
// never split a bar. The region "commits" (stays on screen) once every bar in
|
|
121
|
+
// it has reached 100%. Non-TTY (pipes / CI) falls back to throttled newlines.
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
const BAR_WIDTH = 30;
|
|
124
|
+
const LABEL_COL = 17; // pad "Label:" to this width so every bar's [ ] aligns
|
|
125
|
+
const SUB_BLOCKS = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']; // eighth-block partial fills
|
|
126
|
+
const CLEAR_EOL = '\x1b[K';
|
|
127
|
+
const liveBars = new Map(); // label -> { current, total }; insertion order = display order
|
|
128
|
+
let regionLines = 0; // bar lines currently pinned at the bottom (TTY)
|
|
129
|
+
let lastLoggedPercent = {};
|
|
130
|
+
|
|
131
|
+
function renderBar(current, total, label) {
|
|
132
|
+
const ratio = total > 0 ? Math.max(0, Math.min(1, current / total)) : 1;
|
|
133
|
+
const eighths = Math.round(ratio * BAR_WIDTH * 8);
|
|
134
|
+
const full = Math.floor(eighths / 8);
|
|
135
|
+
const partial = SUB_BLOCKS[eighths % 8];
|
|
136
|
+
const bar = '█'.repeat(full) + partial;
|
|
137
|
+
const empty = '░'.repeat(Math.max(0, BAR_WIDTH - full - (partial ? 1 : 0)));
|
|
138
|
+
const head = `${label}:`.padEnd(LABEL_COL); // right border aligns across phases
|
|
139
|
+
const pct = (ratio * 100).toFixed(1).padStart(5);
|
|
140
|
+
return `${colors.cyan}${head}[${bar}${empty}] ${pct}% (${current}/${total})${colors.reset}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function drawRegion() {
|
|
144
|
+
let out = regionLines > 0 ? `\x1b[${regionLines}A\r` : '\r';
|
|
145
|
+
for (const [label, b] of liveBars) out += renderBar(b.current, b.total, label) + CLEAR_EOL + '\n';
|
|
146
|
+
process.stdout.write(out);
|
|
147
|
+
regionLines = liveBars.size;
|
|
148
|
+
}
|
|
149
|
+
|
|
112
150
|
export function log(message, color = 'reset') {
|
|
113
151
|
if (quietMode) return;
|
|
114
|
-
|
|
152
|
+
const line = `${colors[color]}${message}${colors.reset}`;
|
|
153
|
+
if (regionLines > 0 && process.stdout.isTTY) {
|
|
154
|
+
// Print the line above the pinned bars, then redraw the bars below it.
|
|
155
|
+
let out = `\x1b[${regionLines}A\r${line}${CLEAR_EOL}\n`;
|
|
156
|
+
for (const [label, b] of liveBars) out += renderBar(b.current, b.total, label) + CLEAR_EOL + '\n';
|
|
157
|
+
process.stdout.write(out);
|
|
158
|
+
} else {
|
|
159
|
+
console.log(line);
|
|
160
|
+
}
|
|
115
161
|
}
|
|
116
162
|
|
|
117
|
-
let lastLoggedPercent = {};
|
|
118
|
-
|
|
119
163
|
export function logProgress(current, total, label) {
|
|
120
164
|
if (quietMode) return;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const empty = '░'.repeat(30 - bar.length);
|
|
125
|
-
// In verbose mode or non-TTY, use newlines so output isn't swallowed by pipes.
|
|
126
|
-
// Throttle to every ~2% to avoid flooding.
|
|
127
|
-
if (verboseMode || !process.stdout.isTTY) {
|
|
165
|
+
if (!process.stdout.isTTY) {
|
|
166
|
+
// Pipes / CI: throttle to ~2% and emit newlines so output isn't swallowed.
|
|
167
|
+
const percentNum = total > 0 ? (current / total) * 100 : 100;
|
|
128
168
|
const lastPct = lastLoggedPercent[label] || 0;
|
|
129
|
-
if (percentNum - lastPct >= 2 || current
|
|
169
|
+
if (percentNum - lastPct >= 2 || current >= total || current <= 1) {
|
|
130
170
|
lastLoggedPercent[label] = percentNum;
|
|
131
|
-
console.log(
|
|
132
|
-
}
|
|
133
|
-
} else {
|
|
134
|
-
process.stdout.write(`\r${colors.cyan}${label}: [${bar}${empty}] ${percent}% (${current}/${total})${colors.reset}`);
|
|
135
|
-
if (current === total) {
|
|
136
|
-
process.stdout.write('\n');
|
|
171
|
+
console.log(renderBar(current, total, label));
|
|
137
172
|
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Interactive TTY: update this bar in the live region and redraw.
|
|
176
|
+
liveBars.set(label, { current, total });
|
|
177
|
+
drawRegion();
|
|
178
|
+
// Once every live bar is complete, commit the region (leave it on screen).
|
|
179
|
+
let allDone = true;
|
|
180
|
+
for (const b of liveBars.values()) if (b.current < b.total) { allDone = false; break; }
|
|
181
|
+
if (allDone) {
|
|
182
|
+
for (const k of liveBars.keys()) lastLoggedPercent[k] = 0;
|
|
183
|
+
liveBars.clear();
|
|
184
|
+
regionLines = 0;
|
|
138
185
|
}
|
|
139
186
|
}
|
|
140
187
|
|
|
@@ -192,6 +192,7 @@ export function buildSessionOptions(modelId, suffix, coremlAvailable = false, ru
|
|
|
192
192
|
?? parseInt(process.env.SWEET_SEARCH_ORT_INTER_OP_THREADS || '1', 10);
|
|
193
193
|
const opts = {
|
|
194
194
|
graphOptimizationLevel: 'all',
|
|
195
|
+
logSeverityLevel: 3, // ERROR — silence ORT's expected "optimized model is machine-specific" warning
|
|
195
196
|
intraOpNumThreads: runtimeOptions.intraOpThreads ?? bestIntraOpThreads(runtimeOptions),
|
|
196
197
|
interOpNumThreads: interOpThreads,
|
|
197
198
|
executionMode,
|
|
@@ -72,12 +72,17 @@ async function initWasm() {
|
|
|
72
72
|
|
|
73
73
|
initDone = true;
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
// Load-time tier diagnostic — gated behind DEBUG so it doesn't precede the
|
|
76
|
+
// banner / clutter normal output (the active tier is also shown in `init`'s
|
|
77
|
+
// summary as "MaxSim: …"). Set DEBUG=1 to surface it.
|
|
78
|
+
if (process.env.DEBUG) {
|
|
79
|
+
if (nativeMaxsim) {
|
|
80
|
+
console.error('[MaxSim] Tier 1: Native Rust + Rayon (parallel SIMD)');
|
|
81
|
+
} else if (maxsimExports || wasmExports?.maxsim_f32) {
|
|
82
|
+
console.error('[MaxSim] Tier 2: WASM SIMD f32x4');
|
|
83
|
+
} else {
|
|
84
|
+
console.error('[MaxSim] Tier 3: JS fallback');
|
|
85
|
+
}
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
return true;
|
|
@@ -193,6 +193,7 @@ async function loadModel() {
|
|
|
193
193
|
const { getOptimizedGraphPath } = await import('../infrastructure/onnx-session-utils.js');
|
|
194
194
|
const session = await ort.InferenceSession.create(onnxPath, {
|
|
195
195
|
executionProviders: ['cpu'],
|
|
196
|
+
logSeverityLevel: 3, // ERROR — silence ORT's expected "optimized model is machine-specific" warning
|
|
196
197
|
intraOpNumThreads: lateInteractionRuntimeConfig.intraOpThreads ?? bestIntraOpThreads(),
|
|
197
198
|
interOpNumThreads: 1,
|
|
198
199
|
optimizedModelFilePath: getOptimizedGraphPath(modelConfig.hfId, 'lateon'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sweet-search",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.7",
|
|
4
4
|
"description": "Sweet Search - SOTA Hybrid Code Search Engine with WASM CatBoost Query Router, Semantic/Lexical/Structural Search, and Multilingual Support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "core/search/sweet-search.js",
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
"author": "Marko Sladojevic <marko@panonit.com> (https://panonit.com)",
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
16
|
-
"url": "git+https://github.com/
|
|
16
|
+
"url": "git+https://github.com/mrsladoje/sweet-search.git"
|
|
17
17
|
},
|
|
18
18
|
"bugs": {
|
|
19
|
-
"url": "https://github.com/
|
|
19
|
+
"url": "https://github.com/mrsladoje/sweet-search/issues"
|
|
20
20
|
},
|
|
21
|
-
"homepage": "https://github.com/
|
|
21
|
+
"homepage": "https://github.com/mrsladoje/sweet-search#readme",
|
|
22
22
|
"keywords": [
|
|
23
23
|
"sweet-search",
|
|
24
24
|
"code-search",
|
|
@@ -50,8 +50,11 @@
|
|
|
50
50
|
"core/vector-store/",
|
|
51
51
|
"core/query/",
|
|
52
52
|
"core/skills/",
|
|
53
|
+
"core/banner/",
|
|
54
|
+
"assets/banner/",
|
|
53
55
|
"mcp/",
|
|
54
56
|
"scripts/benchmark-harness.js",
|
|
57
|
+
"scripts/postinstall-banner.js",
|
|
55
58
|
"scripts/init.js",
|
|
56
59
|
"scripts/uninstall.js",
|
|
57
60
|
"scripts/verify-runtime.js",
|
|
@@ -78,6 +81,8 @@
|
|
|
78
81
|
],
|
|
79
82
|
"scripts": {
|
|
80
83
|
"init": "node scripts/init.js",
|
|
84
|
+
"postinstall": "node scripts/postinstall-banner.js",
|
|
85
|
+
"bake:banner": "node scripts/bake-banner.mjs",
|
|
81
86
|
"build:assets": "node scripts/generate-asset-manifest.js",
|
|
82
87
|
"lint": "eslint core/",
|
|
83
88
|
"build": "node -e \"import('./core/search/sweet-search.js')\" && echo 'Build OK'",
|
|
@@ -152,17 +157,18 @@
|
|
|
152
157
|
"eslint": "^9.39.4",
|
|
153
158
|
"fast-check": "^4.5.3",
|
|
154
159
|
"p-map": "^7.0.4",
|
|
160
|
+
"puppeteer-core": "^25.1.0",
|
|
155
161
|
"typescript": "^5.9.3",
|
|
156
162
|
"vitest": "^4.0.16"
|
|
157
163
|
},
|
|
158
164
|
"optionalDependencies": {
|
|
159
165
|
"usearch": "^2.21.4",
|
|
160
|
-
"@sweet-search/native-darwin-arm64": "2.5.
|
|
161
|
-
"@sweet-search/native-darwin-x64": "2.5.
|
|
162
|
-
"@sweet-search/native-linux-arm64-gnu": "2.5.
|
|
163
|
-
"@sweet-search/native-linux-arm64-gnu-cuda": "2.5.
|
|
164
|
-
"@sweet-search/native-linux-x64-gnu": "2.5.
|
|
165
|
-
"@sweet-search/native-linux-x64-gnu-cuda": "2.5.
|
|
166
|
+
"@sweet-search/native-darwin-arm64": "2.5.7",
|
|
167
|
+
"@sweet-search/native-darwin-x64": "2.5.7",
|
|
168
|
+
"@sweet-search/native-linux-arm64-gnu": "2.5.7",
|
|
169
|
+
"@sweet-search/native-linux-arm64-gnu-cuda": "2.5.7",
|
|
170
|
+
"@sweet-search/native-linux-x64-gnu": "2.5.7",
|
|
171
|
+
"@sweet-search/native-linux-x64-gnu-cuda": "2.5.7"
|
|
166
172
|
},
|
|
167
173
|
"engines": {
|
|
168
174
|
"node": ">=18.0.0"
|