@jagit/hook-copilot 0.0.1 → 0.0.2

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,37 @@
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 CopilotTranscriptEntry {
18
+ type?: string;
19
+ timestamp?: string;
20
+ message?: {
21
+ role?: string;
22
+ model?: string;
23
+ content?: unknown;
24
+ usage?: {
25
+ input_tokens?: number;
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
+ };
33
+ };
34
+ }
3
35
  export interface CopilotInfo {
4
36
  model?: string;
5
37
  inputTokens?: number;
@@ -7,4 +39,14 @@ export interface CopilotInfo {
7
39
  cachedInputTokens?: number;
8
40
  toolCallCount?: number | null;
9
41
  }
42
+ /**
43
+ * Build payload from a real VS Code Copilot agent Stop-hook stdin.
44
+ * Reads the transcript to aggregate token usage and detect the model.
45
+ */
46
+ export declare function buildPayloadFromStdin(stdin: CopilotStopStdin, read?: (path: string) => CopilotTranscriptEntry[]): AgentSessionPayload;
47
+ /**
48
+ * Build payload in legacy mode (no stdin) — used when hook-copilot is invoked
49
+ * via the old shell-wrapper pattern around the Copilot CLI. Token counts are
50
+ * not available in this mode (seat-based billing has no per-call telemetry).
51
+ */
10
52
  export declare function buildPayload(cwd: string | undefined, info?: CopilotInfo): AgentSessionPayload;
package/dist/index.js CHANGED
@@ -1,7 +1,81 @@
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 {};
16
+ }
17
+ });
18
+ }
19
+ function hasToolUse(content) {
20
+ return Array.isArray(content) && content.some((b) => b?.type === "tool_use");
21
+ }
22
+ // ─── Payload builders ─────────────────────────────────────────────────────────
23
+ /**
24
+ * Build payload from a real VS Code Copilot agent Stop-hook stdin.
25
+ * Reads the transcript to aggregate token usage and detect the model.
26
+ */
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";
34
+ const entries = stdin.transcript_path ? (() => {
35
+ try {
36
+ return read(stdin.transcript_path);
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ })() : [];
42
+ for (const e of entries) {
43
+ if (e.message?.role !== "assistant")
44
+ continue;
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();
59
+ return {
60
+ tool: "copilot",
61
+ // session_id is optional per VS Code spec; synthesize a fallback if absent
62
+ sessionId: stdin.session_id ?? `copilot-${Date.now()}-${process.pid}`,
63
+ gitUsername: resolveGitUsername(stdin.cwd),
64
+ model,
65
+ inputTokens,
66
+ cachedInputTokens,
67
+ cacheCreationInputTokens,
68
+ outputTokens,
69
+ costUsd: null,
70
+ toolCallCount,
71
+ startedAt,
72
+ };
73
+ }
74
+ /**
75
+ * Build payload in legacy mode (no stdin) — used when hook-copilot is invoked
76
+ * via the old shell-wrapper pattern around the Copilot CLI. Token counts are
77
+ * not available in this mode (seat-based billing has no per-call telemetry).
78
+ */
5
79
  export function buildPayload(cwd, info) {
6
80
  return {
7
81
  tool: "copilot",
@@ -10,15 +84,47 @@ export function buildPayload(cwd, info) {
10
84
  model: info?.model ?? "copilot",
11
85
  inputTokens: info?.inputTokens ?? 0,
12
86
  cachedInputTokens: info?.cachedInputTokens ?? 0,
87
+ cacheCreationInputTokens: 0,
13
88
  outputTokens: info?.outputTokens ?? 0,
14
89
  costUsd: null,
15
90
  toolCallCount: info?.toolCallCount ?? null,
16
91
  startedAt: new Date().toISOString(),
17
92
  };
18
93
  }
94
+ // ─── Try to read a JSON object from stdin (fd 0) ─────────────────────────────
95
+ function tryReadStdin() {
96
+ try {
97
+ const raw = readFileSync(0, "utf-8").trim();
98
+ if (!raw)
99
+ return undefined;
100
+ const parsed = JSON.parse(raw);
101
+ // Accept any object that looks like a VS Code hook payload (session_id is optional per spec)
102
+ if (typeof parsed === "object" && parsed !== null && "hook_event_name" in parsed) {
103
+ return parsed;
104
+ }
105
+ // Also accept if session_id is present (legacy / Claude Code compat)
106
+ if (typeof parsed.session_id === "string") {
107
+ return parsed;
108
+ }
109
+ return undefined;
110
+ }
111
+ catch {
112
+ return undefined;
113
+ }
114
+ }
115
+ // ─── Entry point ─────────────────────────────────────────────────────────────
19
116
  async function main() {
20
117
  try {
21
- await reportSession(buildPayload(process.cwd()));
118
+ const stdin = tryReadStdin();
119
+ // stop_hook_active=true means the agent is re-running because a previous Stop hook
120
+ // blocked it. Skip reporting to avoid duplicate session entries.
121
+ if (stdin?.stop_hook_active === true) {
122
+ process.exit(0);
123
+ }
124
+ const payload = stdin
125
+ ? buildPayloadFromStdin(stdin)
126
+ : buildPayload(process.cwd());
127
+ await reportSession(payload);
22
128
  }
23
129
  catch (err) {
24
130
  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.2",
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.2"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@types/node": "^25.9.3",