context-mode 1.0.96 → 1.0.98

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, symlinkSync, mkdirSync, lstatSync } from "node:fs";
4
- import { dirname, resolve, join } from "node:path";
3
+ import { existsSync, chmodSync, readFileSync, writeFileSync, readdirSync, symlinkSync, mkdirSync, lstatSync, unlinkSync } from "node:fs";
4
+ import { dirname, resolve, join, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { homedir } from "node:os";
7
7
 
@@ -59,7 +59,7 @@ if (cacheMatch) {
59
59
  if (newest && newest !== myVersion) {
60
60
  const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
61
61
  for (const [key, entries] of Object.entries(ip.plugins || {})) {
62
- if (!key.toLowerCase().includes("context-mode")) continue;
62
+ if (key !== "context-mode@context-mode") continue;
63
63
  for (const entry of entries) {
64
64
  entry.installPath = resolve(cacheParent, newest);
65
65
  entry.version = newest;
@@ -71,19 +71,23 @@ if (cacheMatch) {
71
71
  }
72
72
 
73
73
  // Reverse heal: if registry points to non-existent dir, create symlink to us
74
+ const cacheRoot = resolve(homedir(), ".claude", "plugins", "cache");
74
75
  if (existsSync(ipPath)) {
75
76
  const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
76
77
  for (const [key, entries] of Object.entries(ip.plugins || {})) {
77
- if (!key.toLowerCase().includes("context-mode")) continue;
78
+ if (key !== "context-mode@context-mode") continue;
78
79
  for (const entry of entries) {
79
80
  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
- }
81
+ if (!rp || existsSync(rp) || rp === __dirname) continue;
82
+ // Path traversal guard: only allow paths inside plugin cache
83
+ if (!resolve(rp).startsWith(cacheRoot + sep)) continue;
84
+ try {
85
+ // Remove dangling symlink before creating new one
86
+ try { if (lstatSync(rp).isSymbolicLink()) unlinkSync(rp); } catch {}
87
+ const rpParent = dirname(rp);
88
+ if (!existsSync(rpParent)) mkdirSync(rpParent, { recursive: true });
89
+ symlinkSync(__dirname, rp, process.platform === "win32" ? "junction" : undefined);
90
+ } catch { /* best effort */ }
87
91
  }
88
92
  }
89
93
  }
@@ -92,43 +96,70 @@ if (cacheMatch) {
92
96
  }
93
97
  }
94
98
 
95
- // ── Self-heal Layer 4: Deploy global SessionStart hook ──
99
+ // ── Self-heal Layer 4: Deploy global SessionStart hook + register in settings.json ──
96
100
  // This hook lives outside the plugin directory (~/.claude/hooks/) so it works
97
101
  // even when the plugin cache is completely broken. It creates symlinks for any
98
102
  // missing plugin cache directories on every session start.
103
+ // Pure Node.js — no bash dependency. Works on Windows, macOS (SIP), Linux.
99
104
  try {
100
105
  const globalHooksDir = resolve(homedir(), ".claude", "hooks");
101
- const healHookPath = resolve(globalHooksDir, "context-mode-cache-heal.sh");
106
+ const healHookPath = resolve(globalHooksDir, "context-mode-cache-heal.mjs");
107
+ // Clean up old bash version if it exists
108
+ const oldBashHook = resolve(globalHooksDir, "context-mode-cache-heal.sh");
109
+ if (existsSync(oldBashHook)) {
110
+ try { unlinkSync(oldBashHook); } catch {}
111
+ }
102
112
  if (!existsSync(healHookPath)) {
103
113
  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");
114
+ const healScript = `#!/usr/bin/env node
115
+ // context-mode plugin cache self-heal (auto-deployed)
116
+ // Fixes anthropics/claude-code#46915: auto-update breaks CLAUDE_PLUGIN_ROOT
117
+ // Pure Node.js — no bash/shell dependency.
118
+ import{existsSync,readdirSync,statSync,symlinkSync,lstatSync,unlinkSync,readFileSync}from"node:fs";
119
+ import{dirname,join,resolve,sep}from"node:path";
120
+ import{homedir}from"node:os";
112
121
  try{
113
- const ip=JSON.parse(fs.readFileSync(process.argv[1],"utf-8"));
122
+ const f=resolve(homedir(),".claude","plugins","installed_plugins.json");
123
+ if(!existsSync(f))process.exit(0);
124
+ const cacheRoot=resolve(homedir(),".claude","plugins","cache");
125
+ const ip=JSON.parse(readFileSync(f,"utf-8"));
114
126
  for(const[k,es]of Object.entries(ip.plugins||{})){
115
- if(!k.toLowerCase().includes("context-mode"))continue;
127
+ if(k!=="context-mode@context-mode")continue;
116
128
  for(const e of es){
117
129
  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());
130
+ if(!p||existsSync(p))continue;
131
+ if(!resolve(p).startsWith(cacheRoot+sep))continue;
132
+ const parent=dirname(p);
133
+ if(!existsSync(parent))continue;
134
+ try{if(lstatSync(p).isSymbolicLink())unlinkSync(p)}catch{}
135
+ const dirs=readdirSync(parent).filter(d=>/^\\d+\\.\\d+/.test(d)&&statSync(join(parent,d)).isDirectory());
122
136
  if(!dirs.length)continue;
123
137
  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{}
138
+ try{symlinkSync(join(parent,dirs[dirs.length-1]),p,process.platform==="win32"?"junction":undefined)}catch{}
125
139
  }
126
140
  }
127
141
  }catch{}
128
- ' "$PLUGINS_FILE" 2>/dev/null || true
129
142
  `;
130
143
  writeFileSync(healHookPath, healScript, { mode: 0o755 });
131
144
  }
145
+ // Register the hook in ~/.claude/settings.json (Claude Code doesn't auto-discover hook files)
146
+ const settingsPath = resolve(homedir(), ".claude", "settings.json");
147
+ if (existsSync(settingsPath)) {
148
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
149
+ const hooks = settings.hooks ?? {};
150
+ const sessionStart = hooks.SessionStart ?? [];
151
+ const alreadyRegistered = sessionStart.some((h) =>
152
+ h.hooks?.some((hh) => hh.command?.includes("context-mode-cache-heal")),
153
+ );
154
+ if (!alreadyRegistered) {
155
+ sessionStart.push({
156
+ hooks: [{ type: "command", command: `node ${healHookPath}` }],
157
+ });
158
+ hooks.SessionStart = sessionStart;
159
+ settings.hooks = hooks;
160
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
161
+ }
162
+ }
132
163
  } catch { /* best effort */ }
133
164
 
134
165
  // Ensure native dependencies + ABI compatibility (shared with hooks via ensure-deps.mjs)