@gleapai/kai-bridge 0.2.0
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/README.md +45 -0
- package/bin/kai-bridge.mjs +275 -0
- package/package.json +47 -0
- package/runner/acp-runner.mjs +671 -0
- package/runner/lib/acp/harnesses.mjs +342 -0
- package/runner/lib/acp/mapper.mjs +575 -0
- package/runner/lib/acp/transcripts.mjs +238 -0
- package/runner/lib/contract.mjs +1122 -0
- package/runner/lib/wire-proxy.mjs +200 -0
- package/runner/personas/claude/kai-asker.md +69 -0
- package/runner/personas/claude/kai-doc-explorer.md +130 -0
- package/runner/personas/claude/kai-documentarian.md +205 -0
- package/runner/personas/claude/kai-researcher.md +68 -0
- package/runner/personas/claude/kai-resolution-analyst.md +164 -0
- package/runner/personas/codex/kai-asker.md +68 -0
- package/runner/personas/codex/kai-doc-explorer.md +130 -0
- package/runner/personas/codex/kai-documentarian.md +211 -0
- package/runner/personas/codex/kai-researcher.md +67 -0
- package/runner/personas/codex/kai-resolution-analyst.md +164 -0
- package/runner/tools/ask-user-mcp.mjs +130 -0
- package/runner/tools/todo-mcp.mjs +116 -0
- package/scripts/postinstall.mjs +24 -0
- package/src/api.mjs +141 -0
- package/src/config.mjs +76 -0
- package/src/daemon.mjs +904 -0
- package/src/executor.mjs +156 -0
- package/src/harnesses.mjs +252 -0
- package/src/preview.mjs +337 -0
- package/src/profiles.mjs +250 -0
- package/src/repos.mjs +182 -0
- package/src/service.mjs +162 -0
- package/src/setup.mjs +261 -0
- package/src/workspace.mjs +175 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Per-request token usage from the harness's OWN transcript.
|
|
2
|
+
//
|
|
3
|
+
// ACP's `usage_update` is a context gauge (`used`/`size`) plus the
|
|
4
|
+
// native CLI's cost figure — it carries no per-model
|
|
5
|
+
// input / cache-read / cache-write / output split, and Kai Code bills
|
|
6
|
+
// gateway models from exactly that split (registry `modelPricing`).
|
|
7
|
+
// Every harness persists it though: Claude Code writes one JSONL line
|
|
8
|
+
// per streamed assistant message (with `usage` + `model`) under
|
|
9
|
+
// `<CLAUDE_CONFIG_DIR>/projects/<cwd-slug>/<sessionId>.jsonl`; Codex
|
|
10
|
+
// writes `token_count` events into its rollout JSONL under
|
|
11
|
+
// `<CODEX_HOME>/sessions/…/rollout-*-<threadId>.jsonl`. Reading those
|
|
12
|
+
// after the turn gives billing parity with the stream-json runners
|
|
13
|
+
// without forking the adapters.
|
|
14
|
+
//
|
|
15
|
+
// Pure parsers + thin fs locators; tests feed strings.
|
|
16
|
+
|
|
17
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Claude Code project slug: the cwd with every `/` and `.` replaced by
|
|
24
|
+
* `-` (verified against 2.1.x: `/home/user/repos` → `-home-user-repos`).
|
|
25
|
+
*/
|
|
26
|
+
export function claudeProjectSlug(cwd) {
|
|
27
|
+
return String(cwd || "").replace(/[/.]/g, "-");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function claudeTranscriptPath(configDir, cwd, sessionId) {
|
|
31
|
+
return join(configDir, "projects", claudeProjectSlug(cwd), `${sessionId}.jsonl`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a Claude Code session JSONL into per-request usage rows.
|
|
36
|
+
*
|
|
37
|
+
* Streaming writes several lines per assistant message (one per content
|
|
38
|
+
* block) that all repeat the SAME `message.id` + `usage`; keep one row
|
|
39
|
+
* per id (the last wins — it carries the final output count). Rows are
|
|
40
|
+
* returned in file order; `sinceTs` (ISO / epoch ms) drops earlier
|
|
41
|
+
* turns so a resume only bills the new requests.
|
|
42
|
+
*/
|
|
43
|
+
export function parseClaudeTranscript(text, { sinceTs } = {}) {
|
|
44
|
+
const since = sinceTs ? new Date(sinceTs).getTime() : 0;
|
|
45
|
+
const byId = new Map();
|
|
46
|
+
for (const line of String(text || "").split(/\r?\n/)) {
|
|
47
|
+
const t = line.trim();
|
|
48
|
+
if (!t) continue;
|
|
49
|
+
let obj;
|
|
50
|
+
try {
|
|
51
|
+
obj = JSON.parse(t);
|
|
52
|
+
} catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (obj?.type !== "assistant" || !obj.message || typeof obj.message !== "object") continue;
|
|
56
|
+
const ts = obj.timestamp ? new Date(obj.timestamp).getTime() : 0;
|
|
57
|
+
if (since && ts && ts < since) continue;
|
|
58
|
+
const usage = obj.message.usage ?? {};
|
|
59
|
+
const id = String(obj.message.id ?? obj.uuid ?? byId.size);
|
|
60
|
+
byId.set(id, {
|
|
61
|
+
id,
|
|
62
|
+
model: String(obj.message.model ?? ""),
|
|
63
|
+
inputTokens: num(usage.input_tokens),
|
|
64
|
+
cachedInputTokens: num(usage.cache_read_input_tokens),
|
|
65
|
+
cacheWriteInputTokens: num(usage.cache_creation_input_tokens),
|
|
66
|
+
outputTokens: num(usage.output_tokens),
|
|
67
|
+
// `isSidechain` marks subagent transcripts; parentUuid chains the
|
|
68
|
+
// root. Kept so callers can pick the ROOT context snapshot.
|
|
69
|
+
sidechain: obj.isSidechain === true,
|
|
70
|
+
timestamp: ts || null,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return [...byId.values()];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Aggregate rows per model in the runner tracker's shape. */
|
|
77
|
+
export function aggregateUsageRows(rows) {
|
|
78
|
+
const byModel = new Map();
|
|
79
|
+
for (const r of rows) {
|
|
80
|
+
if (!r.model) continue;
|
|
81
|
+
const t = byModel.get(r.model) ?? {
|
|
82
|
+
model: r.model,
|
|
83
|
+
inputTokens: 0,
|
|
84
|
+
cachedInputTokens: 0,
|
|
85
|
+
cacheWriteInputTokens: 0,
|
|
86
|
+
outputTokens: 0,
|
|
87
|
+
requests: 0,
|
|
88
|
+
};
|
|
89
|
+
t.inputTokens += r.inputTokens;
|
|
90
|
+
t.cachedInputTokens += r.cachedInputTokens;
|
|
91
|
+
t.cacheWriteInputTokens += r.cacheWriteInputTokens;
|
|
92
|
+
t.outputTokens += r.outputTokens;
|
|
93
|
+
t.requests += 1;
|
|
94
|
+
byModel.set(r.model, t);
|
|
95
|
+
}
|
|
96
|
+
return [...byModel.values()];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Context occupancy of the LAST root request = fresh + cache read +
|
|
101
|
+
* cache write of that single request (Anthropic's `input_tokens`
|
|
102
|
+
* excludes the cache buckets). Mirrors `noteContextSnapshot` in the
|
|
103
|
+
* claude runner.
|
|
104
|
+
*/
|
|
105
|
+
export function lastRootContextSnapshot(rows) {
|
|
106
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
107
|
+
const r = rows[i];
|
|
108
|
+
if (r.sidechain) continue;
|
|
109
|
+
const tokens = r.inputTokens + r.cachedInputTokens + r.cacheWriteInputTokens;
|
|
110
|
+
if (tokens > 0) return { model: r.model, tokens };
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Locate the session transcript. The CLI keys the project dir by the
|
|
117
|
+
* REAL path of cwd (`/private/var/…` on macOS for a `/var/…` tmpdir), so
|
|
118
|
+
* try the path as given, its realpath, and finally any project dir that
|
|
119
|
+
* holds `<sessionId>.jsonl` (session ids are unique per config dir).
|
|
120
|
+
*/
|
|
121
|
+
export function findClaudeTranscript(configDir, cwd, sessionId) {
|
|
122
|
+
const candidates = [cwd];
|
|
123
|
+
try {
|
|
124
|
+
const real = realpathSync(cwd);
|
|
125
|
+
if (real !== cwd) candidates.push(real);
|
|
126
|
+
} catch {
|
|
127
|
+
/* cwd gone */
|
|
128
|
+
}
|
|
129
|
+
for (const c of candidates) {
|
|
130
|
+
const p = claudeTranscriptPath(configDir, c, sessionId);
|
|
131
|
+
if (existsSync(p)) return p;
|
|
132
|
+
}
|
|
133
|
+
const projects = join(configDir, "projects");
|
|
134
|
+
try {
|
|
135
|
+
for (const dir of readdirSync(projects)) {
|
|
136
|
+
const p = join(projects, dir, `${sessionId}.jsonl`);
|
|
137
|
+
if (existsSync(p)) return p;
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
/* no projects dir yet */
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function readClaudeTurnUsage({ configDir, cwd, sessionId, sinceTs }) {
|
|
146
|
+
const path = findClaudeTranscript(configDir, cwd, sessionId);
|
|
147
|
+
if (!path) return { path: claudeTranscriptPath(configDir, cwd, sessionId), rows: [] };
|
|
148
|
+
return { path, rows: parseClaudeTranscript(readFileSync(path, "utf8"), { sinceTs }) };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Codex ──────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Codex rollout `token_count` events report `last_token_usage` (the
|
|
155
|
+
* most recent request) and a running `total_token_usage`. The per-
|
|
156
|
+
* request rows are the `last_token_usage` deltas; `cached_input_tokens`
|
|
157
|
+
* is INCLUDED in `input_tokens` (OpenAI convention — the tracker treats
|
|
158
|
+
* `inputTokens` as cache-inclusive too, so pass it through unchanged).
|
|
159
|
+
*/
|
|
160
|
+
export function parseCodexRollout(text, { sinceTs, model } = {}) {
|
|
161
|
+
const since = sinceTs ? new Date(sinceTs).getTime() : 0;
|
|
162
|
+
const rows = [];
|
|
163
|
+
let contextWindow = null;
|
|
164
|
+
let activeModel = model || "";
|
|
165
|
+
for (const line of String(text || "").split(/\r?\n/)) {
|
|
166
|
+
const t = line.trim();
|
|
167
|
+
if (!t) continue;
|
|
168
|
+
let obj;
|
|
169
|
+
try {
|
|
170
|
+
obj = JSON.parse(t);
|
|
171
|
+
} catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const ts = obj.timestamp ? new Date(obj.timestamp).getTime() : 0;
|
|
175
|
+
if (obj.type === "turn_context" && obj.payload?.model) {
|
|
176
|
+
activeModel = String(obj.payload.model);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (obj.type !== "event_msg" || obj.payload?.type !== "token_count") continue;
|
|
180
|
+
if (since && ts && ts < since) continue;
|
|
181
|
+
const info = obj.payload.info ?? {};
|
|
182
|
+
const last = info.last_token_usage ?? {};
|
|
183
|
+
if (typeof info.model_context_window === "number" && info.model_context_window > 0) {
|
|
184
|
+
contextWindow = info.model_context_window;
|
|
185
|
+
}
|
|
186
|
+
const input = num(last.input_tokens);
|
|
187
|
+
if (input <= 0 && num(last.output_tokens) <= 0) continue;
|
|
188
|
+
rows.push({
|
|
189
|
+
id: `${ts || rows.length}`,
|
|
190
|
+
model: activeModel,
|
|
191
|
+
inputTokens: input,
|
|
192
|
+
cachedInputTokens: num(last.cached_input_tokens),
|
|
193
|
+
cacheWriteInputTokens: 0,
|
|
194
|
+
outputTokens: num(last.output_tokens) + num(last.reasoning_output_tokens),
|
|
195
|
+
sidechain: false,
|
|
196
|
+
timestamp: ts || null,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return { rows, contextWindow };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Newest rollout file for a thread id under CODEX_HOME/sessions. */
|
|
203
|
+
export function findCodexRollout(codexHome, threadId) {
|
|
204
|
+
const root = join(codexHome, "sessions");
|
|
205
|
+
if (!existsSync(root)) return null;
|
|
206
|
+
let best = null;
|
|
207
|
+
const walk = (dir, depth) => {
|
|
208
|
+
let entries;
|
|
209
|
+
try {
|
|
210
|
+
entries = readdirSync(dir);
|
|
211
|
+
} catch {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
for (const name of entries) {
|
|
215
|
+
const p = join(dir, name);
|
|
216
|
+
let st;
|
|
217
|
+
try {
|
|
218
|
+
st = statSync(p);
|
|
219
|
+
} catch {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (st.isDirectory()) {
|
|
223
|
+
if (depth < 4) walk(p, depth + 1);
|
|
224
|
+
} else if (name.startsWith("rollout-") && name.endsWith(".jsonl") && name.includes(threadId)) {
|
|
225
|
+
if (!best || st.mtimeMs > best.mtimeMs) best = { path: p, mtimeMs: st.mtimeMs };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
walk(root, 0);
|
|
230
|
+
return best?.path ?? null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function readCodexTurnUsage({ codexHome, threadId, sinceTs, model }) {
|
|
234
|
+
const path = findCodexRollout(codexHome, threadId);
|
|
235
|
+
if (!path) return { path: null, rows: [], contextWindow: null };
|
|
236
|
+
const parsed = parseCodexRollout(readFileSync(path, "utf8"), { sinceTs, model });
|
|
237
|
+
return { path, ...parsed };
|
|
238
|
+
}
|