context-mode 1.0.95 → 1.0.96

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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from "node:child_process";
3
- import { existsSync, chmodSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
4
- import { dirname, resolve } from "node:path";
3
+ import { existsSync, chmodSync, readFileSync, writeFileSync, readdirSync, symlinkSync, mkdirSync, lstatSync } from "node:fs";
4
+ import { dirname, resolve, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { homedir } from "node:os";
7
7
 
@@ -29,7 +29,9 @@ if (!process.env.CONTEXT_MODE_PROJECT_DIR) {
29
29
  // - Non-hook platforms: server.ts writeRoutingInstructions() on MCP connect
30
30
  // - Future: explicit `context-mode init` command
31
31
 
32
- // Self-heal: if a newer version dir exists, update registry so next session uses it
32
+ // ── Self-heal Layer 1: Fix registry symlink mismatches (anthropics/claude-code#46915) ──
33
+ // Claude Code auto-update can leave installed_plugins.json pointing to a non-existent
34
+ // directory. We detect this and create symlinks so hooks find the right path.
33
35
  const cacheMatch = __dirname.match(
34
36
  /^(.*[\/\\]plugins[\/\\]cache[\/\\][^\/\\]+[\/\\][^\/\\]+[\/\\])([^\/\\]+)$/,
35
37
  );
@@ -37,6 +39,9 @@ if (cacheMatch) {
37
39
  try {
38
40
  const cacheParent = cacheMatch[1];
39
41
  const myVersion = cacheMatch[2];
42
+ const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
43
+
44
+ // Forward heal: if a newer version dir exists, update registry
40
45
  const dirs = readdirSync(cacheParent).filter((d) =>
41
46
  /^\d+\.\d+\.\d+/.test(d),
42
47
  );
@@ -52,12 +57,6 @@ if (cacheMatch) {
52
57
  });
53
58
  const newest = dirs[dirs.length - 1];
54
59
  if (newest && newest !== myVersion) {
55
- const ipPath = resolve(
56
- homedir(),
57
- ".claude",
58
- "plugins",
59
- "installed_plugins.json",
60
- );
61
60
  const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
62
61
  for (const [key, entries] of Object.entries(ip.plugins || {})) {
63
62
  if (!key.toLowerCase().includes("context-mode")) continue;
@@ -67,11 +66,25 @@ if (cacheMatch) {
67
66
  entry.lastUpdated = new Date().toISOString();
68
67
  }
69
68
  }
70
- writeFileSync(
71
- ipPath,
72
- JSON.stringify(ip, null, 2) + "\n",
73
- "utf-8",
74
- );
69
+ writeFileSync(ipPath, JSON.stringify(ip, null, 2) + "\n", "utf-8");
70
+ }
71
+ }
72
+
73
+ // Reverse heal: if registry points to non-existent dir, create symlink to us
74
+ if (existsSync(ipPath)) {
75
+ const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
76
+ for (const [key, entries] of Object.entries(ip.plugins || {})) {
77
+ if (!key.toLowerCase().includes("context-mode")) continue;
78
+ for (const entry of entries) {
79
+ const rp = entry.installPath;
80
+ if (rp && !existsSync(rp) && rp !== __dirname) {
81
+ try {
82
+ const rpParent = dirname(rp);
83
+ if (!existsSync(rpParent)) mkdirSync(rpParent, { recursive: true });
84
+ symlinkSync(__dirname, rp, process.platform === "win32" ? "junction" : undefined);
85
+ } catch { /* best effort */ }
86
+ }
87
+ }
75
88
  }
76
89
  }
77
90
  } catch {
@@ -79,6 +92,45 @@ if (cacheMatch) {
79
92
  }
80
93
  }
81
94
 
95
+ // ── Self-heal Layer 4: Deploy global SessionStart hook ──
96
+ // This hook lives outside the plugin directory (~/.claude/hooks/) so it works
97
+ // even when the plugin cache is completely broken. It creates symlinks for any
98
+ // missing plugin cache directories on every session start.
99
+ try {
100
+ const globalHooksDir = resolve(homedir(), ".claude", "hooks");
101
+ const healHookPath = resolve(globalHooksDir, "context-mode-cache-heal.sh");
102
+ if (!existsSync(healHookPath)) {
103
+ if (!existsSync(globalHooksDir)) mkdirSync(globalHooksDir, { recursive: true });
104
+ const healScript = `#!/usr/bin/env bash
105
+ # context-mode plugin cache self-heal (auto-deployed)
106
+ # Fixes anthropics/claude-code#46915: auto-update breaks CLAUDE_PLUGIN_ROOT
107
+ set -euo pipefail
108
+ PLUGINS_FILE="$HOME/.claude/plugins/installed_plugins.json"
109
+ [[ -f "$PLUGINS_FILE" ]] || exit 0
110
+ node -e '
111
+ const fs=require("fs"),path=require("path");
112
+ try{
113
+ const ip=JSON.parse(fs.readFileSync(process.argv[1],"utf-8"));
114
+ for(const[k,es]of Object.entries(ip.plugins||{})){
115
+ if(!k.toLowerCase().includes("context-mode"))continue;
116
+ for(const e of es){
117
+ const p=e.installPath;
118
+ if(!p||fs.existsSync(p))continue;
119
+ const parent=path.dirname(p);
120
+ if(!fs.existsSync(parent))continue;
121
+ const dirs=fs.readdirSync(parent).filter(d=>/^\\d+\\.\\d+/.test(d)&&fs.statSync(path.join(parent,d)).isDirectory());
122
+ if(!dirs.length)continue;
123
+ dirs.sort((a,b)=>{const pa=a.split(".").map(Number),pb=b.split(".").map(Number);for(let i=0;i<3;i++){if((pa[i]||0)!==(pb[i]||0))return(pa[i]||0)-(pb[i]||0)}return 0});
124
+ try{fs.symlinkSync(path.join(parent,dirs[dirs.length-1]),p)}catch{}
125
+ }
126
+ }
127
+ }catch{}
128
+ ' "$PLUGINS_FILE" 2>/dev/null || true
129
+ `;
130
+ writeFileSync(healHookPath, healScript, { mode: 0o755 });
131
+ }
132
+ } catch { /* best effort */ }
133
+
82
134
  // Ensure native dependencies + ABI compatibility (shared with hooks via ensure-deps.mjs)
83
135
  // ensure-deps handles better-sqlite3 install + ABI cache/rebuild automatically (#148, #203)
84
136
  import "./hooks/ensure-deps.mjs";