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.
- package/bin/squeezr.js +82 -1
- package/dist/__tests__/bench.test.d.ts +1 -0
- package/dist/__tests__/bench.test.js +55 -0
- package/dist/__tests__/cacheAligner.test.d.ts +1 -0
- package/dist/__tests__/cacheAligner.test.js +75 -0
- package/dist/__tests__/codeStructure.test.d.ts +1 -0
- package/dist/__tests__/codeStructure.test.js +74 -0
- package/dist/__tests__/compressor.test.js +5 -1
- package/dist/__tests__/contentRouter.test.d.ts +1 -0
- package/dist/__tests__/contentRouter.test.js +76 -0
- package/dist/__tests__/controlEndpointGuard.test.d.ts +1 -0
- package/dist/__tests__/controlEndpointGuard.test.js +61 -0
- package/dist/__tests__/deterministic.test.js +13 -0
- package/dist/__tests__/doctor.test.d.ts +1 -0
- package/dist/__tests__/doctor.test.js +86 -0
- package/dist/__tests__/jsonCrush.test.d.ts +1 -0
- package/dist/__tests__/jsonCrush.test.js +168 -0
- package/dist/__tests__/learn.test.d.ts +1 -0
- package/dist/__tests__/learn.test.js +159 -0
- package/dist/__tests__/outputSavings.test.d.ts +1 -0
- package/dist/__tests__/outputSavings.test.js +49 -0
- package/dist/__tests__/outputShaper.test.d.ts +1 -0
- package/dist/__tests__/outputShaper.test.js +216 -0
- package/dist/__tests__/relevance.test.d.ts +1 -0
- package/dist/__tests__/relevance.test.js +66 -0
- package/dist/__tests__/secureKeyFile.test.d.ts +1 -0
- package/dist/__tests__/secureKeyFile.test.js +27 -0
- package/dist/__tests__/serverBinding.test.d.ts +1 -0
- package/dist/__tests__/serverBinding.test.js +18 -0
- package/dist/__tests__/statsOutput.test.d.ts +1 -0
- package/dist/__tests__/statsOutput.test.js +46 -0
- package/dist/__tests__/textCrusher.test.d.ts +1 -0
- package/dist/__tests__/textCrusher.test.js +133 -0
- package/dist/bench.d.ts +45 -0
- package/dist/bench.js +114 -0
- package/dist/cacheAligner.d.ts +34 -0
- package/dist/cacheAligner.js +73 -0
- package/dist/codexMitm.d.ts +1 -0
- package/dist/codexMitm.js +26 -2
- package/dist/compressor.js +42 -4
- package/dist/config.d.ts +6 -0
- package/dist/config.js +25 -0
- package/dist/contentRouter.d.ts +30 -0
- package/dist/contentRouter.js +112 -0
- package/dist/dashboard.d.ts +1 -1
- package/dist/dashboard.js +26 -7
- package/dist/deterministic.d.ts +4 -1
- package/dist/deterministic.js +43 -10
- package/dist/doctor.d.ts +38 -0
- package/dist/doctor.js +113 -0
- package/dist/index.js +4 -1
- package/dist/jsonCrush.d.ts +34 -0
- package/dist/jsonCrush.js +177 -0
- package/dist/learn.d.ts +65 -0
- package/dist/learn.js +244 -0
- package/dist/outputSavings.d.ts +20 -0
- package/dist/outputSavings.js +52 -0
- package/dist/outputShaper.d.ts +71 -0
- package/dist/outputShaper.js +155 -0
- package/dist/relevance.d.ts +27 -0
- package/dist/relevance.js +78 -0
- package/dist/server.js +106 -11
- package/dist/stats.d.ts +17 -0
- package/dist/stats.js +55 -0
- package/dist/systemPrompt.js +3 -2
- package/dist/textCrusher.d.ts +35 -0
- package/dist/textCrusher.js +160 -0
- package/package.json +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
|
|
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
|
|
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">
|
|
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">
|
|
507
|
-
<div class="cache-card"><div class="cache-label">
|
|
508
|
-
<div class="cache-card"><div class="cache-label">
|
|
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
|
|
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;
|
package/dist/deterministic.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/deterministic.js
CHANGED
|
@@ -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
|
|
920
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
package/dist/doctor.d.ts
ADDED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/learn.d.ts
ADDED
|
@@ -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;
|