context-mode 0.9.22 → 1.0.1

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 (100) hide show
  1. package/.claude-plugin/hooks/hooks.json +46 -4
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +4 -4
  4. package/README.md +370 -185
  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 +7 -5
  40. package/build/cli.js +127 -421
  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 +74 -0
  54. package/build/session/snapshot.js +344 -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 +395 -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/server.bundle.mjs +157 -109
  97. package/skills/context-mode/SKILL.md +49 -49
  98. package/skills/ctx-stats/SKILL.md +1 -1
  99. package/start.mjs +47 -0
  100. package/hooks/pretooluse.sh +0 -147
package/build/cli.js CHANGED
@@ -3,30 +3,70 @@
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:ctx-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
14
  import * as p from "@clack/prompts";
13
15
  import color from "picocolors";
14
16
  import { execSync } from "node:child_process";
15
- import { readFileSync, writeFileSync, copyFileSync, cpSync, chmodSync, accessSync, readdirSync, rmSync, constants } from "node:fs";
16
- import { resolve, dirname } from "node:path";
17
+ import { readFileSync, cpSync, accessSync, readdirSync, rmSync, constants } from "node:fs";
18
+ import { resolve, dirname, join } from "node:path";
19
+ import { tmpdir } from "node:os";
17
20
  import { fileURLToPath } from "node:url";
18
- import { homedir } from "node:os";
19
21
  import { detectRuntimes, getRuntimeSummary, hasBunRuntime, getAvailableLanguages, } from "./runtime.js";
