devrage 0.0.4 → 0.5.3
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/dist/cli.js +2436 -210
- package/dist/cli.js.map +4 -4
- package/dist/lib/adapters/amp.d.ts.map +1 -1
- package/dist/lib/adapters/amp.js +171 -7
- package/dist/lib/adapters/amp.js.map +1 -1
- package/dist/lib/adapters/claude.d.ts.map +1 -1
- package/dist/lib/adapters/claude.js +147 -49
- package/dist/lib/adapters/claude.js.map +1 -1
- package/dist/lib/adapters/cline.d.ts.map +1 -1
- package/dist/lib/adapters/cline.js +17 -12
- package/dist/lib/adapters/cline.js.map +1 -1
- package/dist/lib/adapters/codex.d.ts.map +1 -1
- package/dist/lib/adapters/codex.js +169 -14
- package/dist/lib/adapters/codex.js.map +1 -1
- package/dist/lib/adapters/cursor.d.ts +3 -0
- package/dist/lib/adapters/cursor.d.ts.map +1 -0
- package/dist/lib/adapters/cursor.js +540 -0
- package/dist/lib/adapters/cursor.js.map +1 -0
- package/dist/lib/adapters/index.d.ts +61 -0
- package/dist/lib/adapters/index.d.ts.map +1 -1
- package/dist/lib/adapters/index.js +2 -0
- package/dist/lib/adapters/index.js.map +1 -1
- package/dist/lib/adapters/opencode.d.ts.map +1 -1
- package/dist/lib/adapters/opencode.js +96 -16
- package/dist/lib/adapters/opencode.js.map +1 -1
- package/dist/lib/adapters/pi.d.ts.map +1 -1
- package/dist/lib/adapters/pi.js +113 -11
- package/dist/lib/adapters/pi.js.map +1 -1
- package/dist/lib/adapters/zed.d.ts.map +1 -1
- package/dist/lib/adapters/zed.js +18 -11
- package/dist/lib/adapters/zed.js.map +1 -1
- package/dist/lib/detector/index.d.ts.map +1 -1
- package/dist/lib/detector/index.js +12 -6
- package/dist/lib/detector/index.js.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/index.js.map +1 -1
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/commands/scan.ts
|
|
4
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
5
|
+
import { dirname as dirname2, join as join10 } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
3
8
|
// src/adapters/amp.ts
|
|
4
9
|
import { readdir, readFile } from "node:fs/promises";
|
|
5
10
|
import { homedir } from "node:os";
|
|
6
11
|
import { join } from "node:path";
|
|
7
12
|
function getAmpThreadsDir() {
|
|
8
|
-
return join(
|
|
9
|
-
process.env["XDG_DATA_HOME"] ?? join(homedir(), ".local", "share"),
|
|
10
|
-
"amp",
|
|
11
|
-
"threads"
|
|
12
|
-
);
|
|
13
|
+
return join(process.env["XDG_DATA_HOME"] ?? join(homedir(), ".local", "share"), "amp", "threads");
|
|
13
14
|
}
|
|
14
15
|
function ampAdapter() {
|
|
15
16
|
return {
|
|
@@ -27,17 +28,27 @@ function ampAdapter() {
|
|
|
27
28
|
const filePath = join(threadsDir, file);
|
|
28
29
|
const threadId = file.replace(".json", "");
|
|
29
30
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const thread = await readAmpThread(filePath);
|
|
32
|
+
if (!thread) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!thread.messages || !Array.isArray(thread.messages)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
33
38
|
for (const msg of thread.messages) {
|
|
34
|
-
if (msg.role !== "user")
|
|
39
|
+
if (msg.role !== "user") {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
35
42
|
const text = extractText(msg.content);
|
|
36
|
-
if (!text)
|
|
43
|
+
if (!text) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
37
46
|
const timestamp = msg.timestamp ?? msg.createdAt ?? void 0;
|
|
38
47
|
if (options?.since && timestamp) {
|
|
39
48
|
const ts = new Date(timestamp);
|
|
40
|
-
if (ts < options.since)
|
|
49
|
+
if (ts < options.since) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
41
52
|
}
|
|
42
53
|
yield {
|
|
43
54
|
text,
|
|
@@ -48,11 +59,47 @@ function ampAdapter() {
|
|
|
48
59
|
} catch {
|
|
49
60
|
}
|
|
50
61
|
}
|
|
62
|
+
},
|
|
63
|
+
async *usage(options) {
|
|
64
|
+
const threadsDir = getAmpThreadsDir();
|
|
65
|
+
let files;
|
|
66
|
+
try {
|
|
67
|
+
files = await readdir(threadsDir);
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const file of files.filter((f) => f.endsWith(".json"))) {
|
|
72
|
+
const filePath = join(threadsDir, file);
|
|
73
|
+
const threadId = file.replace(".json", "");
|
|
74
|
+
try {
|
|
75
|
+
const thread = await readAmpThread(filePath);
|
|
76
|
+
if (!thread?.usageLedger) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
for (const record of extractAmpUsageRecords(thread.usageLedger, threadId)) {
|
|
80
|
+
if (options?.since && record.timestamp) {
|
|
81
|
+
const ts = new Date(record.timestamp);
|
|
82
|
+
if (ts < options.since) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
yield record;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
51
91
|
}
|
|
52
92
|
};
|
|
53
93
|
}
|
|
94
|
+
async function readAmpThread(filePath) {
|
|
95
|
+
const raw = await readFile(filePath, "utf-8");
|
|
96
|
+
const parsed = JSON.parse(raw);
|
|
97
|
+
return asRecord(parsed) ? parsed : null;
|
|
98
|
+
}
|
|
54
99
|
function extractText(content) {
|
|
55
|
-
if (typeof content === "string")
|
|
100
|
+
if (typeof content === "string") {
|
|
101
|
+
return content;
|
|
102
|
+
}
|
|
56
103
|
if (Array.isArray(content)) {
|
|
57
104
|
const parts = content.filter(
|
|
58
105
|
(p) => typeof p === "object" && p !== null && typeof p.text === "string"
|
|
@@ -61,6 +108,125 @@ function extractText(content) {
|
|
|
61
108
|
}
|
|
62
109
|
return null;
|
|
63
110
|
}
|
|
111
|
+
function extractAmpUsageRecords(usageLedger, threadId) {
|
|
112
|
+
const records = [];
|
|
113
|
+
collectAmpUsage(usageLedger, threadId, records, {});
|
|
114
|
+
return records;
|
|
115
|
+
}
|
|
116
|
+
function collectAmpUsage(value, threadId, records, context, depth = 0) {
|
|
117
|
+
if (depth > 12) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(value)) {
|
|
121
|
+
for (const item of value) {
|
|
122
|
+
collectAmpUsage(item, threadId, records, context, depth + 1);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const record = asRecord(value);
|
|
127
|
+
if (!record) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const nextContext = {
|
|
131
|
+
provider: stringField(record, ["provider", "providerID", "providerId"]) ?? context.provider,
|
|
132
|
+
model: stringField(record, ["model", "modelID", "modelId"]) ?? context.model,
|
|
133
|
+
timestamp: timestampField(record) ?? context.timestamp
|
|
134
|
+
};
|
|
135
|
+
const usageSource = firstRecordField(record, ["usage", "tokens", "tokenUsage"]) ?? record;
|
|
136
|
+
const rawInputTokens = tokenField(usageSource, ["inputTokens", "input_tokens", "promptTokens"]);
|
|
137
|
+
const outputTokens = tokenField(usageSource, [
|
|
138
|
+
"outputTokens",
|
|
139
|
+
"output_tokens",
|
|
140
|
+
"completionTokens"
|
|
141
|
+
]);
|
|
142
|
+
const reasoningTokens = tokenField(usageSource, ["reasoningTokens", "reasoning_output_tokens"]);
|
|
143
|
+
const cachedInputSubset = tokenField(usageSource, ["cachedInputTokens", "cached_input_tokens"]);
|
|
144
|
+
const cacheReadTokens = tokenField(usageSource, [
|
|
145
|
+
"cacheReadTokens",
|
|
146
|
+
"cache_read_tokens",
|
|
147
|
+
"cacheReadInputTokens",
|
|
148
|
+
"cache_read_input_tokens",
|
|
149
|
+
"cachedInputTokens",
|
|
150
|
+
"cached_input_tokens"
|
|
151
|
+
]);
|
|
152
|
+
const inputTokens = Math.max(rawInputTokens - cachedInputSubset, 0);
|
|
153
|
+
const cacheWriteTokens = tokenField(usageSource, [
|
|
154
|
+
"cacheWriteTokens",
|
|
155
|
+
"cache_write_tokens",
|
|
156
|
+
"cacheCreationInputTokens",
|
|
157
|
+
"cache_creation_input_tokens",
|
|
158
|
+
"cacheWriteInputTokens",
|
|
159
|
+
"cache_write_input_tokens"
|
|
160
|
+
]);
|
|
161
|
+
const billedCost = tokenField(record, [
|
|
162
|
+
"cost",
|
|
163
|
+
"totalCost",
|
|
164
|
+
"total_cost",
|
|
165
|
+
"billedCost",
|
|
166
|
+
"billed_cost"
|
|
167
|
+
]);
|
|
168
|
+
if (inputTokens + outputTokens + reasoningTokens + cacheReadTokens + cacheWriteTokens + billedCost > 0) {
|
|
169
|
+
records.push({
|
|
170
|
+
agent: "amp",
|
|
171
|
+
provider: nextContext.provider,
|
|
172
|
+
model: nextContext.model,
|
|
173
|
+
timestamp: nextContext.timestamp,
|
|
174
|
+
session: threadId,
|
|
175
|
+
billedCost,
|
|
176
|
+
inputTokens,
|
|
177
|
+
outputTokens,
|
|
178
|
+
reasoningTokens,
|
|
179
|
+
cacheReadTokens,
|
|
180
|
+
cacheWriteTokens
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
for (const child of Object.values(record)) {
|
|
184
|
+
if (child !== usageSource && typeof child === "object" && child !== null) {
|
|
185
|
+
collectAmpUsage(child, threadId, records, nextContext, depth + 1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function firstRecordField(record, fields) {
|
|
190
|
+
for (const field of fields) {
|
|
191
|
+
const value = asRecord(record[field]);
|
|
192
|
+
if (value) {
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function stringField(record, fields) {
|
|
199
|
+
for (const field of fields) {
|
|
200
|
+
const value = record[field];
|
|
201
|
+
if (typeof value === "string" && value.trim()) {
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return void 0;
|
|
206
|
+
}
|
|
207
|
+
function timestampField(record) {
|
|
208
|
+
const value = stringField(record, ["timestamp", "createdAt", "time", "date"]);
|
|
209
|
+
if (value) {
|
|
210
|
+
const date = new Date(value);
|
|
211
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : void 0;
|
|
212
|
+
}
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
function tokenField(record, fields) {
|
|
216
|
+
for (const field of fields) {
|
|
217
|
+
const value = record[field];
|
|
218
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
function asRecord(value) {
|
|
225
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
64
230
|
|
|
65
231
|
// src/adapters/claude.ts
|
|
66
232
|
import { createReadStream } from "node:fs";
|
|
@@ -73,63 +239,78 @@ function claudeAdapter() {
|
|
|
73
239
|
return {
|
|
74
240
|
name: "claude",
|
|
75
241
|
async *messages(options) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
projectDirs = await readdir2(projectsDir);
|
|
80
|
-
} catch {
|
|
81
|
-
return;
|
|
242
|
+
for await (const file of discoverClaudeJsonlFiles()) {
|
|
243
|
+
yield* parseClaudeJsonl(file.filePath, { ...file, since: options?.since });
|
|
82
244
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const entries = await readdir2(projectPath);
|
|
88
|
-
const jsonlFiles = entries.filter((f) => f.endsWith(".jsonl"));
|
|
89
|
-
for (const file of jsonlFiles) {
|
|
90
|
-
const filePath = join2(projectPath, file);
|
|
91
|
-
const session = file.replace(".jsonl", "");
|
|
92
|
-
yield* parseClaudeJsonl(filePath, {
|
|
93
|
-
session,
|
|
94
|
-
project: projectDir,
|
|
95
|
-
since: options?.since
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
const subdirs = entries.filter((f) => !f.includes("."));
|
|
99
|
-
for (const subdir of subdirs) {
|
|
100
|
-
const subagentsDir = join2(projectPath, subdir, "subagents");
|
|
101
|
-
try {
|
|
102
|
-
const subFiles = await readdir2(subagentsDir);
|
|
103
|
-
const subJsonl = subFiles.filter((f) => f.endsWith(".jsonl"));
|
|
104
|
-
for (const file of subJsonl) {
|
|
105
|
-
yield* parseClaudeJsonl(join2(subagentsDir, file), {
|
|
106
|
-
session: `${subdir}/${file.replace(".jsonl", "")}`,
|
|
107
|
-
project: projectDir,
|
|
108
|
-
since: options?.since
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
} catch {
|
|
112
|
-
}
|
|
113
|
-
}
|
|
245
|
+
},
|
|
246
|
+
async *usage(options) {
|
|
247
|
+
for await (const file of discoverClaudeJsonlFiles()) {
|
|
248
|
+
yield* parseClaudeUsageJsonl(file.filePath, { ...file, since: options?.since });
|
|
114
249
|
}
|
|
115
250
|
}
|
|
116
251
|
};
|
|
117
252
|
}
|
|
253
|
+
async function* discoverClaudeJsonlFiles() {
|
|
254
|
+
let projectDirs;
|
|
255
|
+
try {
|
|
256
|
+
projectDirs = await readdir2(CLAUDE_DIR);
|
|
257
|
+
} catch {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
for (const projectDir of projectDirs) {
|
|
261
|
+
const projectPath = join2(CLAUDE_DIR, projectDir);
|
|
262
|
+
const projectStat = await stat(projectPath);
|
|
263
|
+
if (!projectStat.isDirectory()) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const entries = await readdir2(projectPath);
|
|
267
|
+
const jsonlFiles = entries.filter((f) => f.endsWith(".jsonl"));
|
|
268
|
+
for (const file of jsonlFiles) {
|
|
269
|
+
yield {
|
|
270
|
+
filePath: join2(projectPath, file),
|
|
271
|
+
session: file.replace(".jsonl", ""),
|
|
272
|
+
project: projectDir
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const subdirs = entries.filter((f) => !f.includes("."));
|
|
276
|
+
for (const subdir of subdirs) {
|
|
277
|
+
const subagentsDir = join2(projectPath, subdir, "subagents");
|
|
278
|
+
try {
|
|
279
|
+
const subFiles = await readdir2(subagentsDir);
|
|
280
|
+
const subJsonl = subFiles.filter((f) => f.endsWith(".jsonl"));
|
|
281
|
+
for (const file of subJsonl) {
|
|
282
|
+
yield {
|
|
283
|
+
filePath: join2(subagentsDir, file),
|
|
284
|
+
session: `${subdir}/${file.replace(".jsonl", "")}`,
|
|
285
|
+
project: projectDir
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
118
293
|
async function* parseClaudeJsonl(filePath, context) {
|
|
119
294
|
const rl = createInterface({
|
|
120
295
|
input: createReadStream(filePath, { encoding: "utf-8" }),
|
|
121
296
|
crlfDelay: Infinity
|
|
122
297
|
});
|
|
123
298
|
for await (const line of rl) {
|
|
124
|
-
if (!line.trim())
|
|
299
|
+
if (!line.trim()) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
125
302
|
try {
|
|
126
303
|
const entry = JSON.parse(line);
|
|
127
304
|
const text = extractUserText(entry);
|
|
128
|
-
if (!text)
|
|
305
|
+
if (!text) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
129
308
|
const timestamp = extractTimestamp(entry);
|
|
130
309
|
if (context.since && timestamp) {
|
|
131
310
|
const ts = new Date(timestamp);
|
|
132
|
-
if (ts < context.since)
|
|
311
|
+
if (ts < context.since) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
133
314
|
}
|
|
134
315
|
yield {
|
|
135
316
|
text,
|
|
@@ -144,12 +325,16 @@ async function* parseClaudeJsonl(filePath, context) {
|
|
|
144
325
|
function extractUserText(entry) {
|
|
145
326
|
if (entry["type"] === "user") {
|
|
146
327
|
const message = entry["message"];
|
|
147
|
-
if (!message)
|
|
328
|
+
if (!message) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
148
331
|
return contentToString(message["content"]);
|
|
149
332
|
}
|
|
150
333
|
if (entry["type"] === "human") {
|
|
151
334
|
const message = entry["message"];
|
|
152
|
-
if (!message)
|
|
335
|
+
if (!message) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
153
338
|
return contentToString(message["content"]);
|
|
154
339
|
}
|
|
155
340
|
if (entry["role"] === "user") {
|
|
@@ -158,7 +343,9 @@ function extractUserText(entry) {
|
|
|
158
343
|
return null;
|
|
159
344
|
}
|
|
160
345
|
function contentToString(content) {
|
|
161
|
-
if (typeof content === "string")
|
|
346
|
+
if (typeof content === "string") {
|
|
347
|
+
return content;
|
|
348
|
+
}
|
|
162
349
|
if (Array.isArray(content)) {
|
|
163
350
|
const parts = content.filter(
|
|
164
351
|
(p) => typeof p === "object" && p !== null && p.type === "text"
|
|
@@ -168,10 +355,90 @@ function contentToString(content) {
|
|
|
168
355
|
return null;
|
|
169
356
|
}
|
|
170
357
|
function extractTimestamp(entry) {
|
|
171
|
-
if (typeof entry["timestamp"] === "string")
|
|
172
|
-
|
|
358
|
+
if (typeof entry["timestamp"] === "string") {
|
|
359
|
+
return entry["timestamp"];
|
|
360
|
+
}
|
|
361
|
+
if (typeof entry["createdAt"] === "string") {
|
|
362
|
+
return entry["createdAt"];
|
|
363
|
+
}
|
|
173
364
|
return null;
|
|
174
365
|
}
|
|
366
|
+
async function* parseClaudeUsageJsonl(filePath, context) {
|
|
367
|
+
const rl = createInterface({
|
|
368
|
+
input: createReadStream(filePath, { encoding: "utf-8" }),
|
|
369
|
+
crlfDelay: Infinity
|
|
370
|
+
});
|
|
371
|
+
const seen = /* @__PURE__ */ new Set();
|
|
372
|
+
for await (const line of rl) {
|
|
373
|
+
if (!line.trim()) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const entry = JSON.parse(line);
|
|
378
|
+
const message = asRecord2(entry["message"]);
|
|
379
|
+
if (!message || entry["type"] !== "assistant" || message["role"] !== "assistant") {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const usage2 = asRecord2(message["usage"]);
|
|
383
|
+
if (!usage2) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const model = stringValue(message["model"]);
|
|
387
|
+
const timestamp = extractTimestamp(entry) ?? void 0;
|
|
388
|
+
if (context.since && timestamp) {
|
|
389
|
+
const ts = new Date(timestamp);
|
|
390
|
+
if (ts < context.since) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const inputTokens = numberValue(usage2["input_tokens"]);
|
|
395
|
+
const outputTokens = numberValue(usage2["output_tokens"]);
|
|
396
|
+
const cacheReadTokens = numberValue(usage2["cache_read_input_tokens"]);
|
|
397
|
+
const cacheWriteTokens = cacheCreationTokens(usage2);
|
|
398
|
+
if (inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens === 0) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const dedupeKey = stringValue(entry["requestId"]) ?? stringValue(message["id"]) ?? `${context.session}:${timestamp ?? ""}:${model ?? "unknown"}:${inputTokens}:${outputTokens}`;
|
|
402
|
+
if (seen.has(dedupeKey)) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
seen.add(dedupeKey);
|
|
406
|
+
yield {
|
|
407
|
+
agent: "claude",
|
|
408
|
+
provider: "anthropic",
|
|
409
|
+
model,
|
|
410
|
+
timestamp,
|
|
411
|
+
session: context.session,
|
|
412
|
+
inputTokens,
|
|
413
|
+
outputTokens,
|
|
414
|
+
reasoningTokens: 0,
|
|
415
|
+
cacheReadTokens,
|
|
416
|
+
cacheWriteTokens
|
|
417
|
+
};
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function cacheCreationTokens(usage2) {
|
|
423
|
+
const explicit = numberValue(usage2["cache_creation_input_tokens"]);
|
|
424
|
+
if (explicit > 0) {
|
|
425
|
+
return explicit;
|
|
426
|
+
}
|
|
427
|
+
const cacheCreation = asRecord2(usage2["cache_creation"]);
|
|
428
|
+
return numberValue(cacheCreation?.["ephemeral_1h_input_tokens"]) + numberValue(cacheCreation?.["ephemeral_5m_input_tokens"]);
|
|
429
|
+
}
|
|
430
|
+
function numberValue(value) {
|
|
431
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
432
|
+
}
|
|
433
|
+
function stringValue(value) {
|
|
434
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
435
|
+
}
|
|
436
|
+
function asRecord2(value) {
|
|
437
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
return value;
|
|
441
|
+
}
|
|
175
442
|
|
|
176
443
|
// src/adapters/cline.ts
|
|
177
444
|
import { readdir as readdir3, readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
@@ -185,11 +452,15 @@ function getClineTaskDirs() {
|
|
|
185
452
|
for (const basePath of vscodePaths) {
|
|
186
453
|
for (const extId of extensionIds) {
|
|
187
454
|
const tasksDir = join3(basePath, extId, "tasks");
|
|
188
|
-
if (existsSync(tasksDir))
|
|
455
|
+
if (existsSync(tasksDir)) {
|
|
456
|
+
dirs.push(tasksDir);
|
|
457
|
+
}
|
|
189
458
|
}
|
|
190
459
|
}
|
|
191
460
|
const clineStandalone = join3(homedir3(), ".cline", "data", "tasks");
|
|
192
|
-
if (existsSync(clineStandalone))
|
|
461
|
+
if (existsSync(clineStandalone)) {
|
|
462
|
+
dirs.push(clineStandalone);
|
|
463
|
+
}
|
|
193
464
|
return dirs;
|
|
194
465
|
}
|
|
195
466
|
function getVSCodeGlobalStoragePaths() {
|
|
@@ -232,20 +503,30 @@ function clineAdapter() {
|
|
|
232
503
|
for (const taskId of taskIds) {
|
|
233
504
|
const taskDir = join3(tasksDir, taskId);
|
|
234
505
|
const taskStat = await stat2(taskDir).catch(() => null);
|
|
235
|
-
if (!taskStat?.isDirectory())
|
|
506
|
+
if (!taskStat?.isDirectory()) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
236
509
|
const historyFile = join3(taskDir, "api_conversation_history.json");
|
|
237
510
|
try {
|
|
238
511
|
const raw = await readFile2(historyFile, "utf-8");
|
|
239
512
|
const messages = JSON.parse(raw);
|
|
240
|
-
if (!Array.isArray(messages))
|
|
513
|
+
if (!Array.isArray(messages)) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
241
516
|
for (const msg of messages) {
|
|
242
|
-
if (msg.role !== "user")
|
|
517
|
+
if (msg.role !== "user") {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
243
520
|
const text = extractText2(msg.content);
|
|
244
|
-
if (!text)
|
|
521
|
+
if (!text) {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
245
524
|
const timestamp = msg.ts ?? void 0;
|
|
246
525
|
if (options?.since && timestamp) {
|
|
247
526
|
const ts = new Date(timestamp);
|
|
248
|
-
if (ts < options.since)
|
|
527
|
+
if (ts < options.since) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
249
530
|
}
|
|
250
531
|
yield {
|
|
251
532
|
text,
|
|
@@ -260,7 +541,9 @@ function clineAdapter() {
|
|
|
260
541
|
};
|
|
261
542
|
}
|
|
262
543
|
function extractText2(content) {
|
|
263
|
-
if (typeof content === "string")
|
|
544
|
+
if (typeof content === "string") {
|
|
545
|
+
return content;
|
|
546
|
+
}
|
|
264
547
|
if (Array.isArray(content)) {
|
|
265
548
|
const parts = content.filter(
|
|
266
549
|
(p) => typeof p === "object" && p !== null && p.type === "text" && typeof p.text === "string"
|
|
@@ -281,11 +564,21 @@ function codexAdapter() {
|
|
|
281
564
|
return {
|
|
282
565
|
name: "codex",
|
|
283
566
|
async *messages(options) {
|
|
284
|
-
|
|
567
|
+
for await (const file of discoverCodexSessionFiles(CODEX_SESSIONS_DIR)) {
|
|
568
|
+
yield* parseCodexJsonl(file.filePath, { session: file.session, since: options?.since });
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
async *usage(options) {
|
|
572
|
+
for await (const file of discoverCodexSessionFiles(CODEX_SESSIONS_DIR)) {
|
|
573
|
+
yield* parseCodexUsageJsonl(file.filePath, {
|
|
574
|
+
session: file.session,
|
|
575
|
+
since: options?.since
|
|
576
|
+
});
|
|
577
|
+
}
|
|
285
578
|
}
|
|
286
579
|
};
|
|
287
580
|
}
|
|
288
|
-
async function*
|
|
581
|
+
async function* discoverCodexSessionFiles(dir) {
|
|
289
582
|
let entries;
|
|
290
583
|
try {
|
|
291
584
|
entries = await readdir4(dir);
|
|
@@ -296,10 +589,9 @@ async function* walkCodexSessions(dir, options) {
|
|
|
296
589
|
const fullPath = join4(dir, entry);
|
|
297
590
|
const entryStat = await stat3(fullPath);
|
|
298
591
|
if (entryStat.isDirectory()) {
|
|
299
|
-
yield*
|
|
592
|
+
yield* discoverCodexSessionFiles(fullPath);
|
|
300
593
|
} else if (entry.endsWith(".jsonl")) {
|
|
301
|
-
|
|
302
|
-
yield* parseCodexJsonl(fullPath, { session, since: options?.since });
|
|
594
|
+
yield { filePath: fullPath, session: entry.replace(".jsonl", "") };
|
|
303
595
|
}
|
|
304
596
|
}
|
|
305
597
|
}
|
|
@@ -309,19 +601,33 @@ async function* parseCodexJsonl(filePath, context) {
|
|
|
309
601
|
crlfDelay: Infinity
|
|
310
602
|
});
|
|
311
603
|
for await (const line of rl) {
|
|
312
|
-
if (!line.trim())
|
|
604
|
+
if (!line.trim()) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
313
607
|
try {
|
|
314
608
|
const entry = JSON.parse(line);
|
|
315
|
-
if (entry.type !== "response_item")
|
|
609
|
+
if (entry.type !== "response_item") {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
316
612
|
const payload = entry.payload;
|
|
317
|
-
if (!payload || payload.role !== "user")
|
|
613
|
+
if (!payload || payload.role !== "user") {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
318
616
|
const text = extractText3(payload.content);
|
|
319
|
-
if (!text)
|
|
320
|
-
|
|
321
|
-
|
|
617
|
+
if (!text) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (text.startsWith("<environment_context>")) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (text.startsWith("<permissions instructions>")) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
322
626
|
if (context.since && entry.timestamp) {
|
|
323
627
|
const ts = new Date(entry.timestamp);
|
|
324
|
-
if (ts < context.since)
|
|
628
|
+
if (ts < context.since) {
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
325
631
|
}
|
|
326
632
|
yield {
|
|
327
633
|
text,
|
|
@@ -333,114 +639,868 @@ async function* parseCodexJsonl(filePath, context) {
|
|
|
333
639
|
}
|
|
334
640
|
}
|
|
335
641
|
function extractText3(content) {
|
|
336
|
-
if (!Array.isArray(content))
|
|
642
|
+
if (!Array.isArray(content)) {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
337
645
|
const parts = content.filter(
|
|
338
646
|
(p) => typeof p === "object" && p !== null && p.type === "input_text" && typeof p.text === "string"
|
|
339
647
|
).map((p) => p.text);
|
|
340
648
|
return parts.length > 0 ? parts.join(" ") : null;
|
|
341
649
|
}
|
|
650
|
+
async function* parseCodexUsageJsonl(filePath, context) {
|
|
651
|
+
const rl = createInterface2({
|
|
652
|
+
input: createReadStream2(filePath, { encoding: "utf-8" }),
|
|
653
|
+
crlfDelay: Infinity
|
|
654
|
+
});
|
|
655
|
+
let model;
|
|
656
|
+
let previousTotal = null;
|
|
657
|
+
let previousUsageSignature = null;
|
|
658
|
+
for await (const line of rl) {
|
|
659
|
+
if (!line.trim()) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
const entry = JSON.parse(line);
|
|
664
|
+
const payload = asRecord3(entry["payload"]);
|
|
665
|
+
if (entry["type"] === "turn_context") {
|
|
666
|
+
model = stringValue2(payload?.["model"]) ?? model;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (entry["type"] !== "event_msg" || payload?.["type"] !== "token_count") {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
const info = asRecord3(payload["info"]);
|
|
673
|
+
if (!info) {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const lastUsageValue = info["last_token_usage"];
|
|
677
|
+
const lastUsage = parseCodexTokenUsage(lastUsageValue);
|
|
678
|
+
const total = parseCodexTokenUsage(info["total_token_usage"]);
|
|
679
|
+
let usage2 = null;
|
|
680
|
+
if (lastUsageValue !== void 0) {
|
|
681
|
+
if (lastUsage && hasBillableUsage(lastUsage)) {
|
|
682
|
+
const signature = codexUsageSignature(lastUsage, total);
|
|
683
|
+
if (signature !== previousUsageSignature) {
|
|
684
|
+
usage2 = lastUsage;
|
|
685
|
+
}
|
|
686
|
+
previousUsageSignature = signature;
|
|
687
|
+
}
|
|
688
|
+
} else if (total) {
|
|
689
|
+
const delta = previousTotal ? subtractCodexUsage(total, previousTotal) : total;
|
|
690
|
+
if (hasBillableUsage(delta)) {
|
|
691
|
+
usage2 = delta;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (total && hasBillableUsage(total)) {
|
|
695
|
+
previousTotal = total;
|
|
696
|
+
}
|
|
697
|
+
if (!usage2) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const timestamp = stringValue2(entry["timestamp"]);
|
|
701
|
+
if (context.since && timestamp) {
|
|
702
|
+
const ts = new Date(timestamp);
|
|
703
|
+
if (ts < context.since) {
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const reasoningTokens = Math.min(usage2.reasoningOutputTokens, usage2.outputTokens);
|
|
708
|
+
yield {
|
|
709
|
+
agent: "codex",
|
|
710
|
+
provider: "openai",
|
|
711
|
+
model,
|
|
712
|
+
timestamp,
|
|
713
|
+
session: context.session,
|
|
714
|
+
inputTokens: Math.max(usage2.inputTokens - usage2.cachedInputTokens, 0),
|
|
715
|
+
outputTokens: Math.max(usage2.outputTokens - reasoningTokens, 0),
|
|
716
|
+
reasoningTokens,
|
|
717
|
+
cacheReadTokens: usage2.cachedInputTokens,
|
|
718
|
+
cacheWriteTokens: 0
|
|
719
|
+
};
|
|
720
|
+
} catch {
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function parseCodexTokenUsage(value) {
|
|
725
|
+
const usage2 = asRecord3(value);
|
|
726
|
+
if (!usage2) {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
const hasUsageField = [
|
|
730
|
+
"input_tokens",
|
|
731
|
+
"cached_input_tokens",
|
|
732
|
+
"output_tokens",
|
|
733
|
+
"reasoning_output_tokens",
|
|
734
|
+
"total_tokens"
|
|
735
|
+
].some((key) => typeof usage2[key] === "number" && Number.isFinite(usage2[key]));
|
|
736
|
+
if (!hasUsageField) {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
inputTokens: numberValue2(usage2["input_tokens"]),
|
|
741
|
+
cachedInputTokens: numberValue2(usage2["cached_input_tokens"]),
|
|
742
|
+
outputTokens: numberValue2(usage2["output_tokens"]),
|
|
743
|
+
reasoningOutputTokens: numberValue2(usage2["reasoning_output_tokens"]),
|
|
744
|
+
totalTokens: numberValue2(usage2["total_tokens"])
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function subtractCodexUsage(current, previous) {
|
|
748
|
+
return {
|
|
749
|
+
inputTokens: Math.max(current.inputTokens - previous.inputTokens, 0),
|
|
750
|
+
cachedInputTokens: Math.max(current.cachedInputTokens - previous.cachedInputTokens, 0),
|
|
751
|
+
outputTokens: Math.max(current.outputTokens - previous.outputTokens, 0),
|
|
752
|
+
reasoningOutputTokens: Math.max(
|
|
753
|
+
current.reasoningOutputTokens - previous.reasoningOutputTokens,
|
|
754
|
+
0
|
|
755
|
+
),
|
|
756
|
+
totalTokens: Math.max(current.totalTokens - previous.totalTokens, 0)
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function hasBillableUsage(usage2) {
|
|
760
|
+
return usage2.inputTokens + usage2.cachedInputTokens + usage2.outputTokens + usage2.reasoningOutputTokens > 0;
|
|
761
|
+
}
|
|
762
|
+
function codexUsageSignature(usage2, total) {
|
|
763
|
+
return [
|
|
764
|
+
usage2.inputTokens,
|
|
765
|
+
usage2.cachedInputTokens,
|
|
766
|
+
usage2.outputTokens,
|
|
767
|
+
usage2.reasoningOutputTokens,
|
|
768
|
+
total?.inputTokens ?? "",
|
|
769
|
+
total?.cachedInputTokens ?? "",
|
|
770
|
+
total?.outputTokens ?? "",
|
|
771
|
+
total?.reasoningOutputTokens ?? ""
|
|
772
|
+
].join(":");
|
|
773
|
+
}
|
|
774
|
+
function numberValue2(value) {
|
|
775
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
776
|
+
}
|
|
777
|
+
function stringValue2(value) {
|
|
778
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
779
|
+
}
|
|
780
|
+
function asRecord3(value) {
|
|
781
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
return value;
|
|
785
|
+
}
|
|
342
786
|
|
|
343
|
-
// src/adapters/
|
|
787
|
+
// src/adapters/cursor.ts
|
|
344
788
|
import { existsSync as existsSync2 } from "node:fs";
|
|
789
|
+
import { readdir as readdir5 } from "node:fs/promises";
|
|
345
790
|
import { homedir as homedir5 } from "node:os";
|
|
346
791
|
import { join as join5 } from "node:path";
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
"Library",
|
|
358
|
-
"Application Support",
|
|
359
|
-
"opencode",
|
|
360
|
-
"opencode.db"
|
|
361
|
-
);
|
|
362
|
-
if (existsSync2(macPath)) return macPath;
|
|
363
|
-
}
|
|
364
|
-
return null;
|
|
365
|
-
}
|
|
366
|
-
function opencodeAdapter() {
|
|
792
|
+
var CANDIDATE_KEY_PREFIXES = [
|
|
793
|
+
"bubbleId:",
|
|
794
|
+
"composerData:",
|
|
795
|
+
"composer.composerData",
|
|
796
|
+
"aiService.prompts",
|
|
797
|
+
"aiService.generations",
|
|
798
|
+
"workbench.panel.composerChatViewPane."
|
|
799
|
+
];
|
|
800
|
+
var STATE_TABLES = ["ItemTable", "cursorDiskKV"];
|
|
801
|
+
function cursorAdapter() {
|
|
367
802
|
return {
|
|
368
|
-
name: "
|
|
803
|
+
name: "cursor",
|
|
369
804
|
async *messages(options) {
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
try {
|
|
374
|
-
const BetterSqlite3 = await import("better-sqlite3");
|
|
375
|
-
const Ctor = BetterSqlite3.default ?? BetterSqlite3;
|
|
376
|
-
db = new Ctor(dbPath, { readonly: true });
|
|
377
|
-
} catch {
|
|
378
|
-
console.warn(
|
|
379
|
-
"devrage: better-sqlite3 not available, skipping OpenCode sessions"
|
|
380
|
-
);
|
|
381
|
-
return;
|
|
805
|
+
const stores = await discoverCursorStateStores();
|
|
806
|
+
for (const store of stores) {
|
|
807
|
+
yield* parseCursorStore(store, options);
|
|
382
808
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
809
|
+
},
|
|
810
|
+
async *usage(options) {
|
|
811
|
+
const stores = await discoverCursorStateStores();
|
|
812
|
+
for (const store of stores) {
|
|
813
|
+
yield* parseCursorUsageStore(store, options);
|
|
387
814
|
}
|
|
388
815
|
}
|
|
389
816
|
};
|
|
390
817
|
}
|
|
391
|
-
function
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
818
|
+
async function discoverCursorStateStores() {
|
|
819
|
+
const stores = [];
|
|
820
|
+
const seen = /* @__PURE__ */ new Set();
|
|
821
|
+
for (const userDir of getCursorUserDirs()) {
|
|
822
|
+
if (!existsSync2(userDir)) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
const globalState = join5(userDir, "globalStorage", "state.vscdb");
|
|
826
|
+
if (existsSync2(globalState) && !seen.has(globalState)) {
|
|
827
|
+
seen.add(globalState);
|
|
828
|
+
stores.push({ path: globalState, scope: "global" });
|
|
829
|
+
}
|
|
830
|
+
const workspaceRoot = join5(userDir, "workspaceStorage");
|
|
831
|
+
let workspaceIds = [];
|
|
832
|
+
try {
|
|
833
|
+
workspaceIds = await readdir5(workspaceRoot);
|
|
834
|
+
} catch {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
for (const workspaceId of workspaceIds) {
|
|
838
|
+
const statePath = join5(workspaceRoot, workspaceId, "state.vscdb");
|
|
839
|
+
if (existsSync2(statePath) && !seen.has(statePath)) {
|
|
840
|
+
seen.add(statePath);
|
|
841
|
+
stores.push({ path: statePath, scope: "workspace", project: workspaceId });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
405
844
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
845
|
+
return stores;
|
|
846
|
+
}
|
|
847
|
+
function getCursorUserDirs() {
|
|
848
|
+
const configHome = envOrDefault("XDG_CONFIG_HOME", join5(homedir5(), ".config"));
|
|
849
|
+
const appData = envOrDefault("APPDATA", join5(homedir5(), "AppData", "Roaming"));
|
|
850
|
+
return uniqueStrings([
|
|
851
|
+
join5(homedir5(), "Library", "Application Support", "Cursor", "User"),
|
|
852
|
+
join5(configHome, "Cursor", "User"),
|
|
853
|
+
join5(appData, "Cursor", "User")
|
|
854
|
+
]);
|
|
855
|
+
}
|
|
856
|
+
async function* parseCursorStore(store, options) {
|
|
857
|
+
const db = await openCursorDb(store.path);
|
|
858
|
+
if (!db) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
try {
|
|
862
|
+
const rows = readStateRows(db);
|
|
863
|
+
const seen = /* @__PURE__ */ new Set();
|
|
864
|
+
for (const row of rows) {
|
|
865
|
+
try {
|
|
866
|
+
if (!isCandidateKey(row.key)) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const parsed = parseJsonValue(row.value);
|
|
870
|
+
if (parsed === void 0) {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
for (const message of extractCursorMessages(parsed, row.key)) {
|
|
874
|
+
const text = message.text.trim();
|
|
875
|
+
if (!isLikelyMessageText(text)) {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (options?.since && message.timestamp) {
|
|
879
|
+
const timestamp = new Date(message.timestamp);
|
|
880
|
+
if (Number.isFinite(timestamp.getTime()) && timestamp < options.since) {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const session = message.session ?? `${store.scope}:${row.key}`;
|
|
885
|
+
const dedupeKey = `${session}\0${message.timestamp ?? ""}\0${text}`;
|
|
886
|
+
if (seen.has(dedupeKey)) {
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
seen.add(dedupeKey);
|
|
890
|
+
yield {
|
|
891
|
+
text,
|
|
892
|
+
timestamp: message.timestamp,
|
|
893
|
+
session,
|
|
894
|
+
project: store.project
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
} catch {
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
} finally {
|
|
902
|
+
db.close();
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async function* parseCursorUsageStore(store, options) {
|
|
906
|
+
const db = await openCursorDb(store.path);
|
|
907
|
+
if (!db) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
try {
|
|
911
|
+
const rows = readStateRows(db);
|
|
912
|
+
const composerModels = collectComposerModels(rows);
|
|
913
|
+
const seen = /* @__PURE__ */ new Set();
|
|
914
|
+
for (const row of rows) {
|
|
915
|
+
try {
|
|
916
|
+
if (!row.key.startsWith("bubbleId:")) {
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
const parsed = parseJsonValue(row.value);
|
|
920
|
+
const usage2 = extractCursorBubbleUsage(parsed, row.key, composerModels);
|
|
921
|
+
if (!usage2) {
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (options?.since && usage2.timestamp) {
|
|
925
|
+
const timestamp = new Date(usage2.timestamp);
|
|
926
|
+
if (Number.isFinite(timestamp.getTime()) && timestamp < options.since) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const dedupeKey = `${usage2.session ?? ""}\0${usage2.timestamp ?? ""}\0${usage2.model ?? ""}\0${usage2.inputTokens}\0${usage2.outputTokens}`;
|
|
931
|
+
if (seen.has(dedupeKey)) {
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
seen.add(dedupeKey);
|
|
935
|
+
yield usage2;
|
|
936
|
+
} catch {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
} finally {
|
|
941
|
+
db.close();
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
async function openCursorDb(dbPath) {
|
|
945
|
+
try {
|
|
946
|
+
const BetterSqlite3 = await import("better-sqlite3");
|
|
947
|
+
const Ctor = BetterSqlite3.default ?? BetterSqlite3;
|
|
948
|
+
return new Ctor(
|
|
949
|
+
dbPath,
|
|
950
|
+
{ readonly: true }
|
|
951
|
+
);
|
|
952
|
+
} catch {
|
|
953
|
+
return null;
|
|
415
954
|
}
|
|
416
955
|
}
|
|
956
|
+
function readStateRows(db) {
|
|
957
|
+
const rows = [];
|
|
958
|
+
try {
|
|
959
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all();
|
|
960
|
+
const availableTables = new Set(tables.flatMap((table) => stringValue3(table.name) ?? []));
|
|
961
|
+
for (const table of STATE_TABLES) {
|
|
962
|
+
if (!availableTables.has(table)) {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
const columns = db.prepare(`PRAGMA table_info("${table}")`).all();
|
|
966
|
+
const columnNames = new Set(columns.flatMap((column) => stringValue3(column.name) ?? []));
|
|
967
|
+
if (!columnNames.has("key") || !columnNames.has("value")) {
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
const tableRows = db.prepare(`SELECT key, value FROM "${table}"`).all();
|
|
971
|
+
rows.push(
|
|
972
|
+
...tableRows.flatMap(
|
|
973
|
+
(row) => typeof row.key === "string" ? [{ key: row.key, value: row.value }] : []
|
|
974
|
+
)
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
} catch {
|
|
978
|
+
return rows;
|
|
979
|
+
}
|
|
980
|
+
return rows;
|
|
981
|
+
}
|
|
982
|
+
function isCandidateKey(key) {
|
|
983
|
+
return CANDIDATE_KEY_PREFIXES.some((prefix) => key === prefix || key.startsWith(prefix));
|
|
984
|
+
}
|
|
985
|
+
function parseJsonValue(value) {
|
|
986
|
+
const raw = decodeStateValue(value);
|
|
987
|
+
if (!raw) {
|
|
988
|
+
return void 0;
|
|
989
|
+
}
|
|
990
|
+
try {
|
|
991
|
+
const parsed = JSON.parse(raw);
|
|
992
|
+
if (typeof parsed !== "string") {
|
|
993
|
+
return parsed;
|
|
994
|
+
}
|
|
995
|
+
const trimmed = parsed.trim();
|
|
996
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
997
|
+
return parsed;
|
|
998
|
+
}
|
|
999
|
+
try {
|
|
1000
|
+
return JSON.parse(trimmed);
|
|
1001
|
+
} catch {
|
|
1002
|
+
return parsed;
|
|
1003
|
+
}
|
|
1004
|
+
} catch {
|
|
1005
|
+
return void 0;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function decodeStateValue(value) {
|
|
1009
|
+
if (typeof value === "string") {
|
|
1010
|
+
return value;
|
|
1011
|
+
}
|
|
1012
|
+
if (Buffer.isBuffer(value)) {
|
|
1013
|
+
return value.toString("utf-8");
|
|
1014
|
+
}
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
function extractCursorMessages(root, rowKey) {
|
|
1018
|
+
if (rowKey.startsWith("bubbleId:")) {
|
|
1019
|
+
const message = extractCursorBubbleMessage(root, rowKey);
|
|
1020
|
+
return message ? [message] : [];
|
|
1021
|
+
}
|
|
1022
|
+
const messages = [];
|
|
1023
|
+
collectRoleMessages(root, messages);
|
|
1024
|
+
if (rowKey.startsWith("aiService.prompts") || rowKey.startsWith("aiService.generations")) {
|
|
1025
|
+
collectPromptMessages(root, messages);
|
|
1026
|
+
}
|
|
1027
|
+
return uniqueMessages(messages);
|
|
1028
|
+
}
|
|
1029
|
+
function extractCursorBubbleMessage(root, rowKey) {
|
|
1030
|
+
const record = asRecord4(root);
|
|
1031
|
+
if (!record || numberValue3(record["type"]) !== 1) {
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
const text = firstTextField(record, ["text", "richText"]);
|
|
1035
|
+
if (!text) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
return {
|
|
1039
|
+
text,
|
|
1040
|
+
timestamp: extractTimestamp2(record),
|
|
1041
|
+
session: cursorBubbleSession(rowKey) ?? extractSession(record)
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function collectComposerModels(rows) {
|
|
1045
|
+
const models = /* @__PURE__ */ new Map();
|
|
1046
|
+
for (const row of rows) {
|
|
1047
|
+
if (!row.key.startsWith("composerData:")) {
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
const parsed = parseJsonValue(row.value);
|
|
1051
|
+
const record = asRecord4(parsed);
|
|
1052
|
+
if (!record) {
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
const composerId = stringValue3(record["composerId"]) ?? row.key.slice("composerData:".length);
|
|
1056
|
+
const model = extractCursorModel(record);
|
|
1057
|
+
if (composerId && model) {
|
|
1058
|
+
models.set(composerId, model);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return models;
|
|
1062
|
+
}
|
|
1063
|
+
function extractCursorBubbleUsage(root, rowKey, composerModels) {
|
|
1064
|
+
const record = asRecord4(root);
|
|
1065
|
+
if (!record || numberValue3(record["type"]) !== 2) {
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
const tokenCount = asRecord4(record["tokenCount"]);
|
|
1069
|
+
if (!tokenCount) {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
const inputTokens = numberValue3(tokenCount["inputTokens"] ?? tokenCount["input"]);
|
|
1073
|
+
const outputTokens = numberValue3(tokenCount["outputTokens"] ?? tokenCount["output"]);
|
|
1074
|
+
const cacheReadTokens = numberValue3(tokenCount["cacheReadTokens"] ?? tokenCount["cacheRead"]);
|
|
1075
|
+
const cacheWriteTokens = numberValue3(tokenCount["cacheWriteTokens"] ?? tokenCount["cacheWrite"]);
|
|
1076
|
+
if (inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens === 0) {
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
const composerId = cursorBubbleSession(rowKey);
|
|
1080
|
+
const model = extractCursorModel(record) ?? (composerId ? composerModels.get(composerId) : void 0);
|
|
1081
|
+
return {
|
|
1082
|
+
agent: "cursor",
|
|
1083
|
+
model,
|
|
1084
|
+
timestamp: extractTimestamp2(record),
|
|
1085
|
+
session: composerId ?? extractSession(record),
|
|
1086
|
+
inputTokens,
|
|
1087
|
+
outputTokens,
|
|
1088
|
+
reasoningTokens: numberValue3(tokenCount["reasoningTokens"] ?? tokenCount["reasoning"]),
|
|
1089
|
+
cacheReadTokens,
|
|
1090
|
+
cacheWriteTokens
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
function extractCursorModel(record) {
|
|
1094
|
+
const direct = firstStringField(record, ["model", "modelName", "modelId"]);
|
|
1095
|
+
if (direct) {
|
|
1096
|
+
return direct;
|
|
1097
|
+
}
|
|
1098
|
+
for (const field of ["modelInfo", "modelConfig"]) {
|
|
1099
|
+
const modelRecord = asRecord4(record[field]);
|
|
1100
|
+
if (!modelRecord) {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
const nested = firstStringField(modelRecord, ["modelName", "modelId", "id", "name"]);
|
|
1104
|
+
if (nested && nested !== "default") {
|
|
1105
|
+
return nested;
|
|
1106
|
+
}
|
|
1107
|
+
const selected = modelRecord["selectedModels"];
|
|
1108
|
+
if (Array.isArray(selected)) {
|
|
1109
|
+
for (const item of selected) {
|
|
1110
|
+
const itemRecord = asRecord4(item);
|
|
1111
|
+
const selectedModel = itemRecord ? firstStringField(itemRecord, ["modelId", "modelName", "id", "name"]) : void 0;
|
|
1112
|
+
if (selectedModel && selectedModel !== "default") {
|
|
1113
|
+
return selectedModel;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if (nested) {
|
|
1118
|
+
return nested;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return void 0;
|
|
1122
|
+
}
|
|
1123
|
+
function cursorBubbleSession(rowKey) {
|
|
1124
|
+
const [, composerId] = rowKey.split(":");
|
|
1125
|
+
return composerId?.trim() || void 0;
|
|
1126
|
+
}
|
|
1127
|
+
function collectRoleMessages(value, messages, inheritedSession, depth = 0) {
|
|
1128
|
+
if (depth > 12) {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (Array.isArray(value)) {
|
|
1132
|
+
for (const item of value) {
|
|
1133
|
+
collectRoleMessages(item, messages, inheritedSession, depth + 1);
|
|
1134
|
+
}
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const record = asRecord4(value);
|
|
1138
|
+
if (!record) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const session = extractSession(record) ?? inheritedSession;
|
|
1142
|
+
if (isUserAuthored(record)) {
|
|
1143
|
+
const text = extractMessageText(record);
|
|
1144
|
+
if (text) {
|
|
1145
|
+
messages.push({ text, timestamp: extractTimestamp2(record), session });
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
for (const child of Object.values(record)) {
|
|
1149
|
+
if (typeof child === "object" && child !== null) {
|
|
1150
|
+
collectRoleMessages(child, messages, session, depth + 1);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function collectPromptMessages(value, messages, inheritedSession, depth = 0) {
|
|
1155
|
+
if (depth > 12) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (typeof value === "string") {
|
|
1159
|
+
messages.push({ text: value, session: inheritedSession });
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (Array.isArray(value)) {
|
|
1163
|
+
for (const item of value) {
|
|
1164
|
+
collectPromptMessages(item, messages, inheritedSession, depth + 1);
|
|
1165
|
+
}
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const record = asRecord4(value);
|
|
1169
|
+
if (!record) {
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const session = extractSession(record) ?? inheritedSession;
|
|
1173
|
+
const prompt = firstTextField(record, [
|
|
1174
|
+
"prompt",
|
|
1175
|
+
"userPrompt",
|
|
1176
|
+
"originalPrompt",
|
|
1177
|
+
"currentPrompt",
|
|
1178
|
+
"query",
|
|
1179
|
+
"input"
|
|
1180
|
+
]);
|
|
1181
|
+
if (prompt && !isAssistantAuthored(record)) {
|
|
1182
|
+
messages.push({ text: prompt, timestamp: extractTimestamp2(record), session });
|
|
1183
|
+
}
|
|
1184
|
+
for (const child of Object.values(record)) {
|
|
1185
|
+
if (typeof child === "object" && child !== null) {
|
|
1186
|
+
collectPromptMessages(child, messages, session, depth + 1);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function isUserAuthored(record) {
|
|
1191
|
+
return ["role", "speaker", "sender", "author", "source", "from", "type", "kind"].some(
|
|
1192
|
+
(field) => actorIsUser(record[field])
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
function isAssistantAuthored(record) {
|
|
1196
|
+
return ["role", "speaker", "sender", "author", "source", "from", "type", "kind"].some(
|
|
1197
|
+
(field) => actorIsAssistant(record[field])
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
function actorIsUser(value) {
|
|
1201
|
+
const actor = actorString(value);
|
|
1202
|
+
return actor === "user" || actor === "human" || actor === "usermessage";
|
|
1203
|
+
}
|
|
1204
|
+
function actorIsAssistant(value) {
|
|
1205
|
+
const actor = actorString(value);
|
|
1206
|
+
return actor === "assistant" || actor === "ai" || actor === "assistantmessage";
|
|
1207
|
+
}
|
|
1208
|
+
function actorString(value) {
|
|
1209
|
+
if (typeof value === "string") {
|
|
1210
|
+
return value.toLowerCase().replace(/[^a-z]/g, "");
|
|
1211
|
+
}
|
|
1212
|
+
const record = asRecord4(value);
|
|
1213
|
+
if (!record) {
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
for (const field of ["role", "type", "name"]) {
|
|
1217
|
+
if (typeof record[field] === "string") {
|
|
1218
|
+
return record[field].toLowerCase().replace(/[^a-z]/g, "");
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
function extractMessageText(record) {
|
|
1224
|
+
return firstTextField(record, ["text", "content", "message", "prompt", "query", "input"]);
|
|
1225
|
+
}
|
|
1226
|
+
function firstTextField(record, fields) {
|
|
1227
|
+
for (const field of fields) {
|
|
1228
|
+
const text = contentToText(record[field]);
|
|
1229
|
+
if (text) {
|
|
1230
|
+
return text;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
function firstStringField(record, fields) {
|
|
1236
|
+
for (const field of fields) {
|
|
1237
|
+
const value = stringValue3(record[field]);
|
|
1238
|
+
if (value) {
|
|
1239
|
+
return value;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return void 0;
|
|
1243
|
+
}
|
|
1244
|
+
function contentToText(value) {
|
|
1245
|
+
if (typeof value === "string") {
|
|
1246
|
+
return value;
|
|
1247
|
+
}
|
|
1248
|
+
if (Array.isArray(value)) {
|
|
1249
|
+
const parts = value.map(contentToText).filter((part) => Boolean(part));
|
|
1250
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
1251
|
+
}
|
|
1252
|
+
const record = asRecord4(value);
|
|
1253
|
+
if (!record) {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
return firstTextField(record, ["text", "content", "message", "value"]);
|
|
1257
|
+
}
|
|
1258
|
+
function extractTimestamp2(record) {
|
|
1259
|
+
for (const field of ["timestamp", "createdAt", "updatedAt", "time", "created", "date", "ts"]) {
|
|
1260
|
+
const timestamp = normalizeTimestamp(record[field]);
|
|
1261
|
+
if (timestamp) {
|
|
1262
|
+
return timestamp;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return void 0;
|
|
1266
|
+
}
|
|
1267
|
+
function normalizeTimestamp(value) {
|
|
1268
|
+
if (typeof value === "string") {
|
|
1269
|
+
const date = new Date(value);
|
|
1270
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : void 0;
|
|
1271
|
+
}
|
|
1272
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1273
|
+
const milliseconds = value > 1e12 ? value : value * 1e3;
|
|
1274
|
+
const date = new Date(milliseconds);
|
|
1275
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : void 0;
|
|
1276
|
+
}
|
|
1277
|
+
return void 0;
|
|
1278
|
+
}
|
|
1279
|
+
function extractSession(record) {
|
|
1280
|
+
for (const field of ["conversationId", "composerId", "sessionId", "chatId", "threadId", "id"]) {
|
|
1281
|
+
const value = record[field];
|
|
1282
|
+
if (typeof value === "string" && value.trim()) {
|
|
1283
|
+
return value;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return void 0;
|
|
1287
|
+
}
|
|
1288
|
+
function isLikelyMessageText(text) {
|
|
1289
|
+
return text.length > 0 && !text.startsWith("<environment_context>");
|
|
1290
|
+
}
|
|
1291
|
+
function uniqueMessages(messages) {
|
|
1292
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1293
|
+
const unique = [];
|
|
1294
|
+
for (const message of messages) {
|
|
1295
|
+
const key = `${message.session ?? ""}\0${message.timestamp ?? ""}\0${message.text}`;
|
|
1296
|
+
if (seen.has(key)) {
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
seen.add(key);
|
|
1300
|
+
unique.push(message);
|
|
1301
|
+
}
|
|
1302
|
+
return unique;
|
|
1303
|
+
}
|
|
1304
|
+
function uniqueStrings(values) {
|
|
1305
|
+
return Array.from(new Set(values));
|
|
1306
|
+
}
|
|
1307
|
+
function envOrDefault(name, fallback) {
|
|
1308
|
+
const value = process.env[name];
|
|
1309
|
+
return value && value.trim() ? value : fallback;
|
|
1310
|
+
}
|
|
1311
|
+
function numberValue3(value) {
|
|
1312
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1313
|
+
}
|
|
1314
|
+
function stringValue3(value) {
|
|
1315
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
1316
|
+
}
|
|
1317
|
+
function asRecord4(value) {
|
|
1318
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
return value;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// src/adapters/opencode.ts
|
|
1325
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
1326
|
+
import { homedir as homedir6 } from "node:os";
|
|
1327
|
+
import { join as join6 } from "node:path";
|
|
1328
|
+
function getOpencodeDatabasePath() {
|
|
1329
|
+
const xdgPath = join6(
|
|
1330
|
+
process.env["XDG_DATA_HOME"] ?? join6(homedir6(), ".local", "share"),
|
|
1331
|
+
"opencode",
|
|
1332
|
+
"opencode.db"
|
|
1333
|
+
);
|
|
1334
|
+
if (existsSync3(xdgPath)) {
|
|
1335
|
+
return xdgPath;
|
|
1336
|
+
}
|
|
1337
|
+
if (process.platform === "darwin") {
|
|
1338
|
+
const macPath = join6(homedir6(), "Library", "Application Support", "opencode", "opencode.db");
|
|
1339
|
+
if (existsSync3(macPath)) {
|
|
1340
|
+
return macPath;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
function opencodeAdapter() {
|
|
1346
|
+
return {
|
|
1347
|
+
name: "opencode",
|
|
1348
|
+
async *messages(options) {
|
|
1349
|
+
const db = await openOpencodeDb();
|
|
1350
|
+
if (!db) {
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
try {
|
|
1354
|
+
yield* queryUserMessages(db, options);
|
|
1355
|
+
} finally {
|
|
1356
|
+
db.close();
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
async *usage(options) {
|
|
1360
|
+
const db = await openOpencodeDb();
|
|
1361
|
+
if (!db) {
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
yield* queryUsageRecords(db, options);
|
|
1366
|
+
} finally {
|
|
1367
|
+
db.close();
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
async function openOpencodeDb() {
|
|
1373
|
+
const dbPath = getOpencodeDatabasePath();
|
|
1374
|
+
if (!dbPath) {
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
try {
|
|
1378
|
+
const BetterSqlite3 = await import("better-sqlite3");
|
|
1379
|
+
const Ctor = BetterSqlite3.default ?? BetterSqlite3;
|
|
1380
|
+
return new Ctor(
|
|
1381
|
+
dbPath,
|
|
1382
|
+
{ readonly: true }
|
|
1383
|
+
);
|
|
1384
|
+
} catch {
|
|
1385
|
+
console.warn("devrage: better-sqlite3 not available, skipping OpenCode sessions");
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
function* queryUserMessages(db, options) {
|
|
1390
|
+
let query = `
|
|
1391
|
+
SELECT
|
|
1392
|
+
m.session_id,
|
|
1393
|
+
m.time_created,
|
|
1394
|
+
json_extract(p.data, '$.text') as text
|
|
1395
|
+
FROM message m
|
|
1396
|
+
JOIN part p ON p.message_id = m.id
|
|
1397
|
+
WHERE json_extract(m.data, '$.role') = 'user'
|
|
1398
|
+
AND json_extract(p.data, '$.type') = 'text'
|
|
1399
|
+
`;
|
|
1400
|
+
const params = [];
|
|
1401
|
+
if (options?.since) {
|
|
1402
|
+
query += ` AND m.time_created >= ?`;
|
|
1403
|
+
params.push(options.since.getTime());
|
|
1404
|
+
}
|
|
1405
|
+
query += ` ORDER BY m.time_created ASC`;
|
|
1406
|
+
const rows = db.prepare(query).all(...params);
|
|
1407
|
+
for (const row of rows) {
|
|
1408
|
+
if (!row.text || !row.text.trim()) {
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
yield {
|
|
1412
|
+
text: row.text,
|
|
1413
|
+
timestamp: new Date(row.time_created).toISOString(),
|
|
1414
|
+
session: row.session_id
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function* queryUsageRecords(db, options) {
|
|
1419
|
+
let where = `WHERE json_type(data, '$.tokens') = 'object'`;
|
|
1420
|
+
const params = [];
|
|
1421
|
+
if (options?.since) {
|
|
1422
|
+
where += ` AND time_created >= ?`;
|
|
1423
|
+
params.push(options.since.getTime());
|
|
1424
|
+
}
|
|
1425
|
+
const rows = db.prepare(`
|
|
1426
|
+
SELECT
|
|
1427
|
+
session_id,
|
|
1428
|
+
time_created,
|
|
1429
|
+
COALESCE(json_extract(data, '$.providerID'), json_extract(data, '$.model.providerID')) AS provider,
|
|
1430
|
+
COALESCE(json_extract(data, '$.modelID'), json_extract(data, '$.model.modelID')) AS model,
|
|
1431
|
+
json_extract(data, '$.cost') AS billed_cost,
|
|
1432
|
+
json_extract(data, '$.tokens.input') AS input_tokens,
|
|
1433
|
+
json_extract(data, '$.tokens.output') AS output_tokens,
|
|
1434
|
+
json_extract(data, '$.tokens.reasoning') AS reasoning_tokens,
|
|
1435
|
+
json_extract(data, '$.tokens.cache.read') AS cache_read_tokens,
|
|
1436
|
+
json_extract(data, '$.tokens.cache.write') AS cache_write_tokens
|
|
1437
|
+
FROM message
|
|
1438
|
+
${where}
|
|
1439
|
+
ORDER BY time_created ASC
|
|
1440
|
+
`).all(...params);
|
|
1441
|
+
for (const row of rows) {
|
|
1442
|
+
const inputTokens = numberValue4(row.input_tokens);
|
|
1443
|
+
const outputTokens = numberValue4(row.output_tokens);
|
|
1444
|
+
const reasoningTokens = numberValue4(row.reasoning_tokens);
|
|
1445
|
+
const cacheReadTokens = numberValue4(row.cache_read_tokens);
|
|
1446
|
+
const cacheWriteTokens = numberValue4(row.cache_write_tokens);
|
|
1447
|
+
const billedCost = numberValue4(row.billed_cost);
|
|
1448
|
+
if (inputTokens + outputTokens + reasoningTokens + cacheReadTokens + cacheWriteTokens === 0 && billedCost === 0) {
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
yield {
|
|
1452
|
+
agent: "opencode",
|
|
1453
|
+
provider: stringValue4(row.provider),
|
|
1454
|
+
model: stringValue4(row.model),
|
|
1455
|
+
timestamp: row.time_created ? new Date(row.time_created).toISOString() : void 0,
|
|
1456
|
+
session: stringValue4(row.session_id),
|
|
1457
|
+
billedCost,
|
|
1458
|
+
inputTokens,
|
|
1459
|
+
outputTokens,
|
|
1460
|
+
reasoningTokens,
|
|
1461
|
+
cacheReadTokens,
|
|
1462
|
+
cacheWriteTokens
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
function numberValue4(value) {
|
|
1467
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1468
|
+
}
|
|
1469
|
+
function stringValue4(value) {
|
|
1470
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
1471
|
+
}
|
|
417
1472
|
|
|
418
1473
|
// src/adapters/pi.ts
|
|
419
1474
|
import { createReadStream as createReadStream3 } from "node:fs";
|
|
420
|
-
import { readdir as
|
|
1475
|
+
import { readdir as readdir6, stat as stat4 } from "node:fs/promises";
|
|
421
1476
|
import { createInterface as createInterface3 } from "node:readline";
|
|
422
|
-
import { homedir as
|
|
423
|
-
import { join as
|
|
424
|
-
var PI_SESSIONS_DIR =
|
|
1477
|
+
import { homedir as homedir7 } from "node:os";
|
|
1478
|
+
import { join as join7 } from "node:path";
|
|
1479
|
+
var PI_SESSIONS_DIR = join7(homedir7(), ".pi", "agent", "sessions");
|
|
425
1480
|
function piAdapter() {
|
|
426
1481
|
return {
|
|
427
1482
|
name: "pi",
|
|
428
1483
|
async *messages(options) {
|
|
429
1484
|
yield* walkPiSessions(PI_SESSIONS_DIR, options);
|
|
1485
|
+
},
|
|
1486
|
+
async *usage(options) {
|
|
1487
|
+
yield* walkPiUsageSessions(PI_SESSIONS_DIR, options);
|
|
430
1488
|
}
|
|
431
1489
|
};
|
|
432
1490
|
}
|
|
433
1491
|
async function* walkPiSessions(dir, options, project) {
|
|
434
1492
|
let entries;
|
|
435
1493
|
try {
|
|
436
|
-
entries = await
|
|
1494
|
+
entries = await readdir6(dir);
|
|
437
1495
|
} catch {
|
|
438
1496
|
return;
|
|
439
1497
|
}
|
|
440
1498
|
for (const entry of entries) {
|
|
441
|
-
const fullPath =
|
|
1499
|
+
const fullPath = join7(dir, entry);
|
|
442
1500
|
const entryStat = await stat4(fullPath).catch(() => null);
|
|
443
|
-
if (!entryStat)
|
|
1501
|
+
if (!entryStat) {
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
444
1504
|
if (entryStat.isDirectory()) {
|
|
445
1505
|
yield* walkPiSessions(fullPath, options, project ?? entry);
|
|
446
1506
|
} else if (entry.endsWith(".jsonl")) {
|
|
@@ -449,6 +1509,27 @@ async function* walkPiSessions(dir, options, project) {
|
|
|
449
1509
|
}
|
|
450
1510
|
}
|
|
451
1511
|
}
|
|
1512
|
+
async function* walkPiUsageSessions(dir, options, project) {
|
|
1513
|
+
let entries;
|
|
1514
|
+
try {
|
|
1515
|
+
entries = await readdir6(dir);
|
|
1516
|
+
} catch {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
for (const entry of entries) {
|
|
1520
|
+
const fullPath = join7(dir, entry);
|
|
1521
|
+
const entryStat = await stat4(fullPath).catch(() => null);
|
|
1522
|
+
if (!entryStat) {
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
if (entryStat.isDirectory()) {
|
|
1526
|
+
yield* walkPiUsageSessions(fullPath, options, project ?? entry);
|
|
1527
|
+
} else if (entry.endsWith(".jsonl")) {
|
|
1528
|
+
const session = entry.replace(".jsonl", "");
|
|
1529
|
+
yield* parsePiUsageJsonl(fullPath, { session, project, since: options?.since });
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
452
1533
|
async function* parsePiJsonl(filePath, context) {
|
|
453
1534
|
const rl = createInterface3({
|
|
454
1535
|
input: createReadStream3(filePath, { encoding: "utf-8" }),
|
|
@@ -456,22 +1537,32 @@ async function* parsePiJsonl(filePath, context) {
|
|
|
456
1537
|
});
|
|
457
1538
|
let project = context.project;
|
|
458
1539
|
for await (const line of rl) {
|
|
459
|
-
if (!line.trim())
|
|
1540
|
+
if (!line.trim()) {
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
460
1543
|
try {
|
|
461
1544
|
const entry = JSON.parse(line);
|
|
462
1545
|
if (entry.type === "session") {
|
|
463
1546
|
project = entry.cwd ?? project;
|
|
464
1547
|
continue;
|
|
465
1548
|
}
|
|
466
|
-
if (entry.type !== "message")
|
|
1549
|
+
if (entry.type !== "message") {
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
467
1552
|
const message = entry.message;
|
|
468
|
-
if (!message || message.role !== "user")
|
|
1553
|
+
if (!message || message.role !== "user") {
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
469
1556
|
const text = contentToString2(message.content);
|
|
470
|
-
if (!text)
|
|
1557
|
+
if (!text) {
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
471
1560
|
const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : typeof message.timestamp === "number" ? new Date(message.timestamp).toISOString() : void 0;
|
|
472
1561
|
if (context.since && timestamp) {
|
|
473
1562
|
const ts = new Date(timestamp);
|
|
474
|
-
if (ts < context.since)
|
|
1563
|
+
if (ts < context.since) {
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
475
1566
|
}
|
|
476
1567
|
yield {
|
|
477
1568
|
text,
|
|
@@ -483,8 +1574,64 @@ async function* parsePiJsonl(filePath, context) {
|
|
|
483
1574
|
}
|
|
484
1575
|
}
|
|
485
1576
|
}
|
|
1577
|
+
async function* parsePiUsageJsonl(filePath, context) {
|
|
1578
|
+
const rl = createInterface3({
|
|
1579
|
+
input: createReadStream3(filePath, { encoding: "utf-8" }),
|
|
1580
|
+
crlfDelay: Infinity
|
|
1581
|
+
});
|
|
1582
|
+
for await (const line of rl) {
|
|
1583
|
+
if (!line.trim()) {
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
try {
|
|
1587
|
+
const entry = JSON.parse(line);
|
|
1588
|
+
if (entry.type !== "message") {
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const message = entry.message;
|
|
1592
|
+
if (!message || message.role !== "assistant") {
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
const usage2 = asRecord5(message.usage);
|
|
1596
|
+
if (!usage2) {
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
const inputTokens = numberValue5(usage2["input"]);
|
|
1600
|
+
const outputTokens = numberValue5(usage2["output"]);
|
|
1601
|
+
const cacheReadTokens = numberValue5(usage2["cacheRead"]);
|
|
1602
|
+
const cacheWriteTokens = numberValue5(usage2["cacheWrite"]);
|
|
1603
|
+
if (inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens === 0) {
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : typeof message.timestamp === "number" ? new Date(message.timestamp).toISOString() : void 0;
|
|
1607
|
+
if (context.since && timestamp) {
|
|
1608
|
+
const ts = new Date(timestamp);
|
|
1609
|
+
if (ts < context.since) {
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
const responseModel = stringValue5(message.responseModel);
|
|
1614
|
+
const model = responseModel ?? stringValue5(message.model);
|
|
1615
|
+
yield {
|
|
1616
|
+
agent: "pi",
|
|
1617
|
+
provider: responseModel?.includes("/") ? void 0 : stringValue5(message.provider),
|
|
1618
|
+
model,
|
|
1619
|
+
timestamp,
|
|
1620
|
+
session: context.session,
|
|
1621
|
+
inputTokens,
|
|
1622
|
+
outputTokens,
|
|
1623
|
+
reasoningTokens: 0,
|
|
1624
|
+
cacheReadTokens,
|
|
1625
|
+
cacheWriteTokens
|
|
1626
|
+
};
|
|
1627
|
+
} catch {
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
486
1631
|
function contentToString2(content) {
|
|
487
|
-
if (typeof content === "string")
|
|
1632
|
+
if (typeof content === "string") {
|
|
1633
|
+
return content;
|
|
1634
|
+
}
|
|
488
1635
|
if (Array.isArray(content)) {
|
|
489
1636
|
const parts = content.filter(
|
|
490
1637
|
(p) => typeof p === "object" && p !== null && p.type === "text" && typeof p.text === "string"
|
|
@@ -493,27 +1640,36 @@ function contentToString2(content) {
|
|
|
493
1640
|
}
|
|
494
1641
|
return null;
|
|
495
1642
|
}
|
|
1643
|
+
function numberValue5(value) {
|
|
1644
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1645
|
+
}
|
|
1646
|
+
function stringValue5(value) {
|
|
1647
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
1648
|
+
}
|
|
1649
|
+
function asRecord5(value) {
|
|
1650
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1653
|
+
return value;
|
|
1654
|
+
}
|
|
496
1655
|
|
|
497
1656
|
// src/adapters/zed.ts
|
|
498
|
-
import { readdir as
|
|
499
|
-
import { existsSync as
|
|
500
|
-
import { homedir as
|
|
501
|
-
import { join as
|
|
1657
|
+
import { readdir as readdir7, readFile as readFile3 } from "node:fs/promises";
|
|
1658
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1659
|
+
import { homedir as homedir8 } from "node:os";
|
|
1660
|
+
import { join as join8 } from "node:path";
|
|
502
1661
|
function getZedPaths() {
|
|
503
1662
|
if (process.platform === "darwin") {
|
|
504
|
-
const base2 =
|
|
1663
|
+
const base2 = join8(homedir8(), "Library", "Application Support", "Zed");
|
|
505
1664
|
return {
|
|
506
|
-
conversations:
|
|
507
|
-
db:
|
|
1665
|
+
conversations: join8(base2, "conversations"),
|
|
1666
|
+
db: join8(base2, "db")
|
|
508
1667
|
};
|
|
509
1668
|
}
|
|
510
|
-
const base =
|
|
511
|
-
process.env["XDG_DATA_HOME"] ?? join7(homedir7(), ".local", "share"),
|
|
512
|
-
"zed"
|
|
513
|
-
);
|
|
1669
|
+
const base = join8(process.env["XDG_DATA_HOME"] ?? join8(homedir8(), ".local", "share"), "zed");
|
|
514
1670
|
return {
|
|
515
|
-
conversations:
|
|
516
|
-
db:
|
|
1671
|
+
conversations: join8(base, "conversations"),
|
|
1672
|
+
db: join8(base, "db")
|
|
517
1673
|
};
|
|
518
1674
|
}
|
|
519
1675
|
function zedAdapter() {
|
|
@@ -527,25 +1683,33 @@ function zedAdapter() {
|
|
|
527
1683
|
};
|
|
528
1684
|
}
|
|
529
1685
|
async function* parseTextThreads(dir, _options) {
|
|
530
|
-
if (!
|
|
1686
|
+
if (!existsSync4(dir)) {
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
531
1689
|
let files;
|
|
532
1690
|
try {
|
|
533
|
-
files = await
|
|
1691
|
+
files = await readdir7(dir);
|
|
534
1692
|
} catch {
|
|
535
1693
|
return;
|
|
536
1694
|
}
|
|
537
1695
|
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
538
1696
|
for (const file of jsonFiles) {
|
|
539
|
-
const filePath =
|
|
1697
|
+
const filePath = join8(dir, file);
|
|
540
1698
|
const session = file.replace(".json", "");
|
|
541
1699
|
try {
|
|
542
1700
|
const raw = await readFile3(filePath, "utf-8");
|
|
543
1701
|
const conversation = JSON.parse(raw);
|
|
544
|
-
if (!conversation.messages || !Array.isArray(conversation.messages))
|
|
1702
|
+
if (!conversation.messages || !Array.isArray(conversation.messages)) {
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
545
1705
|
for (const msg of conversation.messages) {
|
|
546
|
-
if (msg.role !== "user")
|
|
1706
|
+
if (msg.role !== "user") {
|
|
1707
|
+
continue;
|
|
1708
|
+
}
|
|
547
1709
|
const text = typeof msg.content === "string" ? msg.content : null;
|
|
548
|
-
if (!text)
|
|
1710
|
+
if (!text) {
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
549
1713
|
yield {
|
|
550
1714
|
text,
|
|
551
1715
|
session
|
|
@@ -556,15 +1720,19 @@ async function* parseTextThreads(dir, _options) {
|
|
|
556
1720
|
}
|
|
557
1721
|
}
|
|
558
1722
|
async function* parseAgentThreads(dbDir, _options) {
|
|
559
|
-
if (!
|
|
1723
|
+
if (!existsSync4(dbDir)) {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
560
1726
|
let dbFiles;
|
|
561
1727
|
try {
|
|
562
|
-
const entries = await
|
|
1728
|
+
const entries = await readdir7(dbDir);
|
|
563
1729
|
dbFiles = entries.filter((f) => f.endsWith(".db"));
|
|
564
1730
|
} catch {
|
|
565
1731
|
return;
|
|
566
1732
|
}
|
|
567
|
-
if (dbFiles.length === 0)
|
|
1733
|
+
if (dbFiles.length === 0) {
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
568
1736
|
let Database;
|
|
569
1737
|
try {
|
|
570
1738
|
const mod = await import("better-sqlite3");
|
|
@@ -573,13 +1741,12 @@ async function* parseAgentThreads(dbDir, _options) {
|
|
|
573
1741
|
return;
|
|
574
1742
|
}
|
|
575
1743
|
for (const dbFile of dbFiles) {
|
|
576
|
-
const dbPath =
|
|
1744
|
+
const dbPath = join8(dbDir, dbFile);
|
|
577
1745
|
let db;
|
|
578
1746
|
try {
|
|
579
|
-
db = new Database(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
);
|
|
1747
|
+
db = new Database(dbPath, {
|
|
1748
|
+
readonly: true
|
|
1749
|
+
});
|
|
583
1750
|
} catch {
|
|
584
1751
|
continue;
|
|
585
1752
|
}
|
|
@@ -604,7 +1771,9 @@ async function* parseAgentThreads(dbDir, _options) {
|
|
|
604
1771
|
let query = `SELECT "${contentCol}" as text FROM "${msgTable}" WHERE role = 'user'`;
|
|
605
1772
|
const rows = db.prepare(query).all();
|
|
606
1773
|
for (const row of rows) {
|
|
607
|
-
if (!row.text?.trim())
|
|
1774
|
+
if (!row.text?.trim()) {
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
608
1777
|
yield { text: row.text };
|
|
609
1778
|
}
|
|
610
1779
|
} catch {
|
|
@@ -618,6 +1787,7 @@ async function* parseAgentThreads(dbDir, _options) {
|
|
|
618
1787
|
var ADAPTERS = {
|
|
619
1788
|
claude: claudeAdapter,
|
|
620
1789
|
codex: codexAdapter,
|
|
1790
|
+
cursor: cursorAdapter,
|
|
621
1791
|
opencode: opencodeAdapter,
|
|
622
1792
|
amp: ampAdapter,
|
|
623
1793
|
cline: clineAdapter,
|
|
@@ -627,9 +1797,7 @@ var ADAPTERS = {
|
|
|
627
1797
|
function createAdapter(name) {
|
|
628
1798
|
const factory = ADAPTERS[name];
|
|
629
1799
|
if (!factory) {
|
|
630
|
-
throw new Error(
|
|
631
|
-
`unknown adapter: ${name} (available: ${Object.keys(ADAPTERS).join(", ")})`
|
|
632
|
-
);
|
|
1800
|
+
throw new Error(`unknown adapter: ${name} (available: ${Object.keys(ADAPTERS).join(", ")})`);
|
|
633
1801
|
}
|
|
634
1802
|
return factory();
|
|
635
1803
|
}
|
|
@@ -773,10 +1941,14 @@ function runPattern(_originalText, searchText, matches, seen) {
|
|
|
773
1941
|
DEFAULT_PATTERN.lastIndex = 0;
|
|
774
1942
|
let match;
|
|
775
1943
|
while ((match = DEFAULT_PATTERN.exec(searchText)) !== null) {
|
|
776
|
-
if (seen.has(match.index))
|
|
1944
|
+
if (seen.has(match.index)) {
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
777
1947
|
const word = match[0].toLowerCase();
|
|
778
1948
|
const entry = WORD_MAP.get(word);
|
|
779
|
-
if (!entry)
|
|
1949
|
+
if (!entry) {
|
|
1950
|
+
continue;
|
|
1951
|
+
}
|
|
780
1952
|
seen.add(match.index);
|
|
781
1953
|
matches.push({
|
|
782
1954
|
word,
|
|
@@ -787,6 +1959,450 @@ function runPattern(_originalText, searchText, matches, seen) {
|
|
|
787
1959
|
}
|
|
788
1960
|
}
|
|
789
1961
|
|
|
1962
|
+
// src/pricing/index.ts
|
|
1963
|
+
import { mkdir, readFile as readFile4, writeFile } from "node:fs/promises";
|
|
1964
|
+
import { homedir as homedir9 } from "node:os";
|
|
1965
|
+
import { dirname, join as join9 } from "node:path";
|
|
1966
|
+
var MODELS_DEV_URL = "https://models.dev/api.json";
|
|
1967
|
+
var CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1968
|
+
var FETCH_TIMEOUT_MS = 2e3;
|
|
1969
|
+
var FALLBACK_COSTS = {
|
|
1970
|
+
openai: {
|
|
1971
|
+
"gpt-5.5": {
|
|
1972
|
+
input: 5,
|
|
1973
|
+
output: 30,
|
|
1974
|
+
cache_read: 0.5,
|
|
1975
|
+
context_over_200k: { input: 10, output: 45, cache_read: 1 }
|
|
1976
|
+
},
|
|
1977
|
+
"gpt-5.5-pro": { input: 30, output: 180 },
|
|
1978
|
+
"gpt-5.4": { input: 2.5, output: 15, cache_read: 0.25 },
|
|
1979
|
+
"gpt-5.4-mini": { input: 0.75, output: 4.5, cache_read: 0.075 },
|
|
1980
|
+
"gpt-5.4-nano": { input: 0.2, output: 1.25, cache_read: 0.02 },
|
|
1981
|
+
"gpt-5.4-pro": { input: 30, output: 180 },
|
|
1982
|
+
"gpt-5.3-codex": { input: 1.75, output: 14, cache_read: 0.175 }
|
|
1983
|
+
},
|
|
1984
|
+
anthropic: {
|
|
1985
|
+
"claude-opus-4-7": { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
var PROVIDER_ALIASES = {
|
|
1989
|
+
anthropic: "anthropic",
|
|
1990
|
+
claude: "anthropic",
|
|
1991
|
+
openai: "openai"
|
|
1992
|
+
};
|
|
1993
|
+
async function loadPricingCatalog(options = {}) {
|
|
1994
|
+
const cachePath = getPricingCachePath();
|
|
1995
|
+
const cache = await readPricingCache(cachePath);
|
|
1996
|
+
const ttlMs = options.cacheTtlMs ?? CACHE_TTL_MS;
|
|
1997
|
+
if (!options.refresh && cache && isFresh(cache.fetchedAt, ttlMs)) {
|
|
1998
|
+
return {
|
|
1999
|
+
source: "catalog",
|
|
2000
|
+
fetchedAt: cache.fetchedAt,
|
|
2001
|
+
cachePath,
|
|
2002
|
+
catalog: cache.catalog
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
try {
|
|
2006
|
+
const catalog = await fetchModelsDevCatalog(options.fetchTimeoutMs ?? FETCH_TIMEOUT_MS);
|
|
2007
|
+
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2008
|
+
await writePricingCache(cachePath, {
|
|
2009
|
+
source: "models.dev",
|
|
2010
|
+
fetchedAt,
|
|
2011
|
+
schemaVersion: 1,
|
|
2012
|
+
catalog
|
|
2013
|
+
});
|
|
2014
|
+
return { source: "catalog", fetchedAt, cachePath, catalog };
|
|
2015
|
+
} catch {
|
|
2016
|
+
if (cache) {
|
|
2017
|
+
return {
|
|
2018
|
+
source: "stale-catalog",
|
|
2019
|
+
fetchedAt: cache.fetchedAt,
|
|
2020
|
+
cachePath,
|
|
2021
|
+
catalog: cache.catalog
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
return { source: "fallback", cachePath };
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
async function summarizeUsage(records, pricing) {
|
|
2028
|
+
const total = createCostAccumulator();
|
|
2029
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
2030
|
+
for await (const record of records) {
|
|
2031
|
+
const priced = priceUsageRecord(record, pricing);
|
|
2032
|
+
const billedCost = record.billedCost ?? 0;
|
|
2033
|
+
const isUnpriced = priced.source === "stored" || priced.source === "unknown";
|
|
2034
|
+
addUsageToAccumulator(total, record, priced, billedCost, isUnpriced);
|
|
2035
|
+
const day = timestampDay(record.timestamp);
|
|
2036
|
+
if (day) {
|
|
2037
|
+
let dayAccumulator = byDay.get(day);
|
|
2038
|
+
if (!dayAccumulator) {
|
|
2039
|
+
dayAccumulator = createCostAccumulator();
|
|
2040
|
+
byDay.set(day, dayAccumulator);
|
|
2041
|
+
}
|
|
2042
|
+
addUsageToAccumulator(dayAccumulator, record, priced, billedCost, isUnpriced);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return {
|
|
2046
|
+
requests: total.requests,
|
|
2047
|
+
estimatedCost: total.estimatedCost,
|
|
2048
|
+
billedCost: total.billedCost,
|
|
2049
|
+
unpricedRequests: total.unpricedRequests,
|
|
2050
|
+
inputTokens: total.inputTokens,
|
|
2051
|
+
outputTokens: total.outputTokens,
|
|
2052
|
+
reasoningTokens: total.reasoningTokens,
|
|
2053
|
+
cacheReadTokens: total.cacheReadTokens,
|
|
2054
|
+
cacheWriteTokens: total.cacheWriteTokens,
|
|
2055
|
+
models: sortedModels(total.byModel),
|
|
2056
|
+
days: Array.from(byDay.entries()).sort(([left], [right]) => left.localeCompare(right)).map(([day, bucket]) => costDaySummary(day, bucket)),
|
|
2057
|
+
pricing: {
|
|
2058
|
+
source: pricing.source,
|
|
2059
|
+
fetchedAt: pricing.fetchedAt
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
function getPricingCachePath() {
|
|
2064
|
+
if (process.env["XDG_CACHE_HOME"]) {
|
|
2065
|
+
return join9(process.env["XDG_CACHE_HOME"], "devrage", "models.dev.json");
|
|
2066
|
+
}
|
|
2067
|
+
if (process.platform === "darwin") {
|
|
2068
|
+
return join9(homedir9(), "Library", "Caches", "devrage", "models.dev.json");
|
|
2069
|
+
}
|
|
2070
|
+
if (process.platform === "win32") {
|
|
2071
|
+
const localAppData = process.env["LOCALAPPDATA"] ?? join9(homedir9(), "AppData", "Local");
|
|
2072
|
+
return join9(localAppData, "devrage", "models.dev.json");
|
|
2073
|
+
}
|
|
2074
|
+
return join9(homedir9(), ".cache", "devrage", "models.dev.json");
|
|
2075
|
+
}
|
|
2076
|
+
function createCostAccumulator() {
|
|
2077
|
+
return {
|
|
2078
|
+
requests: 0,
|
|
2079
|
+
estimatedCost: 0,
|
|
2080
|
+
billedCost: 0,
|
|
2081
|
+
unpricedRequests: 0,
|
|
2082
|
+
inputTokens: 0,
|
|
2083
|
+
outputTokens: 0,
|
|
2084
|
+
reasoningTokens: 0,
|
|
2085
|
+
cacheReadTokens: 0,
|
|
2086
|
+
cacheWriteTokens: 0,
|
|
2087
|
+
byModel: /* @__PURE__ */ new Map()
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
function addUsageToAccumulator(bucket, record, priced, billedCost, isUnpriced) {
|
|
2091
|
+
const key = `${priced.provider ?? ""}:${priced.model}`;
|
|
2092
|
+
let model = bucket.byModel.get(key);
|
|
2093
|
+
if (!model) {
|
|
2094
|
+
model = {
|
|
2095
|
+
model: priced.model,
|
|
2096
|
+
provider: priced.provider,
|
|
2097
|
+
requests: 0,
|
|
2098
|
+
estimatedCost: 0,
|
|
2099
|
+
billedCost: 0,
|
|
2100
|
+
pricingSource: priced.source,
|
|
2101
|
+
unpricedRequests: 0,
|
|
2102
|
+
inputTokens: 0,
|
|
2103
|
+
outputTokens: 0,
|
|
2104
|
+
reasoningTokens: 0,
|
|
2105
|
+
cacheReadTokens: 0,
|
|
2106
|
+
cacheWriteTokens: 0
|
|
2107
|
+
};
|
|
2108
|
+
bucket.byModel.set(key, model);
|
|
2109
|
+
}
|
|
2110
|
+
model.requests += 1;
|
|
2111
|
+
model.estimatedCost += priced.estimatedCost;
|
|
2112
|
+
model.billedCost += billedCost;
|
|
2113
|
+
model.pricingSource = mergePricingSource(model.pricingSource, priced.source);
|
|
2114
|
+
model.inputTokens += record.inputTokens;
|
|
2115
|
+
model.outputTokens += record.outputTokens;
|
|
2116
|
+
model.reasoningTokens += record.reasoningTokens;
|
|
2117
|
+
model.cacheReadTokens += record.cacheReadTokens;
|
|
2118
|
+
model.cacheWriteTokens += record.cacheWriteTokens;
|
|
2119
|
+
bucket.requests += 1;
|
|
2120
|
+
bucket.estimatedCost += priced.estimatedCost;
|
|
2121
|
+
bucket.billedCost += billedCost;
|
|
2122
|
+
bucket.inputTokens += record.inputTokens;
|
|
2123
|
+
bucket.outputTokens += record.outputTokens;
|
|
2124
|
+
bucket.reasoningTokens += record.reasoningTokens;
|
|
2125
|
+
bucket.cacheReadTokens += record.cacheReadTokens;
|
|
2126
|
+
bucket.cacheWriteTokens += record.cacheWriteTokens;
|
|
2127
|
+
if (isUnpriced) {
|
|
2128
|
+
model.unpricedRequests += 1;
|
|
2129
|
+
bucket.unpricedRequests += 1;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
function costDaySummary(day, bucket) {
|
|
2133
|
+
return {
|
|
2134
|
+
day,
|
|
2135
|
+
requests: bucket.requests,
|
|
2136
|
+
estimatedCost: bucket.estimatedCost,
|
|
2137
|
+
billedCost: bucket.billedCost,
|
|
2138
|
+
unpricedRequests: bucket.unpricedRequests,
|
|
2139
|
+
inputTokens: bucket.inputTokens,
|
|
2140
|
+
outputTokens: bucket.outputTokens,
|
|
2141
|
+
reasoningTokens: bucket.reasoningTokens,
|
|
2142
|
+
cacheReadTokens: bucket.cacheReadTokens,
|
|
2143
|
+
cacheWriteTokens: bucket.cacheWriteTokens,
|
|
2144
|
+
models: sortedModels(bucket.byModel)
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
function sortedModels(byModel) {
|
|
2148
|
+
return Array.from(byModel.values()).sort(
|
|
2149
|
+
(a, b) => b.estimatedCost - a.estimatedCost || b.requests - a.requests
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
function timestampDay(timestamp) {
|
|
2153
|
+
if (!timestamp) {
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
const time = new Date(timestamp).getTime();
|
|
2157
|
+
if (!Number.isFinite(time)) {
|
|
2158
|
+
return null;
|
|
2159
|
+
}
|
|
2160
|
+
return new Date(time).toISOString().slice(0, 10);
|
|
2161
|
+
}
|
|
2162
|
+
async function fetchModelsDevCatalog(timeoutMs) {
|
|
2163
|
+
const controller = new AbortController();
|
|
2164
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
2165
|
+
try {
|
|
2166
|
+
const response = await fetch(MODELS_DEV_URL, { signal: controller.signal });
|
|
2167
|
+
if (!response.ok) {
|
|
2168
|
+
throw new Error(`models.dev returned ${response.status}`);
|
|
2169
|
+
}
|
|
2170
|
+
const catalog = await response.json();
|
|
2171
|
+
if (!isModelsDevCatalog(catalog)) {
|
|
2172
|
+
throw new Error("models.dev response did not match expected shape");
|
|
2173
|
+
}
|
|
2174
|
+
return catalog;
|
|
2175
|
+
} finally {
|
|
2176
|
+
clearTimeout(timeout);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
async function readPricingCache(cachePath) {
|
|
2180
|
+
try {
|
|
2181
|
+
const raw = await readFile4(cachePath, "utf-8");
|
|
2182
|
+
const parsed = JSON.parse(raw);
|
|
2183
|
+
const cache = asRecord6(parsed);
|
|
2184
|
+
if (cache?.["source"] !== "models.dev" || cache["schemaVersion"] !== 1 || typeof cache["fetchedAt"] !== "string" || !isModelsDevCatalog(cache["catalog"])) {
|
|
2185
|
+
return null;
|
|
2186
|
+
}
|
|
2187
|
+
return {
|
|
2188
|
+
source: "models.dev",
|
|
2189
|
+
fetchedAt: cache["fetchedAt"],
|
|
2190
|
+
schemaVersion: 1,
|
|
2191
|
+
catalog: cache["catalog"]
|
|
2192
|
+
};
|
|
2193
|
+
} catch {
|
|
2194
|
+
return null;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
async function writePricingCache(cachePath, cache) {
|
|
2198
|
+
try {
|
|
2199
|
+
await mkdir(dirname(cachePath), { recursive: true });
|
|
2200
|
+
await writeFile(cachePath, `${JSON.stringify(cache)}
|
|
2201
|
+
`, "utf-8");
|
|
2202
|
+
} catch {
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
function priceUsageRecord(record, catalog) {
|
|
2206
|
+
const resolved = resolveRates(record, catalog);
|
|
2207
|
+
if (!resolved) {
|
|
2208
|
+
return {
|
|
2209
|
+
provider: normalizeProvider(record.provider),
|
|
2210
|
+
model: normalizeModel(record.model) ?? "unknown",
|
|
2211
|
+
estimatedCost: 0,
|
|
2212
|
+
source: (record.billedCost ?? 0) > 0 ? "stored" : "unknown"
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
const rates = selectContextRates(resolved.rates, record);
|
|
2216
|
+
const inputRate = rates.input ?? 0;
|
|
2217
|
+
const outputRate = rates.output ?? 0;
|
|
2218
|
+
const cacheReadRate = rates.cache_read ?? inputRate;
|
|
2219
|
+
const cacheWriteRate = rates.cache_write ?? inputRate;
|
|
2220
|
+
const outputTokens = record.outputTokens + record.reasoningTokens;
|
|
2221
|
+
return {
|
|
2222
|
+
provider: resolved.provider,
|
|
2223
|
+
model: resolved.model,
|
|
2224
|
+
estimatedCost: (record.inputTokens * inputRate + record.cacheReadTokens * cacheReadRate + record.cacheWriteTokens * cacheWriteRate + outputTokens * outputRate) / 1e6,
|
|
2225
|
+
source: resolved.source
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
function resolveRates(record, pricing) {
|
|
2229
|
+
const candidates = modelCandidates(record.provider, record.model);
|
|
2230
|
+
for (const candidate of candidates) {
|
|
2231
|
+
if (!candidate.provider || !pricing.catalog) {
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
const rates = getCatalogRates(pricing.catalog, candidate.provider, candidate.model);
|
|
2235
|
+
if (rates) {
|
|
2236
|
+
return {
|
|
2237
|
+
provider: candidate.provider,
|
|
2238
|
+
model: candidate.model,
|
|
2239
|
+
rates,
|
|
2240
|
+
source: pricing.source === "stale-catalog" ? "stale-catalog" : "catalog"
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
for (const candidate of candidates) {
|
|
2245
|
+
if (!candidate.provider) {
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
const providerRates = FALLBACK_COSTS[candidate.provider];
|
|
2249
|
+
const rates = providerRates?.[candidate.model];
|
|
2250
|
+
if (rates) {
|
|
2251
|
+
return { provider: candidate.provider, model: candidate.model, rates, source: "fallback" };
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
return null;
|
|
2255
|
+
}
|
|
2256
|
+
function modelCandidates(providerInput, modelInput) {
|
|
2257
|
+
const candidates = [];
|
|
2258
|
+
let provider = normalizeProvider(providerInput);
|
|
2259
|
+
let model = normalizeModel(modelInput);
|
|
2260
|
+
if (!model) {
|
|
2261
|
+
return candidates;
|
|
2262
|
+
}
|
|
2263
|
+
const prefixed = splitProviderModel(model);
|
|
2264
|
+
if (prefixed) {
|
|
2265
|
+
provider = provider ?? prefixed.provider;
|
|
2266
|
+
model = prefixed.model;
|
|
2267
|
+
}
|
|
2268
|
+
addCandidate(candidates, provider, model);
|
|
2269
|
+
addCandidate(candidates, provider, MODEL_ALIASES[model]);
|
|
2270
|
+
const inferred = provider ?? inferProvider(model);
|
|
2271
|
+
addCandidate(candidates, inferred, model);
|
|
2272
|
+
addCandidate(candidates, inferred, MODEL_ALIASES[model]);
|
|
2273
|
+
for (const fallbackProvider of Object.keys(FALLBACK_COSTS)) {
|
|
2274
|
+
addCandidate(candidates, fallbackProvider, model);
|
|
2275
|
+
addCandidate(candidates, fallbackProvider, MODEL_ALIASES[model]);
|
|
2276
|
+
}
|
|
2277
|
+
return candidates;
|
|
2278
|
+
}
|
|
2279
|
+
var MODEL_ALIASES = {
|
|
2280
|
+
"gpt-5.5-chat-latest": "gpt-5.5"
|
|
2281
|
+
};
|
|
2282
|
+
function addCandidate(candidates, provider, model) {
|
|
2283
|
+
if (!model) {
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
if (candidates.some((candidate) => candidate.provider === provider && candidate.model === model)) {
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
candidates.push({ provider, model });
|
|
2290
|
+
}
|
|
2291
|
+
function splitProviderModel(model) {
|
|
2292
|
+
const slash = model.indexOf("/");
|
|
2293
|
+
if (slash <= 0 || slash === model.length - 1) {
|
|
2294
|
+
return null;
|
|
2295
|
+
}
|
|
2296
|
+
const provider = normalizeProvider(model.slice(0, slash));
|
|
2297
|
+
const bareModel = normalizeModel(model.slice(slash + 1));
|
|
2298
|
+
if (!provider || !bareModel) {
|
|
2299
|
+
return null;
|
|
2300
|
+
}
|
|
2301
|
+
return { provider, model: bareModel };
|
|
2302
|
+
}
|
|
2303
|
+
function normalizeProvider(provider) {
|
|
2304
|
+
if (!provider) {
|
|
2305
|
+
return void 0;
|
|
2306
|
+
}
|
|
2307
|
+
const normalized = provider.trim().toLowerCase();
|
|
2308
|
+
return PROVIDER_ALIASES[normalized] ?? normalized;
|
|
2309
|
+
}
|
|
2310
|
+
function normalizeModel(model) {
|
|
2311
|
+
const normalized = model?.trim().toLowerCase();
|
|
2312
|
+
return normalized || void 0;
|
|
2313
|
+
}
|
|
2314
|
+
function inferProvider(model) {
|
|
2315
|
+
if (model.startsWith("gpt-") || /^o\d/.test(model)) {
|
|
2316
|
+
return "openai";
|
|
2317
|
+
}
|
|
2318
|
+
if (model.startsWith("claude-")) {
|
|
2319
|
+
return "anthropic";
|
|
2320
|
+
}
|
|
2321
|
+
return void 0;
|
|
2322
|
+
}
|
|
2323
|
+
function getCatalogRates(catalog, provider, model) {
|
|
2324
|
+
const root = asRecord6(catalog);
|
|
2325
|
+
const providerEntry = asRecord6(root?.[provider]);
|
|
2326
|
+
const models = asRecord6(providerEntry?.["models"]);
|
|
2327
|
+
const modelEntry = asRecord6(models?.[model]);
|
|
2328
|
+
return toRateTable(modelEntry?.["cost"]);
|
|
2329
|
+
}
|
|
2330
|
+
function selectContextRates(rates, record) {
|
|
2331
|
+
const contextTokens = record.inputTokens + record.cacheReadTokens + record.cacheWriteTokens;
|
|
2332
|
+
let selected = rates;
|
|
2333
|
+
let selectedSize = 0;
|
|
2334
|
+
for (const tier of rates.tiers ?? []) {
|
|
2335
|
+
const tierRecord = asRecord6(tier);
|
|
2336
|
+
const tierInfo = asRecord6(tierRecord?.["tier"]);
|
|
2337
|
+
const size = typeof tierInfo?.["size"] === "number" ? tierInfo["size"] : 0;
|
|
2338
|
+
if (tierInfo?.["type"] !== "context" || contextTokens < size || size < selectedSize) {
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
const tierRates = toRateTable(tierRecord);
|
|
2342
|
+
if (tierRates) {
|
|
2343
|
+
selected = { ...rates, ...tierRates };
|
|
2344
|
+
selectedSize = size;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
if (selected === rates && rates.context_over_200k && contextTokens > 2e5) {
|
|
2348
|
+
selected = { ...rates, ...rates.context_over_200k };
|
|
2349
|
+
}
|
|
2350
|
+
return selected;
|
|
2351
|
+
}
|
|
2352
|
+
function toRateTable(value) {
|
|
2353
|
+
const record = asRecord6(value);
|
|
2354
|
+
if (!record) {
|
|
2355
|
+
return null;
|
|
2356
|
+
}
|
|
2357
|
+
const rates = {};
|
|
2358
|
+
const input = numberValue6(record["input"]);
|
|
2359
|
+
const output = numberValue6(record["output"]);
|
|
2360
|
+
const cacheRead = numberValue6(record["cache_read"]);
|
|
2361
|
+
const cacheWrite = numberValue6(record["cache_write"]);
|
|
2362
|
+
const contextOver200k = toRateTable(record["context_over_200k"]);
|
|
2363
|
+
if (input !== void 0) {
|
|
2364
|
+
rates.input = input;
|
|
2365
|
+
}
|
|
2366
|
+
if (output !== void 0) {
|
|
2367
|
+
rates.output = output;
|
|
2368
|
+
}
|
|
2369
|
+
if (cacheRead !== void 0) {
|
|
2370
|
+
rates.cache_read = cacheRead;
|
|
2371
|
+
}
|
|
2372
|
+
if (cacheWrite !== void 0) {
|
|
2373
|
+
rates.cache_write = cacheWrite;
|
|
2374
|
+
}
|
|
2375
|
+
if (Array.isArray(record["tiers"])) {
|
|
2376
|
+
rates.tiers = record["tiers"];
|
|
2377
|
+
}
|
|
2378
|
+
if (contextOver200k) {
|
|
2379
|
+
rates.context_over_200k = contextOver200k;
|
|
2380
|
+
}
|
|
2381
|
+
return rates.input !== void 0 || rates.output !== void 0 ? rates : null;
|
|
2382
|
+
}
|
|
2383
|
+
function numberValue6(value) {
|
|
2384
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2385
|
+
}
|
|
2386
|
+
function isModelsDevCatalog(value) {
|
|
2387
|
+
const catalog = asRecord6(value);
|
|
2388
|
+
const openai = asRecord6(catalog?.["openai"]);
|
|
2389
|
+
const anthropic = asRecord6(catalog?.["anthropic"]);
|
|
2390
|
+
return Boolean(asRecord6(openai?.["models"]) || asRecord6(anthropic?.["models"]));
|
|
2391
|
+
}
|
|
2392
|
+
function isFresh(fetchedAt, ttlMs) {
|
|
2393
|
+
const fetchedTime = new Date(fetchedAt).getTime();
|
|
2394
|
+
return Number.isFinite(fetchedTime) && Date.now() - fetchedTime <= ttlMs;
|
|
2395
|
+
}
|
|
2396
|
+
function mergePricingSource(left, right) {
|
|
2397
|
+
return left === right ? left : "mixed";
|
|
2398
|
+
}
|
|
2399
|
+
function asRecord6(value) {
|
|
2400
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2403
|
+
return value;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
790
2406
|
// src/commands/scan.ts
|
|
791
2407
|
var c = {
|
|
792
2408
|
reset: "\x1B[0m",
|
|
@@ -801,6 +2417,7 @@ var c = {
|
|
|
801
2417
|
white: "\x1B[37m",
|
|
802
2418
|
gray: "\x1B[90m"
|
|
803
2419
|
};
|
|
2420
|
+
var MAX_TERMINAL_MODELS = 10;
|
|
804
2421
|
var SPINNER_MESSAGES = [
|
|
805
2422
|
"Tallying the damage",
|
|
806
2423
|
"Reviewing your outbursts",
|
|
@@ -813,20 +2430,19 @@ var SPINNER_MESSAGES = [
|
|
|
813
2430
|
"Auditing your language",
|
|
814
2431
|
"Tabulating regrets"
|
|
815
2432
|
];
|
|
816
|
-
|
|
2433
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2434
|
+
function createSpinner(messages = SPINNER_MESSAGES) {
|
|
817
2435
|
let messageIdx = 0;
|
|
818
2436
|
let dotCount = 0;
|
|
819
2437
|
let timer = null;
|
|
820
2438
|
return {
|
|
821
2439
|
start() {
|
|
822
|
-
messageIdx = Math.floor(Math.random() *
|
|
2440
|
+
messageIdx = Math.floor(Math.random() * messages.length);
|
|
823
2441
|
timer = setInterval(() => {
|
|
824
2442
|
dotCount = (dotCount + 1) % 4;
|
|
825
|
-
const msg =
|
|
2443
|
+
const msg = messages[messageIdx % messages.length];
|
|
826
2444
|
const dots = ".".repeat(dotCount || 1);
|
|
827
|
-
process.stdout.write(
|
|
828
|
-
`\r ${c.dim}${msg}${dots}${c.reset} `
|
|
829
|
-
);
|
|
2445
|
+
process.stdout.write(`\r ${c.dim}${msg}${dots}${c.reset} `);
|
|
830
2446
|
}, 300);
|
|
831
2447
|
},
|
|
832
2448
|
update() {
|
|
@@ -850,24 +2466,106 @@ function parseArgs(args) {
|
|
|
850
2466
|
} else if (arg === "--since" || arg === "-s") {
|
|
851
2467
|
const val = args[++i];
|
|
852
2468
|
if (val) {
|
|
853
|
-
options
|
|
854
|
-
if (isNaN(options.since.getTime())) {
|
|
855
|
-
console.error(`invalid date: ${val}`);
|
|
856
|
-
process.exit(1);
|
|
857
|
-
}
|
|
2469
|
+
setAbsoluteSince(options, val);
|
|
858
2470
|
}
|
|
2471
|
+
} else if (arg === "--day" || arg === "--days") {
|
|
2472
|
+
const parsed = readOptionalDaysArg(args, i);
|
|
2473
|
+
setRelativeRange(options, parsed.days);
|
|
2474
|
+
if (parsed.consumed) {
|
|
2475
|
+
i++;
|
|
2476
|
+
}
|
|
2477
|
+
} else if (arg === "--week") {
|
|
2478
|
+
setRelativeRange(options, 7);
|
|
2479
|
+
} else if (arg === "--month") {
|
|
2480
|
+
setRelativeRange(options, 30);
|
|
859
2481
|
} else if (arg === "--help" || arg === "-h") {
|
|
860
2482
|
console.log(`devrage scan \u2014 scan sessions for profanity
|
|
861
2483
|
|
|
862
2484
|
Options:
|
|
863
|
-
--agent, -a <name> Scan only a specific agent (claude, codex, opencode, amp, cline, pi, zed)
|
|
2485
|
+
--agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, zed)
|
|
864
2486
|
--since, -s <date> Only scan messages after this date (ISO 8601)
|
|
2487
|
+
--day, --days [n] Only scan the last n days (default: 1)
|
|
2488
|
+
--week Only scan the last 7 days
|
|
2489
|
+
--month Only scan the last 30 days
|
|
2490
|
+
--help, -h Show this help`);
|
|
2491
|
+
process.exit(0);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return options;
|
|
2495
|
+
}
|
|
2496
|
+
function parseCostArgs(args) {
|
|
2497
|
+
const options = {};
|
|
2498
|
+
for (let i = 0; i < args.length; i++) {
|
|
2499
|
+
const arg = args[i];
|
|
2500
|
+
if (arg === "--agent" || arg === "-a") {
|
|
2501
|
+
options.agent = args[++i];
|
|
2502
|
+
} else if (arg === "--refresh-prices") {
|
|
2503
|
+
options.refreshPrices = true;
|
|
2504
|
+
} else if (arg === "--since" || arg === "-s") {
|
|
2505
|
+
const val = args[++i];
|
|
2506
|
+
if (val) {
|
|
2507
|
+
setAbsoluteSince(options, val);
|
|
2508
|
+
}
|
|
2509
|
+
} else if (arg === "--day" || arg === "--days") {
|
|
2510
|
+
const parsed = readOptionalDaysArg(args, i);
|
|
2511
|
+
setRelativeRange(options, parsed.days);
|
|
2512
|
+
if (parsed.consumed) {
|
|
2513
|
+
i++;
|
|
2514
|
+
}
|
|
2515
|
+
} else if (arg === "--week") {
|
|
2516
|
+
setRelativeRange(options, 7);
|
|
2517
|
+
} else if (arg === "--month") {
|
|
2518
|
+
setRelativeRange(options, 30);
|
|
2519
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
2520
|
+
console.log(`devrage cost \u2014 show API-equivalent coding agent cost
|
|
2521
|
+
|
|
2522
|
+
Usage:
|
|
2523
|
+
devrage cost [options]
|
|
2524
|
+
|
|
2525
|
+
Options:
|
|
2526
|
+
--agent, -a <name> Show only a specific agent (claude, codex, cursor, opencode, amp, pi)
|
|
2527
|
+
--refresh-prices Refresh models.dev pricing before estimating cost
|
|
2528
|
+
--since, -s <date> Only include usage after this date (ISO 8601)
|
|
2529
|
+
--day, --days [n] Only include the last n days (default: 1)
|
|
2530
|
+
--week Only include the last 7 days
|
|
2531
|
+
--month Only include the last 30 days
|
|
865
2532
|
--help, -h Show this help`);
|
|
866
2533
|
process.exit(0);
|
|
867
2534
|
}
|
|
868
2535
|
}
|
|
869
2536
|
return options;
|
|
870
2537
|
}
|
|
2538
|
+
function setAbsoluteSince(options, value) {
|
|
2539
|
+
options.since = parseDateArg(value);
|
|
2540
|
+
options.rangeLabel = void 0;
|
|
2541
|
+
}
|
|
2542
|
+
function setRelativeRange(options, days) {
|
|
2543
|
+
options.since = new Date(Date.now() - days * DAY_MS);
|
|
2544
|
+
options.rangeLabel = `last ${days} ${days === 1 ? "day" : "days"}`;
|
|
2545
|
+
}
|
|
2546
|
+
function readOptionalDaysArg(args, index) {
|
|
2547
|
+
const value = args[index + 1];
|
|
2548
|
+
if (!value || value.startsWith("-") && !/^-\d+$/.test(value)) {
|
|
2549
|
+
return { days: 1, consumed: false };
|
|
2550
|
+
}
|
|
2551
|
+
return { days: parseDaysArg(value), consumed: true };
|
|
2552
|
+
}
|
|
2553
|
+
function parseDaysArg(value) {
|
|
2554
|
+
const days = Number(value);
|
|
2555
|
+
if (!Number.isInteger(days) || days < 1) {
|
|
2556
|
+
console.error(`invalid days: ${value ?? ""}`);
|
|
2557
|
+
process.exit(1);
|
|
2558
|
+
}
|
|
2559
|
+
return days;
|
|
2560
|
+
}
|
|
2561
|
+
function parseDateArg(value) {
|
|
2562
|
+
const date = new Date(value);
|
|
2563
|
+
if (isNaN(date.getTime())) {
|
|
2564
|
+
console.error(`invalid date: ${value}`);
|
|
2565
|
+
process.exit(1);
|
|
2566
|
+
}
|
|
2567
|
+
return date;
|
|
2568
|
+
}
|
|
871
2569
|
async function scan(args) {
|
|
872
2570
|
const options = parseArgs(args);
|
|
873
2571
|
const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
|
|
@@ -901,27 +2599,24 @@ async function scan(args) {
|
|
|
901
2599
|
}
|
|
902
2600
|
}
|
|
903
2601
|
spinner.stop();
|
|
904
|
-
console.log("");
|
|
905
|
-
console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}report${c.reset}`);
|
|
906
|
-
console.log(` ${c.dim}${"\u2500".repeat(30)}${c.reset}`);
|
|
907
|
-
console.log("");
|
|
908
|
-
console.log(` ${c.dim}messages scanned${c.reset} ${c.bold}${totalMessages}${c.reset}`);
|
|
909
|
-
console.log(` ${c.dim}total swears${c.reset} ${c.bold}${c.red}${totalSwears}${c.reset}`);
|
|
910
2602
|
const activeAgents = Object.entries(perAgent);
|
|
2603
|
+
console.log("");
|
|
2604
|
+
printReportHeader(options);
|
|
2605
|
+
printBasicOverview(totalMessages, totalSwears);
|
|
911
2606
|
if (activeAgents.length > 1) {
|
|
912
2607
|
console.log("");
|
|
913
|
-
console.log(` ${
|
|
2608
|
+
console.log(` ${sectionTitle("agent language")}`);
|
|
914
2609
|
for (const [name, stats] of activeAgents) {
|
|
915
2610
|
const rate = (stats.swears / stats.messages * 100).toFixed(1);
|
|
916
2611
|
console.log(
|
|
917
|
-
` ${
|
|
2612
|
+
` ${colorText(name.padEnd(10), agentColor(name))} ${c.bold}${String(stats.swears).padStart(4)}${c.reset} ${c.dim}in ${stats.messages} messages (${rate}%)${c.reset}`
|
|
918
2613
|
);
|
|
919
2614
|
}
|
|
920
2615
|
}
|
|
921
2616
|
if (totalSwears > 0) {
|
|
922
2617
|
const sorted = Object.entries(groupTally).sort(([, a], [, b]) => b - a);
|
|
923
2618
|
console.log("");
|
|
924
|
-
console.log(` ${
|
|
2619
|
+
console.log(` ${sectionTitle("top words")}`);
|
|
925
2620
|
for (const [group, count] of sorted.slice(0, 10)) {
|
|
926
2621
|
const variants = variantTally[group] ?? {};
|
|
927
2622
|
const variantList = Object.entries(variants).sort(([, a], [, b]) => b - a).filter(([v]) => v !== group).slice(0, 15).map(([v, cnt]) => `${c.dim}${v}${c.reset} ${cnt}`).join(`${c.dim},${c.reset} `);
|
|
@@ -937,11 +2632,521 @@ async function scan(args) {
|
|
|
937
2632
|
console.log("");
|
|
938
2633
|
}
|
|
939
2634
|
}
|
|
2635
|
+
async function cost(args) {
|
|
2636
|
+
const options = parseCostArgs(args);
|
|
2637
|
+
const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
|
|
2638
|
+
const costByAgent = {};
|
|
2639
|
+
const pricing = await loadPricingCatalog({ refresh: options.refreshPrices });
|
|
2640
|
+
for (const adapter of adapters) {
|
|
2641
|
+
if (!adapter.usage) {
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2644
|
+
const summary = await summarizeUsage(adapter.usage({ since: options.since }), pricing);
|
|
2645
|
+
if (summary.requests > 0) {
|
|
2646
|
+
costByAgent[adapter.name] = summary;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
const totals = getCostTotals(costByAgent);
|
|
2650
|
+
console.log("");
|
|
2651
|
+
if (totals.entries.length === 0) {
|
|
2652
|
+
printCostCommandUnavailable(options);
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
const reportUrl = await writeCostHtmlReport(totals, options);
|
|
2656
|
+
printCostCommand(totals, options, reportUrl);
|
|
2657
|
+
}
|
|
2658
|
+
function printCostCommand(totals, options, reportUrl) {
|
|
2659
|
+
const modelTotals = aggregateModelCosts(totals.entries);
|
|
2660
|
+
printCompactHeader(options);
|
|
2661
|
+
printCompactTotal(totals);
|
|
2662
|
+
printCompactAgents(totals.entries);
|
|
2663
|
+
printCompactModels(modelTotals, totals.totalCost);
|
|
2664
|
+
console.log("");
|
|
2665
|
+
console.log(` ${c.dim}Report:${c.reset} ${reportUrl}`);
|
|
2666
|
+
console.log("");
|
|
2667
|
+
}
|
|
2668
|
+
function printCompactHeader(options) {
|
|
2669
|
+
const filters = [
|
|
2670
|
+
options.agent,
|
|
2671
|
+
options.rangeLabel ?? (options.since ? `since ${formatDate(options.since)}` : null)
|
|
2672
|
+
].filter(Boolean);
|
|
2673
|
+
const suffix = filters.length > 0 ? ` ${c.dim}${filters.join(" \xB7 ")}${c.reset}` : "";
|
|
2674
|
+
console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}cost${c.reset}${suffix}`);
|
|
2675
|
+
console.log("");
|
|
2676
|
+
}
|
|
2677
|
+
function compactMeta(totals) {
|
|
2678
|
+
const parts = [formatRequests(totals.pricedRequests)];
|
|
2679
|
+
if (totals.unpricedRequests > 0) {
|
|
2680
|
+
parts.push(`${formatNumber(totals.unpricedRequests)} unpriced`);
|
|
2681
|
+
}
|
|
2682
|
+
return `${c.dim}${parts.join(" \xB7 ")}${c.reset}`;
|
|
2683
|
+
}
|
|
2684
|
+
function printCompactTotal(totals) {
|
|
2685
|
+
console.log(` ${c.bold}total${c.reset}`);
|
|
2686
|
+
console.log(
|
|
2687
|
+
` ${c.bold}${c.green}${formatCurrency(totals.totalCost)}${c.reset} ${compactMeta(totals)}`
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
function printCompactModels(models, totalCost) {
|
|
2691
|
+
if (models.length === 0) {
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
const visibleModels = models.slice(0, MAX_TERMINAL_MODELS);
|
|
2695
|
+
const maxCost = visibleModels[0]?.estimatedCost ?? 0;
|
|
2696
|
+
console.log("");
|
|
2697
|
+
console.log(` ${c.bold}models${c.reset}`);
|
|
2698
|
+
for (const model of visibleModels) {
|
|
2699
|
+
const share = totalCost > 0 ? model.estimatedCost / totalCost : 0;
|
|
2700
|
+
const color = modelColor(model);
|
|
2701
|
+
console.log(
|
|
2702
|
+
` ${colorText(clip(model.model, 27).padEnd(27), color)} ${formatCurrency(model.estimatedCost).padStart(9)} ${c.dim}${formatPercent(share).padStart(6)}${c.reset} ${renderBar(model.estimatedCost, maxCost, 16, color)}`
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
function printCompactAgents(entries) {
|
|
2707
|
+
console.log("");
|
|
2708
|
+
console.log(` ${c.bold}agents${c.reset}`);
|
|
2709
|
+
for (const [name, stats] of entries.sort(
|
|
2710
|
+
([, left], [, right]) => right.estimatedCost - left.estimatedCost
|
|
2711
|
+
)) {
|
|
2712
|
+
const color = agentColor(name);
|
|
2713
|
+
console.log(
|
|
2714
|
+
` ${colorText(name.padEnd(10), color)} ${colorText(formatCurrency(stats.estimatedCost).padStart(9), color)} ${c.dim}${formatRequests(stats.requests).padStart(12)}${c.reset}`
|
|
2715
|
+
);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
function printCostCommandUnavailable(options) {
|
|
2719
|
+
printCompactHeader(options);
|
|
2720
|
+
console.log(` ${c.gray}no local usage found${c.reset}`);
|
|
2721
|
+
console.log("");
|
|
2722
|
+
}
|
|
2723
|
+
async function writeCostHtmlReport(totals, options) {
|
|
2724
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2725
|
+
const reportPath = join10(
|
|
2726
|
+
dirname2(getPricingCachePath()),
|
|
2727
|
+
`cost-report-${safeTimestamp(generatedAt)}.html`
|
|
2728
|
+
);
|
|
2729
|
+
await mkdir2(dirname2(reportPath), { recursive: true });
|
|
2730
|
+
await writeFile2(
|
|
2731
|
+
reportPath,
|
|
2732
|
+
renderCostHtmlReport(costReportData(totals, options, generatedAt)),
|
|
2733
|
+
"utf-8"
|
|
2734
|
+
);
|
|
2735
|
+
return pathToFileURL(reportPath).href;
|
|
2736
|
+
}
|
|
2737
|
+
function safeTimestamp(value) {
|
|
2738
|
+
return value.replace(/[:.]/g, "-");
|
|
2739
|
+
}
|
|
2740
|
+
function costReportData(totals, options, generatedAt) {
|
|
2741
|
+
return {
|
|
2742
|
+
generatedAt,
|
|
2743
|
+
scope: options.rangeLabel ?? (options.since ? `since ${formatDate(options.since)}` : "all local history"),
|
|
2744
|
+
totalCost: totals.totalCost,
|
|
2745
|
+
pricedRequests: totals.pricedRequests,
|
|
2746
|
+
unpricedRequests: totals.unpricedRequests,
|
|
2747
|
+
agents: totals.entries.map(([name, summary]) => ({
|
|
2748
|
+
name,
|
|
2749
|
+
estimatedCost: summary.estimatedCost,
|
|
2750
|
+
requests: summary.requests - summary.unpricedRequests,
|
|
2751
|
+
models: summary.models.map(costReportModel),
|
|
2752
|
+
days: summary.days.map((day) => ({
|
|
2753
|
+
day: day.day,
|
|
2754
|
+
estimatedCost: day.estimatedCost,
|
|
2755
|
+
requests: day.requests - day.unpricedRequests,
|
|
2756
|
+
models: day.models.map(costReportModel)
|
|
2757
|
+
}))
|
|
2758
|
+
})).sort((left, right) => right.estimatedCost - left.estimatedCost)
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
function costReportModel(model) {
|
|
2762
|
+
return {
|
|
2763
|
+
model: model.model,
|
|
2764
|
+
provider: model.provider,
|
|
2765
|
+
estimatedCost: model.estimatedCost,
|
|
2766
|
+
requests: model.requests - model.unpricedRequests,
|
|
2767
|
+
inputTokens: model.inputTokens,
|
|
2768
|
+
outputTokens: model.outputTokens,
|
|
2769
|
+
reasoningTokens: model.reasoningTokens,
|
|
2770
|
+
cacheReadTokens: model.cacheReadTokens,
|
|
2771
|
+
cacheWriteTokens: model.cacheWriteTokens
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
function renderCostHtmlReport(data) {
|
|
2775
|
+
return `<!doctype html>
|
|
2776
|
+
<html lang="en">
|
|
2777
|
+
<head>
|
|
2778
|
+
<meta charset="utf-8" />
|
|
2779
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2780
|
+
<title>devrage cost report</title>
|
|
2781
|
+
<style>
|
|
2782
|
+
:root {
|
|
2783
|
+
color-scheme: dark;
|
|
2784
|
+
--bg: #0f1117;
|
|
2785
|
+
--panel: #151923;
|
|
2786
|
+
--panel-2: #10141c;
|
|
2787
|
+
--border: #283040;
|
|
2788
|
+
--text: #edf1f7;
|
|
2789
|
+
--muted: #99a3b5;
|
|
2790
|
+
--faint: #677184;
|
|
2791
|
+
--green: #55c98f;
|
|
2792
|
+
--purple: #b18cff;
|
|
2793
|
+
--blue: #75a7ff;
|
|
2794
|
+
--yellow: #e5b75f;
|
|
2795
|
+
--cyan: #62c7df;
|
|
2796
|
+
}
|
|
2797
|
+
* { box-sizing: border-box; }
|
|
2798
|
+
body {
|
|
2799
|
+
margin: 0;
|
|
2800
|
+
background: var(--bg);
|
|
2801
|
+
color: var(--text);
|
|
2802
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2803
|
+
line-height: 1.45;
|
|
2804
|
+
}
|
|
2805
|
+
main { width: min(1180px, calc(100vw - 32px)); margin: 0 auto; padding: 28px 0 48px; }
|
|
2806
|
+
header { display: flex; justify-content: space-between; gap: 20px; align-items: end; margin-bottom: 22px; }
|
|
2807
|
+
h1 { margin: 0; font-size: 22px; letter-spacing: -0.02em; }
|
|
2808
|
+
.scope { color: var(--muted); font-size: 13px; margin-top: 4px; }
|
|
2809
|
+
.generated { color: var(--faint); font-size: 12px; text-align: right; }
|
|
2810
|
+
.summary { display: grid; grid-template-columns: 1.4fr repeat(3, 1fr); gap: 12px; margin-bottom: 18px; }
|
|
2811
|
+
.card, .panel { border: 1px solid var(--border); background: var(--panel); }
|
|
2812
|
+
.card { padding: 16px; min-height: 92px; }
|
|
2813
|
+
.label { color: var(--muted); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; }
|
|
2814
|
+
.value { margin-top: 8px; font-size: 26px; font-weight: 800; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
|
|
2815
|
+
.primary .value { color: var(--green); font-size: 42px; }
|
|
2816
|
+
.controls { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 18px 0; }
|
|
2817
|
+
label { display: grid; gap: 6px; color: var(--muted); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; }
|
|
2818
|
+
select { width: 100%; border: 1px solid var(--border); background: var(--panel-2); color: var(--text); padding: 10px 12px; font: inherit; border-radius: 0; }
|
|
2819
|
+
.grid { display: grid; grid-template-columns: 1fr; gap: 14px; align-items: start; }
|
|
2820
|
+
.panel { min-width: 0; }
|
|
2821
|
+
.panel h2 { margin: 0; padding: 13px 14px; border-bottom: 1px solid var(--border); font-size: 13px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); }
|
|
2822
|
+
.panel-body { padding: 14px; }
|
|
2823
|
+
table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
|
|
2824
|
+
th, td { padding: 9px 8px; border-bottom: 1px solid #202838; text-align: left; white-space: nowrap; }
|
|
2825
|
+
th { color: var(--faint); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; }
|
|
2826
|
+
td:not(:first-child), th:not(:first-child) { text-align: right; }
|
|
2827
|
+
tr:last-child td { border-bottom: 0; }
|
|
2828
|
+
.name { color: var(--text); font-weight: 650; }
|
|
2829
|
+
.muted { color: var(--muted); }
|
|
2830
|
+
.chart-wrap { min-width: 0; }
|
|
2831
|
+
.chart { width: 100%; min-width: 0; height: 190px; display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: clamp(1px, 0.55vw, 8px); align-items: end; overflow: hidden; }
|
|
2832
|
+
.bar-column { display: flex; align-items: end; min-width: 0; height: 190px; overflow: hidden; font-variant-numeric: tabular-nums; }
|
|
2833
|
+
.chart-empty { align-self: center; }
|
|
2834
|
+
.axis { position: relative; height: 24px; margin-top: 8px; border-top: 1px solid #202838; color: var(--muted); font-size: 10px; font-variant-numeric: tabular-nums; overflow: hidden; }
|
|
2835
|
+
.axis-tick { position: absolute; top: 6px; transform: translateX(-50%); white-space: nowrap; }
|
|
2836
|
+
.axis-tick.edge-start { transform: translateX(0); }
|
|
2837
|
+
.axis-tick.edge-end { transform: translateX(-100%); }
|
|
2838
|
+
.column-track { width: 100%; min-width: 0; height: 190px; display: flex; align-items: end; background: #202838; overflow: hidden; }
|
|
2839
|
+
.column-fill { width: 100%; min-height: 2px; background: var(--cyan); }
|
|
2840
|
+
.legend { display: flex; flex-wrap: wrap; gap: 10px 14px; color: var(--muted); font-size: 12px; margin-top: 12px; }
|
|
2841
|
+
.dot { display: inline-block; width: 9px; height: 9px; margin-right: 5px; background: var(--cyan); }
|
|
2842
|
+
.tooltip { position: fixed; z-index: 20; display: none; pointer-events: none; border: 1px solid var(--border); background: #0b0e14; color: var(--text); padding: 7px 9px; font-size: 12px; font-variant-numeric: tabular-nums; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35); }
|
|
2843
|
+
.tooltip .sub { color: var(--muted); margin-top: 2px; }
|
|
2844
|
+
.green { color: var(--green); } .purple { color: var(--purple); } .blue { color: var(--blue); } .yellow { color: var(--yellow); } .cyan { color: var(--cyan); }
|
|
2845
|
+
.bg-green { background: var(--green); } .bg-purple { background: var(--purple); } .bg-blue { background: var(--blue); } .bg-yellow { background: var(--yellow); } .bg-cyan { background: var(--cyan); }
|
|
2846
|
+
@media (max-width: 900px) { header, .grid { display: block; } .summary, .controls { grid-template-columns: 1fr; } .panel { margin-top: 14px; } .generated { text-align: left; margin-top: 8px; } }
|
|
2847
|
+
</style>
|
|
2848
|
+
</head>
|
|
2849
|
+
<body>
|
|
2850
|
+
<main>
|
|
2851
|
+
<header>
|
|
2852
|
+
<div>
|
|
2853
|
+
<h1>devrage cost report</h1>
|
|
2854
|
+
<div class="scope" id="scope"></div>
|
|
2855
|
+
</div>
|
|
2856
|
+
<div class="generated" id="generated"></div>
|
|
2857
|
+
</header>
|
|
2858
|
+
<section class="summary">
|
|
2859
|
+
<div class="card primary"><div class="label">total</div><div class="value" id="totalCost"></div></div>
|
|
2860
|
+
<div class="card"><div class="label">requests</div><div class="value" id="requestCount"></div></div>
|
|
2861
|
+
<div class="card"><div class="label">models</div><div class="value" id="modelCount"></div></div>
|
|
2862
|
+
<div class="card"><div class="label">agents</div><div class="value" id="agentCount"></div></div>
|
|
2863
|
+
</section>
|
|
2864
|
+
<section class="controls">
|
|
2865
|
+
<label>Agent<select id="agentFilter"></select></label>
|
|
2866
|
+
<label>Model<select id="modelFilter"></select></label>
|
|
2867
|
+
<label>Range<select id="rangeFilter"><option value="all">All included data</option><option value="7">Last 7 days</option><option value="30">Last 30 days</option><option value="90">Last 90 days</option></select></label>
|
|
2868
|
+
</section>
|
|
2869
|
+
<section class="grid">
|
|
2870
|
+
<div class="panel"><h2>Agents</h2><div class="panel-body"><table><thead><tr><th>Agent</th><th>Cost</th><th>Reqs</th></tr></thead><tbody id="agentRows"></tbody></table></div></div>
|
|
2871
|
+
<div class="panel"><h2>Models</h2><div class="panel-body"><table><thead><tr><th>Model</th><th>Cost</th><th>Share</th><th>Reqs</th><th>Input</th><th>Output</th><th>Cache</th></tr></thead><tbody id="modelRows"></tbody></table><div class="legend"><span><i class="dot bg-purple"></i>Claude/Anthropic</span><span><i class="dot bg-green"></i>OpenAI</span><span><i class="dot bg-blue"></i>Google</span><span><i class="dot bg-yellow"></i>Kimi/GLM</span></div></div></div>
|
|
2872
|
+
<div class="panel"><h2>Daily</h2><div class="panel-body"><div class="chart-wrap"><div class="chart" id="dailyChart"></div><div class="axis" id="dailyAxis"></div></div></div></div>
|
|
2873
|
+
</section>
|
|
2874
|
+
<div class="tooltip" id="tooltip"></div>
|
|
2875
|
+
</main>
|
|
2876
|
+
<script>
|
|
2877
|
+
const DATA = ${jsonForScript(data)};
|
|
2878
|
+
const $ = (id) => document.getElementById(id);
|
|
2879
|
+
const money = (value) => '$' + (Math.floor(Math.max(0, value) * 100 + 1e-9) / 100).toFixed(2);
|
|
2880
|
+
const number = (value) => Math.round(value).toLocaleString('en-US');
|
|
2881
|
+
const pct = (value) => (value * 100).toFixed(1) + '%';
|
|
2882
|
+
const esc = (value) => String(value).replace(/[&<>"']/g, (ch) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
|
2883
|
+
const modelClass = (name, provider) => {
|
|
2884
|
+
const model = String(name || '').toLowerCase();
|
|
2885
|
+
const p = String(provider || '').toLowerCase();
|
|
2886
|
+
if (p === 'anthropic' || model.startsWith('claude-')) return 'purple';
|
|
2887
|
+
if (p === 'openai' || model.startsWith('gpt-') || /^o\\d/.test(model)) return 'green';
|
|
2888
|
+
if (p === 'google' || model.startsWith('gemini-')) return 'blue';
|
|
2889
|
+
if (model.startsWith('kimi-') || model.startsWith('glm-')) return 'yellow';
|
|
2890
|
+
return 'cyan';
|
|
2891
|
+
};
|
|
2892
|
+
const shortDate = (day) => new Date(day + 'T00:00:00.000Z').toLocaleDateString('en-US', {month:'short', day:'numeric', timeZone:'UTC'});
|
|
2893
|
+
function dailyTicks(days) {
|
|
2894
|
+
const count = days.length;
|
|
2895
|
+
if (count === 0) return [];
|
|
2896
|
+
const maxTicks = count <= 7 ? count : count <= 31 ? 6 : count <= 90 ? 7 : 9;
|
|
2897
|
+
if (maxTicks <= 1) return [{index: 0, day: days[0].day}];
|
|
2898
|
+
const ticks = [];
|
|
2899
|
+
const seen = new Set();
|
|
2900
|
+
for (let tick = 0; tick < maxTicks; tick++) {
|
|
2901
|
+
const index = Math.round(((count - 1) * tick) / (maxTicks - 1));
|
|
2902
|
+
if (seen.has(index)) continue;
|
|
2903
|
+
seen.add(index);
|
|
2904
|
+
ticks.push({index, day: days[index].day});
|
|
2905
|
+
}
|
|
2906
|
+
return ticks;
|
|
2907
|
+
}
|
|
2908
|
+
function addModel(map, incoming) {
|
|
2909
|
+
const key = incoming.model;
|
|
2910
|
+
const row = map.get(key) || {model: incoming.model, provider: incoming.provider, estimatedCost: 0, requests: 0, inputTokens: 0, outputTokens: 0, reasoningTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0};
|
|
2911
|
+
row.estimatedCost += incoming.estimatedCost; row.requests += incoming.requests;
|
|
2912
|
+
row.inputTokens += incoming.inputTokens; row.outputTokens += incoming.outputTokens; row.reasoningTokens += incoming.reasoningTokens;
|
|
2913
|
+
row.cacheReadTokens += incoming.cacheReadTokens; row.cacheWriteTokens += incoming.cacheWriteTokens;
|
|
2914
|
+
map.set(key, row);
|
|
2915
|
+
}
|
|
2916
|
+
function filteredAgents() {
|
|
2917
|
+
const selected = $('agentFilter').value;
|
|
2918
|
+
return DATA.agents.filter((agent) => selected === 'all' || agent.name === selected);
|
|
2919
|
+
}
|
|
2920
|
+
function dayAllowed(day) {
|
|
2921
|
+
const range = $('rangeFilter').value;
|
|
2922
|
+
if (range === 'all') return true;
|
|
2923
|
+
const cutoff = new Date(DATA.generatedAt).getTime() - Number(range) * 24 * 60 * 60 * 1000;
|
|
2924
|
+
return new Date(day + 'T23:59:59.999Z').getTime() >= cutoff;
|
|
2925
|
+
}
|
|
2926
|
+
function selectedModelRows(models) {
|
|
2927
|
+
const selected = $('modelFilter').value;
|
|
2928
|
+
return models.filter((model) => selected === 'all' || model.model === selected);
|
|
2929
|
+
}
|
|
2930
|
+
function compute() {
|
|
2931
|
+
const agents = filteredAgents();
|
|
2932
|
+
const modelMap = new Map();
|
|
2933
|
+
const dayMap = new Map();
|
|
2934
|
+
let totalCost = 0, requests = 0;
|
|
2935
|
+
for (const agent of agents) {
|
|
2936
|
+
const models = selectedModelRows(agent.models);
|
|
2937
|
+
for (const model of models) { addModel(modelMap, model); totalCost += model.estimatedCost; requests += model.requests; }
|
|
2938
|
+
for (const day of agent.days) {
|
|
2939
|
+
if (!dayAllowed(day.day)) continue;
|
|
2940
|
+
const dayModels = selectedModelRows(day.models);
|
|
2941
|
+
const cost = dayModels.reduce((sum, model) => sum + model.estimatedCost, 0);
|
|
2942
|
+
const reqs = dayModels.reduce((sum, model) => sum + model.requests, 0);
|
|
2943
|
+
if (cost <= 0 && reqs <= 0) continue;
|
|
2944
|
+
const row = dayMap.get(day.day) || {day: day.day, estimatedCost: 0, requests: 0};
|
|
2945
|
+
row.estimatedCost += cost; row.requests += reqs; dayMap.set(day.day, row);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
return {agents, models: Array.from(modelMap.values()).sort((a,b) => b.estimatedCost - a.estimatedCost), days: Array.from(dayMap.values()).sort((a,b) => a.day.localeCompare(b.day)), totalCost, requests};
|
|
2949
|
+
}
|
|
2950
|
+
function render() {
|
|
2951
|
+
const view = compute();
|
|
2952
|
+
$('totalCost').textContent = money(view.totalCost);
|
|
2953
|
+
$('requestCount').textContent = number(view.requests);
|
|
2954
|
+
$('modelCount').textContent = number(view.models.length);
|
|
2955
|
+
$('agentCount').textContent = number(view.agents.length);
|
|
2956
|
+
const agentRows = view.agents.map((agent) => '<tr><td class="name">' + esc(agent.name) + '</td><td>' + money(agent.estimatedCost) + '</td><td>' + number(agent.requests) + '</td></tr>').join('');
|
|
2957
|
+
$('agentRows').innerHTML = agentRows || '<tr><td colspan="3" class="muted">No data</td></tr>';
|
|
2958
|
+
const maxModel = Math.max(1, ...view.models.map((model) => model.estimatedCost));
|
|
2959
|
+
$('modelRows').innerHTML = view.models.map((model) => {
|
|
2960
|
+
const klass = modelClass(model.model, model.provider);
|
|
2961
|
+
const cache = model.cacheReadTokens + model.cacheWriteTokens;
|
|
2962
|
+
return '<tr><td class="name ' + klass + '">' + esc(model.model) + '</td><td>' + money(model.estimatedCost) + '</td><td>' + pct(view.totalCost > 0 ? model.estimatedCost / view.totalCost : 0) + '</td><td>' + number(model.requests) + '</td><td>' + number(model.inputTokens) + '</td><td>' + number(model.outputTokens + model.reasoningTokens) + '</td><td>' + number(cache) + '</td></tr>';
|
|
2963
|
+
}).join('') || '<tr><td colspan="7" class="muted">No data</td></tr>';
|
|
2964
|
+
const maxDay = Math.max(1, ...view.days.map((day) => day.estimatedCost));
|
|
2965
|
+
$('dailyChart').innerHTML = view.days.length ? view.days.map((day) => {
|
|
2966
|
+
const tooltip = esc(shortDate(day.day) + '|' + money(day.estimatedCost) + '|' + number(day.requests) + ' reqs');
|
|
2967
|
+
return '<div class="bar-column" data-tooltip="' + tooltip + '"><div class="column-track"><div class="column-fill" style="height:' + Math.max(1, (day.estimatedCost / maxDay) * 100) + '%"></div></div></div>';
|
|
2968
|
+
}).join('') : '<div class="muted chart-empty">No data</div>';
|
|
2969
|
+
$('dailyAxis').innerHTML = dailyTicks(view.days).map((tick) => {
|
|
2970
|
+
const left = view.days.length === 1 ? 50 : ((tick.index + 0.5) / view.days.length) * 100;
|
|
2971
|
+
const edge = view.days.length === 1 ? '' : tick.index === 0 ? ' edge-start' : tick.index === view.days.length - 1 ? ' edge-end' : '';
|
|
2972
|
+
return '<span class="axis-tick' + edge + '" style="left:' + left.toFixed(4) + '%">' + esc(shortDate(tick.day)) + '</span>';
|
|
2973
|
+
}).join('');
|
|
2974
|
+
}
|
|
2975
|
+
function showTooltip(event) {
|
|
2976
|
+
const target = event.target.closest('[data-tooltip]');
|
|
2977
|
+
const tooltip = $('tooltip');
|
|
2978
|
+
if (!target) { tooltip.style.display = 'none'; return; }
|
|
2979
|
+
const [date, amount, requests] = target.dataset.tooltip.split('|');
|
|
2980
|
+
tooltip.innerHTML = '<div>' + esc(date) + '</div><div class="sub">' + esc(amount) + ' \xB7 ' + esc(requests) + '</div>';
|
|
2981
|
+
tooltip.style.display = 'block';
|
|
2982
|
+
moveTooltip(event);
|
|
2983
|
+
}
|
|
2984
|
+
function moveTooltip(event) {
|
|
2985
|
+
const tooltip = $('tooltip');
|
|
2986
|
+
if (tooltip.style.display !== 'block') return;
|
|
2987
|
+
const offset = 12;
|
|
2988
|
+
const nextLeft = Math.min(window.innerWidth - tooltip.offsetWidth - 8, event.clientX + offset);
|
|
2989
|
+
const nextTop = Math.min(window.innerHeight - tooltip.offsetHeight - 8, event.clientY + offset);
|
|
2990
|
+
tooltip.style.left = Math.max(8, nextLeft) + 'px';
|
|
2991
|
+
tooltip.style.top = Math.max(8, nextTop) + 'px';
|
|
2992
|
+
}
|
|
2993
|
+
function init() {
|
|
2994
|
+
$('scope').textContent = DATA.scope;
|
|
2995
|
+
$('generated').textContent = 'Generated ' + new Date(DATA.generatedAt).toLocaleString();
|
|
2996
|
+
$('agentFilter').innerHTML = '<option value="all">All agents</option>' + DATA.agents.map((agent) => '<option value="' + esc(agent.name) + '">' + esc(agent.name) + '</option>').join('');
|
|
2997
|
+
const models = Array.from(new Set(DATA.agents.flatMap((agent) => agent.models.map((model) => model.model)))).sort();
|
|
2998
|
+
$('modelFilter').innerHTML = '<option value="all">All models</option>' + models.map((model) => '<option value="' + esc(model) + '">' + esc(model) + '</option>').join('');
|
|
2999
|
+
['agentFilter', 'modelFilter', 'rangeFilter'].forEach((id) => $(id).addEventListener('change', render));
|
|
3000
|
+
$('dailyChart').addEventListener('mouseover', showTooltip);
|
|
3001
|
+
$('dailyChart').addEventListener('mousemove', moveTooltip);
|
|
3002
|
+
$('dailyChart').addEventListener('mouseleave', () => { $('tooltip').style.display = 'none'; });
|
|
3003
|
+
render();
|
|
3004
|
+
}
|
|
3005
|
+
init();
|
|
3006
|
+
</script>
|
|
3007
|
+
</body>
|
|
3008
|
+
</html>`;
|
|
3009
|
+
}
|
|
3010
|
+
function jsonForScript(value) {
|
|
3011
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
3012
|
+
}
|
|
3013
|
+
function printReportHeader(options) {
|
|
3014
|
+
const scope = options.rangeLabel ?? (options.since ? `since ${formatDate(options.since)}` : "all local history");
|
|
3015
|
+
const agent = options.agent ? ` \xB7 ${options.agent}` : "";
|
|
3016
|
+
console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}report${c.reset}`);
|
|
3017
|
+
console.log(` ${c.dim}${scope}${agent}${c.reset}`);
|
|
3018
|
+
console.log(` ${c.dim}${"\u2500".repeat(54)}${c.reset}`);
|
|
3019
|
+
}
|
|
3020
|
+
function printBasicOverview(totalMessages, totalSwears) {
|
|
3021
|
+
console.log(
|
|
3022
|
+
` ${c.dim}messages scanned${c.reset} ${c.bold}${formatNumber(totalMessages)}${c.reset}`
|
|
3023
|
+
);
|
|
3024
|
+
console.log(
|
|
3025
|
+
` ${c.dim}total swears${c.reset} ${c.bold}${c.red}${formatNumber(totalSwears)}${c.reset}`
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
function getCostTotals(costByAgent) {
|
|
3029
|
+
const entries = Object.entries(costByAgent);
|
|
3030
|
+
const totalCost = entries.reduce((sum, [, stats]) => sum + stats.estimatedCost, 0);
|
|
3031
|
+
const totalRequests = entries.reduce((sum, [, stats]) => sum + stats.requests, 0);
|
|
3032
|
+
const unpricedRequests = entries.reduce((sum, [, stats]) => sum + stats.unpricedRequests, 0);
|
|
3033
|
+
return {
|
|
3034
|
+
entries,
|
|
3035
|
+
totalCost,
|
|
3036
|
+
totalRequests,
|
|
3037
|
+
pricedRequests: totalRequests - unpricedRequests,
|
|
3038
|
+
unpricedRequests
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
function aggregateModelCosts(entries) {
|
|
3042
|
+
const models = /* @__PURE__ */ new Map();
|
|
3043
|
+
for (const [, stats] of entries) {
|
|
3044
|
+
for (const model of stats.models) {
|
|
3045
|
+
mergeModelSummary(models, model);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
return sortedCostModels(models);
|
|
3049
|
+
}
|
|
3050
|
+
function mergeModelSummary(models, incoming) {
|
|
3051
|
+
const key = incoming.model;
|
|
3052
|
+
let model = models.get(key);
|
|
3053
|
+
if (!model) {
|
|
3054
|
+
models.set(key, { ...incoming });
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
model.requests += incoming.requests;
|
|
3058
|
+
model.estimatedCost += incoming.estimatedCost;
|
|
3059
|
+
model.billedCost += incoming.billedCost;
|
|
3060
|
+
model.pricingSource = mergeDisplayPricingSource(model.pricingSource, incoming.pricingSource);
|
|
3061
|
+
model.unpricedRequests += incoming.unpricedRequests;
|
|
3062
|
+
model.inputTokens += incoming.inputTokens;
|
|
3063
|
+
model.outputTokens += incoming.outputTokens;
|
|
3064
|
+
model.reasoningTokens += incoming.reasoningTokens;
|
|
3065
|
+
model.cacheReadTokens += incoming.cacheReadTokens;
|
|
3066
|
+
model.cacheWriteTokens += incoming.cacheWriteTokens;
|
|
3067
|
+
}
|
|
3068
|
+
function sortedCostModels(models) {
|
|
3069
|
+
return Array.from(models.values()).sort(
|
|
3070
|
+
(left, right) => right.estimatedCost - left.estimatedCost || right.requests - left.requests
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
function mergeDisplayPricingSource(left, right) {
|
|
3074
|
+
return left === right ? left : "mixed";
|
|
3075
|
+
}
|
|
3076
|
+
function sectionTitle(label) {
|
|
3077
|
+
const width = 54;
|
|
3078
|
+
const lineLength = Math.max(4, width - label.length - 1);
|
|
3079
|
+
return `${c.bold}${label}${c.reset} ${c.dim}${"\u2500".repeat(lineLength)}${c.reset}`;
|
|
3080
|
+
}
|
|
3081
|
+
function colorText(value, color) {
|
|
3082
|
+
return `${color}${value}${c.reset}`;
|
|
3083
|
+
}
|
|
3084
|
+
function agentColor(agent) {
|
|
3085
|
+
switch (agent) {
|
|
3086
|
+
case "claude":
|
|
3087
|
+
return c.magenta;
|
|
3088
|
+
case "codex":
|
|
3089
|
+
return c.green;
|
|
3090
|
+
case "opencode":
|
|
3091
|
+
return c.cyan;
|
|
3092
|
+
case "amp":
|
|
3093
|
+
return c.yellow;
|
|
3094
|
+
case "pi":
|
|
3095
|
+
return c.blue;
|
|
3096
|
+
case "cursor":
|
|
3097
|
+
return c.blue;
|
|
3098
|
+
default:
|
|
3099
|
+
return c.white;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
function modelColor(model) {
|
|
3103
|
+
const provider = model.provider?.toLowerCase();
|
|
3104
|
+
const modelName = model.model.toLowerCase();
|
|
3105
|
+
if (provider === "anthropic" || modelName.startsWith("claude-")) {
|
|
3106
|
+
return c.magenta;
|
|
3107
|
+
}
|
|
3108
|
+
if (provider === "openai" || modelName.startsWith("gpt-") || /^o\d/.test(modelName)) {
|
|
3109
|
+
return c.green;
|
|
3110
|
+
}
|
|
3111
|
+
if (provider === "google" || modelName.startsWith("gemini-")) {
|
|
3112
|
+
return c.blue;
|
|
3113
|
+
}
|
|
3114
|
+
if (modelName.startsWith("kimi-") || modelName.startsWith("glm-")) {
|
|
3115
|
+
return c.yellow;
|
|
3116
|
+
}
|
|
3117
|
+
return c.cyan;
|
|
3118
|
+
}
|
|
3119
|
+
function renderBar(value, max, width, color = c.cyan) {
|
|
3120
|
+
const filled = max > 0 && value > 0 ? Math.max(1, Math.round(value / max * width)) : 0;
|
|
3121
|
+
const empty = width - filled;
|
|
3122
|
+
return `${color}${"\u2501".repeat(filled)}${c.gray}${"\u2500".repeat(empty)}${c.reset}`;
|
|
3123
|
+
}
|
|
3124
|
+
function clip(value, maxLength) {
|
|
3125
|
+
return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}\u2026`;
|
|
3126
|
+
}
|
|
3127
|
+
function formatCurrency(value) {
|
|
3128
|
+
const cents = Math.floor(Math.max(0, value) * 100 + 1e-9);
|
|
3129
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
3130
|
+
}
|
|
3131
|
+
function formatNumber(value) {
|
|
3132
|
+
return value.toLocaleString("en-US");
|
|
3133
|
+
}
|
|
3134
|
+
function formatRequests(value) {
|
|
3135
|
+
return `${formatNumber(value)} ${value === 1 ? "req" : "reqs"}`;
|
|
3136
|
+
}
|
|
3137
|
+
function formatPercent(value) {
|
|
3138
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
3139
|
+
}
|
|
3140
|
+
function formatDate(value) {
|
|
3141
|
+
return value.toISOString().slice(0, 10);
|
|
3142
|
+
}
|
|
940
3143
|
|
|
941
3144
|
// src/cli.ts
|
|
942
3145
|
var COMMANDS = {
|
|
3146
|
+
cost,
|
|
943
3147
|
scan
|
|
944
3148
|
};
|
|
3149
|
+
var OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--agent", "-a", "--since", "-s"]);
|
|
945
3150
|
function usage() {
|
|
946
3151
|
console.log(`devrage \u2014 count how many times you swear at your coding agents
|
|
947
3152
|
|
|
@@ -949,6 +3154,7 @@ Usage:
|
|
|
949
3154
|
devrage <command> [options]
|
|
950
3155
|
|
|
951
3156
|
Commands:
|
|
3157
|
+
cost Show API-equivalent coding agent cost
|
|
952
3158
|
scan Scan sessions for profanity
|
|
953
3159
|
|
|
954
3160
|
Options:
|
|
@@ -956,6 +3162,7 @@ Options:
|
|
|
956
3162
|
--version Show version
|
|
957
3163
|
|
|
958
3164
|
Examples:
|
|
3165
|
+
devrage cost
|
|
959
3166
|
devrage scan
|
|
960
3167
|
devrage scan --agent claude
|
|
961
3168
|
devrage scan --since 2025-01-01`);
|
|
@@ -968,16 +3175,35 @@ async function main() {
|
|
|
968
3175
|
process.exit(0);
|
|
969
3176
|
}
|
|
970
3177
|
if (command === "--version") {
|
|
971
|
-
console.log("0.
|
|
3178
|
+
console.log("0.5.3");
|
|
972
3179
|
process.exit(0);
|
|
973
3180
|
}
|
|
974
|
-
const
|
|
975
|
-
if (
|
|
976
|
-
await handler(args
|
|
3181
|
+
const parsed = parseCommand(args);
|
|
3182
|
+
if (parsed) {
|
|
3183
|
+
await parsed.handler(parsed.args);
|
|
977
3184
|
} else {
|
|
978
3185
|
await scan(args);
|
|
979
3186
|
}
|
|
980
3187
|
}
|
|
3188
|
+
function parseCommand(args) {
|
|
3189
|
+
for (let index = 0; index < args.length; index++) {
|
|
3190
|
+
const arg = args[index];
|
|
3191
|
+
if (!arg) {
|
|
3192
|
+
continue;
|
|
3193
|
+
}
|
|
3194
|
+
const handler = COMMANDS[arg];
|
|
3195
|
+
if (handler) {
|
|
3196
|
+
return {
|
|
3197
|
+
handler,
|
|
3198
|
+
args: [...args.slice(0, index), ...args.slice(index + 1)]
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
if (OPTIONS_WITH_VALUES.has(arg) && index + 1 < args.length) {
|
|
3202
|
+
index++;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
return null;
|
|
3206
|
+
}
|
|
981
3207
|
main().catch((err) => {
|
|
982
3208
|
console.error(err);
|
|
983
3209
|
process.exit(1);
|