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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +33 -2
- package/build/adapters/claude-code/hooks.d.ts +11 -1
- package/build/adapters/claude-code/hooks.js +31 -1
- package/build/db-base.d.ts +32 -1
- package/build/db-base.js +162 -10
- package/build/lifecycle.d.ts +5 -2
- package/build/lifecycle.js +4 -11
- package/build/server.js +51 -26
- package/build/session/db.js +1 -1
- package/build/store.js +25 -3
- package/cli.bundle.mjs +93 -93
- package/configs/antigravity/GEMINI.md +1 -1
- package/configs/claude-code/CLAUDE.md +1 -1
- package/configs/codex/AGENTS.md +1 -1
- package/configs/gemini-cli/GEMINI.md +1 -1
- package/configs/kilo/AGENTS.md +1 -1
- package/configs/kiro/KIRO.md +1 -1
- package/configs/openclaw/AGENTS.md +1 -1
- package/configs/opencode/AGENTS.md +1 -1
- package/configs/pi/AGENTS.md +1 -1
- package/configs/vscode-copilot/copilot-instructions.md +1 -1
- package/configs/zed/AGENTS.md +1 -1
- package/hooks/codex/posttooluse.mjs +6 -5
- package/hooks/codex/sessionstart.mjs +5 -5
- package/hooks/core/mcp-ready.mjs +31 -0
- package/hooks/core/routing.mjs +27 -12
- package/hooks/cursor/posttooluse.mjs +6 -5
- package/hooks/cursor/sessionstart.mjs +5 -5
- package/hooks/cursor/stop.mjs +5 -4
- package/hooks/ensure-deps.mjs +38 -27
- package/hooks/gemini-cli/aftertool.mjs +5 -4
- package/hooks/gemini-cli/precompress.mjs +5 -4
- package/hooks/gemini-cli/sessionstart.mjs +5 -4
- package/hooks/hooks.json +13 -22
- package/hooks/kiro/posttooluse.mjs +6 -5
- package/hooks/routing-block.mjs +6 -2
- package/hooks/session-db.bundle.mjs +12 -12
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +73 -73
- 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.
|
|
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.
|
|
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.
|
|
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
|
@@ -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.
|
|
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.
|
|
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
|
[](https://www.npmjs.com/package/context-mode) [](https://www.npmjs.com/package/context-mode) [](https://github.com/mksglu/context-mode) [](https://github.com/mksglu/context-mode/stargazers) [](https://github.com/mksglu/context-mode/network/members) [](https://github.com/mksglu/context-mode/commits) [](LICENSE)
|
|
6
6
|
[](https://discord.gg/DCN9jUgN5v)
|
|
7
|
+
[](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
|
-
<
|
|
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", "
|
|
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. */
|
package/build/db-base.d.ts
CHANGED
|
@@ -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 →
|
|
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 →
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/build/lifecycle.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lifecycle — Process lifecycle guard for MCP server.
|
|
3
3
|
*
|
|
4
|
-
* Detects parent process death
|
|
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
|
|
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;
|
package/build/lifecycle.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lifecycle — Process lifecycle guard for MCP server.
|
|
3
3
|
*
|
|
4
|
-
* Detects parent process death
|
|
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
|
};
|