context-mode 1.0.98 → 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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +9 -7
- package/build/adapters/claude-code-base.js +4 -4
- package/build/adapters/codex/index.js +23 -1
- package/build/adapters/qwen-code/index.d.ts +1 -1
- package/build/adapters/qwen-code/index.js +110 -4
- package/build/cli.js +2 -0
- package/build/opencode-plugin.js +1 -1
- package/build/server.js +19 -19
- package/build/store.js +51 -4
- package/cli.bundle.mjs +101 -101
- package/configs/codex/AGENTS.md +1 -1
- package/configs/codex/hooks.json +14 -0
- package/configs/kilo/AGENTS.md +1 -1
- package/configs/openclaw/AGENTS.md +1 -1
- package/hooks/codex/stop.mjs +43 -0
- package/hooks/codex/userpromptsubmit.mjs +75 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +82 -82
- package/skills/context-mode-ops/agent-teams.md +1 -1
|
@@ -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.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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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 `
|
|
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
|
|
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(
|
|
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(
|
|
168
|
-
|
|
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",
|
package/build/opencode-plugin.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 '
|
|
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 '
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1449
|
-
"One
|
|
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
|
|
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
|
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
|
-
|
|
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
|
|
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);
|