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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +104 -27
  3. 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.0";
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: `${num(range.central / scale)} ${unit}`,
34
- range: `${num(range.low / scale)} \u2013 ${num(range.high / scale)}`,
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 ${round(miles)} miles` });
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 ${round(days)} days` });
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: `charging a phone about ${round(charges)} times` });
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 ${round(days)} days` });
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)} used about ${t.central} of electricity (range ${t.range} ${t.unit}), roughly ${first}. Estimated locally with ${APP_NAME}, method v${params.version}: ${repoShort}`;
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}), roughly ${everyday(e.total.central / 1e3, params)}.`,
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}), roughly ${everyday(e.total.central / 1e3, params)}.`,
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");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=>({"&":"&amp;","<":"&lt;",">":"&gt;",\'"\':"&quot;","\'":"&#39;"})[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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");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=>({"&":"&amp;","<":"&lt;",">":"&gt;",\'"\':"&quot;","\'":"&#39;"})[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
- lines.push("", ` How this is estimated: ${APP_NAME} --method \xB7 Share: ${APP_NAME} receipt`);
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
- --path <dir> Read logs from this folder instead
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
- -h, --help Show this help`;
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 } = parseArgs({
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 window = parseSince(values.since ?? (command === "history" ? "all" : "30d"), /* @__PURE__ */ new Date());
924
- const logs = await loadLogs(values.path ? [values.path] : defaultLogRoots());
925
- let rows = rollup(logs.records);
926
- if (!values["no-history"]) {
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
- else if (by) console.log(breakdown(e, params, window, by));
976
- else console.log(summary(e, params, window));
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccwatts",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Estimate the electricity your Claude Code usage consumes.",
5
5
  "type": "module",
6
6
  "bin": {