context-mode 1.0.113 → 1.0.115
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 +6 -2
- package/build/util/project-dir.d.ts +32 -2
- package/build/util/project-dir.js +109 -3
- package/cli.bundle.mjs +142 -141
- 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 +98 -97
- package/start.mjs +26 -0
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.115"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.115",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.115",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -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.115",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.115",
|
|
4
4
|
"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.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
package/build/cli.js
CHANGED
|
@@ -679,9 +679,75 @@ async function upgrade() {
|
|
|
679
679
|
};
|
|
680
680
|
writeFileSync(resolve(pluginRoot, ".mcp.json"), JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
681
681
|
s.stop(color.green(`Updated in-place to v${newVersion}`));
|
|
682
|
+
// v1.0.114 hotfix — pre-flight: verify the in-place copy actually
|
|
683
|
+
// wrote a plugin.json carrying newVersion BEFORE we tell the
|
|
684
|
+
// registry that's the install path. If the manifest still reports
|
|
685
|
+
// the old version (rsync race, partial write, files-array drift),
|
|
686
|
+
// updating the registry would create the silent v1.0.113-class
|
|
687
|
+
// drift Mert hit. Bail out — the next /ctx-upgrade gets to retry.
|
|
688
|
+
const pluginManifest = resolve(pluginRoot, ".claude-plugin", "plugin.json");
|
|
689
|
+
let onDiskVersion = null;
|
|
690
|
+
try {
|
|
691
|
+
const pj = JSON.parse(readFileSync(pluginManifest, "utf-8"));
|
|
692
|
+
if (pj && typeof pj.version === "string")
|
|
693
|
+
onDiskVersion = pj.version;
|
|
694
|
+
}
|
|
695
|
+
catch { /* parse error → onDiskVersion stays null */ }
|
|
696
|
+
if (onDiskVersion !== newVersion) {
|
|
697
|
+
throw new Error(`pluginRoot manifest version mismatch — disk says "${onDiskVersion ?? "<missing>"}" but newVersion is "${newVersion}". Refusing to bump registry.`);
|
|
698
|
+
}
|
|
682
699
|
// Fix registry — adapter-aware
|
|
683
700
|
adapter.updatePluginRegistry(pluginRoot, newVersion);
|
|
684
701
|
p.log.info(color.dim(" Registry synced to " + pluginRoot));
|
|
702
|
+
// v1.0.114 hotfix — post-write assertion: re-read installed_plugins.json
|
|
703
|
+
// and verify installPath/.claude-plugin/plugin.json's version matches
|
|
704
|
+
// the registry entry. Throws on mismatch — fails loudly so a future
|
|
705
|
+
// adapter regression surfaces here, not weeks later in user reports.
|
|
706
|
+
try {
|
|
707
|
+
const ipPath = resolve(resolveClaudeConfigDir(), "plugins", "installed_plugins.json");
|
|
708
|
+
if (existsSync(ipPath)) {
|
|
709
|
+
const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
|
|
710
|
+
const entries = ip?.plugins?.["context-mode@context-mode"];
|
|
711
|
+
if (Array.isArray(entries)) {
|
|
712
|
+
for (const entry of entries) {
|
|
713
|
+
const ip2 = entry?.installPath;
|
|
714
|
+
if (typeof ip2 !== "string" || !ip2)
|
|
715
|
+
continue;
|
|
716
|
+
if (!existsSync(ip2)) {
|
|
717
|
+
throw new Error(`installPath does not exist on disk: ${ip2}`);
|
|
718
|
+
}
|
|
719
|
+
const pjPath = resolve(ip2, ".claude-plugin", "plugin.json");
|
|
720
|
+
if (!existsSync(pjPath)) {
|
|
721
|
+
throw new Error(`missing plugin.json manifest at ${pjPath}`);
|
|
722
|
+
}
|
|
723
|
+
const pj = JSON.parse(readFileSync(pjPath, "utf-8"));
|
|
724
|
+
if (pj?.version !== entry.version) {
|
|
725
|
+
throw new Error(`version mismatch — registry says "${entry.version}" but ${pjPath} says "${pj?.version}"`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
catch (err) {
|
|
732
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
733
|
+
throw new Error(`Registry consistency check failed: ${message}`);
|
|
734
|
+
}
|
|
735
|
+
// v1.0.114 hotfix — marketplace post-pull assertion: clone (if
|
|
736
|
+
// present) MUST be on newVersion. Mert's case showed marketplace
|
|
737
|
+
// stuck at v1.0.89 — the sync block above swallowed that silently.
|
|
738
|
+
// Warn (don't throw) — npm-only users have no marketplace clone.
|
|
739
|
+
try {
|
|
740
|
+
const marketplaceManifest = resolve(marketplaceDir, ".claude-plugin", "plugin.json");
|
|
741
|
+
if (existsSync(marketplaceManifest)) {
|
|
742
|
+
const mpj = JSON.parse(readFileSync(marketplaceManifest, "utf-8"));
|
|
743
|
+
if (mpj?.version !== newVersion) {
|
|
744
|
+
p.log.warn(color.yellow("Marketplace clone version mismatch") +
|
|
745
|
+
` — ${marketplaceDir} reports "${mpj?.version}" but expected "${newVersion}"`);
|
|
746
|
+
p.log.info(color.dim(` Run manually: git -C "${marketplaceDir}" fetch --tags origin && git -C "${marketplaceDir}" reset --hard origin/HEAD`));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
catch { /* best effort */ }
|
|
685
751
|
// Install production deps
|
|
686
752
|
s.start("Installing production dependencies");
|
|
687
753
|
npmExecFile(["install", "--production", "--no-audit", "--no-fund"], {
|
package/build/server.js
CHANGED
|
@@ -184,12 +184,16 @@ function getSessionDir() {
|
|
|
184
184
|
function getProjectDir() {
|
|
185
185
|
// Delegated to the shared resolver so the env-var chain rejects plugin
|
|
186
186
|
// install paths (set by a prior MCP boot's start.mjs after `/ctx-upgrade`)
|
|
187
|
-
// and prefers the shell-set PWD before the chdir'd cwd.
|
|
188
|
-
//
|
|
187
|
+
// and prefers the shell-set PWD before the chdir'd cwd. v1.0.115 adds
|
|
188
|
+
// the Claude Code transcript heuristic — read `cwd` from the most-recently-
|
|
189
|
+
// modified `~/.claude/projects/<encoded>/<session>.jsonl` to recover the
|
|
190
|
+
// real project dir when MCP was launched from a non-project cwd (desktop-
|
|
191
|
+
// app launch, /ctx-upgrade respawn). See src/util/project-dir.ts.
|
|
189
192
|
return resolveProjectDir({
|
|
190
193
|
env: process.env,
|
|
191
194
|
cwd: process.cwd(),
|
|
192
195
|
pwd: process.env.PWD,
|
|
196
|
+
transcriptsRoot: join(homedir(), ".claude", "projects"),
|
|
193
197
|
});
|
|
194
198
|
}
|
|
195
199
|
/**
|
|
@@ -26,6 +26,30 @@
|
|
|
26
26
|
* suffix pattern.
|
|
27
27
|
*/
|
|
28
28
|
export declare function isPluginInstallPath(p: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Read the per-session project dir from Claude Code's transcript files.
|
|
31
|
+
*
|
|
32
|
+
* Claude Code writes session transcripts under
|
|
33
|
+
* `~/.claude/projects/<encoded-cwd>/<session-id>.jsonl`. Each line is a JSON
|
|
34
|
+
* event; an early line (typically line 2) carries a `cwd` field with the
|
|
35
|
+
* literal project directory the session is running against. The encoded dir
|
|
36
|
+
* name itself is lossy (`/` and `.` both become `-`), so we read the JSONL.
|
|
37
|
+
*
|
|
38
|
+
* This is the strongest available signal when Claude Code does NOT propagate
|
|
39
|
+
* `CLAUDE_PROJECT_DIR` to the spawned MCP env (the common case when Claude
|
|
40
|
+
* Code is launched from the desktop app rather than `cd <project> && claude`).
|
|
41
|
+
*
|
|
42
|
+
* Returns `undefined` when no transcript exists, the projects dir is empty,
|
|
43
|
+
* or no transcript carries a `cwd` field — caller falls through.
|
|
44
|
+
*
|
|
45
|
+
* Multi-window safety: the most-recently-modified jsonl wins. When the user
|
|
46
|
+
* actively talks to one Claude Code window, that window's transcript is the
|
|
47
|
+
* one being written to RIGHT NOW, so its mtime is freshest. Other windows'
|
|
48
|
+
* transcripts have older mtimes and are correctly ignored.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveProjectDirFromTranscript(opts: {
|
|
51
|
+
projectsRoot: string;
|
|
52
|
+
}): string | undefined;
|
|
29
53
|
/**
|
|
30
54
|
* Pure project-dir resolver. Mirror of the env-var chain inside
|
|
31
55
|
* `src/server.ts getProjectDir()`, but takes its inputs explicitly so the
|
|
@@ -34,10 +58,14 @@ export declare function isPluginInstallPath(p: string): boolean;
|
|
|
34
58
|
* Resolution order:
|
|
35
59
|
* 1. Adapter-priority env vars (CLAUDE / GEMINI / VSCODE / OPENCODE / PI /
|
|
36
60
|
* IDEA / CONTEXT_MODE) — first non-empty AND non-plugin-path wins.
|
|
37
|
-
* 2.
|
|
61
|
+
* 2. Claude Code transcript heuristic — read `cwd` from the most-recently-
|
|
62
|
+
* modified `~/.claude/projects/<encoded>/<session>.jsonl`. This is the
|
|
63
|
+
* most reliable signal when Claude Code launched MCP from a non-project
|
|
64
|
+
* cwd (desktop-app launch, `/ctx-upgrade` respawn, etc.).
|
|
65
|
+
* 3. `process.env.PWD` — shell-set, NOT updated by `process.chdir()`, so
|
|
38
66
|
* it survives the `start.mjs` chdir into the plugin dir. Skipped if
|
|
39
67
|
* it too points at a plugin install path.
|
|
40
|
-
*
|
|
68
|
+
* 4. `cwd` — last resort. Returned even if it is a plugin path; the
|
|
41
69
|
* caller is responsible for rendering a graceful "no project context"
|
|
42
70
|
* message rather than panicking. Keeping the function total preserves
|
|
43
71
|
* operation of project-independent tools (sandbox execute, fetch).
|
|
@@ -46,4 +74,6 @@ export declare function resolveProjectDir(opts: {
|
|
|
46
74
|
env: Record<string, string | undefined>;
|
|
47
75
|
cwd: string;
|
|
48
76
|
pwd: string | undefined;
|
|
77
|
+
/** Optional override; production code passes `~/.claude/projects`. */
|
|
78
|
+
transcriptsRoot?: string;
|
|
49
79
|
}): string;
|
|
@@ -30,6 +30,103 @@ export function isPluginInstallPath(p) {
|
|
|
30
30
|
return false;
|
|
31
31
|
return /[/\\]\.claude[/\\]plugins[/\\](cache|marketplaces)[/\\]/.test(p);
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Read the per-session project dir from Claude Code's transcript files.
|
|
35
|
+
*
|
|
36
|
+
* Claude Code writes session transcripts under
|
|
37
|
+
* `~/.claude/projects/<encoded-cwd>/<session-id>.jsonl`. Each line is a JSON
|
|
38
|
+
* event; an early line (typically line 2) carries a `cwd` field with the
|
|
39
|
+
* literal project directory the session is running against. The encoded dir
|
|
40
|
+
* name itself is lossy (`/` and `.` both become `-`), so we read the JSONL.
|
|
41
|
+
*
|
|
42
|
+
* This is the strongest available signal when Claude Code does NOT propagate
|
|
43
|
+
* `CLAUDE_PROJECT_DIR` to the spawned MCP env (the common case when Claude
|
|
44
|
+
* Code is launched from the desktop app rather than `cd <project> && claude`).
|
|
45
|
+
*
|
|
46
|
+
* Returns `undefined` when no transcript exists, the projects dir is empty,
|
|
47
|
+
* or no transcript carries a `cwd` field — caller falls through.
|
|
48
|
+
*
|
|
49
|
+
* Multi-window safety: the most-recently-modified jsonl wins. When the user
|
|
50
|
+
* actively talks to one Claude Code window, that window's transcript is the
|
|
51
|
+
* one being written to RIGHT NOW, so its mtime is freshest. Other windows'
|
|
52
|
+
* transcripts have older mtimes and are correctly ignored.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveProjectDirFromTranscript(opts) {
|
|
55
|
+
// Inline imports kept private to this function — keeps the module test-
|
|
56
|
+
// friendly when fs is stubbed at the call sites that don't use this path.
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
58
|
+
const fs = require("node:fs");
|
|
59
|
+
const path = require("node:path");
|
|
60
|
+
if (!fs.existsSync(opts.projectsRoot))
|
|
61
|
+
return undefined;
|
|
62
|
+
let bestPath;
|
|
63
|
+
let bestMtime = 0;
|
|
64
|
+
try {
|
|
65
|
+
for (const dir of fs.readdirSync(opts.projectsRoot)) {
|
|
66
|
+
const dirPath = path.join(opts.projectsRoot, dir);
|
|
67
|
+
let stat;
|
|
68
|
+
try {
|
|
69
|
+
stat = fs.statSync(dirPath);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!stat.isDirectory())
|
|
75
|
+
continue;
|
|
76
|
+
let files;
|
|
77
|
+
try {
|
|
78
|
+
files = fs.readdirSync(dirPath);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const f of files) {
|
|
84
|
+
if (!f.endsWith(".jsonl"))
|
|
85
|
+
continue;
|
|
86
|
+
const fp = path.join(dirPath, f);
|
|
87
|
+
try {
|
|
88
|
+
const m = fs.statSync(fp).mtimeMs;
|
|
89
|
+
if (m > bestMtime) {
|
|
90
|
+
bestMtime = m;
|
|
91
|
+
bestPath = fp;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch { /* skip */ }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (!bestPath)
|
|
102
|
+
return undefined;
|
|
103
|
+
// Read first ~10 lines until we find a cwd field. The jsonl is
|
|
104
|
+
// append-only and can be huge (60+ MB on long sessions) — never load it
|
|
105
|
+
// into memory; stream a small head buffer.
|
|
106
|
+
try {
|
|
107
|
+
const fd = fs.openSync(bestPath, "r");
|
|
108
|
+
try {
|
|
109
|
+
const buf = Buffer.alloc(8192);
|
|
110
|
+
const bytes = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
111
|
+
const text = buf.subarray(0, bytes).toString("utf-8");
|
|
112
|
+
for (const line of text.split("\n").slice(0, 10)) {
|
|
113
|
+
if (!line.trim())
|
|
114
|
+
continue;
|
|
115
|
+
try {
|
|
116
|
+
const obj = JSON.parse(line);
|
|
117
|
+
if (typeof obj.cwd === "string" && obj.cwd.length > 0)
|
|
118
|
+
return obj.cwd;
|
|
119
|
+
}
|
|
120
|
+
catch { /* skip malformed line */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
fs.closeSync(fd);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch { /* file vanished mid-read */ }
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
33
130
|
/**
|
|
34
131
|
* Pure project-dir resolver. Mirror of the env-var chain inside
|
|
35
132
|
* `src/server.ts getProjectDir()`, but takes its inputs explicitly so the
|
|
@@ -38,16 +135,20 @@ export function isPluginInstallPath(p) {
|
|
|
38
135
|
* Resolution order:
|
|
39
136
|
* 1. Adapter-priority env vars (CLAUDE / GEMINI / VSCODE / OPENCODE / PI /
|
|
40
137
|
* IDEA / CONTEXT_MODE) — first non-empty AND non-plugin-path wins.
|
|
41
|
-
* 2.
|
|
138
|
+
* 2. Claude Code transcript heuristic — read `cwd` from the most-recently-
|
|
139
|
+
* modified `~/.claude/projects/<encoded>/<session>.jsonl`. This is the
|
|
140
|
+
* most reliable signal when Claude Code launched MCP from a non-project
|
|
141
|
+
* cwd (desktop-app launch, `/ctx-upgrade` respawn, etc.).
|
|
142
|
+
* 3. `process.env.PWD` — shell-set, NOT updated by `process.chdir()`, so
|
|
42
143
|
* it survives the `start.mjs` chdir into the plugin dir. Skipped if
|
|
43
144
|
* it too points at a plugin install path.
|
|
44
|
-
*
|
|
145
|
+
* 4. `cwd` — last resort. Returned even if it is a plugin path; the
|
|
45
146
|
* caller is responsible for rendering a graceful "no project context"
|
|
46
147
|
* message rather than panicking. Keeping the function total preserves
|
|
47
148
|
* operation of project-independent tools (sandbox execute, fetch).
|
|
48
149
|
*/
|
|
49
150
|
export function resolveProjectDir(opts) {
|
|
50
|
-
const { env, cwd, pwd } = opts;
|
|
151
|
+
const { env, cwd, pwd, transcriptsRoot } = opts;
|
|
51
152
|
const candidates = [
|
|
52
153
|
env.CLAUDE_PROJECT_DIR,
|
|
53
154
|
env.GEMINI_PROJECT_DIR,
|
|
@@ -61,6 +162,11 @@ export function resolveProjectDir(opts) {
|
|
|
61
162
|
if (c && !isPluginInstallPath(c))
|
|
62
163
|
return c;
|
|
63
164
|
}
|
|
165
|
+
if (transcriptsRoot) {
|
|
166
|
+
const fromTranscript = resolveProjectDirFromTranscript({ projectsRoot: transcriptsRoot });
|
|
167
|
+
if (fromTranscript && !isPluginInstallPath(fromTranscript))
|
|
168
|
+
return fromTranscript;
|
|
169
|
+
}
|
|
64
170
|
if (pwd && !isPluginInstallPath(pwd))
|
|
65
171
|
return pwd;
|
|
66
172
|
return cwd;
|