context-mode 1.0.95 → 1.0.97
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/cache-heal.d.ts +48 -0
- package/build/cache-heal.js +150 -0
- package/build/server.js +48 -2
- package/cli.bundle.mjs +93 -93
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.mjs +30 -2
- package/server.bundle.mjs +69 -69
- package/start.mjs +98 -15
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, 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
|
|
|
@@ -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
|
|
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,26 +57,38 @@ 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
|
-
if (
|
|
62
|
+
if (key !== "context-mode@context-mode") continue;
|
|
64
63
|
for (const entry of entries) {
|
|
65
64
|
entry.installPath = resolve(cacheParent, newest);
|
|
66
65
|
entry.version = newest;
|
|
67
66
|
entry.lastUpdated = new Date().toISOString();
|
|
68
67
|
}
|
|
69
68
|
}
|
|
70
|
-
writeFileSync(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
const cacheRoot = resolve(homedir(), ".claude", "plugins", "cache");
|
|
75
|
+
if (existsSync(ipPath)) {
|
|
76
|
+
const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
|
|
77
|
+
for (const [key, entries] of Object.entries(ip.plugins || {})) {
|
|
78
|
+
if (key !== "context-mode@context-mode") continue;
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const rp = entry.installPath;
|
|
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 */ }
|
|
91
|
+
}
|
|
75
92
|
}
|
|
76
93
|
}
|
|
77
94
|
} catch {
|
|
@@ -79,6 +96,72 @@ if (cacheMatch) {
|
|
|
79
96
|
}
|
|
80
97
|
}
|
|
81
98
|
|
|
99
|
+
// ── Self-heal Layer 4: Deploy global SessionStart hook + register in settings.json ──
|
|
100
|
+
// This hook lives outside the plugin directory (~/.claude/hooks/) so it works
|
|
101
|
+
// even when the plugin cache is completely broken. It creates symlinks for any
|
|
102
|
+
// missing plugin cache directories on every session start.
|
|
103
|
+
// Pure Node.js — no bash dependency. Works on Windows, macOS (SIP), Linux.
|
|
104
|
+
try {
|
|
105
|
+
const globalHooksDir = resolve(homedir(), ".claude", "hooks");
|
|
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
|
+
}
|
|
112
|
+
if (!existsSync(healHookPath)) {
|
|
113
|
+
if (!existsSync(globalHooksDir)) mkdirSync(globalHooksDir, { recursive: true });
|
|
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";
|
|
121
|
+
try{
|
|
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"));
|
|
126
|
+
for(const[k,es]of Object.entries(ip.plugins||{})){
|
|
127
|
+
if(k!=="context-mode@context-mode")continue;
|
|
128
|
+
for(const e of es){
|
|
129
|
+
const p=e.installPath;
|
|
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());
|
|
136
|
+
if(!dirs.length)continue;
|
|
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});
|
|
138
|
+
try{symlinkSync(join(parent,dirs[dirs.length-1]),p,process.platform==="win32"?"junction":undefined)}catch{}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}catch{}
|
|
142
|
+
`;
|
|
143
|
+
writeFileSync(healHookPath, healScript, { mode: 0o755 });
|
|
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
|
+
}
|
|
163
|
+
} catch { /* best effort */ }
|
|
164
|
+
|
|
82
165
|
// Ensure native dependencies + ABI compatibility (shared with hooks via ensure-deps.mjs)
|
|
83
166
|
// ensure-deps handles better-sqlite3 install + ABI cache/rebuild automatically (#148, #203)
|
|
84
167
|
import "./hooks/ensure-deps.mjs";
|