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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.113"
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.113",
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.113",
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.113",
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.113",
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. See
188
- // src/util/project-dir.ts for the rationale + safety rules.
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. `process.env.PWD` shell-set, NOT updated by `process.chdir()`, so
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
- * 3. `cwd` — last resort. Returned even if it is a plugin path; the
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. `process.env.PWD` shell-set, NOT updated by `process.chdir()`, so
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
- * 3. `cwd` — last resort. Returned even if it is a plugin path; the
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;