+3
-5
| { | ||
| "name": "dashcc", | ||
| "version": "0.1.8", | ||
| "version": "0.1.9", | ||
| "description": "A session dashboard status line for Claude Code CLI", | ||
@@ -8,4 +8,3 @@ "module": "src/ccstatusline.ts", | ||
| "bin": { | ||
| "dashcc": "dist/ccstatusline.js", | ||
| "cdxusage": "dist/cdxusage.js" | ||
| "dashcc": "dist/ccstatusline.js" | ||
| }, | ||
@@ -16,5 +15,4 @@ "files": [ | ||
| "scripts": { | ||
| "cdxusage": "bun run src/cdxusage.ts", | ||
| "start": "bun run src/ccstatusline.ts", | ||
| "build": "rm -rf dist/* ; bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --target-version=14 ; bun build src/cdxusage.ts --target=node --outfile=dist/cdxusage.js --target-version=14", | ||
| "build": "rm -rf dist/* ; bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --target-version=14", | ||
| "postbuild": "bun run scripts/replace-version.ts", | ||
@@ -21,0 +19,0 @@ "example": "cat scripts/payload.example.json | bun start", |
-914
| #!/usr/bin/env node | ||
| // src/cdxusage/dates.ts | ||
| function getFormatterPart(timestamp, timezone, partType) { | ||
| const formatter = new Intl.DateTimeFormat("en-CA", { | ||
| timeZone: timezone, | ||
| year: "numeric", | ||
| month: "2-digit", | ||
| day: "2-digit" | ||
| }); | ||
| const parts = formatter.formatToParts(new Date(timestamp)); | ||
| return parts.find((part) => part.type === partType)?.value ?? ""; | ||
| } | ||
| function getDateKey(timestamp, timezone) { | ||
| const year = getFormatterPart(timestamp, timezone, "year"); | ||
| const month = getFormatterPart(timestamp, timezone, "month"); | ||
| const day = getFormatterPart(timestamp, timezone, "day"); | ||
| return `${year}-${month}-${day}`; | ||
| } | ||
| function getMonthKey(timestamp, timezone) { | ||
| const year = getFormatterPart(timestamp, timezone, "year"); | ||
| const month = getFormatterPart(timestamp, timezone, "month"); | ||
| return `${year}-${month}`; | ||
| } | ||
| function formatTimestamp(timestamp, timezone, locale) { | ||
| return new Intl.DateTimeFormat(locale, { | ||
| dateStyle: "medium", | ||
| timeStyle: "short", | ||
| timeZone: timezone | ||
| }).format(new Date(timestamp)); | ||
| } | ||
| function normalizeDayInput(input) { | ||
| const trimmed = input.trim(); | ||
| const compactMatch = /^(\d{4})(\d{2})(\d{2})$/.exec(trimmed); | ||
| if (compactMatch) { | ||
| return `${compactMatch[1]}-${compactMatch[2]}-${compactMatch[3]}`; | ||
| } | ||
| return /^\d{4}-\d{2}-\d{2}$/.test(trimmed) ? trimmed : null; | ||
| } | ||
| function normalizeMonthInput(input) { | ||
| const trimmed = input.trim(); | ||
| const compactMatch = /^(\d{4})(\d{2})$/.exec(trimmed); | ||
| if (compactMatch) { | ||
| return `${compactMatch[1]}-${compactMatch[2]}`; | ||
| } | ||
| return /^\d{4}-\d{2}$/.test(trimmed) ? trimmed : null; | ||
| } | ||
| function getMonthBounds(monthKey) { | ||
| const match = /^(\d{4})-(\d{2})$/.exec(monthKey); | ||
| if (!match) { | ||
| throw new Error(`Invalid month key: ${monthKey}`); | ||
| } | ||
| const yearPart = match[1]; | ||
| const monthPart = match[2]; | ||
| if (!yearPart || !monthPart) { | ||
| throw new Error(`Invalid month key: ${monthKey}`); | ||
| } | ||
| const year = Number.parseInt(yearPart, 10); | ||
| const month = Number.parseInt(monthPart, 10); | ||
| const nextMonth = new Date(Date.UTC(year, month, 1)); | ||
| const lastDay = new Date(nextMonth.getTime() - 24 * 60 * 60 * 1000); | ||
| const until = lastDay.toISOString().slice(0, 10); | ||
| return { | ||
| since: `${yearPart}-${monthPart}-01`, | ||
| until | ||
| }; | ||
| } | ||
| // src/cdxusage/format.ts | ||
| function formatInteger(value) { | ||
| return new Intl.NumberFormat("en-US").format(value); | ||
| } | ||
| function formatCompactNumber(value) { | ||
| return new Intl.NumberFormat("en-US", { | ||
| notation: "compact", | ||
| maximumFractionDigits: 1 | ||
| }).format(value); | ||
| } | ||
| function formatTokenValue(value, compact) { | ||
| return compact ? formatCompactNumber(value) : formatInteger(value); | ||
| } | ||
| function formatUsd(value, hasUnpricedUsage) { | ||
| if (value <= 0 && hasUnpricedUsage) { | ||
| return "n/a"; | ||
| } | ||
| const formatted = `$${value.toFixed(2)}`; | ||
| return hasUnpricedUsage ? `${formatted}+` : formatted; | ||
| } | ||
| function renderTable(rows, columns) { | ||
| const widths = columns.map((column) => { | ||
| const valueWidths = rows.map((row) => column.getValue(row).length); | ||
| return Math.max(column.header.length, ...valueWidths); | ||
| }); | ||
| const header = columns.map((column, index) => padCell(column.header, widths[index] ?? column.header.length, column.align)).join(" "); | ||
| const divider = columns.map((_, index) => "-".repeat(widths[index] ?? 0)).join(" "); | ||
| const body = rows.map((row) => columns.map((column, index) => padCell(column.getValue(row), widths[index] ?? column.header.length, column.align)).join(" ")).join(` | ||
| `); | ||
| return [header, divider, body].filter(Boolean).join(` | ||
| `); | ||
| } | ||
| function padCell(value, width, align = "left") { | ||
| return align === "right" ? value.padStart(width) : value.padEnd(width); | ||
| } | ||
| function buildSummary(report) { | ||
| const lines = [ | ||
| `cdxusage ${report.reportType} report`, | ||
| `Source: ${report.codexHome}`, | ||
| `Pricing: API equivalent USD (embedded GPT-5 Codex rates)`, | ||
| `Rows: ${report.rows.length} | Session files: ${report.sessionFiles}` | ||
| ]; | ||
| if (report.since || report.until) { | ||
| lines.push(`Range: ${report.since ?? "min"} -> ${report.until ?? "max"} (${report.timezone})`); | ||
| } else { | ||
| lines.push(`Timezone: ${report.timezone}`); | ||
| } | ||
| if (report.latestRateLimits) { | ||
| const primary = report.latestRateLimits.primaryUsedPercent; | ||
| const secondary = report.latestRateLimits.secondaryUsedPercent; | ||
| const parts = [ | ||
| report.latestRateLimits.planType ? `plan=${report.latestRateLimits.planType}` : null, | ||
| primary !== null ? `5h=${primary.toFixed(1)}%` : null, | ||
| secondary !== null ? `7d=${secondary.toFixed(1)}%` : null | ||
| ].filter(Boolean); | ||
| if (parts.length > 0) { | ||
| lines.push(`Latest limits: ${parts.join(" | ")}`); | ||
| } | ||
| } | ||
| return lines; | ||
| } | ||
| function buildDailyOrMonthlyColumns(report) { | ||
| const compact = report.compact; | ||
| if (compact) { | ||
| return [ | ||
| { header: report.reportType === "monthly" ? "Month" : "Date", getValue: (row) => row.label }, | ||
| { header: "Sessions", getValue: (row) => String(row.sessionCount), align: "right" }, | ||
| { header: "Input", getValue: (row) => formatTokenValue(row.usage.inputTokens, true), align: "right" }, | ||
| { header: "Output", getValue: (row) => formatTokenValue(row.usage.outputTokens + row.usage.reasoningOutputTokens, true), align: "right" }, | ||
| { header: "Cost", getValue: (row) => formatUsd(row.costUsd, row.hasUnpricedUsage), align: "right" } | ||
| ]; | ||
| } | ||
| return [ | ||
| { header: report.reportType === "monthly" ? "Month" : "Date", getValue: (row) => row.label }, | ||
| { header: "Sessions", getValue: (row) => String(row.sessionCount), align: "right" }, | ||
| { header: "Models", getValue: (row) => row.models.join(", ") || "unknown" }, | ||
| { header: "Input", getValue: (row) => formatTokenValue(row.usage.inputTokens, false), align: "right" }, | ||
| { header: "Cached", getValue: (row) => formatTokenValue(row.usage.cachedInputTokens, false), align: "right" }, | ||
| { header: "Output", getValue: (row) => formatTokenValue(row.usage.outputTokens, false), align: "right" }, | ||
| { header: "Reason", getValue: (row) => formatTokenValue(row.usage.reasoningOutputTokens, false), align: "right" }, | ||
| { header: "Total", getValue: (row) => formatTokenValue(row.usage.totalTokens, false), align: "right" }, | ||
| { header: "Cost", getValue: (row) => formatUsd(row.costUsd, row.hasUnpricedUsage), align: "right" } | ||
| ]; | ||
| } | ||
| function buildSessionColumns(report) { | ||
| const compact = report.compact; | ||
| if (compact) { | ||
| return [ | ||
| { header: "Session", getValue: (row) => row.sessionShortId ?? row.label }, | ||
| { header: "Dir", getValue: (row) => row.sessionRelativeDir ?? "." }, | ||
| { header: "Input", getValue: (row) => formatTokenValue(row.usage.inputTokens, true), align: "right" }, | ||
| { header: "Output", getValue: (row) => formatTokenValue(row.usage.outputTokens + row.usage.reasoningOutputTokens, true), align: "right" }, | ||
| { header: "Cost", getValue: (row) => formatUsd(row.costUsd, row.hasUnpricedUsage), align: "right" } | ||
| ]; | ||
| } | ||
| return [ | ||
| { header: "Session", getValue: (row) => row.sessionShortId ?? row.label }, | ||
| { header: "Dir", getValue: (row) => row.sessionRelativeDir ?? "." }, | ||
| { header: "Models", getValue: (row) => row.models.join(", ") || "unknown" }, | ||
| { header: "Input", getValue: (row) => formatTokenValue(row.usage.inputTokens, false), align: "right" }, | ||
| { header: "Cached", getValue: (row) => formatTokenValue(row.usage.cachedInputTokens, false), align: "right" }, | ||
| { header: "Output", getValue: (row) => formatTokenValue(row.usage.outputTokens, false), align: "right" }, | ||
| { header: "Reason", getValue: (row) => formatTokenValue(row.usage.reasoningOutputTokens, false), align: "right" }, | ||
| { header: "Total", getValue: (row) => formatTokenValue(row.usage.totalTokens, false), align: "right" }, | ||
| { header: "Cost", getValue: (row) => formatUsd(row.costUsd, row.hasUnpricedUsage), align: "right" }, | ||
| { header: "Last Activity", getValue: (row) => formatTimestamp(row.lastTimestamp, report.timezone, report.locale) } | ||
| ]; | ||
| } | ||
| function buildBreakdownLines(report, row) { | ||
| if (!report.breakdown || row.breakdown.length === 0) { | ||
| return []; | ||
| } | ||
| return row.breakdown.map((item) => { | ||
| const cost = formatUsd(item.costUsd, !item.hasPricing); | ||
| const output = item.usage.outputTokens + item.usage.reasoningOutputTokens; | ||
| return ` - ${item.model}: input=${formatTokenValue(item.usage.inputTokens, report.compact)}` + ` cached=${formatTokenValue(item.usage.cachedInputTokens, report.compact)}` + ` output=${formatTokenValue(output, report.compact)}` + ` cost=${cost}`; | ||
| }); | ||
| } | ||
| function renderReport(report) { | ||
| const lines = buildSummary(report); | ||
| const columns = report.reportType === "session" ? buildSessionColumns(report) : buildDailyOrMonthlyColumns(report); | ||
| if (report.rows.length === 0) { | ||
| lines.push(""); | ||
| lines.push("No usage records matched the selected filters."); | ||
| return lines.join(` | ||
| `); | ||
| } | ||
| lines.push(""); | ||
| lines.push(renderTable(report.rows, columns)); | ||
| lines.push(""); | ||
| lines.push(`TOTAL input=${formatTokenValue(report.totals.usage.inputTokens, report.compact)}` + ` cached=${formatTokenValue(report.totals.usage.cachedInputTokens, report.compact)}` + ` output=${formatTokenValue(report.totals.usage.outputTokens + report.totals.usage.reasoningOutputTokens, report.compact)}` + ` cost=${formatUsd(report.totals.costUsd, report.totals.hasUnpricedUsage)}`); | ||
| if (report.breakdown) { | ||
| for (const row of report.rows) { | ||
| const breakdownLines = buildBreakdownLines(report, row); | ||
| if (breakdownLines.length === 0) { | ||
| continue; | ||
| } | ||
| lines.push(""); | ||
| lines.push(`${row.label} breakdown`); | ||
| lines.push(...breakdownLines); | ||
| } | ||
| } | ||
| return lines.join(` | ||
| `); | ||
| } | ||
| // src/cdxusage/parser.ts | ||
| import * as fs from "fs"; | ||
| import * as os from "os"; | ||
| import * as path from "path"; | ||
| function isObject(value) { | ||
| return typeof value === "object" && value !== null; | ||
| } | ||
| function toNumber(value) { | ||
| return typeof value === "number" && Number.isFinite(value) ? value : 0; | ||
| } | ||
| function parseTokenTotals(value) { | ||
| if (!isObject(value)) { | ||
| return null; | ||
| } | ||
| const inputTokens = toNumber(value.input_tokens); | ||
| const cachedInputTokens = toNumber(value.cached_input_tokens); | ||
| const outputTokens = toNumber(value.output_tokens); | ||
| const reasoningOutputTokens = toNumber(value.reasoning_output_tokens); | ||
| const providedTotalTokens = toNumber(value.total_tokens); | ||
| return { | ||
| inputTokens, | ||
| cachedInputTokens, | ||
| outputTokens, | ||
| reasoningOutputTokens, | ||
| totalTokens: providedTotalTokens || inputTokens + outputTokens | ||
| }; | ||
| } | ||
| function subtractTotals(current, previous) { | ||
| const hasReset = current.totalTokens < previous.totalTokens || current.inputTokens < previous.inputTokens || current.cachedInputTokens < previous.cachedInputTokens || current.outputTokens < previous.outputTokens || current.reasoningOutputTokens < previous.reasoningOutputTokens; | ||
| if (hasReset) { | ||
| return current; | ||
| } | ||
| return { | ||
| inputTokens: current.inputTokens - previous.inputTokens, | ||
| cachedInputTokens: current.cachedInputTokens - previous.cachedInputTokens, | ||
| outputTokens: current.outputTokens - previous.outputTokens, | ||
| reasoningOutputTokens: current.reasoningOutputTokens - previous.reasoningOutputTokens, | ||
| totalTokens: current.totalTokens - previous.totalTokens | ||
| }; | ||
| } | ||
| function hasUsage(totals) { | ||
| return totals.inputTokens > 0 || totals.cachedInputTokens > 0 || totals.outputTokens > 0 || totals.reasoningOutputTokens > 0 || totals.totalTokens > 0; | ||
| } | ||
| function parseRateLimits(timestamp, value) { | ||
| if (!isObject(value)) { | ||
| return null; | ||
| } | ||
| const primary = isObject(value.primary) ? value.primary : null; | ||
| const secondary = isObject(value.secondary) ? value.secondary : null; | ||
| const credits = isObject(value.credits) ? value.credits : null; | ||
| return { | ||
| timestamp, | ||
| planType: typeof value.plan_type === "string" ? value.plan_type : null, | ||
| primaryUsedPercent: primary ? toNumber(primary.used_percent) : null, | ||
| primaryWindowMinutes: primary ? toNumber(primary.window_minutes) : null, | ||
| primaryResetsAt: primary ? toNumber(primary.resets_at) : null, | ||
| secondaryUsedPercent: secondary ? toNumber(secondary.used_percent) : null, | ||
| secondaryWindowMinutes: secondary ? toNumber(secondary.window_minutes) : null, | ||
| secondaryResetsAt: secondary ? toNumber(secondary.resets_at) : null, | ||
| creditsBalance: credits ? toNumber(credits.balance) : null, | ||
| creditsHasUnlimited: credits && typeof credits.unlimited === "boolean" ? credits.unlimited : null | ||
| }; | ||
| } | ||
| function collectJsonlFiles(dir) { | ||
| if (!fs.existsSync(dir)) { | ||
| return []; | ||
| } | ||
| const files = []; | ||
| for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { | ||
| const fullPath = path.join(dir, entry.name); | ||
| if (entry.isDirectory()) { | ||
| for (const childPath of collectJsonlFiles(fullPath)) { | ||
| files.push(childPath); | ||
| } | ||
| continue; | ||
| } | ||
| if (entry.isFile() && fullPath.endsWith(".jsonl")) { | ||
| files.push(fullPath); | ||
| } | ||
| } | ||
| return files.sort(); | ||
| } | ||
| function parseSessionId(filePath) { | ||
| return path.basename(filePath, ".jsonl"); | ||
| } | ||
| function parseSessionShortId(sessionId) { | ||
| return sessionId.slice(-8); | ||
| } | ||
| function parseSessionRelativeDir(filePath, sessionRoot) { | ||
| const relativeDir = path.relative(sessionRoot, path.dirname(filePath)); | ||
| return relativeDir === "" ? "." : relativeDir; | ||
| } | ||
| function parseUsageFile(filePath, sessionRoot) { | ||
| const raw = fs.readFileSync(filePath, "utf8"); | ||
| const lines = raw.split(` | ||
| `); | ||
| const sessionId = parseSessionId(filePath); | ||
| const sessionShortId = parseSessionShortId(sessionId); | ||
| const sessionRelativeDir = parseSessionRelativeDir(filePath, sessionRoot); | ||
| const records = []; | ||
| let currentModel = null; | ||
| let previousTotals = null; | ||
| let latestRateLimits = null; | ||
| for (const line of lines) { | ||
| if (line.trim() === "") { | ||
| continue; | ||
| } | ||
| let parsed; | ||
| try { | ||
| parsed = JSON.parse(line); | ||
| } catch { | ||
| continue; | ||
| } | ||
| if (!isObject(parsed)) { | ||
| continue; | ||
| } | ||
| if (parsed.type === "turn_context" && isObject(parsed.payload) && typeof parsed.payload.model === "string") { | ||
| currentModel = parsed.payload.model; | ||
| continue; | ||
| } | ||
| if (parsed.type !== "event_msg" || !isObject(parsed.payload) || parsed.payload.type !== "token_count") { | ||
| continue; | ||
| } | ||
| const timestamp = typeof parsed.timestamp === "string" ? parsed.timestamp : null; | ||
| if (!timestamp) { | ||
| continue; | ||
| } | ||
| const usageInfo = isObject(parsed.payload.info) ? parseTokenTotals(parsed.payload.info.total_token_usage) : null; | ||
| const rateLimits = parseRateLimits(timestamp, parsed.payload.rate_limits); | ||
| if (rateLimits && (!latestRateLimits || timestamp > latestRateLimits.timestamp)) { | ||
| latestRateLimits = rateLimits; | ||
| } | ||
| if (!usageInfo) { | ||
| continue; | ||
| } | ||
| const delta = previousTotals ? subtractTotals(usageInfo, previousTotals) : usageInfo; | ||
| previousTotals = usageInfo; | ||
| if (!hasUsage(delta)) { | ||
| continue; | ||
| } | ||
| records.push({ | ||
| timestamp, | ||
| model: currentModel, | ||
| sessionId, | ||
| sessionShortId, | ||
| sessionRelativeDir, | ||
| sessionFile: filePath, | ||
| totals: delta | ||
| }); | ||
| } | ||
| return { | ||
| records, | ||
| latestRateLimits | ||
| }; | ||
| } | ||
| function resolveCodexHome(explicitCodexHome) { | ||
| if (explicitCodexHome) { | ||
| return explicitCodexHome; | ||
| } | ||
| return process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"); | ||
| } | ||
| function parseCodexUsage(explicitCodexHome) { | ||
| const codexHome = resolveCodexHome(explicitCodexHome); | ||
| const sessionRoot = path.join(codexHome, "sessions"); | ||
| const sessionFiles = collectJsonlFiles(sessionRoot); | ||
| const records = []; | ||
| let latestRateLimits = null; | ||
| for (const filePath of sessionFiles) { | ||
| const parsed = parseUsageFile(filePath, sessionRoot); | ||
| records.push(...parsed.records); | ||
| if (parsed.latestRateLimits && (!latestRateLimits || parsed.latestRateLimits.timestamp > latestRateLimits.timestamp)) { | ||
| latestRateLimits = parsed.latestRateLimits; | ||
| } | ||
| } | ||
| return { | ||
| codexHome, | ||
| sessionRoot, | ||
| sessionFiles: sessionFiles.length, | ||
| records, | ||
| latestRateLimits | ||
| }; | ||
| } | ||
| // src/cdxusage/pricing.ts | ||
| var PRICING_TABLE = [ | ||
| { | ||
| canonicalModel: "gpt-5.4", | ||
| inputUsdPerMillion: 2.5, | ||
| cachedInputUsdPerMillion: 0.25, | ||
| outputUsdPerMillion: 15 | ||
| }, | ||
| { | ||
| canonicalModel: "gpt-5.3-codex", | ||
| inputUsdPerMillion: 1.5, | ||
| cachedInputUsdPerMillion: 0.15, | ||
| outputUsdPerMillion: 10 | ||
| }, | ||
| { | ||
| canonicalModel: "gpt-5.1-codex", | ||
| inputUsdPerMillion: 1.25, | ||
| cachedInputUsdPerMillion: 0.125, | ||
| outputUsdPerMillion: 10 | ||
| }, | ||
| { | ||
| canonicalModel: "gpt-5.1-codex-mini", | ||
| inputUsdPerMillion: 0.25, | ||
| cachedInputUsdPerMillion: 0.025, | ||
| outputUsdPerMillion: 2 | ||
| } | ||
| ]; | ||
| var MODEL_ALIASES = { | ||
| "gpt-5-codex": "gpt-5.1-codex", | ||
| "gpt-5-codex-mini": "gpt-5.1-codex-mini", | ||
| "gpt-5.4-codex": "gpt-5.4" | ||
| }; | ||
| function normalizeModelKey(model) { | ||
| return model.trim().toLowerCase(); | ||
| } | ||
| function getPricingRate(model) { | ||
| if (!model) { | ||
| return null; | ||
| } | ||
| const normalized = normalizeModelKey(model); | ||
| const aliased = MODEL_ALIASES[normalized] ?? normalized; | ||
| const exact = PRICING_TABLE.find((rate) => rate.canonicalModel === aliased); | ||
| if (exact) { | ||
| return exact; | ||
| } | ||
| if (aliased.includes("gpt-5.4")) { | ||
| return PRICING_TABLE[0] ?? null; | ||
| } | ||
| if (aliased.includes("gpt-5.3-codex")) { | ||
| return PRICING_TABLE[1] ?? null; | ||
| } | ||
| if (aliased.includes("mini")) { | ||
| return PRICING_TABLE[3] ?? null; | ||
| } | ||
| if (aliased.includes("codex")) { | ||
| return PRICING_TABLE[2] ?? null; | ||
| } | ||
| return null; | ||
| } | ||
| function calculateApiEquivalentCostUsd(usage, model) { | ||
| const rate = getPricingRate(model); | ||
| if (!rate) { | ||
| return null; | ||
| } | ||
| const nonCachedInputTokens = Math.max(0, usage.inputTokens - usage.cachedInputTokens); | ||
| const outputTokens = usage.outputTokens + usage.reasoningOutputTokens; | ||
| const inputCost = nonCachedInputTokens / 1e6 * rate.inputUsdPerMillion; | ||
| const cachedCost = usage.cachedInputTokens / 1e6 * rate.cachedInputUsdPerMillion; | ||
| const outputCost = outputTokens / 1e6 * rate.outputUsdPerMillion; | ||
| return inputCost + cachedCost + outputCost; | ||
| } | ||
| // src/cdxusage/report.ts | ||
| function createEmptyTotals() { | ||
| return { | ||
| inputTokens: 0, | ||
| cachedInputTokens: 0, | ||
| outputTokens: 0, | ||
| reasoningOutputTokens: 0, | ||
| totalTokens: 0 | ||
| }; | ||
| } | ||
| function addTotals(target, source) { | ||
| target.inputTokens += source.inputTokens; | ||
| target.cachedInputTokens += source.cachedInputTokens; | ||
| target.outputTokens += source.outputTokens; | ||
| target.reasoningOutputTokens += source.reasoningOutputTokens; | ||
| target.totalTokens += source.totalTokens; | ||
| } | ||
| function createAccumulator(key, label) { | ||
| return { | ||
| key, | ||
| label, | ||
| firstTimestamp: "", | ||
| lastTimestamp: "", | ||
| sessionIds: new Set, | ||
| models: new Set, | ||
| usage: createEmptyTotals(), | ||
| costUsd: 0, | ||
| hasUnpricedUsage: false, | ||
| breakdown: new Map | ||
| }; | ||
| } | ||
| function getRecordDayKey(record, timezone) { | ||
| return getDateKey(record.timestamp, timezone); | ||
| } | ||
| function applyRecord(accumulator, record) { | ||
| accumulator.sessionIds.add(record.sessionId); | ||
| const modelLabel = record.model ?? "unknown"; | ||
| accumulator.models.add(modelLabel); | ||
| if (accumulator.firstTimestamp === "" || record.timestamp < accumulator.firstTimestamp) { | ||
| accumulator.firstTimestamp = record.timestamp; | ||
| } | ||
| if (accumulator.lastTimestamp === "" || record.timestamp > accumulator.lastTimestamp) { | ||
| accumulator.lastTimestamp = record.timestamp; | ||
| } | ||
| addTotals(accumulator.usage, record.totals); | ||
| const costUsd = calculateApiEquivalentCostUsd(record.totals, record.model); | ||
| if (costUsd === null) { | ||
| accumulator.hasUnpricedUsage = true; | ||
| } else { | ||
| accumulator.costUsd += costUsd; | ||
| } | ||
| const existingBreakdown = accumulator.breakdown.get(modelLabel); | ||
| if (existingBreakdown) { | ||
| addTotals(existingBreakdown.usage, record.totals); | ||
| if (costUsd === null) { | ||
| existingBreakdown.hasPricing = false; | ||
| } else { | ||
| existingBreakdown.costUsd += costUsd; | ||
| } | ||
| return; | ||
| } | ||
| const rate = getPricingRate(record.model); | ||
| accumulator.breakdown.set(modelLabel, { | ||
| model: modelLabel, | ||
| canonicalModel: rate?.canonicalModel ?? null, | ||
| usage: { ...record.totals }, | ||
| costUsd: costUsd ?? 0, | ||
| hasPricing: costUsd !== null | ||
| }); | ||
| } | ||
| function finalizeAccumulator(accumulator) { | ||
| const breakdown = Array.from(accumulator.breakdown.values()).sort((left, right) => { | ||
| if (right.costUsd !== left.costUsd) { | ||
| return right.costUsd - left.costUsd; | ||
| } | ||
| return left.model.localeCompare(right.model); | ||
| }); | ||
| return { | ||
| key: accumulator.key, | ||
| label: accumulator.label, | ||
| firstTimestamp: accumulator.firstTimestamp, | ||
| lastTimestamp: accumulator.lastTimestamp, | ||
| sessionCount: accumulator.sessionIds.size, | ||
| models: Array.from(accumulator.models).sort(), | ||
| usage: accumulator.usage, | ||
| costUsd: accumulator.costUsd, | ||
| hasUnpricedUsage: accumulator.hasUnpricedUsage || breakdown.some((item) => !item.hasPricing), | ||
| breakdown, | ||
| sessionId: accumulator.sessionId, | ||
| sessionShortId: accumulator.sessionShortId, | ||
| sessionRelativeDir: accumulator.sessionRelativeDir | ||
| }; | ||
| } | ||
| function sortRows(rows, order) { | ||
| return rows.sort((left, right) => { | ||
| const comparison = left.key.localeCompare(right.key); | ||
| return order === "asc" ? comparison : -comparison; | ||
| }); | ||
| } | ||
| function filterRecords(records, timezone, since, until, sessionFilter) { | ||
| return records.filter((record) => { | ||
| if (sessionFilter && !record.sessionId.includes(sessionFilter) && !record.sessionShortId.includes(sessionFilter)) { | ||
| return false; | ||
| } | ||
| const dayKey = getRecordDayKey(record, timezone); | ||
| if (since && dayKey < since) { | ||
| return false; | ||
| } | ||
| if (until && dayKey > until) { | ||
| return false; | ||
| } | ||
| return true; | ||
| }); | ||
| } | ||
| function normalizeFilters(options) { | ||
| let since = options.since ? normalizeDayInput(options.since) ?? undefined : undefined; | ||
| let until = options.until ? normalizeDayInput(options.until) ?? undefined : undefined; | ||
| let period = options.period; | ||
| if (options.reportType === "daily" && options.period) { | ||
| const normalizedDay = normalizeDayInput(options.period); | ||
| if (!normalizedDay) { | ||
| throw new Error(`Invalid day period: ${options.period}`); | ||
| } | ||
| since = normalizedDay; | ||
| until = normalizedDay; | ||
| period = normalizedDay; | ||
| } | ||
| if (options.reportType === "monthly" && options.period) { | ||
| const normalizedMonth = normalizeMonthInput(options.period); | ||
| if (!normalizedMonth) { | ||
| throw new Error(`Invalid month period: ${options.period}`); | ||
| } | ||
| const bounds = getMonthBounds(normalizedMonth); | ||
| since = bounds.since; | ||
| until = bounds.until; | ||
| period = normalizedMonth; | ||
| } | ||
| return { | ||
| since, | ||
| until, | ||
| period | ||
| }; | ||
| } | ||
| function buildDailyRows(records, timezone) { | ||
| const byDay = new Map; | ||
| for (const record of records) { | ||
| const key = getDateKey(record.timestamp, timezone); | ||
| const accumulator = byDay.get(key) ?? createAccumulator(key, key); | ||
| applyRecord(accumulator, record); | ||
| byDay.set(key, accumulator); | ||
| } | ||
| return Array.from(byDay.values()).map(finalizeAccumulator); | ||
| } | ||
| function buildMonthlyRows(records, timezone) { | ||
| const byMonth = new Map; | ||
| for (const record of records) { | ||
| const key = getMonthKey(record.timestamp, timezone); | ||
| const accumulator = byMonth.get(key) ?? createAccumulator(key, key); | ||
| applyRecord(accumulator, record); | ||
| byMonth.set(key, accumulator); | ||
| } | ||
| return Array.from(byMonth.values()).map(finalizeAccumulator); | ||
| } | ||
| function buildSessionRows(records, timezone, locale) { | ||
| const bySession = new Map; | ||
| for (const record of records) { | ||
| const key = record.sessionId; | ||
| const accumulator = bySession.get(key) ?? createAccumulator(key, record.sessionShortId); | ||
| accumulator.sessionId = record.sessionId; | ||
| accumulator.sessionShortId = record.sessionShortId; | ||
| accumulator.sessionRelativeDir = record.sessionRelativeDir; | ||
| applyRecord(accumulator, record); | ||
| accumulator.label = `${record.sessionShortId} (${formatTimestamp(record.timestamp, timezone, locale)})`; | ||
| bySession.set(key, accumulator); | ||
| } | ||
| return Array.from(bySession.values()).map(finalizeAccumulator); | ||
| } | ||
| function buildTotalsRow(rows) { | ||
| const accumulator = createAccumulator("TOTAL", "TOTAL"); | ||
| for (const row of rows) { | ||
| accumulator.sessionIds.add(row.key); | ||
| row.models.forEach((model) => accumulator.models.add(model)); | ||
| if (accumulator.firstTimestamp === "" || row.firstTimestamp && row.firstTimestamp < accumulator.firstTimestamp) { | ||
| accumulator.firstTimestamp = row.firstTimestamp; | ||
| } | ||
| if (accumulator.lastTimestamp === "" || row.lastTimestamp > accumulator.lastTimestamp) { | ||
| accumulator.lastTimestamp = row.lastTimestamp; | ||
| } | ||
| addTotals(accumulator.usage, row.usage); | ||
| accumulator.costUsd += row.costUsd; | ||
| accumulator.hasUnpricedUsage = accumulator.hasUnpricedUsage || row.hasUnpricedUsage; | ||
| for (const item of row.breakdown) { | ||
| const existing = accumulator.breakdown.get(item.model); | ||
| if (existing) { | ||
| addTotals(existing.usage, item.usage); | ||
| existing.costUsd += item.costUsd; | ||
| existing.hasPricing = existing.hasPricing && item.hasPricing; | ||
| continue; | ||
| } | ||
| accumulator.breakdown.set(item.model, { | ||
| model: item.model, | ||
| canonicalModel: item.canonicalModel, | ||
| usage: { ...item.usage }, | ||
| costUsd: item.costUsd, | ||
| hasPricing: item.hasPricing | ||
| }); | ||
| } | ||
| } | ||
| accumulator.sessionIds = new Set(rows.map((row) => row.sessionId ?? row.key)); | ||
| return finalizeAccumulator(accumulator); | ||
| } | ||
| function buildReport(data, options) { | ||
| const timezone = options.timezone; | ||
| const normalized = normalizeFilters(options); | ||
| const filteredRecords = filterRecords(data.records, timezone, normalized.since, normalized.until, options.sessionFilter); | ||
| const reportRows = (() => { | ||
| switch (options.reportType) { | ||
| case "monthly": | ||
| return buildMonthlyRows(filteredRecords, timezone); | ||
| case "session": | ||
| return buildSessionRows(filteredRecords, timezone, options.locale); | ||
| case "daily": | ||
| default: | ||
| return buildDailyRows(filteredRecords, timezone); | ||
| } | ||
| })(); | ||
| const order = options.order ?? "desc"; | ||
| const rows = sortRows(reportRows, order); | ||
| const totals = buildTotalsRow(rows); | ||
| return { | ||
| reportType: options.reportType, | ||
| timezone, | ||
| locale: options.locale, | ||
| since: normalized.since, | ||
| until: normalized.until, | ||
| period: normalized.period, | ||
| sessionFilter: options.sessionFilter, | ||
| compact: options.compact ?? false, | ||
| breakdown: options.breakdown ?? false, | ||
| rows, | ||
| totals, | ||
| latestRateLimits: data.latestRateLimits, | ||
| codexHome: data.codexHome, | ||
| sessionFiles: data.sessionFiles | ||
| }; | ||
| } | ||
| // src/utils/terminal.ts | ||
| import * as fs2 from "fs"; | ||
| import * as path2 from "path"; | ||
| var __dirname = "/Users/junyu/coding/ccdash/src/utils"; | ||
| var PACKAGE_VERSION = "0.1.8"; | ||
| function getPackageVersion() { | ||
| if (/^\d+\.\d+\.\d+/.test(PACKAGE_VERSION)) { | ||
| return PACKAGE_VERSION; | ||
| } | ||
| const possiblePaths = [ | ||
| path2.join(__dirname, "..", "..", "package.json"), | ||
| path2.join(__dirname, "..", "package.json") | ||
| ]; | ||
| for (const packageJsonPath of possiblePaths) { | ||
| try { | ||
| if (fs2.existsSync(packageJsonPath)) { | ||
| const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8")); | ||
| return packageJson.version ?? ""; | ||
| } | ||
| } catch {} | ||
| } | ||
| return ""; | ||
| } | ||
| // src/cdxusage.ts | ||
| function printHelp() { | ||
| console.log(`cdxusage v${getPackageVersion() || "dev"} | ||
| Usage: | ||
| cdxusage [daily] [YYYY-MM-DD] [options] | ||
| cdxusage monthly [YYYY-MM] [options] | ||
| cdxusage session [session-id] [options] | ||
| Options: | ||
| --since <YYYY-MM-DD> Filter by start day (inclusive) | ||
| --until <YYYY-MM-DD> Filter by end day (inclusive) | ||
| --timezone <IANA name> Group usage in the given timezone | ||
| --locale <locale> Locale used for human-readable timestamps | ||
| --json Emit JSON instead of a table | ||
| --compact Use compact number formatting | ||
| --breakdown Include per-model breakdowns | ||
| --order <asc|desc> Sort report rows (default: desc) | ||
| --codex-home <path> Override CODEX_HOME for log discovery | ||
| --help Show this message | ||
| --version Show the CLI version | ||
| `); | ||
| } | ||
| function requireValue(args, index, flag) { | ||
| const value = args[index + 1]; | ||
| if (!value || value.startsWith("-")) { | ||
| throw new Error(`${flag} requires a value`); | ||
| } | ||
| return value; | ||
| } | ||
| function parseArgs(argv) { | ||
| const args = [...argv]; | ||
| let reportType = "daily"; | ||
| let period; | ||
| let sessionFilter; | ||
| let timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; | ||
| let locale; | ||
| let json = false; | ||
| let compact = false; | ||
| let breakdown = false; | ||
| let order = "desc"; | ||
| let since; | ||
| let until; | ||
| let codexHome; | ||
| const first = args[0]; | ||
| if (first === "--help" || first === "-h") { | ||
| printHelp(); | ||
| process.exit(0); | ||
| } | ||
| if (first === "--version" || first === "-v") { | ||
| console.log(getPackageVersion() || "dev"); | ||
| process.exit(0); | ||
| } | ||
| if (first === "daily" || first === "monthly" || first === "session") { | ||
| reportType = first; | ||
| args.shift(); | ||
| } | ||
| if (args[0] && !args[0].startsWith("-")) { | ||
| if (reportType === "session") { | ||
| sessionFilter = args[0]; | ||
| } else { | ||
| period = args[0]; | ||
| } | ||
| args.shift(); | ||
| } | ||
| for (let index = 0;index < args.length; index++) { | ||
| const arg = args[index]; | ||
| switch (arg) { | ||
| case "--help": | ||
| case "-h": | ||
| printHelp(); | ||
| process.exit(0); | ||
| break; | ||
| case "--version": | ||
| case "-v": | ||
| console.log(getPackageVersion() || "dev"); | ||
| process.exit(0); | ||
| break; | ||
| case "--since": | ||
| since = requireValue(args, index, "--since"); | ||
| index++; | ||
| break; | ||
| case "--until": | ||
| until = requireValue(args, index, "--until"); | ||
| index++; | ||
| break; | ||
| case "--timezone": | ||
| timezone = requireValue(args, index, "--timezone"); | ||
| index++; | ||
| break; | ||
| case "--locale": | ||
| locale = requireValue(args, index, "--locale"); | ||
| index++; | ||
| break; | ||
| case "--json": | ||
| json = true; | ||
| break; | ||
| case "--compact": | ||
| compact = true; | ||
| break; | ||
| case "--breakdown": | ||
| breakdown = true; | ||
| break; | ||
| case "--order": { | ||
| const value = requireValue(args, index, "--order"); | ||
| if (value !== "asc" && value !== "desc") { | ||
| throw new Error(`--order must be asc or desc, received: ${value}`); | ||
| } | ||
| order = value; | ||
| index++; | ||
| break; | ||
| } | ||
| case "--codex-home": | ||
| codexHome = requireValue(args, index, "--codex-home"); | ||
| index++; | ||
| break; | ||
| case "--id": | ||
| sessionFilter = requireValue(args, index, "--id"); | ||
| index++; | ||
| break; | ||
| case "--mode": | ||
| case "--offline": | ||
| case "-O": | ||
| if (arg === "--mode") { | ||
| index++; | ||
| } | ||
| break; | ||
| default: | ||
| throw new Error(`Unknown argument: ${arg}`); | ||
| } | ||
| } | ||
| return { | ||
| reportType, | ||
| period, | ||
| sessionFilter, | ||
| since, | ||
| until, | ||
| timezone, | ||
| locale, | ||
| json, | ||
| compact, | ||
| breakdown, | ||
| order, | ||
| codexHome | ||
| }; | ||
| } | ||
| function main() { | ||
| try { | ||
| const args = parseArgs(process.argv.slice(2)); | ||
| const parsedUsage = parseCodexUsage(args.codexHome); | ||
| const report = buildReport(parsedUsage, { | ||
| reportType: args.reportType, | ||
| period: args.period, | ||
| sessionFilter: args.sessionFilter, | ||
| since: args.since, | ||
| until: args.until, | ||
| timezone: args.timezone, | ||
| locale: args.locale, | ||
| compact: args.compact, | ||
| breakdown: args.breakdown, | ||
| order: args.order | ||
| }); | ||
| if (args.json) { | ||
| console.log(JSON.stringify(report, null, 2)); | ||
| return; | ||
| } | ||
| console.log(renderReport(report)); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : "Unknown error"; | ||
| console.error(`cdxusage: ${message}`); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| main(); |
Sorry, the diff of this file is too big to display
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
0
-100%2808531
-1.13%4
-20%67785
-1.32%