context-mode 1.0.75 → 1.0.76

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.
Files changed (46) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.mcp.json +1 -1
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +33 -2
  7. package/build/adapters/claude-code/hooks.d.ts +11 -1
  8. package/build/adapters/claude-code/hooks.js +31 -1
  9. package/build/db-base.d.ts +32 -1
  10. package/build/db-base.js +162 -10
  11. package/build/lifecycle.d.ts +5 -2
  12. package/build/lifecycle.js +4 -11
  13. package/build/server.js +51 -26
  14. package/build/session/db.js +1 -1
  15. package/build/store.js +25 -3
  16. package/cli.bundle.mjs +93 -93
  17. package/configs/antigravity/GEMINI.md +1 -1
  18. package/configs/claude-code/CLAUDE.md +1 -1
  19. package/configs/codex/AGENTS.md +1 -1
  20. package/configs/gemini-cli/GEMINI.md +1 -1
  21. package/configs/kilo/AGENTS.md +1 -1
  22. package/configs/kiro/KIRO.md +1 -1
  23. package/configs/openclaw/AGENTS.md +1 -1
  24. package/configs/opencode/AGENTS.md +1 -1
  25. package/configs/pi/AGENTS.md +1 -1
  26. package/configs/vscode-copilot/copilot-instructions.md +1 -1
  27. package/configs/zed/AGENTS.md +1 -1
  28. package/hooks/codex/posttooluse.mjs +6 -5
  29. package/hooks/codex/sessionstart.mjs +5 -5
  30. package/hooks/core/mcp-ready.mjs +31 -0
  31. package/hooks/core/routing.mjs +27 -12
  32. package/hooks/cursor/posttooluse.mjs +6 -5
  33. package/hooks/cursor/sessionstart.mjs +5 -5
  34. package/hooks/cursor/stop.mjs +5 -4
  35. package/hooks/ensure-deps.mjs +38 -27
  36. package/hooks/gemini-cli/aftertool.mjs +5 -4
  37. package/hooks/gemini-cli/precompress.mjs +5 -4
  38. package/hooks/gemini-cli/sessionstart.mjs +5 -4
  39. package/hooks/hooks.json +13 -22
  40. package/hooks/kiro/posttooluse.mjs +6 -5
  41. package/hooks/routing-block.mjs +6 -2
  42. package/hooks/session-db.bundle.mjs +12 -12
  43. package/openclaw.plugin.json +1 -1
  44. package/package.json +1 -1
  45. package/server.bundle.mjs +73 -73
  46. package/.claude-plugin/hooks/hooks.json +0 -132
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.75"
9
+ "version": "1.0.76"
10
10
  },
11
11
  "plugins": [
12
12
  {
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": "1.0.75",
16
+ "version": "1.0.76",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.75",
3
+ "version": "1.0.76",
4
4
  "description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
package/.mcp.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "context-mode": {
4
4
  "command": "node",
5
- "args": ["${CLAUDE_PLUGIN_ROOT}/start.mjs"]
5
+ "args": ["./start.mjs"]
6
6
  }
7
7
  }
8
8
  }
@@ -3,7 +3,7 @@
3
3
  "name": "Context Mode",
4
4
  "kind": "tool",
5
5
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
6
- "version": "1.0.75",
6
+ "version": "1.0.76",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.75",
3
+ "version": "1.0.76",
4
4
  "description": "OpenClaw 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
@@ -4,6 +4,30 @@
4
4
 
