Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
686 changes: 686 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"version": "1.0.0",
"description": "Production-ready hooks for Claude Code, plus an installable plugin marketplace: safety, automation, notifications, cost/observability, git, productivity, and memory",
"scripts": {
"test": "node --test tests/*.test.js plugins/**/tests/*.test.js"
"test": "node --test tests/*.test.js plugins/**/tests/*.test.js",
"build:brand": "node site/brand/build.mjs"
},
"keywords": [
"claude",
Expand All @@ -29,5 +30,8 @@
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"devDependencies": {
"sharp": "^0.35.4"
}
}
2 changes: 2 additions & 0 deletions plugins/cache-tax/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# cache-tax

![cache-tax: the comeback price of a cold prompt cache, before you pay it](../../site/brand/cache-tax-social-1280x640.png)

> The comeback price of a cold prompt cache, shown before you pay it.

Claude Code's main conversation rides a 1-hour prompt cache. Come back at minute 59 and the next message costs cents. Come back at minute 61 and the whole context is re-written at the cache-write rate, which on Fable 5.1 is 80x a cache read ($20 against $0.25 per million tokens). A 500k-token session re-cached cold is $10 at list price, and the message that triggers it is usually "good morning". Claude Code computes all of this and shows none of it at the moment you press Enter. This plugin does.
Expand Down
14 changes: 14 additions & 0 deletions site/brand/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Brand files

Generated by `build.mjs` from hand-written SVG; the PNGs are what to use, the SVGs are the source.

npm install && npm run build:brand

| File | Use |
|---|---|
| cache-tax-social-1280x640.png | README banner, blog post og:image |
| cache-tax-x-1200x675.png | X post card |
| cache-tax-icon-512.png, cache-tax-icon-warm-512.png | icon; the warm one marks the keep-warm state of the Mod form |
| cache-tax-favicon-32.png | favicon |

