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
package/dist/dashboard.js CHANGED
@@ -430,7 +430,7 @@ code{font-family:'Cascadia Code','SF Mono',Consolas,monospace;font-size:.9em}
430
430
  <div class="hero-card">
431
431
  <div class="hc-label">Requests</div>
432
432
  <div class="hc-val" id="h-reqs">—</div>
433
- <div class="hc-sub"><span id="h-comp">—</span> AI-compressed · det. always on</div>
433
+ <div class="hc-sub"><span id="h-comp">—</span> AI-compressed · det. always on<span id="h-bypassed"></span></div>
434
434
  </div>
435
435
  </div>
436
436
 
@@ -498,16 +498,16 @@ code{font-family:'Cascadia Code','SF Mono',Consolas,monospace;font-size:.9em}
498
498
  <div style="margin-top:8px;font-size:11px;color:var(--text3);text-align:center">0 here is normal when AI compression is off</div>
499
499
  </div>
500
500
  </div>
501
- <!-- AI Compression -->
501
+ <!-- Output shaping (output-side token reduction) — pinned in the old AI-Compression slot -->
502
502
  <div class="section" style="margin:0">
503
- <div class="section-head"><span class="section-title">AI Compression</span><span style="font-size:11px;color:var(--text3)">today · persisted</span></div>
503
+ <div class="section-head"><span class="section-title">Output</span><span style="font-size:11px;color:var(--text3)" id="out-note">what the model writes back · needs [output] enabled</span></div>
504
504
  <div class="section-body">
505
505
  <div class="cache-row">
506
- <div class="cache-card"><div class="cache-label">Calls</div><div class="cache-val" id="ai-calls">—</div></div>
507
- <div class="cache-card"><div class="cache-label">Saved</div><div class="cache-val" id="ai-saved" style="color:var(--brand2)">—</div></div>
508
- <div class="cache-card"><div class="cache-label">Spent</div><div class="cache-val" id="ai-spent">—</div></div>
506
+ <div class="cache-card"><div class="cache-label">Echo (restated)</div><div class="cache-val" id="out-echo">—</div></div>
507
+ <div class="cache-card"><div class="cache-label">Steered</div><div class="cache-val" id="out-steered">—</div></div>
508
+ <div class="cache-card"><div class="cache-label">Effort lowered</div><div class="cache-val" id="out-effort">—</div></div>
509
509
  </div>
510
- <div id="ai-net" style="margin-top:8px;font-size:11px;color:var(--text3);text-align:center">—</div>
510
+ <div style="margin-top:8px;font-size:11px;color:var(--text3);text-align:center">Echo = % of the model's output that merely restated context it was already given (lower is better).</div>
511
511
  </div>
512
512
  </div>
513
513
  </div>
@@ -1022,6 +1022,9 @@ function render(d) {
1022
1022
  document.getElementById('h-cost').textContent = fmtUsd(tCost);
1023
1023
  document.getElementById('h-reqs').textContent = fmt(tReqs);
1024
1024
  document.getElementById('h-comp').textContent = fmt(tComps);
1025
+ var byp = (d.bypassed_requests != null) ? d.bypassed_requests : 0;
1026
+ var bypEl = document.getElementById('h-bypassed');
1027
+ if (bypEl) bypEl.textContent = byp > 0 ? ' · ' + fmt(byp) + ' bypassed (not counted)' : '';
1025
1028
  var perEl = document.getElementById('overview-period');
1026
1029
  if (perEl && today.date) perEl.textContent = 'today · ' + today.date;
1027
1030
  // (No quality banner in the Overview — only genuine quality issues, high expand
@@ -1118,6 +1121,22 @@ var eff = (today.efficiency_pct != null) ? today.efficiency_pct : null;
1118
1121
  }
1119
1122
  }
1120
1123
  }
1124
+ // Output shaping card (all-time; only meaningful when [output] enabled)
1125
+ var out = d.output || {};
1126
+ var setOut = function(id, v){ var e = document.getElementById(id); if(e) e.textContent = v; };
1127
+ var echoEl = document.getElementById('out-echo');
1128
+ if (echoEl) {
1129
+ if (out.echo_samples > 0) {
1130
+ echoEl.textContent = out.avg_echo_pct + '%';
1131
+ // lower echo is better: green under 15%, amber 15-35%, red above
1132
+ echoEl.style.color = out.avg_echo_pct <= 15 ? 'var(--brand2)' : out.avg_echo_pct <= 35 ? '#fbbf24' : 'var(--red, #e5484d)';
1133
+ } else {
1134
+ echoEl.textContent = '—';
1135
+ }
1136
+ }
1137
+ setOut('out-steered', out.steered > 0 ? fmt(out.steered) : '—');
1138
+ setOut('out-effort', out.effort_lowered > 0 ? fmt(out.effort_lowered) : '—');
1139
+
1121
1140
  // Cost Comparison is OVERVIEW = TODAY: use today's tokens (not all-time), so the
