context-mode 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +7 -1
- package/build/executor.d.ts +1 -0
- package/build/executor.js +42 -10
- package/build/runtime.d.ts +2 -1
- package/build/runtime.js +10 -0
- package/build/server.js +37 -71
- package/build/store.js +8 -7
- package/package.json +6 -2
- package/skills/doctor/SKILL.md +1 -1
- package/skills/upgrade/SKILL.md +1 -1
- package/start.sh +3 -2
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
|
-
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in
|
|
16
|
-
"version": "0.
|
|
15
|
+
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
+
"version": "0.8.0",
|
|
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.
|
|
4
|
-
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 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
|
@@ -68,7 +68,7 @@ Code Mode showed that tool definitions can be compressed by 99.9%. Context Mode
|
|
|
68
68
|
|
|
69
69
|
Each `execute` call spawns an isolated subprocess with its own process boundary. Scripts can't access each other's memory or state. The subprocess runs your code, captures stdout, and only that stdout enters the conversation context. The raw data — log files, API responses, snapshots — never leaves the sandbox.
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Eleven language runtimes are available: JavaScript, TypeScript, Python, Shell, Ruby, Go, Rust, PHP, Perl, R, and Elixir. Bun is auto-detected for 3-5x faster JS/TS execution.
|
|
72
72
|
|
|
73
73
|
Authenticated CLIs work through credential passthrough — `gh`, `aws`, `gcloud`, `kubectl`, `docker` inherit environment variables and config paths without exposing them to the conversation.
|
|
74
74
|
|
|
@@ -199,6 +199,12 @@ npm test # run tests
|
|
|
199
199
|
npm run test:all # full suite
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
+
## Contributors
|
|
203
|
+
|
|
204
|
+
<a href="https://github.com/mksglu/claude-context-mode/graphs/contributors">
|
|
205
|
+
<img src="https://contrib.rocks/image?repo=mksglu/claude-context-mode" />
|
|
206
|
+
</a>
|
|
207
|
+
|
|
202
208
|
## License
|
|
203
209
|
|
|
204
210
|
MIT
|
package/build/executor.d.ts
CHANGED
package/build/executor.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
var _a;
|
|
2
2
|
import { spawn, execSync } from "node:child_process";
|
|
3
|
-
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync, existsSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { detectRuntimes, buildCommand, } from "./runtime.js";
|
|
7
7
|
export class PolyglotExecutor {
|
|
8
8
|
#maxOutputBytes;
|
|
9
|
+
#hardCapBytes;
|
|
9
10
|
#projectRoot;
|
|
10
11
|
#runtimes;
|
|
11
12
|
constructor(opts) {
|
|
12
13
|
this.#maxOutputBytes = opts?.maxOutputBytes ?? 102_400;
|
|
14
|
+
this.#hardCapBytes = opts?.hardCapBytes ?? 100 * 1024 * 1024; // 100MB
|
|
13
15
|
this.#projectRoot = opts?.projectRoot ?? process.cwd();
|
|
14
16
|
this.#runtimes = opts?.runtimes ?? detectRuntimes();
|
|
15
17
|
}
|
|
@@ -50,6 +52,7 @@ export class PolyglotExecutor {
|
|
|
50
52
|
php: "php",
|
|
51
53
|
perl: "pl",
|
|
52
54
|
r: "R",
|
|
55
|
+
elixir: "exs",
|
|
53
56
|
};
|
|
54
57
|
// Go needs a main package wrapper if not present
|
|
55
58
|
if (language === "go" && !code.includes("package ")) {
|
|
@@ -59,6 +62,11 @@ export class PolyglotExecutor {
|
|
|
59
62
|
if (language === "php" && !code.trimStart().startsWith("<?")) {
|
|
60
63
|
code = `<?php\n${code}`;
|
|
61
64
|
}
|
|
65
|
+
// Elixir: prepend compiled BEAM paths when inside a Mix project
|
|
66
|
+
if (language === "elixir" && existsSync(join(this.#projectRoot, "mix.exs"))) {
|
|
67
|
+
const escaped = JSON.stringify(join(this.#projectRoot, "_build/dev/lib"));
|
|
68
|
+
code = `Path.wildcard(Path.join(${escaped}, "*/ebin"))\n|> Enum.each(&Code.prepend_path/1)\n\n${code}`;
|
|
69
|
+
}
|
|
62
70
|
const fp = join(tmpDir, `script.${extMap[language]}`);
|
|
63
71
|
if (language === "shell") {
|
|
64
72
|
writeFileSync(fp, code, { encoding: "utf-8", mode: 0o700 });
|
|
@@ -139,22 +147,41 @@ export class PolyglotExecutor {
|
|
|
139
147
|
timedOut = true;
|
|
140
148
|
proc.kill("SIGKILL");
|
|
141
149
|
}, timeout);
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
150
|
+
// Stream-level byte cap: kill the process once combined stdout+stderr
|
|
151
|
+
// exceeds hardCapBytes. Without this, a command like `yes` or
|
|
152
|
+
// `cat /dev/urandom | base64` can accumulate gigabytes in memory
|
|
153
|
+
// before the timeout fires.
|
|
146
154
|
const stdoutChunks = [];
|
|
147
155
|
const stderrChunks = [];
|
|
156
|
+
let totalBytes = 0;
|
|
157
|
+
let capExceeded = false;
|
|
148
158
|
proc.stdout.on("data", (chunk) => {
|
|
149
|
-
|
|
159
|
+
totalBytes += chunk.length;
|
|
160
|
+
if (totalBytes <= this.#hardCapBytes) {
|
|
161
|
+
stdoutChunks.push(chunk);
|
|
162
|
+
}
|
|
163
|
+
else if (!capExceeded) {
|
|
164
|
+
capExceeded = true;
|
|
165
|
+
proc.kill("SIGKILL");
|
|
166
|
+
}
|
|
150
167
|
});
|
|
151
168
|
proc.stderr.on("data", (chunk) => {
|
|
152
|
-
|
|
169
|
+
totalBytes += chunk.length;
|
|
170
|
+
if (totalBytes <= this.#hardCapBytes) {
|
|
171
|
+
stderrChunks.push(chunk);
|
|
172
|
+
}
|
|
173
|
+
else if (!capExceeded) {
|
|
174
|
+
capExceeded = true;
|
|
175
|
+
proc.kill("SIGKILL");
|
|
176
|
+
}
|
|
153
177
|
});
|
|
154
178
|
proc.on("close", (exitCode) => {
|
|
155
179
|
clearTimeout(timer);
|
|
156
180
|
const rawStdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
157
|
-
|
|
181
|
+
let rawStderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
182
|
+
if (capExceeded) {
|
|
183
|
+
rawStderr += `\n[output capped at ${(this.#hardCapBytes / 1024 / 1024).toFixed(0)}MB — process killed]`;
|
|
184
|
+
}
|
|
158
185
|
const max = this.#maxOutputBytes;
|
|
159
186
|
const stdout = _a.#smartTruncate(rawStdout, max);
|
|
160
187
|
const stderr = _a.#smartTruncate(rawStderr, max);
|
|
@@ -235,8 +262,11 @@ export class PolyglotExecutor {
|
|
|
235
262
|
return `const FILE_CONTENT_PATH = ${escaped};\nconst FILE_CONTENT = require("fs").readFileSync(FILE_CONTENT_PATH, "utf-8");\n${code}`;
|
|
236
263
|
case "python":
|
|
237
264
|
return `FILE_CONTENT_PATH = ${escaped}\nwith open(FILE_CONTENT_PATH, "r") as _f:\n FILE_CONTENT = _f.read()\n${code}`;
|
|
238
|
-
case "shell":
|
|
239
|
-
|
|
265
|
+
case "shell": {
|
|
266
|
+
// Single-quote the path to prevent $, backtick, and ! expansion
|
|
267
|
+
const sq = "'" + absolutePath.replace(/'/g, "'\\''") + "'";
|
|
268
|
+
return `FILE_CONTENT_PATH=${sq}\nFILE_CONTENT=$(cat ${sq})\n${code}`;
|
|
269
|
+
}
|
|
240
270
|
case "ruby":
|
|
241
271
|
return `FILE_CONTENT_PATH = ${escaped}\nFILE_CONTENT = File.read(FILE_CONTENT_PATH)\n${code}`;
|
|
242
272
|
case "go":
|
|
@@ -249,6 +279,8 @@ export class PolyglotExecutor {
|
|
|
249
279
|
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
280
|
case "r":
|
|
251
281
|
return `FILE_CONTENT_PATH <- ${escaped}\nFILE_CONTENT <- readLines(FILE_CONTENT_PATH, warn=FALSE)\nFILE_CONTENT <- paste(FILE_CONTENT, collapse="\\n")\n${code}`;
|
|
282
|
+
case "elixir":
|
|
283
|
+
return `file_content_path = ${escaped}\nfile_content = File.read!(file_content_path)\n${code}`;
|
|
252
284
|
}
|
|
253
285
|
}
|
|
254
286
|
}
|
package/build/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type Language = "javascript" | "typescript" | "python" | "shell" | "ruby" | "go" | "rust" | "php" | "perl" | "r";
|
|
1
|
+
export type Language = "javascript" | "typescript" | "python" | "shell" | "ruby" | "go" | "rust" | "php" | "perl" | "r" | "elixir";
|
|
2
2
|
export interface RuntimeInfo {
|
|
3
3
|
command: string;
|
|
4
4
|
available: boolean;
|
|
@@ -16,6 +16,7 @@ export interface RuntimeMap {
|
|
|
16
16
|
php: string | null;
|
|
17
17
|
perl: string | null;
|
|
18
18
|
r: string | null;
|
|
19
|
+
elixir: string | null;
|
|
19
20
|
}
|
|
20
21
|
export declare function detectRuntimes(): RuntimeMap;
|
|
21
22
|
export declare function hasBunRuntime(): boolean;
|
package/build/runtime.js
CHANGED
|
@@ -48,6 +48,7 @@ export function detectRuntimes() {
|
|
|
48
48
|
: commandExists("r")
|
|
49
49
|
? "r"
|
|
50
50
|
: null,
|
|
51
|
+
elixir: commandExists("elixir") ? "elixir" : null,
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
export function hasBunRuntime() {
|
|
@@ -83,6 +84,8 @@ export function getRuntimeSummary(runtimes) {
|
|
|
83
84
|
lines.push(` Perl: ${runtimes.perl} (${getVersion(runtimes.perl)})`);
|
|
84
85
|
if (runtimes.r)
|
|
85
86
|
lines.push(` R: ${runtimes.r} (${getVersion(runtimes.r)})`);
|
|
87
|
+
if (runtimes.elixir)
|
|
88
|
+
lines.push(` Elixir: ${runtimes.elixir} (${getVersion(runtimes.elixir)})`);
|
|
86
89
|
if (!bunPreferred) {
|
|
87
90
|
lines.push("");
|
|
88
91
|
lines.push(" Tip: Install Bun for 3-5x faster JS/TS execution → https://bun.sh");
|
|
@@ -107,6 +110,8 @@ export function getAvailableLanguages(runtimes) {
|
|
|
107
110
|
langs.push("perl");
|
|
108
111
|
if (runtimes.r)
|
|
109
112
|
langs.push("r");
|
|
113
|
+
if (runtimes.elixir)
|
|
114
|
+
langs.push("elixir");
|
|
110
115
|
return langs;
|
|
111
116
|
}
|
|
112
117
|
export function buildCommand(runtimes, language, filePath) {
|
|
@@ -163,5 +168,10 @@ export function buildCommand(runtimes, language, filePath) {
|
|
|
163
168
|
throw new Error("R not available. Install R / Rscript.");
|
|
164
169
|
}
|
|
165
170
|
return [runtimes.r, filePath];
|
|
171
|
+
case "elixir":
|
|
172
|
+
if (!runtimes.elixir) {
|
|
173
|
+
throw new Error("Elixir not available. Install elixir.");
|
|
174
|
+
}
|
|
175
|
+
return ["elixir", filePath];
|
|
166
176
|
}
|
|
167
177
|
}
|
package/build/server.js
CHANGED
|
@@ -5,14 +5,17 @@ import { z } from "zod";
|
|
|
5
5
|
import { PolyglotExecutor } from "./executor.js";
|
|
6
6
|
import { ContentStore, cleanupStaleDBs } from "./store.js";
|
|
7
7
|
import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime, } from "./runtime.js";
|
|
8
|
-
const VERSION = "0.
|
|
8
|
+
const VERSION = "0.8.0";
|
|
9
9
|
const runtimes = detectRuntimes();
|
|
10
10
|
const available = getAvailableLanguages(runtimes);
|
|
11
11
|
const server = new McpServer({
|
|
12
12
|
name: "context-mode",
|
|
13
13
|
version: VERSION,
|
|
14
14
|
});
|
|
15
|
-
const executor = new PolyglotExecutor({
|
|
15
|
+
const executor = new PolyglotExecutor({
|
|
16
|
+
runtimes,
|
|
17
|
+
projectRoot: process.env.CLAUDE_PROJECT_DIR,
|
|
18
|
+
});
|
|
16
19
|
// Lazy singleton — no DB overhead unless index/search is used
|
|
17
20
|
let _store = null;
|
|
18
21
|
function getStore() {
|
|
@@ -115,11 +118,12 @@ server.registerTool("execute", {
|
|
|
115
118
|
"php",
|
|
116
119
|
"perl",
|
|
117
120
|
"r",
|
|
121
|
+
"elixir",
|
|
118
122
|
])
|
|
119
123
|
.describe("Runtime language"),
|
|
120
124
|
code: z
|
|
121
125
|
.string()
|
|
122
|
-
.describe("Source code to execute. Use console.log (JS/TS), print (Python/Ruby/Perl/R), echo (Shell), echo (PHP),
|
|
126
|
+
.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."),
|
|
123
127
|
timeout: z
|
|
124
128
|
.number()
|
|
125
129
|
.optional()
|
|
@@ -239,74 +243,40 @@ function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
|
239
243
|
// Index into the PERSISTENT store so user can search() later
|
|
240
244
|
const persistent = getStore();
|
|
241
245
|
const indexed = persistent.indexPlainText(stdout, source);
|
|
242
|
-
// Search
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// Score-based relaxed search: search ALL words, rank by match count
|
|
248
|
-
if (results.length === 0) {
|
|
249
|
-
const words = intent.trim().split(/\s+/).filter(w => w.length > 2).slice(0, 20);
|
|
250
|
-
if (words.length > 0) {
|
|
251
|
-
const sectionScores = new Map();
|
|
252
|
-
for (const word of words) {
|
|
253
|
-
const wordResults = ephemeral.search(word, 10);
|
|
254
|
-
for (const r of wordResults) {
|
|
255
|
-
const existing = sectionScores.get(r.title);
|
|
256
|
-
if (existing) {
|
|
257
|
-
existing.score += 1;
|
|
258
|
-
if (r.rank < existing.bestRank) {
|
|
259
|
-
existing.bestRank = r.rank;
|
|
260
|
-
existing.result = r;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
sectionScores.set(r.title, { result: r, score: 1, bestRank: r.rank });
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
results = Array.from(sectionScores.values())
|
|
269
|
-
.sort((a, b) => b.score - a.score || a.bestRank - b.bestRank)
|
|
270
|
-
.slice(0, maxResults)
|
|
271
|
-
.map(s => s.result);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
// Extract distinctive terms as vocabulary hints for the LLM
|
|
275
|
-
const distinctiveTerms = persistent.getDistinctiveTerms(indexed.sourceId);
|
|
276
|
-
if (results.length === 0) {
|
|
277
|
-
const lines = [
|
|
278
|
-
`Indexed ${indexed.totalChunks} sections from "${source}" into knowledge base.`,
|
|
279
|
-
`No sections matched intent "${intent}" in ${totalLines}-line output (${(totalBytes / 1024).toFixed(1)}KB).`,
|
|
280
|
-
];
|
|
281
|
-
if (distinctiveTerms.length > 0) {
|
|
282
|
-
lines.push("");
|
|
283
|
-
lines.push(`Searchable terms: ${distinctiveTerms.join(", ")}`);
|
|
284
|
-
}
|
|
285
|
-
lines.push("");
|
|
286
|
-
lines.push("Use search() to explore the indexed content.");
|
|
287
|
-
return lines.join("\n");
|
|
288
|
-
}
|
|
289
|
-
// Return ONLY titles + first-line previews — not full content
|
|
246
|
+
// Search the persistent store directly (porter → trigram → fuzzy)
|
|
247
|
+
let results = persistent.searchWithFallback(intent, maxResults, source);
|
|
248
|
+
// Extract distinctive terms as vocabulary hints for the LLM
|
|
249
|
+
const distinctiveTerms = persistent.getDistinctiveTerms(indexed.sourceId);
|
|
250
|
+
if (results.length === 0) {
|
|
290
251
|
const lines = [
|
|
291
252
|
`Indexed ${indexed.totalChunks} sections from "${source}" into knowledge base.`,
|
|
292
|
-
|
|
293
|
-
"",
|
|
253
|
+
`No sections matched intent "${intent}" in ${totalLines}-line output (${(totalBytes / 1024).toFixed(1)}KB).`,
|
|
294
254
|
];
|
|
295
|
-
for (const r of results) {
|
|
296
|
-
const preview = r.content.split("\n")[0].slice(0, 120);
|
|
297
|
-
lines.push(` - ${r.title}: ${preview}`);
|
|
298
|
-
}
|
|
299
255
|
if (distinctiveTerms.length > 0) {
|
|
300
256
|
lines.push("");
|
|
301
257
|
lines.push(`Searchable terms: ${distinctiveTerms.join(", ")}`);
|
|
302
258
|
}
|
|
303
259
|
lines.push("");
|
|
304
|
-
lines.push("Use search(
|
|
260
|
+
lines.push("Use search() to explore the indexed content.");
|
|
305
261
|
return lines.join("\n");
|
|
306
262
|
}
|
|
307
|
-
|
|
308
|
-
|
|
263
|
+
// Return ONLY titles + first-line previews — not full content
|
|
264
|
+
const lines = [
|
|
265
|
+
`Indexed ${indexed.totalChunks} sections from "${source}" into knowledge base.`,
|
|
266
|
+
`${results.length} sections matched "${intent}" (${totalLines} lines, ${(totalBytes / 1024).toFixed(1)}KB):`,
|
|
267
|
+
"",
|
|
268
|
+
];
|
|
269
|
+
for (const r of results) {
|
|
270
|
+
const preview = r.content.split("\n")[0].slice(0, 120);
|
|
271
|
+
lines.push(` - ${r.title}: ${preview}`);
|
|
272
|
+
}
|
|
273
|
+
if (distinctiveTerms.length > 0) {
|
|
274
|
+
lines.push("");
|
|
275
|
+
lines.push(`Searchable terms: ${distinctiveTerms.join(", ")}`);
|
|
309
276
|
}
|
|
277
|
+
lines.push("");
|
|
278
|
+
lines.push("Use search(queries: [...]) to retrieve full content of any section.");
|
|
279
|
+
return lines.join("\n");
|
|
310
280
|
}
|
|
311
281
|
// ─────────────────────────────────────────────────────────
|
|
312
282
|
// Tool: execute_file
|
|
@@ -330,11 +300,12 @@ server.registerTool("execute_file", {
|
|
|
330
300
|
"php",
|
|
331
301
|
"perl",
|
|
332
302
|
"r",
|
|
303
|
+
"elixir",
|
|
333
304
|
])
|
|
334
305
|
.describe("Runtime language"),
|
|
335
306
|
code: z
|
|
336
307
|
.string()
|
|
337
|
-
.describe("Code to process FILE_CONTENT. Print summary via console.log/print/echo."),
|
|
308
|
+
.describe("Code to process FILE_CONTENT (file_content in Elixir). Print summary via console.log/print/echo/IO.puts."),
|
|
338
309
|
timeout: z
|
|
339
310
|
.number()
|
|
340
311
|
.optional()
|
|
@@ -561,7 +532,7 @@ server.registerTool("search", {
|
|
|
561
532
|
sections.push(`## ${q}\n(output cap reached)\n`);
|
|
562
533
|
continue;
|
|
563
534
|
}
|
|
564
|
-
const results = store.
|
|
535
|
+
const results = store.searchWithFallback(q, effectiveLimit, source);
|
|
565
536
|
if (results.length === 0) {
|
|
566
537
|
sections.push(`## ${q}\nNo results found.`);
|
|
567
538
|
continue;
|
|
@@ -841,16 +812,11 @@ server.registerTool("batch_execute", {
|
|
|
841
812
|
queryResults.push(`## ${query}\n(output cap reached — use search(queries: ["${query}"]) for details)\n`);
|
|
842
813
|
continue;
|
|
843
814
|
}
|
|
844
|
-
// Tier 1: scoped search (
|
|
845
|
-
let results = store.
|
|
846
|
-
// Tier 2:
|
|
847
|
-
if (results.length === 0 && sectionTitles.length > 0) {
|
|
848
|
-
const boosted = `${query} ${sectionTitles.join(" ")}`;
|
|
849
|
-
results = store.search(boosted, 3, source);
|
|
850
|
-
}
|
|
851
|
-
// Tier 3: global fallback (no source filter)
|
|
815
|
+
// Tier 1: scoped search with fallback (porter → trigram → fuzzy)
|
|
816
|
+
let results = store.searchWithFallback(query, 3, source);
|
|
817
|
+
// Tier 2: global fallback (no source filter)
|
|
852
818
|
if (results.length === 0) {
|
|
853
|
-
results = store.
|
|
819
|
+
results = store.searchWithFallback(query, 3);
|
|
854
820
|
}
|
|
855
821
|
queryResults.push(`## ${query}`);
|
|
856
822
|
queryResults.push("");
|
package/build/store.js
CHANGED
|
@@ -423,12 +423,11 @@ export class ContentStore {
|
|
|
423
423
|
const totalChunks = stats.chunk_count;
|
|
424
424
|
const minAppearances = 2;
|
|
425
425
|
const maxAppearances = Math.max(3, Math.ceil(totalChunks * 0.4));
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
.all(sourceId);
|
|
426
|
+
// Stream chunks one at a time to avoid loading all content into memory
|
|
427
|
+
const stmt = this.#db.prepare("SELECT content FROM chunks WHERE source_id = ?");
|
|
429
428
|
// Count document frequency (how many sections contain each word)
|
|
430
429
|
const docFreq = new Map();
|
|
431
|
-
for (const row of
|
|
430
|
+
for (const row of stmt.iterate(sourceId)) {
|
|
432
431
|
const words = new Set(row.content
|
|
433
432
|
.toLowerCase()
|
|
434
433
|
.split(/[^\p{L}\p{N}_-]+/u)
|
|
@@ -476,9 +475,11 @@ export class ContentStore {
|
|
|
476
475
|
.filter((w) => w.length >= 3 && !STOPWORDS.has(w));
|
|
477
476
|
const unique = [...new Set(words)];
|
|
478
477
|
const insert = this.#db.prepare("INSERT OR IGNORE INTO vocabulary (word) VALUES (?)");
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
478
|
+
this.#db.transaction(() => {
|
|
479
|
+
for (const word of unique) {
|
|
480
|
+
insert.run(word);
|
|
481
|
+
}
|
|
482
|
+
})();
|
|
482
483
|
}
|
|
483
484
|
// ── Chunking ──
|
|
484
485
|
#chunkMarkdown(text) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
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",
|
|
@@ -52,7 +52,11 @@
|
|
|
52
52
|
"test:store": "npx tsx tests/store.test.ts",
|
|
53
53
|
"test:fuzzy": "npx tsx tests/fuzzy-search.test.ts",
|
|
54
54
|
"test:hooks": "npx tsx tests/hook-integration.test.ts",
|
|
55
|
-
"test:
|
|
55
|
+
"test:project-dir": "npx tsx tests/project-dir.test.ts",
|
|
56
|
+
"test:stream-cap": "npx tsx tests/stream-cap.test.ts",
|
|
57
|
+
"test:search-wiring": "npx tsx tests/search-wiring.test.ts",
|
|
58
|
+
"test:search-fallback": "npx tsx tests/search-fallback-integration.test.ts",
|
|
59
|
+
"test:all": "for f in tests/*.test.ts; do npx tsx \"$f\" || exit 1; done"
|
|
56
60
|
},
|
|
57
61
|
"dependencies": {
|
|
58
62
|
"@clack/prompts": "^1.0.1",
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -16,7 +16,7 @@ Run diagnostics and display results directly in the conversation.
|
|
|
16
16
|
1. Derive the **plugin root** from this skill's base directory (go up 2 levels — remove `/skills/doctor`).
|
|
17
17
|
2. Run with Bash:
|
|
18
18
|
```
|
|
19
|
-
|
|
19
|
+
npx tsx "<PLUGIN_ROOT>/src/cli.ts" doctor
|
|
20
20
|
```
|
|
21
21
|
3. **IMPORTANT**: After the Bash tool completes, re-display the key results as markdown text directly in the conversation so the user sees them without expanding the tool output. Format as a checklist:
|
|
22
22
|
```
|
package/skills/upgrade/SKILL.md
CHANGED
|
@@ -16,7 +16,7 @@ Pull latest from GitHub and reinstall the plugin.
|
|
|
16
16
|
1. Derive the **plugin root** from this skill's base directory (go up 2 levels — remove `/skills/upgrade`).
|
|
17
17
|
2. Run with Bash:
|
|
18
18
|
```
|
|
19
|
-
|
|
19
|
+
npx tsx "<PLUGIN_ROOT>/src/cli.ts" upgrade
|
|
20
20
|
```
|
|
21
21
|
3. **IMPORTANT**: After the Bash tool completes, re-display the key results as markdown text directly in the conversation so the user sees them without expanding the tool output. Format as:
|
|
22
22
|
```
|
package/start.sh
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
+
CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
2
3
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
3
4
|
cd "$DIR"
|
|
4
5
|
|
|
5
6
|
# Bundle exists (CI-built) — start instantly, install native module in background
|
|
6
7
|
if [ -f server.bundle.mjs ]; then
|
|
7
8
|
[ -d node_modules/better-sqlite3 ] || npm install better-sqlite3 --no-package-lock --no-save --silent 2>/dev/null &
|
|
8
|
-
exec node server.bundle.mjs
|
|
9
|
+
CLAUDE_PROJECT_DIR="$CLAUDE_PROJECT_DIR" exec node server.bundle.mjs
|
|
9
10
|
fi
|
|
10
11
|
|
|
11
12
|
# Fallback: no bundle (dev or npm install) — full build
|
|
12
13
|
[ -d node_modules ] || npm install --silent 2>/dev/null
|
|
13
14
|
[ -f build/server.js ] || npx tsc --silent 2>/dev/null
|
|
14
|
-
exec node build/server.js
|
|
15
|
+
CLAUDE_PROJECT_DIR="$CLAUDE_PROJECT_DIR" exec node build/server.js
|