context-mode 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@
13
13
  "name": "context-mode",
14
14
  "source": "./",
15
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",
16
+ "version": "0.5.2",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
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.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
package/README.md CHANGED
@@ -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.0): When you provide an `intent` parameter and output exceeds 5KB, Context Mode uses BM25 search to return only the relevant sections matching your intent.
79
79
 
80
80
  ```
81
81
  Claude calls: execute({
@@ -83,7 +83,7 @@ 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: only the 3 matching log sections (1.5KB) ← instead of 100KB raw log
87
87
  ```
88
88
 
89
89
  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.
@@ -163,7 +163,7 @@ Use instead of WebFetch or Context7 when you need documentation — index once,
163
163
  │ │ • 10 language runtimes │ │
164
164
  │ │ • Sandboxed subprocess │ │
165
165
  │ │ • Auth passthrough │ │
166
- │ │ • Smart truncation │ │
166
+ │ │ • Intent-driven search │ │
167
167
  │ └────────────────────────┘ │
168
168
  │ │
169
169
  │ ┌────────────────────────┐ │
@@ -213,42 +213,29 @@ ORDER BY rank LIMIT 3;
213
213
 
214
214
  **Lazy singleton:** Database created only when `index` or `search` is first called — zero overhead for sessions that don't use it.
215
215
 
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
- ```
225
-
226
- Line-boundary snapping — never cuts mid-line. Error messages at the bottom are always preserved.
227
-
228
216
  ### Intent-Driven Search (v0.5.0)
229
217
 
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:
218
+ When `execute` or `execute_file` is called with an `intent` parameter and output exceeds 5KB, Context Mode chunks the output and uses BM25 search to return only the relevant sections:
231
219
 
232
220
  ```
233
- Traditional truncation:
234
- stdout (100KB) → head(60%) + tail(40%) → ~100KB in context
235
- Problem: relevant info in the middle is lost
221
+ Without intent:
222
+ stdout (100KB) → full output enters context
236
223
 
237
- Intent-driven search:
224
+ With intent:
238
225
  stdout (100KB) → chunk by lines → in-memory FTS5 → search(intent) → 2-5KB relevant sections
239
226
  Result: only what you need enters context
240
227
  ```
241
228
 
242
229
  Tested across 4 real-world scenarios:
243
230
 
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 |
231
+ | Scenario | Without Intent | With Intent | Size Reduction |
232
+ |---|---|---|---|
233
+ | Server log error (line 347/500) | error lost in output | **found** | 1.5 KB vs 5.0 KB |
234
+ | 3 test failures among 200 tests | only 2/3 visible | **all 3 found** | 2.4 KB vs 5.0 KB |
235
+ | 2 build warnings among 300 lines | both lost in output | **both found** | 2.1 KB vs 5.0 KB |
236
+ | API auth error (line 743/1000) | error lost in output | **found** | 1.2 KB vs 4.9 KB |
250
237
 
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.
238
+ Intent search finds the target every time while using 50-75% fewer bytes.
252
239
 
253
240
  ### HTML to Markdown Conversion
254
241
 
@@ -392,9 +379,9 @@ Just ask naturally — Claude automatically routes through Context Mode when it
392
379
 
393
380
  | Suite | Tests | Coverage |
394
381
  |---|---|---|
395
- | Executor | 55 | 10 languages, sandbox, truncation, concurrency, timeouts |
382
+ | Executor | 55 | 10 languages, sandbox, output handling, concurrency, timeouts |
396
383
  | 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 |
384
+ | Intent Search | 4 | Intent-driven search across 4 real-world scenarios |
398
385
  | MCP Integration | 24 | JSON-RPC protocol, all 5 tools, fetch_and_index, errors |
399
386
 
400
387
  ## Development
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.2";
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,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "description": "Claude Code MCP plugin that saves 94% of your context window. Sandboxed code execution, FTS5 knowledge base, and smart truncation.",
6
6
  "author": "Mert Koseoğlu",