context-mode 1.0.155 → 1.0.157

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.155"
9
+ "version": "1.0.157"
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.155",
16
+ "version": "1.0.157",
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.155",
3
+ "version": "1.0.157",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.155",
3
+ "version": "1.0.157",
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.155",
6
+ "version": "1.0.157",
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.155",
3
+ "version": "1.0.157",
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",
@@ -7,6 +7,7 @@
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
9
  import os from "node:os";
10
+ import { execSync } from "node:child_process";
10
11
 
11
12
  const CACHE_TTL_MS = 60_000;
12
13
  const FETCH_TIMEOUT_MS = 2_000;
@@ -110,6 +111,117 @@ export function buildUrl(cfg, _eventType) {
110
111
  return `${cfg.platform_url}/events`;
111
112
  }
112
113
 
114
+ // === Project identity resolution — worktree-invariant canonicalization ===
115
+ // Filesystem path is the wrong identifier for "project": git worktrees fork
116
+ // the path while keeping the same repo, monorepos collapse N packages into
117
+ // one umbrella, and forks of the same repo look like different projects.
118
+ // Resolve to a stable identity using:
119
+ // 1. Closest package.json `name` if it lives DEEPER than the .git root
120
+ // (monorepo sub-package — preserve granularity)
121
+ // 2. git config remote.origin.url, normalized
122
+ // (worktrees of one repo collapse to one identity)
123
+ // 3. Closest package.json `name` at any depth
124
+ // (local-only Node project)
125
+ // 4. basename(projectDir) (last resort)
126
+ const _projectIdentityCache = new Map();
127
+
128
+ function resolveProjectIdentity(projectDir) {
129
+ if (typeof projectDir !== "string" || !projectDir) return null;
130
+ if (_projectIdentityCache.has(projectDir)) return _projectIdentityCache.get(projectDir);
131
+ const id = computeProjectIdentity(projectDir);
132
+ _projectIdentityCache.set(projectDir, id);
133
+ return id;
134
+ }
135
+
136
+ function computeProjectIdentity(projectDir) {
137
+ let absoluteDir;
138
+ try {
139
+ absoluteDir = path.resolve(projectDir);
140
+ } catch {
141
+ return null;
142
+ }
143
+ const walked = walkUpFromDir(absoluteDir);
144
+ const pkg = walked.packageJson;
145
+ const gitTop = walked.gitToplevel;
146
+
147
+ // (1) Monorepo sub-package: package.json STRICTLY deeper than .git root.
148
+ if (pkg && gitTop && pkg.dir !== gitTop && pkg.dir.length > gitTop.length && pkg.name) {
149
+ return pkg.name;
150
+ }
151
+ // (2) Git remote URL.
152
+ const remote = gitTop ? readGitRemote(gitTop) : null;
153
+ if (remote) return normalizeRemoteUrl(remote);
154
+ // (3) Closest package.json (any depth).
155
+ if (pkg?.name) return pkg.name;
156
+ // (4) Basename.
157
+ return path.basename(absoluteDir);
158
+ }
159
+
160
+ function walkUpFromDir(start) {
161
+ let dir = start;
162
+ let pkg = null;
163
+ let gitTop = null;
164
+ // Safety: cap walk to 64 levels; real filesystems never hit this.
165
+ for (let i = 0; i < 64; i++) {
166
+ if (!pkg) {
167
+ const pkgPath = path.join(dir, "package.json");
168
+ if (fs.existsSync(pkgPath)) {
169
+ try {
170
+ const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
171
+ if (typeof parsed?.name === "string" && parsed.name.trim()) {
172
+ pkg = { dir, name: parsed.name.trim() };
173
+ }
174
+ } catch { /* malformed — skip silently */ }
175
+ }
176
+ }
177
+ if (!gitTop && fs.existsSync(path.join(dir, ".git"))) {
178
+ gitTop = dir;
179
+ }
180
+ if (pkg && gitTop) break;
181
+ const parent = path.dirname(dir);
182
+ if (parent === dir) break;
183
+ dir = parent;
184
+ }
185
+ return { packageJson: pkg, gitToplevel: gitTop };
186
+ }
187
+
188
+ function readGitRemote(gitTop) {
189
+ try {
190
+ const url = execSync("git config --get remote.origin.url", {
191
+ cwd: gitTop,
192
+ encoding: "utf8",
193
+ timeout: 500,
194
+ stdio: ["ignore", "pipe", "ignore"],
195
+ }).trim();
196
+ return url || null;
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ // Canonical wire shape: host/path, lowercased host, no scheme, no .git suffix,
203
+ // no embedded credentials. All clone-equivalents collapse to one identity.
204
+ function normalizeRemoteUrl(url) {
205
+ let u = String(url).trim();
206
+ // SSH form (git@host:org/repo) → host/org/repo
207
+ const sshMatch = u.match(/^[a-z0-9_-]+@([^:]+):(.+)$/i);
208
+ if (sshMatch) {
209
+ u = `${sshMatch[1]}/${sshMatch[2]}`;
210
+ } else {
211
+ // scheme://[user[:pass]@]host/path → host/path
212
+ u = u.replace(/^[a-z]+:\/\/(?:[^@/]+@)?/i, "");
213
+ }
214
+ u = u.replace(/\.git\/?$/i, "").replace(/\/+$/, "");
215
+ // Lowercase host segment only (paths can be case-sensitive)
216
+ const slash = u.indexOf("/");
217
+ if (slash > 0) {
218
+ u = u.slice(0, slash).toLowerCase() + u.slice(slash);
219
+ } else {
220
+ u = u.toLowerCase();
221
+ }
222
+ return u;
223
+ }
224
+
113
225
  // === Privacy: secret + PII redaction ===
114
226
  const SECRETS = [
115
227
  /\b(?:ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{20,}\b/g, // GitHub
@@ -157,7 +269,16 @@ export async function maybeForward(event, platform, opts = {}) {
157
269
  const cfg = readConfig();
158
270
  if (!cfg) return;
159
271
 
160
- const ev = sanitizeEvent(event);
272
+ // Project identity must be resolved from the RAW projectDir — the resolver
273
+ // reads `git config` against the actual filesystem path. After sanitize,
274
+ // $HOME-normalization would break the lookup. We overlay the resolved id
275
+ // back onto the event so the sanitize/walk path sees the canonical value
276
+ // (URL or package name, which need no further normalization).
277
+ const resolvedProject = resolveProjectIdentity(event?.projectDir);
278
+ const eventWithProject = resolvedProject !== null
279
+ ? { ...event, project: resolvedProject }
280
+ : event;
281
+ const ev = sanitizeEvent(eventWithProject);
161
282
  const ctrl = new AbortController();
162
283
  const t = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
163
284
 
@@ -170,16 +291,18 @@ export async function maybeForward(event, platform, opts = {}) {
170
291
  "X-Source-Platform": platform,
171
292
  "X-Schema-Version": "2",
172
293
  },
294
+ // Canonical envelope (PRD §5.4 stability ABI):
295
+ // - All event fields passthrough — server-side Zod picks per event.type
296
+ // - `platform` envelope metadata (claude-code, cursor, ...)
297
+ // - `ts` defaulted from event or wall clock
298
+ // Hand-mapping individual fields here is the anti-pattern: every new
299
+ // event field forced a bridge release. With this envelope, new fields
300
+ // ride the existing pipe and the platform schema is the only thing
301
+ // that ever needs to learn them.
173
302
  body: JSON.stringify({
174
- tool: ev.type,
175
- category: ev.category,
176
- error: ev.category === "error" ? 1 : 0,
177
- ts: opts.ts ?? Math.floor(Date.now() / 1000),
303
+ ...ev,
178
304
  platform,
179
- project: opts.project,
180
- session_category: ev.category,
181
- session_type: ev.type,
182
- session_data: typeof ev.data === "string" ? ev.data : undefined,
305
+ ts: ev.ts ?? opts.ts ?? Math.floor(Date.now() / 1000),
183
306
  }),
184
307
  signal: ctrl.signal,
185
308
  });
@@ -202,6 +325,16 @@ export const _internal = {
202
325
  sanitizeEvent,
203
326
  privacyTransform,
204
327
  configPath,
205
- resetState: () => { _cache = null; _cacheLoadedAt = 0; _warned = false; _fsLoads = 0; },
328
+ resolveProjectIdentity,
329
+ normalizeRemoteUrl,
330
+ walkUpFromDir,
331
+ resetState: () => {
332
+ _cache = null;
333
+ _cacheLoadedAt = 0;
334
+ _warned = false;
335
+ _fsLoads = 0;
336
+ _projectIdentityCache.clear();
337
+ },
206
338
  get fsLoads() { return _fsLoads; },
339
+ get projectIdentityCacheSize() { return _projectIdentityCache.size; },
207
340
  };
@@ -107,7 +107,10 @@ export function attributeAndInsertEvents(db, sessionId, events, input, projectDi
107
107
  if (hasPlatformConfig()) {
108
108
  const platform = detectPlatformFromEnv();
109
109
  for (let i = 0; i < events.length; i++) {
110
- maybeForward({ ...events[i], session_id: sessionId, ...attributions[i] }, platform);
110
+ maybeForward(
111
+ { ...events[i], ...attributions[i], session_id: sessionId },
112
+ platform,
113
+ );
111
114
  }
112
115
  }
113
116
 
@@ -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.155",
6
+ "version": "1.0.157",
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.155",
3
+ "version": "1.0.157",
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",