@jagit/hook-codex 0.0.0

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 ADDED
@@ -0,0 +1,42 @@
1
+ # @jagit/hook-codex
2
+
3
+ Reports per-session OpenAI Codex CLI usage to JaGit.
4
+
5
+ ## Setup
6
+
7
+ Codex has no built-in hook mechanism, so install a shell function that wraps
8
+ the real `codex` binary and reports after each session ends:
9
+
10
+ codex() {
11
+ command codex "$@"
12
+ local status=$?
13
+ npx -y @jagit/hook-codex >/dev/null 2>&1 || true
14
+ return $status
15
+ }
16
+
17
+ Add that function to your shell rc (`~/.zshrc`, `~/.bashrc`, etc.) — it must
18
+ appear before any `alias`/`PATH` entry that would otherwise shadow it. On
19
+ exit, the reporter locates the most-recently-modified file under
20
+ `~/.codex/sessions/**/*.jsonl`, parses it, and posts the session summary.
21
+ Uninstall by removing the shell function.
22
+
23
+ For a permanent binary instead of `npx -y`:
24
+ `npm i -g @jagit/hook-codex`, then call `jagit-hook-codex` in the wrapper.
25
+
26
+ You can also point the reporter at a specific transcript:
27
+
28
+ jagit-hook-codex --file ~/.codex/sessions/2026/06/20/rollout-...jsonl
29
+
30
+ ## Environment
31
+
32
+ export JAGIT_BASE_URL="https://your-jagit-host"
33
+ export JAGIT_API_KEY="<your DASHBOARD_API_TOKEN>"
34
+
35
+ Identity defaults to `git config user.email` (read from the session's `cwd`);
36
+ override with `JAGIT_GIT_USERNAME`.
37
+
38
+ ## Notes
39
+
40
+ - Token counts are cumulative per Codex session; the reporter reads the last
41
+ non-null `token_count` event, not a sum across events.
42
+ - `costUsd` is always `null` — Codex JSONL logs do not expose a cost field.
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { type AgentSessionPayload } from "@jagit/agent-reporter";
3
+ export interface CodexRecord {
4
+ type?: string;
5
+ timestamp?: string;
6
+ payload?: {
7
+ id?: string;
8
+ cwd?: string;
9
+ timestamp?: string;
10
+ model?: string;
11
+ type?: string;
12
+ info?: {
13
+ total_token_usage?: {
14
+ input_tokens?: number;
15
+ cached_input_tokens?: number;
16
+ output_tokens?: number;
17
+ };
18
+ } | null;
19
+ };
20
+ }
21
+ export declare function buildPayload(sessionId: string, cwd: string | undefined, records: CodexRecord[]): AgentSessionPayload;
package/dist/index.js ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { realpathSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { resolveGitUsername, reportSession } from "@jagit/agent-reporter";
8
+ const TOOL_CALL_SUBTYPES = new Set(["function_call", "custom_tool_call", "web_search_call"]);
9
+ export function buildPayload(sessionId, cwd, records) {
10
+ let model = "unknown";
11
+ let inputTokens = 0;
12
+ let cachedInputTokens = 0;
13
+ let outputTokens = 0;
14
+ let toolCallCount = 0;
15
+ for (const r of records) {
16
+ if (r.type === "turn_context" && r.payload?.model) {
17
+ model = r.payload.model;
18
+ }
19
+ if (r.type === "event_msg" && r.payload?.type === "token_count" && r.payload.info) {
20
+ const usage = r.payload.info.total_token_usage;
21
+ if (usage) {
22
+ inputTokens = usage.input_tokens ?? 0;
23
+ cachedInputTokens = usage.cached_input_tokens ?? 0;
24
+ outputTokens = usage.output_tokens ?? 0;
25
+ }
26
+ }
27
+ if (r.type === "response_item" && r.payload?.type && TOOL_CALL_SUBTYPES.has(r.payload.type)) {
28
+ toolCallCount += 1;
29
+ }
30
+ }
31
+ const sessionMeta = records.find((r) => r.type === "session_meta");
32
+ const startedAt = records.find((r) => r.timestamp)?.timestamp ?? sessionMeta?.payload?.timestamp ?? new Date().toISOString();
33
+ return {
34
+ tool: "codex",
35
+ sessionId,
36
+ gitUsername: resolveGitUsername(cwd),
37
+ model,
38
+ inputTokens,
39
+ cachedInputTokens,
40
+ outputTokens,
41
+ costUsd: null,
42
+ toolCallCount,
43
+ startedAt,
44
+ };
45
+ }
46
+ function readJsonl(path) {
47
+ return readFileSync(path, "utf-8")
48
+ .split("\n")
49
+ .filter((l) => l.trim().length > 0)
50
+ .map((l) => {
51
+ try {
52
+ return JSON.parse(l);
53
+ }
54
+ catch {
55
+ return {};
56
+ }
57
+ });
58
+ }
59
+ function findLatestSessionFile(root) {
60
+ let latestPath;
61
+ let latestMtime = -Infinity;
62
+ function walk(dir) {
63
+ let entries;
64
+ try {
65
+ entries = readdirSync(dir);
66
+ }
67
+ catch {
68
+ return;
69
+ }
70
+ for (const entry of entries) {
71
+ const fullPath = join(dir, entry);
72
+ let stat;
73
+ try {
74
+ stat = statSync(fullPath);
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ if (stat.isDirectory()) {
80
+ walk(fullPath);
81
+ }
82
+ else if (entry.endsWith(".jsonl") && stat.mtimeMs > latestMtime) {
83
+ latestMtime = stat.mtimeMs;
84
+ latestPath = fullPath;
85
+ }
86
+ }
87
+ }
88
+ walk(root);
89
+ return latestPath;
90
+ }
91
+ function parseArgs(argv) {
92
+ const fileIdx = argv.indexOf("--file");
93
+ if (fileIdx !== -1 && argv[fileIdx + 1]) {
94
+ return { file: argv[fileIdx + 1] };
95
+ }
96
+ return {};
97
+ }
98
+ async function main() {
99
+ try {
100
+ const { file } = parseArgs(process.argv.slice(2));
101
+ const filePath = file ?? findLatestSessionFile(join(homedir(), ".codex", "sessions"));
102
+ if (!filePath)
103
+ return;
104
+ const records = readJsonl(filePath);
105
+ const sessionMeta = records.find((r) => r.type === "session_meta");
106
+ const sessionId = sessionMeta?.payload?.id ?? filePath;
107
+ const cwd = sessionMeta?.payload?.cwd;
108
+ await reportSession(buildPayload(sessionId, cwd, records));
109
+ }
110
+ catch (err) {
111
+ console.error("[hook-codex]", err instanceof Error ? err.message : err);
112
+ }
113
+ finally {
114
+ process.exit(0);
115
+ }
116
+ }
117
+ const isMain = import.meta.url.startsWith("file://") &&
118
+ realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
119
+ if (isMain)
120
+ void main();
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@jagit/hook-codex",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "jagit-hook-codex": "dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "typecheck": "tsc -p tsconfig.json --noEmit",
14
+ "test": "vitest run"
15
+ },
16
+ "dependencies": {
17
+ "@jagit/agent-reporter": "workspace:*"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^25.9.3",
21
+ "vitest": "^2.1.9"
22
+ }
23
+ }