@jagit/hook-copilot 0.0.4 → 0.0.6

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/index.d.ts CHANGED
@@ -65,6 +65,7 @@ interface ModelUsageBucket {
65
65
  cachedInputTokens: number;
66
66
  outputTokens: number;
67
67
  totalTokens: number;
68
+ costUsd: number;
68
69
  observations: number;
69
70
  }
70
71
  interface CopilotDebugUsage {
@@ -75,11 +76,17 @@ interface CopilotDebugUsage {
75
76
  cachedInputTokens: number;
76
77
  outputTokens: number;
77
78
  totalTokens: number;
79
+ costUsd: number | null;
78
80
  sourcePath: string;
79
81
  modelUsage: Record<string, ModelUsageBucket>;
80
82
  }
81
83
  export declare function inferWorkspaceIdBySession(sessionId: string, hookTimestamp?: string, baseDir?: string): WorkspaceCandidate | undefined;
82
- export declare function resolveDebugUsageBySession(sessionId: string | undefined, hookTimestamp?: string, baseDir?: string): CopilotDebugUsage | undefined;
84
+ interface TranscriptPathLocation {
85
+ workspaceId: string;
86
+ sessionId: string;
87
+ }
88
+ export declare function parseTranscriptPathLocation(transcriptPath: string | undefined): TranscriptPathLocation | undefined;
89
+ export declare function resolveDebugUsageBySession(sessionId: string | undefined, hookTimestamp?: string, baseDir?: string, workspaceIdFromTranscript?: string): CopilotDebugUsage | undefined;
83
90
  /**
84
91
  * Build payload from a real VS Code Copilot agent Stop-hook stdin.
85
92
  *
@@ -88,7 +95,7 @@ export declare function resolveDebugUsageBySession(sessionId: string | undefined
88
95
  * VS Code Copilot transcript — Copilot uses seat-based billing and does not
89
96
  * expose per-call telemetry. These fields are reported as 0/null/"copilot".
90
97
  */
91
- export declare function buildPayloadFromStdin(stdin: CopilotStopStdin, read?: (path: string) => CopilotTranscriptEntry[], resolveUsage?: (sessionId: string | undefined, hookTimestamp?: string) => CopilotDebugUsage | undefined): AgentSessionPayload;
98
+ export declare function buildPayloadFromStdin(stdin: CopilotStopStdin, read?: (path: string) => CopilotTranscriptEntry[], resolveUsage?: (sessionId: string | undefined, hookTimestamp?: string, baseDir?: string, workspaceIdFromTranscript?: string) => CopilotDebugUsage | undefined): AgentSessionPayload;
92
99
  /**
93
100
  * Build payload in legacy mode (no stdin) — used when hook-copilot is invoked
94
101
  * via the old shell-wrapper pattern around the Copilot CLI. Token counts are
package/dist/index.js CHANGED
@@ -93,6 +93,23 @@ export function inferWorkspaceIdBySession(sessionId, hookTimestamp, baseDir = WO
93
93
  }
94
94
  return candidates[0];
95
95
  }
96
+ export function parseTranscriptPathLocation(transcriptPath) {
97
+ if (!transcriptPath) {
98
+ return undefined;
99
+ }
100
+ const normalized = transcriptPath.replace(/\\/g, "/");
101
+ const match = normalized.match(/\/workspaceStorage\/([^/]+)\/GitHub\.copilot-chat\/transcripts\/([^/]+)\.jsonl$/);
102
+ if (!match) {
103
+ return undefined;
104
+ }
105
+ return {
106
+ workspaceId: match[1],
107
+ sessionId: match[2],
108
+ };
109
+ }
110
+ function buildMainLogPath(baseDir, workspaceId, sessionId) {
111
+ return join(baseDir, workspaceId, "GitHub.copilot-chat", "debug-logs", sessionId, "main.jsonl");
112
+ }
96
113
  function resolveModelFromObject(obj, fallbackModel) {
97
114
  const keys = ["model", "modelName", "model_name", "resolvedModel", "deployment", "engine"];
98
115
  for (const key of keys) {
@@ -108,7 +125,11 @@ function extractTokensFromObject(obj) {
108
125
  let cachedInputTokens = 0;
109
126
  let outputTokens = 0;
110
127
  let totalTokens = 0;
128
+ let costUsd = 0;
111
129
  let foundAny = false;
130
+ let sawInputKey = false;
131
+ let sawCacheReadStyleKey = false;
132
+ let sawCachedSubsetKey = false;
112
133
  for (const [rawKey, rawValue] of Object.entries(obj)) {
113
134
  const key = normalizeKey(rawKey);
114
135
  const value = toFiniteNumber(rawValue);
@@ -118,11 +139,19 @@ function extractTokensFromObject(obj) {
118
139
  if (["inputtokens", "prompttokens", "requesttokens"].includes(key)) {
119
140
  inputTokens += value;
120
141
  foundAny = true;
142
+ sawInputKey = true;
143
+ continue;
144
+ }
145
+ if (["cachedinputtokens", "cachereadinputtokens"].includes(key)) {
146
+ cachedInputTokens += value;
147
+ foundAny = true;
148
+ sawCacheReadStyleKey = true;
121
149
  continue;
122
150
  }
123
- if (["cachedinputtokens", "cachedtokens", "cachereadinputtokens", "promptcachehitstokens"].includes(key)) {
151
+ if (["cachedtokens", "promptcachehitstokens"].includes(key)) {
124
152
  cachedInputTokens += value;
125
153
  foundAny = true;
154
+ sawCachedSubsetKey = true;
126
155
  continue;
127
156
  }
128
157
  if (["outputtokens", "completiontokens", "responsetokens"].includes(key)) {
@@ -135,14 +164,25 @@ function extractTokensFromObject(obj) {
135
164
  foundAny = true;
136
165
  continue;
137
166
  }
167
+ if (["costusd", "cost", "usd", "usdcost"].includes(key)) {
168
+ costUsd += value;
169
+ foundAny = true;
170
+ continue;
171
+ }
138
172
  }
139
173
  if (!foundAny) {
140
174
  return null;
141
175
  }
176
+ // Some providers report inputTokens as total prompt tokens and expose cached
177
+ // tokens as a subset. Convert to non-cached input tokens to match JaGit's
178
+ // input/cached split and avoid double-counting in aggregates.
179
+ if (sawInputKey && sawCachedSubsetKey && !sawCacheReadStyleKey) {
180
+ inputTokens = Math.max(0, inputTokens - cachedInputTokens);
181
+ }
142
182
  if (totalTokens === 0) {
143
- totalTokens = inputTokens + outputTokens;
183
+ totalTokens = inputTokens + cachedInputTokens + outputTokens;
144
184
  }
145
- return { inputTokens, cachedInputTokens, outputTokens, totalTokens };
185
+ return { inputTokens, cachedInputTokens, outputTokens, totalTokens, costUsd };
146
186
  }
147
187
  function collectModelUsage(value, usageByModel, currentModel = "copilot") {
148
188
  if (Array.isArray(value)) {
@@ -162,12 +202,14 @@ function collectModelUsage(value, usageByModel, currentModel = "copilot") {
162
202
  cachedInputTokens: 0,
163
203
  outputTokens: 0,
164
204
  totalTokens: 0,
205
+ costUsd: 0,
165
206
  observations: 0,
166
207
  };
167
208
  existing.inputTokens += tokens.inputTokens;
168
209
  existing.cachedInputTokens += tokens.cachedInputTokens;
169
210
  existing.outputTokens += tokens.outputTokens;
170
211
  existing.totalTokens += tokens.totalTokens;
212
+ existing.costUsd += tokens.costUsd;
171
213
  existing.observations += 1;
172
214
  usageByModel.set(resolvedModel, existing);
173
215
  }
@@ -175,17 +217,29 @@ function collectModelUsage(value, usageByModel, currentModel = "copilot") {
175
217
  collectModelUsage(child, usageByModel, resolvedModel);
176
218
  }
177
219
  }
178
- export function resolveDebugUsageBySession(sessionId, hookTimestamp, baseDir = WORKSPACE_STORAGE_DIR) {
220
+ export function resolveDebugUsageBySession(sessionId, hookTimestamp, baseDir = WORKSPACE_STORAGE_DIR, workspaceIdFromTranscript) {
179
221
  if (!sessionId) {
180
222
  return undefined;
181
223
  }
182
- const workspace = inferWorkspaceIdBySession(sessionId, hookTimestamp, baseDir);
183
- if (!workspace) {
184
- return undefined;
224
+ let workspaceId = workspaceIdFromTranscript;
225
+ let mainJsonlPath;
226
+ if (workspaceId) {
227
+ const pathFromTranscript = buildMainLogPath(baseDir, workspaceId, sessionId);
228
+ if (existsSync(pathFromTranscript)) {
229
+ mainJsonlPath = pathFromTranscript;
230
+ }
185
231
  }
186
- const mainJsonlPath = join(workspace.debugLogsDir, sessionId, "main.jsonl");
187
- if (!existsSync(mainJsonlPath)) {
188
- return undefined;
232
+ if (!mainJsonlPath) {
233
+ const workspace = inferWorkspaceIdBySession(sessionId, hookTimestamp, baseDir);
234
+ if (!workspace) {
235
+ return undefined;
236
+ }
237
+ workspaceId = workspace.workspaceId;
238
+ const fallbackPath = buildMainLogPath(baseDir, workspace.workspaceId, sessionId);
239
+ if (!existsSync(fallbackPath)) {
240
+ return undefined;
241
+ }
242
+ mainJsonlPath = fallbackPath;
189
243
  }
190
244
  const usageByModel = new Map();
191
245
  const lines = readFileSync(mainJsonlPath, "utf-8").split("\n");
@@ -205,12 +259,13 @@ export function resolveDebugUsageBySession(sessionId, hookTimestamp, baseDir = W
205
259
  if (usageByModel.size === 0) {
206
260
  return {
207
261
  sessionId,
208
- workspaceId: workspace.workspaceId,
262
+ workspaceId: workspaceId ?? "unknown",
209
263
  model: "copilot",
210
264
  inputTokens: 0,
211
265
  cachedInputTokens: 0,
212
266
  outputTokens: 0,
213
267
  totalTokens: 0,
268
+ costUsd: null,
214
269
  sourcePath: mainJsonlPath,
215
270
  modelUsage: {},
216
271
  };
@@ -222,11 +277,13 @@ export function resolveDebugUsageBySession(sessionId, hookTimestamp, baseDir = W
222
277
  let cachedInputTokens = 0;
223
278
  let outputTokens = 0;
224
279
  let totalTokens = 0;
280
+ let totalCostUsd = 0;
225
281
  for (const [model, usage] of usageByModel.entries()) {
226
282
  inputTokens += usage.inputTokens;
227
283
  cachedInputTokens += usage.cachedInputTokens;
228
284
  outputTokens += usage.outputTokens;
229
285
  totalTokens += usage.totalTokens;
286
+ totalCostUsd += usage.costUsd;
230
287
  if (usage.totalTokens > dominantTotal || (usage.totalTokens === dominantTotal && usage.observations > dominantObs)) {
231
288
  dominantModel = model;
232
289
  dominantTotal = usage.totalTokens;
@@ -236,12 +293,13 @@ export function resolveDebugUsageBySession(sessionId, hookTimestamp, baseDir = W
236
293
  const modelUsage = Object.fromEntries(usageByModel.entries());
237
294
  return {
238
295
  sessionId,
239
- workspaceId: workspace.workspaceId,
296
+ workspaceId: workspaceId ?? "unknown",
240
297
  model: dominantModel,
241
298
  inputTokens,
242
299
  cachedInputTokens,
243
300
  outputTokens,
244
301
  totalTokens,
302
+ costUsd: totalCostUsd > 0 ? totalCostUsd : null,
245
303
  sourcePath: mainJsonlPath,
246
304
  modelUsage,
247
305
  };
@@ -252,14 +310,33 @@ export function resolveDebugUsageBySession(sessionId, hookTimestamp, baseDir = W
252
310
  * we sum the total number of individual tool requests across all turns.
253
311
  */
254
312
  function countToolCalls(entries) {
255
- let count = 0;
313
+ const toolCallIds = new Set();
314
+ let anonymousCalls = 0;
256
315
  for (const e of entries) {
257
- if (e.type !== "assistant.message")
316
+ if (e.type === "assistant.message") {
317
+ const msg = e;
318
+ for (const req of msg.data?.toolRequests ?? []) {
319
+ if (typeof req.toolCallId === "string" && req.toolCallId.length > 0) {
320
+ toolCallIds.add(req.toolCallId);
321
+ }
322
+ else {
323
+ anonymousCalls += 1;
324
+ }
325
+ }
258
326
  continue;
259
- const msg = e;
260
- count += msg.data?.toolRequests?.length ?? 0;
327
+ }
328
+ if (e.type === "tool.execution_start" || e.type === "tool.execution_complete") {
329
+ const data = isRecord(e.data) ? e.data : undefined;
330
+ const toolCallId = data?.toolCallId;
331
+ if (typeof toolCallId === "string" && toolCallId.length > 0) {
332
+ toolCallIds.add(toolCallId);
333
+ }
334
+ else {
335
+ anonymousCalls += 1;
336
+ }
337
+ }
261
338
  }
262
- return count;
339
+ return toolCallIds.size + anonymousCalls;
263
340
  }
264
341
  /**
265
342
  * Extract the earliest timestamp from the transcript.
@@ -296,11 +373,13 @@ export function buildPayloadFromStdin(stdin, read = readTranscript, resolveUsage
296
373
  })() : [];
297
374
  const toolCallCount = countToolCalls(entries);
298
375
  const startedAt = extractStartTime(entries) ?? stdin.timestamp ?? new Date().toISOString();
299
- const usage = resolveUsage(stdin.session_id, stdin.timestamp);
376
+ const parsedLocation = parseTranscriptPathLocation(stdin.transcript_path);
377
+ const sessionId = parsedLocation?.sessionId ?? stdin.session_id;
378
+ const usage = resolveUsage(sessionId, stdin.timestamp, WORKSPACE_STORAGE_DIR, parsedLocation?.workspaceId);
300
379
  return {
301
380
  tool: "copilot",
302
381
  // session_id is optional per VS Code spec; synthesize a fallback if absent
303
- sessionId: stdin.session_id ?? `copilot-${Date.now()}-${process.pid}`,
382
+ sessionId: sessionId ?? `copilot-${Date.now()}-${process.pid}`,
304
383
  gitUsername: resolveGitUsername(stdin.cwd),
305
384
  // Model/tokens are inferred from debug logs by session_id; falls back safely.
306
385
  model: usage?.model ?? "copilot",
@@ -308,7 +387,7 @@ export function buildPayloadFromStdin(stdin, read = readTranscript, resolveUsage
308
387
  cachedInputTokens: usage?.cachedInputTokens ?? 0,
309
388
  cacheCreationInputTokens: 0,
310
389
  outputTokens: usage?.outputTokens ?? 0,
311
- costUsd: null,
390
+ costUsd: usage?.costUsd ?? null,
312
391
  toolCallCount,
313
392
  startedAt,
314
393
  rawPayload: usage ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jagit/hook-copilot",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "jagit-hook-copilot": "dist/index.js"
@@ -9,7 +9,7 @@
9
9
  "dist"
10
10
  ],
11
11
  "dependencies": {
12
- "@jagit/agent-reporter": "0.0.4"
12
+ "@jagit/agent-reporter": "0.0.6"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@types/node": "^25.9.3",