squeezr-ai 1.10.0

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 (42) hide show
  1. package/README.md +476 -0
  2. package/bin/squeezr.js +117 -0
  3. package/dist/__tests__/cache.test.d.ts +1 -0
  4. package/dist/__tests__/cache.test.js +73 -0
  5. package/dist/__tests__/compressor.test.d.ts +1 -0
  6. package/dist/__tests__/compressor.test.js +311 -0
  7. package/dist/__tests__/config.test.d.ts +1 -0
  8. package/dist/__tests__/config.test.js +132 -0
  9. package/dist/__tests__/deterministic.test.d.ts +1 -0
  10. package/dist/__tests__/deterministic.test.js +769 -0
  11. package/dist/__tests__/expand.test.d.ts +1 -0
  12. package/dist/__tests__/expand.test.js +192 -0
  13. package/dist/__tests__/sessionCache.test.d.ts +1 -0
  14. package/dist/__tests__/sessionCache.test.js +72 -0
  15. package/dist/cache.d.ts +18 -0
  16. package/dist/cache.js +65 -0
  17. package/dist/compressor.d.ts +49 -0
  18. package/dist/compressor.js +482 -0
  19. package/dist/config.d.ts +27 -0
  20. package/dist/config.js +113 -0
  21. package/dist/deterministic.d.ts +39 -0
  22. package/dist/deterministic.js +1097 -0
  23. package/dist/discover.d.ts +10 -0
  24. package/dist/discover.js +133 -0
  25. package/dist/expand.d.ts +47 -0
  26. package/dist/expand.js +119 -0
  27. package/dist/gain.d.ts +2 -0
  28. package/dist/gain.js +48 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +19 -0
  31. package/dist/server.d.ts +4 -0
  32. package/dist/server.js +253 -0
  33. package/dist/sessionCache.d.ts +30 -0
  34. package/dist/sessionCache.js +17 -0
  35. package/dist/stats.d.ts +29 -0
  36. package/dist/stats.js +90 -0
  37. package/dist/systemPrompt.d.ts +1 -0
  38. package/dist/systemPrompt.js +84 -0
  39. package/dist/version.d.ts +1 -0
  40. package/dist/version.js +1 -0
  41. package/package.json +58 -0
  42. package/squeezr.toml +38 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,192 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { storeOriginal, retrieveOriginal, expandStoreSize, clearExpandStore, injectExpandToolAnthropic, injectExpandToolOpenAI, handleAnthropicExpandCall, handleOpenAIExpandCall, } from '../expand.js';