1122
1141
  // sub-lines match the today money. (modelCosts already comes from today's models.)
1123
1142
  var cmpIn = today.original_tokens || 0;
@@ -31,4 +31,7 @@ export declare function hitPattern(pattern: string, count?: number): void;
31
31
  export declare function preprocess(text: string): string;
32
32
  export declare function preprocessRatio(original: string, processed: string): number;
33
33
  export declare function preprocessAssistant(text: string): string;
34
- export declare function preprocessForTool(text: string, toolName: string, pressure?: number): string;
34
+ export type CodeLang = 'ts' | 'py' | 'go' | 'rs' | 'java' | 'c' | 'cpp';
35
+ export declare function detectCodeLanguage(text: string): CodeLang | null;
36
+ export declare function extractCodeStructure(text: string, lang: CodeLang): string;
37
+ export declare function preprocessForTool(text: string, toolName: string, pressure?: number, query?: string): string;
@@ -27,6 +27,8 @@
27
27
  * Glob — compact large file listings into directory summary
28
28
  */
29
29
  import { storeOriginal, storeSegments } from './expand.js';
30
+ import { crushEmbeddedJson } from './contentRouter.js';
31
+ import { crushText } from './textCrusher.js';
30
32
  // ── Pattern hit tracking (for squeezr discover) ───────────────────────────────
31
33
  export const detPatternHits = {};
