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.
@@ -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
- console.log(` ✓ Resolved ${resolved}/${unresolved.length} relationships`);
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 3: Code Graph + HCGS Preparation (if not --vectors-only)
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 + HCGS Prep', buildCodeGraphWithHCGSPhase, {
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 3: HNSW Index (Incremental) ━━━', 'bright');
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 3: HNSW Index ━━━', 'bright');
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 4: Late Interaction Index (LateOn-Code) ━━━', 'bright');
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(`✓ Enriched ${enriched}/${allChunks.length} chunks with scope/import context`, 'green');
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
- console.log(`${colors[color]}${message}${colors.reset}`);
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
- const percentNum = (current / total) * 100;
122
- const percent = percentNum.toFixed(1);
123
- const bar = '█'.repeat(Math.floor(current / total * 30));
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 === total || current <= 1) {
169
+ if (percentNum - lastPct >= 2 || current >= total || current <= 1) {
130
170
  lastLoggedPercent[label] = percentNum;
131
- console.log(`${colors.cyan}${label}: [${bar}${empty}] ${percent}% (${current}/${total})${colors.reset}`);
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
- if (nativeMaxsim) {
76
- console.error('[MaxSim] Tier 1: Native Rust + Rayon (parallel SIMD)');
77
- } else if (maxsimExports || wasmExports?.maxsim_f32) {
78
- console.error('[MaxSim] Tier 2: WASM SIMD f32x4');
79
- } else {
80
- console.error('[MaxSim] Tier 3: JS fallback');
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.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/panonitorg/sweet-search.git"
16
+ "url": "git+https://github.com/mrsladoje/sweet-search.git"
17
17
  },
18
18
  "bugs": {
19
- "url": "https://github.com/panonitorg/sweet-search/issues"
19
+ "url": "https://github.com/mrsladoje/sweet-search/issues"
20
20
  },
21
- "homepage": "https://github.com/panonitorg/sweet-search#readme",
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.5",
161
- "@sweet-search/native-darwin-x64": "2.5.5",
162
- "@sweet-search/native-linux-arm64-gnu": "2.5.5",
163
- "@sweet-search/native-linux-arm64-gnu-cuda": "2.5.5",
164
- "@sweet-search/native-linux-x64-gnu": "2.5.5",
165
- "@sweet-search/native-linux-x64-gnu-cuda": "2.5.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"