Palette and type follow site/index.html: void #0A0B0D, panel #101216, text #E7EAEE, block #FF5C46, amber #E3B341, plus an ice blue set for the cube. Served by Pages under https://hooks.karanbansal.in/brand/.
81 changes: 81 additions & 0 deletions site/brand/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import sharp from 'sharp'
import { writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'

const here = dirname(fileURLToPath(import.meta.url))

const C = {
void: '#0A0B0D', panel: '#101216', line: '#23272E', text: '#E7EAEE', dim: '#9BA2AC', faint: '#5C636D',
block: '#FF5C46', amber: '#E3B341', pass: '#3FB950',
iceTop: '#BFEFFF', iceLeft: '#5FC6F2', iceRight: '#2E8FC4', iceEdge: '#E9FBFF',
warmTop: '#FFE2A3', warmLeft: '#F0B54A', warmRight: '#C4831C', warmEdge: '#FFF3D6',
}
const MONO = "'JetBrains Mono', Menlo, monospace"
const SANS = "'Inter', 'Helvetica Neue', Arial, sans-serif"

// An isometric cube: top, left, right faces plus a bright edge, in a 100x100 box.
function cube(x, y, size, warm = false) {
const p = warm ? [C.warmTop, C.warmLeft, C.warmRight, C.warmEdge] : [C.iceTop, C.iceLeft, C.iceRight, C.iceEdge]
const s = size / 100
const pt = (px, py) => `${(x + px * s).toFixed(2)},${(y + py * s).toFixed(2)}`
return `
<g>
<polygon points="${pt(50, 8)} ${pt(92, 32)} ${pt(50, 56)} ${pt(8, 32)}" fill="${p[0]}"/>
<polygon points="${pt(8, 32)} ${pt(50, 56)} ${pt(50, 100)} ${pt(8, 76)}" fill="${p[1]}"/>
<polygon points="${pt(50, 56)} ${pt(92, 32)} ${pt(92, 76)} ${pt(50, 100)}" fill="${p[2]}"/>
<polyline points="${pt(8, 32)} ${pt(50, 56)} ${pt(92, 32)}" fill="none" stroke="${p[3]}" stroke-width="${(2.2 * s).toFixed(2)}" stroke-linejoin="round"/>
<line x1="${(x + 50 * s).toFixed(2)}" y1="${(y + 56 * s).toFixed(2)}" x2="${(x + 50 * s).toFixed(2)}" y2="${(y + 100 * s).toFixed(2)}" stroke="${p[3]}" stroke-width="${(2.2 * s).toFixed(2)}" opacity="0.7"/>
<polyline points="${pt(20, 26)} ${pt(50, 12)} ${pt(80, 26)}" fill="none" stroke="#FFFFFF" stroke-width="${(1.6 * s).toFixed(2)}" opacity="0.55" stroke-linecap="round"/>
</g>`
}

function icon(size, warm = false) {
const r = size * 0.22
const pad = size * 0.14
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
<rect width="${size}" height="${size}" rx="${r}" fill="${C.panel}"/>
<rect x="0.5" y="0.5" width="${size - 1}" height="${size - 1}" rx="${r}" fill="none" stroke="${C.line}"/>
${cube(pad, pad, size - 2 * pad, warm)}
</svg>`
}

function chip(x, y, label, color) {
const w = label.length * 13.2 + 34
return `<rect x="${x}" y="${y}" width="${w}" height="40" rx="8" fill="${C.void}" stroke="${color}" stroke-opacity="0.55"/>
<text x="${x + 17}" y="${y + 27}" font-family="${MONO}" font-size="21" fill="${color}">${label}</text>`
}

function card(w, h) {
const cubeSize = h * 0.42
const cx = w * 0.075, cy = (h - cubeSize) / 2 - h * 0.04
const tx = cx + cubeSize + w * 0.06
const base = h * 0.40
const chips = [['read $0.25/MTok', C.iceLeft], ['write $20/MTok', C.block], ['80x', C.amber]]
let chipX = tx, chipSvg = ''
for (const [l, c] of chips) { chipSvg += chip(chipX, base + h * 0.245, l, c); chipX += l.length * 13.2 + 34 + 14 }
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
<defs>
<radialGradient id="g" cx="0.18" cy="0.5" r="0.7"><stop offset="0" stop-color="#17303F"/><stop offset="1" stop-color="${C.void}"/></radialGradient>
</defs>
<rect width="${w}" height="${h}" fill="url(#g)"/>
${cube(cx, cy, cubeSize)}
<text x="${tx}" y="${base}" font-family="${MONO}" font-size="${h * 0.15}" font-weight="700" fill="${C.text}">cache-tax</text>
<text x="${tx}" y="${base + h * 0.095}" font-family="${SANS}" font-size="${h * 0.046}" fill="${C.dim}">The comeback price of a cold prompt cache,</text>
<text x="${tx}" y="${base + h * 0.155}" font-family="${SANS}" font-size="${h * 0.046}" fill="${C.dim}">before you pay it.</text>
${chipSvg}
<text x="${tx}" y="${h - h * 0.075}" font-family="${MONO}" font-size="${h * 0.03}" fill="${C.faint}">a Claude Code hook and a Claude Mod · hooks.karanbansal.in</text>
</svg>`
}

const out = async (name, svg, opts = {}) => {
svg = svg.replace(/[ \t]+$/gm, '')
writeFileSync(join(here, `${name}.svg`), svg)
await sharp(Buffer.from(svg), { density: 192 }).resize(opts.w, opts.h).png().toFile(join(here, `${name}.png`))
console.log('wrote', name + '.png')
}
await out('cache-tax-icon-512', icon(512), { w: 512, h: 512 })
await out('cache-tax-icon-warm-512', icon(512, true), { w: 512, h: 512 })
await out('cache-tax-favicon-32', icon(32), { w: 32, h: 32 })
await out('cache-tax-social-1280x640', card(1280, 640), { w: 1280, h: 640 })
await out('cache-tax-x-1200x675', card(1200, 675), { w: 1200, h: 675 })
Binary file added site/brand/cache-tax-favicon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions site/brand/cache-tax-favicon-32.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/brand/cache-tax-icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions site/brand/cache-tax-icon-512.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/brand/cache-tax-icon-warm-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions site/brand/cache-tax-icon-warm-512.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/brand/cache-tax-social-1280x640.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions site/brand/cache-tax-social-1280x640.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/brand/cache-tax-x-1200x675.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions site/brand/cache-tax-x-1200x675.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading