@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 +9 -2
- package/dist/index.js +99 -20
- package/package.json +2 -2
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
|
-
|
|
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 (["
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
313
|
+
const toolCallIds = new Set();
|
|
314
|
+
let anonymousCalls = 0;
|
|
256
315
|
for (const e of entries) {
|
|
257
|
-
if (e.type
|
|
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
|
-
|
|
260
|
-
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
12
|
+
"@jagit/agent-reporter": "0.0.6"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@types/node": "^25.9.3",
|