context-mode 1.0.145 → 1.0.147
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 +1 -1
- package/build/adapters/codex/index.js +10 -4
- package/build/adapters/openclaw/index.d.ts +11 -2
- package/build/adapters/openclaw/index.js +12 -3
- package/build/adapters/openclaw/plugin.js +30 -7
- 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 +7 -1
- package/build/cli.d.ts +2 -0
- package/build/cli.js +82 -19
- package/build/search/auto-memory.d.ts +6 -1
- package/build/search/auto-memory.js +11 -2
- package/build/server.js +305 -105
- package/build/session/db.d.ts +37 -0
- package/build/session/db.js +197 -2
- package/build/session/extract.js +16 -0
- package/build/truncate.d.ts +15 -0
- package/build/truncate.js +28 -0
- package/cli.bundle.mjs +424 -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 +372 -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.147"
|
|
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.147",
|
|
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.147",
|
|
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.147",
|
|
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.147",
|
|
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.147",
|
|
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();
|
|
@@ -32,7 +32,7 @@ export declare class CodexAdapter extends BaseAdapter implements HookAdapter {
|
|
|
32
32
|
getSettingsPath(): string;
|
|
33
33
|
getSessionDir(): string;
|
|
34
34
|
getInstructionFiles(): string[];
|
|
35
|
-
getMemoryDir(): string;
|
|
35
|
+
getMemoryDir(projectDir?: string): string;
|
|
36
36
|
generateHookConfig(_pluginRoot: string): HookRegistration;
|
|
37
37
|
readSettings(): Record<string, unknown> | null;
|
|
38
38
|
writeSettings(_settings: Record<string, unknown>): void;
|
|
@@ -17,6 +17,7 @@ import { readFileSync, writeFileSync, accessSync, copyFileSync, constants, mkdir
|
|
|
17
17
|
import { resolve, dirname, join } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { BaseAdapter, resolveContextModeDataRoot } from "../base.js";
|
|
20
|
+
import { hashProjectDirCanonical } from "../../session/db.js";
|
|
20
21
|
import { resolveCodexConfigDir } from "./paths.js";
|
|
21
22
|
// PreToolUse matcher: canonical Codex tool names + context-mode bare MCP tool
|
|
22
23
|
// names + external MCP catch-all literal (#529, #547 hotfix).
|
|
@@ -257,16 +258,21 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
257
258
|
// Codex CLI honors AGENTS.md plus an optional override file.
|
|
258
259
|
return ["AGENTS.md", "AGENTS.override.md"];
|
|
259
260
|
}
|
|
260
|
-
getMemoryDir() {
|
|
261
|
+
getMemoryDir(projectDir) {
|
|
261
262
|
// Codex uses "memories" (plural), not the default "memory".
|
|
262
263
|
// Issue #649: honor CONTEXT_MODE_DATA_DIR for context-mode-owned
|
|
263
264
|
// persistent memory while preserving the platform-native plural folder
|
|
264
265
|
// name so legacy Codex tooling continues to find it when DATA_DIR is
|
|
265
266
|
// unset. Under the override, layout is `<DATA_DIR>/context-mode/memories`.
|
|
267
|
+
// Issue #663: scope by projectDir hash so parallel projects can't
|
|
268
|
+
// read each other's memory.
|
|
266
269
|
const override = resolveContextModeDataRoot();
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
+
const base = override
|
|
271
|
+
? join(override, "context-mode", "memories")
|
|
272
|
+
: join(this.getConfigDir(), "memories");
|
|
273
|
+
if (!projectDir)
|
|
274
|
+
return base;
|
|
275
|
+
return join(base, hashProjectDirCanonical(projectDir));
|
|
270
276
|
}
|
|
271
277
|
generateHookConfig(_pluginRoot) {
|
|
272
278
|
return {
|
|
@@ -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
|
|
@@ -34,6 +34,7 @@ import { homedir } from "node:os";
|
|
|
34
34
|
import { dirname, join, resolve } from "node:path";
|
|
35
35
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
36
36
|
import { resolveContextModeDataRoot } from "../base.js";
|
|
37
|
+
import { resolveSessionDbPath } from "../../session/db.js";
|
|
37
38
|
import { OpenClawSessionDB } from "./session-db.js";
|
|
38
39
|
import { extractEvents, extractUserEvents } from "../../session/extract.js";
|
|
39
40
|
import { buildResumeSnapshot } from "../../session/snapshot.js";
|
|
@@ -83,22 +84,44 @@ function getSessionDir() {
|
|
|
83
84
|
mkdirSync(dir, { recursive: true });
|
|
84
85
|
return dir;
|
|
85
86
|
}
|
|
87
|
+
// Issue #645 follow-up — route through the canonical per-project
|
|
88
|
+
// resolver the MCP server uses (src/server.ts ctx_stats / ctx_search
|
|
89
|
+
// timeline). The previous raw `sha256(projectDir).slice(0, 16)` shape
|
|
90
|
+
// produced a different file from the `<canonical-hash>.db` the server
|
|
91
|
+
// reads on darwin/win32 (case-fold) and inside linked worktrees
|
|
92
|
+
// (suffix). The result was silent degradation of `ctx_stats` (zero
|
|
93
|
+
// history) and `ctx_search(sort: "timeline")` (sort dropped) for any
|
|
94
|
+
// OpenClaw user with an uppercase character in their projectDir.
|
|
95
|
+
// Mirrors the matching Pi (src/adapters/pi/extension.ts:223) and OMP
|
|
96
|
+
// (src/adapters/omp/plugin.ts:90) fixes, and the opencode plugin
|
|
97
|
+
// pattern (src/adapters/opencode/plugin.ts:307).
|
|
86
98
|
function getDBPath(projectDir) {
|
|
87
|
-
|
|
88
|
-
.update(projectDir)
|
|
89
|
-
.digest("hex")
|
|
90
|
-
.slice(0, 16);
|
|
91
|
-
return join(getSessionDir(), `${hash}.db`);
|
|
99
|
+
return resolveSessionDbPath({ projectDir, sessionsDir: getSessionDir() });
|
|
92
100
|
}
|
|
93
101
|
// ── Module-level DB singleton ─────────────────────────────
|
|
94
102
|
// Shared across all register() calls (one per agent session).
|
|
95
103
|
// Lazy-initialized on first register() using the first projectDir seen.
|
|
96
104
|
// Uses OpenClawSessionDB for session_key mapping and rename support.
|
|
97
105
|
let _dbSingleton = null;
|
|
106
|
+
let _dbSingletonPath = "";
|
|
98
107
|
function getOrCreateDB(projectDir) {
|
|
99
|
-
if
|
|
100
|
-
|
|
108
|
+
// Reopen the singleton if the resolved DB path changes. Production
|
|
109
|
+
// code normally loads the plugin once per process with a single
|
|
110
|
+
// workspace, but defensive re-keying on resolved path keeps the
|
|
111
|
+
// contract honest if a host ever calls register() twice with
|
|
112
|
+
// different projectDirs, and removes a subtle test-isolation
|
|
113
|
+
// foot-gun where a stale singleton pointed at a prior test's
|
|
114
|
+
// `<hash>.db`. Mirrors the Pi/OMP fix (#645).
|
|
115
|
+
const dbPath = getDBPath(projectDir);
|
|
116
|
+
if (!_dbSingleton || _dbSingletonPath !== dbPath) {
|
|
117
|
+
if (_dbSingleton) {
|
|
118
|
+
try {
|
|
119
|
+
_dbSingleton.close();
|
|
120
|
+
}
|
|
121
|
+
catch { /* best effort */ }
|
|
122
|
+
}
|
|
101
123
|
_dbSingleton = new OpenClawSessionDB({ dbPath });
|
|
124
|
+
_dbSingletonPath = dbPath;
|
|
102
125
|
_dbSingleton.cleanupOldSessions(7);
|
|
103
126
|
}
|
|
104
127
|
return _dbSingleton;
|
|
@@ -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) => {
|
|
@@ -196,8 +196,14 @@ export interface HookAdapter {
|
|
|
196
196
|
* Directory where persistent per-user memory is stored
|
|
197
197
|
* (e.g., ~/.claude/memory, ~/.codex/memories). Auto-memory scans
|
|
198
198
|
* *.md files in this directory.
|
|
199
|
+
*
|
|
200
|
+
* When `projectDir` is supplied, the path MUST be project-scoped (issue
|
|
201
|
+
* #663) so two projects running in parallel cannot read each other's
|
|
202
|
+
* memory. Adapters scope via `hashProjectDirCanonical(projectDir)`.
|
|
203
|
+
* Callers that pre-date this contract may omit `projectDir`; in that
|
|
204
|
+
* case the unscoped legacy path is returned.
|
|
199
205
|
*/
|
|
200
|
-
getMemoryDir(): string;
|
|
206
|
+
getMemoryDir(projectDir?: string): string;
|
|
201
207
|
/** Generate hook registration config for this platform. */
|
|
202
208
|
generateHookConfig(pluginRoot: string): HookRegistration;
|
|
203
209
|
/** Read current platform settings. */
|
package/build/cli.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* context-mode doctor → Diagnose runtime issues, hooks, FTS5, version
|
|
8
8
|
* context-mode upgrade → Fix hooks, permissions, and settings
|
|
9
9
|
* context-mode hook <platform> <event> → Dispatch a hook script (used by platform hook configs)
|
|
10
|
+
* CONTEXT_MODE_DIR=/abs/path context-mode → Override sessions/content storage root
|
|
11
|
+
* Empty/whitespace is ignored; non-empty values must be absolute.
|
|
10
12
|
*
|
|
11
13
|
* Platform auto-detection: CLI detects which platform is running
|
|
12
14
|
* (Claude Code, Gemini CLI, OpenCode, etc.) and uses the appropriate adapter.
|