context-mode 1.0.134 → 1.0.135

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.134"
9
+ "version": "1.0.135"
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.134",
16
+ "version": "1.0.135",
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.134",
3
+ "version": "1.0.135",
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.134",
6
+ "version": "1.0.135",
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.134",
3
+ "version": "1.0.135",
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",
@@ -11,7 +11,9 @@
11
11
  * CLAUDE_PROJECT_DIR, CLAUDE_SESSION_ID | ~/.claude/
12
12
  * - Gemini CLI: GEMINI_PROJECT_DIR (hooks), GEMINI_CLI (MCP) | ~/.gemini/
13
13
  * - KiloCode: KILO, KILO_PID | ~/.config/kilo/
14
- * - OpenCode: OPENCODE, OPENCODE_PID | ~/.config/opencode/
14
+ * - OpenCode: OPENCODE_PROJECT_DIR, OPENCODE_CLIENT,
15
+ * OPENCODE_TERMINAL, OPENCODE, OPENCODE_PID |
16
+ * ~/.config/opencode/
15
17
  * - OpenClaw: OPENCLAW_HOME, OPENCLAW_CLI | ~/.openclaw/
16
18
  * - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
17
19
  * - Cursor: CURSOR_TRACE_ID (MCP), CURSOR_CLI (terminal) | ~/.cursor/
@@ -11,7 +11,9 @@
11
11
  * CLAUDE_PROJECT_DIR, CLAUDE_SESSION_ID | ~/.claude/
12
12
  * - Gemini CLI: GEMINI_PROJECT_DIR (hooks), GEMINI_CLI (MCP) | ~/.gemini/
13
13
  * - KiloCode: KILO, KILO_PID | ~/.config/kilo/
14
- * - OpenCode: OPENCODE, OPENCODE_PID | ~/.config/opencode/
14
+ * - OpenCode: OPENCODE_PROJECT_DIR, OPENCODE_CLIENT,
15
+ * OPENCODE_TERMINAL, OPENCODE, OPENCODE_PID |
16
+ * ~/.config/opencode/
15
17
  * - OpenClaw: OPENCLAW_HOME, OPENCLAW_CLI | ~/.openclaw/
16
18
  * - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
17
19
  * - Cursor: CURSOR_TRACE_ID (MCP), CURSOR_CLI (terminal) | ~/.cursor/
@@ -109,12 +111,15 @@ const _PLATFORM_ENV_VARS_RAW = [
109
111
  { name: "KILO_PID", role: "identification" },
110
112
  ]],
111
113
  // opencode — sst/opencode packages/opencode/src/index.ts:108-109 sets
112
- // OPENCODE=1 + OPENCODE_PID=<pid> on every CLI invocation.
114
+ // OPENCODE=1 + OPENCODE_PID=<pid> on CLI invocations. OpenCode desktop
115
+ // shells also expose OPENCODE_CLIENT=desktop and OPENCODE_TERMINAL=1.
113
116
  // OPENCODE_PROJECT_DIR is the documented workspace var (consumed by the
114
117
  // legacy resolver cascade) — listed first so the workspace cascade picks
115
118
  // it up under strict mode.
116
119
  ["opencode", [
117
120
  { name: "OPENCODE_PROJECT_DIR", role: "workspace" },
121
+ { name: "OPENCODE_CLIENT", role: "identification" },
122
+ { name: "OPENCODE_TERMINAL", role: "identification" },
118
123
  { name: "OPENCODE", role: "identification" },
119
124
  { name: "OPENCODE_PID", role: "identification" },
120
125
  ]],
@@ -85,6 +85,14 @@ export declare class MCPStdioClient {
85
85
  initialize(): Promise<void>;
86
86
  listTools(): Promise<MCPTool[]>;
87
87
  callTool(name: string, args: unknown): Promise<MCPCallResult>;
88
+ /**
89
+ * Respawn the MCP child after an exit (clean idle shutdown or crash).
90
+ * Resets state so a fresh `start()` + `initialize()` cycle runs, then
91
+ * the caller's pending request flows through the new child.
92
+ *
93
+ * Internal — exposed only via the public `callTool()` happy path.
94
+ */
95
+ private respawn;
88
96
  shutdown(): void;
89
97
  }
