context-mode 1.0.97 → 1.0.99

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.97"
9
+ "version": "1.0.99"
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.97",
16
+ "version": "1.0.99",
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.97",
3
+ "version": "1.0.99",
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",
@@ -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.97",
6
+ "version": "1.0.99",
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.97",
3
+ "version": "1.0.99",
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
@@ -523,12 +523,14 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
523
523
  "hooks": {
524
524
  "PreToolUse": [{ "matcher": "local_shell|shell|shell_command|exec_command|container.exec|Bash|Shell|grep_files|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", "hooks": [{ "type": "command", "command": "context-mode hook codex pretooluse" }] }],
525
525
  "PostToolUse": [{ "hooks": [{ "type": "command", "command": "context-mode hook codex posttooluse" }] }],
526
- "SessionStart": [{ "hooks": [{ "type": "command", "command": "context-mode hook codex sessionstart" }] }]
526
+ "SessionStart": [{ "hooks": [{ "type": "command", "command": "context-mode hook codex sessionstart" }] }],
527
+ "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "context-mode hook codex userpromptsubmit" }] }],
528
+ "Stop": [{ "hooks": [{ "type": "command", "command": "context-mode hook codex stop" }] }]
527
529
  }
528
530
  }
529
531
  ```
530
532
 
531
- `PreToolUse` enforces deny/block routing today and is prepared for input rewrites once Codex supports them. `PostToolUse` captures session events. `SessionStart` restores state after compaction.
533
+ `PreToolUse` enforces deny/block routing today and is prepared for input rewrites once Codex supports them. `PostToolUse` captures session events. `SessionStart` restores state after compaction. `UserPromptSubmit` captures user decisions and corrections. `Stop` records turn-end state.
532
534
 
533
535
  > **Note:** Codex PreToolUse routing currently supports deny rules only (blocks dangerous commands). It still needs upstream `updatedInput` support before context-mode can rewrite tool input; track [openai/codex#18491](https://github.com/openai/codex/issues/18491). Context injection (`additionalContext`) is not supported in Codex PreToolUse — it works via PostToolUse and SessionStart instead. This is handled automatically.
534
536
 
@@ -912,12 +914,12 @@ Session continuity requires 4 hooks working together:
912
914
  |---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
913
915
  | **PreToolUse** | Enforces sandbox routing before tool execution | Yes | -- | -- | -- | Yes | -- | -- | -- | Yes | -- | Yes | -- | ✓ (via tool_call event) |
914
916
  | **PostToolUse** | Captures events after each tool call | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | ✓ (via tool_result event) |
915
- | **UserPromptSubmit** | Captures user decisions and corrections | Yes | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- |
917
+ | **UserPromptSubmit** | Captures user decisions and corrections | Yes | -- | -- | -- | -- | -- | -- | -- | Yes | -- | -- | -- | -- |
916
918
  | **PreCompact** | Builds snapshot before compaction | Yes | Yes | Yes | Yes | -- | Plugin | Plugin | Plugin | -- | -- | -- | -- | ✓ (via session_before_compact) |
917
919
  | **SessionStart** | Restores state after compaction or resume | Yes | Yes | Yes | Yes | -- | -- | -- | Plugin | Yes | -- | -- | -- | ✓ (via session_start event) |
918
920
  | | **Session completeness** | **Full** | **High** | **High** | **High** | **Partial** | **High** | **High** | **High** | **Partial** | **--** | **Partial** | **--** | **High** |
919
921
 
920
- > **Note:** Full session continuity (capture + snapshot + restore) works on **Claude Code**, **Gemini CLI**, **VS Code Copilot**, and **JetBrains Copilot**. **OpenCode** provides **high** session continuity: it captures tool events and injects compaction snapshots via the plugin, but SessionStart is not yet available ([#14808](https://github.com/sst/opencode/issues/14808)), so startup/resume restore is not supported. **KiloCode** shares the same plugin architecture as OpenCode via the OpenCodeAdapter, so its continuity level depends on KiloCode's SessionStart support. **Cursor** captures tool events via `preToolUse`/`postToolUse`, but `sessionStart` is currently rejected by Cursor's validator ([forum report](https://forum.cursor.com/t/unknown-hook-type-sessionstart/149566)), so session restore after compaction is not available yet. **OpenClaw** uses native gateway plugin hooks (`api.on()`) for full session continuity. **Pi Coding Agent** provides high session continuity via extension hooks (`tool_call`, `tool_result`, `session_start`, `session_before_compact`). **Codex CLI** provides partial hook-based session tracking through PreToolUse, PostToolUse, and SessionStart; MCP tools work. **Antigravity**, **Kiro**, and **Zed** have no hook support in the current release, so session tracking is not available.
922
+ > **Note:** Full session continuity (capture + snapshot + restore) works on **Claude Code**, **Gemini CLI**, **VS Code Copilot**, and **JetBrains Copilot**. **OpenCode** provides **high** session continuity: it captures tool events and injects compaction snapshots via the plugin, but SessionStart is not yet available ([#14808](https://github.com/sst/opencode/issues/14808)), so startup/resume restore is not supported. **KiloCode** shares the same plugin architecture as OpenCode via the OpenCodeAdapter, so its continuity level depends on KiloCode's SessionStart support. **Cursor** captures tool events via `preToolUse`/`postToolUse`, but `sessionStart` is currently rejected by Cursor's validator ([forum report](https://forum.cursor.com/t/unknown-hook-type-sessionstart/149566)), so session restore after compaction is not available yet. **OpenClaw** uses native gateway plugin hooks (`api.on()`) for full session continuity. **Pi Coding Agent** provides high session continuity via extension hooks (`tool_call`, `tool_result`, `session_start`, `session_before_compact`). **Codex CLI** provides partial hook-based session tracking through PreToolUse, PostToolUse, SessionStart, UserPromptSubmit, and Stop; MCP tools work. **Antigravity**, **Kiro**, and **Zed** have no hook support in the current release, so session tracking is not available.
921
923
 
922
924
  <details>
923
925
  <summary><strong>What gets captured</strong></summary>
@@ -979,7 +981,7 @@ After compaction, the model receives a **Session Guide** — a structured narrat
979
981
  - **Session Intent** — mode classification (implement, investigate, review, discuss)
980
982
  - **User Role** — behavioral directives set during the session
981
983
 
982
- Detailed event data is also indexed into FTS5 for on-demand retrieval via `search()`.
984
+ Detailed event data is also indexed into FTS5 for on-demand retrieval via `ctx_search()`.
983
985
 
984
986
  </details>
985
987
 
@@ -1002,7 +1004,7 @@ Detailed event data is also indexed into FTS5 for on-demand retrieval via `searc
1002
1004
 
