@vibe-cafe/vibe-usage 0.10.6 → 0.10.9
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/README.md +10 -6
- package/package.json +1 -1
- package/src/cline-roots.js +40 -0
- package/src/craft-roots.js +15 -0
- package/src/parsers/alma.js +91 -0
- package/src/parsers/amp.js +16 -4
- package/src/parsers/cline.js +26 -40
- package/src/parsers/craft-agent.js +21 -0
- package/src/parsers/index.js +8 -0
- package/src/parsers/omp.js +10 -0
- package/src/parsers/openclaw.js +24 -2
- package/src/parsers/pi-coding-agent.js +7 -141
- package/src/parsers/pi-session-jsonl.js +148 -0
- package/src/parsers/workbuddy.js +319 -0
- package/src/pi-roots.js +89 -0
- package/src/sync.js +13 -2
- package/src/tools.js +48 -3
- package/src/workbuddy-roots.js +22 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join, relative } from 'node:path';
|
|
3
|
+
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
4
|
+
|
|
5
|
+
const MAX_WARNINGS = 20;
|
|
6
|
+
|
|
7
|
+
function warn(ctx, message) {
|
|
8
|
+
ctx.incomplete = true;
|
|
9
|
+
if (ctx.warnings.length < MAX_WARNINGS) ctx.warnings.push(message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function findJsonlFiles(dir, includeFile, ctx) {
|
|
13
|
+
if (!existsSync(dir)) return [];
|
|
14
|
+
let children;
|
|
15
|
+
try {
|
|
16
|
+
children = readdirSync(dir, { withFileTypes: true });
|
|
17
|
+
} catch (err) {
|
|
18
|
+
warn(ctx, `${ctx.source}: cannot read directory ${dir}: ${err.message}`);
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const files = [];
|
|
23
|
+
for (const child of children) {
|
|
24
|
+
const filePath = join(dir, child.name);
|
|
25
|
+
if (child.isDirectory()) files.push(...findJsonlFiles(filePath, includeFile, ctx));
|
|
26
|
+
else if (child.name.endsWith('.jsonl') && includeFile(filePath)) files.push(filePath);
|
|
27
|
+
}
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function tokenCount(value) {
|
|
32
|
+
const number = Number(value);
|
|
33
|
+
return Number.isFinite(number) && number > 0 ? number : 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function projectFromCwd(cwd) {
|
|
37
|
+
if (typeof cwd !== 'string') return 'unknown';
|
|
38
|
+
const parts = cwd.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
39
|
+
return parts.at(-1) || 'unknown';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function projectFromFirstDir(filePath, sessionsDir) {
|
|
43
|
+
const first = relative(sessionsDir, filePath).split(/[\\/]/)[0];
|
|
44
|
+
if (!first) return 'unknown';
|
|
45
|
+
return first.split('-').filter(Boolean).at(-1) || 'unknown';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function parsePiSessionJsonl({
|
|
49
|
+
source,
|
|
50
|
+
sessionsDirs,
|
|
51
|
+
includeFile = () => true,
|
|
52
|
+
projectFromPath = projectFromFirstDir,
|
|
53
|
+
}) {
|
|
54
|
+
const ctx = { source, warnings: [], incomplete: false };
|
|
55
|
+
const entriesById = new Map();
|
|
56
|
+
const anonymousEntries = [];
|
|
57
|
+
const eventsById = new Map();
|
|
58
|
+
const anonymousEvents = [];
|
|
59
|
+
|
|
60
|
+
for (const sessionsDir of sessionsDirs) {
|
|
61
|
+
for (const filePath of findJsonlFiles(sessionsDir, includeFile, ctx)) {
|
|
62
|
+
let content;
|
|
63
|
+
try {
|
|
64
|
+
content = readFileSync(filePath, 'utf8');
|
|
65
|
+
} catch (err) {
|
|
66
|
+
warn(ctx, `${source}: cannot read ${filePath}: ${err.message}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let sessionId = basename(filePath, '.jsonl');
|
|
71
|
+
let project = projectFromPath(filePath, sessionsDir) || 'unknown';
|
|
72
|
+
|
|
73
|
+
for (const line of content.split('\n')) {
|
|
74
|
+
if (!line.trim()) continue;
|
|
75
|
+
let obj;
|
|
76
|
+
try {
|
|
77
|
+
obj = JSON.parse(line);
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (obj.type === 'session') {
|
|
83
|
+
if (obj.id) sessionId = String(obj.id);
|
|
84
|
+
if (obj.cwd) project = projectFromCwd(obj.cwd);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (obj.type !== 'message' || !obj.message) continue;
|
|
88
|
+
|
|
89
|
+
const message = obj.message;
|
|
90
|
+
const timestamp = new Date(obj.timestamp || message.timestamp || 0);
|
|
91
|
+
if (Number.isNaN(timestamp.getTime())) continue;
|
|
92
|
+
const recordId = obj.id ? `${sessionId}:${obj.id}` : null;
|
|
93
|
+
|
|
94
|
+
if (message.role === 'user' || message.role === 'assistant' || message.role === 'toolResult') {
|
|
95
|
+
const event = {
|
|
96
|
+
sessionId,
|
|
97
|
+
source,
|
|
98
|
+
project,
|
|
99
|
+
timestamp,
|
|
100
|
+
role: message.role === 'user' ? 'user' : 'assistant',
|
|
101
|
+
};
|
|
102
|
+
if (recordId) eventsById.set(recordId, event);
|
|
103
|
+
else anonymousEvents.push(event);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (message.role !== 'assistant' || !message.usage) continue;
|
|
107
|
+
const usage = message.usage;
|
|
108
|
+
const inputTokens = tokenCount(usage.input) + tokenCount(usage.cacheWrite);
|
|
109
|
+
const reasoningOutputTokens = tokenCount(usage.reasoningTokens);
|
|
110
|
+
// OMP/Pi usage.output includes reasoning; the shared bucket contract
|
|
111
|
+
// stores non-reasoning output and reasoning separately.
|
|
112
|
+
const outputTokens = Math.max(0, tokenCount(usage.output) - reasoningOutputTokens);
|
|
113
|
+
const cachedInputTokens = tokenCount(usage.cacheRead);
|
|
114
|
+
const score = inputTokens + outputTokens + cachedInputTokens + reasoningOutputTokens;
|
|
115
|
+
if (score === 0) continue;
|
|
116
|
+
|
|
117
|
+
const entry = {
|
|
118
|
+
source,
|
|
119
|
+
model: message.model || message.modelId || obj.model || obj.modelId || 'unknown',
|
|
120
|
+
project,
|
|
121
|
+
timestamp,
|
|
122
|
+
inputTokens,
|
|
123
|
+
outputTokens,
|
|
124
|
+
cachedInputTokens,
|
|
125
|
+
reasoningOutputTokens,
|
|
126
|
+
};
|
|
127
|
+
if (!recordId) {
|
|
128
|
+
anonymousEntries.push(entry);
|
|
129
|
+
} else {
|
|
130
|
+
const current = entriesById.get(recordId);
|
|
131
|
+
if (!current || score > current.score) entriesById.set(recordId, { score, entry });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const entries = [
|
|
138
|
+
...anonymousEntries,
|
|
139
|
+
...[...entriesById.values()].map(({ entry }) => entry),
|
|
140
|
+
];
|
|
141
|
+
const events = [...anonymousEvents, ...eventsById.values()];
|
|
142
|
+
return {
|
|
143
|
+
buckets: aggregateToBuckets(entries),
|
|
144
|
+
sessions: extractSessions(events),
|
|
145
|
+
...(ctx.incomplete ? { skipped: true } : {}),
|
|
146
|
+
...(ctx.warnings.length > 0 ? { warnings: ctx.warnings } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { createReadStream, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { basename, join, relative, sep } from 'node:path';
|
|
4
|
+
import { findWorkbuddyDataDirs } from '../workbuddy-roots.js';
|
|
5
|
+
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
6
|
+
|
|
7
|
+
const SOURCE = 'workbuddy';
|
|
8
|
+
const MAX_WARNINGS = 20;
|
|
9
|
+
|
|
10
|
+
function warn(ctx, message) {
|
|
11
|
+
ctx.skipped = true;
|
|
12
|
+
if (ctx.warnings.length < MAX_WARNINGS && !ctx.warnings.includes(message)) {
|
|
13
|
+
ctx.warnings.push(message);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function finite(value) {
|
|
18
|
+
const number = Number(value);
|
|
19
|
+
return Number.isFinite(number) && number >= 0 ? number : 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function dateFrom(value) {
|
|
23
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) return value;
|
|
24
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
25
|
+
const date = new Date(value < 1e12 ? value * 1000 : value);
|
|
26
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === 'string' && value.trim()) {
|
|
29
|
+
const date = new Date(value);
|
|
30
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function projectFromFile(filePath, projectsDir) {
|
|
36
|
+
const first = relative(projectsDir, filePath).split(sep).filter(Boolean)[0];
|
|
37
|
+
return first ? basename(first) : 'unknown';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function projectFromRecord(record) {
|
|
41
|
+
const cwd = typeof record.cwd === 'string' ? record.cwd.trim() : '';
|
|
42
|
+
if (!cwd) return null;
|
|
43
|
+
const parts = cwd
|
|
44
|
+
.replace(/[\\/]+$/, '')
|
|
45
|
+
.split(/[\\/]/)
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.filter(part => !/^[a-zA-Z]:$/.test(part));
|
|
48
|
+
return parts.at(-1) || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findJsonlFiles(dir, ctx) {
|
|
52
|
+
let children;
|
|
53
|
+
try {
|
|
54
|
+
children = readdirSync(dir, { withFileTypes: true })
|
|
55
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error?.code === 'ENOENT') return [];
|
|
58
|
+
warn(ctx, 'workbuddy: cannot read a data directory');
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const files = [];
|
|
63
|
+
for (const child of children) {
|
|
64
|
+
const filePath = join(dir, child.name);
|
|
65
|
+
if (child.isDirectory()) files.push(...findJsonlFiles(filePath, ctx));
|
|
66
|
+
else if (child.isFile() && child.name.endsWith('.jsonl')) files.push(filePath);
|
|
67
|
+
}
|
|
68
|
+
return files;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function readJsonl(filePath, size, onRecord, ctx) {
|
|
72
|
+
if (size <= 0) return;
|
|
73
|
+
const stream = createReadStream(filePath, {
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
start: 0,
|
|
76
|
+
end: size - 1,
|
|
77
|
+
});
|
|
78
|
+
let streamError = null;
|
|
79
|
+
stream.on('error', error => { streamError = error; });
|
|
80
|
+
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
for await (const line of lines) {
|
|
84
|
+
if (!line.trim()) continue;
|
|
85
|
+
let record;
|
|
86
|
+
try {
|
|
87
|
+
record = JSON.parse(line);
|
|
88
|
+
} catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (record && typeof record === 'object') onRecord(record);
|
|
92
|
+
}
|
|
93
|
+
if (streamError) throw streamError;
|
|
94
|
+
} catch {
|
|
95
|
+
warn(ctx, 'workbuddy: cannot read a session file');
|
|
96
|
+
} finally {
|
|
97
|
+
lines.close();
|
|
98
|
+
stream.destroy();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function recordId(record) {
|
|
103
|
+
if (typeof record.id !== 'string' && typeof record.id !== 'number') return null;
|
|
104
|
+
const id = String(record.id).trim();
|
|
105
|
+
return id || null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function roleFor(record) {
|
|
109
|
+
const role = record.role ?? record.message?.role;
|
|
110
|
+
if (role === 'user') return 'user';
|
|
111
|
+
if (role === 'assistant' || role === 'assistant_message') return 'assistant';
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isCompletedAssistant(record) {
|
|
116
|
+
if (record.type !== 'message' || roleFor(record) !== 'assistant') return false;
|
|
117
|
+
const status = String(
|
|
118
|
+
record.status ?? record.message?.status ?? record.state ?? record.message?.state ?? ''
|
|
119
|
+
).toLowerCase();
|
|
120
|
+
return status === 'completed' || status === 'complete' || status === 'success';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isUsageRecord(record) {
|
|
124
|
+
return isCompletedAssistant(record)
|
|
125
|
+
|| (record.type === 'function_call'
|
|
126
|
+
&& record.providerData
|
|
127
|
+
&& typeof record.providerData === 'object');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function modelFor(record) {
|
|
131
|
+
const providerData = record.providerData && typeof record.providerData === 'object'
|
|
132
|
+
? record.providerData
|
|
133
|
+
: {};
|
|
134
|
+
for (const value of [
|
|
135
|
+
providerData.requestModelId,
|
|
136
|
+
record.requestModelName,
|
|
137
|
+
providerData.requestModelName,
|
|
138
|
+
providerData.model,
|
|
139
|
+
]) {
|
|
140
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
141
|
+
}
|
|
142
|
+
return 'unknown';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function firstDetailValue(details, ...keys) {
|
|
146
|
+
for (const detail of Array.isArray(details) ? details : [details]) {
|
|
147
|
+
if (!detail || typeof detail !== 'object') continue;
|
|
148
|
+
for (const key of keys) {
|
|
149
|
+
if (detail[key] != null) return finite(detail[key]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function usageFor(record) {
|
|
156
|
+
const providerData = record.providerData && typeof record.providerData === 'object'
|
|
157
|
+
? record.providerData
|
|
158
|
+
: {};
|
|
159
|
+
const primary = providerData.usage && typeof providerData.usage === 'object'
|
|
160
|
+
? providerData.usage
|
|
161
|
+
: record.message?.usage && typeof record.message.usage === 'object'
|
|
162
|
+
? record.message.usage
|
|
163
|
+
: null;
|
|
164
|
+
const raw = providerData.rawUsage && typeof providerData.rawUsage === 'object'
|
|
165
|
+
? providerData.rawUsage
|
|
166
|
+
: null;
|
|
167
|
+
if (!primary && !raw) return null;
|
|
168
|
+
|
|
169
|
+
const inputDetails = primary?.input_details
|
|
170
|
+
?? primary?.inputDetails
|
|
171
|
+
?? primary?.inputTokensDetails
|
|
172
|
+
?? raw?.prompt_tokens_details;
|
|
173
|
+
const outputDetails = primary?.output_details
|
|
174
|
+
?? primary?.outputDetails
|
|
175
|
+
?? primary?.outputTokensDetails
|
|
176
|
+
?? raw?.completion_tokens_details;
|
|
177
|
+
const cachedInputTokens = firstDetailValue(inputDetails, 'cached_tokens', 'cachedTokens')
|
|
178
|
+
|| finite(
|
|
179
|
+
primary?.cachedInputTokens
|
|
180
|
+
?? primary?.cache_read_input_tokens
|
|
181
|
+
?? primary?.cacheReadInputTokens
|
|
182
|
+
?? raw?.prompt_cache_hit_tokens
|
|
183
|
+
?? raw?.cache_read_input_tokens
|
|
184
|
+
);
|
|
185
|
+
const reasoningOutputTokens = firstDetailValue(outputDetails, 'reasoning_tokens', 'reasoningTokens')
|
|
186
|
+
|| finite(
|
|
187
|
+
primary?.reasoningOutputTokens
|
|
188
|
+
?? primary?.completion_thinking_tokens
|
|
189
|
+
?? primary?.reasoning_tokens
|
|
190
|
+
?? primary?.reasoningTokens
|
|
191
|
+
?? raw?.completion_thinking_tokens
|
|
192
|
+
);
|
|
193
|
+
const inclusiveInput = finite(primary?.inputTokens ?? primary?.input_tokens ?? raw?.prompt_tokens);
|
|
194
|
+
const inclusiveOutput = finite(primary?.outputTokens ?? primary?.output_tokens ?? raw?.completion_tokens);
|
|
195
|
+
const cacheMiss = finite(raw?.prompt_cache_miss_tokens);
|
|
196
|
+
|
|
197
|
+
// WorkBuddy's aggregate input/output fields include cache reads/reasoning.
|
|
198
|
+
// Prefer the provider's exclusive cache-miss field when available.
|
|
199
|
+
const inputTokens = cacheMiss > 0
|
|
200
|
+
? cacheMiss
|
|
201
|
+
: Math.max(0, inclusiveInput - cachedInputTokens);
|
|
202
|
+
const outputTokens = Math.max(0, inclusiveOutput - reasoningOutputTokens);
|
|
203
|
+
const score = inputTokens + outputTokens + cachedInputTokens + reasoningOutputTokens;
|
|
204
|
+
if (score === 0) return null;
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
inputTokens,
|
|
208
|
+
outputTokens,
|
|
209
|
+
cachedInputTokens,
|
|
210
|
+
reasoningOutputTokens,
|
|
211
|
+
score,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function timestampFor(record) {
|
|
216
|
+
return dateFrom(
|
|
217
|
+
record.completedAt
|
|
218
|
+
?? record.completed_at
|
|
219
|
+
?? record.timestamp
|
|
220
|
+
?? record.createdAt
|
|
221
|
+
?? record.created_at
|
|
222
|
+
?? record.message?.createdAt
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function sessionEventsWithPrompts(events) {
|
|
227
|
+
const sessionsWithUsers = new Set(
|
|
228
|
+
events.filter(event => event.role === 'user').map(event => event.sessionId)
|
|
229
|
+
);
|
|
230
|
+
return events.filter(event => sessionsWithUsers.has(event.sessionId));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function parse() {
|
|
234
|
+
const entriesById = new Map();
|
|
235
|
+
const eventsByKey = new Map();
|
|
236
|
+
const ctx = { skipped: false, warnings: [] };
|
|
237
|
+
const projectDirs = [...new Set(findWorkbuddyDataDirs().map(root => (
|
|
238
|
+
basename(root) === 'projects' ? root : join(root, 'projects')
|
|
239
|
+
)))];
|
|
240
|
+
|
|
241
|
+
for (const projectsDir of projectDirs) {
|
|
242
|
+
for (const filePath of findJsonlFiles(projectsDir, ctx)) {
|
|
243
|
+
let size;
|
|
244
|
+
try {
|
|
245
|
+
size = statSync(filePath).size;
|
|
246
|
+
} catch {
|
|
247
|
+
warn(ctx, 'workbuddy: cannot stat a session file');
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const fallbackSessionId = basename(filePath, '.jsonl');
|
|
252
|
+
let project = projectFromFile(filePath, projectsDir);
|
|
253
|
+
const fileEntries = [];
|
|
254
|
+
const fileEvents = [];
|
|
255
|
+
|
|
256
|
+
await readJsonl(filePath, size, record => {
|
|
257
|
+
project = projectFromRecord(record) || project;
|
|
258
|
+
const timestamp = timestampFor(record);
|
|
259
|
+
const id = recordId(record);
|
|
260
|
+
const role = roleFor(record);
|
|
261
|
+
const explicitSessionId = record.sessionId ?? record.session_id;
|
|
262
|
+
const sessionId = explicitSessionId == null || String(explicitSessionId).trim() === ''
|
|
263
|
+
? fallbackSessionId
|
|
264
|
+
: String(explicitSessionId);
|
|
265
|
+
|
|
266
|
+
const usage = isUsageRecord(record) ? usageFor(record) : null;
|
|
267
|
+
const eventRole = role === 'user'
|
|
268
|
+
? 'user'
|
|
269
|
+
: isCompletedAssistant(record) || (record.type === 'function_call' && usage)
|
|
270
|
+
? 'assistant'
|
|
271
|
+
: null;
|
|
272
|
+
if (timestamp && eventRole) {
|
|
273
|
+
fileEvents.push({ id, sessionId, timestamp, role: eventRole });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!id || !timestamp || !usage) return;
|
|
277
|
+
fileEntries.push({
|
|
278
|
+
id,
|
|
279
|
+
score: usage.score,
|
|
280
|
+
entry: {
|
|
281
|
+
source: SOURCE,
|
|
282
|
+
model: modelFor(record),
|
|
283
|
+
timestamp,
|
|
284
|
+
inputTokens: usage.inputTokens,
|
|
285
|
+
outputTokens: usage.outputTokens,
|
|
286
|
+
cachedInputTokens: usage.cachedInputTokens,
|
|
287
|
+
reasoningOutputTokens: usage.reasoningOutputTokens,
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
}, ctx);
|
|
291
|
+
|
|
292
|
+
for (const candidate of fileEntries) {
|
|
293
|
+
candidate.entry.project = project;
|
|
294
|
+
const current = entriesById.get(candidate.id);
|
|
295
|
+
if (!current || candidate.score > current.score) entriesById.set(candidate.id, candidate);
|
|
296
|
+
}
|
|
297
|
+
for (const candidate of fileEvents) {
|
|
298
|
+
const event = {
|
|
299
|
+
sessionId: candidate.sessionId,
|
|
300
|
+
source: SOURCE,
|
|
301
|
+
project,
|
|
302
|
+
timestamp: candidate.timestamp,
|
|
303
|
+
role: candidate.role,
|
|
304
|
+
};
|
|
305
|
+
const key = candidate.id
|
|
306
|
+
? `id:${candidate.sessionId}:${candidate.id}:${candidate.role}`
|
|
307
|
+
: `fallback:${candidate.sessionId}:${candidate.role}:${candidate.timestamp.toISOString()}`;
|
|
308
|
+
eventsByKey.set(key, event);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
buckets: aggregateToBuckets([...entriesById.values()].map(({ entry }) => entry)),
|
|
315
|
+
sessions: extractSessions(sessionEventsWithPrompts([...eventsByKey.values()])),
|
|
316
|
+
...(ctx.skipped ? { skipped: true } : {}),
|
|
317
|
+
...(ctx.warnings.length > 0 ? { warnings: ctx.warnings } : {}),
|
|
318
|
+
};
|
|
319
|
+
}
|
package/src/pi-roots.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { delimiter, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
function expandHome(value) {
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
if (trimmed === '~') return homedir();
|
|
8
|
+
if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
|
|
9
|
+
return join(homedir(), trimmed.slice(2));
|
|
10
|
+
}
|
|
11
|
+
return trimmed;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function uniqueExistingDirs(paths) {
|
|
15
|
+
return [...new Set(paths.map(expandHome))].filter(existsSync);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function profileSessionDirs(profilesRoot, includesAgentDir) {
|
|
19
|
+
const dirs = [];
|
|
20
|
+
let profiles;
|
|
21
|
+
try {
|
|
22
|
+
profiles = readdirSync(profilesRoot, { withFileTypes: true });
|
|
23
|
+
} catch {
|
|
24
|
+
return dirs;
|
|
25
|
+
}
|
|
26
|
+
for (const profile of profiles) {
|
|
27
|
+
if (!profile.isDirectory()) continue;
|
|
28
|
+
dirs.push(join(
|
|
29
|
+
profilesRoot,
|
|
30
|
+
profile.name,
|
|
31
|
+
...(includesAgentDir ? ['agent', 'sessions'] : ['sessions']),
|
|
32
|
+
));
|
|
33
|
+
}
|
|
34
|
+
return dirs;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function looksLikeOmpAgentDir(agentDir) {
|
|
38
|
+
const normalized = agentDir.replace(/\\/g, '/');
|
|
39
|
+
return normalized.includes('/.omp/')
|
|
40
|
+
|| existsSync(join(agentDir, 'config.yml'))
|
|
41
|
+
|| existsSync(join(agentDir, 'agent.db'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getPiSessionDirs() {
|
|
45
|
+
const override = process.env.VIBE_USAGE_PI_SESSION_DIRS?.trim();
|
|
46
|
+
if (override) return uniqueExistingDirs(override.split(delimiter));
|
|
47
|
+
|
|
48
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
49
|
+
if (agentDir) {
|
|
50
|
+
const expanded = expandHome(agentDir);
|
|
51
|
+
// OMP inherits PI_CODING_AGENT_DIR from Pi. Do not parse an identifiable
|
|
52
|
+
// OMP store again as source=pi-coding-agent.
|
|
53
|
+
return looksLikeOmpAgentDir(expanded) ? [] : uniqueExistingDirs([join(expanded, 'sessions')]);
|
|
54
|
+
}
|
|
55
|
+
return uniqueExistingDirs([join(homedir(), '.pi', 'agent', 'sessions')]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getOmpSessionDirs() {
|
|
59
|
+
const override = process.env.VIBE_USAGE_OMP_SESSION_DIRS?.trim();
|
|
60
|
+
if (override) return uniqueExistingDirs(override.split(delimiter));
|
|
61
|
+
|
|
62
|
+
const dirs = [];
|
|
63
|
+
const configName = process.env.PI_CONFIG_DIR?.trim() || '.omp';
|
|
64
|
+
const configRoot = join(homedir(), configName);
|
|
65
|
+
dirs.push(join(configRoot, 'agent', 'sessions'));
|
|
66
|
+
dirs.push(...profileSessionDirs(join(configRoot, 'profiles'), true));
|
|
67
|
+
|
|
68
|
+
const agentOverride = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
69
|
+
if (agentOverride) {
|
|
70
|
+
const expanded = expandHome(agentOverride);
|
|
71
|
+
if (looksLikeOmpAgentDir(expanded)) dirs.push(join(expanded, 'sessions'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// OMP's XDG migration flattens the agent/ segment:
|
|
75
|
+
// ~/.omp/agent/sessions -> $XDG_DATA_HOME/omp/sessions.
|
|
76
|
+
if (process.platform === 'linux' || process.platform === 'darwin') {
|
|
77
|
+
const xdgDataHome = process.env.XDG_DATA_HOME?.trim();
|
|
78
|
+
if (xdgDataHome) {
|
|
79
|
+
const xdgRoot = join(expandHome(xdgDataHome), 'omp');
|
|
80
|
+
dirs.push(join(xdgRoot, 'sessions'));
|
|
81
|
+
dirs.push(...profileSessionDirs(join(xdgRoot, 'profiles'), false));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return uniqueExistingDirs(dirs);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const findPiDataDirs = getPiSessionDirs;
|
|
89
|
+
export const findOmpDataDirs = getOmpSessionDirs;
|
package/src/sync.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from './state.js';
|
|
7
7
|
import { ingest, fetchSettings } from './api.js';
|
|
8
8
|
import { createSyncClient, forBatch } from './client-meta.js';
|
|
9
|
-
import { parsers } from './parsers/index.js';
|
|
9
|
+
import { aggregateToBuckets, parsers } from './parsers/index.js';
|
|
10
10
|
import { success, failure, arrow, link, dim } from './output.js';
|
|
11
11
|
|
|
12
12
|
const BATCH_SIZE = 100;
|
|
@@ -31,6 +31,16 @@ export function resolveCodexExtraHome(configured, temporary) {
|
|
|
31
31
|
return temporary ?? configured;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Hiding project names can collapse multiple parser buckets onto one server
|
|
35
|
+
// identity. Merge those buckets before hashing/uploading so no project's usage
|
|
36
|
+
// wins by iteration order.
|
|
37
|
+
export function reaggregateHiddenProjectBuckets(buckets) {
|
|
38
|
+
return aggregateToBuckets(buckets.map(bucket => ({
|
|
39
|
+
...bucket,
|
|
40
|
+
timestamp: new Date(bucket.bucketStart),
|
|
41
|
+
})));
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
export async function runSync({
|
|
35
45
|
throws = false,
|
|
36
46
|
quiet = false,
|
|
@@ -70,7 +80,7 @@ export async function runSync({
|
|
|
70
80
|
process.exit(1);
|
|
71
81
|
}
|
|
72
82
|
|
|
73
|
-
|
|
83
|
+
let allBuckets = [];
|
|
74
84
|
const allSessions = [];
|
|
75
85
|
const parserResults = [];
|
|
76
86
|
const parserProgress = [];
|
|
@@ -166,6 +176,7 @@ export async function runSync({
|
|
|
166
176
|
if (!uploadProject) {
|
|
167
177
|
for (const b of allBuckets) b.project = 'unknown';
|
|
168
178
|
for (const s of allSessions) s.project = 'unknown';
|
|
179
|
+
allBuckets = reaggregateHiddenProjectBuckets(allBuckets);
|
|
169
180
|
}
|
|
170
181
|
|
|
171
182
|
// Incremental upload diff: parsers above emit a complete view of live local
|
package/src/tools.js
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
2
|
+
import { isAbsolute, join, posix, resolve, win32 } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { findClaudeCodeDataDirs } from './claude-roots.js';
|
|
5
5
|
import { codexSessionDirs, resolveCodexHomes } from './codex-roots.js';
|
|
6
|
+
import { findClineDataDirs } from './cline-roots.js';
|
|
7
|
+
import { findCraftDataDirs } from './craft-roots.js';
|
|
8
|
+
import { findOmpDataDirs, findPiDataDirs } from './pi-roots.js';
|
|
9
|
+
import { findWorkbuddyDataDirs } from './workbuddy-roots.js';
|
|
10
|
+
|
|
11
|
+
export function getAlmaDbPath(env = process.env, platform = process.platform, home = homedir()) {
|
|
12
|
+
const pathImpl = platform === 'win32' ? win32 : posix;
|
|
13
|
+
const override = env.VIBE_USAGE_ALMA_DB?.trim();
|
|
14
|
+
if (override) {
|
|
15
|
+
return platform === process.platform ? resolve(override) : pathImpl.resolve(override);
|
|
16
|
+
}
|
|
17
|
+
if (platform === 'darwin') {
|
|
18
|
+
return pathImpl.join(home, 'Library', 'Application Support', 'alma', 'chat_threads.db');
|
|
19
|
+
}
|
|
20
|
+
if (platform === 'win32') {
|
|
21
|
+
const appData = env.APPDATA?.trim() || pathImpl.join(home, 'AppData', 'Roaming');
|
|
22
|
+
return pathImpl.join(appData, 'alma', 'chat_threads.db');
|
|
23
|
+
}
|
|
24
|
+
const configHome = env.XDG_CONFIG_HOME?.trim() || pathImpl.join(home, '.config');
|
|
25
|
+
return pathImpl.join(configHome, 'alma', 'chat_threads.db');
|
|
26
|
+
}
|
|
6
27
|
|
|
7
28
|
function getCursorStateDbPath() {
|
|
8
29
|
const rel = join('User', 'globalStorage', 'state.vscdb');
|
|
@@ -61,7 +82,6 @@ function findExtensionDirs(extensionId) {
|
|
|
61
82
|
return dirs;
|
|
62
83
|
}
|
|
63
84
|
|
|
64
|
-
const findClineDataDirs = () => findExtensionDirs('saoudrizwan.claude-dev');
|
|
65
85
|
const findRooCodeDataDirs = () => findExtensionDirs('rooveterinaryinc.roo-cline');
|
|
66
86
|
|
|
67
87
|
/** Find all OpenClaw data roots: ~/.openclaw and ~/.openclaw-<profile> */
|
|
@@ -175,6 +195,12 @@ export function findDimAgentDataDirs() {
|
|
|
175
195
|
}
|
|
176
196
|
|
|
177
197
|
export const TOOLS = [
|
|
198
|
+
{
|
|
199
|
+
name: 'Alma',
|
|
200
|
+
id: 'alma',
|
|
201
|
+
dataDir: getAlmaDbPath(),
|
|
202
|
+
detectDataDirs: () => [getAlmaDbPath()].filter(existsSync),
|
|
203
|
+
},
|
|
178
204
|
{
|
|
179
205
|
name: 'Claude Code',
|
|
180
206
|
id: 'claude-code',
|
|
@@ -198,6 +224,12 @@ export const TOOLS = [
|
|
|
198
224
|
id: 'copilot-cli',
|
|
199
225
|
dataDir: join(homedir(), '.copilot', 'session-state'),
|
|
200
226
|
},
|
|
227
|
+
{
|
|
228
|
+
name: 'CraftAgent',
|
|
229
|
+
id: 'craft-agent',
|
|
230
|
+
dataDir: join(homedir(), '.craft-agent', 'workspaces'),
|
|
231
|
+
detectDataDirs: findCraftDataDirs,
|
|
232
|
+
},
|
|
201
233
|
{
|
|
202
234
|
name: 'Cursor',
|
|
203
235
|
id: 'cursor',
|
|
@@ -225,10 +257,17 @@ export const TOOLS = [
|
|
|
225
257
|
dataDir: join(homedir(), '.openclaw', 'agents'),
|
|
226
258
|
detectDataDirs: findOpenclawDataDirs,
|
|
227
259
|
},
|
|
260
|
+
{
|
|
261
|
+
name: 'Oh My Pi',
|
|
262
|
+
id: 'omp',
|
|
263
|
+
dataDir: join(homedir(), '.omp', 'agent', 'sessions'),
|
|
264
|
+
detectDataDirs: findOmpDataDirs,
|
|
265
|
+
},
|
|
228
266
|
{
|
|
229
267
|
name: 'pi',
|
|
230
268
|
id: 'pi-coding-agent',
|
|
231
269
|
dataDir: join(homedir(), '.pi', 'agent', 'sessions'),
|
|
270
|
+
detectDataDirs: findPiDataDirs,
|
|
232
271
|
},
|
|
233
272
|
{
|
|
234
273
|
name: 'Qwen Code',
|
|
@@ -284,7 +323,7 @@ export const TOOLS = [
|
|
|
284
323
|
{
|
|
285
324
|
name: 'Cline',
|
|
286
325
|
id: 'cline',
|
|
287
|
-
dataDir: join(homedir(), '
|
|
326
|
+
dataDir: join(homedir(), '.cline'),
|
|
288
327
|
detectDataDirs: findClineDataDirs,
|
|
289
328
|
},
|
|
290
329
|
{
|
|
@@ -293,6 +332,12 @@ export const TOOLS = [
|
|
|
293
332
|
dataDir: join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline'),
|
|
294
333
|
detectDataDirs: findRooCodeDataDirs,
|
|
295
334
|
},
|
|
335
|
+
{
|
|
336
|
+
name: 'WorkBuddy',
|
|
337
|
+
id: 'workbuddy',
|
|
338
|
+
dataDir: join(homedir(), '.workbuddy-ai', 'projects'),
|
|
339
|
+
detectDataDirs: () => findWorkbuddyDataDirs().filter(existsSync),
|
|
340
|
+
},
|
|
296
341
|
{
|
|
297
342
|
name: 'ZCode',
|
|
298
343
|
id: 'zcode',
|