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.4
to
0.1.5
+110
-27
dist/tkusage.js

@@ -25,3 +25,3 @@ #!/usr/bin/env node

function getUsageCacheFilePath() {
return path.join(getCacheRoot(), "usage-cache-v1.json");
return path.join(getCacheRoot(), "usage-cache-v2.json");
}

@@ -32,4 +32,11 @@ function hasUsageCache() {

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;
return typeof value === "object" && value !== null && "version" in value && value.version === 2 && "files" in value && typeof value.files === "object" && value.files !== null;
}
function getCacheKey(filePath, variant) {
return `${variant}\x00${filePath}`;
}
function getFilePathFromCacheKey(key) {
const separatorIndex = key.indexOf("\x00");
return separatorIndex === -1 ? key : key.slice(separatorIndex + 1);
}

@@ -44,8 +51,8 @@ class UsageCacheStore {

}
get(filePath, source, mtimeMs, size) {
const cached = this.data.files[filePath];
get(filePath, source, mtimeMs, size, variant = "default") {
const cached = this.data.files[getCacheKey(filePath, variant)];
if (!cached) {
return null;
}
if (cached.source !== source || cached.mtimeMs !== mtimeMs || cached.size !== size) {
if (cached.source !== source || cached.mtimeMs !== mtimeMs || cached.size !== size || cached.variant !== variant) {
return null;

@@ -56,5 +63,5 @@ }

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];
for (const [cacheKey, entry] of Object.entries(this.data.files)) {
if (entry.source === source && !existingFiles.has(getFilePathFromCacheKey(cacheKey))) {
delete this.data.files[cacheKey];
this.dirty = true;

@@ -72,4 +79,4 @@ }

}
set(filePath, source, mtimeMs, size, records, latestRateLimits) {
this.data.files[filePath] = {
set(filePath, source, mtimeMs, size, records, latestRateLimits, variant = "default") {
this.data.files[getCacheKey(filePath, variant)] = {
latestRateLimits,

@@ -79,3 +86,4 @@ mtimeMs,

size,
source
source,
variant
};

@@ -87,8 +95,8 @@ this.dirty = true;

if (!fs.existsSync(this.cacheFilePath)) {
return { version: 1, files: {} };
return { version: 2, files: {} };
}
const parsed = JSON.parse(fs.readFileSync(this.cacheFilePath, "utf8"));
return isValidCacheFile(parsed) ? parsed : { version: 1, files: {} };
return isValidCacheFile(parsed) ? parsed : { version: 2, files: {} };
} catch {
return { version: 1, files: {} };
return { version: 2, files: {} };
}

@@ -734,3 +742,3 @@ }

function getPricingNote() {
return "Estimated USD using embedded Claude and Codex token pricing. Not a vendor billing statement.";
return "Estimated USD using embedded Claude and Codex token pricing. Claude totals include sidechains/subagents by default unless --main-thread-only is set. Not a vendor billing statement.";
}

@@ -768,5 +776,14 @@

function parseSessionRelativeDir(filePath, transcriptRoot) {
const relativeDir = path2.relative(transcriptRoot, path2.dirname(filePath));
const relativeFilePath = path2.relative(transcriptRoot, filePath);
const pathParts = relativeFilePath.split(path2.sep).filter(Boolean);
if (pathParts.length >= 3 && pathParts[pathParts.length - 2] === "subagents") {
const projectParts = pathParts.slice(0, -3);
return projectParts.length === 0 ? "." : projectParts.join(path2.sep);
}
const relativeDir = path2.dirname(relativeFilePath);
return relativeDir === "" ? "." : relativeDir;
}
function isSubagentTranscriptFile(filePath) {
return path2.basename(path2.dirname(filePath)) === "subagents";
}
function parseClaudeUsagePayload(value) {

@@ -801,3 +818,12 @@ if (!isObject(value)) {

}
function parseUsageFile(filePath, transcriptRoot) {
function shouldSkipClaudeRecord(parsed, filePath, options) {
if (!isObject(parsed) || parsed.type !== "assistant") {
return true;
}
if (!options.mainThreadOnly) {
return false;
}
return parsed.isSidechain === true || isSubagentTranscriptFile(filePath);
}
function parseUsageFile(filePath, transcriptRoot, options) {
const raw = fs2.readFileSync(filePath, "utf8");

@@ -819,5 +845,8 @@ const lines = raw.split(`

}
if (!isObject(parsed) || parsed.isSidechain === true || parsed.type !== "assistant") {
if (shouldSkipClaudeRecord(parsed, filePath, options)) {
continue;
}
if (!isObject(parsed)) {
continue;
}
const timestamp = typeof parsed.timestamp === "string" ? parsed.timestamp : null;

@@ -869,2 +898,24 @@ const message = isObject(parsed.message) ? parsed.message : null;

}
function dedupeClaudeRecords(records) {
const recordsByRequestId = new Map;
const recordsWithoutRequestId = [];
for (const record of records) {
const requestId = record.nativeMetadata?.requestId;
if (!requestId) {
recordsWithoutRequestId.push(record);
continue;
}
const key = `${record.sessionId}:${requestId}`;
const existing = recordsByRequestId.get(key);
if (!existing || shouldReplaceExistingRecord(existing, record)) {
recordsByRequestId.set(key, record);
}
}
return [...recordsByRequestId.values(), ...recordsWithoutRequestId].sort((left, right) => left.timestamp.localeCompare(right.timestamp));
}
function normalizeClaudeUsageOptions(options) {
return {
mainThreadOnly: options?.mainThreadOnly ?? false
};
}
function resolveClaudeHome(explicitClaudeHome) {

@@ -877,3 +928,5 @@ if (explicitClaudeHome) {

}
function parseClaudeUsage(explicitClaudeHome, cache) {
function parseClaudeUsage(explicitClaudeHome, cache, options) {
const normalizedOptions = normalizeClaudeUsageOptions(options);
const cacheVariant = normalizedOptions.mainThreadOnly ? "claude-main-thread-only" : "claude-full-session";
const claudeHome = resolveClaudeHome(explicitClaudeHome);

@@ -886,3 +939,3 @@ const transcriptRoot = path2.join(claudeHome, "projects");

const stats = fs2.statSync(filePath);
const cached = cache?.get(filePath, "claude", stats.mtimeMs, stats.size);
const cached = cache?.get(filePath, "claude", stats.mtimeMs, stats.size, cacheVariant);
if (cached) {

@@ -892,4 +945,4 @@ records.push(...cached.records);

}
const parsedRecords = parseUsageFile(filePath, transcriptRoot);
cache?.set(filePath, "claude", stats.mtimeMs, stats.size, parsedRecords, null);
const parsedRecords = parseUsageFile(filePath, transcriptRoot, normalizedOptions);
cache?.set(filePath, "claude", stats.mtimeMs, stats.size, parsedRecords, null, cacheVariant);
records.push(...parsedRecords);

@@ -905,7 +958,7 @@ }

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

@@ -1143,3 +1196,6 @@ ...data,

if (options.source === "all" || options.source === "claude") {
data = withClaudeUsage(data, options.claudeHome, cache);
const claudeOptions = {
mainThreadOnly: options.mainThreadOnly
};
data = withClaudeUsage(data, options.claudeHome, cache, claudeOptions);
}

@@ -1276,2 +1332,22 @@ if (options.source === "all" || options.source === "codex") {

}
function isSubagentSessionRelativeDir(sessionRelativeDir) {
return sessionRelativeDir.split(/[\\/]/).includes("subagents");
}
function pickPreferredSessionRelativeDir(current, next) {
if (!next) {
return current;
}
if (!current) {
return next;
}
const currentIsSubagent = isSubagentSessionRelativeDir(current);
const nextIsSubagent = isSubagentSessionRelativeDir(next);
if (currentIsSubagent !== nextIsSubagent) {
return nextIsSubagent ? current : next;
}
if (current.length !== next.length) {
return next.length < current.length ? next : current;
}
return next.localeCompare(current) < 0 ? next : current;
}
function filterRecords(records, timezone, since, until, sessionFilter) {

@@ -1332,3 +1408,3 @@ return records.filter((record) => {

accumulator.sessionShortId = record.sessionShortId;
accumulator.sessionRelativeDir = record.sessionRelativeDir;
accumulator.sessionRelativeDir = pickPreferredSessionRelativeDir(accumulator.sessionRelativeDir, record.sessionRelativeDir);
}

@@ -1469,2 +1545,3 @@ applyRecord(accumulator, record);

--codex-home <path> Override Codex home for session discovery
--main-thread-only Claude only: exclude sidechains and subagents
--format <plain|json> Statusline output format (default: plain)

@@ -1516,2 +1593,3 @@ --help Show this message

let breakdown = false;
let mainThreadOnly = false;
let order = "asc";

@@ -1595,2 +1673,5 @@ let since;

break;
case "--main-thread-only":
mainThreadOnly = true;
break;
case "--order": {

@@ -1644,2 +1725,3 @@ const value = requireValue(args, index, "--order");

locale,
mainThreadOnly,
order,

@@ -1659,2 +1741,3 @@ period,

codexHome: options.codexHome,
mainThreadOnly: options.mainThreadOnly,
source: options.source

@@ -1661,0 +1744,0 @@ });

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

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

@@ -69,2 +69,3 @@ # tkusage

--codex-home /path/to/.codex
--main-thread-only # Claude only: exclude sidechains/subagents
--format plain|json # statusline only

@@ -78,3 +79,3 @@ ```

Selection: all
Pricing: Estimated USD using embedded Claude and Codex token pricing. Not a vendor billing statement.
Pricing: Estimated USD using embedded Claude and Codex token pricing. Claude totals include sidechains/subagents by default unless --main-thread-only is set. Not a vendor billing statement.
Tokens: Input = total prompt input, Cached = cached subset of input, Output includes reasoning, Total = Input + Output.

@@ -107,2 +108,3 @@

- `Cost` is an estimated USD equivalent derived from embedded token pricing tables
- Claude reports include sidechains/subagents by default; use `--main-thread-only` to restore the older main-transcript-only view
- It is not your Anthropic invoice

@@ -118,2 +120,3 @@ - It is not your ChatGPT subscription or Codex credits statement

- Claude usage is deduplicated by `requestId` because transcript logs can repeat usage events
- Claude request deduping also collapses duplicate request IDs that appear in both main and subagent transcripts
- Codex usage is reconstructed from cumulative token snapshots, so repeated snapshots do not double count

@@ -120,0 +123,0 @@ - The first run may take a while on large session folders because it builds the local cache