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 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.0",
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
- for (const file of await listCodexRollouts(codexRootDir)) {
79
- const session = await parseCodexSession(file);
80
- if (session === null) continue;
81
- if (issueFromCwd(session.cwd, cwdPattern) === issueIdentifier) sessions.push(session);
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
- for (const file of await listJsonlsRecursively(claudeProjectDir)) {
109
- const session = await parseClaudeSession(file);
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
- for (const file of await listCodexRollouts(codexRootDir)) {
115
- const session = await parseCodexSession(file);
116
- if (session === null) continue;
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);