context-mode 1.0.133 → 1.0.135
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/build/adapters/detect.d.ts +3 -1
- package/build/adapters/detect.js +7 -2
- package/build/adapters/pi/mcp-bridge.d.ts +8 -0
- package/build/adapters/pi/mcp-bridge.js +32 -0
- package/build/cli.js +17 -0
- package/build/runtime.js +8 -5
- package/build/server.d.ts +17 -0
- package/build/server.js +62 -4
- package/build/session/analytics.d.ts +18 -13
- package/build/session/analytics.js +131 -8
- package/build/util/claude-config.d.ts +12 -6
- package/build/util/claude-config.js +16 -23
- package/cli.bundle.mjs +136 -133
- package/hooks/codex/sessionstart.mjs +23 -1
- package/hooks/core/platform-detect.mjs +1 -1
- package/hooks/normalize-hooks.mjs +5 -2
- package/hooks/security.bundle.mjs +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/heal-installed-plugins.mjs +67 -0
- package/server.bundle.mjs +109 -108
- package/start.mjs +73 -11
|
@@ -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.135"
|
|
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.135",
|
|
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.135",
|
|
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.135",
|
|
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.135",
|
|
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",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* CLAUDE_PROJECT_DIR, CLAUDE_SESSION_ID | ~/.claude/
|
|
12
12
|
* - Gemini CLI: GEMINI_PROJECT_DIR (hooks), GEMINI_CLI (MCP) | ~/.gemini/
|
|
13
13
|
* - KiloCode: KILO, KILO_PID | ~/.config/kilo/
|
|
14
|
-
* - OpenCode:
|
|
14
|
+
* - OpenCode: OPENCODE_PROJECT_DIR, OPENCODE_CLIENT,
|
|
15
|
+
* OPENCODE_TERMINAL, OPENCODE, OPENCODE_PID |
|
|
16
|
+
* ~/.config/opencode/
|
|
15
17
|
* - OpenClaw: OPENCLAW_HOME, OPENCLAW_CLI | ~/.openclaw/
|
|
16
18
|
* - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
|
|
17
19
|
* - Cursor: CURSOR_TRACE_ID (MCP), CURSOR_CLI (terminal) | ~/.cursor/
|
package/build/adapters/detect.js
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* CLAUDE_PROJECT_DIR, CLAUDE_SESSION_ID | ~/.claude/
|
|
12
12
|
* - Gemini CLI: GEMINI_PROJECT_DIR (hooks), GEMINI_CLI (MCP) | ~/.gemini/
|
|
13
13
|
* - KiloCode: KILO, KILO_PID | ~/.config/kilo/
|
|
14
|
-
* - OpenCode:
|
|
14
|
+
* - OpenCode: OPENCODE_PROJECT_DIR, OPENCODE_CLIENT,
|
|
15
|
+
* OPENCODE_TERMINAL, OPENCODE, OPENCODE_PID |
|
|
16
|
+
* ~/.config/opencode/
|
|
15
17
|
* - OpenClaw: OPENCLAW_HOME, OPENCLAW_CLI | ~/.openclaw/
|
|
16
18
|
* - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
|
|
17
19
|
* - Cursor: CURSOR_TRACE_ID (MCP), CURSOR_CLI (terminal) | ~/.cursor/
|
|
@@ -109,12 +111,15 @@ const _PLATFORM_ENV_VARS_RAW = [
|
|
|
109
111
|
{ name: "KILO_PID", role: "identification" },
|
|
110
112
|
]],
|
|
111
113
|
// opencode — sst/opencode packages/opencode/src/index.ts:108-109 sets
|
|
112
|
-
// OPENCODE=1 + OPENCODE_PID=<pid> on
|
|
114
|
+
// OPENCODE=1 + OPENCODE_PID=<pid> on CLI invocations. OpenCode desktop
|
|
115
|
+
// shells also expose OPENCODE_CLIENT=desktop and OPENCODE_TERMINAL=1.
|
|
113
116
|
// OPENCODE_PROJECT_DIR is the documented workspace var (consumed by the
|
|
114
117
|
// legacy resolver cascade) — listed first so the workspace cascade picks
|
|
115
118
|
// it up under strict mode.
|
|
116
119
|
["opencode", [
|
|
117
120
|
{ name: "OPENCODE_PROJECT_DIR", role: "workspace" },
|
|
121
|
+
{ name: "OPENCODE_CLIENT", role: "identification" },
|
|
122
|
+
{ name: "OPENCODE_TERMINAL", role: "identification" },
|
|
118
123
|
{ name: "OPENCODE", role: "identification" },
|
|
119
124
|
{ name: "OPENCODE_PID", role: "identification" },
|
|
120
125
|
]],
|
|
@@ -85,6 +85,14 @@ export declare class MCPStdioClient {
|
|
|
85
85
|
initialize(): Promise<void>;
|
|
86
86
|
listTools(): Promise<MCPTool[]>;
|
|
87
87
|
callTool(name: string, args: unknown): Promise<MCPCallResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Respawn the MCP child after an exit (clean idle shutdown or crash).
|
|
90
|
+
* Resets state so a fresh `start()` + `initialize()` cycle runs, then
|
|
91
|
+
* the caller's pending request flows through the new child.
|
|
92
|
+
*
|
|
93
|
+
* Internal — exposed only via the public `callTool()` happy path.
|
|
94
|
+
*/
|
|
95
|
+
private respawn;
|
|
88
96
|
shutdown(): void;
|
|
89
97
|
}
|
|
90
98
|
/**
|
|
@@ -284,8 +284,40 @@ export class MCPStdioClient {
|
|
|
284
284
|
return Array.isArray(result.tools) ? result.tools : [];
|
|
285
285
|
}
|
|
286
286
|
async callTool(name, args) {
|
|
287
|
+
// Respawn-on-idle-exit (#583). The MCP server gained an idle
|
|
288
|
+
// self-shutdown in 1.0.132 (#565/#568, src/lifecycle.ts). When the
|
|
289
|
+
// Pi-spawned child exits cleanly after the idle window, Pi keeps the
|
|
290
|
+
// tool handles registered, but the bridge client is `exited=true`
|
|
291
|
+
// and every subsequent request would reject with
|
|
292
|
+
// "MCP server has exited" — leaving Pi's ctx_* tools permanently
|
|
293
|
+
// broken until the user restarts Pi.
|
|
294
|
+
//
|
|
295
|
+
// The structural fix is here, not in lifecycle.ts: the bridge owns
|
|
296
|
+
// the child lifecycle, so it transparently respawns + re-initialises
|
|
297
|
+
// the server on the next call. Restores parity with adapters whose
|
|
298
|
+
// host MCP client respawns on EOF (Claude Code, Codex, etc.).
|
|
299
|
+
if (this.exited)
|
|
300
|
+
await this.respawn();
|
|
287
301
|
return this.request("tools/call", { name, arguments: args ?? {} }, DEFAULT_CALL_TIMEOUT_MS);
|
|
288
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Respawn the MCP child after an exit (clean idle shutdown or crash).
|
|
305
|
+
* Resets state so a fresh `start()` + `initialize()` cycle runs, then
|
|
306
|
+
* the caller's pending request flows through the new child.
|
|
307
|
+
*
|
|
308
|
+
* Internal — exposed only via the public `callTool()` happy path.
|
|
309
|
+
*/
|
|
310
|
+
async respawn() {
|
|
311
|
+
// Drop the dead child handle and clear stream buffer so leftover
|
|
312
|
+
// bytes from the previous incarnation don't get parsed as JSON-RPC
|
|
313
|
+
// for the new one. Pending map is already cleared by onExit().
|
|
314
|
+
this.child = null;
|
|
315
|
+
this.buffer = "";
|
|
316
|
+
this.exited = false;
|
|
317
|
+
this.initialized = false;
|
|
318
|
+
this.start();
|
|
319
|
+
await this.initialize();
|
|
320
|
+
}
|
|
289
321
|
shutdown() {
|
|
290
322
|
if (!this.child)
|
|
291
323
|
return;
|
package/build/cli.js
CHANGED
|
@@ -933,6 +933,22 @@ async function upgrade(opts) {
|
|
|
933
933
|
const message = err instanceof Error ? err.message : String(err);
|
|
934
934
|
throw new Error(`.mcp.json drift check failed: ${message}`);
|
|
935
935
|
}
|
|
936
|
+
// v1.0.X — Layer 7 heal: update user-level ~/.claude.json MCP server
|
|
937
|
+
// registrations that point to old context-mode version dirs.
|
|
938
|
+
// (anthropics/claude-code#59310 workaround — see heal-installed-plugins.mjs)
|
|
939
|
+
try {
|
|
940
|
+
// @ts-expect-error — JS module, no TS declarations
|
|
941
|
+
const { healClaudeJsonMcpArgs } = await import("../scripts/heal-installed-plugins.mjs");
|
|
942
|
+
const dotClaudeJson = resolve(homedir(), ".claude.json");
|
|
943
|
+
const pluginCacheParent = resolve(resolveClaudeConfigDir(), "plugins", "cache", "context-mode", "context-mode");
|
|
944
|
+
const result = healClaudeJsonMcpArgs({ dotClaudeJsonPath: dotClaudeJson, pluginCacheParent, newPluginRoot: pluginRoot });
|
|
945
|
+
if (result.healed && result.healed.length > 0) {
|
|
946
|
+
p.log.info(color.dim(" ~/.claude.json user MCP registrations updated → " + newVersion));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
/* best effort — never block upgrade */
|
|
951
|
+
}
|
|
936
952
|
// v1.0.114 hotfix — marketplace post-pull assertion: clone (if
|
|
937
953
|
// present) MUST be on newVersion. Mert's case showed marketplace
|
|
938
954
|
// stuck at v1.0.89 — the sync block above swallowed that silently.
|
|
@@ -1151,6 +1167,7 @@ async function upgrade(opts) {
|
|
|
1151
1167
|
stdio: "inherit",
|
|
1152
1168
|
timeout: 30000,
|
|
1153
1169
|
cwd: pluginRoot,
|
|
1170
|
+
env: { ...process.env, CONTEXT_MODE_PLATFORM: detection.platform },
|
|
1154
1171
|
});
|
|
1155
1172
|
}
|
|
1156
1173
|
catch {
|
package/build/runtime.js
CHANGED
|
@@ -12,11 +12,14 @@ import { existsSync } from "node:fs";
|
|
|
12
12
|
* Match is case-insensitive; `.exe` extension tolerated for Windows binaries.
|
|
13
13
|
*/
|
|
14
14
|
const ALLOWED_SHELL_BASENAMES = /^(bash|sh|zsh|dash|pwsh|powershell|cmd)(\.exe)?$/i;
|
|
15
|
+
const BUN_BASENAME = /^bun(\.exe)?$/i;
|
|
16
|
+
function runtimeBasename(runtimePath) {
|
|
17
|
+
const segments = runtimePath.split(/[\\/]/);
|
|
18
|
+
return segments[segments.length - 1] ?? runtimePath;
|
|
19
|
+
}
|
|
15
20
|
export function isAllowlistedShell(shellPath) {
|
|
16
21
|
// Cross-OS basename: split on either separator, take the last segment.
|
|
17
|
-
|
|
18
|
-
const base = segments[segments.length - 1];
|
|
19
|
-
return ALLOWED_SHELL_BASENAMES.test(base);
|
|
22
|
+
return ALLOWED_SHELL_BASENAMES.test(runtimeBasename(shellPath));
|
|
20
23
|
}
|
|
21
24
|
const isWindows = process.platform === "win32";
|
|
22
25
|
function commandExists(cmd) {
|
|
@@ -305,14 +308,14 @@ export function getAvailableLanguages(runtimes) {
|
|
|
305
308
|
export function buildCommand(runtimes, language, filePath) {
|
|
306
309
|
switch (language) {
|
|
307
310
|
case "javascript":
|
|
308
|
-
return runtimes.javascript
|
|
311
|
+
return BUN_BASENAME.test(runtimeBasename(runtimes.javascript))
|
|
309
312
|
? [runtimes.javascript, "run", filePath]
|
|
310
313
|
: [runtimes.javascript, filePath];
|
|
311
314
|
case "typescript":
|
|
312
315
|
if (!runtimes.typescript) {
|
|
313
316
|
throw new Error("No TypeScript runtime available. Install one of: bun (recommended), tsx (npm i -g tsx), or ts-node.");
|
|
314
317
|
}
|
|
315
|
-
if (runtimes.typescript
|
|
318
|
+
if (BUN_BASENAME.test(runtimeBasename(runtimes.typescript)))
|
|
316
319
|
return [runtimes.typescript, "run", filePath];
|
|
317
320
|
if (runtimes.typescript === "tsx")
|
|
318
321
|
return ["tsx", filePath];
|
package/build/server.d.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type SpawnSyncOptions, type SpawnSyncReturns } from "node:child_process";
|
|
3
3
|
import { ContentStore } from "./store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Build the FK-attribution object passed to every ContentStore.index*() call
|
|
6
|
+
* in this process. CLAUDE_SESSION_ID is the only MCP-side handle we have on
|
|
7
|
+
* the current session — eventId stays undefined because MCP tool invocations
|
|
8
|
+
* are not paired with PostToolUse event rows at index time (the hook fires
|
|
9
|
+
* AFTER the tool returns). Empty-string fallback inside #insertChunks keeps
|
|
10
|
+
* legacy unattributed rows readable.
|
|
11
|
+
*/
|
|
12
|
+
export declare function currentAttribution(): {
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
} | undefined;
|
|
15
|
+
/** v1.0.134 SLICE A: opts injection for testability. Production callers pass nothing. */
|
|
16
|
+
export declare function resolveSessionIdFromSessionDB(opts?: {
|
|
17
|
+
projectDir?: string;
|
|
18
|
+
sessionsDir?: string;
|
|
19
|
+
bypassCache?: boolean;
|
|
20
|
+
}): string | undefined;
|
|
4
21
|
/**
|
|
5
22
|
* Parse FTS5 highlight markers to find match positions in the
|
|
6
23
|
* original (marker-free) text. Returns character offsets into the
|
package/build/server.js
CHANGED
|
@@ -29,7 +29,7 @@ import { getHookScriptPaths } from "./util/hook-config.js";
|
|
|
29
29
|
import { resolveClaudeConfigDir } from "./util/claude-config.js";
|
|
30
30
|
import { resolveProjectDir } from "./util/project-dir.js";
|
|
31
31
|
import { loadDatabase } from "./db-base.js";
|
|
32
|
-
import { AnalyticsEngine, formatReport, getConversationStats, getLifetimeStats, getMultiAdapterLifetimeStats, getRealBytesStats, OPUS_INPUT_PRICE_PER_TOKEN } from "./session/analytics.js";
|
|
32
|
+
import { AnalyticsEngine, formatReport, getConversationStats, getContentBytesAllSessions, getLifetimeStats, getMultiAdapterLifetimeStats, getRealBytesStats, OPUS_INPUT_PRICE_PER_TOKEN } from "./session/analytics.js";
|
|
33
33
|
const __pkg_dir = dirname(fileURLToPath(import.meta.url));
|
|
34
34
|
const VERSION = (() => {
|
|
35
35
|
for (const rel of ["../package.json", "./package.json"]) {
|
|
@@ -86,12 +86,56 @@ let _store = null;
|
|
|
86
86
|
* AFTER the tool returns). Empty-string fallback inside #insertChunks keeps
|
|
87
87
|
* legacy unattributed rows readable.
|
|
88
88
|
*/
|
|
89
|
-
function currentAttribution() {
|
|
90
|
-
|
|
89
|
+
export function currentAttribution() {
|
|
90
|
+
// CLAUDE_SESSION_ID env var is NOT propagated to MCP servers (only to hooks).
|
|
91
|
+
// Cross-adapter resolution: every adapter (15 of them) sets *_PROJECT_DIR env
|
|
92
|
+
// and writes session_events via hooks. Read the most-recent session_id from
|
|
93
|
+
// THIS project's session DB. Works for claude-code/cursor/gemini-cli/codex/
|
|
94
|
+
// kiro/opencode/zed/kilo/openclaw/qwen-code/vscode-copilot/jetbrains-copilot/
|
|
95
|
+
// omp/pi/antigravity — no adapter-specific transcript path required.
|
|
96
|
+
const sessionId = process.env.CLAUDE_SESSION_ID ?? resolveSessionIdFromSessionDB();
|
|
91
97
|
if (!sessionId)
|
|
92
98
|
return undefined;
|
|
93
99
|
return { sessionId };
|
|
94
100
|
}
|
|
101
|
+
let __cachedSessionId;
|
|
102
|
+
/** v1.0.134 SLICE A: opts injection for testability. Production callers pass nothing. */
|
|
103
|
+
export function resolveSessionIdFromSessionDB(opts) {
|
|
104
|
+
// 2s cache — ctx_fetch_and_index can fire 5+ chunks/sec; DB open cost adds up.
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
if (!opts?.bypassCache && __cachedSessionId && now - __cachedSessionId.checkedAt < 2000) {
|
|
107
|
+
return __cachedSessionId.sid;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const projectDir = opts?.projectDir
|
|
111
|
+
?? process.env.CLAUDE_PROJECT_DIR
|
|
112
|
+
?? process.env.CONTEXT_MODE_PROJECT_DIR;
|
|
113
|
+
if (!projectDir)
|
|
114
|
+
return undefined;
|
|
115
|
+
const sessionsDir = opts?.sessionsDir ?? getSessionDir();
|
|
116
|
+
const dbPath = resolveSessionDbPath({ projectDir, sessionsDir });
|
|
117
|
+
if (!existsSync(dbPath))
|
|
118
|
+
return undefined;
|
|
119
|
+
const Database = loadDatabase();
|
|
120
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
121
|
+
try {
|
|
122
|
+
const row = db.prepare("SELECT session_id FROM session_events ORDER BY created_at DESC LIMIT 1").get();
|
|
123
|
+
const sid = row?.session_id;
|
|
124
|
+
if (sid)
|
|
125
|
+
__cachedSessionId = { sid, checkedAt: now };
|
|
126
|
+
return sid;
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
try {
|
|
130
|
+
db.close();
|
|
131
|
+
}
|
|
132
|
+
catch { /* best-effort */ }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
95
139
|
/**
|
|
96
140
|
* Auto-index session events files written by SessionStart hook.
|
|
97
141
|
* Scans ~/.claude/context-mode/sessions/ for *-events.md files.
|
|
@@ -2622,7 +2666,21 @@ server.registerTool("ctx_stats", {
|
|
|
2622
2666
|
// Render-time read-only — no DB mutation, no backfill.
|
|
2623
2667
|
const contentDbPath = getStorePath();
|
|
2624
2668
|
const convReal = getRealBytesStats({ sessionId: sid, sessionsDir: getSessionDir(), worktreeHash: dbHash, contentDbPath });
|
|
2625
|
-
const
|
|
2669
|
+
const lifeRealBase = getRealBytesStats({ sessionsDir: getSessionDir() });
|
|
2670
|
+
// v1.0.134 SLICE C: lifetime tier sums ALL chunks (no
|
|
2671
|
+
// session_id filter). Without this fold, lifetime "kept out"
|
|
2672
|
+
// only counts session_events.bytes_avoided and ignores the
|
|
2673
|
+
// bulk of indexed payload across every prior conversation.
|
|
2674
|
+
const lifeContentBytes = getContentBytesAllSessions(contentDbPath);
|
|
2675
|
+
const lifeReal = {
|
|
2676
|
+
...lifeRealBase,
|
|
2677
|
+
contentBytes: lifeRealBase.contentBytes + lifeContentBytes,
|
|
2678
|
+
bytesAvoided: lifeRealBase.bytesAvoided + lifeContentBytes,
|
|
2679
|
+
totalSavedTokens: Math.floor((lifeRealBase.eventDataBytes
|
|
2680
|
+
+ lifeRealBase.bytesAvoided
|
|
2681
|
+
+ lifeContentBytes
|
|
2682
|
+
+ lifeRealBase.snapshotBytes) / 4),
|
|
2683
|
+
};
|
|
2626
2684
|
realBytes = { conversation: convReal, lifetime: lifeReal };
|
|
2627
2685
|
}
|
|
2628
2686
|
}
|
|
@@ -391,6 +391,24 @@ export interface RealBytesStats {
|
|
|
391
391
|
export declare function getContentBytesForSession(sessionId: string, contentDbPath: string, opts?: {
|
|
392
392
|
loadDatabase?: () => unknown;
|
|
393
393
|
}): number;
|
|
394
|
+
/**
|
|
395
|
+
* v1.0.134 SLICE C — lifetime tier all-chunks aggregate.
|
|
396
|
+
*
|
|
397
|
+
* Sibling of {@link getContentBytesForSession} that omits the session_id
|
|
398
|
+
* filter so the lifetime tier sees every chunk in the content store —
|
|
399
|
+
* including legacy unattributed rows (sessionId === '') and chunks
|
|
400
|
+
* attributed to other adapters' sessions. Without this, the lifetime
|
|
401
|
+
* "kept out" headline only counts session_events.bytes_avoided and
|
|
402
|
+
* misses the bulk of indexed payload.
|
|
403
|
+
*
|
|
404
|
+
* Best-effort: returns 0 when the DB file is missing, the schema lacks
|
|
405
|
+
* the `chunks` table, or the query fails. Never throws — same contract
|
|
406
|
+
* as the rest of the analytics module so a corrupt content DB cannot
|
|
407
|
+
* crash ctx_stats.
|
|
408
|
+
*/
|
|
409
|
+
export declare function getContentBytesAllSessions(contentDbPath: string, opts?: {
|
|
410
|
+
loadDatabase?: () => unknown;
|
|
411
|
+
}): number;
|
|
394
412
|
/**
|
|
395
413
|
* Compute real-bytes stats across one session, one project (worktree
|
|
396
414
|
* filter), or every session on disk (lifetime).
|
|
@@ -539,19 +557,6 @@ export declare const adapterLabels: Record<string, string>;
|
|
|
539
557
|
* information. Scale awareness comes from the unit jump between rows.
|
|
540
558
|
*/
|
|
541
559
|
export declare function kb(b: number): string;
|
|
542
|
-
/**
|
|
543
|
-
* Locale + IANA-timezone detection for the narrative renderer.
|
|
544
|
-
*
|
|
545
|
-
* Cascade (each level overrides the next):
|
|
546
|
-
* 1. CONTEXT_MODE_LOCALE / CONTEXT_MODE_TZ env overrides
|
|
547
|
-
* (used by tests + by users who want to pin output regardless of OS).
|
|
548
|
-
* 2. macOS `defaults read -g AppleLocale` → `en_TR` style → `en-TR`.
|
|
549
|
-
* 3. Linux `LANG` / `LC_TIME` env vars.
|
|
550
|
-
* 4. Fallback: `Intl.DateTimeFormat().resolvedOptions().locale`.
|
|
551
|
-
*
|
|
552
|
-
* Timezone always uses `Intl.DateTimeFormat().resolvedOptions().timeZone`
|
|
553
|
-
* — that one's always available and correct regardless of platform.
|
|
554
|
-
*/
|
|
555
560
|
export declare function detectLocaleAndTz(): {
|
|
556
561
|
locale: string;
|
|
557
562
|
tz: string;
|
|
@@ -750,6 +750,52 @@ export function getContentBytesForSession(sessionId, contentDbPath, opts) {
|
|
|
750
750
|
return 0;
|
|
751
751
|
}
|
|
752
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* v1.0.134 SLICE C — lifetime tier all-chunks aggregate.
|
|
755
|
+
*
|
|
756
|
+
* Sibling of {@link getContentBytesForSession} that omits the session_id
|
|
757
|
+
* filter so the lifetime tier sees every chunk in the content store —
|
|
758
|
+
* including legacy unattributed rows (sessionId === '') and chunks
|
|
759
|
+
* attributed to other adapters' sessions. Without this, the lifetime
|
|
760
|
+
* "kept out" headline only counts session_events.bytes_avoided and
|
|
761
|
+
* misses the bulk of indexed payload.
|
|
762
|
+
*
|
|
763
|
+
* Best-effort: returns 0 when the DB file is missing, the schema lacks
|
|
764
|
+
* the `chunks` table, or the query fails. Never throws — same contract
|
|
765
|
+
* as the rest of the analytics module so a corrupt content DB cannot
|
|
766
|
+
* crash ctx_stats.
|
|
767
|
+
*/
|
|
768
|
+
export function getContentBytesAllSessions(contentDbPath, opts) {
|
|
769
|
+
if (!contentDbPath)
|
|
770
|
+
return 0;
|
|
771
|
+
if (!existsSync(contentDbPath))
|
|
772
|
+
return 0;
|
|
773
|
+
let DatabaseCtor = null;
|
|
774
|
+
try {
|
|
775
|
+
DatabaseCtor = opts?.loadDatabase
|
|
776
|
+
? opts.loadDatabase()
|
|
777
|
+
: loadDatabaseImpl();
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
return 0;
|
|
781
|
+
}
|
|
782
|
+
if (!DatabaseCtor)
|
|
783
|
+
return 0;
|
|
784
|
+
try {
|
|
785
|
+
const db = new DatabaseCtor(contentDbPath, { readonly: true });
|
|
786
|
+
try {
|
|
787
|
+
const row = db.prepare(`SELECT COALESCE(SUM(LENGTH(content) + LENGTH(title)), 0) AS bytes
|
|
788
|
+
FROM chunks`).get();
|
|
789
|
+
return Number(row?.bytes ?? 0);
|
|
790
|
+
}
|
|
791
|
+
finally {
|
|
792
|
+
db.close();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
return 0;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
753
799
|
/**
|
|
754
800
|
* Compute real-bytes stats across one session, one project (worktree
|
|
755
801
|
* filter), or every session on disk (lifetime).
|
|
@@ -1044,6 +1090,21 @@ export function getMultiAdapterRealBytesStats(opts) {
|
|
|
1044
1090
|
worktreeHash: opts?.worktreeHash,
|
|
1045
1091
|
loadDatabase: opts?.loadDatabase,
|
|
1046
1092
|
});
|
|
1093
|
+
// ARCH-REVIEW-V134-ABC SLICE C: aggregate this adapter's content DB
|
|
1094
|
+
// bytes into the lifetime sum. `getRealBytesStats` operates on
|
|
1095
|
+
// session events only and never touches the sibling content/ tree —
|
|
1096
|
+
// without this step the lifetime tier in ctx_stats reports 0 for
|
|
1097
|
+
// every adapter except whichever one happens to share the
|
|
1098
|
+
// sessionsDir of the caller. Lifetime tier ignores sessionId so
|
|
1099
|
+
// the all-sessions aggregator is the right helper here.
|
|
1100
|
+
if (!opts?.sessionId) {
|
|
1101
|
+
const contentDbPath = join(entry.contentDir, "content.db");
|
|
1102
|
+
const adapterContentBytes = getContentBytesAllSessions(contentDbPath, {
|
|
1103
|
+
loadDatabase: opts?.loadDatabase,
|
|
1104
|
+
});
|
|
1105
|
+
one.contentBytes += adapterContentBytes;
|
|
1106
|
+
sum.contentBytes += adapterContentBytes;
|
|
1107
|
+
}
|
|
1047
1108
|
perAdapter.push({ name: entry.name, ...one });
|
|
1048
1109
|
sum.eventDataBytes += one.eventDataBytes;
|
|
1049
1110
|
sum.bytesAvoided += one.bytesAvoided;
|
|
@@ -1156,9 +1217,45 @@ function formatDuration(uptimeMin) {
|
|
|
1156
1217
|
* Timezone always uses `Intl.DateTimeFormat().resolvedOptions().timeZone`
|
|
1157
1218
|
* — that one's always available and correct regardless of platform.
|
|
1158
1219
|
*/
|
|
1220
|
+
/**
|
|
1221
|
+
* Validate that a locale string is a usable BCP 47 tag.
|
|
1222
|
+
*
|
|
1223
|
+
* Ubuntu GHA runners default to `LANG=C.UTF-8`. The extractor below strips
|
|
1224
|
+
* that to `"C"` — a valid POSIX locale identifier but NOT a BCP 47 tag.
|
|
1225
|
+
* On macOS / Node 20, `new Intl.DateTimeFormat("C", …)` throws RangeError
|
|
1226
|
+
* outright. CI run 25887250971 caught this via the v1.0.134 SLICE B test.
|
|
1227
|
+
*
|
|
1228
|
+
* Earlier fix attempt used a permissive `supportedLocalesOf || construction`
|
|
1229
|
+
* OR check — that was wrong: on Linux + Node 22.5, `new Intl.DateTimeFormat
|
|
1230
|
+
* ("POSIX")` does NOT throw, it silently falls back to the root locale and
|
|
1231
|
+
* still emits garbage at format time. CI run 25904838577 surfaced that —
|
|
1232
|
+
* "POSIX" round-tripped through the validator unchanged.
|
|
1233
|
+
*
|
|
1234
|
+
* Strict gate: `Intl.DateTimeFormat.supportedLocalesOf(tag)` returns `[]` for
|
|
1235
|
+
* any tag that doesn't map to a real language (regardless of whether
|
|
1236
|
+
* construction with that tag throws). That's the contract we want — "is this
|
|
1237
|
+
* a BCP 47 tag the host actually has data for". Construction is an explicit
|
|
1238
|
+
* sanity check; both must pass.
|
|
1239
|
+
*/
|
|
1240
|
+
function isUsableBcp47Locale(raw) {
|
|
1241
|
+
if (!raw)
|
|
1242
|
+
return false;
|
|
1243
|
+
try {
|
|
1244
|
+
if (Intl.DateTimeFormat.supportedLocalesOf(raw).length === 0)
|
|
1245
|
+
return false;
|
|
1246
|
+
// Belt: confirm construction doesn't throw on this host either.
|
|
1247
|
+
new Intl.DateTimeFormat(raw);
|
|
1248
|
+
return true;
|
|
1249
|
+
}
|
|
1250
|
+
catch {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1159
1254
|
export function detectLocaleAndTz() {
|
|
1160
1255
|
const env = (process.env ?? {});
|
|
1161
1256
|
let locale = env.CONTEXT_MODE_LOCALE ?? "";
|
|
1257
|
+
if (locale && !isUsableBcp47Locale(locale))
|
|
1258
|
+
locale = "";
|
|
1162
1259
|
if (!locale) {
|
|
1163
1260
|
if (process.platform === "darwin") {
|
|
1164
1261
|
try {
|
|
@@ -1173,11 +1270,18 @@ export function detectLocaleAndTz() {
|
|
|
1173
1270
|
locale = out.replace(/_/g, "-");
|
|
1174
1271
|
}
|
|
1175
1272
|
catch { /* defaults missing or sandbox */ }
|
|
1273
|
+
if (locale && !isUsableBcp47Locale(locale))
|
|
1274
|
+
locale = "";
|
|
1176
1275
|
}
|
|
1177
1276
|
if (!locale && (env.LC_TIME || env.LANG)) {
|
|
1178
1277
|
const raw = (env.LC_TIME || env.LANG || "").split(".")[0];
|
|
1179
1278
|
if (raw)
|
|
1180
1279
|
locale = raw.replace(/_/g, "-");
|
|
1280
|
+
// POSIX locale identifiers (`C`, `POSIX`) survive the simple extraction
|
|
1281
|
+
// above but blow up `new Intl.DateTimeFormat(locale, ...)`. Drop and
|
|
1282
|
+
// fall through to the host-default branch below.
|
|
1283
|
+
if (locale && !isUsableBcp47Locale(locale))
|
|
1284
|
+
locale = "";
|
|
1181
1285
|
}
|
|
1182
1286
|
if (!locale) {
|
|
1183
1287
|
try {
|
|
@@ -1197,7 +1301,13 @@ export function detectLocaleAndTz() {
|
|
|
1197
1301
|
tz = "UTC";
|
|
1198
1302
|
}
|
|
1199
1303
|
}
|
|
1200
|
-
|
|
1304
|
+
// Final belt-and-suspenders: if the locale we settled on is somehow still
|
|
1305
|
+
// unusable (env mutation between detection and return, contributor adding
|
|
1306
|
+
// a new extraction path that skips the validator), fall back to en-US so
|
|
1307
|
+
// formatLocalDateTime / monthDay / weekdayCap never throw at render time.
|
|
1308
|
+
if (!isUsableBcp47Locale(locale))
|
|
1309
|
+
locale = "en-US";
|
|
1310
|
+
return { locale, tz: tz || "UTC" };
|
|
1201
1311
|
}
|
|
1202
1312
|
/**
|
|
1203
1313
|
* Format an absolute path as a human-friendly display string by
|
|
@@ -1396,19 +1506,32 @@ function renderNarrative5Section(args) {
|
|
|
1396
1506
|
out.push("");
|
|
1397
1507
|
// Without/With bars — measured from real per-event bytes_returned / bytes_avoided.
|
|
1398
1508
|
//
|
|
1399
|
-
// Honest definitions:
|
|
1400
|
-
// Without = bytes the model WOULD have re-seen with no filtering
|
|
1401
|
-
//
|
|
1509
|
+
// Honest definitions (v1.0.134 SLICE B — eventDataBytes floor):
|
|
1510
|
+
// Without = bytes the model WOULD have re-seen with no filtering
|
|
1511
|
+
// = bytes_avoided + bytes_returned + eventDataBytes
|
|
1512
|
+
// With = bytes the model ACTUALLY re-saw after context-mode
|
|
1513
|
+
// = bytes_returned + eventDataBytes
|
|
1514
|
+
//
|
|
1515
|
+
// Why eventDataBytes belongs on BOTH sides:
|
|
1516
|
+
// `eventDataBytes` is the raw payload captured by the hook (tool args,
|
|
1517
|
+
// prompt body, etc). Those bytes were "kept out" — never inflated back
|
|
1518
|
+
// into context — but they still represent real measured signal. Pre-fix
|
|
1519
|
+
// the formula was `with = max(1, bytesReturned)`, which collapsed to 1
|
|
1520
|
+
// whenever the conversation hadn't accumulated any re-served bytes yet
|
|
1521
|
+
// (early in a session, or for tool-heavy work that never re-hits index).
|
|
1522
|
+
// That produced a degenerate ~100% kept-out bar even when the only
|
|
1523
|
+
// honest signal we had was a few KB of event payloads.
|
|
1402
1524
|
//
|
|
1403
1525
|
// No fallback to heuristic. If the schema has zero signal for this
|
|
1404
|
-
// conversation (no hook ever populated
|
|
1526
|
+
// conversation (no hook ever populated any of the three columns),
|
|
1405
1527
|
// the section is skipped entirely. Honesty over decoration.
|
|
1406
1528
|
const realConv = realBytes?.conversation;
|
|
1407
1529
|
const measuredAvoided = realConv?.bytesAvoided ?? 0;
|
|
1408
1530
|
const measuredReturned = realConv?.bytesReturned ?? 0;
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
const
|
|
1531
|
+
const measuredEvent = realConv?.eventDataBytes ?? 0;
|
|
1532
|
+
if (measuredAvoided + measuredReturned + measuredEvent > 0) {
|
|
1533
|
+
const convBytesWithout = measuredAvoided + measuredReturned + measuredEvent;
|
|
1534
|
+
const convBytesWith = Math.max(1, measuredReturned + measuredEvent);
|
|
1412
1535
|
const convTokensWithout = Math.max(1, Math.floor(convBytesWithout / 4));
|
|
1413
1536
|
const convTokensWith = Math.max(1, Math.floor(convBytesWith / 4));
|
|
1414
1537
|
const withoutBar = dataBar(convTokensWithout, convTokensWithout, 32);
|
|
@@ -13,12 +13,18 @@ export declare function resolveClaudeGlobalSettingsPath(env?: NodeJS.ProcessEnv)
|
|
|
13
13
|
* adapter is non-claude — claude is already covered by entry 2).
|
|
14
14
|
* 2. The claude global settings.json (always — defense in depth).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* Static import of `../adapters/detect.js` is safe — detect.ts only imports
|
|
17
|
+
* `node:` builtins, `./types.js` (type-only), and `./client-map.js` (pure
|
|
18
|
+
* data). It does NOT import claude-config back, so no cycle.
|
|
19
|
+
*
|
|
20
|
+
* History: this used `createRequire(import.meta.url).resolve(...)` to lazy-
|
|
21
|
+
* load detect at call time. That pattern requires `require(esm)`, which is
|
|
22
|
+
* flag-gated on Node 22.x before 22.12 (`--experimental-require-module`).
|
|
23
|
+
* CI run 25877550371 on Node 22.5 silently failed every detect.* call —
|
|
24
|
+
* the catch block ate the error and every cross-adapter deny-policy test
|
|
25
|
+
* returned an empty policy list. Static import sidesteps the require(esm)
|
|
26
|
+
* gate entirely, so the same code works on every supported Node version
|
|
27
|
+
* (20.x, 22.5, 22.12+, 24+) without needing the experimental flag.
|
|
22
28
|
*
|
|
23
29
|
* The returned array is deduplicated and order-stable: adapter-specific path
|
|
24
30
|
* first (most specific), claude global second (fallback).
|