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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +16 -29
- package/build/executor.js +9 -9
- package/build/server.js +82 -27
- package/build/store.d.ts +1 -0
- package/build/store.js +68 -5
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
│ │ •
|
|
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
|
|
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
|
-
|
|
234
|
-
stdout (100KB) →
|
|
235
|
-
Problem: relevant info in the middle is lost
|
|
221
|
+
Without intent:
|
|
222
|
+
stdout (100KB) → full output enters context
|
|
236
223
|
|
|
237
|
-
|
|
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 |
|
|
245
|
-
|
|
246
|
-
| Server log error (line 347/500) |
|
|
247
|
-
| 3 test failures among 200 tests |
|
|
248
|
-
| 2 build warnings among 300 lines |
|
|
249
|
-
| API auth error (line 743/1000) |
|
|
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
|
-
|
|
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,
|
|
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 |
|
|
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(
|
|
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 `
|
|
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 `
|
|
239
|
+
return `FILE_CONTENT_PATH=${escaped}\nFILE_CONTENT=$(cat ${escaped})\n${code}`;
|
|
240
240
|
case "ruby":
|
|
241
|
-
return `
|
|
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(
|
|
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(
|
|
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($
|
|
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 `
|
|
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 `
|
|
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:
|
|
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
|
-
"
|
|
61
|
-
"Example: '
|
|
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
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
.
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
251
|
-
|
|
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.
|
|
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",
|