context-mode 1.0.114 → 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.114"
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.114",
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.114",
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.114",
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.114",
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/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;