slopmeter 0.4.1 → 0.5.0

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 resolve5 } 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: {
@@ -5745,7 +5774,7 @@ var heatmapThemes = {
5745
5774
  }
5746
5775
  },
5747
5776
  all: {
5748
- title: "Codex / Claude Code / Open Code",
5777
+ title: "Codex / Claude Code / Cursor / Open Code",
5749
5778
  titleCaption: "Total usage from",
5750
5779
  colors: {
5751
5780
  light: [
@@ -7007,19 +7036,468 @@ async function loadCodexRows(start, end, warnings = []) {
7007
7036
  return createUsageSummary("codex", totals, modelTotals, recentModelTotals, end);
7008
7037
  }
7009
7038
 
7039
+ // src/lib/cursor.ts
7040
+ import { existsSync as existsSync2 } from "fs";
7041
+ import { copyFile, mkdtemp, rm } from "fs/promises";
7042
+ import { homedir as homedir3, tmpdir } from "os";
7043
+ import { join as join4, resolve as resolve3 } from "path";
7044
+ import Database from "better-sqlite3";
7045
+ var CURSOR_CONFIG_DIR_ENV = "CURSOR_CONFIG_DIR";
7046
+ var CURSOR_STATE_DB_PATH_ENV = "CURSOR_STATE_DB_PATH";
7047
+ var CURSOR_WEB_BASE_URL_ENV = "CURSOR_WEB_BASE_URL";
7048
+ var CURSOR_STATE_DB_RELATIVE_PATH = join4(
7049
+ "User",
7050
+ "globalStorage",
7051
+ "state.vscdb"
7052
+ );
7053
+ var CURSOR_SESSION_COOKIE_NAME = "WorkosCursorSessionToken";
7054
+ function getCursorDefaultStateDbPath() {
7055
+ if (process.platform === "darwin") {
7056
+ return join4(
7057
+ homedir3(),
7058
+ "Library",
7059
+ "Application Support",
7060
+ "Cursor",
7061
+ CURSOR_STATE_DB_RELATIVE_PATH
7062
+ );
7063
+ }
7064
+ if (process.platform === "win32") {
7065
+ const appData = process.env.APPDATA?.trim() || join4(homedir3(), "AppData", "Roaming");
7066
+ return join4(appData, "Cursor", CURSOR_STATE_DB_RELATIVE_PATH);
7067
+ }
7068
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim() || join4(homedir3(), ".config");
7069
+ return join4(xdgConfigHome, "Cursor", CURSOR_STATE_DB_RELATIVE_PATH);
7070
+ }
7071
+ function getCursorStateDbCandidates() {
7072
+ const explicitDbPath = process.env[CURSOR_STATE_DB_PATH_ENV]?.trim();
7073
+ if (explicitDbPath) {
7074
+ return [resolve3(explicitDbPath)];
7075
+ }
7076
+ const configuredDirs = process.env[CURSOR_CONFIG_DIR_ENV]?.trim();
7077
+ if (!configuredDirs) {
7078
+ return [getCursorDefaultStateDbPath()];
7079
+ }
7080
+ return configuredDirs.split(",").map((value) => value.trim()).filter((value) => value !== "").map((value) => {
7081
+ const resolved = resolve3(value);
7082
+ return resolved.endsWith(".vscdb") ? resolved : join4(resolved, CURSOR_STATE_DB_RELATIVE_PATH);
7083
+ });
7084
+ }
7085
+ function getCursorStateDbPath() {
7086
+ const seen = /* @__PURE__ */ new Set();
7087
+ for (const candidate of getCursorStateDbCandidates()) {
7088
+ if (!seen.has(candidate) && existsSync2(candidate)) {
7089
+ return candidate;
7090
+ }
7091
+ seen.add(candidate);
7092
+ }
7093
+ return null;
7094
+ }
7095
+ function normalizeCursorDbValue(value) {
7096
+ if (typeof value === "string") {
7097
+ const trimmed = value.trim();
7098
+ return trimmed === "" ? void 0 : trimmed;
7099
+ }
7100
+ if (Buffer.isBuffer(value)) {
7101
+ const trimmed = value.toString("utf8").trim();
7102
+ return trimmed === "" ? void 0 : trimmed;
7103
+ }
7104
+ return void 0;
7105
+ }
7106
+ function readCursorAuthStateFromDatabase(databasePath) {
7107
+ const database = new Database(databasePath, {
7108
+ readonly: true,
7109
+ fileMustExist: true
7110
+ });
7111
+ try {
7112
+ const query = database.prepare(
7113
+ "SELECT value FROM ItemTable WHERE key = ? LIMIT 1"
7114
+ );
7115
+ const accessRow = query.get("cursorAuth/accessToken");
7116
+ const refreshRow = query.get("cursorAuth/refreshToken");
7117
+ return {
7118
+ accessToken: normalizeCursorDbValue(accessRow?.value),
7119
+ refreshToken: normalizeCursorDbValue(refreshRow?.value)
7120
+ };
7121
+ } finally {
7122
+ database.close();
7123
+ }
7124
+ }
7125
+ function isSqliteLockedError(error) {
7126
+ return error instanceof Error && /database is locked/i.test(error.message);
7127
+ }
7128
+ async function withCursorStateSnapshot(databasePath, callback) {
7129
+ const snapshotDir = await mkdtemp(join4(tmpdir(), "slopmeter-cursor-"));
7130
+ const snapshotPath = join4(snapshotDir, "state.vscdb");
7131
+ await copyFile(databasePath, snapshotPath);
7132
+ for (const suffix of ["-shm", "-wal"]) {
7133
+ const companionPath = `${databasePath}${suffix}`;
7134
+ if (!existsSync2(companionPath)) {
7135
+ continue;
7136
+ }
7137
+ await copyFile(companionPath, `${snapshotPath}${suffix}`);
7138
+ }
7139
+ try {
7140
+ return await callback(snapshotPath);
7141
+ } finally {
7142
+ await rm(snapshotDir, { recursive: true, force: true });
7143
+ }
7144
+ }
7145
+ async function readCursorAuthState(databasePath) {
7146
+ try {
7147
+ return readCursorAuthStateFromDatabase(databasePath);
7148
+ } catch (error) {
7149
+ if (!isSqliteLockedError(error)) {
7150
+ throw error;
7151
+ }
7152
+ return withCursorStateSnapshot(
7153
+ databasePath,
7154
+ async (snapshotPath) => readCursorAuthStateFromDatabase(snapshotPath)
7155
+ );
7156
+ }
7157
+ }
7158
+ function decodeJwtPayload(token) {
7159
+ const encodedPayload = token.split(".")[1];
7160
+ if (!encodedPayload) {
7161
+ return null;
7162
+ }
7163
+ const base64 = encodedPayload.replace(/-/g, "+").replace(/_/g, "/");
7164
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
7165
+ try {
7166
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
7167
+ } catch {
7168
+ return null;
7169
+ }
7170
+ }
7171
+ function getCursorWebBaseUrl() {
7172
+ return (process.env[CURSOR_WEB_BASE_URL_ENV]?.trim() || "https://cursor.com").replace(/\/+$/, "");
7173
+ }
7174
+ function buildCookieHeaderValue(cookieValue) {
7175
+ return `${CURSOR_SESSION_COOKIE_NAME}=${cookieValue}`;
7176
+ }
7177
+ function getCursorFetchAttempts(accessToken) {
7178
+ const attempts = [];
7179
+ const seen = /* @__PURE__ */ new Set();
7180
+ const subject = decodeJwtPayload(accessToken)?.sub?.trim();
7181
+ const cookieValues = [accessToken];
7182
+ if (subject) {
7183
+ cookieValues.push(`${subject}::${accessToken}`);
7184
+ }
7185
+ const pushAttempt = (label, headers) => {
7186
+ const signature = JSON.stringify({
7187
+ label,
7188
+ headers: Object.entries(headers).sort(
7189
+ ([left], [right]) => left.localeCompare(right)
7190
+ )
7191
+ });
7192
+ if (seen.has(signature)) {
7193
+ return;
7194
+ }
7195
+ seen.add(signature);
7196
+ attempts.push({ label, headers });
7197
+ };
7198
+ pushAttempt("bearer", {
7199
+ Authorization: `Bearer ${accessToken}`
7200
+ });
7201
+ for (const cookieValue of cookieValues) {
7202
+ pushAttempt("cookie", {
7203
+ Cookie: buildCookieHeaderValue(cookieValue)
7204
+ });
7205
+ pushAttempt("cookie-encoded", {
7206
+ Cookie: buildCookieHeaderValue(encodeURIComponent(cookieValue))
7207
+ });
7208
+ pushAttempt("bearer+cookie", {
7209
+ Authorization: `Bearer ${accessToken}`,
7210
+ Cookie: buildCookieHeaderValue(cookieValue)
7211
+ });
7212
+ pushAttempt("bearer+cookie-encoded", {
7213
+ Authorization: `Bearer ${accessToken}`,
7214
+ Cookie: buildCookieHeaderValue(encodeURIComponent(cookieValue))
7215
+ });
7216
+ }
7217
+ return attempts;
7218
+ }
7219
+ async function fetchCursorUsageCsv(accessToken) {
7220
+ const url = new URL(
7221
+ "/api/dashboard/export-usage-events-csv?strategy=tokens",
7222
+ getCursorWebBaseUrl()
7223
+ );
7224
+ const failures = [];
7225
+ for (const attempt of getCursorFetchAttempts(accessToken)) {
7226
+ const response = await fetch(url, {
7227
+ headers: {
7228
+ Accept: "text/csv,text/plain;q=0.9,*/*;q=0.8",
7229
+ ...attempt.headers
7230
+ }
7231
+ });
7232
+ if (response.ok) {
7233
+ return response;
7234
+ }
7235
+ failures.push({
7236
+ label: attempt.label,
7237
+ status: response.status,
7238
+ statusText: response.statusText,
7239
+ body: (await response.text()).trim().slice(0, 200)
7240
+ });
7241
+ }
7242
+ const summary = failures.map((failure) => {
7243
+ const statusLine = `${failure.label}: ${failure.status} ${failure.statusText}`.trim();
7244
+ return failure.body ? `${statusLine} (${failure.body})` : statusLine;
7245
+ }).join("; ");
7246
+ throw new Error(
7247
+ `Failed to authenticate Cursor usage export with local auth state from ${url.origin}. ${summary}`
7248
+ );
7249
+ }
7250
+ function parseCsvLine(line) {
7251
+ const values = [];
7252
+ let current = "";
7253
+ let inQuotes = false;
7254
+ for (let index = 0; index < line.length; index += 1) {
7255
+ const char = line[index];
7256
+ if (char === '"') {
7257
+ if (inQuotes && line[index + 1] === '"') {
7258
+ current += '"';
7259
+ index += 1;
7260
+ continue;
7261
+ }
7262
+ inQuotes = !inQuotes;
7263
+ continue;
7264
+ }
7265
+ if (char === "," && !inQuotes) {
7266
+ values.push(current);
7267
+ current = "";
7268
+ continue;
7269
+ }
7270
+ current += char;
7271
+ }
7272
+ values.push(current);
7273
+ return values;
7274
+ }
7275
+ function createCursorCsvRow(headers, values) {
7276
+ const row = {};
7277
+ headers.forEach((header, index) => {
7278
+ row[header] = values[index];
7279
+ });
7280
+ return row;
7281
+ }
7282
+ function processCursorCsvLines(lines, onRow) {
7283
+ let headers = null;
7284
+ for (const rawLine of lines) {
7285
+ const line = rawLine.trim();
7286
+ if (line === "") {
7287
+ continue;
7288
+ }
7289
+ const values = parseCsvLine(line);
7290
+ if (!headers) {
7291
+ headers = values;
7292
+ continue;
7293
+ }
7294
+ onRow(createCursorCsvRow(headers, values));
7295
+ }
7296
+ }
7297
+ function processCursorUsageCsvText(content, onRow) {
7298
+ processCursorCsvLines(content.split(/\r?\n/), onRow);
7299
+ }
7300
+ async function processCursorUsageCsvStream(response, onRow) {
7301
+ if (!response.body) {
7302
+ processCursorUsageCsvText(await response.text(), onRow);
7303
+ return;
7304
+ }
7305
+ let headers = null;
7306
+ let currentField = "";
7307
+ let currentRow = [];
7308
+ let inQuotes = false;
7309
+ let pendingQuote = false;
7310
+ let sawCarriageReturn = false;
7311
+ const decoder = new TextDecoder();
7312
+ const emitField = () => {
7313
+ currentRow.push(currentField);
7314
+ currentField = "";
7315
+ };
7316
+ const emitRow = () => {
7317
+ emitField();
7318
+ if (currentRow.every((value) => value.trim() === "")) {
7319
+ currentRow = [];
7320
+ return;
7321
+ }
7322
+ if (!headers) {
7323
+ headers = currentRow;
7324
+ currentRow = [];
7325
+ return;
7326
+ }
7327
+ onRow(createCursorCsvRow(headers, currentRow));
7328
+ currentRow = [];
7329
+ };
7330
+ const processChunk = (chunk) => {
7331
+ for (const char of chunk) {
7332
+ let shouldReprocess = true;
7333
+ while (shouldReprocess) {
7334
+ shouldReprocess = false;
7335
+ if (sawCarriageReturn) {
7336
+ sawCarriageReturn = false;
7337
+ if (char === "\n") {
7338
+ break;
7339
+ }
7340
+ }
7341
+ if (pendingQuote) {
7342
+ pendingQuote = false;
7343
+ if (char === '"') {
7344
+ currentField += '"';
7345
+ break;
7346
+ }
7347
+ inQuotes = false;
7348
+ shouldReprocess = true;
7349
+ continue;
7350
+ }
7351
+ if (inQuotes) {
7352
+ if (char === '"') {
7353
+ pendingQuote = true;
7354
+ } else {
7355
+ currentField += char;
7356
+ }
7357
+ break;
7358
+ }
7359
+ if (char === '"') {
7360
+ inQuotes = true;
7361
+ break;
7362
+ }
7363
+ if (char === ",") {
7364
+ emitField();
7365
+ break;
7366
+ }
7367
+ if (char === "\n") {
7368
+ emitRow();
7369
+ break;
7370
+ }
7371
+ if (char === "\r") {
7372
+ emitRow();
7373
+ sawCarriageReturn = true;
7374
+ break;
7375
+ }
7376
+ currentField += char;
7377
+ break;
7378
+ }
7379
+ }
7380
+ };
7381
+ const reader = response.body.getReader();
7382
+ try {
7383
+ for (; ; ) {
7384
+ const { done, value } = await reader.read();
7385
+ if (done) {
7386
+ break;
7387
+ }
7388
+ processChunk(decoder.decode(value, { stream: true }));
7389
+ }
7390
+ } finally {
7391
+ reader.releaseLock();
7392
+ }
7393
+ processChunk(decoder.decode());
7394
+ if (pendingQuote) {
7395
+ pendingQuote = false;
7396
+ inQuotes = false;
7397
+ }
7398
+ if (currentField !== "" || currentRow.length > 0) {
7399
+ emitRow();
7400
+ }
7401
+ }
7402
+ function addCursorUsageRow(row, start, end, recentStart, totals, modelTotals, recentModelTotals) {
7403
+ const date = parseCursorDate(row.Date);
7404
+ const rawModel = row.Model?.trim();
7405
+ const tokenTotals = createCursorTokenTotals(row);
7406
+ if (!date || !rawModel || !tokenTotals) {
7407
+ return;
7408
+ }
7409
+ if (date < start || date > end) {
7410
+ return;
7411
+ }
7412
+ const modelName = normalizeModelName(rawModel);
7413
+ addDailyTokenTotals(totals, date, tokenTotals, modelName);
7414
+ addModelTokenTotals(modelTotals, modelName, tokenTotals);
7415
+ if (date >= recentStart) {
7416
+ addModelTokenTotals(recentModelTotals, modelName, tokenTotals);
7417
+ }
7418
+ }
7419
+ function parseCursorDate(value) {
7420
+ const trimmed = value?.trim();
7421
+ if (!trimmed) {
7422
+ return null;
7423
+ }
7424
+ if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
7425
+ return /* @__PURE__ */ new Date(`${trimmed}T00:00:00`);
7426
+ }
7427
+ const parsed = new Date(trimmed);
7428
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
7429
+ }
7430
+ function parseCursorNumber(value) {
7431
+ const numeric = Number(value?.replaceAll(",", "").trim() ?? "");
7432
+ if (!Number.isFinite(numeric) || numeric <= 0) {
7433
+ return null;
7434
+ }
7435
+ return Math.round(numeric);
7436
+ }
7437
+ function createCursorTokenTotals(row) {
7438
+ const total = parseCursorNumber(row["Total Tokens"]) ?? parseCursorNumber(row.Tokens);
7439
+ if (!total) {
7440
+ return null;
7441
+ }
7442
+ const inputWithCacheWrite = parseCursorNumber(row["Input (w/ Cache Write)"]) ?? 0;
7443
+ const inputWithoutCacheWrite = parseCursorNumber(row["Input (w/o Cache Write)"]) ?? 0;
7444
+ const cacheInput = parseCursorNumber(row["Cache Read"]) ?? 0;
7445
+ const outputTokens = parseCursorNumber(row["Output Tokens"]) ?? 0;
7446
+ return {
7447
+ input: inputWithCacheWrite + inputWithoutCacheWrite + cacheInput,
7448
+ output: outputTokens,
7449
+ cache: { input: cacheInput, output: inputWithCacheWrite },
7450
+ total
7451
+ };
7452
+ }
7453
+ async function loadCursorRows(start, end) {
7454
+ const databasePath = getCursorStateDbPath();
7455
+ const totals = /* @__PURE__ */ new Map();
7456
+ const modelTotals = /* @__PURE__ */ new Map();
7457
+ const recentModelTotals = /* @__PURE__ */ new Map();
7458
+ if (!databasePath) {
7459
+ return createUsageSummary("cursor", totals, modelTotals, recentModelTotals, end);
7460
+ }
7461
+ const authState = await readCursorAuthState(databasePath);
7462
+ if (!authState.accessToken) {
7463
+ return createUsageSummary("cursor", totals, modelTotals, recentModelTotals, end);
7464
+ }
7465
+ const recentStart = getRecentWindowStart(end, 30);
7466
+ const response = await fetchCursorUsageCsv(authState.accessToken);
7467
+ return summarizeCursorUsageCsv(response, start, end, recentStart);
7468
+ }
7469
+ async function summarizeCursorUsageCsv(response, start, end, recentStart = getRecentWindowStart(end, 30)) {
7470
+ const totals = /* @__PURE__ */ new Map();
7471
+ const modelTotals = /* @__PURE__ */ new Map();
7472
+ const recentModelTotals = /* @__PURE__ */ new Map();
7473
+ await processCursorUsageCsvStream(response, (row) => {
7474
+ addCursorUsageRow(
7475
+ row,
7476
+ start,
7477
+ end,
7478
+ recentStart,
7479
+ totals,
7480
+ modelTotals,
7481
+ recentModelTotals
7482
+ );
7483
+ });
7484
+ return createUsageSummary("cursor", totals, modelTotals, recentModelTotals, end);
7485
+ }
7486
+
7010
7487
  // src/lib/interfaces.ts
7011
- var providerIds = ["claude", "codex", "opencode"];
7488
+ var providerIds = ["claude", "codex", "cursor", "opencode"];
7012
7489
  var providerStatusLabel = {
7013
7490
  claude: "Claude code",
7014
7491
  codex: "Codex",
7492
+ cursor: "Cursor",
7015
7493
  opencode: "Open Code"
7016
7494
  };
7017
7495
 
7018
7496
  // 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";
7497
+ import { existsSync as existsSync3 } from "fs";
7498
+ import { copyFile as copyFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
7499
+ import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
7500
+ import { join as join5, resolve as resolve4 } from "path";
7023
7501
  function sumOpenCodeTokens(tokens) {
7024
7502
  const cacheInput = tokens?.cache?.read ?? 0;
7025
7503
  const cacheOutput = tokens?.cache?.write ?? 0;
@@ -7036,16 +7514,16 @@ async function parseOpenCodeFile(filePath) {
7036
7514
  return readJsonDocument(filePath);
7037
7515
  }
7038
7516
  function getOpenCodeBaseDir() {
7039
- const baseDir = process.env.OPENCODE_DATA_DIR?.trim() ? resolve3(process.env.OPENCODE_DATA_DIR) : join4(homedir3(), ".local", "share", "opencode");
7517
+ const baseDir = process.env.OPENCODE_DATA_DIR?.trim() ? resolve4(process.env.OPENCODE_DATA_DIR) : join5(homedir4(), ".local", "share", "opencode");
7040
7518
  return baseDir;
7041
7519
  }
7042
7520
  async function getOpenCodeSource() {
7043
7521
  const baseDir = getOpenCodeBaseDir();
7044
- const databasePath = join4(baseDir, "opencode.db");
7045
- if (existsSync2(databasePath)) {
7522
+ const databasePath = join5(baseDir, "opencode.db");
7523
+ if (existsSync3(databasePath)) {
7046
7524
  return { kind: "database", path: databasePath };
7047
7525
  }
7048
- const messagesDir = join4(baseDir, "storage", "message");
7526
+ const messagesDir = join5(baseDir, "storage", "message");
7049
7527
  return { kind: "legacy", files: await listFilesRecursive(messagesDir, ".json") };
7050
7528
  }
7051
7529
  async function loadSqliteModule() {
@@ -7084,24 +7562,24 @@ function parseOpenCodeMessageData(rowId, sourceLabel, content) {
7084
7562
  id: message.id || rowId
7085
7563
  };
7086
7564
  }
7087
- function isSqliteLockedError(error) {
7565
+ function isSqliteLockedError2(error) {
7088
7566
  return error instanceof Error && /database is locked/i.test(error.message);
7089
7567
  }
7090
7568
  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);
7569
+ const snapshotDir = await mkdtemp2(join5(tmpdir2(), "slopmeter-opencode-"));
7570
+ const snapshotPath = join5(snapshotDir, "opencode.db");
7571
+ await copyFile2(databasePath, snapshotPath);
7094
7572
  for (const suffix of ["-shm", "-wal"]) {
7095
7573
  const companionPath = `${databasePath}${suffix}`;
7096
- if (!existsSync2(companionPath)) {
7574
+ if (!existsSync3(companionPath)) {
7097
7575
  continue;
7098
7576
  }
7099
- await copyFile(companionPath, `${snapshotPath}${suffix}`);
7577
+ await copyFile2(companionPath, `${snapshotPath}${suffix}`);
7100
7578
  }
7101
7579
  try {
7102
7580
  return await callback(snapshotPath);
7103
7581
  } finally {
7104
- await rm(snapshotDir, { recursive: true, force: true });
7582
+ await rm2(snapshotDir, { recursive: true, force: true });
7105
7583
  }
7106
7584
  }
7107
7585
  async function iterateOpenCodeDatabaseMessages(databasePath, onMessage) {
@@ -7130,7 +7608,7 @@ async function loadOpenCodeDatabaseMessages(databasePath, onMessage) {
7130
7608
  try {
7131
7609
  await iterateOpenCodeDatabaseMessages(databasePath, onMessage);
7132
7610
  } catch (error) {
7133
- if (!isSqliteLockedError(error)) {
7611
+ if (!isSqliteLockedError2(error)) {
7134
7612
  throw error;
7135
7613
  }
7136
7614
  await withDatabaseSnapshot(databasePath, async (snapshotPath) => {
@@ -7219,11 +7697,12 @@ async function aggregateUsage({
7219
7697
  const rowsByProvider = {
7220
7698
  claude: null,
7221
7699
  codex: null,
7700
+ cursor: null,
7222
7701
  opencode: null
7223
7702
  };
7224
7703
  const warnings = [];
7225
7704
  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);
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);
7227
7706
  rowsByProvider[provider] = hasUsage(summary) ? summary : null;
7228
7707
  }
7229
7708
  return { rowsByProvider, warnings };
@@ -7239,12 +7718,13 @@ var HELP_TEXT = `slopmeter
7239
7718
  Generate rolling 1-year usage heatmap image(s) (today is the latest day).
7240
7719
 
7241
7720
  Usage:
7242
- slopmeter [--all] [--claude] [--codex] [--opencode] [--dark] [--format png|svg|json] [--output ./heatmap-last-year.png]
7721
+ slopmeter [--all] [--claude] [--codex] [--cursor] [--opencode] [--dark] [--format png|svg|json] [--output ./heatmap-last-year.png]
7243
7722
 
7244
7723
  Options:
7245
7724
  --all Render one merged graph for all providers
7246
7725
  --claude Render Claude Code graph
7247
7726
  --codex Render Codex graph
7727
+ --cursor Render Cursor graph
7248
7728
  --opencode Render Open Code graph
7249
7729
  --dark Render with the dark theme
7250
7730
  -f, --format Output format: png, svg, or json (default: png)
@@ -7265,6 +7745,7 @@ function validateArgs(values) {
7265
7745
  all: ow.boolean,
7266
7746
  claude: ow.boolean,
7267
7747
  codex: ow.boolean,
7748
+ cursor: ow.boolean,
7268
7749
  opencode: ow.boolean
7269
7750
  })
7270
7751
  );
@@ -7337,7 +7818,7 @@ function getOutputProviders(values, rowsByProvider, end) {
7337
7818
  const merged = mergeProviderUsage(rowsByProvider, end);
7338
7819
  if (!merged) {
7339
7820
  throw new Error(
7340
- "No usage data found for Claude code, Codex, or Open code."
7821
+ "No usage data found for Claude code, Codex, Cursor, or Open code."
7341
7822
  );
7342
7823
  }
7343
7824
  return [merged];
@@ -7355,7 +7836,7 @@ function selectProvidersToRender(rowsByProvider, requested) {
7355
7836
  }
7356
7837
  if (providersToRender.length === 0) {
7357
7838
  throw new Error(
7358
- "No usage data found for Claude code, Codex, or Open code."
7839
+ "No usage data found for Claude code, Codex, Cursor, or Open code."
7359
7840
  );
7360
7841
  }
7361
7842
  return providersToRender.map((provider) => rowsByProvider[provider]);
@@ -7388,6 +7869,7 @@ async function main() {
7388
7869
  all: { type: "boolean", default: false },
7389
7870
  claude: { type: "boolean", default: false },
7390
7871
  codex: { type: "boolean", default: false },
7872
+ cursor: { type: "boolean", default: false },
7391
7873
  opencode: { type: "boolean", default: false }
7392
7874
  },
7393
7875
  allowPositionals: false
@@ -7424,7 +7906,7 @@ async function main() {
7424
7906
  rowsByProvider,
7425
7907
  end
7426
7908
  );
7427
- const outputPath = resolve4(
7909
+ const outputPath = resolve5(
7428
7910
  values.output ?? `./heatmap-last-year.${format}`
7429
7911
  );
7430
7912
  mkdirSync(dirname(outputPath), { recursive: true });