Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

tkusage

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

tkusage - npm Package Compare versions

Comparing version
0.1.0
to
0.1.1
+270
-66
dist/tkusage.js
#!/usr/bin/env node
// src/cli.ts
import * as fs3 from "node:fs";
import * as path3 from "node:path";
import * as fs4 from "node:fs";
import * as path4 from "node:path";
import { fileURLToPath } from "node:url";
// src/cache.ts
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
function getCacheRoot() {
if (process.env.XDG_CACHE_HOME && process.env.XDG_CACHE_HOME.trim() !== "") {
return path.join(process.env.XDG_CACHE_HOME, "tkusage");
}
if (process.platform === "darwin") {
return path.join(os.homedir(), "Library", "Caches", "tkusage");
}
if (process.platform === "win32" && process.env.LOCALAPPDATA) {
return path.join(process.env.LOCALAPPDATA, "tkusage", "Cache");
}
return path.join(os.homedir(), ".cache", "tkusage");
}
function getUsageCacheFilePath() {
return path.join(getCacheRoot(), "usage-cache-v1.json");
}
function hasUsageCache() {
return fs.existsSync(getUsageCacheFilePath());
}
function isValidCacheFile(value) {
return typeof value === "object" && value !== null && "version" in value && value.version === 1 && "files" in value && typeof value.files === "object" && value.files !== null;
}
class UsageCacheStore {
cacheFilePath;
data;
dirty = false;
constructor() {
this.cacheFilePath = getUsageCacheFilePath();
this.data = this.load();
}
get(filePath, source, mtimeMs, size) {
const cached = this.data.files[filePath];
if (!cached) {
return null;
}
if (cached.source !== source || cached.mtimeMs !== mtimeMs || cached.size !== size) {
return null;
}
return cached;
}
prune(source, existingFiles) {
for (const [filePath, entry] of Object.entries(this.data.files)) {
if (entry.source === source && !existingFiles.has(filePath)) {
delete this.data.files[filePath];
this.dirty = true;
}
}
}
save() {
if (!this.dirty) {
return;
}
fs.mkdirSync(path.dirname(this.cacheFilePath), { recursive: true });
fs.writeFileSync(this.cacheFilePath, JSON.stringify(this.data), "utf8");
this.dirty = false;
}
set(filePath, source, mtimeMs, size, records, latestRateLimits) {
this.data.files[filePath] = {
latestRateLimits,
mtimeMs,
records,
size,
source
};
this.dirty = true;
}
load() {
try {
if (!fs.existsSync(this.cacheFilePath)) {
return { version: 1, files: {} };
}
const parsed = JSON.parse(fs.readFileSync(this.cacheFilePath, "utf8"));
return isValidCacheFile(parsed) ? parsed : { version: 1, files: {} };
} catch {
return { version: 1, files: {} };
}
}
}
// src/dates.ts

