Skip to content

Commit ff47066

Browse files
jiunbaeclaude
andcommitted
fix: resolve biome lint errors and simplify README quickstart
Fix all biome lint errors (formatting, import sorting, noNonNullAssertion, useTemplate, noDelete, useNumberNamespace) and trim the Quickstart for Agents section to a one-liner install command. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 55604c6 commit ff47066

11 files changed

Lines changed: 223 additions & 124 deletions

File tree

README.md

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,11 @@ A context-aware **MCP server** for [Claude Code](https://claude.ai/claude-code)
1919

2020
## Quickstart for Agents
2121

22-
<div><img src="https://quickstart-for-agents.vercel.app/api/header.svg?theme=claude-code&title=Install+and+configure+context-compress&mascot=thinking&lang=MCP" width="100%" /></div>
23-
2422
```
25-
Install context-compress — a context-aware MCP server that compresses tool output
26-
for Claude Code. It keeps raw data in sandboxed subprocesses so only concise
27-
summaries enter the context window. Supports 11 languages (JS, TS, Python, Shell,
28-
Ruby, Go, Rust, PHP, Perl, R, Elixir) with FTS5 search and smart truncation.
29-
30-
Steps:
31-
1. Run: npm install -g context-compress
32-
2. Run: claude mcp add context-compress -- node $(which context-compress)
33-
3. Run: context-compress doctor # verify runtimes, hooks, FTS5
34-
4. Optionally create .context-compress.json with:
35-
{ "passthroughEnvVars": ["GH_TOKEN"], "debug": false }
36-
37-
After setup, the PreToolUse hook auto-redirects data-fetching tools through
38-
the sandbox. Use execute() instead of Bash for large outputs, execute_file()
39-
instead of Read for large files, and fetch_and_index() instead of WebFetch.
23+
npm install -g context-compress && claude mcp add context-compress -- node $(which context-compress)
4024
```
4125

42-
<div><img src="https://quickstart-for-agents.vercel.app/api/footer.svg?theme=claude-code&tokens=4.2k&model=Opus+4.6&project=context-compress" width="100%" /></div>
26+
MCP server that compresses tool output for Claude Code — raw data stays in sandboxed subprocesses, only concise summaries enter your context window.
4327

4428
---
4529

src/cli/doctor.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
import { accessSync, constants, readFileSync } from "node:fs";
2-
import { dirname, resolve } from "node:path";
1+
import { constants, accessSync, readFileSync } from "node:fs";
32
import { homedir } from "node:os";
3+
import { dirname, resolve } from "node:path";
44
import { fileURLToPath } from "node:url";
55
import Database from "better-sqlite3";
6-
import { SubprocessExecutor } from "../executor.js";
76
import { loadConfig } from "../config.js";
7+
import { SubprocessExecutor } from "../executor.js";
88
import { detectRuntimes, getRuntimeSummary, hasBun } from "../runtime/index.js";
99

1010
const __dirname = dirname(fileURLToPath(import.meta.url));
1111

1212
function getVersion(): string {
1313
try {
14-
const pkg = JSON.parse(
15-
readFileSync(resolve(__dirname, "..", "..", "package.json"), "utf-8"),
16-
);
14+
const pkg = JSON.parse(readFileSync(resolve(__dirname, "..", "..", "package.json"), "utf-8"));
1715
return pkg.version ?? "unknown";
1816
} catch {
1917
return "unknown";
@@ -80,7 +78,9 @@ export async function doctor(): Promise<number> {
8078
const settings = readSettings();
8179
if (settings) {
8280
const hooks = settings.hooks as Record<string, unknown[]> | undefined;
83-
const preToolUse = hooks?.PreToolUse as Array<{ hooks?: Array<{ command?: string }> }> | undefined;
81+
const preToolUse = hooks?.PreToolUse as
82+
| Array<{ hooks?: Array<{ command?: string }> }>
83+
| undefined;
8484
if (preToolUse?.some((e) => e.hooks?.some((h) => h.command?.includes("pretooluse.mjs")))) {
8585
console.log(" [PASS] PreToolUse hook configured");
8686
} else {
@@ -105,9 +105,11 @@ export async function doctor(): Promise<number> {
105105
const db = new Database(":memory:");
106106
db.exec("CREATE VIRTUAL TABLE fts_test USING fts5(content)");
107107
db.exec("INSERT INTO fts_test(content) VALUES ('hello world')");
108-
const row = db.prepare("SELECT * FROM fts_test WHERE fts_test MATCH 'hello'").get() as {
109-
content: string;
110-
} | undefined;
108+
const row = db.prepare("SELECT * FROM fts_test WHERE fts_test MATCH 'hello'").get() as
109+
| {
110+
content: string;
111+
}
112+
| undefined;
111113
db.close();
112114
if (row?.content === "hello world") {
113115
console.log(" [PASS] FTS5 / better-sqlite3 works");

src/cli/setup.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,7 @@ export async function setup(): Promise<void> {
2323
}
2424

2525
// Step 3: Missing optional runtimes
26-
const all = [
27-
"python",
28-
"ruby",
29-
"go",
30-
"rust",
31-
"php",
32-
"perl",
33-
"r",
34-
"elixir",
35-
] as const;
26+
const all = ["python", "ruby", "go", "rust", "php", "perl", "r", "elixir"] as const;
3627
const missing = all.filter((lang) => !runtimes.has(lang));
3728
if (missing.length > 0) {
3829
console.log(` Optional runtimes not found: ${missing.join(", ")}`);

src/cli/uninstall.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ export async function uninstall(): Promise<void> {
1717
const before = hooks.PreToolUse.length;
1818
hooks.PreToolUse = (hooks.PreToolUse as Array<Record<string, unknown>>).filter((entry) => {
1919
const entryHooks = entry.hooks as Array<{ command?: string }> | undefined;
20-
return !entryHooks?.some((h) => h.command?.includes("context-compress") || h.command?.includes("pretooluse.mjs"));
20+
return !entryHooks?.some(
21+
(h) => h.command?.includes("context-compress") || h.command?.includes("pretooluse.mjs"),
22+
);
2123
});
2224
if (hooks.PreToolUse.length === 0) {
25+
// biome-ignore lint/performance/noDelete: removing key from JSON object
2326
delete hooks.PreToolUse;
2427
}
2528
if (hooks.PreToolUse === undefined || hooks.PreToolUse.length < before) {
2629
settings.hooks = hooks;
27-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
30+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
2831
changes.push("Removed PreToolUse hooks from settings.json");
2932
}
3033
}
@@ -39,8 +42,8 @@ export async function uninstall(): Promise<void> {
3942
const settings = JSON.parse(readFileSync(mcpPath, "utf-8"));
4043
const mcpServers = settings.mcpServers as Record<string, unknown> | undefined;
4144
if (mcpServers && "context-compress" in mcpServers) {
42-
delete mcpServers["context-compress"];
43-
writeFileSync(mcpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
45+
mcpServers["context-compress"] = undefined;
46+
writeFileSync(mcpPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
4447
changes.push("Removed context-compress MCP server from settings");
4548
}
4649
} catch {
@@ -54,8 +57,8 @@ export async function uninstall(): Promise<void> {
5457
const mcp = JSON.parse(readFileSync(mcpJson, "utf-8"));
5558
const servers = mcp.mcpServers as Record<string, unknown> | undefined;
5659
if (servers && "context-compress" in servers) {
57-
delete servers["context-compress"];
58-
writeFileSync(mcpJson, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
60+
servers["context-compress"] = undefined;
61+
writeFileSync(mcpJson, `${JSON.stringify(mcp, null, 2)}\n`, "utf-8");
5962
changes.push("Removed context-compress from .mcp.json");
6063
}
6164
} catch {
@@ -97,7 +100,5 @@ export async function uninstall(): Promise<void> {
97100
console.log(" Nothing to clean up.");
98101
}
99102

100-
console.log(
101-
"\n Uninstall complete. Restart Claude Code to apply changes.\n",
102-
);
103+
console.log("\n Uninstall complete. Restart Claude Code to apply changes.\n");
103104
}

src/config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ function loadEnvConfig(): Partial<Config> {
7676
partial.debug = true;
7777
}
7878
if (process.env.CONTEXT_COMPRESS_PASSTHROUGH_ENV) {
79-
partial.passthroughEnvVars = process.env.CONTEXT_COMPRESS_PASSTHROUGH_ENV
80-
.split(",")
79+
partial.passthroughEnvVars = process.env.CONTEXT_COMPRESS_PASSTHROUGH_ENV.split(",")
8180
.map((s) => s.trim())
8281
.filter(Boolean);
8382
}

src/executor.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function buildEnv(config: Config): Record<string, string> {
4444
// Copy safe base variables
4545
for (const key of SAFE_ENV_KEYS) {
4646
if (process.env[key]) {
47-
env[key] = process.env[key]!;
47+
env[key] = process.env[key] as string;
4848
}
4949
}
5050

@@ -57,7 +57,7 @@ function buildEnv(config: Config): Record<string, string> {
5757
// Opt-in passthrough (security fix: default is empty)
5858
for (const key of config.passthroughEnvVars) {
5959
if (process.env[key]) {
60-
env[key] = process.env[key]!;
60+
env[key] = process.env[key] as string;
6161
}
6262
}
6363

@@ -297,7 +297,7 @@ export class SubprocessExecutor {
297297
// Extract network bytes from JS/TS stderr marker
298298
const netMatch = stderr.match(/__CM_NET__:(\d+)/);
299299
if (netMatch) {
300-
networkBytes = parseInt(netMatch[1], 10);
300+
networkBytes = Number.parseInt(netMatch[1], 10);
301301
stderr = stderr.replace(/__CM_NET__:\d+\n?/, "");
302302
}
303303

@@ -341,7 +341,8 @@ export class SubprocessExecutor {
341341
* Wrap JS/TS code with fetch interceptor for network tracking.
342342
*/
343343
function wrapWithNetworkTracking(code: string): string {
344-
const preamble = `let __cm_net=0;const __cm_f=globalThis.fetch;if(__cm_f){globalThis.fetch=async(...a)=>{const r=await __cm_f(...a);try{const cl=r.clone();const b=await cl.arrayBuffer();__cm_net+=b.byteLength}catch{}return r};}`;
344+
const preamble =
345+
"let __cm_net=0;const __cm_f=globalThis.fetch;if(__cm_f){globalThis.fetch=async(...a)=>{const r=await __cm_f(...a);try{const cl=r.clone();const b=await cl.arrayBuffer();__cm_net+=b.byteLength}catch{}return r};}";
345346
const epilogue = `\nprocess.stderr.write('__CM_NET__:'+__cm_net+'\\n');`;
346347

347348
// Wrap in async IIFE

src/runtime/languages/rust.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@ export const rustPlugin: LanguagePlugin = {
2626
const escaped = JSON.stringify(filePath);
2727
const preamble = `use std::fs;\nlet file_content_path = ${escaped};\nlet file_content = fs::read_to_string(file_content_path).unwrap();\n`;
2828
if (code.includes("fn main")) {
29-
return code.replace(
30-
/fn main\s*\(\s*\)\s*\{/,
31-
`fn main() {\n${preamble}`,
32-
);
29+
return code.replace(/fn main\s*\(\s*\)\s*\{/, `fn main() {\n${preamble}`);
3330
}
3431
return `fn main() {\n${preamble}${code}\n}`;
3532
},

src/server.ts

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,17 @@ import { ContentStore, cleanupStaleDbs } from "./store.js";
1313
import type { Language } from "./types.js";
1414

1515
const LANGUAGES: [Language, ...Language[]] = [
16-
"javascript", "typescript", "python", "shell", "ruby",
17-
"go", "rust", "php", "perl", "r", "elixir",
16+
"javascript",
17+
"typescript",
18+
"python",
19+
"shell",
20+
"ruby",
21+
"go",
22+
"rust",
23+
"php",
24+
"perl",
25+
"r",
26+
"elixir",
1827
];
1928

2029
function getVersion(): string {
@@ -61,8 +70,17 @@ export async function createServer(config: Config) {
6170
PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, pytest), git queries (git log, git diff), data processing, and ANY CLI command that may produce large output. Bash should only be used for file mutations, git writes, and navigation.`,
6271
{
6372
language: z.enum(LANGUAGES).describe("Runtime language"),
64-
code: z.string().describe("Source code to execute. Use console.log (JS/TS), print (Python/Ruby/Perl/R), echo (Shell), echo (PHP), fmt.Println (Go), or IO.puts (Elixir) to output a summary to context."),
65-
intent: z.string().optional().describe("What you're looking for in the output. When provided and output is large (>5KB), indexes output into knowledge base and returns section titles + previews — not full content. Use search(queries: [...]) to retrieve specific sections."),
73+
code: z
74+
.string()
75+
.describe(
76+
"Source code to execute. Use console.log (JS/TS), print (Python/Ruby/Perl/R), echo (Shell), echo (PHP), fmt.Println (Go), or IO.puts (Elixir) to output a summary to context.",
77+
),
78+
intent: z
79+
.string()
80+
.optional()
81+
.describe(
82+
"What you're looking for in the output. When provided and output is large (>5KB), indexes output into knowledge base and returns section titles + previews — not full content. Use search(queries: [...]) to retrieve specific sections.",
83+
),
6684
timeout: z.number().default(30000).describe("Max execution time in ms"),
6785
},
6886
async ({ language, code, intent, timeout }) => {
@@ -93,7 +111,7 @@ PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, py
93111
if (terms.length > 0) {
94112
filtered += `\nSearchable terms: ${terms.join(", ")}\n`;
95113
}
96-
filtered += `\nUse search(queries: [...]) to retrieve full content of any section.`;
114+
filtered += "\nUse search(queries: [...]) to retrieve full content of any section.";
97115
output = filtered;
98116
}
99117

@@ -112,7 +130,11 @@ PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, py
112130
{
113131
path: z.string().describe("Absolute file path or relative to project root"),
114132
language: z.enum(LANGUAGES).describe("Runtime language"),
115-
code: z.string().describe("Code to process FILE_CONTENT. Print summary via console.log/print/echo/IO.puts."),
133+
code: z
134+
.string()
135+
.describe(
136+
"Code to process FILE_CONTENT. Print summary via console.log/print/echo/IO.puts.",
137+
),
116138
intent: z.string().optional().describe("What you're looking for in the output."),
117139
timeout: z.number().default(30000).describe("Max execution time in ms"),
118140
},
@@ -147,7 +169,7 @@ PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, py
147169
if (terms.length > 0) {
148170
filtered += `\nSearchable terms: ${terms.join(", ")}\n`;
149171
}
150-
filtered += `\nUse search(queries: [...]) to retrieve full content of any section.`;
172+
filtered += "\nUse search(queries: [...]) to retrieve full content of any section.";
151173
output = filtered;
152174
}
153175

@@ -162,10 +184,16 @@ PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, py
162184

163185
server.tool(
164186
"index",
165-
'Index documentation or knowledge content into a searchable BM25 knowledge base. Chunks markdown by headings (keeping code blocks intact) and stores in ephemeral FTS5 database. The full content does NOT stay in context — only a brief summary is returned.\n\nWHEN TO USE:\n- Documentation (API docs, framework guides, code examples)\n- README files, migration guides, changelog entries\n- Any content with code examples you may need to reference precisely\n\nAfter indexing, use \'search\' to retrieve specific sections on-demand.',
187+
"Index documentation or knowledge content into a searchable BM25 knowledge base. Chunks markdown by headings (keeping code blocks intact) and stores in ephemeral FTS5 database. The full content does NOT stay in context — only a brief summary is returned.\n\nWHEN TO USE:\n- Documentation (API docs, framework guides, code examples)\n- README files, migration guides, changelog entries\n- Any content with code examples you may need to reference precisely\n\nAfter indexing, use 'search' to retrieve specific sections on-demand.",
166188
{
167-
content: z.string().optional().describe("Raw text/markdown to index. Provide this OR path, not both."),
168-
path: z.string().optional().describe("File path to read and index (content never enters context)."),
189+
content: z
190+
.string()
191+
.optional()
192+
.describe("Raw text/markdown to index. Provide this OR path, not both."),
193+
path: z
194+
.string()
195+
.optional()
196+
.describe("File path to read and index (content never enters context)."),
169197
source: z.string().optional().describe("Label for the indexed content"),
170198
},
171199
async ({ content, path: filePath, source }) => {
@@ -198,10 +226,15 @@ PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, py
198226

199227
server.tool(
200228
"search",
201-
'Search indexed content. Pass ALL search questions as queries array in ONE call.\n\nTIPS: 2-4 specific terms per query. Use \'source\' to scope results.',
229+
"Search indexed content. Pass ALL search questions as queries array in ONE call.\n\nTIPS: 2-4 specific terms per query. Use 'source' to scope results.",
202230
{
203-
queries: z.array(z.string()).describe("Array of search queries. Batch ALL questions in one call."),
204-
source: z.string().optional().describe("Filter to a specific indexed source (partial match)."),
231+
queries: z
232+
.array(z.string())
233+
.describe("Array of search queries. Batch ALL questions in one call."),
234+
source: z
235+
.string()
236+
.optional()
237+
.describe("Filter to a specific indexed source (partial match)."),
205238
limit: z.number().default(3).describe("Results per query (default: 3)"),
206239
},
207240
async ({ queries, source, limit }) => {
@@ -216,12 +249,14 @@ PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, py
216249
const callCount = searchCalls.length;
217250

218251
if (callCount > config.searchBlockAfter) {
219-
const msg = "Too many search calls in quick succession. Use batch_execute instead to run commands and search in one call.";
252+
const msg =
253+
"Too many search calls in quick succession. Use batch_execute instead to run commands and search in one call.";
220254
tracker.trackCall("search", Buffer.byteLength(msg));
221255
return { content: [{ type: "text" as const, text: msg }] };
222256
}
223257

224-
const effectiveLimit = callCount > config.searchReduceAfter ? 1 : Math.min(limit, config.searchLimit);
258+
const effectiveLimit =
259+
callCount > config.searchReduceAfter ? 1 : Math.min(limit, config.searchLimit);
225260

226261
const allResults: string[] = [];
227262
let totalBytes = 0;
@@ -249,7 +284,9 @@ PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, py
249284
}
250285

251286
if (callCount > config.searchReduceAfter) {
252-
allResults.push(`\n⚠ Search rate limited (${callCount} calls in ${config.searchWindowMs / 1000}s). Results reduced to 1 per query.`);
287+
allResults.push(
288+
`\n⚠ Search rate limited (${callCount} calls in ${config.searchWindowMs / 1000}s). Results reduced to 1 per query.`,
289+
);
253290
}
254291

255292
const output = allResults.join("\n---\n\n");
@@ -315,11 +352,19 @@ PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, py
315352
"batch_execute",
316353
"Execute multiple commands in ONE call, auto-index all output, and search with multiple queries. Returns search results directly — no follow-up calls needed.\n\nTHIS IS THE PRIMARY TOOL. Use this instead of multiple execute() calls.\n\nOne batch_execute call replaces 30+ execute calls + 10+ search calls.\nProvide all commands to run and all queries to search — everything happens in one round trip.",
317354
{
318-
commands: z.array(z.object({
319-
label: z.string().describe("Section header for this command's output"),
320-
command: z.string().describe("Shell command to execute"),
321-
})).describe("Commands to execute as a batch."),
322-
queries: z.array(z.string()).describe("Search queries to extract information from indexed output. Use 5-8 comprehensive queries."),
355+
commands: z
356+
.array(
357+
z.object({
358+
label: z.string().describe("Section header for this command's output"),
359+
command: z.string().describe("Shell command to execute"),
360+
}),
361+
)
362+
.describe("Commands to execute as a batch."),
363+
queries: z
364+
.array(z.string())
365+
.describe(
366+
"Search queries to extract information from indexed output. Use 5-8 comprehensive queries.",
367+
),
323368
timeout: z.number().default(60000).describe("Max execution time in ms (default: 60s)"),
324369
},
325370
async ({ commands, queries, timeout }) => {

src/snippet.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,7 @@ interface Window {
5252
* Extract snippet windows around match positions.
5353
* Returns concatenated windows with ellipsis separators.
5454
*/
55-
export function extractSnippet(
56-
highlighted: string,
57-
maxLen: number = DEFAULT_MAX_LEN,
58-
): string {
55+
export function extractSnippet(highlighted: string, maxLen: number = DEFAULT_MAX_LEN): string {
5956
const positions = positionsFromHighlight(highlighted);
6057
const clean = stripMarkers(highlighted);
6158

0 commit comments

Comments
 (0)