@jagit/hook-claude-code-time-tracking 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/dist/git.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function getHeadSha(cwd?: string): string | null;
package/dist/git.js ADDED
@@ -0,0 +1,14 @@
1
+ import { execSync } from "node:child_process";
2
+ export function getHeadSha(cwd) {
3
+ try {
4
+ const sha = execSync("git rev-parse HEAD", {
5
+ cwd: cwd ?? process.cwd(),
6
+ encoding: "utf-8",
7
+ stdio: ["pipe", "pipe", "pipe"],
8
+ }).trim();
9
+ return sha;
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ interface UserPromptSubmitStdin {
3
+ session_id: string;
4
+ timestamp: string;
5
+ cwd?: string;
6
+ }
7
+ export interface TimeTrackingPayload {
8
+ sessionId: string;
9
+ initialCommitSha: string | null;
10
+ totalDurationMs: number;
11
+ }
12
+ export declare function buildPayload(stdin: UserPromptSubmitStdin): Promise<TimeTrackingPayload>;
13
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, realpathSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { getStatePath, readState, writeState } from "./state.js";
5
+ import { getHeadSha } from "./git.js";
6
+ export async function buildPayload(stdin) {
7
+ const { session_id, timestamp, cwd } = stdin;
8
+ const statePath = cwd ? getStatePath(cwd, session_id) : "";
9
+ let state;
10
+ if (!statePath) {
11
+ // No cwd available, just return minimal payload
12
+ return {
13
+ sessionId: session_id,
14
+ initialCommitSha: null,
15
+ totalDurationMs: 0,
16
+ };
17
+ }
18
+ const existingState = readState(statePath);
19
+ if (!existingState) {
20
+ // Initialize new state
21
+ const initialCommitSha = getHeadSha(cwd);
22
+ state = {
23
+ sessionId: session_id,
24
+ initialCommitSha,
25
+ totalDurationMs: 0,
26
+ lastUpdateTime: timestamp,
27
+ };
28
+ writeState(statePath, state);
29
+ }
30
+ else {
31
+ // Accumulate duration
32
+ const lastTime = new Date(existingState.lastUpdateTime).getTime();
33
+ const currentTime = new Date(timestamp).getTime();
34
+ const elapsed = currentTime - lastTime;
35
+ state = {
36
+ ...existingState,
37
+ totalDurationMs: existingState.totalDurationMs + elapsed,
38
+ lastUpdateTime: timestamp,
39
+ };
40
+ writeState(statePath, state);
41
+ }
42
+ // Async sync to API (fire and forget)
43
+ syncToApi(state).catch((err) => {
44
+ console.error("[time-tracking] Failed to sync to API:", err);
45
+ });
46
+ return {
47
+ sessionId: state.sessionId,
48
+ initialCommitSha: state.initialCommitSha,
49
+ totalDurationMs: state.totalDurationMs,
50
+ };
51
+ }
52
+ async function syncToApi(state) {
53
+ const baseUrl = process.env.JAGIT_BASE_URL?.trim();
54
+ const apiKey = process.env.JAGIT_API_KEY?.trim();
55
+ if (!baseUrl || !apiKey) {
56
+ return; // Silently skip if not configured
57
+ }
58
+ const url = `${baseUrl.replace(/\/+$/, "")}/api/agent-sessions/${state.sessionId}/time-tracking`;
59
+ const body = {};
60
+ if (state.initialCommitSha)
61
+ body.initialCommitSha = state.initialCommitSha;
62
+ if (state.totalDurationMs > 0)
63
+ body.durationMs = state.totalDurationMs;
64
+ if (Object.keys(body).length === 0)
65
+ return;
66
+ await fetch(url, {
67
+ method: "PATCH",
68
+ headers: {
69
+ "content-type": "application/json",
70
+ "x-api-key": apiKey,
71
+ },
72
+ body: JSON.stringify(body),
73
+ });
74
+ }
75
+ async function main() {
76
+ try {
77
+ const raw = readFileSync(0, "utf-8");
78
+ const stdin = JSON.parse(raw);
79
+ await buildPayload(stdin);
80
+ }
81
+ catch (err) {
82
+ console.error("[time-tracking]", err instanceof Error ? err.message : err);
83
+ }
84
+ finally {
85
+ process.exit(0);
86
+ }
87
+ }
88
+ const isMain = import.meta.url.startsWith("file://") &&
89
+ realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
90
+ if (isMain)
91
+ void main();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { buildPayload } from "./index.js";
3
+ import * as state from "./state.js";
4
+ import * as git from "./git.js";
5
+ vi.mock("./state.js", () => ({
6
+ readState: vi.fn(),
7
+ writeState: vi.fn(),
8
+ getStatePath: (cwd, sessionId) => `${cwd}/.jigit-session-${sessionId}.json`,
9
+ }));
10
+ vi.mock("./git.js", () => ({
11
+ getHeadSha: vi.fn(),
12
+ }));
13
+ vi.mock("@jigit/agent-reporter", () => ({
14
+ reportSession: vi.fn(),
15
+ }));
16
+ describe("buildPayload", () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+ it("should initialize state on first call", async () => {
21
+ vi.mocked(state.readState).mockReturnValue(null);
22
+ vi.mocked(git.getHeadSha).mockReturnValue("abc123");
23
+ vi.mocked(state.writeState).mockImplementation(() => { });
24
+ const stdin = {
25
+ session_id: "test-session-1",
26
+ timestamp: "2026-06-21T10:00:00Z",
27
+ cwd: "/tmp/test",
28
+ };
29
+ const result = await buildPayload(stdin);
30
+ expect(result.sessionId).toBe("test-session-1");
31
+ expect(result.initialCommitSha).toBe("abc123");
32
+ expect(result.totalDurationMs).toBe(0);
33
+ expect(state.writeState).toHaveBeenCalledWith("/tmp/test/.jigit-session-test-session-1.json", expect.objectContaining({
34
+ sessionId: "test-session-1",
35
+ initialCommitSha: "abc123",
36
+ totalDurationMs: 0,
37
+ }));
38
+ });
39
+ it("should accumulate duration on subsequent calls", async () => {
40
+ vi.mocked(state.readState).mockReturnValue({
41
+ sessionId: "test-session-1",
42
+ initialCommitSha: "abc123",
43
+ totalDurationMs: 3600000,
44
+ lastUpdateTime: "2026-06-21T10:00:00Z",
45
+ });
46
+ vi.mocked(state.writeState).mockImplementation(() => { });
47
+ const stdin = {
48
+ session_id: "test-session-1",
49
+ timestamp: "2026-06-21T11:00:00Z",
50
+ cwd: "/tmp/test",
51
+ };
52
+ const result = await buildPayload(stdin);
53
+ expect(result.totalDurationMs).toBe(7200000); // 2 hours total
54
+ });
55
+ it("should handle missing cwd", async () => {
56
+ vi.mocked(state.readState).mockReturnValue(null);
57
+ vi.mocked(git.getHeadSha).mockImplementation(() => {
58
+ throw new Error("Not a git repository");
59
+ });
60
+ const stdin = {
61
+ session_id: "test-session-2",
62
+ timestamp: "2026-06-21T10:00:00Z",
63
+ };
64
+ const result = await buildPayload(stdin);
65
+ expect(result.initialCommitSha).toBeNull();
66
+ });
67
+ });
@@ -0,0 +1,9 @@
1
+ export interface TimeTrackingState {
2
+ sessionId: string;
3
+ initialCommitSha: string | null;
4
+ totalDurationMs: number;
5
+ lastUpdateTime: string;
6
+ }
7
+ export declare function getStatePath(cwd: string, sessionId: string): string;
8
+ export declare function readState(path: string): TimeTrackingState | null;
9
+ export declare function writeState(path: string, state: TimeTrackingState): void;
package/dist/state.js ADDED
@@ -0,0 +1,29 @@
1
+ import { readFileSync, writeFileSync, existsSync, renameSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ export function getStatePath(cwd, sessionId) {
5
+ return join(cwd, `.jigit-session-${sessionId}.json`);
6
+ }
7
+ export function readState(path) {
8
+ try {
9
+ if (!existsSync(path))
10
+ return null;
11
+ const data = readFileSync(path, "utf-8");
12
+ return JSON.parse(data);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export function writeState(path, state) {
19
+ try {
20
+ // Atomic write: write to temp file, then rename
21
+ const dir = tmpdir();
22
+ const tempPath = join(dir, `jigit-session-${Date.now()}.json`);
23
+ writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf-8");
24
+ renameSync(tempPath, path);
25
+ }
26
+ catch (err) {
27
+ console.error("[time-tracking] Failed to write state:", err);
28
+ }
29
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@jagit/hook-claude-code-time-tracking",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "bin": {
6
+ "jagit-hook-claude-code-time-tracking": "dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "dependencies": {
12
+ "@jagit/agent-reporter": "0.0.2"
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "^25.9.3",
16
+ "vitest": "^2.1.9"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "typecheck": "tsc -p tsconfig.json --noEmit",
21
+ "test": "vitest run"
22
+ }
23
+ }