@jagit/hook-copilot 0.0.2 → 0.0.4
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 +3 -2
- package/dist/index.d.ts +62 -16
- package/dist/index.js +284 -34
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Reports GitHub Copilot agent session usage to JaGit. Supports two modes:
|
|
4
4
|
|
|
5
|
-
1. **VS Code agent hook** (recommended) — hooks into the VS Code Copilot agent `Stop` lifecycle event, receiving a structured JSON payload from stdin including `session_id`, `cwd`, and `transcript_path`. Token counts and model name are
|
|
5
|
+
1. **VS Code agent hook** (recommended) — hooks into the VS Code Copilot agent `Stop` lifecycle event, receiving a structured JSON payload from stdin including `session_id`, `cwd`, and `transcript_path`. Token counts and model name are read from Copilot debug logs (`main.jsonl`) by mapping `session_id` to the debug-log session folder.
|
|
6
6
|
2. **Legacy shell wrapper** — wraps the Copilot CLI binary and fires after each invocation. No per-call telemetry is available under seat-based billing, so token counts are always zero.
|
|
7
7
|
|
|
8
8
|
## Setup — VS Code Agent Hook (Recommended)
|
|
@@ -52,6 +52,7 @@ Identity defaults to `git config user.email`; override with `JAGIT_GIT_USERNAME`
|
|
|
52
52
|
|
|
53
53
|
## Notes
|
|
54
54
|
|
|
55
|
-
- In **VS Code agent hook mode**,
|
|
55
|
+
- In **VS Code agent hook mode**, tool-call count and start time are read from `transcript_path`; model/token usage is read from `~/.config/Code/User/workspaceStorage/<workspace-id>/GitHub.copilot-chat/debug-logs/<session-id>/main.jsonl`.
|
|
56
|
+
- Workspace detection prefers a workspace that contains the incoming `session_id`. If no exact session match is found, it falls back to the most recently updated workspace debug-log directory near the Stop-hook timestamp.
|
|
56
57
|
- In **legacy shell wrapper mode**, token counts are always zero (`costUsd: null`) since the Copilot CLI exposes no per-invocation telemetry under seat-based billing.
|
|
57
58
|
- `costUsd` is always `null` — there is no per-session USD cost to report.
|
package/dist/index.d.ts
CHANGED
|
@@ -14,24 +14,40 @@ export interface CopilotStopStdin {
|
|
|
14
14
|
*/
|
|
15
15
|
stop_hook_active?: boolean;
|
|
16
16
|
}
|
|
17
|
-
export interface
|
|
17
|
+
export interface CopilotTranscriptSessionStart {
|
|
18
|
+
type: "session.start";
|
|
19
|
+
timestamp?: string;
|
|
20
|
+
data: {
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
version?: number;
|
|
23
|
+
producer?: string;
|
|
24
|
+
copilotVersion?: string;
|
|
25
|
+
vscodeVersion?: string;
|
|
26
|
+
startTime?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface CopilotTranscriptToolRequest {
|
|
30
|
+
toolCallId?: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
arguments?: string;
|
|
18
33
|
type?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface CopilotTranscriptAssistantMessage {
|
|
36
|
+
type: "assistant.message";
|
|
19
37
|
timestamp?: string;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
content?:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
cache_read_input_tokens?: number;
|
|
27
|
-
cache_creation_input_tokens?: number;
|
|
28
|
-
output_tokens?: number;
|
|
29
|
-
inputTokens?: number;
|
|
30
|
-
cachedInputTokens?: number;
|
|
31
|
-
outputTokens?: number;
|
|
32
|
-
};
|
|
38
|
+
id?: string;
|
|
39
|
+
data: {
|
|
40
|
+
messageId?: string;
|
|
41
|
+
content?: string;
|
|
42
|
+
/** Tool calls made in this assistant turn */
|
|
43
|
+
toolRequests?: CopilotTranscriptToolRequest[];
|
|
33
44
|
};
|
|
34
45
|
}
|
|
46
|
+
export type CopilotTranscriptEntry = CopilotTranscriptSessionStart | CopilotTranscriptAssistantMessage | {
|
|
47
|
+
type: string;
|
|
48
|
+
timestamp?: string;
|
|
49
|
+
data?: unknown;
|
|
50
|
+
};
|
|
35
51
|
export interface CopilotInfo {
|
|
36
52
|
model?: string;
|
|
37
53
|
inputTokens?: number;
|
|
@@ -39,14 +55,44 @@ export interface CopilotInfo {
|
|
|
39
55
|
cachedInputTokens?: number;
|
|
40
56
|
toolCallCount?: number | null;
|
|
41
57
|
}
|
|
58
|
+
interface WorkspaceCandidate {
|
|
59
|
+
workspaceId: string;
|
|
60
|
+
debugLogsDir: string;
|
|
61
|
+
updatedAtMs: number;
|
|
62
|
+
}
|
|
63
|
+
interface ModelUsageBucket {
|
|
64
|
+
inputTokens: number;
|
|
65
|
+
cachedInputTokens: number;
|
|
66
|
+
outputTokens: number;
|
|
67
|
+
totalTokens: number;
|
|
68
|
+
observations: number;
|
|
69
|
+
}
|
|
70
|
+
interface CopilotDebugUsage {
|
|
71
|
+
sessionId: string;
|
|
72
|
+
workspaceId: string;
|
|
73
|
+
model: string;
|
|
74
|
+
inputTokens: number;
|
|
75
|
+
cachedInputTokens: number;
|
|
76
|
+
outputTokens: number;
|
|
77
|
+
totalTokens: number;
|
|
78
|
+
sourcePath: string;
|
|
79
|
+
modelUsage: Record<string, ModelUsageBucket>;
|
|
80
|
+
}
|
|
81
|
+
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;
|
|
42
83
|
/**
|
|
43
84
|
* Build payload from a real VS Code Copilot agent Stop-hook stdin.
|
|
44
|
-
*
|
|
85
|
+
*
|
|
86
|
+
* Reads the transcript to count tool calls and extract the session start time.
|
|
87
|
+
* Token usage (input/cached/output) and model name are NOT available in the
|
|
88
|
+
* VS Code Copilot transcript — Copilot uses seat-based billing and does not
|
|
89
|
+
* expose per-call telemetry. These fields are reported as 0/null/"copilot".
|
|
45
90
|
*/
|
|
46
|
-
export declare function buildPayloadFromStdin(stdin: CopilotStopStdin, read?: (path: string) => CopilotTranscriptEntry[]): AgentSessionPayload;
|
|
91
|
+
export declare function buildPayloadFromStdin(stdin: CopilotStopStdin, read?: (path: string) => CopilotTranscriptEntry[], resolveUsage?: (sessionId: string | undefined, hookTimestamp?: string) => CopilotDebugUsage | undefined): AgentSessionPayload;
|
|
47
92
|
/**
|
|
48
93
|
* Build payload in legacy mode (no stdin) — used when hook-copilot is invoked
|
|
49
94
|
* via the old shell-wrapper pattern around the Copilot CLI. Token counts are
|
|
50
95
|
* not available in this mode (seat-based billing has no per-call telemetry).
|
|
51
96
|
*/
|
|
52
97
|
export declare function buildPayload(cwd: string | undefined, info?: CopilotInfo): AgentSessionPayload;
|
|
98
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
3
5
|
import { fileURLToPath } from "node:url";
|
|
4
6
|
import { resolveGitUsername, reportSession } from "@jagit/agent-reporter";
|
|
7
|
+
const WORKSPACE_STORAGE_DIR = join(homedir(), ".config", "Code", "User", "workspaceStorage");
|
|
5
8
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
6
9
|
function readTranscript(path) {
|
|
7
10
|
return readFileSync(path, "utf-8")
|
|
@@ -12,25 +15,277 @@ function readTranscript(path) {
|
|
|
12
15
|
return JSON.parse(l);
|
|
13
16
|
}
|
|
14
17
|
catch {
|
|
15
|
-
return {};
|
|
18
|
+
return { type: "__parse_error__" };
|
|
16
19
|
}
|
|
17
20
|
});
|
|
18
21
|
}
|
|
19
|
-
function
|
|
20
|
-
return
|
|
22
|
+
function normalizeKey(key) {
|
|
23
|
+
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
24
|
+
}
|
|
25
|
+
function toFiniteNumber(value) {
|
|
26
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
30
|
+
const parsed = Number(value);
|
|
31
|
+
if (Number.isFinite(parsed)) {
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function isRecord(value) {
|
|
38
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
function listWorkspaceCandidates(baseDir) {
|
|
41
|
+
if (!existsSync(baseDir)) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const entries = readdirSync(baseDir, { withFileTypes: true });
|
|
45
|
+
const workspaces = [];
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!entry.isDirectory()) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const debugLogsDir = join(baseDir, entry.name, "GitHub.copilot-chat", "debug-logs");
|
|
51
|
+
if (!existsSync(debugLogsDir)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
let updatedAtMs = 0;
|
|
55
|
+
try {
|
|
56
|
+
updatedAtMs = statSync(debugLogsDir).mtimeMs;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
updatedAtMs = 0;
|
|
60
|
+
}
|
|
61
|
+
workspaces.push({
|
|
62
|
+
workspaceId: entry.name,
|
|
63
|
+
debugLogsDir,
|
|
64
|
+
updatedAtMs,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return workspaces;
|
|
68
|
+
}
|
|
69
|
+
function rankWorkspacesByRecency(workspaces, hookTimestamp) {
|
|
70
|
+
if (!hookTimestamp) {
|
|
71
|
+
return [...workspaces].sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
72
|
+
}
|
|
73
|
+
const hookMs = Date.parse(hookTimestamp);
|
|
74
|
+
if (Number.isNaN(hookMs)) {
|
|
75
|
+
return [...workspaces].sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
76
|
+
}
|
|
77
|
+
return [...workspaces].sort((a, b) => {
|
|
78
|
+
const aDelta = Math.abs(a.updatedAtMs - hookMs);
|
|
79
|
+
const bDelta = Math.abs(b.updatedAtMs - hookMs);
|
|
80
|
+
if (aDelta !== bDelta) {
|
|
81
|
+
return aDelta - bDelta;
|
|
82
|
+
}
|
|
83
|
+
return b.updatedAtMs - a.updatedAtMs;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export function inferWorkspaceIdBySession(sessionId, hookTimestamp, baseDir = WORKSPACE_STORAGE_DIR) {
|
|
87
|
+
const candidates = rankWorkspacesByRecency(listWorkspaceCandidates(baseDir), hookTimestamp);
|
|
88
|
+
for (const candidate of candidates) {
|
|
89
|
+
const sessionDir = join(candidate.debugLogsDir, sessionId);
|
|
90
|
+
if (existsSync(sessionDir)) {
|
|
91
|
+
return candidate;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return candidates[0];
|
|
95
|
+
}
|
|
96
|
+
function resolveModelFromObject(obj, fallbackModel) {
|
|
97
|
+
const keys = ["model", "modelName", "model_name", "resolvedModel", "deployment", "engine"];
|
|
98
|
+
for (const key of keys) {
|
|
99
|
+
const value = obj[key];
|
|
100
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
101
|
+
return value.trim();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return fallbackModel;
|
|
105
|
+
}
|
|
106
|
+
function extractTokensFromObject(obj) {
|
|
107
|
+
let inputTokens = 0;
|
|
108
|
+
let cachedInputTokens = 0;
|
|
109
|
+
let outputTokens = 0;
|
|
110
|
+
let totalTokens = 0;
|
|
111
|
+
let foundAny = false;
|
|
112
|
+
for (const [rawKey, rawValue] of Object.entries(obj)) {
|
|
113
|
+
const key = normalizeKey(rawKey);
|
|
114
|
+
const value = toFiniteNumber(rawValue);
|
|
115
|
+
if (value === null) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (["inputtokens", "prompttokens", "requesttokens"].includes(key)) {
|
|
119
|
+
inputTokens += value;
|
|
120
|
+
foundAny = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (["cachedinputtokens", "cachedtokens", "cachereadinputtokens", "promptcachehitstokens"].includes(key)) {
|
|
124
|
+
cachedInputTokens += value;
|
|
125
|
+
foundAny = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (["outputtokens", "completiontokens", "responsetokens"].includes(key)) {
|
|
129
|
+
outputTokens += value;
|
|
130
|
+
foundAny = true;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (["totaltokens", "alltokens"].includes(key)) {
|
|
134
|
+
totalTokens += value;
|
|
135
|
+
foundAny = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!foundAny) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (totalTokens === 0) {
|
|
143
|
+
totalTokens = inputTokens + outputTokens;
|
|
144
|
+
}
|
|
145
|
+
return { inputTokens, cachedInputTokens, outputTokens, totalTokens };
|
|
146
|
+
}
|
|
147
|
+
function collectModelUsage(value, usageByModel, currentModel = "copilot") {
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
for (const item of value) {
|
|
150
|
+
collectModelUsage(item, usageByModel, currentModel);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!isRecord(value)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const resolvedModel = resolveModelFromObject(value, currentModel);
|
|
158
|
+
const tokens = extractTokensFromObject(value);
|
|
159
|
+
if (tokens) {
|
|
160
|
+
const existing = usageByModel.get(resolvedModel) ?? {
|
|
161
|
+
inputTokens: 0,
|
|
162
|
+
cachedInputTokens: 0,
|
|
163
|
+
outputTokens: 0,
|
|
164
|
+
totalTokens: 0,
|
|
165
|
+
observations: 0,
|
|
166
|
+
};
|
|
167
|
+
existing.inputTokens += tokens.inputTokens;
|
|
168
|
+
existing.cachedInputTokens += tokens.cachedInputTokens;
|
|
169
|
+
existing.outputTokens += tokens.outputTokens;
|
|
170
|
+
existing.totalTokens += tokens.totalTokens;
|
|
171
|
+
existing.observations += 1;
|
|
172
|
+
usageByModel.set(resolvedModel, existing);
|
|
173
|
+
}
|
|
174
|
+
for (const child of Object.values(value)) {
|
|
175
|
+
collectModelUsage(child, usageByModel, resolvedModel);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export function resolveDebugUsageBySession(sessionId, hookTimestamp, baseDir = WORKSPACE_STORAGE_DIR) {
|
|
179
|
+
if (!sessionId) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const workspace = inferWorkspaceIdBySession(sessionId, hookTimestamp, baseDir);
|
|
183
|
+
if (!workspace) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const mainJsonlPath = join(workspace.debugLogsDir, sessionId, "main.jsonl");
|
|
187
|
+
if (!existsSync(mainJsonlPath)) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
const usageByModel = new Map();
|
|
191
|
+
const lines = readFileSync(mainJsonlPath, "utf-8").split("\n");
|
|
192
|
+
for (const line of lines) {
|
|
193
|
+
const trimmed = line.trim();
|
|
194
|
+
if (!trimmed) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(trimmed);
|
|
199
|
+
collectModelUsage(parsed, usageByModel, "copilot");
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Ignore malformed lines to keep reporting resilient.
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (usageByModel.size === 0) {
|
|
206
|
+
return {
|
|
207
|
+
sessionId,
|
|
208
|
+
workspaceId: workspace.workspaceId,
|
|
209
|
+
model: "copilot",
|
|
210
|
+
inputTokens: 0,
|
|
211
|
+
cachedInputTokens: 0,
|
|
212
|
+
outputTokens: 0,
|
|
213
|
+
totalTokens: 0,
|
|
214
|
+
sourcePath: mainJsonlPath,
|
|
215
|
+
modelUsage: {},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
let dominantModel = "copilot";
|
|
219
|
+
let dominantTotal = -1;
|
|
220
|
+
let dominantObs = -1;
|
|
221
|
+
let inputTokens = 0;
|
|
222
|
+
let cachedInputTokens = 0;
|
|
223
|
+
let outputTokens = 0;
|
|
224
|
+
let totalTokens = 0;
|
|
225
|
+
for (const [model, usage] of usageByModel.entries()) {
|
|
226
|
+
inputTokens += usage.inputTokens;
|
|
227
|
+
cachedInputTokens += usage.cachedInputTokens;
|
|
228
|
+
outputTokens += usage.outputTokens;
|
|
229
|
+
totalTokens += usage.totalTokens;
|
|
230
|
+
if (usage.totalTokens > dominantTotal || (usage.totalTokens === dominantTotal && usage.observations > dominantObs)) {
|
|
231
|
+
dominantModel = model;
|
|
232
|
+
dominantTotal = usage.totalTokens;
|
|
233
|
+
dominantObs = usage.observations;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const modelUsage = Object.fromEntries(usageByModel.entries());
|
|
237
|
+
return {
|
|
238
|
+
sessionId,
|
|
239
|
+
workspaceId: workspace.workspaceId,
|
|
240
|
+
model: dominantModel,
|
|
241
|
+
inputTokens,
|
|
242
|
+
cachedInputTokens,
|
|
243
|
+
outputTokens,
|
|
244
|
+
totalTokens,
|
|
245
|
+
sourcePath: mainJsonlPath,
|
|
246
|
+
modelUsage,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Count tool calls from assistant.message entries.
|
|
251
|
+
* Each assistant.message with at least one toolRequest counts as one tool-call turn;
|
|
252
|
+
* we sum the total number of individual tool requests across all turns.
|
|
253
|
+
*/
|
|
254
|
+
function countToolCalls(entries) {
|
|
255
|
+
let count = 0;
|
|
256
|
+
for (const e of entries) {
|
|
257
|
+
if (e.type !== "assistant.message")
|
|
258
|
+
continue;
|
|
259
|
+
const msg = e;
|
|
260
|
+
count += msg.data?.toolRequests?.length ?? 0;
|
|
261
|
+
}
|
|
262
|
+
return count;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Extract the earliest timestamp from the transcript.
|
|
266
|
+
* Prefers session.start data.startTime, then falls back to the first entry timestamp.
|
|
267
|
+
*/
|
|
268
|
+
function extractStartTime(entries) {
|
|
269
|
+
for (const e of entries) {
|
|
270
|
+
if (e.type === "session.start") {
|
|
271
|
+
const s = e;
|
|
272
|
+
if (s.data?.startTime)
|
|
273
|
+
return s.data.startTime;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Fall back to the first entry with a timestamp
|
|
277
|
+
return entries.find((e) => e.timestamp)?.timestamp;
|
|
21
278
|
}
|
|
22
279
|
// ─── Payload builders ─────────────────────────────────────────────────────────
|
|
23
280
|
/**
|
|
24
281
|
* Build payload from a real VS Code Copilot agent Stop-hook stdin.
|
|
25
|
-
*
|
|
282
|
+
*
|
|
283
|
+
* Reads the transcript to count tool calls and extract the session start time.
|
|
284
|
+
* Token usage (input/cached/output) and model name are NOT available in the
|
|
285
|
+
* VS Code Copilot transcript — Copilot uses seat-based billing and does not
|
|
286
|
+
* expose per-call telemetry. These fields are reported as 0/null/"copilot".
|
|
26
287
|
*/
|
|
27
|
-
export function buildPayloadFromStdin(stdin, read = readTranscript) {
|
|
28
|
-
let inputTokens = 0;
|
|
29
|
-
let cachedInputTokens = 0;
|
|
30
|
-
let cacheCreationInputTokens = 0;
|
|
31
|
-
let outputTokens = 0;
|
|
32
|
-
let toolCallCount = 0;
|
|
33
|
-
let model = "copilot";
|
|
288
|
+
export function buildPayloadFromStdin(stdin, read = readTranscript, resolveUsage = resolveDebugUsageBySession) {
|
|
34
289
|
const entries = stdin.transcript_path ? (() => {
|
|
35
290
|
try {
|
|
36
291
|
return read(stdin.transcript_path);
|
|
@@ -39,36 +294,31 @@ export function buildPayloadFromStdin(stdin, read = readTranscript) {
|
|
|
39
294
|
return [];
|
|
40
295
|
}
|
|
41
296
|
})() : [];
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (e.message.model)
|
|
46
|
-
model = e.message.model;
|
|
47
|
-
if (hasToolUse(e.message.content))
|
|
48
|
-
toolCallCount += 1;
|
|
49
|
-
const u = e.message.usage;
|
|
50
|
-
if (u) {
|
|
51
|
-
// Prefer snake_case (Claude-compatible), fall back to camelCase (OpenAI-style)
|
|
52
|
-
inputTokens += u.input_tokens ?? u.inputTokens ?? 0;
|
|
53
|
-
cachedInputTokens += u.cache_read_input_tokens ?? u.cachedInputTokens ?? 0;
|
|
54
|
-
cacheCreationInputTokens += u.cache_creation_input_tokens ?? 0;
|
|
55
|
-
outputTokens += u.output_tokens ?? u.outputTokens ?? 0;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const startedAt = entries.find((e) => e.timestamp)?.timestamp ?? stdin.timestamp ?? new Date().toISOString();
|
|
297
|
+
const toolCallCount = countToolCalls(entries);
|
|
298
|
+
const startedAt = extractStartTime(entries) ?? stdin.timestamp ?? new Date().toISOString();
|
|
299
|
+
const usage = resolveUsage(stdin.session_id, stdin.timestamp);
|
|
59
300
|
return {
|
|
60
301
|
tool: "copilot",
|
|
61
302
|
// session_id is optional per VS Code spec; synthesize a fallback if absent
|
|
62
303
|
sessionId: stdin.session_id ?? `copilot-${Date.now()}-${process.pid}`,
|
|
63
304
|
gitUsername: resolveGitUsername(stdin.cwd),
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
305
|
+
// Model/tokens are inferred from debug logs by session_id; falls back safely.
|
|
306
|
+
model: usage?.model ?? "copilot",
|
|
307
|
+
inputTokens: usage?.inputTokens ?? 0,
|
|
308
|
+
cachedInputTokens: usage?.cachedInputTokens ?? 0,
|
|
309
|
+
cacheCreationInputTokens: 0,
|
|
310
|
+
outputTokens: usage?.outputTokens ?? 0,
|
|
69
311
|
costUsd: null,
|
|
70
312
|
toolCallCount,
|
|
71
313
|
startedAt,
|
|
314
|
+
rawPayload: usage ? {
|
|
315
|
+
source: "copilot-debug-logs",
|
|
316
|
+
workspaceId: usage.workspaceId,
|
|
317
|
+
debugLogPath: usage.sourcePath,
|
|
318
|
+
debugSessionId: usage.sessionId,
|
|
319
|
+
totalTokens: usage.totalTokens,
|
|
320
|
+
modelUsage: usage.modelUsage,
|
|
321
|
+
} : undefined,
|
|
72
322
|
};
|
|
73
323
|
}
|
|
74
324
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jagit/hook-copilot",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
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.4"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@types/node": "^25.9.3",
|