@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,196 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolveLedgerDir } from "../../ledger/write.js";
|
|
3
|
+
import { readLedger } from "../../ledger/read.js";
|
|
4
|
+
const fmt = (n) => Math.round(n).toLocaleString('en-US');
|
|
5
|
+
export function parseSince(value) {
|
|
6
|
+
if (value === 'all') {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const days = /^(\d+)d$/.exec(value)?.[1];
|
|
10
|
+
if (days === undefined) {
|
|
11
|
+
throw new Error(`invalid --since '${value}' (expected e.g. 7d, 30d, or 'all')`);
|
|
12
|
+
}
|
|
13
|
+
return new Date(Date.now() - Number(days) * 86_400_000);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Human label for the lookback window. Totals MUST state their window:
|
|
17
|
+
* the default is 30d, and an unqualified headline (especially in the
|
|
18
|
+
* shareable HTML artifact) reads as all-time.
|
|
19
|
+
*/
|
|
20
|
+
export function windowLabel(since) {
|
|
21
|
+
if (since === 'all') {
|
|
22
|
+
return 'all time';
|
|
23
|
+
}
|
|
24
|
+
const days = /^(\d+)d$/.exec(since)?.[1];
|
|
25
|
+
if (days === undefined) {
|
|
26
|
+
return since;
|
|
27
|
+
}
|
|
28
|
+
return Number(days) === 1 ? 'last 1 day' : `last ${days} days`;
|
|
29
|
+
}
|
|
30
|
+
function labelFor(event, by) {
|
|
31
|
+
switch (by) {
|
|
32
|
+
case 'day':
|
|
33
|
+
return event.ts.slice(0, 10);
|
|
34
|
+
case 'tool':
|
|
35
|
+
return event.tool;
|
|
36
|
+
case 'mode':
|
|
37
|
+
return event.mode;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Group savings by dimension. Days sort ascending; tool/mode by size. */
|
|
41
|
+
export function aggregateSavings(events, by) {
|
|
42
|
+
const groups = new Map();
|
|
43
|
+
for (const event of events) {
|
|
44
|
+
const label = labelFor(event, by);
|
|
45
|
+
const row = groups.get(label) ?? { label, savedChars: 0, savedTokens: 0, events: 0 };
|
|
46
|
+
row.savedChars += event.charsIn - event.charsOut;
|
|
47
|
+
row.savedTokens += event.estTokensIn - event.estTokensOut;
|
|
48
|
+
row.events += 1;
|
|
49
|
+
groups.set(label, row);
|
|
50
|
+
}
|
|
51
|
+
const rows = [...groups.values()];
|
|
52
|
+
return by === 'day'
|
|
53
|
+
? rows.sort((a, b) => a.label.localeCompare(b.label))
|
|
54
|
+
: rows.sort((a, b) => b.savedTokens - a.savedTokens);
|
|
55
|
+
}
|
|
56
|
+
const BAR_WIDTH = 40;
|
|
57
|
+
function bar(value, max) {
|
|
58
|
+
if (value <= 0 || max <= 0) {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
return '█'.repeat(Math.max(1, Math.round((value / max) * BAR_WIDTH)));
|
|
62
|
+
}
|
|
63
|
+
function chartLines(rows) {
|
|
64
|
+
const max = Math.max(...rows.map((r) => r.savedTokens), 1);
|
|
65
|
+
const labelWidth = Math.max(...rows.map((r) => r.label.length), 0);
|
|
66
|
+
return rows.map((r) => ` ${r.label.padEnd(labelWidth)} ${bar(r.savedTokens, max).padEnd(BAR_WIDTH)} ≈ ${fmt(r.savedTokens)} tok`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Empty states must distinguish "no ledger at all" (hook not installed /
|
|
70
|
+
* never fired — point at `compressor init`) from "no events INSIDE the
|
|
71
|
+
* window" (the hook works fine; suggesting a reinstall would misdirect).
|
|
72
|
+
* `eventsOutsideWindow` is the all-time count when the window filtered
|
|
73
|
+
* everything out.
|
|
74
|
+
*/
|
|
75
|
+
export function renderEmpty(dir, window, eventsOutsideWindow = 0) {
|
|
76
|
+
if (eventsOutsideWindow > 0) {
|
|
77
|
+
return [
|
|
78
|
+
`no ledger events in the ${window} window (looked in ${dir})`,
|
|
79
|
+
'',
|
|
80
|
+
`the ledger holds ${fmt(eventsOutsideWindow)} events outside this window — the hook is`,
|
|
81
|
+
'working; widen the window (e.g. --since all) to see them.',
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
return [
|
|
85
|
+
`no ledger events yet (looked in ${dir})`,
|
|
86
|
+
'',
|
|
87
|
+
'the compression hook records an event every time it shrinks tool output',
|
|
88
|
+
'during a real agent session — run `compressor init`, use claude-code or',
|
|
89
|
+
'copilot normally, then check back here.',
|
|
90
|
+
'',
|
|
91
|
+
'recording can be disabled with COMPRESSOR_NO_LEDGER=1 (kill switch).',
|
|
92
|
+
].join('\n');
|
|
93
|
+
}
|
|
94
|
+
export function renderSavings(events, by, dir, window) {
|
|
95
|
+
const savedChars = events.reduce((acc, e) => acc + (e.charsIn - e.charsOut), 0);
|
|
96
|
+
const savedTokens = events.reduce((acc, e) => acc + (e.estTokensIn - e.estTokensOut), 0);
|
|
97
|
+
const lines = [
|
|
98
|
+
`saved ${fmt(savedChars)} chars (exact) ≈ ${fmt(savedTokens)} tokens (estimated — cheap estimator, not billable counts) · ${window}`,
|
|
99
|
+
`events: ${fmt(events.length)} (${window})`,
|
|
100
|
+
'',
|
|
101
|
+
`by ${by}:`,
|
|
102
|
+
...chartLines(aggregateSavings(events, by)),
|
|
103
|
+
'',
|
|
104
|
+
'measured savings come from `compressor benchmark` — this view is the live estimated ledger',
|
|
105
|
+
`ledger: ${dir} (disable recording with COMPRESSOR_NO_LEDGER=1)`,
|
|
106
|
+
];
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
}
|
|
109
|
+
function escapeHtml(text) {
|
|
110
|
+
return text
|
|
111
|
+
.replaceAll('&', '&')
|
|
112
|
+
.replaceAll('<', '<')
|
|
113
|
+
.replaceAll('>', '>')
|
|
114
|
+
.replaceAll('"', '"');
|
|
115
|
+
}
|
|
116
|
+
function svgBarChart(rows) {
|
|
117
|
+
if (rows.length === 0) {
|
|
118
|
+
return '<p class="empty">no events in this window</p>';
|
|
119
|
+
}
|
|
120
|
+
const rowH = 26;
|
|
121
|
+
const labelW = 120;
|
|
122
|
+
const barMax = 420;
|
|
123
|
+
const valueW = 140;
|
|
124
|
+
const width = labelW + barMax + valueW;
|
|
125
|
+
const height = rows.length * rowH + 10;
|
|
126
|
+
const max = Math.max(...rows.map((r) => r.savedTokens), 1);
|
|
127
|
+
const parts = rows.map((r, i) => {
|
|
128
|
+
const y = 5 + i * rowH;
|
|
129
|
+
const w = r.savedTokens <= 0 ? 0 : Math.max(2, Math.round((r.savedTokens / max) * barMax));
|
|
130
|
+
return [
|
|
131
|
+
`<text x="${labelW - 10}" y="${y + 17}" text-anchor="end" class="label">${escapeHtml(r.label)}</text>`,
|
|
132
|
+
`<rect x="${labelW}" y="${y + 4}" width="${w}" height="16" rx="3" class="bar"/>`,
|
|
133
|
+
`<text x="${labelW + w + 8}" y="${y + 17}" class="value">≈ ${fmt(r.savedTokens)} tok (${fmt(r.savedChars)} chars)</text>`,
|
|
134
|
+
].join('');
|
|
135
|
+
});
|
|
136
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img">${parts.join('')}</svg>`;
|
|
137
|
+
}
|
|
138
|
+
export function renderSavingsHtml(events, dir, window) {
|
|
139
|
+
const savedChars = events.reduce((acc, e) => acc + (e.charsIn - e.charsOut), 0);
|
|
140
|
+
const savedTokens = events.reduce((acc, e) => acc + (e.estTokensIn - e.estTokensOut), 0);
|
|
141
|
+
const sections = ['day', 'tool', 'mode']
|
|
142
|
+
.map((by) => `<h2>by ${by}</h2>\n${svgBarChart(aggregateSavings(events, by))}`)
|
|
143
|
+
.join('\n');
|
|
144
|
+
// Self-contained on purpose: inline CSS, static SVG, no JS, no requests.
|
|
145
|
+
// The window label is mandatory: this artifact is shared standalone and an
|
|
146
|
+
// unqualified headline would read as all-time.
|
|
147
|
+
return `<!doctype html>
|
|
148
|
+
<html lang="en">
|
|
149
|
+
<head>
|
|
150
|
+
<meta charset="utf-8">
|
|
151
|
+
<title>compressor savings</title>
|
|
152
|
+
<style>
|
|
153
|
+
body { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; margin: 2rem auto; max-width: 760px; color: #1f2328; }
|
|
154
|
+
h1 { font-size: 1.3rem; } h2 { font-size: 1rem; margin-top: 1.6rem; }
|
|
155
|
+
.totals { font-size: 0.95rem; } .footer, .empty { color: #57606a; font-size: 0.8rem; }
|
|
156
|
+
svg .label, svg .value { font-size: 12px; fill: #1f2328; }
|
|
157
|
+
svg .bar { fill: #4c9aff; }
|
|
158
|
+
</style>
|
|
159
|
+
</head>
|
|
160
|
+
<body>
|
|
161
|
+
<h1>compressor savings <span class="footer">(${escapeHtml(window)})</span></h1>
|
|
162
|
+
<p class="totals">saved ${fmt(savedChars)} chars (exact) ≈ ${fmt(savedTokens)} tokens (estimated — cheap estimator, not billable counts) · ${fmt(events.length)} events · ${escapeHtml(window)}</p>
|
|
163
|
+
${sections}
|
|
164
|
+
<p class="footer">measured savings come from <code>compressor benchmark</code> — this view is the live estimated ledger.<br>
|
|
165
|
+
ledger: ${escapeHtml(dir)} · disable recording with COMPRESSOR_NO_LEDGER=1</p>
|
|
166
|
+
</body>
|
|
167
|
+
</html>
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
function parseBy(value) {
|
|
171
|
+
if (value === 'day' || value === 'tool' || value === 'mode') {
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
throw new Error(`invalid --by '${value}' (expected day|tool|mode)`);
|
|
175
|
+
}
|
|
176
|
+
export async function runSavings(opts) {
|
|
177
|
+
const sinceRaw = opts.since ?? '30d';
|
|
178
|
+
const since = parseSince(sinceRaw);
|
|
179
|
+
const window = windowLabel(sinceRaw);
|
|
180
|
+
const by = parseBy(opts.by ?? 'day');
|
|
181
|
+
const dir = opts.ledgerDir ?? resolveLedgerDir();
|
|
182
|
+
const events = await readLedger(since === undefined ? { dir } : { dir, since });
|
|
183
|
+
if (events.length === 0) {
|
|
184
|
+
// distinguish a truly empty ledger from one whose events all fall
|
|
185
|
+
// outside the window — the latter must not suggest reinstalling a hook
|
|
186
|
+
// that works fine
|
|
187
|
+
const allTime = since === undefined ? [] : await readLedger({ dir });
|
|
188
|
+
console.log(renderEmpty(dir, window, allTime.length));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.log(renderSavings(events, by, dir, window));
|
|
192
|
+
if (opts.html !== undefined) {
|
|
193
|
+
await writeFile(opts.html, renderSavingsHtml(events, dir, window), 'utf8');
|
|
194
|
+
console.log(`\nhtml report written to ${opts.html}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { effectNote, installForAgents, parsePackMode, resolveAgents } from "./init.js";
|
|
2
|
+
import { uninstallForAgents } from "./uninstall.js";
|
|
3
|
+
export async function runSetMode(mode, opts) {
|
|
4
|
+
const agents = resolveAgents(opts.agent);
|
|
5
|
+
if (mode === 'full') {
|
|
6
|
+
await uninstallForAgents(agents, opts);
|
|
7
|
+
const names = agents.map((adapter) => adapter.name).join(', ');
|
|
8
|
+
const suffix = opts.dryRun === true ? ' (dry-run: nothing written)' : '';
|
|
9
|
+
console.log(`Mode full: compressor artifacts removed for ${names} (true baseline, no instruction pack, no hook). ${effectNote(agents)}${suffix}`);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
await installForAgents(agents, parsePackMode(mode), opts);
|
|
13
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { addUsage, aggregateUsage, findTranscripts, readSessionUsage, } from "../../claude/transcripts.js";
|
|
4
|
+
function parseSinceDays(value) {
|
|
5
|
+
const days = /^(\d+)d$/.exec(value)?.[1];
|
|
6
|
+
if (days === undefined) {
|
|
7
|
+
throw new Error(`invalid --since '${value}' (expected e.g. 7d, 30d)`);
|
|
8
|
+
}
|
|
9
|
+
return Number(days);
|
|
10
|
+
}
|
|
11
|
+
const fmt = (n) => n.toLocaleString('en-US');
|
|
12
|
+
export async function runStats(opts) {
|
|
13
|
+
const days = parseSinceDays(opts.since ?? '30d');
|
|
14
|
+
const projectDir = path.resolve(opts.project ?? process.cwd());
|
|
15
|
+
const since = new Date(Date.now() - days * 86_400_000);
|
|
16
|
+
const files = await findTranscripts({ projectDir, since });
|
|
17
|
+
const sessions = [];
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
sessions.push(await readSessionUsage(file));
|
|
20
|
+
}
|
|
21
|
+
const totals = aggregateUsage(sessions);
|
|
22
|
+
const turns = sessions.reduce((acc, s) => acc + s.turns, 0);
|
|
23
|
+
const byModel = {};
|
|
24
|
+
for (const session of sessions) {
|
|
25
|
+
for (const [model, usage] of Object.entries(session.byModel)) {
|
|
26
|
+
byModel[model] = addUsage(byModel[model] ?? { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 }, usage);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const rows = [
|
|
30
|
+
['sessions', fmt(sessions.length)],
|
|
31
|
+
['turns', fmt(turns)],
|
|
32
|
+
['input', fmt(totals.input)],
|
|
33
|
+
['output', fmt(totals.output)],
|
|
34
|
+
['cacheCreation', fmt(totals.cacheCreation)],
|
|
35
|
+
['cacheRead', fmt(totals.cacheRead)],
|
|
36
|
+
];
|
|
37
|
+
const width = Math.max(...rows.map(([label]) => label.length));
|
|
38
|
+
for (const [label, value] of rows) {
|
|
39
|
+
console.log(`${label.padEnd(width)} ${value}`);
|
|
40
|
+
}
|
|
41
|
+
const models = Object.entries(byModel);
|
|
42
|
+
if (models.length > 0) {
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log('by model:');
|
|
45
|
+
for (const [model, u] of models) {
|
|
46
|
+
console.log(` ${model}: input=${fmt(u.input)} output=${fmt(u.output)} cacheCreation=${fmt(u.cacheCreation)} cacheRead=${fmt(u.cacheRead)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log(`actual usage from Claude Code transcripts (last ${days}d, ${projectDir})`);
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runStatus(global?: boolean): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { adapters } from "../../adapters/index.js";
|
|
2
|
+
import { buildContext } from "./init.js";
|
|
3
|
+
export async function runStatus(global = false) {
|
|
4
|
+
const ctx = buildContext(global, 'optimized', false);
|
|
5
|
+
for (const adapter of adapters) {
|
|
6
|
+
const status = await adapter.status(ctx);
|
|
7
|
+
const installed = status.installed ? 'installed' : 'not installed';
|
|
8
|
+
const mode = status.mode === undefined ? '' : ` (mode=${status.mode})`;
|
|
9
|
+
console.log(`${status.agent}: ${installed}${mode} — ${status.detail}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Adapter } from '../../adapters/index.ts';
|
|
2
|
+
import type { ScopeOptions } from './init.ts';
|
|
3
|
+
export interface UninstallOptions extends ScopeOptions {
|
|
4
|
+
agent: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function uninstallForAgents(agents: Adapter[], opts: ScopeOptions): Promise<void>;
|
|
7
|
+
export declare function runUninstall(opts: UninstallOptions): Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { applyChanges, renderChanges } from "../../adapters/index.js";
|
|
2
|
+
import { buildContext, resolveAgents } from "./init.js";
|
|
3
|
+
export async function uninstallForAgents(agents, opts) {
|
|
4
|
+
const ctx = buildContext(opts.global === true, 'optimized', false);
|
|
5
|
+
for (const adapter of agents) {
|
|
6
|
+
const changes = await adapter.uninstall(ctx);
|
|
7
|
+
const rendered = renderChanges(changes);
|
|
8
|
+
if (rendered !== '') {
|
|
9
|
+
console.log(rendered);
|
|
10
|
+
}
|
|
11
|
+
if (opts.dryRun !== true) {
|
|
12
|
+
await applyChanges(changes);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function runUninstall(opts) {
|
|
17
|
+
const agents = resolveAgents(opts.agent);
|
|
18
|
+
await uninstallForAgents(agents, opts);
|
|
19
|
+
const names = agents.map((adapter) => adapter.name).join(', ');
|
|
20
|
+
const suffix = opts.dryRun === true ? ' (dry-run: nothing written)' : '';
|
|
21
|
+
console.log(`Compressor artifacts removed for ${names}.${suffix}`);
|
|
22
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
const program = new Command();
|
|
5
|
+
program
|
|
6
|
+
.name('compressor')
|
|
7
|
+
.description('Token optimization for AI coding agents: instruction packs, tool-output compression, measured savings')
|
|
8
|
+
.version('0.1.0');
|
|
9
|
+
program
|
|
10
|
+
.command('init')
|
|
11
|
+
.description('install the instruction pack + PostToolUse hook for the given agents')
|
|
12
|
+
.option('--agent <name...>', 'agent adapters to target', ['claude-code'])
|
|
13
|
+
.option('--mode <mode>', 'pack mode (optimized|slim)', 'optimized')
|
|
14
|
+
.option('--global', 'install at user level instead of project level')
|
|
15
|
+
.option('--dry-run', 'print planned changes without writing')
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
const { runInit } = await import("./commands/init.js");
|
|
18
|
+
await runInit(opts);
|
|
19
|
+
});
|
|
20
|
+
program
|
|
21
|
+
.command('set-mode')
|
|
22
|
+
.description("switch mode; 'full' removes all compressor artifacts (true baseline)")
|
|
23
|
+
.argument('<mode>', 'full|optimized|slim')
|
|
24
|
+
.option('--agent <name...>', 'agent adapters to target', ['claude-code'])
|
|
25
|
+
.option('--global', 'apply at user level instead of project level')
|
|
26
|
+
.option('--dry-run', 'print planned changes without writing')
|
|
27
|
+
.action(async (mode, opts) => {
|
|
28
|
+
const { runSetMode } = await import("./commands/set-mode.js");
|
|
29
|
+
await runSetMode(mode, opts);
|
|
30
|
+
});
|
|
31
|
+
program
|
|
32
|
+
.command('status')
|
|
33
|
+
.description('show installation state per agent')
|
|
34
|
+
.option('--global', 'report user-level (machine-wide) state instead of project state')
|
|
35
|
+
.action(async (opts) => {
|
|
36
|
+
const { runStatus } = await import("./commands/status.js");
|
|
37
|
+
await runStatus(opts.global === true);
|
|
38
|
+
});
|
|
39
|
+
program
|
|
40
|
+
.command('uninstall')
|
|
41
|
+
.description('remove all compressor-owned artifacts')
|
|
42
|
+
.option('--agent <name...>', 'agent adapters to target', ['claude-code'])
|
|
43
|
+
.option('--global', 'apply at user level instead of project level')
|
|
44
|
+
.option('--dry-run', 'print planned changes without writing')
|
|
45
|
+
.action(async (opts) => {
|
|
46
|
+
const { runUninstall } = await import("./commands/uninstall.js");
|
|
47
|
+
await runUninstall(opts);
|
|
48
|
+
});
|
|
49
|
+
program
|
|
50
|
+
.command('compress')
|
|
51
|
+
.description('compress stdin to stdout via the engine; stats on stderr')
|
|
52
|
+
.option('--mode <mode>', 'full|optimized|slim', 'optimized')
|
|
53
|
+
.option('--kind <kind>', 'read|bash|search|other', 'other')
|
|
54
|
+
.option('--file-path <path>', 'source path hint (drives code detection)')
|
|
55
|
+
.option('--marker-style <style>', 'plain|deterrent|informative (default: policy value)')
|
|
56
|
+
.action(async (opts) => {
|
|
57
|
+
const { runCompress } = await import("./commands/compress.js");
|
|
58
|
+
await runCompress(opts);
|
|
59
|
+
});
|
|
60
|
+
program
|
|
61
|
+
.command('count')
|
|
62
|
+
.description('count tokens per file (estimated by default, --exact via Anthropic API)')
|
|
63
|
+
.argument('<file...>', 'files to count')
|
|
64
|
+
.option('--exact', 'use the Anthropic count_tokens endpoint (needs ANTHROPIC_API_KEY)')
|
|
65
|
+
.option('--model <model>', 'model for --exact counts', 'claude-sonnet-4-6')
|
|
66
|
+
.action(async (files, opts) => {
|
|
67
|
+
const { runCount } = await import("./commands/count.js");
|
|
68
|
+
await runCount(files, opts);
|
|
69
|
+
});
|
|
70
|
+
program
|
|
71
|
+
.command('stats')
|
|
72
|
+
.description('aggregate actual token usage from Claude Code transcripts')
|
|
73
|
+
.option('--project <path>', 'project directory (default: cwd)')
|
|
74
|
+
.option('--since <window>', 'lookback window, e.g. 7d or 30d', '30d')
|
|
75
|
+
.action(async (opts) => {
|
|
76
|
+
const { runStats } = await import("./commands/stats.js");
|
|
77
|
+
await runStats(opts);
|
|
78
|
+
});
|
|
79
|
+
program
|
|
80
|
+
.command('savings')
|
|
81
|
+
.description('show what the compression hook saved (live ledger, estimated tokens)')
|
|
82
|
+
.option('--since <window>', "lookback window: e.g. 7d, 30d, or 'all'", '30d')
|
|
83
|
+
.option('--by <dimension>', 'aggregate by day|tool|mode', 'day')
|
|
84
|
+
.option('--html <path>', 'also write a self-contained HTML report (inline SVG, no JS)')
|
|
85
|
+
.option('--ledger-dir <dir>', 'ledger directory (default: COMPRESSOR_LEDGER_DIR or ~/.compressor/ledger)')
|
|
86
|
+
.action(async (opts) => {
|
|
87
|
+
const { runSavings } = await import("./commands/savings.js");
|
|
88
|
+
await runSavings(opts);
|
|
89
|
+
});
|
|
90
|
+
program
|
|
91
|
+
.command('benchmark')
|
|
92
|
+
.description('run the benchmark suite: cells = task × variant × trial, results as JSONL')
|
|
93
|
+
.option('--suite <path>', 'suite JSON file', 'bench/suites/basic.json')
|
|
94
|
+
.option('--modes <modes>', 'comma-separated full|optimized|slim', 'full,optimized,slim')
|
|
95
|
+
.option('--trials <n>', 'trials per task × variant', '5')
|
|
96
|
+
.option('--model <model>', 'requested model (served model verified per cell)', 'claude-sonnet-4-6')
|
|
97
|
+
.option('--ablate <ids>', 'comma-separated atom ids: adds optimized-minus-<id> variants (slim-minus-<id> for slim-only atoms)')
|
|
98
|
+
.option('--ablate-add <ids>', 'comma-separated REJECTED atom ids: adds optimized-plus-<id> variants')
|
|
99
|
+
.option('--ablate-group <groups>', 'comma-separated atom categories (output|behavior): adds optimized-minus-<group>-atoms variants with every atom of that category removed')
|
|
100
|
+
.option('--no-hook', 'skip the compression hook in optimized/slim cells')
|
|
101
|
+
.option('--hook-args <args>', "extra args appended to the hook command in every hook-bearing variant (e.g. '--marker-style informative')")
|
|
102
|
+
.option('--marker-styles <styles>', 'comma-separated plain|deterrent|informative: each hook-bearing variant fans out into one arm per style IN THE SAME RUN (one budget ceiling, balanced task×trial groups)')
|
|
103
|
+
.option('--concurrency <n>', 'cells run in parallel', '2')
|
|
104
|
+
.option('--max-budget-usd <usd>', 'hard cost ceiling; scheduling stops when reached', '5')
|
|
105
|
+
.option('--out <dir>', 'results directory', 'bench/results')
|
|
106
|
+
.action(async (opts) => {
|
|
107
|
+
const { runBenchmarkCommand } = await import("./commands/benchmark.js");
|
|
108
|
+
await runBenchmarkCommand(opts);
|
|
109
|
+
});
|
|
110
|
+
program
|
|
111
|
+
.command('report')
|
|
112
|
+
.description('aggregate a benchmark run: per-variant medians+IQR, deltas vs full, per-atom ablation deltas vs their baseline')
|
|
113
|
+
.option('--run <id>', 'run id (default: latest run in --out)')
|
|
114
|
+
.option('--out <dir>', 'results directory', 'bench/results')
|
|
115
|
+
.option('--compare <runs...>', 'compare two runs side-by-side by variant')
|
|
116
|
+
.option('--format <format>', 'table|md|json', 'table')
|
|
117
|
+
.action(async (opts) => {
|
|
118
|
+
const { runReport } = await import("./commands/report.js");
|
|
119
|
+
await runReport(opts);
|
|
120
|
+
});
|
|
121
|
+
const hook = program.command('hook').description('hook protocol entry points (read stdin)');
|
|
122
|
+
hook
|
|
123
|
+
.command('post-tool-use')
|
|
124
|
+
.description('PostToolUse protocol: payload on stdin, updated output on stdout')
|
|
125
|
+
.option('--mode <mode>', 'full|optimized|slim', 'optimized')
|
|
126
|
+
.option('--marker-style <style>', 'plain|deterrent|informative (default: policy value)')
|
|
127
|
+
.action(async (opts) => {
|
|
128
|
+
const { runHookPostToolUse } = await import("./commands/hook.js");
|
|
129
|
+
await runHookPostToolUse(opts);
|
|
130
|
+
});
|
|
131
|
+
hook
|
|
132
|
+
.command('copilot-post-tool-use')
|
|
133
|
+
.description('Copilot postToolUse protocol: payload on stdin, modifiedResult JSON on stdout')
|
|
134
|
+
.option('--mode <mode>', 'full|optimized|slim', 'optimized')
|
|
135
|
+
.option('--marker-style <style>', 'plain|deterrent|informative (default: policy value)')
|
|
136
|
+
.action(async (opts) => {
|
|
137
|
+
const { runHookCopilotPostToolUse } = await import("./commands/hook.js");
|
|
138
|
+
await runHookCopilotPostToolUse(opts);
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
await program.parseAsync(process.argv);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { handleCopilotPostToolUse } from "./hook/copilot.js";
|
|
3
|
+
import { settleThenExit } from "./hook/exit.js";
|
|
4
|
+
// Copilot postToolUse hook entry, bundled to dist/copilot-hook.js. Fail-open:
|
|
5
|
+
// any failure means emit nothing and exit 0 so the original tool result
|
|
6
|
+
// passes through (Copilot parses stdout as the hook output JSON only when
|
|
7
|
+
// present; empty stdout = no-op).
|
|
8
|
+
function parseMode(argv) {
|
|
9
|
+
const idx = argv.indexOf('--mode');
|
|
10
|
+
const value = idx === -1 ? undefined : argv[idx + 1];
|
|
11
|
+
return value === 'full' || value === 'optimized' || value === 'slim' ? value : 'optimized';
|
|
12
|
+
}
|
|
13
|
+
/** Fail-open: unknown or missing style falls back to the policy default. */
|
|
14
|
+
function parseMarkerStyle(argv) {
|
|
15
|
+
const idx = argv.indexOf('--marker-style');
|
|
16
|
+
const value = idx === -1 ? undefined : argv[idx + 1];
|
|
17
|
+
return value === 'plain' || value === 'deterrent' || value === 'informative'
|
|
18
|
+
? value
|
|
19
|
+
: undefined;
|
|
20
|
+
}
|
|
21
|
+
async function main() {
|
|
22
|
+
const chunks = [];
|
|
23
|
+
for await (const chunk of process.stdin) {
|
|
24
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
25
|
+
}
|
|
26
|
+
const payload = Buffer.concat(chunks).toString('utf8');
|
|
27
|
+
return handleCopilotPostToolUse(payload, parseMode(process.argv), parseMarkerStyle(process.argv)).output;
|
|
28
|
+
}
|
|
29
|
+
// Exit path shared with the claude-code entry and the CLI subcommands
|
|
30
|
+
// (src/hook/exit.ts): stdout first, ledger settle capped at 250ms, SIGKILL
|
|
31
|
+
// on timeout so a stuck filesystem can never hang the agent.
|
|
32
|
+
main().then((output) => {
|
|
33
|
+
settleThenExit(output).catch(() => process.exit(0));
|
|
34
|
+
}, () => {
|
|
35
|
+
settleThenExit(null).catch(() => process.exit(0));
|
|
36
|
+
});
|