@@ -75,2 +158,10 @@ function getFormatterPart(timestamp, timezone, partType) {

// src/format.ts
var ANSI = {
bold: "\x1B[1m",
cyan: "\x1B[36m",
dim: "\x1B[2m",
green: "\x1B[32m",
reset: "\x1B[0m",
yellow: "\x1B[33m"
};
function formatInteger(value) {

@@ -98,11 +189,80 @@ return new Intl.NumberFormat("en-US").format(value);

}
function renderTable(rows, columns) {
const widths = columns.map((column) => Math.max(column.header.length, ...rows.map((row) => column.getValue(row).length)));
const header = columns.map((column, index) => padCell(column.header, widths[index] ?? column.header.length, column.align)).join(" ");
const divider = widths.map((width) => "-".repeat(width)).join(" ");
const body = rows.map((row) => columns.map((column, index) => padCell(column.getValue(row), widths[index] ?? column.header.length, column.align)).join(" ")).join(`
function useAnsi() {
return Boolean(process.stdout.isTTY && !process.env.NO_COLOR);
}
function colorize(value, code) {
return useAnsi() ? `${code}${value}${ANSI.reset}` : value;
}
function splitLines(value) {
return value.split(`
`);
return [header, divider, body].filter(Boolean).join(`
}
function formatSource(source) {
if (source === "claude") {
return "Claude";
}
if (source === "codex") {
return "Codex";
}
return source;
}
function simplifyModelName(model) {
const normalized = model.trim().toLowerCase();
if (normalized.startsWith("claude-")) {
return normalized.replace(/^claude-/, "");
}
return normalized;
}
function formatModelList(row) {
const rawLabels = row.breakdown.length > 0 ? row.breakdown.map((item) => simplifyModelName(item.canonicalModel ?? item.model)) : row.models.map((model) => simplifyModelName(model));
const labels = [...new Set(rawLabels)];
const realLabels = labels.filter((label) => label !== "unknown" && label !== "<synthetic>");
const visibleLabels = realLabels.length > 0 ? realLabels : labels;
if (visibleLabels.length === 0) {
return "unknown";
}
return visibleLabels.map((label) => `• ${label}`).join(`
`);
}
function buildBorder(widths, left, mid, right) {
return left + widths.map((width) => "─".repeat(width + 2)).join(mid) + right;
}
function truncateText(value, maxWidth, trimFrom = "end") {
if (value.length <= maxWidth) {
return value;
}
if (maxWidth <= 3) {
return ".".repeat(maxWidth);
}
return trimFrom === "start" ? `...${value.slice(-(maxWidth - 3))}` : `${value.slice(0, maxWidth - 3)}...`;
}
function getCellLines(column, row) {
const lines = splitLines(column.getValue(row));
if (!column.maxWidth) {
return lines;
}
return lines.map((line) => truncateText(line, column.maxWidth ?? line.length, column.trimFrom));
}
function renderBoxTable(rows, columns) {
const widths = columns.map((column) => {
const valueWidths = rows.flatMap((row) => getCellLines(column, row).map((line) => line.length));
const longest = Math.max(column.header.length, ...valueWidths);
return column.maxWidth ? Math.min(longest, column.maxWidth) : longest;
});
const top = buildBorder(widths, "┌", "┬", "┐");
const header = "│ " + columns.map((column, index) => padCell(column.header, widths[index] ?? column.header.length, column.align)).join(" │ ") + " │";
const divider = buildBorder(widths, "├", "┼", "┤");
const bottom = buildBorder(widths, "└", "┴", "┘");
const body = [];
for (const row of rows) {
const cellLines = columns.map((column) => getCellLines(column, row));
const height = Math.max(...cellLines.map((lines) => lines.length));
for (let lineIndex = 0;lineIndex < height; lineIndex++) {
const line = "│ " + columns.map((column, index) => padCell(cellLines[index]?.[lineIndex] ?? "", widths[index] ?? column.header.length, column.align)).join(" │ ") + " │";
body.push(line);
}
}
return [colorize(top, ANSI.dim), colorize(header, ANSI.bold), colorize(divider, ANSI.dim), ...body, colorize(bottom, ANSI.dim)].join(`
`);
}
function shouldShowSource(selectedSource) {

@@ -147,3 +307,3 @@ return selectedSource === "all";

header: "Source",
getValue: (row) => row.source
getValue: (row) => formatSource(row.source)
});

@@ -153,3 +313,3 @@ }

