feat: global install + CLI hook dispatcher

Replace `node ./node_modules/context-mode/hooks/...` with `context-mode hook <platform> <event>`.
All platforms now use `npm install -g context-mode` as prerequisite.
MCP server configs use `context-mode` directly instead of `npx -y context-mode`.
This ensures ctx-upgrade persists across sessions (no ephemeral npx cache).

- Add `hook` subcommand to CLI with HOOK_MAP dispatcher
- Update all adapter buildHookCommand() to use CLI dispatcher
- Update all adapter configureAllHooks() and generateHookConfig()
- Update all config templates (gemini-cli, vscode-copilot, opencode, codex)
- Update README install instructions for all platforms

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mert Koseoglu
2026-03-05 19:00:06 +03:00
parent 69bfd0f4ba
commit c56afc99e4
14 changed files with 152 additions and 108 deletions
+44 -23
View File
@@ -92,20 +92,25 @@ This gives you the 6 sandbox tools but without automatic routing. The model can
<details>
<summary><strong>Gemini CLI</strong></summary>
**Step 1 — Register the MCP server.** Add to `~/.gemini/settings.json`:
**Step 1 — Install globally:**
```bash
npm install -g context-mode
```
**Step 2 — Register the MCP server.** Add to `~/.gemini/settings.json`:
```json
{
"mcpServers": {
"context-mode": {
"command": "npx",
"args": ["-y", "context-mode"]
"command": "context-mode"
}
}
}
```
**Step 2 — Add hooks.** Without hooks, the model can ignore routing instructions and dump raw output into your context window. Hooks intercept every tool call and enforce sandbox routing programmatically — blocking `curl`, `wget`, and other data-heavy commands before they execute. Add to the same `~/.gemini/settings.json`:
**Step 3 — Add hooks.** Without hooks, the model can ignore routing instructions and dump raw output into your context window. Hooks intercept every tool call and enforce sandbox routing programmatically — blocking `curl`, `wget`, and other data-heavy commands before they execute. Add to the same `~/.gemini/settings.json`:
```json
{
@@ -113,26 +118,26 @@ This gives you the 6 sandbox tools but without automatic routing. The model can
"BeforeTool": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "node ./node_modules/context-mode/hooks/gemini-cli/beforetool.mjs" }]
"hooks": [{ "type": "command", "command": "context-mode hook gemini-cli beforetool" }]
}
],
"AfterTool": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "node ./node_modules/context-mode/hooks/gemini-cli/aftertool.mjs" }]
"hooks": [{ "type": "command", "command": "context-mode hook gemini-cli aftertool" }]
}
],
"SessionStart": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "node ./node_modules/context-mode/hooks/gemini-cli/sessionstart.mjs" }]
"hooks": [{ "type": "command", "command": "context-mode hook gemini-cli sessionstart" }]
}
]
}
}
```
**Step 3 — Restart Gemini CLI.** On first run, a `GEMINI.md` routing instructions file is auto-created in your project root. This works alongside hooks as a parallel enforcement layer — hooks block dangerous commands programmatically, while `GEMINI.md` teaches the model to prefer sandbox tools from the start.
**Step 4 — Restart Gemini CLI.** On first run, a `GEMINI.md` routing instructions file is auto-created in your project root. This works alongside hooks as a parallel enforcement layer — hooks block dangerous commands programmatically, while `GEMINI.md` teaches the model to prefer sandbox tools from the start.
> **Why hooks matter:** Without hooks, context-mode relies on `GEMINI.md` instructions alone (~60% compliance). The model sometimes follows them, but regularly runs raw `curl`, reads large files directly, or dumps unprocessed output into context — a single unrouted Playwright snapshot (56 KB) wipes out an entire session's savings. With hooks, every tool call is intercepted before execution — dangerous commands are blocked, and routing guidance is injected in real-time. This is the difference between ~60% and ~98% context savings.
@@ -143,38 +148,43 @@ Full hook config including PreCompress: [`configs/gemini-cli/settings.json`](con
<details>
<summary><strong>VS Code Copilot</strong></summary>
**Step 1 — Register the MCP server.** Create `.vscode/mcp.json` in your project root:
**Step 1 — Install globally:**
```bash
npm install -g context-mode
```
**Step 2 — Register the MCP server.** Create `.vscode/mcp.json` in your project root:
```json
{
"servers": {
"context-mode": {
"command": "npx",
"args": ["-y", "context-mode"]
"command": "context-mode"
}
}
}
```
**Step 2 — Add hooks.** Without hooks, the model can bypass routing and dump raw output into your context. Hooks intercept every tool call and enforce sandbox routing programmatically. Create `.github/hooks/context-mode.json`:
**Step 3 — Add hooks.** Without hooks, the model can bypass routing and dump raw output into your context. Hooks intercept every tool call and enforce sandbox routing programmatically. Create `.github/hooks/context-mode.json`:
```json
{
"hooks": {
"PreToolUse": [
{ "type": "command", "command": "node ./node_modules/context-mode/hooks/vscode-copilot/pretooluse.mjs" }
{ "type": "command", "command": "context-mode hook vscode-copilot pretooluse" }
],
"PostToolUse": [
{ "type": "command", "command": "node ./node_modules/context-mode/hooks/vscode-copilot/posttooluse.mjs" }
{ "type": "command", "command": "context-mode hook vscode-copilot posttooluse" }
],
"SessionStart": [
{ "type": "command", "command": "node ./node_modules/context-mode/hooks/vscode-copilot/sessionstart.mjs" }
{ "type": "command", "command": "context-mode hook vscode-copilot sessionstart" }
]
}
}
```
**Step 3 — Restart VS Code.** On first run, a `.github/copilot-instructions.md` routing instructions file is auto-created in your project. This works alongside hooks as a parallel enforcement layer — hooks intercept tool calls programmatically, while `copilot-instructions.md` guides the model's tool selection from session start.
**Step 4 — Restart VS Code.** On first run, a `.github/copilot-instructions.md` routing instructions file is auto-created in your project. This works alongside hooks as a parallel enforcement layer — hooks intercept tool calls programmatically, while `copilot-instructions.md` guides the model's tool selection from session start.
> **Why hooks matter:** Without hooks, `copilot-instructions.md` guides the model but can't block commands. A single unrouted Playwright snapshot (56 KB) or `gh issue list` (59 KB) wipes out minutes of context savings. With hooks, these calls are intercepted and redirected to the sandbox before they execute.
@@ -185,7 +195,13 @@ Full hook config including PreCompact: [`configs/vscode-copilot/hooks.json`](con
<details>
<summary><strong>OpenCode</strong></summary>
**Step 1 — Register the MCP server and plugin.** OpenCode uses a TypeScript plugin paradigm instead of JSON hooks. Add both the MCP server and the plugin to `opencode.json` in your project root:
**Step 1 — Install globally:**
```bash
npm install -g context-mode
```
**Step 2 — Register the MCP server and plugin.** OpenCode uses a TypeScript plugin paradigm instead of JSON hooks. Add both the MCP server and the plugin to `opencode.json` in your project root:
```json
{
@@ -193,7 +209,7 @@ Full hook config including PreCompact: [`configs/vscode-copilot/hooks.json`](con
"mcp": {
"context-mode": {
"type": "local",
"command": ["npx", "-y", "context-mode"]
"command": ["context-mode"]
}
},
"plugin": ["context-mode"]
@@ -202,7 +218,7 @@ Full hook config including PreCompact: [`configs/vscode-copilot/hooks.json`](con
The `mcp` entry gives you the 6 sandbox tools. The `plugin` entry enables hooks — OpenCode calls the plugin's TypeScript functions directly before and after each tool execution, blocking dangerous commands (like raw `curl`) and enforcing sandbox routing.
**Step 2 — Restart OpenCode.** On first run, an `AGENTS.md` routing instructions file is auto-created in your project root. This works alongside the plugin as a parallel enforcement layer — the plugin intercepts tool calls at runtime, while `AGENTS.md` guides the model's tool preferences from session start.
**Step 3 — Restart OpenCode.** On first run, an `AGENTS.md` routing instructions file is auto-created in your project root. This works alongside the plugin as a parallel enforcement layer — the plugin intercepts tool calls at runtime, while `AGENTS.md` guides the model's tool preferences from session start.
> **Why the plugin matters:** Without the `plugin` entry, context-mode has no way to intercept tool calls. The model can run raw `curl`, read large files directly, or dump unprocessed output into context — ignoring `AGENTS.md` instructions. With the plugin, `tool.execute.before` fires on every tool call and blocks or redirects data-heavy commands before they execute. The `experimental.session.compacting` hook builds and injects resume snapshots when the conversation compacts, preserving session state.
>
@@ -213,15 +229,20 @@ The `mcp` entry gives you the 6 sandbox tools. The `plugin` entry enables hooks
<details>
<summary><strong>Codex CLI</strong></summary>
**Step 1 — Register the MCP server.** Add to `~/.codex/config.toml`:
**Step 1 — Install globally:**
```bash
npm install -g context-mode
```
**Step 2 — Register the MCP server.** Add to `~/.codex/config.toml`:
```toml
[mcp_servers.context-mode]
command = "npx"
args = ["-y", "context-mode"]
command = "context-mode"
```
**Step 2 — Restart Codex CLI.** On first run, an `AGENTS.md` routing instructions file is auto-created in your project root. Codex CLI reads `AGENTS.md` automatically and learns to prefer context-mode sandbox tools.
**Step 3 — Restart Codex CLI.** On first run, an `AGENTS.md` routing instructions file is auto-created in your project root. Codex CLI reads `AGENTS.md` automatically and learns to prefer context-mode sandbox tools.
**About hooks:** Codex CLI does not support hooks — PRs [#2904](https://github.com/openai/codex/pull/2904) and [#9796](https://github.com/openai/codex/pull/9796) were closed without merge. The `AGENTS.md` routing instructions file is the only enforcement method (~60% compliance). The model receives the instructions at session start and sometimes follows them, but there is no programmatic interception — it can run raw `curl`, read large files, or bypass sandbox tools at any time.
+1 -2
View File
@@ -2,5 +2,4 @@
# Add this to ~/.codex/config.toml
[mcp_servers.context-mode]
command = "npx"
args = ["-y", "context-mode"]
command = "context-mode"
+1 -2
View File
@@ -1,8 +1,7 @@
{
"mcpServers": {
"context-mode": {
"command": "npx",
"args": ["-y", "context-mode"]
"command": "context-mode"
}
}
}
+4 -4
View File
@@ -7,7 +7,7 @@
"hooks": [
{
"type": "command",
"command": "node ./node_modules/context-mode/hooks/gemini-cli/beforetool.mjs"
"command": "context-mode hook gemini-cli beforetool"
}
]
}
@@ -18,7 +18,7 @@
"hooks": [
{
"type": "command",
"command": "node ./node_modules/context-mode/hooks/gemini-cli/aftertool.mjs"
"command": "context-mode hook gemini-cli aftertool"
}
]
}
@@ -29,7 +29,7 @@
"hooks": [
{
"type": "command",
"command": "node ./node_modules/context-mode/hooks/gemini-cli/precompress.mjs"
"command": "context-mode hook gemini-cli precompress"
}
]
}
@@ -40,7 +40,7 @@
"hooks": [
{
"type": "command",
"command": "node ./node_modules/context-mode/hooks/gemini-cli/sessionstart.mjs"
"command": "context-mode hook gemini-cli sessionstart"
}
]
}
+1 -1
View File
@@ -3,7 +3,7 @@
"mcp": {
"context-mode": {
"type": "local",
"command": ["npx", "-y", "context-mode"]
"command": ["context-mode"]
}
},
"plugin": ["context-mode"]
+4 -4
View File
@@ -1,16 +1,16 @@
{
"hooks": {
"PreToolUse": [
{ "type": "command", "command": "node ./node_modules/context-mode/hooks/vscode-copilot/pretooluse.mjs" }
{ "type": "command", "command": "context-mode hook vscode-copilot pretooluse" }
],
"PostToolUse": [
{ "type": "command", "command": "node ./node_modules/context-mode/hooks/vscode-copilot/posttooluse.mjs" }
{ "type": "command", "command": "context-mode hook vscode-copilot posttooluse" }
],
"PreCompact": [
{ "type": "command", "command": "node ./node_modules/context-mode/hooks/vscode-copilot/precompact.mjs" }
{ "type": "command", "command": "context-mode hook vscode-copilot precompact" }
],
"SessionStart": [
{ "type": "command", "command": "node ./node_modules/context-mode/hooks/vscode-copilot/sessionstart.mjs" }
{ "type": "command", "command": "context-mode hook vscode-copilot sessionstart" }
]
}
}
+5 -7
View File
@@ -97,12 +97,10 @@ export function isContextModeHook(
}
/**
* Build the hook command string for a given hook type and plugin root.
* Build the hook command string for a given hook type.
* Uses the CLI dispatcher: `context-mode hook claude-code <event>`
* Requires global install: `npm install -g context-mode`
*/
export function buildHookCommand(
hookType: HookType,
pluginRoot: string,
): string {
const scriptName = HOOK_SCRIPTS[hookType];
return `node ${pluginRoot}/hooks/${scriptName}`;
export function buildHookCommand(hookType: HookType): string {
return `context-mode hook claude-code ${hookType.toLowerCase()}`;
}
+6 -6
View File
@@ -305,7 +305,7 @@ export class ClaudeCodeAdapter implements HookAdapter {
check: "PreToolUse hook",
status: "fail",
message: "Could not read ~/.claude/settings.json",
fix: "npx context-mode upgrade",
fix: "context-mode upgrade",
});
return results;
}
@@ -326,14 +326,14 @@ export class ClaudeCodeAdapter implements HookAdapter {
message: hasHook
? "PreToolUse hook configured"
: "PreToolUse exists but does not point to pretooluse.mjs",
fix: hasHook ? undefined : "npx context-mode upgrade",
fix: hasHook ? undefined : "context-mode upgrade",
});
} else {
results.push({
check: "PreToolUse hook",
status: "fail",
message: "No PreToolUse hooks found",
fix: "npx context-mode upgrade",
fix: "context-mode upgrade",
});
}
@@ -351,14 +351,14 @@ export class ClaudeCodeAdapter implements HookAdapter {
message: hasHook
? "SessionStart hook configured"
: "SessionStart exists but does not point to sessionstart.mjs",
fix: hasHook ? undefined : "npx context-mode upgrade",
fix: hasHook ? undefined : "context-mode upgrade",
});
} else {
results.push({
check: "SessionStart hook",
status: "fail",
message: "No SessionStart hooks found",
fix: "npx context-mode upgrade",
fix: "context-mode upgrade",
});
}
@@ -474,7 +474,7 @@ export class ClaudeCodeAdapter implements HookAdapter {
];
for (const hookType of hookTypes) {
const command = buildHookCommand(hookType, pluginRoot);
const command = buildHookCommand(hookType);
if (hookType === HOOK_TYPES.PRE_TOOL_USE) {
const entry = {
+5 -7
View File
@@ -71,12 +71,10 @@ export function isContextModeHook(
}
/**
* Build the hook command string for a given hook type and plugin root.
* Build the hook command string for a given hook type.
* Uses the CLI dispatcher: `context-mode hook gemini-cli <event>`
* Requires global install: `npm install -g context-mode`
*/
export function buildHookCommand(
hookType: HookType,
pluginRoot: string,
): string {
const scriptName = HOOK_SCRIPTS[hookType];
return `node ${pluginRoot}/hooks/gemini-cli/${scriptName}`;
export function buildHookCommand(hookType: HookType): string {
return `context-mode hook gemini-cli ${hookType.toLowerCase()}`;
}
+14 -24
View File
@@ -226,9 +226,7 @@ export class GeminiCLIAdapter implements HookAdapter {
return join(this.getSessionDir(), `${hash}-events.md`);
}
generateHookConfig(pluginRoot: string): HookRegistration {
const hooksDir = join(pluginRoot, "hooks", "gemini-cli");
generateHookConfig(_pluginRoot: string): HookRegistration {
return {
[GEMINI_HOOK_NAMES.BEFORE_TOOL]: [
{
@@ -236,7 +234,7 @@ export class GeminiCLIAdapter implements HookAdapter {
hooks: [
{
type: "command",
command: `node ${hooksDir}/${GEMINI_HOOK_SCRIPTS[GEMINI_HOOK_NAMES.BEFORE_TOOL]}`,
command: `context-mode hook gemini-cli ${GEMINI_HOOK_NAMES.BEFORE_TOOL.toLowerCase()}`,
},
],
},
@@ -247,7 +245,7 @@ export class GeminiCLIAdapter implements HookAdapter {
hooks: [
{
type: "command",
command: `node ${hooksDir}/${GEMINI_HOOK_SCRIPTS[GEMINI_HOOK_NAMES.AFTER_TOOL]}`,
command: `context-mode hook gemini-cli ${GEMINI_HOOK_NAMES.AFTER_TOOL.toLowerCase()}`,
},
],
},
@@ -258,7 +256,7 @@ export class GeminiCLIAdapter implements HookAdapter {
hooks: [
{
type: "command",
command: `node ${hooksDir}/${GEMINI_HOOK_SCRIPTS[GEMINI_HOOK_NAMES.PRE_COMPRESS]}`,
command: `context-mode hook gemini-cli ${GEMINI_HOOK_NAMES.PRE_COMPRESS.toLowerCase()}`,
},
],
},
@@ -269,7 +267,7 @@ export class GeminiCLIAdapter implements HookAdapter {
hooks: [
{
type: "command",
command: `node ${hooksDir}/${GEMINI_HOOK_SCRIPTS[GEMINI_HOOK_NAMES.SESSION_START]}`,
command: `context-mode hook gemini-cli ${GEMINI_HOOK_NAMES.SESSION_START.toLowerCase()}`,
},
],
},
@@ -307,7 +305,7 @@ export class GeminiCLIAdapter implements HookAdapter {
check: "BeforeTool hook",
status: "fail",
message: "Could not read ~/.gemini/settings.json",
fix: "npx context-mode upgrade --platform gemini-cli",
fix: "context-mode upgrade",
});
return results;
}
@@ -328,14 +326,14 @@ export class GeminiCLIAdapter implements HookAdapter {
message: hasHook
? "BeforeTool hook configured"
: "BeforeTool exists but does not point to context-mode",
fix: hasHook ? undefined : "npx context-mode upgrade --platform gemini-cli",
fix: hasHook ? undefined : "context-mode upgrade",
});
} else {
results.push({
check: "BeforeTool hook",
status: "fail",
message: "No BeforeTool hooks found",
fix: "npx context-mode upgrade --platform gemini-cli",
fix: "context-mode upgrade",
});
}
@@ -353,14 +351,14 @@ export class GeminiCLIAdapter implements HookAdapter {
message: hasHook
? "SessionStart hook configured"
: "SessionStart exists but does not point to context-mode",
fix: hasHook ? undefined : "npx context-mode upgrade --platform gemini-cli",
fix: hasHook ? undefined : "context-mode upgrade",
});
} else {
results.push({
check: "SessionStart hook",
status: "fail",
message: "No SessionStart hooks found",
fix: "npx context-mode upgrade --platform gemini-cli",
fix: "context-mode upgrade",
});
}
@@ -427,28 +425,20 @@ export class GeminiCLIAdapter implements HookAdapter {
// ── Upgrade ────────────────────────────────────────────
configureAllHooks(pluginRoot: string): string[] {
configureAllHooks(_pluginRoot: string): string[] {
const settings = this.readSettings() ?? {};
const hooks = (settings.hooks ?? {}) as Record<string, unknown>;
const changes: string[] = [];
const hooksDir = join(pluginRoot, "hooks", "gemini-cli");
const hookConfigs: Array<{
name: string;
script: string;
}> = [
{
name: GEMINI_HOOK_NAMES.BEFORE_TOOL,
script: GEMINI_HOOK_SCRIPTS[GEMINI_HOOK_NAMES.BEFORE_TOOL],
},
{
name: GEMINI_HOOK_NAMES.SESSION_START,
script: GEMINI_HOOK_SCRIPTS[GEMINI_HOOK_NAMES.SESSION_START],
},
{ name: GEMINI_HOOK_NAMES.BEFORE_TOOL },
{ name: GEMINI_HOOK_NAMES.SESSION_START },
];
for (const config of hookConfigs) {
const command = `node ${hooksDir}/${config.script}`;
const command = `context-mode hook gemini-cli ${config.name.toLowerCase()}`;
const entry = {
matcher: "",
hooks: [{ type: "command", command }],
+4 -4
View File
@@ -273,7 +273,7 @@ export class OpenCodeAdapter implements HookAdapter {
check: "Plugin configuration",
status: "fail",
message: "Could not read opencode.json",
fix: "npx context-mode upgrade --platform opencode",
fix: "context-mode upgrade",
});
return results;
}
@@ -290,14 +290,14 @@ export class OpenCodeAdapter implements HookAdapter {
: "context-mode not found in plugin array",
fix: hasPlugin
? undefined
: "npx context-mode upgrade --platform opencode",
: "context-mode upgrade",
});
} else {
results.push({
check: "Plugin registration",
status: "fail",
message: "No plugin array found in opencode.json",
fix: "npx context-mode upgrade --platform opencode",
fix: "context-mode upgrade",
});
}
@@ -338,7 +338,7 @@ export class OpenCodeAdapter implements HookAdapter {
check: "Plugin registration",
status: "fail",
message: "context-mode not found in opencode.json plugin array",
fix: "npx context-mode upgrade --platform opencode",
fix: "context-mode upgrade",
};
}
+5 -6
View File
@@ -78,15 +78,14 @@ export function isContextModeHook(
}
/**
* Build the hook command string for a given hook type and plugin root.
* Build the hook command string for a given hook type.
* Uses the CLI dispatcher: `context-mode hook vscode-copilot <event>`
* Requires global install: `npm install -g context-mode`
*/
export function buildHookCommand(
hookType: HookType,
pluginRoot: string,
): string {
export function buildHookCommand(hookType: HookType): string {
const scriptName = HOOK_SCRIPTS[hookType];
if (!scriptName) {
throw new Error(`No script defined for hook type: ${hookType}`);
}
return `node ${pluginRoot}/hooks/vscode-copilot/${scriptName}`;
return `context-mode hook vscode-copilot ${hookType.toLowerCase()}`;
}
+11 -14
View File
@@ -250,9 +250,7 @@ export class VSCodeCopilotAdapter implements HookAdapter {
return join(this.getSessionDir(), `${hash}-events.md`);
}
generateHookConfig(pluginRoot: string): HookRegistration {
const hooksDir = join(pluginRoot, "hooks", "vscode-copilot");
generateHookConfig(_pluginRoot: string): HookRegistration {
return {
[VSCODE_HOOK_NAMES.PRE_TOOL_USE]: [
{
@@ -260,7 +258,7 @@ export class VSCodeCopilotAdapter implements HookAdapter {
hooks: [
{
type: "command",
command: `node ${hooksDir}/${VSCODE_HOOK_SCRIPTS[VSCODE_HOOK_NAMES.PRE_TOOL_USE]}`,
command: `context-mode hook vscode-copilot ${VSCODE_HOOK_NAMES.PRE_TOOL_USE.toLowerCase()}`,
},
],
},
@@ -271,7 +269,7 @@ export class VSCodeCopilotAdapter implements HookAdapter {
hooks: [
{
type: "command",
command: `node ${hooksDir}/${VSCODE_HOOK_SCRIPTS[VSCODE_HOOK_NAMES.POST_TOOL_USE]}`,
command: `context-mode hook vscode-copilot ${VSCODE_HOOK_NAMES.POST_TOOL_USE.toLowerCase()}`,
},
],
},
@@ -282,7 +280,7 @@ export class VSCodeCopilotAdapter implements HookAdapter {
hooks: [
{
type: "command",
command: `node ${hooksDir}/${VSCODE_HOOK_SCRIPTS[VSCODE_HOOK_NAMES.PRE_COMPACT]}`,
command: `context-mode hook vscode-copilot ${VSCODE_HOOK_NAMES.PRE_COMPACT.toLowerCase()}`,
},
],
},
@@ -293,7 +291,7 @@ export class VSCodeCopilotAdapter implements HookAdapter {
hooks: [
{
type: "command",
command: `node ${hooksDir}/${VSCODE_HOOK_SCRIPTS[VSCODE_HOOK_NAMES.SESSION_START]}`,
command: `context-mode hook vscode-copilot ${VSCODE_HOOK_NAMES.SESSION_START.toLowerCase()}`,
},
],
},
@@ -343,7 +341,7 @@ export class VSCodeCopilotAdapter implements HookAdapter {
check: "Hooks directory",
status: "fail",
message: ".github/hooks/ directory not found",
fix: "npx context-mode upgrade --platform vscode-copilot",
fix: "context-mode upgrade",
});
return results;
}
@@ -367,7 +365,7 @@ export class VSCodeCopilotAdapter implements HookAdapter {
check: "PreToolUse hook",
status: "fail",
message: "PreToolUse not found in context-mode.json",
fix: "npx context-mode upgrade --platform vscode-copilot",
fix: "context-mode upgrade",
});
}
@@ -383,7 +381,7 @@ export class VSCodeCopilotAdapter implements HookAdapter {
check: "SessionStart hook",
status: "fail",
message: "SessionStart not found in context-mode.json",
fix: "npx context-mode upgrade --platform vscode-copilot",
fix: "context-mode upgrade",
});
}
} catch {
@@ -391,7 +389,7 @@ export class VSCodeCopilotAdapter implements HookAdapter {
check: "Hook configuration",
status: "fail",
message: "Could not read .github/hooks/context-mode.json",
fix: "npx context-mode upgrade --platform vscode-copilot",
fix: "context-mode upgrade",
});
}
@@ -484,9 +482,8 @@ export class VSCodeCopilotAdapter implements HookAdapter {
// ── Upgrade ────────────────────────────────────────────
configureAllHooks(pluginRoot: string): string[] {
configureAllHooks(_pluginRoot: string): string[] {
const changes: string[] = [];
const hooksDir = join(pluginRoot, "hooks", "vscode-copilot");
const hookConfig: Record<string, unknown> = { hooks: {} };
const hooks = hookConfig.hooks as Record<string, unknown>;
@@ -507,7 +504,7 @@ export class VSCodeCopilotAdapter implements HookAdapter {
hooks: [
{
type: "command",
command: `node ${hooksDir}/${script}`,
command: `context-mode hook vscode-copilot ${hookType.toLowerCase()}`,
},
],
},
+47 -4
View File
@@ -3,9 +3,10 @@
* context-mode CLI
*
* Usage:
* context-mode Start MCP server (stdio)
* context-mode doctor Diagnose runtime issues, hooks, FTS5, version
* context-mode upgrade Fix hooks, permissions, and settings
* context-mode Start MCP server (stdio)
* context-mode doctor Diagnose runtime issues, hooks, FTS5, version
* context-mode upgrade Fix hooks, permissions, and settings
* context-mode hook <platform> <event> Dispatch a hook script (used by platform hook configs)
*
* Platform auto-detection: CLI detects which platform is running
* (Claude Code, Gemini CLI, OpenCode, etc.) and uses the appropriate adapter.
@@ -15,7 +16,7 @@ import * as p from "@clack/prompts";
import color from "picocolors";
import { execSync } from "node:child_process";
import { readFileSync, cpSync, accessSync, readdirSync, rmSync, constants } from "node:fs";
import { resolve, dirname } from "node:path";
import { resolve, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
detectRuntimes,
@@ -28,12 +29,54 @@ import {
import { detectPlatform, getAdapter } from "./adapters/detect.js";
import type { HookAdapter } from "./adapters/types.js";
/* -------------------------------------------------------
* Hook dispatcher `context-mode hook <platform> <event>`
* ------------------------------------------------------- */
const HOOK_MAP: Record<string, Record<string, string>> = {
"claude-code": {
pretooluse: "hooks/pretooluse.mjs",
posttooluse: "hooks/posttooluse.mjs",
precompact: "hooks/precompact.mjs",
sessionstart: "hooks/sessionstart.mjs",
userpromptsubmit: "hooks/userpromptsubmit.mjs",
},
"gemini-cli": {
beforetool: "hooks/gemini-cli/beforetool.mjs",
aftertool: "hooks/gemini-cli/aftertool.mjs",
precompress: "hooks/gemini-cli/precompress.mjs",
sessionstart: "hooks/gemini-cli/sessionstart.mjs",
},
"vscode-copilot": {
pretooluse: "hooks/vscode-copilot/pretooluse.mjs",
posttooluse: "hooks/vscode-copilot/posttooluse.mjs",
precompact: "hooks/vscode-copilot/precompact.mjs",
sessionstart: "hooks/vscode-copilot/sessionstart.mjs",
},
};
async function hookDispatch(platform: string, event: string): Promise<void> {
const scriptPath = HOOK_MAP[platform]?.[event];
if (!scriptPath) {
console.error(`Unknown hook: ${platform}/${event}`);
process.exit(1);
}
const pluginRoot = getPluginRoot();
await import(join(pluginRoot, scriptPath));
}
/* -------------------------------------------------------
* Entry point
* ------------------------------------------------------- */
const args = process.argv.slice(2);
if (args[0] === "doctor") {
doctor().then((code) => process.exit(code));
} else if (args[0] === "upgrade") {
upgrade();
} else if (args[0] === "hook") {
hookDispatch(args[1], args[2]);
} else {
// Default: start MCP server
import("./server.js");