squeezr-ai 1.46.2 → 1.80.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.
Files changed (119) hide show
  1. package/README.md +189 -315
  2. package/bin/squeezr.js +2535 -2251
  3. package/dist/__tests__/aiRateLimit.test.d.ts +1 -0
  4. package/dist/__tests__/aiRateLimit.test.js +20 -0
  5. package/dist/__tests__/attachmentDedup.test.d.ts +1 -0
  6. package/dist/__tests__/attachmentDedup.test.js +89 -0
  7. package/dist/__tests__/compressibilityProbe.test.d.ts +1 -0
  8. package/dist/__tests__/compressibilityProbe.test.js +45 -0
  9. package/dist/__tests__/compressionGuard.test.d.ts +1 -0
  10. package/dist/__tests__/compressionGuard.test.js +57 -0
  11. package/dist/__tests__/compressor.test.js +104 -51
  12. package/dist/__tests__/diffRead.test.d.ts +1 -0
  13. package/dist/__tests__/diffRead.test.js +83 -0
  14. package/dist/__tests__/glossaryStore.test.d.ts +1 -0
  15. package/dist/__tests__/glossaryStore.test.js +37 -0
  16. package/dist/__tests__/glossarySub.test.d.ts +1 -0
  17. package/dist/__tests__/glossarySub.test.js +162 -0
  18. package/dist/__tests__/imageDedup.test.d.ts +1 -0
  19. package/dist/__tests__/imageDedup.test.js +80 -0
  20. package/dist/__tests__/largeBlock.test.d.ts +1 -0
  21. package/dist/__tests__/largeBlock.test.js +35 -0
  22. package/dist/__tests__/mcpFilter.test.d.ts +1 -0
  23. package/dist/__tests__/mcpFilter.test.js +87 -0
  24. package/dist/__tests__/newFeatures.test.d.ts +1 -0
  25. package/dist/__tests__/newFeatures.test.js +124 -0
  26. package/dist/__tests__/qualityHarness.test.d.ts +1 -0
  27. package/dist/__tests__/qualityHarness.test.js +98 -0
  28. package/dist/__tests__/rateLimitHeaders.test.js +6 -0
  29. package/dist/__tests__/requestCapture.test.d.ts +1 -0
  30. package/dist/__tests__/requestCapture.test.js +37 -0
  31. package/dist/__tests__/skillDedup.test.d.ts +1 -0
  32. package/dist/__tests__/skillDedup.test.js +57 -0
  33. package/dist/__tests__/staleTurns.test.d.ts +1 -0
  34. package/dist/__tests__/staleTurns.test.js +113 -0
  35. package/dist/__tests__/structuredGuard.test.d.ts +1 -0
  36. package/dist/__tests__/structuredGuard.test.js +72 -0
  37. package/dist/__tests__/toolDescComp.test.d.ts +1 -0
  38. package/dist/__tests__/toolDescComp.test.js +157 -0
  39. package/dist/__tests__/toolResultDedup.test.d.ts +1 -0
  40. package/dist/__tests__/toolResultDedup.test.js +40 -0
  41. package/dist/aiRateLimit.d.ts +19 -0
  42. package/dist/aiRateLimit.js +35 -0
  43. package/dist/aiToggle.d.ts +14 -0
  44. package/dist/aiToggle.js +53 -0
  45. package/dist/attachmentCompress.d.ts +9 -0
  46. package/dist/attachmentCompress.js +211 -0
  47. package/dist/attachmentDedup.d.ts +9 -0
  48. package/dist/attachmentDedup.js +89 -0
  49. package/dist/bypass.d.ts +6 -3
  50. package/dist/bypass.js +37 -5
  51. package/dist/cache.d.ts +3 -0
  52. package/dist/cache.js +10 -0
  53. package/dist/circuitBreaker.d.ts +4 -2
  54. package/dist/circuitBreaker.js +6 -3
  55. package/dist/compressibilityProbe.d.ts +8 -0
  56. package/dist/compressibilityProbe.js +47 -0
  57. package/dist/compressionGuard.d.ts +31 -0
  58. package/dist/compressionGuard.js +101 -0
  59. package/dist/compressor.d.ts +51 -1
  60. package/dist/compressor.js +599 -73
  61. package/dist/config.d.ts +21 -1
  62. package/dist/config.js +58 -2
  63. package/dist/dashboard.d.ts +3 -1
  64. package/dist/dashboard.js +2163 -1655
  65. package/dist/diffRead.d.ts +9 -0
  66. package/dist/diffRead.js +149 -0
  67. package/dist/expand.d.ts +2 -0
  68. package/dist/expand.js +6 -0
  69. package/dist/glossaryStore.d.ts +28 -0
  70. package/dist/glossaryStore.js +131 -0
  71. package/dist/glossarySub.d.ts +38 -0
  72. package/dist/glossarySub.js +123 -0
  73. package/dist/history.d.ts +35 -1
  74. package/dist/history.js +31 -5
  75. package/dist/identGlossary.d.ts +20 -0
  76. package/dist/identGlossary.js +215 -0
  77. package/dist/imageDedup.d.ts +12 -0
  78. package/dist/imageDedup.js +98 -0
  79. package/dist/index.js +7 -0
  80. package/dist/limits.d.ts +5 -2
  81. package/dist/limits.js +47 -4
  82. package/dist/logFeed.d.ts +10 -0
  83. package/dist/logFeed.js +42 -0
  84. package/dist/mcpFilter.d.ts +43 -0
  85. package/dist/mcpFilter.js +89 -0
  86. package/dist/mcpToolFilter.d.ts +32 -0
  87. package/dist/mcpToolFilter.js +140 -0
  88. package/dist/probePort.js +5 -1
  89. package/dist/promptCache.d.ts +44 -0
  90. package/dist/promptCache.js +121 -0
  91. package/dist/qualityGovernor.d.ts +11 -0
  92. package/dist/qualityGovernor.js +69 -0
  93. package/dist/requestCapture.d.ts +21 -0
  94. package/dist/requestCapture.js +79 -0
  95. package/dist/semanticRead.d.ts +9 -0
  96. package/dist/semanticRead.js +188 -0
  97. package/dist/server.js +1398 -992
  98. package/dist/sessionCache.js +9 -2
  99. package/dist/skillDedup.d.ts +5 -0
  100. package/dist/skillDedup.js +89 -0
  101. package/dist/staleTurnSummary.d.ts +9 -0
  102. package/dist/staleTurnSummary.js +110 -0
  103. package/dist/staleTurns.d.ts +14 -0
  104. package/dist/staleTurns.js +80 -0
  105. package/dist/stats.d.ts +16 -3
  106. package/dist/stats.js +157 -21
  107. package/dist/stockToolDescs.d.ts +12 -0
  108. package/dist/stockToolDescs.js +69 -0
  109. package/dist/structuredGuard.d.ts +25 -0
  110. package/dist/structuredGuard.js +116 -0
  111. package/dist/systemPrompt.js +6 -2
  112. package/dist/systemSectioning.d.ts +21 -0
  113. package/dist/systemSectioning.js +111 -0
  114. package/dist/toolDescComp.d.ts +30 -0
  115. package/dist/toolDescComp.js +81 -0
  116. package/dist/toolResultDedup.d.ts +9 -0
  117. package/dist/toolResultDedup.js +88 -0
  118. package/package.json +69 -66
  119. package/squeezr.toml +18 -1
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Tests for v1.47.0 request capture mode.
3
+ * Verifies:
4
+ * - Auth headers are redacted
5
+ * - Disabled flag is a true no-op
6
+ * - Capture limit is honored
7
+ */
8
+ import { describe, it, expect } from 'vitest';
9
+ import { _internal } from '../requestCapture.js';
10
+ describe('requestCapture', () => {
11
+ it('redacts sensitive headers', () => {
12
+ const out = _internal.redactHeaders({
13
+ 'content-type': 'application/json',
14
+ 'authorization': 'Bearer sk-secret-token',
15
+ 'x-api-key': 'sk-ant-secret',
16
+ 'cookie': 'session=abc',
17
+ 'user-agent': 'claude-code/1.0',
18
+ });
19
+ expect(out['authorization']).toBe('<redacted>');
20
+ expect(out['x-api-key']).toBe('<redacted>');
21
+ expect(out['cookie']).toBe('<redacted>');
22
+ expect(out['content-type']).toBe('application/json');
23
+ expect(out['user-agent']).toBe('claude-code/1.0');
24
+ });
25
+ it('redacts header names case-insensitively', () => {
26
+ const out = _internal.redactHeaders({
27
+ 'Authorization': 'Bearer x',
28
+ 'X-Api-Key': 'sk-y',
29
+ });
30
+ expect(out['Authorization']).toBe('<redacted>');
31
+ expect(out['X-Api-Key']).toBe('<redacted>');
32
+ });
33
+ it('generates sequentially-numbered capture filenames', () => {
34
+ const name = _internal.nextCaptureName();
35
+ expect(name).toMatch(/^req-\d{4}\.json$/);
36
+ });
37
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { dedupSkillBlocks } from '../skillDedup.js';
3
+ describe('skillDedup', () => {
4
+ it('collapses exact duplicate blocks in system prompt', () => {
5
+ const block = '## Skill: foo\n' + 'long description line with content\n'.repeat(8) + 'final line';
6
+ const prompt = `intro\n\n${block}\n\nmiddle content\n\n${block}\n\nmore`;
7
+ const r = dedupSkillBlocks(prompt);
8
+ expect(r.dedupCount).toBeGreaterThan(0);
9
+ expect(r.savedChars).toBeGreaterThan(0);
10
+ expect(r.text.length).toBeLessThan(prompt.length);
11
+ expect(r.text).toContain('duplicate of block');
12
+ });
13
+ it('is a no-op on small prompts', () => {
14
+ const r = dedupSkillBlocks('hi');
15
+ expect(r.dedupCount).toBe(0);
16
+ expect(r.text).toBe('hi');
17
+ });
18
+ it('keeps single-occurrence blocks untouched', () => {
19
+ const prompt = '## Section A\nline\nline\nline\nline\n\n## Section B\nline\nline\nline\nline';
20
+ const r = dedupSkillBlocks(prompt);
21
+ expect(r.dedupCount).toBe(0);
22
+ expect(r.text).toBe(prompt);
23
+ });
24
+ it('does not touch blocks below MIN_BLOCK_CHARS', () => {
25
+ const small = 'short\nblock\nhere\nplease';
26
+ const prompt = `${small}\n\nother\n\n${small}`;
27
+ const r = dedupSkillBlocks(prompt);
28
+ expect(r.dedupCount).toBe(0);
29
+ });
30
+ it('does not touch blocks below MIN_BLOCK_LINES', () => {
31
+ const oneLineButLong = 'X'.repeat(500);
32
+ const prompt = `${oneLineButLong}\n\nseparator\n\n${oneLineButLong}`;
33
+ const r = dedupSkillBlocks(prompt);
34
+ expect(r.dedupCount).toBe(0);
35
+ });
36
+ it('preserves separators between non-duplicate blocks', () => {
37
+ const block1 = 'A\n'.repeat(10);
38
+ const block2 = 'B\n'.repeat(10);
39
+ const prompt = `${block1}\n\n${block2}`;
40
+ const r = dedupSkillBlocks(prompt);
41
+ expect(r.text).toBe(prompt);
42
+ });
43
+ it('handles 3+ duplicates collapsing all but first', () => {
44
+ const block = '## Skill\n' + 'description line with content\n'.repeat(8) + 'end-line';
45
+ const between = 'separator block of content\nwith multiple lines\nof content here\nto qualify as block\nwith more text to make it long enough for threshold';
46
+ const prompt = `intro\n\n${block}\n\n${between}\n\n${block}\n\n${between}\n\n${block}\n\nend`;
47
+ const r = dedupSkillBlocks(prompt);
48
+ expect(r.dedupCount).toBeGreaterThanOrEqual(2);
49
+ });
50
+ it('byte-exact matching — one char difference = no dedup', () => {
51
+ const block1 = '## Skill foo\n' + 'description\n'.repeat(8) + 'end';
52
+ const block2 = '## Skill bar\n' + 'description\n'.repeat(8) + 'end'; // different first line
53
+ const prompt = `${block1}\n\n${block2}`;
54
+ const r = dedupSkillBlocks(prompt);
55
+ expect(r.dedupCount).toBe(0);
56
+ });
57
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { collapseStaleTurns } from '../staleTurns.js';
3
+ function makeConversation(turns, assistantContent = 'A short assistant reply.') {
4
+ const msgs = [];
5
+ for (let i = 0; i < turns; i++) {
6
+ msgs.push({ role: 'user', content: `User turn ${i + 1} asking something important.` });
7
+ msgs.push({ role: 'assistant', content: assistantContent });
8
+ }
9
+ return msgs;
10
+ }
11
+ const LONG_TEXT = 'This is a long assistant response. '.repeat(20);
12
+ describe('collapseStaleTurns', () => {
13
+ it('no-op when turns <= threshold', () => {
14
+ const msgs = makeConversation(10, LONG_TEXT);
15
+ const result = collapseStaleTurns(msgs, 10, 5);
16
+ expect(result.savedChars).toBe(0);
17
+ expect(result.collapsedBlocks).toBe(0);
18
+ });
19
+ it('triggers when turns > threshold', () => {
20
+ const msgs = makeConversation(11, LONG_TEXT);
21
+ const result = collapseStaleTurns(msgs, 10, 5);
22
+ expect(result.savedChars).toBeGreaterThan(0);
23
+ expect(result.collapsedBlocks).toBeGreaterThan(0);
24
+ });
25
+ it('never touches user messages', () => {
26
+ const msgs = makeConversation(11, LONG_TEXT);
27
+ const usersBefore = msgs.filter(m => m.role === 'user').map(m => m.content);
28
+ collapseStaleTurns(msgs, 10, 5);
29
+ const usersAfter = msgs.filter(m => m.role === 'user').map(m => m.content);
30
+ expect(usersAfter).toEqual(usersBefore);
31
+ });
32
+ it('never touches tool_use blocks inside assistant messages', () => {
33
+ const msgs = makeConversation(11);
34
+ msgs[1].content = [
35
+ { type: 'tool_use', id: 'tu_1', name: 'Bash', input: { command: 'ls -la' } },
36
+ { type: 'text', text: LONG_TEXT },
37
+ ];
38
+ collapseStaleTurns(msgs, 10, 5);
39
+ const toolBlock = msgs[1].content[0];
40
+ expect(toolBlock.type).toBe('tool_use');
41
+ expect(toolBlock.id).toBe('tu_1');
42
+ });
43
+ it('never touches tool_result blocks inside user messages', () => {
44
+ const msgs = makeConversation(11, LONG_TEXT);
45
+ msgs[0].content = [
46
+ { type: 'tool_result', tool_use_id: 'tu_1', content: LONG_TEXT },
47
+ ];
48
+ collapseStaleTurns(msgs, 10, 5);
49
+ const tr = msgs[0].content[0];
50
+ expect(tr.type).toBe('tool_result');
51
+ expect(tr.tool_use_id).toBe('tu_1');
52
+ });
53
+ it('keeps last keepRecent turns untouched', () => {
54
+ const msgs = makeConversation(12, LONG_TEXT);
55
+ collapseStaleTurns(msgs, 10, 5);
56
+ const assistantMsgs = msgs.filter(m => m.role === 'assistant');
57
+ for (let i = assistantMsgs.length - 5; i < assistantMsgs.length; i++) {
58
+ expect(assistantMsgs[i].content).toBe(LONG_TEXT);
59
+ }
60
+ });
61
+ it('compresses stale assistant string content', () => {
62
+ const msgs = makeConversation(11, LONG_TEXT);
63
+ collapseStaleTurns(msgs, 10, 5);
64
+ const staleAssistant = msgs.filter(m => m.role === 'assistant')[0];
65
+ const compressed = staleAssistant.content;
66
+ expect(compressed.startsWith('[⧖')).toBe(true);
67
+ expect(compressed.length).toBeLessThan(LONG_TEXT.length);
68
+ });
69
+ it('compresses stale assistant text blocks in array content', () => {
70
+ const msgs = makeConversation(11);
71
+ for (let i = 1; i < msgs.length; i += 2) {
72
+ ;
73
+ msgs[i].content = [{ type: 'text', text: LONG_TEXT }];
74
+ }
75
+ collapseStaleTurns(msgs, 10, 5);
76
+ const staleAssistant = msgs.filter(m => m.role === 'assistant')[0];
77
+ const firstBlock = staleAssistant.content[0];
78
+ expect(firstBlock.text.startsWith('[⧖')).toBe(true);
79
+ });
80
+ it('skips blocks below MIN_BLOCK_LEN (250 chars)', () => {
81
+ const msgs = makeConversation(11, 'Short reply.');
82
+ const result = collapseStaleTurns(msgs, 10, 5);
83
+ expect(result.collapsedBlocks).toBe(0);
84
+ expect(result.savedChars).toBe(0);
85
+ });
86
+ it('reports staleCount correctly', () => {
87
+ const msgs = makeConversation(15, LONG_TEXT);
88
+ const result = collapseStaleTurns(msgs, 10, 5);
89
+ expect(result.staleCount).toBe(10);
90
+ });
91
+ it('extracts file path keywords into summary', () => {
92
+ const textWithPath = 'I modified the file src/components/Button.tsx to fix the bug. '.repeat(6);
93
+ const msgs = makeConversation(11, textWithPath);
94
+ collapseStaleTurns(msgs, 10, 5);
95
+ const staleAssistant = msgs.filter(m => m.role === 'assistant')[0];
96
+ expect(staleAssistant.content).toContain('Button.tsx');
97
+ });
98
+ it('handles empty messages array gracefully', () => {
99
+ const result = collapseStaleTurns([], 10, 5);
100
+ expect(result.savedChars).toBe(0);
101
+ expect(result.collapsedBlocks).toBe(0);
102
+ });
103
+ it('non-text blocks in assistant arrays are preserved', () => {
104
+ const msgs = makeConversation(11);
105
+ msgs[1].content = [
106
+ { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'abc' } },
107
+ { type: 'text', text: LONG_TEXT },
108
+ ];
109
+ collapseStaleTurns(msgs, 10, 5);
110
+ const imgBlock = msgs[1].content[0];
111
+ expect(imgBlock.type).toBe('image');
112
+ });
113
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { looksStructured } from '../structuredGuard.js';
3
+ describe('looksStructured', () => {
4
+ it('detects a JSON array', () => {
5
+ expect(looksStructured('[{"a":1},{"a":2}]')).toBe(true);
6
+ });
7
+ it('detects a JSON object', () => {
8
+ expect(looksStructured('{"id":"x","date":"2026-06-09","count":5}')).toBe(true);
9
+ });
10
+ it('detects JSONL (one object per line)', () => {
11
+ const jsonl = [
12
+ '{"id":1,"date":"2026-01-01"}',
13
+ '{"id":2,"date":""}',
14
+ '{"id":3,"date":"2026-01-03"}',
15
+ '{"id":4,"date":"2026-01-04"}',
16
+ ].join('\n');
17
+ expect(looksStructured(jsonl)).toBe(true);
18
+ });
19
+ it('detects a record/dict dump with many key:value lines', () => {
20
+ const dump = [
21
+ 'id: 178895',
22
+ 'date: 2026-06-09',
23
+ 'read: false',
24
+ 'type: info',
25
+ 'title: Hello',
26
+ 'message: world',
27
+ 'userId: abc',
28
+ ].join('\n');
29
+ expect(looksStructured(dump)).toBe(true);
30
+ });
31
+ it('detects pretty-printed quoted-key dumps', () => {
32
+ const dump = [
33
+ '"id": "178895",',
34
+ '"date": "",',
35
+ '"read": false,',
36
+ '"type": "info",',
37
+ '"title": "Hello",',
38
+ '"message": "world",',
39
+ ].join('\n');
40
+ expect(looksStructured(dump)).toBe(true);
41
+ });
42
+ it('detects tab-separated tabular output', () => {
43
+ const table = [
44
+ 'id\tdate\tread',
45
+ '1\t2026-01-01\tfalse',
46
+ '2\t\ttrue',
47
+ '3\t2026-01-03\tfalse',
48
+ '4\t2026-01-04\ttrue',
49
+ ].join('\n');
50
+ expect(looksStructured(table)).toBe(true);
51
+ });
52
+ it('does NOT flag prose', () => {
53
+ const prose = 'The script processed 178,895 notifications and found that most of them ' +
54
+ 'had a non-empty date field. This is a normal paragraph of text that the ' +
55
+ 'AI compressor can safely summarise without losing any important meaning.';
56
+ expect(looksStructured(prose)).toBe(false);
57
+ });
58
+ it('does NOT flag prose with a couple of incidental "foo: bar" lines', () => {
59
+ const prose = [
60
+ 'Here is what I found while reviewing the logs.',
61
+ 'Summary: the build passed and tests are green.',
62
+ 'Next step: deploy to staging and verify behaviour.',
63
+ 'Everything looks good and ready to ship now.',
64
+ ].join('\n');
65
+ expect(looksStructured(prose)).toBe(false);
66
+ });
67
+ it('handles empty / tiny input safely', () => {
68
+ expect(looksStructured('')).toBe(false);
69
+ expect(looksStructured('{')).toBe(false);
70
+ expect(looksStructured('ok')).toBe(false);
71
+ });
72
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,157 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { compressToolDescriptions } from '../toolDescComp.js';
3
+ const LONG_DESC = 'Execute bash commands in a sandbox environment.\n\nUsage notes:\n- Always quote paths.\n- Never use interactive commands.\n- Timeout is 30 seconds.\n\nCommon patterns:\n git status\n npm test\n\nReturns stdout and stderr combined.';
4
+ const SHORT_DESC = 'Short description.';
5
+ describe('compressToolDescriptions', () => {
6
+ it('no-op when tools array is empty', () => {
7
+ const r = compressToolDescriptions([], 0, false, false, false);
8
+ expect(r.savedChars).toBe(0);
9
+ expect(r.totalTools).toBe(0);
10
+ });
11
+ it('no-op when description is shorter than MIN_DESC_LEN', () => {
12
+ const tools = [{ name: 'Bash', description: SHORT_DESC, input_schema: { type: 'object' } }];
13
+ const r = compressToolDescriptions(tools, 0, false, false, false);
14
+ expect(r.savedChars).toBe(0);
15
+ expect(r.compressedTools).toBe(0);
16
+ });
17
+ it('normalizes whitespace in long descriptions', () => {
18
+ const tools = [{ name: 'Bash', description: LONG_DESC + '\n\n\n\n extra ', input_schema: {} }];
19
+ const before = tools[0].description.length;
20
+ const r = compressToolDescriptions(tools, 0, false, false, false);
21
+ const after = tools[0].description.length;
22
+ expect(after).toBeLessThan(before);
23
+ expect(r.savedChars).toBeGreaterThan(0);
24
+ expect(r.compressedTools).toBe(1);
25
+ });
26
+ it('truncates to maxChars when set', () => {
27
+ const tools = [{ name: 'Read', description: LONG_DESC, input_schema: {} }];
28
+ compressToolDescriptions(tools, 80, false, false, false);
29
+ const desc = tools[0].description;
30
+ expect(desc.length).toBeLessThanOrEqual(81);
31
+ expect(desc.endsWith('…')).toBe(true);
32
+ });
33
+ it('no truncation when maxChars is 0', () => {
34
+ const tools = [{ name: 'Read', description: LONG_DESC, input_schema: {} }];
35
+ compressToolDescriptions(tools, 0, false, false, false);
36
+ const desc = tools[0].description;
37
+ expect(desc.endsWith('…')).toBe(false);
38
+ expect(desc.length).toBeGreaterThan(80);
39
+ });
40
+ it('NEVER touches input_schema', () => {
41
+ const schema = { type: 'object', properties: { command: { type: 'string', description: 'A long command description that should not be touched at all ever.' } } };
42
+ const schemaStr = JSON.stringify(schema);
43
+ const tools = [{ name: 'Bash', description: LONG_DESC, input_schema: schema }];
44
+ compressToolDescriptions(tools, 50, false, false, false);
45
+ expect(JSON.stringify(tools[0].input_schema)).toBe(schemaStr);
46
+ });
47
+ it('NEVER touches tool name', () => {
48
+ const tools = [{ name: 'Bash', description: LONG_DESC, input_schema: {} }];
49
+ compressToolDescriptions(tools, 50, false, false, false);
50
+ expect(tools[0].name).toBe('Bash');
51
+ });
52
+ it('skips non-object entries gracefully', () => {
53
+ const tools = [null, undefined, 'string', 42, { name: 'Bash', description: LONG_DESC }];
54
+ expect(() => compressToolDescriptions(tools, 0, false, false, false)).not.toThrow();
55
+ });
56
+ it('skips tools without a description field', () => {
57
+ const tools = [{ name: 'Bash', input_schema: {} }];
58
+ const r = compressToolDescriptions(tools, 0, false, false, false);
59
+ expect(r.savedChars).toBe(0);
60
+ });
61
+ it('does not produce a longer result', () => {
62
+ const tools = [{ name: 'Bash', description: LONG_DESC, input_schema: {} }];
63
+ const before = LONG_DESC.length;
64
+ compressToolDescriptions(tools, 0, false, false, false);
65
+ const after = tools[0].description.length;
66
+ expect(after).toBeLessThanOrEqual(before);
67
+ });
68
+ it('compresses multiple tools independently', () => {
69
+ const tools = [
70
+ { name: 'Bash', description: LONG_DESC + '\n\n\n', input_schema: {} },
71
+ { name: 'Read', description: LONG_DESC + '\n\n\n\n extra ', input_schema: {} },
72
+ { name: 'Tiny', description: SHORT_DESC, input_schema: {} },
73
+ ];
74
+ const r = compressToolDescriptions(tools, 0, false, false, false);
75
+ expect(r.compressedTools).toBe(2);
76
+ expect(r.totalTools).toBe(3);
77
+ expect(r.savedChars).toBeGreaterThan(0);
78
+ });
79
+ it('handles non-array input gracefully', () => {
80
+ const r = compressToolDescriptions('not an array', 0, false, false, false);
81
+ expect(r.savedChars).toBe(0);
82
+ });
83
+ it('first-para mode truncates at first blank line', () => {
84
+ // Need desc > 500 chars with a blank line after first para
85
+ const bigDesc = LONG_DESC + '\n\n' + LONG_DESC + '\n\n' + LONG_DESC; // ~700 chars, has blank lines
86
+ const tools = [{ name: 'Bash', description: bigDesc, input_schema: {} }];
87
+ compressToolDescriptions(tools, 0, true, false, false);
88
+ const desc = tools[0].description;
89
+ expect(desc).toContain('Execute bash commands');
90
+ expect(desc.endsWith('…')).toBe(true);
91
+ expect(desc.length).toBeLessThan(LONG_DESC.length);
92
+ });
93
+ it('first-para mode no-op when description <= MIN_FIRST_PARA_LEN', () => {
94
+ // LONG_DESC is ~230 chars — below the 500-char threshold for first-para
95
+ const tools = [{ name: 'Bash', description: LONG_DESC, input_schema: {} }];
96
+ const before = tools[0].description.length;
97
+ compressToolDescriptions(tools, 0, true, false, false);
98
+ const after = tools[0].description.length;
99
+ // Only whitespace normalization applies (no truncation since < 500 chars)
100
+ expect(after).toBeLessThanOrEqual(before);
101
+ });
102
+ it('first-para mode works on very long descriptions (>500 chars)', () => {
103
+ const bigDesc = LONG_DESC.repeat(3); // ~690 chars, above 500 threshold
104
+ const tools = [{ name: 'Workflow', description: bigDesc, input_schema: {} }];
105
+ compressToolDescriptions(tools, 0, true, false, false);
106
+ const desc = tools[0].description;
107
+ expect(desc.length).toBeLessThan(bigDesc.length);
108
+ expect(desc.endsWith('…')).toBe(true);
109
+ });
110
+ it('safe-only mode skips non-whitelist tools', () => {
111
+ const bigDesc = LONG_DESC.repeat(3);
112
+ const tools = [
113
+ { name: 'Workflow', description: bigDesc, input_schema: {} }, // NOT in whitelist
114
+ { name: 'Bash', description: bigDesc, input_schema: {} }, // IN whitelist
115
+ ];
116
+ compressToolDescriptions(tools, 0, true, true, false);
117
+ const workflow = tools[0].description;
118
+ const bash = tools[1].description;
119
+ expect(workflow).toBe(bigDesc); // untouched
120
+ expect(bash.length).toBeLessThan(bigDesc.length); // compressed
121
+ });
122
+ it('safe-only mode=false compresses all tools', () => {
123
+ const tools = [{ name: 'Workflow', description: LONG_DESC.repeat(3), input_schema: {} }];
124
+ compressToolDescriptions(tools, 0, true, false, false);
125
+ expect(tools[0].description.length).toBeLessThan(LONG_DESC.repeat(3).length);
126
+ });
127
+ it('expand mode embeds squeezr_expand hint in truncated description', () => {
128
+ const bigDesc = LONG_DESC.repeat(3);
129
+ const tools = [{ name: 'Workflow', description: bigDesc, input_schema: {} }];
130
+ compressToolDescriptions(tools, 0, true, false, true);
131
+ const desc = tools[0].description;
132
+ expect(desc).toContain("squeezr_expand(");
133
+ expect(desc.length).toBeLessThan(bigDesc.length);
134
+ });
135
+ it('expand mode works on all tools regardless of whitelist', () => {
136
+ const bigDesc = LONG_DESC.repeat(3);
137
+ const tools = [
138
+ { name: 'Workflow', description: bigDesc, input_schema: {} },
139
+ { name: 'mcp__custom__tool', description: bigDesc, input_schema: {} },
140
+ ];
141
+ compressToolDescriptions(tools, 0, true, true, true); // safeOnly=true but expand=true overrides
142
+ const workflow = tools[0].description;
143
+ const mcp = tools[1].description;
144
+ expect(workflow).toContain('squeezr_expand(');
145
+ expect(mcp).toContain('squeezr_expand(');
146
+ });
147
+ it('expand mode: same content same ID (deterministic for prefix cache)', () => {
148
+ const bigDesc = LONG_DESC.repeat(3);
149
+ const tools1 = [{ name: 'Bash', description: bigDesc, input_schema: {} }];
150
+ const tools2 = [{ name: 'Bash', description: bigDesc, input_schema: {} }];
151
+ compressToolDescriptions(tools1, 0, true, false, true);
152
+ compressToolDescriptions(tools2, 0, true, false, true);
153
+ // Same content → same expand ID → stable for Anthropic prefix cache
154
+ expect(tools1[0].description)
155
+ .toBe(tools2[0].description);
156
+ });
157
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { compressDuplicateToolResults } from '../toolResultDedup.js';
3
+ import { clearExpandStore } from '../expand.js';
4
+ function userToolResult(id, text) {
5
+ return { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, content: text }] };
6
+ }
7
+ describe('compressDuplicateToolResults', () => {
8
+ it('collapses an identical tool output that reappears, keeping the latest full', () => {
9
+ clearExpandStore();
10
+ const big = 'line of bash output that is fairly long '.repeat(40); // > 500 chars
11
+ const msgs = [
12
+ userToolResult('a', big),
13
+ { role: 'assistant', content: [{ type: 'text', text: 'ok' }] },
14
+ userToolResult('b', big), // identical re-run
15
+ ];
16
+ const r = compressDuplicateToolResults(msgs);
17
+ expect(r.collapsedCount).toBe(1);
18
+ expect(r.savedChars).toBeGreaterThan(0);
19
+ // earlier one replaced with a reference, latest kept full
20
+ expect(msgs[0].content[0].content).toMatch(/identical tool output/);
21
+ expect(msgs[2].content[0].content).toBe(big);
22
+ });
23
+ it('ignores short outputs and unique outputs', () => {
24
+ clearExpandStore();
25
+ const msgs = [
26
+ userToolResult('a', 'short'),
27
+ userToolResult('b', 'short'),
28
+ userToolResult('c', 'a unique long output '.repeat(40)),
29
+ ];
30
+ const r = compressDuplicateToolResults(msgs);
31
+ expect(r.collapsedCount).toBe(0);
32
+ });
33
+ it('does not reprocess already-compressed squeezr blocks', () => {
34
+ clearExpandStore();
35
+ const tag = '[squeezr: identical tool output as message #2 — squeezr_expand(abc123)]'.padEnd(600, ' ');
36
+ const msgs = [userToolResult('a', tag), userToolResult('b', tag)];
37
+ const r = compressDuplicateToolResults(msgs);
38
+ expect(r.collapsedCount).toBe(0);
39
+ });
40
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * AI compression rate limiter (v1.60.0)
3
+ *
4
+ * Hard safety net so AI compression can NEVER stampede the user's quota again
5
+ * (the 2026-06-04 incident: 215 Haiku calls in ~10 min). Sliding window — when
6
+ * the window is full, AI compression is skipped (deterministic compression still
7
+ * runs, it's free). Independent of the cache-barrier fix; this is the last line
8
+ * of defence against any future bug that re-enables a burst.
9
+ *
10
+ * Window is process-global (not per-conversation): the user's quota is global.
11
+ */
12
+ /** Returns true if another AI call is allowed right now (and records it). */
13
+ export declare function tryConsumeAiCall(): boolean;
14
+ /** How many calls remain in the current window (for logging / dashboard). */
15
+ export declare function aiCallsRemaining(): number;
16
+ export declare const _config: {
17
+ WINDOW_MS: number;
18
+ MAX_CALLS_PER_WINDOW: number;
19
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * AI compression rate limiter (v1.60.0)
3
+ *
4
+ * Hard safety net so AI compression can NEVER stampede the user's quota again
5
+ * (the 2026-06-04 incident: 215 Haiku calls in ~10 min). Sliding window — when
6
+ * the window is full, AI compression is skipped (deterministic compression still
7
+ * runs, it's free). Independent of the cache-barrier fix; this is the last line
8
+ * of defence against any future bug that re-enables a burst.
9
+ *
10
+ * Window is process-global (not per-conversation): the user's quota is global.
11
+ */
12
+ const WINDOW_MS = 5 * 60 * 1000; // 5 minutes — matches Claude Code's 5h window cadence
13
+ const MAX_CALLS_PER_WINDOW = 20; // generous for normal use, hard ceiling against bursts
14
+ const callTimestamps = [];
15
+ /** Returns true if another AI call is allowed right now (and records it). */
16
+ export function tryConsumeAiCall() {
17
+ const now = Date.now();
18
+ // Drop timestamps outside the window
19
+ while (callTimestamps.length > 0 && now - callTimestamps[0] > WINDOW_MS) {
20
+ callTimestamps.shift();
21
+ }
22
+ if (callTimestamps.length >= MAX_CALLS_PER_WINDOW)
23
+ return false;
24
+ callTimestamps.push(now);
25
+ return true;
26
+ }
27
+ /** How many calls remain in the current window (for logging / dashboard). */
28
+ export function aiCallsRemaining() {
29
+ const now = Date.now();
30
+ while (callTimestamps.length > 0 && now - callTimestamps[0] > WINDOW_MS) {
31
+ callTimestamps.shift();
32
+ }
33
+ return Math.max(0, MAX_CALLS_PER_WINDOW - callTimestamps.length);
34
+ }
35
+ export const _config = { WINDOW_MS, MAX_CALLS_PER_WINDOW };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * AI compression toggle — PERSISTED across restarts.
3
+ *
4
+ * Master on/off for AI compression calls (Haiku/GPT/Gemini), controllable from
5
+ * the dashboard. Persisted to ~/.squeezr/ai-compression.json so a restart keeps
6
+ * the user's choice. Initial value falls back to the TOML `ai_compression`
7
+ * setting (default false) when no persisted file exists.
8
+ *
9
+ * This is the SECOND gate (alongside the per-request rate limit and the cache
10
+ * barrier). When OFF, zero AI calls happen regardless of anything else.
11
+ */
12
+ export declare function isAiCompressionEnabled(): boolean;
13
+ export declare function setAiCompression(val: boolean): void;
14
+ export declare function toggleAiCompression(): boolean;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * AI compression toggle — PERSISTED across restarts.
3
+ *
4
+ * Master on/off for AI compression calls (Haiku/GPT/Gemini), controllable from
5
+ * the dashboard. Persisted to ~/.squeezr/ai-compression.json so a restart keeps
6
+ * the user's choice. Initial value falls back to the TOML `ai_compression`
7
+ * setting (default false) when no persisted file exists.
8
+ *
9
+ * This is the SECOND gate (alongside the per-request rate limit and the cache
10
+ * barrier). When OFF, zero AI calls happen regardless of anything else.
11
+ */
12
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { homedir } from 'node:os';
15
+ import { config } from './config.js';
16
+ const TOGGLE_FILE = join(homedir(), '.squeezr', 'ai-compression.json');
17
+ function loadPersisted() {
18
+ try {
19
+ if (!existsSync(TOGGLE_FILE))
20
+ return config.aiCompression; // TOML default
21
+ const data = JSON.parse(readFileSync(TOGGLE_FILE, 'utf-8'));
22
+ return data.enabled === true;
23
+ }
24
+ catch {
25
+ return config.aiCompression;
26
+ }
27
+ }
28
+ let enabled = loadPersisted();
29
+ function persist() {
30
+ try {
31
+ const dir = join(homedir(), '.squeezr');
32
+ if (!existsSync(dir))
33
+ mkdirSync(dir, { recursive: true });
34
+ const tmp = TOGGLE_FILE + '.tmp';
35
+ writeFileSync(tmp, JSON.stringify({ enabled }));
36
+ renameSync(tmp, TOGGLE_FILE);
37
+ }
38
+ catch {
39
+ /* best-effort */
40
+ }
41
+ }
42
+ export function isAiCompressionEnabled() {
43
+ return enabled;
44
+ }
45
+ export function setAiCompression(val) {
46
+ enabled = val;
47
+ persist();
48
+ console.log(`[squeezr] AI compression ${val ? 'ON — will call the compression backend' : 'OFF — deterministic only (free)'} (persisted)`);
49
+ }
50
+ export function toggleAiCompression() {
51
+ setAiCompression(!enabled);
52
+ return enabled;
53
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Try each type-specific compressor in order; return the first one that
3
+ * produces a meaningfully smaller output. Returns original if none apply.
4
+ */
5
+ export declare function compressAttachmentText(text: string): {
6
+ compressed: string;
7
+ savedChars: number;
8
+ type: string | null;
9
+ };