ccwatts 0.1.0 → 0.1.1
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/README.md +1 -0
- package/dist/index.js +104 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ npx ccwatts history # weekly trend, survives log cleanup
|
|
|
9
9
|
npx ccwatts receipt # share line and card
|
|
10
10
|
npx ccwatts dashboard # offline HTML dashboard
|
|
11
11
|
npx ccwatts --method # every parameter and its source
|
|
12
|
+
npx ccwatts --demo # no Claude Code logs yet? try synthetic sample data
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
- **Local only.** No network calls, no accounts, no telemetry. It reads token counts, never prompt or response text.
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { writeFile as writeFile3 } from "fs/promises";
|
|
4
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
5
5
|
import { resolve } from "path";
|
|
6
6
|
import { parseArgs } from "util";
|
|
7
7
|
|
|
8
8
|
// ../core/src/meta.ts
|
|
9
9
|
var APP_NAME = "ccwatts";
|
|
10
|
-
var APP_VERSION = "0.1.
|
|
10
|
+
var APP_VERSION = "0.1.1";
|
|
11
11
|
var REPO_URL = "https://github.com/holroydchase-afk/ccwatts";
|
|
12
12
|
var PLAYGROUND_URL = "https://chaseholroyd.com/ccwatts/";
|
|
13
13
|
|
|
@@ -25,13 +25,14 @@ function num(n) {
|
|
|
25
25
|
if (n >= 0.01) return (Math.round(n * 100) / 100).toString();
|
|
26
26
|
return n === 0 ? "0" : n.toPrecision(1);
|
|
27
27
|
}
|
|
28
|
+
var fixed = (n) => n >= 1 && n < 100 ? n.toFixed(1) : num(n);
|
|
28
29
|
function formatEnergy(range) {
|
|
29
30
|
const kwh = range.high >= 1e3;
|
|
30
31
|
const scale = kwh ? 1e3 : 1;
|
|
31
32
|
const unit = kwh ? "kWh" : "Wh";
|
|
32
33
|
return {
|
|
33
|
-
central: `${
|
|
34
|
-
range: `${
|
|
34
|
+
central: `${fixed(range.central / scale)} ${unit}`,
|
|
35
|
+
range: `${fixed(range.low / scale)} \u2013 ${fixed(range.high / scale)}`,
|
|
35
36
|
unit
|
|
36
37
|
};
|
|
37
38
|
}
|
|
@@ -165,25 +166,34 @@ var projectName = (key) => key.split("#")[0] ?? key;
|
|
|
165
166
|
function since(records, isoStart) {
|
|
166
167
|
return records.filter((r) => r.timestamp >= isoStart);
|
|
167
168
|
}
|
|
169
|
+
function phoneText(charges, round) {
|
|
170
|
+
if (round(charges) === 1) return "charging a phone about once";
|
|
171
|
+
if (charges >= 1) return `charging a phone about ${round(charges)} times`;
|
|
172
|
+
const pct = Math.round(charges * 100);
|
|
173
|
+
if (pct < 1) return "under 1% of a phone charge";
|
|
174
|
+
if (pct >= 45 && pct <= 55) return "about half a phone charge";
|
|
175
|
+
return `about ${Math.round(pct / 10) * 10 || pct}% of a phone charge`;
|
|
176
|
+
}
|
|
168
177
|
function comparisons(kwh, params) {
|
|
169
178
|
const c = params.comparisons;
|
|
170
179
|
const out = [];
|
|
171
180
|
const round = (n) => n >= 10 ? Math.round(n) : Math.round(n * 10) / 10;
|
|
181
|
+
const count = (n, one, many) => round(n) === 1 ? `1 ${one}` : `${round(n)} ${many}`;
|
|
172
182
|
if (c.ev_kwh_per_mile) {
|
|
173
183
|
const miles = kwh / c.ev_kwh_per_mile.value;
|
|
174
|
-
out.push({ id: "ev", amount: miles, text: `driving an EV about ${
|
|
184
|
+
out.push({ id: "ev", amount: miles, text: `driving an EV about ${count(miles, "mile", "miles")}` });
|
|
175
185
|
}
|
|
176
186
|
if (c.fridge_kwh_per_day) {
|
|
177
187
|
const days = kwh / c.fridge_kwh_per_day.value;
|
|
178
|
-
out.push({ id: "fridge", amount: days, text: `running a fridge about ${
|
|
188
|
+
out.push({ id: "fridge", amount: days, text: `running a fridge about ${count(days, "day", "days")}` });
|
|
179
189
|
}
|
|
180
190
|
if (c.phone_charge_kwh) {
|
|
181
191
|
const charges = kwh / c.phone_charge_kwh.value;
|
|
182
|
-
out.push({ id: "phone", amount: charges, text:
|
|
192
|
+
out.push({ id: "phone", amount: charges, text: phoneText(charges, round) });
|
|
183
193
|
}
|
|
184
194
|
if (c.us_home_kwh_per_day) {
|
|
185
195
|
const days = kwh / c.us_home_kwh_per_day.value;
|
|
186
|
-
out.push({ id: "home", amount: days, text: `powering a US home about ${
|
|
196
|
+
out.push({ id: "home", amount: days, text: `powering a US home about ${count(days, "day", "days")}` });
|
|
187
197
|
}
|
|
188
198
|
return out;
|
|
189
199
|
}
|
|
@@ -207,7 +217,7 @@ function sampleWhose(windowLabel) {
|
|
|
207
217
|
function shareLine({ estimate: e, params, windowLabel, sample }) {
|
|
208
218
|
const t = formatEnergy(e.total);
|
|
209
219
|
const first = everyday(e.total.central / 1e3, params).split(" \xB7 ")[0];
|
|
210
|
-
return `${sample ? sampleWhose(windowLabel) : whose(windowLabel)}
|
|
220
|
+
return `${sample ? sampleWhose(windowLabel) : whose(windowLabel)} came to about ${t.central} of electricity (range ${t.range} ${t.unit}), like ${first}. Estimated locally with ${APP_NAME}, method v${params.version}: ${repoShort}`;
|
|
211
221
|
}
|
|
212
222
|
function linkedInPost(input) {
|
|
213
223
|
const { estimate: e, params, windowLabel, sample } = input;
|
|
@@ -215,7 +225,7 @@ function linkedInPost(input) {
|
|
|
215
225
|
const cacheShare = e.total.central > 0 ? Math.round((e.byType.find((r) => r.key === "cacheRead")?.energy.central ?? 0) / e.total.central * 100) : 0;
|
|
216
226
|
if (sample) {
|
|
217
227
|
return [
|
|
218
|
-
`(Sample data) This is what ${APP_NAME} shows for a sample developer, not a real measurement. ${sampleWhose(windowLabel).replace("(Sample data) ", "")}: about ${t.central} (range ${t.range} ${t.unit}),
|
|
228
|
+
`(Sample data) This is what ${APP_NAME} shows for a sample developer, not a real measurement. ${sampleWhose(windowLabel).replace("(Sample data) ", "")}: about ${t.central} (range ${t.range} ${t.unit}), like ${everyday(e.total.central / 1e3, params).replace(" \xB7 ", " or ")}.`,
|
|
219
229
|
"",
|
|
220
230
|
`${cacheShare}% of it came from the agent re-reading context it had already seen.`,
|
|
221
231
|
"",
|
|
@@ -226,7 +236,7 @@ function linkedInPost(input) {
|
|
|
226
236
|
].join("\n");
|
|
227
237
|
}
|
|
228
238
|
return [
|
|
229
|
-
`I estimated the electricity behind my AI coding. ${whose(windowLabel)}: about ${t.central} (range ${t.range} ${t.unit}),
|
|
239
|
+
`I estimated the electricity behind my AI coding. ${whose(windowLabel)}: about ${t.central} (range ${t.range} ${t.unit}), like ${everyday(e.total.central / 1e3, params).replace(" \xB7 ", " or ")}.`,
|
|
230
240
|
"",
|
|
231
241
|
`${cacheShare >= 50 ? "The surprise: " : ""}${cacheShare}% of it came from the agent re-reading context it had already seen.`,
|
|
232
242
|
"",
|
|
@@ -640,9 +650,15 @@ function injectData(html, payload) {
|
|
|
640
650
|
if (!DATA_MARKER.test(html)) throw new Error("Dashboard template is missing its data slot.");
|
|
641
651
|
return html.replace(DATA_MARKER, (_m, open, _old, close) => `${open}${safeJson(payload)}${close}`);
|
|
642
652
|
}
|
|
653
|
+
var SAMPLE_MARKER = /<script id="ccwatts-sample" type="application\/json">([\s\S]*?)<\/script>/;
|
|
654
|
+
function readSample(html) {
|
|
655
|
+
const json2 = SAMPLE_MARKER.exec(html)?.[1];
|
|
656
|
+
if (!json2) throw new Error("Page is missing its sample data.");
|
|
657
|
+
return JSON.parse(json2);
|
|
658
|
+
}
|
|
643
659
|
|
|
644
660
|
// ../web/dist/ccwatts.html
|
|
645
|
-
var ccwatts_default = '<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; script-src \'sha256-7q/PdAdCG3ZsmZDjCI8gAjVev2OuHqaTGUfCFPxxE+I=\'; style-src \'unsafe-inline\'; img-src data: blob:; base-uri \'none\'; form-action \'none\'">\n<meta name="referrer" content="no-referrer">\n<title>ccwatts</title>\n<meta name="description" content="Estimate the electricity your Claude Code usage consumed, as a sourced range. Runs entirely in your browser.">\n<!-- Link preview for LinkedIn and others. The crawler fetches the image; this page never does. -->\n<meta property="og:type" content="website">\n<meta property="og:title" content="How much electricity does your Claude Code use?">\n<meta property="og:description" content="A sourced kWh range from your own logs. Runs in your browser, and your logs never leave it.">\n<meta property="og:url" content="https://chaseholroyd.com/ccwatts/">\n<meta property="og:image" content="https://chaseholroyd.com/ccwatts/og.png">\n<meta property="og:image:width" content="1200">\n<meta property="og:image:height" content="627">\n<meta property="og:image:alt" content="ccwatts: how much electricity does your Claude Code use?">\n<meta name="twitter:card" content="summary_large_image">\n<style>\n/* Layout: one reading column; the headline reads like the top of a utility bill, the share card like a till receipt. */\n:root{\n --bg:#f4f2ea; --surface:#fbfaf6; --ink:#1f2a24; --muted:#5e6b63; --line:#d9ddd3; --accent:#2f6b4f; --accent-ink:#ffffff;\n --t-cacheRead:#2f6b4f; --t-cacheWrite:#c08a2e; --t-output:#b4553a; --t-input:#4a6fa5;\n --sample:#8a5a12; --control:#7f8a83;\n --sans:"Helvetica Neue",Helvetica,Arial,system-ui,sans-serif;\n --mono:ui-monospace,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;\n color-scheme:light;\n}\n@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){\n --bg:#10201a; --surface:#16291f; --ink:#e8eee9; --muted:#9fb3a6; --line:#29402f; --accent:#7cc4a0; --accent-ink:#10201a;\n --t-cacheRead:#7cc4a0; --t-cacheWrite:#e0b56a; --t-output:#e58a6e; --t-input:#8fb2e8; --sample:#e0b56a; --control:#5d7a68; color-scheme:dark}}\n:root[data-theme="dark"]{\n --bg:#10201a; --surface:#16291f; --ink:#e8eee9; --muted:#9fb3a6; --line:#29402f; --accent:#7cc4a0; --accent-ink:#10201a;\n --t-cacheRead:#7cc4a0; --t-cacheWrite:#e0b56a; --t-output:#e58a6e; --t-input:#8fb2e8; --sample:#e0b56a; --control:#5d7a68; color-scheme:dark}\n*{box-sizing:border-box}\n[hidden]{display:none!important}\nbody{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:16px;line-height:1.55}\n.wrap{max-width:960px;margin:0 auto;padding-block:28px 64px;padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px));display:flex;flex-direction:column;gap:40px}\nheader.top{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px}\n.back{font-size:15px;display:inline-flex;align-items:center;min-height:44px;text-decoration:none;color:var(--muted);align-self:flex-start;margin-block:-12px -28px}\n.back:hover{color:var(--accent);text-decoration:underline}\na:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}\n.intro{display:flex;flex-direction:column;gap:8px}\nh1{font-size:clamp(26px,5vw,36px);line-height:1.15;margin:0;letter-spacing:-.01em;text-wrap:balance}\n.lede{margin:0;font-size:17px;color:var(--muted);max-width:60ch}\n.brand{font-family:var(--mono);font-weight:700;font-size:18px;letter-spacing:.02em}\n.brand span{color:var(--accent)}\n.pill{font-family:var(--mono);font-size:12px;letter-spacing:.06em;text-transform:uppercase;border:1px solid var(--line);border-radius:999px;padding:4px 10px;color:var(--muted)}\n.pill.sample{border-color:var(--sample);color:var(--sample)}\n.privacy{display:flex;gap:10px;align-items:flex-start;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:12px 14px;font-size:15px}\n.privacy b{color:var(--accent)}\n.hero{display:flex;flex-direction:column;gap:6px}\n.eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}\n.big{font-weight:700;font-size:clamp(56px,13vw,112px);line-height:1;letter-spacing:-.02em;font-variant-numeric:tabular-nums}\n.big small{font-size:.42em;font-weight:500;margin-left:.2em;letter-spacing:0}\n.range{font-size:clamp(18px,3vw,22px)}\n.compare{color:var(--muted);font-size:17px}\n.meta{font-family:var(--mono);font-size:13px;color:var(--muted)}\n.controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center}\nbutton,.btn{font:inherit;font-size:15px;min-height:44px;display:inline-flex;align-items:center;border:1px solid var(--control);background:var(--surface);color:var(--ink);border-radius:8px;padding:8px 14px;cursor:pointer}\nbutton:hover,.btn:hover{border-color:var(--accent)}\nbutton:focus-visible,.btn:focus-visible,input:focus-visible,.vh:focus-visible+.btn{outline:2px solid var(--accent);outline-offset:2px}\n/* Visually hidden but still focusable, so the styled label stays reachable by keyboard. */\n.vh{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}\nbutton[aria-pressed="true"]{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}\n.primary{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}\n.drop{border:1.5px dashed var(--line);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:10px;background:var(--surface)}\n.drop.over{border-color:var(--accent)}\n.drop p{margin:0;color:var(--muted);font-size:14px}\ncode{font-family:var(--mono);font-size:.92em}\nsection{display:flex;flex-direction:column;gap:14px;min-width:0}\nh2{font-size:20px;margin:0;text-wrap:balance}\n.grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:32px}\n.chart{width:100%;height:auto;display:block}\n.chart text{fill:var(--muted);font-family:var(--mono);font-size:11px}\n.legend{display:flex;flex-wrap:wrap;gap:14px;font-size:13px;color:var(--muted)}\n.legend i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:6px;vertical-align:-1px}\n.rank{display:flex;flex-direction:column;gap:10px;margin:0;padding:0;list-style:none}\n.rank li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 12px;font-size:14px}\n.rank .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.rank .val{font-family:var(--mono);font-variant-numeric:tabular-nums;color:var(--muted);text-align:right}\n.rank .bar{grid-column:1/-1;height:8px;background:var(--line);border-radius:4px;overflow:hidden}\n.rank .bar span{display:block;height:100%;background:var(--accent)}\n.explain{max-width:65ch;margin:0}\n.tablewrap{overflow-x:auto;border:1px solid var(--line);border-radius:10px}\ntable{border-collapse:collapse;width:100%;font-size:14px;min-width:620px}\nth,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--line);vertical-align:top}\nth{font-family:var(--mono);font-size:12px;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);font-weight:500}\ntd.num{font-family:var(--mono);font-variant-numeric:tabular-nums;white-space:nowrap}\ntr:last-child td{border-bottom:0}\n.label{font-family:var(--mono);font-size:11px;padding:2px 6px;border-radius:4px;border:1px solid currentColor}\na{color:var(--accent)}\na.src{display:inline-flex;align-items:center;min-height:44px}\n.phone-only{display:none}\n@media (max-width:600px){\n .phone-only{display:block}\n /* Method table: one card per parameter instead of a sideways scroll. */\n table{min-width:0}\n thead{display:none}\n table,tbody,tr,td{display:block}\n tr{padding:10px 12px;border-bottom:1px solid var(--line)}\n tr:last-child{border-bottom:0}\n td{border:0;padding:2px 0;white-space:normal}\n td[data-label]:not([data-label="Parameter"])::before{content:attr(data-label) " ";font-family:var(--mono);font-size:12px;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);margin-right:8px}\n td.num{white-space:normal}\n}\n.card{max-width:100%;height:auto;border-radius:10px;border:1px solid var(--line);display:block}\n.sharetext{font-size:14px;background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:12px;white-space:pre-wrap;margin:0}\n.status{font-size:14px;color:var(--muted);min-height:1.4em}\nfooter{color:var(--muted);font-size:13px;border-top:1px solid var(--line);padding-top:16px;display:flex;flex-direction:column;gap:6px}\n@media (prefers-reduced-motion:no-preference){.rank .bar span{transition:width .3s ease}}\n</style>\n</head>\n<body>\n<main class="wrap">\n <a class="back" id="back-link" href="#" hidden>\u2190 Chase Holroyd\'s portfolio</a>\n <header class="top">\n <div class="brand">cc<span>watts</span></div>\n <span class="pill" id="source-pill">Loading</span>\n </header>\n\n <div class="intro">\n <h1>How much electricity does your AI coding use?</h1>\n <p class="lede" id="lede">ccwatts estimates the electricity behind Claude Code, an AI coding agent, from the token counts in its local logs. You\'re looking at sample data; try your own logs below.</p>\n </div>\n\n <div class="privacy" id="privacy"><span aria-hidden="true">\u{1F512}</span><div><b>Your logs never leave this browser.</b> This page makes no network requests. It reads token counts only, never your prompts or code.</div></div>\n\n <section class="hero" aria-live="polite">\n <span class="eyebrow" id="hero-eyebrow">Claude Code electricity</span>\n <div class="big" id="hero-big">\u2013</div>\n <div class="range" id="hero-range"></div>\n <div class="compare" id="hero-compare"></div>\n <div class="meta" id="hero-meta"></div>\n <div class="controls" role="group" aria-label="Time window">\n <button type="button" data-window="7d" id="w-7d">7 days</button>\n <button type="button" data-window="30d" id="w-30d">30 days</button>\n <button type="button" data-window="all" id="w-all">All time</button>\n </div>\n </section>\n\n <section class="drop" id="drop" hidden>\n <h2>Try it on your own logs</h2>\n <p>Claude Code saves logs in <code>~/.claude/projects</code>. Pick that folder, or drop any <code>.jsonl</code> files here. Files are read by this page on your device and never uploaded.</p>\n <div class="controls">\n <input type="file" class="vh" id="pick-folder" webkitdirectory multiple>\n <label class="btn primary" for="pick-folder">Choose folder</label>\n <input type="file" class="vh" id="pick-files" accept=".jsonl" multiple>\n <label class="btn" for="pick-files">Choose files</label>\n <button type="button" id="use-sample">Use sample data</button>\n </div>\n <p class="phone-only" id="phone-hint">On a phone? Claude Code keeps its logs on your computer, so open this page there to try your own.</p>\n <div class="status" id="drop-status"></div>\n </section>\n\n <section>\n <h2>Over time</h2>\n <div class="legend" id="legend"></div>\n <svg class="chart" id="chart" viewBox="0 0 720 240" role="img" aria-label="Daily electricity by token type"></svg>\n <p class="meta" id="chart-note"></p>\n </section>\n\n <div class="grid2">\n <section><h2>By model</h2><ul class="rank" id="by-model"></ul></section>\n <section><h2>By project</h2><ul class="rank" id="by-project"></ul></section>\n </div>\n\n <section>\n <h2>What drives your number</h2>\n <p class="explain" id="explain"></p>\n </section>\n\n <section>\n <h2>Share it</h2>\n <img class="card" id="card" alt="Electricity receipt card" width="1200" height="627">\n <div class="controls">\n <button type="button" class="primary" id="save-png">Save PNG for LinkedIn</button>\n <button type="button" id="save-svg">Save SVG</button>\n <button type="button" id="copy-line">Copy share line</button>\n <button type="button" id="copy-post">Copy LinkedIn post</button>\n </div>\n <div class="status" id="share-status"></div>\n <pre class="sharetext" id="post"></pre>\n </section>\n\n <section>\n <h2>How this is estimated</h2>\n <p class="explain" id="method-intro"></p>\n <div class="tablewrap"><table id="params"></table></div>\n </section>\n\n <footer>\n <div id="footer-version"></div>\n <div>Open source. Read the code and the full methodology on <a id="repo-link" href="#" rel="noopener noreferrer">GitHub</a>.</div>\n <div>Built by <a id="built-by" href="#" rel="noopener noreferrer">Chase Holroyd</a>.</div>\n </footer>\n</main>\n<script id="ccwatts-data" type="application/json">{"mode":"playground","generatedAt":null,"rows":[]}</script>\n<script id="ccwatts-sample" type="application/json">[{"date":"2026-09-21","source":"claude-code","model":"claude-opus-4-6","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":47,"tokens":{"input":481,"cacheWrite":224858,"cacheRead":5433921,"output":21302},"outputContext":2453872321},{"date":"2026-09-21","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":17,"tokens":{"input":190,"cacheWrite":86694,"cacheRead":873313,"output":7714},"outputContext":425266546},{"date":"2026-09-22","source":"claude-code","model":"claude-sonnet-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":40,"tokens":{"input":393,"cacheWrite":206396,"cacheRead":4259456,"output":20119},"outputContext":2244470466},{"date":"2026-09-23","source":"claude-code","model":"claude-opus-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":52,"tokens":{"input":503,"cacheWrite":212076,"cacheRead":5914935,"output":28587},"outputContext":3409886608},{"date":"2026-09-24","source":"claude-code","model":"claude-haiku-4-5","projectId":"367b937ac815b163","projectName":"ccwatts","responses":21,"tokens":{"input":190,"cacheWrite":94458,"cacheRead":1218020,"output":10292},"outputContext":590700102},{"date":"2026-09-24","source":"claude-code","model":"claude-opus-4-6","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":95,"tokens":{"input":977,"cacheWrite":439114,"cacheRead":11248882,"output":44237},"outputContext":5490113479},{"date":"2026-09-25","source":"claude-code","model":"claude-opus-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":19,"tokens":{"input":209,"cacheWrite":93535,"cacheRead":1038543,"output":9584},"outputContext":589452635},{"date":"2026-09-27","source":"claude-code","model":"claude-sonnet-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":64,"tokens":{"input":655,"cacheWrite":253381,"cacheRead":7300740,"output":31952},"outputContext":3769121520},{"date":"2026-09-28","source":"claude-code","model":"claude-opus-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":28,"tokens":{"input":276,"cacheWrite":140541,"cacheRead":2230744,"output":13360},"outputContext":1153747881},{"date":"2026-09-29","source":"claude-code","model":"claude-haiku-4-5","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":38,"tokens":{"input":356,"cacheWrite":152315,"cacheRead":3272451,"output":18960},"outputContext":1641654756},{"date":"2026-09-29","source":"claude-code","model":"claude-haiku-4-5","projectId":"367b937ac815b163","projectName":"ccwatts","responses":32,"tokens":{"input":340,"cacheWrite":134170,"cacheRead":2682489,"output":16997},"outputContext":1568755458},{"date":"2026-09-30","source":"claude-code","model":"claude-sonnet-4-6","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":31,"tokens":{"input":353,"cacheWrite":135035,"cacheRead":2371226,"output":16712},"outputContext":1324962703},{"date":"2026-10-01","source":"claude-code","model":"claude-opus-4-6","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":16,"tokens":{"input":138,"cacheWrite":80233,"cacheRead":768856,"output":6943},"outputContext":382418077},{"date":"2026-10-01","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":24,"tokens":{"input":303,"cacheWrite":108412,"cacheRead":1401918,"output":11697},"outputContext":695089914},{"date":"2026-10-02","source":"claude-code","model":"claude-opus-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":50,"tokens":{"input":467,"cacheWrite":243377,"cacheRead":6114791,"output":24218},"outputContext":3052033764},{"date":"2026-10-02","source":"claude-code","model":"claude-sonnet-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":31,"tokens":{"input":291,"cacheWrite":156640,"cacheRead":2602032,"output":14670},"outputContext":1290375171},{"date":"2026-10-03","source":"claude-code","model":"claude-sonnet-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":67,"tokens":{"input":677,"cacheWrite":296672,"cacheRead":8900888,"output":33220},"outputContext":4724382924},{"date":"2026-10-05","source":"claude-code","model":"claude-opus-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":61,"tokens":{"input":601,"cacheWrite":250048,"cacheRead":7595553,"output":29379},"outputContext":3577266281},{"date":"2026-10-05","source":"claude-code","model":"claude-opus-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":32,"tokens":{"input":349,"cacheWrite":148766,"cacheRead":2857812,"output":18526},"outputContext":1716421621},{"date":"2026-10-06","source":"claude-code","model":"claude-haiku-4-5","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":42,"tokens":{"input":371,"cacheWrite":175342,"cacheRead":4217525,"output":20010},"outputContext":2086579461},{"date":"2026-10-07","source":"claude-code","model":"claude-haiku-4-5","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":53,"tokens":{"input":519,"cacheWrite":232336,"cacheRead":6626491,"output":26226},"outputContext":3535583255},{"date":"2026-10-07","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":27,"tokens":{"input":299,"cacheWrite":126737,"cacheRead":2032642,"output":15950},"outputContext":1332905565},{"date":"2026-10-08","source":"claude-code","model":"claude-opus-4-6","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":72,"tokens":{"input":805,"cacheWrite":314745,"cacheRead":9938479,"output":36755},"outputContext":5269927123},{"date":"2026-10-08","source":"claude-code","model":"claude-opus-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":71,"tokens":{"input":823,"cacheWrite":315410,"cacheRead":9639360,"output":33996},"outputContext":4708628928},{"date":"2026-10-08","source":"claude-code","model":"claude-sonnet-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":29,"tokens":{"input":288,"cacheWrite":144763,"cacheRead":2133553,"output":18135},"outputContext":1399436901},{"date":"2026-10-09","source":"claude-code","model":"claude-opus-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":32,"tokens":{"input":316,"cacheWrite":129817,"cacheRead":2427569,"output":15515},"outputContext":1186325442},{"date":"2026-10-09","source":"claude-code","model":"claude-opus-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":15,"tokens":{"input":122,"cacheWrite":58466,"cacheRead":581654,"output":7239},"outputContext":307424407},{"date":"2026-10-09","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":52,"tokens":{"input":438,"cacheWrite":243114,"cacheRead":6293294,"output":26306},"outputContext":3344828844},{"date":"2026-10-10","source":"claude-code","model":"claude-opus-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":66,"tokens":{"input":673,"cacheWrite":273538,"cacheRead":8614195,"output":31043},"outputContext":4430386287},{"date":"2026-10-11","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":71,"tokens":{"input":885,"cacheWrite":325807,"cacheRead":9676339,"output":33624},"outputContext":4753439717},{"date":"2026-10-12","source":"claude-code","model":"claude-opus-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":16,"tokens":{"input":186,"cacheWrite":90712,"cacheRead":869169,"output":8201},"outputContext":521288313},{"date":"2026-10-12","source":"claude-code","model":"claude-opus-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":46,"tokens":{"input":504,"cacheWrite":208473,"cacheRead":4885505,"output":22366},"outputContext":2523675175},{"date":"2026-10-13","source":"claude-code","model":"claude-opus-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":20,"tokens":{"input":244,"cacheWrite":111650,"cacheRead":1255969,"output":9499},"outputContext":611397728},{"date":"2026-10-13","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":25,"tokens":{"input":221,"cacheWrite":111841,"cacheRead":1586210,"output":11735},"outputContext":798440653},{"date":"2026-10-14","source":"claude-code","model":"claude-opus-4-6","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":43,"tokens":{"input":530,"cacheWrite":183803,"cacheRead":4389683,"output":20586},"outputContext":2203768836},{"date":"2026-10-14","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":59,"tokens":{"input":674,"cacheWrite":217090,"cacheRead":6575361,"output":28579},"outputContext":3253006122},{"date":"2026-10-15","source":"claude-code","model":"claude-haiku-4-5","projectId":"367b937ac815b163","projectName":"ccwatts","responses":74,"tokens":{"input":825,"cacheWrite":309110,"cacheRead":10212638,"output":38706},"outputContext":5535358176},{"date":"2026-10-15","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":20,"tokens":{"input":265,"cacheWrite":109170,"cacheRead":1296443,"output":8112},"outputContext":599370597},{"date":"2026-10-15","source":"claude-code","model":"claude-sonnet-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":31,"tokens":{"input":253,"cacheWrite":156477,"cacheRead":2747706,"output":13093},"outputContext":1311357449},{"date":"2026-10-16","source":"claude-code","model":"claude-haiku-4-5","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":28,"tokens":{"input":336,"cacheWrite":130298,"cacheRead":1943815,"output":14701},"outputContext":1081930686},{"date":"2026-10-16","source":"claude-code","model":"claude-opus-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":48,"tokens":{"input":494,"cacheWrite":215976,"cacheRead":5622539,"output":24681},"outputContext":3062604147},{"date":"2026-10-17","source":"claude-code","model":"claude-opus-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":56,"tokens":{"input":530,"cacheWrite":237670,"cacheRead":6869143,"output":25467},"outputContext":3159646087},{"date":"2026-10-18","source":"claude-code","model":"claude-haiku-4-5","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":38,"tokens":{"input":416,"cacheWrite":162013,"cacheRead":3360821,"output":20262},"outputContext":1907957687},{"date":"2026-10-19","source":"claude-code","model":"claude-haiku-4-5","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":97,"tokens":{"input":1108,"cacheWrite":414100,"cacheRead":10652494,"output":48446},"outputContext":5589764863},{"date":"2026-10-19","source":"claude-code","model":"claude-sonnet-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":65,"tokens":{"input":708,"cacheWrite":292947,"cacheRead":8732693,"output":35185},"outputContext":4755010745},{"date":"2026-10-20","source":"claude-code","model":"claude-haiku-4-5","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":66,"tokens":{"input":726,"cacheWrite":297357,"cacheRead":8882173,"output":32673},"outputContext":4543520242},{"date":"2026-10-20","source":"claude-code","model":"claude-sonnet-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":128,"tokens":{"input":1264,"cacheWrite":557853,"cacheRead":16226037,"output":59579},"outputContext":7838321066}]</script>\n<script>"use strict";(()=>{var R="ccwatts",ne="0.1.0",A="https://github.com/holroydchase-afk/ccwatts",q="https://chaseholroyd.com/ccwatts/",K="https://chaseholroyd.com/ccwatts.html";var W={input:"input",cacheWrite:"cache write",cacheRead:"cache read",output:"output"},P=e=>Math.round(e).toLocaleString("en-US");function M(e){return e>=100?P(e):e>=1?(Math.round(e*10)/10).toString():e>=.01?(Math.round(e*100)/100).toString():e===0?"0":e.toPrecision(1)}function L(e){let t=e.high>=1e3,n=t?1e3:1,o=t?"kWh":"Wh";return{central:`${M(e.central/n)} ${o}`,range:`${M(e.low/n)} \\u2013 ${M(e.high/n)}`,unit:o}}var O=e=>e.replace(/^claude-/,"");var _=["input","cacheWrite","cacheRead","output"];var V=()=>({low:0,central:0,high:0});function oe(e,t){return e.low+=t.low,e.central+=t.central,e.high+=t.high,e}function $e(e,t){let n=e.toLowerCase();for(let[o,s]of Object.entries(t.tiers))if(Array.isArray(s)&&s.some(r=>n.includes(r)))return{tier:o,known:!0};return{tier:String(t.tiers.fallback),known:!1}}function Oe(e,t){let n=e.params[t];if(!n)throw new Error(`Missing parameter "${t}" in params.json`);return n}function Ne(e,t){let n=`output_wh_per_1k.${e}`;switch(t){case"output":return[n];case"input":return[n,"input_vs_output"];case"cacheWrite":return[n,"input_vs_output","cache_write_vs_input"];case"cacheRead":return[n,"input_vs_output","cache_read_vs_input"]}}function He(e,t,n){let o=.001,s=0,r=0;for(let a of Ne(t,n)){let c=Oe(e,a);o*=c.value,s+=Math.log(c.high/c.value)**2,r+=Math.log(c.value/c.low)**2}return{whPerToken:o,up:Math.sqrt(s),down:Math.sqrt(r)}}var ke=()=>({input:0,cacheWrite:0,cacheRead:0,output:0});var xe=new WeakMap;function Be(e,t,n){let o=xe.get(e);o||xe.set(e,o=new Map);let s=`${t}:${n}`,r=o.get(s);return r||o.set(s,r=He(e,t,n)),r}function be(e,t,n,o){if(e<=0)return V();let s=Be(o,t,n),r=e*s.whPerToken;return{low:r/Math.exp(s.down),central:r,high:r*Math.exp(s.up)}}var Fe=e=>e.slice(0,10);function G(e,t){let n=V(),o=ke(),s={model:new Map,type:new Map,day:new Map,project:new Map},r=new Set,a=null,c=null,k=(l,h,m,p,u)=>{let g=s[l].get(h);g||s[l].set(h,g={key:h,energy:V(),tokens:ke()}),oe(g.energy,m),g.tokens[p]+=u};for(let l of e){let{tier:h,known:m}=$e(l.model,t);m||r.add(l.model);let p=Fe(l.timestamp);(a===null||p<a)&&(a=p),(c===null||p>c)&&(c=p);let u=l.project?.name??"(unknown)";for(let g of _){let y=l.tokens[g];if(y<=0)continue;let T=be(y,h,g,t);oe(n,T),o[g]+=y,k("model",l.model,T,g,y),k("type",g,T,g,y),k("day",p,T,g,y),k("project",l.project?`${u}#${l.project.id}`:u,T,g,y)}}let x=l=>[...l.values()].sort((h,m)=>m.energy.central-h.energy.central),j=[...r].map(l=>`Unknown model "${l}": estimated as ${String(t.tiers.fallback)}.`);return{methodVersion:t.version,total:n,tokens:o,responses:e.length,byModel:x(s.model),byType:x(s.type),byDay:[...s.day.values()].sort((l,h)=>l.key.localeCompare(h.key)),byProject:x(s.project),span:a&&c?{from:a,to:c}:null,warnings:j}}var re=e=>e.split("#")[0]??e;function se(e,t){let n=t.comparisons,o=[],s=r=>r>=10?Math.round(r):Math.round(r*10)/10;if(n.ev_kwh_per_mile){let r=e/n.ev_kwh_per_mile.value;o.push({id:"ev",amount:r,text:`driving an EV about ${s(r)} miles`})}if(n.fridge_kwh_per_day){let r=e/n.fridge_kwh_per_day.value;o.push({id:"fridge",amount:r,text:`running a fridge about ${s(r)} days`})}if(n.phone_charge_kwh){let r=e/n.phone_charge_kwh.value;o.push({id:"phone",amount:r,text:`charging a phone about ${s(r)} times`})}if(n.us_home_kwh_per_day){let r=e/n.us_home_kwh_per_day.value;o.push({id:"home",amount:r,text:`powering a US home about ${s(r)} days`})}return o}function U(e,t){let n=se(e,t),o=n.filter(r=>r.amount>=1).slice(0,2),s=[...n].sort((r,a)=>a.amount-r.amount).slice(0,1);return(o.length?o:s).map(r=>r.text).join(" \\xB7 ")}var Re=A.replace(/^https:\\/\\//,"");function Te(e){return e==="all time"?"All my Claude Code use so far":e.startsWith("since")?`My Claude Code use ${e}`:`My ${e} of Claude Code`}function ve(e){return e==="all time"?"(Sample data) All of a sample\'s Claude Code use":`(Sample data) ${e[0].toUpperCase()}${e.slice(1)} of Claude Code`}function J({estimate:e,params:t,windowLabel:n,sample:o}){let s=L(e.total),r=U(e.total.central/1e3,t).split(" \\xB7 ")[0];return`${o?ve(n):Te(n)} used about ${s.central} of electricity (range ${s.range} ${s.unit}), roughly ${r}. Estimated locally with ${R}, method v${t.version}: ${Re}`}function ae(e){let{estimate:t,params:n,windowLabel:o,sample:s}=e,r=L(t.total),a=t.total.central>0?Math.round((t.byType.find(c=>c.key==="cacheRead")?.energy.central??0)/t.total.central*100):0;return s?[`(Sample data) This is what ${R} shows for a sample developer, not a real measurement. ${ve(o).replace("(Sample data) ","")}: about ${r.central} (range ${r.range} ${r.unit}), roughly ${U(t.total.central/1e3,n)}.`,"",`${a}% of it came from the agent re-reading context it had already seen.`,"","It\'s a range because nobody outside the AI labs knows the real energy per token. Every assumption is sourced, and it runs entirely on your own machine, so no logs leave it.","",`Try it on your own logs: ${q}`,`Code and method: ${A}`].join(`\n`):[`I estimated the electricity behind my AI coding. ${Te(o)}: about ${r.central} (range ${r.range} ${r.unit}), roughly ${U(t.total.central/1e3,n)}.`,"",`${a>=50?"The surprise: ":""}${a}% of it came from the agent re-reading context it had already seen.`,"","It\'s a range because nobody outside the AI labs knows the real energy per token. Every assumption is sourced, and the tool runs entirely on my laptop, so no logs leave it.","",`Try it in your browser: ${q}`,`Code and method: ${A}`].join(`\n`)}var E=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function N(e,t,n){let o=Math.max(2,n-e.length-t.length-2);return`${e} ${".".repeat(o)} ${t}`}function ie({estimate:e,params:t,windowLabel:n,sample:o}){let a=L(e.total),c=e.total.central/1e3,k=U(c,t).split(" \\xB7 "),x=a.unit,j=x==="kWh"?1e3:1,l=712,h=432,m=36,p=[],u="-".repeat(m);p.push({text:"ELECTRICITY RECEIPT",weight:"bold"}),p.push({text:e.span?`${e.span.from} \\u2192 ${e.span.to}`:n,muted:!0}),p.push({text:u,muted:!0});let g=[..._].sort((d,w)=>(e.byType.find(v=>v.key===w)?.energy.central??0)-(e.byType.find(v=>v.key===d)?.energy.central??0));for(let d of g){let w=e.byType.find(v=>v.key===d);p.push({text:N(W[d],`${M((w?.energy.central??0)/j)} ${x}`,m)})}p.push({text:u,muted:!0}),p.push({text:N("TOTAL (central)",a.central,m),weight:"bold"}),p.push({text:N("RANGE",`${a.range} ${x}`,m)}),p.push({text:u,muted:!0});for(let d of e.byModel.slice(0,3)){let w=e.total.central>0?Math.round(d.energy.central/e.total.central*100):0;p.push({text:N(O(d.key),`${w}%`,m),muted:!0})}p.push({text:N("responses",P(e.responses),m),muted:!0}),p.push({text:`method v${t.version} \\xB7 counts only`,muted:!0});let y=25,T=46,B=T+44,I=B+p.length*y+6,S=I+70,D=e.byDay.slice(-30),F=Math.max(...D.map(d=>d.energy.central),1e-9),ge=(h-64)/Math.max(D.length,1),Ie=D.map((d,w)=>{let v=6+d.energy.central/F*40;return`<rect x="${(l+32+w*ge).toFixed(1)}" y="${(I+46-v).toFixed(1)}" width="${Math.max(1,ge-2).toFixed(1)}" height="${v.toFixed(1)}" fill="#1f2a24"/>`}).join(""),fe=[];for(let d=0,w=l+h;w>l;d++,w-=9)fe.push(`${w.toFixed(1)},${S+(d%2?10:0)}`);let We=`M${l},${T} H${l+h} V${S} L${fe.join(" L")} L${l},${S} Z`,Y="ui-monospace, \'SF Mono\', Menlo, Consolas, \'Liberation Mono\', monospace",z="\'Helvetica Neue\', Helvetica, Arial, sans-serif",De=p.map((d,w)=>{let v=B+w*y,we=w<2||d.text.startsWith("method");return`<text x="${we?l+h/2:l+30}" y="${v}" font-family="${Y}" font-size="16.5" ${we?\'text-anchor="middle"\':""} font-weight="${d.weight??"normal"}" fill="${d.muted?"#5e6b63":"#1f2a24"}">${E(d.text)}</text>`}).join(`\n `),ye=a.central.split(" ");return`<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="627" viewBox="0 0 1200 627" role="img" aria-label="${E(J({estimate:e,params:t,windowLabel:n,sample:o}))}">\n <rect width="1200" height="627" fill="#12251c"/>\n <rect x="0" y="621" width="1200" height="6" fill="#7cc4a0"/>\n <text x="64" y="104" font-family="${Y}" font-size="20" letter-spacing="2" fill="#7cc4a0">${o?\'<tspan fill="#e0b56a">SAMPLE DATA \\xB7 </tspan>\':""}CLAUDE CODE \\xB7 ${E(n.toUpperCase())}</text>\n <text x="60" y="262" font-family="${z}" font-size="148" font-weight="700" fill="#f4f1e8">${E(ye[0]??"")}<tspan font-size="64" font-weight="500" dx="14">${E(ye[1]??"")}</tspan></text>\n <text x="64" y="318" font-family="${z}" font-size="28" fill="#cfe3d6">of electricity, range ${E(a.range)} ${x}</text>\n ${k.map((d,w)=>`<text x="64" y="${384+w*38}" font-family="${z}" font-size="26" fill="#f4f1e8">\\u2248 ${E(d)}</text>`).join(`\n `)}\n <text x="64" y="520" font-family="${z}" font-size="20" fill="${o?"#e0b56a":"#9fb8a8"}">${o?"Sample data, not a real measurement.":"Estimated on my laptop. No logs left it."}</text>\n <text x="64" y="552" font-family="${Y}" font-size="17" fill="#7cc4a0">${E(R)} \\xB7 method v${E(t.version)}</text>\n <text x="64" y="578" font-family="${Y}" font-size="17" fill="#7cc4a0">${E(Re)}</text>\n <path d="${We}" fill="#f7f5ef"/>\n <g>\n ${De}\n </g>\n ${Ie}\n</svg>\n`}function Ye(e){let t=0xcbf29ce484222325n,n=0x100000001b3n;for(let o=0;o<e.length;o++)t^=BigInt(e.charCodeAt(o)),t=t*n&0xffffffffffffffffn;return t.toString(16).padStart(16,"0")}function ce(e){let t=e.replace(/[\\\\/]+$/,""),n=t.split(/[\\\\/]/).pop()||t;return{id:Ye(t),name:n}}var Z=e=>typeof e=="number"&&Number.isFinite(e)&&e>0?e:0,H=e=>typeof e=="string"&&e!==""?e:null;function ze(e){if(e.type!=="assistant")return null;let t=e.message,n=t?.usage,o=H(t?.model),s=H(e.timestamp),r=H(t?.id);if(!n||!o||!s||!r||o==="<synthetic>")return null;let a=H(e.cwd);return{source:"claude-code",key:`${r}:${H(e.requestId)??""}`,timestamp:s,model:o,project:a?ce(a):null,tokens:{input:Z(n.input_tokens),cacheWrite:Z(n.cache_creation_input_tokens),cacheRead:Z(n.cache_read_input_tokens),output:Z(n.output_tokens)}}}var le={id:"claude-code",parse(e){let t=new Map;for(let n of e.split(`\n`)){if(!n.includes(\'"assistant"\'))continue;let o;try{o=JSON.parse(n)}catch{continue}let s=ze(o);s&&t.set(s.key,s)}return[...t.values()]}};function ue(e){let t=new Map;for(let n of e)t.set(`${n.source}:${n.key}`,n);return[...t.values()]}var _e={version:"0.1.0",as_of:"2026-10-21",license:"CC BY 4.0",boundary:"comprehensive: accelerators, host CPU and memory, idle capacity, and data-center overhead (PUE)",interval:"low and high are each parameter\'s plausible bounds, treated as a ~90% interval",params:{"output_wh_per_1k.opus":{value:1.2,low:.4,high:3,unit:"Wh per 1k output tokens",label:"MODELED",source:"https://www.theclimatebrink.com/p/the-real-energy-use-of-agentic-ai",as_of:"2026-10-21",note:"Largest tier. Scaled up from the Sonnet value; Anthropic does not disclose model size or energy. Bounds cover published per-token estimates for frontier models."},"output_wh_per_1k.sonnet":{value:.6,low:.25,high:1.5,unit:"Wh per 1k output tokens",label:"MODELED",source:"https://arxiv.org/abs/2508.15734",as_of:"2026-10-21",note:"Anchored so a typical chat query (about 1k input, 400 output tokens) lands near Google\'s measured 0.24 Wh median Gemini prompt and the 0.34 Wh median frontier query (IQR 0.18-0.67). Third-party estimates for Claude output run 0.4-2 Wh per 1k."},"output_wh_per_1k.haiku":{value:.25,low:.08,high:.6,unit:"Wh per 1k output tokens",label:"GUESS",source:"https://www.anthropic.com/pricing",as_of:"2026-10-21",note:"Smallest tier. Scaled down from Sonnet roughly in line with the price gap between tiers, which is a weak proxy for compute."},input_vs_output:{value:.3,low:.15,high:.5,unit:"ratio",label:"MODELED",source:"https://www.anthropic.com/pricing",as_of:"2026-10-21",note:"Energy of one fresh input token relative to one output token. Input is processed in parallel (prefill), so it costs less per token. Anthropic prices input at 1/5 of output; a Watershed-based estimate uses about 1/3."},cache_write_vs_input:{value:1,low:.8,high:1.25,unit:"ratio",label:"MODELED",source:"https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching",as_of:"2026-10-21",note:"Writing to the cache means processing the tokens fresh, then storing the result. Priced at 1.25x input."},cache_read_vs_input:{value:.1,low:.01,high:.25,unit:"ratio",label:"MODELED",source:"https://www.theclimatebrink.com/p/the-real-energy-use-of-agentic-ai",as_of:"2026-10-21",note:"The parameter that matters most for agents, where cache reads are 90%+ of tokens. Hausfather\'s central case is 10% of fresh input, tested at 1% and 25%. Anthropic prices cache reads at 0.1x input."}},tiers:{opus:["opus"],sonnet:["sonnet"],haiku:["haiku"],fallback:"sonnet"},comparisons:{ev_kwh_per_mile:{value:.3,unit:"kWh per mile",label:"MEASURED",source:"https://www.fueleconomy.gov/feg/evsbs.shtml",as_of:"2026-10-21",note:"Typical EV uses 25-35 kWh per 100 miles (EPA)."},fridge_kwh_per_day:{value:1.5,unit:"kWh per day",label:"MEASURED",source:"https://www.energystar.gov/products/refrigerators",as_of:"2026-10-21",note:"A typical US refrigerator uses about 400-600 kWh a year."},us_home_kwh_per_day:{value:29,unit:"kWh per day",label:"MEASURED",source:"https://www.eia.gov/tools/faqs/faq.php?id=97&t=3",as_of:"2026-10-21",note:"Average US household used about 10,800 kWh in a year (EIA)."},phone_charge_kwh:{value:.019,unit:"kWh per smartphone charge",label:"MEASURED",source:"https://www.epa.gov/energy/greenhouse-gas-equivalencies-calculator-calculations-and-references",as_of:"2026-10-21",note:"EPA\'s figure for fully charging one smartphone."}}};var $=_e;var Q=e=>`${e.date}|${e.source}|${e.model}|${e.projectId??""}`;var Ee=e=>e.output*(e.input+e.cacheWrite+e.cacheRead);function pe(e){let t=new Map;for(let n of e){let o={date:n.timestamp.slice(0,10),source:n.source,model:n.model,projectId:n.project?.id??null,projectName:n.project?.name??null},s=Q(o),r=t.get(s);r||t.set(s,r={...o,responses:0,tokens:{input:0,cacheWrite:0,cacheRead:0,output:0},outputContext:0}),r.responses+=1;for(let a of _)r.tokens[a]+=n.tokens[a];r.outputContext+=Ee(n.tokens)}return Ke([...t.values()])}function Ke(e){return e.sort((t,n)=>Q(t).localeCompare(Q(n)))}function X(e){return e.map(t=>({source:t.source,key:Q(t),timestamp:`${t.date}T00:00:00.000Z`,model:t.model,project:t.projectId?{id:t.projectId,name:t.projectName??"(unknown)"}:null,tokens:{...t.tokens}}))}var i=e=>document.getElementById(e),b=e=>e.replace(/[&<>"\']/g,t=>({"&":"&","<":"<",">":">",\'"\':""","\'":"'"})[t]);function Pe(e,t){try{return JSON.parse(i(e).textContent??"")}catch{return t}}var C=Pe("ccwatts-data",{mode:"playground",generatedAt:null,rows:[]}),Se=Pe("ccwatts-sample",[]),f={rows:C.mode==="dashboard"&&C.rows.length?C.rows:Se,source:C.mode==="dashboard"&&C.rows.length?"history":"sample",window:"30d"},Ve={"7d":"last 7 days","30d":"last 30 days",all:"all time"};function Ge(e,t){if(t==="all"||e.length===0)return e;let n=t==="7d"?7:30,o=e.reduce((a,c)=>c.date>a?c.date:a,"0000-00-00"),s=f.source==="history"?Date.now():Date.parse(`${o}T00:00:00Z`)+864e5,r=new Date(s-n*864e5).toISOString().slice(0,10);return e.filter(a=>a.date>=r)}function Je(e){let t=G(X(e),$);return t.responses=e.reduce((n,o)=>n+o.responses,0),t}function Ze(e,t){let n=L(e.total),[o,s]=n.central.split(" ");i("hero-eyebrow").textContent=`Claude Code electricity \\xB7 ${t}`,i("hero-big").innerHTML=e.responses?`${b(o??"")}<small>${b(s??"")}</small>`:"\\u2013",i("hero-range").textContent=e.responses?`Plausible range ${n.range} ${n.unit}, from the bounds on every assumption.`:"No usage in this window.",i("hero-compare").textContent=e.responses?`That\'s like ${U(e.total.central/1e3,$).replace(" \\xB7 ",", or ")}.`:"";let r=e.span?` \\xB7 ${e.span.from} to ${e.span.to}`:"",a=_.reduce((c,k)=>c+e.tokens[k],0);i("hero-meta").textContent=`${P(e.responses)} responses \\xB7 ${P(a)} tokens${r} \\xB7 method v${$.version}`;for(let c of["7d","30d","all"])i(`w-${c}`).setAttribute("aria-pressed",String(f.window===c))}function Ae(e){let t=i("chart"),n=Math.round(Math.min(720,Math.max(320,t.getBoundingClientRect().width||720))),o=240;t.setAttribute("viewBox",`0 0 ${n} ${o}`);let s={l:44,r:8,t:22,b:28},r=e.byDay;if(i("legend").innerHTML=["cacheRead","cacheWrite","output","input"].map(u=>`<span><i style="background:var(--t-${u})"></i>${W[u]}</span>`).join(""),!r.length){t.innerHTML=`<text x="${n/2}" y="${o/2}" text-anchor="middle">No usage in this window</text>`,i("chart-note").textContent="";return}let a=L(e.total),c=a.unit==="kWh"?1e3:1,k=Math.max(...r.map(u=>u.energy.central))/c,x=et(k/3),j=Math.max(x*Math.ceil(k/x),x),l=u=>s.t+(o-s.t-s.b)*(1-u/j),h=(n-s.l-s.r)/r.length,m=[];for(let u=0;u<=j+1e-9;u+=x)m.push(`<line x1="${s.l}" x2="${n-s.r}" y1="${l(u)}" y2="${l(u)}" stroke="var(--line)" stroke-width="1"/>`),m.push(`<text x="${s.l-6}" y="${l(u)+4}" text-anchor="end">${M(u)}</text>`);m.push(`<text x="0" y="10">${a.unit} per day</text>`),r.forEach((u,g)=>{let y=0,T=s.l+g*h+h*.12,B=Math.max(1,h*.76);for(let I of["cacheRead","cacheWrite","output","input"]){let S=Qe(u,I);if(S<=0)continue;let D=y/c;y+=S;let F=y/c;m.push(`<rect x="${T.toFixed(1)}" y="${l(F).toFixed(1)}" width="${B.toFixed(1)}" height="${Math.max(.5,l(D)-l(F)).toFixed(1)}" fill="var(--t-${I})"><title>${u.key} ${W[I]} ${M(S/c)} ${a.unit}</title></rect>`)}});let p=r.length===1?[0]:[0,Math.floor((r.length-1)/2),r.length-1];for(let u of new Set(p)){let g=u===0?"start":u===r.length-1?"end":"middle",y=u===0?s.l:u===r.length-1?n-s.r:s.l+(u+.5)*h;m.push(`<text x="${y}" y="${o-8}" text-anchor="${g}">${r[u].key.slice(5)}</text>`)}t.innerHTML=m.join(""),i("chart-note").textContent=`Bars show the central estimate per day. The total for this window is ${a.central}, with a range of ${a.range} ${a.unit}.`}function Qe(e,t){return ee.get(e.key)?.[t]??0}var ee=new Map;function Xe(e){ee=new Map;for(let t of e){let n=G(X([t]),$),o=ee.get(t.date);o||ee.set(t.date,o={input:0,cacheWrite:0,cacheRead:0,output:0});for(let s of n.byType)o[s.key]+=s.energy.central}}function et(e){if(e<=0)return 1;let t=10**Math.floor(Math.log10(e)),n=e/t;return(n<=1?1:n<=2?2:n<=5?5:10)*t}function Ce(e,t,n,o){let s=Math.max(...t.map(a=>a.energy.central),1e-9),r=t.slice(0,6);i(e).innerHTML=r.length?r.map(a=>{let c=L(a.energy),k=o.total.central?Math.round(a.energy.central/o.total.central*100):0;return`<li><span class="name" title="${b(n(a.key))}">${b(n(a.key))}</span><span class="val">${b(c.central)} \\xB7 ${k}%</span><span class="bar"><span style="width:${(a.energy.central/s*100).toFixed(1)}%"></span></span><span class="val" style="grid-column:1/-1;text-align:left">range ${b(c.range)} ${b(c.unit)}</span></li>`}).join(""):\'<li><span class="name">No usage in this window</span></li>\'}function tt(e){let t=s=>e.total.central?Math.round((e.byType.find(r=>r.key===s)?.energy.central??0)/e.total.central*100):0,n=_.reduce((s,r)=>s+e.tokens[r],0),o=n?Math.round(e.tokens.cacheRead/n*100):0;i("explain").textContent=e.responses?`Each time Claude Code takes a step, it re-reads the conversation so far. Most of that comes from a cache, which is cheap per token but happens constantly. Here, cache reads were ${o}% of tokens and about ${t("cacheRead")}% of the electricity. Writing new context into the cache was ${t("cacheWrite")}%, and the text the model wrote back was ${t("output")}%. Long sessions grow the context the agent re-reads, so starting fresh sessions for new tasks is the simplest way to lower your number.`:"Load some usage to see what drives it."}var de="",Ue="",he="";function nt(e,t){let n={estimate:e,params:$,windowLabel:t,sample:f.source==="sample"};de=ie(n),Ue=J(n),he=ae(n),i("card").src=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(de)}`,i("post").textContent=he,i("share-status").textContent=f.source==="sample"?"This card shows sample data. Load your own logs to make yours.":""}function ot(e){try{return new URL(e).hostname.replace(/^www\\./,"")}catch{return"source"}}function rt(){i("method-intro").textContent=`Energy = tokens \\xD7 energy per token, by model family and token type. Each parameter below has a low and high bound, and they combine into the range you see. The boundary is ${$.boundary}. Labels say how solid each number is: MODELED is derived from published figures, GUESS is a best estimate with wide bounds. Method v${$.version}, parameters as of ${$.as_of}, licensed ${$.license}.`;let e=Object.entries($.params).map(([t,n])=>`<tr><td data-label="Parameter"><code>${b(t)}</code><div class="meta">${b(n.note)}</div></td><td class="num" data-label="Value">${n.value}</td><td class="num" data-label="Range">${n.low} \\u2013 ${n.high}</td><td data-label="Unit">${b(n.unit)}</td><td data-label="Label"><span class="label">${b(n.label)}</span></td><td data-label="Source"><a class="src" href="${b(n.source)}" rel="noopener noreferrer">${b(ot(n.source))}</a></td></tr>`).join("");i("params").innerHTML=`<thead><tr><th>Parameter</th><th>Value</th><th>Range</th><th>Unit</th><th>Label</th><th>Source</th></tr></thead><tbody>${e}</tbody>`}var st={sample:"ccwatts estimates the electricity behind Claude Code, an AI coding agent, from the token counts in its local logs. You\'re looking at sample data; try your own logs below.",files:"ccwatts estimates the electricity behind Claude Code, an AI coding agent, from the token counts in its local logs. You\'re looking at the files you loaded; nothing was uploaded.",history:"ccwatts estimates the electricity behind your Claude Code use from the token counts in its local logs, as a range with every assumption sourced."},me=null;function te(){let e=Ge(f.rows,f.window);Xe(e);let t=Je(e);me=t;let n=Ve[f.window],o=i("source-pill");o.textContent=f.source==="sample"?"Sample data":f.source==="history"?"Your history":"Your files",o.classList.toggle("sample",f.source==="sample"),i("lede").textContent=st[f.source],Ze(t,n),Ae(t),Ce("by-model",t.byModel,O,t),Ce("by-project",t.byProject,re,t),tt(t),nt(t,n)}async function Me(e){let t=[...e].filter(r=>r.name.endsWith(".jsonl")),n=i("drop-status");if(!t.length){n.textContent="No .jsonl files found there. Claude Code keeps them in ~/.claude/projects.";return}n.textContent=`Reading ${t.length} file${t.length===1?"":"s"} on this device\\u2026`;let o=[];for(let r of t)o.push(...le.parse(await r.text()));let s=pe(ue(o));if(!s.length){n.textContent="Those files had no Claude Code usage in them.";return}f.rows=s,f.source="files",f.window="all",n.textContent=`Read ${P(o.length)} responses from ${t.length} file${t.length===1?"":"s"}. Nothing was uploaded.`,te()}function je(e,t){let n=document.createElement("a");n.href=URL.createObjectURL(t),n.download=e,document.body.append(n),n.click(),n.remove(),setTimeout(()=>URL.revokeObjectURL(n.href),1e3)}async function Le(e,t){let n=i("share-status");try{await navigator.clipboard.writeText(e),n.textContent=`Copied the ${t}.`}catch{let o=document.createRange();o.selectNodeContents(i("post")),getSelection()?.removeAllRanges(),getSelection()?.addRange(o),n.textContent="Couldn\'t reach the clipboard. The post text is selected so you can copy it."}}function at(){let e=new Image;e.onload=()=>{let t=document.createElement("canvas");t.width=1200,t.height=627,t.getContext("2d").drawImage(e,0,0,1200,627),t.toBlob(n=>n&&je(`${R}-receipt.png`,n),"image/png")},e.src=i("card").src}function it(){for(let o of["7d","30d","all"])i(`w-${o}`).addEventListener("click",()=>{f.window=o,te()});if(i("save-png").addEventListener("click",at),i("save-svg").addEventListener("click",()=>je(`${R}-receipt.svg`,new Blob([de],{type:"image/svg+xml"}))),i("copy-line").addEventListener("click",()=>{Le(Ue,"share line")}),i("copy-post").addEventListener("click",()=>{Le(he,"LinkedIn post")}),C.mode==="playground"){let o=i("drop");o.hidden=!1;for(let r of["pick-folder","pick-files"])i(r).addEventListener("change",a=>{let c=a.target.files;c&&Me(c)});i("use-sample").addEventListener("click",()=>{f.rows=Se,f.source="sample",f.window="30d",i("drop-status").textContent="",te()});let s=r=>a=>{a.preventDefault(),o.classList.toggle("over",r)};o.addEventListener("dragover",s(!0)),o.addEventListener("dragleave",s(!1)),o.addEventListener("drop",r=>{s(!1)(r),r.dataTransfer?.files&&Me(r.dataTransfer.files)})}let e=0,t=i("chart").getBoundingClientRect().width;if(window.addEventListener("resize",()=>{clearTimeout(e),e=window.setTimeout(()=>{let o=i("chart").getBoundingClientRect().width;me&&Math.round(o)!==Math.round(t)&&Ae(me),t=o},150)}),i("repo-link").href=A,i("built-by").href=K,C.mode==="playground"){let o=i("back-link");o.href=K,o.hidden=!1}let n=C.generatedAt?` \\xB7 written ${C.generatedAt.slice(0,16).replace("T"," ")} UTC`:"";i("footer-version").textContent=`${R} v${ne} \\xB7 method v${$.version}${n}. Electricity only, not carbon or water.`,document.title=C.mode==="dashboard"?`${R} dashboard`:`${R} playground`}it();rt();te();})();</script>\n</body>\n</html>\n';
|
|
661
|
+
var ccwatts_default = '<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; script-src \'sha256-wA+ap0z0OmyKk8/OnIKkpDoFfHm2rZcmC63kF0+LITE=\'; style-src \'unsafe-inline\'; img-src data: blob:; base-uri \'none\'; form-action \'none\'">\n<meta name="referrer" content="no-referrer">\n<title>ccwatts</title>\n<meta name="description" content="Estimate the electricity your Claude Code usage consumed, as a sourced range. Runs entirely in your browser.">\n<!-- Link preview for LinkedIn and others. The crawler fetches the image; this page never does. -->\n<meta property="og:type" content="website">\n<meta property="og:title" content="How much electricity does your Claude Code use?">\n<meta property="og:description" content="A sourced kWh range from your own logs. Runs in your browser, and your logs never leave it.">\n<meta property="og:url" content="https://chaseholroyd.com/ccwatts/">\n<meta property="og:image" content="https://chaseholroyd.com/ccwatts/og.png">\n<meta property="og:image:width" content="1200">\n<meta property="og:image:height" content="627">\n<meta property="og:image:alt" content="ccwatts: how much electricity does your Claude Code use?">\n<meta name="twitter:card" content="summary_large_image">\n<style>\n/* Layout: one reading column; the headline reads like the top of a utility bill, the share card like a till receipt. */\n:root{\n --bg:#f4f2ea; --surface:#fbfaf6; --ink:#1f2a24; --muted:#5e6b63; --line:#d9ddd3; --accent:#2f6b4f; --accent-ink:#ffffff;\n --t-cacheRead:#2f6b4f; --t-cacheWrite:#c08a2e; --t-output:#b4553a; --t-input:#4a6fa5;\n --sample:#8a5a12; --control:#7f8a83;\n --sans:"Helvetica Neue",Helvetica,Arial,system-ui,sans-serif;\n --mono:ui-monospace,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;\n color-scheme:light;\n}\n@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){\n --bg:#10201a; --surface:#16291f; --ink:#e8eee9; --muted:#9fb3a6; --line:#29402f; --accent:#7cc4a0; --accent-ink:#10201a;\n --t-cacheRead:#7cc4a0; --t-cacheWrite:#e0b56a; --t-output:#e58a6e; --t-input:#8fb2e8; --sample:#e0b56a; --control:#5d7a68; color-scheme:dark}}\n:root[data-theme="dark"]{\n --bg:#10201a; --surface:#16291f; --ink:#e8eee9; --muted:#9fb3a6; --line:#29402f; --accent:#7cc4a0; --accent-ink:#10201a;\n --t-cacheRead:#7cc4a0; --t-cacheWrite:#e0b56a; --t-output:#e58a6e; --t-input:#8fb2e8; --sample:#e0b56a; --control:#5d7a68; color-scheme:dark}\n*{box-sizing:border-box}\n[hidden]{display:none!important}\nbody{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:16px;line-height:1.55}\n.wrap{max-width:960px;margin:0 auto;padding-block:28px 64px;padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px));display:flex;flex-direction:column;gap:40px}\nheader.top{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px}\n.back{font-size:15px;display:inline-flex;align-items:center;min-height:44px;text-decoration:none;color:var(--muted);align-self:flex-start;margin-block:-12px -28px}\n.back:hover{color:var(--accent);text-decoration:underline}\na:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}\n.intro{display:flex;flex-direction:column;gap:8px}\nh1{font-size:clamp(26px,5vw,36px);line-height:1.15;margin:0;letter-spacing:-.01em;text-wrap:balance}\n.lede{margin:0;font-size:17px;color:var(--muted);max-width:60ch}\n.brand{font-family:var(--mono);font-weight:700;font-size:18px;letter-spacing:.02em}\n.brand span{color:var(--accent)}\n.pill{font-family:var(--mono);font-size:12px;letter-spacing:.06em;text-transform:uppercase;border:1px solid var(--line);border-radius:999px;padding:4px 10px;color:var(--muted)}\n.pill.sample{border-color:var(--sample);color:var(--sample)}\n.privacy{display:flex;gap:10px;align-items:flex-start;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:12px 14px;font-size:15px}\n.privacy b{color:var(--accent)}\n.hero{display:flex;flex-direction:column;gap:6px}\n.eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}\n.big{font-weight:700;font-size:clamp(56px,13vw,112px);line-height:1;letter-spacing:-.02em;font-variant-numeric:tabular-nums}\n.big small{font-size:.42em;font-weight:500;margin-left:.2em;letter-spacing:0}\n.range{font-size:clamp(18px,3vw,22px)}\n.compare{color:var(--muted);font-size:17px}\n.meta{font-family:var(--mono);font-size:13px;color:var(--muted)}\n.controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center}\nbutton,.btn{font:inherit;font-size:15px;min-height:44px;display:inline-flex;align-items:center;border:1px solid var(--control);background:var(--surface);color:var(--ink);border-radius:8px;padding:8px 14px;cursor:pointer}\nbutton:hover,.btn:hover{border-color:var(--accent)}\nbutton:focus-visible,.btn:focus-visible,input:focus-visible,.vh:focus-visible+.btn{outline:2px solid var(--accent);outline-offset:2px}\n/* Visually hidden but still focusable, so the styled label stays reachable by keyboard. */\n#hero-meta span{white-space:nowrap}\n.vh{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}\nbutton[aria-pressed="true"]{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}\n.primary{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}\n.drop{border:1.5px dashed var(--line);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:10px;background:var(--surface)}\n.drop.over{border-color:var(--accent)}\n.drop p{margin:0;color:var(--muted);font-size:14px}\ncode{font-family:var(--mono);font-size:.92em}\nsection{display:flex;flex-direction:column;gap:14px;min-width:0}\nh2{font-size:20px;margin:0;text-wrap:balance}\n.grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:32px}\n.chart{width:100%;height:auto;display:block}\n.chart text{fill:var(--muted);font-family:var(--mono);font-size:11px}\n.legend{display:flex;flex-wrap:wrap;gap:14px;font-size:13px;color:var(--muted)}\n.legend i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:6px;vertical-align:-1px}\n.rank{display:flex;flex-direction:column;gap:10px;margin:0;padding:0;list-style:none}\n.rank li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 12px;font-size:14px}\n.rank .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.rank .val{font-family:var(--mono);font-variant-numeric:tabular-nums;color:var(--muted);text-align:right}\n.rank .bar{grid-column:1/-1;height:8px;background:var(--line);border-radius:4px;overflow:hidden}\n.rank .bar span{display:block;height:100%;background:var(--accent)}\n.explain{max-width:65ch;margin:0}\n.tablewrap{overflow-x:auto;border:1px solid var(--line);border-radius:10px}\ntable{border-collapse:collapse;width:100%;font-size:14px;min-width:620px}\nth,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--line);vertical-align:top}\nth{font-family:var(--mono);font-size:12px;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);font-weight:500}\ntd.num{font-family:var(--mono);font-variant-numeric:tabular-nums;white-space:nowrap}\ntr:last-child td{border-bottom:0}\n.label{font-family:var(--mono);font-size:11px;padding:2px 6px;border-radius:4px;border:1px solid currentColor}\na{color:var(--accent)}\na.src{display:inline-flex;align-items:center;min-height:44px}\n.phone-only{display:none}\n@media (max-width:600px){\n .phone-only{display:block}\n /* Method table: one card per parameter instead of a sideways scroll. */\n table{min-width:0}\n thead{display:none}\n table,tbody,tr,td{display:block}\n tr{padding:10px 12px;border-bottom:1px solid var(--line)}\n tr:last-child{border-bottom:0}\n td{border:0;padding:2px 0;white-space:normal}\n td[data-label]:not([data-label="Parameter"])::before{content:attr(data-label) " ";font-family:var(--mono);font-size:12px;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);margin-right:8px}\n td.num{white-space:normal}\n}\n.card{max-width:100%;height:auto;border-radius:10px;border:1px solid var(--line);display:block}\n.sharetext{font-size:14px;background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:12px;white-space:pre-wrap;margin:0}\n.status{font-size:14px;color:var(--muted);min-height:1.4em}\n.export>summary{cursor:pointer;min-height:44px;padding:11px 0;box-sizing:border-box;font-size:15px;color:var(--ink)}\n.export>*+*{margin-top:10px}\n.export label{display:block;font-size:14px;color:var(--muted)}\n.export textarea{display:block;font-family:var(--mono);font-size:13px;width:100%;box-sizing:border-box;border:1px solid var(--control);border-radius:8px;padding:10px;background:var(--surface);color:var(--ink)}\nkbd{font-family:var(--mono);font-size:.85em;border:1px solid var(--line);border-radius:4px;padding:0 4px}\nfooter{color:var(--muted);font-size:13px;border-top:1px solid var(--line);padding-top:16px;display:flex;flex-direction:column;gap:6px}\n@media (prefers-reduced-motion:no-preference){.rank .bar span{transition:width .3s ease}}\n</style>\n</head>\n<body>\n<main class="wrap">\n <a class="back" id="back-link" href="#" hidden>\u2190 Chase Holroyd\'s portfolio</a>\n <header class="top">\n <div class="brand">cc<span>watts</span></div>\n <span class="pill" id="source-pill">Loading</span>\n </header>\n\n <div class="intro">\n <h1>How much electricity does your AI coding use?</h1>\n <p class="lede" id="lede">ccwatts estimates the electricity behind Claude Code, an AI coding agent, from the token counts in its local logs. You\'re looking at sample data; try your own logs below.</p>\n </div>\n\n <div class="privacy" id="privacy"><span aria-hidden="true">\u{1F512}</span><div><b>Your logs never leave this browser.</b> This page makes no network requests. It reads token counts only, never your prompts or code.</div></div>\n\n <section class="hero" aria-live="polite">\n <span class="eyebrow" id="hero-eyebrow">Claude Code electricity</span>\n <div class="big" id="hero-big">\u2013</div>\n <div class="range" id="hero-range"></div>\n <div class="compare" id="hero-compare"></div>\n <div class="meta" id="hero-meta"></div>\n <div class="controls" role="group" aria-label="Time window">\n <button type="button" data-window="7d" id="w-7d">7 days</button>\n <button type="button" data-window="30d" id="w-30d">30 days</button>\n <button type="button" data-window="all" id="w-all">All time</button>\n </div>\n </section>\n\n <section class="drop" id="drop" hidden>\n <h2>Try it on your own logs</h2>\n <p>Claude Code saves logs in <code>~/.claude/projects</code>. Pick that folder, or drop any <code>.jsonl</code> files here. Files are read by this page on your device and never uploaded. On a Mac the folder is hidden: in the picker, press <kbd>\u2318</kbd> <kbd>\u21E7</kbd> <kbd>.</kbd> to show it.</p>\n <div class="controls">\n <input type="file" class="vh" id="pick-folder" webkitdirectory multiple>\n <label class="btn primary" for="pick-folder">Choose folder</label>\n <input type="file" class="vh" id="pick-files" accept=".jsonl" multiple>\n <label class="btn" for="pick-files">Choose files</label>\n <button type="button" id="use-sample">Use sample data</button>\n </div>\n <p id="cli-line">Prefer the terminal? Run <code id="cli-cmd">npx ccwatts</code> with Node 20+. Same numbers, plus a history that outlasts Claude Code\'s log cleanup.</p>\n <p class="phone-only" id="phone-hint">On a phone? Claude Code keeps its logs on the computer it runs on. Open this page there, or use the prompt below if you run Claude Code on the web.</p>\n <details class="export" id="export">\n <summary>Use Claude Code on the web, or can\'t find the folder?</summary>\n <p>Paste this prompt into a Claude Code session. It writes a copy of that machine\'s usage with token counts only, and no prompts or code. Then drop the file here, or paste what it prints into the box below.</p>\n <pre class="sharetext" id="export-prompt">Make a counts-only copy of my Claude Code usage for ccwatts.\n\nRead every .jsonl file under ~/.claude/projects. For each line whose "type" is "assistant" and that has message.usage, write one JSON line containing only:\n- type, timestamp, requestId\n- cwd, reduced to the last folder name (never the full path)\n- message.id and message.model\n- message.usage with input_tokens, cache_creation_input_tokens, cache_read_input_tokens and output_tokens\n\nLeave out everything else, especially message content. Save the lines as ccwatts-usage.jsonl and give me the file to download. If you can\'t offer a download, print the lines in one code block instead.</pre>\n <div class="controls">\n <button type="button" id="copy-prompt">Copy prompt</button>\n </div>\n <label for="paste-log">Paste the lines it printed</label>\n <textarea id="paste-log" rows="4" spellcheck="false" placeholder=\'{"type":"assistant", \u2026}\'></textarea>\n <div class="controls">\n <button type="button" id="read-paste">Read pasted lines</button>\n </div>\n </details>\n <div class="status" id="drop-status"></div>\n </section>\n\n <section>\n <h2>Over time</h2>\n <div class="legend" id="legend"></div>\n <svg class="chart" id="chart" viewBox="0 0 720 240" role="img" aria-label="Daily electricity by token type"></svg>\n <p class="meta" id="chart-note"></p>\n </section>\n\n <div class="grid2">\n <section><h2>By model</h2><ul class="rank" id="by-model"></ul></section>\n <section><h2>By project</h2><ul class="rank" id="by-project"></ul></section>\n </div>\n\n <section>\n <h2>What drives your number</h2>\n <p class="explain" id="explain"></p>\n </section>\n\n <section>\n <h2>Share it</h2>\n <img class="card" id="card" alt="Electricity receipt card" width="1200" height="627">\n <div class="controls">\n <button type="button" class="primary" id="save-png">Save PNG for LinkedIn</button>\n <button type="button" id="save-svg">Save SVG</button>\n <button type="button" id="copy-line">Copy share line</button>\n <button type="button" id="copy-post">Copy LinkedIn post</button>\n </div>\n <div class="status" id="share-status"></div>\n <pre class="sharetext" id="post"></pre>\n </section>\n\n <section>\n <h2>How this is estimated</h2>\n <p class="explain" id="method-intro"></p>\n <div class="tablewrap"><table id="params"></table></div>\n </section>\n\n <footer>\n <div id="footer-version"></div>\n <div>Open source. Read the code and the full methodology on <a id="repo-link" href="#" rel="noopener noreferrer">GitHub</a>.</div>\n <div>Built by <a id="built-by" href="#" rel="noopener noreferrer">Chase Holroyd</a>.</div>\n </footer>\n</main>\n<script id="ccwatts-data" type="application/json">{"mode":"playground","generatedAt":null,"rows":[]}</script>\n<script id="ccwatts-sample" type="application/json">[{"date":"2026-09-21","source":"claude-code","model":"claude-opus-4-6","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":47,"tokens":{"input":481,"cacheWrite":224858,"cacheRead":5433921,"output":21302},"outputContext":2453872321},{"date":"2026-09-21","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":17,"tokens":{"input":190,"cacheWrite":86694,"cacheRead":873313,"output":7714},"outputContext":425266546},{"date":"2026-09-22","source":"claude-code","model":"claude-sonnet-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":40,"tokens":{"input":393,"cacheWrite":206396,"cacheRead":4259456,"output":20119},"outputContext":2244470466},{"date":"2026-09-23","source":"claude-code","model":"claude-opus-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":52,"tokens":{"input":503,"cacheWrite":212076,"cacheRead":5914935,"output":28587},"outputContext":3409886608},{"date":"2026-09-24","source":"claude-code","model":"claude-haiku-4-5","projectId":"367b937ac815b163","projectName":"ccwatts","responses":21,"tokens":{"input":190,"cacheWrite":94458,"cacheRead":1218020,"output":10292},"outputContext":590700102},{"date":"2026-09-24","source":"claude-code","model":"claude-opus-4-6","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":95,"tokens":{"input":977,"cacheWrite":439114,"cacheRead":11248882,"output":44237},"outputContext":5490113479},{"date":"2026-09-25","source":"claude-code","model":"claude-opus-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":19,"tokens":{"input":209,"cacheWrite":93535,"cacheRead":1038543,"output":9584},"outputContext":589452635},{"date":"2026-09-27","source":"claude-code","model":"claude-sonnet-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":64,"tokens":{"input":655,"cacheWrite":253381,"cacheRead":7300740,"output":31952},"outputContext":3769121520},{"date":"2026-09-28","source":"claude-code","model":"claude-opus-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":28,"tokens":{"input":276,"cacheWrite":140541,"cacheRead":2230744,"output":13360},"outputContext":1153747881},{"date":"2026-09-29","source":"claude-code","model":"claude-haiku-4-5","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":38,"tokens":{"input":356,"cacheWrite":152315,"cacheRead":3272451,"output":18960},"outputContext":1641654756},{"date":"2026-09-29","source":"claude-code","model":"claude-haiku-4-5","projectId":"367b937ac815b163","projectName":"ccwatts","responses":32,"tokens":{"input":340,"cacheWrite":134170,"cacheRead":2682489,"output":16997},"outputContext":1568755458},{"date":"2026-09-30","source":"claude-code","model":"claude-sonnet-4-6","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":31,"tokens":{"input":353,"cacheWrite":135035,"cacheRead":2371226,"output":16712},"outputContext":1324962703},{"date":"2026-10-01","source":"claude-code","model":"claude-opus-4-6","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":16,"tokens":{"input":138,"cacheWrite":80233,"cacheRead":768856,"output":6943},"outputContext":382418077},{"date":"2026-10-01","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":24,"tokens":{"input":303,"cacheWrite":108412,"cacheRead":1401918,"output":11697},"outputContext":695089914},{"date":"2026-10-02","source":"claude-code","model":"claude-opus-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":50,"tokens":{"input":467,"cacheWrite":243377,"cacheRead":6114791,"output":24218},"outputContext":3052033764},{"date":"2026-10-02","source":"claude-code","model":"claude-sonnet-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":31,"tokens":{"input":291,"cacheWrite":156640,"cacheRead":2602032,"output":14670},"outputContext":1290375171},{"date":"2026-10-03","source":"claude-code","model":"claude-sonnet-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":67,"tokens":{"input":677,"cacheWrite":296672,"cacheRead":8900888,"output":33220},"outputContext":4724382924},{"date":"2026-10-05","source":"claude-code","model":"claude-opus-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":61,"tokens":{"input":601,"cacheWrite":250048,"cacheRead":7595553,"output":29379},"outputContext":3577266281},{"date":"2026-10-05","source":"claude-code","model":"claude-opus-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":32,"tokens":{"input":349,"cacheWrite":148766,"cacheRead":2857812,"output":18526},"outputContext":1716421621},{"date":"2026-10-06","source":"claude-code","model":"claude-haiku-4-5","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":42,"tokens":{"input":371,"cacheWrite":175342,"cacheRead":4217525,"output":20010},"outputContext":2086579461},{"date":"2026-10-07","source":"claude-code","model":"claude-haiku-4-5","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":53,"tokens":{"input":519,"cacheWrite":232336,"cacheRead":6626491,"output":26226},"outputContext":3535583255},{"date":"2026-10-07","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":27,"tokens":{"input":299,"cacheWrite":126737,"cacheRead":2032642,"output":15950},"outputContext":1332905565},{"date":"2026-10-08","source":"claude-code","model":"claude-opus-4-6","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":72,"tokens":{"input":805,"cacheWrite":314745,"cacheRead":9938479,"output":36755},"outputContext":5269927123},{"date":"2026-10-08","source":"claude-code","model":"claude-opus-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":71,"tokens":{"input":823,"cacheWrite":315410,"cacheRead":9639360,"output":33996},"outputContext":4708628928},{"date":"2026-10-08","source":"claude-code","model":"claude-sonnet-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":29,"tokens":{"input":288,"cacheWrite":144763,"cacheRead":2133553,"output":18135},"outputContext":1399436901},{"date":"2026-10-09","source":"claude-code","model":"claude-opus-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":32,"tokens":{"input":316,"cacheWrite":129817,"cacheRead":2427569,"output":15515},"outputContext":1186325442},{"date":"2026-10-09","source":"claude-code","model":"claude-opus-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":15,"tokens":{"input":122,"cacheWrite":58466,"cacheRead":581654,"output":7239},"outputContext":307424407},{"date":"2026-10-09","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":52,"tokens":{"input":438,"cacheWrite":243114,"cacheRead":6293294,"output":26306},"outputContext":3344828844},{"date":"2026-10-10","source":"claude-code","model":"claude-opus-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":66,"tokens":{"input":673,"cacheWrite":273538,"cacheRead":8614195,"output":31043},"outputContext":4430386287},{"date":"2026-10-11","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":71,"tokens":{"input":885,"cacheWrite":325807,"cacheRead":9676339,"output":33624},"outputContext":4753439717},{"date":"2026-10-12","source":"claude-code","model":"claude-opus-4-6","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":16,"tokens":{"input":186,"cacheWrite":90712,"cacheRead":869169,"output":8201},"outputContext":521288313},{"date":"2026-10-12","source":"claude-code","model":"claude-opus-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":46,"tokens":{"input":504,"cacheWrite":208473,"cacheRead":4885505,"output":22366},"outputContext":2523675175},{"date":"2026-10-13","source":"claude-code","model":"claude-opus-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":20,"tokens":{"input":244,"cacheWrite":111650,"cacheRead":1255969,"output":9499},"outputContext":611397728},{"date":"2026-10-13","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":25,"tokens":{"input":221,"cacheWrite":111841,"cacheRead":1586210,"output":11735},"outputContext":798440653},{"date":"2026-10-14","source":"claude-code","model":"claude-opus-4-6","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":43,"tokens":{"input":530,"cacheWrite":183803,"cacheRead":4389683,"output":20586},"outputContext":2203768836},{"date":"2026-10-14","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":59,"tokens":{"input":674,"cacheWrite":217090,"cacheRead":6575361,"output":28579},"outputContext":3253006122},{"date":"2026-10-15","source":"claude-code","model":"claude-haiku-4-5","projectId":"367b937ac815b163","projectName":"ccwatts","responses":74,"tokens":{"input":825,"cacheWrite":309110,"cacheRead":10212638,"output":38706},"outputContext":5535358176},{"date":"2026-10-15","source":"claude-code","model":"claude-sonnet-4-6","projectId":"367b937ac815b163","projectName":"ccwatts","responses":20,"tokens":{"input":265,"cacheWrite":109170,"cacheRead":1296443,"output":8112},"outputContext":599370597},{"date":"2026-10-15","source":"claude-code","model":"claude-sonnet-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":31,"tokens":{"input":253,"cacheWrite":156477,"cacheRead":2747706,"output":13093},"outputContext":1311357449},{"date":"2026-10-16","source":"claude-code","model":"claude-haiku-4-5","projectId":"0bb3418a8c1418a8","projectName":"hearth","responses":28,"tokens":{"input":336,"cacheWrite":130298,"cacheRead":1943815,"output":14701},"outputContext":1081930686},{"date":"2026-10-16","source":"claude-code","model":"claude-opus-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":48,"tokens":{"input":494,"cacheWrite":215976,"cacheRead":5622539,"output":24681},"outputContext":3062604147},{"date":"2026-10-17","source":"claude-code","model":"claude-opus-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":56,"tokens":{"input":530,"cacheWrite":237670,"cacheRead":6869143,"output":25467},"outputContext":3159646087},{"date":"2026-10-18","source":"claude-code","model":"claude-haiku-4-5","projectId":"48cee83ad258011a","projectName":"dotfiles","responses":38,"tokens":{"input":416,"cacheWrite":162013,"cacheRead":3360821,"output":20262},"outputContext":1907957687},{"date":"2026-10-19","source":"claude-code","model":"claude-haiku-4-5","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":97,"tokens":{"input":1108,"cacheWrite":414100,"cacheRead":10652494,"output":48446},"outputContext":5589764863},{"date":"2026-10-19","source":"claude-code","model":"claude-sonnet-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":65,"tokens":{"input":708,"cacheWrite":292947,"cacheRead":8732693,"output":35185},"outputContext":4755010745},{"date":"2026-10-20","source":"claude-code","model":"claude-haiku-4-5","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":66,"tokens":{"input":726,"cacheWrite":297357,"cacheRead":8882173,"output":32673},"outputContext":4543520242},{"date":"2026-10-20","source":"claude-code","model":"claude-sonnet-4-6","projectId":"d11d2b07dcb20490","projectName":"portfolio-site","responses":128,"tokens":{"input":1264,"cacheWrite":557853,"cacheRead":16226037,"output":59579},"outputContext":7838321066}]</script>\n<script>"use strict";(()=>{var R="ccwatts",ne="0.1.1",S="https://github.com/holroydchase-afk/ccwatts",q="https://chaseholroyd.com/ccwatts/",K="https://chaseholroyd.com/ccwatts.html";var W={input:"input",cacheWrite:"cache write",cacheRead:"cache read",output:"output"},L=e=>Math.round(e).toLocaleString("en-US");function A(e){return e>=100?L(e):e>=1?(Math.round(e*10)/10).toString():e>=.01?(Math.round(e*100)/100).toString():e===0?"0":e.toPrecision(1)}var oe=e=>e>=1&&e<100?e.toFixed(1):A(e);function M(e){let t=e.high>=1e3,n=t?1e3:1,o=t?"kWh":"Wh";return{central:`${oe(e.central/n)} ${o}`,range:`${oe(e.low/n)} \\u2013 ${oe(e.high/n)}`,unit:o}}var O=e=>e.replace(/^claude-/,"");var E=["input","cacheWrite","cacheRead","output"];var V=()=>({low:0,central:0,high:0});function re(e,t){return e.low+=t.low,e.central+=t.central,e.high+=t.high,e}function Re(e,t){let n=e.toLowerCase();for(let[o,r]of Object.entries(t.tiers))if(Array.isArray(r)&&r.some(s=>n.includes(s)))return{tier:o,known:!0};return{tier:String(t.tiers.fallback),known:!1}}function He(e,t){let n=e.params[t];if(!n)throw new Error(`Missing parameter "${t}" in params.json`);return n}function Be(e,t){let n=`output_wh_per_1k.${e}`;switch(t){case"output":return[n];case"input":return[n,"input_vs_output"];case"cacheWrite":return[n,"input_vs_output","cache_write_vs_input"];case"cacheRead":return[n,"input_vs_output","cache_read_vs_input"]}}function Fe(e,t,n){let o=.001,r=0,s=0;for(let a of Be(t,n)){let c=He(e,a);o*=c.value,r+=Math.log(c.high/c.value)**2,s+=Math.log(c.value/c.low)**2}return{whPerToken:o,up:Math.sqrt(r),down:Math.sqrt(s)}}var $e=()=>({input:0,cacheWrite:0,cacheRead:0,output:0});var be=new WeakMap;function Ye(e,t,n){let o=be.get(e);o||be.set(e,o=new Map);let r=`${t}:${n}`,s=o.get(r);return s||o.set(r,s=Fe(e,t,n)),s}function Te(e,t,n,o){if(e<=0)return V();let r=Ye(o,t,n),s=e*r.whPerToken;return{low:s/Math.exp(r.down),central:s,high:s*Math.exp(r.up)}}var ze=e=>e.slice(0,10);function G(e,t){let n=V(),o=$e(),r={model:new Map,type:new Map,day:new Map,project:new Map},s=new Set,a=null,c=null,k=(l,h,m,p,u)=>{let g=r[l].get(h);g||r[l].set(h,g={key:h,energy:V(),tokens:$e()}),re(g.energy,m),g.tokens[p]+=u};for(let l of e){let{tier:h,known:m}=Re(l.model,t);m||s.add(l.model);let p=ze(l.timestamp);(a===null||p<a)&&(a=p),(c===null||p>c)&&(c=p);let u=l.project?.name??"(unknown)";for(let g of E){let y=l.tokens[g];if(y<=0)continue;let T=Te(y,h,g,t);re(n,T),o[g]+=y,k("model",l.model,T,g,y),k("type",g,T,g,y),k("day",p,T,g,y),k("project",l.project?`${u}#${l.project.id}`:u,T,g,y)}}let $=l=>[...l.values()].sort((h,m)=>m.energy.central-h.energy.central),j=[...s].map(l=>`Unknown model "${l}": estimated as ${String(t.tiers.fallback)}.`);return{methodVersion:t.version,total:n,tokens:o,responses:e.length,byModel:$(r.model),byType:$(r.type),byDay:[...r.day.values()].sort((l,h)=>l.key.localeCompare(h.key)),byProject:$(r.project),span:a&&c?{from:a,to:c}:null,warnings:j}}var se=e=>e.split("#")[0]??e;function qe(e,t){if(t(e)===1)return"charging a phone about once";if(e>=1)return`charging a phone about ${t(e)} times`;let n=Math.round(e*100);return n<1?"under 1% of a phone charge":n>=45&&n<=55?"about half a phone charge":`about ${Math.round(n/10)*10||n}% of a phone charge`}function ae(e,t){let n=t.comparisons,o=[],r=a=>a>=10?Math.round(a):Math.round(a*10)/10,s=(a,c,k)=>r(a)===1?`1 ${c}`:`${r(a)} ${k}`;if(n.ev_kwh_per_mile){let a=e/n.ev_kwh_per_mile.value;o.push({id:"ev",amount:a,text:`driving an EV about ${s(a,"mile","miles")}`})}if(n.fridge_kwh_per_day){let a=e/n.fridge_kwh_per_day.value;o.push({id:"fridge",amount:a,text:`running a fridge about ${s(a,"day","days")}`})}if(n.phone_charge_kwh){let a=e/n.phone_charge_kwh.value;o.push({id:"phone",amount:a,text:qe(a,r)})}if(n.us_home_kwh_per_day){let a=e/n.us_home_kwh_per_day.value;o.push({id:"home",amount:a,text:`powering a US home about ${s(a,"day","days")}`})}return o}function U(e,t){let n=ae(e,t),o=n.filter(s=>s.amount>=1).slice(0,2),r=[...n].sort((s,a)=>a.amount-s.amount).slice(0,1);return(o.length?o:r).map(s=>s.text).join(" \\xB7 ")}var ve=S.replace(/^https:\\/\\//,"");function Ee(e){return e==="all time"?"All my Claude Code use so far":e.startsWith("since")?`My Claude Code use ${e}`:`My ${e} of Claude Code`}function _e(e){return e==="all time"?"(Sample data) All of a sample\'s Claude Code use":`(Sample data) ${e[0].toUpperCase()}${e.slice(1)} of Claude Code`}function J({estimate:e,params:t,windowLabel:n,sample:o}){let r=M(e.total),s=U(e.total.central/1e3,t).split(" \\xB7 ")[0];return`${o?_e(n):Ee(n)} came to about ${r.central} of electricity (range ${r.range} ${r.unit}), like ${s}. Estimated locally with ${R}, method v${t.version}: ${ve}`}function ie(e){let{estimate:t,params:n,windowLabel:o,sample:r}=e,s=M(t.total),a=t.total.central>0?Math.round((t.byType.find(c=>c.key==="cacheRead")?.energy.central??0)/t.total.central*100):0;return r?[`(Sample data) This is what ${R} shows for a sample developer, not a real measurement. ${_e(o).replace("(Sample data) ","")}: about ${s.central} (range ${s.range} ${s.unit}), like ${U(t.total.central/1e3,n).replace(" \\xB7 "," or ")}.`,"",`${a}% of it came from the agent re-reading context it had already seen.`,"","It\'s a range because nobody outside the AI labs knows the real energy per token. Every assumption is sourced, and it runs entirely on your own machine, so no logs leave it.","",`Try it on your own logs: ${q}`,`Code and method: ${S}`].join(`\n`):[`I estimated the electricity behind my AI coding. ${Ee(o)}: about ${s.central} (range ${s.range} ${s.unit}), like ${U(t.total.central/1e3,n).replace(" \\xB7 "," or ")}.`,"",`${a>=50?"The surprise: ":""}${a}% of it came from the agent re-reading context it had already seen.`,"","It\'s a range because nobody outside the AI labs knows the real energy per token. Every assumption is sourced, and the tool runs entirely on my laptop, so no logs leave it.","",`Try it in your browser: ${q}`,`Code and method: ${S}`].join(`\n`)}var _=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function N(e,t,n){let o=Math.max(2,n-e.length-t.length-2);return`${e} ${".".repeat(o)} ${t}`}function ce({estimate:e,params:t,windowLabel:n,sample:o}){let a=M(e.total),c=e.total.central/1e3,k=U(c,t).split(" \\xB7 "),$=a.unit,j=$==="kWh"?1e3:1,l=712,h=432,m=36,p=[],u="-".repeat(m);p.push({text:"ELECTRICITY RECEIPT",weight:"bold"}),p.push({text:e.span?`${e.span.from} \\u2192 ${e.span.to}`:n,muted:!0}),p.push({text:u,muted:!0});let g=[...E].sort((d,w)=>(e.byType.find(v=>v.key===w)?.energy.central??0)-(e.byType.find(v=>v.key===d)?.energy.central??0));for(let d of g){let w=e.byType.find(v=>v.key===d);p.push({text:N(W[d],`${A((w?.energy.central??0)/j)} ${$}`,m)})}p.push({text:u,muted:!0}),p.push({text:N("TOTAL (central)",a.central,m),weight:"bold"}),p.push({text:N("RANGE",`${a.range} ${$}`,m)}),p.push({text:u,muted:!0});for(let d of e.byModel.slice(0,3)){let w=e.total.central>0?Math.round(d.energy.central/e.total.central*100):0;p.push({text:N(O(d.key),`${w}%`,m),muted:!0})}p.push({text:N("responses",L(e.responses),m),muted:!0}),p.push({text:`method v${t.version} \\xB7 counts only`,muted:!0});let y=25,T=46,B=T+44,I=B+p.length*y+6,P=I+70,D=e.byDay.slice(-30),F=Math.max(...D.map(d=>d.energy.central),1e-9),ye=(h-64)/Math.max(D.length,1),De=D.map((d,w)=>{let v=6+d.energy.central/F*40;return`<rect x="${(l+32+w*ye).toFixed(1)}" y="${(I+46-v).toFixed(1)}" width="${Math.max(1,ye-2).toFixed(1)}" height="${v.toFixed(1)}" fill="#1f2a24"/>`}).join(""),we=[];for(let d=0,w=l+h;w>l;d++,w-=9)we.push(`${w.toFixed(1)},${P+(d%2?10:0)}`);let Oe=`M${l},${T} H${l+h} V${P} L${we.join(" L")} L${l},${P} Z`,Y="ui-monospace, \'SF Mono\', Menlo, Consolas, \'Liberation Mono\', monospace",z="\'Helvetica Neue\', Helvetica, Arial, sans-serif",Ne=p.map((d,w)=>{let v=B+w*y,xe=w<2||d.text.startsWith("method");return`<text x="${xe?l+h/2:l+30}" y="${v}" font-family="${Y}" font-size="16.5" ${xe?\'text-anchor="middle"\':""} font-weight="${d.weight??"normal"}" fill="${d.muted?"#5e6b63":"#1f2a24"}">${_(d.text)}</text>`}).join(`\n `),ke=a.central.split(" ");return`<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="627" viewBox="0 0 1200 627" role="img" aria-label="${_(J({estimate:e,params:t,windowLabel:n,sample:o}))}">\n <rect width="1200" height="627" fill="#12251c"/>\n <rect x="0" y="621" width="1200" height="6" fill="#7cc4a0"/>\n <text x="64" y="104" font-family="${Y}" font-size="20" letter-spacing="2" fill="#7cc4a0">${o?\'<tspan fill="#e0b56a">SAMPLE DATA \\xB7 </tspan>\':""}CLAUDE CODE \\xB7 ${_(n.toUpperCase())}</text>\n <text x="60" y="262" font-family="${z}" font-size="148" font-weight="700" fill="#f4f1e8">${_(ke[0]??"")}<tspan font-size="64" font-weight="500" dx="14">${_(ke[1]??"")}</tspan></text>\n <text x="64" y="318" font-family="${z}" font-size="28" fill="#cfe3d6">of electricity, range ${_(a.range)} ${$}</text>\n ${k.map((d,w)=>`<text x="64" y="${384+w*38}" font-family="${z}" font-size="26" fill="#f4f1e8">\\u2248 ${_(d)}</text>`).join(`\n `)}\n <text x="64" y="520" font-family="${z}" font-size="20" fill="${o?"#e0b56a":"#9fb8a8"}">${o?"Sample data, not a real measurement.":"Estimated on my laptop. No logs left it."}</text>\n <text x="64" y="552" font-family="${Y}" font-size="17" fill="#7cc4a0">${_(R)} \\xB7 method v${_(t.version)}</text>\n <text x="64" y="578" font-family="${Y}" font-size="17" fill="#7cc4a0">${_(ve)}</text>\n <path d="${Oe}" fill="#f7f5ef"/>\n <g>\n ${Ne}\n </g>\n ${De}\n</svg>\n`}function Ke(e){let t=0xcbf29ce484222325n,n=0x100000001b3n;for(let o=0;o<e.length;o++)t^=BigInt(e.charCodeAt(o)),t=t*n&0xffffffffffffffffn;return t.toString(16).padStart(16,"0")}function le(e){let t=e.replace(/[\\\\/]+$/,""),n=t.split(/[\\\\/]/).pop()||t;return{id:Ke(t),name:n}}var Z=e=>typeof e=="number"&&Number.isFinite(e)&&e>0?e:0,H=e=>typeof e=="string"&&e!==""?e:null;function Ve(e){if(e.type!=="assistant")return null;let t=e.message,n=t?.usage,o=H(t?.model),r=H(e.timestamp),s=H(t?.id);if(!n||!o||!r||!s||o==="<synthetic>")return null;let a=H(e.cwd);return{source:"claude-code",key:`${s}:${H(e.requestId)??""}`,timestamp:r,model:o,project:a?le(a):null,tokens:{input:Z(n.input_tokens),cacheWrite:Z(n.cache_creation_input_tokens),cacheRead:Z(n.cache_read_input_tokens),output:Z(n.output_tokens)}}}var ue={id:"claude-code",parse(e){let t=new Map;for(let n of e.split(`\n`)){if(!n.includes(\'"assistant"\'))continue;let o;try{o=JSON.parse(n)}catch{continue}let r=Ve(o);r&&t.set(r.key,r)}return[...t.values()]}};function pe(e){let t=new Map;for(let n of e)t.set(`${n.source}:${n.key}`,n);return[...t.values()]}var Ce={version:"0.1.0",as_of:"2026-10-21",license:"CC BY 4.0",boundary:"comprehensive: accelerators, host CPU and memory, idle capacity, and data-center overhead (PUE)",interval:"low and high are each parameter\'s plausible bounds, treated as a ~90% interval",params:{"output_wh_per_1k.opus":{value:1.2,low:.4,high:3,unit:"Wh per 1k output tokens",label:"MODELED",source:"https://www.theclimatebrink.com/p/the-real-energy-use-of-agentic-ai",as_of:"2026-10-21",note:"Largest tier. Scaled up from the Sonnet value; Anthropic does not disclose model size or energy. Bounds cover published per-token estimates for frontier models."},"output_wh_per_1k.sonnet":{value:.6,low:.25,high:1.5,unit:"Wh per 1k output tokens",label:"MODELED",source:"https://arxiv.org/abs/2508.15734",as_of:"2026-10-21",note:"Anchored so a typical chat query (about 1k input, 400 output tokens) lands near Google\'s measured 0.24 Wh median Gemini prompt and the 0.34 Wh median frontier query (IQR 0.18-0.67). Third-party estimates for Claude output run 0.4-2 Wh per 1k."},"output_wh_per_1k.haiku":{value:.25,low:.08,high:.6,unit:"Wh per 1k output tokens",label:"GUESS",source:"https://www.anthropic.com/pricing",as_of:"2026-10-21",note:"Smallest tier. Scaled down from Sonnet roughly in line with the price gap between tiers, which is a weak proxy for compute."},input_vs_output:{value:.3,low:.15,high:.5,unit:"ratio",label:"MODELED",source:"https://www.anthropic.com/pricing",as_of:"2026-10-21",note:"Energy of one fresh input token relative to one output token. Input is processed in parallel (prefill), so it costs less per token. Anthropic prices input at 1/5 of output; a Watershed-based estimate uses about 1/3."},cache_write_vs_input:{value:1,low:.8,high:1.25,unit:"ratio",label:"MODELED",source:"https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching",as_of:"2026-10-21",note:"Writing to the cache means processing the tokens fresh, then storing the result. Priced at 1.25x input."},cache_read_vs_input:{value:.1,low:.01,high:.25,unit:"ratio",label:"MODELED",source:"https://www.theclimatebrink.com/p/the-real-energy-use-of-agentic-ai",as_of:"2026-10-21",note:"The parameter that matters most for agents, where cache reads are 90%+ of tokens. Hausfather\'s central case is 10% of fresh input, tested at 1% and 25%. Anthropic prices cache reads at 0.1x input."}},tiers:{opus:["opus"],sonnet:["sonnet"],haiku:["haiku"],fallback:"sonnet"},comparisons:{ev_kwh_per_mile:{value:.3,unit:"kWh per mile",label:"MEASURED",source:"https://www.fueleconomy.gov/feg/evsbs.shtml",as_of:"2026-10-21",note:"Typical EV uses 25-35 kWh per 100 miles (EPA)."},fridge_kwh_per_day:{value:1.5,unit:"kWh per day",label:"MEASURED",source:"https://www.energystar.gov/products/refrigerators",as_of:"2026-10-21",note:"A typical US refrigerator uses about 400-600 kWh a year."},us_home_kwh_per_day:{value:29,unit:"kWh per day",label:"MEASURED",source:"https://www.eia.gov/tools/faqs/faq.php?id=97&t=3",as_of:"2026-10-21",note:"Average US household used about 10,800 kWh in a year (EIA)."},phone_charge_kwh:{value:.019,unit:"kWh per smartphone charge",label:"MEASURED",source:"https://www.epa.gov/energy/greenhouse-gas-equivalencies-calculator-calculations-and-references",as_of:"2026-10-21",note:"EPA\'s figure for fully charging one smartphone."}}};var b=Ce;var Q=e=>`${e.date}|${e.source}|${e.model}|${e.projectId??""}`;var Me=e=>e.output*(e.input+e.cacheWrite+e.cacheRead);function de(e){let t=new Map;for(let n of e){let o={date:n.timestamp.slice(0,10),source:n.source,model:n.model,projectId:n.project?.id??null,projectName:n.project?.name??null},r=Q(o),s=t.get(r);s||t.set(r,s={...o,responses:0,tokens:{input:0,cacheWrite:0,cacheRead:0,output:0},outputContext:0}),s.responses+=1;for(let a of E)s.tokens[a]+=n.tokens[a];s.outputContext+=Me(n.tokens)}return Je([...t.values()])}function Je(e){return e.sort((t,n)=>Q(t).localeCompare(Q(n)))}function X(e){return e.map(t=>({source:t.source,key:Q(t),timestamp:`${t.date}T00:00:00.000Z`,model:t.model,project:t.projectId?{id:t.projectId,name:t.projectName??"(unknown)"}:null,tokens:{...t.tokens}}))}var i=e=>document.getElementById(e),x=e=>e.replace(/[&<>"\']/g,t=>({"&":"&","<":"<",">":">",\'"\':""","\'":"'"})[t]);function Se(e,t){try{return JSON.parse(i(e).textContent??"")}catch{return t}}var C=Se("ccwatts-data",{mode:"playground",generatedAt:null,rows:[]}),Ae=Se("ccwatts-sample",[]),f={rows:C.mode==="dashboard"&&C.rows.length?C.rows:Ae,source:C.mode==="dashboard"&&C.rows.length?"history":"sample",window:"30d"},Ze={"7d":"last 7 days","30d":"last 30 days",all:"all time"};function Qe(e,t){if(t==="all"||e.length===0)return e;let n=t==="7d"?7:30,o=e.reduce((a,c)=>c.date>a?c.date:a,"0000-00-00"),r=f.source==="history"?Date.now():Date.parse(`${o}T00:00:00Z`)+864e5,s=new Date(r-n*864e5).toISOString().slice(0,10);return e.filter(a=>a.date>=s)}function Xe(e){let t=G(X(e),b);return t.responses=e.reduce((n,o)=>n+o.responses,0),t}function et(e,t){let n=M(e.total),[o,r]=n.central.split(" ");i("hero-eyebrow").textContent=`Claude Code electricity \\xB7 ${t}`,i("hero-big").innerHTML=e.responses?`${x(o??"")}<small>${x(r??"")}</small>`:"\\u2013",i("hero-range").textContent=e.responses?`Plausible range ${n.range} ${n.unit}, from the bounds on every assumption.`:"No usage in this window.",i("hero-compare").textContent=e.responses?`That\'s like ${U(e.total.central/1e3,b).replace(" \\xB7 ",", or ")}.`:"";let s=E.reduce((c,k)=>c+e.tokens[k],0),a=[`${L(e.responses)} responses`,`${L(s)} tokens`,...e.span?[`${e.span.from} to ${e.span.to}`]:[],`method v${b.version}`];i("hero-meta").innerHTML=a.map(c=>`<span>${x(c)}</span>`).join(" \\xB7 ");for(let c of["7d","30d","all"])i(`w-${c}`).setAttribute("aria-pressed",String(f.window===c))}function Ue(e){let t=i("chart"),n=Math.round(Math.min(720,Math.max(320,t.getBoundingClientRect().width||720))),o=240;t.setAttribute("viewBox",`0 0 ${n} ${o}`);let r={l:44,r:8,t:22,b:28},s=e.byDay;if(i("legend").innerHTML=["cacheRead","cacheWrite","output","input"].map(u=>`<span><i style="background:var(--t-${u})"></i>${W[u]}</span>`).join(""),!s.length){t.innerHTML=`<text x="${n/2}" y="${o/2}" text-anchor="middle">No usage in this window</text>`,i("chart-note").textContent="";return}let a=M(e.total),c=a.unit==="kWh"?1e3:1,k=Math.max(...s.map(u=>u.energy.central))/c,$=ot(k/3),j=Math.max($*Math.ceil(k/$),$),l=u=>r.t+(o-r.t-r.b)*(1-u/j),h=(n-r.l-r.r)/s.length,m=[];for(let u=0;u<=j+1e-9;u+=$)m.push(`<line x1="${r.l}" x2="${n-r.r}" y1="${l(u)}" y2="${l(u)}" stroke="var(--line)" stroke-width="1"/>`),m.push(`<text x="${r.l-6}" y="${l(u)+4}" text-anchor="end">${A(u)}</text>`);m.push(`<text x="0" y="10">${a.unit} per day</text>`),s.forEach((u,g)=>{let y=0,T=r.l+g*h+h*.12,B=Math.max(1,h*.76);for(let I of["cacheRead","cacheWrite","output","input"]){let P=tt(u,I);if(P<=0)continue;let D=y/c;y+=P;let F=y/c;m.push(`<rect x="${T.toFixed(1)}" y="${l(F).toFixed(1)}" width="${B.toFixed(1)}" height="${Math.max(.5,l(D)-l(F)).toFixed(1)}" fill="var(--t-${I})"><title>${u.key} ${W[I]} ${A(P/c)} ${a.unit}</title></rect>`)}});let p=s.length===1?[0]:[0,Math.floor((s.length-1)/2),s.length-1];for(let u of new Set(p)){let g=u===0?"start":u===s.length-1?"end":"middle",y=u===0?r.l:u===s.length-1?n-r.r:r.l+(u+.5)*h;m.push(`<text x="${y}" y="${o-8}" text-anchor="${g}">${s[u].key.slice(5)}</text>`)}t.innerHTML=m.join(""),i("chart-note").textContent=`Bars show the central estimate per day. The total for this window is ${a.central}, with a range of ${a.range} ${a.unit}.`}function tt(e,t){return ee.get(e.key)?.[t]??0}var ee=new Map;function nt(e){ee=new Map;for(let t of e){let n=G(X([t]),b),o=ee.get(t.date);o||ee.set(t.date,o={input:0,cacheWrite:0,cacheRead:0,output:0});for(let r of n.byType)o[r.key]+=r.energy.central}}function ot(e){if(e<=0)return 1;let t=10**Math.floor(Math.log10(e)),n=e/t;return(n<=1?1:n<=2?2:n<=5?5:10)*t}function Le(e,t,n,o){let r=Math.max(...t.map(a=>a.energy.central),1e-9),s=t.slice(0,6);i(e).innerHTML=s.length?s.map(a=>{let c=M(a.energy),k=o.total.central?Math.round(a.energy.central/o.total.central*100):0;return`<li><span class="name" title="${x(n(a.key))}">${x(n(a.key))}</span><span class="val">${x(c.central)} \\xB7 ${k}%</span><span class="bar"><span style="width:${(a.energy.central/r*100).toFixed(1)}%"></span></span><span class="val" style="grid-column:1/-1;text-align:left">range ${x(c.range)} ${x(c.unit)}</span></li>`}).join(""):\'<li><span class="name">No usage in this window</span></li>\'}function rt(e){let t=r=>e.total.central?Math.round((e.byType.find(s=>s.key===r)?.energy.central??0)/e.total.central*100):0,n=E.reduce((r,s)=>r+e.tokens[s],0),o=n?Math.round(e.tokens.cacheRead/n*100):0;i("explain").textContent=e.responses?`Each time Claude Code takes a step, it re-reads the conversation so far. Most of that comes from a cache, which is cheap per token but happens constantly. Here, cache reads were ${o}% of tokens and about ${t("cacheRead")}% of the electricity. Writing new context into the cache was ${t("cacheWrite")}%, and the text the model wrote back was ${t("output")}%. Long sessions grow the context the agent re-reads, so starting fresh sessions for new tasks is the simplest way to lower your number.`:"Load some usage to see what drives it."}var me="",je="",ge="";function st(e,t){let n={estimate:e,params:b,windowLabel:t,sample:f.source==="sample"};me=ce(n),je=J(n),ge=ie(n),i("card").src=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(me)}`,i("post").textContent=ge,i("share-status").textContent=f.source==="sample"?"This card shows sample data. Load your own logs to make yours.":""}function at(e){try{return new URL(e).hostname.replace(/^www\\./,"")}catch{return"source"}}function it(){i("method-intro").textContent=`Energy = tokens \\xD7 energy per token, by model family and token type. Each parameter below has a low and high bound, and they combine into the range you see. The boundary is ${b.boundary}. Labels say how solid each number is: MODELED is derived from published figures, GUESS is a best estimate with wide bounds. Method v${b.version}, parameters as of ${b.as_of}, licensed ${b.license}.`;let e=Object.entries(b.params).map(([t,n])=>`<tr><td data-label="Parameter"><code>${x(t)}</code><div class="meta">${x(n.note)}</div></td><td class="num" data-label="Value">${n.value}</td><td class="num" data-label="Range">${n.low} \\u2013 ${n.high}</td><td data-label="Unit">${x(n.unit)}</td><td data-label="Label"><span class="label">${x(n.label)}</span></td><td data-label="Source"><a class="src" href="${x(n.source)}" rel="noopener noreferrer">${x(at(n.source))}</a></td></tr>`).join("");i("params").innerHTML=`<thead><tr><th>Parameter</th><th>Value</th><th>Range</th><th>Unit</th><th>Label</th><th>Source</th></tr></thead><tbody>${e}</tbody>`}var ct={sample:"ccwatts estimates the electricity behind Claude Code, an AI coding agent, from the token counts in its local logs. You\'re looking at sample data; try your own logs below.",files:"ccwatts estimates the electricity behind Claude Code, an AI coding agent, from the token counts in its local logs. You\'re looking at the files you loaded; nothing was uploaded.",history:"ccwatts estimates the electricity behind your Claude Code use from the token counts in its local logs, as a range with every assumption sourced."},fe=null;function te(){let e=Qe(f.rows,f.window);nt(e);let t=Xe(e);fe=t;let n=Ze[f.window],o=i("source-pill");o.textContent=f.source==="sample"?"Sample data":f.source==="history"?"Your history":"Your files",o.classList.toggle("sample",f.source==="sample"),i("lede").textContent=ct[f.source],et(t,n),Ue(t),Le("by-model",t.byModel,O,t),Le("by-project",t.byProject,se,t),rt(t),st(t,n)}async function Pe(e){let t=[...e].filter(r=>r.name.endsWith(".jsonl")),n=i("drop-status");if(!t.length){n.textContent="No .jsonl files found there. Claude Code keeps them in ~/.claude/projects.";return}let o=`${t.length} file${t.length===1?"":"s"}`;n.textContent=`Reading ${o} on this device\\u2026`,Ie(await Promise.all(t.map(r=>r.text())),o)}function Ie(e,t){let n=i("drop-status"),o=[];for(let s of e)o.push(...ue.parse(s));let r=de(pe(o));if(!r.length){n.textContent=`${t==="pasted text"?"That text":"Those files"} had no Claude Code usage in them.`;return}f.rows=r,f.source="files",f.window="all",n.textContent=`Read ${L(o.length)} responses from ${t}. Nothing was uploaded.`,te()}function We(e,t){let n=document.createElement("a");n.href=URL.createObjectURL(t),n.download=e,document.body.append(n),n.click(),n.remove(),setTimeout(()=>URL.revokeObjectURL(n.href),1e3)}async function he(e,t,n="share-status",o="post"){let r=i(n);try{await navigator.clipboard.writeText(e),r.textContent=`Copied the ${t}.`}catch{let s=document.createRange();s.selectNodeContents(i(o)),getSelection()?.removeAllRanges(),getSelection()?.addRange(s),r.textContent=`Couldn\'t reach the clipboard. The ${o==="post"?"post text":t} is selected so you can copy it.`}}function lt(){let e=new Image;e.onload=()=>{let t=document.createElement("canvas");t.width=1200,t.height=627,t.getContext("2d").drawImage(e,0,0,1200,627),t.toBlob(n=>n&&We(`${R}-receipt.png`,n),"image/png")},e.src=i("card").src}function ut(){for(let o of["7d","30d","all"])i(`w-${o}`).addEventListener("click",()=>{f.window=o,te()});if(i("save-png").addEventListener("click",lt),i("save-svg").addEventListener("click",()=>We(`${R}-receipt.svg`,new Blob([me],{type:"image/svg+xml"}))),i("copy-line").addEventListener("click",()=>{he(je,"share line")}),i("copy-post").addEventListener("click",()=>{he(ge,"LinkedIn post")}),C.mode==="playground"){let o=i("drop");o.hidden=!1,i("copy-prompt").addEventListener("click",()=>{he(i("export-prompt").textContent??"","prompt","drop-status","export-prompt")}),i("read-paste").addEventListener("click",()=>{Ie([i("paste-log").value],"pasted text")});for(let s of["pick-folder","pick-files"])i(s).addEventListener("change",a=>{let c=a.target.files;c&&Pe(c)});i("use-sample").addEventListener("click",()=>{f.rows=Ae,f.source="sample",f.window="30d",i("drop-status").textContent="",te()});let r=s=>a=>{a.preventDefault(),o.classList.toggle("over",s)};o.addEventListener("dragover",r(!0)),o.addEventListener("dragleave",r(!1)),o.addEventListener("drop",s=>{r(!1)(s),s.dataTransfer?.files&&Pe(s.dataTransfer.files)})}let e=0,t=i("chart").getBoundingClientRect().width;if(window.addEventListener("resize",()=>{clearTimeout(e),e=window.setTimeout(()=>{let o=i("chart").getBoundingClientRect().width;fe&&Math.round(o)!==Math.round(t)&&Ue(fe),t=o},150)}),i("repo-link").href=S,i("cli-cmd").textContent=`npx ${R}`,i("built-by").href=K,C.mode==="playground"){let o=i("back-link");o.href=K,o.hidden=!1}let n=C.generatedAt?` \\xB7 written ${C.generatedAt.slice(0,16).replace("T"," ")} UTC`:"";i("footer-version").textContent=`${R} v${ne} \\xB7 method v${b.version}${n}. Electricity only, not carbon or water.`,document.title=C.mode==="dashboard"?`${R} dashboard`:`${R} playground`}ut();it();te();})();</script>\n</body>\n</html>\n';
|
|
646
662
|
|
|
647
663
|
// src/store.ts
|
|
648
664
|
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
@@ -683,6 +699,7 @@ function openFile(path) {
|
|
|
683
699
|
} catch {
|
|
684
700
|
}
|
|
685
701
|
}
|
|
702
|
+
var sampleRows = () => readSample(ccwatts_default);
|
|
686
703
|
|
|
687
704
|
// src/logs.ts
|
|
688
705
|
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
@@ -722,7 +739,7 @@ function shares(rows, total, label, max = 4) {
|
|
|
722
739
|
function header(params, window) {
|
|
723
740
|
return `${APP_NAME} v${APP_VERSION} \xB7 method v${params.version} \xB7 ${window.label}`;
|
|
724
741
|
}
|
|
725
|
-
function summary(e, params, window) {
|
|
742
|
+
function summary(e, params, window, sample = false) {
|
|
726
743
|
if (e.responses === 0) return `${header(params, window)}
|
|
727
744
|
|
|
728
745
|
No Claude Code usage found in this window. Try --since all.`;
|
|
@@ -746,7 +763,8 @@ function summary(e, params, window) {
|
|
|
746
763
|
const span = e.span ? ` \xB7 ${e.span.from} to ${e.span.to}` : "";
|
|
747
764
|
lines.push(` Based on ${int(e.responses)} responses, ${int(totalTokens2(e.tokens))} tokens${span}`);
|
|
748
765
|
for (const w of e.warnings) lines.push(` Note ${w}`);
|
|
749
|
-
|
|
766
|
+
const next = sample ? `Your own usage: ${APP_NAME}` : `Share: ${APP_NAME} receipt`;
|
|
767
|
+
lines.push("", ` How this is estimated: ${APP_NAME} --method \xB7 ${next}`);
|
|
750
768
|
return lines.join("\n");
|
|
751
769
|
}
|
|
752
770
|
function table(head, rows, alignLeft = 1) {
|
|
@@ -788,7 +806,7 @@ function method(params) {
|
|
|
788
806
|
lines.push("", "Full write-up: METHODOLOGY.md");
|
|
789
807
|
return lines.join("\n");
|
|
790
808
|
}
|
|
791
|
-
function json(e, params, window) {
|
|
809
|
+
function json(e, params, window, sample = false) {
|
|
792
810
|
const rows = (list, dim) => list.map((r) => ({
|
|
793
811
|
key: dim === "project" ? projectName(r.key) : r.key,
|
|
794
812
|
...dim === "project" ? { projectId: r.key.split("#")[1] ?? null } : {},
|
|
@@ -801,6 +819,7 @@ function json(e, params, window) {
|
|
|
801
819
|
version: APP_VERSION,
|
|
802
820
|
methodVersion: params.version,
|
|
803
821
|
unit: "Wh",
|
|
822
|
+
...sample ? { sample: true } : {},
|
|
804
823
|
window,
|
|
805
824
|
span: e.span,
|
|
806
825
|
responses: e.responses,
|
|
@@ -864,6 +883,11 @@ function historyCsv(rows, params) {
|
|
|
864
883
|
});
|
|
865
884
|
return [head, ...body].map((cells) => cells.map(csvCell).join(",")).join("\n");
|
|
866
885
|
}
|
|
886
|
+
function statusLine(e, params) {
|
|
887
|
+
if (e.responses === 0) return `\u26A1 ${APP_NAME}: no usage yet this session`;
|
|
888
|
+
const t = formatEnergy(e.total);
|
|
889
|
+
return `\u26A1 ~${t.central} this session (range ${t.range}) \xB7 method v${params.version}`;
|
|
890
|
+
}
|
|
867
891
|
|
|
868
892
|
// src/index.ts
|
|
869
893
|
var HELP = `Usage: ${APP_NAME} [command] [options]
|
|
@@ -876,6 +900,7 @@ Commands:
|
|
|
876
900
|
history Weekly trend, all-time total, best and worst weeks
|
|
877
901
|
dashboard Write an offline HTML dashboard of your history and open it
|
|
878
902
|
receipt Share line, LinkedIn post text and a 1200\xD7627 card (SVG)
|
|
903
|
+
statusline One line for the Claude Code status line (reads its JSON on stdin)
|
|
879
904
|
|
|
880
905
|
Options:
|
|
881
906
|
--since <when> 7d, 12h, 4w, 2026-10-01 or all (default 30d; history defaults to all)
|
|
@@ -884,14 +909,27 @@ Options:
|
|
|
884
909
|
--method Parameters, sources and method version
|
|
885
910
|
--export csv With history: daily rows as CSV on stdout
|
|
886
911
|
--out <file> With receipt: where to write the card (default ./${APP_NAME}-receipt.svg)
|
|
887
|
-
--
|
|
912
|
+
--demo Try it on synthetic sample data (no logs needed)
|
|
913
|
+
--path <dir> Estimate just this folder or .jsonl file (history is left alone)
|
|
888
914
|
--no-open With dashboard: write the file but don't open it
|
|
889
915
|
--no-history Don't read or update ~/.${APP_NAME}/history.jsonl
|
|
890
|
-
-
|
|
916
|
+
-v, --version Show the version
|
|
917
|
+
-h, --help Show this help
|
|
918
|
+
|
|
919
|
+
Logs somewhere else? Set CLAUDE_CONFIG_DIR, as Claude Code does, to keep your history.`;
|
|
891
920
|
var DIMS = ["model", "day", "project", "type"];
|
|
892
|
-
var COMMANDS = ["summary", "history", "receipt", "dashboard"];
|
|
921
|
+
var COMMANDS = ["summary", "history", "receipt", "dashboard", "statusline"];
|
|
922
|
+
function plainArgError(error) {
|
|
923
|
+
const code = error.code;
|
|
924
|
+
const message = error instanceof Error ? error.message : "";
|
|
925
|
+
const option = message.match(/'(-{1,2}[\w-]+)/)?.[1];
|
|
926
|
+
const help = `Run ${APP_NAME} --help to see the options.`;
|
|
927
|
+
if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") return new Error(`Unknown option "${option}". ${help}`);
|
|
928
|
+
if (code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE") return new Error(`${option} needs a value. ${help}`);
|
|
929
|
+
return void 0;
|
|
930
|
+
}
|
|
893
931
|
async function main() {
|
|
894
|
-
const { values, positionals } =
|
|
932
|
+
const { values, positionals } = parseArgsPlain({
|
|
895
933
|
allowPositionals: true,
|
|
896
934
|
options: {
|
|
897
935
|
since: { type: "string" },
|
|
@@ -901,12 +939,15 @@ async function main() {
|
|
|
901
939
|
export: { type: "string" },
|
|
902
940
|
out: { type: "string" },
|
|
903
941
|
path: { type: "string" },
|
|
942
|
+
demo: { type: "boolean" },
|
|
904
943
|
"no-history": { type: "boolean" },
|
|
905
944
|
"no-open": { type: "boolean" },
|
|
945
|
+
version: { type: "boolean", short: "v" },
|
|
906
946
|
help: { type: "boolean", short: "h" }
|
|
907
947
|
}
|
|
908
948
|
});
|
|
909
949
|
if (values.help) return void console.log(HELP);
|
|
950
|
+
if (values.version) return void console.log(`${APP_NAME} ${APP_VERSION}`);
|
|
910
951
|
const params = defaultParams;
|
|
911
952
|
if (values.method) return void console.log(method(params));
|
|
912
953
|
const command = positionals[0] ?? "summary";
|
|
@@ -917,13 +958,22 @@ ${HELP}`);
|
|
|
917
958
|
process.exitCode = 1;
|
|
918
959
|
return;
|
|
919
960
|
}
|
|
961
|
+
if (command === "statusline") return void console.log(await sessionLine(values.path));
|
|
920
962
|
const by = values.by;
|
|
921
963
|
if (by && !DIMS.includes(by)) throw new Error(`--by must be one of: ${DIMS.join(", ")}`);
|
|
922
964
|
if (values.export && values.export !== "csv") throw new Error(`--export supports csv only`);
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
965
|
+
const demo = Boolean(values.demo);
|
|
966
|
+
if (demo && (command === "receipt" || command === "dashboard")) {
|
|
967
|
+
throw new Error(
|
|
968
|
+
`${command} is for your own usage, so it doesn't run on sample data. The playground shows the sample ${command === "receipt" ? "card" : "dashboard"}: ${PLAYGROUND_URL}`
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
const logs = demo ? { roots: [], files: 0, records: [] } : await loadLogs(values.path ? [values.path] : defaultLogRoots());
|
|
972
|
+
const sample = demo ? sampleRows() : [];
|
|
973
|
+
const now = demo ? /* @__PURE__ */ new Date(`${sample.map((r) => r.date).sort().at(-1)}T23:59:59Z`) : /* @__PURE__ */ new Date();
|
|
974
|
+
const window = parseSince(values.since ?? (command === "history" || demo ? "all" : "30d"), now);
|
|
975
|
+
let rows = demo ? sample : rollup(logs.records);
|
|
976
|
+
if (!values["no-history"] && !values.path && !demo) {
|
|
927
977
|
const stored = await readHistory().catch((error) => {
|
|
928
978
|
console.error(`Couldn't read history (${error instanceof Error ? error.message : String(error)}). Using current logs only.`);
|
|
929
979
|
return [];
|
|
@@ -939,6 +989,7 @@ ${HELP}`);
|
|
|
939
989
|
console.log(`No Claude Code logs found. Looked in:
|
|
940
990
|
${logs.roots.map((r) => ` ${r}`).join("\n")}`);
|
|
941
991
|
console.log(`Point ${APP_NAME} at another folder with --path <dir>.`);
|
|
992
|
+
console.log(`No Claude Code yet? See it on sample data with ${APP_NAME} --demo, or in your browser: ${PLAYGROUND_URL}`);
|
|
942
993
|
return;
|
|
943
994
|
}
|
|
944
995
|
if (command === "dashboard") {
|
|
@@ -953,9 +1004,11 @@ ${logs.roots.map((r) => ` ${r}`).join("\n")}`);
|
|
|
953
1004
|
if (!(window.start && window.label.includes("hour"))) {
|
|
954
1005
|
e.responses = inWindow.reduce((n, r) => n + r.responses, 0);
|
|
955
1006
|
}
|
|
1007
|
+
const label = demo && !values.json && !values.export ? "Sample data (synthetic), not your usage.\n\n" : "";
|
|
956
1008
|
if (command === "history") {
|
|
957
1009
|
if (values.export) return void console.log(historyCsv(inWindow, params));
|
|
958
|
-
if (values.json) return void console.log(json(e, params, window));
|
|
1010
|
+
if (values.json) return void console.log(json(e, params, window, demo));
|
|
1011
|
+
if (label) process.stdout.write(label);
|
|
959
1012
|
return void console.log(history(e, params, window, new Set(rows.map((r) => r.date)).size));
|
|
960
1013
|
}
|
|
961
1014
|
if (command === "receipt") {
|
|
@@ -971,9 +1024,33 @@ Ready-to-paste post:
|
|
|
971
1024
|
${linkedInPost(input)}`);
|
|
972
1025
|
return;
|
|
973
1026
|
}
|
|
974
|
-
if (values.json) console.log(json(e, params, window));
|
|
975
|
-
|
|
976
|
-
|
|
1027
|
+
if (values.json) return void console.log(json(e, params, window, demo));
|
|
1028
|
+
if (label) process.stdout.write(label);
|
|
1029
|
+
if (by) console.log(breakdown(e, params, window, by));
|
|
1030
|
+
else console.log(summary(e, params, window, demo));
|
|
1031
|
+
}
|
|
1032
|
+
function parseArgsPlain(config) {
|
|
1033
|
+
try {
|
|
1034
|
+
return parseArgs(config);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
throw plainArgError(error) ?? error;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
async function sessionLine(path) {
|
|
1040
|
+
try {
|
|
1041
|
+
const file = path ?? JSON.parse(await readStdin()).transcript_path;
|
|
1042
|
+
if (!file) return `\u26A1 ${APP_NAME}: waiting for the first response`;
|
|
1043
|
+
const records = dedupe(claudeCodeAdapter.parse(await readFile3(file, "utf8")));
|
|
1044
|
+
return statusLine(estimate(records, defaultParams), defaultParams);
|
|
1045
|
+
} catch {
|
|
1046
|
+
return `\u26A1 ${APP_NAME}: no estimate yet`;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
async function readStdin() {
|
|
1050
|
+
if (process.stdin.isTTY) return "{}";
|
|
1051
|
+
let text = "";
|
|
1052
|
+
for await (const chunk of process.stdin) text += chunk;
|
|
1053
|
+
return text || "{}";
|
|
977
1054
|
}
|
|
978
1055
|
main().catch((error) => {
|
|
979
1056
|
console.error(error instanceof Error ? error.message : error);
|