context-mode 0.9.21 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/.claude-plugin/hooks/hooks.json +46 -4
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/.claude-plugin/plugin.json +4 -4
  4. package/README.md +377 -191
  5. package/build/adapters/claude-code/config.d.ts +8 -0
  6. package/build/adapters/claude-code/config.js +8 -0
  7. package/build/adapters/claude-code/hooks.d.ts +53 -0
  8. package/build/adapters/claude-code/hooks.js +88 -0
  9. package/build/adapters/claude-code/index.d.ts +50 -0
  10. package/build/adapters/claude-code/index.js +523 -0
  11. package/build/adapters/codex/config.d.ts +8 -0
  12. package/build/adapters/codex/config.js +8 -0
  13. package/build/adapters/codex/hooks.d.ts +21 -0
  14. package/build/adapters/codex/hooks.js +27 -0
  15. package/build/adapters/codex/index.d.ts +44 -0
  16. package/build/adapters/codex/index.js +223 -0
  17. package/build/adapters/detect.d.ts +26 -0
  18. package/build/adapters/detect.js +131 -0
  19. package/build/adapters/gemini-cli/config.d.ts +8 -0
  20. package/build/adapters/gemini-cli/config.js +8 -0
  21. package/build/adapters/gemini-cli/hooks.d.ts +44 -0
  22. package/build/adapters/gemini-cli/hooks.js +64 -0
  23. package/build/adapters/gemini-cli/index.d.ts +57 -0
  24. package/build/adapters/gemini-cli/index.js +468 -0
  25. package/build/adapters/opencode/config.d.ts +8 -0
  26. package/build/adapters/opencode/config.js +8 -0
  27. package/build/adapters/opencode/hooks.d.ts +38 -0
  28. package/build/adapters/opencode/hooks.js +50 -0
  29. package/build/adapters/opencode/index.d.ts +52 -0
  30. package/build/adapters/opencode/index.js +386 -0
  31. package/build/adapters/types.d.ts +218 -0
  32. package/build/adapters/types.js +13 -0
  33. package/build/adapters/vscode-copilot/config.d.ts +8 -0
  34. package/build/adapters/vscode-copilot/config.js +8 -0
  35. package/build/adapters/vscode-copilot/hooks.d.ts +49 -0
  36. package/build/adapters/vscode-copilot/hooks.js +76 -0
  37. package/build/adapters/vscode-copilot/index.d.ts +58 -0
  38. package/build/adapters/vscode-copilot/index.js +512 -0
  39. package/build/cli.d.ts +9 -6
  40. package/build/cli.js +133 -423
  41. package/build/db-base.d.ts +84 -0
  42. package/build/db-base.js +128 -0
  43. package/build/executor.d.ts +6 -7
  44. package/build/executor.js +111 -51
  45. package/build/opencode-plugin.d.ts +37 -0
  46. package/build/opencode-plugin.js +118 -0
  47. package/build/runtime.js +1 -1
  48. package/build/server.js +436 -117
  49. package/build/session/db.d.ts +110 -0
  50. package/build/session/db.js +285 -0
  51. package/build/session/extract.d.ts +51 -0
  52. package/build/session/extract.js +407 -0
  53. package/build/session/snapshot.d.ts +70 -0
  54. package/build/session/snapshot.js +309 -0
  55. package/build/store.d.ts +4 -22
  56. package/build/store.js +67 -55
  57. package/build/truncate.d.ts +59 -0
  58. package/build/truncate.js +157 -0
  59. package/build/types.d.ts +101 -0
  60. package/build/types.js +20 -0
  61. package/configs/claude-code/CLAUDE.md +62 -0
  62. package/configs/codex/AGENTS.md +58 -0
  63. package/configs/codex/config.toml +5 -0
  64. package/configs/gemini-cli/GEMINI.md +58 -0
  65. package/configs/gemini-cli/mcp.json +7 -0
  66. package/configs/gemini-cli/settings.json +49 -0
  67. package/configs/opencode/AGENTS.md +58 -0
  68. package/configs/opencode/opencode.json +10 -0
  69. package/configs/vscode-copilot/copilot-instructions.md +58 -0
  70. package/configs/vscode-copilot/hooks.json +16 -0
  71. package/configs/vscode-copilot/mcp.json +8 -0
  72. package/hooks/core/formatters.mjs +86 -0
  73. package/hooks/core/routing.mjs +262 -0
  74. package/hooks/core/stdin.mjs +19 -0
  75. package/hooks/formatters/claude-code.mjs +57 -0
  76. package/hooks/formatters/gemini-cli.mjs +55 -0
  77. package/hooks/formatters/vscode-copilot.mjs +55 -0
  78. package/hooks/gemini-cli/aftertool.mjs +58 -0
  79. package/hooks/gemini-cli/beforetool.mjs +25 -0
  80. package/hooks/gemini-cli/precompress.mjs +51 -0
  81. package/hooks/gemini-cli/sessionstart.mjs +117 -0
  82. package/hooks/hooks.json +46 -4
  83. package/hooks/posttooluse.mjs +53 -0
  84. package/hooks/precompact.mjs +55 -0
  85. package/hooks/pretooluse.mjs +23 -266
  86. package/hooks/routing-block.mjs +19 -6
  87. package/hooks/session-directive.mjs +353 -0
  88. package/hooks/session-helpers.mjs +112 -0
  89. package/hooks/sessionstart.mjs +123 -16
  90. package/hooks/userpromptsubmit.mjs +58 -0
  91. package/hooks/vscode-copilot/posttooluse.mjs +58 -0
  92. package/hooks/vscode-copilot/precompact.mjs +51 -0
  93. package/hooks/vscode-copilot/pretooluse.mjs +25 -0
  94. package/hooks/vscode-copilot/sessionstart.mjs +115 -0
  95. package/package.json +20 -17
  96. package/skills/context-mode/SKILL.md +49 -49
  97. package/skills/{doctor → ctx-doctor}/SKILL.md +3 -3
  98. package/skills/{stats → ctx-stats}/SKILL.md +3 -3
  99. package/skills/{upgrade → ctx-upgrade}/SKILL.md +3 -3
  100. package/start.mjs +47 -0
  101. package/hooks/pretooluse.sh +0 -147
  102. package/server.bundle.mjs +0 -341
