context-mode 1.0.146 → 1.0.148

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 (38) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +26 -23
  7. package/bin/statusline.mjs +22 -9
  8. package/build/adapters/base.d.ts +9 -4
  9. package/build/adapters/base.js +16 -7
  10. package/build/adapters/codex/index.d.ts +8 -1
  11. package/build/adapters/codex/index.js +43 -6
  12. package/build/adapters/openclaw/index.d.ts +11 -2
  13. package/build/adapters/openclaw/index.js +12 -3
  14. package/build/adapters/pi/mcp-bridge.d.ts +8 -0
  15. package/build/adapters/pi/mcp-bridge.js +118 -15
  16. package/build/adapters/types.d.ts +11 -2
  17. package/build/cli.d.ts +2 -0
  18. package/build/cli.js +87 -20
  19. package/build/search/auto-memory.d.ts +6 -1
  20. package/build/search/auto-memory.js +11 -2
  21. package/build/server.js +346 -106
  22. package/build/session/analytics.d.ts +19 -0
  23. package/build/session/analytics.js +71 -21
  24. package/build/session/db.d.ts +81 -0
  25. package/build/session/db.js +282 -20
  26. package/build/session/extract.js +16 -0
  27. package/build/truncate.d.ts +15 -0
  28. package/build/truncate.js +28 -0
  29. package/cli.bundle.mjs +435 -350
  30. package/hooks/core/routing.mjs +4 -4
  31. package/hooks/routing-block.mjs +18 -23
  32. package/hooks/session-db.bundle.mjs +21 -19
  33. package/hooks/session-extract.bundle.mjs +2 -2
  34. package/hooks/session-helpers.mjs +13 -2
  35. package/hooks/session-snapshot.bundle.mjs +7 -7
  36. package/openclaw.plugin.json +1 -1
  37. package/package.json +4 -2
  38. package/server.bundle.mjs +383 -300
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.146"
9
+ "version": "1.0.148"
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.146",
16
+ "version": "1.0.148",
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.146",
3
+ "version": "1.0.148",
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",
@@ -27,5 +27,5 @@
27
27
  ]
28
28
  }
29
29
  },
30
- "skills": "./.claude/skills/"
30
+ "skills": "./skills/"
31
31
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.146",
3
+ "version": "1.0.148",
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.146",
6
+ "version": "1.0.148",
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.146",
3
+ "version": "1.0.148",
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",
package/README.md CHANGED
@@ -410,13 +410,7 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
410
410
 
411
411
  **Install:**
412
412
 
413
- 1. Install context-mode globally:
414
-
415
- ```bash
416
- npm install -g context-mode
417
- ```
418
-
419
- 2. Add to `opencode.json` in your project root (or `~/.config/opencode/opencode.json` for global):
413
+ 1. Add to `opencode.json` in your project root (or `~/.config/opencode/opencode.json` for global):
420
414
 
421
415
  ```json
422
416
  {
@@ -427,7 +421,7 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
427
421
 
428
422
  The `plugin` entry registers all 11 `ctx_*` tools natively and enables hooks — OpenCode calls context-mode's TypeScript plugin in-process, so there is no redundant stdio MCP child per session.
429
423
 
430
- 3. *(Optional)* Copy the routing rules file. The model needs an `AGENTS.md` file for routing awareness:
424
+ 2. *(Optional)* Copy the routing rules file. The model needs an `AGENTS.md` file for routing awareness:
431
425
 
432
426
  ```bash
433
427
  cp node_modules/context-mode/configs/opencode/AGENTS.md AGENTS.md
@@ -435,7 +429,7 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
435
429
 
436
430
  This tells the model which tools to use and which commands are blocked. Without it, hooks still enforce routing — but the model won't know *why* a command was denied.
437
431
 
438
- 4. Restart OpenCode.
432
+ 3. Restart OpenCode.
439
433
 
440
434
  **Verify:** In the OpenCode session, type `ctx stats`. Context-mode tools should appear and respond.
441
435
 
@@ -456,13 +450,7 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
456
450
 
457
451
  **Install:**
458
452
 
459
- 1. Install context-mode globally:
460
-
461
- ```bash
462
- npm install -g context-mode
463
- ```
464
-
465
- 2. Add to `kilo.json` in your project root (or `~/.config/kilo/kilo.json` for global):
453
+ 1. Add to `kilo.json` in your project root (or `~/.config/kilo/kilo.json` for global):
466
454
 
