context-mode 1.0.134 → 1.0.136

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.
Files changed (50) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/hooks.json +65 -0
  4. package/.codex-plugin/mcp.json +9 -0
  5. package/.codex-plugin/plugin.json +31 -0
  6. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  7. package/.openclaw-plugin/package.json +1 -1
  8. package/README.md +60 -12
  9. package/build/adapters/detect.d.ts +3 -1
  10. package/build/adapters/detect.js +7 -2
  11. package/build/adapters/pi/mcp-bridge.d.ts +44 -0
  12. package/build/adapters/pi/mcp-bridge.js +149 -3
  13. package/build/cli.js +17 -0
  14. package/build/lifecycle.d.ts +13 -13
  15. package/build/lifecycle.js +14 -14
  16. package/build/runtime.js +8 -5
  17. package/build/session/analytics.d.ts +0 -13
  18. package/build/session/analytics.js +50 -1
  19. package/build/session/extract.js +39 -1
  20. package/build/util/claude-config.d.ts +12 -6
  21. package/build/util/claude-config.js +16 -23
  22. package/cli.bundle.mjs +135 -133
  23. package/configs/kilo/kilo.json +9 -2
  24. package/configs/opencode/opencode.json +9 -2
  25. package/hooks/codex/platform.mjs +1 -0
  26. package/hooks/codex/posttooluse.mjs +1 -0
  27. package/hooks/codex/precompact.mjs +1 -0
  28. package/hooks/codex/pretooluse.mjs +1 -0
  29. package/hooks/codex/sessionstart.mjs +24 -1
  30. package/hooks/codex/stop.mjs +1 -0
  31. package/hooks/codex/userpromptsubmit.mjs +1 -0
  32. package/hooks/core/platform-detect.mjs +1 -1
  33. package/hooks/core/routing.mjs +112 -10
  34. package/hooks/ensure-deps.mjs +14 -3
  35. package/hooks/normalize-hooks.mjs +5 -2
  36. package/hooks/security.bundle.mjs +1 -1
  37. package/hooks/session-extract.bundle.mjs +2 -2
  38. package/openclaw.plugin.json +1 -1
  39. package/package.json +2 -1
  40. package/scripts/heal-installed-plugins.mjs +67 -0
  41. package/server.bundle.mjs +99 -99
  42. package/start.mjs +73 -11
  43. package/build/openclaw-plugin.d.ts +0 -130
  44. package/build/openclaw-plugin.js +0 -626
  45. package/build/opencode-plugin.d.ts +0 -122
  46. package/build/opencode-plugin.js +0 -372
  47. package/build/pi-extension.d.ts +0 -14
  48. package/build/pi-extension.js +0 -451
  49. package/build/util/db-lock.d.ts +0 -65
  50. package/build/util/db-lock.js +0 -166
