aqua-wasted 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mehdev67
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # aqua-wasted 💧
2
+
3
+ Every token your AI burns runs in a data center that drinks fresh water to stay cool. **aqua-wasted** puts that hidden cost right in your Claude Code statusline, so you watch it climb while you work.
4
+
5
+ ```
6
+ ⬆ /build │ Opus 4.7 │ myproject 💧 Aqua wasted 2 130 ml (≈ 4 bottles)
7
+ ```
8
+
9
+ It is a fun, slightly guilty little awareness tool. The number is real enough to make you think, and grounded in published research (sources below).
10
+
11
+ ## Install
12
+
13
+ One command, zero dependencies. Claude Code already ships Node, so this just works.
14
+
15
+ ```bash
16
+ npx aqua-wasted install
17
+ ```
18
+
19
+ That drops a tiny script into `~/.claude/aqua-wasted/` and wires your `~/.claude/settings.json`. Then run `/statusline` in Claude Code or start a new session.
20
+
21
+ Already have a statusline? aqua-wasted detects it and runs it first, then appends the water segment after it. Your line is kept, not replaced.
22
+
23
+ Want it scarier or quieter:
24
+
25
+ ```bash
26
+ npx aqua-wasted install --spicy # bigger numbers, still under the studied maximum
27
+ npx aqua-wasted install --conservative # honest figures for modern efficient models
28
+ npx aqua-wasted install --unit=dl # ml is the default, dl and L also work
29
+ npx aqua-wasted install --no-bottles # drop the bottle count
30
+ ```
31
+
32
+ ## Uninstall
33
+
34
+ ```bash
35
+ npx aqua-wasted uninstall
36
+ ```
37
+
38
+ This restores the statusline you had before, or removes ours if there was none. Your settings are backed up to `settings.json.bak-<timestamp>` on every install, just in case.
39
+
40
+ ## The lifetime brag card
41
+
42
+ See the total water across every session you have ever run:
43
+
44
+ ```bash
45
+ npx aqua-wasted card --unit=L
46
+ ```
47
+
48
+ ```
49
+ 💧 aqua-wasted lifetime report
50
+ ════════════════════════════════════════════
51
+ sessions counted : 312
52
+ tokens burned : 84 200 000
53
+ water wasted : 2 526 L
54
+ bottles : 5 052 (500 ml each)
55
+ showers : 36.1
56
+ ════════════════════════════════════════════
57
+ ```
58
+
59
+ ## How the number works
60
+
61
+ Token usage is summed straight from your Claude Code session transcript, so it reflects everything the session has actually generated and read. Generating tokens costs far more water than reading context, so output and input tokens are weighted separately. Three tiers ship in the box:
62
+
63
+ | Tier | output token | input token | basis |
64
+ |------|--------------|-------------|-------|
65
+ | conservative | 0.01 ml | 0.002 ml | modern, efficient 2025 models |
66
+ | headline (default) | 0.03 ml | 0.0075 ml | maps 1:1 to the UC Riverside figures |
67
+ | spicy | 0.1 ml | 0.025 ml | GPT 4 era, hot region, still below the studied maximum |
68
+
69
+ Edit `~/.claude/aqua-wasted/config.json` any time to change tier, unit, label, emoji, or color.
70
+
71
+ ## True comparisons you can quote
72
+
73
+ - A 100 word AI email pours out about a bottle of water (519 ml), straight from the UC Riverside and Washington Post reporting.
74
+ - Roughly 16,500 generated tokens drink one 500 ml bottle.
75
+ - One million tokens is about 30 liters, two full kitchen sinks.
76
+ - A heavy coding week of around 2.5 million tokens is about 75 liters, one full shower.
77
+ - Training GPT 3 once evaporated 700,000 liters of clean freshwater, before anyone typed a single prompt.
78
+
79
+ ## Honest disclaimer
80
+
81
+ These water figures are rough, illustrative estimates. Actual water use per token varies enormously by model, data center location, cooling technology, electricity grid, and season, easily by ten times or more, and includes both the on site cooling water and the water used to generate the electricity. They are meant to raise awareness of a real but hard to pin down cost, not to be a precise measurement.
82
+
83
+ Primary sources:
84
+
85
+ - Li, Yang, Islam, Ren. "Making AI Less Thirsty." UC Riverside, 2023, peer reviewed in Communications of the ACM, 2025. https://arxiv.org/abs/2304.03271
86
+ - Washington Post, "AI is exhausting the power grid," September 2024.
87
+ - Epoch AI, "How much energy does ChatGPT use?", 2025.
88
+
89
+ ## Use as a Claude Code plugin
90
+
91
+ Prefer the plugin browser:
92
+
93
+ ```
94
+ /plugin marketplace add mehdev67/aqua-wasted
95
+ /plugin install aqua-wasted@aqua-wasted
96
+ ```
97
+
98
+ Then run the bundled command to wire it up:
99
+
100
+ ```
101
+ /aqua-wasted
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT. Use it, fork it, make the planet a tiny bit more visible.
package/bin/cli.js ADDED
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // aqua-wasted CLI: install | uninstall | status | card | help
5
+ // Safely wires the statusline into Claude Code and renders a shareable card.
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+ const core = require('../statusline.js');
11
+
12
+ const HOME = os.homedir();
13
+ const CLAUDE_DIR = path.join(HOME, '.claude');
14
+ const SETTINGS = path.join(CLAUDE_DIR, 'settings.json');
15
+ const INSTALL_DIR = path.join(CLAUDE_DIR, 'aqua-wasted');
16
+ const RUNTIME = path.join(INSTALL_DIR, 'statusline.js');
17
+ const CONFIG = path.join(INSTALL_DIR, 'config.json');
18
+ const PKG_RUNTIME = path.join(__dirname, '..', 'statusline.js');
19
+ const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
20
+
21
+ // --- small helpers ----------------------------------------------------------
22
+
23
+ function readJson(file, fallback) {
24
+ try {
25
+ if (!fs.existsSync(file)) return fallback;
26
+ const raw = fs.readFileSync(file, 'utf8');
27
+ if (!raw.trim()) return fallback;
28
+ return JSON.parse(raw);
29
+ } catch (e) {
30
+ return undefined; // signals a parse failure to the caller
31
+ }
32
+ }
33
+
34
+ function atomicWrite(file, contents) {
35
+ const tmp = file + '.tmp-' + process.pid;
36
+ fs.writeFileSync(tmp, contents);
37
+ fs.renameSync(tmp, file);
38
+ }
39
+
40
+ function ourCommand() {
41
+ return 'node "' + RUNTIME + '"';
42
+ }
43
+
44
+ function isOurCommand(cmd) {
45
+ return typeof cmd === 'string' && cmd.indexOf('aqua-wasted') !== -1;
46
+ }
47
+
48
+ function parseFlags(argv) {
49
+ const flags = {};
50
+ for (let i = 0; i < argv.length; i++) {
51
+ const a = argv[i];
52
+ if (a === '--spicy') flags.tier = 'spicy';
53
+ else if (a === '--conservative') flags.tier = 'conservative';
54
+ else if (a === '--headline') flags.tier = 'headline';
55
+ else if (a === '--no-bottles') flags.bottles = false;
56
+ else if (a === '--no-color') flags.color = false;
57
+ else if (a === '--no-chain') flags.noChain = true;
58
+ else if (a.startsWith('--tier=')) flags.tier = a.slice(7);
59
+ else if (a.startsWith('--unit=')) flags.unit = a.slice(7);
60
+ else if (a.startsWith('--label=')) flags.label = a.slice(8);
61
+ else if (a.startsWith('--locale=')) flags.locale = a.slice(9);
62
+ else if (a.startsWith('--color=')) flags.color = a.slice(8);
63
+ }
64
+ return flags;
65
+ }
66
+
67
+ // --- commands ---------------------------------------------------------------
68
+
69
+ function install(argv) {
70
+ const flags = parseFlags(argv);
71
+
72
+ const settings = readJson(SETTINGS, {});
73
+ if (settings === undefined) {
74
+ console.error('aqua-wasted: could not parse ' + SETTINGS + '. It may contain comments or be invalid JSON.');
75
+ console.error('Nothing was changed. Add this block manually instead:');
76
+ console.error(' "statusLine": { "type": "command", "command": ' + JSON.stringify(ourCommand()) + ' }');
77
+ process.exit(1);
78
+ }
79
+
80
+ // Detect an existing statusLine so we can chain rather than clobber it.
81
+ let chain = null;
82
+ const existing = settings.statusLine;
83
+ if (existing && existing.type === 'command' && existing.command && !isOurCommand(existing.command)) {
84
+ if (!flags.noChain) {
85
+ chain = existing.command;
86
+ console.log('Found an existing statusLine. aqua-wasted will run it first and append after it.');
87
+ } else {
88
+ console.log('Found an existing statusLine. Replacing it (--no-chain).');
89
+ }
90
+ } else if (existing && existing.type !== 'command' && !isOurCommand(JSON.stringify(existing))) {
91
+ console.log('Found a non command statusLine. Leaving it and adding ours on top.');
92
+ }
93
+
94
+ // Write runtime + config into a stable location, independent of the npx cache.
95
+ fs.mkdirSync(INSTALL_DIR, { recursive: true });
96
+ fs.copyFileSync(PKG_RUNTIME, RUNTIME);
97
+
98
+ const cfg = Object.assign({}, core.DEFAULT_CONFIG, {
99
+ tier: flags.tier || core.DEFAULT_CONFIG.tier,
100
+ unit: flags.unit || core.DEFAULT_CONFIG.unit,
101
+ bottles: flags.bottles === false ? false : core.DEFAULT_CONFIG.bottles,
102
+ color: flags.color === false ? false : flags.color || core.DEFAULT_CONFIG.color,
103
+ label: flags.label || core.DEFAULT_CONFIG.label,
104
+ locale: flags.locale || core.DEFAULT_CONFIG.locale,
105
+ chain: chain,
106
+ });
107
+ if (!core.TIERS[cfg.tier]) {
108
+ console.error('aqua-wasted: unknown tier "' + cfg.tier + '". Use conservative, headline, or spicy.');
109
+ process.exit(1);
110
+ }
111
+ atomicWrite(CONFIG, JSON.stringify(cfg, null, 2) + '\n');
112
+
113
+ // Back up settings, then patch only the statusLine key.
114
+ if (fs.existsSync(SETTINGS)) {
115
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
116
+ fs.copyFileSync(SETTINGS, SETTINGS + '.bak-' + stamp);
117
+ } else {
118
+ fs.mkdirSync(CLAUDE_DIR, { recursive: true });
119
+ }
120
+ settings.statusLine = { type: 'command', command: ourCommand(), padding: 0 };
121
+ atomicWrite(SETTINGS, JSON.stringify(settings, null, 2) + '\n');
122
+
123
+ console.log('');
124
+ console.log(' ' + core.renderSegment({ output: 9000, input: 60000, cacheCreation: 0, cacheRead: 0 }, cfg));
125
+ console.log('');
126
+ console.log('aqua-wasted installed. Tier: ' + cfg.tier + ', unit: ' + cfg.unit + '.');
127
+ console.log('Run /statusline in Claude Code or start a new session to see it.');
128
+ console.log('Config: ' + CONFIG);
129
+ }
130
+
131
+ function uninstall() {
132
+ const settings = readJson(SETTINGS, {});
133
+ if (settings === undefined) {
134
+ console.error('aqua-wasted: could not parse ' + SETTINGS + '. Remove the statusLine block manually.');
135
+ process.exit(1);
136
+ }
137
+ const cfg = readJson(CONFIG, {}) || {};
138
+ const sl = settings.statusLine;
139
+ if (sl && sl.command && isOurCommand(sl.command)) {
140
+ if (cfg.chain) {
141
+ settings.statusLine = { type: 'command', command: cfg.chain };
142
+ console.log('Restored your previous statusLine.');
143
+ } else {
144
+ delete settings.statusLine;
145
+ console.log('Removed the aqua-wasted statusLine.');
146
+ }
147
+ atomicWrite(SETTINGS, JSON.stringify(settings, null, 2) + '\n');
148
+ } else {
149
+ console.log('No aqua-wasted statusLine found in settings. Nothing changed.');
150
+ }
151
+ try {
152
+ fs.rmSync(INSTALL_DIR, { recursive: true, force: true });
153
+ } catch (e) {
154
+ // leave it; not fatal
155
+ }
156
+ console.log('Done. Your settings backups (.bak-*) were kept.');
157
+ }
158
+
159
+ function status() {
160
+ const cfg = readJson(CONFIG, undefined);
161
+ const settings = readJson(SETTINGS, {});
162
+ const installed =
163
+ settings && settings.statusLine && isOurCommand(settings.statusLine.command || '');
164
+ console.log('aqua-wasted status');
165
+ console.log(' installed: ' + (installed ? 'yes' : 'no'));
166
+ if (cfg) {
167
+ console.log(' tier: ' + cfg.tier);
168
+ console.log(' unit: ' + cfg.unit);
169
+ console.log(' bottles: ' + cfg.bottles);
170
+ console.log(' chained: ' + (cfg.chain ? 'yes' : 'no'));
171
+ }
172
+ console.log(' config: ' + CONFIG);
173
+ }
174
+
175
+ // Walk ~/.claude/projects for every transcript and sum lifetime tokens.
176
+ function lifetimeTotals() {
177
+ const totals = { input: 0, cacheCreation: 0, cacheRead: 0, output: 0 };
178
+ let files = 0;
179
+ function walk(dir) {
180
+ let entries = [];
181
+ try {
182
+ entries = fs.readdirSync(dir, { withFileTypes: true });
183
+ } catch (e) {
184
+ return;
185
+ }
186
+ for (const ent of entries) {
187
+ const full = path.join(dir, ent.name);
188
+ if (ent.isDirectory()) walk(full);
189
+ else if (ent.isFile() && ent.name.endsWith('.jsonl')) {
190
+ files++;
191
+ const t = core.sumTokensFromTranscript(full);
192
+ totals.input += t.input;
193
+ totals.cacheCreation += t.cacheCreation;
194
+ totals.cacheRead += t.cacheRead;
195
+ totals.output += t.output;
196
+ }
197
+ }
198
+ }
199
+ walk(PROJECTS_DIR);
200
+ return { totals: totals, files: files };
201
+ }
202
+
203
+ function card(argv) {
204
+ const flags = parseFlags(argv);
205
+ const cfg = Object.assign({}, core.DEFAULT_CONFIG, readJson(CONFIG, {}) || {}, {
206
+ tier: flags.tier || (readJson(CONFIG, {}) || {}).tier || core.DEFAULT_CONFIG.tier,
207
+ unit: flags.unit || 'auto',
208
+ });
209
+ const res = lifetimeTotals();
210
+ const ml = core.computeWaterMl(res.totals, cfg.tier);
211
+ const liters = ml / 1000;
212
+ const bottles = Math.round(ml / core.BOTTLE_ML);
213
+ const showers = liters / 70; // a typical shower is roughly 70 liters
214
+ const tokens =
215
+ res.totals.input + res.totals.cacheCreation + res.totals.cacheRead + res.totals.output;
216
+
217
+ const nf = (n, d) => core.formatNumber(n, d, cfg.locale);
218
+ const line = ' ' + '═'.repeat(44);
219
+ console.log('');
220
+ console.log(' \u{1F4A7} aqua-wasted lifetime report');
221
+ console.log(line);
222
+ console.log(' sessions counted : ' + nf(res.files, 0));
223
+ console.log(' tokens burned : ' + nf(tokens, 0));
224
+ console.log(' water wasted : ' + core.formatWater(ml, cfg.unit, cfg.locale));
225
+ console.log(' bottles : ' + nf(bottles, 0) + ' (500 ml each)');
226
+ console.log(' showers : ' + nf(showers, 1));
227
+ console.log(line);
228
+ console.log(' tier: ' + cfg.tier + '. Rough illustrative estimate. See the README.');
229
+ console.log('');
230
+ }
231
+
232
+ function help() {
233
+ console.log('aqua-wasted: turn your Claude Code token usage into water wasted.');
234
+ console.log('');
235
+ console.log('Usage:');
236
+ console.log(' npx aqua-wasted install [--spicy|--conservative] [--unit=ml|dl|L|auto] [--no-bottles] [--no-chain]');
237
+ console.log(' npx aqua-wasted uninstall');
238
+ console.log(' npx aqua-wasted status');
239
+ console.log(' npx aqua-wasted card [--unit=L]');
240
+ console.log('');
241
+ console.log('Install drops the statusline into ~/.claude/aqua-wasted/ and wires settings.json.');
242
+ console.log('If you already have a statusLine, it is chained, not replaced.');
243
+ }
244
+
245
+ // --- dispatch ---------------------------------------------------------------
246
+
247
+ const [, , cmd, ...rest] = process.argv;
248
+ switch (cmd) {
249
+ case 'install':
250
+ install(rest);
251
+ break;
252
+ case 'uninstall':
253
+ case 'remove':
254
+ uninstall();
255
+ break;
256
+ case 'status':
257
+ status();
258
+ break;
259
+ case 'card':
260
+ card(rest);
261
+ break;
262
+ case 'help':
263
+ case '--help':
264
+ case '-h':
265
+ case undefined:
266
+ help();
267
+ break;
268
+ default:
269
+ console.error('aqua-wasted: unknown command "' + cmd + '"');
270
+ help();
271
+ process.exit(1);
272
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "aqua-wasted",
3
+ "version": "0.1.0",
4
+ "description": "A Claude Code statusline that turns your token usage into the cooling water your AI session burns.",
5
+ "bin": {
6
+ "aqua-wasted": "bin/cli.js"
7
+ },
8
+ "type": "commonjs",
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "statusline.js",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "card": "node bin/cli.js card",
20
+ "demo": "echo '{\"transcript_path\":\"\",\"context_window\":{\"current_usage\":{\"output_tokens\":9000,\"input_tokens\":60000}}}' | node statusline.js"
21
+ },
22
+ "keywords": [
23
+ "claude-code",
24
+ "statusline",
25
+ "tokens",
26
+ "water",
27
+ "sustainability",
28
+ "awareness",
29
+ "cli"
30
+ ],
31
+ "author": "mehdev67",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/mehdev67/aqua-wasted.git"
36
+ },
37
+ "homepage": "https://github.com/mehdev67/aqua-wasted#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/mehdev67/aqua-wasted/issues"
40
+ },
41
+ "dependencies": {}
42
+ }
package/statusline.js ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // aqua-wasted: a Claude Code statusline that turns your token usage into the
5
+ // cooling water your AI session burns, to make an invisible cost visible.
6
+ //
7
+ // This file is BOTH the runtime (Claude Code runs it as the statusLine command)
8
+ // and a small library the CLI reuses. It has zero dependencies on purpose so it
9
+ // can be copied to ~/.claude/aqua-wasted/ and run standalone forever.
10
+ //
11
+ // Water model (see README for sources and the honest disclaimer):
12
+ // Based on Li et al. "Making AI Less Thirsty" (UC Riverside) and the
13
+ // Washington Post 2024 reporting. Generating tokens costs far more water than
14
+ // reading context, so output and input tokens are weighted separately.
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { spawnSync } = require('child_process');
19
+
20
+ // ml of cooling water attributed per token, by scariness tier.
21
+ const TIERS = {
22
+ conservative: { output: 0.01, input: 0.002 }, // modern, efficient 2025 models
23
+ headline: { output: 0.03, input: 0.0075 }, // maps 1:1 to the UC Riverside table
24
+ spicy: { output: 0.1, input: 0.025 }, // GPT 4 era, hot region, still under the studied max
25
+ };
26
+
27
+ const BOTTLE_ML = 500; // a standard water bottle, for relatable framing
28
+
29
+ const DEFAULT_CONFIG = {
30
+ tier: 'headline',
31
+ unit: 'ml', // auto | ml | dl | L
32
+ bottles: true,
33
+ emoji: '\u{1F4A7}', // droplet
34
+ label: 'Aqua wasted',
35
+ color: 'cyan', // cyan | blue | red | yellow | green | magenta | false
36
+ locale: 'en-US',
37
+ chain: null, // an existing statusLine command to run first, then append ours
38
+ };
39
+
40
+ function loadConfig(dir) {
41
+ try {
42
+ const p = path.join(dir, 'config.json');
43
+ if (fs.existsSync(p)) {
44
+ return Object.assign({}, DEFAULT_CONFIG, JSON.parse(fs.readFileSync(p, 'utf8')));
45
+ }
46
+ } catch (e) {
47
+ // fall through to defaults
48
+ }
49
+ return Object.assign({}, DEFAULT_CONFIG);
50
+ }
51
+
52
+ // Sum every token recorded in a session transcript (the cumulative figure that
53
+ // makes the number climb dramatically). Shape matches Claude Code JSONL lines.
54
+ function sumTokensFromTranscript(transcriptPath) {
55
+ const totals = { input: 0, cacheCreation: 0, cacheRead: 0, output: 0 };
56
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return totals;
57
+ let text = '';
58
+ try {
59
+ text = fs.readFileSync(transcriptPath, 'utf8');
60
+ } catch (e) {
61
+ return totals;
62
+ }
63
+ for (const line of text.split('\n')) {
64
+ if (!line.trim()) continue;
65
+ let obj;
66
+ try {
67
+ obj = JSON.parse(line);
68
+ } catch (e) {
69
+ continue;
70
+ }
71
+ const u = obj && obj.message && obj.message.usage;
72
+ if (!u) continue;
73
+ totals.input += u.input_tokens || 0;
74
+ totals.cacheCreation += u.cache_creation_input_tokens || 0;
75
+ totals.cacheRead += u.cache_read_input_tokens || 0;
76
+ totals.output += u.output_tokens || 0;
77
+ }
78
+ return totals;
79
+ }
80
+
81
+ // Fallback when the transcript is missing: use the live context window numbers
82
+ // Claude Code now passes on stdin.
83
+ function tokensFromPayload(payload) {
84
+ const totals = { input: 0, cacheCreation: 0, cacheRead: 0, output: 0 };
85
+ const cu = payload && payload.context_window && payload.context_window.current_usage;
86
+ if (cu) {
87
+ totals.input = cu.input_tokens || 0;
88
+ totals.cacheCreation = cu.cache_creation_input_tokens || 0;
89
+ totals.cacheRead = cu.cache_read_input_tokens || 0;
90
+ totals.output = cu.output_tokens || 0;
91
+ }
92
+ return totals;
93
+ }
94
+
95
+ function computeWaterMl(totals, tierName) {
96
+ const tier = TIERS[tierName] || TIERS.headline;
97
+ const inputSide = totals.input + totals.cacheCreation + totals.cacheRead;
98
+ return totals.output * tier.output + inputSide * tier.input;
99
+ }
100
+
101
+ function formatNumber(n, decimals, locale) {
102
+ const v = Number(n.toFixed(decimals));
103
+ try {
104
+ return v.toLocaleString(locale || 'en-US');
105
+ } catch (e) {
106
+ return String(v);
107
+ }
108
+ }
109
+
110
+ function formatWater(ml, unit, locale) {
111
+ if (unit === 'ml') return formatNumber(Math.round(ml), 0, locale) + ' ml';
112
+ if (unit === 'dl') return formatNumber(ml / 100, 1, locale) + ' dl';
113
+ if (unit === 'L') return formatNumber(ml / 1000, 2, locale) + ' L';
114
+ // auto
115
+ if (ml < 1000) return formatNumber(Math.round(ml), 0, locale) + ' ml';
116
+ const liters = ml / 1000;
117
+ return liters < 10
118
+ ? formatNumber(liters, 2, locale) + ' L'
119
+ : formatNumber(liters, 1, locale) + ' L';
120
+ }
121
+
122
+ const ANSI = { cyan: '36', blue: '34', red: '31', yellow: '33', green: '32', magenta: '35' };
123
+
124
+ function colorize(text, color) {
125
+ if (!color || !ANSI[color]) return text;
126
+ return '\x1b[' + ANSI[color] + 'm' + text + '\x1b[0m';
127
+ }
128
+
129
+ function renderSegment(totals, cfg) {
130
+ const ml = computeWaterMl(totals, cfg.tier);
131
+ const bottles = Math.round(ml / BOTTLE_ML);
132
+ const bottleWord = bottles === 1 ? 'bottle' : 'bottles';
133
+ const bottleNote =
134
+ cfg.bottles && bottles >= 1
135
+ ? ' (≈ ' + formatNumber(bottles, 0, cfg.locale) + ' ' + bottleWord + ')'
136
+ : '';
137
+ const text = cfg.emoji + ' ' + cfg.label + ' ' + formatWater(ml, cfg.unit, cfg.locale) + bottleNote;
138
+ return colorize(text, cfg.color);
139
+ }
140
+
141
+ // Run an existing statusLine command, passing the same stdin through, so we
142
+ // append to it rather than replacing it.
143
+ function runChain(chainCmd, stdinRaw) {
144
+ if (!chainCmd) return '';
145
+ try {
146
+ const res = spawnSync(chainCmd, {
147
+ shell: true,
148
+ input: stdinRaw,
149
+ encoding: 'utf8',
150
+ timeout: 5000,
151
+ });
152
+ return (res.stdout || '').replace(/\s+$/, '');
153
+ } catch (e) {
154
+ return '';
155
+ }
156
+ }
157
+
158
+ function main() {
159
+ let stdinRaw = '';
160
+ try {
161
+ stdinRaw = fs.readFileSync(0, 'utf8');
162
+ } catch (e) {
163
+ stdinRaw = '';
164
+ }
165
+ let payload = {};
166
+ try {
167
+ payload = JSON.parse(stdinRaw) || {};
168
+ } catch (e) {
169
+ payload = {};
170
+ }
171
+
172
+ const cfg = loadConfig(__dirname);
173
+
174
+ let totals = sumTokensFromTranscript(payload.transcript_path);
175
+ const sum = totals.input + totals.cacheCreation + totals.cacheRead + totals.output;
176
+ if (sum === 0) totals = tokensFromPayload(payload);
177
+
178
+ const base = runChain(cfg.chain, stdinRaw);
179
+ const segment = renderSegment(totals, cfg);
180
+ process.stdout.write(base ? base + ' ' + segment : segment);
181
+ }
182
+
183
+ module.exports = {
184
+ TIERS,
185
+ BOTTLE_ML,
186
+ DEFAULT_CONFIG,
187
+ loadConfig,
188
+ sumTokensFromTranscript,
189
+ tokensFromPayload,
190
+ computeWaterMl,
191
+ formatWater,
192
+ formatNumber,
193
+ renderSegment,
194
+ };
195
+
196
+ if (require.main === module) main();