20
- const args = process.argv.slice(2);
21
- if (args[0] === "setup") {
22
- setup();
22
+ // ── Adapter imports ──────────────────────────────────────
23
+ import { detectPlatform, getAdapter } from "./adapters/detect.js";
24
+ /* -------------------------------------------------------
25
+ * Hook dispatcher — `context-mode hook <platform> <event>`
26
+ * ------------------------------------------------------- */
27
+ const HOOK_MAP = {
28
+ "claude-code": {
29
+ pretooluse: "hooks/pretooluse.mjs",
30
+ posttooluse: "hooks/posttooluse.mjs",
31
+ precompact: "hooks/precompact.mjs",
32
+ sessionstart: "hooks/sessionstart.mjs",
33
+ userpromptsubmit: "hooks/userpromptsubmit.mjs",
34
+ },
35
+ "gemini-cli": {
36
+ beforetool: "hooks/gemini-cli/beforetool.mjs",
37
+ aftertool: "hooks/gemini-cli/aftertool.mjs",
38
+ precompress: "hooks/gemini-cli/precompress.mjs",
39
+ sessionstart: "hooks/gemini-cli/sessionstart.mjs",
40
+ },
41
+ "vscode-copilot": {
42
+ pretooluse: "hooks/vscode-copilot/pretooluse.mjs",
43
+ posttooluse: "hooks/vscode-copilot/posttooluse.mjs",
44
+ precompact: "hooks/vscode-copilot/precompact.mjs",
45
+ sessionstart: "hooks/vscode-copilot/sessionstart.mjs",
46
+ },
47
+ };
48
+ async function hookDispatch(platform, event) {
49
+ const scriptPath = HOOK_MAP[platform]?.[event];
50
+ if (!scriptPath) {
51
+ console.error(`Unknown hook: ${platform}/${event}`);
52
+ process.exit(1);
53
+ }
54
+ const pluginRoot = getPluginRoot();
55
+ await import(join(pluginRoot, scriptPath));
23
56
  }
24
- else if (args[0] === "doctor") {
57
+ /* -------------------------------------------------------
58
+ * Entry point
59
+ * ------------------------------------------------------- */
60
+ const args = process.argv.slice(2);
61
+ if (args[0] === "doctor") {
25
62
  doctor().then((code) => process.exit(code));
26
63
  }
27
64
  else if (args[0] === "upgrade") {
28
65
  upgrade();
29
66
  }
67
+ else if (args[0] === "hook") {
68
+ hookDispatch(args[1], args[2]);
69
+ }
30
70
  else {
31
71
  // Default: start MCP server
32
72
  import("./server.js");
@@ -43,21 +83,6 @@ function getPluginRoot() {
43
83
  const __dirname = dirname(__filename);
44
84
  return resolve(__dirname, "..");
45
85
  }
46
- function getSettingsPath() {
47
- return resolve(homedir(), ".claude", "settings.json");
48
- }
49
- function readSettings() {
50
- try {
51
- const raw = readFileSync(getSettingsPath(), "utf-8");
52
- return JSON.parse(raw);
53
- }
54
- catch {
55
- return null;
56
- }
57
- }
58
- function getHookScriptPath() {
59
- return resolve(getPluginRoot(), "hooks", "pretooluse.mjs");
60
- }
61
86
  function getLocalVersion() {
62
87
  try {
63
88
  const pkg = JSON.parse(readFileSync(resolve(getPluginRoot(), "package.json"), "utf-8"));
@@ -79,71 +104,18 @@ async function fetchLatestVersion() {
79
104
  return "unknown";
80
105
  }
81
106
  }
82
- function getMarketplaceVersion() {
83
- // Primary: read from installed_plugins.json (source of truth for Claude Code)
84
- try {
85
- const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
86
- const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8"));
87
- const plugins = ipRaw.plugins ?? {};
88
- for (const [key, entries] of Object.entries(plugins)) {
89
- if (!key.toLowerCase().includes("context-mode"))
90
- continue;
91
- const arr = entries;
92
- if (arr.length > 0 && typeof arr[0].version === "string") {
93
- return arr[0].version;
94
- }
95
- }
96
- }
97
- catch { /* fallback below */ }
98
- // Fallback: read from own package.json
99
- const localVer = getLocalVersion();
100
- if (localVer !== "unknown")
101
- return localVer;
102
- // Last resort: scan common plugin cache locations
103
- const bases = [
104
- resolve(homedir(), ".claude"),
105
- resolve(homedir(), ".config", "claude"),
106
- ];
107
- for (const base of bases) {
108
- const cacheDir = resolve(base, "plugins", "cache", "claude-context-mode", "context-mode");
109
- try {
110
- const entries = readdirSync(cacheDir);
111
- const versions = entries
112
- .filter((e) => /^\d+\.\d+\.\d+/.test(e))
113
- .sort((a, b) => {
114
- const pa = a.split(".").map(Number);
115
- const pb = b.split(".").map(Number);
116
- for (let i = 0; i < 3; i++) {
117
- if ((pa[i] ?? 0) !== (pb[i] ?? 0))
118
- return (pa[i] ?? 0) - (pb[i] ?? 0);
119
- }
120
- return 0;
121
- });
122
- if (versions.length > 0)
123
- return versions[versions.length - 1];
124
- }
125
- catch { /* continue */ }
126
- }
127
- return "not installed";
128
- }
129
- function semverGt(a, b) {
130
- const pa = a.split(".").map(Number);
131
- const pb = b.split(".").map(Number);
132
- for (let i = 0; i < 3; i++) {
133
- if ((pa[i] ?? 0) > (pb[i] ?? 0))
134
- return true;
135
- if ((pa[i] ?? 0) < (pb[i] ?? 0))
136
- return false;
137
- }
138
- return false;
139
- }
140
107
  /* -------------------------------------------------------
141
- * Doctor
108
+ * Doctor — adapter-aware diagnostics
142
109
  * ------------------------------------------------------- */
143
110
  async function doctor() {
144
111
  if (process.stdout.isTTY)
145
112
  console.clear();
113
+ // Detect platform
114
+ const detection = detectPlatform();
115
+ const adapter = await getAdapter(detection.platform);
146
116
  p.intro(color.bgMagenta(color.white(" context-mode doctor ")));
117
+ p.log.info(`Platform: ${color.cyan(adapter.name)}` +
118
+ color.dim(` (${detection.confidence} confidence — ${detection.reason})`));
147
119
  let criticalFails = 0;
148
120
  const s = p.spinner();
149
121
  s.start("Running diagnostics");
@@ -156,7 +128,7 @@ async function doctor() {
156
128
  catch {
157
129
  s.stop("Diagnostics partial");
158
130
  p.log.warn(color.yellow("Could not detect runtimes") + color.dim(" — module may be missing, restart session after upgrade"));
159
- p.outro(color.yellow("Doctor could not fully run — try again after restarting Claude Code"));
131
+ p.outro(color.yellow("Doctor could not fully run — try again after restarting"));
160
132
  return 1;
161
133
  }
162
134
  s.stop("Diagnostics complete");
@@ -212,55 +184,23 @@ async function doctor() {
212
184
  p.log.error(color.red("Server test: FAIL") + ` — ${message}`);
213
185
  }
214
186
  }
215
- // Hooks installed
216
- p.log.step("Checking hooks configuration...");
217
- const settings = readSettings();
218
- const hookScriptPath = getHookScriptPath();
219
- if (settings) {
220
- const hooks = settings.hooks;
221
- const preToolUse = hooks?.PreToolUse;
222
- if (preToolUse && preToolUse.length > 0) {
223
- const hasCorrectHook = preToolUse.some((entry) => entry.hooks?.some((h) => h.command?.includes("pretooluse.mjs")));
224
- if (hasCorrectHook) {
225
- p.log.success(color.green("Hooks installed: PASS") + " — PreToolUse hook configured");
226
- }
227
- else {
228
- p.log.error(color.red("Hooks installed: FAIL") +
229
- " — PreToolUse exists but does not point to pretooluse.mjs" +
230
- color.dim("\n Run: npx context-mode upgrade"));
231
- }
187
+ // Hooks — adapter-aware validation
188
+ p.log.step(`Checking ${adapter.name} hooks configuration...`);
189
+ const pluginRoot = getPluginRoot();
190
+ const hookResults = adapter.validateHooks(pluginRoot);
191
+ for (const result of hookResults) {
192
+ if (result.status === "pass") {
193
+ p.log.success(color.green(`${result.check}: PASS`) + ` — ${result.message}`);
232
194
  }
233
195
  else {
234
- p.log.error(color.red("Hooks installed: FAIL") +
235
- "No PreToolUse hooks found" +
236
- color.dim("\n Run: npx context-mode upgrade"));
237
- }
238
- // Check SessionStart hook
239
- const sessionStart = hooks?.SessionStart;
240
- if (sessionStart && sessionStart.length > 0) {
241
- const hasSessionHook = sessionStart.some((entry) => entry.hooks?.some((h) => h.command?.includes("sessionstart.mjs")));
242
- if (hasSessionHook) {
243
- p.log.success(color.green("SessionStart hook: PASS") + " — SessionStart hook configured");
244
- }
245
- else {
246
- p.log.error(color.red("SessionStart hook: FAIL") +
247
- " — SessionStart exists but does not point to sessionstart.mjs" +
248
- color.dim("\n Run: npx context-mode upgrade"));
249
- }
250
- }
251
- else {
252
- p.log.error(color.red("SessionStart hook: FAIL") +
253
- " — No SessionStart hooks found" +
254
- color.dim("\n Run: npx context-mode upgrade"));
196
+ p.log.error(color.red(`${result.check}: FAIL`) +
197
+ `${result.message}` +
198
+ (result.fix ? color.dim(`\n Run: ${result.fix}`) : ""));
255
199
  }
256
200
  }
257
- else {
258
- p.log.error(color.red("Hooks installed: FAIL") +
259
- " — Could not read ~/.claude/settings.json" +
260
- color.dim("\n Run: npx context-mode upgrade"));
261
- }
262
201
  // Hook script exists
263
202
  p.log.step("Checking hook script...");
203
+ const hookScriptPath = resolve(pluginRoot, "hooks", "pretooluse.mjs");
264
204
  try {
265
205
  accessSync(hookScriptPath, constants.R_OK);
266
206
  p.log.success(color.green("Hook script exists: PASS") + color.dim(` — ${hookScriptPath}`));
@@ -269,30 +209,15 @@ async function doctor() {
269
209
  p.log.error(color.red("Hook script exists: FAIL") +
270
210
  color.dim(` — not found at ${hookScriptPath}`));
271
211
  }
272
- // Plugin enabled
273
- p.log.step("Checking plugin registration...");
274
- if (settings) {
275
- const enabledPlugins = settings.enabledPlugins;
276
- if (enabledPlugins) {
277
- const pluginKey = Object.keys(enabledPlugins).find((k) => k.startsWith("context-mode"));
278
- if (pluginKey && enabledPlugins[pluginKey]) {
279
- p.log.success(color.green("Plugin enabled: PASS") + color.dim(` — ${pluginKey}`));
280
- }
281
- else {
282
- p.log.warn(color.yellow("Plugin enabled: WARN") +
283
- " — context-mode not in enabledPlugins" +
284
- color.dim(" (might be using standalone MCP mode)"));
285
- }
286
- }
287
- else {
288
- p.log.warn(color.yellow("Plugin enabled: WARN") +
289
- " — no enabledPlugins section found" +
290
- color.dim(" (might be using standalone MCP mode)"));
291
- }
212
+ // Plugin registration — adapter-aware
213
+ p.log.step(`Checking ${adapter.name} plugin registration...`);
214
+ const pluginCheck = adapter.checkPluginRegistration();
215
+ if (pluginCheck.status === "pass") {
216
+ p.log.success(color.green("Plugin enabled: PASS") + color.dim(` — ${pluginCheck.message}`));
292
217
  }
293
218
  else {
294
219
  p.log.warn(color.yellow("Plugin enabled: WARN") +
295
- "could not read settings.json");
220
+ `${pluginCheck.message}`);
296
221
  }
297
222
  // FTS5 / better-sqlite3
298
223
  p.log.step("Checking FTS5 / better-sqlite3...");
@@ -323,12 +248,11 @@ async function doctor() {
323
248
  color.dim("\n Try: npm rebuild better-sqlite3"));
324
249
  }
325
250
  }
326
- // Version check
251
+ // Version check — adapter-aware
327
252
  p.log.step("Checking versions...");
328
253
  const localVersion = getLocalVersion();
329
254
  const latestVersion = await fetchLatestVersion();
330
- const marketplaceVersion = getMarketplaceVersion();
331
- // npm / MCP version
255
+ const installedVersion = adapter.getInstalledVersion();
332
256
  if (latestVersion === "unknown") {
333
257
  p.log.warn(color.yellow("npm (MCP): WARN") +
334
258
  ` — local v${localVersion}, could not reach npm registry`);
@@ -342,22 +266,21 @@ async function doctor() {
342
266
  ` — local v${localVersion}, latest v${latestVersion}` +
343
267
  color.dim("\n Run: /context-mode:ctx-upgrade"));
344
268
  }
345
- // Marketplace version
346
- if (marketplaceVersion === "not installed") {
347
- p.log.info(color.dim("Marketplace: not installed") +
269
+ if (installedVersion === "not installed") {
270
+ p.log.info(color.dim(`${adapter.name}: not installed`) +
348
271
  " — using standalone MCP mode");
349
272
  }
350
- else if (latestVersion !== "unknown" && marketplaceVersion === latestVersion) {
351
- p.log.success(color.green("Marketplace: PASS") +
352
- ` — v${marketplaceVersion}`);
273
+ else if (latestVersion !== "unknown" && installedVersion === latestVersion) {
274
+ p.log.success(color.green(`${adapter.name}: PASS`) +
275
+ ` — v${installedVersion}`);
353
276
  }
354
277
  else if (latestVersion !== "unknown") {
355
- p.log.warn(color.yellow("Marketplace: WARN") +
356
- ` — v${marketplaceVersion}, latest v${latestVersion}` +
278
+ p.log.warn(color.yellow(`${adapter.name}: WARN`) +
279
+ ` — v${installedVersion}, latest v${latestVersion}` +
357
280
  color.dim("\n Run: /context-mode:ctx-upgrade"));
358
281
  }
359
282
  else {
360
- p.log.info(`Marketplace: v${marketplaceVersion}` +
283
+ p.log.info(`${adapter.name}: v${installedVersion}` +
361
284
  color.dim(" — could not verify against npm registry"));
362
285
  }
363
286
  // Summary
@@ -371,26 +294,29 @@ async function doctor() {
371
294
  return 0;
372
295
  }
373
296
  /* -------------------------------------------------------
374
- * Upgrade
297
+ * Upgrade — adapter-aware hook configuration
375
298
  * ------------------------------------------------------- */
376
299
  async function upgrade() {
377
300
  if (process.stdout.isTTY)
378
301
  console.clear();
302
+ // Detect platform
303
+ const detection = detectPlatform();
304
+ const adapter = await getAdapter(detection.platform);
379
305
  p.intro(color.bgCyan(color.black(" context-mode upgrade ")));
306
+ p.log.info(`Platform: ${color.cyan(adapter.name)}` +
307
+ color.dim(` (${detection.confidence} confidence)`));
380
308
  let pluginRoot = getPluginRoot();
381
- const settingsPath = getSettingsPath();
382
309
  const changes = [];
383
310
  const s = p.spinner();
384
- // Step 1: Pull latest from GitHub (same source as marketplace)
311
+ // Step 1: Pull latest from GitHub
385
312
  p.log.step("Pulling latest from GitHub...");
386
313
  const localVersion = getLocalVersion();
387
- const tmpDir = `/tmp/context-mode-upgrade-${Date.now()}`;
388
- s.start("Cloning mksglu/claude-context-mode");
314
+ const tmpDir = join(tmpdir(), `context-mode-upgrade-${Date.now()}`);
315
+ s.start("Cloning mksglu/context-mode");
389
316
  try {
390
- execSync(`git clone --depth 1 https://github.com/mksglu/claude-context-mode.git "${tmpDir}"`, { stdio: "pipe", timeout: 30000 });
317
+ execSync(`git clone --depth 1 https://github.com/mksglu/context-mode.git "${tmpDir}"`, { stdio: "pipe", timeout: 30000 });
391
318
  s.stop("Downloaded");
392
319
  const srcDir = tmpDir;
393
- // Read new version
394
320
  const newPkg = JSON.parse(readFileSync(resolve(srcDir, "package.json"), "utf-8"));
395
321
  const newVersion = newPkg.version ?? "unknown";
396
322
  if (newVersion === localVersion) {
@@ -412,9 +338,8 @@ async function upgrade() {
412
338
  timeout: 30000,
413
339
  });
414
340
  s.stop("Built successfully");
415
- // Step 3: Update in-place (same directory, no registry changes needed)
341
+ // Step 3: Update in-place
416
342
  s.start("Updating files in-place");
417
- // Clean stale version dirs from previous upgrade attempts
418
343
  const cacheParentMatch = pluginRoot.match(/^(.*[\\/]plugins[\\/]cache[\\/][^\\/]+[\\/][^\\/]+[\\/])/);
419
344
  if (cacheParentMatch) {
420
345
  const cacheParent = cacheParentMatch[1];
@@ -433,7 +358,6 @@ async function upgrade() {
433
358
  }
434
359
  catch { /* parent may not exist */ }
435
360
  }
436
- // Copy new files over old ones — same path, no registry update needed
437
361
  const items = [
438
362
  "build", "src", "hooks", "skills", ".claude-plugin",
439
363
  "start.mjs", "server.bundle.mjs", "package.json", ".mcp.json",
@@ -446,24 +370,10 @@ async function upgrade() {
446
370
  catch { /* some files may not exist in source */ }
447
371
  }
448
372
  s.stop(color.green(`Updated in-place to v${newVersion}`));
449
- // Fix registry to point back to this pluginRoot (self-heal may have changed it)
450
- try {
451
- const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
452
- const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8"));
453
- for (const [key, entries] of Object.entries(ipRaw.plugins || {})) {
454
- if (!key.toLowerCase().includes("context-mode"))
455
- continue;
456
- for (const entry of entries) {
457
- entry.installPath = pluginRoot;
458
- entry.version = newVersion;
459
- entry.lastUpdated = new Date().toISOString();
460
- }
461
- }
462
- writeFileSync(ipPath, JSON.stringify(ipRaw, null, 2) + "\n", "utf-8");
463
- p.log.info(color.dim(" Registry synced to " + pluginRoot));
464
- }
465
- catch { /* best effort */ }
466
- // Install production deps (rebuild native modules if needed)
373
+ // Fix registry adapter-aware
374
+ adapter.updatePluginRegistry(pluginRoot, newVersion);
375
+ p.log.info(color.dim(" Registry synced to " + pluginRoot));
376
+ // Install production deps
467
377
  s.start("Installing production dependencies");
468
378
  execSync("npm install --production --no-audit --no-fund", {
469
379
  cwd: pluginRoot,
@@ -471,10 +381,10 @@ async function upgrade() {
471
381
  timeout: 60000,
472
382
  });
473
383
  s.stop("Dependencies ready");
474
- // Update global npm package from same GitHub source
384
+ // Update global npm
475
385
  s.start("Updating npm global package");
476
386
  try {
477
- execSync(`npm install -g "${pluginRoot}" --no-audit --no-fund 2>/dev/null`, {
387
+ execSync(`npm install -g "${pluginRoot}" --no-audit --no-fund`, {
478
388
  stdio: "pipe",
479
389
  timeout: 30000,
480
390
  });
@@ -498,120 +408,40 @@ async function upgrade() {
498
408
  s.stop(color.red("Update failed"));
499
409
  p.log.error(color.red("GitHub pull failed") + ` — ${message}`);
500
410
  p.log.info(color.dim("Continuing with hooks/settings fix..."));
501
- // Cleanup on failure
502
411
  try {
503
412
  rmSync(tmpDir, { recursive: true, force: true });
504
413
  }
505
414
  catch { /* ignore */ }
506
415
  }
507
- // Step 3: Backup settings.json
508
- p.log.step("Backing up settings.json...");
509
- try {
510
- accessSync(settingsPath, constants.R_OK);
511
- const backupPath = settingsPath + ".bak";
512
- copyFileSync(settingsPath, backupPath);
416
+ // Step 3: Backup settings — adapter-aware
417
+ p.log.step(`Backing up ${adapter.name} settings...`);
418
+ const backupPath = adapter.backupSettings();
419
+ if (backupPath) {
513
420
  p.log.success(color.green("Backup created") + color.dim(" -> " + backupPath));
514
- changes.push("Backed up settings.json");
515
- }
516
- catch {
517
- p.log.warn(color.yellow("No existing settings.json to backup") +
518
- " — a new one will be created");
519
- }
520
- // Step 4: Fix hooks
521
- p.log.step("Configuring PreToolUse hooks...");
522
- const hookScriptPath = toUnixPath(resolve(pluginRoot, "hooks", "pretooluse.mjs"));
523
- const settings = readSettings() ?? {};
524
- const desiredHookEntry = {
525
- matcher: "Bash|Read|Grep|WebFetch|Task|mcp__plugin_context-mode_context-mode__execute|mcp__plugin_context-mode_context-mode__execute_file|mcp__plugin_context-mode_context-mode__batch_execute",
526
- hooks: [
527
- {
528
- type: "command",
529
- command: "node " + hookScriptPath,
530
- },
531
- ],
532
- };
533
- const hooks = (settings.hooks ?? {});
534
- const existingPreToolUse = hooks.PreToolUse;
535
- if (existingPreToolUse && Array.isArray(existingPreToolUse)) {
536
- const existingIdx = existingPreToolUse.findIndex((entry) => {
537
- const entryHooks = entry.hooks;
538
- return entryHooks?.some((h) => h.command?.includes("pretooluse.mjs"));
539
- });
540
- if (existingIdx >= 0) {
541
- existingPreToolUse[existingIdx] = desiredHookEntry;
542
- p.log.info(color.dim("Updated existing PreToolUse hook entry"));
543
- changes.push("Updated existing PreToolUse hook entry");
544
- }
545
- else {
546
- existingPreToolUse.push(desiredHookEntry);
547
- p.log.info(color.dim("Added PreToolUse hook entry"));
548
- changes.push("Added PreToolUse hook entry to existing hooks");
549
- }
550
- hooks.PreToolUse = existingPreToolUse;
421
+ changes.push("Backed up settings");
551
422
  }
552
423
  else {
553
- hooks.PreToolUse = [desiredHookEntry];
554
- p.log.info(color.dim("Created PreToolUse hooks section"));
555
- changes.push("Created PreToolUse hooks section");
556
- }
557
- // --- SessionStart hook ---
558
- p.log.step("Configuring SessionStart hook...");
559
- const sessionHookScriptPath = resolve(pluginRoot, "hooks", "sessionstart.mjs");
560
- const desiredSessionHookEntry = {
561
- matcher: "",
562
- hooks: [
563
- {
564
- type: "command",
565
- command: "node " + sessionHookScriptPath,
566
- },
567
- ],
568
- };
569
- const existingSessionStart = hooks.SessionStart;
570
- if (existingSessionStart && Array.isArray(existingSessionStart)) {
571
- const existingSessionIdx = existingSessionStart.findIndex((entry) => {
572
- const entryHooks = entry.hooks;
573
- return entryHooks?.some((h) => h.command?.includes("sessionstart.mjs"));
574
- });
575
- if (existingSessionIdx >= 0) {
576
- existingSessionStart[existingSessionIdx] = desiredSessionHookEntry;
577
- p.log.info(color.dim("Updated existing SessionStart hook entry"));
578
- changes.push("Updated existing SessionStart hook entry");
579
- }
580
- else {
581
- existingSessionStart.push(desiredSessionHookEntry);
582
- p.log.info(color.dim("Added SessionStart hook entry"));
583
- changes.push("Added SessionStart hook entry to existing hooks");
584
- }
585
- hooks.SessionStart = existingSessionStart;
586
- }
587
- else {
588
- hooks.SessionStart = [desiredSessionHookEntry];
589
- p.log.info(color.dim("Created SessionStart hooks section"));
590
- changes.push("Created SessionStart hooks section");
424
+ p.log.warn(color.yellow("No existing settings to backup") +
425
+ " a new one will be created");
591
426
  }
592
- settings.hooks = hooks;
593
- // Write updated settings
594
- try {
595
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
596
- p.log.success(color.green("Hooks configured") + color.dim(" -> " + settingsPath));
427
+ // Step 4: Configure hooks adapter-aware
428
+ p.log.step(`Configuring ${adapter.name} hooks...`);
429
+ const hookChanges = adapter.configureAllHooks(pluginRoot);
430
+ for (const change of hookChanges) {
431
+ p.log.info(color.dim(` ${change}`));
432
+ changes.push(change);
597
433
  }
598
- catch (err) {
599
- const message = err instanceof Error ? err.message : String(err);
600
- p.log.error(color.red("Failed to write settings.json") + " — " + message);
601
- p.outro(color.red("Upgrade failed."));
602
- process.exit(1);
603
- }
604
- // Step 5: Set hook script permissions
434
+ p.log.success(color.green("Hooks configured") + color.dim(` — ${adapter.name}`));
435
+ // Step 5: Set hook script permissions adapter-aware
605
436
  p.log.step("Setting hook script permissions...");
606
- try {
607
- accessSync(hookScriptPath, constants.R_OK);
608
- chmodSync(hookScriptPath, 0o755);
609
- p.log.success(color.green("Permissions set") + color.dim(" chmod +x " + hookScriptPath));
610
- changes.push("Set pretooluse.mjs as executable");
437
+ const permSet = adapter.setHookPermissions(pluginRoot);
438
+ if (permSet.length > 0) {
439
+ p.log.success(color.green("Permissions set") + color.dim(` — ${permSet.length} hook script(s)`));
440
+ changes.push(`Set ${permSet.length} hook scripts as executable`);
611
441
  }
612
- catch {
613
- p.log.error(color.red("Hook script not found") +
614
- color.dim(" — expected at " + hookScriptPath));
442
+ else {
443
+ p.log.error(color.red("No hook scripts found") +
444
+ color.dim(" — expected in " + resolve(pluginRoot, "hooks")));
615
445
  }
616
446
  // Step 6: Report
617
447
  if (changes.length > 0) {
@@ -620,7 +450,7 @@ async function upgrade() {
620
450
  else {
621
451
  p.log.info(color.dim("No changes were needed."));
622
452
  }
623
- // Step 7: Run doctor from updated pluginRoot
453
+ // Step 7: Run doctor
624
454
  p.log.step("Running doctor to verify...");
625
455
  console.log();
626
456
  try {
@@ -632,130 +462,6 @@ async function upgrade() {
632
462
  }
633
463
  catch {
634
464
  p.log.warn(color.yellow("Doctor had warnings") +
635
- color.dim(" — restart your Claude Code session to pick up the new version"));
636
- }
637
- }
638
- /* -------------------------------------------------------
639
- * Setup
640
- * ------------------------------------------------------- */
641
- async function setup() {
642
- if (process.stdout.isTTY)
643
- console.clear();
644
- p.intro(color.bgCyan(color.black(" context-mode setup ")));
645
- const s = p.spinner();
646
- // Step 1: Detect runtimes
647
- s.start("Detecting installed runtimes");
648
- const runtimes = detectRuntimes();
649
- const available = getAvailableLanguages(runtimes);
650
- s.stop("Detected " + available.length + " languages");
651
- // Show what's available
652
- p.note(getRuntimeSummary(runtimes), "Detected Runtimes");
653
- // Step 2: Check Bun
654
- if (!hasBunRuntime()) {
655
- p.log.warn(color.yellow("Bun is not installed.") +
656
- " JS/TS will run with Node.js (3-5x slower).");
657
- const installBun = await p.confirm({
658
- message: "Would you like to install Bun for faster execution?",
659
- initialValue: true,
660
- });
661
- if (p.isCancel(installBun)) {
662
- p.cancel("Setup cancelled.");
663
- process.exit(0);
664
- }
665
- if (installBun) {
666
- s.start("Installing Bun");
667
- try {
668
- execSync("curl -fsSL https://bun.sh/install | bash", {
669
- stdio: "pipe",
670
- timeout: 60000,
671
- });
672
- s.stop(color.green("Bun installed successfully!"));
673
- // Re-detect runtimes
674
- const newRuntimes = detectRuntimes();
675
- if (hasBunRuntime()) {
676
- p.log.success("JavaScript and TypeScript will now use Bun " +
677
- color.dim("(3-5x faster)"));
678
- }
679
- p.note(getRuntimeSummary(newRuntimes), "Updated Runtimes");
680
- }
681
- catch (err) {
682
- const message = err instanceof Error ? err.message : String(err);
683
- s.stop(color.red("Failed to install Bun"));
684
- p.log.error("Installation failed: " +
685
- message +
686
- "\nYou can install manually: curl -fsSL https://bun.sh/install | bash");
687
- p.log.info(color.dim("Continuing with Node.js — everything will still work."));
688
- }
689
- }
690
- else {
691
- p.log.info(color.dim("No problem! Using Node.js. You can install Bun later: curl -fsSL https://bun.sh/install | bash"));
692
- }
693
- }
694
- else {
695
- p.log.success(color.green("Bun detected!") +
696
- " JS/TS will run at maximum speed.");
697
- }
698
- // Step 3: Check optional runtimes
699
- const missing = [];
700
- if (!runtimes.python)
701
- missing.push("Python (python3)");
702
- if (!runtimes.ruby)
703
- missing.push("Ruby (ruby)");
704
- if (!runtimes.go)
705
- missing.push("Go (go)");
706
- if (!runtimes.php)
707
- missing.push("PHP (php)");
708
- if (!runtimes.r)
709
- missing.push("R (Rscript)");
710
- if (missing.length > 0) {
711
- p.log.info(color.dim("Optional runtimes not found: " + missing.join(", ")));
712
- p.log.info(color.dim("Install them to enable additional language support in context-mode."));
713
- }
714
- // Step 4: Installation instructions
715
- const installMethod = await p.select({
716
- message: "How would you like to configure context-mode?",
717
- options: [
718
- {
719
- value: "claude-code",
720
- label: "Claude Code (recommended)",
721
- hint: "claude mcp add",
722
- },
723
- {
724
- value: "manual",
725
- label: "Show manual configuration",
726
- hint: ".mcp.json",
727
- },
728
- { value: "skip", label: "Skip — I'll configure later" },
729
- ],
730
- });
731
- if (p.isCancel(installMethod)) {
732
- p.cancel("Setup cancelled.");
733
- process.exit(0);
734
- }
735
- const serverPath = new URL("./server.js", import.meta.url).pathname;
736
- if (installMethod === "claude-code") {
737
- s.start("Adding to Claude Code");
738
- try {
739
- execSync(`claude mcp add context-mode -- node ${serverPath}`, { stdio: "pipe", timeout: 10000 });
740
- s.stop(color.green("Added to Claude Code!"));
741
- }
742
- catch {
743
- s.stop(color.yellow("Could not add automatically"));
744
- p.log.info("Run manually:\n" +
745
- color.cyan(` claude mcp add context-mode -- node ${serverPath}`));
746
- }
465
+ color.dim(` — restart your ${adapter.name} session to pick up the new version`));
747
466
  }
748
- else if (installMethod === "manual") {
749
- p.note(JSON.stringify({
750
- mcpServers: {
751
- "context-mode": {
752
- command: "node",
753
- args: [serverPath],
754
- },
755
- },
756
- }, null, 2), "Add to your .mcp.json or Claude Code settings");
757
- }
758
- p.outro(color.green("Setup complete!") +
759
- " " +
760
- color.dim(available.length + " languages ready."));
761
467
  }