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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.6",
3
+ "version": "0.9.7-beta.1",
4
4
  "description": "Connect Claude Code, Codex, Cursor, or VS Code Copilot to Codiedev for org-wide session capture and artifact collaboration",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",