context-mode 1.0.133 → 1.0.135

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/start.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { execSync } from "node:child_process";
2
+ import { execSync, spawn } from "node:child_process";
3
3
  import { existsSync, chmodSync, readFileSync, writeFileSync, readdirSync, symlinkSync, mkdirSync, lstatSync, unlinkSync } from "node:fs";
4
4
  import { dirname, resolve, join, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -9,6 +9,24 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
9
9
  const originalCwd = process.cwd();
10
10
  process.chdir(__dirname);
11
11
 
12
+ // Resolve the Claude Code config dir, honoring $CLAUDE_CONFIG_DIR (incl. leading ~).
13
+ // Mirrors hooks/session-helpers.mjs::resolveConfigDir and hooks/run-hook.mjs (#453).
14
+ // Inlined here because start.mjs runs before any other module loads — we cannot
15
+ // dynamic-import session-helpers without circularity through the bundle path.
16
+ // Fix for #577: cache-heal layer below was hardcoding ~/.claude regardless of
17
+ // the env var, silently no-op'ing for users with a non-default config dir AND
18
+ // creating an unwanted ~/.claude/ directory on disk.
19
+ function resolveClaudeConfigDir() {
20
+ const envVal = process.env.CLAUDE_CONFIG_DIR;
21
+ if (envVal && envVal.trim() !== "") {
22
+ if (envVal.startsWith("~")) {
23
+ return resolve(homedir(), envVal.replace(/^~[/\\]?/, ""));
24
+ }
25
+ return resolve(envVal);
26
+ }
27
+ return resolve(homedir(), ".claude");
28
+ }
29
+
12
30
  // Plugin-install-path guard (mirror of src/util/project-dir.ts isPluginInstallPath
13
31
  // — duplicated here because start.mjs ships as raw JS and cannot import TS).
14
32
  // When Claude Code runs `/ctx-upgrade` it kills + respawns the MCP server with
@@ -41,6 +59,39 @@ if (!process.env.CONTEXT_MODE_PROJECT_DIR && safeOriginalCwd) {
41
59
  // - Non-hook platforms: server.ts writeRoutingInstructions() on MCP connect
42
60
  // - Future: explicit `context-mode init` command
43
61
 
62
+ // ── Linux: re-exec with Bun to avoid better-sqlite3 SIGSEGV (#564) ──
63
+ // server.bundle.mjs has two SQLite paths: bun:sqlite (safe) or better-sqlite3
64
+ // (SIGSEGV on Linux under Node's V8). When invoked via node on Linux, detect
65
+ // a Bun installation and re-exec this file under Bun so the bundle takes the
66
+ // safe path. No-op when already running under Bun or on non-Linux platforms.
67
+ if (typeof globalThis.Bun === "undefined" && process.platform === "linux") {
68
+ const bunCandidates = [
69
+ process.env.BUN_INSTALL ? join(process.env.BUN_INSTALL, "bin", "bun") : null,
70
+ join(homedir(), ".bun", "bin", "bun"),
71
+ "/usr/local/bin/bun",
72
+ "/usr/bin/bun",
73
+ ].filter(Boolean);
74
+ const bunBin = bunCandidates.find((p) => existsSync(p));
75
+ if (bunBin) {
76
+ const child = spawn(bunBin, [fileURLToPath(import.meta.url)], {
77
+ stdio: ["pipe", "inherit", "inherit"],
78
+ env: process.env,
79
+ });
80
+ process.stdin.on("data", (chunk) => {
81
+ if (!child.stdin.destroyed) child.stdin.write(chunk);
82
+ });
83
+ process.stdin.on("end", () => {});
84
+ const _keepAlive = setInterval(() => {}, 2147483647);
85
+ child.on("exit", (code) => {
86
+ clearInterval(_keepAlive);
87
+ process.exit(code ?? 0);
88
+ });
89
+ // Prevent rest of start.mjs from running — child owns the MCP session.
90
+ process.stdin.resume();
91
+ await new Promise(() => {}); // park this process forever
92
+ }
93
+ }
94
+
44
95
  // ── Self-heal Layer 1: Fix registry → symlink mismatches (anthropics/claude-code#46915) ──
45
96
  // Claude Code auto-update can leave installed_plugins.json pointing to a non-existent
46
97
  // directory. We detect this and create symlinks so hooks find the right path.
@@ -51,7 +102,8 @@ if (cacheMatch) {
51
102
  try {
52
103
  const cacheParent = cacheMatch[1];
53
104
  const myVersion = cacheMatch[2];
54
- const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
105
+ const claudeConfigDir = resolveClaudeConfigDir();
106
+ const ipPath = resolve(claudeConfigDir, "plugins", "installed_plugins.json");
55
107
 
56
108
  // Forward heal: if a newer version dir exists, update registry
57
109
  const dirs = readdirSync(cacheParent).filter((d) =>
@@ -83,7 +135,7 @@ if (cacheMatch) {
83
135
  }
84
136
 
85
137
  // Reverse heal: if registry points to non-existent dir, create symlink to us
86
- const cacheRoot = resolve(homedir(), ".claude", "plugins", "cache");
138
+ const cacheRoot = resolve(claudeConfigDir, "plugins", "cache");
87
139
  if (existsSync(ipPath)) {
88
140
  const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
89
141
  for (const [key, entries] of Object.entries(ip.plugins || {})) {
@@ -125,9 +177,10 @@ try {
125
177
  const { healInstalledPlugins, healSettingsEnabledPlugins, healPluginJsonMcpServers, healMcpJsonArgs } =
126
178
  await import("./scripts/heal-installed-plugins.mjs");
127
179
  const pluginKey = "context-mode@context-mode";
128
- const registryPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
129
- const pluginCacheRoot = resolve(homedir(), ".claude", "plugins", "cache");
130
- const settingsPath = resolve(homedir(), ".claude", "settings.json");
180
+ const claudeConfigDir = resolveClaudeConfigDir();
181
+ const registryPath = resolve(claudeConfigDir, "plugins", "installed_plugins.json");
182
+ const pluginCacheRoot = resolve(claudeConfigDir, "plugins", "cache");
183
+ const settingsPath = resolve(claudeConfigDir, "settings.json");
131
184
  try { healInstalledPlugins({ registryPath, pluginCacheRoot, pluginKey }); }
132
185
  catch { /* best effort */ }
133
186
  // v1.0.116: Claude Code's plugin loader reads settings.json.enabledPlugins
@@ -191,7 +244,12 @@ try {
191
244
  const { buildHookCommand, selfHealCacheHealHook, ensureShebangAndExecBit } =
192
245
  await import("./hooks/cache-heal-utils.mjs");
193
246
 
194
- const globalHooksDir = resolve(homedir(), ".claude", "hooks");
247
+ // #577: honor $CLAUDE_CONFIG_DIR — without this, Claude Code spawns hooks
248
+ // from $CLAUDE_CONFIG_DIR/settings.json but we deploy them to ~/.claude/hooks/
249
+ // and register them in ~/.claude/settings.json. The mismatch silently
250
+ // disables the heal AND creates an unwanted ~/.claude directory.
251
+ const claudeConfigDir = resolveClaudeConfigDir();
252
+ const globalHooksDir = resolve(claudeConfigDir, "hooks");
195
253
  const healHookPath = resolve(globalHooksDir, "context-mode-cache-heal.mjs");
196
254
  // Clean up old bash version if it exists
197
255
  const oldBashHook = resolve(globalHooksDir, "context-mode-cache-heal.sh");
@@ -203,14 +261,17 @@ try {
203
261
  const healScript = `#!/usr/bin/env node
204
262
  // context-mode plugin cache self-heal (auto-deployed)
205
263
  // Fixes anthropics/claude-code#46915: auto-update breaks CLAUDE_PLUGIN_ROOT
264
+ // Honors CLAUDE_CONFIG_DIR (#577) — checked at this script's runtime so users
265
+ // who set CLAUDE_CONFIG_DIR after install still get healed correctly.
206
266
  // Pure Node.js — no bash/shell dependency.
207
267
  import{existsSync,readdirSync,statSync,symlinkSync,lstatSync,unlinkSync,readFileSync}from"node:fs";
208
268
  import{dirname,join,resolve,sep}from"node:path";
209
269
  import{homedir}from"node:os";
270
+ function cfgDir(){const e=process.env.CLAUDE_CONFIG_DIR;if(e&&e.trim()!==""){return e.startsWith("~")?resolve(homedir(),e.replace(/^~[/\\\\]?/,"")):resolve(e)}return resolve(homedir(),".claude")}
210
271
  try{
211
- const f=resolve(homedir(),".claude","plugins","installed_plugins.json");
272
+ const f=resolve(cfgDir(),"plugins","installed_plugins.json");
212
273
  if(!existsSync(f))process.exit(0);
213
- const cacheRoot=resolve(homedir(),".claude","plugins","cache");
274
+ const cacheRoot=resolve(cfgDir(),"plugins","cache");
214
275
  const ip=JSON.parse(readFileSync(f,"utf-8"));
215
276
  for(const[k,es]of Object.entries(ip.plugins||{})){
216
277
  if(k!=="context-mode@context-mode")continue;
@@ -238,8 +299,9 @@ try{
238
299
  try { ensureShebangAndExecBit(healHookPath); } catch { /* best effort */ }
239
300
  }
240
301
 
241
- // Register the hook in ~/.claude/settings.json (Claude Code doesn't auto-discover hook files)
242
- const settingsPath = resolve(homedir(), ".claude", "settings.json");
302
+ // Register the hook in $CLAUDE_CONFIG_DIR/settings.json (Claude Code doesn't auto-discover hook files).
303
+ // #577: must follow the same dir resolution as globalHooksDir above.
304
+ const settingsPath = resolve(claudeConfigDir, "settings.json");
243
305
  if (existsSync(settingsPath)) {
244
306
  const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
245
307
  const hooks = settings.hooks ?? {};