tokenflexing 1.1.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/README.md +138 -0
- package/bin/tokenflexing.js +2 -0
- package/package.json +29 -0
- package/src/cli.js +1027 -0
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# tokenflex
|
|
2
|
+
|
|
3
|
+
Show off your AI token usage. Universal CLI for Claude Code, Codex, Cursor, and more.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx tokenflex@latest scan
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Scans your entire device for AI tool usage data, extracts token counts where possible, and shows everything else it detected. **Never reads prompts or completions — only usage/token blocks.**
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
| Command | What |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `tokenflex` / `tokenflex stats` | Today / 7-day / 30-day / all-time dollar + token spend per source, per model |
|
|
16
|
+
| `tokenflex scan` | Universal device inventory — measured, detected, and not-found in one view |
|
|
17
|
+
| `tokenflex flex` | Shareable ASCII card — drop in tweets, HN comments, or Slack |
|
|
18
|
+
| `tokenflex daemon` | Preview the daily auto-refresh LaunchAgent (macOS) or Task Scheduler entry (Windows) |
|
|
19
|
+
| `tokenflex daemon --install` | Install the daemon (runs `tokenflex scan` at 6:00 AM daily) |
|
|
20
|
+
| `tokenflex daemon --uninstall` | Remove the daemon |
|
|
21
|
+
| `tokenflex mcp` | Start MCP server — Claude Code, Cursor, and any MCP-capable agent can call tokenflex tools natively |
|
|
22
|
+
| `tokenflex install-hooks --apply` | Register tokenflex as an MCP server in Claude Code or Cursor settings |
|
|
23
|
+
| `tokenflex --help` | Usage |
|
|
24
|
+
|
|
25
|
+
## What `tokenflex scan` detects
|
|
26
|
+
|
|
27
|
+
| Tool | Status |
|
|
28
|
+
|---|---|
|
|
29
|
+
| **Claude Code** | ✅ Token counts + cost extracted (`~/.claude/projects/**/*.jsonl`) |
|
|
30
|
+
| **Codex CLI** | ✅ Token counts + cost extracted (`~/.codex/sessions/**/*.jsonl`) |
|
|
31
|
+
| **Cursor** | 🔍 Detected — SQLite `state.vscdb` reader planned |
|
|
32
|
+
| **Claude Desktop** | 🔍 Detected — Electron, no token events exposed |
|
|
33
|
+
| **ChatGPT Desktop** | 🔍 Detected — Electron, conversation data only |
|
|
34
|
+
| **Windsurf (Codeium)** | 🔍 Detected — structured token log not exposed yet |
|
|
35
|
+
| **Continue.dev** | 🔍 Detected — telemetry parser planned |
|
|
36
|
+
| **Aider** | 🔍 Detected — cost-line parser planned |
|
|
37
|
+
| **Cline** | 🔍 Detected — `ui_messages.json` task parser planned |
|
|
38
|
+
| **Roo Code** | 🔍 Detected — `ui_messages.json` task parser planned |
|
|
39
|
+
| **Kilo Code** | 🔍 Detected — `ui_messages.json` task parser planned |
|
|
40
|
+
| **Zed AI** | 🔍 Detected — SQLite `threads.db` reader planned |
|
|
41
|
+
| **Gemini CLI** | 🔍 Detected — `~/.gemini/tmp/*.jsonl` parser planned |
|
|
42
|
+
| **OpenCode** | 🔍 Detected — `opencode/storage/message/*.json` parser planned |
|
|
43
|
+
| **Amp** | 🔍 Detected — `amp/threads/T-*.json` parser planned |
|
|
44
|
+
| **Antigravity (Google)** | 🔍 Detected — session cache format TBD |
|
|
45
|
+
| **GitHub Copilot** | 🔍 Detected — OTEL trace extraction planned |
|
|
46
|
+
| **Goose (Block)** | 🔍 Detected — SQLite `sessions.db` reader planned |
|
|
47
|
+
| **Kiro (Amazon)** | 🔍 Detected — `~/.kiro/sessions/cli/*.json` parser planned |
|
|
48
|
+
| **Mux** | 🔍 Detected — `session-usage.json` parser planned |
|
|
49
|
+
| **OpenClaw** | 🔍 Detected — JSONL agent sessions parser planned |
|
|
50
|
+
| **Crush** | 🔍 Detected — `projects.json` parser planned |
|
|
51
|
+
| **Kimi (Moonshot)** | 🔍 Detected — `wire.jsonl` parser planned |
|
|
52
|
+
| **Hermes** | 🔍 Detected — SQLite `state.db` reader planned |
|
|
53
|
+
|
|
54
|
+
macOS, Windows, and Linux (XDG) paths covered.
|
|
55
|
+
|
|
56
|
+
## Why local files?
|
|
57
|
+
|
|
58
|
+
Provider Admin APIs (Anthropic / OpenAI) require **organization admin** keys that individual developers don't have. Local session JSONL files contain per-turn usage data for **anyone** using Claude Code or Codex CLI on any plan — no org key needed.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# one-shot
|
|
64
|
+
npx tokenflex@latest stats
|
|
65
|
+
|
|
66
|
+
# or install globally
|
|
67
|
+
npm install -g tokenflex
|
|
68
|
+
tokenflex scan
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Requires Node ≥ 18.
|
|
72
|
+
|
|
73
|
+
## MCP Server (agent-native access)
|
|
74
|
+
|
|
75
|
+
Register tokenflex as an MCP server so Claude Code, Cursor, and any MCP-capable agent can query your stats inline:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Dry-run — shows what will be written
|
|
79
|
+
npx tokenflex@latest install-hooks
|
|
80
|
+
|
|
81
|
+
# Write MCP entry to ~/.claude/settings.json (Claude Code)
|
|
82
|
+
npx tokenflex@latest install-hooks --apply
|
|
83
|
+
|
|
84
|
+
# Write to ~/.cursor/mcp.json (Cursor)
|
|
85
|
+
npx tokenflex@latest install-hooks --client cursor --apply
|
|
86
|
+
|
|
87
|
+
# Project-local (writes to .claude/settings.json in current dir)
|
|
88
|
+
npx tokenflex@latest install-hooks --apply --project
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
After restarting your editor, your agent gets three new tools:
|
|
92
|
+
|
|
93
|
+
| Tool | What it does |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `get_my_stats` | Token spend by period and model |
|
|
96
|
+
| `get_device_scan` | Which AI tools are installed + measured |
|
|
97
|
+
| `get_flex_card` | Shareable one-liner for X / HN / Slack |
|
|
98
|
+
|
|
99
|
+
Or run the MCP server directly (for custom integrations):
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx tokenflex@latest mcp
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
MCP entry format for manual config:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"mcpServers": {
|
|
110
|
+
"tokenflex": { "command": "npx", "args": ["-y", "tokenflex@latest", "mcp"], "type": "stdio" }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Daily auto-refresh
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# macOS: installs a LaunchAgent that runs tokenflex scan at 6 AM
|
|
119
|
+
tokenflex daemon --install
|
|
120
|
+
|
|
121
|
+
# Windows: creates a Task Scheduler entry
|
|
122
|
+
tokenflex daemon --install
|
|
123
|
+
|
|
124
|
+
# Remove
|
|
125
|
+
tokenflex daemon --uninstall
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Cloud sync (`tokenflex login` → push to tokenflexing.com profile) ships in v0.3.
|
|
129
|
+
|
|
130
|
+
## Privacy
|
|
131
|
+
|
|
132
|
+
- No network calls. Pure local read.
|
|
133
|
+
- Only usage blocks (token counts, model ids) are parsed — never message content.
|
|
134
|
+
- `tokenflex daemon` logs to `/tmp/tokenflex-sync.log` only.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tokenflexing",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Show off your AI token usage. CLI for Claude Code, Codex, Cursor, OpenCode and more.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tokenflexing": "bin/tokenflexing.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude-code",
|
|
19
|
+
"codex",
|
|
20
|
+
"tokens",
|
|
21
|
+
"ai-usage",
|
|
22
|
+
"tokenflexing",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"start": "node bin/tokenflexing.js"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,1027 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tokenflexing — show off your AI token usage.
|
|
3
|
+
* Universal scanner: Claude Code, Codex CLI, Cursor, Windsurf, and more.
|
|
4
|
+
* Never reads prompts or completions — only usage/token blocks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
readdirSync, statSync, readFileSync, writeFileSync,
|
|
9
|
+
existsSync, mkdirSync, unlinkSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { join, dirname } from "node:path";
|
|
12
|
+
import { homedir, userInfo } from "node:os";
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
// node:readline used for interactive prompts in commandLogin
|
|
15
|
+
import { createInterface } from "node:readline";
|
|
16
|
+
|
|
17
|
+
/* ───── Platform ───────────────────────────────────────────────── */
|
|
18
|
+
|
|
19
|
+
const IS_WIN = process.platform === "win32";
|
|
20
|
+
const IS_MAC = process.platform === "darwin";
|
|
21
|
+
const APPDATA = process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
|
|
22
|
+
const LIB = join(homedir(), "Library", "Application Support");
|
|
23
|
+
const XDG = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
24
|
+
|
|
25
|
+
/** Return mac / win / linux variant of a path array. */
|
|
26
|
+
function pf(mac, win, linux) {
|
|
27
|
+
return IS_MAC ? mac : IS_WIN ? (win ?? mac) : (linux ?? mac);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Path to a VSCode-family extension's globalStorage directory. */
|
|
31
|
+
function vscodeExt(extId) {
|
|
32
|
+
return pf(
|
|
33
|
+
join(LIB, "Code", "User", "globalStorage", extId),
|
|
34
|
+
join(APPDATA,"Code", "User", "globalStorage", extId),
|
|
35
|
+
join(homedir(), ".config", "Code", "User", "globalStorage", extId)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ───── Cloud sync config ──────────────────────────────────────── */
|
|
40
|
+
|
|
41
|
+
const SITE = process.env.TOKENFLEX_SITE ?? "https://tokenflexing.vercel.app";
|
|
42
|
+
|
|
43
|
+
/** Where the CLI token is persisted locally. */
|
|
44
|
+
function tokenPath() {
|
|
45
|
+
const base = process.env.XDG_CONFIG_HOME
|
|
46
|
+
?? (IS_WIN ? join(APPDATA, "tokenflexing") : join(homedir(), ".config", "tokenflexing"));
|
|
47
|
+
return join(base, "token");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadToken() {
|
|
51
|
+
const p = tokenPath();
|
|
52
|
+
if (!existsSync(p)) return null;
|
|
53
|
+
return readFileSync(p, "utf8").trim() || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function saveToken(raw) {
|
|
57
|
+
const p = tokenPath();
|
|
58
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
59
|
+
writeFileSync(p, raw + "\n", { mode: 0o600 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ───── ANSI ───────────────────────────────────────────────────── */
|
|
63
|
+
|
|
64
|
+
const isTTY = process.stdout.isTTY === true;
|
|
65
|
+
const c = isTTY
|
|
66
|
+
? {
|
|
67
|
+
reset: "\x1b[0m", dim: "\x1b[2m", bold: "\x1b[1m",
|
|
68
|
+
green: "\x1b[38;5;46m", flex: "\x1b[48;5;46m\x1b[38;5;232m",
|
|
69
|
+
faint: "\x1b[38;5;245m", yellow: "\x1b[38;5;226m",
|
|
70
|
+
}
|
|
71
|
+
: new Proxy({}, { get: () => "" });
|
|
72
|
+
|
|
73
|
+
function pad(s, n) {
|
|
74
|
+
s = String(s);
|
|
75
|
+
return s + " ".repeat(Math.max(0, n - s.replace(/\x1b\[[0-9;]*m/g, "").length));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fmtUsd = (n) =>
|
|
79
|
+
isFinite(n)
|
|
80
|
+
? new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 2 }).format(n)
|
|
81
|
+
: "—";
|
|
82
|
+
const fmtUsdC = (n) =>
|
|
83
|
+
isFinite(n)
|
|
84
|
+
? new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", notation: "compact", maximumFractionDigits: 1 }).format(n)
|
|
85
|
+
: "—";
|
|
86
|
+
const fmtNum = (n) => new Intl.NumberFormat("en-US").format(n);
|
|
87
|
+
const fmtC = (n) => new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }).format(n);
|
|
88
|
+
|
|
89
|
+
/* ───── Sparkline ──────────────────────────────────────────────── */
|
|
90
|
+
|
|
91
|
+
const BLOCKS = " ▁▂▃▄▅▆▇█";
|
|
92
|
+
|
|
93
|
+
/** 14-day ASCII cost sparkline from a Map<date, {cost, tokens}>. */
|
|
94
|
+
function sparkline(byDay, days = 14) {
|
|
95
|
+
const today = new Date();
|
|
96
|
+
const dates = Array.from({ length: days }, (_, i) => {
|
|
97
|
+
const d = new Date(today);
|
|
98
|
+
d.setDate(d.getDate() - (days - 1 - i));
|
|
99
|
+
return d.toISOString().slice(0, 10);
|
|
100
|
+
});
|
|
101
|
+
const vals = dates.map((d) => byDay.get(d)?.cost ?? 0);
|
|
102
|
+
const max = Math.max(...vals, 0.0001);
|
|
103
|
+
return vals.map((v) => BLOCKS[Math.min(8, Math.round((v / max) * 8))]).join("");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Merge byDay maps from multiple parsed sources. */
|
|
107
|
+
function mergeByDay(sources) {
|
|
108
|
+
const m = new Map();
|
|
109
|
+
for (const r of sources) {
|
|
110
|
+
for (const [d, b] of (r.stats.byDay ?? new Map())) {
|
|
111
|
+
const e = m.get(d) ?? { tokens: 0, cost: 0 };
|
|
112
|
+
e.tokens += b.tokens; e.cost += b.cost;
|
|
113
|
+
m.set(d, e);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return m;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ───── Pricing ────────────────────────────────────────────────── */
|
|
120
|
+
|
|
121
|
+
const ANT_PRICE = {
|
|
122
|
+
"claude-opus-4-7": { in: 15, out: 75, cached: 1.5 },
|
|
123
|
+
"claude-opus-4-6": { in: 15, out: 75, cached: 1.5 },
|
|
124
|
+
"claude-sonnet-4-6": { in: 3, out: 15, cached: 0.3 },
|
|
125
|
+
"claude-sonnet-4-5": { in: 3, out: 15, cached: 0.3 },
|
|
126
|
+
"claude-haiku-4-5": { in: 0.8, out: 4, cached: 0.08 },
|
|
127
|
+
};
|
|
128
|
+
const ANT_DEFAULT = { in: 3, out: 15, cached: 0.3 };
|
|
129
|
+
const CODEX_PRICE = { in: 1.25, cached: 0.13, out: 10 };
|
|
130
|
+
|
|
131
|
+
function priceAnt(model, inT, outT, cachedT) {
|
|
132
|
+
const p = ANT_PRICE[model]
|
|
133
|
+
?? Object.entries(ANT_PRICE).find(([id]) => model.startsWith(id))?.[1]
|
|
134
|
+
?? ANT_DEFAULT;
|
|
135
|
+
return (inT * p.in + outT * p.out + cachedT * p.cached) / 1_000_000;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ───── File utils ─────────────────────────────────────────────── */
|
|
139
|
+
|
|
140
|
+
function listJsonl(dir) {
|
|
141
|
+
try { return readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join(dir, f)); }
|
|
142
|
+
catch { return []; }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function walkJsonl(dir, out = [], depth = 0) {
|
|
146
|
+
if (depth > 6) return out;
|
|
147
|
+
let entries;
|
|
148
|
+
try { entries = readdirSync(dir); } catch { return out; }
|
|
149
|
+
for (const e of entries) {
|
|
150
|
+
const p = join(dir, e);
|
|
151
|
+
let st; try { st = statSync(p); } catch { continue; }
|
|
152
|
+
if (st.isDirectory()) walkJsonl(p, out, depth + 1);
|
|
153
|
+
else if (e.endsWith(".jsonl")) out.push(p);
|
|
154
|
+
if (out.length >= 200) return out;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ───── Parsers ────────────────────────────────────────────────── */
|
|
160
|
+
|
|
161
|
+
function parseClaudeCode() {
|
|
162
|
+
const roots = [
|
|
163
|
+
join(homedir(), ".claude", "projects"),
|
|
164
|
+
...(IS_WIN ? [join(APPDATA, "Claude", "projects")] : []),
|
|
165
|
+
];
|
|
166
|
+
const s = {
|
|
167
|
+
source: "Claude Code", available: false, events: 0,
|
|
168
|
+
input: 0, cached: 0, output: 0, cost: 0,
|
|
169
|
+
models: new Map(), byDay: new Map(), files: 0, projects: 0,
|
|
170
|
+
};
|
|
171
|
+
const root = roots.find(existsSync);
|
|
172
|
+
if (!root) return s;
|
|
173
|
+
let projects;
|
|
174
|
+
try {
|
|
175
|
+
projects = readdirSync(root).filter((p) => {
|
|
176
|
+
try { return statSync(join(root, p)).isDirectory(); } catch { return false; }
|
|
177
|
+
});
|
|
178
|
+
} catch { return s; }
|
|
179
|
+
if (!projects.length) return s;
|
|
180
|
+
s.available = true;
|
|
181
|
+
s.projects = projects.length;
|
|
182
|
+
for (const proj of projects.slice(0, 200)) {
|
|
183
|
+
for (const file of listJsonl(join(root, proj)).slice(0, 50)) {
|
|
184
|
+
s.files++;
|
|
185
|
+
let text; try { text = readFileSync(file, "utf8"); } catch { continue; }
|
|
186
|
+
for (const line of text.split("\n")) {
|
|
187
|
+
if (!line.trim()) continue;
|
|
188
|
+
let obj; try { obj = JSON.parse(line); } catch { continue; }
|
|
189
|
+
if (obj.type !== "assistant") continue;
|
|
190
|
+
const usage = obj.message?.usage;
|
|
191
|
+
const model = obj.message?.model;
|
|
192
|
+
if (!usage || !model) continue;
|
|
193
|
+
const inT = usage.input_tokens ?? 0;
|
|
194
|
+
const cached = (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
|
|
195
|
+
const out = usage.output_tokens ?? 0;
|
|
196
|
+
const cost = priceAnt(model, inT, out, cached);
|
|
197
|
+
s.events++; s.input += inT; s.cached += cached; s.output += out; s.cost += cost;
|
|
198
|
+
const m = s.models.get(model) ?? { events: 0, in: 0, out: 0, cached: 0, cost: 0 };
|
|
199
|
+
m.events++; m.in += inT; m.out += out; m.cached += cached; m.cost += cost;
|
|
200
|
+
s.models.set(model, m);
|
|
201
|
+
const day = (obj.timestamp ?? "").slice(0, 10);
|
|
202
|
+
if (day) {
|
|
203
|
+
const b = s.byDay.get(day) ?? { tokens: 0, cost: 0 };
|
|
204
|
+
b.tokens += inT + cached + out; b.cost += cost;
|
|
205
|
+
s.byDay.set(day, b);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return s;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseCodex() {
|
|
214
|
+
const ROOT = join(homedir(), ".codex", "sessions");
|
|
215
|
+
const s = {
|
|
216
|
+
source: "Codex CLI", available: false, events: 0,
|
|
217
|
+
input: 0, cached: 0, output: 0, thinking: 0, cost: 0,
|
|
218
|
+
models: new Map(), byDay: new Map(), files: 0, sessions: 0,
|
|
219
|
+
};
|
|
220
|
+
const files = walkJsonl(ROOT);
|
|
221
|
+
if (!files.length) return s;
|
|
222
|
+
s.available = true;
|
|
223
|
+
s.files = files.length;
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
let text; try { text = readFileSync(file, "utf8"); } catch { continue; }
|
|
226
|
+
let model = "codex";
|
|
227
|
+
let prev = { in: 0, out: 0, cached: 0, reasoning: 0 };
|
|
228
|
+
let hadEvent = false;
|
|
229
|
+
for (const line of text.split("\n")) {
|
|
230
|
+
if (!line.trim()) continue;
|
|
231
|
+
let obj; try { obj = JSON.parse(line); } catch { continue; }
|
|
232
|
+
if (obj.type === "session_meta" && obj.payload?.model) model = obj.payload.model;
|
|
233
|
+
if (obj.type !== "event_msg" || obj.payload?.type !== "token_count") continue;
|
|
234
|
+
const t = obj.payload.info?.total_token_usage;
|
|
235
|
+
if (!t) continue;
|
|
236
|
+
const inT = Math.max(0, (t.input_tokens ?? 0) - prev.in);
|
|
237
|
+
const outT = Math.max(0, (t.output_tokens ?? 0) - prev.out);
|
|
238
|
+
const cachedT= Math.max(0, (t.cached_input_tokens ?? 0) - prev.cached);
|
|
239
|
+
const reasonT= Math.max(0, (t.reasoning_output_tokens?? 0) - prev.reasoning);
|
|
240
|
+
prev = {
|
|
241
|
+
in: t.input_tokens ?? prev.in, out: t.output_tokens ?? prev.out,
|
|
242
|
+
cached: t.cached_input_tokens ?? prev.cached,
|
|
243
|
+
reasoning: t.reasoning_output_tokens ?? prev.reasoning,
|
|
244
|
+
};
|
|
245
|
+
if (!inT && !outT && !cachedT) continue;
|
|
246
|
+
const cost = (inT * CODEX_PRICE.in + cachedT * CODEX_PRICE.cached + (outT + reasonT) * CODEX_PRICE.out) / 1_000_000;
|
|
247
|
+
s.events++; s.input += inT; s.cached += cachedT; s.output += outT; s.thinking += reasonT; s.cost += cost;
|
|
248
|
+
const m = s.models.get(model) ?? { events: 0, cost: 0 };
|
|
249
|
+
m.events++; m.cost += cost; s.models.set(model, m);
|
|
250
|
+
const day = (obj.timestamp ?? "").slice(0, 10);
|
|
251
|
+
if (day) {
|
|
252
|
+
const b = s.byDay.get(day) ?? { tokens: 0, cost: 0 };
|
|
253
|
+
b.tokens += inT + cachedT + outT + reasonT; b.cost += cost;
|
|
254
|
+
s.byDay.set(day, b);
|
|
255
|
+
}
|
|
256
|
+
hadEvent = true;
|
|
257
|
+
}
|
|
258
|
+
if (hadEvent) s.sessions++;
|
|
259
|
+
}
|
|
260
|
+
return s;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function periodWindows(byDay) {
|
|
264
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
265
|
+
const weekAgo = new Date(Date.now() - 7 * 864e5).toISOString().slice(0, 10);
|
|
266
|
+
const monthAgo = new Date(Date.now() - 30 * 864e5).toISOString().slice(0, 10);
|
|
267
|
+
const w = { today: 0, week: 0, month: 0, tToday: 0, tWeek: 0, tMonth: 0 };
|
|
268
|
+
for (const [d, b] of byDay) {
|
|
269
|
+
if (d === today) { w.today += b.cost; w.tToday += b.tokens; }
|
|
270
|
+
if (d >= weekAgo) { w.week += b.cost; w.tWeek += b.tokens; }
|
|
271
|
+
if (d >= monthAgo) { w.month += b.cost; w.tMonth += b.tokens; }
|
|
272
|
+
}
|
|
273
|
+
return w;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* ───── Detector registry ──────────────────────────────────────── */
|
|
277
|
+
|
|
278
|
+
const DETECTORS = [
|
|
279
|
+
{
|
|
280
|
+
id: "claude-code", name: "Claude Code", parseable: true, parser: parseClaudeCode,
|
|
281
|
+
roots: [join(homedir(), ".claude", "projects"), ...(IS_WIN ? [join(APPDATA, "Claude", "projects")] : [])],
|
|
282
|
+
hint: "~/.claude/projects/**/*.jsonl",
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: "codex-cli", name: "Codex CLI", parseable: true, parser: parseCodex,
|
|
286
|
+
roots: [join(homedir(), ".codex", "sessions")],
|
|
287
|
+
hint: "~/.codex/sessions/**/*.jsonl",
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: "cursor", name: "Cursor", parseable: false, parser: null,
|
|
291
|
+
roots: pf(
|
|
292
|
+
[join(LIB, "Cursor", "User", "globalStorage", "state.vscdb")],
|
|
293
|
+
[join(APPDATA, "Cursor", "User", "globalStorage", "state.vscdb")],
|
|
294
|
+
[join(homedir(), ".config", "Cursor", "User", "globalStorage", "state.vscdb")]
|
|
295
|
+
),
|
|
296
|
+
hint: "SQLite state.vscdb — no token counts yet (planned v0.2)",
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: "claude-desktop", name: "Claude Desktop", parseable: false, parser: null,
|
|
300
|
+
roots: pf(
|
|
301
|
+
[join(LIB, "Claude")],
|
|
302
|
+
[join(APPDATA, "Claude")],
|
|
303
|
+
[join(homedir(), ".config", "Claude")]
|
|
304
|
+
),
|
|
305
|
+
hint: "Electron app — no per-message token events exposed",
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: "chatgpt-desktop", name: "ChatGPT Desktop", parseable: false, parser: null,
|
|
309
|
+
roots: pf(
|
|
310
|
+
[join(LIB, "com.openai.chat")],
|
|
311
|
+
[join(APPDATA, "OpenAI", "ChatGPT")],
|
|
312
|
+
[]
|
|
313
|
+
),
|
|
314
|
+
hint: "Electron app — conversation data only, no token counts",
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
id: "windsurf", name: "Windsurf (Codeium)", parseable: false, parser: null,
|
|
318
|
+
roots: pf(
|
|
319
|
+
[join(LIB, "Windsurf")],
|
|
320
|
+
[join(APPDATA, "Windsurf")],
|
|
321
|
+
[join(homedir(), ".config", "Windsurf")]
|
|
322
|
+
),
|
|
323
|
+
hint: "App data found — structured token log not exposed yet",
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: "continue", name: "Continue.dev", parseable: false, parser: null,
|
|
327
|
+
roots: [join(homedir(), ".continue")],
|
|
328
|
+
hint: "~/.continue/ — telemetry events not yet in token-count format",
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
id: "aider", name: "Aider", parseable: false, parser: null,
|
|
332
|
+
roots: [join(homedir(), ".aider"), join(process.cwd(), ".aider.chat.history.md")],
|
|
333
|
+
hint: ".aider.chat.history.md per-repo — cost-line parser planned v0.2",
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: "cline", name: "Cline", parseable: false, parser: null,
|
|
337
|
+
roots: [vscodeExt("saoudrizwan.claude-dev")],
|
|
338
|
+
hint: "VSCode extension — ui_messages.json task parser planned",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: "roo-code", name: "Roo Code", parseable: false, parser: null,
|
|
342
|
+
roots: [vscodeExt("rooveterinaryinc.roo-cline")],
|
|
343
|
+
hint: "VSCode extension — ui_messages.json task parser planned",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: "kilo-code", name: "Kilo Code", parseable: false, parser: null,
|
|
347
|
+
roots: [vscodeExt("kilocode.kilo-code")],
|
|
348
|
+
hint: "VSCode extension — ui_messages.json task parser planned",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: "zed", name: "Zed AI", parseable: false, parser: null,
|
|
352
|
+
roots: pf(
|
|
353
|
+
[join(LIB, "Zed")],
|
|
354
|
+
[join(APPDATA, "Zed")],
|
|
355
|
+
[join(homedir(), ".config", "zed")]
|
|
356
|
+
),
|
|
357
|
+
hint: "SQLite threads.db — reader planned",
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: "gemini-cli", name: "Gemini CLI", parseable: false, parser: null,
|
|
361
|
+
roots: [join(homedir(), ".gemini"), join(homedir(), ".config", "gemini")],
|
|
362
|
+
hint: "~/.gemini/tmp/*.json|*.jsonl — parser planned",
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: "opencode", name: "OpenCode", parseable: false, parser: null,
|
|
366
|
+
roots: [join(XDG, "opencode"), join(homedir(), ".opencode")],
|
|
367
|
+
hint: "opencode/storage/message/*.json — parser planned",
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
id: "ampcode", name: "Amp", parseable: false, parser: null,
|
|
371
|
+
roots: [join(XDG, "amp"), join(homedir(), ".amp"), join(homedir(), ".config", "amp")],
|
|
372
|
+
hint: "amp/threads/T-*.json — parser planned",
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: "antigravity", name: "Antigravity (Google)", parseable: false, parser: null,
|
|
376
|
+
roots: pf(
|
|
377
|
+
[join(LIB, "Antigravity")],
|
|
378
|
+
[join(APPDATA, "Antigravity")],
|
|
379
|
+
[join(homedir(), ".config", "antigravity"), join(homedir(), ".config", "tokscale", "antigravity-cache")]
|
|
380
|
+
),
|
|
381
|
+
hint: "Antigravity session cache — format TBD",
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
id: "github-copilot", name: "GitHub Copilot", parseable: false, parser: null,
|
|
385
|
+
roots: [join(homedir(), ".copilot", "otel"), ...(process.env.COPILOT_OTEL_FILE_EXPORTER_PATH ? [process.env.COPILOT_OTEL_FILE_EXPORTER_PATH] : [])],
|
|
386
|
+
hint: "OTEL trace files — usage extraction planned",
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
id: "goose", name: "Goose (Block)", parseable: false, parser: null,
|
|
390
|
+
roots: [join(XDG, "goose", "sessions")],
|
|
391
|
+
hint: "SQLite sessions.db — reader planned",
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id: "kiro", name: "Kiro (Amazon)", parseable: false, parser: null,
|
|
395
|
+
roots: [join(homedir(), ".kiro", "sessions", "cli")],
|
|
396
|
+
hint: "~/.kiro/sessions/cli/*.json — parser planned",
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: "mux", name: "Mux", parseable: false, parser: null,
|
|
400
|
+
roots: [join(homedir(), ".mux", "sessions")],
|
|
401
|
+
hint: "session-usage.json — parser planned",
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
id: "openclaw", name: "OpenClaw", parseable: false, parser: null,
|
|
405
|
+
roots: [join(homedir(), ".openclaw", "agents")],
|
|
406
|
+
hint: "*.jsonl agent sessions — parser planned",
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
id: "crush", name: "Crush", parseable: false, parser: null,
|
|
410
|
+
roots: [join(XDG, "crush")],
|
|
411
|
+
hint: "projects.json — parser planned",
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: "kimi", name: "Kimi (Moonshot)", parseable: false, parser: null,
|
|
415
|
+
roots: [join(homedir(), ".kimi", "sessions")],
|
|
416
|
+
hint: "wire.jsonl sessions — parser planned",
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
id: "hermes", name: "Hermes", parseable: false, parser: null,
|
|
420
|
+
roots: [process.env.HERMES_HOME ? join(process.env.HERMES_HOME) : join(homedir(), ".hermes")],
|
|
421
|
+
hint: "SQLite state.db — reader planned",
|
|
422
|
+
},
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
function runScan() {
|
|
426
|
+
return DETECTORS.map((det) => {
|
|
427
|
+
const foundRoot = det.roots.find((r) => r && existsSync(r)) ?? null;
|
|
428
|
+
if (det.parseable && det.parser) {
|
|
429
|
+
const stats = det.parser();
|
|
430
|
+
return { ...det, found: !!foundRoot || stats.available, hasData: stats.available, parsed: true, stats, foundRoot };
|
|
431
|
+
}
|
|
432
|
+
return { ...det, found: !!foundRoot, hasData: false, parsed: false, stats: null, foundRoot };
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* ───── MCP server ─────────────────────────────────────────────── */
|
|
437
|
+
|
|
438
|
+
function commandMcp() {
|
|
439
|
+
const TOOLS = [
|
|
440
|
+
{
|
|
441
|
+
name: "get_my_stats",
|
|
442
|
+
description: "Get local AI token usage stats from Claude Code, Codex CLI, and other tools installed on this device. Returns all-time and period spend.",
|
|
443
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: "get_device_scan",
|
|
447
|
+
description: "Scan this device for all installed AI coding tools (Claude Code, Cursor, Codex, Gemini CLI, etc.) and show which have extractable token data.",
|
|
448
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: "get_flex_card",
|
|
452
|
+
description: "Get a shareable one-liner showing all-time AI token spend — great for posting on X, HN, or Slack.",
|
|
453
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
454
|
+
},
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
function send(obj) { process.stdout.write(JSON.stringify(obj) + "\n"); }
|
|
458
|
+
|
|
459
|
+
function handle(req) {
|
|
460
|
+
const { method, id, params } = req;
|
|
461
|
+
if (method === "initialize") {
|
|
462
|
+
send({ jsonrpc: "2.0", id, result: { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "tokenflexing", version: "1.1.0" } } });
|
|
463
|
+
} else if (method === "notifications/initialized" || method === "ping") {
|
|
464
|
+
if (id !== undefined) send({ jsonrpc: "2.0", id, result: {} });
|
|
465
|
+
} else if (method === "tools/list") {
|
|
466
|
+
send({ jsonrpc: "2.0", id, result: { tools: TOOLS } });
|
|
467
|
+
} else if (method === "tools/call") {
|
|
468
|
+
const name = params?.name;
|
|
469
|
+
let text = "";
|
|
470
|
+
if (name === "get_my_stats") {
|
|
471
|
+
const measured = runScan().filter((r) => r.hasData);
|
|
472
|
+
if (!measured.length) {
|
|
473
|
+
text = "No AI usage data found on this device. Start using Claude Code or Codex CLI, then run `tokenflexing scan`.";
|
|
474
|
+
} else {
|
|
475
|
+
let totalCost = 0, totalTokens = 0;
|
|
476
|
+
const lines = [];
|
|
477
|
+
for (const r of measured) {
|
|
478
|
+
const s = r.stats;
|
|
479
|
+
const t = s.input + s.cached + s.output + (s.thinking ?? 0);
|
|
480
|
+
totalCost += s.cost; totalTokens += t;
|
|
481
|
+
lines.push(`**${s.source}**: ${fmtUsd(s.cost)} · ${fmtC(t)} tokens · ${fmtNum(s.events)} events`);
|
|
482
|
+
}
|
|
483
|
+
text = lines.join("\n") + `\n\n**Grand Total**: ${fmtUsd(totalCost)} · ${fmtC(totalTokens)} tokens\n\nFull profile → tokenflexing.com`;
|
|
484
|
+
}
|
|
485
|
+
} else if (name === "get_device_scan") {
|
|
486
|
+
const scan = runScan();
|
|
487
|
+
const measured = scan.filter((r) => r.hasData);
|
|
488
|
+
const detected = scan.filter((r) => r.found && !r.hasData);
|
|
489
|
+
const notFound = scan.filter((r) => !r.found);
|
|
490
|
+
const parts = [];
|
|
491
|
+
if (measured.length) { parts.push("**Measured** (token counts extracted):"); for (const r of measured) { const s = r.stats; parts.push(`- ${s.source}: ${fmtUsd(s.cost)} · ${fmtC(s.input + s.cached + s.output)} tokens`); } }
|
|
492
|
+
if (detected.length) { parts.push("\n**Detected** (installed, token counts not yet accessible):"); for (const r of detected) parts.push(`- ${r.name}`); }
|
|
493
|
+
if (notFound.length) { parts.push("\n**Not found** on this device:"); for (const r of notFound) parts.push(`- ${r.name}`); }
|
|
494
|
+
text = parts.join("\n");
|
|
495
|
+
} else if (name === "get_flex_card") {
|
|
496
|
+
const sources = runScan().filter((r) => r.hasData);
|
|
497
|
+
const total = sources.reduce((a, r) => a + r.stats.cost, 0);
|
|
498
|
+
const tokens = sources.reduce((a, r) => a + r.stats.input + r.stats.cached + r.stats.output + (r.stats.thinking ?? 0), 0);
|
|
499
|
+
text = `I burned ${fmtUsdC(total)} on AI this all-time.\n${fmtC(tokens)} tokens · ${sources.map((r) => r.stats.source).join(" + ")}\nhttps://tokenflexing.com`;
|
|
500
|
+
} else {
|
|
501
|
+
send({ jsonrpc: "2.0", id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
send({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text }] } });
|
|
505
|
+
} else {
|
|
506
|
+
if (id !== undefined) send({ jsonrpc: "2.0", id, error: { code: -32601, message: "Method not found" } });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let buf = "";
|
|
511
|
+
process.stdin.setEncoding("utf8");
|
|
512
|
+
process.stdin.on("data", (chunk) => {
|
|
513
|
+
buf += chunk;
|
|
514
|
+
const lines = buf.split("\n");
|
|
515
|
+
buf = lines.pop() ?? "";
|
|
516
|
+
for (const line of lines) { const t = line.trim(); if (t) try { handle(JSON.parse(t)); } catch {} }
|
|
517
|
+
});
|
|
518
|
+
process.stdin.on("end", () => { if (buf.trim()) try { handle(JSON.parse(buf)); } catch {}; });
|
|
519
|
+
process.stdin.resume();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/* ───── Commands ───────────────────────────────────────────────── */
|
|
523
|
+
|
|
524
|
+
function commandStats() {
|
|
525
|
+
const scan = runScan();
|
|
526
|
+
const measured = scan.filter((r) => r.hasData);
|
|
527
|
+
|
|
528
|
+
console.log("");
|
|
529
|
+
console.log(`${c.bold}tokenflexing${c.reset}${c.faint} · local AI usage${c.reset}`);
|
|
530
|
+
console.log("");
|
|
531
|
+
|
|
532
|
+
if (!measured.length) {
|
|
533
|
+
const detected = scan.filter((r) => r.found);
|
|
534
|
+
if (detected.length) {
|
|
535
|
+
console.log(`${c.faint}Detected ${detected.map((d) => d.name).join(", ")} — no usage data parsed yet.${c.reset}`);
|
|
536
|
+
console.log(`${c.faint}Run 'tokenflexing scan' for full device inventory.${c.reset}`);
|
|
537
|
+
} else {
|
|
538
|
+
console.log(`${c.faint}No local AI tools detected. Run 'tokenflexing scan' for details.${c.reset}`);
|
|
539
|
+
}
|
|
540
|
+
console.log("");
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
let totalCost = 0, totalTokens = 0, totalEvents = 0;
|
|
545
|
+
for (const r of measured) {
|
|
546
|
+
const s = r.stats;
|
|
547
|
+
const w = periodWindows(s.byDay);
|
|
548
|
+
totalCost += s.cost;
|
|
549
|
+
totalTokens += s.input + s.cached + s.output + (s.thinking ?? 0);
|
|
550
|
+
totalEvents += s.events;
|
|
551
|
+
|
|
552
|
+
console.log(`${c.green}■${c.reset} ${c.bold}${s.source}${c.reset}`);
|
|
553
|
+
const detail = s.source === "Claude Code"
|
|
554
|
+
? `${fmtNum(s.events)} turns · ${s.files} files · ${s.projects} projects`
|
|
555
|
+
: `${fmtNum(s.events)} deltas · ${s.sessions} sessions · ${s.files} files`;
|
|
556
|
+
console.log(` ${c.faint}${detail}${c.reset}`);
|
|
557
|
+
console.log("");
|
|
558
|
+
console.log(` ${c.faint}TODAY${c.reset} ${pad(fmtUsd(w.today), 12)}${c.faint}${fmtC(w.tToday)} tokens${c.reset}`);
|
|
559
|
+
console.log(` ${c.faint}7-DAY${c.reset} ${pad(fmtUsd(w.week), 12)}${c.faint}${fmtC(w.tWeek)} tokens${c.reset}`);
|
|
560
|
+
console.log(` ${c.faint}30-DAY${c.reset} ${pad(fmtUsd(w.month), 12)}${c.faint}${fmtC(w.tMonth)} tokens${c.reset}`);
|
|
561
|
+
console.log(` ${c.faint}TOTAL${c.reset} ${c.bold}${pad(fmtUsd(s.cost), 12)}${c.reset}${c.faint}${fmtC(s.input + s.cached + s.output + (s.thinking ?? 0))} tokens${c.reset}`);
|
|
562
|
+
|
|
563
|
+
if (s.models.size) {
|
|
564
|
+
console.log("");
|
|
565
|
+
console.log(` ${c.faint}MODELS${c.reset}`);
|
|
566
|
+
const rows = [...s.models.entries()].sort((a, b) => (b[1].cost ?? 0) - (a[1].cost ?? 0));
|
|
567
|
+
for (const [model, m] of rows) {
|
|
568
|
+
console.log(` ${pad(model, 28)}${c.faint}${pad(fmtNum(m.events) + " turns", 16)}${c.reset}${c.bold}${fmtUsd(m.cost)}${c.reset}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
console.log("");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
console.log(c.flex + " GRAND TOTAL " + c.reset);
|
|
575
|
+
console.log(` ${c.bold}${fmtUsd(totalCost)}${c.reset}${c.faint} · ${fmtC(totalTokens)} tokens · ${fmtNum(totalEvents)} events${c.reset}`);
|
|
576
|
+
const unmeasured = scan.filter((r) => r.found && !r.hasData);
|
|
577
|
+
if (unmeasured.length) {
|
|
578
|
+
console.log("");
|
|
579
|
+
console.log(` ${c.faint}Also detected (not yet measurable): ${unmeasured.map((r) => r.name).join(", ")}${c.reset}`);
|
|
580
|
+
console.log(` ${c.faint}Run 'tokenflexing scan' for full device inventory.${c.reset}`);
|
|
581
|
+
}
|
|
582
|
+
console.log("");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function commandFlex() {
|
|
586
|
+
const sources = runScan().filter((r) => r.hasData);
|
|
587
|
+
const total = sources.reduce((a, r) => a + r.stats.cost, 0);
|
|
588
|
+
const tokens = sources.reduce((a, r) => a + r.stats.input + r.stats.cached + r.stats.output + (r.stats.thinking ?? 0), 0);
|
|
589
|
+
const big = fmtUsdC(total);
|
|
590
|
+
const spark = sparkline(mergeByDay(sources));
|
|
591
|
+
const W = 46; // inner width
|
|
592
|
+
|
|
593
|
+
function row(inner) {
|
|
594
|
+
const raw = inner.replace(/\x1b\[[0-9;]*m/g, "");
|
|
595
|
+
const pad2 = " ".repeat(Math.max(0, W - raw.length));
|
|
596
|
+
return ` ║ ${inner}${pad2} ║`;
|
|
597
|
+
}
|
|
598
|
+
const bar = "═".repeat(W + 2);
|
|
599
|
+
|
|
600
|
+
console.log("");
|
|
601
|
+
console.log(` ╔${bar}╗`);
|
|
602
|
+
console.log(row(""));
|
|
603
|
+
console.log(row(`${c.green}◈ tokenflexing${c.reset} ${c.faint}tokenflexing.com${c.reset}`));
|
|
604
|
+
console.log(row(""));
|
|
605
|
+
console.log(row(`${c.bold}I burned ${c.green}${big}${c.reset}${c.bold} on AI.${c.reset}`));
|
|
606
|
+
console.log(row(`${c.faint}all-time · ${fmtC(tokens)} tokens${c.reset}`));
|
|
607
|
+
console.log(row(""));
|
|
608
|
+
if (sources.length) {
|
|
609
|
+
for (const r of sources) {
|
|
610
|
+
const s = r.stats;
|
|
611
|
+
const t = s.input + s.cached + s.output + (s.thinking ?? 0);
|
|
612
|
+
console.log(row(` ${c.faint}${pad(s.source, 18)}${c.reset}${pad(fmtUsd(s.cost), 12)}${c.faint}${fmtC(t)} tokens${c.reset}`));
|
|
613
|
+
}
|
|
614
|
+
console.log(row(""));
|
|
615
|
+
}
|
|
616
|
+
console.log(row(`${c.faint}14d ${c.reset}${c.green}${spark}${c.reset}`));
|
|
617
|
+
console.log(row(""));
|
|
618
|
+
console.log(` ╚${bar}╝`);
|
|
619
|
+
console.log("");
|
|
620
|
+
console.log(` ${c.faint}Drop this anywhere — it auto-updates daily via tokenflexing daemon.${c.reset}`);
|
|
621
|
+
console.log("");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function commandScan() {
|
|
625
|
+
const scan = runScan();
|
|
626
|
+
const measured = scan.filter((r) => r.hasData);
|
|
627
|
+
const detected = scan.filter((r) => r.found && !r.hasData);
|
|
628
|
+
const notFound = scan.filter((r) => !r.found);
|
|
629
|
+
|
|
630
|
+
const summary = [
|
|
631
|
+
measured.length ? `${c.green}${measured.length} measured${c.reset}` : null,
|
|
632
|
+
detected.length ? `${c.yellow}${detected.length} detected${c.reset}` : null,
|
|
633
|
+
notFound.length ? `${c.faint}${notFound.length} not found${c.reset}` : null,
|
|
634
|
+
].filter(Boolean).join(" · ");
|
|
635
|
+
|
|
636
|
+
console.log("");
|
|
637
|
+
console.log(`${c.bold}tokenflexing scan${c.reset} ${summary}`);
|
|
638
|
+
console.log("");
|
|
639
|
+
|
|
640
|
+
if (measured.length) {
|
|
641
|
+
console.log(`${c.green}${c.bold}MEASURED${c.reset} ${c.faint}token counts extracted${c.reset}`);
|
|
642
|
+
console.log("─".repeat(56));
|
|
643
|
+
for (const r of measured) {
|
|
644
|
+
const s = r.stats;
|
|
645
|
+
const total = s.input + s.cached + s.output + (s.thinking ?? 0);
|
|
646
|
+
const spark = sparkline(s.byDay ?? new Map(), 14);
|
|
647
|
+
console.log(
|
|
648
|
+
` ${c.green}●${c.reset} ${pad(s.source, 18)}` +
|
|
649
|
+
`${c.bold}${pad(fmtUsd(s.cost), 11)}${c.reset}` +
|
|
650
|
+
`${c.faint}${fmtC(total)} tokens${c.reset} ${c.green}${spark}${c.reset}`
|
|
651
|
+
);
|
|
652
|
+
console.log(` ${c.faint}${r.hint}${c.reset}`);
|
|
653
|
+
}
|
|
654
|
+
console.log("");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (detected.length) {
|
|
658
|
+
console.log(`${c.yellow}${c.bold}DETECTED${c.reset} ${c.faint}installed, token counts not yet accessible${c.reset}`);
|
|
659
|
+
console.log("─".repeat(56));
|
|
660
|
+
for (const r of detected) {
|
|
661
|
+
const shortRoot = (r.foundRoot ?? "").replace(homedir(), "~");
|
|
662
|
+
console.log(` ${c.yellow}◎${c.reset} ${pad(r.name, 22)}${c.faint}${shortRoot}${c.reset}`);
|
|
663
|
+
console.log(` ${c.faint}${r.hint}${c.reset}`);
|
|
664
|
+
}
|
|
665
|
+
console.log("");
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (notFound.length) {
|
|
669
|
+
console.log(`${c.faint}NOT FOUND — ${notFound.map((r) => r.name).join(" · ")}${c.reset}`);
|
|
670
|
+
console.log("");
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
console.log(`${c.faint}Run 'tokenflexing stats' for full breakdown · 'tokenflexing daemon --install' for daily refresh${c.reset}`);
|
|
674
|
+
console.log("");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function commandDaemon(args) {
|
|
678
|
+
const doInstall = args.includes("--install");
|
|
679
|
+
const doUninstall = args.includes("--uninstall");
|
|
680
|
+
const LABEL = "com.tokenflexing.sync";
|
|
681
|
+
const PLIST = join(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
682
|
+
const WIN_TASK = "Tokenflexing Sync";
|
|
683
|
+
// Use npx so the daemon works for any user regardless of where the package is installed.
|
|
684
|
+
// npx lives in the same directory as node on all platforms.
|
|
685
|
+
const npxPath = join(dirname(process.execPath), IS_WIN ? "npx.cmd" : "npx");
|
|
686
|
+
|
|
687
|
+
const plistContent = [
|
|
688
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
689
|
+
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`,
|
|
690
|
+
`<plist version="1.0"><dict>`,
|
|
691
|
+
` <key>Label</key><string>${LABEL}</string>`,
|
|
692
|
+
` <key>ProgramArguments</key><array>`,
|
|
693
|
+
` <string>${npxPath}</string>`,
|
|
694
|
+
` <string>-y</string>`,
|
|
695
|
+
` <string>tokenflexing@latest</string>`,
|
|
696
|
+
` <string>scan</string>`,
|
|
697
|
+
` </array>`,
|
|
698
|
+
` <key>StartCalendarInterval</key><dict>`,
|
|
699
|
+
` <key>Hour</key><integer>6</integer>`,
|
|
700
|
+
` <key>Minute</key><integer>0</integer>`,
|
|
701
|
+
` </dict>`,
|
|
702
|
+
` <key>StandardOutPath</key><string>/tmp/tokenflexing-sync.log</string>`,
|
|
703
|
+
` <key>StandardErrorPath</key><string>/tmp/tokenflexing-sync.err</string>`,
|
|
704
|
+
` <key>RunAtLoad</key><false/>`,
|
|
705
|
+
`</dict></plist>`,
|
|
706
|
+
].join("\n");
|
|
707
|
+
|
|
708
|
+
console.log("");
|
|
709
|
+
console.log(`${c.bold}tokenflexing daemon${c.reset}${c.faint} · daily auto-refresh at 6:00 AM${c.reset}`);
|
|
710
|
+
console.log("");
|
|
711
|
+
|
|
712
|
+
if (IS_WIN) {
|
|
713
|
+
const trArg = `"${npxPath}" -y tokenflexing@latest scan`;
|
|
714
|
+
console.log(`Platform: ${c.bold}Windows Task Scheduler${c.reset}`);
|
|
715
|
+
console.log("");
|
|
716
|
+
if (doInstall) {
|
|
717
|
+
try {
|
|
718
|
+
execFileSync("schtasks", ["/create", "/tn", WIN_TASK, "/tr", trArg, "/sc", "DAILY", "/st", "06:00", "/f"], { stdio: "inherit" });
|
|
719
|
+
console.log(`${c.green}✓${c.reset} Task created: "${WIN_TASK}"`);
|
|
720
|
+
console.log(`${c.faint}Remove: schtasks /delete /tn "${WIN_TASK}" /f${c.reset}`);
|
|
721
|
+
} catch {
|
|
722
|
+
console.error("Failed. Try running as Administrator.");
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
} else {
|
|
726
|
+
console.log(`${c.faint}Pass --install to create the Task Scheduler entry.${c.reset}`);
|
|
727
|
+
console.log(`${c.faint}Command: schtasks /create /tn "${WIN_TASK}" /tr "${trArg}" /sc DAILY /st 06:00 /f${c.reset}`);
|
|
728
|
+
}
|
|
729
|
+
} else {
|
|
730
|
+
const uid = String(userInfo().uid);
|
|
731
|
+
console.log(`Platform: ${c.bold}macOS LaunchAgent${c.reset}`);
|
|
732
|
+
console.log(`Target: ${c.faint}${PLIST}${c.reset}`);
|
|
733
|
+
console.log("");
|
|
734
|
+
if (doUninstall) {
|
|
735
|
+
try {
|
|
736
|
+
if (existsSync(PLIST)) {
|
|
737
|
+
try { execFileSync("launchctl", ["bootout", `gui/${uid}`, PLIST], { stdio: "ignore" }); } catch {}
|
|
738
|
+
unlinkSync(PLIST);
|
|
739
|
+
}
|
|
740
|
+
console.log(`${c.green}✓${c.reset} Daemon removed.`);
|
|
741
|
+
} catch (err) {
|
|
742
|
+
console.error("Failed to uninstall:", err.message);
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
} else if (doInstall) {
|
|
746
|
+
try {
|
|
747
|
+
const dir = join(homedir(), "Library", "LaunchAgents");
|
|
748
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
749
|
+
writeFileSync(PLIST, plistContent, "utf8");
|
|
750
|
+
execFileSync("launchctl", ["bootstrap", `gui/${uid}`, PLIST], { stdio: "ignore" });
|
|
751
|
+
console.log(`${c.green}✓${c.reset} Daemon installed. Runs daily at 6:00 AM.`);
|
|
752
|
+
console.log(` Log: ${c.faint}/tmp/tokenflexing-sync.log${c.reset}`);
|
|
753
|
+
console.log(` Remove: ${c.faint}tokenflexing daemon --uninstall${c.reset}`);
|
|
754
|
+
} catch (err) {
|
|
755
|
+
console.error("Failed to install daemon:", err.message);
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
console.log(`${c.faint}Plist preview:${c.reset}`);
|
|
760
|
+
console.log(plistContent.split("\n").map((l) => ` ${c.faint}${l}${c.reset}`).join("\n"));
|
|
761
|
+
console.log("");
|
|
762
|
+
console.log(`Pass ${c.bold}--install${c.reset} to write and load. Pass ${c.bold}--uninstall${c.reset} to remove.`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
console.log("");
|
|
766
|
+
console.log(`${c.faint}Cloud sync (tokenflexing login) is live. Run it to push stats to your profile.${c.reset}`);
|
|
767
|
+
console.log(`${c.faint}the daemon pushes stats to tokenflexing.com automatically.${c.reset}`);
|
|
768
|
+
console.log("");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function commandInstallHooks(args) {
|
|
772
|
+
const client = arg(args, "--client") ?? "claude-code";
|
|
773
|
+
const apply = args.includes("--apply");
|
|
774
|
+
const forProject = args.includes("--project");
|
|
775
|
+
|
|
776
|
+
const MCP_ENTRY = { command: "npx", args: ["-y", "tokenflexing@latest", "mcp"], type: "stdio" };
|
|
777
|
+
|
|
778
|
+
const CONFIGS = {
|
|
779
|
+
"claude-code": {
|
|
780
|
+
path: forProject ? join(process.cwd(), ".claude", "settings.json") : join(homedir(), ".claude", "settings.json"),
|
|
781
|
+
key: "mcpServers",
|
|
782
|
+
},
|
|
783
|
+
cursor: {
|
|
784
|
+
path: forProject ? join(process.cwd(), ".cursor", "mcp.json") : join(homedir(), ".cursor", "mcp.json"),
|
|
785
|
+
key: "mcpServers",
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const cfg = CONFIGS[client];
|
|
790
|
+
if (!cfg) {
|
|
791
|
+
console.log(`${c.faint}Supported: --client claude-code --client cursor${c.reset}`);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
console.log("");
|
|
796
|
+
console.log(`${c.bold}tokenflexing install-hooks${c.reset} · ${client}`);
|
|
797
|
+
console.log("");
|
|
798
|
+
console.log(` Registers tokenflexing as an MCP server — your agent gets three new tools:`);
|
|
799
|
+
console.log(` · ${c.green}get_my_stats${c.reset} "What's my token spend this week?"`);
|
|
800
|
+
console.log(` · ${c.green}get_device_scan${c.reset} "Which AI tools do I have installed?"`);
|
|
801
|
+
console.log(` · ${c.green}get_flex_card${c.reset} "Show me my share card."`);
|
|
802
|
+
console.log("");
|
|
803
|
+
console.log(` Target: ${c.dim}${cfg.path}${c.reset}`);
|
|
804
|
+
console.log(` Status: ${c.dim}${existsSync(cfg.path) ? "exists — will merge" : "will be created"}${c.reset}`);
|
|
805
|
+
console.log("");
|
|
806
|
+
|
|
807
|
+
if (!apply) {
|
|
808
|
+
console.log(`${c.bold}DRY RUN${c.reset} · Pass ${c.green}--apply${c.reset} to write.`);
|
|
809
|
+
console.log(`${c.faint}Adds: { "${cfg.key}": { "tokenflexing": ${JSON.stringify(MCP_ENTRY)} } }${c.reset}`);
|
|
810
|
+
console.log("");
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
let existing = {};
|
|
815
|
+
if (existsSync(cfg.path)) try { existing = JSON.parse(readFileSync(cfg.path, "utf8")); } catch {}
|
|
816
|
+
const updated = { ...existing, [cfg.key]: { ...(existing[cfg.key] ?? {}), tokenflexing: MCP_ENTRY } };
|
|
817
|
+
const dir = dirname(cfg.path);
|
|
818
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
819
|
+
writeFileSync(cfg.path, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
820
|
+
console.log(`${c.green}✓${c.reset} tokenflexing MCP server added to ${client}.`);
|
|
821
|
+
console.log(` Restart ${client}, then ask: "What's my token usage?" or "Show me my flex card."`);
|
|
822
|
+
console.log("");
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async function commandLogin(args) {
|
|
826
|
+
// Support: tokenflexing login --token <raw> (paste token from Settings page)
|
|
827
|
+
const manualToken = args && args[args.indexOf("--token") + 1];
|
|
828
|
+
if (manualToken && manualToken.startsWith("tf_live_")) {
|
|
829
|
+
saveToken(manualToken);
|
|
830
|
+
console.log("");
|
|
831
|
+
console.log(`${c.green}✓${c.reset} Token saved. Run ${c.green}tokenflexing sync${c.reset} to push your stats.`);
|
|
832
|
+
console.log("");
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Device-code flow: POST → open browser → poll → save token
|
|
837
|
+
console.log("");
|
|
838
|
+
console.log(`${c.bold}tokenflexing login${c.reset}`);
|
|
839
|
+
console.log("");
|
|
840
|
+
|
|
841
|
+
let deviceRes;
|
|
842
|
+
try {
|
|
843
|
+
const r = await fetch(`${SITE}/api/cli/device`, { method: "POST" });
|
|
844
|
+
deviceRes = await r.json();
|
|
845
|
+
} catch {
|
|
846
|
+
console.error(`${c.faint}Could not reach ${SITE}. Check your connection.${c.reset}`);
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (deviceRes.error) {
|
|
851
|
+
console.error(`Error: ${deviceRes.error}`);
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const { code, url } = deviceRes;
|
|
856
|
+
console.log(`Open this URL in your browser to authorize:`);
|
|
857
|
+
console.log("");
|
|
858
|
+
console.log(` ${c.green}${url}${c.reset}`);
|
|
859
|
+
console.log("");
|
|
860
|
+
console.log(`Code: ${c.bold}${code}${c.reset} (expires in 5 min)`);
|
|
861
|
+
console.log("");
|
|
862
|
+
|
|
863
|
+
// Try to open the browser automatically
|
|
864
|
+
try {
|
|
865
|
+
if (IS_MAC) execFileSync("open", [url]);
|
|
866
|
+
else if (IS_WIN) execFileSync("cmd", ["/c", "start", url]);
|
|
867
|
+
else execFileSync("xdg-open", [url]);
|
|
868
|
+
} catch { /* silently ignore — user can open manually */ }
|
|
869
|
+
|
|
870
|
+
// Poll every 2 seconds until authorized or expired
|
|
871
|
+
process.stdout.write("Waiting for authorization");
|
|
872
|
+
let rawToken = null;
|
|
873
|
+
|
|
874
|
+
for (let i = 0; i < 150; i++) { // up to 5 min
|
|
875
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
876
|
+
process.stdout.write(".");
|
|
877
|
+
|
|
878
|
+
let pollRes;
|
|
879
|
+
try {
|
|
880
|
+
const r = await fetch(`${SITE}/api/cli/device?code=${code}`);
|
|
881
|
+
pollRes = await r.json();
|
|
882
|
+
} catch { continue; }
|
|
883
|
+
|
|
884
|
+
if (pollRes.status === "authorized") {
|
|
885
|
+
rawToken = pollRes.token;
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
if (pollRes.status === "expired") {
|
|
889
|
+
process.stdout.write("\n");
|
|
890
|
+
console.log(`\n${c.faint}Code expired. Re-run tokenflexing login.${c.reset}`);
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
process.stdout.write("\n");
|
|
896
|
+
|
|
897
|
+
if (!rawToken) {
|
|
898
|
+
console.log(`${c.faint}Timed out. Re-run tokenflexing login.${c.reset}`);
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
saveToken(rawToken);
|
|
903
|
+
console.log("");
|
|
904
|
+
console.log(`${c.green}✓ Authorized!${c.reset} Token saved to ${tokenPath()}`);
|
|
905
|
+
console.log(`Run ${c.green}tokenflexing sync${c.reset} to push your stats now.`);
|
|
906
|
+
console.log("");
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
async function commandSync() {
|
|
910
|
+
const token = loadToken();
|
|
911
|
+
if (!token) {
|
|
912
|
+
console.log("");
|
|
913
|
+
console.log(`${c.faint}No token found. Run ${c.green}tokenflexing login${c.faint} first.${c.reset}`);
|
|
914
|
+
console.log("");
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
console.log("");
|
|
919
|
+
console.log(`${c.bold}tokenflexing sync${c.reset}`);
|
|
920
|
+
console.log("");
|
|
921
|
+
|
|
922
|
+
// Collect parsed source data
|
|
923
|
+
const sources = collectSources();
|
|
924
|
+
if (!sources.length) {
|
|
925
|
+
console.log(`${c.faint}Nothing to sync — no parseable sources found.${c.reset}`);
|
|
926
|
+
console.log("");
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const scans = sources.map((s) => ({
|
|
931
|
+
source: s.id,
|
|
932
|
+
events: s.stats.events,
|
|
933
|
+
input: s.stats.input,
|
|
934
|
+
output: s.stats.output,
|
|
935
|
+
cached: s.stats.cached,
|
|
936
|
+
cost: s.stats.cost,
|
|
937
|
+
models: Object.fromEntries(s.stats.models),
|
|
938
|
+
}));
|
|
939
|
+
|
|
940
|
+
let res;
|
|
941
|
+
try {
|
|
942
|
+
const r = await fetch(`${SITE}/api/sync/device`, {
|
|
943
|
+
method: "POST",
|
|
944
|
+
headers: {
|
|
945
|
+
"Content-Type": "application/json",
|
|
946
|
+
"Authorization": `Bearer ${token}`,
|
|
947
|
+
},
|
|
948
|
+
body: JSON.stringify({ scans }),
|
|
949
|
+
});
|
|
950
|
+
res = await r.json();
|
|
951
|
+
if (!r.ok) throw new Error(res.error ?? `HTTP ${r.status}`);
|
|
952
|
+
} catch (err) {
|
|
953
|
+
console.error(`Sync failed: ${err.message}`);
|
|
954
|
+
console.log(`${c.faint}Token may be expired. Re-run ${c.green}tokenflexing login${c.faint}.${c.reset}`);
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
console.log(`${c.green}✓ Synced ${res.synced} source(s)${c.reset} at ${res.timestamp}`);
|
|
959
|
+
console.log(`${c.faint}View your profile at ${SITE}${c.reset}`);
|
|
960
|
+
console.log("");
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** Return all sources that were successfully parsed and have data (for sync). */
|
|
964
|
+
function collectSources() {
|
|
965
|
+
return runScan()
|
|
966
|
+
.filter((r) => r.hasData)
|
|
967
|
+
.map((r) => ({ id: r.id, name: r.name, stats: r.stats }));
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function commandVersion() { console.log("1.1.0"); }
|
|
971
|
+
|
|
972
|
+
function commandHelp() {
|
|
973
|
+
console.log("");
|
|
974
|
+
console.log(`${c.bold}tokenflexing${c.reset}${c.faint} — show off your AI token usage${c.reset}`);
|
|
975
|
+
console.log("");
|
|
976
|
+
console.log(` ${c.green}tokenflexing${c.reset} show stats (default)`);
|
|
977
|
+
console.log(` ${c.green}tokenflexing stats${c.reset} breakdown per source + model`);
|
|
978
|
+
console.log(` ${c.green}tokenflexing scan${c.reset} detect ALL AI tools on this device`);
|
|
979
|
+
console.log(` ${c.green}tokenflexing flex${c.reset} print a shareable ASCII card`);
|
|
980
|
+
console.log(` ${c.green}tokenflexing daemon${c.reset} preview daily auto-refresh daemon`);
|
|
981
|
+
console.log(` ${c.green}tokenflexing daemon --install${c.reset} install macOS LaunchAgent / Windows task`);
|
|
982
|
+
console.log(` ${c.green}tokenflexing daemon --uninstall${c.reset} remove daemon`);
|
|
983
|
+
console.log(` ${c.green}tokenflexing mcp${c.reset} start MCP server (Claude Code, Cursor, etc.)`);
|
|
984
|
+
console.log(` ${c.green}tokenflexing install-hooks --apply${c.reset} wire MCP into Claude Code or Cursor`);
|
|
985
|
+
console.log(` ${c.green}tokenflexing login${c.reset} sign in and save a cloud sync token`);
|
|
986
|
+
console.log(` ${c.green}tokenflexing login --token <tok>${c.reset} save a token from tokenflexing.com/settings`);
|
|
987
|
+
console.log(` ${c.green}tokenflexing sync${c.reset} push local stats to your profile`);
|
|
988
|
+
console.log(` ${c.green}tokenflexing --version${c.reset} show version`);
|
|
989
|
+
console.log("");
|
|
990
|
+
console.log(` ${c.faint}Parsed today: Claude Code · Codex CLI${c.reset}`);
|
|
991
|
+
console.log(` ${c.faint}Detected (25+ tools): Cursor · Windsurf · Cline · Roo Code · Kilo Code${c.reset}`);
|
|
992
|
+
console.log(` ${c.faint} Zed · Gemini CLI · Amp · OpenCode · Antigravity${c.reset}`);
|
|
993
|
+
console.log(` ${c.faint} Copilot · Goose · Kiro · Mux · Kimi · Crush · more${c.reset}`);
|
|
994
|
+
console.log(` ${c.faint}Never reads prompts or completions — only usage blocks.${c.reset}`);
|
|
995
|
+
console.log("");
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function arg(args, name) {
|
|
999
|
+
const i = args.indexOf(name);
|
|
1000
|
+
return i === -1 ? undefined : args[i + 1];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/* ───── Entry ──────────────────────────────────────────────────── */
|
|
1004
|
+
|
|
1005
|
+
export async function main(argv) {
|
|
1006
|
+
const cmd = argv[0] ?? "stats";
|
|
1007
|
+
switch (cmd) {
|
|
1008
|
+
case "stats":
|
|
1009
|
+
case undefined: return commandStats();
|
|
1010
|
+
case "scan": return commandScan();
|
|
1011
|
+
case "flex": return commandFlex();
|
|
1012
|
+
case "daemon": return commandDaemon(argv.slice(1));
|
|
1013
|
+
case "mcp": return commandMcp();
|
|
1014
|
+
case "install-hooks": return commandInstallHooks(argv.slice(1));
|
|
1015
|
+
case "login": return commandLogin(argv.slice(1));
|
|
1016
|
+
case "sync": return commandSync();
|
|
1017
|
+
case "--version":
|
|
1018
|
+
case "-V": return commandVersion();
|
|
1019
|
+
case "--help":
|
|
1020
|
+
case "-h":
|
|
1021
|
+
case "help": return commandHelp();
|
|
1022
|
+
default:
|
|
1023
|
+
console.error(`Unknown command: ${cmd}`);
|
|
1024
|
+
console.error("Run 'tokenflexing --help' for usage.");
|
|
1025
|
+
process.exit(1);
|
|
1026
|
+
}
|
|
1027
|
+
}
|