context-mode 0.5.0 → 0.5.3

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.
@@ -12,8 +12,8 @@
12
12
  {
13
13
  "name": "context-mode",
14
14
  "source": "./",
15
- "description": "Claude Code MCP plugin that saves 94% of your context window. Sandboxed code execution in 10 languages, FTS5 knowledge base with BM25 ranking, and smart truncation.",
16
- "version": "0.5.0",
15
+ "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 10 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
16
+ "version": "0.5.3",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "0.5.0",
4
- "description": "Claude Code MCP plugin that saves 94% of your context window. Sandboxed code execution in 10 languages, FTS5 knowledge base with BM25 ranking, and smart truncation.",
3
+ "version": "0.5.3",
4
+ "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 10 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
7
7
  "url": "https://github.com/mksglu"
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Context Mode
2
2
 
3
- **Claude Code MCP plugin that saves 94% of your context window.**
3
+ **Claude Code MCP plugin that saves 98% of your context window.**
4
4
 
5
5
  Every tool call in Claude Code consumes context tokens. A single Playwright snapshot burns 10K-135K tokens. A Context7 docs lookup dumps 4K-10K tokens. GitHub's `list_commits` with 30 results costs 29K-64K tokens. With 5+ MCP servers active, you lose ~55K tokens before your first message — and after 30 minutes of real debugging, responses slow to a crawl.
6
6
 
@@ -12,14 +12,14 @@ Claude Code has a 200K token context window. Here's how fast popular MCP servers
12
12
 
13
13
  | MCP Server | Tool | Without Context Mode | With Context Mode | Savings | Source |
14
14
  |---|---|---|---|---|---|
15
- | **Playwright** | `browser_snapshot` | 10K-135K tokens | ~20 tokens | **99%** | [playwright-mcp#1233](https://github.com/microsoft/playwright-mcp/issues/1233) |
16
- | **Context7** | `query-docs` | 4K-10K tokens | ~70 tokens | **98%** | [upstash/context7](https://github.com/upstash/context7) |
17
- | **GitHub** | `list_commits` (30) | 29K-64K tokens | ~10 tokens | **99%** | [github-mcp-server#142](https://github.com/github/github-mcp-server/issues/142) |
18
- | **Sentry** | issue analysis | 5K-30K tokens | ~25 tokens | **99%** | [getsentry/sentry-mcp](https://github.com/getsentry/sentry-mcp) |
19
- | **Supabase** | schema queries | 2K-30K tokens | ~30 tokens | **99%** | [supabase-community/supabase-mcp](https://github.com/supabase-community/supabase-mcp) |
20
- | **Firecrawl** | `scrape` / `crawl` | 5K-50K+ tokens | ~70 tokens | **99%** | [firecrawl](https://github.com/mendableai/firecrawl) |
21
- | **Chrome DevTools** | DOM / network | 5K-50K+ tokens | ~25 tokens | **99%** | Community benchmark |
22
- | **Fetch** | `fetch` | 5K-50K tokens | ~70 tokens | **99%** | Official reference server |
15
+ | **Playwright** | `browser_snapshot` | 10K-135K tokens | ~75 tokens | **99%** | [playwright-mcp#1233](https://github.com/microsoft/playwright-mcp/issues/1233) |
16
+ | **Context7** | `query-docs` | 4K-10K tokens | ~65 tokens | **98%** | [upstash/context7](https://github.com/upstash/context7) |
17
+ | **GitHub** | `list_commits` (30) | 29K-64K tokens | ~180 tokens | **99%** | [github-mcp-server#142](https://github.com/github/github-mcp-server/issues/142) |
18
+ | **Sentry** | issue analysis | 5K-30K tokens | ~85 tokens | **99%** | [getsentry/sentry-mcp](https://github.com/getsentry/sentry-mcp) |
19
+ | **Supabase** | schema queries | 2K-30K tokens | ~80 tokens | **99%** | [supabase-community/supabase-mcp](https://github.com/supabase-community/supabase-mcp) |
20
+ | **Firecrawl** | `scrape` / `crawl` | 5K-50K+ tokens | ~65 tokens | **99%** | [firecrawl](https://github.com/mendableai/firecrawl) |
21
+ | **Chrome DevTools** | DOM / network | 5K-50K+ tokens | ~75 tokens | **99%** | Community benchmark |
22
+ | **Fetch** | `fetch` | 5K-50K tokens | ~65 tokens | **99%** | Official reference server |
23
23
 
24
24
  **Real measurement** ([Scott Spence, 2025](https://scottspence.com/posts/optimising-mcp-server-context-usage-in-claude-code)): With 81+ MCP tools enabled across multiple servers, **143K of 200K tokens (72%) consumed** — 82K tokens just for MCP tool definitions. Only 28% left for actual work.
25
25
 
@@ -29,15 +29,15 @@ Claude Code has a 200K token context window. Here's how fast popular MCP servers
29
29
 
30
30
  | What you're doing | Without Context Mode | With Context Mode | Savings |
31
31
  |---|---|---|---|
32
- | Playwright `browser_snapshot` | 12 KB into context | 50 B summary | **99%** |
33
- | Context7 `query-docs` (React) | 60 KB raw docs | 285 B search result | **99%** |
34
- | `gh pr list` / `gh api` | 8 KB JSON response | 40 B summary | **99%** |
35
- | Read `access.log` (500 req) | 45 KB raw log | 71 B status breakdown | **99%** |
36
- | `npm test` (30 suites) | 6 KB raw output | 37 B pass/fail | **99%** |
37
- | Git log (153 commits) | 12 KB raw log | 18 B summary | **99%** |
38
- | Supabase Edge Functions docs | 4 KB raw docs | 123 B code example | **97%** |
32
+ | Playwright `browser_snapshot` | 56 KB into context | 299 B summary | **99%** |
33
+ | Context7 `query-docs` (React) | 5.9 KB raw docs | 261 B summary | **96%** |
34
+ | GitHub issues (20) | 59 KB JSON response | 1.1 KB summary | **98%** |
35
+ | Read `access.log` (500 req) | 45 KB raw log | 155 B status breakdown | **100%** |
36
+ | `vitest` (30 suites) | 6 KB raw output | 337 B pass/fail | **95%** |
37
+ | Git log (153 commits) | 12 KB raw log | 107 B summary | **99%** |
38
+ | Analytics CSV (500 rows) | 86 KB raw data | 222 B summary | **100%** |
39
39
 
40
- **Real aggregate across 13 scenarios: 194 KB raw → 12.6 KB context (94% savings)**
40
+ **Real aggregate across 14 scenarios: 315 KB raw → 5.4 KB context (98% savings)**
41
41
 
42
42
  ## Quick Start
43
43
 
@@ -75,7 +75,7 @@ Claude calls: execute({ language: "shell", code: "gh pr list --json title,state
75
75
  Returns: "3" ← 2 bytes instead of 8KB JSON
76
76
  ```
77
77
 
78
- **Intent-driven search** (v0.5.0): When you provide an `intent` parameter and output exceeds 5KB, Context Mode uses BM25 search to return only the relevant sections instead of blind head/tail truncation.
78
+ **Intent-driven search** (v0.5.2): When you provide an `intent` parameter and output exceeds 5KB, Context Mode uses score-based BM25 search to return only the relevant sections matching your intent.
79
79
 
80
80
  ```
81
81
  Claude calls: execute({
@@ -83,9 +83,12 @@ Claude calls: execute({
83
83
  code: "cat /var/log/app.log",
84
84
  intent: "connection refused database error"
85
85
  })
86
- Returns: only the 3 matching log sections (1.5KB) ← instead of 100KB truncated log
86
+ Returns: section titles + searchable terms (500B) ← instead of 100KB raw log
87
87
  ```
88
88
 
89
+ When intent search runs, the response includes `Searchable terms` — distinctive vocabulary
90
+ extracted from the output via IDF scoring. Use these terms for targeted follow-up `search()` calls.
91
+
89
92
  Authenticated CLIs work out of the box — `gh`, `aws`, `gcloud`, `kubectl`, `docker` credentials are passed through securely. Bun auto-detected for 3-5x faster JS/TS.
90
93
 
91
94
  ### `execute_file` — Process Files Without Loading
@@ -132,12 +135,12 @@ Use instead of WebFetch or Context7 when you need documentation — index once,
132
135
  ┌──────────────────────────────────────────────────────────────────┐
133
136
  │ Without Context Mode │
134
137
  │ │
135
- │ Claude Code → Playwright snapshot → 12KB into context │
136
- │ Claude Code → Context7 docs → 60KB into context │
137
- │ Claude Code → gh pr list → 8KB into context │
138
+ │ Claude Code → Playwright snapshot → 56KB into context │
139
+ │ Claude Code → Context7 docs → 6KB into context │
140
+ │ Claude Code → gh pr list → 6KB into context │
138
141
  │ Claude Code → cat access.log → 45KB into context │
139
142
  │ │
140
- │ Total: 125KB consumed = ~32,000 tokens = 16% of context gone │
143
+ │ Total: 113KB consumed = ~29,000 tokens = 14% of context gone │
141
144
  └──────────────────────────────────────────────────────────────────┘
142
145
 
143
146
  ┌──────────────────────────────────────────────────────────────────┐
@@ -145,10 +148,10 @@ Use instead of WebFetch or Context7 when you need documentation — index once,
145
148
  │ │
146
149
  │ Claude Code → fetch_and_index(url) → "Indexed 8 sections" (50B)│
147
150
  │ Claude Code → search("snapshot") → exact element (500B) │
148
- │ Claude Code → execute("gh pr list") → "3 open PRs" (40B)│
149
- │ Claude Code → execute_file(log) → "500:14, 404:89" (30B)│
151
+ │ Claude Code → execute("gh pr list") → "5 PRs, +59 -0" (719B)│
152
+ │ Claude Code → execute_file(log) → "500:13, 404:13" (155B)│
150
153
  │ │
151
- │ Total: 620B consumed = ~160 tokens = 0.08% of context
154
+ │ Total: 1.4KB consumed = ~350 tokens = 0.18% of context
152
155
  └──────────────────────────────────────────────────────────────────┘
153
156
  ```
154
157
 
@@ -163,7 +166,7 @@ Use instead of WebFetch or Context7 when you need documentation — index once,
163
166
  │ │ • 10 language runtimes │ │
164
167
  │ │ • Sandboxed subprocess │ │
165
168
  │ │ • Auth passthrough │ │
166
- │ │ • Smart truncation │ │
169
+ │ │ • Intent-driven search │ │
167
170
  │ └────────────────────────┘ │
168
171
  │ │
169
172
  │ ┌────────────────────────┐ │
@@ -172,6 +175,7 @@ Use instead of WebFetch or Context7 when you need documentation — index once,
172
175
  │ │ • BM25 ranking │ │
173
176
  │ │ • Porter stemming │ │
174
177
  │ │ • Heading-aware chunks │ │
178
+ │ │ • Vocabulary hints │ │
175
179
  │ └────────────────────────┘ │
176
180
  └──────────────────────────────┘
177
181
  ```
@@ -213,42 +217,36 @@ ORDER BY rank LIMIT 3;
213
217
 
214
218
  **Lazy singleton:** Database created only when `index` or `search` is first called — zero overhead for sessions that don't use it.
215
219
 
216
- ### Smart Truncation
217
-
218
- When subprocess output exceeds the 100KB buffer, Context Mode preserves both head and tail:
219
-
220
- ```
221
- Head (60%): Initial output with context
222
- ... [47 lines / 3.2KB truncated — showing first 12 + last 8 lines] ...
223
- Tail (40%): Final output with errors/results
224
- ```
220
+ ### Intent-Driven Search (v0.5.2)
225
221
 
226
- Line-boundary snapping never cuts mid-line. Error messages at the bottom are always preserved.
222
+ When `execute` or `execute_file` is called with an `intent` parameter and output exceeds 5KB, Context Mode uses score-based BM25 search to return only the relevant sections:
227
223
 
228
- ### Intent-Driven Search (v0.5.0)
229
-
230
- When `execute` or `execute_file` is called with an `intent` parameter and output exceeds 5KB, Context Mode replaces blind truncation with intelligent BM25 search:
224
+ - **Score-based search**: Searches ALL intent words independently, ranks chunks by match count
225
+ - **Searchable terms**: Distinctive vocabulary hints extracted via IDF scoring, helping you craft precise follow-up `search()` calls
226
+ - **Smarter chunk titles**: Uses the first content line of each chunk instead of generic "Section N" labels
231
227
 
232
228
  ```
233
- Traditional truncation:
234
- stdout (100KB) → head(60%) + tail(40%) → ~100KB in context
235
- Problem: relevant info in the middle is lost
229
+ Without intent:
230
+ stdout (100KB) → full output enters context
236
231
 
237
- Intent-driven search:
238
- stdout (100KB) → chunk by lines → in-memory FTS5 → search(intent)2-5KB relevant sections
239
- Result: only what you need enters context
232
+ With intent:
233
+ stdout (100KB) → chunk by lines → in-memory FTS5 → score all intent words top chunks + searchable terms
234
+ Result: only what you need enters context, plus vocabulary for targeted follow-ups
240
235
  ```
241
236
 
242
- Tested across 4 real-world scenarios:
237
+ **31% to 100% recall on real-world CHANGELOG test** — the score-based approach finds every relevant section, not just those matching a single query string.
243
238
 
244
- | Scenario | Smart Truncation | Intent Search | Intent Size | Truncation Size |
245
- |---|---|---|---|---|
246
- | Server log error (line 347/500) | **missed** | **found** | 1.5 KB | 5.0 KB |
247
- | 3 test failures among 200 tests | found 2/3 | **found 3/3** | 2.4 KB | 5.0 KB |
248
- | 2 build warnings among 300 lines | **missed both** | **found both** | 2.1 KB | 5.0 KB |
249
- | API auth error (line 743/1000) | **missed** | **found** | 1.2 KB | 4.9 KB |
239
+ Tested across 5 real-world scenarios:
240
+
241
+ | Scenario | Without Intent | With Intent | Size Reduction |
242
+ |---|---|---|---|
243
+ | Server log error (line 347/500) | error lost in output | **found** | 1.5 KB vs 5.0 KB |
244
+ | 3 test failures among 200 tests | only 2/3 visible | **all 3 found** | 2.4 KB vs 5.0 KB |
245
+ | 2 build warnings among 300 lines | both lost in output | **both found** | 2.1 KB vs 5.0 KB |
246
+ | API auth error (line 743/1000) | error lost in output | **found** | 1.2 KB vs 4.9 KB |
247
+ | Semantic gap (CHANGELOG search) | 31% recall | **100% recall** | Full coverage |
250
248
 
251
- Smart truncation fails on 3 of 4 scenarios because relevant content is in the dropped middle section. Intent search finds the target every time while using 50-75% fewer bytes.
249
+ Intent search finds the target every time while using 50-75% fewer bytes.
252
250
 
253
251
  ### HTML to Markdown Conversion
254
252
 
@@ -269,17 +267,17 @@ Tested with tools from popular MCP servers and Claude Code workflows:
269
267
 
270
268
  | Scenario | Tool | Raw | Context | Savings |
271
269
  |---|---|---|---|---|
272
- | Playwright page snapshot | `execute_file` | 50+ KB | 78 B | **99%** |
273
- | Context7 React docs | `index + search` | 5.9 KB | 285 B | **95%** |
274
- | Context7 Supabase docs | `index + search` | 3.9 KB | 123 B | **97%** |
275
- | Context7 Next.js docs | `index + search` | 6.5 KB | 273 B | **96%** |
276
- | httpbin.org API docs | `fetch_and_index` | 9.4 KB | 50 B | **99%** |
277
- | GitHub API response | `execute` | 8+ KB | 40 B | **99%** |
278
- | Access log (500 req) | `execute_file` | 45.1 KB | 71 B | **100%** |
279
- | Analytics CSV (500 rows) | `execute_file` | 85.5 KB | 11.5 KB | **87%** |
280
- | MCP tools manifest (40 tools) | `execute_file` | 17.0 KB | 78 B | **100%** |
281
- | npm test (30 suites) | `execute_file` | 6.0 KB | 37 B | **99%** |
282
- | Git log (153 commits) | `execute` | 11.6 KB | 18 B | **100%** |
270
+ | Playwright page snapshot | `execute` | 56.2 KB | 299 B | **99%** |
271
+ | Context7 React docs | `execute` | 5.9 KB | 261 B | **96%** |
272
+ | Context7 Next.js docs | `execute` | 6.5 KB | 249 B | **96%** |
273
+ | Context7 Tailwind docs | `execute` | 4.0 KB | 186 B | **95%** |
274
+ | GitHub Issues (20) | `execute` | 58.9 KB | 1.1 KB | **98%** |
275
+ | GitHub PR list (5) | `execute` | 6.4 KB | 719 B | **89%** |
276
+ | Access log (500 req) | `execute_file` | 45.1 KB | 155 B | **100%** |
277
+ | Analytics CSV (500 rows) | `execute_file` | 85.5 KB | 222 B | **100%** |
278
+ | MCP tools manifest (40 tools) | `execute_file` | 17.0 KB | 742 B | **96%** |
279
+ | Test output (30 suites) | `execute` | 6.0 KB | 337 B | **95%** |
280
+ | Git log (153 commits) | `execute` | 11.6 KB | 107 B | **99%** |
283
281
 
284
282
  ### Session Impact
285
283
 
@@ -287,9 +285,9 @@ Typical 45-minute debugging session:
287
285
 
288
286
  | Metric | Without | With | Delta |
289
287
  |---|---|---|---|
290
- | Context consumed | 177 KB | 10 KB | **-94%** |
291
- | Tokens used | ~45,300 | ~2,600 | **-94%** |
292
- | Context remaining | 77% | 95% | **+18pp** |
288
+ | Context consumed | 315 KB | 5.4 KB | **-98%** |
289
+ | Tokens used | ~80,600 | ~1,400 | **-98%** |
290
+ | Context remaining | 60% | 99% | **+39pp** |
293
291
  | Time before slowdown | ~30 min | ~3 hours | **+6x** |
294
292
 
295
293
  ## Tool Decision Matrix
@@ -388,13 +386,13 @@ Just ask naturally — Claude automatically routes through Context Mode when it
388
386
 
389
387
  ## Test Suite
390
388
 
391
- 99+ tests across 4 suites:
389
+ 100+ tests across 4 suites:
392
390
 
393
391
  | Suite | Tests | Coverage |
394
392
  |---|---|---|
395
- | Executor | 55 | 10 languages, sandbox, truncation, concurrency, timeouts |
393
+ | Executor | 55 | 10 languages, sandbox, output handling, concurrency, timeouts |
396
394
  | ContentStore | 40 | FTS5 schema, BM25 ranking, chunking, stemming, plain text indexing |
397
- | Intent Search | 4 | Smart truncation vs intent-driven search across 4 real-world scenarios |
395
+ | Intent Search | 5 | Intent-driven search across 5 real-world scenarios (incl. semantic gap) |
398
396
  | MCP Integration | 24 | JSON-RPC protocol, all 5 tools, fetch_and_index, errors |
399
397
 
400
398
  ## Development
@@ -406,7 +404,7 @@ npm install
406
404
  npm run build
407
405
  npm test # executor (55 tests)
408
406
  npm run test:store # FTS5/BM25 (40 tests)
409
- npm run test:all # all suites (99+ tests)
407
+ npm run test:all # all suites (100+ tests)
410
408
  ```
411
409
 
412
410
  ## License
package/build/executor.js CHANGED
@@ -232,23 +232,23 @@ export class PolyglotExecutor {
232
232
  switch (language) {
233
233
  case "javascript":
234
234
  case "typescript":
235
- return `const FILE_CONTENT = require("fs").readFileSync(${escaped}, "utf-8");\n${code}`;
235
+ return `const FILE_CONTENT_PATH = ${escaped};\nconst FILE_CONTENT = require("fs").readFileSync(FILE_CONTENT_PATH, "utf-8");\n${code}`;
236
236
  case "python":
237
- return `with open(${escaped}, "r") as _f:\n FILE_CONTENT = _f.read()\n${code}`;
237
+ return `FILE_CONTENT_PATH = ${escaped}\nwith open(FILE_CONTENT_PATH, "r") as _f:\n FILE_CONTENT = _f.read()\n${code}`;
238
238
  case "shell":
239
- return `FILE_CONTENT=$(cat ${escaped})\n${code}`;
239
+ return `FILE_CONTENT_PATH=${escaped}\nFILE_CONTENT=$(cat ${escaped})\n${code}`;
240
240
  case "ruby":
241
- return `FILE_CONTENT = File.read(${escaped})\n${code}`;
241
+ return `FILE_CONTENT_PATH = ${escaped}\nFILE_CONTENT = File.read(FILE_CONTENT_PATH)\n${code}`;
242
242
  case "go":
243
- return `package main\n\nimport (\n\t"fmt"\n\t"os"\n)\n\nfunc main() {\n\tb, _ := os.ReadFile(${escaped})\n\tFILE_CONTENT := string(b)\n\t_ = FILE_CONTENT\n${code}\n}\n`;
243
+ return `package main\n\nimport (\n\t"fmt"\n\t"os"\n)\n\nvar FILE_CONTENT_PATH = ${escaped}\n\nfunc main() {\n\tb, _ := os.ReadFile(FILE_CONTENT_PATH)\n\tFILE_CONTENT := string(b)\n\t_ = FILE_CONTENT\n\t_ = fmt.Sprint()\n${code}\n}\n`;
244
244
  case "rust":
245
- return `use std::fs;\n\nfn main() {\n let file_content = fs::read_to_string(${escaped}).unwrap();\n${code}\n}\n`;
245
+ return `use std::fs;\n\nfn main() {\n let file_content_path = ${escaped};\n let file_content = fs::read_to_string(file_content_path).unwrap();\n${code}\n}\n`;
246
246
  case "php":
247
- return `<?php\n$FILE_CONTENT = file_get_contents(${escaped});\n${code}`;
247
+ return `<?php\n$FILE_CONTENT_PATH = ${escaped};\n$FILE_CONTENT = file_get_contents($FILE_CONTENT_PATH);\n${code}`;
248
248
  case "perl":
249
- return `open(my $fh, '<', ${escaped}) or die "Cannot open: $!";\nmy $FILE_CONTENT = do { local $/; <$fh> };\nclose($fh);\n${code}`;
249
+ return `my $FILE_CONTENT_PATH = ${escaped};\nopen(my $fh, '<', $FILE_CONTENT_PATH) or die "Cannot open: $!";\nmy $FILE_CONTENT = do { local $/; <$fh> };\nclose($fh);\n${code}`;
250
250
  case "r":
251
- return `FILE_CONTENT <- readLines(${escaped}, warn=FALSE)\nFILE_CONTENT <- paste(FILE_CONTENT, collapse="\\n")\n${code}`;
251
+ return `FILE_CONTENT_PATH <- ${escaped}\nFILE_CONTENT <- readLines(FILE_CONTENT_PATH, warn=FALSE)\nFILE_CONTENT <- paste(FILE_CONTENT, collapse="\\n")\n${code}`;
252
252
  }
253
253
  }
254
254
  }
package/build/server.js CHANGED
@@ -5,11 +5,12 @@ import { z } from "zod";
5
5
  import { PolyglotExecutor } from "./executor.js";
6
6
  import { ContentStore } from "./store.js";
7
7
  import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime, } from "./runtime.js";
8
+ const VERSION = "0.5.3";
8
9
  const runtimes = detectRuntimes();
9
10
  const available = getAvailableLanguages(runtimes);
10
11
  const server = new McpServer({
11
12
  name: "context-mode",
12
- version: "0.5.0",
13
+ version: VERSION,
13
14
  });
14
15
  const executor = new PolyglotExecutor({ runtimes });
15
16
  // Lazy singleton — no DB overhead unless index/search is used
@@ -57,8 +58,9 @@ server.registerTool("execute", {
57
58
  .string()
58
59
  .optional()
59
60
  .describe("What you're looking for in the output. When provided and output is large (>5KB), " +
60
- "returns only matching sections via BM25 search instead of truncated output. " +
61
- "Example: 'find failing tests', 'HTTP 500 errors', 'memory usage statistics'."),
61
+ "indexes output into knowledge base and returns section titles + previews — not full content. " +
62
+ "Use search() to retrieve specific sections. Example: 'failing tests', 'HTTP 500 errors'." +
63
+ "\n\nTIP: Use specific technical terms, not just concepts. Check 'Searchable terms' in the response for available vocabulary."),
62
64
  }),
63
65
  }, async ({ language, code, timeout, intent }) => {
64
66
  try {
@@ -79,7 +81,7 @@ server.registerTool("execute", {
79
81
  if (intent && intent.trim().length > 0 && Buffer.byteLength(output) > INTENT_SEARCH_THRESHOLD) {
80
82
  return {
81
83
  content: [
82
- { type: "text", text: intentSearch(output, intent) },
84
+ { type: "text", text: intentSearch(output, intent, `execute:${language}:error`) },
83
85
  ],
84
86
  isError: true,
85
87
  };
@@ -96,7 +98,7 @@ server.registerTool("execute", {
96
98
  if (intent && intent.trim().length > 0 && Buffer.byteLength(stdout) > INTENT_SEARCH_THRESHOLD) {
97
99
  return {
98
100
  content: [
99
- { type: "text", text: intentSearch(stdout, intent) },
101
+ { type: "text", text: intentSearch(stdout, intent, `execute:${language}`) },
100
102
  ],
101
103
  };
102
104
  }
@@ -135,30 +137,79 @@ function indexStdout(stdout, source) {
135
137
  // Helper: intent-driven search on execution output
136
138
  // ─────────────────────────────────────────────────────────
137
139
  const INTENT_SEARCH_THRESHOLD = 5_000; // bytes — ~80-100 lines
138
- function intentSearch(stdout, intent, maxResults = 5) {
139
- const store = new ContentStore(":memory:");
140
+ function intentSearch(stdout, intent, source, maxResults = 5) {
141
+ const totalLines = stdout.split("\n").length;
142
+ const totalBytes = Buffer.byteLength(stdout);
143
+ // Index into the PERSISTENT store so user can search() later
144
+ const persistent = getStore();
145
+ const indexed = persistent.indexPlainText(stdout, source);
146
+ // Search with an ephemeral store to find matching section titles
147
+ const ephemeral = new ContentStore(":memory:");
140
148
  try {
141
- const totalLines = stdout.split("\n").length;
142
- const totalBytes = Buffer.byteLength(stdout);
143
- store.indexPlainText(stdout, "exec-output");
144
- const results = store.search(intent, maxResults);
149
+ ephemeral.indexPlainText(stdout, source);
150
+ let results = ephemeral.search(intent, maxResults);
151
+ // Score-based relaxed search: search ALL words, rank by match count
145
152
  if (results.length === 0) {
146
- return (`[Intent search: no matches for "${intent}" in ${totalLines}-line output. Returning full output.]\n\n` +
147
- stdout);
153
+ const words = intent.trim().split(/\s+/).filter(w => w.length > 2).slice(0, 20);
154
+ if (words.length > 0) {
155
+ const sectionScores = new Map();
156
+ for (const word of words) {
157
+ const wordResults = ephemeral.search(word, 10);
158
+ for (const r of wordResults) {
159
+ const existing = sectionScores.get(r.title);
160
+ if (existing) {
161
+ existing.score += 1;
162
+ if (r.rank < existing.bestRank) {
163
+ existing.bestRank = r.rank;
164
+ existing.result = r;
165
+ }
166
+ }
167
+ else {
168
+ sectionScores.set(r.title, { result: r, score: 1, bestRank: r.rank });
169
+ }
170
+ }
171
+ }
172
+ results = Array.from(sectionScores.values())
173
+ .sort((a, b) => b.score - a.score || a.bestRank - b.bestRank)
174
+ .slice(0, maxResults)
175
+ .map(s => s.result);
176
+ }
148
177
  }
149
- const totalChunks = store.getStats().chunks;
150
- const header = `[Intent search: ${results.length} of ${totalChunks} sections matched "${intent}" from ${totalLines}-line output (${(totalBytes / 1024).toFixed(1)}KB)]`;
151
- const formatted = results
152
- .map((r, i) => {
153
- const matchLabel = i === 0 ? " (best match)" : "";
154
- return `--- ${r.title}${matchLabel} ---\n${r.content}`;
155
- })
156
- .join("\n\n");
157
- const footer = `[Full output: ${totalLines} lines / ${(totalBytes / 1024).toFixed(1)}KB. Re-run without intent to see raw output.]`;
158
- return `${header}\n\n${formatted}\n\n${footer}`;
178
+ // Extract distinctive terms as vocabulary hints for the LLM
179
+ const distinctiveTerms = persistent.getDistinctiveTerms(indexed.sourceId);
180
+ if (results.length === 0) {
181
+ const lines = [
182
+ `Indexed ${indexed.totalChunks} sections from "${source}" into knowledge base.`,
183
+ `No sections matched intent "${intent}" in ${totalLines}-line output (${(totalBytes / 1024).toFixed(1)}KB).`,
184
+ ];
185
+ if (distinctiveTerms.length > 0) {
186
+ lines.push("");
187
+ lines.push(`Searchable terms: ${distinctiveTerms.join(", ")}`);
188
+ }
189
+ lines.push("");
190
+ lines.push("Use search() to explore the indexed content.");
191
+ return lines.join("\n");
192
+ }
193
+ // Return ONLY titles + first-line previews — not full content
194
+ const lines = [
195
+ `Indexed ${indexed.totalChunks} sections from "${source}" into knowledge base.`,
196
+ `${results.length} sections matched "${intent}" (${totalLines} lines, ${(totalBytes / 1024).toFixed(1)}KB):`,
197
+ "",
198
+ ];
199
+ for (const r of results) {
200
+ const preview = r.content.split("\n")[0].slice(0, 120);
201
+ lines.push(` - ${r.title}: ${preview}`);
202
+ }
203
+ if (distinctiveTerms.length > 0) {
204
+ lines.push("");
205
+ lines.push(`Searchable terms: ${distinctiveTerms.join(", ")}`);
206
+ }
207
+ lines.push("");
208
+ lines.push("Use search() to retrieve full content of any section.");
209
+ return lines.join("\n");
159
210
  }
160
211
  finally {
161
- store.close();
212
+ ephemeral.close();
162
213
  }
163
214
  }
164
215
  // ─────────────────────────────────────────────────────────
@@ -223,7 +274,7 @@ server.registerTool("execute_file", {
223
274
  if (intent && intent.trim().length > 0 && Buffer.byteLength(output) > INTENT_SEARCH_THRESHOLD) {
224
275
  return {
225
276
  content: [
226
- { type: "text", text: intentSearch(output, intent) },
277
+ { type: "text", text: intentSearch(output, intent, `file:${path}:error`) },
227
278
  ],
228
279
  isError: true,
229
280
  };
@@ -239,7 +290,7 @@ server.registerTool("execute_file", {
239
290
  if (intent && intent.trim().length > 0 && Buffer.byteLength(stdout) > INTENT_SEARCH_THRESHOLD) {
240
291
  return {
241
292
  content: [
242
- { type: "text", text: intentSearch(stdout, intent) },
293
+ { type: "text", text: intentSearch(stdout, intent, `file:${path}`) },
243
294
  ],
244
295
  };
245
296
  }
@@ -337,6 +388,10 @@ server.registerTool("search", {
337
388
  "- Look up API signatures ('Supabase RLS policy syntax')\n" +
338
389
  "- Get configuration details ('Tailwind responsive breakpoints')\n" +
339
390
  "- Find migration steps ('App Router data fetching')\n\n" +
391
+ "SEARCH TIPS:\n" +
392
+ "- Use specific technical terms, not concepts ('__proto__' not 'security')\n" +
393
+ "- Check 'Searchable terms' from execute/execute_file results for available vocabulary\n" +
394
+ "- Combine multiple specific terms for better results\n\n" +
340
395
  "Returns exact content — not summaries. Each result includes heading hierarchy and full section text.",
341
396
  inputSchema: z.object({
342
397
  query: z.string().describe("Natural language search query"),
@@ -514,7 +569,7 @@ server.registerTool("fetch_and_index", {
514
569
  async function main() {
515
570
  const transport = new StdioServerTransport();
516
571
  await server.connect(transport);
517
- console.error("Context Mode MCP server v0.4.0 running on stdio");
572
+ console.error(`Context Mode MCP server v${VERSION} running on stdio`);
518
573
  console.error(`Detected runtimes:\n${getRuntimeSummary(runtimes)}`);
519
574
  if (!hasBunRuntime()) {
520
575
  console.error("\nPerformance tip: Install Bun for 3-5x faster JS/TS execution");
package/build/store.d.ts CHANGED
@@ -40,6 +40,7 @@ export declare class ContentStore {
40
40
  */
41
41
  indexPlainText(content: string, source: string, linesPerChunk?: number): IndexResult;
42
42
  search(query: string, limit?: number): SearchResult[];
43
+ getDistinctiveTerms(sourceId: number, maxTerms?: number): string[];
43
44
  getStats(): StoreStats;
44
45
  close(): void;
45
46
  }
package/build/store.js CHANGED
@@ -12,6 +12,24 @@ import { readFileSync } from "node:fs";
12
12
  import { tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  // ─────────────────────────────────────────────────────────
15
+ // Constants
16
+ // ─────────────────────────────────────────────────────────
17
+ const STOPWORDS = new Set([
18
+ "the", "and", "for", "are", "but", "not", "you", "all", "can", "had",
19
+ "her", "was", "one", "our", "out", "has", "his", "how", "its", "may",
20
+ "new", "now", "old", "see", "way", "who", "did", "get", "got", "let",
21
+ "say", "she", "too", "use", "will", "with", "this", "that", "from",
22
+ "they", "been", "have", "many", "some", "them", "than", "each", "make",
23
+ "like", "just", "over", "such", "take", "into", "year", "your", "good",
24
+ "could", "would", "about", "which", "their", "there", "other", "after",
25
+ "should", "through", "also", "more", "most", "only", "very", "when",
26
+ "what", "then", "these", "those", "being", "does", "done", "both",
27
+ "same", "still", "while", "where", "here", "were", "much",
28
+ // Common in code/changelogs
29
+ "update", "updates", "updated", "deps", "dev", "tests", "test",
30
+ "add", "added", "fix", "fixed", "run", "running", "using",
31
+ ]);
32
+ // ─────────────────────────────────────────────────────────
15
33
  // Helpers
16
34
  // ─────────────────────────────────────────────────────────
17
35
  function sanitizeQuery(query) {
@@ -155,6 +173,46 @@ export class ContentStore {
155
173
  contentType: r.content_type,
156
174
  }));
157
175
  }
176
+ // ── Vocabulary ──
177
+ getDistinctiveTerms(sourceId, maxTerms = 40) {
178
+ const stats = this.#db
179
+ .prepare("SELECT chunk_count FROM sources WHERE id = ?")
180
+ .get(sourceId);
181
+ if (!stats || stats.chunk_count < 3)
182
+ return [];
183
+ const totalChunks = stats.chunk_count;
184
+ const minAppearances = 2;
185
+ const maxAppearances = Math.max(3, Math.ceil(totalChunks * 0.4));
186
+ const rows = this.#db
187
+ .prepare("SELECT content FROM chunks WHERE source_id = ?")
188
+ .all(sourceId);
189
+ // Count document frequency (how many sections contain each word)
190
+ const docFreq = new Map();
191
+ for (const row of rows) {
192
+ const words = new Set(row.content
193
+ .toLowerCase()
194
+ .split(/[^\p{L}\p{N}_-]+/u)
195
+ .filter((w) => w.length >= 3 && !STOPWORDS.has(w)));
196
+ for (const word of words) {
197
+ docFreq.set(word, (docFreq.get(word) ?? 0) + 1);
198
+ }
199
+ }
200
+ const filtered = Array.from(docFreq.entries())
201
+ .filter(([, count]) => count >= minAppearances && count <= maxAppearances);
202
+ // Score: IDF (rarity) + length bonus + identifier bonus (underscore/camelCase)
203
+ const scored = filtered.map(([word, count]) => {
204
+ const idf = Math.log(totalChunks / count);
205
+ const lenBonus = Math.min(word.length / 20, 0.5);
206
+ const hasSpecialChars = /[_]/.test(word);
207
+ const isCamelOrLong = word.length >= 12;
208
+ const identifierBonus = hasSpecialChars ? 1.5 : isCamelOrLong ? 0.8 : 0;
209
+ return { word, score: idf + lenBonus + identifierBonus };
210
+ });
211
+ return scored
212
+ .sort((a, b) => b.score - a.score)
213
+ .slice(0, maxTerms)
214
+ .map((s) => s.word);
215
+ }
158
216
  // ── Stats ──
159
217
  getStats() {
160
218
  const sources = this.#db.prepare("SELECT COUNT(*) as c FROM sources").get()?.c ?? 0;
@@ -246,10 +304,14 @@ export class ContentStore {
246
304
  sections.length <= 200 &&
247
305
  sections.every((s) => Buffer.byteLength(s) < 5000)) {
248
306
  return sections
249
- .map((section, i) => ({
250
- title: `Section ${i + 1}`,
251
- content: section.trim(),
252
- }))
307
+ .map((section, i) => {
308
+ const trimmed = section.trim();
309
+ const firstLine = trimmed.split("\n")[0].slice(0, 80);
310
+ return {
311
+ title: firstLine || `Section ${i + 1}`,
312
+ content: trimmed,
313
+ };
314
+ })
253
315
  .filter((s) => s.content.length > 0);
254
316
  }
255
317
  const lines = text.split("\n");
@@ -267,8 +329,9 @@ export class ContentStore {
267
329
  break;
268
330
  const startLine = i + 1;
269
331
  const endLine = Math.min(i + slice.length, lines.length);
332
+ const firstLine = slice[0]?.trim().slice(0, 80);
270
333
  chunks.push({
271
- title: `Lines ${startLine}-${endLine}`,
334
+ title: firstLine || `Lines ${startLine}-${endLine}`,
272
335
  content: slice.join("\n"),
273
336
  });
274
337
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "0.5.0",
3
+ "version": "0.5.3",
4
4
  "type": "module",
5
- "description": "Claude Code MCP plugin that saves 94% of your context window. Sandboxed code execution, FTS5 knowledge base, and smart truncation.",
5
+ "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
6
6
  "author": "Mert Koseoğlu",
7
7
  "license": "MIT",
8
8
  "keywords": [