@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 +1 -0
- package/dist/git.js +14 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +91 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +67 -0
- package/dist/state.d.ts +9 -0
- package/dist/state.js +29 -0
- package/package.json +23 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
});
|
package/dist/state.d.ts
ADDED
|
@@ -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
|
+
}
|