mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-07-22 09:07:35 -03:00
Merge branch 'main' into add-ui-theme-designer
This commit is contained in:
commit
d3d3830978
@ -173,7 +173,7 @@
|
||||
"url": "https://github.com/amd/skills.git",
|
||||
"path": "skills",
|
||||
"ref": "main",
|
||||
"sha": "7ef8dd99ffd6d82208f1a0ae211a782ec2edee82"
|
||||
"sha": "228980933abd97a8aa4a2656c8ea3ae9e8b590c7"
|
||||
},
|
||||
"strict": false,
|
||||
"skills": [
|
||||
@ -207,7 +207,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/apolloio/apollo-mcp-plugin.git",
|
||||
"sha": "d0676ea9bba52b0f97d810dade775de059ccd2bd"
|
||||
"sha": "2adde980e45f421b7e9383d92870455627936bce"
|
||||
},
|
||||
"homepage": "https://www.apollo.io/"
|
||||
},
|
||||
@ -253,7 +253,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/astronomer/agents.git",
|
||||
"sha": "5432198caa8ef9ec33218124bcd44016db2d31bf"
|
||||
"sha": "87d780cd5dc85a25c7ceb1f1720031fa4e48f781"
|
||||
},
|
||||
"homepage": "https://github.com/astronomer/agents"
|
||||
},
|
||||
@ -306,7 +306,7 @@
|
||||
"url": "https://github.com/auth0/agent-skills.git",
|
||||
"path": "plugins/auth0",
|
||||
"ref": "main",
|
||||
"sha": "0d426960256fe7e54e05495fab64208e1924f23b"
|
||||
"sha": "de929462cae879bab1c880fc4b9ad3645bf8588b"
|
||||
},
|
||||
"homepage": "https://auth0.com"
|
||||
},
|
||||
@ -367,7 +367,7 @@
|
||||
"url": "https://github.com/aws/agent-toolkit-for-aws.git",
|
||||
"path": "plugins/aws-core",
|
||||
"ref": "main",
|
||||
"sha": "36f16570de2015c0f0ce94ba9e391bd703c9ffb7"
|
||||
"sha": "eb96852087e2b2757b67d358d6d38c17c5c28205"
|
||||
},
|
||||
"homepage": "https://github.com/aws/agent-toolkit-for-aws"
|
||||
},
|
||||
@ -477,7 +477,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/base44/skills.git",
|
||||
"sha": "876f080f34efcc17fc606774feccecd6b61f44a7"
|
||||
"sha": "40e548021670ed409d3bae15601726cf652d846f"
|
||||
},
|
||||
"homepage": "https://docs.base44.com"
|
||||
},
|
||||
@ -597,7 +597,7 @@
|
||||
"url": "https://github.com/carta/plugins.git",
|
||||
"path": "plugins/carta-cap-table",
|
||||
"ref": "main",
|
||||
"sha": "4f46ba0181f4d07b45b7fd8e51208c9699afd566"
|
||||
"sha": "a007723c7149acab9f9357eaa74b98635bb63934"
|
||||
},
|
||||
"homepage": "https://carta.com"
|
||||
},
|
||||
@ -629,7 +629,7 @@
|
||||
"url": "https://github.com/carta/plugins.git",
|
||||
"path": "plugins/carta-investors",
|
||||
"ref": "main",
|
||||
"sha": "70c61a2abbc976280e102edbc696cdb84da6a7f1"
|
||||
"sha": "f2c2cbb11ed4dfabf88c59d61827b4b1aefb0611"
|
||||
},
|
||||
"homepage": "https://carta.com"
|
||||
},
|
||||
@ -656,7 +656,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/ChromeDevTools/chrome-devtools-mcp.git",
|
||||
"sha": "76fd2424984827802867672fcc8d0e0036f4a3af"
|
||||
"sha": "dcbaf495183a34d9bb0e3d9e8f3e566de6b425f8"
|
||||
},
|
||||
"homepage": "https://github.com/ChromeDevTools/chrome-devtools-mcp"
|
||||
},
|
||||
@ -885,7 +885,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/coderabbitai/skills.git",
|
||||
"sha": "2fd091d9582deccf19929e21f934921d0e8f686e"
|
||||
"sha": "c61c4908d36dda6fb08a343cc8b19c59fbb47de3"
|
||||
},
|
||||
"homepage": "https://github.com/coderabbitai/skills"
|
||||
},
|
||||
@ -899,7 +899,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/CodSpeedHQ/codspeed.git",
|
||||
"sha": "5c27231b94437287b71ab505faba533d06e0cb24"
|
||||
"sha": "26fb4b598f08faee386a8fa4212b8350c33dc013"
|
||||
},
|
||||
"homepage": "https://codspeed.io"
|
||||
},
|
||||
@ -1027,7 +1027,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/dash0hq/dash0-agent-plugin.git",
|
||||
"sha": "8a0a41996dd5de9b7ef0f624347584d76c1fe48d"
|
||||
"sha": "d7d95883b2e65290c6c08bf4404696afae4b26be"
|
||||
},
|
||||
"homepage": "https://dash0.com/"
|
||||
},
|
||||
@ -1038,7 +1038,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/astronomer/agents.git",
|
||||
"sha": "5432198caa8ef9ec33218124bcd44016db2d31bf"
|
||||
"sha": "87d780cd5dc85a25c7ceb1f1720031fa4e48f781"
|
||||
},
|
||||
"homepage": "https://github.com/astronomer/agents"
|
||||
},
|
||||
@ -1062,7 +1062,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/astronomer/agents.git",
|
||||
"sha": "5432198caa8ef9ec33218124bcd44016db2d31bf"
|
||||
"sha": "87d780cd5dc85a25c7ceb1f1720031fa4e48f781"
|
||||
},
|
||||
"homepage": "https://github.com/astronomer/agents"
|
||||
},
|
||||
@ -1175,7 +1175,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/confident-ai/deepeval.git",
|
||||
"sha": "58c9ef78a4634ba119c7d2cc145f5cf9aeb24524"
|
||||
"sha": "6bb1881c8caaae3a0348ad9040d506df7332be80"
|
||||
},
|
||||
"homepage": "https://github.com/confident-ai/deepeval"
|
||||
},
|
||||
@ -1492,7 +1492,7 @@
|
||||
"url": "https://github.com/grafana/ai-marketplace.git",
|
||||
"path": "plugins/grafana-cloud-mcp",
|
||||
"ref": "main",
|
||||
"sha": "a5c72f2d74c640e9675eb0249526447968535015"
|
||||
"sha": "12be5634a492f73c189d466c5449d09b853ad7a4"
|
||||
},
|
||||
"homepage": "https://grafana.com"
|
||||
},
|
||||
@ -1582,7 +1582,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/hunter-io/claude-plugin.git",
|
||||
"sha": "d2bab5bc4879abf88e7e8fc11891375c217fecfc"
|
||||
"sha": "2a89a5ec3821eb0a4c2eb3412729b154f2ac8ebb"
|
||||
},
|
||||
"homepage": "https://hunter.io"
|
||||
},
|
||||
@ -1596,7 +1596,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/heygen-com/hyperframes.git",
|
||||
"sha": "735128a61a152267a00b99defbd290f489482cbc"
|
||||
"sha": "911b332bb2131f1b3fc4abbea563bf0c70d165c4"
|
||||
},
|
||||
"homepage": "https://hyperframes.heygen.com"
|
||||
},
|
||||
@ -2146,6 +2146,20 @@
|
||||
],
|
||||
"homepage": "https://github.com/oracle/netsuite-suitecloud-sdk"
|
||||
},
|
||||
{
|
||||
"name": "newrelic",
|
||||
"description": "New Relic observability intelligence for Claude Code. Investigate APM performance, analyze cloud costs, debug Kubernetes, write NRQL queries, and respond to alerts — all from your terminal using New Relic telemetry data.",
|
||||
"author": {
|
||||
"name": "New Relic"
|
||||
},
|
||||
"category": "monitoring",
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/newrelic/claude-code-plugin.git",
|
||||
"sha": "f8e5f8b62139072a22153ec92de1c0cc1af2f56a"
|
||||
},
|
||||
"homepage": "https://newrelic.com"
|
||||
},
|
||||
{
|
||||
"name": "nightvision",
|
||||
"description": "Skills for working with NightVision, a DAST and API Discovery platform that finds exploitable vulnerabilities in web applications and REST APIs",
|
||||
@ -2391,7 +2405,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/PostHog/ai-plugin.git",
|
||||
"sha": "936b2d2c38f78a36c563f68da8df4a5f2c0c008f"
|
||||
"sha": "4bdfca8c6d58b8c9a303374f5f93485b8d9281fa"
|
||||
},
|
||||
"homepage": "https://posthog.com/docs/model-context-protocol"
|
||||
},
|
||||
@ -2511,7 +2525,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/qdrant/skills.git",
|
||||
"sha": "e043b7314279f3ff6623c5f0c95c4471cdf0c5f8"
|
||||
"sha": "240854b3068b6a9cd1ebf75ab008b5758071052b"
|
||||
},
|
||||
"homepage": "https://skills.qdrant.tech"
|
||||
},
|
||||
@ -2592,7 +2606,7 @@
|
||||
},
|
||||
{
|
||||
"name": "receipts",
|
||||
"description": "Generate a personal Claude Code impact report from your local ~/.claude/projects transcripts, cross-referenced against your local git history — what you shipped, which projects it went to, and each project's share of your usage — for justifying usage to a manager or a self-review. Reads your session transcripts and runs read-only git commands in the projects they mention; mining is local and only a small aggregate summary (counts and project names) is sent to write the report, which is saved to your home directory and published nowhere.",
|
||||
"description": "A personal Claude Code impact report for justifying your usage to a manager or a self-review: what you shipped, which projects it went to, and each project's share of your usage. Reads your ~/.claude/projects transcripts and runs read-only git locally; only counts and project names are sent to write it up, and the report stays on your machine.",
|
||||
"author": {
|
||||
"name": "Anthropic",
|
||||
"email": "support@anthropic.com"
|
||||
@ -2842,7 +2856,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/SAP/mdk-mcp-server.git",
|
||||
"sha": "65c088d14b2078e5409470838a93adb37528874e"
|
||||
"sha": "006c41c8c0962c1cf83e0a6416598595b8833ae0"
|
||||
},
|
||||
"homepage": "https://help.sap.com/docs/MDK"
|
||||
},
|
||||
@ -2858,7 +2872,7 @@
|
||||
"url": "https://github.com/spotify/save-to-spotify.git",
|
||||
"path": "plugin",
|
||||
"ref": "main",
|
||||
"sha": "ce79a3d38b932b351530e204ec856419f9ba64b6"
|
||||
"sha": "9d05c8cd8c8559552f84e27fe30f46b7693ce69a"
|
||||
},
|
||||
"homepage": "https://github.com/spotify/save-to-spotify"
|
||||
},
|
||||
@ -2882,7 +2896,7 @@
|
||||
"source": "git-subdir",
|
||||
"url": "https://github.com/semgrep/mcp-marketplace.git",
|
||||
"path": "plugin",
|
||||
"sha": "63fac121296f3f102651de6beb7c15649c8b11d3"
|
||||
"sha": "7ae38e4bec51877cec71fcacc5857d746fb8b468"
|
||||
},
|
||||
"homepage": "https://github.com/semgrep/mcp-marketplace.git"
|
||||
},
|
||||
@ -3059,7 +3073,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/spotify/ads-claude-plugin.git",
|
||||
"sha": "aedf7ed87312891ef2e357c02b9c22fcca8907b4"
|
||||
"sha": "6f0b741c8dabb92492a74e6e6893f81e1b94b038"
|
||||
},
|
||||
"homepage": "https://github.com/spotify/ads-claude-plugin"
|
||||
},
|
||||
@ -3095,7 +3109,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/supabase-community/supabase-plugin.git",
|
||||
"sha": "3b3184501844df3d2e0accac129fdcae73f4bf95"
|
||||
"sha": "5d74232687a0a82184ecd4266ea07372edb43f4b"
|
||||
},
|
||||
"homepage": "https://github.com/supabase-community/supabase-plugin"
|
||||
},
|
||||
@ -3421,7 +3435,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/wix/skills.git",
|
||||
"sha": "b9934f834685e8fb5d47df02dee22e6cc69cc53d"
|
||||
"sha": "5d7b1e90215fc5be8b62980f185f235e4d3bfb39"
|
||||
},
|
||||
"homepage": "https://dev.wix.com/docs/wix-cli/guides/development/about-wix-skills"
|
||||
},
|
||||
@ -3542,7 +3556,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/langfuse/skills.git",
|
||||
"sha": "21033b441816025d52e9cff940aeb61680618ea3"
|
||||
"sha": "abe69fac6b0e0df94605dc5ae162da4726d0fb78"
|
||||
},
|
||||
"homepage": "https://langfuse.com"
|
||||
},
|
||||
|
||||
9
.github/workflows/validate-plugins.yml
vendored
9
.github/workflows/validate-plugins.yml
vendored
@ -19,6 +19,15 @@ on:
|
||||
# pull_request and the required check would sit "Expected" forever (a dispatch
|
||||
# check run isn't associated with the PR, so it can't satisfy the gate either).
|
||||
- '.github/policy/**'
|
||||
# And once more for a plugin's own docs: a PR that only edits a README or
|
||||
# adds a screenshot matches nothing above, so the required check never
|
||||
# reports and the PR can't be merged. Spelled out per level because `*`
|
||||
# doesn't cross a `/` — plugins live at plugins/<name>/, so `*/README.md`
|
||||
# would not match one.
|
||||
- 'plugins/*/README.md'
|
||||
- 'plugins/*/assets/**'
|
||||
- 'external_plugins/*/README.md'
|
||||
- 'external_plugins/*/assets/**'
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
|
||||
@ -4,6 +4,20 @@ Generate a personal Claude Code impact report — "receipts" — from your own
|
||||
session transcripts, for the conversation where someone asks what all this
|
||||
Claude Code usage is actually buying.
|
||||
|
||||
<img src="assets/sample-receipt.png" width="400"
|
||||
alt="A printed-receipt-styled report headed 'Claude Code — usage receipt, Morgan Lunt', covering 2026-06-21 to 2026-07-20, active on 28 of 30 days. It lists 40 sessions, 64 prompts, 14 files touched, 3,120 lines touched, 12 commits carrying that work and 7 PRs opened, headlined as 19 commits and PRs shipped. A by-project table gives 'Research and investigation (no project)' 53% of spend with no files touched, then acme-api 29%, acme-web 10%, billing-service 5%, infra-terraform 3%, and a plain ~/notes directory 1%. Footnotes explain which columns do not sum, and an Export CSV button sits above a barcode.">
|
||||
|
||||
*Sample output — the projects and numbers are invented. It's rendered from a
|
||||
synthetic corpus, not from anyone's real usage.*
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
/plugin install receipts@claude-plugins-official
|
||||
```
|
||||
|
||||
## Use
|
||||
|
||||
```
|
||||
/receipts # last 30 days (default)
|
||||
/receipts week # last 7 days
|
||||
@ -12,115 +26,52 @@ Claude Code usage is actually buying.
|
||||
/receipts for myrepo # scope to one project
|
||||
```
|
||||
|
||||
You get two files in your home directory: a markdown report to paste into a
|
||||
doc or a review, and a self-contained HTML receipt to open or attach. The
|
||||
receipt has an **Export CSV** button for the by-project table, and prints to a
|
||||
clean PDF.
|
||||
Two files land in your home directory: a markdown report to paste into a doc or
|
||||
a review, and a self-contained HTML receipt to open or attach. The receipt has
|
||||
an **Export CSV** button and prints to a clean PDF. Nothing is published
|
||||
anywhere.
|
||||
|
||||
## What it reports
|
||||
Takes a few seconds — about 1s for a week, 5s for a year.
|
||||
|
||||
## What you get
|
||||
|
||||
- **What you shipped** — files and lines touched, commits carrying that work,
|
||||
PRs opened.
|
||||
- **By project** — sessions, active days, and each project's share of your
|
||||
total compute.
|
||||
usage. Work outside a repo is named for its directory; sessions that touched
|
||||
no files at all (web searches, chat tools, dashboards) show as
|
||||
*Research & investigation (no project)*, which for a lot of people is the
|
||||
biggest row.
|
||||
- **Framing for a manager** — how to present the above without overclaiming.
|
||||
|
||||
## What counts
|
||||
|
||||
The report's universe is **work you did with Claude Code**, mapped to the
|
||||
project you did it on. Two consequences worth knowing before you read a number:
|
||||
|
||||
**Claude Code's own machinery isn't your work.** The agent's scratchpad, its
|
||||
per-session tool output, and `~/.claude` are excluded. On a real 30-day corpus
|
||||
that removed 82% of the raw "lines touched" figure — files Claude wrote to talk
|
||||
to itself, which no one shipped.
|
||||
|
||||
**A project is where work landed, not where your shell was.** Sessions are
|
||||
attributed to the projects their file operations touched (reads included —
|
||||
reading a repo to answer a question is work in that repo), resolved to the git
|
||||
root, or to the containing directory when it isn't a repo. Work outside a repo
|
||||
still counts; it's named for its directory. Subagents share their parent's
|
||||
session, so their work lands on the same project — there's no "delegated"
|
||||
category, because delegation is a mechanism, not a kind of work.
|
||||
|
||||
Sessions that touched no files and didn't run in a repo — web searches, Slack
|
||||
reads, dashboard queries — land in **Research & investigation (no project)**.
|
||||
That row is often the biggest one. It's real work that genuinely has no home on
|
||||
disk, and naming it beats inventing a project for it.
|
||||
|
||||
## Design notes
|
||||
|
||||
**No dollar figures, anywhere.** A cost computed from local token counts is
|
||||
inferred, not measured, and won't match your actual bill. Presenting one
|
||||
invites the "that can't be right" reaction that discredits everything else in
|
||||
the report. Spend appears only as relative percentages.
|
||||
|
||||
**No invented "hours saved."** There's no baseline in local data to compute a
|
||||
counterfactual from, and a fabricated multiplier undermines the real numbers
|
||||
sitting next to it. The report deliberately leaves room for you to add
|
||||
concrete wins by hand — those land better than any aggregate anyway.
|
||||
|
||||
**No breakdown of spend by activity.** "38% of your compute went to reading
|
||||
code" is the chart everyone wants and the data can't support. A turn's cost is
|
||||
~90% context handling, half of it re-reading what earlier turns added, so
|
||||
charging it to whichever tool fired that turn is a modeling choice rather than
|
||||
a measurement — and the choice decides the answer. On one real month, three
|
||||
equally defensible weightings put web search at 11%, 28% or 51%. Spend appears
|
||||
once, per project, where it divides a real quantity by a real fact and the
|
||||
ranking holds whichever weighting you pick.
|
||||
|
||||
**Careful claims.** Commits are counted only when they were authored under the
|
||||
identity git uses in that repo *and* their changed files include something
|
||||
Claude Code touched. Both tests have to pass, which is what keeps a snapshot
|
||||
cron out — it commits under your name but never touches the files Claude
|
||||
edited — while still counting the commit you made by hand after Claude wrote
|
||||
the code. The gap it leaves: a repo configured with a *shared* identity (a
|
||||
release bot's, say) makes that bot "you" for that repo, so a bot commit
|
||||
touching a file Claude also edited would count. Rare, and the alternative —
|
||||
reading only your global identity — silently zeroes the commit count for
|
||||
anyone using git's standard `includeIf` work/personal split, which is far more
|
||||
common.
|
||||
|
||||
Lines are "touched", not "written". Sessions, active days and commits are all
|
||||
marked as columns that don't sum: a session spanning two projects is genuinely
|
||||
in both, and worktrees of one repo share commits.
|
||||
A few things the report deliberately won't tell you: no dollar figures, no
|
||||
"hours saved", and no breakdown of spend by activity. Each of those would be a
|
||||
guess dressed up as a measurement, and one bad number discredits the rest of
|
||||
the page. What's left is meant to survive someone pushing back on it.
|
||||
|
||||
## Privacy
|
||||
|
||||
Mining is a local Node script — file I/O and `git`, no network calls.
|
||||
Everything is read locally — file I/O and read-only `git`, no network calls.
|
||||
|
||||
It reads `~/.claude/projects/**/*.jsonl` — your own session history, already on
|
||||
disk, all projects, for the window you ask for. To find out which of the
|
||||
directories in there are repos, it runs `git rev-parse` in **every** directory
|
||||
any session mentioned (on one real month, 152 of them), and in the ones that
|
||||
are repos it also reads `user.email` and runs `git log`. All read-only, all
|
||||
local.
|
||||
It reads `~/.claude/projects/**/*.jsonl`, your own session history, already on
|
||||
disk. To work out which of the directories mentioned there are git repos, it
|
||||
runs `git rev-parse` in each of them, and in the ones that are, reads
|
||||
`user.email` and runs `git log`.
|
||||
|
||||
The only thing that reaches the model is a small JSON summary: your name from
|
||||
`git config user.name`, aggregate counts, and project names. Your email is read
|
||||
but never emitted — it's used locally to match commit authorship. No code, no
|
||||
conversation content, and no tool or MCP server names — the report has no
|
||||
per-tool breakdown at all, so the list of services you've connected never
|
||||
leaves the script.
|
||||
The only thing that reaches the model is a small summary: your name from
|
||||
`git config`, aggregate counts, and project names. No code, no conversation
|
||||
content, and no tool or MCP server names — so the list of services you've
|
||||
connected never leaves your machine. Your email is used locally to match commit
|
||||
authorship and is never sent.
|
||||
|
||||
The report is written to your disk and published nowhere unless you explicitly
|
||||
ask for a shareable version. Because repo names appear verbatim, the skill
|
||||
lists them before you send the report anywhere.
|
||||
Project names appear verbatim in the report, so the skill reads them back to
|
||||
you before you send it anywhere. Use `/receipts for <project>` to scope it down.
|
||||
|
||||
## Relationship to `session-report`
|
||||
## Which plugin do I want?
|
||||
|
||||
Both plugins read the same transcripts, and that's about where the similarity
|
||||
ends.
|
||||
[`session-report`](../session-report) reads the same transcripts to answer
|
||||
*where am I wasting tokens* — cache hit rates, expensive prompts — and hands
|
||||
you a list of optimizations. Install it to make your usage cheaper.
|
||||
|
||||
[`session-report`](../session-report) is a tuning tool. It asks *where am I
|
||||
wasting tokens* — cache hit rates, disproportionate projects, expensive
|
||||
prompts — and its output is a list of optimizations. The audience is you, and
|
||||
the goal is to drive usage down.
|
||||
|
||||
`receipts` is a justification tool. It asks *was this worth it* — what shipped,
|
||||
in which repos, against what spend — and cross-references local git history to
|
||||
tie usage to output. The audience is your manager, and the goal is to defend
|
||||
the spend rather than trim it.
|
||||
|
||||
Install `session-report` to make your usage cheaper. Install `receipts` to
|
||||
explain why it was worth paying for.
|
||||
`receipts` answers *was this worth it* — what shipped, in which projects,
|
||||
against what spend. Install it to explain why the usage was worth paying for.
|
||||
|
||||
183
plugins/receipts/assets/make-sample.mjs
Normal file
183
plugins/receipts/assets/make-sample.mjs
Normal file
@ -0,0 +1,183 @@
|
||||
// make-sample.mjs — regenerate assets/sample-receipt.png for the README.
|
||||
//
|
||||
// The README's sample must never contain anyone's real projects, so it isn't a
|
||||
// screenshot of a real run. This builds a throwaway HOME with invented repos,
|
||||
// invented files and invented sessions, then points the real miner at it — the
|
||||
// picture is genuinely what the tool produces, from data that never existed.
|
||||
//
|
||||
// node assets/make-sample.mjs ~/receipts-sample
|
||||
// HOME=~/receipts-sample/home node skills/receipts/scripts/mine-transcripts.mjs \
|
||||
// --days 30 --html /tmp/sample.html
|
||||
// # screenshot /tmp/sample.html at 620px wide, crop to the receipt
|
||||
//
|
||||
// Build it somewhere with no symlink above it — NOT /tmp, which on macOS is a
|
||||
// symlink to /private/tmp. `git rev-parse --show-toplevel` resolves symlinks
|
||||
// and these transcripts don't, so under /tmp every repo reports zero commits
|
||||
// and the sample comes out silently wrong.
|
||||
//
|
||||
// Keep the invented names obviously fake (acme-*, example.com).
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const ROOT = process.argv[2];
|
||||
const HOME = path.join(ROOT, 'home');
|
||||
fs.rmSync(ROOT, { recursive: true, force: true });
|
||||
|
||||
const NAME = 'Morgan Lunt';
|
||||
const EMAIL = 'morgan@example.com';
|
||||
|
||||
const git = (cwd, ...a) => execFileSync('git', ['-C', cwd, ...a], { stdio: ['ignore', 'pipe', 'ignore'] });
|
||||
|
||||
// --- invented projects -------------------------------------------------------
|
||||
const REPOS = {
|
||||
'acme-api': ['src/routes/orders.ts', 'src/routes/refunds.ts', 'src/db/schema.sql', 'src/lib/auth.ts', 'tests/orders.test.ts'],
|
||||
'acme-web': ['app/checkout/page.tsx', 'app/cart/state.ts', 'components/PriceTag.tsx'],
|
||||
'billing-service': ['internal/invoice/render.go', 'internal/tax/rates.go'],
|
||||
'infra-terraform': ['envs/prod/main.tf', 'modules/rds/variables.tf'],
|
||||
};
|
||||
const NOTES = ['migration-plan.md', 'oncall-runbook.md'];
|
||||
|
||||
fs.mkdirSync(path.join(HOME, 'notes'), { recursive: true });
|
||||
fs.writeFileSync(path.join(HOME, '.gitconfig'), `[user]\n\tname = ${NAME}\n\temail = ${EMAIL}\n`);
|
||||
for (const f of NOTES) fs.writeFileSync(path.join(HOME, 'notes', f), 'note\n'.repeat(40));
|
||||
|
||||
for (const [repo, files] of Object.entries(REPOS)) {
|
||||
const dir = path.join(HOME, 'code', repo);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
git(dir, 'init', '-q', '-b', 'main');
|
||||
git(dir, 'config', 'user.name', NAME);
|
||||
git(dir, 'config', 'user.email', EMAIL);
|
||||
git(dir, 'config', 'commit.gpgsign', 'false');
|
||||
for (const f of files) {
|
||||
fs.mkdirSync(path.dirname(path.join(dir, f)), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, f), 'x\n'.repeat(60));
|
||||
}
|
||||
git(dir, 'add', '-A');
|
||||
git(dir, 'commit', '-qm', 'initial');
|
||||
}
|
||||
|
||||
// --- invented transcripts ----------------------------------------------------
|
||||
const PROJ = path.join(HOME, '.claude', 'projects', 'sample');
|
||||
fs.mkdirSync(PROJ, { recursive: true });
|
||||
|
||||
let uid = 0;
|
||||
const U = () => `u${++uid}`;
|
||||
const day = (back) => {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - back);
|
||||
d.setHours(10 + (back % 6), 15, 0, 0);
|
||||
return d.toISOString();
|
||||
};
|
||||
const usage = (out) => ({
|
||||
input_tokens: 900,
|
||||
output_tokens: out,
|
||||
cache_creation: { ephemeral_5m_input_tokens: 4000, ephemeral_1h_input_tokens: 0 },
|
||||
cache_read_input_tokens: 30000,
|
||||
});
|
||||
const asst = (sid, ts, cwd, blocks, out = 300) => ({
|
||||
type: 'assistant', sessionId: sid, uuid: U(), requestId: `r${uid}`, timestamp: ts, cwd,
|
||||
message: { usage: usage(out), content: blocks },
|
||||
});
|
||||
const user = (sid, ts, cwd, text) => ({
|
||||
type: 'user', sessionId: sid, uuid: U(), promptId: `p${uid}`, timestamp: ts, cwd,
|
||||
message: { content: text },
|
||||
});
|
||||
const tool = (name, input) => ({ type: 'tool_use', id: `t${++uid}`, name, input });
|
||||
|
||||
// One .jsonl per session, the way Claude Code actually lays them out.
|
||||
const bySession = new Map();
|
||||
const emit = (o) => {
|
||||
const k = o.sessionId;
|
||||
if (!bySession.has(k)) bySession.set(k, []);
|
||||
bySession.get(k).push(JSON.stringify(o));
|
||||
};
|
||||
|
||||
// Sessions that build things, spread across the invented repos.
|
||||
const plan = [
|
||||
{ repo: 'acme-api', sessions: 9, daysBack: [2, 3, 5, 6, 9, 12, 16, 20, 24], edits: 5, writes: 2 },
|
||||
{ repo: 'acme-web', sessions: 5, daysBack: [4, 7, 11, 18, 22], edits: 3, writes: 1 },
|
||||
{ repo: 'billing-service', sessions: 3, daysBack: [8, 15, 26], edits: 2, writes: 1 },
|
||||
{ repo: 'infra-terraform', sessions: 2, daysBack: [13, 19], edits: 2, writes: 0 },
|
||||
];
|
||||
let sid = 0;
|
||||
for (const p of plan) {
|
||||
const dir = path.join(HOME, 'code', p.repo);
|
||||
const files = REPOS[p.repo];
|
||||
for (let i = 0; i < p.sessions; i++) {
|
||||
const S = `s-${p.repo}-${++sid}`;
|
||||
const ts = day(p.daysBack[i % p.daysBack.length]);
|
||||
emit(user(S, ts, dir, 'add the thing'));
|
||||
for (let e = 0; e < p.edits; e++) {
|
||||
const f = path.join(dir, files[e % files.length]);
|
||||
emit(asst(S, ts, dir, [tool('Read', { file_path: f })], 120));
|
||||
emit(asst(S, ts, dir, [tool('Edit', { file_path: f, old_string: 'x\n'.repeat(9), new_string: 'y\n'.repeat(14) })], 400));
|
||||
}
|
||||
for (let w = 0; w < p.writes; w++) {
|
||||
const f = path.join(dir, files[(w + 1) % files.length]);
|
||||
emit(asst(S, ts, dir, [tool('Write', { file_path: f, content: 'z\n'.repeat(70) })], 700));
|
||||
}
|
||||
emit(asst(S, ts, dir, [tool('Bash', { command: 'npm test' })], 200));
|
||||
if (i % 3 === 0) {
|
||||
emit(user(S, ts, dir, 'open the PR'));
|
||||
emit(asst(S, ts, dir, [tool('Bash', { command: 'gh pr create --fill' })], 150));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Work in a plain directory — no repo.
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const S = `s-notes-${i}`;
|
||||
const ts = day([6, 10, 17, 23][i]);
|
||||
const dir = path.join(HOME, 'notes');
|
||||
emit(user(S, ts, dir, 'draft the migration plan'));
|
||||
emit(asst(S, ts, dir, [tool('Write', { file_path: path.join(dir, NOTES[i % 2]), content: 'n\n'.repeat(55) })], 900));
|
||||
}
|
||||
|
||||
// Research: no files touched, not in a repo. The row that surprises people.
|
||||
for (let i = 0; i < 17; i++) {
|
||||
const S = `s-res-${i}`;
|
||||
const ts = day([1, 2, 3, 5, 7, 8, 9, 11, 12, 14, 16, 18, 20, 21, 25, 27, 28][i]);
|
||||
emit(user(S, ts, HOME, 'what changed in the pricing API?'));
|
||||
for (let k = 0; k < 6; k++) {
|
||||
emit(asst(S, ts, HOME, [tool('WebFetch', { url: 'https://example.com/docs' })], 350));
|
||||
emit(asst(S, ts, HOME, [tool('WebSearch', { query: 'pricing api changelog' })], 250));
|
||||
}
|
||||
emit(user(S, ts, HOME, 'and the rate limits?'));
|
||||
emit(asst(S, ts, HOME, [tool('WebFetch', { url: 'https://example.com/limits' })], 400));
|
||||
}
|
||||
|
||||
for (const [k, ls] of bySession) fs.writeFileSync(path.join(PROJ, `${k}.jsonl`), ls.join('\n') + '\n');
|
||||
|
||||
// --- commits carrying that work ---------------------------------------------
|
||||
const COMMITS = {
|
||||
'acme-api': [
|
||||
['src/routes/orders.ts', 'orders: handle partial refunds', 2],
|
||||
['src/lib/auth.ts', 'auth: rotate signing keys', 5],
|
||||
['src/db/schema.sql', 'schema: add refund_reason', 9],
|
||||
['tests/orders.test.ts', 'tests: cover partial refunds', 16],
|
||||
],
|
||||
'acme-web': [
|
||||
['app/checkout/page.tsx', 'checkout: inline tax breakdown', 4],
|
||||
['app/cart/state.ts', 'cart: fix stale totals', 11],
|
||||
],
|
||||
'billing-service': [['internal/invoice/render.go', 'invoice: round to minor units', 8]],
|
||||
'infra-terraform': [['envs/prod/main.tf', 'prod: bump rds instance class', 13]],
|
||||
};
|
||||
// Date each commit to the day the session that produced it ran, so the
|
||||
// "N of your M active days ended in a commit" line reflects a real rhythm
|
||||
// rather than a fixture written all at once.
|
||||
for (const [repo, cs] of Object.entries(COMMITS)) {
|
||||
const dir = path.join(HOME, 'code', repo);
|
||||
for (const [f, msg, back] of cs) {
|
||||
fs.appendFileSync(path.join(dir, f), 'changed\n');
|
||||
const when = day(back);
|
||||
execFileSync('git', ['-C', dir, 'add', '-A'], { stdio: 'ignore' });
|
||||
execFileSync('git', ['-C', dir, 'commit', '-qm', msg], {
|
||||
stdio: 'ignore',
|
||||
env: { ...process.env, GIT_AUTHOR_DATE: when, GIT_COMMITTER_DATE: when },
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(HOME);
|
||||
BIN
plugins/receipts/assets/sample-receipt.png
Normal file
BIN
plugins/receipts/assets/sample-receipt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
Loading…
x
Reference in New Issue
Block a user