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
JavaScript
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