@jagit/hook-copilot 0.0.5 → 0.0.7

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 CHANGED
@@ -80,6 +80,31 @@ interface CopilotDebugUsage {
80
80
  sourcePath: string;
81
81
  modelUsage: Record<string, ModelUsageBucket>;
82
82
  }
83
+ /** Resolves the root workspaceStorage directory for the current platform. */
84
+ export interface WorkspaceStorageResolver {
85
+ resolve(): string;
86
+ }
87
+ /** Linux: ~/.config/Code/User/workspaceStorage */
88
+ export declare class LinuxWorkspaceStorageResolver implements WorkspaceStorageResolver {
89
+ resolve(): string;
90
+ }
91
+ /** macOS: ~/Library/Application Support/Code/User/workspaceStorage */
92
+ export declare class MacOSWorkspaceStorageResolver implements WorkspaceStorageResolver {
93
+ resolve(): string;
94
+ }
95
+ /**
96
+ * Windows: %APPDATA%\Code\User\workspaceStorage
97
+ * Falls back to %USERPROFILE%\AppData\Roaming when APPDATA is unset.
98
+ */
99
+ export declare class WindowsWorkspaceStorageResolver implements WorkspaceStorageResolver {
100
+ resolve(): string;
101
+ }
102
+ /**
103
+ * Returns the appropriate WorkspaceStorageResolver for the current OS.
104
+ * Override in tests by passing a custom resolver directly to the functions
105
+ * that accept a `baseDir` parameter.
106
+ */
107
+ export declare function platformWorkspaceStorageResolver(): WorkspaceStorageResolver;
83
108
  export declare function inferWorkspaceIdBySession(sessionId: string, hookTimestamp?: string, baseDir?: string): WorkspaceCandidate | undefined;
