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.
Files changed (151) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/index.ts +3 -2
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +152 -34
  7. package/bin/statusline.mjs +144 -127
  8. package/build/adapters/base.d.ts +8 -5
  9. package/build/adapters/base.js +8 -18
  10. package/build/adapters/claude-code/index.d.ts +24 -3
  11. package/build/adapters/claude-code/index.js +44 -11
  12. package/build/adapters/codex/hooks.d.ts +10 -5
  13. package/build/adapters/codex/hooks.js +10 -5
  14. package/build/adapters/codex/index.d.ts +17 -5
  15. package/build/adapters/codex/index.js +337 -37
  16. package/build/adapters/codex/paths.d.ts +1 -0
  17. package/build/adapters/codex/paths.js +12 -0
  18. package/build/adapters/cursor/index.d.ts +6 -0
  19. package/build/adapters/cursor/index.js +83 -2
  20. package/build/adapters/detect.d.ts +1 -1
  21. package/build/adapters/detect.js +29 -6
  22. package/build/adapters/omp/index.d.ts +65 -0
  23. package/build/adapters/omp/index.js +182 -0
  24. package/build/adapters/omp/plugin.d.ts +75 -0
  25. package/build/adapters/omp/plugin.js +220 -0
  26. package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
  27. package/build/adapters/openclaw/mcp-tools.js +198 -0
  28. package/build/adapters/openclaw/plugin.d.ts +130 -0
  29. package/build/adapters/openclaw/plugin.js +629 -0
  30. package/build/adapters/openclaw/workspace-router.d.ts +29 -0
  31. package/build/adapters/openclaw/workspace-router.js +64 -0
  32. package/build/adapters/opencode/plugin.d.ts +145 -0
  33. package/build/adapters/opencode/plugin.js +457 -0
  34. package/build/adapters/pi/extension.d.ts +26 -0
  35. package/build/adapters/pi/extension.js +552 -0
  36. package/build/adapters/pi/index.d.ts +57 -0
  37. package/build/adapters/pi/index.js +173 -0
  38. package/build/adapters/pi/mcp-bridge.d.ts +113 -0
  39. package/build/adapters/pi/mcp-bridge.js +251 -0
  40. package/build/adapters/types.d.ts +11 -6
  41. package/build/cli.js +186 -170
  42. package/build/db-base.d.ts +15 -2
  43. package/build/db-base.js +50 -5
  44. package/build/executor.d.ts +2 -0
  45. package/build/executor.js +15 -2
  46. package/build/opencode-plugin.js +1 -1
  47. package/build/runPool.d.ts +36 -0
  48. package/build/runPool.js +51 -0
  49. package/build/runtime.js +64 -5
  50. package/build/search/auto-memory.js +6 -4
  51. package/build/security.js +30 -10
  52. package/build/server.d.ts +23 -1
  53. package/build/server.js +652 -174
  54. package/build/session/analytics.d.ts +404 -1
  55. package/build/session/analytics.js +1347 -42
  56. package/build/session/db.d.ts +114 -5
  57. package/build/session/db.js +275 -27
  58. package/build/session/event-emit.d.ts +48 -0
  59. package/build/session/event-emit.js +101 -0
  60. package/build/session/extract.d.ts +1 -0
  61. package/build/session/extract.js +79 -12
  62. package/build/session/purge.d.ts +111 -0
  63. package/build/session/purge.js +138 -0
  64. package/build/store.d.ts +7 -0
  65. package/build/store.js +69 -6
  66. package/build/util/claude-config.d.ts +26 -0
  67. package/build/util/claude-config.js +91 -0
  68. package/build/util/hook-config.d.ts +4 -0
  69. package/build/util/hook-config.js +39 -0
  70. package/cli.bundle.mjs +411 -208
  71. package/configs/antigravity/GEMINI.md +0 -3
  72. package/configs/claude-code/CLAUDE.md +1 -4
  73. package/configs/codex/AGENTS.md +1 -4
  74. package/configs/codex/config.toml +3 -0
  75. package/configs/codex/hooks.json +8 -0
  76. package/configs/cursor/context-mode.mdc +0 -3
  77. package/configs/gemini-cli/GEMINI.md +0 -3
  78. package/configs/jetbrains-copilot/copilot-instructions.md +0 -3
  79. package/configs/kilo/AGENTS.md +0 -3
  80. package/configs/kiro/KIRO.md +0 -3
  81. package/configs/omp/SYSTEM.md +85 -0
  82. package/configs/omp/mcp.json +7 -0
  83. package/configs/openclaw/AGENTS.md +0 -3
  84. package/configs/opencode/AGENTS.md +0 -3
  85. package/configs/pi/AGENTS.md +0 -3
  86. package/configs/qwen-code/QWEN.md +1 -4
  87. package/configs/vscode-copilot/copilot-instructions.md +0 -3
  88. package/configs/zed/AGENTS.md +0 -3
  89. package/hooks/codex/posttooluse.mjs +9 -2
  90. package/hooks/codex/precompact.mjs +69 -0
  91. package/hooks/codex/sessionstart.mjs +13 -9
  92. package/hooks/codex/stop.mjs +1 -2
  93. package/hooks/codex/userpromptsubmit.mjs +1 -2
  94. package/hooks/core/routing.mjs +237 -18
  95. package/hooks/cursor/afteragentresponse.mjs +1 -1
  96. package/hooks/cursor/hooks.json +31 -0
  97. package/hooks/cursor/posttooluse.mjs +1 -1
  98. package/hooks/cursor/sessionstart.mjs +5 -5
  99. package/hooks/cursor/stop.mjs +1 -1
  100. package/hooks/ensure-deps.mjs +12 -13
  101. package/hooks/gemini-cli/aftertool.mjs +1 -1
  102. package/hooks/gemini-cli/beforeagent.mjs +1 -1
  103. package/hooks/gemini-cli/precompress.mjs +3 -2
  104. package/hooks/gemini-cli/sessionstart.mjs +9 -9
  105. package/hooks/jetbrains-copilot/posttooluse.mjs +1 -1
  106. package/hooks/jetbrains-copilot/precompact.mjs +3 -2
  107. package/hooks/jetbrains-copilot/sessionstart.mjs +9 -9
  108. package/hooks/kiro/agentspawn.mjs +5 -5
  109. package/hooks/kiro/posttooluse.mjs +2 -2
  110. package/hooks/kiro/userpromptsubmit.mjs +1 -1
  111. package/hooks/posttooluse.mjs +45 -0
  112. package/hooks/precompact.mjs +17 -0
  113. package/hooks/pretooluse.mjs +23 -0
  114. package/hooks/routing-block.mjs +0 -12
  115. package/hooks/run-hook.mjs +16 -3
  116. package/hooks/session-db.bundle.mjs +27 -18
  117. package/hooks/session-extract.bundle.mjs +2 -2
  118. package/hooks/session-helpers.mjs +101 -64
  119. package/hooks/sessionstart.mjs +51 -2
  120. package/hooks/vscode-copilot/posttooluse.mjs +1 -1
  121. package/hooks/vscode-copilot/precompact.mjs +3 -2
  122. package/hooks/vscode-copilot/sessionstart.mjs +9 -9
  123. package/openclaw.plugin.json +1 -1
  124. package/package.json +14 -8
  125. package/server.bundle.mjs +349 -147
  126. package/skills/UPSTREAM-CREDITS.md +0 -51
  127. package/skills/context-mode-ops/SKILL.md +0 -299
  128. package/skills/context-mode-ops/agent-teams.md +0 -198
  129. package/skills/context-mode-ops/communication.md +0 -224
  130. package/skills/context-mode-ops/marketing.md +0 -124
  131. package/skills/context-mode-ops/release.md +0 -214
  132. package/skills/context-mode-ops/review-pr.md +0 -269
  133. package/skills/context-mode-ops/tdd.md +0 -329
  134. package/skills/context-mode-ops/triage-issue.md +0 -266
  135. package/skills/context-mode-ops/validation.md +0 -307
  136. package/skills/diagnose/SKILL.md +0 -122
  137. package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
  138. package/skills/grill-me/SKILL.md +0 -15
  139. package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
  140. package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
  141. package/skills/grill-with-docs/SKILL.md +0 -93
  142. package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
  143. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
  144. package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
  145. package/skills/improve-codebase-architecture/SKILL.md +0 -76
  146. package/skills/tdd/SKILL.md +0 -114
  147. package/skills/tdd/deep-modules.md +0 -33
  148. package/skills/tdd/interface-design.md +0 -31
  149. package/skills/tdd/mocking.md +0 -59
  150. package/skills/tdd/refactoring.md +0 -10
  151. package/skills/tdd/tests.md +0 -61