@@ -23,7 +23,7 @@ export interface LifecycleGuardOptions {
23
23
  /**
24
24
  * Idle shutdown threshold in ms (#565). When the server has handled no
25
25
  * MCP activity for this long, `onShutdown` fires. `0` disables.
26
- * Default: env `CONTEXT_MODE_IDLE_TIMEOUT_MS`, else 15 minutes.
26
+ * Default: env `CONTEXT_MODE_IDLE_TIMEOUT_MS`, else 0 (disabled).
27
27
  * Skipped on TTY stdin (interactive dev / OpenCode ts-plugin standalone).
28
28
  *
29
29
  * Pair with the returned `recordActivity()` callback — call it on every
@@ -50,20 +50,20 @@ export interface LifecycleGuardHandle {
50
50
  /**
51
51
  * Resolve the idle-shutdown threshold (#565).
52
52
  *
53
- * OpenCode + KiloCode open a fresh MCP client per session AND per subagent
54
- * task, but never tear them down for the host's lifetime. A host alive for
55
- * a working day accumulates one stdio child per session observed live at
56
- * 26 children / 1.6 GB RSS under a single `opencode serve` parent.
53
+ * Idle shutdown is OFF by default (#592) because most hosts (Claude
54
+ * Code, Codex, editor MCP clients) keep registered tool handles after a
55
+ * clean MCP child exit and do NOT transparently respawn on the next call.
56
+ * The global 15 min default introduced in #568 solved OpenCode's child
57
+ * accumulation, but stranded ctx_* tools in Claude Code/Codex-style
58
+ * hosts once the MCP server exited cleanly while the editor stayed alive.
57
59
  *
58
- * None of the existing exit paths (ppid poll, grandparent reparent, stdin
59
- * EOF, SIGTERM) fire while the host stays alive. Idle shutdown is the
60
- * structural fix: a server with no work to do should release its memory.
60
+ * Hosts that are known to benefit from idle shutdown MUST opt in via
61
+ * CONTEXT_MODE_IDLE_TIMEOUT_MS in their MCP config. Today that is
62
+ * OpenCode/KiloCode (their configs set 900000 = 15 min). Users and test
63
+ * harnesses can also opt in explicitly with any positive integer.
61
64
  *
62
- * Default 15 min strikes a balance long enough that a paused
63
- * conversation does not pay a cold-start on every resume, short enough
64
- * that 8 hours of unused sessions do not pin GB of RAM.
65
- *
66
- * Set env to `0` to disable entirely.
65
+ * Missing or malformed env = 0 (disabled, safe default). Set env to
66
+ * `0` to disable explicitly.
67
67
  *
68
68
  * Exported for unit-testing.
69
69
  */
@@ -17,30 +17,30 @@ import { execFileSync } from "node:child_process";
17
17
  /**
18
18
  * Resolve the idle-shutdown threshold (#565).
19
19
  *
20
- * OpenCode + KiloCode open a fresh MCP client per session AND per subagent
21
- * task, but never tear them down for the host's lifetime. A host alive for
22
- * a working day accumulates one stdio child per session observed live at
23
- * 26 children / 1.6 GB RSS under a single `opencode serve` parent.
20
+ * Idle shutdown is OFF by default (#592) because most hosts (Claude
21
+ * Code, Codex, editor MCP clients) keep registered tool handles after a
22
+ * clean MCP child exit and do NOT transparently respawn on the next call.
23
+ * The global 15 min default introduced in #568 solved OpenCode's child
24
+ * accumulation, but stranded ctx_* tools in Claude Code/Codex-style
25
+ * hosts once the MCP server exited cleanly while the editor stayed alive.
24
26
  *
25
- * None of the existing exit paths (ppid poll, grandparent reparent, stdin
26
- * EOF, SIGTERM) fire while the host stays alive. Idle shutdown is the
27
- * structural fix: a server with no work to do should release its memory.
27
+ * Hosts that are known to benefit from idle shutdown MUST opt in via
28
+ * CONTEXT_MODE_IDLE_TIMEOUT_MS in their MCP config. Today that is
29
+ * OpenCode/KiloCode (their configs set 900000 = 15 min). Users and test
30
+ * harnesses can also opt in explicitly with any positive integer.
28
31
  *
29
- * Default 15 min strikes a balance long enough that a paused
30
- * conversation does not pay a cold-start on every resume, short enough
31
- * that 8 hours of unused sessions do not pin GB of RAM.
32
- *
33
- * Set env to `0` to disable entirely.
32
+ * Missing or malformed env = 0 (disabled, safe default). Set env to
33
+ * `0` to disable explicitly.
34
34
  *
35
35
  * Exported for unit-testing.
36
36
  */
37
37
  export function idleTimeoutForEnv(env = process.env) {
38
38
  const raw = env.CONTEXT_MODE_IDLE_TIMEOUT_MS;
39
39
  if (raw === undefined)
40
- return 15 * 60 * 1000;
40
+ return 0;
41
41
  const n = Number.parseInt(raw, 10);
42
42
  if (!Number.isFinite(n) || n < 0)
43
- return 15 * 60 * 1000;
43
+ return 0;
44
44
  return n;
45
45
  }
46
46
  /** Read grandparent PID via `ps -o ppid= -p $PPID`. Returns NaN on failure or Windows. */
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
@@ -613,7 +613,45 @@ function extractDecision(input) {
613
613
  const questionText = Array.isArray(questions) && questions.length > 0
614
614
  ? String(questions[0]["question"] ?? "")
615
615
  : "";
616
- const answer = safeString(String(input.tool_response ?? ""));
616
+ // tool_response is a JSON string that echoes the full request payload
617
+ // alongside the answers map: {"questions":[...],"answers":{"<q>":"<label>"}}.
618
+ // Stringifying the raw blob leaks the echoed questions/options into the
619
+ // event row and surfaces as "Unhandled case: [object Object]" downstream.
620
+ const rawResponse = String(input.tool_response ?? "");
621
+ let answerText = "";
622
+ try {
623
+ const parsed = JSON.parse(rawResponse);
624
+ const answers = parsed?.answers;
625
+ if (answers && typeof answers === "object") {
626
+ // multiSelect: true answers arrive as string[]; single-select arrive as
627
+ // string. Normalize both into a `" | "`-joined string so neither shape
628
+ // silently produces an empty answer.
629
+ const toAnswerText = (value) => {
630
+ if (typeof value === "string")
631
+ return value;
632
+ if (Array.isArray(value)) {
633
+ return value.filter((v) => typeof v === "string").join(" | ");
634
+ }
635
+ return "";
636
+ };
637
+ const matched = questionText ? toAnswerText(answers[questionText]) : "";
638
+ if (matched) {
639
+ answerText = matched;
640
+ }
641
+ else {
642
+ const values = Object.values(answers)
643
+ .map(toAnswerText)
644
+ .filter((v) => v.length > 0);
645
+ answerText = values.join(" | ");
646
+ }
647
+ }
648
+ }
649
+ catch {
650
+ // Non-JSON tool_response — fail safe with empty answer rather than
651
+ // leaking the raw text (which would re-introduce the original bug
652
+ // for any future caller that sends a non-JSON payload).
653
+ }
654
+ const answer = safeString(answerText);
617
655
  const summary = questionText
618
656
  ? `Q: ${safeString(questionText)} → A: ${answer}`
619
657
  : `answer: ${answer}`;
@@ -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
  }