slopmeter 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/dist/cli.js +370 -71
- package/dist/cli.js.map +1 -1
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { mkdirSync, writeFileSync } from "fs";
|
|
5
|
-
import { dirname, extname, resolve as
|
|
5
|
+
import { dirname, extname, resolve as resolve6 } from "path";
|
|
6
6
|
import { parseArgs } from "util";
|
|
7
7
|
import ora from "ora";
|
|
8
8
|
import ow from "ow";
|
|
@@ -5773,8 +5773,37 @@ var heatmapThemes = {
|
|
|
5773
5773
|
]
|
|
5774
5774
|
}
|
|
5775
5775
|
},
|
|
5776
|
+
pi: {
|
|
5777
|
+
title: "Pi Coding Agent",
|
|
5778
|
+
colors: {
|
|
5779
|
+
light: [
|
|
5780
|
+
"#ecfdf5",
|
|
5781
|
+
// emerald-50
|
|
5782
|
+
"#a7f3d0",
|
|
5783
|
+
// emerald-200
|
|
5784
|
+
"#6ee7b7",
|
|
5785
|
+
// emerald-300
|
|
5786
|
+
"#10b981",
|
|
5787
|
+
// emerald-500
|
|
5788
|
+
"#047857"
|
|
5789
|
+
// emerald-700
|
|
5790
|
+
],
|
|
5791
|
+
dark: [
|
|
5792
|
+
"#022c22",
|
|
5793
|
+
// emerald-950
|
|
5794
|
+
"#065f46",
|
|
5795
|
+
// emerald-800
|
|
5796
|
+
"#059669",
|
|
5797
|
+
// emerald-600
|
|
5798
|
+
"#34d399",
|
|
5799
|
+
// emerald-400
|
|
5800
|
+
"#a7f3d0"
|
|
5801
|
+
// emerald-200
|
|
5802
|
+
]
|
|
5803
|
+
}
|
|
5804
|
+
},
|
|
5776
5805
|
all: {
|
|
5777
|
-
title: "Codex / Claude Code / Cursor / Open Code",
|
|
5806
|
+
title: "Codex / Claude Code / Cursor / Open Code / Pi Coding Agent",
|
|
5778
5807
|
titleCaption: "Total usage from",
|
|
5779
5808
|
colors: {
|
|
5780
5809
|
light: [
|
|
@@ -6000,7 +6029,6 @@ function drawHeatmapSection(svg, {
|
|
|
6000
6029
|
totalTokens += row.total;
|
|
6001
6030
|
}
|
|
6002
6031
|
const topMetricGap = 120;
|
|
6003
|
-
const headerLast30DaysX = rightEdge - topMetricGap * 3;
|
|
6004
6032
|
const headerInputX = rightEdge - topMetricGap * 2;
|
|
6005
6033
|
const headerOutputX = rightEdge - topMetricGap;
|
|
6006
6034
|
const totalTokensLabel = formatTokenTotal(totalTokens);
|
|
@@ -6514,6 +6542,9 @@ function getClaudeHistoryFiles() {
|
|
|
6514
6542
|
}
|
|
6515
6543
|
return files;
|
|
6516
6544
|
}
|
|
6545
|
+
function isClaudeAvailable() {
|
|
6546
|
+
return getClaudeProjectDirs().length > 0 || getClaudeStatsCacheFiles().length > 0 || getClaudeHistoryFiles().length > 0;
|
|
6547
|
+
}
|
|
6517
6548
|
async function loadClaudeStatsCacheRows(startDate, endDate, coveredDates, totals, modelTotals, recentModelTotals, recentStart) {
|
|
6518
6549
|
const statsCacheFiles = getClaudeStatsCacheFiles();
|
|
6519
6550
|
for (const file of statsCacheFiles) {
|
|
@@ -6647,6 +6678,7 @@ async function loadClaudeRows(startDate, endDate) {
|
|
|
6647
6678
|
}
|
|
6648
6679
|
|
|
6649
6680
|
// src/lib/codex.ts
|
|
6681
|
+
import { existsSync as existsSync2 } from "fs";
|
|
6650
6682
|
import { homedir as homedir2 } from "os";
|
|
6651
6683
|
import { join as join3, resolve as resolve2 } from "path";
|
|
6652
6684
|
var CLASSIFICATION_PREFIX_BYTES = 32 * 1024;
|
|
@@ -6667,6 +6699,15 @@ function normalizeCodexUsage(value) {
|
|
|
6667
6699
|
total_tokens: total > 0 ? total : input + output
|
|
6668
6700
|
};
|
|
6669
6701
|
}
|
|
6702
|
+
function addCodexUsage(base, delta) {
|
|
6703
|
+
return {
|
|
6704
|
+
input_tokens: (base?.input_tokens ?? 0) + delta.input_tokens,
|
|
6705
|
+
cached_input_tokens: (base?.cached_input_tokens ?? 0) + delta.cached_input_tokens,
|
|
6706
|
+
output_tokens: (base?.output_tokens ?? 0) + delta.output_tokens,
|
|
6707
|
+
reasoning_output_tokens: (base?.reasoning_output_tokens ?? 0) + delta.reasoning_output_tokens,
|
|
6708
|
+
total_tokens: (base?.total_tokens ?? 0) + delta.total_tokens
|
|
6709
|
+
};
|
|
6710
|
+
}
|
|
6670
6711
|
function subtractCodexUsage(current, previous) {
|
|
6671
6712
|
return {
|
|
6672
6713
|
input_tokens: Math.max(
|
|
@@ -6691,6 +6732,12 @@ function subtractCodexUsage(current, previous) {
|
|
|
6691
6732
|
)
|
|
6692
6733
|
};
|
|
6693
6734
|
}
|
|
6735
|
+
function didCodexTotalsRollback(current, previous) {
|
|
6736
|
+
if (!previous) {
|
|
6737
|
+
return false;
|
|
6738
|
+
}
|
|
6739
|
+
return current.input_tokens < previous.input_tokens || current.cached_input_tokens < previous.cached_input_tokens || current.output_tokens < previous.output_tokens || current.reasoning_output_tokens < previous.reasoning_output_tokens || current.total_tokens < previous.total_tokens;
|
|
6740
|
+
}
|
|
6694
6741
|
function asNonEmptyString(value) {
|
|
6695
6742
|
const trimmed = value?.trim();
|
|
6696
6743
|
return trimmed === "" ? void 0 : trimmed;
|
|
@@ -6717,10 +6764,16 @@ function extractCodexModel(payload) {
|
|
|
6717
6764
|
}
|
|
6718
6765
|
return void 0;
|
|
6719
6766
|
}
|
|
6767
|
+
function getCodexHome() {
|
|
6768
|
+
return process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) : join3(homedir2(), ".codex");
|
|
6769
|
+
}
|
|
6720
6770
|
async function getCodexFiles() {
|
|
6721
|
-
const codexHome =
|
|
6771
|
+
const codexHome = getCodexHome();
|
|
6722
6772
|
return listFilesRecursive(join3(codexHome, "sessions"), ".jsonl");
|
|
6723
6773
|
}
|
|
6774
|
+
function isCodexAvailable() {
|
|
6775
|
+
return existsSync2(join3(getCodexHome(), "sessions"));
|
|
6776
|
+
}
|
|
6724
6777
|
function readJsonString(source, start) {
|
|
6725
6778
|
if (source[start] !== '"') {
|
|
6726
6779
|
return null;
|
|
@@ -6959,12 +7012,15 @@ async function processCodexFile(filePath, start, end, maxRecordBytes) {
|
|
|
6959
7012
|
const info = entry.payload?.info;
|
|
6960
7013
|
const lastUsage = normalizeCodexUsage(info?.last_token_usage);
|
|
6961
7014
|
const totalUsage = normalizeCodexUsage(info?.total_token_usage);
|
|
6962
|
-
let rawUsage =
|
|
6963
|
-
if (!rawUsage && totalUsage) {
|
|
6964
|
-
rawUsage = subtractCodexUsage(totalUsage, previousTotals);
|
|
6965
|
-
}
|
|
7015
|
+
let rawUsage = null;
|
|
6966
7016
|
if (totalUsage) {
|
|
7017
|
+
rawUsage = didCodexTotalsRollback(totalUsage, previousTotals) ? lastUsage ?? totalUsage : subtractCodexUsage(totalUsage, previousTotals);
|
|
6967
7018
|
previousTotals = totalUsage;
|
|
7019
|
+
} else {
|
|
7020
|
+
rawUsage = lastUsage;
|
|
7021
|
+
if (rawUsage) {
|
|
7022
|
+
previousTotals = addCodexUsage(previousTotals, rawUsage);
|
|
7023
|
+
}
|
|
6968
7024
|
}
|
|
6969
7025
|
if (!rawUsage) {
|
|
6970
7026
|
continue;
|
|
@@ -7033,11 +7089,17 @@ async function loadCodexRows(start, end, warnings = []) {
|
|
|
7033
7089
|
`Skipped ${skippedOversizedIrrelevantRecords} oversized irrelevant Codex record(s) across ${skippedFiles} file(s); usage totals exclude those records. Relevant oversized records fail the file. Override ${MAX_JSONL_RECORD_BYTES_ENV} to raise the cap.`
|
|
7034
7090
|
);
|
|
7035
7091
|
}
|
|
7036
|
-
return createUsageSummary(
|
|
7092
|
+
return createUsageSummary(
|
|
7093
|
+
"codex",
|
|
7094
|
+
totals,
|
|
7095
|
+
modelTotals,
|
|
7096
|
+
recentModelTotals,
|
|
7097
|
+
end
|
|
7098
|
+
);
|
|
7037
7099
|
}
|
|
7038
7100
|
|
|
7039
7101
|
// src/lib/cursor.ts
|
|
7040
|
-
import { existsSync as
|
|
7102
|
+
import { existsSync as existsSync3 } from "fs";
|
|
7041
7103
|
import { copyFile, mkdtemp, rm } from "fs/promises";
|
|
7042
7104
|
import { homedir as homedir3, tmpdir } from "os";
|
|
7043
7105
|
import { join as join4, resolve as resolve3 } from "path";
|
|
@@ -7085,7 +7147,7 @@ function getCursorStateDbCandidates() {
|
|
|
7085
7147
|
function getCursorStateDbPath() {
|
|
7086
7148
|
const seen = /* @__PURE__ */ new Set();
|
|
7087
7149
|
for (const candidate of getCursorStateDbCandidates()) {
|
|
7088
|
-
if (!seen.has(candidate) &&
|
|
7150
|
+
if (!seen.has(candidate) && existsSync3(candidate)) {
|
|
7089
7151
|
return candidate;
|
|
7090
7152
|
}
|
|
7091
7153
|
seen.add(candidate);
|
|
@@ -7131,7 +7193,7 @@ async function withCursorStateSnapshot(databasePath, callback) {
|
|
|
7131
7193
|
await copyFile(databasePath, snapshotPath);
|
|
7132
7194
|
for (const suffix of ["-shm", "-wal"]) {
|
|
7133
7195
|
const companionPath = `${databasePath}${suffix}`;
|
|
7134
|
-
if (!
|
|
7196
|
+
if (!existsSync3(companionPath)) {
|
|
7135
7197
|
continue;
|
|
7136
7198
|
}
|
|
7137
7199
|
await copyFile(companionPath, `${snapshotPath}${suffix}`);
|
|
@@ -7155,6 +7217,14 @@ async function readCursorAuthState(databasePath) {
|
|
|
7155
7217
|
);
|
|
7156
7218
|
}
|
|
7157
7219
|
}
|
|
7220
|
+
async function isCursorAvailable() {
|
|
7221
|
+
const databasePath = getCursorStateDbPath();
|
|
7222
|
+
if (!databasePath) {
|
|
7223
|
+
return false;
|
|
7224
|
+
}
|
|
7225
|
+
const authState = await readCursorAuthState(databasePath);
|
|
7226
|
+
return Boolean(authState.accessToken);
|
|
7227
|
+
}
|
|
7158
7228
|
function decodeJwtPayload(token) {
|
|
7159
7229
|
const encodedPayload = token.split(".")[1];
|
|
7160
7230
|
if (!encodedPayload) {
|
|
@@ -7303,61 +7373,60 @@ async function processCursorUsageCsvStream(response, onRow) {
|
|
|
7303
7373
|
return;
|
|
7304
7374
|
}
|
|
7305
7375
|
let headers = null;
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7376
|
+
const state = {
|
|
7377
|
+
currentField: "",
|
|
7378
|
+
currentRow: [],
|
|
7379
|
+
inQuotes: false,
|
|
7380
|
+
pendingQuote: false,
|
|
7381
|
+
sawCarriageReturn: false
|
|
7382
|
+
};
|
|
7311
7383
|
const decoder = new TextDecoder();
|
|
7312
7384
|
const emitField = () => {
|
|
7313
|
-
currentRow.push(currentField);
|
|
7314
|
-
currentField = "";
|
|
7385
|
+
state.currentRow.push(state.currentField);
|
|
7386
|
+
state.currentField = "";
|
|
7315
7387
|
};
|
|
7316
7388
|
const emitRow = () => {
|
|
7317
7389
|
emitField();
|
|
7318
|
-
if (currentRow.every((value) => value.trim() === "")) {
|
|
7319
|
-
currentRow = [];
|
|
7390
|
+
if (state.currentRow.every((value) => value.trim() === "")) {
|
|
7391
|
+
state.currentRow = [];
|
|
7320
7392
|
return;
|
|
7321
7393
|
}
|
|
7322
7394
|
if (!headers) {
|
|
7323
|
-
headers = currentRow;
|
|
7324
|
-
currentRow = [];
|
|
7395
|
+
headers = state.currentRow;
|
|
7396
|
+
state.currentRow = [];
|
|
7325
7397
|
return;
|
|
7326
7398
|
}
|
|
7327
|
-
onRow(createCursorCsvRow(headers, currentRow));
|
|
7328
|
-
currentRow = [];
|
|
7399
|
+
onRow(createCursorCsvRow(headers, state.currentRow));
|
|
7400
|
+
state.currentRow = [];
|
|
7329
7401
|
};
|
|
7330
7402
|
const processChunk = (chunk) => {
|
|
7331
7403
|
for (const char of chunk) {
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
if (sawCarriageReturn) {
|
|
7336
|
-
sawCarriageReturn = false;
|
|
7404
|
+
for (; ; ) {
|
|
7405
|
+
if (state.sawCarriageReturn) {
|
|
7406
|
+
state.sawCarriageReturn = false;
|
|
7337
7407
|
if (char === "\n") {
|
|
7338
7408
|
break;
|
|
7339
7409
|
}
|
|
7340
7410
|
}
|
|
7341
|
-
if (pendingQuote) {
|
|
7342
|
-
pendingQuote = false;
|
|
7411
|
+
if (state.pendingQuote) {
|
|
7412
|
+
state.pendingQuote = false;
|
|
7343
7413
|
if (char === '"') {
|
|
7344
|
-
currentField += '"';
|
|
7414
|
+
state.currentField += '"';
|
|
7345
7415
|
break;
|
|
7346
7416
|
}
|
|
7347
|
-
inQuotes = false;
|
|
7348
|
-
shouldReprocess = true;
|
|
7417
|
+
state.inQuotes = false;
|
|
7349
7418
|
continue;
|
|
7350
7419
|
}
|
|
7351
|
-
if (inQuotes) {
|
|
7420
|
+
if (state.inQuotes) {
|
|
7352
7421
|
if (char === '"') {
|
|
7353
|
-
pendingQuote = true;
|
|
7422
|
+
state.pendingQuote = true;
|
|
7354
7423
|
} else {
|
|
7355
|
-
currentField += char;
|
|
7424
|
+
state.currentField += char;
|
|
7356
7425
|
}
|
|
7357
7426
|
break;
|
|
7358
7427
|
}
|
|
7359
7428
|
if (char === '"') {
|
|
7360
|
-
inQuotes = true;
|
|
7429
|
+
state.inQuotes = true;
|
|
7361
7430
|
break;
|
|
7362
7431
|
}
|
|
7363
7432
|
if (char === ",") {
|
|
@@ -7370,10 +7439,10 @@ async function processCursorUsageCsvStream(response, onRow) {
|
|
|
7370
7439
|
}
|
|
7371
7440
|
if (char === "\r") {
|
|
7372
7441
|
emitRow();
|
|
7373
|
-
sawCarriageReturn = true;
|
|
7442
|
+
state.sawCarriageReturn = true;
|
|
7374
7443
|
break;
|
|
7375
7444
|
}
|
|
7376
|
-
currentField += char;
|
|
7445
|
+
state.currentField += char;
|
|
7377
7446
|
break;
|
|
7378
7447
|
}
|
|
7379
7448
|
}
|
|
@@ -7391,11 +7460,10 @@ async function processCursorUsageCsvStream(response, onRow) {
|
|
|
7391
7460
|
reader.releaseLock();
|
|
7392
7461
|
}
|
|
7393
7462
|
processChunk(decoder.decode());
|
|
7394
|
-
if (pendingQuote) {
|
|
7395
|
-
|
|
7396
|
-
inQuotes = false;
|
|
7463
|
+
if (state.pendingQuote) {
|
|
7464
|
+
state.inQuotes = false;
|
|
7397
7465
|
}
|
|
7398
|
-
if (currentField !== "" || currentRow.length > 0) {
|
|
7466
|
+
if (state.currentField !== "" || state.currentRow.length > 0) {
|
|
7399
7467
|
emitRow();
|
|
7400
7468
|
}
|
|
7401
7469
|
}
|
|
@@ -7456,11 +7524,23 @@ async function loadCursorRows(start, end) {
|
|
|
7456
7524
|
const modelTotals = /* @__PURE__ */ new Map();
|
|
7457
7525
|
const recentModelTotals = /* @__PURE__ */ new Map();
|
|
7458
7526
|
if (!databasePath) {
|
|
7459
|
-
return createUsageSummary(
|
|
7527
|
+
return createUsageSummary(
|
|
7528
|
+
"cursor",
|
|
7529
|
+
totals,
|
|
7530
|
+
modelTotals,
|
|
7531
|
+
recentModelTotals,
|
|
7532
|
+
end
|
|
7533
|
+
);
|
|
7460
7534
|
}
|
|
7461
7535
|
const authState = await readCursorAuthState(databasePath);
|
|
7462
7536
|
if (!authState.accessToken) {
|
|
7463
|
-
return createUsageSummary(
|
|
7537
|
+
return createUsageSummary(
|
|
7538
|
+
"cursor",
|
|
7539
|
+
totals,
|
|
7540
|
+
modelTotals,
|
|
7541
|
+
recentModelTotals,
|
|
7542
|
+
end
|
|
7543
|
+
);
|
|
7464
7544
|
}
|
|
7465
7545
|
const recentStart = getRecentWindowStart(end, 30);
|
|
7466
7546
|
const response = await fetchCursorUsageCsv(authState.accessToken);
|
|
@@ -7481,20 +7561,34 @@ async function summarizeCursorUsageCsv(response, start, end, recentStart = getRe
|
|
|
7481
7561
|
recentModelTotals
|
|
7482
7562
|
);
|
|
7483
7563
|
});
|
|
7484
|
-
return createUsageSummary(
|
|
7564
|
+
return createUsageSummary(
|
|
7565
|
+
"cursor",
|
|
7566
|
+
totals,
|
|
7567
|
+
modelTotals,
|
|
7568
|
+
recentModelTotals,
|
|
7569
|
+
end
|
|
7570
|
+
);
|
|
7485
7571
|
}
|
|
7486
7572
|
|
|
7487
7573
|
// src/lib/interfaces.ts
|
|
7488
|
-
var providerIds = [
|
|
7574
|
+
var providerIds = [
|
|
7575
|
+
"claude",
|
|
7576
|
+
"codex",
|
|
7577
|
+
"cursor",
|
|
7578
|
+
"opencode",
|
|
7579
|
+
"pi"
|
|
7580
|
+
];
|
|
7581
|
+
var defaultProviderIds = ["claude", "codex", "cursor"];
|
|
7489
7582
|
var providerStatusLabel = {
|
|
7490
7583
|
claude: "Claude code",
|
|
7491
7584
|
codex: "Codex",
|
|
7492
7585
|
cursor: "Cursor",
|
|
7493
|
-
opencode: "Open Code"
|
|
7586
|
+
opencode: "Open Code",
|
|
7587
|
+
pi: "Pi Coding Agent"
|
|
7494
7588
|
};
|
|
7495
7589
|
|
|
7496
7590
|
// src/lib/open-code.ts
|
|
7497
|
-
import { existsSync as
|
|
7591
|
+
import { existsSync as existsSync4 } from "fs";
|
|
7498
7592
|
import { copyFile as copyFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
7499
7593
|
import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
|
|
7500
7594
|
import { join as join5, resolve as resolve4 } from "path";
|
|
@@ -7520,12 +7614,16 @@ function getOpenCodeBaseDir() {
|
|
|
7520
7614
|
async function getOpenCodeSource() {
|
|
7521
7615
|
const baseDir = getOpenCodeBaseDir();
|
|
7522
7616
|
const databasePath = join5(baseDir, "opencode.db");
|
|
7523
|
-
if (
|
|
7617
|
+
if (existsSync4(databasePath)) {
|
|
7524
7618
|
return { kind: "database", path: databasePath };
|
|
7525
7619
|
}
|
|
7526
7620
|
const messagesDir = join5(baseDir, "storage", "message");
|
|
7527
7621
|
return { kind: "legacy", files: await listFilesRecursive(messagesDir, ".json") };
|
|
7528
7622
|
}
|
|
7623
|
+
function isOpenCodeAvailable() {
|
|
7624
|
+
const baseDir = getOpenCodeBaseDir();
|
|
7625
|
+
return existsSync4(join5(baseDir, "opencode.db")) || existsSync4(join5(baseDir, "storage", "message"));
|
|
7626
|
+
}
|
|
7529
7627
|
async function loadSqliteModule() {
|
|
7530
7628
|
try {
|
|
7531
7629
|
const moduleName = "node:sqlite";
|
|
@@ -7571,7 +7669,7 @@ async function withDatabaseSnapshot(databasePath, callback) {
|
|
|
7571
7669
|
await copyFile2(databasePath, snapshotPath);
|
|
7572
7670
|
for (const suffix of ["-shm", "-wal"]) {
|
|
7573
7671
|
const companionPath = `${databasePath}${suffix}`;
|
|
7574
|
-
if (!
|
|
7672
|
+
if (!existsSync4(companionPath)) {
|
|
7575
7673
|
continue;
|
|
7576
7674
|
}
|
|
7577
7675
|
await copyFile2(companionPath, `${snapshotPath}${suffix}`);
|
|
@@ -7680,7 +7778,152 @@ async function loadOpenCodeRows(start, end) {
|
|
|
7680
7778
|
);
|
|
7681
7779
|
}
|
|
7682
7780
|
|
|
7781
|
+
// src/lib/pi.ts
|
|
7782
|
+
import { existsSync as existsSync5 } from "fs";
|
|
7783
|
+
import { homedir as homedir5 } from "os";
|
|
7784
|
+
import { join as join6, resolve as resolve5 } from "path";
|
|
7785
|
+
var PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
|
|
7786
|
+
var CLASSIFICATION_PREFIX_BYTES2 = 16 * 1024;
|
|
7787
|
+
function getPiAgentDir() {
|
|
7788
|
+
const configuredAgentDir = process.env[PI_AGENT_DIR_ENV]?.trim();
|
|
7789
|
+
return configuredAgentDir ? resolve5(configuredAgentDir) : join6(homedir5(), ".pi", "agent");
|
|
7790
|
+
}
|
|
7791
|
+
async function getPiSessionFiles() {
|
|
7792
|
+
return listFilesRecursive(join6(getPiAgentDir(), "sessions"), ".jsonl");
|
|
7793
|
+
}
|
|
7794
|
+
function isPiAvailable() {
|
|
7795
|
+
return existsSync5(join6(getPiAgentDir(), "sessions"));
|
|
7796
|
+
}
|
|
7797
|
+
function classifyPiRecord(prefix) {
|
|
7798
|
+
if (prefix.includes('"type":"message"') && prefix.includes('"role":"assistant"')) {
|
|
7799
|
+
return { kind: "keep", classification: void 0 };
|
|
7800
|
+
}
|
|
7801
|
+
return { kind: "skip" };
|
|
7802
|
+
}
|
|
7803
|
+
function asNonEmptyString2(value) {
|
|
7804
|
+
const trimmed = value?.trim();
|
|
7805
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
7806
|
+
}
|
|
7807
|
+
function createPiTokenTotals(usage) {
|
|
7808
|
+
const cacheRead = usage.cacheRead ?? 0;
|
|
7809
|
+
const cacheWrite = usage.cacheWrite ?? 0;
|
|
7810
|
+
const input = (usage.input ?? 0) + cacheRead;
|
|
7811
|
+
const output = (usage.output ?? 0) + cacheWrite;
|
|
7812
|
+
const total = usage.totalTokens ?? input + output;
|
|
7813
|
+
return {
|
|
7814
|
+
input,
|
|
7815
|
+
output,
|
|
7816
|
+
cache: { input: cacheRead, output: cacheWrite },
|
|
7817
|
+
total
|
|
7818
|
+
};
|
|
7819
|
+
}
|
|
7820
|
+
function getPiTimestamp(entry) {
|
|
7821
|
+
const rawTimestamp = entry.timestamp ?? entry.message?.timestamp;
|
|
7822
|
+
if (typeof rawTimestamp === "string" || typeof rawTimestamp === "number") {
|
|
7823
|
+
return new Date(rawTimestamp);
|
|
7824
|
+
}
|
|
7825
|
+
return null;
|
|
7826
|
+
}
|
|
7827
|
+
async function loadPiRows(start, end) {
|
|
7828
|
+
const files = await getPiSessionFiles();
|
|
7829
|
+
const totals = /* @__PURE__ */ new Map();
|
|
7830
|
+
const modelTotals = /* @__PURE__ */ new Map();
|
|
7831
|
+
const recentModelTotals = /* @__PURE__ */ new Map();
|
|
7832
|
+
const recentStart = getRecentWindowStart(end, 30);
|
|
7833
|
+
const maxRecordBytes = getPositiveIntegerEnv(
|
|
7834
|
+
MAX_JSONL_RECORD_BYTES_ENV,
|
|
7835
|
+
DEFAULT_MAX_JSONL_RECORD_BYTES
|
|
7836
|
+
);
|
|
7837
|
+
for (const file of files) {
|
|
7838
|
+
for await (const record of readJsonlRecords(file, {
|
|
7839
|
+
classificationPrefixBytes: CLASSIFICATION_PREFIX_BYTES2,
|
|
7840
|
+
classify: classifyPiRecord,
|
|
7841
|
+
maxRecordBytes,
|
|
7842
|
+
oversizedErrorMessage: ({
|
|
7843
|
+
filePath,
|
|
7844
|
+
lineNumber,
|
|
7845
|
+
maxRecordBytes: maxRecordBytes2,
|
|
7846
|
+
envVarName
|
|
7847
|
+
}) => `Relevant Pi Coding Agent record exceeds ${maxRecordBytes2} bytes in ${filePath}:${lineNumber}. Increase ${envVarName} to process this file.`
|
|
7848
|
+
})) {
|
|
7849
|
+
let entry;
|
|
7850
|
+
try {
|
|
7851
|
+
entry = JSON.parse(record.rawLine);
|
|
7852
|
+
} catch {
|
|
7853
|
+
continue;
|
|
7854
|
+
}
|
|
7855
|
+
if (entry.type !== "message" || entry.message?.role !== "assistant") {
|
|
7856
|
+
continue;
|
|
7857
|
+
}
|
|
7858
|
+
const usage = entry.message.usage;
|
|
7859
|
+
if (!usage) {
|
|
7860
|
+
continue;
|
|
7861
|
+
}
|
|
7862
|
+
const timestamp = getPiTimestamp(entry);
|
|
7863
|
+
if (!timestamp || Number.isNaN(timestamp.getTime())) {
|
|
7864
|
+
continue;
|
|
7865
|
+
}
|
|
7866
|
+
if (timestamp < start || timestamp > end) {
|
|
7867
|
+
continue;
|
|
7868
|
+
}
|
|
7869
|
+
const tokenTotals = createPiTokenTotals(usage);
|
|
7870
|
+
if (tokenTotals.total <= 0) {
|
|
7871
|
+
continue;
|
|
7872
|
+
}
|
|
7873
|
+
const modelName = asNonEmptyString2(entry.message.model);
|
|
7874
|
+
const normalizedModelName = modelName ? normalizeModelName(modelName) : void 0;
|
|
7875
|
+
addDailyTokenTotals(totals, timestamp, tokenTotals, normalizedModelName);
|
|
7876
|
+
if (!normalizedModelName) {
|
|
7877
|
+
continue;
|
|
7878
|
+
}
|
|
7879
|
+
addModelTokenTotals(modelTotals, normalizedModelName, tokenTotals);
|
|
7880
|
+
if (timestamp >= recentStart) {
|
|
7881
|
+
addModelTokenTotals(
|
|
7882
|
+
recentModelTotals,
|
|
7883
|
+
normalizedModelName,
|
|
7884
|
+
tokenTotals
|
|
7885
|
+
);
|
|
7886
|
+
}
|
|
7887
|
+
}
|
|
7888
|
+
}
|
|
7889
|
+
return createUsageSummary("pi", totals, modelTotals, recentModelTotals, end);
|
|
7890
|
+
}
|
|
7891
|
+
|
|
7683
7892
|
// src/providers.ts
|
|
7893
|
+
function createEmptyProviderAvailability() {
|
|
7894
|
+
return {
|
|
7895
|
+
claude: false,
|
|
7896
|
+
codex: false,
|
|
7897
|
+
cursor: false,
|
|
7898
|
+
opencode: false,
|
|
7899
|
+
pi: false
|
|
7900
|
+
};
|
|
7901
|
+
}
|
|
7902
|
+
async function isProviderAvailable(provider) {
|
|
7903
|
+
switch (provider) {
|
|
7904
|
+
case "claude":
|
|
7905
|
+
return isClaudeAvailable();
|
|
7906
|
+
case "codex":
|
|
7907
|
+
return isCodexAvailable();
|
|
7908
|
+
case "cursor":
|
|
7909
|
+
return isCursorAvailable();
|
|
7910
|
+
case "opencode":
|
|
7911
|
+
return isOpenCodeAvailable();
|
|
7912
|
+
case "pi":
|
|
7913
|
+
return isPiAvailable();
|
|
7914
|
+
default: {
|
|
7915
|
+
const exhaustiveCheck = provider;
|
|
7916
|
+
throw new Error(`Unhandled provider: ${String(exhaustiveCheck)}`);
|
|
7917
|
+
}
|
|
7918
|
+
}
|
|
7919
|
+
}
|
|
7920
|
+
async function getProviderAvailability(providers = providerIds) {
|
|
7921
|
+
const availability = createEmptyProviderAvailability();
|
|
7922
|
+
for (const provider of providers) {
|
|
7923
|
+
availability[provider] = await isProviderAvailable(provider);
|
|
7924
|
+
}
|
|
7925
|
+
return availability;
|
|
7926
|
+
}
|
|
7684
7927
|
function mergeProviderUsage(rowsByProvider, end) {
|
|
7685
7928
|
const summaries = providerIds.map((provider) => rowsByProvider[provider]).filter((summary) => summary !== null);
|
|
7686
7929
|
if (summaries.length === 0) {
|
|
@@ -7698,11 +7941,33 @@ async function aggregateUsage({
|
|
|
7698
7941
|
claude: null,
|
|
7699
7942
|
codex: null,
|
|
7700
7943
|
cursor: null,
|
|
7701
|
-
opencode: null
|
|
7944
|
+
opencode: null,
|
|
7945
|
+
pi: null
|
|
7702
7946
|
};
|
|
7703
7947
|
const warnings = [];
|
|
7704
7948
|
for (const provider of providersToLoad) {
|
|
7705
|
-
|
|
7949
|
+
let summary;
|
|
7950
|
+
switch (provider) {
|
|
7951
|
+
case "claude":
|
|
7952
|
+
summary = await loadClaudeRows(start, end);
|
|
7953
|
+
break;
|
|
7954
|
+
case "codex":
|
|
7955
|
+
summary = await loadCodexRows(start, end, warnings);
|
|
7956
|
+
break;
|
|
7957
|
+
case "cursor":
|
|
7958
|
+
summary = await loadCursorRows(start, end);
|
|
7959
|
+
break;
|
|
7960
|
+
case "opencode":
|
|
7961
|
+
summary = await loadOpenCodeRows(start, end);
|
|
7962
|
+
break;
|
|
7963
|
+
case "pi":
|
|
7964
|
+
summary = await loadPiRows(start, end);
|
|
7965
|
+
break;
|
|
7966
|
+
default: {
|
|
7967
|
+
const exhaustiveCheck = provider;
|
|
7968
|
+
throw new Error(`Unhandled provider: ${String(exhaustiveCheck)}`);
|
|
7969
|
+
}
|
|
7970
|
+
}
|
|
7706
7971
|
rowsByProvider[provider] = hasUsage(summary) ? summary : null;
|
|
7707
7972
|
}
|
|
7708
7973
|
return { rowsByProvider, warnings };
|
|
@@ -7718,7 +7983,7 @@ var HELP_TEXT = `slopmeter
|
|
|
7718
7983
|
Generate rolling 1-year usage heatmap image(s) (today is the latest day).
|
|
7719
7984
|
|
|
7720
7985
|
Usage:
|
|
7721
|
-
slopmeter [--all] [--claude] [--codex] [--cursor] [--opencode] [--dark] [--format png|svg|json] [--output ./heatmap-last-year.png]
|
|
7986
|
+
slopmeter [--all] [--claude] [--codex] [--cursor] [--opencode] [--pi] [--dark] [--format png|svg|json] [--output ./heatmap-last-year.png]
|
|
7722
7987
|
|
|
7723
7988
|
Options:
|
|
7724
7989
|
--all Render one merged graph for all providers
|
|
@@ -7726,6 +7991,7 @@ Options:
|
|
|
7726
7991
|
--codex Render Codex graph
|
|
7727
7992
|
--cursor Render Cursor graph
|
|
7728
7993
|
--opencode Render Open Code graph
|
|
7994
|
+
--pi Render Pi Coding Agent graph
|
|
7729
7995
|
--dark Render with the dark theme
|
|
7730
7996
|
-f, --format Output format: png, svg, or json (default: png)
|
|
7731
7997
|
-o, --output Output file path (default: ./heatmap-last-year.png)
|
|
@@ -7746,7 +8012,8 @@ function validateArgs(values) {
|
|
|
7746
8012
|
claude: ow.boolean,
|
|
7747
8013
|
codex: ow.boolean,
|
|
7748
8014
|
cursor: ow.boolean,
|
|
7749
|
-
opencode: ow.boolean
|
|
8015
|
+
opencode: ow.boolean,
|
|
8016
|
+
pi: ow.boolean
|
|
7750
8017
|
})
|
|
7751
8018
|
);
|
|
7752
8019
|
}
|
|
@@ -7801,33 +8068,52 @@ function getDateWindow() {
|
|
|
7801
8068
|
end.setHours(23, 59, 59, 999);
|
|
7802
8069
|
return { start, end };
|
|
7803
8070
|
}
|
|
7804
|
-
function printProviderAvailability(
|
|
8071
|
+
function printProviderAvailability(availabilityByProvider, providers) {
|
|
7805
8072
|
for (const provider of providers) {
|
|
7806
|
-
const
|
|
7807
|
-
process.stdout.write(`${providerStatusLabel[provider]} ${
|
|
8073
|
+
const status = availabilityByProvider[provider] ? "available" : "not available";
|
|
8074
|
+
process.stdout.write(`${providerStatusLabel[provider]} ${status}
|
|
7808
8075
|
`);
|
|
7809
8076
|
}
|
|
7810
8077
|
}
|
|
7811
8078
|
function getRequestedProviders(values) {
|
|
7812
8079
|
return providerIds.filter((id) => values[id]);
|
|
7813
8080
|
}
|
|
7814
|
-
function getOutputProviders(values, rowsByProvider, end) {
|
|
8081
|
+
function getOutputProviders(values, availabilityByProvider, rowsByProvider, end) {
|
|
7815
8082
|
if (!values.all) {
|
|
7816
|
-
return selectProvidersToRender(
|
|
8083
|
+
return selectProvidersToRender(
|
|
8084
|
+
availabilityByProvider,
|
|
8085
|
+
rowsByProvider,
|
|
8086
|
+
getRequestedProviders(values)
|
|
8087
|
+
);
|
|
7817
8088
|
}
|
|
7818
8089
|
const merged = mergeProviderUsage(rowsByProvider, end);
|
|
7819
8090
|
if (!merged) {
|
|
7820
|
-
throw new Error(
|
|
7821
|
-
"No usage data found for Claude code, Codex, Cursor, or Open code."
|
|
7822
|
-
);
|
|
8091
|
+
throw new Error("No usage data found for any provider.");
|
|
7823
8092
|
}
|
|
7824
8093
|
return [merged];
|
|
7825
8094
|
}
|
|
8095
|
+
function getDefaultOutputProviderIds(rowsByProvider) {
|
|
8096
|
+
const selected = [];
|
|
8097
|
+
const fallbackProviders = providerIds.filter(
|
|
8098
|
+
(provider) => !defaultProviderIds.includes(provider)
|
|
8099
|
+
);
|
|
8100
|
+
for (const provider of [...defaultProviderIds, ...fallbackProviders]) {
|
|
8101
|
+
if (!rowsByProvider[provider] || selected.includes(provider)) {
|
|
8102
|
+
continue;
|
|
8103
|
+
}
|
|
8104
|
+
selected.push(provider);
|
|
8105
|
+
if (selected.length === 3) {
|
|
8106
|
+
return selected;
|
|
8107
|
+
}
|
|
8108
|
+
}
|
|
8109
|
+
return selected;
|
|
8110
|
+
}
|
|
7826
8111
|
function getMergedProviderTitle(rowsByProvider) {
|
|
7827
8112
|
return providerIds.filter((provider) => rowsByProvider[provider] !== null).map((provider) => heatmapThemes[provider].title).join(" / ");
|
|
7828
8113
|
}
|
|
7829
|
-
function selectProvidersToRender(rowsByProvider, requested) {
|
|
7830
|
-
const
|
|
8114
|
+
function selectProvidersToRender(availabilityByProvider, rowsByProvider, requested) {
|
|
8115
|
+
const defaultProviders = getDefaultOutputProviderIds(rowsByProvider);
|
|
8116
|
+
const providersToRender = requested.length > 0 ? requested.filter((provider) => rowsByProvider[provider]) : defaultProviders.filter((provider) => rowsByProvider[provider]);
|
|
7831
8117
|
if (requested.length > 0 && providersToRender.length < requested.length) {
|
|
7832
8118
|
const missing = requested.filter((provider) => !rowsByProvider[provider]);
|
|
7833
8119
|
throw new Error(
|
|
@@ -7835,8 +8121,18 @@ function selectProvidersToRender(rowsByProvider, requested) {
|
|
|
7835
8121
|
);
|
|
7836
8122
|
}
|
|
7837
8123
|
if (providersToRender.length === 0) {
|
|
8124
|
+
const availableProviders = providerIds.filter(
|
|
8125
|
+
(provider) => availabilityByProvider[provider]
|
|
8126
|
+
);
|
|
8127
|
+
if (availableProviders.length > 0) {
|
|
8128
|
+
const availableLabels = availableProviders.map((provider) => providerStatusLabel[provider]).join(", ");
|
|
8129
|
+
const defaultLabels = defaultProviderIds.map((provider) => providerStatusLabel[provider]).join(", ");
|
|
8130
|
+
throw new Error(
|
|
8131
|
+
`No usage data found for available providers (${availableLabels}). Preferred order is ${defaultLabels}. Use --all or specify providers explicitly.`
|
|
8132
|
+
);
|
|
8133
|
+
}
|
|
7838
8134
|
throw new Error(
|
|
7839
|
-
"No usage data found for
|
|
8135
|
+
"No usage data found for any provider."
|
|
7840
8136
|
);
|
|
7841
8137
|
}
|
|
7842
8138
|
return providersToRender.map((provider) => rowsByProvider[provider]);
|
|
@@ -7870,7 +8166,8 @@ async function main() {
|
|
|
7870
8166
|
claude: { type: "boolean", default: false },
|
|
7871
8167
|
codex: { type: "boolean", default: false },
|
|
7872
8168
|
cursor: { type: "boolean", default: false },
|
|
7873
|
-
opencode: { type: "boolean", default: false }
|
|
8169
|
+
opencode: { type: "boolean", default: false },
|
|
8170
|
+
pi: { type: "boolean", default: false }
|
|
7874
8171
|
},
|
|
7875
8172
|
allowPositionals: false
|
|
7876
8173
|
});
|
|
@@ -7890,6 +8187,7 @@ async function main() {
|
|
|
7890
8187
|
const format = inferFormat(values.format, values.output);
|
|
7891
8188
|
const requestedProviders = values.all ? providerIds : getRequestedProviders(values);
|
|
7892
8189
|
const inspectedProviders = requestedProviders.length > 0 ? requestedProviders : providerIds;
|
|
8190
|
+
const availabilityByProvider = await getProviderAvailability(inspectedProviders);
|
|
7893
8191
|
const { rowsByProvider, warnings } = await aggregateUsage({
|
|
7894
8192
|
start,
|
|
7895
8193
|
end,
|
|
@@ -7900,13 +8198,14 @@ async function main() {
|
|
|
7900
8198
|
process.stderr.write(`${warning}
|
|
7901
8199
|
`);
|
|
7902
8200
|
}
|
|
7903
|
-
printProviderAvailability(
|
|
8201
|
+
printProviderAvailability(availabilityByProvider, inspectedProviders);
|
|
7904
8202
|
const exportProviders = getOutputProviders(
|
|
7905
8203
|
values,
|
|
8204
|
+
availabilityByProvider,
|
|
7906
8205
|
rowsByProvider,
|
|
7907
8206
|
end
|
|
7908
8207
|
);
|
|
7909
|
-
const outputPath =
|
|
8208
|
+
const outputPath = resolve6(
|
|
7910
8209
|
values.output ?? `./heatmap-last-year.${format}`
|
|
7911
8210
|
);
|
|
7912
8211
|
mkdirSync(dirname(outputPath), { recursive: true });
|