context-mode 0.9.16 → 0.9.17

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 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
16
- "version": "0.9.16",
16
+ "version": "0.9.17",
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.9.16",
3
+ "version": "0.9.17",
4
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",
package/README.md CHANGED
@@ -190,7 +190,9 @@ with code examples. Then run /context-mode:stats.
190
190
  - **Claude Code** with MCP support
191
191
  - Optional: Bun (auto-detected, 3-5x faster JS/TS)
192
192
 
193
- ## Development
193
+ ## Contributing
194
+
195
+ We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for the full local development workflow, TDD guidelines, and how to test your changes in a live Claude Code session.
194
196
 
195
197
  ```bash
196
198
  git clone https://github.com/mksglu/claude-context-mode.git
package/build/executor.js CHANGED
@@ -36,7 +36,14 @@ export class PolyglotExecutor {
36
36
  const tmpDir = mkdtempSync(join(tmpdir(), "ctx-mode-"));
37
37
  try {
38
38
  const filePath = this.#writeScript(tmpDir, code, language);
39
- const cmd = buildCommand(this.#runtimes, language, filePath);
39
+ let cmd;
40
+ try {
41
+ cmd = buildCommand(this.#runtimes, language, filePath);
42
+ }
43
+ catch (err) {
44
+ const msg = err instanceof Error ? err.message : String(err);
45
+ return { exitCode: 1, stdout: "", stderr: msg, timedOut: false };
46
+ }
40
47
  // Rust: compile then run
41
48
  if (cmd[0] === "__rust_compile_run__") {
42
49
  return await this.#compileAndRun(filePath, tmpDir, timeout);
@@ -44,7 +51,13 @@ export class PolyglotExecutor {
44
51
  return await this.#spawn(cmd, tmpDir, timeout);
45
52
  }
46
53
  finally {
47
- rmSync(tmpDir, { recursive: true, force: true });
54
+ try {
55
+ rmSync(tmpDir, { recursive: true, force: true });
56
+ }
57
+ catch {
58
+ // On Windows, EBUSY/EPERM is common due to delayed handle release
59
+ // after child process exit. Silently ignore — OS cleans temp dirs.
60
+ }
48
61
  }
49
62
  }
50
63
  async executeFile(opts) {
@@ -9,7 +9,7 @@ export interface RuntimeMap {
9
9
  javascript: string;
10
10
  typescript: string | null;
11
11
  python: string | null;
12
- shell: string;
12
+ shell: string | null;
13
13
  ruby: string | null;
14
14
  go: string | null;
15
15
  rust: string | null;
package/build/runtime.js CHANGED
@@ -40,7 +40,15 @@ export function detectRuntimes() {
40
40
  : commandExists("python")
41
41
  ? "python"
42
42
  : null,
43
- shell: commandExists("bash") ? "bash" : "sh",
43
+ shell: commandExists("bash")
44
+ ? "bash"
45
+ : commandExists("sh")
46
+ ? "sh"
47
+ : commandExists("powershell")
48
+ ? "powershell"
49
+ : commandExists("cmd.exe")
50
+ ? "cmd.exe"
51
+ : null,
44
52
  ruby: commandExists("ruby") ? "ruby" : null,
45
53
  go: commandExists("go") ? "go" : null,
46
54
  rust: commandExists("rustc") ? "rustc" : null,
@@ -73,7 +81,12 @@ export function getRuntimeSummary(runtimes) {
73
81
  else {
74
82
  lines.push(` Python: not available`);
75
83
  }
76
- lines.push(` Shell: ${runtimes.shell} (${getVersion(runtimes.shell)})`);
84
+ if (runtimes.shell) {
85
+ lines.push(` Shell: ${runtimes.shell} (${getVersion(runtimes.shell)})`);
86
+ }
87
+ else {
88
+ lines.push(` Shell: not available`);
89
+ }
77
90
  // Optional runtimes — only show if available
78
91
  if (runtimes.ruby)
79
92
  lines.push(` Ruby: ${runtimes.ruby} (${getVersion(runtimes.ruby)})`);
@@ -96,7 +109,9 @@ export function getRuntimeSummary(runtimes) {
96
109
  return lines.join("\n");
97
110
  }
98
111
  export function getAvailableLanguages(runtimes) {
99
- const langs = ["javascript", "shell"];
112
+ const langs = ["javascript"];
113
+ if (runtimes.shell)
114
+ langs.push("shell");
100
115
  if (runtimes.typescript)
101
116
  langs.push("typescript");
102
117
  if (runtimes.python)
@@ -138,6 +153,9 @@ export function buildCommand(runtimes, language, filePath) {
138
153
  }
139
154
  return [runtimes.python, filePath];
140
155
  case "shell":
156
+ if (!runtimes.shell) {
157
+ throw new Error("No shell runtime available. Install bash, sh, powershell, or cmd.");
158
+ }
141
159
  return [runtimes.shell, filePath];
142
160
  case "ruby":
143
161
  if (!runtimes.ruby) {
package/build/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { createRequire } from "node:module";
4
5
  import { z } from "zod";
5
6
  import { PolyglotExecutor } from "./executor.js";
6
7
  import { ContentStore, cleanupStaleDBs } from "./store.js";
@@ -628,75 +629,48 @@ server.registerTool("search", {
628
629
  }
629
630
  });
630
631
  // ─────────────────────────────────────────────────────────
632
+ // Turndown path resolution (external dep, like better-sqlite3)
633
+ // ─────────────────────────────────────────────────────────
634
+ let _turndownPath = null;
635
+ let _gfmPluginPath = null;
636
+ function resolveTurndownPath() {
637
+ if (!_turndownPath) {
638
+ const require = createRequire(import.meta.url);
639
+ _turndownPath = require.resolve("turndown");
640
+ }
641
+ return _turndownPath;
642
+ }
643
+ function resolveGfmPluginPath() {
644
+ if (!_gfmPluginPath) {
645
+ const require = createRequire(import.meta.url);
646
+ _gfmPluginPath = require.resolve("turndown-plugin-gfm");
647
+ }
648
+ return _gfmPluginPath;
649
+ }
650
+ // ─────────────────────────────────────────────────────────
631
651
  // Tool: fetch_and_index
632
652
  // ─────────────────────────────────────────────────────────
633
- const HTML_TO_MARKDOWN_CODE = `
634
- const url = process.argv[1];
635
- if (!url) { console.error("No URL provided"); process.exit(1); }
653
+ function buildFetchCode(url) {
654
+ const turndownPath = JSON.stringify(resolveTurndownPath());
655
+ const gfmPath = JSON.stringify(resolveGfmPluginPath());
656
+ return `
657
+ const TurndownService = require(${turndownPath});
658
+ const { gfm } = require(${gfmPath});
659
+ const url = ${JSON.stringify(url)};
636
660
 
637
661
  async function main() {
638
662
  const resp = await fetch(url);
639
663
  if (!resp.ok) { console.error("HTTP " + resp.status); process.exit(1); }
664
+ const html = await resp.text();
640
665
 
641
- let html = await resp.text();
642
-
643
- // Strip script, style, nav, header, footer tags with content
644
- html = html.replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, "");
645
- html = html.replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, "");
646
- html = html.replace(/<nav[^>]*>[\\s\\S]*?<\\/nav>/gi, "");
647
- html = html.replace(/<header[^>]*>[\\s\\S]*?<\\/header>/gi, "");
648
- html = html.replace(/<footer[^>]*>[\\s\\S]*?<\\/footer>/gi, "");
649
-
650
- // Convert headings to markdown
651
- html = html.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, "\\n# $1\\n");
652
- html = html.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, "\\n## $1\\n");
653
- html = html.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, "\\n### $1\\n");
654
- html = html.replace(/<h4[^>]*>(.*?)<\\/h4>/gi, "\\n#### $1\\n");
655
-
656
- // Convert code blocks
657
- html = html.replace(/<pre[^>]*><code[^>]*class="[^"]*language-(\\w+)"[^>]*>([\\s\\S]*?)<\\/code><\\/pre>/gi,
658
- (_, lang, code) => "\\n\\\`\\\`\\\`" + lang + "\\n" + decodeEntities(code) + "\\n\\\`\\\`\\\`\\n");
659
- html = html.replace(/<pre[^>]*><code[^>]*>([\\s\\S]*?)<\\/code><\\/pre>/gi,
660
- (_, code) => "\\n\\\`\\\`\\\`\\n" + decodeEntities(code) + "\\n\\\`\\\`\\\`\\n");
661
- html = html.replace(/<code[^>]*>([^<]*)<\\/code>/gi, "\\\`$1\\\`");
662
-
663
- // Convert links
664
- html = html.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\\/a>/gi, "[$2]($1)");
665
-
666
- // Convert lists
667
- html = html.replace(/<li[^>]*>(.*?)<\\/li>/gi, "- $1\\n");
668
-
669
- // Convert paragraphs and line breaks
670
- html = html.replace(/<p[^>]*>(.*?)<\\/p>/gi, "\\n$1\\n");
671
- html = html.replace(/<br\\s*\\/?>/gi, "\\n");
672
- html = html.replace(/<hr\\s*\\/?>/gi, "\\n---\\n");
673
-
674
- // Strip remaining HTML tags
675
- html = html.replace(/<[^>]+>/g, "");
676
-
677
- // Decode HTML entities
678
- html = decodeEntities(html);
679
-
680
- // Clean up whitespace
681
- html = html.replace(/\\n{3,}/g, "\\n\\n").trim();
682
-
683
- console.log(html);
684
- }
685
-
686
- function decodeEntities(s) {
687
- return s
688
- .replace(/&amp;/g, "&")
689
- .replace(/&lt;/g, "<")
690
- .replace(/&gt;/g, ">")
691
- .replace(/&quot;/g, '"')
692
- .replace(/&#39;/g, "'")
693
- .replace(/&#x27;/g, "'")
694
- .replace(/&#x2F;/g, "/")
695
- .replace(/&nbsp;/g, " ");
666
+ const td = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
667
+ td.use(gfm);
668
+ td.remove(['script', 'style', 'nav', 'header', 'footer', 'noscript']);
669
+ console.log(td.turndown(html));
696
670
  }
697
-
698
671
  main();
699
672
  `;
673
+ }
700
674
  server.registerTool("fetch_and_index", {
701
675
  title: "Fetch & Index URL",
702
676
  description: "Fetches URL content, converts HTML to markdown, indexes into searchable knowledge base, " +
@@ -712,7 +686,7 @@ server.registerTool("fetch_and_index", {
712
686
  }, async ({ url, source }) => {
713
687
  try {
714
688
  // Execute fetch inside subprocess — raw HTML never enters context
715
- const fetchCode = `process.argv[1] = ${JSON.stringify(url)};\n${HTML_TO_MARKDOWN_CODE}`;
689
+ const fetchCode = buildFetchCode(url);
716
690
  const result = await executor.execute({
717
691
  language: "javascript",
718
692
  code: fetchCode,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "0.9.16",
3
+ "version": "0.9.17",
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",
@@ -38,7 +38,7 @@
38
38
  ],
39
39
  "scripts": {
40
40
  "build": "tsc",
41
- "bundle": "esbuild src/server.ts --bundle --platform=node --target=node18 --format=esm --outfile=server.bundle.mjs --external:better-sqlite3 --minify",
41
+ "bundle": "esbuild src/server.ts --bundle --platform=node --target=node18 --format=esm --outfile=server.bundle.mjs --external:better-sqlite3 --external:turndown --external:turndown-plugin-gfm --external:@mixmark-io/domino --minify",
42
42
  "prepublishOnly": "npm run build",
43
43
  "dev": "npx tsx src/server.ts",
44
44
  "setup": "npx tsx src/cli.ts setup",
@@ -56,18 +56,23 @@
56
56
  "test:stream-cap": "npx tsx tests/stream-cap.test.ts",
57
57
  "test:search-wiring": "npx tsx tests/search-wiring.test.ts",
58
58
  "test:search-fallback": "npx tsx tests/search-fallback-integration.test.ts",
59
+ "test:turndown": "npx tsx tests/turndown.test.ts",
59
60
  "test:all": "for f in tests/*.test.ts; do npx tsx \"$f\" || exit 1; done"
60
61
  },
61
62
  "dependencies": {
62
63
  "@clack/prompts": "^1.0.1",
64
+ "@mixmark-io/domino": "^2.2.0",
63
65
  "@modelcontextprotocol/sdk": "^1.26.0",
64
66
  "better-sqlite3": "^12.6.2",
65
67
  "picocolors": "^1.1.1",
68
+ "turndown": "^7.2.0",
69
+ "turndown-plugin-gfm": "^1.0.2",
66
70
  "zod": "^3.25.0"
67
71
  },
68
72
  "devDependencies": {
69
73
  "@types/better-sqlite3": "^7.6.13",
70
74
  "@types/node": "^22.19.11",
75
+ "@types/turndown": "^5.0.5",
71
76
  "esbuild": "^0.27.3",
72
77
  "tsx": "^4.21.0",
73
78
  "typescript": "^5.7.0"