@vibe-cafe/vibe-usage 0.7.17 → 0.7.18
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/codex.js +52 -55
package/package.json
CHANGED
package/src/parsers/codex.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createReadStream, readdirSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
4
5
|
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
5
6
|
|
|
6
7
|
const SESSIONS_DIR = join(homedir(), '.codex', 'sessions');
|
|
@@ -27,24 +28,55 @@ function findJsonlFiles(dir) {
|
|
|
27
28
|
return results;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
function readLines(filePath) {
|
|
32
|
+
return createInterface({
|
|
33
|
+
input: createReadStream(filePath, { encoding: 'utf-8' }),
|
|
34
|
+
crlfDelay: Infinity,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractProject(meta) {
|
|
39
|
+
if (meta.git?.repository_url) {
|
|
40
|
+
// e.g. https://github.com/org/repo.git → org/repo
|
|
41
|
+
const match = meta.git.repository_url.match(/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
42
|
+
if (match) return match[1];
|
|
43
|
+
}
|
|
44
|
+
if (meta.cwd) return meta.cwd.split('/').pop() || 'unknown';
|
|
45
|
+
return 'unknown';
|
|
46
|
+
}
|
|
47
|
+
|
|
30
48
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
49
|
+
* Stream a session file once and extract its index metadata: the session
|
|
50
|
+
* id, the forked-from id, the project name, and the total count of
|
|
51
|
+
* `event_msg/token_count` records. The token_count total is used to size
|
|
52
|
+
* the replayed-history block of a forked session — a fork copies the
|
|
53
|
+
* original conversation verbatim, so it begins with exactly as many
|
|
54
|
+
* token_count records as the source session has in total.
|
|
35
55
|
*/
|
|
36
|
-
function
|
|
37
|
-
let
|
|
38
|
-
|
|
56
|
+
async function indexSessionFile(filePath) {
|
|
57
|
+
let sessionId = null;
|
|
58
|
+
let forkedFromId = null;
|
|
59
|
+
let sessionProject = 'unknown';
|
|
60
|
+
let tokenCountRecords = 0;
|
|
61
|
+
|
|
62
|
+
for await (const line of readLines(filePath)) {
|
|
39
63
|
if (!line.trim()) continue;
|
|
40
64
|
try {
|
|
41
65
|
const obj = JSON.parse(line);
|
|
42
|
-
if (obj.type === '
|
|
66
|
+
if (obj.type === 'session_meta' && obj.payload) {
|
|
67
|
+
const meta = obj.payload;
|
|
68
|
+
sessionId = meta.id || sessionId;
|
|
69
|
+
forkedFromId = meta.forked_from_id || null;
|
|
70
|
+
sessionProject = extractProject(meta);
|
|
71
|
+
} else if (obj.type === 'event_msg' && obj.payload?.type === 'token_count') {
|
|
72
|
+
tokenCountRecords++;
|
|
73
|
+
}
|
|
43
74
|
} catch {
|
|
44
75
|
continue;
|
|
45
76
|
}
|
|
46
77
|
}
|
|
47
|
-
|
|
78
|
+
|
|
79
|
+
return { sessionId, forkedFromId, sessionProject, tokenCountRecords };
|
|
48
80
|
}
|
|
49
81
|
|
|
50
82
|
export async function parse() {
|
|
@@ -66,30 +98,17 @@ export async function parse() {
|
|
|
66
98
|
// instant, within the same 1–3s window), so we instead skip exactly the
|
|
67
99
|
// original session's token_count count from the start of each fork.
|
|
68
100
|
const tokenCountById = new Map(); // sessionId → number of token_count records
|
|
69
|
-
const fileMeta = new Map(); // filePath
|
|
101
|
+
const fileMeta = new Map(); // filePath -> { forkedFromId, sessionProject }
|
|
70
102
|
for (const filePath of files) {
|
|
71
|
-
let
|
|
103
|
+
let meta;
|
|
72
104
|
try {
|
|
73
|
-
|
|
105
|
+
meta = await indexSessionFile(filePath);
|
|
74
106
|
} catch {
|
|
75
107
|
continue;
|
|
76
108
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (!line.trim()) continue;
|
|
81
|
-
try {
|
|
82
|
-
const obj = JSON.parse(line);
|
|
83
|
-
if (obj.type === 'session_meta' && obj.payload) {
|
|
84
|
-
sessionId = obj.payload.id || null;
|
|
85
|
-
forkedFromId = obj.payload.forked_from_id || null;
|
|
86
|
-
}
|
|
87
|
-
} catch { /* ignore */ }
|
|
88
|
-
break; // session_meta is always the first line
|
|
89
|
-
}
|
|
90
|
-
fileMeta.set(filePath, { content, forkedFromId });
|
|
91
|
-
if (sessionId) {
|
|
92
|
-
tokenCountById.set(sessionId, countTokenCountRecords(content));
|
|
109
|
+
fileMeta.set(filePath, meta);
|
|
110
|
+
if (meta.sessionId) {
|
|
111
|
+
tokenCountById.set(meta.sessionId, meta.tokenCountRecords);
|
|
93
112
|
}
|
|
94
113
|
}
|
|
95
114
|
|
|
@@ -97,9 +116,7 @@ export async function parse() {
|
|
|
97
116
|
for (const filePath of files) {
|
|
98
117
|
const fm = fileMeta.get(filePath);
|
|
99
118
|
if (!fm) continue;
|
|
100
|
-
const {
|
|
101
|
-
|
|
102
|
-
const lines = content.split('\n');
|
|
119
|
+
const { forkedFromId } = fm;
|
|
103
120
|
|
|
104
121
|
// How many leading token_count records are copied history. A fork's file
|
|
105
122
|
// begins with the *entire* source file replayed verbatim, so the count
|
|
@@ -116,31 +133,11 @@ export async function parse() {
|
|
|
116
133
|
}
|
|
117
134
|
let tokenCountSeen = 0;
|
|
118
135
|
|
|
119
|
-
|
|
120
|
-
let sessionProject = 'unknown';
|
|
121
|
-
let sessionModel = 'unknown';
|
|
122
|
-
for (const line of lines) {
|
|
123
|
-
if (!line.trim()) continue;
|
|
124
|
-
try {
|
|
125
|
-
const obj = JSON.parse(line);
|
|
126
|
-
if (obj.type === 'session_meta' && obj.payload) {
|
|
127
|
-
const meta = obj.payload;
|
|
128
|
-
if (meta.cwd) {
|
|
129
|
-
sessionProject = meta.cwd.split('/').pop() || 'unknown';
|
|
130
|
-
}
|
|
131
|
-
if (meta.git?.repository_url) {
|
|
132
|
-
// e.g. https://github.com/org/repo.git → org/repo
|
|
133
|
-
const match = meta.git.repository_url.match(/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
134
|
-
if (match) sessionProject = match[1];
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
} catch { /* ignore */ }
|
|
138
|
-
break; // session_meta is always the first line
|
|
139
|
-
}
|
|
136
|
+
const sessionProject = fm.sessionProject;
|
|
140
137
|
|
|
141
138
|
let turnContextModel = 'unknown';
|
|
142
139
|
const prevTotal = new Map();
|
|
143
|
-
for (const line of
|
|
140
|
+
for await (const line of readLines(filePath)) {
|
|
144
141
|
if (!line.trim()) continue;
|
|
145
142
|
try {
|
|
146
143
|
const obj = JSON.parse(line);
|
|
@@ -225,7 +222,7 @@ export async function parse() {
|
|
|
225
222
|
if (!usage) continue;
|
|
226
223
|
if (isReplayedHistory) continue;
|
|
227
224
|
|
|
228
|
-
const model = info.model || payload.model || turnContextModel ||
|
|
225
|
+
const model = info.model || payload.model || turnContextModel || 'unknown';
|
|
229
226
|
|
|
230
227
|
// OpenAI API: input_tokens INCLUDES cached, output_tokens INCLUDES reasoning.
|
|
231
228
|
// Normalize to Anthropic-style semantics where each field is non-overlapping.
|