32
34
  function hit(pattern) {
@@ -62,6 +64,12 @@ function looksCodeLine(raw) {
62
64
  return true; // markup / code punctuation
63
65
  if (/^[}\])>]+[,;]?$/.test(t) || /^[[({<]+$/.test(t))
64
66
  return true; // pure brackets
67
+ // Bare object/property key on its own line — `body:`, `"data":`, `items:` — whose
68
+ // value sits on following lines. No code punctuation, so the checks above miss it,
69
+ // yet folding repeated keys shatters the surrounding structure. A key has no spaces
70
+ // (rules out log prose like `ERROR: connection refused`).
71
+ if (/^["']?[\w$.-]+["']?:$/.test(t))
72
+ return true;
65
73
  return false;
66
74
  }
67
75
  function deduplicateLines(text) {
@@ -916,13 +924,21 @@ function extractGenericErrors(text) {
916
924
  return out.join('\n') + `\n... [${saved} non-error lines omitted]`;
917
925
  return text;
918
926
  }
919
- // Generic fallback: long unrecognised bash output keep last N lines
920
- function truncateLongOutput(text, pressure = 0) {
927
+ // Generic fallback: long unrecognised bash output. Prefer EXTRACTIVE crushing
928
+ // (collapse near-duplicate spam, keep head/tail + high-signal lines) over blind
929
+ // tail-keep — deterministic and cache-safe. Falls back to tail-keep if the crusher
930
+ // can't beat it (e.g. all lines unique and low-signal).
931
+ function truncateLongOutput(text, pressure = 0, query = '') {
921
932
  const threshold = pressure >= 0.9 ? 50 : 80;
922
933
  const keepLines = pressure >= 0.9 ? 30 : 50;
923
934
  const lines = text.split('\n');
924
935
  if (lines.length <= threshold)
925
936
  return text;
937
+ const crushed = crushText(text, { maxLines: keepLines, headKeep: 5, tailKeep: 8, query });
938
+ if (crushed.dropped > 0 && crushed.text.length < text.length) {
939
+ hit('textCrush');
940
+ return crushed.text;
941
+ }
926
942
  const omitted = lines.length - keepLines;
927
943
  return `... [${omitted} earlier lines omitted]\n` + lines.slice(-keepLines).join('\n');
928
944
  }
@@ -943,7 +959,7 @@ function compactFileListing(text) {
943
959
  const summary = Object.entries(byDir).sort(([, a], [, b]) => b - a).map(([d, n]) => `${d}/ (${n} files)`).join('\n');
944
960
  return `${lines.length} files total:\n${summary}`;
945
961
  }
946
- function applyBashPatterns(text, pressure = 0) {
962
+ function applyBashPatterns(text, pressure = 0, query = '') {
947
963
  if (looksLikeGitDiff(text)) {
948
964
  hit('gitDiff');
949
965
  return compactGitDiff(text, pressure);
@@ -1067,7 +1083,7 @@ function applyBashPatterns(text, pressure = 0) {
1067
1083
  return errExtracted;
1068
1084
  }
1069
1085
  hit('truncated');
1070
- return truncateLongOutput(text, pressure);
1086
+ return truncateLongOutput(text, pressure, query);
1071
1087
  }
1072
1088
  // ── Grep tool ─────────────────────────────────────────────────────────────────
1073
1089
  // Group matches by file, cap at MAX_PER_FILE lines each, truncate if too many files
@@ -1139,17 +1155,20 @@ function looksLikeLockfile(text) {
1139
1155
  const resolved = (text.match(/"resolved":|resolved "/g) ?? []).length;
1140
1156
  return integrity >= 10 || resolved >= 10;
1141
1157
  }
1142
- // Detect source language from first 30 lines of content
1143
- function detectCodeLanguage(text) {
1158
+ export function detectCodeLanguage(text) {
1144
1159
  const sample = text.split('\n').slice(0, 30).join('\n');
1145
1160
  if (/^import .+ from ['"]|^export (function|class|const|type|interface|default)\b|: (string|number|boolean|void|unknown)\b/.test(sample))
1146
1161
  return 'ts';
1147
1162
  if (/^from \S+ import |^def \w+\(|^class \w+[:(]/.test(sample))
1148
1163
  return 'py';
1149
- if (/^package \w+|^func \w+\(|^\s*import \(/.test(sample))
1164
+ if (/^package \w+\n|^func \w+\(|^\s*import \(/.test(sample))
1150
1165
  return 'go';
1151
1166
  if (/^use (std|crate|self)::|^(pub\s+)?fn \w+\(|^impl\s+/.test(sample))
1152
1167
  return 'rs';
1168
+ if (/^package [\w.]+;|^import [\w.]+;|^(public|private|protected|abstract|final|sealed)\s+(class|interface|enum|record)\s+\w+|public\s+static\s+void\s+main/.test(sample))
1169
+ return 'java';
1170
+ if (/#include\s*[<"]/.test(sample))
1171
+ return /\b(std::|template\s*<|namespace\s|::\w|class\s+\w+)/.test(sample) ? 'cpp' : 'c';
1153
1172
  return null;
1154
1173
  }
1155
1174
  // Top-level structural line (column 0) per language.
@@ -1158,6 +1177,9 @@ const STRUCT_CHECKS = {
1158
1177
  py: (t) => /^(import |from .+ import|def |class |@\w)/.test(t),
1159
1178
  go: (t) => /^(import|func |type |var |const |package )\b/.test(t),
1160
1179
  rs: (t) => /^(use |pub |fn |struct |enum |impl |trait |mod |const |static )\b/.test(t),
1180
+ java: (t) => /^(package |import |@\w|(public |private |protected |abstract |final |sealed |static )*\s*(class|interface|enum|record) )/.test(t),
1181
+ c: (t) => /^(#include|#define|typedef |struct |enum |union |static |extern |const |void |int |char |long |double |float |unsigned |[A-Za-z_]\w*\s+\**[A-Za-z_]\w*\s*\()/.test(t),
1182
+ cpp: (t) => /^(#include|#define|using |namespace |template|class |struct |enum |union |typedef |public:|private:|protected:|void |int |auto |[A-Za-z_][\w:<>]*\s+\**[A-Za-z_]\w*\s*\()/.test(t),
1161
1183
  };
1162
1184
  // An INDENTED member/method that opens a block — so we can segment per method,
1163
1185
  // not just per top-level symbol. Conservative: must open a block ('{' for brace
@@ -1181,7 +1203,7 @@ function isMemberOpen(line, lang) {
1181
1203
  // Extract structure (imports + signatures) and make each member's BODY recoverable
1182
1204
  // on its own via squeezr_expand("<id>~i") — so the model can pull back ONE function
1183
1205
  // instead of re-fetching the whole file. Segments are contiguous slices (safe).
1184
- function extractCodeStructure(text, lang) {
1206
+ export function extractCodeStructure(text, lang) {
1185
1207
  const lines = text.split('\n');
1186
1208
  const check = STRUCT_CHECKS[lang];
1187
1209
  // Anchors = lines we KEEP (top-level structural + indented member openings).
@@ -1322,7 +1344,7 @@ function makeRecoverable(original, compacted) {
1322
1344
  const id = storeOriginal(original);
1323
1345
  return `${compacted}\n[squeezr_expand("${id}") — full untruncated output]`;
1324
1346
  }
1325
- export function preprocessForTool(text, toolName, pressure = 0) {
1347
+ export function preprocessForTool(text, toolName, pressure = 0, query = '') {
1326
1348
  const tool = toolName.toLowerCase();
1327
1349
  // NEVER compress the result of an expand call. The whole point of squeezr_expand
1328
1350
  // is to deliver the FULL original back to the model; re-compacting it here (or
@@ -1332,9 +1354,20 @@ export function preprocessForTool(text, toolName, pressure = 0) {
1332
1354
  if (tool === 'read') {
1333
1355
  return makeRecoverable(text, compactReadOutput(preprocessRead(text)));
1334
1356
  }
1357
+ // JSON array crush (deterministic, cache-safe, reversible). Now via the content router,
1358
+ // so it fires on arrays of homogeneous objects whether they are the WHOLE result or
1359
+ // EMBEDDED inside a larger one (gh api with a header, MCP envelopes, bash echo + json…).
1360
+ // Repeated per-row keys collapse to a single header; each crushed span is self-recoverable
1361
+ // (its marker carries squeezr_expand). Runs BEFORE the base pipeline so line-dedup/JSON-minify
1362
+ // never touch the structured data; returned directly.
1363
+ const crushed = crushEmbeddedJson(text);
1364
+ if (crushed.savedChars > 0) {
1365
+ hit('jsonTableCrush');
1366
+ return crushed.text;
1367
+ }
1335
1368
  let t = preprocess(text);
1336
1369
  if (tool === 'bash') {
1337
- t = applyBashPatterns(t, pressure);
1370
+ t = applyBashPatterns(t, pressure, query);
1338
1371
  }
1339
1372
  else if (tool === 'grep') {
1340
1373
  const before = t;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * squeezr doctor — reconciles the state nothing else reconciles, because Squeezr's
3
+ * failure mode is SILENT: if the proxy dies, the env var drifts, or the running code is
4
+ * stale, you keep working — you just quietly stop saving tokens (or worse, route direct).
5
+ *
6
+ * Squeezr's take on headroom's `headroom doctor`. Each check is pass / warn / fail / skip;
7
+ * the process exit code is 0 (all good), 1 (warnings), or 2 (a failure) so it composes in
8
+ * scripts. The check LOGIC is pure and unit-tested; runDoctor() wires the real IO.
9
+ */
10
+ export type CheckStatus = 'pass' | 'warn' | 'fail' | 'skip';
11
+ export interface CheckResult {
12
+ name: string;
13
+ status: CheckStatus;
14
+ detail: string;
15
+ }
16
+ type Health = {
17
+ identity?: string;
18
+ version?: string;
19
+ bypassed?: boolean;
20
+ port?: number;
21
+ compression?: {
22
+ requests?: number;
23
+ savings_pct?: number;
24
+ };
25
+ } | null;
26
+ export declare function checkProxy(health: Health): CheckResult;
27
+ export declare function checkVersion(installed: string, running: string | null): CheckResult;
28
+ export declare function checkEnv(baseUrl: string | undefined, port: number): CheckResult;
29
+ export declare function checkBypass(bypassed: boolean | undefined): CheckResult;
30
+ export declare function checkSavings(requests: number | undefined, savingsPct: number | undefined): CheckResult;
31
+ export declare function computeExitCode(checks: CheckResult[]): number;
32
+ export declare function formatDoctor(checks: CheckResult[], exitCode: number): string;
33
+ export declare function runDoctor(): Promise<{
34
+ checks: CheckResult[];
35
+ exitCode: number;
36
+ report: string;
37
+ }>;
38
+ export {};
package/dist/doctor.js ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * squeezr doctor — reconciles the state nothing else reconciles, because Squeezr's
3
+ * failure mode is SILENT: if the proxy dies, the env var drifts, or the running code is
4
+ * stale, you keep working — you just quietly stop saving tokens (or worse, route direct).
5
+ *
6
+ * Squeezr's take on headroom's `headroom doctor`. Each check is pass / warn / fail / skip;
7
+ * the process exit code is 0 (all good), 1 (warnings), or 2 (a failure) so it composes in
8
+ * scripts. The check LOGIC is pure and unit-tested; runDoctor() wires the real IO.
9
+ */
10
+ import { createRequire } from 'node:module';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { dirname, join } from 'node:path';
13
+ import http from 'node:http';
14
+ // ── Pure check logic ─────────────────────────────────────────────────────────
15
+ export function checkProxy(health) {
16
+ if (health && health.identity === 'squeezr') {
17
+ return { name: 'Proxy', status: 'pass', detail: `running v${health.version ?? '?'}${health.port ? ` on :${health.port}` : ''}` };
18
+ }
19
+ return { name: 'Proxy', status: 'fail', detail: 'not reachable — start it with "squeezr start"' };
20
+ }
21
+ export function checkVersion(installed, running) {
22
+ if (!running)
23
+ return { name: 'Version', status: 'skip', detail: 'proxy not running' };
24
+ if (installed === running)
25
+ return { name: 'Version', status: 'pass', detail: `installed and running both v${installed}` };
26
+ return { name: 'Version', status: 'warn', detail: `running proxy is STALE (v${running}) vs installed v${installed} — restart: "squeezr update" or "squeezr start"` };
27
+ }
28
+ export function checkEnv(baseUrl, port) {
29
+ if (!baseUrl) {
30
+ return { name: 'Routing', status: 'warn', detail: 'ANTHROPIC_BASE_URL is unset — traffic is NOT going through Squeezr (open a new terminal after setup)' };
31
+ }
32
+ const loopback = new RegExp(`^https?://(localhost|127\\.0\\.0\\.1):${port}(/|$)`);
33
+ if (loopback.test(baseUrl)) {
34
+ return { name: 'Routing', status: 'pass', detail: `ANTHROPIC_BASE_URL → ${baseUrl}` };
35
+ }
36
+ return { name: 'Routing', status: 'fail', detail: `ANTHROPIC_BASE_URL points elsewhere (${baseUrl}); expected http://localhost:${port}` };
37
+ }
38
+ export function checkBypass(bypassed) {
39
+ if (bypassed === undefined)
40
+ return { name: 'Bypass', status: 'skip', detail: 'proxy not running' };
41
+ if (bypassed)
42
+ return { name: 'Bypass', status: 'warn', detail: 'bypass is ON — compression is disabled (squeezr bypass --off to resume)' };
43
+ return { name: 'Bypass', status: 'pass', detail: 'compression active' };
44
+ }
45
+ export function checkSavings(requests, savingsPct) {
46
+ if (requests === undefined)
47
+ return { name: 'Savings', status: 'skip', detail: 'proxy not running' };
48
+ if (requests === 0)
49
+ return { name: 'Savings', status: 'skip', detail: 'no traffic yet' };
50
+ if (!savingsPct || savingsPct <= 0) {
51
+ return { name: 'Savings', status: 'warn', detail: `${requests} requests seen but ~0% saved — bypass on? stale build? all content incompressible?` };
52
+ }
53
+ return { name: 'Savings', status: 'pass', detail: `~${Math.round(savingsPct)}% saved over ${requests} requests` };
54
+ }
55
+ export function computeExitCode(checks) {
56
+ if (checks.some(c => c.status === 'fail'))
57
+ return 2;
58
+ if (checks.some(c => c.status === 'warn'))
59
+ return 1;
60
+ return 0;
61
+ }
62
+ const ICON = { pass: '[ok]', warn: '[warn]', fail: '[FAIL]', skip: '[skip]' };
63
+ export function formatDoctor(checks, exitCode) {
64
+ const lines = ['Squeezr doctor', ''];
65
+ for (const c of checks)
66
+ lines.push(` ${ICON[c.status].padEnd(7)} ${c.name.padEnd(9)} ${c.detail}`);
67
+ lines.push('');
68
+ lines.push(exitCode === 0 ? 'All good.' : exitCode === 1 ? 'Warnings — Squeezr works but may not be saving optimally.' : 'Failure — Squeezr is not in the request path. Fix the [FAIL] above.');
69
+ return lines.join('\n');
70
+ }
71
+ // ── IO orchestrator ──────────────────────────────────────────────────────────
72
+ function installedVersion() {
73
+ try {
74
+ const require = createRequire(import.meta.url);
75
+ return require(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')).version;
76
+ }
77
+ catch {
78
+ return '?';
79
+ }
80
+ }
81
+ function probeHealth(port, timeoutMs = 1500) {
82
+ return new Promise(resolve => {
83
+ const req = http.get({ host: '127.0.0.1', port, path: '/squeezr/health' }, res => {
84
+ let data = '';
85
+ res.on('data', c => { data += c; });
86
+ res.on('end', () => {
87
+ try {
88
+ const json = JSON.parse(data);
89
+ resolve(json && json.identity === 'squeezr' ? json : null);
90
+ }
91
+ catch {
92
+ resolve(null);
93
+ }
94
+ });
95
+ });
96
+ req.on('error', () => resolve(null));
97
+ req.setTimeout(timeoutMs, () => { req.destroy(); resolve(null); });
98
+ });
99
+ }
100
+ export async function runDoctor() {
101
+ const port = process.env.SQUEEZR_PORT ? parseInt(process.env.SQUEEZR_PORT) : 8080;
102
+ const health = await probeHealth(port);
103
+ const runningVersion = health?.version ?? null;
104
+ const checks = [
105
+ checkProxy(health),
106
+ checkVersion(installedVersion(), runningVersion),
107
+ checkEnv(process.env.ANTHROPIC_BASE_URL, health?.port ?? port),
108
+ checkBypass(health?.bypassed),
109
+ checkSavings(health?.compression?.requests, health?.compression?.savings_pct),
110
+ ];
111
+ const exitCode = computeExitCode(checks);
112
+ return { checks, exitCode, report: formatDoctor(checks, exitCode) };
113
+ }
package/dist/index.js CHANGED
@@ -46,7 +46,10 @@ if (PORT !== config.port) {
46
46
  const httpServer = createAdaptorServer({ fetch: app.fetch });
47
47
  // Persist caches every 60s so a crash doesn't lose more than a minute of work
48
48
  setInterval(() => { persistSessionCache(); persistExpandStore(); persistHistory(); }, 60_000).unref();
49
- httpServer.listen(PORT, () => {
49
+ // Bind to loopback only: the proxy holds the user's API keys and exposes
50
+ // state-changing control endpoints, so it must never be reachable from the LAN.
51
+ // Matches the desktop proxy / MITM listeners, which already bind 127.0.0.1.
52
+ httpServer.listen(PORT, '127.0.0.1', () => {
50
53
  console.log(`Squeezr v${VERSION} listening on http://localhost:${PORT}`);
51
54
  console.log(`Mode: ${config.dryRun ? 'dry-run' : 'active'}`);
52
55
  if (config.disabled)
@@ -0,0 +1,34 @@
1
+ /**
2
+ * JSON array crusher — Squeezr's take on headroom's SmartCrusher, deterministic path.
3
+ *
4
+ * A huge amount of tool output is an ARRAY OF HOMOGENEOUS OBJECTS: `gh api`, `curl`
5
+ * to a REST endpoint, MCP tools that return record lists, `kubectl get -o json`, etc.
6
+ * Minifying that JSON removes whitespace but leaves EVERY key repeated on EVERY row —
7
+ * `"id":`, `"name":`, `"status":`… ×N. The dominant cost is the repeated schema.
8
+ *
9
+ * This reshapes such an array into a compact table: the column names appear ONCE in a
10
+ * header, then one row of values per element. No key is repeated. It is:
11
+ *
12
+ * - Deterministic → same input → byte-identical output → prompt-cache safe.
13
+ * - Reversible → the full original JSON is stored in the expand store; the marker
14
+ * carries `squeezr_expand("<id>")` so the model can get exact JSON.
15
+ * - Lossless (repr) → every value still appears in the table body; nothing is dropped.
16
+ *
17
+ * Conservative by design: it only fires when the WHOLE (trimmed) input is a JSON array of
18
+ * mostly-objects, there are enough rows, and the table actually saves a meaningful amount.
19
+ * Anything else is returned untouched. This is the low-risk, high-value core; row-dropping
20
+ * (the lossy SmartCrusher path) is intentionally NOT done here.
21
+ */
22
+ export declare const TABLE_MARKER_RE: RegExp;
23
+ /** 32-bit SimHash fingerprint of a text (bit-voting over its word tokens). Two texts
24
+ * that share most tokens get fingerprints a small Hamming distance apart. */
25
+ export declare function simhash32(text: string): number;
26
+ export declare function hammingDistance(a: number, b: number): number;
27
+ /**
28
+ * If `text` is (entirely) a crushable JSON array of objects, return the table form plus
29
+ * the char count saved. Otherwise return the input unchanged with savedChars = 0.
30
+ */
31
+ export declare function crushJsonArrays(text: string): {
32
+ text: string;
33
+ savedChars: number;
34
+ };
@@ -0,0 +1,177 @@
1
+ /**
2
+ * JSON array crusher — Squeezr's take on headroom's SmartCrusher, deterministic path.
3
+ *
4
+ * A huge amount of tool output is an ARRAY OF HOMOGENEOUS OBJECTS: `gh api`, `curl`
5
+ * to a REST endpoint, MCP tools that return record lists, `kubectl get -o json`, etc.
6
+ * Minifying that JSON removes whitespace but leaves EVERY key repeated on EVERY row —
7
+ * `"id":`, `"name":`, `"status":`… ×N. The dominant cost is the repeated schema.
8
+ *
9
+ * This reshapes such an array into a compact table: the column names appear ONCE in a
10
+ * header, then one row of values per element. No key is repeated. It is:
11
+ *
12
+ * - Deterministic → same input → byte-identical output → prompt-cache safe.
13
+ * - Reversible → the full original JSON is stored in the expand store; the marker
14
+ * carries `squeezr_expand("<id>")` so the model can get exact JSON.
15
+ * - Lossless (repr) → every value still appears in the table body; nothing is dropped.
16
+ *
17
+ * Conservative by design: it only fires when the WHOLE (trimmed) input is a JSON array of
18
+ * mostly-objects, there are enough rows, and the table actually saves a meaningful amount.
19
+ * Anything else is returned untouched. This is the low-risk, high-value core; row-dropping
20
+ * (the lossy SmartCrusher path) is intentionally NOT done here.
21
+ */
22
+ import { storeOriginal } from './expand.js';
23
+ const MIN_ITEMS = 5;
24
+ const MIN_OBJECT_FRACTION = 0.8; // ≥80% of elements must be plain objects
25
+ const MIN_SAVINGS_RATIO = 0.15; // only crush if it shaves ≥15% of the chars
26
+ const LOSSY_ROW_THRESHOLD = 50; // above this many rows, drop near-duplicates (SimHash)
27
+ const HAMMING_NEAR_DUP = 3; // rows within this 32-bit Hamming distance are "the same"
28
+ const ROW_SIGNAL_RE = /\b(error|err|fail|failed|failure|exception|fatal|panic|denied|refused|timeout|critical|crash|oom|unhealthy|degraded)\b/i;
29
+ // Recognises the table header this module emits (anywhere in the text, since the table
30
+ // may be embedded in a larger tool result). Group 1 = the expand id.
31
+ export const TABLE_MARKER_RE = /\[squeezr:table ([0-9a-f]{6,}(?:~\d+)?) —/;
32
+ function isPlainObject(v) {
33
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
34
+ }
35
+ // ── SimHash near-duplicate detection (deterministic, dependency-free) ─────────
36
+ /** FNV-1a 32-bit hash of a token. */
37
+ function fnv1a32(s) {
38
+ let h = 0x811c9dc5;
39
+ for (let i = 0; i < s.length; i++) {
40
+ h ^= s.charCodeAt(i);
41
+ h = Math.imul(h, 0x01000193);
42
+ }
43
+ return h >>> 0;
44
+ }
45
+ /** 32-bit SimHash fingerprint of a text (bit-voting over its word tokens). Two texts
46
+ * that share most tokens get fingerprints a small Hamming distance apart. */
47
+ export function simhash32(text) {
48
+ const tokens = text.toLowerCase().match(/[a-z0-9_]+/g) ?? [];
49
+ if (tokens.length === 0)
50
+ return 0;
51
+ const bits = new Array(32).fill(0);
52
+ for (const t of tokens) {
53
+ const h = fnv1a32(t);
54
+ for (let b = 0; b < 32; b++)
55
+ bits[b] += ((h >>> b) & 1) ? 1 : -1;
56
+ }
57
+ let f = 0;
58
+ for (let b = 0; b < 32; b++)
59
+ if (bits[b] > 0)
60
+ f |= (1 << b);
61
+ return f >>> 0;
62
+ }
63
+ export function hammingDistance(a, b) {
64
+ let x = (a ^ b) >>> 0;
65
+ let c = 0;
66
+ while (x) {
67
+ c += x & 1;
68
+ x >>>= 1;
69
+ }
70
+ return c;
71
+ }
72
+ /**
73
+ * Given the per-row rendered strings, return the indices to KEEP: one representative per
74
+ * SimHash cluster (near-duplicates dropped), plus every row that carries an error/anomaly
75
+ * signal (always kept). Order preserved. Deterministic.
76
+ */
77
+ function selectRepresentativeRows(rowStrings) {
78
+ const reps = []; // simhash fingerprints of chosen representatives
79
+ const kept = [];
80
+ for (let i = 0; i < rowStrings.length; i++) {
81
+ if (ROW_SIGNAL_RE.test(rowStrings[i])) {
82
+ kept.push(i);
83
+ continue;
84
+ } // anomaly → always keep
85
+ const sig = simhash32(rowStrings[i]);
86
+ if (reps.some(r => hammingDistance(r, sig) <= HAMMING_NEAR_DUP))
87
+ continue; // near-dup → drop
88
+ reps.push(sig);
89
+ kept.push(i);
90
+ }
91
+ return kept;
92
+ }
93
+ /** Union of keys across rows, in first-seen order (deterministic). */
94
+ function collectColumns(rows) {
95
+ const cols = [];
96
+ const seen = new Set();
97
+ for (const row of rows) {
98
+ if (!isPlainObject(row))
99
+ continue;
100
+ for (const k of Object.keys(row)) {
101
+ if (!seen.has(k)) {
102
+ seen.add(k);
103
+ cols.push(k);
104
+ }
105
+ }
106
+ }
107
+ return cols;
108
+ }
109
+ const CELL_DELIM = '\t';
110
+ /** Render one cell. Scalars raw; strings raw unless they carry the delimiter/newline;
111
+ * objects/arrays as compact JSON. Absent keys → empty cell. */
112
+ function cell(value, present) {
113
+ if (!present)
114
+ return '';
115
+ if (value === null)
116
+ return 'null';
117
+ const t = typeof value;
118
+ if (t === 'number' || t === 'boolean')
119
+ return String(value);
120
+ if (t === 'string') {
121
+ const s = value;
122
+ return s.includes(CELL_DELIM) || s.includes('\n') ? JSON.stringify(s) : s;
123
+ }
124
+ return JSON.stringify(value);
125
+ }
126
+ /**
127
+ * If `text` is (entirely) a crushable JSON array of objects, return the table form plus
128
+ * the char count saved. Otherwise return the input unchanged with savedChars = 0.
129
+ */
130
+ export function crushJsonArrays(text) {
131
+ const trimmed = text.trim();
132
+ if (trimmed.length === 0 || (trimmed[0] !== '['))
133
+ return { text, savedChars: 0 };
134
+ let parsed;
135
+ try {
136
+ parsed = JSON.parse(trimmed);
137
+ }
138
+ catch {
139
+ return { text, savedChars: 0 };
140
+ }
141
+ if (!Array.isArray(parsed) || parsed.length < MIN_ITEMS)
142
+ return { text, savedChars: 0 };
143
+ const objectCount = parsed.filter(isPlainObject).length;
144
+ if (objectCount / parsed.length < MIN_OBJECT_FRACTION)
145
+ return { text, savedChars: 0 };
146
+ const rows = parsed;
147
+ const cols = collectColumns(rows.filter(isPlainObject));
148
+ if (cols.length === 0)
149
+ return { text, savedChars: 0 };
150
+ const header = cols.join(CELL_DELIM);
151
+ const rowLines = rows.map(row => isPlainObject(row)
152
+ ? cols.map(c => cell(row[c], c in row)).join(CELL_DELIM)
153
+ // Non-object element inside a mostly-object array: keep it verbatim as JSON so
154
+ // nothing is lost (recoverable in full via expand anyway).
155
+ : JSON.stringify(row));
156
+ // Lossy row-drop for LARGE arrays: collapse near-duplicate rows (SimHash), always
157
+ // keeping anomaly/error rows. Deterministic → cache-safe. The full original is in the
158
+ // expand store, so dropped rows are recoverable. Small arrays keep every row.
159
+ let bodyLines = rowLines;
160
+ let omitted = 0;
161
+ if (rows.length > LOSSY_ROW_THRESHOLD) {
162
+ const kept = selectRepresentativeRows(rowLines);
163
+ if (kept.length < rowLines.length) {
164
+ bodyLines = kept.map(i => rowLines[i]);
165
+ omitted = rowLines.length - kept.length;
166
+ }
167
+ }
168
+ const id = storeOriginal(text);
169
+ const marker = omitted > 0
170
+ ? `[squeezr:table ${id} — showing ${bodyLines.length} of ${rows.length} rows × ${cols.length} cols; ${omitted} near-duplicate rows omitted; squeezr_expand("${id}") for original JSON]`
171
+ : `[squeezr:table ${id} — ${rows.length} rows × ${cols.length} cols; squeezr_expand("${id}") for original JSON]`;
172
+ const out = `${marker}\n${header}\n${bodyLines.join('\n')}`;
173
+ const savedChars = text.length - out.length;
174
+ if (savedChars <= 0 || savedChars / text.length < MIN_SAVINGS_RATIO)
175
+ return { text, savedChars: 0 };
176
+ return { text: out, savedChars };
177
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * squeezr learn — offline mining of Claude Code sessions for wasteful LOOPS, writing
3
+ * corrections to CLAUDE.local.md. Squeezr's take on headroom's `headroom learn`.
4
+ *
5
+ * The highest-value, cheapest signal is loop detection (no LLM needed):
6
+ * - error loops — the same command fails and is retried ≥3× (the agent is stuck).
7
+ * - refetch loops — the agent re-runs pagination VARIANTS of a command (`| head -50`
8
+ * → `head -100` → `head -200`) because the output kept getting truncated. These
9
+ * SUCCEED, so failure-only analysis misses them, yet each re-fetch re-bills the
10
+ * whole (bigger) output. Canonicalising away the pagination fragment collapses the
11
+ * variants to one signature so the loop becomes visible.
12
+ *
13
+ * Wasted bytes are a MEASURED lower bound from real tool-output sizes, not a guess.
14
+ *
15
+ * The pure functions here (canonicalSignature / extractToolCalls / detectLoops /
16
+ * renderCorrections / writeMarkerBlock) are unit-tested; runLearn() is the thin
17
+ * filesystem orchestrator the CLI calls.
18
+ */
19
+ export interface ToolCall {
20
+ tool: string;
21
+ signature: string;
22
+ raw: string;
23
+ isError: boolean;
24
+ outputBytes: number;
25
+ }
26
+ export type LoopKind = 'error' | 'refetch';
27
+ export interface Loop {
28
+ kind: LoopKind;
29
+ signature: string;
30
+ count: number;
31
+ wastedBytes: number;
32
+ sampleRaw: string;
33
+ tool: string;
34
+ }
35
+ export declare const MIN_OCCURRENCES = 3;
36
+ /** Stable, pagination-stripped signature. Same intent → same signature. */
37
+ export declare function canonicalSignature(tool: string, input: unknown): string;
38
+ /**
39
+ * Extract normalized tool calls from Claude Code session JSONL lines. Tolerant of
40
+ * malformed lines and both string/array tool_result content shapes.
41
+ */
42
+ export declare function extractToolCalls(lines: string[]): ToolCall[];
43
+ export declare function detectLoops(calls: ToolCall[], minOccurrences?: number): Loop[];
44
+ export declare const LEARN_START = "<!-- squeezr:learn:start -->";
45
+ export declare const LEARN_END = "<!-- squeezr:learn:end -->";
46
+ export declare function renderCorrections(loops: Loop[]): string;
47
+ /**
48
+ * Insert/replace the squeezr-managed block in a CLAUDE.local.md-style file. Idempotent:
49
+ * re-applying the same rules yields byte-identical content; a new set replaces the old.
50
+ */
51
+ export declare function writeMarkerBlock(existing: string, rulesBody: string): string;
52
+ export interface LearnOptions {
53
+ apply?: boolean;
54
+ projectsDir?: string;
55
+ targetFile?: string;
56
+ maxSessions?: number;
57
+ }
58
+ export interface LearnResult {
59
+ sessionsScanned: number;
60
+ loops: Loop[];
61
+ report: string;
62
+ applied: boolean;
63
+ targetFile: string;
64
+ }
65
+ export declare function runLearn(opts?: LearnOptions): LearnResult;