token-flex 1.0.0 → 1.0.1
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/package.json +7 -7
- package/dist/api-client.js +0 -55
- package/dist/config.js +0 -37
- package/dist/index.js +0 -39
- package/dist/setup.js +0 -60
- package/dist/token-extractor.js +0 -92
- package/dist/types.js +0 -2
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-flex",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"author": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "See your Claude Code tokens on a leaderboard",
|
|
5
|
+
"author": "feedcode",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/gibtang/
|
|
9
|
+
"url": "https://github.com/gibtang/token-flex.git"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://github.com/gibtang/
|
|
11
|
+
"homepage": "https://github.com/gibtang/token-flex#readme",
|
|
12
12
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/gibtang/
|
|
13
|
+
"url": "https://github.com/gibtang/token-flex/issues"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
16
|
"claude",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"LICENSE"
|
|
32
32
|
],
|
|
33
33
|
"bin": {
|
|
34
|
-
"token-flex": "dist/index.js"
|
|
34
|
+
"token-flex": "./dist/index.js"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsc",
|
package/dist/api-client.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
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;
|
package/dist/token-extractor.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
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
DELETED