tokely 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,22 +24,25 @@ tokely
24
24
  ## Usage
25
25
 
26
26
  ```bash
27
- tokely [--all] [--claude] [--codex] [--cursor] [--gemini] [--opencode] [--pi] [--dark] [--format png|svg|json] [--output ./heatmap-last-year.png]
27
+ tokely [--all] [--today] [--claude] [--codex] [--cursor] [--gemini] [--opencode] [--openclaw] [--pi] [--dark] [--format png|svg|json] [--output ./heatmap-last-year.png]
28
28
  ```
29
29
 
30
30
  By default, the CLI:
31
31
 
32
32
  - scans all supported providers
33
+ - renders the top 5 providers with available data (priority: Claude Code → Codex → Cursor → Open Code → OpenClaw)
33
34
  - writes `./heatmap-last-year.png`
34
35
  - infers the date window as the rolling last year ending today
35
36
 
36
37
  ## Options
37
38
 
39
+ - `--today`: show today's usage stats in terminal
38
40
  - `--claude`: include only Claude Code data
39
41
  - `--codex`: include only Codex data
40
42
  - `--cursor`: include only Cursor data
41
43
  - `--gemini`: include only Gemini CLI data
42
44
  - `--opencode`: include only Open Code data
45
+ - `--openclaw`: include only OpenClaw data
43
46
  - `--pi`: include only Pi Coding Agent data
44
47
  - `--all`: merge all providers into one combined graph
45
48
  - `--dark`: render the image with the dark theme
@@ -121,6 +124,7 @@ npx tokely --dark --format svg --output ./out/heatmap-dark.svg
121
124
  - Cursor: reads `cursorAuth/accessToken` and `cursorAuth/refreshToken` from `$CURSOR_STATE_DB_PATH`, `$CURSOR_CONFIG_DIR/User/globalStorage/state.vscdb`, `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb` (macOS), `%APPDATA%/Cursor/User/globalStorage/state.vscdb` (Windows), or `~/.config/Cursor/User/globalStorage/state.vscdb` (Linux), then loads usage from Cursor's CSV export endpoint
122
125
  - Gemini CLI: `$GEMINI_CONFIG_DIR/tmp/**/chats/session-*.json` or `~/.gemini/tmp/**/chats/session-*.json`
123
126
  - Open Code: prefers `$OPENCODE_DATA_DIR/opencode.db` or `~/.local/share/opencode/opencode.db`, and falls back to `$OPENCODE_DATA_DIR/storage/message` or `~/.local/share/opencode/storage/message`
127
+ - OpenClaw: `$HOME/.openclaw/agents`, `$HOME/.clawdbot/agents`, `$HOME/.moltbot/agents`, or `$HOME/.moldbot/agents` (reads `*.jsonl` session transcripts)
124
128
  - Pi Coding Agent: `$PI_CODING_AGENT_DIR/sessions` or `~/.pi/agent/sessions`
125
129
 
126
130
  When Claude Code falls back to `stats-cache.json`, the daily input/output/cache split is reconstructed from Claude's cached model totals because the older layout does not keep per-request usage logs.
@@ -128,8 +132,9 @@ When Claude Code falls back to `history.jsonl`, those days are rendered as activ
128
132
 
129
133
  ## Exit behavior
130
134
 
131
- - If no provider flags are passed, `tokely` renders every provider with available data.
132
- - If `--all` is passed, `tokely` loads all providers and renders one combined graph with merged totals, streaks, and model rankings.
135
+ - By default, renders the top 4 providers with available data.
136
+ - Priority order: Claude Code Codex Cursor Open Code Amp Gemini CLI Pi Coding Agent.
137
+ - Use `--all` to merge all providers, or specify providers explicitly.
133
138
  - Pi Coding Agent usage is derived from assistant messages in Pi session logs, grouped by the model that handled each turn.
134
139
  - If provider flags are passed and a requested provider has no data, the command exits with an error.
135
140
  - If no provider has data, the command exits with an error.
package/dist/cli.js CHANGED
@@ -5833,6 +5833,35 @@ var heatmapThemes = {
5833
5833
  ]
5834
5834
  }
5835
5835
  },
