llm-cost-attribution 0.1.0 → 0.1.1
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/bin/llm-cost.mjs +18 -0
- package/package.json +1 -1
- package/src/index.mjs +18 -10
package/bin/llm-cost.mjs
CHANGED
|
@@ -63,6 +63,7 @@ async function main() {
|
|
|
63
63
|
const options = { cwdPattern };
|
|
64
64
|
if (values['claude-dir'] !== undefined) options.claudeProjectsDir = values['claude-dir'];
|
|
65
65
|
if (values['codex-dir'] !== undefined) options.codexSessionsDir = values['codex-dir'];
|
|
66
|
+
if (process.stderr.isTTY) options.onProgress = makeProgressReporter();
|
|
66
67
|
|
|
67
68
|
const withPricing = values['no-pricing'] !== true;
|
|
68
69
|
|
|
@@ -160,6 +161,23 @@ async function main() {
|
|
|
160
161
|
printMultiIssueRollup(multi, fromUsage !== undefined, withPricing);
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Returns an onProgress callback that writes a live scan counter to stderr,
|
|
166
|
+
* overwriting the same line each tick. Clears the line when the Codex phase
|
|
167
|
+
* completes so the output table starts on a clean line.
|
|
168
|
+
* Only wired up when stderr is a TTY (not when piping --json output).
|
|
169
|
+
*/
|
|
170
|
+
function makeProgressReporter() {
|
|
171
|
+
return ({ phase, processed, total }) => {
|
|
172
|
+
const pct = total === 0 ? 100 : Math.round((processed / total) * 100);
|
|
173
|
+
process.stderr.write(
|
|
174
|
+
` scanning ${phase} sessions: ${processed.toLocaleString()} / ${total.toLocaleString()} (${pct}%)\r`,
|
|
175
|
+
);
|
|
176
|
+
// Clear the line once each phase finishes so the results table is uncluttered.
|
|
177
|
+
if (processed === total) process.stderr.write(' '.repeat(60) + '\r');
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
163
181
|
function attachPricingToRollup(rollup) {
|
|
164
182
|
for (const provider of ['claude', 'codex']) {
|
|
165
183
|
const totals = rollup.providerTotals[provider];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cost-attribution",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Per-issue token, turn, and quota analytics for Claude Code and Codex CLI sessions. Reads the CLIs' own session JSONLs — no telemetry pipeline required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/index.mjs
CHANGED
|
@@ -59,6 +59,7 @@ export async function computeIssueCost(issueIdentifier, options = {}) {
|
|
|
59
59
|
const cwdPattern = options.cwdPattern ?? DEFAULT_CWD_PATTERN;
|
|
60
60
|
const claudeRootDir = options.claudeProjectsDir ?? join(homedir(), '.claude', 'projects');
|
|
61
61
|
const codexRootDir = options.codexSessionsDir ?? join(homedir(), '.codex', 'sessions');
|
|
62
|
+
const onProgress = options.onProgress ?? (() => undefined);
|
|
62
63
|
|
|
63
64
|
const sessions = [];
|
|
64
65
|
|
|
@@ -75,10 +76,13 @@ export async function computeIssueCost(issueIdentifier, options = {}) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
// Codex: session_meta.cwd match, scanned across all rollouts.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (issueFromCwd(session.cwd, cwdPattern) === issueIdentifier)
|
|
79
|
+
const codexFiles = await listCodexRollouts(codexRootDir);
|
|
80
|
+
for (let i = 0; i < codexFiles.length; i++) {
|
|
81
|
+
const session = await parseCodexSession(codexFiles[i]);
|
|
82
|
+
if (session !== null && issueFromCwd(session.cwd, cwdPattern) === issueIdentifier) {
|
|
83
|
+
sessions.push(session);
|
|
84
|
+
}
|
|
85
|
+
onProgress({ phase: 'codex', processed: i + 1, total: codexFiles.length });
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
return rollupSessions(issueIdentifier, sessions);
|
|
@@ -98,6 +102,7 @@ export async function computeIssueCost(issueIdentifier, options = {}) {
|
|
|
98
102
|
export async function computeWorktreeCost(worktreePath, options = {}) {
|
|
99
103
|
const claudeRootDir = options.claudeProjectsDir ?? join(homedir(), '.claude', 'projects');
|
|
100
104
|
const codexRootDir = options.codexSessionsDir ?? join(homedir(), '.codex', 'sessions');
|
|
105
|
+
const onProgress = options.onProgress ?? (() => undefined);
|
|
101
106
|
|
|
102
107
|
const sessions = [];
|
|
103
108
|
|
|
@@ -105,16 +110,19 @@ export async function computeWorktreeCost(worktreePath, options = {}) {
|
|
|
105
110
|
// `.` replaced by `-`. Look it up directly — no regex needed.
|
|
106
111
|
const encodedPath = worktreePath.replace(/[/.]/g, '-');
|
|
107
112
|
const claudeProjectDir = join(claudeRootDir, encodedPath);
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
const claudeFiles = await listJsonlsRecursively(claudeProjectDir);
|
|
114
|
+
for (let i = 0; i < claudeFiles.length; i++) {
|
|
115
|
+
const session = await parseClaudeSession(claudeFiles[i]);
|
|
110
116
|
if (session !== null) sessions.push(session);
|
|
117
|
+
onProgress({ phase: 'claude', processed: i + 1, total: claudeFiles.length });
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
// Codex: scan all rollouts, keep those whose session_meta.cwd matches exactly.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (session.cwd === worktreePath) sessions.push(session);
|
|
121
|
+
const codexFiles = await listCodexRollouts(codexRootDir);
|
|
122
|
+
for (let i = 0; i < codexFiles.length; i++) {
|
|
123
|
+
const session = await parseCodexSession(codexFiles[i]);
|
|
124
|
+
if (session !== null && session.cwd === worktreePath) sessions.push(session);
|
|
125
|
+
onProgress({ phase: 'codex', processed: i + 1, total: codexFiles.length });
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
return rollupSessions(basename(worktreePath), sessions);
|