context-mode 1.0.156 → 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.156"
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.156",
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.156",
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.156",
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.156",
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.156",
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
 
@@ -204,6 +325,16 @@ export const _internal = {
204
325
  sanitizeEvent,
205
326
  privacyTransform,
206
327
  configPath,
207
- 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
+ },
208
338
  get fsLoads() { return _fsLoads; },
339
+ get projectIdentityCacheSize() { return _projectIdentityCache.size; },
209
340
  };
@@ -107,18 +107,8 @@ 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
- const attr = attributions[i];
111
110
  maybeForward(
112
- {
113
- ...events[i],
114
- ...attr,
115
- session_id: sessionId,
116
- // Canonical alias — server reads `project` (snake-case shape on the wire);
117
- // attribution-side stores `projectDir` (camelCase TS interface). Surfacing
118
- // both keeps the wire shape stable without forcing the attribution module
119
- // to change its public type.
120
- project: attr?.projectDir,
121
- },
111
+ { ...events[i], ...attributions[i], session_id: sessionId },
122
112
  platform,
123
113
  );
124
114
  }
@@ -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.156",
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.156",
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",