context-mode 0.9.8 → 0.9.10

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.
@@ -13,7 +13,7 @@
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": "0.9.8",
16
+ "version": "0.9.10",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "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.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
package/build/cli.js CHANGED
@@ -143,8 +143,18 @@ async function doctor() {
143
143
  let criticalFails = 0;
144
144
  const s = p.spinner();
145
145
  s.start("Running diagnostics");
146
- const runtimes = detectRuntimes();
147
- const available = getAvailableLanguages(runtimes);
146
+ let runtimes;
147
+ let available;
148
+ try {
149
+ runtimes = detectRuntimes();
150
+ available = getAvailableLanguages(runtimes);
151
+ }
152
+ catch {
153
+ s.stop("Diagnostics partial");
154
+ p.log.warn(color.yellow("Could not detect runtimes") + color.dim(" — module may be missing, restart session after upgrade"));
155
+ p.outro(color.yellow("Doctor could not fully run — try again after restarting Claude Code"));
156
+ return 1;
157
+ }
148
158
  s.stop("Diagnostics complete");
149
159
  // Runtime check
150
160
  p.note(getRuntimeSummary(runtimes), "Runtimes");
@@ -189,9 +199,14 @@ async function doctor() {
189
199
  }
190
200
  }
191
201
  catch (err) {
192
- criticalFails++;
193
202
  const message = err instanceof Error ? err.message : String(err);
194
- p.log.error(color.red("Server test: FAIL") + ` — ${message}`);
203
+ if (message.includes("Cannot find module") || message.includes("MODULE_NOT_FOUND")) {
204
+ p.log.warn(color.yellow("Server test: SKIP") + color.dim(" — module not available (restart session after upgrade)"));
205
+ }
206
+ else {
207
+ criticalFails++;
208
+ p.log.error(color.red("Server test: FAIL") + ` — ${message}`);
209
+ }
195
210
  }
196
211
  // Hooks installed
197
212
  p.log.step("Checking hooks configuration...");
@@ -275,11 +290,16 @@ async function doctor() {
275
290
  }
276
291
  }
277
292
  catch (err) {
278
- criticalFails++;
279
293
  const message = err instanceof Error ? err.message : String(err);
280
- p.log.error(color.red("FTS5 / better-sqlite3: FAIL") +
281
- `${message}` +
282
- color.dim("\n Try: npm rebuild better-sqlite3"));
294
+ if (message.includes("Cannot find module") || message.includes("MODULE_NOT_FOUND")) {
295
+ p.log.warn(color.yellow("FTS5 / better-sqlite3: SKIP") + color.dim(" module not available (restart session after upgrade)"));
296
+ }
297
+ else {
298
+ criticalFails++;
299
+ p.log.error(color.red("FTS5 / better-sqlite3: FAIL") +
300
+ ` — ${message}` +
301
+ color.dim("\n Try: npm rebuild better-sqlite3"));
302
+ }
283
303
  }
284
304
  // Version check
285
305
  p.log.step("Checking versions...");
@@ -370,34 +390,41 @@ async function upgrade() {
370
390
  timeout: 30000,
371
391
  });
372
392
  s.stop("Built successfully");
373
- // Step 3: Install fresh into new version directory (don't touch old dir - process runs from it)
374
- s.start("Installing fresh");
393
+ // Step 3: Update in-place (same directory, no registry changes needed)
394
+ s.start("Updating files in-place");
395
+ // Clean stale version dirs from previous upgrade attempts
375
396
  const cacheParentMatch = pluginRoot.match(/^(.*[\\/]plugins[\\/]cache[\\/][^\\/]+[\\/][^\\/]+[\\/])/);
