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
@@ -206,10 +206,12 @@ async function compressWithGptMini(text, apiKey, extra) {
206
206
  return resp.choices[0].message.content ?? '';
207
207
  }
208
208
  async function compressWithGeminiFlash(text, apiKey, extra) {
209
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_FLASH_MODEL}:generateContent?key=${apiKey}`;
209
+ // Pass the key in the x-goog-api-key header, never in the URL query string:
210
+ // URLs leak into logs, proxies and error traces where a secret must not appear.
211
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_FLASH_MODEL}:generateContent`;
210
212
  const resp = await fetch(url, {
211
213
  method: 'POST',
212
- headers: { 'content-type': 'application/json' },
214
+ headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
213
215
  body: JSON.stringify({
214
216
  contents: [{ role: 'user', parts: [{ text: `${promptWith(extra)}\n\n---\n${text.slice(0, OLLAMA_SAFE_INPUT)}` }] }],
215
217
  }),
@@ -663,6 +665,25 @@ function buildAnthropicToolIdMap(messages) {
663
665
  }
664
666
  return { nameMap, skipIds };
665
667
  }
668
+ // Most recent user-typed text across the conversation (skips tool-result-only turns),
669
+ // used as the relevance query for deterministic crushing. Capped so it stays cheap.
670
+ function lastUserText(messages) {
671
+ for (let i = messages.length - 1; i >= 0; i--) {
672
+ const m = messages[i];
673
+ if (m.role !== 'user')
674
+ continue;
675
+ if (typeof m.content === 'string')
676
+ return m.content.slice(0, 2000);
677
+ if (Array.isArray(m.content)) {
678
+ const texts = m.content
679
+ .filter(b => b && b.type === 'text' && typeof b.text === 'string')
680
+ .map(b => b.text);
681
+ if (texts.length > 0)
682
+ return texts.join(' ').slice(0, 2000);
683
+ }
684
+ }
685
+ return '';
686
+ }
666
687
  export async function compressAnthropicMessages(messages, apiKey, config, systemExtraChars = 0) {
667
688
  if (config.disabled)
668
689
  return [messages, emptySavings()];
@@ -792,12 +813,19 @@ export async function compressAnthropicMessages(messages, apiKey, config, system
792
813
  // changes every turn = cache miss = the 2026-06-04 over-bill). One cache miss
793
814
  // happens the first time the level changes; stable forever after.
794
815
  const DET_PRESSURE = 0;
816
+ // BM25 relevance in the deterministic pass is QUERY-dependent → it changes when the
817
+ // user's task changes. That is ONLY cache-safe where the output is not part of a
818
+ // byte-stable cached prefix. So we feed a query ONLY when there are NO cache markers
819
+ // (non-caching clients); for cache-marker clients (Claude Code) query = '' keeps the
820
+ // pass byte-identical between requests. (Relevance for the cached path needs per-block
821
+ // content-hash freezing — a follow-up.)
822
+ const detQuery = hasCacheMarkers ? '' : lastUserText(messages);
795
823
  const detT0 = Date.now();
796
824
  let detSaved = 0;
797
825
  for (const { index, subIndex, text, tool } of allResults) {
798
826
  if (dedupedSet.has(`${index}:${subIndex}`))
799
827
  continue; // already replaced by dedup
800
- const det = preprocessForTool(text, tool, DET_PRESSURE);
828
+ const det = preprocessForTool(text, tool, DET_PRESSURE, detQuery);
801
829
  if (det !== text) {
802
830
  ;
803
831
  msgs[index].content[subIndex].content = det;
@@ -886,7 +914,17 @@ export async function compressAnthropicMessages(messages, apiKey, config, system
886
914
  // Write's content. These are huge and previously unprocessed. Skip the most
887
915
  // recent N assistant turns (`keepRecentAssistant`) so the live tool call
888
916
  // stays intact.
889
- if (config.compressConversation) {
917
+ //
918
+ // DEFAULT OFF (compressToolInputs = false). Unlike tool RESULTS, tool INPUTS are
919
+ // model-authored code that must survive verbatim: Write.content is written to disk
920
+ // byte-for-byte, and Edit.old_string must byte-match the file for the edit to apply.
921
+ // The deterministic pipeline (line dedup, whitespace collapse+trim, JSON minify,
922
+ // timestamp strip) mutates those bytes. Because Squeezr rewrites history, the model
923
+ // then sees its OWN past Write as "body: ... [repeated 4 more times]" with dropped
924
+ // braces/`>`, and propagates that corruption to disk on the next edit. Old-turn
925
+ // bloat is reclaimed safely by stale_turns (whole-turn collapse) instead. Opt back
926
+ // in with compression.compress_tool_inputs = true only if you accept that risk.
927
+ if (config.compressConversation && config.compressToolInputs) {
890
928
  const toolUses = extractAnthropicAssistantToolUses(msgs, config.keepRecentAssistant);
891
929
  let tuSaved = 0;
892
930
  let tuCount = 0;
package/dist/config.d.ts CHANGED
@@ -9,6 +9,7 @@ export declare class Config {
9
9
  readonly aiCompression: boolean;
10
10
  readonly compressSystemPrompt: boolean;
11
11
  readonly compressConversation: boolean;
12
+ readonly compressToolInputs: boolean;
12
13
  readonly captureRequests: boolean;
13
14
  readonly captureLimit: number;
14
15
  readonly staleTurns: boolean;
@@ -42,6 +43,11 @@ export declare class Config {
42
43
  readonly localUpstreamUrl: string;
43
44
  readonly localCompressionModel: string;
44
45
  readonly localDummyKeys: Set<string>;
46
+ readonly outputShaperEnabled: boolean;
47
+ readonly outputVerbositySteering: boolean;
48
+ readonly outputLevel: 1 | 2 | 3 | 4;
49
+ readonly outputEffortRouting: boolean;
50
+ readonly outputMechanicalThinkingFloor: number;
45
51
  constructor();
46
52
  thresholdForPressure(pressure: number): number;
47
53
  shouldSkipTool(toolName: string): boolean;
package/dist/config.js CHANGED
@@ -87,6 +87,7 @@ export class Config {
87
87
  aiCompression;
88
88
  compressSystemPrompt;
89
89
  compressConversation;
90
+ compressToolInputs;
90
91
  captureRequests;
91
92
  captureLimit;
92
93
  staleTurns;
@@ -120,6 +121,11 @@ export class Config {
120
121
  localUpstreamUrl;
121
122
  localCompressionModel;
122
123
  localDummyKeys;
124
+ outputShaperEnabled;
125
+ outputVerbositySteering;
126
+ outputLevel;
127
+ outputEffortRouting;
128
+ outputMechanicalThinkingFloor;
123
129
  constructor() {
124
130
  const t = loadToml();
125
131
  const p = t.proxy ?? {};
@@ -127,6 +133,7 @@ export class Config {
127
133
  const ca = t.cache ?? {};
128
134
  const ad = t.adaptive ?? {};
129
135
  const lo = t.local ?? {};
136
+ const ou = t.output ?? {};
130
137
  this.port = parseInt(env('SQUEEZR_PORT', String(p.port ?? 8080)));
131
138
  this.mitmPort = parseInt(env('SQUEEZR_MITM_PORT', String(p.mitm_port ?? this.port + 1)));
132
139
  this.threshold = parseInt(env('SQUEEZR_THRESHOLD', String(c.threshold ?? 800)));
@@ -140,6 +147,13 @@ export class Config {
140
147
  // compress_system_prompt also makes a Haiku call — gate it behind aiCompression too.
141
148
  this.compressSystemPrompt = (c.compress_system_prompt ?? true) && this.aiCompression;
142
149
  this.compressConversation = c.compress_conversation ?? true; // safe by default — only deterministic on assistant msgs
150
+ // Lossy-clean OLD tool_use INPUTS (Write.content, Edit.old/new_string, Bash.command).
151
+ // DEFAULT FALSE: these are model-authored code that round-trips to disk verbatim
152
+ // (Write) or must byte-match disk (Edit old_string). Running dedup/whitespace/JSON
153
+ // minify over them folded repeated lines into "... [repeated N more times]", dropped
154
+ // braces and JSX `>`, and .trim()'d file bodies — corruption that reached disk when
155
+ // the model later re-edited from its now-mangled view of its own past writes.
156
+ this.compressToolInputs = c.compress_tool_inputs ?? false;
143
157
  this.captureRequests = c.capture_requests ?? false;
144
158
  this.captureLimit = c.capture_limit ?? 20;
145
159
  this.staleTurns = c.stale_turns ?? true;
@@ -183,6 +197,17 @@ export class Config {
183
197
  this.localCompressionModel = env('SQUEEZR_LOCAL_MODEL', lo.compression_model ?? 'qwen2.5-coder:1.5b');
184
198
  const rawDummies = lo.dummy_keys ?? ['ollama', 'lm-studio', 'sk-no-key-required', 'local', 'none', ''];
185
199
  this.localDummyKeys = new Set(rawDummies.map(k => k.toLowerCase()));
200
+ // Output-side token reduction. DEFAULT OFF — opt-in, like all levers that
201
+ // reshape behaviour rather than just clean tool output. Enable via TOML
202
+ // [output] enabled = true, or env SQUEEZR_OUTPUT_SHAPER=1. Both sub-levers
203
+ // (verbosity steering, effort routing) default ON once the master is on.
204
+ this.outputShaperEnabled =
205
+ env('SQUEEZR_OUTPUT_SHAPER', '') === '1' || env('SQUEEZR_OUTPUT_SHAPER', '') === 'true' || (ou.enabled ?? false);
206
+ this.outputVerbositySteering = ou.verbosity_steering ?? true;
207
+ const lvl = ou.level ?? 2;
208
+ this.outputLevel = (lvl >= 1 && lvl <= 4 ? lvl : 2);
209
+ this.outputEffortRouting = ou.effort_routing ?? true;
210
+ this.outputMechanicalThinkingFloor = ou.mechanical_thinking_floor ?? 1024;
186
211
  }
187
212
  thresholdForPressure(pressure) {
188
213
  if (!this.adaptiveEnabled)
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Content router — extends the JSON crusher's reach from "the WHOLE tool result is a JSON
3
+ * array" to "a JSON array appears ANYWHERE inside the tool result". Squeezr's take on the
4
+ * routing idea behind headroom's ContentRouter.
5
+ *
6
+ * A lot of structured data arrives wrapped: `gh api` with a status line, an MCP tool that
7
+ * returns a labelled envelope, a bash command that echoes a header then the JSON. Before,
8
+ * jsonCrush only fired on a pure-JSON result, so those were missed. This locates embedded
9
+ * arrays-of-objects and crushes each in place, leaving the surrounding text untouched.
10
+ *
11
+ * Safety: candidate spans are found by balanced-bracket scanning (string/escape aware),
12
+ * but the real safety net is that each span is handed to crushJsonArrays, which JSON.parses
13
+ * it — a false-positive span that isn't valid JSON simply doesn't crush and is left verbatim.
14
+ * Deterministic → cache-safe. Each crushed span is self-recoverable via its own expand id.
15
+ */
16
+ import { TABLE_MARKER_RE } from './jsonCrush.js';
17
+ export { TABLE_MARKER_RE };
18
+ /** Non-overlapping, top-level spans of `[ {...`-style arrays (arrays of objects). */
19
+ export declare function findObjectArraySpans(text: string): Array<{
20
+ start: number;
21
+ end: number;
22
+ }>;
23
+ /**
24
+ * Crush every embedded array-of-objects in `text`, splicing each crushed table back in and
25
+ * leaving the surrounding text verbatim. Returns the new text and total chars saved.
26
+ */
27
+ export declare function crushEmbeddedJson(text: string): {
28
+ text: string;
29
+ savedChars: number;
30
+ };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Content router — extends the JSON crusher's reach from "the WHOLE tool result is a JSON
3
+ * array" to "a JSON array appears ANYWHERE inside the tool result". Squeezr's take on the
4
+ * routing idea behind headroom's ContentRouter.
5
+ *
6
+ * A lot of structured data arrives wrapped: `gh api` with a status line, an MCP tool that
7
+ * returns a labelled envelope, a bash command that echoes a header then the JSON. Before,
8
+ * jsonCrush only fired on a pure-JSON result, so those were missed. This locates embedded
9
+ * arrays-of-objects and crushes each in place, leaving the surrounding text untouched.
10
+ *
11
+ * Safety: candidate spans are found by balanced-bracket scanning (string/escape aware),
12
+ * but the real safety net is that each span is handed to crushJsonArrays, which JSON.parses
13
+ * it — a false-positive span that isn't valid JSON simply doesn't crush and is left verbatim.
14
+ * Deterministic → cache-safe. Each crushed span is self-recoverable via its own expand id.
15
+ */
16
+ import { crushJsonArrays, TABLE_MARKER_RE } from './jsonCrush.js';
17
+ export { TABLE_MARKER_RE };
18
+ /** Scan from an opening '[' to its matching ']' (exclusive end), string/escape aware.
19
+ * Returns -1 if unbalanced. */
20
+ function scanArray(text, start) {
21
+ let depth = 0;
22
+ let inStr = false;
23
+ for (let i = start; i < text.length; i++) {
24
+ const c = text[i];
25
+ if (inStr) {
26
+ if (c === '\\') {
27
+ i++;
28
+ continue;
29
+ }
30
+ if (c === '"')
31
+ inStr = false;
32
+ continue;
33
+ }
34
+ if (c === '"') {
35
+ inStr = true;
36
+ continue;
37
+ }
38
+ if (c === '[')
39
+ depth++;
40
+ else if (c === ']') {
41
+ depth--;
42
+ if (depth === 0)
43
+ return i + 1;
44
+ }
45
+ }
46
+ return -1;
47
+ }
48
+ /** Non-overlapping, top-level spans of `[ {...`-style arrays (arrays of objects). */
49
+ export function findObjectArraySpans(text) {
50
+ const spans = [];
51
+ const n = text.length;
52
+ let i = 0;
53
+ let inStr = false;
54
+ while (i < n) {
55
+ const c = text[i];
56
+ if (inStr) {
57
+ if (c === '\\') {
58
+ i += 2;
59
+ continue;
60
+ }
61
+ if (c === '"')
62
+ inStr = false;
63
+ i++;
64
+ continue;
65
+ }
66
+ if (c === '"') {
67
+ inStr = true;
68
+ i++;
69
+ continue;
70
+ }
71
+ if (c === '[') {
72
+ // peek past whitespace for '{' → array of objects
73
+ let j = i + 1;
74
+ while (j < n && /\s/.test(text[j]))
75
+ j++;
76
+ if (text[j] === '{') {
77
+ const end = scanArray(text, i);
78
+ if (end > i) {
79
+ spans.push({ start: i, end });
80
+ i = end;
81
+ continue;
82
+ }
83
+ }
84
+ }
85
+ i++;
86
+ }
87
+ return spans;
88
+ }
89
+ /**
90
+ * Crush every embedded array-of-objects in `text`, splicing each crushed table back in and
91
+ * leaving the surrounding text verbatim. Returns the new text and total chars saved.
92
+ */
93
+ export function crushEmbeddedJson(text) {
94
+ const spans = findObjectArraySpans(text);
95
+ if (spans.length === 0)
96
+ return { text, savedChars: 0 };
97
+ let out = '';
98
+ let last = 0;
99
+ let saved = 0;
100
+ for (const { start, end } of spans) {
101
+ out += text.slice(last, start);
102
+ const sub = text.slice(start, end);
103
+ const crushed = crushJsonArrays(sub);
104
+ out += crushed.text;
105
+ saved += crushed.savedChars;
106
+ last = end;
107
+ }
108
+ out += text.slice(last);
109
+ if (saved <= 0)
110
+ return { text, savedChars: 0 };
111
+ return { text: out, savedChars: saved };
112
+ }