@tokz/cli 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildBlocks,
4
+ burnRate,
5
+ fmtMs
6
+ } from "./chunk-6Y6MKFHZ.js";
7
+ import {
8
+ compact,
9
+ costUsd,
10
+ dayKey,
11
+ findTranscripts,
12
+ parseTranscript,
13
+ shortModel,
14
+ usd
15
+ } from "./chunk-Z5W5QH2Z.js";
16
+
17
+ // src/statusline.ts
18
+ import { mkdir, readFile, stat, writeFile } from "fs/promises";
19
+ import { homedir } from "os";
20
+ import { join } from "path";
21
+ import pc from "picocolors";
22
+ var CONTEXT_WINDOW = 2e5;
23
+ var LOOKBACK_MS = 6 * 60 * 60 * 1e3;
24
+ var BURN_MODERATE = 2e3;
25
+ var BURN_HIGH = 5e3;
26
+ async function lastContextTokens(transcriptPath) {
27
+ try {
28
+ const lines = (await readFile(transcriptPath, "utf8")).split("\n");
29
+ for (let i = lines.length - 1; i >= 0; i--) {
30
+ if (!lines[i].includes('"usage"')) continue;
31
+ try {
32
+ const j = JSON.parse(lines[i]);
33
+ const u = j?.message?.usage;
34
+ if (u && typeof u.input_tokens === "number") {
35
+ return u.input_tokens + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
36
+ }
37
+ } catch {
38
+ }
39
+ }
40
+ } catch {
41
+ }
42
+ return void 0;
43
+ }
44
+ async function statusline(input, now = Date.now(), home, opts = {}) {
45
+ const costSource = opts.costSource ?? "auto";
46
+ const files = await findTranscripts(void 0, home);
47
+ const recent = [];
48
+ await Promise.all(
49
+ files.map(async (f) => {
50
+ try {
51
+ if ((await stat(f)).mtimeMs >= now - Math.max(LOOKBACK_MS, msSinceMidnight(now) + 6e4))
52
+ recent.push(f);
53
+ } catch {
54
+ }
55
+ })
56
+ );
57
+ const seen = /* @__PURE__ */ new Map();
58
+ const seenTools = /* @__PURE__ */ new Set();
59
+ const events = [];
60
+ const sessions = await Promise.all(recent.map((f) => parseTranscript(f, seen, seenTools, events)));
61
+ const today = dayKey(now);
62
+ let todayCost = 0;
63
+ let calcSessionCost;
64
+ for (const s of sessions) {
65
+ for (const [model, u] of Object.entries(s.dailyUsage[today] ?? {})) todayCost += costUsd(u, model).total;
66
+ if (input.transcript_path && s.file === input.transcript_path) {
67
+ calcSessionCost = Object.entries(s.usageByModel).reduce((sum, [m, u]) => sum + costUsd(u, m).total, 0);
68
+ }
69
+ }
70
+ const ccCost = input.cost?.total_cost_usd;
71
+ const blocks = buildBlocks(events, { now });
72
+ const active = blocks.find((b) => b.active);
73
+ const rate = active ? burnRate(active, now) : void 0;
74
+ const modelName = input.model?.display_name ?? shortModel(input.model?.id ?? "?");
75
+ const effort = input.effort?.level;
76
+ const parts = [`\u{1F916} ${modelName}${effort ? ` (${effort})` : ""}`];
77
+ parts.push(`\u{1F4B0} ${moneySegment(costSource, ccCost, calcSessionCost, todayCost, active, now)}`);
78
+ if (rate) {
79
+ const tpm = rate.tokensPerMinute;
80
+ const label = `\u{1F525} ${usd(rate.costPerHour)}/hr`;
81
+ parts.push(tpm > BURN_HIGH ? pc.red(label) : tpm > BURN_MODERATE ? pc.yellow(label) : pc.green(label));
82
+ }
83
+ const ctx = await contextTokens(input);
84
+ if (ctx !== void 0) {
85
+ const window = input.context_window?.context_window_size ?? CONTEXT_WINDOW;
86
+ const p = Math.round(ctx / window * 100);
87
+ const colored = p >= 80 ? pc.red(`${p}%`) : p >= 50 ? pc.yellow(`${p}%`) : pc.green(`${p}%`);
88
+ parts.push(`\u{1F9E0} ${compact(ctx)} (${colored})`);
89
+ }
90
+ return parts.join(pc.dim(" | "));
91
+ }
92
+ function moneySegment(source, ccCost, calcCost, todayCost, active, now) {
93
+ let session;
94
+ if (source === "both") {
95
+ session = `(${usd(ccCost ?? 0)} cc / ${usd(calcCost ?? 0)} calc) session`;
96
+ } else {
97
+ const pick = source === "cc" ? ccCost : source === "calc" ? calcCost : ccCost ?? calcCost;
98
+ session = `${usd(pick ?? 0)} session`;
99
+ }
100
+ const block = active ? `${usd(active.costUsd)} block (${fmtMs(active.end - now)} left)` : "No active block";
101
+ return `${session} / ${usd(todayCost)} today / ${block}`;
102
+ }
103
+ async function contextTokens(input) {
104
+ const cw = input.context_window;
105
+ if (cw && typeof cw.total_input_tokens === "number") return cw.total_input_tokens;
106
+ return input.transcript_path ? lastContextTokens(input.transcript_path) : void 0;
107
+ }
108
+ function msSinceMidnight(now) {
109
+ const day = dayKey(now);
110
+ const midnight = Date.parse(`${day}T00:00:00Z`);
111
+ const diff = now - midnight;
112
+ return Math.min(Math.max(diff, 0), 36 * 60 * 60 * 1e3);
113
+ }
114
+ async function readStdin() {
115
+ let data = "";
116
+ for await (const chunk of process.stdin) data += chunk;
117
+ return data;
118
+ }
119
+ var STATUSLINE_COMMAND = "npx -y @tokz/cli statusline";
120
+ function settingsPath(home = homedir()) {
121
+ return join(home, ".claude", "settings.json");
122
+ }
123
+ async function readSettings(file) {
124
+ let raw;
125
+ try {
126
+ raw = await readFile(file, "utf8");
127
+ } catch {
128
+ return {};
129
+ }
130
+ const parsed = JSON.parse(raw);
131
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
132
+ throw new Error(`${file} is not a JSON object`);
133
+ return parsed;
134
+ }
135
+ async function enableStatusline(home) {
136
+ const file = settingsPath(home);
137
+ const settings = await readSettings(file);
138
+ const prev = settings.statusLine;
139
+ if (prev?.command === STATUSLINE_COMMAND) return `Already enabled in ${file}.`;
140
+ settings.statusLine = { type: "command", command: STATUSLINE_COMMAND };
141
+ await mkdir(join(file, ".."), { recursive: true });
142
+ await writeFile(file, `${JSON.stringify(settings, null, 2)}
143
+ `);
144
+ const note = prev?.command ? ` (replaced previous statusLine: ${prev.command})` : "";
145
+ return `Statusline enabled \u2014 Claude Code will run "${STATUSLINE_COMMAND}".${note}
146
+ Restart Claude Code (or start a new session) to see it.`;
147
+ }
148
+ async function disableStatusline(home) {
149
+ const file = settingsPath(home);
150
+ const settings = await readSettings(file);
151
+ const prev = settings.statusLine;
152
+ if (!prev) return `No statusLine configured in ${file}; nothing to do.`;
153
+ if (prev.command !== void 0 && !prev.command.includes("tokz") && !prev.command.includes("@tokz/cli"))
154
+ return `statusLine in ${file} is "${prev.command}" \u2014 not tokz, leaving it alone.`;
155
+ delete settings.statusLine;
156
+ await writeFile(file, `${JSON.stringify(settings, null, 2)}
157
+ `);
158
+ return `Statusline disabled \u2014 removed from ${file}.`;
159
+ }
160
+ export {
161
+ STATUSLINE_COMMAND,
162
+ disableStatusline,
163
+ enableStatusline,
164
+ readStdin,
165
+ statusline
166
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokz/cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Audit where your coding agent's context window and API dollars go.",
5
5
  "type": "module",
6
6
  "license": "MIT",