90
98
  /**
@@ -284,8 +284,40 @@ export class MCPStdioClient {
284
284
  return Array.isArray(result.tools) ? result.tools : [];
285
285
  }
286
286
  async callTool(name, args) {
287
+ // Respawn-on-idle-exit (#583). The MCP server gained an idle
288
+ // self-shutdown in 1.0.132 (#565/#568, src/lifecycle.ts). When the
289
+ // Pi-spawned child exits cleanly after the idle window, Pi keeps the
290
+ // tool handles registered, but the bridge client is `exited=true`
291
+ // and every subsequent request would reject with
292
+ // "MCP server has exited" — leaving Pi's ctx_* tools permanently
293
+ // broken until the user restarts Pi.
294
+ //
295
+ // The structural fix is here, not in lifecycle.ts: the bridge owns
296
+ // the child lifecycle, so it transparently respawns + re-initialises
297
+ // the server on the next call. Restores parity with adapters whose
298
+ // host MCP client respawns on EOF (Claude Code, Codex, etc.).
299
+ if (this.exited)
300
+ await this.respawn();
287
301
  return this.request("tools/call", { name, arguments: args ?? {} }, DEFAULT_CALL_TIMEOUT_MS);
288
302
  }
303
+ /**
304
+ * Respawn the MCP child after an exit (clean idle shutdown or crash).
305
+ * Resets state so a fresh `start()` + `initialize()` cycle runs, then
306
+ * the caller's pending request flows through the new child.
307
+ *
308
+ * Internal — exposed only via the public `callTool()` happy path.
309
+ */
310
+ async respawn() {
311
+ // Drop the dead child handle and clear stream buffer so leftover
312
+ // bytes from the previous incarnation don't get parsed as JSON-RPC
313
+ // for the new one. Pending map is already cleared by onExit().
314
+ this.child = null;
315
+ this.buffer = "";
316
+ this.exited = false;
317
+ this.initialized = false;
318
+ this.start();
319
+ await this.initialize();
320
+ }
289
321
  shutdown() {
290
322
  if (!this.child)
291
323
  return;
package/build/cli.js CHANGED
@@ -933,6 +933,22 @@ async function upgrade(opts) {
933
933
  const message = err instanceof Error ? err.message : String(err);
934
934
  throw new Error(`.mcp.json drift check failed: ${message}`);
935
935
  }
936
+ // v1.0.X — Layer 7 heal: update user-level ~/.claude.json MCP server
937
+ // registrations that point to old context-mode version dirs.
938
+ // (anthropics/claude-code#59310 workaround — see heal-installed-plugins.mjs)
939
+ try {
940
+ // @ts-expect-error — JS module, no TS declarations
941
+ const { healClaudeJsonMcpArgs } = await import("../scripts/heal-installed-plugins.mjs");
942
+ const dotClaudeJson = resolve(homedir(), ".claude.json");
943
+ const pluginCacheParent = resolve(resolveClaudeConfigDir(), "plugins", "cache", "context-mode", "context-mode");
944
+ const result = healClaudeJsonMcpArgs({ dotClaudeJsonPath: dotClaudeJson, pluginCacheParent, newPluginRoot: pluginRoot });
945
+ if (result.healed && result.healed.length > 0) {
946
+ p.log.info(color.dim(" ~/.claude.json user MCP registrations updated → " + newVersion));
947
+ }
948
+ }
949
+ catch {
950
+ /* best effort — never block upgrade */
951
+ }
936
952
  // v1.0.114 hotfix — marketplace post-pull assertion: clone (if
937
953
  // present) MUST be on newVersion. Mert's case showed marketplace
938
954
  // stuck at v1.0.89 — the sync block above swallowed that silently.
@@ -1151,6 +1167,7 @@ async function upgrade(opts) {
1151
1167
  stdio: "inherit",
1152
1168
  timeout: 30000,
1153
1169
  cwd: pluginRoot,
1170
+ env: { ...process.env, CONTEXT_MODE_PLATFORM: detection.platform },
1154
1171
  });
1155
1172
  }