467
455
  ```json
468
456
  {
@@ -473,13 +461,13 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
473
461
 
474
462
  The `plugin` entry registers all 11 `ctx_*` tools natively and enables hooks — KiloCode calls context-mode's TypeScript plugin in-process, so there is no redundant stdio MCP child per session.
475
463
 
476
- 3. *(Optional)* Copy the routing rules file. KiloCode shares the OpenCode plugin architecture, so the model needs an `AGENTS.md` file for routing awareness:
464
+ 2. *(Optional)* Copy the routing rules file. KiloCode shares the OpenCode plugin architecture, so the model needs an `AGENTS.md` file for routing awareness:
477
465
 
478
466
  ```bash
479
467
  cp node_modules/context-mode/configs/opencode/AGENTS.md AGENTS.md
480
468
  ```
481
469
 
482
- 4. Restart KiloCode.
470
+ 3. Restart KiloCode.
483
471
 
484
472
  **Verify:** In the KiloCode session, type `ctx stats`. Context-mode tools should appear and respond.
485
473
 
@@ -556,6 +544,14 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
556
544
  > remains accepted as a legacy alias in current Codex builds. Bundled plugin hooks
557
545
  > additionally require `plugin_hooks` until Codex enables plugin hooks by default.
558
546
 
547
+ **Custom storage location:** if Codex cannot write the adapter default storage directory, set
548
+ `CONTEXT_MODE_DIR` to an absolute writable root in the environment that launches Codex. Sessions
549
+ and stats use `<root>/sessions`; indexed content uses `<root>/content`.
550
+
551
+ ```bash
552
+ CONTEXT_MODE_DIR="$HOME/.codex-context-mode" codex
553
+ ```
554
+
559
555
  3. Restart Codex CLI and verify MCP with `ctx stats`.
560
556
 
561
557
  `ctx stats` proves the plugin MCP server is installed and reachable; it does
@@ -995,7 +991,7 @@ npm install -g context-mode
995
991
  | `ctx_execute_file` | Process files in sandbox. Raw content never leaves. | 45 KB → 155 B |
996
992
  | `ctx_index` | Chunk markdown into FTS5 with BM25 ranking. | 60 KB → 40 B |
997
993
  | `ctx_search` | Query indexed content with multiple queries in one call. | On-demand retrieval |
998
- | `ctx_fetch_and_index` | Fetch URL, chunk and index. 24h TTL cache repeat calls skip network. `force: true` to bypass. Pass `requests: [{url, source}, ...]` + `concurrency: 1-8` for parallel multi-URL. | 60 KB → 40 B |
994
+ | `ctx_fetch_and_index` | Fetch URL, chunk and index. Cache reuses content within TTL (default 24h, override per-call with `ttl: <ms>`). `ttl: 0` or `force: true` to bypass. Pass `requests: [{url, source}, ...]` + `concurrency: 1-8` for parallel multi-URL. | 60 KB → 40 B |
999
995
  | `ctx_stats` | Show context savings, call counts, and session statistics. | — |
1000
996
  | `ctx_doctor` | Diagnose installation: runtimes, hooks, FTS5, versions. | — |
1001
997
  | `ctx_upgrade` | Upgrade to latest version from GitHub, rebuild, reconfigure hooks. | — |
@@ -1040,11 +1036,12 @@ Search results use intelligent extraction instead of truncation. Instead of retu
1040
1036
 
1041
1037
  ### TTL Cache
1042
1038
 
1043
- Indexed content persists in a per-project SQLite database at `~/.context-mode/content/`. When `ctx_fetch_and_index` is called for a URL that was already indexed within the last 24 hours, the fetch is skipped entirely. The model searches the existing index directly.
1039
+ Indexed content persists in a per-project SQLite database at `~/.context-mode/content/`. When `ctx_fetch_and_index` is called for a URL that was already indexed within its TTL window, the fetch is skipped entirely and the model searches the existing index directly.
1044
1040
 
1045
- - **Fresh (<24h):** Returns a cache hint (0.3KB) instead of re-fetching (48KB+). Model proceeds to `ctx_search`.
1046
- - **Stale (>24h):** Re-fetches silently. No user action needed.
1047
- - **`force: true`:** Bypasses cache and re-fetches regardless of TTL.
1041
+ - **Default TTL:** 24 hours. Override per-call with `ttl: <milliseconds>` (PR #666). Longer for stable specs, shorter for changelogs you want re-checked often.
1042
+ - **Cache hit (within TTL):** Returns a cache hint (~0.3KB) instead of re-fetching (48KB+). Model proceeds to `ctx_search`.
1043
+ - **Cache miss (TTL expired):** Re-fetches silently. No user action needed.
1044
+ - **`ttl: 0`** or **`force: true`:** Bypasses cache and re-fetches regardless of freshness.
1048
1045
  - **14-day cleanup:** Content databases and sources older than 14 days are removed on startup.
1049
1046
 
1050
1047
  This means `--continue` sessions preserve indexed docs across restarts. No re-fetching, no wasted context tokens.
@@ -1389,6 +1386,12 @@ That blocks loopback + RFC1918 + ULA in addition to the always-blocked ranges. U
1389
1386
 
1390
1387
  `tool_input` for any `mcp__*` tool call is also redacted before persistence — the regex matcher in `hooks/posttooluse.mjs` masks `authorization`, `auth_token`, `access_token`, `refresh_token`, `bearer`, `token`, `secret`, `password`, `passwd`, `pwd`, `api_key` / `apikey` / `x_api_key`, `cookie` / `set-cookie`, `signature`, `private_key`, and `client_secret` (case-insensitive, hyphen/underscore-insensitive) to `[REDACTED]` so credentials in MCP arguments don't end up in the session DB.
1391
1388
 
1389
+ ### Storage environment variables
1390
+
1391
+ | Variable | Default | Purpose |
1392
+ |---|---|---|
1393
+ | `CONTEXT_MODE_DIR` | Adapter default, for example `~/.codex/context-mode` or `~/.claude/context-mode` | Since v1.0.147. Absolute writable root for context-mode storage. Sessions and stats use `<root>/sessions`; indexed content uses `<root>/content`. Empty or whitespace-only values are treated as unset and shown by `ctx_doctor`; non-empty values must be absolute. `~` is not expanded. |
1394
+
1392
1395
  ### Routing-guidance environment variables
1393
1396
 
1394
1397
  | Variable | Default | Purpose |
@@ -26,10 +26,14 @@
26
26
  */
27
27
 
28
28
  import { existsSync, readFileSync } from "node:fs";
29
- import { join, dirname, resolve } from "node:path";
29
+ import { dirname, resolve } from "node:path";
30
30
  import { fileURLToPath, pathToFileURL } from "node:url";
31
- import { homedir } from "node:os";
32
31
  import { execFileSync } from "node:child_process";
32
+ import {
33
+ ensureWritableStorageDir,
34
+ resolveDefaultSessionDir,
35
+ resolveSessionStorageDir,
36
+ } from "../hooks/session-db.bundle.mjs";
33
37
 
34
38
  // ── Analytics import — resolved relative to this script ─────────────────
35
39
  // statusline.mjs ships in `bin/`; the compiled analytics module lives in
@@ -68,10 +72,10 @@ function platform() {
68
72
 
69
73
  // Single-shot stderr warning latch — keep noise out of Claude Code's
70
74
  // statusline output even when our parent runs us repeatedly per session.
71
- let __winWarned = false;
75
+ const __warnedKeys = new Set();
72
76
  function warnOnce(key, msg) {
73
- if (key === "win" && __winWarned) return;
74
- if (key === "win") __winWarned = true;
77
+ if (__warnedKeys.has(key)) return;
78
+ __warnedKeys.add(key);
75
79
  try { process.stderr.write(`context-mode statusline: ${msg}\n`); } catch { /* ignore */ }
76
80
  }
77
81
 
@@ -98,10 +102,19 @@ function readStdinJson() {
98
102
  }
99
103
 
100
104
  function resolveSessionDir() {
101
- if (process.env.CONTEXT_MODE_SESSION_DIR) {
102
- return process.env.CONTEXT_MODE_SESSION_DIR;
103
- }
104
- return join(homedir(), ".claude", "context-mode", "sessions");
105
+ return ensureWritableStorageDir(
106
+ resolveSessionStorageDir(() => resolveDefaultSessionDir({
107
+ configDir: ".claude",
108
+ configDirEnv: "CLAUDE_CONFIG_DIR",
109
+ legacySessionDirEnv: "CONTEXT_MODE_SESSION_DIR",
110
+ onLegacySessionDir: () => {
111
+ warnOnce(
112
+ "legacy-session-dir",
113
+ "CONTEXT_MODE_SESSION_DIR is deprecated; set CONTEXT_MODE_DIR to the parent context-mode root.",
114
+ );
115
+ },
116
+ })),
117
+ );
105
118
  }
106
119
 
107
120
  /**
@@ -73,15 +73,20 @@ export declare abstract class BaseAdapter {
73
73
  */
74
74
  getInstructionFiles(): string[];
75
75
  /**
76
- * Default: <configDir>/memory. Always absolute (configDir is absolute by
77
- * contract). Adapters with a different memory dir name (e.g., codex uses
78
- * "memories" plural) override this.
76
+ * Default: <configDir>/memory/<projectHash>. Always absolute (configDir is
77
+ * absolute by contract). Adapters with a different memory dir name (e.g.,
78
+ * codex uses "memories" plural) override this.
79
79
  *
80
80
  * Issue #649: when `CONTEXT_MODE_DATA_DIR` is set, memory follows storage
81
81
  * to `<DATA_DIR>/context-mode/memory/` since persistent memory is
82
82
  * context-mode-owned state, not platform-native config.
83
+ *
84
+ * Issue #663: when `projectDir` is supplied the path is scoped via
85
+ * `hashProjectDirCanonical(projectDir)` so two projects running in
86
+ * parallel never share auto-memory contents. When omitted (legacy
87
+ * callers), the unscoped path is returned for backwards compatibility.
83
88
  */
84
- getMemoryDir(): string;
89
+ getMemoryDir(projectDir?: string): string;
85
90
  backupSettings(): string | null;
86
91
  abstract getSettingsPath(): string;
87
92
  }
@@ -37,6 +37,7 @@
37
37
  import { join, resolve } from "node:path";
38
38
  import { accessSync, copyFileSync, constants, mkdirSync } from "node:fs";
39
39
  import { homedir } from "node:os";
40
+ import { hashProjectDirCanonical } from "../session/db.js";
40
41
  /**
41
42
  * Universal storage-root override. Returns the resolved absolute path when
42
43
  * `CONTEXT_MODE_DATA_DIR` is set to a non-blank value, otherwise `null` so
@@ -97,19 +98,27 @@ export class BaseAdapter {
97
98
  return ["CLAUDE.md"];
98
99
  }
99
100
  /**
100
- * Default: <configDir>/memory. Always absolute (configDir is absolute by
101
- * contract). Adapters with a different memory dir name (e.g., codex uses
102
- * "memories" plural) override this.
101
+ * Default: <configDir>/memory/<projectHash>. Always absolute (configDir is
102
+ * absolute by contract). Adapters with a different memory dir name (e.g.,
103
+ * codex uses "memories" plural) override this.
103
104
  *
104
105
  * Issue #649: when `CONTEXT_MODE_DATA_DIR` is set, memory follows storage
105
106
  * to `<DATA_DIR>/context-mode/memory/` since persistent memory is
106
107
  * context-mode-owned state, not platform-native config.
108
+ *
109
+ * Issue #663: when `projectDir` is supplied the path is scoped via
110
+ * `hashProjectDirCanonical(projectDir)` so two projects running in
111
+ * parallel never share auto-memory contents. When omitted (legacy
112
+ * callers), the unscoped path is returned for backwards compatibility.
107
113
  */
108
- getMemoryDir() {
114
+ getMemoryDir(projectDir) {
109
115
  const override = resolveContextModeDataRoot();
110
- if (override)
111
- return join(override, "context-mode", "memory");
112
- return join(this.getConfigDir(), "memory");
116
+ const base = override
117
+ ? join(override, "context-mode", "memory")
118
+ : join(this.getConfigDir(), "memory");
119
+ if (!projectDir)
120
+ return base;
121
+ return join(base, hashProjectDirCanonical(projectDir));
113
122
  }
114
123
  backupSettings() {
115
124
  const settingsPath = this.getSettingsPath();
@@ -15,6 +15,12 @@
15
15
  */
16
16
  import { BaseAdapter } from "../base.js";
17
17
  import { type HookAdapter, type HookParadigm, type PlatformCapabilities, type DiagnosticResult, type PreToolUseEvent, type PostToolUseEvent, type PreCompactEvent, type SessionStartEvent, type PreToolUseResponse, type PostToolUseResponse, type PreCompactResponse, type SessionStartResponse, type HookRegistration } from "../types.js";
18
+ type CodexVersionRunner = (file: string, args: string[], options: {
19
+ encoding: BufferEncoding;
20
+ stdio: ["ignore", "pipe", "ignore"];
21
+ timeout: number;
22
+ }) => string | Buffer;
23
+ export declare function probeCodexCliVersion(runCommand?: CodexVersionRunner): string | null;
18
24
  export declare class CodexAdapter extends BaseAdapter implements HookAdapter {
19
25
  constructor();
20
26
  readonly name = "Codex CLI";
@@ -32,7 +38,7 @@ export declare class CodexAdapter extends BaseAdapter implements HookAdapter {
32
38
  getSettingsPath(): string;
33
39
  getSessionDir(): string;
34
40
  getInstructionFiles(): string[];
35
- getMemoryDir(): string;
41
+ getMemoryDir(projectDir?: string): string;
36
42
  generateHookConfig(_pluginRoot: string): HookRegistration;
37
43
  readSettings(): Record<string, unknown> | null;
38
44
  writeSettings(_settings: Record<string, unknown>): void;
@@ -67,3 +73,4 @@ export declare class CodexAdapter extends BaseAdapter implements HookAdapter {
67
73
  */
68
74
  private extractSessionId;
69
75
  }
76
+ export {};
@@ -13,10 +13,12 @@
13
13
  * while input rewriting remains blocked on upstream updatedInput support.
14
14
  * Track: https://github.com/openai/codex/issues/18491
15
15
  */
16
+ import { execFileSync } from "node:child_process";
16
17
  import { readFileSync, writeFileSync, accessSync, copyFileSync, constants, mkdirSync, } from "node:fs";
17
18
  import { resolve, dirname, join } from "node:path";
18
19
  import { fileURLToPath } from "node:url";
19
20
  import { BaseAdapter, resolveContextModeDataRoot } from "../base.js";
21
+ import { hashProjectDirCanonical } from "../../session/db.js";
20
22
  import { resolveCodexConfigDir } from "./paths.js";
21
23
  // PreToolUse matcher: canonical Codex tool names + context-mode bare MCP tool
22
24
  // names + external MCP catch-all literal (#529, #547 hotfix).
@@ -53,6 +55,26 @@ const LEGACY_HOOK_PATH_SUFFIXES = {
53
55
  UserPromptSubmit: ["hooks/userpromptsubmit.mjs", "hooks/codex/userpromptsubmit.mjs"],
54
56
  Stop: ["hooks/stop.mjs", "hooks/codex/stop.mjs"],
55
57
  };
58
+ export function probeCodexCliVersion(runCommand = execFileSync) {
59
+ try {
60
+ const output = process.platform === "win32"
61
+ ? runCommand("cmd.exe", ["/d", "/s", "/c", "codex --version"], {
62
+ encoding: "utf-8",
63
+ stdio: ["ignore", "pipe", "ignore"],
64
+ timeout: 5000,
65
+ })
66
+ : runCommand("codex", ["--version"], {
67
+ encoding: "utf-8",
68
+ stdio: ["ignore", "pipe", "ignore"],
69
+ timeout: 1500,
70
+ });
71
+ const version = String(output).trim();
72
+ return version.length > 0 ? version : "available (version output empty)";
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
56
78
  function getTomlSection(raw, sectionName) {
57
79
  const lines = raw.split(/\r?\n/);
58
80
  let inSection = false;
@@ -257,16 +279,21 @@ export class CodexAdapter extends BaseAdapter {
257
279
  // Codex CLI honors AGENTS.md plus an optional override file.
258
280
  return ["AGENTS.md", "AGENTS.override.md"];
259
281
  }
260
- getMemoryDir() {
282
+ getMemoryDir(projectDir) {
261
283
  // Codex uses "memories" (plural), not the default "memory".
262
284
  // Issue #649: honor CONTEXT_MODE_DATA_DIR for context-mode-owned
263
285
  // persistent memory while preserving the platform-native plural folder
264
286
  // name so legacy Codex tooling continues to find it when DATA_DIR is
265
287
  // unset. Under the override, layout is `<DATA_DIR>/context-mode/memories`.
288
+ // Issue #663: scope by projectDir hash so parallel projects can't
289
+ // read each other's memory.
266
290
  const override = resolveContextModeDataRoot();
267
- if (override)
268
- return join(override, "context-mode", "memories");
269
- return join(this.getConfigDir(), "memories");
291
+ const base = override
292
+ ? join(override, "context-mode", "memories")
293
+ : join(this.getConfigDir(), "memories");
294
+ if (!projectDir)
295
+ return base;
296
+ return join(base, hashProjectDirCanonical(projectDir));
270
297
  }
271
298
  generateHookConfig(_pluginRoot) {
272
299
  return {
@@ -359,6 +386,15 @@ export class CodexAdapter extends BaseAdapter {
359
386
  // ── Diagnostics (doctor) ─────────────────────────────────
360
387
  validateHooks(_pluginRoot) {
361
388
  const results = [];
389
+ const codexCliVersion = probeCodexCliVersion();
390
+ results.push({
391
+ check: "Codex CLI binary",
392
+ status: codexCliVersion ? "pass" : "warn",
393
+ message: codexCliVersion
394
+ ? `codex --version resolved to ${codexCliVersion}`
395
+ : "Could not run codex --version; hooks need the Codex CLI available on PATH",
396
+ ...(codexCliVersion ? {} : { fix: "Install Codex CLI or make codex available on PATH" }),
397
+ });
362
398
  try {
363
399
  const raw = readFileSync(this.getSettingsPath(), "utf-8");
364
400
  const enabled = hasCodexHooksFeature(raw);
@@ -492,8 +528,9 @@ export class CodexAdapter extends BaseAdapter {
492
528
  }
493
529
  }
494
530
  getInstalledVersion() {
495
- // Codex CLI has no marketplace or plugin system
496
- return "not installed";
531
+ // Codex uses standalone MCP registration; there is no platform-owned
532
+ // plugin version to compare against the context-mode npm package.
533
+ return "standalone";
497
534
  }
498
535
  // ── Upgrade ────────────────────────────────────────────
499
536
  configureAllHooks(pluginRoot) {
@@ -38,8 +38,17 @@ export declare class OpenClawAdapter extends BaseAdapter implements HookAdapter
38
38
  */
39
39
  getConfigDir(projectDir?: string): string;
40
40
  getInstructionFiles(): string[];
41
- /** Absolute <projectRoot>/memory directory. */
42
- getMemoryDir(): string;
41
+ /**
42
+ * Absolute <projectRoot>/memory directory.
43
+ *
44
+ * OpenClaw's `getConfigDir(projectDir)` already returns the project root,
45
+ * so the memory dir is naturally project-scoped per the OpenClaw
46
+ * convention. The `projectDir` parameter is honored for explicit
47
+ * resolution; without it, falls back to the implicit `process.cwd()`
48
+ * inside `getConfigDir`. Either way, two projects never share a path
49
+ * — no hash suffix needed (issue #663).
50
+ */
51
+ getMemoryDir(projectDir?: string): string;
43
52
  generateHookConfig(_pluginRoot: string): HookRegistration;
44
53
  readSettings(): Record<string, unknown> | null;
45
54
  writeSettings(settings: Record<string, unknown>): void;
@@ -154,9 +154,18 @@ export class OpenClawAdapter extends BaseAdapter {
154
154
  getInstructionFiles() {
155
155
  return ["AGENTS.md"];
156
156
  }
157
- /** Absolute <projectRoot>/memory directory. */
158
- getMemoryDir() {
159
- return join(this.getConfigDir(), "memory");
157
+ /**
158
+ * Absolute <projectRoot>/memory directory.
159
+ *
160
+ * OpenClaw's `getConfigDir(projectDir)` already returns the project root,
161
+ * so the memory dir is naturally project-scoped per the OpenClaw
162
+ * convention. The `projectDir` parameter is honored for explicit
163
+ * resolution; without it, falls back to the implicit `process.cwd()`
164
+ * inside `getConfigDir`. Either way, two projects never share a path
165
+ * — no hash suffix needed (issue #663).
166
+ */
167
+ getMemoryDir(projectDir) {
168
+ return join(this.getConfigDir(projectDir), "memory");
160
169
  }
161
170
  generateHookConfig(_pluginRoot) {
162
171
  // OpenClaw uses TS plugin paradigm — hooks are registered via
@@ -47,6 +47,14 @@ export interface MCPCallResult {
47
47
  }>;
48
48
  isError?: boolean;
49
49
  }
50
+ export declare class PiTextComponent {
51
+ private text;
52
+ constructor(text?: string);
53
+ setText(text: string): void;
54
+ invalidate(): void;
55
+ render(width: number): string[];
56
+ }
57
+ export declare function truncateAnsiLine(line: string, maxWidth: number): string;
50
58
  interface PiRenderTheme {
51
59
  bold(text: string): string;
52
60
  fg(color: string, text: string): string;
@@ -121,7 +121,7 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
121
121
  // self-healing the transient warm-up case.
122
122
  const MAX_INIT_RETRIES = 2;
123
123
  const INIT_RETRY_DELAY_MS = 1_000;
124
- class PiTextComponent {
124
+ export class PiTextComponent {
125
125
  text;
126
126
  constructor(text = "") {
127
127
  this.text = text;
@@ -141,29 +141,132 @@ class PiTextComponent {
141
141
  .map((line) => truncateAnsiLine(line, Math.max(1, width)));
142
142
  }
143
143
  }
144
- const ANSI_PATTERN = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
145
- function truncateAnsiLine(line, maxWidth) {
144
+ const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
145
+ function extractTerminalEscape(str, pos) {
146
+ if (pos >= str.length || str[pos] !== "\x1b")
147
+ return null;
148
+ const next = str[pos + 1];
149
+ // CSI sequence: ESC [ ... final-byte. Covers SGR plus cursor/control codes.
150
+ if (next === "[") {
151
+ let j = pos + 2;
152
+ while (j < str.length) {
153
+ const code = str.charCodeAt(j);
154
+ if (code >= 0x40 && code <= 0x7e) {
155
+ return { code: str.slice(pos, j + 1), length: j + 1 - pos };
156
+ }
157
+ j++;
158
+ }
159
+ return null;
160
+ }
161
+ // OSC/APC sequence: ESC ]/_ ... BEL or ST (ESC \). Stop at the FIRST
162
+ // terminator so OSC 8 hyperlinks don't swallow visible link text.
163
+ if (next === "]" || next === "_") {
164
+ let j = pos + 2;
165
+ while (j < str.length) {
166
+ if (str[j] === "\x07")
167
+ return { code: str.slice(pos, j + 1), length: j + 1 - pos };
168
+ if (str[j] === "\x1b" && str[j + 1] === "\\") {
169
+ return { code: str.slice(pos, j + 2), length: j + 2 - pos };
170
+ }
171
+ j++;
172
+ }
173
+ return null;
174
+ }
175
+ return null;
176
+ }
177
+ function couldBeEmoji(segment) {
178
+ const cp = segment.codePointAt(0) ?? 0;
179
+ return ((cp >= 0x1f000 && cp <= 0x1fbff) ||
180
+ (cp >= 0x2300 && cp <= 0x23ff) ||
181
+ (cp >= 0x2600 && cp <= 0x27bf) ||
182
+ (cp >= 0x2b50 && cp <= 0x2b55) ||
183
+ segment.includes("\uFE0F") ||
184
+ segment.includes("\u200D"));
185
+ }
186
+ function isZeroWidthCodePoint(cp) {
187
+ return (cp < 0x20 ||
188
+ (cp >= 0x7f && cp <= 0x9f) ||
189
+ (cp >= 0x300 && cp <= 0x36f) || // Combining Diacritical Marks
190
+ (cp >= 0x1ab0 && cp <= 0x1aff) || // Combining Diacritical Marks Extended
191
+ (cp >= 0x1dc0 && cp <= 0x1dff) || // Combining Diacritical Marks Supplement
192
+ (cp >= 0x20d0 && cp <= 0x20ff) || // Combining Diacritical Marks for Symbols
193
+ (cp >= 0xfe00 && cp <= 0xfe0f) || // Variation Selectors
194
+ (cp >= 0xfe20 && cp <= 0xfe2f) || // Combining Half Marks
195
+ cp === 0x200b ||
196
+ cp === 0x200c ||
197
+ cp === 0x200d ||
198
+ cp === 0xfeff);
199
+ }
200
+ function isZeroWidthGrapheme(segment) {
201
+ if (segment.length === 0)
202
+ return true;
203
+ for (const char of segment) {
204
+ if (!isZeroWidthCodePoint(char.codePointAt(0) ?? 0))
205
+ return false;
206
+ }
207
+ return true;
208
+ }
209
+ /**
210
+ * Returns the terminal display width of a code point.
211
+ * CJK ideographs, Hangul, fullwidth forms, etc. → 2; everything else → 1.
212
+ * Mirrors the Unicode east-asian-width "W"/"F" categories.
213
+ */
214
+ function charWidth(cp) {
215
+ return cp >= 0x1100 && (cp <= 0x115f || // Hangul Jamo
216
+ (cp >= 0xa960 && cp <= 0xa97c) || // Hangul Jamo Extended-A
217
+ cp === 0x2329 || cp === 0x232a ||
218
+ (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) || // CJK
219
+ (cp >= 0xac00 && cp <= 0xd7a3) || // Hangul syllables
220
+ (cp >= 0xd7b0 && cp <= 0xd7fb) || // Hangul Jamo Extended-B
221
+ (cp >= 0xf900 && cp <= 0xfaff) || // CJK compat
222
+ (cp >= 0xfe10 && cp <= 0xfe19) || // Vertical forms
223
+ (cp >= 0xfe30 && cp <= 0xfe6f) || // CJK compat forms
224
+ (cp >= 0xff01 && cp <= 0xff60) || // Fullwidth forms
225
+ (cp >= 0xffe0 && cp <= 0xffe6) || // Fullwidth signs
226
+ (cp >= 0x20000 && cp <= 0x2fffd) || // CJK extensions
227
+ (cp >= 0x30000 && cp <= 0x3fffd) // CJK extensions B+
228
+ ) ? 2 : 1;
229
+ }
230
+ function graphemeWidth(segment) {
231
+ const cp = segment.codePointAt(0);
232
+ if (cp === undefined)
233
+ return 0;
234
+ if (isZeroWidthGrapheme(segment))
235
+ return 0;
236
+ if (couldBeEmoji(segment))
237
+ return 2;
238
+ // Regional indicator symbols render as wide emoji flags in Pi's TUI.
239
+ if (cp >= 0x1f1e6 && cp <= 0x1f1ff)
240
+ return 2;
241
+ return charWidth(cp);
242
+ }
243
+ export function truncateAnsiLine(line, maxWidth) {
146
244
  if (maxWidth <= 0)
147
245
  return "";
148
246
  let output = "";
149
247
  let visible = 0;
150
248
  let index = 0;
151
- ANSI_PATTERN.lastIndex = 0;
152
- for (;;) {
153
- const match = ANSI_PATTERN.exec(line);
154
- const end = match?.index ?? line.length;
249
+ while (index < line.length) {
250
+ const escape = extractTerminalEscape(line, index);
251
+ if (escape) {
252
+ output += escape.code;
253
+ index += escape.length;
254
+ continue;
255
+ }
256
+ let end = index + 1;
257
+ while (end < line.length && !extractTerminalEscape(line, end))
258
+ end++;
155
259
  const chunk = line.slice(index, end);
156
- for (const char of chunk) {
157
- if (visible >= maxWidth)
260
+ for (const { segment } of GRAPHEME_SEGMENTER.segment(chunk)) {
261
+ const w = graphemeWidth(segment);
262
+ if (visible + w > maxWidth)
158
263
  return output;
159
- output += char;
160
- visible++;
264
+ output += segment;
265
+ visible += w;
161
266
  }
162
- if (!match)
163
- return output;
164
- output += match[0];
165
- index = ANSI_PATTERN.lastIndex;
267
+ index = end;
166
268
  }
269
+ return output;
167
270
  }
168
271
  function createContextModeCallRenderer(toolName) {
169
272
  return (_args, theme, context) => {