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/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 resolve5 } from "path";
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 = process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) : join3(homedir2(), ".codex");
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 = lastUsage;
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("codex", totals, modelTotals, recentModelTotals, end);
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 existsSync2 } from "fs";
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) && existsSync2(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 (!existsSync2(companionPath)) {
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
- let currentField = "";
7307
- let currentRow = [];
7308
- let inQuotes = false;
7309
- let pendingQuote = false;
7310
- let sawCarriageReturn = false;
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
- let shouldReprocess = true;
7333
- while (shouldReprocess) {
7334
- shouldReprocess = false;
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
- pendingQuote = false;
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("cursor", totals, modelTotals, recentModelTotals, end);
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("cursor", totals, modelTotals, recentModelTotals, end);
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("cursor", totals, modelTotals, recentModelTotals, end);
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 = ["claude", "codex", "cursor", "opencode"];
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 existsSync3 } from "fs";
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 (existsSync3(databasePath)) {
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 (!existsSync3(companionPath)) {
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
- const summary = provider === "claude" ? await loadClaudeRows(start, end) : provider === "codex" ? await loadCodexRows(start, end, warnings) : provider === "cursor" ? await loadCursorRows(start, end) : await loadOpenCodeRows(start, end);
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(rowsByProvider, providers) {
8071
+ function printProviderAvailability(availabilityByProvider, providers) {
7805
8072
  for (const provider of providers) {
7806
- const found = rowsByProvider[provider] ? "found" : "not found";
7807
- process.stdout.write(`${providerStatusLabel[provider]} ${found}
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(rowsByProvider, getRequestedProviders(values));
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 providersToRender = requested.length > 0 ? requested.filter((provider) => rowsByProvider[provider]) : providerIds.filter((provider) => rowsByProvider[provider]);
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 Claude code, Codex, Cursor, or Open code."
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(rowsByProvider, inspectedProviders);
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 = resolve5(
8208
+ const outputPath = resolve6(
7910
8209
  values.output ?? `./heatmap-last-year.${format}`
7911
8210
  );
7912
8211
  mkdirSync(dirname(outputPath), { recursive: true });