token-flex 1.0.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 ADDED
@@ -0,0 +1,256 @@
1
+ # Claude Tracker CLI
2
+
3
+ Track your Claude Code token usage and compete on the global leaderboard!
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g token-flex
9
+ ```
10
+
11
+ Or use directly with npx:
12
+
13
+ ```bash
14
+ npx token-flex upload
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ### First Time Setup
20
+
21
+ When you first run the CLI, you'll be prompted to configure:
22
+
23
+ ```bash
24
+ token-flex upload
25
+ ```
26
+
27
+ You'll need to provide:
28
+ 1. **API Endpoint**: The URL of your Tokenize web server (default: http://localhost:3000)
29
+ 2. **Username**: Your display name on the leaderboard
30
+
31
+ ### Upload Your Usage
32
+
33
+ ```bash
34
+ token-flex upload
35
+ ```
36
+
37
+ This will:
38
+ - Scan your Claude Code logs for the current session
39
+ - Calculate total token usage and cost
40
+ - Upload the data to the leaderboard
41
+ - Display your current ranking and position
42
+
43
+ ### View Your Configuration
44
+
45
+ ```bash
46
+ token-flex config
47
+ ```
48
+
49
+ ### Reset Configuration
50
+
51
+ To reconfigure the CLI:
52
+
53
+ ```bash
54
+ token-flex config
55
+ # Follow prompts to update settings
56
+ ```
57
+
58
+ ## Development
59
+
60
+ ### Build
61
+
62
+ ```bash
63
+ npm run build
64
+ ```
65
+
66
+ ### Run Locally
67
+
68
+ ```bash
69
+ npm run start upload
70
+ ```
71
+
72
+ ### Create Package
73
+
74
+ ```bash
75
+ npm pack
76
+ ```
77
+
78
+ This creates `token-flex-1.0.0.tgz` which can be installed with:
79
+
80
+ ```bash
81
+ npm install -g token-flex-1.0.0.tgz
82
+ ```
83
+
84
+ ## How It Works
85
+
86
+ ### Token Extraction
87
+
88
+ The CLI scans your Claude Code log files to extract token usage information:
89
+
90
+ 1. **Log Location**: `~/.claude/logs/<date>-conversation.json`
91
+ 2. **Token Counting**: Uses the `tokscale` package for accurate counting
92
+ 3. **Cost Calculation**: Based on model pricing (Sonnet 4: $2/M tokens)
93
+
94
+ ### Data Uploaded
95
+
96
+ Only aggregated statistics are uploaded - no conversation content:
97
+
98
+ - Total tokens used
99
+ - Total cost in USD
100
+ - Number of requests
101
+ - Upload timestamp
102
+ - Your username
103
+
104
+ ### Privacy
105
+
106
+ - No conversation content or prompts are uploaded
107
+ - Only aggregated statistics are sent
108
+ - Your user ID is anonymized
109
+ - Uploads are opt-in
110
+
111
+ ## Commands
112
+
113
+ ### `upload`
114
+
115
+ Upload your current session's token usage to the leaderboard.
116
+
117
+ ```bash
118
+ token-flex upload
119
+ ```
120
+
121
+ **Options:**
122
+ - `--force`: Upload even if no new tokens detected
123
+ - `--verbose`: Show detailed information
124
+
125
+ **Example output:**
126
+ ```
127
+ ✓ Found 15 requests with 125,000 tokens
128
+ ✓ Total cost: $0.25
129
+ ✓ Uploading to http://localhost:3000...
130
+ ✓ Upload successful!
131
+
132
+ Your stats:
133
+ Tokens: 125,000
134
+ Cost: $0.25
135
+ Rank: #42
136
+
137
+ View the leaderboard: http://localhost:3000
138
+ ```
139
+
140
+ ### `config`
141
+
142
+ Configure or view your CLI settings.
143
+
144
+ ```bash
145
+ token-flex config
146
+ ```
147
+
148
+ **Settings:**
149
+ - `apiEndpoint`: URL of the Tokenize web server
150
+ - `username`: Your display name
151
+
152
+ ## Configuration File
153
+
154
+ Configuration is stored in:
155
+ - macOS: `~/Library/Preferences/token-flex/config.json`
156
+ - Linux: `~/.config/token-flex/config.json`
157
+ - Windows: `%APPDATA%\token-flex\config.json`
158
+
159
+ ## Troubleshooting
160
+
161
+ ### "No logs found"
162
+
163
+ Make sure you have:
164
+ 1. Used Claude Code at least once
165
+ 2. Logs are in `~/.claude/logs/`
166
+ 3. Current session has activity
167
+
168
+ ### "Upload failed"
169
+
170
+ Check:
171
+ 1. The web server is running (`cd packages/web && pnpm dev`)
172
+ 2. API endpoint is correct (`token-flex config`)
173
+ 3. Network connectivity
174
+
175
+ ### "Invalid credentials"
176
+
177
+ Make sure:
178
+ 1. Your username is set (`token-flex config`)
179
+ 2. Username is not already taken by another user
180
+
181
+ ## Examples
182
+
183
+ ### Basic Upload
184
+
185
+ ```bash
186
+ $ token-flex upload
187
+ ✓ Found 23 requests with 145,000 tokens
188
+ ✓ Total cost: $0.29
189
+ ✓ Uploading to leaderboard...
190
+ ✓ Upload successful!
191
+
192
+ Your stats:
193
+ Username: alice_dev
194
+ Tokens: 145,000
195
+ Cost: $0.29
196
+ Rank: #15
197
+
198
+ View leaderboard: http://localhost:3000
199
+ ```
200
+
201
+ ### With Verbose Output
202
+
203
+ ```bash
204
+ $ token-flex upload --verbose
205
+ Scanning logs...
206
+ Log file: ~/.claude/logs/2024-02-23-conversation.json
207
+ Requests: 23
208
+ Tokens: 145,000
209
+ Cost: $0.29
210
+
211
+ Uploading...
212
+ Endpoint: http://localhost:3000/api/upload
213
+ Payload: {userId: 'xxx', username: 'alice_dev', tokens: 145000, ...}
214
+
215
+ Response: 200 OK
216
+ ✓ Upload successful!
217
+ ```
218
+
219
+ ## API Integration
220
+
221
+ The CLI communicates with the Tokenize web API:
222
+
223
+ **POST /api/upload**
224
+
225
+ ```typescript
226
+ {
227
+ userId: string; // Unique user identifier
228
+ username: string; // Display name
229
+ tokens: number; // Token count
230
+ cost: number; // Cost in USD
231
+ model: string; // Model used (default: "claude-sonnet-4")
232
+ timestamp: string; // ISO timestamp
233
+ }
234
+ ```
235
+
236
+ **Response:**
237
+
238
+ ```typescript
239
+ {
240
+ success: true;
241
+ message: string;
242
+ stats: {
243
+ totalTokens: number;
244
+ totalCost: number;
245
+ rank: number;
246
+ };
247
+ }
248
+ ```
249
+
250
+ ## Contributing
251
+
252
+ Contributions welcome! Please feel free to submit a Pull Request.
253
+
254
+ ## License
255
+
256
+ MIT
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ApiClient = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ class ApiClient {
10
+ constructor(config) {
11
+ this.config = config;
12
+ }
13
+ async uploadUsage(usage, username) {
14
+ try {
15
+ const response = await axios_1.default.post(`${this.config.server_url}/api/tokenize/upload`, {
16
+ total_tokens: usage.total_tokens,
17
+ username: username || this.config.display_name, // Use display_name as username if not provided
18
+ display_name: this.config.display_name
19
+ }, {
20
+ headers: {
21
+ 'Authorization': `Bearer ${this.config.api_token}`,
22
+ 'Content-Type': 'application/json'
23
+ }
24
+ });
25
+ console.log(chalk_1.default.green('✅ Successfully uploaded your token usage!'));
26
+ console.log(chalk_1.default.gray(`Total tokens: ${usage.total_tokens}\n`));
27
+ return true;
28
+ }
29
+ catch (error) {
30
+ if (axios_1.default.isAxiosError(error)) {
31
+ const axiosError = error;
32
+ if (axiosError.response?.status === 401) {
33
+ console.log(chalk_1.default.red('❌ Invalid API token. Please re-run setup.'));
34
+ console.log(chalk_1.default.gray('Delete ~/.config/configstore/tokenize.json and try again.\n'));
35
+ }
36
+ else if (axiosError.response?.status === 409) {
37
+ console.log(chalk_1.default.red('❌ Username already taken. Please choose a different display name.'));
38
+ console.log(chalk_1.default.gray('Delete ~/.config/configstore/tokenize.json and run: npx token-flex upload\n'));
39
+ }
40
+ else if (axiosError.code === 'ECONNREFUSED' || !axiosError.response) {
41
+ console.log(chalk_1.default.red('❌ Cannot connect to server. Please try again later.'));
42
+ console.log(chalk_1.default.gray(`Server: ${this.config.server_url}\n`));
43
+ }
44
+ else {
45
+ console.log(chalk_1.default.red('❌ Upload failed. Please try again later.\n'));
46
+ }
47
+ }
48
+ else {
49
+ console.log(chalk_1.default.red('❌ Unexpected error occurred.\n'));
50
+ }
51
+ return false;
52
+ }
53
+ }
54
+ }
55
+ exports.ApiClient = ApiClient;
package/dist/config.js ADDED
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ConfigManager = void 0;
7
+ const configstore_1 = __importDefault(require("configstore"));
8
+ const uuid_1 = require("uuid");
9
+ const CONFIG_NAME = 'tokenize';
10
+ class ConfigManager {
11
+ constructor() {
12
+ this.config = new configstore_1.default(CONFIG_NAME);
13
+ }
14
+ getConfig() {
15
+ const apiToken = this.config.get('api_token');
16
+ if (!apiToken)
17
+ return null;
18
+ return {
19
+ api_token: apiToken,
20
+ display_name: this.config.get('display_name') || '',
21
+ server_url: this.config.get('server_url') || 'https://tokenize.example.com',
22
+ username: this.config.get('username')
23
+ };
24
+ }
25
+ saveConfig(config) {
26
+ this.config.set('api_token', config.api_token);
27
+ this.config.set('display_name', config.display_name);
28
+ this.config.set('server_url', config.server_url);
29
+ if (config.username) {
30
+ this.config.set('username', config.username);
31
+ }
32
+ }
33
+ generateApiToken() {
34
+ return (0, uuid_1.v4)();
35
+ }
36
+ }
37
+ exports.ConfigManager = ConfigManager;
package/dist/index.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const config_1 = require("./config");
10
+ const setup_1 = require("./setup");
11
+ const token_extractor_1 = require("./token-extractor");
12
+ const api_client_1 = require("./api-client");
13
+ const program = new commander_1.Command();
14
+ program
15
+ .name('token-flex')
16
+ .description('Track your Claude Code token usage')
17
+ .version('1.0.0');
18
+ program
19
+ .command('upload')
20
+ .description('Upload your token usage to the leaderboard')
21
+ .action(async () => {
22
+ const configManager = new config_1.ConfigManager();
23
+ let config = configManager.getConfig();
24
+ if (!config) {
25
+ const setupHandler = new setup_1.SetupHandler();
26
+ config = await setupHandler.runSetup();
27
+ }
28
+ console.log(chalk_1.default.blue('📊 Extracting your token usage...'));
29
+ const extractor = new token_extractor_1.TokenExtractor();
30
+ const usage = await extractor.extractTokens();
31
+ if (!extractor.validateTokenUsage(usage)) {
32
+ console.log(chalk_1.default.red('❌ Invalid token data. Please try again.\n'));
33
+ return;
34
+ }
35
+ console.log(chalk_1.default.blue('🚀 Uploading to leaderboard...'));
36
+ const apiClient = new api_client_1.ApiClient(config);
37
+ await apiClient.uploadUsage(usage, config.username);
38
+ });
39
+ program.parse();
package/dist/setup.js ADDED
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SetupHandler = void 0;
7
+ const inquirer_1 = __importDefault(require("inquirer"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const config_1 = require("./config");
13
+ class SetupHandler {
14
+ constructor() {
15
+ this.configManager = new config_1.ConfigManager();
16
+ }
17
+ async runSetup() {
18
+ console.log(chalk_1.default.blue('\n👋 Welcome to Token Flex!'));
19
+ console.log(chalk_1.default.gray('Let\'s set up your account...\n'));
20
+ // Try to get Claude Code username
21
+ const claudeUsername = this.getClaudeUsername();
22
+ // Prompt for display name only
23
+ const answers = await inquirer_1.default.prompt([
24
+ {
25
+ type: 'input',
26
+ name: 'display_name',
27
+ message: 'Choose a display name for the leaderboard:',
28
+ default: claudeUsername || 'Anonymous'
29
+ }
30
+ ]);
31
+ // Generate API token
32
+ const apiToken = this.configManager.generateApiToken();
33
+ // Hardcode the production server URL
34
+ const config = {
35
+ api_token: apiToken,
36
+ display_name: answers.display_name,
37
+ server_url: 'https://tokenize-leaderboard.fly.dev',
38
+ username: claudeUsername || answers.display_name // Use Claude username or fall back to display name
39
+ };
40
+ // Save config
41
+ this.configManager.saveConfig(config);
42
+ console.log(chalk_1.default.green('\n✅ Setup complete!'));
43
+ console.log(chalk_1.default.gray(`Your API token has been saved to ~/.config/configstore/tokenize.json\n`));
44
+ return config;
45
+ }
46
+ getClaudeUsername() {
47
+ try {
48
+ const settingsPath = path_1.default.join(os_1.default.homedir(), '.claude', 'settings.json');
49
+ if (fs_1.default.existsSync(settingsPath)) {
50
+ const settings = JSON.parse(fs_1.default.readFileSync(settingsPath, 'utf-8'));
51
+ return settings.username || null;
52
+ }
53
+ }
54
+ catch (error) {
55
+ // Ignore errors
56
+ }
57
+ return null;
58
+ }
59
+ }
60
+ exports.SetupHandler = SetupHandler;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TokenExtractor = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
10
+ class TokenExtractor {
11
+ constructor() {
12
+ const homeDir = os_1.default.homedir();
13
+ this.claudeConfigPath = path_1.default.join(homeDir, '.claude', 'history.jsonl');
14
+ this.statsCachePath = path_1.default.join(homeDir, '.claude', 'stats-cache.json');
15
+ }
16
+ async extractTokens() {
17
+ // Try stats-cache.json first (most accurate)
18
+ const statsResult = this.parseStatsCache();
19
+ if (statsResult.total_tokens > 0) {
20
+ return statsResult;
21
+ }
22
+ // Try tokscale second
23
+ try {
24
+ const tokscale = require('tokscale');
25
+ const result = await tokscale({ claude: true });
26
+ if (result && result.total !== undefined) {
27
+ return { total_tokens: result.total };
28
+ }
29
+ }
30
+ catch (error) {
31
+ // Fall back to manual parsing
32
+ console.log('tokscale not available, using manual parsing...');
33
+ }
34
+ // Fallback: Parse history.jsonl manually
35
+ return this.parseHistoryJsonl();
36
+ }
37
+ parseStatsCache() {
38
+ try {
39
+ if (!fs_1.default.existsSync(this.statsCachePath)) {
40
+ return { total_tokens: 0 };
41
+ }
42
+ const content = fs_1.default.readFileSync(this.statsCachePath, 'utf-8');
43
+ const stats = JSON.parse(content);
44
+ let totalTokens = 0;
45
+ // Sum tokens from modelUsage
46
+ if (stats.modelUsage) {
47
+ for (const model in stats.modelUsage) {
48
+ const usage = stats.modelUsage[model];
49
+ totalTokens += (usage.inputTokens || 0) + (usage.outputTokens || 0);
50
+ }
51
+ }
52
+ return { total_tokens: totalTokens };
53
+ }
54
+ catch (error) {
55
+ console.error('Error parsing stats cache:', error);
56
+ return { total_tokens: 0 };
57
+ }
58
+ }
59
+ parseHistoryJsonl() {
60
+ try {
61
+ if (!fs_1.default.existsSync(this.claudeConfigPath)) {
62
+ console.log('No Claude Code history found, starting with 0 tokens');
63
+ return { total_tokens: 0 };
64
+ }
65
+ const content = fs_1.default.readFileSync(this.claudeConfigPath, 'utf-8');
66
+ const lines = content.trim().split('\n');
67
+ let totalTokens = 0;
68
+ for (const line of lines) {
69
+ try {
70
+ const entry = JSON.parse(line);
71
+ // Sum tokens from message usage data
72
+ if (entry.usage) {
73
+ totalTokens += (entry.usage.input_tokens || 0) + (entry.usage.output_tokens || 0);
74
+ }
75
+ }
76
+ catch (parseError) {
77
+ // Skip invalid lines
78
+ continue;
79
+ }
80
+ }
81
+ return { total_tokens: totalTokens };
82
+ }
83
+ catch (error) {
84
+ console.error('Error parsing history:', error);
85
+ return { total_tokens: 0 };
86
+ }
87
+ }
88
+ validateTokenUsage(usage) {
89
+ return typeof usage.total_tokens === 'number' && usage.total_tokens >= 0;
90
+ }
91
+ }
92
+ exports.TokenExtractor = TokenExtractor;
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "token-flex",
3
+ "version": "1.0.0",
4
+ "description": "Track your Claude Code token usage",
5
+ "author": "Gibson Tang",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/gibtang/tokenize.git"
10
+ },
11
+ "homepage": "https://github.com/gibtang/tokenize#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/gibtang/tokenize/issues"
14
+ },
15
+ "keywords": [
16
+ "claude",
17
+ "claude-code",
18
+ "token",
19
+ "tracking",
20
+ "cli",
21
+ "anthropic",
22
+ "ai",
23
+ "leaderboard"
24
+ ],
25
+ "engines": {
26
+ "node": ">=16.0.0"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "bin": {
34
+ "token-flex": "dist/index.js"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "start": "node dist/index.js",
39
+ "dev": "ts-node src/index.ts"
40
+ },
41
+ "dependencies": {
42
+ "axios": "^1.6.5",
43
+ "chalk": "^4.1.2",
44
+ "commander": "^11.1.0",
45
+ "configstore": "^5.0.1",
46
+ "inquirer": "^8.2.5",
47
+ "tokscale": "^1.0.0",
48
+ "uuid": "^13.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/configstore": "^6.0.0",
52
+ "@types/inquirer": "^9.0.6",
53
+ "@types/node": "^20.10.6",
54
+ "@types/uuid": "^11.0.0",
55
+ "ts-node": "^10.9.2",
56
+ "typescript": "^5.3.3"
57
+ }
58
+ }