columns.push({
header: "Sessions",
header: "Sess",
getValue: (row) => String(row.sessionCount),

@@ -173,3 +333,3 @@ align: "right"

columns.push({
header: "Sessions",
header: "Sess",
getValue: (row) => String(row.sessionCount),

@@ -179,3 +339,4 @@ align: "right"

header: "Models",
getValue: (row) => row.models.join(", ") || "unknown"
getValue: (row) => formatModelList(row),
maxWidth: 18
}, {

@@ -191,9 +352,5 @@ header: "Input",

header: "Output",
getValue: (row) => formatTokenValue(row.usage.outputTokens, false),
getValue: (row) => formatTokenValue(row.usage.outputTokens + row.usage.reasoningOutputTokens, false),
align: "right"
}, {
header: "Reason",
getValue: (row) => formatTokenValue(row.usage.reasoningOutputTokens, false),
align: "right"
}, {
header: "Total",

@@ -219,3 +376,3 @@ getValue: (row) => formatTokenValue(row.usage.totalTokens, false),

header: "Source",
getValue: (row) => row.source
getValue: (row) => formatSource(row.source)
});

@@ -225,3 +382,5 @@ }

header: "Dir",
getValue: (row) => row.sessionRelativeDir ?? "."
getValue: (row) => row.sessionRelativeDir ?? ".",
maxWidth: report.compact ? 36 : 42,
trimFrom: "start"
});

@@ -246,3 +405,4 @@ if (report.compact) {

header: "Models",
getValue: (row) => row.models.join(", ") || "unknown"
getValue: (row) => formatModelList(row),
maxWidth: 18
}, {

@@ -258,9 +418,5 @@ header: "Input",

header: "Output",
getValue: (row) => formatTokenValue(row.usage.outputTokens, false),
getValue: (row) => formatTokenValue(row.usage.outputTokens + row.usage.reasoningOutputTokens, false),
align: "right"
}, {
header: "Reason",
getValue: (row) => formatTokenValue(row.usage.reasoningOutputTokens, false),
align: "right"
}, {
header: "Total",

@@ -275,3 +431,4 @@ getValue: (row) => formatTokenValue(row.usage.totalTokens, false),

header: "Last Activity",
getValue: (row) => formatTimestamp(row.lastTimestamp, report.timezone, report.locale)
getValue: (row) => formatTimestamp(row.lastTimestamp, report.timezone, report.locale),
maxWidth: 22
});

@@ -298,4 +455,4 @@ return columns;