1003
1005
  **OpenClaw / Pi Agent** — High coverage. All tool lifecycle hooks (`after_tool_call`, `before_compaction`, `session_start`) fire via the native gateway plugin. User decisions aren't captured but file edits, git ops, errors, and tasks are fully tracked. Falls back to DB snapshot reconstruction if compaction hooks fail on older gateway versions. See [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md).
1004
1006
 
1005
- **Codex CLI** — MCP active, hooks stable. Hook scripts (PreToolUse, PostToolUse, SessionStart) are implemented and tested. PreToolUse deny routing works; input rewriting still depends on upstream `updatedInput` support ([openai/codex#18491](https://github.com/openai/codex/issues/18491)).
1007
+ **Codex CLI** — MCP active, hooks stable. Hook scripts (PreToolUse, PostToolUse, SessionStart, UserPromptSubmit, Stop) are implemented and tested. PreToolUse deny routing works; input rewriting still depends on upstream `updatedInput` support ([openai/codex#18491](https://github.com/openai/codex/issues/18491)).
1006
1008
 
1007
1009
  **Antigravity** — No session support. No hooks, no event capture. Requires manually copying `GEMINI.md` to your project root. Auto-detected via MCP protocol handshake (`clientInfo.name`).
1008
1010
 
@@ -1035,7 +1037,7 @@ Detailed event data is also indexed into FTS5 for on-demand retrieval via `searc
1035
1037
  >
1036
1038
  > **OpenClaw** runs context-mode as a native gateway plugin targeting Pi Agent sessions. Hooks register via `api.on()` (tool/lifecycle) and `api.registerHook()` (commands). All tool interception and compaction hooks are supported. See [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md).
1037
1039
  >
1038
- > **Codex CLI** hooks are stable. MCP tools work, and hook scripts activate through `~/.codex/hooks.json`. PreToolUse supports `permissionDecision: "deny"` only; input modification still needs upstream `updatedInput` support ([openai/codex#18491](https://github.com/openai/codex/issues/18491)). `additionalContext` is not supported in PreToolUse (context injection works via PostToolUse and SessionStart instead; the codex formatter handles this automatically). See the Codex install section for setup. **Antigravity** and **Zed** do not support hooks. They rely solely on manually-copied routing instruction files (`AGENTS.md` / `GEMINI.md`) for enforcement (~60% compliance). See each platform's install section for copy instructions. Antigravity and Zed are auto-detected via MCP protocol handshake — no manual platform configuration needed.
1040
+ > **Codex CLI** hooks are stable. MCP tools work, and hook scripts activate through `~/.codex/hooks.json`. PreToolUse supports `permissionDecision: "deny"` only; input modification still needs upstream `updatedInput` support ([openai/codex#18491](https://github.com/openai/codex/issues/18491)). `additionalContext` is not supported in PreToolUse (context injection works via PostToolUse and SessionStart instead; the codex formatter handles this automatically). UserPromptSubmit and Stop capture prompt and turn-end continuity events. See the Codex install section for setup. **Antigravity** and **Zed** do not support hooks. They rely solely on manually-copied routing instruction files (`AGENTS.md` / `GEMINI.md`) for enforcement (~60% compliance). See each platform's install section for copy instructions. Antigravity and Zed are auto-detected via MCP protocol handshake — no manual platform configuration needed.
1039
1041
  >
1040
1042
  > **Kiro** supports native `preToolUse` and `postToolUse` hooks for routing enforcement and tool event capture. `agentSpawn` (SessionStart equivalent) and `stop` are not yet wired. Requires manually copying `KIRO.md` to your project root. Kiro is auto-detected via MCP protocol handshake (`clientInfo.name`).
1041
1043
  >
@@ -26,7 +26,7 @@ export class ClaudeCodeBaseAdapter extends BaseAdapter {
26
26
  toolName: input.tool_name ?? "",
27
27
  toolInput: input.tool_input ?? {},
28
28
  sessionId: this.extractSessionId(input),
29
- projectDir: process.env[this.projectDirEnvVar],
29
+ projectDir: process.env[this.projectDirEnvVar] ?? process.cwd(),
30
30
  raw,
31
31
  };
32
32
  }
@@ -38,7 +38,7 @@ export class ClaudeCodeBaseAdapter extends BaseAdapter {
38
38
  toolOutput: input.tool_output,
39
39
  isError: input.is_error,
40
40
  sessionId: this.extractSessionId(input),
41
- projectDir: process.env[this.projectDirEnvVar],
41
+ projectDir: process.env[this.projectDirEnvVar] ?? process.cwd(),
42
42
  raw,
43
43
  };
44
44
  }
@@ -46,7 +46,7 @@ export class ClaudeCodeBaseAdapter extends BaseAdapter {
46
46
  const input = raw;
47
47
  return {
48
48
  sessionId: this.extractSessionId(input),
49
- projectDir: process.env[this.projectDirEnvVar],
49
+ projectDir: process.env[this.projectDirEnvVar] ?? process.cwd(),
50
50
  raw,
51
51
  };
52
52
  }
@@ -70,7 +70,7 @@ export class ClaudeCodeBaseAdapter extends BaseAdapter {
70
70
  return {
71
71
  sessionId: this.extractSessionId(input),
72
72
  source,
73
- projectDir: process.env[this.projectDirEnvVar],
73
+ projectDir: process.env[this.projectDirEnvVar] ?? process.cwd(),
74
74
  raw,
75
75
  };
76
76
  }
@@ -182,6 +182,28 @@ export class CodexAdapter extends BaseAdapter {
182
182
  ],
183
183
  },
184
184
  ],
185
+ UserPromptSubmit: [
186
+ {
187
+ matcher: "",
188
+ hooks: [
189
+ {
190
+ type: "command",
191
+ command: `node ${pluginRoot}/hooks/codex/userpromptsubmit.mjs`,
192
+ },
193
+ ],
194
+ },
195
+ ],
196
+ Stop: [
197
+ {
198
+ matcher: "",
199
+ hooks: [
200
+ {
201
+ type: "command",
202
+ command: `node ${pluginRoot}/hooks/codex/stop.mjs`,
203
+ },
204
+ ],
205
+ },
206
+ ],
185
207
  };
186
208
  }
187
209
  readSettings() {
@@ -208,7 +230,7 @@ export class CodexAdapter extends BaseAdapter {
208
230
  {
209
231
  check: "Hook support",
210
232
  status: "pass",
211
- message: "Codex CLI hooks are stable. Configure ~/.codex/hooks.json for PreToolUse, PostToolUse, and SessionStart.",
233
+ message: "Codex CLI hooks are stable. Configure ~/.codex/hooks.json for PreToolUse, PostToolUse, SessionStart, UserPromptSubmit, and Stop.",
212
234
  },
213
235
  ];
214
236
  }
@@ -27,7 +27,7 @@ export declare class QwenCodeAdapter extends ClaudeCodeBaseAdapter implements Ho
27
27
  validateHooks(_pluginRoot: string): DiagnosticResult[];
28
28
  checkPluginRegistration(): DiagnosticResult;
29
29
  getInstalledVersion(): string;
30
- configureAllHooks(_pluginRoot: string): string[];
30
+ configureAllHooks(pluginRoot: string): string[];
31
31
  setHookPermissions(_pluginRoot: string): string[];
32
32
  updatePluginRegistry(_pluginRoot: string, _version: string): void;
33
33
  getRoutingInstructionsConfig(): {
@@ -12,7 +12,7 @@
12
12
  * - MCP clientInfo: qwen-cli-mcp-client-* (pattern)
13
13
  * - 12 hook events (superset of Claude's 5, but context-mode uses the shared 5)
14
14
  */
15
- import { readFileSync, } from "node:fs";
15
+ import { readFileSync, existsSync, } from "node:fs";
16
16
  import { resolve, join } from "node:path";
17
17
  import { homedir } from "node:os";
18
18
  import { ClaudeCodeBaseAdapter } from "../claude-code-base.js";
@@ -63,7 +63,7 @@ export class QwenCodeAdapter extends ClaudeCodeBaseAdapter {
63
63
  ],
64
64
  PostToolUse: [
65
65
  {
66
- matcher: "",
66
+ matcher: "run_shell_command|read_file|write_file|edit|glob|grep_search|todo_write|agent|ask_user_question|mcp__",
67
67
  hooks: [
68
68
  { type: "command", command: `node ${pluginRoot}/hooks/posttooluse.mjs` },
69
69
  ],
@@ -162,10 +162,116 @@ export class QwenCodeAdapter extends ClaudeCodeBaseAdapter {
162
162
  }
163
163
  }
164
164
  getInstalledVersion() {
165
+ const settings = this.readSettings();
166
+ if (!settings)
167
+ return "not installed";
168
+ const hooks = settings.hooks;
169
+ if (!hooks)
170
+ return "not installed";
171
+ // Check if any hook type has context-mode scripts configured
172
+ const contextModeScripts = [
173
+ "pretooluse.mjs",
174
+ "posttooluse.mjs",
175
+ "precompact.mjs",
176
+ "sessionstart.mjs",
177
+ "userpromptsubmit.mjs",
178
+ ];
179
+ for (const [, entries] of Object.entries(hooks)) {
180
+ if (!Array.isArray(entries))
181
+ continue;
182
+ for (const entry of entries) {
183
+ const e = entry;
184
+ if (e.hooks?.some((h) => h.command && contextModeScripts.some((s) => h.command.includes(s)))) {
185
+ return "installed (hooks configured)";
186
+ }
187
+ }
188
+ }
165
189
  return "not installed";
166
190
  }
167
- configureAllHooks(_pluginRoot) {
168
- return [];
191
+ configureAllHooks(pluginRoot) {
192
+ const settings = this.readSettings() ?? {};
193
+ const hooks = (settings.hooks ?? {});
194
+ const changes = [];
195
+ // ── Phase 1: Clean stale context-mode hooks ──────────
196
+ // After an upgrade, settings.json may contain hardcoded paths
197
+ // pointing to deleted version directories. Remove those.
198
+ for (const hookType of Object.keys(hooks)) {
199
+ const entries = hooks[hookType];
200
+ if (!Array.isArray(entries))
201
+ continue;
202
+ const filtered = entries.filter((entry) => {
203
+ const e = entry;
204
+ const commands = e.hooks ?? [];
205
+ // Preserve entries that are not context-mode hooks
206
+ const isContextMode = commands.some((h) => h.command && /context-mode|pretooluse|posttooluse|precompact|sessionstart|userpromptsubmit/i.test(h.command));
207
+ if (!isContextMode)
208
+ return true;
209
+ // For context-mode hooks, check if referenced script files exist
210
+ return commands.every((h) => {
211
+ if (!h.command)
212
+ return true;
213
+ // Extract path from "node /path/to/script.mjs" format
214
+ const match = h.command.match(/node\s+"?([^"]+\.mjs)"?/);
215
+ if (!match)
216
+ return true; // CLI dispatcher format, always valid
217
+ return existsSync(match[1]);
218
+ });
219
+ });
220
+ const removed = entries.length - filtered.length;
221
+ if (removed > 0) {
222
+ hooks[hookType] = filtered;
223
+ changes.push(`Removed ${removed} stale ${hookType} hook(s)`);
224
+ }
225
+ }
226
+ // ── Phase 2: Register fresh hooks ────────────────────
227
+ const hookTypes = [
228
+ {
229
+ name: "PreToolUse",
230
+ script: "pretooluse.mjs",
231
+ matcher: [
232
+ "run_shell_command", "read_file", "read_many_files", "grep_search",
233
+ "web_fetch", "agent",
234
+ "mcp__plugin_context-mode_context-mode__ctx_execute",
235
+ "mcp__plugin_context-mode_context-mode__ctx_execute_file",
236
+ "mcp__plugin_context-mode_context-mode__ctx_batch_execute",
237
+ ].join("|"),
238
+ },
239
+ {
240
+ name: "SessionStart",
241
+ script: "sessionstart.mjs",
242
+ matcher: "",
243
+ },
244
+ ];
245
+ for (const { name, script, matcher } of hookTypes) {
246
+ const entry = {
247
+ matcher,
248
+ hooks: [{ type: "command", command: `node ${pluginRoot}/hooks/${script}` }],
249
+ };
250
+ const existing = hooks[name];
251
+ if (existing && Array.isArray(existing)) {
252
+ // Replace existing context-mode entry or append
253
+ const idx = existing.findIndex((e) => {
254
+ const typed = e;
255
+ return typed.hooks?.some((h) => h.command?.includes(script)) ?? false;
256
+ });
257
+ if (idx >= 0) {
258
+ existing[idx] = entry;
259
+ changes.push(`Updated ${name} hook`);
260
+ }
261
+ else {
262
+ existing.push(entry);
263
+ changes.push(`Added ${name} hook`);
264
+ }
265
+ hooks[name] = existing;
266
+ }
267
+ else {
268
+ hooks[name] = [entry];
269
+ changes.push(`Created ${name} hooks`);
270
+ }
271
+ }
272
+ settings.hooks = hooks;
273
+ this.writeSettings(settings);
274
+ return changes;
169
275
  }
170
276
  setHookPermissions(_pluginRoot) {
171
277
  return [];
package/build/cli.js CHANGED
@@ -55,6 +55,8 @@ const HOOK_MAP = {
55
55
  pretooluse: "hooks/codex/pretooluse.mjs",
56
56
  posttooluse: "hooks/codex/posttooluse.mjs",
57
57
  sessionstart: "hooks/codex/sessionstart.mjs",
58
+ userpromptsubmit: "hooks/codex/userpromptsubmit.mjs",
59
+ stop: "hooks/codex/stop.mjs",
58
60
  },
59
61
  "kiro": {
60
62
  pretooluse: "hooks/kiro/pretooluse.mjs",
@@ -27,7 +27,7 @@ import { buildResumeSnapshot } from "./session/snapshot.js";
27
27
  import { OpenCodeAdapter } from "./adapters/opencode/index.js";
28
28
  // ── Helpers ───────────────────────────────────────────────
29
29
  function getPlatform() {
30
- return process.env.KILO ? "kilo" : "opencode";
30
+ return process.env.KILO_PID ? "kilo" : "opencode";
31
31
  }
32
32
  // ── Plugin Factory ────────────────────────────────────────
33
33
  /**
package/build/server.js CHANGED
@@ -58,7 +58,7 @@ const executor = new PolyglotExecutor({
58
58
  projectRoot: process.env.CLAUDE_PROJECT_DIR,
59
59
  });
60
60
  // ─────────────────────────────────────────────────────────
61
- // FS read tracking preload for batch_execute
61
+ // FS read tracking preload for ctx_batch_execute
62
62
  // ─────────────────────────────────────────────────────────
63
63
  // NODE_OPTIONS is denied by the executor's #buildSafeEnv (security).
64
64
  // Instead, we inject it as an inline shell env prefix in each batch command.
@@ -515,7 +515,7 @@ export function formatBatchQueryResults(store, queries, source, maxOutput = 80 *
515
515
  let outputSize = 0;
516
516
  for (const query of queries) {
517
517
  if (outputSize > maxOutput) {
518
- sections.push(`## ${query}\n(output cap reached — use search(queries: ["${query}"]) for details)\n`);
518
+ sections.push(`## ${query}\n(output cap reached — use ctx_search(queries: ["${query}"]) for details)\n`);
519
519
  continue;
520
520
  }
521
521
  const results = store.searchWithFallback(query, 3, source, undefined, "exact");
@@ -577,7 +577,7 @@ server.registerTool("ctx_execute", {
577
577
  .optional()
578
578
  .describe("What you're looking for in the output. When provided and output is large (>5KB), " +
579
579
  "indexes output into knowledge base and returns section titles + previews — not full content. " +
580
- "Use search(queries: [...]) to retrieve specific sections. Example: 'failing tests', 'HTTP 500 errors'." +
580
+ "Use ctx_search(queries: [...]) to retrieve specific sections. Example: 'failing tests', 'HTTP 500 errors'." +
581
581
  "\n\nTIP: Use specific technical terms, not just concepts. Check 'Searchable terms' in the response for available vocabulary."),
582
582
  }),
583
583
  }, async ({ language, code, timeout, background, intent }) => {
@@ -779,7 +779,7 @@ function indexStdout(stdout, source) {
779
779
  content: [
780
780
  {
781
781
  type: "text",
782
- text: `Indexed ${indexed.totalChunks} sections (${indexed.codeChunks} with code) from: ${indexed.label}\nUse search(queries: ["..."]) to query this content. Use source: "${indexed.label}" to scope results.`,
782
+ text: `Indexed ${indexed.totalChunks} sections (${indexed.codeChunks} with code) from: ${indexed.label}\nUse ctx_search(queries: ["..."]) to query this content. Use source: "${indexed.label}" to scope results.`,
783
783
  },
784
784
  ],
785
785
  };
@@ -792,7 +792,7 @@ const LARGE_OUTPUT_THRESHOLD = 102_400; // 100KB — auto-index into FTS5, retur
792
792
  function intentSearch(stdout, intent, source, maxResults = 5) {
793
793
  const totalLines = stdout.split("\n").length;
794
794
  const totalBytes = Buffer.byteLength(stdout);
795
- // Index into the PERSISTENT store so user can search() later
795
+ // Index into the PERSISTENT store so user can ctx_search() later
796
796
  const persistent = getStore();
797
797
  const indexed = persistent.indexPlainText(stdout, source);
798
798
  // Search the persistent store directly (porter → trigram → fuzzy)
@@ -809,7 +809,7 @@ function intentSearch(stdout, intent, source, maxResults = 5) {
809
809
  lines.push(`Searchable terms: ${distinctiveTerms.join(", ")}`);
810
810
  }
811
811
  lines.push("");
812
- lines.push("Use search() to explore the indexed content.");
812
+ lines.push("Use ctx_search(queries: [...]) to explore the indexed content.");
813
813
  return lines.join("\n");
814
814
  }
815
815
  // Return ONLY titles + first-line previews — not full content
@@ -827,7 +827,7 @@ function intentSearch(stdout, intent, source, maxResults = 5) {
827
827
  lines.push(`Searchable terms: ${distinctiveTerms.join(", ")}`);
828
828
  }
829
829
  lines.push("");
830
- lines.push("Use search(queries: [...]) to retrieve full content of any section.");
830
+ lines.push("Use ctx_search(queries: [...]) to retrieve full content of any section.");
831
831
  return lines.join("\n");
832
832
  }
833
833
  // ─────────────────────────────────────────────────────────
@@ -977,9 +977,9 @@ server.registerTool("ctx_index", {
977
977
  "- Skill prompts and instructions that are too large for context\n" +
978
978
  "- README files, migration guides, changelog entries\n" +
979
979
  "- Any content with code examples you may need to reference precisely\n\n" +
980
- "After indexing, use 'search' to retrieve specific sections on-demand.\n" +
980
+ "After indexing, use 'ctx_search' to retrieve specific sections on-demand.\n" +
981
981
  "When `path` is provided, a content hash is stored for automatic stale detection in search results.\n" +
982
- "Do NOT use for: log files, test output, CSV, build output — use 'execute_file' for those.",
982
+ "Do NOT use for: log files, test output, CSV, build output — use 'ctx_execute_file' for those.",
983
983
  inputSchema: z.object({
984
984
  content: z
985
985
  .string()
@@ -1023,7 +1023,7 @@ server.registerTool("ctx_index", {
1023
1023
  content: [
1024
1024
  {
1025
1025
  type: "text",
1026
- text: `Indexed ${result.totalChunks} sections (${result.codeChunks} with code) from: ${result.label}\nUse search(queries: ["..."]) to query this content. Use source: "${result.label}" to scope results.`,
1026
+ text: `Indexed ${result.totalChunks} sections (${result.codeChunks} with code) from: ${result.label}\nUse ctx_search(queries: ["..."]) to query this content. Use source: "${result.label}" to scope results.`,
1027
1027
  },
1028
1028
  ],
1029
1029
  });
@@ -1151,7 +1151,7 @@ server.registerTool("ctx_search", {
1151
1151
  type: "text",
1152
1152
  text: `BLOCKED: ${searchCallCount} search calls in ${Math.round((now - searchWindowStart) / 1000)}s. ` +
1153
1153
  "You're flooding context. STOP making individual search calls. " +
1154
- "Use batch_execute(commands, queries) for your next research step.",
1154
+ "Use ctx_batch_execute(commands, queries) for your next research step.",
1155
1155
  }],
1156
1156
  isError: true,
1157
1157
  });
@@ -1193,7 +1193,7 @@ server.registerTool("ctx_search", {
1193
1193
  if (searchCallCount >= SEARCH_MAX_RESULTS_AFTER) {
1194
1194
  output += `\n\n⚠ search call #${searchCallCount}/${SEARCH_BLOCK_AFTER} in this window. ` +
1195
1195
  `Results limited to ${effectiveLimit}/query. ` +
1196
- `Batch queries: search(queries: ["q1","q2","q3"]) or use batch_execute.`;
1196
+ `Batch queries: ctx_search(queries: ["q1","q2","q3"]) or use ctx_batch_execute.`;
1197
1197
  }
1198
1198
  if (output.trim().length === 0) {
1199
1199
  const sources = store.listSources();
@@ -1296,7 +1296,7 @@ main();
1296
1296
  server.registerTool("ctx_fetch_and_index", {
1297
1297
  title: "Fetch & Index URL",
1298
1298
  description: "Fetches URL content, converts HTML to markdown, indexes into searchable knowledge base, " +
1299
- "and returns a ~3KB preview. Full content stays in sandbox — use search() for deeper lookups.\n\n" +
1299
+ "and returns a ~3KB preview. Full content stays in sandbox — use ctx_search() for deeper lookups.\n\n" +
1300
1300
  "Better than WebFetch: preview is immediate, full content is searchable, raw HTML never enters context.\n\n" +
1301
1301
  "Content-type aware: HTML is converted to markdown, JSON is chunked by key paths, plain text is indexed directly.\n\n" +
1302
1302
  "When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
@@ -1332,7 +1332,7 @@ server.registerTool("ctx_fetch_and_index", {
1332
1332
  return trackResponse("ctx_fetch_and_index", {
1333
1333
  content: [{
1334
1334
  type: "text",
1335
- text: `Cached: **${meta.label}** — ${meta.chunkCount} sections, indexed ${ageStr} (fresh, TTL: 24h).\nTo refresh: call ctx_fetch_and_index again with \`force: true\`.\n\nYou MUST call search() to answer questions about this content — this cached response contains no content.\nUse: search(queries: [...], source: "${meta.label}")`,
1335
+ text: `Cached: **${meta.label}** — ${meta.chunkCount} sections, indexed ${ageStr} (fresh, TTL: 24h).\nTo refresh: call ctx_fetch_and_index again with \`force: true\`.\n\nYou MUST call ctx_search() to answer questions about this content — this cached response contains no content.\nUse: ctx_search(queries: [...], source: "${meta.label}")`,
1336
1336
  }],
1337
1337
  });
1338
1338
  }
@@ -1406,12 +1406,12 @@ server.registerTool("ctx_fetch_and_index", {
1406
1406
  // Build preview — first ~3KB of markdown for immediate use
1407
1407
  const PREVIEW_LIMIT = 3072;
1408
1408
  const preview = markdown.length > PREVIEW_LIMIT
1409
- ? markdown.slice(0, PREVIEW_LIMIT) + "\n\n…[truncated — use search() for full content]"
1409
+ ? markdown.slice(0, PREVIEW_LIMIT) + "\n\n…[truncated — use ctx_search() for full content]"
1410
1410
  : markdown;
1411
1411
  const totalKB = (Buffer.byteLength(markdown) / 1024).toFixed(1);
1412
1412
  const text = [
1413
1413
  `Fetched and indexed **${indexed.totalChunks} sections** (${totalKB}KB) from: ${indexed.label}`,
1414
- `Full content indexed in sandbox — use search(queries: [...], source: "${indexed.label}") for specific lookups.`,
1414
+ `Full content indexed in sandbox — use ctx_search(queries: [...], source: "${indexed.label}") for specific lookups.`,
1415
1415
  "",
1416
1416
  "---",
1417
1417
  "",
@@ -1445,8 +1445,8 @@ server.registerTool("ctx_batch_execute", {
1445
1445
  title: "Batch Execute & Search",
1446
1446
  description: "Execute multiple commands in ONE call, auto-index all output, and search with multiple queries. " +
1447
1447
  "Returns search results directly — no follow-up calls needed.\n\n" +
1448
- "THIS IS THE PRIMARY TOOL. Use this instead of multiple execute() calls.\n\n" +
1449
- "One batch_execute call replaces 30+ execute calls + 10+ search calls.\n" +
1448
+ "THIS IS THE PRIMARY TOOL. Use this instead of multiple ctx_execute() calls.\n\n" +
1449
+ "One ctx_batch_execute call replaces 30+ ctx_execute calls + 10+ ctx_search calls.\n" +
1450
1450
  "Provide all commands to run and all queries to search — everything happens in one round trip.\n\n" +
1451
1451
  "THINK IN CODE: When commands produce data you need to analyze, add processing commands that filter and summarize. Don't pull raw output into context — let the sandbox do the work.\n\n" +
1452
1452
  "When reporting results — terse like caveman. Technical substance exact. Only fluff die. Pattern: [thing] [action] [reason]. [next step].",
@@ -1560,7 +1560,7 @@ server.registerTool("ctx_batch_execute", {
1560
1560
  sectionTitles.push(s.title);
1561
1561
  }
1562
1562
  // Run all search queries — source scoped only.
1563
- // Cross-source search remains available via explicit search().
1563
+ // Cross-source search remains available via explicit ctx_search().
1564
1564
  const queryResults = formatBatchQueryResults(store, queries, source);
1565
1565
  // Get searchable terms for edge cases where follow-up is needed
1566
1566
  const distinctiveTerms = store.getDistinctiveTerms
@@ -2154,8 +2154,11 @@ async function main() {
2154
2154
  if (cleaned > 0) {
2155
2155
  console.error(`Cleaned up ${cleaned} stale DB file(s) from previous sessions`);
2156
2156
  }
2157
- // MCP readiness sentinel path (#230)
2158
- const mcpSentinel = join(tmpdir(), `context-mode-mcp-ready-${process.ppid}`);
2157
+ // MCP readiness sentinel path (#230, #347)
2158
+ // Uses process.pid (not ppid) — hooks use directory-scan to find any live sentinel.
2159
+ // Hardcoded /tmp on Unix to avoid TMPDIR mismatch (#347).
2160
+ const mcpSentinelDir = process.platform === "win32" ? tmpdir() : "/tmp";
2161
+ const mcpSentinel = join(mcpSentinelDir, `context-mode-mcp-ready-${process.pid}`);
2159
2162
  // Clean up own DB + backgrounded processes + preload script on shutdown
2160
2163
  const shutdown = () => {
2161
2164
  executor.cleanupBackgrounded();
package/build/store.js CHANGED
@@ -218,6 +218,40 @@ function findAllPositions(text, term) {
218
218
  }
219
219
  return positions;
220
220
  }
221
+ /**
222
+ * Count matched adjacent pairs across consecutive query terms.
223
+ * For each pair (term[i], term[i+1]), pairs each left position with at most one
224
+ * right position whose offset falls within `gap` chars of `p + len(term[i])`.
225
+ * `positionLists` must be sorted ascending (output of `findAllPositions` is).
226
+ * Each right position is consumed by at most one left, so `"foo foo bar"`
227
+ * counts 1 pair, not 2 — matches IR phrase-occurrence intent and avoids
228
+ * inflating boosts for repeated-token queries.
229
+ * Used by reranker to layer a frequency signal on top of minSpan proximity:
230
+ * 30-char gap covers natural prose without rewarding distant matches.
231
+ */
232
+ function countAdjacentPairs(positionLists, terms, gap = 30) {
233
+ if (positionLists.length < 2 || terms.length < 2)
234
+ return 0;
235
+ let total = 0;
236
+ const pairs = Math.min(positionLists.length, terms.length) - 1;
237
+ for (let i = 0; i < pairs; i++) {
238
+ const left = positionLists[i];
239
+ const right = positionLists[i + 1];
240
+ const leftLen = terms[i].length;
241
+ let j = 0;
242
+ for (const p of left) {
243
+ const minStart = p + leftLen;
244
+ const maxStart = minStart + gap;
245
+ while (j < right.length && right[j] < minStart)
246
+ j++;
247
+ if (j < right.length && right[j] <= maxStart) {
248
+ total++;
249
+ j++;
250
+ }
251
+ }
252
+ }
253
+ return total;
254
+ }
221
255
  /**
222
256
  * Find minimum span (window) covering at least one position from each list.
223
257
  * Uses a sweep-line approach: advance the pointer at the current minimum.
@@ -603,10 +637,16 @@ export class ContentStore {
603
637
  // ── Index ──
604
638
  index(options) {
605
639
  const { content, path, source } = options;
606
- if (!content && !path) {
640
+ // Treat empty string as "no content" so an empty `content` paired with a
641
+ // valid `path` falls back to reading the file. Some MCP clients
642
+ // materialize optional string fields as `""` and the previous
643
+ // `content ?? readFileSync(path)` kept the empty string, indexing 0
644
+ // chunks. See issue #350.
645
+ const hasContent = typeof content === "string" && content.length > 0;
646
+ if (!hasContent && !path) {
607
647
  throw new Error("Either content or path must be provided");
608
648
  }
609
- const text = content ?? readFileSync(path, "utf-8");
649
+ const text = hasContent ? content : readFileSync(path, "utf-8");
610
650
  const label = source ?? path ?? "untitled";
611
651
  const chunks = this.#chunkMarkdown(text);
612
652
  // Stale detection: store file_path + SHA-256 for file-backed sources
@@ -859,17 +899,24 @@ export class ContentStore {
859
899
  const titleHits = terms.filter((t) => titleLower.includes(t)).length;
860
900
  const titleWeight = r.contentType === "code" ? 0.6 : 0.3;
861
901
  const titleBoost = titleHits > 0 ? titleWeight * (titleHits / terms.length) : 0;
862
- // Proximity boost for multi-term queries
902
+ // Proximity boost for multi-term queries. minSpan picks the single
903
+ // tightest window — frequency doesn't move it, so a long doc with one
904
+ // tight occurrence outranks a short doc with several. Phrase-frequency
905
+ // reward layers a saturating frequency signal on top: cap 0.5 (below
906
+ // proximity max ≈1.0, in title-boost range), saturates at 4 hits.
863
907
  let proximityBoost = 0;
908
+ let phraseBoost = 0;
864
909
  if (terms.length >= 2) {
865
910
  const content = r.content.toLowerCase();
866
911
  const positions = terms.map((t) => findAllPositions(content, t));
867
912
  if (!positions.some((p) => p.length === 0)) {
868
913
  const minSpan = findMinSpan(positions);
869
914
  proximityBoost = 1 / (1 + minSpan / Math.max(content.length, 1));
915
+ const adjacentPairs = countAdjacentPairs(positions, terms);
916
+ phraseBoost = 0.5 * Math.min(1, adjacentPairs / 4);
870
917
  }
871
918
  }
872
- return { result: r, boost: titleBoost + proximityBoost };
919
+ return { result: r, boost: titleBoost + proximityBoost + phraseBoost };
873
920
  })
874
921
  .sort((a, b) => b.boost - a.boost || a.result.rank - b.result.rank)
875
922
  .map(({ result }) => result);