slopmeter 0.4.1 → 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 resolve4 } 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";
@@ -5715,6 +5715,35 @@ var heatmapThemes = {
5715
5715
  ]
5716
5716
  }
5717
5717
  },
5718
+ cursor: {
5719
+ title: "Cursor",
5720
+ colors: {
5721
+ light: [
5722
+ "#fff7ed",
5723
+ // orange-50
5724
+ "#fed7aa",
5725
+ // orange-200
5726
+ "#fdba74",
5727
+ // orange-300
5728
+ "#f97316",
5729
+ // orange-500
5730
+ "#9a3412"
5731
+ // orange-800
5732
+ ],
5733
+ dark: [
5734
+ "#431407",
5735
+ // orange-950
5736
+ "#9a3412",
5737
+ // orange-800
5738
+ "#c2410c",
5739
+ // orange-700
5740
+ "#f97316",
5741
+ // orange-500
5742
+ "#fdba74"
5743
+ // orange-300
5744
+ ]
5745
+ }
5746
+ },
5718
5747
  opencode: {
5719
5748
  title: "Open Code",
5720
5749
  colors: {
@@ -5744,8 +5773,37 @@ var heatmapThemes = {
5744
5773
  ]
5745
5774
  }
5746
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
+ },
5747
5805
  all: {
5748
- title: "Codex / Claude Code / Open Code",
5806
+ title: "Codex / Claude Code / Cursor / Open Code / Pi Coding Agent",
5749
5807
  titleCaption: "Total usage from",
5750
5808
  colors: {
5751
5809
  light: [
@@ -5971,7 +6029,6 @@ function drawHeatmapSection(svg, {
5971
6029
  totalTokens += row.total;
5972
6030
  }
5973
6031
  const topMetricGap = 120;
5974
- const headerLast30DaysX = rightEdge - topMetricGap * 3;
5975
6032
  const headerInputX = rightEdge - topMetricGap * 2;
5976
6033
  const headerOutputX = rightEdge - topMetricGap;
5977
6034
  const totalTokensLabel = formatTokenTotal(totalTokens);
@@ -6485,6 +6542,9 @@ function getClaudeHistoryFiles() {
6485
6542
  }
6486
6543
  return files;
6487
6544
  }
6545
+ function isClaudeAvailable() {
6546
+ return getClaudeProjectDirs().length > 0 || getClaudeStatsCacheFiles().length > 0 || getClaudeHistoryFiles().length > 0;
6547
+ }
6488
6548
  async function loadClaudeStatsCacheRows(startDate, endDate, coveredDates, totals, modelTotals, recentModelTotals, recentStart) {
6489
6549
  const statsCacheFiles = getClaudeStatsCacheFiles();
6490
6550
  for (const file of statsCacheFiles) {
@@ -6618,6 +6678,7 @@ async function loadClaudeRows(startDate, endDate) {
6618
6678
  }
6619
6679
 
6620
6680
  // src/lib/codex.ts
6681
+ import { existsSync as existsSync2 } from "fs";
6621
6682
  import { homedir as homedir2 } from "os";
6622
6683
  import { join as join3, resolve as resolve2 } from "path";
6623
6684
  var CLASSIFICATION_PREFIX_BYTES = 32 * 1024;
@@ -6638,6 +6699,15 @@ function normalizeCodexUsage(value) {
6638
6699
  total_tokens: total > 0 ? total : input + output
6639
6700
  };
6640
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
+ }
6641
6711
  function subtractCodexUsage(current, previous) {
6642
6712
  return {
6643
6713
  input_tokens: Math.max(
@@ -6662,6 +6732,12 @@ function subtractCodexUsage(current, previous) {
6662
6732
  )
6663
6733
  };
6664
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
+ }
6665
6741
  function asNonEmptyString(value) {
6666
6742
  const trimmed = value?.trim();
6667
6743
  return trimmed === "" ? void 0 : trimmed;
@@ -6688,10 +6764,16 @@ function extractCodexModel(payload) {
6688
6764
  }
6689
6765
  return void 0;
6690
6766
  }
6767
+ function getCodexHome() {
6768
+ return process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) : join3(homedir2(), ".codex");
6769
+ }
6691
6770
  async function getCodexFiles() {
6692
- const codexHome = process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) : join3(homedir2(), ".codex");
6771
+ const codexHome = getCodexHome();
6693
6772
  return listFilesRecursive(join3(codexHome, "sessions"), ".jsonl");
6694
6773
  }
6774
+ function isCodexAvailable() {
6775
+ return existsSync2(join3(getCodexHome(), "sessions"));
6776
+ }
6695
6777
  function readJsonString(source, start) {
6696
6778
  if (source[start] !== '"') {
6697
6779
  return null;
@@ -6930,12 +7012,15 @@ async function processCodexFile(filePath, start, end, maxRecordBytes) {
6930
7012
  const info = entry.payload?.info;
6931
7013
  const lastUsage = normalizeCodexUsage(info?.last_token_usage);
6932
7014
  const totalUsage = normalizeCodexUsage(info?.total_token_usage);
6933
- let rawUsage = lastUsage;
6934
- if (!rawUsage && totalUsage) {
6935
- rawUsage = subtractCodexUsage(totalUsage, previousTotals);
6936
- }
7015
+ let rawUsage = null;
6937
7016
  if (totalUsage) {
7017
+ rawUsage = didCodexTotalsRollback(totalUsage, previousTotals) ? lastUsage ?? totalUsage : subtractCodexUsage(totalUsage, previousTotals);
6938
7018
  previousTotals = totalUsage;
7019
+ } else {
7020
+ rawUsage = lastUsage;
7021
+ if (rawUsage) {
7022
+ previousTotals = addCodexUsage(previousTotals, rawUsage);
7023
+ }
6939
7024
  }
6940
7025
  if (!rawUsage) {
6941
7026
  continue;
@@ -7004,22 +7089,509 @@ async function loadCodexRows(start, end, warnings = []) {
7004
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.`
7005
7090
  );
7006
7091
  }
7007
- return createUsageSummary("codex", totals, modelTotals, recentModelTotals, end);
7092
+ return createUsageSummary(
7093
+ "codex",
7094
+ totals,
7095
+ modelTotals,
7096
+ recentModelTotals,
7097
+ end
7098
+ );
7099
+ }
7100
+
7101
+ // src/lib/cursor.ts
7102
+ import { existsSync as existsSync3 } from "fs";
7103
+ import { copyFile, mkdtemp, rm } from "fs/promises";
7104
+ import { homedir as homedir3, tmpdir } from "os";
7105
+ import { join as join4, resolve as resolve3 } from "path";
7106
+ import Database from "better-sqlite3";
7107
+ var CURSOR_CONFIG_DIR_ENV = "CURSOR_CONFIG_DIR";
7108
+ var CURSOR_STATE_DB_PATH_ENV = "CURSOR_STATE_DB_PATH";
7109
+ var CURSOR_WEB_BASE_URL_ENV = "CURSOR_WEB_BASE_URL";
7110
+ var CURSOR_STATE_DB_RELATIVE_PATH = join4(
7111
+ "User",
7112
+ "globalStorage",
7113
+ "state.vscdb"
7114
+ );
7115
+ var CURSOR_SESSION_COOKIE_NAME = "WorkosCursorSessionToken";
7116
+ function getCursorDefaultStateDbPath() {
7117
+ if (process.platform === "darwin") {
7118
+ return join4(
7119
+ homedir3(),
7120
+ "Library",
7121
+ "Application Support",
7122
+ "Cursor",
7123
+ CURSOR_STATE_DB_RELATIVE_PATH
7124
+ );
7125
+ }
7126
+ if (process.platform === "win32") {
7127
+ const appData = process.env.APPDATA?.trim() || join4(homedir3(), "AppData", "Roaming");
7128
+ return join4(appData, "Cursor", CURSOR_STATE_DB_RELATIVE_PATH);
7129
+ }
7130
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim() || join4(homedir3(), ".config");
7131
+ return join4(xdgConfigHome, "Cursor", CURSOR_STATE_DB_RELATIVE_PATH);
7132
+ }
7133
+ function getCursorStateDbCandidates() {
7134
+ const explicitDbPath = process.env[CURSOR_STATE_DB_PATH_ENV]?.trim();
7135
+ if (explicitDbPath) {
7136
+ return [resolve3(explicitDbPath)];
7137
+ }
7138
+ const configuredDirs = process.env[CURSOR_CONFIG_DIR_ENV]?.trim();
7139
+ if (!configuredDirs) {
7140
+ return [getCursorDefaultStateDbPath()];
7141
+ }
7142
+ return configuredDirs.split(",").map((value) => value.trim()).filter((value) => value !== "").map((value) => {
7143
+ const resolved = resolve3(value);
7144
+ return resolved.endsWith(".vscdb") ? resolved : join4(resolved, CURSOR_STATE_DB_RELATIVE_PATH);
7145
+ });
7146
+ }
7147
+ function getCursorStateDbPath() {
7148
+ const seen = /* @__PURE__ */ new Set();
7149
+ for (const candidate of getCursorStateDbCandidates()) {
7150
+ if (!seen.has(candidate) && existsSync3(candidate)) {
7151
+ return candidate;
7152
+ }
7153
+ seen.add(candidate);
7154
+ }
7155
+ return null;
7156
+ }
7157
+ function normalizeCursorDbValue(value) {
7158
+ if (typeof value === "string") {
7159
+ const trimmed = value.trim();
7160
+ return trimmed === "" ? void 0 : trimmed;
7161
+ }
7162
+ if (Buffer.isBuffer(value)) {
7163
+ const trimmed = value.toString("utf8").trim();
7164
+ return trimmed === "" ? void 0 : trimmed;
7165
+ }
7166
+ return void 0;
7167
+ }
7168
+ function readCursorAuthStateFromDatabase(databasePath) {
7169
+ const database = new Database(databasePath, {
7170
+ readonly: true,
7171
+ fileMustExist: true
7172
+ });
7173
+ try {
7174
+ const query = database.prepare(
7175
+ "SELECT value FROM ItemTable WHERE key = ? LIMIT 1"
7176
+ );
7177
+ const accessRow = query.get("cursorAuth/accessToken");
7178
+ const refreshRow = query.get("cursorAuth/refreshToken");
7179
+ return {
7180
+ accessToken: normalizeCursorDbValue(accessRow?.value),
7181
+ refreshToken: normalizeCursorDbValue(refreshRow?.value)
7182
+ };
7183
+ } finally {
7184
+ database.close();
7185
+ }
7186
+ }
7187
+ function isSqliteLockedError(error) {
7188
+ return error instanceof Error && /database is locked/i.test(error.message);
7189
+ }
7190
+ async function withCursorStateSnapshot(databasePath, callback) {
7191
+ const snapshotDir = await mkdtemp(join4(tmpdir(), "slopmeter-cursor-"));
7192
+ const snapshotPath = join4(snapshotDir, "state.vscdb");
7193
+ await copyFile(databasePath, snapshotPath);
7194
+ for (const suffix of ["-shm", "-wal"]) {
7195
+ const companionPath = `${databasePath}${suffix}`;
7196
+ if (!existsSync3(companionPath)) {
7197
+ continue;
7198
+ }
7199
+ await copyFile(companionPath, `${snapshotPath}${suffix}`);
7200
+ }
7201
+ try {
7202
+ return await callback(snapshotPath);
7203
+ } finally {
7204
+ await rm(snapshotDir, { recursive: true, force: true });
7205
+ }
7206
+ }
7207
+ async function readCursorAuthState(databasePath) {
7208
+ try {
7209
+ return readCursorAuthStateFromDatabase(databasePath);
7210
+ } catch (error) {
7211
+ if (!isSqliteLockedError(error)) {
7212
+ throw error;
7213
+ }
7214
+ return withCursorStateSnapshot(
7215
+ databasePath,
7216
+ async (snapshotPath) => readCursorAuthStateFromDatabase(snapshotPath)
7217
+ );
7218
+ }
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
+ }
7228
+ function decodeJwtPayload(token) {
7229
+ const encodedPayload = token.split(".")[1];
7230
+ if (!encodedPayload) {
7231
+ return null;
7232
+ }
7233
+ const base64 = encodedPayload.replace(/-/g, "+").replace(/_/g, "/");
7234
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
7235
+ try {
7236
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
7237
+ } catch {
7238
+ return null;
7239
+ }
7240
+ }
7241
+ function getCursorWebBaseUrl() {
7242
+ return (process.env[CURSOR_WEB_BASE_URL_ENV]?.trim() || "https://cursor.com").replace(/\/+$/, "");
7243
+ }
7244
+ function buildCookieHeaderValue(cookieValue) {
7245
+ return `${CURSOR_SESSION_COOKIE_NAME}=${cookieValue}`;
7246
+ }
7247
+ function getCursorFetchAttempts(accessToken) {
7248
+ const attempts = [];
7249
+ const seen = /* @__PURE__ */ new Set();
7250
+ const subject = decodeJwtPayload(accessToken)?.sub?.trim();
7251
+ const cookieValues = [accessToken];
7252
+ if (subject) {
7253
+ cookieValues.push(`${subject}::${accessToken}`);
7254
+ }
7255
+ const pushAttempt = (label, headers) => {
7256
+ const signature = JSON.stringify({
7257
+ label,
7258
+ headers: Object.entries(headers).sort(
7259
+ ([left], [right]) => left.localeCompare(right)
7260
+ )
7261
+ });
7262
+ if (seen.has(signature)) {
7263
+ return;
7264
+ }
7265
+ seen.add(signature);
7266
+ attempts.push({ label, headers });
7267
+ };
7268
+ pushAttempt("bearer", {
7269
+ Authorization: `Bearer ${accessToken}`
7270
+ });
7271
+ for (const cookieValue of cookieValues) {
7272
+ pushAttempt("cookie", {
7273
+ Cookie: buildCookieHeaderValue(cookieValue)
7274
+ });
7275
+ pushAttempt("cookie-encoded", {
7276
+ Cookie: buildCookieHeaderValue(encodeURIComponent(cookieValue))
7277
+ });
7278
+ pushAttempt("bearer+cookie", {
7279
+ Authorization: `Bearer ${accessToken}`,
7280
+ Cookie: buildCookieHeaderValue(cookieValue)
7281
+ });
7282
+ pushAttempt("bearer+cookie-encoded", {
7283
+ Authorization: `Bearer ${accessToken}`,
7284
+ Cookie: buildCookieHeaderValue(encodeURIComponent(cookieValue))
7285
+ });
7286
+ }
7287
+ return attempts;
7288
+ }
7289
+ async function fetchCursorUsageCsv(accessToken) {
7290
+ const url = new URL(
7291
+ "/api/dashboard/export-usage-events-csv?strategy=tokens",
7292
+ getCursorWebBaseUrl()
7293
+ );
7294
+ const failures = [];
7295
+ for (const attempt of getCursorFetchAttempts(accessToken)) {
7296
+ const response = await fetch(url, {
7297
+ headers: {
7298
+ Accept: "text/csv,text/plain;q=0.9,*/*;q=0.8",
7299
+ ...attempt.headers
7300
+ }
7301
+ });
7302
+ if (response.ok) {
7303
+ return response;
7304
+ }
7305
+ failures.push({
7306
+ label: attempt.label,
7307
+ status: response.status,
7308
+ statusText: response.statusText,
7309
+ body: (await response.text()).trim().slice(0, 200)
7310
+ });
7311
+ }
7312
+ const summary = failures.map((failure) => {
7313
+ const statusLine = `${failure.label}: ${failure.status} ${failure.statusText}`.trim();
7314
+ return failure.body ? `${statusLine} (${failure.body})` : statusLine;
7315
+ }).join("; ");
7316
+ throw new Error(
7317
+ `Failed to authenticate Cursor usage export with local auth state from ${url.origin}. ${summary}`
7318
+ );
7319
+ }
7320
+ function parseCsvLine(line) {
7321
+ const values = [];
7322
+ let current = "";
7323
+ let inQuotes = false;
7324
+ for (let index = 0; index < line.length; index += 1) {
7325
+ const char = line[index];
7326
+ if (char === '"') {
7327
+ if (inQuotes && line[index + 1] === '"') {
7328
+ current += '"';
7329
+ index += 1;
7330
+ continue;
7331
+ }
7332
+ inQuotes = !inQuotes;
7333
+ continue;
7334
+ }
7335
+ if (char === "," && !inQuotes) {
7336
+ values.push(current);
7337
+ current = "";
7338
+ continue;
7339
+ }
7340
+ current += char;
7341
+ }
7342
+ values.push(current);
7343
+ return values;
7344
+ }
7345
+ function createCursorCsvRow(headers, values) {
7346
+ const row = {};
7347
+ headers.forEach((header, index) => {
7348
+ row[header] = values[index];
7349
+ });
7350
+ return row;
7351
+ }
7352
+ function processCursorCsvLines(lines, onRow) {
7353
+ let headers = null;
7354
+ for (const rawLine of lines) {
7355
+ const line = rawLine.trim();
7356
+ if (line === "") {
7357
+ continue;
7358
+ }
7359
+ const values = parseCsvLine(line);
7360
+ if (!headers) {
7361
+ headers = values;
7362
+ continue;
7363
+ }
7364
+ onRow(createCursorCsvRow(headers, values));
7365
+ }
7366
+ }
7367
+ function processCursorUsageCsvText(content, onRow) {
7368
+ processCursorCsvLines(content.split(/\r?\n/), onRow);
7369
+ }
7370
+ async function processCursorUsageCsvStream(response, onRow) {
7371
+ if (!response.body) {
7372
+ processCursorUsageCsvText(await response.text(), onRow);
7373
+ return;
7374
+ }
7375
+ let headers = null;
7376
+ const state = {
7377
+ currentField: "",
7378
+ currentRow: [],
7379
+ inQuotes: false,
7380
+ pendingQuote: false,
7381
+ sawCarriageReturn: false
7382
+ };
7383
+ const decoder = new TextDecoder();
7384
+ const emitField = () => {
7385
+ state.currentRow.push(state.currentField);
7386
+ state.currentField = "";
7387
+ };
7388
+ const emitRow = () => {
7389
+ emitField();
7390
+ if (state.currentRow.every((value) => value.trim() === "")) {
7391
+ state.currentRow = [];
7392
+ return;
7393
+ }
7394
+ if (!headers) {
7395
+ headers = state.currentRow;
7396
+ state.currentRow = [];
7397
+ return;
7398
+ }
7399
+ onRow(createCursorCsvRow(headers, state.currentRow));
7400
+ state.currentRow = [];
7401
+ };
7402
+ const processChunk = (chunk) => {
7403
+ for (const char of chunk) {
7404
+ for (; ; ) {
7405
+ if (state.sawCarriageReturn) {
7406
+ state.sawCarriageReturn = false;
7407
+ if (char === "\n") {
7408
+ break;
7409
+ }
7410
+ }
7411
+ if (state.pendingQuote) {
7412
+ state.pendingQuote = false;
7413
+ if (char === '"') {
7414
+ state.currentField += '"';
7415
+ break;
7416
+ }
7417
+ state.inQuotes = false;
7418
+ continue;
7419
+ }
7420
+ if (state.inQuotes) {
7421
+ if (char === '"') {
7422
+ state.pendingQuote = true;
7423
+ } else {
7424
+ state.currentField += char;
7425
+ }
7426
+ break;
7427
+ }
7428
+ if (char === '"') {
7429
+ state.inQuotes = true;
7430
+ break;
7431
+ }
7432
+ if (char === ",") {
7433
+ emitField();
7434
+ break;
7435
+ }
7436
+ if (char === "\n") {
7437
+ emitRow();
7438
+ break;
7439
+ }
7440
+ if (char === "\r") {
7441
+ emitRow();
7442
+ state.sawCarriageReturn = true;
7443
+ break;
7444
+ }
7445
+ state.currentField += char;
7446
+ break;
7447
+ }
7448
+ }
7449
+ };
7450
+ const reader = response.body.getReader();
7451
+ try {
7452
+ for (; ; ) {
7453
+ const { done, value } = await reader.read();
7454
+ if (done) {
7455
+ break;
7456
+ }
7457
+ processChunk(decoder.decode(value, { stream: true }));
7458
+ }
7459
+ } finally {
7460
+ reader.releaseLock();
7461
+ }
7462
+ processChunk(decoder.decode());
7463
+ if (state.pendingQuote) {
7464
+ state.inQuotes = false;
7465
+ }
7466
+ if (state.currentField !== "" || state.currentRow.length > 0) {
7467
+ emitRow();
7468
+ }
7469
+ }
7470
+ function addCursorUsageRow(row, start, end, recentStart, totals, modelTotals, recentModelTotals) {
7471
+ const date = parseCursorDate(row.Date);
7472
+ const rawModel = row.Model?.trim();
7473
+ const tokenTotals = createCursorTokenTotals(row);
7474
+ if (!date || !rawModel || !tokenTotals) {
7475
+ return;
7476
+ }
7477
+ if (date < start || date > end) {
7478
+ return;
7479
+ }
7480
+ const modelName = normalizeModelName(rawModel);
7481
+ addDailyTokenTotals(totals, date, tokenTotals, modelName);
7482
+ addModelTokenTotals(modelTotals, modelName, tokenTotals);
7483
+ if (date >= recentStart) {
7484
+ addModelTokenTotals(recentModelTotals, modelName, tokenTotals);
7485
+ }
7486
+ }
7487
+ function parseCursorDate(value) {
7488
+ const trimmed = value?.trim();
7489
+ if (!trimmed) {
7490
+ return null;
7491
+ }
7492
+ if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
7493
+ return /* @__PURE__ */ new Date(`${trimmed}T00:00:00`);
7494
+ }
7495
+ const parsed = new Date(trimmed);
7496
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
7497
+ }
7498
+ function parseCursorNumber(value) {
7499
+ const numeric = Number(value?.replaceAll(",", "").trim() ?? "");
7500
+ if (!Number.isFinite(numeric) || numeric <= 0) {
7501
+ return null;
7502
+ }
7503
+ return Math.round(numeric);
7504
+ }
7505
+ function createCursorTokenTotals(row) {
7506
+ const total = parseCursorNumber(row["Total Tokens"]) ?? parseCursorNumber(row.Tokens);
7507
+ if (!total) {
7508
+ return null;
7509
+ }
7510
+ const inputWithCacheWrite = parseCursorNumber(row["Input (w/ Cache Write)"]) ?? 0;
7511
+ const inputWithoutCacheWrite = parseCursorNumber(row["Input (w/o Cache Write)"]) ?? 0;
7512
+ const cacheInput = parseCursorNumber(row["Cache Read"]) ?? 0;
7513
+ const outputTokens = parseCursorNumber(row["Output Tokens"]) ?? 0;
7514
+ return {
7515
+ input: inputWithCacheWrite + inputWithoutCacheWrite + cacheInput,
7516
+ output: outputTokens,
7517
+ cache: { input: cacheInput, output: inputWithCacheWrite },
7518
+ total
7519
+ };
7520
+ }
7521
+ async function loadCursorRows(start, end) {
7522
+ const databasePath = getCursorStateDbPath();
7523
+ const totals = /* @__PURE__ */ new Map();
7524
+ const modelTotals = /* @__PURE__ */ new Map();
7525
+ const recentModelTotals = /* @__PURE__ */ new Map();
7526
+ if (!databasePath) {
7527
+ return createUsageSummary(
7528
+ "cursor",
7529
+ totals,
7530
+ modelTotals,
7531
+ recentModelTotals,
7532
+ end
7533
+ );
7534
+ }
7535
+ const authState = await readCursorAuthState(databasePath);
7536
+ if (!authState.accessToken) {
7537
+ return createUsageSummary(
7538
+ "cursor",
7539
+ totals,
7540
+ modelTotals,
7541
+ recentModelTotals,
7542
+ end
7543
+ );
7544
+ }
7545
+ const recentStart = getRecentWindowStart(end, 30);
7546
+ const response = await fetchCursorUsageCsv(authState.accessToken);
7547
+ return summarizeCursorUsageCsv(response, start, end, recentStart);
7548
+ }
7549
+ async function summarizeCursorUsageCsv(response, start, end, recentStart = getRecentWindowStart(end, 30)) {
7550
+ const totals = /* @__PURE__ */ new Map();
7551
+ const modelTotals = /* @__PURE__ */ new Map();
7552
+ const recentModelTotals = /* @__PURE__ */ new Map();
7553
+ await processCursorUsageCsvStream(response, (row) => {
7554
+ addCursorUsageRow(
7555
+ row,
7556
+ start,
7557
+ end,
7558
+ recentStart,
7559
+ totals,
7560
+ modelTotals,
7561
+ recentModelTotals
7562
+ );
7563
+ });
7564
+ return createUsageSummary(
7565
+ "cursor",
7566
+ totals,
7567
+ modelTotals,
7568
+ recentModelTotals,
7569
+ end
7570
+ );
7008
7571
  }
7009
7572
 
7010
7573
  // src/lib/interfaces.ts
7011
- var providerIds = ["claude", "codex", "opencode"];
7574
+ var providerIds = [
7575
+ "claude",
7576
+ "codex",
7577
+ "cursor",
7578
+ "opencode",
7579
+ "pi"
7580
+ ];
7581
+ var defaultProviderIds = ["claude", "codex", "cursor"];
7012
7582
  var providerStatusLabel = {
7013
7583
  claude: "Claude code",
7014
7584
  codex: "Codex",
7015
- opencode: "Open Code"
7585
+ cursor: "Cursor",
7586
+ opencode: "Open Code",
7587
+ pi: "Pi Coding Agent"
7016
7588
  };
7017
7589
 
7018
7590
  // src/lib/open-code.ts
7019
- import { existsSync as existsSync2 } from "fs";
7020
- import { copyFile, mkdtemp, rm } from "fs/promises";
7021
- import { homedir as homedir3, tmpdir } from "os";
7022
- import { join as join4, resolve as resolve3 } from "path";
7591
+ import { existsSync as existsSync4 } from "fs";
7592
+ import { copyFile as copyFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
7593
+ import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
7594
+ import { join as join5, resolve as resolve4 } from "path";
7023
7595
  function sumOpenCodeTokens(tokens) {
7024
7596
  const cacheInput = tokens?.cache?.read ?? 0;
7025
7597
  const cacheOutput = tokens?.cache?.write ?? 0;
@@ -7036,18 +7608,22 @@ async function parseOpenCodeFile(filePath) {
7036
7608
  return readJsonDocument(filePath);
7037
7609
  }
7038
7610
  function getOpenCodeBaseDir() {
7039
- const baseDir = process.env.OPENCODE_DATA_DIR?.trim() ? resolve3(process.env.OPENCODE_DATA_DIR) : join4(homedir3(), ".local", "share", "opencode");
7611
+ const baseDir = process.env.OPENCODE_DATA_DIR?.trim() ? resolve4(process.env.OPENCODE_DATA_DIR) : join5(homedir4(), ".local", "share", "opencode");
7040
7612
  return baseDir;
7041
7613
  }
7042
7614
  async function getOpenCodeSource() {
7043
7615
  const baseDir = getOpenCodeBaseDir();
7044
- const databasePath = join4(baseDir, "opencode.db");
7045
- if (existsSync2(databasePath)) {
7616
+ const databasePath = join5(baseDir, "opencode.db");
7617
+ if (existsSync4(databasePath)) {
7046
7618
  return { kind: "database", path: databasePath };
7047
7619
  }
7048
- const messagesDir = join4(baseDir, "storage", "message");
7620
+ const messagesDir = join5(baseDir, "storage", "message");
7049
7621
  return { kind: "legacy", files: await listFilesRecursive(messagesDir, ".json") };
7050
7622
  }
7623
+ function isOpenCodeAvailable() {
7624
+ const baseDir = getOpenCodeBaseDir();
7625
+ return existsSync4(join5(baseDir, "opencode.db")) || existsSync4(join5(baseDir, "storage", "message"));
7626
+ }
7051
7627
  async function loadSqliteModule() {
7052
7628
  try {
7053
7629
  const moduleName = "node:sqlite";
@@ -7084,24 +7660,24 @@ function parseOpenCodeMessageData(rowId, sourceLabel, content) {
7084
7660
  id: message.id || rowId
7085
7661
  };
7086
7662
  }
7087
- function isSqliteLockedError(error) {
7663
+ function isSqliteLockedError2(error) {
7088
7664
  return error instanceof Error && /database is locked/i.test(error.message);
7089
7665
  }
7090
7666
  async function withDatabaseSnapshot(databasePath, callback) {
7091
- const snapshotDir = await mkdtemp(join4(tmpdir(), "slopmeter-opencode-"));
7092
- const snapshotPath = join4(snapshotDir, "opencode.db");
7093
- await copyFile(databasePath, snapshotPath);
7667
+ const snapshotDir = await mkdtemp2(join5(tmpdir2(), "slopmeter-opencode-"));
7668
+ const snapshotPath = join5(snapshotDir, "opencode.db");
7669
+ await copyFile2(databasePath, snapshotPath);
7094
7670
  for (const suffix of ["-shm", "-wal"]) {
7095
7671
  const companionPath = `${databasePath}${suffix}`;
7096
- if (!existsSync2(companionPath)) {
7672
+ if (!existsSync4(companionPath)) {
7097
7673
  continue;
7098
7674
  }
7099
- await copyFile(companionPath, `${snapshotPath}${suffix}`);
7675
+ await copyFile2(companionPath, `${snapshotPath}${suffix}`);
7100
7676
  }
7101
7677
  try {
7102
7678
  return await callback(snapshotPath);
7103
7679
  } finally {
7104
- await rm(snapshotDir, { recursive: true, force: true });
7680
+ await rm2(snapshotDir, { recursive: true, force: true });
7105
7681
  }
7106
7682
  }
7107
7683
  async function iterateOpenCodeDatabaseMessages(databasePath, onMessage) {
@@ -7130,7 +7706,7 @@ async function loadOpenCodeDatabaseMessages(databasePath, onMessage) {
7130
7706
  try {
7131
7707
  await iterateOpenCodeDatabaseMessages(databasePath, onMessage);
7132
7708
  } catch (error) {
7133
- if (!isSqliteLockedError(error)) {
7709
+ if (!isSqliteLockedError2(error)) {
7134
7710
  throw error;
7135
7711
  }
7136
7712
  await withDatabaseSnapshot(databasePath, async (snapshotPath) => {
@@ -7202,7 +7778,152 @@ async function loadOpenCodeRows(start, end) {
7202
7778
  );
7203
7779
  }
7204
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
+
7205
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
+ }
7206
7927
  function mergeProviderUsage(rowsByProvider, end) {
7207
7928
  const summaries = providerIds.map((provider) => rowsByProvider[provider]).filter((summary) => summary !== null);
7208
7929
  if (summaries.length === 0) {
@@ -7219,11 +7940,34 @@ async function aggregateUsage({
7219
7940
  const rowsByProvider = {
7220
7941
  claude: null,
7221
7942
  codex: null,
7222
- opencode: null
7943
+ cursor: null,
7944
+ opencode: null,
7945
+ pi: null
7223
7946
  };
7224
7947
  const warnings = [];
7225
7948
  for (const provider of providersToLoad) {
7226
- const summary = provider === "claude" ? await loadClaudeRows(start, end) : provider === "codex" ? await loadCodexRows(start, end, warnings) : 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
+ }
7227
7971
  rowsByProvider[provider] = hasUsage(summary) ? summary : null;
7228
7972
  }
7229
7973
  return { rowsByProvider, warnings };
@@ -7239,13 +7983,15 @@ var HELP_TEXT = `slopmeter
7239
7983
  Generate rolling 1-year usage heatmap image(s) (today is the latest day).
7240
7984
 
7241
7985
  Usage:
7242
- slopmeter [--all] [--claude] [--codex] [--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]
7243
7987
 
7244
7988
  Options:
7245
7989
  --all Render one merged graph for all providers
7246
7990
  --claude Render Claude Code graph
7247
7991
  --codex Render Codex graph
7992
+ --cursor Render Cursor graph
7248
7993
  --opencode Render Open Code graph
7994
+ --pi Render Pi Coding Agent graph
7249
7995
  --dark Render with the dark theme
7250
7996
  -f, --format Output format: png, svg, or json (default: png)
7251
7997
  -o, --output Output file path (default: ./heatmap-last-year.png)
@@ -7265,7 +8011,9 @@ function validateArgs(values) {
7265
8011
  all: ow.boolean,
7266
8012
  claude: ow.boolean,
7267
8013
  codex: ow.boolean,
7268
- opencode: ow.boolean
8014
+ cursor: ow.boolean,
8015
+ opencode: ow.boolean,
8016
+ pi: ow.boolean
7269
8017
  })
7270
8018
  );
7271
8019
  }
@@ -7320,33 +8068,52 @@ function getDateWindow() {
7320
8068
  end.setHours(23, 59, 59, 999);
7321
8069
  return { start, end };
7322
8070
  }
7323
- function printProviderAvailability(rowsByProvider, providers) {
8071
+ function printProviderAvailability(availabilityByProvider, providers) {
7324
8072
  for (const provider of providers) {
7325
- const found = rowsByProvider[provider] ? "found" : "not found";
7326
- process.stdout.write(`${providerStatusLabel[provider]} ${found}
8073
+ const status = availabilityByProvider[provider] ? "available" : "not available";
8074
+ process.stdout.write(`${providerStatusLabel[provider]} ${status}
7327
8075
  `);
7328
8076
  }
7329
8077
  }
7330
8078
  function getRequestedProviders(values) {
7331
8079
  return providerIds.filter((id) => values[id]);
7332
8080
  }
7333
- function getOutputProviders(values, rowsByProvider, end) {
8081
+ function getOutputProviders(values, availabilityByProvider, rowsByProvider, end) {
7334
8082
  if (!values.all) {
7335
- return selectProvidersToRender(rowsByProvider, getRequestedProviders(values));
8083
+ return selectProvidersToRender(
8084
+ availabilityByProvider,
8085
+ rowsByProvider,
8086
+ getRequestedProviders(values)
8087
+ );
7336
8088
  }
7337
8089
  const merged = mergeProviderUsage(rowsByProvider, end);
7338
8090
  if (!merged) {
7339
- throw new Error(
7340
- "No usage data found for Claude code, Codex, or Open code."
7341
- );
8091
+ throw new Error("No usage data found for any provider.");
7342
8092
  }
7343
8093
  return [merged];
7344
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
+ }
7345
8111
  function getMergedProviderTitle(rowsByProvider) {
7346
8112
  return providerIds.filter((provider) => rowsByProvider[provider] !== null).map((provider) => heatmapThemes[provider].title).join(" / ");
7347
8113
  }
7348
- function selectProvidersToRender(rowsByProvider, requested) {
7349
- 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]);
7350
8117
  if (requested.length > 0 && providersToRender.length < requested.length) {
7351
8118
  const missing = requested.filter((provider) => !rowsByProvider[provider]);
7352
8119
  throw new Error(
@@ -7354,8 +8121,18 @@ function selectProvidersToRender(rowsByProvider, requested) {
7354
8121
  );
7355
8122
  }
7356
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
+ }
7357
8134
  throw new Error(
7358
- "No usage data found for Claude code, Codex, or Open code."
8135
+ "No usage data found for any provider."
7359
8136
  );
7360
8137
  }
7361
8138
  return providersToRender.map((provider) => rowsByProvider[provider]);
@@ -7388,7 +8165,9 @@ async function main() {
7388
8165
  all: { type: "boolean", default: false },
7389
8166
  claude: { type: "boolean", default: false },
7390
8167
  codex: { type: "boolean", default: false },
7391
- opencode: { type: "boolean", default: false }
8168
+ cursor: { type: "boolean", default: false },
8169
+ opencode: { type: "boolean", default: false },
8170
+ pi: { type: "boolean", default: false }
7392
8171
  },
7393
8172
  allowPositionals: false
7394
8173
  });
@@ -7408,6 +8187,7 @@ async function main() {
7408
8187
  const format = inferFormat(values.format, values.output);
7409
8188
  const requestedProviders = values.all ? providerIds : getRequestedProviders(values);
7410
8189
  const inspectedProviders = requestedProviders.length > 0 ? requestedProviders : providerIds;
8190
+ const availabilityByProvider = await getProviderAvailability(inspectedProviders);
7411
8191
  const { rowsByProvider, warnings } = await aggregateUsage({
7412
8192
  start,
7413
8193
  end,
@@ -7418,13 +8198,14 @@ async function main() {
7418
8198
  process.stderr.write(`${warning}
7419
8199
  `);
7420
8200
  }
7421
- printProviderAvailability(rowsByProvider, inspectedProviders);
8201
+ printProviderAvailability(availabilityByProvider, inspectedProviders);
7422
8202
  const exportProviders = getOutputProviders(
7423
8203
  values,
8204
+ availabilityByProvider,
7424
8205
  rowsByProvider,
7425
8206
  end
7426
8207
  );
7427
- const outputPath = resolve4(
8208
+ const outputPath = resolve6(
7428
8209
  values.output ?? `./heatmap-last-year.${format}`
7429
8210
  );
7430
8211
  mkdirSync(dirname(outputPath), { recursive: true });