context-mode 1.0.103 → 1.0.105
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 +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +39 -7
- package/bin/statusline.mjs +321 -0
- package/build/adapters/antigravity/index.d.ts +6 -0
- package/build/adapters/antigravity/index.js +10 -0
- package/build/adapters/base.d.ts +23 -0
- package/build/adapters/base.js +29 -0
- package/build/adapters/codex/index.d.ts +10 -0
- package/build/adapters/codex/index.js +22 -4
- package/build/adapters/cursor/index.d.ts +7 -0
- package/build/adapters/cursor/index.js +11 -0
- package/build/adapters/detect.d.ts +12 -1
- package/build/adapters/detect.js +69 -7
- package/build/adapters/gemini-cli/index.d.ts +8 -1
- package/build/adapters/gemini-cli/index.js +19 -7
- package/build/adapters/jetbrains-copilot/index.d.ts +7 -0
- package/build/adapters/jetbrains-copilot/index.js +12 -0
- package/build/adapters/kiro/index.d.ts +8 -0
- package/build/adapters/kiro/index.js +12 -0
- package/build/adapters/openclaw/index.d.ts +17 -0
- package/build/adapters/openclaw/index.js +29 -4
- package/build/adapters/opencode/index.d.ts +8 -0
- package/build/adapters/opencode/index.js +18 -6
- package/build/adapters/qwen-code/index.d.ts +1 -0
- package/build/adapters/qwen-code/index.js +3 -0
- package/build/adapters/types.d.ts +33 -0
- package/build/adapters/vscode-copilot/index.d.ts +6 -0
- package/build/adapters/vscode-copilot/index.js +10 -0
- package/build/adapters/zed/index.d.ts +1 -0
- package/build/adapters/zed/index.js +3 -0
- package/build/cli.d.ts +15 -0
- package/build/cli.js +62 -16
- package/build/concurrency/runPool.d.ts +36 -0
- package/build/concurrency/runPool.js +51 -0
- package/build/executor.d.ts +11 -1
- package/build/executor.js +77 -21
- package/build/fetch-cache.d.ts +13 -0
- package/build/fetch-cache.js +15 -0
- package/build/lifecycle.d.ts +6 -2
- package/build/lifecycle.js +29 -2
- package/build/opencode-plugin.d.ts +23 -0
- package/build/opencode-plugin.js +80 -6
- package/build/routing-block.d.ts +8 -0
- package/build/routing-block.js +86 -0
- package/build/runtime.d.ts +1 -0
- package/build/runtime.js +54 -3
- package/build/search/auto-memory.d.ts +23 -10
- package/build/search/auto-memory.js +64 -26
- package/build/search/unified.d.ts +3 -0
- package/build/search/unified.js +2 -2
- package/build/server.d.ts +47 -0
- package/build/server.js +736 -188
- package/build/session/analytics.d.ts +49 -1
- package/build/session/analytics.js +278 -16
- package/build/session/db.d.ts +53 -8
- package/build/session/db.js +200 -19
- package/build/session/extract.js +124 -2
- package/build/tool-naming.d.ts +4 -0
- package/build/tool-naming.js +24 -0
- package/cli.bundle.mjs +208 -158
- package/configs/antigravity/GEMINI.md +11 -0
- package/configs/claude-code/CLAUDE.md +11 -0
- package/configs/codex/AGENTS.md +11 -0
- package/configs/cursor/context-mode.mdc +11 -0
- package/configs/gemini-cli/GEMINI.md +11 -0
- package/configs/jetbrains-copilot/copilot-instructions.md +3 -0
- package/configs/kilo/AGENTS.md +11 -0
- package/configs/kiro/KIRO.md +11 -0
- package/configs/openclaw/AGENTS.md +11 -0
- package/configs/opencode/AGENTS.md +11 -0
- package/configs/pi/AGENTS.md +11 -0
- package/configs/qwen-code/QWEN.md +11 -0
- package/configs/vscode-copilot/copilot-instructions.md +3 -0
- package/configs/zed/AGENTS.md +11 -0
- package/hooks/auto-injection.mjs +36 -10
- package/hooks/cache-heal-utils.mjs +231 -0
- package/hooks/codex/sessionstart.mjs +7 -4
- package/hooks/core/routing.mjs +8 -2
- package/hooks/cursor/sessionstart.mjs +7 -4
- package/hooks/formatters/claude-code.mjs +20 -0
- package/hooks/gemini-cli/sessionstart.mjs +7 -2
- package/hooks/jetbrains-copilot/sessionstart.mjs +7 -2
- package/hooks/normalize-hooks.mjs +184 -0
- package/hooks/session-db.bundle.mjs +41 -14
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +68 -20
- package/hooks/session-loaders.mjs +8 -2
- package/hooks/sessionstart.mjs +8 -2
- package/hooks/vscode-copilot/sessionstart.mjs +7 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/server.bundle.mjs +181 -134
- package/skills/ctx-doctor/SKILL.md +3 -3
- package/skills/ctx-insight/SKILL.md +1 -1
- package/start.mjs +63 -3
|
@@ -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.105"
|
|
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.105",
|
|
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.105",
|
|
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.105",
|
|
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.105",
|
|
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
|
@@ -83,7 +83,7 @@ Restart Claude Code (or run `/reload-plugins`).
|
|
|
83
83
|
|
|
84
84
|
All checks should show `[x]`. The doctor validates runtimes, hooks, FTS5, and plugin registration.
|
|
85
85
|
|
|
86
|
-
**Routing:** Automatic. The SessionStart hook injects routing instructions at runtime — no file is written to your project. The plugin registers all hooks (PreToolUse, PostToolUse, PreCompact, SessionStart) and
|
|
86
|
+
**Routing:** Automatic. The SessionStart hook injects routing instructions at runtime — no file is written to your project. The plugin registers all hooks (PreToolUse, PostToolUse, PreCompact, SessionStart) and 11 MCP tools — six sandbox tools (`ctx_batch_execute`, `ctx_execute`, `ctx_execute_file`, `ctx_index`, `ctx_search`, `ctx_fetch_and_index`) plus five meta-tools (`ctx_stats`, `ctx_doctor`, `ctx_upgrade`, `ctx_purge`, `ctx_insight`).
|
|
87
87
|
|
|
88
88
|
| Slash Command | What it does |
|
|
89
89
|
|---|---|
|
|
@@ -95,6 +95,19 @@ All checks should show `[x]`. The doctor validates runtimes, hooks, FTS5, and pl
|
|
|
95
95
|
|
|
96
96
|
> **Note:** Slash commands are a Claude Code plugin feature. On other platforms, type `ctx stats`, `ctx doctor`, `ctx upgrade`, or `ctx insight` in the chat — the model calls the MCP tool automatically. See [Utility Commands](#utility-commands).
|
|
97
97
|
|
|
98
|
+
**Status line (optional):** Claude Code's plugin manifest cannot declare a status line, so this is a one-time manual edit to `~/.claude/settings.json`:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"statusLine": {
|
|
103
|
+
"type": "command",
|
|
104
|
+
"command": "context-mode statusline"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
After saving, restart Claude Code. The bar shows `$ saved this session · $ saved across sessions · % efficient` so you can see savings accumulate in real time. The wiring is path-free — `context-mode statusline` resolves through the bundled CLI regardless of where the plugin cache lives.
|
|
110
|
+
|
|
98
111
|
<details>
|
|
99
112
|
<summary>Alternative — MCP-only install (no hooks or slash commands)</summary>
|
|
100
113
|
|
|
@@ -102,7 +115,7 @@ All checks should show `[x]`. The doctor validates runtimes, hooks, FTS5, and pl
|
|
|
102
115
|
claude mcp add context-mode -- npx -y context-mode
|
|
103
116
|
```
|
|
104
117
|
|
|
105
|
-
This gives you
|
|
118
|
+
This gives you all 11 MCP tools without automatic routing. The model can still use them — it just won't be nudged to prefer them over raw Bash/Read/WebFetch. Good for trying it out before committing to the full plugin.
|
|
106
119
|
|
|
107
120
|
</details>
|
|
108
121
|
|
|
@@ -389,7 +402,7 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
|
|
|
389
402
|
}
|
|
390
403
|
```
|
|
391
404
|
|
|
392
|
-
The `mcp` entry registers
|
|
405
|
+
The `mcp` entry registers all 11 MCP tools. The `plugin` entry enables hooks — OpenCode calls the plugin's TypeScript functions directly before and after each tool execution, blocking dangerous commands and enforcing sandbox routing.
|
|
393
406
|
|
|
394
407
|
3. *(Optional)* Copy the routing rules file. OpenCode lacks a SessionStart hook, so the model needs an `AGENTS.md` file for routing awareness:
|
|
395
408
|
|
|
@@ -439,7 +452,7 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
|
|
|
439
452
|
}
|
|
440
453
|
```
|
|
441
454
|
|
|
442
|
-
The `mcp` entry registers
|
|
455
|
+
The `mcp` entry registers all 11 MCP tools. The `plugin` entry enables hooks — KiloCode calls the plugin's TypeScript functions directly before and after each tool execution, blocking dangerous commands and enforcing sandbox routing.
|
|
443
456
|
|
|
444
457
|
3. *(Optional)* Copy the routing rules file. KiloCode shares the OpenCode plugin architecture and lacks SessionStart, so the model needs an `AGENTS.md` file for routing awareness:
|
|
445
458
|
|
|
@@ -835,12 +848,12 @@ npm install -g context-mode
|
|
|
835
848
|
|
|
836
849
|
| Tool | What it does | Context saved |
|
|
837
850
|
|---|---|---|
|
|
838
|
-
| `ctx_batch_execute` | Run multiple commands + search multiple queries in ONE call. | 986 KB → 62 KB |
|
|
851
|
+
| `ctx_batch_execute` | Run multiple commands + search multiple queries in ONE call. Opt-in `concurrency: 1-8` for I/O-bound batches. | 986 KB → 62 KB |
|
|
839
852
|
| `ctx_execute` | Run code in 11 languages. Only stdout enters context. | 56 KB → 299 B |
|
|
840
853
|
| `ctx_execute_file` | Process files in sandbox. Raw content never leaves. | 45 KB → 155 B |
|
|
841
854
|
| `ctx_index` | Chunk markdown into FTS5 with BM25 ranking. | 60 KB → 40 B |
|
|
842
855
|
| `ctx_search` | Query indexed content with multiple queries in one call. | On-demand retrieval |
|
|
843
|
-
| `ctx_fetch_and_index` | Fetch URL, chunk and index. 24h TTL cache — repeat calls skip network. `force: true` to bypass. | 60 KB → 40 B |
|
|
856
|
+
| `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 |
|
|
844
857
|
| `ctx_stats` | Show context savings, call counts, and session statistics. | — |
|
|
845
858
|
| `ctx_doctor` | Diagnose installation: runtimes, hooks, FTS5, versions. | — |
|
|
846
859
|
| `ctx_upgrade` | Upgrade to latest version from GitHub, rebuild, reconfigure hooks. | — |
|
|
@@ -1055,7 +1068,7 @@ Detailed event data is also indexed into FTS5 for on-demand retrieval via `ctx_s
|
|
|
1055
1068
|
>
|
|
1056
1069
|
> **Kiro** supports native `preToolUse` and `postToolUse` hooks for routing enforcement and tool event capture. `agentSpawn` (SessionStart equivalent) and `stop` are not yet wired. Requires manually copying `KIRO.md` to your project root. Kiro is auto-detected via MCP protocol handshake (`clientInfo.name`).
|
|
1057
1070
|
>
|
|
1058
|
-
> **Pi Coding Agent** runs context-mode as an extension with full hook support. The extension registers `tool_call`, `tool_result`, `session_start`, and `session_before_compact` events, providing high session continuity coverage. The MCP server provides
|
|
1071
|
+
> **Pi Coding Agent** runs context-mode as an extension with full hook support. The extension registers `tool_call`, `tool_result`, `session_start`, and `session_before_compact` events, providing high session continuity coverage. The MCP server provides all 11 MCP tools.
|
|
1059
1072
|
|
|
1060
1073
|
### Routing Enforcement
|
|
1061
1074
|
|
|
@@ -1207,6 +1220,25 @@ Commands chained with `&&`, `;`, or `|` are split — each part is checked separ
|
|
|
1207
1220
|
|
|
1208
1221
|
**deny** always wins over **allow**. More specific (project-level) rules override global ones.
|
|
1209
1222
|
|
|
1223
|
+
### Network fetch hardening
|
|
1224
|
+
|
|
1225
|
+
`ctx_fetch_and_index` blocks dangerous URL targets by default:
|
|
1226
|
+
|
|
1227
|
+
- **Schemes**: only `http:` and `https:` allowed (no `file://`, `gopher://`, `javascript:`, `data:`).
|
|
1228
|
+
- **Cloud metadata + link-local**: `169.254.0.0/16` (incl. AWS/GCP/Azure IMDS endpoint `169.254.169.254`) hard-blocked even if a hostname resolves to it (DNS-rebinding defense).
|
|
1229
|
+
- **Multicast / reserved**: `224.0.0.0/4`, `0.0.0.0/8`, IPv6 `ff00::/8`, `fe80::/10` blocked.
|
|
1230
|
+
- **Loopback + RFC1918** (`localhost`, `127.x`, `10.x`, `172.16-31.x`, `192.168.x`, IPv6 `::1`, `fc00::/7`) **allowed by default** so local dev servers + internal-network fetches keep working.
|
|
1231
|
+
|
|
1232
|
+
For hosted/CI environments where you want to block private targets too, set:
|
|
1233
|
+
|
|
1234
|
+
```bash
|
|
1235
|
+
export CTX_FETCH_STRICT=1
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
That blocks loopback + RFC1918 + ULA in addition to the always-blocked ranges. Useful when context-mode runs as a shared service, not on a developer's own machine.
|
|
1239
|
+
|
|
1240
|
+
`tool_input` for any `mcp__*` tool call is also redacted before persistence — keys matching `authorization`, `token`, `secret`, `password`, `api_key`, `cookie`, `signature`, `private_key` get masked to `[REDACTED]` so credentials in MCP arguments don't end up in the session DB.
|
|
1241
|
+
|
|
1210
1242
|
## Contributing
|
|
1211
1243
|
|
|
1212
1244
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and TDD guidelines.
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* context-mode status line — Claude Code statusLine integration.
|
|
4
|
+
*
|
|
5
|
+
* Reads the persisted stats file written by the MCP server and prints a
|
|
6
|
+
* single-line, value-first status string designed for enterprise dev
|
|
7
|
+
* surfaces (Loom demos, Slack screen shares, over-the-shoulder closes).
|
|
8
|
+
*
|
|
9
|
+
* Discipline (Datadog / Stripe / Vercel pattern):
|
|
10
|
+
* - "context-mode" full brand label, never abbreviated
|
|
11
|
+
* - ONE chromatic accent (status dot ●), everything else monochrome
|
|
12
|
+
* - Bold for KPI numbers ($, %), dim for context
|
|
13
|
+
* - No counts (calls / tokens / events) — only $ and % pass the
|
|
14
|
+
* value-per-pixel test
|
|
15
|
+
*
|
|
16
|
+
* Wire it up in ~/.claude/settings.json (path-free — uses the bundled CLI
|
|
17
|
+
* forwarder so users don't have to know the absolute install path):
|
|
18
|
+
* {
|
|
19
|
+
* "statusLine": {
|
|
20
|
+
* "type": "command",
|
|
21
|
+
* "command": "context-mode statusline"
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* Or, if you prefer to skip the CLI shim, point directly at this file:
|
|
26
|
+
* "command": "node /absolute/path/to/context-mode/bin/statusline.mjs"
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
import { homedir } from "node:os";
|
|
32
|
+
import { execFileSync } from "node:child_process";
|
|
33
|
+
|
|
34
|
+
// ── Schema versioning ───────────────────────────────────────────────────
|
|
35
|
+
// Bumped by the MCP writer (src/server.ts) when the persisted stats payload
|
|
36
|
+
// shape changes. Statusline reads `schemaVersion` from the payload:
|
|
37
|
+
// - missing → legacy v1.0.103 era, proceed with sensible defaults
|
|
38
|
+
// - <= KNOWN → safe to render fully
|
|
39
|
+
// - > KNOWN → newer writer than this reader; warn once + render what we
|
|
40
|
+
// still understand (graceful degrade rather than blank bar)
|
|
41
|
+
const KNOWN_SCHEMA_VERSION = 1;
|
|
42
|
+
|
|
43
|
+
// Test seams — keep production behaviour identical when env vars unset.
|
|
44
|
+
// CTX_TEST_PLATFORM — override process.platform for cross-OS resolver tests
|
|
45
|
+
// CTX_TEST_PROC_DIR — override /proc base dir for Linux PID-walk tests
|
|
46
|
+
const TEST_PLATFORM = process.env.CTX_TEST_PLATFORM;
|
|
47
|
+
const PROC_DIR = process.env.CTX_TEST_PROC_DIR || "/proc";
|
|
48
|
+
function platform() {
|
|
49
|
+
return TEST_PLATFORM || process.platform;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Single-shot stderr warning latch — keep noise out of Claude Code's
|
|
53
|
+
// statusline output even when our parent runs us repeatedly per session.
|
|
54
|
+
let __winWarned = false;
|
|
55
|
+
function warnOnce(key, msg) {
|
|
56
|
+
if (key === "win" && __winWarned) return;
|
|
57
|
+
if (key === "win") __winWarned = true;
|
|
58
|
+
try { process.stderr.write(`context-mode statusline: ${msg}\n`); } catch { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── ANSI palette (single chromatic accent on the status dot) ────────────
|
|
62
|
+
const NO_COLOR = process.env.NO_COLOR || !process.stdout.isTTY;
|
|
63
|
+
const ansi = (code, text) => (NO_COLOR ? text : `\x1b[${code}m${text}\x1b[0m`);
|
|
64
|
+
const brand = (t) => ansi("1;36", t); // bold cyan — brand presence
|
|
65
|
+
const bold = (t) => ansi("1", t); // bold default fg — KPI numbers
|
|
66
|
+
const dim = (t) => ansi("2", t); // dim default fg — context
|
|
67
|
+
const green = (t) => ansi("32", t); // healthy dot
|
|
68
|
+
const yellow = (t) => ansi("33", t); // degraded dot
|
|
69
|
+
const red = (t) => ansi("31", t); // stale dot
|
|
70
|
+
const SEP = dim("·");
|
|
71
|
+
|
|
72
|
+
// ── Stats file lookup ────────────────────────────────────────────────────
|
|
73
|
+
function readStdinJson() {
|
|
74
|
+
try {
|
|
75
|
+
const raw = readFileSync(0, "utf-8");
|
|
76
|
+
if (!raw.trim()) return {};
|
|
77
|
+
return JSON.parse(raw);
|
|
78
|
+
} catch {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveSessionDir() {
|
|
84
|
+
if (process.env.CONTEXT_MODE_SESSION_DIR) {
|
|
85
|
+
return process.env.CONTEXT_MODE_SESSION_DIR;
|
|
86
|
+
}
|
|
87
|
+
return join(homedir(), ".claude", "context-mode", "sessions");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Walk up the parent process chain to find the Claude Code PID.
|
|
92
|
+
*
|
|
93
|
+
* Claude Code spawns the status line through a shell, so process.ppid is
|
|
94
|
+
* the intermediate shell, not Claude Code itself. We walk up until we find
|
|
95
|
+
* a process whose name matches /claude/i.
|
|
96
|
+
*
|
|
97
|
+
* Per-OS resolver:
|
|
98
|
+
* - linux: read PPid + Name from /proc/<pid>/status
|
|
99
|
+
* - darwin: ps -o ppid=,comm= -p <pid> (BSD ps; works without /proc)
|
|
100
|
+
* - win32: degraded — process.ppid only, with a one-shot stderr warning
|
|
101
|
+
*
|
|
102
|
+
* Without this walk, multiple concurrent Claude sessions all see the same
|
|
103
|
+
* shell ppid and collide on the fuzzy mtime fallback in findStatsFile.
|
|
104
|
+
*/
|
|
105
|
+
function findClaudePid() {
|
|
106
|
+
const plat = platform();
|
|
107
|
+
if (plat === "linux") return findClaudePidLinux();
|
|
108
|
+
if (plat === "darwin") return findClaudePidDarwin();
|
|
109
|
+
if (plat === "win32") {
|
|
110
|
+
warnOnce(
|
|
111
|
+
"win",
|
|
112
|
+
"Windows process-tree walk unsupported; multiple concurrent Claude sessions may collide. Set CLAUDE_SESSION_ID for deterministic resolution.",
|
|
113
|
+
);
|
|
114
|
+
return process.ppid;
|
|
115
|
+
}
|
|
116
|
+
return process.ppid;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function findClaudePidLinux() {
|
|
120
|
+
let pid = process.ppid;
|
|
121
|
+
for (let i = 0; i < 8 && pid && pid > 1; i++) {
|
|
122
|
+
try {
|
|
123
|
+
const status = readFileSync(`${PROC_DIR}/${pid}/status`, "utf-8");
|
|
124
|
+
const nameMatch = status.match(/^Name:\s+(.+)$/m);
|
|
125
|
+
const ppidMatch = status.match(/^PPid:\s+(\d+)/m);
|
|
126
|
+
const name = nameMatch?.[1]?.trim() ?? "";
|
|
127
|
+
if (/claude/i.test(name)) return pid;
|
|
128
|
+
pid = ppidMatch ? Number(ppidMatch[1]) : 0;
|
|
129
|
+
} catch {
|
|
130
|
+
return process.ppid;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return process.ppid;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function findClaudePidDarwin() {
|
|
137
|
+
let pid = process.ppid;
|
|
138
|
+
for (let i = 0; i < 8 && pid && pid > 1; i++) {
|
|
139
|
+
try {
|
|
140
|
+
// `ps -o ppid=,comm= -p <pid>` → " 12345 /path/to/claude"
|
|
141
|
+
const out = execFileSync(
|
|
142
|
+
"ps",
|
|
143
|
+
["-o", "ppid=,comm=", "-p", String(pid)],
|
|
144
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] },
|
|
145
|
+
).trim();
|
|
146
|
+
if (!out) return process.ppid;
|
|
147
|
+
const m = out.match(/^\s*(\d+)\s+(.+)$/);
|
|
148
|
+
if (!m) return process.ppid;
|
|
149
|
+
const parentPid = Number(m[1]);
|
|
150
|
+
const comm = m[2].trim();
|
|
151
|
+
// comm may be a path; check basename for claude
|
|
152
|
+
const base = comm.split("/").pop() || comm;
|
|
153
|
+
if (/claude/i.test(base)) return pid;
|
|
154
|
+
pid = parentPid;
|
|
155
|
+
} catch {
|
|
156
|
+
return process.ppid;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return process.ppid;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveSessionId() {
|
|
163
|
+
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
164
|
+
return `pid-${findClaudePid()}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function findStatsFile(sessionDir, sessionId) {
|
|
168
|
+
const direct = join(sessionDir, `stats-${sessionId}.json`);
|
|
169
|
+
if (existsSync(direct)) return direct;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const candidates = readdirSync(sessionDir)
|
|
173
|
+
.filter((f) => f.startsWith("stats-") && f.endsWith(".json"))
|
|
174
|
+
.map((f) => {
|
|
175
|
+
const full = join(sessionDir, f);
|
|
176
|
+
try {
|
|
177
|
+
return { full, mtime: statSync(full).mtimeMs };
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
.filter(Boolean)
|
|
183
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
184
|
+
|
|
185
|
+
// Only fall back to a file modified within the last 30 minutes —
|
|
186
|
+
// older files almost always belong to a stopped MCP server.
|
|
187
|
+
const fresh = candidates.find(
|
|
188
|
+
(c) => Date.now() - c.mtime < 30 * 60 * 1000,
|
|
189
|
+
);
|
|
190
|
+
if (fresh) return fresh.full;
|
|
191
|
+
} catch { /* ignore — sessionDir might not exist yet */ }
|
|
192
|
+
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function loadStats(path) {
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
199
|
+
if (parsed && typeof parsed === "object") {
|
|
200
|
+
// schemaVersion is optional — legacy v1.0.103 payloads omit it.
|
|
201
|
+
// Default to 0 so unknown-newer detection still has a clean compare.
|
|
202
|
+
const version = Number.isFinite(parsed.schemaVersion)
|
|
203
|
+
? parsed.schemaVersion
|
|
204
|
+
: 0;
|
|
205
|
+
if (version > KNOWN_SCHEMA_VERSION) {
|
|
206
|
+
try {
|
|
207
|
+
process.stderr.write(
|
|
208
|
+
`context-mode statusline: stats schemaVersion=${version} newer than known=${KNOWN_SCHEMA_VERSION}; rendering known fields only. Upgrade context-mode to suppress this warning.\n`,
|
|
209
|
+
);
|
|
210
|
+
} catch { /* ignore */ }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return parsed;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Formatters ───────────────────────────────────────────────────────────
|
|
220
|
+
function fmtUsd(n) {
|
|
221
|
+
const safe = Number.isFinite(n) && n >= 0 ? n : 0;
|
|
222
|
+
if (safe >= 100) return `$${safe.toFixed(0)}`;
|
|
223
|
+
if (safe >= 10) return `$${safe.toFixed(2)}`;
|
|
224
|
+
return `$${safe.toFixed(2)}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function fmtUptime(ms) {
|
|
228
|
+
const sec = Math.floor(ms / 1000);
|
|
229
|
+
if (sec < 60) return `${sec}s`;
|
|
230
|
+
const min = Math.floor(sec / 60);
|
|
231
|
+
if (min < 60) return `${min}m`;
|
|
232
|
+
const hr = Math.floor(min / 60);
|
|
233
|
+
const remMin = min % 60;
|
|
234
|
+
return remMin > 0 ? `${hr}h${remMin}m` : `${hr}h`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Status dot — the ONE accent ──────────────────────────────────────────
|
|
238
|
+
function statusDot(pct, isStale) {
|
|
239
|
+
if (isStale) return red("●");
|
|
240
|
+
if (pct >= 50) return green("●");
|
|
241
|
+
if (pct >= 1) return yellow("●");
|
|
242
|
+
return green("●");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Main render ──────────────────────────────────────────────────────────
|
|
246
|
+
function main() {
|
|
247
|
+
readStdinJson(); // drain stdin even if unused, keeps Claude Code happy
|
|
248
|
+
const sessionDir = resolveSessionDir();
|
|
249
|
+
const sessionId = resolveSessionId();
|
|
250
|
+
const statsFile = findStatsFile(sessionDir, sessionId);
|
|
251
|
+
|
|
252
|
+
// BRAND-NEW — no stats file. Use only the substantiated README headline
|
|
253
|
+
// claim ("saves ~98% of context window"). No fabricated $/dev/month or
|
|
254
|
+
// social-proof numbers we cannot back with data.
|
|
255
|
+
if (!statsFile) {
|
|
256
|
+
process.stdout.write(
|
|
257
|
+
`${brand("context-mode")} ${green("●")} ${dim("saves ~98% of context window")}`,
|
|
258
|
+
);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const stats = loadStats(statsFile);
|
|
263
|
+
if (!stats) {
|
|
264
|
+
process.stdout.write(
|
|
265
|
+
`${brand("context-mode")} ${green("●")} ${dim("saves ~98% of context window")}`,
|
|
266
|
+
);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// STALE — stats file >30min old, MCP likely stopped
|
|
271
|
+
const ageMs = Date.now() - (stats.updated_at || 0);
|
|
272
|
+
const stale = ageMs > 30 * 60 * 1000;
|
|
273
|
+
if (stale) {
|
|
274
|
+
process.stdout.write(
|
|
275
|
+
`${brand("context-mode")} ${red("●")} ${dim("stale — restart to resume saving")}`,
|
|
276
|
+
);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const sessionUsd = stats.dollars_saved_session ?? 0;
|
|
281
|
+
const lifetimeUsd = stats.dollars_saved_lifetime ?? 0;
|
|
282
|
+
const pct = stats.reduction_pct ?? 0;
|
|
283
|
+
const uptime = fmtUptime(stats.uptime_ms ?? 0);
|
|
284
|
+
const dot = statusDot(pct, false);
|
|
285
|
+
|
|
286
|
+
// FRESH — no session $ yet, lead with persistence value
|
|
287
|
+
if (sessionUsd === 0) {
|
|
288
|
+
if (lifetimeUsd > 0) {
|
|
289
|
+
// Lifetime $ exists — persistence as primary value, brand-poem echo
|
|
290
|
+
process.stdout.write(
|
|
291
|
+
`${brand("context-mode")} ${dot} ${bold(fmtUsd(lifetimeUsd))} ${dim("saved across sessions")} ${SEP} ${dim("preserved across compact, restart & upgrade")}`,
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
// First-ever session, no lifetime data yet — substantiated headline only
|
|
295
|
+
process.stdout.write(
|
|
296
|
+
`${brand("context-mode")} ${dot} ${dim("ready — saves ~98% of context window")}`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ACTIVE / DEGRADED — session $ · [lifetime $ when present] · % efficient · uptime
|
|
303
|
+
// Status dot color encodes degraded vs healthy via pct.
|
|
304
|
+
// Lifetime block is conditional: persistStats omits dollars_saved_lifetime
|
|
305
|
+
// when no analytics aggregator is available, so we degrade gracefully to
|
|
306
|
+
// a session-only render rather than printing "$0.00 saved across sessions".
|
|
307
|
+
const valueBlocks = [
|
|
308
|
+
`${bold(fmtUsd(sessionUsd))} ${dim("saved this session")}`,
|
|
309
|
+
];
|
|
310
|
+
if (lifetimeUsd > 0) {
|
|
311
|
+
valueBlocks.push(`${bold(fmtUsd(lifetimeUsd))} ${dim("saved across sessions")}`);
|
|
312
|
+
}
|
|
313
|
+
valueBlocks.push(`${bold(`${pct}%`)} ${dim("efficient")}`);
|
|
314
|
+
valueBlocks.push(dim(uptime));
|
|
315
|
+
|
|
316
|
+
const head = `${brand("context-mode")} ${dot} `;
|
|
317
|
+
const tail = valueBlocks.join(` ${SEP} `);
|
|
318
|
+
process.stdout.write(head + tail);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
main();
|
|
@@ -32,6 +32,12 @@ export declare class AntigravityAdapter extends BaseAdapter implements HookAdapt
|
|
|
32
32
|
formatPreCompactResponse(_response: PreCompactResponse): unknown;
|
|
33
33
|
formatSessionStartResponse(_response: SessionStartResponse): unknown;
|
|
34
34
|
getSettingsPath(): string;
|
|
35
|
+
/**
|
|
36
|
+
* Antigravity nests under ~/.gemini/antigravity/. Always absolute.
|
|
37
|
+
* `_projectDir` accepted for interface symmetry but unused — home-rooted.
|
|
38
|
+
*/
|
|
39
|
+
getConfigDir(_projectDir?: string): string;
|
|
40
|
+
getInstructionFiles(): string[];
|
|
35
41
|
generateHookConfig(_pluginRoot: string): HookRegistration;
|
|
36
42
|
readSettings(): Record<string, unknown> | null;
|
|
37
43
|
writeSettings(settings: Record<string, unknown>): void;
|
|
@@ -72,6 +72,16 @@ export class AntigravityAdapter extends BaseAdapter {
|
|
|
72
72
|
getSettingsPath() {
|
|
73
73
|
return resolve(homedir(), ".gemini", "antigravity", "mcp_config.json");
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Antigravity nests under ~/.gemini/antigravity/. Always absolute.
|
|
77
|
+
* `_projectDir` accepted for interface symmetry but unused — home-rooted.
|
|
78
|
+
*/
|
|
79
|
+
getConfigDir(_projectDir) {
|
|
80
|
+
return resolve(homedir(), ".gemini", "antigravity");
|
|
81
|
+
}
|
|
82
|
+
getInstructionFiles() {
|
|
83
|
+
return ["GEMINI.md"];
|
|
84
|
+
}
|
|
75
85
|
generateHookConfig(_pluginRoot) {
|
|
76
86
|
return {};
|
|
77
87
|
}
|
package/build/adapters/base.d.ts
CHANGED
|
@@ -22,6 +22,29 @@ export declare abstract class BaseAdapter {
|
|
|
22
22
|
getSessionDir(): string;
|
|
23
23
|
getSessionDBPath(projectDir: string): string;
|
|
24
24
|
getSessionEventsPath(projectDir: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Default: build config dir from sessionDirSegments rooted at $HOME.
|
|
27
|
+
*
|
|
28
|
+
* Contract: ALWAYS returns an absolute path. Adapters with project-scoped
|
|
29
|
+
* or non-home-rooted config dirs (cursor, vscode-copilot, jetbrains-copilot,
|
|
30
|
+
* openclaw, opencode) override this and resolve their segments against
|
|
31
|
+
* `projectDir` (or `process.cwd()` when omitted).
|
|
32
|
+
*
|
|
33
|
+
* @param _projectDir Unused by the home-rooted default — accepted so
|
|
34
|
+
* project-scoped overrides honor the same signature.
|
|
35
|
+
*/
|
|
36
|
+
getConfigDir(_projectDir?: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Default: Claude Code convention. Most adapters override with their
|
|
39
|
+
* own platform-specific instruction file name (AGENTS.md, GEMINI.md, ...).
|
|
40
|
+
*/
|
|
41
|
+
getInstructionFiles(): string[];
|
|
42
|
+
/**
|
|
43
|
+
* Default: <configDir>/memory. Always absolute (configDir is absolute by
|
|
44
|
+
* contract). Adapters with a different memory dir name (e.g., codex uses
|
|
45
|
+
* "memories" plural) override this.
|
|
46
|
+
*/
|
|
47
|
+
getMemoryDir(): string;
|
|
25
48
|
backupSettings(): string | null;
|
|
26
49
|
abstract getSettingsPath(): string;
|
|
27
50
|
}
|
package/build/adapters/base.js
CHANGED
|
@@ -44,6 +44,35 @@ export class BaseAdapter {
|
|
|
44
44
|
.slice(0, 16);
|
|
45
45
|
return join(this.getSessionDir(), `${hash}-events.md`);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Default: build config dir from sessionDirSegments rooted at $HOME.
|
|
49
|
+
*
|
|
50
|
+
* Contract: ALWAYS returns an absolute path. Adapters with project-scoped
|
|
51
|
+
* or non-home-rooted config dirs (cursor, vscode-copilot, jetbrains-copilot,
|
|
52
|
+
* openclaw, opencode) override this and resolve their segments against
|
|
53
|
+
* `projectDir` (or `process.cwd()` when omitted).
|
|
54
|
+
*
|
|
55
|
+
* @param _projectDir Unused by the home-rooted default — accepted so
|
|
56
|
+
* project-scoped overrides honor the same signature.
|
|
57
|
+
*/
|
|
58
|
+
getConfigDir(_projectDir) {
|
|
59
|
+
return join(homedir(), ...this.sessionDirSegments);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Default: Claude Code convention. Most adapters override with their
|
|
63
|
+
* own platform-specific instruction file name (AGENTS.md, GEMINI.md, ...).
|
|
64
|
+
*/
|
|
65
|
+
getInstructionFiles() {
|
|
66
|
+
return ["CLAUDE.md"];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Default: <configDir>/memory. Always absolute (configDir is absolute by
|
|
70
|
+
* contract). Adapters with a different memory dir name (e.g., codex uses
|
|
71
|
+
* "memories" plural) override this.
|
|
72
|
+
*/
|
|
73
|
+
getMemoryDir() {
|
|
74
|
+
return join(this.getConfigDir(), "memory");
|
|
75
|
+
}
|
|
47
76
|
backupSettings() {
|
|
48
77
|
const settingsPath = this.getSettingsPath();
|
|
49
78
|
try {
|
|
@@ -29,6 +29,8 @@ export declare class CodexAdapter extends BaseAdapter implements HookAdapter {
|
|
|
29
29
|
formatPreCompactResponse(response: PreCompactResponse): unknown;
|
|
30
30
|
formatSessionStartResponse(response: SessionStartResponse): unknown;
|
|
31
31
|
getSettingsPath(): string;
|
|
32
|
+
getInstructionFiles(): string[];
|
|
33
|
+
getMemoryDir(): string;
|
|
32
34
|
generateHookConfig(pluginRoot: string): HookRegistration;
|
|
33
35
|
readSettings(): Record<string, unknown> | null;
|
|
34
36
|
writeSettings(_settings: Record<string, unknown>): void;
|
|
@@ -39,6 +41,14 @@ export declare class CodexAdapter extends BaseAdapter implements HookAdapter {
|
|
|
39
41
|
setHookPermissions(_pluginRoot: string): string[];
|
|
40
42
|
updatePluginRegistry(_pluginRoot: string, _version: string): void;
|
|
41
43
|
getRoutingInstructions(): string;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the project directory for a Codex hook input.
|
|
46
|
+
* Priority: input.cwd > CODEX_PROJECT_DIR env > process.cwd().
|
|
47
|
+
* Mirrors the cursor / opencode pattern so downstream hooks always
|
|
48
|
+
* receive a defined projectDir even under worktrees or when the
|
|
49
|
+
* platform omits cwd from the wire payload.
|
|
50
|
+
*/
|
|
51
|
+
private getProjectDir;
|
|
42
52
|
/**
|
|
43
53
|
* Extract session ID from Codex CLI hook input.
|
|
44
54
|
* Priority: session_id field > fallback to ppid.
|
|
@@ -44,7 +44,7 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
44
44
|
toolName: input.tool_name ?? "",
|
|
45
45
|
toolInput: input.tool_input ?? {},
|
|
46
46
|
sessionId: this.extractSessionId(input),
|
|
47
|
-
projectDir: input
|
|
47
|
+
projectDir: this.getProjectDir(input),
|
|
48
48
|
raw,
|
|
49
49
|
};
|
|
50
50
|
}
|
|
@@ -55,7 +55,7 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
55
55
|
toolInput: input.tool_input ?? {},
|
|
56
56
|
toolOutput: input.tool_response,
|
|
57
57
|
sessionId: this.extractSessionId(input),
|
|
58
|
-
projectDir: input
|
|
58
|
+
projectDir: this.getProjectDir(input),
|
|
59
59
|
raw,
|
|
60
60
|
};
|
|
61
61
|
}
|
|
@@ -63,7 +63,7 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
63
63
|
const input = raw;
|
|
64
64
|
return {
|
|
65
65
|
sessionId: this.extractSessionId(input),
|
|
66
|
-
projectDir: input
|
|
66
|
+
projectDir: this.getProjectDir(input),
|
|
67
67
|
raw,
|
|
68
68
|
};
|
|
69
69
|
}
|
|
@@ -87,7 +87,7 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
87
87
|
return {
|
|
88
88
|
sessionId: this.extractSessionId(input),
|
|
89
89
|
source,
|
|
90
|
-
projectDir: input
|
|
90
|
+
projectDir: this.getProjectDir(input),
|
|
91
91
|
raw,
|
|
92
92
|
};
|
|
93
93
|
}
|
|
@@ -148,6 +148,14 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
148
148
|
getSettingsPath() {
|
|
149
149
|
return resolve(homedir(), ".codex", "config.toml");
|
|
150
150
|
}
|
|
151
|
+
getInstructionFiles() {
|
|
152
|
+
// Codex CLI honors AGENTS.md plus an optional override file.
|
|
153
|
+
return ["AGENTS.md", "AGENTS.override.md"];
|
|
154
|
+
}
|
|
155
|
+
getMemoryDir() {
|
|
156
|
+
// Codex uses "memories" (plural), not the default "memory".
|
|
157
|
+
return resolve(homedir(), ".codex", "memories");
|
|
158
|
+
}
|
|
151
159
|
generateHookConfig(pluginRoot) {
|
|
152
160
|
return {
|
|
153
161
|
PreToolUse: [
|
|
@@ -298,6 +306,16 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
298
306
|
}
|
|
299
307
|
}
|
|
300
308
|
// ── Internal helpers ───────────────────────────────────
|
|
309
|
+
/**
|
|
310
|
+
* Resolve the project directory for a Codex hook input.
|
|
311
|
+
* Priority: input.cwd > CODEX_PROJECT_DIR env > process.cwd().
|
|
312
|
+
* Mirrors the cursor / opencode pattern so downstream hooks always
|
|
313
|
+
* receive a defined projectDir even under worktrees or when the
|
|
314
|
+
* platform omits cwd from the wire payload.
|
|
315
|
+
*/
|
|
316
|
+
getProjectDir(input) {
|
|
317
|
+
return input.cwd ?? process.env.CODEX_PROJECT_DIR ?? process.cwd();
|
|
318
|
+
}
|
|
301
319
|
/**
|
|
302
320
|
* Extract session ID from Codex CLI hook input.
|
|
303
321
|
* Priority: session_id field > fallback to ppid.
|