@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 CHANGED
@@ -1,40 +1,57 @@
1
1
  # @jagit/hook-copilot
2
2
 
3
- Reports per-invocation GitHub Copilot CLI usage to JaGit.
3
+ Reports GitHub Copilot agent session usage to JaGit. Supports two modes:
4
4
 
5
- ## Setup
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
- The Copilot CLI has no hook mechanism and no persistent session telemetry, so
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
- copilot() {
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
- Add that function to your shell rc (`~/.zshrc`, `~/.bashrc`, etc.) — it must
19
- appear before any `alias`/`PATH` entry that would otherwise shadow it. (Users
20
- on the legacy `gh copilot` preview wrapper can apply the same pattern to a
21
- `gh` function instead.) Uninstall by removing the shell function.
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
- For a permanent binary instead of `npx -y`:
24
- `npm i -g @jagit/hook-copilot`, then call `jagit-hook-copilot` in the wrapper.
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
- export JAGIT_BASE_URL="https://your-jagit-host"
29
- export JAGIT_API_KEY="<your DASHBOARD_API_TOKEN>"
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
- - Copilot CLI exposes no local token/usage telemetry (billing is seat-based),
36
- so each report uses a synthetic session id (`copilot-<timestamp>-<pid>`),
37
- zero token counts, and `model: "copilot"` unless a future CLI version
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
- await reportSession(buildPayload(process.cwd()));
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.1",
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.1"
12
+ "@jagit/agent-reporter": "0.0.3"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@types/node": "^25.9.3",