1156
1173
  catch {
package/build/runtime.js CHANGED
@@ -12,11 +12,14 @@ import { existsSync } from "node:fs";
12
12
  * Match is case-insensitive; `.exe` extension tolerated for Windows binaries.
13
13
  */
14
14
  const ALLOWED_SHELL_BASENAMES = /^(bash|sh|zsh|dash|pwsh|powershell|cmd)(\.exe)?$/i;
15
+ const BUN_BASENAME = /^bun(\.exe)?$/i;
16
+ function runtimeBasename(runtimePath) {
17
+ const segments = runtimePath.split(/[\\/]/);
18
+ return segments[segments.length - 1] ?? runtimePath;
19
+ }
15
20
  export function isAllowlistedShell(shellPath) {
16
21
  // Cross-OS basename: split on either separator, take the last segment.
17
- const segments = shellPath.split(/[\\/]/);
18
- const base = segments[segments.length - 1];
19
- return ALLOWED_SHELL_BASENAMES.test(base);
22
+ return ALLOWED_SHELL_BASENAMES.test(runtimeBasename(shellPath));
20
23
  }
21
24
  const isWindows = process.platform === "win32";
22
25
  function commandExists(cmd) {
@@ -305,14 +308,14 @@ export function getAvailableLanguages(runtimes) {
305
308
  export function buildCommand(runtimes, language, filePath) {
306
309
  switch (language) {
307
310
  case "javascript":
308
- return runtimes.javascript.endsWith("bun")
311
+ return BUN_BASENAME.test(runtimeBasename(runtimes.javascript))
309
312
  ? [runtimes.javascript, "run", filePath]
310
313
  : [runtimes.javascript, filePath];
311
314
  case "typescript":
312
315
  if (!runtimes.typescript) {
313
316
  throw new Error("No TypeScript runtime available. Install one of: bun (recommended), tsx (npm i -g tsx), or ts-node.");
314
317
  }
315
- if (runtimes.typescript?.endsWith("bun"))
318
+ if (BUN_BASENAME.test(runtimeBasename(runtimes.typescript)))
316
319
  return [runtimes.typescript, "run", filePath];
317
320
  if (runtimes.typescript === "tsx")
318
321
  return ["tsx", filePath];
@@ -557,19 +557,6 @@ export declare const adapterLabels: Record<string, string>;
557
557
  * information. Scale awareness comes from the unit jump between rows.
558
558
  */
559
559
  export declare function kb(b: number): string;
560
- /**
561
- * Locale + IANA-timezone detection for the narrative renderer.
562
- *
563
- * Cascade (each level overrides the next):
564
- * 1. CONTEXT_MODE_LOCALE / CONTEXT_MODE_TZ env overrides
565
- * (used by tests + by users who want to pin output regardless of OS).
566
- * 2. macOS `defaults read -g AppleLocale` → `en_TR` style → `en-TR`.
567
- * 3. Linux `LANG` / `LC_TIME` env vars.
568
- * 4. Fallback: `Intl.DateTimeFormat().resolvedOptions().locale`.
569
- *
570
- * Timezone always uses `Intl.DateTimeFormat().resolvedOptions().timeZone`
571
- * — that one's always available and correct regardless of platform.
572
- */
573
560
  export declare function detectLocaleAndTz(): {
574
561
  locale: string;
575
562
  tz: string;
@@ -1217,9 +1217,45 @@ function formatDuration(uptimeMin) {
1217
1217
  * Timezone always uses `Intl.DateTimeFormat().resolvedOptions().timeZone`
1218
1218
  * — that one's always available and correct regardless of platform.
1219
1219
  */
1220
+ /**
1221
+ * Validate that a locale string is a usable BCP 47 tag.
1222
+ *
1223
+ * Ubuntu GHA runners default to `LANG=C.UTF-8`. The extractor below strips
1224
+ * that to `"C"` — a valid POSIX locale identifier but NOT a BCP 47 tag.
1225
+ * On macOS / Node 20, `new Intl.DateTimeFormat("C", …)` throws RangeError
1226
+ * outright. CI run 25887250971 caught this via the v1.0.134 SLICE B test.
1227
+ *
1228
+ * Earlier fix attempt used a permissive `supportedLocalesOf || construction`
1229
+ * OR check — that was wrong: on Linux + Node 22.5, `new Intl.DateTimeFormat
1230
+ * ("POSIX")` does NOT throw, it silently falls back to the root locale and
1231
+ * still emits garbage at format time. CI run 25904838577 surfaced that —
1232
+ * "POSIX" round-tripped through the validator unchanged.
1233
+ *
1234
+ * Strict gate: `Intl.DateTimeFormat.supportedLocalesOf(tag)` returns `[]` for
1235
+ * any tag that doesn't map to a real language (regardless of whether
1236
+ * construction with that tag throws). That's the contract we want — "is this
1237
+ * a BCP 47 tag the host actually has data for". Construction is an explicit
1238
+ * sanity check; both must pass.
1239
+ */
1240
+ function isUsableBcp47Locale(raw) {
1241
+ if (!raw)
1242
+ return false;
1243
+ try {
1244
+ if (Intl.DateTimeFormat.supportedLocalesOf(raw).length === 0)
1245
+ return false;
1246
+ // Belt: confirm construction doesn't throw on this host either.
1247
+ new Intl.DateTimeFormat(raw);
1248
+ return true;
1249
+ }
1250
+ catch {
1251
+ return false;
1252
+ }
1253
+ }
1220
1254
  export function detectLocaleAndTz() {
1221
1255
  const env = (process.env ?? {});
1222
1256
  let locale = env.CONTEXT_MODE_LOCALE ?? "";
1257
+ if (locale && !isUsableBcp47Locale(locale))
1258
+ locale = "";
1223
1259
  if (!locale) {
1224
1260
  if (process.platform === "darwin") {
1225
1261
  try {
@@ -1234,11 +1270,18 @@ export function detectLocaleAndTz() {
1234
1270
  locale = out.replace(/_/g, "-");
1235
1271
  }
1236
1272
  catch { /* defaults missing or sandbox */ }
1273
+ if (locale && !isUsableBcp47Locale(locale))
1274
+ locale = "";
1237
1275
  }
1238
1276
  if (!locale && (env.LC_TIME || env.LANG)) {
1239
1277
  const raw = (env.LC_TIME || env.LANG || "").split(".")[0];
1240
1278
  if (raw)
1241
1279
  locale = raw.replace(/_/g, "-");
1280
+ // POSIX locale identifiers (`C`, `POSIX`) survive the simple extraction
1281
+ // above but blow up `new Intl.DateTimeFormat(locale, ...)`. Drop and
1282
+ // fall through to the host-default branch below.
1283
+ if (locale && !isUsableBcp47Locale(locale))
1284
+ locale = "";
1242
1285
  }
1243
1286
  if (!locale) {
1244
1287
  try {
@@ -1258,7 +1301,13 @@ export function detectLocaleAndTz() {
1258
1301
  tz = "UTC";
1259
1302
  }
1260
1303
  }
1261
- return { locale: locale || "en-US", tz: tz || "UTC" };
1304
+ // Final belt-and-suspenders: if the locale we settled on is somehow still
1305
+ // unusable (env mutation between detection and return, contributor adding
1306
+ // a new extraction path that skips the validator), fall back to en-US so
1307
+ // formatLocalDateTime / monthDay / weekdayCap never throw at render time.
1308
+ if (!isUsableBcp47Locale(locale))
1309
+ locale = "en-US";
1310
+ return { locale, tz: tz || "UTC" };
1262
1311
  }
1263
1312
  /**
1264
1313
  * Format an absolute path as a human-friendly display string by
@@ -13,12 +13,18 @@ export declare function resolveClaudeGlobalSettingsPath(env?: NodeJS.ProcessEnv)
13
13
  * adapter is non-claude — claude is already covered by entry 2).
14
14
  * 2. The claude global settings.json (always — defense in depth).
15
15
  *
16
- * Lazy import of `./adapters/detect.js` keeps this file free of any direct
17
- * adapter dependency: the detect module itself only `import type`s adapter
18
- * types at the top level (concrete adapters are loaded dynamically inside
19
- * `getAdapter()`), so a static import is safe — but we use `createRequire`
20
- * to make the dependency direction crystal clear and to avoid surprising
21
- * future maintainers who add eager adapter imports to detect.ts.
16
+ * Static import of `../adapters/detect.js` is safe detect.ts only imports
17
+ * `node:` builtins, `./types.js` (type-only), and `./client-map.js` (pure
18
+ * data). It does NOT import claude-config back, so no cycle.
19
+ *
20
+ * History: this used `createRequire(import.meta.url).resolve(...)` to lazy-
21
+ * load detect at call time. That pattern requires `require(esm)`, which is
22
+ * flag-gated on Node 22.x before 22.12 (`--experimental-require-module`).
23
+ * CI run 25877550371 on Node 22.5 silently failed every detect.* call —
24
+ * the catch block ate the error and every cross-adapter deny-policy test
25
+ * returned an empty policy list. Static import sidesteps the require(esm)
26
+ * gate entirely, so the same code works on every supported Node version
27
+ * (20.x, 22.5, 22.12+, 24+) without needing the experimental flag.
22
28
  *
23
29
  * The returned array is deduplicated and order-stable: adapter-specific path
24
30
  * first (most specific), claude global second (fallback).
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import { resolve } from "node:path";
25
25
  import { homedir } from "node:os";
26
- import { createRequire } from "node:module";
26
+ import { detectPlatform, getSessionDirSegments } from "../adapters/detect.js";
27
27
  export function resolveClaudeConfigDir(env = process.env) {
28
28
  const envVal = env.CLAUDE_CONFIG_DIR;
29
29
  if (envVal && envVal.trim() !== "") {
@@ -50,34 +50,27 @@ export function resolveClaudeGlobalSettingsPath(env = process.env) {
50
50
  * adapter is non-claude — claude is already covered by entry 2).
51
51
  * 2. The claude global settings.json (always — defense in depth).
52
52
  *
53
- * Lazy import of `./adapters/detect.js` keeps this file free of any direct
54
- * adapter dependency: the detect module itself only `import type`s adapter
55
- * types at the top level (concrete adapters are loaded dynamically inside
56
- * `getAdapter()`), so a static import is safe — but we use `createRequire`
57
- * to make the dependency direction crystal clear and to avoid surprising
58
- * future maintainers who add eager adapter imports to detect.ts.
53
+ * Static import of `../adapters/detect.js` is safe detect.ts only imports
54
+ * `node:` builtins, `./types.js` (type-only), and `./client-map.js` (pure
55
+ * data). It does NOT import claude-config back, so no cycle.
56
+ *
57
+ * History: this used `createRequire(import.meta.url).resolve(...)` to lazy-
58
+ * load detect at call time. That pattern requires `require(esm)`, which is
59
+ * flag-gated on Node 22.x before 22.12 (`--experimental-require-module`).
60
+ * CI run 25877550371 on Node 22.5 silently failed every detect.* call —
61
+ * the catch block ate the error and every cross-adapter deny-policy test
62
+ * returned an empty policy list. Static import sidesteps the require(esm)
63
+ * gate entirely, so the same code works on every supported Node version
64
+ * (20.x, 22.5, 22.12+, 24+) without needing the experimental flag.
59
65
  *
60
66
  * The returned array is deduplicated and order-stable: adapter-specific path
61
67
  * first (most specific), claude global second (fallback).
62
68
  */
63
69
  export function resolveAdapterGlobalSettingsPaths(env = process.env) {
64
70
  const paths = [];
65
- // Lazy-load detect module to avoid any chance of an adapter import cycle.
66
- // `detect.ts` exports pure functions — `detectPlatform` (env-driven) and
67
- // `getSessionDirSegments` (sync map). Neither instantiates an adapter.
68
- let detected = null;
69
- let segmentsFor = null;
70
- try {
71
- const lazyRequire = createRequire(import.meta.url);
72
- const detect = lazyRequire("../adapters/detect.js");
73
- detected = detect.detectPlatform();
74
- segmentsFor = detect.getSessionDirSegments;
75
- }
76
- catch {
77
- // If detection fails for any reason, fall back to claude-only behavior.
78
- }
79
- if (detected && segmentsFor && detected.platform !== "claude-code") {
80
- const segments = segmentsFor(detected.platform);
71
+ const detected = detectPlatform();
72
+ if (detected.platform !== "claude-code") {
73
+ const segments = getSessionDirSegments(detected.platform);
81
74
  if (segments && segments.length > 0) {
82
75
  paths.push(resolve(homedir(), ...segments, "settings.json"));
83
76
  }