@astudioplus/compressor 0.1.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.
- package/CHANGELOG.md +52 -0
- package/LICENSE +20 -0
- package/README.md +167 -0
- package/dist/adapters/agents-md.d.ts +2 -0
- package/dist/adapters/agents-md.js +91 -0
- package/dist/adapters/apply.d.ts +3 -0
- package/dist/adapters/apply.js +83 -0
- package/dist/adapters/claude-code.d.ts +2 -0
- package/dist/adapters/claude-code.js +403 -0
- package/dist/adapters/copilot.d.ts +2 -0
- package/dist/adapters/copilot.js +418 -0
- package/dist/adapters/cursor.d.ts +2 -0
- package/dist/adapters/cursor.js +149 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.js +19 -0
- package/dist/adapters/markers.d.ts +7 -0
- package/dist/adapters/markers.js +129 -0
- package/dist/adapters/types.d.ts +44 -0
- package/dist/adapters/types.js +1 -0
- package/dist/bench/ablate.d.ts +35 -0
- package/dist/bench/ablate.js +163 -0
- package/dist/bench/cell.d.ts +33 -0
- package/dist/bench/cell.js +437 -0
- package/dist/bench/results.d.ts +37 -0
- package/dist/bench/results.js +157 -0
- package/dist/bench/runner.d.ts +24 -0
- package/dist/bench/runner.js +121 -0
- package/dist/bench/tasks.d.ts +4 -0
- package/dist/bench/tasks.js +147 -0
- package/dist/bench/types.d.ts +109 -0
- package/dist/bench/types.js +1 -0
- package/dist/claude/transcripts.d.ts +30 -0
- package/dist/claude/transcripts.js +154 -0
- package/dist/cli/commands/benchmark.d.ts +33 -0
- package/dist/cli/commands/benchmark.js +203 -0
- package/dist/cli/commands/compress.d.ts +8 -0
- package/dist/cli/commands/compress.js +45 -0
- package/dist/cli/commands/count.d.ts +5 -0
- package/dist/cli/commands/count.js +25 -0
- package/dist/cli/commands/hook.d.ts +6 -0
- package/dist/cli/commands/hook.js +30 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +76 -0
- package/dist/cli/commands/report.d.ts +90 -0
- package/dist/cli/commands/report.js +464 -0
- package/dist/cli/commands/savings.d.ts +38 -0
- package/dist/cli/commands/savings.js +196 -0
- package/dist/cli/commands/set-mode.d.ts +5 -0
- package/dist/cli/commands/set-mode.js +13 -0
- package/dist/cli/commands/stats.d.ts +5 -0
- package/dist/cli/commands/stats.js +51 -0
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +11 -0
- package/dist/cli/commands/uninstall.d.ts +7 -0
- package/dist/cli/commands/uninstall.js +22 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +146 -0
- package/dist/copilot-hook-entry.d.ts +1 -0
- package/dist/copilot-hook-entry.js +36 -0
- package/dist/copilot-hook.js +1000 -0
- package/dist/engine/detect.d.ts +2 -0
- package/dist/engine/detect.js +47 -0
- package/dist/engine/index.d.ts +4 -0
- package/dist/engine/index.js +90 -0
- package/dist/engine/policy.d.ts +2 -0
- package/dist/engine/policy.js +48 -0
- package/dist/engine/tiers/code.d.ts +7 -0
- package/dist/engine/tiers/code.js +206 -0
- package/dist/engine/tiers/logs.d.ts +4 -0
- package/dist/engine/tiers/logs.js +139 -0
- package/dist/engine/tiers/structural.d.ts +28 -0
- package/dist/engine/tiers/structural.js +199 -0
- package/dist/engine/types.d.ts +71 -0
- package/dist/engine/types.js +5 -0
- package/dist/hook/copilot.d.ts +5 -0
- package/dist/hook/copilot.js +136 -0
- package/dist/hook/core.d.ts +36 -0
- package/dist/hook/core.js +138 -0
- package/dist/hook/exit.d.ts +22 -0
- package/dist/hook/exit.js +56 -0
- package/dist/hook/post-tool-use.d.ts +5 -0
- package/dist/hook/post-tool-use.js +57 -0
- package/dist/hook-entry.d.ts +1 -0
- package/dist/hook-entry.js +35 -0
- package/dist/hook.js +946 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/ledger/read.d.ts +9 -0
- package/dist/ledger/read.js +91 -0
- package/dist/ledger/write.d.ts +29 -0
- package/dist/ledger/write.js +61 -0
- package/dist/packs/atoms.d.ts +3 -0
- package/dist/packs/atoms.js +108 -0
- package/dist/packs/modes.d.ts +3 -0
- package/dist/packs/modes.js +34 -0
- package/dist/packs/render.d.ts +24 -0
- package/dist/packs/render.js +115 -0
- package/dist/packs/types.d.ts +32 -0
- package/dist/packs/types.js +1 -0
- package/dist/paths.d.ts +29 -0
- package/dist/paths.js +87 -0
- package/dist/tokens/estimate.d.ts +12 -0
- package/dist/tokens/estimate.js +23 -0
- package/dist/tokens/exact.d.ts +5 -0
- package/dist/tokens/exact.js +16 -0
- package/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.js +2 -0
- package/package.json +77 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
// src/copilot-hook-entry.ts
|
|
2
|
+
import process3 from "node:process";
|
|
3
|
+
|
|
4
|
+
// src/engine/types.ts
|
|
5
|
+
var OMISSION_MARKER = "[compressor:";
|
|
6
|
+
|
|
7
|
+
// src/engine/detect.ts
|
|
8
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
9
|
+
"ts",
|
|
10
|
+
"tsx",
|
|
11
|
+
"js",
|
|
12
|
+
"jsx",
|
|
13
|
+
"mjs",
|
|
14
|
+
"cjs",
|
|
15
|
+
"rs",
|
|
16
|
+
"py",
|
|
17
|
+
"go",
|
|
18
|
+
"java",
|
|
19
|
+
"c",
|
|
20
|
+
"h",
|
|
21
|
+
"cpp",
|
|
22
|
+
"hpp",
|
|
23
|
+
"rb",
|
|
24
|
+
"php",
|
|
25
|
+
"swift",
|
|
26
|
+
"kt",
|
|
27
|
+
"scala",
|
|
28
|
+
"sh",
|
|
29
|
+
"zsh",
|
|
30
|
+
"css",
|
|
31
|
+
"scss",
|
|
32
|
+
"sql",
|
|
33
|
+
"toml",
|
|
34
|
+
"yaml",
|
|
35
|
+
"yml"
|
|
36
|
+
]);
|
|
37
|
+
var TEST_LOG_RES = [
|
|
38
|
+
/^(PASS|FAIL)\s/m,
|
|
39
|
+
/^Tests:\s/m,
|
|
40
|
+
/\d+\s+pass(?:ed|ing)\b/,
|
|
41
|
+
/\d+\s+fail(?:ed|ing)\b/,
|
|
42
|
+
/^test result:/m,
|
|
43
|
+
/--- FAIL/,
|
|
44
|
+
/^test\s+\S+\s+\.\.\.\s+(?:ok|FAILED)\s*$/m,
|
|
45
|
+
// node:test spec ('ℹ pass 118') and tap ('# pass 118') summary counters
|
|
46
|
+
/^(?:ℹ|#) (?:tests|suites|pass|fail|cancelled|skipped|todo) \d+\s*$/m
|
|
47
|
+
];
|
|
48
|
+
var BUILD_LOG_RES = [
|
|
49
|
+
/error\[E\d+\]/,
|
|
50
|
+
/^\s*(error|warning)(\s+TS\d+)?:/m,
|
|
51
|
+
/\bCompiling\s/,
|
|
52
|
+
/npm ERR!/
|
|
53
|
+
];
|
|
54
|
+
var STACK_FRAME_RE = /^\s+at .+:\d+:\d+/m;
|
|
55
|
+
function detectKind(content, filePath) {
|
|
56
|
+
if (filePath !== void 0) {
|
|
57
|
+
const ext = extensionOf(filePath);
|
|
58
|
+
if (ext !== void 0 && CODE_EXTENSIONS.has(ext)) return "code";
|
|
59
|
+
}
|
|
60
|
+
if (content.startsWith("#!")) return "code";
|
|
61
|
+
if (TEST_LOG_RES.some((re) => re.test(content))) return "test-log";
|
|
62
|
+
if (BUILD_LOG_RES.some((re) => re.test(content))) return "build-log";
|
|
63
|
+
if (STACK_FRAME_RE.test(content) && /\berror\b/i.test(content)) return "build-log";
|
|
64
|
+
return "generic";
|
|
65
|
+
}
|
|
66
|
+
function extensionOf(filePath) {
|
|
67
|
+
const match = /\.([A-Za-z0-9]+)$/.exec(filePath);
|
|
68
|
+
return match?.[1]?.toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/engine/tiers/structural.ts
|
|
72
|
+
function tierResult(before, after, id) {
|
|
73
|
+
if (after === before) return { content: before };
|
|
74
|
+
return { content: after, transform: { id, charsSaved: before.length - after.length } };
|
|
75
|
+
}
|
|
76
|
+
var ANSI_RE = /\u001b\[[0-9;:<=>?]*[ -\/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)|\u001b[@-_]/g;
|
|
77
|
+
var CONTROL_RE = /[\u0000-\u0008\u000b-\u001f\u007f]/g;
|
|
78
|
+
function stripAnsi(content) {
|
|
79
|
+
const next = content.replace(ANSI_RE, "").replace(CONTROL_RE, "");
|
|
80
|
+
return tierResult(content, next, "strip-ansi");
|
|
81
|
+
}
|
|
82
|
+
var BLANK_RE = /^[ \t]*$/;
|
|
83
|
+
function collapseBlankRuns(content) {
|
|
84
|
+
const lines = content.split("\n");
|
|
85
|
+
const out = [];
|
|
86
|
+
let run = [];
|
|
87
|
+
const flush = () => {
|
|
88
|
+
if (run.length === 0) return;
|
|
89
|
+
if (run.length >= 3) out.push("");
|
|
90
|
+
else out.push(...run);
|
|
91
|
+
run = [];
|
|
92
|
+
};
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
if (BLANK_RE.test(line)) {
|
|
95
|
+
run.push(line);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
flush();
|
|
99
|
+
out.push(line);
|
|
100
|
+
}
|
|
101
|
+
flush();
|
|
102
|
+
return tierResult(content, out.join("\n"), "collapse-blank");
|
|
103
|
+
}
|
|
104
|
+
function dedupeLines(content) {
|
|
105
|
+
const lines = content.split("\n");
|
|
106
|
+
const out = [];
|
|
107
|
+
let i = 0;
|
|
108
|
+
while (i < lines.length) {
|
|
109
|
+
const line = lines[i];
|
|
110
|
+
if (line === void 0) break;
|
|
111
|
+
let run = 1;
|
|
112
|
+
while (i + run < lines.length && lines[i + run] === line) run += 1;
|
|
113
|
+
if (run >= 3 && line.trim() !== "") {
|
|
114
|
+
out.push(line, `[compressor: previous line repeated ${run - 1} more times]`);
|
|
115
|
+
} else {
|
|
116
|
+
for (let k = 0; k < run; k += 1) out.push(line);
|
|
117
|
+
}
|
|
118
|
+
i += run;
|
|
119
|
+
}
|
|
120
|
+
return tierResult(content, out.join("\n"), "dedupe-lines");
|
|
121
|
+
}
|
|
122
|
+
var LINE_NUM_CAPTURE_RE = /^ *(\d+)(?:→|\t)/;
|
|
123
|
+
function lineNumberOf(line) {
|
|
124
|
+
const digits = LINE_NUM_CAPTURE_RE.exec(line)?.[1];
|
|
125
|
+
return digits === void 0 ? void 0 : Number(digits);
|
|
126
|
+
}
|
|
127
|
+
var FAILURE_LINE_RE = /\b(error|fail(ed|ure)?|warn(ing)?|exception|panic|fatal)\b/i;
|
|
128
|
+
function scanFailureLines(lines, firstFileLine) {
|
|
129
|
+
let count = 0;
|
|
130
|
+
const found = [];
|
|
131
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
132
|
+
const line = lines[i];
|
|
133
|
+
if (line === void 0 || line.includes(OMISSION_MARKER)) continue;
|
|
134
|
+
if (!FAILURE_LINE_RE.test(line)) continue;
|
|
135
|
+
count += 1;
|
|
136
|
+
if (found.length < 3) {
|
|
137
|
+
const n = lineNumberOf(line) ?? (firstFileLine === void 0 ? void 0 : firstFileLine + i);
|
|
138
|
+
if (n !== void 0) found.push(n);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { count, lines: found };
|
|
142
|
+
}
|
|
143
|
+
function formatMatchLines(scan) {
|
|
144
|
+
return scan.lines.join(", ") + (scan.count > 3 ? " (first 3)" : "");
|
|
145
|
+
}
|
|
146
|
+
function omissionMarker(a, b, estTokens, meta, style, omittedLines) {
|
|
147
|
+
const head = `[compressor: lines ${a}-${b} omitted (~${estTokens} est tokens)`;
|
|
148
|
+
const limit = b - a + 1;
|
|
149
|
+
if (meta.tool === "read" && meta.filePath !== void 0) {
|
|
150
|
+
const file = meta.filePath;
|
|
151
|
+
if (style === "deterrent") {
|
|
152
|
+
return `${head} \u2014 likely irrelevant; Read ${file} offset=${a} limit=${limit} ONLY if the problem you are chasing points into this range]`;
|
|
153
|
+
}
|
|
154
|
+
if (style === "informative") {
|
|
155
|
+
const scan = scanFailureLines(omittedLines, a);
|
|
156
|
+
if (scan.count === 0) {
|
|
157
|
+
return `${head} \u2014 no error/failure/warning lines in the omitted range; safe to skip. Read ${file} offset=${a} limit=${limit} only if needed]`;
|
|
158
|
+
}
|
|
159
|
+
const nearest = scan.lines[0] ?? a;
|
|
160
|
+
return `${head} \u2014 ${scan.count} lines matching error/fail/warn at lines ${formatMatchLines(scan)} \u2014 Read ${file} offset=${nearest} limit=20 for the nearest match; full range offset=${a} limit=${limit}]`;
|
|
161
|
+
}
|
|
162
|
+
return `${head} \u2014 Read ${file} with offset=${a} and limit=${limit} to retrieve]`;
|
|
163
|
+
}
|
|
164
|
+
if (style === "deterrent") {
|
|
165
|
+
return `${head} \u2014 likely irrelevant; re-run with a narrower filter (grep, --quiet, head) ONLY if the problem you are chasing points into this range]`;
|
|
166
|
+
}
|
|
167
|
+
if (style === "informative") {
|
|
168
|
+
const scan = scanFailureLines(omittedLines, a);
|
|
169
|
+
if (scan.count === 0) {
|
|
170
|
+
return `${head} \u2014 no error/failure/warning lines in the omitted range; safe to skip. Re-run with a narrower filter (grep, --quiet, head) only if needed]`;
|
|
171
|
+
}
|
|
172
|
+
return `${head} \u2014 ${scan.count} lines matching error/fail/warn at lines ${formatMatchLines(scan)} \u2014 re-run with a narrower filter (grep, --quiet, head) to retrieve]`;
|
|
173
|
+
}
|
|
174
|
+
return `${head} \u2014 re-run with a narrower filter (grep, --quiet, head) to retrieve]`;
|
|
175
|
+
}
|
|
176
|
+
function countMarker(count, unit, estTokens, meta, style, omitted) {
|
|
177
|
+
const head = `[compressor: ${count} ${unit} omitted (~${estTokens} est tokens)`;
|
|
178
|
+
if (meta.tool === "read" && meta.filePath !== void 0) {
|
|
179
|
+
const file = meta.filePath;
|
|
180
|
+
if (style === "deterrent") {
|
|
181
|
+
return `${head} \u2014 likely irrelevant; Read ${file} ONLY if the problem you are chasing points into the omitted content]`;
|
|
182
|
+
}
|
|
183
|
+
if (style === "informative") {
|
|
184
|
+
const matches = scanFailureLines(omitted.split("\n")).count;
|
|
185
|
+
return matches === 0 ? `${head} \u2014 no error/failure/warning lines in the omitted content; safe to skip. Read ${file} only if needed]` : `${head} \u2014 ${matches} lines matching error/fail/warn in the omitted content \u2014 Read ${file} to retrieve]`;
|
|
186
|
+
}
|
|
187
|
+
return `${head} \u2014 Read ${file} to retrieve]`;
|
|
188
|
+
}
|
|
189
|
+
if (style === "deterrent") {
|
|
190
|
+
return `${head} \u2014 likely irrelevant; re-run with a narrower filter (grep, --quiet, head) ONLY if the problem you are chasing points into the omitted content]`;
|
|
191
|
+
}
|
|
192
|
+
if (style === "informative") {
|
|
193
|
+
const matches = scanFailureLines(omitted.split("\n")).count;
|
|
194
|
+
return matches === 0 ? `${head} \u2014 no error/failure/warning lines in the omitted content; safe to skip. Re-run with a narrower filter (grep, --quiet, head) only if needed]` : `${head} \u2014 ${matches} lines matching error/fail/warn in the omitted content \u2014 re-run with a narrower filter (grep, --quiet, head) to retrieve]`;
|
|
195
|
+
}
|
|
196
|
+
return `${head} \u2014 re-run with a narrower filter (grep, --quiet, head) to retrieve]`;
|
|
197
|
+
}
|
|
198
|
+
function truncateChars(content, meta, policy, estimate) {
|
|
199
|
+
const est = estimate(content);
|
|
200
|
+
const ratio = policy.truncateBudget / est;
|
|
201
|
+
const headChars = Math.max(1, Math.floor(content.length * ratio * 0.6));
|
|
202
|
+
const tailChars = Math.max(1, Math.floor(content.length * ratio * 0.4));
|
|
203
|
+
if (headChars + tailChars >= content.length) return { content };
|
|
204
|
+
const omitted = content.slice(headChars, content.length - tailChars);
|
|
205
|
+
const marker = countMarker(
|
|
206
|
+
omitted.length,
|
|
207
|
+
"chars",
|
|
208
|
+
estimate(omitted),
|
|
209
|
+
meta,
|
|
210
|
+
policy.markerStyle,
|
|
211
|
+
omitted
|
|
212
|
+
);
|
|
213
|
+
const next = `${content.slice(0, headChars)}
|
|
214
|
+
${marker}
|
|
215
|
+
${content.slice(content.length - tailChars)}`;
|
|
216
|
+
return tierResult(content, next, "truncate");
|
|
217
|
+
}
|
|
218
|
+
function truncateHeadTail(content, meta, policy, estimate, positionsAreFileLines = true) {
|
|
219
|
+
const est = estimate(content);
|
|
220
|
+
if (est <= policy.truncateBudget) return { content };
|
|
221
|
+
const lines = content.split("\n");
|
|
222
|
+
const total = lines.length;
|
|
223
|
+
const ratio = policy.truncateBudget / est;
|
|
224
|
+
const keep = Math.max(2, Math.floor(total * ratio));
|
|
225
|
+
if (keep >= total) return truncateChars(content, meta, policy, estimate);
|
|
226
|
+
const headCount = Math.max(1, Math.floor(keep * 0.6));
|
|
227
|
+
const tailCount = Math.max(1, keep - headCount);
|
|
228
|
+
const omitStart = headCount + 1;
|
|
229
|
+
const omitEnd = total - tailCount;
|
|
230
|
+
if (omitEnd < omitStart) return { content };
|
|
231
|
+
const omittedLines = lines.slice(headCount, total - tailCount);
|
|
232
|
+
const omitted = omittedLines.join("\n");
|
|
233
|
+
const estOmitted = estimate(omitted);
|
|
234
|
+
const a = lineNumberOf(omittedLines[0] ?? "");
|
|
235
|
+
const b = lineNumberOf(omittedLines[omittedLines.length - 1] ?? "");
|
|
236
|
+
const marker = a !== void 0 && b !== void 0 && a <= b ? omissionMarker(a, b, estOmitted, meta, policy.markerStyle, omittedLines) : positionsAreFileLines ? omissionMarker(omitStart, omitEnd, estOmitted, meta, policy.markerStyle, omittedLines) : countMarker(omittedLines.length, "lines", estOmitted, meta, policy.markerStyle, omitted);
|
|
237
|
+
const next = [...lines.slice(0, headCount), marker, ...lines.slice(total - tailCount)].join("\n");
|
|
238
|
+
return tierResult(content, next, "truncate");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/engine/tiers/code.ts
|
|
242
|
+
var LANG_BY_EXT = {
|
|
243
|
+
ts: "ts-js",
|
|
244
|
+
tsx: "ts-js",
|
|
245
|
+
js: "ts-js",
|
|
246
|
+
jsx: "ts-js",
|
|
247
|
+
mjs: "ts-js",
|
|
248
|
+
cjs: "ts-js",
|
|
249
|
+
rs: "rust",
|
|
250
|
+
py: "python",
|
|
251
|
+
go: "go",
|
|
252
|
+
java: "c-like",
|
|
253
|
+
c: "c-like",
|
|
254
|
+
h: "c-like",
|
|
255
|
+
cpp: "c-like",
|
|
256
|
+
hpp: "c-like",
|
|
257
|
+
swift: "c-like",
|
|
258
|
+
kt: "c-like",
|
|
259
|
+
scala: "c-like",
|
|
260
|
+
php: "c-like",
|
|
261
|
+
rb: "ruby",
|
|
262
|
+
sh: "shell",
|
|
263
|
+
zsh: "shell",
|
|
264
|
+
css: "css",
|
|
265
|
+
scss: "css",
|
|
266
|
+
sql: "sql",
|
|
267
|
+
toml: "config",
|
|
268
|
+
yaml: "config",
|
|
269
|
+
yml: "config"
|
|
270
|
+
};
|
|
271
|
+
function langFromPath(filePath) {
|
|
272
|
+
if (filePath === void 0) return void 0;
|
|
273
|
+
const match = /\.([A-Za-z0-9]+)$/.exec(filePath);
|
|
274
|
+
const ext = match?.[1]?.toLowerCase();
|
|
275
|
+
return ext === void 0 ? void 0 : LANG_BY_EXT[ext];
|
|
276
|
+
}
|
|
277
|
+
var LINE_NUM_RE = /^ *\d+(?:→|\t)/;
|
|
278
|
+
function hasLineNumbers(content) {
|
|
279
|
+
const lines = content.split("\n").filter((l) => l.length > 0);
|
|
280
|
+
if (lines.length < 2) return false;
|
|
281
|
+
return lines.every((l) => LINE_NUM_RE.test(l));
|
|
282
|
+
}
|
|
283
|
+
function lineText(line) {
|
|
284
|
+
const match = LINE_NUM_RE.exec(line);
|
|
285
|
+
return match === null ? line : line.slice(match[0].length);
|
|
286
|
+
}
|
|
287
|
+
var COMMENT_SYNTAX = {
|
|
288
|
+
"ts-js": { line: ["//"], block: { open: "/*", close: "*/" } },
|
|
289
|
+
rust: { line: ["//"], block: { open: "/*", close: "*/" } },
|
|
290
|
+
python: { line: ["#"] },
|
|
291
|
+
go: { line: ["//"], block: { open: "/*", close: "*/" } },
|
|
292
|
+
"c-like": { line: ["//"], block: { open: "/*", close: "*/" } },
|
|
293
|
+
ruby: { line: ["#"] },
|
|
294
|
+
shell: { line: ["#"] },
|
|
295
|
+
css: { line: ["//"], block: { open: "/*", close: "*/" } },
|
|
296
|
+
sql: { line: ["--"], block: { open: "/*", close: "*/" } },
|
|
297
|
+
config: { line: ["#"] }
|
|
298
|
+
};
|
|
299
|
+
function scanPythonTriples(text, open) {
|
|
300
|
+
let state = open;
|
|
301
|
+
let i = 0;
|
|
302
|
+
while (i < text.length) {
|
|
303
|
+
if (state === null) {
|
|
304
|
+
const dq = text.indexOf('"""', i);
|
|
305
|
+
const sq = text.indexOf("'''", i);
|
|
306
|
+
const next = dq === -1 ? sq : sq === -1 ? dq : Math.min(dq, sq);
|
|
307
|
+
if (next === -1) break;
|
|
308
|
+
state = text.startsWith('"""', next) ? '"""' : "'''";
|
|
309
|
+
i = next + 3;
|
|
310
|
+
} else {
|
|
311
|
+
const close = text.indexOf(state, i);
|
|
312
|
+
if (close === -1) break;
|
|
313
|
+
i = close + 3;
|
|
314
|
+
state = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return state;
|
|
318
|
+
}
|
|
319
|
+
function commentStripMarker(stripped, strippedLines, style) {
|
|
320
|
+
const head = `[compressor: ${stripped} comment/blank lines stripped \u2014 line numbers preserved`;
|
|
321
|
+
if (style === "deterrent") {
|
|
322
|
+
return `${head}; comments are likely irrelevant to the problem you are chasing]`;
|
|
323
|
+
}
|
|
324
|
+
if (style === "informative") {
|
|
325
|
+
const scan = scanFailureLines(strippedLines);
|
|
326
|
+
return scan.count === 0 ? `${head}; no error/failure/warning text among them; safe to skip]` : `${head}; ${scan.count} stripped lines matching error/fail/warn at lines ${formatMatchLines(scan)}]`;
|
|
327
|
+
}
|
|
328
|
+
return `${head}]`;
|
|
329
|
+
}
|
|
330
|
+
function stripComments(content, lang, style = "plain") {
|
|
331
|
+
if (lang === void 0 || !hasLineNumbers(content)) return { content };
|
|
332
|
+
if (lang === "config") return { content };
|
|
333
|
+
const syntax = COMMENT_SYNTAX[lang];
|
|
334
|
+
const block = syntax.block;
|
|
335
|
+
const lines = content.split("\n");
|
|
336
|
+
const kept = [];
|
|
337
|
+
const strippedLines = [];
|
|
338
|
+
let stripped = 0;
|
|
339
|
+
let inBlock = false;
|
|
340
|
+
let tripleOpen = null;
|
|
341
|
+
for (const line of lines) {
|
|
342
|
+
if (line.length === 0) {
|
|
343
|
+
kept.push(line);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const text = lineText(line);
|
|
347
|
+
if (lang === "python" && tripleOpen !== null) {
|
|
348
|
+
tripleOpen = scanPythonTriples(text, tripleOpen);
|
|
349
|
+
kept.push(line);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const trimmed = text.trim();
|
|
353
|
+
let drop = false;
|
|
354
|
+
if (inBlock && block !== void 0) {
|
|
355
|
+
const closeIdx = trimmed.indexOf(block.close);
|
|
356
|
+
if (closeIdx >= 0) {
|
|
357
|
+
inBlock = false;
|
|
358
|
+
drop = trimmed.slice(closeIdx + block.close.length).trim() === "";
|
|
359
|
+
} else {
|
|
360
|
+
drop = true;
|
|
361
|
+
}
|
|
362
|
+
} else if (trimmed === "") {
|
|
363
|
+
drop = true;
|
|
364
|
+
} else if (trimmed.startsWith("#!")) {
|
|
365
|
+
drop = false;
|
|
366
|
+
} else if (syntax.line.some((p) => trimmed.startsWith(p))) {
|
|
367
|
+
drop = true;
|
|
368
|
+
} else if (block !== void 0 && trimmed.startsWith(block.open)) {
|
|
369
|
+
const rest = trimmed.slice(block.open.length);
|
|
370
|
+
const closeIdx = rest.indexOf(block.close);
|
|
371
|
+
if (closeIdx >= 0) {
|
|
372
|
+
drop = rest.slice(closeIdx + block.close.length).trim() === "";
|
|
373
|
+
} else {
|
|
374
|
+
drop = true;
|
|
375
|
+
inBlock = true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (drop) {
|
|
379
|
+
stripped += 1;
|
|
380
|
+
strippedLines.push(line);
|
|
381
|
+
} else {
|
|
382
|
+
if (lang === "python") tripleOpen = scanPythonTriples(text, null);
|
|
383
|
+
kept.push(line);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (stripped === 0) return { content };
|
|
387
|
+
const marker = commentStripMarker(stripped, strippedLines, style);
|
|
388
|
+
const trailing = kept.length > 0 && kept[kept.length - 1] === "" ? kept.pop() : void 0;
|
|
389
|
+
kept.push(marker);
|
|
390
|
+
if (trailing !== void 0) kept.push(trailing);
|
|
391
|
+
return tierResult(content, kept.join("\n"), "comment-strip");
|
|
392
|
+
}
|
|
393
|
+
var SIGNATURE_TESTS = {
|
|
394
|
+
"ts-js": (t) => /^(import|export|function|async function|class|interface|type)\b/.test(t),
|
|
395
|
+
rust: (t) => /^\s*(use|pub|fn|struct|enum|trait|impl|mod)\b/.test(t),
|
|
396
|
+
python: (t) => /^\s*(import|from|def|class|async def)\b/.test(t),
|
|
397
|
+
go: (t) => /^(package|import|func|type)\b/.test(t)
|
|
398
|
+
};
|
|
399
|
+
function skeleton(content, lang, meta, estimate, style = "plain") {
|
|
400
|
+
if (lang === void 0 || !hasLineNumbers(content)) return { content };
|
|
401
|
+
const isSignature = SIGNATURE_TESTS[lang];
|
|
402
|
+
if (isSignature === void 0) return { content };
|
|
403
|
+
const lines = content.split("\n");
|
|
404
|
+
const out = [];
|
|
405
|
+
let gap = [];
|
|
406
|
+
const flushGap = () => {
|
|
407
|
+
if (gap.length === 0) return;
|
|
408
|
+
const first = gap[0];
|
|
409
|
+
const last = gap[gap.length - 1];
|
|
410
|
+
const a = first === void 0 ? void 0 : lineNumberOf(first);
|
|
411
|
+
const b = last === void 0 ? void 0 : lineNumberOf(last);
|
|
412
|
+
if (gap.length < 2 || a === void 0 || b === void 0) {
|
|
413
|
+
out.push(...gap);
|
|
414
|
+
} else {
|
|
415
|
+
out.push(omissionMarker(a, b, estimate(gap.join("\n")), meta, style, gap));
|
|
416
|
+
}
|
|
417
|
+
gap = [];
|
|
418
|
+
};
|
|
419
|
+
for (const line of lines) {
|
|
420
|
+
if (line.length === 0) {
|
|
421
|
+
flushGap();
|
|
422
|
+
out.push(line);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (isSignature(lineText(line))) {
|
|
426
|
+
flushGap();
|
|
427
|
+
out.push(line);
|
|
428
|
+
} else {
|
|
429
|
+
gap.push(line);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
flushGap();
|
|
433
|
+
return tierResult(content, out.join("\n"), "skeleton");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/engine/tiers/logs.ts
|
|
437
|
+
function logMarker(noun, omittedLines, style) {
|
|
438
|
+
const head = `[compressor: ${omittedLines.length} ${noun} lines omitted`;
|
|
439
|
+
if (style === "deterrent") {
|
|
440
|
+
return `${head} \u2014 likely irrelevant; re-run with a narrower filter ONLY if the problem you are chasing points into the omitted output]`;
|
|
441
|
+
}
|
|
442
|
+
if (style === "informative") {
|
|
443
|
+
const matches = scanFailureLines(omittedLines).count;
|
|
444
|
+
return matches === 0 ? `${head} \u2014 no error/failure/warning lines in the omitted output; safe to skip. Re-run with a narrower filter only if needed]` : `${head} \u2014 ${matches} omitted lines matching error/fail/warn \u2014 re-run with a narrower filter to retrieve]`;
|
|
445
|
+
}
|
|
446
|
+
return `${head}]`;
|
|
447
|
+
}
|
|
448
|
+
var TEST_PASS_RES = [
|
|
449
|
+
/^\s*[✓✔√]\s/,
|
|
450
|
+
/^PASS\s/,
|
|
451
|
+
/^\s*--- PASS:/,
|
|
452
|
+
/^test\s+\S+\s+\.\.\.\s+ok\s*$/,
|
|
453
|
+
/\bPASSED\s*$/,
|
|
454
|
+
/^ok\s+\d+\s/
|
|
455
|
+
];
|
|
456
|
+
var TEST_FAIL_RES = [
|
|
457
|
+
/^\s*[✗✘✖×]\s/,
|
|
458
|
+
/^FAIL\b/,
|
|
459
|
+
/--- FAIL/,
|
|
460
|
+
/\bFAILED\b/,
|
|
461
|
+
/^not ok\s/,
|
|
462
|
+
/\bpanicked\b/,
|
|
463
|
+
/^\s*●/,
|
|
464
|
+
// error reports only ('Error: boom', 'error: x'), not prose containing 'error'
|
|
465
|
+
/^\s*(?:[A-Za-z]*Error|error)\s*[:(]/
|
|
466
|
+
];
|
|
467
|
+
var TEST_SUMMARY_RES = [
|
|
468
|
+
/^(Tests|Test Suites|Snapshots|Time|Duration):/,
|
|
469
|
+
/\d+\s+pass(?:ed|ing)\b/,
|
|
470
|
+
/\d+\s+fail(?:ed|ing)\b/,
|
|
471
|
+
/^test result:/,
|
|
472
|
+
/^Ran all test suites/,
|
|
473
|
+
/^=+ .* =+$/,
|
|
474
|
+
// node:test spec ('ℹ pass 118') and tap ('# pass 118') summary counters
|
|
475
|
+
/^(?:ℹ|#) (?:tests|suites|pass|fail|cancelled|skipped|todo|duration_ms) \d+\s*$/
|
|
476
|
+
];
|
|
477
|
+
function filterTestLog(content, style = "plain") {
|
|
478
|
+
const lines = content.split("\n");
|
|
479
|
+
const isFail = (l) => TEST_FAIL_RES.some((re) => re.test(l));
|
|
480
|
+
const isSummary = (l) => TEST_SUMMARY_RES.some((re) => re.test(l));
|
|
481
|
+
const isPass = (l) => TEST_PASS_RES.some((re) => re.test(l));
|
|
482
|
+
if (!lines.some(isFail) && !lines.some(isSummary)) return { content };
|
|
483
|
+
const out = [];
|
|
484
|
+
const omittedLines = [];
|
|
485
|
+
let markerIdx = -1;
|
|
486
|
+
for (const line of lines) {
|
|
487
|
+
if (isPass(line) && !isFail(line) && !isSummary(line)) {
|
|
488
|
+
if (markerIdx < 0) markerIdx = out.length;
|
|
489
|
+
omittedLines.push(line);
|
|
490
|
+
} else {
|
|
491
|
+
out.push(line);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (omittedLines.length === 0) return { content };
|
|
495
|
+
out.splice(markerIdx, 0, logMarker("passing-test", omittedLines, style));
|
|
496
|
+
return tierResult(content, out.join("\n"), "log-filter");
|
|
497
|
+
}
|
|
498
|
+
var BUILD_ERROR_RES = [
|
|
499
|
+
/error\[E\d+\]/,
|
|
500
|
+
/^\s*(error|warning)(\s+[A-Z]+\d+)?:/i,
|
|
501
|
+
/\b(error|warning)\s+TS\d+:/,
|
|
502
|
+
/:\d+(?::\d+)?:\s*(?:fatal\s+)?(?:error|warning)\b/,
|
|
503
|
+
/npm ERR!/,
|
|
504
|
+
/^\s*(Error|TypeError|SyntaxError|ReferenceError|RangeError)\b/
|
|
505
|
+
];
|
|
506
|
+
var BUILD_STATUS_RES = [
|
|
507
|
+
/^\s*error: aborting/,
|
|
508
|
+
/^\s*error: could not compile/,
|
|
509
|
+
/Found \d+ errors?/,
|
|
510
|
+
/\d+ errors? generated/,
|
|
511
|
+
/^make: \*\*\*/,
|
|
512
|
+
/^Build (?:FAILED|failed|succeeded)/,
|
|
513
|
+
/^\s*Finished\b/,
|
|
514
|
+
/[✖✗] \d+ problems?/,
|
|
515
|
+
/exited with code \d+/
|
|
516
|
+
];
|
|
517
|
+
function filterBuildLog(content, style = "plain") {
|
|
518
|
+
const lines = content.split("\n");
|
|
519
|
+
const isError = (l) => BUILD_ERROR_RES.some((re) => re.test(l));
|
|
520
|
+
const isStatus = (l) => BUILD_STATUS_RES.some((re) => re.test(l));
|
|
521
|
+
if (!lines.some(isError) && !lines.some(isStatus)) return { content };
|
|
522
|
+
const out = [];
|
|
523
|
+
const omittedLines = [];
|
|
524
|
+
let markerIdx = -1;
|
|
525
|
+
let inErrorBlock = false;
|
|
526
|
+
for (const line of lines) {
|
|
527
|
+
let keep;
|
|
528
|
+
if (isError(line)) {
|
|
529
|
+
inErrorBlock = true;
|
|
530
|
+
keep = true;
|
|
531
|
+
} else if (isStatus(line)) {
|
|
532
|
+
inErrorBlock = false;
|
|
533
|
+
keep = true;
|
|
534
|
+
} else if (inErrorBlock) {
|
|
535
|
+
if (line.trim() === "") {
|
|
536
|
+
inErrorBlock = false;
|
|
537
|
+
keep = false;
|
|
538
|
+
} else {
|
|
539
|
+
keep = true;
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
keep = false;
|
|
543
|
+
}
|
|
544
|
+
if (keep) {
|
|
545
|
+
out.push(line);
|
|
546
|
+
} else {
|
|
547
|
+
if (markerIdx < 0) markerIdx = out.length;
|
|
548
|
+
omittedLines.push(line);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (omittedLines.length === 0) return { content };
|
|
552
|
+
out.splice(markerIdx, 0, logMarker("build-log", omittedLines, style));
|
|
553
|
+
return tierResult(content, out.join("\n"), "log-filter");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/engine/policy.ts
|
|
557
|
+
function policyFor(mode) {
|
|
558
|
+
switch (mode) {
|
|
559
|
+
case "full":
|
|
560
|
+
return {
|
|
561
|
+
structural: false,
|
|
562
|
+
codeAware: false,
|
|
563
|
+
logAware: false,
|
|
564
|
+
// 'plain' everywhere for now: the marker-style experiment (bench
|
|
565
|
+
// hookArgs --marker-style) varies this per arm and picks the winner.
|
|
566
|
+
markerStyle: "plain",
|
|
567
|
+
touch: Infinity,
|
|
568
|
+
truncateBudget: Infinity,
|
|
569
|
+
commentStrip: Infinity,
|
|
570
|
+
skeleton: Infinity,
|
|
571
|
+
logFilter: Infinity
|
|
572
|
+
};
|
|
573
|
+
// PLAN.md: optimized = tier 1 + comment-strip; lossy tier-3 log filtering
|
|
574
|
+
// is reserved for slim.
|
|
575
|
+
case "optimized":
|
|
576
|
+
return {
|
|
577
|
+
structural: true,
|
|
578
|
+
codeAware: true,
|
|
579
|
+
logAware: false,
|
|
580
|
+
markerStyle: "plain",
|
|
581
|
+
touch: 600,
|
|
582
|
+
truncateBudget: 5e3,
|
|
583
|
+
commentStrip: 2e3,
|
|
584
|
+
skeleton: Infinity,
|
|
585
|
+
logFilter: Infinity
|
|
586
|
+
};
|
|
587
|
+
case "slim":
|
|
588
|
+
return {
|
|
589
|
+
structural: true,
|
|
590
|
+
codeAware: true,
|
|
591
|
+
logAware: true,
|
|
592
|
+
markerStyle: "plain",
|
|
593
|
+
touch: 300,
|
|
594
|
+
// measured (bench-20260610-114234/-123102): a 2,500 budget pushed the
|
|
595
|
+
// model into offset/limit pagination — targeted reads pass through, so
|
|
596
|
+
// recovery re-reads nullified all savings (worst cell exceeded the
|
|
597
|
+
// uncompressed baseline). 5,000 stays under the recovery trigger.
|
|
598
|
+
truncateBudget: 5e3,
|
|
599
|
+
commentStrip: 1e3,
|
|
600
|
+
skeleton: 6e3,
|
|
601
|
+
logFilter: 800
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/engine/index.ts
|
|
607
|
+
function compress(content, meta, policy, estimate) {
|
|
608
|
+
const estTokensIn = estimate(content);
|
|
609
|
+
const bytesIn = utf8Bytes(content);
|
|
610
|
+
const passthrough = meta.mode === "full" || meta.targeted === true || content.includes(OMISSION_MARKER) || estTokensIn < policy.touch;
|
|
611
|
+
if (passthrough) {
|
|
612
|
+
return {
|
|
613
|
+
content,
|
|
614
|
+
stats: {
|
|
615
|
+
bytesIn,
|
|
616
|
+
bytesOut: bytesIn,
|
|
617
|
+
estTokensIn,
|
|
618
|
+
estTokensOut: estTokensIn,
|
|
619
|
+
kind: detectKind(content, meta.filePath),
|
|
620
|
+
transforms: []
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
let current = content;
|
|
625
|
+
const transforms = [];
|
|
626
|
+
const apply = (result) => {
|
|
627
|
+
current = result.content;
|
|
628
|
+
if (result.transform !== void 0) transforms.push(result.transform);
|
|
629
|
+
};
|
|
630
|
+
const decide = (text) => estimate(
|
|
631
|
+
text.includes(OMISSION_MARKER) ? text.split("\n").filter((line) => !line.includes(OMISSION_MARKER)).join("\n") : text
|
|
632
|
+
);
|
|
633
|
+
if (policy.structural) {
|
|
634
|
+
apply(stripAnsi(current));
|
|
635
|
+
apply(collapseBlankRuns(current));
|
|
636
|
+
apply(dedupeLines(current));
|
|
637
|
+
}
|
|
638
|
+
const kind = detectKind(current, meta.filePath);
|
|
639
|
+
if (policy.codeAware && kind === "code") {
|
|
640
|
+
const lang = langFromPath(meta.filePath);
|
|
641
|
+
if (decide(current) > policy.skeleton) {
|
|
642
|
+
apply(skeleton(current, lang, meta, estimate, policy.markerStyle));
|
|
643
|
+
} else if (decide(current) > policy.commentStrip) {
|
|
644
|
+
apply(stripComments(current, lang, policy.markerStyle));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (policy.logAware && decide(current) > policy.logFilter) {
|
|
648
|
+
if (kind === "test-log") apply(filterTestLog(current, policy.markerStyle));
|
|
649
|
+
else if (kind === "build-log") apply(filterBuildLog(current, policy.markerStyle));
|
|
650
|
+
}
|
|
651
|
+
if (decide(current) > policy.truncateBudget) {
|
|
652
|
+
const positionsAreFileLines = transforms.every((t) => t.id === "strip-ansi");
|
|
653
|
+
apply(truncateHeadTail(current, meta, policy, decide, positionsAreFileLines));
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
content: current,
|
|
657
|
+
stats: {
|
|
658
|
+
bytesIn,
|
|
659
|
+
bytesOut: utf8Bytes(current),
|
|
660
|
+
estTokensIn,
|
|
661
|
+
estTokensOut: estimate(current),
|
|
662
|
+
kind,
|
|
663
|
+
transforms
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function utf8Bytes(text) {
|
|
668
|
+
return new TextEncoder().encode(text).length;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/tokens/estimate.ts
|
|
672
|
+
var cheapEstimator = (text) => Math.ceil(text.length / 3.5);
|
|
673
|
+
|
|
674
|
+
// src/ledger/write.ts
|
|
675
|
+
import os from "node:os";
|
|
676
|
+
import path from "node:path";
|
|
677
|
+
import process from "node:process";
|
|
678
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
679
|
+
function resolveLedgerDir() {
|
|
680
|
+
return process.env["COMPRESSOR_LEDGER_DIR"] ?? path.join(os.homedir(), ".compressor", "ledger");
|
|
681
|
+
}
|
|
682
|
+
function monthOf(ts) {
|
|
683
|
+
const month = ts.slice(0, 7);
|
|
684
|
+
return /^\d{4}-\d{2}$/.test(month) ? month : "unknown";
|
|
685
|
+
}
|
|
686
|
+
var mkdirCache = /* @__PURE__ */ new Map();
|
|
687
|
+
var pending = /* @__PURE__ */ new Set();
|
|
688
|
+
async function appendLedger(event) {
|
|
689
|
+
if (process.env["COMPRESSOR_NO_LEDGER"] === "1") {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const task = (async () => {
|
|
693
|
+
try {
|
|
694
|
+
const dir = resolveLedgerDir();
|
|
695
|
+
let made = mkdirCache.get(dir);
|
|
696
|
+
if (made === void 0) {
|
|
697
|
+
made = mkdir(dir, { recursive: true }).then(
|
|
698
|
+
() => void 0,
|
|
699
|
+
() => void 0
|
|
700
|
+
);
|
|
701
|
+
mkdirCache.set(dir, made);
|
|
702
|
+
}
|
|
703
|
+
await made;
|
|
704
|
+
const file = path.join(dir, `${monthOf(event.ts)}.jsonl`);
|
|
705
|
+
await appendFile(file, `${JSON.stringify(event)}
|
|
706
|
+
`, "utf8");
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
})();
|
|
710
|
+
pending.add(task);
|
|
711
|
+
void task.finally(() => pending.delete(task));
|
|
712
|
+
return task;
|
|
713
|
+
}
|
|
714
|
+
async function settleLedger() {
|
|
715
|
+
try {
|
|
716
|
+
await Promise.all([...pending]);
|
|
717
|
+
} catch {
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/hook/core.ts
|
|
722
|
+
var MIN_SAVED_CHARS = 200;
|
|
723
|
+
var MIN_SAVED_RATIO = 0.1;
|
|
724
|
+
function lengthSansMarkers(text) {
|
|
725
|
+
if (!text.includes(OMISSION_MARKER)) {
|
|
726
|
+
return text.length;
|
|
727
|
+
}
|
|
728
|
+
return text.split("\n").filter((line) => !line.includes(OMISSION_MARKER)).join("\n").length;
|
|
729
|
+
}
|
|
730
|
+
function compressCall(call, mode, markerStyle) {
|
|
731
|
+
try {
|
|
732
|
+
const meta = { tool: call.toolKind, mode, targeted: call.targeted };
|
|
733
|
+
if (call.filePath !== void 0) {
|
|
734
|
+
meta.filePath = call.filePath;
|
|
735
|
+
}
|
|
736
|
+
const base = policyFor(mode);
|
|
737
|
+
const policy = markerStyle === void 0 ? base : { ...base, markerStyle };
|
|
738
|
+
const result = compress(call.text, meta, policy, cheapEstimator);
|
|
739
|
+
const saved = call.text.length - lengthSansMarkers(result.content);
|
|
740
|
+
if (saved < MIN_SAVED_CHARS || saved < call.text.length * MIN_SAVED_RATIO) {
|
|
741
|
+
return { text: call.text, worthwhile: false };
|
|
742
|
+
}
|
|
743
|
+
return { text: result.content, worthwhile: true, stats: result.stats };
|
|
744
|
+
} catch {
|
|
745
|
+
return { text: call.text, worthwhile: false };
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function recordCompression(agent, call, compressed, mode) {
|
|
749
|
+
try {
|
|
750
|
+
if (!compressed.worthwhile) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
void appendLedger({
|
|
754
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
755
|
+
agent,
|
|
756
|
+
tool: call.toolKind,
|
|
757
|
+
mode,
|
|
758
|
+
charsIn: call.text.length,
|
|
759
|
+
charsOut: compressed.text.length,
|
|
760
|
+
estTokensIn: compressed.stats?.estTokensIn ?? cheapEstimator(call.text),
|
|
761
|
+
estTokensOut: compressed.stats?.estTokensOut ?? cheapEstimator(compressed.text),
|
|
762
|
+
transforms: compressed.stats?.transforms.map((t) => t.id) ?? []
|
|
763
|
+
}).catch(() => {
|
|
764
|
+
});
|
|
765
|
+
} catch {
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function isRecord(value) {
|
|
769
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
770
|
+
}
|
|
771
|
+
function longestStringLeaf(value, path2, best) {
|
|
772
|
+
if (typeof value === "string") {
|
|
773
|
+
return best === null || value.length > best.text.length ? { path: path2, text: value } : best;
|
|
774
|
+
}
|
|
775
|
+
if (Array.isArray(value)) {
|
|
776
|
+
return value.reduce(
|
|
777
|
+
(acc, item, i) => longestStringLeaf(item, [...path2, i], acc),
|
|
778
|
+
best
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
if (isRecord(value)) {
|
|
782
|
+
return Object.entries(value).reduce(
|
|
783
|
+
(acc, [key, item]) => longestStringLeaf(item, [...path2, key], acc),
|
|
784
|
+
best
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
return best;
|
|
788
|
+
}
|
|
789
|
+
function pickLeaf(toolResponse, tool) {
|
|
790
|
+
if (typeof toolResponse === "string") {
|
|
791
|
+
return { path: [], text: toolResponse };
|
|
792
|
+
}
|
|
793
|
+
if (tool === "bash" && isRecord(toolResponse) && typeof toolResponse["stdout"] === "string") {
|
|
794
|
+
return { path: ["stdout"], text: toolResponse["stdout"] };
|
|
795
|
+
}
|
|
796
|
+
if (isRecord(toolResponse) || Array.isArray(toolResponse)) {
|
|
797
|
+
return longestStringLeaf(toolResponse, [], null);
|
|
798
|
+
}
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
function rebuildWithLeaf(toolResponse, path2, text) {
|
|
802
|
+
if (path2.length === 0) {
|
|
803
|
+
return text;
|
|
804
|
+
}
|
|
805
|
+
const clone = structuredClone(toolResponse);
|
|
806
|
+
let cursor = clone;
|
|
807
|
+
for (let i = 0; i < path2.length - 1; i += 1) {
|
|
808
|
+
const key = path2[i];
|
|
809
|
+
if (Array.isArray(cursor) && typeof key === "number") {
|
|
810
|
+
cursor = cursor[key];
|
|
811
|
+
} else if (isRecord(cursor) && typeof key === "string") {
|
|
812
|
+
cursor = cursor[key];
|
|
813
|
+
} else {
|
|
814
|
+
throw new Error("leaf path mismatch");
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const last = path2[path2.length - 1];
|
|
818
|
+
if (Array.isArray(cursor) && typeof last === "number") {
|
|
819
|
+
cursor[last] = text;
|
|
820
|
+
} else if (isRecord(cursor) && typeof last === "string") {
|
|
821
|
+
cursor[last] = text;
|
|
822
|
+
} else {
|
|
823
|
+
throw new Error("leaf path mismatch");
|
|
824
|
+
}
|
|
825
|
+
return clone;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/hook/copilot.ts
|
|
829
|
+
function toolKindFor(toolName) {
|
|
830
|
+
switch (toolName) {
|
|
831
|
+
case "view":
|
|
832
|
+
return "read";
|
|
833
|
+
case "bash":
|
|
834
|
+
case "powershell":
|
|
835
|
+
return "bash";
|
|
836
|
+
case "grep":
|
|
837
|
+
case "glob":
|
|
838
|
+
return "search";
|
|
839
|
+
default:
|
|
840
|
+
return "other";
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
var FILE_PATH_KEYS = ["path", "filePath", "file_path", "file"];
|
|
844
|
+
function parseToolArgs(raw) {
|
|
845
|
+
if (isRecord(raw)) {
|
|
846
|
+
return raw;
|
|
847
|
+
}
|
|
848
|
+
if (typeof raw === "string") {
|
|
849
|
+
try {
|
|
850
|
+
const parsed = JSON.parse(raw);
|
|
851
|
+
if (isRecord(parsed)) {
|
|
852
|
+
return parsed;
|
|
853
|
+
}
|
|
854
|
+
} catch {
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return {};
|
|
858
|
+
}
|
|
859
|
+
var RANGE_KEYS = [
|
|
860
|
+
"offset",
|
|
861
|
+
"limit",
|
|
862
|
+
"startLine",
|
|
863
|
+
"endLine",
|
|
864
|
+
"start_line",
|
|
865
|
+
"end_line",
|
|
866
|
+
"range",
|
|
867
|
+
"viewRange",
|
|
868
|
+
"view_range"
|
|
869
|
+
];
|
|
870
|
+
function filePathFrom(args) {
|
|
871
|
+
for (const key of FILE_PATH_KEYS) {
|
|
872
|
+
const value = args[key];
|
|
873
|
+
if (typeof value === "string") {
|
|
874
|
+
return value;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return void 0;
|
|
878
|
+
}
|
|
879
|
+
function isTargeted(args) {
|
|
880
|
+
return RANGE_KEYS.some((key) => args[key] != null);
|
|
881
|
+
}
|
|
882
|
+
function handleCopilotPostToolUse(payloadJson, mode, markerStyle) {
|
|
883
|
+
try {
|
|
884
|
+
if (mode === "full") {
|
|
885
|
+
return { output: null };
|
|
886
|
+
}
|
|
887
|
+
const payload = JSON.parse(payloadJson);
|
|
888
|
+
if (!isRecord(payload)) {
|
|
889
|
+
return { output: null };
|
|
890
|
+
}
|
|
891
|
+
const toolName = typeof payload["toolName"] === "string" ? payload["toolName"] : "";
|
|
892
|
+
const toolArgs = parseToolArgs(payload["toolArgs"]);
|
|
893
|
+
const tool = toolKindFor(toolName);
|
|
894
|
+
const toolResult = payload["toolResult"];
|
|
895
|
+
if (isRecord(toolResult) && toolResult["resultType"] !== void 0 && toolResult["resultType"] !== "success") {
|
|
896
|
+
return { output: null };
|
|
897
|
+
}
|
|
898
|
+
let text = null;
|
|
899
|
+
let genericLeaf = null;
|
|
900
|
+
if (isRecord(toolResult) && typeof toolResult["textResultForLlm"] === "string") {
|
|
901
|
+
text = toolResult["textResultForLlm"];
|
|
902
|
+
} else {
|
|
903
|
+
genericLeaf = pickLeaf(toolResult, tool);
|
|
904
|
+
if (genericLeaf === null) {
|
|
905
|
+
return { output: null };
|
|
906
|
+
}
|
|
907
|
+
text = genericLeaf.text;
|
|
908
|
+
}
|
|
909
|
+
const call = {
|
|
910
|
+
toolKind: tool,
|
|
911
|
+
targeted: tool === "read" && isTargeted(toolArgs),
|
|
912
|
+
text
|
|
913
|
+
};
|
|
914
|
+
const filePath = tool === "read" ? filePathFrom(toolArgs) : void 0;
|
|
915
|
+
if (filePath !== void 0) {
|
|
916
|
+
call.filePath = filePath;
|
|
917
|
+
}
|
|
918
|
+
const compressed = compressCall(call, mode, markerStyle);
|
|
919
|
+
if (!compressed.worthwhile) {
|
|
920
|
+
return { output: null };
|
|
921
|
+
}
|
|
922
|
+
recordCompression("copilot", call, compressed, mode);
|
|
923
|
+
const replacement = genericLeaf === null || genericLeaf.path.length === 0 ? compressed.text : JSON.stringify(rebuildWithLeaf(toolResult, genericLeaf.path, compressed.text));
|
|
924
|
+
return {
|
|
925
|
+
output: JSON.stringify({
|
|
926
|
+
modifiedResult: {
|
|
927
|
+
resultType: "success",
|
|
928
|
+
textResultForLlm: replacement
|
|
929
|
+
}
|
|
930
|
+
})
|
|
931
|
+
};
|
|
932
|
+
} catch {
|
|
933
|
+
return { output: null };
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/hook/exit.ts
|
|
938
|
+
import process2 from "node:process";
|
|
939
|
+
var SETTLE_CAP_MS = 250;
|
|
940
|
+
async function settleThenExit(output) {
|
|
941
|
+
try {
|
|
942
|
+
if (output !== null) {
|
|
943
|
+
await new Promise((resolve) => {
|
|
944
|
+
process2.stdout.write(output, () => resolve());
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
} catch {
|
|
948
|
+
}
|
|
949
|
+
let timedOut = true;
|
|
950
|
+
try {
|
|
951
|
+
let timer;
|
|
952
|
+
timedOut = await Promise.race([
|
|
953
|
+
settleLedger().then(() => false),
|
|
954
|
+
new Promise((resolve) => {
|
|
955
|
+
timer = setTimeout(() => resolve(true), SETTLE_CAP_MS);
|
|
956
|
+
})
|
|
957
|
+
]);
|
|
958
|
+
if (timer !== void 0) {
|
|
959
|
+
clearTimeout(timer);
|
|
960
|
+
}
|
|
961
|
+
} catch {
|
|
962
|
+
timedOut = true;
|
|
963
|
+
}
|
|
964
|
+
if (timedOut) {
|
|
965
|
+
process2.kill(process2.pid, "SIGKILL");
|
|
966
|
+
}
|
|
967
|
+
process2.exit(0);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// src/copilot-hook-entry.ts
|
|
971
|
+
function parseMode(argv) {
|
|
972
|
+
const idx = argv.indexOf("--mode");
|
|
973
|
+
const value = idx === -1 ? void 0 : argv[idx + 1];
|
|
974
|
+
return value === "full" || value === "optimized" || value === "slim" ? value : "optimized";
|
|
975
|
+
}
|
|
976
|
+
function parseMarkerStyle(argv) {
|
|
977
|
+
const idx = argv.indexOf("--marker-style");
|
|
978
|
+
const value = idx === -1 ? void 0 : argv[idx + 1];
|
|
979
|
+
return value === "plain" || value === "deterrent" || value === "informative" ? value : void 0;
|
|
980
|
+
}
|
|
981
|
+
async function main() {
|
|
982
|
+
const chunks = [];
|
|
983
|
+
for await (const chunk of process3.stdin) {
|
|
984
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
985
|
+
}
|
|
986
|
+
const payload = Buffer.concat(chunks).toString("utf8");
|
|
987
|
+
return handleCopilotPostToolUse(
|
|
988
|
+
payload,
|
|
989
|
+
parseMode(process3.argv),
|
|
990
|
+
parseMarkerStyle(process3.argv)
|
|
991
|
+
).output;
|
|
992
|
+
}
|
|
993
|
+
main().then(
|
|
994
|
+
(output) => {
|
|
995
|
+
settleThenExit(output).catch(() => process3.exit(0));
|
|
996
|
+
},
|
|
997
|
+
() => {
|
|
998
|
+
settleThenExit(null).catch(() => process3.exit(0));
|
|
999
|
+
}
|
|
1000
|
+
);
|