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 +256 -0
- package/dist/api-client.js +55 -0
- package/dist/config.js +37 -0
- package/dist/index.js +39 -0
- package/dist/setup.js +60 -0
- package/dist/token-extractor.js +92 -0
- package/dist/types.js +2 -0
- package/package.json +58 -0
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
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
|
+
}
|