commit 00afed6930d1a03d90137ed28b62f5543c2f6c7e Author: AgentSeal Date: Mon Apr 13 15:10:27 2026 -0700 v0.1.0 - initial release Interactive TUI dashboard for Claude Code token observability. 13-category task classifier, per-project/model/tool breakdowns, gradient bar charts, SwiftBar menu bar widget, CSV/JSON export. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..eeccf71 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: agentseal diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5d9e55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.tgz +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..e232860 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +

+ CodeBurn +

+ +

CodeBurn

+ +

See where your AI coding tokens go.

+ +

+ npm version + npm downloads + license + node version +

+ +

+ CodeBurn TUI dashboard +

+ +By task type, tool, model, MCP server, and project. Interactive TUI dashboard with gradient charts, responsive panels, and keyboard navigation. macOS menu bar widget via SwiftBar. CSV/JSON export. + +Works by reading Claude Code session transcripts directly from disk. No wrapper, no proxy, no API keys. Pricing from LiteLLM (auto-cached, all models supported). + +## Install + +```bash +npm install -g codeburn +``` + +Or run without installing: + +```bash +npx codeburn +``` + +### Requirements + +- Node.js 18+ +- Claude Code (reads `~/.claude/projects/` session data) + +## Usage + +```bash +codeburn # interactive dashboard (default: 7 days) +codeburn today # today's usage +codeburn month # this month's usage +codeburn report -p month # same as above +codeburn status # compact one-liner (today + month) +codeburn status --format json +codeburn export # CSV with today, 7 days, 30 days +codeburn export -f json # JSON export +``` + +Arrow keys switch between Today / 7 Days / Month. Press `q` to quit, `1` `2` `3` as shortcuts. + +## Menu Bar + +CodeBurn SwiftBar menu bar widget + +```bash +codeburn install-menubar # install SwiftBar/xbar plugin +codeburn uninstall-menubar # remove it +``` + +Requires [SwiftBar](https://github.com/swiftbar/SwiftBar) (`brew install --cask swiftbar`). Shows today's cost in the menu bar with a flame icon. Dropdown shows activity breakdown, model costs, and token stats for today, 7 days, and month. Refreshes every 5 minutes. + +## What it tracks + +**13 task categories** classified from tool usage patterns and user message keywords. No LLM calls, fully deterministic. + +| Category | What triggers it | +|---|---| +| Coding | Edit, Write tools | +| Debugging | Error/fix keywords + tool usage | +| Feature Dev | "add", "create", "implement" keywords | +| Refactoring | "refactor", "rename", "simplify" | +| Testing | pytest, vitest, jest in Bash | +| Exploration | Read, Grep, WebSearch without edits | +| Planning | EnterPlanMode, TaskCreate tools | +| Delegation | Agent tool spawns | +| Git Ops | git push/commit/merge in Bash | +| Build/Deploy | npm build, docker, pm2 | +| Brainstorming | "brainstorm", "what if", "design" | +| Conversation | No tools, pure text exchange | +| General | Skill tool, uncategorized | + +**Breakdowns**: daily cost chart, per-project, per-model (Opus/Sonnet/Haiku/GPT-4o/Gemini), per-activity, core tools, MCP servers. + +**Pricing**: Fetched from [LiteLLM](https://github.com/BerriAI/litellm) model prices (auto-cached 24h at `~/.cache/codeburn/`). Handles input, output, cache write, cache read, and web search costs. Fast mode multiplier for Claude. Fallback to hardcoded prices if fetch fails. + +## How it reads data + +Claude Code stores session transcripts as JSONL at `~/.claude/projects//.jsonl`. Each assistant entry contains model name, token usage (input, output, cache read, cache write), tool_use blocks, and timestamps. + +CodeBurn reads these files, deduplicates messages by API message ID (prevents double-counting across sessions), filters by date range per entry (not per session), and classifies each turn. + +## Project structure + +``` +src/ + cli.ts Commander.js entry point + dashboard.tsx Ink TUI (React for terminals) + parser.ts JSONL reader, dedup, date filter + models.ts LiteLLM pricing, cost calculation + classifier.ts 13-category task classifier + types.ts Type definitions + format.ts Text rendering (status bar) + menubar.ts SwiftBar plugin generator + export.ts CSV/JSON multi-period export +``` + +## License + +MIT + +## Credits + +Inspired by [ccusage](https://github.com/ryoppippi/ccusage). Pricing data from [LiteLLM](https://github.com/BerriAI/litellm). + +Built by [AgentSeal](https://agentseal.org). diff --git a/assets/dashboard.jpg b/assets/dashboard.jpg new file mode 100644 index 0000000..95963c1 Binary files /dev/null and b/assets/dashboard.jpg differ diff --git a/assets/logo.ico b/assets/logo.ico new file mode 100644 index 0000000..cf74aed Binary files /dev/null and b/assets/logo.ico differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..4560137 Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/menubar.png b/assets/menubar.png new file mode 100644 index 0000000..1f93d4d Binary files /dev/null and b/assets/menubar.png differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d2aa30b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2618 @@ +{ + "name": "codeburn", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codeburn", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "ink": "^7.0.0", + "react": "^19.2.5" + }, + "bin": { + "codeburn": "dist/cli.js" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "tsup": "^8.4.0", + "tsx": "^4.19.0", + "typescript": "^5.8.0", + "vitest": "^3.1.0" + } + }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", + "integrity": "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-boxes": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-4.0.1.tgz", + "integrity": "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==", + "license": "MIT", + "engines": { + "node": ">=18.20 <19 || >=20.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-6.0.0.tgz", + "integrity": "sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^9.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ink/-/ink-7.0.0.tgz", + "integrity": "sha512-fMie5/VwIYXofMyND0s+fOVhwVBBPYx+uuqJ6V6rUBGjui+2UYp+0fWtvhSeKT4z+X1uH98a4ge5Vj3aTlL6mg==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.3.0", + "ansi-escapes": "^7.3.0", + "ansi-styles": "^6.2.3", + "auto-bind": "^5.0.1", + "chalk": "^5.6.2", + "cli-boxes": "^4.0.1", + "cli-cursor": "^4.0.0", + "cli-truncate": "^6.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.45.1", + "indent-string": "^5.0.0", + "is-in-ci": "^2.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.33.0", + "scheduler": "^0.27.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^9.0.0", + "stack-utils": "^2.0.6", + "string-width": "^8.2.0", + "terminal-size": "^4.0.1", + "type-fest": "^5.5.0", + "widest-line": "^6.0.0", + "wrap-ansi": "^10.0.0", + "ws": "^8.20.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@types/react": ">=19.2.0", + "react": ">=19.2.0", + "react-devtools-core": ">=6.1.2" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", + "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/slice-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-9.0.0.tgz", + "integrity": "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-size": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/terminal-size/-/terminal-size-4.0.1.tgz", + "integrity": "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-fest": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/widest-line": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-6.0.0.tgz", + "integrity": "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==", + "license": "MIT", + "dependencies": { + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5a6e4a3 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "codeburn", + "version": "0.1.0", + "description": "See where your AI coding tokens go - by task, tool, model, and project", + "type": "module", + "bin": { + "codeburn": "./dist/cli.js" + }, + "scripts": { + "build": "tsup", + "dev": "tsx src/cli.ts", + "test": "vitest", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "claude-code", + "cursor", + "codex", + "ai-coding", + "token-usage", + "cost-tracking", + "observability", + "developer-tools" + ], + "author": "AgentSeal ", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "ink": "^7.0.0", + "react": "^19.2.5" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "tsup": "^8.4.0", + "tsx": "^4.19.0", + "typescript": "^5.8.0", + "vitest": "^3.1.0" + } +} diff --git a/src/classifier.ts b/src/classifier.ts new file mode 100644 index 0000000..83f2b42 --- /dev/null +++ b/src/classifier.ts @@ -0,0 +1,130 @@ +import type { ClassifiedTurn, ParsedTurn, TaskCategory } from './types.js' + +const TEST_PATTERNS = /\b(test|pytest|vitest|jest|mocha|spec|coverage|npm\s+test|npx\s+vitest|npx\s+jest)\b/i +const GIT_PATTERNS = /\bgit\s+(push|pull|commit|merge|rebase|checkout|branch|stash|log|diff|status|add|reset|cherry-pick|tag)\b/i +const BUILD_PATTERNS = /\b(npm\s+run\s+build|npm\s+publish|pip\s+install|docker|deploy|make\s+build|npm\s+run\s+dev|npm\s+start|pm2|systemctl|brew|cargo\s+build)\b/i +const INSTALL_PATTERNS = /\b(npm\s+install|pip\s+install|brew\s+install|apt\s+install|cargo\s+add)\b/i + +const DEBUG_KEYWORDS = /\b(fix|bug|error|broken|failing|crash|issue|debug|traceback|exception|stack\s*trace|not\s+working|wrong|unexpected)\b/i +const FEATURE_KEYWORDS = /\b(add|create|implement|new|build|feature|introduce|set\s*up|scaffold|generate)\b/i +const REFACTOR_KEYWORDS = /\b(refactor|clean\s*up|rename|reorganize|simplify|extract|restructure|move|migrate|split)\b/i +const BRAINSTORM_KEYWORDS = /\b(brainstorm|idea|what\s+if|explore|think\s+about|approach|strategy|design|consider|how\s+should|what\s+would|opinion|suggest|recommend)\b/i +const RESEARCH_KEYWORDS = /\b(research|investigate|look\s+into|find\s+out|check|search|analyze|review|understand|explain|how\s+does|what\s+is|show\s+me|list|compare)\b/i + +const EDIT_TOOLS = new Set(['Edit', 'Write', 'FileEditTool', 'FileWriteTool', 'NotebookEdit']) +const READ_TOOLS = new Set(['Read', 'Grep', 'Glob', 'FileReadTool', 'GrepTool', 'GlobTool']) +const BASH_TOOLS = new Set(['Bash', 'BashTool', 'PowerShellTool']) +const TASK_TOOLS = new Set(['TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList', 'TaskOutput', 'TaskStop', 'TodoWrite']) +const SEARCH_TOOLS = new Set(['WebSearch', 'WebFetch', 'ToolSearch']) + +function hasEditTools(tools: string[]): boolean { + return tools.some(t => EDIT_TOOLS.has(t)) +} + +function hasReadTools(tools: string[]): boolean { + return tools.some(t => READ_TOOLS.has(t)) +} + +function hasBashTool(tools: string[]): boolean { + return tools.some(t => BASH_TOOLS.has(t)) +} + +function hasTaskTools(tools: string[]): boolean { + return tools.some(t => TASK_TOOLS.has(t)) +} + +function hasSearchTools(tools: string[]): boolean { + return tools.some(t => SEARCH_TOOLS.has(t)) +} + +function hasMcpTools(tools: string[]): boolean { + return tools.some(t => t.startsWith('mcp__')) +} + +function hasSkillTool(tools: string[]): boolean { + return tools.some(t => t === 'Skill') +} + +function getAllTools(turn: ParsedTurn): string[] { + return turn.assistantCalls.flatMap(c => c.tools) +} + +function classifyByToolPattern(turn: ParsedTurn): TaskCategory | null { + const tools = getAllTools(turn) + if (tools.length === 0) return null + + if (turn.assistantCalls.some(c => c.hasPlanMode)) return 'planning' + if (turn.assistantCalls.some(c => c.hasAgentSpawn)) return 'delegation' + + const hasEdits = hasEditTools(tools) + const hasReads = hasReadTools(tools) + const hasBash = hasBashTool(tools) + const hasTasks = hasTaskTools(tools) + const hasSearch = hasSearchTools(tools) + const hasMcp = hasMcpTools(tools) + const hasSkill = hasSkillTool(tools) + + if (hasBash && !hasEdits) { + const userMsg = turn.userMessage + if (TEST_PATTERNS.test(userMsg)) return 'testing' + if (GIT_PATTERNS.test(userMsg)) return 'git' + if (BUILD_PATTERNS.test(userMsg)) return 'build/deploy' + if (INSTALL_PATTERNS.test(userMsg)) return 'build/deploy' + } + + if (hasEdits) return 'coding' + + if (hasBash && hasReads) return 'exploration' + if (hasBash) return 'coding' + + if (hasSearch || hasMcp) return 'exploration' + if (hasReads && !hasEdits) return 'exploration' + if (hasTasks && !hasEdits) return 'planning' + if (hasSkill) return 'general' + + return null +} + +function refineByKeywords(category: TaskCategory, userMessage: string): TaskCategory { + if (category === 'coding') { + if (DEBUG_KEYWORDS.test(userMessage)) return 'debugging' + if (REFACTOR_KEYWORDS.test(userMessage)) return 'refactoring' + if (FEATURE_KEYWORDS.test(userMessage)) return 'feature' + return 'coding' + } + + if (category === 'exploration') { + if (RESEARCH_KEYWORDS.test(userMessage)) return 'exploration' + if (DEBUG_KEYWORDS.test(userMessage)) return 'debugging' + return 'exploration' + } + + return category +} + +function classifyConversation(userMessage: string): TaskCategory { + if (BRAINSTORM_KEYWORDS.test(userMessage)) return 'brainstorming' + if (RESEARCH_KEYWORDS.test(userMessage)) return 'exploration' + if (DEBUG_KEYWORDS.test(userMessage)) return 'debugging' + if (FEATURE_KEYWORDS.test(userMessage)) return 'feature' + return 'conversation' +} + +export function classifyTurn(turn: ParsedTurn): ClassifiedTurn { + const tools = getAllTools(turn) + + let category: TaskCategory + + if (tools.length === 0) { + category = classifyConversation(turn.userMessage) + } else { + const toolCategory = classifyByToolPattern(turn) + if (toolCategory) { + category = refineByKeywords(toolCategory, turn.userMessage) + } else { + category = classifyConversation(turn.userMessage) + } + } + + return { ...turn, category } +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..2f7b046 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,184 @@ +import { Command } from 'commander' +import { exportCsv, exportJson, type PeriodExport } from './export.js' +import { loadPricing } from './models.js' +import { parseAllSessions } from './parser.js' +import { renderStatusBar } from './format.js' +import { installMenubar, renderMenubarFormat, type PeriodData, uninstallMenubar } from './menubar.js' +import { CATEGORY_LABELS, type DateRange, type ProjectSummary, type TaskCategory } from './types.js' +import { renderDashboard } from './dashboard.js' + +function getDateRange(period: string): { range: DateRange; label: string } { + const now = new Date() + const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999) + + switch (period) { + case 'today': { + const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + return { range: { start, end }, label: `Today (${start.toISOString().slice(0, 10)})` } + } + case 'yesterday': { + const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1) + const yesterdayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 23, 59, 59, 999) + return { range: { start, end: yesterdayEnd }, label: `Yesterday (${start.toISOString().slice(0, 10)})` } + } + case 'week': { + const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7) + return { range: { start, end }, label: 'Last 7 Days' } + } + case 'month': { + const start = new Date(now.getFullYear(), now.getMonth(), 1) + return { range: { start, end }, label: `${now.toLocaleString('default', { month: 'long' })} ${now.getFullYear()}` } + } + case 'all': { + return { range: { start: new Date(0), end }, label: 'All Time' } + } + default: { + const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7) + return { range: { start, end }, label: 'Last 7 Days' } + } + } +} + +function toPeriod(s: string): 'today' | 'week' | 'month' { + if (s === 'today') return 'today' + if (s === 'month') return 'month' + return 'week' +} + +const program = new Command() + .name('codeburn') + .description('See where your AI coding tokens go - by task, tool, model, and project') + .version('0.1.0') + +program + .command('report', { isDefault: true }) + .description('Interactive usage dashboard') + .option('-p, --period ', 'Starting period: today, week, month', 'week') + .action(async (opts) => { + await renderDashboard(toPeriod(opts.period)) + }) + +function buildPeriodData(label: string, projects: ProjectSummary[]): PeriodData { + const sessions = projects.flatMap(p => p.sessions) + const catTotals: Record = {} + const modelTotals: Record = {} + let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheWriteTokens = 0 + + for (const sess of sessions) { + inputTokens += sess.totalInputTokens + outputTokens += sess.totalOutputTokens + cacheReadTokens += sess.totalCacheReadTokens + cacheWriteTokens += sess.totalCacheWriteTokens + for (const [cat, d] of Object.entries(sess.categoryBreakdown)) { + if (!catTotals[cat]) catTotals[cat] = { turns: 0, cost: 0 } + catTotals[cat].turns += d.turns + catTotals[cat].cost += d.costUSD + } + for (const [model, d] of Object.entries(sess.modelBreakdown)) { + if (!modelTotals[model]) modelTotals[model] = { calls: 0, cost: 0 } + modelTotals[model].calls += d.calls + modelTotals[model].cost += d.costUSD + } + } + + return { + label, + cost: projects.reduce((s, p) => s + p.totalCostUSD, 0), + calls: projects.reduce((s, p) => s + p.totalApiCalls, 0), + inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, + categories: Object.entries(catTotals) + .sort(([, a], [, b]) => b.cost - a.cost) + .map(([cat, d]) => ({ name: CATEGORY_LABELS[cat as TaskCategory] ?? cat, ...d })), + models: Object.entries(modelTotals) + .sort(([, a], [, b]) => b.cost - a.cost) + .map(([name, d]) => ({ name, ...d })), + } +} + +program + .command('status') + .description('Compact status output (today + week + month)') + .option('--format ', 'Output format: terminal, menubar, json', 'terminal') + .action(async (opts) => { + await loadPricing() + if (opts.format === 'menubar') { + const todayData = buildPeriodData('Today', await parseAllSessions(getDateRange('today').range)) + const weekData = buildPeriodData('7 Days', await parseAllSessions(getDateRange('week').range)) + const monthData = buildPeriodData('Month', await parseAllSessions(getDateRange('month').range)) + console.log(renderMenubarFormat(todayData, weekData, monthData)) + return + } + + if (opts.format === 'json') { + const todayData = buildPeriodData('today', await parseAllSessions(getDateRange('today').range)) + const monthData = buildPeriodData('month', await parseAllSessions(getDateRange('month').range)) + console.log(JSON.stringify({ today: { cost: todayData.cost, calls: todayData.calls }, month: { cost: monthData.cost, calls: monthData.calls } })) + return + } + + const monthProjects = await parseAllSessions(getDateRange('month').range) + console.log(renderStatusBar(monthProjects)) + }) + +program + .command('today') + .description('Today\'s usage dashboard') + .action(async () => { + await renderDashboard('today') + }) + +program + .command('month') + .description('This month\'s usage dashboard') + .action(async () => { + await renderDashboard('month') + }) + +program + .command('export') + .description('Export usage data to CSV or JSON (includes 1 day, 7 days, 30 days)') + .option('-f, --format ', 'Export format: csv, json', 'csv') + .option('-o, --output ', 'Output file path') + .action(async (opts) => { + await loadPricing() + const periods: PeriodExport[] = [ + { label: 'Today', projects: await parseAllSessions(getDateRange('today').range) }, + { label: '7 Days', projects: await parseAllSessions(getDateRange('week').range) }, + { label: '30 Days', projects: await parseAllSessions(getDateRange('month').range) }, + ] + + if (periods.every(p => p.projects.length === 0)) { + console.log('\n No usage data found.\n') + return + } + + const defaultName = `codeburn-${new Date().toISOString().slice(0, 10)}` + const outputPath = opts.output ?? `${defaultName}.${opts.format}` + + let savedPath: string + if (opts.format === 'json') { + savedPath = await exportJson(periods, outputPath) + } else { + savedPath = await exportCsv(periods, outputPath) + } + + console.log(`\n Exported (Today + 7 Days + 30 Days) to: ${savedPath}\n`) + }) + +program + .command('install-menubar') + .description('Install macOS menu bar plugin (SwiftBar/xbar)') + .action(async () => { + const result = await installMenubar() + console.log(result) + }) + +program + .command('uninstall-menubar') + .description('Remove macOS menu bar plugin') + .action(async () => { + const result = await uninstallMenubar() + console.log(result) + }) + +program.parse() diff --git a/src/dashboard.tsx b/src/dashboard.tsx new file mode 100644 index 0000000..3d4b691 --- /dev/null +++ b/src/dashboard.tsx @@ -0,0 +1,484 @@ +import React, { useState, useCallback } from 'react' +import { render, Box, Text, useInput, useApp } from 'ink' +import { CATEGORY_LABELS, type ProjectSummary, type TaskCategory } from './types.js' +import { formatCost, formatTokens } from './format.js' +import { parseAllSessions } from './parser.js' +import { loadPricing } from './models.js' + +type Period = 'today' | 'week' | 'month' + +const PERIODS: Period[] = ['today', 'week', 'month'] +const PERIOD_LABELS: Record = { + today: 'Today', + week: '7 Days', + month: 'This Month', +} + +const MIN_WIDE = 90 +const ORANGE = '#FF8C42' +const DIM = '#555555' +const GOLD = '#FFD700' + +const PANEL_COLORS = { + overview: '#FF8C42', + daily: '#5B9EF5', + project: '#5BF5A0', + model: '#E05BF5', + activity: '#F5C85B', + tools: '#5BF5E0', + mcp: '#F55BE0', +} + +const CATEGORY_COLORS: Record = { + coding: '#5B9EF5', + debugging: '#F55B5B', + feature: '#5BF58C', + refactoring: '#F5E05B', + testing: '#E05BF5', + exploration: '#5BF5E0', + planning: '#7B9EF5', + delegation: '#F5C85B', + git: '#CCCCCC', + 'build/deploy': '#5BF5A0', + conversation: '#888888', + brainstorming: '#F55BE0', + general: '#666666', +} + +function toHex(r: number, g: number, b: number): string { + return '#' + [r, g, b].map(v => Math.round(v).toString(16).padStart(2, '0')).join('') +} + +function lerp(a: number, b: number, t: number): number { + return a + t * (b - a) +} + +// Blue -> amber -> orange gradient across the bar width +function gradientColor(pct: number): string { + if (pct <= 0.33) { + const t = pct / 0.33 + return toHex(lerp(91, 245, t), lerp(158, 200, t), lerp(245, 91, t)) + } + if (pct <= 0.66) { + const t = (pct - 0.33) / 0.33 + return toHex(lerp(245, 255, t), lerp(200, 140, t), lerp(91, 66, t)) + } + const t = (pct - 0.66) / 0.34 + return toHex(lerp(255, 245, t), lerp(140, 91, t), lerp(66, 91, t)) +} + +function getDateRange(period: Period): { start: Date; end: Date } { + const now = new Date() + const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999) + switch (period) { + case 'today': return { start: new Date(now.getFullYear(), now.getMonth(), now.getDate()), end } + case 'week': return { start: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7), end } + case 'month': return { start: new Date(now.getFullYear(), now.getMonth(), 1), end } + } +} + +type Layout = { dashWidth: number; wide: boolean; halfWidth: number; barWidth: number } + +function getLayout(): Layout { + const termWidth = process.stdout.columns || parseInt(process.env['COLUMNS'] ?? '') || 80 + const dashWidth = Math.min(104, termWidth) + const wide = dashWidth >= MIN_WIDE + const halfWidth = wide ? Math.floor(dashWidth / 2) : dashWidth + const inner = halfWidth - 4 + const barWidth = Math.max(6, Math.min(10, inner - 30)) + return { dashWidth, wide, halfWidth, barWidth } +} + +function HBar({ value, max, width }: { value: number; max: number; width: number }) { + if (max === 0) return {'░'.repeat(width)} + const filled = Math.round((value / max) * width) + const fillChars: React.ReactNode[] = [] + for (let i = 0; i < Math.min(filled, width); i++) { + fillChars.push({'█'}) + } + return ( + + {fillChars} + {'░'.repeat(Math.max(width - filled, 0))} + + ) +} + +function Panel({ title, color, children, width }: { title: string; color: string; children: React.ReactNode; width: number }) { + return ( + + {title} + {children} + + ) +} + +function fit(s: string, n: number): string { + return s.length > n ? s.slice(0, n) : s.padEnd(n) +} + +function Overview({ projects, label, width }: { projects: ProjectSummary[]; label: string; width: number }) { + const totalCost = projects.reduce((s, p) => s + p.totalCostUSD, 0) + const totalCalls = projects.reduce((s, p) => s + p.totalApiCalls, 0) + const totalSessions = projects.reduce((s, p) => s + p.sessions.length, 0) + const allSessions = projects.flatMap(p => p.sessions) + const totalInput = allSessions.reduce((s, sess) => s + sess.totalInputTokens, 0) + const totalOutput = allSessions.reduce((s, sess) => s + sess.totalOutputTokens, 0) + const totalCacheRead = allSessions.reduce((s, sess) => s + sess.totalCacheReadTokens, 0) + const totalCacheWrite = allSessions.reduce((s, sess) => s + sess.totalCacheWriteTokens, 0) + const cacheHit = totalInput + totalCacheRead > 0 + ? (totalCacheRead / (totalInput + totalCacheRead)) * 100 : 0 + + return ( + + + CodeBurn + {label} + + + {formatCost(totalCost)} + cost + {totalCalls.toLocaleString()} + calls + {String(totalSessions)} + sessions + {cacheHit.toFixed(0)}% + cache hit + + + {formatTokens(totalInput)} in {formatTokens(totalOutput)} out {formatTokens(totalCacheRead)} cached {formatTokens(totalCacheWrite)} written + + + ) +} + +function DailyActivity({ projects, days = 14, pw, bw }: { projects: ProjectSummary[]; days?: number; pw: number; bw: number }) { + const dailyCosts: Record = {} + const dailyCalls: Record = {} + for (const project of projects) { + for (const session of project.sessions) { + for (const turn of session.turns) { + if (!turn.timestamp) continue + const day = turn.timestamp.slice(0, 10) + dailyCosts[day] = (dailyCosts[day] ?? 0) + turn.assistantCalls.reduce((s, c) => s + c.costUSD, 0) + dailyCalls[day] = (dailyCalls[day] ?? 0) + turn.assistantCalls.length + } + } + } + const sortedDays = Object.keys(dailyCosts).sort().slice(-days) + const maxCost = Math.max(...sortedDays.map(d => dailyCosts[d] ?? 0)) + + return ( + + {''.padEnd(6 + bw)}{'cost'.padStart(8)}{'calls'.padStart(6)} + {sortedDays.map(day => ( + + {day.slice(5)} + + {formatCost(dailyCosts[day] ?? 0).padStart(8)} + {String(dailyCalls[day] ?? 0).padStart(6)} + + ))} + + ) +} + +function shortProject(project: string): string { + const parts = project.replace(/^-/, '').split('-').filter(Boolean) + if (parts.length <= 2) return parts.join('/') + return parts.slice(-2).join('/') +} + +function ProjectBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) { + const maxCost = Math.max(...projects.map(p => p.totalCostUSD)) + const nw = Math.max(8, pw - bw - 23) + return ( + + {''.padEnd(bw + 1 + nw)}{'cost'.padStart(8)}{'sess'.padStart(6)} + {projects.slice(0, 8).map((project, i) => ( + + + {fit(shortProject(project.project), nw)} + {formatCost(project.totalCostUSD).padStart(8)} + {String(project.sessions.length).padStart(6)} + + ))} + + ) +} + +function ModelBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) { + const modelTotals: Record = {} + for (const project of projects) { + for (const session of project.sessions) { + for (const [model, data] of Object.entries(session.modelBreakdown)) { + if (!modelTotals[model]) modelTotals[model] = { calls: 0, costUSD: 0 } + modelTotals[model].calls += data.calls + modelTotals[model].costUSD += data.costUSD + } + } + } + const sorted = Object.entries(modelTotals).sort(([, a], [, b]) => b.costUSD - a.costUSD) + const maxCost = sorted[0]?.[1]?.costUSD ?? 0 + const nw = Math.max(6, pw - bw - 25) + + return ( + + {''.padEnd(bw + 1 + nw)}{'cost'.padStart(8)}{'calls'.padStart(7)} + {sorted.map(([model, data], i) => ( + + + {fit(model, nw)} + {formatCost(data.costUSD).padStart(8)} + {String(data.calls).padStart(7)} + + ))} + + ) +} + +function ActivityBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) { + const categoryTotals: Record = {} + for (const project of projects) { + for (const session of project.sessions) { + for (const [cat, data] of Object.entries(session.categoryBreakdown)) { + if (!categoryTotals[cat]) categoryTotals[cat] = { turns: 0, costUSD: 0 } + categoryTotals[cat].turns += data.turns + categoryTotals[cat].costUSD += data.costUSD + } + } + } + const sorted = Object.entries(categoryTotals).sort(([, a], [, b]) => b.costUSD - a.costUSD) + const maxCost = sorted[0]?.[1]?.costUSD ?? 0 + + return ( + + {''.padEnd(bw + 14)}{'cost'.padStart(8)}{'turns'.padStart(6)} + {sorted.map(([cat, data]) => ( + + + + {' '}{fit(CATEGORY_LABELS[cat as TaskCategory] ?? cat, 13)} + + {formatCost(data.costUSD).padStart(8)} + {String(data.turns).padStart(6)} + + ))} + + ) +} + +function ToolBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) { + const toolTotals: Record = {} + for (const project of projects) { + for (const session of project.sessions) { + for (const [tool, data] of Object.entries(session.toolBreakdown)) { + toolTotals[tool] = (toolTotals[tool] ?? 0) + data.calls + } + } + } + const sorted = Object.entries(toolTotals).sort(([, a], [, b]) => b - a) + const maxCalls = sorted[0]?.[1] ?? 0 + const nw = Math.max(6, pw - bw - 15) + + return ( + + {''.padEnd(bw + 1 + nw)}{'calls'.padStart(7)} + {sorted.slice(0, 10).map(([tool, calls]) => ( + + + {fit(tool, nw)} + {String(calls).padStart(7)} + + ))} + + ) +} + +function McpBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) { + const mcpTotals: Record = {} + for (const project of projects) { + for (const session of project.sessions) { + for (const [server, data] of Object.entries(session.mcpBreakdown)) { + mcpTotals[server] = (mcpTotals[server] ?? 0) + data.calls + } + } + } + const sorted = Object.entries(mcpTotals).sort(([, a], [, b]) => b - a) + if (sorted.length === 0) { + return No MCP usage + } + const maxCalls = sorted[0]?.[1] ?? 0 + const nw = Math.max(6, pw - bw - 15) + + return ( + + {''.padEnd(bw + 1 + nw)}{'calls'.padStart(6)} + {sorted.slice(0, 8).map(([server, calls]) => ( + + + {fit(server, nw)} + {String(calls).padStart(6)} + + ))} + + ) +} + +function PeriodTabs({ active }: { active: Period }) { + return ( + + {PERIODS.map(p => ( + + {active === p ? `[ ${PERIOD_LABELS[p]} ]` : ` ${PERIOD_LABELS[p]} `} + + ))} + + ) +} + +function StatusBar({ width }: { width: number }) { + return ( + + + {'<'}{'>'} + switch + q + quit + 1 + today + 2 + week + 3 + month + + + ) +} + +function Row({ wide, width, children }: { wide: boolean; width: number; children: React.ReactNode }) { + if (wide) return {children} + return <>{children} +} + +function DashboardContent({ projects, period }: { projects: ProjectSummary[]; period: Period }) { + const { dashWidth, wide, halfWidth, barWidth } = getLayout() + + if (projects.length === 0) { + return ( + + No usage data found for {PERIOD_LABELS[period]}. + + ) + } + + const pw = wide ? halfWidth : dashWidth + + return ( + + + + + + + + + + + + + + + + + + + ) +} + +function InteractiveDashboard({ initialProjects, initialPeriod }: { + initialProjects: ProjectSummary[] + initialPeriod: Period +}) { + const { exit } = useApp() + const [period, setPeriod] = useState(initialPeriod) + const [projects, setProjects] = useState(initialProjects) + const [loading, setLoading] = useState(false) + const { dashWidth } = getLayout() + + const switchPeriod = useCallback(async (newPeriod: Period) => { + if (newPeriod === period) return + setLoading(true) + setPeriod(newPeriod) + const range = getDateRange(newPeriod) + const data = await parseAllSessions(range) + setProjects(data) + setLoading(false) + }, [period]) + + useInput((input, key) => { + if (input === 'q') { + exit() + return + } + const idx = PERIODS.indexOf(period) + if (key.leftArrow) { + switchPeriod(PERIODS[(idx - 1 + PERIODS.length) % PERIODS.length]) + } else if (key.rightArrow || key.tab) { + switchPeriod(PERIODS[(idx + 1) % PERIODS.length]) + } else if (input === '1') switchPeriod('today') + else if (input === '2') switchPeriod('week') + else if (input === '3') switchPeriod('month') + }) + + if (loading) { + return ( + + + + Loading {PERIOD_LABELS[period]}... + + + + ) + } + + return ( + + + + + + ) +} + +function StaticDashboard({ projects, period }: { projects: ProjectSummary[]; period: Period }) { + const { dashWidth } = getLayout() + return ( + + + + + ) +} + +export async function renderDashboard(period: Period = 'week'): Promise { + await loadPricing() + const range = getDateRange(period) + const projects = await parseAllSessions(range) + + const isTTY = process.stdin.isTTY && process.stdout.isTTY + + if (isTTY) { + const { waitUntilExit } = render( + + ) + await waitUntilExit() + } else { + const { unmount } = render( + , + { patchConsole: false } + ) + unmount() + } +} diff --git a/src/export.ts b/src/export.ts new file mode 100644 index 0000000..d1f0da0 --- /dev/null +++ b/src/export.ts @@ -0,0 +1,194 @@ +import { writeFile } from 'fs/promises' +import { resolve } from 'path' +import { CATEGORY_LABELS, type ProjectSummary, type TaskCategory } from './types.js' + +function escCsv(s: string): string { + if (s.includes(',') || s.includes('"') || s.includes('\n')) { + return `"${s.replace(/"/g, '""')}"` + } + return s +} + +function buildDailyRows(projects: ProjectSummary[]): Array> { + const daily: Record = {} + + for (const project of projects) { + for (const session of project.sessions) { + for (const turn of session.turns) { + if (!turn.timestamp) continue + const day = turn.timestamp.slice(0, 10) + if (!daily[day]) daily[day] = { cost: 0, calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } + for (const call of turn.assistantCalls) { + daily[day].cost += call.costUSD + daily[day].calls++ + daily[day].input += call.usage.inputTokens + daily[day].output += call.usage.outputTokens + daily[day].cacheRead += call.usage.cacheReadInputTokens + daily[day].cacheWrite += call.usage.cacheCreationInputTokens + } + } + } + } + + return Object.entries(daily).sort().map(([date, d]) => ({ + Date: date, + 'Cost (USD)': Math.round(d.cost * 100) / 100, + 'API Calls': d.calls, + 'Input Tokens': d.input, + 'Output Tokens': d.output, + 'Cache Read Tokens': d.cacheRead, + 'Cache Write Tokens': d.cacheWrite, + })) +} + +function buildActivityRows(projects: ProjectSummary[]): Array> { + const catTotals: Record = {} + for (const project of projects) { + for (const session of project.sessions) { + for (const [cat, d] of Object.entries(session.categoryBreakdown)) { + if (!catTotals[cat]) catTotals[cat] = { turns: 0, cost: 0 } + catTotals[cat].turns += d.turns + catTotals[cat].cost += d.costUSD + } + } + } + return Object.entries(catTotals) + .sort(([, a], [, b]) => b.cost - a.cost) + .map(([cat, d]) => ({ + Activity: CATEGORY_LABELS[cat as TaskCategory] ?? cat, + 'Cost (USD)': Math.round(d.cost * 100) / 100, + Turns: d.turns, + })) +} + +function buildModelRows(projects: ProjectSummary[]): Array> { + const modelTotals: Record = {} + for (const project of projects) { + for (const session of project.sessions) { + for (const [model, d] of Object.entries(session.modelBreakdown)) { + if (!modelTotals[model]) modelTotals[model] = { calls: 0, cost: 0, input: 0, output: 0 } + modelTotals[model].calls += d.calls + modelTotals[model].cost += d.costUSD + modelTotals[model].input += d.tokens.inputTokens + modelTotals[model].output += d.tokens.outputTokens + } + } + } + return Object.entries(modelTotals) + .sort(([, a], [, b]) => b.cost - a.cost) + .map(([model, d]) => ({ + Model: model, + 'Cost (USD)': Math.round(d.cost * 100) / 100, + 'API Calls': d.calls, + 'Input Tokens': d.input, + 'Output Tokens': d.output, + })) +} + +function buildToolRows(projects: ProjectSummary[]): Array> { + const toolTotals: Record = {} + for (const project of projects) { + for (const session of project.sessions) { + for (const [tool, d] of Object.entries(session.toolBreakdown)) { + toolTotals[tool] = (toolTotals[tool] ?? 0) + d.calls + } + } + } + return Object.entries(toolTotals) + .sort(([, a], [, b]) => b - a) + .map(([tool, calls]) => ({ Tool: tool, Calls: calls })) +} + +function buildProjectRows(projects: ProjectSummary[]): Array> { + return projects.map(p => ({ + Project: p.projectPath, + 'Cost (USD)': Math.round(p.totalCostUSD * 100) / 100, + 'API Calls': p.totalApiCalls, + Sessions: p.sessions.length, + })) +} + +function rowsToCsv(rows: Array>): string { + if (rows.length === 0) return '' + const headers = Object.keys(rows[0]) + const lines = [headers.map(escCsv).join(',')] + for (const row of rows) { + lines.push(headers.map(h => escCsv(String(row[h] ?? ''))).join(',')) + } + return lines.join('\n') +} + +export type PeriodExport = { + label: string + projects: ProjectSummary[] +} + +function buildSummaryRow(period: PeriodExport): Record { + const cost = period.projects.reduce((s, p) => s + p.totalCostUSD, 0) + const calls = period.projects.reduce((s, p) => s + p.totalApiCalls, 0) + const sessions = period.projects.reduce((s, p) => s + p.sessions.length, 0) + return { Period: period.label, 'Cost (USD)': Math.round(cost * 100) / 100, 'API Calls': calls, Sessions: sessions } +} + +export async function exportCsv(periods: PeriodExport[], outputPath: string): Promise { + const allProjects = periods.find(p => p.label === '30 Days')?.projects + ?? periods[periods.length - 1].projects + + const parts: string[] = [] + + parts.push('# Summary') + parts.push(rowsToCsv(periods.map(buildSummaryRow))) + parts.push('') + + for (const period of periods) { + parts.push(`# Daily - ${period.label}`) + parts.push(rowsToCsv(buildDailyRows(period.projects))) + parts.push('') + + parts.push(`# Activity - ${period.label}`) + parts.push(rowsToCsv(buildActivityRows(period.projects))) + parts.push('') + + parts.push(`# Models - ${period.label}`) + parts.push(rowsToCsv(buildModelRows(period.projects))) + parts.push('') + } + + parts.push('# Tools - All') + parts.push(rowsToCsv(buildToolRows(allProjects))) + parts.push('') + + parts.push('# Projects - All') + parts.push(rowsToCsv(buildProjectRows(allProjects))) + parts.push('') + + const fullPath = resolve(outputPath) + await writeFile(fullPath, parts.join('\n'), 'utf-8') + return fullPath +} + +export async function exportJson(periods: PeriodExport[], outputPath: string): Promise { + const allProjects = periods.find(p => p.label === '30 Days')?.projects + ?? periods[periods.length - 1].projects + + const periodData: Record = {} + for (const period of periods) { + periodData[period.label] = { + summary: buildSummaryRow(period), + daily: buildDailyRows(period.projects), + activity: buildActivityRows(period.projects), + models: buildModelRows(period.projects), + } + } + + const data = { + generated: new Date().toISOString(), + periods: periodData, + tools: buildToolRows(allProjects), + projects: buildProjectRows(allProjects), + } + + const fullPath = resolve(outputPath) + await writeFile(fullPath, JSON.stringify(data, null, 2), 'utf-8') + return fullPath +} diff --git a/src/format.ts b/src/format.ts new file mode 100644 index 0000000..201b798 --- /dev/null +++ b/src/format.ts @@ -0,0 +1,41 @@ +import chalk from 'chalk' +import type { ProjectSummary } from './types.js' + +export function formatCost(cost: number): string { + if (cost >= 1) return `$${cost.toFixed(2)}` + if (cost >= 0.01) return `$${cost.toFixed(3)}` + return `$${cost.toFixed(4)}` +} + +export function formatTokens(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` + return n.toString() +} + +export function renderStatusBar(projects: ProjectSummary[]): string { + const now = new Date() + const today = now.toISOString().slice(0, 10) + const monthStart = `${today.slice(0, 7)}-01` + + let todayCost = 0, todayCalls = 0, monthCost = 0, monthCalls = 0 + + for (const project of projects) { + for (const session of project.sessions) { + for (const turn of session.turns) { + if (!turn.timestamp) continue + const day = turn.timestamp.slice(0, 10) + const turnCost = turn.assistantCalls.reduce((s, c) => s + c.costUSD, 0) + const turnCalls = turn.assistantCalls.length + if (day === today) { todayCost += turnCost; todayCalls += turnCalls } + if (day >= monthStart) { monthCost += turnCost; monthCalls += turnCalls } + } + } + } + + const lines: string[] = [''] + lines.push(` ${chalk.bold('Today')} ${chalk.yellowBright(formatCost(todayCost))} ${chalk.dim(`${todayCalls} calls`)} ${chalk.bold('Month')} ${chalk.yellowBright(formatCost(monthCost))} ${chalk.dim(`${monthCalls} calls`)}`) + lines.push('') + + return lines.join('\n') +} diff --git a/src/menubar.ts b/src/menubar.ts new file mode 100644 index 0000000..fa88f84 --- /dev/null +++ b/src/menubar.ts @@ -0,0 +1,210 @@ +import { execSync } from 'child_process' +import { existsSync } from 'fs' +import { chmod, mkdir, unlink, writeFile } from 'fs/promises' +import { homedir, platform } from 'os' +import { join } from 'path' +import { formatCost, formatTokens } from './format.js' + +const PLUGIN_REFRESH = '5m' + +function getSwiftBarPluginDir(): string { + return join(homedir(), 'Library', 'Application Support', 'SwiftBar', 'plugins') +} + +function getXbarPluginDir(): string { + return join(homedir(), 'Library', 'Application Support', 'xbar', 'plugins') +} + +function getCodeburnBin(): string { + try { + return execSync('which codeburn', { encoding: 'utf-8' }).trim() + } catch { + return 'npx --yes codeburn' + } +} + +function generatePlugin(bin: string): string { + const home = homedir() + return `#!/bin/bash +# CodeBurn +# v0.1.0 +# AgentSeal +# agentseal +# See where your AI coding tokens burn. Tracks cost, activity, and model usage across Claude Code, Cursor, and Codex by task type, tool, MCP server, and project. +# file://${home}/codeburn/assets/logo.png +# https://github.com/agentseal/codeburn +# node + +export PATH="/usr/local/bin:/opt/homebrew/bin:$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" + +${bin} status --format menubar 2>/dev/null || echo "-- | sfimage=flame.fill" +` +} + +function miniBar(value: number, max: number, width: number = 10): string { + if (max === 0) return '·'.repeat(width) + const filled = Math.round((value / max) * width) + return '█'.repeat(Math.min(filled, width)) + '·'.repeat(Math.max(width - filled, 0)) +} + +export type PeriodData = { + label: string + cost: number + calls: number + inputTokens: number + outputTokens: number + cacheReadTokens: number + cacheWriteTokens: number + categories: Array<{ name: string; cost: number; turns: number }> + models: Array<{ name: string; cost: number; calls: number }> +} + +export function renderMenubarFormat( + today: PeriodData, + week: PeriodData, + month: PeriodData, +): string { + const lines: string[] = [] + + lines.push(`${formatCost(today.cost)} | sfimage=flame.fill color=#FF8C42`) + lines.push('---') + + lines.push(`CodeBurn | size=15 color=#FF8C42`) + lines.push(`AI Coding Cost Tracker | size=11`) + lines.push('---') + + lines.push(`Today ${formatCost(today.cost)} ${today.calls.toLocaleString()} calls | size=14`) + lines.push('---') + + const maxCat = Math.max(...today.categories.map(c => c.cost), 0.01) + lines.push(`Activity - Today | size=12 color=#FF8C42`) + for (const cat of today.categories.slice(0, 8)) { + const bar = miniBar(cat.cost, maxCat) + const name = cat.name.padEnd(14) + lines.push(`${bar} ${name} ${formatCost(cat.cost).padStart(8)} ${String(cat.turns).padStart(4)} turns | font=Menlo size=11`) + } + lines.push('---') + + const maxModel = Math.max(...today.models.filter(m => m.name !== '').map(m => m.cost), 0.01) + lines.push(`Models - Today | size=12 color=#FF8C42`) + for (const model of today.models.slice(0, 5)) { + if (model.name === '') continue + const bar = miniBar(model.cost, maxModel) + const name = model.name.padEnd(14) + lines.push(`${bar} ${name} ${formatCost(model.cost).padStart(8)} ${String(model.calls).padStart(5)} calls | font=Menlo size=11`) + } + + const cacheHit = today.inputTokens + today.cacheReadTokens > 0 + ? ((today.cacheReadTokens / (today.inputTokens + today.cacheReadTokens)) * 100).toFixed(0) + : '0' + lines.push(`Tokens: ${formatTokens(today.inputTokens)} in · ${formatTokens(today.outputTokens)} out · ${cacheHit}% cache hit | font=Menlo size=10`) + lines.push('---') + + lines.push(`7 Days ${formatCost(week.cost)} ${week.calls.toLocaleString()} calls | size=14`) + const weekMaxCat = Math.max(...week.categories.map(c => c.cost), 0.01) + const weekMaxModel = Math.max(...week.models.filter(m => m.name !== '').map(m => m.cost), 0.01) + lines.push(`--Activity | size=12 color=#FF8C42`) + for (const cat of week.categories.slice(0, 8)) { + const bar = miniBar(cat.cost, weekMaxCat) + const name = cat.name.padEnd(14) + lines.push(`--${bar} ${name} ${formatCost(cat.cost).padStart(8)} ${String(cat.turns).padStart(4)} turns | font=Menlo size=11`) + } + lines.push(`-----`) + lines.push(`--Models | size=12 color=#FF8C42`) + for (const model of week.models.slice(0, 5)) { + if (model.name === '') continue + const bar = miniBar(model.cost, weekMaxModel) + const name = model.name.padEnd(14) + lines.push(`--${bar} ${name} ${formatCost(model.cost).padStart(8)} ${String(model.calls).padStart(5)} calls | font=Menlo size=11`) + } + + lines.push(`Month ${formatCost(month.cost)} ${month.calls.toLocaleString()} calls | size=14`) + const monthMaxCat = Math.max(...month.categories.map(c => c.cost), 0.01) + const monthMaxModel = Math.max(...month.models.filter(m => m.name !== '').map(m => m.cost), 0.01) + lines.push(`--Activity | size=12 color=#FF8C42`) + for (const cat of month.categories.slice(0, 8)) { + const bar = miniBar(cat.cost, monthMaxCat) + const name = cat.name.padEnd(14) + lines.push(`--${bar} ${name} ${formatCost(cat.cost).padStart(8)} ${String(cat.turns).padStart(4)} turns | font=Menlo size=11`) + } + lines.push(`-----`) + lines.push(`--Models | size=12 color=#FF8C42`) + for (const model of month.models.slice(0, 5)) { + if (model.name === '') continue + const bar = miniBar(model.cost, monthMaxModel) + const name = model.name.padEnd(14) + lines.push(`--${bar} ${name} ${formatCost(model.cost).padStart(8)} ${String(model.calls).padStart(5)} calls | font=Menlo size=11`) + } + + lines.push('---') + const home = process.env.HOME ?? '~' + lines.push(`Open Full Report | terminal=true shell=/bin/bash param1=-c param2="cd '${home}/codeburn' && npx tsx src/cli.ts report; echo ''; echo 'Press any key to close...'; read -n1"`) + lines.push(`Export CSV to Desktop | terminal=false shell=/bin/bash param1=-c param2="cd '${home}/codeburn' && npx tsx src/cli.ts export -o '${home}/Desktop/codeburn-report.csv' 2>/dev/null"`) + lines.push(`Refresh | refresh=true`) + + return lines.join('\n') +} + +export async function installMenubar(): Promise { + if (platform() !== 'darwin') { + return 'Menu bar integration is only available on macOS. Use `codeburn watch` or `codeburn status` instead.' + } + + const bin = getCodeburnBin() + const pluginContent = generatePlugin(bin) + + let pluginDir: string + let appName: string + + if (existsSync(getSwiftBarPluginDir())) { + pluginDir = getSwiftBarPluginDir() + appName = 'SwiftBar' + } else if (existsSync(getXbarPluginDir())) { + pluginDir = getXbarPluginDir() + appName = 'xbar' + } else { + pluginDir = getSwiftBarPluginDir() + appName = 'SwiftBar' + await mkdir(pluginDir, { recursive: true }) + } + + const pluginPath = join(pluginDir, `codeburn.${PLUGIN_REFRESH}.sh`) + await writeFile(pluginPath, pluginContent, 'utf-8') + await chmod(pluginPath, 0o755) + + const swiftbarInstalled = existsSync('/Applications/SwiftBar.app') || existsSync(join(homedir(), 'Applications', 'SwiftBar.app')) + const xbarInstalled = existsSync('/Applications/xbar.app') || existsSync(join(homedir(), 'Applications', 'xbar.app')) + + const lines: string[] = [] + lines.push(`\n Plugin installed to: ${pluginPath}`) + + if (swiftbarInstalled || xbarInstalled) { + lines.push(` ${appName} detected - plugin should appear in your menu bar shortly.`) + lines.push(` If not, open ${appName} and refresh plugins.\n`) + } else { + lines.push(`\n To see CodeBurn in your menu bar, install SwiftBar:`) + lines.push(` brew install --cask swiftbar`) + lines.push(`\n Then launch SwiftBar - the plugin will load automatically.\n`) + } + + return lines.join('\n') +} + +export async function uninstallMenubar(): Promise { + const paths = [ + join(getSwiftBarPluginDir(), `codeburn.${PLUGIN_REFRESH}.sh`), + join(getXbarPluginDir(), `codeburn.${PLUGIN_REFRESH}.sh`), + ] + + let removed = false + for (const p of paths) { + if (existsSync(p)) { + await unlink(p) + removed = true + } + } + + return removed + ? '\n Menu bar plugin removed.\n' + : '\n No menu bar plugin found.\n' +} diff --git a/src/models.ts b/src/models.ts new file mode 100644 index 0000000..cf07ccf --- /dev/null +++ b/src/models.ts @@ -0,0 +1,178 @@ +import { readFile, writeFile, mkdir } from 'fs/promises' +import { join } from 'path' +import { homedir } from 'os' + +export type ModelCosts = { + inputCostPerToken: number + outputCostPerToken: number + cacheWriteCostPerToken: number + cacheReadCostPerToken: number + webSearchCostPerRequest: number + fastMultiplier: number +} + +type LiteLLMEntry = { + input_cost_per_token?: number + output_cost_per_token?: number + cache_creation_input_token_cost?: number + cache_read_input_token_cost?: number + provider_specific_entry?: { fast?: number } +} + +const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json' +const CACHE_TTL_MS = 24 * 60 * 60 * 1000 +const WEB_SEARCH_COST = 0.01 + +const FALLBACK_PRICING: Record = { + 'claude-opus-4-6': { inputCostPerToken: 5e-6, outputCostPerToken: 25e-6, cacheWriteCostPerToken: 6.25e-6, cacheReadCostPerToken: 0.5e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 6 }, + 'claude-opus-4-5': { inputCostPerToken: 5e-6, outputCostPerToken: 25e-6, cacheWriteCostPerToken: 6.25e-6, cacheReadCostPerToken: 0.5e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, + 'claude-opus-4-1': { inputCostPerToken: 15e-6, outputCostPerToken: 75e-6, cacheWriteCostPerToken: 18.75e-6, cacheReadCostPerToken: 1.5e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, + 'claude-opus-4': { inputCostPerToken: 15e-6, outputCostPerToken: 75e-6, cacheWriteCostPerToken: 18.75e-6, cacheReadCostPerToken: 1.5e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, + 'claude-sonnet-4-6': { inputCostPerToken: 3e-6, outputCostPerToken: 15e-6, cacheWriteCostPerToken: 3.75e-6, cacheReadCostPerToken: 0.3e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, + 'claude-sonnet-4-5': { inputCostPerToken: 3e-6, outputCostPerToken: 15e-6, cacheWriteCostPerToken: 3.75e-6, cacheReadCostPerToken: 0.3e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, + 'claude-sonnet-4': { inputCostPerToken: 3e-6, outputCostPerToken: 15e-6, cacheWriteCostPerToken: 3.75e-6, cacheReadCostPerToken: 0.3e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, + 'claude-3-7-sonnet': { inputCostPerToken: 3e-6, outputCostPerToken: 15e-6, cacheWriteCostPerToken: 3.75e-6, cacheReadCostPerToken: 0.3e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, + 'claude-3-5-sonnet': { inputCostPerToken: 3e-6, outputCostPerToken: 15e-6, cacheWriteCostPerToken: 3.75e-6, cacheReadCostPerToken: 0.3e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, + 'claude-haiku-4-5': { inputCostPerToken: 1e-6, outputCostPerToken: 5e-6, cacheWriteCostPerToken: 1.25e-6, cacheReadCostPerToken: 0.1e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, + 'claude-3-5-haiku': { inputCostPerToken: 0.8e-6, outputCostPerToken: 4e-6, cacheWriteCostPerToken: 1e-6, cacheReadCostPerToken: 0.08e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, + 'gpt-4o': { inputCostPerToken: 2.5e-6, outputCostPerToken: 10e-6, cacheWriteCostPerToken: 2.5e-6, cacheReadCostPerToken: 1.25e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, + 'gpt-4o-mini': { inputCostPerToken: 0.15e-6, outputCostPerToken: 0.6e-6, cacheWriteCostPerToken: 0.15e-6, cacheReadCostPerToken: 0.075e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, + 'gemini-2.5-pro': { inputCostPerToken: 1.25e-6, outputCostPerToken: 10e-6, cacheWriteCostPerToken: 1.25e-6, cacheReadCostPerToken: 0.315e-6, webSearchCostPerRequest: WEB_SEARCH_COST, fastMultiplier: 1 }, +} + +let pricingCache: Map | null = null + +function getCacheDir(): string { + return join(homedir(), '.cache', 'codeburn') +} + +function getCachePath(): string { + return join(getCacheDir(), 'litellm-pricing.json') +} + +function parseLiteLLMEntry(entry: LiteLLMEntry): ModelCosts | null { + if (!entry.input_cost_per_token || !entry.output_cost_per_token) return null + return { + inputCostPerToken: entry.input_cost_per_token, + outputCostPerToken: entry.output_cost_per_token, + cacheWriteCostPerToken: entry.cache_creation_input_token_cost ?? entry.input_cost_per_token * 1.25, + cacheReadCostPerToken: entry.cache_read_input_token_cost ?? entry.input_cost_per_token * 0.1, + webSearchCostPerRequest: WEB_SEARCH_COST, + fastMultiplier: entry.provider_specific_entry?.fast ?? 1, + } +} + +async function fetchAndCachePricing(): Promise> { + const response = await fetch(LITELLM_URL) + if (!response.ok) throw new Error(`HTTP ${response.status}`) + const data = await response.json() as Record + const pricing = new Map() + + for (const [name, entry] of Object.entries(data)) { + if (name.includes('/') || name.includes('.')) continue + const costs = parseLiteLLMEntry(entry) + if (costs) pricing.set(name, costs) + } + + await mkdir(getCacheDir(), { recursive: true }) + await writeFile(getCachePath(), JSON.stringify({ + timestamp: Date.now(), + data: Object.fromEntries(pricing), + })) + + return pricing +} + +async function loadCachedPricing(): Promise | null> { + try { + const raw = await readFile(getCachePath(), 'utf-8') + const cached = JSON.parse(raw) as { timestamp: number; data: Record } + if (Date.now() - cached.timestamp > CACHE_TTL_MS) return null + return new Map(Object.entries(cached.data)) + } catch { + return null + } +} + +export async function loadPricing(): Promise { + const cached = await loadCachedPricing() + if (cached) { + pricingCache = cached + return + } + + try { + pricingCache = await fetchAndCachePricing() + } catch { + pricingCache = new Map(Object.entries(FALLBACK_PRICING)) + } +} + +function getCanonicalName(model: string): string { + return model + .replace(/@.*$/, '') + .replace(/-\d{8}$/, '') +} + +export function getModelCosts(model: string): ModelCosts | null { + const canonical = getCanonicalName(model) + + if (pricingCache?.has(canonical)) return pricingCache.get(canonical)! + + for (const [key, costs] of pricingCache ?? new Map()) { + if (canonical.startsWith(key) || key.startsWith(canonical)) return costs + } + + for (const [key, costs] of Object.entries(FALLBACK_PRICING)) { + if (canonical.startsWith(key)) return costs + } + + return null +} + +export function calculateCost( + model: string, + inputTokens: number, + outputTokens: number, + cacheCreationTokens: number, + cacheReadTokens: number, + webSearchRequests: number, + speed: 'standard' | 'fast' = 'standard', +): number { + const costs = getModelCosts(model) + if (!costs) return 0 + + const multiplier = speed === 'fast' ? costs.fastMultiplier : 1 + + return multiplier * ( + inputTokens * costs.inputCostPerToken + + outputTokens * costs.outputCostPerToken + + cacheCreationTokens * costs.cacheWriteCostPerToken + + cacheReadTokens * costs.cacheReadCostPerToken + + webSearchRequests * costs.webSearchCostPerRequest + ) +} + +export function getShortModelName(model: string): string { + const canonical = getCanonicalName(model) + const shortNames: Record = { + 'claude-opus-4-6': 'Opus 4.6', + 'claude-opus-4-5': 'Opus 4.5', + 'claude-opus-4-1': 'Opus 4.1', + 'claude-opus-4': 'Opus 4', + 'claude-sonnet-4-6': 'Sonnet 4.6', + 'claude-sonnet-4-5': 'Sonnet 4.5', + 'claude-sonnet-4': 'Sonnet 4', + 'claude-3-7-sonnet': 'Sonnet 3.7', + 'claude-3-5-sonnet': 'Sonnet 3.5', + 'claude-haiku-4-5': 'Haiku 4.5', + 'claude-3-5-haiku': 'Haiku 3.5', + 'gpt-4o': 'GPT-4o', + 'gpt-4o-mini': 'GPT-4o Mini', + 'gemini-2.5-pro': 'Gemini 2.5 Pro', + } + for (const [key, name] of Object.entries(shortNames)) { + if (canonical.startsWith(key)) return name + } + return canonical +} diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..609e728 --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,314 @@ +import { readdir, readFile, stat } from 'fs/promises' +import { basename, join } from 'path' +import { homedir } from 'os' +import { calculateCost, getShortModelName } from './models.js' +import type { + AssistantMessageContent, + ClassifiedTurn, + ContentBlock, + DateRange, + JournalEntry, + ParsedApiCall, + ParsedTurn, + ProjectSummary, + SessionSummary, + TokenUsage, + ToolUseBlock, +} from './types.js' +import { classifyTurn } from './classifier.js' + +function getClaudeDir(): string { + return join(homedir(), '.claude') +} + +function getProjectsDir(): string { + return join(getClaudeDir(), 'projects') +} + +function unsanitizePath(dirName: string): string { + return dirName.replace(/-/g, '/') +} + +function parseJsonlLine(line: string): JournalEntry | null { + try { + return JSON.parse(line) as JournalEntry + } catch { + return null + } +} + +function extractToolNames(content: ContentBlock[]): string[] { + return content + .filter((b): b is ToolUseBlock => b.type === 'tool_use') + .map(b => b.name) +} + +function extractMcpTools(tools: string[]): string[] { + return tools.filter(t => t.startsWith('mcp__')) +} + +function extractCoreTools(tools: string[]): string[] { + return tools.filter(t => !t.startsWith('mcp__')) +} + +function getUserMessageText(entry: JournalEntry): string { + if (!entry.message || entry.message.role !== 'user') return '' + const content = entry.message.content + if (typeof content === 'string') return content + if (Array.isArray(content)) { + return content + .filter((b): b is { type: 'text'; text: string } => b.type === 'text') + .map(b => b.text) + .join(' ') + } + return '' +} + +function getMessageId(entry: JournalEntry): string | null { + if (entry.type !== 'assistant') return null + const msg = entry.message as AssistantMessageContent | undefined + return msg?.id ?? null +} + +function parseApiCall(entry: JournalEntry): ParsedApiCall | null { + if (entry.type !== 'assistant') return null + const msg = entry.message as AssistantMessageContent | undefined + if (!msg?.usage || !msg?.model) return null + + const usage = msg.usage + const tokens: TokenUsage = { + inputTokens: usage.input_tokens ?? 0, + outputTokens: usage.output_tokens ?? 0, + cacheCreationInputTokens: usage.cache_creation_input_tokens ?? 0, + cacheReadInputTokens: usage.cache_read_input_tokens ?? 0, + webSearchRequests: usage.server_tool_use?.web_search_requests ?? 0, + } + + const tools = extractToolNames(msg.content ?? []) + const costUSD = calculateCost( + msg.model, + tokens.inputTokens, + tokens.outputTokens, + tokens.cacheCreationInputTokens, + tokens.cacheReadInputTokens, + tokens.webSearchRequests, + usage.speed ?? 'standard', + ) + + return { + model: msg.model, + usage: tokens, + costUSD, + tools, + mcpTools: extractMcpTools(tools), + hasAgentSpawn: tools.includes('Agent'), + hasPlanMode: tools.includes('EnterPlanMode'), + speed: usage.speed ?? 'standard', + timestamp: entry.timestamp ?? '', + } +} + +function groupIntoTurns(entries: JournalEntry[], seenMsgIds: Set): ParsedTurn[] { + const turns: ParsedTurn[] = [] + let currentUserMessage = '' + let currentCalls: ParsedApiCall[] = [] + let currentTimestamp = '' + let currentSessionId = '' + + for (const entry of entries) { + if (entry.type === 'user') { + if (currentCalls.length > 0) { + turns.push({ + userMessage: currentUserMessage, + assistantCalls: currentCalls, + timestamp: currentTimestamp, + sessionId: currentSessionId, + }) + } + currentUserMessage = getUserMessageText(entry) + currentCalls = [] + currentTimestamp = entry.timestamp ?? '' + currentSessionId = entry.sessionId ?? '' + } else if (entry.type === 'assistant') { + const msgId = getMessageId(entry) + if (msgId && seenMsgIds.has(msgId)) continue + if (msgId) seenMsgIds.add(msgId) + const call = parseApiCall(entry) + if (call) currentCalls.push(call) + } + } + + if (currentCalls.length > 0) { + turns.push({ + userMessage: currentUserMessage, + assistantCalls: currentCalls, + timestamp: currentTimestamp, + sessionId: currentSessionId, + }) + } + + return turns +} + +function buildSessionSummary( + sessionId: string, + project: string, + turns: ClassifiedTurn[], +): SessionSummary { + const modelBreakdown: SessionSummary['modelBreakdown'] = {} + const toolBreakdown: SessionSummary['toolBreakdown'] = {} + const mcpBreakdown: SessionSummary['mcpBreakdown'] = {} + const categoryBreakdown: SessionSummary['categoryBreakdown'] = {} as SessionSummary['categoryBreakdown'] + + let totalCost = 0 + let totalInput = 0 + let totalOutput = 0 + let totalCacheRead = 0 + let totalCacheWrite = 0 + let apiCalls = 0 + let firstTs = '' + let lastTs = '' + + for (const turn of turns) { + const turnCost = turn.assistantCalls.reduce((s, c) => s + c.costUSD, 0) + + if (!categoryBreakdown[turn.category]) { + categoryBreakdown[turn.category] = { turns: 0, costUSD: 0 } + } + categoryBreakdown[turn.category].turns++ + categoryBreakdown[turn.category].costUSD += turnCost + + for (const call of turn.assistantCalls) { + totalCost += call.costUSD + totalInput += call.usage.inputTokens + totalOutput += call.usage.outputTokens + totalCacheRead += call.usage.cacheReadInputTokens + totalCacheWrite += call.usage.cacheCreationInputTokens + apiCalls++ + + const modelKey = getShortModelName(call.model) + if (!modelBreakdown[modelKey]) { + modelBreakdown[modelKey] = { + calls: 0, + costUSD: 0, + tokens: { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0, webSearchRequests: 0 }, + } + } + modelBreakdown[modelKey].calls++ + modelBreakdown[modelKey].costUSD += call.costUSD + modelBreakdown[modelKey].tokens.inputTokens += call.usage.inputTokens + modelBreakdown[modelKey].tokens.outputTokens += call.usage.outputTokens + modelBreakdown[modelKey].tokens.cacheReadInputTokens += call.usage.cacheReadInputTokens + modelBreakdown[modelKey].tokens.cacheCreationInputTokens += call.usage.cacheCreationInputTokens + + for (const tool of extractCoreTools(call.tools)) { + toolBreakdown[tool] = toolBreakdown[tool] ?? { calls: 0 } + toolBreakdown[tool].calls++ + } + for (const mcp of call.mcpTools) { + const server = mcp.split('__')[1] ?? mcp + mcpBreakdown[server] = mcpBreakdown[server] ?? { calls: 0 } + mcpBreakdown[server].calls++ + } + + if (!firstTs || call.timestamp < firstTs) firstTs = call.timestamp + if (!lastTs || call.timestamp > lastTs) lastTs = call.timestamp + } + } + + return { + sessionId, + project, + firstTimestamp: firstTs || turns[0]?.timestamp || '', + lastTimestamp: lastTs || turns[turns.length - 1]?.timestamp || '', + totalCostUSD: totalCost, + totalInputTokens: totalInput, + totalOutputTokens: totalOutput, + totalCacheReadTokens: totalCacheRead, + totalCacheWriteTokens: totalCacheWrite, + apiCalls, + turns, + modelBreakdown, + toolBreakdown, + mcpBreakdown, + categoryBreakdown, + } +} + +async function parseSessionFile( + filePath: string, + project: string, + seenMsgIds: Set, + dateRange?: DateRange, +): Promise { + const content = await readFile(filePath, 'utf-8') + const lines = content.split('\n').filter(l => l.trim()) + const entries: JournalEntry[] = [] + + for (const line of lines) { + const entry = parseJsonlLine(line) + if (entry) entries.push(entry) + } + + if (entries.length === 0) return null + + let filteredEntries = entries + if (dateRange) { + filteredEntries = entries.filter(e => { + if (!e.timestamp) return e.type === 'user' + const ts = new Date(e.timestamp) + return ts >= dateRange.start && ts <= dateRange.end + }) + if (filteredEntries.length === 0) return null + } + + const sessionId = basename(filePath, '.jsonl') + const turns = groupIntoTurns(filteredEntries, seenMsgIds) + const classified = turns.map(classifyTurn) + + return buildSessionSummary(sessionId, project, classified) +} + +export async function parseAllSessions(dateRange?: DateRange): Promise { + const projectsDir = getProjectsDir() + const projects: ProjectSummary[] = [] + const seenMsgIds = new Set() + + let projectDirs: string[] + try { + projectDirs = await readdir(projectsDir) + } catch { + return [] + } + + for (const dirName of projectDirs) { + const dirPath = join(projectsDir, dirName) + const dirStat = await stat(dirPath).catch(() => null) + if (!dirStat?.isDirectory()) continue + + const files = await readdir(dirPath).catch(() => []) + const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-')) + + const sessions: SessionSummary[] = [] + for (const file of jsonlFiles) { + const session = await parseSessionFile(join(dirPath, file), dirName, seenMsgIds, dateRange) + if (session && session.apiCalls > 0) { + sessions.push(session) + } + } + + if (sessions.length > 0) { + const totalCost = sessions.reduce((s, sess) => s + sess.totalCostUSD, 0) + const totalCalls = sessions.reduce((s, sess) => s + sess.apiCalls, 0) + projects.push({ + project: dirName, + projectPath: unsanitizePath(dirName), + sessions, + totalCostUSD: totalCost, + totalApiCalls: totalCalls, + }) + } + } + + return projects.sort((a, b) => b.totalCostUSD - a.totalCostUSD) +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..2d64197 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,142 @@ +export type TokenUsage = { + inputTokens: number + outputTokens: number + cacheCreationInputTokens: number + cacheReadInputTokens: number + webSearchRequests: number +} + +export type ToolUseBlock = { + type: 'tool_use' + id: string + name: string + input: Record +} + +export type ContentBlock = + | { type: 'text'; text: string } + | { type: 'thinking'; thinking: string } + | ToolUseBlock + | { type: string; [key: string]: unknown } + +export type ApiUsage = { + input_tokens: number + output_tokens: number + cache_creation_input_tokens?: number + cache_read_input_tokens?: number + server_tool_use?: { + web_search_requests?: number + web_fetch_requests?: number + } + speed?: 'standard' | 'fast' +} + +export type AssistantMessageContent = { + model: string + id?: string + type: 'message' + role: 'assistant' + content: ContentBlock[] + usage: ApiUsage + stop_reason?: string +} + +export type JournalEntry = { + type: string + uuid?: string + parentUuid?: string | null + timestamp?: string + sessionId?: string + cwd?: string + version?: string + gitBranch?: string + promptId?: string + message?: AssistantMessageContent | { role: 'user'; content: string | ContentBlock[] } + isSidechain?: boolean + [key: string]: unknown +} + +export type ParsedTurn = { + userMessage: string + assistantCalls: ParsedApiCall[] + timestamp: string + sessionId: string +} + +export type ParsedApiCall = { + model: string + usage: TokenUsage + costUSD: number + tools: string[] + mcpTools: string[] + hasAgentSpawn: boolean + hasPlanMode: boolean + speed: 'standard' | 'fast' + timestamp: string +} + +export type TaskCategory = + | 'coding' + | 'debugging' + | 'feature' + | 'refactoring' + | 'testing' + | 'exploration' + | 'planning' + | 'delegation' + | 'git' + | 'build/deploy' + | 'conversation' + | 'brainstorming' + | 'general' + +export type ClassifiedTurn = ParsedTurn & { + category: TaskCategory +} + +export type SessionSummary = { + sessionId: string + project: string + firstTimestamp: string + lastTimestamp: string + totalCostUSD: number + totalInputTokens: number + totalOutputTokens: number + totalCacheReadTokens: number + totalCacheWriteTokens: number + apiCalls: number + turns: ClassifiedTurn[] + modelBreakdown: Record + toolBreakdown: Record + mcpBreakdown: Record + categoryBreakdown: Record +} + +export type ProjectSummary = { + project: string + projectPath: string + sessions: SessionSummary[] + totalCostUSD: number + totalApiCalls: number +} + +export type DateRange = { + start: Date + end: Date +} + +export const CATEGORY_LABELS: Record = { + coding: 'Coding', + debugging: 'Debugging', + feature: 'Feature Dev', + refactoring: 'Refactoring', + testing: 'Testing', + exploration: 'Exploration', + planning: 'Planning', + delegation: 'Delegation', + git: 'Git Ops', + 'build/deploy': 'Build/Deploy', + conversation: 'Conversation', + brainstorming: 'Brainstorming', + general: 'General', +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e153610 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "sourceMap": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..6d12ab9 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/cli.ts'], + format: ['esm'], + target: 'node18', + outDir: 'dist', + clean: true, + splitting: false, + sourcemap: true, + dts: false, + banner: { + js: '#!/usr/bin/env node', + }, +})