3
+ describe('storeOriginal / retrieveOriginal', () => {
4
+ beforeEach(() => clearExpandStore());
5
+ it('stores and retrieves original content', () => {
6
+ const id = storeOriginal('hello world');
7
+ expect(retrieveOriginal(id)).toBe('hello world');
8
+ });
9
+ it('returns undefined for unknown ID', () => {
10
+ expect(retrieveOriginal('zzzzzz')).toBeUndefined();
11
+ });
12
+ it('returns a 6-char hex ID', () => {
13
+ const id = storeOriginal('test');
14
+ expect(id).toMatch(/^[a-f0-9]{6}$/);
15
+ });
16
+ it('is deterministic — same content always same ID', () => {
17
+ const id1 = storeOriginal('foo bar baz');
18
+ clearExpandStore();
19
+ const id2 = storeOriginal('foo bar baz');
20
+ expect(id1).toBe(id2);
21
+ });
22
+ it('different content produces different IDs', () => {
23
+ const id1 = storeOriginal('content one');
24
+ const id2 = storeOriginal('content two');
25
+ expect(id1).not.toBe(id2);
26
+ });
27
+ it('overwrites store entry if same content stored twice', () => {
28
+ storeOriginal('same');
29
+ storeOriginal('same');
30
+ expect(expandStoreSize()).toBe(1);
31
+ });
32
+ });
33
+ describe('expandStoreSize / clearExpandStore', () => {
34
+ beforeEach(() => clearExpandStore());
35
+ it('starts at 0', () => {
36
+ expect(expandStoreSize()).toBe(0);
37
+ });
38
+ it('increments on store', () => {
39
+ storeOriginal('a');
40
+ storeOriginal('b');
41
+ expect(expandStoreSize()).toBe(2);
42
+ });
43
+ it('clears to 0', () => {
44
+ storeOriginal('x');
45
+ clearExpandStore();
46
+ expect(expandStoreSize()).toBe(0);
47
+ });
48
+ });
49
+ // ── injectExpandToolAnthropic ─────────────────────────────────────────────────
50
+ describe('injectExpandToolAnthropic', () => {
51
+ it('adds squeezr_expand tool when tools array is empty', () => {
52
+ const body = { tools: [] };
53
+ injectExpandToolAnthropic(body);
54
+ expect(body.tools.length).toBe(1);
55
+ expect(body.tools[0].name).toBe('squeezr_expand');
56
+ });
57
+ it('creates tools array if missing', () => {
58
+ const body = {};
59
+ injectExpandToolAnthropic(body);
60
+ expect(Array.isArray(body.tools)).toBe(true);
61
+ expect(body.tools.length).toBe(1);
62
+ });
63
+ it('does not add duplicate if already injected', () => {
64
+ const body = { tools: [] };
65
+ injectExpandToolAnthropic(body);
66
+ injectExpandToolAnthropic(body);
67
+ expect(body.tools.length).toBe(1);
68
+ });
69
+ it('preserves existing tools', () => {
70
+ const body = { tools: [{ name: 'read_file' }] };
71
+ injectExpandToolAnthropic(body);
72
+ expect(body.tools.length).toBe(2);
73
+ expect(body.tools[0].name).toBe('read_file');
74
+ });
75
+ it('injected tool has correct input_schema', () => {
76
+ const body = {};
77
+ injectExpandToolAnthropic(body);
78
+ const tool = body.tools[0];
79
+ expect(tool.input_schema.properties.id).toBeDefined();
80
+ });
81
+ });
82
+ // ── injectExpandToolOpenAI ────────────────────────────────────────────────────
83
+ describe('injectExpandToolOpenAI', () => {
84
+ it('adds squeezr_expand tool in OpenAI format', () => {
85
+ const body = {};
86
+ injectExpandToolOpenAI(body);
87
+ const tools = body.tools;
88
+ expect(tools[0].type).toBe('function');
89
+ expect(tools[0].function.name).toBe('squeezr_expand');
90
+ });
91
+ it('does not add duplicate', () => {
92
+ const body = {};
93
+ injectExpandToolOpenAI(body);
94
+ injectExpandToolOpenAI(body);
95
+ expect(body.tools.length).toBe(1);
96
+ });
97
+ });
98
+ // ── handleAnthropicExpandCall ─────────────────────────────────────────────────
99
+ describe('handleAnthropicExpandCall', () => {
100
+ beforeEach(() => clearExpandStore());
101
+ it('returns null when no tool_use in response', () => {
102
+ const resp = { content: [{ type: 'text', text: 'hello' }] };
103
+ expect(handleAnthropicExpandCall(resp)).toBeNull();
104
+ });
105
+ it('returns null for non-squeezr tool calls', () => {
106
+ const resp = { content: [{ type: 'tool_use', id: 'x', name: 'read_file', input: { path: '/foo' } }] };
107
+ expect(handleAnthropicExpandCall(resp)).toBeNull();
108
+ });
109
+ it('returns null when ID not in store', () => {
110
+ const resp = {
111
+ content: [{ type: 'tool_use', id: 'call_1', name: 'squeezr_expand', input: { id: 'aabbcc' } }],
112
+ };
113
+ expect(handleAnthropicExpandCall(resp)).toBeNull();
114
+ });
115
+ it('returns toolUseId and original when ID found', () => {
116
+ const id = storeOriginal('the original content');
117
+ const resp = {
118
+ content: [{ type: 'tool_use', id: 'call_abc', name: 'squeezr_expand', input: { id } }],
119
+ };
120
+ const result = handleAnthropicExpandCall(resp);
121
+ expect(result).not.toBeNull();
122
+ expect(result.toolUseId).toBe('call_abc');
123
+ expect(result.original).toBe('the original content');
124
+ });
125
+ it('returns null when response has no content', () => {
126
+ expect(handleAnthropicExpandCall({})).toBeNull();
127
+ });
128
+ });
129
+ // ── handleOpenAIExpandCall ────────────────────────────────────────────────────
130
+ describe('handleOpenAIExpandCall', () => {
131
+ beforeEach(() => clearExpandStore());
132
+ it('returns null when no choices', () => {
133
+ expect(handleOpenAIExpandCall({})).toBeNull();
134
+ });
135
+ it('returns null when no tool_calls', () => {
136
+ const resp = { choices: [{ message: { content: 'hello' } }] };
137
+ expect(handleOpenAIExpandCall(resp)).toBeNull();
138
+ });
139
+ it('returns null for non-squeezr tool calls', () => {
140
+ const resp = {
141
+ choices: [{
142
+ message: {
143
+ tool_calls: [{ id: 'call_1', function: { name: 'read_file', arguments: '{}' } }],
144
+ },
145
+ }],
146
+ };
147
+ expect(handleOpenAIExpandCall(resp)).toBeNull();
148
+ });
149
+ it('returns toolCallId and original when ID found', () => {
150
+ const id = storeOriginal('openai original');
151
+ const resp = {
152
+ choices: [{
153
+ message: {
154
+ tool_calls: [{
155
+ id: 'call_xyz',
156
+ function: { name: 'squeezr_expand', arguments: JSON.stringify({ id }) },
157
+ }],
158
+ },
159
+ }],
160
+ };
161
+ const result = handleOpenAIExpandCall(resp);
162
+ expect(result).not.toBeNull();
163
+ expect(result.toolCallId).toBe('call_xyz');
164
+ expect(result.original).toBe('openai original');
165
+ });
166
+ it('returns null when ID not in store', () => {
167
+ const resp = {
168
+ choices: [{
169
+ message: {
170
+ tool_calls: [{
171
+ id: 'call_1',
172
+ function: { name: 'squeezr_expand', arguments: JSON.stringify({ id: 'zzzzzz' }) },
173
+ }],
174
+ },
175
+ }],
176
+ };
177
+ expect(handleOpenAIExpandCall(resp)).toBeNull();
178
+ });
179
+ it('handles malformed arguments gracefully', () => {
180
+ const resp = {
181
+ choices: [{
182
+ message: {
183
+ tool_calls: [{
184
+ id: 'call_1',
185
+ function: { name: 'squeezr_expand', arguments: 'NOT JSON{{{' },
186
+ }],
187
+ },
188
+ }],
189
+ };
190
+ expect(handleOpenAIExpandCall(resp)).toBeNull();
191
+ });
192
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { hashText, getBlock, setBlock, sessionCacheSize, clearSessionCache, } from '../sessionCache.js';
3
+ describe('sessionCache', () => {
4
+ beforeEach(() => {
5
+ clearSessionCache();
6
+ });
7
+ describe('hashText', () => {
8
+ it('returns a non-empty string', () => {
9
+ expect(hashText('hello')).toBeTruthy();
10
+ });
11
+ it('is deterministic — same input always same hash', () => {
12
+ expect(hashText('foo bar')).toBe(hashText('foo bar'));
13
+ });
14
+ it('produces different hashes for different inputs', () => {
15
+ expect(hashText('abc')).not.toBe(hashText('def'));
16
+ });
17
+ it('returns a 32-char hex string (MD5)', () => {
18
+ expect(hashText('test')).toMatch(/^[a-f0-9]{32}$/);
19
+ });
20
+ });
21
+ describe('getBlock / setBlock', () => {
22
+ it('returns undefined for unknown hash', () => {
23
+ expect(getBlock('nonexistent')).toBeUndefined();
24
+ });
25
+ it('stores and retrieves a block', () => {
26
+ const block = { fullString: '[squeezr:abc123 -80%] summary', savedChars: 100, originalChars: 500 };
27
+ setBlock('key1', block);
28
+ expect(getBlock('key1')).toEqual(block);
29
+ });
30
+ it('overwrites existing block', () => {
31
+ const block1 = { fullString: 'first', savedChars: 10, originalChars: 50 };
32
+ const block2 = { fullString: 'second', savedChars: 20, originalChars: 50 };
33
+ setBlock('k', block1);
34
+ setBlock('k', block2);
35
+ expect(getBlock('k')).toEqual(block2);
36
+ });
37
+ it('stores multiple independent blocks', () => {
38
+ setBlock('k1', { fullString: 'a', savedChars: 1, originalChars: 10 });
39
+ setBlock('k2', { fullString: 'b', savedChars: 2, originalChars: 20 });
40
+ expect(getBlock('k1')?.fullString).toBe('a');
41
+ expect(getBlock('k2')?.fullString).toBe('b');
42
+ });
43
+ });
44
+ describe('sessionCacheSize', () => {
45
+ it('starts at 0 after clear', () => {
46
+ expect(sessionCacheSize()).toBe(0);
47
+ });
48
+ it('increments on new entries', () => {
49
+ setBlock('a', { fullString: 'x', savedChars: 1, originalChars: 10 });
50
+ expect(sessionCacheSize()).toBe(1);
51
+ setBlock('b', { fullString: 'y', savedChars: 1, originalChars: 10 });
52
+ expect(sessionCacheSize()).toBe(2);
53
+ });
54
+ it('does not increment on overwrite', () => {
55
+ setBlock('a', { fullString: 'x', savedChars: 1, originalChars: 10 });
56
+ setBlock('a', { fullString: 'y', savedChars: 2, originalChars: 10 });
57
+ expect(sessionCacheSize()).toBe(1);
58
+ });
59
+ });
60
+ describe('clearSessionCache', () => {
61
+ it('resets size to 0', () => {
62
+ setBlock('x', { fullString: 'y', savedChars: 1, originalChars: 10 });
63
+ clearSessionCache();
64
+ expect(sessionCacheSize()).toBe(0);
65
+ });
66
+ it('makes previously set blocks unretrievable', () => {
67
+ setBlock('k', { fullString: 'v', savedChars: 1, originalChars: 10 });
68
+ clearSessionCache();
69
+ expect(getBlock('k')).toBeUndefined();
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,18 @@
1
+ export declare class CompressionCache {
2
+ private maxEntries;
3
+ private store;
4
+ private hits;
5
+ private misses;
6
+ constructor(maxEntries: number);
7
+ private key;
8
+ get(text: string): string | undefined;
9
+ set(text: string, compressed: string): void;
10
+ stats(): {
11
+ size: number;
12
+ hits: number;
13
+ misses: number;
14
+ hit_rate_pct: number;
15
+ };
16
+ private load;
17
+ private persist;
18
+ }
package/dist/cache.js ADDED
@@ -0,0 +1,65 @@
1
+ import { createHash } from 'crypto';
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ const CACHE_FILE = join(homedir(), '.squeezr', 'cache.json');
6
+ export class CompressionCache {
7
+ maxEntries;
8
+ store = new Map();
9
+ hits = 0;
10
+ misses = 0;
11
+ constructor(maxEntries) {
12
+ this.maxEntries = maxEntries;
13
+ this.load();
14
+ }
15
+ key(text) {
16
+ return createHash('md5').update(text).digest('hex');
17
+ }
18
+ get(text) {
19
+ const entry = this.store.get(this.key(text));
20
+ if (entry) {
21
+ entry.hits++;
22
+ this.hits++;
23
+ return entry.compressed;
24
+ }
25
+ this.misses++;
26
+ return undefined;
27
+ }
28
+ set(text, compressed) {
29
+ if (this.store.size >= this.maxEntries) {
30
+ const oldest = [...this.store.entries()].sort((a, b) => a[1].ts - b[1].ts)[0];
31
+ this.store.delete(oldest[0]);
32
+ }
33
+ this.store.set(this.key(text), { compressed, ts: Date.now(), hits: 0 });
34
+ this.persist();
35
+ }
36
+ stats() {
37
+ const total = this.hits + this.misses;
38
+ return {
39
+ size: this.store.size,
40
+ hits: this.hits,
41
+ misses: this.misses,
42
+ hit_rate_pct: total > 0 ? Math.round((this.hits / total) * 1000) / 10 : 0,
43
+ };
44
+ }
45
+ load() {
46
+ try {
47
+ if (existsSync(CACHE_FILE)) {
48
+ const raw = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
49
+ for (const [k, v] of Object.entries(raw)) {
50
+ this.store.set(k, v);
51
+ }
52
+ }
53
+ }
54
+ catch { /* ignore */ }
55
+ }
56
+ persist() {
57
+ try {
58
+ const dir = join(homedir(), '.squeezr');
59
+ if (!existsSync(dir))
60
+ mkdirSync(dir, { recursive: true });
61
+ writeFileSync(CACHE_FILE, JSON.stringify(Object.fromEntries(this.store)));
62
+ }
63
+ catch { /* ignore */ }
64
+ }
65
+ }
@@ -0,0 +1,49 @@
1
+ import { CompressionCache } from './cache.js';
2
+ import type { Config } from './config.js';
3
+ export interface Savings {
4
+ compressed: number;
5
+ savedChars: number;
6
+ originalChars: number;
7
+ byTool: Array<{
8
+ tool: string;
9
+ savedChars: number;
10
+ originalChars: number;
11
+ }>;
12
+ dryRun: boolean;
13
+ sessionCacheHits: number;
14
+ }
15
+ export declare function getCache(config: Config): CompressionCache;
16
+ interface AnthropicMessage {
17
+ role: string;
18
+ content: string | Array<{
19
+ type: string;
20
+ tool_use_id?: string;
21
+ content?: unknown;
22
+ }>;
23
+ }
24
+ export declare function compressAnthropicMessages(messages: AnthropicMessage[], apiKey: string, config: Config): Promise<[AnthropicMessage[], Savings]>;
25
+ interface OpenAIMessage {
26
+ role: string;
27
+ content?: string | null;
28
+ tool_call_id?: string;
29
+ tool_calls?: Array<{
30
+ id: string;
31
+ function: {
32
+ name: string;
33
+ };
34
+ }>;
35
+ }
36
+ export declare function compressOpenAIMessages(messages: OpenAIMessage[], apiKey: string, config: Config, isLocal?: boolean): Promise<[OpenAIMessage[], Savings]>;
37
+ interface GeminiContent {
38
+ role: string;
39
+ parts: Array<{
40
+ text?: string;
41
+ functionCall?: unknown;
42
+ functionResponse?: {
43
+ name: string;
44
+ response: unknown;
45
+ };
46
+ }>;
47
+ }
48
+ export declare function compressGeminiContents(contents: GeminiContent[], apiKey: string, config: Config): Promise<[GeminiContent[], Savings]>;
49
+ export {};