@vibe-cafe/vibe-usage 0.6.5 → 0.6.6
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/amp.js +144 -0
- package/src/parsers/droid.js +113 -0
- package/src/parsers/index.js +4 -0
- package/src/tools.js +10 -0
package/package.json
CHANGED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
5
|
+
|
|
6
|
+
function resolveThreadsDir() {
|
|
7
|
+
if (process.env.AMP_DATA_DIR) return process.env.AMP_DATA_DIR;
|
|
8
|
+
if (process.env.XDG_DATA_HOME) return join(process.env.XDG_DATA_HOME, 'amp', 'threads');
|
|
9
|
+
return join(homedir(), '.local', 'share', 'amp', 'threads');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function findThreadFiles(dir) {
|
|
13
|
+
const results = [];
|
|
14
|
+
if (!existsSync(dir)) return results;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
18
|
+
const fullPath = join(dir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
results.push(...findThreadFiles(fullPath));
|
|
21
|
+
} else if (entry.isFile() && entry.name.startsWith('T-') && entry.name.endsWith('.json')) {
|
|
22
|
+
results.push(fullPath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setMessageTimestamp(map, messageId, timestamp) {
|
|
32
|
+
if (!Number.isInteger(messageId)) return;
|
|
33
|
+
const current = map.get(messageId);
|
|
34
|
+
if (!current || timestamp < current) {
|
|
35
|
+
map.set(messageId, timestamp);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildMessageTimestampMap(events) {
|
|
40
|
+
const map = new Map();
|
|
41
|
+
if (!Array.isArray(events)) return map;
|
|
42
|
+
|
|
43
|
+
for (const event of events) {
|
|
44
|
+
const ts = new Date(event?.timestamp);
|
|
45
|
+
if (isNaN(ts.getTime())) continue;
|
|
46
|
+
|
|
47
|
+
setMessageTimestamp(map, event.fromMessageId, ts);
|
|
48
|
+
setMessageTimestamp(map, event.toMessageId, ts);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return map;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function parse() {
|
|
55
|
+
const threadsDir = resolveThreadsDir();
|
|
56
|
+
const threadFiles = findThreadFiles(threadsDir);
|
|
57
|
+
if (threadFiles.length === 0) return { buckets: [], sessions: [] };
|
|
58
|
+
|
|
59
|
+
const entries = [];
|
|
60
|
+
const sessionEvents = [];
|
|
61
|
+
|
|
62
|
+
for (const filePath of threadFiles) {
|
|
63
|
+
let thread;
|
|
64
|
+
try {
|
|
65
|
+
thread = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const sessionId = thread?.id || filePath;
|
|
71
|
+
const messages = Array.isArray(thread?.messages) ? thread.messages : [];
|
|
72
|
+
const ledgerEvents = Array.isArray(thread?.usageLedger?.events) ? thread.usageLedger.events : [];
|
|
73
|
+
const hasLedger = ledgerEvents.length > 0;
|
|
74
|
+
|
|
75
|
+
if (hasLedger) {
|
|
76
|
+
for (const event of ledgerEvents) {
|
|
77
|
+
const ts = new Date(event?.timestamp);
|
|
78
|
+
if (isNaN(ts.getTime())) continue;
|
|
79
|
+
|
|
80
|
+
const inputTokens = event?.tokens?.input || 0;
|
|
81
|
+
const outputTokens = event?.tokens?.output || 0;
|
|
82
|
+
if (inputTokens === 0 && outputTokens === 0) continue;
|
|
83
|
+
|
|
84
|
+
const toMessage = Number.isInteger(event.toMessageId) ? messages[event.toMessageId] : null;
|
|
85
|
+
const cacheReadInputTokens = toMessage?.usage?.cacheReadInputTokens || 0;
|
|
86
|
+
|
|
87
|
+
entries.push({
|
|
88
|
+
source: 'amp',
|
|
89
|
+
model: event?.model || 'unknown',
|
|
90
|
+
project: 'unknown',
|
|
91
|
+
timestamp: ts,
|
|
92
|
+
inputTokens,
|
|
93
|
+
outputTokens,
|
|
94
|
+
cachedInputTokens: cacheReadInputTokens,
|
|
95
|
+
reasoningOutputTokens: 0,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
for (const message of messages) {
|
|
100
|
+
const usage = message?.usage;
|
|
101
|
+
if (!usage) continue;
|
|
102
|
+
|
|
103
|
+
const ts = new Date(message?.timestamp || thread?.created);
|
|
104
|
+
if (isNaN(ts.getTime())) continue;
|
|
105
|
+
|
|
106
|
+
const inputTokens = usage.inputTokens || 0;
|
|
107
|
+
const outputTokens = usage.outputTokens || 0;
|
|
108
|
+
if (inputTokens === 0 && outputTokens === 0 && (usage.cacheReadInputTokens || 0) === 0) continue;
|
|
109
|
+
|
|
110
|
+
entries.push({
|
|
111
|
+
source: 'amp',
|
|
112
|
+
model: usage.model || 'unknown',
|
|
113
|
+
project: 'unknown',
|
|
114
|
+
timestamp: ts,
|
|
115
|
+
inputTokens,
|
|
116
|
+
outputTokens,
|
|
117
|
+
cachedInputTokens: usage.cacheReadInputTokens || 0,
|
|
118
|
+
reasoningOutputTokens: 0,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const messageTsMap = buildMessageTimestampMap(ledgerEvents);
|
|
124
|
+
const baseTimestamp = new Date(thread?.created);
|
|
125
|
+
const hasBaseTimestamp = !isNaN(baseTimestamp.getTime());
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < messages.length; i++) {
|
|
128
|
+
const message = messages[i];
|
|
129
|
+
const mappedTs = messageTsMap.get(i);
|
|
130
|
+
const ts = mappedTs || (hasBaseTimestamp ? baseTimestamp : null);
|
|
131
|
+
if (!ts || isNaN(ts.getTime())) continue;
|
|
132
|
+
|
|
133
|
+
sessionEvents.push({
|
|
134
|
+
sessionId,
|
|
135
|
+
source: 'amp',
|
|
136
|
+
project: 'unknown',
|
|
137
|
+
timestamp: ts,
|
|
138
|
+
role: message?.role === 'user' ? 'user' : 'assistant',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { buckets: aggregateToBuckets(entries), sessions: extractSessions(sessionEvents) };
|
|
144
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, basename, dirname } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
5
|
+
|
|
6
|
+
const DROID_SESSIONS_DIR = join(homedir(), '.factory', 'sessions');
|
|
7
|
+
|
|
8
|
+
function findJsonlFiles(dir) {
|
|
9
|
+
const results = [];
|
|
10
|
+
if (!existsSync(dir)) return results;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
14
|
+
const fullPath = join(dir, entry.name);
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
results.push(...findJsonlFiles(fullPath));
|
|
17
|
+
} else if (entry.name.endsWith('.jsonl') && !entry.name.endsWith('.settings.json')) {
|
|
18
|
+
results.push(fullPath);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return results;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractProjectFromSlug(slug) {
|
|
28
|
+
const parts = slug.split('-').filter(Boolean);
|
|
29
|
+
return parts.length > 0 ? parts[parts.length - 1] : 'unknown';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toSafeNumber(value) {
|
|
33
|
+
const n = Number(value);
|
|
34
|
+
return Number.isFinite(n) ? n : 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function parse() {
|
|
38
|
+
const entries = [];
|
|
39
|
+
const sessionEvents = [];
|
|
40
|
+
const sessionFiles = findJsonlFiles(DROID_SESSIONS_DIR);
|
|
41
|
+
|
|
42
|
+
for (const filePath of sessionFiles) {
|
|
43
|
+
const sessionId = basename(filePath, '.jsonl');
|
|
44
|
+
const slug = basename(dirname(filePath));
|
|
45
|
+
const project = extractProjectFromSlug(slug);
|
|
46
|
+
let firstMessageTimestamp = null;
|
|
47
|
+
|
|
48
|
+
let content;
|
|
49
|
+
try {
|
|
50
|
+
content = readFileSync(filePath, 'utf-8');
|
|
51
|
+
} catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const line of content.split('\n')) {
|
|
56
|
+
if (!line.trim()) continue;
|
|
57
|
+
|
|
58
|
+
let obj;
|
|
59
|
+
try {
|
|
60
|
+
obj = JSON.parse(line);
|
|
61
|
+
} catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (obj.type !== 'message') continue;
|
|
66
|
+
if (!obj.timestamp) continue;
|
|
67
|
+
|
|
68
|
+
const ts = new Date(obj.timestamp);
|
|
69
|
+
if (isNaN(ts.getTime())) continue;
|
|
70
|
+
|
|
71
|
+
if (firstMessageTimestamp === null) firstMessageTimestamp = ts;
|
|
72
|
+
|
|
73
|
+
sessionEvents.push({
|
|
74
|
+
sessionId,
|
|
75
|
+
source: 'droid',
|
|
76
|
+
project,
|
|
77
|
+
timestamp: ts,
|
|
78
|
+
role: obj.message?.role === 'user' ? 'user' : 'assistant',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const settingsPath = join(dirname(filePath), `${sessionId}.settings.json`);
|
|
83
|
+
if (!existsSync(settingsPath) || firstMessageTimestamp === null) continue;
|
|
84
|
+
|
|
85
|
+
let settings;
|
|
86
|
+
try {
|
|
87
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
88
|
+
} catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tokenUsage = settings?.tokenUsage;
|
|
93
|
+
if (!tokenUsage) continue;
|
|
94
|
+
|
|
95
|
+
const cacheReadTokens = toSafeNumber(tokenUsage.cacheReadTokens);
|
|
96
|
+
const thinkingTokens = toSafeNumber(tokenUsage.thinkingTokens);
|
|
97
|
+
const inputTokens = Math.max(0, toSafeNumber(tokenUsage.inputTokens) - cacheReadTokens);
|
|
98
|
+
const outputTokens = Math.max(0, toSafeNumber(tokenUsage.outputTokens) - thinkingTokens);
|
|
99
|
+
|
|
100
|
+
entries.push({
|
|
101
|
+
source: 'droid',
|
|
102
|
+
model: settings.model || 'unknown',
|
|
103
|
+
project,
|
|
104
|
+
timestamp: firstMessageTimestamp,
|
|
105
|
+
inputTokens,
|
|
106
|
+
outputTokens,
|
|
107
|
+
cachedInputTokens: cacheReadTokens,
|
|
108
|
+
reasoningOutputTokens: thinkingTokens,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { buckets: aggregateToBuckets(entries), sessions: extractSessions(sessionEvents) };
|
|
113
|
+
}
|
package/src/parsers/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { parse as parseOpencode } from './opencode.js';
|
|
|
7
7
|
import { parse as parseOpenclaw } from './openclaw.js';
|
|
8
8
|
import { parse as parseQwenCode } from './qwen-code.js';
|
|
9
9
|
import { parse as parseKimiCode } from './kimi-code.js';
|
|
10
|
+
import { parse as parseAmp } from './amp.js';
|
|
11
|
+
import { parse as parseDroid } from './droid.js';
|
|
10
12
|
|
|
11
13
|
export const parsers = {
|
|
12
14
|
'claude-code': parseClaudeCode,
|
|
@@ -17,6 +19,8 @@ export const parsers = {
|
|
|
17
19
|
'openclaw': parseOpenclaw,
|
|
18
20
|
'qwen-code': parseQwenCode,
|
|
19
21
|
'kimi-code': parseKimiCode,
|
|
22
|
+
'amp': parseAmp,
|
|
23
|
+
'droid': parseDroid,
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
|
package/src/tools.js
CHANGED
|
@@ -43,6 +43,16 @@ export const TOOLS = [
|
|
|
43
43
|
id: 'kimi-code',
|
|
44
44
|
dataDir: join(homedir(), '.kimi', 'sessions'),
|
|
45
45
|
},
|
|
46
|
+
{
|
|
47
|
+
name: 'Amp',
|
|
48
|
+
id: 'amp',
|
|
49
|
+
dataDir: join(homedir(), '.local', 'share', 'amp', 'threads'),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'Droid',
|
|
53
|
+
id: 'droid',
|
|
54
|
+
dataDir: join(homedir(), '.factory', 'sessions'),
|
|
55
|
+
},
|
|
46
56
|
];
|
|
47
57
|
|
|
48
58
|
export function detectInstalledTools() {
|