@jagit/hook-copilot 0.0.1 → 0.0.3
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 +42 -25
- package/dist/index.d.ts +62 -0
- package/dist/index.js +120 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,40 +1,57 @@
|
|
|
1
1
|
# @jagit/hook-copilot
|
|
2
2
|
|
|
3
|
-
Reports
|
|
3
|
+
Reports GitHub Copilot agent session usage to JaGit. Supports two modes:
|
|
4
4
|
|
|
5
|
-
|
|
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 parsed from the session transcript.
|
|
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.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
install a shell function that wraps the real `copilot` binary and reports
|
|
9
|
-
after each invocation ends:
|
|
8
|
+
## Setup — VS Code Agent Hook (Recommended)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
command copilot "$@"
|
|
13
|
-
local status=$?
|
|
14
|
-
npx -y @jagit/hook-copilot >/dev/null 2>&1 || true
|
|
15
|
-
return $status
|
|
16
|
-
}
|
|
10
|
+
Add the hook to your workspace's `.github/hooks/jagit.json` (or any [supported hook location](https://code.visualstudio.com/docs/agent-customization/hooks#_hook-file-locations)):
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"hooks": {
|
|
15
|
+
"Stop": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "npx -y @jagit/hook-copilot"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
VS Code automatically loads this file. When the Copilot agent session ends, JaGit receives the session report (session ID, model, aggregated token counts, tool call count).
|
|
26
|
+
|
|
27
|
+
For a permanent binary instead of `npx -y`: `npm i -g @jagit/hook-copilot`, then use `jagit-hook-copilot` as the command.
|
|
28
|
+
|
|
29
|
+
## Setup — Legacy Shell Wrapper (Copilot CLI)
|
|
30
|
+
|
|
31
|
+
If you are using the Copilot CLI directly (not the VS Code agent), install a shell function that wraps the real `copilot` binary and reports after each invocation ends:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
copilot() {
|
|
35
|
+
command copilot "$@"
|
|
36
|
+
local status=$?
|
|
37
|
+
npx -y @jagit/hook-copilot >/dev/null 2>&1 || true
|
|
38
|
+
return $status
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Add that function to your shell rc (`~/.zshrc`, `~/.bashrc`, etc.). Uninstall by removing the shell function.
|
|
25
43
|
|
|
26
44
|
## Environment
|
|
27
45
|
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
```sh
|
|
47
|
+
export JAGIT_BASE_URL="https://your-jagit-host"
|
|
48
|
+
export JAGIT_API_KEY="<your DASHBOARD_API_TOKEN>"
|
|
49
|
+
```
|
|
30
50
|
|
|
31
51
|
Identity defaults to `git config user.email`; override with `JAGIT_GIT_USERNAME`.
|
|
32
52
|
|
|
33
53
|
## Notes
|
|
34
54
|
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
surfaces real usage data.
|
|
39
|
-
- `costUsd` is always `null` and will remain so — there is no per-invocation
|
|
40
|
-
cost to report under seat-based billing.
|
|
55
|
+
- In **VS Code agent hook mode**, tokens and model are read from the session transcript at `transcript_path`. Both snake_case (`input_tokens`) and camelCase (`inputTokens`) transcript formats are supported.
|
|
56
|
+
- 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
|
+
- `costUsd` is always `null` — there is no per-session USD cost to report.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type AgentSessionPayload } from "@jagit/agent-reporter";
|
|
3
|
+
export interface CopilotStopStdin {
|
|
4
|
+
/** Optional per VS Code spec — common hook field */
|
|
5
|
+
session_id?: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
hook_event_name?: string;
|
|
8
|
+
/** Absolute path to session transcript. Format is unstable — may change in future VS Code releases. */
|
|
9
|
+
transcript_path?: string;
|
|
10
|
+
timestamp?: string;
|
|
11
|
+
/**
|
|
12
|
+
* true when the agent is already continuing as a result of a previous Stop hook.
|
|
13
|
+
* Check this to prevent the hook from reporting duplicate sessions.
|
|
14
|
+
*/
|
|
15
|
+
stop_hook_active?: boolean;
|
|
16
|
+
}
|
|
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;
|
|
33
|
+
type?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface CopilotTranscriptAssistantMessage {
|
|
36
|
+
type: "assistant.message";
|
|
37
|
+
timestamp?: string;
|
|
38
|
+
id?: string;
|
|
39
|
+
data: {
|
|
40
|
+
messageId?: string;
|
|
41
|
+
content?: string;
|
|
42
|
+
/** Tool calls made in this assistant turn */
|
|
43
|
+
toolRequests?: CopilotTranscriptToolRequest[];
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export type CopilotTranscriptEntry = CopilotTranscriptSessionStart | CopilotTranscriptAssistantMessage | {
|
|
47
|
+
type: string;
|
|
48
|
+
timestamp?: string;
|
|
49
|
+
data?: unknown;
|
|
50
|
+
};
|
|
3
51
|
export interface CopilotInfo {
|
|
4
52
|
model?: string;
|
|
5
53
|
inputTokens?: number;
|
|
@@ -7,4 +55,18 @@ export interface CopilotInfo {
|
|
|
7
55
|
cachedInputTokens?: number;
|
|
8
56
|
toolCallCount?: number | null;
|
|
9
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Build payload from a real VS Code Copilot agent Stop-hook stdin.
|
|
60
|
+
*
|
|
61
|
+
* Reads the transcript to count tool calls and extract the session start time.
|
|
62
|
+
* Token usage (input/cached/output) and model name are NOT available in the
|
|
63
|
+
* VS Code Copilot transcript — Copilot uses seat-based billing and does not
|
|
64
|
+
* expose per-call telemetry. These fields are reported as 0/null/"copilot".
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildPayloadFromStdin(stdin: CopilotStopStdin, read?: (path: string) => CopilotTranscriptEntry[]): AgentSessionPayload;
|
|
67
|
+
/**
|
|
68
|
+
* Build payload in legacy mode (no stdin) — used when hook-copilot is invoked
|
|
69
|
+
* via the old shell-wrapper pattern around the Copilot CLI. Token counts are
|
|
70
|
+
* not available in this mode (seat-based billing has no per-call telemetry).
|
|
71
|
+
*/
|
|
10
72
|
export declare function buildPayload(cwd: string | undefined, info?: CopilotInfo): AgentSessionPayload;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,93 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { resolveGitUsername, reportSession } from "@jagit/agent-reporter";
|
|
5
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
6
|
+
function readTranscript(path) {
|
|
7
|
+
return readFileSync(path, "utf-8")
|
|
8
|
+
.split("\n")
|
|
9
|
+
.filter((l) => l.trim().length > 0)
|
|
10
|
+
.map((l) => {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(l);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return { type: "__parse_error__" };
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Count tool calls from assistant.message entries.
|
|
21
|
+
* Each assistant.message with at least one toolRequest counts as one tool-call turn;
|
|
22
|
+
* we sum the total number of individual tool requests across all turns.
|
|
23
|
+
*/
|
|
24
|
+
function countToolCalls(entries) {
|
|
25
|
+
let count = 0;
|
|
26
|
+
for (const e of entries) {
|
|
27
|
+
if (e.type !== "assistant.message")
|
|
28
|
+
continue;
|
|
29
|
+
const msg = e;
|
|
30
|
+
count += msg.data?.toolRequests?.length ?? 0;
|
|
31
|
+
}
|
|
32
|
+
return count;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extract the earliest timestamp from the transcript.
|
|
36
|
+
* Prefers session.start data.startTime, then falls back to the first entry timestamp.
|
|
37
|
+
*/
|
|
38
|
+
function extractStartTime(entries) {
|
|
39
|
+
for (const e of entries) {
|
|
40
|
+
if (e.type === "session.start") {
|
|
41
|
+
const s = e;
|
|
42
|
+
if (s.data?.startTime)
|
|
43
|
+
return s.data.startTime;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Fall back to the first entry with a timestamp
|
|
47
|
+
return entries.find((e) => e.timestamp)?.timestamp;
|
|
48
|
+
}
|
|
49
|
+
// ─── Payload builders ─────────────────────────────────────────────────────────
|
|
50
|
+
/**
|
|
51
|
+
* Build payload from a real VS Code Copilot agent Stop-hook stdin.
|
|
52
|
+
*
|
|
53
|
+
* Reads the transcript to count tool calls and extract the session start time.
|
|
54
|
+
* Token usage (input/cached/output) and model name are NOT available in the
|
|
55
|
+
* VS Code Copilot transcript — Copilot uses seat-based billing and does not
|
|
56
|
+
* expose per-call telemetry. These fields are reported as 0/null/"copilot".
|
|
57
|
+
*/
|
|
58
|
+
export function buildPayloadFromStdin(stdin, read = readTranscript) {
|
|
59
|
+
const entries = stdin.transcript_path ? (() => {
|
|
60
|
+
try {
|
|
61
|
+
return read(stdin.transcript_path);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
})() : [];
|
|
67
|
+
const toolCallCount = countToolCalls(entries);
|
|
68
|
+
const startedAt = extractStartTime(entries) ?? stdin.timestamp ?? new Date().toISOString();
|
|
69
|
+
return {
|
|
70
|
+
tool: "copilot",
|
|
71
|
+
// session_id is optional per VS Code spec; synthesize a fallback if absent
|
|
72
|
+
sessionId: stdin.session_id ?? `copilot-${Date.now()}-${process.pid}`,
|
|
73
|
+
gitUsername: resolveGitUsername(stdin.cwd),
|
|
74
|
+
// Model name is not exposed in the Copilot hook transcript (seat-based billing)
|
|
75
|
+
model: "copilot",
|
|
76
|
+
// Token usage is not available in the Copilot hook transcript
|
|
77
|
+
inputTokens: 0,
|
|
78
|
+
cachedInputTokens: 0,
|
|
79
|
+
cacheCreationInputTokens: 0,
|
|
80
|
+
outputTokens: 0,
|
|
81
|
+
costUsd: null,
|
|
82
|
+
toolCallCount,
|
|
83
|
+
startedAt,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build payload in legacy mode (no stdin) — used when hook-copilot is invoked
|
|
88
|
+
* via the old shell-wrapper pattern around the Copilot CLI. Token counts are
|
|
89
|
+
* not available in this mode (seat-based billing has no per-call telemetry).
|
|
90
|
+
*/
|
|
5
91
|
export function buildPayload(cwd, info) {
|
|
6
92
|
return {
|
|
7
93
|
tool: "copilot",
|
|
@@ -10,15 +96,47 @@ export function buildPayload(cwd, info) {
|
|
|
10
96
|
model: info?.model ?? "copilot",
|
|
11
97
|
inputTokens: info?.inputTokens ?? 0,
|
|
12
98
|
cachedInputTokens: info?.cachedInputTokens ?? 0,
|
|
99
|
+
cacheCreationInputTokens: 0,
|
|
13
100
|
outputTokens: info?.outputTokens ?? 0,
|
|
14
101
|
costUsd: null,
|
|
15
102
|
toolCallCount: info?.toolCallCount ?? null,
|
|
16
103
|
startedAt: new Date().toISOString(),
|
|
17
104
|
};
|
|
18
105
|
}
|
|
106
|
+
// ─── Try to read a JSON object from stdin (fd 0) ─────────────────────────────
|
|
107
|
+
function tryReadStdin() {
|
|
108
|
+
try {
|
|
109
|
+
const raw = readFileSync(0, "utf-8").trim();
|
|
110
|
+
if (!raw)
|
|
111
|
+
return undefined;
|
|
112
|
+
const parsed = JSON.parse(raw);
|
|
113
|
+
// Accept any object that looks like a VS Code hook payload (session_id is optional per spec)
|
|
114
|
+
if (typeof parsed === "object" && parsed !== null && "hook_event_name" in parsed) {
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
// Also accept if session_id is present (legacy / Claude Code compat)
|
|
118
|
+
if (typeof parsed.session_id === "string") {
|
|
119
|
+
return parsed;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
19
128
|
async function main() {
|
|
20
129
|
try {
|
|
21
|
-
|
|
130
|
+
const stdin = tryReadStdin();
|
|
131
|
+
// stop_hook_active=true means the agent is re-running because a previous Stop hook
|
|
132
|
+
// blocked it. Skip reporting to avoid duplicate session entries.
|
|
133
|
+
if (stdin?.stop_hook_active === true) {
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
const payload = stdin
|
|
137
|
+
? buildPayloadFromStdin(stdin)
|
|
138
|
+
: buildPayload(process.cwd());
|
|
139
|
+
await reportSession(payload);
|
|
22
140
|
}
|
|
23
141
|
catch (err) {
|
|
24
142
|
console.error("[hook-copilot]", err instanceof Error ? err.message : err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jagit/hook-copilot",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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.3"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@types/node": "^25.9.3",
|