context-mode 1.0.110 → 1.0.112
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/index.ts +3 -2
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +152 -34
- package/bin/statusline.mjs +144 -127
- package/build/adapters/base.d.ts +8 -5
- package/build/adapters/base.js +8 -18
- package/build/adapters/claude-code/index.d.ts +24 -3
- package/build/adapters/claude-code/index.js +44 -11
- package/build/adapters/codex/hooks.d.ts +10 -5
- package/build/adapters/codex/hooks.js +10 -5
- package/build/adapters/codex/index.d.ts +17 -5
- package/build/adapters/codex/index.js +337 -37
- package/build/adapters/codex/paths.d.ts +1 -0
- package/build/adapters/codex/paths.js +12 -0
- package/build/adapters/cursor/index.d.ts +6 -0
- package/build/adapters/cursor/index.js +83 -2
- package/build/adapters/detect.d.ts +1 -1
- package/build/adapters/detect.js +29 -6
- package/build/adapters/omp/index.d.ts +65 -0
- package/build/adapters/omp/index.js +182 -0
- package/build/adapters/omp/plugin.d.ts +75 -0
- package/build/adapters/omp/plugin.js +220 -0
- package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
- package/build/adapters/openclaw/mcp-tools.js +198 -0
- package/build/adapters/openclaw/plugin.d.ts +130 -0
- package/build/adapters/openclaw/plugin.js +629 -0
- package/build/adapters/openclaw/workspace-router.d.ts +29 -0
- package/build/adapters/openclaw/workspace-router.js +64 -0
- package/build/adapters/opencode/plugin.d.ts +145 -0
- package/build/adapters/opencode/plugin.js +457 -0
- package/build/adapters/pi/extension.d.ts +26 -0
- package/build/adapters/pi/extension.js +552 -0
- package/build/adapters/pi/index.d.ts +57 -0
- package/build/adapters/pi/index.js +173 -0
- package/build/adapters/pi/mcp-bridge.d.ts +113 -0
- package/build/adapters/pi/mcp-bridge.js +251 -0
- package/build/adapters/types.d.ts +11 -6
- package/build/cli.js +186 -170
- package/build/db-base.d.ts +15 -2
- package/build/db-base.js +50 -5
- package/build/executor.d.ts +2 -0
- package/build/executor.js +15 -2
- package/build/opencode-plugin.js +1 -1
- package/build/runPool.d.ts +36 -0
- package/build/runPool.js +51 -0
- package/build/runtime.js +64 -5
- package/build/search/auto-memory.js +6 -4
- package/build/security.js +30 -10
- package/build/server.d.ts +23 -1
- package/build/server.js +652 -174
- package/build/session/analytics.d.ts +404 -1
- package/build/session/analytics.js +1347 -42
- package/build/session/db.d.ts +114 -5
- package/build/session/db.js +275 -27
- package/build/session/event-emit.d.ts +48 -0
- package/build/session/event-emit.js +101 -0
- package/build/session/extract.d.ts +1 -0
- package/build/session/extract.js +79 -12
- package/build/session/purge.d.ts +111 -0
- package/build/session/purge.js +138 -0
- package/build/store.d.ts +7 -0
- package/build/store.js +69 -6
- package/build/util/claude-config.d.ts +26 -0
- package/build/util/claude-config.js +91 -0
- package/build/util/hook-config.d.ts +4 -0
- package/build/util/hook-config.js +39 -0
- package/cli.bundle.mjs +411 -208
- package/configs/antigravity/GEMINI.md +0 -3
- package/configs/claude-code/CLAUDE.md +1 -4
- package/configs/codex/AGENTS.md +1 -4
- package/configs/codex/config.toml +3 -0
- package/configs/codex/hooks.json +8 -0
- package/configs/cursor/context-mode.mdc +0 -3
- package/configs/gemini-cli/GEMINI.md +0 -3
- package/configs/jetbrains-copilot/copilot-instructions.md +0 -3
- package/configs/kilo/AGENTS.md +0 -3
- package/configs/kiro/KIRO.md +0 -3
- package/configs/omp/SYSTEM.md +85 -0
- package/configs/omp/mcp.json +7 -0
- package/configs/openclaw/AGENTS.md +0 -3
- package/configs/opencode/AGENTS.md +0 -3
- package/configs/pi/AGENTS.md +0 -3
- package/configs/qwen-code/QWEN.md +1 -4
- package/configs/vscode-copilot/copilot-instructions.md +0 -3
- package/configs/zed/AGENTS.md +0 -3
- package/hooks/codex/posttooluse.mjs +9 -2
- package/hooks/codex/precompact.mjs +69 -0
- package/hooks/codex/sessionstart.mjs +13 -9
- package/hooks/codex/stop.mjs +1 -2
- package/hooks/codex/userpromptsubmit.mjs +1 -2
- package/hooks/core/routing.mjs +237 -18
- package/hooks/cursor/afteragentresponse.mjs +1 -1
- package/hooks/cursor/hooks.json +31 -0
- package/hooks/cursor/posttooluse.mjs +1 -1
- package/hooks/cursor/sessionstart.mjs +5 -5
- package/hooks/cursor/stop.mjs +1 -1
- package/hooks/ensure-deps.mjs +12 -13
- package/hooks/gemini-cli/aftertool.mjs +1 -1
- package/hooks/gemini-cli/beforeagent.mjs +1 -1
- package/hooks/gemini-cli/precompress.mjs +3 -2
- package/hooks/gemini-cli/sessionstart.mjs +9 -9
- package/hooks/jetbrains-copilot/posttooluse.mjs +1 -1
- package/hooks/jetbrains-copilot/precompact.mjs +3 -2
- package/hooks/jetbrains-copilot/sessionstart.mjs +9 -9
- package/hooks/kiro/agentspawn.mjs +5 -5
- package/hooks/kiro/posttooluse.mjs +2 -2
- package/hooks/kiro/userpromptsubmit.mjs +1 -1
- package/hooks/posttooluse.mjs +45 -0
- package/hooks/precompact.mjs +17 -0
- package/hooks/pretooluse.mjs +23 -0
- package/hooks/routing-block.mjs +0 -12
- package/hooks/run-hook.mjs +16 -3
- package/hooks/session-db.bundle.mjs +27 -18
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +101 -64
- package/hooks/sessionstart.mjs +51 -2
- package/hooks/vscode-copilot/posttooluse.mjs +1 -1
- package/hooks/vscode-copilot/precompact.mjs +3 -2
- package/hooks/vscode-copilot/sessionstart.mjs +9 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +14 -8
- package/server.bundle.mjs +349 -147
- package/skills/UPSTREAM-CREDITS.md +0 -51
- package/skills/context-mode-ops/SKILL.md +0 -299
- package/skills/context-mode-ops/agent-teams.md +0 -198
- package/skills/context-mode-ops/communication.md +0 -224
- package/skills/context-mode-ops/marketing.md +0 -124
- package/skills/context-mode-ops/release.md +0 -214
- package/skills/context-mode-ops/review-pr.md +0 -269
- package/skills/context-mode-ops/tdd.md +0 -329
- package/skills/context-mode-ops/triage-issue.md +0 -266
- package/skills/context-mode-ops/validation.md +0 -307
- package/skills/diagnose/SKILL.md +0 -122
- package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
- package/skills/grill-me/SKILL.md +0 -15
- package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
- package/skills/grill-with-docs/SKILL.md +0 -93
- package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
- package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
- package/skills/improve-codebase-architecture/SKILL.md +0 -76
- package/skills/tdd/SKILL.md +0 -114
- package/skills/tdd/deep-modules.md +0 -33
- package/skills/tdd/interface-design.md +0 -31
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/refactoring.md +0 -10
- package/skills/tdd/tests.md +0 -61
package/bin/statusline.mjs
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* context-mode status line — Claude Code statusLine integration.
|
|
4
4
|
*
|
|
5
|
-
* Reads
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Reads stats DIRECTLY from SessionDB (`session_events` + `session_resume`),
|
|
6
|
+
* mirroring the `ctx_stats` MCP handler at src/server.ts:2807-2891 so the
|
|
7
|
+
* statusline and ctx_stats never drift. The legacy per-PID sidecar JSON
|
|
8
|
+
* (`stats-pid-*.json`) is no longer the source of truth — sidecars were
|
|
9
|
+
* eventually-consistent (500ms+30s throttles) and PID-scoped (multiple
|
|
10
|
+
* Claude sessions colliding on the same shell ppid).
|
|
8
11
|
*
|
|
9
12
|
* Discipline (Datadog / Stripe / Vercel pattern):
|
|
10
13
|
* - "context-mode" full brand label, never abbreviated
|
|
@@ -13,32 +16,46 @@
|
|
|
13
16
|
* - No counts (calls / tokens / events) — only $ and % pass the
|
|
14
17
|
* value-per-pixel test
|
|
15
18
|
*
|
|
16
|
-
* Wire it up in ~/.claude/settings.json
|
|
17
|
-
* forwarder so users don't have to know the absolute install path):
|
|
19
|
+
* Wire it up in ~/.claude/settings.json:
|
|
18
20
|
* {
|
|
19
21
|
* "statusLine": {
|
|
20
22
|
* "type": "command",
|
|
21
23
|
* "command": "context-mode statusline"
|
|
22
24
|
* }
|
|
23
25
|
* }
|
|
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
26
|
*/
|
|
28
27
|
|
|
29
|
-
import { existsSync, readFileSync
|
|
30
|
-
import { join } from "node:path";
|
|
28
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
29
|
+
import { join, dirname, resolve } from "node:path";
|
|
30
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
31
31
|
import { homedir } from "node:os";
|
|
32
32
|
import { execFileSync } from "node:child_process";
|
|
33
33
|
|
|
34
|
-
// ──
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
34
|
+
// ── Analytics import — resolved relative to this script ─────────────────
|
|
35
|
+
// statusline.mjs ships in `bin/`; the compiled analytics module lives in
|
|
36
|
+
// `build/session/analytics.js`. Import lazily so a missing build doesn't
|
|
37
|
+
// crash the renderer — degrade to the substantiated headline instead.
|
|
38
|
+
//
|
|
39
|
+
// The dynamic import target MUST be a `file://` URL on Windows. Node's
|
|
40
|
+
// ESM loader rejects absolute drive-letter paths (`C:\...`) with
|
|
41
|
+
// ERR_UNSUPPORTED_ESM_URL_SCHEME — which the catch below silently
|
|
42
|
+
// swallows, leaving `_analytics = null` and rendering the empty-state
|
|
43
|
+
// headline forever. Convert to a file URL so Windows accepts it.
|
|
44
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
45
|
+
const __dirname = dirname(__filename);
|
|
46
|
+
const ANALYTICS_PATH = resolve(__dirname, "..", "build", "session", "analytics.js");
|
|
47
|
+
const ANALYTICS_URL = pathToFileURL(ANALYTICS_PATH).href;
|
|
48
|
+
|
|
49
|
+
let _analytics = null;
|
|
50
|
+
async function loadAnalytics() {
|
|
51
|
+
if (_analytics) return _analytics;
|
|
52
|
+
try {
|
|
53
|
+
_analytics = await import(ANALYTICS_URL);
|
|
54
|
+
} catch {
|
|
55
|
+
_analytics = null;
|
|
56
|
+
}
|
|
57
|
+
return _analytics;
|
|
58
|
+
}
|
|
42
59
|
|
|
43
60
|
// Test seams — keep production behaviour identical when env vars unset.
|
|
44
61
|
// CTX_TEST_PLATFORM — override process.platform for cross-OS resolver tests
|
|
@@ -69,7 +86,7 @@ const yellow = (t) => ansi("33", t); // degraded dot
|
|
|
69
86
|
const red = (t) => ansi("31", t); // stale dot
|
|
70
87
|
const SEP = dim("·");
|
|
71
88
|
|
|
72
|
-
// ──
|
|
89
|
+
// ── Stdin drain ─────────────────────────────────────────────────────────
|
|
73
90
|
function readStdinJson() {
|
|
74
91
|
try {
|
|
75
92
|
const raw = readFileSync(0, "utf-8");
|
|
@@ -100,7 +117,7 @@ function resolveSessionDir() {
|
|
|
100
117
|
* - win32: degraded — process.ppid only, with a one-shot stderr warning
|
|
101
118
|
*
|
|
102
119
|
* Without this walk, multiple concurrent Claude sessions all see the same
|
|
103
|
-
* shell ppid and collide on
|
|
120
|
+
* shell ppid and collide on per-PID stats lookup.
|
|
104
121
|
*/
|
|
105
122
|
function findClaudePid() {
|
|
106
123
|
const plat = platform();
|
|
@@ -137,7 +154,6 @@ function findClaudePidDarwin() {
|
|
|
137
154
|
let pid = process.ppid;
|
|
138
155
|
for (let i = 0; i < 8 && pid && pid > 1; i++) {
|
|
139
156
|
try {
|
|
140
|
-
// `ps -o ppid=,comm= -p <pid>` → " 12345 /path/to/claude"
|
|
141
157
|
const out = execFileSync(
|
|
142
158
|
"ps",
|
|
143
159
|
["-o", "ppid=,comm=", "-p", String(pid)],
|
|
@@ -148,7 +164,6 @@ function findClaudePidDarwin() {
|
|
|
148
164
|
if (!m) return process.ppid;
|
|
149
165
|
const parentPid = Number(m[1]);
|
|
150
166
|
const comm = m[2].trim();
|
|
151
|
-
// comm may be a path; check basename for claude
|
|
152
167
|
const base = comm.split("/").pop() || comm;
|
|
153
168
|
if (/claude/i.test(base)) return pid;
|
|
154
169
|
pid = parentPid;
|
|
@@ -164,158 +179,160 @@ function resolveSessionId() {
|
|
|
164
179
|
return `pid-${findClaudePid()}`;
|
|
165
180
|
}
|
|
166
181
|
|
|
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
182
|
// ── Formatters ───────────────────────────────────────────────────────────
|
|
220
183
|
function fmtUsd(n) {
|
|
221
184
|
const safe = Number.isFinite(n) && n >= 0 ? n : 0;
|
|
222
185
|
if (safe >= 100) return `$${safe.toFixed(0)}`;
|
|
223
|
-
if (safe >= 10) return `$${safe.toFixed(2)}`;
|
|
224
186
|
return `$${safe.toFixed(2)}`;
|
|
225
187
|
}
|
|
226
188
|
|
|
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
189
|
// ── Status dot — the ONE accent ──────────────────────────────────────────
|
|
238
|
-
function statusDot(pct
|
|
239
|
-
if (isStale) return red("●");
|
|
190
|
+
function statusDot(pct) {
|
|
240
191
|
if (pct >= 50) return green("●");
|
|
241
192
|
if (pct >= 1) return yellow("●");
|
|
242
193
|
return green("●");
|
|
243
194
|
}
|
|
244
195
|
|
|
245
196
|
// ── Main render ──────────────────────────────────────────────────────────
|
|
246
|
-
function main() {
|
|
197
|
+
async function main() {
|
|
247
198
|
readStdinJson(); // drain stdin even if unused, keeps Claude Code happy
|
|
248
|
-
const
|
|
199
|
+
const sessionsDir = resolveSessionDir();
|
|
249
200
|
const sessionId = resolveSessionId();
|
|
250
|
-
const statsFile = findStatsFile(sessionDir, sessionId);
|
|
251
201
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
//
|
|
255
|
-
if (!
|
|
202
|
+
const analytics = await loadAnalytics();
|
|
203
|
+
|
|
204
|
+
// BRAND-NEW / build missing — substantiated headline only
|
|
205
|
+
if (!analytics) {
|
|
256
206
|
process.stdout.write(
|
|
257
207
|
`${brand("context-mode")} ${green("●")} ${dim("saves ~98% of context window")}`,
|
|
258
208
|
);
|
|
259
209
|
return;
|
|
260
210
|
}
|
|
261
211
|
|
|
262
|
-
const
|
|
263
|
-
|
|
212
|
+
const {
|
|
213
|
+
getRealBytesStats,
|
|
214
|
+
getMultiAdapterLifetimeStats,
|
|
215
|
+
OPUS_INPUT_PRICE_PER_TOKEN,
|
|
216
|
+
} = analytics;
|
|
217
|
+
|
|
218
|
+
// Sessions dir doesn't exist yet — first ever launch
|
|
219
|
+
if (!existsSync(sessionsDir)) {
|
|
264
220
|
process.stdout.write(
|
|
265
221
|
`${brand("context-mode")} ${green("●")} ${dim("saves ~98% of context window")}`,
|
|
266
222
|
);
|
|
267
223
|
return;
|
|
268
224
|
}
|
|
269
225
|
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
226
|
+
// Lifetime real-bytes across this adapter's sessions dir.
|
|
227
|
+
// Mirrors src/server.ts:2860 — the same call ctx_stats uses.
|
|
228
|
+
let lifetime;
|
|
229
|
+
try {
|
|
230
|
+
lifetime = getRealBytesStats({ sessionsDir });
|
|
231
|
+
} catch {
|
|
232
|
+
lifetime = null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Per-conversation real-bytes for the session $ KPI.
|
|
236
|
+
// Statusline doesn't know the worktree hash, so scan every db in the
|
|
237
|
+
// dir and let getRealBytesStats filter by sessionId.
|
|
238
|
+
let conversation;
|
|
239
|
+
try {
|
|
240
|
+
conversation = getRealBytesStats({ sessionsDir, sessionId });
|
|
241
|
+
} catch {
|
|
242
|
+
conversation = null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Cross-adapter lifetime — drives the "across N tools" headline when
|
|
246
|
+
// 2+ real adapters are present. Mirrors src/server.ts:2840.
|
|
247
|
+
let multi;
|
|
248
|
+
try {
|
|
249
|
+
multi = getMultiAdapterLifetimeStats();
|
|
250
|
+
} catch {
|
|
251
|
+
multi = null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const PRICE = OPUS_INPUT_PRICE_PER_TOKEN ?? (15 / 1_000_000);
|
|
255
|
+
const lifetimeTokens = lifetime?.totalSavedTokens ?? 0;
|
|
256
|
+
const sessionTokens = conversation?.totalSavedTokens ?? 0;
|
|
257
|
+
const lifetimeUsd = lifetimeTokens * PRICE;
|
|
258
|
+
const sessionUsd = sessionTokens * PRICE;
|
|
259
|
+
|
|
260
|
+
// Reduction % — bytes avoided + snapshot bytes vs returned bytes.
|
|
261
|
+
// Mirrors persistStats() math in src/server.ts:565-568.
|
|
262
|
+
const totalReturned = lifetime?.bytesReturned ?? 0;
|
|
263
|
+
const totalKept =
|
|
264
|
+
(lifetime?.bytesAvoided ?? 0)
|
|
265
|
+
+ (lifetime?.snapshotBytes ?? 0)
|
|
266
|
+
+ (lifetime?.eventDataBytes ?? 0);
|
|
267
|
+
const totalProcessed = totalKept + totalReturned;
|
|
268
|
+
const pct = totalProcessed > 0
|
|
269
|
+
? Math.round((totalKept / totalProcessed) * 100)
|
|
270
|
+
: 0;
|
|
271
|
+
|
|
272
|
+
const dot = statusDot(pct);
|
|
273
|
+
|
|
274
|
+
// Multi-adapter aggregation. Real adapters = those passing the isReal
|
|
275
|
+
// filter (>=100 events, >=5 distinct projects, recent activity, avg
|
|
276
|
+
// bytes >= 50). When 2+ real adapters exist, surface a cross-tool $.
|
|
277
|
+
// multi.totalBytes is dataBytes + rescueBytes, NOT bytes-avoided — so
|
|
278
|
+
// it's a different (and typically smaller) lens than getRealBytesStats.
|
|
279
|
+
// Render the multi $ alongside lifetime $ rather than instead of it.
|
|
280
|
+
const realAdapters = (multi?.perAdapter ?? []).filter((a) => a?.isReal);
|
|
281
|
+
const multiTotalTokens = (multi?.totalBytes ?? 0) / 4;
|
|
282
|
+
const multiUsd = multiTotalTokens * PRICE;
|
|
283
|
+
const showMultiAdapter = realAdapters.length >= 2 && multiUsd > 0;
|
|
284
|
+
|
|
285
|
+
// BRAND-NEW: no local SessionDB data at all → headline.
|
|
286
|
+
// Multi-adapter alone (without local data) means another tool has
|
|
287
|
+
// history but THIS Claude session is fresh — still show headline,
|
|
288
|
+
// not someone else's lifetime $, to avoid surprising users with a
|
|
289
|
+
// number they can't trace to their current adapter.
|
|
290
|
+
if (lifetimeTokens === 0 && sessionTokens === 0) {
|
|
274
291
|
process.stdout.write(
|
|
275
|
-
`${brand("context-mode")} ${
|
|
292
|
+
`${brand("context-mode")} ${green("●")} ${dim("saves ~98% of context window")}`,
|
|
276
293
|
);
|
|
277
294
|
return;
|
|
278
295
|
}
|
|
279
296
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
);
|
|
297
|
+
// FRESH session, no session $ yet — lead with persistence value.
|
|
298
|
+
if (sessionUsd === 0 && lifetimeUsd > 0) {
|
|
299
|
+
const blocks = [
|
|
300
|
+
`${bold(fmtUsd(lifetimeUsd))} ${dim("saved across sessions")}`,
|
|
301
|
+
];
|
|
302
|
+
if (showMultiAdapter) {
|
|
303
|
+
blocks.push(`${bold(fmtUsd(multiUsd))} ${dim(`across ${realAdapters.length} tools`)}`);
|
|
298
304
|
}
|
|
305
|
+
blocks.push(dim("preserved across compact, restart & upgrade"));
|
|
306
|
+
process.stdout.write(
|
|
307
|
+
`${brand("context-mode")} ${dot} ${blocks.join(` ${SEP} `)}`,
|
|
308
|
+
);
|
|
299
309
|
return;
|
|
300
310
|
}
|
|
301
311
|
|
|
302
|
-
// ACTIVE
|
|
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".
|
|
312
|
+
// ACTIVE: session $ · lifetime $ · [multi $] · % efficient
|
|
307
313
|
const valueBlocks = [
|
|
308
314
|
`${bold(fmtUsd(sessionUsd))} ${dim("saved this session")}`,
|
|
309
315
|
];
|
|
310
316
|
if (lifetimeUsd > 0) {
|
|
311
317
|
valueBlocks.push(`${bold(fmtUsd(lifetimeUsd))} ${dim("saved across sessions")}`);
|
|
312
318
|
}
|
|
313
|
-
|
|
314
|
-
|
|
319
|
+
if (showMultiAdapter) {
|
|
320
|
+
valueBlocks.push(`${bold(fmtUsd(multiUsd))} ${dim(`across ${realAdapters.length} tools`)}`);
|
|
321
|
+
}
|
|
322
|
+
if (pct > 0) {
|
|
323
|
+
valueBlocks.push(`${bold(`${pct}%`)} ${dim("efficient")}`);
|
|
324
|
+
}
|
|
315
325
|
|
|
316
326
|
const head = `${brand("context-mode")} ${dot} `;
|
|
317
327
|
const tail = valueBlocks.join(` ${SEP} `);
|
|
318
328
|
process.stdout.write(head + tail);
|
|
319
329
|
}
|
|
320
330
|
|
|
321
|
-
main()
|
|
331
|
+
main().catch(() => {
|
|
332
|
+
// Last-resort fallback — a thrown error must never produce a blank statusline.
|
|
333
|
+
try {
|
|
334
|
+
process.stdout.write(
|
|
335
|
+
`${brand("context-mode")} ${green("●")} ${dim("saves ~98% of context window")}`,
|
|
336
|
+
);
|
|
337
|
+
} catch { /* ignore */ }
|
|
338
|
+
});
|
package/build/adapters/base.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BaseAdapter — shared implementation for methods identical across all adapters.
|
|
3
3
|
*
|
|
4
|
-
* Eliminates ~288 lines of duplication across 12 adapters.
|
|
5
4
|
* Each concrete adapter extends this and provides platform-specific logic.
|
|
6
5
|
*
|
|
7
6
|
* Shared methods:
|
|
8
7
|
* - getSessionDir() — builds session dir from sessionDirSegments
|
|
9
|
-
* - getSessionDBPath() — SHA-256 hash of projectDir → .db file
|
|
10
|
-
* - getSessionEventsPath()— SHA-256 hash of projectDir → -events.md file
|
|
11
8
|
* - backupSettings() — copies settings file to .bak
|
|
12
9
|
*
|
|
13
10
|
* Adapters with custom logic override the relevant method:
|
|
@@ -15,13 +12,19 @@
|
|
|
15
12
|
* - opencode: overrides getSessionDir (XDG_CONFIG_HOME / APPDATA)
|
|
16
13
|
* and backupSettings (calls checkPluginRegistration first)
|
|
17
14
|
* - openclaw: overrides backupSettings (searches 3 config paths)
|
|
15
|
+
*
|
|
16
|
+
* NOTE — C2 narrowing (2026-05): `getSessionDBPath` and `getSessionEventsPath`
|
|
17
|
+
* were removed. Both were SHALLOW pure derivatives of `getSessionDir() +
|
|
18
|
+
* projectDir` (interface complexity == implementation complexity). All
|
|
19
|
+
* adapter-storage path computation now flows through ONE site:
|
|
20
|
+
* `resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() })`
|
|
21
|
+
* in `src/session/db.ts`. Adapters expose only `getSessionDir()` for
|
|
22
|
+
* storage-related path concerns.
|
|
18
23
|
*/
|
|
19
24
|
export declare abstract class BaseAdapter {
|
|
20
25
|
protected readonly sessionDirSegments: string[];
|
|
21
26
|
constructor(sessionDirSegments: string[]);
|
|
22
27
|
getSessionDir(): string;
|
|
23
|
-
getSessionDBPath(projectDir: string): string;
|
|
24
|
-
getSessionEventsPath(projectDir: string): string;
|
|
25
28
|
/**
|
|
26
29
|
* Default: build config dir from sessionDirSegments rooted at $HOME.
|
|
27
30
|
*
|
package/build/adapters/base.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BaseAdapter — shared implementation for methods identical across all adapters.
|
|
3
3
|
*
|
|
4
|
-
* Eliminates ~288 lines of duplication across 12 adapters.
|
|
5
4
|
* Each concrete adapter extends this and provides platform-specific logic.
|
|
6
5
|
*
|
|
7
6
|
* Shared methods:
|
|
8
7
|
* - getSessionDir() — builds session dir from sessionDirSegments
|
|
9
|
-
* - getSessionDBPath() — SHA-256 hash of projectDir → .db file
|
|
10
|
-
* - getSessionEventsPath()— SHA-256 hash of projectDir → -events.md file
|
|
11
8
|
* - backupSettings() — copies settings file to .bak
|
|
12
9
|
*
|
|
13
10
|
* Adapters with custom logic override the relevant method:
|
|
@@ -15,8 +12,15 @@
|
|
|
15
12
|
* - opencode: overrides getSessionDir (XDG_CONFIG_HOME / APPDATA)
|
|
16
13
|
* and backupSettings (calls checkPluginRegistration first)
|
|
17
14
|
* - openclaw: overrides backupSettings (searches 3 config paths)
|
|
15
|
+
*
|
|
16
|
+
* NOTE — C2 narrowing (2026-05): `getSessionDBPath` and `getSessionEventsPath`
|
|
17
|
+
* were removed. Both were SHALLOW pure derivatives of `getSessionDir() +
|
|
18
|
+
* projectDir` (interface complexity == implementation complexity). All
|
|
19
|
+
* adapter-storage path computation now flows through ONE site:
|
|
20
|
+
* `resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() })`
|
|
21
|
+
* in `src/session/db.ts`. Adapters expose only `getSessionDir()` for
|
|
22
|
+
* storage-related path concerns.
|
|
18
23
|
*/
|
|
19
|
-
import { createHash } from "node:crypto";
|
|
20
24
|
import { join } from "node:path";
|
|
21
25
|
import { accessSync, copyFileSync, constants, mkdirSync } from "node:fs";
|
|
22
26
|
import { homedir } from "node:os";
|
|
@@ -30,20 +34,6 @@ export class BaseAdapter {
|
|
|
30
34
|
mkdirSync(dir, { recursive: true });
|
|
31
35
|
return dir;
|
|
32
36
|
}
|
|
33
|
-
getSessionDBPath(projectDir) {
|
|
34
|
-
const hash = createHash("sha256")
|
|
35
|
-
.update(projectDir)
|
|
36
|
-
.digest("hex")
|
|
37
|
-
.slice(0, 16);
|
|
38
|
-
return join(this.getSessionDir(), `${hash}.db`);
|
|
39
|
-
}
|
|
40
|
-
getSessionEventsPath(projectDir) {
|
|
41
|
-
const hash = createHash("sha256")
|
|
42
|
-
.update(projectDir)
|
|
43
|
-
.digest("hex")
|
|
44
|
-
.slice(0, 16);
|
|
45
|
-
return join(this.getSessionDir(), `${hash}-events.md`);
|
|
46
|
-
}
|
|
47
37
|
/**
|
|
48
38
|
* Default: build config dir from sessionDirSegments rooted at $HOME.
|
|
49
39
|
*
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Claude Code hook specifics:
|
|
8
8
|
* - Session ID: transcript_path UUID > session_id > CLAUDE_SESSION_ID > ppid
|
|
9
|
-
* - Config: ~/.claude
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
9
|
+
* - Config root: $CLAUDE_CONFIG_DIR (when set) or ~/.claude
|
|
10
|
+
* - Settings: <configDir>/settings.json
|
|
11
|
+
* - Session dir: <configDir>/context-mode/sessions/
|
|
12
|
+
* - Plugin registry: <configDir>/plugins/installed_plugins.json
|
|
12
13
|
*/
|
|
13
14
|
import { ClaudeCodeBaseAdapter, type ClaudeCodeWireInput } from "../claude-code-base.js";
|
|
14
15
|
import type { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, HookRegistration } from "../types.js";
|
|
@@ -18,6 +19,26 @@ export declare class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter implements
|
|
|
18
19
|
readonly paradigm: HookParadigm;
|
|
19
20
|
protected readonly projectDirEnvVar = "CLAUDE_PROJECT_DIR";
|
|
20
21
|
readonly capabilities: PlatformCapabilities;
|
|
22
|
+
/**
|
|
23
|
+
* Honor `CLAUDE_CONFIG_DIR` (the canonical Claude Code config root) before
|
|
24
|
+
* falling back to `~/.claude`. Mirrors the contract that
|
|
25
|
+
* `hooks/session-helpers.mjs::resolveConfigDir` already follows — including
|
|
26
|
+
* tilde expansion for shells that pass `~/foo` through unchanged — so server
|
|
27
|
+
* and hooks agree on where session-scoped state lives. See issue #453.
|
|
28
|
+
*
|
|
29
|
+
* Tilde regex `/^~[/\\]?/` only handles the current-user form (`~`, `~/`,
|
|
30
|
+
* `~\`); `~user/` is NOT expanded to a per-user homedir (matches
|
|
31
|
+
* `resolveConfigDir`). Non-tilde values are run through `resolve()` to
|
|
32
|
+
* normalize relative paths to absolute against cwd; the hook helper
|
|
33
|
+
* intentionally leaves them raw, but the adapter contract guarantees an
|
|
34
|
+
* absolute path (BaseAdapter.getConfigDir docstring).
|
|
35
|
+
*
|
|
36
|
+
* Issue #460 round-3: routed through the canonical
|
|
37
|
+
* `resolveClaudeConfigDir` util so server, CLI, security, and adapter
|
|
38
|
+
* agree byte-for-byte (incl. empty/whitespace-only env fallback).
|
|
39
|
+
*/
|
|
40
|
+
getConfigDir(_projectDir?: string): string;
|
|
41
|
+
getSessionDir(): string;
|
|
21
42
|
getSettingsPath(): string;
|
|
22
43
|
generateHookConfig(pluginRoot: string): HookRegistration;
|
|
23
44
|
readSettings(): Record<string, unknown> | null;
|
|
@@ -6,14 +6,16 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Claude Code hook specifics:
|
|
8
8
|
* - Session ID: transcript_path UUID > session_id > CLAUDE_SESSION_ID > ppid
|
|
9
|
-
* - Config: ~/.claude
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
9
|
+
* - Config root: $CLAUDE_CONFIG_DIR (when set) or ~/.claude
|
|
10
|
+
* - Settings: <configDir>/settings.json
|
|
11
|
+
* - Session dir: <configDir>/context-mode/sessions/
|
|
12
|
+
* - Plugin registry: <configDir>/plugins/installed_plugins.json
|
|
12
13
|
*/
|
|
13
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync, chmodSync, accessSync, constants, } from "node:fs";
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, chmodSync, accessSync, mkdirSync, constants, } from "node:fs";
|
|
14
15
|
import { resolve, join } from "node:path";
|
|
15
16
|
import { homedir } from "node:os";
|
|
16
17
|
import { ClaudeCodeBaseAdapter } from "../claude-code-base.js";
|
|
18
|
+
import { resolveClaudeConfigDir } from "../../util/claude-config.js";
|
|
17
19
|
import { HOOK_TYPES, HOOK_SCRIPTS, REQUIRED_HOOKS, PRE_TOOL_USE_MATCHER_PATTERN, isContextModeHook, isAnyContextModeHook, extractHookScriptPath, buildHookCommand, } from "./hooks.js";
|
|
18
20
|
// ─────────────────────────────────────────────────────────
|
|
19
21
|
// Adapter implementation
|
|
@@ -35,8 +37,34 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
35
37
|
canInjectSessionContext: true,
|
|
36
38
|
};
|
|
37
39
|
// ── Configuration ──────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Honor `CLAUDE_CONFIG_DIR` (the canonical Claude Code config root) before
|
|
42
|
+
* falling back to `~/.claude`. Mirrors the contract that
|
|
43
|
+
* `hooks/session-helpers.mjs::resolveConfigDir` already follows — including
|
|
44
|
+
* tilde expansion for shells that pass `~/foo` through unchanged — so server
|
|
45
|
+
* and hooks agree on where session-scoped state lives. See issue #453.
|
|
46
|
+
*
|
|
47
|
+
* Tilde regex `/^~[/\\]?/` only handles the current-user form (`~`, `~/`,
|
|
48
|
+
* `~\`); `~user/` is NOT expanded to a per-user homedir (matches
|
|
49
|
+
* `resolveConfigDir`). Non-tilde values are run through `resolve()` to
|
|
50
|
+
* normalize relative paths to absolute against cwd; the hook helper
|
|
51
|
+
* intentionally leaves them raw, but the adapter contract guarantees an
|
|
52
|
+
* absolute path (BaseAdapter.getConfigDir docstring).
|
|
53
|
+
*
|
|
54
|
+
* Issue #460 round-3: routed through the canonical
|
|
55
|
+
* `resolveClaudeConfigDir` util so server, CLI, security, and adapter
|
|
56
|
+
* agree byte-for-byte (incl. empty/whitespace-only env fallback).
|
|
57
|
+
*/
|
|
58
|
+
getConfigDir(_projectDir) {
|
|
59
|
+
return resolveClaudeConfigDir();
|
|
60
|
+
}
|
|
61
|
+
getSessionDir() {
|
|
62
|
+
const dir = join(this.getConfigDir(), "context-mode", "sessions");
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
return dir;
|
|
65
|
+
}
|
|
38
66
|
getSettingsPath() {
|
|
39
|
-
return
|
|
67
|
+
return join(this.getConfigDir(), "settings.json");
|
|
40
68
|
}
|
|
41
69
|
generateHookConfig(pluginRoot) {
|
|
42
70
|
const preToolUseCommand = `node ${pluginRoot}/hooks/pretooluse.mjs`;
|
|
@@ -121,7 +149,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
121
149
|
results.push({
|
|
122
150
|
check: "PreToolUse hook",
|
|
123
151
|
status: "fail",
|
|
124
|
-
message:
|
|
152
|
+
message: `Could not read ${this.getSettingsPath()}`,
|
|
125
153
|
fix: "context-mode upgrade",
|
|
126
154
|
});
|
|
127
155
|
return results;
|
|
@@ -221,7 +249,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
221
249
|
getInstalledVersion() {
|
|
222
250
|
// Primary: read from installed_plugins.json
|
|
223
251
|
try {
|
|
224
|
-
const ipPath =
|
|
252
|
+
const ipPath = join(this.getConfigDir(), "plugins", "installed_plugins.json");
|
|
225
253
|
const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8"));
|
|
226
254
|
const plugins = ipRaw.plugins ?? {};
|
|
227
255
|
for (const [key, entries] of Object.entries(plugins)) {
|
|
@@ -236,11 +264,16 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
236
264
|
catch {
|
|
237
265
|
/* fallback below */
|
|
238
266
|
}
|
|
239
|
-
// Fallback: scan common plugin cache locations
|
|
240
|
-
|
|
267
|
+
// Fallback: scan common plugin cache locations.
|
|
268
|
+
// `resolveClaudeConfigDir` honors $CLAUDE_CONFIG_DIR; the literal
|
|
269
|
+
// `~/.claude` is also retained as a hard floor so environments that
|
|
270
|
+
// misconfigure the env still find the canonical dir if it exists.
|
|
271
|
+
const bases = Array.from(new Set([
|
|
272
|
+
this.getConfigDir(),
|
|
273
|
+
resolveClaudeConfigDir(),
|
|
241
274
|
resolve(homedir(), ".claude"),
|
|
242
275
|
resolve(homedir(), ".config", "claude"),
|
|
243
|
-
];
|
|
276
|
+
]));
|
|
244
277
|
for (const base of bases) {
|
|
245
278
|
const cacheDir = resolve(base, "plugins", "cache", "context-mode", "context-mode");
|
|
246
279
|
try {
|
|
@@ -421,7 +454,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
421
454
|
}
|
|
422
455
|
updatePluginRegistry(pluginRoot, version) {
|
|
423
456
|
try {
|
|
424
|
-
const ipPath =
|
|
457
|
+
const ipPath = join(this.getConfigDir(), "plugins", "installed_plugins.json");
|
|
425
458
|
const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8"));
|
|
426
459
|
for (const [key, entries] of Object.entries(ipRaw.plugins || {})) {
|
|
427
460
|
if (!key.toLowerCase().includes("context-mode"))
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* adapters/codex/hooks — Codex CLI hook definitions.
|
|
3
3
|
*
|
|
4
|
-
* Codex CLI hooks
|
|
5
|
-
*
|
|
4
|
+
* Codex CLI hooks run behind the current `hooks` feature flag surface.
|
|
5
|
+
* Prefer `[features].hooks`; the legacy `[features].codex_hooks` alias is still
|
|
6
|
+
* accepted in current Codex builds.
|
|
7
|
+
* 6 hook events: PreToolUse, PostToolUse, PreCompact, SessionStart,
|
|
8
|
+
* UserPromptSubmit, Stop. PreCompact is runtime-gated on Codex builds that emit
|
|
9
|
+
* the event.
|
|
6
10
|
* Same JSON stdin/stdout wire protocol as Claude Code.
|
|
7
11
|
*
|
|
8
|
-
* Config: ~/.codex/hooks.json
|
|
9
|
-
* MCP: full support via [mcp_servers] in
|
|
12
|
+
* Config: $CODEX_HOME/hooks.json or ~/.codex/hooks.json.
|
|
13
|
+
* MCP: full support via [mcp_servers] in $CODEX_HOME/config.toml.
|
|
10
14
|
*
|
|
11
15
|
* Known limitations:
|
|
12
16
|
* - PreToolUse: deny works, updatedInput not yet supported (openai/codex#18491)
|
|
13
17
|
* - PostToolUse: updatedMCPToolOutput parsed but logged as unsupported
|
|
14
18
|
* - PostToolUse does not fire on failing Bash calls (upstream bug)
|
|
15
19
|
*/
|
|
16
|
-
/** Codex CLI hook types — mirrors Claude Code's
|
|
20
|
+
/** Codex CLI hook types — mirrors Claude Code's continuity events. */
|
|
17
21
|
export declare const HOOK_TYPES: {
|
|
18
22
|
readonly PRE_TOOL_USE: "PreToolUse";
|
|
19
23
|
readonly POST_TOOL_USE: "PostToolUse";
|
|
24
|
+
readonly PRE_COMPACT: "PreCompact";
|
|
20
25
|
readonly SESSION_START: "SessionStart";
|
|
21
26
|
readonly USER_PROMPT_SUBMIT: "UserPromptSubmit";
|
|
22
27
|
readonly STOP: "Stop";
|