@@ -0,0 +1,76 @@
1
+ /**
2
+ * adapters/vscode-copilot/hooks — VS Code Copilot hook definitions and matchers.
3
+ *
4
+ * Defines the hook types, matchers, and registration format specific to
5
+ * VS Code Copilot's hook system. This module is used by:
6
+ * - CLI setup/upgrade commands (to configure hooks)
7
+ * - Doctor command (to validate hook configuration)
8
+ * - Hook config generation
9
+ *
10
+ * VS Code Copilot hook system reference:
11
+ * - Hooks are registered in .github/hooks/*.json
12
+ * - Hook names: PreToolUse, PostToolUse, PreCompact, SessionStart (PascalCase)
13
+ * - Additional hooks: Stop, SubagentStart, SubagentStop (unique to VS Code)
14
+ * - CRITICAL: matchers are parsed but IGNORED (all hooks fire on all tools)
15
+ * - Input: JSON on stdin
16
+ * - Output: JSON on stdout (or empty for passthrough)
17
+ * - Preview status — API may change
18
+ */
19
+ // ─────────────────────────────────────────────────────────
20
+ // Hook type constants
21
+ // ─────────────────────────────────────────────────────────
22
+ /** VS Code Copilot hook types. */
23
+ export const HOOK_TYPES = {
24
+ PRE_TOOL_USE: "PreToolUse",
25
+ POST_TOOL_USE: "PostToolUse",
26
+ PRE_COMPACT: "PreCompact",
27
+ SESSION_START: "SessionStart",
28
+ // Additional hooks unique to VS Code Copilot
29
+ STOP: "Stop",
30
+ SUBAGENT_START: "SubagentStart",
31
+ SUBAGENT_STOP: "SubagentStop",
32
+ };
33
+ // ─────────────────────────────────────────────────────────
34
+ // Hook script file names
35
+ // ─────────────────────────────────────────────────────────
36
+ /** Map of hook types to their script file names. */
37
+ export const HOOK_SCRIPTS = {
38
+ [HOOK_TYPES.PRE_TOOL_USE]: "pretooluse.mjs",
39
+ [HOOK_TYPES.POST_TOOL_USE]: "posttooluse.mjs",
40
+ [HOOK_TYPES.PRE_COMPACT]: "precompact.mjs",
41
+ [HOOK_TYPES.SESSION_START]: "sessionstart.mjs",
42
+ };
43
+ // ─────────────────────────────────────────────────────────
44
+ // Hook validation
45
+ // ─────────────────────────────────────────────────────────
46
+ /** Required hooks that must be configured for context-mode to function. */
47
+ export const REQUIRED_HOOKS = [
48
+ HOOK_TYPES.PRE_TOOL_USE,
49
+ HOOK_TYPES.SESSION_START,
50
+ ];
51
+ /** Optional hooks that enhance functionality but aren't critical. */
52
+ export const OPTIONAL_HOOKS = [
53
+ HOOK_TYPES.POST_TOOL_USE,
54
+ HOOK_TYPES.PRE_COMPACT,
55
+ ];
56
+ /**
57
+ * Check if a hook entry points to a context-mode hook script.
58
+ */
59
+ export function isContextModeHook(entry, hookType) {
60
+ const scriptName = HOOK_SCRIPTS[hookType];
61
+ if (!scriptName)
62
+ return false;
63
+ return (entry.hooks?.some((h) => h.command?.includes(scriptName)) ?? false);
64
+ }
65
+ /**
66
+ * Build the hook command string for a given hook type.
67
+ * Uses the CLI dispatcher: `context-mode hook vscode-copilot <event>`
68
+ * Requires global install: `npm install -g context-mode`
69
+ */
70
+ export function buildHookCommand(hookType) {
71
+ const scriptName = HOOK_SCRIPTS[hookType];
72
+ if (!scriptName) {
73
+ throw new Error(`No script defined for hook type: ${hookType}`);
74
+ }
75
+ return `context-mode hook vscode-copilot ${hookType.toLowerCase()}`;
76
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * adapters/vscode-copilot — VS Code Copilot platform adapter.
3
+ *
4
+ * Implements HookAdapter for VS Code Copilot's JSON stdin/stdout hook paradigm.
5
+ *
6
+ * VS Code Copilot hook specifics:
7
+ * - I/O: JSON on stdin, JSON on stdout (same paradigm as Claude Code)
8
+ * - Hook names: PreToolUse, PostToolUse, PreCompact, SessionStart (PascalCase)
9
+ * - Additional hooks: Stop, SubagentStart, SubagentStop (unique to VS Code)
10
+ * - Arg modification: `updatedInput` in hookSpecificOutput wrapper (NOT flat)
11
+ * - Blocking: `permissionDecision: "deny"` (same as Claude Code)
12
+ * - Output modification: `additionalContext` in hookSpecificOutput,
13
+ * `decision: "block"` + reason
14
+ * - Tool input fields: tool_name, tool_input (snake_case, same as Claude Code)
15
+ * - But tool input PROPERTY names are camelCase (filePath not file_path)
16
+ * - Session ID: sessionId (camelCase, NOT session_id)
17
+ * - MCP tool prefix: f1e_ (not mcp__server__tool)
18
+ * - CRITICAL: matchers are parsed but IGNORED (all hooks fire on all tools)
19
+ * - Config: .github/hooks/*.json (primary), also reads .claude/settings.json
20
+ * - Env detection: VSCODE_PID, TERM_PROGRAM=vscode
21
+ * - Session dir: ~/.vscode/context-mode/sessions/ (fallback)
22
+ * - Preview status — API may change
23
+ */
24
+ import type { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, PreToolUseEvent, PostToolUseEvent, PreCompactEvent, SessionStartEvent, PreToolUseResponse, PostToolUseResponse, PreCompactResponse, SessionStartResponse, HookRegistration, RoutingInstructionsConfig } from "../types.js";
25
+ export declare class VSCodeCopilotAdapter implements HookAdapter {
26
+ readonly name = "VS Code Copilot";
27
+ readonly paradigm: HookParadigm;
28
+ readonly capabilities: PlatformCapabilities;
29
+ parsePreToolUseInput(raw: unknown): PreToolUseEvent;
30
+ parsePostToolUseInput(raw: unknown): PostToolUseEvent;
31
+ parsePreCompactInput(raw: unknown): PreCompactEvent;
32
+ parseSessionStartInput(raw: unknown): SessionStartEvent;
33
+ formatPreToolUseResponse(response: PreToolUseResponse): unknown;
34
+ formatPostToolUseResponse(response: PostToolUseResponse): unknown;
35
+ formatPreCompactResponse(response: PreCompactResponse): unknown;
36
+ formatSessionStartResponse(response: SessionStartResponse): unknown;
37
+ getSettingsPath(): string;
38
+ getSessionDir(): string;
39
+ getSessionDBPath(projectDir: string): string;
40
+ getSessionEventsPath(projectDir: string): string;
41
+ generateHookConfig(_pluginRoot: string): HookRegistration;
42
+ readSettings(): Record<string, unknown> | null;
43
+ writeSettings(settings: Record<string, unknown>): void;
44
+ validateHooks(pluginRoot: string): DiagnosticResult[];
45
+ checkPluginRegistration(): DiagnosticResult;
46
+ getInstalledVersion(): string;
47
+ configureAllHooks(_pluginRoot: string): string[];
48
+ backupSettings(): string | null;
49
+ setHookPermissions(pluginRoot: string): string[];
50
+ updatePluginRegistry(_pluginRoot: string, _version: string): void;
51
+ getRoutingInstructionsConfig(): RoutingInstructionsConfig;
52
+ writeRoutingInstructions(projectDir: string, pluginRoot: string): string | null;
53
+ /**
54
+ * Extract session ID from VS Code Copilot hook input.
55
+ * VS Code Copilot uses camelCase sessionId (NOT session_id).
56
+ */
57
+ private extractSessionId;
58
+ }
@@ -0,0 +1,512 @@
1
+ /**
2
+ * adapters/vscode-copilot — VS Code Copilot platform adapter.
3
+ *
4
+ * Implements HookAdapter for VS Code Copilot's JSON stdin/stdout hook paradigm.
5
+ *
6
+ * VS Code Copilot hook specifics:
7
+ * - I/O: JSON on stdin, JSON on stdout (same paradigm as Claude Code)
8
+ * - Hook names: PreToolUse, PostToolUse, PreCompact, SessionStart (PascalCase)
9
+ * - Additional hooks: Stop, SubagentStart, SubagentStop (unique to VS Code)
10
+ * - Arg modification: `updatedInput` in hookSpecificOutput wrapper (NOT flat)
11
+ * - Blocking: `permissionDecision: "deny"` (same as Claude Code)
12
+ * - Output modification: `additionalContext` in hookSpecificOutput,
13
+ * `decision: "block"` + reason
14
+ * - Tool input fields: tool_name, tool_input (snake_case, same as Claude Code)
15
+ * - But tool input PROPERTY names are camelCase (filePath not file_path)
16
+ * - Session ID: sessionId (camelCase, NOT session_id)
17
+ * - MCP tool prefix: f1e_ (not mcp__server__tool)
18
+ * - CRITICAL: matchers are parsed but IGNORED (all hooks fire on all tools)
19
+ * - Config: .github/hooks/*.json (primary), also reads .claude/settings.json
20
+ * - Env detection: VSCODE_PID, TERM_PROGRAM=vscode
21
+ * - Session dir: ~/.vscode/context-mode/sessions/ (fallback)
22
+ * - Preview status — API may change
23
+ */
24
+ import { createHash } from "node:crypto";
25
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, accessSync, existsSync, chmodSync, constants, } from "node:fs";
26
+ import { resolve, join } from "node:path";
27
+ import { homedir } from "node:os";
28
+ // ─────────────────────────────────────────────────────────
29
+ // Hook constants (re-exported from hooks.ts)
30
+ // ─────────────────────────────────────────────────────────
31
+ import { HOOK_TYPES as VSCODE_HOOK_NAMES, HOOK_SCRIPTS as VSCODE_HOOK_SCRIPTS, } from "./hooks.js";
32
+ // ─────────────────────────────────────────────────────────
33
+ // Adapter implementation
34
+ // ─────────────────────────────────────────────────────────
35
+ export class VSCodeCopilotAdapter {
36
+ name = "VS Code Copilot";
37
+ paradigm = "json-stdio";
38
+ capabilities = {
39
+ preToolUse: true,
40
+ postToolUse: true,
41
+ preCompact: true,
42
+ sessionStart: true,
43
+ canModifyArgs: true,
44
+ canModifyOutput: true,
45
+ canInjectSessionContext: true,
46
+ };
47
+ // ── Input parsing ──────────────────────────────────────
48
+ parsePreToolUseInput(raw) {
49
+ const input = raw;
50
+ return {
51
+ toolName: input.tool_name ?? "",
52
+ toolInput: input.tool_input ?? {},
53
+ sessionId: this.extractSessionId(input),
54
+ projectDir: process.env.CLAUDE_PROJECT_DIR || process.cwd(),
55
+ raw,
56
+ };
57
+ }
58
+ parsePostToolUseInput(raw) {
59
+ const input = raw;
60
+ return {
61
+ toolName: input.tool_name ?? "",
62
+ toolInput: input.tool_input ?? {},
63
+ toolOutput: input.tool_output,
64
+ isError: input.is_error,
65
+ sessionId: this.extractSessionId(input),
66
+ projectDir: process.env.CLAUDE_PROJECT_DIR || process.cwd(),
67
+ raw,
68
+ };
69
+ }
70
+ parsePreCompactInput(raw) {
71
+ const input = raw;
72
+ return {
73
+ sessionId: this.extractSessionId(input),
74
+ projectDir: process.env.CLAUDE_PROJECT_DIR || process.cwd(),
75
+ raw,
76
+ };
77
+ }
78
+ parseSessionStartInput(raw) {
79
+ const input = raw;
80
+ const rawSource = input.source ?? "startup";
81
+ let source;
82
+ switch (rawSource) {
83
+ case "compact":
84
+ source = "compact";
85
+ break;
86
+ case "resume":
87
+ source = "resume";
88
+ break;
89
+ case "clear":
90
+ source = "clear";
91
+ break;
92
+ default:
93
+ source = "startup";
94
+ }
95
+ return {
96
+ sessionId: this.extractSessionId(input),
97
+ source,
98
+ projectDir: process.env.CLAUDE_PROJECT_DIR || process.cwd(),
99
+ raw,
100
+ };
101
+ }
102
+ // ── Response formatting ────────────────────────────────
103
+ formatPreToolUseResponse(response) {
104
+ if (response.decision === "deny") {
105
+ return {
106
+ permissionDecision: "deny",
107
+ reason: response.reason ?? "Blocked by context-mode hook",
108
+ };
109
+ }
110
+ if (response.decision === "modify" && response.updatedInput) {
111
+ // VS Code Copilot: updatedInput is wrapped in hookSpecificOutput
112
+ return {
113
+ hookSpecificOutput: {
114
+ hookEventName: VSCODE_HOOK_NAMES.PRE_TOOL_USE,
115
+ updatedInput: response.updatedInput,
116
+ },
117
+ };
118
+ }
119
+ if (response.decision === "context" && response.additionalContext) {
120
+ // VS Code Copilot: inject additionalContext via hookSpecificOutput
121
+ return {
122
+ hookSpecificOutput: {
123
+ hookEventName: VSCODE_HOOK_NAMES.PRE_TOOL_USE,
124
+ additionalContext: response.additionalContext,
125
+ },
126
+ };
127
+ }
128
+ if (response.decision === "ask") {
129
+ // VS Code Copilot: use deny to force user attention (no native "ask")
130
+ return {
131
+ permissionDecision: "deny",
132
+ reason: response.reason ?? "Action requires user confirmation (security policy)",
133
+ };
134
+ }
135
+ // "allow" — return undefined for passthrough
136
+ return undefined;
137
+ }
138
+ formatPostToolUseResponse(response) {
139
+ if (response.updatedOutput) {
140
+ // VS Code Copilot: decision "block" + reason for output replacement
141
+ return {
142
+ hookSpecificOutput: {
143
+ hookEventName: VSCODE_HOOK_NAMES.POST_TOOL_USE,
144
+ decision: "block",
145
+ reason: response.updatedOutput,
146
+ },
147
+ };
148
+ }
149
+ if (response.additionalContext) {
150
+ return {
151
+ hookSpecificOutput: {
152
+ hookEventName: VSCODE_HOOK_NAMES.POST_TOOL_USE,
153
+ additionalContext: response.additionalContext,
154
+ },
155
+ };
156
+ }
157
+ return undefined;
158
+ }
159
+ formatPreCompactResponse(response) {
160
+ // VS Code Copilot: stdout content on exit 0 is injected as context
161
+ return response.context ?? "";
162
+ }
163
+ formatSessionStartResponse(response) {
164
+ // VS Code Copilot: stdout content is injected as additional context
165
+ return response.context ?? "";
166
+ }
167
+ // ── Configuration ──────────────────────────────────────
168
+ getSettingsPath() {
169
+ // VS Code Copilot primarily uses .github/hooks/*.json
170
+ // but also reads .claude/settings.json
171
+ return resolve(".github", "hooks", "context-mode.json");
172
+ }
173
+ getSessionDir() {
174
+ // Prefer .github/context-mode/sessions/ if .github exists,
175
+ // otherwise fall back to ~/.vscode/context-mode/sessions/
176
+ const githubDir = resolve(".github", "context-mode", "sessions");
177
+ const fallbackDir = join(homedir(), ".vscode", "context-mode", "sessions");
178
+ const dir = existsSync(resolve(".github")) ? githubDir : fallbackDir;
179
+ mkdirSync(dir, { recursive: true });
180
+ return dir;
181
+ }
182
+ getSessionDBPath(projectDir) {
183
+ const hash = createHash("sha256")
184
+ .update(projectDir)
185
+ .digest("hex")
186
+ .slice(0, 16);
187
+ return join(this.getSessionDir(), `${hash}.db`);
188
+ }
189
+ getSessionEventsPath(projectDir) {
190
+ const hash = createHash("sha256")
191
+ .update(projectDir)
192
+ .digest("hex")
193
+ .slice(0, 16);
194
+ return join(this.getSessionDir(), `${hash}-events.md`);
195
+ }
196
+ generateHookConfig(_pluginRoot) {
197
+ return {
198
+ [VSCODE_HOOK_NAMES.PRE_TOOL_USE]: [
199
+ {
200
+ matcher: "",
201
+ hooks: [
202
+ {
203
+ type: "command",
204
+ command: `context-mode hook vscode-copilot ${VSCODE_HOOK_NAMES.PRE_TOOL_USE.toLowerCase()}`,
205
+ },
206
+ ],
207
+ },
208
+ ],
209
+ [VSCODE_HOOK_NAMES.POST_TOOL_USE]: [
210
+ {
211
+ matcher: "",
212
+ hooks: [
213
+ {
214
+ type: "command",
215
+ command: `context-mode hook vscode-copilot ${VSCODE_HOOK_NAMES.POST_TOOL_USE.toLowerCase()}`,
216
+ },
217
+ ],
218
+ },
219
+ ],
220
+ [VSCODE_HOOK_NAMES.PRE_COMPACT]: [
221
+ {
222
+ matcher: "",
223
+ hooks: [
224
+ {
225
+ type: "command",
226
+ command: `context-mode hook vscode-copilot ${VSCODE_HOOK_NAMES.PRE_COMPACT.toLowerCase()}`,
227
+ },
228
+ ],
229
+ },
230
+ ],
231
+ [VSCODE_HOOK_NAMES.SESSION_START]: [
232
+ {
233
+ matcher: "",
234
+ hooks: [
235
+ {
236
+ type: "command",
237
+ command: `context-mode hook vscode-copilot ${VSCODE_HOOK_NAMES.SESSION_START.toLowerCase()}`,
238
+ },
239
+ ],
240
+ },
241
+ ],
242
+ };
243
+ }
244
+ readSettings() {
245
+ // Try .github/hooks/context-mode.json first, then .claude/settings.json
246
+ const paths = [
247
+ this.getSettingsPath(),
248
+ resolve(".claude", "settings.json"),
249
+ ];
250
+ for (const configPath of paths) {
251
+ try {
252
+ const raw = readFileSync(configPath, "utf-8");
253
+ return JSON.parse(raw);
254
+ }
255
+ catch {
256
+ continue;
257
+ }
258
+ }
259
+ return null;
260
+ }
261
+ writeSettings(settings) {
262
+ const configPath = this.getSettingsPath();
263
+ const dir = resolve(".github", "hooks");
264
+ mkdirSync(dir, { recursive: true });
265
+ writeFileSync(configPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
266
+ }
267
+ // ── Diagnostics (doctor) ─────────────────────────────────
268
+ validateHooks(pluginRoot) {
269
+ const results = [];
270
+ // Check .github/hooks/ directory for hook JSON files
271
+ const hooksDir = resolve(".github", "hooks");
272
+ try {
273
+ accessSync(hooksDir, constants.R_OK);
274
+ }
275
+ catch {
276
+ results.push({
277
+ check: "Hooks directory",
278
+ status: "fail",
279
+ message: ".github/hooks/ directory not found",
280
+ fix: "context-mode upgrade",
281
+ });
282
+ return results;
283
+ }
284
+ // Check for context-mode hook config
285
+ const hookConfigPath = resolve(hooksDir, "context-mode.json");
286
+ try {
287
+ const raw = readFileSync(hookConfigPath, "utf-8");
288
+ const config = JSON.parse(raw);
289
+ const hooks = config.hooks;
290
+ // Check PreToolUse
291
+ if (hooks?.[VSCODE_HOOK_NAMES.PRE_TOOL_USE]) {
292
+ results.push({
293
+ check: "PreToolUse hook",
294
+ status: "pass",
295
+ message: "PreToolUse hook configured in context-mode.json",
296
+ });
297
+ }
298
+ else {
299
+ results.push({
300
+ check: "PreToolUse hook",
301
+ status: "fail",
302
+ message: "PreToolUse not found in context-mode.json",
303
+ fix: "context-mode upgrade",
304
+ });
305
+ }
306
+ // Check SessionStart
307
+ if (hooks?.[VSCODE_HOOK_NAMES.SESSION_START]) {
308
+ results.push({
309
+ check: "SessionStart hook",
310
+ status: "pass",
311
+ message: "SessionStart hook configured in context-mode.json",
312
+ });
313
+ }
314
+ else {
315
+ results.push({
316
+ check: "SessionStart hook",
317
+ status: "fail",
318
+ message: "SessionStart not found in context-mode.json",
319
+ fix: "context-mode upgrade",
320
+ });
321
+ }
322
+ }
323
+ catch {
324
+ results.push({
325
+ check: "Hook configuration",
326
+ status: "fail",
327
+ message: "Could not read .github/hooks/context-mode.json",
328
+ fix: "context-mode upgrade",
329
+ });
330
+ }
331
+ // Warn about preview status
332
+ results.push({
333
+ check: "API stability",
334
+ status: "warn",
335
+ message: "VS Code Copilot hooks are in preview — API may change without notice",
336
+ });
337
+ // Warn about matcher behavior
338
+ results.push({
339
+ check: "Matcher support",
340
+ status: "warn",
341
+ message: "Matchers are parsed but IGNORED — all hooks fire on all tools",
342
+ });
343
+ return results;
344
+ }
345
+ checkPluginRegistration() {
346
+ // Check MCP config in .vscode/mcp.json
347
+ try {
348
+ const mcpConfigPath = resolve(".vscode", "mcp.json");
349
+ const raw = readFileSync(mcpConfigPath, "utf-8");
350
+ const config = JSON.parse(raw);
351
+ const servers = config.servers;
352
+ if (servers) {
353
+ const hasPlugin = Object.keys(servers).some((k) => k.includes("context-mode"));
354
+ if (hasPlugin) {
355
+ return {
356
+ check: "MCP registration",
357
+ status: "pass",
358
+ message: "context-mode found in .vscode/mcp.json",
359
+ };
360
+ }
361
+ }
362
+ return {
363
+ check: "MCP registration",
364
+ status: "fail",
365
+ message: "context-mode not found in .vscode/mcp.json",
366
+ fix: "Add context-mode server to .vscode/mcp.json",
367
+ };
368
+ }
369
+ catch {
370
+ return {
371
+ check: "MCP registration",
372
+ status: "warn",
373
+ message: "Could not read .vscode/mcp.json",
374
+ };
375
+ }
376
+ }
377
+ getInstalledVersion() {
378
+ // Check VS Code extensions for context-mode
379
+ const extensionDirs = [
380
+ join(homedir(), ".vscode", "extensions"),
381
+ join(homedir(), ".vscode-insiders", "extensions"),
382
+ ];
383
+ for (const extDir of extensionDirs) {
384
+ try {
385
+ const entries = readFileSync(join(extDir, "extensions.json"), "utf-8");
386
+ const exts = JSON.parse(entries);
387
+ const contextMode = exts.find((e) => typeof e.identifier === "object" &&
388
+ e.identifier !== null &&
389
+ e.identifier.id?.toString().includes("context-mode"));
390
+ if (contextMode && typeof contextMode.version === "string") {
391
+ return contextMode.version;
392
+ }
393
+ }
394
+ catch {
395
+ continue;
396
+ }
397
+ }
398
+ return "not installed";
399
+ }
400
+ // ── Upgrade ────────────────────────────────────────────
401
+ configureAllHooks(_pluginRoot) {
402
+ const changes = [];
403
+ const hookConfig = { hooks: {} };
404
+ const hooks = hookConfig.hooks;
405
+ const hookTypes = [
406
+ VSCODE_HOOK_NAMES.PRE_TOOL_USE,
407
+ VSCODE_HOOK_NAMES.POST_TOOL_USE,
408
+ VSCODE_HOOK_NAMES.PRE_COMPACT,
409
+ VSCODE_HOOK_NAMES.SESSION_START,
410
+ ];
411
+ for (const hookType of hookTypes) {
412
+ const script = VSCODE_HOOK_SCRIPTS[hookType];
413
+ if (!script)
414
+ continue;
415
+ hooks[hookType] = [
416
+ {
417
+ matcher: "",
418
+ hooks: [
419
+ {
420
+ type: "command",
421
+ command: `context-mode hook vscode-copilot ${hookType.toLowerCase()}`,
422
+ },
423
+ ],
424
+ },
425
+ ];
426
+ changes.push(`Configured ${hookType} hook`);
427
+ }
428
+ // Write to .github/hooks/context-mode.json
429
+ const outputDir = resolve(".github", "hooks");
430
+ mkdirSync(outputDir, { recursive: true });
431
+ const outputPath = resolve(outputDir, "context-mode.json");
432
+ writeFileSync(outputPath, JSON.stringify(hookConfig, null, 2) + "\n", "utf-8");
433
+ changes.push(`Wrote hook config to ${outputPath}`);
434
+ return changes;
435
+ }
436
+ backupSettings() {
437
+ const settingsPath = this.getSettingsPath();
438
+ try {
439
+ accessSync(settingsPath, constants.R_OK);
440
+ const backupPath = settingsPath + ".bak";
441
+ copyFileSync(settingsPath, backupPath);
442
+ return backupPath;
443
+ }
444
+ catch {
445
+ return null;
446
+ }
447
+ }
448
+ setHookPermissions(pluginRoot) {
449
+ const set = [];
450
+ const hooksDir = join(pluginRoot, "hooks", "vscode-copilot");
451
+ for (const scriptName of Object.values(VSCODE_HOOK_SCRIPTS)) {
452
+ const scriptPath = resolve(hooksDir, scriptName);
453
+ try {
454
+ accessSync(scriptPath, constants.R_OK);
455
+ chmodSync(scriptPath, 0o755);
456
+ set.push(scriptPath);
457
+ }
458
+ catch {
459
+ /* skip missing scripts */
460
+ }
461
+ }
462
+ return set;
463
+ }
464
+ updatePluginRegistry(_pluginRoot, _version) {
465
+ // VS Code manages extensions through its own marketplace/extension system.
466
+ // No manual registry update needed.
467
+ }
468
+ // ── Routing Instructions (soft enforcement) ────────────
469
+ getRoutingInstructionsConfig() {
470
+ return {
471
+ fileName: "copilot-instructions.md",
472
+ globalPath: "", // VS Code Copilot uses org-level, not global file
473
+ projectRelativePath: join(".github", "copilot-instructions.md"),
474
+ };
475
+ }
476
+ writeRoutingInstructions(projectDir, pluginRoot) {
477
+ const config = this.getRoutingInstructionsConfig();
478
+ const targetPath = resolve(projectDir, config.projectRelativePath);
479
+ const sourcePath = resolve(pluginRoot, "configs", "vscode-copilot", config.fileName);
480
+ try {
481
+ const content = readFileSync(sourcePath, "utf-8");
482
+ // Ensure .github directory exists
483
+ mkdirSync(resolve(projectDir, ".github"), { recursive: true });
484
+ try {
485
+ const existing = readFileSync(targetPath, "utf-8");
486
+ if (existing.includes("context-mode"))
487
+ return null;
488
+ writeFileSync(targetPath, existing.trimEnd() + "\n\n" + content, "utf-8");
489
+ return targetPath;
490
+ }
491
+ catch {
492
+ writeFileSync(targetPath, content, "utf-8");
493
+ return targetPath;
494
+ }
495
+ }
496
+ catch {
497
+ return null;
498
+ }
499
+ }
500
+ // ── Internal helpers ───────────────────────────────────
501
+ /**
502
+ * Extract session ID from VS Code Copilot hook input.
503
+ * VS Code Copilot uses camelCase sessionId (NOT session_id).
504
+ */
505
+ extractSessionId(input) {
506
+ if (input.sessionId)
507
+ return input.sessionId;
508
+ if (process.env.VSCODE_PID)
509
+ return `vscode-${process.env.VSCODE_PID}`;
510
+ return `pid-${process.ppid}`;
511
+ }
512
+ }
package/build/cli.d.ts CHANGED
@@ -3,10 +3,13 @@
3
3
  * context-mode CLI
4
4
  *
5
5
  * Usage:
6
- * context-mode → Start MCP server (stdio)
7
- * context-mode setup Interactive setup (detect runtimes, install Bun)
8
- * context-mode doctor Diagnose runtime issues, hooks, FTS5, version
9
- * context-mode upgrade Fix hooks, permissions, and settings
10
- * context-mode stats → (skill only — /context-mode:stats)
6
+ * context-mode → Start MCP server (stdio)
7
+ * context-mode doctor Diagnose runtime issues, hooks, FTS5, version
8
+ * context-mode upgrade Fix hooks, permissions, and settings
9
+ * context-mode hook <platform> <event> Dispatch a hook script (used by platform hook configs)
10
+ *
11
+ * Platform auto-detection: CLI detects which platform is running
12
+ * (Claude Code, Gemini CLI, OpenCode, etc.) and uses the appropriate adapter.
11
13
  */
12
- export {};
14
+ /** Normalize Windows backslash paths to forward slashes for Bash (MSYS2) compatibility. */
15
+ export declare function toUnixPath(p: string): string;