squeezr-ai 1.81.2 → 1.99.2

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.
Files changed (69) hide show
  1. package/bin/squeezr.js +82 -1
  2. package/dist/__tests__/bench.test.d.ts +1 -0
  3. package/dist/__tests__/bench.test.js +55 -0
  4. package/dist/__tests__/cacheAligner.test.d.ts +1 -0
  5. package/dist/__tests__/cacheAligner.test.js +75 -0
  6. package/dist/__tests__/codeStructure.test.d.ts +1 -0
  7. package/dist/__tests__/codeStructure.test.js +74 -0
  8. package/dist/__tests__/compressor.test.js +5 -1
  9. package/dist/__tests__/contentRouter.test.d.ts +1 -0
  10. package/dist/__tests__/contentRouter.test.js +76 -0
  11. package/dist/__tests__/controlEndpointGuard.test.d.ts +1 -0
  12. package/dist/__tests__/controlEndpointGuard.test.js +61 -0
  13. package/dist/__tests__/deterministic.test.js +13 -0
  14. package/dist/__tests__/doctor.test.d.ts +1 -0
  15. package/dist/__tests__/doctor.test.js +86 -0
  16. package/dist/__tests__/jsonCrush.test.d.ts +1 -0
  17. package/dist/__tests__/jsonCrush.test.js +168 -0
  18. package/dist/__tests__/learn.test.d.ts +1 -0
  19. package/dist/__tests__/learn.test.js +159 -0
  20. package/dist/__tests__/outputSavings.test.d.ts +1 -0
  21. package/dist/__tests__/outputSavings.test.js +49 -0
  22. package/dist/__tests__/outputShaper.test.d.ts +1 -0
  23. package/dist/__tests__/outputShaper.test.js +216 -0
  24. package/dist/__tests__/relevance.test.d.ts +1 -0
  25. package/dist/__tests__/relevance.test.js +66 -0
  26. package/dist/__tests__/secureKeyFile.test.d.ts +1 -0
  27. package/dist/__tests__/secureKeyFile.test.js +27 -0
  28. package/dist/__tests__/serverBinding.test.d.ts +1 -0
  29. package/dist/__tests__/serverBinding.test.js +18 -0
  30. package/dist/__tests__/statsOutput.test.d.ts +1 -0
  31. package/dist/__tests__/statsOutput.test.js +46 -0
  32. package/dist/__tests__/textCrusher.test.d.ts +1 -0
  33. package/dist/__tests__/textCrusher.test.js +133 -0
  34. package/dist/bench.d.ts +45 -0
  35. package/dist/bench.js +114 -0
  36. package/dist/cacheAligner.d.ts +34 -0
  37. package/dist/cacheAligner.js +73 -0
  38. package/dist/codexMitm.d.ts +1 -0
  39. package/dist/codexMitm.js +26 -2
  40. package/dist/compressor.js +42 -4
  41. package/dist/config.d.ts +6 -0
  42. package/dist/config.js +25 -0
  43. package/dist/contentRouter.d.ts +30 -0
  44. package/dist/contentRouter.js +112 -0
  45. package/dist/dashboard.d.ts +1 -1
  46. package/dist/dashboard.js +26 -7
  47. package/dist/deterministic.d.ts +4 -1
  48. package/dist/deterministic.js +43 -10
  49. package/dist/doctor.d.ts +38 -0
  50. package/dist/doctor.js +113 -0
  51. package/dist/index.js +4 -1
  52. package/dist/jsonCrush.d.ts +34 -0
  53. package/dist/jsonCrush.js +177 -0
  54. package/dist/learn.d.ts +65 -0
  55. package/dist/learn.js +244 -0
  56. package/dist/outputSavings.d.ts +20 -0
  57. package/dist/outputSavings.js +52 -0
  58. package/dist/outputShaper.d.ts +71 -0
  59. package/dist/outputShaper.js +155 -0
  60. package/dist/relevance.d.ts +27 -0
  61. package/dist/relevance.js +78 -0
  62. package/dist/server.js +106 -11
  63. package/dist/stats.d.ts +17 -0
  64. package/dist/stats.js +55 -0
  65. package/dist/systemPrompt.js +3 -2
  66. package/dist/textCrusher.d.ts +35 -0
  67. package/dist/textCrusher.js +160 -0
  68. package/package.json +69 -69
  69. package/squeezr.toml +15 -1
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { tokenize, bm25Scores, rankByRelevance } from '../relevance.js';
3
+ describe('tokenize', () => {
4
+ it('lowercases and splits on non-alphanumerics', () => {
5
+ expect(tokenize('Foo.Bar(baz)')).toEqual(['foo', 'bar', 'baz']);
6
+ });
7
+ it('drops 1-char noise tokens but keeps numbers/identifiers', () => {
8
+ expect(tokenize('a in the db_pool 42')).toEqual(['in', 'the', 'db_pool', '42']);
9
+ });
10
+ it('returns [] for empty/symbol-only input', () => {
11
+ expect(tokenize(' %%$$ ')).toEqual([]);
12
+ });
13
+ });
14
+ describe('bm25Scores', () => {
15
+ it('scores a matching doc above a non-matching one', () => {
16
+ const docs = ['the database connection failed', 'the cat sat on the mat'];
17
+ const s = bm25Scores('database connection', docs);
18
+ expect(s[0]).toBeGreaterThan(s[1]);
19
+ });
20
+ it('weights a RARE query term more than a common one (idf)', () => {
21
+ // "apple" appears in 1 doc (rare), "banana" in 3 (common). A doc matching the
22
+ // rare term should outscore a doc matching only the common one.
23
+ const docs = ['apple', 'banana', 'banana', 'banana'];
24
+ const s = bm25Scores('apple banana', docs);
25
+ expect(s[0]).toBeGreaterThan(s[1]);
26
+ });
27
+ it('returns all-zero scores when the query has no usable terms', () => {
28
+ const docs = ['anything here', 'and here'];
29
+ expect(bm25Scores('', docs)).toEqual([0, 0]);
30
+ expect(bm25Scores('%%%', docs)).toEqual([0, 0]);
31
+ });
32
+ it('gives a non-matching doc a score of 0', () => {
33
+ const docs = ['relevant match here', 'totally unrelated'];
34
+ const s = bm25Scores('relevant', docs);
35
+ expect(s[1]).toBe(0);
36
+ expect(s[0]).toBeGreaterThan(0);
37
+ });
38
+ it('does not let a long repetitive doc dominate unboundedly (length normalization)', () => {
39
+ const short = 'error';
40
+ const long = ('error ' + 'filler '.repeat(200)).trim();
41
+ const s = bm25Scores('error', [short, long]);
42
+ // the concise doc that is mostly the query term should score at least as high
43
+ expect(s[0]).toBeGreaterThanOrEqual(s[1]);
44
+ });
45
+ it('is deterministic', () => {
46
+ const docs = ['alpha beta', 'beta gamma', 'gamma delta'];
47
+ expect(bm25Scores('beta gamma', docs)).toEqual(bm25Scores('beta gamma', docs));
48
+ });
49
+ it('produces one score per doc', () => {
50
+ const docs = ['a b c', 'd e f', 'g h i'];
51
+ expect(bm25Scores('a', docs).length).toBe(3);
52
+ });
53
+ });
54
+ describe('rankByRelevance', () => {
55
+ it('returns document indices sorted by descending relevance', () => {
56
+ const docs = ['no match', 'strong error error match', 'weak error'];
57
+ const order = rankByRelevance('error', docs);
58
+ expect(order[0]).toBe(1); // most "error" hits
59
+ expect(order[order.length - 1]).toBe(0); // no match last
60
+ });
61
+ it('is a stable permutation of all indices', () => {
62
+ const docs = ['a', 'b', 'c', 'd'];
63
+ const order = rankByRelevance('a b', docs);
64
+ expect([...order].sort((x, y) => x - y)).toEqual([0, 1, 2, 3]);
65
+ });
66
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mkdtempSync, writeFileSync, readFileSync, rmSync, statSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { secureKeyFile } from '../codexMitm.js';
6
+ // Security regression: the CA private key must be locked to the current user.
7
+ // 0o600 is a no-op on Windows, so secureKeyFile applies a per-OS restriction
8
+ // (icacls on Windows, chmod on POSIX). It must never throw — a key we cannot
9
+ // secure is warned about, not fatal — and must leave the file readable by us.
10
+ describe('secureKeyFile', () => {
11
+ it('secures a key file without throwing and keeps it owner-readable', () => {
12
+ const dir = mkdtempSync(join(tmpdir(), 'squeezr-key-'));
13
+ const keyPath = join(dir, 'ca.key');
14
+ writeFileSync(keyPath, 'PRIVATE-KEY-MATERIAL', { mode: 0o600 });
15
+ try {
16
+ expect(() => secureKeyFile(keyPath)).not.toThrow();
17
+ expect(readFileSync(keyPath, 'utf-8')).toBe('PRIVATE-KEY-MATERIAL');
18
+ if (process.platform !== 'win32') {
19
+ // On POSIX the mode must be owner-only (no group/other bits).
20
+ expect(statSync(keyPath).mode & 0o077).toBe(0);
21
+ }
22
+ }
23
+ finally {
24
+ rmSync(dir, { recursive: true, force: true });
25
+ }
26
+ });
27
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ // Security regression: the main proxy holds the user's API keys and exposes
5
+ // state-changing control endpoints, so it MUST bind to loopback only and never
6
+ // be reachable from the LAN. This guards against silently reverting that fix.
7
+ describe('main proxy loopback binding', () => {
8
+ const src = readFileSync(fileURLToPath(new URL('../index.ts', import.meta.url)), 'utf-8');
9
+ it('binds httpServer.listen to 127.0.0.1', () => {
10
+ expect(src).toMatch(/httpServer\.listen\(\s*PORT\s*,\s*['"]127\.0\.0\.1['"]/);
11
+ });
12
+ it('never binds the main proxy to all interfaces', () => {
13
+ expect(src).not.toMatch(/httpServer\.listen\(\s*PORT\s*,\s*['"]0\.0\.0\.0['"]/);
14
+ expect(src).not.toMatch(/httpServer\.listen\(\s*PORT\s*,\s*['"]::['"]/);
15
+ // A two-arg listen(PORT, cb) would bind all interfaces by default — forbid it.
16
+ expect(src).not.toMatch(/httpServer\.listen\(\s*PORT\s*,\s*\(/);
17
+ });
18
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Stats } from '../stats.js';
3
+ // recordEcho / recordShaping only mutate in-memory counters (persistence happens on the
4
+ // main record() path), so these tests never touch ~/.squeezr. We assert relative deltas
5
+ // so a pre-existing stats.json baseline doesn't matter.
6
+ describe('Stats output metrics', () => {
7
+ it('recordEcho updates sample count and average echo', () => {
8
+ const s = new Stats();
9
+ const before = s.summary().output;
10
+ s.recordEcho(0.2);
11
+ s.recordEcho(0.4);
12
+ const after = s.summary().output;
13
+ expect(after.echo_samples).toBe(before.echo_samples + 2);
14
+ expect(after.avg_echo_pct).toBeGreaterThanOrEqual(0);
15
+ expect(after.avg_echo_pct).toBeLessThanOrEqual(100);
16
+ });
17
+ it('recordShaping counts only the true flags', () => {
18
+ const s = new Stats();
19
+ const before = s.summary().output;
20
+ s.recordShaping(true, false); // steered only
21
+ s.recordShaping(true, true); // steered + effort
22
+ s.recordShaping(false, false); // neither
23
+ const after = s.summary().output;
24
+ expect(after.steered).toBe(before.steered + 2);
25
+ expect(after.effort_lowered).toBe(before.effort_lowered + 1);
26
+ });
27
+ it('avg echo reflects the mean of the samples added', () => {
28
+ const s = new Stats();
29
+ // add a large batch so any baseline is diluted toward our known mean
30
+ for (let i = 0; i < 200; i++)
31
+ s.recordEcho(0.3);
32
+ expect(Math.abs(s.summary().output.avg_echo_pct - 30)).toBeLessThan(10);
33
+ });
34
+ it('recordBypassed counts separately and never touches processed/saved totals', () => {
35
+ const s = new Stats();
36
+ const before = s.summary();
37
+ s.recordBypassed();
38
+ s.recordBypassed();
39
+ const after = s.summary();
40
+ expect(after.bypassed_requests).toBe(before.bypassed_requests + 2);
41
+ // bypassed traffic must NOT inflate processed or saved
42
+ expect(after.total_original_chars).toBe(before.total_original_chars);
43
+ expect(after.total_saved_chars).toBe(before.total_saved_chars);
44
+ expect(after.requests).toBe(before.requests);
45
+ });
46
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,133 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { crushText, normalizeLineKey, wordShingles, jaccard } from '../textCrusher.js';
3
+ describe('normalizeLineKey', () => {
4
+ it('collapses numbers, hex and timestamps so near-duplicates share a key', () => {
5
+ const a = normalizeLineKey('2026-07-21T10:00:01Z downloaded chunk 12 (0x1a2b)');
6
+ const b = normalizeLineKey('2026-07-21T11:22:33Z downloaded chunk 998 (0xff00)');
7
+ expect(a).toBe(b);
8
+ });
9
+ it('keeps genuinely different lines distinct', () => {
10
+ expect(normalizeLineKey('connecting to db')).not.toBe(normalizeLineKey('closing db handle'));
11
+ });
12
+ });
13
+ describe('crushText', () => {
14
+ function repetitiveLog(n) {
15
+ const lines = [];
16
+ for (let i = 0; i < n; i++)
17
+ lines.push(`[worker] processed record ${i} in ${i * 2}ms`);
18
+ return lines.join('\n');
19
+ }
20
+ it('returns short input unchanged', () => {
21
+ const input = 'line a\nline b\nline c';
22
+ expect(crushText(input, { maxLines: 60 }).text).toBe(input);
23
+ });
24
+ it('collapses near-duplicate lines that differ only in numbers', () => {
25
+ const input = repetitiveLog(200);
26
+ const res = crushText(input);
27
+ expect(res.dropped).toBeGreaterThan(0);
28
+ expect(res.text.length).toBeLessThan(input.length);
29
+ expect(res.text.split('\n').length).toBeLessThan(200);
30
+ });
31
+ it('always keeps lines carrying error/failure signal', () => {
32
+ const lines = [];
33
+ for (let i = 0; i < 150; i++)
34
+ lines.push(`[worker] processed record ${i} ok`);
35
+ lines.splice(75, 0, 'ERROR: database connection refused at db.example.com:5432');
36
+ const input = lines.join('\n');
37
+ const res = crushText(input);
38
+ expect(res.text.includes('ERROR: database connection refused at db.example.com:5432')).toBe(true);
39
+ });
40
+ it('keeps head and tail anchor lines', () => {
41
+ const lines = ['FIRST LINE MARKER'];
42
+ for (let i = 0; i < 150; i++)
43
+ lines.push(`noise line ${i} value ${i}`);
44
+ lines.push('LAST LINE MARKER');
45
+ const input = lines.join('\n');
46
+ const res = crushText(input, { headKeep: 3, tailKeep: 3 });
47
+ expect(res.text.includes('FIRST LINE MARKER')).toBe(true);
48
+ expect(res.text.includes('LAST LINE MARKER')).toBe(true);
49
+ });
50
+ it('respects the maxLines budget (plus the omitted-summary line)', () => {
51
+ const res = crushText(repetitiveLog(500), { maxLines: 40 });
52
+ // kept content lines should not wildly exceed the budget
53
+ expect(res.text.split('\n').length).toBeLessThanOrEqual(45);
54
+ });
55
+ it('preserves the original order of kept lines', () => {
56
+ const lines = ['alpha unique', ...Array.from({ length: 100 }, (_, i) => `dup ${i}`), 'omega unique'];
57
+ const res = crushText(lines.join('\n'), { headKeep: 2, tailKeep: 2 });
58
+ const idxA = res.text.indexOf('alpha unique');
59
+ const idxO = res.text.indexOf('omega unique');
60
+ expect(idxA).toBeGreaterThanOrEqual(0);
61
+ expect(idxO).toBeGreaterThan(idxA);
62
+ });
63
+ it('is deterministic', () => {
64
+ const input = repetitiveLog(300);
65
+ expect(crushText(input).text).toBe(crushText(input).text);
66
+ });
67
+ it('emits an omitted-count marker when it drops lines', () => {
68
+ const res = crushText(repetitiveLog(300));
69
+ expect(/\[\d+ lines omitted/.test(res.text)).toBe(true);
70
+ });
71
+ it('with a query, keeps task-relevant lines that a blind pass would drop', () => {
72
+ // 120 noise lines + one line mentioning the task keyword, positioned in the middle
73
+ // (not an anchor) so only relevance can save it.
74
+ const lines = [];
75
+ for (let i = 0; i < 60; i++)
76
+ lines.push(`background chore ${i} finished`);
77
+ lines.push('updated the authentication middleware token refresh logic');
78
+ for (let i = 0; i < 60; i++)
79
+ lines.push(`background chore ${i + 60} finished`);
80
+ const input = lines.join('\n');
81
+ const withQuery = crushText(input, { maxLines: 20, headKeep: 3, tailKeep: 3, query: 'authentication middleware token' });
82
+ expect(withQuery.text.includes('authentication middleware token refresh')).toBe(true);
83
+ });
84
+ it('stays deterministic for a fixed query', () => {
85
+ const input = repetitiveLog(300);
86
+ const a = crushText(input, { query: 'record ms' }).text;
87
+ const b = crushText(input, { query: 'record ms' }).text;
88
+ expect(a).toBe(b);
89
+ });
90
+ it('collapses REWORDED near-duplicate lines (not just number-varied)', () => {
91
+ // Two wordings of the same idea, no numbers, no signal keywords → exact-key dedup
92
+ // would keep both; shingle near-dup should collapse them.
93
+ const a = 'the worker finished processing the batch quickly and moved on';
94
+ const b = 'the worker completed processing the batch quickly and moved on';
95
+ // Unique anchor lines at head/tail (no "worker") so the variants live in the MIDDLE,
96
+ // where the budget-fill + shingle collapse applies.
97
+ const lines = ['ALPHA HEADER ONE', 'ALPHA HEADER TWO', 'ALPHA HEADER THREE'];
98
+ for (let i = 0; i < 100; i++)
99
+ lines.push(i % 2 === 0 ? a : b);
100
+ lines.push('OMEGA FOOTER ONE', 'OMEGA FOOTER TWO', 'OMEGA FOOTER THREE');
101
+ const res = crushText(lines.join('\n'), { maxLines: 30, headKeep: 3, tailKeep: 3 });
102
+ const distinct = new Set(res.text.split('\n').filter(l => l.includes('worker')));
103
+ // both wordings collapse to (at most) one representative
104
+ expect(distinct.size).toBeLessThanOrEqual(1);
105
+ });
106
+ it('does NOT collapse genuinely different lines', () => {
107
+ const lines = ['UNIQUE HEADER'];
108
+ for (let i = 0; i < 30; i++) {
109
+ lines.push('alpha subsystem initialised the primary cache layer');
110
+ lines.push('gamma module rejected the inbound websocket handshake');
111
+ }
112
+ lines.push('UNIQUE FOOTER');
113
+ const res = crushText(lines.join('\n'), { maxLines: 40, headKeep: 2, tailKeep: 2 });
114
+ expect(res.text.includes('alpha subsystem')).toBe(true);
115
+ expect(res.text.includes('gamma module')).toBe(true);
116
+ });
117
+ });
118
+ describe('wordShingles / jaccard', () => {
119
+ it('reworded lines share most shingles (high jaccard)', () => {
120
+ const a = wordShingles('the worker finished processing the batch quickly');
121
+ const b = wordShingles('the worker completed processing the batch quickly');
122
+ expect(jaccard(a, b)).toBeGreaterThan(0.4);
123
+ });
124
+ it('unrelated lines share few shingles (low jaccard)', () => {
125
+ const a = wordShingles('database connection pool exhausted');
126
+ const b = wordShingles('the cat sat on the warm mat');
127
+ expect(jaccard(a, b)).toBeLessThan(0.1);
128
+ });
129
+ it('identical lines have jaccard 1', () => {
130
+ const a = wordShingles('exactly the same words here now');
131
+ expect(jaccard(a, a)).toBe(1);
132
+ });
133
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * squeezr bench — honest, reproducible compression+accuracy harness.
3
+ *
4
+ * Headroom's headline claim is "same answers, fewer tokens", proven by having an LLM
5
+ * answer questions over compressed data. That needs an API key and costs money, so it
6
+ * can't live in CI. This harness measures the same thing with a zero-cost, deterministic
7
+ * proxy for accuracy: FACT RECALL.
8
+ *
9
+ * For each fixture we declare the CRITICAL FACTS an answer would need (an error code, a
10
+ * filename, a specific value, a count). We compress the fixture through Squeezr's real
11
+ * deterministic pipeline and measure:
12
+ *
13
+ * - compressionPct — chars removed / original chars.
14
+ * - inlineRecall — fraction of critical facts still present VERBATIM in the compressed
15
+ * output (i.e. answerable WITHOUT an expand round-trip).
16
+ *
17
+ * Recoverability is 100% by design (every compressed block stores its original in the
18
+ * expand store — see expand.test.ts), so the meaningful, honest metric is how much
19
+ * survives inline. A fixture that trades recall for ratio (huge-log head/tail) shows it
20
+ * here rather than hiding it.
21
+ */
22
+ export interface Fixture {
23
+ name: string;
24
+ tool: string;
25
+ content: string;
26
+ facts: string[];
27
+ }
28
+ export interface BenchRow {
29
+ name: string;
30
+ originalChars: number;
31
+ compressedChars: number;
32
+ compressionPct: number;
33
+ inlineRecall: number;
34
+ }
35
+ export interface BenchSummary {
36
+ rows: BenchRow[];
37
+ avgCompressionPct: number;
38
+ avgInlineRecall: number;
39
+ }
40
+ /** Fraction of `facts` present verbatim in `text`. Empty fact list → 1 (nothing to lose). */
41
+ export declare function factRecall(text: string, facts: string[]): number;
42
+ export declare const FIXTURES: Fixture[];
43
+ export declare function runBench(fixtures?: Fixture[]): BenchSummary;
44
+ /** Human-readable table for the CLI. */
45
+ export declare function formatBench(summary: BenchSummary): string;
package/dist/bench.js ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * squeezr bench — honest, reproducible compression+accuracy harness.
3
+ *
4
+ * Headroom's headline claim is "same answers, fewer tokens", proven by having an LLM
5
+ * answer questions over compressed data. That needs an API key and costs money, so it
6
+ * can't live in CI. This harness measures the same thing with a zero-cost, deterministic
7
+ * proxy for accuracy: FACT RECALL.
8
+ *
9
+ * For each fixture we declare the CRITICAL FACTS an answer would need (an error code, a
10
+ * filename, a specific value, a count). We compress the fixture through Squeezr's real
11
+ * deterministic pipeline and measure:
12
+ *
13
+ * - compressionPct — chars removed / original chars.
14
+ * - inlineRecall — fraction of critical facts still present VERBATIM in the compressed
15
+ * output (i.e. answerable WITHOUT an expand round-trip).
16
+ *
17
+ * Recoverability is 100% by design (every compressed block stores its original in the
18
+ * expand store — see expand.test.ts), so the meaningful, honest metric is how much
19
+ * survives inline. A fixture that trades recall for ratio (huge-log head/tail) shows it
20
+ * here rather than hiding it.
21
+ */
22
+ import { preprocessForTool } from './deterministic.js';
23
+ /** Fraction of `facts` present verbatim in `text`. Empty fact list → 1 (nothing to lose). */
24
+ export function factRecall(text, facts) {
25
+ if (facts.length === 0)
26
+ return 1;
27
+ const present = facts.filter(f => text.includes(f)).length;
28
+ return present / facts.length;
29
+ }
30
+ // ── Fixtures ─────────────────────────────────────────────────────────────────
31
+ function jsonArrayFixture() {
32
+ const rows = [];
33
+ for (let i = 0; i < 40; i++) {
34
+ rows.push({ id: i, service: `svc-${i}`, status: i === 17 ? 'CrashLoopBackOff' : 'Running', restarts: i === 17 ? 42 : 0, node: 'ip-10-0-1-5' });
35
+ }
36
+ return JSON.stringify(rows);
37
+ }
38
+ function buildLogFixture() {
39
+ const lines = [];
40
+ for (let i = 0; i < 60; i++)
41
+ lines.push('[webpack] compiling module... ok');
42
+ lines.push('ERROR in ./src/app.ts:128:14');
43
+ lines.push("TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.");
44
+ for (let i = 0; i < 60; i++)
45
+ lines.push('[webpack] compiling module... ok');
46
+ return lines.join('\n');
47
+ }
48
+ function grepFixture() {
49
+ const lines = [];
50
+ for (let f = 0; f < 30; f++) {
51
+ for (let l = 0; l < 5; l++)
52
+ lines.push(`src/mod${f}.ts:${l + 1}: const value = computeThing(${l})`);
53
+ }
54
+ lines.push('src/critical.ts:99: throw new FatalError("DISK_FULL")');
55
+ return lines.join('\n');
56
+ }
57
+ export const FIXTURES = [
58
+ {
59
+ name: 'json-array (k8s pods)',
60
+ tool: 'Bash',
61
+ content: jsonArrayFixture(),
62
+ // The one anomalous row an answer would need + its schema.
63
+ facts: ['CrashLoopBackOff', '42', 'svc-17', 'ip-10-0-1-5'],
64
+ },
65
+ {
66
+ name: 'build-log (tsc error in noise)',
67
+ tool: 'Bash',
68
+ content: buildLogFixture(),
69
+ facts: ['TS2345', './src/app.ts:128:14'],
70
+ },
71
+ {
72
+ name: 'grep-dump (fatal among matches)',
73
+ tool: 'Grep',
74
+ content: grepFixture(),
75
+ facts: ['DISK_FULL', 'src/critical.ts:99'],
76
+ },
77
+ ];
78
+ // ── Runner ───────────────────────────────────────────────────────────────────
79
+ export function runBench(fixtures = FIXTURES) {
80
+ const rows = fixtures.map(f => {
81
+ const compressed = preprocessForTool(f.content, f.tool, 0);
82
+ const originalChars = f.content.length;
83
+ const compressedChars = compressed.length;
84
+ const compressionPct = originalChars > 0 ? Math.max(0, ((originalChars - compressedChars) / originalChars) * 100) : 0;
85
+ return {
86
+ name: f.name,
87
+ originalChars,
88
+ compressedChars,
89
+ compressionPct: Math.round(compressionPct * 10) / 10,
90
+ inlineRecall: factRecall(compressed, f.facts),
91
+ };
92
+ });
93
+ const n = rows.length || 1;
94
+ return {
95
+ rows,
96
+ avgCompressionPct: Math.round((rows.reduce((s, r) => s + r.compressionPct, 0) / n) * 10) / 10,
97
+ avgInlineRecall: Math.round((rows.reduce((s, r) => s + r.inlineRecall, 0) / n) * 100) / 100,
98
+ };
99
+ }
100
+ /** Human-readable table for the CLI. */
101
+ export function formatBench(summary) {
102
+ const lines = [];
103
+ lines.push('Squeezr compression + fact-recall benchmark');
104
+ lines.push('(recoverability is 100% by design via squeezr_expand; inline recall = answerable without expanding)');
105
+ lines.push('');
106
+ lines.push('fixture orig comp saved inline-recall');
107
+ lines.push('─'.repeat(78));
108
+ for (const r of summary.rows) {
109
+ lines.push(`${r.name.padEnd(36)} ${String(r.originalChars).padStart(6)} ${String(r.compressedChars).padStart(6)} ${(r.compressionPct + '%').padStart(6)} ${Math.round(r.inlineRecall * 100) + '%'}`);
110
+ }
111
+ lines.push('─'.repeat(78));
112
+ lines.push(`AVG: ${summary.avgCompressionPct}% compression · ${Math.round(summary.avgInlineRecall * 100)}% inline fact recall · 100% recoverable`);
113
+ return lines.join('\n');
114
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * CacheAligner (detector-only) — Squeezr's take on headroom's CacheAligner.
3
+ *
4
+ * Anthropic bills the cached prefix at 0.1x, but ONLY if the prefix arrives byte-for-byte
5
+ * identical between requests. If the CLIENT's own system prompt embeds volatile content
6
+ * (a per-session UUID, a live timestamp, a build hash, a JWT) INSIDE the cached region,
7
+ * that prefix changes every request and the cache never hits — re-billing the whole
8
+ * context at full price. That is exactly the failure that once burned a 5h plan.
9
+ *
10
+ * Squeezr already monitors cache hit-health %; this tells you the likely CAUSE. It is a
11
+ * pure DETECTOR: it never mutates the prompt (mutating the cache hot zone is itself a
12
+ * cache-buster). It just surfaces "your prefix is unstable because of X".
13
+ *
14
+ * Detection is structural (shape-based regex over known volatile token forms), not
15
+ * semantic. Order matters: JWTs are matched before hashes so a JWT segment isn't
16
+ * miscounted as a hex hash.
17
+ */
18
+ export type VolatileKind = 'uuid' | 'timestamp' | 'jwt' | 'hash';
19
+ export interface VolatileFinding {
20
+ kind: VolatileKind;
21
+ count: number;
22
+ }
23
+ /** Structural scan for volatile tokens. Matched kinds are masked out before later
24
+ * patterns run, so a JWT is never also counted as a hash. */
25
+ export declare function detectVolatile(text: string): VolatileFinding[];
26
+ export interface SystemAnalysis {
27
+ hasVolatile: boolean;
28
+ findings: VolatileFinding[];
29
+ }
30
+ export declare function analyzeSystemPrompt(system: unknown): SystemAnalysis;
31
+ export declare function formatVolatileWarning(findings: VolatileFinding[]): string;
32
+ /** Warn at most once per distinct finding signature. Returns true iff it warned now. */
33
+ export declare function warnIfVolatile(system: unknown): boolean;
34
+ export declare function _resetWarnedForTest(): void;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * CacheAligner (detector-only) — Squeezr's take on headroom's CacheAligner.
3
+ *
4
+ * Anthropic bills the cached prefix at 0.1x, but ONLY if the prefix arrives byte-for-byte
5
+ * identical between requests. If the CLIENT's own system prompt embeds volatile content
6
+ * (a per-session UUID, a live timestamp, a build hash, a JWT) INSIDE the cached region,
7
+ * that prefix changes every request and the cache never hits — re-billing the whole
8
+ * context at full price. That is exactly the failure that once burned a 5h plan.
9
+ *
10
+ * Squeezr already monitors cache hit-health %; this tells you the likely CAUSE. It is a
11
+ * pure DETECTOR: it never mutates the prompt (mutating the cache hot zone is itself a
12
+ * cache-buster). It just surfaces "your prefix is unstable because of X".
13
+ *
14
+ * Detection is structural (shape-based regex over known volatile token forms), not
15
+ * semantic. Order matters: JWTs are matched before hashes so a JWT segment isn't
16
+ * miscounted as a hex hash.
17
+ */
18
+ const PATTERNS = [
19
+ { kind: 'jwt', re: /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g },
20
+ { kind: 'uuid', re: /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi },
21
+ { kind: 'timestamp', re: /\b\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?\b/g },
22
+ { kind: 'hash', re: /\b[0-9a-f]{40}\b|\b[0-9a-f]{64}\b/gi },
23
+ ];
24
+ /** Structural scan for volatile tokens. Matched kinds are masked out before later
25
+ * patterns run, so a JWT is never also counted as a hash. */
26
+ export function detectVolatile(text) {
27
+ let scan = text;
28
+ const findings = [];
29
+ for (const { kind, re } of PATTERNS) {
30
+ const matches = scan.match(re);
31
+ if (matches && matches.length > 0) {
32
+ findings.push({ kind, count: matches.length });
33
+ scan = scan.replace(re, ' '); // mask so subsequent, looser patterns don't re-match
34
+ }
35
+ }
36
+ return findings;
37
+ }
38
+ function systemToText(system) {
39
+ if (typeof system === 'string')
40
+ return system;
41
+ if (Array.isArray(system)) {
42
+ return system
43
+ .filter(b => b && b.type === 'text' && typeof b.text === 'string')
44
+ .map(b => b.text)
45
+ .join('\n');
46
+ }
47
+ return '';
48
+ }
49
+ export function analyzeSystemPrompt(system) {
50
+ const findings = detectVolatile(systemToText(system));
51
+ return { hasVolatile: findings.length > 0, findings };
52
+ }
53
+ export function formatVolatileWarning(findings) {
54
+ const parts = findings.map(f => `${f.count}× ${f.kind}`).join(', ');
55
+ return `[squeezr/cache-aligner] system prompt contains volatile content (${parts}) — if it sits inside the cached prefix it will bust Anthropic's prompt cache every request. Squeezr never mutates the prefix; consider moving volatile tokens after the cache_control breakpoint.`;
56
+ }
57
+ // ── Throttled warning (no per-request spam) ──────────────────────────────────
58
+ const warned = new Set();
59
+ /** Warn at most once per distinct finding signature. Returns true iff it warned now. */
60
+ export function warnIfVolatile(system) {
61
+ const { hasVolatile, findings } = analyzeSystemPrompt(system);
62
+ if (!hasVolatile)
63
+ return false;
64
+ const sig = findings.map(f => `${f.kind}:${f.count}`).sort().join('|');
65
+ if (warned.has(sig))
66
+ return false;
67
+ warned.add(sig);
68
+ console.log(formatVolatileWarning(findings));
69
+ return true;
70
+ }
71
+ export function _resetWarnedForTest() {
72
+ warned.clear();
73
+ }
@@ -1,4 +1,5 @@
1
1
  export declare const BUNDLE_PATH: string;
2
2
  export declare const MITM_PORT: number;
3
+ export declare function secureKeyFile(path: string): void;
3
4
  export declare function startMitmProxy(): void;
4
5
  export declare function stopMitmProxy(): void;
package/dist/codexMitm.js CHANGED
@@ -4,7 +4,8 @@ import http from 'node:http';
4
4
  import https from 'node:https';
5
5
  import fs from 'node:fs';
6
6
  import crypto from 'node:crypto';
7
- import { homedir } from 'node:os';
7
+ import { execFileSync } from 'node:child_process';
8
+ import { homedir, userInfo } from 'node:os';
8
9
  import { join } from 'node:path';
9
10
  import forge from 'node-forge';
10
11
  import { config } from './config.js';
@@ -15,6 +16,26 @@ const CA_CERT_PATH = join(CA_DIR, 'ca.crt');
15
16
  export const BUNDLE_PATH = join(CA_DIR, 'bundle.crt');
16
17
  export const MITM_PORT = config.mitmPort;
17
18
  // ── CA generation ─────────────────────────────────────────────────────────────
19
+ // POSIX permission bits (0o600) are ignored on Windows, so the CA private key
20
+ // would be left readable by other accounts. Lock it down per-OS: an ACL that
21
+ // grants only the current user on Windows, chmod 0o600 on POSIX. A private key
22
+ // we cannot secure is a real risk, so failure is surfaced loudly, never silent.
23
+ export function secureKeyFile(path) {
24
+ if (process.platform === 'win32') {
25
+ const user = process.env.USERNAME || userInfo().username;
26
+ try {
27
+ // /inheritance:r drops inherited ACEs; /grant:r sets the current user as
28
+ // the only principal with Full control.
29
+ execFileSync('icacls', [path, '/inheritance:r', '/grant:r', `${user}:F`], { stdio: 'ignore' });
30
+ }
31
+ catch (err) {
32
+ console.warn(`[squeezr/mitm] WARNING: could not restrict permissions on ${path} — ` +
33
+ `${err.message}. Secure this private key manually.`);
34
+ }
35
+ return;
36
+ }
37
+ fs.chmodSync(path, 0o600);
38
+ }
18
39
  function ensureCA() {
19
40
  const certsExist = fs.existsSync(CA_KEY_PATH) && fs.existsSync(CA_CERT_PATH);
20
41
  if (!certsExist) {
@@ -35,6 +56,7 @@ function ensureCA() {
35
56
  ]);
36
57
  cert.sign(keys.privateKey, forge.md.sha256.create());
37
58
  fs.writeFileSync(CA_KEY_PATH, forge.pki.privateKeyToPem(keys.privateKey), { mode: 0o600 });
59
+ secureKeyFile(CA_KEY_PATH);
38
60
  fs.writeFileSync(CA_CERT_PATH, forge.pki.certificateToPem(cert), { mode: 0o644 });
39
61
  console.log(`[squeezr/mitm] CA generated → ${CA_CERT_PATH}`);
40
62
  }
@@ -438,7 +460,9 @@ export function startMitmProxy() {
438
460
  if (err.code !== 'EADDRINUSE')
439
461
  console.error('[squeezr/mitm] error:', err.message);
440
462
  });
441
- mitmServer.listen(MITM_PORT, () => {
463
+ // Loopback only: the MITM proxy terminates TLS for the user's traffic and must
464
+ // never be reachable from the LAN (matches the other listeners).
465
+ mitmServer.listen(MITM_PORT, '127.0.0.1', () => {
442
466
  console.log(`[squeezr/mitm] HTTPS proxy on http://localhost:${MITM_PORT}`);
443
467
  });
444
468
  }