UNPKG

vibetime

Version:

Track your Claude AI usage and costs. Built on ccusage. See rankings, sync data, and monitor your AI spending. Works with all Claude models.

510 lines (501 loc) 24.2 kB
#!/usr/bin/env node var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/index.ts import { Command as Command4 } from "commander"; import { readFileSync as readFileSync2 } from "fs"; import { fileURLToPath } from "url"; import { dirname, join as join2 } from "path"; import pc2 from "picocolors"; // src/ccusage-proxy.ts import { spawn } from "child_process"; async function executeCcusage(args) { return new Promise((resolve) => { console.log(`\u{1F50D} Executing ccusage: npx ccusage ${args.join(" ")}`); const child = spawn("npx", ["ccusage", ...args], { stdio: ["inherit", "pipe", "pipe"], shell: false }); let stdout = ""; let stderr = ""; child.stdout.on("data", (data) => { const output = data.toString(); stdout += output; process.stdout.write(output); }); child.stderr.on("data", (data) => { const output = data.toString(); stderr += output; process.stderr.write(output); }); child.on("close", (code) => { resolve({ stdout, stderr, exitCode: code || 0 }); }); child.on("error", (error) => { resolve({ stdout: "", stderr: error.message, exitCode: 1 }); }); }); } __name(executeCcusage, "executeCcusage"); async function getCcusageData(command, options = []) { return new Promise((resolve, reject) => { const args = [command, "--json", ...options]; const child = spawn("npx", ["ccusage", ...args], { stdio: ["inherit", "pipe", "pipe"], shell: false }); let stdout = ""; let stderr = ""; child.stdout.on("data", (data) => { stdout += data.toString(); }); child.stderr.on("data", (data) => { stderr += data.toString(); }); child.on("close", (code) => { if (code !== 0) { reject(new Error(`ccusage failed with code ${code}: ${stderr}`)); return; } try { const lines = stdout.split("\n"); let jsonStr = ""; let foundJson = false; for (const line of lines) { if (line.trim().startsWith("{")) { foundJson = true; } if (foundJson) { jsonStr += line + "\n"; } } if (!jsonStr.trim()) { reject(new Error(`No JSON output from ccusage: ${stdout}`)); return; } const parsed = JSON.parse(jsonStr.trim()); resolve(parsed); } catch (error) { reject(new Error(`Failed to parse ccusage JSON output: ${error} Output: ${stdout}`)); } }); child.on("error", (error) => { reject(error); }); }); } __name(getCcusageData, "getCcusageData"); // src/config.ts import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import { input, password } from "@inquirer/prompts"; import chalk from "chalk"; var CONFIG_DIR = path.join(os.homedir(), ".vibetime"); var CONFIG_FILE = path.join(CONFIG_DIR, "config.json"); function ensureConfigDir() { if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, { recursive: true }); } } __name(ensureConfigDir, "ensureConfigDir"); function loadConfig() { ensureConfigDir(); if (fs.existsSync(CONFIG_FILE)) { try { const data = fs.readFileSync(CONFIG_FILE, "utf-8"); return JSON.parse(data); } catch (error) { console.error(chalk.yellow("\u26A0\uFE0F Failed to load configuration file")); return {}; } } return {}; } __name(loadConfig, "loadConfig"); function saveConfig(config) { ensureConfigDir(); try { fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); } catch (error) { console.error(chalk.red("\u274C Failed to save configuration file")); throw error; } } __name(saveConfig, "saveConfig"); async function setupApiKey(forceSetup = false) { const config = loadConfig(); if (config.apiKey && !forceSetup) { return config.apiKey; } console.log(chalk.cyan("\n\u{1F511} VibeTime API Key Setup\n")); if (config.apiKey && forceSetup) { console.log(chalk.yellow("Current API Key: ") + chalk.gray(`...${config.apiKey.slice(-8)}`)); console.log(chalk.gray("Enter a new API key or press Enter to keep the current one\n")); } else { console.log(chalk.gray("You can get your API key from the VibeTime dashboard:")); console.log(chalk.blue("https://vibetime.ekusiadadus.workers.dev/dashboard/api-keys\n")); } const apiKey = await password({ message: "Enter your API key:", mask: "\u2022", validate: /* @__PURE__ */ __name((value) => { if (!value && config.apiKey && forceSetup) { return true; } if (!value || value.trim() === "") { return "API key is required"; } if (!value.startsWith("vbt_")) { return 'API key must start with "vbt_"'; } if (value.length < 20) { return "API key is too short"; } return true; }, "validate") }); const finalApiKey = apiKey && apiKey.trim() || config.apiKey || ""; const currentEndpoint = config.apiEndpoint || "https://vibetime.ekusiadadus.workers.dev"; console.log(chalk.gray(` API Endpoint (default: ${currentEndpoint})`)); const endpoint = await input({ message: "API endpoint:", default: currentEndpoint }); const newConfig = { apiKey: finalApiKey, apiEndpoint: endpoint || currentEndpoint }; saveConfig(newConfig); console.log(chalk.green("\n\u2705 Configuration saved!\n")); console.log(chalk.gray("Configuration file: ") + CONFIG_FILE); return finalApiKey; } __name(setupApiKey, "setupApiKey"); async function getApiKey(options) { if (options?.apiKey) { return options.apiKey; } if (process.env.VIBE_API_KEY) { return process.env.VIBE_API_KEY; } const config = loadConfig(); if (config.apiKey) { return config.apiKey; } console.log(chalk.red("\n\u274C API key not configured")); console.log(chalk.gray("An API key is required to use VibeTime.\n")); console.log(chalk.cyan("\u{1F4CB} How to get your API key:")); console.log(chalk.white("1. Visit the VibeTime dashboard:")); console.log(chalk.blue(" \u2192 https://vibetime.ekusiadadus.workers.dev/dashboard/api-keys")); console.log(chalk.white("2. Create a new API key")); console.log(chalk.white("3. Configure it using: ") + chalk.green("vibetime config set")); console.log(chalk.gray("\nAlternatively, set the VIBE_API_KEY environment variable.")); throw new Error("API key is required. Please configure it first."); } __name(getApiKey, "getApiKey"); function getApiEndpoint() { const config = loadConfig(); return config.apiEndpoint || process.env.VIBE_API_ENDPOINT || "https://vibetime.ekusiadadus.workers.dev"; } __name(getApiEndpoint, "getApiEndpoint"); function resetConfig() { if (fs.existsSync(CONFIG_FILE)) { fs.unlinkSync(CONFIG_FILE); console.log(chalk.green("\u2705 Configuration reset successfully")); } else { console.log(chalk.yellow("\u26A0\uFE0F Configuration file not found")); } } __name(resetConfig, "resetConfig"); // src/commands/sync.ts import { Command } from "commander"; import ora from "ora"; import pc from "picocolors"; var syncCommand = new Command("sync").description("Sync Claude usage data to database").option("-k, --api-key <key>", "API key ID to sync data for").option("--since <date>", "Start date for syncing (ISO format)").option("--until <date>", "End date for syncing (ISO format)").option("--dry-run", "Show what would be synced without actually syncing").option("--remote", "Sync to remote VibeTime server (default)").option("--local", "Sync to local database only").action(async (options) => { const spinner = ora("Initializing...").start(); try { let apiKey; try { apiKey = await getApiKey({ apiKey: options.apiKey }); } catch (error) { spinner.stop(); console.log(pc.yellow("\n\u26A0\uFE0F API key is required for syncing data to VibeTime cloud.\n")); const confirm = await import("@inquirer/prompts").then((m) => m.confirm({ message: "Would you like to set up your API key now?", default: true })); if (confirm) { apiKey = await setupApiKey(); } else { console.log(pc.gray("\nTo configure later, run: ") + pc.green("vibetime config set")); process.exit(0); } spinner.start("Initializing..."); } const apiEndpoint = getApiEndpoint(); spinner.text = "Loading usage data from ccusage..."; const ccusageOptions = []; if (options.since) { const sinceDate = new Date(options.since); const sinceDateStr = sinceDate.toISOString().split("T")[0].replace(/-/g, ""); ccusageOptions.push("--since", sinceDateStr); } if (options.until) { const untilDate = new Date(options.until); const untilDateStr = untilDate.toISOString().split("T")[0].replace(/-/g, ""); ccusageOptions.push("--until", untilDateStr); } ccusageOptions.push("--mode", "calculate"); ccusageOptions.push("--instances"); const dailyData = await getCcusageData("daily", ccusageOptions); let dailyArray = []; if (dailyData.daily && Array.isArray(dailyData.daily)) { dailyArray = dailyData.daily; } else if (dailyData.projects) { for (const projectDays of Object.values(dailyData.projects)) { dailyArray = dailyArray.concat(projectDays); } } if (dailyArray.length === 0) { spinner.succeed("No usage data found for the specified period"); return; } spinner.text = `Processing ${dailyArray.length} days of usage data...`; const dailyAggregates = dailyArray.map((day) => { if (day.modelBreakdowns && day.modelBreakdowns.length > 0) { return day.modelBreakdowns.map((mb) => ({ date: day.date, model: mb.modelName || mb.model || "unknown", totalInputTokens: mb.inputTokens || 0, totalOutputTokens: mb.outputTokens || 0, totalCacheCreationTokens: mb.cacheCreationTokens || 0, totalCacheReadTokens: mb.cacheReadTokens || 0, totalCostUSD: mb.cost || 0 })); } else { return [{ date: day.date, model: day.modelsUsed?.[0] || "unknown", totalInputTokens: day.inputTokens || 0, totalOutputTokens: day.outputTokens || 0, totalCacheCreationTokens: day.cacheCreationTokens || 0, totalCacheReadTokens: day.cacheReadTokens || 0, totalCostUSD: day.totalCost || 0 }]; } }).flat(); spinner.text = `Syncing ${dailyAggregates.length} aggregates to VibeTime API...`; if (options.dryRun) { spinner.succeed("Dry run - showing what would be synced:"); console.log(pc.cyan("\nDaily aggregates to sync:")); const byDate = dailyAggregates.reduce((acc, agg) => { if (!acc[agg.date]) acc[agg.date] = []; acc[agg.date].push(agg); return acc; }, {}); Object.entries(byDate).forEach(([date, aggs]) => { const totalCost = aggs.reduce((sum, a) => sum + a.totalCostUSD, 0); const totalTokens = aggs.reduce((sum, a) => sum + a.totalInputTokens + a.totalOutputTokens + a.totalCacheCreationTokens + a.totalCacheReadTokens, 0); console.log(` ${date}: ${totalTokens.toLocaleString()} tokens, $${totalCost.toFixed(2)} (${aggs.length} models)`); }); return; } const response = await fetch(`${apiEndpoint}/api/usage/daily`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` }, body: JSON.stringify({ dailyAggregates }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`API request failed: ${response.status} ${response.statusText} ${errorText}`); } const result = await response.json(); spinner.succeed(`\u2705 ${result.message || `Successfully synced ${result.count} aggregates`}`); console.log(pc.green("\nSync Summary:")); const totalDays = new Set(dailyAggregates.map((a) => a.date)).size; const totalModels = new Set(dailyAggregates.map((a) => a.model)).size; console.log(`- Days synced: ${totalDays}`); console.log(`- Models tracked: ${totalModels}`); console.log(`- Total aggregates: ${result.count}`); console.log(pc.cyan("\n\u{1F4CA} View your usage dashboard:")); console.log(pc.blue(`${apiEndpoint}/dashboard/usage`)); } catch (error) { spinner.fail("Sync failed"); if (error instanceof Error) { console.error(pc.red("Error:"), error.message); } else { console.error(pc.red("Error:"), error); } process.exit(1); } }); // src/commands/config.ts import { Command as Command2 } from "commander"; import chalk2 from "chalk"; var configCommand = new Command2("config").description("Manage VibeTime CLI configuration").action(async () => { const config = loadConfig(); console.log(chalk2.cyan("\n\u{1F4CB} Current Configuration\n")); if (config.apiKey) { console.log(chalk2.green("\u2713") + " API Key: " + chalk2.gray(`...${config.apiKey.slice(-8)}`)); } else { console.log(chalk2.red("\u2717") + " API Key: " + chalk2.gray("Not configured")); } console.log(" API Endpoint: " + chalk2.gray(config.apiEndpoint || "Default")); console.log("\n" + chalk2.gray("Run `vibetime config set` to change configuration")); }); configCommand.command("set").description("Set API key and configuration interactively").action(async () => { await setupApiKey(true); }); configCommand.command("reset").description("Reset all configuration").action(() => { resetConfig(); }); configCommand.command("show").description("Show current configuration").action(() => { const config = loadConfig(); console.log(chalk2.cyan("\n\u{1F4CB} Configuration:\n")); console.log(JSON.stringify(config, null, 2)); }); // src/commands/test.ts import { Command as Command3 } from "commander"; import chalk4 from "chalk"; // src/api.ts import chalk3 from "chalk"; async function validateApiKey(apiKey) { try { const key = apiKey || await getApiKey(); const endpoint = getApiEndpoint(); const response = await fetch(`${endpoint}/api/validate`, { method: "GET", headers: { "Authorization": `Bearer ${key}` } }); if (response.ok) { const result = await response.json(); return result.valid === true; } return false; } catch (error) { return false; } } __name(validateApiKey, "validateApiKey"); async function testApiConnection(apiKey) { console.log(chalk3.gray("\n\u{1F50D} Testing API connection...")); const isValid = await validateApiKey(apiKey); if (isValid) { console.log(chalk3.green("\u2705 API connection successful!")); return true; } else { console.log(chalk3.red("\u274C API connection failed")); console.log(chalk3.yellow("\nPlease check the following:")); console.log("1. Is the API key correct?"); console.log("2. Is the VibeTime server running?"); console.log("3. Is the network connection working?"); return false; } } __name(testApiConnection, "testApiConnection"); // src/commands/test.ts var testCommand = new Command3("test").description("Test API connection and API key").option("-k, --api-key <key>", "API key to test").action(async (options) => { console.log(chalk4.cyan("\n\u{1F9EA} VibeTime Connection Test\n")); try { const apiKey = await getApiKey(options); const endpoint = getApiEndpoint(); console.log("API Endpoint: " + chalk4.gray(endpoint)); console.log("API Key: " + chalk4.gray(`...${apiKey.slice(-8)}`)); const success = await testApiConnection(apiKey); if (success) { console.log(chalk4.green("\n\u2705 All tests passed!")); console.log(chalk4.gray("VibeTime is ready to use.")); } else { console.log(chalk4.red("\n\u274C Test failed")); console.log(chalk4.gray("Please reconfigure your API key with `vibetime config set`.")); process.exit(1); } } catch (error) { console.error(chalk4.red("\n\u274C Error:"), error.message); process.exit(1); } }); // src/index.ts var __filename2 = fileURLToPath(import.meta.url); var __dirname2 = dirname(__filename2); var packageJson = JSON.parse( readFileSync2(join2(__dirname2, "..", "package.json"), "utf-8") ); var program = new Command4(); program.name("vibetime").description("Extended CLI tool for Claude API usage tracking and ranking\nFully compatible with ccusage commands").version(packageJson.version); program.command("daily").description("View daily Claude usage report (ccusage compatible)").option("-s, --since <since>", "Filter from date (YYYYMMDD format)").option("-u, --until <until>", "Filter until date (YYYYMMDD format)").option("-j, --json", "Output in JSON format").option("-m, --mode <mode>", "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)", "auto").option("-d, --debug", "Show pricing mismatch information for debugging").option("--debug-samples <samples>", "Number of sample discrepancies to show in debug output", "5").option("-o, --order <order>", "Sort order: desc (newest first) or asc (oldest first)", "asc").option("-b, --breakdown", "Show per-model cost breakdown").option("-O, --offline", "Use cached pricing data for Claude models instead of fetching from API").option("--no-offline", "Use online pricing data").option("--color", "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.").option("--no-color", "Disable colored output (default: auto). NO_COLOR=1 has the same effect.").option("-i, --instances", "Show usage breakdown by project/instance").option("-p, --project <project>", "Filter to specific project name").action(async () => { const args = process.argv.slice(3); const result = await executeCcusage(["daily", ...args]); process.exit(result.exitCode); }); program.command("monthly").description("View monthly Claude usage report (ccusage compatible)").option("-s, --since <since>", "Filter from date (YYYYMMDD format)").option("-u, --until <until>", "Filter until date (YYYYMMDD format)").option("-j, --json", "Output in JSON format").option("-m, --mode <mode>", "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)", "auto").option("-d, --debug", "Show pricing mismatch information for debugging").option("--debug-samples <samples>", "Number of sample discrepancies to show in debug output", "5").option("-o, --order <order>", "Sort order: desc (newest first) or asc (oldest first)", "asc").option("-b, --breakdown", "Show per-model cost breakdown").option("-O, --offline", "Use cached pricing data for Claude models instead of fetching from API").option("--no-offline", "Use online pricing data").option("--color", "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.").option("--no-color", "Disable colored output (default: auto). NO_COLOR=1 has the same effect.").option("-i, --instances", "Show usage breakdown by project/instance").option("-p, --project <project>", "Filter to specific project name").action(async () => { const args = process.argv.slice(3); const result = await executeCcusage(["monthly", ...args]); process.exit(result.exitCode); }); program.command("session").description("View session-based Claude usage report (ccusage compatible)").option("-s, --since <since>", "Filter from date (YYYYMMDD format)").option("-u, --until <until>", "Filter until date (YYYYMMDD format)").option("-j, --json", "Output in JSON format").option("-m, --mode <mode>", "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)", "auto").option("-d, --debug", "Show pricing mismatch information for debugging").option("--debug-samples <samples>", "Number of sample discrepancies to show in debug output", "5").option("-o, --order <order>", "Sort order: desc (newest first) or asc (oldest first)", "asc").option("-b, --breakdown", "Show per-model cost breakdown").option("-O, --offline", "Use cached pricing data for Claude models instead of fetching from API").option("--no-offline", "Use online pricing data").option("--color", "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.").option("--no-color", "Disable colored output (default: auto). NO_COLOR=1 has the same effect.").action(async () => { const args = process.argv.slice(3); const result = await executeCcusage(["session", ...args]); process.exit(result.exitCode); }); program.command("blocks").description("View 5-hour block usage report (ccusage compatible)").option("-s, --since <since>", "Filter from date (YYYYMMDD format)").option("-u, --until <until>", "Filter until date (YYYYMMDD format)").option("-j, --json", "Output in JSON format").option("-m, --mode <mode>", "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)", "auto").option("-d, --debug", "Show pricing mismatch information for debugging").option("--debug-samples <samples>", "Number of sample discrepancies to show in debug output", "5").option("-o, --order <order>", "Sort order: desc (newest first) or asc (oldest first)", "asc").option("-b, --breakdown", "Show per-model cost breakdown").option("-O, --offline", "Use cached pricing data for Claude models instead of fetching from API").option("--no-offline", "Use online pricing data").option("--color", "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.").option("--no-color", "Disable colored output (default: auto). NO_COLOR=1 has the same effect.").option("-a, --active", "Show only active block with projections").option("-r, --recent", "Show blocks from last 3 days (including active)").option("-t, --token-limit <limit>", 'Token limit for quota warnings (e.g., 500000 or "max")').option("-l, --session-length <hours>", "Session block duration in hours", "5").option("--live", "Live monitoring mode with real-time updates").option("--refresh-interval <seconds>", "Refresh interval in seconds for live mode", "1").action(async () => { const args = process.argv.slice(3); const result = await executeCcusage(["blocks", ...args]); process.exit(result.exitCode); }); program.addCommand(configCommand); program.addCommand(testCommand); program.addCommand(syncCommand); async function checkApiKeyStatus() { try { await getApiKey(); } catch { console.log(pc2.cyan("\n\u{1F389} Welcome to VibeTime CLI!\n")); console.log(pc2.white("VibeTime = ccusage + cloud sync")); console.log(pc2.gray("All ccusage commands work immediately:\n")); console.log(pc2.green(" vibetime daily") + pc2.gray(" - View daily usage report")); console.log(pc2.green(" vibetime monthly") + pc2.gray(" - View monthly usage report")); console.log(pc2.green(" vibetime session") + pc2.gray(" - View session usage report")); console.log(pc2.green(" vibetime blocks") + pc2.gray(" - View 5-hour block usage report")); console.log(pc2.cyan("\n\u2601\uFE0F Want to sync to cloud dashboard?")); console.log(pc2.green(" vibetime config set") + pc2.gray(" - Set up your API key")); console.log(pc2.green(" vibetime sync") + pc2.gray(" - Upload your usage data")); console.log(pc2.yellow("\n\u{1F4CC} Get your FREE API key at:")); console.log(pc2.blue(" https://vibetime.ekusiadadus.workers.dev")); console.log(pc2.gray("\nNote: ccusage commands work without an API key\n")); } } __name(checkApiKeyStatus, "checkApiKeyStatus"); program.exitOverride(); try { if (process.argv.length === 2) { await checkApiKeyStatus(); } await program.parseAsync(process.argv); } catch (error) { if (error.code === "commander.helpDisplayed" || error.code === "commander.version") { process.exit(0); } console.error(pc2.red("Error:"), error.message); process.exit(1); } //# sourceMappingURL=index.js.map