5836
+ openclaw: {
5837
+ title: "OpenClaw",
5838
+ colors: {
5839
+ light: [
5840
+ "#fef2f2",
5841
+ // red-50
5842
+ "#fecaca",
5843
+ // red-200
5844
+ "#fca5a5",
5845
+ // red-300
5846
+ "#ef4444",
5847
+ // red-500
5848
+ "#991b1b"
5849
+ // red-800
5850
+ ],
5851
+ dark: [
5852
+ "#450a0a",
5853
+ // red-950
5854
+ "#991b1b",
5855
+ // red-800
5856
+ "#dc2626",
5857
+ // red-600
5858
+ "#fca5a5",
5859
+ // red-300
5860
+ "#fecaca"
5861
+ // red-200
5862
+ ]
5863
+ }
5864
+ },
5836
5865
  pi: {
5837
5866
  title: "Pi Coding Agent",
5838
5867
  colors: {
@@ -5863,7 +5892,7 @@ var heatmapThemes = {
5863
5892
  }
5864
5893
  },
5865
5894
  all: {
5866
- title: "Amp / Claude Code / Codex / Cursor / Gemini CLI / Open Code / Pi Coding Agent",
5895
+ title: "Amp / Claude Code / Codex / Cursor / Gemini CLI / Open Code / OpenClaw / Pi Coding Agent",
5867
5896
  titleCaption: "Total usage from",
5868
5897
  colors: {
5869
5898
  light: [
@@ -7874,6 +7903,7 @@ var providerIds = [
7874
7903
  "codex",
7875
7904
  "cursor",
7876
7905
  "opencode",
7906
+ "openclaw",
7877
7907
  "amp",
7878
7908
  "gemini",
7879
7909
  "pi"
@@ -7882,7 +7912,8 @@ var defaultProviderIds = [
7882
7912
  "claude",
7883
7913
  "codex",
7884
7914
  "cursor",
7885
- "opencode"
7915
+ "opencode",
7916
+ "openclaw"
7886
7917
  ];
7887
7918
  var providerStatusLabel = {
7888
7919
  amp: "Amp",
@@ -7891,6 +7922,7 @@ var providerStatusLabel = {
7891
7922
  cursor: "Cursor",
7892
7923
  gemini: "Gemini CLI",
7893
7924
  opencode: "Open Code",
7925
+ openclaw: "OpenClaw",
7894
7926
  pi: "Pi Coding Agent"
7895
7927
  };
7896
7928
 
@@ -8088,21 +8120,149 @@ async function loadOpenCodeRows(start, end) {
8088
8120
  );
8089
8121
  }
8090
8122
 
8091
- // src/lib/pi.ts
8092
- import { existsSync as existsSync7 } from "fs";
8123
+ // src/lib/openclaw.ts
8124
+ import { existsSync as existsSync7, readFileSync, statSync } from "fs";
8125
+ import { readdirSync as readdirSync2 } from "fs";
8126
+ import { join as join8 } from "path";
8093
8127
  import { homedir as homedir7 } from "os";
8094
- import { join as join8, resolve as resolve7 } from "path";
8128
+ function scanOpenClawJsonlFiles() {
8129
+ const dirs = [
8130
+ join8(homedir7(), ".openclaw", "agents"),
8131
+ join8(homedir7(), ".clawdbot", "agents"),
8132
+ join8(homedir7(), ".moltbot", "agents"),
8133
+ join8(homedir7(), ".moldbot", "agents")
8134
+ ];
8135
+ const files = [];
8136
+ for (const dir of dirs) {
8137
+ if (!existsSync7(dir)) continue;
8138
+ scanDirRecursive(dir, files);
8139
+ }
8140
+ return files;
8141
+ }
8142
+ function scanDirRecursive(dir, files) {
8143
+ try {
8144
+ const entries = readdirSync2(dir, { withFileTypes: true });
8145
+ for (const entry of entries) {
8146
+ const fullPath = join8(dir, entry.name);
8147
+ if (entry.isDirectory()) {
8148
+ scanDirRecursive(fullPath, files);
8149
+ } else if (entry.name.endsWith(".jsonl")) {
8150
+ files.push(fullPath);
8151
+ }
8152
+ }
8153
+ } catch {
8154
+ }
8155
+ }
8156
+ function parseTranscript(filePath, start, end, totals, modelTotals, recentModelTotals) {
8157
+ const content = readFileSync(filePath, "utf-8");
8158
+ const lines = content.split("\n").filter((l) => l.trim());
8159
+ let currentModel = null;
8160
+ let currentProvider = null;
8161
+ const fileMtime = statSync(filePath).mtimeMs;
8162
+ const recentStart = getRecentWindowStart(end);
8163
+ for (const line of lines) {
8164
+ let entry;
8165
+ try {
8166
+ entry = JSON.parse(line);
8167
+ } catch {
8168
+ continue;
8169
+ }
8170
+ const type = entry.type;
8171
+ if (type === "model_change") {
8172
+ currentModel = entry.modelId ?? currentModel;
8173
+ currentProvider = entry.provider ?? currentProvider;
8174
+ continue;
8175
+ }
8176
+ if (type === "custom" && entry.customType === "model-snapshot") {
8177
+ const data = entry.data;
8178
+ if (data) {
8179
+ currentModel = data.modelId ?? currentModel;
8180
+ currentProvider = data.provider ?? currentProvider;
8181
+ }
8182
+ continue;
8183
+ }
8184
+ if (type === "message") {
8185
+ const msg = entry.message;
8186
+ if (!msg || msg.role !== "assistant") continue;
8187
+ const model = msg.model ?? currentModel;
8188
+ if (!model) continue;
8189
+ const usage = msg.usage;
8190
+ if (!usage) continue;
8191
+ const input = Math.max(usage.input ?? 0, 0);
8192
+ const output = Math.max(usage.output ?? 0, 0);
8193
+ const cacheRead = Math.max(usage.cacheRead ?? 0, 0);
8194
+ const cacheWrite = Math.max(usage.cacheWrite ?? 0, 0);
8195
+ const total = Math.max(usage.totalTokens ?? 0, 0);
8196
+ if (total <= 0 && input <= 0 && output <= 0) continue;
8197
+ const ts = msg.timestamp ?? fileMtime;
8198
+ const timestamp = new Date(ts);
8199
+ if (timestamp < start || timestamp > end) continue;
8200
+ const normalizedModel = normalizeModelName(model);
8201
+ addDailyTokenTotals(
8202
+ totals,
8203
+ timestamp,
8204
+ {
8205
+ input,
8206
+ output,
8207
+ cache: { input: cacheRead, output: cacheWrite },
8208
+ total
8209
+ },
8210
+ normalizedModel
8211
+ );
8212
+ addModelTokenTotals(modelTotals, normalizedModel, {
8213
+ input,
8214
+ output,
8215
+ cache: { input: cacheRead, output: cacheWrite },
8216
+ total
8217
+ });
8218
+ if (timestamp >= recentStart) {
8219
+ addModelTokenTotals(recentModelTotals, normalizedModel, {
8220
+ input,
8221
+ output,
8222
+ cache: { input: cacheRead, output: cacheWrite },
8223
+ total
8224
+ });
8225
+ }
8226
+ }
8227
+ }
8228
+ }
8229
+ function isOpenClawAvailable() {
8230
+ return scanOpenClawJsonlFiles().length > 0;
8231
+ }
8232
+ async function loadOpenClawRows(start, end) {
8233
+ const files = scanOpenClawJsonlFiles();
8234
+ const totals = /* @__PURE__ */ new Map();
8235
+ const modelTotals = /* @__PURE__ */ new Map();
8236
+ const recentModelTotals = /* @__PURE__ */ new Map();
8237
+ for (const file of files) {
8238
+ try {
8239
+ parseTranscript(file, start, end, totals, modelTotals, recentModelTotals);
8240
+ } catch {
8241
+ }
8242
+ }
8243
+ const daily = totalsToRows(totals);
8244
+ return {
8245
+ provider: "openclaw",
8246
+ daily,
8247
+ insights: getProviderInsights(modelTotals, recentModelTotals, daily, end)
8248
+ };
8249
+ }
8250
+
8251
+ // src/lib/pi.ts
8252
+ import { existsSync as existsSync8 } from "fs";
8253
+ import { homedir as homedir8 } from "os";
8254
+ import { join as join9, resolve as resolve7 } from "path";
8095
8255
  var PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
8096
8256
  var CLASSIFICATION_PREFIX_BYTES2 = 16 * 1024;
8097
8257
  function getPiAgentDir() {
8098
8258
  const configuredAgentDir = process.env[PI_AGENT_DIR_ENV]?.trim();
8099
- return configuredAgentDir ? resolve7(configuredAgentDir) : join8(homedir7(), ".pi", "agent");
8259
+ return configuredAgentDir ? resolve7(configuredAgentDir) : join9(homedir8(), ".pi", "agent");
8100
8260
  }
8101
8261
  async function getPiSessionFiles() {
8102
- return listFilesRecursive(join8(getPiAgentDir(), "sessions"), ".jsonl");
8262
+ return listFilesRecursive(join9(getPiAgentDir(), "sessions"), ".jsonl");
8103
8263
  }
8104
8264
  function isPiAvailable() {
8105
- return existsSync7(join8(getPiAgentDir(), "sessions"));
8265
+ return existsSync8(join9(getPiAgentDir(), "sessions"));
8106
8266
  }
8107
8267
  function classifyPiRecord(prefix) {
8108
8268
  if (prefix.includes('"type":"message"') && prefix.includes('"role":"assistant"')) {
@@ -8208,6 +8368,7 @@ function createEmptyProviderAvailability() {
8208
8368
  cursor: false,
8209
8369
  gemini: false,
8210
8370
  opencode: false,
8371
+ openclaw: false,
8211
8372
  pi: false
8212
8373
  };
8213
8374
  }
@@ -8225,6 +8386,8 @@ async function isProviderAvailable(provider) {
8225
8386
  return isGeminiAvailable();
8226
8387
  case "opencode":
8227
8388
  return isOpenCodeAvailable();
8389
+ case "openclaw":
8390
+ return isOpenClawAvailable();
8228
8391
  case "pi":
8229
8392
  return isPiAvailable();
8230
8393
  default: {
@@ -8260,6 +8423,7 @@ async function aggregateUsage({
8260
8423
  cursor: null,
8261
8424
  gemini: null,
8262
8425
  opencode: null,
8426
+ openclaw: null,
8263
8427
  pi: null
8264
8428
  };
8265
8429
  const warnings = [];
@@ -8284,6 +8448,9 @@ async function aggregateUsage({
8284
8448
  case "opencode":
8285
8449
  summary = await loadOpenCodeRows(start, end);
8286
8450
  break;
8451
+ case "openclaw":
8452
+ summary = await loadOpenClawRows(start, end);
8453
+ break;
8287
8454
  case "pi":
8288
8455
  summary = await loadPiRows(start, end);
8289
8456
  break;
@@ -8307,16 +8474,18 @@ var HELP_TEXT = `tokely
8307
8474
  Generate rolling 1-year usage heatmap image(s) (today is the latest day).
8308
8475
 
8309
8476
  Usage:
8310
- tokely [--all] [--amp] [--claude] [--codex] [--cursor] [--gemini] [--opencode] [--pi] [--dark] [--format png|svg|json] [--output ./heatmap-last-year.png]
8477
+ tokely [--all] [--today] [--amp] [--claude] [--codex] [--cursor] [--gemini] [--opencode] [--openclaw] [--pi] [--dark] [--format png|svg|json] [--output ./heatmap-last-year.png]
8311
8478
 
8312
8479
  Options:
8313
8480
  --all Render one merged graph for all providers
8481
+ --today Show today's usage stats in terminal
8314
8482
  --amp Render Amp graph
8315
8483
  --claude Render Claude Code graph
8316
8484
  --codex Render Codex graph
8317
8485
  --cursor Render Cursor graph
8318
8486
  --gemini Render Gemini CLI graph
8319
8487
  --opencode Render Open Code graph
8488
+ --openclaw Render OpenClaw graph
8320
8489
  --pi Render Pi Coding Agent graph
8321
8490
  --dark Render with the dark theme
8322
8491
  -f, --format Output format: png, svg, or json (default: png)
@@ -8335,12 +8504,14 @@ function validateArgs(values) {
8335
8504
  help: ow.boolean,
8336
8505
  dark: ow.boolean,
8337
8506
  all: ow.boolean,
8507
+ today: ow.boolean,
8338
8508
  amp: ow.boolean,
8339
8509
  claude: ow.boolean,
8340
8510
  codex: ow.boolean,
8341
8511
  cursor: ow.boolean,
8342
8512
  gemini: ow.boolean,
8343
8513
  opencode: ow.boolean,
8514
+ openclaw: ow.boolean,
8344
8515
  pi: ow.boolean
8345
8516
  })
8346
8517
  );
@@ -8407,7 +8578,7 @@ function getRequestedProviders(values) {
8407
8578
  return providerIds.filter((id) => values[id]);
8408
8579
  }
8409
8580
  function getMergedNoDataMessage() {
8410
- return "No usage data found for Amp, Claude Code, Codex, Cursor, Gemini CLI, Open Code, or Pi Coding Agent.";
8581
+ return "No usage data found for Claude Code, Codex, Cursor, Open Code, OpenClaw, Amp, Gemini CLI, or Pi Coding Agent.";
8411
8582
  }
8412
8583
  function getRequestedMissingProvidersMessage(missing) {
8413
8584
  return `Requested provider data not found: ${missing.map((provider) => providerStatusLabel[provider]).join(", ")}`;
@@ -8439,7 +8610,7 @@ function getDefaultOutputProviderIds(rowsByProvider) {
8439
8610
  continue;
8440
8611
  }
8441
8612
  selected.push(provider);
8442
- if (selected.length === 4) {
8613
+ if (selected.length === 5) {
8443
8614
  return selected;
8444
8615
  }
8445
8616
  }
@@ -8487,6 +8658,62 @@ function printRunSummary(outputPath, format, colorMode, startDate, endDate, rend
8487
8658
  `
8488
8659
  );
8489
8660
  }
8661
+ function formatTokens(value) {
8662
+ const units = [
8663
+ { size: 1e12, suffix: "T" },
8664
+ { size: 1e9, suffix: "B" },
8665
+ { size: 1e6, suffix: "M" },
8666
+ { size: 1e3, suffix: "K" }
8667
+ ];
8668
+ for (const unit of units) {
8669
+ if (value >= unit.size) {
8670
+ const scaled = value / unit.size;
8671
+ const precision = scaled >= 100 ? 0 : scaled >= 10 ? 1 : 2;
8672
+ const compact = scaled.toFixed(precision).replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1");
8673
+ return `${compact}${unit.suffix}`;
8674
+ }
8675
+ }
8676
+ return new Intl.NumberFormat("en-US").format(value);
8677
+ }
8678
+ function printTodayStats(rowsByProvider, providers) {
8679
+ const today = formatLocalDate(/* @__PURE__ */ new Date());
8680
+ const rows = [];
8681
+ for (const provider of providers) {
8682
+ const summary = rowsByProvider[provider];
8683
+ if (!summary) {
8684
+ continue;
8685
+ }
8686
+ const todayRow = summary.daily.find(
8687
+ (row) => formatLocalDate(row.date) === today
8688
+ );
8689
+ if (!todayRow || todayRow.total <= 0 && (todayRow.displayValue ?? 0) <= 0) {
8690
+ continue;
8691
+ }
8692
+ const topModel = todayRow.breakdown.length > 0 ? `${todayRow.breakdown[0].name} (${formatTokens(todayRow.breakdown[0].tokens.total)})` : "-";
8693
+ rows.push({
8694
+ provider: providerStatusLabel[provider],
8695
+ input: formatTokens(todayRow.input),
8696
+ output: formatTokens(todayRow.output),
8697
+ total: formatTokens(todayRow.total),
8698
+ topModel
8699
+ });
8700
+ }
8701
+ if (rows.length === 0) {
8702
+ process.stdout.write(`
8703
+ No usage data found for today (${today}).
8704
+ `);
8705
+ return;
8706
+ }
8707
+ const header = "| Provider | Input | Output | Total | Top Model |\n| --- | --- | --- | --- | --- |";
8708
+ const body = rows.map(
8709
+ (row) => `| ${row.provider} | ${row.input} | ${row.output} | ${row.total} | ${row.topModel} |`
8710
+ ).join("\n");
8711
+ process.stdout.write(`
8712
+ ${header}
8713
+ ${body}
8714
+
8715
+ `);
8716
+ }
8490
8717
  async function main() {
8491
8718
  let spinner;
8492
8719
  const parsed = parseArgs({
@@ -8496,12 +8723,14 @@ async function main() {
8496
8723
  help: { type: "boolean", short: "h", default: false },
8497
8724
  dark: { type: "boolean", default: false },
8498
8725
  all: { type: "boolean", default: false },
8726
+ today: { type: "boolean", default: false },
8499
8727
  amp: { type: "boolean", default: false },
8500
8728
  claude: { type: "boolean", default: false },
8501
8729
  codex: { type: "boolean", default: false },
8502
8730
  cursor: { type: "boolean", default: false },
8503
8731
  gemini: { type: "boolean", default: false },
8504
8732
  opencode: { type: "boolean", default: false },
8733
+ openclaw: { type: "boolean", default: false },
8505
8734
  pi: { type: "boolean", default: false }
8506
8735
  },
8507
8736
  allowPositionals: false
@@ -8533,7 +8762,13 @@ async function main() {
8533
8762
  process.stderr.write(`${warning}
8534
8763
  `);
8535
8764
  }
8536
- printProviderAvailability(availabilityByProvider, inspectedProviders);
8765
+ if (!values.today) {
8766
+ printProviderAvailability(availabilityByProvider, inspectedProviders);
8767
+ }
8768
+ if (values.today) {
8769
+ printTodayStats(rowsByProvider, inspectedProviders);
8770
+ return;
8771
+ }
8537
8772
  const exportProviders = getOutputProviders(
8538
8773
  values,
8539
8774
  availabilityByProvider,