@@ -2,9 +2,12 @@
2
2
  /**
3
3
  * context-mode status line — Claude Code statusLine integration.
4
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).
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 (path-free — uses the bundled CLI
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, readdirSync, statSync } from "node:fs";
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
- // ── 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;
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
- // ── Stats file lookup ────────────────────────────────────────────────────
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 the fuzzy mtime fallback in findStatsFile.
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, isStale) {
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 sessionDir = resolveSessionDir();
199
+ const sessionsDir = resolveSessionDir();
249
200
  const sessionId = resolveSessionId();
250
- const statsFile = findStatsFile(sessionDir, sessionId);
251
201
 
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) {
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 stats = loadStats(statsFile);
263
- if (!stats) {
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
- // 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) {
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")} ${red("●")} ${dim("stale restart to resume saving")}`,
292
+ `${brand("context-mode")} ${green("●")} ${dim("saves ~98% of context window")}`,
276
293
  );
277
294
  return;
278
295
  }
279
296
 
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
- );
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 / 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".
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
- valueBlocks.push(`${bold(`${pct}%`)} ${dim("efficient")}`);
314
- valueBlocks.push(dim(uptime));
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
+ });
@@ -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
  *
@@ -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/settings.json
10
- * - Session dir: ~/.claude/context-mode/sessions/
11
- * - Plugin registry: ~/.claude/plugins/installed_plugins.json
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/settings.json
10
- * - Session dir: ~/.claude/context-mode/sessions/
11
- * - Plugin registry: ~/.claude/plugins/installed_plugins.json
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 resolve(homedir(), ".claude", "settings.json");
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: "Could not read ~/.claude/settings.json",
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 = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
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
- const bases = [
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 = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
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 are stable (codex_hooks Stage::Stable, default_enabled: true).
5
- * 5 hook events: PreToolUse, PostToolUse, SessionStart, UserPromptSubmit, Stop.
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 (JSON format, same schema as Claude Code)
9
- * MCP: full support via [mcp_servers] in ~/.codex/config.toml
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 5-event model. */
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";