codiedev 0.9.6 → 0.9.7-beta.1
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/dist/hook.js +18 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.js +236 -0
- package/package.json +1 -1
package/dist/hook.js
CHANGED
|
@@ -252,6 +252,15 @@ async function main() {
|
|
|
252
252
|
? { messageCount: 0, toolCallCount: 0 }
|
|
253
253
|
: (0, utils_1.parseClaudeCodeStats)(transcriptContent);
|
|
254
254
|
const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
|
|
255
|
+
// Session→merge join keys: the branch + HEAD sha the work happened on. Matched
|
|
256
|
+
// server-side against a merged MR/PR's source_branch / commits for AI attribution.
|
|
257
|
+
const branch = (0, utils_1.getGitBranch)(workspaceRoot);
|
|
258
|
+
const headSha = (0, utils_1.getGitHeadSha)(workspaceRoot);
|
|
259
|
+
// Cost/velocity/steering signals parsed straight from the transcript we're
|
|
260
|
+
// already holding — no extra hooks, no extra git/process calls.
|
|
261
|
+
const usage = (0, utils_1.parseUsage)(transcriptContent, mode);
|
|
262
|
+
const sessionStartedAt = (0, utils_1.parseSessionStart)(transcriptContent);
|
|
263
|
+
const steering = (0, utils_1.parseSteering)(transcriptContent, mode);
|
|
255
264
|
const payload = (0, utils_1.buildCapturePayload)({
|
|
256
265
|
sessionId: session_id,
|
|
257
266
|
transcriptBase64,
|
|
@@ -262,6 +271,15 @@ async function main() {
|
|
|
262
271
|
toolCallCount: stats.toolCallCount,
|
|
263
272
|
capturedAt: Date.now(),
|
|
264
273
|
source: mode,
|
|
274
|
+
branch,
|
|
275
|
+
headSha,
|
|
276
|
+
inputTokens: usage.inputTokens,
|
|
277
|
+
outputTokens: usage.outputTokens,
|
|
278
|
+
cacheTokens: usage.cacheTokens,
|
|
279
|
+
model: usage.model,
|
|
280
|
+
sessionStartedAt,
|
|
281
|
+
interruptCount: steering.interrupts,
|
|
282
|
+
rejectionCount: steering.rejections,
|
|
265
283
|
});
|
|
266
284
|
// Fire the upload from a fully detached child and exit immediately. Awaiting
|
|
267
285
|
// it inline let Claude Code's SessionEnd shutdown window elapse and kill the
|
package/dist/utils.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export declare function matchRepo(remoteUrl: string, repos: Array<{
|
|
|
27
27
|
url: string;
|
|
28
28
|
fullName: string;
|
|
29
29
|
} | null;
|
|
30
|
+
export declare function getGitBranch(cwd: string): string | null;
|
|
31
|
+
export declare function getGitHeadSha(cwd: string): string | null;
|
|
30
32
|
export interface CapturePayloadInput {
|
|
31
33
|
sessionId?: string;
|
|
32
34
|
transcriptBase64: string;
|
|
@@ -37,6 +39,15 @@ export interface CapturePayloadInput {
|
|
|
37
39
|
toolCallCount: number;
|
|
38
40
|
capturedAt: number;
|
|
39
41
|
source: string;
|
|
42
|
+
branch?: string | null;
|
|
43
|
+
headSha?: string | null;
|
|
44
|
+
inputTokens?: number;
|
|
45
|
+
outputTokens?: number;
|
|
46
|
+
cacheTokens?: number;
|
|
47
|
+
model?: string | null;
|
|
48
|
+
sessionStartedAt?: number | null;
|
|
49
|
+
interruptCount?: number;
|
|
50
|
+
rejectionCount?: number;
|
|
40
51
|
}
|
|
41
52
|
export declare function buildCapturePayload(input: CapturePayloadInput): Record<string, unknown>;
|
|
42
53
|
export declare function hashToken(token: string): string;
|
|
@@ -115,6 +126,21 @@ export interface ParsedStats {
|
|
|
115
126
|
messageCount: number;
|
|
116
127
|
toolCallCount: number;
|
|
117
128
|
}
|
|
129
|
+
export interface SteeringStats {
|
|
130
|
+
interrupts: number;
|
|
131
|
+
rejections: number;
|
|
132
|
+
}
|
|
133
|
+
export declare function parseClaudeSteering(content: string): SteeringStats;
|
|
134
|
+
export declare function parseCodexSteering(content: string): SteeringStats;
|
|
135
|
+
export declare function parseSteering(content: string, mode: "claude-code" | "codex" | "cursor"): SteeringStats;
|
|
136
|
+
export interface UsageStats {
|
|
137
|
+
inputTokens: number;
|
|
138
|
+
outputTokens: number;
|
|
139
|
+
cacheTokens: number;
|
|
140
|
+
model: string | null;
|
|
141
|
+
}
|
|
142
|
+
export declare function parseUsage(content: string, mode: "claude-code" | "codex" | "cursor"): UsageStats;
|
|
143
|
+
export declare function parseSessionStart(content: string): number | null;
|
|
118
144
|
export declare function parseClaudeCodeStats(content: string): ParsedStats;
|
|
119
145
|
export declare function parseCodexStats(content: string): ParsedStats;
|
|
120
146
|
export declare function extractCodexRepoUrl(content: string): string | null;
|
package/dist/utils.js
CHANGED
|
@@ -40,6 +40,8 @@ exports.readGitRemoteFromConfig = readGitRemoteFromConfig;
|
|
|
40
40
|
exports.getGitRemoteUrl = getGitRemoteUrl;
|
|
41
41
|
exports.normalizeRepoUrl = normalizeRepoUrl;
|
|
42
42
|
exports.matchRepo = matchRepo;
|
|
43
|
+
exports.getGitBranch = getGitBranch;
|
|
44
|
+
exports.getGitHeadSha = getGitHeadSha;
|
|
43
45
|
exports.buildCapturePayload = buildCapturePayload;
|
|
44
46
|
exports.hashToken = hashToken;
|
|
45
47
|
exports.claudeCodeInstalled = claudeCodeInstalled;
|
|
@@ -63,6 +65,11 @@ exports.cleanupCodexMcp = cleanupCodexMcp;
|
|
|
63
65
|
exports.installCodexMcp = installCodexMcp;
|
|
64
66
|
exports.installCodexHook = installCodexHook;
|
|
65
67
|
exports.installCursorHook = installCursorHook;
|
|
68
|
+
exports.parseClaudeSteering = parseClaudeSteering;
|
|
69
|
+
exports.parseCodexSteering = parseCodexSteering;
|
|
70
|
+
exports.parseSteering = parseSteering;
|
|
71
|
+
exports.parseUsage = parseUsage;
|
|
72
|
+
exports.parseSessionStart = parseSessionStart;
|
|
66
73
|
exports.parseClaudeCodeStats = parseClaudeCodeStats;
|
|
67
74
|
exports.parseCodexStats = parseCodexStats;
|
|
68
75
|
exports.extractCodexRepoUrl = extractCodexRepoUrl;
|
|
@@ -247,17 +254,115 @@ function matchRepo(remoteUrl, repos) {
|
|
|
247
254
|
}
|
|
248
255
|
return null;
|
|
249
256
|
}
|
|
257
|
+
// Current branch via direct .git/HEAD read (no git binary required, same
|
|
258
|
+
// robustness as getGitRemoteUrl). Returns null on detached HEAD or when
|
|
259
|
+
// unresolvable. This is the session→merge join key: a captured session's
|
|
260
|
+
// branch is matched against a merged MR/PR's source_branch.
|
|
261
|
+
function getGitBranch(cwd) {
|
|
262
|
+
const gitDir = resolveGitDir(cwd);
|
|
263
|
+
if (gitDir) {
|
|
264
|
+
try {
|
|
265
|
+
const head = fs.readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
|
|
266
|
+
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
267
|
+
if (m)
|
|
268
|
+
return m[1].trim();
|
|
269
|
+
// raw sha => detached HEAD, no branch
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// fall through to git
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const b = (0, child_process_1.execSync)("git rev-parse --abbrev-ref HEAD", {
|
|
277
|
+
cwd,
|
|
278
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
279
|
+
})
|
|
280
|
+
.toString()
|
|
281
|
+
.trim();
|
|
282
|
+
return b && b !== "HEAD" ? b : null;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Current commit SHA (HEAD), direct-read first then git fallback. Exact join
|
|
289
|
+
// key — survives branch renames that branch-matching would miss.
|
|
290
|
+
function getGitHeadSha(cwd) {
|
|
291
|
+
const gitDir = resolveGitDir(cwd);
|
|
292
|
+
if (gitDir) {
|
|
293
|
+
try {
|
|
294
|
+
const head = fs.readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
|
|
295
|
+
if (/^[0-9a-f]{40}$/i.test(head))
|
|
296
|
+
return head.toLowerCase();
|
|
297
|
+
const m = head.match(/^ref:\s*(.+)$/);
|
|
298
|
+
if (m) {
|
|
299
|
+
const ref = m[1].trim();
|
|
300
|
+
for (const cfg of gitConfigCandidates(gitDir)) {
|
|
301
|
+
const baseDir = path.dirname(cfg);
|
|
302
|
+
try {
|
|
303
|
+
const sha = fs.readFileSync(path.join(baseDir, ref), "utf8").trim();
|
|
304
|
+
if (/^[0-9a-f]{40}$/i.test(sha))
|
|
305
|
+
return sha.toLowerCase();
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
/* try packed-refs */
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const packed = fs.readFileSync(path.join(baseDir, "packed-refs"), "utf8");
|
|
312
|
+
const line = packed
|
|
313
|
+
.split("\n")
|
|
314
|
+
.find((l) => l.endsWith(" " + ref));
|
|
315
|
+
if (line) {
|
|
316
|
+
const sha = line.split(" ")[0].trim();
|
|
317
|
+
if (/^[0-9a-f]{40}$/i.test(sha))
|
|
318
|
+
return sha.toLowerCase();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
/* ignore */
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// fall through to git
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const sha = (0, child_process_1.execSync)("git rev-parse HEAD", {
|
|
333
|
+
cwd,
|
|
334
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
335
|
+
})
|
|
336
|
+
.toString()
|
|
337
|
+
.trim();
|
|
338
|
+
return /^[0-9a-f]{40}$/i.test(sha) ? sha.toLowerCase() : null;
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
250
344
|
function buildCapturePayload(input) {
|
|
251
345
|
return {
|
|
252
346
|
sessionId: input.sessionId,
|
|
253
347
|
repoUrl: input.remoteUrl ?? "",
|
|
254
348
|
repoId: input.matchedRepoId,
|
|
255
349
|
companyId: input.companyId,
|
|
350
|
+
branch: input.branch ?? "",
|
|
351
|
+
headSha: input.headSha ?? "",
|
|
256
352
|
transcript: input.transcriptBase64,
|
|
257
353
|
messageCount: input.messageCount,
|
|
258
354
|
toolCallCount: input.toolCallCount,
|
|
259
355
|
capturedAt: input.capturedAt,
|
|
260
356
|
source: input.source,
|
|
357
|
+
// New capture signals (cost / velocity / steering). Optional — omitted
|
|
358
|
+
// (undefined) keys drop out of the JSON body, so older servers ignore them.
|
|
359
|
+
inputTokens: input.inputTokens,
|
|
360
|
+
outputTokens: input.outputTokens,
|
|
361
|
+
cacheTokens: input.cacheTokens,
|
|
362
|
+
model: input.model ?? undefined,
|
|
363
|
+
sessionStartedAt: input.sessionStartedAt ?? undefined,
|
|
364
|
+
interruptCount: input.interruptCount,
|
|
365
|
+
rejectionCount: input.rejectionCount,
|
|
261
366
|
};
|
|
262
367
|
}
|
|
263
368
|
function hashToken(token) {
|
|
@@ -941,6 +1046,137 @@ function installCursorHook() {
|
|
|
941
1046
|
hooks.sessionEnd = filtered;
|
|
942
1047
|
fs.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
|
|
943
1048
|
}
|
|
1049
|
+
function parseClaudeSteering(content) {
|
|
1050
|
+
let interrupts = 0;
|
|
1051
|
+
let rejections = 0;
|
|
1052
|
+
for (const line of content.split("\n")) {
|
|
1053
|
+
if (!line.trim())
|
|
1054
|
+
continue;
|
|
1055
|
+
// "[Request interrupted by user]" / "...for tool use" — Claude Code markers
|
|
1056
|
+
if (line.includes("[Request interrupted by user"))
|
|
1057
|
+
interrupts++;
|
|
1058
|
+
// Standard Claude Code tool-rejection sentinels in tool_result content
|
|
1059
|
+
if (line.includes("doesn't want to take this action") ||
|
|
1060
|
+
line.includes("user doesn't want to proceed"))
|
|
1061
|
+
rejections++;
|
|
1062
|
+
}
|
|
1063
|
+
return { interrupts, rejections };
|
|
1064
|
+
}
|
|
1065
|
+
function parseCodexSteering(content) {
|
|
1066
|
+
let interrupts = 0;
|
|
1067
|
+
let rejections = 0;
|
|
1068
|
+
for (const line of content.split("\n")) {
|
|
1069
|
+
if (!line.trim())
|
|
1070
|
+
continue;
|
|
1071
|
+
try {
|
|
1072
|
+
const e = JSON.parse(line);
|
|
1073
|
+
const pt = e?.payload?.type;
|
|
1074
|
+
if (e?.type === "event_msg" && pt === "turn_aborted")
|
|
1075
|
+
interrupts++;
|
|
1076
|
+
if (e?.type === "response_item" && pt === "function_call_output") {
|
|
1077
|
+
const out = String(e.payload?.output ?? "");
|
|
1078
|
+
if (/\b(rejected|denied|not approved)\b/i.test(out))
|
|
1079
|
+
rejections++;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
catch {
|
|
1083
|
+
// skip malformed
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return { interrupts, rejections };
|
|
1087
|
+
}
|
|
1088
|
+
function parseSteering(content, mode) {
|
|
1089
|
+
return mode === "codex"
|
|
1090
|
+
? parseCodexSteering(content)
|
|
1091
|
+
: parseClaudeSteering(content);
|
|
1092
|
+
}
|
|
1093
|
+
function parseClaudeUsage(content) {
|
|
1094
|
+
// Claude Code logs per-turn usage; sum across the session. `input_tokens` is
|
|
1095
|
+
// already fresh (cache read/creation are reported separately).
|
|
1096
|
+
let inputTokens = 0;
|
|
1097
|
+
let outputTokens = 0;
|
|
1098
|
+
let cacheTokens = 0;
|
|
1099
|
+
let model = null;
|
|
1100
|
+
for (const line of content.split("\n")) {
|
|
1101
|
+
if (!line.trim())
|
|
1102
|
+
continue;
|
|
1103
|
+
try {
|
|
1104
|
+
const e = JSON.parse(line);
|
|
1105
|
+
const m = e?.message;
|
|
1106
|
+
const u = m?.usage;
|
|
1107
|
+
if (u) {
|
|
1108
|
+
inputTokens += u.input_tokens ?? 0;
|
|
1109
|
+
outputTokens += u.output_tokens ?? 0;
|
|
1110
|
+
cacheTokens += (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0);
|
|
1111
|
+
}
|
|
1112
|
+
if (typeof m?.model === "string" && m.model)
|
|
1113
|
+
model = m.model;
|
|
1114
|
+
}
|
|
1115
|
+
catch {
|
|
1116
|
+
// skip malformed
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return { inputTokens, outputTokens, cacheTokens, model };
|
|
1120
|
+
}
|
|
1121
|
+
function parseCodexUsage(content) {
|
|
1122
|
+
// Codex reports a CUMULATIVE token_count per turn — the last one is the
|
|
1123
|
+
// session total. `input_tokens` includes the cached portion, so subtract it.
|
|
1124
|
+
let model = null;
|
|
1125
|
+
let last = null;
|
|
1126
|
+
for (const line of content.split("\n")) {
|
|
1127
|
+
if (!line.trim())
|
|
1128
|
+
continue;
|
|
1129
|
+
try {
|
|
1130
|
+
const e = JSON.parse(line);
|
|
1131
|
+
if (e?.type === "turn_context" && typeof e.payload?.model === "string" && e.payload.model) {
|
|
1132
|
+
model = e.payload.model;
|
|
1133
|
+
}
|
|
1134
|
+
if (e?.type === "event_msg" && e.payload?.type === "token_count") {
|
|
1135
|
+
const t = e.payload?.info?.total_token_usage;
|
|
1136
|
+
if (t)
|
|
1137
|
+
last = t;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
catch {
|
|
1141
|
+
// skip malformed
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (!last)
|
|
1145
|
+
return { inputTokens: 0, outputTokens: 0, cacheTokens: 0, model };
|
|
1146
|
+
const cached = last.cached_input_tokens ?? 0;
|
|
1147
|
+
const input = last.input_tokens ?? 0;
|
|
1148
|
+
return {
|
|
1149
|
+
inputTokens: Math.max(0, input - cached),
|
|
1150
|
+
outputTokens: (last.output_tokens ?? 0) + (last.reasoning_output_tokens ?? 0),
|
|
1151
|
+
cacheTokens: cached,
|
|
1152
|
+
model,
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function parseUsage(content, mode) {
|
|
1156
|
+
return mode === "codex" ? parseCodexUsage(content) : parseClaudeUsage(content);
|
|
1157
|
+
}
|
|
1158
|
+
// Lead-time start proxy: the earliest top-level `timestamp` in the transcript.
|
|
1159
|
+
// Both Claude Code and Codex stamp every line, so this is mode-agnostic.
|
|
1160
|
+
function parseSessionStart(content) {
|
|
1161
|
+
let min = null;
|
|
1162
|
+
for (const line of content.split("\n")) {
|
|
1163
|
+
if (!line.trim())
|
|
1164
|
+
continue;
|
|
1165
|
+
try {
|
|
1166
|
+
const e = JSON.parse(line);
|
|
1167
|
+
const ts = e?.timestamp;
|
|
1168
|
+
if (typeof ts === "string") {
|
|
1169
|
+
const ms = Date.parse(ts);
|
|
1170
|
+
if (!Number.isNaN(ms) && (min === null || ms < min))
|
|
1171
|
+
min = ms;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
catch {
|
|
1175
|
+
// skip malformed
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return min;
|
|
1179
|
+
}
|
|
944
1180
|
function parseClaudeCodeStats(content) {
|
|
945
1181
|
let messageCount = 0;
|
|
946
1182
|
let toolCallCount = 0;
|
package/package.json
CHANGED