376
- const freshDir = cacheParentMatch
377
- ? resolve(cacheParentMatch[1], newVersion)
378
- : pluginRoot;
379
- // If fresh dir is different from current, create it. Otherwise update in-place.
380
- if (freshDir !== pluginRoot) {
381
- rmSync(freshDir, { recursive: true, force: true });
382
- cpSync(srcDir, freshDir, { recursive: true });
383
- pluginRoot = freshDir;
384
- }
385
- else {
386
- // Same dir — copy files in-place
387
- const items = [
388
- "build", "src", "hooks", "skills", ".claude-plugin",
389
- "start.mjs", "server.bundle.mjs", "package.json", ".mcp.json",
390
- ];
391
- for (const item of items) {
392
- try {
393
- rmSync(resolve(pluginRoot, item), { recursive: true, force: true });
394
- cpSync(resolve(srcDir, item), resolve(pluginRoot, item), { recursive: true });
397
+ if (cacheParentMatch) {
398
+ const cacheParent = cacheParentMatch[1];
399
+ const myDir = pluginRoot.replace(cacheParent, "").replace(/[\\/]/g, "");
400
+ try {
401
+ const oldDirs = readdirSync(cacheParent).filter(d => d !== myDir);
402
+ for (const d of oldDirs) {
403
+ try {
404
+ rmSync(resolve(cacheParent, d), { recursive: true, force: true });
405
+ }
406
+ catch { /* skip */ }
407
+ }
408
+ if (oldDirs.length > 0) {
409
+ p.log.info(color.dim(` Cleaned ${oldDirs.length} stale cache dir(s)`));
395
410
  }
396
- catch { /* some files may not exist */ }
397
411
  }
412
+ catch { /* parent may not exist */ }
398
413
  }
399
- s.stop(color.green(`Installed to ${freshDir.split("/").pop() ?? newVersion}`));
400
- // Install production deps in fresh dir
414
+ // Copy new files over old ones — same path, no registry update needed
415
+ const items = [
416
+ "build", "src", "hooks", "skills", ".claude-plugin",
417
+ "start.mjs", "server.bundle.mjs", "package.json", ".mcp.json",
418
+ ];
419
+ for (const item of items) {
420
+ try {
421
+ rmSync(resolve(pluginRoot, item), { recursive: true, force: true });
422
+ cpSync(resolve(srcDir, item), resolve(pluginRoot, item), { recursive: true });
423
+ }
424
+ catch { /* some files may not exist in source */ }
425
+ }
426
+ s.stop(color.green(`Updated in-place to v${newVersion}`));
427
+ // Install production deps (rebuild native modules if needed)
401
428
  s.start("Installing production dependencies");
402
429
  execSync("npm install --production --no-audit --no-fund", {
403
430
  cwd: pluginRoot,
@@ -405,65 +432,6 @@ async function upgrade() {
405
432
  timeout: 60000,
406
433
  });
407
434
  s.stop("Dependencies ready");
408
- // Step 4: Update installed_plugins.json via spawned script (works from any old version)
409
- s.start("Updating plugin registry");
410
- try {
411
- const scriptPath = resolve(tmpDir + "-post-upgrade.mjs");
412
- const scriptContent = [
413
- `import { readFileSync, writeFileSync, readdirSync, rmSync as rm } from "fs";`,
414
- `import { resolve } from "path";`,
415
- `import { homedir } from "os";`,
416
- `const pluginRoot = ${JSON.stringify(pluginRoot)};`,
417
- `const newVersion = ${JSON.stringify(newVersion)};`,
418
- `const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");`,
419
- `try {`,
420
- ` const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8"));`,
421
- ` const plugins = ipRaw.plugins || {};`,
422
- ` let updated = false;`,
423
- ` for (const [key, entries] of Object.entries(plugins)) {`,
424
- ` if (!key.toLowerCase().includes("context-mode")) continue;`,
425
- ` for (const entry of entries) {`,
426
- ` entry.installPath = pluginRoot;`,
427
- ` entry.version = newVersion;`,
428
- ` entry.lastUpdated = new Date().toISOString();`,
429
- ` updated = true;`,
430
- ` }`,
431
- ` }`,
432
- ` if (updated) {`,
433
- ` writeFileSync(ipPath, JSON.stringify(ipRaw, null, 2) + "\\n", "utf-8");`,
434
- ` console.log("REGISTRY_UPDATED");`,
435
- ` }`,
436
- `} catch (e) { console.error("REGISTRY_FAILED:" + e.message); }`,
437
- `const curDir = pluginRoot.split(/[\\/\\\\]/).pop();`,
438
- `const parDir = pluginRoot.replace(/[\\/\\\\][^\\/\\\\]+$/, "");`,
439
- `try {`,
440
- ` for (const d of readdirSync(parDir).filter(x => x !== curDir)) {`,
441
- ` try { rm(resolve(parDir, d), { recursive: true, force: true }); console.log("CLEANED:" + d); } catch {}`,
442
- ` }`,
443
- `} catch {}`,
444
- ].join("\n");
445
- writeFileSync(scriptPath, scriptContent, "utf-8");
446
- const result = execSync(`node ${scriptPath}`, {
447
- stdio: "pipe",
448
- timeout: 10000,
449
- encoding: "utf-8",
450
- });
451
- try {
452
- rmSync(scriptPath, { force: true });
453
- }
454
- catch { /* ignore */ }
455
- if (result.includes("REGISTRY_UPDATED")) {
456
- s.stop(color.green("Plugin registry updated"));
457
- changes.push("Updated plugin registry");
458
- }
459
- else {
460
- s.stop(color.yellow("Plugin registry unchanged"));
461
- }
462
- }
463
- catch (err) {
464
- s.stop(color.yellow("Plugin registry update skipped"));
465
- p.log.warn(color.yellow("Could not update plugin registry") + color.dim(` — ${err instanceof Error ? err.message : String(err)}`));
466
- }
467
435
  // Update global npm package from same GitHub source
468
436
  s.start("Updating npm global package");
469
437
  try {
@@ -578,12 +546,19 @@ async function upgrade() {
578
546
  else {
579
547
  p.log.info(color.dim("No changes were needed."));
580
548
  }
581
- // Step 7: Run doctor
549
+ // Step 7: Run doctor from updated pluginRoot
582
550
  p.log.step("Running doctor to verify...");
583
551
  console.log();
584
- const doctorCode = await doctor();
585
- if (doctorCode !== 0) {
586
- process.exit(doctorCode);
552
+ try {
553
+ execSync(`node "${resolve(pluginRoot, "build", "cli.js")}" doctor`, {
554
+ stdio: "inherit",
555
+ timeout: 30000,
556
+ cwd: pluginRoot,
557
+ });
558
+ }
559
+ catch {
560
+ p.log.warn(color.yellow("Doctor had warnings") +
561
+ color.dim(" — restart your Claude Code session to pick up the new version"));
587
562
  }
588
563
  }
589
564
  /* -------------------------------------------------------
@@ -6,6 +6,44 @@
6
6
  * Cross-platform (Windows/macOS/Linux) — no bash/jq dependency.
7
7
  */
8
8
 
9
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
10
+ import { resolve, dirname, basename } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { homedir, tmpdir } from "node:os";
13
+
14
+ // ─── Self-heal registry (runs once per session via marker file) ───
15
+ try {
16
+ const marker = resolve(tmpdir(), "context-mode-registry-healed");
17
+ if (!existsSync(marker)) {
18
+ const hookDir = dirname(fileURLToPath(import.meta.url));
19
+ const myRoot = resolve(hookDir, "..");
20
+ const myVersion = basename(myRoot);
21
+
22
+ if (/^\d+\.\d+\.\d+/.test(myVersion)) {
23
+ const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
24
+ const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
25
+ let fixed = false;
26
+
27
+ for (const [key, entries] of Object.entries(ip.plugins || {})) {
28
+ if (!key.toLowerCase().includes("context-mode")) continue;
29
+ for (const entry of entries) {
30
+ if (entry.installPath !== myRoot) {
31
+ entry.installPath = myRoot;
32
+ entry.version = myVersion;
33
+ entry.lastUpdated = new Date().toISOString();
34
+ fixed = true;
35
+ }
36
+ }
37
+ }
38
+
39
+ if (fixed) {
40
+ writeFileSync(ipPath, JSON.stringify(ip, null, 2) + "\n", "utf-8");
41
+ }
42
+ writeFileSync(marker, Date.now().toString(), "utf-8");
43
+ }
44
+ }
45
+ } catch { /* best effort — don't block hook */ }
46
+
9
47
  let raw = "";
10
48
  process.stdin.setEncoding("utf-8");
11
49
  for await (const chunk of process.stdin) raw += chunk;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "type": "module",
5
5
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
6
6
  "author": "Mert Koseoğlu",
@@ -18,14 +18,29 @@ Pull latest from GitHub and reinstall the plugin.
18
18
  ```
19
19
  node "<PLUGIN_ROOT>/build/cli.js" upgrade
20
20
  ```
21
- 3. **IMPORTANT**: After the Bash tool completes, re-display the key results as markdown text directly in the conversation so the user sees them without expanding the tool output. Format as:
21
+ 3. **Registry verification** (CRITICAL do this even if upgrade reported success):
22
+ Run a Bash command that does ALL of the following in a single `node -e` script:
23
+ - Read `~/.claude/plugins/installed_plugins.json`
24
+ - Find the `context-mode@claude-context-mode` entry (or any key containing `context-mode`)
25
+ - List directories in `~/.claude/plugins/cache/claude-context-mode/context-mode/`
26
+ - Find the newest semver directory (e.g., `0.9.9` > `0.7.0`)
27
+ - If `installPath` does NOT already point to the newest directory, update:
28
+ - `installPath` → full path to newest version dir
29
+ - `version` → the newest version string
30
+ - `lastUpdated` → `new Date().toISOString()`
31
+ - Write updated JSON back to the file
32
+ - Print what was changed (or "already correct")
33
+
34
+ 4. **IMPORTANT**: After the Bash tool completes, re-display the key results as markdown text directly in the conversation so the user sees them without expanding the tool output. Format as:
22
35
  ```
23
36
  ## context-mode upgrade
24
37
  - [x] Pulled latest from GitHub
25
- - [x] Built and installed v0.6.2
38
+ - [x] Built and installed v0.9.9
26
39
  - [x] npm global updated
27
40
  - [x] Hooks configured
28
41
  - [x] Permissions set
42
+ - [x] Registry verified
29
43
  - [x] Doctor: all checks PASS
30
44
  ```
31
45
  Use `[x]` for success, `[ ]` for failure. Show the actual version numbers and any warnings.
46
+ Tell the user to **restart their Claude Code session** to pick up the new version.
package/start.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from "node:child_process";
3
- import { existsSync } from "node:fs";
3
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { homedir } from "node:os";
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
9
  process.chdir(__dirname);
@@ -11,6 +12,56 @@ if (!process.env.CLAUDE_PROJECT_DIR) {
11
12
  process.env.CLAUDE_PROJECT_DIR = process.cwd();
12
13
  }
13
14
 
15
+ // Self-heal: if a newer version dir exists, update registry so next session uses it
16
+ const cacheMatch = __dirname.match(
17
+ /^(.*[\/\\]plugins[\/\\]cache[\/\\][^\/\\]+[\/\\][^\/\\]+[\/\\])([^\/\\]+)$/,
18
+ );
19
+ if (cacheMatch) {
20
+ try {
21
+ const cacheParent = cacheMatch[1];
22
+ const myVersion = cacheMatch[2];
23
+ const dirs = readdirSync(cacheParent).filter((d) =>
24
+ /^\d+\.\d+\.\d+/.test(d),
25
+ );
26
+ if (dirs.length > 1) {
27
+ dirs.sort((a, b) => {
28
+ const pa = a.split(".").map(Number);
29
+ const pb = b.split(".").map(Number);
30
+ for (let i = 0; i < 3; i++) {
31
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0))
32
+ return (pa[i] ?? 0) - (pb[i] ?? 0);
33
+ }
34
+ return 0;
35
+ });
36
+ const newest = dirs[dirs.length - 1];
37
+ if (newest && newest !== myVersion) {
38
+ const ipPath = resolve(
39
+ homedir(),
40
+ ".claude",
41
+ "plugins",
42
+ "installed_plugins.json",
43
+ );
44
+ const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
45
+ for (const [key, entries] of Object.entries(ip.plugins || {})) {
46
+ if (!key.toLowerCase().includes("context-mode")) continue;
47
+ for (const entry of entries) {
48
+ entry.installPath = resolve(cacheParent, newest);
49
+ entry.version = newest;
50
+ entry.lastUpdated = new Date().toISOString();
51
+ }
52
+ }
53
+ writeFileSync(
54
+ ipPath,
55
+ JSON.stringify(ip, null, 2) + "\n",
56
+ "utf-8",
57
+ );
58
+ }
59
+ }
60
+ } catch {
61
+ /* best effort — don't block server startup */
62
+ }
63
+ }
64
+
14
65
  // Ensure native module is available
15
66
  if (!existsSync(resolve(__dirname, "node_modules", "better-sqlite3"))) {
16
67
  try {