@vibe-cafe/vibe-usage 0.2.3 → 0.2.5
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 +1 -1
- package/src/parsers/claude-code.js +88 -10
- package/src/parsers/index.js +3 -1
- package/src/sync.js +10 -2
package/package.json
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { loadSessionData } from 'ccusage/data-loader';
|
|
2
2
|
import { aggregateToBuckets } from './index.js';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
6
|
+
|
|
7
|
+
const STATE_FILE = join(homedir(), '.vibe-usage', 'claude-code-state.json');
|
|
8
|
+
|
|
9
|
+
/** Pending state staged during parse(), committed only after successful upload. */
|
|
10
|
+
let _pendingState = null;
|
|
11
|
+
|
|
12
|
+
function loadState() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(STATE_FILE, 'utf-8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function saveState(state) {
|
|
21
|
+
const dir = join(homedir(), '.vibe-usage');
|
|
22
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
23
|
+
writeFileSync(STATE_FILE, JSON.stringify(state), 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Commit pending state to disk.
|
|
28
|
+
* Called by sync.js AFTER successful upload to ensure we only advance
|
|
29
|
+
* the watermark when data has been safely delivered to the server.
|
|
30
|
+
*/
|
|
31
|
+
export function commitState() {
|
|
32
|
+
if (_pendingState) {
|
|
33
|
+
saveState(_pendingState);
|
|
34
|
+
_pendingState = null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
3
37
|
|
|
4
38
|
export async function parse(lastSync) {
|
|
5
39
|
let sessions;
|
|
@@ -11,36 +45,80 @@ export async function parse(lastSync) {
|
|
|
11
45
|
|
|
12
46
|
if (!sessions || sessions.length === 0) return [];
|
|
13
47
|
|
|
48
|
+
const state = loadState();
|
|
49
|
+
const nextState = { ...state };
|
|
14
50
|
const entries = [];
|
|
15
51
|
|
|
16
52
|
for (const session of sessions) {
|
|
17
53
|
if (lastSync && new Date(session.lastActivity) <= new Date(lastSync)) continue;
|
|
18
54
|
|
|
55
|
+
const project = resolveProject(session);
|
|
56
|
+
const sessionKey = `${session.projectPath}\0${session.sessionId}`;
|
|
57
|
+
const prev = state[sessionKey] || {};
|
|
58
|
+
|
|
19
59
|
for (const breakdown of session.modelBreakdowns || []) {
|
|
60
|
+
const model = breakdown.modelName;
|
|
61
|
+
const prevModel = prev[model] || { i: 0, o: 0, c: 0 };
|
|
62
|
+
|
|
63
|
+
const deltaInput = (breakdown.inputTokens || 0) - (prevModel.i || 0);
|
|
64
|
+
const deltaOutput = (breakdown.outputTokens || 0) - (prevModel.o || 0);
|
|
65
|
+
const deltaCached = (breakdown.cacheReadTokens || 0) - (prevModel.c || 0);
|
|
66
|
+
|
|
67
|
+
// Always record current cumulative totals for next sync
|
|
68
|
+
if (!nextState[sessionKey]) nextState[sessionKey] = {};
|
|
69
|
+
nextState[sessionKey][model] = {
|
|
70
|
+
i: breakdown.inputTokens || 0,
|
|
71
|
+
o: breakdown.outputTokens || 0,
|
|
72
|
+
c: breakdown.cacheReadTokens || 0,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Only emit entries with positive deltas
|
|
76
|
+
if (deltaInput <= 0 && deltaOutput <= 0 && deltaCached <= 0) continue;
|
|
77
|
+
|
|
20
78
|
entries.push({
|
|
21
79
|
source: 'claude-code',
|
|
22
|
-
model
|
|
23
|
-
project
|
|
80
|
+
model,
|
|
81
|
+
project,
|
|
24
82
|
timestamp: new Date(session.lastActivity),
|
|
25
|
-
inputTokens:
|
|
26
|
-
outputTokens:
|
|
27
|
-
cachedInputTokens:
|
|
83
|
+
inputTokens: Math.max(0, deltaInput),
|
|
84
|
+
outputTokens: Math.max(0, deltaOutput),
|
|
85
|
+
cachedInputTokens: Math.max(0, deltaCached),
|
|
28
86
|
reasoningOutputTokens: 0,
|
|
29
87
|
});
|
|
30
88
|
}
|
|
31
89
|
}
|
|
32
90
|
|
|
91
|
+
// Stage state — only persisted to disk after successful upload
|
|
92
|
+
_pendingState = nextState;
|
|
93
|
+
|
|
33
94
|
return aggregateToBuckets(entries);
|
|
34
95
|
}
|
|
35
96
|
|
|
36
97
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
98
|
+
* Resolve project name from ccusage session data.
|
|
99
|
+
*
|
|
100
|
+
* ccusage v18 assumes 3-layer: projects/{projectPath}/{sessionId}/{file}.jsonl
|
|
101
|
+
* but Claude Code main sessions are 2-layer: projects/{projectPath}/{sessionId}.jsonl
|
|
102
|
+
*
|
|
103
|
+
* For 2-layer files ccusage incorrectly puts the project dir name into sessionId
|
|
104
|
+
* and sets projectPath to "Unknown Project". We detect and correct this.
|
|
105
|
+
*/
|
|
106
|
+
function resolveProject(session) {
|
|
107
|
+
if (session.projectPath === 'Unknown Project') {
|
|
108
|
+
// 2-layer: sessionId actually holds the project directory name
|
|
109
|
+
return cleanProjectDir(session.sessionId);
|
|
110
|
+
}
|
|
111
|
+
// 3-layer: projectPath is correct, strip any session UUID suffix
|
|
112
|
+
return cleanProjectDir(session.projectPath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clean a raw project directory name from ccusage.
|
|
117
|
+
* Strips session UUID suffix for subagent paths like '-Users-foo-project/77e854f9-...'.
|
|
40
118
|
*/
|
|
41
|
-
function
|
|
119
|
+
function cleanProjectDir(raw) {
|
|
42
120
|
if (!raw || raw === 'unknown' || raw === 'Unknown Project') return 'unknown';
|
|
43
121
|
const slashIdx = raw.indexOf('/');
|
|
44
|
-
if (slashIdx !== -1)
|
|
122
|
+
if (slashIdx !== -1) raw = raw.slice(0, slashIdx);
|
|
45
123
|
return raw;
|
|
46
124
|
}
|
package/src/parsers/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parse as parseClaudeCode } from './claude-code.js';
|
|
1
|
+
import { parse as parseClaudeCode, commitState as commitClaudeCodeState } from './claude-code.js';
|
|
2
2
|
import { parse as parseCodex } from './codex.js';
|
|
3
3
|
import { parse as parseGeminiCli } from './gemini-cli.js';
|
|
4
4
|
import { parse as parseOpencode } from './opencode.js';
|
|
@@ -12,6 +12,8 @@ export const parsers = {
|
|
|
12
12
|
'openclaw': parseOpenclaw,
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
export const postSyncHooks = [commitClaudeCodeState];
|
|
16
|
+
|
|
15
17
|
export function roundToHalfHour(date) {
|
|
16
18
|
const d = new Date(date);
|
|
17
19
|
d.setMinutes(d.getMinutes() < 30 ? 0 : 30, 0, 0);
|
package/src/sync.js
CHANGED
|
@@ -4,10 +4,10 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { loadConfig, saveConfig } from './config.js';
|
|
6
6
|
import { ingest } from './api.js';
|
|
7
|
-
import { parsers } from './parsers/index.js';
|
|
7
|
+
import { parsers, postSyncHooks } from './parsers/index.js';
|
|
8
8
|
import { TOOLS } from './hooks.js';
|
|
9
9
|
|
|
10
|
-
const BATCH_SIZE =
|
|
10
|
+
const BATCH_SIZE = 100;
|
|
11
11
|
|
|
12
12
|
function formatBytes(bytes) {
|
|
13
13
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -75,6 +75,14 @@ export async function runSync() {
|
|
|
75
75
|
saveConfig(config);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
|
|
79
|
+
// Commit parser state now that all data has been uploaded successfully.
|
|
80
|
+
// State is staged during parse() but only persisted here to prevent
|
|
81
|
+
// data loss if uploads fail (deltas would be re-computed on retry).
|
|
82
|
+
for (const hook of postSyncHooks) {
|
|
83
|
+
try { hook(); } catch { /* best effort */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
if (totalBatches > 1 || allBuckets.length > 0) {
|
|
79
87
|
process.stdout.write('\n');
|
|
80
88
|
}
|