const columns = report.reportType === "session" ? buildSessionColumns(report) : buildTimeColumns(report);
lines.push("", renderTable(report.rows, columns), "");
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)}`);
lines.push("", renderBoxTable(report.rows, columns), "");
lines.push(colorize(`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)}`, ANSI.yellow));
if (report.breakdown) {

@@ -331,5 +488,5 @@ for (const row of report.rows) {

// src/sources/claude.ts
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import * as fs2 from "node:fs";
import * as os2 from "node:os";
import * as path2 from "node:path";

@@ -498,8 +655,8 @@ // src/pricing.ts

function collectJsonlFiles(dir) {
if (!fs.existsSync(dir)) {
if (!fs2.existsSync(dir)) {
return [];
}
const files = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path2.join(dir, entry.name);
if (entry.isDirectory()) {

@@ -521,3 +678,3 @@ for (const childPath of collectJsonlFiles(fullPath)) {

function parseSessionRelativeDir(filePath, transcriptRoot) {
const relativeDir = path.relative(transcriptRoot, path.dirname(filePath));
const relativeDir = path2.relative(transcriptRoot, path2.dirname(filePath));
return relativeDir === "" ? "." : relativeDir;

@@ -555,7 +712,7 @@ }

function parseUsageFile(filePath, transcriptRoot) {
const raw = fs.readFileSync(filePath, "utf8");
const raw = fs2.readFileSync(filePath, "utf8");
const lines = raw.split(`
`);
const recordsByKey = new Map;
const fallbackSessionId = path.basename(filePath, ".jsonl");
const fallbackSessionId = path2.basename(filePath, ".jsonl");
const sessionRelativeDir = parseSessionRelativeDir(filePath, transcriptRoot);

@@ -625,9 +782,22 @@ for (const [index, line] of lines.entries()) {

const envConfigDir = process.env.CLAUDE_CONFIG_DIR;
return envConfigDir && envConfigDir.trim() !== "" ? envConfigDir : path.join(os.homedir(), ".claude");
return envConfigDir && envConfigDir.trim() !== "" ? envConfigDir : path2.join(os2.homedir(), ".claude");
}
function parseClaudeUsage(explicitClaudeHome) {
function parseClaudeUsage(explicitClaudeHome, cache) {
const claudeHome = resolveClaudeHome(explicitClaudeHome);
const transcriptRoot = path.join(claudeHome, "projects");
const transcriptRoot = path2.join(claudeHome, "projects");
const sessionFiles = collectJsonlFiles(transcriptRoot);
const records = sessionFiles.flatMap((filePath) => parseUsageFile(filePath, transcriptRoot));
const records = [];
const existingFiles = new Set(sessionFiles);
for (const filePath of sessionFiles) {
const stats = fs2.statSync(filePath);
const cached = cache?.get(filePath, "claude", stats.mtimeMs, stats.size);
if (cached) {
records.push(...cached.records);
continue;
}
const parsedRecords = parseUsageFile(filePath, transcriptRoot);
cache?.set(filePath, "claude", stats.mtimeMs, stats.size, parsedRecords, null);
records.push(...parsedRecords);
}
cache?.prune("claude", existingFiles);
return {

@@ -643,4 +813,4 @@ metadata: {

}
function withClaudeUsage(data, explicitClaudeHome) {
const parsed = parseClaudeUsage(explicitClaudeHome);
function withClaudeUsage(data, explicitClaudeHome, cache) {
const parsed = parseClaudeUsage(explicitClaudeHome, cache);
return {

@@ -657,5 +827,5 @@ ...data,

// src/sources/codex.ts
import * as fs2 from "node:fs";
import * as os2 from "node:os";
import * as path2 from "node:path";
import * as fs3 from "node:fs";
import * as os3 from "node:os";
import * as path3 from "node:path";
function isObject2(value) {

@@ -721,8 +891,8 @@ return typeof value === "object" && value !== null;

function collectJsonlFiles2(dir) {
if (!fs2.existsSync(dir)) {
if (!fs3.existsSync(dir)) {
return [];
}
const files = [];
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path2.join(dir, entry.name);
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path3.join(dir, entry.name);
if (entry.isDirectory()) {

@@ -741,3 +911,3 @@ for (const childPath of collectJsonlFiles2(fullPath)) {

function parseSessionId(filePath) {
return path2.basename(filePath, ".jsonl");
return path3.basename(filePath, ".jsonl");
}

@@ -748,7 +918,7 @@ function parseSessionShortId2(sessionId) {

function parseSessionRelativeDir2(filePath, sessionRoot) {
const relativeDir = path2.relative(sessionRoot, path2.dirname(filePath));
const relativeDir = path3.relative(sessionRoot, path3.dirname(filePath));
return relativeDir === "" ? "." : relativeDir;
}
function parseUsageFile2(filePath, sessionRoot) {
const raw = fs2.readFileSync(filePath, "utf8");
const raw = fs3.readFileSync(filePath, "utf8");
const lines = raw.split(`

