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
22 changes: 12 additions & 10 deletions packages/wrapped/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,26 @@ your machine.**

`seshy-wrapped` reads the session logs your coding agents already keep on
disk — **Claude Code, Codex, Gemini CLI, OpenCode** (plus pi and Droid) — and
renders a synthwave stat card: how many prompts you fired, how many lines of
code it wrote for you, how many times you swore at it, how many times it
caved and said *"you're right"*, your 3am prompt count, and a
tongue-in-cheek **delegation grade**.
renders a stat card: how many prompts you fired, how many lines of code it
wrote for you, how many times you swore at it, how many times it caved and
said *"you're right"*, your 3am prompt count, a **yearbook title** earned
from your own numbers, and a tongue-in-cheek **delegation grade**.

![example card](docs/example.png)

*(Example rendered from the synthetic test fixtures — run it yourself for
your real numbers.)*

## The hero move
## Run it

```bash
npx seshy-wrapped --model fable
npx seshy-wrapped
```

Filters to work answered by Fable 5 and retitles the card `FABLE 5 · 2026`.
Works for any model: `--model opus-4-8`, `--model gpt-5.4`.
Run bare and it asks what to wrap: **fable week** (Jul 1–7, Fable 5 only —
the default), the **last 30 days**, or **all time**. Any scoping flag answers
the question in advance — `--model opus-4-8`, `--since 2026-01-01`,
`--period year`.

## Cuts and themes

Expand All @@ -48,7 +50,7 @@ npx seshy-wrapped --all-cuts --theme terminal # every cut, terminal look
| `chaos` | times you said "actually" |
| `agent` | times it said "let me…" |

