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