@@ -825,12 +995,23 @@ `);

}
return process.env.CODEX_HOME ?? path2.join(os2.homedir(), ".codex");
return process.env.CODEX_HOME ?? path3.join(os3.homedir(), ".codex");
}
function parseCodexUsage(explicitCodexHome) {
function parseCodexUsage(explicitCodexHome, cache) {
const codexHome = resolveCodexHome(explicitCodexHome);
const sessionRoot = path2.join(codexHome, "sessions");
const sessionRoot = path3.join(codexHome, "sessions");
const sessionFiles = collectJsonlFiles2(sessionRoot);
const records = [];
let latestRateLimits = null;
const existingFiles = new Set(sessionFiles);
for (const filePath of sessionFiles) {
const stats = fs3.statSync(filePath);
const cached = cache?.get(filePath, "codex", stats.mtimeMs, stats.size);
if (cached) {
records.push(...cached.records);
if (cached.latestRateLimits && (!latestRateLimits || cached.latestRateLimits.timestamp > latestRateLimits.timestamp)) {
latestRateLimits = cached.latestRateLimits;
}
continue;
}
const parsed = parseUsageFile2(filePath, sessionRoot);
cache?.set(filePath, "codex", stats.mtimeMs, stats.size, parsed.records, parsed.latestRateLimits);
records.push(...parsed.records);

@@ -841,2 +1022,3 @@ if (parsed.latestRateLimits && (!latestRateLimits || parsed.latestRateLimits.timestamp > latestRateLimits.timestamp)) {

}
cache?.prune("codex", existingFiles);
return {

@@ -852,4 +1034,4 @@ metadata: {

}
function withCodexUsage(data, explicitCodexHome) {
const parsed = parseCodexUsage(explicitCodexHome);
function withCodexUsage(data, explicitCodexHome, cache) {
const parsed = parseCodexUsage(explicitCodexHome, cache);
return {

@@ -867,2 +1049,3 @@ ...data,

function loadUsageData(options) {
const cache = new UsageCacheStore;
let data = {

@@ -874,8 +1057,9 @@ selectedSource: options.source,

if (options.source === "all" || options.source === "claude") {
data = withClaudeUsage(data, options.claudeHome);
data = withClaudeUsage(data, options.claudeHome, cache);
}
if (options.source === "all" || options.source === "codex") {
data = withCodexUsage(data, options.codexHome);
data = withCodexUsage(data, options.codexHome, cache);
}
data.records.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
cache.save();
return data;

@@ -1139,4 +1323,4 @@ }

const currentFilePath = fileURLToPath(import.meta.url);
const packageJsonPath = path3.resolve(path3.dirname(currentFilePath), "..", "package.json");
const packageJson = JSON.parse(fs3.readFileSync(packageJsonPath, "utf8"));
const packageJsonPath = path4.resolve(path4.dirname(currentFilePath), "..", "package.json");
const packageJson = JSON.parse(fs4.readFileSync(packageJsonPath, "utf8"));
return typeof packageJson.version === "string" && packageJson.version.trim() !== "" ? packageJson.version : "dev";

@@ -1186,2 +1370,24 @@ } catch {

}
function getEstimatedDurationRange(options) {
const hasCache = hasUsageCache();
if (options.command === "statusline") {
return hasCache ? "~2-5s" : "~20-40s";
}
if (options.command === "session") {
return hasCache ? "~4-8s" : "~25-45s";
}
if (options.source === "codex") {
return hasCache ? "~3-6s" : "~20-35s";
}
return hasCache ? "~4-10s" : "~30-50s";
}
function printStartupEstimate(options) {
if (!process.stderr.isTTY) {
return;
}
const cacheState = hasUsageCache() ? "warm cache" : "first run";
const estimate = getEstimatedDurationRange(options);
process.stderr.write(`tkusage: loading ${options.source} ${options.command} data ` + `(${cacheState}, estimated ${estimate})...
`);
}
function parseArgs(argv, version = "0.1.0") {

@@ -1358,9 +1564,7 @@ const args = [...argv];

}
function runCli(argv, dependencies = DEFAULT_DEPENDENCIES) {
const options = parseArgs(argv, dependencies.version);
return createCliOutput(options, dependencies);
}
function main(argv = process.argv.slice(2)) {
try {
const output = runCli(argv);
const options = parseArgs(argv);
printStartupEstimate(options);
const output = createCliOutput(options);
console.log(output);

@@ -1367,0 +1571,0 @@ } catch (error) {

+1
-1
{
"name": "tkusage",
"version": "0.1.0",
"version": "0.1.1",
"description": "Unified local usage and cost estimation CLI for Claude Code and Codex",

@@ -5,0 +5,0 @@ "type": "module",

@@ -7,2 +7,4 @@ # tkusage

The first run builds a local cache from your transcript/session files. Later runs reuse that cache and are much faster.
## Commands

@@ -9,0 +11,0 @@