vibeusage 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeusage",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -190,7 +190,7 @@ function renderWelcome() {
190
190
  DIVIDER,
191
191
  "",
192
192
  "This tool will:",
193
- " - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Kimi, Opencode, Hermes, OpenClaw)",
193
+ " - Analyze your local AI CLI configurations (Codex, Every Code, Claude Code, Gemini, Kimi, Hermes, OpenCode, OpenClaw)",
194
194
  " - Set up lightweight hooks to track your flow state",
195
195
  " - Link your device to your VibeScore account",
196
196
  "",
@@ -8,7 +8,7 @@ const { isDir, isFile } = require("./utils");
8
8
 
9
9
  module.exports = {
10
10
  name: "claude",
11
- summaryLabel: "Claude",
11
+ summaryLabel: "Claude Code",
12
12
  statusLabel: "Claude plugin",
13
13
  async probe(ctx) {
14
14
  const hasConfigDir = await isDir(ctx.claude.configDir);
@@ -3,8 +3,8 @@ const { isDir } = require("./utils");
3
3
 
4
4
  module.exports = {
5
5
  name: "opencode",
6
- summaryLabel: "Opencode Plugin",
7
- statusLabel: "Opencode plugin",
6
+ summaryLabel: "OpenCode Plugin",
7
+ statusLabel: "OpenCode plugin",
8
8
  async probe(ctx) {
9
9
  const hasConfigDir = await isDir(ctx.opencode.configDir);
10
10
  if (!hasConfigDir) {
@@ -246,6 +246,8 @@ async function parseClaudeIncremental({
246
246
  const prev = cursors.files[key] || null;
247
247
  const inode = st.ino || 0;
248
248
  const startOffset = prev && prev.inode === inode ? prev.offset || 0 : 0;
249
+ const priorSeenIds =
250
+ prev && prev.inode === inode && Array.isArray(prev.seenIds) ? prev.seenIds : [];
249
251
 
250
252
  const projectContext = projectEnabled
251
253
  ? await resolveProjectContextForFile({
@@ -269,11 +271,13 @@ async function parseClaudeIncremental({
269
271
  projectTouchedBuckets,
270
272
  projectRef,
271
273
  projectKey,
274
+ priorSeenIds,
272
275
  });
273
276
 
274
277
  cursors.files[key] = {
275
278
  inode,
276
279
  offset: result.endOffset,
280
+ seenIds: result.seenIds,
277
281
  updatedAt: new Date().toISOString(),
278
282
  };
279
283
 
@@ -778,6 +782,8 @@ async function parseRolloutFile({
778
782
  return { endOffset, lastTotal: totals, lastModel: model, eventsAggregated };
779
783
  }
780
784
 
785
+ const CLAUDE_SEEN_IDS_LIMIT = 500;
786
+
781
787
  async function parseClaudeFile({
782
788
  filePath,
783
789
  startOffset,
@@ -788,12 +794,18 @@ async function parseClaudeFile({
788
794
  projectTouchedBuckets,
789
795
  projectRef,
790
796
  projectKey,
797
+ priorSeenIds,
791
798
  }) {
799
+ const seenOrder = Array.isArray(priorSeenIds) ? priorSeenIds.slice() : [];
800
+ const seenSet = new Set(seenOrder);
801
+
792
802
  const st = await fs.stat(filePath).catch(() => null);
793
- if (!st || !st.isFile()) return { endOffset: startOffset, eventsAggregated: 0 };
803
+ if (!st || !st.isFile()) {
804
+ return { endOffset: startOffset, eventsAggregated: 0, seenIds: seenOrder };
805
+ }
794
806
 
795
807
  const endOffset = st.size;
796
- if (startOffset >= endOffset) return { endOffset, eventsAggregated: 0 };
808
+ if (startOffset >= endOffset) return { endOffset, eventsAggregated: 0, seenIds: seenOrder };
797
809
 
798
810
  const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
799
811
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
@@ -811,6 +823,12 @@ async function parseClaudeFile({
811
823
  const usage = obj?.message?.usage || obj?.usage;
812
824
  if (!usage || typeof usage !== "object") continue;
813
825
 
826
+ // Claude Code writes the same assistant message multiple times in the session log
827
+ // (same `message.id` / `requestId`, different outer `uuid`). Aggregate once per
828
+ // upstream Anthropic response to avoid multi-counting token usage.
829
+ const dedupeId = obj?.message?.id || obj?.requestId || null;
830
+ if (dedupeId && seenSet.has(dedupeId)) continue;
831
+
814
832
  const model = normalizeModelInput(obj?.message?.model || obj?.model) || DEFAULT_MODEL;
815
833
  const tokenTimestamp = typeof obj?.timestamp === "string" ? obj.timestamp : null;
816
834
  if (!tokenTimestamp) continue;
@@ -835,12 +853,20 @@ async function parseClaudeFile({
835
853
  addTotals(projectBucket.totals, delta);
836
854
  projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
837
855
  }
856
+ if (dedupeId) {
857
+ seenSet.add(dedupeId);
858
+ seenOrder.push(dedupeId);
859
+ }
838
860
  eventsAggregated += 1;
839
861
  }
840
862
 
841
863
  rl.close();
842
864
  stream.close?.();
843
- return { endOffset, eventsAggregated };
865
+ const trimmedSeenIds =
866
+ seenOrder.length > CLAUDE_SEEN_IDS_LIMIT
867
+ ? seenOrder.slice(seenOrder.length - CLAUDE_SEEN_IDS_LIMIT)
868
+ : seenOrder;
869
+ return { endOffset, eventsAggregated, seenIds: trimmedSeenIds };
844
870
  }
845
871
 
846
872
  async function parseKimiFile({
@@ -2181,7 +2207,10 @@ function normalizeOpencodeTokens(tokens) {
2181
2207
  const cached = toNonNegativeInt(tokens.cache?.read);
2182
2208
  const cacheWrite = toNonNegativeInt(tokens.cache?.write);
2183
2209
  const inputTokens = input + cacheWrite;
2184
- const total = inputTokens + output + reasoning;
2210
+ // Include cache-read tokens in the total so OpenCode sessions do not
2211
+ // under-count the way Claude did before the parallel fix; cache-read is
2212
+ // real spend the user pays for on every turn.
2213
+ const total = inputTokens + cached + output + reasoning;
2185
2214
 
2186
2215
  return {
2187
2216
  input_tokens: inputTokens,
@@ -2304,12 +2333,19 @@ function normalizeUsage(u) {
2304
2333
  function normalizeClaudeUsage(u) {
2305
2334
  const inputTokens =
2306
2335
  toNonNegativeInt(u?.input_tokens) + toNonNegativeInt(u?.cache_creation_input_tokens);
2336
+ const cachedInputTokens = toNonNegativeInt(u?.cache_read_input_tokens);
2307
2337
  const outputTokens = toNonNegativeInt(u?.output_tokens);
2308
2338
  const hasTotal = u && Object.prototype.hasOwnProperty.call(u, "total_tokens");
2309
- const totalTokens = hasTotal ? toNonNegativeInt(u?.total_tokens) : inputTokens + outputTokens;
2339
+ // Claude's Messages API does not emit `total_tokens`. When absent, compose it
2340
+ // from all four channels (input / cache_creation / cache_read / output). The
2341
+ // old formula omitted cache_read, which is ~99% of token spend on long
2342
+ // Claude Opus sessions and was the main driver of user-visible under-counts.
2343
+ const totalTokens = hasTotal
2344
+ ? toNonNegativeInt(u?.total_tokens)
2345
+ : inputTokens + cachedInputTokens + outputTokens;
2310
2346
  return {
2311
2347
  input_tokens: inputTokens,
2312
- cached_input_tokens: toNonNegativeInt(u?.cache_read_input_tokens),
2348
+ cached_input_tokens: cachedInputTokens,
2313
2349
  output_tokens: outputTokens,
2314
2350
  reasoning_output_tokens: 0,
2315
2351
  total_tokens: totalTokens,