Themes: `sunset` (default), `terminal`, `starfield`, `receipt`, `billboard`, `crt`.
Themes: `dusk` (default — the launch film's road scene), `sunset`, `terminal`, `starfield`, `receipt`, `billboard`, `crt`.

## All options

Expand All @@ -58,7 +60,7 @@ Themes: `sunset` (default), `terminal`, `starfield`, `receipt`, `billboard`, `cr
--since <date> only sessions after YYYY-MM-DD
-p, --period <win> week | month | year | all
-c, --cut <cut> classic | machine | chaos | agent
-t, --theme <look> sunset | terminal | starfield | receipt | billboard | crt
-t, --theme <look> dusk | sunset | terminal | starfield | receipt | billboard | crt
--all-cuts render every cut in the chosen theme
-h, --handle <@you> handle printed on the card
--json emit stats.json only
Expand Down
Binary file modified packages/wrapped/docs/example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/wrapped/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "seshy-wrapped",
"version": "0.1.0",
"version": "0.2.0",
"description": "Your AI-coding life, on a card. 100% local — reads the session logs your coding agents already keep and renders shareable stat cards.",
"type": "module",
"bin": {
Expand Down
Binary file added packages/wrapped/src/assets/dusk-road.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 12 additions & 8 deletions packages/wrapped/src/card.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,15 @@ function grid(hy) {
}

function tile(x, y, w, h, value, label, accent) {
// Value and label shrink to their tile — the 2x4 grid runs narrower than
// the old 2x3, and labels like "words back per word you typed" must fit.
const vSize = Math.min(48, Math.floor((w - 36) / (String(value).length * 0.56)));
const lSize = Math.min(17, Math.floor((w - 34) / (label.length * 0.47)));
return `
<g>
<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="20" fill="url(#tile)" stroke="${accent}" stroke-opacity="0.55" stroke-width="1.5"/>
<text x="${x + 26}" y="${y + 74}" font-family="${DISPLAY}" font-size="60" font-weight="700" fill="${accent}" filter="url(#glow)">${esc(value)}</text>
<text x="${x + 27}" y="${y + h - 26}" font-family="${LABEL}" font-size="23" fill="${C.lav}">${esc(label)}</text>
<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="18" fill="url(#tile)" stroke="${accent}" stroke-opacity="0.55" stroke-width="1.5"/>
<text x="${x + 20}" y="${y + 70}" font-family="${DISPLAY}" font-size="${vSize}" font-weight="700" fill="${accent}" filter="url(#glow)">${esc(value)}</text>
<text x="${x + 20}" y="${y + h - 24}" font-family="${LABEL}" font-size="${lSize}" fill="${C.lav}">${esc(label)}</text>
</g>`;
}

Expand All @@ -106,17 +110,17 @@ function sunsetTheme(report, opts) {
const hy = 360;
const heroY = 636;
const gridTop = 772;
const tileW = (W - PAD * 2 - 28 * 2) / 3;
const tileW = (W - PAD * 2 - 20 * 3) / 4;
const tileH = 150;
const row2 = gridTop + tileH + 26;
const sigY = row2 + tileH + 62;
const badgeCy = row2 + tileH + 12 + 60;

let tileSVG = '';
m.tiles.slice(0, 6).forEach((t, i) => {
const col = i % 3;
const row = Math.floor(i / 3);
const x = PAD + col * (tileW + 28);
m.tiles.slice(0, 8).forEach((t, i) => {
const col = i % 4;
const row = Math.floor(i / 4);
const x = PAD + col * (tileW + 20);
const y = gridTop + row * (tileH + 26);
tileSVG += tile(x, y, tileW, tileH, t.value, t.label, t.accent);
});
Expand Down
19 changes: 7 additions & 12 deletions packages/wrapped/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { execFileSync } from 'node:child_process';
import { createInterface } from 'node:readline';
import { collect, stableStringify } from './pipeline.js';
import { KNOWN_AGENTS } from './discover.js';
import { CUTS, fmt, pickHeadline, signature, prettyModel } from './copy.js';
import { CUTS, fmt, pickHeadline, pickTiles, signature, prettyModel } from './copy.js';
import { yearbook } from './yearbook.js';

// ANSI helpers — degrade to plain text when not a TTY.
Expand All @@ -34,7 +34,7 @@ const SCAN_LINES = [
function parseArgs(argv) {
const o = {
agent: 'all', model: '', since: 0, until: 0, window: 'all',
cut: 'classic', theme: 'sunset', allCuts: false, handle: '',
cut: 'classic', theme: 'dusk', allCuts: false, handle: '',
json: false, out: '', open: false, help: false, now: 0,
};
for (let i = 0; i < argv.length; i++) {
Expand Down Expand Up @@ -75,7 +75,7 @@ Options:
--since <date> only sessions after YYYY-MM-DD
-p, --period <win> week | month | year | all (default: all)
-c, --cut <cut> ${CUTS.join(' | ')} (default: classic)
-t, --theme <look> sunset | terminal | starfield | receipt | billboard | crt
-t, --theme <look> dusk | sunset | terminal | starfield | receipt | billboard | crt (default: dusk)
--all-cuts render every cut in the chosen theme
-h, --handle <@you> handle printed on the card (default: none)
--json emit stats.json to stdout only, no images
Expand Down Expand Up @@ -234,7 +234,7 @@ function validate(o) {
if (o.unknown) return `Unknown option "${o.unknown}". Run with --help to see every flag.`;
if (!['all', ...KNOWN_AGENTS].includes(o.agent)) return `Unknown agent "${o.agent}". Pick one of: ${KNOWN_AGENTS.join(', ')}, all.`;
if (!CUTS.includes(o.cut)) return `Unknown cut "${o.cut}". Pick one of: ${CUTS.join(', ')}.`;
const themes = ['sunset', 'terminal', 'starfield', 'receipt', 'billboard', 'crt'];
const themes = ['dusk', 'sunset', 'terminal', 'starfield', 'receipt', 'billboard', 'crt'];
if (!themes.includes(o.theme)) return `Unknown theme "${o.theme}". Pick one of: ${themes.join(', ')}.`;
if (!['all', 'week', 'month', 'year'].includes(o.window) && !o.since) return `Could not parse --since/--period "${o.window}". Use YYYY-MM-DD or week|month|year|all.`;
return '';
Expand All @@ -257,16 +257,11 @@ function printSummary(report, o) {
const label = report.meta.model ? prettyModel(report.deep.topModel.id || report.meta.model) : null;

const title = `SESHY WRAPPED${label ? ` · ${label.toUpperCase()}` : ''}${report.meta.rookie ? ' · ROOKIE CARD' : ''}`;
// The box mirrors the card: the same gated tile pick, so the screenshot
// and the PNG never disagree.
const rows = [
[head.value, head.label],
[fmt(t.sessions), `sessions across ${fmt(t.projects)} projects`],
[`${Math.round(report.time.activeHours)}h`, 'agent hours on the clock'],
// Headless runs sit outside the interactive session count above, so the
// line names its own universe — screenshot auditors will do the math.
[fmt(report.automation.headless), `headless runs alongside (${report.automation.headlessShare}% of all activity)`],
[fmt(report.tics.youreRight), `"you're right"s pried out of it`],
[fmt(report.you.fbombs), 'F-bombs dropped'],
[fmt(report.you.nightOwl), 'prompts after midnight'],
...pickTiles(report, o.cut).map((tl) => [tl.value, tl.label]),
[report.grade.letter, `delegation grade — ${report.grade.why}`],
];

Expand Down
27 changes: 19 additions & 8 deletions packages/wrapped/src/copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function pickHeadline(report, cut = 'classic') {
// render. Each entry carries its raw number for the gate. Exported so
// tooling (stat pickers, audits) can enumerate the full candidate pool.
export function tilePool(report, cut) {
const { totals, tics, you, awards, machine, alt, automation, time } = report;
const { totals, tics, you, awards, machine, alt, automation, time, deep } = report;
if (cut === 'machine') {
const ratio = totals.userWords ? totals.assistantWords / totals.userWords : 0;
return [
Expand Down Expand Up @@ -98,13 +98,22 @@ export function tilePool(report, cut) {
{ raw: tics.youreRight, value: fmt(tics.youreRight), label: 'times it caved: "you’re right"', accent: C.yellow, min: 3 },
];
}
// The default card leads with leverage (projects, lines, commits, cache,
// ratio) and closes with the funny scorelines. F-bombs may show as a zero,
// but only when there's enough volume behind it for the zero to be a flex.
const ratio = totals.userWords ? totals.assistantWords / totals.userWords : 0;
return [
{ raw: totals.projects, value: fmt(totals.projects), label: 'projects touched', accent: C.cyan, keep: true },
{ raw: machine.linesWritten, value: fmt(machine.linesWritten), label: 'lines of code it wrote', accent: C.pink, min: 100 },
{ raw: deep.gitCommits, value: fmt(deep.gitCommits), label: 'git commits landed', accent: C.yellow, min: 5 },
{ raw: machine.cacheRead, value: fmt(machine.cacheRead), label: 'cached tokens re-read', accent: C.magenta, min: 1000 },
{ raw: ratio, value: ratio >= 1 ? `${Math.round(ratio)}x` : '—', label: 'words back per word you typed', accent: C.violet, min: 2 },
{ raw: you.nightOwl, value: fmt(you.nightOwl), label: 'prompts after midnight', accent: C.aqua, min: 5 },
{ raw: tics.agentSorry, value: `${fmt(tics.agentSorry)} : ${fmt(you.sorry)}`, label: 'apologies: extracted vs issued', accent: C.orange, min: 2 },
{ raw: you.fbombs, value: fmt(you.fbombs), label: 'F-bombs you dropped', accent: C.orange, keep: you.fbombs > 0 || totals.prompts >= 200 },
{ raw: totals.sessions, value: fmt(totals.sessions), label: 'coding sessions', accent: C.cyan, keep: true },
{ raw: totals.assistantWords, value: fmt(totals.assistantWords), label: 'words written for you', accent: C.pink, keep: true },
{ raw: totals.assistantWords, value: fmt(totals.assistantWords), label: 'words written for you', accent: C.pink, min: 200 },
{ raw: tics.youreRight, value: fmt(tics.youreRight), label: 'times it caved: "you’re right"', accent: C.yellow, min: 3 },
{ raw: you.fbombs, value: fmt(you.fbombs), label: 'F-bombs you dropped', accent: C.orange, min: 1 },
{ raw: tics.agentSorry, value: fmt(tics.agentSorry), label: 'apologies you extracted', accent: C.violet, min: 2 },
{ raw: you.nightOwl, value: fmt(you.nightOwl), label: 'prompts after midnight', accent: C.aqua, min: 5 },
{ raw: awards.longestSession.msgs, value: fmt(awards.longestSession.msgs), label: 'prompts in one marathon session', accent: C.magenta, min: 40 },
{ raw: totals.interrupts, value: fmt(totals.interrupts), label: 'rage-quit interruptions', accent: C.pink, min: 5 },
{ raw: you.youWrong, value: fmt(you.youWrong), label: 'times you told it it was wrong', accent: C.orange, min: 3 },
Expand All @@ -115,18 +124,20 @@ export function tilePool(report, cut) {
];
}

export const TILE_COUNT = 8;

export function pickTiles(report, cut = 'classic') {
const pool = tilePool(report, cut);
const chosen = [];
for (const t of pool) {
if (chosen.length >= 6) break;
if (chosen.length >= TILE_COUNT) break;
if (t.keep || t.raw >= (t.min || 0)) chosen.push(t);
}
for (const t of pool) {
if (chosen.length >= 6) break;
if (chosen.length >= TILE_COUNT) break;
if (!chosen.includes(t)) chosen.push(t);
}
return chosen.slice(0, 6);
return chosen.slice(0, TILE_COUNT);
}

// --- The punchline picker. ------------------------------------------------
Expand Down
Loading
Loading