tokely 0.5.2 → 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,13 +24,13 @@ tokely
24
24
  ## Usage
25
25
 
26
26
  ```bash
27
- tokely [--all] [--today] [--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 4 providers with available data (priority: Claude Code → Codex → Cursor → Open Code)
33
+ - renders the top 5 providers with available data (priority: Claude Code → Codex → Cursor → Open Code → OpenClaw)
34
34
  - writes `./heatmap-last-year.png`
35
35
  - infers the date window as the rolling last year ending today
36
36
 
@@ -42,6 +42,7 @@ By default, the CLI:
42
42
  - `--cursor`: include only Cursor data
43
43
  - `--gemini`: include only Gemini CLI data
44
44
  - `--opencode`: include only Open Code data
45
+ - `--openclaw`: include only OpenClaw data
45
46
  - `--pi`: include only Pi Coding Agent data
46
47
  - `--all`: merge all providers into one combined graph
47
48
  - `--dark`: render the image with the dark theme
@@ -123,6 +124,7 @@ npx tokely --dark --format svg --output ./out/heatmap-dark.svg
123
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
124
125
  - Gemini CLI: `$GEMINI_CONFIG_DIR/tmp/**/chats/session-*.json` or `~/.gemini/tmp/**/chats/session-*.json`
125
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)
126
128
  - Pi Coding Agent: `$PI_CODING_AGENT_DIR/sessions` or `~/.pi/agent/sessions`
127
129
 
128
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.
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,7 +8474,7 @@ 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] [--today] [--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
@@ -8318,6 +8485,7 @@ Options:
8318
8485
  --cursor Render Cursor graph
8319
8486
  --gemini Render Gemini CLI graph
8320
8487
  --opencode Render Open Code graph
8488
+ --openclaw Render OpenClaw graph
8321
8489
  --pi Render Pi Coding Agent graph
8322
8490
  --dark Render with the dark theme
8323
8491
  -f, --format Output format: png, svg, or json (default: png)
@@ -8343,6 +8511,7 @@ function validateArgs(values) {
8343
8511
  cursor: ow.boolean,
8344
8512
  gemini: ow.boolean,
8345
8513
  opencode: ow.boolean,
8514
+ openclaw: ow.boolean,
8346
8515
  pi: ow.boolean
8347
8516
  })
8348
8517
  );
@@ -8409,7 +8578,7 @@ function getRequestedProviders(values) {
8409
8578
  return providerIds.filter((id) => values[id]);
8410
8579
  }
8411
8580
  function getMergedNoDataMessage() {
8412
- 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.";
8413
8582
  }
8414
8583
  function getRequestedMissingProvidersMessage(missing) {
8415
8584
  return `Requested provider data not found: ${missing.map((provider) => providerStatusLabel[provider]).join(", ")}`;
@@ -8441,7 +8610,7 @@ function getDefaultOutputProviderIds(rowsByProvider) {
8441
8610
  continue;
8442
8611
  }
8443
8612
  selected.push(provider);
8444
- if (selected.length === 4) {
8613
+ if (selected.length === 5) {
8445
8614
  return selected;
8446
8615
  }
8447
8616
  }
@@ -8561,6 +8730,7 @@ async function main() {
8561
8730
  cursor: { type: "boolean", default: false },
8562
8731
  gemini: { type: "boolean", default: false },
8563
8732
  opencode: { type: "boolean", default: false },
8733
+ openclaw: { type: "boolean", default: false },
8564
8734
  pi: { type: "boolean", default: false }
8565
8735
  },
8566
8736
  allowPositionals: false