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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +26 -23
- package/bin/statusline.mjs +22 -9
- package/build/adapters/base.d.ts +9 -4
- package/build/adapters/base.js +16 -7
- package/build/adapters/codex/index.d.ts +8 -1
- package/build/adapters/codex/index.js +43 -6
- package/build/adapters/openclaw/index.d.ts +11 -2
- package/build/adapters/openclaw/index.js +12 -3
- package/build/adapters/pi/mcp-bridge.d.ts +8 -0
- package/build/adapters/pi/mcp-bridge.js +118 -15
- package/build/adapters/types.d.ts +11 -2
- package/build/cli.d.ts +2 -0
- package/build/cli.js +87 -20
- package/build/search/auto-memory.d.ts +6 -1
- package/build/search/auto-memory.js +11 -2
- package/build/server.js +346 -106
- package/build/session/analytics.d.ts +19 -0
- package/build/session/analytics.js +71 -21
- package/build/session/db.d.ts +81 -0
- package/build/session/db.js +282 -20
- package/build/session/extract.js +16 -0
- package/build/truncate.d.ts +15 -0
- package/build/truncate.js +28 -0
- package/cli.bundle.mjs +435 -350
- package/hooks/core/routing.mjs +4 -4
- package/hooks/routing-block.mjs +18 -23
- package/hooks/session-db.bundle.mjs +21 -19
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +13 -2
- package/hooks/session-snapshot.bundle.mjs +7 -7
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -2
- 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.
|
|
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.
|
|
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.
|
|
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": "
|
|
30
|
+
"skills": "./skills/"
|
|
31
31
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
- **
|
|
1046
|
-
- **
|
|
1047
|
-
-
|
|
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 |
|
package/bin/statusline.mjs
CHANGED
|
@@ -26,10 +26,14 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { existsSync, readFileSync } from "node:fs";
|
|
29
|
-
import {
|
|
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
|
-
|
|
75
|
+
const __warnedKeys = new Set();
|
|
72
76
|
function warnOnce(key, msg) {
|
|
73
|
-
if (key
|
|
74
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
/**
|
package/build/adapters/base.d.ts
CHANGED
|
@@ -73,15 +73,20 @@ export declare abstract class BaseAdapter {
|
|
|
73
73
|
*/
|
|
74
74
|
getInstructionFiles(): string[];
|
|
75
75
|
/**
|
|
76
|
-
* Default: <configDir>/memory
|
|
77
|
-
* contract). Adapters with a different memory dir name (e.g.,
|
|
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
|
}
|
package/build/adapters/base.js
CHANGED
|
@@ -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
|
|
101
|
-
* contract). Adapters with a different memory dir name (e.g.,
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
496
|
-
|
|
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
|
-
/**
|
|
42
|
-
|
|
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
|
-
/**
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
145
|
-
function
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
157
|
-
|
|
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 +=
|
|
160
|
-
visible
|
|
264
|
+
output += segment;
|
|
265
|
+
visible += w;
|
|
161
266
|
}
|
|
162
|
-
|
|
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) => {
|