context-mode 1.0.112 → 1.0.114
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/cli.js +66 -0
- package/build/server.js +10 -8
- package/build/util/project-dir.d.ts +49 -0
- package/build/util/project-dir.js +67 -0
- package/cli.bundle.mjs +137 -137
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/heal-installed-plugins.mjs +129 -0
- package/scripts/postinstall.mjs +58 -0
- package/server.bundle.mjs +73 -73
- package/start.mjs +42 -4
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.114",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.114",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP plugin that saves 98% of your context window. Works with Claude Code, Gemini CLI, VS Code Copilot, OpenCode, and Codex CLI. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
|
|
6
6
|
"author": "Mert Koseoğlu",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"start.mjs",
|
|
80
80
|
"scripts/postinstall.mjs",
|
|
81
81
|
"scripts/heal-better-sqlite3.mjs",
|
|
82
|
+
"scripts/heal-installed-plugins.mjs",
|
|
82
83
|
"README.md",
|
|
83
84
|
"LICENSE"
|
|
84
85
|
],
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-heal `~/.claude/plugins/installed_plugins.json` (#46915 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* v1.0.113's `/ctx-upgrade` poisoned this file in two ways:
|
|
5
|
+
* 1. Per-entry `version` drifted from the actual cache directory's
|
|
6
|
+
* `plugin.json` version.
|
|
7
|
+
* 2. The top-level `enabledPlugins[<key>]` was emptied (or never set)
|
|
8
|
+
* so Claude Code's plugin loader skipped context-mode → MCP died.
|
|
9
|
+
*
|
|
10
|
+
* Single source of truth shared by:
|
|
11
|
+
* - `start.mjs` HEAL 3+4 (every MCP boot)
|
|
12
|
+
* - `scripts/postinstall.mjs` (every `npm install -g context-mode`)
|
|
13
|
+
*
|
|
14
|
+
* Pure Node.js (built-ins only). Best-effort: never throws, always
|
|
15
|
+
* returns a plain result object so callers can log a one-liner.
|
|
16
|
+
*
|
|
17
|
+
* @see https://github.com/anthropics/claude-code/issues/46915
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { resolve, sep } from "node:path";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} HealResult
|
|
25
|
+
* @property {string[]} healed - one of: "entry-version", "enabled-plugins"
|
|
26
|
+
* @property {string} [skipped] - reason if no work performed
|
|
27
|
+
* @property {string} [error] - error message if heal aborted
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Heal a single plugin entry inside installed_plugins.json.
|
|
32
|
+
*
|
|
33
|
+
* @param {{
|
|
34
|
+
* registryPath: string,
|
|
35
|
+
* pluginCacheRoot: string,
|
|
36
|
+
* pluginKey: string,
|
|
37
|
+
* }} opts
|
|
38
|
+
* @returns {HealResult}
|
|
39
|
+
*/
|
|
40
|
+
export function healInstalledPlugins({ registryPath, pluginCacheRoot, pluginKey }) {
|
|
41
|
+
if (!registryPath || !existsSync(registryPath)) {
|
|
42
|
+
return { healed: [], skipped: "no-registry" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = readFileSync(registryPath, "utf-8");
|
|
48
|
+
} catch (err) {
|
|
49
|
+
return { healed: [], error: `read-failed: ${(err && err.message) || err}` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let ip;
|
|
53
|
+
try {
|
|
54
|
+
ip = JSON.parse(raw);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return { healed: [], error: `parse-failed: ${(err && err.message) || err}` };
|
|
57
|
+
}
|
|
58
|
+
if (!ip || typeof ip !== "object") {
|
|
59
|
+
return { healed: [], error: "bad-shape" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const entries = (ip.plugins && ip.plugins[pluginKey]) || [];
|
|
63
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
64
|
+
return { healed: [], skipped: "no-entry" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @type {string[]} */
|
|
68
|
+
const healed = [];
|
|
69
|
+
let syncedVersion = null;
|
|
70
|
+
|
|
71
|
+
// ── HEAL 3: per-entry version <- cache plugin.json version ──
|
|
72
|
+
// We trust the cache directory because that's what start.mjs actually
|
|
73
|
+
// boots from; the registry is just a stale label.
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (!entry || typeof entry !== "object") continue;
|
|
76
|
+
const installPath = entry.installPath;
|
|
77
|
+
if (!installPath || typeof installPath !== "string") continue;
|
|
78
|
+
|
|
79
|
+
// Path-traversal guard: only consult plugin.json files inside the
|
|
80
|
+
// declared plugin cache root.
|
|
81
|
+
const resolvedInstall = resolve(installPath);
|
|
82
|
+
const cacheRootWithSep = resolve(pluginCacheRoot) + sep;
|
|
83
|
+
if (!resolvedInstall.startsWith(cacheRootWithSep)) continue;
|
|
84
|
+
|
|
85
|
+
const cachePluginJson = resolve(installPath, ".claude-plugin", "plugin.json");
|
|
86
|
+
if (!existsSync(cachePluginJson)) continue;
|
|
87
|
+
let actualVersion = null;
|
|
88
|
+
try {
|
|
89
|
+
const pj = JSON.parse(readFileSync(cachePluginJson, "utf-8"));
|
|
90
|
+
if (pj && typeof pj.version === "string" && pj.version) {
|
|
91
|
+
actualVersion = pj.version;
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!actualVersion) continue;
|
|
97
|
+
|
|
98
|
+
syncedVersion = actualVersion;
|
|
99
|
+
if (entry.version !== actualVersion) {
|
|
100
|
+
entry.version = actualVersion;
|
|
101
|
+
if (!healed.includes("entry-version")) healed.push("entry-version");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── HEAL 4: top-level enabledPlugins[key] presence ──
|
|
106
|
+
// Claude Code's plugin loader checks enabledPlugins. When /ctx-upgrade
|
|
107
|
+
// emptied it, our plugin was silently disabled. Set it to `true` (the
|
|
108
|
+
// simplest enabled-flag form) when missing or falsy.
|
|
109
|
+
if (syncedVersion) {
|
|
110
|
+
if (!ip.enabledPlugins || typeof ip.enabledPlugins !== "object" || Array.isArray(ip.enabledPlugins)) {
|
|
111
|
+
ip.enabledPlugins = {};
|
|
112
|
+
}
|
|
113
|
+
const current = ip.enabledPlugins[pluginKey];
|
|
114
|
+
if (current === undefined || current === null || current === false || current === "") {
|
|
115
|
+
ip.enabledPlugins[pluginKey] = true;
|
|
116
|
+
healed.push("enabled-plugins");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (healed.length > 0) {
|
|
121
|
+
try {
|
|
122
|
+
writeFileSync(registryPath, JSON.stringify(ip, null, 2) + "\n", "utf-8");
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return { healed: [], error: `write-failed: ${(err && err.message) || err}` };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { healed };
|
|
129
|
+
}
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -14,10 +14,33 @@ import { dirname, resolve, join, sep } from "node:path";
|
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { healBetterSqlite3Binding } from "./heal-better-sqlite3.mjs";
|
|
17
|
+
import { healInstalledPlugins } from "./heal-installed-plugins.mjs";
|
|
17
18
|
|
|
18
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const pkgRoot = resolve(__dirname, "..");
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* True when running as a real `npm install -g context-mode`. We use this
|
|
24
|
+
* to keep contributors' local `npm install` runs from rewriting their HOME's
|
|
25
|
+
* Claude Code registry (would be very surprising during dev).
|
|
26
|
+
*
|
|
27
|
+
* Heuristic: npm sets `npm_config_global=true` for global installs AND the
|
|
28
|
+
* package directory has no nearby `.git` (a contributor's clone always
|
|
29
|
+
* does). Both signals must agree.
|
|
30
|
+
*/
|
|
31
|
+
function isGlobalInstall() {
|
|
32
|
+
if (process.env.npm_config_global !== "true") return false;
|
|
33
|
+
// Walk up a few levels looking for .git — contributors always have one.
|
|
34
|
+
let dir = pkgRoot;
|
|
35
|
+
for (let i = 0; i < 4; i++) {
|
|
36
|
+
if (existsSync(join(dir, ".git"))) return false;
|
|
37
|
+
const parent = dirname(dir);
|
|
38
|
+
if (parent === dir) break;
|
|
39
|
+
dir = parent;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
/**
|
|
22
45
|
* Validate that a path is safe to interpolate into a cmd.exe command.
|
|
23
46
|
* Rejects characters that could enable command injection via cmd.exe.
|
|
@@ -26,6 +49,41 @@ function isSafeWindowsPath(p) {
|
|
|
26
49
|
return !/[&|<>"^%\r\n]/.test(p);
|
|
27
50
|
}
|
|
28
51
|
|
|
52
|
+
// ── -1. v1.0.114 hotfix — installed_plugins.json registry repair ─────
|
|
53
|
+
// /ctx-upgrade in v1.0.113 poisoned the registry (entry.version drifted
|
|
54
|
+
// + enabledPlugins emptied), making Claude Code's plugin loader skip
|
|
55
|
+
// context-mode entirely. start.mjs HEAL 3+4 fix this on every MCP boot,
|
|
56
|
+
// but already-broken users have no MCP to boot — they need the heal to
|
|
57
|
+
// run from npm postinstall. Shared module so both call sites stay in
|
|
58
|
+
// sync. Only runs in real `npm install -g` to avoid surprising
|
|
59
|
+
// contributors. Best effort, never blocks install. (#46915 follow-up.)
|
|
60
|
+
if (isGlobalInstall()) {
|
|
61
|
+
try {
|
|
62
|
+
const registryPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
63
|
+
const pluginCacheRoot = resolve(homedir(), ".claude", "plugins", "cache");
|
|
64
|
+
const result = healInstalledPlugins({
|
|
65
|
+
registryPath,
|
|
66
|
+
pluginCacheRoot,
|
|
67
|
+
pluginKey: "context-mode@context-mode",
|
|
68
|
+
});
|
|
69
|
+
if (result.skipped === "no-registry") {
|
|
70
|
+
// Standalone npm user (no Claude Code) — silent success.
|
|
71
|
+
process.stderr.write("context-mode: install OK, no Claude Code registry found\n");
|
|
72
|
+
} else if (result.error) {
|
|
73
|
+
process.stderr.write(`context-mode: install OK, registry heal skipped (${result.error})\n`);
|
|
74
|
+
} else if (result.healed && result.healed.length > 0) {
|
|
75
|
+
process.stderr.write(`context-mode: healed installed_plugins.json (${result.healed.join(", ")})\n`);
|
|
76
|
+
} else {
|
|
77
|
+
process.stderr.write("context-mode: install OK, no heal needed\n");
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// Never block install on a heal failure.
|
|
81
|
+
try {
|
|
82
|
+
process.stderr.write(`context-mode: install OK, heal aborted (${(err && err.message) || err})\n`);
|
|
83
|
+
} catch { /* truly best effort */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
29
87
|
// ── 0. Self-heal Layer 3: Backward symlink for stale registry (anthropics/claude-code#46915) ──
|
|
30
88
|
// When this install completes, installed_plugins.json may still point to an old
|
|
31
89
|
// non-existent path. Create a symlink from that old path → our new directory.
|