sweet-search 2.5.4 → 2.5.6
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/NOTICE +5 -5
- package/README.md +754 -0
- 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/indexing/index-codebase-v21.js +5 -0
- package/core/indexing/indexer-ann.js +1 -1
- package/core/indexing/indexer-utils.js +49 -17
- package/core/infrastructure/simd-distance.js +11 -6
- package/core/search/context-expander.js +10 -1
- package/core/search/search-cli.js +1 -1
- package/core/search/search-pattern-planner.js +1 -1
- package/core/search/search-trace.js +1 -1
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +16 -3
- package/eval/agent-read-workflows/bin/ss-search +1 -1
- package/mcp/server.js +1 -1
- package/package.json +17 -11
- package/scripts/init.js +7 -1
- package/scripts/postinstall-banner.js +46 -0
|
Binary file
|
|
@@ -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
|
+
}
|
|
@@ -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) >
|
|
@@ -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;
|
|
@@ -109,32 +109,64 @@ export function isVerboseMode() {
|
|
|
109
109
|
return verboseMode;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Progress rendering — an in-place "sticky" bar that animates as a phase runs.
|
|
114
|
+
//
|
|
115
|
+
// On a TTY (verbose or not) the bar redraws on a single line via carriage return
|
|
116
|
+
// + erase-to-EOL, with smooth 1/8-block fill. While a bar is active, log() pins it:
|
|
117
|
+
// it clears the bar, prints the log line above, then redraws the bar below — so
|
|
118
|
+
// interleaved diagnostics (e.g. the HNSW "checkpoint:" line) never split the bar.
|
|
119
|
+
// Non-TTY (pipes / CI) falls back to throttled newlines so nothing is swallowed.
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
const BAR_WIDTH = 30;
|
|
122
|
+
const LABEL_COL = 17; // pad "Label:" to this width so every bar's [ ] aligns
|
|
123
|
+
const SUB_BLOCKS = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']; // eighth-block partial fills
|
|
124
|
+
const CLEAR_EOL = '\x1b[K';
|
|
125
|
+
let activeBar = null; // last-rendered bar string while a phase is in progress (TTY only)
|
|
126
|
+
let lastLoggedPercent = {};
|
|
127
|
+
|
|
128
|
+
function renderBar(current, total, label) {
|
|
129
|
+
const ratio = total > 0 ? Math.max(0, Math.min(1, current / total)) : 1;
|
|
130
|
+
const eighths = Math.round(ratio * BAR_WIDTH * 8);
|
|
131
|
+
const full = Math.floor(eighths / 8);
|
|
132
|
+
const partial = SUB_BLOCKS[eighths % 8];
|
|
133
|
+
const bar = '█'.repeat(full) + partial;
|
|
134
|
+
const empty = '░'.repeat(Math.max(0, BAR_WIDTH - full - (partial ? 1 : 0)));
|
|
135
|
+
const head = `${label}:`.padEnd(LABEL_COL); // right border aligns across phases
|
|
136
|
+
const pct = (ratio * 100).toFixed(1).padStart(5);
|
|
137
|
+
return `${colors.cyan}${head}[${bar}${empty}] ${pct}% (${current}/${total})${colors.reset}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
112
140
|
export function log(message, color = 'reset') {
|
|
113
141
|
if (quietMode) return;
|
|
114
|
-
|
|
142
|
+
const line = `${colors[color]}${message}${colors.reset}`;
|
|
143
|
+
if (activeBar && process.stdout.isTTY) {
|
|
144
|
+
// Pin the bar: clear it, print the log line above, redraw the bar below.
|
|
145
|
+
process.stdout.write(`\r${CLEAR_EOL}${line}\n${activeBar}${CLEAR_EOL}`);
|
|
146
|
+
} else {
|
|
147
|
+
console.log(line);
|
|
148
|
+
}
|
|
115
149
|
}
|
|
116
150
|
|
|
117
|
-
let lastLoggedPercent = {};
|
|
118
|
-
|
|
119
151
|
export function logProgress(current, total, label) {
|
|
120
152
|
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) {
|
|
153
|
+
if (!process.stdout.isTTY) {
|
|
154
|
+
// Pipes / CI: throttle to ~2% and emit newlines so output isn't swallowed.
|
|
155
|
+
const percentNum = total > 0 ? (current / total) * 100 : 100;
|
|
128
156
|
const lastPct = lastLoggedPercent[label] || 0;
|
|
129
|
-
if (percentNum - lastPct >= 2 || current
|
|
157
|
+
if (percentNum - lastPct >= 2 || current >= total || current <= 1) {
|
|
130
158
|
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');
|
|
159
|
+
console.log(renderBar(current, total, label));
|
|
137
160
|
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Interactive TTY: animate the bar in place.
|
|
164
|
+
activeBar = renderBar(current, total, label);
|
|
165
|
+
process.stdout.write(`\r${activeBar}${CLEAR_EOL}`);
|
|
166
|
+
if (current >= total) {
|
|
167
|
+
process.stdout.write('\n');
|
|
168
|
+
activeBar = null;
|
|
169
|
+
lastLoggedPercent[label] = 0;
|
|
138
170
|
}
|
|
139
171
|
}
|
|
140
172
|
|
|
@@ -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;
|
|
@@ -1677,8 +1677,17 @@ function resolveSubMode(format) {
|
|
|
1677
1677
|
// space, and small-N entropy is dominated by the 1/log(n) denominator and
|
|
1678
1678
|
// stops being a reliable distribution-width signal.
|
|
1679
1679
|
|
|
1680
|
+
// Preview-tier budget: 3000 (was 4000 until 2026-06-11). The 4-model budget
|
|
1681
|
+
// sweep (DeepSeek/MiMo/GPT-5.5-codex/Opus-CC, 12 dev probes, paired vs 4k)
|
|
1682
|
+
// found 3k keeps every accuracy/usefulness metric flat-to-up with zero
|
|
1683
|
+
// call-compensation, and cuts realized cost −11–15% on the flagship cells.
|
|
1684
|
+
// Below 3k, flagship models re-buy the trimmed context with extra calls
|
|
1685
|
+
// (Opus calls Δ: 3k −0.08 → 2.8k +0.33 → 2.5k +0.67 → 2k +0.83), erasing
|
|
1686
|
+
// the savings — 3k is the floor. SWEET_SEARCH_PREVIEW_BUDGET overrides for
|
|
1687
|
+
// experiments; full/xl escalation tiers are unchanged.
|
|
1688
|
+
const PREVIEW_TIER_BUDGET = Number(process.env.SWEET_SEARCH_PREVIEW_BUDGET || '') || 3000;
|
|
1680
1689
|
const BUDGET_TIERS = {
|
|
1681
|
-
preview: { subMode: 'agent_preview', budget:
|
|
1690
|
+
preview: { subMode: 'agent_preview', budget: PREVIEW_TIER_BUDGET },
|
|
1682
1691
|
full: { subMode: 'agent_full', budget: 8000 },
|
|
1683
1692
|
xl: { subMode: 'agent_full_xl', budget: 12000 },
|
|
1684
1693
|
};
|
|
@@ -52,7 +52,7 @@ Options:
|
|
|
52
52
|
--fusion <type> Legacy: cc or rrf (ignored for hybrid - always uses robust CC fusion)
|
|
53
53
|
--late-interaction Enable late interaction reranking (if index available)
|
|
54
54
|
--late-interaction-model=ID Use specific model (lateon-code or lateon-code-edge)
|
|
55
|
-
--agent Agent mode: self-contained code blocks. Auto-picks
|
|
55
|
+
--agent Agent mode: self-contained code blocks. Auto-picks 3k/8k/12k
|
|
56
56
|
tier from score-distribution signals (top-1 dominance,
|
|
57
57
|
entropy, candidate-pool breadth) — no need to choose a tier.
|
|
58
58
|
--agent-preview Force the 4k preview tier (rarely needed; --agent auto-picks)
|
|
@@ -268,7 +268,7 @@ export async function generateRegexMatches(searcher, regex, searchDir, options =
|
|
|
268
268
|
const literalStart = performance.now();
|
|
269
269
|
|
|
270
270
|
const sparseForPrefilter = globs.length === 0 ? ensureSparseGramIndex(searcher, options) : null;
|
|
271
|
-
const prefilterFiles = sparseForPrefilter ?
|
|
271
|
+
const prefilterFiles = sparseForPrefilter ? getSparseGramAllFilesWithOverlay(searcher, sparseForPrefilter, options) : null;
|
|
272
272
|
|
|
273
273
|
if (prefilterFiles && prefilterFiles.length > 0) {
|
|
274
274
|
const combined = new Set();
|
|
@@ -83,7 +83,7 @@ Options:
|
|
|
83
83
|
--in <file> Disambiguate symbols by indexed file path
|
|
84
84
|
--query <hint> Natural-language hint used only for structural ranking
|
|
85
85
|
--depth <n> Impact depth, 1-4 (default: 3)
|
|
86
|
-
--budget <n> Token budget, 1000-16000 (default: adaptive
|
|
86
|
+
--budget <n> Token budget, 1000-16000 (default: adaptive 3k/8k/12k)
|
|
87
87
|
--json Output structured JSON
|
|
88
88
|
--format <fmt> plain (no banner) or json
|
|
89
89
|
--no-banner Suppress the identity line
|
|
@@ -126,6 +126,9 @@ async function cmdFind(args) {
|
|
|
126
126
|
process.stderr.write('Usage: ss-find "<query>" --regex "<regex>" [--full|--xl] [-k N]\n');
|
|
127
127
|
process.exit(2);
|
|
128
128
|
}
|
|
129
|
+
// Budget-sweep experiment hook: lets the bench pin the response token budget
|
|
130
|
+
// per-process without changing the agent-visible tool surface.
|
|
131
|
+
const envFindBudget = Number(process.env.SS_SMOKE_FIND_BUDGET || '') || null;
|
|
129
132
|
const effectiveRegex = regex || '';
|
|
130
133
|
const s = await getSweetSearch();
|
|
131
134
|
if (!s.hasLateInteractionIndex) {
|
|
@@ -136,6 +139,7 @@ async function cmdFind(args) {
|
|
|
136
139
|
regex: effectiveRegex || `\\b\\w+\\b`,
|
|
137
140
|
k,
|
|
138
141
|
format,
|
|
142
|
+
...(envFindBudget ? { tokenBudget: envFindBudget } : {}),
|
|
139
143
|
});
|
|
140
144
|
|
|
141
145
|
// Header (visible to agent)
|
|
@@ -212,7 +216,7 @@ async function cmdAgentSearch(args) {
|
|
|
212
216
|
// Main sweet-search auto/CatBoost search with token-budgeted agent packaging.
|
|
213
217
|
//
|
|
214
218
|
// Usage:
|
|
215
|
-
// ss-search "<query>" → format=agent (auto-pick
|
|
219
|
+
// ss-search "<query>" → format=agent (auto-pick 3k/8k/12k)
|
|
216
220
|
// ss-search "<query>" --full → force 8k (rarely needed; default auto-picks)
|
|
217
221
|
// ss-search "<query>" --xl → force 12k (rarely needed; default auto-picks)
|
|
218
222
|
// ss-search "<query>" -k 5 → top-K results
|
|
@@ -240,7 +244,10 @@ async function cmdAgentSearch(args) {
|
|
|
240
244
|
process.exit(1);
|
|
241
245
|
}
|
|
242
246
|
|
|
243
|
-
|
|
247
|
+
// Budget-sweep experiment hook: per-request explicit budget (overrides the
|
|
248
|
+
// auto-tier on the warm server; flows as the `budget` URL param).
|
|
249
|
+
const envSearchBudget = Number(process.env.SS_SMOKE_SEARCH_BUDGET || '') || null;
|
|
250
|
+
const response = await queryServer(query, { topK: k, mode, format, ...(envSearchBudget ? { tokenBudget: envSearchBudget } : {}) });
|
|
244
251
|
if (response?.error) {
|
|
245
252
|
process.stderr.write(`[ss-search] server error: ${response.error}\n`);
|
|
246
253
|
process.exit(1);
|
|
@@ -383,7 +390,11 @@ async function cmdSemantic(args) {
|
|
|
383
390
|
process.stderr.write('Usage: ss-semantic <file> "<question>" [--max-tokens N]\n');
|
|
384
391
|
process.exit(2);
|
|
385
392
|
}
|
|
386
|
-
|
|
393
|
+
// Default 600 (was 800) per the 2026-06 budget sweep — scaled with the 3k
|
|
394
|
+
// preview tier. Env hook overrides the default for sweeps; an explicit
|
|
395
|
+
// --max-tokens flag from the agent always wins.
|
|
396
|
+
const maxTokens = +parseFlag(args.slice(2), '--max-tokens',
|
|
397
|
+
Number(process.env.SS_SMOKE_SEMANTIC_MAXTOKENS || '') || 600);
|
|
387
398
|
const { readSemantic } = await import(path.join(REPO_ROOT, 'core/search/search-read-semantic.js'));
|
|
388
399
|
const r = await readSemantic({
|
|
389
400
|
path: file, query, projectRoot: PROJECT_ROOT,
|
|
@@ -423,7 +434,9 @@ async function cmdTrace(args) {
|
|
|
423
434
|
if (file) opts.filePath = file;
|
|
424
435
|
if (queryHint) opts.queryHint = queryHint;
|
|
425
436
|
if (depth != null) opts.maxDepth = +depth;
|
|
437
|
+
// Budget-sweep experiment hook: env sets the default; explicit --budget wins.
|
|
426
438
|
if (budget != null) opts.tokenBudget = +budget;
|
|
439
|
+
else if (Number(process.env.SS_SMOKE_TRACE_BUDGET || '') > 0) opts.tokenBudget = Number(process.env.SS_SMOKE_TRACE_BUDGET);
|
|
427
440
|
|
|
428
441
|
const response = traceSymbol(symbol, opts);
|
|
429
442
|
if (json) process.stdout.write(JSON.stringify(response, null, 2) + '\n');
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# semantic / hybrid / structural) with auto-tier budget by default.
|
|
5
5
|
#
|
|
6
6
|
# Usage:
|
|
7
|
-
# ss-search "<query>" # auto-picks
|
|
7
|
+
# ss-search "<query>" # auto-picks 3k / 8k / 12k from signals
|
|
8
8
|
# ss-search "<query>" --full # force 8k (rarely needed; default auto-picks)
|
|
9
9
|
# ss-search "<query>" --xl # force 12k (rarely needed; default auto-picks)
|
|
10
10
|
# ss-search "<query>" -k N # top-K (default 5)
|
package/mcp/server.js
CHANGED
|
@@ -167,7 +167,7 @@ server.registerTool('trace', {
|
|
|
167
167
|
maxDepth: z.number().int().min(1).max(4).default(3).optional()
|
|
168
168
|
.describe('Maximum transitive impact depth (default: 3, capped at 4)'),
|
|
169
169
|
tokenBudget: z.number().int().min(1000).max(16000).optional()
|
|
170
|
-
.describe('Optional token budget. Omit for adaptive
|
|
170
|
+
.describe('Optional token budget. Omit for adaptive 3k/8k/12k selection.'),
|
|
171
171
|
},
|
|
172
172
|
outputSchema: TraceOutputSchema,
|
|
173
173
|
annotations: {
|