84
109
  interface TranscriptPathLocation {
85
110
  workspaceId: string;
package/dist/index.js CHANGED
@@ -1,10 +1,48 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
3
- import { homedir } from "node:os";
3
+ import { homedir, platform } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { resolveGitUsername, reportSession } from "@jagit/agent-reporter";
7
- const WORKSPACE_STORAGE_DIR = join(homedir(), ".config", "Code", "User", "workspaceStorage");
7
+ /** Linux: ~/.config/Code/User/workspaceStorage */
8
+ export class LinuxWorkspaceStorageResolver {
9
+ resolve() {
10
+ return join(homedir(), ".config", "Code", "User", "workspaceStorage");
11
+ }
12
+ }
13
+ /** macOS: ~/Library/Application Support/Code/User/workspaceStorage */
14
+ export class MacOSWorkspaceStorageResolver {
15
+ resolve() {
16
+ return join(homedir(), "Library", "Application Support", "Code", "User", "workspaceStorage");
17
+ }
18
+ }
19
+ /**
20
+ * Windows: %APPDATA%\Code\User\workspaceStorage
21
+ * Falls back to %USERPROFILE%\AppData\Roaming when APPDATA is unset.
22
+ */
23
+ export class WindowsWorkspaceStorageResolver {
24
+ resolve() {
25
+ const appData = process.env["APPDATA"] ??
26
+ join(homedir(), "AppData", "Roaming");
27
+ return join(appData, "Code", "User", "workspaceStorage");
28
+ }
29
+ }
30
+ /**
31
+ * Returns the appropriate WorkspaceStorageResolver for the current OS.
32
+ * Override in tests by passing a custom resolver directly to the functions
33
+ * that accept a `baseDir` parameter.
34
+ */
35
+ export function platformWorkspaceStorageResolver() {
36
+ switch (platform()) {
37
+ case "darwin":
38
+ return new MacOSWorkspaceStorageResolver();
39
+ case "win32":
40
+ return new WindowsWorkspaceStorageResolver();
41
+ default:
42
+ return new LinuxWorkspaceStorageResolver();
43
+ }
44
+ }
45
+ const WORKSPACE_STORAGE_DIR = platformWorkspaceStorageResolver().resolve();
8
46
  // ─── Helpers ─────────────────────────────────────────────────────────────────
9
47
  function readTranscript(path) {
10
48
  return readFileSync(path, "utf-8")
@@ -127,6 +165,9 @@ function extractTokensFromObject(obj) {
127
165
  let totalTokens = 0;
128
166
  let costUsd = 0;
129
167
  let foundAny = false;
168
+ let sawInputKey = false;
169
+ let sawCacheReadStyleKey = false;
170
+ let sawCachedSubsetKey = false;
130
171
  for (const [rawKey, rawValue] of Object.entries(obj)) {
131
172
  const key = normalizeKey(rawKey);
132
173
  const value = toFiniteNumber(rawValue);
@@ -136,11 +177,19 @@ function extractTokensFromObject(obj) {
136
177
  if (["inputtokens", "prompttokens", "requesttokens"].includes(key)) {
137
178
  inputTokens += value;
138
179
  foundAny = true;
180
+ sawInputKey = true;
139
181
  continue;
140
182
  }
141
- if (["cachedinputtokens", "cachedtokens", "cachereadinputtokens", "promptcachehitstokens"].includes(key)) {
183
+ if (["cachedinputtokens", "cachereadinputtokens"].includes(key)) {
142
184
  cachedInputTokens += value;
143
185
  foundAny = true;
186
+ sawCacheReadStyleKey = true;
187
+ continue;
188
+ }
189
+ if (["cachedtokens", "promptcachehitstokens"].includes(key)) {
190
+ cachedInputTokens += value;
191
+ foundAny = true;
192
+ sawCachedSubsetKey = true;
144
193
  continue;
145
194
  }
146
195
  if (["outputtokens", "completiontokens", "responsetokens"].includes(key)) {
@@ -162,8 +211,14 @@ function extractTokensFromObject(obj) {
162
211
  if (!foundAny) {
163
212
  return null;
164
213
  }
214
+ // Some providers report inputTokens as total prompt tokens and expose cached
215
+ // tokens as a subset. Convert to non-cached input tokens to match JaGit's
216
+ // input/cached split and avoid double-counting in aggregates.
217
+ if (sawInputKey && sawCachedSubsetKey && !sawCacheReadStyleKey) {
218
+ inputTokens = Math.max(0, inputTokens - cachedInputTokens);
219
+ }
165
220
  if (totalTokens === 0) {
166
- totalTokens = inputTokens + outputTokens;
221
+ totalTokens = inputTokens + cachedInputTokens + outputTokens;
167
222
  }
168
223
  return { inputTokens, cachedInputTokens, outputTokens, totalTokens, costUsd };
169
224
  }
@@ -293,14 +348,33 @@ export function resolveDebugUsageBySession(sessionId, hookTimestamp, baseDir = W
293
348
  * we sum the total number of individual tool requests across all turns.
294
349
  */
295
350
  function countToolCalls(entries) {
296
- let count = 0;
351
+ const toolCallIds = new Set();
352
+ let anonymousCalls = 0;
297
353
  for (const e of entries) {
298
- if (e.type !== "assistant.message")
354
+ if (e.type === "assistant.message") {
355
+ const msg = e;
356
+ for (const req of msg.data?.toolRequests ?? []) {
357
+ if (typeof req.toolCallId === "string" && req.toolCallId.length > 0) {
358
+ toolCallIds.add(req.toolCallId);
359
+ }
360
+ else {
361
+ anonymousCalls += 1;
362
+ }
363
+ }
299
364
  continue;
300
- const msg = e;
301
- count += msg.data?.toolRequests?.length ?? 0;
365
+ }
366
+ if (e.type === "tool.execution_start" || e.type === "tool.execution_complete") {
367
+ const data = isRecord(e.data) ? e.data : undefined;
368
+ const toolCallId = data?.toolCallId;
369
+ if (typeof toolCallId === "string" && toolCallId.length > 0) {
370
+ toolCallIds.add(toolCallId);
371
+ }
372
+ else {
373
+ anonymousCalls += 1;
374
+ }
375
+ }
302
376
  }
303
- return count;
377
+ return toolCallIds.size + anonymousCalls;
304
378
  }
305
379
  /**
306
380
  * Extract the earliest timestamp from the transcript.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jagit/hook-copilot",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
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.5"
12
+ "@jagit/agent-reporter": "0.0.7"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@types/node": "^25.9.3",
@@ -18,6 +18,7 @@
18
18
  "scripts": {
19
19
  "build": "tsc -p tsconfig.json",
20
20
  "typecheck": "tsc -p tsconfig.json --noEmit",
21
- "test": "vitest run"
21
+ "test": "vitest run",
22
+ "test:live": "tsx scripts/test-live.ts"
22
23
  }
23
24
  }