+270
-66
| #!/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", |
+2
-0
@@ -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 @@ |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
54511
14.57%1535
15.15%46
4.55%10
150%