5
5
  [![users](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fmksglu%2Fcontext-mode%40main%2Fstats.json&query=%24.message&label=users&color=brightgreen)](https://www.npmjs.com/package/context-mode) [![npm](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fmksglu%2Fcontext-mode%40main%2Fstats.json&query=%24.npm&label=npm&color=blue)](https://www.npmjs.com/package/context-mode) [![marketplace](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fmksglu%2Fcontext-mode%40main%2Fstats.json&query=%24.marketplace&label=marketplace&color=blue)](https://github.com/mksglu/context-mode) [![GitHub stars](https://img.shields.io/github/stars/mksglu/context-mode?style=flat&color=yellow)](https://github.com/mksglu/context-mode/stargazers) [![GitHub forks](https://img.shields.io/github/forks/mksglu/context-mode?style=flat&color=blue)](https://github.com/mksglu/context-mode/network/members) [![Last commit](https://img.shields.io/github/last-commit/mksglu/context-mode?color=green)](https://github.com/mksglu/context-mode/commits) [![License: ELv2](https://img.shields.io/badge/License-ELv2-blue.svg)](LICENSE)
6
6
  [![Discord](https://img.shields.io/discord/1478479412700909750?label=Discord&logo=discord&color=5865f2)](https://discord.gg/DCN9jUgN5v)
7
+ [![Hacker News #1](https://img.shields.io/badge/Hacker%20News-%231%20%E2%80%A2%20570%2B%20points-ff6600?logo=ycombinator&logoColor=white)](https://news.ycombinator.com/item?id=47193064)
8
+
9
+ <p align="center">
10
+ <sub>Used across teams at</sub>
11
+ <br><br>
12
+ <a href="#"><img src="https://img.shields.io/badge/Microsoft-141414?style=flat" alt="Microsoft" /></a>
13
+ <a href="#"><img src="https://img.shields.io/badge/Google-141414?style=flat&logo=google&logoColor=white" alt="Google" /></a>
14
+ <a href="#"><img src="https://img.shields.io/badge/Meta-141414?style=flat&logo=meta&logoColor=white" alt="Meta" /></a>
15
+ <a href="#"><img src="https://img.shields.io/badge/Amazon-141414?style=flat" alt="Amazon" /></a>
16
+ <a href="#"><img src="https://img.shields.io/badge/IBM-141414?style=flat" alt="IBM" /></a>
17
+ <a href="#"><img src="https://img.shields.io/badge/NVIDIA-141414?style=flat&logo=nvidia&logoColor=white" alt="NVIDIA" /></a>
18
+ <a href="#"><img src="https://img.shields.io/badge/ByteDance-141414?style=flat&logo=bytedance&logoColor=white" alt="ByteDance" /></a>
19
+ <a href="#"><img src="https://img.shields.io/badge/Stripe-141414?style=flat&logo=stripe&logoColor=white" alt="Stripe" /></a>
20
+ <a href="#"><img src="https://img.shields.io/badge/Datadog-141414?style=flat&logo=datadog&logoColor=white" alt="Datadog" /></a>
21
+ <a href="#"><img src="https://img.shields.io/badge/Salesforce-141414?style=flat" alt="Salesforce" /></a>
22
+ <a href="#"><img src="https://img.shields.io/badge/GitHub-141414?style=flat&logo=github&logoColor=white" alt="GitHub" /></a>
23
+ <a href="#"><img src="https://img.shields.io/badge/Red%20Hat-141414?style=flat&logo=redhat&logoColor=white" alt="Red Hat" /></a>
24
+ <a href="#"><img src="https://img.shields.io/badge/Supabase-141414?style=flat&logo=supabase&logoColor=white" alt="Supabase" /></a>
25
+ <a href="#"><img src="https://img.shields.io/badge/Canva-141414?style=flat" alt="Canva" /></a>
26
+ <a href="#"><img src="https://img.shields.io/badge/Notion-141414?style=flat&logo=notion&logoColor=white" alt="Notion" /></a>
27
+ <a href="#"><img src="https://img.shields.io/badge/Hasura-141414?style=flat&logo=hasura&logoColor=white" alt="Hasura" /></a>
28
+ <a href="#"><img src="https://img.shields.io/badge/Framer-141414?style=flat&logo=framer&logoColor=white" alt="Framer" /></a>
29
+ <a href="#"><img src="https://img.shields.io/badge/Cursor-141414?style=flat&logo=cursor&logoColor=white" alt="Cursor" /></a>
30
+ </p>
7
31
 
8
32
  ## The Problem
9
33
 
@@ -16,8 +40,11 @@ Context Mode is an MCP server that solves all three sides of this problem:
16
40
  3. **Think in Code** — The LLM should program the analysis, not compute it. Instead of reading 50 files into context to count functions, the agent writes a script that does the counting and `console.log()`s only the result. One script replaces ten tool calls and saves 100x context. This is a mandatory paradigm across all 12 platforms: stop treating the LLM as a data processor, treat it as a code generator.
17
41
 
18
42
  <a href="https://www.youtube.com/watch?v=QUHrntlfPo4">
19
- <img src="https://img.youtube.com/vi/QUHrntlfPo4/maxresdefault.jpg" alt="context-mode demo" width="100%">
43
+ <picture>
44
+ <img src="https://img.youtube.com/vi/QUHrntlfPo4/maxresdefault.jpg" alt="Watch context-mode demo on YouTube" width="100%">
45
+ </picture>
20
46
  </a>
47
+ <p align="center"><a href="https://www.youtube.com/watch?v=QUHrntlfPo4"><img src="https://img.shields.io/badge/%E2%96%B6%EF%B8%8F_Watch_Demo-YouTube-FF0000?style=for-the-badge&logo=youtube&logoColor=white" alt="Watch on YouTube"></a></p>
21
48
 
22
49
  ## Install
23
50
 
@@ -257,6 +284,8 @@ Full hook config including PreCompact: [`configs/vscode-copilot/hooks.json`](con
257
284
 
258
285
  **Routing:** Hooks enforce routing programmatically via `preToolUse`/`postToolUse`/`stop`. The `.cursor/rules/context-mode.mdc` file provides routing instructions at session start since Cursor's `sessionStart` hook is currently rejected by their validator ([forum report](https://forum.cursor.com/t/unknown-hook-type-sessionstart/149566)). Project `.cursor/hooks.json` overrides `~/.cursor/hooks.json`.
259
286
 
287
+ **Known limitation:** Cursor accepts `additional_context` in hook responses but does not surface it to the model ([forum #155689](https://forum.cursor.com/t/native-posttooluse-hooks-accept-and-log-additional-context-successfully-but-the-injected-context-is-not-surfaced-to-the-model/155689)). Routing relies on the `.mdc` rules file instead of hook context injection.
288
+
260
289
  Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`configs/cursor/mcp.json`](configs/cursor/mcp.json) | [`configs/cursor/context-mode.mdc`](configs/cursor/context-mode.mdc)
261
290
 
262
291
  </details>
@@ -645,6 +674,8 @@ Full configs: [`configs/kiro/mcp.json`](configs/kiro/mcp.json) | [`configs/kiro/
645
674
 
646
675
  Context Mode uses [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) on Node.js, which ships prebuilt native binaries for most platforms. On glibc >= 2.31 systems (Ubuntu 20.04+, Debian 11+, Fedora 34+, macOS, Windows), `npm install` works without any build tools.
647
676
 
677
+ **Linux + Node.js >= 22.13:** Context Mode automatically uses the built-in `node:sqlite` module instead of `better-sqlite3`. This eliminates the native addon entirely, avoiding [sporadic SIGSEGV crashes](https://github.com/nodejs/node/issues/62515) caused by V8's `madvise(MADV_DONTNEED)` corrupting the addon's `.got.plt` section on Linux. No configuration needed — detection is automatic. Falls back to `better-sqlite3` on older Node.js versions.
678
+
648
679
  **Bun users:** No native compilation needed. Context Mode automatically detects Bun and uses the built-in `bun:sqlite` module via a compatibility adapter. `better-sqlite3` and all its build dependencies are skipped entirely.
649
680
 
650
681
  On older glibc systems (CentOS 7/8, RHEL 8, Debian 10), prebuilt binaries don't load and better-sqlite3 **automatically falls back to compiling from source** via `prebuild-install || node-gyp rebuild --release`. This requires a C++20 compiler (GCC 10+), Make, and Python with setuptools.
@@ -703,7 +734,7 @@ When output exceeds 5 KB and an `intent` is provided, Context Mode switches to i
703
734
 
704
735
  ## How the Knowledge Base Works
705
736
 
706
- The `ctx_index` tool chunks markdown content by headings while keeping code blocks intact, then stores them in a **SQLite FTS5** (Full-Text Search 5) virtual table. Search uses **BM25 ranking** — a probabilistic relevance algorithm that scores documents based on term frequency, inverse document frequency, and document length normalization. **Porter stemming** is applied at index time so "running", "runs", and "ran" match the same stem. Titles and headings are weighted **5x** in BM25 scoring for precise navigational queries.
737
+ The `ctx_index` tool chunks markdown content by headings while keeping code blocks intact, then stores them in a **SQLite FTS5** (Full-Text Search 5) virtual table. The SQLite backend is selected automatically at runtime: `bun:sqlite` on Bun, `node:sqlite` on Linux + Node.js >= 22.13, and `better-sqlite3` everywhere else. Search uses **BM25 ranking** — a probabilistic relevance algorithm that scores documents based on term frequency, inverse document frequency, and document length normalization. **Porter stemming** is applied at index time so "running", "runs", and "ran" match the same stem. Titles and headings are weighted **5x** in BM25 scoring for precise navigational queries.
707
738
 
708
739
  When you call `ctx_search`, it returns relevant content snippets focused around matching query terms — not full documents, not approximations, the actual indexed content with smart extraction around what you're looking for. `ctx_fetch_and_index` extends this to URLs: fetch, convert HTML to markdown, chunk, index. The raw page never enters context. Use the `contentType` parameter to filter results by type (e.g. `code` or `prose`).
709
740
 
@@ -25,12 +25,22 @@ export declare const HOOK_TYPES: {
25
25
  };
26
26
  export type HookType = (typeof HOOK_TYPES)[keyof typeof HOOK_TYPES];
27
27
  /** Tools that context-mode's PreToolUse hook intercepts. */
28
- export declare const PRE_TOOL_USE_MATCHERS: readonly ["Bash", "WebFetch", "Read", "Grep", "Agent", "Task", "mcp__plugin_context-mode_context-mode__ctx_execute", "mcp__plugin_context-mode_context-mode__ctx_execute_file", "mcp__plugin_context-mode_context-mode__ctx_batch_execute"];
28
+ export declare const PRE_TOOL_USE_MATCHERS: readonly ["Bash", "WebFetch", "Read", "Grep", "Agent", "mcp__plugin_context-mode_context-mode__ctx_execute", "mcp__plugin_context-mode_context-mode__ctx_execute_file", "mcp__plugin_context-mode_context-mode__ctx_batch_execute"];
29
29
  /**
30
30
  * Combined matcher pattern for settings.json (pipe-separated).
31
31
  * Used by the upgrade command when writing a single consolidated entry.
32
32
  */
33
33
  export declare const PRE_TOOL_USE_MATCHER_PATTERN: string;
34
+ /**
35
+ * Tools that context-mode's PostToolUse hook should fire on.
36
+ * Only tools that extractEvents() actually handles — all others
37
+ * produce zero events and cause false "hook error" display.
38
+ */
39
+ export declare const POST_TOOL_USE_MATCHERS: readonly ["Bash", "Read", "Write", "Edit", "NotebookEdit", "Glob", "Grep", "TodoWrite", "TaskCreate", "TaskUpdate", "EnterPlanMode", "ExitPlanMode", "Skill", "Agent", "AskUserQuestion", "EnterWorktree", "mcp__"];
40
+ /**
41
+ * Combined matcher pattern for PostToolUse in hooks.json / settings.json.
42
+ */
43
+ export declare const POST_TOOL_USE_MATCHER_PATTERN: string;
34
44
  /** Map of hook types to their script file names. */
35
45
  export declare const HOOK_SCRIPTS: Record<HookType, string>;
36
46
  /** Required hooks that must be configured for context-mode to function. */
@@ -36,7 +36,6 @@ export const PRE_TOOL_USE_MATCHERS = [
36
36
  "Read",
37
37
  "Grep",
38
38
  "Agent",
39
- "Task",
40
39
  "mcp__plugin_context-mode_context-mode__ctx_execute",
41
40
  "mcp__plugin_context-mode_context-mode__ctx_execute_file",
42
41
  "mcp__plugin_context-mode_context-mode__ctx_batch_execute",
@@ -47,6 +46,37 @@ export const PRE_TOOL_USE_MATCHERS = [
47
46
  */
48
47
  export const PRE_TOOL_USE_MATCHER_PATTERN = PRE_TOOL_USE_MATCHERS.join("|");
49
48
  // ─────────────────────────────────────────────────────────
49
+ // PostToolUse matchers (#229)
50
+ // ─────────────────────────────────────────────────────────
51
+ /**
52
+ * Tools that context-mode's PostToolUse hook should fire on.
53
+ * Only tools that extractEvents() actually handles — all others
54
+ * produce zero events and cause false "hook error" display.
55
+ */
56
+ export const POST_TOOL_USE_MATCHERS = [
57
+ "Bash",
58
+ "Read",
59
+ "Write",
60
+ "Edit",
61
+ "NotebookEdit",
62
+ "Glob",
63
+ "Grep",
64
+ "TodoWrite",
65
+ "TaskCreate",
66
+ "TaskUpdate",
67
+ "EnterPlanMode",
68
+ "ExitPlanMode",
69
+ "Skill",
70
+ "Agent",
71
+ "AskUserQuestion",
72
+ "EnterWorktree",
73
+ "mcp__",
74
+ ];
75
+ /**
76
+ * Combined matcher pattern for PostToolUse in hooks.json / settings.json.
77
+ */
78
+ export const POST_TOOL_USE_MATCHER_PATTERN = POST_TOOL_USE_MATCHERS.join("|");
79
+ // ─────────────────────────────────────────────────────────
50
80
  // Hook script file names
51
81
  // ─────────────────────────────────────────────────────────
52
82
  /** Map of hook types to their script file names. */
@@ -34,10 +34,25 @@ export declare class BunSQLiteAdapter {
34
34
  transaction(fn: (...args: any[]) => any): any;
35
35
  close(): void;
36
36
  }
37
+ /**
38
+ * Wraps node:sqlite's DatabaseSync to provide better-sqlite3-compatible API.
39
+ * Bridges: .pragma(), .transaction(). Everything else is passthrough.
40
+ * Eliminates native addon SIGSEGV on Linux (nodejs/node#62515).
41
+ */
42
+ export declare class NodeSQLiteAdapter {
43
+ #private;
44
+ constructor(rawDb: any);
45
+ pragma(source: string): any;
46
+ exec(sql: string): any;
47
+ prepare(sql: string): any;
48
+ transaction(fn: (...args: any[]) => any): any;
49
+ close(): void;
50
+ }
37
51
  /**
38
52
  * Lazy-load the SQLite driver for the current runtime.
39
53
  * Bun → bun:sqlite via BunSQLiteAdapter (issue #45).
40
- * Node → better-sqlite3 (native addon).
54
+ * Linux Node → node:sqlite via NodeSQLiteAdapter (issue #228).
55
+ * Other Node → better-sqlite3 (native addon).
41
56
  */
42
57
  export declare function loadDatabase(): typeof DatabaseConstructor;
43
58
  /**
@@ -51,6 +66,12 @@ export declare function loadDatabase(): typeof DatabaseConstructor;
51
66
  * transaction.
52
67
  */
53
68
  export declare function applyWALPragmas(db: DatabaseInstance): void;
69
+ /**
70
+ * Remove orphaned WAL/SHM files when the main DB file doesn't exist.
71
+ * On Windows, stale -wal/-shm files from crashed processes cause
72
+ * "file is not a database" errors when creating a fresh DB.
73
+ */
74
+ export declare function cleanOrphanedWALFiles(dbPath: string): void;
54
75
  /**
55
76
  * Delete all three SQLite files for a given db path (main, WAL, SHM).
56
77
  * Silently ignores individual deletion errors so a partial cleanup
@@ -76,6 +97,16 @@ export declare function defaultDBPath(prefix?: string): string;
76
97
  * Pass custom delays for testing (e.g., [0, 0, 0] to skip waits).
77
98
  */
78
99
  export declare function withRetry<T>(fn: () => T, delays?: number[]): T;
100
+ /**
101
+ * Detect SQLite corruption errors that warrant a rename-and-recreate.
102
+ * Matches SQLITE_CORRUPT, SQLITE_NOTADB, and their human-readable equivalents.
103
+ */
104
+ export declare function isSQLiteCorruptionError(msg: string): boolean;
105
+ /**
106
+ * Rename a corrupt DB and its WAL/SHM files so a fresh DB can be created.
107
+ * Best-effort — individual rename failures are silently ignored.
108
+ */
109
+ export declare function renameCorruptDB(dbPath: string): void;
79
110
  export declare abstract class SQLiteBase {
80
111
  #private;
81
112
  constructor(dbPath: string);
package/build/db-base.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * ContentStore and SessionDB build on top of these primitives.
7
7
  */
8
8
  import { createRequire } from "node:module";
9
- import { unlinkSync } from "node:fs";
9
+ import { existsSync, unlinkSync, renameSync } from "node:fs";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  // ─────────────────────────────────────────────────────────
@@ -84,13 +84,82 @@ export class BunSQLiteAdapter {
84
84
  }
85
85
  }
86
86
  // ─────────────────────────────────────────────────────────
87
+ // node:sqlite adapter (#228)
88
+ // ─────────────────────────────────────────────────────────
89
+ /**
90
+ * Wraps node:sqlite's DatabaseSync to provide better-sqlite3-compatible API.
91
+ * Bridges: .pragma(), .transaction(). Everything else is passthrough.
92
+ * Eliminates native addon SIGSEGV on Linux (nodejs/node#62515).
93
+ */
94
+ export class NodeSQLiteAdapter {
95
+ #raw; // DatabaseSync instance
96
+ constructor(rawDb) {
97
+ this.#raw = rawDb;
98
+ }
99
+ pragma(source) {
100
+ // "journal_mode = WAL" → PRAGMA journal_mode = WAL
101
+ // "table_xinfo(session_events)" → PRAGMA table_xinfo(session_events)
102
+ // "wal_checkpoint(TRUNCATE)" → PRAGMA wal_checkpoint(TRUNCATE)
103
+ const stmt = this.#raw.prepare(`PRAGMA ${source}`);
104
+ const rows = stmt.all();
105
+ if (!rows || rows.length === 0)
106
+ return undefined;
107
+ if (rows.length > 1)
108
+ return rows;
109
+ const values = Object.values(rows[0]);
110
+ return values.length === 1 ? values[0] : rows[0];
111
+ }
112
+ exec(sql) {
113
+ // node:sqlite's exec() supports multi-statement natively
114
+ this.#raw.exec(sql);
115
+ return this;
116
+ }
117
+ prepare(sql) {
118
+ const stmt = this.#raw.prepare(sql);
119
+ return {
120
+ run: (...args) => stmt.run(...args),
121
+ get: (...args) => stmt.get(...args),
122
+ all: (...args) => stmt.all(...args),
123
+ iterate: (...args) => {
124
+ // node:sqlite uses Symbol.iterator on StatementSync, not .iterate()
125
+ // Check if iterate exists, otherwise use Symbol.iterator
126
+ if (typeof stmt.iterate === 'function') {
127
+ return stmt.iterate(...args);
128
+ }
129
+ // Fallback: use all() to create an iterator
130
+ const rows = stmt.all(...args);
131
+ return rows[Symbol.iterator]();
132
+ },
133
+ };
134
+ }
135
+ transaction(fn) {
136
+ // node:sqlite has no transaction() method — manual BEGIN/COMMIT/ROLLBACK
137
+ return (...args) => {
138
+ this.#raw.exec("BEGIN");
139
+ try {
140
+ const result = fn(...args);
141
+ this.#raw.exec("COMMIT");
142
+ return result;
143
+ }
144
+ catch (err) {
145
+ this.#raw.exec("ROLLBACK");
146
+ throw err;
147
+ }
148
+ };
149
+ }
150
+ close() {
151
+ this.#raw.close();
152
+ }
153
+ }
154
+ // ─────────────────────────────────────────────────────────
87
155
  // Lazy loader
88
156
  // ─────────────────────────────────────────────────────────
89
157
  let _Database = null;
90
158
  /**
91
159
  * Lazy-load the SQLite driver for the current runtime.
92
160
  * Bun → bun:sqlite via BunSQLiteAdapter (issue #45).
93
- * Node → better-sqlite3 (native addon).
161
+ * Linux Node → node:sqlite via NodeSQLiteAdapter (issue #228).
162
+ * Other Node → better-sqlite3 (native addon).
94
163
  */
95
164
  export function loadDatabase() {
96
165
  if (!_Database) {
@@ -104,11 +173,34 @@ export function loadDatabase() {
104
173
  readonly: opts?.readonly,
105
174
  create: true,
106
175
  });
107
- return new BunSQLiteAdapter(raw);
176
+ const adapter = new BunSQLiteAdapter(raw);
177
+ // Propagate busy_timeout — better-sqlite3 does this via constructor
178
+ // option but bun:sqlite does not, so we set it via pragma (#243)
179
+ if (opts?.timeout) {
180
+ adapter.pragma(`busy_timeout = ${opts.timeout}`);
181
+ }
182
+ return adapter;
108
183
  };
109
184
  }
185
+ else if (process.platform === "linux") {
186
+ // Linux — try node:sqlite to avoid native addon SIGSEGV (nodejs/node#62515).
187
+ // node:sqlite is built into Node >= 22.5, no flag needed since 22.13.
188
+ try {
189
+ const { DatabaseSync } = require(["node", "sqlite"].join(":"));
190
+ _Database = function NodeDatabaseFactory(path, opts) {
191
+ const raw = new DatabaseSync(path, {
192
+ readOnly: opts?.readonly ?? false,
193
+ });
194
+ return new NodeSQLiteAdapter(raw);
195
+ };
196
+ }
197
+ catch {
198
+ // node:sqlite not available — fall through to better-sqlite3
199
+ _Database = require("better-sqlite3");
200
+ }
201
+ }
110
202
  else {
111
- // Node.js — use better-sqlite3.
203
+ // Non-Linux Node.js — use better-sqlite3.
112
204
  _Database = require("better-sqlite3");
113
205
  }
114
206
  }
@@ -134,6 +226,21 @@ export function applyWALPragmas(db) {
134
226
  // ─────────────────────────────────────────────────────────
135
227
  // DB file helpers
136
228
  // ─────────────────────────────────────────────────────────
229
+ /**
230
+ * Remove orphaned WAL/SHM files when the main DB file doesn't exist.
231
+ * On Windows, stale -wal/-shm files from crashed processes cause
232
+ * "file is not a database" errors when creating a fresh DB.
233
+ */
234
+ export function cleanOrphanedWALFiles(dbPath) {
235
+ if (!existsSync(dbPath)) {
236
+ for (const suffix of ["-wal", "-shm"]) {
237
+ try {
238
+ unlinkSync(dbPath + suffix);
239
+ }
240
+ catch { /* ignore */ }
241
+ }
242
+ }
243
+ }
137
244
  /**
138
245
  * Delete all three SQLite files for a given db path (main, WAL, SHM).
139
246
  * Silently ignores individual deletion errors so a partial cleanup
@@ -210,6 +317,32 @@ export function withRetry(fn, delays = [100, 500, 2000]) {
210
317
  `Original error: ${lastError?.message}`);
211
318
  }
212
319
  // ─────────────────────────────────────────────────────────
320
+ // Corrupt DB recovery (#244)
321
+ // ─────────────────────────────────────────────────────────
322
+ /**
323
+ * Detect SQLite corruption errors that warrant a rename-and-recreate.
324
+ * Matches SQLITE_CORRUPT, SQLITE_NOTADB, and their human-readable equivalents.
325
+ */
326
+ export function isSQLiteCorruptionError(msg) {
327
+ return (msg.includes("SQLITE_CORRUPT") ||
328
+ msg.includes("SQLITE_NOTADB") ||
329
+ msg.includes("database disk image is malformed") ||
330
+ msg.includes("file is not a database"));
331
+ }
332
+ /**
333
+ * Rename a corrupt DB and its WAL/SHM files so a fresh DB can be created.
334
+ * Best-effort — individual rename failures are silently ignored.
335
+ */
336
+ export function renameCorruptDB(dbPath) {
337
+ const ts = Date.now();
338
+ for (const suffix of ["", "-wal", "-shm"]) {
339
+ try {
340
+ renameSync(dbPath + suffix, `${dbPath}${suffix}.corrupt-${ts}`);
341
+ }
342
+ catch { /* file may not exist */ }
343
+ }
344
+ }
345
+ // ─────────────────────────────────────────────────────────
213
346
  // Base class
214
347
  // ─────────────────────────────────────────────────────────
215
348
  /**
@@ -236,10 +369,7 @@ const _liveDBs = (() => {
236
369
  g[_kLiveDBs] = new Set();
237
370
  process.on("exit", () => {
238
371
  for (const db of g[_kLiveDBs]) {
239
- try {
240
- db.close();
241
- }
242
- catch { /* already closed */ }
372
+ closeDB(db);
243
373
  }
244
374
  g[_kLiveDBs].clear();
245
375
  });
@@ -252,9 +382,31 @@ export class SQLiteBase {
252
382
  constructor(dbPath) {
253
383
  const Database = loadDatabase();
254
384
  this.#dbPath = dbPath;
255
- this.#db = new Database(dbPath, { timeout: 30000 });
385
+ cleanOrphanedWALFiles(dbPath);
386
+ let db;
387
+ try {
388
+ db = new Database(dbPath, { timeout: 30000 });
389
+ applyWALPragmas(db);
390
+ }
391
+ catch (err) {
392
+ const msg = err instanceof Error ? err.message : String(err);
393
+ if (isSQLiteCorruptionError(msg)) {
394
+ renameCorruptDB(dbPath);
395
+ cleanOrphanedWALFiles(dbPath);
396
+ try {
397
+ db = new Database(dbPath, { timeout: 30000 });
398
+ applyWALPragmas(db);
399
+ }
400
+ catch (retryErr) {
401
+ throw new Error(`Failed to create fresh DB after renaming corrupt file: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`);
402
+ }
403
+ }
404
+ else {
405
+ throw err;
406
+ }
407
+ }
408
+ this.#db = db;
256
409
  _liveDBs.add(this.#db);
257
- applyWALPragmas(this.#db);
258
410
  this.initSchema();
259
411
  this.prepareStatements();
260
412
  }
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * lifecycle — Process lifecycle guard for MCP server.
3
3
  *
4
- * Detects parent process death, stdin close, and OS signals to prevent
4
+ * Detects parent process death (ppid polling) and OS signals to prevent
5
5
  * orphaned MCP server processes consuming 100% CPU (issue #103).
6
6
  *
7
+ * Stdin close is NOT used as a shutdown signal — the MCP stdio transport
8
+ * owns stdin and transient pipe events cause spurious -32000 errors (#236).
9
+ *
7
10
  * Cross-platform: macOS, Linux, Windows.
8
11
  */
9
12
  export interface LifecycleGuardOptions {
10
13
  /** Interval in ms to check parent liveness. Default: 30_000 */
11
14
  checkIntervalMs?: number;
12
- /** Called when parent death or stdin close is detected. */
15
+ /** Called when parent death or OS signal is detected. */
13
16
  onShutdown: () => void;
14
17
  /** Injectable parent-alive check (for testing). Default: ppid-based check. */
15
18
  isParentAlive?: () => boolean;
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * lifecycle — Process lifecycle guard for MCP server.
3
3
  *
4
- * Detects parent process death, stdin close, and OS signals to prevent
4
+ * Detects parent process death (ppid polling) and OS signals to prevent
5
5
  * orphaned MCP server processes consuming 100% CPU (issue #103).
6
6
  *
7
+ * Stdin close is NOT used as a shutdown signal — the MCP stdio transport
8
+ * owns stdin and transient pipe events cause spurious -32000 errors (#236).
9
+ *
7
10
  * Cross-platform: macOS, Linux, Windows.
8
11
  */
9
12
  /**
@@ -43,13 +46,6 @@ export function startLifecycleGuard(opts) {
43
46
  shutdown();
44
47
  }, interval);
45
48
  timer.unref();
46
- // P0: Stdin close — parent pipe broken
47
- // Must resume stdin to receive close/end events (Node starts paused)
48
- const onStdinClose = () => shutdown();
49
- process.stdin.resume();
50
- process.stdin.on("end", onStdinClose);
51
- process.stdin.on("close", onStdinClose);
52
- process.stdin.on("error", onStdinClose);
53
49
  // P0: OS signals — terminal close, kill, ctrl+c
54
50
  const signals = ["SIGTERM", "SIGINT"];
55
51
  if (process.platform !== "win32")
@@ -59,9 +55,6 @@ export function startLifecycleGuard(opts) {
59
55
  return () => {
60
56
  stopped = true;
61
57
  clearInterval(timer);
62
- process.stdin.removeListener("end", onStdinClose);
63
- process.stdin.removeListener("close", onStdinClose);
64
- process.stdin.removeListener("error", onStdinClose);
65
58
  for (const sig of signals)
66
59
  process.removeListener(sig, shutdown);
67
60
  };