context-mode 1.0.166 → 1.0.167
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/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +6 -4
- package/build/adapters/codex/usage.d.ts +107 -0
- package/build/adapters/codex/usage.js +227 -0
- package/build/adapters/gemini-cli/hooks.d.ts +7 -1
- package/build/adapters/gemini-cli/hooks.js +9 -1
- package/build/adapters/gemini-cli/index.js +11 -0
- package/build/adapters/kimi/paths.d.ts +20 -0
- package/build/adapters/kimi/paths.js +41 -1
- package/build/adapters/kimi/usage.d.ts +82 -0
- package/build/adapters/kimi/usage.js +217 -0
- package/build/adapters/omp/plugin.d.ts +6 -0
- package/build/adapters/omp/plugin.js +87 -2
- package/build/adapters/omp/usage.d.ts +49 -0
- package/build/adapters/omp/usage.js +110 -0
- package/build/adapters/openclaw/plugin.d.ts +10 -0
- package/build/adapters/openclaw/plugin.js +57 -0
- package/build/adapters/openclaw/usage.d.ts +34 -0
- package/build/adapters/openclaw/usage.js +52 -0
- package/build/adapters/opencode/plugin.d.ts +17 -0
- package/build/adapters/opencode/plugin.js +40 -1
- package/build/adapters/pi/extension.js +34 -1
- package/build/adapters/qwen-code/index.js +23 -1
- package/build/adapters/qwen-code/usage.d.ts +90 -0
- package/build/adapters/qwen-code/usage.js +222 -0
- package/build/session/db.d.ts +11 -0
- package/build/session/db.js +33 -0
- package/build/session/extract.d.ts +208 -0
- package/build/session/extract.js +670 -43
- package/build/session/model-prices.json +429 -0
- package/build/session/pricing.d.ts +64 -0
- package/build/session/pricing.js +151 -0
- package/cli.bundle.mjs +62 -62
- package/configs/antigravity-cli/plugin.json +1 -1
- package/configs/copilot-cli/.github/plugin/plugin.json +1 -1
- package/configs/gemini-cli/settings.json +11 -0
- package/hooks/codex/stop.mjs +91 -4
- package/hooks/gemini-cli/aftermodel.mjs +70 -0
- package/hooks/kimi/stop.mjs +74 -3
- package/hooks/qwen-code/platform.mjs +1 -0
- package/hooks/qwen-code/stop.mjs +168 -0
- package/hooks/session-db.bundle.mjs +7 -7
- package/hooks/session-extract.bundle.mjs +3 -2
- package/hooks/session-loaders.mjs +9 -1
- package/hooks/stop.mjs +35 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +90 -90
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.167",
|
|
4
4
|
"description": "context-mode for Antigravity CLI (agy): sandboxed code execution, FTS5 knowledge base, and session capture. Saves your context window by keeping raw bytes out of the conversation.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
3
|
"description": "context-mode for GitHub Copilot CLI: sandboxed code execution in 11 languages, an FTS5 knowledge base with BM25 ranking, and session capture. Saves your context window by keeping raw bytes out of the conversation.",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.167",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
7
7
|
"context-window",
|
package/hooks/codex/stop.mjs
CHANGED
|
@@ -11,14 +11,70 @@ import "../ensure-deps.mjs";
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, CODEX_OPTS } from "../session-helpers.mjs";
|
|
14
|
-
import { createSessionLoaders } from "../session-loaders.mjs";
|
|
15
|
-
import { dirname } from "node:path";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
17
19
|
|
|
18
20
|
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
|
|
21
|
+
const { loadSessionDB, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
|
|
20
22
|
const OPTS = CODEX_OPTS;
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Locate the codex rollout JSONL for this session. Codex persists turns at
|
|
26
|
+
* $CODEX_HOME/sessions/YYYY/MM/DD/rollout-<ts>-<session_id>.jsonl (default
|
|
27
|
+
* $CODEX_HOME = ~/.codex). The hook stdin does NOT carry the path, so we walk
|
|
28
|
+
* the sessions tree and match the filename suffix on the session id. Pure
|
|
29
|
+
* directory walk + string suffix test — NO regex. Best-effort: returns null on
|
|
30
|
+
* any failure so cost capture never blocks the turn.
|
|
31
|
+
*/
|
|
32
|
+
function findCodexRollout(sessionId) {
|
|
33
|
+
try {
|
|
34
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
35
|
+
const home = process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
36
|
+
const root = join(home, "sessions");
|
|
37
|
+
if (!existsSync(root)) return null;
|
|
38
|
+
const suffix = `${sessionId}.jsonl`;
|
|
39
|
+
const stack = [root];
|
|
40
|
+
while (stack.length > 0) {
|
|
41
|
+
const dir = stack.pop();
|
|
42
|
+
let entries;
|
|
43
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
44
|
+
for (const e of entries) {
|
|
45
|
+
const full = join(dir, e.name);
|
|
46
|
+
if (e.isDirectory()) { stack.push(full); continue; }
|
|
47
|
+
if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(suffix)) {
|
|
48
|
+
return full;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore — best-effort
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load the codex usage extractor (build/adapters/codex/usage.js), mirroring the
|
|
60
|
+
* loadModule build-path fallback in session-loaders. Returns null if the module
|
|
61
|
+
* is absent (e.g. pre-build dev tree) so the hook degrades gracefully.
|
|
62
|
+
*/
|
|
63
|
+
async function loadCodexUsage() {
|
|
64
|
+
try {
|
|
65
|
+
const pluginRoot = join(HOOK_DIR, "..", "..");
|
|
66
|
+
const candidates = [
|
|
67
|
+
join(pluginRoot, "build", "adapters", "codex", "usage.js"),
|
|
68
|
+
];
|
|
69
|
+
for (const p of candidates) {
|
|
70
|
+
if (existsSync(p)) return await import(pathToFileURL(p).href);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
22
78
|
try {
|
|
23
79
|
const raw = await readStdin();
|
|
24
80
|
const input = parseStdin(raw);
|
|
@@ -43,6 +99,37 @@ try {
|
|
|
43
99
|
priority: 1,
|
|
44
100
|
}, "Stop");
|
|
45
101
|
|
|
102
|
+
// ─── codex MAIN-turn cost capture (cursor-aware, no double-count) ─────────
|
|
103
|
+
// Codex carries no tokens on the hook payload (model only); per-turn usage is
|
|
104
|
+
// persisted to the session rollout JSONL as `event_msg`/`token_count`
|
|
105
|
+
// records. We tail that file, cursor-gated by rollout LINE INDEX, summing
|
|
106
|
+
// ONLY the completed turns NEW since the last Stop. Each step is best-effort
|
|
107
|
+
// — a hook must never block the session, so any read/extract failure here is
|
|
108
|
+
// swallowed without aborting the turn_end write above.
|
|
109
|
+
try {
|
|
110
|
+
const rolloutPath = findCodexRollout(sessionId);
|
|
111
|
+
if (rolloutPath) {
|
|
112
|
+
let rollout = null;
|
|
113
|
+
try { rollout = readFileSync(rolloutPath, "utf-8"); } catch { /* unreadable — skip */ }
|
|
114
|
+
if (rollout) {
|
|
115
|
+
const usageMod = await loadCodexUsage();
|
|
116
|
+
if (usageMod && typeof usageMod.extractCodexUsageSince === "function") {
|
|
117
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
118
|
+
const cursor = db.getUsageCursor(sessionId);
|
|
119
|
+
const { events, cursor: next } = usageMod.extractCodexUsageSince(rollout, cursor);
|
|
120
|
+
if (events.length > 0) {
|
|
121
|
+
// attributeAndInsertEvents both INSERTS locally and FORWARDS to the
|
|
122
|
+
// platform (gated on ~/.context-mode/platform.json).
|
|
123
|
+
attributeAndInsertEvents(db, sessionId, events, input, projectDir, "Stop", resolveProjectAttributions);
|
|
124
|
+
}
|
|
125
|
+
if (next) db.setUsageCursor(sessionId, next);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Best-effort cost capture — never block the session on failure.
|
|
131
|
+
}
|
|
132
|
+
|
|
46
133
|
db.close();
|
|
47
134
|
} catch {
|
|
48
135
|
// Codex hooks must not block the session.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "../suppress-stderr.mjs";
|
|
3
|
+
import "../ensure-deps.mjs";
|
|
4
|
+
/**
|
|
5
|
+
* Gemini CLI AfterModel hook — per-turn token / cost capture.
|
|
6
|
+
*
|
|
7
|
+
* AfterModel fires per model call inside gemini-cli's stream loop
|
|
8
|
+
* (packages/core/src/core/geminiChat.ts:1213). The hook payload carries
|
|
9
|
+
* `llm_request` + `llm_response` (hooks/types.ts:692-695); the real Gemini
|
|
10
|
+
* `usageMetadata` (promptTokenCount, candidatesTokenCount, +cachedContent/
|
|
11
|
+
* thoughts when present) and resolved model live on `llm_response`
|
|
12
|
+
* (hookTranslator.ts:60-64, loggingContentGenerator.ts:405,553).
|
|
13
|
+
*
|
|
14
|
+
* parseGeminiUsage maps that into a builder `agent_usage` event; cost_usd is
|
|
15
|
+
* derived from the pricing catalog (gemini exposes no native cost). The event
|
|
16
|
+
* is forwarded via attributeAndInsertEvents — same path as hooks/stop.mjs.
|
|
17
|
+
*
|
|
18
|
+
* One AfterModel call = one billed model round-trip = one priced event.
|
|
19
|
+
* Per-userPromptId summation into a single per-turn total is deferred; emitting
|
|
20
|
+
* per-call never double-counts (each call's usageMetadata is authoritative).
|
|
21
|
+
*
|
|
22
|
+
* Must be fast (<20ms). No network, no LLM, just a parse + SQLite write.
|
|
23
|
+
* AfterModel is non-blocking — no stdout output.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, GEMINI_OPTS } from "../session-helpers.mjs";
|
|
27
|
+
import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
|
|
28
|
+
import { appendFileSync } from "node:fs";
|
|
29
|
+
import { join, dirname } from "node:path";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
|
|
33
|
+
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
|
|
35
|
+
const OPTS = GEMINI_OPTS;
|
|
36
|
+
const DEBUG_LOG = join(homedir(), ".gemini", "context-mode", "aftermodel-debug.log");
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const raw = await readStdin();
|
|
40
|
+
const input = parseStdin(raw);
|
|
41
|
+
const projectDir = getInputProjectDir(input, OPTS);
|
|
42
|
+
|
|
43
|
+
const { parseGeminiUsage } = await loadExtract();
|
|
44
|
+
const event = parseGeminiUsage(input);
|
|
45
|
+
|
|
46
|
+
if (event) {
|
|
47
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
48
|
+
const { SessionDB } = await loadSessionDB();
|
|
49
|
+
|
|
50
|
+
const dbPath = getSessionDBPath(OPTS, projectDir);
|
|
51
|
+
const db = new SessionDB({ dbPath });
|
|
52
|
+
const sessionId = getSessionId(input, OPTS);
|
|
53
|
+
db.ensureSession(sessionId, projectDir);
|
|
54
|
+
|
|
55
|
+
// attributeAndInsertEvents both INSERTS locally and FORWARDS to the
|
|
56
|
+
// platform (gated on ~/.context-mode/platform.json) — same as stop.mjs.
|
|
57
|
+
attributeAndInsertEvents(db, sessionId, [event], input, projectDir, "AfterModel", resolveProjectAttributions);
|
|
58
|
+
db.close();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] OK: ${event.model_id ?? "?"} in:${event.input_tokens ?? 0} out:${event.output_tokens ?? 0}\n`);
|
|
62
|
+
} catch { /* silent */ }
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
try {
|
|
66
|
+
appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ERR: ${err?.message || err}\n`);
|
|
67
|
+
} catch { /* silent */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// AfterModel is non-blocking — no stdout output
|
package/hooks/kimi/stop.mjs
CHANGED
|
@@ -16,14 +16,53 @@ import "../ensure-deps.mjs";
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, KIMI_OPTS } from "../session-helpers.mjs";
|
|
19
|
-
import { createSessionLoaders } from "../session-loaders.mjs";
|
|
20
|
-
import { dirname } from "node:path";
|
|
19
|
+
import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
22
24
|
|
|
23
25
|
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
|
|
26
|
+
const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
|
|
25
27
|
const OPTS = KIMI_OPTS;
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Resolve <kimiConfigDir> (mirrors src/adapters/kimi/paths.ts:resolveKimiConfigDir
|
|
31
|
+
* — hooks cannot import the TS module, so the logic is duplicated here).
|
|
32
|
+
*/
|
|
33
|
+
function resolveKimiConfigDir() {
|
|
34
|
+
const envVal = process.env.KIMI_CODE_HOME;
|
|
35
|
+
if (envVal) {
|
|
36
|
+
if (envVal.startsWith("~")) return resolve(homedir(), envVal.replace(/^~[/\\]?/, ""));
|
|
37
|
+
return resolve(envVal);
|
|
38
|
+
}
|
|
39
|
+
return resolve(homedir(), ".kimi-code");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Best-effort <sessionDir>/wire.jsonl resolution keyed by session id.
|
|
44
|
+
*
|
|
45
|
+
* NOTE / WIRE GAP: adapter-matrix/kimi.md confirms usage lands at
|
|
46
|
+
* <sessionDir>/wire.jsonl (agent/index.ts:142), but the exact session_id ->
|
|
47
|
+
* sessionDir directory layout is not carried in the hook stdin payload and the
|
|
48
|
+
* kimi-code refs are not checked out in this worktree to pin it. We probe the
|
|
49
|
+
* documented candidate layouts and return the first whose wire.jsonl exists,
|
|
50
|
+
* else null -> the cost block degrades to a no-op rather than guessing wrong.
|
|
51
|
+
*/
|
|
52
|
+
function resolveKimiWireJsonlPath(sessionId) {
|
|
53
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
54
|
+
const configDir = resolveKimiConfigDir();
|
|
55
|
+
const candidates = [
|
|
56
|
+
join(configDir, "sessions", sessionId, "wire.jsonl"),
|
|
57
|
+
join(configDir, "agents", sessionId, "wire.jsonl"),
|
|
58
|
+
join(configDir, sessionId, "wire.jsonl"),
|
|
59
|
+
];
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
try { if (existsSync(candidate)) return candidate; } catch { /* try next */ }
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
27
66
|
try {
|
|
28
67
|
const raw = await readStdin();
|
|
29
68
|
const input = parseStdin(raw);
|
|
@@ -53,6 +92,38 @@ try {
|
|
|
53
92
|
priority: 1,
|
|
54
93
|
}, "Stop");
|
|
55
94
|
|
|
95
|
+
// ─── kimi-code per-turn cost capture (cursor-gated, no double-count) ───
|
|
96
|
+
// Usage lives ONLY on <sessionDir>/wire.jsonl `usage.record` lines, never in
|
|
97
|
+
// hook stdin (adapter-matrix/kimi.md). Tail the file, sum NEW delta lines
|
|
98
|
+
// since a per-session high-water cursor, and forward. Best-effort — a missing
|
|
99
|
+
// wire file or read/extract failure must never block the turn_end write or
|
|
100
|
+
// the session, so the whole block is wrapped and swallowed.
|
|
101
|
+
try {
|
|
102
|
+
const wirePath = resolveKimiWireJsonlPath(sessionId);
|
|
103
|
+
if (wirePath) {
|
|
104
|
+
let wireText = null;
|
|
105
|
+
try {
|
|
106
|
+
wireText = readFileSync(wirePath, "utf-8");
|
|
107
|
+
} catch {
|
|
108
|
+
// unreadable/missing wire.jsonl — skip capture this turn.
|
|
109
|
+
}
|
|
110
|
+
if (wireText) {
|
|
111
|
+
const { extractKimiUsageSince } = await loadExtract();
|
|
112
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
113
|
+
const cursor = db.getUsageCursor(sessionId);
|
|
114
|
+
const { events, cursor: next } = extractKimiUsageSince(wireText, cursor);
|
|
115
|
+
if (events.length > 0) {
|
|
116
|
+
// attributeAndInsertEvents both INSERTS locally and FORWARDS to the
|
|
117
|
+
// platform (gated on ~/.context-mode/platform.json).
|
|
118
|
+
attributeAndInsertEvents(db, sessionId, events, input, projectDir, "Stop", resolveProjectAttributions);
|
|
119
|
+
}
|
|
120
|
+
if (next) db.setUsageCursor(sessionId, next);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Best-effort cost capture — never block the session on failure.
|
|
125
|
+
}
|
|
126
|
+
|
|
56
127
|
db.close();
|
|
57
128
|
} catch {
|
|
58
129
|
// Kimi Code hooks must not block the session.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
process.env.CONTEXT_MODE_PLATFORM = "qwen-code";
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./platform.mjs";
|
|
3
|
+
import "../suppress-stderr.mjs";
|
|
4
|
+
import "../ensure-deps.mjs";
|
|
5
|
+
/**
|
|
6
|
+
* Qwen Code Stop hook — record turn-end state + capture per-turn token cost.
|
|
7
|
+
*
|
|
8
|
+
* Stop fires at the end of an assistant turn, not at true session shutdown, so
|
|
9
|
+
* we record a `turn_end` marker (session_end stays reserved for a genuine
|
|
10
|
+
* terminal lifecycle event) and tail the session JSONL for new usage.
|
|
11
|
+
*
|
|
12
|
+
* COST CAPTURE: qwen-code's hook stdin carries tool I/O ONLY — token usage is
|
|
13
|
+
* unreachable through the hook stream (matrix §4). The single live capture path
|
|
14
|
+
* is a cursor-gated tail of the session record file at
|
|
15
|
+
* ~/.qwen/tmp/<project_id>/chats/<sessionId>.jsonl
|
|
16
|
+
* where <project_id> = sha256(projectRoot) hex
|
|
17
|
+
* (refs chatRecordingService.ts:451 + storage.ts:316-320 getProjectTempDir;
|
|
18
|
+
* paths.ts:262 getProjectHash). The extractor lives in
|
|
19
|
+
* src/adapters/qwen-code/usage.ts and is re-exported through the
|
|
20
|
+
* session-extract bundle (src/session/extract.ts), reachable here via
|
|
21
|
+
* loadExtract() — exactly the kimi wire.jsonl pattern.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir } from "../session-helpers.mjs";
|
|
25
|
+
import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
|
|
26
|
+
import { dirname, join, resolve } from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { homedir, platform } from "node:os";
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
30
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
31
|
+
|
|
32
|
+
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
|
|
34
|
+
|
|
35
|
+
// Qwen Code session options. Mirrors the QwenCodeAdapter config (index.ts):
|
|
36
|
+
// config dir ~/.qwen, project dir via QWEN_PROJECT_DIR (no config-dir env var,
|
|
37
|
+
// no session-id env var — session_id arrives on the hook stdin). Kept local
|
|
38
|
+
// because session-helpers.mjs ships no QWEN_OPTS export.
|
|
39
|
+
const QWEN_OPTS = {
|
|
40
|
+
configDir: ".qwen",
|
|
41
|
+
configDirEnv: undefined,
|
|
42
|
+
projectDirEnv: "QWEN_PROJECT_DIR",
|
|
43
|
+
sessionIdEnv: undefined,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the qwen config dir (~/.qwen). Qwen Code exposes no documented
|
|
48
|
+
* config-dir override env var (unlike CODEX_HOME / KIMI_CODE_HOME), so this is
|
|
49
|
+
* a fixed home-rooted path. NO regex.
|
|
50
|
+
*/
|
|
51
|
+
function resolveQwenHome() {
|
|
52
|
+
return resolve(homedir(), ".qwen");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* EXACT port of qwen's getProjectHash (paths.ts:262): sha256 hex of the project
|
|
57
|
+
* root, lowercased first on win32 for case-insensitive FS parity. The TS twin
|
|
58
|
+
* is `qwenProjectHash` in src/adapters/qwen-code/usage.ts (unit-tested); the
|
|
59
|
+
* hook cannot import that TS at runtime, so the logic is duplicated here — same
|
|
60
|
+
* precedent as kimi's resolveKimiConfigDir. NO regex.
|
|
61
|
+
*/
|
|
62
|
+
function qwenProjectHash(projectRoot) {
|
|
63
|
+
const normalized = platform() === "win32" ? String(projectRoot).toLowerCase() : String(projectRoot);
|
|
64
|
+
return createHash("sha256").update(normalized).digest("hex");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve <qwenHome>/tmp/<sha256(projectRoot)>/chats/<sessionId>.jsonl.
|
|
69
|
+
*
|
|
70
|
+
* Primary path is fully deterministic from the hashing scheme above. Should the
|
|
71
|
+
* hash diverge (e.g. an upstream qwen normalization we did not pin), we fall
|
|
72
|
+
* back to a glob of every <qwenHome>/tmp/<*>/chats dir for <sessionId>.jsonl —
|
|
73
|
+
* a pure directory walk + filename equality test, NO regex. Best-effort:
|
|
74
|
+
* returns null on any failure so cost capture never blocks the turn.
|
|
75
|
+
*/
|
|
76
|
+
function resolveQwenChatJsonlPath(projectDir, sessionId) {
|
|
77
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
78
|
+
const qwenHome = resolveQwenHome();
|
|
79
|
+
const fileName = `${sessionId}.jsonl`;
|
|
80
|
+
|
|
81
|
+
// 1) Canonical hashed path.
|
|
82
|
+
try {
|
|
83
|
+
if (typeof projectDir === "string" && projectDir.length > 0) {
|
|
84
|
+
const direct = join(qwenHome, "tmp", qwenProjectHash(projectDir), "chats", fileName);
|
|
85
|
+
if (existsSync(direct)) return direct;
|
|
86
|
+
}
|
|
87
|
+
} catch { /* fall through to glob */ }
|
|
88
|
+
|
|
89
|
+
// 2) Glob fallback — scan every tmp/<hash>/chats for this session file.
|
|
90
|
+
try {
|
|
91
|
+
const tmpRoot = join(qwenHome, "tmp");
|
|
92
|
+
if (!existsSync(tmpRoot)) return null;
|
|
93
|
+
let projectDirs;
|
|
94
|
+
try { projectDirs = readdirSync(tmpRoot, { withFileTypes: true }); } catch { return null; }
|
|
95
|
+
for (const entry of projectDirs) {
|
|
96
|
+
if (!entry.isDirectory()) continue;
|
|
97
|
+
const candidate = join(tmpRoot, entry.name, "chats", fileName);
|
|
98
|
+
try { if (existsSync(candidate)) return candidate; } catch { /* try next */ }
|
|
99
|
+
}
|
|
100
|
+
} catch { /* best-effort */ }
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const raw = await readStdin();
|
|
107
|
+
const input = parseStdin(raw);
|
|
108
|
+
const projectDir = getInputProjectDir(input, QWEN_OPTS);
|
|
109
|
+
|
|
110
|
+
const { SessionDB } = await loadSessionDB();
|
|
111
|
+
const dbPath = getSessionDBPath(QWEN_OPTS, projectDir);
|
|
112
|
+
const db = new SessionDB({ dbPath });
|
|
113
|
+
const sessionId = getSessionId(input, QWEN_OPTS);
|
|
114
|
+
|
|
115
|
+
db.ensureSession(sessionId, projectDir);
|
|
116
|
+
// SessionEvent contract requires {type, category, data, priority}. insertEvent
|
|
117
|
+
// hashes `data` for the dedup key, so encode the turn snapshot into `data`.
|
|
118
|
+
const payload = {
|
|
119
|
+
stop_hook_active: input.stop_hook_active ?? false,
|
|
120
|
+
last_assistant_message: typeof input.last_assistant_message === "string"
|
|
121
|
+
? input.last_assistant_message.slice(0, 2000)
|
|
122
|
+
: null,
|
|
123
|
+
};
|
|
124
|
+
db.insertEvent(sessionId, {
|
|
125
|
+
type: "turn_end",
|
|
126
|
+
category: "session",
|
|
127
|
+
data: JSON.stringify(payload),
|
|
128
|
+
priority: 1,
|
|
129
|
+
}, "Stop");
|
|
130
|
+
|
|
131
|
+
// ─── qwen-code per-turn cost capture (cursor-gated, no double-count) ───
|
|
132
|
+
// Usage lives ONLY on the session chats JSONL ChatRecords, never in hook
|
|
133
|
+
// stdin. Tail the file, sum NEW usage records since a per-session high-water
|
|
134
|
+
// cursor, and forward. Best-effort — a missing file or read/extract failure
|
|
135
|
+
// must never block the turn_end write or the session, so the whole block is
|
|
136
|
+
// wrapped and swallowed.
|
|
137
|
+
try {
|
|
138
|
+
const jsonlPath = resolveQwenChatJsonlPath(projectDir, sessionId);
|
|
139
|
+
if (jsonlPath) {
|
|
140
|
+
let jsonlText = null;
|
|
141
|
+
try {
|
|
142
|
+
jsonlText = readFileSync(jsonlPath, "utf-8");
|
|
143
|
+
} catch {
|
|
144
|
+
// unreadable/missing chats JSONL — skip capture this turn.
|
|
145
|
+
}
|
|
146
|
+
if (jsonlText) {
|
|
147
|
+
const { extractQwenUsageSince } = await loadExtract();
|
|
148
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
149
|
+
const cursor = db.getUsageCursor(sessionId);
|
|
150
|
+
const { events, cursor: next } = extractQwenUsageSince(jsonlText, cursor);
|
|
151
|
+
if (events.length > 0) {
|
|
152
|
+
// attributeAndInsertEvents both INSERTS locally and FORWARDS to the
|
|
153
|
+
// platform (gated on ~/.context-mode/platform.json).
|
|
154
|
+
attributeAndInsertEvents(db, sessionId, events, input, projectDir, "Stop", resolveProjectAttributions);
|
|
155
|
+
}
|
|
156
|
+
if (next) db.setUsageCursor(sessionId, next);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// Best-effort cost capture — never block the session on failure.
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
db.close();
|
|
164
|
+
} catch {
|
|
165
|
+
// Qwen Code hooks must not block the session.
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
process.stdout.write("{}\n");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{createRequire as ie}from"node:module";import{existsSync as ae,unlinkSync as P,renameSync as ce}from"node:fs";import{tmpdir as ue}from"node:os";import{join as de}from"node:path";var A=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){let t="",r=null;for(let a=0;a<e.length;a++){let i=e[a];if(r)t+=i,i===r&&(r=null);else if(i==="'"||i==='"')t+=i,r=i;else if(i===";"){let c=t.trim();c&&this.#e.prepare(c).run(),t=""}else t+=i}let s=t.trim();return s&&this.#e.prepare(s).run(),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>{let s=t.get(...r);return s===null?void 0:s},all:(...r)=>t.all(...r),iterate:(...r)=>t.iterate(...r)}}transaction(e){return this.#e.transaction(e)}close(){this.#e.close()}},w=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){return this.#e.exec(e),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>t.get(...r),all:(...r)=>t.all(...r),iterate:(...r)=>typeof t.iterate=="function"?t.iterate(...r):t.all(...r)[Symbol.iterator]()}}transaction(e){return(...t)=>{this.#e.exec("BEGIN");try{let r=e(...t);return this.#e.exec("COMMIT"),r}catch(r){throw this.#e.exec("ROLLBACK"),r}}}close(){this.#e.close()}},m=null;function le(n){let e=null;try{return e=new n(":memory:"),e.exec("CREATE VIRTUAL TABLE __fts5_probe USING fts5(x)"),!0}catch{return!1}finally{try{e?.close()}catch{}}}function
|
|
2
|
-
`))}function K(n){let e=process.env[l];if(e===void 0)return{kind:"unset"};let t=e.trim();if(!t)return{kind:"ignored-empty",ignoredEnvVar:l,ignoredReason:"empty"};if(!G(t))throw De(n,t,`${l} must be an absolute path.`);return{kind:"override",root:p(t)}}function Le(n){return n.kind==="ignored-empty"?{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason}:{}}function z(n,e){let t=K(n);return t.kind!=="override"?null:{kind:n,path:
|
|
3
|
-
`)}function Oe(n){return n.ignoredEnvVar&&n.ignoredReason==="empty"?`Ignored empty ${n.ignoredEnvVar}; using adapter default.`:null}function J(){return`Set ${l} to a writable absolute path.`}function Ae(n){if(!n||typeof n!="object")return null;let e=n.path;return typeof e=="string"&&e.length>0?e:null}var _;function
|
|
1
|
+
import{createRequire as ie}from"node:module";import{existsSync as ae,unlinkSync as P,renameSync as ce}from"node:fs";import{tmpdir as ue}from"node:os";import{join as de}from"node:path";var A=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){let t="",r=null;for(let a=0;a<e.length;a++){let i=e[a];if(r)t+=i,i===r&&(r=null);else if(i==="'"||i==='"')t+=i,r=i;else if(i===";"){let c=t.trim();c&&this.#e.prepare(c).run(),t=""}else t+=i}let s=t.trim();return s&&this.#e.prepare(s).run(),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>{let s=t.get(...r);return s===null?void 0:s},all:(...r)=>t.all(...r),iterate:(...r)=>t.iterate(...r)}}transaction(e){return this.#e.transaction(e)}close(){this.#e.close()}},w=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){return this.#e.exec(e),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>t.get(...r),all:(...r)=>t.all(...r),iterate:(...r)=>typeof t.iterate=="function"?t.iterate(...r):t.all(...r)[Symbol.iterator]()}}transaction(e){return(...t)=>{this.#e.exec("BEGIN");try{let r=e(...t);return this.#e.exec("COMMIT"),r}catch(r){throw this.#e.exec("ROLLBACK"),r}}}close(){this.#e.close()}},m=null;function le(n){let e=null;try{return e=new n(":memory:"),e.exec("CREATE VIRTUAL TABLE __fts5_probe USING fts5(x)"),!0}catch{return!1}finally{try{e?.close()}catch{}}}function ge(n,e){let t=e!==void 0?e:globalThis.Bun;if(typeof t<"u"&&t!==null)return!0;let r=n??process.versions,[s,a]=(r.node??"0.0.0").split("."),i=Number(s),c=Number(a);return!Number.isFinite(i)||!Number.isFinite(c)?!1:i>22||i===22&&c>=5}function Ee(){if(!m){let n=ie(import.meta.url);if(globalThis.Bun){let e=n(["bun","sqlite"].join(":")).Database;m=function(r,s){let a=new e(r,{readonly:s?.readonly,create:!0}),i=new A(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}}else if(ge()){let e=null;try{({DatabaseSync:e}=n(["node","sqlite"].join(":")))}catch{e=null}e&&le(e)?m=function(r,s){let a=new e(r,{readOnly:s?.readonly??!1}),i=new w(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}:m=n("better-sqlite3")}else m=n("better-sqlite3")}return m}function F(n){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");try{n.pragma("mmap_size = 268435456")}catch{}}function k(n){if(!ae(n))for(let e of["-wal","-shm"])try{P(n+e)}catch{}}function me(n){for(let e of["","-wal","-shm"])try{P(n+e)}catch{}}function x(n){try{n.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{n.close()}catch{}}function B(n="context-mode"){return de(ue(),`${n}-${process.pid}.db`)}function _e(n,e=[100,500,2e3]){let t;for(let r=0;r<=e.length;r++)try{return n()}catch(s){let a=s instanceof Error?s.message:String(s);if(!a.includes("SQLITE_BUSY")&&!a.includes("database is locked"))throw s;if(t=s instanceof Error?s:new Error(a),r<e.length){let i=e[r],c=Date.now();for(;Date.now()-c<i;);}}throw new Error(`SQLITE_BUSY: database is locked after ${e.length} retries. Original error: ${t?.message}`)}function pe(n){return n.includes("SQLITE_CORRUPT")||n.includes("SQLITE_NOTADB")||n.includes("database disk image is malformed")||n.includes("file is not a database")}function ye(n){let e=Date.now();for(let t of["","-wal","-shm"])try{ce(n+t,`${n}${t}.corrupt-${e}`)}catch{}}var S=Symbol.for("__context_mode_live_dbs_v3__"),O=(()=>{let n=globalThis;return n[S]||(n[S]=new Set,process.on("exit",()=>{for(let e of n[S])x(e);n[S].clear()})),n[S]})(),R=class{#e;#t;constructor(e){let t=Ee();this.#e=e,k(e);let r;try{r=new t(e,{timeout:3e4}),F(r)}catch(s){let a=s instanceof Error?s.message:String(s);if(pe(a)){ye(e),k(e);try{r=new t(e,{timeout:3e4}),F(r)}catch(i){throw new Error(`Failed to create fresh DB after renaming corrupt file: ${i instanceof Error?i.message:String(i)}`)}}else throw s}this.#t=r,O.add(this.#t),this.initSchema(),this.prepareStatements()}get db(){return this.#t}get dbPath(){return this.#e}close(){O.delete(this.#t),x(this.#t)}withRetry(e){return _e(e)}cleanup(){O.delete(this.#t),x(this.#t),me(this.#e)}};import{createHash as f}from"node:crypto";import{execFileSync as Se}from"node:child_process";import{accessSync as fe,constants as Te,existsSync as L,mkdirSync as he,realpathSync as ve,renameSync as I}from"node:fs";import{homedir as q}from"node:os";import{dirname as Re,isAbsolute as G,join as g,resolve as p}from"node:path";var l="CONTEXT_MODE_DIR",Y="sessions",j="content",T=class extends Error{kind;path;overrideEnvVar;ignoredEnvVar;ignoredReason;constructor(e,t,r=l,s,a,i={}){super(a??Ce(e,t,i),{cause:s}),this.name="StorageDirectoryError",this.kind=e,this.path=t,this.overrideEnvVar=r,this.ignoredEnvVar=i.ignoredEnvVar,this.ignoredReason=i.ignoredReason}},D=new Map;function Ge(n){let e=n.env??process.env,t=n.legacySessionDirEnv,r=t?e[t]?.trim():void 0;return r&&t?(n.onLegacySessionDir?.(t,r),r):g(be(n.configDir,n.configDirEnv,e),"context-mode","sessions")}function be(n,e,t){let r=e?t[e]:void 0;return r&&r.trim()!==""?V(r.trim()):V(n,q())}function V(n,e){return n.startsWith("~")?p(q(),n.replace(/^~[/\\]?/,"")):G(n)?p(n):e?p(e,n):p(n)}function De(n,e,t){return new T(n,e,l,void 0,[`Invalid ${l} for context-mode ${n} directory: ${t}`,J()].join(`
|
|
2
|
+
`))}function K(n){let e=process.env[l];if(e===void 0)return{kind:"unset"};let t=e.trim();if(!t)return{kind:"ignored-empty",ignoredEnvVar:l,ignoredReason:"empty"};if(!G(t))throw De(n,t,`${l} must be an absolute path.`);return{kind:"override",root:p(t)}}function Le(n){return n.kind==="ignored-empty"?{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason}:{}}function z(n,e){let t=K(n);return t.kind!=="override"?null:{kind:n,path:g(t.root,e),envVar:l,source:"override"}}function Ne(n,e,t){return{kind:n,path:p(e()),envVar:null,source:"default",...t}}function Q(n){let e=K("session");return e.kind==="override"?{kind:"session",path:g(e.root,Y),envVar:l,source:"override"}:Ne("session",n,Le(e))}function Ye(n){let e=z("content",j);if(e)return e;let t=Q(n);return{kind:"content",path:g(Re(t.path),j),envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function Ke(n){let e=z("stats",Y);if(e)return e;let t=Q(n);return{kind:"stats",path:t.path,envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function ze(n){return n.message}function Qe(n){return n.source==="override"&&n.envVar?`via ${n.envVar}`:n.ignoredEnvVar&&n.ignoredReason==="empty"?`default; ignored empty ${n.ignoredEnvVar}`:"default"}function Je(){D.clear()}function Ze(n){let e=[n.kind,n.path,n.source,n.envVar??"",n.ignoredEnvVar??"",n.ignoredReason??""].join("\0"),t=D.get(e);if(t instanceof T)throw t;if(t===n.path)return t;try{return he(n.path,{recursive:!0}),fe(n.path,Te.W_OK),D.set(e,n.path),n.path}catch(r){let s=new T(n.kind,Ae(r)??n.path,l,r,void 0,{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason});throw D.set(e,s),s}}function Ce(n,e,t={}){return[`context-mode ${n} directory is not writable: ${e}`,Oe(t),J()].filter(Boolean).join(`
|
|
3
|
+
`)}function Oe(n){return n.ignoredEnvVar&&n.ignoredReason==="empty"?`Ignored empty ${n.ignoredEnvVar}; using adapter default.`:null}function J(){return`Set ${l} to a writable absolute path.`}function Ae(n){if(!n||typeof n!="object")return null;let e=n.path;return typeof e=="string"&&e.length>0?e:null}var _;function E(n){let e=n.replace(/\\/g,"/");return/^\/+$/.test(e)?"/":/^[A-Za-z]:\/+$/.test(e)?`${e.slice(0,2)}/`:e.replace(/\/+$/,"")}function H(n){let e=n;try{e=ve.native(n)}catch{}let t=E(e);return process.platform==="win32"||process.platform==="darwin"?t.toLowerCase():t}function Z(n,e){return Se("git",["-C",n,...e],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function we(n){let e=Z(n,["rev-parse","--show-toplevel"]);return e.length>0?E(e):null}function xe(n){let e=Z(n,["worktree","list","--porcelain"]).split(/\r?\n/).find(t=>t.startsWith("worktree "))?.replace("worktree ","")?.trim();return e?E(e):null}function Ie(n=process.cwd()){let e=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(_&&_.projectDir===n&&_.envSuffix===e)return _.suffix;let t="";if(e!==void 0)t=e?`__${e}`:"";else try{let r=we(n),s=xe(n);if(r&&s){let a=H(r),i=H(s);a!==i&&(t=`__${f("sha256").update(a).digest("hex").slice(0,8)}`)}}catch{}return _={projectDir:n,envSuffix:e,suffix:t},t}function et(){_=void 0}function ee(n){return f("sha256").update(E(n)).digest("hex").slice(0,16)}function te(n){let e=E(n),t=process.platform==="darwin"||process.platform==="win32"?e.toLowerCase():e;return f("sha256").update(t).digest("hex").slice(0,16)}function tt(n){let{projectDir:e,contentDir:t}=n,r=te(e),s=g(t,`${r}.db`);if(L(s))return s;let a=ee(e);if(a===r)return s;let i=g(t,`${a}.db`);if(L(i))try{I(i,s);for(let c of["-wal","-shm"])try{I(i+c,s+c)}catch{}}catch{}return s}function nt(n){return Ue({...n,ext:".db"})}function Ue(n){let{projectDir:e,sessionsDir:t,ext:r}=n,s=n.suffix??Ie(e),a=te(e),i=g(t,`${a}${s}${r}`);if(L(i))return i;let c=ee(e);if(c===a)return i;let d=g(t,`${c}${s}${r}`);if(L(d))try{I(d,i)}catch{}return i}var W=1e3,X=5;function b(n){let e=Number(n);return!Number.isFinite(e)||e<=0?0:Math.floor(e)}var o={insertEvent:"insertEvent",getEvents:"getEvents",getEventsByType:"getEventsByType",getEventsByPriority:"getEventsByPriority",getEventsByTypeAndPriority:"getEventsByTypeAndPriority",getEventCount:"getEventCount",getLatestAttributedProject:"getLatestAttributedProject",checkDuplicate:"checkDuplicate",evictLowestPriority:"evictLowestPriority",updateMetaLastEvent:"updateMetaLastEvent",ensureSession:"ensureSession",getSessionStats:"getSessionStats",getSessionRollup:"getSessionRollup",getMaxFileEdits:"getMaxFileEdits",getLatestCommitMessage:"getLatestCommitMessage",incrementCompactCount:"incrementCompactCount",getUsageCursor:"getUsageCursor",setUsageCursor:"setUsageCursor",upsertResume:"upsertResume",getResume:"getResume",markResumeConsumed:"markResumeConsumed",claimLatestUnconsumedResume:"claimLatestUnconsumedResume",deleteEvents:"deleteEvents",deleteMeta:"deleteMeta",deleteResume:"deleteResume",getOldSessions:"getOldSessions",searchEvents:"searchEvents",incrementToolCall:"incrementToolCall",getToolCallTotals:"getToolCallTotals",getToolCallByTool:"getToolCallByTool",getEventBytesSummary:"getEventBytesSummary"},Me=[["project_dir","TEXT NOT NULL DEFAULT ''"],["attribution_source","TEXT NOT NULL DEFAULT 'unknown'"],["attribution_confidence","REAL NOT NULL DEFAULT 0"],["bytes_avoided","INTEGER NOT NULL DEFAULT 0"],["bytes_returned","INTEGER NOT NULL DEFAULT 0"]];function ne(n){let e=n.pragma("table_xinfo(session_events)"),t=new Set(e.map(s=>s.name)),r=!1;for(let[s,a]of Me)t.has(s)||(n.exec(`ALTER TABLE session_events ADD COLUMN ${s} ${a}`),r=!0);return r&&n.exec("CREATE INDEX IF NOT EXISTS idx_session_events_project ON session_events(session_id, project_dir)"),r}function rt(n,e){let t=null;try{t=new e(n),ne(t)}catch{}finally{try{t?.close()}catch{}}}var $=class extends R{constructor(e){super(e?.dbPath??B("session"))}stmt(e){return this.stmts.get(e)}initSchema(){try{let t=this.db.pragma("table_xinfo(session_events)").find(r=>r.name==="data_hash");t&&t.hidden!==0&&this.db.exec("DROP TABLE session_events")}catch{}this.db.exec(`
|
|
4
4
|
CREATE TABLE IF NOT EXISTS session_events (
|
|
5
5
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
6
|
session_id TEXT NOT NULL,
|
|
@@ -50,7 +50,7 @@ import{createRequire as ie}from"node:module";import{existsSync as ae,unlinkSync
|
|
|
50
50
|
);
|
|
51
51
|
|
|
52
52
|
CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);
|
|
53
|
-
`);try{ne(this.db)}catch{}}prepareStatements(){this.stmts=new Map;let e=(t,r)=>{this.stmts.set(t,this.db.prepare(r))};e(o.insertEvent,`INSERT INTO session_events (
|
|
53
|
+
`);try{ne(this.db)}catch{}try{this.db.pragma("table_xinfo(session_meta)").some(t=>t.name==="usage_cursor")||this.db.exec("ALTER TABLE session_meta ADD COLUMN usage_cursor TEXT")}catch{}}prepareStatements(){this.stmts=new Map;let e=(t,r)=>{this.stmts.set(t,this.db.prepare(r))};e(o.insertEvent,`INSERT INTO session_events (
|
|
54
54
|
session_id, type, category, priority, data,
|
|
55
55
|
project_dir, attribution_source, attribution_confidence,
|
|
56
56
|
bytes_avoided, bytes_returned,
|
|
@@ -108,7 +108,7 @@ import{createRequire as ie}from"node:module";import{existsSync as ae,unlinkSync
|
|
|
108
108
|
FROM session_events
|
|
109
109
|
WHERE session_id = ? AND type = 'git_commit'
|
|
110
110
|
ORDER BY id DESC
|
|
111
|
-
LIMIT 1`),e(o.incrementCompactCount,"UPDATE session_meta SET compact_count = compact_count + 1 WHERE session_id = ?"),e(o.upsertResume,`INSERT INTO session_resume (session_id, snapshot, event_count)
|
|
111
|
+
LIMIT 1`),e(o.incrementCompactCount,"UPDATE session_meta SET compact_count = compact_count + 1 WHERE session_id = ?"),e(o.getUsageCursor,"SELECT usage_cursor FROM session_meta WHERE session_id = ?"),e(o.setUsageCursor,"UPDATE session_meta SET usage_cursor = ? WHERE session_id = ?"),e(o.upsertResume,`INSERT INTO session_resume (session_id, snapshot, event_count)
|
|
112
112
|
VALUES (?, ?, ?)
|
|
113
113
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
114
114
|
snapshot = excluded.snapshot,
|
|
@@ -139,6 +139,6 @@ import{createRequire as ie}from"node:module";import{existsSync as ae,unlinkSync
|
|
|
139
139
|
FROM tool_calls WHERE session_id = ?`),e(o.getToolCallByTool,`SELECT tool, calls, bytes_returned
|
|
140
140
|
FROM tool_calls WHERE session_id = ? ORDER BY calls DESC`),e(o.getEventBytesSummary,`SELECT COALESCE(SUM(bytes_avoided), 0) AS bytes_avoided,
|
|
141
141
|
COALESCE(SUM(bytes_returned), 0) AS bytes_returned
|
|
142
|
-
FROM session_events WHERE session_id = ?`)}insertEvent(e,t,r="PostToolUse",s,a){let i=f("sha256").update(t.data).digest("hex").slice(0,16).toUpperCase(),c=String(s?.projectDir??t.project_dir??this._getSessionProjectDir(e)).trim(),d=String(s?.source??t.attribution_source??"unknown"),u=Number(s?.confidence??t.attribution_confidence??0),
|
|
142
|
+
FROM session_events WHERE session_id = ?`)}insertEvent(e,t,r="PostToolUse",s,a){let i=f("sha256").update(t.data).digest("hex").slice(0,16).toUpperCase(),c=String(s?.projectDir??t.project_dir??this._getSessionProjectDir(e)).trim(),d=String(s?.source??t.attribution_source??"unknown"),u=Number(s?.confidence??t.attribution_confidence??0),h=Number.isFinite(u)?Math.max(0,Math.min(1,u)):0,y=b(a?.bytesAvoided),v=b(a?.bytesReturned),N=this.db.transaction(()=>{if(this.stmt(o.checkDuplicate).get(e,X,t.type,i))return;this.stmt(o.getEventCount).get(e).cnt>=W&&this.stmt(o.evictLowestPriority).run(e),this.stmt(o.insertEvent).run(e,t.type,t.category,t.priority,t.data,c,d,h,y,v,r,i),this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>N())}bulkInsertEvents(e,t,r="PostToolUse",s,a){if(!t||t.length===0)return;if(t.length===1){this.insertEvent(e,t[0],r,s?.[0],a?.[0]);return}let i=t.map((d,u)=>{let h=f("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),y=s?.[u],v=String(y?.projectDir??d.project_dir??this._getSessionProjectDir(e)??"").trim(),N=v===""?"":E(v),U=String(y?.source??d.attribution_source??"unknown"),C=Number(y?.confidence??d.attribution_confidence??0),re=Number.isFinite(C)?Math.max(0,Math.min(1,C)):0,M=a?.[u],se=b(M?.bytesAvoided),oe=b(M?.bytesReturned);return{event:d,dataHash:h,projectDir:N,attributionSource:U,attributionConfidence:re,bytesAvoided:se,bytesReturned:oe}}),c=this.db.transaction(()=>{let d=this.stmt(o.getEventCount).get(e).cnt;for(let u of i)this.stmt(o.checkDuplicate).get(e,X,u.event.type,u.dataHash)||(d>=W?this.stmt(o.evictLowestPriority).run(e):d++,this.stmt(o.insertEvent).run(e,u.event.type,u.event.category,u.event.priority,u.event.data,u.projectDir,u.attributionSource,u.attributionConfidence,u.bytesAvoided,u.bytesReturned,r,u.dataHash));this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>c())}getEvents(e,t){let r=t?.limit??1e3,s=t?.type,a=t?.minPriority;return s&&a!==void 0?this.stmt(o.getEventsByTypeAndPriority).all(e,s,a,r):s?this.stmt(o.getEventsByType).all(e,s,r):a!==void 0?this.stmt(o.getEventsByPriority).all(e,a,r):this.stmt(o.getEvents).all(e,r)}getEventCount(e){return this.stmt(o.getEventCount).get(e).cnt}getEventBytesSummary(e){let t=this.stmt(o.getEventBytesSummary).get(e);return{bytesAvoided:Number(t?.bytes_avoided??0),bytesReturned:Number(t?.bytes_returned??0)}}getLatestAttributedProjectDir(e){return this.stmt(o.getLatestAttributedProject).get(e)?.project_dir||null}_getSessionProjectDir(e){try{return this.db.prepare("SELECT project_dir FROM session_meta WHERE session_id = ?").get(e)?.project_dir||""}catch{return""}}searchEvents(e,t,r,s){try{let a=e.replace(/[%_]/g,c=>"\\"+c),i=s??null;return this.stmt(o.searchEvents).all(r,a,a,i,i,t)}catch{return[]}}getSessionIdsForProject(e){try{let t=E(e);return this.db.prepare(`SELECT DISTINCT session_id
|
|
143
143
|
FROM session_events
|
|
144
|
-
WHERE RTRIM(REPLACE(project_dir, '\\', '/'), '/') = ?`).all(t).map(s=>s.session_id)}catch{return[]}}ensureSession(e,t){this.stmt(o.ensureSession).run(e,t)}getSessionStats(e){return this.stmt(o.getSessionStats).get(e)??null}getSessionRollup(e){let t=this.stmt(o.getSessionRollup).get(e),r=this.stmt(o.getMaxFileEdits).get(e),s=this.stmt(o.getLatestCommitMessage).get(e),a=this.getSessionStats(e),i=(t?.tool_calls??0)>0?t?.unique_files??0:0,c=t?.errors??0,d=Math.min(i,c);return{tool_calls:t?.tool_calls??0,errors:t?.errors??0,unique_tools:t?.unique_tools??0,unique_files:t?.unique_files??0,max_file_edits:r?.max_file_edits??0,has_commit:t?.has_commit??0,commit_message:s?.data??"",edit_test_cycles:d,duration_min:t?.duration_min??0,compact_count:a?.compact_count??0,sources_indexed:t?.sources_indexed??0,total_chunks:t?.total_chunks??0,search_queries:t?.search_queries??0}}incrementCompactCount(e){this.stmt(o.incrementCompactCount).run(e)}upsertResume(e,t,r){this.stmt(o.upsertResume).run(e,t,r??0)}getResume(e){return this.stmt(o.getResume).get(e)??null}markResumeConsumed(e){this.stmt(o.markResumeConsumed).run(e)}claimLatestUnconsumedResume(e){let t=this.stmt(o.claimLatestUnconsumedResume).get(e);return t?{sessionId:t.session_id,snapshot:t.snapshot}:null}getLatestSessionId(){try{return this.db.prepare("SELECT session_id FROM session_meta ORDER BY started_at DESC LIMIT 1").get()?.session_id??null}catch{return null}}incrementToolCall(e,t,r=0){let s=Number.isFinite(r)&&r>0?Math.round(r):0;try{this.stmt(o.incrementToolCall).run(e,t,s)}catch{}}getToolCallStats(e){try{let t=this.stmt(o.getToolCallTotals).get(e),r=this.stmt(o.getToolCallByTool).all(e),s={};for(let a of r)s[a.tool]={calls:a.calls,bytesReturned:a.bytes_returned};return{totalCalls:t?.calls??0,totalBytesReturned:t?.bytes_returned??0,byTool:s}}catch{return{totalCalls:0,totalBytesReturned:0,byTool:{}}}}deleteSession(e){this.db.transaction(()=>{this.stmt(o.deleteEvents).run(e),this.stmt(o.deleteResume).run(e),this.stmt(o.deleteMeta).run(e)})()}cleanupOldSessions(e=7){let t=`-${e}`,r=this.stmt(o.getOldSessions).all(t);for(let{session_id:s}of r)this.deleteSession(s);return r.length}pruneOrphanedEvents(){let e=this.db.prepare("DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)").run();return Number(e.changes??0)}};export{$ as SessionDB,
|
|
144
|
+
WHERE RTRIM(REPLACE(project_dir, '\\', '/'), '/') = ?`).all(t).map(s=>s.session_id)}catch{return[]}}ensureSession(e,t){this.stmt(o.ensureSession).run(e,t)}getSessionStats(e){return this.stmt(o.getSessionStats).get(e)??null}getSessionRollup(e){let t=this.stmt(o.getSessionRollup).get(e),r=this.stmt(o.getMaxFileEdits).get(e),s=this.stmt(o.getLatestCommitMessage).get(e),a=this.getSessionStats(e),i=(t?.tool_calls??0)>0?t?.unique_files??0:0,c=t?.errors??0,d=Math.min(i,c);return{tool_calls:t?.tool_calls??0,errors:t?.errors??0,unique_tools:t?.unique_tools??0,unique_files:t?.unique_files??0,max_file_edits:r?.max_file_edits??0,has_commit:t?.has_commit??0,commit_message:s?.data??"",edit_test_cycles:d,duration_min:t?.duration_min??0,compact_count:a?.compact_count??0,sources_indexed:t?.sources_indexed??0,total_chunks:t?.total_chunks??0,search_queries:t?.search_queries??0}}incrementCompactCount(e){this.stmt(o.incrementCompactCount).run(e)}getUsageCursor(e){return this.stmt(o.getUsageCursor).get(e)?.usage_cursor??null}setUsageCursor(e,t){this.stmt(o.setUsageCursor).run(t,e)}upsertResume(e,t,r){this.stmt(o.upsertResume).run(e,t,r??0)}getResume(e){return this.stmt(o.getResume).get(e)??null}markResumeConsumed(e){this.stmt(o.markResumeConsumed).run(e)}claimLatestUnconsumedResume(e){let t=this.stmt(o.claimLatestUnconsumedResume).get(e);return t?{sessionId:t.session_id,snapshot:t.snapshot}:null}getLatestSessionId(){try{return this.db.prepare("SELECT session_id FROM session_meta ORDER BY started_at DESC LIMIT 1").get()?.session_id??null}catch{return null}}incrementToolCall(e,t,r=0){let s=Number.isFinite(r)&&r>0?Math.round(r):0;try{this.stmt(o.incrementToolCall).run(e,t,s)}catch{}}getToolCallStats(e){try{let t=this.stmt(o.getToolCallTotals).get(e),r=this.stmt(o.getToolCallByTool).all(e),s={};for(let a of r)s[a.tool]={calls:a.calls,bytesReturned:a.bytes_returned};return{totalCalls:t?.calls??0,totalBytesReturned:t?.bytes_returned??0,byTool:s}}catch{return{totalCalls:0,totalBytesReturned:0,byTool:{}}}}deleteSession(e){this.db.transaction(()=>{this.stmt(o.deleteEvents).run(e),this.stmt(o.deleteResume).run(e),this.stmt(o.deleteMeta).run(e)})()}cleanupOldSessions(e=7){let t=`-${e}`,r=this.stmt(o.getOldSessions).all(t);for(let{session_id:s}of r)this.deleteSession(s);return r.length}pruneOrphanedEvents(){let e=this.db.prepare("DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)").run();return Number(e.changes??0)}};export{$ as SessionDB,T as StorageDirectoryError,et as _resetWorktreeSuffixCacheForTests,ne as applyMissingSessionEventsColumns,Je as clearStorageDirectoryCheckCacheForTests,Qe as describeStorageDirectorySource,rt as ensureSessionEventsSchema,Ze as ensureWritableStorageDir,ze as formatStorageDirectoryError,Ie as getWorktreeSuffix,te as hashProjectDirCanonical,ee as hashProjectDirLegacy,E as normalizeWorktreePath,Ye as resolveContentStorageDir,tt as resolveContentStorePath,Ge as resolveDefaultSessionDir,nt as resolveSessionDbPath,Ue as resolveSessionPath,Q as resolveSessionStorageDir,Ke as resolveStatsStorageDir};
|